aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/command_policy.rs79
-rw-r--r--src/entity_number.rs10
-rw-r--r--src/entity_switch.rs10
-rw-r--r--src/lib.rs199
4 files changed, 277 insertions, 21 deletions
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)]
67pub 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
75impl 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 @@
1use crate::{ 1use 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)]
65pub struct NumberConfig { 69pub 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
76impl Default for NumberConfig { 80impl 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 @@
1use crate::{ 1use 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)]
14pub struct SwitchConfig { 18pub 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
20impl Default for SwitchConfig { 24impl 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}
diff --git a/src/lib.rs b/src/lib.rs
index 7766171..1b7c79a 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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;
40mod binary_state; 117mod binary_state;
41pub use binary_state::*; 118pub use binary_state::*;
42 119
120mod command_policy;
121pub use command_policy::*;
122
43mod entity; 123mod entity;
44pub use entity::*; 124pub use entity::*;
45 125
@@ -305,7 +385,7 @@ pub(crate) struct SwitchState {
305pub(crate) struct SwitchStorage { 385pub(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 {
350pub(crate) struct NumberStorage { 430pub(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`].
664pub async fn run<T: Transport>(device: &mut Device<'_>, transport: &mut T) -> Result<(), Error> { 787pub 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/// ```
1159pub async fn connect_and_run( 1328pub async fn connect_and_run(
1160 stack: embassy_net::Stack<'_>, 1329 stack: embassy_net::Stack<'_>,
1161 mut device: Device<'_>, 1330 mut device: Device<'_>,