diff options
| author | diogo464 <[email protected]> | 2025-12-14 14:49:02 +0000 |
|---|---|---|
| committer | diogo464 <[email protected]> | 2025-12-14 14:49:02 +0000 |
| commit | 993d2a9fd34ce08760933a013e638108827f6f70 (patch) | |
| tree | 13897ca1a8eac0564fabc5b730bf9ae49a360fbb /src/lib.rs | |
| parent | ab4a7c83e00314a2f5d2f455987ba530fd08bdd7 (diff) | |
Improve documentation and replace publish_on_command with CommandPolicy enum
- Enhanced crate-level documentation with comprehensive examples and feature list
- Improved README with badges, better structure, and clearer examples
- Added README.tpl template and generate-readme.sh script for cargo-readme
- Documented run() and connect_and_run() functions with detailed behavior explanations
- Replaced publish_on_command boolean with CommandPolicy enum (PublishState/Manual)
- Added comprehensive documentation for CommandPolicy explaining both modes
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
Diffstat (limited to 'src/lib.rs')
| -rw-r--r-- | src/lib.rs | 199 |
1 files changed, 184 insertions, 15 deletions
| @@ -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<'_>, |
