diff --git a/src/core/analogsensor.cpp b/src/core/analogsensor.cpp index 1bb66e64c..4e461b9c1 100644 --- a/src/core/analogsensor.cpp +++ b/src/core/analogsensor.cpp @@ -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(), EMSdevice::DeviceType::ANALOGSENSOR, valueType, sensor.uom()); - // dev section with model is only created on the 1st sensor - Mqtt::add_ha_dev_section(config.as(), "Analog Sensors", !ha_dev_created); + Mqtt::add_ha_dev_section(config.as(), "Analog Sensors", !ha_dev_created); // dev section with model is only created on the 1st sensor Mqtt::add_ha_avty_section(config.as(), stat_t, val_cond); sensor.ha_registered = Mqtt::queue_ha(topic, config.as()); diff --git a/src/core/emsdevice.cpp b/src/core/emsdevice.cpp index e62169ca6..8d07660d0 100644 --- a/src/core/emsdevice.cpp +++ b/src/core/emsdevice.cpp @@ -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); diff --git a/src/core/emsdevicevalue.h b/src/core/emsdevicevalue.h index f6dcb3827..04d28809c 100644 --- a/src/core/emsdevicevalue.h +++ b/src/core/emsdevicevalue.h @@ -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 diff --git a/src/core/mqtt.cpp b/src/core/mqtt.cpp index 5fc577b72..e9e6819a3 100644 --- a/src/core/mqtt.cpp +++ b/src/core/mqtt.cpp @@ -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()); } -// 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) { diff --git a/src/core/mqtt.h b/src/core/mqtt.h index e51e16fac..8a2f82137 100644 --- a/src/core/mqtt.h +++ b/src/core/mqtt.h @@ -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, diff --git a/src/web/WebCustomEntityService.cpp b/src/web/WebCustomEntityService.cpp index dcb3ff23b..2b25953c4 100644 --- a/src/web/WebCustomEntityService.cpp +++ b/src/web/WebCustomEntityService.cpp @@ -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 }