Merge branch 'homeassistant' of github.com:kng/AmsToMqttBridge into pull-172

This commit is contained in:
Gunnar Skjold 2022-02-12 11:14:58 +01:00
commit 68906b54a6
11 changed files with 390 additions and 1 deletions

View File

@ -37,6 +37,7 @@ ADC_MODE(ADC_VCC);
#include "mqtt/JsonMqttHandler.h"
#include "mqtt/RawMqttHandler.h"
#include "mqtt/DomoticzMqttHandler.h"
#include "mqtt/HomeAssistantMqttHandler.h"
#include "Uptime.h"
@ -1057,6 +1058,9 @@ void MQTT_connect() {
config.getDomoticzConfig(domo);
mqttHandler = new DomoticzMqttHandler(mqtt, domo);
break;
case 4:
mqttHandler = new HomeAssistantMqttHandler(mqtt, mqttConfig.clientId, mqttConfig.publishTopic, &hw);
break;
}
if(mqttConfig.ssl) {

View File

@ -0,0 +1,272 @@
#include "HomeAssistantMqttHandler.h"
#include "hexutils.h"
#include "Uptime.h"
#include "web/root/ha1_json.h"
#include "web/root/ha2_json.h"
#include "web/root/ha3_json.h"
#include "web/root/jsonsys_json.h"
#include "web/root/jsonprices_json.h"
#include "web/root/hadiscover1_json.h"
#include "web/root/hadiscover2_json.h"
bool HomeAssistantMqttHandler::publish(AmsData* data, AmsData* previousState) {
if(topic.isEmpty() || !mqtt->connected())
return false;
listType = data->getListType(); // for discovery stuff in publishSystem()
if(data->getListType() == 3) { // publish energy counts
char json[256];
snprintf_P(json, sizeof(json), HA2_JSON,
data->getActiveImportCounter(),
data->getActiveExportCounter(),
data->getReactiveImportCounter(),
data->getReactiveExportCounter(),
data->getMeterTimestamp()
);
mqtt->publish(topic + "/energy", json);
}
if(data->getListType() == 1) { // publish power counts
char json[192];
snprintf_P(json, sizeof(json), HA1_JSON,
data->getActiveImportPower()
);
return mqtt->publish(topic + "/power", json);
} else if(data->getListType() == 2 || data->getListType() == 3) { // publish power counts and volts/amps
char json[768];
snprintf_P(json, sizeof(json), HA3_JSON,
data->getListId().c_str(),
data->getMeterId().c_str(),
data->getMeterModel().c_str(),
data->getActiveImportPower(),
data->getReactiveImportPower(),
data->getActiveExportPower(),
data->getReactiveExportPower(),
data->getL1Current(),
data->getL2Current(),
data->getL3Current(),
data->getL1Voltage(),
data->getL2Voltage(),
data->getL3Voltage(),
data->getPowerFactor() == 0 ? 1 : data->getPowerFactor(),
data->getPowerFactor() == 0 ? 1 : data->getL1PowerFactor(),
data->getPowerFactor() == 0 ? 1 : data->getL2PowerFactor(),
data->getPowerFactor() == 0 ? 1 : data->getL3PowerFactor()
);
return mqtt->publish(topic + "/power", json);
}
return false;
}
bool HomeAssistantMqttHandler::publishTemperatures(AmsConfiguration* config, HwTools* hw) {
int count = hw->getTempSensorCount();
if(count == 0) return false;
int size = 32 + (count * 26);
char buf[size];
snprintf(buf, 24, "{\"temperatures\":{");
for(int i = 0; i < count; i++) {
TempSensorData* data = hw->getTempSensorData(i);
if(data != NULL) {
char* pos = buf+strlen(buf);
snprintf(pos, 26, "\"%s\":%.2f,",
toHex(data->address, 8).c_str(),
data->lastRead
);
data->changed = false;
delay(1);
}
}
char* pos = buf+strlen(buf);
snprintf(count == 0 ? pos : pos-1, 8, "}}");
return mqtt->publish(topic + "/temperatures", buf);
}
bool HomeAssistantMqttHandler::publishPrices(EntsoeApi* eapi) {
if(topic.isEmpty() || !mqtt->connected())
return false;
if(strlen(eapi->getToken()) == 0)
return false;
time_t now = time(nullptr);
float min1hr, min3hr, min6hr;
int8_t min1hrIdx = -1, min3hrIdx = -1, min6hrIdx = -1;
float min = INT16_MAX, max = INT16_MIN;
float values[24] = {0};
for(uint8_t i = 0; i < 24; i++) {
float val = eapi->getValueForHour(now, i);
values[i] = val;
if(val == ENTSOE_NO_VALUE) break;
if(val < min) min = val;
if(val > max) max = val;
if(min1hrIdx == -1 || min1hr > val) {
min1hr = val;
min1hrIdx = i;
}
if(i >= 2) {
i -= 2;
float val1 = values[i++];
float val2 = values[i++];
float val3 = val;
if(val1 == ENTSOE_NO_VALUE || val2 == ENTSOE_NO_VALUE || val3 == ENTSOE_NO_VALUE) continue;
float val3hr = val1+val2+val3;
if(min3hrIdx == -1 || min3hr > val3hr) {
min3hr = val3hr;
min3hrIdx = i-2;
}
}
if(i >= 5) {
i -= 5;
float val1 = values[i++];
float val2 = values[i++];
float val3 = values[i++];
float val4 = values[i++];
float val5 = values[i++];
float val6 = val;
if(val1 == ENTSOE_NO_VALUE || val2 == ENTSOE_NO_VALUE || val3 == ENTSOE_NO_VALUE || val4 == ENTSOE_NO_VALUE || val5 == ENTSOE_NO_VALUE || val6 == ENTSOE_NO_VALUE) continue;
float val6hr = val1+val2+val3+val4+val5+val6;
if(min6hrIdx == -1 || min6hr > val6hr) {
min6hr = val6hr;
min6hrIdx = i-5;
}
}
}
char ts1hr[21];
if(min1hrIdx > -1) {
time_t ts = now + (SECS_PER_HOUR * min1hrIdx);
//Serial.printf("1hr: %d %lu\n", min1hrIdx, ts);
tmElements_t tm;
breakTime(ts, tm);
sprintf(ts1hr, "%04d-%02d-%02dT%02d:00:00Z", tm.Year+1970, tm.Month, tm.Day, tm.Hour);
}
char ts3hr[21];
if(min3hrIdx > -1) {
time_t ts = now + (SECS_PER_HOUR * min3hrIdx);
//Serial.printf("3hr: %d %lu\n", min3hrIdx, ts);
tmElements_t tm;
breakTime(ts, tm);
sprintf(ts3hr, "%04d-%02d-%02dT%02d:00:00Z", tm.Year+1970, tm.Month, tm.Day, tm.Hour);
}
char ts6hr[21];
if(min6hrIdx > -1) {
time_t ts = now + (SECS_PER_HOUR * min6hrIdx);
//Serial.printf("6hr: %d %lu\n", min6hrIdx, ts);
tmElements_t tm;
breakTime(ts, tm);
sprintf(ts6hr, "%04d-%02d-%02dT%02d:00:00Z", tm.Year+1970, tm.Month, tm.Day, tm.Hour);
}
char json[384];
snprintf_P(json, sizeof(json), JSONPRICES_JSON,
WiFi.macAddress().c_str(),
values[0],
values[1],
values[2],
values[3],
values[4],
values[5],
values[6],
values[7],
values[8],
values[9],
values[10],
values[11],
min == INT16_MAX ? 0.0 : min,
max == INT16_MIN ? 0.0 : max,
ts1hr,
ts3hr,
ts6hr
);
return mqtt->publish(topic + "/prices", json);
}
bool HomeAssistantMqttHandler::publishSystem(HwTools* hw) {
if(topic.isEmpty() || !mqtt->connected()){
sequence = 0;
return false;
}
if(sequence % 3 == 0){
char json[192];
snprintf_P(json, sizeof(json), JSONSYS_JSON,
WiFi.macAddress().c_str(),
clientId.c_str(),
(uint32_t) (millis64()/1000),
hw->getVcc(),
hw->getWifiRssi(),
hw->getTemperature()
);
mqtt->publish(topic + "/state", json);
}
if(sequence % 60 == 1 && listType > 1){ // every 60 ams message, publish mqtt discovery. TODO: publish once with retain
char json[512];
String haTopic = "homeassistant/sensor/"; // home-assistant discovery topic
String haUID = "ams-3a08"; // unit identity (wifi hostname)
int sensors = 17;
String topics[sensors] = {"/state", "/state", "/state", "/power", "/power", "/power", "/power", "/power", "/power", "/power", "/power", "/power", "/power", "/energy", "/energy", "/energy", "/energy"};
String names[sensors] = {"Status", "Supply volt", "Temperature", "Active import", "Reactive import", "Active export", "Reactive export", "L1 current", "L2 current", "L3 current",
"L1 voltage", "L2 voltage", "L3 voltage", "Accumulated active import", "Accumulated active export", "Accumulated reactive import", "Accumulated reactive export"};
String params[sensors] = {"rssi", "vcc", "temp", "P", "Q", "PO", "QO", "I1", "I2", "I3", "U1", "U2", "U3", "tPI", "tPO", "tQI", "tQO"};
String uom[sensors] = {"dBm", "V", "C", "W", "W", "W", "W", "A", "A", "A", "V", "V", "V", "kWh", "kWh", "kWh", "kWh"};
String devcl[sensors] = {"signal_strength", "voltage", "temperature", "power", "power", "power", "power", "current", "current", "current", "voltage", "voltage", "voltage", "energy", "energy", "energy", "energy"};
String stacl[sensors] = {"", "", "", "measurement", "measurement", "measurement", "measurement", "", "", "", "", "", "", "total_increasing", "total_increasing", "total_increasing", "total_increasing"};
//String category[sensors] = {"Diagnostic", "Diagnostic", "Diagnostic", "Power", "Power", "Power", "Power", "Voltage", "Voltage", "Voltage", "Current", "Current", "Current", "Energy", "Energy", "Energy", "Energy"};
String haName = "AMS reader";
String haModel = "ESP32";
String haVersion = "2.0.0";
String haManuf = "AmsToMqttBridge";
String haUrl = "http://" + haUID + ".local/";
for(int i=0;i<sensors;i++){
if(stacl[i].length() > 0) { // TODO: reduce to single JSON, state_class: null (witout quotation). or make it some extra optional string that us appended
snprintf_P(json, sizeof(json), HADISCOVER2_JSON,
names[i].c_str(), // name
(topic + topics[i]).c_str(), // state_topic
(haUID + "_" + params[i]).c_str(), // unique_id
(haUID + "_" + params[i]).c_str(), // object_id
uom[i].c_str(), // unit_of_measurement
params[i].c_str(), // value_template
devcl[i].c_str(), // device_class
haUID.c_str(), // dev ids
haName.c_str(), // name
haModel.c_str(), // model
haVersion.c_str(), // fw version
haManuf.c_str(), // manufacturer
haUrl.c_str(), // configuration_url
stacl[i].c_str() // state_class
);
} else {
snprintf_P(json, sizeof(json), HADISCOVER1_JSON,
names[i].c_str(), // name
(topic + topics[i]).c_str(), // state_topic
(haUID + "_" + params[i]).c_str(), // unique_id
(haUID + "_" + params[i]).c_str(), // object_id
uom[i].c_str(), // unit_of_measurement
params[i].c_str(), // value_template
devcl[i].c_str(), // device_class
haUID.c_str(), // dev ids
haName.c_str(), // name
haModel.c_str(), // model
haVersion.c_str(), // fw version
haManuf.c_str(), // manufacturer
haUrl.c_str() // configuration_url
);
}
mqtt->publish(haTopic + haUID + "_" + params[i] + "/config", json);
}
}
if(listType>0) sequence++;
return true;
}

View File

@ -0,0 +1,24 @@
#ifndef _HOMEASSISTANTMQTTHANDLER_H
#define _HOMEASSISTANTMQTTHANDLER_H
#include "AmsMqttHandler.h"
class HomeAssistantMqttHandler : public AmsMqttHandler {
public:
HomeAssistantMqttHandler(MQTTClient* mqtt, const char* clientId, const char* topic, HwTools* hw) : AmsMqttHandler(mqtt) {
this->clientId = clientId;
this->topic = String(topic);
this->hw = hw;
};
bool publish(AmsData* data, AmsData* previousState);
bool publishTemperatures(AmsConfiguration*, HwTools*);
bool publishPrices(EntsoeApi*);
bool publishSystem(HwTools*);
private:
String clientId;
String topic;
HwTools* hw;
uint8_t sequence = 0, listType = 0;
};
#endif

View File

@ -547,7 +547,7 @@ void AmsWebServer::configMqttHtml() {
html.replace("{u}", mqtt.username);
html.replace("{pw}", mqtt.password);
html.replace("{f}", String(mqtt.payloadFormat));
for(int i = 0; i<4; i++) {
for(int i = 0; i<5; i++) {
html.replace("{f" + String(i) + "}", mqtt.payloadFormat == i ? "selected" : "");
}

3
web/ha1.json Normal file
View File

@ -0,0 +1,3 @@
{
"P" : %d,
}

7
web/ha2.json Normal file
View File

@ -0,0 +1,7 @@
{
"tPI" : %.2f,
"tPO" : %.2f,
"tQI" : %.2f,
"tQO" : %.2f,
"rtc" : %llu
}

19
web/ha3.json Normal file
View File

@ -0,0 +1,19 @@
{
"lv" : "%s",
"id" : "%s",
"type" : "%s",
"P" : %d,
"Q" : %d,
"PO" : %d,
"QO" : %d,
"I1" : %.2f,
"I2" : %.2f,
"I3" : %.2f,
"U1" : %.2f,
"U2" : %.2f,
"U3" : %.2f,
"PF" : %.2f,
"PF1" : %.2f,
"PF2" : %.2f,
"PF3" : %.2f
}

17
web/hadiscover1.json Normal file
View File

@ -0,0 +1,17 @@
{
"name" : "%s",
"stat_t" : "%s",
"uniq_id" : "%s",
"obj_id" : "%s",
"unit_of_meas" : "%s",
"val_tpl" : "{{value_json['%s']}}",
"dev_cla" : "%s",
"dev" : {
"ids" : [ "%s" ],
"name" : "%s",
"mdl" : "%s",
"sw" : "%s",
"mf" : "%s",
"cu" : "%s"
}
}

18
web/hadiscover2.json Normal file
View File

@ -0,0 +1,18 @@
{
"name" : "%s",
"stat_t" : "%s",
"uniq_id" : "%s",
"obj_id" : "%s",
"unit_of_meas" : "%s",
"val_tpl" : "{{value_json['%s']}}",
"dev_cla" : "%s",
"dev" : {
"ids" : [ "%s" ],
"name" : "%s",
"mdl" : "%s",
"sw" : "%s",
"mf" : "%s",
"cu" : "%s"
},
"stat_cla" : "%s"
}

24
web/jsonha.json Normal file
View File

@ -0,0 +1,24 @@
{
"lv" : "%s",
"id" : "%s",
"type" : "%s",
"P" : %d,
"Q" : %d,
"PO" : %d,
"QO" : %d,
"I1" : %.2f,
"I2" : %.2f,
"I3" : %.2f,
"U1" : %.2f,
"U2" : %.2f,
"U3" : %.2f,
"PF" : %.2f,
"PF1" : %.2f,
"PF2" : %.2f,
"PF3" : %.2f,
"tPI" : %.2f,
"tPO" : %.2f,
"tQI" : %.2f,
"tQO" : %.2f,
"rtc" : %lu
}

View File

@ -62,6 +62,7 @@
<option value="1" {f1}>Raw (minimal)</option>
<option value="2" {f2}>Raw (full)</option>
<option value="3" {f3}>Domoticz</option>
<option value="4" {f4}>Home-Assistant</option>
</select>
</div>
</div>