Skip to main content

matter_interaction/
path.rs

1//! Concrete IM paths: `CommandPathIB` and `AttributePathIB` โ€” Matter
2//! Appendix A.6.
3
4#![forbid(unsafe_code)]
5
6use crate::error::ImError;
7use matter_codec::{Tag, Value};
8
9/// A concrete command path: `(endpoint, cluster, command)`.
10///
11/// Encoded as a `CommandPathIB` TLV **list** (Matter Appendix A.6):
12/// context tag 0 = endpoint, 1 = cluster, 2 = command.
13#[derive(Copy, Clone, Debug, PartialEq, Eq)]
14pub struct CommandPath {
15    /// Matter endpoint (always 0 for commissioning).
16    pub endpoint: u16,
17    /// Cluster ID.
18    pub cluster: u32,
19    /// Command ID.
20    pub command: u32,
21}
22
23/// A concrete attribute path: `(endpoint, cluster, attribute)`.
24///
25/// Encoded as an `AttributePathIB` TLV **list** (Matter Appendix A.6):
26/// context tag 2 = endpoint, 3 = cluster, 4 = attribute. Commissioning
27/// reads only concrete attributes, so no wildcard/list-index fields are
28/// emitted.
29#[derive(Copy, Clone, Debug, PartialEq, Eq)]
30pub struct AttributePath {
31    /// Matter endpoint.
32    pub endpoint: u16,
33    /// Cluster ID.
34    pub cluster: u32,
35    /// Attribute ID.
36    pub attribute: u32,
37}
38
39/// A read-request attribute path with optional (wildcard) components. A `None`
40/// field is **omitted** from the encoded `AttributePathIB`, which the Matter IM
41/// interprets as a wildcard (Appendix A.6): omit `attribute` โ†’ all attributes of
42/// the cluster; omit `endpoint` โ†’ all endpoints; etc. Responses are always keyed
43/// by a concrete [`AttributePath`].
44#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
45pub struct ReadPath {
46    /// Endpoint, or `None` for all endpoints.
47    pub endpoint: Option<u16>,
48    /// Cluster, or `None` for all clusters.
49    pub cluster: Option<u32>,
50    /// Attribute, or `None` for all attributes.
51    pub attribute: Option<u32>,
52}
53
54impl ReadPath {
55    /// A concrete `(endpoint, cluster, attribute)` path (no wildcards).
56    #[must_use]
57    pub fn concrete(endpoint: u16, cluster: u32, attribute: u32) -> Self {
58        Self {
59            endpoint: Some(endpoint),
60            cluster: Some(cluster),
61            attribute: Some(attribute),
62        }
63    }
64
65    /// All attributes of `cluster` on `endpoint`.
66    #[must_use]
67    pub fn cluster(endpoint: u16, cluster: u32) -> Self {
68        Self {
69            endpoint: Some(endpoint),
70            cluster: Some(cluster),
71            attribute: None,
72        }
73    }
74
75    /// Every attribute on every endpoint/cluster (full wildcard).
76    #[must_use]
77    pub fn all() -> Self {
78        Self {
79            endpoint: None,
80            cluster: None,
81            attribute: None,
82        }
83    }
84}
85
86impl From<AttributePath> for ReadPath {
87    fn from(p: AttributePath) -> Self {
88        Self {
89            endpoint: Some(p.endpoint),
90            cluster: Some(p.cluster),
91            attribute: Some(p.attribute),
92        }
93    }
94}
95
96/// Read an `AttributePathIB` list (`Value::List` members) into an
97/// [`AttributePath`]. Out-of-range values surface as
98/// [`ImError::UnexpectedValue`] (not as a missing field).
99pub(crate) fn attribute_path_from_value(
100    members: &[(Tag, Value)],
101) -> Result<AttributePath, ImError> {
102    let mut endpoint = None;
103    let mut cluster = None;
104    let mut attribute = None;
105    for (tag, v) in members {
106        match (tag, v) {
107            (Tag::Context(2), Value::Uint(n)) => {
108                endpoint =
109                    Some(u16::try_from(*n).map_err(|_| {
110                        ImError::UnexpectedValue("AttributePath.endpoint exceeds u16")
111                    })?);
112            }
113            (Tag::Context(3), Value::Uint(n)) => {
114                cluster =
115                    Some(u32::try_from(*n).map_err(|_| {
116                        ImError::UnexpectedValue("AttributePath.cluster exceeds u32")
117                    })?);
118            }
119            (Tag::Context(4), Value::Uint(n)) => {
120                attribute = Some(u32::try_from(*n).map_err(|_| {
121                    ImError::UnexpectedValue("AttributePath.attribute exceeds u32")
122                })?);
123            }
124            _ => {}
125        }
126    }
127    Ok(AttributePath {
128        endpoint: endpoint.ok_or(ImError::MissingField("AttributePath.endpoint"))?,
129        cluster: cluster.ok_or(ImError::MissingField("AttributePath.cluster"))?,
130        attribute: attribute.ok_or(ImError::MissingField("AttributePath.attribute"))?,
131    })
132}
133
134/// Like [`attribute_path_from_value`], but also reports whether the path
135/// carried a `ListIndex` (context tag 5) equal to `null`, which in a
136/// `ReportData` signals a list **append** (Matter ยง10.6.4). Returns
137/// `(path, list_index_is_null_append)`.
138pub(crate) fn attribute_path_and_append_from_value(
139    members: &[(Tag, Value)],
140) -> Result<(AttributePath, bool), ImError> {
141    let path = attribute_path_from_value(members)?;
142    let append = members
143        .iter()
144        .any(|(tag, v)| matches!(tag, Tag::Context(5)) && matches!(v, Value::Null));
145    Ok((path, append))
146}