Add energy sensors to MQTT + HA discovery for active power, reactive power, power factor (#1102)

* HASS discovery energy stats fixed except energycounter_clear_date

* HASS: add friendly entity names, remove timestamp class from energycounter_clear_date as workaround for hass-incompatible date format

* HA energycounter_clear_date fixed for correct interpreting as home assistant timestamp sensor

* refactor HA power sensors discovery info

* refactor HA power sensors discovery more

* add apparent power, reactive power, power factor to mqtt + hass discovery, refactor some vars into new energy_sensors[] struct

* amend hass sensor unique_ids due to mqtt topic/channel too long; 'Error:MQTT:Unable to queue! Topic (13), channel (66) or value (437) exceeds size limit'

* hass sensors: add 'energy 2 days ago', 'energy 3 days ago', 'uptime'
web UI: energy sensors apply their rounding setting
drv_bl_shared.c: add enum for daily_stats[], put rearrange energy_sensor[] struct to expose only names via DRV_GetEnergySensorNames()

* -HA energy sensor uniq_id values made consistent with prior builds via .hass_uniq_id_suffix
-Refactor drv_bl_shared sensor/counter vars into energy_sensors[] to simplify mqtt transmissions etc
-Add energy '2 days ago'/'3 days ago' to main web ui, data from vars already being saved to/from flash
-NTP fix html formatting in web ui

* -HA energy sensor uniq_id values made consistent with prior builds via .hass_uniq_id_suffix
-Refactor drv_bl_shared sensor/counter vars into energy_sensors[] to simplify mqtt transmissions etc
-Add energy '2 days ago'/'3 days ago' to main web ui, data from vars already being saved to/from flash
-NTP fix html formatting in web ui

* Update settings.json

ignore vscode settings...

* Update settings.json

* Update settings.json

* minor fix

* fix OBK_CONSUMPTION_LAST_HOUR missing from mqtt

---------

Co-authored-by: Stefan Smith <stefan064>
This commit is contained in:
stefan064
2024-03-02 17:55:01 +11:00
committed by GitHub
parent d4ec659cc0
commit 29cc2cc323
10 changed files with 320 additions and 464 deletions

View File

@ -50,7 +50,11 @@ void hass_populate_unique_id(ENTITY_TYPE type, int index, char* uniq_id) {
sprintf(uniq_id, "%s_%s_%d", longDeviceName, "relay", index);
break;
case VCP_SENSOR:
case ENERGY_METER_SENSOR:
#ifndef OBK_DISABLE_ALL_DRIVERS
sprintf(uniq_id, "%s_sensor_%s", longDeviceName, DRV_GetEnergySensorNames(index)->hass_uniq_id_suffix);
#endif
break;
case POWER_SENSOR:
case ENERGY_SENSOR:
case TIMESTAMP_SENSOR:
@ -94,6 +98,9 @@ void hass_populate_unique_id(ENTITY_TYPE type, int index, char* uniq_id) {
break;
case HASS_RSSI:
sprintf(uniq_id, "%s_rssi", longDeviceName);
break;
case HASS_UPTIME:
sprintf(uniq_id, "%s_uptime", longDeviceName);
break;
default:
// TODO: USE type here as well?
@ -141,7 +148,7 @@ void hass_populate_device_config_channel(ENTITY_TYPE type, char* uniq_id, HassDe
case CO2_SENSOR:
case TVOC_SENSOR:
case POWER_SENSOR:
case VCP_SENSOR:
case ENERGY_METER_SENSOR:
case ENERGY_SENSOR:
case TIMESTAMP_SENSOR:
case BATTERY_SENSOR:
@ -182,7 +189,7 @@ cJSON* hass_build_device_node(cJSON* ids) {
/// @brief Initializes HomeAssistant device discovery storage with common values.
/// @param type
/// @param index This is used to generate generate unique_id and name.
/// It is ignored for RGB. For power sensors, index corresponds to sensor_mqttNames. For regular sensor, index can be be the channel.
/// It is ignored for RGB. For energy sensors, index corresponds to energySensor_t. For regular sensor, index can be be the channel.
/// @param payload_on The payload that represents enabled state. This is not added for POWER_SENSOR.
/// @param payload_off The payload that represents disabled state. This is not added for POWER_SENSOR.
/// @return
@ -218,20 +225,19 @@ HassDeviceInfo* hass_init_device_info(ENTITY_TYPE type, int index, const char* p
//for 2 PWM case.
sprintf(g_hassBuffer, "Light");
break;
case VCP_SENSOR:
case ENERGY_METER_SENSOR:
isSensor = true;
#ifndef OBK_DISABLE_ALL_DRIVERS
if ((index >= OBK_VOLTAGE) && (index <= OBK_POWER))
sprintf(g_hassBuffer, "%s", sensor_hassNames[index]);
if (index <= OBK__LAST)
sprintf(g_hassBuffer, "%s", DRV_GetEnergySensorNames(index)->name_friendly);
else
sprintf(g_hassBuffer, "Voltage or Current or Power");
sprintf(g_hassBuffer, "Unknown Energy Meter Sensor");
#endif
break;
case POWER_SENSOR:
isSensor = true;
sprintf(g_hassBuffer, "Power");
break;
case TEMPERATURE_SENSOR:
isSensor = true;
sprintf(g_hassBuffer, "Temperature");
@ -271,22 +277,15 @@ HassDeviceInfo* hass_init_device_info(ENTITY_TYPE type, int index, const char* p
case HASS_RSSI:
sprintf(g_hassBuffer, "RSSI");
break;
case HASS_UPTIME:
sprintf(g_hassBuffer, "Uptime");
break;
case ENERGY_SENSOR:
isSensor = true;
#ifndef OBK_DISABLE_ALL_DRIVERS
if ((index >= OBK_CONSUMPTION_TOTAL) && (index <= OBK_CONSUMPTION_TODAY))
sprintf(g_hassBuffer, "%s", sensor_hassNames[index]);
else
sprintf(g_hassBuffer, "Energy");
#endif
sprintf(g_hassBuffer, "Energy");
break;
case TIMESTAMP_SENSOR:
#ifndef OBK_DISABLE_ALL_DRIVERS
if (index == OBK_CONSUMPTION_CLEAR_DATE)
sprintf(g_hassBuffer, "%s", sensor_hassNames[index]);
else
sprintf(g_hassBuffer, "Timestamp");
#endif
sprintf(g_hassBuffer, "Timestamp");
break;
default:
sprintf(g_hassBuffer, "%s", CHANNEL_GetLabel(index));
@ -430,48 +429,35 @@ HassDeviceInfo* hass_init_binary_sensor_device_info(int index, bool bInverse) {
#ifndef OBK_DISABLE_ALL_DRIVERS
/// @brief Initializes HomeAssistant power sensor device discovery storage.
/// @param index Index corresponding to sensor_mqttNames.
/// @param index Index corresponding to energySensor_t.
/// @return
HassDeviceInfo* hass_init_power_sensor_device_info(int index) {
HassDeviceInfo* hass_init_energy_sensor_device_info(int index) {
HassDeviceInfo* info = 0;
//https://developers.home-assistant.io/docs/core/entity/sensor/#available-device-classes
//device_class automatically assigns unit,icon
if ((index >= OBK_VOLTAGE) && (index <= OBK_POWER))
{
info = hass_init_device_info(VCP_SENSOR, index, NULL, NULL);
cJSON_AddStringToObject(info->root, "dev_cla", sensor_mqtt_device_classes[index]); //device_class=voltage,current,power
cJSON_AddStringToObject(info->root, "unit_of_meas", sensor_mqtt_device_units[index]); //unit_of_measurement
if (index > OBK__LAST) return info;
if (index >= OBK_CONSUMPTION__DAILY_FIRST && !DRV_IsRunning("NTP")) return info; //include daily stats only when time is valid
sprintf(g_hassBuffer, "~/%s/get", sensor_mqttNames[index]);
cJSON_AddStringToObject(info->root, "stat_t", g_hassBuffer);
info = hass_init_device_info(ENERGY_METER_SENSOR, index, NULL, NULL);
cJSON_AddStringToObject(info->root, "dev_cla", DRV_GetEnergySensorNames(index)->hass_dev_class); //device_class=voltage,current,power, energy, timestamp
cJSON_AddStringToObject(info->root, "unit_of_meas", DRV_GetEnergySensorNames(index)->units); //unit_of_measurement. Sets as empty string if not present. HA doesn't seem to mind
sprintf(g_hassBuffer, "~/%s/get", DRV_GetEnergySensorNames(index)->name_mqtt);
cJSON_AddStringToObject(info->root, "stat_t", g_hassBuffer);
if (!strcmp(DRV_GetEnergySensorNames(index)->hass_dev_class, "energy")) {
//state_class can be measurement, total or total_increasing. Energy values should be total_increasing.
cJSON_AddStringToObject(info->root, "stat_cla", "total_increasing");
cJSON_AddStringToObject(info->root, "unit_of_meas", CFG_HasFlag(OBK_FLAG_MQTT_ENERGY_IN_KWH) ? "kWh" : "Wh");
} else {
cJSON_AddStringToObject(info->root, "stat_cla", "measurement");
cJSON_AddStringToObject(info->root, "unit_of_meas", DRV_GetEnergySensorNames(index)->units);
}
else if ((index >= OBK_CONSUMPTION_TOTAL) && (index <= OBK_NUM_EMUNS_MAX))
{
if (index >= OBK_CONSUMPTION_YESTERDAY && !DRV_IsRunning("NTP")) return info; //include daily stats only when time is valid
info = hass_init_device_info(index == OBK_CONSUMPTION_CLEAR_DATE ? TIMESTAMP_SENSOR : ENERGY_SENSOR, index, NULL, NULL);
const char* device_class_value = counter_devClasses[index - OBK_CONSUMPTION_TOTAL];
if (strlen(device_class_value) > 0) {
cJSON_AddStringToObject(info->root, "dev_cla", device_class_value); //device_class=energy,timestamp
if (!strcmp(device_class_value, "energy")) {
if (CFG_HasFlag(OBK_FLAG_MQTT_ENERGY_IN_KWH)) {
cJSON_AddStringToObject(info->root, "unit_of_meas", "kWh"); //unit_of_measurement
}
else {
cJSON_AddStringToObject(info->root, "unit_of_meas", "Wh"); //unit_of_measurement
}
//state_class can be measurement, total or total_increasing. Energy values should be total_increasing.
cJSON_AddStringToObject(info->root, "stat_cla", "total_increasing");
}
}
sprintf(g_hassBuffer, "~/%s/get", counter_mqttNames[index - OBK_CONSUMPTION_TOTAL]);
cJSON_AddStringToObject(info->root, "stat_t", g_hassBuffer);
}
// if (index == OBK_CONSUMPTION_STATS) { //hide this as its not working anyway at present
// cJSON_AddStringToObject(info->root, "enabled_by_default ", "false");
// }
return info;
}
@ -657,8 +643,15 @@ HassDeviceInfo* hass_init_sensor_device_info(ENTITY_TYPE type, int channel, int
cJSON_AddStringToObject(info->root, "dev_cla", "signal_strength");
cJSON_AddStringToObject(info->root, "stat_t", "~/rssi");
cJSON_AddStringToObject(info->root, "unit_of_meas", "dBm");
cJSON_AddStringToObject(info->root, "entity_category", "diagnostic");
//cJSON_AddStringToObject(info->root, "icon_template", "mdi:access-point");
break;
case HASS_UPTIME:
cJSON_AddStringToObject(info->root, "dev_cla", "duration");
cJSON_AddStringToObject(info->root, "stat_t", "~/uptime");
cJSON_AddStringToObject(info->root, "unit_of_meas", "s");
cJSON_AddStringToObject(info->root, "entity_category", "diagnostic");
cJSON_AddStringToObject(info->root, "stat_cla", "total_increasing");
break;
default:
sprintf(g_hassBuffer, "~/%d/get", channel);
@ -666,7 +659,7 @@ HassDeviceInfo* hass_init_sensor_device_info(ENTITY_TYPE type, int channel, int
return NULL;
}
if (type != READONLYLOWMIDHIGH_SENSOR && type != ENERGY_SENSOR && type != HASS_RSSI) {
if (type != READONLYLOWMIDHIGH_SENSOR && !cJSON_HasObjectItem(info->root, "stat_cla")) {
cJSON_AddStringToObject(info->root, "stat_cla", "measurement");
}

View File

@ -25,7 +25,7 @@ typedef enum {
LIGHT_RGBCW,
/// @brief Power sensors (voltage, current, power)
VCP_SENSOR,
ENERGY_METER_SENSOR,
POWER_SENSOR,
@ -65,8 +65,10 @@ typedef enum {
READONLYLOWMIDHIGH_SENSOR,
// lx unit
ILLUMINANCE_SENSOR,
// dBm unit
/// @brief dBm unit
HASS_RSSI,
/// @brief Time firmware is alive in secs
HASS_UPTIME,
/// @brief Wh, kWh
ENERGY_SENSOR,
// hPa
@ -102,7 +104,7 @@ void hass_print_unique_id(http_request_t* request, const char* fmt, ENTITY_TYPE
HassDeviceInfo* hass_init_relay_device_info(int index, ENTITY_TYPE type, bool bInverse);
HassDeviceInfo* hass_init_device_info(ENTITY_TYPE type, int index, const char* payload_on, const char* payload_off);
HassDeviceInfo* hass_init_light_device_info(ENTITY_TYPE type);
HassDeviceInfo* hass_init_power_sensor_device_info(int index);
HassDeviceInfo* hass_init_energy_sensor_device_info(int index);
HassDeviceInfo* hass_init_light_singleColor_onChannels(int toggle, int dimmer, int brightness_scale);
HassDeviceInfo* hass_init_binary_sensor_device_info(int index, bool bInverse);
HassDeviceInfo* hass_init_sensor_device_info(ENTITY_TYPE type, int channel, int decPlaces, int decOffset, int divider);

View File

@ -1819,9 +1819,9 @@ void doHomeAssistantDiscovery(const char* topic, http_request_t* request) {
#ifndef OBK_DISABLE_ALL_DRIVERS
if (measuringPower == true) {
for (i = 0; i < OBK_NUM_SENSOR_COUNT; i++)
for (i = OBK__FIRST; i < OBK__LAST; i++)
{
dev_info = hass_init_power_sensor_device_info(i);
dev_info = hass_init_energy_sensor_device_info(i);
if (dev_info) {
MQTT_QueuePublish(topic, dev_info->channel, hass_build_discovery_json(dev_info), OBK_PUBLISH_FLAG_RETAIN);
hass_free_device_info(dev_info);
@ -2130,7 +2130,9 @@ void doHomeAssistantDiscovery(const char* topic, http_request_t* request) {
dev_info = hass_init_sensor_device_info(HASS_RSSI, 0, -1, -1, 1);
MQTT_QueuePublish(topic, dev_info->channel, hass_build_discovery_json(dev_info), OBK_PUBLISH_FLAG_RETAIN);
hass_free_device_info(dev_info);
dev_info = hass_init_sensor_device_info(HASS_UPTIME, 0, -1, -1, 1);
MQTT_QueuePublish(topic, dev_info->channel, hass_build_discovery_json(dev_info), OBK_PUBLISH_FLAG_RETAIN);
hass_free_device_info(dev_info);
discoveryQueued = true;
}

View File

@ -193,17 +193,14 @@ static int http_tasmota_json_power(void* request, jsonCb_t printer) {
{"StatusSNS":{"Time":"2022-07-30T10:11:26","ENERGY":{"TotalStartTime":"2022-05-12T10:56:31","Total":0.003,"Yesterday":0.003,"Today":0.000,"Power": 0,"ApparentPower": 0,"ReactivePower": 0,"Factor":0.00,"Voltage":236,"Current":0.000}}}
*/
// returns NaN values as 0
static float _getReading_NanToZero(energySensor_t type) {
float retval = DRV_GetReading(type);
return OBK_IS_NAN(retval) ? 0 : retval;
}
static int http_tasmota_json_ENERGY(void* request, jsonCb_t printer) {
float power, voltage, current, batterypercentage = 0;
float energy, energy_hour, energy_yesterday;
voltage = DRV_GetReading(OBK_VOLTAGE);
current = DRV_GetReading(OBK_CURRENT);
power = DRV_GetReading(OBK_POWER);
energy = DRV_GetReading(OBK_CONSUMPTION_TOTAL);
energy_hour = DRV_GetReading(OBK_CONSUMPTION_LAST_HOUR);
energy_yesterday = DRV_GetReading(OBK_CONSUMPTION_YESTERDAY);
float voltage, batterypercentage = 0;
if (DRV_IsMeasuringBattery()) {
#ifdef ENABLE_DRIVER_BATTERY
@ -211,33 +208,22 @@ static int http_tasmota_json_ENERGY(void* request, jsonCb_t printer) {
batterypercentage = Battery_lastreading(OBK_BATT_LEVEL);
#endif
printer(request, "{");
printer(request, "\"Voltage\":%.4f,", voltage);
printer(request, "\"Voltage\":%.4f,", _getReading_NanToZero(OBK_VOLTAGE));
printer(request, "\"Batterypercentage\":%.0f", batterypercentage);
// close ENERGY block
printer(request, "}");
}
else {
// following check will clear NaN values
if (OBK_IS_NAN(energy)) {
energy = 0;
}
if (OBK_IS_NAN(energy_hour)) {
energy_hour = 0;
}
if (OBK_IS_NAN(energy_yesterday)) {
energy_yesterday = 0;
}
printer(request, "{");
printer(request, "\"Power\": %f,", power);
printer(request, "\"ApparentPower\": %f,", g_apparentPower);
printer(request, "\"ReactivePower\": %f,", g_reactivePower);
printer(request, "\"Factor\":%f,", g_powerFactor);
printer(request, "\"Voltage\":%f,", voltage);
printer(request, "\"Current\":%f,", current);
printer(request, "\"ConsumptionTotal\":%f,", energy);
printer(request, "\"Yesterday\": %f,", energy_yesterday);
printer(request, "\"ConsumptionLastHour\":%f", energy_hour);
printer(request, "\"Power\": %f,", _getReading_NanToZero(OBK_POWER));
printer(request, "\"ApparentPower\": %f,", _getReading_NanToZero(OBK_POWER_APPARENT));
printer(request, "\"ReactivePower\": %f,", _getReading_NanToZero(OBK_POWER_REACTIVE));
printer(request, "\"Factor\":%f,", _getReading_NanToZero(OBK_POWER_FACTOR));
printer(request, "\"Voltage\":%f,", _getReading_NanToZero(OBK_VOLTAGE));
printer(request, "\"Current\":%f,", _getReading_NanToZero(OBK_CURRENT));
printer(request, "\"ConsumptionTotal\":%f,", _getReading_NanToZero(OBK_CONSUMPTION_TOTAL));
printer(request, "\"Yesterday\": %f,", _getReading_NanToZero(OBK_CONSUMPTION_YESTERDAY));
printer(request, "\"ConsumptionLastHour\":%f", _getReading_NanToZero(OBK_CONSUMPTION_LAST_HOUR));
// close ENERGY block
printer(request, "}");
}