Skip to main content

rs_matter/dm/clusters/app/
zone_mgmt.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//! Implementation of the Matter Zone Management cluster (0x0550).
19//!
20//! Manages 2-D Cartesian detection regions ("zones") on a camera and
21//! the duration triggers that turn those zones into `ZoneTriggered` /
22//! `ZoneStopped` events. Zones are referenced by ID from
23//! `ZoneTriggered` / `ZoneStopped` events and from the
24//! `PushAvStreamTransport` cluster, so once a zone is created its ID
25//! must remain stable until explicitly removed.
26//!
27//! # Architecture (Pattern B1 — "Hooks")
28//!
29//! [`ZoneMgmtHandler`] owns all spec-defined state (zone table, trigger
30//! table, global sensitivity) and performs all spec validation. The
31//! application provides a [`ZoneMgmtHooks`] implementation that hears
32//! about lifecycle changes and feeds them into the camera's actual
33//! motion-detection pipeline.
34//!
35//! # Const generics
36//!
37//! * `NZ` — maximum number of zones (manufacturer + user-defined).
38//! * `NV` — maximum number of vertices per zone polygon.
39//! * `NT` — maximum number of triggers (typically equals `NZ`).
40//!
41//! # Feature support in this revision
42//!
43//! * `TWO_DIMENSIONAL_CARTESIAN_ZONE` — fully supported.
44//! * `USER_DEFINED` — fully supported (Create / Update / Remove).
45//! * `PER_ZONE_SENSITIVITY` — not yet (per-zone `sensitivity` field on
46//!   triggers is accepted but not enforced).
47//! * `FOCUS_ZONES` — not yet.
48
49use core::cell::{Cell, RefCell};
50use core::future::Future;
51
52use heapless::String as HString;
53
54use crate::dm::{
55    ArrayAttributeRead, Cluster, Dataver, EndptId, InvokeContext, ReadContext, WriteContext,
56};
57use crate::error::{Error, ErrorCode};
58use crate::tlv::{TLVBuilderParent, Utf8Str};
59use crate::utils::storage::Vec;
60use crate::utils::sync::blocking::Mutex;
61use crate::with;
62
63#[allow(unused_imports)]
64pub use crate::dm::clusters::decl::zone_management::*;
65
66use super::super::decl::zone_management as decl;
67
68/// Maximum length, in bytes, of a zone `name` string (Matter spec cap).
69pub const MAX_ZONE_NAME_LEN: usize = 16;
70
71/// Maximum length, in bytes, of a zone `color` string (`#RRGGBB`).
72pub const MAX_ZONE_COLOR_LEN: usize = 7;
73
74/// Errors a [`ZoneMgmtHooks`] implementation can surface.
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
76#[cfg_attr(feature = "defmt", derive(defmt::Format))]
77pub enum ZoneError {
78    ResourceExhausted,
79    DynamicConstraint,
80    NotFound,
81    Failure,
82}
83
84impl From<ZoneError> for Error {
85    fn from(e: ZoneError) -> Self {
86        match e {
87            ZoneError::ResourceExhausted => ErrorCode::ResourceExhausted.into(),
88            ZoneError::DynamicConstraint => ErrorCode::ConstraintError.into(),
89            ZoneError::NotFound => ErrorCode::NotFound.into(),
90            ZoneError::Failure => ErrorCode::Failure.into(),
91        }
92    }
93}
94
95/// One row in the `Zones` attribute. Vertices are stored inline.
96#[derive(Debug, Clone, PartialEq, Eq)]
97pub struct Zone<const NV: usize> {
98    pub zone_id: u16,
99    pub zone_type: ZoneTypeEnum,
100    pub zone_source: ZoneSourceEnum,
101    pub name: HString<MAX_ZONE_NAME_LEN>,
102    pub zone_use: ZoneUseEnum,
103    pub vertices: Vec<(u16, u16), NV>,
104    pub color: Option<HString<MAX_ZONE_COLOR_LEN>>,
105}
106
107/// One row in the `Triggers` attribute.
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109#[cfg_attr(feature = "defmt", derive(defmt::Format))]
110pub struct Trigger {
111    pub zone_id: u16,
112    pub initial_duration: u32,
113    pub augmentation_duration: u32,
114    pub max_duration: u32,
115    pub blind_duration: u32,
116    pub sensitivity: Option<u8>,
117}
118
119/// Application hooks for the side-effecting pieces of zone-management
120/// lifecycle. Each method has a no-op default body — implementors only
121/// need to override the events they actually care about.
122#[allow(unused_variables)]
123pub trait ZoneMgmtHooks<const NV: usize> {
124    fn zone_created(&self, zone: &Zone<NV>) -> impl Future<Output = Result<(), ZoneError>> {
125        async { Ok(()) }
126    }
127
128    fn zone_updated(&self, zone: &Zone<NV>) -> impl Future<Output = Result<(), ZoneError>> {
129        async { Ok(()) }
130    }
131
132    fn zone_removed(&self, zone_id: u16) -> impl Future<Output = Result<(), ZoneError>> {
133        async { Ok(()) }
134    }
135
136    fn trigger_set(&self, trigger: &Trigger) -> impl Future<Output = Result<(), ZoneError>> {
137        async { Ok(()) }
138    }
139
140    fn trigger_removed(&self, zone_id: u16) -> impl Future<Output = Result<(), ZoneError>> {
141        async { Ok(()) }
142    }
143
144    fn set_sensitivity(&self, value: u8) -> impl Future<Output = Result<(), ZoneError>> {
145        async { Ok(()) }
146    }
147}
148
149impl<const NV: usize, T> ZoneMgmtHooks<NV> for &T
150where
151    T: ZoneMgmtHooks<NV>,
152{
153    fn zone_created(&self, zone: &Zone<NV>) -> impl Future<Output = Result<(), ZoneError>> {
154        (*self).zone_created(zone)
155    }
156
157    fn zone_updated(&self, zone: &Zone<NV>) -> impl Future<Output = Result<(), ZoneError>> {
158        (*self).zone_updated(zone)
159    }
160
161    fn zone_removed(&self, zone_id: u16) -> impl Future<Output = Result<(), ZoneError>> {
162        (*self).zone_removed(zone_id)
163    }
164
165    fn trigger_set(&self, trigger: &Trigger) -> impl Future<Output = Result<(), ZoneError>> {
166        (*self).trigger_set(trigger)
167    }
168
169    fn trigger_removed(&self, zone_id: u16) -> impl Future<Output = Result<(), ZoneError>> {
170        (*self).trigger_removed(zone_id)
171    }
172
173    fn set_sensitivity(&self, value: u8) -> impl Future<Output = Result<(), ZoneError>> {
174        (*self).set_sensitivity(value)
175    }
176}
177
178/// Static configuration for a [`ZoneMgmtHandler`].
179#[derive(Debug, Clone, Copy)]
180pub struct ZoneMgmtConfig {
181    pub max_zones: u8,
182    pub max_user_defined_zones: u8,
183    /// 0 disables the global `Sensitivity` attribute.
184    pub sensitivity_max: u8,
185    pub default_sensitivity: u8,
186    /// Maximum vertex coordinates accepted by `CreateTwoDCartesianZone`
187    /// / `UpdateTwoDCartesianZone` (advertised via `TwoDCartesianMax`).
188    pub two_d_cartesian_max: (u16, u16),
189}
190
191struct State<const NZ: usize, const NV: usize, const NT: usize> {
192    zones: Vec<Zone<NV>, NZ>,
193    triggers: Vec<Trigger, NT>,
194    sensitivity: u8,
195    seeded: bool,
196}
197
198impl<const NZ: usize, const NV: usize, const NT: usize> State<NZ, NV, NT> {
199    const fn new() -> Self {
200        Self {
201            zones: Vec::new(),
202            triggers: Vec::new(),
203            sensitivity: 0,
204            seeded: false,
205        }
206    }
207}
208
209/// Handler for the Zone Management cluster (0x0550).
210pub struct ZoneMgmtHandler<H, const NZ: usize, const NV: usize, const NT: usize>
211where
212    H: ZoneMgmtHooks<NV>,
213{
214    dataver: Dataver,
215    endpoint_id: EndptId,
216    config: ZoneMgmtConfig,
217    features: u32,
218    hooks: H,
219    state: Mutex<RefCell<State<NZ, NV, NT>>>,
220    next_id: Mutex<Cell<u16>>,
221}
222
223impl<H, const NZ: usize, const NV: usize, const NT: usize> ZoneMgmtHandler<H, NZ, NV, NT>
224where
225    H: ZoneMgmtHooks<NV>,
226{
227    /// Cluster metadata advertising `TWO_DIMENSIONAL_CARTESIAN_ZONE` +
228    /// `USER_DEFINED`.
229    pub const CLUSTER: Cluster<'static> = decl::FULL_CLUSTER
230        .with_revision(1)
231        .with_features(
232            decl::Feature::TWO_DIMENSIONAL_CARTESIAN_ZONE.bits()
233                | decl::Feature::USER_DEFINED.bits(),
234        )
235        .with_attrs(with!(
236            required;
237            AttributeId::MaxUserDefinedZones
238                | AttributeId::MaxZones
239                | AttributeId::Zones
240                | AttributeId::Triggers
241                | AttributeId::SensitivityMax
242                | AttributeId::Sensitivity
243                | AttributeId::TwoDCartesianMax
244        ))
245        .with_cmds(with!(
246            decl::CommandId::CreateTwoDCartesianZone
247                | decl::CommandId::UpdateTwoDCartesianZone
248                | decl::CommandId::RemoveZone
249                | decl::CommandId::CreateOrUpdateTrigger
250                | decl::CommandId::RemoveTrigger
251        ));
252
253    pub const fn new(
254        dataver: Dataver,
255        endpoint_id: EndptId,
256        config: ZoneMgmtConfig,
257        features: u32,
258        hooks: H,
259    ) -> Self {
260        Self {
261            dataver,
262            endpoint_id,
263            config,
264            features,
265            hooks,
266            state: Mutex::new(RefCell::new(State::new())),
267            next_id: Mutex::new(Cell::new(1)),
268        }
269    }
270
271    pub const fn adapt(self) -> decl::HandlerAsyncAdaptor<Self> {
272        decl::HandlerAsyncAdaptor(self)
273    }
274
275    pub const fn endpoint_id(&self) -> EndptId {
276        self.endpoint_id
277    }
278
279    /// Pre-seed a manufacturer zone at boot. The supplied zone gets a
280    /// fresh ID and `zone_source` is forced to `Mfg`. Such zones cannot
281    /// be removed via `RemoveZone`. Returns the assigned ID.
282    pub fn add_mfg_zone(&self, mut zone: Zone<NV>) -> Result<u16, Error> {
283        zone.zone_source = ZoneSourceEnum::Mfg;
284        zone.zone_id = self.alloc_zone_id();
285        let id = zone.zone_id;
286        let pushed = self.state.lock(|cell| {
287            let mut s = cell.borrow_mut();
288            s.zones.push(zone).is_ok()
289        });
290        if !pushed {
291            return Err(ErrorCode::ResourceExhausted.into());
292        }
293        self.dataver.changed();
294        Ok(id)
295    }
296
297    fn alloc_zone_id(&self) -> u16 {
298        self.next_id.lock(|cell| {
299            let mut id = cell.get();
300            if id == 0 {
301                id = 1;
302            }
303            cell.set(id.wrapping_add(1).max(1));
304            id
305        })
306    }
307
308    fn ensure_seeded(&self) {
309        self.state.lock(|cell| {
310            let mut s = cell.borrow_mut();
311            if !s.seeded {
312                s.sensitivity = self.config.default_sensitivity;
313                s.seeded = true;
314            }
315        });
316    }
317
318    fn has_feature(&self, bit: u32) -> bool {
319        self.features & bit != 0
320    }
321
322    fn user_defined_zone_count(&self) -> usize {
323        self.state.lock(|cell| {
324            cell.borrow()
325                .zones
326                .iter()
327                .filter(|z| z.zone_source == ZoneSourceEnum::User)
328                .count()
329        })
330    }
331
332    /// Validate-and-extract a `TwoDCartesianZoneStruct` payload.
333    #[allow(clippy::type_complexity)]
334    fn parse_zone_payload(
335        &self,
336        s: &TwoDCartesianZoneStruct<'_>,
337    ) -> Result<
338        (
339            HString<MAX_ZONE_NAME_LEN>,
340            ZoneUseEnum,
341            Vec<(u16, u16), NV>,
342            Option<HString<MAX_ZONE_COLOR_LEN>>,
343        ),
344        Error,
345    > {
346        let name_str: Utf8Str<'_> = s.name()?;
347        let mut name: HString<MAX_ZONE_NAME_LEN> = HString::new();
348        if name_str.len() > MAX_ZONE_NAME_LEN || name.push_str(name_str).is_err() {
349            return Err(ErrorCode::ConstraintError.into());
350        }
351
352        let zone_use = s.r#use()?;
353
354        let verts_arr = s.vertices()?;
355        let mut vertices: Vec<(u16, u16), NV> = Vec::new();
356        for v in verts_arr.iter() {
357            let v = v?;
358            let x = v.x()?;
359            let y = v.y()?;
360            if x > self.config.two_d_cartesian_max.0 || y > self.config.two_d_cartesian_max.1 {
361                return Err(ErrorCode::ConstraintError.into());
362            }
363            vertices
364                .push((x, y))
365                .map_err(|_| Error::from(ErrorCode::ResourceExhausted))?;
366        }
367        // Spec: a polygon needs at least 3 vertices.
368        if vertices.len() < 3 {
369            return Err(ErrorCode::ConstraintError.into());
370        }
371
372        let color = match s.color()? {
373            Some(c) => {
374                let mut h: HString<MAX_ZONE_COLOR_LEN> = HString::new();
375                if c.len() > MAX_ZONE_COLOR_LEN || h.push_str(c).is_err() {
376                    return Err(ErrorCode::ConstraintError.into());
377                }
378                Some(h)
379            }
380            None => None,
381        };
382
383        Ok((name, zone_use, vertices, color))
384    }
385}
386
387impl<H, const NZ: usize, const NV: usize, const NT: usize> ClusterAsyncHandler
388    for ZoneMgmtHandler<H, NZ, NV, NT>
389where
390    H: ZoneMgmtHooks<NV>,
391{
392    const CLUSTER: Cluster<'static> = Self::CLUSTER;
393
394    fn dataver(&self) -> u32 {
395        self.dataver.get()
396    }
397
398    fn dataver_changed(&self) {
399        self.dataver.changed();
400    }
401
402    async fn max_user_defined_zones(&self, _ctx: impl ReadContext) -> Result<u8, Error> {
403        Ok(self.config.max_user_defined_zones)
404    }
405
406    async fn max_zones(&self, _ctx: impl ReadContext) -> Result<u8, Error> {
407        Ok(self.config.max_zones)
408    }
409
410    async fn sensitivity_max(&self, _ctx: impl ReadContext) -> Result<u8, Error> {
411        Ok(self.config.sensitivity_max)
412    }
413
414    async fn sensitivity(&self, _ctx: impl ReadContext) -> Result<u8, Error> {
415        if self.config.sensitivity_max == 0 {
416            return Err(ErrorCode::AttributeNotFound.into());
417        }
418        self.ensure_seeded();
419        Ok(self.state.lock(|cell| cell.borrow().sensitivity))
420    }
421
422    async fn set_sensitivity(&self, _ctx: impl WriteContext, value: u8) -> Result<(), Error> {
423        if self.config.sensitivity_max == 0 {
424            return Err(ErrorCode::AttributeNotFound.into());
425        }
426        if value < 1 || value > self.config.sensitivity_max {
427            return Err(ErrorCode::ConstraintError.into());
428        }
429        self.hooks.set_sensitivity(value).await?;
430        self.state.lock(|cell| {
431            let mut s = cell.borrow_mut();
432            s.sensitivity = value;
433            s.seeded = true;
434        });
435        Ok(())
436    }
437
438    async fn two_d_cartesian_max<P: TLVBuilderParent>(
439        &self,
440        _ctx: impl ReadContext,
441        builder: TwoDCartesianVertexStructBuilder<P>,
442    ) -> Result<P, Error> {
443        builder
444            .x(self.config.two_d_cartesian_max.0)?
445            .y(self.config.two_d_cartesian_max.1)?
446            .end()
447    }
448
449    async fn zones<P: TLVBuilderParent>(
450        &self,
451        _ctx: impl ReadContext,
452        builder: ArrayAttributeRead<
453            ZoneInformationStructArrayBuilder<P>,
454            ZoneInformationStructBuilder<P>,
455        >,
456    ) -> Result<P, Error> {
457        let snapshot = self.state.lock(|cell| cell.borrow().zones.clone());
458        match builder {
459            ArrayAttributeRead::ReadAll(mut b) => {
460                for z in snapshot.iter() {
461                    b = write_zone_info(b.push()?, z)?;
462                }
463                b.end()
464            }
465            ArrayAttributeRead::ReadOne(idx, b) => {
466                let Some(z) = snapshot.get(idx as usize) else {
467                    return Err(ErrorCode::ConstraintError.into());
468                };
469                write_zone_info(b, z)
470            }
471            ArrayAttributeRead::ReadNone(b) => b.end(),
472        }
473    }
474
475    async fn triggers<P: TLVBuilderParent>(
476        &self,
477        _ctx: impl ReadContext,
478        builder: ArrayAttributeRead<
479            ZoneTriggerControlStructArrayBuilder<P>,
480            ZoneTriggerControlStructBuilder<P>,
481        >,
482    ) -> Result<P, Error> {
483        let snapshot = self.state.lock(|cell| cell.borrow().triggers.clone());
484        match builder {
485            ArrayAttributeRead::ReadAll(mut b) => {
486                for t in snapshot.iter() {
487                    b = write_trigger(b.push()?, t)?;
488                }
489                b.end()
490            }
491            ArrayAttributeRead::ReadOne(idx, b) => {
492                let Some(t) = snapshot.get(idx as usize) else {
493                    return Err(ErrorCode::ConstraintError.into());
494                };
495                write_trigger(b, t)
496            }
497            ArrayAttributeRead::ReadNone(b) => b.end(),
498        }
499    }
500
501    async fn handle_create_two_d_cartesian_zone<P: TLVBuilderParent>(
502        &self,
503        ctx: impl InvokeContext,
504        request: CreateTwoDCartesianZoneRequest<'_>,
505        response: CreateTwoDCartesianZoneResponseBuilder<P>,
506    ) -> Result<P, Error> {
507        if !self.has_feature(decl::Feature::USER_DEFINED.bits()) {
508            return Err(ErrorCode::InvalidAction.into());
509        }
510
511        let payload = request.zone()?;
512        let (name, zone_use, vertices, color) = self.parse_zone_payload(&payload)?;
513
514        if self.user_defined_zone_count() >= self.config.max_user_defined_zones as usize {
515            return Err(ErrorCode::ResourceExhausted.into());
516        }
517
518        let zone = Zone {
519            zone_id: self.alloc_zone_id(),
520            zone_type: ZoneTypeEnum::TwoDCARTZone,
521            zone_source: ZoneSourceEnum::User,
522            name,
523            zone_use,
524            vertices,
525            color,
526        };
527        let id = zone.zone_id;
528
529        let pushed = self.state.lock(|cell| {
530            let mut s = cell.borrow_mut();
531            s.zones.push(zone.clone()).is_ok()
532        });
533        if !pushed {
534            return Err(ErrorCode::ResourceExhausted.into());
535        }
536
537        if let Err(e) = self.hooks.zone_created(&zone).await {
538            self.state.lock(|cell| {
539                let mut s = cell.borrow_mut();
540                s.zones.retain(|z| z.zone_id != id);
541            });
542            return Err(e.into());
543        }
544        ctx.notify_own_attr_changed(AttributeId::Zones as _);
545
546        response.zone_id(id)?.end()
547    }
548
549    async fn handle_update_two_d_cartesian_zone(
550        &self,
551        ctx: impl InvokeContext,
552        request: UpdateTwoDCartesianZoneRequest<'_>,
553    ) -> Result<(), Error> {
554        if !self.has_feature(decl::Feature::USER_DEFINED.bits()) {
555            return Err(ErrorCode::InvalidAction.into());
556        }
557
558        let id = request.zone_id()?;
559        let payload = request.zone()?;
560        let (name, zone_use, vertices, color) = self.parse_zone_payload(&payload)?;
561
562        let prior = self.state.lock(|cell| {
563            cell.borrow()
564                .zones
565                .iter()
566                .find(|z| z.zone_id == id)
567                .cloned()
568        });
569        let Some(prior) = prior else {
570            return Err(ErrorCode::NotFound.into());
571        };
572        if prior.zone_source != ZoneSourceEnum::User {
573            return Err(ErrorCode::InvalidAction.into());
574        }
575
576        let updated = Zone {
577            zone_id: id,
578            zone_type: prior.zone_type,
579            zone_source: ZoneSourceEnum::User,
580            name,
581            zone_use,
582            vertices,
583            color,
584        };
585
586        let prev = self.state.lock(|cell| {
587            let mut s = cell.borrow_mut();
588            s.zones.iter().position(|z| z.zone_id == id).map(|i| {
589                let prev = s.zones[i].clone();
590                s.zones[i] = updated.clone();
591                prev
592            })
593        });
594        let Some(prev) = prev else {
595            return Err(ErrorCode::NotFound.into());
596        };
597
598        if let Err(e) = self.hooks.zone_updated(&updated).await {
599            self.state.lock(|cell| {
600                let mut s = cell.borrow_mut();
601                if let Some(i) = s.zones.iter().position(|z| z.zone_id == id) {
602                    s.zones[i] = prev;
603                }
604            });
605            return Err(e.into());
606        }
607        ctx.notify_own_attr_changed(AttributeId::Zones as _);
608        Ok(())
609    }
610
611    async fn handle_remove_zone(
612        &self,
613        ctx: impl InvokeContext,
614        request: RemoveZoneRequest<'_>,
615    ) -> Result<(), Error> {
616        let id = request.zone_id()?;
617
618        let outcome = self.state.lock(|cell| {
619            let s = cell.borrow();
620            match s.zones.iter().find(|z| z.zone_id == id) {
621                None => Err(ErrorCode::NotFound),
622                Some(z) if z.zone_source != ZoneSourceEnum::User => Err(ErrorCode::InvalidAction),
623                _ => Ok(()),
624            }
625        });
626        outcome.map_err(Error::from)?;
627
628        self.hooks.zone_removed(id).await?;
629
630        let triggers_changed = self.state.lock(|cell| {
631            let mut s = cell.borrow_mut();
632            s.zones.retain(|z| z.zone_id != id);
633            let before = s.triggers.len();
634            s.triggers.retain(|t| t.zone_id != id);
635            s.triggers.len() != before
636        });
637        ctx.notify_own_attr_changed(AttributeId::Zones as _);
638        if triggers_changed {
639            ctx.notify_own_attr_changed(AttributeId::Triggers as _);
640        }
641        Ok(())
642    }
643
644    async fn handle_create_or_update_trigger(
645        &self,
646        ctx: impl InvokeContext,
647        request: CreateOrUpdateTriggerRequest<'_>,
648    ) -> Result<(), Error> {
649        let payload = request.trigger()?;
650        let zone_id = payload.zone_id()?;
651        let initial_duration = payload.initial_duration()?;
652        let augmentation_duration = payload.augmentation_duration()?;
653        let max_duration = payload.max_duration()?;
654        let blind_duration = payload.blind_duration()?;
655        let sensitivity = payload.sensitivity()?;
656
657        let zone_exists = self
658            .state
659            .lock(|cell| cell.borrow().zones.iter().any(|z| z.zone_id == zone_id));
660        if !zone_exists {
661            return Err(ErrorCode::NotFound.into());
662        }
663
664        if max_duration < initial_duration
665            || (augmentation_duration > 0 && initial_duration >= max_duration)
666        {
667            return Err(ErrorCode::ConstraintError.into());
668        }
669        if let Some(s) = sensitivity {
670            if s < 1 || s > self.config.sensitivity_max {
671                return Err(ErrorCode::ConstraintError.into());
672            }
673        }
674
675        let trigger = Trigger {
676            zone_id,
677            initial_duration,
678            augmentation_duration,
679            max_duration,
680            blind_duration,
681            sensitivity,
682        };
683
684        let pushed = self.state.lock(|cell| {
685            let mut s = cell.borrow_mut();
686            if let Some(t) = s.triggers.iter_mut().find(|t| t.zone_id == zone_id) {
687                *t = trigger;
688                true
689            } else {
690                s.triggers.push(trigger).is_ok()
691            }
692        });
693        if !pushed {
694            return Err(ErrorCode::ResourceExhausted.into());
695        }
696
697        if let Err(e) = self.hooks.trigger_set(&trigger).await {
698            self.state.lock(|cell| {
699                let mut s = cell.borrow_mut();
700                s.triggers.retain(|t| t.zone_id != zone_id);
701            });
702            return Err(e.into());
703        }
704        ctx.notify_own_attr_changed(AttributeId::Triggers as _);
705        Ok(())
706    }
707
708    async fn handle_remove_trigger(
709        &self,
710        ctx: impl InvokeContext,
711        request: RemoveTriggerRequest<'_>,
712    ) -> Result<(), Error> {
713        let zone_id = request.zone_id()?;
714        let existed = self
715            .state
716            .lock(|cell| cell.borrow().triggers.iter().any(|t| t.zone_id == zone_id));
717        if !existed {
718            return Err(ErrorCode::NotFound.into());
719        }
720        self.hooks.trigger_removed(zone_id).await?;
721        self.state.lock(|cell| {
722            let mut s = cell.borrow_mut();
723            s.triggers.retain(|t| t.zone_id != zone_id);
724        });
725        ctx.notify_own_attr_changed(AttributeId::Triggers as _);
726        Ok(())
727    }
728}
729
730// -----------------------------------------------------------------------
731// Local helpers
732// -----------------------------------------------------------------------
733
734fn write_zone_info<P: TLVBuilderParent, const NV: usize>(
735    builder: ZoneInformationStructBuilder<P>,
736    z: &Zone<NV>,
737) -> Result<P, Error> {
738    let b = builder
739        .zone_id(z.zone_id)?
740        .zone_type(z.zone_type)?
741        .zone_source(z.zone_source)?;
742    // The 2-D Cartesian zone payload is an OptionalBuilder; for
743    // ZoneTypeEnum::TwoDCARTZone we always emit it.
744    b.two_d_cartesian_zone()?
745        .with_some_if(z.zone_type == ZoneTypeEnum::TwoDCARTZone, |zone_b| {
746            let zone_b = zone_b.name(z.name.as_str())?.r#use(z.zone_use)?;
747            let mut va = zone_b.vertices()?;
748            for (x, y) in z.vertices.iter().copied() {
749                va = va.push()?.x(x)?.y(y)?.end()?;
750            }
751            let zone_b = va.end()?;
752            zone_b.color(z.color.as_ref().map(|c| c.as_str()))?.end()
753        })?
754        .end()
755}
756
757fn write_trigger<P: TLVBuilderParent>(
758    builder: ZoneTriggerControlStructBuilder<P>,
759    t: &Trigger,
760) -> Result<P, Error> {
761    builder
762        .zone_id(t.zone_id)?
763        .initial_duration(t.initial_duration)?
764        .augmentation_duration(t.augmentation_duration)?
765        .max_duration(t.max_duration)?
766        .blind_duration(t.blind_duration)?
767        .sensitivity(t.sensitivity)?
768        .end()
769}