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.
This commit is contained in:
Bernhard Kirchen 2024-05-08 11:08:04 +02:00
parent 8ec1695d1b
commit 2397e5cdf5
21 changed files with 667 additions and 456 deletions

View File

@ -1,79 +1,27 @@
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#pragma once #pragma once
#include "Configuration.h" #include "PowerMeterProvider.h"
#include <espMqttClient.h>
#include <Arduino.h>
#include <map>
#include <list>
#include <mutex>
#include "SDM.h"
#include "sml.h"
#include <TaskSchedulerDeclarations.h> #include <TaskSchedulerDeclarations.h>
#include <SoftwareSerial.h> #include <memory>
#include <mutex>
typedef struct {
const unsigned char OBIS[6];
void (*Fn)(double&);
float* Arg;
} OBISHandler;
class PowerMeterClass { class PowerMeterClass {
public: public:
enum class Source : unsigned {
MQTT = 0,
SDM1PH = 1,
SDM3PH = 2,
HTTP = 3,
SML = 4,
SMAHM2 = 5,
TIBBER = 6
};
void init(Scheduler& scheduler); void init(Scheduler& scheduler);
float getPowerTotal(bool forceUpdate = true);
uint32_t getLastPowerMeterUpdate();
bool isDataValid();
const std::list<OBISHandler> smlHandlerList{ void updateSettings();
{{0x01, 0x00, 0x10, 0x07, 0x00, 0xff}, &smlOBISW, &_powerMeter1Power},
{{0x01, 0x00, 0x01, 0x08, 0x00, 0xff}, &smlOBISWh, &_powerMeterImport}, float getPowerTotal() const;
{{0x01, 0x00, 0x02, 0x08, 0x00, 0xff}, &smlOBISWh, &_powerMeterExport} uint32_t getLastUpdate() const;
}; bool isDataValid() const;
private: private:
void loop(); 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; 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<String, float*> _mqttSubscriptions;
mutable std::mutex _mutex; mutable std::mutex _mutex;
std::unique_ptr<PowerMeterProvider> _upProvider = nullptr;
static char constexpr _sdmSerialPortOwner[] = "SDM power meter";
std::unique_ptr<HardwareSerial> _upSdmSerial = nullptr;
std::unique_ptr<SDM> _upSdm = nullptr;
std::unique_ptr<SoftwareSerial> _upSmlSerial = nullptr;
void readPowerMeter();
bool smlReadLoop();
}; };
extern PowerMeterClass PowerMeter; extern PowerMeterClass PowerMeter;

View File

@ -5,22 +5,29 @@
#include <Arduino.h> #include <Arduino.h>
#include <HTTPClient.h> #include <HTTPClient.h>
#include "Configuration.h" #include "Configuration.h"
#include "PowerMeterProvider.h"
using Auth_t = PowerMeterHttpConfig::Auth; using Auth_t = PowerMeterHttpConfig::Auth;
using Unit_t = PowerMeterHttpConfig::Unit; using Unit_t = PowerMeterHttpConfig::Unit;
class HttpPowerMeterClass { class PowerMeterHttpJson : public PowerMeterProvider {
public: public:
void init(); bool init() final { return true; }
bool updateValues(); void deinit() final { }
float getPower(int8_t phase); void loop() final;
char httpPowerMeterError[256]; float getPowerTotal() const final;
void doMqttPublish() const final;
bool queryPhase(int phase, PowerMeterHttpConfig const& config); bool queryPhase(int phase, PowerMeterHttpConfig const& config);
char httpPowerMeterError[256];
private: private:
float power[POWERMETER_MAX_PHASES]; uint32_t _lastPoll;
std::array<float,POWERMETER_MAX_PHASES> _cache;
std::array<float,POWERMETER_MAX_PHASES> _powerValues;
HTTPClient httpClient; HTTPClient httpClient;
String httpResponse; String httpResponse;
bool httpRequest(int phase, WiFiClient &wifiClient, const String& host, uint16_t port, const String& uri, bool https, PowerMeterHttpConfig const& config); 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); 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); 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); void prepareRequest(uint32_t timeout, const char* httpHeader, const char* httpValue);
String sha256(const String& data); String sha256(const String& data);
}; };
extern HttpPowerMeterClass HttpPowerMeter;

View File

