diff --git a/include/Configuration.h b/include/Configuration.h index 01c0f5b..52eef24 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -4,7 +4,7 @@ #include #define CONFIG_FILENAME "/config.bin" -#define CONFIG_VERSION 0x00011100 // 0.1.17 // make sure to clean all after change +#define CONFIG_VERSION 0x00011200 // 0.1.18 // make sure to clean all after change #define WIFI_MAX_SSID_STRLEN 31 #define WIFI_MAX_PASSWORD_STRLEN 64 @@ -65,6 +65,11 @@ struct CONFIG_T { uint64_t Dtu_Serial; uint32_t Dtu_PollInterval; uint8_t Dtu_PaLevel; + + bool Mqtt_Hass_Enabled; + bool Mqtt_Hass_Retain; + char Mqtt_Hass_Topic[MQTT_MAX_TOPIC_STRLEN + 1]; + bool Mqtt_Hass_IndividualPanels; }; class ConfigurationClass { diff --git a/include/MqttHassPublishing.h b/include/MqttHassPublishing.h new file mode 100644 index 0000000..7a9cb8c --- /dev/null +++ b/include/MqttHassPublishing.h @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include "Configuration.h" +#include +#include +#include + +// mqtt discovery device classes +enum { + DEVICE_CLS_NONE = 0, + DEVICE_CLS_CURRENT, + DEVICE_CLS_ENERGY, + DEVICE_CLS_PWR, + DEVICE_CLS_VOLTAGE, + DEVICE_CLS_FREQ, + DEVICE_CLS_TEMP, + DEVICE_CLS_POWER_FACTOR +}; +const char* const deviceClasses[] = { 0, "current", "energy", "power", "voltage", "frequency", "temperature", "power_factor" }; +enum { + STATE_CLS_NONE = 0, + STATE_CLS_MEASUREMENT, + STATE_CLS_TOTAL_INCREASING +}; +const char* const stateClasses[] = { 0, "measurement", "total_increasing" }; + +typedef struct { + uint8_t fieldId; // field id + uint8_t deviceClsId; // device class + uint8_t stateClsId; // state class +} byteAssign_fieldDeviceClass_t; + +const byteAssign_fieldDeviceClass_t deviceFieldAssignment[] = { + { FLD_UDC, DEVICE_CLS_VOLTAGE, STATE_CLS_MEASUREMENT }, + { FLD_IDC, DEVICE_CLS_CURRENT, STATE_CLS_MEASUREMENT }, + { FLD_PDC, DEVICE_CLS_PWR, STATE_CLS_MEASUREMENT }, + { FLD_YD, DEVICE_CLS_ENERGY, STATE_CLS_TOTAL_INCREASING }, + { FLD_YT, DEVICE_CLS_ENERGY, STATE_CLS_TOTAL_INCREASING }, + { FLD_UAC, DEVICE_CLS_VOLTAGE, STATE_CLS_MEASUREMENT }, + { FLD_IAC, DEVICE_CLS_CURRENT, STATE_CLS_MEASUREMENT }, + { FLD_PAC, DEVICE_CLS_PWR, STATE_CLS_MEASUREMENT }, + { FLD_F, DEVICE_CLS_FREQ, STATE_CLS_MEASUREMENT }, + { FLD_T, DEVICE_CLS_TEMP, STATE_CLS_MEASUREMENT }, + { FLD_PCT, DEVICE_CLS_POWER_FACTOR, STATE_CLS_MEASUREMENT }, + { FLD_EFF, DEVICE_CLS_NONE, STATE_CLS_NONE }, + { FLD_IRR, DEVICE_CLS_NONE, STATE_CLS_NONE } +}; +#define DEVICE_CLS_ASSIGN_LIST_LEN (sizeof(deviceFieldAssignment) / sizeof(byteAssign_fieldDeviceClass_t)) + +class MqttHassPublishingClass { +public: + void init(); + void loop(); + void publishConfig(); + +private: + void publishField(std::shared_ptr inv, uint8_t channel, byteAssign_fieldDeviceClass_t fieldType, bool clear = false); + + bool _wasConnected = false; +}; + +extern MqttHassPublishingClass MqttHassPublishing; \ No newline at end of file diff --git a/include/MqttPublishing.h b/include/MqttPublishing.h index 14bc14c..54a755e 100644 --- a/include/MqttPublishing.h +++ b/include/MqttPublishing.h @@ -11,9 +11,10 @@ public: void init(); void loop(); + static String getTopic(std::shared_ptr inv, uint8_t channel, uint8_t fieldId); + private: void publishField(std::shared_ptr inv, uint8_t channel, uint8_t fieldId); - String getTopic(std::shared_ptr inv, uint8_t channel, uint8_t fieldId); uint32_t _lastPublishStats[INV_MAX_COUNT]; uint32_t _lastPublish; diff --git a/include/MqttSettings.h b/include/MqttSettings.h index c89d523..407380b 100644 --- a/include/MqttSettings.h +++ b/include/MqttSettings.h @@ -14,6 +14,7 @@ public: void performReconnect(); bool getConnected(); void publish(String subtopic, String payload); + void publishHass(String subtopic, String payload); String getPrefix(); diff --git a/include/defaults.h b/include/defaults.h index be542b2..1d09157 100644 --- a/include/defaults.h +++ b/include/defaults.h @@ -36,4 +36,9 @@ #define DTU_SERIAL 0x99978563412 #define DTU_POLL_INTERVAL 5 -#define DTU_PA_LEVEL 0 \ No newline at end of file +#define DTU_PA_LEVEL 0 + +#define MQTT_HASS_ENABLED false +#define MQTT_HASS_RETAIN true +#define MQTT_HASS_TOPIC "homeassistant/" +#define MQTT_HASS_INDIVIDUALPANELS false \ No newline at end of file diff --git a/src/Configuration.cpp b/src/Configuration.cpp index e94e7f8..cbe2d54 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -49,6 +49,11 @@ void ConfigurationClass::init() config.Dtu_Serial = DTU_SERIAL; config.Dtu_PollInterval = DTU_POLL_INTERVAL; config.Dtu_PaLevel = DTU_PA_LEVEL; + + config.Mqtt_Hass_Enabled = MQTT_HASS_ENABLED; + config.Mqtt_Hass_Retain = MQTT_HASS_RETAIN; + strlcpy(config.Mqtt_Hass_Topic, MQTT_TOPIC, sizeof(config.Mqtt_Hass_Topic)); + config.Mqtt_Hass_IndividualPanels = MQTT_HASS_INDIVIDUALPANELS; } bool ConfigurationClass::write() @@ -128,6 +133,13 @@ void ConfigurationClass::migrate() init(); // Config will be completly incompatible after this update } + if (config.Cfg_Version < 0x00011200) { + config.Mqtt_Hass_Enabled = MQTT_HASS_ENABLED; + config.Mqtt_Hass_Retain = MQTT_HASS_RETAIN; + strlcpy(config.Mqtt_Hass_Topic, MQTT_HASS_TOPIC, sizeof(config.Mqtt_Hass_Topic)); + config.Mqtt_Hass_IndividualPanels = MQTT_HASS_INDIVIDUALPANELS; + } + config.Cfg_Version = CONFIG_VERSION; write(); } diff --git a/src/MqttHassPublishing.cpp b/src/MqttHassPublishing.cpp new file mode 100644 index 0000000..ceba1ca --- /dev/null +++ b/src/MqttHassPublishing.cpp @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Thomas Basler and others + */ +#include "MqttHassPublishing.h" +#include "ArduinoJson.h" +#include "MqttPublishing.h" +#include "MqttSettings.h" +#include "WiFiSettings.h" + +MqttHassPublishingClass MqttHassPublishing; + +void MqttHassPublishingClass::init() +{ +} + +void MqttHassPublishingClass::loop() +{ + if (MqttSettings.getConnected() && !_wasConnected) { + // Connection established + _wasConnected = true; + publishConfig(); + } else if (!MqttSettings.getConnected() && _wasConnected) { + // Connection lost + _wasConnected = false; + } +} + +void MqttHassPublishingClass::publishConfig() +{ + if (!Configuration.get().Mqtt_Hass_Enabled) { + return; + } + + if (!MqttSettings.getConnected() && Hoymiles.getRadio()->isIdle()) { + return; + } + + CONFIG_T& config = Configuration.get(); + + // Loop all inverters + for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) { + auto inv = Hoymiles.getInverterByPos(i); + + // Loop all channels + for (uint8_t c = 0; c <= inv->Statistics()->getChannelCount(); c++) { + for (uint8_t f = 0; f < DEVICE_CLS_ASSIGN_LIST_LEN; f++) { + bool clear = false; + if (c > 0 && !config.Mqtt_Hass_IndividualPanels) { + clear = true; + } + publishField(inv, c, deviceFieldAssignment[f], clear); + } + } + + yield(); + } +} + +void MqttHassPublishingClass::publishField(std::shared_ptr inv, uint8_t channel, byteAssign_fieldDeviceClass_t fieldType, bool clear) +{ + if (!inv->Statistics()->hasChannelFieldValue(channel, fieldType.fieldId)) { + return; + } + + char serial[sizeof(uint64_t) * 8 + 1]; + sprintf(serial, "%0lx%08lx", + ((uint32_t)((inv->serial() >> 32) & 0xFFFFFFFF)), + ((uint32_t)(inv->serial() & 0xFFFFFFFF))); + + String fieldName; + if (channel == CH0 && fieldType.fieldId == FLD_PDC) { + fieldName = "PowerDC"; + } else { + fieldName = inv->Statistics()->getChannelFieldName(channel, fieldType.fieldId); + } + + String configTopic = "sensor/dtu_" + String(serial) + + "/" + "ch" + String(channel) + "_" + fieldName + + "/config"; + + if (!clear) { + String stateTopic = MqttSettings.getPrefix() + MqttPublishing.getTopic(inv, channel, fieldType.fieldId); + const char* devCls = deviceClasses[fieldType.deviceClsId]; + const char* stateCls = stateClasses[fieldType.stateClsId]; + + String name; + if (channel == CH0) { + name = String(inv->name()) + " " + fieldName; + } else { + name = String(inv->name()) + " CH" + String(channel) + " " + fieldName; + } + + DynamicJsonDocument deviceDoc(512); + deviceDoc[F("name")] = inv->name(); + deviceDoc[F("ids")] = String(serial); + deviceDoc[F("cu")] = String(F("http://")) + String(WiFi.localIP().toString()); + deviceDoc[F("mf")] = F("OpenDTU"); + deviceDoc[F("mdl")] = inv->typeName(); + deviceDoc[F("sw")] = AUTO_GIT_HASH; + JsonObject deviceObj = deviceDoc.as(); + + DynamicJsonDocument root(1024); + root[F("name")] = name; + root[F("stat_t")] = stateTopic; + root[F("unit_of_meas")] = inv->Statistics()->getChannelFieldUnit(channel, fieldType.fieldId); + root[F("uniq_id")] = String(serial) + "_ch" + String(channel) + "_" + fieldName; + root[F("dev")] = deviceObj; + root[F("exp_aft")] = Configuration.get().Mqtt_PublishInterval * 2; + if (devCls != 0) { + root[F("dev_cla")] = devCls; + } + if (stateCls != 0) { + root[F("stat_cla")] = stateCls; + } + + char buffer[512]; + serializeJson(root, buffer); + MqttSettings.publishHass(configTopic, buffer); + } + else { + MqttSettings.publishHass(configTopic, ""); + } +} \ No newline at end of file diff --git a/src/MqttSettings.cpp b/src/MqttSettings.cpp index d66f80a..679ea7d 100644 --- a/src/MqttSettings.cpp +++ b/src/MqttSettings.cpp @@ -93,6 +93,13 @@ void MqttSettingsClass::publish(String subtopic, String payload) mqttClient.publish(topic.c_str(), 0, Configuration.get().Mqtt_Retain, payload.c_str()); } +void MqttSettingsClass::publishHass(String subtopic, String payload) +{ + String topic = Configuration.get().Mqtt_Hass_Topic; + topic += subtopic; + mqttClient.publish(topic.c_str(), 0, Configuration.get().Mqtt_Hass_Retain, payload.c_str()); +} + void MqttSettingsClass::init() { using namespace std::placeholders; diff --git a/src/WebApi_mqtt.cpp b/src/WebApi_mqtt.cpp index 174aa2e..b6a6f37 100644 --- a/src/WebApi_mqtt.cpp +++ b/src/WebApi_mqtt.cpp @@ -6,6 +6,7 @@ #include "ArduinoJson.h" #include "AsyncJson.h" #include "Configuration.h" +#include "MqttHassPublishing.h" #include "MqttSettings.h" #include "helper.h" @@ -39,6 +40,10 @@ void WebApiMqttClass::onMqttStatus(AsyncWebServerRequest* request) root[F("mqtt_retain")] = config.Mqtt_Retain; root[F("mqtt_lwt_topic")] = String(config.Mqtt_Topic) + config.Mqtt_LwtTopic; root[F("mqtt_publish_interval")] = config.Mqtt_PublishInterval; + root[F("mqtt_hass_enabled")] = config.Mqtt_Hass_Enabled; + root[F("mqtt_hass_retain")] = config.Mqtt_Hass_Retain; + root[F("mqtt_hass_topic")] = config.Mqtt_Hass_Topic; + root[F("mqtt_hass_individualpanels")] = config.Mqtt_Hass_IndividualPanels; response->setLength(); request->send(response); @@ -61,6 +66,10 @@ void WebApiMqttClass::onMqttAdminGet(AsyncWebServerRequest* request) root[F("mqtt_lwt_online")] = config.Mqtt_LwtValue_Online; root[F("mqtt_lwt_offline")] = config.Mqtt_LwtValue_Offline; root[F("mqtt_publish_interval")] = config.Mqtt_PublishInterval; + root[F("mqtt_hass_enabled")] = config.Mqtt_Hass_Enabled; + root[F("mqtt_hass_retain")] = config.Mqtt_Hass_Retain; + root[F("mqtt_hass_topic")] = config.Mqtt_Hass_Topic; + root[F("mqtt_hass_individualpanels")] = config.Mqtt_Hass_IndividualPanels; response->setLength(); request->send(response); @@ -98,7 +107,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) return; } - if (!(root.containsKey("mqtt_enabled") && root.containsKey("mqtt_hostname") && root.containsKey("mqtt_port") && root.containsKey("mqtt_username") && root.containsKey("mqtt_password") && root.containsKey("mqtt_topic") && root.containsKey("mqtt_retain") && root.containsKey("mqtt_lwt_topic") && root.containsKey("mqtt_lwt_online") && root.containsKey("mqtt_lwt_offline") && root.containsKey("mqtt_publish_interval"))) { + if (!(root.containsKey("mqtt_enabled") && root.containsKey("mqtt_hostname") && root.containsKey("mqtt_port") && root.containsKey("mqtt_username") && root.containsKey("mqtt_password") && root.containsKey("mqtt_topic") && root.containsKey("mqtt_retain") && root.containsKey("mqtt_lwt_topic") && root.containsKey("mqtt_lwt_online") && root.containsKey("mqtt_lwt_offline") && root.containsKey("mqtt_publish_interval") && root.containsKey("mqtt_hass_enabled") && root.containsKey("mqtt_hass_retain") && root.containsKey("mqtt_hass_topic") && root.containsKey("mqtt_hass_individualpanels"))) { retMsg[F("message")] = F("Values are missing!"); response->setLength(); request->send(response); @@ -166,6 +175,15 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) request->send(response); return; } + + if (root[F("mqtt_hass_enabled")].as()) { + if (root[F("mqtt_hass_topic")].as().length() > MQTT_MAX_TOPIC_STRLEN) { + retMsg[F("message")] = F("Hass topic must not longer then " STR(MQTT_MAX_TOPIC_STRLEN) " characters!"); + response->setLength(); + request->send(response); + return; + } + } } CONFIG_T& config = Configuration.get(); @@ -180,6 +198,10 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) strcpy(config.Mqtt_LwtValue_Online, root[F("mqtt_lwt_online")].as().c_str()); strcpy(config.Mqtt_LwtValue_Offline, root[F("mqtt_lwt_offline")].as().c_str()); config.Mqtt_PublishInterval = root[F("mqtt_publish_interval")].as(); + config.Mqtt_Hass_Enabled = root[F("mqtt_hass_enabled")].as(); + config.Mqtt_Hass_Retain = root[F("mqtt_hass_retain")].as(); + config.Mqtt_Hass_IndividualPanels = root[F("mqtt_hass_individualpanels")].as(); + strcpy(config.Mqtt_Hass_Topic, root[F("mqtt_hass_topic")].as().c_str()); Configuration.write(); retMsg[F("type")] = F("success"); @@ -189,4 +211,5 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) request->send(response); MqttSettings.performReconnect(); + MqttHassPublishing.publishConfig(); } \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index ac6a460..4f39f1b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -4,6 +4,7 @@ */ #include "Configuration.h" #include "Hoymiles.h" +#include "MqttHassPublishing.h" #include "MqttPublishing.h" #include "MqttSettings.h" #include "NtpSettings.h" @@ -67,6 +68,7 @@ void setup() Serial.print(F("Initialize MqTT... ")); MqttSettings.init(); MqttPublishing.init(); + MqttHassPublishing.init(); Serial.println(F("done")); // Initialize WebApi @@ -104,6 +106,8 @@ void loop() yield(); MqttPublishing.loop(); yield(); + MqttHassPublishing.loop(); + yield(); WebApi.loop(); yield(); } \ No newline at end of file diff --git a/webapp/src/components/MqttAdminView.vue b/webapp/src/components/MqttAdminView.vue index b2f3755..fdc395c 100644 --- a/webapp/src/components/MqttAdminView.vue +++ b/webapp/src/components/MqttAdminView.vue @@ -18,10 +18,25 @@
MqTT Configuration
-
- - +
+ +
+
+ +
+
+
+ +
+ +
+
+ +
+
@@ -135,6 +150,42 @@
+ +
+
Home Assistant MQTT Auto Discovery Parameters
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+ +
+ +
+
+ +
+
+
+ +
+
+ @@ -163,7 +214,11 @@ export default defineComponent({ mqtt_retain: false, mqtt_lwt_topic: "", mqtt_lwt_online: "", - mqtt_lwt_offline: "" + mqtt_lwt_offline: "", + mqtt_hass_enabled: false, + mqtt_hass_retain: false, + mqtt_hass_topic: "", + mqtt_hass_individualpanels: false }, alertMessage: "", alertType: "info", diff --git a/webapp/src/components/MqttInfoView.vue b/webapp/src/components/MqttInfoView.vue index 35c98ba..7a1033b 100644 --- a/webapp/src/components/MqttInfoView.vue +++ b/webapp/src/components/MqttInfoView.vue @@ -63,6 +63,52 @@ +
+
Home Assistant MQTT Auto Discovery Configuration Summary
+
+
+ + + + + + + + + + + + + + + + + + + +
Status + enabled + disabled +
Base Topic{{ mqttDataList.mqtt_hass_topic }}
Retain + enabled + disabled +
Individual Panels + enabled + disabled +
+
+
+
+
Runtime Summary
@@ -103,7 +149,11 @@ export default defineComponent({ mqtt_topic: "", mqtt_publish_interval: 0, mqtt_retain: false, - mqtt_connected: false + mqtt_connected: false, + mqtt_hass_enabled: false, + mqtt_hass_retain: false, + mqtt_hass_topic: "", + mqtt_hass_individualpanels: false }, }; }, diff --git a/webapp_dist/js/app.js.gz b/webapp_dist/js/app.js.gz index 8cad885..df258bf 100644 Binary files a/webapp_dist/js/app.js.gz and b/webapp_dist/js/app.js.gz differ