diff options
| author | diogo464 <[email protected]> | 2025-12-14 14:51:08 +0000 |
|---|---|---|
| committer | diogo464 <[email protected]> | 2025-12-14 14:51:08 +0000 |
| commit | f6331eec208b2160dce93a6e2d95162d19273cc1 (patch) | |
| tree | db309557c5b5d3b260ef869f7eec444e0eb31b78 | |
| parent | c4f74c992b93f6fbcf8d41f77752942225e97457 (diff) | |
| parent | a16dbf1d55a6f5b28236eef566eea0f44524840c (diff) | |
Merge branch 'main' into embassy-git
| -rw-r--r-- | Cargo.lock | 2 | ||||
| -rw-r--r-- | Cargo.toml | 2 | ||||
| -rw-r--r-- | README.md | 105 | ||||
| -rw-r--r-- | README.tpl | 15 | ||||
| -rwxr-xr-x | generate-readme.sh | 5 | ||||
| -rw-r--r-- | src/command_policy.rs | 79 | ||||
| -rw-r--r-- | src/entity_number.rs | 10 | ||||
| -rw-r--r-- | src/entity_switch.rs | 10 | ||||
| -rw-r--r-- | src/lib.rs | 199 |
9 files changed, 394 insertions, 33 deletions
| @@ -175,7 +175,7 @@ source = "git+https://github.com/embassy-rs/embassy#d2740f8fad566f30bed24df970f1 | |||
| 175 | 175 | ||
| 176 | [[package]] | 176 | [[package]] |
| 177 | name = "embassy-ha" | 177 | name = "embassy-ha" |
| 178 | version = "0.1.1" | 178 | version = "0.2.0" |
| 179 | dependencies = [ | 179 | dependencies = [ |
| 180 | "critical-section", | 180 | "critical-section", |
| 181 | "defmt 1.0.1", | 181 | "defmt 1.0.1", |
| @@ -1,6 +1,6 @@ | |||
| 1 | [package] | 1 | [package] |
| 2 | name = "embassy-ha" | 2 | name = "embassy-ha" |
| 3 | version = "0.1.1" | 3 | version = "0.2.0" |
| 4 | edition = "2024" | 4 | edition = "2024" |
| 5 | authors = ["diogo464 <[email protected]>"] | 5 | authors = ["diogo464 <[email protected]>"] |
| 6 | description = "MQTT Home Assistant integration library for Embassy async runtime" | 6 | description = "MQTT Home Assistant integration library for Embassy async runtime" |
| @@ -1,17 +1,102 @@ | |||
| 1 | # embassy-ha | 1 | # embassy-ha |
| 2 | 2 | ||
| 3 | Home Assistant MQTT device library for embassy. | 3 | [](https://crates.io/crates/embassy-ha) |
| 4 | [](https://docs.rs/embassy-ha) | ||
| 4 | 5 | ||
| 5 | To create a device use the [`new`] function. | 6 | MQTT Home Assistant integration library for the [Embassy](https://embassy.dev/) async runtime. |
| 6 | 7 | ||
| 7 | After the device is created you should create one or more entities using functions such as | 8 | ## Features |
| 8 | [`create_button`]/[`create_sensor`]/... | ||
| 9 | 9 | ||
| 10 | Once the entities have been created either [`run`] or [`connect_and_run`] should be called in a | 10 | - Support for multiple entity types: sensors, buttons, switches, binary sensors, numbers, device trackers |
| 11 | seperate task. | 11 | - Built on top of Embassy's async runtime for embedded systems |
| 12 | - No-std compatible | ||
| 13 | - Automatic MQTT discovery for Home Assistant | ||
| 14 | - No runtime allocation | ||
| 12 | 15 | ||
| 13 | There are various examples you can run locally (ex: `cargo run --features tracing --example | 16 | ## Installation |
| 14 | button`) assuming you have a home assistant instance running. To run the examples the | ||
| 15 | environment variable `MQTT_ADDRESS` should be set to the mqtt server used by home assistant. | ||
| 16 | 17 | ||
| 17 | License: MIT OR Apache-2.0 | 18 | ```bash |
| 19 | cargo add embassy-ha | ||
| 20 | ``` | ||
| 21 | |||
| 22 | ## Quick Start | ||
| 23 | |||
| 24 | This example does not compile as-is because it requires device-specific setup, but it should | ||
| 25 | be easy to adapt if you already have Embassy running on your microcontroller. | ||
| 26 | |||
| 27 | ```rust | ||
| 28 | use embassy_executor::Spawner; | ||
| 29 | use embassy_ha::{DeviceConfig, SensorConfig, SensorClass, StateClass}; | ||
| 30 | use embassy_time::Timer; | ||
| 31 | use static_cell::StaticCell; | ||
| 32 | |||
| 33 | static HA_RESOURCES: StaticCell<embassy_ha::DeviceResources> = StaticCell::new(); | ||
| 34 | |||
| 35 | #[embassy_executor::main] | ||
| 36 | async fn main(spawner: Spawner) { | ||
| 37 | // Initialize your network stack | ||
| 38 | // This is device specific | ||
| 39 | let stack: embassy_net::Stack<'static>; | ||
| 40 | |||
| 41 | // Create a Home Assistant device | ||
| 42 | let device = embassy_ha::new( | ||
| 43 | HA_RESOURCES.init(Default::default()), | ||
| 44 | DeviceConfig { | ||
| 45 | device_id: "my-device", | ||
| 46 | device_name: "My Device", | ||
| 47 | manufacturer: "ACME Corp", | ||
| 48 | model: "Model X", | ||
| 49 | }, | ||
| 50 | ); | ||
| 51 | |||
| 52 | // Create a temperature sensor | ||
| 53 | let sensor_config = SensorConfig { | ||
| 54 | class: SensorClass::Temperature, | ||
| 55 | state_class: StateClass::Measurement, | ||
| 56 | unit: Some(embassy_ha::constants::HA_UNIT_TEMPERATURE_CELSIUS), | ||
| 57 | ..Default::default() | ||
| 58 | }; | ||
| 59 | let mut sensor = embassy_ha::create_sensor(&device, "temp-sensor", sensor_config); | ||
| 60 | |||
| 61 | // Spawn the Home Assistant communication task | ||
| 62 | spawner.spawn(ha_task(stack, device)).unwrap(); | ||
| 63 | |||
| 64 | // Main loop - read and publish temperature | ||
| 65 | loop { | ||
| 66 | // let temperature = read_temperature().await; | ||
| 67 | sensor.publish(temperature); | ||
| 68 | Timer::after_secs(60).await; | ||
| 69 | } | ||
| 70 | } | ||
| 71 | |||
| 72 | #[embassy_executor::task] | ||
| 73 | async fn ha_task(stack: embassy_net::Stack<'static>, device: embassy_ha::Device<'static>) { | ||
| 74 | embassy_ha::connect_and_run(stack, device, "mqtt-broker-address").await; | ||
| 75 | } | ||
| 76 | ``` | ||
| 77 | |||
| 78 | ## Examples | ||
| 79 | |||
| 80 | The repository includes several examples demonstrating different entity types. To run an example: | ||
| 81 | |||
| 82 | ```bash | ||
| 83 | export MQTT_ADDRESS="mqtt://your-mqtt-broker:1883" | ||
| 84 | cargo run --example sensor | ||
| 85 | ``` | ||
| 86 | |||
| 87 | Available examples: | ||
| 88 | - `sensor` - Temperature and humidity sensors | ||
| 89 | - `button` - Triggerable button entity | ||
| 90 | - `switch` - On/off switch control | ||
| 91 | - `binary_sensor` - Binary state sensor | ||
| 92 | - `number` - Numeric input entity | ||
| 93 | - `device_tracker` - Location tracking entity | ||
| 94 | |||
| 95 | ## License | ||
| 96 | |||
| 97 | Licensed under either of: | ||
| 98 | |||
| 99 | - Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) | ||
| 100 | - MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) | ||
| 101 | |||
| 102 | at your option. | ||
diff --git a/README.tpl b/README.tpl new file mode 100644 index 0000000..dba64ff --- /dev/null +++ b/README.tpl | |||
| @@ -0,0 +1,15 @@ | |||
| 1 | # {{crate}} | ||
| 2 | |||
| 3 | [](https://crates.io/crates/embassy-ha) | ||
| 4 | [](https://docs.rs/embassy-ha) | ||
| 5 | |||
| 6 | {{readme}} | ||
| 7 | |||
| 8 | ## License | ||
| 9 | |||
| 10 | Licensed under either of: | ||
| 11 | |||
| 12 | - Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) | ||
| 13 | - MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) | ||
| 14 | |||
| 15 | at your option. | ||
diff --git a/generate-readme.sh b/generate-readme.sh new file mode 100755 index 0000000..18c2123 --- /dev/null +++ b/generate-readme.sh | |||
| @@ -0,0 +1,5 @@ | |||
| 1 | #!/bin/bash | ||
| 2 | set -e | ||
| 3 | |||
| 4 | cargo readme -o README.md | ||
| 5 | echo "README.md generated successfully" | ||
diff --git a/src/command_policy.rs b/src/command_policy.rs new file mode 100644 index 0000000..049d56a --- /dev/null +++ b/src/command_policy.rs | |||
| @@ -0,0 +1,79 @@ | |||
| 1 | /// Determines how an entity handles commands received from Home Assistant. | ||
| 2 | /// | ||
| 3 | /// This policy controls whether an entity automatically publishes its state when it receives | ||
| 4 | /// a command from Home Assistant, or if the application should handle state updates manually. | ||
| 5 | /// | ||
| 6 | /// # Variants | ||
| 7 | /// | ||
| 8 | /// ## `PublishState` (Default) | ||
| 9 | /// | ||
| 10 | /// When a command is received from Home Assistant, the entity automatically: | ||
| 11 | /// 1. Updates its internal state to match the command value | ||
| 12 | /// 2. Publishes the new state back to Home Assistant | ||
| 13 | /// | ||
| 14 | /// This is useful for simple entities where the command should immediately be reflected as the | ||
| 15 | /// current state, such as: | ||
| 16 | /// - A switch that turns on/off immediately when commanded | ||
| 17 | /// - A number input that updates its value when changed in the UI | ||
| 18 | /// | ||
| 19 | /// ## `Manual` | ||
| 20 | /// | ||
| 21 | /// When a command is received from Home Assistant, the entity: | ||
| 22 | /// 1. Stores the command for the application to read via `wait()` or `command()` | ||
| 23 | /// 2. Does NOT automatically update or publish the state | ||
| 24 | /// | ||
| 25 | /// The application must manually update the entity's state after processing the command. | ||
| 26 | /// This is useful when: | ||
| 27 | /// - The command triggers an action that may fail (e.g., turning on a motor) | ||
| 28 | /// - The actual state may differ from the commanded state | ||
| 29 | /// - You need to validate or transform the command before applying it | ||
| 30 | /// | ||
| 31 | /// # Examples | ||
| 32 | /// | ||
| 33 | /// ## Auto-publish (default) | ||
| 34 | /// | ||
| 35 | /// ```no_run | ||
| 36 | /// # use embassy_ha::{CommandPolicy, SwitchConfig}; | ||
| 37 | /// let config = SwitchConfig { | ||
| 38 | /// command_policy: CommandPolicy::PublishState, // or just use default | ||
| 39 | /// ..Default::default() | ||
| 40 | /// }; | ||
| 41 | /// // When Home Assistant sends "ON", the switch state automatically becomes "ON" | ||
| 42 | /// ``` | ||
| 43 | /// | ||
| 44 | /// ## Manual control | ||
| 45 | /// | ||
| 46 | /// ```no_run | ||
| 47 | /// # use embassy_ha::{CommandPolicy, SwitchConfig, BinaryState, Switch}; | ||
| 48 | /// # async fn example(mut switch: Switch<'_>) { | ||
| 49 | /// let config = SwitchConfig { | ||
| 50 | /// command_policy: CommandPolicy::Manual, | ||
| 51 | /// ..Default::default() | ||
| 52 | /// }; | ||
| 53 | /// | ||
| 54 | /// loop { | ||
| 55 | /// let command = switch.wait().await; | ||
| 56 | /// | ||
| 57 | /// // Try to perform the action | ||
| 58 | /// if turn_on_motor().await.is_ok() { | ||
| 59 | /// // Only update state if the action succeeded | ||
| 60 | /// switch.set(command); | ||
| 61 | /// } | ||
| 62 | /// } | ||
| 63 | /// # } | ||
| 64 | /// # async fn turn_on_motor() -> Result<(), ()> { Ok(()) } | ||
| 65 | /// ``` | ||
| 66 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] | ||
| 67 | pub enum CommandPolicy { | ||
| 68 | /// Automatically publish the entity's state when a command is received. | ||
| 69 | PublishState, | ||
| 70 | |||
| 71 | /// Do not automatically publish state. The application must manually update the state. | ||
| 72 | Manual, | ||
| 73 | } | ||
| 74 | |||
| 75 | impl Default for CommandPolicy { | ||
| 76 | fn default() -> Self { | ||
| 77 | Self::PublishState | ||
| 78 | } | ||
| 79 | } | ||
diff --git a/src/entity_number.rs b/src/entity_number.rs index e2a89c1..1573000 100644 --- a/src/entity_number.rs +++ b/src/entity_number.rs | |||
| @@ -1,5 +1,6 @@ | |||
| 1 | use crate::{ | 1 | use crate::{ |
| 2 | Entity, EntityCommonConfig, EntityConfig, NumberCommand, NumberState, NumberUnit, constants, | 2 | CommandPolicy, Entity, EntityCommonConfig, EntityConfig, NumberCommand, NumberState, |
| 3 | NumberUnit, constants, | ||
| 3 | }; | 4 | }; |
| 4 | 5 | ||
| 5 | #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] | 6 | #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] |
| @@ -61,6 +62,9 @@ pub enum NumberClass { | |||
| 61 | WindSpeed, | 62 | WindSpeed, |
| 62 | } | 63 | } |
| 63 | 64 | ||
| 65 | /// Configuration for a number entity. | ||
| 66 | /// | ||
| 67 | /// See [`CommandPolicy`] for details on how commands are handled. | ||
| 64 | #[derive(Debug)] | 68 | #[derive(Debug)] |
| 65 | pub struct NumberConfig { | 69 | pub struct NumberConfig { |
| 66 | pub common: EntityCommonConfig, | 70 | pub common: EntityCommonConfig, |
| @@ -70,7 +74,7 @@ pub struct NumberConfig { | |||
| 70 | pub step: Option<f32>, | 74 | pub step: Option<f32>, |
| 71 | pub mode: NumberMode, | 75 | pub mode: NumberMode, |
| 72 | pub class: NumberClass, | 76 | pub class: NumberClass, |
| 73 | pub publish_on_command: bool, | 77 | pub command_policy: CommandPolicy, |
| 74 | } | 78 | } |
| 75 | 79 | ||
| 76 | impl Default for NumberConfig { | 80 | impl Default for NumberConfig { |
| @@ -83,7 +87,7 @@ impl Default for NumberConfig { | |||
| 83 | step: None, | 87 | step: None, |
| 84 | mode: NumberMode::Auto, | 88 | mode: NumberMode::Auto, |
| 85 | class: NumberClass::Generic, | 89 | class: NumberClass::Generic, |
| 86 | publish_on_command: true, | 90 | command_policy: CommandPolicy::default(), |
| 87 | } | 91 | } |
| 88 | } | 92 | } |
| 89 | } | 93 | } |
diff --git a/src/entity_switch.rs b/src/entity_switch.rs index 299d299..c1531eb 100644 --- a/src/entity_switch.rs +++ b/src/entity_switch.rs | |||
| @@ -1,5 +1,6 @@ | |||
| 1 | use crate::{ | 1 | use crate::{ |
| 2 | BinaryState, Entity, EntityCommonConfig, EntityConfig, SwitchCommand, SwitchState, constants, | 2 | BinaryState, CommandPolicy, Entity, EntityCommonConfig, EntityConfig, SwitchCommand, |
| 3 | SwitchState, constants, | ||
| 3 | }; | 4 | }; |
| 4 | 5 | ||
| 5 | #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] | 6 | #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] |
| @@ -10,11 +11,14 @@ pub enum SwitchClass { | |||
| 10 | Switch, | 11 | Switch, |
| 11 | } | 12 | } |
| 12 | 13 | ||
| 14 | /// Configuration for a switch entity. | ||
| 15 | /// | ||
| 16 | /// See [`CommandPolicy`] for details on how commands are handled. | ||
| 13 | #[derive(Debug)] | 17 | #[derive(Debug)] |
| 14 | pub struct SwitchConfig { | 18 | pub struct SwitchConfig { |
| 15 | pub common: EntityCommonConfig, | 19 | pub common: EntityCommonConfig, |
| 16 | pub class: SwitchClass, | 20 | pub class: SwitchClass, |
| 17 | pub publish_on_command: bool, | 21 | pub command_policy: CommandPolicy, |
| 18 | } | 22 | } |
| 19 | 23 | ||
| 20 | impl Default for SwitchConfig { | 24 | impl Default for SwitchConfig { |
| @@ -22,7 +26,7 @@ impl Default for SwitchConfig { | |||
| 22 | Self { | 26 | Self { |
| 23 | common: Default::default(), | 27 | common: Default::default(), |
| 24 | class: Default::default(), | 28 | class: Default::default(), |
| 25 | publish_on_command: true, | 29 | command_policy: CommandPolicy::default(), |
| 26 | } | 30 | } |
| 27 | } | 31 | } |
| 28 | } | 32 | } |
| @@ -1,16 +1,93 @@ | |||
| 1 | //! Home Assistant MQTT device library for embassy. | 1 | //! MQTT Home Assistant integration library for the [Embassy](https://embassy.dev/) async runtime. |
| 2 | //! | 2 | //! |
| 3 | //! To create a device use the [`new`] function. | 3 | //! # Features |
| 4 | //! | 4 | //! |
| 5 | //! After the device is created you should create one or more entities using functions such as | 5 | //! - Support for multiple entity types: sensors, buttons, switches, binary sensors, numbers, device trackers |
| 6 | //! [`create_button`]/[`create_sensor`]/... | 6 | //! - Built on top of Embassy's async runtime for embedded systems |
| 7 | //! - No-std compatible | ||
| 8 | //! - Automatic MQTT discovery for Home Assistant | ||
| 9 | //! - No runtime allocation | ||
| 7 | //! | 10 | //! |
| 8 | //! Once the entities have been created either [`run`] or [`connect_and_run`] should be called in a | 11 | //! # Installation |
| 9 | //! seperate task. | ||
| 10 | //! | 12 | //! |
| 11 | //! There are various examples you can run locally (ex: `cargo run --features tracing --example | 13 | //! ```bash |
| 12 | //! button`) assuming you have a home assistant instance running. To run the examples the | 14 | //! cargo add embassy-ha |
| 13 | //! environment variable `MQTT_ADDRESS` should be set to the mqtt server used by home assistant. | 15 | //! ``` |
| 16 | //! | ||
| 17 | //! # Quick Start | ||
| 18 | //! | ||
| 19 | //! This example does not compile as-is because it requires device-specific setup, but it should | ||
| 20 | //! be easy to adapt if you already have Embassy running on your microcontroller. | ||
| 21 | //! | ||
| 22 | //! ```no_run | ||
| 23 | //! use embassy_executor::Spawner; | ||
| 24 | //! use embassy_ha::{DeviceConfig, SensorConfig, SensorClass, StateClass}; | ||
| 25 | //! use embassy_time::Timer; | ||
| 26 | //! use static_cell::StaticCell; | ||
| 27 | //! | ||
| 28 | //! static HA_RESOURCES: StaticCell<embassy_ha::DeviceResources> = StaticCell::new(); | ||
| 29 | //! | ||
| 30 | //! #[embassy_executor::main] | ||
| 31 | //! async fn main(spawner: Spawner) { | ||
| 32 | //! // Initialize your network stack | ||
| 33 | //! // This is device specific | ||
| 34 | //! let stack: embassy_net::Stack<'static>; | ||
| 35 | //! # let stack = unsafe { core::mem::zeroed() }; | ||
| 36 | //! | ||
| 37 | //! // Create a Home Assistant device | ||
| 38 | //! let device = embassy_ha::new( | ||
| 39 | //! HA_RESOURCES.init(Default::default()), | ||
| 40 | //! DeviceConfig { | ||
| 41 | //! device_id: "my-device", | ||
| 42 | //! device_name: "My Device", | ||
| 43 | //! manufacturer: "ACME Corp", | ||
| 44 | //! model: "Model X", | ||
| 45 | //! }, | ||
| 46 | //! ); | ||
| 47 | //! | ||
| 48 | //! // Create a temperature sensor | ||
| 49 | //! let sensor_config = SensorConfig { | ||
| 50 | //! class: SensorClass::Temperature, | ||
| 51 | //! state_class: StateClass::Measurement, | ||
| 52 | //! unit: Some(embassy_ha::constants::HA_UNIT_TEMPERATURE_CELSIUS), | ||
| 53 | //! ..Default::default() | ||
| 54 | //! }; | ||
| 55 | //! let mut sensor = embassy_ha::create_sensor(&device, "temp-sensor", sensor_config); | ||
| 56 | //! | ||
| 57 | //! // Spawn the Home Assistant communication task | ||
| 58 | //! spawner.spawn(ha_task(stack, device)).unwrap(); | ||
| 59 | //! | ||
| 60 | //! // Main loop - read and publish temperature | ||
| 61 | //! loop { | ||
| 62 | //! # let temperature = 0.0; | ||
| 63 | //! // let temperature = read_temperature().await; | ||
| 64 | //! sensor.publish(temperature); | ||
| 65 | //! Timer::after_secs(60).await; | ||
| 66 | //! } | ||
| 67 | //! } | ||
| 68 | //! | ||
| 69 | //! #[embassy_executor::task] | ||
| 70 | //! async fn ha_task(stack: embassy_net::Stack<'static>, device: embassy_ha::Device<'static>) { | ||
| 71 | //! embassy_ha::connect_and_run(stack, device, "mqtt-broker-address").await; | ||
| 72 | //! } | ||
| 73 | //! ``` | ||
| 74 | //! | ||
| 75 | //! # Examples | ||
| 76 | //! | ||
| 77 | //! The repository includes several examples demonstrating different entity types. To run an example: | ||
| 78 | //! | ||
| 79 | //! ```bash | ||
| 80 | //! export MQTT_ADDRESS="mqtt://your-mqtt-broker:1883" | ||
| 81 | //! cargo run --example sensor | ||
| 82 | //! ``` | ||
| 83 | //! | ||
| 84 | //! Available examples: | ||
| 85 | //! - `sensor` - Temperature and humidity sensors | ||
| 86 | //! - `button` - Triggerable button entity | ||
| 87 | //! - `switch` - On/off switch control | ||
| 88 | //! - `binary_sensor` - Binary state sensor | ||
| 89 | //! - `number` - Numeric input entity | ||
| 90 | //! - `device_tracker` - Location tracking entity | ||
| 14 | 91 | ||
| 15 | #![no_std] | 92 | #![no_std] |
| 16 | 93 | ||
| @@ -40,6 +117,9 @@ pub mod constants; | |||
| 40 | mod binary_state; | 117 | mod binary_state; |
| 41 | pub use binary_state::*; | 118 | pub use binary_state::*; |
| 42 | 119 | ||
| 120 | mod command_policy; | ||
| 121 | pub use command_policy::*; | ||
| 122 | |||
| 43 | mod entity; | 123 | mod entity; |
| 44 | pub use entity::*; | 124 | pub use entity::*; |
| 45 | 125 | ||
| @@ -305,7 +385,7 @@ pub(crate) struct SwitchState { | |||
| 305 | pub(crate) struct SwitchStorage { | 385 | pub(crate) struct SwitchStorage { |
| 306 | pub state: Option<SwitchState>, | 386 | pub state: Option<SwitchState>, |
| 307 | pub command: Option<SwitchCommand>, | 387 | pub command: Option<SwitchCommand>, |
| 308 | pub publish_on_command: bool, | 388 | pub command_policy: CommandPolicy, |
| 309 | } | 389 | } |
| 310 | 390 | ||
| 311 | #[derive(Debug)] | 391 | #[derive(Debug)] |
| @@ -350,7 +430,7 @@ pub(crate) struct NumberCommand { | |||
| 350 | pub(crate) struct NumberStorage { | 430 | pub(crate) struct NumberStorage { |
| 351 | pub state: Option<NumberState>, | 431 | pub state: Option<NumberState>, |
| 352 | pub command: Option<NumberCommand>, | 432 | pub command: Option<NumberCommand>, |
| 353 | pub publish_on_command: bool, | 433 | pub command_policy: CommandPolicy, |
| 354 | } | 434 | } |
| 355 | 435 | ||
| 356 | #[derive(Debug, Serialize)] | 436 | #[derive(Debug, Serialize)] |
| @@ -594,7 +674,7 @@ pub fn create_number<'a>( | |||
| 594 | device, | 674 | device, |
| 595 | entity_config, | 675 | entity_config, |
| 596 | EntityStorage::Number(NumberStorage { | 676 | EntityStorage::Number(NumberStorage { |
| 597 | publish_on_command: config.publish_on_command, | 677 | command_policy: config.command_policy, |
| 598 | ..Default::default() | 678 | ..Default::default() |
| 599 | }), | 679 | }), |
| 600 | ); | 680 | ); |
| @@ -616,7 +696,7 @@ pub fn create_switch<'a>( | |||
| 616 | device, | 696 | device, |
| 617 | entity_config, | 697 | entity_config, |
| 618 | EntityStorage::Switch(SwitchStorage { | 698 | EntityStorage::Switch(SwitchStorage { |
| 619 | publish_on_command: config.publish_on_command, | 699 | command_policy: config.command_policy, |
| 620 | ..Default::default() | 700 | ..Default::default() |
| 621 | }), | 701 | }), |
| 622 | ); | 702 | ); |
| @@ -661,6 +741,49 @@ pub fn create_device_tracker<'a>( | |||
| 661 | DeviceTracker::new(entity) | 741 | DeviceTracker::new(entity) |
| 662 | } | 742 | } |
| 663 | 743 | ||
| 744 | /// Runs the main Home Assistant device event loop. | ||
| 745 | /// | ||
| 746 | /// This function handles MQTT communication, entity discovery, and state updates. It will run | ||
| 747 | /// until the first error is encountered, at which point it returns immediately. | ||
| 748 | /// | ||
| 749 | /// # Behavior | ||
| 750 | /// | ||
| 751 | /// - Connects to the MQTT broker using the provided transport | ||
| 752 | /// - Publishes discovery messages for all entities | ||
| 753 | /// - Subscribes to command topics for controllable entities | ||
| 754 | /// - Enters the main event loop to handle state updates and commands | ||
| 755 | /// - Returns on the first error (connection loss, timeout, protocol error, etc.) | ||
| 756 | /// | ||
| 757 | /// # Error Handling | ||
| 758 | /// | ||
| 759 | /// This function should be called inside a retry loop, as any network error will cause this | ||
| 760 | /// function to fail. When an error occurs, the transport may be in an invalid state and should | ||
| 761 | /// be re-established before calling `run` again. | ||
| 762 | /// | ||
| 763 | /// # Example | ||
| 764 | /// | ||
| 765 | /// ```no_run | ||
| 766 | /// # use embassy_ha::{Device, Transport}; | ||
| 767 | /// # async fn example(mut device: Device<'_>, create_transport: impl Fn() -> impl Transport) { | ||
| 768 | /// loop { | ||
| 769 | /// let mut transport = create_transport(); | ||
| 770 | /// | ||
| 771 | /// match embassy_ha::run(&mut device, &mut transport).await { | ||
| 772 | /// Ok(()) => { | ||
| 773 | /// // Normal exit (this shouldn't happen in practice) | ||
| 774 | /// break; | ||
| 775 | /// } | ||
| 776 | /// Err(err) => { | ||
| 777 | /// // Log error and retry after delay | ||
| 778 | /// // The transport connection should be re-established | ||
| 779 | /// embassy_time::Timer::after_secs(5).await; | ||
| 780 | /// } | ||
| 781 | /// } | ||
| 782 | /// } | ||
| 783 | /// # } | ||
| 784 | /// ``` | ||
| 785 | /// | ||
| 786 | /// For a higher-level alternative that handles retries automatically, see [`connect_and_run`]. | ||
| 664 | pub async fn run<T: Transport>(device: &mut Device<'_>, transport: &mut T) -> Result<(), Error> { | 787 | pub async fn run<T: Transport>(device: &mut Device<'_>, transport: &mut T) -> Result<(), Error> { |
| 665 | use core::fmt::Write; | 788 | use core::fmt::Write; |
| 666 | 789 | ||
| @@ -1109,7 +1232,7 @@ pub async fn run<T: Transport>(device: &mut Device<'_>, transport: &mut T) -> Re | |||
| 1109 | } | 1232 | } |
| 1110 | }; | 1233 | }; |
| 1111 | let timestamp = embassy_time::Instant::now(); | 1234 | let timestamp = embassy_time::Instant::now(); |
| 1112 | if switch_storage.publish_on_command { | 1235 | if switch_storage.command_policy == CommandPolicy::PublishState { |
| 1113 | data.publish = true; | 1236 | data.publish = true; |
| 1114 | switch_storage.state = Some(SwitchState { | 1237 | switch_storage.state = Some(SwitchState { |
| 1115 | value: command, | 1238 | value: command, |
| @@ -1134,7 +1257,7 @@ pub async fn run<T: Transport>(device: &mut Device<'_>, transport: &mut T) -> Re | |||
| 1134 | } | 1257 | } |
| 1135 | }; | 1258 | }; |
| 1136 | let timestamp = embassy_time::Instant::now(); | 1259 | let timestamp = embassy_time::Instant::now(); |
| 1137 | if number_storage.publish_on_command { | 1260 | if number_storage.command_policy == CommandPolicy::PublishState { |
| 1138 | data.publish = true; | 1261 | data.publish = true; |
| 1139 | number_storage.state = Some(NumberState { | 1262 | number_storage.state = Some(NumberState { |
| 1140 | value: command, | 1263 | value: command, |
| @@ -1156,6 +1279,52 @@ pub async fn run<T: Transport>(device: &mut Device<'_>, transport: &mut T) -> Re | |||
| 1156 | } | 1279 | } |
| 1157 | } | 1280 | } |
| 1158 | 1281 | ||
| 1282 | /// High-level function that manages TCP connections and runs the device event loop with automatic retries. | ||
| 1283 | /// | ||
| 1284 | /// This is a convenience wrapper around [`run`] that handles: | ||
| 1285 | /// - DNS resolution (if hostname is provided) | ||
| 1286 | /// - TCP connection establishment | ||
| 1287 | /// - Automatic reconnection on failure with 5-second delay | ||
| 1288 | /// - Infinite retry loop | ||
| 1289 | /// | ||
| 1290 | /// # Arguments | ||
| 1291 | /// | ||
| 1292 | /// * `stack` - The Embassy network stack for TCP connections | ||
| 1293 | /// * `device` - The Home Assistant device to run | ||
| 1294 | /// * `address` - MQTT broker address in one of these formats: | ||
| 1295 | /// - `"192.168.1.100"` - IPv4 address (uses default port 1883) | ||
| 1296 | /// - `"192.168.1.100:1883"` - IPv4 address with explicit port | ||
| 1297 | /// - `"mqtt.example.com"` - Hostname (uses default port 1883) | ||
| 1298 | /// - `"mqtt.example.com:1883"` - Hostname with explicit port | ||
| 1299 | /// | ||
| 1300 | /// # Returns | ||
| 1301 | /// | ||
| 1302 | /// This function never returns normally (returns `!`). It runs indefinitely, automatically | ||
| 1303 | /// reconnecting on any error. | ||
| 1304 | /// | ||
| 1305 | /// # Example | ||
| 1306 | /// | ||
| 1307 | /// ```no_run | ||
| 1308 | /// # use embassy_executor::Spawner; | ||
| 1309 | /// # use embassy_ha::{Device, DeviceConfig}; | ||
| 1310 | /// # use static_cell::StaticCell; | ||
| 1311 | /// # static HA_RESOURCES: StaticCell<embassy_ha::DeviceResources> = StaticCell::new(); | ||
| 1312 | /// #[embassy_executor::task] | ||
| 1313 | /// async fn ha_task(stack: embassy_net::Stack<'static>) { | ||
| 1314 | /// let device = embassy_ha::new( | ||
| 1315 | /// HA_RESOURCES.init(Default::default()), | ||
| 1316 | /// DeviceConfig { | ||
| 1317 | /// device_id: "my-device", | ||
| 1318 | /// device_name: "My Device", | ||
| 1319 | /// manufacturer: "ACME", | ||
| 1320 | /// model: "X", | ||
| 1321 | /// }, | ||
| 1322 | /// ); | ||
| 1323 | /// | ||
| 1324 | /// // This function never returns | ||
| 1325 | /// embassy_ha::connect_and_run(stack, device, "mqtt.example.com:1883").await; | ||
| 1326 | /// } | ||
| 1327 | /// ``` | ||
| 1159 | pub async fn connect_and_run( | 1328 | pub async fn connect_and_run( |
| 1160 | stack: embassy_net::Stack<'_>, | 1329 | stack: embassy_net::Stack<'_>, |
| 1161 | mut device: Device<'_>, | 1330 | mut device: Device<'_>, |
