From c393e521852290fcbc633a71500bbc48e850c3ee Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 17 Jul 2023 09:50:58 +0200 Subject: [PATCH] Feature: add Home Assistant MQTT discovery for Pylontech battery (#314) When OpenDTU has a Pylontech CAN Bus Battery connected and enabled, this patch adds the discovery routine for Home Assistant Signed-off-by: Martin Dummer --- include/MqttHandlePylontechHass.h | 24 ++++ src/MqttHandlePylontechHass.cpp | 185 ++++++++++++++++++++++++++++++ src/main.cpp | 3 + 3 files changed, 212 insertions(+) create mode 100644 include/MqttHandlePylontechHass.h create mode 100644 src/MqttHandlePylontechHass.cpp diff --git a/include/MqttHandlePylontechHass.h b/include/MqttHandlePylontechHass.h new file mode 100644 index 00000000..7204bc57 --- /dev/null +++ b/include/MqttHandlePylontechHass.h @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include + +class MqttHandlePylontechHassClass { +public: + void init(); + void loop(); + void publishConfig(); + void forceUpdate(); + +private: + void publish(const String& subtopic, const String& payload); + void publishBinarySensor(const char* caption, const char* subTopic, const char* payload_on, const char* payload_off); + void publishSensor(const char* caption, const char* subTopic, const char* deviceClass = NULL, const char* stateClass = NULL, const char* unitOfMeasurement = NULL); + void createDeviceInfo(JsonObject& object); + + bool _wasConnected = false; + bool _updateForced = false; + String serial = "0001"; // pseudo-serial, can be replaced in future with real serialnumber +}; + +extern MqttHandlePylontechHassClass MqttHandlePylontechHass; \ No newline at end of file diff --git a/src/MqttHandlePylontechHass.cpp b/src/MqttHandlePylontechHass.cpp new file mode 100644 index 00000000..5458119a --- /dev/null +++ b/src/MqttHandlePylontechHass.cpp @@ -0,0 +1,185 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "PylontechCanReceiver.h" +#include "Battery.h" +#include "MqttHandlePylontechHass.h" +#include "Configuration.h" +#include "MqttSettings.h" +#include "MessageOutput.h" + +MqttHandlePylontechHassClass MqttHandlePylontechHass; + +void MqttHandlePylontechHassClass::init() +{ +} + +void MqttHandlePylontechHassClass::loop() +{ + CONFIG_T& config = Configuration.get(); + if (!config.Battery_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 MqttHandlePylontechHassClass::forceUpdate() +{ + _updateForced = true; +} + +void MqttHandlePylontechHassClass::publishConfig() +{ + CONFIG_T& config = Configuration.get(); + if ((!config.Mqtt_Hass_Enabled) || (!config.Battery_Enabled)) { + return; + } + + if (!MqttSettings.getConnected()) { + return; + } + + // device info + publishSensor("Manufacturer", "manufacturer"); + + // battery info + publishSensor("Battery voltage", "voltage", "voltage", "measurement", "V"); + publishSensor("Battery current", "current", "current", "measurement", "A"); + publishSensor("Temperature", "temperature", "temperature", "measurement", "°C"); + publishSensor("State of Charge (SOC)", "stateOfCharge", "battery", "measurement", "%"); + publishSensor("State of Health (SOH)", "stateOfHealth", NULL, "measurement", "%"); + publishSensor("Charge voltage (BMS)", "settings/chargeVoltage", "voltage", "measurement", "V"); + publishSensor("Charge current limit", "settings/chargeCurrentLimitation", "current", "measurement", "A"); + publishSensor("Discharge current limit", "settings/dischargeCurrentLimitation", "current", "measurement", "A"); + + publishBinarySensor("Alarm Discharge current", "alarm/overCurrentDischarge", "1", "0"); + publishBinarySensor("Warning Discharge current", "warning/highCurrentDischarge", "1", "0"); + + publishBinarySensor("Alarm Temperature low", "alarm/underTemperature", "1", "0"); + publishBinarySensor("Warning Temperature low", "warning/lowTemperature", "1", "0"); + + publishBinarySensor("Alarm Temperature high", "alarm/overTemperature", "1", "0"); + publishBinarySensor("Warning Temperature high", "warning/highTemperature", "1", "0"); + + publishBinarySensor("Alarm Voltage low", "alarm/underVoltage", "1", "0"); + publishBinarySensor("Warning Voltage low", "warning/lowVoltage", "1", "0"); + + publishBinarySensor("Alarm Voltage high", "alarm/overVoltage", "1", "0"); + publishBinarySensor("Warning Voltage high", "warning/highVoltage", "1", "0"); + + publishBinarySensor("Alarm BMS internal", "alarm/bmsInternal", "1", "0"); + publishBinarySensor("Warning BMS internal", "warning/bmsInternal", "1", "0"); + + publishBinarySensor("Alarm High charge current", "alarm/overCurrentCharge", "1", "0"); + publishBinarySensor("Warning High charge current", "warning/highCurrentCharge", "1", "0"); + + publishBinarySensor("Charge enabled", "charging/chargeEnabled", "1", "0"); + publishBinarySensor("Discharge enabled", "charging/dischargeEnabled", "1", "0"); + publishBinarySensor("Charge immediately", "charging/chargeImmediately", "1", "0"); + + yield(); +} + +void MqttHandlePylontechHassClass::publishSensor(const char* caption, const char* subTopic, const char* deviceClass, const char* stateClass, const char* unitOfMeasurement ) +{ + String sensorId = caption; + sensorId.replace(" ", "_"); + sensorId.replace("(", ""); + sensorId.replace(")", ""); + sensorId.toLowerCase(); + + String configTopic = "sensor/dtu_battery_" + serial + + "/" + sensorId + + "/config"; + + String statTopic = MqttSettings.getPrefix() + "battery/"; + // omit serial to avoid a breaking change + // statTopic.concat(serial); + // statTopic.concat("/"); + statTopic.concat(subTopic); + + DynamicJsonDocument root(1024); + root[F("name")] = caption; + root[F("stat_t")] = statTopic; + root[F("uniq_id")] = serial + "_" + sensorId; + + if (unitOfMeasurement != NULL) { + root[F("unit_of_meas")] = unitOfMeasurement; + } + + JsonObject deviceObj = root.createNestedObject("dev"); + createDeviceInfo(deviceObj); + + if (Configuration.get().Mqtt_Hass_Expire) { + root[F("exp_aft")] = Configuration.get().Mqtt_PublishInterval * 3; + } + if (deviceClass != NULL) { + root[F("dev_cla")] = deviceClass; + } + if (stateClass != NULL) { + root[F("stat_cla")] = stateClass; + } + + char buffer[512]; + serializeJson(root, buffer); + publish(configTopic, buffer); + +} +void MqttHandlePylontechHassClass::publishBinarySensor(const char* caption, const char* subTopic, const char* payload_on, const char* payload_off) +{ + String sensorId = caption; + sensorId.replace(" ", "_"); + sensorId.toLowerCase(); + + String configTopic = "binary_sensor/dtu_battery_" + serial + + "/" + sensorId + + "/config"; + + String statTopic = MqttSettings.getPrefix() + "battery/"; + // omit serial to avoid a breaking change + // statTopic.concat(serial); + // statTopic.concat("/"); + statTopic.concat(subTopic); + + DynamicJsonDocument root(1024); + root[F("name")] = caption; + root[F("uniq_id")] = serial + "_" + sensorId; + root[F("stat_t")] = statTopic; + root[F("pl_on")] = payload_on; + root[F("pl_off")] = payload_off; + + JsonObject deviceObj = root.createNestedObject("dev"); + createDeviceInfo(deviceObj); + + char buffer[512]; + serializeJson(root, buffer); + publish(configTopic, buffer); +} + +void MqttHandlePylontechHassClass::createDeviceInfo(JsonObject& object) +{ + object[F("name")] = "Battery(" + serial + ")"; + object[F("ids")] = serial; + object[F("cu")] = String(F("http://")) + NetworkSettings.localIP().toString(); + object[F("mf")] = F("OpenDTU"); + object[F("mdl")] = Battery.manufacturer; + object[F("sw")] = AUTO_GIT_HASH; +} + +void MqttHandlePylontechHassClass::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 cda2259f..6152a61d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -14,6 +14,7 @@ #include "MqttHandleDtu.h" #include "MqttHandleHass.h" #include "MqttHandleVedirectHass.h" +#include "MqttHandlePylontechHass.h" #include "MqttHandleInverter.h" #include "MqttHandleInverterTotal.h" #include "MqttHandleVedirect.h" @@ -240,6 +241,8 @@ void loop() yield(); PylontechCanReceiver.loop(); yield(); + MqttHandlePylontechHass.loop(); + yield(); HuaweiCan.loop(); yield(); LedSingle.loop();