diff --git a/crates/hue/src/api/grouped_light.rs b/crates/hue/src/api/grouped_light.rs index 73bc1301..6dee0b56 100644 --- a/crates/hue/src/api/grouped_light.rs +++ b/crates/hue/src/api/grouped_light.rs @@ -47,6 +47,27 @@ impl GroupedLight { } } +#[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")] @@ -59,6 +80,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 { @@ -103,6 +126,11 @@ impl GroupedLightUpdate { ..self } } + + #[must_use] + pub const fn with_dynamics(self, dynamics: Option) -> Self { + Self { dynamics, ..self } + } } /* conversion from v1 api */ @@ -113,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 2ccdb691..93268069 100644 --- a/crates/hue/src/api/light.rs +++ b/crates/hue/src/api/light.rs @@ -449,16 +449,27 @@ 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 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 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 { @@ -688,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 { @@ -698,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, } } } 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..28a82336 100644 --- a/src/backend/z2m/backend_event.rs +++ b/src/backend/z2m/backend_event.rs @@ -92,11 +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_color_xy(upd.color.map(|col| col.xy)) + .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