Compare commits

..

14 Commits

Author SHA1 Message Date
Gunnar Skjold
e81ef7a93b Trying to fix tariff on wrong date. Also some code cleanup 2026-03-05 13:14:48 +01:00
Gunnar Skjold
0b4884652f Allow for more errors during upgrade (#1139)
* Allow for more errors during upgrade

* More instead of equals
2026-02-12 12:28:31 +01:00
Gunnar Skjold
82aeae8699 Fixed compile error for 8266 after #1121 (#1138) 2026-02-12 09:45:04 +01:00
Gunnar Skjold
a7333653b0 Fixed decimal accuracy on saved values (#1133) 2026-02-12 08:25:32 +01:00
Gunnar Skjold
24e63d5e32 Fixed HA object id (#1132) 2026-02-12 08:25:17 +01:00
Gunnar Skjold
eb7c266378 Fixed double slash on Wiki links (#1123) 2026-02-12 08:25:04 +01:00
Gunnar Skjold
cf8c48ab99 Added code to ensure stable boot (#1121)
* If BUS powered, wait for capacitor to charge on boot, this ensures better boot stability

* Some cleanup
2026-02-12 08:24:50 +01:00
Gunnar Skjold
78a1cd78ea Added support for a new format for a Iskra meter in Switzerland (#1118) 2026-02-12 08:24:26 +01:00
Gunnar Skjold
fdfa6c1b52 Fixed MQTT JSON for prices (#1116) 2026-01-01 20:07:48 +01:00
Gunnar Skjold
4f1790a464 Added support for Iskraemeco IE.5 in Croatia (#1107)
* Added support for Croation Iskra

* Temp removed meterid

* Fixed HDLC block decoding

* Fixed context length

* Changing some stuff back

* Change some stuff back

* Final test

* Added debugging

* Updated selector for iskra dataformat

* Added fake test frame
2025-12-30 10:12:31 +01:00
Gunnar Skjold
ca4cef5233 Fixed empty timestamp in Home-Assistant JSON (#1105)
* Nullable timestamps for HA JSON

* Nullable timestamps for MQTT JSON
2025-12-30 10:05:39 +01:00
Gunnar Skjold
a0d7fd0d95 Fixed extraction of negative prices from server (#1104) 2025-12-30 10:04:55 +01:00
Gunnar Skjold
489dbf9254 Fixed IPv6 formatting (#1106) 2025-12-30 10:04:30 +01:00
Gunnar Skjold
a81aa11558 Added support for frames without checksum (#1108) 2025-12-29 13:25:36 +01:00
34 changed files with 594 additions and 287 deletions

48
frames/iskra_croatia.txt Normal file
View File

@@ -0,0 +1,48 @@
They actually use multiple frames, so this is a "fake" frame combining the two into one, but without checksum fields.
7E
A0 BD
CF 02 23 03 00 00
E6 E7 00
0F 00 03 46 3B
0C 07 E9 0C 13 05 17 37 28 00 FF C4 00
02 21
09 08 39 32 30 32 39 36 39 31
09 04 17 37 28 00
09 05 07 E9 0C 13 05
06 00 6C 28 5A
06 00 4B 76 1A
06 00 20 B2 40
06 00 58 68 AA
06 00 57 A1 62
06 00 00 C7 48
06 00 17 EE D7
06 00 12 F5 5C
06 00 00 D9 6A
06 00 15 36 84
06 00 00 01 7E
06 00 00 00 00
12 03 79
06 00 00 00 7F
06 00 00 00 BD
06 00 00 00 41
06 00 00 00 00
06 00 00 00 00
06 00 00 00 00
12 09 54
12 09 35
12 09 49
12 00 37
12 00 59
12 00 4D
06 00 00 43 62
01 01
12 24 B8
01 01
12 24 B8
01 01
12 24 B8
03 01
00 00 7E

View File

@@ -591,7 +591,6 @@ void AmsConfiguration::clearGpio(GpioConfig& config, bool all) {
config.tempAnalogSensorPin = 0xFF;
config.vccPin = 0xFF;
config.ledDisablePin = 0xFF;
config.powersaving = 0;
if(all) {
config.vccOffset = 0;
@@ -600,6 +599,7 @@ void AmsConfiguration::clearGpio(GpioConfig& config, bool all) {
config.vccResistorGnd = 0;
config.vccResistorVcc = 0;
config.ledBehaviour = LED_BEHAVIOUR_DEFAULT;
config.powersaving = 0;
}
}

View File

@@ -7,8 +7,7 @@
#ifndef _AMSDATA_H
#define _AMSDATA_H
#include "Arduino.h"
#include <Timezone.h>
#include <WString.h>
#include "OBIScodes.h"
enum AmsType {
@@ -28,7 +27,7 @@ public:
AmsData();
void apply(AmsData& other);
void apply(const OBIS_code_t obis, double value);
void apply(const OBIS_code_t obis, double value, uint64_t millis64);
uint64_t getLastUpdateMillis();

View File

@@ -5,6 +5,7 @@
*/
#include "AmsData.h"
#include <algorithm>
AmsData::AmsData() {}
@@ -17,7 +18,6 @@ void AmsData::apply(AmsData& other) {
uint32_t power = (activeImportPower + other.getActiveImportPower()) / 2;
float add = power * (((float) ms) / 3600000.0);
activeImportCounter += add / 1000.0;
//Serial.printf("%dW, %dms, %.6fkWh added\n", other.getActiveImportPower(), ms, add);
}
if(other.getListType() > 1) {
@@ -112,7 +112,7 @@ void AmsData::apply(AmsData& other) {
this->activeExportPower = other.getActiveExportPower();
}
void AmsData::apply(OBIS_code_t obis, double value) {
void AmsData::apply(OBIS_code_t obis, double value, uint64_t millis64) {
if(obis.sensor == 0 && obis.gr == 0 && obis.tariff == 0) {
meterType = value;
}
@@ -127,138 +127,137 @@ void AmsData::apply(OBIS_code_t obis, double value) {
}
}
if(obis.tariff != 0) {
Serial.println("Tariff not implemented");
return;
}
if(obis.gr == 7) { // Instant values
switch(obis.sensor) {
case 1:
activeImportPower = value;
listType = max(listType, (uint8_t) 2);
listType = std::max(listType, (uint8_t) 2);
break;
case 2:
activeExportPower = value;
listType = max(listType, (uint8_t) 2);
listType = std::max(listType, (uint8_t) 2);
break;
case 3:
reactiveImportPower = value;
listType = max(listType, (uint8_t) 2);
listType = std::max(listType, (uint8_t) 2);
break;
case 4:
reactiveExportPower = value;
listType = max(listType, (uint8_t) 2);
listType = std::max(listType, (uint8_t) 2);
break;
case 13:
powerFactor = value;
listType = max(listType, (uint8_t) 4);
listType = std::max(listType, (uint8_t) 4);
break;
case 21:
l1activeImportPower = value;
listType = max(listType, (uint8_t) 4);
listType = std::max(listType, (uint8_t) 4);
break;
case 22:
l1activeExportPower = value;
listType = max(listType, (uint8_t) 4);
listType = std::max(listType, (uint8_t) 4);
break;
case 31:
l1current = value;
listType = max(listType, (uint8_t) 2);
listType = std::max(listType, (uint8_t) 2);
break;
case 32:
l1voltage = value;
listType = max(listType, (uint8_t) 2);
listType = std::max(listType, (uint8_t) 2);
break;
case 33:
l1PowerFactor = value;
listType = max(listType, (uint8_t) 4);
listType = std::max(listType, (uint8_t) 4);
break;
case 41:
l2activeImportPower = value;
listType = max(listType, (uint8_t) 4);
listType = std::max(listType, (uint8_t) 4);
break;
case 42:
l2activeExportPower = value;
listType = max(listType, (uint8_t) 4);
listType = std::max(listType, (uint8_t) 4);
break;
case 51:
l2current = value;
listType = max(listType, (uint8_t) 2);
listType = std::max(listType, (uint8_t) 2);
break;
case 52:
l2voltage = value;
listType = max(listType, (uint8_t) 2);
listType = std::max(listType, (uint8_t) 2);
break;
case 53:
l2PowerFactor = value;
listType = max(listType, (uint8_t) 4);
listType = std::max(listType, (uint8_t) 4);
break;
case 61:
l3activeImportPower = value;
listType = max(listType, (uint8_t) 4);
listType = std::max(listType, (uint8_t) 4);
break;
case 62:
l3activeExportPower = value;
listType = max(listType, (uint8_t) 4);
listType = std::max(listType, (uint8_t) 4);
break;
case 71:
l3current = value;
listType = max(listType, (uint8_t) 2);
listType = std::max(listType, (uint8_t) 2);
break;
case 72:
l3voltage = value;
listType = max(listType, (uint8_t) 2);
listType = std::max(listType, (uint8_t) 2);
break;
case 73:
l3PowerFactor = value;
listType = max(listType, (uint8_t) 4);
listType = std::max(listType, (uint8_t) 4);
break;
}
} else if(obis.gr == 8) { // Accumulated values
switch(obis.sensor) {
case 1:
activeImportCounter = value;
listType = max(listType, (uint8_t) 3);
listType = std::max(listType, (uint8_t) 3);
break;
case 2:
activeExportCounter = value;
listType = max(listType, (uint8_t) 3);
listType = std::max(listType, (uint8_t) 3);
break;
case 3:
reactiveImportCounter = value;
listType = max(listType, (uint8_t) 3);
listType = std::max(listType, (uint8_t) 3);
break;
case 4:
reactiveExportCounter = value;
listType = max(listType, (uint8_t) 3);
listType = std::max(listType, (uint8_t) 3);
break;
case 21:
l1activeImportCounter = value;
listType = max(listType, (uint8_t) 4);
listType = std::max(listType, (uint8_t) 4);
break;
case 22:
l1activeExportCounter = value;
listType = max(listType, (uint8_t) 4);
listType = std::max(listType, (uint8_t) 4);
break;
case 41:
l2activeImportCounter = value;
listType = max(listType, (uint8_t) 4);
listType = std::max(listType, (uint8_t) 4);
break;
case 42:
l2activeExportCounter = value;
listType = max(listType, (uint8_t) 4);
listType = std::max(listType, (uint8_t) 4);
break;
case 61:
l3activeImportCounter = value;
listType = max(listType, (uint8_t) 4);
listType = std::max(listType, (uint8_t) 4);
break;
case 62:
l3activeExportCounter = value;
listType = max(listType, (uint8_t) 4);
listType = std::max(listType, (uint8_t) 4);
break;
}
}
if(listType > 0)
lastUpdateMillis = millis();
lastUpdateMillis = millis64;
threePhase = l1voltage > 0 && l2voltage > 0 && l3voltage > 0;
if(!threePhase)

View File

@@ -74,7 +74,7 @@ int8_t DSMRParser::parse(uint8_t *buf, DataParserContext &ctx, bool verified, Pr
fromHex((uint8_t*) &crc, String((char*) buf+crcPos), 2);
crc = ntohs(crc);
if(crc != crc_calc) {
if(crc > 0 && crc != crc_calc) {
if(debugger != NULL) {
debugger->printf_P(PSTR("CRC incorrrect, %04X != %04X at position %lu\n"), crc, crc_calc, crcPos);
}

View File

@@ -32,7 +32,7 @@ int8_t HDLCParser::parse(uint8_t *d, DataParserContext &ctx) {
return DATA_PARSE_BOUNDARY_FLAG_MISSING;
// Verify FCS
if(ntohs(f->fcs) != crc16_x25(d + 1, len - sizeof *f - 1))
if(f->fcs > 0 && ntohs(f->fcs) != crc16_x25(d + 1, len - sizeof *f - 1))
return DATA_PARSE_FOOTER_CHECKSUM_ERROR;
// Skip destination address, LSB marks last byte
@@ -50,7 +50,7 @@ int8_t HDLCParser::parse(uint8_t *d, DataParserContext &ctx) {
HDLC3CtrlHcs* t3 = (HDLC3CtrlHcs*) (ptr);
// Verify HCS
if(ntohs(t3->hcs) != crc16_x25(d + 1, ptr-d))
if(t3->hcs > 0 && ntohs(t3->hcs) != crc16_x25(d + 1, ptr-d))
return DATA_PARSE_HEADER_CHECKSUM_ERROR;
ptr += 3;
@@ -69,7 +69,12 @@ int8_t HDLCParser::parse(uint8_t *d, DataParserContext &ctx) {
if(buf == NULL) return DATA_PARSE_FAIL;
memcpy(buf + pos, ptr+3, ctx.length); // +3 to skip LLC
if((*ptr) == DATA_TAG_LLC) {
ptr += 3; // Skip LLC
ctx.length -= 3;
}
memcpy(buf + pos, ptr, ctx.length);
pos += ctx.length;
lastSequenceNumber++;
@@ -78,7 +83,12 @@ int8_t HDLCParser::parse(uint8_t *d, DataParserContext &ctx) {
lastSequenceNumber = 0;
if(buf == NULL) return DATA_PARSE_FAIL;
memcpy(buf + pos, ptr+3, ctx.length); // +3 to skip LLC
if((*ptr) == DATA_TAG_LLC) {
ptr += 3; // Skip LLC
ctx.length -= 3;
}
memcpy(buf + pos, ptr, ctx.length);
pos += ctx.length;
memcpy((uint8_t *) d, buf, pos);

View File

@@ -39,6 +39,8 @@
#define AMS_UPDATE_ERR_SUCCESS_CONFIRMED 123
#define UPDATE_BUF_SIZE 4096
#define UPDATE_MAX_BLOCK_RETRY 25
#define UPDATE_MAX_REBOOT_RETRY 12
class AmsFirmwareUpdater {
public:

View File

@@ -74,7 +74,7 @@ void AmsFirmwareUpdater::setUpgradeInformation(UpgradeInformation& upinfo) {
#endif
debugger->printf_P(PSTR("Resuming uprade to %s\n"), updateStatus.toVersion);
if(updateStatus.reboot_count++ < 8) {
if(updateStatus.reboot_count++ < UPDATE_MAX_REBOOT_RETRY) {
updateStatus.errorCode = AMS_UPDATE_ERR_OK;
} else {
updateStatus.errorCode = AMS_UPDATE_ERR_REBOOT;
@@ -129,7 +129,7 @@ void AmsFirmwareUpdater::loop() {
HTTPClient http;
start = millis();
if(!fetchFirmwareChunk(http)) {
if(updateStatus.retry_count++ == 3) {
if(updateStatus.retry_count++ > UPDATE_MAX_BLOCK_RETRY) {
updateStatus.errorCode = AMS_UPDATE_ERR_FETCH;
updateStatusChanged = true;
}

View File

@@ -7,8 +7,6 @@
#ifndef _ENERGYACCOUNTING_H
#define _ENERGYACCOUNTING_H
#include "Arduino.h"
#include "AmsData.h"
#include "AmsDataStorage.h"
#include "PriceService.h"
@@ -83,7 +81,7 @@ public:
void setPriceService(PriceService *ps);
void setTimezone(Timezone*);
EnergyAccountingConfig* getConfig();
bool update(AmsData* amsData);
bool update(time_t now, uint64_t lastUpdatedMillis, uint8_t listType, uint32_t activeImportPower, uint32_t activeExportPower);
bool load();
bool save();
bool isInitialized();

View File

@@ -54,9 +54,8 @@ bool EnergyAccounting::isInitialized() {
return this->init;
}
bool EnergyAccounting::update(AmsData* amsData) {
bool EnergyAccounting::update(time_t now, uint64_t lastUpdatedMillis, uint8_t listType, uint32_t activeImportPower, uint32_t activeExportPower) {
if(config == NULL) return false;
time_t now = time(nullptr);
if(now < FirmwareVersion::BuildEpoch) return false;
if(tz == NULL) {
return false;
@@ -90,7 +89,7 @@ bool EnergyAccounting::update(AmsData* amsData) {
calcDayCost();
}
if(local.Hour != realtimeData->currentHour && (amsData->getListType() >= 3 || local.Minute == 1)) {
if(local.Hour != realtimeData->currentHour && (listType >= 3 || local.Minute == 1)) {
tmElements_t oneHrAgo, oneHrAgoLocal;
breakTime(now-3600, oneHrAgo);
uint16_t val = round(ds->getHourImport(oneHrAgo.Hour) / 10.0);
@@ -156,9 +155,9 @@ bool EnergyAccounting::update(AmsData* amsData) {
}
}
if(realtimeData->lastImportUpdateMillis < amsData->getLastUpdateMillis()) {
unsigned long ms = amsData->getLastUpdateMillis() - realtimeData->lastImportUpdateMillis;
float kwhi = (amsData->getActiveImportPower() * (((float) ms) / 3600000.0)) / 1000.0;
if(realtimeData->lastImportUpdateMillis < lastUpdatedMillis) {
unsigned long ms = lastUpdatedMillis - realtimeData->lastImportUpdateMillis;
float kwhi = (activeImportPower * (((float) ms) / 3600000.0)) / 1000.0;
if(kwhi > 0) {
realtimeData->use += kwhi;
float importPrice = ps == NULL ? PRICE_NO_VALUE : ps->getCurrentPrice(PRICE_DIRECTION_IMPORT);
@@ -168,12 +167,12 @@ bool EnergyAccounting::update(AmsData* amsData) {
realtimeData->costDay += cost;
}
}
realtimeData->lastImportUpdateMillis = amsData->getLastUpdateMillis();
realtimeData->lastImportUpdateMillis = lastUpdatedMillis;
}
if(amsData->getListType() > 1 && realtimeData->lastExportUpdateMillis < amsData->getLastUpdateMillis()) {
unsigned long ms = amsData->getLastUpdateMillis() - realtimeData->lastExportUpdateMillis;
float kwhe = (amsData->getActiveExportPower() * (((float) ms) / 3600000.0)) / 1000.0;
if(listType > 1 && realtimeData->lastExportUpdateMillis < lastUpdatedMillis) {
unsigned long ms = lastUpdatedMillis - realtimeData->lastExportUpdateMillis;
float kwhe = (activeExportPower * (((float) ms) / 3600000.0)) / 1000.0;
if(kwhe > 0) {
realtimeData->produce += kwhe;
float exportPrice = ps == NULL ? PRICE_NO_VALUE : ps->getCurrentPrice(PRICE_DIRECTION_EXPORT);
@@ -183,7 +182,7 @@ bool EnergyAccounting::update(AmsData* amsData) {
realtimeData->incomeDay += income;
}
}
realtimeData->lastExportUpdateMillis = amsData->getLastUpdateMillis();
realtimeData->lastExportUpdateMillis = lastUpdatedMillis;
}
if(config != NULL) {

View File

@@ -79,6 +79,7 @@ private:
void publishPriceSensors(PriceService* ps);
void publishSystemSensors();
void publishThresholdSensors();
void toJsonIsoTimestamp(time_t t, char* buf, size_t buflen);
String boardTypeToString(uint8_t b) {
switch(b) {

View File

@@ -1,4 +1,4 @@
{
"P" : %lu,
"t" : "%s"
"t" : %s
}

View File

@@ -3,6 +3,6 @@
"tPO" : %.3f,
"tQI" : %.3f,
"tQO" : %.3f,
"rtc" : "%s",
"t" : "%s"
"rtc" : %s,
"t" : %s
}

View File

@@ -12,5 +12,5 @@
"U1" : %.2f,
"U2" : %.2f,
"U3" : %.2f,
"t" : "%s"
"t" : %s
}

View File

@@ -28,5 +28,5 @@
"tPO1" : %.3f,
"tPO2" : %.3f,
"tPO3" : %.3f,
"t" : "%s"
"t" : %s
}

View File

@@ -2,7 +2,7 @@
"name" : "%s%s",
"stat_t" : "%s%s",
"uniq_id" : "%s_%s",
"obj_id" : "%s_%s",
"default_entity_id" : "sensor.%s_%s",
"val_tpl" : "{{ value_json.%s | is_defined }}",
"expire_after" : %d,
"dev" : {

View File

@@ -28,7 +28,8 @@ void HomeAssistantMqttHandler::setHomeAssistantConfig(HomeAssistantConfig config
snprintf_P(json, 128, PSTR("[%s] "), config.discoveryNameTag);
sensorNamePrefix = String(json);
} else {
deviceName = F("AMS reader");
snprintf_P(json, 128, PSTR("AMS reader"));
deviceName = String(json);
sensorNamePrefix = "";
}
deviceModel = boardTypeToString(boardType);
@@ -52,20 +53,18 @@ void HomeAssistantMqttHandler::setHomeAssistantConfig(HomeAssistantConfig config
deviceUrl = String(json);
}
if(strlen(config.discoveryPrefix) > 0) {
snprintf_P(json, 128, PSTR("%s/status"), config.discoveryPrefix);
statusTopic = String(json);
snprintf_P(json, 128, PSTR("%s/sensor"), config.discoveryPrefix);
sensorTopic = String(json);
snprintf_P(json, 128, PSTR("%s/update"), config.discoveryPrefix);
updateTopic = String(json);
} else {
statusTopic = F("homeassistant/status");
sensorTopic = F("homeassistant/sensor");
updateTopic = F("homeassistant/update");
if(strlen(config.discoveryPrefix) == 0) {
snprintf_P(config.discoveryPrefix, 64, PSTR("homeassistant"));
}
snprintf_P(json, 128, PSTR("%s/status"), config.discoveryPrefix);
statusTopic = String(json);
snprintf_P(json, 128, PSTR("%s/sensor"), config.discoveryPrefix);
sensorTopic = String(json);
snprintf_P(json, 128, PSTR("%s/update"), config.discoveryPrefix);
updateTopic = String(json);
strcpy(this->mqttConfig.subscribeTopic, statusTopic.c_str());
}
@@ -134,12 +133,7 @@ bool HomeAssistantMqttHandler::publishList1(AmsData* data, EnergyAccounting* ea)
publishList1Sensors();
char pt[24];
memset(pt, 0, 24);
if(data->getPackageTimestamp() > 0) {
tmElements_t tm;
breakTime(data->getPackageTimestamp(), tm);
sprintf_P(pt, PSTR("%04d-%02d-%02dT%02d:%02d:%02dZ"), tm.Year+1970, tm.Month, tm.Day, tm.Hour, tm.Minute, tm.Second);
}
toJsonIsoTimestamp(data->getPackageTimestamp(), pt, sizeof(pt));
snprintf_P(json, BufferSize, HA1_JSON, data->getActiveImportPower(), pt);
return mqtt.publish(pubTopic + "/power", json);
@@ -150,12 +144,7 @@ bool HomeAssistantMqttHandler::publishList2(AmsData* data, EnergyAccounting* ea)
if(data->getActiveExportPower() > 0) publishList2ExportSensors();
char pt[24];
memset(pt, 0, 24);
if(data->getPackageTimestamp() > 0) {
tmElements_t tm;
breakTime(data->getPackageTimestamp(), tm);
sprintf_P(pt, PSTR("%04d-%02d-%02dT%02d:%02d:%02dZ"), tm.Year+1970, tm.Month, tm.Day, tm.Hour, tm.Minute, tm.Second);
}
toJsonIsoTimestamp(data->getPackageTimestamp(), pt, sizeof(pt));
snprintf_P(json, BufferSize, HA3_JSON,
data->getListId().c_str(),
@@ -181,20 +170,11 @@ bool HomeAssistantMqttHandler::publishList3(AmsData* data, EnergyAccounting* ea)
if(data->getActiveExportCounter() > 0.0) publishList3ExportSensors();
char mt[24];
memset(mt, 0, 24);
if(data->getMeterTimestamp() > 0) {
tmElements_t tm;
breakTime(data->getMeterTimestamp(), tm);
sprintf_P(mt, PSTR("%04d-%02d-%02dT%02d:%02d:%02dZ"), tm.Year+1970, tm.Month, tm.Day, tm.Hour, tm.Minute, tm.Second);
}
toJsonIsoTimestamp(data->getMeterTimestamp(), mt, sizeof(mt));
char pt[24];
memset(pt, 0, 24);
if(data->getPackageTimestamp() > 0) {
tmElements_t tm;
breakTime(data->getPackageTimestamp(), tm);
sprintf_P(pt, PSTR("%04d-%02d-%02dT%02d:%02d:%02dZ"), tm.Year+1970, tm.Month, tm.Day, tm.Hour, tm.Minute, tm.Second);
}
toJsonIsoTimestamp(data->getPackageTimestamp(), pt, sizeof(pt));
snprintf_P(json, BufferSize, HA2_JSON,
data->getActiveImportCounter(),
@@ -212,12 +192,7 @@ bool HomeAssistantMqttHandler::publishList4(AmsData* data, EnergyAccounting* ea)
if(data->getL1ActiveExportPower() > 0 || data->getL2ActiveExportPower() > 0 || data->getL3ActiveExportPower() > 0) publishList4ExportSensors();
char pt[24];
memset(pt, 0, 24);
if(data->getPackageTimestamp() > 0) {
tmElements_t tm;
breakTime(data->getPackageTimestamp(), tm);
sprintf_P(pt, PSTR("%04d-%02d-%02dT%02d:%02d:%02dZ"), tm.Year+1970, tm.Month, tm.Day, tm.Hour, tm.Minute, tm.Second);
}
toJsonIsoTimestamp(data->getPackageTimestamp(), pt, sizeof(pt));
snprintf_P(json, BufferSize, HA4_JSON,
data->getListId().c_str(),
@@ -307,13 +282,8 @@ bool HomeAssistantMqttHandler::publishRealtime(AmsData* data, EnergyAccounting*
time_t now = time(nullptr);
char pt[24];
memset(pt, 0, 24);
if(now > 0) {
tmElements_t tm;
breakTime(now, tm);
sprintf_P(pt, PSTR("%04d-%02d-%02dT%02d:%02d:%02dZ"), tm.Year+1970, tm.Month, tm.Day, tm.Hour, tm.Minute, tm.Second);
}
pos += snprintf_P(json+pos, BufferSize-pos, PSTR(",\"t\":\"%s\""), pt);
toJsonIsoTimestamp(now, pt, sizeof(pt));
pos += snprintf_P(json+pos, BufferSize-pos, PSTR(",\"t\":%s"), pt);
json[pos++] = '}';
json[pos] = '\0';
@@ -343,13 +313,8 @@ bool HomeAssistantMqttHandler::publishTemperatures(AmsConfiguration* config, HwT
time_t now = time(nullptr);
char pt[24];
memset(pt, 0, 24);
if(now > 0) {
tmElements_t tm;
breakTime(now, tm);
sprintf_P(pt, PSTR("%04d-%02d-%02dT%02d:%02d:%02dZ"), tm.Year+1970, tm.Month, tm.Day, tm.Hour, tm.Minute, tm.Second);
}
pos += snprintf_P(json+pos, BufferSize-pos, PSTR(",\"t\":\"%s\""), pt);
toJsonIsoTimestamp(now, pt, sizeof(pt));
pos += snprintf_P(json+pos, BufferSize-pos, PSTR(",\"t\":%s"), pt);
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("}"));
bool ret = mqtt.publish(pubTopic + "/temperatures", json);
@@ -421,25 +386,34 @@ bool HomeAssistantMqttHandler::publishPrices(PriceService* ps) {
memset(ts1hr, 0, 24);
if(min1hrIdx > -1) {
time_t ts = now + (SECS_PER_HOUR * min1hrIdx);
tmElements_t tm;
tmElements_t tm;
breakTime(ts, tm);
sprintf_P(ts1hr, PSTR("%04d-%02d-%02dT%02d:00:00Z"), tm.Year+1970, tm.Month, tm.Day, tm.Hour);
tm.Minute = 0;
tm.Second = 0;
ts = makeTime(tm);
toJsonIsoTimestamp(ts, ts1hr, sizeof(ts1hr));
}
char ts3hr[24];
memset(ts3hr, 0, 24);
if(min3hrIdx > -1) {
time_t ts = now + (SECS_PER_HOUR * min3hrIdx);
tmElements_t tm;
tmElements_t tm;
breakTime(ts, tm);
sprintf_P(ts3hr, PSTR("%04d-%02d-%02dT%02d:00:00Z"), tm.Year+1970, tm.Month, tm.Day, tm.Hour);
tm.Minute = 0;
tm.Second = 0;
ts = makeTime(tm);
toJsonIsoTimestamp(ts, ts3hr, sizeof(ts3hr));
}
char ts6hr[24];
memset(ts6hr, 0, 24);
if(min6hrIdx > -1) {
time_t ts = now + (SECS_PER_HOUR * min6hrIdx);
tmElements_t tm;
tmElements_t tm;
breakTime(ts, tm);
sprintf_P(ts6hr, PSTR("%04d-%02d-%02dT%02d:00:00Z"), tm.Year+1970, tm.Month, tm.Day, tm.Hour);
tm.Minute = 0;
tm.Second = 0;
ts = makeTime(tm);
toJsonIsoTimestamp(ts, ts6hr, sizeof(ts6hr));
}
uint16_t pos = snprintf_P(json, BufferSize, PSTR("{\"id\":\"%s\",\"prices\":{\"import\":["), WiFi.macAddress().c_str());
@@ -468,7 +442,7 @@ bool HomeAssistantMqttHandler::publishPrices(PriceService* ps) {
}
pos--;
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("],\"min\":%.4f,\"max\":%.4f,\"cheapest1hr\":\"%s\",\"cheapest3hr\":\"%s\",\"cheapest6hr\":\"%s\"}"),
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("],\"min\":%.4f,\"max\":%.4f,\"cheapest1hr\":%s,\"cheapest3hr\":%s,\"cheapest6hr\":%s}"),
min == INT16_MAX ? 0.0 : min,
max == INT16_MIN ? 0.0 : max,
ts1hr,
@@ -477,13 +451,8 @@ bool HomeAssistantMqttHandler::publishPrices(PriceService* ps) {
);
char pt[24];
memset(pt, 0, 24);
if(now > 0) {
tmElements_t tm;
breakTime(now, tm);
sprintf_P(pt, PSTR("%04d-%02d-%02dT%02d:%02d:%02dZ"), tm.Year+1970, tm.Month, tm.Day, tm.Hour, tm.Minute, tm.Second);
}
pos += snprintf_P(json+pos, BufferSize-pos, PSTR(",\"t\":\"%s\""), pt);
toJsonIsoTimestamp(now, pt, sizeof(pt));
pos += snprintf_P(json+pos, BufferSize-pos, PSTR(",\"t\":%s"), pt);
json[pos++] = '}';
json[pos] = '\0';
@@ -502,14 +471,9 @@ bool HomeAssistantMqttHandler::publishSystem(HwTools* hw, PriceService* ps, Ener
time_t now = time(nullptr);
char pt[24];
memset(pt, 0, 24);
if(now > 0) {
tmElements_t tm;
breakTime(now, tm);
sprintf_P(pt, PSTR("%04d-%02d-%02dT%02d:%02d:%02dZ"), tm.Year+1970, tm.Month, tm.Day, tm.Hour, tm.Minute, tm.Second);
}
toJsonIsoTimestamp(now, pt, sizeof(pt));
snprintf_P(json, BufferSize, PSTR("{\"id\":\"%s\",\"name\":\"%s\",\"up\":%d,\"vcc\":%.3f,\"rssi\":%d,\"temp\":%.2f,\"version\":\"%s\",\"t\":\"%s\"}"),
snprintf_P(json, BufferSize, PSTR("{\"id\":\"%s\",\"name\":\"%s\",\"up\":%d,\"vcc\":%.3f,\"rssi\":%d,\"temp\":%.2f,\"version\":\"%s\",\"t\":%s}"),
WiFi.macAddress().c_str(),
mqttConfig.clientId,
(uint32_t) (millis64()/1000),
@@ -902,3 +866,14 @@ void HomeAssistantMqttHandler::onMessage(String &topic, String &payload) {
}
}
}
void HomeAssistantMqttHandler::toJsonIsoTimestamp(time_t t, char* buf, size_t buflen) {
memset(buf, 0, buflen);
if(t > 0) {
tmElements_t tm;
breakTime(t, tm);
snprintf_P(buf, buflen, PSTR("\"%04d-%02d-%02dT%02d:%02d:%02dZ\""), tm.Year+1970, tm.Month, tm.Day, tm.Hour, tm.Minute, tm.Second);
} else {
snprintf_P(buf, buflen, PSTR("null"));
}
}

View File

@@ -45,6 +45,7 @@ public:
bool applyBoardConfig(uint8_t boardType, GpioConfig& gpioConfig, MeterConfig& meterConfig, uint8_t hanPin);
void setup(SystemConfig* sys, GpioConfig* gpio);
float getVcc();
void setMaxVcc(float maxVcc);
uint8_t getTempSensorCount();
TempSensorData* getTempSensorData(uint8_t);
bool updateTemperatures();
@@ -68,7 +69,7 @@ private:
uint8_t vccPin, vccGnd_r, vccVcc_r;
float vccOffset, vccMultiplier;
float vcc = 3.3; // Last known Vcc
float maxVcc = 3.25; // Best to have this close to max as a start, in case Pow-U reboots and starts off with a low voltage, we dont want that to be perceived as max
float maxVcc = 3.28; // Best to have this close to max as a start, in case Pow-U reboots and starts off with a low voltage, we dont want that to be perceived as max
unsigned long lastVccRead = 0;
uint16_t analogRange = 1024;

View File

@@ -677,4 +677,8 @@ bool HwTools::isVoltageOptimal(float range) {
uint8_t HwTools::getBoardType() {
return boardType;
}
void HwTools::setMaxVcc(float vcc) {
this->maxVcc = min(3.3f, vcc);
}

View File

@@ -42,5 +42,6 @@ private:
bool publishList3(AmsData* data, EnergyAccounting* ea);
bool publishList4(AmsData* data, EnergyAccounting* ea);
String getMeterModel(AmsData* data);
void toJsonIsoTimestamp(time_t t, char* buf, size_t buflen);
};
#endif

View File

@@ -356,25 +356,34 @@ bool JsonMqttHandler::publishPrices(PriceService* ps) {
memset(ts1hr, 0, 24);
if(min1hrIdx > -1) {
time_t ts = now + (SECS_PER_HOUR * min1hrIdx);
tmElements_t tm;
tmElements_t tm;
breakTime(ts, tm);
sprintf_P(ts1hr, PSTR("%04d-%02d-%02dT%02d:00:00Z"), tm.Year+1970, tm.Month, tm.Day, tm.Hour);
tm.Minute = 0;
tm.Second = 0;
ts = makeTime(tm);
toJsonIsoTimestamp(ts, ts1hr, sizeof(ts1hr));
}
char ts3hr[24];
memset(ts3hr, 0, 24);
if(min3hrIdx > -1) {
time_t ts = now + (SECS_PER_HOUR * min3hrIdx);
tmElements_t tm;
tmElements_t tm;
breakTime(ts, tm);
sprintf_P(ts3hr, PSTR("%04d-%02d-%02dT%02d:00:00Z"), tm.Year+1970, tm.Month, tm.Day, tm.Hour);
tm.Minute = 0;
tm.Second = 0;
ts = makeTime(tm);
toJsonIsoTimestamp(ts, ts3hr, sizeof(ts3hr));
}
char ts6hr[24];
memset(ts6hr, 0, 24);
if(min6hrIdx > -1) {
time_t ts = now + (SECS_PER_HOUR * min6hrIdx);
tmElements_t tm;
tmElements_t tm;
breakTime(ts, tm);
sprintf_P(ts6hr, PSTR("%04d-%02d-%02dT%02d:00:00Z"), tm.Year+1970, tm.Month, tm.Day, tm.Hour);
tm.Minute = 0;
tm.Second = 0;
ts = makeTime(tm);
toJsonIsoTimestamp(ts, ts6hr, sizeof(ts6hr));
}
if(mqttConfig.payloadFormat == 6) {
@@ -388,7 +397,7 @@ bool JsonMqttHandler::publishPrices(PriceService* ps) {
}
}
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("\"pr_min\":%.4f,\"pr_max\":%.4f,\"pr_cheapest1hr\":\"%s\",\"pr_cheapest3hr\":\"%s\",\"pr_cheapest6hr\":\"%s\"}"),
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("\"pr_min\":%.4f,\"pr_max\":%.4f,\"pr_cheapest1hr\":%s,\"pr_cheapest3hr\":%s,\"pr_cheapest6hr\":%s}"),
min == INT16_MAX ? 0.0 : min,
max == INT16_MIN ? 0.0 : max,
ts1hr,
@@ -422,7 +431,7 @@ bool JsonMqttHandler::publishPrices(PriceService* ps) {
}
pos--;
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("],\"min\":%.4f,\"max\":%.4f,\"cheapest1hr\":\"%s\",\"cheapest3hr\":\"%s\",\"cheapest6hr\":\"%s\"}}"),
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("],\"min\":%.4f,\"max\":%.4f,\"cheapest1hr\":%s,\"cheapest3hr\":%s,\"cheapest6hr\":%s}}"),
min == INT16_MAX ? 0.0 : min,
max == INT16_MIN ? 0.0 : max,
ts1hr,
@@ -535,3 +544,14 @@ void JsonMqttHandler::onMessage(String &topic, String &payload) {
}
}
}
void JsonMqttHandler::toJsonIsoTimestamp(time_t t, char* buf, size_t buflen) {
memset(buf, 0, buflen);
if(t > 0) {
tmElements_t tm;
breakTime(t, tm);
snprintf_P(buf, buflen, PSTR("\"%04d-%02d-%02dT%02d:%02d:%02dZ\""), tm.Year+1970, tm.Month, tm.Day, tm.Hour, tm.Minute, tm.Second);
} else {
snprintf_P(buf, buflen, PSTR("null"));
}
}

View File

@@ -11,6 +11,7 @@
#include "AmsConfiguration.h"
#include "DataParser.h"
#include "Cosem.h"
#include "Timezone.h"
#if defined(AMS_REMOTE_DEBUG)
#include "RemoteDebug.h"
#endif

View File

@@ -25,16 +25,206 @@ IEC6205675::IEC6205675(const char* d, Timezone* tz, uint8_t useMeterType, MeterC
if(val == NOVALUE) {
CosemData* data = getCosemDataAt(1, ((char *) (d)));
// Kaifa special case...
if(useMeterType == AmsTypeKaifa && data->base.type == CosemTypeDLongUnsigned) {
this->packageTimestamp = this->packageTimestamp > 0 && tz != NULL ? tz->toUTC(this->packageTimestamp) : 0;
if(useMeterType == AmsTypeIskra) { // Iskra special case
meterType = AmsTypeIskra;
uint8_t idx = 0;
data = getCosemDataAt(idx, ((char *) (d)));
if(data->base.length == 0x21) {
idx = 4;
// 1.8.0
data = getCosemDataAt(idx++, ((char *) (d)));
activeImportCounter = ntohl(data->dlu.data) / 1000.0;
// 1.8.1
// 1.8.2
idx += 2;
// 2.8.0
data = getCosemDataAt(idx++, ((char *) (d)));
activeExportCounter = ntohl(data->dlu.data) / 1000.0;
// 2.8.1
// 2.8.2
idx += 2;
// 5.8.0
// 6.8.0
// 7.8.0
// 8.8.0
idx += 4;
// 1.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
activeImportPower = ntohl(data->dlu.data);
// 2.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
activeExportPower = ntohl(data->dlu.data);
// 13.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
powerFactor= ntohl(data->dlu.data) / 1000.0;
// 21.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
l1activeImportPower = ntohl(data->dlu.data);
// 41.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
l2activeImportPower = ntohl(data->dlu.data);
// 61.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
l3activeImportPower = ntohl(data->dlu.data);
// 22.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
l1activeExportPower = ntohl(data->dlu.data);
// 42.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
l2activeExportPower = ntohl(data->dlu.data);
// 62.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
l3activeExportPower = ntohl(data->dlu.data);
// 32.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
l1voltage = ntohs(data->lu.data) / 10.0;
// 52.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
l2voltage = ntohs(data->lu.data) / 10.0;
// 72.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
l3voltage = ntohs(data->lu.data) / 10.0;
// 31.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
l1current = ntohs(data->lu.data) / 100.0;
// 51.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
l2current = ntohs(data->lu.data) / 100.0;
// 71.7.0
data = getCosemDataAt(idx++, ((char *) (d)));
l3current = ntohs(data->lu.data) / 100.0;
listType = 4;
lastUpdateMillis = millis64();
} else if(data->base.length == 0x0F) {
idx = 1;
// 1.8.0 ?
data = getCosemDataAt(idx++, ((char *) (d)));
activeImportCounter = ntohl(data->dlu.data) / 1000.0;
// 1.8.1 ?
// 1.8.2 ?
idx += 2;
// 2.8.0 ?
data = getCosemDataAt(idx++, ((char *) (d)));
activeExportCounter = ntohl(data->dlu.data) / 1000.0;
// 2.8.1 ?
// 2.8.2 ?
idx += 2;
idx++; // Unknown empty octet string
CosemData* meterTs = getCosemDataAt(idx++, ((char *) (d)));
if(meterTs != NULL) {
AmsOctetTimestamp* amst = (AmsOctetTimestamp*) meterTs;
time_t ts = decodeCosemDateTime(amst->dt);
meterTimestamp = ts;
}
// 2.7.0 ?
data = getCosemDataAt(idx++, ((char *) (d)));
activeExportPower = ntohl(data->dlu.data);
// 1.7.0 ?
data = getCosemDataAt(idx++, ((char *) (d)));
activeImportPower = ntohl(data->dlu.data);
// 31.7.0 ?
data = getCosemDataAt(idx++, ((char *) (d)));
l1current = ntohs(data->lu.data) / 100.0;
// 51.7.0 ?
data = getCosemDataAt(idx++, ((char *) (d)));
l2current = ntohs(data->lu.data) / 100.0;
// 71.7.0 ?
data = getCosemDataAt(idx++, ((char *) (d)));
l3current = ntohs(data->lu.data) / 100.0;
// 32.7.0 ?
data = getCosemDataAt(idx++, ((char *) (d)));
l1voltage = ntohs(data->lu.data) / 10.0;
// 72.7.0 ?
data = getCosemDataAt(idx++, ((char *) (d)));
l3voltage = ntohs(data->lu.data) / 10.0;
// 52.7.0 missing?
l2voltage = sqrt(pow(l1voltage - l3voltage * cos(60 * (PI/180)), 2) + pow(l3voltage * sin(60 * (PI/180)),2));
listType = 3;
lastUpdateMillis = millis64();
} else {
idx = 5;
data = getCosemDataAt(idx++, ((char *) (d)));
if(data != NULL) {
activeImportCounter = ntohl(data->dlu.data) / 1000.0;
}
data = getCosemDataAt(idx++, ((char *) (d)));
if(data != NULL) {
activeExportCounter = ntohl(data->dlu.data) / 1000.0;
}
data = getCosemDataAt(idx++, ((char *) (d)));
if(data != NULL) {
reactiveImportCounter = ntohl(data->dlu.data) / 1000.0;
}
data = getCosemDataAt(idx++, ((char *) (d)));
if(data != NULL) {
reactiveExportCounter = ntohl(data->dlu.data) / 1000.0;
}
data = getCosemDataAt(idx++, ((char *) (d)));
if(data != NULL) {
activeImportPower = ntohl(data->dlu.data);
}
data = getCosemDataAt(idx++, ((char *) (d)));
if(data != NULL) {
activeExportPower = ntohl(data->dlu.data);
}
uint8_t str_len = 0;
str_len = getString(AMS_OBIS_UNKNOWN_1, sizeof(AMS_OBIS_UNKNOWN_1), ((char *) (d)), str);
if(str_len > 0) {
meterId = String(str);
}
listType = 4;
lastUpdateMillis = millis64();
}
} else if(useMeterType == AmsTypeKaifa && data->base.type == CosemTypeDLongUnsigned) { // Kaifa special case
listType = 1;
meterType = AmsTypeKaifa;
activeImportPower = ntohl(data->dlu.data);
lastUpdateMillis = millis64();
} else if(data->base.type == CosemTypeOctetString) {
this->packageTimestamp = this->packageTimestamp > 0 && tz != NULL ? tz->toUTC(this->packageTimestamp) : 0;
} else if(data->base.type == CosemTypeOctetString) { // Assuming first string is a list identifier
memcpy(str, data->oct.data, data->oct.length);
str[data->oct.length] = 0x00;
String listId = String(str);
@@ -42,7 +232,7 @@ IEC6205675::IEC6205675(const char* d, Timezone* tz, uint8_t useMeterType, MeterC
this->listId = listId;
meterType = AmsTypeKaifa;
int idx = 0;
uint8_t idx = 0;
data = getCosemDataAt(idx, ((char *) (d)));
idx+=2;
if(data->base.length == 0x0D || data->base.length == 0x12) {
@@ -144,7 +334,7 @@ IEC6205675::IEC6205675(const char* d, Timezone* tz, uint8_t useMeterType, MeterC
this->listId = listId;
meterType = AmsTypeIskra;
int idx = 0;
uint8_t idx = 0;
data = getCosemDataAt(idx++, ((char *) (d)));
if(data->base.length == 0x12) {
apply(state);
@@ -559,49 +749,31 @@ IEC6205675::IEC6205675(const char* d, Timezone* tz, uint8_t useMeterType, MeterC
data = getCosemDataAt(idx++, ((char *) (d)));
activeExportCounter = ntohl(data->dlu.data) / 1000.0;
}
} else if(useMeterType == AmsTypeIskra && data->base.type == CosemTypeOctetString) { // Iskra special case
}
}
if(meterType == AmsTypeUnknown && useMeterType == AmsTypeUnknown) {
debugger->println("AMS unknown meter type, trying to identify...");
CosemData* d1 = getCosemDataAt(1, ((char *) (d)));
CosemData* d2 = getCosemDataAt(2, ((char *) (d)));
CosemData* d3 = getCosemDataAt(3, ((char *) (d)));
CosemData* d7 = getCosemDataAt(7, ((char *) (d)));
CosemData* d8 = getCosemDataAt(8, ((char *) (d)));
if(d1->base.type == CosemTypeDLongUnsigned &&
d2->base.type == CosemTypeDLongUnsigned &&
d3->base.type == CosemTypeDLongUnsigned &&
d7->base.type == CosemTypeOctetString &&
d8->base.type == CosemTypeOctetString
) {
meterType = AmsTypeIskra;
uint8_t idx = 5;
data = getCosemDataAt(idx++, ((char *) (d)));
if(data != NULL) {
activeImportCounter = ntohl(data->dlu.data) / 1000.0;
}
data = getCosemDataAt(idx++, ((char *) (d)));
if(data != NULL) {
activeExportCounter = ntohl(data->dlu.data) / 1000.0;
}
data = getCosemDataAt(idx++, ((char *) (d)));
if(data != NULL) {
reactiveImportCounter = ntohl(data->dlu.data) / 1000.0;
}
data = getCosemDataAt(idx++, ((char *) (d)));
if(data != NULL) {
reactiveExportCounter = ntohl(data->dlu.data) / 1000.0;
}
data = getCosemDataAt(idx++, ((char *) (d)));
if(data != NULL) {
activeImportPower = ntohl(data->dlu.data);
}
data = getCosemDataAt(idx++, ((char *) (d)));
if(data != NULL) {
activeExportPower = ntohl(data->dlu.data);
}
uint8_t str_len = 0;
str_len = getString(AMS_OBIS_UNKNOWN_1, sizeof(AMS_OBIS_UNKNOWN_1), ((char *) (d)), str);
if(str_len > 0) {
meterId = String(str);
}
listType = 3;
lastUpdateMillis = millis64();
} else if(useMeterType == AmsTypeUnknown) {
listType = 3;
} else if(d1->base.type == CosemTypeOctetString && d2->base.type == CosemTypeOctetString && d3->base.type == CosemTypeOctetString) {
meterType = AmsTypeIskra;
lastUpdateMillis = millis64();
listType = 3;
} else {
uint8_t str_len = 0;
str_len = getString(AMS_OBIS_UNKNOWN_1, sizeof(AMS_OBIS_UNKNOWN_1), ((char *) (d)), str);
if(str_len > 0) {
@@ -612,7 +784,7 @@ IEC6205675::IEC6205675(const char* d, Timezone* tz, uint8_t useMeterType, MeterC
}
}
}
} else {
} else { // OBIS code parsing
listType = 1;
activeImportPower = val;

View File

@@ -24,8 +24,6 @@ void KmpCommunicator::configure(MeterConfig& meterConfig) {
}
bool KmpCommunicator::loop() {
uint64_t now = millis64();
bool ret = talker->loop();
int lastError = getLastError();
if(ret) {
@@ -58,35 +56,36 @@ AmsData* KmpCommunicator::getData(AmsData& meterState) {
if(talker == NULL) return NULL;
KmpDataHolder kmpData;
talker->getData(kmpData);
uint64_t now = millis64();
AmsData* data = new AmsData();
data->apply(OBIS_ACTIVE_IMPORT_COUNT, kmpData.activeImportCounter);
data->apply(OBIS_ACTIVE_EXPORT_COUNT, kmpData.activeExportCounter);
data->apply(OBIS_REACTIVE_IMPORT_COUNT, kmpData.reactiveImportCounter);
data->apply(OBIS_REACTIVE_EXPORT_COUNT, kmpData.reactiveExportCounter);
data->apply(OBIS_ACTIVE_IMPORT, kmpData.activeImportPower);
data->apply(OBIS_ACTIVE_EXPORT, kmpData.activeExportPower);
data->apply(OBIS_REACTIVE_IMPORT, kmpData.reactiveImportPower);
data->apply(OBIS_REACTIVE_EXPORT, kmpData.reactiveExportPower);
data->apply(OBIS_VOLTAGE_L1, kmpData.l1voltage);
data->apply(OBIS_VOLTAGE_L2, kmpData.l2voltage);
data->apply(OBIS_VOLTAGE_L3, kmpData.l3voltage);
data->apply(OBIS_CURRENT_L1, kmpData.l1current);
data->apply(OBIS_CURRENT_L2, kmpData.l2current);
data->apply(OBIS_CURRENT_L3, kmpData.l3current);
data->apply(OBIS_POWER_FACTOR_L1, kmpData.l1PowerFactor);
data->apply(OBIS_POWER_FACTOR_L2, kmpData.l2PowerFactor);
data->apply(OBIS_POWER_FACTOR_L3, kmpData.l3PowerFactor);
data->apply(OBIS_POWER_FACTOR, kmpData.powerFactor);
data->apply(OBIS_ACTIVE_IMPORT_L1, kmpData.l1activeImportPower);
data->apply(OBIS_ACTIVE_IMPORT_L2, kmpData.l2activeImportPower);
data->apply(OBIS_ACTIVE_IMPORT_L3, kmpData.l3activeImportPower);
data->apply(OBIS_ACTIVE_EXPORT_L1, kmpData.l1activeExportPower);
data->apply(OBIS_ACTIVE_EXPORT_L2, kmpData.l2activeExportPower);
data->apply(OBIS_ACTIVE_EXPORT_L3, kmpData.l3activeExportPower);
data->apply(OBIS_ACTIVE_IMPORT_COUNT_L1, kmpData.l1activeImportCounter);
data->apply(OBIS_ACTIVE_IMPORT_COUNT_L2, kmpData.l2activeImportCounter);
data->apply(OBIS_ACTIVE_IMPORT_COUNT_L3, kmpData.l3activeImportCounter);
data->apply(OBIS_METER_ID, kmpData.meterId);
data->apply(OBIS_NULL, AmsTypeKamstrup);
data->apply(OBIS_ACTIVE_IMPORT_COUNT, kmpData.activeImportCounter, now);
data->apply(OBIS_ACTIVE_EXPORT_COUNT, kmpData.activeExportCounter, now);
data->apply(OBIS_REACTIVE_IMPORT_COUNT, kmpData.reactiveImportCounter, now);
data->apply(OBIS_REACTIVE_EXPORT_COUNT, kmpData.reactiveExportCounter, now);
data->apply(OBIS_ACTIVE_IMPORT, kmpData.activeImportPower, now);
data->apply(OBIS_ACTIVE_EXPORT, kmpData.activeExportPower, now);
data->apply(OBIS_REACTIVE_IMPORT, kmpData.reactiveImportPower, now);
data->apply(OBIS_REACTIVE_EXPORT, kmpData.reactiveExportPower, now);
data->apply(OBIS_VOLTAGE_L1, kmpData.l1voltage, now);
data->apply(OBIS_VOLTAGE_L2, kmpData.l2voltage, now);
data->apply(OBIS_VOLTAGE_L3, kmpData.l3voltage, now);
data->apply(OBIS_CURRENT_L1, kmpData.l1current, now);
data->apply(OBIS_CURRENT_L2, kmpData.l2current, now);
data->apply(OBIS_CURRENT_L3, kmpData.l3current, now);
data->apply(OBIS_POWER_FACTOR_L1, kmpData.l1PowerFactor, now);
data->apply(OBIS_POWER_FACTOR_L2, kmpData.l2PowerFactor, now);
data->apply(OBIS_POWER_FACTOR_L3, kmpData.l3PowerFactor, now);
data->apply(OBIS_POWER_FACTOR, kmpData.powerFactor, now);
data->apply(OBIS_ACTIVE_IMPORT_L1, kmpData.l1activeImportPower, now);
data->apply(OBIS_ACTIVE_IMPORT_L2, kmpData.l2activeImportPower, now);
data->apply(OBIS_ACTIVE_IMPORT_L3, kmpData.l3activeImportPower, now);
data->apply(OBIS_ACTIVE_EXPORT_L1, kmpData.l1activeExportPower, now);
data->apply(OBIS_ACTIVE_EXPORT_L2, kmpData.l2activeExportPower, now);
data->apply(OBIS_ACTIVE_EXPORT_L3, kmpData.l3activeExportPower, now);
data->apply(OBIS_ACTIVE_IMPORT_COUNT_L1, kmpData.l1activeImportCounter, now);
data->apply(OBIS_ACTIVE_IMPORT_COUNT_L2, kmpData.l2activeImportCounter, now);
data->apply(OBIS_ACTIVE_IMPORT_COUNT_L3, kmpData.l3activeImportCounter, now);
data->apply(OBIS_METER_ID, kmpData.meterId, now);
data->apply(OBIS_NULL, AmsTypeKamstrup, now);
return data;
}

View File

@@ -574,7 +574,8 @@ PricesContainer* PriceService::fetchPrices(time_t t) {
for(uint8_t i = 0; i < ret->getNumberOfPoints(); i++) {
// To avoid alignment issues on ESP8266, we use memcpy
memcpy(&intval, &points[i], sizeof(int32_t));
float value = ntohl(intval) / 10000.0;
intval = ntohl(intval); // Change byte order before converting to float, to support negative values
float value = intval / 10000.0;
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::VERBOSE))
#endif
@@ -585,7 +586,8 @@ PricesContainer* PriceService::fetchPrices(time_t t) {
for(uint8_t i = 0; i < ret->getNumberOfPoints(); i++) {
// To avoid alignment issues on ESP8266, we use memcpy
memcpy(&intval, &points[ret->getNumberOfPoints()+i], sizeof(int32_t));
float value = ntohl(intval) / 10000.0;
intval = ntohl(intval); // Change byte order before converting to float, to support negative values
float value = intval / 10000.0;
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::VERBOSE))
#endif

View File

@@ -7,7 +7,6 @@
#ifndef _REALTIMEPLOT_H
#define _REALTIMEPLOT_H
#include <stdint.h>
#include "AmsData.h"
#define REALTIME_SAMPLE 10000

View File

@@ -4,6 +4,7 @@
*
*/
#include "Arduino.h"
#include "RealtimePlot.h"
#include <stdlib.h>

File diff suppressed because one or more lines are too long

View File

@@ -9,7 +9,8 @@
"version": "0.0.0",
"dependencies": {
"cssnano": "^5.1.15",
"esbuild": ">=0.25.0"
"esbuild": ">=0.25.0",
"ipaddr.js": "^2.3.0"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^2.1.0",
@@ -1584,6 +1585,14 @@
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true
},
"node_modules/ipaddr.js": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz",
"integrity": "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==",
"engines": {
"node": ">= 10"
}
},
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",

View File

@@ -10,10 +10,10 @@
"preview": "vite preview"
},
"overrides": {
"svelte-navigator": {
"svelte": ">=4.x"
}
},
"svelte-navigator": {
"svelte": ">=4.x"
}
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^2.1.0",
"@tailwindcss/forms": "^0.5.3",
@@ -30,6 +30,7 @@
},
"dependencies": {
"cssnano": "^5.1.15",
"esbuild": ">=0.25.0"
"esbuild": ">=0.25.0",
"ipaddr.js": "^2.3.0"
}
}

View File

@@ -130,7 +130,7 @@ export function uiVisibility(choice, state) {
}
export function wiki(page) {
let ret = "https://wiki.amsleser.no/";
let ret = "https://wiki.amsleser.no";
if(page) {
ret += "/en/firmware#" + page;
}

View File

@@ -7,6 +7,7 @@
import Clock from './Clock.svelte';
import Mask from './Mask.svelte';
import { scanForDevice } from './Helpers.js';
import ipaddr from 'ipaddr.js';
export let data;
export let sysinfo;
@@ -207,11 +208,11 @@
</div>
{#if sysinfo.net.ipv6}
<div class="my-2">
IPv6: <span style="font-size: 14px;">{sysinfo.net.ipv6.replace(/\b:?(?:0+:?){2,}/, '::')}</span>
IPv6: <span style="font-size: 14px;">{ipaddr.parse(sysinfo.net.ipv6)}</span>
</div>
<div class="my-2">
{#if sysinfo.net.dns1v6}DNSv6: <span style="font-size: 14px;">{sysinfo.net.dns1v6.replace(/\b:?(?:0+:?){2,}/, '::')}</span>{/if}
{#if sysinfo.net.dns2v6}DNSv6: <span style="font-size: 14px;">{sysinfo.net.dns2v6.replace(/\b:?(?:0+:?){2,}/, '::')}</span>{/if}
{#if sysinfo.net.dns1v6}DNSv6: <span style="font-size: 14px;">{ipaddr.parse(sysinfo.net.dns1v6)}</span>{/if}
{#if sysinfo.net.dns2v6}DNSv6: <span style="font-size: 14px;">{ipaddr.parse(sysinfo.net.dns2v6)}</span>{/if}
</div>
{/if}
</div>

View File

@@ -1389,10 +1389,10 @@ void AmsWebServer::handleSave() {
memset(meterConfig.authenticationKey, 0, 16);
}
meterConfig.wattageMultiplier = server.arg(F("mmw")).toFloat() * 1000;
meterConfig.voltageMultiplier = server.arg(F("mmv")).toFloat() * 1000;
meterConfig.amperageMultiplier = server.arg(F("mma")).toFloat() * 1000;
meterConfig.accumulatedMultiplier = server.arg(F("mmc")).toFloat() * 1000;
meterConfig.wattageMultiplier = server.arg(F("mmw")).toDouble() * 1000.0;
meterConfig.voltageMultiplier = server.arg(F("mmv")).toDouble() * 1000.0;
meterConfig.amperageMultiplier = server.arg(F("mma")).toDouble() * 1000.0;
meterConfig.accumulatedMultiplier = server.arg(F("mmc")).toDouble() * 1000.0;
config->setMeterConfig(meterConfig);
}
@@ -1408,7 +1408,7 @@ void AmsWebServer::handleSave() {
if(!psk.equals("***")) {
strcpy(network.psk, psk.c_str());
}
network.power = server.arg(F("ww")).toFloat() * 10;
network.power = server.arg(F("ww")).toDouble() * 10.0;
network.sleep = server.arg(F("wz")).toInt();
network.use11b = server.hasArg(F("wb")) && server.arg(F("wb")) == F("true");
}
@@ -1569,9 +1569,9 @@ void AmsWebServer::handleSave() {
}
if(server.hasArg(F("iv")) && server.arg(F("iv")) == F("true")) {
gpioConfig->vccOffset = server.hasArg(F("ivo")) && !server.arg(F("ivo")).isEmpty() ? server.arg(F("ivo")).toFloat() * 100 : 0;
gpioConfig->vccMultiplier = server.hasArg(F("ivm")) && !server.arg(F("ivm")).isEmpty() ? server.arg(F("ivm")).toFloat() * 1000 : 1000;
gpioConfig->vccBootLimit = server.hasArg(F("ivb")) && !server.arg(F("ivb")).isEmpty() ? server.arg(F("ivb")).toFloat() * 10 : 0;
gpioConfig->vccOffset = server.hasArg(F("ivo")) && !server.arg(F("ivo")).isEmpty() ? server.arg(F("ivo")).toDouble() * 100.0 : 0;
gpioConfig->vccMultiplier = server.hasArg(F("ivm")) && !server.arg(F("ivm")).isEmpty() ? server.arg(F("ivm")).toDouble() * 1000.0 : 1000;
gpioConfig->vccBootLimit = server.hasArg(F("ivb")) && !server.arg(F("ivb")).isEmpty() ? server.arg(F("ivb")).toDouble() * 10.0 : 0;
config->setGpioConfig(*gpioConfig);
}
@@ -1686,7 +1686,7 @@ void AmsWebServer::handleSave() {
snprintf_P(buf, BufferSize, PSTR("rd%d"), i);
pc.direction = server.arg(buf).toInt();
snprintf_P(buf, BufferSize, PSTR("rv%d"), i);
pc.value = server.arg(buf).toFloat() * 10000;
pc.value = server.arg(buf).toDouble() * 10000.0;
snprintf_P(buf, BufferSize, PSTR("rn%d"), i);
String name = server.arg(buf);
strcpy(pc.name, name.c_str());

View File

@@ -190,10 +190,15 @@ CloudConnector *cloud = NULL;
#if defined(ZMART_CHARGE)
ZmartChargeCloudConnector *zcloud = NULL;
#endif
#define MAX_BOOT_CYCLES 6
#if defined(ESP32)
__NOINIT_ATTR EnergyAccountingRealtimeData rtd;
RTC_DATA_ATTR uint8_t bootcount = 0;
#else
EnergyAccountingRealtimeData rtd;
uint32_t bootcount = 0;
#endif
EnergyAccounting ea(&Debug, &rtd);
@@ -326,6 +331,31 @@ void rxerr(int err) {
}
#endif
uint8_t incrementBootCycleCounter(bool deepSleep) {
#if defined(ESP8266)
if(deepSleep) {
if(ESP.rtcUserMemoryRead(0, &bootcount, sizeof(bootcount))) {
bootcount++;
ESP.rtcUserMemoryWrite(0, &bootcount, sizeof(bootcount));
}
return bootcount;
} else {
return ++bootcount;
}
#else
return ++bootcount;
#endif
}
void resetBootCycleCounter(bool deepSleep) {
#if defined(ESP8266)
bootcount = 0;
if(deepSleep) {
ESP.rtcUserMemoryWrite(0, &bootcount, sizeof(bootcount));
}
#else
bootcount = 0;
#endif
}
void setup() {
Serial.begin(115200);
@@ -380,20 +410,6 @@ void setup() {
}
}
hw.ledBlink(LED_INTERNAL, 1);
hw.ledBlink(LED_RED, 1);
hw.ledBlink(LED_YELLOW, 1);
hw.ledBlink(LED_GREEN, 1);
hw.ledBlink(LED_BLUE, 1);
PriceServiceConfig price;
if(config.getPriceServiceConfig(price)) {
ps = new PriceService(&Debug);
ps->setup(price);
ws.setPriceService(ps);
}
ws.setPriceSettings(price.area, price.currency);
ea.setCurrency(price.currency);
bool shared = false;
Serial.flush();
Serial.end();
@@ -441,22 +457,51 @@ void setup() {
yield();
#endif
float vcc = hw.getVcc();
if(!hw.ledOn(LED_YELLOW)) {
hw.ledOn(LED_INTERNAL);
}
debugI_P(PSTR("AMS reader %s started"), FirmwareVersion::VersionString);
debugI_P(PSTR("Configuration version: %d, board type: %d"), config.getConfigVersion(), sysConfig.boardType);
float vcc = hw.getVcc();
debugI_P(PSTR("Voltage: %.2fV"), vcc);
float vccBootLimit = gpioConfig.vccBootLimit == 0 ? 0 : min(3.29, gpioConfig.vccBootLimit / 10.0); // Make sure it is never above 3.3v
if(vcc > 2.5 && vccBootLimit > 2.5 && vccBootLimit < 3.3 && (gpioConfig.apPin == 0xFF || digitalRead(gpioConfig.apPin) == HIGH)) { // Skip if user is holding AP button while booting (HIGH = button is released)
if (vcc < vccBootLimit) {
{
Debug.printf_P(PSTR("(setup) Voltage is too low (%.2f < %.2f), sleeping\n"), vcc, vccBootLimit);
bool deepSleep = true;
#if defined(ESP32)
float allowedDrift = bootcount * 0.01;
#else
float allowedDrift = gpioConfig.vccBootLimit == 0 ? 0.05 : 3.3 - min(3.29, gpioConfig.vccBootLimit / 10.0); // Make sure boot limit is never above 3.3v
deepSleep = gpioConfig.vccBootLimit > 0; // If a boot limit is set, we are assume the hardware has been configured for deep sleep (Hint: GPIO16)
#endif
while(!hw.isVoltageOptimal(allowedDrift)) {
uint8_t bootCycles = incrementBootCycleCounter(deepSleep);
debugW_P(PSTR("Voltage is outside optimal range (%.2fV)"), allowedDrift);
if(gpioConfig.apPin != 0xFF && digitalRead(gpioConfig.apPin) == LOW) {
debugW_P(PSTR("AP button is pressed, skipping voltage wait"));
} else if(bootCycles < MAX_BOOT_CYCLES) {
int secs = MAX_BOOT_CYCLES - bootCycles;
if(deepSleep) {
debugI_P(PSTR("Sleeping for %d seconds to allow capacitor charge (%d.cycle)"), secs, bootCycles);
Serial.flush();
ESP.deepSleep(secs * 1000000); // Deep sleep to allow output cap to charge up
return;
} else {
debugI_P(PSTR("Waiting (no sleep) for %d seconds to allow capacitor charge (%d.cycle)"), secs, bootCycles);
delay(secs * 1000); // Just delay to allow output cap to charge up
vcc = hw.getVcc();
debugI_P(PSTR("Voltage: %.2fV"), vcc);
}
ESP.deepSleep(10000000); //Deep sleep to allow output cap to charge up
}
} else {
debugE_P(PSTR("Voltage not reaching optimal level after multiple attempts, continuing boot"));
hw.setMaxVcc(vcc); // Since we had to sleep, set max Vcc to current level because this is probably the highest we will get
break;
}
}
#if defined(ESP8266)
resetBootCycleCounter(deepSleep);
#endif
hw.ledOff(LED_YELLOW);
hw.ledOff(LED_INTERNAL);
if(!hw.ledOn(LED_GREEN)) {
hw.ledOn(LED_INTERNAL);
@@ -472,6 +517,12 @@ void setup() {
hw.ledOff(LED_GREEN);
hw.ledOff(LED_INTERNAL);
hw.ledBlink(LED_INTERNAL, 1);
hw.ledBlink(LED_RED, 1);
hw.ledBlink(LED_YELLOW, 1);
hw.ledBlink(LED_GREEN, 1);
hw.ledBlink(LED_BLUE, 1);
WiFi.disconnect(true);
WiFi.softAPdisconnect(true);
WiFi.mode(WIFI_OFF);
@@ -537,6 +588,15 @@ void setup() {
toggleSetupMode();
}
PriceServiceConfig price;
if(config.getPriceServiceConfig(price)) {
ps = new PriceService(&Debug);
ps->setup(price);
ws.setPriceService(ps);
}
ws.setPriceSettings(price.area, price.currency);
ea.setCurrency(price.currency);
EnergyAccountingConfig *eac = new EnergyAccountingConfig();
if(!config.getEnergyAccountingConfig(*eac)) {
config.clearEnergyAccountingConfig(*eac);
@@ -633,7 +693,12 @@ void loop() {
handleEnergySpeedometer();
#endif
#endif
handlePriceService(now);
// In case of BUS powered meters, we need to be sure voltage is stable before fetching prices. But we refuse to wait forever, so max 30 seconds
if(now > 30000 || hw.isVoltageOptimal(0.01)) {
handlePriceService(now);
}
#if defined(AMS_CLOUD)
handleCloud();
#endif
@@ -1542,7 +1607,7 @@ void handleDataSuccess(AmsData* data) {
debugD_P(PSTR("NOT Ready to update (internal clock %02d:%02d:%02d UTC, meter clock: %02d:%02d:%02d, list type %d, est: %d)"), tm.Hour, tm.Minute, tm.Second, mtm.Hour, mtm.Minute, mtm.Second, data->getListType(), wasCounterEstimated);
}
if(ea.update(data)) {
if(ea.update(dataUpdateTime, data->getLastUpdateMillis(), data->getListType(), data->getActiveImportPower(), data->getActiveExportPower())) {
debugI_P(PSTR("Saving energy accounting"));
if(!ea.save()) {
debugW_P(PSTR("Unable to save energy accounting"));