diff --git a/include/PowerMeter.h b/include/PowerMeter.h index 3823dfbc0..1ac6d65a1 100644 --- a/include/PowerMeter.h +++ b/include/PowerMeter.h @@ -36,7 +36,8 @@ class PowerMeterClass { SOURCE_SDM1PH = 1, SOURCE_SDM3PH = 2, SOURCE_HTTP = 3, - SOURCE_SML = 4 + SOURCE_SML = 4, + SOURCE_SMAHM2 = 5 }; void init(Scheduler& scheduler); float getPowerTotal(bool forceUpdate = true); diff --git a/include/SMA_HM.h b/include/SMA_HM.h new file mode 100644 index 000000000..46be4d64b --- /dev/null +++ b/include/SMA_HM.h @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2024 Holger-Steffen Stapf + */ +#pragma once + +#include +#include + +class SMA_HMClass { +public: + void init(Scheduler& scheduler); + void loop(); + void event1(); + float getPowerTotal(); + float getPowerL1(); + float getPowerL2(); + float getPowerL3(); + uint32_t serial = 0; +private: + uint32_t _lastUpdate = 0; + float _powerMeterPower = 0.0; + float _powerMeterL1 = 0.0; + float _powerMeterL2 = 0.0; + float _powerMeterL3 = 0.0; + uint32_t previousMillis = 0; + Task _loopTask; +}; +extern SMA_HMClass SMA_HM; diff --git a/src/PowerMeter.cpp b/src/PowerMeter.cpp index 0ef583812..e36fe818b 100644 --- a/src/PowerMeter.cpp +++ b/src/PowerMeter.cpp @@ -11,6 +11,7 @@ #include "MessageOutput.h" #include #include +#include PowerMeterClass PowerMeter; @@ -72,6 +73,10 @@ void PowerMeterClass::init(Scheduler& scheduler) inputSerial.enableTx(false); inputSerial.flush(); break; + + case SOURCE_SMAHM2: + SMA_HM.init(scheduler); + break; } } @@ -169,7 +174,7 @@ void PowerMeterClass::loop() void PowerMeterClass::readPowerMeter() { CONFIG_T& config = Configuration.get(); - + uint8_t _address = config.PowerMeter.SdmAddress; if (config.PowerMeter.Source == SOURCE_SDM1PH) { @@ -223,6 +228,12 @@ void PowerMeterClass::readPowerMeter() _lastPowerMeterUpdate = millis(); } } + else if (config.PowerMeter.Source == SOURCE_SMAHM2) { + _powerMeter1Power = SMA_HM.getPowerL1(); + _powerMeter2Power = SMA_HM.getPowerL2(); + _powerMeter3Power = SMA_HM.getPowerL3(); + _lastPowerMeterUpdate = millis(); + } } bool PowerMeterClass::smlReadLoop() diff --git a/src/SMA_HM.cpp b/src/SMA_HM.cpp new file mode 100644 index 000000000..19aab12b7 --- /dev/null +++ b/src/SMA_HM.cpp @@ -0,0 +1,192 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2024 Holger-Steffen Stapf + */ +#include "SMA_HM.h" +#include +#include "Configuration.h" +#include "NetworkSettings.h" +#include +#include "MessageOutput.h" + +unsigned int multicastPort = 9522; // local port to listen on +IPAddress multicastIP(239, 12, 255, 254); +WiFiUDP SMAUdp; + +const uint32_t interval = 1000; + +static void Soutput(int kanal, int index, int art, int tarif, String Bezeichnung, double value, int timestamp){ + MessageOutput.print(Bezeichnung); + MessageOutput.print('='); + MessageOutput.println(value); +} + +SMA_HMClass SMA_HM; + +void SMA_HMClass::init(Scheduler& scheduler) +{ + scheduler.addTask(_loopTask); + _loopTask.setCallback(std::bind(&SMA_HMClass::loop, this)); + _loopTask.setIterations(TASK_FOREVER); + _loopTask.enable(); + SMAUdp.begin(multicastPort); + SMAUdp.beginMulticast(multicastIP, multicastPort); +} + +void SMA_HMClass::loop() +{ + uint32_t currentMillis = millis(); + if (currentMillis - previousMillis >= interval) { + previousMillis = currentMillis; + event1(); + } +} + +void SMA_HMClass::event1() +{ + uint8_t buffer[1024]; + int packetSize = SMAUdp.parsePacket(); + float Pbezug,BezugL1,BezugL2,BezugL3; + Pbezug = 0; + BezugL1 = 0; + BezugL2 = 0; + BezugL3 = 0; + float Peinspeisung,EinspeisungL1,EinspeisungL2,EinspeisungL3; + Peinspeisung = 0; + EinspeisungL1 = 0; + EinspeisungL2 = 0; + EinspeisungL3 = 0; + int count =0; + if (packetSize) { + int rSize = SMAUdp.read(buffer, 1024); + if (buffer[0] != 'S' || buffer[1] != 'M' || buffer[2] != 'A') { + MessageOutput.println("Not an SMA packet?"); + return; + } + uint16_t grouplen; + uint16_t grouptag; + uint8_t* offset = buffer + 4; + do { + grouplen = (offset[0] << 8) + offset[1]; + grouptag = (offset[2] << 8) + offset[3]; + offset += 4; + if (grouplen == 0xffff) return; + if (grouptag == 0x02A0 && grouplen == 4) { + offset += 4; + } else if (grouptag == 0x0010) { + uint8_t* endOfGroup = offset + grouplen; + uint16_t protocolID = (offset[0] << 8) + offset[1]; + offset += 2; + uint16_t susyID = (offset[0] << 8) + offset[1]; + offset += 2; + uint32_t serial = (offset[0] << 24) + (offset[1] << 16) + (offset[2] << 8) + offset[3]; + SMA_HM.serial=serial; + offset += 4; + uint32_t timestamp = (offset[0] << 24) + (offset[1] << 16) + (offset[2] << 8) + offset[3]; + offset += 4; + while (offset < endOfGroup) { + uint8_t kanal = offset[0]; + uint8_t index = offset[1]; + uint8_t art = offset[2]; + uint8_t tarif = offset[3]; + offset += 4; + if (art == 8) { + uint64_t data = ((uint64_t)offset[0] << 56) + + ((uint64_t)offset[1] << 48) + + ((uint64_t)offset[2] << 40) + + ((uint64_t)offset[3] << 32) + + ((uint64_t)offset[4] << 24) + + ((uint64_t)offset[5] << 16) + + ((uint64_t)offset[6] << 8) + + offset[7]; + offset += 8; + } else if (art == 4) { + uint32_t data = (offset[0] << 24) + + (offset[1] << 16) + + (offset[2] << 8) + + offset[3]; + offset += 4; + switch (index) { + case (1): + Pbezug = data * 0.1; + count +=1; + break; + case (2): + Peinspeisung = data * 0.1; + count +=1; + break; + case (21): + BezugL1 = data * 0.1; + count +=1; + break; + case (22): + EinspeisungL1 = data * 0.1; + count +=1; + break; + case (41): + BezugL2 = data * 0.1; + count +=1; + break; + case (42): + EinspeisungL2 = data * 0.1; + count +=1; + break; + case (61): + BezugL3 = data * 0.1; + count +=1; + break; + case (62): + count +=1; + EinspeisungL3 = data * 0.1; + break; + default: + break; // Wird nicht benötigt, wenn Statement(s) vorhanden sind + } + if (count == 8){ + _powerMeterPower = Peinspeisung - Pbezug; + _powerMeterL1=EinspeisungL1-BezugL1; + _powerMeterL2=EinspeisungL2-BezugL2; + _powerMeterL3=EinspeisungL3-BezugL3; + Soutput(kanal, index, art, tarif, "Leistung", _powerMeterPower, timestamp); + Soutput(kanal, index, art, tarif, "Leistung L1", _powerMeterL1, timestamp); + Soutput(kanal, index, art, tarif, "Leistung L2", _powerMeterL2, timestamp); + Soutput(kanal, index, art, tarif, "Leistung L3", _powerMeterL3, timestamp); + count=0; + } + } else if (kanal==144) { + // Optional: Versionsnummer auslesen... aber interessiert die? + offset += 4; + } else { + offset += art; + MessageOutput.println("Strange measurement skipped"); + } + } + } else if (grouptag == 0) { + // end marker + offset += grouplen; + } else { + MessageOutput.print("unhandled group "); + MessageOutput.print(grouptag); + MessageOutput.print(" with len="); + MessageOutput.println(grouplen); + offset += grouplen; + } + } while (grouplen > 0 && offset + 4 < buffer + rSize); + } +} +float SMA_HMClass::getPowerTotal() +{ + return _powerMeterPower; +} +float SMA_HMClass::getPowerL1() +{ + return _powerMeterL1; +} +float SMA_HMClass::getPowerL2() +{ + return _powerMeterL2; +} +float SMA_HMClass::getPowerL3() +{ + return _powerMeterL3; +} diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index e99a865aa..29523f747 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -553,6 +553,7 @@ "typeSDM3ph": "SDM 3 phase (SDM72/630)", "typeHTTP": "HTTP(S) + JSON", "typeSML": "SML (OBIS 16.7.0)", + "typeSMAHM2": "SMA Homemanager 2.0", "MqttTopicPowerMeter1": "MQTT topic - Stromzähler #1", "MqttTopicPowerMeter2": "MQTT topic - Stromzähler #2 (Optional)", "MqttTopicPowerMeter3": "MQTT topic - Stromzähler #3 (Optional)", @@ -817,7 +818,7 @@ "Close": "Schließen", "SetVoltageLimit": "Spannungslimit:", "SetCurrentLimit": "Stromlimit:", - "CurrentLimit": "Aktuelles Limit: " + "CurrentLimit": "Aktuelles Limit: " }, "acchargeradmin": { "ChargerSettings": "AC Ladegerät Einstellungen", diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index 532ac8b48..177d28bc9 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -555,6 +555,7 @@ "typeSDM3ph": "SDM 3 phase (SDM72/630)", "typeHTTP": "HTTP(s) + JSON", "typeSML": "SML (OBIS 16.7.0)", + "typeSMAHM2": "SMA Homemanager 2.0", "MqttTopicPowerMeter1": "MQTT topic - Power meter #1", "MqttTopicPowerMeter2": "MQTT topic - Power meter #2", "MqttTopicPowerMeter3": "MQTT topic - Power meter #3", @@ -824,7 +825,7 @@ "Close": "close", "SetVoltageLimit": "Voltage limit:", "SetCurrentLimit": "Current limit:", - "CurrentLimit": "Current limit:" + "CurrentLimit": "Current limit:" }, "acchargeradmin": { "ChargerSettings": "AC Charger Settings", diff --git a/webapp/src/views/PowerMeterAdminView.vue b/webapp/src/views/PowerMeterAdminView.vue index 280171cf1..b490b7c58 100644 --- a/webapp/src/views/PowerMeterAdminView.vue +++ b/webapp/src/views/PowerMeterAdminView.vue @@ -3,7 +3,7 @@ {{ alertMessage }} - +
@@ -83,7 +83,7 @@ - +
@@ -126,7 +126,7 @@ placeholder="http://admin:supersecret@mypowermeter.home/status" prefix="GET " :tooltip="$t('powermeteradmin.httpUrlDescription')" /> - +
@@ -236,6 +236,7 @@ export default defineComponent({ { key: 2, value: this.$t('powermeteradmin.typeSDM3ph') }, { key: 3, value: this.$t('powermeteradmin.typeHTTP') }, { key: 4, value: this.$t('powermeteradmin.typeSML') }, + { key: 5, value: this.$t('powermeteradmin.typeSMAHM2') }, ], powerMeterAuthList: [ { key: 0, value: "None" },