From 6ee6eaf0b20d850d2dc49628d2c9219f5b849d2e Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Tue, 23 Jul 2024 21:17:25 +0200 Subject: [PATCH] Fix: inverter power limits precision Hoymiles inverters allow setting relative limits with a precision of 0.1 %. this changeset allows to utilize this precision. * preserve accuracy when decoding power limit * Web API: process floating point limits * MQTT: process floating point limits * use appropriate accuracy for limits in web UI * HASS: step for relative inverter limit is 0.1 % --- include/MqttHandleHass.h | 2 +- .../src/commands/ActivePowerControlCommand.cpp | 2 +- src/MqttHandleHass.cpp | 7 ++++--- src/MqttHandleInverter.cpp | 16 ++++++++-------- src/WebApi_limit.cpp | 4 ++-- webapp/src/locales/index.ts | 6 ++++++ webapp/src/views/HomeView.vue | 6 +++--- 7 files changed, 25 insertions(+), 18 deletions(-) diff --git a/include/MqttHandleHass.h b/include/MqttHandleHass.h index a76cb0c7b..9ab746a30 100644 --- a/include/MqttHandleHass.h +++ b/include/MqttHandleHass.h @@ -63,7 +63,7 @@ class MqttHandleHassClass { 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 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, float step = 1.0); void publishInverterBinarySensor(std::shared_ptr inv, const char* caption, const char* subTopic, const char* payload_on, const char* payload_off); static void createInverterInfo(JsonDocument& doc, std::shared_ptr inv); diff --git a/lib/Hoymiles/src/commands/ActivePowerControlCommand.cpp b/lib/Hoymiles/src/commands/ActivePowerControlCommand.cpp index b9e8eea26..dcd2370c1 100644 --- a/lib/Hoymiles/src/commands/ActivePowerControlCommand.cpp +++ b/lib/Hoymiles/src/commands/ActivePowerControlCommand.cpp @@ -85,7 +85,7 @@ bool ActivePowerControlCommand::handleResponse(const fragment_t fragment[], cons float ActivePowerControlCommand::getLimit() const { - const uint16_t l = (((uint16_t)_payload[12] << 8) | _payload[13]); + const float l = (((uint16_t)_payload[12] << 8) | _payload[13]); return l / 10; } diff --git a/src/MqttHandleHass.cpp b/src/MqttHandleHass.cpp index 119571889..87f1ccb9b 100644 --- a/src/MqttHandleHass.cpp +++ b/src/MqttHandleHass.cpp @@ -73,8 +73,8 @@ void MqttHandleHassClass::publishConfig() publishInverterButton(inv, "Turn Inverter On", "mdi:power-plug", "config", "", "cmd/power", "1"); publishInverterButton(inv, "Restart Inverter", "", "config", "restart", "cmd/restart", "1"); - publishInverterNumber(inv, "Limit NonPersistent Relative", "mdi:speedometer", "config", "cmd/limit_nonpersistent_relative", "status/limit_relative", "%", 0, 100); - publishInverterNumber(inv, "Limit Persistent Relative", "mdi:speedometer", "config", "cmd/limit_persistent_relative", "status/limit_relative", "%", 0, 100); + publishInverterNumber(inv, "Limit NonPersistent Relative", "mdi:speedometer", "config", "cmd/limit_nonpersistent_relative", "status/limit_relative", "%", 0, 100, 0.1); + publishInverterNumber(inv, "Limit Persistent Relative", "mdi:speedometer", "config", "cmd/limit_persistent_relative", "status/limit_relative", "%", 0, 100, 0.1); publishInverterNumber(inv, "Limit NonPersistent Absolute", "mdi:speedometer", "config", "cmd/limit_nonpersistent_absolute", "status/limit_absolute", "W", 0, MAX_INVERTER_LIMIT); publishInverterNumber(inv, "Limit Persistent Absolute", "mdi:speedometer", "config", "cmd/limit_persistent_absolute", "status/limit_absolute", "W", 0, MAX_INVERTER_LIMIT); @@ -215,7 +215,7 @@ void MqttHandleHassClass::publishInverterButton(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, const int16_t max) + const int16_t min, const int16_t max, float step) { const String serial = inv->serialString(); @@ -243,6 +243,7 @@ void MqttHandleHassClass::publishInverterNumber( root["unit_of_meas"] = unitOfMeasure; root["min"] = min; root["max"] = max; + root["step"] = step; createInverterInfo(root, inv); diff --git a/src/MqttHandleInverter.cpp b/src/MqttHandleInverter.cpp index 624033e1c..d099b4443 100644 --- a/src/MqttHandleInverter.cpp +++ b/src/MqttHandleInverter.cpp @@ -183,7 +183,7 @@ void MqttHandleInverterClass::onMqttMessage(const espMqttClientTypes::MessagePro char* strlimit = new char[len + 1]; memcpy(strlimit, payload, len); strlimit[len] = '\0'; - const int32_t payload_val = strtol(strlimit, NULL, 10); + const float payload_val = strtof(strlimit, NULL); delete[] strlimit; if (payload_val < 0) { @@ -193,17 +193,17 @@ void MqttHandleInverterClass::onMqttMessage(const espMqttClientTypes::MessagePro if (!strcmp(setting, TOPIC_SUB_LIMIT_PERSISTENT_RELATIVE)) { // Set inverter limit relative persistent - MessageOutput.printf("Limit Persistent: %d %%\r\n", payload_val); + MessageOutput.printf("Limit Persistent: %.1f %%\r\n", payload_val); inv->sendActivePowerControlRequest(payload_val, PowerLimitControlType::RelativPersistent); } else if (!strcmp(setting, TOPIC_SUB_LIMIT_PERSISTENT_ABSOLUTE)) { // Set inverter limit absolute persistent - MessageOutput.printf("Limit Persistent: %d W\r\n", payload_val); + MessageOutput.printf("Limit Persistent: %.1f W\r\n", payload_val); inv->sendActivePowerControlRequest(payload_val, PowerLimitControlType::AbsolutPersistent); } else if (!strcmp(setting, TOPIC_SUB_LIMIT_NONPERSISTENT_RELATIVE)) { // Set inverter limit relative non persistent - MessageOutput.printf("Limit Non-Persistent: %d %%\r\n", payload_val); + MessageOutput.printf("Limit Non-Persistent: %.1f %%\r\n", payload_val); if (!properties.retain) { inv->sendActivePowerControlRequest(payload_val, PowerLimitControlType::RelativNonPersistent); } else { @@ -212,7 +212,7 @@ void MqttHandleInverterClass::onMqttMessage(const espMqttClientTypes::MessagePro } else if (!strcmp(setting, TOPIC_SUB_LIMIT_NONPERSISTENT_ABSOLUTE)) { // Set inverter limit absolute non persistent - MessageOutput.printf("Limit Non-Persistent: %d W\r\n", payload_val); + MessageOutput.printf("Limit Non-Persistent: %.1f W\r\n", payload_val); if (!properties.retain) { inv->sendActivePowerControlRequest(payload_val, PowerLimitControlType::AbsolutNonPersistent); } else { @@ -221,8 +221,8 @@ void MqttHandleInverterClass::onMqttMessage(const espMqttClientTypes::MessagePro } else if (!strcmp(setting, TOPIC_SUB_POWER)) { // Turn inverter on or off - MessageOutput.printf("Set inverter power to: %d\r\n", payload_val); - inv->sendPowerControlRequest(payload_val > 0); + MessageOutput.printf("Set inverter power to: %d\r\n", static_cast(payload_val)); + inv->sendPowerControlRequest(static_cast(payload_val) > 0); } else if (!strcmp(setting, TOPIC_SUB_RESTART)) { // Restart inverter @@ -230,7 +230,7 @@ void MqttHandleInverterClass::onMqttMessage(const espMqttClientTypes::MessagePro if (!properties.retain && payload_val == 1) { inv->sendRestartControlRequest(); } else { - MessageOutput.println("Ignored because retained"); + MessageOutput.println("Ignored because retained or numeric value not '1'"); } } } diff --git a/src/WebApi_limit.cpp b/src/WebApi_limit.cpp index 9a622deae..6a6c90ca4 100644 --- a/src/WebApi_limit.cpp +++ b/src/WebApi_limit.cpp @@ -83,7 +83,7 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request) return; } - if (root["limit_value"].as() > MAX_INVERTER_LIMIT) { + if (root["limit_value"].as() > MAX_INVERTER_LIMIT) { retMsg["message"] = "Limit must between 0 and " STR(MAX_INVERTER_LIMIT) "!"; retMsg["code"] = WebApiError::LimitInvalidLimit; retMsg["param"]["max"] = MAX_INVERTER_LIMIT; @@ -102,7 +102,7 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request) return; } - uint16_t limit = root["limit_value"].as(); + float limit = root["limit_value"].as(); PowerLimitControlType type = root["limit_type"].as(); auto inv = Hoymiles.getInverterBySerial(serial); diff --git a/webapp/src/locales/index.ts b/webapp/src/locales/index.ts index 7ea42cf46..5edf8013f 100644 --- a/webapp/src/locales/index.ts +++ b/webapp/src/locales/index.ts @@ -35,12 +35,18 @@ LOCALES.forEach((locale) => { decimalNoDigits: { style: 'decimal', minimumFractionDigits: 0, maximumFractionDigits: 0 }, + decimalOneDigit: { + style: 'decimal', minimumFractionDigits: 1, maximumFractionDigits: 1 + }, decimalTwoDigits: { style: 'decimal', minimumFractionDigits: 2, maximumFractionDigits: 2 }, percent: { style: 'percent', }, + percentOneDigit: { + style: 'percent', minimumFractionDigits: 1, maximumFractionDigits: 1 + }, byte: { style: 'unit', unit: 'byte', }, diff --git a/webapp/src/views/HomeView.vue b/webapp/src/views/HomeView.vue index 2dd9ef448..68c1e9a1e 100644 --- a/webapp/src/views/HomeView.vue +++ b/webapp/src/views/HomeView.vue @@ -49,7 +49,7 @@
{{ $t('home.CurrentLimit') }}{{ $n(inverter.limit_relative / 100, 'percent') }} + }} W | {{ $n(inverter.limit_relative / 100, 'percentOneDigit') }}
{{ $t('home.DataAge') }} {{ $t('home.Seconds', {'val': $n(inverter.data_age) }) }} @@ -416,13 +416,13 @@ export default defineComponent({ currentLimitAbsolute(): string { if (this.currentLimitList.max_power > 0) { return this.$n(this.currentLimitList.limit_relative * this.currentLimitList.max_power / 100, - 'decimalTwoDigits'); + 'decimalNoDigits'); } return "0"; }, currentLimitRelative(): string { return this.$n(this.currentLimitList.limit_relative, - 'decimalTwoDigits'); + 'decimalOneDigit'); }, inverterData(): Inverter[] { return this.liveData.inverters.slice().sort((a: Inverter, b: Inverter) => {