diff --git a/src/AmsToMqttBridge.ino b/src/AmsToMqttBridge.ino index 87da55bd..c45c1d54 100644 --- a/src/AmsToMqttBridge.ino +++ b/src/AmsToMqttBridge.ino @@ -37,6 +37,7 @@ ADC_MODE(ADC_VCC); #include "mqtt/JsonMqttHandler.h" #include "mqtt/RawMqttHandler.h" #include "mqtt/DomoticzMqttHandler.h" +#include "mqtt/HomeAssistantMqttHandler.h" #include "Uptime.h" @@ -1057,6 +1058,9 @@ void MQTT_connect() { config.getDomoticzConfig(domo); mqttHandler = new DomoticzMqttHandler(mqtt, domo); break; + case 4: + mqttHandler = new HomeAssistantMqttHandler(mqtt, mqttConfig.clientId, mqttConfig.publishTopic, &hw); + break; } if(mqttConfig.ssl) { diff --git a/src/mqtt/HomeAssistantMqttHandler.cpp b/src/mqtt/HomeAssistantMqttHandler.cpp new file mode 100644 index 00000000..511e28f1 --- /dev/null +++ b/src/mqtt/HomeAssistantMqttHandler.cpp @@ -0,0 +1,272 @@ +#include "HomeAssistantMqttHandler.h" +#include "hexutils.h" +#include "Uptime.h" +#include "web/root/ha1_json.h" +#include "web/root/ha2_json.h" +#include "web/root/ha3_json.h" +#include "web/root/jsonsys_json.h" +#include "web/root/jsonprices_json.h" +#include "web/root/hadiscover1_json.h" +#include "web/root/hadiscover2_json.h" + +bool HomeAssistantMqttHandler::publish(AmsData* data, AmsData* previousState) { + if(topic.isEmpty() || !mqtt->connected()) + return false; + + listType = data->getListType(); // for discovery stuff in publishSystem() + if(data->getListType() == 3) { // publish energy counts + char json[256]; + snprintf_P(json, sizeof(json), HA2_JSON, + data->getActiveImportCounter(), + data->getActiveExportCounter(), + data->getReactiveImportCounter(), + data->getReactiveExportCounter(), + data->getMeterTimestamp() + ); + mqtt->publish(topic + "/energy", json); + } + if(data->getListType() == 1) { // publish power counts + char json[192]; + snprintf_P(json, sizeof(json), HA1_JSON, + data->getActiveImportPower() + ); + return mqtt->publish(topic + "/power", json); + } else if(data->getListType() == 2 || data->getListType() == 3) { // publish power counts and volts/amps + char json[768]; + snprintf_P(json, sizeof(json), HA3_JSON, + data->getListId().c_str(), + data->getMeterId().c_str(), + data->getMeterModel().c_str(), + data->getActiveImportPower(), + data->getReactiveImportPower(), + data->getActiveExportPower(), + data->getReactiveExportPower(), + data->getL1Current(), + data->getL2Current(), + data->getL3Current(), + data->getL1Voltage(), + data->getL2Voltage(), + data->getL3Voltage(), + data->getPowerFactor() == 0 ? 1 : data->getPowerFactor(), + data->getPowerFactor() == 0 ? 1 : data->getL1PowerFactor(), + data->getPowerFactor() == 0 ? 1 : data->getL2PowerFactor(), + data->getPowerFactor() == 0 ? 1 : data->getL3PowerFactor() + ); + return mqtt->publish(topic + "/power", json); + } + return false; +} + +bool HomeAssistantMqttHandler::publishTemperatures(AmsConfiguration* config, HwTools* hw) { + int count = hw->getTempSensorCount(); + if(count == 0) return false; + + int size = 32 + (count * 26); + + char buf[size]; + snprintf(buf, 24, "{\"temperatures\":{"); + + for(int i = 0; i < count; i++) { + TempSensorData* data = hw->getTempSensorData(i); + if(data != NULL) { + char* pos = buf+strlen(buf); + snprintf(pos, 26, "\"%s\":%.2f,", + toHex(data->address, 8).c_str(), + data->lastRead + ); + data->changed = false; + delay(1); + } + } + char* pos = buf+strlen(buf); + snprintf(count == 0 ? pos : pos-1, 8, "}}"); + return mqtt->publish(topic + "/temperatures", buf); +} + +bool HomeAssistantMqttHandler::publishPrices(EntsoeApi* eapi) { + if(topic.isEmpty() || !mqtt->connected()) + return false; + if(strlen(eapi->getToken()) == 0) + return false; + + time_t now = time(nullptr); + + float min1hr, min3hr, min6hr; + int8_t min1hrIdx = -1, min3hrIdx = -1, min6hrIdx = -1; + float min = INT16_MAX, max = INT16_MIN; + float values[24] = {0}; + for(uint8_t i = 0; i < 24; i++) { + float val = eapi->getValueForHour(now, i); + values[i] = val; + + if(val == ENTSOE_NO_VALUE) break; + + if(val < min) min = val; + if(val > max) max = val; + + if(min1hrIdx == -1 || min1hr > val) { + min1hr = val; + min1hrIdx = i; + } + + if(i >= 2) { + i -= 2; + float val1 = values[i++]; + float val2 = values[i++]; + float val3 = val; + if(val1 == ENTSOE_NO_VALUE || val2 == ENTSOE_NO_VALUE || val3 == ENTSOE_NO_VALUE) continue; + float val3hr = val1+val2+val3; + if(min3hrIdx == -1 || min3hr > val3hr) { + min3hr = val3hr; + min3hrIdx = i-2; + } + } + + if(i >= 5) { + i -= 5; + float val1 = values[i++]; + float val2 = values[i++]; + float val3 = values[i++]; + float val4 = values[i++]; + float val5 = values[i++]; + float val6 = val; + if(val1 == ENTSOE_NO_VALUE || val2 == ENTSOE_NO_VALUE || val3 == ENTSOE_NO_VALUE || val4 == ENTSOE_NO_VALUE || val5 == ENTSOE_NO_VALUE || val6 == ENTSOE_NO_VALUE) continue; + float val6hr = val1+val2+val3+val4+val5+val6; + if(min6hrIdx == -1 || min6hr > val6hr) { + min6hr = val6hr; + min6hrIdx = i-5; + } + } + + } + + char ts1hr[21]; + if(min1hrIdx > -1) { + time_t ts = now + (SECS_PER_HOUR * min1hrIdx); + //Serial.printf("1hr: %d %lu\n", min1hrIdx, ts); + tmElements_t tm; + breakTime(ts, tm); + sprintf(ts1hr, "%04d-%02d-%02dT%02d:00:00Z", tm.Year+1970, tm.Month, tm.Day, tm.Hour); + } + char ts3hr[21]; + if(min3hrIdx > -1) { + time_t ts = now + (SECS_PER_HOUR * min3hrIdx); + //Serial.printf("3hr: %d %lu\n", min3hrIdx, ts); + tmElements_t tm; + breakTime(ts, tm); + sprintf(ts3hr, "%04d-%02d-%02dT%02d:00:00Z", tm.Year+1970, tm.Month, tm.Day, tm.Hour); + } + char ts6hr[21]; + if(min6hrIdx > -1) { + time_t ts = now + (SECS_PER_HOUR * min6hrIdx); + //Serial.printf("6hr: %d %lu\n", min6hrIdx, ts); + tmElements_t tm; + breakTime(ts, tm); + sprintf(ts6hr, "%04d-%02d-%02dT%02d:00:00Z", tm.Year+1970, tm.Month, tm.Day, tm.Hour); + } + + char json[384]; + snprintf_P(json, sizeof(json), JSONPRICES_JSON, + WiFi.macAddress().c_str(), + values[0], + values[1], + values[2], + values[3], + values[4], + values[5], + values[6], + values[7], + values[8], + values[9], + values[10], + values[11], + min == INT16_MAX ? 0.0 : min, + max == INT16_MIN ? 0.0 : max, + ts1hr, + ts3hr, + ts6hr + ); + return mqtt->publish(topic + "/prices", json); +} + +bool HomeAssistantMqttHandler::publishSystem(HwTools* hw) { + if(topic.isEmpty() || !mqtt->connected()){ + sequence = 0; + return false; + } + + if(sequence % 3 == 0){ + char json[192]; + snprintf_P(json, sizeof(json), JSONSYS_JSON, + WiFi.macAddress().c_str(), + clientId.c_str(), + (uint32_t) (millis64()/1000), + hw->getVcc(), + hw->getWifiRssi(), + hw->getTemperature() + ); + mqtt->publish(topic + "/state", json); + } + + if(sequence % 60 == 1 && listType > 1){ // every 60 ams message, publish mqtt discovery. TODO: publish once with retain + char json[512]; + String haTopic = "homeassistant/sensor/"; // home-assistant discovery topic + String haUID = "ams-3a08"; // unit identity (wifi hostname) + + int sensors = 17; + String topics[sensors] = {"/state", "/state", "/state", "/power", "/power", "/power", "/power", "/power", "/power", "/power", "/power", "/power", "/power", "/energy", "/energy", "/energy", "/energy"}; + String names[sensors] = {"Status", "Supply volt", "Temperature", "Active import", "Reactive import", "Active export", "Reactive export", "L1 current", "L2 current", "L3 current", + "L1 voltage", "L2 voltage", "L3 voltage", "Accumulated active import", "Accumulated active export", "Accumulated reactive import", "Accumulated reactive export"}; + String params[sensors] = {"rssi", "vcc", "temp", "P", "Q", "PO", "QO", "I1", "I2", "I3", "U1", "U2", "U3", "tPI", "tPO", "tQI", "tQO"}; + String uom[sensors] = {"dBm", "V", "C", "W", "W", "W", "W", "A", "A", "A", "V", "V", "V", "kWh", "kWh", "kWh", "kWh"}; + String devcl[sensors] = {"signal_strength", "voltage", "temperature", "power", "power", "power", "power", "current", "current", "current", "voltage", "voltage", "voltage", "energy", "energy", "energy", "energy"}; + String stacl[sensors] = {"", "", "", "measurement", "measurement", "measurement", "measurement", "", "", "", "", "", "", "total_increasing", "total_increasing", "total_increasing", "total_increasing"}; + //String category[sensors] = {"Diagnostic", "Diagnostic", "Diagnostic", "Power", "Power", "Power", "Power", "Voltage", "Voltage", "Voltage", "Current", "Current", "Current", "Energy", "Energy", "Energy", "Energy"}; + + String haName = "AMS reader"; + String haModel = "ESP32"; + String haVersion = "2.0.0"; + String haManuf = "AmsToMqttBridge"; + String haUrl = "http://" + haUID + ".local/"; + + for(int i=0;i 0) { // TODO: reduce to single JSON, state_class: null (witout quotation). or make it some extra optional string that us appended + snprintf_P(json, sizeof(json), HADISCOVER2_JSON, + names[i].c_str(), // name + (topic + topics[i]).c_str(), // state_topic + (haUID + "_" + params[i]).c_str(), // unique_id + (haUID + "_" + params[i]).c_str(), // object_id + uom[i].c_str(), // unit_of_measurement + params[i].c_str(), // value_template + devcl[i].c_str(), // device_class + haUID.c_str(), // dev ids + haName.c_str(), // name + haModel.c_str(), // model + haVersion.c_str(), // fw version + haManuf.c_str(), // manufacturer + haUrl.c_str(), // configuration_url + stacl[i].c_str() // state_class + ); + } else { + snprintf_P(json, sizeof(json), HADISCOVER1_JSON, + names[i].c_str(), // name + (topic + topics[i]).c_str(), // state_topic + (haUID + "_" + params[i]).c_str(), // unique_id + (haUID + "_" + params[i]).c_str(), // object_id + uom[i].c_str(), // unit_of_measurement + params[i].c_str(), // value_template + devcl[i].c_str(), // device_class + haUID.c_str(), // dev ids + haName.c_str(), // name + haModel.c_str(), // model + haVersion.c_str(), // fw version + haManuf.c_str(), // manufacturer + haUrl.c_str() // configuration_url + ); + } + mqtt->publish(haTopic + haUID + "_" + params[i] + "/config", json); + } + } + if(listType>0) sequence++; + return true; +} diff --git a/src/mqtt/HomeAssistantMqttHandler.h b/src/mqtt/HomeAssistantMqttHandler.h new file mode 100644 index 00000000..4725d303 --- /dev/null +++ b/src/mqtt/HomeAssistantMqttHandler.h @@ -0,0 +1,24 @@ +#ifndef _HOMEASSISTANTMQTTHANDLER_H +#define _HOMEASSISTANTMQTTHANDLER_H + +#include "AmsMqttHandler.h" + +class HomeAssistantMqttHandler : public AmsMqttHandler { +public: + HomeAssistantMqttHandler(MQTTClient* mqtt, const char* clientId, const char* topic, HwTools* hw) : AmsMqttHandler(mqtt) { + this->clientId = clientId; + this->topic = String(topic); + this->hw = hw; + }; + bool publish(AmsData* data, AmsData* previousState); + bool publishTemperatures(AmsConfiguration*, HwTools*); + bool publishPrices(EntsoeApi*); + bool publishSystem(HwTools*); + +private: + String clientId; + String topic; + HwTools* hw; + uint8_t sequence = 0, listType = 0; +}; +#endif diff --git a/src/web/AmsWebServer.cpp b/src/web/AmsWebServer.cpp index d5aeacb9..d6be95a5 100644 --- a/src/web/AmsWebServer.cpp +++ b/src/web/AmsWebServer.cpp @@ -547,7 +547,7 @@ void AmsWebServer::configMqttHtml() { html.replace("{u}", mqtt.username); html.replace("{pw}", mqtt.password); html.replace("{f}", String(mqtt.payloadFormat)); - for(int i = 0; i<4; i++) { + for(int i = 0; i<5; i++) { html.replace("{f" + String(i) + "}", mqtt.payloadFormat == i ? "selected" : ""); } diff --git a/web/ha1.json b/web/ha1.json new file mode 100644 index 00000000..62d98528 --- /dev/null +++ b/web/ha1.json @@ -0,0 +1,3 @@ +{ + "P" : %d, +} diff --git a/web/ha2.json b/web/ha2.json new file mode 100644 index 00000000..9ffcdaf5 --- /dev/null +++ b/web/ha2.json @@ -0,0 +1,7 @@ +{ + "tPI" : %.2f, + "tPO" : %.2f, + "tQI" : %.2f, + "tQO" : %.2f, + "rtc" : %llu +} diff --git a/web/ha3.json b/web/ha3.json new file mode 100644 index 00000000..41fad56b --- /dev/null +++ b/web/ha3.json @@ -0,0 +1,19 @@ +{ + "lv" : "%s", + "id" : "%s", + "type" : "%s", + "P" : %d, + "Q" : %d, + "PO" : %d, + "QO" : %d, + "I1" : %.2f, + "I2" : %.2f, + "I3" : %.2f, + "U1" : %.2f, + "U2" : %.2f, + "U3" : %.2f, + "PF" : %.2f, + "PF1" : %.2f, + "PF2" : %.2f, + "PF3" : %.2f +} diff --git a/web/hadiscover1.json b/web/hadiscover1.json new file mode 100644 index 00000000..984da268 --- /dev/null +++ b/web/hadiscover1.json @@ -0,0 +1,17 @@ +{ + "name" : "%s", + "stat_t" : "%s", + "uniq_id" : "%s", + "obj_id" : "%s", + "unit_of_meas" : "%s", + "val_tpl" : "{{value_json['%s']}}", + "dev_cla" : "%s", + "dev" : { + "ids" : [ "%s" ], + "name" : "%s", + "mdl" : "%s", + "sw" : "%s", + "mf" : "%s", + "cu" : "%s" + } +} \ No newline at end of file diff --git a/web/hadiscover2.json b/web/hadiscover2.json new file mode 100644 index 00000000..493312fa --- /dev/null +++ b/web/hadiscover2.json @@ -0,0 +1,18 @@ +{ + "name" : "%s", + "stat_t" : "%s", + "uniq_id" : "%s", + "obj_id" : "%s", + "unit_of_meas" : "%s", + "val_tpl" : "{{value_json['%s']}}", + "dev_cla" : "%s", + "dev" : { + "ids" : [ "%s" ], + "name" : "%s", + "mdl" : "%s", + "sw" : "%s", + "mf" : "%s", + "cu" : "%s" + }, + "stat_cla" : "%s" +} \ No newline at end of file diff --git a/web/jsonha.json b/web/jsonha.json new file mode 100644 index 00000000..dd3e4a5b --- /dev/null +++ b/web/jsonha.json @@ -0,0 +1,24 @@ +{ + "lv" : "%s", + "id" : "%s", + "type" : "%s", + "P" : %d, + "Q" : %d, + "PO" : %d, + "QO" : %d, + "I1" : %.2f, + "I2" : %.2f, + "I3" : %.2f, + "U1" : %.2f, + "U2" : %.2f, + "U3" : %.2f, + "PF" : %.2f, + "PF1" : %.2f, + "PF2" : %.2f, + "PF3" : %.2f, + "tPI" : %.2f, + "tPO" : %.2f, + "tQI" : %.2f, + "tQO" : %.2f, + "rtc" : %lu +} diff --git a/web/mqtt.html b/web/mqtt.html index 9fefb1d7..34d445a9 100644 --- a/web/mqtt.html +++ b/web/mqtt.html @@ -62,6 +62,7 @@ +