Implemented first version of Home Assistant Auto Discovery

This commit is contained in:
Thomas Basler 2022-07-18 22:28:03 +02:00
parent dcc7e47b19
commit 1124a9aaa1
13 changed files with 360 additions and 10 deletions

View File

@ -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
@ -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 {

View File

@ -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;

View File

@ -11,9 +11,10 @@ public:
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;

View File

@ -14,6 +14,7 @@ public:
void performReconnect();
bool getConnected();
void publish(String subtopic, String payload);
void publishHass(String subtopic, String payload);
String getPrefix();

View File

@ -37,3 +37,8 @@
#define DTU_SERIAL 0x99978563412
#define DTU_POLL_INTERVAL 5
#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

View File

@ -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()
@ -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();
}

124
src/MqttHassPublishing.cpp Normal file
View File

@ -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, "");
}
}

View File

@ -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;

View File

@ -6,6 +6,7 @@
#include "ArduinoJson.h"
#include "AsyncJson.h"
#include "Configuration.h"
#include "MqttHassPublishing.h"
#include "MqttSettings.h"
#include "helper.h"
@ -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);
@ -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);
@ -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);
@ -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();
@ -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");
@ -189,4 +211,5 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
request->send(response);
MqttSettings.performReconnect();
MqttHassPublishing.publishConfig();
}

View File

@ -4,6 +4,7 @@
*/
#include "Configuration.h"
#include "Hoymiles.h"
#include "MqttHassPublishing.h"
#include "MqttPublishing.h"
#include "MqttSettings.h"
#include "NtpSettings.h"
@ -67,6 +68,7 @@ void setup()
Serial.print(F("Initialize MqTT... "));
MqttSettings.init();
MqttPublishing.init();
MqttHassPublishing.init();
Serial.println(F("done"));
// Initialize WebApi
@ -104,6 +106,8 @@ void loop()
yield();
MqttPublishing.loop();
yield();
MqttHassPublishing.loop();
yield();
WebApi.loop();
yield();
}

View File

@ -18,10 +18,25 @@
<div class="card">
<div class="card-header text-white bg-primary">MqTT Configuration</div>
<div class="card-body">
<div class="row mb-3">
<label class="col-sm-4 form-check-label" for="inputMqtt">Enable MqTT</label>
<div class="col-sm-8">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="inputMqtt"
v-model="mqttConfigList.mqtt_enabled" />
<label class="form-check-label" for="inputMqtt">Enable MqTT</label>
</div>
</div>
</div>
<div class="row mb-3" v-show="mqttConfigList.mqtt_enabled">
<label class="col-sm-4 form-check-label" for="inputMqttHass">Enable Home Assistant MQTT Auto
Discovery</label>
<div class="col-sm-8">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="inputMqttHass"
v-model="mqttConfigList.mqtt_hass_enabled" />
</div>
</div>
</div>
</div>
</div>
@ -135,6 +150,42 @@
</div>
</div>
</div>
<div class="card mt-5" v-show="mqttConfigList.mqtt_enabled && mqttConfigList.mqtt_hass_enabled">
<div class="card-header text-white bg-primary">Home Assistant MQTT Auto Discovery Parameters</div>
<div class="card-body">
<div class="row mb-3">
<label for="inputHassTopic" class="col-sm-2 col-form-label">Prefix Topic:</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="inputHassTopic" maxlength="32"
placeholder="The prefix for the discovery topic"
v-model="mqttConfigList.mqtt_hass_topic" />
</div>
</div>
<div class="row mb-3">
<label class="col-sm-2 form-check-label" for="inputHassRetain">Enable Retain Flag</label>
<div class="col-sm-10">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="inputHassRetain"
v-model="mqttConfigList.mqtt_hass_retain" />
</div>
</div>
</div>
<div class="row mb-3">
<label class="col-sm-2 form-check-label" for="inputIndividualPanels">Individual Panels:</label>
<div class="col-sm-10">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="inputIndividualPanels"
v-model="mqttConfigList.mqtt_hass_individualpanels" />
</div>
</div>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary mb-3">Save</button>
</form>
</template>
@ -163,7 +214,11 @@ export default defineComponent({
mqtt_retain: false,
mqtt_lwt_topic: "",
mqtt_lwt_online: "",
mqtt_lwt_offline: ""
mqtt_lwt_offline: "",
mqtt_hass_enabled: false,
mqtt_hass_retain: false,
mqtt_hass_topic: "",
mqtt_hass_individualpanels: false
},
alertMessage: "",
alertType: "info",

View File

@ -63,6 +63,52 @@
</div>
</div>
<div class="card mt-5">
<div class="card-header text-white bg-primary">Home Assistant MQTT Auto Discovery Configuration Summary</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover table-condensed">
<tbody>
<tr>
<th>Status</th>
<td class="badge" :class="{
'bg-danger': !mqttDataList.mqtt_hass_enabled,
'bg-success': mqttDataList.mqtt_hass_enabled,
}">
<span v-if="mqttDataList.mqtt_hass_enabled">enabled</span>
<span v-else>disabled</span>
</td>
</tr>
<tr>
<th>Base Topic</th>
<td>{{ mqttDataList.mqtt_hass_topic }}</td>
</tr>
<tr>
<th>Retain</th>
<td class="badge" :class="{
'bg-danger': !mqttDataList.mqtt_hass_retain,
'bg-success': mqttDataList.mqtt_hass_retain,
}">
<span v-if="mqttDataList.mqtt_hass_retain">enabled</span>
<span v-else>disabled</span>
</td>
</tr>
<tr>
<th>Individual Panels</th>
<td class="badge" :class="{
'bg-danger': !mqttDataList.mqtt_hass_individualpanels,
'bg-success': mqttDataList.mqtt_hass_individualpanels,
}">
<span v-if="mqttDataList.mqtt_hass_individualpanels">enabled</span>
<span v-else>disabled</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="card mt-5">
<div class="card-header text-white bg-primary">Runtime Summary</div>
<div class="card-body">
@ -103,7 +149,11 @@ export default defineComponent({
mqtt_topic: "",
mqtt_publish_interval: 0,
mqtt_retain: false,
mqtt_connected: false
mqtt_connected: false,
mqtt_hass_enabled: false,
mqtt_hass_retain: false,
mqtt_hass_topic: "",
mqtt_hass_individualpanels: false
},
};
},

Binary file not shown.