From fba5c023466eb38a3d572548389ca8ef7bb074a1 Mon Sep 17 00:00:00 2001 From: LukasK13 Date: Sun, 18 Feb 2024 12:14:01 +0100 Subject: [PATCH] Feature: Set powerlimiter thresholds via MQTT publish DPL thresholds to MQTT, add support for setting powerlimiter thresholds via MQTT, and make these auto-discoverable for Home Assistent. --- include/MqttHandlePowerLimiter.h | 16 ++- include/MqttHandlePowerLimiterHass.h | 26 +++++ src/MqttHandlePowerLimiter.cpp | 116 ++++++++++++++----- src/MqttHandlePowerLimiterHass.cpp | 163 +++++++++++++++++++++++++++ src/main.cpp | 2 + 5 files changed, 295 insertions(+), 28 deletions(-) create mode 100644 include/MqttHandlePowerLimiterHass.h create mode 100644 src/MqttHandlePowerLimiterHass.cpp diff --git a/include/MqttHandlePowerLimiter.h b/include/MqttHandlePowerLimiter.h index d78c3f19d..fa7ef12cc 100644 --- a/include/MqttHandlePowerLimiter.h +++ b/include/MqttHandlePowerLimiter.h @@ -14,7 +14,19 @@ class MqttHandlePowerLimiterClass { private: void loop(); - void onCmdMode(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total); + + enum class MqttPowerLimiterCommand : unsigned { + Mode, + BatterySoCStartThreshold, + BatterySoCStopThreshold, + FullSolarPassthroughSoC, + VoltageStartThreshold, + VoltageStopThreshold, + FullSolarPassThroughStartVoltage, + FullSolarPassThroughStopVoltage + }; + + void onMqttCmd(MqttPowerLimiterCommand command, const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total); Task _loopTask; @@ -28,4 +40,4 @@ class MqttHandlePowerLimiterClass { std::deque> _mqttCallbacks; }; -extern MqttHandlePowerLimiterClass MqttHandlePowerLimiter; \ No newline at end of file +extern MqttHandlePowerLimiterClass MqttHandlePowerLimiter; diff --git a/include/MqttHandlePowerLimiterHass.h b/include/MqttHandlePowerLimiterHass.h new file mode 100644 index 000000000..f465994c6 --- /dev/null +++ b/include/MqttHandlePowerLimiterHass.h @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#include + +class MqttHandlePowerLimiterHassClass { +public: + void init(Scheduler& scheduler); + void publishConfig(); + void forceUpdate(); + +private: + void loop(); + void publish(const String& subtopic, const String& payload); + void publishNumber(const char* caption, const char* icon, const char* category, const char* commandTopic, const char* stateTopic, const char* unitOfMeasure, const int16_t min, const int16_t max); + void publishSelect(const char* caption, const char* icon, const char* category, const char* commandTopic, const char* stateTopic); + void createDeviceInfo(JsonObject& object); + + Task _loopTask; + + bool _wasConnected = false; + bool _updateForced = false; +}; + +extern MqttHandlePowerLimiterHassClass MqttHandlePowerLimiterHass; diff --git a/src/MqttHandlePowerLimiter.cpp b/src/MqttHandlePowerLimiter.cpp index b807fef0e..065f65105 100644 --- a/src/MqttHandlePowerLimiter.cpp +++ b/src/MqttHandlePowerLimiter.cpp @@ -25,8 +25,25 @@ void MqttHandlePowerLimiterClass::init(Scheduler& scheduler) using std::placeholders::_5; using std::placeholders::_6; - String topic = MqttSettings.getPrefix() + "powerlimiter/cmd/mode"; - MqttSettings.subscribe(topic.c_str(), 0, std::bind(&MqttHandlePowerLimiterClass::onCmdMode, this, _1, _2, _3, _4, _5, _6)); + String const& prefix = MqttSettings.getPrefix(); + + auto subscribe = [&prefix, this](char const* subTopic, MqttPowerLimiterCommand command) { + String fullTopic(prefix + "powerlimiter/cmd/" + subTopic); + MqttSettings.subscribe(fullTopic.c_str(), 0, + std::bind(&MqttHandlePowerLimiterClass::onMqttCmd, this, command, + std::placeholders::_1, std::placeholders::_2, + std::placeholders::_3, std::placeholders::_4, + std::placeholders::_5, std::placeholders::_6)); + }; + + subscribe("threshold/soc/start", MqttPowerLimiterCommand::BatterySoCStartThreshold); + subscribe("threshold/soc/stop", MqttPowerLimiterCommand::BatterySoCStopThreshold); + subscribe("threshold/soc/full_solar_passthrough", MqttPowerLimiterCommand::FullSolarPassthroughSoC); + subscribe("threshold/voltage/start", MqttPowerLimiterCommand::VoltageStartThreshold); + subscribe("threshold/voltage/stop", MqttPowerLimiterCommand::VoltageStopThreshold); + subscribe("threshold/voltage/full_solar_passthrough_start", MqttPowerLimiterCommand::FullSolarPassThroughStartVoltage); + subscribe("threshold/voltage/full_solar_passthrough_stop", MqttPowerLimiterCommand::FullSolarPassThroughStopVoltage); + subscribe("mode", MqttPowerLimiterCommand::Mode); _lastPublish = millis(); } @@ -53,48 +70,95 @@ void MqttHandlePowerLimiterClass::loop() if ((millis() - _lastPublish) > (config.Mqtt.PublishInterval * 1000) ) { auto val = static_cast(PowerLimiter.getMode()); MqttSettings.publish("powerlimiter/status/mode", String(val)); + MqttSettings.publish("powerlimiter/status/threshold/soc/start", String(config.PowerLimiter.BatterySocStartThreshold)); + MqttSettings.publish("powerlimiter/status/threshold/soc/stop", String(config.PowerLimiter.BatterySocStopThreshold)); + MqttSettings.publish("powerlimiter/status/threshold/soc/full_solar_passthrough", String(config.PowerLimiter.FullSolarPassThroughSoc)); + MqttSettings.publish("powerlimiter/status/threshold/voltage/start", String(config.PowerLimiter.VoltageStartThreshold)); + MqttSettings.publish("powerlimiter/status/threshold/voltage/stop", String(config.PowerLimiter.VoltageStopThreshold)); + MqttSettings.publish("powerlimiter/status/threshold/voltage/full_solar_passthrough_start", String(config.PowerLimiter.FullSolarPassThroughStartVoltage)); + MqttSettings.publish("powerlimiter/status/threshold/voltage/full_solar_passthrough_stop", String(config.PowerLimiter.FullSolarPassThroughStopVoltage)); - yield(); _lastPublish = millis(); } } - -void MqttHandlePowerLimiterClass::onCmdMode(const espMqttClientTypes::MessageProperties& properties, - const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) +void MqttHandlePowerLimiterClass::onMqttCmd(MqttPowerLimiterCommand command, const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) { + CONFIG_T& config = Configuration.get(); + std::string strValue(reinterpret_cast(payload), len); - int intValue = -1; + float payload_val = -1; try { - intValue = std::stoi(strValue); + payload_val = std::stof(strValue); } catch (std::invalid_argument const& e) { - MessageOutput.printf("PowerLimiter MQTT handler: cannot parse payload of topic '%s' as int: %s\r\n", + MessageOutput.printf("PowerLimiter MQTT handler: cannot parse payload of topic '%s' as float: %s\r\n", topic, strValue.c_str()); return; } + const int intValue = static_cast(payload_val); std::lock_guard mqttLock(_mqttMutex); - using Mode = PowerLimiterClass::Mode; - switch (static_cast(intValue)) { - case Mode::UnconditionalFullSolarPassthrough: - MessageOutput.println("Power limiter unconditional full solar PT"); - _mqttCallbacks.push_back(std::bind(&PowerLimiterClass::setMode, - &PowerLimiter, Mode::UnconditionalFullSolarPassthrough)); + switch (command) { + case MqttPowerLimiterCommand::Mode: + { + using Mode = PowerLimiterClass::Mode; + Mode mode = static_cast(intValue); + if (mode == Mode::UnconditionalFullSolarPassthrough) { + MessageOutput.println("Power limiter unconditional full solar PT"); + _mqttCallbacks.push_back(std::bind(&PowerLimiterClass::setMode, + &PowerLimiter, Mode::UnconditionalFullSolarPassthrough)); + } else if (mode == Mode::Disabled) { + MessageOutput.println("Power limiter disabled (override)"); + _mqttCallbacks.push_back(std::bind(&PowerLimiterClass::setMode, + &PowerLimiter, Mode::Disabled)); + } else if (mode == Mode::Normal) { + MessageOutput.println("Power limiter normal operation"); + _mqttCallbacks.push_back(std::bind(&PowerLimiterClass::setMode, + &PowerLimiter, Mode::Normal)); + } else { + MessageOutput.printf("PowerLimiter - unknown mode %d\r\n", intValue); + } + return; + } + case MqttPowerLimiterCommand::BatterySoCStartThreshold: + if (config.PowerLimiter.BatterySocStartThreshold == intValue) { return; } + MessageOutput.printf("Setting battery SoC start threshold to: %d %%\r\n", intValue); + config.PowerLimiter.BatterySocStartThreshold = intValue; + break; + case MqttPowerLimiterCommand::BatterySoCStopThreshold: + if (config.PowerLimiter.BatterySocStopThreshold == intValue) { return; } + MessageOutput.printf("Setting battery SoC stop threshold to: %d %%\r\n", intValue); + config.PowerLimiter.BatterySocStopThreshold = intValue; + break; + case MqttPowerLimiterCommand::FullSolarPassthroughSoC: + if (config.PowerLimiter.FullSolarPassThroughSoc == intValue) { return; } + MessageOutput.printf("Setting full solar passthrough SoC to: %d %%\r\n", intValue); + config.PowerLimiter.FullSolarPassThroughSoc = intValue; break; - case Mode::Disabled: - MessageOutput.println("Power limiter disabled (override)"); - _mqttCallbacks.push_back(std::bind(&PowerLimiterClass::setMode, - &PowerLimiter, Mode::Disabled)); + case MqttPowerLimiterCommand::VoltageStartThreshold: + if (config.PowerLimiter.VoltageStartThreshold == payload_val) { return; } + MessageOutput.printf("Setting voltage start threshold to: %.2f V\r\n", payload_val); + config.PowerLimiter.VoltageStartThreshold = payload_val; break; - case Mode::Normal: - MessageOutput.println("Power limiter normal operation"); - _mqttCallbacks.push_back(std::bind(&PowerLimiterClass::setMode, - &PowerLimiter, Mode::Normal)); + case MqttPowerLimiterCommand::VoltageStopThreshold: + if (config.PowerLimiter.VoltageStopThreshold == payload_val) { return; } + MessageOutput.printf("Setting voltage stop threshold to: %.2f V\r\n", payload_val); + config.PowerLimiter.VoltageStopThreshold = payload_val; break; - default: - MessageOutput.printf("PowerLimiter - unknown mode %d\r\n", intValue); + case MqttPowerLimiterCommand::FullSolarPassThroughStartVoltage: + if (config.PowerLimiter.FullSolarPassThroughStartVoltage == payload_val) { return; } + MessageOutput.printf("Setting full solar passthrough start voltage to: %.2f V\r\n", payload_val); + config.PowerLimiter.FullSolarPassThroughStartVoltage = payload_val; + break; + case MqttPowerLimiterCommand::FullSolarPassThroughStopVoltage: + if (config.PowerLimiter.FullSolarPassThroughStopVoltage == payload_val) { return; } + MessageOutput.printf("Setting full solar passthrough stop voltage to: %.2f V\r\n", payload_val); + config.PowerLimiter.FullSolarPassThroughStopVoltage = payload_val; break; } -} \ No newline at end of file + + // not reached if the value did not change + Configuration.write(); +} diff --git a/src/MqttHandlePowerLimiterHass.cpp b/src/MqttHandlePowerLimiterHass.cpp new file mode 100644 index 000000000..17b9ad7cb --- /dev/null +++ b/src/MqttHandlePowerLimiterHass.cpp @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Thomas Basler and others + */ +#include "MqttHandlePowerLimiterHass.h" +#include "Configuration.h" +#include "MqttSettings.h" +#include "NetworkSettings.h" +#include "MessageOutput.h" +#include "Utils.h" + +MqttHandlePowerLimiterHassClass MqttHandlePowerLimiterHass; + +void MqttHandlePowerLimiterHassClass::init(Scheduler& scheduler) +{ + scheduler.addTask(_loopTask); + _loopTask.setCallback(std::bind(&MqttHandlePowerLimiterHassClass::loop, this)); + _loopTask.setIterations(TASK_FOREVER); + _loopTask.enable(); +} + +void MqttHandlePowerLimiterHassClass::loop() +{ + if (!Configuration.get().PowerLimiter.Enabled) { + return; + } + if (_updateForced) { + publishConfig(); + _updateForced = false; + } + + if (MqttSettings.getConnected() && !_wasConnected) { + // Connection established + _wasConnected = true; + publishConfig(); + } else if (!MqttSettings.getConnected() && _wasConnected) { + // Connection lost + _wasConnected = false; + } +} + +void MqttHandlePowerLimiterHassClass::forceUpdate() +{ + _updateForced = true; +} + +void MqttHandlePowerLimiterHassClass::publishConfig() +{ + if (!Configuration.get().Mqtt.Hass.Enabled) { + return; + } + + if (!MqttSettings.getConnected()) { + return; + } + + if (!Configuration.get().PowerLimiter.Enabled) { + publishSelect("DPL Mode", "mdi:gauge", "config", "mode", "mode"); + publishNumber("DPL battery SoC start threshold", "mdi:battery-charging", "config", "threshold/soc/start", "threshold/soc/start", "%", 0, 100); + publishNumber("DPL battery SoC stop threshold", "mdi:battery-charging", "config", "threshold/soc/stop", "threshold/soc/stop", "%", 0, 100); + } + if (!Configuration.get().Vedirect.Enabled) { + publishNumber("DPL full solar passthrough SoC", "mdi:transmission-tower-import", "config", "threshold/soc/full_solar_passthrough", "threshold/soc/full_solar_passthrough", "%", 0, 100); + } +} + +void MqttHandlePowerLimiterHassClass::publishSelect( + const char* caption, const char* icon, const char* category, + const char* commandTopic, const char* stateTopic) +{ + + String selectId = caption; + selectId.replace(" ", "_"); + selectId.toLowerCase(); + + const String configTopic = "select/powerlimiter/" + selectId + "/config"; + + const String cmdTopic = MqttSettings.getPrefix() + "powerlimiter/cmd/" + commandTopic; + const String statTopic = MqttSettings.getPrefix() + "powerlimiter/status/" + stateTopic; + + DynamicJsonDocument root(1024); + if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { + return; + } + + root["name"] = caption; + root["uniq_id"] = selectId; + if (strcmp(icon, "")) { + root["ic"] = icon; + } + root["ent_cat"] = category; + root["cmd_t"] = cmdTopic; + root["stat_t"] = statTopic; + JsonArray options = root.createNestedArray("options"); + options.add("0"); + options.add("1"); + options.add("2"); + + JsonObject deviceObj = root.createNestedObject("dev"); + createDeviceInfo(deviceObj); + + String buffer; + serializeJson(root, buffer); + publish(configTopic, buffer); +} + +void MqttHandlePowerLimiterHassClass::publishNumber( + const char* caption, const char* icon, const char* category, + const char* commandTopic, const char* stateTopic, const char* unitOfMeasure, + const int16_t min, const int16_t max) +{ + + String numberId = caption; + numberId.replace(" ", "_"); + numberId.toLowerCase(); + + const String configTopic = "number/powerlimiter/" + numberId + "/config"; + + const String cmdTopic = MqttSettings.getPrefix() + "powerlimiter/cmd/" + commandTopic; + const String statTopic = MqttSettings.getPrefix() + "powerlimiter/status/" + stateTopic; + + DynamicJsonDocument root(1024); + if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { + return; + } + + root["name"] = caption; + root["uniq_id"] = numberId; + if (strcmp(icon, "")) { + root["ic"] = icon; + } + root["ent_cat"] = category; + root["cmd_t"] = cmdTopic; + root["stat_t"] = statTopic; + root["unit_of_meas"] = unitOfMeasure; + root["min"] = min; + root["max"] = max; + root["mode"] = "box"; + + JsonObject deviceObj = root.createNestedObject("dev"); + createDeviceInfo(deviceObj); + + String buffer; + serializeJson(root, buffer); + publish(configTopic, buffer); +} + +void MqttHandlePowerLimiterHassClass::createDeviceInfo(JsonObject& object) +{ + object["name"] = "Dynamic Power Limiter"; + object["ids"] = "0002"; + object["cu"] = String("http://") + NetworkSettings.localIP().toString(); + object["mf"] = "OpenDTU"; + object["mdl"] = "Dynamic Power Limiter"; + object["sw"] = AUTO_GIT_HASH; +} + +void MqttHandlePowerLimiterHassClass::publish(const String& subtopic, const String& payload) +{ + String topic = Configuration.get().Mqtt.Hass.Topic; + topic += subtopic; + MqttSettings.publishGeneric(topic.c_str(), payload.c_str(), Configuration.get().Mqtt.Hass.Retain); +} diff --git a/src/main.cpp b/src/main.cpp index c72bde4a7..e0a54c155 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -20,6 +20,7 @@ #include "MqttHandleVedirect.h" #include "MqttHandleHuawei.h" #include "MqttHandlePowerLimiter.h" +#include "MqttHandlePowerLimiterHass.h" #include "MqttSettings.h" #include "NetworkSettings.h" #include "NtpSettings.h" @@ -123,6 +124,7 @@ void setup() MqttHandleBatteryHass.init(scheduler); MqttHandleHuawei.init(scheduler); MqttHandlePowerLimiter.init(scheduler); + MqttHandlePowerLimiterHass.init(scheduler); MessageOutput.println("done"); // Initialize WebApi