Skip to content

Commit

Permalink
Implemented first version of Home Assistant Auto Discovery
Browse files Browse the repository at this point in the history
  • Loading branch information
tbnobody committed Jul 18, 2022
1 parent dcc7e47 commit 1124a9a
Show file tree
Hide file tree
Showing 13 changed files with 360 additions and 10 deletions.
7 changes: 6 additions & 1 deletion include/Configuration.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
#include <Arduino.h>

#define CONFIG_FILENAME "/config.bin"
#define CONFIG_VERSION 0x00011100 // 0.1.17 // make sure to clean all after change
#define CONFIG_VERSION 0x00011200 // 0.1.18 // make sure to clean all after change

#define WIFI_MAX_SSID_STRLEN 31
#define WIFI_MAX_PASSWORD_STRLEN 64
Expand Down Expand Up @@ -65,6 +65,11 @@ struct CONFIG_T {
uint64_t Dtu_Serial;
uint32_t Dtu_PollInterval;
uint8_t Dtu_PaLevel;

bool Mqtt_Hass_Enabled;
bool Mqtt_Hass_Retain;
char Mqtt_Hass_Topic[MQTT_MAX_TOPIC_STRLEN + 1];
bool Mqtt_Hass_IndividualPanels;
};

class ConfigurationClass {
Expand Down
63 changes: 63 additions & 0 deletions include/MqttHassPublishing.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once

#include "Configuration.h"
#include <Arduino.h>
#include <Hoymiles.h>
#include <memory>

// mqtt discovery device classes
enum {
DEVICE_CLS_NONE = 0,
DEVICE_CLS_CURRENT,
DEVICE_CLS_ENERGY,
DEVICE_CLS_PWR,
DEVICE_CLS_VOLTAGE,
DEVICE_CLS_FREQ,
DEVICE_CLS_TEMP,
DEVICE_CLS_POWER_FACTOR
};
const char* const deviceClasses[] = { 0, "current", "energy", "power", "voltage", "frequency", "temperature", "power_factor" };
enum {
STATE_CLS_NONE = 0,
STATE_CLS_MEASUREMENT,
STATE_CLS_TOTAL_INCREASING
};
const char* const stateClasses[] = { 0, "measurement", "total_increasing" };

typedef struct {
uint8_t fieldId; // field id
uint8_t deviceClsId; // device class
uint8_t stateClsId; // state class
} byteAssign_fieldDeviceClass_t;

const byteAssign_fieldDeviceClass_t deviceFieldAssignment[] = {
{ FLD_UDC, DEVICE_CLS_VOLTAGE, STATE_CLS_MEASUREMENT },
{ FLD_IDC, DEVICE_CLS_CURRENT, STATE_CLS_MEASUREMENT },
{ FLD_PDC, DEVICE_CLS_PWR, STATE_CLS_MEASUREMENT },
{ FLD_YD, DEVICE_CLS_ENERGY, STATE_CLS_TOTAL_INCREASING },
{ FLD_YT, DEVICE_CLS_ENERGY, STATE_CLS_TOTAL_INCREASING },
{ FLD_UAC, DEVICE_CLS_VOLTAGE, STATE_CLS_MEASUREMENT },
{ FLD_IAC, DEVICE_CLS_CURRENT, STATE_CLS_MEASUREMENT },
{ FLD_PAC, DEVICE_CLS_PWR, STATE_CLS_MEASUREMENT },
{ FLD_F, DEVICE_CLS_FREQ, STATE_CLS_MEASUREMENT },
{ FLD_T, DEVICE_CLS_TEMP, STATE_CLS_MEASUREMENT },
{ FLD_PCT, DEVICE_CLS_POWER_FACTOR, STATE_CLS_MEASUREMENT },
{ FLD_EFF, DEVICE_CLS_NONE, STATE_CLS_NONE },
{ FLD_IRR, DEVICE_CLS_NONE, STATE_CLS_NONE }
};
#define DEVICE_CLS_ASSIGN_LIST_LEN (sizeof(deviceFieldAssignment) / sizeof(byteAssign_fieldDeviceClass_t))

class MqttHassPublishingClass {
public:
void init();
void loop();
void publishConfig();

private:
void publishField(std::shared_ptr<InverterAbstract> inv, uint8_t channel, byteAssign_fieldDeviceClass_t fieldType, bool clear = false);

bool _wasConnected = false;
};

extern MqttHassPublishingClass MqttHassPublishing;
3 changes: 2 additions & 1 deletion include/MqttPublishing.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ class MqttPublishingClass {
void init();
void loop();

static String getTopic(std::shared_ptr<InverterAbstract> inv, uint8_t channel, uint8_t fieldId);

private:
void publishField(std::shared_ptr<InverterAbstract> inv, uint8_t channel, uint8_t fieldId);
String getTopic(std::shared_ptr<InverterAbstract> inv, uint8_t channel, uint8_t fieldId);

uint32_t _lastPublishStats[INV_MAX_COUNT];
uint32_t _lastPublish;
Expand Down
1 change: 1 addition & 0 deletions include/MqttSettings.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class MqttSettingsClass {
void performReconnect();
bool getConnected();
void publish(String subtopic, String payload);
void publishHass(String subtopic, String payload);

String getPrefix();

Expand Down
7 changes: 6 additions & 1 deletion include/defaults.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,9 @@

#define DTU_SERIAL 0x99978563412
#define DTU_POLL_INTERVAL 5
#define DTU_PA_LEVEL 0
#define DTU_PA_LEVEL 0

#define MQTT_HASS_ENABLED false
#define MQTT_HASS_RETAIN true
#define MQTT_HASS_TOPIC "homeassistant/"
#define MQTT_HASS_INDIVIDUALPANELS false
12 changes: 12 additions & 0 deletions src/Configuration.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ void ConfigurationClass::init()
config.Dtu_Serial = DTU_SERIAL;
config.Dtu_PollInterval = DTU_POLL_INTERVAL;
config.Dtu_PaLevel = DTU_PA_LEVEL;

config.Mqtt_Hass_Enabled = MQTT_HASS_ENABLED;
config.Mqtt_Hass_Retain = MQTT_HASS_RETAIN;
strlcpy(config.Mqtt_Hass_Topic, MQTT_TOPIC, sizeof(config.Mqtt_Hass_Topic));
config.Mqtt_Hass_IndividualPanels = MQTT_HASS_INDIVIDUALPANELS;
}

bool ConfigurationClass::write()
Expand Down Expand Up @@ -128,6 +133,13 @@ void ConfigurationClass::migrate()
init(); // Config will be completly incompatible after this update
}

if (config.Cfg_Version < 0x00011200) {
config.Mqtt_Hass_Enabled = MQTT_HASS_ENABLED;
config.Mqtt_Hass_Retain = MQTT_HASS_RETAIN;
strlcpy(config.Mqtt_Hass_Topic, MQTT_HASS_TOPIC, sizeof(config.Mqtt_Hass_Topic));
config.Mqtt_Hass_IndividualPanels = MQTT_HASS_INDIVIDUALPANELS;
}

config.Cfg_Version = CONFIG_VERSION;
write();
}
Expand Down
124 changes: 124 additions & 0 deletions src/MqttHassPublishing.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022 Thomas Basler and others
*/
#include "MqttHassPublishing.h"
#include "ArduinoJson.h"
#include "MqttPublishing.h"
#include "MqttSettings.h"
#include "WiFiSettings.h"

