Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
3fcdc17
Add CRUD support for setting wake up routine
duvholt Feb 1, 2025
dc87fd8
Proof of concept implementation of wake up automation
duvholt Feb 1, 2025
ab023a5
Support wake up weekday selection
duvholt Feb 2, 2025
c99342a
Implement wake up brightness and transition
duvholt Feb 2, 2025
0ee02bf
Properly implement choosing devices for wakeup automation
duvholt Feb 2, 2025
eddd947
Disable unimplemented wake up sunrise style
duvholt Feb 3, 2025
fee447a
Implement non repeating wakeup
duvholt Feb 3, 2025
da67172
Implement turn off lights after wake up
duvholt Feb 5, 2025
4b548c4
Start wake up time when fade in should start
duvholt Feb 5, 2025
8926811
Spawn actual wake up task in the background to mimic Hue
duvholt Feb 7, 2025
318fab2
Introduce WakeupJob and refactor
duvholt Feb 7, 2025
9d1344b
Properly implement start time calculation
duvholt Feb 7, 2025
86087f0
Refactor wake up job error handling
duvholt Feb 7, 2025
ace0fdb
Get rid of Time struct
duvholt Feb 7, 2025
33263af
Set wake up color temperature
duvholt Feb 8, 2025
0c7079a
Add WakeupRequest abstraction so that backend requests are better gro…
duvholt Feb 8, 2025
342385b
Reset wake up lights before fade in
duvholt Feb 8, 2025
7de4da5
Fix bug where wake up alarm was disabled before it completed
duvholt Feb 8, 2025
156d8ff
Clean up behavior config parsing
duvholt Feb 8, 2025
be5dd0c
Initial support for sunrise effect with hardcoded transition time
duvholt Apr 20, 2025
db04497
Calculate effect duration for sunrise effect
duvholt Apr 20, 2025
a6621c4
Move behavior script details to hue crate
duvholt Apr 21, 2025
48dbbea
Add behavior_instance/{id} GET endpoint
duvholt Apr 21, 2025
23aa411
Unmerge some imports
duvholt Apr 21, 2025
cda0c1e
Improve behavior instance serde models
duvholt Apr 21, 2025
5132724
Add wake-up style enum
duvholt Apr 21, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ zcl = { path = "crates/zcl" }
openssl = { version = "0.10.72", optional = true }
tokio-util = { version = "0.7.13", features = ["net"] }
tokio-openssl = "0.6.5"
tokio_schedule = "0.3.2"
udp-stream = "0.0.12"
maplit = "1.0.2"
svc = { version = "0.1.0", path = "crates/svc" }
Expand Down
218 changes: 218 additions & 0 deletions crates/hue/src/api/behavior.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
use std::ops::AddAssign;

use serde::{Deserialize, Deserializer, Serialize};
use serde_json::Value;
use uuid::{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<u32>,
pub metadata: BehaviorScriptMetadata,
pub state_schema: DollarRef,
pub supported_features: Vec<String>,
pub trigger_schema: DollarRef,
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,
pub category: String,
}

fn deserialize_optional_field<'de, D>(deserializer: D) -> Result<Option<Value>, 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<Value>,
pub enabled: bool,
pub last_error: Option<String>,
pub metadata: BehaviorInstanceMetadata,
// Wake up: ff8957e3-2eb9-4699-a0c8-ad2cb3ede704
pub script_id: Uuid,
pub status: Option<String>,
#[serde(
default,
deserialize_with = "deserialize_optional_field",
skip_serializing_if = "Option::is_none"
)]
pub state: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub migrated_from: Option<Value>,
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<configuration::Duration>,
#[serde(skip_serializing_if = "Option::is_none")]
pub style: Option<WakeupStyle>,
pub when: configuration::When,
#[serde(rename = "where")]
pub where_field: Vec<configuration::Where>,
}

#[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;

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<Vec<Weekday>>,
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<Vec<ResourceLink>>,
}
}

#[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<BehaviorInstanceConfiguration>,
pub enabled: Option<bool>,
pub metadata: Option<BehaviorInstanceMetadata>,
}

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<BehaviorInstanceUpdate> 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;
}
}
}
Comment on lines +204 to +218
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice - follows the style completely 👍

11 changes: 11 additions & 0 deletions crates/hue/src/api/grouped_light.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ pub struct GroupedLightUpdate {
pub color: Option<ColorUpdate>,
#[serde(skip_serializing_if = "Option::is_none")]
pub color_temperature: Option<ColorTemperatureUpdate>,

#[serde(skip)]
pub transition: Option<f64>,
Comment on lines +60 to +61
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait, serde skip?

So this isn't in the protocol?

Are we using this for internal message passing in Bifrost?

If so, we have to rework this :(

We can't inject Bifrost-specific fields into the hue crate. It's meant to be project-agnostic.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, but I think the main problem is that BackendRequest::LightUpdate and GroupedLightUpdate are using the hue::api types directly so there's currently no other way to send light updates than using the Hue serde types. In my head we need a domain layer type here, but I didn't want to introduce such a large change in this already large PR 😅

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, that's fair, hehe :)

I actually wanted to talk to you about that. I think it would be a good idea to split this PR into more than one - at least two.

Once we've shined up the rough edges, I think we'll get a pretty clear idea of what goes into improving the hue crate, and then "everything else". So at least there's those two natural "chunks" to work with.

And it should be quite easy to split it up later. I think it makes sense to work on the rough edges first.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I completely serde(skip)'ed the main point here 😂

You're absolutely right - we do need some kind of side channel, and it's certainly not the easiest change to implement.

On the other hand, I'm firmly against adding bifrost-specific code to the hue crate. If we open that pandora's box, we'll never close it 😅

So how about this - if you add some clear, eye-catching comments in those 2 places we use serde(skip), explaining why those fields are there, I can live with it for now.

Then we can make a refactoring run later, to change the update channel to include a sideband. Deal?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deal 🤝😈

}

impl GroupedLightUpdate {
Expand Down Expand Up @@ -100,4 +103,12 @@ impl GroupedLightUpdate {
..self
}
}

#[must_use]
pub fn with_transition(self, transition: Option<impl Into<f64>>) -> Self {
Self {
transition: transition.map(Into::into),
..self
}
}
}
12 changes: 12 additions & 0 deletions crates/hue/src/api/light.rs
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,7 @@ pub enum LightEffect {
Cosmos,
Sunbeam,
Enchant,
Sunrise,
}

impl LightEffect {
Expand Down Expand Up @@ -579,6 +580,9 @@ pub struct LightUpdate {
pub gradient: Option<LightGradientUpdate>,
#[serde(skip_serializing_if = "Option::is_none")]
pub effects_v2: Option<LightEffectsV2Update>,

#[serde(skip)]
pub transition: Option<f64>,
}

impl LightUpdate {
Expand Down Expand Up @@ -637,6 +641,14 @@ impl LightUpdate {
..self
}
}

#[must_use]
pub fn with_transition(self, transition: Option<impl Into<f64>>) -> Self {
Self {
transition: transition.map(Into::into),
..self
}
}
}

#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
Expand Down
17 changes: 11 additions & 6 deletions crates/hue/src/api/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod behavior;
mod device;
mod entertainment;
mod entertainment_config;
Expand All @@ -10,6 +11,11 @@ mod stream;
mod stubs;
mod update;

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};
pub use entertainment_config::{
Expand Down Expand Up @@ -43,12 +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,
ZigbeeDeviceDiscovery, 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, ZigbeeDeviceDiscovery, Zone,
};
pub use update::{Update, UpdateRecord};

Expand Down
Loading