Skip to content

Commit

Permalink
Feature: Set powerlimiter thresholds via MQTT
Browse files Browse the repository at this point in the history
publish DPL thresholds to MQTT, add support for setting powerlimiter
thresholds via MQTT, and make these auto-discoverable for Home
Assistent.
  • Loading branch information
LukasK13 authored and schlimmchen committed Mar 10, 2024
1 parent 803b30c commit fba5c02
Show file tree
Hide file tree
Showing 5 changed files with 295 additions and 28 deletions.
16 changes: 14 additions & 2 deletions include/MqttHandlePowerLimiter.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -28,4 +40,4 @@ class MqttHandlePowerLimiterClass {
std::deque<std::function<void()>> _mqttCallbacks;
};

extern MqttHandlePowerLimiterClass MqttHandlePowerLimiter;
extern MqttHandlePowerLimiterClass MqttHandlePowerLimiter;
26 changes: 26 additions & 0 deletions include/MqttHandlePowerLimiterHass.h
Original file line number Diff line number Diff line change
@@ -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;
116 changes: 90 additions & 26 deletions src/MqttHandlePowerLimiter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand All @@ -53,48 +70,95 @@ void MqttHandlePowerLimiterClass::loop()
if ((millis() - _lastPublish) > (config.Mqtt.PublishInterval * 1000) ) {
auto val = static_cast<unsigned>(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<const char*>(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<int>(payload_val);

std::lock_guard<std::mutex> mqttLock(_mqttMutex);

using Mode = PowerLimiterClass::Mode;
switch (static_cast<Mode>(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<Mode>(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;
}
}

// not reached if the value did not change
Configuration.write();
}
163 changes: 163 additions & 0 deletions src/MqttHandlePowerLimiterHass.cpp
Original file line number Diff line number Diff line change
@@ -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);
}
2 changes: 2 additions & 0 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -123,6 +124,7 @@ void setup()
MqttHandleBatteryHass.init(scheduler);
MqttHandleHuawei.init(scheduler);
MqttHandlePowerLimiter.init(scheduler);
MqttHandlePowerLimiterHass.init(scheduler);
MessageOutput.println("done");

// Initialize WebApi
Expand Down

3 comments on commit fba5c02

@stanpete78
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Is this changing the data in RAM or NAND?

@schlimmchen
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes are saved to the configuration on non-volatile flash storage, except for the mode override, which is volatile.

@stanpete78
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I see. Thanks for always responding so quick and also your great efforts.

Please sign in to comment.