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.
This commit is contained in:
parent
803b30ca11
commit
fba5c02346
@ -14,7 +14,19 @@ public:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
void loop();
|
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;
|
Task _loopTask;
|
||||||
|
|
||||||
|
|||||||
26
include/MqttHandlePowerLimiterHass.h
Normal file
26
include/MqttHandlePowerLimiterHass.h
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
#include <TaskSchedulerDeclarations.h>
|
||||||
|
|
||||||
|
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;
|
||||||
@ -25,8 +25,25 @@ void MqttHandlePowerLimiterClass::init(Scheduler& scheduler)
|
|||||||
using std::placeholders::_5;
|
using std::placeholders::_5;
|
||||||
using std::placeholders::_6;
|
using std::placeholders::_6;
|
||||||
|
|
||||||
String topic = MqttSettings.getPrefix() + "powerlimiter/cmd/mode";
|
String const& prefix = MqttSettings.getPrefix();
|
||||||
MqttSettings.subscribe(topic.c_str(), 0, std::bind(&MqttHandlePowerLimiterClass::onCmdMode, this, _1, _2, _3, _4, _5, _6));
|
|
||||||
|
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();
|
_lastPublish = millis();
|
||||||
}
|
}
|
||||||
@ -53,48 +70,95 @@ void MqttHandlePowerLimiterClass::loop()
|
|||||||
if ((millis() - _lastPublish) > (config.Mqtt.PublishInterval * 1000) ) {
|
if ((millis() - _lastPublish) > (config.Mqtt.PublishInterval * 1000) ) {
|
||||||
auto val = static_cast<unsigned>(PowerLimiter.getMode());
|
auto val = static_cast<unsigned>(PowerLimiter.getMode());
|
||||||
MqttSettings.publish("powerlimiter/status/mode", String(val));
|
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();
|
_lastPublish = millis();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
void MqttHandlePowerLimiterClass::onCmdMode(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<const char*>(payload), len);
|
std::string strValue(reinterpret_cast<const char*>(payload), len);
|
||||||
int intValue = -1;
|
float payload_val = -1;
|
||||||
try {
|
try {
|
||||||
intValue = std::stoi(strValue);
|
payload_val = std::stof(strValue);
|
||||||
}
|
}
|
||||||
catch (std::invalid_argument const& e) {
|
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());
|
topic, strValue.c_str());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const int intValue = static_cast<int>(payload_val);
|
||||||
|
|
||||||
std::lock_guard<std::mutex> mqttLock(_mqttMutex);
|
std::lock_guard<std::mutex> mqttLock(_mqttMutex);
|
||||||
|
|
||||||
|
switch (command) {
|
||||||
|
case MqttPowerLimiterCommand::Mode:
|
||||||
|
{
|
||||||
using Mode = PowerLimiterClass::Mode;
|
using Mode = PowerLimiterClass::Mode;
|
||||||
switch (static_cast<Mode>(intValue)) {
|
Mode mode = static_cast<Mode>(intValue);
|
||||||
case Mode::UnconditionalFullSolarPassthrough:
|
if (mode == Mode::UnconditionalFullSolarPassthrough) {
|
||||||
MessageOutput.println("Power limiter unconditional full solar PT");
|
MessageOutput.println("Power limiter unconditional full solar PT");
|
||||||
_mqttCallbacks.push_back(std::bind(&PowerLimiterClass::setMode,
|
_mqttCallbacks.push_back(std::bind(&PowerLimiterClass::setMode,
|
||||||
&PowerLimiter, Mode::UnconditionalFullSolarPassthrough));
|
&PowerLimiter, Mode::UnconditionalFullSolarPassthrough));
|
||||||
break;
|
} else if (mode == Mode::Disabled) {
|
||||||
case Mode::Disabled:
|
|
||||||
MessageOutput.println("Power limiter disabled (override)");
|
MessageOutput.println("Power limiter disabled (override)");
|
||||||
_mqttCallbacks.push_back(std::bind(&PowerLimiterClass::setMode,
|
_mqttCallbacks.push_back(std::bind(&PowerLimiterClass::setMode,
|
||||||
&PowerLimiter, Mode::Disabled));
|
&PowerLimiter, Mode::Disabled));
|
||||||
break;
|
} else if (mode == Mode::Normal) {
|
||||||
case Mode::Normal:
|
|
||||||
MessageOutput.println("Power limiter normal operation");
|
MessageOutput.println("Power limiter normal operation");
|
||||||
_mqttCallbacks.push_back(std::bind(&PowerLimiterClass::setMode,
|
_mqttCallbacks.push_back(std::bind(&PowerLimiterClass::setMode,
|
||||||
&PowerLimiter, Mode::Normal));
|
&PowerLimiter, Mode::Normal));
|
||||||
break;
|
} else {
|
||||||
default:
|
|
||||||
MessageOutput.printf("PowerLimiter - unknown mode %d\r\n", intValue);
|
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 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 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;
|
||||||
|
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;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// not reached if the value did not change
|
||||||
|
Configuration.write();
|
||||||
}
|
}
|
||||||
163
src/MqttHandlePowerLimiterHass.cpp
Normal file
163
src/MqttHandlePowerLimiterHass.cpp
Normal file
@ -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);
|
||||||
|
}
|
||||||
@ -20,6 +20,7 @@
|
|||||||
#include "MqttHandleVedirect.h"
|
#include "MqttHandleVedirect.h"
|
||||||
#include "MqttHandleHuawei.h"
|
#include "MqttHandleHuawei.h"
|
||||||
#include "MqttHandlePowerLimiter.h"
|
#include "MqttHandlePowerLimiter.h"
|
||||||
|
#include "MqttHandlePowerLimiterHass.h"
|
||||||
#include "MqttSettings.h"
|
#include "MqttSettings.h"
|
||||||
#include "NetworkSettings.h"
|
#include "NetworkSettings.h"
|
||||||
#include "NtpSettings.h"
|
#include "NtpSettings.h"
|
||||||
@ -123,6 +124,7 @@ void setup()
|
|||||||
MqttHandleBatteryHass.init(scheduler);
|
MqttHandleBatteryHass.init(scheduler);
|
||||||
MqttHandleHuawei.init(scheduler);
|
MqttHandleHuawei.init(scheduler);
|
||||||
MqttHandlePowerLimiter.init(scheduler);
|
MqttHandlePowerLimiter.init(scheduler);
|
||||||
|
MqttHandlePowerLimiterHass.init(scheduler);
|
||||||
MessageOutput.println("done");
|
MessageOutput.println("done");
|
||||||
|
|
||||||
// Initialize WebApi
|
// Initialize WebApi
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user