mod inbound;
mod key;
mod pacing;
mod replay;
#[cfg(test)]
mod tests;
mod types;
pub use types::{
Config, ConfigError, DeviceView, Discovery, Domain, Event, Handled, Id, Missing, Origin,
Pacing, PropertyAddr, PropertyView, SetError, SetOptions, Value,
};
use alloc::{
collections::{BTreeMap, BTreeSet, VecDeque},
format,
string::String,
sync::Arc,
vec::Vec,
};
use core::ops::Bound;
use ebus_mqtt::{Command, FilterLedger, Instant, QoS};
use crate::HOMIE_TOPIC_VERSION;
use crate::datatype::{self, Current, Datatype};
use crate::description::{Ignored, PropertyDescription};
use crate::id::is_valid_id;
use crate::payload::EMPTY_STRING_PAYLOAD;
use crate::state::{DeviceState, effective_state};
use key::{DevKey, KeyRef};
use types::Slot;
const RETRY_MIN_MS: u64 = 1_000;
const RETRY_MAX_MS: u64 = 30_000;
const WHOLE_DEVICE: [&str; 2] = ["+/+", "+/+/$target"];
const ALERTS: &str = "$alert/+";
#[derive(Clone, Debug, PartialEq, Eq)]
enum Coverage {
None,
All,
Nodes(BTreeSet<Id>),
}
#[derive(Clone, Debug, Default)]
struct Early {
value: Option<(String, Option<bool>)>,
target: Option<(String, Option<bool>)>,
}
#[derive(Clone, Copy, Debug, Default)]
struct Marks {
burst: u8,
seen: u8,
}
#[derive(Clone, Debug)]
struct Device {
view: DeviceView,
raw_hash: Option<[u8; 32]>,
content_hash: Option<[u8; 32]>,
announced: Option<[u8; 32]>,
ignored: Vec<Ignored>,
early: BTreeMap<(String, String), Early>,
marks: BTreeMap<Id, BTreeMap<Id, Marks>>,
coverage: Coverage,
retained_total: u32,
retained_present: u32,
sync_at: Option<Instant>,
sync_expired: bool,
absent: bool,
windows: Vec<Window>,
seen_alerts: BTreeSet<Id>,
changed_while_sleeping: bool,
tree_complete: bool,
root_filter: Option<(String, String)>,
}
impl Device {
fn new(key: &DevKey) -> Self {
Device {
view: DeviceView::new(key.id.clone(), key.domain.clone()),
raw_hash: None,
content_hash: None,
announced: None,
ignored: Vec::new(),
early: BTreeMap::new(),
marks: BTreeMap::new(),
coverage: Coverage::None,
retained_total: 0,
retained_present: 0,
sync_at: None,
sync_expired: false,
absent: false,
windows: Vec::new(),
seen_alerts: BTreeSet::new(),
changed_while_sleeping: false,
tree_complete: false,
root_filter: None,
}
}
fn touch(&mut self) {
self.view.confirmed = true;
self.absent = false;
}
fn root_id(&self) -> Option<&str> {
let root = self.view.description.as_ref()?.root.as_deref()?;
(root != self.view.id.as_str()).then_some(root)
}
fn missing(&self) -> u32 {
self.retained_total.saturating_sub(self.retained_present)
}
fn marks(&self, node: &str, prop: &str) -> Marks {
self.marks
.get(node)
.and_then(|m| m.get(prop))
.copied()
.unwrap_or_default()
}
}
#[derive(Clone, Debug)]
struct Attrs {
filters: Vec<String>,
subscribed_at: Instant,
attempts: u8,
refresh: bool,
props: bool,
}
#[derive(Clone, Debug)]
struct Window {
marker: u64,
nodes: Option<Vec<Id>>,
alerts: bool,
}
#[derive(Clone, Debug)]
struct Burst {
nodes: Option<Vec<Id>>,
expected: u32,
received: u32,
subscribed_at: Instant,
attempts: u8,
last_received: Option<u32>,
}
impl Burst {
fn owed(&self) -> u64 {
(2 * u64::from(self.expected)).saturating_sub(u64::from(self.received))
}
fn covers(&self, node: &str) -> bool {
self.nodes
.as_ref()
.is_none_or(|ns| ns.iter().any(|n| n.as_str() == node))
}
}
#[derive(Clone, Copy, Debug, Default)]
struct PendingEntry {
attempts: u8,
last_received: Option<u32>,
}
#[derive(Clone, Debug, Default)]
struct Holds {
attrs: Vec<String>,
props: Vec<String>,
}
#[derive(Debug)]
pub struct Controller {
cfg: Config,
named: Option<Arc<str>>,
prefix: String,
devices: BTreeMap<DevKey, Device>,
holds: BTreeMap<DevKey, Holds>,
children: BTreeMap<DevKey, BTreeSet<Id>>,
aux_states: BTreeMap<Id, DeviceState>,
ledger: FilterLedger,
discovery_filter: Option<String>,
awaiting: BTreeMap<DevKey, Attrs>,
pending: VecDeque<DevKey>,
pending_entries: BTreeMap<DevKey, PendingEntry>,
draining: BTreeMap<DevKey, Burst>,
commands: VecDeque<Command>,
events: VecDeque<Event>,
connected: bool,
ever_up: bool,
shut_down: bool,
absent_due: Option<Instant>,
retries: Vec<(Instant, Command)>,
backoff_ms: u64,
dropped: BTreeSet<DevKey>,
dropped_discovery: bool,
dropped_due: Option<Instant>,
sync_floor: Option<Instant>,
stuck_floor: Option<Instant>,
requests: BTreeMap<DevKey, replay::Requests>,
seq: u64,
floor: u64,
resumed: bool,
no_retain_bit: bool,
}
impl Controller {
pub fn new(config: Config) -> Result<Self, ConfigError> {
config.validate()?;
let (named, prefix) = match &config.domain {
Domain::Named(d) => (
Some(Arc::<str>::from(d.as_str())),
format!("{d}/{HOMIE_TOPIC_VERSION}/"),
),
Domain::Any => (None, String::new()),
};
let discovery_filter = match (&config.discovery, &config.domain) {
(Discovery::Wildcard, Domain::Named(d)) => {
Some(format!("{d}/{HOMIE_TOPIC_VERSION}/+/$state"))
}
(Discovery::Wildcard, Domain::Any) => Some(format!("+/{HOMIE_TOPIC_VERSION}/+/$state")),
_ => None,
};
Ok(Controller {
ledger: FilterLedger::new(config.host_filters.iter().cloned()),
cfg: config,
named,
prefix,
devices: BTreeMap::new(),
holds: BTreeMap::new(),
children: BTreeMap::new(),
aux_states: BTreeMap::new(),
discovery_filter,
awaiting: BTreeMap::new(),
pending: VecDeque::new(),
pending_entries: BTreeMap::new(),
draining: BTreeMap::new(),
commands: VecDeque::new(),
events: VecDeque::new(),
connected: false,
ever_up: false,
shut_down: false,
absent_due: None,
retries: Vec::new(),
backoff_ms: RETRY_MIN_MS,
dropped: BTreeSet::new(),
dropped_discovery: false,
dropped_due: None,
sync_floor: None,
stuck_floor: None,
requests: BTreeMap::new(),
seq: 0,
floor: 0,
resumed: false,
no_retain_bit: false,
})
}
pub fn config(&self) -> &Config {
&self.cfg
}
pub fn poll_command(&mut self) -> Option<Command> {
self.commands.pop_front()
}
pub fn poll_event(&mut self) -> Option<Event> {
self.events.pop_front()
}
pub fn poll_timeout(&self) -> Option<Instant> {
if self.shut_down {
return None;
}
let mut next: Option<Instant> = None;
let mut consider = |t: Instant| next = Some(next.map_or(t, |n| n.min(t)));
if self.connected {
if let (Some(stuck), Some(t)) = (self.cfg.stuck_timeout_ms, self.stuck_floor) {
consider(t.add_ms(stuck));
}
for (due, _) in &self.retries {
consider(*due);
}
if let Some(t) = self.dropped_due {
consider(t);
}
if let Some(t) = self.absent_due {
consider(t);
}
}
if let Some(t) = self.sync_floor {
consider(t);
}
next
}
fn floor_sync(&mut self, t: Instant) {
self.sync_floor = Some(self.sync_floor.map_or(t, |f| f.min(t)));
}
fn floor_stuck(&mut self, t: Instant) {
self.stuck_floor = Some(self.stuck_floor.map_or(t, |f| f.min(t)));
}
fn recompute_floors(&mut self) {
self.sync_floor = self.devices.values().filter_map(|d| d.sync_at).min();
self.stuck_floor = self
.awaiting
.values()
.map(|a| a.subscribed_at)
.chain(self.draining.values().map(|b| b.subscribed_at))
.min();
}
pub fn wants(&self, topic: &str) -> bool {
match self.named {
Some(_) => topic.starts_with(self.prefix.as_str()),
None => {
let mut levels = topic.splitn(3, '/');
let _domain = levels.next();
levels.next() == Some(HOMIE_TOPIC_VERSION) && levels.next().is_some()
}
}
}
pub fn is_connected(&self) -> bool {
self.connected && !self.shut_down
}
pub fn device(&self, id: &str) -> Option<&DeviceView> {
self.find(id).map(|(_, d)| &d.view)
}
pub fn devices(&self) -> impl Iterator<Item = &DeviceView> + '_ {
self.devices
.values()
.filter(|d| d.view.state.is_some())
.map(|d| &d.view)
}
pub fn devices_with_id<'a>(&'a self, id: &'a str) -> impl Iterator<Item = &'a DeviceView> + 'a {
self.devices
.range::<dyn KeyRef, _>((Bound::Included(&(id, "") as &dyn KeyRef), Bound::Unbounded))
.take_while(move |(k, _)| k.id.as_str() == id)
.filter(|(_, d)| d.view.state.is_some())
.map(|(_, d)| &d.view)
}
pub fn effective_state(&self, id: &str) -> Option<DeviceState> {
self.device(id).and_then(|v| v.effective)
}
pub fn is_tree_complete(&self, root: &str) -> bool {
let Some((key, _)) = self.find_any(root) else {
return false;
};
let key = key.clone();
self.tree_complete(&key)
}
pub fn set(
&mut self,
addr: &PropertyAddr,
payload: &str,
opts: SetOptions,
) -> Result<(), SetError> {
if !self.is_connected() {
return Err(SetError::NotConnected);
}
let (key, dev) = self
.find(addr.device.as_str())
.ok_or(SetError::UnknownDevice)?;
let desc = dev
.view
.description
.as_deref()
.ok_or(SetError::UnknownProperty)?;
let slot = dev
.view
.props
.get(addr.node.as_str())
.and_then(|m| m.get(addr.property.as_str()))
.ok_or(SetError::UnknownProperty)?;
let decl = prop_decl(desc, slot).ok_or(SetError::UnknownProperty)?;
if !decl.settable {
return Err(SetError::NotSettable);
}
let effective = dev.view.effective.unwrap_or(DeviceState::Init);
if effective != DeviceState::Ready && !opts.allow_not_ready {
return Err(SetError::DeviceNotReady(effective));
}
if !opts.unchecked {
let current = slot
.view
.last_valid
.as_deref()
.and_then(|t| current_of(decl.datatype, t));
datatype::parse_set_payload(payload, decl.datatype, decl.format.as_deref(), current)
.map_err(SetError::Invalid)?;
}
let topic = crate::topic::property_set(
&key.domain,
key.id.as_str(),
addr.node.as_str(),
addr.property.as_str(),
);
let payload = if payload.is_empty() && decl.datatype == Datatype::String {
Vec::from(EMPTY_STRING_PAYLOAD)
} else {
Vec::from(payload.as_bytes())
};
crate::log::info!("settingProperty", topic = topic);
self.commands
.push_back(Command::publish(topic, payload, QoS::AtMostOnce, false));
Ok(())
}
pub fn broadcast(&mut self, subtopic: &str, payload: &str) -> Result<(), SetError> {
if !self.is_connected() {
return Err(SetError::NotConnected);
}
let Some(domain) = &self.named else {
return Err(SetError::InvalidTopic);
};
if !subtopic.split('/').all(is_valid_id) {
return Err(SetError::InvalidTopic);
}
let topic = crate::topic::broadcast(domain, subtopic);
crate::log::info!("broadcasting", topic = topic);
self.commands.push_back(Command::publish(
topic,
Vec::from(payload.as_bytes()),
QoS::AtMostOnce,
false,
));
Ok(())
}
pub fn shutdown(&mut self) {
if self.shut_down {
return;
}
crate::log::info!("stoppingController", devices = self.devices.len());
self.shut_down = true;
let cmd = self.ledger.release_all();
self.push(cmd);
self.holds.clear();
self.awaiting.clear();
self.pending.clear();
self.pending_entries.clear();
self.draining.clear();
self.retries.clear();
self.dropped.clear();
self.dropped_discovery = false;
self.dropped_due = None;
self.absent_due = None;
self.sync_floor = None;
self.stuck_floor = None;
self.requests.clear();
for d in self.devices.values_mut() {
d.sync_at = None;
}
}
fn find(&self, id: &str) -> Option<(&DevKey, &Device)> {
match &self.named {
Some(dom) => self
.devices
.get_key_value(&(id, &**dom) as &dyn KeyRef)
.filter(|(_, d)| d.view.state.is_some()),
None => self
.devices
.range::<dyn KeyRef, _>((
Bound::Included(&(id, "") as &dyn KeyRef),
Bound::Unbounded,
))
.take_while(|(k, _)| k.id.as_str() == id)
.find(|(_, d)| d.view.state.is_some()),
}
}
fn find_any(&self, id: &str) -> Option<(&DevKey, &Device)> {
match &self.named {
Some(dom) => self.devices.get_key_value(&(id, &**dom) as &dyn KeyRef),
None => self
.devices
.range::<dyn KeyRef, _>((
Bound::Included(&(id, "") as &dyn KeyRef),
Bound::Unbounded,
))
.next()
.filter(|(k, _)| k.id.as_str() == id),
}
}
fn key_of(&self, id: &str, domain: &str) -> Option<DevKey> {
self.devices
.get_key_value(&(id, domain) as &dyn KeyRef)
.map(|(k, _)| k.clone())
}
fn named_key(&self, id: &Id) -> Option<DevKey> {
self.named.as_ref().map(|d| DevKey {
id: id.clone(),
domain: d.clone(),
})
}
fn topic_filter(domain: &str, id: &str, suffix: &str) -> String {
format!("{domain}/{HOMIE_TOPIC_VERSION}/{id}/{suffix}")
}
fn device_filter(key: &DevKey, suffix: &str) -> String {
Self::topic_filter(&key.domain, key.id.as_str(), suffix)
}
fn attr_filters(&self, key: &DevKey) -> Vec<String> {
let mut out = Vec::with_capacity(3);
if !matches!(self.cfg.discovery, Discovery::Wildcard) {
out.push(Self::device_filter(key, "$state"));
}
out.push(Self::device_filter(key, "$description"));
if let Some((_, f)) = self.devices.get(key).and_then(|d| d.root_filter.clone()) {
out.push(f);
}
out
}
fn prop_filters(key: &DevKey, nodes: Option<&[Id]>) -> Vec<String> {
match nodes {
None => WHOLE_DEVICE
.iter()
.map(|s| Self::device_filter(key, s))
.collect(),
Some(nodes) => nodes
.iter()
.flat_map(|n| {
[
Self::device_filter(key, &format!("{n}/+")),
Self::device_filter(key, &format!("{n}/+/$target")),
]
})
.collect(),
}
}
fn push(&mut self, cmd: Option<Command>) {
let Some(cmd) = cmd else {
return;
};
if let Command::Subscribe { filters, .. } = &cmd {
self.number_subscribe(filters);
}
self.commands.push_back(cmd);
}
fn hold(&mut self, key: &DevKey, attrs: Vec<String>, props: Vec<String>, marker: bool) {
let qos = self.cfg.subscribe_qos;
let holds = self.holds.entry(key.clone()).or_default();
let mut order: Vec<String> = Vec::with_capacity(attrs.len() + props.len() + 1);
let mut fresh = Vec::new();
let mut again = Vec::new();
for (list, filters) in [(&mut holds.attrs, attrs), (&mut holds.props, props)] {
for f in filters {
order.push(f.clone());
if list.contains(&f) {
again.push(f);
} else {
list.push(f.clone());
fresh.push((f, qos));
}
}
}
if marker {
let f = Self::device_filter(key, "$description");
order.push(f.clone());
again.push(f);
}
let mut filters = Vec::new();
for cmd in [
self.ledger.subscribe(fresh),
self.ledger.resubscribe(again.iter()),
] {
if let Some(Command::Subscribe { filters: more, .. }) = cmd {
for (f, q) in more {
match filters
.iter_mut()
.find(|(g, _): &&mut (String, QoS)| *g == f)
{
Some(e) => e.1 = e.1.max(q),
None => filters.push((f, q)),
}
}
}
}
if filters.is_empty() {
return;
}
filters.sort_by_key(|(f, _)| order.iter().position(|o| o == f));
self.push(Some(Command::subscribe(filters)));
}
fn resubscribe(&mut self, filters: &[String]) {
let cmd = self.ledger.resubscribe(filters.iter());
self.push(cmd);
}
fn release_props(&mut self, key: &DevKey) {
let Some(h) = self.holds.get_mut(key) else {
return;
};
let props = core::mem::take(&mut h.props);
let cmd = self.ledger.unsubscribe(props);
self.push(cmd);
}
fn release_all(&mut self, key: &DevKey) {
let Some(h) = self.holds.remove(key) else {
return;
};
let mut all = h.props;
all.extend(h.attrs);
let cmd = self.ledger.unsubscribe(all);
self.push(cmd);
}
fn root_state(&self, key: &DevKey, dev: &Device) -> Option<DeviceState> {
let root = dev.root_id()?;
self.devices
.get(&(root, &*key.domain) as &dyn KeyRef)
.and_then(|r| r.view.state)
.or_else(|| self.aux_states.get(root).copied())
}
fn effective_of(&self, key: &DevKey) -> Option<DeviceState> {
let dev = self.devices.get(key)?;
let own = dev.view.state?;
Some(effective_state(self.root_state(key, dev), own))
}
}
fn prop_decl<'a>(
desc: &'a crate::description::DeviceDescription,
slot: &Slot,
) -> Option<&'a PropertyDescription> {
desc.nodes
.get(slot.node_idx)?
.1
.properties
.get(slot.prop_idx)
.map(|(_, p)| p)
}
fn current_of(datatype: Datatype, text: &str) -> Option<Current> {
match datatype {
Datatype::Integer => text.parse().ok().map(Current::Integer),
Datatype::Float => text.parse().ok().map(Current::Float),
_ => None,
}
}
fn is_set_level(s: &str) -> bool {
s.ends_with("/set")
}
impl ebus_mqtt::Role for Controller {
fn handle_link(&mut self, now: Instant, link: ebus_mqtt::Link) {
Controller::handle_link(self, now, link);
}
fn handle_message(&mut self, now: Instant, msg: ebus_mqtt::Message<'_>) -> Handled {
Controller::handle_message(self, now, msg)
}
fn handle_command_failed(&mut self, now: Instant, cmd: Command) {
Controller::handle_command_failed(self, now, cmd);
}
fn handle_timeout(&mut self, now: Instant) {
Controller::handle_timeout(self, now);
}
fn handle_dropped(&mut self, now: Instant, topic: &str) {
Controller::handle_dropped(self, now, topic);
}
fn handle_lost(&mut self, topic: &str) {
Controller::handle_lost(self, topic);
}
fn poll_command(&mut self) -> Option<Command> {
Controller::poll_command(self)
}
fn poll_timeout(&self) -> Option<Instant> {
Controller::poll_timeout(self)
}
fn wants(&self, topic: &str) -> bool {
Controller::wants(self, topic)
}
fn owns(&self, cmd: &Command) -> bool {
match cmd {
Command::Publish { topic, .. } => {
self.wants(topic)
&& (is_set_level(topic) || topic.split('/').nth(2) == Some("$broadcast"))
}
Command::Subscribe { filters, .. } => filters
.iter()
.any(|(f, _)| self.wants(f) && !is_set_level(f)),
Command::Unsubscribe { filters, .. } => {
filters.iter().any(|f| self.wants(f) && !is_set_level(f))
}
_ => false,
}
}
fn holds(&self, filter: &str) -> bool {
self.ledger.count(filter) > 0
}
}