Skip to main content

matter_controller/
binding.rs

1//! `Binding` (0x001E) targets and their `Value` encode/decode. The controller
2//! hand-builds the `TargetStruct` `Value` (decoder-agnostic, matching the ACL /
3//! groups pattern); the generated codec is the read oracle.
4
5use matter_codec::{Tag, Value};
6use matter_interaction::AttributePath;
7
8/// Binding cluster id.
9pub(crate) const BINDING_CLUSTER: u32 = 0x001E;
10/// `Binding` attribute id (the writable list-of-`TargetStruct`).
11pub(crate) const ATTR_BINDING: u32 = 0x0000;
12
13/// One `Binding.TargetStruct` — a unicast (`node` + `endpoint` [+ `cluster`]) or
14/// group (`group` [+ `cluster`]) binding. The device stamps the fabric index;
15/// callers never set it.
16#[derive(Clone, Debug, PartialEq, Eq)]
17#[non_exhaustive]
18pub struct BindingTarget {
19    /// Target node id (unicast binding).
20    pub node: Option<u64>,
21    /// Target group id (group binding).
22    pub group: Option<u16>,
23    /// Target endpoint (unicast binding).
24    pub endpoint: Option<u16>,
25    /// Optional target cluster; `None` binds all clusters.
26    pub cluster: Option<u32>,
27}
28
29impl BindingTarget {
30    /// Construct a [`BindingTarget`]. Provided because the struct is
31    /// `#[non_exhaustive]` (external callers cannot use a struct literal).
32    #[must_use]
33    pub fn new(
34        node: Option<u64>,
35        group: Option<u16>,
36        endpoint: Option<u16>,
37        cluster: Option<u32>,
38    ) -> Self {
39        Self {
40            node,
41            group,
42            endpoint,
43            cluster,
44        }
45    }
46}
47
48/// Build the `TargetStruct` `Value` for one target (ctx1 Node / ctx2 Group /
49/// ctx3 Endpoint / ctx4 Cluster; only the present fields are emitted).
50pub(crate) fn binding_target_value(t: &BindingTarget) -> Value {
51    let mut m = Vec::new();
52    if let Some(n) = t.node {
53        m.push((Tag::Context(1), Value::Uint(n)));
54    }
55    if let Some(g) = t.group {
56        m.push((Tag::Context(2), Value::Uint(u64::from(g))));
57    }
58    if let Some(e) = t.endpoint {
59        m.push((Tag::Context(3), Value::Uint(u64::from(e))));
60    }
61    if let Some(c) = t.cluster {
62        m.push((Tag::Context(4), Value::Uint(u64::from(c))));
63    }
64    Value::Structure(m)
65}
66
67/// Parse the `Binding` attribute (a `Value::Array` of `TargetStruct`) out of read
68/// reports into [`BindingTarget`]s. Unknown members are ignored.
69pub(crate) fn parse_bindings(reports: &[(AttributePath, Value)]) -> Vec<BindingTarget> {
70    let mut out = Vec::new();
71    for (p, v) in reports {
72        if p.cluster != BINDING_CLUSTER || p.attribute != ATTR_BINDING {
73            continue;
74        }
75        if let Value::Array(entries) = v {
76            for entry in entries {
77                if let Value::Structure(members) = entry {
78                    let mut t = BindingTarget::new(None, None, None, None);
79                    for (tag, val) in members {
80                        match (*tag, val) {
81                            (Tag::Context(1), Value::Uint(n)) => t.node = Some(*n),
82                            (Tag::Context(2), Value::Uint(g)) => t.group = u16::try_from(*g).ok(),
83                            (Tag::Context(3), Value::Uint(e)) => {
84                                t.endpoint = u16::try_from(*e).ok();
85                            }
86                            (Tag::Context(4), Value::Uint(c)) => {
87                                t.cluster = u32::try_from(*c).ok();
88                            }
89                            _ => {}
90                        }
91                    }
92                    out.push(t);
93                }
94            }
95        }
96    }
97    out
98}
99
100#[cfg(test)]
101mod tests {
102    #![allow(clippy::unwrap_used, clippy::expect_used)] // Test code: CLAUDE.md carve-out.
103    use super::*;
104
105    #[test]
106    fn target_value_and_parse_roundtrip() {
107        let targets = vec![
108            BindingTarget::new(Some(0x1122), None, Some(1), Some(0x0006)), // unicast
109            BindingTarget::new(None, Some(0x0007), None, Some(0x0006)),    // group
110        ];
111        // Build the list Value, wrap it as a read report, parse it back.
112        let list = Value::Array(targets.iter().map(binding_target_value).collect());
113        let reports = vec![(
114            AttributePath {
115                endpoint: 1,
116                cluster: BINDING_CLUSTER,
117                attribute: ATTR_BINDING,
118            },
119            list,
120        )];
121        assert_eq!(parse_bindings(&reports), targets);
122    }
123}