Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/SantaClaas/embedded-fan-control.
Something went wrong. Try again.
13 kB · 303 lines
Rust
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304//! # Home Assistant Discovery Library//! Tools and utilities for creating the discovery payload for MQTT Home Assistant devices during build time for embedded systems.
use std::{collections::BTreeMap, rc::Rc};
use mqtt::QualityOfService;use serde::Serialize;
/// Utility type for when the Home Assistant type definition allows either a list or a string.#[derive(Serialize, Clone, Debug)]#[serde(untagged)]pub enum ListOrString { List(Vec<String>), String(&'static str),}
impl From<&'static str> for ListOrString { fn from(value: &'static str) -> Self { ListOrString::String(value) }}
impl From<Vec<String>> for ListOrString { fn from(value: Vec<String>) -> Self { ListOrString::List(value) }}
/// Information about the device this fan is a part of to tie it into the device registry. Only works when unique_id is set. At least one of identifiers or connections must be present to identify the device.#[derive(Serialize, Default)]pub struct Device { /// A list of IDs that uniquely identify the device. For example a serial number. #[serde(rename = "ids")] #[serde(skip_serializing_if = "Option::is_none")] pub identifiers: Option<ListOrString>, /// The name of the device. #[serde(skip_serializing_if = "Option::is_none")] pub name: Option<&'static str>, /// The model of the device. #[serde(rename = "mdl")] #[serde(skip_serializing_if = "Option::is_none")] pub model: Option<&'static str>, /// The manufacturer of the device. #[serde(rename = "mf")] #[serde(skip_serializing_if = "Option::is_none")] pub manufacturer: Option<&'static str>, /// The hardware version of the device. #[serde(rename = "hw")] #[serde(skip_serializing_if = "Option::is_none")] pub hardware_version: Option<&'static str>, #[serde(rename = "sn")] #[serde(skip_serializing_if = "Option::is_none")] pub serial_number: Option<&'static str>, /// The firmware version of the device. #[serde(rename = "sw")] #[serde(skip_serializing_if = "Option::is_none")] pub software_version: Option<Rc<str>>,}
#[derive(Serialize, Default)]pub struct Origin { /// The name of the application that is the origin of the discovered MQTT item. (Required) pub name: &'static str, /// Software version of the application that supplies the discovered MQTT item. #[serde(rename = "sw")] #[serde(skip_serializing_if = "Option::is_none")] pub software_version: Option<Rc<str>>, /// Support URL of the application that supplies the discovered MQTT item. #[serde(rename = "url")] #[serde(skip_serializing_if = "Option::is_none")] pub support_url: Option<&'static str>,}
/// Device classes added as needed based on https://www.home-assistant.io/integrations/sensor/#device-class#[derive(Serialize)]#[serde(rename_all = "lowercase")]pub enum DeviceClass { Temperature, Humidity, Power, Energy,}
/// What kind of quantity a sensor reports over time, which is what decides whether Home Assistant/// keeps long term statistics for it and whether it can go into the energy dashboard./// See https://developers.home-assistant.io/docs/core/entity/sensor/#available-state-classes#[derive(Serialize)]#[serde(rename_all = "snake_case")]pub enum StateClass { /// The current value of something that moves in both directions, like a temperature Measurement, /// A counter that only ever goes up, short of a reset. Home Assistant handles the reset by /// treating a drop as the start of a new cycle rather than as negative consumption TotalIncreasing,}
/// Internally tagged by the required `platform` (`p`) field#[derive(Serialize)]#[serde(tag = "p", rename_all = "lowercase")]pub enum Component { Fan { /// The name of the fan. Can be set to null if only the device name is relevant. name: Option<&'static str>, /// An ID that uniquely identifies this fan. If two fans have the same unique ID, Home Assistant will raise an exception. Required when used with device-based discovery. #[serde(rename = "uniq_id")] #[serde(skip_serializing_if = "Option::is_none")] unique_id: Option<&'static str>, /// The MQTT topic subscribed to receive state updates. A “None” payload resets to an unknown state. An empty payload is ignored. By default, valid state payloads are OFF and ON. The accepted payloads can be overridden with the payload_off and payload_on config options. #[serde(rename = "stat_t")] #[serde(skip_serializing_if = "Option::is_none")] state_topic: Option<&'static str>,
/// The MQTT topic to publish commands to change the fan state. #[serde(rename = "cmd_t")] command_topic: &'static str, /// The MQTT topic subscribed to receive fan speed based on percentage. #[serde(rename = "pct_stat_t")] percentage_state_topic: Option<&'static str>, /// The MQTT topic to publish commands to change the fan speed state based on a percentage. #[serde(rename = "pct_cmd_t")] percentage_command_topic: Option<&'static str>, /// The maximum of numeric output range (representing 100 %). The percentage_step is defined by 100 / the number of speeds within the speed range. /// Default: 100 #[serde(rename = "spd_rng_max")] speed_range_max: Option<u16>, }, Sensor { /// The name of the sensor. Owned rather than borrowed, unlike the name of a fan, because /// both fans report the same five values and each name is composed from the fan it /// belongs to #[serde(skip_serializing_if = "Option::is_none")] name: Option<String>, /// The MQTT topic the value arrives on. Several sensors can share one topic and pick their /// own value out of it with a `value_template`, which is how one publish per fan feeds all /// of its sensors #[serde(rename = "stat_t")] #[serde(skip_serializing_if = "Option::is_none")] state_topic: Option<&'static str>, #[serde(rename = "dev_cla")] #[serde(skip_serializing_if = "Option::is_none")] device_class: Option<DeviceClass>, #[serde(rename = "stat_cla")] #[serde(skip_serializing_if = "Option::is_none")] state_class: Option<StateClass>, #[serde(rename = "unit_of_meas")] #[serde(skip_serializing_if = "Option::is_none")] unit_of_measurement: Option<&'static str>, #[serde(rename = "val_tpl")] #[serde(skip_serializing_if = "Option::is_none")] value_template: Option<&'static str>, #[serde(rename = "uniq_id")] #[serde(skip_serializing_if = "Option::is_none")] unique_id: Option<&'static str>, },}
/// Home Assistant MQTT device-based Discovery Payload/// This is for the multi [device discovery payload](https://www.home-assistant.io/integrations/mqtt/#device-discovery-payload)./// It requires/// - device/// - origin#[derive(Serialize, Default)]pub struct DiscoveryPayload { #[serde(rename = "dev")] pub device: Device, #[serde(rename = "o")] pub origin: Origin, #[serde(rename = "cmps")] pub components: BTreeMap<String, Component>, #[serde(rename = "qos")] #[serde(skip_serializing_if = "Option::is_none")] pub quality_of_service: Option<QualityOfService>, #[serde(rename = "stat_t")] #[serde(skip_serializing_if = "Option::is_none")] pub state_topic: Option<&'static str>, #[serde(rename = "cmd_t")] #[serde(skip_serializing_if = "Option::is_none")] pub command_topic: Option<&'static str>, /// The encoding of the payloads received and published messages. Set to "" to disable decoding of incoming payload. /// Default is "utf-8" #[serde(rename = "e")] #[serde(skip_serializing_if = "Option::is_none")] pub encoding: Option<String>,}
#[cfg(test)]mod tests { use super::*;
#[test] fn serialize_home_assistant_example() { let payload = DiscoveryPayload { device: Device { identifiers: ListOrString::String("ea334450945afc").into(), name: Some("Kitchen"), manufacturer: Some("Bla electronics"), model: Some("xya"), software_version: Some("1.0".into()), serial_number: Some("ea334450945afc"), hardware_version: Some("1.0rev2"), }, origin: Origin { name: "bla2mqtt", software_version: Some("2.1".into()), support_url: Some("https://bla2mqtt.example.com/support"), }, components: BTreeMap::from([ ( "some_unique_component_id1".to_string(), Component::Sensor { name: None, state_topic: None, device_class: Some(DeviceClass::Temperature), state_class: None, unit_of_measurement: Some("°C"), value_template: Some("{{ value_json.temperature}}"), unique_id: Some("temp01ae_t"), }, ), ( "some_unique_id2".to_string(), Component::Sensor { name: None, state_topic: None, device_class: Some(DeviceClass::Humidity), state_class: None, unit_of_measurement: Some("%"), value_template: Some("{{ value_json.humidity}}"), unique_id: Some("temp01ae_h"), }, ), ]), state_topic: Some("sensorBedroom/state"), quality_of_service: Some(QualityOfService::ExactlyOnceDelivery), ..Default::default() };
insta::assert_json_snapshot!(payload); }
#[test] fn serialize_custom_example() { let version = Some(Rc::from("test-version")); let payload = DiscoveryPayload { device: Device { identifiers: Some(ListOrString::String("fancontroller-device")), name: Some("Fan Controller"), model: Some("Raspberry Pi Pico W 1"), manufacturer: Some("claas.dev"), hardware_version: Some("1.0"), software_version: version.clone(), ..Default::default() }, origin: Origin { name: "fan-controller", software_version: version, support_url: Some("https://github.com/SantaClaas/embedded-fan-control"), }, components: BTreeMap::from([ // Fan 1 ( "fan-1".to_string(), Component::Fan { name: Some("Fan 1"), unique_id: Some("fancontroller/fan-1"), state_topic: Some(topic::fan_controller::fan_1::state::STATE), command_topic: topic::fan_controller::fan_1::state::COMMAND, percentage_state_topic: Some( topic::fan_controller::fan_1::percentage::STATE, ), percentage_command_topic: Some( topic::fan_controller::fan_1::percentage::COMMAND, ), speed_range_max: Some(32_000), }, ), // Fan 2 ( "fan-2".to_string(), Component::Fan { name: Some("Fan 2"), unique_id: Some("fancontroller/fan-2"), state_topic: Some(topic::fan_controller::fan_2::state::STATE), command_topic: topic::fan_controller::fan_2::state::COMMAND, percentage_state_topic: Some( topic::fan_controller::fan_2::percentage::STATE, ), percentage_command_topic: Some( topic::fan_controller::fan_2::percentage::COMMAND, ), speed_range_max: Some(32_000), }, ), ]), quality_of_service: None, state_topic: Some(topic::fan_controller::STATE), command_topic: Some(topic::fan_controller::COMMAND), encoding: None, };
insta::assert_json_snapshot!(payload) }}