Skip to content

Commit

Permalink
Feature: implement PowerMeter pin config for serial interfaces
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
schlimmchen committed Mar 19, 2024
1 parent 7d4a30d commit cd339a3
Show file tree
Hide file tree
Showing 5 changed files with 104 additions and 69 deletions.
7 changes: 6 additions & 1 deletion include/PinMapping.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ struct PinMapping_t {
uint8_t display_clk;
uint8_t display_cs;
uint8_t display_reset;
int8_t led[PINMAPPING_LED_COUNT];

// OpenDTU-OnBattery-specific pins below
int8_t victron_tx;
int8_t victron_rx;
int8_t victron_tx2;
Expand All @@ -52,7 +55,9 @@ struct PinMapping_t {
int8_t huawei_irq;
int8_t huawei_cs;
int8_t huawei_power;
int8_t led[PINMAPPING_LED_COUNT];
int8_t powermeter_rx;
int8_t powermeter_tx;
int8_t powermeter_dere;
};

class PinMappingClass {
Expand Down
32 changes: 12 additions & 20 deletions include/PowerMeter.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,7 @@
#include "SDM.h"
#include "sml.h"
#include <TaskSchedulerDeclarations.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
#include <SoftwareSerial.h>

typedef struct {
const unsigned char OBIS[6];
Expand All @@ -31,13 +20,13 @@ typedef struct {

class PowerMeterClass {
public:
enum SOURCE {
SOURCE_MQTT = 0,
SOURCE_SDM1PH = 1,
SOURCE_SDM3PH = 2,
SOURCE_HTTP = 3,
SOURCE_SML = 4,
SOURCE_SMAHM2 = 5
enum class Source : unsigned {
MQTT = 0,
SDM1PH = 1,
SDM3PH = 2,
HTTP = 3,
SML = 4,
SMAHM2 = 5
};
void init(Scheduler& scheduler);
float getPowerTotal(bool forceUpdate = true);
Expand All @@ -50,7 +39,7 @@ class PowerMeterClass {
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;
Expand All @@ -70,6 +59,9 @@ class PowerMeterClass {

mutable std::mutex _mutex;

std::unique_ptr<SDM> _upSdm = nullptr;
std::unique_ptr<SoftwareSerial> _upSmlSerial = nullptr;

void readPowerMeter();

bool smlReadLoop();
Expand Down
31 changes: 27 additions & 4 deletions src/PinMapping.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,18 @@
#define HUAWEI_PIN_POWER -1
#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::PinMappingClass()
Expand Down Expand Up @@ -182,6 +194,10 @@ PinMappingClass::PinMappingClass()
_pinMapping.display_cs = DISPLAY_CS;
_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_tx = VICTRON_PIN_TX;

Expand All @@ -199,8 +215,10 @@ PinMappingClass::PinMappingClass()
_pinMapping.huawei_cs = HUAWEI_PIN_CS;
_pinMapping.huawei_irq = HUAWEI_PIN_IRQ;
_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()
Expand Down Expand Up @@ -260,6 +278,10 @@ bool PinMappingClass::init(const String& deviceMapping)
_pinMapping.display_cs = doc[i]["display"]["cs"] | DISPLAY_CS;
_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_tx = doc[i]["victron"]["tx"] | VICTRON_PIN_TX;
_pinMapping.victron_rx2 = doc[i]["victron"]["rx2"] | VICTRON_PIN_RX;
Expand All @@ -277,8 +299,9 @@ bool PinMappingClass::init(const String& deviceMapping)
_pinMapping.huawei_cs = doc[i]["huawei"]["cs"] | HUAWEI_PIN_CS;
_pinMapping.huawei_power = doc[i]["huawei"]["power"] | HUAWEI_PIN_POWER;

_pinMapping.led[0] = doc[i]["led"]["led0"] | LED0;
_pinMapping.led[1] = doc[i]["led"]["led1"] | LED1;
_pinMapping.powermeter_rx = doc[i]["powermeter"]["rx"] | POWERMETER_PIN_RX;
_pinMapping.powermeter_tx = doc[i]["powermeter"]["tx"] | POWERMETER_PIN_TX;
_pinMapping.powermeter_dere = doc[i]["powermeter"]["dere"] | POWERMETER_PIN_DERE;

return true;
}
Expand Down
101 changes: 58 additions & 43 deletions src/PowerMeter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,16 @@
*/
#include "PowerMeter.h"
#include "Configuration.h"
#include "PinMapping.h"
#include "HttpPowerMeter.h"
#include "MqttSettings.h"
#include "NetworkSettings.h"
#include "SDM.h"
#include "MessageOutput.h"
#include <ctime>
#include <SoftwareSerial.h>
#include <SMA_HM.h>

PowerMeterClass PowerMeter;

SDM sdm(Serial2, 9600, NOT_A_PIN, SERIAL_8N1, SDM_RX_PIN, SDM_TX_PIN);

SoftwareSerial inputSerial;

void PowerMeterClass::init(Scheduler& scheduler)
{
scheduler.addTask(_loopTask);
Expand All @@ -38,8 +33,12 @@ void PowerMeterClass::init(Scheduler& scheduler)
return;
}

switch(config.PowerMeter.Source) {
case SOURCE_MQTT: {
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,
Expand All @@ -57,24 +56,37 @@ void PowerMeterClass::init(Scheduler& scheduler)
break;
}

case SOURCE_SDM1PH:
case SOURCE_SDM3PH:
sdm.begin();
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;
}

_upSdm = std::make_unique<SDM>(Serial2, 9600, pin.powermeter_dere,
SERIAL_8N1, pin.powermeter_rx, pin.powermeter_tx);
_upSdm->begin();
break;

case SOURCE_HTTP:
case Source::HTTP:
HttpPowerMeter.init();
break;

case SOURCE_SML:
pinMode(SML_RX_PIN, INPUT);
inputSerial.begin(9600, SWSERIAL_8N1, SML_RX_PIN, -1, false, 128, 95);
inputSerial.enableRx(true);
inputSerial.enableTx(false);
inputSerial.flush();
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:
case Source::SMAHM2:
SMA_HM.init(scheduler, config.PowerMeter.VerboseLogging);
break;
}
Expand Down Expand Up @@ -150,12 +162,10 @@ void PowerMeterClass::loop()

if (!config.PowerMeter.Enabled) { return; }

if (config.PowerMeter.Source == SOURCE_SML) {
if (!smlReadLoop()) {
return;
} else {
_lastPowerMeterUpdate = millis();
}
if (static_cast<Source>(config.PowerMeter.Source) == Source::SML &&
nullptr != _upSmlSerial) {
if (!smlReadLoop()) { return; }
_lastPowerMeterUpdate = millis();
}

if ((millis() - _lastPowerMeterCheck) < (config.PowerMeter.Interval * 1000)) {
Expand All @@ -176,14 +186,17 @@ 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; }

if (config.PowerMeter.Source == SOURCE_SDM1PH) {
// this takes a "very long" time as each readVal() is a synchronous
// exchange of serial messages. cache the values and write later.
auto phase1Power = sdm.readVal(SDM_PHASE_1_POWER, _address);
auto phase1Voltage = sdm.readVal(SDM_PHASE_1_VOLTAGE, _address);
auto energyImport = sdm.readVal(SDM_IMPORT_ACTIVE_ENERGY, _address);
auto energyExport = sdm.readVal(SDM_EXPORT_ACTIVE_ENERGY, _address);
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);
Expand All @@ -196,17 +209,19 @@ void PowerMeterClass::readPowerMeter()
_powerMeterExport = static_cast<float>(energyExport);
_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
// exchange of serial messages. cache the values and write later.
auto phase1Power = sdm.readVal(SDM_PHASE_1_POWER, _address);
auto phase2Power = sdm.readVal(SDM_PHASE_2_POWER, _address);
auto phase3Power = sdm.readVal(SDM_PHASE_3_POWER, _address);
auto phase1Voltage = sdm.readVal(SDM_PHASE_1_VOLTAGE, _address);
auto phase2Voltage = sdm.readVal(SDM_PHASE_2_VOLTAGE, _address);
auto phase3Voltage = sdm.readVal(SDM_PHASE_3_VOLTAGE, _address);
auto energyImport = sdm.readVal(SDM_IMPORT_ACTIVE_ENERGY, _address);
auto energyExport = sdm.readVal(SDM_EXPORT_ACTIVE_ENERGY, _address);
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);
Expand All @@ -219,7 +234,7 @@ void PowerMeterClass::readPowerMeter()
_powerMeterExport = static_cast<float>(energyExport);
_lastPowerMeterUpdate = millis();
}
else if (config.PowerMeter.Source == SOURCE_HTTP) {
else if (configuredSource == Source::HTTP) {
if (HttpPowerMeter.updateValues()) {
std::lock_guard<std::mutex> l(_mutex);
_powerMeter1Power = HttpPowerMeter.getPower(1);
Expand All @@ -228,7 +243,7 @@ void PowerMeterClass::readPowerMeter()
_lastPowerMeterUpdate = millis();
}
}
else if (config.PowerMeter.Source == SOURCE_SMAHM2) {
else if (configuredSource == Source::SMAHM2) {
std::lock_guard<std::mutex> l(_mutex);
_powerMeter1Power = SMA_HM.getPowerL1();
_powerMeter2Power = SMA_HM.getPowerL2();
Expand All @@ -239,9 +254,9 @@ void PowerMeterClass::readPowerMeter()

bool PowerMeterClass::smlReadLoop()
{
while (inputSerial.available()) {
while (_upSmlSerial->available()) {
double readVal = 0;
unsigned char smlCurrentChar = inputSerial.read();
unsigned char smlCurrentChar = _upSmlSerial->read();
sml_states_t smlCurrentState = smlState(smlCurrentChar);
if (smlCurrentState == SML_LISTEND) {
for (auto& handler: smlHandlerList) {
Expand Down
2 changes: 1 addition & 1 deletion src/WebApi_powermeter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ void WebApiPowerMeterClass::onAdminPost(AsyncWebServerRequest* request)
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"];
for (uint8_t i = 0; i < http_phases.size(); i++) {
JsonObject phase = http_phases[i].as<JsonObject>();
Expand Down

0 comments on commit cd339a3

Please sign in to comment.