rs-matter 0.2.0

Native Rust implementation of the Matter (Smart-Home) ecosystem
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
/*
 *
 *    Copyright (c) 2022-2026 Project CHIP Authors
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

//! Implementation of the Matter On/Off cluster.
//!
//! This module provides the core logic and state management for the OnOff cluster as defined by the Matter specification v1.3.
//!
//! Key features:
//! - Provides hooks for device-specific logic via the `OnOffHooks` trait.
//! - Validates cluster configuration and feature dependencies.
//! - Manages OnWithTimedOff guards and OffWithEffect transitions.
//! - Provides coupling with a LevelControl cluster on the same endpoint.
//!
//! Unsupported features:
//! - The attribute and logic related to the Scenes cluster are not fully implemented since the Scenes cluster is not yet implemented.

use core::cell::Cell;
use core::future::{ready, Future};
use core::pin::pin;

use embassy_futures::select::{select, select3, Either, Either3};

use crate::dm::clusters::app::level_control::{LevelControlHandler, LevelControlHooks};
use crate::dm::clusters::decl::scenes_management::{
    AttributeValuePairStruct, AttributeValuePairStructArrayBuilder,
};
use crate::dm::clusters::decl::{level_control, on_off};
use crate::dm::clusters::scenes::{SceneClusterHandler, SceneInvalidator};
use crate::dm::types::EndptId;
use crate::dm::{
    AttrId, Cluster, ClusterId, Dataver, HandlerContext, InvokeContext, ReadContext, WriteContext,
};
use crate::error::{Error, ErrorCode};
use crate::tlv::{TLVArray, TLVBuilderParent};

pub use crate::dm::clusters::decl::on_off::*;

use crate::tlv::Nullable;
use crate::utils::cell::RefCell;
use crate::utils::sync::blocking::Mutex;
use crate::utils::sync::Signal;

/// Messages passed to the `notify` closure in `OnOffHooks::run()` method.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum OutOfBandMessage {
    /// Indicates to the handler that the state of the device has changed and it should update the Matter state accordingly.
    Update,
    /// Indicates to the handler that a request to change the state to On has been made.
    /// This will change the state of the device if and when appropriate according to the Matter logic.
    On,
    /// Indicates to the handler that a request to change the state to Off has been made.
    /// This will change the state of the device if and when appropriate according to the Matter logic.
    Off,
    /// Indicates to the handler that a request to toggle the OnOff state has been made.
    /// This will change the state of the device if and when appropriate according to the Matter logic.
    Toggle,
}

/// A rust friendly combined enum that groups the effect and its variant.
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum EffectVariantEnum {
    DelayedAllOff(DelayedAllOffEffectVariantEnum),
    DyingLight(DyingLightEffectVariantEnum),
}

// The state of the internal OnOff state machine.
#[derive(Clone, Copy, PartialEq)]
enum OnOffClusterState {
    On,
    Off,
    TimedOn,
    DelayedOff,
}

// Internal enum for managing sending commands to the state machine.
enum OnOffCommand {
    Off,
    On,
    Toggle,
    OffWithEffect(EffectVariantEnum),
    OnWithTimedOff,
    CoupledClusterOn,
    CoupledClusterOff,
    // This indicates that the physical state of the device has changed and our state machine should
    // reflect that without making any changes to the state of the device.
    Update,
}

struct OnOffState {
    state: OnOffClusterState,
    global_scene_control: bool,
    on_time: u16,
    off_wait_time: u16,
}

impl OnOffState {
    pub const fn new(state: OnOffClusterState) -> Self {
        Self {
            state,
            global_scene_control: true,
            on_time: 0,
            off_wait_time: 0,
        }
    }
}

/// Implementation of the Matter On/Off cluster handler.
///
/// This struct provides the logic for managing the On/Off cluster state machine, handling commands,
/// attributes, and feature dependencies as specified by the Matter specification. It supports coupling
/// with a LevelControl cluster, manages timed transitions, and enforces feature-specific requirements.
///
/// # Usage
/// - Implement the `OnOffHooks` trait to provide device-specific persistence and effect handling.
/// - Instantiate with a `Dataver` and user-provided `OnOffHooks` implementation.
/// - Initialise and optionally couple with a LevelControl cluster via `init`.
/// - Use the async `run` method to process incoming commands and manage state transitions.
///
/// # Panics
/// - The handler will panic during initialisation if the cluster configuration is invalid or missing required
///   attributes/commands for enabled features.
// TODO:
// #[derive(Clone, Debug)]
// #[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct OnOffHandler<'a, H: OnOffHooks, LH: LevelControlHooks> {
    dataver: Dataver,
    endpoint_id: EndptId,
    hooks: H,
    level_control_handler: Mutex<Cell<Option<&'a LevelControlHandler<'a, LH, H>>>>,
    /// Set via [`OnOffHandler::with_scene_invalidator`] when this
    /// device hosts Scenes Management on the same endpoint.
    scene_invalidator: Mutex<Cell<Option<&'a dyn SceneInvalidator>>>,
    state: Mutex<RefCell<OnOffState>>,
    state_change_signal: Signal<Option<OnOffCommand>>,
}

impl<H: OnOffHooks> OnOffHandler<'_, H, NoLevelControl> {
    /// Creates a new `OnOffHandler` with the given hooks which is **not** coupled with a `LevelControl` handler.
    ///
    /// NOTE: This constructor automatically calls `init` with no coupled `LevelControl` handler.
    ///
    /// # Arguments
    /// - `hooks` - A reference to the struct implementing the device-specific on/off logic.
    pub fn new_standalone(dataver: Dataver, endpoint_id: EndptId, hooks: H) -> Self {
        let this = Self::new(dataver, endpoint_id, hooks);

        this.init(None);

        this
    }
}

impl<'a, H: OnOffHooks, LH: LevelControlHooks> OnOffHandler<'a, H, LH> {
    /// Creates a new `OnOffHandler` with the given hooks.
    ///
    /// # Arguments
    /// - `hooks` - A reference to the struct implementing the device-specific on/off logic.
    ///
    /// # Usage
    /// - Initialise and optionally couple with a LevelControl handler via `init`.
    pub fn new(dataver: Dataver, endpoint_id: EndptId, hooks: H) -> Self {
        let state = match hooks.on_off() {
            true => OnOffClusterState::On,
            false => OnOffClusterState::Off,
        };

        Self {
            dataver,
            endpoint_id,
            hooks,
            level_control_handler: Mutex::new(Cell::new(None)),
            scene_invalidator: Mutex::new(Cell::new(None)),
            state: Mutex::new(RefCell::new(OnOffState::new(state))),
            state_change_signal: Signal::new(None),
        }
    }

    /// Checks that the cluster is correctly configured, including required attributes, commands, and feature dependencies.
    ///
    /// # Panics
    ///
    /// Panics with an error message if the handler's cluster configuration (`Self::CLUSTER`) is misconfigured.
    fn validate(&self) {
        if Self::CLUSTER.revision != 6 {
            panic!(
                "OnOff validation: incorrect version number: expected 6 got {}",
                Self::CLUSTER.revision
            );
        }

        // Check for mandatory attributes
        if Self::CLUSTER.attribute(AttributeId::OnOff as _).is_none() {
            panic!("OnOff validation: missing required attribute: OnOff");
        }

        // Check for mandatory commands
        if Self::CLUSTER.command(CommandId::Off as _).is_none() {
            panic!("OnOff validation: missing required command: Off");
        }

        // Check LIGHTING feature requirements
        if Self::supports_feature(on_off::Feature::LIGHTING.bits()) {
            if Self::CLUSTER
                .attribute(AttributeId::GlobalSceneControl as _)
                .is_none()
                || Self::CLUSTER.attribute(AttributeId::OnTime as _).is_none()
                || Self::CLUSTER
                    .attribute(AttributeId::OffWaitTime as _)
                    .is_none()
                || Self::CLUSTER
                    .attribute(AttributeId::StartUpOnOff as _)
                    .is_none()
            {
                panic!("OnOff validation: missing attributes required by LIGHTING feature: GlobalSceneControl, OnTime, OffWaitTime, StartUpOnOff")
            }

            if Self::CLUSTER
                .command(CommandId::OffWithEffect as _)
                .is_none()
                || Self::CLUSTER
                    .command(CommandId::OnWithRecallGlobalScene as _)
                    .is_none()
                || Self::CLUSTER
                    .command(CommandId::OnWithTimedOff as _)
                    .is_none()
            {
                panic!("OnOff validation: missing commands required by LIGHTING feature: OffWithEffect, OnWithRecallGlobalScene, OnWithTimedOff")
            }
        }

        // Check OFFONLY feature requirements
        if Self::supports_feature(on_off::Feature::OFF_ONLY.bits())
            && (Self::CLUSTER.command(CommandId::On as _).is_some()
                || Self::CLUSTER.command(CommandId::Toggle as _).is_some())
        {
            panic!("OnOff validation: extra commands while using OFFONLY feature: On, Toggle")
        }
    }

    /// Initialise the cluster on startup.
    /// - wire coupled handlers
    /// - validate the handler setup with the configuration
    /// - update the OnOff state based on the StartUpOnOff attribute
    ///
    /// # Parameters
    /// *level_control_handler: the LevelControlHandler instance coupled with this OnOffHandler, i.e. the LevelControl cluster on the same endpoint.
    ///
    /// # Panics
    ///
    /// panics if the `state`'s `CLUSTER` is misconfigured.
    pub fn init(&self, level_control_handler: Option<&'a LevelControlHandler<'a, LH, H>>) {
        // Wire any coupled clusters
        self.level_control_handler
            .lock(|h| h.set(level_control_handler));

        self.validate();

        // 1.5.6.6. StartUpOnOff Attribute
        // This attribute SHALL define the desired startup behavior of a device when it is supplied with power
        // and this state SHALL be reflected in the OnOff attribute. If the value is null, the OnOff attribute is
        // set to its previous value. Otherwise, the behavior is defined in the table defining StartUpOnOffEnum.
        // todo: Implement checking the reason for reboot.
        // This behavior does not apply to reboots associated with OTA. After an OTA restart, the OnOff
        // attribute SHALL return to its value prior to the restart.
        //
        // Note: We assume that since the on_off state is persisted by the user and it is entangled with the
        // actual state of the device, if start_up_on_off == null we don't need to do anything.
        if let Some(start_up_state) = self.hooks.start_up_on_off().into_option() {
            match start_up_state {
                StartUpOnOffEnum::Off => self.hooks.set_on_off(false),
                StartUpOnOffEnum::On => self.hooks.set_on_off(true),
                StartUpOnOffEnum::Toggle => self.hooks.set_on_off(!self.hooks.on_off()),
            }
        }
    }

    /// Adapt the handler instance to the generic `rs-matter` `Handler` trait
    pub const fn adapt(self) -> HandlerAsyncAdaptor<Self> {
        HandlerAsyncAdaptor(self)
    }

    /// Attach a [`SceneInvalidator`] — typically the
    /// [`crate::dm::clusters::scenes::ScenesState`] backing Scenes
    /// Management on the same endpoint — so command-driven `OnOff`
    /// mutations flip `SceneValid → false` for any recalled scene.
    /// No-op when unset.
    pub fn with_scene_invalidator(self, invalidator: &'a dyn SceneInvalidator) -> Self {
        self.scene_invalidator
            .lock(|cell| cell.set(Some(invalidator)));
        self
    }

    fn notify_scenable_changed(&self) {
        if let Some(inv) = self.scene_invalidator.lock(|cell| cell.get()) {
            inv.scenable_attribute_changed(self.endpoint_id);
        }
    }

    /// Request an out-of-band change to the OnOff state.
    /// This method can be used, for example, when the device state changes due to physical interactions
    /// or when the device autonomously decides to change its state.
    ///
    /// This method behaves the same as the OnOff cluster's On or Off commands.
    /// I.e, This method will trigger the appropriate state change logic, including any coupled cluster interactions,
    /// feature-dependent attribute updates and device-specific update logic.
    pub fn set_on_off(&self, on: bool) {
        match on {
            true => self.state_change_signal.signal(OnOffCommand::On),
            false => self.state_change_signal.signal(OnOffCommand::Off),
        }
    }

    // Allows coupled clusters or user code to get the on_off state.
    pub fn on_off(&self) -> bool {
        self.hooks.on_off()
    }

    /// Sets the on_off state to true and updates the off_wait_time and global_scene_control accordingly.
    /// If not initiated by LevelControl and LevelControl cluster is coupled, call the LevelControl coupling logic.
    fn set_on(
        &self,
        state: &mut OnOffState,
        level_control_initiated: bool,
        scene_apply: bool,
        ctx: impl HandlerContext,
    ) {
        if self.hooks.on_off() {
            return;
        }

        // 1.5.7.2. On Command
        // ... on receipt of the On command, a server SHALL set the OnOff attribute to TRUE.
        self.hooks.set_on_off(true);

        let lighting_attrs_updated = Self::update_attr_on(state);

        ctx.notify_attr_changed(self.endpoint_id, Self::CLUSTER.id, AttributeId::OnOff as _);
        // Scene-recall mutations transition *into* the recalled state,
        // so they must not trigger `SceneValid` drift-detection.
        if !scene_apply {
            self.notify_scenable_changed();
        }
        if lighting_attrs_updated {
            // `update_attr_on` may have forced OffWaitTime to 0 and GlobalSceneControl to TRUE
            ctx.notify_attr_changed(
                self.endpoint_id,
                Self::CLUSTER.id,
                AttributeId::OffWaitTime as _,
            );
            ctx.notify_attr_changed(
                self.endpoint_id,
                Self::CLUSTER.id,
                AttributeId::GlobalSceneControl as _,
            );
        }

        // LevelControl coupling logic defined in the spec
        if !level_control_initiated {
            if let Some(level_control_handler) = self.level_control_handler.lock(|h| h.get()) {
                level_control_handler.coupled_on_off_cluster_on_off_state_change(true);
            }
        }
    }

    // Updates Matter attributes when the state changes to On.
    // Returns true if attributes have been updated and hence Matter notification is required.
    fn update_attr_on(state: &mut OnOffState) -> bool {
        // Note: The OnTime, OffWaitTime and GlobalScenesControl attributes are only supported and must
        // be supported when the LIGHTING feature is enabled.
        // This configuration is ensured by the validate method upon initialisation.
        if Self::supports_feature(on_off::Feature::LIGHTING.bits()) {
            // 1.5.7.2. On Command
            // ... when the OnTime and OffWaitTime attributes are both supported, if the value of the
            // OnTime attribute is equal to 0, the server SHALL set the OffWaitTime attribute to 0.
            if state.on_time == 0 {
                state.off_wait_time = 0;
            }

            // 1.5.6.3. GlobalSceneControl Attribute
            // This attribute SHALL be set to TRUE after the reception of a command which causes the OnOff
            // attribute to be set to TRUE, such as a standard On command, a MoveToLevel(WithOnOff) command,
            // a RecallScene command or a OnWithRecallGlobalScene command.
            state.global_scene_control = true;

            return true;
        }
        false
    }

    /// Sets the on_off state to false.
    /// If a LevelControl cluster is coupled with this OnOff cluster and this command was not initiated by the
    /// LevelControl cluster, the coupled flow is initiated.
    /// In this case, the method will not set the on_off state to false and returns false.
    /// Otherwise, we set the on_off state to false and return true.
    /// The return boolean indicates if the on_off state has been set.
    fn set_off(
        &self,
        state: &mut OnOffState,
        level_control_initiated: bool,
        scene_apply: bool,
        ctx: impl HandlerContext,
    ) -> bool {
        if !self.hooks.on_off() {
            return true;
        }

        let on_time_updated = Self::update_attr_off(state);

        // LevelControl coupling logic defined in the spec
        let level_control_handler = self.level_control_handler.lock(|h| h.get());
        if let Some(level_control_handler) = level_control_handler {
            if !level_control_initiated {
                level_control_handler.coupled_on_off_cluster_on_off_state_change(false);

                if on_time_updated {
                    ctx.notify_attr_changed(
                        self.endpoint_id,
                        Self::CLUSTER.id,
                        AttributeId::OnOff as _,
                    );
                    // `update_attr_off` forced OnTime to 0
                    ctx.notify_attr_changed(
                        self.endpoint_id,
                        Self::CLUSTER.id,
                        AttributeId::OnTime as _,
                    );
                }

                // When calling the LevelControl with false (off), the levelControl cluster will call
                // back into the OnOff cluster to set the OnOff attribute to false when it is done.
                // Hence, we return without setting the on_off attribute.
                return false;
            }
        }

        // 1.5.7.1. Off Command
        // On receipt of the Off command, a server SHALL set the OnOff attribute to FALSE.
        self.hooks.set_on_off(false);
        ctx.notify_attr_changed(self.endpoint_id, Self::CLUSTER.id, AttributeId::OnOff as _);
        // See `set_on` for why this is gated by `scene_apply`.
        if !scene_apply {
            self.notify_scenable_changed();
        }
        if on_time_updated {
            // `update_attr_off` forced OnTime to 0
            ctx.notify_attr_changed(self.endpoint_id, Self::CLUSTER.id, AttributeId::OnTime as _);
        }

        true
    }

    // Update Matter attributes when the state changes to Off.
    // Returns true if attributes have been updated and hence Matter notification is required.
    fn update_attr_off(state: &mut OnOffState) -> bool {
        if Self::supports_feature(on_off::Feature::LIGHTING.bits()) && state.on_time != 0 {
            // 1.5.7.1. Off Command
            // ... when the OnTime attribute is supported, the server SHALL set the OnTime attribute to 0.
            state.on_time = 0;
            return true;
        }
        false
    }

    fn supports_feature(features: u32) -> bool {
        H::CLUSTER.feature_map & features != 0
    }

    // Updates the state of the state machine and Matter attributes to match the state of the physical device.
    // The state of the physical device is not modified.
    fn update(&self, state: &mut OnOffState, ctx: impl HandlerContext) {
        match self.on_off() {
            true => {
                if state.state == OnOffClusterState::On {
                    return;
                }

                state.state = OnOffClusterState::On;

                let lighting_attrs_updated = Self::update_attr_on(state);

                ctx.notify_attr_changed(
                    self.endpoint_id,
                    Self::CLUSTER.id,
                    AttributeId::OnOff as _,
                );
                if lighting_attrs_updated {
                    ctx.notify_attr_changed(
                        self.endpoint_id,
                        Self::CLUSTER.id,
                        AttributeId::OffWaitTime as _,
                    );
                    ctx.notify_attr_changed(
                        self.endpoint_id,
                        Self::CLUSTER.id,
                        AttributeId::GlobalSceneControl as _,
                    );
                }
            }
            false => {
                if state.state == OnOffClusterState::Off {
                    return;
                }

                state.state = OnOffClusterState::Off;

                let on_time_updated = Self::update_attr_off(state);

                ctx.notify_attr_changed(
                    self.endpoint_id,
                    Self::CLUSTER.id,
                    AttributeId::OnOff as _,
                );
                if on_time_updated {
                    ctx.notify_attr_changed(
                        self.endpoint_id,
                        Self::CLUSTER.id,
                        AttributeId::OnTime as _,
                    );
                }
            }
        }
    }

    async fn state_machine(&self, command: OnOffCommand, ctx: impl HandlerContext) {
        enum Outcome {
            Done,
            Continue,
            OffWithEffect {
                effect_variant: EffectVariantEnum,
                final_state: OnOffClusterState,
            },
            Delay,
        }

        loop {
            let outcome = self.with_state(|state| {
                match state.state {
                    OnOffClusterState::On => match command {
                        OnOffCommand::Off | OnOffCommand::Toggle => {
                            if self.set_off(state, false, false, &ctx) {
                                state.state = OnOffClusterState::Off;
                            }
                            Outcome::Done
                        }
                        OnOffCommand::CoupledClusterOff => {
                            self.set_off(state, true, false, &ctx);
                            state.state = OnOffClusterState::Off;
                            Outcome::Done
                        }
                        OnOffCommand::On | OnOffCommand::CoupledClusterOn => Outcome::Done,
                        OnOffCommand::OffWithEffect(effect) => {
                            // 1.5.7.4.3. Effect on Receipt
                            // On receipt of the OffWithEffect command the server SHALL check the value of the
                            // GlobalSceneControl attribute.
                            // If the GlobalSceneControl attribute is equal to TRUE, the server SHALL store its settings in its global
                            // scene then set the GlobalSceneControl attribute to FALSE...
                            // todo: store the GlobalSceneControl setting (true) in the global scene.
                            let gsc_changed = state.global_scene_control;
                            state.global_scene_control = false;
                            if gsc_changed {
                                ctx.notify_attr_changed(
                                    self.endpoint_id,
                                    Self::CLUSTER.id,
                                    AttributeId::GlobalSceneControl as _,
                                );
                            }

                            Outcome::OffWithEffect {
                                effect_variant: effect,
                                final_state: OnOffClusterState::Off,
                            }
                        }
                        OnOffCommand::OnWithTimedOff => {
                            state.state = OnOffClusterState::TimedOn;
                            Outcome::Continue
                        }
                        OnOffCommand::Update => {
                            self.update(state, &ctx);
                            Outcome::Done
                        }
                    },
                    OnOffClusterState::Off => match command {
                        OnOffCommand::Off
                        | OnOffCommand::OffWithEffect(_)
                        | OnOffCommand::CoupledClusterOff => Outcome::Done,
                        OnOffCommand::On | OnOffCommand::Toggle => {
                            state.state = OnOffClusterState::On;
                            self.set_on(state, false, false, &ctx);
                            Outcome::Done
                        }
                        OnOffCommand::CoupledClusterOn => {
                            state.state = OnOffClusterState::On;
                            self.set_on(state, true, false, &ctx);
                            Outcome::Done
                        }
                        OnOffCommand::OnWithTimedOff => {
                            state.state = OnOffClusterState::TimedOn;
                            Outcome::Continue
                        }
                        OnOffCommand::Update => {
                            self.update(state, &ctx);
                            Outcome::Done
                        }
                    },
                    OnOffClusterState::TimedOn => {
                        match command {
                            OnOffCommand::Off | OnOffCommand::Toggle => {
                                trace!("Got Off command from TimedOn state");
                                if self.set_off(state, false, false, &ctx) {
                                    state.state = OnOffClusterState::DelayedOff;
                                    Outcome::Continue
                                } else {
                                    // If set_off returns false, we brake and expect to be called again by the CoupledClusterOff command.
                                    Outcome::Done
                                }
                            }
                            OnOffCommand::CoupledClusterOff => {
                                self.set_off(state, true, false, &ctx);
                                state.state = OnOffClusterState::DelayedOff;
                                Outcome::Done
                            }
                            OnOffCommand::OffWithEffect(effect) => {
                                // 1.5.7.4.3. Effect on Receipt
                                // On receipt of the OffWithEffect command the server SHALL check the value of the
                                // GlobalSceneControl attribute.
                                // If the GlobalSceneControl attribute is equal to TRUE, the server SHALL store its settings in its global
                                // scene then set the GlobalSceneControl attribute to FALSE...
                                // todo: store the GlobalSceneControl setting (true) in the global scene.
                                let gsc_changed = state.global_scene_control;
                                state.global_scene_control = false;
                                if gsc_changed {
                                    ctx.notify_attr_changed(
                                        self.endpoint_id,
                                        Self::CLUSTER.id,
                                        AttributeId::GlobalSceneControl as _,
                                    );
                                }

                                Outcome::OffWithEffect {
                                    effect_variant: effect,
                                    final_state: OnOffClusterState::DelayedOff,
                                }
                            }
                            // 1.5.7.6.4. Effect on Receipt
                            // If the value of the OnOff attribute is equal to TRUE and the value of the OnTime attribute is
                            // greater than zero, the server SHALL decrement the value of the OnTime attribute. If the value of
                            // the OnTime attribute reaches 0, the server SHALL set the OffWaitTime and OnOff attributes to 0
                            // and FALSE, respectively.
                            OnOffCommand::On | OnOffCommand::OnWithTimedOff => {
                                if state.on_time > 0 {
                                    state.on_time -= 1;
                                    Outcome::Delay
                                } else {
                                    state.off_wait_time = 0;
                                    if self.set_off(state, false, false, &ctx) {
                                        state.state = OnOffClusterState::Off;
                                    }
                                    Outcome::Done
                                }
                            }
                            OnOffCommand::CoupledClusterOn => {
                                // This should not be reachable as the device would already be on so a change in the LevelControl cluster cannot cause the OnOff cluster to switch to On.
                                unreachable!("CoupledClusterOn should not be reachable in TimedOn state: device is already on")
                            }
                            OnOffCommand::Update => {
                                self.update(state, &ctx);
                                Outcome::Done
                            }
                        }
                    }
                    OnOffClusterState::DelayedOff => {
                        match command {
                            // 1.5.6.5. OffWaitTime Attribute
                            // This attribute specifies the length of time (in 1/10ths second) that the Off state SHALL be guarded to
                            // prevent another OnWithTimedOff command turning the server back to its On state.
                            OnOffCommand::On | OnOffCommand::Toggle => {
                                state.state = OnOffClusterState::On;
                                self.set_on(state, false, false, &ctx);
                                Outcome::Done
                            }
                            OnOffCommand::CoupledClusterOn => {
                                state.state = OnOffClusterState::On;
                                self.set_on(state, true, false, &ctx);
                                Outcome::Done
                            }
                            OnOffCommand::Off
                            | OnOffCommand::OffWithEffect(_)
                            | OnOffCommand::OnWithTimedOff
                            | OnOffCommand::CoupledClusterOff => {
                                // 1.5.7.6.4. Effect on Receipt
                                // If the value of the OnOff attribute is equal to FALSE and the value of the OffWaitTime attribute
                                // is greater than zero, the server SHALL decrement the value of the OffWaitTime attribute. If the
                                // value of the OffWaitTime attribute reaches 0, the server SHALL terminate the update.
                                if state.off_wait_time > 0 {
                                    state.off_wait_time -= 1;
                                    Outcome::Delay
                                } else {
                                    state.state = OnOffClusterState::Off;
                                    Outcome::Done
                                }
                            }
                            OnOffCommand::Update => {
                                self.update(state, &ctx);
                                Outcome::Done
                            }
                        }
                    }
                }
            });

            match outcome {
                Outcome::Done => break,
                Outcome::Continue => (),
                Outcome::Delay => embassy_time::Timer::after_millis(100).await,
                Outcome::OffWithEffect {
                    effect_variant,
                    final_state,
                } => {
                    self.hooks.handle_off_with_effect(effect_variant).await;

                    self.with_state(|state| {
                        // This is set to true because in this case we do not want to also run the effects from the LevelControl cluster.
                        let _ = self.set_off(state, true, false, &ctx);

                        state.state = final_state;
                    });

                    break;
                }
            }
        }
    }

    fn out_of_band_message(&self, message: OutOfBandMessage) {
        match message {
            OutOfBandMessage::Update => self.state_change_signal.signal(OnOffCommand::Update),
            OutOfBandMessage::On => self.state_change_signal.signal(OnOffCommand::On),
            OutOfBandMessage::Off => self.state_change_signal.signal(OnOffCommand::Off),
            OutOfBandMessage::Toggle => self.state_change_signal.signal(OnOffCommand::Toggle),
        }
    }

    // The method that should be used by coupled clusters to update the on_off state.
    pub(crate) fn coupled_cluster_set_on_off(&self, on: bool) {
        info!(
            "OnOffCluster: coupled_cluster_set_on_off: Setting on_off to {}",
            on
        );

        self.with_state(|state| {
            match on {
                true => {
                    if state.state == OnOffClusterState::DelayedOff {
                        warn!("LevelControl is trying to set OnOff to true while the OnOff cluster is in the guarded 'Delayed Off' state");
                        return;
                    }

                    self.state_change_signal
                        .signal(OnOffCommand::CoupledClusterOn);
                }
                false => self
                    .state_change_signal
                    .signal(OnOffCommand::CoupledClusterOff),
            }
        })
    }

    fn with_state<F, R>(&self, f: F) -> R
    where
        F: FnOnce(&mut OnOffState) -> R,
    {
        self.state.lock(|state| {
            let mut state = state.borrow_mut();

            f(&mut state)
        })
    }
}

impl<H: OnOffHooks, LH: LevelControlHooks> ClusterAsyncHandler for OnOffHandler<'_, H, LH> {
    #[doc = "The cluster-metadata corresponding to this handler trait."]
    const CLUSTER: Cluster<'static> = H::CLUSTER;

    fn dataver(&self) -> u32 {
        self.dataver.get()
    }

    fn dataver_changed(&self) {
        self.dataver.changed();
    }

    async fn run(&self, ctx: impl HandlerContext) -> Result<(), Error> {
        let mut hooks_fut = pin!(self.hooks.run(|message| self.out_of_band_message(message)));

        loop {
            let mut command = match select(
                &mut hooks_fut,
                self.state_change_signal.wait_signalled()
            ).await {
                Either::First(_) => panic!("OnOffHooks::run returned; implementers MUST not return. Implementations should loop forever or await core::future::pending::<()>()."),
                Either::Second(command) => command,
            };

            loop {
                match select3(
                    &mut hooks_fut,
                    self.state_machine(command, &ctx),
                    self.state_change_signal.wait_signalled(),
                )
                .await
                {
                    Either3::First(_) => panic!("OnOffHooks::run returned; implementers MUST not return. Implementations should loop forever or await core::future::pending::<()>()."),
                    Either3::Second(_) => break,
                    Either3::Third(new_command) => command = new_command,
                }
            }
        }
    }

    // Attribute accessors
    fn on_off(&self, _ctx: impl ReadContext) -> impl Future<Output = Result<bool, Error>> {
        ready(Ok(self.hooks.on_off()))
    }

    fn global_scene_control(
        &self,
        _ctx: impl ReadContext,
    ) -> impl Future<Output = Result<bool, Error>> {
        ready(Ok(self.with_state(|state| state.global_scene_control)))
    }

    fn on_time(&self, _ctx: impl ReadContext) -> impl Future<Output = Result<u16, Error>> {
        ready(Ok(self.with_state(|state| state.on_time)))
    }

    fn off_wait_time(&self, _ctx: impl ReadContext) -> impl Future<Output = Result<u16, Error>> {
        ready(Ok(self.with_state(|state| state.off_wait_time)))
    }

    fn start_up_on_off(
        &self,
        _ctx: impl ReadContext,
    ) -> impl Future<Output = Result<Nullable<StartUpOnOffEnum>, Error>> {
        ready(Ok(self.hooks.start_up_on_off()))
    }

    fn set_on_time(
        &self,
        ctx: impl WriteContext,
        value: u16,
    ) -> impl Future<Output = Result<(), Error>> {
        ready(self.with_state(|state| {
            state.on_time = value;
            ctx.notify_changed();
            Ok(())
        }))
    }

    fn set_off_wait_time(
        &self,
        ctx: impl WriteContext,
        value: u16,
    ) -> impl Future<Output = Result<(), Error>> {
        ready(self.with_state(|state| {
            state.off_wait_time = value;
            ctx.notify_changed();
            Ok(())
        }))
    }

    fn set_start_up_on_off(
        &self,
        ctx: impl WriteContext,
        value: Nullable<StartUpOnOffEnum>,
    ) -> impl Future<Output = Result<(), Error>> {
        ready(match self.hooks.set_start_up_on_off(value) {
            Ok(()) => {
                ctx.notify_changed();
                Ok(())
            }
            Err(e) => Err(e),
        })
    }

    // Commands
    fn handle_off(&self, _ctx: impl InvokeContext) -> impl Future<Output = Result<(), Error>> {
        ready({
            self.state_change_signal.signal(OnOffCommand::Off);
            Ok(())
        })
    }

    fn handle_on(&self, _ctx: impl InvokeContext) -> impl Future<Output = Result<(), Error>> {
        ready({
            self.state_change_signal.signal(OnOffCommand::On);
            Ok(())
        })
    }

    fn handle_toggle(&self, _ctx: impl InvokeContext) -> impl Future<Output = Result<(), Error>> {
        ready({
            self.state_change_signal.signal(OnOffCommand::Toggle);
            Ok(())
        })
    }

    fn handle_off_with_effect(
        &self,
        _ctx: impl InvokeContext,
        request: OffWithEffectRequest<'_>,
    ) -> impl Future<Output = Result<(), Error>> {
        ready('a: {
            if !Self::supports_feature(on_off::Feature::LIGHTING.bits()) {
                // This error is currently mapped to the IM status UnsupportedCommand.
                break 'a Err(ErrorCode::CommandNotFound.into());
            }

            let effect_variant = match request.effect_identifier() {
                Err(e) => break 'a Err(e),
                Ok(EffectIdentifierEnum::DelayedAllOff) => match request.effect_variant() {
                    Err(e) => break 'a Err(e),
                    // todo Impl TryFrom for DelayedAllOffEffectVariantEnum and remove this match.
                    Ok(0) => EffectVariantEnum::DelayedAllOff(
                        DelayedAllOffEffectVariantEnum::DelayedOffFastFade,
                    ),
                    Ok(1) => {
                        EffectVariantEnum::DelayedAllOff(DelayedAllOffEffectVariantEnum::NoFade)
                    }
                    Ok(2) => EffectVariantEnum::DelayedAllOff(
                        DelayedAllOffEffectVariantEnum::DelayedOffSlowFade,
                    ),
                    Ok(_) => break 'a Err(ErrorCode::Failure.into()),
                },
                Ok(EffectIdentifierEnum::DyingLight) => match request.effect_variant() {
                    Err(e) => break 'a Err(e),
                    // todo Impl TryFrom for DyingLightEffectVariantEnum and remove this match.
                    Ok(0) => EffectVariantEnum::DyingLight(
                        DyingLightEffectVariantEnum::DyingLightFadeOff,
                    ),
                    Ok(_) => break 'a Err(ErrorCode::Failure.into()),
                },
            };

            self.state_change_signal
                .signal(OnOffCommand::OffWithEffect(effect_variant));

            Ok(())
        })
    }

    fn handle_on_with_recall_global_scene(
        &self,
        _ctx: impl InvokeContext,
    ) -> impl Future<Output = Result<(), Error>> {
        ready(self.with_state(|state| {
            // 1.5.7.5.1. Effect on Receipt
            // On receipt of the OnWithRecallGlobalScene command, if the GlobalSceneControl attribute is equal
            // to TRUE, the server SHALL discard the command.
            if state.global_scene_control {
                return Ok(());
            }

            // If the GlobalSceneControl attribute is equal to FALSE, the Scene cluster server on the same endpoint
            // SHALL recall its global scene, updating the OnOff attribute accordingly. The OnOff server SHALL
            // then set the GlobalSceneControl attribute to TRUE.
            // Additionally, when the OnTime and OffWaitTime attributes are both supported, if the value of the
            // OnTime attribute is equal to 0, the server SHALL set the OffWaitTime attribute to 0.
            // todo Implement the above statement once the Scene cluster is implemented.
            // self.set_on(false);

            // This error is currently mapped to the IM status UnsupportedCommand.
            Err(ErrorCode::CommandNotFound.into())
        }))
    }

    fn handle_on_with_timed_off(
        &self,
        ctx: impl InvokeContext,
        request: OnWithTimedOffRequest<'_>,
    ) -> impl Future<Output = Result<(), Error>> {
        ready(match request.on_off_control() {
            Err(e) => Err(e),
            // 1.5.7.6.4. Effect on Receipt
            // On receipt of this command, if the AcceptOnlyWhenOn sub-field of the OnOffControl field is set to 1,
            // and the value of the OnOff attribute is equal to FALSE, the command SHALL be discarded.
            Ok(ctrl)
                if ctrl.contains(OnOffControlBitmap::ACCEPT_ONLY_WHEN_ON)
                    && !self.hooks.on_off() =>
            {
                Ok(())
            }
            Ok(_) => self.with_state(|state| {
                // If the value of the OffWaitTime attribute is greater than zero and the value of the OnOff attribute is
                // equal to FALSE, then the server SHALL set the OffWaitTime attribute to the minimum of the
                // OffWaitTime attribute and the value specified in the OffWaitTime field.
                if state.off_wait_time > 0 && !self.hooks.on_off() {
                    let new_off_wait_time = state.off_wait_time.min(request.off_wait_time()?);
                    if new_off_wait_time != state.off_wait_time {
                        state.off_wait_time = new_off_wait_time;
                        ctx.notify_own_attr_changed(AttributeId::OffWaitTime as _);
                    }
                }
                // In all other cases, the server SHALL set the OnTime attribute to the maximum of the OnTime
                // attribute and the value specified in the OnTime field, set the OffWaitTime attribute to the value
                // specified in the OffWaitTime field and set the OnOff attribute to TRUE.
                else {
                    let new_on_time = state.on_time.max(request.on_time()?);
                    let new_off_wait_time = request.off_wait_time()?;
                    let on_time_changed = new_on_time != state.on_time;
                    let off_wait_time_changed = new_off_wait_time != state.off_wait_time;
                    state.on_time = new_on_time;
                    state.off_wait_time = new_off_wait_time;
                    if on_time_changed || off_wait_time_changed {
                        if on_time_changed {
                            ctx.notify_own_attr_changed(AttributeId::OnTime as _);
                        }
                        if off_wait_time_changed {
                            ctx.notify_own_attr_changed(AttributeId::OffWaitTime as _);
                        }
                    }
                    self.set_on(state, false, false, &ctx);
                }

                // If the values of the OnTime and OffWaitTime attributes are both not equal to 0xFFFF, the server
                // SHALL then update these attributes every 1/10th second until both the OnTime and OffWaitTime
                // attributes are equal to 0, as follows:
                if state.on_time == 0xFFFF && state.off_wait_time == 0xFFFF {
                    return Ok(());
                }

                self.state_change_signal
                    .signal(OnOffCommand::OnWithTimedOff);

                Ok(())
            }),
        })
    }
}

pub trait OnOffHooks {
    const CLUSTER: Cluster<'static>;

    // Get the current device on/off state. This value SHALL be persisted across reboots.
    fn on_off(&self) -> bool;
    // todo should we allow this to return an error? If so, we'd need to know if the state has changed even if error occurs.
    // todo make `async`
    // Switch the device to the `on` value and persist this setting.
    fn set_on_off(&self, on: bool);

    // Get the start_up_on_off attribute. This value SHALL be persisted across reboots.
    fn start_up_on_off(&self) -> Nullable<StartUpOnOffEnum>;
    // Set the start_up_on_off attribute. This value SHALL be persisted across reboots.
    fn set_start_up_on_off(&self, value: Nullable<StartUpOnOffEnum>) -> Result<(), Error>;

    async fn handle_off_with_effect(&self, effect: EffectVariantEnum);

    /// Background task for out-of-band notifications to the handler.
    ///
    /// This future MUST NOT return. Implementers should either loop forever or await
    /// core::future::pending::<()>(), so the SDK's task does not observe a completed future.
    ///
    /// # Panics
    /// The SDK will panic if this method returns.
    async fn run<F: Fn(OutOfBandMessage)>(&self, _notify: F) {
        core::future::pending::<()>().await
    }
}

impl<T> OnOffHooks for &T
where
    T: OnOffHooks,
{
    const CLUSTER: Cluster<'static> = T::CLUSTER;

    fn on_off(&self) -> bool {
        (*self).on_off()
    }

    fn set_on_off(&self, on: bool) {
        (*self).set_on_off(on)
    }

    fn start_up_on_off(&self) -> Nullable<StartUpOnOffEnum> {
        (*self).start_up_on_off()
    }

    fn set_start_up_on_off(&self, value: Nullable<StartUpOnOffEnum>) -> Result<(), Error> {
        (*self).set_start_up_on_off(value)
    }

    fn handle_off_with_effect(&self, effect: EffectVariantEnum) -> impl Future<Output = ()> {
        (*self).handle_off_with_effect(effect)
    }

    fn run<F: Fn(OutOfBandMessage)>(&self, notify: F) -> impl Future<Output = ()> {
        (*self).run(notify)
    }
}

/// This is a phantom type for when the OnOff cluster is not coupled with a LevelControl cluster.
/// This type should only be used for annotations and not for actual LevelControl functionality.
/// All methods will panic.
pub struct NoLevelControl;

impl LevelControlHooks for NoLevelControl {
    const MIN_LEVEL: u8 = 1;
    const MAX_LEVEL: u8 = 1;
    const FASTEST_RATE: u8 = 1;
    const CLUSTER: Cluster<'static> = level_control::FULL_CLUSTER;

    fn set_device_level(&self, _: u8) -> Result<Option<u8>, ()> {
        panic!("NoLevelControl: set_device_level called unexpectedly - this phantom type should not be used for LevelControl functionality")
    }

    fn current_level(&self) -> Option<u8> {
        panic!("NoLevelControl: current_level called unexpectedly - this phantom type should not be used for LevelControl functionality")
    }

    fn set_current_level(&self, _level: Option<u8>) {
        panic!("NoLevelControl: set_current_level called unexpectedly - this phantom type should not be used for LevelControl functionality")
    }
}

/// Scenes Management integration for the OnOff cluster. The only
/// scenable attribute is `OnOff`; apply routes through `set_on` /
/// `set_off` rather than an attribute write.
impl<H, LH> SceneClusterHandler for OnOffHandler<'_, H, LH>
where
    H: OnOffHooks,
    LH: LevelControlHooks,
{
    const CLUSTER_ID: ClusterId = FULL_CLUSTER.id;

    fn endpoint_id(&self) -> EndptId {
        self.endpoint_id
    }

    fn is_scenable_attribute(attribute_id: AttrId) -> bool {
        attribute_id == AttributeId::OnOff as AttrId
    }

    fn capture<P: TLVBuilderParent>(
        &self,
        avp_array: AttributeValuePairStructArrayBuilder<P>,
    ) -> Result<AttributeValuePairStructArrayBuilder<P>, Error> {
        let v = self.hooks.on_off();
        avp_array.push_u8(AttributeId::OnOff as _, v as u8)
    }

    async fn apply<C: HandlerContext>(
        &self,
        ctx: &C,
        avp_list: &TLVArray<'_, AttributeValuePairStruct<'_>>,
        _transition_time_ms: u32,
    ) -> Result<(), Error> {
        for avp in avp_list.iter() {
            let avp = avp?;
            if avp.attribute_id()? != AttributeId::OnOff as _ {
                continue;
            }
            let Some(value) = avp.value_unsigned_8()? else {
                continue;
            };
            // OnOff scene apply is a discrete transition (no per-scene
            // fade), so mutate inline via `set_on` / `set_off` rather
            // than the deferred `state_change_signal` path — Scenes
            // then calls `remember_current` to restore `SceneValid` in
            // the same await. `level_control_initiated=true` suppresses
            // OnOff↔LC coupling, since the scene blob carries its own
            // `CurrentLevel` AVP that LevelControl applies directly;
            // `scene_apply=true` suppresses drift-invalidation.
            self.with_state(|state| {
                if value != 0 {
                    self.set_on(state, true, true, ctx);
                } else {
                    self.set_off(state, true, true, ctx);
                }
            });
            return Ok(());
        }
        Ok(())
    }
}

pub mod test {
    use embassy_time::{Duration, Timer};

    use crate::dm::clusters::app::on_off::{
        EffectVariantEnum, OnOffHooks, OutOfBandMessage, StartUpOnOffEnum,
    };
    use crate::dm::clusters::decl::on_off as on_off_cluster;
    use crate::dm::clusters::decl::on_off::Feature;
    use crate::dm::Cluster;
    use crate::error::Error;
    use crate::tlv::Nullable;
    use crate::utils::cell::RefCell;
    use crate::utils::sync::blocking::Mutex;
    use crate::with;

    struct TestOnOffState {
        on_off: bool,
        start_up_on_off: Option<StartUpOnOffEnum>,
    }

    impl TestOnOffState {
        const fn new() -> Self {
            Self {
                on_off: false,
                start_up_on_off: None,
            }
        }
    }

    /// This is a basic implementation of the OnOff device logic, an implementer of OnOffHooks, used for testing.
    // TODO:
    // #[derive(Clone, Debug)]
    // #[cfg_attr(feature = "defmt", derive(defmt::Format))]
    pub struct TestOnOffDeviceLogic {
        state: Mutex<RefCell<TestOnOffState>>,
        toggle_periodically: bool,
    }

    impl TestOnOffDeviceLogic {
        pub const fn new(toggle_periodically: bool) -> Self {
            Self {
                state: Mutex::new(RefCell::new(TestOnOffState::new())),
                toggle_periodically,
            }
        }
    }

    impl OnOffHooks for TestOnOffDeviceLogic {
        // The On/Off Light device type (Matter Device Library) requires
        // the `LT` (Lighting) feature on the OnOff cluster, which gates the
        // `GlobalSceneControl`/`OnTime`/`OffWaitTime`/`StartUpOnOff` attributes
        // and the `OffWithEffect`/`OnWithRecallGlobalScene`/`OnWithTimedOff`
        // commands. The library `OnOffHandler` already implements all of
        // these — we just opt in via the cluster metadata. Matches the
        // FeatureMap conformance check in `TC_DeviceConformance::test_TC_IDM_10_5`.
        const CLUSTER: Cluster<'static> = on_off_cluster::FULL_CLUSTER
            .with_revision(6)
            .with_features(Feature::LIGHTING.bits())
            .with_attrs(with!(
                required;
                on_off_cluster::AttributeId::OnOff
                    | on_off_cluster::AttributeId::GlobalSceneControl
                    | on_off_cluster::AttributeId::OnTime
                    | on_off_cluster::AttributeId::OffWaitTime
                    | on_off_cluster::AttributeId::StartUpOnOff
            ))
            .with_cmds(with!(
                on_off_cluster::CommandId::Off
                    | on_off_cluster::CommandId::On
                    | on_off_cluster::CommandId::Toggle
                    | on_off_cluster::CommandId::OffWithEffect
                    | on_off_cluster::CommandId::OnWithRecallGlobalScene
                    | on_off_cluster::CommandId::OnWithTimedOff
            ));

        fn on_off(&self) -> bool {
            self.state.lock(|state| state.borrow().on_off)
        }

        fn set_on_off(&self, on: bool) {
            self.state.lock(|state| state.borrow_mut().on_off = on);
        }

        fn start_up_on_off(&self) -> Nullable<StartUpOnOffEnum> {
            match self.state.lock(|state| state.borrow().start_up_on_off) {
                Some(value) => Nullable::some(value),
                None => Nullable::none(),
            }
        }

        fn set_start_up_on_off(&self, value: Nullable<StartUpOnOffEnum>) -> Result<(), Error> {
            self.state
                .lock(|state| state.borrow_mut().start_up_on_off = value.into_option());
            Ok(())
        }

        async fn handle_off_with_effect(&self, _effect: EffectVariantEnum) {
            // no effect
        }

        async fn run<F: Fn(OutOfBandMessage)>(&self, notify: F) {
            if self.toggle_periodically {
                loop {
                    // In a real example we wait for physical interaction.
                    Timer::after(Duration::from_secs(5)).await;
                    info!("Emulation: out of band toggle request");
                    notify(OutOfBandMessage::Toggle);
                }
            } else {
                core::future::pending::<()>().await
            }
        }
    }
}