From 2397e5cdf5bba4108dc968f8476701a9bc814ad9 Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Wed, 8 May 2024 11:08:04 +0200 Subject: [PATCH] powermeter refactor: split providers into their own classes it is important to separate the capabilities of each power meter provider into their own class/source file, as the providers work fundamentally different and their implementations must not be intermangled, which made maintenance and improvements a nightmare in the past. --- include/PowerMeter.h | 72 +--- ...{HttpPowerMeter.h => PowerMeterHttpJson.h} | 21 +- ...TibberPowerMeter.h => PowerMeterHttpSml.h} | 29 +- include/PowerMeterMqtt.h | 28 ++ include/PowerMeterProvider.h | 46 +++ include/PowerMeterSerialSdm.h | 33 ++ include/PowerMeterSerialSml.h | 39 ++ ...SMA_HM.h => PowerMeterUdpSmaHomeManager.h} | 20 +- src/Display_Graphic.cpp | 2 +- src/Huawei_can.cpp | 4 +- src/PowerLimiter.cpp | 2 +- src/PowerMeter.cpp | 335 +++--------------- ...pPowerMeter.cpp => PowerMeterHttpJson.cpp} | 86 +++-- ...erPowerMeter.cpp => PowerMeterHttpSml.cpp} | 44 ++- src/PowerMeterMqtt.cpp | 74 ++++ src/PowerMeterProvider.cpp | 22 ++ src/PowerMeterSerialSdm.cpp | 113 ++++++ src/PowerMeterSerialSml.cpp | 68 ++++ ...HM.cpp => PowerMeterUdpSmaHomeManager.cpp} | 58 +-- src/WebApi_powermeter.cpp | 23 +- src/WebApi_ws_live.cpp | 4 +- 21 files changed, 667 insertions(+), 456 deletions(-) rename include/{HttpPowerMeter.h => PowerMeterHttpJson.h} (75%) rename include/{TibberPowerMeter.h => PowerMeterHttpSml.h} (51%) create mode 100644 include/PowerMeterMqtt.h create mode 100644 include/PowerMeterProvider.h create mode 100644 include/PowerMeterSerialSdm.h create mode 100644 include/PowerMeterSerialSml.h rename include/{SMA_HM.h => PowerMeterUdpSmaHomeManager.h} (53%) rename src/{HttpPowerMeter.cpp => PowerMeterHttpJson.cpp} (84%) rename src/{TibberPowerMeter.cpp => PowerMeterHttpSml.cpp} (84%) create mode 100644 src/PowerMeterMqtt.cpp create mode 100644 src/PowerMeterProvider.cpp create mode 100644 src/PowerMeterSerialSdm.cpp create mode 100644 src/PowerMeterSerialSml.cpp rename src/{SMA_HM.cpp => PowerMeterUdpSmaHomeManager.cpp} (76%) diff --git a/include/PowerMeter.h b/include/PowerMeter.h index 2d2c8749..44c99d06 100644 --- a/include/PowerMeter.h +++ b/include/PowerMeter.h @@ -1,79 +1,27 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once -#include "Configuration.h" -#include -#include -#include -#include -#include -#include "SDM.h" -#include "sml.h" +#include "PowerMeterProvider.h" #include -#include - -typedef struct { - const unsigned char OBIS[6]; - void (*Fn)(double&); - float* Arg; -} OBISHandler; +#include +#include class PowerMeterClass { public: - enum class Source : unsigned { - MQTT = 0, - SDM1PH = 1, - SDM3PH = 2, - HTTP = 3, - SML = 4, - SMAHM2 = 5, - TIBBER = 6 - }; void init(Scheduler& scheduler); - float getPowerTotal(bool forceUpdate = true); - uint32_t getLastPowerMeterUpdate(); - bool isDataValid(); - const std::list smlHandlerList{ - {{0x01, 0x00, 0x10, 0x07, 0x00, 0xff}, &smlOBISW, &_powerMeter1Power}, - {{0x01, 0x00, 0x01, 0x08, 0x00, 0xff}, &smlOBISWh, &_powerMeterImport}, - {{0x01, 0x00, 0x02, 0x08, 0x00, 0xff}, &smlOBISWh, &_powerMeterExport} - }; + void updateSettings(); + + float getPowerTotal() const; + uint32_t getLastUpdate() const; + bool isDataValid() const; + private: void loop(); - void mqtt(); - - void onMqttMessage(const espMqttClientTypes::MessageProperties& properties, - const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total); Task _loopTask; - - bool _verboseLogging = true; - uint32_t _lastPowerMeterCheck; - // Used in Power limiter for safety check - uint32_t _lastPowerMeterUpdate; - - float _powerMeter1Power = 0.0; - float _powerMeter2Power = 0.0; - float _powerMeter3Power = 0.0; - float _powerMeter1Voltage = 0.0; - float _powerMeter2Voltage = 0.0; - float _powerMeter3Voltage = 0.0; - float _powerMeterImport = 0.0; - float _powerMeterExport = 0.0; - - std::map _mqttSubscriptions; - mutable std::mutex _mutex; - - static char constexpr _sdmSerialPortOwner[] = "SDM power meter"; - std::unique_ptr _upSdmSerial = nullptr; - std::unique_ptr _upSdm = nullptr; - std::unique_ptr _upSmlSerial = nullptr; - - void readPowerMeter(); - - bool smlReadLoop(); + std::unique_ptr _upProvider = nullptr; }; extern PowerMeterClass PowerMeter; diff --git a/include/HttpPowerMeter.h b/include/PowerMeterHttpJson.h similarity index 75% rename from include/HttpPowerMeter.h rename to include/PowerMeterHttpJson.h index 8f703bba..9e548210 100644 --- a/include/HttpPowerMeter.h +++ b/include/PowerMeterHttpJson.h @@ -5,22 +5,29 @@ #include #include #include "Configuration.h" +#include "PowerMeterProvider.h" using Auth_t = PowerMeterHttpConfig::Auth; using Unit_t = PowerMeterHttpConfig::Unit; -class HttpPowerMeterClass { +class PowerMeterHttpJson : public PowerMeterProvider { public: - void init(); - bool updateValues(); - float getPower(int8_t phase); - char httpPowerMeterError[256]; + bool init() final { return true; } + void deinit() final { } + void loop() final; + float getPowerTotal() const final; + void doMqttPublish() const final; + bool queryPhase(int phase, PowerMeterHttpConfig const& config); + char httpPowerMeterError[256]; private: - float power[POWERMETER_MAX_PHASES]; + uint32_t _lastPoll; + std::array _cache; + std::array _powerValues; HTTPClient httpClient; String httpResponse; + bool httpRequest(int phase, WiFiClient &wifiClient, const String& host, uint16_t port, const String& uri, bool https, PowerMeterHttpConfig const& config); bool extractUrlComponents(String url, String& _protocol, String& _hostname, String& _uri, uint16_t& uint16_t, String& _base64Authorization); String extractParam(String& authReq, const String& param, const char delimit); @@ -30,5 +37,3 @@ private: void prepareRequest(uint32_t timeout, const char* httpHeader, const char* httpValue); String sha256(const String& data); }; - -extern HttpPowerMeterClass HttpPowerMeter; diff --git a/include/TibberPowerMeter.h b/include/PowerMeterHttpSml.h similarity index 51% rename from include/TibberPowerMeter.h rename to include/PowerMeterHttpSml.h index 84639ab7..31b44244 100644 --- a/include/TibberPowerMeter.h +++ b/include/PowerMeterHttpSml.h @@ -1,23 +1,46 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once +#include +#include #include #include #include #include "Configuration.h" +#include "PowerMeterProvider.h" +#include "sml.h" -class TibberPowerMeterClass { +class PowerMeterHttpSml : public PowerMeterProvider { public: + bool init() final { return true; } + void deinit() final { } + void loop() final; + float getPowerTotal() const final; + void doMqttPublish() const final; bool updateValues(); char tibberPowerMeterError[256]; bool query(PowerMeterTibberConfig const& config); private: + mutable std::mutex _mutex; + + uint32_t _lastPoll = 0; + + float _activePower = 0.0; + + typedef struct { + const unsigned char OBIS[6]; + void (*Fn)(double&); + float* Arg; + } OBISHandler; + + const std::list smlHandlerList{ + {{0x01, 0x00, 0x10, 0x07, 0x00, 0xff}, &smlOBISW, &_activePower} + }; + HTTPClient httpClient; String httpResponse; bool httpRequest(WiFiClient &wifiClient, const String& host, uint16_t port, const String& uri, bool https, PowerMeterTibberConfig const& config); bool extractUrlComponents(String url, String& _protocol, String& _hostname, String& _uri, uint16_t& uint16_t, String& _base64Authorization); void prepareRequest(uint32_t timeout); }; - -extern TibberPowerMeterClass TibberPowerMeter; diff --git a/include/PowerMeterMqtt.h b/include/PowerMeterMqtt.h new file mode 100644 index 00000000..5dd01d2c --- /dev/null +++ b/include/PowerMeterMqtt.h @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include "PowerMeterProvider.h" +#include +#include +#include + +class PowerMeterMqtt : public PowerMeterProvider { +public: + bool init() final; + void deinit() final; + void loop() final { } + float getPowerTotal() const final; + void doMqttPublish() const final; + +private: + void onMqttMessage(const espMqttClientTypes::MessageProperties& properties, + const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total); + + float _powerValueOne = 0; + float _powerValueTwo = 0; + float _powerValueThree = 0; + + std::map _mqttSubscriptions; + + mutable std::mutex _mutex; +}; diff --git a/include/PowerMeterProvider.h b/include/PowerMeterProvider.h new file mode 100644 index 00000000..9fb74c78 --- /dev/null +++ b/include/PowerMeterProvider.h @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include "Configuration.h" + +class PowerMeterProvider { +public: + virtual ~PowerMeterProvider() { } + + enum class Type : unsigned { + MQTT = 0, + SDM1PH = 1, + SDM3PH = 2, + HTTP = 3, + SML = 4, + SMAHM2 = 5, + TIBBER = 6 + }; + + // returns true if the provider is ready for use, false otherwise + virtual bool init() = 0; + + virtual void deinit() = 0; + virtual void loop() = 0; + virtual float getPowerTotal() const = 0; + + uint32_t getLastUpdate() const { return _lastUpdate; } + bool isDataValid() const; + void mqttLoop() const; + +protected: + PowerMeterProvider() { + auto const& config = Configuration.get(); + _verboseLogging = config.PowerMeter.VerboseLogging; + } + + void gotUpdate() { _lastUpdate = millis(); } + + bool _verboseLogging; + +private: + virtual void doMqttPublish() const = 0; + + uint32_t _lastUpdate = 0; + mutable uint32_t _lastMqttPublish = 0; +}; diff --git a/include/PowerMeterSerialSdm.h b/include/PowerMeterSerialSdm.h new file mode 100644 index 00000000..7e01c8f7 --- /dev/null +++ b/include/PowerMeterSerialSdm.h @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#include "PowerMeterProvider.h" +#include "SDM.h" + +class PowerMeterSerialSdm : public PowerMeterProvider { +public: + bool init() final; + void deinit() final; + void loop() final; + float getPowerTotal() const final; + void doMqttPublish() const final; + +private: + uint32_t _lastPoll; + + float _phase1Power = 0.0; + float _phase2Power = 0.0; + float _phase3Power = 0.0; + float _phase1Voltage = 0.0; + float _phase2Voltage = 0.0; + float _phase3Voltage = 0.0; + float _energyImport = 0.0; + float _energyExport = 0.0; + + mutable std::mutex _mutex; + + static char constexpr _sdmSerialPortOwner[] = "SDM power meter"; + std::unique_ptr _upSdmSerial = nullptr; + std::unique_ptr _upSdm = nullptr; +}; diff --git a/include/PowerMeterSerialSml.h b/include/PowerMeterSerialSml.h new file mode 100644 index 00000000..31a90484 --- /dev/null +++ b/include/PowerMeterSerialSml.h @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include "PowerMeterProvider.h" +#include "Configuration.h" +#include "sml.h" +#include +#include +#include + +class PowerMeterSerialSml : public PowerMeterProvider { +public: + bool init() final; + void deinit() final; + void loop() final; + float getPowerTotal() const final { return _activePower; } + void doMqttPublish() const final; + +private: + float _activePower = 0.0; + float _energyImport = 0.0; + float _energyExport = 0.0; + + mutable std::mutex _mutex; + + std::unique_ptr _upSmlSerial = nullptr; + + typedef struct { + const unsigned char OBIS[6]; + void (*Fn)(double&); + float* Arg; + } OBISHandler; + + const std::list smlHandlerList{ + {{0x01, 0x00, 0x10, 0x07, 0x00, 0xff}, &smlOBISW, &_activePower}, + {{0x01, 0x00, 0x01, 0x08, 0x00, 0xff}, &smlOBISWh, &_energyImport}, + {{0x01, 0x00, 0x02, 0x08, 0x00, 0xff}, &smlOBISWh, &_energyExport} + }; +}; diff --git a/include/SMA_HM.h b/include/PowerMeterUdpSmaHomeManager.h similarity index 53% rename from include/SMA_HM.h rename to include/PowerMeterUdpSmaHomeManager.h index e5600902..34e47f4d 100644 --- a/include/SMA_HM.h +++ b/include/PowerMeterUdpSmaHomeManager.h @@ -5,17 +5,15 @@ #pragma once #include -#include +#include "PowerMeterProvider.h" -class SMA_HMClass { +class PowerMeterUdpSmaHomeManager : public PowerMeterProvider { public: - void init(Scheduler& scheduler, bool verboseLogging); - void loop(); - void event1(); - float getPowerTotal() const { return _powerMeterPower; } - float getPowerL1() const { return _powerMeterL1; } - float getPowerL2() const { return _powerMeterL2; } - float getPowerL3() const { return _powerMeterL3; } + bool init() final; + void deinit() final; + void loop() final; + float getPowerTotal() const final { return _powerMeterPower; } + void doMqttPublish() const final; private: void Soutput(int kanal, int index, int art, int tarif, @@ -23,14 +21,10 @@ private: uint8_t* decodeGroup(uint8_t* offset, uint16_t grouplen); - bool _verboseLogging = false; float _powerMeterPower = 0.0; float _powerMeterL1 = 0.0; float _powerMeterL2 = 0.0; float _powerMeterL3 = 0.0; uint32_t _previousMillis = 0; uint32_t _serial = 0; - Task _loopTask; }; - -extern SMA_HMClass SMA_HM; diff --git a/src/Display_Graphic.cpp b/src/Display_Graphic.cpp index 1b08aff8..540554f2 100644 --- a/src/Display_Graphic.cpp +++ b/src/Display_Graphic.cpp @@ -294,7 +294,7 @@ void DisplayGraphicClass::loop() _display->drawBox(0, y, _display->getDisplayWidth(), lineHeight); _display->setDrawColor(1); - auto acPower = PowerMeter.getPowerTotal(false); + auto acPower = PowerMeter.getPowerTotal(); if (acPower > 999) { snprintf(_fmtText, sizeof(_fmtText), i18n_meter_power_kw[_display_language], (acPower / 1000)); } else { diff --git a/src/Huawei_can.cpp b/src/Huawei_can.cpp index 5f378602..aef57a19 100644 --- a/src/Huawei_can.cpp +++ b/src/Huawei_can.cpp @@ -371,12 +371,12 @@ void HuaweiCanClass::loop() } } - if (PowerMeter.getLastPowerMeterUpdate() > _lastPowerMeterUpdateReceivedMillis && + if (PowerMeter.getLastUpdate() > _lastPowerMeterUpdateReceivedMillis && _autoPowerEnabledCounter > 0) { // We have received a new PowerMeter value. Also we're _autoPowerEnabled // So we're good to calculate a new limit - _lastPowerMeterUpdateReceivedMillis = PowerMeter.getLastPowerMeterUpdate(); + _lastPowerMeterUpdateReceivedMillis = PowerMeter.getLastUpdate(); // Calculate new power limit float newPowerLimit = -1 * round(PowerMeter.getPowerTotal()); diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp index d9e28048..5851f327 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -201,7 +201,7 @@ void PowerLimiterClass::loop() // arrives. this can be the case for readings provided by networked meter // readers, where a packet needs to travel through the network for some // time after the actual measurement was done by the reader. - if (PowerMeter.isDataValid() && PowerMeter.getLastPowerMeterUpdate() <= (*_oInverterStatsMillis + 2000)) { + if (PowerMeter.isDataValid() && PowerMeter.getLastUpdate() <= (*_oInverterStatsMillis + 2000)) { return announceStatus(Status::PowerMeterPending); } diff --git a/src/PowerMeter.cpp b/src/PowerMeter.cpp index e1e2d432..8212529b 100644 --- a/src/PowerMeter.cpp +++ b/src/PowerMeter.cpp @@ -1,18 +1,12 @@ // SPDX-License-Identifier: GPL-2.0-or-later -/* - * Copyright (C) 2022 Thomas Basler and others - */ #include "PowerMeter.h" #include "Configuration.h" -#include "PinMapping.h" -#include "HttpPowerMeter.h" -#include "TibberPowerMeter.h" -#include "MqttSettings.h" -#include "NetworkSettings.h" -#include "MessageOutput.h" -#include "SerialPortManager.h" -#include -#include +#include "PowerMeterHttpJson.h" +#include "PowerMeterHttpSml.h" +#include "PowerMeterMqtt.h" +#include "PowerMeterSerialSdm.h" +#include "PowerMeterSerialSml.h" +#include "PowerMeterUdpSmaHomeManager.h" PowerMeterClass PowerMeter; @@ -23,285 +17,74 @@ void PowerMeterClass::init(Scheduler& scheduler) _loopTask.setIterations(TASK_FOREVER); _loopTask.enable(); - _lastPowerMeterCheck = 0; - _lastPowerMeterUpdate = 0; - - for (auto const& s: _mqttSubscriptions) { MqttSettings.unsubscribe(s.first); } - _mqttSubscriptions.clear(); - - CONFIG_T& config = Configuration.get(); - - if (!config.PowerMeter.Enabled) { - return; - } - - const PinMapping_t& pin = PinMapping.get(); - MessageOutput.printf("[PowerMeter] rx = %d, tx = %d, dere = %d\r\n", - pin.powermeter_rx, pin.powermeter_tx, pin.powermeter_dere); - - switch(static_cast(config.PowerMeter.Source)) { - case Source::MQTT: { - auto subscribe = [this](char const* topic, float* target) { - if (strlen(topic) == 0) { return; } - MqttSettings.subscribe(topic, 0, - std::bind(&PowerMeterClass::onMqttMessage, - this, std::placeholders::_1, std::placeholders::_2, - std::placeholders::_3, std::placeholders::_4, - std::placeholders::_5, std::placeholders::_6) - ); - _mqttSubscriptions.try_emplace(topic, target); - }; - - subscribe(config.PowerMeter.MqttTopicPowerMeter1, &_powerMeter1Power); - subscribe(config.PowerMeter.MqttTopicPowerMeter2, &_powerMeter2Power); - subscribe(config.PowerMeter.MqttTopicPowerMeter3, &_powerMeter3Power); - break; - } - - case Source::SDM1PH: - case Source::SDM3PH: { - if (pin.powermeter_rx < 0 || pin.powermeter_tx < 0) { - MessageOutput.println("[PowerMeter] invalid pin config for SDM power meter (RX and TX pins must be defined)"); - return; - } - - auto oHwSerialPort = SerialPortManager.allocatePort(_sdmSerialPortOwner); - if (!oHwSerialPort) { return; } - - _upSdmSerial = std::make_unique(*oHwSerialPort); - _upSdmSerial->end(); // make sure the UART will be re-initialized - _upSdm = std::make_unique(*_upSdmSerial, 9600, pin.powermeter_dere, - SERIAL_8N1, pin.powermeter_rx, pin.powermeter_tx); - _upSdm->begin(); - break; - } - - case Source::HTTP: - HttpPowerMeter.init(); - break; - - case Source::SML: - if (pin.powermeter_rx < 0) { - MessageOutput.println("[PowerMeter] invalid pin config for SML power meter (RX pin must be defined)"); - return; - } - - pinMode(pin.powermeter_rx, INPUT); - _upSmlSerial = std::make_unique(); - _upSmlSerial->begin(9600, SWSERIAL_8N1, pin.powermeter_rx, -1, false, 128, 95); - _upSmlSerial->enableRx(true); - _upSmlSerial->enableTx(false); - _upSmlSerial->flush(); - break; - - case Source::SMAHM2: - SMA_HM.init(scheduler, config.PowerMeter.VerboseLogging); - break; - - case Source::TIBBER: - break; - } + updateSettings(); } -void PowerMeterClass::onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) -{ - for (auto const& subscription: _mqttSubscriptions) { - if (subscription.first != topic) { continue; } - - std::string value(reinterpret_cast(payload), len); - try { - *subscription.second = std::stof(value); - } - catch(std::invalid_argument const& e) { - MessageOutput.printf("PowerMeterClass: cannot parse payload of topic '%s' as float: %s\r\n", - topic, value.c_str()); - return; - } - - if (_verboseLogging) { - MessageOutput.printf("PowerMeterClass: Updated from '%s', TotalPower: %5.2f\r\n", - topic, getPowerTotal()); - } - - _lastPowerMeterUpdate = millis(); - } -} - -float PowerMeterClass::getPowerTotal(bool forceUpdate) -{ - if (forceUpdate) { - CONFIG_T& config = Configuration.get(); - if (config.PowerMeter.Enabled - && (millis() - _lastPowerMeterUpdate) > (1000)) { - readPowerMeter(); - } - } - - std::lock_guard l(_mutex); - return _powerMeter1Power + _powerMeter2Power + _powerMeter3Power; -} - -uint32_t PowerMeterClass::getLastPowerMeterUpdate() +void PowerMeterClass::updateSettings() { std::lock_guard l(_mutex); - return _lastPowerMeterUpdate; -} -bool PowerMeterClass::isDataValid() -{ + if (_upProvider) { + _upProvider->deinit(); + _upProvider = nullptr; + } + auto const& config = Configuration.get(); - std::lock_guard l(_mutex); + if (!config.PowerMeter.Enabled) { return; } - bool valid = config.PowerMeter.Enabled && - _lastPowerMeterUpdate > 0 && - ((millis() - _lastPowerMeterUpdate) < (30 * 1000)); + switch(static_cast(config.PowerMeter.Source)) { + case PowerMeterProvider::Type::MQTT: + _upProvider = std::make_unique(); + break; + case PowerMeterProvider::Type::SDM1PH: + case PowerMeterProvider::Type::SDM3PH: + _upProvider = std::make_unique(); + break; + case PowerMeterProvider::Type::HTTP: + _upProvider = std::make_unique(); + break; + case PowerMeterProvider::Type::SML: + _upProvider = std::make_unique(); + break; + case PowerMeterProvider::Type::SMAHM2: + _upProvider = std::make_unique(); + break; + case PowerMeterProvider::Type::TIBBER: + _upProvider = std::make_unique(); + break; + } - // reset if timed out to avoid glitch once - // (millis() - _lastPowerMeterUpdate) overflows - if (!valid) { _lastPowerMeterUpdate = 0; } - - return valid; + if (!_upProvider->init()) { + _upProvider = nullptr; + } } -void PowerMeterClass::mqtt() +float PowerMeterClass::getPowerTotal() const { - if (!MqttSettings.getConnected()) { return; } - - String topic = "powermeter"; - auto totalPower = getPowerTotal(); - std::lock_guard l(_mutex); - MqttSettings.publish(topic + "/power1", String(_powerMeter1Power)); - MqttSettings.publish(topic + "/power2", String(_powerMeter2Power)); - MqttSettings.publish(topic + "/power3", String(_powerMeter3Power)); - MqttSettings.publish(topic + "/powertotal", String(totalPower)); - MqttSettings.publish(topic + "/voltage1", String(_powerMeter1Voltage)); - MqttSettings.publish(topic + "/voltage2", String(_powerMeter2Voltage)); - MqttSettings.publish(topic + "/voltage3", String(_powerMeter3Voltage)); - MqttSettings.publish(topic + "/import", String(_powerMeterImport)); - MqttSettings.publish(topic + "/export", String(_powerMeterExport)); + if (!_upProvider) { return 0.0; } + return _upProvider->getPowerTotal(); +} + +uint32_t PowerMeterClass::getLastUpdate() const +{ + std::lock_guard l(_mutex); + if (!_upProvider) { return 0; } + return _upProvider->getLastUpdate(); +} + +bool PowerMeterClass::isDataValid() const +{ + std::lock_guard l(_mutex); + if (!_upProvider) { return false; } + return _upProvider->isDataValid(); } void PowerMeterClass::loop() { - CONFIG_T const& config = Configuration.get(); - _verboseLogging = config.PowerMeter.VerboseLogging; - - if (!config.PowerMeter.Enabled) { return; } - - if (static_cast(config.PowerMeter.Source) == Source::SML && - nullptr != _upSmlSerial) { - if (!smlReadLoop()) { return; } - _lastPowerMeterUpdate = millis(); - } - - if ((millis() - _lastPowerMeterCheck) < (config.PowerMeter.Interval * 1000)) { - return; - } - - readPowerMeter(); - - MessageOutput.printf("PowerMeterClass: TotalPower: %5.2f\r\n", getPowerTotal()); - - mqtt(); - - _lastPowerMeterCheck = millis(); -} - -void PowerMeterClass::readPowerMeter() -{ - CONFIG_T& config = Configuration.get(); - - uint8_t _address = config.PowerMeter.SdmAddress; - Source configuredSource = static_cast(config.PowerMeter.Source); - - if (configuredSource == Source::SDM1PH) { - if (!_upSdm) { return; } - - // this takes a "very long" time as each readVal() is a synchronous - // exchange of serial messages. cache the values and write later. - auto phase1Power = _upSdm->readVal(SDM_PHASE_1_POWER, _address); - auto phase1Voltage = _upSdm->readVal(SDM_PHASE_1_VOLTAGE, _address); - auto energyImport = _upSdm->readVal(SDM_IMPORT_ACTIVE_ENERGY, _address); - auto energyExport = _upSdm->readVal(SDM_EXPORT_ACTIVE_ENERGY, _address); - - std::lock_guard l(_mutex); - _powerMeter1Power = static_cast(phase1Power); - _powerMeter2Power = 0; - _powerMeter3Power = 0; - _powerMeter1Voltage = static_cast(phase1Voltage); - _powerMeter2Voltage = 0; - _powerMeter3Voltage = 0; - _powerMeterImport = static_cast(energyImport); - _powerMeterExport = static_cast(energyExport); - _lastPowerMeterUpdate = millis(); - } - else if (configuredSource == Source::SDM3PH) { - if (!_upSdm) { return; } - - // this takes a "very long" time as each readVal() is a synchronous - // exchange of serial messages. cache the values and write later. - auto phase1Power = _upSdm->readVal(SDM_PHASE_1_POWER, _address); - auto phase2Power = _upSdm->readVal(SDM_PHASE_2_POWER, _address); - auto phase3Power = _upSdm->readVal(SDM_PHASE_3_POWER, _address); - auto phase1Voltage = _upSdm->readVal(SDM_PHASE_1_VOLTAGE, _address); - auto phase2Voltage = _upSdm->readVal(SDM_PHASE_2_VOLTAGE, _address); - auto phase3Voltage = _upSdm->readVal(SDM_PHASE_3_VOLTAGE, _address); - auto energyImport = _upSdm->readVal(SDM_IMPORT_ACTIVE_ENERGY, _address); - auto energyExport = _upSdm->readVal(SDM_EXPORT_ACTIVE_ENERGY, _address); - - std::lock_guard l(_mutex); - _powerMeter1Power = static_cast(phase1Power); - _powerMeter2Power = static_cast(phase2Power); - _powerMeter3Power = static_cast(phase3Power); - _powerMeter1Voltage = static_cast(phase1Voltage); - _powerMeter2Voltage = static_cast(phase2Voltage); - _powerMeter3Voltage = static_cast(phase3Voltage); - _powerMeterImport = static_cast(energyImport); - _powerMeterExport = static_cast(energyExport); - _lastPowerMeterUpdate = millis(); - } - else if (configuredSource == Source::HTTP) { - if (HttpPowerMeter.updateValues()) { - std::lock_guard l(_mutex); - _powerMeter1Power = HttpPowerMeter.getPower(1); - _powerMeter2Power = HttpPowerMeter.getPower(2); - _powerMeter3Power = HttpPowerMeter.getPower(3); - _lastPowerMeterUpdate = millis(); - } - } - else if (configuredSource == Source::SMAHM2) { - std::lock_guard l(_mutex); - _powerMeter1Power = SMA_HM.getPowerL1(); - _powerMeter2Power = SMA_HM.getPowerL2(); - _powerMeter3Power = SMA_HM.getPowerL3(); - _lastPowerMeterUpdate = millis(); - } - else if (configuredSource == Source::TIBBER) { - if (TibberPowerMeter.updateValues()) { - _lastPowerMeterUpdate = millis(); - } - } -} - -bool PowerMeterClass::smlReadLoop() -{ - while (_upSmlSerial->available()) { - double readVal = 0; - unsigned char smlCurrentChar = _upSmlSerial->read(); - sml_states_t smlCurrentState = smlState(smlCurrentChar); - if (smlCurrentState == SML_LISTEND) { - for (auto& handler: smlHandlerList) { - if (smlOBISCheck(handler.OBIS)) { - handler.Fn(readVal); - *handler.Arg = readVal; - } - } - } else if (smlCurrentState == SML_FINAL) { - return true; - } - } - - return false; + std::lock_guard lock(_mutex); + if (!_upProvider) { return; } + _upProvider->loop(); + _upProvider->mqttLoop(); } diff --git a/src/HttpPowerMeter.cpp b/src/PowerMeterHttpJson.cpp similarity index 84% rename from src/HttpPowerMeter.cpp rename to src/PowerMeterHttpJson.cpp index e3033cbb..bf13ca25 100644 --- a/src/HttpPowerMeter.cpp +++ b/src/PowerMeterHttpJson.cpp @@ -1,7 +1,8 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include "Configuration.h" -#include "HttpPowerMeter.h" +#include "PowerMeterHttpJson.h" #include "MessageOutput.h" +#include "MqttSettings.h" #include #include #include "mbedtls/sha256.h" @@ -9,48 +10,63 @@ #include #include -void HttpPowerMeterClass::init() -{ -} - -float HttpPowerMeterClass::getPower(int8_t phase) -{ - if (phase < 1 || phase > POWERMETER_MAX_PHASES) { return 0.0; } - - return power[phase - 1]; -} - -bool HttpPowerMeterClass::updateValues() +void PowerMeterHttpJson::loop() { auto const& config = Configuration.get(); + if ((millis() - _lastPoll) < (config.PowerMeter.Interval * 1000)) { + return; + } + + _lastPoll = millis(); for (uint8_t i = 0; i < POWERMETER_MAX_PHASES; i++) { auto const& phaseConfig = config.PowerMeter.Http_Phase[i]; if (!phaseConfig.Enabled) { - power[i] = 0.0; + _cache[i] = 0.0; continue; } if (i == 0 || config.PowerMeter.HttpIndividualRequests) { if (!queryPhase(i, phaseConfig)) { - MessageOutput.printf("[HttpPowerMeter] Getting the power of phase %d failed.\r\n", i + 1); + MessageOutput.printf("[PowerMeterHttpJson] Getting HTTP response for phase %d failed.\r\n", i + 1); MessageOutput.printf("%s\r\n", httpPowerMeterError); - return false; + return; } continue; } if(!tryGetFloatValueForPhase(i, phaseConfig.JsonPath, phaseConfig.PowerUnit, phaseConfig.SignInverted)) { - MessageOutput.printf("[HttpPowerMeter] Getting the power of phase %d (from JSON fetched with Phase 1 config) failed.\r\n", i + 1); + MessageOutput.printf("[PowerMeterHttpJson] Reading power of phase %d (from JSON fetched with Phase 1 config) failed.\r\n", i + 1); MessageOutput.printf("%s\r\n", httpPowerMeterError); - return false; + return; } } - return true; + + gotUpdate(); + + _powerValues = _cache; } -bool HttpPowerMeterClass::queryPhase(int phase, PowerMeterHttpConfig const& config) +float PowerMeterHttpJson::getPowerTotal() const +{ + float sum = 0.0; + for (auto v: _powerValues) { sum += v; } + return sum; +} + +void PowerMeterHttpJson::doMqttPublish() const +{ + String topic = "powermeter"; + auto power = getPowerTotal(); + + MqttSettings.publish(topic + "/power1", String(_powerValues[0])); + MqttSettings.publish(topic + "/power2", String(_powerValues[1])); + MqttSettings.publish(topic + "/power3", String(_powerValues[2])); + MqttSettings.publish(topic + "/powertotal", String(power)); +} + +bool PowerMeterHttpJson::queryPhase(int phase, PowerMeterHttpConfig const& config) { //hostByName in WiFiGeneric fails to resolve local names. issue described in //https://github.com/espressif/arduino-esp32/issues/3822 @@ -107,7 +123,7 @@ bool HttpPowerMeterClass::queryPhase(int phase, PowerMeterHttpConfig const& conf return httpRequest(phase, *wifiClient, ipaddr.toString(), port, uri, https, config); } -bool HttpPowerMeterClass::httpRequest(int phase, WiFiClient &wifiClient, const String& host, uint16_t port, const String& uri, bool https, PowerMeterHttpConfig const& config) +bool PowerMeterHttpJson::httpRequest(int phase, WiFiClient &wifiClient, const String& host, uint16_t port, const String& uri, bool https, PowerMeterHttpConfig const& config) { if(!httpClient.begin(wifiClient, host, port, uri, https)){ snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("httpClient.begin() failed for %s://%s"), (https ? "https" : "http"), host.c_str()); @@ -163,13 +179,13 @@ bool HttpPowerMeterClass::httpRequest(int phase, WiFiClient &wifiClient, const S return tryGetFloatValueForPhase(phase, config.JsonPath, config.PowerUnit, config.SignInverted); } -String HttpPowerMeterClass::extractParam(String& authReq, const String& param, const char delimit) { +String PowerMeterHttpJson::extractParam(String& authReq, const String& param, const char delimit) { int _begin = authReq.indexOf(param); if (_begin == -1) { return ""; } return authReq.substring(_begin + param.length(), authReq.indexOf(delimit, _begin + param.length())); } -String HttpPowerMeterClass::getcNonce(const int len) { +String PowerMeterHttpJson::getcNonce(const int len) { static const char alphanum[] = "0123456789" "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "abcdefghijklmnopqrstuvwxyz"; @@ -180,7 +196,7 @@ String HttpPowerMeterClass::getcNonce(const int len) { return s; } -String HttpPowerMeterClass::getDigestAuth(String& authReq, const String& username, const String& password, const String& method, const String& uri, unsigned int counter) { +String PowerMeterHttpJson::getDigestAuth(String& authReq, const String& username, const String& password, const String& method, const String& uri, unsigned int counter) { // extracting required parameters for RFC 2617 Digest String realm = extractParam(authReq, "realm=\"", '"'); String nonce = extractParam(authReq, "nonce=\"", '"'); @@ -218,13 +234,13 @@ String HttpPowerMeterClass::getDigestAuth(String& authReq, const String& usernam return authorization; } -bool HttpPowerMeterClass::tryGetFloatValueForPhase(int phase, String jsonPath, Unit_t unit, bool signInverted) +bool PowerMeterHttpJson::tryGetFloatValueForPhase(int phase, String jsonPath, Unit_t unit, bool signInverted) { JsonDocument root; const DeserializationError error = deserializeJson(root, httpResponse); if (error) { snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), - PSTR("[HttpPowerMeter] Unable to parse server response as JSON")); + PSTR("[PowerMeterHttpJson] Unable to parse server response as JSON")); return false; } @@ -286,32 +302,32 @@ bool HttpPowerMeterClass::tryGetFloatValueForPhase(int phase, String jsonPath, U if (!value.is()) { snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), - PSTR("[HttpPowerMeter] not a float: '%s'"), + PSTR("[PowerMeterHttpJson] not a float: '%s'"), value.as().c_str()); return false; } // this value is supposed to be in Watts and positive if energy is consumed. - power[phase] = value.as(); + _cache[phase] = value.as(); switch (unit) { case Unit_t::MilliWatts: - power[phase] /= 1000; + _cache[phase] /= 1000; break; case Unit_t::KiloWatts: - power[phase] *= 1000; + _cache[phase] *= 1000; break; default: break; } - if (signInverted) { power[phase] *= -1; } + if (signInverted) { _cache[phase] *= -1; } return true; } //extract url component as done by httpClient::begin(String url, const char* expectedProtocol) https://github.com/espressif/arduino-esp32/blob/da6325dd7e8e152094b19fe63190907f38ef1ff0/libraries/HTTPClient/src/HTTPClient.cpp#L250 -bool HttpPowerMeterClass::extractUrlComponents(String url, String& _protocol, String& _host, String& _uri, uint16_t& _port, String& _base64Authorization) +bool PowerMeterHttpJson::extractUrlComponents(String url, String& _protocol, String& _host, String& _uri, uint16_t& _port, String& _base64Authorization) { // check for : (http: or https: int index = url.indexOf(':'); @@ -361,7 +377,7 @@ bool HttpPowerMeterClass::extractUrlComponents(String url, String& _protocol, St return true; } -String HttpPowerMeterClass::sha256(const String& data) { +String PowerMeterHttpJson::sha256(const String& data) { uint8_t hash[32]; mbedtls_sha256_context ctx; @@ -379,7 +395,7 @@ String HttpPowerMeterClass::sha256(const String& data) { return res; } -void HttpPowerMeterClass::prepareRequest(uint32_t timeout, const char* httpHeader, const char* httpValue) { +void PowerMeterHttpJson::prepareRequest(uint32_t timeout, const char* httpHeader, const char* httpValue) { httpClient.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); httpClient.setUserAgent("OpenDTU-OnBattery"); httpClient.setConnectTimeout(timeout); @@ -391,5 +407,3 @@ void HttpPowerMeterClass::prepareRequest(uint32_t timeout, const char* httpHeade httpClient.addHeader(httpHeader, httpValue); } } - -HttpPowerMeterClass HttpPowerMeter; diff --git a/src/TibberPowerMeter.cpp b/src/PowerMeterHttpSml.cpp similarity index 84% rename from src/TibberPowerMeter.cpp rename to src/PowerMeterHttpSml.cpp index d7889c22..e7462fdf 100644 --- a/src/TibberPowerMeter.cpp +++ b/src/PowerMeterHttpSml.cpp @@ -1,28 +1,44 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include "Configuration.h" -#include "TibberPowerMeter.h" +#include "PowerMeterHttpSml.h" #include "MessageOutput.h" +#include "MqttSettings.h" #include #include #include -#include -bool TibberPowerMeterClass::updateValues() +float PowerMeterHttpSml::getPowerTotal() const +{ + std::lock_guard l(_mutex); + return _activePower; +} + +void PowerMeterHttpSml::doMqttPublish() const +{ + String topic = "powermeter"; + + std::lock_guard l(_mutex); + MqttSettings.publish(topic + "/powertotal", String(_activePower)); +} + +void PowerMeterHttpSml::loop() { auto const& config = Configuration.get(); + if ((millis() - _lastPoll) < (config.PowerMeter.Interval * 1000)) { + return; + } + + _lastPoll = millis(); auto const& tibberConfig = config.PowerMeter.Tibber; if (!query(tibberConfig)) { - MessageOutput.printf("[TibberPowerMeter] Getting the power of tibber failed.\r\n"); + MessageOutput.printf("[PowerMeterHttpSml] Getting the power value failed.\r\n"); MessageOutput.printf("%s\r\n", tibberPowerMeterError); - return false; } - - return true; } -bool TibberPowerMeterClass::query(PowerMeterTibberConfig const& config) +bool PowerMeterHttpSml::query(PowerMeterTibberConfig const& config) { //hostByName in WiFiGeneric fails to resolve local names. issue described in //https://github.com/espressif/arduino-esp32/issues/3822 @@ -79,7 +95,7 @@ bool TibberPowerMeterClass::query(PowerMeterTibberConfig const& config) return httpRequest(*wifiClient, ipaddr.toString(), port, uri, https, config); } -bool TibberPowerMeterClass::httpRequest(WiFiClient &wifiClient, const String& host, uint16_t port, const String& uri, bool https, PowerMeterTibberConfig const& config) +bool PowerMeterHttpSml::httpRequest(WiFiClient &wifiClient, const String& host, uint16_t port, const String& uri, bool https, PowerMeterTibberConfig const& config) { if(!httpClient.begin(wifiClient, host, port, uri, https)){ snprintf_P(tibberPowerMeterError, sizeof(tibberPowerMeterError), PSTR("httpClient.begin() failed for %s://%s"), (https ? "https" : "http"), host.c_str()); @@ -112,10 +128,12 @@ bool TibberPowerMeterClass::httpRequest(WiFiClient &wifiClient, const String& ho unsigned char smlCurrentChar = httpClient.getStream().read(); sml_states_t smlCurrentState = smlState(smlCurrentChar); if (smlCurrentState == SML_LISTEND) { - for (auto& handler: PowerMeter.smlHandlerList) { + for (auto& handler: smlHandlerList) { if (smlOBISCheck(handler.OBIS)) { + std::lock_guard l(_mutex); handler.Fn(readVal); *handler.Arg = readVal; + gotUpdate(); } } } @@ -126,7 +144,7 @@ bool TibberPowerMeterClass::httpRequest(WiFiClient &wifiClient, const String& ho } //extract url component as done by httpClient::begin(String url, const char* expectedProtocol) https://github.com/espressif/arduino-esp32/blob/da6325dd7e8e152094b19fe63190907f38ef1ff0/libraries/HTTPClient/src/HTTPClient.cpp#L250 -bool TibberPowerMeterClass::extractUrlComponents(String url, String& _protocol, String& _host, String& _uri, uint16_t& _port, String& _base64Authorization) +bool PowerMeterHttpSml::extractUrlComponents(String url, String& _protocol, String& _host, String& _uri, uint16_t& _port, String& _base64Authorization) { // check for : (http: or https: int index = url.indexOf(':'); @@ -176,7 +194,7 @@ bool TibberPowerMeterClass::extractUrlComponents(String url, String& _protocol, return true; } -void TibberPowerMeterClass::prepareRequest(uint32_t timeout) { +void PowerMeterHttpSml::prepareRequest(uint32_t timeout) { httpClient.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); httpClient.setUserAgent("OpenDTU-OnBattery"); httpClient.setConnectTimeout(timeout); @@ -184,5 +202,3 @@ void TibberPowerMeterClass::prepareRequest(uint32_t timeout) { httpClient.addHeader("Content-Type", "application/json"); httpClient.addHeader("Accept", "application/json"); } - -TibberPowerMeterClass TibberPowerMeter; diff --git a/src/PowerMeterMqtt.cpp b/src/PowerMeterMqtt.cpp new file mode 100644 index 00000000..9a470ccf --- /dev/null +++ b/src/PowerMeterMqtt.cpp @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include "PowerMeterMqtt.h" +#include "Configuration.h" +#include "MqttSettings.h" +#include "MessageOutput.h" + +bool PowerMeterMqtt::init() +{ + auto subscribe = [this](char const* topic, float* target) { + if (strlen(topic) == 0) { return; } + MqttSettings.subscribe(topic, 0, + std::bind(&PowerMeterMqtt::onMqttMessage, + this, std::placeholders::_1, std::placeholders::_2, + std::placeholders::_3, std::placeholders::_4, + std::placeholders::_5, std::placeholders::_6) + ); + _mqttSubscriptions.try_emplace(topic, target); + }; + + auto const& config = Configuration.get(); + subscribe(config.PowerMeter.MqttTopicPowerMeter1, &_powerValueOne); + subscribe(config.PowerMeter.MqttTopicPowerMeter2, &_powerValueTwo); + subscribe(config.PowerMeter.MqttTopicPowerMeter3, &_powerValueThree); + + return _mqttSubscriptions.size() > 0; +} + +void PowerMeterMqtt::deinit() +{ + for (auto const& s: _mqttSubscriptions) { MqttSettings.unsubscribe(s.first); } + _mqttSubscriptions.clear(); +} + +void PowerMeterMqtt::onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) +{ + for (auto const& subscription: _mqttSubscriptions) { + if (subscription.first != topic) { continue; } + + std::string value(reinterpret_cast(payload), len); + try { + *subscription.second = std::stof(value); + } + catch(std::invalid_argument const& e) { + MessageOutput.printf("[PowerMeterMqtt] cannot parse payload of topic '%s' as float: %s\r\n", + topic, value.c_str()); + return; + } + + if (_verboseLogging) { + MessageOutput.printf("[PowerMeterMqtt] Updated from '%s', TotalPower: %5.2f\r\n", + topic, getPowerTotal()); + } + + gotUpdate(); + } +} + +float PowerMeterMqtt::getPowerTotal() const +{ + std::lock_guard l(_mutex); + return _powerValueOne + _powerValueTwo + _powerValueThree; +} + +void PowerMeterMqtt::doMqttPublish() const +{ + String topic = "powermeter"; + auto totalPower = getPowerTotal(); + + std::lock_guard l(_mutex); + MqttSettings.publish(topic + "/power1", String(_powerValueOne)); + MqttSettings.publish(topic + "/power2", String(_powerValueTwo)); + MqttSettings.publish(topic + "/power3", String(_powerValueThree)); + MqttSettings.publish(topic + "/powertotal", String(totalPower)); +} diff --git a/src/PowerMeterProvider.cpp b/src/PowerMeterProvider.cpp new file mode 100644 index 00000000..8becdebd --- /dev/null +++ b/src/PowerMeterProvider.cpp @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include "PowerMeterProvider.h" +#include "MqttSettings.h" + +bool PowerMeterProvider::isDataValid() const +{ + return _lastUpdate > 0 && ((millis() - _lastUpdate) < (30 * 1000)); +} + +void PowerMeterProvider::mqttLoop() const +{ + if (!MqttSettings.getConnected()) { return; } + + if (!isDataValid()) { return; } + + auto constexpr halfOfAllMillis = std::numeric_limits::max() / 2; + if ((_lastUpdate - _lastMqttPublish) > halfOfAllMillis) { return; } + + doMqttPublish(); + + _lastMqttPublish = millis(); +} diff --git a/src/PowerMeterSerialSdm.cpp b/src/PowerMeterSerialSdm.cpp new file mode 100644 index 00000000..d08cac19 --- /dev/null +++ b/src/PowerMeterSerialSdm.cpp @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include "PowerMeterSerialSdm.h" +#include "Configuration.h" +#include "PinMapping.h" +#include "MessageOutput.h" +#include "MqttSettings.h" +#include "SerialPortManager.h" + +void PowerMeterSerialSdm::deinit() +{ + if (_upSdmSerial) { + _upSdmSerial->end(); + _upSdmSerial = nullptr; + } +} + +bool PowerMeterSerialSdm::init() +{ + const PinMapping_t& pin = PinMapping.get(); + + MessageOutput.printf("[PowerMeterSerialSdm] rx = %d, tx = %d, dere = %d\r\n", + pin.powermeter_rx, pin.powermeter_tx, pin.powermeter_dere); + + if (pin.powermeter_rx < 0 || pin.powermeter_tx < 0) { + MessageOutput.println("[PowerMeterSerialSdm] invalid pin config for SDM " + "power meter (RX and TX pins must be defined)"); + return false; + } + + auto oHwSerialPort = SerialPortManager.allocatePort(_sdmSerialPortOwner); + if (!oHwSerialPort) { return false; } + + _upSdmSerial = std::make_unique(*oHwSerialPort); + _upSdmSerial->end(); // make sure the UART will be re-initialized + _upSdm = std::make_unique(*_upSdmSerial, 9600, pin.powermeter_dere, + SERIAL_8N1, pin.powermeter_rx, pin.powermeter_tx); + _upSdm->begin(); + + return true; +} + +float PowerMeterSerialSdm::getPowerTotal() const +{ + std::lock_guard l(_mutex); + return _phase1Power + _phase2Power + _phase3Power; +} + +void PowerMeterSerialSdm::doMqttPublish() const +{ + String topic = "powermeter"; + auto power = getPowerTotal(); + + std::lock_guard l(_mutex); + MqttSettings.publish(topic + "/power1", String(_phase1Power)); + MqttSettings.publish(topic + "/power2", String(_phase2Power)); + MqttSettings.publish(topic + "/power3", String(_phase3Power)); + MqttSettings.publish(topic + "/powertotal", String(power)); + MqttSettings.publish(topic + "/voltage1", String(_phase1Voltage)); + MqttSettings.publish(topic + "/voltage2", String(_phase2Voltage)); + MqttSettings.publish(topic + "/voltage3", String(_phase3Voltage)); + MqttSettings.publish(topic + "/import", String(_energyImport)); + MqttSettings.publish(topic + "/export", String(_energyExport)); +} + +void PowerMeterSerialSdm::loop() +{ + if (!_upSdm) { return; } + + auto const& config = Configuration.get(); + + if ((millis() - _lastPoll) < (config.PowerMeter.Interval * 1000)) { + return; + } + + uint8_t addr = config.PowerMeter.SdmAddress; + + // reading takes a "very long" time as each readVal() is a synchronous + // exchange of serial messages. cache the values and write later to + // enforce consistent values. + float phase1Power = _upSdm->readVal(SDM_PHASE_1_POWER, addr); + float phase2Power = 0.0; + float phase3Power = 0.0; + float phase1Voltage = _upSdm->readVal(SDM_PHASE_1_VOLTAGE, addr); + float phase2Voltage = 0.0; + float phase3Voltage = 0.0; + float energyImport = _upSdm->readVal(SDM_IMPORT_ACTIVE_ENERGY, addr); + float energyExport = _upSdm->readVal(SDM_EXPORT_ACTIVE_ENERGY, addr); + + if (static_cast(config.PowerMeter.Source) == PowerMeterProvider::Type::SDM3PH) { + phase2Power = _upSdm->readVal(SDM_PHASE_2_POWER, addr); + phase3Power = _upSdm->readVal(SDM_PHASE_3_POWER, addr); + phase2Voltage = _upSdm->readVal(SDM_PHASE_2_VOLTAGE, addr); + phase3Voltage = _upSdm->readVal(SDM_PHASE_3_VOLTAGE, addr); + } + + { + std::lock_guard l(_mutex); + _phase1Power = static_cast(phase1Power); + _phase2Power = static_cast(phase2Power); + _phase3Power = static_cast(phase3Power); + _phase1Voltage = static_cast(phase1Voltage); + _phase2Voltage = static_cast(phase2Voltage); + _phase3Voltage = static_cast(phase3Voltage); + _energyImport = static_cast(energyImport); + _energyExport = static_cast(energyExport); + } + + gotUpdate(); + + MessageOutput.printf("[PowerMeterSerialSdm] TotalPower: %5.2f\r\n", getPowerTotal()); + + _lastPoll = millis(); +} diff --git a/src/PowerMeterSerialSml.cpp b/src/PowerMeterSerialSml.cpp new file mode 100644 index 00000000..04792514 --- /dev/null +++ b/src/PowerMeterSerialSml.cpp @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include "PowerMeterSerialSml.h" +#include "Configuration.h" +#include "PinMapping.h" +#include "MessageOutput.h" +#include "MqttSettings.h" + +bool PowerMeterSerialSml::init() +{ + const PinMapping_t& pin = PinMapping.get(); + + MessageOutput.printf("[PowerMeterSerialSml] rx = %d\r\n", pin.powermeter_rx); + + if (pin.powermeter_rx < 0) { + MessageOutput.println("[PowerMeterSerialSml] invalid pin config " + "for serial SML power meter (RX pin must be defined)"); + return false; + } + + pinMode(pin.powermeter_rx, INPUT); + _upSmlSerial = std::make_unique(); + _upSmlSerial->begin(9600, SWSERIAL_8N1, pin.powermeter_rx, -1, false, 128, 95); + _upSmlSerial->enableRx(true); + _upSmlSerial->enableTx(false); + _upSmlSerial->flush(); + + return true; +} + +void PowerMeterSerialSml::deinit() +{ + if (!_upSmlSerial) { return; } + _upSmlSerial->end(); +} + +void PowerMeterSerialSml::doMqttPublish() const +{ + String topic = "powermeter"; + + std::lock_guard l(_mutex); + MqttSettings.publish(topic + "/powertotal", String(_activePower)); + MqttSettings.publish(topic + "/import", String(_energyImport)); + MqttSettings.publish(topic + "/export", String(_energyExport)); +} + +void PowerMeterSerialSml::loop() +{ + if (!_upSmlSerial) { return; } + + while (_upSmlSerial->available()) { + double readVal = 0; + unsigned char smlCurrentChar = _upSmlSerial->read(); + sml_states_t smlCurrentState = smlState(smlCurrentChar); + if (smlCurrentState == SML_LISTEND) { + for (auto& handler: smlHandlerList) { + if (smlOBISCheck(handler.OBIS)) { + handler.Fn(readVal); + std::lock_guard l(_mutex); + *handler.Arg = readVal; + } + } + } else if (smlCurrentState == SML_FINAL) { + gotUpdate(); + } + } + + MessageOutput.printf("[PowerMeterSerialSml]: TotalPower: %5.2f\r\n", getPowerTotal()); +} diff --git a/src/SMA_HM.cpp b/src/PowerMeterUdpSmaHomeManager.cpp similarity index 76% rename from src/SMA_HM.cpp rename to src/PowerMeterUdpSmaHomeManager.cpp index 7a3a9fe2..b79ae3d9 100644 --- a/src/SMA_HM.cpp +++ b/src/PowerMeterUdpSmaHomeManager.cpp @@ -2,51 +2,50 @@ /* * Copyright (C) 2024 Holger-Steffen Stapf */ -#include "SMA_HM.h" +#include "PowerMeterUdpSmaHomeManager.h" #include -#include "Configuration.h" -#include "NetworkSettings.h" +#include "MqttSettings.h" #include #include "MessageOutput.h" -unsigned int multicastPort = 9522; // local port to listen on -IPAddress multicastIP(239, 12, 255, 254); -WiFiUDP SMAUdp; +static constexpr unsigned int multicastPort = 9522; // local port to listen on +static const IPAddress multicastIP(239, 12, 255, 254); +static WiFiUDP SMAUdp; constexpr uint32_t interval = 1000; -SMA_HMClass SMA_HM; - -void SMA_HMClass::Soutput(int kanal, int index, int art, int tarif, +void PowerMeterUdpSmaHomeManager::Soutput(int kanal, int index, int art, int tarif, char const* name, float value, uint32_t timestamp) { if (!_verboseLogging) { return; } - MessageOutput.printf("SMA_HM: %s = %.1f (timestamp %d)\r\n", + MessageOutput.printf("[PowerMeterUdpSmaHomeManager] %s = %.1f (timestamp %d)\r\n", name, value, timestamp); } -void SMA_HMClass::init(Scheduler& scheduler, bool verboseLogging) +bool PowerMeterUdpSmaHomeManager::init() { - _verboseLogging = verboseLogging; - scheduler.addTask(_loopTask); - _loopTask.setCallback(std::bind(&SMA_HMClass::loop, this)); - _loopTask.setIterations(TASK_FOREVER); - _loopTask.enable(); SMAUdp.begin(multicastPort); SMAUdp.beginMulticast(multicastIP, multicastPort); + return true; } -void SMA_HMClass::loop() +void PowerMeterUdpSmaHomeManager::deinit() { - uint32_t currentMillis = millis(); - if (currentMillis - _previousMillis >= interval) { - _previousMillis = currentMillis; - event1(); - } + SMAUdp.stop(); } -uint8_t* SMA_HMClass::decodeGroup(uint8_t* offset, uint16_t grouplen) +void PowerMeterUdpSmaHomeManager::doMqttPublish() const +{ + String topic = "powermeter"; + + MqttSettings.publish(topic + "/powertotal", String(_powerMeterPower)); + MqttSettings.publish(topic + "/power1", String(_powerMeterL1)); + MqttSettings.publish(topic + "/power2", String(_powerMeterL2)); + MqttSettings.publish(topic + "/power3", String(_powerMeterL3)); +} + +uint8_t* PowerMeterUdpSmaHomeManager::decodeGroup(uint8_t* offset, uint16_t grouplen) { float Pbezug = 0; float BezugL1 = 0; @@ -149,7 +148,7 @@ uint8_t* SMA_HMClass::decodeGroup(uint8_t* offset, uint16_t grouplen) continue; } - MessageOutput.printf("SMA_HM: Skipped unknown measurement: %d %d %d %d\r\n", + MessageOutput.printf("[PowerMeterUdpSmaHomeManager] Skipped unknown measurement: %d %d %d %d\r\n", kanal, index, art, tarif); offset += art; } @@ -157,15 +156,20 @@ uint8_t* SMA_HMClass::decodeGroup(uint8_t* offset, uint16_t grouplen) return offset; } -void SMA_HMClass::event1() +void PowerMeterUdpSmaHomeManager::loop() { + uint32_t currentMillis = millis(); + if (currentMillis - _previousMillis < interval) { return; } + + _previousMillis = currentMillis; + int packetSize = SMAUdp.parsePacket(); if (!packetSize) { return; } uint8_t buffer[1024]; int rSize = SMAUdp.read(buffer, 1024); if (buffer[0] != 'S' || buffer[1] != 'M' || buffer[2] != 'A') { - MessageOutput.println("SMA_HM: Not an SMA packet?"); + MessageOutput.println("[PowerMeterUdpSmaHomeManager] Not an SMA packet?"); return; } @@ -196,7 +200,7 @@ void SMA_HMClass::event1() continue; } - MessageOutput.printf("SMA_HM: Unhandled group 0x%04x with length %d\r\n", + MessageOutput.printf("[PowerMeterUdpSmaHomeManager] Unhandled group 0x%04x with length %d\r\n", grouptag, grouplen); offset += grouplen; } while (grouplen > 0 && offset + 4 < buffer + rSize); diff --git a/src/WebApi_powermeter.cpp b/src/WebApi_powermeter.cpp index 74472bc0..c6cf373f 100644 --- a/src/WebApi_powermeter.cpp +++ b/src/WebApi_powermeter.cpp @@ -12,8 +12,8 @@ #include "MqttSettings.h" #include "PowerLimiter.h" #include "PowerMeter.h" -#include "HttpPowerMeter.h" -#include "TibberPowerMeter.h" +#include "PowerMeterHttpJson.h" +#include "PowerMeterHttpSml.h" #include "WebApi.h" #include "helper.h" @@ -128,7 +128,7 @@ void WebApiPowerMeterClass::onAdminPost(AsyncWebServerRequest* request) return; } - if (static_cast(root["source"].as()) == PowerMeterClass::Source::HTTP) { + if (static_cast(root["source"].as()) == PowerMeterProvider::Type::HTTP) { JsonArray http_phases = root["http_phases"]; for (uint8_t i = 0; i < http_phases.size(); i++) { JsonObject phase = http_phases[i].as(); @@ -174,7 +174,7 @@ void WebApiPowerMeterClass::onAdminPost(AsyncWebServerRequest* request) } } - if (static_cast(root["source"].as()) == PowerMeterClass::Source::TIBBER) { + if (static_cast(root["source"].as()) == PowerMeterProvider::Type::TIBBER) { JsonObject tibber = root["tibber"]; if (!tibber.containsKey("url") @@ -260,14 +260,14 @@ void WebApiPowerMeterClass::onTestHttpRequest(AsyncWebServerRequest* request) char response[256]; - int phase = 0;//"absuing" index 0 of the float power[3] in HttpPowerMeter to store the result PowerMeterHttpConfig phaseConfig; decodeJsonPhaseConfig(root.as(), phaseConfig); - if (HttpPowerMeter.queryPhase(phase, phaseConfig)) { + auto upMeter = std::make_unique(); + if (upMeter->queryPhase(0/*phase*/, phaseConfig)) { retMsg["type"] = "success"; - snprintf_P(response, sizeof(response), "Success! Power: %5.2fW", HttpPowerMeter.getPower(phase + 1)); + snprintf_P(response, sizeof(response), "Success! Power: %5.2fW", upMeter->getPowerTotal()); } else { - snprintf_P(response, sizeof(response), "%s", HttpPowerMeter.httpPowerMeterError); + snprintf_P(response, sizeof(response), "%s", upMeter->httpPowerMeterError); } retMsg["message"] = response; @@ -302,11 +302,12 @@ void WebApiPowerMeterClass::onTestTibberRequest(AsyncWebServerRequest* request) PowerMeterTibberConfig tibberConfig; decodeJsonTibberConfig(root.as(), tibberConfig); - if (TibberPowerMeter.query(tibberConfig)) { + auto upMeter = std::make_unique(); + if (upMeter->query(tibberConfig)) { retMsg["type"] = "success"; - snprintf_P(response, sizeof(response), "Success! Power: %5.2fW", PowerMeter.getPowerTotal()); + snprintf_P(response, sizeof(response), "Success! Power: %5.2fW", upMeter->getPowerTotal()); } else { - snprintf_P(response, sizeof(response), "%s", TibberPowerMeter.tibberPowerMeterError); + snprintf_P(response, sizeof(response), "%s", upMeter->tibberPowerMeterError); } retMsg["message"] = response; diff --git a/src/WebApi_ws_live.cpp b/src/WebApi_ws_live.cpp index ab54d479..85781bce 100644 --- a/src/WebApi_ws_live.cpp +++ b/src/WebApi_ws_live.cpp @@ -98,12 +98,12 @@ void WebApiWsLiveClass::generateOnBatteryJsonResponse(JsonVariant& root, bool al if (!all) { _lastPublishBattery = millis(); } } - if (all || (PowerMeter.getLastPowerMeterUpdate() - _lastPublishPowerMeter) < halfOfAllMillis) { + if (all || (PowerMeter.getLastUpdate() - _lastPublishPowerMeter) < halfOfAllMillis) { auto powerMeterObj = root["power_meter"].to(); powerMeterObj["enabled"] = config.PowerMeter.Enabled; if (config.PowerMeter.Enabled) { - addTotalField(powerMeterObj, "Power", PowerMeter.getPowerTotal(false), "W", 1); + addTotalField(powerMeterObj, "Power", PowerMeter.getPowerTotal(), "W", 1); } if (!all) { _lastPublishPowerMeter = millis(); }