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