Skip to main content

rs_matter/dm/clusters/app/
on_off.rs

1/*
2 *
3 *    Copyright (c) 2022-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 On/Off cluster.
19//!
20//! This module provides the core logic and state management for the OnOff cluster as defined by the Matter specification v1.3.
21//!
22//! Key features:
23//! - Provides hooks for device-specific logic via the `OnOffHooks` trait.
24//! - Validates cluster configuration and feature dependencies.
25//! - Manages OnWithTimedOff guards and OffWithEffect transitions.
26//! - Provides coupling with a LevelControl cluster on the same endpoint.
27//!
28//! Unsupported features:
29//! - The attribute and logic related to the Scenes cluster are not fully implemented since the Scenes cluster is not yet implemented.
30
31use core::cell::Cell;
32use core::future::{ready, Future};
33use core::pin::pin;
34
35use embassy_futures::select::{select, select3, Either, Either3};
36
37use crate::dm::clusters::app::level_control::{LevelControlHandler, LevelControlHooks};
38use crate::dm::clusters::decl::scenes_management::{
39    AttributeValuePairStruct, AttributeValuePairStructArrayBuilder,
40};
41use crate::dm::clusters::decl::{level_control, on_off};
42use crate::dm::clusters::scenes::{SceneClusterHandler, SceneInvalidator};
43use crate::dm::types::EndptId;
44use crate::dm::{
45    AttrId, Cluster, ClusterId, Dataver, HandlerContext, InvokeContext, ReadContext, WriteContext,
46};
47use crate::error::{Error, ErrorCode};
48use crate::tlv::{TLVArray, TLVBuilderParent};
49
50pub use crate::dm::clusters::decl::on_off::*;
51
52use crate::tlv::Nullable;
53use crate::utils::cell::RefCell;
54use crate::utils::sync::blocking::Mutex;
55use crate::utils::sync::Signal;
56
57/// Messages passed to the `notify` closure in `OnOffHooks::run()` method.
58#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
59#[cfg_attr(feature = "defmt", derive(defmt::Format))]
60pub enum OutOfBandMessage {
61    /// Indicates to the handler that the state of the device has changed and it should update the Matter state accordingly.
62    Update,
63    /// Indicates to the handler that a request to change the state to On has been made.
64    /// This will change the state of the device if and when appropriate according to the Matter logic.
65    On,
66    /// Indicates to the handler that a request to change the state to Off has been made.
67    /// This will change the state of the device if and when appropriate according to the Matter logic.
68    Off,
69    /// Indicates to the handler that a request to toggle the OnOff state has been made.
70    /// This will change the state of the device if and when appropriate according to the Matter logic.
71    Toggle,
72}
73
74/// A rust friendly combined enum that groups the effect and its variant.
75#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
76#[cfg_attr(feature = "defmt", derive(defmt::Format))]
77pub enum EffectVariantEnum {
78    DelayedAllOff(DelayedAllOffEffectVariantEnum),
79    DyingLight(DyingLightEffectVariantEnum),
80}
81
82// The state of the internal OnOff state machine.
83#[derive(Clone, Copy, PartialEq)]
84enum OnOffClusterState {
85    On,
86    Off,
87    TimedOn,
88    DelayedOff,
89}
90
91// Internal enum for managing sending commands to the state machine.
92enum OnOffCommand {
93    Off,
94    On,
95    Toggle,
96    OffWithEffect(EffectVariantEnum),
97    OnWithTimedOff,
98    CoupledClusterOn,
99    CoupledClusterOff,
100    // This indicates that the physical state of the device has changed and our state machine should
101    // reflect that without making any changes to the state of the device.
102    Update,
103}
104
105struct OnOffState {
106    state: OnOffClusterState,
107    global_scene_control: bool,
108    on_time: u16,
109    off_wait_time: u16,
110}
111
112impl OnOffState {
113    pub const fn new(state: OnOffClusterState) -> Self {
114        Self {
115            state,
116            global_scene_control: true,
117            on_time: 0,
118            off_wait_time: 0,
119        }
120    }
121}
122
123/// Implementation of the Matter On/Off cluster handler.
124///
125/// This struct provides the logic for managing the On/Off cluster state machine, handling commands,
126/// attributes, and feature dependencies as specified by the Matter specification. It supports coupling
127/// with a LevelControl cluster, manages timed transitions, and enforces feature-specific requirements.
128///
129/// # Usage
130/// - Implement the `OnOffHooks` trait to provide device-specific persistence and effect handling.
131/// - Instantiate with a `Dataver` and user-provided `OnOffHooks` implementation.
132/// - Initialise and optionally couple with a LevelControl cluster via `init`.
133/// - Use the async `run` method to process incoming commands and manage state transitions.
134///
135/// # Panics
136/// - The handler will panic during initialisation if the cluster configuration is invalid or missing required
137///   attributes/commands for enabled features.
138// TODO:
139// #[derive(Clone, Debug)]
140// #[cfg_attr(feature = "defmt", derive(defmt::Format))]
141pub struct OnOffHandler<'a, H: OnOffHooks, LH: LevelControlHooks> {
142    dataver: Dataver,
143    endpoint_id: EndptId,
144    hooks: H,
145    level_control_handler: Mutex<Cell<Option<&'a LevelControlHandler<'a, LH, H>>>>,
146    /// Set via [`OnOffHandler::with_scene_invalidator`] when this
147    /// device hosts Scenes Management on the same endpoint.
148    scene_invalidator: Mutex<Cell<Option<&'a dyn SceneInvalidator>>>,
149    state: Mutex<RefCell<OnOffState>>,
150    state_change_signal: Signal<Option<OnOffCommand>>,
151}
152
153impl<H: OnOffHooks> OnOffHandler<'_, H, NoLevelControl> {
154    /// Creates a new `OnOffHandler` with the given hooks which is **not** coupled with a `LevelControl` handler.
155    ///
156    /// NOTE: This constructor automatically calls `init` with no coupled `LevelControl` handler.
157    ///
158    /// # Arguments
159    /// - `hooks` - A reference to the struct implementing the device-specific on/off logic.
160    pub fn new_standalone(dataver: Dataver, endpoint_id: EndptId, hooks: H) -> Self {
161        let this = Self::new(dataver, endpoint_id, hooks);
162
163        this.init(None);
164
165        this
166    }
167}
168
169impl<'a, H: OnOffHooks, LH: LevelControlHooks> OnOffHandler<'a, H, LH> {
170    /// Creates a new `OnOffHandler` with the given hooks.
171    ///
172    /// # Arguments
173    /// - `hooks` - A reference to the struct implementing the device-specific on/off logic.
174    ///
175    /// # Usage
176    /// - Initialise and optionally couple with a LevelControl handler via `init`.
177    pub fn new(dataver: Dataver, endpoint_id: EndptId, hooks: H) -> Self {
178        let state = match hooks.on_off() {
179            true => OnOffClusterState::On,
180            false => OnOffClusterState::Off,
181        };
182
183        Self {
184            dataver,
185            endpoint_id,
186            hooks,
187            level_control_handler: Mutex::new(Cell::new(None)),
188            scene_invalidator: Mutex::new(Cell::new(None)),
189            state: Mutex::new(RefCell::new(OnOffState::new(state))),
190            state_change_signal: Signal::new(None),
191        }
192    }
193
194    /// Checks that the cluster is correctly configured, including required attributes, commands, and feature dependencies.
195    ///
196    /// # Panics
197    ///
198    /// Panics with an error message if the handler's cluster configuration (`Self::CLUSTER`) is misconfigured.
199    fn validate(&self) {
200        if Self::CLUSTER.revision != 6 {
201            panic!(
202                "OnOff validation: incorrect version number: expected 6 got {}",
203                Self::CLUSTER.revision
204            );
205        }
206
207        // Check for mandatory attributes
208        if Self::CLUSTER.attribute(AttributeId::OnOff as _).is_none() {
209            panic!("OnOff validation: missing required attribute: OnOff");
210        }
211
212        // Check for mandatory commands
213        if Self::CLUSTER.command(CommandId::Off as _).is_none() {
214            panic!("OnOff validation: missing required command: Off");
215        }
216
217        // Check LIGHTING feature requirements
218        if Self::supports_feature(on_off::Feature::LIGHTING.bits()) {
219            if Self::CLUSTER
220                .attribute(AttributeId::GlobalSceneControl as _)
221                .is_none()
222                || Self::CLUSTER.attribute(AttributeId::OnTime as _).is_none()
223                || Self::CLUSTER
224                    .attribute(AttributeId::OffWaitTime as _)
225                    .is_none()
226                || Self::CLUSTER
227                    .attribute(AttributeId::StartUpOnOff as _)
228                    .is_none()
229            {
230                panic!("OnOff validation: missing attributes required by LIGHTING feature: GlobalSceneControl, OnTime, OffWaitTime, StartUpOnOff")
231            }
232
233            if Self::CLUSTER
234                .command(CommandId::OffWithEffect as _)
235                .is_none()
236                || Self::CLUSTER
237                    .command(CommandId::OnWithRecallGlobalScene as _)
238                    .is_none()
239                || Self::CLUSTER
240                    .command(CommandId::OnWithTimedOff as _)
241                    .is_none()
242            {
243                panic!("OnOff validation: missing commands required by LIGHTING feature: OffWithEffect, OnWithRecallGlobalScene, OnWithTimedOff")
244            }
245        }
246
247        // Check OFFONLY feature requirements
248        if Self::supports_feature(on_off::Feature::OFF_ONLY.bits())
249            && (Self::CLUSTER.command(CommandId::On as _).is_some()
250                || Self::CLUSTER.command(CommandId::Toggle as _).is_some())
251        {
252            panic!("OnOff validation: extra commands while using OFFONLY feature: On, Toggle")
253        }
254    }
255
256    /// Initialise the cluster on startup.
257    /// - wire coupled handlers
258    /// - validate the handler setup with the configuration
259    /// - update the OnOff state based on the StartUpOnOff attribute
260    ///
261    /// # Parameters
262    /// *level_control_handler: the LevelControlHandler instance coupled with this OnOffHandler, i.e. the LevelControl cluster on the same endpoint.
263    ///
264    /// # Panics
265    ///
266    /// panics if the `state`'s `CLUSTER` is misconfigured.
267    pub fn init(&self, level_control_handler: Option<&'a LevelControlHandler<'a, LH, H>>) {
268        // Wire any coupled clusters
269        self.level_control_handler
270            .lock(|h| h.set(level_control_handler));
271
272        self.validate();
273
274        // 1.5.6.6. StartUpOnOff Attribute
275        // This attribute SHALL define the desired startup behavior of a device when it is supplied with power
276        // and this state SHALL be reflected in the OnOff attribute. If the value is null, the OnOff attribute is
277        // set to its previous value. Otherwise, the behavior is defined in the table defining StartUpOnOffEnum.
278        // todo: Implement checking the reason for reboot.
279        // This behavior does not apply to reboots associated with OTA. After an OTA restart, the OnOff
280        // attribute SHALL return to its value prior to the restart.
281        //
282        // Note: We assume that since the on_off state is persisted by the user and it is entangled with the
283        // actual state of the device, if start_up_on_off == null we don't need to do anything.
284        if let Some(start_up_state) = self.hooks.start_up_on_off().into_option() {
285            match start_up_state {
286                StartUpOnOffEnum::Off => self.hooks.set_on_off(false),
287                StartUpOnOffEnum::On => self.hooks.set_on_off(true),
288                StartUpOnOffEnum::Toggle => self.hooks.set_on_off(!self.hooks.on_off()),
289            }
290        }
291    }
292
293    /// Adapt the handler instance to the generic `rs-matter` `Handler` trait
294    pub const fn adapt(self) -> HandlerAsyncAdaptor<Self> {
295        HandlerAsyncAdaptor(self)
296    }
297
298    /// Attach a [`SceneInvalidator`] — typically the
299    /// [`crate::dm::clusters::scenes::ScenesState`] backing Scenes
300    /// Management on the same endpoint — so command-driven `OnOff`
301    /// mutations flip `SceneValid → false` for any recalled scene.
302    /// No-op when unset.
303    pub fn with_scene_invalidator(self, invalidator: &'a dyn SceneInvalidator) -> Self {
304        self.scene_invalidator
305            .lock(|cell| cell.set(Some(invalidator)));
306        self
307    }
308
309    fn notify_scenable_changed(&self) {
310        if let Some(inv) = self.scene_invalidator.lock(|cell| cell.get()) {
311            inv.scenable_attribute_changed(self.endpoint_id);
312        }
313    }
314
315    /// Request an out-of-band change to the OnOff state.
316    /// This method can be used, for example, when the device state changes due to physical interactions
317    /// or when the device autonomously decides to change its state.
318    ///
319    /// This method behaves the same as the OnOff cluster's On or Off commands.
320    /// I.e, This method will trigger the appropriate state change logic, including any coupled cluster interactions,
321    /// feature-dependent attribute updates and device-specific update logic.
322    pub fn set_on_off(&self, on: bool) {
323        match on {
324            true => self.state_change_signal.signal(OnOffCommand::On),
325            false => self.state_change_signal.signal(OnOffCommand::Off),
326        }
327    }
328
329    // Allows coupled clusters or user code to get the on_off state.
330    pub fn on_off(&self) -> bool {
331        self.hooks.on_off()
332    }
333
334    /// Sets the on_off state to true and updates the off_wait_time and global_scene_control accordingly.
335    /// If not initiated by LevelControl and LevelControl cluster is coupled, call the LevelControl coupling logic.
336    fn set_on(
337        &self,
338        state: &mut OnOffState,
339        level_control_initiated: bool,
340        scene_apply: bool,
341        ctx: impl HandlerContext,
342    ) {
343        if self.hooks.on_off() {
344            return;
345        }
346
347        // 1.5.7.2. On Command
348        // ... on receipt of the On command, a server SHALL set the OnOff attribute to TRUE.
349        self.hooks.set_on_off(true);
350
351        let lighting_attrs_updated = Self::update_attr_on(state);
352
353        ctx.notify_attr_changed(self.endpoint_id, Self::CLUSTER.id, AttributeId::OnOff as _);
354        // Scene-recall mutations transition *into* the recalled state,
355        // so they must not trigger `SceneValid` drift-detection.
356        if !scene_apply {
357            self.notify_scenable_changed();
358        }
359        if lighting_attrs_updated {
360            // `update_attr_on` may have forced OffWaitTime to 0 and GlobalSceneControl to TRUE
361            ctx.notify_attr_changed(
362                self.endpoint_id,
363                Self::CLUSTER.id,
364                AttributeId::OffWaitTime as _,
365            );
366            ctx.notify_attr_changed(
367                self.endpoint_id,
368                Self::CLUSTER.id,
369                AttributeId::GlobalSceneControl as _,
370            );
371        }
372
373        // LevelControl coupling logic defined in the spec
374        if !level_control_initiated {
375            if let Some(level_control_handler) = self.level_control_handler.lock(|h| h.get()) {
376                level_control_handler.coupled_on_off_cluster_on_off_state_change(true);
377            }
378        }
379    }
380
381    // Updates Matter attributes when the state changes to On.
382    // Returns true if attributes have been updated and hence Matter notification is required.
383    fn update_attr_on(state: &mut OnOffState) -> bool {
384        // Note: The OnTime, OffWaitTime and GlobalScenesControl attributes are only supported and must
385        // be supported when the LIGHTING feature is enabled.
386        // This configuration is ensured by the validate method upon initialisation.
387        if Self::supports_feature(on_off::Feature::LIGHTING.bits()) {
388            // 1.5.7.2. On Command
389            // ... when the OnTime and OffWaitTime attributes are both supported, if the value of the
390            // OnTime attribute is equal to 0, the server SHALL set the OffWaitTime attribute to 0.
391            if state.on_time == 0 {
392                state.off_wait_time = 0;
393            }
394
395            // 1.5.6.3. GlobalSceneControl Attribute
396            // This attribute SHALL be set to TRUE after the reception of a command which causes the OnOff
397            // attribute to be set to TRUE, such as a standard On command, a MoveToLevel(WithOnOff) command,
398            // a RecallScene command or a OnWithRecallGlobalScene command.
399            state.global_scene_control = true;
400
401            return true;
402        }
403        false
404    }
405
406    /// Sets the on_off state to false.
407    /// If a LevelControl cluster is coupled with this OnOff cluster and this command was not initiated by the
408    /// LevelControl cluster, the coupled flow is initiated.
409    /// In this case, the method will not set the on_off state to false and returns false.
410    /// Otherwise, we set the on_off state to false and return true.
411    /// The return boolean indicates if the on_off state has been set.
412    fn set_off(
413        &self,
414        state: &mut OnOffState,
415        level_control_initiated: bool,
416        scene_apply: bool,
417        ctx: impl HandlerContext,
418    ) -> bool {
419        if !self.hooks.on_off() {
420            return true;
421        }
422
423        let on_time_updated = Self::update_attr_off(state);
424
425        // LevelControl coupling logic defined in the spec
426        let level_control_handler = self.level_control_handler.lock(|h| h.get());
427        if let Some(level_control_handler) = level_control_handler {
428            if !level_control_initiated {
429                level_control_handler.coupled_on_off_cluster_on_off_state_change(false);
430
431                if on_time_updated {
432                    ctx.notify_attr_changed(
433                        self.endpoint_id,
434                        Self::CLUSTER.id,
435                        AttributeId::OnOff as _,
436                    );
437                    // `update_attr_off` forced OnTime to 0
438                    ctx.notify_attr_changed(
439                        self.endpoint_id,
440                        Self::CLUSTER.id,
441                        AttributeId::OnTime as _,
442                    );
443                }
444
445                // When calling the LevelControl with false (off), the levelControl cluster will call
446                // back into the OnOff cluster to set the OnOff attribute to false when it is done.
447                // Hence, we return without setting the on_off attribute.
448                return false;
449            }
450        }
451
452        // 1.5.7.1. Off Command
453        // On receipt of the Off command, a server SHALL set the OnOff attribute to FALSE.
454        self.hooks.set_on_off(false);
455        ctx.notify_attr_changed(self.endpoint_id, Self::CLUSTER.id, AttributeId::OnOff as _);
456        // See `set_on` for why this is gated by `scene_apply`.
457        if !scene_apply {
458            self.notify_scenable_changed();
459        }
460        if on_time_updated {
461            // `update_attr_off` forced OnTime to 0
462            ctx.notify_attr_changed(self.endpoint_id, Self::CLUSTER.id, AttributeId::OnTime as _);
463        }
464
465        true
466    }
467
468    // Update Matter attributes when the state changes to Off.
469    // Returns true if attributes have been updated and hence Matter notification is required.
470    fn update_attr_off(state: &mut OnOffState) -> bool {
471        if Self::supports_feature(on_off::Feature::LIGHTING.bits()) && state.on_time != 0 {
472            // 1.5.7.1. Off Command
473            // ... when the OnTime attribute is supported, the server SHALL set the OnTime attribute to 0.
474            state.on_time = 0;
475            return true;
476        }
477        false
478    }
479
480    fn supports_feature(features: u32) -> bool {
481        H::CLUSTER.feature_map & features != 0
482    }
483
484    // Updates the state of the state machine and Matter attributes to match the state of the physical device.
485    // The state of the physical device is not modified.
486    fn update(&self, state: &mut OnOffState, ctx: impl HandlerContext) {
487        match self.on_off() {
488            true => {
489                if state.state == OnOffClusterState::On {
490                    return;
491                }
492
493                state.state = OnOffClusterState::On;
494
495                let lighting_attrs_updated = Self::update_attr_on(state);
496
497                ctx.notify_attr_changed(
498                    self.endpoint_id,
499                    Self::CLUSTER.id,
500                    AttributeId::OnOff as _,
501                );
502                if lighting_attrs_updated {
503                    ctx.notify_attr_changed(
504                        self.endpoint_id,
505                        Self::CLUSTER.id,
506                        AttributeId::OffWaitTime as _,
507                    );
508                    ctx.notify_attr_changed(
509                        self.endpoint_id,
510                        Self::CLUSTER.id,
511                        AttributeId::GlobalSceneControl as _,
512                    );
513                }
514            }
515            false => {
516                if state.state == OnOffClusterState::Off {
517                    return;
518                }
519
520                state.state = OnOffClusterState::Off;
521
522                let on_time_updated = Self::update_attr_off(state);
523
524                ctx.notify_attr_changed(
525                    self.endpoint_id,
526                    Self::CLUSTER.id,
527                    AttributeId::OnOff as _,
528                );
529                if on_time_updated {
530                    ctx.notify_attr_changed(
531                        self.endpoint_id,
532                        Self::CLUSTER.id,
533                        AttributeId::OnTime as _,
534                    );
535                }
536            }
537        }
538    }
539
540    async fn state_machine(&self, command: OnOffCommand, ctx: impl HandlerContext) {
541        enum Outcome {
542            Done,
543            Continue,
544            OffWithEffect {
545                effect_variant: EffectVariantEnum,
546                final_state: OnOffClusterState,
547            },
548            Delay,
549        }
550
551        loop {
552            let outcome = self.with_state(|state| {
553                match state.state {
554                    OnOffClusterState::On => match command {
555                        OnOffCommand::Off | OnOffCommand::Toggle => {
556                            if self.set_off(state, false, false, &ctx) {
557                                state.state = OnOffClusterState::Off;
558                            }
559                            Outcome::Done
560                        }
561                        OnOffCommand::CoupledClusterOff => {
562                            self.set_off(state, true, false, &ctx);
563                            state.state = OnOffClusterState::Off;
564                            Outcome::Done
565                        }
566                        OnOffCommand::On | OnOffCommand::CoupledClusterOn => Outcome::Done,
567                        OnOffCommand::OffWithEffect(effect) => {
568                            // 1.5.7.4.3. Effect on Receipt
569                            // On receipt of the OffWithEffect command the server SHALL check the value of the
570                            // GlobalSceneControl attribute.
571                            // If the GlobalSceneControl attribute is equal to TRUE, the server SHALL store its settings in its global
572                            // scene then set the GlobalSceneControl attribute to FALSE...
573                            // todo: store the GlobalSceneControl setting (true) in the global scene.
574                            let gsc_changed = state.global_scene_control;
575                            state.global_scene_control = false;
576                            if gsc_changed {
577                                ctx.notify_attr_changed(
578                                    self.endpoint_id,
579                                    Self::CLUSTER.id,
580                                    AttributeId::GlobalSceneControl as _,
581                                );
582                            }
583
584                            Outcome::OffWithEffect {
585                                effect_variant: effect,
586                                final_state: OnOffClusterState::Off,
587                            }
588                        }
589                        OnOffCommand::OnWithTimedOff => {
590                            state.state = OnOffClusterState::TimedOn;
591                            Outcome::Continue
592                        }
593                        OnOffCommand::Update => {
594                            self.update(state, &ctx);
595                            Outcome::Done
596                        }
597                    },
598                    OnOffClusterState::Off => match command {
599                        OnOffCommand::Off
600                        | OnOffCommand::OffWithEffect(_)
601                        | OnOffCommand::CoupledClusterOff => Outcome::Done,
602                        OnOffCommand::On | OnOffCommand::Toggle => {
603                            state.state = OnOffClusterState::On;
604                            self.set_on(state, false, false, &ctx);
605                            Outcome::Done
606                        }
607                        OnOffCommand::CoupledClusterOn => {
608                            state.state = OnOffClusterState::On;
609                            self.set_on(state, true, false, &ctx);
610                            Outcome::Done
611                        }
612                        OnOffCommand::OnWithTimedOff => {
613                            state.state = OnOffClusterState::TimedOn;
614                            Outcome::Continue
615                        }
616                        OnOffCommand::Update => {
617                            self.update(state, &ctx);
618                            Outcome::Done
619                        }
620                    },
621                    OnOffClusterState::TimedOn => {
622                        match command {
623                            OnOffCommand::Off | OnOffCommand::Toggle => {
624                                trace!("Got Off command from TimedOn state");
625                                if self.set_off(state, false, false, &ctx) {
626                                    state.state = OnOffClusterState::DelayedOff;
627                                    Outcome::Continue
628                                } else {
629                                    // If set_off returns false, we brake and expect to be called again by the CoupledClusterOff command.
630                                    Outcome::Done
631                                }
632                            }
633                            OnOffCommand::CoupledClusterOff => {
634                                self.set_off(state, true, false, &ctx);
635                                state.state = OnOffClusterState::DelayedOff;
636                                // Same as the `Off` / `Toggle` arm above: the
637                                // guarded `DelayedOff` state carries a
638                                // non-zero `OffWaitTime` that the 1/10th-second
639                                // update must count down to 0 (App Cluster
640                                // spec). This is the path an `Off` takes when
641                                // a coupled LevelControl runs its own
642                                // off-transition first.
643                                Outcome::Continue
644                            }
645                            OnOffCommand::OffWithEffect(effect) => {
646                                // 1.5.7.4.3. Effect on Receipt
647                                // On receipt of the OffWithEffect command the server SHALL check the value of the
648                                // GlobalSceneControl attribute.
649                                // If the GlobalSceneControl attribute is equal to TRUE, the server SHALL store its settings in its global
650                                // scene then set the GlobalSceneControl attribute to FALSE...
651                                // todo: store the GlobalSceneControl setting (true) in the global scene.
652                                let gsc_changed = state.global_scene_control;
653                                state.global_scene_control = false;
654                                if gsc_changed {
655                                    ctx.notify_attr_changed(
656                                        self.endpoint_id,
657                                        Self::CLUSTER.id,
658                                        AttributeId::GlobalSceneControl as _,
659                                    );
660                                }
661
662                                Outcome::OffWithEffect {
663                                    effect_variant: effect,
664                                    final_state: OnOffClusterState::DelayedOff,
665                                }
666                            }
667                            // 1.5.7.6.4. Effect on Receipt
668                            // If the value of the OnOff attribute is equal to TRUE and the value of the OnTime attribute is
669                            // greater than zero, the server SHALL decrement the value of the OnTime attribute. If the value of
670                            // the OnTime attribute reaches 0, the server SHALL set the OffWaitTime and OnOff attributes to 0
671                            // and FALSE, respectively.
672                            OnOffCommand::On | OnOffCommand::OnWithTimedOff => {
673                                if state.on_time > 0 {
674                                    state.on_time -= 1;
675                                    Outcome::Delay
676                                } else {
677                                    state.off_wait_time = 0;
678                                    if self.set_off(state, false, false, &ctx) {
679                                        state.state = OnOffClusterState::Off;
680                                    }
681                                    Outcome::Done
682                                }
683                            }
684                            OnOffCommand::CoupledClusterOn => {
685                                // This should not be reachable as the device would already be on so a change in the LevelControl cluster cannot cause the OnOff cluster to switch to On.
686                                unreachable!("CoupledClusterOn should not be reachable in TimedOn state: device is already on")
687                            }
688                            OnOffCommand::Update => {
689                                self.update(state, &ctx);
690                                Outcome::Done
691                            }
692                        }
693                    }
694                    OnOffClusterState::DelayedOff => {
695                        match command {
696                            // 1.5.6.5. OffWaitTime Attribute
697                            // This attribute specifies the length of time (in 1/10ths second) that the Off state SHALL be guarded to
698                            // prevent another OnWithTimedOff command turning the server back to its On state.
699                            OnOffCommand::On | OnOffCommand::Toggle => {
700                                state.state = OnOffClusterState::On;
701                                self.set_on(state, false, false, &ctx);
702                                Outcome::Done
703                            }
704                            OnOffCommand::CoupledClusterOn => {
705                                state.state = OnOffClusterState::On;
706                                self.set_on(state, true, false, &ctx);
707                                Outcome::Done
708                            }
709                            OnOffCommand::Off
710                            | OnOffCommand::OffWithEffect(_)
711                            | OnOffCommand::OnWithTimedOff
712                            | OnOffCommand::CoupledClusterOff => {
713                                // 1.5.7.6.4. Effect on Receipt
714                                // If the value of the OnOff attribute is equal to FALSE and the value of the OffWaitTime attribute
715                                // is greater than zero, the server SHALL decrement the value of the OffWaitTime attribute. If the
716                                // value of the OffWaitTime attribute reaches 0, the server SHALL terminate the update.
717                                if state.off_wait_time > 0 {
718                                    state.off_wait_time -= 1;
719                                    Outcome::Delay
720                                } else {
721                                    state.state = OnOffClusterState::Off;
722                                    Outcome::Done
723                                }
724                            }
725                            OnOffCommand::Update => {
726                                self.update(state, &ctx);
727                                Outcome::Done
728                            }
729                        }
730                    }
731                }
732            });
733
734            match outcome {
735                Outcome::Done => break,
736                Outcome::Continue => (),
737                Outcome::Delay => embassy_time::Timer::after_millis(100).await,
738                Outcome::OffWithEffect {
739                    effect_variant,
740                    final_state,
741                } => {
742                    self.hooks.handle_off_with_effect(effect_variant).await;
743
744                    self.with_state(|state| {
745                        // This is set to true because in this case we do not want to also run the effects from the LevelControl cluster.
746                        let _ = self.set_off(state, true, false, &ctx);
747
748                        state.state = final_state;
749                    });
750
751                    // Landing in the guarded `DelayedOff` state (an
752                    // `OffWithEffect` received while a timed-on cycle was
753                    // running) leaves a non-zero `OffWaitTime` behind, and
754                    // per App Cluster spec the 1/10th-second update must
755                    // keep running until it reaches 0 - so keep looping and
756                    // let the `DelayedOff` arm count it down. Any other
757                    // final state has no timers left to service.
758                    if final_state != OnOffClusterState::DelayedOff {
759                        break;
760                    }
761                }
762            }
763        }
764    }
765
766    fn out_of_band_message(&self, message: OutOfBandMessage) {
767        match message {
768            OutOfBandMessage::Update => self.state_change_signal.signal(OnOffCommand::Update),
769            OutOfBandMessage::On => self.state_change_signal.signal(OnOffCommand::On),
770            OutOfBandMessage::Off => self.state_change_signal.signal(OnOffCommand::Off),
771            OutOfBandMessage::Toggle => self.state_change_signal.signal(OnOffCommand::Toggle),
772        }
773    }
774
775    // The method that should be used by coupled clusters to update the on_off state.
776    pub(crate) fn coupled_cluster_set_on_off(&self, on: bool) {
777        info!(
778            "OnOffCluster: coupled_cluster_set_on_off: Setting on_off to {}",
779            on
780        );
781
782        self.with_state(|state| {
783            match on {
784                true => {
785                    if state.state == OnOffClusterState::DelayedOff {
786                        warn!("LevelControl is trying to set OnOff to true while the OnOff cluster is in the guarded 'Delayed Off' state");
787                        return;
788                    }
789
790                    self.state_change_signal
791                        .signal(OnOffCommand::CoupledClusterOn);
792                }
793                false => self
794                    .state_change_signal
795                    .signal(OnOffCommand::CoupledClusterOff),
796            }
797        })
798    }
799
800    fn with_state<F, R>(&self, f: F) -> R
801    where
802        F: FnOnce(&mut OnOffState) -> R,
803    {
804        self.state.lock(|state| {
805            let mut state = state.borrow_mut();
806
807            f(&mut state)
808        })
809    }
810}
811
812impl<H: OnOffHooks, LH: LevelControlHooks> ClusterAsyncHandler for OnOffHandler<'_, H, LH> {
813    #[doc = "The cluster-metadata corresponding to this handler trait."]
814    const CLUSTER: Cluster<'static> = H::CLUSTER;
815
816    fn dataver(&self) -> u32 {
817        self.dataver.get()
818    }
819
820    fn dataver_changed(&self) {
821        self.dataver.changed();
822    }
823
824    async fn run(&self, ctx: impl HandlerContext) -> Result<(), Error> {
825        let mut hooks_fut = pin!(self.hooks.run(|message| self.out_of_band_message(message)));
826
827        loop {
828            let mut command = match select(
829                &mut hooks_fut,
830                self.state_change_signal.wait_signalled()
831            ).await {
832                Either::First(_) => panic!("OnOffHooks::run returned; implementers MUST not return. Implementations should loop forever or await core::future::pending::<()>()."),
833                Either::Second(command) => command,
834            };
835
836            loop {
837                match select3(
838                    &mut hooks_fut,
839                    self.state_machine(command, &ctx),
840                    self.state_change_signal.wait_signalled(),
841                )
842                .await
843                {
844                    Either3::First(_) => panic!("OnOffHooks::run returned; implementers MUST not return. Implementations should loop forever or await core::future::pending::<()>()."),
845                    Either3::Second(_) => break,
846                    Either3::Third(new_command) => command = new_command,
847                }
848            }
849        }
850    }
851
852    // Attribute accessors
853    fn on_off(&self, _ctx: impl ReadContext) -> impl Future<Output = Result<bool, Error>> {
854        ready(Ok(self.hooks.on_off()))
855    }
856
857    fn global_scene_control(
858        &self,
859        _ctx: impl ReadContext,
860    ) -> impl Future<Output = Result<bool, Error>> {
861        ready(Ok(self.with_state(|state| state.global_scene_control)))
862    }
863
864    fn on_time(&self, _ctx: impl ReadContext) -> impl Future<Output = Result<u16, Error>> {
865        ready(Ok(self.with_state(|state| state.on_time)))
866    }
867
868    fn off_wait_time(&self, _ctx: impl ReadContext) -> impl Future<Output = Result<u16, Error>> {
869        ready(Ok(self.with_state(|state| state.off_wait_time)))
870    }
871
872    fn start_up_on_off(
873        &self,
874        _ctx: impl ReadContext,
875    ) -> impl Future<Output = Result<Nullable<StartUpOnOffEnum>, Error>> {
876        ready(Ok(self.hooks.start_up_on_off()))
877    }
878
879    fn set_on_time(
880        &self,
881        ctx: impl WriteContext,
882        value: u16,
883    ) -> impl Future<Output = Result<(), Error>> {
884        ready(self.with_state(|state| {
885            state.on_time = value;
886            ctx.notify_changed();
887            Ok(())
888        }))
889    }
890
891    fn set_off_wait_time(
892        &self,
893        ctx: impl WriteContext,
894        value: u16,
895    ) -> impl Future<Output = Result<(), Error>> {
896        ready(self.with_state(|state| {
897            state.off_wait_time = value;
898            ctx.notify_changed();
899            Ok(())
900        }))
901    }
902
903    fn set_start_up_on_off(
904        &self,
905        ctx: impl WriteContext,
906        value: Nullable<StartUpOnOffEnum>,
907    ) -> impl Future<Output = Result<(), Error>> {
908        ready(match self.hooks.set_start_up_on_off(value) {
909            Ok(()) => {
910                ctx.notify_changed();
911                Ok(())
912            }
913            Err(e) => Err(e),
914        })
915    }
916
917    // Commands
918    fn handle_off(&self, _ctx: impl InvokeContext) -> impl Future<Output = Result<(), Error>> {
919        ready({
920            self.state_change_signal.signal(OnOffCommand::Off);
921            Ok(())
922        })
923    }
924
925    fn handle_on(&self, _ctx: impl InvokeContext) -> impl Future<Output = Result<(), Error>> {
926        ready({
927            self.state_change_signal.signal(OnOffCommand::On);
928            Ok(())
929        })
930    }
931
932    fn handle_toggle(&self, _ctx: impl InvokeContext) -> impl Future<Output = Result<(), Error>> {
933        ready({
934            self.state_change_signal.signal(OnOffCommand::Toggle);
935            Ok(())
936        })
937    }
938
939    fn handle_off_with_effect(
940        &self,
941        _ctx: impl InvokeContext,
942        request: OffWithEffectRequest<'_>,
943    ) -> impl Future<Output = Result<(), Error>> {
944        ready('a: {
945            if !Self::supports_feature(on_off::Feature::LIGHTING.bits()) {
946                // This error is currently mapped to the IM status UnsupportedCommand.
947                break 'a Err(ErrorCode::CommandNotFound.into());
948            }
949
950            let effect_variant = match request.effect_identifier() {
951                Err(e) => break 'a Err(e),
952                Ok(EffectIdentifierEnum::DelayedAllOff) => match request.effect_variant() {
953                    Err(e) => break 'a Err(e),
954                    // todo Impl TryFrom for DelayedAllOffEffectVariantEnum and remove this match.
955                    Ok(0) => EffectVariantEnum::DelayedAllOff(
956                        DelayedAllOffEffectVariantEnum::DelayedOffFastFade,
957                    ),
958                    Ok(1) => {
959                        EffectVariantEnum::DelayedAllOff(DelayedAllOffEffectVariantEnum::NoFade)
960                    }
961                    Ok(2) => EffectVariantEnum::DelayedAllOff(
962                        DelayedAllOffEffectVariantEnum::DelayedOffSlowFade,
963                    ),
964                    Ok(_) => break 'a Err(ErrorCode::Failure.into()),
965                },
966                Ok(EffectIdentifierEnum::DyingLight) => match request.effect_variant() {
967                    Err(e) => break 'a Err(e),
968                    // todo Impl TryFrom for DyingLightEffectVariantEnum and remove this match.
969                    Ok(0) => EffectVariantEnum::DyingLight(
970                        DyingLightEffectVariantEnum::DyingLightFadeOff,
971                    ),
972                    Ok(_) => break 'a Err(ErrorCode::Failure.into()),
973                },
974            };
975
976            self.state_change_signal
977                .signal(OnOffCommand::OffWithEffect(effect_variant));
978
979            Ok(())
980        })
981    }
982
983    fn handle_on_with_recall_global_scene(
984        &self,
985        _ctx: impl InvokeContext,
986    ) -> impl Future<Output = Result<(), Error>> {
987        ready(self.with_state(|state| {
988            // 1.5.7.5.1. Effect on Receipt
989            // On receipt of the OnWithRecallGlobalScene command, if the GlobalSceneControl attribute is equal
990            // to TRUE, the server SHALL discard the command.
991            if state.global_scene_control {
992                return Ok(());
993            }
994
995            // If the GlobalSceneControl attribute is equal to FALSE, the Scene cluster server on the same endpoint
996            // SHALL recall its global scene, updating the OnOff attribute accordingly. The OnOff server SHALL
997            // then set the GlobalSceneControl attribute to TRUE.
998            // Additionally, when the OnTime and OffWaitTime attributes are both supported, if the value of the
999            // OnTime attribute is equal to 0, the server SHALL set the OffWaitTime attribute to 0.
1000            // todo Implement the above statement once the Scene cluster is implemented.
1001            // self.set_on(false);
1002
1003            // This error is currently mapped to the IM status UnsupportedCommand.
1004            Err(ErrorCode::CommandNotFound.into())
1005        }))
1006    }
1007
1008    fn handle_on_with_timed_off(
1009        &self,
1010        ctx: impl InvokeContext,
1011        request: OnWithTimedOffRequest<'_>,
1012    ) -> impl Future<Output = Result<(), Error>> {
1013        ready(match request.on_off_control() {
1014            Err(e) => Err(e),
1015            // 1.5.7.6.4. Effect on Receipt
1016            // On receipt of this command, if the AcceptOnlyWhenOn sub-field of the OnOffControl field is set to 1,
1017            // and the value of the OnOff attribute is equal to FALSE, the command SHALL be discarded.
1018            Ok(ctrl)
1019                if ctrl.contains(OnOffControlBitmap::ACCEPT_ONLY_WHEN_ON)
1020                    && !self.hooks.on_off() =>
1021            {
1022                Ok(())
1023            }
1024            Ok(_) => self.with_state(|state| {
1025                // If the value of the OffWaitTime attribute is greater than zero and the value of the OnOff attribute is
1026                // equal to FALSE, then the server SHALL set the OffWaitTime attribute to the minimum of the
1027                // OffWaitTime attribute and the value specified in the OffWaitTime field.
1028                if state.off_wait_time > 0 && !self.hooks.on_off() {
1029                    let new_off_wait_time = state.off_wait_time.min(request.off_wait_time()?);
1030                    if new_off_wait_time != state.off_wait_time {
1031                        state.off_wait_time = new_off_wait_time;
1032                        ctx.notify_own_attr_changed(AttributeId::OffWaitTime as _);
1033                    }
1034                }
1035                // In all other cases, the server SHALL set the OnTime attribute to the maximum of the OnTime
1036                // attribute and the value specified in the OnTime field, set the OffWaitTime attribute to the value
1037                // specified in the OffWaitTime field and set the OnOff attribute to TRUE.
1038                else {
1039                    let new_on_time = state.on_time.max(request.on_time()?);
1040                    let new_off_wait_time = request.off_wait_time()?;
1041                    let on_time_changed = new_on_time != state.on_time;
1042                    let off_wait_time_changed = new_off_wait_time != state.off_wait_time;
1043                    state.on_time = new_on_time;
1044                    state.off_wait_time = new_off_wait_time;
1045                    if on_time_changed || off_wait_time_changed {
1046                        if on_time_changed {
1047                            ctx.notify_own_attr_changed(AttributeId::OnTime as _);
1048                        }
1049                        if off_wait_time_changed {
1050                            ctx.notify_own_attr_changed(AttributeId::OffWaitTime as _);
1051                        }
1052                    }
1053                    self.set_on(state, false, false, &ctx);
1054                }
1055
1056                // If the values of the OnTime and OffWaitTime attributes are both not equal to 0xFFFF, the server
1057                // SHALL then update these attributes every 1/10th second until both the OnTime and OffWaitTime
1058                // attributes are equal to 0, as follows:
1059                if state.on_time == 0xFFFF && state.off_wait_time == 0xFFFF {
1060                    return Ok(());
1061                }
1062
1063                self.state_change_signal
1064                    .signal(OnOffCommand::OnWithTimedOff);
1065
1066                Ok(())
1067            }),
1068        })
1069    }
1070}
1071
1072pub trait OnOffHooks {
1073    const CLUSTER: Cluster<'static>;
1074
1075    // Get the current device on/off state. This value SHALL be persisted across reboots.
1076    fn on_off(&self) -> bool;
1077    // todo should we allow this to return an error? If so, we'd need to know if the state has changed even if error occurs.
1078    // todo make `async`
1079    // Switch the device to the `on` value and persist this setting.
1080    fn set_on_off(&self, on: bool);
1081
1082    // Get the start_up_on_off attribute. This value SHALL be persisted across reboots.
1083    fn start_up_on_off(&self) -> Nullable<StartUpOnOffEnum>;
1084    // Set the start_up_on_off attribute. This value SHALL be persisted across reboots.
1085    fn set_start_up_on_off(&self, value: Nullable<StartUpOnOffEnum>) -> Result<(), Error>;
1086
1087    async fn handle_off_with_effect(&self, effect: EffectVariantEnum);
1088
1089    /// Background task for out-of-band notifications to the handler.
1090    ///
1091    /// This future MUST NOT return. Implementers should either loop forever or await
1092    /// core::future::pending::<()>(), so the SDK's task does not observe a completed future.
1093    ///
1094    /// # Panics
1095    /// The SDK will panic if this method returns.
1096    async fn run<F: Fn(OutOfBandMessage)>(&self, _notify: F) {
1097        core::future::pending::<()>().await
1098    }
1099}
1100
1101impl<T> OnOffHooks for &T
1102where
1103    T: OnOffHooks,
1104{
1105    const CLUSTER: Cluster<'static> = T::CLUSTER;
1106
1107    fn on_off(&self) -> bool {
1108        (*self).on_off()
1109    }
1110
1111    fn set_on_off(&self, on: bool) {
1112        (*self).set_on_off(on)
1113    }
1114
1115    fn start_up_on_off(&self) -> Nullable<StartUpOnOffEnum> {
1116        (*self).start_up_on_off()
1117    }
1118
1119    fn set_start_up_on_off(&self, value: Nullable<StartUpOnOffEnum>) -> Result<(), Error> {
1120        (*self).set_start_up_on_off(value)
1121    }
1122
1123    fn handle_off_with_effect(&self, effect: EffectVariantEnum) -> impl Future<Output = ()> {
1124        (*self).handle_off_with_effect(effect)
1125    }
1126
1127    fn run<F: Fn(OutOfBandMessage)>(&self, notify: F) -> impl Future<Output = ()> {
1128        (*self).run(notify)
1129    }
1130}
1131
1132/// This is a phantom type for when the OnOff cluster is not coupled with a LevelControl cluster.
1133/// This type should only be used for annotations and not for actual LevelControl functionality.
1134/// All methods will panic.
1135pub struct NoLevelControl;
1136
1137impl LevelControlHooks for NoLevelControl {
1138    const MIN_LEVEL: u8 = 1;
1139    const MAX_LEVEL: u8 = 1;
1140    const FASTEST_RATE: u8 = 1;
1141    const CLUSTER: Cluster<'static> = level_control::FULL_CLUSTER;
1142
1143    fn set_device_level(&self, _: u8) -> Result<Option<u8>, ()> {
1144        panic!("NoLevelControl: set_device_level called unexpectedly - this phantom type should not be used for LevelControl functionality")
1145    }
1146
1147    fn current_level(&self) -> Option<u8> {
1148        panic!("NoLevelControl: current_level called unexpectedly - this phantom type should not be used for LevelControl functionality")
1149    }
1150
1151    fn set_current_level(&self, _level: Option<u8>) {
1152        panic!("NoLevelControl: set_current_level called unexpectedly - this phantom type should not be used for LevelControl functionality")
1153    }
1154}
1155
1156/// Scenes Management integration for the OnOff cluster. The only
1157/// scenable attribute is `OnOff`; apply routes through `set_on` /
1158/// `set_off` rather than an attribute write.
1159impl<H, LH> SceneClusterHandler for OnOffHandler<'_, H, LH>
1160where
1161    H: OnOffHooks,
1162    LH: LevelControlHooks,
1163{
1164    const CLUSTER_ID: ClusterId = FULL_CLUSTER.id;
1165
1166    fn endpoint_id(&self) -> EndptId {
1167        self.endpoint_id
1168    }
1169
1170    fn is_scenable_attribute(attribute_id: AttrId) -> bool {
1171        attribute_id == AttributeId::OnOff as AttrId
1172    }
1173
1174    fn capture<P: TLVBuilderParent>(
1175        &self,
1176        avp_array: AttributeValuePairStructArrayBuilder<P>,
1177    ) -> Result<AttributeValuePairStructArrayBuilder<P>, Error> {
1178        let v = self.hooks.on_off();
1179        avp_array.push_u8(AttributeId::OnOff as _, v as u8)
1180    }
1181
1182    async fn apply<C: HandlerContext>(
1183        &self,
1184        ctx: &C,
1185        avp_list: &TLVArray<'_, AttributeValuePairStruct<'_>>,
1186        _transition_time_ms: u32,
1187    ) -> Result<(), Error> {
1188        for avp in avp_list.iter() {
1189            let avp = avp?;
1190            if avp.attribute_id()? != AttributeId::OnOff as _ {
1191                continue;
1192            }
1193            let Some(value) = avp.value_unsigned_8()? else {
1194                continue;
1195            };
1196            // OnOff scene apply is a discrete transition (no per-scene
1197            // fade), so mutate inline via `set_on` / `set_off` rather
1198            // than the deferred `state_change_signal` path — Scenes
1199            // then calls `remember_current` to restore `SceneValid` in
1200            // the same await. `level_control_initiated=true` suppresses
1201            // OnOff↔LC coupling, since the scene blob carries its own
1202            // `CurrentLevel` AVP that LevelControl applies directly;
1203            // `scene_apply=true` suppresses drift-invalidation.
1204            self.with_state(|state| {
1205                if value != 0 {
1206                    self.set_on(state, true, true, ctx);
1207                } else {
1208                    self.set_off(state, true, true, ctx);
1209                }
1210            });
1211            return Ok(());
1212        }
1213        Ok(())
1214    }
1215}
1216
1217pub mod test {
1218    use embassy_time::{Duration, Timer};
1219
1220    use crate::dm::clusters::app::on_off::{
1221        EffectVariantEnum, OnOffHooks, OutOfBandMessage, StartUpOnOffEnum,
1222    };
1223    use crate::dm::clusters::decl::on_off as on_off_cluster;
1224    use crate::dm::clusters::decl::on_off::Feature;
1225    use crate::dm::Cluster;
1226    use crate::error::Error;
1227    use crate::tlv::Nullable;
1228    use crate::utils::cell::RefCell;
1229    use crate::utils::sync::blocking::Mutex;
1230    use crate::with;
1231
1232    struct TestOnOffState {
1233        on_off: bool,
1234        start_up_on_off: Option<StartUpOnOffEnum>,
1235    }
1236
1237    impl TestOnOffState {
1238        const fn new() -> Self {
1239            Self {
1240                on_off: false,
1241                start_up_on_off: None,
1242            }
1243        }
1244    }
1245
1246    /// This is a basic implementation of the OnOff device logic, an implementer of OnOffHooks, used for testing.
1247    // TODO:
1248    // #[derive(Clone, Debug)]
1249    // #[cfg_attr(feature = "defmt", derive(defmt::Format))]
1250    pub struct TestOnOffDeviceLogic {
1251        state: Mutex<RefCell<TestOnOffState>>,
1252        toggle_periodically: bool,
1253    }
1254
1255    impl TestOnOffDeviceLogic {
1256        pub const fn new(toggle_periodically: bool) -> Self {
1257            Self {
1258                state: Mutex::new(RefCell::new(TestOnOffState::new())),
1259                toggle_periodically,
1260            }
1261        }
1262    }
1263
1264    impl OnOffHooks for TestOnOffDeviceLogic {
1265        // The On/Off Light device type (Matter Device Library) requires
1266        // the `LT` (Lighting) feature on the OnOff cluster, which gates the
1267        // `GlobalSceneControl`/`OnTime`/`OffWaitTime`/`StartUpOnOff` attributes
1268        // and the `OffWithEffect`/`OnWithRecallGlobalScene`/`OnWithTimedOff`
1269        // commands. The library `OnOffHandler` already implements all of
1270        // these — we just opt in via the cluster metadata. Matches the
1271        // FeatureMap conformance check in `TC_DeviceConformance::test_TC_IDM_10_5`.
1272        const CLUSTER: Cluster<'static> = on_off_cluster::FULL_CLUSTER
1273            .with_revision(6)
1274            .with_features(Feature::LIGHTING.bits())
1275            .with_attrs(with!(
1276                required;
1277                on_off_cluster::AttributeId::OnOff
1278                    | on_off_cluster::AttributeId::GlobalSceneControl
1279                    | on_off_cluster::AttributeId::OnTime
1280                    | on_off_cluster::AttributeId::OffWaitTime
1281                    | on_off_cluster::AttributeId::StartUpOnOff
1282            ))
1283            .with_cmds(with!(
1284                on_off_cluster::CommandId::Off
1285                    | on_off_cluster::CommandId::On
1286                    | on_off_cluster::CommandId::Toggle
1287                    | on_off_cluster::CommandId::OffWithEffect
1288                    | on_off_cluster::CommandId::OnWithRecallGlobalScene
1289                    | on_off_cluster::CommandId::OnWithTimedOff
1290            ));
1291
1292        fn on_off(&self) -> bool {
1293            self.state.lock(|state| state.borrow().on_off)
1294        }
1295
1296        fn set_on_off(&self, on: bool) {
1297            self.state.lock(|state| state.borrow_mut().on_off = on);
1298        }
1299
1300        fn start_up_on_off(&self) -> Nullable<StartUpOnOffEnum> {
1301            match self.state.lock(|state| state.borrow().start_up_on_off) {
1302                Some(value) => Nullable::some(value),
1303                None => Nullable::none(),
1304            }
1305        }
1306
1307        fn set_start_up_on_off(&self, value: Nullable<StartUpOnOffEnum>) -> Result<(), Error> {
1308            self.state
1309                .lock(|state| state.borrow_mut().start_up_on_off = value.into_option());
1310            Ok(())
1311        }
1312
1313        async fn handle_off_with_effect(&self, _effect: EffectVariantEnum) {
1314            // no effect
1315        }
1316
1317        async fn run<F: Fn(OutOfBandMessage)>(&self, notify: F) {
1318            if self.toggle_periodically {
1319                loop {
1320                    // In a real example we wait for physical interaction.
1321                    Timer::after(Duration::from_secs(5)).await;
1322                    info!("Emulation: out of band toggle request");
1323                    notify(OutOfBandMessage::Toggle);
1324                }
1325            } else {
1326                core::future::pending::<()>().await
1327            }
1328        }
1329    }
1330}