@ -1,23 +1,46 @@
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#pragma once #pragma once
#include <list>
#include <mutex>
#include <stdint.h> #include <stdint.h>
#include <Arduino.h> #include <Arduino.h>
#include <HTTPClient.h> #include <HTTPClient.h>
#include "Configuration.h" #include "Configuration.h"
#include "PowerMeterProvider.h"
#include "sml.h"
class TibberPowerMeterClass { class PowerMeterHttpSml : public PowerMeterProvider {
public: public:
bool init() final { return true; }
void deinit() final { }
void loop() final;
float getPowerTotal() const final;
void doMqttPublish() const final;
bool updateValues(); bool updateValues();
char tibberPowerMeterError[256]; char tibberPowerMeterError[256];
bool query(PowerMeterTibberConfig const& config); bool query(PowerMeterTibberConfig const& config);
private: 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<OBISHandler> smlHandlerList{
{{0x01, 0x00, 0x10, 0x07, 0x00, 0xff}, &smlOBISW, &_activePower}
};
HTTPClient httpClient; HTTPClient httpClient;
String httpResponse; String httpResponse;
bool httpRequest(WiFiClient &wifiClient, const String& host, uint16_t port, const String& uri, bool https, PowerMeterTibberConfig const& config); 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); bool extractUrlComponents(String url, String& _protocol, String& _hostname, String& _uri, uint16_t& uint16_t, String& _base64Authorization);
void prepareRequest(uint32_t timeout); void prepareRequest(uint32_t timeout);
}; };
extern TibberPowerMeterClass TibberPowerMeter;

28
include/PowerMeterMqtt.h Normal file
View File

@ -0,0 +1,28 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "PowerMeterProvider.h"
#include <espMqttClient.h>
#include <map>
#include <mutex>
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<String, float*> _mqttSubscriptions;
mutable std::mutex _mutex;
};

View File

@ -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;
};

View File

@ -0,0 +1,33 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <mutex>
#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<HardwareSerial> _upSdmSerial = nullptr;
std::unique_ptr<SDM> _upSdm = nullptr;
};

View File

@ -0,0 +1,39 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "PowerMeterProvider.h"
#include "Configuration.h"
#include "sml.h"
#include <SoftwareSerial.h>
#include <list>
#include <mutex>
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<SoftwareSerial> _upSmlSerial = nullptr;
typedef struct {
const unsigned char OBIS[6];
void (*Fn)(double&);
float* Arg;
} OBISHandler;
const std::list<OBISHandler> 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}
};
};

View File

@ -5,17 +5,15 @@
#pragma once #pragma once
#include <cstdint> #include <cstdint>
#include <TaskSchedulerDeclarations.h> #include "PowerMeterProvider.h"
class SMA_HMClass { class PowerMeterUdpSmaHomeManager : public PowerMeterProvider {
public: public:
void init(Scheduler& scheduler, bool verboseLogging); bool init() final;
void loop(); void deinit() final;
void event1(); void loop() final;
float getPowerTotal() const { return _powerMeterPower; } float getPowerTotal() const final { return _powerMeterPower; }
float getPowerL1() const { return _powerMeterL1; } void doMqttPublish() const final;
float getPowerL2() const { return _powerMeterL2; }
float getPowerL3() const { return _powerMeterL3; }
private: private:
void Soutput(int kanal, int index, int art, int tarif, void Soutput(int kanal, int index, int art, int tarif,
@ -23,14 +21,10 @@ private:
uint8_t* decodeGroup(uint8_t* offset, uint16_t grouplen); uint8_t* decodeGroup(uint8_t* offset, uint16_t grouplen);
bool _verboseLogging = false;
float _powerMeterPower = 0.0; float _powerMeterPower = 0.0;
float _powerMeterL1 = 0.0; float _powerMeterL1 = 0.0;
float _powerMeterL2 = 0.0; float _powerMeterL2 = 0.0;
float _powerMeterL3 = 0.0; float _powerMeterL3 = 0.0;
uint32_t _previousMillis = 0; uint32_t _previousMillis = 0;
uint32_t _serial = 0; uint32_t _serial = 0;
Task _loopTask;
}; };
extern SMA_HMClass SMA_HM;

View File

@ -294,7 +294,7 @@ void DisplayGraphicClass::loop()
_display->drawBox(0, y, _display->getDisplayWidth(), lineHeight); _display->drawBox(0, y, _display->getDisplayWidth(), lineHeight);
_display->setDrawColor(1); _display->setDrawColor(1);
auto acPower = PowerMeter.getPowerTotal(false); auto acPower = PowerMeter.getPowerTotal();
if (acPower > 999) { if (acPower > 999) {
snprintf(_fmtText, sizeof(_fmtText), i18n_meter_power_kw[_display_language], (acPower / 1000)); snprintf(_fmtText, sizeof(_fmtText), i18n_meter_power_kw[_display_language], (acPower / 1000));
} else { } else {

View File

@ -371,12 +371,12 @@ void HuaweiCanClass::loop()
} }
} }
if (PowerMeter.getLastPowerMeterUpdate() > _lastPowerMeterUpdateReceivedMillis && if (PowerMeter.getLastUpdate() > _lastPowerMeterUpdateReceivedMillis &&
_autoPowerEnabledCounter > 0) { _autoPowerEnabledCounter > 0) {
// We have received a new PowerMeter value. Also we're _autoPowerEnabled // We have received a new PowerMeter value. Also we're _autoPowerEnabled
// So we're good to calculate a new limit // So we're good to calculate a new limit
_lastPowerMeterUpdateReceivedMillis = PowerMeter.getLastPowerMeterUpdate(); _lastPowerMeterUpdateReceivedMillis = PowerMeter.getLastUpdate();
// Calculate new power limit // Calculate new power limit
float newPowerLimit = -1 * round(PowerMeter.getPowerTotal()); float newPowerLimit = -1 * round(PowerMeter.getPowerTotal());

View File

@ -201,7 +201,7 @@ void PowerLimiterClass::loop()
// arrives. this can be the case for readings provided by networked meter // arrives. this can be the case for readings provided by networked meter
// readers, where a packet needs to travel through the network for some // readers, where a packet needs to travel through the network for some
// time after the actual measurement was done by the reader. // 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); return announceStatus(Status::PowerMeterPending);
} }

