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///
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/// Read an `EventPathIB` list's members into an [`EventPath`] (tags 0–4).
207fn event_path_from_members(members: &[(Tag, Value)]) -> EventPath {
208    let mut p = EventPath::default();
209    for (tag, v) in members {
210        match (tag, v) {
211            (Tag::Context(0), Value::Uint(n)) => p.node = Some(*n),
212            (Tag::Context(1), Value::Uint(n)) => p.endpoint = u16::try_from(*n).ok(),
213            (Tag::Context(2), Value::Uint(n)) => p.cluster = u32::try_from(*n).ok(),
214            (Tag::Context(3), Value::Uint(n)) => p.event = u32::try_from(*n).ok(),
215            (Tag::Context(4), Value::Bool(b)) => p.is_urgent = Some(*b),
216            _ => {}
217        }
218    }
219    p
220}
221
222/// Parse the body of one `EventReportIB` (reader positioned just after its struct
223/// start). Returns the report, or `None` for an empty IB.
224///
225/// # Errors
226///
227/// Returns [`ImError`] if the input ends mid-container, or an `EventData` is
228/// missing its `Data` member.
229fn parse_event_report_ib(r: &mut TlvReader<'_>) -> Result<Option<EventReport>, ImError> {
230    let mut out: Option<EventReport> = None;
231    loop {
232        match r.next()? {
233            None => return Err(ImError::Codec(matter_codec::Error::UnclosedContainer)),
234            Some(Element::ContainerEnd) => break,
235            // EventData [1]
236            Some(Element::ContainerStart {
237                tag: Tag::Context(1),
238                kind: ContainerKind::Structure,
239            }) => out = Some(EventReport::Data(parse_event_data(r)?)),
240            // EventStatus [0]
241            Some(Element::ContainerStart {
242                tag: Tag::Context(0),
243                kind: ContainerKind::Structure,
244            }) => out = Some(parse_event_status(r)?),
245            Some(Element::ContainerStart { .. }) => skip_container(r)?,
246            Some(_) => {}
247        }
248    }
249    Ok(out)
250}
251
252/// Parse an `EventDataIB` body (reader just after the struct start at ctx 1).
253///
254/// # Errors
255///
256/// Returns [`ImError::MissingField`] if `Data` (tag 7) is absent, or propagates a
257/// codec error.
258fn parse_event_data(r: &mut TlvReader<'_>) -> Result<EventReportItem, ImError> {
259    let mut path = EventPath::default();
260    let mut event_number = 0u64;
261    let mut priority = EventPriority::Unknown(0xFF);
262    let mut timestamp = EventTimestamp::None;
263    let mut value: Option<Value> = None;
264    loop {
265        match r.next()? {
266            None => return Err(ImError::Codec(matter_codec::Error::UnclosedContainer)),
267            Some(Element::ContainerEnd) => break,
268            // Path [0] — EventPathIB list.
269            Some(Element::ContainerStart {
270                tag: Tag::Context(0),
271                kind: ContainerKind::List,
272            }) => {
273                let members = read_container_members(r)?;
274                path = event_path_from_members(&members);
275            }
276            Some(Element::Scalar {
277                tag: Tag::Context(1),
278                value: Value::Uint(n),
279            }) => event_number = n,
280            Some(Element::Scalar {
281                tag: Tag::Context(2),
282                value: Value::Uint(n),
283            }) => priority = EventPriority::from_u8(u8::try_from(n).unwrap_or(0xFF)),
284            Some(Element::Scalar {
285                tag: Tag::Context(3),
286                value: Value::Uint(n),
287            }) => timestamp = EventTimestamp::Epoch(n),
288            Some(Element::Scalar {
289                tag: Tag::Context(4),
290                value: Value::Uint(n),
291            }) => timestamp = EventTimestamp::System(n),
292            Some(Element::Scalar {
293                tag: Tag::Context(5),
294                value: Value::Uint(n),
295            }) => timestamp = EventTimestamp::DeltaEpoch(n),
296            Some(Element::Scalar {
297                tag: Tag::Context(6),
298                value: Value::Uint(n),
299            }) => timestamp = EventTimestamp::DeltaSystem(n),
300            // Data [7] — scalar or container.
301            Some(Element::Scalar {
302                tag: Tag::Context(7),
303                value: v,
304            }) => value = Some(v),
305            Some(Element::ContainerStart {
306                tag: Tag::Context(7),
307                kind,
308            }) => value = Some(read_container_value(r, kind)?),
309            Some(Element::ContainerStart { .. }) => skip_container(r)?,
310            Some(_) => {}
311        }
312    }
313    Ok(EventReportItem {
314        path,
315        event_number,
316        priority,
317        timestamp,
318        value: value.ok_or(ImError::MissingField("EventData.Data"))?,
319    })
320}
321
322/// Parse an `EventStatusIB` body (reader just after the struct start at ctx 0).
323///
324/// # Errors
325///
326/// Propagates a codec error if the input ends mid-container.
327fn parse_event_status(r: &mut TlvReader<'_>) -> Result<EventReport, ImError> {
328    let mut path = EventPath::default();
329    let mut status = 0u8;
330    loop {
331        match r.next()? {
332            None => return Err(ImError::Codec(matter_codec::Error::UnclosedContainer)),
333            Some(Element::ContainerEnd) => break,
334            // Path [0] — EventPathIB list.
335            Some(Element::ContainerStart {
336                tag: Tag::Context(0),
337                kind: ContainerKind::List,
338            }) => {
339                let members = read_container_members(r)?;
340                path = event_path_from_members(&members);
341            }
342            // Status [1] — StatusIB struct { 0: Status u8, 1: ClusterStatus? }.
343            Some(Element::ContainerStart {
344                tag: Tag::Context(1),
345                kind: ContainerKind::Structure,
346            }) => {
347                for (tag, v) in read_container_members(r)? {
348                    if let (Tag::Context(0), Value::Uint(n)) = (tag, v) {
349                        status = u8::try_from(n).unwrap_or(0);
350                    }
351                }
352            }
353            Some(Element::ContainerStart { .. }) => skip_container(r)?,
354            Some(_) => {}
355        }
356    }
357    Ok(EventReport::Status { path, status })
358}
359
360/// Parse a `DataReport`'s `eventReports[2]` array body (reader positioned just
361/// after the array start at ctx 2), pushing one [`EventReport`] per IB.
362///
363/// # Errors
364///
365/// Propagates any [`ImError`] from parsing an individual `EventReportIB`.
366pub(crate) fn parse_event_reports(
367    r: &mut TlvReader<'_>,
368    out: &mut Vec<EventReport>,
369) -> Result<(), ImError> {
370    loop {
371        match r.next()? {
372            None => return Err(ImError::Codec(matter_codec::Error::UnclosedContainer)),
373            Some(Element::ContainerEnd) => return Ok(()),
374            Some(Element::ContainerStart {
375                kind: ContainerKind::Structure,
376                ..
377            }) => {
378                if let Some(rep) = parse_event_report_ib(r)? {
379                    out.push(rep);
380                }
381            }
382            Some(Element::ContainerStart { .. }) => skip_container(r)?,
383            Some(_) => {}
384        }
385    }
386}
387
388#[cfg(test)]
389mod tests {
390    #![allow(clippy::unwrap_used, clippy::expect_used)] // Test code: CLAUDE.md test-code carve-out.
391    use super::*;
392    use matter_codec::{ContainerKind, Element, Tag, TlvReader, Value};
393
394    #[test]
395    fn event_path_encodes_as_list_with_tags_1_2_3() {
396        let mut buf = Vec::new();
397        let mut w = TlvWriter::new(&mut buf);
398        EventPath::concrete(0, 0x28, 0x00).write(&mut w).unwrap();
399        let mut r = TlvReader::new(&buf);
400        // EventPathIB is a LIST (not a struct).
401        assert!(matches!(
402            r.next().unwrap(),
403            Some(Element::ContainerStart {
404                tag: Tag::Anonymous,
405                kind: ContainerKind::List
406            })
407        ));
408        // Endpoint=tag 1, Cluster=tag 2, Event=tag 3 (NOT 2/3/4 like AttributePath).
409        assert!(matches!(
410            r.next().unwrap(),
411            Some(Element::Scalar {
412                tag: Tag::Context(1),
413                value: Value::Uint(0)
414            })
415        ));
416        assert!(matches!(
417            r.next().unwrap(),
418            Some(Element::Scalar {
419                tag: Tag::Context(2),
420                value: Value::Uint(0x28)
421            })
422        ));
423        assert!(matches!(
424            r.next().unwrap(),
425            Some(Element::Scalar {
426                tag: Tag::Context(3),
427                value: Value::Uint(0x00)
428            })
429        ));
430    }
431
432    #[test]
433    fn event_filter_encodes_as_struct() {
434        let mut buf = Vec::new();
435        let mut w = TlvWriter::new(&mut buf);
436        EventFilter::from_event_min(0).write(&mut w).unwrap();
437        let mut r = TlvReader::new(&buf);
438        // EventFilterIB is a STRUCTURE (not a list) — vectors-confirmed.
439        assert!(matches!(
440            r.next().unwrap(),
441            Some(Element::ContainerStart {
442                tag: Tag::Anonymous,
443                kind: ContainerKind::Structure
444            })
445        ));
446        assert!(matches!(
447            r.next().unwrap(),
448            Some(Element::Scalar {
449                tag: Tag::Context(1),
450                value: Value::Uint(0)
451            })
452        ));
453    }
454
455    #[test]
456    fn parses_event_data_ib() {
457        // EventReportIB { EventData[1] { Path[0](list){1:ep,2:cl,3:ev}, 1:num,
458        // 2:prio, 3:epoch, 7:data } }
459        let mut buf = Vec::new();
460        let mut w = TlvWriter::new(&mut buf);
461        w.start_structure(Tag::Anonymous).unwrap(); // EventReportIB
462        w.start_structure(Tag::Context(1)).unwrap(); // EventData
463        w.start_list(Tag::Context(0)).unwrap(); // Path (EventPathIB list)
464        w.put_uint(Tag::Context(1), 0).unwrap();
465        w.put_uint(Tag::Context(2), 0x28).unwrap();
466        w.put_uint(Tag::Context(3), 0x00).unwrap();
467        w.end_container().unwrap();
468        w.put_uint(Tag::Context(1), 1).unwrap(); // EventNumber
469        w.put_uint(Tag::Context(2), 2).unwrap(); // Priority = Critical
470        w.put_uint(Tag::Context(3), 0).unwrap(); // EpochTimestamp
471        w.put_uint(Tag::Context(7), 7).unwrap(); // Data (scalar for the test)
472        w.end_container().unwrap();
473        w.end_container().unwrap();
474
475        let mut r = TlvReader::new(&buf);
476        assert!(matches!(
477            r.next().unwrap(),
478            Some(Element::ContainerStart { .. })
479        ));
480        let rep = parse_event_report_ib(&mut r).unwrap().unwrap();
481        match rep {
482            EventReport::Data(it) => {
483                assert_eq!(it.path.endpoint, Some(0));
484                assert_eq!(it.path.cluster, Some(0x28));
485                assert_eq!(it.path.event, Some(0x00));
486                assert_eq!(it.event_number, 1);
487                assert_eq!(it.priority, EventPriority::Critical);
488                assert_eq!(it.timestamp, EventTimestamp::Epoch(0));
489                assert_eq!(it.value, Value::Uint(7));
490            }
491            EventReport::Status { .. } => panic!("expected Data, got Status"),
492        }
493    }
494
495    #[test]
496    fn parses_event_status_ib() {
497        // EventReportIB { EventStatus[0] { Path[0](list){1:ep,2:cl,3:ev},
498        // Status[1](struct){0:status} } }
499        let mut buf = Vec::new();
500        let mut w = TlvWriter::new(&mut buf);
501        w.start_structure(Tag::Anonymous).unwrap(); // EventReportIB
502        w.start_structure(Tag::Context(0)).unwrap(); // EventStatus
503        w.start_list(Tag::Context(0)).unwrap(); // Path
504        w.put_uint(Tag::Context(1), 1).unwrap();
505        w.put_uint(Tag::Context(2), 0x28).unwrap();
506        w.put_uint(Tag::Context(3), 0x02).unwrap();
507        w.end_container().unwrap();
508        w.start_structure(Tag::Context(1)).unwrap(); // Status (StatusIB)
509        w.put_uint(Tag::Context(0), 0x86).unwrap(); // UnsupportedEvent (example)
510        w.end_container().unwrap();
511        w.end_container().unwrap();
512        w.end_container().unwrap();
513
514        let mut r = TlvReader::new(&buf);
515        assert!(matches!(
516            r.next().unwrap(),
517            Some(Element::ContainerStart { .. })
518        ));
519        let rep = parse_event_report_ib(&mut r).unwrap().unwrap();
520        match rep {
521            EventReport::Status { path, status } => {
522                assert_eq!(path.endpoint, Some(1));
523                assert_eq!(path.event, Some(0x02));
524                assert_eq!(status, 0x86);
525            }
526            EventReport::Data(_) => panic!("expected Status, got Data"),
527        }
528    }
529}