Compare commits

..

5 Commits

Author SHA1 Message Date
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
17 changed files with 322 additions and 154 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

@@ -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); fromHex((uint8_t*) &crc, String((char*) buf+crcPos), 2);
crc = ntohs(crc); crc = ntohs(crc);
if(crc != crc_calc) { if(crc > 0 && crc != crc_calc) {
if(debugger != NULL) { if(debugger != NULL) {
debugger->printf_P(PSTR("CRC incorrrect, %04X != %04X at position %lu\n"), crc, crc_calc, crcPos); 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; return DATA_PARSE_BOUNDARY_FLAG_MISSING;
// Verify FCS // 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; return DATA_PARSE_FOOTER_CHECKSUM_ERROR;
// Skip destination address, LSB marks last byte // Skip destination address, LSB marks last byte
@@ -50,7 +50,7 @@ int8_t HDLCParser::parse(uint8_t *d, DataParserContext &ctx) {
HDLC3CtrlHcs* t3 = (HDLC3CtrlHcs*) (ptr); HDLC3CtrlHcs* t3 = (HDLC3CtrlHcs*) (ptr);
// Verify HCS // 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; return DATA_PARSE_HEADER_CHECKSUM_ERROR;
ptr += 3; ptr += 3;
@@ -69,7 +69,12 @@ int8_t HDLCParser::parse(uint8_t *d, DataParserContext &ctx) {
if(buf == NULL) return DATA_PARSE_FAIL; 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; pos += ctx.length;
lastSequenceNumber++; lastSequenceNumber++;
@@ -78,7 +83,12 @@ int8_t HDLCParser::parse(uint8_t *d, DataParserContext &ctx) {
lastSequenceNumber = 0; lastSequenceNumber = 0;
if(buf == NULL) return DATA_PARSE_FAIL; 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; pos += ctx.length;
memcpy((uint8_t *) d, buf, pos); memcpy((uint8_t *) d, buf, pos);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -28,7 +28,8 @@ void HomeAssistantMqttHandler::setHomeAssistantConfig(HomeAssistantConfig config
snprintf_P(json, 128, PSTR("[%s] "), config.discoveryNameTag); snprintf_P(json, 128, PSTR("[%s] "), config.discoveryNameTag);
sensorNamePrefix = String(json); sensorNamePrefix = String(json);
} else { } else {
deviceName = F("AMS reader"); snprintf_P(json, 128, PSTR("AMS reader"));
deviceName = String(json);
sensorNamePrefix = ""; sensorNamePrefix = "";
} }
deviceModel = boardTypeToString(boardType); deviceModel = boardTypeToString(boardType);
@@ -52,20 +53,18 @@ void HomeAssistantMqttHandler::setHomeAssistantConfig(HomeAssistantConfig config
deviceUrl = String(json); deviceUrl = String(json);
} }
if(strlen(config.discoveryPrefix) > 0) { if(strlen(config.discoveryPrefix) == 0) {
snprintf_P(json, 128, PSTR("%s/status"), config.discoveryPrefix); snprintf_P(config.discoveryPrefix, 64, PSTR("homeassistant"));
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");
} }
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()); strcpy(this->mqttConfig.subscribeTopic, statusTopic.c_str());
} }
@@ -134,12 +133,7 @@ bool HomeAssistantMqttHandler::publishList1(AmsData* data, EnergyAccounting* ea)
publishList1Sensors(); publishList1Sensors();
char pt[24]; char pt[24];
memset(pt, 0, 24); toJsonIsoTimestamp(data->getPackageTimestamp(), pt, sizeof(pt));
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);
}
snprintf_P(json, BufferSize, HA1_JSON, data->getActiveImportPower(), pt); snprintf_P(json, BufferSize, HA1_JSON, data->getActiveImportPower(), pt);
return mqtt.publish(pubTopic + "/power", json); return mqtt.publish(pubTopic + "/power", json);
@@ -150,12 +144,7 @@ bool HomeAssistantMqttHandler::publishList2(AmsData* data, EnergyAccounting* ea)
if(data->getActiveExportPower() > 0) publishList2ExportSensors(); if(data->getActiveExportPower() > 0) publishList2ExportSensors();
char pt[24]; char pt[24];
memset(pt, 0, 24); toJsonIsoTimestamp(data->getPackageTimestamp(), pt, sizeof(pt));
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);
}
snprintf_P(json, BufferSize, HA3_JSON, snprintf_P(json, BufferSize, HA3_JSON,
data->getListId().c_str(), data->getListId().c_str(),
@@ -181,20 +170,11 @@ bool HomeAssistantMqttHandler::publishList3(AmsData* data, EnergyAccounting* ea)
if(data->getActiveExportCounter() > 0.0) publishList3ExportSensors(); if(data->getActiveExportCounter() > 0.0) publishList3ExportSensors();
char mt[24]; char mt[24];
memset(mt, 0, 24); toJsonIsoTimestamp(data->getMeterTimestamp(), mt, sizeof(mt));
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);
}
char pt[24]; char pt[24];
memset(pt, 0, 24); memset(pt, 0, 24);
if(data->getPackageTimestamp() > 0) { toJsonIsoTimestamp(data->getPackageTimestamp(), pt, sizeof(pt));
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);
}
snprintf_P(json, BufferSize, HA2_JSON, snprintf_P(json, BufferSize, HA2_JSON,
data->getActiveImportCounter(), data->getActiveImportCounter(),
@@ -212,12 +192,7 @@ bool HomeAssistantMqttHandler::publishList4(AmsData* data, EnergyAccounting* ea)
if(data->getL1ActiveExportPower() > 0 || data->getL2ActiveExportPower() > 0 || data->getL3ActiveExportPower() > 0) publishList4ExportSensors(); if(data->getL1ActiveExportPower() > 0 || data->getL2ActiveExportPower() > 0 || data->getL3ActiveExportPower() > 0) publishList4ExportSensors();
char pt[24]; char pt[24];
memset(pt, 0, 24); toJsonIsoTimestamp(data->getPackageTimestamp(), pt, sizeof(pt));
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);
}
snprintf_P(json, BufferSize, HA4_JSON, snprintf_P(json, BufferSize, HA4_JSON,
data->getListId().c_str(), data->getListId().c_str(),
@@ -307,13 +282,8 @@ bool HomeAssistantMqttHandler::publishRealtime(AmsData* data, EnergyAccounting*
time_t now = time(nullptr); time_t now = time(nullptr);
char pt[24]; char pt[24];
memset(pt, 0, 24); toJsonIsoTimestamp(now, pt, sizeof(pt));
if(now > 0) { pos += snprintf_P(json+pos, BufferSize-pos, PSTR(",\"t\":%s"), pt);
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);
json[pos++] = '}'; json[pos++] = '}';
json[pos] = '\0'; json[pos] = '\0';
@@ -343,13 +313,8 @@ bool HomeAssistantMqttHandler::publishTemperatures(AmsConfiguration* config, HwT
time_t now = time(nullptr); time_t now = time(nullptr);
char pt[24]; char pt[24];
memset(pt, 0, 24); toJsonIsoTimestamp(now, pt, sizeof(pt));
if(now > 0) { pos += snprintf_P(json+pos, BufferSize-pos, PSTR(",\"t\":%s"), pt);
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);
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("}")); pos += snprintf_P(json+pos, BufferSize-pos, PSTR("}"));
bool ret = mqtt.publish(pubTopic + "/temperatures", json); bool ret = mqtt.publish(pubTopic + "/temperatures", json);
@@ -421,25 +386,34 @@ bool HomeAssistantMqttHandler::publishPrices(PriceService* ps) {
memset(ts1hr, 0, 24); memset(ts1hr, 0, 24);
if(min1hrIdx > -1) { if(min1hrIdx > -1) {
time_t ts = now + (SECS_PER_HOUR * min1hrIdx); time_t ts = now + (SECS_PER_HOUR * min1hrIdx);
tmElements_t tm; tmElements_t tm;
breakTime(ts, 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]; char ts3hr[24];
memset(ts3hr, 0, 24); memset(ts3hr, 0, 24);
if(min3hrIdx > -1) { if(min3hrIdx > -1) {
time_t ts = now + (SECS_PER_HOUR * min3hrIdx); time_t ts = now + (SECS_PER_HOUR * min3hrIdx);
tmElements_t tm; tmElements_t tm;
breakTime(ts, 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]; char ts6hr[24];
memset(ts6hr, 0, 24); memset(ts6hr, 0, 24);
if(min6hrIdx > -1) { if(min6hrIdx > -1) {
time_t ts = now + (SECS_PER_HOUR * min6hrIdx); time_t ts = now + (SECS_PER_HOUR * min6hrIdx);
tmElements_t tm; tmElements_t tm;
breakTime(ts, 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()); 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--;
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, min == INT16_MAX ? 0.0 : min,
max == INT16_MIN ? 0.0 : max, max == INT16_MIN ? 0.0 : max,
ts1hr, ts1hr,
@@ -477,13 +451,8 @@ bool HomeAssistantMqttHandler::publishPrices(PriceService* ps) {
); );
char pt[24]; char pt[24];
memset(pt, 0, 24); toJsonIsoTimestamp(now, pt, sizeof(pt));
if(now > 0) { pos += snprintf_P(json+pos, BufferSize-pos, PSTR(",\"t\":%s"), pt);
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);
json[pos++] = '}'; json[pos++] = '}';
json[pos] = '\0'; json[pos] = '\0';
@@ -502,14 +471,9 @@ bool HomeAssistantMqttHandler::publishSystem(HwTools* hw, PriceService* ps, Ener
time_t now = time(nullptr); time_t now = time(nullptr);
char pt[24]; char pt[24];
memset(pt, 0, 24); toJsonIsoTimestamp(now, pt, sizeof(pt));
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);
}
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(), WiFi.macAddress().c_str(),
mqttConfig.clientId, mqttConfig.clientId,
(uint32_t) (millis64()/1000), (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

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

View File

@@ -356,25 +356,34 @@ bool JsonMqttHandler::publishPrices(PriceService* ps) {
memset(ts1hr, 0, 24); memset(ts1hr, 0, 24);
if(min1hrIdx > -1) { if(min1hrIdx > -1) {
time_t ts = now + (SECS_PER_HOUR * min1hrIdx); time_t ts = now + (SECS_PER_HOUR * min1hrIdx);
tmElements_t tm; tmElements_t tm;
breakTime(ts, 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]; char ts3hr[24];
memset(ts3hr, 0, 24); memset(ts3hr, 0, 24);
if(min3hrIdx > -1) { if(min3hrIdx > -1) {
time_t ts = now + (SECS_PER_HOUR * min3hrIdx); time_t ts = now + (SECS_PER_HOUR * min3hrIdx);
tmElements_t tm; tmElements_t tm;
breakTime(ts, 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]; char ts6hr[24];
memset(ts6hr, 0, 24); memset(ts6hr, 0, 24);
if(min6hrIdx > -1) { if(min6hrIdx > -1) {
time_t ts = now + (SECS_PER_HOUR * min6hrIdx); time_t ts = now + (SECS_PER_HOUR * min6hrIdx);
tmElements_t tm; tmElements_t tm;
breakTime(ts, 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) { 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, min == INT16_MAX ? 0.0 : min,
max == INT16_MIN ? 0.0 : max, max == INT16_MIN ? 0.0 : max,
ts1hr, 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

@@ -27,14 +27,11 @@ IEC6205675::IEC6205675(const char* d, Timezone* tz, uint8_t useMeterType, MeterC
// Kaifa special case... // Kaifa special case...
if(useMeterType == AmsTypeKaifa && data->base.type == CosemTypeDLongUnsigned) { if(useMeterType == AmsTypeKaifa && data->base.type == CosemTypeDLongUnsigned) {
this->packageTimestamp = this->packageTimestamp > 0 && tz != NULL ? tz->toUTC(this->packageTimestamp) : 0;
listType = 1; listType = 1;
meterType = AmsTypeKaifa; meterType = AmsTypeKaifa;
activeImportPower = ntohl(data->dlu.data); activeImportPower = ntohl(data->dlu.data);
lastUpdateMillis = millis64(); lastUpdateMillis = millis64();
} else if(data->base.type == CosemTypeOctetString) { } else if(data->base.type == CosemTypeOctetString) {
this->packageTimestamp = this->packageTimestamp > 0 && tz != NULL ? tz->toUTC(this->packageTimestamp) : 0;
memcpy(str, data->oct.data, data->oct.length); memcpy(str, data->oct.data, data->oct.length);
str[data->oct.length] = 0x00; str[data->oct.length] = 0x00;
String listId = String(str); String listId = String(str);
@@ -42,7 +39,7 @@ IEC6205675::IEC6205675(const char* d, Timezone* tz, uint8_t useMeterType, MeterC
this->listId = listId; this->listId = listId;
meterType = AmsTypeKaifa; meterType = AmsTypeKaifa;
int idx = 0; uint8_t idx = 0;
data = getCosemDataAt(idx, ((char *) (d))); data = getCosemDataAt(idx, ((char *) (d)));
idx+=2; idx+=2;
if(data->base.length == 0x0D || data->base.length == 0x12) { if(data->base.length == 0x0D || data->base.length == 0x12) {
@@ -144,7 +141,7 @@ IEC6205675::IEC6205675(const char* d, Timezone* tz, uint8_t useMeterType, MeterC
this->listId = listId; this->listId = listId;
meterType = AmsTypeIskra; meterType = AmsTypeIskra;
int idx = 0; uint8_t idx = 0;
data = getCosemDataAt(idx++, ((char *) (d))); data = getCosemDataAt(idx++, ((char *) (d)));
if(data->base.length == 0x12) { if(data->base.length == 0x12) {
apply(state); apply(state);
@@ -561,54 +558,157 @@ IEC6205675::IEC6205675(const char* d, Timezone* tz, uint8_t useMeterType, MeterC
} }
} else if(useMeterType == AmsTypeIskra && data->base.type == CosemTypeOctetString) { // Iskra special case } else if(useMeterType == AmsTypeIskra && data->base.type == CosemTypeOctetString) { // Iskra special case
meterType = AmsTypeIskra; meterType = AmsTypeIskra;
uint8_t idx = 5;
data = getCosemDataAt(idx++, ((char *) (d))); uint8_t idx = 0;
if(data != NULL) { 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; activeImportCounter = ntohl(data->dlu.data) / 1000.0;
}
// 1.8.1
data = getCosemDataAt(idx++, ((char *) (d))); // 1.8.2
if(data != NULL) { idx += 2;
// 2.8.0
data = getCosemDataAt(idx++, ((char *) (d)));
activeExportCounter = ntohl(data->dlu.data) / 1000.0; 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))); // 2.8.1
if(data != NULL) { // 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); activeImportPower = ntohl(data->dlu.data);
}
data = getCosemDataAt(idx++, ((char *) (d))); // 2.7.0
if(data != NULL) { data = getCosemDataAt(idx++, ((char *) (d)));
activeExportPower = ntohl(data->dlu.data); activeExportPower = ntohl(data->dlu.data);
}
uint8_t str_len = 0; // 13.7.0
str_len = getString(AMS_OBIS_UNKNOWN_1, sizeof(AMS_OBIS_UNKNOWN_1), ((char *) (d)), str); data = getCosemDataAt(idx++, ((char *) (d)));
if(str_len > 0) { powerFactor= ntohl(data->dlu.data) / 1000.0;
meterId = String(str);
}
listType = 3; // 21.7.0
lastUpdateMillis = millis64(); 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 {
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 == AmsTypeUnknown) { } else if(useMeterType == AmsTypeUnknown) {
uint8_t str_len = 0; uint8_t idx = 1;
str_len = getString(AMS_OBIS_UNKNOWN_1, sizeof(AMS_OBIS_UNKNOWN_1), ((char *) (d)), str); CosemData* d1 = getCosemDataAt(idx++, ((char *) (d)));
if(str_len > 0) { CosemData* d2 = getCosemDataAt(idx++, ((char *) (d)));
CosemData* d3 = getCosemDataAt(idx++, ((char *) (d)));
if(d1->base.type == CosemTypeOctetString && d2->base.type == CosemTypeOctetString && d3->base.type == CosemTypeOctetString) {
meterType = AmsTypeIskra; meterType = AmsTypeIskra;
meterId = String(str);
lastUpdateMillis = millis64(); lastUpdateMillis = millis64();
listType = 3; 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) {
meterType = AmsTypeIskra;
meterId = String(str);
lastUpdateMillis = millis64();
listType = 3;
}
} }
} }
} }

View File

@@ -574,7 +574,8 @@ PricesContainer* PriceService::fetchPrices(time_t t) {
for(uint8_t i = 0; i < ret->getNumberOfPoints(); i++) { for(uint8_t i = 0; i < ret->getNumberOfPoints(); i++) {
// To avoid alignment issues on ESP8266, we use memcpy // To avoid alignment issues on ESP8266, we use memcpy
memcpy(&intval, &points[i], sizeof(int32_t)); 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 defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::VERBOSE)) if (debugger->isActive(RemoteDebug::VERBOSE))
#endif #endif
@@ -585,7 +586,8 @@ PricesContainer* PriceService::fetchPrices(time_t t) {
for(uint8_t i = 0; i < ret->getNumberOfPoints(); i++) { for(uint8_t i = 0; i < ret->getNumberOfPoints(); i++) {
// To avoid alignment issues on ESP8266, we use memcpy // To avoid alignment issues on ESP8266, we use memcpy
memcpy(&intval, &points[ret->getNumberOfPoints()+i], sizeof(int32_t)); 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 defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::VERBOSE)) if (debugger->isActive(RemoteDebug::VERBOSE))
#endif #endif

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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