Skip to content

Commit 0f9baec

Browse files
authored
Merge pull request #136 from chrivers/chrivers/z2m-new-features
After the many fundamental and infrastructure changes in Bifrost, it's finally time for a set of changes that add new features! This one is particularly exciting, as it contains a number of exciting additions that directly improve the end-user experience. Bifrost is now able to: - Update existing scenes (closes #85) - Delete lights - Add new lights to the bridge - Add new lights to rooms - Remove lights from rooms - Make lights pulse when selected - Learn scenes with gradient colors All these actions now work from the api, including directly from the Hue app! No more jumping back and forth between the app and z2m to perform common maintenance tasks!
2 parents d13989e + 3673f26 commit 0f9baec

28 files changed

+1820
-1085
lines changed

crates/bifrost-api/src/backend.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
use serde::{Deserialize, Serialize};
22
use uuid::Uuid;
33

4-
use hue::api::{GroupedLightUpdate, LightUpdate, ResourceLink, RoomUpdate, Scene, SceneUpdate};
4+
use hue::api::{
5+
GroupedLightUpdate, LightUpdate, ResourceLink, RoomUpdate, Scene, SceneUpdate,
6+
ZigbeeDeviceDiscoveryUpdate,
7+
};
58
use hue::stream::HueStreamLightsV2;
69

710
use crate::Client;
@@ -25,6 +28,8 @@ pub enum BackendRequest {
2528
EntertainmentStart(Uuid),
2629
EntertainmentFrame(HueStreamLightsV2),
2730
EntertainmentStop(),
31+
32+
ZigbeeDeviceDiscovery(ResourceLink, ZigbeeDeviceDiscoveryUpdate),
2833
}
2934

3035
impl Client {

crates/hue/src/api/device.rs

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ pub struct DeviceUpdate {
2727
pub services: Option<Vec<ResourceLink>>,
2828
#[serde(skip_serializing_if = "Option::is_none")]
2929
pub product_data: Option<Value>,
30+
#[serde(skip_serializing_if = "Option::is_none")]
31+
pub identify: Option<DeviceIdentifyUpdate>,
3032
}
3133

3234
impl Device {
@@ -46,6 +48,17 @@ impl Device {
4648
}
4749
}
4850

51+
#[derive(Debug, Serialize, Deserialize, Copy, Clone, PartialEq, Eq)]
52+
#[serde(rename_all = "snake_case")]
53+
pub enum DeviceIdentify {
54+
Identify,
55+
}
56+
57+
#[derive(Debug, Serialize, Deserialize, Copy, Clone, PartialEq, Eq)]
58+
pub struct DeviceIdentifyUpdate {
59+
pub action: DeviceIdentify,
60+
}
61+
4962
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
5063
pub struct Identify {}
5164

@@ -103,14 +116,14 @@ impl DeviceUpdate {
103116
}
104117
}
105118

