updates to add_ha_classes

This commit is contained in:
proddy
2025-12-22 15:41:14 +01:00
parent 0e6108b5a9
commit ea4d613d12
6 changed files with 126 additions and 77 deletions

View File

@@ -809,9 +809,9 @@ void AnalogSensor::publish_values(const bool force) {
std::string topic_str(topic);
config["def_ent_id"] = topic_str.substr(0, topic_str.find("/")) + "." + uniq_s;
// add HA Discovery config
Mqtt::add_ha_classes(config.as<JsonObject>(), EMSdevice::DeviceType::ANALOGSENSOR, valueType, sensor.uom());
// dev section with model is only created on the 1st sensor
Mqtt::add_ha_dev_section(config.as<JsonObject>(), "Analog Sensors", !ha_dev_created);
Mqtt::add_ha_dev_section(config.as<JsonObject>(), "Analog Sensors", !ha_dev_created); // dev section with model is only created on the 1st sensor
Mqtt::add_ha_avty_section(config.as<JsonObject>(), stat_t, val_cond);
sensor.ha_registered = Mqtt::queue_ha(topic, config.as<JsonObject>());

View File

@@ -1687,7 +1687,7 @@ void EMSdevice::get_value_json(JsonObject json, DeviceValue & dv) {
}
// add uom, state class and device class
Mqtt::add_ha_classes(json, device_type(), dv.type, dv.uom, dv.short_name, false); // no icon
Mqtt::add_ha_classes(json, device_type(), dv.type, dv.uom, dv.short_name, true); // display only
json["readable"] = dv.type != DeviceValueType::CMD && !dv.has_state(DeviceValueState::DV_API_MQTT_EXCLUDE);
json["writeable"] = dv.has_cmd && !dv.has_state(DeviceValueState::DV_READONLY);

View File

@@ -47,36 +47,37 @@ class DeviceValue {
// Unit Of Measurement mapping - maps to DeviceValueUOM_s in emsdevicevalue.cpp. Sequence is important!!
// also used with HA as uom
// shows also the HA device class being used
enum DeviceValueUOM : uint8_t {
NONE = 0, // 0
DEGREES, // 1 - °C
DEGREES_R, // 2 - °C (relative temperature)
PERCENT, // 3 - %
LMIN, // 4 - l/min
KWH, // 5 - kWh
WH, // 6 - Wh
HOURS, // 7 - h
MINUTES, // 8 - m
UA, // 9 - µA
BAR, // 10 - bar
KW, // 11 - kW
W, // 12 - W
KB, // 13 - kB
SECONDS, // 14 - s
DBM, // 15 - dBm
FAHRENHEIT, // 16 - °F
MV, // 17 - mV
SQM, // 18 - m²
M3, // 19 - m³
L, // 20 - L
DEGREES, // 1 - °C - temperature
DEGREES_R, // 2 - °C (relative temperature) - temperature
PERCENT, // 3 - % - power factor
LMIN, // 4 - l/min - volume flow rate
KWH, // 5 - kWh - energy
WH, // 6 - Wh - energy
HOURS, // 7 - h - duration
MINUTES, // 8 - m - duration
UA, // 9 - µA - current
BAR, // 10 - bar - pressure
KW, // 11 - kW - power
W, // 12 - W - power
KB, // 13 - kB - data size
SECONDS, // 14 - s - duration
DBM, // 15 - dBm - signal strength
FAHRENHEIT, // 16 - °F - temperature
MV, // 17 - mV - voltage
SQM, // 18 - m² - area
M3, // 19 - m³ - volume
L, // 20 - L - volume
KMIN, // 21 - K*min
K, // 22 - K
VOLTS, // 23 - V
MBAR, // 24 - mbar
LH, // 25 - l/h
CTKWH, // 26 - ct/kWh
HZ, // 27 - Hz
CONNECTIVITY // 28 - used in HA
K, // 22 - K - temperature
VOLTS, // 23 - V - voltage
MBAR, // 24 - mbar - atmospheric pressure
LH, // 25 - l/h - volume flow rate
CTKWH, // 26 - ct/kWh - monetary
HZ, // 27 - Hz - frequency
CONNECTIVITY // 28 - used in HA - connectivity
};
// TAG mapping - maps to DeviceValueTAG_s in emsdevicevalue.cpp

View File

@@ -62,8 +62,6 @@ std::string Mqtt::lastresponse_ = "";
// Home Assistant specific
// icons from https://materialdesignicons.com used with the UOMs (unit of measurements)
MAKE_WORD(measurement)
MAKE_WORD(total_increasing)
// MAKE_WORD_CUSTOM(icondegrees, "mdi:coolant-temperature") // DeviceValueUOM::DEGREES
MAKE_WORD_CUSTOM(iconpercent, "mdi:percent-outline") // DeviceValueUOM::PERCENT
MAKE_WORD_CUSTOM(iconkb, "mdi:memory") // DeviceValueUOM::KB
@@ -1124,18 +1122,29 @@ bool Mqtt::publish_ha_sensor_config(uint8_t type, // EMSdev
return queue_ha(topic, doc.as<JsonObject>());
}
// Add the uom, state class, device class and an optional icon based on the uom
void Mqtt::add_ha_classes(JsonObject doc, const uint8_t device_type, const uint8_t type, const uint8_t uom, const char * entity, bool is_discovery) {
// Add the uom, state class, device class and an optional icon based on the uom (unless display_only is true)
// https://developers.home-assistant.io/docs/core/entity/sensor/
// Home Assistant tracks the min, max and mean value during the statistics period
// For statistics, the sensor must have state_class set to either MEASUREMENT, TOTAL or TOTAL_INCREASING
// When set to MEASUREMENT the device_class must cannot be DATE, ENUM, ENERGY, GAS, MONETARY, TIMESTAMP, VOLUME or WATER
//
void Mqtt::add_ha_classes(JsonObject doc, const uint8_t device_type, const uint8_t type, const uint8_t uom, const char * entity, bool display_only) {
if (device_type == EMSdevice::DeviceType::SYSTEM) {
doc["ent_cat"] = "diagnostic"; // instead of 'config'
}
// for HA discovery we use different namings
const char * dc_ha = is_discovery ? "dev_cla" : "device_class"; // device class
const char * sc_ha = is_discovery ? "stat_cla" : "state_class"; // state class
const char * uom_ha = is_discovery ? "unit_of_meas" : "uom"; // unit of measure
// For display_only, we don't use the aliases, also we don't display the icon
const char * dc_ha = display_only ? "device_class" : "dev_cla"; // device class, dev_cla
const char * sc_ha = display_only ? "state_class" : "stat_cla"; // state class, stat_cla
const char * uom_ha = display_only ? "uom" : "unit_of_meas"; // unit of measure, unit_of_meas
const char * ic_ha = display_only ? "icon" : "ic"; // icon, ic
// set uom, unless boolean
// state class constants
const char * sc_ha_measurement = "measurement";
const char * sc_ha_total = "total";
const char * sc_ha_total_increasing = "total_increasing";
// set uom, unless boolean - as HA is fussy with the naming
// using HA uom specific codes from https://github.com/home-assistant/core/blob/dev/homeassistant/const.py
if (type != DeviceValueType::BOOL) {
if (uom == DeviceValueUOM::HOURS) {
@@ -1144,6 +1153,8 @@ void Mqtt::add_ha_classes(JsonObject doc, const uint8_t device_type, const uint8
doc[uom_ha] = "min";
} else if (uom == DeviceValueUOM::SECONDS) {
doc[uom_ha] = "s";
} else if (uom == DeviceValueUOM::KB) {
doc[uom_ha] = "kB";
} else if (uom != DeviceValueUOM::NONE) {
doc[uom_ha] = EMSdevice::uom_to_string(uom); // use default
} else if (discovery_type() != discoveryType::HOMEASSISTANT) {
@@ -1151,99 +1162,136 @@ void Mqtt::add_ha_classes(JsonObject doc, const uint8_t device_type, const uint8
}
}
// set state and device class
// also icon, when there is no device class that sets one
// set state and device class - always
// see https://developers.home-assistant.io/docs/core/entity/sensor/#available-device-classes
switch (uom) {
case DeviceValueUOM::DEGREES:
case DeviceValueUOM::DEGREES_R:
case DeviceValueUOM::K:
doc[sc_ha] = F_(measurement);
doc[sc_ha] = sc_ha_measurement;
doc[dc_ha] = "temperature";
// override uom if fahrenheit
doc[uom_ha] = EMSESP::system_.fahrenheit() ? DeviceValue::DeviceValueUOM_s[DeviceValueUOM::FAHRENHEIT] : DeviceValue::DeviceValueUOM_s[uom];
break;
case DeviceValueUOM::PERCENT:
doc[sc_ha] = F_(measurement);
if (display_only) {
doc[ic_ha] = F_(iconpercent); // set icon
}
doc[sc_ha] = sc_ha_measurement;
doc[dc_ha] = "power_factor";
if (is_discovery)
doc["ic"] = F_(iconpercent); // icon
break;
case DeviceValueUOM::SECONDS:
case DeviceValueUOM::MINUTES:
case DeviceValueUOM::HOURS:
if (type == DeviceValueType::TIME) {
doc[sc_ha] = F_(total_increasing);
doc[sc_ha] = sc_ha_total_increasing;
} else {
doc[sc_ha] = F_(measurement);
doc[sc_ha] = sc_ha_measurement;
}
doc[dc_ha] = "duration"; // https://github.com/emsesp/EMS-ESP32/issues/822
break;
case DeviceValueUOM::KB:
if (is_discovery)
doc["ic"] = F_(iconkb);
if (display_only) {
doc[ic_ha] = F_(iconkb);
}
doc[dc_ha] = "data_size";
break;
case DeviceValueUOM::LMIN:
case DeviceValueUOM::LH:
if (is_discovery)
doc["ic"] = F_(iconlmin);
doc[sc_ha] = F_(measurement);
if (display_only) {
doc[ic_ha] = F_(iconlmin);
}
doc[sc_ha] = sc_ha_measurement;
doc[dc_ha] = "volume_flow_rate";
break;
case DeviceValueUOM::WH:
// https://github.com/emsesp/EMS-ESP32/issues/1796
if (entity == FL_(energyToday)[0]) {
doc[sc_ha] = F_(total_increasing);
doc[sc_ha] = sc_ha_total_increasing;
doc[dc_ha] = "energy";
} else if (entity == FL_(energyLastHour)[0]) {
doc[sc_ha] = "total";
doc[sc_ha] = sc_ha_total;
doc[dc_ha] = "energy";
} else {
doc[sc_ha] = F_(measurement);
doc[sc_ha] = sc_ha_measurement; // no device class for this
}
doc[dc_ha] = "energy";
break;
case DeviceValueUOM::KWH:
doc[sc_ha] = F_(total_increasing);
doc[sc_ha] = sc_ha_total_increasing;
doc[dc_ha] = "energy";
break;
case DeviceValueUOM::UA:
if (is_discovery)
doc["ic"] = F_(iconua);
doc[sc_ha] = F_(measurement);
if (display_only) {
doc[ic_ha] = F_(iconua);
}
doc[sc_ha] = sc_ha_measurement;
doc[dc_ha] = "current";
break;
case DeviceValueUOM::BAR:
doc[sc_ha] = F_(measurement);
doc[sc_ha] = sc_ha_measurement;
doc[dc_ha] = "pressure";
break;
case DeviceValueUOM::W:
case DeviceValueUOM::KW:
doc[sc_ha] = F_(measurement);
doc[sc_ha] = sc_ha_measurement;
doc[dc_ha] = "power";
break;
case DeviceValueUOM::DBM:
doc[sc_ha] = F_(measurement);
doc[sc_ha] = sc_ha_measurement;
doc[dc_ha] = "signal_strength";
break;
case DeviceValueUOM::CONNECTIVITY:
doc[sc_ha] = F_(measurement);
doc[sc_ha] = sc_ha_measurement;
doc[dc_ha] = "connectivity";
break;
case DeviceValueUOM::NONE:
case DeviceValueUOM::MV:
case DeviceValueUOM::VOLTS:
doc[sc_ha] = sc_ha_measurement;
doc[dc_ha] = "voltage";
break;
case DeviceValueUOM::MBAR:
doc[sc_ha] = sc_ha_measurement;
doc[dc_ha] = "atmospheric_pressure";
break;
case DeviceValueUOM::CTKWH:
doc[sc_ha] = sc_ha_total_increasing;
doc[dc_ha] = "monetary";
break;
case DeviceValueUOM::HZ:
doc[sc_ha] = sc_ha_measurement;
doc[dc_ha] = "frequency";
break;
case DeviceValueUOM::M3:
case DeviceValueUOM::L:
// this could be Gas or Water, so going with the generic 'volume' for now
// state_class can't be a Measurement
doc[sc_ha] = sc_ha_total_increasing;
doc[dc_ha] = "volume";
break;
case DeviceValueUOM::SQM:
doc[sc_ha] = sc_ha_measurement;
doc[dc_ha] = "area";
break;
default:
// DeviceValueUOM::NONE:
// DeviceValueUOM::KMIN:
// for device entities which have numerical values, with no UOM
if ((type != DeviceValueType::STRING)
&& (type == DeviceValueType::INT8 || type == DeviceValueType::UINT8 || type == DeviceValueType::INT16 || type == DeviceValueType::UINT16
|| type == DeviceValueType::UINT24 || type == DeviceValueType::UINT32)) {
if (is_discovery)
doc["ic"] = F_(iconnum); // set icon
if (display_only) {
doc[ic_ha] = F_(iconnum); // set icon
}
// determine if its a measurement or total increasing
// most of the values are measurement. for example Tx Reads will increment but can be reset to 0 after a restart
// all the starts are increasing, and they are ULONGs
if (type == DeviceValueType::UINT24 || type == DeviceValueType::UINT32) {
doc[sc_ha] = F_(total_increasing);
doc[sc_ha] = sc_ha_total_increasing;
} else {
doc[sc_ha] = F_(measurement); // default to measurement
doc[sc_ha] = sc_ha_measurement; // default to measurement
}
}
break;
default:
break;
}
}
@@ -1444,9 +1492,8 @@ std::string Mqtt::tag_to_topic(uint8_t device_type, int8_t tag) {
}
}
// add devs section to an existing doc, only for HA
// under devs node it will create ids and name and optional mf, mdl, via_device
// name could be EMSdevice::device_type_2_device_name(dv.device_type));
// adds dev section for HA Discovery to an existing JSON doc
// under devs node it will create ids and optional name, mf, mdl, sw and via_device
void Mqtt::add_ha_dev_section(JsonObject doc, const char * name, const bool create_model, const char * model, const char * brand, const char * version) {
// only works for HA
if (discovery_type() != discoveryType::HOMEASSISTANT) {
@@ -1488,6 +1535,7 @@ void Mqtt::add_ha_dev_section(JsonObject doc, const char * name, const bool crea
}
// adds avty section for HA Discovery to an existing JSON doc
// also adds LWT (Last Will and Testament)
void Mqtt::add_ha_avty_section(JsonObject doc, const char * state_t, const char * cond1, const char * cond2, const char * negcond) {
// only works for HA
if (discovery_type() != discoveryType::HOMEASSISTANT) {

View File

@@ -258,7 +258,7 @@ class Mqtt {
static std::string tag_to_topic(uint8_t device_type, int8_t tag);
static void
add_ha_classes(JsonObject doc, const uint8_t device_type, const uint8_t type, const uint8_t uom, const char * entity = nullptr, bool is_discovery = true);
add_ha_classes(JsonObject doc, const uint8_t device_type, const uint8_t type, const uint8_t uom, const char * entity = nullptr, bool display_only = false);
static void add_ha_dev_section(JsonObject doc,
const char * name = nullptr,
const bool create_model = false,

View File

@@ -352,8 +352,8 @@ void WebCustomEntityService::get_value_json(JsonObject output, CustomEntityItem
}
}
// add uom state class and device class
Mqtt::add_ha_classes(output, EMSdevice::DeviceType::SYSTEM, entity.value_type, entity.uom, nullptr, false);
// add uom state class and device class to output json, excluding icon
Mqtt::add_ha_classes(output, EMSdevice::DeviceType::SYSTEM, entity.value_type, entity.uom, nullptr, true); // display only
render_value(output, entity, true); // create the "value" field
}