Compare commits

...

11 Commits

Author SHA1 Message Date
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
Gunnar Skjold
271ce2081f Fixed reboot loop for some meters (#1075) 2025-12-05 10:02:59 +01:00
Gunnar Skjold
8438020dbd Feature: Dump hex data from meter to MQTT (#1071)
* Send raw data debug to MQTT

* Publish hexdump to /data

* Sensor for /data, but it doesnt work
2025-12-01 14:02:07 +01:00
Gunnar Skjold
9252a810df Improve power stability when using MQTT (#1070)
* Changes to improve MQTT and power stability

* Re-added the memory leak fix

* Re-added the memory leak fix

* Stop client before deleting

* Fixed potential nullpointer
2025-12-01 10:01:20 +01:00
Gunnar Skjold
c0c696a55c Fixed default MQTT subscription (#1065) 2025-11-27 09:37:06 +01:00
Gunnar Skjold
ef70d39f70 Fixed what hours the fixed price is applied to (#1069) 2025-11-27 09:36:10 +01:00
28 changed files with 268 additions and 135 deletions

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;
@@ -43,6 +43,7 @@ public:
void setConfig(MqttConfig& mqttConfig);
bool connect();
bool defaultSubscribe();
void disconnect();
lwmqtt_err_t lastError();
bool connected();
@@ -56,11 +57,16 @@ public:
virtual bool publishTemperatures(AmsConfiguration*, HwTools*) { return false; };
virtual bool publishPrices(PriceService* ps) { return false; };
virtual bool publishSystem(HwTools*, PriceService*, EnergyAccounting*) { return false; };
virtual bool publishRaw(String data) { return false; };
virtual bool publishRaw(uint8_t* raw, size_t length) { return false; };
virtual bool publishFirmware() { return false; };
virtual void onMessage(String &topic, String &payload) {};
virtual ~AmsMqttHandler() {
if(mqttSecureClient != NULL) {
mqttSecureClient->stop();
delete mqttSecureClient;
}
if(mqttClient != NULL) {
mqttClient->stop();
delete mqttClient;
@@ -80,6 +86,7 @@ protected:
bool caVerification = true;
WiFiClient *mqttClient = NULL;
WiFiClientSecure *mqttSecureClient = NULL;
boolean _connected = false;
char* json;
uint16_t BufferSize = 2048;
uint64_t lastStateUpdate = 0;

View File

@@ -103,6 +103,17 @@ bool AmsMqttHandler::connect() {
actualClient = mqttClient;
}
// 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);
mqtt.setKeepAlive(mqttConfig.keepalive);
@@ -125,8 +136,9 @@ bool AmsMqttHandler::connect() {
#endif
debugger->printf_P(PSTR("Successfully connected to MQTT\n"));
mqtt.onMessage(std::bind(&AmsMqttHandler::onMessage, this, std::placeholders::_1, std::placeholders::_2));
mqtt.publish(statusTopic, "online", true, 0);
_connected = mqtt.publish(statusTopic, "online", true, 0);
mqtt.loop();
defaultSubscribe();
postConnect();
return true;
} else {
@@ -146,13 +158,29 @@ bool AmsMqttHandler::connect() {
}
}
bool AmsMqttHandler::defaultSubscribe() {
bool ret = true;
if(!subTopic.isEmpty()) {
if(mqtt.subscribe(subTopic)) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::ERROR))
#endif
debugger->printf_P(PSTR(" Subscribed to [%s]\n"), subTopic.c_str());
} else {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::ERROR))
#endif
debugger->printf_P(PSTR(" Unable to subscribe to [%s]\n"), subTopic.c_str());
ret = false;
}
}
return ret;
}
void AmsMqttHandler::disconnect() {
mqtt.disconnect();
mqtt.loop();
if(mqttSecureClient != NULL) {
delete mqttSecureClient;
mqttSecureClient = NULL;
}
_connected = false;
delay(10);
yield();
}
@@ -162,12 +190,12 @@ lwmqtt_err_t AmsMqttHandler::lastError() {
}
bool AmsMqttHandler::connected() {
return mqtt.connected();
return _connected && mqtt.connected();
}
bool AmsMqttHandler::loop() {
uint64_t now = millis64();
bool ret = mqtt.connected() && mqtt.loop();
bool ret = connected() && mqtt.loop();
if(ret) {
lastSuccessfulLoop = now;
} else if(mqttConfig.rebootMinutes > 0) {

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
@@ -25,7 +25,7 @@ public:
bool publishTemperatures(AmsConfiguration*, HwTools*);
bool publishPrices(PriceService*);
bool publishSystem(HwTools* hw, PriceService* ps, EnergyAccounting* ea);
bool publishRaw(String data);
bool publishRaw(uint8_t* raw, size_t length);
void onMessage(String &topic, String &payload);

View File

@@ -103,7 +103,7 @@ uint8_t DomoticzMqttHandler::getFormat() {
return 3;
}
bool DomoticzMqttHandler::publishRaw(String data) {
bool DomoticzMqttHandler::publishRaw(uint8_t* raw, size_t length) {
return false;
}

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;
@@ -27,7 +27,7 @@ public:
bool publishTemperatures(AmsConfiguration*, HwTools*);
bool publishPrices(PriceService*);
bool publishSystem(HwTools* hw, PriceService* ps, EnergyAccounting* ea);
bool publishRaw(String data);
bool publishRaw(uint8_t* raw, size_t length);
bool publishFirmware();
bool postConnect();
@@ -51,7 +51,7 @@ private:
String updateTopic;
String sensorNamePrefix;
bool l1Init, l2Init, l2eInit, l3Init, l3eInit, l4Init, l4eInit, rtInit, rteInit, pInit, sInit, rInit, fInit;
bool l1Init, l2Init, l2eInit, l3Init, l3eInit, l4Init, l4eInit, rtInit, rteInit, pInit, sInit, rInit, fInit, dInit;
bool tInit[32] = {false};
uint8_t priceImportInit = 0, priceExportInit = 0;
uint32_t lastThresholdPublish = 0;

View File

@@ -124,5 +124,6 @@ const HomeAssistantSensor SystemSensors[SystemSensorCount] PROGMEM = {
const HomeAssistantSensor TemperatureSensor PROGMEM = {"Temperature sensor %s", "/temperatures", "temperatures['%s']", 900, "°C", "temperature", "measurement", ""};
const HomeAssistantSensor DataSensor PROGMEM = {"Data", "/data", "data", 900, "", "", "", ""};
#endif

View File

@@ -3,7 +3,6 @@
"stat_t" : "%s%s",
"uniq_id" : "%s_%s",
"obj_id" : "%s_%s",
"unit_of_meas" : "%s",
"val_tpl" : "{{ value_json.%s | is_defined }}",
"expire_after" : %d,
"dev" : {
@@ -13,5 +12,8 @@
"sw" : "%s",
"mf" : "%s",
"cu" : "%s"
}%s%s%s%s%s%s
}
%s%s%s
%s%s%s
%s%s%s
}

View File

@@ -20,7 +20,7 @@
#endif
void HomeAssistantMqttHandler::setHomeAssistantConfig(HomeAssistantConfig config, char* hostname) {
l1Init = l2Init = l2eInit = l3Init = l3eInit = l4Init = l4eInit = rtInit = rteInit = pInit = sInit = rInit = fInit = false;
l1Init = l2Init = l2eInit = l3Init = l3eInit = l4Init = l4eInit = rtInit = rteInit = pInit = sInit = rInit = fInit = dInit = false;
if(strlen(config.discoveryNameTag) > 0) {
snprintf_P(json, 128, PSTR("AMS reader (%s)"), config.discoveryNameTag);
@@ -70,18 +70,26 @@ void HomeAssistantMqttHandler::setHomeAssistantConfig(HomeAssistantConfig config
}
bool HomeAssistantMqttHandler::postConnect() {
if(!subTopic.isEmpty() && !mqtt.subscribe(subTopic)) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::ERROR))
#endif
debugger->printf_P(PSTR(" Unable to subscribe to to [%s]\n"), subTopic.c_str());
return false;
bool ret = true;
if(!statusTopic.isEmpty()) {
if(mqtt.subscribe(statusTopic)) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::ERROR))
#endif
debugger->printf_P(PSTR(" Subscribed to [%s]\n"), statusTopic.c_str());
} else {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::ERROR))
#endif
debugger->printf_P(PSTR(" Unable to subscribe to [%s]\n"), statusTopic.c_str());
ret = false;
}
}
return true;
return ret;
}
bool HomeAssistantMqttHandler::publish(AmsData* update, AmsData* previousState, EnergyAccounting* ea, PriceService* ps) {
if(pubTopic.isEmpty() || !mqtt.connected())
if(pubTopic.isEmpty() || !connected())
return false;
if(time(nullptr) < FirmwareVersion::BuildEpoch)
@@ -350,7 +358,7 @@ bool HomeAssistantMqttHandler::publishTemperatures(AmsConfiguration* config, HwT
}
bool HomeAssistantMqttHandler::publishPrices(PriceService* ps) {
if(pubTopic.isEmpty() || !mqtt.connected())
if(pubTopic.isEmpty() || !connected())
return false;
if(!ps->hasPrice())
return false;
@@ -486,7 +494,7 @@ bool HomeAssistantMqttHandler::publishPrices(PriceService* ps) {
}
bool HomeAssistantMqttHandler::publishSystem(HwTools* hw, PriceService* ps, EnergyAccounting* ea) {
if(pubTopic.isEmpty() || !mqtt.connected())
if(pubTopic.isEmpty() || !connected())
return false;
publishSystemSensors();
@@ -533,7 +541,6 @@ void HomeAssistantMqttHandler::publishSensor(const HomeAssistantSensor sensor) {
mqttConfig.publishTopic, sensor.topic,
deviceUid.c_str(), uid.c_str(),
deviceUid.c_str(), uid.c_str(),
sensor.uom,
sensor.path,
sensor.ttl,
deviceUid.c_str(),
@@ -542,13 +549,20 @@ void HomeAssistantMqttHandler::publishSensor(const HomeAssistantSensor sensor) {
FirmwareVersion::VersionString,
manufacturer.c_str(),
deviceUrl.c_str(),
strlen_P(sensor.devcl) > 0 ? ",\"dev_cla\":\"" : "",
strlen_P(sensor.devcl) > 0 ? (char *) FPSTR(sensor.devcl) : "",
strlen_P(sensor.devcl) > 0 ? "\"" : "",
strlen_P(sensor.stacl) > 0 ? ",\"stat_cla\":\"" : "",
strlen_P(sensor.stacl) > 0 ? (char *) FPSTR(sensor.stacl) : "",
strlen_P(sensor.stacl) > 0 ? "\"" : ""
strlen_P(sensor.stacl) > 0 ? "\"" : "",
strlen_P(sensor.uom) > 0 ? ",\"unit_of_meas\":\"" : "",
strlen_P(sensor.uom) > 0 ? (char *) FPSTR(sensor.uom) : "",
strlen_P(sensor.uom) > 0 ? "\"" : ""
);
mqtt.publish(sensorTopic + "/" + deviceUid + "_" + uid + "/config", json, true, 0);
loop();
}
@@ -823,8 +837,26 @@ uint8_t HomeAssistantMqttHandler::getFormat() {
return 4;
}
bool HomeAssistantMqttHandler::publishRaw(String data) {
return false;
bool HomeAssistantMqttHandler::publishRaw(uint8_t* raw, size_t length) {
if(strlen(mqttConfig.publishTopic) == 0 || !mqtt.connected())
return false;
if(length <= 0 || length > BufferSize) return false;
if(!dInit) {
// Not sure how this sensor should be defined in HA, so skipping for now
//publishSensor(DataSensor);
dInit = true;
}
String str = toHex(raw, length);
snprintf_P(json, BufferSize, PSTR("{\"data\":\"%s\"}"), str.c_str());
char topic[192];
snprintf_P(topic, 192, PSTR("%s/data"), mqttConfig.publishTopic);
bool ret = mqtt.publish(topic, json);
loop();
return ret;
}
bool HomeAssistantMqttHandler::publishFirmware() {
@@ -857,7 +889,7 @@ void HomeAssistantMqttHandler::onMessage(String &topic, String &payload) {
if (debugger->isActive(RemoteDebug::INFO))
#endif
debugger->printf_P(PSTR("Received online status from HA, resetting sensor status\n"));
l1Init = l2Init = l2eInit = l3Init = l3eInit = l4Init = l4eInit = rtInit = rteInit = pInit = sInit = rInit = false;
l1Init = l2Init = l2eInit = l3Init = l3eInit = l4Init = l4eInit = rtInit = rteInit = pInit = sInit = rInit = fInit = dInit = false;
for(uint8_t i = 0; i < 32; i++) tInit[i] = false;
priceImportInit = 0;
priceExportInit = 0;

View File

@@ -23,11 +23,9 @@ public:
bool publishTemperatures(AmsConfiguration*, HwTools*);
bool publishPrices(PriceService*);
bool publishSystem(HwTools* hw, PriceService* ps, EnergyAccounting* ea);
bool publishRaw(String data);
bool publishRaw(uint8_t* raw, size_t length);
bool publishFirmware();
bool postConnect();
void onMessage(String &topic, String &payload);
uint8_t getFormat();

View File

@@ -10,22 +10,11 @@
#include "Uptime.h"
#include "AmsJsonGenerator.h"
bool JsonMqttHandler::postConnect() {
if(!subTopic.isEmpty() && !mqtt.subscribe(subTopic)) {
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::ERROR))
#endif
debugger->printf_P(PSTR(" Unable to subscribe to to [%s]\n"), subTopic.c_str());
return false;
}
return true;
}
bool JsonMqttHandler::publish(AmsData* update, AmsData* previousState, EnergyAccounting* ea, PriceService* ps) {
if(strlen(mqttConfig.publishTopic) == 0) {
return false;
}
if(!mqtt.connected()) {
if(!connected()) {
return false;
}
@@ -306,7 +295,7 @@ bool JsonMqttHandler::publishTemperatures(AmsConfiguration* config, HwTools* hw)
}
bool JsonMqttHandler::publishPrices(PriceService* ps) {
if(strlen(mqttConfig.publishTopic) == 0 || !mqtt.connected())
if(strlen(mqttConfig.publishTopic) == 0 || !connected())
return false;
if(!ps->hasPrice())
return false;
@@ -455,7 +444,7 @@ bool JsonMqttHandler::publishPrices(PriceService* ps) {
}
bool JsonMqttHandler::publishSystem(HwTools* hw, PriceService* ps, EnergyAccounting* ea) {
if(strlen(mqttConfig.publishTopic) == 0 || !mqtt.connected())
if(strlen(mqttConfig.publishTopic) == 0 || !connected())
return false;
snprintf_P(json, BufferSize, PSTR("{\"id\":\"%s\",\"name\":\"%s\",\"up\":%d,\"vcc\":%.3f,\"rssi\":%d,\"temp\":%.2f,\"version\":\"%s\"}"),
@@ -483,8 +472,20 @@ uint8_t JsonMqttHandler::getFormat() {
return 0;
}
bool JsonMqttHandler::publishRaw(String data) {
return false;
bool JsonMqttHandler::publishRaw(uint8_t* raw, size_t length) {
if(strlen(mqttConfig.publishTopic) == 0 || !mqtt.connected())
return false;
if(length <= 0 || length > BufferSize) return false;
String str = toHex(raw, length);
snprintf_P(json, BufferSize, PSTR("{\"data\":\"%s\"}"), str.c_str());
char topic[192];
snprintf_P(topic, 192, PSTR("%s/data"), mqttConfig.publishTopic);
bool ret = mqtt.publish(topic, json);
loop();
return ret;
}
bool JsonMqttHandler::publishFirmware() {
@@ -502,7 +503,7 @@ bool JsonMqttHandler::publishFirmware() {
}
void JsonMqttHandler::onMessage(String &topic, String &payload) {
if(strlen(mqttConfig.publishTopic) == 0 || !mqtt.connected())
if(strlen(mqttConfig.publishTopic) == 0 || !connected())
return;
#if defined(AMS_REMOTE_DEBUG)

View File

@@ -13,6 +13,7 @@
#endif
#include "AmsData.h"
#include "AmsConfiguration.h"
#include "AmsMqttHandler.h"
class MeterCommunicator {
public:
@@ -24,6 +25,13 @@ public:
virtual bool isConfigChanged();
virtual void ackConfigChanged();
virtual void getCurrentConfig(MeterConfig& meterConfig);
virtual void setMqttHandlerForDebugging(AmsMqttHandler* mqttHandler) {
this->mqttDebug = mqttHandler;
};
protected:
AmsMqttHandler* mqttDebug = NULL;
};
#endif

View File

@@ -14,7 +14,7 @@
#include "AmsConfiguration.h"
#include "DataParsers.h"
#include "Timezone.h"
#include "PassthroughMqttHandler.h"
#include "AmsMqttHandler.h"
#if defined(ESP8266)
#include "SoftwareSerial.h"
@@ -36,7 +36,9 @@ public:
bool isConfigChanged();
void ackConfigChanged();
void getCurrentConfig(MeterConfig& meterConfig);
void setPassthroughMqttHandler(PassthroughMqttHandler*);
void setTimezone(Timezone* tz) {
this->tz = tz;
};
HardwareSerial* getHwSerial();
void rxerr(int err);
@@ -51,8 +53,6 @@ protected:
bool configChanged = false;
Timezone* tz;
PassthroughMqttHandler* pt = NULL;
uint8_t *hanBuffer = NULL;
uint16_t hanBufferSize = 0;
Stream *hanSerial;

View File

@@ -27,13 +27,13 @@ IEC6205675::IEC6205675(const char* d, Timezone* tz, uint8_t useMeterType, MeterC
// Kaifa special case...
if(useMeterType == AmsTypeKaifa && data->base.type == CosemTypeDLongUnsigned) {
this->packageTimestamp = this->packageTimestamp > 0 ? tz->toUTC(this->packageTimestamp) : 0;
this->packageTimestamp = this->packageTimestamp > 0 && tz != NULL ? tz->toUTC(this->packageTimestamp) : 0;
listType = 1;
meterType = AmsTypeKaifa;
activeImportPower = ntohl(data->dlu.data);
lastUpdateMillis = millis64();
} else if(data->base.type == CosemTypeOctetString) {
this->packageTimestamp = this->packageTimestamp > 0 ? tz->toUTC(this->packageTimestamp) : 0;
this->packageTimestamp = this->packageTimestamp > 0 && tz != NULL ? tz->toUTC(this->packageTimestamp) : 0;
memcpy(str, data->oct.data, data->oct.length);
str[data->oct.length] = 0x00;
@@ -123,7 +123,7 @@ IEC6205675::IEC6205675(const char* d, Timezone* tz, uint8_t useMeterType, MeterC
if(data->oct.length == 0x0C) {
AmsOctetTimestamp* amst = (AmsOctetTimestamp*) data;
time_t ts = decodeCosemDateTime(amst->dt);
meterTimestamp = tz->toUTC(ts);
meterTimestamp = tz != NULL ? tz->toUTC(ts) : ts;
}
}
}
@@ -1121,7 +1121,7 @@ time_t IEC6205675::adjustForKnownIssues(CosemDateTime dt, Timezone* tz, uint8_t
// 21.09.24, the clock is now correct for Aidon
// 23.10.25, the clock is now correct for Kamstrup
ts -= 3600;
} else {
} else if(tz != NULL) {
// Adjust from localtime to UTC
ts = tz->toUTC(ts);
}

View File

@@ -174,8 +174,8 @@ bool PassiveMeterCommunicator::loop() {
lastError = pos;
printHanReadError(pos);
len += hanSerial->readBytes(hanBuffer+len, hanBufferSize-len);
if(pt != NULL) {
pt->publishBytes(hanBuffer, len);
if(mqttDebug != NULL) {
mqttDebug->publishRaw(hanBuffer, len);
}
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::VERBOSE))
@@ -229,8 +229,8 @@ AmsData* PassiveMeterCommunicator::getData(AmsData& meterState) {
char* payload = ((char *) (hanBuffer)) + pos;
if(maxDetectedPayloadSize < pos) maxDetectedPayloadSize = pos;
if(ctx.type == DATA_TAG_DLMS) {
if(pt != NULL) {
pt->publishBytes((uint8_t*) payload, ctx.length);
if(mqttDebug != NULL) {
mqttDebug->publishRaw((uint8_t*) payload, ctx.length);
}
#if defined(AMS_REMOTE_DEBUG)
@@ -405,8 +405,8 @@ int16_t PassiveMeterCommunicator::unwrapData(uint8_t *buf, DataParserContext &co
if (debugger->isActive(RemoteDebug::VERBOSE))
#endif
debugger->printf_P(PSTR("HDLC frame:\n"));
if(pt != NULL) {
pt->publishBytes(buf, curLen);
if(mqttDebug != NULL) {
mqttDebug->publishRaw(buf, curLen);
}
break;
case DATA_TAG_MBUS:
@@ -414,8 +414,8 @@ int16_t PassiveMeterCommunicator::unwrapData(uint8_t *buf, DataParserContext &co
if (debugger->isActive(RemoteDebug::VERBOSE))
#endif
debugger->printf_P(PSTR("MBUS frame:\n"));
if(pt != NULL) {
pt->publishBytes(buf, curLen);
if(mqttDebug != NULL) {
mqttDebug->publishRaw(buf, curLen);
}
break;
case DATA_TAG_GBT:
@@ -447,8 +447,8 @@ int16_t PassiveMeterCommunicator::unwrapData(uint8_t *buf, DataParserContext &co
if (debugger->isActive(RemoteDebug::VERBOSE))
#endif
debugger->printf_P(PSTR("DSMR frame:\n"));
if(pt != NULL) {
pt->publishString((char*) buf);
if(mqttDebug != NULL) {
mqttDebug->publishRaw(buf, curLen);
}
break;
case DATA_TAG_SNRM:
@@ -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,14 +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.Minute = 0;
tm.Second = 0;
breakTime(makeTime(tm) + (hour * SECS_PER_HOUR), tm);
tm.Minute = tm.Second = 0;
float value = PRICE_NO_VALUE;
for (uint8_t i = 0; i < priceConfig.size(); i++) {
@@ -429,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();
@@ -476,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,
@@ -513,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,
@@ -546,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) {
@@ -568,9 +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]);
float value = intval / 10000.0;
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));
float value = ntohl(intval) / 10000.0;
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::VERBOSE))
#endif
@@ -578,9 +582,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]);
float value = intval / 10000.0;
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;
#if defined(AMS_REMOTE_DEBUG)
if (debugger->isActive(RemoteDebug::VERBOSE))
#endif
@@ -755,6 +760,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);
};
@@ -26,7 +26,7 @@ public:
bool publishTemperatures(AmsConfiguration*, HwTools*);
bool publishPrices(PriceService*);
bool publishSystem(HwTools* hw, PriceService* ps, EnergyAccounting* ea);
bool publishRaw(String data);
bool publishRaw(uint8_t* raw, size_t length);
void onMessage(String &topic, String &payload);

View File

@@ -10,7 +10,7 @@
#include "FirmwareVersion.h"
bool RawMqttHandler::publish(AmsData* update, AmsData* previousState, EnergyAccounting* ea, PriceService* ps) {
if(topic.isEmpty() || !mqtt.connected())
if(topic.isEmpty() || !connected())
return false;
AmsData data;
@@ -237,7 +237,7 @@ bool RawMqttHandler::publishTemperatures(AmsConfiguration* config, HwTools* hw)
}
bool RawMqttHandler::publishPrices(PriceService* ps) {
if(topic.isEmpty() || !mqtt.connected())
if(topic.isEmpty() || !connected())
return false;
if(!ps->hasPrice())
return false;
@@ -369,7 +369,7 @@ bool RawMqttHandler::publishPrices(PriceService* ps) {
}
bool RawMqttHandler::publishSystem(HwTools* hw, PriceService* ps, EnergyAccounting* ea) {
if(topic.isEmpty() || !mqtt.connected())
if(topic.isEmpty() || !connected())
return false;
mqtt.publish(topic + "/id", WiFi.macAddress(), true, 0);
@@ -396,8 +396,16 @@ uint8_t RawMqttHandler::getFormat() {
return full ? 3 : 2;
}
bool RawMqttHandler::publishRaw(String data) {
return false;
bool RawMqttHandler::publishRaw(uint8_t* raw, size_t length) {
if(topic.isEmpty() || !connected())
return false;
if(length <= 0 || length > BufferSize) return false;
String str = toHex(raw, length);
bool ret = mqtt.publish(topic + "/data", str);
loop();
return ret;
}
void RawMqttHandler::onMessage(String &topic, String &payload) {

File diff suppressed because one or more lines are too long

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

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

@@ -16,7 +16,8 @@
"day" : "day",
"days" : "days",
"month" : "month",
"unknown" : "Unknown"
"unknown" : "Unknown",
"now" : "Now"
},
"btn" : {
"reboot" : "Reboot",

View File

@@ -777,11 +777,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"
);

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

@@ -655,7 +655,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();
@@ -809,6 +809,9 @@ void handleNtp() {
ds.setTimezone(tz);
ea.setTimezone(tz);
ps->setTimezone(tz);
if(passiveMc != NULL) {
passiveMc->setTimezone(tz);
}
}
config.ackNtpChange();
@@ -902,7 +905,11 @@ void handleCloud() {
unsigned long handleMqtt() {
unsigned long start = millis();
if (mqttEnabled || config.isMqttChanged()) {
if(!networkConnected) {
if(mqttHandler != NULL) {
mqttHandler->disconnect();
}
} else if (mqttEnabled || config.isMqttChanged()) {
if(mqttHandler == NULL || !mqttHandler->connected() || config.isMqttChanged()) {
if(mqttHandler != NULL && config.isMqttChanged()) {
mqttHandler->disconnect();
@@ -1037,6 +1044,15 @@ void handleMeterConfig() {
debugE_P(PSTR("Unknown meter source selected: %d"), meterConfig.source);
}
ws.setMeterConfig(meterConfig.distributionSystem, meterConfig.mainFuse, meterConfig.productionCapacity);
if(mc != NULL &&
#if defined(AMS_REMOTE_DEBUG)
Debug.isActive(RemoteDebug::DEBUG)
#else
false // Never send debug data
#endif
) {
mc->setMqttHandlerForDebugging(mqttHandler);
}
config.ackMeterChanged();
}
}
@@ -1553,6 +1569,15 @@ void postConnect() {
if(!debug.telnet) {
Debug.stop();
}
if(mc != NULL &&
#if defined(AMS_REMOTE_DEBUG)
Debug.isActive(RemoteDebug::DEBUG)
#else
false // Never send debug data
#endif
) {
mc->setMqttHandlerForDebugging(mqttHandler);
}
} else {
Debug.stop();
}
@@ -1628,6 +1653,7 @@ void MQTT_connect() {
if(mqttHandler->getFormat() != mqttConfig.payloadFormat) {
delete mqttHandler;
mqttHandler = NULL;
mc->setMqttHandlerForDebugging(NULL);
} else if(config.isMqttChanged()) {
mqttHandler->setConfig(mqttConfig);
switch(mqttConfig.payloadFormat) {
@@ -1676,10 +1702,20 @@ void MQTT_connect() {
break;
case 255:
mqttHandler = new PassthroughMqttHandler(mqttConfig, &Debug, (char*) commonBuffer, &updater);
mc->setMqttHandlerForDebugging(mqttHandler);
break;
}
}
ws.setMqttHandler(mqttHandler);
if(mc != NULL &&
#if defined(AMS_REMOTE_DEBUG)
Debug.isActive(RemoteDebug::DEBUG)
#else
false // Never send debug data
#endif
) {
mc->setMqttHandlerForDebugging(mqttHandler);
}
if(mqttHandler != NULL) {
mqttHandler->connect();