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;
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum OutOfBandMessage {
Update,
On,
Off,
Toggle,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum EffectVariantEnum {
DelayedAllOff(DelayedAllOffEffectVariantEnum),
DyingLight(DyingLightEffectVariantEnum),
}
#[derive(Clone, Copy, PartialEq)]
enum OnOffClusterState {
On,
Off,
TimedOn,
DelayedOff,
}
enum OnOffCommand {
Off,
On,
Toggle,
OffWithEffect(EffectVariantEnum),
OnWithTimedOff,
CoupledClusterOn,
CoupledClusterOff,
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,
}
}
}
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>>>>,
scene_invalidator: Mutex<Cell<Option<&'a dyn SceneInvalidator>>>,
state: Mutex<RefCell<OnOffState>>,
state_change_signal: Signal<Option<OnOffCommand>>,
}
impl<H: OnOffHooks> OnOffHandler<'_, H, NoLevelControl> {
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> {
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),
}
}
fn validate(&self) {
if Self::CLUSTER.revision != 6 {
panic!(
"OnOff validation: incorrect version number: expected 6 got {}",
Self::CLUSTER.revision
);
}
if Self::CLUSTER.attribute(AttributeId::OnOff as _).is_none() {
panic!("OnOff validation: missing required attribute: OnOff");
}
if Self::CLUSTER.command(CommandId::Off as _).is_none() {
panic!("OnOff validation: missing required command: Off");
}
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")
}
}
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")
}
}
pub fn init(&self, level_control_handler: Option<&'a LevelControlHandler<'a, LH, H>>) {
self.level_control_handler
.lock(|h| h.set(level_control_handler));
self.validate();
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()),
}
}
}
pub const fn adapt(self) -> HandlerAsyncAdaptor<Self> {
HandlerAsyncAdaptor(self)
}
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);
}
}
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),
}
}
pub fn on_off(&self) -> bool {
self.hooks.on_off()
}
fn set_on(
&self,
state: &mut OnOffState,
level_control_initiated: bool,
scene_apply: bool,
ctx: impl HandlerContext,
) {
if self.hooks.on_off() {
return;
}
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 _);
if !scene_apply {
self.notify_scenable_changed();
}
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 _,
);
}
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);
}
}
}
fn update_attr_on(state: &mut OnOffState) -> bool {
if Self::supports_feature(on_off::Feature::LIGHTING.bits()) {
if state.on_time == 0 {
state.off_wait_time = 0;
}
state.global_scene_control = true;
return true;
}
false
}
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);
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 _,
);
ctx.notify_attr_changed(
self.endpoint_id,
Self::CLUSTER.id,
AttributeId::OnTime as _,
);
}
return false;
}
}
self.hooks.set_on_off(false);
ctx.notify_attr_changed(self.endpoint_id, Self::CLUSTER.id, AttributeId::OnOff as _);
if !scene_apply {
self.notify_scenable_changed();
}
if on_time_updated {
ctx.notify_attr_changed(self.endpoint_id, Self::CLUSTER.id, AttributeId::OnTime as _);
}
true
}
fn update_attr_off(state: &mut OnOffState) -> bool {
if Self::supports_feature(on_off::Feature::LIGHTING.bits()) && state.on_time != 0 {
state.on_time = 0;
return true;
}
false
}
fn supports_feature(features: u32) -> bool {
H::CLUSTER.feature_map & features != 0
}
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) => {
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 {
Outcome::Done
}
}
OnOffCommand::CoupledClusterOff => {
self.set_off(state, true, false, &ctx);
state.state = OnOffClusterState::DelayedOff;
Outcome::Done
}
OnOffCommand::OffWithEffect(effect) => {
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,
}
}
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 => {
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 {
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 => {
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| {
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),
}
}
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,
}
}
}
}
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),
})
}
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()) {
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),
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),
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| {
if state.global_scene_control {
return Ok(());
}
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),
Ok(ctrl)
if ctrl.contains(OnOffControlBitmap::ACCEPT_ONLY_WHEN_ON)
&& !self.hooks.on_off() =>
{
Ok(())
}
Ok(_) => self.with_state(|state| {
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 _);
}
}
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 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>;
fn on_off(&self) -> bool;
fn set_on_off(&self, on: bool);
fn start_up_on_off(&self) -> Nullable<StartUpOnOffEnum>;
fn set_start_up_on_off(&self, value: Nullable<StartUpOnOffEnum>) -> Result<(), Error>;
async fn handle_off_with_effect(&self, effect: EffectVariantEnum);
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)
}
}
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")
}
}
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;
};
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,
}
}
}
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 {
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) {
}
async fn run<F: Fn(OutOfBandMessage)>(&self, notify: F) {
if self.toggle_periodically {
loop {
Timer::after(Duration::from_secs(5)).await;
info!("Emulation: out of band toggle request");
notify(OutOfBandMessage::Toggle);
}
} else {
core::future::pending::<()>().await
}
}
}
}