ebus_homie/lib.rs
1//! Homie 5 protocol core for eBus.
2//!
3//! The crate performs no I/O: callers pass in received MQTT messages and send the
4//! messages it returns, so the same core serves tokio services and microcontrollers.
5//!
6//! | Module | Contents |
7//! |---|---|
8//! | [`controller`] | The controller state machine: discovery, pacing, device views, events, `/set` and `$broadcast` (`alloc`) |
9//! | [`device`] | The device role: a tree of devices, nodes and properties, publishing and `/set` handling (`alloc`) |
10//! | [`description`] | The `$description` model, parsing, validation and wire form (`alloc`) |
11//! | [`datatype`] | The nine datatypes, payload checks, `/set` parsing and the encoders |
12//! | [`topic`] | Parsing and building `{domain}/5/{device}/...` topics |
13//! | [`id`], [`state`], [`payload`] | Topic-level ids, `$state` values, empty-payload rules |
14//!
15//! A controller runs on the caller's connection and clock. Feed it a link
16//! change and messages, then send what [`poll_command`](controller::Controller::poll_command)
17//! returns and read what [`poll_event`](controller::Controller::poll_event) returns:
18//!
19//! ```
20//! use ebus_homie::controller::{Config, Controller, Event};
21//! use ebus_mqtt::{Command, Instant, Link, Message};
22//!
23//! let mut c = Controller::new(Config::device("meter-1")?)?;
24//! let now = Instant(0);
25//! c.handle_link(now, Link::up(false));
26//! while let Some(cmd) = c.poll_command() {
27//! if let Command::Subscribe { filters, .. } = cmd {
28//! assert!(filters.iter().any(|(f, _)| f == "ebus/5/meter-1/$state"));
29//! }
30//! }
31//!
32//! let retained = |topic, payload: &'static str| Message::new(topic, payload.as_bytes(), Some(true));
33//! c.handle_message(now, retained("ebus/5/meter-1/$state", "ready"));
34//! c.handle_message(
35//! now,
36//! retained(
37//! "ebus/5/meter-1/$description",
38//! r#"{"homie":"5.0","version":1,"nodes":{"power":{"properties":{"watts":{"datatype":"float"}}}}}"#,
39//! ),
40//! );
41//! while c.poll_command().is_some() {}
42//! c.handle_message(now, retained("ebus/5/meter-1/power/watts", "1234.5"));
43//!
44//! let mut synced = false;
45//! while let Some(e) = c.poll_event() {
46//! synced |= matches!(e, Event::DeviceSynced { missing: 0, .. });
47//! }
48//! assert!(synced);
49//! let watts = c.device("meter-1").and_then(|d| d.property("power", "watts"));
50//! assert_eq!(watts.and_then(|p| p.value.as_ref()).map(|v| &*v.raw), Some("1234.5"));
51//! # Ok::<(), Box<dyn std::error::Error>>(())
52//! ```
53//!
54//! # Features
55//!
56//! - `std` (default): links `std`; implies `alloc`.
57//! - `alloc`: the [`controller`], [`device`] and [`description`] modules and
58//! the allocating parts of [`datatype`].
59//! - `log`: diagnostics through the `log` crate.
60
61#![cfg_attr(not(feature = "std"), no_std)]
62#![cfg_attr(docsrs, feature(doc_cfg))]
63
64#[cfg(feature = "alloc")]
65extern crate alloc;
66
67#[cfg(feature = "alloc")]
68mod addr;
69#[cfg(feature = "alloc")]
70pub mod controller;
71pub mod datatype;
72#[cfg(feature = "alloc")]
73pub mod description;
74#[cfg(feature = "alloc")]
75pub mod device;
76pub mod id;
77mod log;
78pub mod payload;
79// Public only for tests/vectors.rs; the `datatype` encoders are the text API.
80#[cfg(feature = "alloc")]
81#[doc(hidden)]
82pub mod pyfmt;
83pub mod state;
84pub mod topic;
85
86#[cfg(feature = "alloc")]
87pub use addr::{Id, PropertyAddr};
88/// The `serde_json` this crate's JSON values ([`serde_json::Value`]) come from.
89#[cfg(feature = "alloc")]
90pub use serde_json;
91
92// Every error type is a `core::error::Error`, with or without `std`.
93const _: () = {
94 const fn is_error<E: core::error::Error>() {}
95 is_error::<datatype::Problem>();
96 is_error::<state::ParseStateError>();
97 is_error::<topic::TopicError>();
98 #[cfg(feature = "alloc")]
99 {
100 is_error::<description::DescriptionError>();
101 is_error::<controller::ConfigError>();
102 is_error::<controller::SetError>();
103 is_error::<device::AlertError>();
104 is_error::<device::BuildError>();
105 is_error::<device::ValueError>();
106 }
107};
108
109/// Homie convention major version, as it appears in topics (`{domain}/5/...`).
110pub const HOMIE_TOPIC_VERSION: &str = "5";
111
112/// Value of the `homie` field in a `$description`.
113pub const HOMIE_DESCRIPTION_VERSION: &str = "5.0";
114
115/// Topic domain the eBus framework uses in place of Homie's default `homie`.
116pub const EBUS_DOMAIN: &str = "ebus";
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121
122 #[test]
123 fn description_version_extends_topic_version() {
124 assert!(HOMIE_DESCRIPTION_VERSION.starts_with(HOMIE_TOPIC_VERSION));
125 }
126}
127
128#[cfg(doctest)]
129#[doc = include_str!("../README.md")]
130struct ReadmeDoctests;