View File

@ -1,18 +1,12 @@
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022 Thomas Basler and others
*/
#include "PowerMeter.h" #include "PowerMeter.h"
#include "Configuration.h" #include "Configuration.h"
#include "PinMapping.h" #include "PowerMeterHttpJson.h"
#include "HttpPowerMeter.h" #include "PowerMeterHttpSml.h"
#include "TibberPowerMeter.h" #include "PowerMeterMqtt.h"
#include "MqttSettings.h" #include "PowerMeterSerialSdm.h"
#include "NetworkSettings.h" #include "PowerMeterSerialSml.h"
#include "MessageOutput.h" #include "PowerMeterUdpSmaHomeManager.h"
#include "SerialPortManager.h"
#include <ctime>
#include <SMA_HM.h>
PowerMeterClass PowerMeter; PowerMeterClass PowerMeter;
@ -23,285 +17,74 @@ void PowerMeterClass::init(Scheduler& scheduler)
_loopTask.setIterations(TASK_FOREVER); _loopTask.setIterations(TASK_FOREVER);
_loopTask.enable(); _loopTask.enable();
_lastPowerMeterCheck = 0; updateSettings();
_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<Source>(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<HardwareSerial>(*oHwSerialPort);
_upSdmSerial->end(); // make sure the UART will be re-initialized
_upSdm = std::make_unique<SDM>(*_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<SoftwareSerial>();
_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;
}
} }
void PowerMeterClass::onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) void PowerMeterClass::updateSettings()
{
for (auto const& subscription: _mqttSubscriptions) {
if (subscription.first != topic) { continue; }
std::string value(reinterpret_cast<const char*>(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<std::mutex> l(_mutex);
return _powerMeter1Power + _powerMeter2Power + _powerMeter3Power;
}
uint32_t PowerMeterClass::getLastPowerMeterUpdate()
{ {
std::lock_guard<std::mutex> l(_mutex); std::lock_guard<std::mutex> l(_mutex);
return _lastPowerMeterUpdate;
}
bool PowerMeterClass::isDataValid() if (_upProvider) {
{ _upProvider->deinit();
_upProvider = nullptr;
}
auto const& config = Configuration.get(); auto const& config = Configuration.get();
std::lock_guard<std::mutex> l(_mutex); if (!config.PowerMeter.Enabled) { return; }
bool valid = config.PowerMeter.Enabled && switch(static_cast<PowerMeterProvider::Type>(config.PowerMeter.Source)) {
_lastPowerMeterUpdate > 0 && case PowerMeterProvider::Type::MQTT:
((millis() - _lastPowerMeterUpdate) < (30 * 1000)); _upProvider = std::make_unique<PowerMeterMqtt>();
break;
case PowerMeterProvider::Type::SDM1PH:
case PowerMeterProvider::Type::SDM3PH:
_upProvider = std::make_unique<PowerMeterSerialSdm>();
break;
case PowerMeterProvider::Type::HTTP:
_upProvider = std::make_unique<PowerMeterHttpJson>();
break;
case PowerMeterProvider::Type::SML:
_upProvider = std::make_unique<PowerMeterSerialSml>();
break;
case PowerMeterProvider::Type::SMAHM2:
_upProvider = std::make_unique<PowerMeterUdpSmaHomeManager>();
break;
case PowerMeterProvider::Type::TIBBER:
_upProvider = std::make_unique<PowerMeterHttpSml>();
break;
}
// reset if timed out to avoid glitch once if (!_upProvider->init()) {
// (millis() - _lastPowerMeterUpdate) overflows _upProvider = nullptr;
if (!valid) { _lastPowerMeterUpdate = 0; } }
return valid;
} }
void PowerMeterClass::mqtt() float PowerMeterClass::getPowerTotal() const
{ {
if (!MqttSettings.getConnected()) { return; }
String topic = "powermeter";
auto totalPower = getPowerTotal();
std::lock_guard<std::mutex> l(_mutex); std::lock_guard<std::mutex> l(_mutex);
MqttSettings.publish(topic + "/power1", String(_powerMeter1Power)); if (!_upProvider) { return 0.0; }
MqttSettings.publish(topic + "/power2", String(_powerMeter2Power)); return _upProvider->getPowerTotal();
MqttSettings.publish(topic + "/power3", String(_powerMeter3Power)); }
MqttSettings.publish(topic + "/powertotal", String(totalPower));
MqttSettings.publish(topic + "/voltage1", String(_powerMeter1Voltage)); uint32_t PowerMeterClass::getLastUpdate() const
MqttSettings.publish(topic + "/voltage2", String(_powerMeter2Voltage)); {
MqttSettings.publish(topic + "/voltage3", String(_powerMeter3Voltage)); std::lock_guard<std::mutex> l(_mutex);
MqttSettings.publish(topic + "/import", String(_powerMeterImport)); if (!_upProvider) { return 0; }
MqttSettings.publish(topic + "/export", String(_powerMeterExport)); return _upProvider->getLastUpdate();
}
bool PowerMeterClass::isDataValid() const
{
std::lock_guard<std::mutex> l(_mutex);
if (!_upProvider) { return false; }
return _upProvider->isDataValid();
} }
void PowerMeterClass::loop() void PowerMeterClass::loop()
{ {
CONFIG_T const& config = Configuration.get(); std::lock_guard<std::mutex> lock(_mutex);
_verboseLogging = config.PowerMeter.VerboseLogging; if (!_upProvider) { return; }
_upProvider->loop();
if (!config.PowerMeter.Enabled) { return; } _upProvider->mqttLoop();
if (static_cast<Source>(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<Source>(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<std::mutex> l(_mutex);
_powerMeter1Power = static_cast<float>(phase1Power);
_powerMeter2Power = 0;
_powerMeter3Power = 0;
_powerMeter1Voltage = static_cast<float>(phase1Voltage);
_powerMeter2Voltage = 0;
_powerMeter3Voltage = 0;
_powerMeterImport = static_cast<float>(energyImport);
_powerMeterExport = static_cast<float>(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<std::mutex> l(_mutex);
_powerMeter1Power = static_cast<float>(phase1Power);
_powerMeter2Power = static_cast<float>(phase2Power);
_powerMeter3Power = static_cast<float>(phase3Power);
_powerMeter1Voltage = static_cast<float>(phase1Voltage);
_powerMeter2Voltage = static_cast<float>(phase2Voltage);
_powerMeter3Voltage = static_cast<float>(phase3Voltage);
_powerMeterImport = static_cast<float>(energyImport);
_powerMeterExport = static_cast<float>(energyExport);
_lastPowerMeterUpdate = millis();
}
else if (configuredSource == Source::HTTP) {
if (HttpPowerMeter.updateValues()) {
std::lock_guard<std::mutex> l(_mutex);
_powerMeter1Power = HttpPowerMeter.getPower(1);
_powerMeter2Power = HttpPowerMeter.getPower(2);
_powerMeter3Power = HttpPowerMeter.getPower(3);
_lastPowerMeterUpdate = millis();
}
}
else if (configuredSource == Source::SMAHM2) {
std::lock_guard<std::mutex> 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;
} }

View File

@ -1,7 +1,8 @@
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#include "Configuration.h" #include "Configuration.h"
#include "HttpPowerMeter.h" #include "PowerMeterHttpJson.h"
#include "MessageOutput.h" #include "MessageOutput.h"
#include "MqttSettings.h"
#include <WiFiClientSecure.h> #include <WiFiClientSecure.h>
#include <ArduinoJson.h> #include <ArduinoJson.h>
#include "mbedtls/sha256.h" #include "mbedtls/sha256.h"
@ -9,48 +10,63 @@
#include <memory> #include <memory>
#include <ESPmDNS.h> #include <ESPmDNS.h>
void HttpPowerMeterClass::init() void PowerMeterHttpJson::loop()
{
}
float HttpPowerMeterClass::getPower(int8_t phase)
{
if (phase < 1 || phase > POWERMETER_MAX_PHASES) { return 0.0; }
return power[phase - 1];
}
bool HttpPowerMeterClass::updateValues()
{ {
auto const& config = Configuration.get(); 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++) { for (uint8_t i = 0; i < POWERMETER_MAX_PHASES; i++) {
auto const& phaseConfig = config.PowerMeter.Http_Phase[i]; auto const& phaseConfig = config.PowerMeter.Http_Phase[i];
if (!phaseConfig.Enabled) { if (!phaseConfig.Enabled) {
power[i] = 0.0; _cache[i] = 0.0;
continue; continue;
} }
if (i == 0 || config.PowerMeter.HttpIndividualRequests) { if (i == 0 || config.PowerMeter.HttpIndividualRequests) {
if (!queryPhase(i, phaseConfig)) { 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); MessageOutput.printf("%s\r\n", httpPowerMeterError);
return false; return;
} }
continue; continue;
} }
if(!tryGetFloatValueForPhase(i, phaseConfig.JsonPath, phaseConfig.PowerUnit, phaseConfig.SignInverted)) { 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); 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 //hostByName in WiFiGeneric fails to resolve local names. issue described in
//https://github.com/espressif/arduino-esp32/issues/3822 //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); 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)){ 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()); 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); 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); int _begin = authReq.indexOf(param);
if (_begin == -1) { return ""; } if (_begin == -1) { return ""; }
return authReq.substring(_begin + param.length(), authReq.indexOf(delimit, _begin + param.length())); 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" static const char alphanum[] = "0123456789"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ" "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"; "abcdefghijklmnopqrstuvwxyz";
@ -180,7 +196,7 @@ String HttpPowerMeterClass::getcNonce(const int len) {
return s; 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 // extracting required parameters for RFC 2617 Digest
String realm = extractParam(authReq, "realm=\"", '"'); String realm = extractParam(authReq, "realm=\"", '"');
String nonce = extractParam(authReq, "nonce=\"", '"'); String nonce = extractParam(authReq, "nonce=\"", '"');
@ -218,13 +234,13 @@ String HttpPowerMeterClass::getDigestAuth(String& authReq, const String& usernam
return authorization; 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; JsonDocument root;
const DeserializationError error = deserializeJson(root, httpResponse); const DeserializationError error = deserializeJson(root, httpResponse);
if (error) { if (error) {
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), 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; return false;
} }
@ -286,32 +302,32 @@ bool HttpPowerMeterClass::tryGetFloatValueForPhase(int phase, String jsonPath, U
if (!value.is<float>()) { if (!value.is<float>()) {
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError),
PSTR("[HttpPowerMeter] not a float: '%s'"), PSTR("[PowerMeterHttpJson] not a float: '%s'"),
value.as<String>().c_str()); value.as<String>().c_str());
return false; return false;
} }
// this value is supposed to be in Watts and positive if energy is consumed. // this value is supposed to be in Watts and positive if energy is consumed.
power[phase] = value.as<float>(); _cache[phase] = value.as<float>();
switch (unit) { switch (unit) {
case Unit_t::MilliWatts: case Unit_t::MilliWatts:
power[phase] /= 1000; _cache[phase] /= 1000;
break; break;
case Unit_t::KiloWatts: case Unit_t::KiloWatts:
power[phase] *= 1000; _cache[phase] *= 1000;
break; break;
default: default:
break; break;
} }
if (signInverted) { power[phase] *= -1; } if (signInverted) { _cache[phase] *= -1; }
return true; 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 //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: // check for : (http: or https:
int index = url.indexOf(':'); int index = url.indexOf(':');
@ -361,7 +377,7 @@ bool HttpPowerMeterClass::extractUrlComponents(String url, String& _protocol, St
return true; return true;
} }
String HttpPowerMeterClass::sha256(const String& data) { String PowerMeterHttpJson::sha256(const String& data) {
uint8_t hash[32]; uint8_t hash[32];
mbedtls_sha256_context ctx; mbedtls_sha256_context ctx;
@ -379,7 +395,7 @@ String HttpPowerMeterClass::sha256(const String& data) {
return res; 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.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
httpClient.setUserAgent("OpenDTU-OnBattery"); httpClient.setUserAgent("OpenDTU-OnBattery");
httpClient.setConnectTimeout(timeout); httpClient.setConnectTimeout(timeout);
@ -391,5 +407,3 @@ void HttpPowerMeterClass::prepareRequest(uint32_t timeout, const char* httpHeade
httpClient.addHeader(httpHeader, httpValue); httpClient.addHeader(httpHeader, httpValue);
} }
} }
HttpPowerMeterClass HttpPowerMeter;

View File

@ -1,28 +1,44 @@
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#include "Configuration.h" #include "Configuration.h"
#include "TibberPowerMeter.h" #include "PowerMeterHttpSml.h"
#include "MessageOutput.h" #include "MessageOutput.h"
#include "MqttSettings.h"
#include <WiFiClientSecure.h> #include <WiFiClientSecure.h>
#include <base64.h> #include <base64.h>
#include <ESPmDNS.h> #include <ESPmDNS.h>
#include <PowerMeter.h>
bool TibberPowerMeterClass::updateValues() float PowerMeterHttpSml::getPowerTotal() const
{
std::lock_guard<std::mutex> l(_mutex);
return _activePower;
}
void PowerMeterHttpSml::doMqttPublish() const
{
String topic = "powermeter";
std::lock_guard<std::mutex> l(_mutex);
MqttSettings.publish(topic + "/powertotal", String(_activePower));
}
void PowerMeterHttpSml::loop()
{ {
auto const& config = Configuration.get(); auto const& config = Configuration.get();
if ((millis() - _lastPoll) < (config.PowerMeter.Interval * 1000)) {
return;
}
_lastPoll = millis();
auto const& tibberConfig = config.PowerMeter.Tibber; auto const& tibberConfig = config.PowerMeter.Tibber;
if (!query(tibberConfig)) { 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); 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 //hostByName in WiFiGeneric fails to resolve local names. issue described in
//https://github.com/espressif/arduino-esp32/issues/3822 //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); 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)){ 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()); 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(); unsigned char smlCurrentChar = httpClient.getStream().read();
sml_states_t smlCurrentState = smlState(smlCurrentChar); sml_states_t smlCurrentState = smlState(smlCurrentChar);
if (smlCurrentState == SML_LISTEND) { if (smlCurrentState == SML_LISTEND) {
for (auto& handler: PowerMeter.smlHandlerList) { for (auto& handler: smlHandlerList) {
if (smlOBISCheck(handler.OBIS)) { if (smlOBISCheck(handler.OBIS)) {
std::lock_guard<std::mutex> l(_mutex);
handler.Fn(readVal); handler.Fn(readVal);
*handler.Arg = 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 //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: // check for : (http: or https:
int index = url.indexOf(':'); int index = url.indexOf(':');
@ -176,7 +194,7 @@ bool TibberPowerMeterClass::extractUrlComponents(String url, String& _protocol,
return true; return true;
} }
void TibberPowerMeterClass::prepareRequest(uint32_t timeout) { void PowerMeterHttpSml::prepareRequest(uint32_t timeout) {
httpClient.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); httpClient.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
httpClient.setUserAgent("OpenDTU-OnBattery"); httpClient.setUserAgent("OpenDTU-OnBattery");
httpClient.setConnectTimeout(timeout); httpClient.setConnectTimeout(timeout);
@ -184,5 +202,3 @@ void TibberPowerMeterClass::prepareRequest(uint32_t timeout) {
httpClient.addHeader("Content-Type", "application/json"); httpClient.addHeader("Content-Type", "application/json");
httpClient.addHeader("Accept", "application/json"); httpClient.addHeader("Accept", "application/json");
} }
TibberPowerMeterClass TibberPowerMeter;

74
src/PowerMeterMqtt.cpp Normal file
View File

@ -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<const char*>(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<std::mutex> l(_mutex);
return _powerValueOne + _powerValueTwo + _powerValueThree;
}
void PowerMeterMqtt::doMqttPublish() const
{
String topic = "powermeter";
auto totalPower = getPowerTotal();
std::lock_guard<std::mutex> 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));
}

View File

@ -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<uint32_t>::max() / 2;
if ((_lastUpdate - _lastMqttPublish) > halfOfAllMillis) { return; }
doMqttPublish();
_lastMqttPublish = millis();
}

113
src/PowerMeterSerialSdm.cpp Normal file
View File

@ -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<HardwareSerial>(*oHwSerialPort);
_upSdmSerial->end(); // make sure the UART will be re-initialized
_upSdm = std::make_unique<SDM>(*_upSdmSerial, 9600, pin.powermeter_dere,
SERIAL_8N1, pin.powermeter_rx, pin.powermeter_tx);
_upSdm->begin();
return true;
}
float PowerMeterSerialSdm::getPowerTotal() const
{
std::lock_guard<std::mutex> l(_mutex);
return _phase1Power + _phase2Power + _phase3Power;
}
void PowerMeterSerialSdm::doMqttPublish() const
{
String topic = "powermeter";
auto power = getPowerTotal();
std::lock_guard<std::mutex> 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<PowerMeterProvider::Type>(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<std::mutex> l(_mutex);
_phase1Power = static_cast<float>(phase1Power);
_phase2Power = static_cast<float>(phase2Power);
_phase3Power = static_cast<float>(phase3Power);
_phase1Voltage = static_cast<float>(phase1Voltage);
_phase2Voltage = static_cast<float>(phase2Voltage);
_phase3Voltage = static_cast<float>(phase3Voltage);
_energyImport = static_cast<float>(energyImport);
_energyExport = static_cast<float>(energyExport);
}
gotUpdate();
MessageOutput.printf("[PowerMeterSerialSdm] TotalPower: %5.2f\r\n", getPowerTotal());
_lastPoll = millis();
}

View File

@ -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<SoftwareSerial>();
_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<std::mutex> 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<std::mutex> l(_mutex);
*handler.Arg = readVal;
}
}
} else if (smlCurrentState == SML_FINAL) {
gotUpdate();
}
}
MessageOutput.printf("[PowerMeterSerialSml]: TotalPower: %5.2f\r\n", getPowerTotal());
}

View File

@ -2,51 +2,50 @@
/* /*
* Copyright (C) 2024 Holger-Steffen Stapf * Copyright (C) 2024 Holger-Steffen Stapf
*/ */
#include "SMA_HM.h" #include "PowerMeterUdpSmaHomeManager.h"
#include <Arduino.h> #include <Arduino.h>
#include "Configuration.h" #include "MqttSettings.h"
#include "NetworkSettings.h"
#include <WiFiUdp.h> #include <WiFiUdp.h>
#include "MessageOutput.h" #include "MessageOutput.h"
unsigned int multicastPort = 9522; // local port to listen on static constexpr unsigned int multicastPort = 9522; // local port to listen on
IPAddress multicastIP(239, 12, 255, 254); static const IPAddress multicastIP(239, 12, 255, 254);
WiFiUDP SMAUdp; static WiFiUDP SMAUdp;
constexpr uint32_t interval = 1000; constexpr uint32_t interval = 1000;
SMA_HMClass SMA_HM; void PowerMeterUdpSmaHomeManager::Soutput(int kanal, int index, int art, int tarif,
void SMA_HMClass::Soutput(int kanal, int index, int art, int tarif,
char const* name, float value, uint32_t timestamp) char const* name, float value, uint32_t timestamp)
{ {
if (!_verboseLogging) { return; } 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); 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.begin(multicastPort);
SMAUdp.beginMulticast(multicastIP, multicastPort); SMAUdp.beginMulticast(multicastIP, multicastPort);
return true;
} }
void SMA_HMClass::loop() void PowerMeterUdpSmaHomeManager::deinit()
{ {
uint32_t currentMillis = millis(); SMAUdp.stop();
if (currentMillis - _previousMillis >= interval) {
_previousMillis = currentMillis;
event1();
}
} }
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 Pbezug = 0;
float BezugL1 = 0; float BezugL1 = 0;
@ -149,7 +148,7 @@ uint8_t* SMA_HMClass::decodeGroup(uint8_t* offset, uint16_t grouplen)
continue; 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); kanal, index, art, tarif);
offset += art; offset += art;
} }
@ -157,15 +156,20 @@ uint8_t* SMA_HMClass::decodeGroup(uint8_t* offset, uint16_t grouplen)
return offset; return offset;
} }
void SMA_HMClass::event1() void PowerMeterUdpSmaHomeManager::loop()
{ {
uint32_t currentMillis = millis();
if (currentMillis - _previousMillis < interval) { return; }
_previousMillis = currentMillis;
int packetSize = SMAUdp.parsePacket(); int packetSize = SMAUdp.parsePacket();
if (!packetSize) { return; } if (!packetSize) { return; }
uint8_t buffer[1024]; uint8_t buffer[1024];
int rSize = SMAUdp.read(buffer, 1024); int rSize = SMAUdp.read(buffer, 1024);
if (buffer[0] != 'S' || buffer[1] != 'M' || buffer[2] != 'A') { 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; return;
} }
@ -196,7 +200,7 @@ void SMA_HMClass::event1()
continue; 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); grouptag, grouplen);
offset += grouplen; offset += grouplen;
} while (grouplen > 0 && offset + 4 < buffer + rSize); } while (grouplen > 0 && offset + 4 < buffer + rSize);