MqttHassPublishingClass MqttHassPublishing;

void MqttHassPublishingClass::init()
{
}

void MqttHassPublishingClass::loop()
{
if (MqttSettings.getConnected() && !_wasConnected) {
// Connection established
_wasConnected = true;
publishConfig();
} else if (!MqttSettings.getConnected() && _wasConnected) {
// Connection lost
_wasConnected = false;
}
}

void MqttHassPublishingClass::publishConfig()
{
if (!Configuration.get().Mqtt_Hass_Enabled) {
return;
}

if (!MqttSettings.getConnected() && Hoymiles.getRadio()->isIdle()) {
return;
}

CONFIG_T& config = Configuration.get();

// Loop all inverters
for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) {
auto inv = Hoymiles.getInverterByPos(i);

// Loop all channels
for (uint8_t c = 0; c <= inv->Statistics()->getChannelCount(); c++) {
for (uint8_t f = 0; f < DEVICE_CLS_ASSIGN_LIST_LEN; f++) {
bool clear = false;
if (c > 0 && !config.Mqtt_Hass_IndividualPanels) {
clear = true;
}
publishField(inv, c, deviceFieldAssignment[f], clear);
}
}

yield();
}
}

void MqttHassPublishingClass::publishField(std::shared_ptr<InverterAbstract> inv, uint8_t channel, byteAssign_fieldDeviceClass_t fieldType, bool clear)
{
if (!inv->Statistics()->hasChannelFieldValue(channel, fieldType.fieldId)) {
return;
}

char serial[sizeof(uint64_t) * 8 + 1];
sprintf(serial, "%0lx%08lx",
((uint32_t)((inv->serial() >> 32) & 0xFFFFFFFF)),
((uint32_t)(inv->serial() & 0xFFFFFFFF)));

String fieldName;
if (channel == CH0 && fieldType.fieldId == FLD_PDC) {
fieldName = "PowerDC";
} else {
fieldName = inv->Statistics()->getChannelFieldName(channel, fieldType.fieldId);
}

String configTopic = "sensor/dtu_" + String(serial)
+ "/" + "ch" + String(channel) + "_" + fieldName
+ "/config";

if (!clear) {
String stateTopic = MqttSettings.getPrefix() + MqttPublishing.getTopic(inv, channel, fieldType.fieldId);
const char* devCls = deviceClasses[fieldType.deviceClsId];
const char* stateCls = stateClasses[fieldType.stateClsId];

String name;
if (channel == CH0) {
name = String(inv->name()) + " " + fieldName;
} else {
name = String(inv->name()) + " CH" + String(channel) + " " + fieldName;
}

DynamicJsonDocument deviceDoc(512);
deviceDoc[F("name")] = inv->name();
deviceDoc[F("ids")] = String(serial);
deviceDoc[F("cu")] = String(F("http://")) + String(WiFi.localIP().toString());
deviceDoc[F("mf")] = F("OpenDTU");
deviceDoc[F("mdl")] = inv->typeName();
deviceDoc[F("sw")] = AUTO_GIT_HASH;
JsonObject deviceObj = deviceDoc.as<JsonObject>();

DynamicJsonDocument root(1024);
root[F("name")] = name;
root[F("stat_t")] = stateTopic;
root[F("unit_of_meas")] = inv->Statistics()->getChannelFieldUnit(channel, fieldType.fieldId);
root[F("uniq_id")] = String(serial) + "_ch" + String(channel) + "_" + fieldName;
root[F("dev")] = deviceObj;
root[F("exp_aft")] = Configuration.get().Mqtt_PublishInterval * 2;
if (devCls != 0) {
root[F("dev_cla")] = devCls;
}
if (stateCls != 0) {
root[F("stat_cla")] = stateCls;
}

char buffer[512];
serializeJson(root, buffer);
MqttSettings.publishHass(configTopic, buffer);
}
else {
MqttSettings.publishHass(configTopic, "");
}
}
7 changes: 7 additions & 0 deletions src/MqttSettings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,13 @@ void MqttSettingsClass::publish(String subtopic, String payload)
mqttClient.publish(topic.c_str(), 0, Configuration.get().Mqtt_Retain, payload.c_str());
}

