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}