diff --git a/README.md b/README.md index 365f0efc6..cb9f4ef48 100644 --- a/README.md +++ b/README.md @@ -75,3 +75,6 @@ Generated using: `git log --date=short --pretty=format:"* %h%x09%ad%x09%s" | gre | TSUN TSOL-M350 | NRF24L01+ | 1 | 1 | 1 | | TSUN TSOL-M800 | NRF24L01+ | 2 | 2 | 1 | | TSUN TSOL-M1600 | NRF24L01+ | 4 | 2 | 1 | +| E-Star HERF-800 | NRF24L01+ | 2 | 2 | 1 | +| E-Star HERF-1600 | NRF24L01+ | 4 | 2 | 1 | +| E-Star HERF-1800 | NRF24L01+ | 4 | 2 | 1 | diff --git a/lib/Hoymiles/src/Hoymiles.cpp b/lib/Hoymiles/src/Hoymiles.cpp index e7b3d2632..eda3500b5 100644 --- a/lib/Hoymiles/src/Hoymiles.cpp +++ b/lib/Hoymiles/src/Hoymiles.cpp @@ -1,9 +1,11 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022-2023 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ #include "Hoymiles.h" #include "Utils.h" +#include "inverters/HERF_2CH.h" +#include "inverters/HERF_4CH.h" #include "inverters/HMS_1CH.h" #include "inverters/HMS_1CHv2.h" #include "inverters/HMS_2CH.h" @@ -168,6 +170,10 @@ std::shared_ptr HoymilesClass::addInverter(const char* name, c i = std::make_shared(_radioNrf.get(), serial); } else if (HM_1CH::isValidSerial(serial)) { i = std::make_shared(_radioNrf.get(), serial); + } else if (HERF_2CH::isValidSerial(serial)) { + i = std::make_shared(_radioNrf.get(), serial); + } else if (HERF_4CH::isValidSerial(serial)) { + i = std::make_shared(_radioNrf.get(), serial); } if (i) { @@ -271,4 +277,4 @@ void HoymilesClass::setMessageOutput(Print* output) Print* HoymilesClass::getMessageOutput() { return _messageOutput; -} \ No newline at end of file +} diff --git a/lib/Hoymiles/src/inverters/HERF_2CH.cpp b/lib/Hoymiles/src/inverters/HERF_2CH.cpp new file mode 100644 index 000000000..37799d14b --- /dev/null +++ b/lib/Hoymiles/src/inverters/HERF_2CH.cpp @@ -0,0 +1,62 @@ + +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022-2024 Thomas Basler and others + */ +#include "HERF_2CH.h" + +static const byteAssign_t byteAssignment[] = { + { TYPE_DC, CH0, FLD_UDC, UNIT_V, 2, 2, 10, false, 1 }, + { TYPE_DC, CH0, FLD_IDC, UNIT_A, 6, 2, 100, false, 2 }, + { TYPE_DC, CH0, FLD_PDC, UNIT_W, 10, 2, 10, false, 1 }, + { TYPE_DC, CH0, FLD_YD, UNIT_WH, 22, 2, 1, false, 0 }, + { TYPE_DC, CH0, FLD_YT, UNIT_KWH, 14, 4, 1000, false, 3 }, + { TYPE_DC, CH0, FLD_IRR, UNIT_PCT, CALC_CH_IRR, CH0, CMD_CALC, false, 3 }, + + { TYPE_DC, CH1, FLD_UDC, UNIT_V, 4, 2, 10, false, 1 }, + { TYPE_DC, CH1, FLD_IDC, UNIT_A, 8, 2, 100, false, 2 }, + { TYPE_DC, CH1, FLD_PDC, UNIT_W, 12, 2, 10, false, 1 }, + { TYPE_DC, CH1, FLD_YD, UNIT_WH, 24, 2, 1, false, 0 }, + { TYPE_DC, CH1, FLD_YT, UNIT_KWH, 18, 4, 1000, false, 3 }, + { TYPE_DC, CH1, FLD_IRR, UNIT_PCT, CALC_CH_IRR, CH1, CMD_CALC, false, 3 }, + + { TYPE_AC, CH0, FLD_UAC, UNIT_V, 26, 2, 10, false, 1 }, + { TYPE_AC, CH0, FLD_IAC, UNIT_A, 34, 2, 100, false, 2 }, + { TYPE_AC, CH0, FLD_PAC, UNIT_W, 30, 2, 10, false, 1 }, + { TYPE_AC, CH0, FLD_Q, UNIT_VAR, 32, 2, 10, false, 1 }, + { TYPE_AC, CH0, FLD_F, UNIT_HZ, 28, 2, 100, false, 2 }, + { TYPE_AC, CH0, FLD_PF, UNIT_NONE, 36, 2, 1000, false, 3 }, + + { TYPE_INV, CH0, FLD_T, UNIT_C, 38, 2, 10, true, 1 }, + { TYPE_INV, CH0, FLD_EVT_LOG, UNIT_NONE, 40, 2, 1, false, 0 }, + + { TYPE_INV, CH0, FLD_YD, UNIT_WH, CALC_TOTAL_YD, 0, CMD_CALC, false, 0 }, + { TYPE_INV, CH0, FLD_YT, UNIT_KWH, CALC_TOTAL_YT, 0, CMD_CALC, false, 3 }, + { TYPE_INV, CH0, FLD_PDC, UNIT_W, CALC_TOTAL_PDC, 0, CMD_CALC, false, 1 }, + { TYPE_INV, CH0, FLD_EFF, UNIT_PCT, CALC_TOTAL_EFF, 0, CMD_CALC, false, 3 } +}; + +HERF_2CH::HERF_2CH(HoymilesRadio* radio, const uint64_t serial) + : HM_Abstract(radio, serial) {}; + +bool HERF_2CH::isValidSerial(const uint64_t serial) +{ + // serial >= 0x282100000000 && serial <= 0x282199999999 + uint16_t preSerial = (serial >> 32) & 0xffff; + return preSerial == 0x2821; +} + +String HERF_2CH::typeName() const +{ + return "HERF-800-2T"; +} + +const byteAssign_t* HERF_2CH::getByteAssignment() const +{ + return byteAssignment; +} + +uint8_t HERF_2CH::getByteAssignmentSize() const +{ + return sizeof(byteAssignment) / sizeof(byteAssignment[0]); +} diff --git a/lib/Hoymiles/src/inverters/HERF_2CH.h b/lib/Hoymiles/src/inverters/HERF_2CH.h new file mode 100644 index 000000000..048ccb618 --- /dev/null +++ b/lib/Hoymiles/src/inverters/HERF_2CH.h @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include "HM_Abstract.h" + +class HERF_2CH : public HM_Abstract { +public: + explicit HERF_2CH(HoymilesRadio* radio, const uint64_t serial); + static bool isValidSerial(const uint64_t serial); + String typeName() const; + const byteAssign_t* getByteAssignment() const; + uint8_t getByteAssignmentSize() const; +}; diff --git a/lib/Hoymiles/src/inverters/HERF_4CH.cpp b/lib/Hoymiles/src/inverters/HERF_4CH.cpp new file mode 100644 index 000000000..f47b35e2d --- /dev/null +++ b/lib/Hoymiles/src/inverters/HERF_4CH.cpp @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022-2024 Thomas Basler and others + */ +#include "HERF_4CH.h" + +HERF_4CH::HERF_4CH(HoymilesRadio* radio, const uint64_t serial) + : HM_4CH(radio, serial) {}; + +bool HERF_4CH::isValidSerial(const uint64_t serial) +{ + // serial >= 0x280100000000 && serial <= 0x280199999999 + uint16_t preSerial = (serial >> 32) & 0xffff; + return preSerial == 0x2801; +} + +String HERF_4CH::typeName() const +{ + return "HERF-1600/1800-4T"; +} diff --git a/lib/Hoymiles/src/inverters/HERF_4CH.h b/lib/Hoymiles/src/inverters/HERF_4CH.h new file mode 100644 index 000000000..70c1ad216 --- /dev/null +++ b/lib/Hoymiles/src/inverters/HERF_4CH.h @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include "HM_4CH.h" + +class HERF_4CH : public HM_4CH { +public: + explicit HERF_4CH(HoymilesRadio* radio, const uint64_t serial); + static bool isValidSerial(const uint64_t serial); + String typeName() const; +}; diff --git a/lib/Hoymiles/src/inverters/README.md b/lib/Hoymiles/src/inverters/README.md index c080a7351..6d6104a20 100644 --- a/lib/Hoymiles/src/inverters/README.md +++ b/lib/Hoymiles/src/inverters/README.md @@ -11,3 +11,5 @@ | HMS_4CH | HMS-1600/1800/2000-4T | 1164 | | HMT_4CH | HMT-1600/1800/2000-4T | 1361 | | HMT_6CH | HMT-1800/2250-6T | 1382 | +| HERF_2CH | HERF 800 | 2821 | +| HERF_4CH | HERF 1800 | 2801 | diff --git a/lib/Hoymiles/src/parser/DevInfoParser.cpp b/lib/Hoymiles/src/parser/DevInfoParser.cpp index 08970ff13..0900a1ace 100644 --- a/lib/Hoymiles/src/parser/DevInfoParser.cpp +++ b/lib/Hoymiles/src/parser/DevInfoParser.cpp @@ -52,7 +52,11 @@ const devInfo_t devInfo[] = { { { 0x10, 0x32, 0x71, ALL }, 2000, "HMT-2000-4T" }, // 0 { { 0x10, 0x33, 0x11, ALL }, 1800, "HMT-1800-6T" }, // 01 - { { 0x10, 0x33, 0x31, ALL }, 2250, "HMT-2250-6T" } // 01 + { { 0x10, 0x33, 0x31, ALL }, 2250, "HMT-2250-6T" }, // 01 + + { { 0xF1, 0x01, 0x14, ALL }, 800, "HERF-800" }, // 00 + { { 0xF1, 0x01, 0x24, ALL }, 1600, "HERF-1600" }, // 00 + { { 0xF1, 0x01, 0x22, ALL }, 1800, "HERF-1800" }, // 00 }; DevInfoParser::DevInfoParser() diff --git a/src/WebApi_dtu.cpp b/src/WebApi_dtu.cpp index aed098340..817e71b27 100644 --- a/src/WebApi_dtu.cpp +++ b/src/WebApi_dtu.cpp @@ -129,7 +129,10 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request) return; } - if (root["serial"].as() == 0) { + // Interpret the string as a hex value and convert it to uint64_t + const uint64_t serial = strtoll(root["serial"].as().c_str(), NULL, 16); + + if (serial == 0) { retMsg["message"] = "Serial cannot be zero!"; retMsg["code"] = WebApiError::DtuSerialZero; response->setLength(); @@ -185,8 +188,7 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request) CONFIG_T& config = Configuration.get(); - // Interpret the string as a hex value and convert it to uint64_t - config.Dtu.Serial = strtoll(root["serial"].as().c_str(), NULL, 16); + config.Dtu.Serial = serial; config.Dtu.PollInterval = root["pollinterval"].as(); config.Dtu.Nrf.PaLevel = root["nrf_palevel"].as(); config.Dtu.Cmt.PaLevel = root["cmt_palevel"].as(); diff --git a/src/WebApi_inverter.cpp b/src/WebApi_inverter.cpp index 32a472350..68983ab9c 100644 --- a/src/WebApi_inverter.cpp +++ b/src/WebApi_inverter.cpp @@ -129,7 +129,10 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request) return; } - if (root["serial"].as() == 0) { + // Interpret the string as a hex value and convert it to uint64_t + const uint64_t serial = strtoll(root["serial"].as().c_str(), NULL, 16); + + if (serial == 0) { retMsg["message"] = "Serial must be a number > 0!"; retMsg["code"] = WebApiError::InverterSerialZero; response->setLength(); @@ -158,7 +161,7 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request) } // Interpret the string as a hex value and convert it to uint64_t - inverter->Serial = strtoll(root["serial"].as().c_str(), NULL, 16); + inverter->Serial = serial; strncpy(inverter->Name, root["name"].as().c_str(), INV_MAX_NAME_STRLEN); @@ -233,7 +236,10 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) return; } - if (root["serial"].as() == 0) { + // Interpret the string as a hex value and convert it to uint64_t + const uint64_t serial = strtoll(root["serial"].as().c_str(), NULL, 16); + + if (serial == 0) { retMsg["message"] = "Serial must be a number > 0!"; retMsg["code"] = WebApiError::InverterSerialZero; response->setLength(); @@ -261,7 +267,7 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) INVERTER_CONFIG_T& inverter = Configuration.get().Inverter[root["id"].as()]; - uint64_t new_serial = strtoll(root["serial"].as().c_str(), NULL, 16); + uint64_t new_serial = serial; uint64_t old_serial = inverter.Serial; // Interpret the string as a hex value and convert it to uint64_t diff --git a/src/WebApi_limit.cpp b/src/WebApi_limit.cpp index 1d9c111a5..b5b9e1726 100644 --- a/src/WebApi_limit.cpp +++ b/src/WebApi_limit.cpp @@ -100,7 +100,10 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request) return; } - if (root["serial"].as() == 0) { + // Interpret the string as a hex value and convert it to uint64_t + const uint64_t serial = strtoll(root["serial"].as().c_str(), NULL, 16); + + if (serial == 0) { retMsg["message"] = "Serial must be a number > 0!"; retMsg["code"] = WebApiError::LimitSerialZero; response->setLength(); @@ -129,7 +132,6 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request) return; } - uint64_t serial = strtoll(root["serial"].as().c_str(), NULL, 16); uint16_t limit = root["limit_value"].as(); PowerLimitControlType type = root["limit_type"].as(); diff --git a/src/WebApi_power.cpp b/src/WebApi_power.cpp index b51967894..08fe9c051 100644 --- a/src/WebApi_power.cpp +++ b/src/WebApi_power.cpp @@ -93,7 +93,10 @@ void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request) return; } - if (root["serial"].as() == 0) { + // Interpret the string as a hex value and convert it to uint64_t + const uint64_t serial = strtoll(root["serial"].as().c_str(), NULL, 16); + + if (serial == 0) { retMsg["message"] = "Serial must be a number > 0!"; retMsg["code"] = WebApiError::PowerSerialZero; response->setLength(); @@ -101,7 +104,6 @@ void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request) return; } - uint64_t serial = strtoll(root["serial"].as().c_str(), NULL, 16); auto inv = Hoymiles.getInverterBySerial(serial); if (inv == nullptr) { retMsg["message"] = "Invalid inverter specified!"; diff --git a/webapp/src/components/InputSerial.vue b/webapp/src/components/InputSerial.vue new file mode 100644 index 000000000..26aa4d617 --- /dev/null +++ b/webapp/src/components/InputSerial.vue @@ -0,0 +1,114 @@ + + + diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index da510947b..c9dfa975b 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -618,5 +618,12 @@ "Name": "Name", "ValueSelected": "Ausgewählt", "ValueActive": "Aktiv" + }, + "inputserial": { + "format_hoymiles": "Hoymiles Seriennummerformat", + "format_converted": "Bereits konvertierte Seriennummer", + "format_herf_valid": "E-Star HERF Format (wird konvertiert gespeichert): {serial}", + "format_herf_invalid": "E-Star HERF Format: Ungültige Prüfsumme", + "format_unknown": "Unbekanntes Format" } } diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index 82fabba9f..4375137b9 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -619,5 +619,12 @@ "Number": "Number", "ValueSelected": "Selected", "ValueActive": "Active" + }, + "inputserial": { + "format_hoymiles": "Hoymiles serial number format", + "format_converted": "Already converted serial number", + "format_herf_valid": "E-Star HERF format (will be saved converted): {serial}", + "format_herf_invalid": "E-Star HERF format: Invalid checksum", + "format_unknown": "Unknown format" } } diff --git a/webapp/src/locales/fr.json b/webapp/src/locales/fr.json index 6a706b5b6..24f0a951d 100644 --- a/webapp/src/locales/fr.json +++ b/webapp/src/locales/fr.json @@ -618,5 +618,12 @@ "Name": "Nom", "ValueSelected": "Sélectionné", "ValueActive": "Activé" + }, + "inputserial": { + "format_hoymiles": "Hoymiles serial number format", + "format_converted": "Already converted serial number", + "format_herf_valid": "E-Star HERF format (will be saved converted): {serial}", + "format_herf_invalid": "E-Star HERF format: Invalid checksum", + "format_unknown": "Unknown format" } } diff --git a/webapp/src/types/DevInfoStatus.ts b/webapp/src/types/DevInfoStatus.ts index 4c09e6b46..7c37a5673 100644 --- a/webapp/src/types/DevInfoStatus.ts +++ b/webapp/src/types/DevInfoStatus.ts @@ -1,5 +1,5 @@ export interface DevInfoStatus { - serial: number; + serial: string; valid_data: boolean; fw_bootloader_version: number; fw_build_version: number; @@ -8,4 +8,4 @@ export interface DevInfoStatus { hw_version: number; hw_model_name: string; max_power: number; -} \ No newline at end of file +} diff --git a/webapp/src/types/InverterConfig.ts b/webapp/src/types/InverterConfig.ts index 1f2167aa4..da7fa43c4 100644 --- a/webapp/src/types/InverterConfig.ts +++ b/webapp/src/types/InverterConfig.ts @@ -6,7 +6,7 @@ export interface InverterChannel { export interface Inverter { id: string; - serial: number; + serial: string; name: string; type: string; order: number; diff --git a/webapp/src/types/LimitConfig.ts b/webapp/src/types/LimitConfig.ts index d311ca612..b218c1140 100644 --- a/webapp/src/types/LimitConfig.ts +++ b/webapp/src/types/LimitConfig.ts @@ -1,5 +1,5 @@ export interface LimitConfig { - serial: number; + serial: string; limit_value: number; limit_type: number; -} \ No newline at end of file +} diff --git a/webapp/src/types/LiveDataStatus.ts b/webapp/src/types/LiveDataStatus.ts index 6da7266b1..5d2c51674 100644 --- a/webapp/src/types/LiveDataStatus.ts +++ b/webapp/src/types/LiveDataStatus.ts @@ -22,7 +22,7 @@ export interface InverterStatistics { } export interface Inverter { - serial: number; + serial: string; name: string; order: number; data_age: number; @@ -53,4 +53,4 @@ export interface LiveData { inverters: Inverter[]; total: Total; hints: Hints; -} \ No newline at end of file +} diff --git a/webapp/src/views/HomeView.vue b/webapp/src/views/HomeView.vue index 953d5cf49..0bd013335 100644 --- a/webapp/src/views/HomeView.vue +++ b/webapp/src/views/HomeView.vue @@ -346,7 +346,7 @@ export default defineComponent({ showAlertLimit: false, powerSettingView: {} as bootstrap.Modal, - powerSettingSerial: 0, + powerSettingSerial: "", powerSettingLoading: true, alertMessagePower: "", alertTypePower: "info", @@ -515,7 +515,7 @@ export default defineComponent({ this.heartInterval && clearTimeout(this.heartInterval); this.isFirstFetchAfterConnect = true; }, - onShowEventlog(serial: number) { + onShowEventlog(serial: string) { this.eventLogLoading = true; fetch("/api/eventlog/status?inv=" + serial + "&locale=" + this.$i18n.locale, { headers: authHeader() }) .then((response) => handleResponse(response, this.$emitter, this.$router)) @@ -526,7 +526,7 @@ export default defineComponent({ this.eventLogView.show(); }, - onShowDevInfo(serial: number) { + onShowDevInfo(serial: string) { this.devInfoLoading = true; fetch("/api/devinfo/status?inv=" + serial, { headers: authHeader() }) .then((response) => handleResponse(response, this.$emitter, this.$router)) @@ -538,7 +538,7 @@ export default defineComponent({ this.devInfoView.show(); }, - onShowGridProfile(serial: number) { + onShowGridProfile(serial: string) { this.gridProfileLoading = true; fetch("/api/gridprofile/status?inv=" + serial, { headers: authHeader() }) .then((response) => handleResponse(response, this.$emitter, this.$router)) @@ -555,9 +555,9 @@ export default defineComponent({ this.gridProfileView.show(); }, - onShowLimitSettings(serial: number) { + onShowLimitSettings(serial: string) { this.showAlertLimit = false; - this.targetLimitList.serial = 0; + this.targetLimitList.serial = ""; this.targetLimitList.limit_value = 0; this.targetLimitType = 1; this.targetLimitTypeText = this.$t('home.Relative'); @@ -611,9 +611,9 @@ export default defineComponent({ this.targetLimitType = type; }, - onShowPowerSettings(serial: number) { + onShowPowerSettings(serial: string) { this.showAlertPower = false; - this.powerSettingSerial = 0; + this.powerSettingSerial = ""; this.powerSettingLoading = true; fetch("/api/power/status", { headers: authHeader() }) .then((response) => handleResponse(response, this.$emitter, this.$router)) diff --git a/webapp/src/views/InverterAdminView.vue b/webapp/src/views/InverterAdminView.vue index 21ea8dd76..9216f592f 100644 --- a/webapp/src/views/InverterAdminView.vue +++ b/webapp/src/views/InverterAdminView.vue @@ -8,8 +8,7 @@
- +
@@ -91,7 +90,7 @@ - + @@ -207,6 +206,7 @@ import BasePage from '@/components/BasePage.vue'; import BootstrapAlert from "@/components/BootstrapAlert.vue"; import CardElement from '@/components/CardElement.vue'; import InputElement from '@/components/InputElement.vue'; +import InputSerial from '@/components/InputSerial.vue'; import ModalDialog from '@/components/ModalDialog.vue'; import type { Inverter } from '@/types/InverterConfig'; import { authHeader, handleResponse } from '@/utils/authentication'; @@ -235,6 +235,7 @@ export default defineComponent({ BootstrapAlert, CardElement, InputElement, + InputSerial, ModalDialog, BIconInfoCircle, BIconPencil,