From 75126ebee160027f510e0cb6050b1e86c52c52a7 Mon Sep 17 00:00:00 2001 From: Christian Duvholt Date: Sat, 1 Feb 2025 19:15:34 +0100 Subject: [PATCH 01/11] Add Hue API types for BehaviorScript, BehaviorInstance and BehaviorInstanceUpdate --- crates/hue/src/api/behavior.rs | 183 +++++++++++++++++++++++++++++++++ crates/hue/src/api/mod.rs | 15 ++- crates/hue/src/api/stubs.rs | 48 +-------- crates/hue/src/api/update.rs | 5 +- 4 files changed, 196 insertions(+), 55 deletions(-) create mode 100644 crates/hue/src/api/behavior.rs diff --git a/crates/hue/src/api/behavior.rs b/crates/hue/src/api/behavior.rs new file mode 100644 index 00000000..2ef3dde6 --- /dev/null +++ b/crates/hue/src/api/behavior.rs @@ -0,0 +1,183 @@ +use std::ops::AddAssign; + +use serde::{Deserialize, Deserializer, Serialize}; +use serde_json::Value; +use uuid::Uuid; + +use super::DollarRef; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct BehaviorScript { + pub configuration_schema: DollarRef, + pub description: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub max_number_instances: Option, + pub metadata: BehaviorScriptMetadata, + pub state_schema: DollarRef, + pub supported_features: Vec, + pub trigger_schema: DollarRef, + pub version: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct BehaviorScriptMetadata { + pub name: String, + pub category: String, +} + +fn deserialize_optional_field<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + Ok(Some(Value::deserialize(deserializer)?)) +} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +pub struct BehaviorInstance { + #[serde(default)] + pub dependees: Vec, + pub enabled: bool, + pub last_error: Option, + pub metadata: BehaviorInstanceMetadata, + pub script_id: Uuid, + pub status: Option, + #[serde( + default, + deserialize_with = "deserialize_optional_field", + skip_serializing_if = "Option::is_none" + )] + pub state: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub migrated_from: Option, + pub configuration: BehaviorInstanceConfiguration, +} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +#[serde(untagged)] +pub enum BehaviorInstanceConfiguration { + Wakeup(WakeupConfiguration), +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct WakeupConfiguration { + pub end_brightness: f64, + pub fade_in_duration: configuration::Duration, + #[serde(skip_serializing_if = "Option::is_none")] + pub turn_lights_off_after: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub style: Option, + pub when: configuration::When, + #[serde(rename = "where")] + pub where_field: Vec, +} + +pub mod configuration { + use std::time::Duration as StdDuration; + + use chrono::Weekday; + use serde::{Deserialize, Serialize}; + + use crate::api::ResourceLink; + + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] + pub struct Duration { + pub seconds: u32, + } + + impl Duration { + pub fn to_std(&self) -> StdDuration { + StdDuration::from_secs(self.seconds.into()) + } + } + + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] + pub struct When { + pub recurrence_days: Option>, + pub time_point: TimePoint, + } + + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] + #[serde(tag = "type", rename_all = "snake_case")] + pub enum TimePoint { + Time { time: Time }, + } + + impl TimePoint { + pub const fn time(&self) -> &Time { + match self { + Self::Time { time } => time, + } + } + } + + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] + pub struct Time { + pub hour: u32, + pub minute: u32, + } + + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] + pub struct Where { + pub group: ResourceLink, + pub items: Option>, + } +} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] +pub struct BehaviorInstanceMetadata { + pub name: String, +} + +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct BehaviorInstanceUpdate { + pub configuration: Option, + pub enabled: Option, + pub metadata: Option, +} + +impl BehaviorInstanceUpdate { + #[must_use] + pub fn new() -> Self { + Self::default() + } + + #[must_use] + pub fn with_metadata(self, metadata: BehaviorInstanceMetadata) -> Self { + Self { + metadata: Some(metadata), + ..self + } + } + + #[must_use] + pub fn with_enabled(self, enabled: bool) -> Self { + Self { + enabled: Some(enabled), + ..self + } + } + + #[must_use] + pub fn with_configuration(self, configuration: BehaviorInstanceConfiguration) -> Self { + Self { + configuration: Some(configuration), + ..self + } + } +} + +impl AddAssign for BehaviorInstance { + fn add_assign(&mut self, upd: BehaviorInstanceUpdate) { + if let Some(md) = upd.metadata { + self.metadata = md; + } + + if let Some(enabled) = upd.enabled { + self.enabled = enabled; + } + + if let Some(configuration) = upd.configuration { + self.configuration = configuration; + } + } +} diff --git a/crates/hue/src/api/mod.rs b/crates/hue/src/api/mod.rs index a84ee7b9..a6a5491c 100644 --- a/crates/hue/src/api/mod.rs +++ b/crates/hue/src/api/mod.rs @@ -1,3 +1,4 @@ +mod behavior; mod device; mod entertainment; mod entertainment_config; @@ -11,6 +12,10 @@ mod stubs; mod update; mod zigbee_device_discovery; +pub use behavior::{ + BehaviorInstance, BehaviorInstanceConfiguration, BehaviorInstanceMetadata, + BehaviorInstanceUpdate, BehaviorScript, BehaviorScriptMetadata, WakeupConfiguration, +}; pub use device::{Device, DeviceArchetype, DeviceProductData, DeviceUpdate, Identify}; pub use entertainment::{Entertainment, EntertainmentSegment, EntertainmentSegments}; pub use entertainment_config::{ @@ -44,11 +49,11 @@ pub use scene::{ use serde::ser::SerializeMap; pub use stream::HueStreamKey; pub use stubs::{ - BehaviorInstance, BehaviorInstanceMetadata, BehaviorScript, Bridge, BridgeHome, Button, - ButtonData, ButtonMetadata, ButtonReport, DevicePower, DeviceSoftwareUpdate, DollarRef, - GeofenceClient, Geolocation, GroupedLightLevel, GroupedMotion, Homekit, LightLevel, Matter, - Metadata, MetadataUpdate, Motion, PrivateGroup, PublicImage, RelativeRotary, SmartScene, - Taurus, Temperature, TimeZone, ZigbeeConnectivity, ZigbeeConnectivityStatus, Zone, + Bridge, BridgeHome, Button, ButtonData, ButtonMetadata, ButtonReport, DevicePower, + DeviceSoftwareUpdate, DollarRef, GeofenceClient, Geolocation, GroupedLightLevel, GroupedMotion, + Homekit, LightLevel, Matter, Metadata, MetadataUpdate, Motion, PrivateGroup, PublicImage, + RelativeRotary, SmartScene, Taurus, Temperature, TimeZone, ZigbeeConnectivity, + ZigbeeConnectivityStatus, Zone, }; pub use update::Update; pub use zigbee_device_discovery::{ diff --git a/crates/hue/src/api/stubs.rs b/crates/hue/src/api/stubs.rs index 354bd65d..092d6b68 100644 --- a/crates/hue/src/api/stubs.rs +++ b/crates/hue/src/api/stubs.rs @@ -1,9 +1,8 @@ use std::collections::BTreeSet; use chrono::{DateTime, Utc}; -use serde::{Deserialize, Deserializer, Serialize}; +use serde::{Deserialize, Serialize}; use serde_json::Value; -use uuid::Uuid; use crate::api::{DeviceArchetype, LightFunction, ResourceLink, SceneMetadata}; use crate::{best_guess_timezone, date_format}; @@ -71,51 +70,6 @@ pub struct DeviceSoftwareUpdate { pub problems: Vec, } -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct BehaviorScript { - pub configuration_schema: DollarRef, - pub description: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub max_number_instances: Option, - pub metadata: Value, - pub state_schema: DollarRef, - pub supported_features: Vec, - pub trigger_schema: DollarRef, - pub version: String, -} - -fn deserialize_optional_field<'de, D>(deserializer: D) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - Ok(Some(Value::deserialize(deserializer)?)) -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct BehaviorInstance { - pub configuration: Value, - #[serde(default)] - pub dependees: Vec, - pub enabled: bool, - pub last_error: Option, - pub metadata: BehaviorInstanceMetadata, - pub script_id: Uuid, - pub status: Option, - #[serde( - default, - deserialize_with = "deserialize_optional_field", - skip_serializing_if = "Option::is_none" - )] - pub state: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub migrated_from: Option, -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct BehaviorInstanceMetadata { - pub name: String, -} - #[derive(Debug, Serialize, Deserialize, Clone)] pub struct GeofenceClient { pub name: String, diff --git a/crates/hue/src/api/update.rs b/crates/hue/src/api/update.rs index ccd2f176..b0b1ee50 100644 --- a/crates/hue/src/api/update.rs +++ b/crates/hue/src/api/update.rs @@ -2,14 +2,13 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; use crate::api::{ - DeviceUpdate, EntertainmentConfigurationUpdate, GroupedLightUpdate, LightUpdate, RType, - RoomUpdate, SceneUpdate, + BehaviorInstanceUpdate, DeviceUpdate, EntertainmentConfigurationUpdate, GroupedLightUpdate, + LightUpdate, RType, RoomUpdate, SceneUpdate, }; type BridgeUpdate = Value; type BridgeHomeUpdate = Value; type ZigbeeDeviceDiscoveryUpdate = Value; -type BehaviorInstanceUpdate = Value; type SmartSceneUpdate = Value; type ZoneUpdate = Value; type GeolocationUpdate = Value; From 40eea1bd7eea2dacf3bc6c74c07e47c08701cdef Mon Sep 17 00:00:00 2001 From: Christian Duvholt Date: Sun, 20 Apr 2025 19:34:40 +0200 Subject: [PATCH 02/11] Add sunrise Hue effect --- crates/hue/src/api/light.rs | 1 + crates/hue/src/zigbee/composite.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/crates/hue/src/api/light.rs b/crates/hue/src/api/light.rs index e4371512..2ccdb691 100644 --- a/crates/hue/src/api/light.rs +++ b/crates/hue/src/api/light.rs @@ -490,6 +490,7 @@ pub enum LightEffect { Cosmos, Sunbeam, Enchant, + Sunrise, } impl LightEffect { diff --git a/crates/hue/src/zigbee/composite.rs b/crates/hue/src/zigbee/composite.rs index 3508c536..342c2d6b 100644 --- a/crates/hue/src/zigbee/composite.rs +++ b/crates/hue/src/zigbee/composite.rs @@ -41,6 +41,7 @@ impl From for EffectType { LightEffect::Cosmos => Self::Cosmos, LightEffect::Sunbeam => Self::Sunbeam, LightEffect::Enchant => Self::Enchant, + LightEffect::Sunrise => Self::Sunrise, } } } From 768a0dab53ae6f89134011d6c76a01600ffca708 Mon Sep 17 00:00:00 2001 From: Christian Duvholt Date: Sun, 20 Apr 2025 20:13:50 +0200 Subject: [PATCH 03/11] Calculate effect duration for sunrise effect --- crates/hue/src/effect_duration.rs | 71 +++++++++++++++++++++++++++++++ crates/hue/src/lib.rs | 1 + 2 files changed, 72 insertions(+) create mode 100644 crates/hue/src/effect_duration.rs diff --git a/crates/hue/src/effect_duration.rs b/crates/hue/src/effect_duration.rs new file mode 100644 index 00000000..2f9203fe --- /dev/null +++ b/crates/hue/src/effect_duration.rs @@ -0,0 +1,71 @@ +#[derive(PartialEq, Eq, Debug)] +pub struct EffectDuration(pub u8); + +const RESOLUTION_01S_BASE: u8 = 0xFC; +const RESOLUTION_05S_BASE: u8 = 0xCC; +const RESOLUTION_15S_BASE: u8 = 0xA5; +const RESOLUTION_01M_BASE: u8 = 0x79; +const RESOLUTION_05M_BASE: u8 = 0x3F; + +const RESOLUTION_01S: u32 = 1; // 01s. +const RESOLUTION_05S: u32 = 5; // 05s. +const RESOLUTION_15S: u32 = 15; // 15s. +const RESOLUTION_01M: u32 = 60; // 01min. +const RESOLUTION_05M: u32 = 5 * 600; // 05min. + +const RESOLUTION_01S_LIMIT: u32 = 60; // 01min. +const RESOLUTION_05S_LIMIT: u32 = 5 * 60; // 05min. +const RESOLUTION_15S_LIMIT: u32 = 15 * 60; // 15min. +const RESOLUTION_01M_LIMIT: u32 = 60 * 60; // 60min. +const RESOLUTION_05M_LIMIT: u32 = 6 * 60 * 60; // 06hrs. + +impl EffectDuration { + #[must_use] + #[allow(clippy::cast_possible_truncation)] + pub const fn from_seconds(seconds: u32) -> Self { + let (base, resolution) = if seconds < RESOLUTION_01S_LIMIT { + (RESOLUTION_01S_BASE, RESOLUTION_01S) + } else if seconds < RESOLUTION_05S_LIMIT { + (RESOLUTION_05S_BASE, RESOLUTION_05S) + } else if seconds < RESOLUTION_15S_LIMIT { + (RESOLUTION_15S_BASE, RESOLUTION_15S) + } else if seconds < RESOLUTION_01M_LIMIT { + (RESOLUTION_01M_BASE, RESOLUTION_01M) + } else if seconds < RESOLUTION_05M_LIMIT { + (RESOLUTION_05M_BASE, RESOLUTION_05M) + } else { + return Self(0); + }; + Self(base - ((seconds / resolution) as u8)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + pub fn seconds_to_effect_duration() { + // sniffed from the real Hue hub + let values = vec![ + (5, 145), + (10, 125), + (15, 106), + (20, 101), + (25, 96), + (30, 91), + (35, 86), + (40, 81), + (45, 76), + (50, 71), + (55, 66), + (60, 62), + ]; + for (input, output) in values { + assert_eq!( + EffectDuration::from_seconds(input * 60), + EffectDuration(output) + ); + } + } +} diff --git a/crates/hue/src/lib.rs b/crates/hue/src/lib.rs index 87009a5a..c089776f 100644 --- a/crates/hue/src/lib.rs +++ b/crates/hue/src/lib.rs @@ -8,6 +8,7 @@ pub mod colortemp; pub mod date_format; pub mod devicedb; pub mod diff; +pub mod effect_duration; pub mod error; pub mod flags; pub mod gamma; From 52c06533b0dcaecfff218a3d7559e675e1ce5011 Mon Sep 17 00:00:00 2001 From: Christian Duvholt Date: Mon, 21 Apr 2025 20:06:02 +0200 Subject: [PATCH 04/11] Add wake-up behavior script --- crates/hue/src/api/behavior.rs | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/crates/hue/src/api/behavior.rs b/crates/hue/src/api/behavior.rs index 2ef3dde6..322571dc 100644 --- a/crates/hue/src/api/behavior.rs +++ b/crates/hue/src/api/behavior.rs @@ -2,7 +2,7 @@ use std::ops::AddAssign; use serde::{Deserialize, Deserializer, Serialize}; use serde_json::Value; -use uuid::Uuid; +use uuid::{uuid, Uuid}; use super::DollarRef; @@ -19,6 +19,33 @@ pub struct BehaviorScript { pub version: String, } +impl BehaviorScript { + pub const WAKE_UP_ID: Uuid = uuid!("ff8957e3-2eb9-4699-a0c8-ad2cb3ede704"); + + #[must_use] + pub fn wake_up() -> Self { + Self { + configuration_schema: DollarRef { + dref: Some("basic_wake_up_config.json#".to_string()), + }, + description: + "Get your body in the mood to wake up by fading on the lights in the morning." + .to_string(), + max_number_instances: None, + metadata: BehaviorScriptMetadata { + name: "Basic wake up routine".to_string(), + category: "automation".to_string(), + }, + state_schema: DollarRef { dref: None }, + supported_features: vec!["style_sunrise".to_string(), "intensity".to_string()], + trigger_schema: DollarRef { + dref: Some("trigger.json#".to_string()), + }, + version: "0.0.1".to_string(), + } + } +} + #[derive(Debug, Serialize, Deserialize, Clone)] pub struct BehaviorScriptMetadata { pub name: String, From 10ef8adc4b371fb6f76040fe4fe3846bc515ca13 Mon Sep 17 00:00:00 2001 From: Christian Duvholt Date: Mon, 21 Apr 2025 20:59:36 +0200 Subject: [PATCH 05/11] Add wake-up style enum --- crates/hue/src/api/behavior.rs | 9 ++++++++- crates/hue/src/api/mod.rs | 1 + 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/hue/src/api/behavior.rs b/crates/hue/src/api/behavior.rs index 322571dc..41d23644 100644 --- a/crates/hue/src/api/behavior.rs +++ b/crates/hue/src/api/behavior.rs @@ -92,12 +92,19 @@ pub struct WakeupConfiguration { #[serde(skip_serializing_if = "Option::is_none")] pub turn_lights_off_after: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub style: Option, + pub style: Option, pub when: configuration::When, #[serde(rename = "where")] pub where_field: Vec, } +#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +pub enum WakeupStyle { + Sunrise, + Basic, +} + pub mod configuration { use std::time::Duration as StdDuration; diff --git a/crates/hue/src/api/mod.rs b/crates/hue/src/api/mod.rs index a6a5491c..7edf737a 100644 --- a/crates/hue/src/api/mod.rs +++ b/crates/hue/src/api/mod.rs @@ -15,6 +15,7 @@ mod zigbee_device_discovery; pub use behavior::{ BehaviorInstance, BehaviorInstanceConfiguration, BehaviorInstanceMetadata, BehaviorInstanceUpdate, BehaviorScript, BehaviorScriptMetadata, WakeupConfiguration, + WakeupStyle, }; pub use device::{Device, DeviceArchetype, DeviceProductData, DeviceUpdate, Identify}; pub use entertainment::{Entertainment, EntertainmentSegment, EntertainmentSegments}; From 5b47484848d3222ee320272af0c4cb2b9f97b0ad Mon Sep 17 00:00:00 2001 From: Christian Duvholt Date: Thu, 8 May 2025 21:07:56 +0200 Subject: [PATCH 06/11] Store behavior instance configuration as a Value Since this type is both highly dynamic and unknown we'll instead deserialize it after retrieving it from the state --- crates/hue/src/api/behavior.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/crates/hue/src/api/behavior.rs b/crates/hue/src/api/behavior.rs index 41d23644..e9f6109c 100644 --- a/crates/hue/src/api/behavior.rs +++ b/crates/hue/src/api/behavior.rs @@ -76,11 +76,10 @@ pub struct BehaviorInstance { pub state: Option, #[serde(skip_serializing_if = "Option::is_none")] pub migrated_from: Option, - pub configuration: BehaviorInstanceConfiguration, + pub configuration: Value, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[serde(untagged)] pub enum BehaviorInstanceConfiguration { Wakeup(WakeupConfiguration), } @@ -211,7 +210,14 @@ impl AddAssign for BehaviorInstance { } if let Some(configuration) = upd.configuration { - self.configuration = configuration; + match serde_json::to_value(configuration) { + Ok(value) => { + self.configuration = value; + }, + Err(err) => { + // todo: what do we do here? + }, + } } } } From 74b165e05424accfdf20fb84799ccec1d12d8cbb Mon Sep 17 00:00:00 2001 From: Christian Duvholt Date: Thu, 8 May 2025 21:20:31 +0200 Subject: [PATCH 07/11] Add types for BehaviorInstance.dependees --- crates/hue/src/api/behavior.rs | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/crates/hue/src/api/behavior.rs b/crates/hue/src/api/behavior.rs index e9f6109c..fcdea2ae 100644 --- a/crates/hue/src/api/behavior.rs +++ b/crates/hue/src/api/behavior.rs @@ -2,9 +2,9 @@ use std::ops::AddAssign; use serde::{Deserialize, Deserializer, Serialize}; use serde_json::Value; -use uuid::{uuid, Uuid}; +use uuid::{Uuid, uuid}; -use super::DollarRef; +use super::{DollarRef, ResourceLink}; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct BehaviorScript { @@ -59,10 +59,10 @@ where Ok(Some(Value::deserialize(deserializer)?)) } -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] pub struct BehaviorInstance { #[serde(default)] - pub dependees: Vec, + pub dependees: Vec, pub enabled: bool, pub last_error: Option, pub metadata: BehaviorInstanceMetadata, @@ -156,6 +156,21 @@ pub mod configuration { } } +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] +pub struct BehaviorInstanceDependee { + #[serde(rename = "type")] + pub type_field: Option, + pub target: ResourceLink, + pub level: BehaviorInstanceDependeeLevel, +} + +#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum BehaviorInstanceDependeeLevel { + Critical, + NonCritical, +} + #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] pub struct BehaviorInstanceMetadata { pub name: String, @@ -213,10 +228,10 @@ impl AddAssign for BehaviorInstance { match serde_json::to_value(configuration) { Ok(value) => { self.configuration = value; - }, + } Err(err) => { // todo: what do we do here? - }, + } } } } From 3097b427ed3157c9d2022a8c324b0ddfb9831d6f Mon Sep 17 00:00:00 2001 From: Christian Duvholt Date: Thu, 8 May 2025 21:35:58 +0200 Subject: [PATCH 08/11] Return HueResult from EffectDuration conversion --- crates/hue/src/effect_duration.rs | 17 ++++++++++++----- crates/hue/src/error.rs | 3 +++ src/routes/mod.rs | 1 + 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/crates/hue/src/effect_duration.rs b/crates/hue/src/effect_duration.rs index 2f9203fe..a057eb9d 100644 --- a/crates/hue/src/effect_duration.rs +++ b/crates/hue/src/effect_duration.rs @@ -1,3 +1,5 @@ +use crate::{error::HueResult}; + #[derive(PartialEq, Eq, Debug)] pub struct EffectDuration(pub u8); @@ -20,9 +22,8 @@ const RESOLUTION_01M_LIMIT: u32 = 60 * 60; // 60min. const RESOLUTION_05M_LIMIT: u32 = 6 * 60 * 60; // 06hrs. impl EffectDuration { - #[must_use] #[allow(clippy::cast_possible_truncation)] - pub const fn from_seconds(seconds: u32) -> Self { + pub const fn from_seconds(seconds: u32) -> HueResult { let (base, resolution) = if seconds < RESOLUTION_01S_LIMIT { (RESOLUTION_01S_BASE, RESOLUTION_01S) } else if seconds < RESOLUTION_05S_LIMIT { @@ -34,9 +35,9 @@ impl EffectDuration { } else if seconds < RESOLUTION_05M_LIMIT { (RESOLUTION_05M_BASE, RESOLUTION_05M) } else { - return Self(0); + return Err(crate::error::HueError::EffectDurationOutOfRange(seconds)); }; - Self(base - ((seconds / resolution) as u8)) + Ok(Self(base - ((seconds / resolution) as u8))) } } @@ -63,9 +64,15 @@ mod tests { ]; for (input, output) in values { assert_eq!( - EffectDuration::from_seconds(input * 60), + EffectDuration::from_seconds(input * 60).unwrap(), EffectDuration(output) ); } } + + #[test] + pub fn out_of_range() { + let seconds = 10 * 60 * 60; // 10h + assert!(EffectDuration::from_seconds(seconds).is_err()); + } } diff --git a/crates/hue/src/error.rs b/crates/hue/src/error.rs index 29ce322a..5ef491f1 100644 --- a/crates/hue/src/error.rs +++ b/crates/hue/src/error.rs @@ -55,6 +55,9 @@ pub enum HueError { #[error("Cannot merge json difference between non-map object")] Unmergable, + + #[error("Effect duration out of range: {0}")] + EffectDurationOutOfRange(u32), } /// Error types for Hue Bridge v1 API diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 77dc4822..e3470779 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -126,6 +126,7 @@ impl IntoResponse for ApiError { | HueError::PackedStructError(_) | HueError::UuidError(_) | HueError::HueEntertainmentBadHeader + | HueError::EffectDurationOutOfRange(_) | HueError::HueZigbeeUnknownFlags(_) => StatusCode::BAD_REQUEST, HueError::NotFound(_) | HueError::V1NotFound(_) | HueError::WrongType(_, _) => { From badaf128b43849c51e2626dfbaa4e22f6c697dde Mon Sep 17 00:00:00 2001 From: Christian Duvholt Date: Sun, 25 May 2025 16:23:14 +0200 Subject: [PATCH 09/11] Change last effect duration step to 60min This is still just a guess but it makes sense with the minimal sampling we have --- crates/hue/src/effect_duration.rs | 34 +++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/crates/hue/src/effect_duration.rs b/crates/hue/src/effect_duration.rs index a057eb9d..24fc4dd1 100644 --- a/crates/hue/src/effect_duration.rs +++ b/crates/hue/src/effect_duration.rs @@ -1,4 +1,4 @@ -use crate::{error::HueResult}; +use crate::error::HueResult; #[derive(PartialEq, Eq, Debug)] pub struct EffectDuration(pub u8); @@ -7,19 +7,20 @@ const RESOLUTION_01S_BASE: u8 = 0xFC; const RESOLUTION_05S_BASE: u8 = 0xCC; const RESOLUTION_15S_BASE: u8 = 0xA5; const RESOLUTION_01M_BASE: u8 = 0x79; -const RESOLUTION_05M_BASE: u8 = 0x3F; +const RESOLUTION_60M_BASE: u8 = 0x3F; -const RESOLUTION_01S: u32 = 1; // 01s. -const RESOLUTION_05S: u32 = 5; // 05s. +const RESOLUTION_01S: u32 = 1; // 1s. +const RESOLUTION_05S: u32 = 5; // 5s. const RESOLUTION_15S: u32 = 15; // 15s. -const RESOLUTION_01M: u32 = 60; // 01min. -const RESOLUTION_05M: u32 = 5 * 600; // 05min. +const RESOLUTION_01M: u32 = 60; // 1min. +// This value is just a guess. More real world testing is required +const RESOLUTION_60M: u32 = 60 * 60; // 60min. const RESOLUTION_01S_LIMIT: u32 = 60; // 01min. const RESOLUTION_05S_LIMIT: u32 = 5 * 60; // 05min. const RESOLUTION_15S_LIMIT: u32 = 15 * 60; // 15min. const RESOLUTION_01M_LIMIT: u32 = 60 * 60; // 60min. -const RESOLUTION_05M_LIMIT: u32 = 6 * 60 * 60; // 06hrs. +const RESOLUTION_60M_LIMIT: u32 = 6 * 60 * 60; // 06hrs. impl EffectDuration { #[allow(clippy::cast_possible_truncation)] @@ -32,8 +33,8 @@ impl EffectDuration { (RESOLUTION_15S_BASE, RESOLUTION_15S) } else if seconds < RESOLUTION_01M_LIMIT { (RESOLUTION_01M_BASE, RESOLUTION_01M) - } else if seconds < RESOLUTION_05M_LIMIT { - (RESOLUTION_05M_BASE, RESOLUTION_05M) + } else if seconds < RESOLUTION_60M_LIMIT { + (RESOLUTION_60M_BASE, RESOLUTION_60M) } else { return Err(crate::error::HueError::EffectDurationOutOfRange(seconds)); }; @@ -70,6 +71,21 @@ mod tests { } } + #[test] + pub fn check_for_gaps() { + // this test only verifies that there are no gaps when converting from seconds to effect duration + // the steps and resolution might still be wrong + let six_hours = 6 * 60 * 60; + let mut prev = 253; + for seconds in 0..six_hours { + let EffectDuration(next) = EffectDuration::from_seconds(seconds).unwrap(); + if next != prev { + assert_eq!(next, prev - 1, "Skipped at {seconds}s"); + prev = next; + } + } + } + #[test] pub fn out_of_range() { let seconds = 10 * 60 * 60; // 10h From d273af4d1f833c0903601616bd8c269e75ddc068 Mon Sep 17 00:00:00 2001 From: Christian Duvholt Date: Sun, 25 May 2025 16:25:51 +0200 Subject: [PATCH 10/11] Add more derives to EffectDuration --- crates/hue/src/effect_duration.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/hue/src/effect_duration.rs b/crates/hue/src/effect_duration.rs index 24fc4dd1..9c013b9c 100644 --- a/crates/hue/src/effect_duration.rs +++ b/crates/hue/src/effect_duration.rs @@ -1,6 +1,6 @@ use crate::error::HueResult; -#[derive(PartialEq, Eq, Debug)] +#[derive(Clone, Copy, PartialEq, Eq, Debug)] pub struct EffectDuration(pub u8); const RESOLUTION_01S_BASE: u8 = 0xFC; From a97fd39ca5bb73c8e2ffb518bea6b718f3712455 Mon Sep 17 00:00:00 2001 From: Christian Duvholt Date: Sun, 25 May 2025 16:28:02 +0200 Subject: [PATCH 11/11] Use Value for behavior instance updates We'll do any error handling when creating the update instead --- crates/hue/src/api/behavior.rs | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/crates/hue/src/api/behavior.rs b/crates/hue/src/api/behavior.rs index fcdea2ae..5e6fe618 100644 --- a/crates/hue/src/api/behavior.rs +++ b/crates/hue/src/api/behavior.rs @@ -178,7 +178,7 @@ pub struct BehaviorInstanceMetadata { #[derive(Debug, Default, Serialize, Deserialize, Clone)] pub struct BehaviorInstanceUpdate { - pub configuration: Option, + pub configuration: Option, pub enabled: Option, pub metadata: Option, } @@ -206,7 +206,7 @@ impl BehaviorInstanceUpdate { } #[must_use] - pub fn with_configuration(self, configuration: BehaviorInstanceConfiguration) -> Self { + pub fn with_configuration(self, configuration: Value) -> Self { Self { configuration: Some(configuration), ..self @@ -225,14 +225,7 @@ impl AddAssign for BehaviorInstance { } if let Some(configuration) = upd.configuration { - match serde_json::to_value(configuration) { - Ok(value) => { - self.configuration = value; - } - Err(err) => { - // todo: what do we do here? - } - } + self.configuration = configuration; } } }