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///
45/// `#[non_exhaustive]`: a read/subscribe path may gain optional spec components
46/// (e.g. a data-version filter); marking it keeps such additions non-breaking.
47/// Build via [`ReadPath::concrete`] / [`ReadPath::cluster`] / [`ReadPath::all`]
48/// / [`ReadPath::new`].
49#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
50#[non_exhaustive]
51pub struct ReadPath {
52    /// Endpoint, or `None` for all endpoints.
53    pub endpoint: Option<u16>,
54    /// Cluster, or `None` for all clusters.
55    pub cluster: Option<u32>,
56    /// Attribute, or `None` for all attributes.
57    pub attribute: Option<u32>,
58}
59
60impl ReadPath {
61    /// A read path from raw optional components (a `None` component is a
62    /// wildcard). Prefer [`Self::concrete`] / [`Self::cluster`] / [`Self::all`]
63    /// for the common shapes.
64    #[must_use]
65    pub fn new(endpoint: Option<u16>, cluster: Option<u32>, attribute: Option<u32>) -> Self {
66        Self {
67            endpoint,
68            cluster,
69            attribute,
70        }
71    }
72
73    /// A concrete `(endpoint, cluster, attribute)` path (no wildcards).
74    #[must_use]
75    pub fn concrete(endpoint: u16, cluster: u32, attribute: u32) -> Self {
76        Self {
77            endpoint: Some(endpoint),
78            cluster: Some(cluster),
79            attribute: Some(attribute),
80        }
81    }
82
83    /// All attributes of `cluster` on `endpoint`.
84    #[must_use]
85    pub fn cluster(endpoint: u16, cluster: u32) -> Self {
86        Self {
87            endpoint: Some(endpoint),
88            cluster: Some(cluster),
89            attribute: None,
90        }
91    }
92
93    /// Every attribute on every endpoint/cluster (full wildcard).
94    #[must_use]
95    pub fn all() -> Self {
96        Self {
97            endpoint: None,
98            cluster: None,
99            attribute: None,
100        }
101    }
102}
103
104impl From<AttributePath> for ReadPath {
105    fn from(p: AttributePath) -> Self {
106        Self {
107            endpoint: Some(p.endpoint),
108            cluster: Some(p.cluster),
109            attribute: Some(p.attribute),
110        }
111    }
112}
113
114/// Read an `AttributePathIB` list (`Value::List` members) into an
115/// [`AttributePath`]. Out-of-range values surface as
116/// [`ImError::UnexpectedValue`] (not as a missing field).
117pub(crate) fn attribute_path_from_value(
118    members: &[(Tag, Value)],
119) -> Result<AttributePath, ImError> {
120    let mut endpoint = None;
121    let mut cluster = None;
122    let mut attribute = None;
123    for (tag, v) in members {
124        match (tag, v) {
125            (Tag::Context(2), Value::Uint(n)) => {
126                endpoint =
127                    Some(u16::try_from(*n).map_err(|_| {
128                        ImError::UnexpectedValue("AttributePath.endpoint exceeds u16")
129                    })?);
130            }
131            (Tag::Context(3), Value::Uint(n)) => {
132                cluster =
133                    Some(u32::try_from(*n).map_err(|_| {
134                        ImError::UnexpectedValue("AttributePath.cluster exceeds u32")
135                    })?);
136            }
137            (Tag::Context(4), Value::Uint(n)) => {
138                attribute = Some(u32::try_from(*n).map_err(|_| {
139                    ImError::UnexpectedValue("AttributePath.attribute exceeds u32")
140                })?);
141            }
142            _ => {}
143        }
144    }
145    Ok(AttributePath {
146        endpoint: endpoint.ok_or(ImError::MissingField("AttributePath.endpoint"))?,
147        cluster: cluster.ok_or(ImError::MissingField("AttributePath.cluster"))?,
148        attribute: attribute.ok_or(ImError::MissingField("AttributePath.attribute"))?,
149    })
150}
151
152/// Like [`attribute_path_from_value`], but also reports whether the path
153/// carried a `ListIndex` (context tag 5) equal to `null`, which in a
154/// `ReportData` signals a list **append** (Matter ยง10.6.4). Returns
155/// `(path, list_index_is_null_append)`.
156pub(crate) fn attribute_path_and_append_from_value(
157    members: &[(Tag, Value)],
158) -> Result<(AttributePath, bool), ImError> {
159    let path = attribute_path_from_value(members)?;
160    let append = members
161        .iter()
162        .any(|(tag, v)| matches!(tag, Tag::Context(5)) && matches!(v, Value::Null));
163    Ok((path, append))
164}