Compare commits

...

21 Commits

Author SHA1 Message Date
Gunnar Skjold
60d9e1fd22 Debugger adjustments 2026-03-05 08:28:32 +01:00
Gunnar Skjold
4698442101 Use unique SSID on first boot 2026-02-19 12:34:13 +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
Gunnar Skjold
2e4a0fc0a3 Fixed price shift for non-CET price area (#1090)
* Fixed non-CET price presentation

* Added compiled version

* Updated fix for non cet

* Fixed! I think...
2025-12-11 11:42:17 +01:00
Gunnar Skjold
fc6e9e8085 Fixed ESP8266 memory issue with price decoding (#1089) 2025-12-11 11:37:52 +01:00
Gunnar Skjold
ad73821f1c Disable auto buffer size for HAN on ESP8266 (#1086) 2025-12-11 11:34:46 +01:00
Gunnar Skjold
98bf5b958f Fixed float infinity issue (#1087) 2025-12-11 11:34:15 +01:00
Gunnar Skjold
f323c5a4f6 Fixed building without remote debug (#1084) 2025-12-09 12:19:00 +01:00
Gunnar Skjold
ea91248e67 Changed MQTT client timeout setting for ESP8266 (#1077) 2025-12-05 15:37:23 +01:00
40 changed files with 650 additions and 290 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

@@ -381,6 +381,8 @@ public:
bool isZmartChargeConfigChanged();
void ackZmartChargeConfig();
uint32_t getChipId();
void getUniqueName(char* buffer, size_t length);
void clear();

View File

@@ -124,16 +124,12 @@ void AmsConfiguration::clearNetworkConfig(NetworkConfig& config) {
memset(config.ssid, 0, 32);
memset(config.psk, 0, 64);
clearNetworkConfigIp(config);
uint16_t chipId;
getUniqueName(config.hostname, 32);
#if defined(ESP32)
chipId = ( ESP.getEfuseMac() >> 32 ) % 0xFFFFFFFF;
config.power = 195;
#else
chipId = ESP.getChipId();
config.power = 205;
#endif
strcpy(config.hostname, (String("ams-") + String(chipId, HEX)).c_str());
config.mdns = true;
config.sleep = 0xFF;
config.use11b = 1;
@@ -591,7 +587,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 +595,7 @@ void AmsConfiguration::clearGpio(GpioConfig& config, bool all) {
config.vccResistorGnd = 0;
config.vccResistorVcc = 0;
config.ledBehaviour = LED_BEHAVIOUR_DEFAULT;
config.powersaving = 0;
}
}
@@ -983,6 +979,23 @@ void AmsConfiguration::setUiLanguageChanged() {
uiLanguageChanged = true;
}
uint32_t AmsConfiguration::getChipId() {
uint32_t chipId;
#if defined(ESP32)
for(int i=0; i<17; i=i+8) {
chipId |= ((ESP.getEfuseMac() >> (40 - i)) & 0xff) << i;
}
#else
chipId = ESP.getChipId();
#endif
return chipId;
}
void AmsConfiguration::getUniqueName(char* buffer, size_t length) {
uint32_t chipId = getChipId();
snprintf(buffer, length, "ams-%06x", chipId);
}
void AmsConfiguration::clear() {
EEPROM.begin(EEPROM_SIZE);

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

@@ -25,7 +25,7 @@ public:
#if defined(AMS_REMOTE_DEBUG)
AmsMqttHandler(MqttConfig& mqttConfig, RemoteDebug* debugger, char* buf, AmsFirmwareUpdater* updater) {
#else
AmsMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf) {
AmsMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf, AmsFirmwareUpdater* updater) {
#endif
this->mqttConfig = mqttConfig;
this->mqttConfigChanged = true;

View File

@@ -102,10 +102,17 @@ bool AmsMqttHandler::connect() {
}
actualClient = mqttClient;
}
int clientTimeout = mqttConfig.timeout / 1000;
if(clientTimeout > 3) clientTimeout = 3; // 3000ms is default, see WiFiClient.cpp WIFI_CLIENT_DEF_CONN_TIMEOUT_MS
actualClient->setTimeout(clientTimeout);
// Why can't we set number of retries for write here? WiFiClient defaults to 10 (10*3s == 30s)
// This section helps with power saving on ESP32 devices by reducing timeouts
// The timeout is multiplied by 10 because WiFiClient is retrying 10 times internally
// Power drain for this timeout is too great when using the default 3s timeout
// On ESP8266 the timeout is used differently and the following code causes MQTT instability
#if defined(ESP32)
int clientTimeout = mqttConfig.timeout / 1000;
if(clientTimeout > 3) clientTimeout = 3; // 3000ms is default, see WiFiClient.cpp WIFI_CLIENT_DEF_CONN_TIMEOUT_MS
actualClient->setTimeout(clientTimeout);
// Why can't we set number of retries for write here? WiFiClient defaults to 10 (10*3s == 30s)
#endif
mqttConfigChanged = false;
mqtt.setTimeout(mqttConfig.timeout);

View File

@@ -17,7 +17,7 @@ public:
this->config = config;
};
#else
DomoticzMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf, DomoticzConfig config) : AmsMqttHandler(mqttConfig, debugger, buf) {
DomoticzMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf, DomoticzConfig config, AmsFirmwareUpdater* updater) : AmsMqttHandler(mqttConfig, debugger, buf, updater) {
this->config = config;
};
#endif

View File

@@ -260,7 +260,9 @@ float EnergyAccounting::getUseThisMonth() {
}
float EnergyAccounting::getUseLastMonth() {
return (data.lastMonthImport * pow(10, data.lastMonthAccuracy)) / 1000;
float ret = (data.lastMonthImport * pow(10, data.lastMonthAccuracy)) / 1000;
if(std::isnan(ret)) return 0.0;
return ret;
}
float EnergyAccounting::getProducedThisHour() {
@@ -292,7 +294,9 @@ float EnergyAccounting::getProducedThisMonth() {
}
float EnergyAccounting::getProducedLastMonth() {
return (data.lastMonthExport * pow(10, data.lastMonthAccuracy)) / 1000;
float ret = (data.lastMonthExport * pow(10, data.lastMonthAccuracy)) / 1000;
if(std::isnan(ret)) return 0.0;
return ret;
}
float EnergyAccounting::getCostThisHour() {

View File

@@ -17,7 +17,7 @@ public:
#if defined(AMS_REMOTE_DEBUG)
HomeAssistantMqttHandler(MqttConfig& mqttConfig, RemoteDebug* debugger, char* buf, uint8_t boardType, HomeAssistantConfig config, HwTools* hw, AmsFirmwareUpdater* updater, char* hostname) : AmsMqttHandler(mqttConfig, debugger, buf, updater) {
#else
HomeAssistantMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf, uint8_t boardType, HomeAssistantConfig config, HwTools* hw) : AmsMqttHandler(mqttConfig, debugger, buf) {
HomeAssistantMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf, uint8_t boardType, HomeAssistantConfig config, HwTools* hw, AmsFirmwareUpdater* updater, char* hostname) : AmsMqttHandler(mqttConfig, debugger, buf, updater) {
#endif
this->boardType = boardType;
this->hw = hw;
@@ -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

@@ -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

@@ -807,6 +807,7 @@ void PassiveMeterCommunicator::rxerr(int err) {
#endif
debugger->printf_P(PSTR("Serial buffer overflow\n"));
rxBufferErrors++;
#if defined(ESP32)
if(rxBufferErrors > 1 && meterConfig.bufferSize < 8) {
meterConfig.bufferSize += 2;
#if defined(AMS_REMOTE_DEBUG)
@@ -816,6 +817,7 @@ void PassiveMeterCommunicator::rxerr(int err) {
configChanged = true;
rxBufferErrors = 0;
}
#endif
break;
case 3:
#if defined(AMS_REMOTE_DEBUG)

View File

@@ -16,7 +16,7 @@ public:
this->topic = String(mqttConfig.publishTopic);
};
#else
PassthroughMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf) : AmsMqttHandler(mqttConfig, debugger, buf) {
PassthroughMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf, AmsFirmwareUpdater* updater) : AmsMqttHandler(mqttConfig, debugger, buf, updater) {
this->topic = String(mqttConfig.publishTopic);
};
#endif

View File

@@ -124,9 +124,6 @@ private:
Timezone* tz = NULL;
Timezone* entsoeTz = NULL;
static const uint16_t BufferSize = 256;
char* buf;
bool hub = false;
uint8_t* key = NULL;
uint8_t* auth = NULL;
@@ -139,7 +136,7 @@ private:
bool retrieve(const char* url, Stream* doc);
float getCurrencyMultiplier(const char* from, const char* to, time_t t);
bool timeIsInPeriod(tmElements_t tm, PriceConfig pc);
float getFixedPrice(uint8_t direction, int8_t hour);
float getFixedPrice(uint8_t direction, int8_t point);
float getEnergyPricePoint(uint8_t direction, uint8_t point);
};
#endif

View File

@@ -25,8 +25,6 @@ PriceService::PriceService(RemoteDebug* Debug) : priceConfig(std::vector<PriceCo
#else
PriceService::PriceService(Stream* Debug) : priceConfig(std::vector<PriceConfig>()) {
#endif
this->buf = (char*) malloc(BufferSize);
debugger = Debug;
// Entso-E uses CET/CEST
@@ -129,15 +127,16 @@ bool PriceService::isExportPricesDifferentFromImport() {
}
float PriceService::getPricePoint(uint8_t direction, uint8_t point) {
float value = getFixedPrice(direction, point * getResolutionInMinutes() / 60);
float value = getFixedPrice(direction, point);
if(value == PRICE_NO_VALUE) value = getEnergyPricePoint(direction, point);
if(value == PRICE_NO_VALUE) return PRICE_NO_VALUE;
tmElements_t tm;
time_t ts = time(nullptr);
breakTime(tz->toLocal(ts), tm);
breakTime(entsoeTz->toLocal(ts), tm);
tm.Hour = tm.Minute = tm.Second = 0;
breakTime(makeTime(tm) + (point * SECS_PER_MIN * getResolutionInMinutes()), tm);
ts = entsoeTz->toUTC(makeTime(tm)) + (point * SECS_PER_MIN * getResolutionInMinutes());
breakTime(tz->toLocal(ts), tm);
for (uint8_t i = 0; i < priceConfig.size(); i++) {
PriceConfig pc = priceConfig.at(i);
@@ -164,8 +163,6 @@ float PriceService::getPricePoint(uint8_t direction, uint8_t point) {
float PriceService::getCurrentPrice(uint8_t direction) {
time_t ts = time(nullptr);
tmElements_t tm;
breakTime(tz->toLocal(ts), tm);
uint8_t pos = getCurrentPricePointIndex();
return getPricePoint(direction, pos);
@@ -173,6 +170,7 @@ float PriceService::getCurrentPrice(uint8_t direction) {
float PriceService::getEnergyPricePoint(uint8_t direction, uint8_t point) {
uint8_t pos = point;
float multiplier = 1.0;
uint8_t numberOfPointsToday = 24;
if(today != NULL) {
@@ -208,10 +206,10 @@ float PriceService::getPriceForRelativeHour(uint8_t direction, int8_t hour) {
time_t ts = time(nullptr);
tmElements_t tm;
breakTime(tz->toLocal(ts), tm);
int8_t targetHour = tm.Hour + hour;
breakTime(entsoeTz->toLocal(ts), tm);
uint8_t targetHour = tm.Hour + hour;
tm.Hour = tm.Minute = tm.Second = 0;
time_t startOfDay = tz->toUTC(makeTime(tm));
time_t startOfDay = entsoeTz->toUTC(makeTime(tm));
if((ts + (hour * SECS_PER_HOUR)) < startOfDay) {
return PRICE_NO_VALUE;
@@ -237,15 +235,15 @@ float PriceService::getPriceForRelativeHour(uint8_t direction, int8_t hour) {
return valueSum / valueCount;
}
float PriceService::getFixedPrice(uint8_t direction, int8_t hour) {
float PriceService::getFixedPrice(uint8_t direction, int8_t point) {
time_t ts = time(nullptr);
tmElements_t tm;
breakTime(entsoeTz->toLocal(ts), tm);
tm.Hour = tm.Minute = tm.Second = 0;
ts = entsoeTz->toUTC(makeTime(tm)) + (point * SECS_PER_MIN * getResolutionInMinutes());
breakTime(tz->toLocal(ts), tm);
tm.Hour = hour;
tm.Minute = 0;
tm.Second = 0;
breakTime(makeTime(tm), tm);
tm.Minute = tm.Second = 0;
float value = PRICE_NO_VALUE;
for (uint8_t i = 0; i < priceConfig.size(); i++) {
@@ -430,11 +428,12 @@ float PriceService::getCurrencyMultiplier(const char* from, const char* to, time
#endif
float currencyMultiplier = 0;
snprintf_P(buf, BufferSize, PSTR("https://data.norges-bank.no/api/data/EXR/B.%s.NOK.SP?lastNObservations=1"), from);
char buf[80];
snprintf_P(buf, 80, PSTR("https://data.norges-bank.no/api/data/EXR/B.%s.NOK.SP?lastNObservations=1"), from);
if(retrieve(buf, &p)) {
currencyMultiplier = p.getValue();
if(strncmp(to, "NOK", 3) != 0) {
snprintf_P(buf, BufferSize, PSTR("https://data.norges-bank.no/api/data/EXR/B.%s.NOK.SP?lastNObservations=1"), to);
snprintf_P(buf, 80, PSTR("https://data.norges-bank.no/api/data/EXR/B.%s.NOK.SP?lastNObservations=1"), to);
if(retrieve(buf, &p)) {
if(p.getValue() > 0.0) {
currencyMultiplier /= p.getValue();
@@ -477,7 +476,8 @@ PricesContainer* PriceService::fetchPrices(time_t t) {
breakTime(e1, d1);
breakTime(e2, d2);
snprintf_P(buf, BufferSize, PSTR("https://web-api.tp.entsoe.eu/api?securityToken=%s&documentType=A44&periodStart=%04d%02d%02d%02d%02d&periodEnd=%04d%02d%02d%02d%02d&in_Domain=%s&out_Domain=%s"),
char buf[256];
snprintf_P(buf, 256, PSTR("https://web-api.tp.entsoe.eu/api?securityToken=%s&documentType=A44&periodStart=%04d%02d%02d%02d%02d&periodEnd=%04d%02d%02d%02d%02d&in_Domain=%s&out_Domain=%s"),
getToken(),
d1.Year+1970, d1.Month, d1.Day, d1.Hour, 00,
d2.Year+1970, d2.Month, d2.Day, d2.Hour, 00,
@@ -514,8 +514,8 @@ PricesContainer* PriceService::fetchPrices(time_t t) {
tmElements_t tm;
breakTime(entsoeTz->toLocal(t), tm);
String data;
snprintf_P(buf, BufferSize, PSTR("http://hub.amsleser.no/hub/price/%s/%d/%d/%d/pt%dm?currency=%s"),
char buf[128];
snprintf_P(buf, 128, PSTR("http://hub.amsleser.no/hub/price/%s/%d/%d/%d/pt%dm?currency=%s"),
config->area,
tm.Year+1970,
tm.Month,
@@ -547,13 +547,14 @@ PricesContainer* PriceService::fetchPrices(time_t t) {
#endif
if(status == HTTP_CODE_OK) {
data = http->getString();
http->end();
uint8_t* content = (uint8_t*) (data.c_str());
uint8_t content[1024];
WiFiClient* stream = http->getStreamPtr();
DataParserContext ctx = {0,0,0,0};
ctx.length = data.length();
ctx.length = stream->readBytes(content, http->getSize());
http->end();
GCMParser gcm(key, auth);
int8_t gcmRet = gcm.parse(content, ctx);
if(gcmRet > 0) {
@@ -569,8 +570,11 @@ PricesContainer* PriceService::fetchPrices(time_t t) {
ret->setCurrency(header->currency);
int32_t* points = (int32_t*) &header[1];
for(uint8_t i = 0; i < header->numberOfPoints; i++) {
int32_t intval = ntohl(points[i]);
int32_t intval;
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));
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))
@@ -579,8 +583,10 @@ PricesContainer* PriceService::fetchPrices(time_t t) {
ret->setPrice(i, value, PRICE_DIRECTION_IMPORT);
}
if(header->differentExportPrices) {
for(uint8_t i = 0; i < header->numberOfPoints; i++) {
int32_t intval = ntohl(points[i]);
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));
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))
@@ -756,6 +762,6 @@ bool PriceService::timeIsInPeriod(tmElements_t tm, PriceConfig pc) {
uint8_t PriceService::getCurrentPricePointIndex() {
time_t ts = time(nullptr);
tmElements_t tm;
breakTime(tz->toLocal(ts), tm);
breakTime(entsoeTz->toLocal(ts), tm);
return ((tm.Hour * 60) + tm.Minute) / getResolutionInMinutes();
}

View File

@@ -17,7 +17,7 @@ public:
topic = String(mqttConfig.publishTopic);
};
#else
RawMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf) : AmsMqttHandler(mqttConfig, debugger, buf) {
RawMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf, AmsFirmwareUpdater* updater) : AmsMqttHandler(mqttConfig, debugger, buf, updater) {
full = mqttConfig.payloadFormat == 2;
topic = String(mqttConfig.publishTopic);
};

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

@@ -73,7 +73,7 @@
{#each config.x.ticks as point, i}
{#if !isNaN(xScale(i))}
<g class="tick" transform="translate({xScale(i)},{heightAvailable})">
{#if barWidth > 20 || i%2 == 0}
{#if barWidth > 20 || i%2 == 0 || !config.x.ticks[i-1].label}
<text x="{barWidth/2}" y="-4">{point.label}</text>
{/if}
</g>

View File

@@ -140,17 +140,17 @@
{#if uiVisibility(sysinfo.ui.p, data.p && !Number.isNaN(data.p))}
{#if importPrices?.importExportPriceDifferent && (data.om || data.e > 0)}
<div class="cnt gwf">
<PricePlot title="{translations.dashboard?.price_import ?? "Price import"}" json={importPrices}/>
<PricePlot title="{translations.dashboard?.price_import ?? "Price import"}" json={importPrices} sysinfo={sysinfo}/>
</div>
{:else}
<div class="cnt gwf">
<PricePlot title={translations.dashboard?.price ?? "Price"} json={importPrices}/>
<PricePlot title={translations.dashboard?.price ?? "Price"} json={importPrices} sysinfo={sysinfo}/>
</div>
{/if}
{/if}
{#if importPrices?.importExportPriceDifferent && (data.om || data.e > 0) && uiVisibility(sysinfo.ui.p, data.pe && !Number.isNaN(data.pe))}
<div class="cnt gwf">
<PricePlot title={translations.dashboard?.price_export ?? "Price export"} json={exportPrices}/>
<PricePlot title={translations.dashboard?.price_export ?? "Price export"} json={exportPrices} sysinfo={sysinfo}/>
</div>
{/if}
{#if uiVisibility(sysinfo.ui.d, dayPlot)}

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

@@ -5,6 +5,7 @@
export let title;
export let json;
export let sysinfo;
let config = {};
let max;
@@ -39,7 +40,8 @@
let xTicks = [];
let values = [];
min = max = 0;
let i = Math.floor(((cur.getHours()*60) + cur.getMinutes()) / json?.resolution);
addHours(cur, sysinfo.clock_offset - ((24 + cur.getHours() - cur.getUTCHours())%24));
let i = json?.cursor ? json.cursor : 0;
cur.setMinutes(Math.floor(cur.getMinutes()/json?.resolution)*json?.resolution,0,0);
while(i < json?.prices?.length) {
val = json.prices[i];

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

@@ -1,7 +1,7 @@
{
"version": "%s",
"chip": "%s",
"chipId": "%s",
"chipId": "%06x",
"cpu": %d,
"mac": "%s",
"apmac": "%s",

View File

@@ -306,22 +306,14 @@ void AmsWebServer::sysinfoJson() {
SystemConfig sys;
config->getSystemConfig(sys);
uint32_t chipId;
#if defined(ESP32)
chipId = ( ESP.getEfuseMac() >> 32 ) % 0xFFFFFFFF;
#else
chipId = ESP.getChipId();
#endif
String chipIdStr = String(chipId, HEX);
String hostname;
char hostname[32];
if(sys.userConfigured) {
NetworkConfig networkConfig;
config->getNetworkConfig(networkConfig);
hostname = String(networkConfig.hostname);
strncpy(hostname, networkConfig.hostname, 32);
} else {
hostname = "ams-"+chipIdStr;
config->getUniqueName(hostname, 32);
}
IPAddress localIp;
@@ -421,7 +413,7 @@ void AmsWebServer::sysinfoJson() {
#elif defined(ESP8266)
"esp8266",
#endif
chipIdStr.c_str(),
config->getChipId(),
cpu_freq,
macStr,
apMacStr,
@@ -429,7 +421,7 @@ void AmsWebServer::sysinfoJson() {
sys.vendorConfigured ? "true" : "false",
sys.userConfigured ? "true" : "false",
sys.dataCollectionConsent,
hostname.c_str(),
hostname,
performRestart ? "true" : "false",
updater->getProgress() > 0.0 && upinfo.errorCode == 0 ? "true" : "false",
#if defined(ESP8266)
@@ -777,11 +769,12 @@ void AmsWebServer::priceJson(uint8_t direction) {
prices[i] = ps->getPricePoint(direction, i);
}
snprintf_P(buf, BufferSize, PSTR("{\"currency\":\"%s\",\"source\":\"%s\",\"resolution\":%d,\"direction\":\"%s\",\"importExportPriceDifferent\":%s,\"prices\":["),
snprintf_P(buf, BufferSize, PSTR("{\"currency\":\"%s\",\"source\":\"%s\",\"resolution\":%d,\"direction\":\"%s\",\"cursor\":%d,\"importExportPriceDifferent\":%s,\"prices\":["),
ps->getCurrency(),
ps->getSource(),
ps->getResolutionInMinutes(),
direction == PRICE_DIRECTION_IMPORT ? "import" : direction == PRICE_DIRECTION_EXPORT ? "export" : "both",
ps->getCurrentPricePointIndex(),
ps->isExportPricesDifferentFromImport() ? "true" : "false"
);
@@ -1388,10 +1381,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);
}
@@ -1407,7 +1400,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");
}
@@ -1568,9 +1561,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);
}
@@ -1685,7 +1678,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

@@ -2,7 +2,7 @@
extra_configs = platformio-user.ini
[common]
lib_deps = EEPROM, LittleFS, DNSServer, 256dpi/MQTT@2.5.2, OneWireNg@0.13.3, DallasTemperature@4.0.4, https://github.com/gskjold/RemoteDebug.git, PaulStoffregen/Time@1.6.1, JChristensen/Timezone@1.2.4, bblanchon/ArduinoJson@7.0.4, FirmwareVersion, AmsConfiguration, AmsData, AmsDataStorage, HwTools, Uptime, AmsDecoder, PriceService, EnergyAccounting, AmsFirmwareUpdater, AmsJsonGenerator, AmsMqttHandler, RawMqttHandler, JsonMqttHandler, DomoticzMqttHandler, HomeAssistantMqttHandler, PassthroughMqttHandler, RealtimePlot, ConnectionHandler, MeterCommunicators
lib_deps = EEPROM, LittleFS, DNSServer, 256dpi/MQTT@2.5.2, OneWireNg@0.13.3, DallasTemperature@4.0.4, https://github.com/gskjold/RemoteDebug.git, PaulStoffregen/Time@1.6.1, JChristensen/Timezone@1.2.4, FirmwareVersion, AmsConfiguration, AmsData, AmsDataStorage, HwTools, Uptime, AmsDecoder, PriceService, EnergyAccounting, AmsFirmwareUpdater, AmsJsonGenerator, AmsMqttHandler, RawMqttHandler, JsonMqttHandler, DomoticzMqttHandler, HomeAssistantMqttHandler, PassthroughMqttHandler, RealtimePlot, ConnectionHandler, MeterCommunicators
lib_ignore = OneWire
extra_scripts =
pre:scripts/addversion.py
@@ -19,7 +19,7 @@ build_flags =
-fexceptions
[esp32]
lib_deps = WiFi, Ethernet, ESPmDNS, WiFiClientSecure, HTTPClient, FS, WebServer, ESP32 Async UDP, ESP32SSDP, mulmer89/ESPRandom@1.5.0, ${common.lib_deps}, CloudConnector, ZmartCharge, SvelteUi
lib_deps = WiFi, Ethernet, ESPmDNS, WiFiClientSecure, HTTPClient, FS, WebServer, ESP32 Async UDP, ESP32SSDP, mulmer89/ESPRandom@1.5.0, ${common.lib_deps}, bblanchon/ArduinoJson@7.0.4, CloudConnector, ZmartCharge, SvelteUi
[env:esp8266]
platform = espressif8266@4.2.1

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
@@ -655,7 +720,7 @@ void loop() {
}
communicationTime += handleWebserver();
if(communicationTime > 10 && updater.getProgress() >= 0) {
if(communicationTime > 25 && updater.getProgress() >= 0) {
debugI_P(PSTR("Communication is active (%dms), forcing updater to wait"), communicationTime);
} else {
handleUpdater();
@@ -836,13 +901,7 @@ void handleEnergySpeedometer() {
if(sysConfig.energyspeedometer == 7) {
if(!meterState.getMeterId().isEmpty()) {
if(energySpeedometer == NULL) {
uint16_t chipId;
#if defined(ESP32)
chipId = ( ESP.getEfuseMac() >> 32 ) % 0xFFFFFFFF;
#else
chipId = ESP.getChipId();
#endif
strcpy(energySpeedometerConfig.clientId, (String("ams") + String(chipId, HEX)).c_str());
config.getUniqueName(energySpeedometerConfig.clientId, 32);
energySpeedometer = new JsonMqttHandler(energySpeedometerConfig, &Debug, (char*) commonBuffer, &hw, &ds, &updater);
energySpeedometer->setCaVerification(false);
}
@@ -1044,7 +1103,13 @@ void handleMeterConfig() {
debugE_P(PSTR("Unknown meter source selected: %d"), meterConfig.source);
}
ws.setMeterConfig(meterConfig.distributionSystem, meterConfig.mainFuse, meterConfig.productionCapacity);
if(mc != NULL && Debug.isActive(RemoteDebug::DEBUG)) {
if(mc != NULL &&
#if defined(AMS_REMOTE_DEBUG)
Debug.isActive(RemoteDebug::DEBUG)
#else
false // Never send debug data
#endif
) {
mc->setMqttHandlerForDebugging(mqttHandler);
}
config.ackMeterChanged();
@@ -1389,17 +1454,29 @@ void toggleSetupMode() {
#else
WiFi.beginSmartConfig();
#endif
WiFi.softAP(PSTR("AMS2MQTT"));
char ssid[32];
if(sysConfig.vendorConfigured) {
// Use the standard SSID if the vendor has configured the device
strcpy_P(ssid, PSTR("AMS2MQTT"));
} else {
// If not vendor configured, use a unique SSID to avoid conflicts if multiple devices are in setup mode at the same time
config.getUniqueName(ssid, 32);
}
uint8_t debugLevel = RemoteDebug::INFO;
#if defined(DEBUG_MODE)
debugLevel = RemoteDebug::VERBOSE;
#endif
WiFi.softAP(ssid);
Debug.setSerialEnabled(true);
Debug.begin(F("192.168.4.1"), 23, debugLevel);
debugI_P(PSTR("SSID: %s"), ssid);
if(dnsServer == NULL) {
dnsServer = new DNSServer();
}
dnsServer->setErrorReplyCode(DNSReplyCode::NoError);
dnsServer->start(53, PSTR("*"), WiFi.softAPIP());
#if defined(DEBUG_MODE)
Debug.setSerialEnabled(true);
Debug.begin(F("192.168.4.1"), 23, RemoteDebug::VERBOSE);
#endif
setupMode = true;
hw.setBootSuccessful(false);
@@ -1563,7 +1640,13 @@ void postConnect() {
if(!debug.telnet) {
Debug.stop();
}
if(mc != NULL && Debug.isActive(RemoteDebug::DEBUG)) {
if(mc != NULL &&
#if defined(AMS_REMOTE_DEBUG)
Debug.isActive(RemoteDebug::DEBUG)
#else
false // Never send debug data
#endif
) {
mc->setMqttHandlerForDebugging(mqttHandler);
}
} else {
@@ -1695,7 +1778,13 @@ void MQTT_connect() {
}
}
ws.setMqttHandler(mqttHandler);
if(mc != NULL && Debug.isActive(RemoteDebug::DEBUG)) {
if(mc != NULL &&
#if defined(AMS_REMOTE_DEBUG)
Debug.isActive(RemoteDebug::DEBUG)
#else
false // Never send debug data
#endif
) {
mc->setMqttHandlerForDebugging(mqttHandler);
}