Skip to main content

matc/
im.rs

1//! Typed Interaction Model report layer.
2//!
3//! Parses ReportData and SubscribeResponse messages into typed structures,
4//! replacing manual TLV path navigation. Used by [crate::controller::Connection]
5//! for chunked report reassembly (reads and subscriptions) and by the
6//! subscription event stream ([crate::controller::Subscription]).
7
8use anyhow::Result;
9
10use crate::tlv::{TlvItem, TlvItemValue};
11
12/// Attribute path from an AttributePathIB. Fields absent in the report
13/// (e.g. wildcard responses with unusual encodings) are None.
14#[derive(Debug, Clone, PartialEq)]
15pub struct AttributePath {
16    pub endpoint: Option<u16>,
17    pub cluster: Option<u32>,
18    pub attribute: Option<u32>,
19}
20
21/// Payload of a single attribute report - either a value or a status code.
22#[derive(Debug, Clone, PartialEq)]
23pub enum AttributeData {
24    Value(TlvItemValue),
25    Status { status: u8, cluster_status: Option<u8> },
26}
27
28/// One decoded AttributeReportIB.
29#[derive(Debug, Clone, PartialEq)]
30pub struct AttributeReport {
31    pub path: AttributePath,
32    pub data: AttributeData,
33    pub data_version: Option<u32>,
34}
35
36/// One decoded EventReportIB.
37#[derive(Debug, Clone, PartialEq)]
38pub struct EventReport {
39    pub endpoint: Option<u16>,
40    pub cluster: Option<u32>,
41    pub event: Option<u32>,
42    pub event_number: Option<u64>,
43    pub data: Option<TlvItemValue>,
44}
45
46/// Decoded ReportData message (single chunk or merged multi-chunk).
47#[derive(Debug, Clone, Default)]
48pub struct ReportData {
49    pub subscription_id: Option<u32>,
50    pub attribute_reports: Vec<AttributeReport>,
51    pub event_reports: Vec<EventReport>,
52    pub more_chunks: bool,
53    pub suppress_response: bool,
54}
55
56impl ReportData {
57    /// Parse the TLV payload of an IM ReportData message.
58    /// Missing AttributeReports/EventReports lists are not an error
59    /// (status-only or event-only reports).
60    pub fn parse(tlv: &TlvItem) -> Result<ReportData> {
61        let mut out = ReportData {
62            subscription_id: tlv.get_u32(&[0]),
63            more_chunks: tlv.get_bool(&[3]).unwrap_or(false),
64            suppress_response: tlv.get_bool(&[4]).unwrap_or(false),
65            ..Default::default()
66        };
67
68        if let Some(reports) = tlv.get_item(&[1]) {
69            if let TlvItemValue::List(list) = &reports.value {
70                for ib in list {
71                    if let Some(report) = parse_attribute_report_ib(ib) {
72                        out.attribute_reports.push(report);
73                    }
74                }
75            }
76        }
77
78        if let Some(reports) = tlv.get_item(&[2]) {
79            if let TlvItemValue::List(list) = &reports.value {
80                for ib in list {
81                    out.event_reports.push(EventReport {
82                        endpoint: ib.get_u16(&[1, 0, 1]),
83                        cluster: ib.get_u32(&[1, 0, 2]),
84                        event: ib.get_u32(&[1, 0, 3]),
85                        event_number: ib.get_u64(&[1, 1]),
86                        data: ib.get(&[1, 7]).cloned(),
87                    });
88                }
89            }
90        }
91
92        Ok(out)
93    }
94
95    /// Append reports from the next chunk; flags are taken from the last chunk.
96    pub fn merge(&mut self, next: ReportData) {
97        if self.subscription_id.is_none() {
98            self.subscription_id = next.subscription_id;
99        }
100        self.attribute_reports.extend(next.attribute_reports);
101        self.event_reports.extend(next.event_reports);
102        self.more_chunks = next.more_chunks;
103        self.suppress_response = next.suppress_response;
104    }
105}
106
107fn parse_attribute_report_ib(ib: &TlvItem) -> Option<AttributeReport> {
108    if let Some(data) = ib.get(&[1, 2]) {
109        Some(AttributeReport {
110            path: AttributePath {
111                endpoint: ib.get_u16(&[1, 1, 2]),
112                cluster: ib.get_u32(&[1, 1, 3]),
113                attribute: ib.get_u32(&[1, 1, 4]),
114            },
115            data: AttributeData::Value(data.clone()),
116            data_version: ib.get_u32(&[1, 0]),
117        })
118    } else {
119        ib.get_u8(&[0, 1, 0]).map(|status| AttributeReport {
120            path: AttributePath {
121                endpoint: ib.get_u16(&[0, 0, 2]),
122                cluster: ib.get_u32(&[0, 0, 3]),
123                attribute: ib.get_u32(&[0, 0, 4]),
124            },
125            data: AttributeData::Status {
126                status,
127                cluster_status: ib.get_u8(&[0, 1, 1]),
128            },
129            data_version: None,
130        })
131    }
132}
133
134/// Decoded SubscribeResponse message.
135#[derive(Debug, Clone)]
136pub struct SubscribeResponse {
137    pub subscription_id: u32,
138    pub max_interval: u16,
139}
140
141impl SubscribeResponse {
142    /// Parse the TLV payload of an IM SubscribeResponse message.
143    pub fn parse(tlv: &TlvItem) -> Result<SubscribeResponse> {
144        Ok(SubscribeResponse {
145            subscription_id: tlv
146                .get_u32(&[0])
147                .ok_or_else(|| anyhow::anyhow!("subscribe response missing subscription id"))?,
148            max_interval: tlv.get_u16(&[2]).unwrap_or(0),
149        })
150    }
151}
152
153/// One reassembled subscription update delivered by
154/// [crate::controller::Subscription::next].
155#[derive(Debug, Clone)]
156pub struct ReportUpdate {
157    pub subscription_id: u32,
158    pub attribute_reports: Vec<AttributeReport>,
159    pub event_reports: Vec<EventReport>,
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165    use crate::device_messages::{self, AttrReport};
166    use crate::messages::ProtocolMessageHeader;
167    use crate::tlv;
168
169    fn parse_payload(msg: &[u8]) -> ReportData {
170        let (_, rest) = ProtocolMessageHeader::decode(msg).unwrap();
171        let tlv = tlv::decode_tlv(&rest).unwrap();
172        ReportData::parse(&tlv).unwrap()
173    }
174
175    fn bool_value_tlv(value: bool) -> Vec<u8> {
176        let mut buf = tlv::TlvBuffer::new();
177        buf.write_bool(2, value).unwrap();
178        buf.data
179    }
180
181    #[test]
182    fn test_parse_data_report() {
183        let value_tlv = bool_value_tlv(true);
184        let msg = device_messages::im_report_data(
185            7,
186            &[AttrReport::Data { endpoint: 1, cluster: 6, attribute: 0, value_tlv }],
187            -1,
188            Some(0x1234),
189            false,
190        )
191        .unwrap();
192        let rd = parse_payload(&msg);
193        assert_eq!(rd.subscription_id, Some(0x1234));
194        assert!(!rd.more_chunks);
195        assert_eq!(rd.attribute_reports.len(), 1);
196        let rep = &rd.attribute_reports[0];
197        assert_eq!(rep.path.endpoint, Some(1));
198        assert_eq!(rep.path.cluster, Some(6));
199        assert_eq!(rep.path.attribute, Some(0));
200        assert_eq!(rep.data, AttributeData::Value(TlvItemValue::Bool(true)));
201        assert_eq!(rep.data_version, Some(0));
202    }
203
204    #[test]
205    fn test_parse_status_report() {
206        let msg = device_messages::im_report_data_status(7, 1, 6, 0, 0x86, -1).unwrap();
207        let rd = parse_payload(&msg);
208        assert_eq!(rd.attribute_reports.len(), 1);
209        let rep = &rd.attribute_reports[0];
210        assert_eq!(rep.path.endpoint, Some(1));
211        assert_eq!(
212            rep.data,
213            AttributeData::Status { status: 0x86, cluster_status: None }
214        );
215    }
216
217    #[test]
218    fn test_parse_mixed_reports_and_chunk_flag() {
219        let value_tlv = bool_value_tlv(false);
220        let msg = device_messages::im_report_data(
221            7,
222            &[
223                AttrReport::Data { endpoint: 1, cluster: 6, attribute: 0, value_tlv },
224                AttrReport::Status { endpoint: 2, cluster: 8, attribute: 3, status: 1 },
225            ],
226            -1,
227            None,
228            true,
229        )
230        .unwrap();
231        let rd = parse_payload(&msg);
232        assert_eq!(rd.subscription_id, None);
233        assert!(rd.more_chunks);
234        assert_eq!(rd.attribute_reports.len(), 2);
235        assert!(matches!(rd.attribute_reports[0].data, AttributeData::Value(_)));
236        assert!(matches!(rd.attribute_reports[1].data, AttributeData::Status { status: 1, .. }));
237    }
238
239    #[test]
240    fn test_parse_suppress_response() {
241        let mut buf = tlv::TlvBuffer::new();
242        buf.write_anon_struct().unwrap();
243        buf.write_array(1).unwrap();
244        buf.write_struct_end().unwrap();
245        buf.write_bool(4, true).unwrap();
246        buf.write_struct_end().unwrap();
247        let tlv = tlv::decode_tlv(&buf.data).unwrap();
248        let rd = ReportData::parse(&tlv).unwrap();
249        assert!(rd.suppress_response);
250        assert!(rd.attribute_reports.is_empty());
251    }
252
253    #[test]
254    fn test_parse_event_report() {
255        let mut buf = tlv::TlvBuffer::new();
256        buf.write_anon_struct().unwrap();
257        buf.write_uint32(0, 99).unwrap();
258        buf.write_array(2).unwrap();
259        buf.write_anon_struct().unwrap();
260        buf.write_struct(1).unwrap();
261        buf.write_list(0).unwrap();
262        buf.write_uint16(1, 1).unwrap();
263        buf.write_uint32(2, 0x101).unwrap();
264        buf.write_uint32(3, 2).unwrap();
265        buf.write_struct_end().unwrap();
266        buf.write_uint64(1, 42).unwrap();
267        buf.write_uint8(7, 5).unwrap();
268        buf.write_struct_end().unwrap();
269        buf.write_struct_end().unwrap();
270        buf.write_struct_end().unwrap();
271        let tlv = tlv::decode_tlv(&buf.data).unwrap();
272        let rd = ReportData::parse(&tlv).unwrap();
273        assert_eq!(rd.subscription_id, Some(99));
274        assert_eq!(rd.event_reports.len(), 1);
275        let ev = &rd.event_reports[0];
276        assert_eq!(ev.endpoint, Some(1));
277        assert_eq!(ev.cluster, Some(0x101));
278        assert_eq!(ev.event, Some(2));
279        assert_eq!(ev.event_number, Some(42));
280        assert_eq!(ev.data, Some(TlvItemValue::Int(5)));
281    }
282
283    #[test]
284    fn test_parse_missing_lists() {
285        let mut buf = tlv::TlvBuffer::new();
286        buf.write_anon_struct().unwrap();
287        buf.write_struct_end().unwrap();
288        let tlv = tlv::decode_tlv(&buf.data).unwrap();
289        let rd = ReportData::parse(&tlv).unwrap();
290        assert!(rd.attribute_reports.is_empty());
291        assert!(rd.event_reports.is_empty());
292        assert!(!rd.more_chunks);
293        assert!(!rd.suppress_response);
294    }
295
296    #[test]
297    fn test_merge() {
298        let v1 = bool_value_tlv(true);
299        let chunk1 = parse_payload(
300            &device_messages::im_report_data(
301                7,
302                &[AttrReport::Data { endpoint: 1, cluster: 6, attribute: 0, value_tlv: v1 }],
303                -1,
304                Some(5),
305                true,
306            )
307            .unwrap(),
308        );
309        let v2 = bool_value_tlv(false);
310        let chunk2 = parse_payload(
311            &device_messages::im_report_data(
312                7,
313                &[AttrReport::Data { endpoint: 2, cluster: 6, attribute: 0, value_tlv: v2 }],
314                -1,
315                None,
316                false,
317            )
318            .unwrap(),
319        );
320        let mut merged = chunk1;
321        assert!(merged.more_chunks);
322        merged.merge(chunk2);
323        assert!(!merged.more_chunks);
324        assert_eq!(merged.subscription_id, Some(5));
325        assert_eq!(merged.attribute_reports.len(), 2);
326        assert_eq!(merged.attribute_reports[1].path.endpoint, Some(2));
327    }
328
329    #[test]
330    fn test_parse_subscribe_response() {
331        let msg = device_messages::im_subscribe_response(77, 9, -1, 60).unwrap();
332        let (_, rest) = ProtocolMessageHeader::decode(&msg).unwrap();
333        let tlv = tlv::decode_tlv(&rest).unwrap();
334        let sr = SubscribeResponse::parse(&tlv).unwrap();
335        assert_eq!(sr.subscription_id, 77);
336        assert_eq!(sr.max_interval, 60);
337    }
338}