Skip to main content

ebus_homie/controller/
inbound.rs

1//! Received messages: `$state`, `$description`, values, targets, alerts and
2//! logs; removal, tree reconciliation, effective state and sync.
3
4use alloc::{
5    collections::{BTreeSet, VecDeque},
6    string::String,
7    sync::Arc,
8    vec::Vec,
9};
10
11use ebus_mqtt::{Instant, Link, Message};
12use sha2::{Digest, Sha256};
13
14use super::key::{DevKey, KeyRef};
15use super::replay;
16use super::types::{Props, Slot};
17use super::{Controller, Device, Discovery, Event, Handled, Id, PropertyAddr, Value, prop_decl};
18use super::{Origin, PropertyView};
19use crate::datatype::check_value;
20use crate::description::{self, DescriptionError, DeviceDescription, Ignored};
21use crate::payload::{self, Received};
22use crate::state::{DeviceState, effective_state};
23use crate::topic::{self, TopicKind};
24use rpds::RedBlackTreeMapSync;
25
26/// The event address of a slot.
27pub(super) fn addr(key: &DevKey, slot: &Slot) -> PropertyAddr {
28    PropertyAddr {
29        device: key.id.clone(),
30        node: slot.node.clone(),
31        property: slot.prop.clone(),
32    }
33}
34
35/// Stores `slot` at its node and property, copying only the path to it.
36pub(super) fn put_slot(props: &mut Props, slot: Slot) {
37    if let Some(s) = props
38        .get_mut(slot.node.as_str())
39        .and_then(|m| m.get_mut(slot.prop.as_str()))
40    {
41        *s = slot;
42    }
43}
44
45/// Rebuilds a device's property slots for a new description: values of
46/// properties still declared are kept and checked again, the rest dropped. A
47/// slot that comes out unchanged keeps its entry.
48fn rebuild_slots(dev: &mut Device, desc: &DeviceDescription) -> usize {
49    let before: usize = dev.view.props.values().map(RedBlackTreeMapSync::size).sum();
50    let mut props = dev.view.props.clone();
51    let mut nodes: BTreeSet<&str> = BTreeSet::new();
52    let (mut total, mut present, mut kept) = (0u32, 0u32, 0usize);
53    for (ni, (nid, node)) in desc.nodes.iter().enumerate() {
54        let Some(node_id) = Id::new(nid) else {
55            continue;
56        };
57        nodes.insert(nid.as_str());
58        let old_map = props.get(nid.as_str()).cloned();
59        let mut map = old_map
60            .clone()
61            .unwrap_or_else(RedBlackTreeMapSync::new_sync);
62        let declared: BTreeSet<&str> = node.properties.iter().map(|(p, _)| p.as_str()).collect();
63        let gone: Vec<Id> = map
64            .keys()
65            .filter(|p| !declared.contains(p.as_str()))
66            .cloned()
67            .collect();
68        for p in gone {
69            map.remove_mut(p.as_str());
70        }
71        for (pi, (pid, p)) in node.properties.iter().enumerate() {
72            let Some(prop_id) = Id::new(pid) else {
73                continue;
74            };
75            let old = map.get(pid.as_str());
76            kept += usize::from(old.is_some());
77            let mut slot = old.cloned().unwrap_or_else(|| Slot {
78                view: PropertyView::default(),
79                node: node_id.clone(),
80                prop: prop_id.clone(),
81                node_idx: ni,
82                prop_idx: pi,
83                retained: p.retained,
84            });
85            slot.node_idx = ni;
86            slot.prop_idx = pi;
87            slot.retained = p.retained;
88            if !p.retained {
89                slot.view.value = None;
90                slot.view.last_valid = None;
91            }
92            if let Some(v) = &mut slot.view.value {
93                v.check = check_value(&v.raw, p);
94            }
95            if let Some(v) = &mut slot.view.target {
96                v.check = check_value(&v.raw, p);
97            }
98            slot.view.last_valid = match &slot.view.value {
99                Some(v) if v.check.is_ok() => Some(v.raw.clone()),
100                _ => slot
101                    .view
102                    .last_valid
103                    .take()
104                    .filter(|lv| check_value(lv, p).is_ok()),
105            };
106            if p.retained {
107                total += 1;
108                if slot.view.value.is_some() {
109                    present += 1;
110                }
111            }
112            if old != Some(&slot) {
113                map.insert_mut(prop_id, slot);
114            }
115        }
116        if !old_map.is_some_and(|m| m.ptr_eq(&map)) {
117            props.insert_mut(node_id, map);
118        }
119    }
120    let gone: Vec<Id> = props
121        .keys()
122        .filter(|n| !nodes.contains(n.as_str()))
123        .cloned()
124        .collect();
125    for n in gone {
126        props.remove_mut(n.as_str());
127    }
128    dev.marks.retain(|n, m| match props.get(n.as_str()) {
129        Some(pm) => {
130            m.retain(|p, _| pm.contains_key(p.as_str()));
131            !m.is_empty()
132        }
133        None => false,
134    });
135    dev.view.props = props;
136    dev.retained_total = total;
137    dev.retained_present = present;
138    before.saturating_sub(kept)
139}
140
141impl Controller {
142    /// Handles one received message. A message while the link is down is
143    /// taken as evidence of a new session (`reason=implicitLinkUp`): devices
144    /// are re-paced as after a clean session, while filters already sent stay
145    /// counted as on the broker, so releasing one still unsubscribes it. The
146    /// host should still forward every CONNACK.
147    pub fn handle_message(&mut self, now: Instant, msg: Message<'_>) -> Handled {
148        if self.shut_down || !self.wants(msg.topic) {
149            return Handled::Ignored;
150        }
151        let Ok(t) = topic::parse(msg.topic) else {
152            crate::log::debug!("invalidTopic", topic = msg.topic);
153            return Handled::Ignored;
154        };
155        let Some(device) = t.device else {
156            return Handled::Ignored;
157        };
158        if matches!(
159            t.kind,
160            TopicKind::PropertySet { .. } | TopicKind::Broadcast { .. }
161        ) {
162            return Handled::Ignored;
163        }
164        if msg.retain.is_none() && !self.no_retain_bit {
165            self.no_retain_bit = true;
166            for d in self.devices.values_mut() {
167                replay::abandon(d);
168            }
169        }
170        if !self.connected {
171            crate::log::warn!("implicitLinkUp", topic = msg.topic);
172            self.link(now, Link::up(false), true);
173        }
174        match t.kind {
175            TopicKind::DeviceState => self.on_state(now, t.domain, device, msg.payload),
176            TopicKind::DeviceDescription => self.on_description(now, t.domain, device, msg),
177            TopicKind::PropertyValue { node, property } => {
178                self.on_value(now, t.domain, device, node, property, msg, false)
179            }
180            TopicKind::PropertyTarget { node, property } => {
181                self.on_value(now, t.domain, device, node, property, msg, true)
182            }
183            TopicKind::DeviceAlert { alert_id } => {
184                self.on_alert(t.domain, device, alert_id, msg.payload)
185            }
186            TopicKind::DeviceLog { level } => {
187                let Some(dev) = self.devices.get_mut(&(device, t.domain) as &dyn KeyRef) else {
188                    return Handled::Ignored;
189                };
190                dev.touch();
191                crate::log::debug!("deviceLog", device = device, level = level);
192                Handled::Consumed
193            }
194            TopicKind::PropertySet { .. } | TopicKind::Broadcast { .. } => Handled::Ignored,
195        }
196    }
197
198    fn at_capacity(&self) -> bool {
199        self.cfg
200            .max_devices
201            .is_some_and(|m| self.devices.len() >= m)
202    }
203
204    // ----- $state -----
205
206    fn on_state(&mut self, now: Instant, domain: &str, id: &str, raw: &[u8]) -> Handled {
207        let received = match payload::classify(raw) {
208            Ok(r) => r,
209            Err(_) => {
210                crate::log::warn!("invalidState", device = id);
211                return Handled::Ignored;
212            }
213        };
214        let key = match self.key_of(id, domain) {
215            Some(k) => k,
216            None => match self.adopt(now, domain, id, received) {
217                Some(Ok(k)) => k,
218                Some(Err(h)) => return h,
219                None => return Handled::Ignored,
220            },
221        };
222        match received {
223            Received::Deleted => {
224                self.remove_device(now, &key);
225            }
226            Received::Text(text) => match DeviceState::parse(text) {
227                Some(state) => self.apply_state(now, &key, state),
228                None => {
229                    crate::log::warn!("invalidState", device = id, state = text);
230                    if let Some(d) = self.devices.get_mut(&key) {
231                        d.touch();
232                    }
233                }
234            },
235        }
236        Handled::Consumed
237    }
238
239    /// A `$state` for a device not in the registry: start tracking it when the
240    /// mode allows. `Some(Err(_))` is a message handled without a device.
241    fn adopt(
242        &mut self,
243        now: Instant,
244        domain: &str,
245        id: &str,
246        received: Received<'_>,
247    ) -> Option<Result<DevKey, Handled>> {
248        let Received::Text(text) = received else {
249            return self.aux_state(domain, id, None);
250        };
251        let Some(state) = DeviceState::parse(text) else {
252            crate::log::warn!("invalidState", device = id, state = text);
253            return None;
254        };
255        let new_key = |c: &Controller| -> Option<DevKey> {
256            let domain: Arc<str> = match &c.named {
257                Some(d) => d.clone(),
258                None => Arc::from(domain),
259            };
260            Some(DevKey {
261                id: Id::new(id)?,
262                domain,
263            })
264        };
265        match &self.cfg.discovery {
266            Discovery::Wildcard => {
267                if self.at_capacity() {
268                    crate::log::warn!("deviceLimit", device = id);
269                    return None;
270                }
271                let key = new_key(self)?;
272                crate::log::info!("deviceDiscovered", device = id, state = state);
273                self.devices.insert(key.clone(), Device::new(&key));
274                self.request_subscription(now, &key, true, false);
275                Some(Ok(key))
276            }
277            Discovery::Tree { root } => {
278                let is_root = root.as_str() == id;
279                let declared = is_root || self.children.values().any(|s| s.contains(id));
280                if !declared || self.at_capacity() {
281                    return None;
282                }
283                let key = new_key(self)?;
284                crate::log::info!("removedDeviceReturned", device = id);
285                self.devices.insert(key.clone(), Device::new(&key));
286                self.request_subscription(now, &key, !is_root, false);
287                Some(Ok(key))
288            }
289            Discovery::Device(dev_id) => {
290                if dev_id.as_str() != id {
291                    return self.aux_state(domain, id, Some(state));
292                }
293                let key = new_key(self)?;
294                crate::log::info!("removedDeviceReturned", device = id);
295                self.devices.insert(key.clone(), Device::new(&key));
296                self.request_subscription(now, &key, false, false);
297                Some(Ok(key))
298            }
299        }
300    }
301
302    /// Device mode: the `$state` of the tracked device's root.
303    fn aux_state(
304        &mut self,
305        domain: &str,
306        id: &str,
307        state: Option<DeviceState>,
308    ) -> Option<Result<DevKey, Handled>> {
309        let Discovery::Device(dev_id) = &self.cfg.discovery else {
310            return None;
311        };
312        let dev_key = self.named_key(dev_id)?;
313        let is_root = self
314            .devices
315            .get(&dev_key)
316            .and_then(|d| d.root_filter.as_ref())
317            .is_some_and(|(r, _)| r == id);
318        if !is_root {
319            return None;
320        }
321        let root = Id::new(id)?;
322        match state {
323            Some(s) => {
324                self.aux_states.insert(root, s);
325            }
326            None => {
327                self.aux_states.remove(id);
328            }
329        }
330        let domain: Arc<str> = Arc::from(domain);
331        self.propagate_root(&domain, id, state);
332        Some(Err(Handled::Consumed))
333    }
334
335    fn apply_state(&mut self, now: Instant, key: &DevKey, new: DeviceState) {
336        let Some(dev) = self.devices.get_mut(key) else {
337            return;
338        };
339        dev.touch();
340        let old = dev.view.state;
341        let changed = old != Some(new);
342        if changed {
343            dev.view.state = Some(new);
344            let effective = self.effective_of(key).unwrap_or(new);
345            if let Some(dev) = self.devices.get_mut(key) {
346                dev.view.effective = Some(effective);
347            }
348            match old {
349                None => {
350                    self.events.push_back(Event::DeviceDiscovered {
351                        device: key.id.clone(),
352                        state: new,
353                    });
354                }
355                Some(o) => {
356                    crate::log::info!("deviceStateChanged", device = key.id, old = o, new = new);
357                }
358            }
359            self.events.push_back(Event::StateChanged {
360                device: key.id.clone(),
361                old,
362                new,
363                effective,
364            });
365            self.propagate_root(&key.domain, key.id.as_str(), Some(new));
366            if new == DeviceState::Ready {
367                self.announce(key, false);
368            }
369        }
370        if changed
371            && new == DeviceState::Ready
372            && matches!(self.cfg.discovery, Discovery::Tree { .. })
373        {
374            self.reconcile(now, key);
375        }
376        self.note_progress(now, key);
377    }
378
379    /// Emits [`Event::DescriptionChanged`] when the applied description differs
380    /// from the last one announced.
381    fn announce(&mut self, key: &DevKey, while_ready: bool) {
382        let Some(dev) = self.devices.get_mut(key) else {
383            return;
384        };
385        let while_sleeping = core::mem::take(&mut dev.changed_while_sleeping);
386        let (Some(desc), Some(hash)) = (&dev.view.description, dev.content_hash) else {
387            return;
388        };
389        if dev.announced == Some(hash) {
390            return;
391        }
392        let while_ready = (while_ready || while_sleeping) && dev.announced.is_some();
393        if while_ready {
394            crate::log::warn!("descriptionChangedWhileReady", device = key.id);
395        }
396        dev.announced = Some(hash);
397        self.events.push_back(Event::DescriptionChanged {
398            device: key.id.clone(),
399            description: desc.clone(),
400            ignored: dev.ignored.clone(),
401            while_ready,
402        });
403    }
404
405    /// Recomputes the effective state of every device whose declared root is
406    /// `root` in `domain`, now in `root_state`.
407    fn propagate_root(&mut self, domain: &Arc<str>, root: &str, root_state: Option<DeviceState>) {
408        for (k, d) in self.devices.iter_mut() {
409            if k.domain != *domain || d.root_id() != Some(root) {
410                continue;
411            }
412            let Some(own) = d.view.state else {
413                continue;
414            };
415            let new = effective_state(root_state, own);
416            if d.view.effective == Some(new) {
417                continue;
418            }
419            if let Some(old) = d.view.effective.replace(new) {
420                self.events.push_back(Event::EffectiveStateChanged {
421                    device: k.id.clone(),
422                    old,
423                    new,
424                });
425            }
426        }
427    }
428
429    /// Recomputes one device's effective state after its root may have changed.
430    fn refresh_effective(&mut self, key: &DevKey) {
431        let new = self.effective_of(key);
432        let Some(dev) = self.devices.get_mut(key) else {
433            return;
434        };
435        if dev.view.effective == new {
436            return;
437        }
438        if let (Some(old), Some(new)) = (core::mem::replace(&mut dev.view.effective, new), new) {
439            self.events.push_back(Event::EffectiveStateChanged {
440                device: key.id.clone(),
441                old,
442                new,
443            });
444        }
445    }
446
447    // ----- removal -----
448
449    /// A zero-length `$state`: the device leaves the registry. Tree and device
450    /// modes keep its `$state` and `$description` filters so a return is heard.
451    fn remove_device(&mut self, now: Instant, key: &DevKey) {
452        let Some(dev) = self.devices.remove(key) else {
453            return;
454        };
455        self.forget(now, key);
456        match self.cfg.discovery {
457            Discovery::Wildcard => self.release_all(key),
458            _ => self.release_props(key),
459        }
460        if let Some((root, filter)) = &dev.root_filter {
461            if let Some(h) = self.holds.get_mut(key) {
462                h.attrs.retain(|f| f != filter);
463            }
464            let cmd = self.ledger.unsubscribe(alloc::vec![filter.clone()]);
465            self.push(cmd);
466            self.aux_states.remove(root.as_str());
467        }
468        self.dropped.remove(key);
469        if dev.view.state.is_some() {
470            crate::log::info!("deviceRemoved", device = key.id);
471            self.events.push_back(Event::DeviceRemoved {
472                device: key.id.clone(),
473            });
474        }
475        self.propagate_root(&key.domain, key.id.as_str(), None);
476        self.reevaluate_trees(key, dev.root_id());
477    }
478
479    // ----- tree mode -----
480
481    /// Diffs a device's declared children against those subscribed: adds the
482    /// new ones (paced), drops the undeclared ones recursively.
483    fn reconcile(&mut self, now: Instant, key: &DevKey) {
484        let Some(desc) = self
485            .devices
486            .get(key)
487            .and_then(|d| d.view.description.clone())
488        else {
489            return;
490        };
491        let mut declared: Vec<Id> = Vec::new();
492        for c in &desc.children {
493            if c.as_str() == key.id.as_str() || declared.iter().any(|d| d == c.as_str()) {
494                continue;
495            }
496            if let Some(id) = Id::new(c) {
497                declared.push(id);
498            }
499        }
500        let current = self.children.get(key).cloned().unwrap_or_default();
501        for c in &declared {
502            let ckey = DevKey {
503                id: c.clone(),
504                domain: key.domain.clone(),
505            };
506            let known = self.devices.contains_key(&ckey);
507            if known && current.contains(c) {
508                continue;
509            }
510            if !known {
511                if self.at_capacity() {
512                    crate::log::warn!("deviceLimit", device = c);
513                    continue;
514                }
515                self.devices.insert(ckey.clone(), Device::new(&ckey));
516            }
517            self.children
518                .entry(key.clone())
519                .or_default()
520                .insert(c.clone());
521            if known {
522                // Another parent declares it, or it is the root: subscribed.
523                continue;
524            }
525            crate::log::info!("treeRootedAddDescendant", parent = key.id, child = c);
526            self.request_subscription(now, &ckey, true, false);
527        }
528        if let Some(set) = self.children.get_mut(key) {
529            set.retain(|c| declared.contains(c));
530        }
531        let undeclared: Vec<&Id> = current.iter().filter(|c| !declared.contains(c)).collect();
532        if !undeclared.is_empty() {
533            let reachable = self.reachable(&key.domain);
534            for c in undeclared {
535                let ckey = DevKey {
536                    id: c.clone(),
537                    domain: key.domain.clone(),
538                };
539                if reachable.contains(c.as_str()) {
540                    crate::log::info!("treeRootedKeepDescendant", parent = key.id, child = c);
541                    continue;
542                }
543                crate::log::info!("treeRootedRemoveDescendant", parent = key.id, child = c);
544                let mut visited = BTreeSet::new();
545                self.drop_tree(now, &ckey, &reachable, &mut visited);
546            }
547        }
548        self.reevaluate_trees(key, None);
549    }
550
551    /// Tree mode: the ids reachable from the configured root through the
552    /// children subscribed for each parent.
553    fn reachable(&self, domain: &Arc<str>) -> BTreeSet<Id> {
554        let mut seen = BTreeSet::new();
555        let Discovery::Tree { root } = &self.cfg.discovery else {
556            return seen;
557        };
558        let mut queue: VecDeque<Id> = VecDeque::from([root.clone()]);
559        while let Some(id) = queue.pop_front() {
560            if !seen.insert(id.clone()) {
561                continue;
562            }
563            let k = DevKey {
564                id,
565                domain: domain.clone(),
566            };
567            if let Some(set) = self.children.get(&k) {
568                queue.extend(set.iter().cloned());
569            }
570        }
571        seen
572    }
573
574    /// Drops a device and its subscribed descendants, leaves first, except
575    /// those another parent still declares (`reachable`).
576    fn drop_tree(
577        &mut self,
578        now: Instant,
579        key: &DevKey,
580        reachable: &BTreeSet<Id>,
581        visited: &mut BTreeSet<DevKey>,
582    ) {
583        if reachable.contains(key.id.as_str()) {
584            return;
585        }
586        if let Discovery::Tree { root } = &self.cfg.discovery {
587            if *root == key.id {
588                return;
589            }
590        }
591        if !visited.insert(key.clone()) {
592            return;
593        }
594        for c in self.children.remove(key).unwrap_or_default() {
595            let ckey = DevKey {
596                id: c,
597                domain: key.domain.clone(),
598            };
599            self.drop_tree(now, &ckey, reachable, visited);
600        }
601        for set in self.children.values_mut() {
602            set.remove(key.id.as_str());
603        }
604        let dev = self.devices.remove(key);
605        self.forget(now, key);
606        self.release_all(key);
607        self.dropped.remove(key);
608        if dev.is_some_and(|d| d.view.state.is_some()) {
609            crate::log::info!("deviceRemoved", device = key.id);
610            self.events.push_back(Event::DeviceRemoved {
611                device: key.id.clone(),
612            });
613        }
614    }
615
616    /// True when `root` and every device declared under it have a description.
617    pub(super) fn tree_complete(&self, root: &DevKey) -> bool {
618        let Some(desc) = self
619            .devices
620            .get(root)
621            .and_then(|r| r.view.description.as_ref())
622        else {
623            return false;
624        };
625        let mut seen: BTreeSet<&str> = BTreeSet::new();
626        seen.insert(root.id.as_str());
627        let mut queue: VecDeque<&str> = desc.children.iter().map(String::as_str).collect();
628        while let Some(c) = queue.pop_front() {
629            if !seen.insert(c) {
630                continue;
631            }
632            let Some(cd) = self
633                .devices
634                .get(&(c, &*root.domain) as &dyn KeyRef)
635                .and_then(|d| d.view.description.as_ref())
636            else {
637                return false;
638            };
639            queue.extend(cd.children.iter().map(String::as_str));
640        }
641        true
642    }
643
644    fn reevaluate_tree(&mut self, root: &DevKey) {
645        let complete = self.tree_complete(root);
646        let Some(d) = self.devices.get_mut(root) else {
647            return;
648        };
649        let was = core::mem::replace(&mut d.tree_complete, complete);
650        if complete && !was {
651            crate::log::info!("treeComplete", root = root.id);
652            self.events.push_back(Event::TreeComplete {
653                root: root.id.clone(),
654            });
655        }
656    }
657
658    /// Re-evaluates the tree `key` belongs to (its declared root, or itself),
659    /// and in tree mode the configured root.
660    fn reevaluate_trees(&mut self, key: &DevKey, declared_root: Option<&str>) {
661        let root = match declared_root.or_else(|| self.devices.get(key).and_then(Device::root_id)) {
662            Some(r) => Id::new(r).map(|id| DevKey {
663                id,
664                domain: key.domain.clone(),
665            }),
666            None => Some(key.clone()),
667        };
668        if let Some(r) = &root {
669            self.reevaluate_tree(r);
670        }
671        if let Discovery::Tree { root: tree_root } = &self.cfg.discovery {
672            if let Some(tk) = self.named_key(tree_root) {
673                if root.as_ref() != Some(&tk) {
674                    self.reevaluate_tree(&tk);
675                }
676            }
677        }
678    }
679
680    // ----- $description -----
681
682    fn on_description(
683        &mut self,
684        now: Instant,
685        domain: &str,
686        id: &str,
687        msg: Message<'_>,
688    ) -> Handled {
689        let Some(key) = self.key_of(id, domain) else {
690            return Handled::Ignored;
691        };
692        let marker = msg.retain != Some(false) && self.draining.contains_key(&key);
693        if msg.payload.is_empty() {
694            if let Some(d) = self.devices.get_mut(&key) {
695                d.touch();
696            }
697            crate::log::debug!("descriptionCleared", device = id);
698            if marker {
699                self.release_slot(now, &key);
700            }
701            return Handled::Consumed;
702        }
703        if let Some(a) = self.awaiting.get_mut(&key) {
704            a.refresh = false;
705        }
706        let hash: [u8; 32] = Sha256::digest(msg.payload).into();
707        let (same, was_rejected) = {
708            let Some(dev) = self.devices.get_mut(&key) else {
709                return Handled::Ignored;
710            };
711            dev.touch();
712            let was_rejected = dev.raw_hash.is_some() && dev.view.description.is_none();
713            (dev.raw_hash.replace(hash) == Some(hash), was_rejected)
714        };
715        if msg.retain == Some(true) {
716            self.note_replay(&key, msg.topic);
717        }
718        if marker {
719            self.release_slot(now, &key);
720        }
721        if !same {
722            let parsed = match core::str::from_utf8(msg.payload) {
723                Ok(text) => description::parse_with_ignored(text),
724                Err(_) => Err(DescriptionError::InvalidJson),
725            };
726            match parsed {
727                Ok((desc, ignored)) => {
728                    self.apply_description(now, &key, desc, ignored);
729                    if was_rejected {
730                        self.replay_props(&key);
731                    }
732                }
733                Err(e) => self.reject_description(now, &key, e),
734            }
735        }
736        self.note_progress(now, &key);
737        Handled::Consumed
738    }
739
740    /// Sends the SUBSCRIBE again for the property filters a device holds, so
741    /// the broker replays the values discarded while it had no model.
742    fn replay_props(&mut self, key: &DevKey) {
743        if self.pending_entries.contains_key(key) || self.draining.contains_key(key) {
744            return;
745        }
746        let props = self
747            .holds
748            .get(key)
749            .map(|h| h.props.clone())
750            .unwrap_or_default();
751        if props.is_empty() {
752            return;
753        }
754        crate::log::info!("modelRestoredResubscribe", device = key.id);
755        let window = self
756            .devices
757            .get(key)
758            .filter(|_| !self.no_retain_bit)
759            .and_then(|d| {
760                let nodes = match &d.coverage {
761                    super::Coverage::Nodes(s) => Some(s.iter().cloned().collect::<Vec<_>>()),
762                    _ => None,
763                };
764                let alerts =
765                    nodes.is_none() || props.contains(&Self::device_filter(key, super::ALERTS));
766                replay::holds_any(d, nodes.as_deref(), alerts).then_some((nodes, alerts))
767            });
768        match window {
769            Some((nodes, alerts)) => {
770                let mut filters = props;
771                filters.push(Self::device_filter(key, "$description"));
772                self.resubscribe(&filters);
773                self.open_window(key, nodes, alerts);
774            }
775            None => self.resubscribe(&props),
776        }
777    }
778
779    fn reject_description(&mut self, now: Instant, key: &DevKey, error: DescriptionError) {
780        let Some(dev) = self.devices.get_mut(key) else {
781            return;
782        };
783        crate::log::warn!("descriptionRejected", device = key.id, error = error);
784        let declared_root = dev.root_id().map(String::from);
785        dev.view.description = None;
786        dev.content_hash = None;
787        dev.announced = None;
788        dev.ignored.clear();
789        if !dev.view.props.is_empty() {
790            dev.view.props = Props::new_sync();
791        }
792        dev.marks.clear();
793        dev.retained_total = 0;
794        dev.retained_present = 0;
795        dev.view.synced = false;
796        dev.sync_at = None;
797        dev.sync_expired = false;
798        dev.early.clear();
799        dev.changed_while_sleeping = false;
800        replay::abandon(dev);
801        self.events.push_back(Event::DescriptionRejected {
802            device: key.id.clone(),
803            error,
804        });
805        self.forget(now, key);
806        self.update_root_filter(key);
807        self.refresh_effective(key);
808        self.reevaluate_trees(key, declared_root.as_deref());
809    }
810
811    fn apply_description(
812        &mut self,
813        now: Instant,
814        key: &DevKey,
815        desc: DeviceDescription,
816        ignored: Vec<Ignored>,
817    ) {
818        let content = desc.content_hash();
819        let sync_ms = self.cfg.sync_timeout_ms;
820        let Some(dev) = self.devices.get_mut(key) else {
821            return;
822        };
823        if dev.content_hash == Some(content) {
824            dev.ignored = ignored;
825            return;
826        }
827        let desc = Arc::new(desc);
828        let dropped = rebuild_slots(dev, &desc);
829        if dropped > 0 {
830            crate::log::debug!(
831                "vanishedPropertiesDropped",
832                device = key.id,
833                count = dropped
834            );
835        }
836        dev.view.description = Some(desc);
837        dev.content_hash = Some(content);
838        dev.ignored = ignored;
839        dev.view.synced = false;
840        dev.sync_at = Some(now.add_ms(sync_ms));
841        dev.sync_expired = false;
842        let early = core::mem::take(&mut dev.early);
843        let ready = dev.view.state == Some(DeviceState::Ready);
844        if dev.view.state == Some(DeviceState::Sleeping) && dev.announced.is_some() {
845            crate::log::warn!("descriptionChangedWhileSleeping", device = key.id);
846            dev.changed_while_sleeping = true;
847        }
848        crate::log::info!("descriptionReceived", device = key.id);
849        self.floor_sync(now.add_ms(sync_ms));
850        if ready {
851            self.announce(key, true);
852        }
853        self.prune_coverage(key);
854        self.update_root_filter(key);
855        self.refresh_effective(key);
856        self.recount_burst(now, key);
857        for ((node, prop), e) in early {
858            if let Some((text, retain)) = e.value {
859                self.store_value(key, &node, &prop, Some(&text), retain, false);
860            }
861            if let Some((text, retain)) = e.target {
862                self.store_value(key, &node, &prop, Some(&text), retain, true);
863            }
864        }
865        if ready && matches!(self.cfg.discovery, Discovery::Tree { .. }) {
866            self.reconcile(now, key);
867        }
868        self.reevaluate_trees(key, None);
869        self.check_sync(now, key);
870    }
871
872    /// Node by node: releases the property filters of covered nodes the
873    /// description no longer declares.
874    fn prune_coverage(&mut self, key: &DevKey) {
875        let Some(dev) = self.devices.get_mut(key) else {
876            return;
877        };
878        let super::Coverage::Nodes(set) = &mut dev.coverage else {
879            return;
880        };
881        let props = &dev.view.props;
882        let gone: Vec<Id> = set
883            .iter()
884            .filter(|n| !props.contains_key(n.as_str()))
885            .cloned()
886            .collect();
887        if gone.is_empty() {
888            return;
889        }
890        for n in &gone {
891            set.remove(n);
892        }
893        if set.is_empty() {
894            dev.coverage = super::Coverage::None;
895        }
896        let filters = Self::prop_filters(key, Some(&gone));
897        if let Some(h) = self.holds.get_mut(key) {
898            h.props.retain(|f| !filters.contains(f));
899        }
900        crate::log::debug!("vanishedNodesReleased", device = key.id, nodes = gone.len());
901        let cmd = self.ledger.unsubscribe(filters);
902        self.push(cmd);
903    }
904
905    /// Device mode: holds `{root}/$state` for the root the description names.
906    fn update_root_filter(&mut self, key: &DevKey) {
907        if !matches!(self.cfg.discovery, Discovery::Device(_)) {
908            return;
909        }
910        let Some(dev) = self.devices.get_mut(key) else {
911            return;
912        };
913        let want = dev.root_id().map(|r| {
914            (
915                String::from(r),
916                Self::topic_filter(&key.domain, r, "$state"),
917            )
918        });
919        if dev.root_filter == want {
920            return;
921        }
922        let old = core::mem::replace(&mut dev.root_filter, want.clone());
923        if let Some((root, filter)) = old {
924            if let Some(h) = self.holds.get_mut(key) {
925                h.attrs.retain(|f| *f != filter);
926            }
927            let cmd = self.ledger.unsubscribe(alloc::vec![filter]);
928            self.push(cmd);
929            self.aux_states.remove(root.as_str());
930        }
931        if let Some((_, filter)) = want {
932            self.hold(key, alloc::vec![filter], Vec::new(), false);
933        }
934    }
935
936    // ----- values -----
937
938    #[allow(clippy::too_many_arguments)]
939    fn on_value(
940        &mut self,
941        now: Instant,
942        domain: &str,
943        id: &str,
944        node: &str,
945        prop: &str,
946        msg: Message<'_>,
947        target: bool,
948    ) -> Handled {
949        let max_early = self.cfg.max_early_values;
950        let Some((key, dev)) = self.devices.get_key_value(&(id, domain) as &dyn KeyRef) else {
951            return Handled::Ignored;
952        };
953        let key = key.clone();
954        let has_model = dev.view.description.is_some();
955        let rejected = dev.raw_hash.is_some() && !has_model;
956        let text = match payload::classify(msg.payload) {
957            Ok(Received::Deleted) => None,
958            Ok(Received::Text(t)) => Some(t),
959            Err(_) => {
960                crate::log::warn!("invalidUtf8", topic = msg.topic);
961                return Handled::Consumed;
962            }
963        };
964        let Some(dev) = self.devices.get_mut(&key) else {
965            return Handled::Ignored;
966        };
967        dev.touch();
968        if !has_model {
969            if rejected {
970                crate::log::debug!("valueWithoutModel", topic = msg.topic);
971                return Handled::Consumed;
972            }
973            let k = (String::from(node), String::from(prop));
974            match text {
975                None => {
976                    if let Some(e) = dev.early.get_mut(&k) {
977                        if target {
978                            e.target = None;
979                        } else {
980                            e.value = None;
981                        }
982                        if e.value.is_none() && e.target.is_none() {
983                            dev.early.remove(&k);
984                        }
985                    }
986                }
987                Some(t) => {
988                    if !dev.early.contains_key(&k) && dev.early.len() >= max_early {
989                        crate::log::debug!("earlyValueDropped", topic = msg.topic);
990                        return Handled::Consumed;
991                    }
992                    let e = dev.early.entry(k).or_default();
993                    let v = Some((String::from(t), msg.retain));
994                    if target {
995                        e.target = v;
996                    } else {
997                        e.value = v;
998                    }
999                }
1000            }
1001            return Handled::Consumed;
1002        }
1003        let counted = self.store_value(&key, node, prop, text, msg.retain, target);
1004        if counted {
1005            let done = self.draining.get(&key).is_some_and(|b| b.owed() == 0);
1006            if done {
1007                self.release_slot(now, &key);
1008            } else if !self.pending.is_empty() {
1009                self.take_pending(now);
1010            }
1011        }
1012        self.check_sync(now, &key);
1013        Handled::Consumed
1014    }
1015
1016    /// Applies a value or `$target` (`text: None` clears it) to a device with a
1017    /// model, emitting its event. True when it counted toward the device's
1018    /// burst for the first time.
1019    fn store_value(
1020        &mut self,
1021        key: &DevKey,
1022        node: &str,
1023        prop: &str,
1024        text: Option<&str>,
1025        retain: Option<bool>,
1026        target: bool,
1027    ) -> bool {
1028        let Some(dev) = self.devices.get_mut(key) else {
1029            return false;
1030        };
1031        let Some(desc) = dev.view.description.as_deref() else {
1032            return false;
1033        };
1034        let Some(old) = dev.view.props.get(node).and_then(|m| m.get(prop)) else {
1035            crate::log::debug!(
1036                "undeclaredProperty",
1037                device = key.id,
1038                node = node,
1039                property = prop
1040            );
1041            return false;
1042        };
1043        let Some(decl) = prop_decl(desc, old) else {
1044            return false;
1045        };
1046        let bit = if target { 2 } else { 1 };
1047        let marks = dev
1048            .marks
1049            .entry(old.node.clone())
1050            .or_default()
1051            .entry(old.prop.clone())
1052            .or_default();
1053        marks.seen |= bit;
1054        let mut slot = old.clone();
1055        let events = &mut self.events;
1056        let mut counts = false;
1057        if target {
1058            match text {
1059                None => {
1060                    if slot.view.target.take().is_some() {
1061                        events.push_back(Event::TargetChanged {
1062                            addr: addr(key, &slot),
1063                            target: None,
1064                        });
1065                    }
1066                }
1067                Some(t) => {
1068                    counts = decl.retained;
1069                    if slot.view.target.as_ref().is_none_or(|v| &*v.raw != t) {
1070                        let v = Value {
1071                            raw: Arc::from(t),
1072                            check: check_value(t, decl),
1073                        };
1074                        slot.view.target = Some(v.clone());
1075                        events.push_back(Event::TargetChanged {
1076                            addr: addr(key, &slot),
1077                            target: Some(v),
1078                        });
1079                    }
1080                }
1081            }
1082        } else if !decl.retained {
1083            match text {
1084                None => {
1085                    crate::log::debug!(
1086                        "eventPropertyCleared",
1087                        device = key.id,
1088                        node = node,
1089                        property = prop
1090                    );
1091                }
1092                Some(t) => {
1093                    if retain == Some(true) {
1094                        crate::log::info!(
1095                            "retainedEventProperty",
1096                            device = key.id,
1097                            node = node,
1098                            property = prop
1099                        );
1100                    }
1101                    events.push_back(Event::PropertyEvent {
1102                        addr: addr(key, &slot),
1103                        value: Value {
1104                            raw: Arc::from(t),
1105                            check: check_value(t, decl),
1106                        },
1107                        origin: Origin::of(retain),
1108                    });
1109                }
1110            }
1111        } else {
1112            match text {
1113                None => {
1114                    if slot.view.value.take().is_some() {
1115                        slot.view.last_valid = None;
1116                        dev.retained_present = dev.retained_present.saturating_sub(1);
1117                        events.push_back(Event::PropertyCleared {
1118                            addr: addr(key, &slot),
1119                        });
1120                    }
1121                }
1122                Some(t) => {
1123                    counts = true;
1124                    if slot.view.value.as_ref().is_none_or(|v| &*v.raw != t) {
1125                        if slot.view.value.is_none() {
1126                            dev.retained_present += 1;
1127                        }
1128                        let v = Value {
1129                            raw: Arc::from(t),
1130                            check: check_value(t, decl),
1131                        };
1132                        if v.check.is_ok() {
1133                            slot.view.last_valid = Some(v.raw.clone());
1134                        }
1135                        slot.view.value = Some(v.clone());
1136                        events.push_back(Event::PropertyChanged {
1137                            addr: addr(key, &slot),
1138                            value: v,
1139                            origin: Origin::of(retain),
1140                        });
1141                    }
1142                }
1143            }
1144        }
1145        if slot != *old {
1146            put_slot(&mut dev.view.props, slot);
1147        }
1148        if !counts {
1149            return false;
1150        }
1151        let Some(b) = self.draining.get_mut(key) else {
1152            return false;
1153        };
1154        if !b.covers(node) || marks.burst & bit != 0 {
1155            return false;
1156        }
1157        marks.burst |= bit;
1158        b.received = b.received.saturating_add(1);
1159        true
1160    }
1161
1162    // ----- alerts -----
1163
1164    fn on_alert(&mut self, domain: &str, id: &str, alert: &str, raw: &[u8]) -> Handled {
1165        let max_alerts = self.cfg.max_alerts;
1166        let Some((key, _)) = self.devices.get_key_value(&(id, domain) as &dyn KeyRef) else {
1167            return Handled::Ignored;
1168        };
1169        let device = key.id.clone();
1170        let Some(dev) = self.devices.get_mut(&(id, domain) as &dyn KeyRef) else {
1171            return Handled::Ignored;
1172        };
1173        dev.touch();
1174        let Some(alert_id) = Id::new(alert) else {
1175            return Handled::Consumed;
1176        };
1177        match payload::classify(raw) {
1178            Ok(Received::Deleted) => {
1179                // `remove_mut` copies the path even for an absent key.
1180                if dev.view.alerts.contains_key(alert) {
1181                    dev.view.alerts.remove_mut(alert);
1182                    self.events.push_back(Event::Alert {
1183                        device,
1184                        alert: alert_id,
1185                        message: None,
1186                    });
1187                }
1188            }
1189            Ok(Received::Text(t)) => {
1190                let known = dev.view.alerts.contains_key(alert);
1191                if !known && dev.view.alerts.size() >= max_alerts {
1192                    crate::log::debug!("alertDropped", device = id, alert = alert);
1193                    return Handled::Consumed;
1194                }
1195                if dev.windows.iter().any(|w| w.alerts) {
1196                    dev.seen_alerts.insert(alert_id.clone());
1197                }
1198                if dev.view.alerts.get(alert).map(String::as_str) != Some(t) {
1199                    dev.view
1200                        .alerts
1201                        .insert_mut(alert_id.clone(), String::from(t));
1202                    self.events.push_back(Event::Alert {
1203                        device,
1204                        alert: alert_id,
1205                        message: Some(String::from(t)),
1206                    });
1207                }
1208            }
1209            Err(_) => {
1210                crate::log::warn!("invalidUtf8", device = id, alert = alert);
1211            }
1212        }
1213        Handled::Consumed
1214    }
1215
1216    // ----- sync -----
1217
1218    pub(super) fn in_flight(&self, key: &DevKey) -> bool {
1219        self.awaiting.contains_key(key)
1220            || self.pending_entries.contains_key(key)
1221            || self.draining.contains_key(key)
1222    }
1223
1224    /// Emits [`Event::DeviceSynced`] when its conditions hold.
1225    pub(super) fn check_sync(&mut self, now: Instant, key: &DevKey) {
1226        match self.devices.get(key) {
1227            Some(d) if !d.view.synced && d.view.state.is_some() && d.view.description.is_some() => {
1228            }
1229            _ => return,
1230        }
1231        if self.in_flight(key) {
1232            return;
1233        }
1234        let Some(dev) = self.devices.get_mut(key) else {
1235            return;
1236        };
1237        let missing = dev.missing();
1238        let expired = dev.sync_expired || dev.sync_at.is_some_and(|t| t <= now);
1239        if (missing > 0 || !dev.windows.is_empty()) && !expired {
1240            return;
1241        }
1242        dev.view.synced = true;
1243        dev.sync_at = None;
1244        dev.sync_expired = false;
1245        crate::log::info!("deviceSynced", device = key.id, missing = missing);
1246        self.events.push_back(Event::DeviceSynced {
1247            device: key.id.clone(),
1248            missing,
1249        });
1250    }
1251}