diff --git a/automation_devices/src/lib.rs b/automation_devices/src/lib.rs index 612eab5..70679cf 100644 --- a/automation_devices/src/lib.rs +++ b/automation_devices/src/lib.rs @@ -15,7 +15,7 @@ use std::ops::Deref; use automation_cast::Cast; use automation_lib::device::{Device, LuaDeviceCreate}; -use zigbee::light::{LightBrightness, LightOnOff}; +use zigbee::light::{LightBrightness, LightColorTemperature, LightOnOff}; use zigbee::outlet::{OutletOnOff, OutletPower}; pub use self::air_filter::AirFilter; @@ -144,6 +144,7 @@ macro_rules! impl_device { impl_device!(LightOnOff); impl_device!(LightBrightness); +impl_device!(LightColorTemperature); impl_device!(OutletOnOff); impl_device!(OutletPower); impl_device!(AirFilter); @@ -161,6 +162,7 @@ impl_device!(Washer); pub fn register_with_lua(lua: &mlua::Lua) -> mlua::Result<()> { register_device!(lua, LightOnOff); register_device!(lua, LightBrightness); + register_device!(lua, LightColorTemperature); register_device!(lua, OutletOnOff); register_device!(lua, OutletPower); register_device!(lua, AirFilter); diff --git a/automation_devices/src/zigbee/light.rs b/automation_devices/src/zigbee/light.rs index 53ddfa9..5aa3bfd 100644 --- a/automation_devices/src/zigbee/light.rs +++ b/automation_devices/src/zigbee/light.rs @@ -13,7 +13,7 @@ use automation_lib::mqtt::WrappedAsyncClient; use automation_macro::LuaDeviceConfig; use google_home::device; use google_home::errors::ErrorCode; -use google_home::traits::{Brightness, OnOff}; +use google_home::traits::{Brightness, Color, ColorSetting, ColorTemperatureRange, OnOff}; use google_home::types::Type; use rumqttc::{matches, Publish}; use serde::{Deserialize, Serialize}; @@ -63,6 +63,31 @@ impl From for StateOnOff { } } +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct StateColorTemperature { + #[serde(deserialize_with = "state_deserializer")] + state: bool, + brightness: f32, + color_temp: u32, +} + +impl LightState for StateColorTemperature {} + +impl From for StateOnOff { + fn from(state: StateColorTemperature) -> Self { + StateOnOff { state: state.state } + } +} + +impl From for StateBrightness { + fn from(state: StateColorTemperature) -> Self { + StateBrightness { + state: state.state, + brightness: state.brightness, + } + } +} + #[derive(Debug, Clone)] pub struct Light { config: Config, @@ -72,6 +97,7 @@ pub struct Light { pub type LightOnOff = Light; pub type LightBrightness = Light; +pub type LightColorTemperature = Light; impl Light { async fn state(&self) -> RwLockReadGuard { @@ -181,6 +207,47 @@ impl OnMqtt for Light { } } +#[async_trait] +impl OnMqtt for Light { + async fn on_mqtt(&self, message: Publish) { + // Check if the message is from the deviec itself or from a remote + if matches(&message.topic, &self.config.mqtt.topic) { + let state = match serde_json::from_slice::(&message.payload) { + Ok(state) => state, + Err(err) => { + warn!(id = Device::get_id(self), "Failed to parse message: {err}"); + return; + } + }; + + { + let current_state = self.state().await; + // No need to do anything if the state has not changed + if state.state == current_state.state + && state.brightness == current_state.brightness + && state.color_temp == current_state.color_temp + { + return; + } + } + + self.state_mut().await.state = state.state; + self.state_mut().await.brightness = state.brightness; + self.state_mut().await.color_temp = state.color_temp; + debug!( + id = Device::get_id(self), + "Updating state to {:?}", + self.state().await + ); + + self.config + .callback + .call(self, self.state().await.deref()) + .await; + } + } +} + #[async_trait] impl OnPresence for Light { async fn on_presence(&self, presence: bool) { @@ -297,3 +364,50 @@ where Ok(()) } } + +#[async_trait] +impl ColorSetting for Light +where + T: LightState, + T: Into, +{ + fn color_temperature_range(&self) -> ColorTemperatureRange { + ColorTemperatureRange { + temperature_min_k: 2200, + temperature_max_k: 4000, + } + } + + async fn color(&self) -> Color { + let state = self.state().await; + let state: StateColorTemperature = state.deref().clone().into(); + + let temperature = 1_000_000 / state.color_temp; + + Color { temperature } + } + + async fn set_color(&self, color: Color) -> Result<(), ErrorCode> { + let temperature = 1_000_000 / color.temperature; + + let message = json!({ + "color_temp": temperature, + }); + + let topic = format!("{}/set", self.config.mqtt.topic); + // TODO: Handle potential errors here + self.config + .client + .publish( + &topic, + rumqttc::QoS::AtLeastOnce, + false, + serde_json::to_string(&message).unwrap(), + ) + .await + .map_err(|err| warn!("Failed to update state on {topic}: {err}")) + .ok(); + + Ok(()) + } +}