Skip to main content

rs_matter/im/encoding/
attr.rs

1/*
2 *
3 *    Copyright (c) 2025-2026 Project CHIP Authors
4 *
5 *    Licensed under the Apache License, Version 2.0 (the "License");
6 *    you may not use this file except in compliance with the License.
7 *    You may obtain a copy of the License at
8 *
9 *        http://www.apache.org/licenses/LICENSE-2.0
10 *
11 *    Unless required by applicable law or agreed to in writing, software
12 *    distributed under the License is distributed on an "AS IS" BASIS,
13 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 *    See the License for the specific language governing permissions and
15 *    limitations under the License.
16 */
17
18use crate::error::{Error, ErrorCode};
19use crate::im::{EventFilter, NodeId};
20use crate::tlv::{FromTLV, Nullable, TLVArray, TLVElement, ToTLV};
21
22use super::{AttrId, ClusterId, EndptId, EventPath, EventResp, GenericPath, IMStatusCode, Status};
23
24pub use read::*;
25pub use read_builder::*;
26pub use subscribe::*;
27pub use subscribe_builder::*;
28pub use write::*;
29pub use write_builder::*;
30
31mod read;
32mod read_builder;
33mod subscribe;
34mod subscribe_builder;
35mod write;
36mod write_builder;
37
38/// A path to an attribute in the Interaction Model.
39///
40/// Corresponds to the `AttrPathIB` TLV structure in the Interaction Model.
41#[derive(Default, Clone, Debug, PartialEq, Eq, Hash, FromTLV, ToTLV)]
42#[cfg_attr(feature = "defmt", derive(defmt::Format))]
43#[tlvargs(datatype = "list")]
44pub struct AttrPath {
45    /// `EnableTagCompression` per Matter Core spec. When set
46    /// to `true`, the spec defines a "tag compression scheme" whereby
47    /// omitted fields in this `AttrPath` should be inherited from the
48    /// previous `AttrPath` in the same list (rather than treated as
49    /// wildcards).
50    ///
51    /// **rs-matter parses this bit but does not act on it.**
52    /// Omitted fields are always treated as wildcards regardless of value.
53    ///
54    /// This matches the de-facto behaviour of every other Matter
55    /// implementation in the wild:
56    ///   - chip's `AttributePathIB::Parser::ParsePath` reads the bit
57    ///     and ignores its semantics, treating omitted = wildcard.
58    ///   - matter.js explicitly leaves it unimplemented with a TODO
59    ///     comment "or likely remove it".
60    ///   - per <https://github.com/project-chip/connectedhomeip/issues/29359>,
61    ///     neither chip-tool nor Google's controllers handle
62    ///     tag-compressed reports either, and the spec feature is
63    ///     widely expected to be removed rather than implemented.
64    ///
65    /// If/when that landscape changes, real inheritance semantics
66    /// would slot into `PathExpanderIterator` in the `im::expand` module.
67    pub tag_compression: Option<bool>,
68    pub node: Option<NodeId>,
69    pub endpoint: Option<EndptId>,
70    pub cluster: Option<ClusterId>,
71    pub attr: Option<AttrId>,
72    pub list_index: Option<Nullable<u16>>,
73}
74
75/// Tags corresponding to the fields in the `AttributePathIB` TLV
76/// structure (Matter Core spec). `AttrPath` is encoded as a
77/// TLV *list* with positional context tags 0..5. Used by callers that
78/// need to perform low-level TLV serde on `AttrPath` data.
79#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
80#[cfg_attr(feature = "defmt", derive(defmt::Format))]
81#[repr(u8)]
82pub enum AttrPathTag {
83    TagCompression = 0,
84    Node = 1,
85    Endpoint = 2,
86    Cluster = 3,
87    Attribute = 4,
88    ListIndex = 5,
89}
90
91impl AttrPath {
92    /// Create a new `AttrPath` from the provided `GenericPath`,
93    /// filling all fields which are not provided with their default values.
94    pub const fn from_gp(path: &GenericPath) -> Self {
95        Self {
96            endpoint: path.endpoint,
97            cluster: path.cluster,
98            attr: path.leaf,
99            tag_compression: None,
100            node: None,
101            list_index: None,
102        }
103    }
104
105    /// Convert this `AttrPath` to a `GenericPath`.
106    pub const fn to_gp(&self) -> GenericPath {
107        GenericPath::new(self.endpoint, self.cluster, self.attr)
108    }
109
110    /// Return true, if the path is wildcard
111    pub const fn is_wildcard(&self) -> bool {
112        self.endpoint.is_none() || self.cluster.is_none() || self.attr.is_none()
113    }
114}
115
116/// A status response for an attribute in the Interaction Model.
117///
118/// Corresponds to the `AttrStatusIB` TLV structure in the Interaction Model.
119#[derive(Debug, Clone, PartialEq, Eq, Hash, FromTLV, ToTLV)]
120#[cfg_attr(feature = "defmt", derive(defmt::Format))]
121pub struct AttrStatus {
122    /// The path to the attribute.
123    pub path: AttrPath,
124    /// The status of the attribute operation.
125    pub status: Status,
126}
127
128impl AttrStatus {
129    /// Create a new `AttrStatus` with the given path, status code, and optional cluster status.
130    pub const fn new(path: AttrPath, status: IMStatusCode, cluster_status: Option<u16>) -> Self {
131        Self {
132            path,
133            status: Status::new(status, cluster_status),
134        }
135    }
136
137    /// Create a new `AttrStatus` from a `GenericPath`, status code, and optional cluster status.
138    ///
139    /// ATTENTION: the actual reply `AttrPath` will be filled with the `GenericPath` values,
140    /// however these are not necessarily expressing the full path of the incoming data as `AttrPath` does.
141    ///
142    /// Hence, this method is primarily useful for unit tests.
143    pub const fn from_gp(
144        path: &GenericPath,
145        status: IMStatusCode,
146        cluster_status: Option<u16>,
147    ) -> Self {
148        Self::new(AttrPath::from_gp(path), status, cluster_status)
149    }
150}
151
152/// A data response for an attribute in the Interaction Model.
153///
154/// Corresponds to the `AttrDataIB` TLV structure in the Interaction Model.
155#[derive(Debug, Clone, PartialEq, FromTLV, ToTLV)]
156#[cfg_attr(feature = "defmt", derive(defmt::Format))]
157#[tlvargs(lifetime = "'a")]
158pub struct AttrData<'a> {
159    /// The cluster dataver
160    pub data_ver: Option<u32>,
161    /// The path to the attribute.
162    pub path: AttrPath,
163    /// The data for the attribute, represented as a TLV element.
164    pub data: TLVElement<'a>,
165}
166
167impl<'a> AttrData<'a> {
168    /// Create a new `AttrData` with the given data version, path, and data.
169    pub const fn new(data_ver: Option<u32>, path: AttrPath, data: TLVElement<'a>) -> Self {
170        Self {
171            data_ver,
172            path,
173            data,
174        }
175    }
176}
177
178/// Tags corresponding to the fields in the `AttrDataIB` TLV structure.
179///
180/// Used when there is a need to perform low-level TLV serde on
181/// 1AttrDataIB` structures.
182#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
183#[cfg_attr(feature = "defmt", derive(defmt::Format))]
184#[repr(u8)]
185pub enum AttrDataTag {
186    DataVer = 0,
187    Path = 1,
188    Data = 2,
189}
190
191/// Attribute Response
192///
193/// Corresponds to the `AttributeReportIB` TLV structure in the Interaction Model.
194#[derive(Clone, FromTLV, ToTLV, PartialEq, Debug)]
195#[cfg_attr(feature = "defmt", derive(defmt::Format))]
196#[tlvargs(lifetime = "'a")]
197pub enum AttrResp<'a> {
198    Status(AttrStatus),
199    Data(AttrData<'a>),
200}
201
202impl<'a> From<AttrData<'a>> for AttrResp<'a> {
203    fn from(value: AttrData<'a>) -> Self {
204        Self::Data(value)
205    }
206}
207
208impl From<AttrStatus> for AttrResp<'_> {
209    fn from(value: AttrStatus) -> Self {
210        Self::Status(value)
211    }
212}
213
214/// Tags corresponding to the fields in the `AttributeReportIB` TLV structure.
215///
216/// Used when there is a need to perform low-level TLV serde on
217/// `AttributeReportIB` structures.
218#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
219#[cfg_attr(feature = "defmt", derive(defmt::Format))]
220#[repr(u8)]
221pub enum AttrRespTag {
222    Status = 0,
223    Data = 1,
224}
225
226/// Cluster Path
227///
228/// Corresponds to the `ClusterPathIB` TLV structure in the Interaction Model.
229#[derive(Default, Debug, Clone, PartialEq, Eq, Hash, FromTLV, ToTLV)]
230#[tlvargs(datatype = "list")]
231#[cfg_attr(feature = "defmt", derive(defmt::Format))]
232pub struct ClusterPath {
233    pub node: Option<u64>,
234    pub endpoint: EndptId,
235    pub cluster: ClusterId,
236}
237
238/// Data Version Filter
239///
240/// Corresponds to the `DataVersionFilterIB` TLV structure in the Interaction Model.
241#[derive(Default, Debug, Clone, PartialEq, Eq, Hash, FromTLV, ToTLV)]
242#[cfg_attr(feature = "defmt", derive(defmt::Format))]
243pub struct DataVersionFilter {
244    pub path: ClusterPath,
245    pub data_ver: u32,
246}
247
248/// A wrapper enum for `ReadReq` and `SubscribeReq` that allows downstream code to
249/// treat the two in a unified manner with regards to `OpCode::ReportDataResp` type responses.
250#[derive(Debug, Clone, Eq, PartialEq, Hash)]
251#[cfg_attr(feature = "defmt", derive(defmt::Format))]
252pub enum ReportDataReq<'a> {
253    Read(&'a ReadReq<'a>),
254    Subscribe(&'a SubscribeReq<'a>),
255    SubscribeReport(&'a SubscribeReq<'a>),
256}
257
258impl<'a> ReportDataReq<'a> {
259    pub fn attr_requests(&self) -> Result<Option<TLVArray<'a, AttrPath>>, Error> {
260        match self {
261            Self::Read(req) => req.attr_requests(),
262            Self::Subscribe(req) | Self::SubscribeReport(req) => req.attr_requests(),
263        }
264    }
265
266    pub fn event_requests(&self) -> Result<Option<TLVArray<'a, EventPath>>, Error> {
267        match self {
268            Self::Read(req) => req.event_requests(),
269            Self::Subscribe(req) | Self::SubscribeReport(req) => req.event_requests(),
270        }
271    }
272
273    pub fn dataver_filters(&self) -> Result<Option<TLVArray<'_, DataVersionFilter>>, Error> {
274        match self {
275            Self::Read(req) => req.dataver_filters(),
276            Self::Subscribe(req) => req.dataver_filters(),
277            Self::SubscribeReport(_) => Ok(None),
278        }
279    }
280
281    pub fn event_filters(&self) -> Result<Option<TLVArray<'_, EventFilter>>, Error> {
282        match self {
283            Self::Read(req) => req.event_filters(),
284            Self::Subscribe(req) | Self::SubscribeReport(req) => req.event_filters(),
285        }
286    }
287
288    pub fn fabric_filtered(&self) -> Result<bool, Error> {
289        match self {
290            Self::Read(req) => req.fabric_filtered(),
291            Self::Subscribe(req) | Self::SubscribeReport(req) => req.fabric_filtered(),
292        }
293    }
294}
295
296/// Report Data Message
297///
298/// Corresponds to the `ReportDataMessage` TLV structure in the Interaction Model.
299#[derive(FromTLV, ToTLV, Debug)]
300#[cfg_attr(feature = "defmt", derive(defmt::Format))]
301#[tlvargs(lifetime = "'a")]
302pub struct ReportDataResp<'a> {
303    pub subscription_id: Option<u32>,
304    pub attr_reports: Option<TLVArray<'a, AttrResp<'a>>>,
305    pub event_reports: Option<TLVArray<'a, EventResp<'a>>>,
306    pub more_chunks: Option<bool>,
307    pub suppress_response: Option<bool>,
308    /// `interactionModelRevision` (TLV context tag `0xFF`). Mandatory in
309    /// every IM message we send; modelled as `Option<u8>` so we tolerate
310    /// peers that omit it (the C++ SDK is tolerant in practice).
311    #[tagval(crate::im::encoding::IM_REVISION_TAG)]
312    pub interaction_model_revision: Option<u8>,
313}
314
315impl<'a> ReportDataResp<'a> {
316    /// Iterate the entries in `attr_reports` whose path matches the
317    /// given `(cluster, attr)` pair, in `(endpoint, result)` form.
318    ///
319    /// - **`Ok(T)`** — `AttrResp::Data` entry; the embedded `data` is
320    ///   decoded via `FromTLV` into `T`.
321    /// - **`Err(_)`** — `AttrResp::Status` entry; the `IMStatusCode`
322    ///   becomes an [`Error`]. This catches access-check failures
323    ///   (`UnsupportedAccess`, …) and `Unsupported{Endpoint,Cluster,Attribute}`
324    ///   uniformly — the peer echoes the requested path on status, so
325    ///   the filter still matches those entries.
326    /// - Entries with non-matching cluster/attr are silently skipped.
327    /// - Entries with an absent endpoint in the path are skipped
328    ///   (would indicate a malformed report).
329    ///
330    /// Wildcard reads (path missing endpoint, cluster, or attr in the
331    /// request) legally produce multiple matching reports — the
332    /// iterator yields one per expanded path, in wire order.
333    pub fn attrs<T>(
334        &self,
335        cluster: ClusterId,
336        attr: AttrId,
337    ) -> impl Iterator<Item = (EndptId, Result<T, Error>)> + use<'_, 'a, T>
338    where
339        T: FromTLV<'a> + 'a,
340    {
341        self.attr_reports
342            .as_ref()
343            .into_iter()
344            .flat_map(|arr| arr.iter())
345            .filter_map(move |resp| filter_attr_resp::<T>(resp.ok()?, cluster, attr))
346    }
347}
348
349/// Helper for [`ReportDataResp::attrs`] — extracts `(endpoint,
350/// Result<T, Error>)` from a single `AttrResp` if it matches the
351/// requested `(cluster, attr)` filter.
352fn filter_attr_resp<'a, T>(
353    resp: AttrResp<'a>,
354    cluster: ClusterId,
355    attr: AttrId,
356) -> Option<(EndptId, Result<T, Error>)>
357where
358    T: FromTLV<'a>,
359{
360    match resp {
361        AttrResp::Data(data) => {
362            if data.path.cluster != Some(cluster) || data.path.attr != Some(attr) {
363                return None;
364            }
365            let endpoint = data.path.endpoint?;
366            Some((endpoint, T::from_tlv(&data.data)))
367        }
368        AttrResp::Status(s) => {
369            if s.path.cluster != Some(cluster) || s.path.attr != Some(attr) {
370                return None;
371            }
372            let endpoint = s.path.endpoint?;
373            let err: Error = s
374                .status
375                .status
376                .to_error_code()
377                .unwrap_or(ErrorCode::Failure)
378                .into();
379            Some((endpoint, Err(err)))
380        }
381    }
382}
383
384/// Tags corresponding to the fields in the `ReportDataMessage` TLV structure.
385///
386/// Used when there is a need to perform low-level TLV serde on
387/// `ReportDataMessage` structures.
388#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
389#[cfg_attr(feature = "defmt", derive(defmt::Format))]
390#[repr(u8)]
391pub enum ReportDataRespTag {
392    SubscriptionId = 0,
393    AttributeReports = 1,
394    EventReports = 2,
395    MoreChunkedMsgs = 3,
396    SupressResponse = 4,
397}