void MqttSettingsClass::publishHass(String subtopic, String payload)
{
String topic = Configuration.get().Mqtt_Hass_Topic;
topic += subtopic;
mqttClient.publish(topic.c_str(), 0, Configuration.get().Mqtt_Hass_Retain, payload.c_str());
}

void MqttSettingsClass::init()
{
using namespace std::placeholders;
Expand Down
25 changes: 24 additions & 1 deletion src/WebApi_mqtt.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include "ArduinoJson.h"
#include "AsyncJson.h"
#include "Configuration.h"
#include "MqttHassPublishing.h"
#include "MqttSettings.h"
#include "helper.h"

Expand Down Expand Up @@ -39,6 +40,10 @@ void WebApiMqttClass::onMqttStatus(AsyncWebServerRequest* request)
root[F("mqtt_retain")] = config.Mqtt_Retain;
root[F("mqtt_lwt_topic")] = String(config.Mqtt_Topic) + config.Mqtt_LwtTopic;
root[F("mqtt_publish_interval")] = config.Mqtt_PublishInterval;
root[F("mqtt_hass_enabled")] = config.Mqtt_Hass_Enabled;
root[F("mqtt_hass_retain")] = config.Mqtt_Hass_Retain;
root[F("mqtt_hass_topic")] = config.Mqtt_Hass_Topic;
root[F("mqtt_hass_individualpanels")] = config.Mqtt_Hass_IndividualPanels;

response->setLength();
request->send(response);
Expand All @@ -61,6 +66,10 @@ void WebApiMqttClass::onMqttAdminGet(AsyncWebServerRequest* request)
root[F("mqtt_lwt_online")] = config.Mqtt_LwtValue_Online;
root[F("mqtt_lwt_offline")] = config.Mqtt_LwtValue_Offline;
root[F("mqtt_publish_interval")] = config.Mqtt_PublishInterval;
root[F("mqtt_hass_enabled")] = config.Mqtt_Hass_Enabled;
root[F("mqtt_hass_retain")] = config.Mqtt_Hass_Retain;
root[F("mqtt_hass_topic")] = config.Mqtt_Hass_Topic;
root[F("mqtt_hass_individualpanels")] = config.Mqtt_Hass_IndividualPanels;

response->setLength();
request->send(response);
Expand Down Expand Up @@ -98,7 +107,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
return;
}

