diff --git a/docs/MQTT_Topics.md b/docs/MQTT_Topics.md index 404e5e57d..4f73b6166 100644 --- a/docs/MQTT_Topics.md +++ b/docs/MQTT_Topics.md @@ -24,6 +24,9 @@ serial will be replaced with the serial number of the inverter. | [serial]/device/fwbuilddatetime | R | Build date / time of inverter firmware | | | [serial]/device/hwpartnumber | R | Hardware part number of the inverter | | | [serial]/device/hwversion | R | Hardware version of the inverter | | +| [serial]/status/reachable | R | Indicates whether the inverter is reachable | 0 or 1 | +| [serial]/status/producing | R | Indicates whether the inverter is producing AC power | 0 or 1 | +| [serial]/status/last_update | R | Unix timestamp of last inverter statistics udpate | seconds since JAN 01 1970 (UTC) | ### AC channel / global specific topics @@ -62,8 +65,6 @@ cmd topics are used to set values. Status topics are updated from values set in | ----------------------------------------- | ----- | ---------------------------------------------------- | -------------------------- | | [serial]/status/limit_relative | R | Current applied production limit of the inverter | % of total possible output | | [serial]/status/limit_absolute | R | Current applied production limit of the inverter | Watt (W) | -| [serial]/status/reachable | R | Indicates whether the inverter is reachable | 0 or 1 | -| [serial]/status/producing | R | Indicates whether the inverter is producing AC power | 0 or 1 | | [serial]/cmd/limit_persistent_relative | W | Set the inverter limit as a percentage of total production capability. The value will survive the night without power. The updated value will show up in the web GUI and limit_relative topic immediatly. | % | | [serial]/cmd/limit_persistent_absolute | W | Set the inverter limit as a absolute value. The value will survive the night without power. The updated value will show up in the web GUI and limit_relative topic after around 4 minutes. | Watt (W) | | [serial]/cmd/limit_nonpersistent_relative | W | Set the inverter limit as a percentage of total production capability. The value will reset to the last persistent value at night without power. The updated value will show up in the web GUI and limit_relative topic immediatly. | % | diff --git a/include/MqttHassPublishing.h b/include/MqttHassPublishing.h index e50447464..ff40dc5ea 100644 --- a/include/MqttHassPublishing.h +++ b/include/MqttHassPublishing.h @@ -44,7 +44,7 @@ const byteAssign_fieldDeviceClass_t deviceFieldAssignment[] = { { 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_PF, DEVICE_CLS_POWER_FACTOR, STATE_CLS_MEASUREMENT }, { FLD_EFF, DEVICE_CLS_NONE, STATE_CLS_NONE }, { FLD_IRR, DEVICE_CLS_NONE, STATE_CLS_NONE }, { FLD_PRA, DEVICE_CLS_REACTIVE_POWER, STATE_CLS_MEASUREMENT } diff --git a/include/MqttPublishing.h b/include/MqttPublishing.h index efaddb742..5667b40cb 100644 --- a/include/MqttPublishing.h +++ b/include/MqttPublishing.h @@ -30,7 +30,7 @@ class MqttPublishingClass { FLD_PAC, FLD_F, FLD_T, - FLD_PCT, + FLD_PF, FLD_EFF, FLD_IRR, FLD_PRA diff --git a/include/WebApi_ws_live.h b/include/WebApi_ws_live.h index d21f376a2..7f21f8034 100644 --- a/include/WebApi_ws_live.h +++ b/include/WebApi_ws_live.h @@ -13,7 +13,8 @@ class WebApiWsLiveClass { private: void generateJsonResponse(JsonVariant& root); - void addField(JsonVariant& root, uint8_t idx, std::shared_ptr inv, uint8_t channel, uint8_t fieldId, String topic = ""); + void addField(JsonObject& root, uint8_t idx, std::shared_ptr inv, uint8_t channel, uint8_t fieldId, String topic = ""); + void addTotalField(JsonObject& root, String name, float value, String unit, uint8_t digits); void onLivedataStatus(AsyncWebServerRequest* request); void onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len); diff --git a/lib/Hoymiles/src/commands/README.md b/lib/Hoymiles/src/commands/README.md index 6f5f1d45e..60cf08a8d 100644 --- a/lib/Hoymiles/src/commands/README.md +++ b/lib/Hoymiles/src/commands/README.md @@ -3,6 +3,7 @@ * CommandAbstract * DevControlCommand * ActivePowerControlCommand + * PowerControlCommand * MultiDataCommand * AlarmDataCommand * DevInfoAllCommand diff --git a/lib/Hoymiles/src/inverters/HM_1CH.h b/lib/Hoymiles/src/inverters/HM_1CH.h index 6f10745da..318bfc79d 100644 --- a/lib/Hoymiles/src/inverters/HM_1CH.h +++ b/lib/Hoymiles/src/inverters/HM_1CH.h @@ -24,9 +24,9 @@ class HM_1CH : public HM_Abstract { { FLD_PAC, UNIT_W, CH0, 18, 2, 10, false }, { FLD_PRA, UNIT_VA, CH0, 20, 2, 10, false }, { FLD_F, UNIT_HZ, CH0, 16, 2, 100, false }, - { FLD_PCT, UNIT_PCT, CH0, 24, 2, 10, false }, + { FLD_PF, UNIT_NONE, CH0, 24, 2, 1000, false }, { FLD_T, UNIT_C, CH0, 26, 2, 10, true }, - { FLD_EVT_LOG, UNIT_CNT, CH0, 28, 2, 1, false }, + { FLD_EVT_LOG, UNIT_NONE, CH0, 28, 2, 1, false }, { FLD_YD, UNIT_WH, CH0, CALC_YD_CH0, 0, CMD_CALC, false }, { FLD_YT, UNIT_KWH, CH0, CALC_YT_CH0, 0, CMD_CALC, false }, { FLD_PDC, UNIT_W, CH0, CALC_PDC_CH0, 0, CMD_CALC, false }, diff --git a/lib/Hoymiles/src/inverters/HM_2CH.h b/lib/Hoymiles/src/inverters/HM_2CH.h index 71a5cfe15..9f1e4a8cb 100644 --- a/lib/Hoymiles/src/inverters/HM_2CH.h +++ b/lib/Hoymiles/src/inverters/HM_2CH.h @@ -31,9 +31,9 @@ class HM_2CH : public HM_Abstract { { FLD_PAC, UNIT_W, CH0, 30, 2, 10, false }, { FLD_PRA, UNIT_VA, CH0, 32, 2, 10, false }, { FLD_F, UNIT_HZ, CH0, 28, 2, 100, false }, - { FLD_PCT, UNIT_PCT, CH0, 36, 2, 10, false }, + { FLD_PF, UNIT_NONE, CH0, 36, 2, 1000, false }, { FLD_T, UNIT_C, CH0, 38, 2, 10, true }, - { FLD_EVT_LOG, UNIT_CNT, CH0, 40, 2, 1, false }, + { FLD_EVT_LOG, UNIT_NONE, CH0, 40, 2, 1, false }, { FLD_YD, UNIT_WH, CH0, CALC_YD_CH0, 0, CMD_CALC, false }, { FLD_YT, UNIT_KWH, CH0, CALC_YT_CH0, 0, CMD_CALC, false }, { FLD_PDC, UNIT_W, CH0, CALC_PDC_CH0, 0, CMD_CALC, false }, diff --git a/lib/Hoymiles/src/inverters/HM_4CH.h b/lib/Hoymiles/src/inverters/HM_4CH.h index d824be647..3d2eefe7f 100644 --- a/lib/Hoymiles/src/inverters/HM_4CH.h +++ b/lib/Hoymiles/src/inverters/HM_4CH.h @@ -45,9 +45,9 @@ class HM_4CH : public HM_Abstract { { FLD_PAC, UNIT_W, CH0, 50, 2, 10, false }, { FLD_PRA, UNIT_VA, CH0, 52, 2, 10, false }, { FLD_F, UNIT_HZ, CH0, 48, 2, 100, false }, - { FLD_PCT, UNIT_PCT, CH0, 56, 2, 10, false }, + { FLD_PF, UNIT_NONE, CH0, 56, 2, 1000, false }, { FLD_T, UNIT_C, CH0, 58, 2, 10, true }, - { FLD_EVT_LOG, UNIT_CNT, CH0, 60, 2, 1, false }, + { FLD_EVT_LOG, UNIT_NONE, CH0, 60, 2, 1, false }, { FLD_YD, UNIT_WH, CH0, CALC_YD_CH0, 0, CMD_CALC, false }, { FLD_YT, UNIT_KWH, CH0, CALC_YT_CH0, 0, CMD_CALC, false }, { FLD_PDC, UNIT_W, CH0, CALC_PDC_CH0, 0, CMD_CALC, false }, diff --git a/lib/Hoymiles/src/parser/StatisticsParser.cpp b/lib/Hoymiles/src/parser/StatisticsParser.cpp index a6abc83a7..dbbdf135d 100644 --- a/lib/Hoymiles/src/parser/StatisticsParser.cpp +++ b/lib/Hoymiles/src/parser/StatisticsParser.cpp @@ -120,6 +120,25 @@ const char* StatisticsParser::getChannelFieldName(uint8_t channel, uint8_t field return fields[b[pos].fieldId]; } +uint8_t StatisticsParser::getChannelFieldDigits(uint8_t channel, uint8_t fieldId) +{ + uint8_t pos = getAssignIdxByChannelField(channel, fieldId); + const byteAssign_t* b = _byteAssignment; + + switch (b[pos].div) { + case 1: + return 0; + case 10: + return 1; + case 100: + return 2; + case 1000: + return 3; + default: + return 2; + } +} + uint8_t StatisticsParser::getChannelCount() { const byteAssign_t* b = _byteAssignment; diff --git a/lib/Hoymiles/src/parser/StatisticsParser.h b/lib/Hoymiles/src/parser/StatisticsParser.h index b3327fe11..968d3e988 100644 --- a/lib/Hoymiles/src/parser/StatisticsParser.h +++ b/lib/Hoymiles/src/parser/StatisticsParser.h @@ -16,7 +16,7 @@ enum { UNIT_C, UNIT_PCT, UNIT_VA, - UNIT_CNT + UNIT_NONE }; const char* const units[] = { "V", "A", "W", "Wh", "kWh", "Hz", "°C", "%", "var", "" }; @@ -32,7 +32,7 @@ enum { FLD_PAC, FLD_F, FLD_T, - FLD_PCT, + FLD_PF, FLD_EFF, FLD_IRR, FLD_PRA, @@ -83,6 +83,7 @@ class StatisticsParser : public Parser { bool hasChannelFieldValue(uint8_t channel, uint8_t fieldId); const char* getChannelFieldUnit(uint8_t channel, uint8_t fieldId); const char* getChannelFieldName(uint8_t channel, uint8_t fieldId); + uint8_t getChannelFieldDigits(uint8_t channel, uint8_t fieldId); uint8_t getChannelCount(); diff --git a/src/MqttPublishing.cpp b/src/MqttPublishing.cpp index 46819bce8..f297d21ec 100644 --- a/src/MqttPublishing.cpp +++ b/src/MqttPublishing.cpp @@ -71,6 +71,12 @@ void MqttPublishingClass::loop() MqttSettings.publish(subtopic + "/status/reachable", String(inv->isReachable())); MqttSettings.publish(subtopic + "/status/producing", String(inv->isProducing())); + if (inv->Statistics()->getLastUpdate() > 0) { + MqttSettings.publish(subtopic + "/status/last_update", String(std::time(0) - (millis() - inv->Statistics()->getLastUpdate()) / 1000)); + } else { + MqttSettings.publish(subtopic + "/status/last_update", String(0)); + } + uint32_t lastUpdate = inv->Statistics()->getLastUpdate(); if (lastUpdate > 0 && lastUpdate != _lastPublishStats[i]) { _lastPublishStats[i] = lastUpdate; diff --git a/src/WebApi_ws_live.cpp b/src/WebApi_ws_live.cpp index 41ce2b5e7..2c6f9c8ba 100644 --- a/src/WebApi_ws_live.cpp +++ b/src/WebApi_ws_live.cpp @@ -73,57 +73,78 @@ void WebApiWsLiveClass::loop() void WebApiWsLiveClass::generateJsonResponse(JsonVariant& root) { + JsonArray invArray = root.createNestedArray("inverters"); + + float totalPower = 0; + float totalYieldDay = 0; + float totalYieldTotal = 0; + // Loop all inverters for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) { auto inv = Hoymiles.getInverterByPos(i); + if (inv == nullptr) { + continue; + } - root[i][F("serial")] = inv->serialString(); - root[i][F("name")] = inv->name(); - root[i][F("data_age")] = (millis() - inv->Statistics()->getLastUpdate()) / 1000; - root[i][F("reachable")] = inv->isReachable(); - root[i][F("producing")] = inv->isProducing(); - root[i][F("limit_relative")] = inv->SystemConfigPara()->getLimitPercent(); + JsonObject invObject = invArray.createNestedObject(); + + invObject[F("serial")] = inv->serialString(); + invObject[F("name")] = inv->name(); + invObject[F("data_age")] = (millis() - inv->Statistics()->getLastUpdate()) / 1000; + invObject[F("reachable")] = inv->isReachable(); + invObject[F("producing")] = inv->isProducing(); + invObject[F("limit_relative")] = inv->SystemConfigPara()->getLimitPercent(); if (inv->DevInfo()->getMaxPower() > 0) { - root[i][F("limit_absolute")] = inv->SystemConfigPara()->getLimitPercent() * inv->DevInfo()->getMaxPower() / 100.0; + invObject[F("limit_absolute")] = inv->SystemConfigPara()->getLimitPercent() * inv->DevInfo()->getMaxPower() / 100.0; } else { - root[i][F("limit_absolute")] = -1; + invObject[F("limit_absolute")] = -1; } // Loop all channels for (uint8_t c = 0; c <= inv->Statistics()->getChannelCount(); c++) { - addField(root, i, inv, c, FLD_PAC); - addField(root, i, inv, c, FLD_UAC); - addField(root, i, inv, c, FLD_IAC); + addField(invObject, i, inv, c, FLD_PAC); + addField(invObject, i, inv, c, FLD_UAC); + addField(invObject, i, inv, c, FLD_IAC); if (c == 0) { - addField(root, i, inv, c, FLD_PDC, F("Power DC")); + addField(invObject, i, inv, c, FLD_PDC, F("Power DC")); } else { - addField(root, i, inv, c, FLD_PDC); + addField(invObject, i, inv, c, FLD_PDC); } - addField(root, i, inv, c, FLD_UDC); - addField(root, i, inv, c, FLD_IDC); - addField(root, i, inv, c, FLD_YD); - addField(root, i, inv, c, FLD_YT); - addField(root, i, inv, c, FLD_F); - addField(root, i, inv, c, FLD_T); - addField(root, i, inv, c, FLD_PCT); - addField(root, i, inv, c, FLD_PRA); - addField(root, i, inv, c, FLD_EFF); - addField(root, i, inv, c, FLD_IRR); + addField(invObject, i, inv, c, FLD_UDC); + addField(invObject, i, inv, c, FLD_IDC); + addField(invObject, i, inv, c, FLD_YD); + addField(invObject, i, inv, c, FLD_YT); + addField(invObject, i, inv, c, FLD_F); + addField(invObject, i, inv, c, FLD_T); + addField(invObject, i, inv, c, FLD_PF); + addField(invObject, i, inv, c, FLD_PRA); + addField(invObject, i, inv, c, FLD_EFF); + addField(invObject, i, inv, c, FLD_IRR); } if (inv->Statistics()->hasChannelFieldValue(CH0, FLD_EVT_LOG)) { - root[i][F("events")] = inv->EventLog()->getEntryCount(); + invObject[F("events")] = inv->EventLog()->getEntryCount(); } else { - root[i][F("events")] = -1; + invObject[F("events")] = -1; } if (inv->Statistics()->getLastUpdate() > _newestInverterTimestamp) { _newestInverterTimestamp = inv->Statistics()->getLastUpdate(); } + + totalPower += inv->Statistics()->getChannelFieldValue(CH0, FLD_PAC); + totalYieldDay += inv->Statistics()->getChannelFieldValue(CH0, FLD_YD); + totalYieldTotal += inv->Statistics()->getChannelFieldValue(CH0, FLD_YT); } + + JsonObject totalObj = root.createNestedObject("total"); + // todo: Fixed hard coded name, unit and digits + addTotalField(totalObj, "Power", totalPower, "W", 1); + addTotalField(totalObj, "YieldDay", totalYieldDay, "Wh", 0); + addTotalField(totalObj, "YieldTotal", totalYieldTotal, "kWh", 2); } -void WebApiWsLiveClass::addField(JsonVariant& root, uint8_t idx, std::shared_ptr inv, uint8_t channel, uint8_t fieldId, String topic) +void WebApiWsLiveClass::addField(JsonObject& root, uint8_t idx, std::shared_ptr inv, uint8_t channel, uint8_t fieldId, String topic) { if (inv->Statistics()->hasChannelFieldValue(channel, fieldId)) { String chanName; @@ -132,11 +153,19 @@ void WebApiWsLiveClass::addField(JsonVariant& root, uint8_t idx, std::shared_ptr } else { chanName = topic; } - root[idx][String(channel)][chanName]["v"] = inv->Statistics()->getChannelFieldValue(channel, fieldId); - root[idx][String(channel)][chanName]["u"] = inv->Statistics()->getChannelFieldUnit(channel, fieldId); + root[String(channel)][chanName]["v"] = inv->Statistics()->getChannelFieldValue(channel, fieldId); + root[String(channel)][chanName]["u"] = inv->Statistics()->getChannelFieldUnit(channel, fieldId); + root[String(channel)][chanName]["d"] = inv->Statistics()->getChannelFieldDigits(channel, fieldId); } } +void WebApiWsLiveClass::addTotalField(JsonObject& root, String name, float value, String unit, uint8_t digits) +{ + root[name]["v"] = value; + root[name]["u"] = unit; + root[name]["d"] = digits; +} + void WebApiWsLiveClass::onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len) { if (type == WS_EVT_CONNECT) { @@ -152,8 +181,8 @@ void WebApiWsLiveClass::onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketC void WebApiWsLiveClass::onLivedataStatus(AsyncWebServerRequest* request) { - AsyncJsonResponse* response = new AsyncJsonResponse(true, 40960U); - JsonVariant root = response->getRoot().as(); + AsyncJsonResponse* response = new AsyncJsonResponse(false, 40960U); + JsonVariant root = response->getRoot(); generateJsonResponse(root); diff --git a/webapp/package.json b/webapp/package.json index 9339b3ab0..449fb09b0 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -21,18 +21,18 @@ "devDependencies": { "@rushstack/eslint-patch": "^1.2.0", "@types/bootstrap": "^5.2.5", - "@types/node": "^18.11.4", + "@types/node": "^18.11.8", "@types/spark-md5": "^3.0.2", - "@vitejs/plugin-vue": "^3.1.2", + "@vitejs/plugin-vue": "^3.2.0", "@vue/eslint-config-typescript": "^11.0.2", "@vue/tsconfig": "^0.1.3", "eslint": "^8.26.0", - "eslint-plugin-vue": "^9.6.0", + "eslint-plugin-vue": "^9.7.0", "npm-run-all": "^4.1.5", "typescript": "^4.8.4", - "vite": "^3.1.8", + "vite": "^3.2.2", "vite-plugin-compression": "^0.5.1", - "vite-plugin-css-injected-by-js": "^2.1.0", + "vite-plugin-css-injected-by-js": "^2.1.1", "vue-tsc": "^1.0.9" } } diff --git a/webapp/src/components/FirmwareInfo.vue b/webapp/src/components/FirmwareInfo.vue index 385d22b27..a0ae8deee 100644 --- a/webapp/src/components/FirmwareInfo.vue +++ b/webapp/src/components/FirmwareInfo.vue @@ -1,6 +1,6 @@