diff --git a/README.md b/README.md index 35b77bf4..fed9fc77 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ Sends text raw data as difined in VE.Direct spec. * Show inverters internal event log * Show inverter information like firmware version, firmware build date, hardware revision and hardware version * Show and set the current inverter limit -* Function to turn the inverter off an on +* Function to turn the inverter off and on * Uses ESP32 microcontroller and NRF24L01+ * Multi-Inverter support * MQTT support (with TLS) diff --git a/docs/MQTT_Topics.md b/docs/MQTT_Topics.md index 6d9243c1..7ee28662 100644 --- a/docs/MQTT_Topics.md +++ b/docs/MQTT_Topics.md @@ -61,6 +61,7 @@ cmd topics are used to set values. Status topics are updated from values set in | Topic | R / W | Description | Value / Unit | | ----------------------------------------- | ----- | ---------------------------------------------------- | -------------------------- | | [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. | % | diff --git a/lib/Hoymiles/src/HoymilesRadio.cpp b/lib/Hoymiles/src/HoymilesRadio.cpp index 9764ea2a..8e26caa0 100644 --- a/lib/Hoymiles/src/HoymilesRadio.cpp +++ b/lib/Hoymiles/src/HoymilesRadio.cpp @@ -48,6 +48,7 @@ void HoymilesRadio::loop() f = _rxBuffer.getFront(); memset(f->fragment, 0xcc, MAX_RF_PAYLOAD_SIZE); f->len = _radio->getDynamicPayloadSize(); + f->channel = _radio->getChannel(); if (f->len > MAX_RF_PAYLOAD_SIZE) f->len = MAX_RF_PAYLOAD_SIZE; @@ -69,7 +70,9 @@ void HoymilesRadio::loop() if (nullptr != inv) { // Save packet in inverter rx buffer - dumpBuf("RX ", f->fragment, f->len); + char buf[30]; + snprintf(buf, sizeof(buf), "RX Channel: %d --> ", f->channel); + dumpBuf(buf, f->fragment, f->len); inv->addRxFragment(f->fragment, f->len); } else { Serial.println(F("Inverter Not found!")); @@ -161,6 +164,11 @@ bool HoymilesRadio::isIdle() return !_busyFlag; } +bool HoymilesRadio::isConnected() +{ + return _radio->isChipConnected(); +} + void HoymilesRadio::openReadingPipe() { serial_u s; diff --git a/lib/Hoymiles/src/HoymilesRadio.h b/lib/Hoymiles/src/HoymilesRadio.h index 9d6062cc..b60468c9 100644 --- a/lib/Hoymiles/src/HoymilesRadio.h +++ b/lib/Hoymiles/src/HoymilesRadio.h @@ -46,9 +46,7 @@ public: void setDtuSerial(uint64_t serial); bool isIdle(); - void sendEsbPacket(CommandAbstract* cmd); - void sendRetransmitPacket(uint8_t fragment_id); - void sendLastPacketAgain(); + bool isConnected(); template T* enqueCommand() @@ -68,6 +66,10 @@ private: bool checkFragmentCrc(fragment_t* fragment); void dumpBuf(const char* info, uint8_t buf[], uint8_t len); + void sendEsbPacket(CommandAbstract* cmd); + void sendRetransmitPacket(uint8_t fragment_id); + void sendLastPacketAgain(); + std::unique_ptr _hspi; std::unique_ptr _radio; uint8_t _rxChLst[5] = { 3, 23, 40, 61, 75 }; diff --git a/lib/Hoymiles/src/commands/ActivePowerControlCommand.cpp b/lib/Hoymiles/src/commands/ActivePowerControlCommand.cpp index 9c004087..05e671da 100644 --- a/lib/Hoymiles/src/commands/ActivePowerControlCommand.cpp +++ b/lib/Hoymiles/src/commands/ActivePowerControlCommand.cpp @@ -49,7 +49,12 @@ bool ActivePowerControlCommand::handleResponse(InverterAbstract* inverter, fragm if ((getType() == PowerLimitControlType::RelativNonPersistent) || (getType() == PowerLimitControlType::RelativPersistent)) { inverter->SystemConfigPara()->setLimitPercent(getLimit()); } else { - // TODO(tbnobody): Not implemented yet because we only can publish the percentage value + uint16_t max_power = inverter->DevInfo()->getMaxPower(); + if (max_power > 0) { + inverter->SystemConfigPara()->setLimitPercent(static_cast(getLimit()) / max_power * 100); + } else { + // TODO(tbnobody): Not implemented yet because we only can publish the percentage value + } } inverter->SystemConfigPara()->setLastUpdateCommand(millis()); inverter->SystemConfigPara()->setLastLimitCommandSuccess(CMD_OK); diff --git a/lib/Hoymiles/src/commands/AlarmDataCommand.cpp b/lib/Hoymiles/src/commands/AlarmDataCommand.cpp index 28633dba..e55ddd0f 100644 --- a/lib/Hoymiles/src/commands/AlarmDataCommand.cpp +++ b/lib/Hoymiles/src/commands/AlarmDataCommand.cpp @@ -6,7 +6,7 @@ AlarmDataCommand::AlarmDataCommand(uint64_t target_address, uint64_t router_addr { setTime(time); setDataType(0x11); - setTimeout(500); + setTimeout(600); } String AlarmDataCommand::getCommandName() diff --git a/lib/Hoymiles/src/inverters/HM_1CH.cpp b/lib/Hoymiles/src/inverters/HM_1CH.cpp index f4a31d60..730a63da 100644 --- a/lib/Hoymiles/src/inverters/HM_1CH.cpp +++ b/lib/Hoymiles/src/inverters/HM_1CH.cpp @@ -5,7 +5,22 @@ HM_1CH::HM_1CH(uint64_t serial) bool HM_1CH::isValidSerial(uint64_t serial) { - return serial >= 0x112100000000 && serial <= 0x112199999999; + // serial >= 0x112100000000 && serial <= 0x112199999999 + + uint8_t preId[2]; + preId[0] = (uint8_t)(serial >> 40); + preId[1] = (uint8_t)(serial >> 32); + + if ((uint8_t)(((((uint16_t)preId[0] << 8) | preId[1]) >> 4) & 0xff) == 0x12) { + return true; + } + + if ((((preId[1] & 0xf0) == 0x10) || ((preId[1] & 0xf0) == 0x20)) + && (((preId[0] == 0x10) && (preId[1] == 0x22)) || ((preId[0] == 0x11) && (preId[1] == 0x21)))) { + return true; + } + + return false; } String HM_1CH::typeName() diff --git a/lib/Hoymiles/src/inverters/HM_2CH.cpp b/lib/Hoymiles/src/inverters/HM_2CH.cpp index 2ec32d55..6730a259 100644 --- a/lib/Hoymiles/src/inverters/HM_2CH.cpp +++ b/lib/Hoymiles/src/inverters/HM_2CH.cpp @@ -5,7 +5,22 @@ HM_2CH::HM_2CH(uint64_t serial) bool HM_2CH::isValidSerial(uint64_t serial) { - return serial >= 0x114100000000 && serial <= 0x114199999999; + // serial >= 0x114100000000 && serial <= 0x114199999999 + + uint8_t preId[2]; + preId[0] = (uint8_t)(serial >> 40); + preId[1] = (uint8_t)(serial >> 32); + + if ((uint8_t)(((((uint16_t)preId[0] << 8) | preId[1]) >> 4) & 0xff) == 0x14) { + return true; + } + + if ((((preId[1] & 0xf0) == 0x30) || ((preId[1] & 0xf0) == 0x40)) + && (((preId[0] == 0x10) && (preId[1] == 0x42)) || ((preId[0] == 0x11) && (preId[1] == 0x41)))) { + return true; + } + + return false; } String HM_2CH::typeName() diff --git a/lib/Hoymiles/src/inverters/HM_4CH.cpp b/lib/Hoymiles/src/inverters/HM_4CH.cpp index 0773d9ad..af8548f5 100644 --- a/lib/Hoymiles/src/inverters/HM_4CH.cpp +++ b/lib/Hoymiles/src/inverters/HM_4CH.cpp @@ -5,12 +5,27 @@ HM_4CH::HM_4CH(uint64_t serial) bool HM_4CH::isValidSerial(uint64_t serial) { - return serial >= 0x116100000000 && serial <= 0x116199999999; + // serial >= 0x116100000000 && serial <= 0x116199999999 + + uint8_t preId[2]; + preId[0] = (uint8_t)(serial >> 40); + preId[1] = (uint8_t)(serial >> 32); + + if ((uint8_t)(((((uint16_t)preId[0] << 8) | preId[1]) >> 4) & 0xff) == 0x16) { + return true; + } + + if ((((preId[1] & 0xf0) == 0x50) || ((preId[1] & 0xf0) == 0x60)) + && (((preId[0] == 0x10) && (preId[1] == 0x62)) || ((preId[0] == 0x11) && (preId[1] == 0x61)))) { + return true; + } + + return false; } String HM_4CH::typeName() { - return String(F("HM-1200, HM-1500")); + return String(F("HM-1000, HM-1200, HM-1500")); } const byteAssign_t* HM_4CH::getByteAssignment() diff --git a/lib/Hoymiles/src/parser/DevInfoParser.cpp b/lib/Hoymiles/src/parser/DevInfoParser.cpp index ce32bfc7..864e7c0e 100644 --- a/lib/Hoymiles/src/parser/DevInfoParser.cpp +++ b/lib/Hoymiles/src/parser/DevInfoParser.cpp @@ -1,6 +1,23 @@ #include "DevInfoParser.h" #include +typedef struct { + uint8_t hwPart[3]; + uint16_t maxPower; + const char* modelName; +} devInfo_t; + +const devInfo_t devInfo[] = { + { { 0x10, 0x10, 0x10 }, 300, "HM-300" }, + { { 0x10, 0x10, 0x20 }, 350, "HM-350" }, + { { 0x10, 0x10, 0x40 }, 400, "HM-400" }, + { { 0x10, 0x11, 0x10 }, 600, "HM-600" }, + { { 0x10, 0x11, 0x20 }, 700, "HM-700" }, + { { 0x10, 0x11, 0x40 }, 800, "HM-800" }, + { { 0x10, 0x12, 0x10 }, 1200, "HM-1200" }, + { { 0x10, 0x12, 0x30 }, 1500, "HM-1500" } +}; + void DevInfoParser::clearBufferAll() { memset(_payloadDevInfoAll, 0, DEV_INFO_SIZE); @@ -92,9 +109,40 @@ uint32_t DevInfoParser::getHwPartNumber() String DevInfoParser::getHwVersion() { - char buf[6]; - snprintf(buf, sizeof(buf), "%02X.%02X", _payloadDevInfoSimple[6], _payloadDevInfoSimple[7]); - return String(buf); + char buf[8]; + snprintf(buf, sizeof(buf), "%02d.%02d", _payloadDevInfoSimple[6], _payloadDevInfoSimple[7]); + return buf; +} + +uint16_t DevInfoParser::getMaxPower() +{ + uint8_t idx = getDevIdx(); + if (idx == 0xff) { + return 0; + } + return devInfo[idx].maxPower; +} + +String DevInfoParser::getHwModelName() +{ + uint8_t idx = getDevIdx(); + if (idx == 0xff) { + return ""; + } + return devInfo[idx].modelName; +} + +uint8_t DevInfoParser::getDevIdx() +{ + uint8_t pos; + for (pos = 0; pos < sizeof(devInfo) / sizeof(devInfo_t); pos++) { + if (devInfo[pos].hwPart[0] == _payloadDevInfoSimple[2] + && devInfo[pos].hwPart[1] == _payloadDevInfoSimple[3] + && devInfo[pos].hwPart[2] == _payloadDevInfoSimple[4]) { + return pos; + } + } + return 0xff; } /* struct tm to seconds since Unix epoch */ diff --git a/lib/Hoymiles/src/parser/DevInfoParser.h b/lib/Hoymiles/src/parser/DevInfoParser.h index 46065861..a4ce10e7 100644 --- a/lib/Hoymiles/src/parser/DevInfoParser.h +++ b/lib/Hoymiles/src/parser/DevInfoParser.h @@ -25,8 +25,12 @@ public: uint32_t getHwPartNumber(); String getHwVersion(); + uint16_t getMaxPower(); + String getHwModelName(); + private: time_t timegm(struct tm* tm); + uint8_t getDevIdx(); uint32_t _lastUpdateAll = 0; uint32_t _lastUpdateSimple = 0; diff --git a/lib/Hoymiles/src/types.h b/lib/Hoymiles/src/types.h index 43c6ffee..a0883a0b 100644 --- a/lib/Hoymiles/src/types.h +++ b/lib/Hoymiles/src/types.h @@ -14,5 +14,6 @@ typedef struct { uint8_t mainCmd; uint8_t fragment[MAX_RF_PAYLOAD_SIZE]; uint8_t len; + uint8_t channel; bool wasReceived; } fragment_t; diff --git a/src/MqttPublishing.cpp b/src/MqttPublishing.cpp index 9d569012..fc74b785 100644 --- a/src/MqttPublishing.cpp +++ b/src/MqttPublishing.cpp @@ -65,6 +65,11 @@ void MqttPublishingClass::loop() if (inv->SystemConfigPara()->getLastUpdate() > 0) { // Limit MqttSettings.publish(subtopic + "/status/limit_relative", String(inv->SystemConfigPara()->getLimitPercent())); + + uint16_t maxpower = inv->DevInfo()->getMaxPower(); + if (maxpower > 0) { + MqttSettings.publish(subtopic + "/status/limit_absolute", String(inv->SystemConfigPara()->getLimitPercent() * maxpower / 100)); + } } MqttSettings.publish(subtopic + "/status/reachable", String(inv->isReachable())); diff --git a/src/WebApi_devinfo.cpp b/src/WebApi_devinfo.cpp index c9a18436..7795f567 100644 --- a/src/WebApi_devinfo.cpp +++ b/src/WebApi_devinfo.cpp @@ -41,6 +41,7 @@ void WebApiDevInfoClass::onDevInfoStatus(AsyncWebServerRequest* request) devInfoObj[F("fw_build_version")] = inv->DevInfo()->getFwBuildVersion(); devInfoObj[F("hw_part_number")] = inv->DevInfo()->getHwPartNumber(); devInfoObj[F("hw_version")] = inv->DevInfo()->getHwVersion(); + devInfoObj[F("hw_model_name")] = inv->DevInfo()->getHwModelName(); char timebuffer[32]; const time_t t = inv->DevInfo()->getFwBuildDateTime(); diff --git a/src/WebApi_limit.cpp b/src/WebApi_limit.cpp index 56a3f0fc..fda74c55 100644 --- a/src/WebApi_limit.cpp +++ b/src/WebApi_limit.cpp @@ -35,7 +35,7 @@ void WebApiLimitClass::onLimitStatus(AsyncWebServerRequest* request) ((uint32_t)((inv->serial() >> 32) & 0xFFFFFFFF)), ((uint32_t)(inv->serial() & 0xFFFFFFFF))); - root[buffer]["limit"] = inv->SystemConfigPara()->getLimitPercent(); + root[buffer]["limit_relative"] = inv->SystemConfigPara()->getLimitPercent(); LastCommandSuccess status = inv->SystemConfigPara()->getLastLimitCommandSuccess(); String limitStatus = "Unknown"; diff --git a/src/WebApi_sysstatus.cpp b/src/WebApi_sysstatus.cpp index 1500a6da..36f71edf 100644 --- a/src/WebApi_sysstatus.cpp +++ b/src/WebApi_sysstatus.cpp @@ -7,6 +7,7 @@ #include "AsyncJson.h" #include "Configuration.h" #include "NetworkSettings.h" +#include #include #include @@ -64,6 +65,8 @@ void WebApiSysstatusClass::onSystemStatus(AsyncWebServerRequest* request) root[F("uptime")] = esp_timer_get_time() / 1000000; + root[F("radio_connected")] = Hoymiles.getRadio()->isConnected(); + response->setLength(); request->send(response); } \ No newline at end of file diff --git a/webapp/src/components/HomeView.vue b/webapp/src/components/HomeView.vue index a854f149..e35377db 100644 --- a/webapp/src/components/HomeView.vue +++ b/webapp/src/components/HomeView.vue @@ -495,7 +495,7 @@ export default defineComponent({ fetch("/api/limit/status") .then((response) => response.json()) .then((data) => { - this.currentLimit = data[serial].limit; + this.currentLimit = data[serial].limit_relative; this.successCommandLimit = data[serial].limit_set_status; this.limitSettingSerial = serial; this.limitSettingLoading = false; diff --git a/webapp/src/components/SystemInfoView.vue b/webapp/src/components/SystemInfoView.vue index 0e73c57e..d0284782 100644 --- a/webapp/src/components/SystemInfoView.vue +++ b/webapp/src/components/SystemInfoView.vue @@ -17,6 +17,8 @@
+ +
@@ -26,12 +28,14 @@ import { defineComponent } from 'vue'; import HardwareInfo from "@/components/partials/HardwareInfo.vue"; import FirmwareInfo from "@/components/partials/FirmwareInfo.vue"; import MemoryInfo from "@/components/partials/MemoryInfo.vue"; +import RadioInfo from "@/components/partials/RadioInfo.vue"; export default defineComponent({ components: { HardwareInfo, FirmwareInfo, MemoryInfo, + RadioInfo, }, data() { return { @@ -60,7 +64,9 @@ export default defineComponent({ littlefs_total: 0, littlefs_used: 0, sketch_total: 0, - sketch_used: 0 + sketch_used: 0, + // RadioInfo + radio_connected: false, } } }, diff --git a/webapp/src/components/partials/DevInfo.vue b/webapp/src/components/partials/DevInfo.vue index 598c9c2b..bb20e68d 100644 --- a/webapp/src/components/partials/DevInfo.vue +++ b/webapp/src/components/partials/DevInfo.vue @@ -6,6 +6,12 @@ + + + + + @@ -40,7 +46,8 @@ declare interface DevInfoData { fw_build_version: number, fw_build_datetime: Date, hw_part_number: number, - hw_version: number + hw_version: number, + hw_model_name: string, } export default defineComponent({ diff --git a/webapp/src/components/partials/EventLog.vue b/webapp/src/components/partials/EventLog.vue index f8ebe324..0b2c01c0 100644 --- a/webapp/src/components/partials/EventLog.vue +++ b/webapp/src/components/partials/EventLog.vue @@ -41,14 +41,20 @@ export default defineComponent({ computed: { timeInHours() { return (value: number) => { - const hours = Math.floor((value) / 3600); - const minutes = Math.floor((value - hours * 3600) / 60); - const seconds = (value - hours * 3600 + minutes * 60) % 60; + const days = Math.floor(value / (24 * 60 * 60)); + const secAfterDays = value - days * (24 * 60 * 60); + const hours = Math.floor(secAfterDays / (60 * 60)); + const secAfterHours = secAfterDays - hours * (60 * 60); + const minutes = Math.floor(secAfterHours / 60); + const seconds = secAfterHours - minutes * 60; const dHours = hours > 9 ? hours : "0" + hours; const dMins = minutes > 9 ? minutes : "0" + minutes; const dSecs = seconds > 9 ? seconds : "0" + seconds; + if (days > 0) { + return days + " " + dHours + ":" + dMins + ":" + dSecs; + } return dHours + ":" + dMins + ":" + dSecs; }; }, diff --git a/webapp/src/components/partials/RadioInfo.vue b/webapp/src/components/partials/RadioInfo.vue new file mode 100644 index 00000000..4d80360b --- /dev/null +++ b/webapp/src/components/partials/RadioInfo.vue @@ -0,0 +1,35 @@ + + +
Model{{ devInfoList.hw_model_name }}Unknown model! Please report the "Hardware Part Number" and model (e.g. HM-350) as an issue + here.
Bootloader Version {{ formatVersion(devInfoList.fw_bootloader_version) }}