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}