1mod inbound;
29mod key;
30mod pacing;
31mod replay;
32#[cfg(test)]
33mod tests;
34mod types;
35
36pub use types::{
37 Config, ConfigError, DeviceView, Discovery, Domain, Event, Handled, Id, Missing, Origin,
38 Pacing, PropertyAddr, PropertyView, SetError, SetOptions, Value,
39};
40
41use alloc::{
42 collections::{BTreeMap, BTreeSet, VecDeque},
43 format,
44 string::String,
45 sync::Arc,
46 vec::Vec,
47};
48use core::ops::Bound;
49
50use ebus_mqtt::{Command, FilterLedger, Instant, QoS};
51
52use crate::HOMIE_TOPIC_VERSION;
53use crate::datatype::{self, Current, Datatype};
54use crate::description::{Ignored, PropertyDescription};
55use crate::id::is_valid_id;
56use crate::payload::EMPTY_STRING_PAYLOAD;
57use crate::state::{DeviceState, effective_state};
58use key::{DevKey, KeyRef};
59use types::Slot;
60
61const RETRY_MIN_MS: u64 = 1_000;
64const RETRY_MAX_MS: u64 = 30_000;
65
66const WHOLE_DEVICE: [&str; 2] = ["+/+", "+/+/$target"];
68
69const ALERTS: &str = "$alert/+";
71
72#[derive(Clone, Debug, PartialEq, Eq)]
74enum Coverage {
75 None,
76 All,
77 Nodes(BTreeSet<Id>),
78}
79
80#[derive(Clone, Debug, Default)]
82struct Early {
83 value: Option<(String, Option<bool>)>,
84 target: Option<(String, Option<bool>)>,
85}
86
87#[derive(Clone, Copy, Debug, Default)]
91struct Marks {
92 burst: u8,
94 seen: u8,
96}
97
98#[derive(Clone, Debug)]
101struct Device {
102 view: DeviceView,
103 raw_hash: Option<[u8; 32]>,
105 content_hash: Option<[u8; 32]>,
107 announced: Option<[u8; 32]>,
109 ignored: Vec<Ignored>,
110 early: BTreeMap<(String, String), Early>,
111 marks: BTreeMap<Id, BTreeMap<Id, Marks>>,
113 coverage: Coverage,
114 retained_total: u32,
115 retained_present: u32,
116 sync_at: Option<Instant>,
118 sync_expired: bool,
119 absent: bool,
120 windows: Vec<Window>,
122 seen_alerts: BTreeSet<Id>,
124 changed_while_sleeping: bool,
127 tree_complete: bool,
128 root_filter: Option<(String, String)>,
131}
132
133impl Device {
134 fn new(key: &DevKey) -> Self {
135 Device {
136 view: DeviceView::new(key.id.clone(), key.domain.clone()),
137 raw_hash: None,
138 content_hash: None,
139 announced: None,
140 ignored: Vec::new(),
141 early: BTreeMap::new(),
142 marks: BTreeMap::new(),
143 coverage: Coverage::None,
144 retained_total: 0,
145 retained_present: 0,
146 sync_at: None,
147 sync_expired: false,
148 absent: false,
149 windows: Vec::new(),
150 seen_alerts: BTreeSet::new(),
151 changed_while_sleeping: false,
152 tree_complete: false,
153 root_filter: None,
154 }
155 }
156
157 fn touch(&mut self) {
158 self.view.confirmed = true;
159 self.absent = false;
160 }
161
162 fn root_id(&self) -> Option<&str> {
164 let root = self.view.description.as_ref()?.root.as_deref()?;
165 (root != self.view.id.as_str()).then_some(root)
166 }
167
168 fn missing(&self) -> u32 {
169 self.retained_total.saturating_sub(self.retained_present)
170 }
171
172 fn marks(&self, node: &str, prop: &str) -> Marks {
173 self.marks
174 .get(node)
175 .and_then(|m| m.get(prop))
176 .copied()
177 .unwrap_or_default()
178 }
179}
180
181#[derive(Clone, Debug)]
183struct Attrs {
184 filters: Vec<String>,
185 subscribed_at: Instant,
186 attempts: u8,
187 refresh: bool,
189 props: bool,
191}
192
193#[derive(Clone, Debug)]
197struct Window {
198 marker: u64,
200 nodes: Option<Vec<Id>>,
202 alerts: bool,
204}
205
206#[derive(Clone, Debug)]
209struct Burst {
210 nodes: Option<Vec<Id>>,
212 expected: u32,
214 received: u32,
216 subscribed_at: Instant,
217 attempts: u8,
218 last_received: Option<u32>,
220}
221
222impl Burst {
223 fn owed(&self) -> u64 {
224 (2 * u64::from(self.expected)).saturating_sub(u64::from(self.received))
225 }
226
227 fn covers(&self, node: &str) -> bool {
228 self.nodes
229 .as_ref()
230 .is_none_or(|ns| ns.iter().any(|n| n.as_str() == node))
231 }
232}
233
234#[derive(Clone, Copy, Debug, Default)]
236struct PendingEntry {
237 attempts: u8,
238 last_received: Option<u32>,
239}
240
241#[derive(Clone, Debug, Default)]
243struct Holds {
244 attrs: Vec<String>,
246 props: Vec<String>,
248}
249
250#[derive(Debug)]
252pub struct Controller {
253 cfg: Config,
254 named: Option<Arc<str>>,
256 prefix: String,
258 devices: BTreeMap<DevKey, Device>,
259 holds: BTreeMap<DevKey, Holds>,
260 children: BTreeMap<DevKey, BTreeSet<Id>>,
262 aux_states: BTreeMap<Id, DeviceState>,
264 ledger: FilterLedger,
265 discovery_filter: Option<String>,
267 awaiting: BTreeMap<DevKey, Attrs>,
268 pending: VecDeque<DevKey>,
269 pending_entries: BTreeMap<DevKey, PendingEntry>,
270 draining: BTreeMap<DevKey, Burst>,
271 commands: VecDeque<Command>,
272 events: VecDeque<Event>,
273 connected: bool,
274 ever_up: bool,
275 shut_down: bool,
276 absent_due: Option<Instant>,
277 retries: Vec<(Instant, Command)>,
278 backoff_ms: u64,
279 dropped: BTreeSet<DevKey>,
280 dropped_discovery: bool,
281 dropped_due: Option<Instant>,
282 sync_floor: Option<Instant>,
284 stuck_floor: Option<Instant>,
286 requests: BTreeMap<DevKey, replay::Requests>,
289 seq: u64,
291 floor: u64,
293 resumed: bool,
295 no_retain_bit: bool,
297}
298
299impl Controller {
300 pub fn new(config: Config) -> Result<Self, ConfigError> {
304 config.validate()?;
305 let (named, prefix) = match &config.domain {
306 Domain::Named(d) => (
307 Some(Arc::<str>::from(d.as_str())),
308 format!("{d}/{HOMIE_TOPIC_VERSION}/"),
309 ),
310 Domain::Any => (None, String::new()),
311 };
312 let discovery_filter = match (&config.discovery, &config.domain) {
313 (Discovery::Wildcard, Domain::Named(d)) => {
314 Some(format!("{d}/{HOMIE_TOPIC_VERSION}/+/$state"))
315 }
316 (Discovery::Wildcard, Domain::Any) => Some(format!("+/{HOMIE_TOPIC_VERSION}/+/$state")),
317 _ => None,
318 };
319 Ok(Controller {
320 ledger: FilterLedger::new(config.host_filters.iter().cloned()),
321 cfg: config,
322 named,
323 prefix,
324 devices: BTreeMap::new(),
325 holds: BTreeMap::new(),
326 children: BTreeMap::new(),
327 aux_states: BTreeMap::new(),
328 discovery_filter,
329 awaiting: BTreeMap::new(),
330 pending: VecDeque::new(),
331 pending_entries: BTreeMap::new(),
332 draining: BTreeMap::new(),
333 commands: VecDeque::new(),
334 events: VecDeque::new(),
335 connected: false,
336 ever_up: false,
337 shut_down: false,
338 absent_due: None,
339 retries: Vec::new(),
340 backoff_ms: RETRY_MIN_MS,
341 dropped: BTreeSet::new(),
342 dropped_discovery: false,
343 dropped_due: None,
344 sync_floor: None,
345 stuck_floor: None,
346 requests: BTreeMap::new(),
347 seq: 0,
348 floor: 0,
349 resumed: false,
350 no_retain_bit: false,
351 })
352 }
353
354 pub fn config(&self) -> &Config {
356 &self.cfg
357 }
358
359 pub fn poll_command(&mut self) -> Option<Command> {
361 self.commands.pop_front()
362 }
363
364 pub fn poll_event(&mut self) -> Option<Event> {
366 self.events.pop_front()
367 }
368
369 pub fn poll_timeout(&self) -> Option<Instant> {
375 if self.shut_down {
376 return None;
377 }
378 let mut next: Option<Instant> = None;
379 let mut consider = |t: Instant| next = Some(next.map_or(t, |n| n.min(t)));
380 if self.connected {
381 if let (Some(stuck), Some(t)) = (self.cfg.stuck_timeout_ms, self.stuck_floor) {
382 consider(t.add_ms(stuck));
383 }
384 for (due, _) in &self.retries {
385 consider(*due);
386 }
387 if let Some(t) = self.dropped_due {
388 consider(t);
389 }
390 if let Some(t) = self.absent_due {
391 consider(t);
392 }
393 }
394 if let Some(t) = self.sync_floor {
395 consider(t);
396 }
397 next
398 }
399
400 fn floor_sync(&mut self, t: Instant) {
402 self.sync_floor = Some(self.sync_floor.map_or(t, |f| f.min(t)));
403 }
404
405 fn floor_stuck(&mut self, t: Instant) {
407 self.stuck_floor = Some(self.stuck_floor.map_or(t, |f| f.min(t)));
408 }
409
410 fn recompute_floors(&mut self) {
412 self.sync_floor = self.devices.values().filter_map(|d| d.sync_at).min();
413 self.stuck_floor = self
414 .awaiting
415 .values()
416 .map(|a| a.subscribed_at)
417 .chain(self.draining.values().map(|b| b.subscribed_at))
418 .min();
419 }
420
421 pub fn wants(&self, topic: &str) -> bool {
424 match self.named {
425 Some(_) => topic.starts_with(self.prefix.as_str()),
426 None => {
427 let mut levels = topic.splitn(3, '/');
428 let _domain = levels.next();
429 levels.next() == Some(HOMIE_TOPIC_VERSION) && levels.next().is_some()
430 }
431 }
432 }
433
434 pub fn is_connected(&self) -> bool {
437 self.connected && !self.shut_down
438 }
439
440 pub fn device(&self, id: &str) -> Option<&DeviceView> {
444 self.find(id).map(|(_, d)| &d.view)
445 }
446
447 pub fn devices(&self) -> impl Iterator<Item = &DeviceView> + '_ {
449 self.devices
450 .values()
451 .filter(|d| d.view.state.is_some())
452 .map(|d| &d.view)
453 }
454
455 pub fn devices_with_id<'a>(&'a self, id: &'a str) -> impl Iterator<Item = &'a DeviceView> + 'a {
459 self.devices
460 .range::<dyn KeyRef, _>((Bound::Included(&(id, "") as &dyn KeyRef), Bound::Unbounded))
461 .take_while(move |(k, _)| k.id.as_str() == id)
462 .filter(|(_, d)| d.view.state.is_some())
463 .map(|(_, d)| &d.view)
464 }
465
466 pub fn effective_state(&self, id: &str) -> Option<DeviceState> {
468 self.device(id).and_then(|v| v.effective)
469 }
470
471 pub fn is_tree_complete(&self, root: &str) -> bool {
475 let Some((key, _)) = self.find_any(root) else {
476 return false;
477 };
478 let key = key.clone();
479 self.tree_complete(&key)
480 }
481
482 pub fn set(
496 &mut self,
497 addr: &PropertyAddr,
498 payload: &str,
499 opts: SetOptions,
500 ) -> Result<(), SetError> {
501 if !self.is_connected() {
502 return Err(SetError::NotConnected);
503 }
504 let (key, dev) = self
505 .find(addr.device.as_str())
506 .ok_or(SetError::UnknownDevice)?;
507 let desc = dev
508 .view
509 .description
510 .as_deref()
511 .ok_or(SetError::UnknownProperty)?;
512 let slot = dev
513 .view
514 .props
515 .get(addr.node.as_str())
516 .and_then(|m| m.get(addr.property.as_str()))
517 .ok_or(SetError::UnknownProperty)?;
518 let decl = prop_decl(desc, slot).ok_or(SetError::UnknownProperty)?;
519 if !decl.settable {
520 return Err(SetError::NotSettable);
521 }
522 let effective = dev.view.effective.unwrap_or(DeviceState::Init);
523 if effective != DeviceState::Ready && !opts.allow_not_ready {
524 return Err(SetError::DeviceNotReady(effective));
525 }
526 if !opts.unchecked {
527 let current = slot
528 .view
529 .last_valid
530 .as_deref()
531 .and_then(|t| current_of(decl.datatype, t));
532 datatype::parse_set_payload(payload, decl.datatype, decl.format.as_deref(), current)
533 .map_err(SetError::Invalid)?;
534 }
535 let topic = crate::topic::property_set(
536 &key.domain,
537 key.id.as_str(),
538 addr.node.as_str(),
539 addr.property.as_str(),
540 );
541 let payload = if payload.is_empty() && decl.datatype == Datatype::String {
542 Vec::from(EMPTY_STRING_PAYLOAD)
543 } else {
544 Vec::from(payload.as_bytes())
545 };
546 crate::log::info!("settingProperty", topic = topic);
547 self.commands
548 .push_back(Command::publish(topic, payload, QoS::AtMostOnce, false));
549 Ok(())
550 }
551
552 pub fn broadcast(&mut self, subtopic: &str, payload: &str) -> Result<(), SetError> {
556 if !self.is_connected() {
557 return Err(SetError::NotConnected);
558 }
559 let Some(domain) = &self.named else {
560 return Err(SetError::InvalidTopic);
561 };
562 if !subtopic.split('/').all(is_valid_id) {
563 return Err(SetError::InvalidTopic);
564 }
565 let topic = crate::topic::broadcast(domain, subtopic);
566 crate::log::info!("broadcasting", topic = topic);
567 self.commands.push_back(Command::publish(
568 topic,
569 Vec::from(payload.as_bytes()),
570 QoS::AtMostOnce,
571 false,
572 ));
573 Ok(())
574 }
575
576 pub fn shutdown(&mut self) {
581 if self.shut_down {
582 return;
583 }
584 crate::log::info!("stoppingController", devices = self.devices.len());
585 self.shut_down = true;
586 let cmd = self.ledger.release_all();
587 self.push(cmd);
588 self.holds.clear();
589 self.awaiting.clear();
590 self.pending.clear();
591 self.pending_entries.clear();
592 self.draining.clear();
593 self.retries.clear();
594 self.dropped.clear();
595 self.dropped_discovery = false;
596 self.dropped_due = None;
597 self.absent_due = None;
598 self.sync_floor = None;
599 self.stuck_floor = None;
600 self.requests.clear();
601 for d in self.devices.values_mut() {
602 d.sync_at = None;
603 }
604 }
605
606 fn find(&self, id: &str) -> Option<(&DevKey, &Device)> {
610 match &self.named {
611 Some(dom) => self
612 .devices
613 .get_key_value(&(id, &**dom) as &dyn KeyRef)
614 .filter(|(_, d)| d.view.state.is_some()),
615 None => self
616 .devices
617 .range::<dyn KeyRef, _>((
618 Bound::Included(&(id, "") as &dyn KeyRef),
619 Bound::Unbounded,
620 ))
621 .take_while(|(k, _)| k.id.as_str() == id)
622 .find(|(_, d)| d.view.state.is_some()),
623 }
624 }
625
626 fn find_any(&self, id: &str) -> Option<(&DevKey, &Device)> {
628 match &self.named {
629 Some(dom) => self.devices.get_key_value(&(id, &**dom) as &dyn KeyRef),
630 None => self
631 .devices
632 .range::<dyn KeyRef, _>((
633 Bound::Included(&(id, "") as &dyn KeyRef),
634 Bound::Unbounded,
635 ))
636 .next()
637 .filter(|(k, _)| k.id.as_str() == id),
638 }
639 }
640
641 fn key_of(&self, id: &str, domain: &str) -> Option<DevKey> {
642 self.devices
643 .get_key_value(&(id, domain) as &dyn KeyRef)
644 .map(|(k, _)| k.clone())
645 }
646
647 fn named_key(&self, id: &Id) -> Option<DevKey> {
649 self.named.as_ref().map(|d| DevKey {
650 id: id.clone(),
651 domain: d.clone(),
652 })
653 }
654
655 fn topic_filter(domain: &str, id: &str, suffix: &str) -> String {
658 format!("{domain}/{HOMIE_TOPIC_VERSION}/{id}/{suffix}")
659 }
660
661 fn device_filter(key: &DevKey, suffix: &str) -> String {
662 Self::topic_filter(&key.domain, key.id.as_str(), suffix)
663 }
664
665 fn attr_filters(&self, key: &DevKey) -> Vec<String> {
668 let mut out = Vec::with_capacity(3);
669 if !matches!(self.cfg.discovery, Discovery::Wildcard) {
670 out.push(Self::device_filter(key, "$state"));
671 }
672 out.push(Self::device_filter(key, "$description"));
673 if let Some((_, f)) = self.devices.get(key).and_then(|d| d.root_filter.clone()) {
674 out.push(f);
675 }
676 out
677 }
678
679 fn prop_filters(key: &DevKey, nodes: Option<&[Id]>) -> Vec<String> {
681 match nodes {
682 None => WHOLE_DEVICE
683 .iter()
684 .map(|s| Self::device_filter(key, s))
685 .collect(),
686 Some(nodes) => nodes
687 .iter()
688 .flat_map(|n| {
689 [
690 Self::device_filter(key, &format!("{n}/+")),
691 Self::device_filter(key, &format!("{n}/+/$target")),
692 ]
693 })
694 .collect(),
695 }
696 }
697
698 fn push(&mut self, cmd: Option<Command>) {
699 let Some(cmd) = cmd else {
700 return;
701 };
702 if let Command::Subscribe { filters, .. } = &cmd {
703 self.number_subscribe(filters);
704 }
705 self.commands.push_back(cmd);
706 }
707
708 fn hold(&mut self, key: &DevKey, attrs: Vec<String>, props: Vec<String>, marker: bool) {
713 let qos = self.cfg.subscribe_qos;
714 let holds = self.holds.entry(key.clone()).or_default();
715 let mut order: Vec<String> = Vec::with_capacity(attrs.len() + props.len() + 1);
716 let mut fresh = Vec::new();
717 let mut again = Vec::new();
718 for (list, filters) in [(&mut holds.attrs, attrs), (&mut holds.props, props)] {
719 for f in filters {
720 order.push(f.clone());
721 if list.contains(&f) {
722 again.push(f);
723 } else {
724 list.push(f.clone());
725 fresh.push((f, qos));
726 }
727 }
728 }
729 if marker {
730 let f = Self::device_filter(key, "$description");
731 order.push(f.clone());
732 again.push(f);
733 }
734 let mut filters = Vec::new();
735 for cmd in [
736 self.ledger.subscribe(fresh),
737 self.ledger.resubscribe(again.iter()),
738 ] {
739 if let Some(Command::Subscribe { filters: more, .. }) = cmd {
740 for (f, q) in more {
741 match filters
742 .iter_mut()
743 .find(|(g, _): &&mut (String, QoS)| *g == f)
744 {
745 Some(e) => e.1 = e.1.max(q),
746 None => filters.push((f, q)),
747 }
748 }
749 }
750 }
751 if filters.is_empty() {
752 return;
753 }
754 filters.sort_by_key(|(f, _)| order.iter().position(|o| o == f));
755 self.push(Some(Command::subscribe(filters)));
756 }
757
758 fn resubscribe(&mut self, filters: &[String]) {
761 let cmd = self.ledger.resubscribe(filters.iter());
762 self.push(cmd);
763 }
764
765 fn release_props(&mut self, key: &DevKey) {
766 let Some(h) = self.holds.get_mut(key) else {
767 return;
768 };
769 let props = core::mem::take(&mut h.props);
770 let cmd = self.ledger.unsubscribe(props);
771 self.push(cmd);
772 }
773
774 fn release_all(&mut self, key: &DevKey) {
775 let Some(h) = self.holds.remove(key) else {
776 return;
777 };
778 let mut all = h.props;
779 all.extend(h.attrs);
780 let cmd = self.ledger.unsubscribe(all);
781 self.push(cmd);
782 }
783
784 fn root_state(&self, key: &DevKey, dev: &Device) -> Option<DeviceState> {
788 let root = dev.root_id()?;
789 self.devices
790 .get(&(root, &*key.domain) as &dyn KeyRef)
791 .and_then(|r| r.view.state)
792 .or_else(|| self.aux_states.get(root).copied())
793 }
794
795 fn effective_of(&self, key: &DevKey) -> Option<DeviceState> {
796 let dev = self.devices.get(key)?;
797 let own = dev.view.state?;
798 Some(effective_state(self.root_state(key, dev), own))
799 }
800}
801
802fn prop_decl<'a>(
804 desc: &'a crate::description::DeviceDescription,
805 slot: &Slot,
806) -> Option<&'a PropertyDescription> {
807 desc.nodes
808 .get(slot.node_idx)?
809 .1
810 .properties
811 .get(slot.prop_idx)
812 .map(|(_, p)| p)
813}
814
815fn current_of(datatype: Datatype, text: &str) -> Option<Current> {
817 match datatype {
818 Datatype::Integer => text.parse().ok().map(Current::Integer),
819 Datatype::Float => text.parse().ok().map(Current::Float),
820 _ => None,
821 }
822}
823
824fn is_set_level(s: &str) -> bool {
827 s.ends_with("/set")
828}
829
830impl ebus_mqtt::Role for Controller {
831 fn handle_link(&mut self, now: Instant, link: ebus_mqtt::Link) {
832 Controller::handle_link(self, now, link);
833 }
834
835 fn handle_message(&mut self, now: Instant, msg: ebus_mqtt::Message<'_>) -> Handled {
836 Controller::handle_message(self, now, msg)
837 }
838
839 fn handle_command_failed(&mut self, now: Instant, cmd: Command) {
840 Controller::handle_command_failed(self, now, cmd);
841 }
842
843 fn handle_timeout(&mut self, now: Instant) {
844 Controller::handle_timeout(self, now);
845 }
846
847 fn handle_dropped(&mut self, now: Instant, topic: &str) {
848 Controller::handle_dropped(self, now, topic);
849 }
850
851 fn handle_lost(&mut self, topic: &str) {
852 Controller::handle_lost(self, topic);
853 }
854
855 fn poll_command(&mut self) -> Option<Command> {
856 Controller::poll_command(self)
857 }
858
859 fn poll_timeout(&self) -> Option<Instant> {
860 Controller::poll_timeout(self)
861 }
862
863 fn wants(&self, topic: &str) -> bool {
864 Controller::wants(self, topic)
865 }
866
867 fn owns(&self, cmd: &Command) -> bool {
871 match cmd {
872 Command::Publish { topic, .. } => {
873 self.wants(topic)
874 && (is_set_level(topic) || topic.split('/').nth(2) == Some("$broadcast"))
875 }
876 Command::Subscribe { filters, .. } => filters
877 .iter()
878 .any(|(f, _)| self.wants(f) && !is_set_level(f)),
879 Command::Unsubscribe { filters, .. } => {
880 filters.iter().any(|f| self.wants(f) && !is_set_level(f))
881 }
882 _ => false,
883 }
884 }
885
886 fn holds(&self, filter: &str) -> bool {
887 self.ledger.count(filter) > 0
888 }
889}