View File

@ -12,8 +12,8 @@
#include "MqttSettings.h" #include "MqttSettings.h"
#include "PowerLimiter.h" #include "PowerLimiter.h"
#include "PowerMeter.h" #include "PowerMeter.h"
#include "HttpPowerMeter.h" #include "PowerMeterHttpJson.h"
#include "TibberPowerMeter.h" #include "PowerMeterHttpSml.h"
#include "WebApi.h" #include "WebApi.h"
#include "helper.h" #include "helper.h"
@ -128,7 +128,7 @@ void WebApiPowerMeterClass::onAdminPost(AsyncWebServerRequest* request)
return; return;
} }
if (static_cast<PowerMeterClass::Source>(root["source"].as<uint8_t>()) == PowerMeterClass::Source::HTTP) { if (static_cast<PowerMeterProvider::Type>(root["source"].as<uint8_t>()) == PowerMeterProvider::Type::HTTP) {
JsonArray http_phases = root["http_phases"]; JsonArray http_phases = root["http_phases"];
for (uint8_t i = 0; i < http_phases.size(); i++) { for (uint8_t i = 0; i < http_phases.size(); i++) {
JsonObject phase = http_phases[i].as<JsonObject>(); JsonObject phase = http_phases[i].as<JsonObject>();
@ -174,7 +174,7 @@ void WebApiPowerMeterClass::onAdminPost(AsyncWebServerRequest* request)
} }
} }
if (static_cast<PowerMeterClass::Source>(root["source"].as<uint8_t>()) == PowerMeterClass::Source::TIBBER) { if (static_cast<PowerMeterProvider::Type>(root["source"].as<uint8_t>()) == PowerMeterProvider::Type::TIBBER) {
JsonObject tibber = root["tibber"]; JsonObject tibber = root["tibber"];
if (!tibber.containsKey("url") if (!tibber.containsKey("url")
@ -260,14 +260,14 @@ void WebApiPowerMeterClass::onTestHttpRequest(AsyncWebServerRequest* request)
char response[256]; char response[256];
int phase = 0;//"absuing" index 0 of the float power[3] in HttpPowerMeter to store the result
PowerMeterHttpConfig phaseConfig; PowerMeterHttpConfig phaseConfig;
decodeJsonPhaseConfig(root.as<JsonObject>(), phaseConfig); decodeJsonPhaseConfig(root.as<JsonObject>(), phaseConfig);
if (HttpPowerMeter.queryPhase(phase, phaseConfig)) { auto upMeter = std::make_unique<PowerMeterHttpJson>();
if (upMeter->queryPhase(0/*phase*/, phaseConfig)) {
retMsg["type"] = "success"; 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 { } else {
snprintf_P(response, sizeof(response), "%s", HttpPowerMeter.httpPowerMeterError); snprintf_P(response, sizeof(response), "%s", upMeter->httpPowerMeterError);
} }
retMsg["message"] = response; retMsg["message"] = response;
@ -302,11 +302,12 @@ void WebApiPowerMeterClass::onTestTibberRequest(AsyncWebServerRequest* request)
PowerMeterTibberConfig tibberConfig; PowerMeterTibberConfig tibberConfig;
decodeJsonTibberConfig(root.as<JsonObject>(), tibberConfig); decodeJsonTibberConfig(root.as<JsonObject>(), tibberConfig);
if (TibberPowerMeter.query(tibberConfig)) { auto upMeter = std::make_unique<PowerMeterHttpSml>();
if (upMeter->query(tibberConfig)) {
retMsg["type"] = "success"; 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 { } else {
snprintf_P(response, sizeof(response), "%s", TibberPowerMeter.tibberPowerMeterError); snprintf_P(response, sizeof(response), "%s", upMeter->tibberPowerMeterError);
} }
retMsg["message"] = response; retMsg["message"] = response;

View File

@ -98,12 +98,12 @@ void WebApiWsLiveClass::generateOnBatteryJsonResponse(JsonVariant& root, bool al
if (!all) { _lastPublishBattery = millis(); } if (!all) { _lastPublishBattery = millis(); }
} }
if (all || (PowerMeter.getLastPowerMeterUpdate() - _lastPublishPowerMeter) < halfOfAllMillis) { if (all || (PowerMeter.getLastUpdate() - _lastPublishPowerMeter) < halfOfAllMillis) {
auto powerMeterObj = root["power_meter"].to<JsonObject>(); auto powerMeterObj = root["power_meter"].to<JsonObject>();
powerMeterObj["enabled"] = config.PowerMeter.Enabled; powerMeterObj["enabled"] = config.PowerMeter.Enabled;
if (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(); } if (!all) { _lastPublishPowerMeter = millis(); }