Skip to main content

matter_interaction/
event.rs

1//! Matter event paths, filters, and reports — `EventPathIB` / `EventFilterIB` /
2//! `EventDataIB` / `EventReportIB` (Matter §10.6 / Appendix A).
3//!
4//! Distinct from the attribute path/report code: `EventPathIB` uses tag base 0
5//! (Node), not 2. Wire shapes are pinned by the matter.js byte-parity fixtures
6//! (`test-vectors/commissioning/im/{read/events_basic_information,report/report_data_event}.json`)
7//! and cross-checked against connectedhomeip `src/app/MessageDef/Event*IB.h`:
8//! `EventPathIB` is a TLV **list**, `EventFilterIB` is a TLV **structure**.
9
10#![forbid(unsafe_code)]
11
12use crate::error::ImError;
13use crate::{read_container_members, read_container_value, skip_container};
14use matter_codec::{ContainerKind, Element, Tag, TlvReader, TlvWriter, Value};
15
16/// A read/subscribe event path with optional (wildcard) components. A `None`
17/// field is omitted from the encoded `EventPathIB`, which the IM interprets as a
18/// wildcard. `node` is normally `None` for a controller addressing the connected
19/// node; `is_urgent` requests urgent reporting on a subscription (B2).
20#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
21pub struct EventPath {
22    /// Node, or `None` (the connected node / wildcard).
23    pub node: Option<u64>,
24    /// Endpoint, or `None` for all endpoints.
25    pub endpoint: Option<u16>,
26    /// Cluster, or `None` for all clusters.
27    pub cluster: Option<u32>,
28    /// Event, or `None` for all events of the cluster.
29    pub event: Option<u32>,
30    /// Urgent-reporting hint (subscriptions); omitted when `None`.
31    pub is_urgent: Option<bool>,
32}
33
34impl EventPath {
35    /// A concrete `(endpoint, cluster, event)` path (no node, no urgent flag).
36    #[must_use]
37    pub fn concrete(endpoint: u16, cluster: u32, event: u32) -> Self {
38        Self {
39            node: None,
40            endpoint: Some(endpoint),
41            cluster: Some(cluster),
42            event: Some(event),
43            is_urgent: None,
44        }
45    }
46
47    /// All events of `cluster` on `endpoint`.
48    #[must_use]
49    pub fn cluster(endpoint: u16, cluster: u32) -> Self {
50        Self {
51            node: None,
52            endpoint: Some(endpoint),
53            cluster: Some(cluster),
54            event: None,
55            is_urgent: None,
56        }
57    }
58
59    /// Encode this path as an anonymous-tagged `EventPathIB` **list** element.
60    ///
61    /// Tags: Node 0, Endpoint 1, Cluster 2, Event 3, `IsUrgent` 4 (Matter
62    /// Appendix A). Omitted (`None`) fields are wildcards.
63    pub(crate) fn write(&self, w: &mut TlvWriter<'_>) -> Result<(), matter_codec::Error> {
64        w.start_list(Tag::Anonymous)?;
65        if let Some(n) = self.node {
66            w.put_uint(Tag::Context(0), n)?;
67        }
68        if let Some(e) = self.endpoint {
69            w.put_uint(Tag::Context(1), u64::from(e))?;
70        }
71        if let Some(c) = self.cluster {
72            w.put_uint(Tag::Context(2), u64::from(c))?;
73        }
74        if let Some(ev) = self.event {
75            w.put_uint(Tag::Context(3), u64::from(ev))?;
76        }
77        if let Some(u) = self.is_urgent {
78            w.put_bool(Tag::Context(4), u)?;
79        }
80        w.end_container()
81    }
82}
83
84/// An `EventFilterIB`: only events with `event_number >= event_min` are reported
85/// (used to resume after the last seen event). `node` is omitted when `None`.
86#[derive(Copy, Clone, Debug, PartialEq, Eq)]
87pub struct EventFilter {
88    /// Node scope, or `None`.
89    pub node: Option<u64>,
90    /// Minimum event number to report (inclusive).
91    pub event_min: u64,
92}
93
94impl EventFilter {
95    /// A filter reporting events with number `>= event_min`.
96    #[must_use]
97    pub fn from_event_min(event_min: u64) -> Self {
98        Self {
99            node: None,
100            event_min,
101        }
102    }
103
104    /// Encode this filter as an anonymous-tagged `EventFilterIB` element.
105    ///
106    /// NB: `EventFilterIB` is a TLV **structure** (`0x15`), unlike `EventPathIB`
107    /// which is a **list** (`0x17`). Confirmed by the captured matter.js bytes
108    /// (`events_basic_information.json`): array[2] holds a struct, not a list.
109    pub(crate) fn write(&self, w: &mut TlvWriter<'_>) -> Result<(), matter_codec::Error> {
110        w.start_structure(Tag::Anonymous)?;
111        if let Some(n) = self.node {
112            w.put_uint(Tag::Context(0), n)?;
113        }
114        w.put_uint(Tag::Context(1), self.event_min)?;
115        w.end_container()
116    }
117}
118
119/// Event priority (Matter §14.3). Unknown values are preserved verbatim so a
120/// newer-revision device does not break decoding.
121#[derive(Copy, Clone, Debug, PartialEq, Eq)]
122#[non_exhaustive]
123pub enum EventPriority {
124    /// Debug priority (0).
125    Debug,
126    /// Info priority (1).
127    Info,
128    /// Critical priority (2).
129    Critical,
130    /// Any other (future) priority value.
131    Unknown(u8),
132}
133
134impl EventPriority {
135    #[must_use]
136    fn from_u8(v: u8) -> Self {
137        match v {
138            0 => Self::Debug,
139            1 => Self::Info,
140            2 => Self::Critical,
141            other => Self::Unknown(other),
142        }
143    }
144}
145
146/// The timestamp carried by an `EventDataIB`. A report carries exactly one of
147/// these (absolute epoch/system, or a delta against the prior event in a
148/// subscription stream); [`None`](EventTimestamp::None) if the device omitted all
149/// four (tolerated rather than rejected).
150#[derive(Copy, Clone, Debug, PartialEq, Eq)]
151#[non_exhaustive]
152pub enum EventTimestamp {
153    /// Milliseconds since the Unix epoch (`EpochTimestamp`, tag 3).
154    Epoch(u64),
155    /// Milliseconds since boot (`SystemTimestamp`, tag 4).
156    System(u64),
157    /// Delta-epoch against the prior event in the stream (tag 5).
158    DeltaEpoch(u64),
159    /// Delta-system against the prior event in the stream (tag 6).
160    DeltaSystem(u64),
161    /// No timestamp present.
162    None,
163}
164
165/// One `EventDataIB` (a real event with data).
166#[derive(Clone, Debug, PartialEq)]
167#[non_exhaustive]
168pub struct EventReportItem {
169    /// The event's `(node?, endpoint, cluster, event)` path.
170    pub path: EventPath,
171    /// Monotonic event number (scoped to priority).
172    pub event_number: u64,
173    /// Event priority.
174    pub priority: EventPriority,
175    /// Event timestamp.
176    pub timestamp: EventTimestamp,
177    /// The event payload (cluster-defined TLV; decode with `matter-clusters`).
178    pub value: Value,
179}
180
181/// One `EventReportIB`: a real event ([`Data`](EventReport::Data)) or a per-path
182/// error ([`Status`](EventReport::Status)).
183#[derive(Clone, Debug, PartialEq)]
184#[non_exhaustive]
185pub enum EventReport {
186    /// An `EventDataIB` carrying a real event.
187    Data(EventReportItem),
188    /// An `EventStatusIB` carrying a status for a requested event path.
189    Status {
190        /// The event path the status refers to.
191        path: EventPath,
192        /// The IM status code (`StatusIB.Status`).
193        status: u8,
194    },
195}
196
197/// Read an `EventPathIB` list's members into an [`EventPath`] (tags 0–4).
198fn event_path_from_members(members: &[(Tag, Value)]) -> EventPath {
199    let mut p = EventPath::default();
200    for (tag, v) in members {
201        match (tag, v) {
202            (Tag::Context(0), Value::Uint(n)) => p.node = Some(*n),
203            (Tag::Context(1), Value::Uint(n)) => p.endpoint = u16::try_from(*n).ok(),
204            (Tag::Context(2), Value::Uint(n)) => p.cluster = u32::try_from(*n).ok(),
205            (Tag::Context(3), Value::Uint(n)) => p.event = u32::try_from(*n).ok(),
206            (Tag::Context(4), Value::Bool(b)) => p.is_urgent = Some(*b),
207            _ => {}
208        }
209    }
210    p
211}
212
213/// Parse the body of one `EventReportIB` (reader positioned just after its struct
214/// start). Returns the report, or `None` for an empty IB.
215///
216/// # Errors
217///
218/// Returns [`ImError`] if the input ends mid-container, or an `EventData` is
219/// missing its `Data` member.
220fn parse_event_report_ib(r: &mut TlvReader<'_>) -> Result<Option<EventReport>, ImError> {
221    let mut out: Option<EventReport> = None;
222    loop {
223        match r.next()? {
224            None => return Err(ImError::Codec(matter_codec::Error::UnclosedContainer)),
225            Some(Element::ContainerEnd) => break,
226            // EventData [1]
227            Some(Element::ContainerStart {
228                tag: Tag::Context(1),
229                kind: ContainerKind::Structure,
230            }) => out = Some(EventReport::Data(parse_event_data(r)?)),
231            // EventStatus [0]
232            Some(Element::ContainerStart {
233                tag: Tag::Context(0),
234                kind: ContainerKind::Structure,
235            }) => out = Some(parse_event_status(r)?),
236            Some(Element::ContainerStart { .. }) => skip_container(r)?,
237            Some(_) => {}
238        }
239    }
240    Ok(out)
241}
242
243/// Parse an `EventDataIB` body (reader just after the struct start at ctx 1).
244///
245/// # Errors
246///
247/// Returns [`ImError::MissingField`] if `Data` (tag 7) is absent, or propagates a
248/// codec error.
249fn parse_event_data(r: &mut TlvReader<'_>) -> Result<EventReportItem, ImError> {
250    let mut path = EventPath::default();
251    let mut event_number = 0u64;
252    let mut priority = EventPriority::Unknown(0xFF);
253    let mut timestamp = EventTimestamp::None;
254    let mut value: Option<Value> = None;
255    loop {
256        match r.next()? {
257            None => return Err(ImError::Codec(matter_codec::Error::UnclosedContainer)),
258            Some(Element::ContainerEnd) => break,
259            // Path [0] — EventPathIB list.
260            Some(Element::ContainerStart {
261                tag: Tag::Context(0),
262                kind: ContainerKind::List,
263            }) => {
264                let members = read_container_members(r)?;
265                path = event_path_from_members(&members);
266            }
267            Some(Element::Scalar {
268                tag: Tag::Context(1),
269                value: Value::Uint(n),
270            }) => event_number = n,
271            Some(Element::Scalar {
272                tag: Tag::Context(2),
273                value: Value::Uint(n),
274            }) => priority = EventPriority::from_u8(u8::try_from(n).unwrap_or(0xFF)),
275            Some(Element::Scalar {
276                tag: Tag::Context(3),
277                value: Value::Uint(n),
278            }) => timestamp = EventTimestamp::Epoch(n),
279            Some(Element::Scalar {
280                tag: Tag::Context(4),
281                value: Value::Uint(n),
282            }) => timestamp = EventTimestamp::System(n),
283            Some(Element::Scalar {
284                tag: Tag::Context(5),
285                value: Value::Uint(n),
286            }) => timestamp = EventTimestamp::DeltaEpoch(n),
287            Some(Element::Scalar {
288                tag: Tag::Context(6),
289                value: Value::Uint(n),
290            }) => timestamp = EventTimestamp::DeltaSystem(n),
291            // Data [7] — scalar or container.
292            Some(Element::Scalar {
293                tag: Tag::Context(7),
294                value: v,
295            }) => value = Some(v),
296            Some(Element::ContainerStart {
297                tag: Tag::Context(7),
298                kind,
299            }) => value = Some(read_container_value(r, kind)?),
300            Some(Element::ContainerStart { .. }) => skip_container(r)?,
301            Some(_) => {}
302        }
303    }
304    Ok(EventReportItem {
305        path,
306        event_number,
307        priority,
308        timestamp,
309        value: value.ok_or(ImError::MissingField("EventData.Data"))?,
310    })
311}
312
313/// Parse an `EventStatusIB` body (reader just after the struct start at ctx 0).
314///
315/// # Errors
316///
317/// Propagates a codec error if the input ends mid-container.
318fn parse_event_status(r: &mut TlvReader<'_>) -> Result<EventReport, ImError> {
319    let mut path = EventPath::default();
320    let mut status = 0u8;
321    loop {
322        match r.next()? {
323            None => return Err(ImError::Codec(matter_codec::Error::UnclosedContainer)),
324            Some(Element::ContainerEnd) => break,
325            // Path [0] — EventPathIB list.
326            Some(Element::ContainerStart {
327                tag: Tag::Context(0),
328                kind: ContainerKind::List,
329            }) => {
330                let members = read_container_members(r)?;
331                path = event_path_from_members(&members);
332            }
333            // Status [1] — StatusIB struct { 0: Status u8, 1: ClusterStatus? }.
334            Some(Element::ContainerStart {
335                tag: Tag::Context(1),
336                kind: ContainerKind::Structure,
337            }) => {
338                for (tag, v) in read_container_members(r)? {
339                    if let (Tag::Context(0), Value::Uint(n)) = (tag, v) {
340                        status = u8::try_from(n).unwrap_or(0);
341                    }
342                }
343            }
344            Some(Element::ContainerStart { .. }) => skip_container(r)?,
345            Some(_) => {}
346        }
347    }
348    Ok(EventReport::Status { path, status })
349}
350
351/// Parse a `DataReport`'s `eventReports[2]` array body (reader positioned just
352/// after the array start at ctx 2), pushing one [`EventReport`] per IB.
353///
354/// # Errors
355///
356/// Propagates any [`ImError`] from parsing an individual `EventReportIB`.
357pub(crate) fn parse_event_reports(
358    r: &mut TlvReader<'_>,
359    out: &mut Vec<EventReport>,
360) -> Result<(), ImError> {
361    loop {
362        match r.next()? {
363            None => return Err(ImError::Codec(matter_codec::Error::UnclosedContainer)),
364            Some(Element::ContainerEnd) => return Ok(()),
365            Some(Element::ContainerStart {
366                kind: ContainerKind::Structure,
367                ..
368            }) => {
369                if let Some(rep) = parse_event_report_ib(r)? {
370                    out.push(rep);
371                }
372            }
373            Some(Element::ContainerStart { .. }) => skip_container(r)?,
374            Some(_) => {}
375        }
376    }
377}
378
379#[cfg(test)]
380mod tests {
381    #![allow(clippy::unwrap_used, clippy::expect_used)] // Test code: CLAUDE.md test-code carve-out.
382    use super::*;
383    use matter_codec::{ContainerKind, Element, Tag, TlvReader, Value};
384
385    #[test]
386    fn event_path_encodes_as_list_with_tags_1_2_3() {
387        let mut buf = Vec::new();
388        let mut w = TlvWriter::new(&mut buf);
389        EventPath::concrete(0, 0x28, 0x00).write(&mut w).unwrap();
390        let mut r = TlvReader::new(&buf);
391        // EventPathIB is a LIST (not a struct).
392        assert!(matches!(
393            r.next().unwrap(),
394            Some(Element::ContainerStart {
395                tag: Tag::Anonymous,
396                kind: ContainerKind::List
397            })
398        ));
399        // Endpoint=tag 1, Cluster=tag 2, Event=tag 3 (NOT 2/3/4 like AttributePath).
400        assert!(matches!(
401            r.next().unwrap(),
402            Some(Element::Scalar {
403                tag: Tag::Context(1),
404                value: Value::Uint(0)
405            })
406        ));
407        assert!(matches!(
408            r.next().unwrap(),
409            Some(Element::Scalar {
410                tag: Tag::Context(2),
411                value: Value::Uint(0x28)
412            })
413        ));
414        assert!(matches!(
415            r.next().unwrap(),
416            Some(Element::Scalar {
417                tag: Tag::Context(3),
418                value: Value::Uint(0x00)
419            })
420        ));
421    }
422
423    #[test]
424    fn event_filter_encodes_as_struct() {
425        let mut buf = Vec::new();
426        let mut w = TlvWriter::new(&mut buf);
427        EventFilter::from_event_min(0).write(&mut w).unwrap();
428        let mut r = TlvReader::new(&buf);
429        // EventFilterIB is a STRUCTURE (not a list) — vectors-confirmed.
430        assert!(matches!(
431            r.next().unwrap(),
432            Some(Element::ContainerStart {
433                tag: Tag::Anonymous,
434                kind: ContainerKind::Structure
435            })
436        ));
437        assert!(matches!(
438            r.next().unwrap(),
439            Some(Element::Scalar {
440                tag: Tag::Context(1),
441                value: Value::Uint(0)
442            })
443        ));
444    }
445
446    #[test]
447    fn parses_event_data_ib() {
448        // EventReportIB { EventData[1] { Path[0](list){1:ep,2:cl,3:ev}, 1:num,
449        // 2:prio, 3:epoch, 7:data } }
450        let mut buf = Vec::new();
451        let mut w = TlvWriter::new(&mut buf);
452        w.start_structure(Tag::Anonymous).unwrap(); // EventReportIB
453        w.start_structure(Tag::Context(1)).unwrap(); // EventData
454        w.start_list(Tag::Context(0)).unwrap(); // Path (EventPathIB list)
455        w.put_uint(Tag::Context(1), 0).unwrap();
456        w.put_uint(Tag::Context(2), 0x28).unwrap();
457        w.put_uint(Tag::Context(3), 0x00).unwrap();
458        w.end_container().unwrap();
459        w.put_uint(Tag::Context(1), 1).unwrap(); // EventNumber
460        w.put_uint(Tag::Context(2), 2).unwrap(); // Priority = Critical
461        w.put_uint(Tag::Context(3), 0).unwrap(); // EpochTimestamp
462        w.put_uint(Tag::Context(7), 7).unwrap(); // Data (scalar for the test)
463        w.end_container().unwrap();
464        w.end_container().unwrap();
465
466        let mut r = TlvReader::new(&buf);
467        assert!(matches!(
468            r.next().unwrap(),
469            Some(Element::ContainerStart { .. })
470        ));
471        let rep = parse_event_report_ib(&mut r).unwrap().unwrap();
472        match rep {
473            EventReport::Data(it) => {
474                assert_eq!(it.path.endpoint, Some(0));
475                assert_eq!(it.path.cluster, Some(0x28));
476                assert_eq!(it.path.event, Some(0x00));
477                assert_eq!(it.event_number, 1);
478                assert_eq!(it.priority, EventPriority::Critical);
479                assert_eq!(it.timestamp, EventTimestamp::Epoch(0));
480                assert_eq!(it.value, Value::Uint(7));
481            }
482            EventReport::Status { .. } => panic!("expected Data, got Status"),
483        }
484    }
485
486    #[test]
487    fn parses_event_status_ib() {
488        // EventReportIB { EventStatus[0] { Path[0](list){1:ep,2:cl,3:ev},
489        // Status[1](struct){0:status} } }
490        let mut buf = Vec::new();
491        let mut w = TlvWriter::new(&mut buf);
492        w.start_structure(Tag::Anonymous).unwrap(); // EventReportIB
493        w.start_structure(Tag::Context(0)).unwrap(); // EventStatus
494        w.start_list(Tag::Context(0)).unwrap(); // Path
495        w.put_uint(Tag::Context(1), 1).unwrap();
496        w.put_uint(Tag::Context(2), 0x28).unwrap();
497        w.put_uint(Tag::Context(3), 0x02).unwrap();
498        w.end_container().unwrap();
499        w.start_structure(Tag::Context(1)).unwrap(); // Status (StatusIB)
500        w.put_uint(Tag::Context(0), 0x86).unwrap(); // UnsupportedEvent (example)
501        w.end_container().unwrap();
502        w.end_container().unwrap();
503        w.end_container().unwrap();
504
505        let mut r = TlvReader::new(&buf);
506        assert!(matches!(
507            r.next().unwrap(),
508            Some(Element::ContainerStart { .. })
509        ));
510        let rep = parse_event_report_ib(&mut r).unwrap().unwrap();
511        match rep {
512            EventReport::Status { path, status } => {
513                assert_eq!(path.endpoint, Some(1));
514                assert_eq!(path.event, Some(0x02));
515                assert_eq!(status, 0x86);
516            }
517            EventReport::Data(_) => panic!("expected Status, got Data"),
518        }
519    }
520}