diff --git a/include/MqttHandleHass.h b/include/MqttHandleHass.h index 02ff8bd9a..41f7bf8c3 100644 --- a/include/MqttHandleHass.h +++ b/include/MqttHandleHass.h @@ -58,11 +58,20 @@ class MqttHandleHassClass { private: void loop(); void publish(const String& subtopic, const String& payload); - void publishField(std::shared_ptr inv, const ChannelType_t type, const ChannelNum_t channel, const byteAssign_fieldDeviceClass_t fieldType, const bool clear = false); + void publishDtuSensor(const char* name, const char* device_class, const char* category, const char* icon, const char* unit_of_measure, const char* subTopic); + void publishDtuBinarySensor(const char* name, const char* device_class, const char* category, const char* payload_on, const char* payload_off, const char* subTopic = ""); + void publishInverterField(std::shared_ptr inv, const ChannelType_t type, const ChannelNum_t channel, const byteAssign_fieldDeviceClass_t fieldType, const bool clear = false); void publishInverterButton(std::shared_ptr inv, const char* caption, const char* icon, const char* category, const char* deviceClass, const char* subTopic, const char* payload); void publishInverterNumber(std::shared_ptr inv, const char* caption, const char* icon, const char* category, const char* commandTopic, const char* stateTopic, const char* unitOfMeasure, const int16_t min = 1, const int16_t max = 100); void publishInverterBinarySensor(std::shared_ptr inv, const char* caption, const char* subTopic, const char* payload_on, const char* payload_off); - void createDeviceInfo(JsonObject& object, std::shared_ptr inv); + + static void createInverterInfo(DynamicJsonDocument& doc, std::shared_ptr inv); + static void createDtuInfo(DynamicJsonDocument& doc); + + static void createDeviceInfo(DynamicJsonDocument& doc, const String& name, const String& identifiers, const String& configuration_url, const String& manufacturer, const String& model, const String& sw_version, const String& via_device = ""); + + static String getDtuUniqueId(); + static String getDtuUrl(); Task _loopTask; diff --git a/lib/Hoymiles/src/Hoymiles.cpp b/lib/Hoymiles/src/Hoymiles.cpp index 3ff6ad9dc..06d125cf1 100644 --- a/lib/Hoymiles/src/Hoymiles.cpp +++ b/lib/Hoymiles/src/Hoymiles.cpp @@ -57,7 +57,6 @@ void HoymilesClass::loop() if (iv != nullptr && iv->getRadio()->isInitialized() && iv->getRadio()->isQueueEmpty()) { if (iv->getZeroValuesIfUnreachable() && !iv->isReachable()) { - Hoymiles.getMessageOutput()->println("Set runtime data to zero"); iv->Statistics()->zeroRuntimeData(); } diff --git a/lib/Hoymiles/src/parser/GridProfileParser.cpp b/lib/Hoymiles/src/parser/GridProfileParser.cpp index 9bd0dd600..42ca61d76 100644 --- a/lib/Hoymiles/src/parser/GridProfileParser.cpp +++ b/lib/Hoymiles/src/parser/GridProfileParser.cpp @@ -14,7 +14,7 @@ const std::array GridProfileParser::_pr { 0x0a, 0x00, "European - EN 50549-1:2019" }, { 0x0c, 0x00, "AT Tor - EU_EN50438" }, { 0x0d, 0x04, "France" }, - { 0x12, 0x00, "Poland" }, + { 0x12, 0x00, "Poland - EU_EN50438" }, { 0x37, 0x00, "Swiss - CH_NA EEA-NE7-CH2020" }, } }; @@ -45,7 +45,7 @@ constexpr GridProfileItemDefinition_t make_value(frozen::string Name, frozen::st return v; } -constexpr frozen::map itemDefinitions = { +constexpr frozen::map itemDefinitions = { { 0x01, make_value("Nominale Voltage (NV)", "V", 10) }, { 0x02, make_value("Low Voltage 1 (LV1)", "V", 10) }, { 0x03, make_value("LV1 Maximum Trip Time (MTT)", "s", 10) }, @@ -102,6 +102,7 @@ constexpr frozen::map itemDefinition { 0x36, make_value("WPF Function Activated", "bool", 1) }, { 0x37, make_value("Start of Power of WPF (Pstart)", "%Pn", 10) }, { 0x38, make_value("Power Factor ar Rated Power (PFRP)", "", 100) }, + { 0xff, make_value("Unkown Value", "", 1) }, }; const std::array GridProfileParser::_profileValues = { { @@ -123,6 +124,14 @@ const std::array GridProfileParse { 0x00, 0x03, 0x08 }, { 0x00, 0x03, 0x09 }, + // Version 0x08 + { 0x00, 0x08, 0x01 }, + { 0x00, 0x08, 0x02 }, + { 0x00, 0x08, 0x03 }, + { 0x00, 0x08, 0x04 }, + { 0x00, 0x08, 0x05 }, + { 0x00, 0x08, 0xff }, + // Version 0x0a { 0x00, 0x0a, 0x01 }, { 0x00, 0x0a, 0x02 }, @@ -159,6 +168,21 @@ const std::array GridProfileParse { 0x00, 0x0c, 0x0c }, { 0x00, 0x0c, 0x0a }, + // Version 0x35 + { 0x00, 0x35, 0x01 }, + { 0x00, 0x35, 0x02 }, + { 0x00, 0x35, 0x03 }, + { 0x00, 0x35, 0x04 }, + { 0x00, 0x35, 0x05 }, + { 0x00, 0x35, 0x06 }, + { 0x00, 0x35, 0x07 }, + { 0x00, 0x35, 0x08 }, + { 0x00, 0x35, 0x09 }, + { 0x00, 0x35, 0xff }, + { 0x00, 0x35, 0xff }, + { 0x00, 0x35, 0xff }, + { 0x00, 0x35, 0xff }, + // Frequency (H/LFRT) // Version 0x00 { 0x10, 0x00, 0x0d }, @@ -190,6 +214,15 @@ const std::array GridProfileParse { 0x30, 0x03, 0x1a }, { 0x30, 0x03, 0x1b }, + // Version 0x07 + { 0x30, 0x07, 0x17 }, + { 0x30, 0x07, 0x18 }, + { 0x30, 0x07, 0x19 }, + { 0x30, 0x07, 0x1a }, + { 0x30, 0x07, 0x1b }, + { 0x30, 0x07, 0xff }, + { 0x30, 0x07, 0xff }, + // Ramp Rates (RR) // Version 0x00 { 0x40, 0x00, 0x1c }, @@ -217,6 +250,13 @@ const std::array GridProfileParse { 0x50, 0x08, 0x22 }, { 0x50, 0x08, 0x23 }, + // Version 0x11 + { 0x50, 0x11, 0x1e }, + { 0x50, 0x11, 0x1f }, + { 0x50, 0x11, 0x20 }, + { 0x50, 0x11, 0x21 }, + { 0x50, 0x11, 0x22 }, + // Volt Watt (VW) // Version 0x00 { 0x60, 0x00, 0x24 }, @@ -336,7 +376,7 @@ std::list GridProfileParser::getProfile() const do { const uint8_t section_id = _payloadGridProfile[pos]; const uint8_t section_version = _payloadGridProfile[pos + 1]; - const int8_t section_start = getSectionStart(section_id, section_version); + const int16_t section_start = getSectionStart(section_id, section_version); const uint8_t section_size = getSectionSize(section_id, section_version); pos += 2; @@ -348,6 +388,11 @@ std::list GridProfileParser::getProfile() const break; } + if (section_start == -1) { + section.SectionName = "Unknown"; + break; + } + for (uint8_t val_id = 0; val_id < section_size; val_id++) { auto itemDefinition = itemDefinitions.at(_profileValues[section_start + val_id].ItemDefinition); @@ -382,9 +427,9 @@ uint8_t GridProfileParser::getSectionSize(const uint8_t section_id, const uint8_ return count; } -int8_t GridProfileParser::getSectionStart(const uint8_t section_id, const uint8_t section_version) +int16_t GridProfileParser::getSectionStart(const uint8_t section_id, const uint8_t section_version) { - uint8_t count = -1; + int16_t count = -1; for (auto& values : _profileValues) { count++; if (values.Section == section_id && values.Version == section_version) { diff --git a/lib/Hoymiles/src/parser/GridProfileParser.h b/lib/Hoymiles/src/parser/GridProfileParser.h index 370463d57..031891f3f 100644 --- a/lib/Hoymiles/src/parser/GridProfileParser.h +++ b/lib/Hoymiles/src/parser/GridProfileParser.h @@ -5,7 +5,7 @@ #define GRID_PROFILE_SIZE 141 #define PROFILE_TYPE_COUNT 7 -#define SECTION_VALUE_COUNT 113 +#define SECTION_VALUE_COUNT 144 typedef struct { uint8_t lIdx; @@ -45,7 +45,7 @@ class GridProfileParser : public Parser { private: static uint8_t getSectionSize(const uint8_t section_id, const uint8_t section_version); - static int8_t getSectionStart(const uint8_t section_id, const uint8_t section_version); + static int16_t getSectionStart(const uint8_t section_id, const uint8_t section_version); uint8_t _payloadGridProfile[GRID_PROFILE_SIZE] = {}; uint8_t _gridProfileLength = 0; diff --git a/lib/Hoymiles/src/parser/StatisticsParser.cpp b/lib/Hoymiles/src/parser/StatisticsParser.cpp index df6e585e5..831c1ad1f 100644 --- a/lib/Hoymiles/src/parser/StatisticsParser.cpp +++ b/lib/Hoymiles/src/parser/StatisticsParser.cpp @@ -82,8 +82,6 @@ void StatisticsParser::clearBuffer() { memset(_payloadStatistic, 0, STATISTIC_PACKET_SIZE); _statisticLength = 0; - - memset(_lastYieldDay, 0, sizeof(_lastYieldDay)); } void StatisticsParser::appendFragment(const uint8_t offset, const uint8_t* payload, const uint8_t len) @@ -111,8 +109,7 @@ void StatisticsParser::endAppendFragment() // currently all values are zero --> Add last known values to offset Hoymiles.getMessageOutput()->printf("Yield Day reset detected!\r\n"); - setChannelFieldOffset(TYPE_DC, c, FLD_YD, - getChannelFieldOffset(TYPE_DC, c, FLD_YD) + _lastYieldDay[static_cast(c)]); + setChannelFieldOffset(TYPE_DC, c, FLD_YD, _lastYieldDay[static_cast(c)]); _lastYieldDay[static_cast(c)] = 0; } else { diff --git a/lib/Hoymiles/src/parser/StatisticsParser.h b/lib/Hoymiles/src/parser/StatisticsParser.h index 2893cbd8a..10f06e04b 100644 --- a/lib/Hoymiles/src/parser/StatisticsParser.h +++ b/lib/Hoymiles/src/parser/StatisticsParser.h @@ -168,5 +168,5 @@ class StatisticsParser : public Parser { uint32_t _lastUpdateFromInternal = 0; bool _enableYieldDayCorrection = false; - float _lastYieldDay[CH_CNT]; + float _lastYieldDay[CH_CNT] = {}; }; \ No newline at end of file diff --git a/src/MqttHandleHass.cpp b/src/MqttHandleHass.cpp index cb10230bc..88553e15e 100644 --- a/src/MqttHandleHass.cpp +++ b/src/MqttHandleHass.cpp @@ -6,6 +6,7 @@ #include "MqttHandleInverter.h" #include "MqttSettings.h" #include "NetworkSettings.h" +#include "Utils.h" #include "defaults.h" MqttHandleHassClass MqttHandleHass; @@ -52,6 +53,14 @@ void MqttHandleHassClass::publishConfig() const CONFIG_T& config = Configuration.get(); + // publish DTU sensors + publishDtuSensor("IP", "", "diagnostic", "mdi:network-outline", "", ""); + publishDtuSensor("WiFi Signal", "signal_strength", "diagnostic", "", "dBm", "rssi"); + publishDtuSensor("Uptime", "duration", "diagnostic", "", "s", ""); + publishDtuBinarySensor("Status", "connectivity", "diagnostic", config.Mqtt.Lwt.Value_Online, config.Mqtt.Lwt.Value_Offline, config.Mqtt.Lwt.Topic); + + yield(); + // Loop all inverters for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) { auto inv = Hoymiles.getInverterByPos(i); @@ -77,7 +86,7 @@ void MqttHandleHassClass::publishConfig() if (t == TYPE_DC && !config.Mqtt.Hass.IndividualPanels) { clear = true; } - publishField(inv, t, c, deviceFieldAssignment[f], clear); + publishInverterField(inv, t, c, deviceFieldAssignment[f], clear); } } } @@ -86,7 +95,7 @@ void MqttHandleHassClass::publishConfig() } } -void MqttHandleHassClass::publishField(std::shared_ptr inv, const ChannelType_t type, const ChannelNum_t channel, const byteAssign_fieldDeviceClass_t fieldType, const bool clear) +void MqttHandleHassClass::publishInverterField(std::shared_ptr inv, const ChannelType_t type, const ChannelNum_t channel, const byteAssign_fieldDeviceClass_t fieldType, const bool clear) { if (!inv->Statistics()->hasChannelFieldValue(type, channel, fieldType.fieldId)) { return; @@ -135,8 +144,7 @@ void MqttHandleHassClass::publishField(std::shared_ptr inv, co root["unit_of_meas"] = unit_of_measure; } - JsonObject deviceObj = root.createNestedObject("dev"); - createDeviceInfo(deviceObj, inv); + createInverterInfo(root, inv); if (Configuration.get().Mqtt.Hass.Expire) { root["exp_aft"] = Hoymiles.getNumInverters() * max(Hoymiles.PollInterval(), Configuration.get().Mqtt.PublishInterval) * inv->getReachableThreshold(); @@ -183,8 +191,7 @@ void MqttHandleHassClass::publishInverterButton(std::shared_ptr inv) +void MqttHandleHassClass::createInverterInfo(DynamicJsonDocument& root, std::shared_ptr inv) +{ + createDeviceInfo( + root, + inv->name(), + inv->serialString(), + getDtuUrl(), + "OpenDTU", + inv->typeName(), + AUTO_GIT_HASH, + getDtuUniqueId()); +} + +void MqttHandleHassClass::createDtuInfo(DynamicJsonDocument& root) +{ + createDeviceInfo( + root, + NetworkSettings.getHostname(), + getDtuUniqueId(), + getDtuUrl(), + "OpenDTU", + "OpenDTU", + AUTO_GIT_HASH); +} + +void MqttHandleHassClass::createDeviceInfo( + DynamicJsonDocument& root, + const String& name, const String& identifiers, const String& configuration_url, + const String& manufacturer, const String& model, const String& sw_version, + const String& via_device) +{ + auto object = root.createNestedObject("dev"); + + object["name"] = name; + object["ids"] = identifiers; + object["cu"] = configuration_url; + object["mf"] = manufacturer; + object["mdl"] = model; + object["sw"] = sw_version; + + if (via_device != "") { + object["via_device"] = via_device; + } +} + +String MqttHandleHassClass::getDtuUniqueId() +{ + return NetworkSettings.getHostname() + "_" + Utils::getChipId(); +} + +String MqttHandleHassClass::getDtuUrl() { - object["name"] = inv->name(); - object["ids"] = inv->serialString(); - object["cu"] = String("http://") + NetworkSettings.localIP().toString(); - object["mf"] = "OpenDTU"; - object["mdl"] = inv->typeName(); - object["sw"] = AUTO_GIT_HASH; + return String("http://") + NetworkSettings.localIP().toString(); } void MqttHandleHassClass::publish(const String& subtopic, const String& payload) diff --git a/webapp/package.json b/webapp/package.json index 0005db4fe..f09ce08dd 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -18,28 +18,28 @@ "mitt": "^3.0.1", "sortablejs": "^1.15.1", "spark-md5": "^3.0.2", - "vue": "^3.3.11", + "vue": "^3.3.12", "vue-i18n": "^9.8.0", "vue-router": "^4.2.5" }, "devDependencies": { - "@intlify/unplugin-vue-i18n": "^1.5.0", - "@rushstack/eslint-patch": "^1.6.0", + "@intlify/unplugin-vue-i18n": "^1.6.0", + "@rushstack/eslint-patch": "^1.6.1", "@tsconfig/node18": "^18.2.2", "@types/bootstrap": "^5.2.10", - "@types/node": "^20.10.4", + "@types/node": "^20.10.5", "@types/sortablejs": "^1.15.7", "@types/spark-md5": "^3.0.4", "@vitejs/plugin-vue": "^4.5.2", "@vue/eslint-config-typescript": "^12.0.0", - "@vue/tsconfig": "^0.4.0", - "eslint": "^8.55.0", + "@vue/tsconfig": "^0.5.1", + "eslint": "^8.56.0", "eslint-plugin-vue": "^9.19.2", "npm-run-all": "^4.1.5", "sass": "^1.69.5", "terser": "^5.26.0", "typescript": "^5.3.3", - "vite": "^5.0.7", + "vite": "^5.0.10", "vite-plugin-compression": "^0.5.1", "vite-plugin-css-injected-by-js": "^3.3.0", "vue-tsc": "^1.8.25" diff --git a/webapp/src/components/GridProfile.vue b/webapp/src/components/GridProfile.vue index 941a7bc8c..6c93caece 100644 --- a/webapp/src/components/GridProfile.vue +++ b/webapp/src/components/GridProfile.vue @@ -20,7 +20,7 @@
-
+