From 783fd90cb92bd6948b2b8dab236d6a2e22a3ca3d Mon Sep 17 00:00:00 2001 From: Christian Duvholt Date: Sun, 25 May 2025 19:21:29 +0200 Subject: [PATCH 1/4] Map out Light/GroupedLight dynamics type --- crates/hue/src/api/grouped_light.rs | 8 ++++++++ crates/hue/src/api/light.rs | 6 +----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/crates/hue/src/api/grouped_light.rs b/crates/hue/src/api/grouped_light.rs index 73bc1301..68345eb2 100644 --- a/crates/hue/src/api/grouped_light.rs +++ b/crates/hue/src/api/grouped_light.rs @@ -47,6 +47,12 @@ impl GroupedLight { } } +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct GroupedLightDynamicsUpdate { + #[serde(skip_serializing_if = "Option::is_none")] + pub duration: Option, +} + #[derive(Debug, Serialize, Deserialize, Clone, Default)] pub struct GroupedLightUpdate { #[serde(skip_serializing_if = "Option::is_none")] @@ -59,6 +65,8 @@ pub struct GroupedLightUpdate { pub color_temperature: Option, #[serde(skip_serializing_if = "Option::is_none")] pub owner: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub dynamics: Option, } impl GroupedLightUpdate { diff --git a/crates/hue/src/api/light.rs b/crates/hue/src/api/light.rs index 2ccdb691..c9b60190 100644 --- a/crates/hue/src/api/light.rs +++ b/crates/hue/src/api/light.rs @@ -451,14 +451,10 @@ pub struct LightDynamics { #[derive(Debug, Serialize, Deserialize, Clone)] pub struct LightDynamicsUpdate { - #[serde(skip_serializing_if = "Option::is_none")] - pub status: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub status_values: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub speed: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub speed_valid: Option, + pub duration: Option, } impl Default for LightDynamics { From 16d772588934c50ddc23a177abbc140e7a049e71 Mon Sep 17 00:00:00 2001 From: Christian Duvholt Date: Sun, 25 May 2025 21:30:39 +0200 Subject: [PATCH 2/4] Set transition time on device updates --- crates/z2m/src/convert.rs | 5 +++++ crates/z2m/src/update.rs | 5 +++++ src/backend/z2m/backend_event.rs | 7 ++++++- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/crates/z2m/src/convert.rs b/crates/z2m/src/convert.rs index 22252c85..daf245f3 100644 --- a/crates/z2m/src/convert.rs +++ b/crates/z2m/src/convert.rs @@ -190,5 +190,10 @@ impl From<&GroupedLightUpdate> for DeviceUpdate { .with_brightness(upd.dimming.map(|dim| dim.brightness / 100.0 * 254.0)) .with_color_temp(upd.color_temperature.and_then(|ct| ct.mirek)) .with_color_xy(upd.color.map(|col| col.xy)) + .with_transition( + upd.dynamics + .as_ref() + .and_then(|d| d.duration.map(|duration| f64::from(duration) / 1000.0)), + ) } } diff --git a/crates/z2m/src/update.rs b/crates/z2m/src/update.rs index 7c8d768a..58d08ba8 100644 --- a/crates/z2m/src/update.rs +++ b/crates/z2m/src/update.rs @@ -122,6 +122,11 @@ impl DeviceUpdate { ..self } } + + #[must_use] + pub fn with_transition(self, transition: Option) -> Self { + Self { transition, ..self } + } } #[derive(Copy, Debug, Serialize, Deserialize, Clone)] diff --git a/src/backend/z2m/backend_event.rs b/src/backend/z2m/backend_event.rs index 8fe44076..81cbc4d3 100644 --- a/src/backend/z2m/backend_event.rs +++ b/src/backend/z2m/backend_event.rs @@ -96,7 +96,12 @@ impl Z2mBackend { .with_state(upd.on.map(|on| on.on)) .with_brightness(upd.dimming.map(|dim| dim.brightness / 100.0 * 254.0)) .with_color_temp(upd.color_temperature.and_then(|ct| ct.mirek)) - .with_color_xy(upd.color.map(|col| col.xy)); + .with_color_xy(upd.color.map(|col| col.xy)) + .with_transition( + upd.dynamics + .as_ref() + .and_then(|d| d.duration.map(|duration| f64::from(duration) / 1000.0)), + ); // We don't want to send gradient updates twice, but if hue // effects are not supported for this light, this is the best From 8b74d6dd102449f9c6854e871f2353f250ee149f Mon Sep 17 00:00:00 2001 From: Christian Duvholt Date: Tue, 27 May 2025 19:25:36 +0200 Subject: [PATCH 3/4] Implement transition support for legacy v1 api --- crates/hue/src/api/grouped_light.rs | 26 +++++++++++++++++++++++++- crates/hue/src/api/light.rs | 26 +++++++++++++++++++++++++- crates/hue/src/api/mod.rs | 3 ++- crates/hue/src/legacy_api.rs | 3 +++ 4 files changed, 55 insertions(+), 3 deletions(-) diff --git a/crates/hue/src/api/grouped_light.rs b/crates/hue/src/api/grouped_light.rs index 68345eb2..6dee0b56 100644 --- a/crates/hue/src/api/grouped_light.rs +++ b/crates/hue/src/api/grouped_light.rs @@ -47,12 +47,27 @@ impl GroupedLight { } } -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Default, Serialize, Deserialize, Clone)] pub struct GroupedLightDynamicsUpdate { #[serde(skip_serializing_if = "Option::is_none")] pub duration: Option, } +impl GroupedLightDynamicsUpdate { + #[must_use] + pub fn new() -> Self { + Self::default() + } + + #[must_use] + pub fn with_duration(self, duration: Option>) -> Self { + Self { + duration: duration.map(Into::into), + ..self + } + } +} + #[derive(Debug, Serialize, Deserialize, Clone, Default)] pub struct GroupedLightUpdate { #[serde(skip_serializing_if = "Option::is_none")] @@ -111,6 +126,11 @@ impl GroupedLightUpdate { ..self } } + + #[must_use] + pub const fn with_dynamics(self, dynamics: Option) -> Self { + Self { dynamics, ..self } + } } /* conversion from v1 api */ @@ -121,5 +141,9 @@ impl From<&ApiLightStateUpdate> for GroupedLightUpdate { .with_brightness(upd.bri.map(|b| f64::from(b) / 2.54)) .with_color_xy(upd.xy.map(XY::from)) .with_color_temperature(upd.ct) + .with_dynamics( + upd.transitiontime + .map(|t| GroupedLightDynamicsUpdate::new().with_duration(Some(t * 100))), + ) } } diff --git a/crates/hue/src/api/light.rs b/crates/hue/src/api/light.rs index c9b60190..93268069 100644 --- a/crates/hue/src/api/light.rs +++ b/crates/hue/src/api/light.rs @@ -449,7 +449,7 @@ pub struct LightDynamics { pub speed_valid: bool, } -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, Default)] pub struct LightDynamicsUpdate { #[serde(skip_serializing_if = "Option::is_none")] pub speed: Option, @@ -457,6 +457,21 @@ pub struct LightDynamicsUpdate { pub duration: Option, } +impl LightDynamicsUpdate { + #[must_use] + pub fn new() -> Self { + Self::default() + } + + #[must_use] + pub fn with_duration(self, duration: Option>) -> Self { + Self { + duration: duration.map(Into::into), + ..self + } + } +} + impl Default for LightDynamics { fn default() -> Self { Self { @@ -684,6 +699,11 @@ impl LightUpdate { pub fn with_gradient(self, gradient: Option) -> Self { Self { gradient, ..self } } + + #[must_use] + pub fn with_dynamics(self, dynamics: Option) -> Self { + Self { dynamics, ..self } + } } impl From<&ApiLightStateUpdate> for LightUpdate { @@ -694,6 +714,10 @@ impl From<&ApiLightStateUpdate> for LightUpdate { .with_color_temperature(upd.ct) .with_color_hs(upd.hs.map(Into::into)) .with_color_xy(upd.xy.map(Into::into)) + .with_dynamics( + upd.transitiontime + .map(|t| LightDynamicsUpdate::new().with_duration(Some(t * 100))), + ) } } diff --git a/crates/hue/src/api/mod.rs b/crates/hue/src/api/mod.rs index 7edf737a..81a30b24 100644 --- a/crates/hue/src/api/mod.rs +++ b/crates/hue/src/api/mod.rs @@ -382,7 +382,8 @@ impl<'a> V1Reply<'a> { self.add_option("on", upd.on)? .add_option("bri", upd.bri)? .add_option("xy", upd.xy)? - .add_option("ct", upd.ct) + .add_option("ct", upd.ct)? + .add_option("transitiontime", upd.transitiontime) } pub fn add(mut self, name: &'a str, value: T) -> HueResult { diff --git a/crates/hue/src/legacy_api.rs b/crates/hue/src/legacy_api.rs index dce7b632..4a892544 100644 --- a/crates/hue/src/legacy_api.rs +++ b/crates/hue/src/legacy_api.rs @@ -484,6 +484,8 @@ pub struct ApiLightStateUpdate { pub ct: Option, #[serde(skip_serializing_if = "Option::is_none", flatten)] pub hs: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub transitiontime: Option, } #[derive(Debug, Serialize, Deserialize)] @@ -519,6 +521,7 @@ impl From for ApiLightStateUpdate { xy: action.color.map(|col| col.xy.into()), ct: action.color_temperature.and_then(|ct| ct.mirek), hs: None, + transitiontime: None, } } } From 875317f258596bdac53b0be85c4f51792e1a1be6 Mon Sep 17 00:00:00 2001 From: Christian Duvholt Date: Tue, 27 May 2025 22:25:18 +0200 Subject: [PATCH 4/4] Add default transition for brightness and color --- src/backend/z2m/backend_event.rs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/backend/z2m/backend_event.rs b/src/backend/z2m/backend_event.rs index 81cbc4d3..28a82336 100644 --- a/src/backend/z2m/backend_event.rs +++ b/src/backend/z2m/backend_event.rs @@ -92,16 +92,23 @@ impl Z2mBackend { drop(lock); /* step 1: send generic light update */ + let transition = upd + .dynamics + .as_ref() + .and_then(|d| d.duration.map(|duration| f64::from(duration) / 1000.0)) + .or_else(|| { + if upd.dimming.is_some() || upd.color_temperature.is_some() || upd.color.is_some() { + Some(0.4) + } else { + None + } + }); let mut payload = DeviceUpdate::default() .with_state(upd.on.map(|on| on.on)) .with_brightness(upd.dimming.map(|dim| dim.brightness / 100.0 * 254.0)) .with_color_temp(upd.color_temperature.and_then(|ct| ct.mirek)) .with_color_xy(upd.color.map(|col| col.xy)) - .with_transition( - upd.dynamics - .as_ref() - .and_then(|d| d.duration.map(|duration| f64::from(duration) / 1000.0)), - ); + .with_transition(transition); // We don't want to send gradient updates twice, but if hue // effects are not supported for this light, this is the best