Feature: implement PowerMeter pin config for serial interfaces

in your pin_mapping.json, add a powermeter object like this:

[
    {
        "name": "My Board",
        ...
        "powermeter": {
            "rx": <num>,
            "tx": <num>,
            "dere": <num>
        },
        ...
    }
]

the SML power meter requires the rx pin to be set. the SDM power meter
requires the rx and tx pins are set. the "dere" pin pin is optional and
if set, this pin controls the driver enable and receiver enable pins of
the RS485 transceiver. the SDM library handles this pin.

closes #771.
This commit is contained in:
Bernhard Kirchen 2024-03-18 21:40:33 +01:00 committed by Bernhard Kirchen
parent 7d4a30dde4
commit cd339a3a14
5 changed files with 104 additions and 69 deletions

View File

@ -38,6 +38,9 @@ struct PinMapping_t {
uint8_t display_clk; uint8_t display_clk;
uint8_t display_cs; uint8_t display_cs;
uint8_t display_reset; uint8_t display_reset;
int8_t led[PINMAPPING_LED_COUNT];
// OpenDTU-OnBattery-specific pins below
int8_t victron_tx; int8_t victron_tx;
int8_t victron_rx; int8_t victron_rx;
int8_t victron_tx2; int8_t victron_tx2;
@ -52,7 +55,9 @@ struct PinMapping_t {
int8_t huawei_irq; int8_t huawei_irq;
int8_t huawei_cs; int8_t huawei_cs;
int8_t huawei_power; int8_t huawei_power;
int8_t led[PINMAPPING_LED_COUNT]; int8_t powermeter_rx;
int8_t powermeter_tx;
int8_t powermeter_dere;
}; };
class PinMappingClass { class PinMappingClass {

View File

@ -10,18 +10,7 @@
#include "SDM.h" #include "SDM.h"
#include "sml.h" #include "sml.h"
#include <TaskSchedulerDeclarations.h> #include <TaskSchedulerDeclarations.h>
#include <SoftwareSerial.h>
#ifndef SDM_RX_PIN
#define SDM_RX_PIN 13
#endif
#ifndef SDM_TX_PIN
#define SDM_TX_PIN 32
#endif
#ifndef SML_RX_PIN
#define SML_RX_PIN 35
#endif
typedef struct { typedef struct {
const unsigned char OBIS[6]; const unsigned char OBIS[6];
@ -31,13 +20,13 @@ typedef struct {
class PowerMeterClass { class PowerMeterClass {
public: public:
enum SOURCE { enum class Source : unsigned {
SOURCE_MQTT = 0, MQTT = 0,
SOURCE_SDM1PH = 1, SDM1PH = 1,
SOURCE_SDM3PH = 2, SDM3PH = 2,
SOURCE_HTTP = 3, HTTP = 3,
SOURCE_SML = 4, SML = 4,
SOURCE_SMAHM2 = 5 SMAHM2 = 5
}; };
void init(Scheduler& scheduler); void init(Scheduler& scheduler);
float getPowerTotal(bool forceUpdate = true); float getPowerTotal(bool forceUpdate = true);
@ -70,6 +59,9 @@ private:
mutable std::mutex _mutex; mutable std::mutex _mutex;
std::unique_ptr<SDM> _upSdm = nullptr;
std::unique_ptr<SoftwareSerial> _upSmlSerial = nullptr;
void readPowerMeter(); void readPowerMeter();
bool smlReadLoop(); bool smlReadLoop();

View File

@ -144,6 +144,18 @@
#define HUAWEI_PIN_POWER -1 #define HUAWEI_PIN_POWER -1
#endif #endif
#ifndef POWERMETER_PIN_RX
#define POWERMETER_PIN_RX -1
#endif
#ifndef POWERMETER_PIN_TX
#define POWERMETER_PIN_TX -1
#endif
#ifndef POWERMETER_PIN_DERE
#define POWERMETER_PIN_DERE -1
#endif
PinMappingClass PinMapping; PinMappingClass PinMapping;
PinMappingClass::PinMappingClass() PinMappingClass::PinMappingClass()
@ -182,6 +194,10 @@ PinMappingClass::PinMappingClass()
_pinMapping.display_cs = DISPLAY_CS; _pinMapping.display_cs = DISPLAY_CS;
_pinMapping.display_reset = DISPLAY_RESET; _pinMapping.display_reset = DISPLAY_RESET;
_pinMapping.led[0] = LED0;
_pinMapping.led[1] = LED1;
// OpenDTU-OnBattery-specific pins below
_pinMapping.victron_rx = VICTRON_PIN_RX; _pinMapping.victron_rx = VICTRON_PIN_RX;
_pinMapping.victron_tx = VICTRON_PIN_TX; _pinMapping.victron_tx = VICTRON_PIN_TX;
@ -199,8 +215,10 @@ PinMappingClass::PinMappingClass()
_pinMapping.huawei_cs = HUAWEI_PIN_CS; _pinMapping.huawei_cs = HUAWEI_PIN_CS;
_pinMapping.huawei_irq = HUAWEI_PIN_IRQ; _pinMapping.huawei_irq = HUAWEI_PIN_IRQ;
_pinMapping.huawei_power = HUAWEI_PIN_POWER; _pinMapping.huawei_power = HUAWEI_PIN_POWER;
_pinMapping.led[0] = LED0;
_pinMapping.led[1] = LED1; _pinMapping.powermeter_rx = POWERMETER_PIN_RX;
_pinMapping.powermeter_tx = POWERMETER_PIN_TX;
_pinMapping.powermeter_dere = POWERMETER_PIN_DERE;
} }
PinMapping_t& PinMappingClass::get() PinMapping_t& PinMappingClass::get()
@ -260,6 +278,10 @@ bool PinMappingClass::init(const String& deviceMapping)
_pinMapping.display_cs = doc[i]["display"]["cs"] | DISPLAY_CS; _pinMapping.display_cs = doc[i]["display"]["cs"] | DISPLAY_CS;
_pinMapping.display_reset = doc[i]["display"]["reset"] | DISPLAY_RESET; _pinMapping.display_reset = doc[i]["display"]["reset"] | DISPLAY_RESET;
_pinMapping.led[0] = doc[i]["led"]["led0"] | LED0;
_pinMapping.led[1] = doc[i]["led"]["led1"] | LED1;
// OpenDTU-OnBattery-specific pins below
_pinMapping.victron_rx = doc[i]["victron"]["rx"] | VICTRON_PIN_RX; _pinMapping.victron_rx = doc[i]["victron"]["rx"] | VICTRON_PIN_RX;
_pinMapping.victron_tx = doc[i]["victron"]["tx"] | VICTRON_PIN_TX; _pinMapping.victron_tx = doc[i]["victron"]["tx"] | VICTRON_PIN_TX;
_pinMapping.victron_rx2 = doc[i]["victron"]["rx2"] | VICTRON_PIN_RX; _pinMapping.victron_rx2 = doc[i]["victron"]["rx2"] | VICTRON_PIN_RX;
@ -277,8 +299,9 @@ bool PinMappingClass::init(const String& deviceMapping)
_pinMapping.huawei_cs = doc[i]["huawei"]["cs"] | HUAWEI_PIN_CS; _pinMapping.huawei_cs = doc[i]["huawei"]["cs"] | HUAWEI_PIN_CS;
_pinMapping.huawei_power = doc[i]["huawei"]["power"] | HUAWEI_PIN_POWER; _pinMapping.huawei_power = doc[i]["huawei"]["power"] | HUAWEI_PIN_POWER;
_pinMapping.led[0] = doc[i]["led"]["led0"] | LED0; _pinMapping.powermeter_rx = doc[i]["powermeter"]["rx"] | POWERMETER_PIN_RX;
_pinMapping.led[1] = doc[i]["led"]["led1"] | LED1; _pinMapping.powermeter_tx = doc[i]["powermeter"]["tx"] | POWERMETER_PIN_TX;
_pinMapping.powermeter_dere = doc[i]["powermeter"]["dere"] | POWERMETER_PIN_DERE;
return true; return true;
} }

View File

@ -4,21 +4,16 @@
*/ */
#include "PowerMeter.h" #include "PowerMeter.h"
#include "Configuration.h" #include "Configuration.h"
#include "PinMapping.h"
#include "HttpPowerMeter.h" #include "HttpPowerMeter.h"
#include "MqttSettings.h" #include "MqttSettings.h"
#include "NetworkSettings.h" #include "NetworkSettings.h"
#include "SDM.h"
#include "MessageOutput.h" #include "MessageOutput.h"
#include <ctime> #include <ctime>
#include <SoftwareSerial.h>
#include <SMA_HM.h> #include <SMA_HM.h>
PowerMeterClass PowerMeter; PowerMeterClass PowerMeter;
SDM sdm(Serial2, 9600, NOT_A_PIN, SERIAL_8N1, SDM_RX_PIN, SDM_TX_PIN);
SoftwareSerial inputSerial;
void PowerMeterClass::init(Scheduler& scheduler) void PowerMeterClass::init(Scheduler& scheduler)
{ {
scheduler.addTask(_loopTask); scheduler.addTask(_loopTask);
@ -38,8 +33,12 @@ void PowerMeterClass::init(Scheduler& scheduler)
return; return;
} }
switch(config.PowerMeter.Source) { const PinMapping_t& pin = PinMapping.get();
case SOURCE_MQTT: { 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) { auto subscribe = [this](char const* topic, float* target) {
if (strlen(topic) == 0) { return; } if (strlen(topic) == 0) { return; }
MqttSettings.subscribe(topic, 0, MqttSettings.subscribe(topic, 0,
@ -57,24 +56,37 @@ void PowerMeterClass::init(Scheduler& scheduler)
break; break;
} }
case SOURCE_SDM1PH: case Source::SDM1PH:
case SOURCE_SDM3PH: case Source::SDM3PH:
sdm.begin(); 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;
}
_upSdm = std::make_unique<SDM>(Serial2, 9600, pin.powermeter_dere,
SERIAL_8N1, pin.powermeter_rx, pin.powermeter_tx);
_upSdm->begin();
break; break;
case SOURCE_HTTP: case Source::HTTP:
HttpPowerMeter.init(); HttpPowerMeter.init();
break; break;
case SOURCE_SML: case Source::SML:
pinMode(SML_RX_PIN, INPUT); if (pin.powermeter_rx < 0) {
inputSerial.begin(9600, SWSERIAL_8N1, SML_RX_PIN, -1, false, 128, 95); MessageOutput.println("[PowerMeter] invalid pin config for SML power meter (RX pin must be defined)");
inputSerial.enableRx(true); return;
inputSerial.enableTx(false); }
inputSerial.flush();
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; break;
case SOURCE_SMAHM2: case Source::SMAHM2:
SMA_HM.init(scheduler, config.PowerMeter.VerboseLogging); SMA_HM.init(scheduler, config.PowerMeter.VerboseLogging);
break; break;
} }
@ -150,13 +162,11 @@ void PowerMeterClass::loop()
if (!config.PowerMeter.Enabled) { return; } if (!config.PowerMeter.Enabled) { return; }
if (config.PowerMeter.Source == SOURCE_SML) { if (static_cast<Source>(config.PowerMeter.Source) == Source::SML &&
if (!smlReadLoop()) { nullptr != _upSmlSerial) {
return; if (!smlReadLoop()) { return; }
} else {
_lastPowerMeterUpdate = millis(); _lastPowerMeterUpdate = millis();
} }
}
if ((millis() - _lastPowerMeterCheck) < (config.PowerMeter.Interval * 1000)) { if ((millis() - _lastPowerMeterCheck) < (config.PowerMeter.Interval * 1000)) {
return; return;
@ -176,14 +186,17 @@ void PowerMeterClass::readPowerMeter()
CONFIG_T& config = Configuration.get(); CONFIG_T& config = Configuration.get();
uint8_t _address = config.PowerMeter.SdmAddress; uint8_t _address = config.PowerMeter.SdmAddress;
Source configuredSource = static_cast<Source>(config.PowerMeter.Source);
if (configuredSource == Source::SDM1PH) {
if (!_upSdm) { return; }
if (config.PowerMeter.Source == SOURCE_SDM1PH) {
// this takes a "very long" time as each readVal() is a synchronous // this takes a "very long" time as each readVal() is a synchronous
// exchange of serial messages. cache the values and write later. // exchange of serial messages. cache the values and write later.
auto phase1Power = sdm.readVal(SDM_PHASE_1_POWER, _address); auto phase1Power = _upSdm->readVal(SDM_PHASE_1_POWER, _address);
auto phase1Voltage = sdm.readVal(SDM_PHASE_1_VOLTAGE, _address); auto phase1Voltage = _upSdm->readVal(SDM_PHASE_1_VOLTAGE, _address);
auto energyImport = sdm.readVal(SDM_IMPORT_ACTIVE_ENERGY, _address); auto energyImport = _upSdm->readVal(SDM_IMPORT_ACTIVE_ENERGY, _address);
auto energyExport = sdm.readVal(SDM_EXPORT_ACTIVE_ENERGY, _address); auto energyExport = _upSdm->readVal(SDM_EXPORT_ACTIVE_ENERGY, _address);
std::lock_guard<std::mutex> l(_mutex); std::lock_guard<std::mutex> l(_mutex);
_powerMeter1Power = static_cast<float>(phase1Power); _powerMeter1Power = static_cast<float>(phase1Power);
@ -196,17 +209,19 @@ void PowerMeterClass::readPowerMeter()
_powerMeterExport = static_cast<float>(energyExport); _powerMeterExport = static_cast<float>(energyExport);
_lastPowerMeterUpdate = millis(); _lastPowerMeterUpdate = millis();
} }
else if (config.PowerMeter.Source == SOURCE_SDM3PH) { else if (configuredSource == Source::SDM3PH) {
if (!_upSdm) { return; }
// this takes a "very long" time as each readVal() is a synchronous // this takes a "very long" time as each readVal() is a synchronous
// exchange of serial messages. cache the values and write later. // exchange of serial messages. cache the values and write later.
auto phase1Power = sdm.readVal(SDM_PHASE_1_POWER, _address); auto phase1Power = _upSdm->readVal(SDM_PHASE_1_POWER, _address);
auto phase2Power = sdm.readVal(SDM_PHASE_2_POWER, _address); auto phase2Power = _upSdm->readVal(SDM_PHASE_2_POWER, _address);
auto phase3Power = sdm.readVal(SDM_PHASE_3_POWER, _address); auto phase3Power = _upSdm->readVal(SDM_PHASE_3_POWER, _address);
auto phase1Voltage = sdm.readVal(SDM_PHASE_1_VOLTAGE, _address); auto phase1Voltage = _upSdm->readVal(SDM_PHASE_1_VOLTAGE, _address);
auto phase2Voltage = sdm.readVal(SDM_PHASE_2_VOLTAGE, _address); auto phase2Voltage = _upSdm->readVal(SDM_PHASE_2_VOLTAGE, _address);
auto phase3Voltage = sdm.readVal(SDM_PHASE_3_VOLTAGE, _address); auto phase3Voltage = _upSdm->readVal(SDM_PHASE_3_VOLTAGE, _address);
auto energyImport = sdm.readVal(SDM_IMPORT_ACTIVE_ENERGY, _address); auto energyImport = _upSdm->readVal(SDM_IMPORT_ACTIVE_ENERGY, _address);
auto energyExport = sdm.readVal(SDM_EXPORT_ACTIVE_ENERGY, _address); auto energyExport = _upSdm->readVal(SDM_EXPORT_ACTIVE_ENERGY, _address);
std::lock_guard<std::mutex> l(_mutex); std::lock_guard<std::mutex> l(_mutex);
_powerMeter1Power = static_cast<float>(phase1Power); _powerMeter1Power = static_cast<float>(phase1Power);
@ -219,7 +234,7 @@ void PowerMeterClass::readPowerMeter()
_powerMeterExport = static_cast<float>(energyExport); _powerMeterExport = static_cast<float>(energyExport);
_lastPowerMeterUpdate = millis(); _lastPowerMeterUpdate = millis();
} }
else if (config.PowerMeter.Source == SOURCE_HTTP) { else if (configuredSource == Source::HTTP) {
if (HttpPowerMeter.updateValues()) { if (HttpPowerMeter.updateValues()) {
std::lock_guard<std::mutex> l(_mutex); std::lock_guard<std::mutex> l(_mutex);
_powerMeter1Power = HttpPowerMeter.getPower(1); _powerMeter1Power = HttpPowerMeter.getPower(1);
@ -228,7 +243,7 @@ void PowerMeterClass::readPowerMeter()
_lastPowerMeterUpdate = millis(); _lastPowerMeterUpdate = millis();
} }
} }
else if (config.PowerMeter.Source == SOURCE_SMAHM2) { else if (configuredSource == Source::SMAHM2) {
std::lock_guard<std::mutex> l(_mutex); std::lock_guard<std::mutex> l(_mutex);
_powerMeter1Power = SMA_HM.getPowerL1(); _powerMeter1Power = SMA_HM.getPowerL1();
_powerMeter2Power = SMA_HM.getPowerL2(); _powerMeter2Power = SMA_HM.getPowerL2();
@ -239,9 +254,9 @@ void PowerMeterClass::readPowerMeter()
bool PowerMeterClass::smlReadLoop() bool PowerMeterClass::smlReadLoop()
{ {
while (inputSerial.available()) { while (_upSmlSerial->available()) {
double readVal = 0; double readVal = 0;
unsigned char smlCurrentChar = inputSerial.read(); unsigned char smlCurrentChar = _upSmlSerial->read();
sml_states_t smlCurrentState = smlState(smlCurrentChar); sml_states_t smlCurrentState = smlState(smlCurrentChar);
if (smlCurrentState == SML_LISTEND) { if (smlCurrentState == SML_LISTEND) {
for (auto& handler: smlHandlerList) { for (auto& handler: smlHandlerList) {

View File

@ -118,7 +118,7 @@ void WebApiPowerMeterClass::onAdminPost(AsyncWebServerRequest* request)
return; return;
} }
if (root["source"].as<uint8_t>() == PowerMeter.SOURCE_HTTP) { if (static_cast<PowerMeterClass::Source>(root["source"].as<uint8_t>()) == PowerMeterClass::Source::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>();