diff --git a/config/config.toml b/config/config.toml index 6ba5d6e..ab2d600 100644 --- a/config/config.toml +++ b/config/config.toml @@ -93,15 +93,16 @@ room = "Workbench" topic = "zigbee2mqtt/workbench/outlet" -[device.hallway_light] -type = "HueLight" +[device.hallway_lights] +type = "HueGroup" ip = "10.0.0.146" login = "${HUE_TOKEN}" -light_id = 16 +group_id = 81 +scene_id = "3qWKxGVadXFFG4o" timer_id = 1 [device.hallway_frontdoor] type = "ContactSensor" topic = "zigbee2mqtt/hallway/frontdoor" presence = { topic = "automation/presence/contact/frontdoor", timeout = 900 } -trigger = { devices = ["hallway_light"], timeout = 60 } +trigger = { devices = ["hallway_lights"], timeout = 60 } diff --git a/config/zeus.dev.toml b/config/zeus.dev.toml index 4244def..b805138 100644 --- a/config/zeus.dev.toml +++ b/config/zeus.dev.toml @@ -95,15 +95,16 @@ room = "Workbench" topic = "zigbee2mqtt/workbench/outlet" -[device.hallway_light] -type = "HueLight" +[device.hallway_lights] +type = "HueGroup" ip = "10.0.0.146" login = "${HUE_TOKEN}" -light_id = 16 +group_id = 81 +scene_id = "3qWKxGVadXFFG4o" timer_id = 1 [device.hallway_frontdoor] type = "ContactSensor" topic = "zigbee2mqtt/hallway/frontdoor" presence = { topic = "automation_dev/presence/contact/frontdoor", timeout = 10 } -trigger = { devices = ["hallway_light"], timeout = 10 } +trigger = { devices = ["hallway_lights"], timeout = 10 } diff --git a/src/config.rs b/src/config.rs index 404e380..84a78ce 100644 --- a/src/config.rs +++ b/src/config.rs @@ -13,7 +13,7 @@ use tracing::debug; use crate::{ auth::OpenIDConfig, device_manager::DeviceConfigs, - devices::{DebugBridgeConfig, PresenceConfig}, + devices::PresenceConfig, error::{ConfigParseError, MissingEnv}, }; @@ -26,7 +26,6 @@ pub struct Config { pub fullfillment: FullfillmentConfig, pub ntfy: Option, pub presence: PresenceConfig, - pub debug_bridge: Option, #[serde(rename = "device")] pub devices: IndexMap, } diff --git a/src/device_manager.rs b/src/device_manager.rs index ed4a89a..5da5653 100644 --- a/src/device_manager.rs +++ b/src/device_manager.rs @@ -12,7 +12,7 @@ use tracing::{debug, error, instrument, trace}; use crate::{ devices::{ As, AudioSetupConfig, ContactSensorConfig, DebugBridgeConfig, Device, HueBridgeConfig, - HueLightConfig, IkeaOutletConfig, KasaOutletConfig, LightSensorConfig, WakeOnLANConfig, + HueGroupConfig, IkeaOutletConfig, KasaOutletConfig, LightSensorConfig, WakeOnLANConfig, WasherConfig, }, error::DeviceConfigError, @@ -50,7 +50,7 @@ pub enum DeviceConfigs { WakeOnLAN(WakeOnLANConfig), Washer(WasherConfig), HueBridge(HueBridgeConfig), - HueLight(HueLightConfig), + HueGroup(HueGroupConfig), LightSensor(LightSensorConfig), } diff --git a/src/devices/contact_sensor.rs b/src/devices/contact_sensor.rs index 65fa3d1..f62fcf9 100644 --- a/src/devices/contact_sensor.rs +++ b/src/devices/contact_sensor.rs @@ -167,7 +167,13 @@ impl OnMqtt for ContactSensor { let mut light = light.write().await; if let Some(light) = As::::cast_mut(light.as_mut()) { *previous = light.is_on().await.unwrap(); - light.set_on(true).await.ok(); + // Only turn the light on when it is currently off + // This is done such that if the light is on but dimmed for example it + // won't suddenly blast at full brightness but instead retain the current + // state + if !*previous { + light.set_on(true).await.ok(); + } } } } else { diff --git a/src/devices/hue_light.rs b/src/devices/hue_light.rs index 18fe16b..d83daa2 100644 --- a/src/devices/hue_light.rs +++ b/src/devices/hue_light.rs @@ -18,25 +18,27 @@ use crate::{ use super::Device; #[derive(Debug, Clone, Deserialize)] -pub struct HueLightConfig { +pub struct HueGroupConfig { pub ip: Ipv4Addr, pub login: String, - pub light_id: isize, + pub group_id: isize, pub timer_id: isize, + pub scene_id: String, } #[async_trait] -impl DeviceConfig for HueLightConfig { +impl DeviceConfig for HueGroupConfig { async fn create( self, identifier: &str, _ext: &ConfigExternal, ) -> Result, DeviceConfigError> { - let device = HueLight { + let device = HueGroup { identifier: identifier.into(), addr: (self.ip, 80).into(), login: self.login, - light_id: self.light_id, + group_id: self.group_id, + scene_id: self.scene_id, timer_id: self.timer_id, }; @@ -45,16 +47,17 @@ impl DeviceConfig for HueLightConfig { } #[derive(Debug)] -struct HueLight { +struct HueGroup { pub identifier: String, pub addr: SocketAddr, pub login: String, - pub light_id: isize, + pub group_id: isize, pub timer_id: isize, + pub scene_id: String, } // Couple of helper function to get the correct urls -impl HueLight { +impl HueGroup { fn url_base(&self) -> String { format!("http://{}/api/{}", self.addr, self.login) } @@ -63,30 +66,35 @@ impl HueLight { format!("{}/schedules/{}", self.url_base(), self.timer_id) } - fn url_set_state(&self) -> String { - format!("{}/lights/{}/state", self.url_base(), self.light_id) + fn url_set_action(&self) -> String { + format!("{}/groups/{}/action", self.url_base(), self.group_id) } fn url_get_state(&self) -> String { - format!("{}/lights/{}", self.url_base(), self.light_id) + format!("{}/groups/{}", self.url_base(), self.group_id) } } -impl Device for HueLight { +impl Device for HueGroup { fn get_id(&self) -> &str { &self.identifier } } #[async_trait] -impl OnOff for HueLight { +impl OnOff for HueGroup { async fn set_on(&mut self, on: bool) -> Result<(), ErrorCode> { // Abort any timer that is currently running self.stop_timeout().await.unwrap(); - let message = message::State::new(on); + let message = if on { + message::Action::scene(self.scene_id.clone()) + } else { + message::Action::on(true) + }; + let res = reqwest::Client::new() - .put(self.url_set_state()) + .put(self.url_set_action()) .json(&message) .send() .await; @@ -118,7 +126,7 @@ impl OnOff for HueLight { } let on = match res.json::().await { - Ok(info) => info.is_on(), + Ok(info) => info.any_on(), Err(err) => { error!(id = self.identifier, "Failed to parse message: {err}"); // TODO: Error code @@ -136,11 +144,12 @@ impl OnOff for HueLight { } #[async_trait] -impl Timeout for HueLight { +impl Timeout for HueGroup { async fn start_timeout(&mut self, timeout: Duration) -> Result<()> { // Abort any timer that is currently running self.stop_timeout().await?; + // NOTE: This uses an existing timer, as we are unable to cancel it on the hub otherwise let message = message::Timeout::new(Some(timeout)); let res = reqwest::Client::new() .put(self.url_set_schedule()) @@ -185,14 +194,33 @@ mod message { use serde::{ser::SerializeStruct, Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize)] - pub struct State { - on: bool, + pub struct Action { + #[serde(skip_serializing_if = "Option::is_none")] + on: Option, + #[serde(skip_serializing_if = "Option::is_none")] + scene: Option, } - impl State { - pub fn new(on: bool) -> Self { - Self { on } + impl Action { + pub fn on(on: bool) -> Self { + Self { + on: Some(on), + scene: None, + } } + + pub fn scene(scene: String) -> Self { + Self { + on: None, + scene: Some(scene), + } + } + } + + #[derive(Debug, Serialize, Deserialize)] + struct State { + all_on: bool, + any_on: bool, } #[derive(Debug, Serialize, Deserialize)] @@ -201,9 +229,13 @@ mod message { } impl Info { - pub fn is_on(&self) -> bool { - self.state.on + pub fn any_on(&self) -> bool { + self.state.any_on } + + // pub fn all_on(&self) -> bool { + // self.state.all_on + // } } #[derive(Debug)] diff --git a/src/devices/mod.rs b/src/devices/mod.rs index f4897d5..1262d7e 100644 --- a/src/devices/mod.rs +++ b/src/devices/mod.rs @@ -15,7 +15,7 @@ pub use self::audio_setup::AudioSetupConfig; pub use self::contact_sensor::ContactSensorConfig; pub use self::debug_bridge::DebugBridgeConfig; pub use self::hue_bridge::HueBridgeConfig; -pub use self::hue_light::HueLightConfig; +pub use self::hue_light::HueGroupConfig; pub use self::ikea_outlet::IkeaOutletConfig; pub use self::kasa_outlet::KasaOutletConfig; pub use self::light_sensor::{LightSensor, LightSensorConfig};