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:
parent
8ec1695d1b
commit
2397e5cdf5
@ -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;
|
||||||
|
|||||||
@ -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;
|
|
||||||
@ -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
28
include/PowerMeterMqtt.h
Normal 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;
|
||||||
|
};
|
||||||
46
include/PowerMeterProvider.h
Normal file
46
include/PowerMeterProvider.h
Normal 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;
|
||||||
|
};
|
||||||
33
include/PowerMeterSerialSdm.h
Normal file
33
include/PowerMeterSerialSdm.h
Normal 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;
|
||||||
|
};
|
||||||
39
include/PowerMeterSerialSml.h
Normal file
39
include/PowerMeterSerialSml.h
Normal 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}
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -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;
|
|
||||||
@ -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 {
|
||||||
|
|||||||
@ -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());
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
|
||||||
@ -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
74
src/PowerMeterMqtt.cpp
Normal 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));
|
||||||
|
}
|
||||||
22
src/PowerMeterProvider.cpp
Normal file
22
src/PowerMeterProvider.cpp
Normal 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
113
src/PowerMeterSerialSdm.cpp
Normal 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();
|
||||||
|
}
|
||||||
68
src/PowerMeterSerialSml.cpp
Normal file
68
src/PowerMeterSerialSml.cpp
Normal 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());
|
||||||
|
}
|
||||||
@ -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);
|
||||||
@ -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;
|
||||||
|
|||||||
@ -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(); }
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user