106-
impl AddAssign<DeviceUpdate> for Device {
107-
fn add_assign(&mut self, upd: DeviceUpdate) {
108-
if let Some(md) = upd.metadata {
109-
if let Some(name) = md.name {
110-
self.metadata.name = name;
119+
impl AddAssign<&DeviceUpdate> for Device {
120+
fn add_assign(&mut self, upd: &DeviceUpdate) {
121+
if let Some(md) = &upd.metadata {
122+
if let Some(name) = &md.name {
123+
self.metadata.name.clone_from(name);
111124
}
112-
if let Some(archetype) = md.archetype {
113-
self.metadata.archetype = archetype;
125+
if let Some(archetype) = &md.archetype {
126+
self.metadata.archetype.clone_from(archetype);
114127
}
115128
}
116129
}

crates/hue/src/api/light.rs

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use std::ops::{AddAssign, Sub};
44
use serde::{Deserialize, Serialize};
55
use serde_json::{Value, json};
66

7+
use crate::api::device::DeviceIdentifyUpdate;
78
use crate::api::{DeviceArchetype, Identify, Metadata, MetadataUpdate, ResourceLink, Stub};
89
use crate::hs::HS;
910
use crate::legacy_api::ApiLightStateUpdate;
@@ -32,7 +33,8 @@ pub struct Light {
3233
pub effects: Option<LightEffects>,
3334
#[serde(skip_serializing_if = "Option::is_none")]
3435
pub effects_v2: Option<LightEffectsV2>,
35-
pub service_id: u32,
36+
#[serde(skip_serializing_if = "Option::is_none")]
37+
pub service_id: Option<u32>,
3638
#[serde(skip_serializing_if = "Option::is_none")]
3739
pub gradient: Option<LightGradient>,
3840
#[serde(default)]
@@ -107,7 +109,7 @@ impl Light {
107109
dynamics: Some(LightDynamics::default()),
108110
effects: None,
109111
effects_v2: None,
110-
service_id: 0,
112+
service_id: Some(0),
111113
gradient: None,
112114
identify: Identify {},
113115
timed_effects: Some(LightTimedEffects {
@@ -182,14 +184,14 @@ impl Light {
182184
}
183185
}
184186

185-
impl AddAssign<LightUpdate> for Light {
186-
fn add_assign(&mut self, upd: LightUpdate) {
187-
if let Some(md) = upd.metadata {
188-
if let Some(name) = md.name {
189-
self.metadata.name = name;
187+
impl AddAssign<&LightUpdate> for Light {
188+
fn add_assign(&mut self, upd: &LightUpdate) {
189+
if let Some(md) = &upd.metadata {
190+
if let Some(name) = &md.name {
191+
self.metadata.name.clone_from(name);
190192
}
191-
if let Some(archetype) = md.archetype {
192-
self.metadata.archetype = archetype;
193+
if let Some(archetype) = &md.archetype {
194+
self.metadata.archetype = archetype.clone();
193195
}
194196
}
195197

@@ -217,9 +219,9 @@ impl AddAssign<LightUpdate> for Light {
217219
}
218220

219221
if let Some(grad) = &mut self.gradient {
220-
if let Some(grupd) = upd.gradient {
222+
if let Some(grupd) = &upd.gradient {
221223
grad.mode = grupd.mode.unwrap_or(grad.mode);
222-
grad.points = grupd.points;
224+
grad.points.clone_from(&grupd.points);
223225
}
224226
}
225227
}
@@ -626,6 +628,8 @@ pub struct LightUpdate {
626628
pub powerup: Option<Value>,
627629
#[serde(skip_serializing_if = "Option::is_none")]
628630
pub dynamics: Option<LightDynamicsUpdate>,
631+
#[serde(skip_serializing_if = "Option::is_none")]
632+
pub identify: Option<DeviceIdentifyUpdate>,
629633
}
630634

631635
impl LightUpdate {
@@ -674,6 +678,11 @@ impl LightUpdate {
674678
}
675679
}
676680

681+
#[must_use]
682+
pub fn with_identify(self, identify: Option<DeviceIdentifyUpdate>) -> Self {
683+
Self { identify, ..self }
684+
}
685+
677686
#[must_use]
678687
pub fn with_gradient(self, gradient: Option<LightGradientUpdate>) -> Self {
679688
Self { gradient, ..self }

crates/hue/src/api/mod.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ mod scene;
99
mod stream;
1010
mod stubs;
1111
mod update;
12+
mod zigbee_device_discovery;
1213

1314
pub use device::{Device, DeviceArchetype, DeviceProductData, DeviceUpdate, Identify};
1415
pub use entertainment::{Entertainment, EntertainmentSegment, EntertainmentSegments};
@@ -47,10 +48,14 @@ pub use stubs::{
4748
ButtonData, ButtonMetadata, ButtonReport, DevicePower, DeviceSoftwareUpdate, DollarRef,
4849
GeofenceClient, Geolocation, GroupedLightLevel, GroupedMotion, Homekit, LightLevel, Matter,
4950
Metadata, MetadataUpdate, Motion, PrivateGroup, PublicImage, RelativeRotary, SmartScene,
50-
Taurus, Temperature, TimeZone, ZigbeeConnectivity, ZigbeeConnectivityStatus,
51-
ZigbeeDeviceDiscovery, Zone,
51+
Taurus, Temperature, TimeZone, ZigbeeConnectivity, ZigbeeConnectivityStatus, Zone,
5252
};
5353
pub use update::Update;
54+
pub use zigbee_device_discovery::{
55+
ZigbeeDeviceDiscovery, ZigbeeDeviceDiscoveryAction, ZigbeeDeviceDiscoveryInstallCode,
56+
ZigbeeDeviceDiscoveryStatus, ZigbeeDeviceDiscoveryUpdate, ZigbeeDeviceDiscoveryUpdateAction,
57+
ZigbeeDeviceDiscoveryUpdateActionType,
58+
};
5459

5560
use std::fmt::Debug;
5661

crates/hue/src/api/resource.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,9 +170,10 @@ impl ResourceLink {
170170

171171
impl Debug for ResourceLink {
172172
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
173-
let rtype = format!("{:?}", self.rtype).to_lowercase();
173+
// we need serde(rename_all = "snake_case") translation
174+
let rtype = serde_json::to_string(&self.rtype).unwrap();
174175
let rid = self.rid;
175-
write!(f, "{rtype}/{rid}")
176+
write!(f, "{}/{rid}", rtype.trim_matches('"'))
176177
}
177178
}
178179

crates/hue/src/api/room.rs

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -133,24 +133,24 @@ impl RoomMetadata {
133133
}
134134
}
135135

136-
impl AddAssign<RoomUpdate> for Room {
137-
fn add_assign(&mut self, rhs: RoomUpdate) {
138-
if let Some(md) = rhs.metadata {
136+
impl AddAssign<&RoomUpdate> for Room {
137+
fn add_assign(&mut self, rhs: &RoomUpdate) {
138+
if let Some(md) = &rhs.metadata {
139139
self.metadata += md;
140140
}
141-
if let Some(children) = rhs.children {
142-
self.children = children;
141+
if let Some(children) = &rhs.children {
142+
self.children.clone_from(children);
143143
}
144144
}
145145
}
146146

147-
impl AddAssign<RoomMetadataUpdate> for RoomMetadata {
148-
fn add_assign(&mut self, upd: RoomMetadataUpdate) {
149-
if let Some(name) = upd.name {
150-
self.name = name;
147+
impl AddAssign<&RoomMetadataUpdate> for RoomMetadata {
148+
fn add_assign(&mut self, upd: &RoomMetadataUpdate) {
149+
if let Some(name) = &upd.name {
150+
self.name.clone_from(name);
151151
}
152-
if let Some(archetype) = upd.archetype {
153-
self.archetype = archetype;
152+
if let Some(archetype) = &upd.archetype {
153+
self.archetype = *archetype;
154154
}
155155
}
156156
}

crates/hue/src/api/scene.rs

Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ use chrono::{DateTime, Utc};
44
use serde::{Deserialize, Serialize};
55
use serde_json::Value;
66

7-
use crate::api::{ColorTemperatureUpdate, ColorUpdate, DimmingUpdate, On, ResourceLink};
7+
use crate::api::{
8+
ColorTemperatureUpdate, ColorUpdate, DimmingUpdate, LightGradientUpdate, On, ResourceLink,
9+
};
810
use crate::date_format;
911

1012
#[derive(Copy, Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
@@ -26,18 +28,6 @@ pub struct SceneStatus {
2628
pub last_recall: Option<DateTime<Utc>>,
2729
}
2830

29-
#[derive(Copy, Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
30-
pub struct SceneStatusUpdate {
31-
#[serde(skip_serializing_if = "Option::is_none")]
32-
pub active: Option<SceneActive>,
33-
#[serde(
34-
with = "date_format::utc_ms_opt",
35-
default,
36-
skip_serializing_if = "Option::is_none"
37-
)]
38-
pub last_recall: Option<DateTime<Utc>>,
39-
}
40-
4131
#[derive(Copy, Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
4232
#[serde(rename_all = "snake_case")]
4333
pub enum SceneStatusEnum {
@@ -88,7 +78,7 @@ pub struct SceneAction {
8878
#[serde(skip_serializing_if = "Option::is_none")]
8979
pub on: Option<On>,
9080
#[serde(skip_serializing_if = "Option::is_none")]
91-
pub gradient: Option<Value>,
81+
pub gradient: Option<LightGradientUpdate>,
9282
#[serde(default, skip_serializing_if = "Value::is_null")]
9383
pub effects: Value,
9484
}
@@ -131,8 +121,6 @@ pub struct SceneUpdate {
131121
pub speed: Option<f64>,
132122
#[serde(skip_serializing_if = "Option::is_none")]
133123
pub auto_dynamic: Option<bool>,
134-
#[serde(skip_serializing_if = "Option::is_none")]
135-
pub status: Option<SceneStatusUpdate>,
136124
}
137125

138126
impl SceneUpdate {
@@ -163,16 +151,36 @@ impl SceneUpdate {
163151
}
164152
}
165153

166-
impl AddAssign<SceneMetadataUpdate> for SceneMetadata {
167-
fn add_assign(&mut self, upd: SceneMetadataUpdate) {
168-
if let Some(appdata) = upd.appdata {
169-
self.appdata = Some(appdata);
154+
impl AddAssign<&SceneUpdate> for Scene {
155+
fn add_assign(&mut self, upd: &SceneUpdate) {
156+
if let Some(actions) = &upd.actions {
157+
self.actions.clone_from(actions);
158+
}
159+
if let Some(md) = &upd.metadata {
160+
self.metadata += md;
161+
}
162+
if let Some(palette) = &upd.palette {
163+
self.palette.clone_from(palette);
164+
}
165+
if let Some(speed) = upd.speed {
166+
self.speed = speed;
167+
}
168+
if let Some(auto_dynamic) = upd.auto_dynamic {
169+
self.auto_dynamic = auto_dynamic;
170+
}
171+
}
172+
}
173+
174+
impl AddAssign<&SceneMetadataUpdate> for SceneMetadata {
175+
fn add_assign(&mut self, upd: &SceneMetadataUpdate) {
176+
if let Some(appdata) = &upd.appdata {
177+
self.appdata = Some(appdata.to_string());
170178
}
171-
if let Some(image) = upd.image {
172-
self.image = Some(image);
179+
if let Some(image) = &upd.image {
180+
self.image = Some(*image);
173181
}
174-
if let Some(name) = upd.name {
175-
self.name = name;
182+
if let Some(name) = &upd.name {
183+
self.name.clone_from(name);
176184
}
177185
}
178186
}

crates/hue/src/api/stubs.rs

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -239,15 +239,6 @@ pub struct ZigbeeConnectivity {
239239
pub status: ZigbeeConnectivityStatus,
240240
}
241241

242-
#[derive(Debug, Serialize, Deserialize, Clone)]
243-
pub struct ZigbeeDeviceDiscovery {
244-
pub owner: ResourceLink,
245-
pub status: String,
246-
#[serde(default)]
247-
#[serde(skip_serializing_if = "Value::is_null")]
248-
pub action: Value,
249-
}
250-
251242
#[derive(Debug, Serialize, Deserialize, Clone)]
252243
pub struct Zone {
253244
pub metadata: Metadata,
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
use serde::{Deserialize, Serialize};
2+
use serde_json::Value;
3+
use uuid::Uuid;
4+
5+
use crate::api::ResourceLink;
6+
7+
#[derive(Debug, Serialize, Deserialize, Clone)]
8+
#[serde(rename_all = "snake_case")]
9+
pub enum ZigbeeDeviceDiscoveryStatus {
10+
Active,
11+
Ready,
12+
}
13+
14+
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
15+
pub struct ZigbeeDeviceDiscoveryAction {
16+
pub action_type_values: Vec<Value>,
17+
#[serde(default, skip_serializing_if = "Vec::is_empty")]
18+
pub search_codes: Vec<String>,
19+
}
20+
21+
impl ZigbeeDeviceDiscoveryAction {
22+
#[must_use]
23+
pub fn is_empty(&self) -> bool {
24+
self.action_type_values.is_empty()
25+
}
26+
}
27+
28+
#[derive(Debug, Serialize, Deserialize, Clone)]
29+
pub struct ZigbeeDeviceDiscovery {
30+
pub owner: ResourceLink,
31+
pub status: ZigbeeDeviceDiscoveryStatus,
32+
33+
#[serde(default, skip_serializing_if = "ZigbeeDeviceDiscoveryAction::is_empty")]
34+
pub action: ZigbeeDeviceDiscoveryAction,
35+
}
36+
37+
#[derive(Debug, Serialize, Deserialize, Clone)]
38+
pub struct ZigbeeDeviceDiscoveryInstallCode {
39+
pub mac_address: String,
40+
pub ic: Uuid,
41+
}
42+
43+
#[derive(Debug, Serialize, Deserialize, Clone)]
44+
#[serde(rename_all = "snake_case")]
45+
pub enum ZigbeeDeviceDiscoveryUpdateActionType {
46+
Search,
47+
SearchAllowDefaultLinkKey,
48+
}
49+
50+
#[derive(Debug, Serialize, Deserialize, Clone)]
51+
pub struct ZigbeeDeviceDiscoveryUpdateAction {
52+
pub action_type: ZigbeeDeviceDiscoveryUpdateActionType,
53+
pub search_codes: Option<Vec<String>>,
54+
}
55+
56+
#[derive(Debug, Serialize, Deserialize, Clone)]
57+
pub struct ZigbeeDeviceDiscoveryUpdate {
58+
pub action: ZigbeeDeviceDiscoveryUpdateAction,
59+
pub add_install_code: Option<ZigbeeDeviceDiscoveryInstallCode>,
60+
}

0 commit comments

Comments
 (0)