mirror of
https://github.com/UtilitechAS/amsreader-firmware.git
synced 2026-01-13 15:37:03 +00:00
Merge branch 'homeassistant' of github.com:kng/AmsToMqttBridge into pull-172
This commit is contained in:
commit
68906b54a6
@ -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) {
|
||||
|
||||
272
src/mqtt/HomeAssistantMqttHandler.cpp
Normal file
272
src/mqtt/HomeAssistantMqttHandler.cpp
Normal 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;
|
||||
}
|
||||
24
src/mqtt/HomeAssistantMqttHandler.h
Normal file
24
src/mqtt/HomeAssistantMqttHandler.h
Normal 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
|
||||
@ -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
3
web/ha1.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"P" : %d,
|
||||
}
|
||||
7
web/ha2.json
Normal file
7
web/ha2.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"tPI" : %.2f,
|
||||
"tPO" : %.2f,
|
||||
"tQI" : %.2f,
|
||||
"tQO" : %.2f,
|
||||
"rtc" : %llu
|
||||
}
|
||||
19
web/ha3.json
Normal file
19
web/ha3.json
Normal 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
17
web/hadiscover1.json
Normal 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
18
web/hadiscover2.json
Normal 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
24
web/jsonha.json
Normal 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
|
||||
}
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user