if (!(root.containsKey("mqtt_enabled") && root.containsKey("mqtt_hostname") && root.containsKey("mqtt_port") && root.containsKey("mqtt_username") && root.containsKey("mqtt_password") && root.containsKey("mqtt_topic") && root.containsKey("mqtt_retain") && root.containsKey("mqtt_lwt_topic") && root.containsKey("mqtt_lwt_online") && root.containsKey("mqtt_lwt_offline") && root.containsKey("mqtt_publish_interval"))) {
if (!(root.containsKey("mqtt_enabled") && root.containsKey("mqtt_hostname") && root.containsKey("mqtt_port") && root.containsKey("mqtt_username") && root.containsKey("mqtt_password") && root.containsKey("mqtt_topic") && root.containsKey("mqtt_retain") && root.containsKey("mqtt_lwt_topic") && root.containsKey("mqtt_lwt_online") && root.containsKey("mqtt_lwt_offline") && root.containsKey("mqtt_publish_interval") && root.containsKey("mqtt_hass_enabled") && root.containsKey("mqtt_hass_retain") && root.containsKey("mqtt_hass_topic") && root.containsKey("mqtt_hass_individualpanels"))) {
retMsg[F("message")] = F("Values are missing!");
response->setLength();
request->send(response);
Expand Down Expand Up @@ -166,6 +175,15 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
request->send(response);
return;
}

if (root[F("mqtt_hass_enabled")].as<bool>()) {
if (root[F("mqtt_hass_topic")].as<String>().length() > MQTT_MAX_TOPIC_STRLEN) {
retMsg[F("message")] = F("Hass topic must not longer then " STR(MQTT_MAX_TOPIC_STRLEN) " characters!");
response->setLength();
request->send(response);
return;
}
}
}

CONFIG_T& config = Configuration.get();
Expand All @@ -180,6 +198,10 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
strcpy(config.Mqtt_LwtValue_Online, root[F("mqtt_lwt_online")].as<String>().c_str());
strcpy(config.Mqtt_LwtValue_Offline, root[F("mqtt_lwt_offline")].as<String>().c_str());
config.Mqtt_PublishInterval = root[F("mqtt_publish_interval")].as<uint32_t>();
config.Mqtt_Hass_Enabled = root[F("mqtt_hass_enabled")].as<bool>();
config.Mqtt_Hass_Retain = root[F("mqtt_hass_retain")].as<bool>();
config.Mqtt_Hass_IndividualPanels = root[F("mqtt_hass_individualpanels")].as<bool>();
strcpy(config.Mqtt_Hass_Topic, root[F("mqtt_hass_topic")].as<String>().c_str());
Configuration.write();

retMsg[F("type")] = F("success");
Expand All @@ -189,4 +211,5 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
request->send(response);

MqttSettings.performReconnect();
MqttHassPublishing.publishConfig();
}
4 changes: 4 additions & 0 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/
#include "Configuration.h"
#include "Hoymiles.h"
#include "MqttHassPublishing.h"
#include "MqttPublishing.h"
#include "MqttSettings.h"
#include "NtpSettings.h"
Expand Down Expand Up @@ -67,6 +68,7 @@ void setup()
Serial.print(F("Initialize MqTT... "));
MqttSettings.init();
MqttPublishing.init();
MqttHassPublishing.init();
Serial.println(F("done"));

// Initialize WebApi
Expand Down Expand Up @@ -104,6 +106,8 @@ void loop()
yield();
MqttPublishing.loop();
yield();
MqttHassPublishing.loop();
yield();
WebApi.loop();
yield();
}
Loading

0 comments on commit 1124a9a

Please sign in to comment.