OpenDTU-old/src/PowerMeter.cpp
helgeerbe d494810975
merge V23.12.16 (#556)
* Optimize Sun data calculation

* Remove not required enum

* Split config struct into different sub structs

* Feature: Allow configuration of LWT QoS

* Made resetreason methods static

* Feature: Implement offset cache for "YieldDay"

Thanks to @broth-itk for the idea!
Fix: #1258 #1397

* Add Esp32-Stick-PoE-A

* remove broken LilyGO_T_ETH_POE config, use device profile instead

* Feature: High resolution Icon and PWA (Progressive Web App) functionality

Fix: #1289

* webapp: Update dependencies

* Initialize TaskScheduler

* Migrate SunPosition to TaskScheduler

* Migrate Datastore to TaskScheduler

* Migrate MqttHandleInverterTotal to TaskSchedule

* Migrate MqttHandleHass to TaskScheduler

* Migrate MqttHandleDtu to TaskScheduler

* Migrate MqttHandleInverter to TaskScheduler

* Migrate LedSingle to TaskScheduler

* Migrate NetworkSettings to TaskScheduler

* Migrate InverterSettings to TaskScheduler

* Migrate MessageOutput to TaskScheduler

* Migrate Display_Graphic to TaskScheduler

* Migrate WebApi to TaskScheduler

* Split InverterSettings into multiple tasks

* Calculate SunPosition only every 5 seconds

* Split LedSingle into multiple tasks

* Upgrade espMqttClient from 1.4.5 to 1.5.0

* Doc: Correct amount of MPP-Tracker

* Added HMT-1600-4T and HMT-1800-4T to DevInfoParser

Fix #1524

* Adjusted inverter names for HMS-1600/1800/2000-4T

* Add channel count to description of detected inverter type (DevInfoParser)

* Adjust device web api endpoint for dynamic led count

* Feature: Added ability to change the brightness of the LEDs

Based on the idea of @moritzlerch with several modifications like pwmTable and structure

* webapp: Update dependencies

* Update olikraus/U8g2 from 2.35.7 to 2.35.8

* Remove not required onWebsocketEvent

* Remove code nesting

* Introduce several const statements

* Remove not required AsyncEventSource

* Doc: Added byte specification to each command

* Feature: Added basic Grid Profile parser which shows the used profile and version

Other values are still outstanding.

* Optimize AlarmLogParser to save memory

* Add libfrozen to project to create constexpr maps

* Feature: First version of GridProfile Parser which shows all values contained in the profile.

* webapp: Update dependencies

* Apply better variable names

* Remove not required casts

* Add additional compiler flags to prevent errors

* Add const statement to several variables

* Replace NULL by nullptr

* Update bblanchon/ArduinoJson from 6.21.3 to 6.21.4

* Add const keyword to method parameters

* Add const keyword to methods

* Use references instead of pointers whenver possible

* Adjust member variable names in MqttSettings

* Adjust member variable names in NetworkSettings

* webapp: Update timezone database to latest version

* webapp: Beautify and unify form footers

* Feature: Allow setting of an inverter limit of 0% and 0W

Thanks to @madmartin in #1270

* Feature: Allow links in device profiles

These links will be shown on the hardware settings page.

* Doc: Added hint regarding HMS-xxxx-xT-NA inverters

* Feature: Added DeviceProfile for CASmo-DTU

Based on #1565

* Upgrade actions/upload-artifact from v3 to v4

* Upgrade actions/download-artifact from v3 to v4

* webapp: add app.js.gz

* Gridprofileparser: Added latest known values

Thanks to @stefan123t and @noone2k

* webapp: Fix lint errors

* Feature: Add DTU to Home Assistant Auto Discovery

This is based on PR 1365 from @CFenner with several fixes and optimizations

* Fix: Remove debug output as it floods the console

* Fix: Gridprofileparser: Add additional error handling if profile is unknown

* webapp: add app.js.gz

* Fix: Offset cache for "YieldDay" did not work correctly

* webapp: update dependencies

* webapp: add app.js.gz

* Fix: yarn.lock was outdated

* Fix: yarn build error

* Fix: Reset Yield day correction in combination with Zero Yield Day on Midnight lead to wrong values.

* Fix: Allow negative values in GridProfileParser

* Correct variable name

* Fix #1579: Static IP in Ethernet mode did not work correctly

* Feature: Added diagram to display

This is based on the idea of @Henrik-Ingenieur and was discussed in #1504

* webapp: update dependencies

* webapp: add app.js.gz

---------

Co-authored-by: Thomas Basler <thomas@familie-basler.net>
Co-authored-by: Pierre Kancir <pierre.kancir.emn@gmail.com>
2023-12-27 11:49:57 +01:00

223 lines
7.4 KiB
C++

// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022 Thomas Basler and others
*/
#include "PowerMeter.h"
#include "Configuration.h"
#include "HttpPowerMeter.h"
#include "MqttSettings.h"
#include "NetworkSettings.h"
#include "SDM.h"
#include "MessageOutput.h"
#include <ctime>
#include <SoftwareSerial.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);
_loopTask.setCallback(std::bind(&PowerMeterClass::loop, this));
_loopTask.setIterations(TASK_FOREVER);
_loopTask.enable();
_lastPowerMeterCheck = 0;
_lastPowerMeterUpdate = 0;
for (auto const& s: _mqttSubscriptions) { MqttSettings.unsubscribe(s.first); }
_mqttSubscriptions.clear();
CONFIG_T& config = Configuration.get();
if (!config.PowerMeter.Enabled) {
return;
}
switch(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:
sdm.begin();
break;
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();
break;
}
}
void PowerMeterClass::onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total)
{
for (auto const& subscription: _mqttSubscriptions) {
if (subscription.first != topic) { continue; }
std::string value(reinterpret_cast<const char*>(payload), len);
try {
*subscription.second = std::stof(value);
}
catch(std::invalid_argument const& e) {
MessageOutput.printf("PowerMeterClass: cannot parse payload of topic '%s' as float: %s\r\n",
topic, value.c_str());
return;
}
if (_verboseLogging) {
MessageOutput.printf("PowerMeterClass: Updated from '%s', TotalPower: %5.2f\r\n",
topic, getPowerTotal());
}
_lastPowerMeterUpdate = millis();
}
}
float PowerMeterClass::getPowerTotal(bool forceUpdate)
{
if (forceUpdate) {
CONFIG_T& config = Configuration.get();
if (config.PowerMeter.Enabled
&& (millis() - _lastPowerMeterUpdate) > (1000)) {
readPowerMeter();
}
}
return _powerMeter1Power + _powerMeter2Power + _powerMeter3Power;
}
uint32_t PowerMeterClass::getLastPowerMeterUpdate()
{
return _lastPowerMeterUpdate;
}
void PowerMeterClass::mqtt()
{
if (!MqttSettings.getConnected()) {
return;
} else {
String topic = "powermeter";
MqttSettings.publish(topic + "/power1", String(_powerMeter1Power));
MqttSettings.publish(topic + "/power2", String(_powerMeter2Power));
MqttSettings.publish(topic + "/power3", String(_powerMeter3Power));
MqttSettings.publish(topic + "/powertotal", String(getPowerTotal()));
MqttSettings.publish(topic + "/voltage1", String(_powerMeter1Voltage));
MqttSettings.publish(topic + "/voltage2", String(_powerMeter2Voltage));
MqttSettings.publish(topic + "/voltage3", String(_powerMeter3Voltage));
MqttSettings.publish(topic + "/import", String(_powerMeterImport));
MqttSettings.publish(topic + "/export", String(_powerMeterExport));
}
}
void PowerMeterClass::loop()
{
CONFIG_T const& config = Configuration.get();
_verboseLogging = config.PowerMeter.VerboseLogging;
if (!config.PowerMeter.Enabled) { return; }
if (config.PowerMeter.Source == SOURCE_SML) {
if (!smlReadLoop()) {
return;
} else {
_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;
if (config.PowerMeter.Source == SOURCE_SDM1PH) {
_powerMeter1Power = static_cast<float>(sdm.readVal(SDM_PHASE_1_POWER, _address));
_powerMeter2Power = 0.0;
_powerMeter3Power = 0.0;
_powerMeter1Voltage = static_cast<float>(sdm.readVal(SDM_PHASE_1_VOLTAGE, _address));
_powerMeter2Voltage = 0.0;
_powerMeter3Voltage = 0.0;
_powerMeterImport = static_cast<float>(sdm.readVal(SDM_IMPORT_ACTIVE_ENERGY, _address));
_powerMeterExport = static_cast<float>(sdm.readVal(SDM_EXPORT_ACTIVE_ENERGY, _address));
_lastPowerMeterUpdate = millis();
}
else if (config.PowerMeter.Source == SOURCE_SDM3PH) {
_powerMeter1Power = static_cast<float>(sdm.readVal(SDM_PHASE_1_POWER, _address));
_powerMeter2Power = static_cast<float>(sdm.readVal(SDM_PHASE_2_POWER, _address));
_powerMeter3Power = static_cast<float>(sdm.readVal(SDM_PHASE_3_POWER, _address));
_powerMeter1Voltage = static_cast<float>(sdm.readVal(SDM_PHASE_1_VOLTAGE, _address));
_powerMeter2Voltage = static_cast<float>(sdm.readVal(SDM_PHASE_2_VOLTAGE, _address));
_powerMeter3Voltage = static_cast<float>(sdm.readVal(SDM_PHASE_3_VOLTAGE, _address));
_powerMeterImport = static_cast<float>(sdm.readVal(SDM_IMPORT_ACTIVE_ENERGY, _address));
_powerMeterExport = static_cast<float>(sdm.readVal(SDM_EXPORT_ACTIVE_ENERGY, _address));
_lastPowerMeterUpdate = millis();
}
else if (config.PowerMeter.Source == SOURCE_HTTP) {
if (HttpPowerMeter.updateValues()) {
_powerMeter1Power = HttpPowerMeter.getPower(1);
_powerMeter2Power = HttpPowerMeter.getPower(2);
_powerMeter3Power = HttpPowerMeter.getPower(3);
_lastPowerMeterUpdate = millis();
}
}
}
bool PowerMeterClass::smlReadLoop()
{
while (inputSerial.available()) {
double readVal = 0;
unsigned char smlCurrentChar = inputSerial.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;
}