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
#pragma once
#include "Configuration.h"
#include <espMqttClient.h>
#include <Arduino.h>
#include <map>
#include <list>
#include <mutex>
#include "SDM.h"
#include "sml.h"
#include "PowerMeterProvider.h"
#include <TaskSchedulerDeclarations.h>
#include <SoftwareSerial.h>
typedef struct {
const unsigned char OBIS[6];
void (*Fn)(double&);
float* Arg;
} OBISHandler;
#include <memory>
#include <mutex>
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<OBISHandler> 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<String, float*> _mqttSubscriptions;
mutable std::mutex _mutex;
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();
std::unique_ptr<PowerMeterProvider> _upProvider = nullptr;
};
extern PowerMeterClass PowerMeter;

View File

@ -5,22 +5,29 @@
#include <Arduino.h>
#include <HTTPClient.h>
#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<float,POWERMETER_MAX_PHASES> _cache;
std::array<float,POWERMETER_MAX_PHASES> _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;

View File

@ -1,23 +1,46 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <list>
#include <mutex>
#include <stdint.h>
#include <Arduino.h>
#include <HTTPClient.h>
#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<OBISHandler> 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;

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
#include <cstdint>
#include <TaskSchedulerDeclarations.h>
#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;

View File

@ -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 {

View File

@ -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());

View File

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

View File

@ -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 <ctime>
#include <SMA_HM.h>
#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;
updateSettings();
}
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)
{
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()
void PowerMeterClass::updateSettings()
{
std::lock_guard<std::mutex> l(_mutex);
return _lastPowerMeterUpdate;
if (_upProvider) {
_upProvider->deinit();
_upProvider = nullptr;
}
bool PowerMeterClass::isDataValid()
{
auto const& config = Configuration.get();
std::lock_guard<std::mutex> l(_mutex);
if (!config.PowerMeter.Enabled) { return; }
bool valid = config.PowerMeter.Enabled &&
_lastPowerMeterUpdate > 0 &&
((millis() - _lastPowerMeterUpdate) < (30 * 1000));
// reset if timed out to avoid glitch once
// (millis() - _lastPowerMeterUpdate) overflows
if (!valid) { _lastPowerMeterUpdate = 0; }
return valid;
switch(static_cast<PowerMeterProvider::Type>(config.PowerMeter.Source)) {
case PowerMeterProvider::Type::MQTT:
_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;
}
void PowerMeterClass::mqtt()
if (!_upProvider->init()) {
_upProvider = nullptr;
}
}
float PowerMeterClass::getPowerTotal() const
{
if (!MqttSettings.getConnected()) { return; }
String topic = "powermeter";
auto totalPower = getPowerTotal();
std::lock_guard<std::mutex> 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<std::mutex> l(_mutex);
if (!_upProvider) { return 0; }
return _upProvider->getLastUpdate();
}
bool PowerMeterClass::isDataValid() const
{
std::lock_guard<std::mutex> 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<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;
std::lock_guard<std::mutex> lock(_mutex);
if (!_upProvider) { return; }
_upProvider->loop();
_upProvider->mqttLoop();
}

View File

@ -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 <WiFiClientSecure.h>
#include <ArduinoJson.h>
#include "mbedtls/sha256.h"
@ -9,48 +10,63 @@
#include <memory>
#include <ESPmDNS.h>
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;
}
bool HttpPowerMeterClass::queryPhase(int phase, PowerMeterHttpConfig const& config)
gotUpdate();
_powerValues = _cache;
}
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<float>()) {
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError),
PSTR("[HttpPowerMeter] not a float: '%s'"),
PSTR("[PowerMeterHttpJson] not a float: '%s'"),
value.as<String>().c_str());
return false;
}
// 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) {
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;

View File

@ -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 <WiFiClientSecure.h>
#include <base64.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();
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<std::mutex> 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;

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
*/
#include "SMA_HM.h"
#include "PowerMeterUdpSmaHomeManager.h"
#include <Arduino.h>
#include "Configuration.h"
#include "NetworkSettings.h"
#include "MqttSettings.h"
#include <WiFiUdp.h>
#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);

View File

@ -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<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"];
for (uint8_t i = 0; i < http_phases.size(); i++) {
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"];
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<JsonObject>(), phaseConfig);
if (HttpPowerMeter.queryPhase(phase, phaseConfig)) {
auto upMeter = std::make_unique<PowerMeterHttpJson>();
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<JsonObject>(), tibberConfig);
if (TibberPowerMeter.query(tibberConfig)) {
auto upMeter = std::make_unique<PowerMeterHttpSml>();
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;

View File

@ -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<JsonObject>();
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(); }