Skip to content

Commit e846afd

Browse files
committed
Parse new z2m field philips_raw and use it update light state
This let's use both remove some old state hacks and ensure that Bifrost's state is up to date with Hue changes from z2m. It also let's us parse the raw update directly instead of relying on z2m to do this correctly.
1 parent 8c8a547 commit e846afd

9 files changed

Lines changed: 152 additions & 52 deletions

File tree

Cargo.lock

Lines changed: 4 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/hue/src/api/light.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,12 @@ impl AddAssign<&LightUpdate> for Light {
238238
effects.status = light_effect;
239239
}
240240
}
241+
242+
if let Some(upd) = &upd.timed_effects {
243+
if let Some(timed_effects) = &mut self.timed_effects {
244+
timed_effects.status = upd.effect.unwrap_or(LightTimedEffect::NoEffect);
245+
}
246+
}
241247
}
242248
}
243249

@@ -589,6 +595,22 @@ pub struct LightEffectsV2Update {
589595
pub status: Option<Value>,
590596
}
591597

598+
impl LightEffectsV2Update {
599+
pub fn no_effect() -> Self {
600+
Self {
601+
action: Some(LightEffectActionUpdate {
602+
effect: Some(LightEffect::NoEffect),
603+
parameters: LightEffectParameters {
604+
color: None,
605+
color_temperature: None,
606+
speed: None,
607+
},
608+
}),
609+
status: None,
610+
}
611+
}
612+
}
613+
592614
impl AddAssign<&LightEffectsV2Update> for LightEffectsV2 {
593615
fn add_assign(&mut self, upd: &LightEffectsV2Update) {
594616
let light_effect = upd
@@ -661,6 +683,15 @@ pub struct LightTimedEffectsUpdate {
661683
pub duration: Option<u32>,
662684
}
663685

686+
impl LightTimedEffectsUpdate {
687+
pub fn no_effect() -> Self {
688+
Self {
689+
effect: Some(LightTimedEffect::NoEffect),
690+
duration: None,
691+
}
692+
}
693+
}
694+
664695
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
665696
pub struct LightUpdate {
666697
#[serde(skip_serializing_if = "Option::is_none")]

crates/hue/src/clamp.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ pub trait Clamp {
22
fn unit_to_u8_clamped(self) -> u8;
33
fn unit_to_u8_clamped_light(self) -> u8;
44
fn unit_from_u8(value: u8) -> Self;
5+
fn unit_from_u8_light(value: u8) -> Self;
56
}
67

78
impl Clamp for f32 {
@@ -18,6 +19,10 @@ impl Clamp for f32 {
1819
fn unit_from_u8(value: u8) -> Self {
1920
Self::from(value) / 255.0
2021
}
22+
23+
fn unit_from_u8_light(value: u8) -> Self {
24+
Self::from(value) / 254.0
25+
}
2126
}
2227

2328
impl Clamp for f64 {
@@ -34,6 +39,10 @@ impl Clamp for f64 {
3439
fn unit_from_u8(value: u8) -> Self {
3540
Self::from(value) / 255.0
3641
}
42+
43+
fn unit_from_u8_light(value: u8) -> Self {
44+
Self::from(value) / 254.0
45+
}
3746
}
3847

3948
#[cfg(test)]

crates/hue/src/zigbee/composite.rs

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,18 @@ use byteorder::{LittleEndian as LE, ReadBytesExt, WriteBytesExt};
55
use packed_struct::derive::{PackedStruct, PrimitiveEnum_u8};
66
use packed_struct::{PackedStruct, PackedStructSlice, PrimitiveEnum};
77

8-
use crate::api::{LightEffect, LightGradientMode, LightTimedEffect};
8+
use crate::api::{
9+
ColorUpdate, LightEffect, LightEffectActionUpdate, LightEffectParameters, LightEffectsV2Update,
10+
LightGradientMode, LightGradientPoint, LightGradientUpdate, LightTimedEffect,
11+
LightTimedEffectsUpdate, LightUpdate, On,
12+
};
13+
use crate::clamp::Clamp;
914
use crate::effect_duration::EffectDuration;
1015
use crate::error::{HueError, HueResult};
1116
use crate::flags::TakeFlag;
1217
use crate::xy::XY;
1318

14-
#[derive(PrimitiveEnum_u8, Debug, Copy, Clone)]
19+
#[derive(PrimitiveEnum_u8, Debug, Copy, Clone, PartialEq, Eq)]
1520
pub enum EffectType {
1621
NoEffect = 0x00,
1722
Candle = 0x01,
@@ -396,6 +401,69 @@ impl HueZigbeeUpdate {
396401
}
397402
}
398403

404+
impl From<HueZigbeeUpdate> for LightUpdate {
405+
fn from(hz: HueZigbeeUpdate) -> Self {
406+
let mut upd = Self::new()
407+
.with_on(hz.onoff.map(|o| On::new(o > 0)))
408+
.with_brightness(hz.brightness.map(|b| (b as f64) / 254.0 * 100.0))
409+
.with_color_temperature(hz.color_mirek)
410+
.with_color_xy(hz.color_xy);
411+
412+
if let Some(effect_type) = hz.effect_type {
413+
if let Some(light_effect) = LightEffect::ALL.iter().copied().find(|&e| {
414+
let e: EffectType = e.into();
415+
e == effect_type
416+
}) {
417+
let action = Some(LightEffectActionUpdate {
418+
effect: Some(light_effect),
419+
parameters: LightEffectParameters {
420+
color: upd.color,
421+
color_temperature: upd.color_temperature,
422+
speed: hz.effect_speed.map(Clamp::unit_from_u8_light),
423+
},
424+
});
425+
upd.effects_v2 = Some(LightEffectsV2Update {
426+
action: action,
427+
status: None,
428+
});
429+
upd.timed_effects = Some(LightTimedEffectsUpdate::no_effect());
430+
} else if let Some(light_timed_effect) = LightTimedEffect::ALL.into_iter().find(|&e| {
431+
let e: EffectType = e.into();
432+
e == effect_type
433+
}) {
434+
upd.effects_v2 = Some(LightEffectsV2Update::no_effect());
435+
upd.timed_effects = Some(LightTimedEffectsUpdate {
436+
effect: Some(light_timed_effect),
437+
duration: None,
438+
});
439+
}
440+
} else {
441+
upd.effects_v2 = Some(LightEffectsV2Update::no_effect());
442+
upd.timed_effects = Some(LightTimedEffectsUpdate::no_effect());
443+
}
444+
445+
if let Some(gradient_colors) = &hz.gradient_colors {
446+
upd.gradient = Some(LightGradientUpdate {
447+
mode: Some(match gradient_colors.header.style {
448+
GradientStyle::Linear => LightGradientMode::InterpolatedPalette,
449+
GradientStyle::Mirrored => LightGradientMode::InterpolatedPaletteMirrored,
450+
GradientStyle::Scattered => LightGradientMode::RandomPixelated,
451+
}),
452+
points: gradient_colors
453+
.points
454+
.iter()
455+
.copied()
456+
.map(|point| LightGradientPoint {
457+
color: ColorUpdate::new(point),
458+
})
459+
.collect(),
460+
})
461+
}
462+
463+
upd
464+
}
465+
}
466+
399467
#[cfg_attr(coverage_nightly, coverage(off))]
400468
#[cfg(test)]
401469
mod tests {

crates/z2m/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ keywords.workspace = true
1616
workspace = true
1717

1818
[dependencies]
19+
hex = "0.4.3"
1920
hue = { version = "0.1.0", path = "../hue", default-features = false }
21+
log = "0.4.29"
2022
serde = "1.0.219"
2123
serde_json = "1.0.140"
2224
thiserror = "2.0.12"

crates/z2m/src/convert.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
use std::collections::BTreeSet;
2+
use std::io::Cursor;
23

34
use hue::api::{
45
ColorGamut, ColorTemperature, DeviceProductData, Dimming, DimmingDeltaAction, GamutType,
56
GroupedLightUpdate, LightColor, LightGradient, LightGradientMode, LightGradientPoint,
67
LightGradientUpdate, LightUpdate, MirekSchema,
78
};
89
use hue::devicedb::{hardware_platform_type, product_archetype};
10+
use hue::error::HueError;
911
use hue::xy::XY;
12+
use hue::zigbee::HueZigbeeUpdate;
1013

1114
use crate::api::{Device, Expose, ExposeList, ExposeNumeric};
1215
use crate::update::{DeviceColorMode, DeviceUpdate};
@@ -156,6 +159,24 @@ impl ExtractDeviceProductData for DeviceProductData {
156159

157160
impl From<&DeviceUpdate> for LightUpdate {
158161
fn from(value: &DeviceUpdate) -> Self {
162+
if let Some(philips_raw) = &value.philips_raw {
163+
match hex::decode(&philips_raw)
164+
.map_err(HueError::from)
165+
.and_then(|data| {
166+
let mut cur = Cursor::new(data);
167+
HueZigbeeUpdate::from_reader(&mut cur)
168+
}) {
169+
Ok(hz) => {
170+
let upd = hz.into();
171+
log::trace!("Converted Philips raw update to light update {philips_raw} {upd:#?}");
172+
return upd;
173+
}
174+
Err(err) => {
175+
log::error!("Failed to parse Philips Hue raw update {philips_raw}: {err}. Falling back to using z2m data");
176+
}
177+
}
178+
}
179+
159180
let mut upd = Self::new()
160181
.with_on(value.state.map(Into::into))
161182
.with_brightness(value.brightness.map(|b| b / 254.0 * 100.0))

crates/z2m/src/update.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ pub struct DeviceUpdate {
5050
pub transition: Option<f64>,
5151
#[serde(skip_serializing_if = "Option::is_none")]
5252
pub effect: Option<DeviceEffect>,
53+
#[serde(skip_serializing_if = "Option::is_none")]
54+
pub philips_raw: Option<String>,
5355

5456
/* all other fields */
5557
#[serde(skip_serializing_if = "HashMap::is_empty")]

src/backend/z2m/backend_event.rs

Lines changed: 12 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,9 @@ use uuid::Uuid;
1010
use bifrost_api::backend::BackendRequest;
1111
use hue::api::{
1212
BridgeHome, ColorTemperatureUpdate, DimmingDeltaAction, Entertainment,
13-
EntertainmentConfiguration, GroupedLight, GroupedLightUpdate, Light, LightEffect,
14-
LightEffectsV2Update, LightGradientMode, LightUpdate, RType, Resource, ResourceLink, Room,
15-
RoomUpdate, Scene, SceneActive, SceneStatus, SceneStatusEnum, SceneUpdate,
16-
ZigbeeDeviceDiscoveryUpdate,
13+
EntertainmentConfiguration, GroupedLight, GroupedLightUpdate, Light, LightEffectsV2Update,
14+
LightGradientMode, LightUpdate, RType, Resource, ResourceLink, Room, RoomUpdate, Scene,
15+
SceneActive, SceneStatus, SceneStatusEnum, SceneUpdate, ZigbeeDeviceDiscoveryUpdate,
1716
};
1817
use hue::error::HueError;
1918
use hue::stream::HueStreamLightsV2;
@@ -72,7 +71,7 @@ impl Z2mBackend {
7271
hz = hz.with_effect_type(fx.into());
7372
}
7473
if let Some(speed) = &act.parameters.speed {
75-
hz = hz.with_effect_speed(speed.unit_to_u8_clamped());
74+
hz = hz.with_effect_speed(speed.unit_to_u8_clamped_light());
7675
}
7776
if let Some(mirek) = &act.parameters.color_temperature.and_then(|ct| ct.mirek) {
7877
hz = hz.with_color_mirek(*mirek);
@@ -105,43 +104,18 @@ impl Z2mBackend {
105104
return Ok(());
106105
};
107106

108-
let mut lock = self.state.lock().await;
109-
110-
// We cannot recover .mode from backend updates, since these only contain
111-
// the gradient colors. So we have no choice, but to update the mode
112-
// here. Otherwise, the information would be lost.
113-
if let Some(mode) = upd.gradient.as_ref().and_then(|gr| gr.mode) {
114-
lock.update::<Light>(&link.rid, |light| {
115-
if let Some(gr) = &mut light.gradient {
116-
gr.mode = mode;
117-
}
118-
})?;
119-
}
120-
// Effect state is currently not retrieved from backend updates either
121-
if let Some(upd) = upd.effects_v2.as_ref() {
122-
lock.update::<Light>(&link.rid, |light| {
123-
if let Some(effects_v2) = &mut light.effects_v2 {
124-
*effects_v2 += upd;
125-
}
126-
if let Some(effects) = &mut light.effects {
127-
let light_effect = upd
128-
.action
129-
.as_ref()
130-
.and_then(|a| a.effect)
131-
.unwrap_or(LightEffect::NoEffect);
132-
effects.status = light_effect;
133-
}
134-
})?;
135-
}
136-
let hue_effects = lock.get::<Light>(link)?.effects.is_some();
137-
drop(lock);
107+
let hue_effects = self
108+
.state
109+
.lock()
110+
.await
111+
.get::<Light>(link)?
112+
.effects
113+
.is_some();
138114

139115
let mut payload: Option<DeviceUpdate> = None;
140116

141117
// handle "identify" request (light breathing)
142118
if upd.identify.is_some() {
143-
// handle "identify" request (light breathing)
144-
145119
payload = Some(
146120
payload
147121
.unwrap_or_default()
@@ -209,17 +183,7 @@ impl Z2mBackend {
209183
if !hz.is_empty() {
210184
hz = hz.with_fade_speed(0x0001);
211185

212-
let read_payload = DeviceRead::default()
213-
.with_state(
214-
hz.onoff.is_some() || hz.brightness.is_some() || hz.effect_type.is_some(),
215-
)
216-
.with_color(
217-
hz.color_mirek.is_some()
218-
|| hz.color_xy.is_some()
219-
|| hz.effect_type.is_some(),
220-
)
221-
.with_brightness(hz.brightness.is_some() || hz.effect_type.is_some());
222-
186+
let read_payload = DeviceRead::default().with_state(true);
223187
z2mws.send_hue_effects(topic, hz).await?;
224188

225189
// Do an explicit attribute read since Hue specific updates do not automatically update z2m state

src/backend/z2m/bridge_event.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ impl Z2mBackend {
5050
}
5151

5252
let upd = DeviceUpdate::deserialize(payload)?;
53+
log::trace!("Device update {upd:#?}");
5354

5455
let obj = self.state.lock().await.get_resource_by_id(rid)?.obj;
5556
match obj {

0 commit comments

Comments
 (0)