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                // Always run the quiet write - it stores the new value as a
939                // side effect, so short-circuiting it on `should_notify`
940                // would leave `RemainingTime` stale.
941                let remaining_time_reportable = self.with_state(|state| {
942                    state
943                        .write_remaining_time_quietly(Duration::from_millis(0), is_transition_start)
944                });
945
946                if should_notify {
947                    ctx.notify_attr_changed(
948                        self.endpoint_id,
949                        Self::CLUSTER.id,
950                        AttributeId::CurrentLevel as _,
951                    );
952                }
953
954                // `RemainingTime` is Q-quality: reported only when the quiet
955                // write says so (App Cluster spec), and against its OWN
956                // attribute id - not `CurrentLevel`.
957                if remaining_time_reportable {
958                    ctx.notify_attr_changed(
959                        self.endpoint_id,
960                        Self::CLUSTER.id,
961                        AttributeId::RemainingTime as _,
962                    );
963                }
964
965                return Ok(());
966            }
967
968            match remaining_time > event_duration {
969                true => remaining_time -= event_duration,
970                false => {
971                    warn!("remaining time is 0 before level reached target");
972                    remaining_time = Duration::from_millis(0)
973                }
974            }
975
976            let remaining_time_reportable = self.with_state(|state| {
977                state.write_remaining_time_quietly(remaining_time, is_transition_start)
978            });
979
980            if should_notify {
981                ctx.notify_attr_changed(
982                    self.endpoint_id,
983                    Self::CLUSTER.id,
984                    AttributeId::CurrentLevel as _,
985                );
986            }
987
988            if remaining_time_reportable {
989                ctx.notify_attr_changed(
990                    self.endpoint_id,
991                    Self::CLUSTER.id,
992                    AttributeId::RemainingTime as _,
993                );
994            }
995
996            let latency = match is_transition_start {
997                false => embassy_time::Instant::now() - event_start_time,
998                true => (embassy_time::Instant::now() - event_start_time) + startup_latency,
999            };
1000            match event_duration.checked_sub(latency) {
1001                Some(wait_time) => embassy_time::Timer::after(wait_time).await,
1002                None => warn!("no wait time. Consider dynamically adjusting the step size?"),
1003            }
1004        }
1005    }
1006
1007    /// Handles Move commands, determining the rate and initiating transitions.
1008    fn move_command(
1009        &self,
1010        state: &mut LevelControlState,
1011        with_on_off: bool,
1012        move_mode: MoveModeEnum,
1013        rate: Option<u8>,
1014        options_mask: OptionsBitmap,
1015        options_override: OptionsBitmap,
1016    ) -> Result<(), Error> {
1017        // From the spec
1018        //
1019        // If the Rate field is null, then the value of the
1020        // DefaultMoveRate attribute SHALL be used if that attribute is supported and its value is not null. If
1021        // the Rate field is null and the DefaultMoveRate attribute is either not supported or set to null, then
1022        // the device SHOULD move as fast as it is able.
1023        let rate = match rate {
1024            // A rate of zero would be a move that never arrives: the units-per-second
1025            // rate is the divisor of the movement's step interval, so zero has no
1026            // meaningful interpretation. Rejected as an invalid command (per the
1027            // App Cluster spec's Move / MoveWithOnOff constraints; `TC-LVL-4.1`
1028            // step 4h asserts `INVALID_COMMAND`).
1029            Some(0) => return Err(ErrorCode::InvalidCommand.into()),
1030            Some(val) => val,
1031            None => match state.default_move_rate.as_opt_ref() {
1032                Some(val) => *val,
1033                None => H::FASTEST_RATE,
1034            },
1035        };
1036
1037        // This will catch the case where H::FASTEST_RATE is 0.
1038        // The spec is not explicit about what should be done if this happens.
1039        // For now we error out if DefaultMoveRate is equal to 0 as this is invalid
1040        // until spec defines a behaviour.
1041        if rate == 0 {
1042            return Err(Error::new(ErrorCode::InvalidCommand));
1043        }
1044
1045        if !self.should_continue(with_on_off, options_mask, options_override)? {
1046            return Ok(());
1047        }
1048
1049        // Exit if we are already at the limit in the direct of movement.
1050        if let Some(current_level) = self.hooks.current_level() {
1051            if (current_level == H::MIN_LEVEL && move_mode == MoveModeEnum::Down)
1052                || (current_level == H::MAX_LEVEL && move_mode == MoveModeEnum::Up)
1053            {
1054                return Ok(());
1055            }
1056        }
1057
1058        let event_duration = Duration::from_hz(rate as u64);
1059
1060        info!("moving with rate {}", rate);
1061
1062        self.task_signal.signal(Task::Move {
1063            with_on_off,
1064            move_mode,
1065            event_duration,
1066        });
1067
1068        Ok(())
1069    }
1070
1071    /// Asynchronously moves the current level up or down at a specified rate.
1072    async fn move_transition(
1073        &self,
1074        ctx: impl HandlerContext,
1075        with_on_off: bool,
1076        move_mode: MoveModeEnum,
1077        event_duration: Duration,
1078    ) -> Result<(), Error> {
1079        loop {
1080            let event_start_time = Instant::now();
1081
1082            let current_level = match self.hooks.current_level() {
1083                Some(cl) => cl,
1084                None => return Err(ErrorCode::InvalidState.into()),
1085            };
1086
1087            let new_level = match move_mode {
1088                MoveModeEnum::Up => current_level.checked_add(1),
1089                MoveModeEnum::Down => current_level.checked_sub(1),
1090            };
1091
1092            let new_level = match new_level {
1093                Some(nl) => nl,
1094                None => return Ok(()),
1095            };
1096
1097            // If we start at min and go up, we need to update the onoff cluster immediately in case this method is halted.
1098            if current_level == H::MIN_LEVEL && new_level > H::MIN_LEVEL {
1099                self.update_coupled_on_off(new_level, with_on_off)?;
1100            }
1101
1102            let is_end_of_transition = (new_level == H::MAX_LEVEL) || (new_level == H::MIN_LEVEL);
1103
1104            // `Move` command path is command-driven only — no scene
1105            // recall queues a `Move` task.
1106            let (new_level, should_notify) = self.with_state(|state| {
1107                self.set_level(state, new_level, is_end_of_transition, true, false)
1108            })?;
1109            if should_notify {
1110                ctx.notify_attr_changed(
1111                    self.endpoint_id,
1112                    Self::CLUSTER.id,
1113                    AttributeId::CurrentLevel as _,
1114                );
1115            }
1116            let new_level = match new_level {
1117                Some(level) => level,
1118                None => return Err(ErrorCode::Failure.into()),
1119            };
1120
1121            if is_end_of_transition {
1122                self.update_coupled_on_off(new_level, with_on_off)?;
1123                return Ok(());
1124            }
1125
1126            let latency = embassy_time::Instant::now() - event_start_time;
1127            match event_duration.checked_sub(latency) {
1128                Some(wait_time) => embassy_time::Timer::after(wait_time).await,
1129                None => warn!("no wait time. Consider dynamically adjusting the step size?"),
1130            }
1131        }
1132    }
1133
1134    /// Handles Step commands, adjusting the level by a step size and managing transition time proportionally.
1135    fn step(
1136        &self,
1137        with_on_off: bool,
1138        step_mode: StepModeEnum,
1139        step_size: u8,
1140        transition_time: Option<u16>,
1141        options_mask: OptionsBitmap,
1142        options_override: OptionsBitmap,
1143    ) -> Result<(), Error> {
1144        // From the spec
1145        //
1146        // if the StepSize field has a value of zero, the command has no effect and
1147        // a response SHALL be returned with the status code set to INVALID_COMMAND.
1148        if step_size == 0 {
1149            return Err(ErrorCode::InvalidCommand.into());
1150        }
1151
1152        if !self.should_continue(with_on_off, options_mask, options_override)? {
1153            return Ok(());
1154        }
1155
1156        let current_level = match self.hooks.current_level() {
1157            Some(val) => val,
1158            None => return Err(ErrorCode::InvalidState.into()),
1159        };
1160
1161        let new_level = match step_mode {
1162            StepModeEnum::Up => current_level.saturating_add(step_size).min(H::MAX_LEVEL),
1163            StepModeEnum::Down => current_level.saturating_sub(step_size).max(H::MIN_LEVEL),
1164        };
1165
1166        // From the spec. Effect on Receipt
1167        // Increase/Decrease CurrentLevel by StepSize units, or until
1168        // it reaches the maximum/minimum level allowed for the
1169        // device if this reached in the process. In the latter
1170        // case, the transition time SHALL be
1171        // proportionally reduced.
1172        let transition_time = match transition_time {
1173            Some(val) => {
1174                if current_level.abs_diff(new_level) != step_size {
1175                    let new_step_size = current_level.abs_diff(new_level);
1176                    val.mul(new_step_size as u16).div_euclid(step_size as u16)
1177                } else {
1178                    val
1179                }
1180            }
1181            None => 0,
1182        };
1183
1184        // This will run some extra unnecessary checks, they will all pass, but benefits
1185        // of code reuse and a single source of truth for this logic outweigh the minor
1186        // performance cost of a few extra checks.
1187        self.move_to_level(
1188            with_on_off,
1189            new_level,
1190            Some(transition_time),
1191            options_mask,
1192            options_override,
1193            false,
1194        )
1195    }
1196
1197    /// Stops any ongoing transitions and resets the remaining time.
1198    fn stop(
1199        &self,
1200        ctx: impl HandlerContext,
1201        with_on_off: bool,
1202        options_mask: OptionsBitmap,
1203        options_override: OptionsBitmap,
1204    ) -> Result<(), Error> {
1205        if !self.should_continue(with_on_off, options_mask, options_override)? {
1206            return Ok(());
1207        }
1208        self.task_signal.signal(Task::Stop);
1209        if self
1210            .with_state(|state| state.write_remaining_time_quietly(Duration::from_millis(0), false))
1211        {
1212            ctx.notify_attr_changed(
1213                self.endpoint_id,
1214                Self::CLUSTER.id,
1215                AttributeId::RemainingTime as _,
1216            );
1217        }
1218
1219        Ok(())
1220    }
1221
1222    fn handle_out_of_band_message(&self, ctx: impl HandlerContext, message: OutOfBandMessage) {
1223        self.with_state(|state| {
1224            match message {
1225                OutOfBandMessage::Update(current_level) => {
1226                    self.task_signal.signal(Task::Stop);
1227
1228                    // OOB "device level changed under us" — genuine
1229                    // drift, never a scene apply.
1230                    match self.set_level(state, current_level, true, false, false) {
1231                        Ok((_, should_notify)) => {
1232                            let remaining_time_reportable = state
1233                                .write_remaining_time_quietly(Duration::from_millis(0), false);
1234
1235                            if should_notify {
1236                                ctx.notify_attr_changed(
1237                                    self.endpoint_id,
1238                                    Self::CLUSTER.id,
1239                                    AttributeId::CurrentLevel as _,
1240                                );
1241                            }
1242
1243                            if remaining_time_reportable {
1244                                ctx.notify_attr_changed(
1245                                    self.endpoint_id,
1246                                    Self::CLUSTER.id,
1247                                    AttributeId::RemainingTime as _,
1248                                );
1249                            }
1250                        }
1251                        Err(e) => {
1252                            error!("OutOfBandMessage::Update failed: set_level failed unexpectedly with set_device == false: {}", e);
1253                        }
1254                    }
1255                }
1256                OutOfBandMessage::MoveToLevel {
1257                    with_on_off,
1258                    level,
1259                    transition_time,
1260                    options_mask,
1261                    options_override,
1262                } => {
1263                    if let Err(e) = self.move_to_level(
1264                        with_on_off,
1265                        level,
1266                        transition_time,
1267                        options_mask,
1268                        options_override,
1269                        false,
1270                    ) {
1271                        error!(
1272                            "Device initiated MoveToLevel failed: {} | with_on_off: {}, level: {}, transition_time: {:?}, options_mask: {:?}, options_override: {:?}",
1273                            e, with_on_off, level, transition_time, options_mask, options_override
1274                        );
1275                    }
1276                }
1277                OutOfBandMessage::Move {
1278                    with_on_off,
1279                    move_mode,
1280                    rate,
1281                    options_mask,
1282                    options_override,
1283                } => {
1284                    if let Err(e) =
1285                        self.move_command(state, with_on_off, move_mode, rate, options_mask, options_override)
1286                    {
1287                        error!(
1288                            "Device initiated Move failed: {} | with_on_off: {}, move_mode: {:?}, rate: {:?}, options_mask: {:?}, options_override: {:?}",
1289                            e, with_on_off, move_mode, rate, options_mask, options_override
1290                        );
1291                    }
1292                }
1293                OutOfBandMessage::Step {
1294                    with_on_off,
1295                    step_mode,
1296                    step_size,
1297                    transition_time,
1298                    options_mask,
1299                    options_override,
1300                } => {
1301                    if let Err(e) = self.step(
1302                        with_on_off,
1303                        step_mode,
1304                        step_size,
1305                        transition_time,
1306                        options_mask,
1307                        options_override,
1308                    ) {
1309                        error!(
1310                            "Device initiated Step failed: {} | with_on_off: {}, step_mode: {:?}, step_size: {}, transition_time: {:?}, options_mask: {:?}, options_override: {:?}",
1311                            e, with_on_off, step_mode, step_size, transition_time, options_mask, options_override
1312                        );
1313                    }
1314                }
1315                OutOfBandMessage::Stop => {
1316                    self.task_signal.signal(Task::Stop);
1317                    if state.write_remaining_time_quietly(Duration::from_millis(0), false) {
1318                        ctx.notify_attr_changed(
1319                            self.endpoint_id,
1320                            Self::CLUSTER.id,
1321                            AttributeId::RemainingTime as _,
1322                        );
1323                    }
1324                }
1325            }
1326        })
1327    }
1328}
1329
1330impl<H: LevelControlHooks, OH: OnOffHooks> ClusterAsyncHandler for LevelControlHandler<'_, H, OH> {
1331    const CLUSTER: Cluster<'static> = H::CLUSTER;
1332
1333    // Runs an async task manager for the cluster handler.
1334    async fn run(&self, ctx: impl HandlerContext) -> Result<(), Error> {
1335        let mut hooks_fut = pin!(self
1336            .hooks
1337            .run(|message| self.handle_out_of_band_message(&ctx, message)));
1338
1339        loop {
1340            let mut task = match select(
1341                &mut hooks_fut,
1342                self.task_signal.wait_signalled(),
1343            ).await {
1344                Either::First(_) => panic!("LevelControlHooks::run returned; implementers MUST not return. Implementations should loop forever or await core::future::pending::<()>()."),
1345                Either::Second(task) => task,
1346            };
1347
1348            loop {
1349                match select3(
1350                    &mut hooks_fut,
1351                    self.task_manager(&ctx, task),
1352                    self.task_signal.wait_signalled(),
1353                )
1354                .await
1355                {
1356                    Either3::First(_) => panic!("LevelControlHooks::run returned; implementers MUST not return. Implementations should loop forever or await core::future::pending::<()>()."),
1357                    Either3::Second(_) => break,
1358                    Either3::Third(new_task) => task = new_task,
1359                };
1360            }
1361        }
1362    }
1363
1364    fn dataver(&self) -> u32 {
1365        self.dataver.get()
1366    }
1367
1368    fn dataver_changed(&self) {
1369        self.dataver.changed();
1370    }
1371
1372    fn current_level(
1373        &self,
1374        _ctx: impl ReadContext,
1375    ) -> impl Future<Output = Result<Nullable<u8>, Error>> {
1376        ready(match self.hooks.current_level() {
1377            Some(level) => Ok(Nullable::some(level)),
1378            None => Ok(Nullable::none()),
1379        })
1380    }
1381
1382    fn on_level(
1383        &self,
1384        _ctx: impl ReadContext,
1385    ) -> impl Future<Output = Result<Nullable<u8>, Error>> {
1386        ready(Ok(self.with_state(|state| state.on_level.clone())))
1387    }
1388
1389    fn set_on_level(
1390        &self,
1391        ctx: impl WriteContext,
1392        value: Nullable<u8>,
1393    ) -> impl Future<Output = Result<(), Error>> {
1394        ready('a: {
1395            if let Some(level) = value.clone().into_option() {
1396                if level > H::MAX_LEVEL || level < H::MIN_LEVEL {
1397                    break 'a Err(ErrorCode::ConstraintError.into());
1398                }
1399            }
1400
1401            self.with_state_notify(ctx, |state| {
1402                state.on_level = value;
1403            });
1404
1405            Ok(())
1406        })
1407    }
1408
1409    fn options(
1410        &self,
1411        _ctx: impl ReadContext,
1412    ) -> impl Future<Output = Result<OptionsBitmap, Error>> {
1413        ready(Ok(self.with_state(|state| state.options)))
1414    }
1415
1416    fn set_options(
1417        &self,
1418        ctx: impl WriteContext,
1419        value: OptionsBitmap,
1420    ) -> impl Future<Output = Result<(), Error>> {
1421        ready({
1422            self.with_state_notify(ctx, |state| {
1423                state.options = value;
1424            });
1425
1426            Ok(())
1427        })
1428    }
1429
1430    fn remaining_time(&self, _ctx: impl ReadContext) -> impl Future<Output = Result<u16, Error>> {
1431        ready(Ok(self.with_state(|state| state.remaining_time)))
1432    }
1433
1434    fn max_level(&self, _ctx: impl ReadContext) -> impl Future<Output = Result<u8, Error>> {
1435        ready(Ok(H::MAX_LEVEL))
1436    }
1437
1438    fn min_level(&self, _ctx: impl ReadContext) -> impl Future<Output = Result<u8, Error>> {
1439        ready(Ok(H::MIN_LEVEL))
1440    }
1441
1442    fn on_off_transition_time(
1443        &self,
1444        _ctx: impl ReadContext,
1445    ) -> impl Future<Output = Result<u16, Error>> {
1446        ready(Ok(self.with_state(|state| state.on_off_transition_time)))
1447    }
1448
1449    fn set_on_off_transition_time(
1450        &self,
1451        ctx: impl WriteContext,
1452        value: u16,
1453    ) -> impl Future<Output = Result<(), Error>> {
1454        ready({
1455            self.with_state_notify(ctx, |state| {
1456                state.on_off_transition_time = value;
1457            });
1458
1459            Ok(())
1460        })
1461    }
1462
1463    fn on_transition_time(
1464        &self,
1465        _ctx: impl ReadContext,
1466    ) -> impl Future<Output = Result<Nullable<u16>, Error>> {
1467        ready(Ok(self.with_state(|state| state.on_transition_time.clone())))
1468    }
1469
1470    fn set_on_transition_time(
1471        &self,
1472        ctx: impl WriteContext,
1473        value: Nullable<u16>,
1474    ) -> impl Future<Output = Result<(), Error>> {
1475        ready({
1476            self.with_state_notify(ctx, |state| {
1477                state.on_transition_time = value;
1478            });
1479
1480            Ok(())
1481        })
1482    }
1483
1484    fn off_transition_time(
1485        &self,
1486        _ctx: impl ReadContext,
1487    ) -> impl Future<Output = Result<Nullable<u16>, Error>> {
1488        ready(Ok(
1489            self.with_state(|state| state.off_transition_time.clone())
1490        ))
1491    }
1492
1493    fn set_off_transition_time(
1494        &self,
1495        ctx: impl WriteContext,
1496        value: Nullable<u16>,
1497    ) -> impl Future<Output = Result<(), Error>> {
1498        ready({
1499            self.with_state_notify(ctx, |state| {
1500                state.off_transition_time = value;
1501            });
1502
1503            Ok(())
1504        })
1505    }
1506
1507    fn default_move_rate(
1508        &self,
1509        _ctx: impl ReadContext,
1510    ) -> impl Future<Output = Result<Nullable<u8>, Error>> {
1511        ready(Ok(self.with_state(|state| state.default_move_rate.clone())))
1512    }
1513
1514    fn set_default_move_rate(
1515        &self,
1516        ctx: impl WriteContext,
1517        value: Nullable<u8>,
1518    ) -> impl Future<Output = Result<(), Error>> {
1519        ready('a: {
1520            // The spec is not explicit about what should be done if this happens.
1521            // For now we error out if DefaultMoveRate is equal to 0 as this is invalid
1522            // until spec defines a behaviour.
1523            if Some(0) == value.clone().into_option() {
1524                break 'a Err(ErrorCode::InvalidData.into());
1525            }
1526
1527            self.with_state_notify(ctx, |state| {
1528                state.default_move_rate = value;
1529            });
1530
1531            Ok(())
1532        })
1533    }
1534
1535    fn start_up_current_level(
1536        &self,
1537        _ctx: impl ReadContext,
1538    ) -> impl Future<Output = Result<Nullable<u8>, Error>> {
1539        ready(match self.hooks.start_up_current_level() {
1540            Ok(Some(val)) => Ok(Nullable::some(val)),
1541            Ok(None) => Ok(Nullable::none()),
1542            Err(e) => Err(e),
1543        })
1544    }
1545
1546    fn set_start_up_current_level(
1547        &self,
1548        ctx: impl WriteContext,
1549        value: Nullable<u8>,
1550    ) -> impl Future<Output = Result<(), Error>> {
1551        ready('a: {
1552            // According to the current spec, this attribute does not have any constraints at this stage.
1553            // However, it's usage is bounded by min/max hence it makes sense to restrict the settable values to this range.
1554            if let Some(level) = value.clone().into_option() {
1555                if level > H::MAX_LEVEL || level < H::MIN_LEVEL {
1556                    break 'a Err(ErrorCode::ConstraintError.into());
1557                }
1558            }
1559
1560            match self.hooks.set_start_up_current_level(value.into_option()) {
1561                Ok(()) => {
1562                    ctx.notify_changed();
1563                    Ok(())
1564                }
1565                Err(e) => Err(e),
1566            }
1567        })
1568    }
1569
1570    fn handle_move_to_level(
1571        &self,
1572        _ctx: impl InvokeContext,
1573        request: MoveToLevelRequest<'_>,
1574    ) -> impl Future<Output = Result<(), Error>> {
1575        ready('a: {
1576            let level = match request.level() {
1577                Ok(v) => v,
1578                Err(e) => break 'a Err(e),
1579            };
1580            let transition_time = match request.transition_time() {
1581                Ok(v) => v.into_option(),
1582                Err(e) => break 'a Err(e),
1583            };
1584            let options_mask = match request.options_mask() {
1585                Ok(v) => v,
1586                Err(e) => break 'a Err(e),
1587            };
1588            let options_override = match request.options_override() {
1589                Ok(v) => v,
1590                Err(e) => break 'a Err(e),
1591            };
1592            self.move_to_level(
1593                false,
1594                level,
1595                transition_time,
1596                options_mask,
1597                options_override,
1598                false,
1599            )
1600        })
1601    }
1602
1603    fn handle_move(
1604        &self,
1605        _ctx: impl InvokeContext,
1606        request: MoveRequest<'_>,
1607    ) -> impl Future<Output = Result<(), Error>> {
1608        ready(self.with_state(|state| {
1609            self.move_command(
1610                state,
1611                false,
1612                request.move_mode()?,
1613                request.rate()?.into_option(),
1614                request.options_mask()?,
1615                request.options_override()?,
1616            )
1617        }))
1618    }
1619
1620    fn handle_step(
1621        &self,
1622        _ctx: impl InvokeContext,
1623        request: StepRequest<'_>,
1624    ) -> impl Future<Output = Result<(), Error>> {
1625        ready('a: {
1626            let step_mode = match request.step_mode() {
1627                Ok(v) => v,
1628                Err(e) => break 'a Err(e),
1629            };
1630            let step_size = match request.step_size() {
1631                Ok(v) => v,
1632                Err(e) => break 'a Err(e),
1633            };
1634            let transition_time = match request.transition_time() {
1635                Ok(v) => v.into_option(),
1636                Err(e) => break 'a Err(e),
1637            };
1638            let options_mask = match request.options_mask() {
1639                Ok(v) => v,
1640                Err(e) => break 'a Err(e),
1641            };
1642            let options_override = match request.options_override() {
1643                Ok(v) => v,
1644                Err(e) => break 'a Err(e),
1645            };
1646            self.step(
1647                false,
1648                step_mode,
1649                step_size,
1650                transition_time,
1651                options_mask,
1652                options_override,
1653            )
1654        })
1655    }
1656
1657    fn handle_stop(
1658        &self,
1659        ctx: impl InvokeContext,
1660        request: StopRequest<'_>,
1661    ) -> impl Future<Output = Result<(), Error>> {
1662        ready('a: {
1663            let options_mask = match request.options_mask() {
1664                Ok(v) => v,
1665                Err(e) => break 'a Err(e),
1666            };
1667            let options_override = match request.options_override() {
1668                Ok(v) => v,
1669                Err(e) => break 'a Err(e),
1670            };
1671            self.stop(&ctx, false, options_mask, options_override)
1672        })
1673    }
1674
1675    fn handle_move_to_level_with_on_off(
1676        &self,
1677        _ctx: impl InvokeContext,
1678        request: MoveToLevelWithOnOffRequest<'_>,
1679    ) -> impl Future<Output = Result<(), Error>> {
1680        ready('a: {
1681            let level = match request.level() {
1682                Ok(v) => v,
1683                Err(e) => break 'a Err(e),
1684            };
1685            let transition_time = match request.transition_time() {
1686                Ok(v) => v.into_option(),
1687                Err(e) => break 'a Err(e),
1688            };
1689            let options_mask = match request.options_mask() {
1690                Ok(v) => v,
1691                Err(e) => break 'a Err(e),
1692            };
1693            let options_override = match request.options_override() {
1694                Ok(v) => v,
1695                Err(e) => break 'a Err(e),
1696            };
1697            self.move_to_level(
1698                true,
1699                level,
1700                transition_time,
1701                options_mask,
1702                options_override,
1703                false,
1704            )
1705        })
1706    }
1707
1708    fn handle_move_with_on_off(
1709        &self,
1710        _ctx: impl InvokeContext,
1711        request: MoveWithOnOffRequest<'_>,
1712    ) -> impl Future<Output = Result<(), Error>> {
1713        ready(self.with_state(|state| {
1714            self.move_command(
1715                state,
1716                true,
1717                request.move_mode()?,
1718                request.rate()?.into_option(),
1719                request.options_mask()?,
1720                request.options_override()?,
1721            )
1722        }))
1723    }
1724
1725    fn handle_step_with_on_off(
1726        &self,
1727        _ctx: impl InvokeContext,
1728        request: StepWithOnOffRequest<'_>,
1729    ) -> impl Future<Output = Result<(), Error>> {
1730        ready('a: {
1731            let step_mode = match request.step_mode() {
1732                Ok(v) => v,
1733                Err(e) => break 'a Err(e),
1734            };
1735            let step_size = match request.step_size() {
1736                Ok(v) => v,
1737                Err(e) => break 'a Err(e),
1738            };
1739            let transition_time = match request.transition_time() {
1740                Ok(v) => v.into_option(),
1741                Err(e) => break 'a Err(e),
1742            };
1743            let options_mask = match request.options_mask() {
1744                Ok(v) => v,
1745                Err(e) => break 'a Err(e),
1746            };
1747            let options_override = match request.options_override() {
1748                Ok(v) => v,
1749                Err(e) => break 'a Err(e),
1750            };
1751            self.step(
1752                true,
1753                step_mode,
1754                step_size,
1755                transition_time,
1756                options_mask,
1757                options_override,
1758            )
1759        })
1760    }
1761
1762    fn handle_stop_with_on_off(
1763        &self,
1764        ctx: impl InvokeContext,
1765        request: StopWithOnOffRequest<'_>,
1766    ) -> impl Future<Output = Result<(), Error>> {
1767        ready('a: {
1768            let options_mask = match request.options_mask() {
1769                Ok(v) => v,
1770                Err(e) => break 'a Err(e),
1771            };
1772            let options_override = match request.options_override() {
1773                Ok(v) => v,
1774                Err(e) => break 'a Err(e),
1775            };
1776            self.stop(&ctx, true, options_mask, options_override)
1777        })
1778    }
1779
1780    fn handle_move_to_closest_frequency(
1781        &self,
1782        _ctx: impl InvokeContext,
1783        _request: MoveToClosestFrequencyRequest<'_>,
1784    ) -> impl Future<Output = Result<(), Error>> {
1785        ready(Err(ErrorCode::InvalidCommand.into()))
1786    }
1787}
1788
1789pub trait LevelControlHooks {
1790    const MIN_LEVEL: u8;
1791    const MAX_LEVEL: u8;
1792    const FASTEST_RATE: u8;
1793    const CLUSTER: Cluster<'static>;
1794
1795    /// Implements the business logic for setting the level of the device.
1796    /// Returns the level the device was set to.
1797    /// If this method returns Err, the `LevelControlHandler` will represent this as an error with `ImStatusCode` of `Failure`.
1798    /// Note: The above is the only responsibility of this method. There is no need to update Matter attributes.
1799    #[allow(clippy::result_unit_err)]
1800    fn set_device_level(&self, level: u8) -> Result<Option<u8>, ()>;
1801
1802    // Raw accessors
1803    //  These methods should not perform any checks.
1804    //  They should simply get or set values.
1805    //  They should not error.
1806
1807    /// Raw current_level getter.
1808    /// This value should persist across reboots.
1809    fn current_level(&self) -> Option<u8>;
1810
1811    /// Raw current_level setter.
1812    /// This value should persist across reboots.
1813    fn set_current_level(&self, level: Option<u8>);
1814
1815    /// Raw start_up_current_level getter.
1816    /// This value should persist across reboots.
1817    fn start_up_current_level(&self) -> Result<Option<u8>, Error> {
1818        Err(ErrorCode::AttributeNotFound.into())
1819    }
1820    /// Raw start_up_current_level setter.
1821    /// This value should persist across reboots.
1822    fn set_start_up_current_level(&self, _value: Option<u8>) -> Result<(), Error> {
1823        Err(ErrorCode::AttributeNotFound.into())
1824    }
1825
1826    /// Background task for out-of-band notifications to the handler.
1827    ///
1828    /// This future MUST NOT return. Implementers should either loop forever or await
1829    /// core::future::pending::<()>(), so the SDK's task does not observe a completed future.
1830    ///
1831    /// # Panics
1832    /// The SDK will panic if this method returns.
1833    fn run<F: Fn(OutOfBandMessage)>(&self, _notify: F) -> impl Future<Output = ()> {
1834        pending::<()>()
1835    }
1836}
1837
1838impl<T> LevelControlHooks for &T
1839where
1840    T: LevelControlHooks,
1841{
1842    const MIN_LEVEL: u8 = T::MIN_LEVEL;
1843    const MAX_LEVEL: u8 = T::MAX_LEVEL;
1844    const FASTEST_RATE: u8 = T::FASTEST_RATE;
1845    const CLUSTER: Cluster<'static> = T::CLUSTER;
1846
1847    fn set_device_level(&self, level: u8) -> Result<Option<u8>, ()> {
1848        (*self).set_device_level(level)
1849    }
1850
1851    fn current_level(&self) -> Option<u8> {
1852        (*self).current_level()
1853    }
1854
1855    fn set_current_level(&self, level: Option<u8>) {
1856        (*self).set_current_level(level)
1857    }
1858
1859    fn start_up_current_level(&self) -> Result<Option<u8>, Error> {
1860        (*self).start_up_current_level()
1861    }
1862
1863    fn set_start_up_current_level(&self, value: Option<u8>) -> Result<(), Error> {
1864        (*self).set_start_up_current_level(value)
1865    }
1866
1867    fn run<F: Fn(OutOfBandMessage)>(&self, notify: F) -> impl Future<Output = ()> {
1868        (*self).run(notify)
1869    }
1870}
1871
1872/// This is a phantom type for when the LevelControl cluster is not coupled with an OnOff cluster.
1873/// This type should only be used for annotations and not for actual OnOff functionality.
1874/// All methods will panic.
1875pub struct NoOnOff;
1876
1877impl OnOffHooks for NoOnOff {
1878    const CLUSTER: Cluster<'static> = ON_OFF_FULL_CLUSTER;
1879
1880    fn on_off(&self) -> bool {
1881        panic!("NoOnOff: on_off called unexpectedly - this phantom type should not be used for OnOff functionality")
1882    }
1883
1884    fn set_on_off(&self, _on: bool) {
1885        panic!("NoOnOff: set_on_off called unexpectedly - this phantom type should not be used for OnOff functionality")
1886    }
1887
1888    fn start_up_on_off(&self) -> Nullable<super::on_off::StartUpOnOffEnum> {
1889        panic!("NoOnOff: start_up_on_off called unexpectedly - this phantom type should not be used for OnOff functionality")
1890    }
1891
1892    fn set_start_up_on_off(
1893        &self,
1894        _value: Nullable<super::on_off::StartUpOnOffEnum>,
1895    ) -> Result<(), Error> {
1896        panic!("NoOnOff: set_start_up_on_off called unexpectedly - this method should not be called when LevelControl is not coupled with OnOff")
1897    }
1898
1899    async fn handle_off_with_effect(&self, _effect: super::on_off::EffectVariantEnum) {
1900        panic!("NoOnOff: handle_off_with_effect called unexpectedly - this phantom type should not be used for OnOff functionality")
1901    }
1902}
1903
1904/// Scenes Management integration for the LevelControl cluster. The
1905/// only scenable attribute is `CurrentLevel`; apply routes through
1906/// `MoveToLevel` with the scene's transition time.
1907impl<H, OH> SceneClusterHandler for LevelControlHandler<'_, H, OH>
1908where
1909    H: LevelControlHooks,
1910    OH: OnOffHooks,
1911{
1912    const CLUSTER_ID: ClusterId = FULL_CLUSTER.id;
1913
1914    fn endpoint_id(&self) -> EndptId {
1915        self.endpoint_id
1916    }
1917
1918    fn is_scenable_attribute(attribute_id: AttrId) -> bool {
1919        attribute_id == AttributeId::CurrentLevel as AttrId
1920    }
1921
1922    fn capture<P: TLVBuilderParent>(
1923        &self,
1924        avp_array: AttributeValuePairStructArrayBuilder<P>,
1925    ) -> Result<AttributeValuePairStructArrayBuilder<P>, Error> {
1926        // `CurrentLevel` is nullable; null → skip the AVP entry.
1927        if let Some(level) = self.hooks.current_level() {
1928            avp_array.push_u8(AttributeId::CurrentLevel as _, level)
1929        } else {
1930            Ok(avp_array)
1931        }
1932    }
1933
1934    async fn apply<C: HandlerContext>(
1935        &self,
1936        _ctx: &C,
1937        avp_list: &TLVArray<'_, AttributeValuePairStruct<'_>>,
1938        transition_time_ms: u32,
1939    ) -> Result<(), Error> {
1940        for avp in avp_list.iter() {
1941            let avp = avp?;
1942            if avp.attribute_id()? != AttributeId::CurrentLevel as _ {
1943                continue;
1944            }
1945            let Some(level) = avp.value_unsigned_8()? else {
1946                continue;
1947            };
1948            // Reuse the command-driven `MoveToLevel` pipeline with
1949            // `scene_apply=true` (suppresses `SceneValid` drift on
1950            // every step) and `with_on_off=false` (OnOff lands its own
1951            // AVP via `OnOffHandler::apply`). RecallScene transition
1952            // time is ms; MoveToLevel is deciseconds — convert with
1953            // saturation.
1954            let transition_ds = (transition_time_ms / 100).min(u16::MAX as u32) as u16;
1955            return self.move_to_level(
1956                false,
1957                level,
1958                Some(transition_ds),
1959                OptionsBitmap::empty(),
1960                OptionsBitmap::empty(),
1961                true,
1962            );
1963        }
1964        Ok(())
1965    }
1966}
1967
1968pub mod test {
1969    use crate::dm::clusters::app::level_control::{
1970        AttributeId, CommandId, Feature, LevelControlHooks, FULL_CLUSTER,
1971    };
1972    use crate::dm::Cluster;
1973    use crate::error::Error;
1974    use crate::utils::cell::RefCell;
1975    use crate::utils::sync::blocking::Mutex;
1976    use crate::with;
1977
1978    struct TestLevelControlState {
1979        current_level: Option<u8>,
1980        start_up_current_level: Option<u8>,
1981    }
1982
1983    impl TestLevelControlState {
1984        const fn new() -> Self {
1985            Self {
1986                current_level: Some(1),
1987                start_up_current_level: None,
1988            }
1989        }
1990    }
1991
1992    pub struct TestLevelControlDeviceLogic {
1993        state: Mutex<RefCell<TestLevelControlState>>,
1994    }
1995
1996    impl TestLevelControlDeviceLogic {
1997        pub const fn new() -> Self {
1998            Self {
1999                state: Mutex::new(RefCell::new(TestLevelControlState::new())),
2000            }
2001        }
2002    }
2003
2004    impl Default for TestLevelControlDeviceLogic {
2005        fn default() -> Self {
2006            Self::new()
2007        }
2008    }
2009
2010    impl LevelControlHooks for TestLevelControlDeviceLogic {
2011        const MIN_LEVEL: u8 = 1;
2012        const MAX_LEVEL: u8 = 254;
2013        const FASTEST_RATE: u8 = 50;
2014        const CLUSTER: Cluster<'static> = FULL_CLUSTER
2015            .with_revision(6)
2016            .with_features(Feature::ON_OFF.bits())
2017            .with_attrs(with!(
2018                required;
2019                AttributeId::CurrentLevel
2020                | AttributeId::MinLevel
2021                | AttributeId::MaxLevel
2022                | AttributeId::OnLevel
2023                | AttributeId::Options
2024            ))
2025            .with_cmds(with!(
2026                CommandId::MoveToLevel
2027                    | CommandId::Move
2028                    | CommandId::Step
2029                    | CommandId::Stop
2030                    | CommandId::MoveToLevelWithOnOff
2031                    | CommandId::MoveWithOnOff
2032                    | CommandId::StepWithOnOff
2033                    | CommandId::StopWithOnOff
2034            ));
2035
2036        fn set_device_level(&self, level: u8) -> Result<Option<u8>, ()> {
2037            // This is where business logic is implemented to physically change the level of the device.
2038            Ok(Some(level))
2039        }
2040
2041        fn current_level(&self) -> Option<u8> {
2042            self.state.lock(|state| state.borrow().current_level)
2043        }
2044
2045        fn set_current_level(&self, level: Option<u8>) {
2046            info!(
2047                "LevelControlDeviceLogic::set_current_level: setting level to {:?}",
2048                level
2049            );
2050            self.state
2051                .lock(|state| state.borrow_mut().current_level = level);
2052        }
2053
2054        fn start_up_current_level(&self) -> Result<Option<u8>, Error> {
2055            Ok(self
2056                .state
2057                .lock(|state| state.borrow().start_up_current_level))
2058        }
2059
2060        fn set_start_up_current_level(&self, value: Option<u8>) -> Result<(), Error> {
2061            self.state
2062                .lock(|state| state.borrow_mut().start_up_current_level = value);
2063            Ok(())
2064        }
2065    }
2066}
2067
2068#[cfg(test)]
2069mod tests {
2070    use super::test::TestLevelControlDeviceLogic;
2071    use super::{AttributeDefaults, LevelControlHandler};
2072    use crate::dm::clusters::app::on_off::test::TestOnOffDeviceLogic;
2073    use crate::dm::clusters::app::on_off::OnOffHandler;
2074    use crate::dm::Dataver;
2075
2076    /// Catches drift between `TestLevelControlDeviceLogic::CLUSTER` and
2077    /// `LevelControlHandler::validate()`.
2078    #[test]
2079    fn test_logic_passes_handler_validate() {
2080        let level_logic = TestLevelControlDeviceLogic::new();
2081        let on_off_logic = TestOnOffDeviceLogic::new(false);
2082        let level = LevelControlHandler::new(
2083            Dataver::new(1),
2084            1,
2085            &level_logic,
2086            AttributeDefaults::default(),
2087        );
2088        let on_off = OnOffHandler::new(Dataver::new(2), 1, &on_off_logic);
2089        on_off.init(Some(&level));
2090        level.init(Some(&on_off));
2091    }
2092}