Skip to main content

rs_matter/dm/clusters/app/
level_control.rs

1/*
2 *
3 *    Copyright (c) 2025-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 Level Control cluster.
19//!
20//! This module provides the core logic and state management for the LevelControl cluster as defined by the Matter specification v1.3.
21//! It handles commands and attributes related to device level control, such as dimming lights or adjusting motor positions.
22//! The implementation supports asynchronous transitions, step and move operations, and integration with the OnOff cluster.
23//!
24//! Key features:
25//! - Validates cluster configuration and feature dependencies.
26//! - Manages level transitions with optional timing and rate control.
27//! - Supports quiet reporting of attribute changes according to specification rules.
28//! - Provides hooks for device-specific logic via the `LevelControlHooks` trait.
29//! - Designed for extensibility and integration with other clusters (e.g., OnOff).
30
31use core::cell::Cell;
32use core::future::{pending, ready, Future};
33use core::ops::Mul;
34use core::pin::pin;
35
36use embassy_futures::select::{select, select3, Either, Either3};
37use embassy_time::{Duration, Instant};
38
39use crate::dm::clusters::app::on_off::{OnOffHooks, FULL_CLUSTER as ON_OFF_FULL_CLUSTER};
40use crate::dm::clusters::app::{level_control, on_off::OnOffHandler};
41pub use crate::dm::clusters::decl::level_control::*;
42use crate::dm::clusters::decl::scenes_management::{
43    AttributeValuePairStruct, AttributeValuePairStructArrayBuilder,
44};
45use crate::dm::clusters::scenes::{SceneClusterHandler, SceneInvalidator};
46use crate::dm::{
47    AttrId, Cluster, ClusterId, Dataver, EndptId, HandlerContext, InvokeContext, ReadContext,
48    WriteContext,
49};
50use crate::error::{Error, ErrorCode};
51use crate::tlv::{Nullable, TLVArray, TLVBuilderParent};
52use crate::utils::cell::RefCell;
53use crate::utils::sync::blocking::Mutex;
54use crate::utils::sync::Signal;
55
56/// Messages passed to the `notify` closure in `LevelControlHooks::run()` method.
57#[derive(Clone, Copy, Debug, Eq, PartialEq)]
58#[cfg_attr(feature = "defmt", derive(defmt::Format))]
59pub enum OutOfBandMessage {
60    /// Indicates to the handler that the value of the current level has change and it should update the Matter state accordingly.
61    /// Takes the new value of the CurrentLevel.
62    Update(u8),
63    /// Initiates a MoveToLevel command.
64    /// This will change the state of the device if and when appropriate according to Matter logic.
65    /// See Matter Application Clusters specification.
66    MoveToLevel {
67        with_on_off: bool,
68        level: u8,
69        transition_time: Option<u16>,
70        options_mask: OptionsBitmap,
71        options_override: OptionsBitmap,
72    },
73    /// Initiates a Move command.
74    /// This will change the state of the device if and when appropriate according to Matter logic.
75    /// See Matter Application Clusters specification.
76    Move {
77        with_on_off: bool,
78        move_mode: MoveModeEnum,
79        rate: Option<u8>,
80        options_mask: OptionsBitmap,
81        options_override: OptionsBitmap,
82    },
83    /// Initiates a Step command.
84    /// This will change the state of the device if and when appropriate according to Matter logic.
85    /// See Matter Application Clusters specification.
86    Step {
87        with_on_off: bool,
88        step_mode: StepModeEnum,
89        step_size: u8,
90        transition_time: Option<u16>,
91        options_mask: OptionsBitmap,
92        options_override: OptionsBitmap,
93    },
94    /// Stop any running LevelControl transitions.
95    Stop,
96}
97
98enum Task {
99    MoveToLevel {
100        with_on_off: bool,
101        target: u8,
102        transition_time: u16,
103        /// When `true`, the transition was queued by a scene recall;
104        /// `set_level` skips `notify_scenable_changed` so `SceneValid`
105        /// is preserved.
106        scene_apply: bool,
107    },
108    Move {
109        with_on_off: bool,
110        move_mode: MoveModeEnum,
111        event_duration: Duration,
112    },
113    Stop,
114    OnOffStateChange {
115        on: bool,
116    },
117}
118
119struct LevelControlState {
120    on_level: Nullable<u8>,
121    options: OptionsBitmap,
122    remaining_time: u16,
123    on_off_transition_time: u16,
124    on_transition_time: Nullable<u16>,
125    off_transition_time: Nullable<u16>,
126    default_move_rate: Nullable<u8>,
127    previous_current_level: Option<u8>,
128    last_current_level_notification: Instant,
129}
130
131impl LevelControlState {
132    fn new(attribute_defaults: AttributeDefaults) -> Self {
133        Self {
134            on_level: attribute_defaults.on_level,
135            options: attribute_defaults.options,
136            remaining_time: 0,
137            on_off_transition_time: attribute_defaults.on_off_transition_time,
138            on_transition_time: attribute_defaults.on_transition_time,
139            off_transition_time: attribute_defaults.off_transition_time,
140            default_move_rate: attribute_defaults.default_move_rate,
141            previous_current_level: None,
142            last_current_level_notification: Instant::from_millis(0),
143        }
144    }
145
146    /// Updates the RemainingTime attribute and returns true if a Matter notification is required.
147    /// Matter notifications, reporting changes to this attribute, are only required under specific conditions.
148    ///
149    /// # Arguments
150    /// - `remaining_time` - The new remaining time.
151    /// - `is_start_of_transition` - Indicates if this is the start of a transition.
152    fn write_remaining_time_quietly(
153        &mut self,
154        remaining_time: Duration,
155        is_start_of_transition: bool,
156    ) -> bool {
157        let remaining_time_ds = remaining_time.as_millis().div_ceil(100) as u16;
158
159        // RemainingTime Quiet report conditions:
160        // - When it changes to 0, or
161        // - When it changes from 0 to any value higher than 10, or
162        // - When it changes, with a delta larger than 10, caused by the invoke of a command.
163        let previous_remaining_time = self.remaining_time;
164        let changed_to_zero = remaining_time_ds == 0 && previous_remaining_time != 0;
165        let changed_from_zero_gt_10 = previous_remaining_time == 0 && remaining_time_ds > 10;
166        let changed_by_gt_10 =
167            remaining_time_ds.abs_diff(previous_remaining_time) > 10 && is_start_of_transition;
168
169        self.remaining_time = remaining_time_ds;
170
171        if changed_to_zero || changed_from_zero_gt_10 || changed_by_gt_10 {
172            return true;
173        }
174
175        false
176    }
177}
178
179/// Implementation of the LevelControlHandler, providing functionality for the Matter Level Control cluster.
180///
181/// # Type Parameters
182/// - `'a`: Lifetime for references held by the cluster.
183/// - `H`: Handler implementing the LevelControlHooks trait, providing cluster-specific configuration and logic.
184/// - `OH` : Handler implementing the OnOffHooks trait.
185///
186/// # Constants
187/// - `MAXIMUM_LEVEL`: The maximum allowed level value (254).
188///
189/// # Panics
190/// - Initialisation panics if the cluster configuration is invalid or required attributes/commands are missing.
191///
192/// # Notes
193/// - This implementation follows version 1.3 of the Matter specification.
194// TODO:
195// #[derive(Clone, Debug)]
196// #[cfg_attr(feature = "defmt", derive(defmt::Format))]
197pub struct LevelControlHandler<'a, H: LevelControlHooks, OH: OnOffHooks> {
198    dataver: Dataver,
199    endpoint_id: EndptId,
200    hooks: H,
201    on_off_handler: Mutex<Cell<Option<&'a OnOffHandler<'a, OH, H>>>>,
202    /// See [`OnOffHandler::with_scene_invalidator`] — same role, fired
203    /// when `CurrentLevel` mutates.
204    scene_invalidator: Mutex<Cell<Option<&'a dyn SceneInvalidator>>>,
205    state: Mutex<RefCell<LevelControlState>>,
206    task_signal: Signal<Option<Task>>,
207}
208
209/// Default values for the attributes with manufacturer specific defaults.
210#[derive(Clone, Debug, PartialEq, Eq, Hash)]
211#[cfg_attr(feature = "defmt", derive(defmt::Format))]
212pub struct AttributeDefaults {
213    pub on_level: Nullable<u8>,
214    pub options: OptionsBitmap,
215    pub on_off_transition_time: u16,
216    pub on_transition_time: Nullable<u16>,
217    pub off_transition_time: Nullable<u16>,
218    pub default_move_rate: Nullable<u8>,
219}
220
221impl AttributeDefaults {
222    /// Creates an `AttributeDefaults` instance with default values.
223    ///
224    /// # Default Values
225    /// - `on_level`: `Nullable::none()` (not set)
226    /// - `options`: 0 (no options set)
227    /// - `on_off_transition_time`: 0 (no transition delay by default)
228    /// - `on_transition_time`: `Nullable::none()` (not set)
229    /// - `off_transition_time`: `Nullable::none()` (not set)
230    /// - `default_move_rate`: `Nullable::none()` (not set)
231    pub const fn new() -> Self {
232        Self {
233            on_level: Nullable::none(),
234            options: OptionsBitmap::from_bits(0).unwrap(),
235            on_off_transition_time: 0,
236            on_transition_time: Nullable::none(),
237            off_transition_time: Nullable::none(),
238            default_move_rate: Nullable::none(),
239        }
240    }
241}
242
243impl Default for AttributeDefaults {
244    fn default() -> Self {
245        Self::new()
246    }
247}
248
249impl<H: LevelControlHooks> LevelControlHandler<'_, H, NoOnOff> {
250    /// Creates a new `LevelControlHandler` with the given hooks which is **not** coupled to an OnOff cluster.
251    ///
252    /// NOTE: This constructor automatically calls `init` with no coupled `OnOff` handler.
253    ///
254    /// # Arguments
255    /// - `hooks` - A reference to the struct implementing the device-specific level control logic.
256    pub fn new_standalone(
257        dataver: Dataver,
258        endpoint_id: EndptId,
259        hooks: H,
260        attribute_defaults: AttributeDefaults,
261    ) -> Self {
262        let this = Self::new(dataver, endpoint_id, hooks, attribute_defaults);
263
264        this.init(None);
265
266        this
267    }
268}
269
270impl<'a, H: LevelControlHooks, OH: OnOffHooks> LevelControlHandler<'a, H, OH> {
271    const MAXIMUM_LEVEL: u8 = 254;
272
273    /// Creates a new `LevelControlHandler` with the given hooks.
274    ///
275    /// # Arguments
276    /// - `hooks` - A reference to the struct implementing the device-specific level control logic.
277    ///
278    /// # Usage
279    /// - Initialise and optionally couple with an OnOff handler via `init`.
280    pub fn new(
281        dataver: Dataver,
282        endpoint_id: EndptId,
283        hooks: H,
284        attribute_defaults: AttributeDefaults,
285    ) -> Self {
286        Self {
287            dataver,
288            endpoint_id,
289            hooks,
290            on_off_handler: Mutex::new(Cell::new(None)),
291            scene_invalidator: Mutex::new(Cell::new(None)),
292            state: Mutex::new(RefCell::new(LevelControlState::new(attribute_defaults))),
293            task_signal: Signal::new(None),
294        }
295    }
296
297    /// Attach a [`SceneInvalidator`] — typically the
298    /// [`crate::dm::clusters::scenes::ScenesState`] backing Scenes
299    /// Management on the same endpoint — so command-driven
300    /// `CurrentLevel` mutations flip `SceneValid → false` for any
301    /// recalled scene. No-op when unset.
302    pub fn with_scene_invalidator(self, invalidator: &'a dyn SceneInvalidator) -> Self {
303        self.scene_invalidator
304            .lock(|cell| cell.set(Some(invalidator)));
305        self
306    }
307
308    fn notify_scenable_changed(&self) {
309        if let Some(inv) = self.scene_invalidator.lock(|cell| cell.get()) {
310            inv.scenable_attribute_changed(self.endpoint_id);
311        }
312    }
313
314    /// Checks that the cluster is correctly configured, including required attributes, commands, and feature dependencies.
315    ///
316    /// # Panics
317    ///
318    /// panics with error message if the `state`'s `CLUSTER` is misconfigured.
319    fn validate(&self) {
320        if H::CLUSTER.revision != 6 {
321            panic!(
322                "LevelControl validation: incorrect version number: expected 6 got {}",
323                H::CLUSTER.revision
324            );
325        }
326
327        // Check for mandatory attributes
328        if H::CLUSTER
329            .attribute(AttributeId::CurrentLevel as _)
330            .is_none()
331            || H::CLUSTER.attribute(AttributeId::OnLevel as _).is_none()
332            || H::CLUSTER.attribute(AttributeId::Options as _).is_none()
333        {
334            panic!("LevelControl validation: missing required attributes: CurrentLevel, OnLevel, or Options");
335        }
336
337        // Check for mandatory commands
338        if H::CLUSTER.command(CommandId::MoveToLevel as _).is_none()
339            || H::CLUSTER.command(CommandId::Move as _).is_none()
340            || H::CLUSTER.command(CommandId::Step as _).is_none()
341            || H::CLUSTER.command(CommandId::Stop as _).is_none()
342            || H::CLUSTER
343                .command(CommandId::MoveToLevelWithOnOff as _)
344                .is_none()
345            || H::CLUSTER.command(CommandId::MoveWithOnOff as _).is_none()
346            || H::CLUSTER.command(CommandId::StepWithOnOff as _).is_none()
347            || H::CLUSTER.command(CommandId::StopWithOnOff as _).is_none()
348        {
349            panic!("LevelControl validation: missing required commands: MoveToLevel, Move, Step, Stop, MoveToLevelWithOnOff, MoveWithOnOff, StepWithOnOff or StopWithOnOff");
350        }
351
352        // If the ON_OFF feature in enabled, check that an OnOff cluster is coupled.
353        if H::CLUSTER.feature_map & level_control::Feature::ON_OFF.bits() != 0 {
354            // Ideally we should confirm that they are on the same endpoint.
355            if self.on_off_handler.lock(|h| h.get()).is_none() {
356                panic!("LevelControl validation: a reference to the OnOff cluster must be set when the ON_OFF feature is enabled");
357            }
358        }
359
360        if H::MAX_LEVEL > Self::MAXIMUM_LEVEL {
361            panic!(
362                "LevelControl validation: the MAX_LEVEL cannot be higher than {}",
363                Self::MAXIMUM_LEVEL
364            );
365        }
366
367        if H::CLUSTER.feature_map & level_control::Feature::LIGHTING.bits() != 0 {
368            // From the spec
369            // A value of 0x00 SHALL NOT be used.
370            // A value of 0x01 SHALL indicate the minimum level that can be attained on a device.
371            // A value of 0xFE SHALL indicate the maximum level that can be attained on a device.
372            if H::MIN_LEVEL == 0 {
373                panic!("LevelControl validation: MIN_LEVEL cannot be 0 when the LIGHTING feature is enabled");
374            }
375
376            // Check for required attributes when using this feature
377            if H::CLUSTER
378                .attribute(AttributeId::RemainingTime as _)
379                .is_none()
380                || H::CLUSTER
381                    .attribute(AttributeId::StartUpCurrentLevel as _)
382                    .is_none()
383            {
384                panic!("LevelControl validation: the RemainingTime and StartUpCurrentLevel attributes are required by the LIGHTING feature");
385            }
386        }
387    }
388
389    /// Initializes the cluster on startup;
390    /// - wire coupled handlers
391    /// - validate the handler setup with the configuration
392    /// - set the CurrentLevel attribute according to the StartUpCurrentLevel attribute.
393    ///
394    /// # Parameters
395    /// *on_off_handler: the OnOffHandler instance coupled with this LevelControlHandler, i.e. the OnOff cluster on the same endpoint. This should be set if the OnOff feature is set.
396    ///
397    /// # Panics
398    ///
399    /// panics if the `state`'s `CLUSTER` is misconfigured.
400    pub fn init(&self, on_off_handler: Option<&'a OnOffHandler<'a, OH, H>>) {
401        // 1.6.6.15. StartUpCurrentLevel Attribute
402        // This attribute SHALL indicate the desired startup level for a device when it is supplied with power
403        // and this level SHALL be reflected in the CurrentLevel attribute. The values of the
404        // StartUpCurrentLevel attribute are listed below:
405        // | Value        | Action on power up |
406        // |--------------| -------------------|
407        // | 0            | Set the CurrentLevel attribute to the minimum value permitted on the device |
408        // | null         | Set the CurrentLevel attribute to its previous value |
409        // | other values | Set the CurrentLevel attribute to this value |
410        // todo: Implement checking the reason for reboot.
411        // This behavior does not apply to reboots associated with OTA. After an OTA restart, the CurrentLevel
412        // attribute SHALL return to its value prior to the restart.
413
414        // Wire any coupled clusters
415        self.on_off_handler.lock(|h| h.set(on_off_handler));
416
417        self.validate();
418
419        // `self.hooks` holds the previous current level as supplied by the SDK consumer.
420        // Hence, if this process errors, we quietly abort resulting in the previous current level.
421        if let Ok(Some(startup_current_level)) = self.hooks.start_up_current_level() {
422            // The spec fails to mention the need for this bounding.
423            let level = if startup_current_level < H::MIN_LEVEL {
424                H::MIN_LEVEL
425            } else if startup_current_level > H::MAX_LEVEL {
426                H::MAX_LEVEL
427            } else {
428                startup_current_level
429            };
430
431            match self.hooks.set_device_level(level) {
432                Ok(current_level) => self.hooks.set_current_level(current_level),
433                Err(_) => error!("Failed to set Current Level to Start Up Current Level."),
434            }
435        }
436    }
437
438    /// Adapt the handler instance to the generic `rs-matter` `Handler` trait
439    pub const fn adapt(self) -> HandlerAsyncAdaptor<Self> {
440        HandlerAsyncAdaptor(self)
441    }
442
443    fn with_state<F, R>(&self, f: F) -> R
444    where
445        F: FnOnce(&mut LevelControlState) -> R,
446    {
447        self.state.lock(|state| {
448            let mut state = state.borrow_mut();
449
450            f(&mut state)
451        })
452    }
453
454    fn with_state_notify<F, R>(&self, ctx: impl WriteContext, f: F) -> R
455    where
456        F: FnOnce(&mut LevelControlState) -> R,
457    {
458        let result = self.with_state(f);
459
460        ctx.notify_changed();
461
462        result
463    }
464
465    /// Sets the CurrentLevel attribute.
466    /// If `set_device` is true, this method sets the level of the device, via the `set_level` hook.
467    /// This method calculates if a Matter notification is required according to the quiet reporting conditions described in the spec.
468    ///
469    /// # Arguments
470    /// - `level` - The new current level.
471    /// - `is_end_of_transition` - Indicates if this is the end of a transition.
472    /// - `set_device` - Indicates if the state of the physical device should be changed.
473    ///
474    /// # Returns
475    /// A tuple with the current level of the device, and a boolean signifying if a Matter notification is required.
476    fn set_level(
477        &self,
478        state: &mut LevelControlState,
479        level: u8,
480        is_end_of_transition: bool,
481        set_device: bool,
482        scene_apply: bool,
483    ) -> Result<(Option<u8>, bool), Error> {
484        // Store the previous current level before updating, for quiet reporting logic.
485        state.previous_current_level = self.hooks.current_level();
486        let current_level = match set_device {
487            true => self
488                .hooks
489                .set_device_level(level)
490                .map_err(|_| ErrorCode::Failure)?,
491            false => Some(level),
492        };
493        self.hooks.set_current_level(current_level);
494        // Scene-recall transitions move *toward* the recalled state,
495        // so they must not invalidate `SceneValid` on intermediate
496        // steps. Scenes restores the bit after `apply` returns.
497        if !scene_apply {
498            self.notify_scenable_changed();
499        }
500        let last_notification = Instant::now() - state.last_current_level_notification;
501
502        // CurrentLevel Quiet report conditions:
503        // - At most once per second, or
504        // - At the end of the movement/transition, or
505        // - When it changes from null to any other value and vice versa.
506        if last_notification.ge(&Duration::from_secs(1))
507            || is_end_of_transition
508            || state.previous_current_level.is_none()
509            || current_level.is_none()
510        {
511            state.last_current_level_notification = Instant::now();
512            return Ok((current_level, true));
513        }
514
515        Ok((current_level, false))
516    }
517
518    /// Checks if a command should proceed beyond the Options processing.
519    /// Returns true if execution of the command should continue, false otherwise.
520    //
521    // From the spec
522    // Command execution SHALL NOT continue beyond the Options processing if all of these criteria are true:
523    // - The command is one of the ‘without On/Off’ commands: Move, Move to Level, Step, or Stop.
524    // - The On/Off cluster exists on the same endpoint as this cluster.
525    // - The OnOff attribute of the On/Off cluster, on this endpoint, is FALSE.
526    // - The value of the ExecuteIfOff bit is 0.
527    fn should_continue(
528        &self,
529        with_on_off: bool,
530        options_mask: OptionsBitmap,
531        options_override: OptionsBitmap,
532    ) -> Result<bool, Error> {
533        if with_on_off {
534            return Ok(true);
535        }
536
537        let Some(on_off_handler) = self.on_off_handler.lock(|h| h.get()) else {
538            // This should be sufficient to satisfy "The On/Off cluster exists on the same endpoint as this cluster"
539            // if we can check the NODE configuration in validate.
540            return Ok(true);
541        };
542
543        if on_off_handler.on_off() {
544            return Ok(true);
545        }
546
547        // The OptionsMask and OptionsOverride fields SHALL both be present. Default values are provided
548        // to interpret missing fields from legacy devices. A temporary Options bitmap SHALL be created from
549        // the Options attribute, using the OptionsMask and OptionsOverride fields. Each bit of the temporary
550        // Options bitmap SHALL be determined as follows:
551        // Each bit in the Options attribute SHALL determine the corresponding bit in the temporary Options
552        // bitmap, unless the OptionsMask field is present and has the corresponding bit set to 1, in which
553        // case the corresponding bit in the OptionsOverride field SHALL determine the corresponding bit in
554        // the temporary Options bitmap.
555        if options_mask.contains(level_control::OptionsBitmap::EXECUTE_IF_OFF) {
556            return Ok(options_override.contains(level_control::OptionsBitmap::EXECUTE_IF_OFF));
557        }
558
559        // TODO: Think if the whole method should instead be executed when the state lock is held
560        Ok(self
561            .with_state(|state| state.options)
562            .contains(level_control::OptionsBitmap::EXECUTE_IF_OFF))
563    }
564
565    /// Handles asynchronous tasks for level transitions and moves.
566    async fn task_manager(&self, ctx: impl HandlerContext, task: Task) {
567        match task {
568            Task::MoveToLevel {
569                with_on_off,
570                target,
571                transition_time,
572                scene_apply,
573            } => {
574                if let Err(e) = self
575                    .move_to_level_transition(
576                        ctx,
577                        with_on_off,
578                        target,
579                        transition_time,
580                        scene_apply,
581                    )
582                    .await
583                {
584                    error!("Task::MoveToLevel: {:?}", e);
585                }
586            }
587            Task::Move {
588                with_on_off,
589                move_mode,
590                event_duration,
591            } => {
592                if let Err(e) = self
593                    .move_transition(ctx, with_on_off, move_mode, event_duration)
594                    .await
595                {
596                    error!("Task::Move: {:?}", e);
597                }
598            }
599            Task::Stop => (),
600            Task::OnOffStateChange { on } => {
601                if let Err(e) = self.handle_on_off_state_change(ctx, on).await {
602                    error!("Task::OnOffStateChange: {:?}", e);
603                }
604            }
605        }
606    }
607
608    /// This method is called by an OnOff cluster that is coupled with this LevelControl cluster.
609    /// This method updates the CurrentLevel of the device when the state of the OnOff cluster changes.
610    pub(crate) fn coupled_on_off_cluster_on_off_state_change(&self, on: bool) {
611        self.task_signal.signal(Task::OnOffStateChange { on });
612    }
613
614    // From the spec
615    // ## On
616    // Temporarily store CurrentLevel.
617    // Set CurrentLevel to the minimum level allowed for the device.
618    // Change CurrentLevel to OnLevel, or to the stored level if OnLevel is not defined, over the time period OnOffTransitionTime.
619    // ## off
620    // Temporarily store CurrentLevel.
621    // Change CurrentLevel to the minimum level allowed for the device over the time period OnOffTransitionTime.
622    // If OnLevel is not defined, set the CurrentLevel to the stored level.
623    async fn handle_on_off_state_change(
624        &self,
625        ctx: impl HandlerContext,
626        on: bool,
627    ) -> Result<(), Error> {
628        info!("handle_on_off_state_change");
629
630        let (target_level, transition_time, bitmap, temp_current_level) =
631            self.with_state(|state| {
632                let temp_current_level = self.hooks.current_level();
633
634                // use of unwrap is justified since this will option is always valid.
635                let bitmap = OptionsBitmap::from_bits(0).unwrap();
636
637                let mut transition_time = state.on_off_transition_time;
638
639                // 1.6.6.10. OnOffTransitionTime Attribute
640                // This attribute SHALL indicate the time taken to move to or from the target level when On or Off
641                // commands are received by an On/Off cluster on the same endpoint.
642                if on {
643                    // OnOff-coupling driven: user toggled OnOff and
644                    // LC follows. Not a scene apply.
645                    let (level, should_notify) =
646                        self.set_level(state, H::MIN_LEVEL, false, true, false)?;
647                    if should_notify {
648                        ctx.notify_attr_changed(
649                            self.endpoint_id,
650                            Self::CLUSTER.id,
651                            AttributeId::CurrentLevel as _,
652                        );
653                    }
654                    if level.is_none() {
655                        Err(ErrorCode::Failure)?;
656                    }
657
658                    let target_level = match state.on_level.as_opt_ref() {
659                        Some(on_level) => *on_level,
660                        None => temp_current_level.ok_or(ErrorCode::Failure)?,
661                    };
662
663                    // 1.6.6.12. OnTransitionTime Attribute
664                    // This attribute SHALL indicate the time taken to move the current level from the minimum level to
665                    // the maximum level when an On command is received by an On/Off cluster on the same endpoint.
666                    // If this attribute is not implemented, or contains a null value, the
667                    // OnOffTransitionTime SHALL be used instead.
668                    if let Some(tt) = state.on_transition_time.as_opt_ref() {
669                        transition_time = *tt;
670                    }
671
672                    Ok::<_, Error>((target_level, transition_time, bitmap, temp_current_level))
673                } else {
674                    // 1.6.6.13. OffTransitionTime Attribute
675                    // This attribute SHALL indicate the time taken to move the current level from the maximum level to
676                    // the minimum level when an Off command is received by an On/Off cluster on the same endpoint.
677                    // If this attribute is not implemented, or contains a null value, the
678                    // OnOffTransitionTime SHALL be used instead.
679                    if let Some(tt) = state.off_transition_time.as_opt_ref() {
680                        transition_time = *tt;
681                    }
682
683                    Ok((H::MIN_LEVEL, transition_time, bitmap, temp_current_level))
684                }
685            })?;
686
687        self.move_to_level_blocking(
688            &ctx,
689            true,
690            target_level,
691            Some(transition_time),
692            bitmap,
693            bitmap,
694        )
695        .await?;
696
697        if !on {
698            let restored = self.with_state(|state| {
699                if state.on_level.is_none() {
700                    self.hooks.set_current_level(temp_current_level);
701                    true
702                } else {
703                    false
704                }
705            });
706            if restored {
707                // CurrentLevel was restored to the pre-Off stored value
708                ctx.notify_attr_changed(
709                    self.endpoint_id,
710                    Self::CLUSTER.id,
711                    AttributeId::CurrentLevel as _,
712                );
713            }
714        }
715
716        Ok(())
717    }
718
719    /// Updates the OnOff attribute of the coupled OnOff cluster based on the current level and command type.
720    //
721    // From the spec
722    // When the level is reduced to its minimum the OnOff attribute is automatically turned to FALSE,
723    // and when the level is increased above its minimum the OnOff attribute is automatically turned to TRUE.
724    fn update_coupled_on_off(&self, current_level: u8, with_on_off: bool) -> Result<(), Error> {
725        // From the spec.
726        // There are two sets of commands provided in the Level Control cluster. These are identical, except
727        // that the first set (MoveToLevel, Move and Step commands) SHALL NOT affect the OnOff attribute,
728        // whereas the second set ('with On/Off' variants) SHALL.
729        if !with_on_off {
730            return Ok(());
731        }
732
733        let new_on_off_value = current_level > H::MIN_LEVEL;
734
735        // The `validate` method ensures that the on_off_handler is set if this function is called.
736        if let Some(on_off) = self.on_off_handler.lock(|h| h.get()) {
737            let current_on_off = on_off.on_off();
738            if current_on_off != new_on_off_value {
739                info!(
740                    "Updating the OnOff cluster with on_off = {}",
741                    new_on_off_value
742                );
743                on_off.coupled_cluster_set_on_off(new_on_off_value);
744            }
745        }
746
747        Ok(())
748    }
749
750    // Helper method performing initial validation for the move-to-level command.
751    // Used by move_to_level and move_to_level_blocking.
752    // Return true if processing should continue. False otherwise.
753    fn move_to_level_validation(
754        &self,
755        level: &mut u8,
756        with_on_off: bool,
757        options_mask: OptionsBitmap,
758        options_override: OptionsBitmap,
759    ) -> Result<bool, Error> {
760        if *level > Self::MAXIMUM_LEVEL {
761            return Err(ErrorCode::InvalidCommand.into());
762        }
763
764        if !self.should_continue(with_on_off, options_mask, options_override)? {
765            return Ok(false);
766        }
767
768        if *level > H::MAX_LEVEL {
769            *level = H::MAX_LEVEL;
770            debug!("target level > MAX_LEVEL. level set to MAX_LEVEL")
771        } else if *level < H::MIN_LEVEL {
772            *level = H::MIN_LEVEL;
773            debug!("target level < MIN_LEVEL. level set to MIN_LEVEL")
774        }
775
776        Ok(true)
777    }
778
779    /// Handles MoveToLevel commands, including validation, bounding, and transition logic.
780    /// Note: This will try to update the OnOff cluster's OnOff attribute at the start and end of the transition.
781    /// Note: If calling this from another Task, use the blocking version `move_to_level_blocking`, otherwise the calling Task will be halted.
782    ///
783    /// # Parameters
784    ///
785    /// * with_on_off: Is the LevelControl command calling this method one of the "WithOnOff" variant?
786    /// * level: The target level to move to.
787    /// * transition_time: The time for the transition in 1/10ts of a second.
788    /// * options_mask: The options mask in the command attributes.
789    /// * options_override: The options override in the command attributes.
790    fn move_to_level(
791        &self,
792        with_on_off: bool,
793        mut level: u8,
794        transition_time: Option<u16>,
795        options_mask: OptionsBitmap,
796        options_override: OptionsBitmap,
797        scene_apply: bool,
798    ) -> Result<(), Error> {
799        if let Ok(false) =
800            self.move_to_level_validation(&mut level, with_on_off, options_mask, options_override)
801        {
802            return Ok(());
803        }
804
805        info!(
806            "setting level to {} with transition time {:?}",
807            level, transition_time
808        );
809
810        // Stop any ongoing transitions and check if we happen to be where we need to be.
811        // If so, there is nothing to do.
812        self.task_signal.signal(Task::Stop);
813        if self.hooks.current_level() == Some(level) {
814            self.update_coupled_on_off(level, with_on_off)?;
815            return Ok(());
816        }
817
818        let t_time = transition_time.unwrap_or(0);
819
820        self.task_signal.signal(Task::MoveToLevel {
821            with_on_off,
822            target: level,
823            transition_time: t_time,
824            scene_apply,
825        });
826
827        Ok(())
828    }
829
830    // This version does not call Task::Stop. If we are called from another Task, we shouldn't stop it.
831    /// Handles MoveToLevel commands, including validation, bounding, and transition logic.
832    /// Note: This will try to update the OnOff cluster's OnOff attribute at the start and end of the transition.
833    /// Note: This will block until the transition completes.
834    ///
835    /// # Parameters
836    ///
837    /// * with_on_off: Is the LevelControl command calling this method one of the "WithOnOff" variant?
838    /// * level: The target level to move to.
839    /// * transition_time: The time for the transition in 1/10ts of a second.
840    /// * options_mask: The options mask in the command attributes.
841    /// * options_override: The options override in the command attributes.
842    async fn move_to_level_blocking(
843        &self,
844        ctx: impl HandlerContext,
845        with_on_off: bool,
846        mut level: u8,
847        transition_time: Option<u16>,
848        options_mask: OptionsBitmap,
849        options_override: OptionsBitmap,
850    ) -> Result<(), Error> {
851        if let Ok(false) =
852            self.move_to_level_validation(&mut level, with_on_off, options_mask, options_override)
853        {
854            return Ok(());
855        }
856
857        info!(
858            "setting level to {} with transition time {:?}",
859            level, transition_time
860        );
861
862        if self.hooks.current_level() == Some(level) {
863            self.update_coupled_on_off(level, with_on_off)?;
864            return Ok(());
865        }
866
867        let t_time = transition_time.unwrap_or(0);
868
869        // Command-driven only — never reached from scene apply.
870        self.move_to_level_transition(ctx, with_on_off, level, t_time, false)
871            .await?;
872
873        Ok(())
874    }
875
876    /// Asynchronously transitions the current level to a target level over a specified time.
877    /// Note: This will try to update the OnOff cluster's OnOff attribute at the start and end of the transition.
878    async fn move_to_level_transition(
879        &self,
880        ctx: impl HandlerContext,
881        with_on_off: bool,
882        target_level: u8,
883        transition_time: u16,
884        scene_apply: bool,
885    ) -> Result<(), Error> {
886        let event_start_time = Instant::now();
887
888        // Check if current_level is null. If so, return error.
889        let mut current_level = match self.hooks.current_level() {
890            Some(cl) => cl,
891            None => return Err(ErrorCode::Failure.into()),
892        };
893
894        let increasing = current_level < target_level;
895
896        let steps = target_level.abs_diff(current_level);
897
898        if steps == 0 {
899            return Ok(());
900        }
901
902        let mut remaining_time = Duration::from_millis(transition_time as u64 * 100);
903        let event_duration = Duration::from_millis_floor(remaining_time.as_millis() / steps as u64);
904
905        let startup_latency = Instant::now() - event_start_time;
906        loop {
907            let event_start_time = Instant::now();
908
909            if transition_time == 0 {
910                current_level = target_level;
911            } else {
912                match increasing {
913                    true => current_level += 1,
914                    false => current_level -= 1,
915                }
916            }
917
918            let is_transition_start = remaining_time.as_millis() == (transition_time as u64 * 100);
919            let is_transition_end = current_level == target_level;
920
921            debug!(
922                "move_to_level_transition: Setting current level: {}",
923                current_level
924            );
925            let (current_level, should_notify) = self.with_state(|state| {
926                self.set_level(state, current_level, is_transition_end, true, scene_apply)
927            })?;
928            let current_level = match current_level {
929                Some(level) => level,
930                None => return Err(ErrorCode::Failure.into()),
931            };
932
933            if is_transition_start || is_transition_end {
934                self.update_coupled_on_off(current_level, with_on_off)?;
935            }
936
937            if is_transition_end {
938                if should_notify
939                    || self.with_state(|state| {
940                        state.write_remaining_time_quietly(
941                            Duration::from_millis(0),
942                            is_transition_start,
943                        )
944                    })
945                {
946                    ctx.notify_attr_changed(
947                        self.endpoint_id,
948                        Self::CLUSTER.id,
949                        AttributeId::CurrentLevel as _,
950                    );
951                }
952                return Ok(());
953            }
954
955            match remaining_time > event_duration {
956                true => remaining_time -= event_duration,
957                false => {
958                    warn!("remaining time is 0 before level reached target");
959                    remaining_time = Duration::from_millis(0)
960                }
961            }
962
963            if should_notify
964                || self.with_state(|state| {
965                    state.write_remaining_time_quietly(remaining_time, is_transition_start)
966                })
967            {
968                ctx.notify_attr_changed(
969                    self.endpoint_id,
970                    Self::CLUSTER.id,
971                    AttributeId::CurrentLevel as _,
972                );
973            }
974
975            let latency = match is_transition_start {
976                false => embassy_time::Instant::now() - event_start_time,
977                true => (embassy_time::Instant::now() - event_start_time) + startup_latency,
978            };
979            match event_duration.checked_sub(latency) {
980                Some(wait_time) => embassy_time::Timer::after(wait_time).await,
981                None => warn!("no wait time. Consider dynamically adjusting the step size?"),
982            }
983        }
984    }
985
986    /// Handles Move commands, determining the rate and initiating transitions.
987    fn move_command(
988        &self,
989        state: &mut LevelControlState,
990        with_on_off: bool,
991        move_mode: MoveModeEnum,
992        rate: Option<u8>,
993        options_mask: OptionsBitmap,
994        options_override: OptionsBitmap,
995    ) -> Result<(), Error> {
996        // From the spec
997        //
998        // If the Rate field is null, then the value of the
999        // DefaultMoveRate attribute SHALL be used if that attribute is supported and its value is not null. If
1000        // the Rate field is null and the DefaultMoveRate attribute is either not supported or set to null, then
1001        // the device SHOULD move as fast as it is able.
1002        let rate = match rate {
1003            // Move at a rate of zero is no move at all. Immediately succeed without touching anything.
1004            Some(0) => return Ok(()),
1005            Some(val) => val,
1006            None => match state.default_move_rate.as_opt_ref() {
1007                Some(val) => *val,
1008                None => H::FASTEST_RATE,
1009            },
1010        };
1011
1012        // This will catch the case where H::FASTEST_RATE is 0.
1013        // The spec is not explicit about what should be done if this happens.
1014        // For now we error out if DefaultMoveRate is equal to 0 as this is invalid
1015        // until spec defines a behaviour.
1016        if rate == 0 {
1017            return Err(Error::new(ErrorCode::InvalidCommand));
1018        }
1019
1020        if !self.should_continue(with_on_off, options_mask, options_override)? {
1021            return Ok(());
1022        }
1023
1024        // Exit if we are already at the limit in the direct of movement.
1025        if let Some(current_level) = self.hooks.current_level() {
1026            if (current_level == H::MIN_LEVEL && move_mode == MoveModeEnum::Down)
1027                || (current_level == H::MAX_LEVEL && move_mode == MoveModeEnum::Up)
1028            {
1029                return Ok(());
1030            }
1031        }
1032
1033        let event_duration = Duration::from_hz(rate as u64);
1034
1035        info!("moving with rate {}", rate);
1036
1037        self.task_signal.signal(Task::Move {
1038            with_on_off,
1039            move_mode,
1040            event_duration,
1041        });
1042
1043        Ok(())
1044    }
1045
1046    /// Asynchronously moves the current level up or down at a specified rate.
1047    async fn move_transition(
1048        &self,
1049        ctx: impl HandlerContext,
1050        with_on_off: bool,
1051        move_mode: MoveModeEnum,
1052        event_duration: Duration,
1053    ) -> Result<(), Error> {
1054        loop {
1055            let event_start_time = Instant::now();
1056
1057            let current_level = match self.hooks.current_level() {
1058                Some(cl) => cl,
1059                None => return Err(ErrorCode::InvalidState.into()),
1060            };
1061
1062            let new_level = match move_mode {
1063                MoveModeEnum::Up => current_level.checked_add(1),
1064                MoveModeEnum::Down => current_level.checked_sub(1),
1065            };
1066
1067            let new_level = match new_level {
1068                Some(nl) => nl,
1069                None => return Ok(()),
1070            };
1071
1072            // If we start at min and go up, we need to update the onoff cluster immediately in case this method is halted.
1073            if current_level == H::MIN_LEVEL && new_level > H::MIN_LEVEL {
1074                self.update_coupled_on_off(new_level, with_on_off)?;
1075            }
1076
1077            let is_end_of_transition = (new_level == H::MAX_LEVEL) || (new_level == H::MIN_LEVEL);
1078
1079            // `Move` command path is command-driven only — no scene
1080            // recall queues a `Move` task.
1081            let (new_level, should_notify) = self.with_state(|state| {
1082                self.set_level(state, new_level, is_end_of_transition, true, false)
1083            })?;
1084            if should_notify {
1085                ctx.notify_attr_changed(
1086                    self.endpoint_id,
1087                    Self::CLUSTER.id,
1088                    AttributeId::CurrentLevel as _,
1089                );
1090            }
1091            let new_level = match new_level {
1092                Some(level) => level,
1093                None => return Err(ErrorCode::Failure.into()),
1094            };
1095
1096            if is_end_of_transition {
1097                self.update_coupled_on_off(new_level, with_on_off)?;
1098                return Ok(());
1099            }
1100
1101            let latency = embassy_time::Instant::now() - event_start_time;
1102            match event_duration.checked_sub(latency) {
1103                Some(wait_time) => embassy_time::Timer::after(wait_time).await,
1104                None => warn!("no wait time. Consider dynamically adjusting the step size?"),
1105            }
1106        }
1107    }
1108
1109    /// Handles Step commands, adjusting the level by a step size and managing transition time proportionally.
1110    fn step(
1111        &self,
1112        with_on_off: bool,
1113        step_mode: StepModeEnum,
1114        step_size: u8,
1115        transition_time: Option<u16>,
1116        options_mask: OptionsBitmap,
1117        options_override: OptionsBitmap,
1118    ) -> Result<(), Error> {
1119        // From the spec
1120        //
1121        // if the StepSize field has a value of zero, the command has no effect and
1122        // a response SHALL be returned with the status code set to INVALID_COMMAND.
1123        if step_size == 0 {
1124            return Err(ErrorCode::InvalidCommand.into());
1125        }
1126
1127        if !self.should_continue(with_on_off, options_mask, options_override)? {
1128            return Ok(());
1129        }
1130
1131        let current_level = match self.hooks.current_level() {
1132            Some(val) => val,
1133            None => return Err(ErrorCode::InvalidState.into()),
1134        };
1135
1136        let new_level = match step_mode {
1137            StepModeEnum::Up => current_level.saturating_add(step_size).min(H::MAX_LEVEL),
1138            StepModeEnum::Down => current_level.saturating_sub(step_size).max(H::MIN_LEVEL),
1139        };
1140
1141        // From the spec. Effect on Receipt
1142        // Increase/Decrease CurrentLevel by StepSize units, or until
1143        // it reaches the maximum/minimum level allowed for the
1144        // device if this reached in the process. In the latter
1145        // case, the transition time SHALL be
1146        // proportionally reduced.
1147        let transition_time = match transition_time {
1148            Some(val) => {
1149                if current_level.abs_diff(new_level) != step_size {
1150                    let new_step_size = current_level.abs_diff(new_level);
1151                    val.mul(new_step_size as u16).div_euclid(step_size as u16)
1152                } else {
1153                    val
1154                }
1155            }
1156            None => 0,
1157        };
1158
1159        // This will run some extra unnecessary checks, they will all pass, but benefits
1160        // of code reuse and a single source of truth for this logic outweigh the minor
1161        // performance cost of a few extra checks.
1162        self.move_to_level(
1163            with_on_off,
1164            new_level,
1165            Some(transition_time),
1166            options_mask,
1167            options_override,
1168            false,
1169        )
1170    }
1171
1172    /// Stops any ongoing transitions and resets the remaining time.
1173    fn stop(
1174        &self,
1175        ctx: impl HandlerContext,
1176        with_on_off: bool,
1177        options_mask: OptionsBitmap,
1178        options_override: OptionsBitmap,
1179    ) -> Result<(), Error> {
1180        if !self.should_continue(with_on_off, options_mask, options_override)? {
1181            return Ok(());
1182        }
1183        self.task_signal.signal(Task::Stop);
1184        if self
1185            .with_state(|state| state.write_remaining_time_quietly(Duration::from_millis(0), false))
1186        {
1187            ctx.notify_attr_changed(
1188                self.endpoint_id,
1189                Self::CLUSTER.id,
1190                AttributeId::RemainingTime as _,
1191            );
1192        }
1193
1194        Ok(())
1195    }
1196
1197    fn handle_out_of_band_message(&self, ctx: impl HandlerContext, message: OutOfBandMessage) {
1198        self.with_state(|state| {
1199            match message {
1200                OutOfBandMessage::Update(current_level) => {
1201                    self.task_signal.signal(Task::Stop);
1202
1203                    // OOB "device level changed under us" — genuine
1204                    // drift, never a scene apply.
1205                    match self.set_level(state, current_level, true, false, false) {
1206                        Ok((_, should_notify)) => {
1207                            if should_notify
1208                                || state.write_remaining_time_quietly(Duration::from_millis(0), false)
1209                            {
1210                                ctx.notify_attr_changed(
1211                                    self.endpoint_id,
1212                                    Self::CLUSTER.id,
1213                                    AttributeId::CurrentLevel as _,
1214                                );
1215                            }
1216                        }
1217                        Err(e) => {
1218                            error!("OutOfBandMessage::Update failed: set_level failed unexpectedly with set_device == false: {}", e);
1219                        }
1220                    }
1221                }
1222                OutOfBandMessage::MoveToLevel {
1223                    with_on_off,
1224                    level,
1225                    transition_time,
1226                    options_mask,
1227                    options_override,
1228                } => {
1229                    if let Err(e) = self.move_to_level(
1230                        with_on_off,
1231                        level,
1232                        transition_time,
1233                        options_mask,
1234                        options_override,
1235                        false,
1236                    ) {
1237                        error!(
1238                            "Device initiated MoveToLevel failed: {} | with_on_off: {}, level: {}, transition_time: {:?}, options_mask: {:?}, options_override: {:?}",
1239                            e, with_on_off, level, transition_time, options_mask, options_override
1240                        );
1241                    }
1242                }
1243                OutOfBandMessage::Move {
1244                    with_on_off,
1245                    move_mode,
1246                    rate,
1247                    options_mask,
1248                    options_override,
1249                } => {
1250                    if let Err(e) =
1251                        self.move_command(state, with_on_off, move_mode, rate, options_mask, options_override)
1252                    {
1253                        error!(
1254                            "Device initiated Move failed: {} | with_on_off: {}, move_mode: {:?}, rate: {:?}, options_mask: {:?}, options_override: {:?}",
1255                            e, with_on_off, move_mode, rate, options_mask, options_override
1256                        );
1257                    }
1258                }
1259                OutOfBandMessage::Step {
1260                    with_on_off,
1261                    step_mode,
1262                    step_size,
1263                    transition_time,
1264                    options_mask,
1265                    options_override,
1266                } => {
1267                    if let Err(e) = self.step(
1268                        with_on_off,
1269                        step_mode,
1270                        step_size,
1271                        transition_time,
1272                        options_mask,
1273                        options_override,
1274                    ) {
1275                        error!(
1276                            "Device initiated Step failed: {} | with_on_off: {}, step_mode: {:?}, step_size: {}, transition_time: {:?}, options_mask: {:?}, options_override: {:?}",
1277                            e, with_on_off, step_mode, step_size, transition_time, options_mask, options_override
1278                        );
1279                    }
1280                }
1281                OutOfBandMessage::Stop => {
1282                    self.task_signal.signal(Task::Stop);
1283                    if state.write_remaining_time_quietly(Duration::from_millis(0), false) {
1284                        ctx.notify_attr_changed(
1285                            self.endpoint_id,
1286                            Self::CLUSTER.id,
1287                            AttributeId::RemainingTime as _,
1288                        );
1289                    }
1290                }
1291            }
1292        })
1293    }
1294}
1295
1296impl<H: LevelControlHooks, OH: OnOffHooks> ClusterAsyncHandler for LevelControlHandler<'_, H, OH> {
1297    const CLUSTER: Cluster<'static> = H::CLUSTER;
1298
1299    // Runs an async task manager for the cluster handler.
1300    async fn run(&self, ctx: impl HandlerContext) -> Result<(), Error> {
1301        let mut hooks_fut = pin!(self
1302            .hooks
1303            .run(|message| self.handle_out_of_band_message(&ctx, message)));
1304
1305        loop {
1306            let mut task = match select(
1307                &mut hooks_fut,
1308                self.task_signal.wait_signalled(),
1309            ).await {
1310                Either::First(_) => panic!("LevelControlHooks::run returned; implementers MUST not return. Implementations should loop forever or await core::future::pending::<()>()."),
1311                Either::Second(task) => task,
1312            };
1313
1314            loop {
1315                match select3(
1316                    &mut hooks_fut,
1317                    self.task_manager(&ctx, task),
1318                    self.task_signal.wait_signalled(),
1319                )
1320                .await
1321                {
1322                    Either3::First(_) => panic!("LevelControlHooks::run returned; implementers MUST not return. Implementations should loop forever or await core::future::pending::<()>()."),
1323                    Either3::Second(_) => break,
1324                    Either3::Third(new_task) => task = new_task,
1325                };
1326            }
1327        }
1328    }
1329
1330    fn dataver(&self) -> u32 {
1331        self.dataver.get()
1332    }
1333
1334    fn dataver_changed(&self) {
1335        self.dataver.changed();
1336    }
1337
1338    fn current_level(
1339        &self,
1340        _ctx: impl ReadContext,
1341    ) -> impl Future<Output = Result<Nullable<u8>, Error>> {
1342        ready(match self.hooks.current_level() {
1343            Some(level) => Ok(Nullable::some(level)),
1344            None => Ok(Nullable::none()),
1345        })
1346    }
1347
1348    fn on_level(
1349        &self,
1350        _ctx: impl ReadContext,
1351    ) -> impl Future<Output = Result<Nullable<u8>, Error>> {
1352        ready(Ok(self.with_state(|state| state.on_level.clone())))
1353    }
1354
1355    fn set_on_level(
1356        &self,
1357        ctx: impl WriteContext,
1358        value: Nullable<u8>,
1359    ) -> impl Future<Output = Result<(), Error>> {
1360        ready('a: {
1361            if let Some(level) = value.clone().into_option() {
1362                if level > H::MAX_LEVEL || level < H::MIN_LEVEL {
1363                    break 'a Err(ErrorCode::ConstraintError.into());
1364                }
1365            }
1366
1367            self.with_state_notify(ctx, |state| {
1368                state.on_level = value;
1369            });
1370
1371            Ok(())
1372        })
1373    }
1374
1375    fn options(
1376        &self,
1377        _ctx: impl ReadContext,
1378    ) -> impl Future<Output = Result<OptionsBitmap, Error>> {
1379        ready(Ok(self.with_state(|state| state.options)))
1380    }
1381
1382    fn set_options(
1383        &self,
1384        ctx: impl WriteContext,
1385        value: OptionsBitmap,
1386    ) -> impl Future<Output = Result<(), Error>> {
1387        ready({
1388            self.with_state_notify(ctx, |state| {
1389                state.options = value;
1390            });
1391
1392            Ok(())
1393        })
1394    }
1395
1396    fn remaining_time(&self, _ctx: impl ReadContext) -> impl Future<Output = Result<u16, Error>> {
1397        ready(Ok(self.with_state(|state| state.remaining_time)))
1398    }
1399
1400    fn max_level(&self, _ctx: impl ReadContext) -> impl Future<Output = Result<u8, Error>> {
1401        ready(Ok(H::MAX_LEVEL))
1402    }
1403
1404    fn min_level(&self, _ctx: impl ReadContext) -> impl Future<Output = Result<u8, Error>> {
1405        ready(Ok(H::MIN_LEVEL))
1406    }
1407
1408    fn on_off_transition_time(
1409        &self,
1410        _ctx: impl ReadContext,
1411    ) -> impl Future<Output = Result<u16, Error>> {
1412        ready(Ok(self.with_state(|state| state.on_off_transition_time)))
1413    }
1414
1415    fn set_on_off_transition_time(
1416        &self,
1417        ctx: impl WriteContext,
1418        value: u16,
1419    ) -> impl Future<Output = Result<(), Error>> {
1420        ready({
1421            self.with_state_notify(ctx, |state| {
1422                state.on_off_transition_time = value;
1423            });
1424
1425            Ok(())
1426        })
1427    }
1428
1429    fn on_transition_time(
1430        &self,
1431        _ctx: impl ReadContext,
1432    ) -> impl Future<Output = Result<Nullable<u16>, Error>> {
1433        ready(Ok(self.with_state(|state| state.on_transition_time.clone())))
1434    }
1435
1436    fn set_on_transition_time(
1437        &self,
1438        ctx: impl WriteContext,
1439        value: Nullable<u16>,
1440    ) -> impl Future<Output = Result<(), Error>> {
1441        ready({
1442            self.with_state_notify(ctx, |state| {
1443                state.on_transition_time = value;
1444            });
1445
1446            Ok(())
1447        })
1448    }
1449
1450    fn off_transition_time(
1451        &self,
1452        _ctx: impl ReadContext,
1453    ) -> impl Future<Output = Result<Nullable<u16>, Error>> {
1454        ready(Ok(
1455            self.with_state(|state| state.off_transition_time.clone())
1456        ))
1457    }
1458
1459    fn set_off_transition_time(
1460        &self,
1461        ctx: impl WriteContext,
1462        value: Nullable<u16>,
1463    ) -> impl Future<Output = Result<(), Error>> {
1464        ready({
1465            self.with_state_notify(ctx, |state| {
1466                state.off_transition_time = value;
1467            });
1468
1469            Ok(())
1470        })
1471    }
1472
1473    fn default_move_rate(
1474        &self,
1475        _ctx: impl ReadContext,
1476    ) -> impl Future<Output = Result<Nullable<u8>, Error>> {
1477        ready(Ok(self.with_state(|state| state.default_move_rate.clone())))
1478    }
1479
1480    fn set_default_move_rate(
1481        &self,
1482        ctx: impl WriteContext,
1483        value: Nullable<u8>,
1484    ) -> impl Future<Output = Result<(), Error>> {
1485        ready('a: {
1486            // The spec is not explicit about what should be done if this happens.
1487            // For now we error out if DefaultMoveRate is equal to 0 as this is invalid
1488            // until spec defines a behaviour.
1489            if Some(0) == value.clone().into_option() {
1490                break 'a Err(ErrorCode::InvalidData.into());
1491            }
1492
1493            self.with_state_notify(ctx, |state| {
1494                state.default_move_rate = value;
1495            });
1496
1497            Ok(())
1498        })
1499    }
1500
1501    fn start_up_current_level(
1502        &self,
1503        _ctx: impl ReadContext,
1504    ) -> impl Future<Output = Result<Nullable<u8>, Error>> {
1505        ready(match self.hooks.start_up_current_level() {
1506            Ok(Some(val)) => Ok(Nullable::some(val)),
1507            Ok(None) => Ok(Nullable::none()),
1508            Err(e) => Err(e),
1509        })
1510    }
1511
1512    fn set_start_up_current_level(
1513        &self,
1514        ctx: impl WriteContext,
1515        value: Nullable<u8>,
1516    ) -> impl Future<Output = Result<(), Error>> {
1517        ready('a: {
1518            // According to the current spec, this attribute does not have any constraints at this stage.
1519            // However, it's usage is bounded by min/max hence it makes sense to restrict the settable values to this range.
1520            if let Some(level) = value.clone().into_option() {
1521                if level > H::MAX_LEVEL || level < H::MIN_LEVEL {
1522                    break 'a Err(ErrorCode::ConstraintError.into());
1523                }
1524            }
1525
1526            match self.hooks.set_start_up_current_level(value.into_option()) {
1527                Ok(()) => {
1528                    ctx.notify_changed();
1529                    Ok(())
1530                }
1531                Err(e) => Err(e),
1532            }
1533        })
1534    }
1535
1536    fn handle_move_to_level(
1537        &self,
1538        _ctx: impl InvokeContext,
1539        request: MoveToLevelRequest<'_>,
1540    ) -> impl Future<Output = Result<(), Error>> {
1541        ready('a: {
1542            let level = match request.level() {
1543                Ok(v) => v,
1544                Err(e) => break 'a Err(e),
1545            };
1546            let transition_time = match request.transition_time() {
1547                Ok(v) => v.into_option(),
1548                Err(e) => break 'a Err(e),
1549            };
1550            let options_mask = match request.options_mask() {
1551                Ok(v) => v,
1552                Err(e) => break 'a Err(e),
1553            };
1554            let options_override = match request.options_override() {
1555                Ok(v) => v,
1556                Err(e) => break 'a Err(e),
1557            };
1558            self.move_to_level(
1559                false,
1560                level,
1561                transition_time,
1562                options_mask,
1563                options_override,
1564                false,
1565            )
1566        })
1567    }
1568
1569    fn handle_move(
1570        &self,
1571        _ctx: impl InvokeContext,
1572        request: MoveRequest<'_>,
1573    ) -> impl Future<Output = Result<(), Error>> {
1574        ready(self.with_state(|state| {
1575            self.move_command(
1576                state,
1577                false,
1578                request.move_mode()?,
1579                request.rate()?.into_option(),
1580                request.options_mask()?,
1581                request.options_override()?,
1582            )
1583        }))
1584    }
1585
1586    fn handle_step(
1587        &self,
1588        _ctx: impl InvokeContext,
1589        request: StepRequest<'_>,
1590    ) -> impl Future<Output = Result<(), Error>> {
1591        ready('a: {
1592            let step_mode = match request.step_mode() {
1593                Ok(v) => v,
1594                Err(e) => break 'a Err(e),
1595            };
1596            let step_size = match request.step_size() {
1597                Ok(v) => v,
1598                Err(e) => break 'a Err(e),
1599            };
1600            let transition_time = match request.transition_time() {
1601                Ok(v) => v.into_option(),
1602                Err(e) => break 'a Err(e),
1603            };
1604            let options_mask = match request.options_mask() {
1605                Ok(v) => v,
1606                Err(e) => break 'a Err(e),
1607            };
1608            let options_override = match request.options_override() {
1609                Ok(v) => v,
1610                Err(e) => break 'a Err(e),
1611            };
1612            self.step(
1613                false,
1614                step_mode,
1615                step_size,
1616                transition_time,
1617                options_mask,
1618                options_override,
1619            )
1620        })
1621    }
1622
1623    fn handle_stop(
1624        &self,
1625        ctx: impl InvokeContext,
1626        request: StopRequest<'_>,
1627    ) -> impl Future<Output = Result<(), Error>> {
1628        ready('a: {
1629            let options_mask = match request.options_mask() {
1630                Ok(v) => v,
1631                Err(e) => break 'a Err(e),
1632            };
1633            let options_override = match request.options_override() {
1634                Ok(v) => v,
1635                Err(e) => break 'a Err(e),
1636            };
1637            self.stop(&ctx, false, options_mask, options_override)
1638        })
1639    }
1640
1641    fn handle_move_to_level_with_on_off(
1642        &self,
1643        _ctx: impl InvokeContext,
1644        request: MoveToLevelWithOnOffRequest<'_>,
1645    ) -> impl Future<Output = Result<(), Error>> {
1646        ready('a: {
1647            let level = match request.level() {
1648                Ok(v) => v,
1649                Err(e) => break 'a Err(e),
1650            };
1651            let transition_time = match request.transition_time() {
1652                Ok(v) => v.into_option(),
1653                Err(e) => break 'a Err(e),
1654            };
1655            let options_mask = match request.options_mask() {
1656                Ok(v) => v,
1657                Err(e) => break 'a Err(e),
1658            };
1659            let options_override = match request.options_override() {
1660                Ok(v) => v,
1661                Err(e) => break 'a Err(e),
1662            };
1663            self.move_to_level(
1664                true,
1665                level,
1666                transition_time,
1667                options_mask,
1668                options_override,
1669                false,
1670            )
1671        })
1672    }
1673
1674    fn handle_move_with_on_off(
1675        &self,
1676        _ctx: impl InvokeContext,
1677        request: MoveWithOnOffRequest<'_>,
1678    ) -> impl Future<Output = Result<(), Error>> {
1679        ready(self.with_state(|state| {
1680            self.move_command(
1681                state,
1682                true,
1683                request.move_mode()?,
1684                request.rate()?.into_option(),
1685                request.options_mask()?,
1686                request.options_override()?,
1687            )
1688        }))
1689    }
1690
1691    fn handle_step_with_on_off(
1692        &self,
1693        _ctx: impl InvokeContext,
1694        request: StepWithOnOffRequest<'_>,
1695    ) -> impl Future<Output = Result<(), Error>> {
1696        ready('a: {
1697            let step_mode = match request.step_mode() {
1698                Ok(v) => v,
1699                Err(e) => break 'a Err(e),
1700            };
1701            let step_size = match request.step_size() {
1702                Ok(v) => v,
1703                Err(e) => break 'a Err(e),
1704            };
1705            let transition_time = match request.transition_time() {
1706                Ok(v) => v.into_option(),
1707                Err(e) => break 'a Err(e),
1708            };
1709            let options_mask = match request.options_mask() {
1710                Ok(v) => v,
1711                Err(e) => break 'a Err(e),
1712            };
1713            let options_override = match request.options_override() {
1714                Ok(v) => v,
1715                Err(e) => break 'a Err(e),
1716            };
1717            self.step(
1718                true,
1719                step_mode,
1720                step_size,
1721                transition_time,
1722                options_mask,
1723                options_override,
1724            )
1725        })
1726    }
1727
1728    fn handle_stop_with_on_off(
1729        &self,
1730        ctx: impl InvokeContext,
1731        request: StopWithOnOffRequest<'_>,
1732    ) -> impl Future<Output = Result<(), Error>> {
1733        ready('a: {
1734            let options_mask = match request.options_mask() {
1735                Ok(v) => v,
1736                Err(e) => break 'a Err(e),
1737            };
1738            let options_override = match request.options_override() {
1739                Ok(v) => v,
1740                Err(e) => break 'a Err(e),
1741            };
1742            self.stop(&ctx, true, options_mask, options_override)
1743        })
1744    }
1745
1746    fn handle_move_to_closest_frequency(
1747        &self,
1748        _ctx: impl InvokeContext,
1749        _request: MoveToClosestFrequencyRequest<'_>,
1750    ) -> impl Future<Output = Result<(), Error>> {
1751        ready(Err(ErrorCode::InvalidCommand.into()))
1752    }
1753}
1754
1755pub trait LevelControlHooks {
1756    const MIN_LEVEL: u8;
1757    const MAX_LEVEL: u8;
1758    const FASTEST_RATE: u8;
1759    const CLUSTER: Cluster<'static>;
1760
1761    /// Implements the business logic for setting the level of the device.
1762    /// Returns the level the device was set to.
1763    /// If this method returns Err, the `LevelControlHandler` will represent this as an error with `ImStatusCode` of `Failure`.
1764    /// Note: The above is the only responsibility of this method. There is no need to update Matter attributes.
1765    #[allow(clippy::result_unit_err)]
1766    fn set_device_level(&self, level: u8) -> Result<Option<u8>, ()>;
1767
1768    // Raw accessors
1769    //  These methods should not perform any checks.
1770    //  They should simply get or set values.
1771    //  They should not error.
1772
1773    /// Raw current_level getter.
1774    /// This value should persist across reboots.
1775    fn current_level(&self) -> Option<u8>;
1776
1777    /// Raw current_level setter.
1778    /// This value should persist across reboots.
1779    fn set_current_level(&self, level: Option<u8>);
1780
1781    /// Raw start_up_current_level getter.
1782    /// This value should persist across reboots.
1783    fn start_up_current_level(&self) -> Result<Option<u8>, Error> {
1784        Err(ErrorCode::AttributeNotFound.into())
1785    }
1786    /// Raw start_up_current_level setter.
1787    /// This value should persist across reboots.
1788    fn set_start_up_current_level(&self, _value: Option<u8>) -> Result<(), Error> {
1789        Err(ErrorCode::AttributeNotFound.into())
1790    }
1791
1792    /// Background task for out-of-band notifications to the handler.
1793    ///
1794    /// This future MUST NOT return. Implementers should either loop forever or await
1795    /// core::future::pending::<()>(), so the SDK's task does not observe a completed future.
1796    ///
1797    /// # Panics
1798    /// The SDK will panic if this method returns.
1799    fn run<F: Fn(OutOfBandMessage)>(&self, _notify: F) -> impl Future<Output = ()> {
1800        pending::<()>()
1801    }
1802}
1803
1804impl<T> LevelControlHooks for &T
1805where
1806    T: LevelControlHooks,
1807{
1808    const MIN_LEVEL: u8 = T::MIN_LEVEL;
1809    const MAX_LEVEL: u8 = T::MAX_LEVEL;
1810    const FASTEST_RATE: u8 = T::FASTEST_RATE;
1811    const CLUSTER: Cluster<'static> = T::CLUSTER;
1812
1813    fn set_device_level(&self, level: u8) -> Result<Option<u8>, ()> {
1814        (*self).set_device_level(level)
1815    }
1816
1817    fn current_level(&self) -> Option<u8> {
1818        (*self).current_level()
1819    }
1820
1821    fn set_current_level(&self, level: Option<u8>) {
1822        (*self).set_current_level(level)
1823    }
1824
1825    fn start_up_current_level(&self) -> Result<Option<u8>, Error> {
1826        (*self).start_up_current_level()
1827    }
1828
1829    fn set_start_up_current_level(&self, value: Option<u8>) -> Result<(), Error> {
1830        (*self).set_start_up_current_level(value)
1831    }
1832
1833    fn run<F: Fn(OutOfBandMessage)>(&self, notify: F) -> impl Future<Output = ()> {
1834        (*self).run(notify)
1835    }
1836}
1837
1838/// This is a phantom type for when the LevelControl cluster is not coupled with an OnOff cluster.
1839/// This type should only be used for annotations and not for actual OnOff functionality.
1840/// All methods will panic.
1841pub struct NoOnOff;
1842
1843impl OnOffHooks for NoOnOff {
1844    const CLUSTER: Cluster<'static> = ON_OFF_FULL_CLUSTER;
1845
1846    fn on_off(&self) -> bool {
1847        panic!("NoOnOff: on_off called unexpectedly - this phantom type should not be used for OnOff functionality")
1848    }
1849
1850    fn set_on_off(&self, _on: bool) {
1851        panic!("NoOnOff: set_on_off called unexpectedly - this phantom type should not be used for OnOff functionality")
1852    }
1853
1854    fn start_up_on_off(&self) -> Nullable<super::on_off::StartUpOnOffEnum> {
1855        panic!("NoOnOff: start_up_on_off called unexpectedly - this phantom type should not be used for OnOff functionality")
1856    }
1857
1858    fn set_start_up_on_off(
1859        &self,
1860        _value: Nullable<super::on_off::StartUpOnOffEnum>,
1861    ) -> Result<(), Error> {
1862        panic!("NoOnOff: set_start_up_on_off called unexpectedly - this method should not be called when LevelControl is not coupled with OnOff")
1863    }
1864
1865    async fn handle_off_with_effect(&self, _effect: super::on_off::EffectVariantEnum) {
1866        panic!("NoOnOff: handle_off_with_effect called unexpectedly - this phantom type should not be used for OnOff functionality")
1867    }
1868}
1869
1870/// Scenes Management integration for the LevelControl cluster. The
1871/// only scenable attribute is `CurrentLevel`; apply routes through
1872/// `MoveToLevel` with the scene's transition time.
1873impl<H, OH> SceneClusterHandler for LevelControlHandler<'_, H, OH>
1874where
1875    H: LevelControlHooks,
1876    OH: OnOffHooks,
1877{
1878    const CLUSTER_ID: ClusterId = FULL_CLUSTER.id;
1879
1880    fn endpoint_id(&self) -> EndptId {
1881        self.endpoint_id
1882    }
1883
1884    fn is_scenable_attribute(attribute_id: AttrId) -> bool {
1885        attribute_id == AttributeId::CurrentLevel as AttrId
1886    }
1887
1888    fn capture<P: TLVBuilderParent>(
1889        &self,
1890        avp_array: AttributeValuePairStructArrayBuilder<P>,
1891    ) -> Result<AttributeValuePairStructArrayBuilder<P>, Error> {
1892        // `CurrentLevel` is nullable; null → skip the AVP entry.
1893        if let Some(level) = self.hooks.current_level() {
1894            avp_array.push_u8(AttributeId::CurrentLevel as _, level)
1895        } else {
1896            Ok(avp_array)
1897        }
1898    }
1899
1900    async fn apply<C: HandlerContext>(
1901        &self,
1902        _ctx: &C,
1903        avp_list: &TLVArray<'_, AttributeValuePairStruct<'_>>,
1904        transition_time_ms: u32,
1905    ) -> Result<(), Error> {
1906        for avp in avp_list.iter() {
1907            let avp = avp?;
1908            if avp.attribute_id()? != AttributeId::CurrentLevel as _ {
1909                continue;
1910            }
1911            let Some(level) = avp.value_unsigned_8()? else {
1912                continue;
1913            };
1914            // Reuse the command-driven `MoveToLevel` pipeline with
1915            // `scene_apply=true` (suppresses `SceneValid` drift on
1916            // every step) and `with_on_off=false` (OnOff lands its own
1917            // AVP via `OnOffHandler::apply`). RecallScene transition
1918            // time is ms; MoveToLevel is deciseconds — convert with
1919            // saturation.
1920            let transition_ds = (transition_time_ms / 100).min(u16::MAX as u32) as u16;
1921            return self.move_to_level(
1922                false,
1923                level,
1924                Some(transition_ds),
1925                OptionsBitmap::empty(),
1926                OptionsBitmap::empty(),
1927                true,
1928            );
1929        }
1930        Ok(())
1931    }
1932}
1933
1934pub mod test {
1935    use crate::dm::clusters::app::level_control::{
1936        AttributeId, CommandId, Feature, LevelControlHooks, FULL_CLUSTER,
1937    };
1938    use crate::dm::Cluster;
1939    use crate::error::Error;
1940    use crate::utils::cell::RefCell;
1941    use crate::utils::sync::blocking::Mutex;
1942    use crate::with;
1943
1944    struct TestLevelControlState {
1945        current_level: Option<u8>,
1946        start_up_current_level: Option<u8>,
1947    }
1948
1949    impl TestLevelControlState {
1950        const fn new() -> Self {
1951            Self {
1952                current_level: Some(1),
1953                start_up_current_level: None,
1954            }
1955        }
1956    }
1957
1958    pub struct TestLevelControlDeviceLogic {
1959        state: Mutex<RefCell<TestLevelControlState>>,
1960    }
1961
1962    impl TestLevelControlDeviceLogic {
1963        pub const fn new() -> Self {
1964            Self {
1965                state: Mutex::new(RefCell::new(TestLevelControlState::new())),
1966            }
1967        }
1968    }
1969
1970    impl Default for TestLevelControlDeviceLogic {
1971        fn default() -> Self {
1972            Self::new()
1973        }
1974    }
1975
1976    impl LevelControlHooks for TestLevelControlDeviceLogic {
1977        const MIN_LEVEL: u8 = 1;
1978        const MAX_LEVEL: u8 = 254;
1979        const FASTEST_RATE: u8 = 50;
1980        const CLUSTER: Cluster<'static> = FULL_CLUSTER
1981            .with_revision(6)
1982            .with_features(Feature::ON_OFF.bits())
1983            .with_attrs(with!(
1984                required;
1985                AttributeId::CurrentLevel
1986                | AttributeId::MinLevel
1987                | AttributeId::MaxLevel
1988                | AttributeId::OnLevel
1989                | AttributeId::Options
1990            ))
1991            .with_cmds(with!(
1992                CommandId::MoveToLevel
1993                    | CommandId::Move
1994                    | CommandId::Step
1995                    | CommandId::Stop
1996                    | CommandId::MoveToLevelWithOnOff
1997                    | CommandId::MoveWithOnOff
1998                    | CommandId::StepWithOnOff
1999                    | CommandId::StopWithOnOff
2000            ));
2001
2002        fn set_device_level(&self, level: u8) -> Result<Option<u8>, ()> {
2003            // This is where business logic is implemented to physically change the level of the device.
2004            Ok(Some(level))
2005        }
2006
2007        fn current_level(&self) -> Option<u8> {
2008            self.state.lock(|state| state.borrow().current_level)
2009        }
2010
2011        fn set_current_level(&self, level: Option<u8>) {
2012            info!(
2013                "LevelControlDeviceLogic::set_current_level: setting level to {:?}",
2014                level
2015            );
2016            self.state
2017                .lock(|state| state.borrow_mut().current_level = level);
2018        }
2019
2020        fn start_up_current_level(&self) -> Result<Option<u8>, Error> {
2021            Ok(self
2022                .state
2023                .lock(|state| state.borrow().start_up_current_level))
2024        }
2025
2026        fn set_start_up_current_level(&self, value: Option<u8>) -> Result<(), Error> {
2027            self.state
2028                .lock(|state| state.borrow_mut().start_up_current_level = value);
2029            Ok(())
2030        }
2031    }
2032}
2033
2034#[cfg(test)]
2035mod tests {
2036    use super::test::TestLevelControlDeviceLogic;
2037    use super::{AttributeDefaults, LevelControlHandler};
2038    use crate::dm::clusters::app::on_off::test::TestOnOffDeviceLogic;
2039    use crate::dm::clusters::app::on_off::OnOffHandler;
2040    use crate::dm::Dataver;
2041
2042    /// Catches drift between `TestLevelControlDeviceLogic::CLUSTER` and
2043    /// `LevelControlHandler::validate()`.
2044    #[test]
2045    fn test_logic_passes_handler_validate() {
2046        let level_logic = TestLevelControlDeviceLogic::new();
2047        let on_off_logic = TestOnOffDeviceLogic::new(false);
2048        let level = LevelControlHandler::new(
2049            Dataver::new(1),
2050            1,
2051            &level_logic,
2052            AttributeDefaults::default(),
2053        );
2054        let on_off = OnOffHandler::new(Dataver::new(2), 1, &on_off_logic);
2055        on_off.init(Some(&level));
2056        level.init(Some(&on_off));
2057    }
2058}