Skip to main content

rs_matter/dm/clusters/
groups.rs

1/*
2 *
3 *    Copyright (c) 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
18//! This module contains the implementation of the Groups cluster and its handler.
19
20use core::num::NonZeroU8;
21
22use crate::dm::{Cluster, Dataver, InvokeContext, ReadContext};
23use crate::error::{Error, ErrorCode};
24use crate::fabric::FabricPersist;
25use crate::im::encoding::IMStatusCode;
26use crate::tlv::{Nullable, TLVBuilderParent};
27use crate::{with, MatterState};
28
29pub use crate::dm::clusters::decl::groups::*;
30
31/// The handler for the Groups Matter cluster.
32///
33/// This handler manages per-endpoint group membership in the node-wide Group Table.
34#[derive(Debug, Clone)]
35#[cfg_attr(feature = "defmt", derive(defmt::Format))]
36pub struct GroupsHandler {
37    dataver: Dataver,
38}
39
40impl GroupsHandler {
41    /// Creates a new instance of the `GroupsHandler`.
42    ///
43    /// # Arguments
44    /// * `dataver` - The data version tracker
45    pub const fn new(dataver: Dataver) -> Self {
46        Self { dataver }
47    }
48
49    /// Adapt the handler instance to the generic `rs-matter` `Handler` trait
50    pub const fn adapt(self) -> HandlerAdaptor<Self> {
51        HandlerAdaptor(self)
52    }
53
54    /// Check if the fabric has security material (a group key map entry) for the given group ID.
55    fn has_group_material(
56        state: &mut MatterState,
57        fab_idx: NonZeroU8,
58        group_id: u16,
59    ) -> Result<bool, Error> {
60        let fabric = state.fabrics.fabric(fab_idx)?;
61
62        let result = fabric
63            .groups()
64            .key_map_iter()
65            .any(|entry| entry.group_id == group_id);
66
67        Ok(result)
68    }
69}
70
71impl ClusterHandler for GroupsHandler {
72    const CLUSTER: Cluster<'static> = FULL_CLUSTER
73        .with_features(Feature::GROUP_NAMES.bits())
74        .with_attrs(with!(required));
75
76    fn dataver(&self) -> u32 {
77        self.dataver.get()
78    }
79
80    fn dataver_changed(&self) {
81        self.dataver.changed();
82    }
83
84    fn name_support(&self, _ctx: impl ReadContext) -> Result<NameSupportBitmap, Error> {
85        // Bit 7 (GroupNames) = 1 when GN feature is supported
86        Ok(NameSupportBitmap::GROUP_NAMES)
87    }
88
89    fn handle_add_group<P: TLVBuilderParent>(
90        &self,
91        ctx: impl InvokeContext,
92        request: AddGroupRequest<'_>,
93        response: AddGroupResponseBuilder<P>,
94    ) -> Result<P, Error> {
95        let fab_idx = ctx.exchange().accessor()?.fab_idx()?;
96        let group_id = request.group_id()?;
97        let group_name: &str = request.group_name()?;
98
99        // Validate constraints
100        if (group_id == 0) || (group_name.len() > 16) {
101            return response
102                .status(IMStatusCode::ConstraintError as u8)?
103                .group_id(group_id)?
104                .end();
105        }
106
107        let mut persist = FabricPersist::new(ctx.kv());
108
109        let status = ctx.exchange().with_state(|state| {
110            // Check if group security material is available
111            if !Self::has_group_material(state, fab_idx, group_id)? {
112                return Ok(IMStatusCode::UnsupportedAccess);
113            }
114
115            // Add or update group membership
116            let endpoint_id = ctx.cmd().endpoint_id;
117            let fabric = state.fabrics.fabric_mut(fab_idx)?;
118
119            match fabric.groups_mut().add(endpoint_id, group_id, group_name) {
120                Ok(_) => {
121                    // NOTE: Not sure this is a spec-compliant behavor:
122                    // If the failsafe is armed for our fabric, we'll NOT persist the group changes until commissioning is complete.
123                    // And we'll LOSE those changes if the failsafe times out before commissioning completes.
124                    if !state.failsafe.is_armed_for(fab_idx.get()) {
125                        persist.store(fabric)?;
126                    }
127
128                    ctx.exchange().matter().transport().notify_groups_changed();
129
130                    Ok(IMStatusCode::Success)
131                }
132                Err(e) if e.code() == ErrorCode::ResourceExhausted => {
133                    Ok(IMStatusCode::ResourceExhausted)
134                }
135                Err(e) => Err(e)?,
136            }
137        })?;
138
139        persist.run()?;
140
141        response.status(status as u8)?.group_id(group_id)?.end()
142    }
143
144    fn handle_view_group<P: TLVBuilderParent>(
145        &self,
146        ctx: impl InvokeContext,
147        request: ViewGroupRequest<'_>,
148        response: ViewGroupResponseBuilder<P>,
149    ) -> Result<P, Error> {
150        let fab_idx = ctx.exchange().accessor()?.fab_idx()?;
151        let group_id = request.group_id()?;
152
153        // Validate constraints
154        if group_id == 0 {
155            return response
156                .status(IMStatusCode::ConstraintError as u8)?
157                .group_id(group_id)?
158                .group_name("")?
159                .end();
160        }
161
162        ctx.exchange().with_state(|state| {
163            // Check membership for group_id
164            let fabric = state.fabrics.fabric(fab_idx)?;
165
166            let endpoint_id = ctx.cmd().endpoint_id;
167            if let Some(entry) = fabric.groups().get(group_id) {
168                if entry.endpoints.contains(&endpoint_id) {
169                    return response
170                        .status(IMStatusCode::Success as u8)?
171                        .group_id(group_id)?
172                        .group_name(entry.group_name.as_str())?
173                        .end();
174                }
175            }
176
177            response
178                .status(IMStatusCode::NotFound as u8)?
179                .group_id(group_id)?
180                .group_name("")?
181                .end()
182        })
183    }
184
185    fn handle_get_group_membership<P: TLVBuilderParent>(
186        &self,
187        ctx: impl InvokeContext,
188        request: GetGroupMembershipRequest<'_>,
189        response: GetGroupMembershipResponseBuilder<P>,
190    ) -> Result<P, Error> {
191        let fab_idx = ctx.exchange().accessor()?.fab_idx()?;
192        let request_group_list = request.group_list()?;
193
194        ctx.exchange().with_state(|state| {
195            let fabric = state.fabrics.fabric(fab_idx)?;
196
197            // Capacity is nullable - return null to indicate unknown capacity
198            let capacity = Nullable::<u8>::none();
199
200            let endpoint_id = ctx.cmd().endpoint_id;
201            let mut group_list = response.capacity(capacity)?.group_list()?;
202
203            if request_group_list.iter().count() == 0 {
204                // Return all groups this endpoint is a member of
205                for entry in fabric.groups().iter() {
206                    if entry.endpoints.contains(&endpoint_id) {
207                        group_list = group_list.push(&entry.group_id)?;
208                    }
209                }
210            } else {
211                // Return intersection: only requested groups that this endpoint is a member of
212                for gid in request_group_list.into_iter().flatten() {
213                    if let Some(entry) = fabric.groups().get(gid) {
214                        if entry.endpoints.contains(&endpoint_id) {
215                            group_list = group_list.push(&gid)?;
216                        }
217                    }
218                }
219            }
220
221            group_list.end()?.end()
222        })
223    }
224
225    fn handle_remove_group<P: TLVBuilderParent>(
226        &self,
227        ctx: impl InvokeContext,
228        request: RemoveGroupRequest<'_>,
229        response: RemoveGroupResponseBuilder<P>,
230    ) -> Result<P, Error> {
231        let fab_idx = ctx.exchange().accessor()?.fab_idx()?;
232        let group_id = request.group_id()?;
233        let endpoint_id = ctx.cmd().endpoint_id;
234
235        let mut persist = FabricPersist::new(ctx.kv());
236
237        let status = ctx.exchange().with_state(|state| {
238            // Step 1: Validate constraints
239            if group_id == 0 {
240                return Ok(IMStatusCode::ConstraintError);
241            }
242
243            let fabric = state.fabrics.fabric_mut(fab_idx)?;
244
245            // Steps 2-3: Remove membership
246            if fabric.groups_mut().remove(endpoint_id, Some(group_id)) {
247                // NOTE: Not sure this is a spec-compliant behavor:
248                // If the failsafe is armed for our fabric, we'll NOT persist the group changes until commissioning is complete.
249                // And we'll LOSE those changes if the failsafe times out before commissioning completes.
250                if !state.failsafe.is_armed_for(fab_idx.get()) {
251                    persist.store(fabric)?;
252                }
253
254                ctx.exchange().matter().transport().notify_groups_changed();
255
256                Ok(IMStatusCode::Success)
257            } else {
258                Ok(IMStatusCode::NotFound)
259            }
260        })?;
261
262        persist.run()?;
263
264        response.status(status as u8)?.group_id(group_id)?.end()
265    }
266
267    fn handle_remove_all_groups(&self, ctx: impl InvokeContext) -> Result<(), Error> {
268        let fab_idx = ctx.exchange().accessor()?.fab_idx()?;
269        let endpoint_id = ctx.cmd().endpoint_id;
270
271        let mut persist = FabricPersist::new(ctx.kv());
272
273        ctx.exchange().with_state(|state| {
274            let fabric = state.fabrics.fabric_mut(fab_idx)?;
275
276            fabric.groups_mut().remove(endpoint_id, None);
277
278            // NOTE: Not sure this is a spec-compliant behavor:
279            // If the failsafe is armed for our fabric, we'll NOT persist the group changes until commissioning is complete.
280            // And we'll LOSE those changes if the failsafe times out before commissioning completes.
281            if !state.failsafe.is_armed_for(fab_idx.get()) {
282                persist.store(fabric)?;
283            }
284
285            ctx.exchange().matter().transport().notify_groups_changed();
286
287            Ok(())
288        })?;
289
290        persist.run()?;
291
292        Ok(())
293    }
294
295    fn handle_add_group_if_identifying(
296        &self,
297        _ctx: impl InvokeContext,
298        _request: AddGroupIfIdentifyingRequest<'_>,
299    ) -> Result<(), Error> {
300        // TODO: implement with Identity Cluster
301        todo!()
302    }
303}