diff --git a/Cargo.lock b/Cargo.lock index bcc6139..0e31e50 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anyhow" version = "1.0.72" @@ -65,6 +80,7 @@ dependencies = [ "bytes", "console-subscriber", "dotenvy", + "enum_dispatch", "eui48", "futures", "google-home", @@ -78,6 +94,7 @@ dependencies = [ "serde-tuple-vec-map", "serde_json", "serde_repr", + "serde_with", "thiserror", "tokio", "toml", @@ -201,6 +218,19 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "winapi", +] + [[package]] name = "console-api" version = "0.5.0" @@ -281,6 +311,50 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "darling" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "177e3443818124b357d8e76f53be906d60937f0d3a90773a664fa63fa253e621" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.28", +] + +[[package]] +name = "darling_macro" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "deranged" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7684a49fb1af197853ef7b2ee694bc1f5b4179556f1e5710e1760c5db6f5e929" +dependencies = [ + "serde", +] + [[package]] name = "dotenvy" version = "0.15.7" @@ -302,6 +376,24 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "enum_dispatch" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f33313078bb8d4d05a2733a94ac4c2d8a0df9a2b84424ebf4f33bfc224a890e" +dependencies = [ + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "eui48" version = "1.1.0" @@ -483,7 +575,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap", + "indexmap 1.9.3", "slab", "tokio", "tokio-util", @@ -496,6 +588,12 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" + [[package]] name = "hdrhistogram" version = "7.5.2" @@ -611,6 +709,35 @@ dependencies = [ "tokio-io-timeout", ] +[[package]] +name = "iana-time-zone" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.4.0" @@ -636,7 +763,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", - "hashbrown", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +dependencies = [ + "equivalent", + "hashbrown 0.14.0", + "serde", ] [[package]] @@ -1280,6 +1419,35 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1402f54f9a3b9e2efe71c1cea24e648acce55887983553eeb858cf3115acfd49" +dependencies = [ + "base64 0.21.2", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.0.0", + "serde", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9197f1ad0e3c173a0222d3c4404fb04c3afe87e962bcb327af73e8301fa203c7" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.28", +] + [[package]] name = "sharded-slab" version = "0.1.4" @@ -1339,6 +1507,12 @@ dependencies = [ "lock_api", ] +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "syn" version = "1.0.109" @@ -1397,6 +1571,34 @@ dependencies = [ "once_cell", ] +[[package]] +name = "time" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fdd63d58b18d663fbdf70e049f00a22c8e42be082203be7f26589213cd75ea" +dependencies = [ + "deranged", + "itoa", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" + +[[package]] +name = "time-macros" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb71511c991639bb078fd5bf97757e03914361c48100d52878b8e52b46fb92cd" +dependencies = [ + "time-core", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -1542,7 +1744,7 @@ checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" dependencies = [ "futures-core", "futures-util", - "indexmap", + "indexmap 1.9.3", "pin-project", "pin-project-lite", "rand", @@ -1821,6 +2023,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/Cargo.toml b/Cargo.toml index 11bfa85..20fdab4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,8 @@ wakey = "0.3.0" console-subscriber = "0.1.8" tracing-subscriber = "0.3.16" serde-tuple-vec-map = "1.0.1" +serde_with = "3.2.0" +enum_dispatch = "0.3.12" [patch.crates-io] wakey = { git = "https://github.com/DreadedX/wakey" } diff --git a/impl_cast/src/lib.rs b/impl_cast/src/lib.rs index e92740e..a94adc5 100644 --- a/impl_cast/src/lib.rs +++ b/impl_cast/src/lib.rs @@ -73,7 +73,7 @@ pub fn device(attr: TokenStream, item: TokenStream) -> TokenStream { let prefix = quote! { pub trait #name { - fn consume(self: Box) -> Option>; + fn is(&self) -> bool; fn cast(&self) -> Option<&T>; fn cast_mut(&mut self) -> Option<&mut T>; } @@ -95,8 +95,8 @@ pub fn device(attr: TokenStream, item: TokenStream) -> TokenStream { where T: #interface_ident + 'static, { - default fn consume(self: Box) -> Option> { - None + default fn is(&self) -> bool { + false } default fn cast(&self) -> Option<&(dyn #device_trait + 'static)> { @@ -114,8 +114,8 @@ pub fn device(attr: TokenStream, item: TokenStream) -> TokenStream { where T: #interface_ident + #device_trait + 'static, { - fn consume(self: Box) -> Option> { - Some(self) + fn is(&self) -> bool { + true } fn cast(&self) -> Option<&(dyn #device_trait + 'static)> { diff --git a/src/config.rs b/src/config.rs index 39df54f..6c079f3 100644 --- a/src/config.rs +++ b/src/config.rs @@ -5,6 +5,7 @@ use std::{ }; use async_trait::async_trait; +use enum_dispatch::enum_dispatch; use regex::{Captures, Regex}; use rumqttc::{AsyncClient, MqttOptions, Transport}; use serde::{Deserialize, Deserializer}; @@ -14,10 +15,11 @@ use crate::{ auth::OpenIDConfig, device_manager::DeviceManager, devices::{ - AudioSetup, ContactSensor, DebugBridgeConfig, Device, HueBridgeConfig, HueLight, - IkeaOutlet, KasaOutlet, LightSensorConfig, PresenceConfig, WakeOnLAN, Washer, + AudioSetupConfig, ContactSensorConfig, DebugBridgeConfig, Device, HueBridgeConfig, + HueLightConfig, IkeaOutletConfig, KasaOutletConfig, LightSensorConfig, PresenceConfig, + WakeOnLANConfig, WasherConfig, }, - error::{ConfigParseError, CreateDeviceError, MissingEnv}, + error::{ConfigParseError, DeviceConfigError, MissingEnv}, event::EventChannel, }; @@ -34,7 +36,7 @@ pub struct Config { pub hue_bridge: Option, pub debug_bridge: Option, #[serde(default, with = "tuple_vec_map")] - pub devices: Vec<(String, DeviceConfig)>, + pub devices: Vec<(String, DeviceConfigs)>, } #[derive(Debug, Clone, Deserialize)] @@ -122,18 +124,6 @@ pub struct MqttDeviceConfig { pub topic: String, } -#[derive(Debug, Clone, Deserialize)] -#[serde(tag = "type")] -pub enum DeviceConfig { - AudioSetup(::Config), - ContactSensor(::Config), - IkeaOutlet(::Config), - KasaOutlet(::Config), - WakeOnLAN(::Config), - Washer(::Config), - HueLight(::Config), -} - impl Config { pub fn parse_file(filename: &str) -> Result { debug!("Loading config: {filename}"); @@ -162,50 +152,32 @@ impl Config { } } +pub struct ConfigExternal<'a> { + pub client: &'a AsyncClient, + pub device_manager: &'a DeviceManager, + pub presence_topic: &'a str, + pub event_channel: &'a EventChannel, +} + #[async_trait] -pub trait CreateDevice { - type Config; - +#[enum_dispatch] +pub trait DeviceConfig { async fn create( - identifier: &str, - config: Self::Config, - event_channel: &EventChannel, - client: &AsyncClient, - // TODO: Not a big fan of passing in the global config - presence_topic: &str, - devices: &DeviceManager, - ) -> Result - where - Self: Sized; -} - -macro_rules! create { - (($self:ident, $id:ident, $event_channel:ident, $client:ident, $presence_topic:ident, $device_manager:ident), [ $( $Variant:ident ),* ]) => { - match $self { - $(DeviceConfig::$Variant(c) => Box::new($Variant::create($id, c, $event_channel, $client, $presence_topic, $device_manager).await?),)* - } - }; -} - -impl DeviceConfig { - pub async fn create( self, - id: &str, - event_channel: &EventChannel, - client: &AsyncClient, - presence_topic: &str, - device_manager: &DeviceManager, - ) -> Result, CreateDeviceError> { - Ok(create! { - (self, id, event_channel, client, presence_topic, device_manager), [ - AudioSetup, - ContactSensor, - IkeaOutlet, - KasaOutlet, - WakeOnLAN, - Washer, - HueLight - ] - }) - } + identifier: &str, + ext: &ConfigExternal, + ) -> Result, DeviceConfigError>; +} + +#[derive(Debug, Deserialize)] +#[serde(tag = "type")] +#[enum_dispatch(DeviceConfig)] +pub enum DeviceConfigs { + AudioSetup(AudioSetupConfig), + ContactSensor(ContactSensorConfig), + IkeaOutlet(IkeaOutletConfig), + KasaOutlet(KasaOutletConfig), + WakeOnLAN(WakeOnLANConfig), + Washer(WasherConfig), + HueLight(HueLightConfig), } diff --git a/src/devices/audio_setup.rs b/src/devices/audio_setup.rs index 908e515..bf47f9d 100644 --- a/src/devices/audio_setup.rs +++ b/src/devices/audio_setup.rs @@ -1,15 +1,13 @@ use async_trait::async_trait; use google_home::traits::OnOff; -use rumqttc::AsyncClient; use serde::Deserialize; use tracing::{debug, error, trace, warn}; use crate::{ - config::{CreateDevice, MqttDeviceConfig}, - device_manager::{DeviceManager, WrappedDevice}, + config::{ConfigExternal, DeviceConfig, MqttDeviceConfig}, + device_manager::WrappedDevice, devices::As, - error::CreateDeviceError, - event::EventChannel, + error::DeviceConfigError, event::OnMqtt, event::OnPresence, messages::{RemoteAction, RemoteMessage}, @@ -25,63 +23,66 @@ pub struct AudioSetupConfig { speakers: String, } +#[async_trait] +impl DeviceConfig for AudioSetupConfig { + async fn create( + self, + identifier: &str, + ext: &ConfigExternal, + ) -> Result, DeviceConfigError> { + trace!(id = identifier, "Setting up AudioSetup"); + + // TODO: Make sure they implement OnOff? + let mixer = ext + .device_manager + .get(&self.mixer) + .await + // NOTE: We need to clone to make the compiler happy, how ever if this clone happens the next one can never happen... + .ok_or(DeviceConfigError::MissingChild( + identifier.into(), + self.mixer.clone(), + ))?; + + if !As::::is(mixer.read().await.as_ref()) { + return Err(DeviceConfigError::MissingTrait(self.mixer, "OnOff".into())); + } + + let speakers = + ext.device_manager + .get(&self.speakers) + .await + .ok_or(DeviceConfigError::MissingChild( + identifier.into(), + self.speakers.clone(), + ))?; + + if !As::::is(speakers.read().await.as_ref()) { + return Err(DeviceConfigError::MissingTrait( + self.speakers, + "OnOff".into(), + )); + } + + let device = AudioSetup { + identifier: identifier.to_owned(), + mqtt: self.mqtt, + mixer, + speakers, + }; + + Ok(Box::new(device)) + } +} + // TODO: We need a better way to store the children devices #[derive(Debug)] -pub struct AudioSetup { +struct AudioSetup { identifier: String, mqtt: MqttDeviceConfig, mixer: WrappedDevice, speakers: WrappedDevice, } -#[async_trait] -impl CreateDevice for AudioSetup { - type Config = AudioSetupConfig; - - async fn create( - identifier: &str, - config: Self::Config, - _event_channel: &EventChannel, - _client: &AsyncClient, - _presence_topic: &str, - device_manager: &DeviceManager, - ) -> Result { - trace!(id = identifier, "Setting up AudioSetup"); - - // TODO: Make sure they implement OnOff? - let mixer = device_manager - .get(&config.mixer) - .await - // NOTE: We need to clone to make the compiler happy, how ever if this clone happens the next one can never happen... - .ok_or(CreateDeviceError::DeviceDoesNotExist(config.mixer.clone()))?; - - { - let mixer = mixer.read().await; - if As::::cast(mixer.as_ref()).is_none() { - return Err(CreateDeviceError::OnOffExpected(config.mixer)); - } - } - - let speakers = device_manager.get(&config.speakers).await.ok_or( - CreateDeviceError::DeviceDoesNotExist(config.speakers.clone()), - )?; - - { - let speakers = speakers.read().await; - if As::::cast(speakers.as_ref()).is_none() { - return Err(CreateDeviceError::OnOffExpected(config.speakers)); - } - } - - Ok(Self { - identifier: identifier.to_owned(), - mqtt: config.mqtt, - mixer, - speakers, - }) - } -} - impl Device for AudioSetup { fn get_id(&self) -> &str { &self.identifier diff --git a/src/devices/contact_sensor.rs b/src/devices/contact_sensor.rs index 063a60b..c89aae3 100644 --- a/src/devices/contact_sensor.rs +++ b/src/devices/contact_sensor.rs @@ -4,15 +4,15 @@ use async_trait::async_trait; use google_home::traits::OnOff; use rumqttc::{has_wildcards, AsyncClient}; use serde::Deserialize; +use serde_with::{serde_as, DurationSeconds}; use tokio::task::JoinHandle; use tracing::{debug, error, trace, warn}; use crate::{ - config::{CreateDevice, MqttDeviceConfig}, - device_manager::{DeviceManager, WrappedDevice}, + config::{ConfigExternal, DeviceConfig, MqttDeviceConfig}, + device_manager::WrappedDevice, devices::{As, DEFAULT_PRESENCE}, - error::{CreateDeviceError, MissingWildcard}, - event::EventChannel, + error::{DeviceConfigError, MissingWildcard}, event::OnMqtt, event::OnPresence, messages::{ContactMessage, PresenceMessage}, @@ -22,11 +22,13 @@ use crate::{ use super::Device; // NOTE: If we add more presence devices we might need to move this out of here +#[serde_as] #[derive(Debug, Clone, Deserialize)] pub struct PresenceDeviceConfig { #[serde(flatten)] pub mqtt: Option, - pub timeout: u64, // Timeout in seconds + #[serde_as(as = "DurationSeconds")] + pub timeout: Duration, } impl PresenceDeviceConfig { @@ -56,11 +58,13 @@ impl PresenceDeviceConfig { } } +#[serde_as] #[derive(Debug, Clone, Deserialize)] pub struct LightsConfig { lights: Vec, #[serde(default)] - timeout: u64, // Timeout in seconds + #[serde_as(as = "DurationSeconds")] + pub timeout: Duration, } #[derive(Debug, Clone, Deserialize)] @@ -71,14 +75,66 @@ pub struct ContactSensorConfig { lights: Option, } +#[async_trait] +impl DeviceConfig for ContactSensorConfig { + async fn create( + self, + identifier: &str, + ext: &ConfigExternal, + ) -> Result, DeviceConfigError> { + trace!(id = identifier, "Setting up ContactSensor"); + + let presence = self + .presence + .map(|p| p.generate_topic("contact", identifier, ext.presence_topic)) + .transpose()?; + + let lights = + if let Some(lights_config) = self.lights { + let mut lights = Vec::new(); + for name in lights_config.lights { + let light = ext.device_manager.get(&name).await.ok_or( + DeviceConfigError::MissingChild(name.clone(), "OnOff".into()), + )?; + + if !As::::is(light.read().await.as_ref()) { + return Err(DeviceConfigError::MissingTrait(name, "OnOff".into())); + } + + lights.push((light, false)); + } + + Some(Lights { + lights, + timeout: lights_config.timeout, + }) + } else { + None + }; + + let device = ContactSensor { + identifier: identifier.to_owned(), + mqtt: self.mqtt, + presence, + client: ext.client.clone(), + overall_presence: DEFAULT_PRESENCE, + is_closed: true, + handle: None, + lights, + }; + + Ok(Box::new(device)) + } +} + #[derive(Debug)] -pub struct Lights { +struct Lights { lights: Vec<(WrappedDevice, bool)>, timeout: Duration, // Timeout in seconds } #[derive(Debug)] -pub struct ContactSensor { +struct ContactSensor { identifier: String, mqtt: MqttDeviceConfig, presence: Option, @@ -91,64 +147,6 @@ pub struct ContactSensor { lights: Option, } -#[async_trait] -impl CreateDevice for ContactSensor { - type Config = ContactSensorConfig; - - async fn create( - identifier: &str, - config: Self::Config, - _event_channel: &EventChannel, - client: &AsyncClient, - presence_topic: &str, - device_manager: &DeviceManager, - ) -> Result { - trace!(id = identifier, "Setting up ContactSensor"); - - let presence = config - .presence - .map(|p| p.generate_topic("contact", identifier, presence_topic)) - .transpose()?; - - let lights = if let Some(lights_config) = config.lights { - let mut lights = Vec::new(); - for name in lights_config.lights { - let light = device_manager - .get(&name) - .await - .ok_or(CreateDeviceError::DeviceDoesNotExist(name.clone()))?; - - { - let light = light.read().await; - if As::::cast(light.as_ref()).is_none() { - return Err(CreateDeviceError::OnOffExpected(name)); - } - } - - lights.push((light, false)); - } - - Some(Lights { - lights, - timeout: Duration::from_secs(lights_config.timeout), - }) - } else { - None - }; - - Ok(Self { - identifier: identifier.to_owned(), - mqtt: config.mqtt, - presence, - client: client.clone(), - overall_presence: DEFAULT_PRESENCE, - is_closed: true, - handle: None, - lights, - }) - } -} - impl Device for ContactSensor { fn get_id(&self) -> &str { &self.identifier @@ -249,7 +247,7 @@ impl OnMqtt for ContactSensor { // Once the door is closed again we start a timeout for removing the presence let client = self.client.clone(); let id = self.identifier.clone(); - let timeout = Duration::from_secs(presence.timeout); + let timeout = presence.timeout; self.handle = Some(tokio::spawn(async move { debug!(id, "Starting timeout ({timeout:?}) for contact sensor..."); tokio::time::sleep(timeout).await; diff --git a/src/devices/debug_bridge.rs b/src/devices/debug_bridge.rs index 10aa63c..a79dc87 100644 --- a/src/devices/debug_bridge.rs +++ b/src/devices/debug_bridge.rs @@ -24,14 +24,11 @@ pub struct DebugBridge { } impl DebugBridge { - pub fn new( - config: DebugBridgeConfig, - client: &AsyncClient, - ) -> Result { - Ok(Self { + pub fn new(config: DebugBridgeConfig, client: &AsyncClient) -> Self { + Self { mqtt: config.mqtt, client: client.clone(), - }) + } } } diff --git a/src/devices/hue_bridge.rs b/src/devices/hue_bridge.rs index b72c77c..080a93a 100644 --- a/src/devices/hue_bridge.rs +++ b/src/devices/hue_bridge.rs @@ -5,14 +5,17 @@ use std::{ use async_trait::async_trait; use google_home::{errors::ErrorCode, traits::OnOff}; -use rumqttc::AsyncClient; use serde::{Deserialize, Serialize}; use serde_json::Value; use tracing::{debug, error, trace, warn}; use crate::{ - config::CreateDevice, device_manager::DeviceManager, devices::Device, error::CreateDeviceError, - event::EventChannel, event::OnDarkness, event::OnPresence, traits::Timeout, + config::{ConfigExternal, DeviceConfig}, + devices::Device, + error::DeviceConfigError, + event::OnDarkness, + event::OnPresence, + traits::Timeout, }; #[derive(Debug)] @@ -117,8 +120,27 @@ pub struct HueLightConfig { pub timer_id: isize, } +#[async_trait] +impl DeviceConfig for HueLightConfig { + async fn create( + self, + identifier: &str, + _ext: &ConfigExternal, + ) -> Result, DeviceConfigError> { + let device = HueLight { + identifier: identifier.to_owned(), + addr: (self.ip, 80).into(), + login: self.login, + light_id: self.light_id, + timer_id: self.timer_id, + }; + + Ok(Box::new(device)) + } +} + #[derive(Debug)] -pub struct HueLight { +struct HueLight { pub identifier: String, pub addr: SocketAddr, pub login: String, @@ -126,28 +148,6 @@ pub struct HueLight { pub timer_id: isize, } -#[async_trait] -impl CreateDevice for HueLight { - type Config = HueLightConfig; - - async fn create( - identifier: &str, - config: Self::Config, - _event_channel: &EventChannel, - _client: &AsyncClient, - _presence_topic: &str, - _devices: &DeviceManager, - ) -> Result { - Ok(Self { - identifier: identifier.to_owned(), - addr: (config.ip, 80).into(), - login: config.login, - light_id: config.light_id, - timer_id: config.timer_id, - }) - } -} - impl Device for HueLight { fn get_id(&self) -> &str { &self.identifier diff --git a/src/devices/ikea_outlet.rs b/src/devices/ikea_outlet.rs index f22a137..c5f26aa 100644 --- a/src/devices/ikea_outlet.rs +++ b/src/devices/ikea_outlet.rs @@ -8,15 +8,15 @@ use google_home::{ }; use rumqttc::{AsyncClient, Publish}; use serde::Deserialize; +use serde_with::serde_as; +use serde_with::DurationSeconds; use std::time::Duration; use tokio::task::JoinHandle; use tracing::{debug, error, trace, warn}; -use crate::config::{CreateDevice, InfoConfig, MqttDeviceConfig}; -use crate::device_manager::DeviceManager; +use crate::config::{ConfigExternal, DeviceConfig, InfoConfig, MqttDeviceConfig}; use crate::devices::Device; -use crate::error::CreateDeviceError; -use crate::event::EventChannel; +use crate::error::DeviceConfigError; use crate::event::OnMqtt; use crate::event::OnPresence; use crate::messages::OnOffMessage; @@ -30,6 +30,7 @@ pub enum OutletType { Light, } +#[serde_as] #[derive(Debug, Clone, Deserialize)] pub struct IkeaOutletConfig { #[serde(flatten)] @@ -38,15 +39,45 @@ pub struct IkeaOutletConfig { mqtt: MqttDeviceConfig, #[serde(default = "default_outlet_type")] outlet_type: OutletType, - timeout: Option, // Timeout in seconds + #[serde_as(as = "Option")] + timeout: Option, // Timeout in seconds } fn default_outlet_type() -> OutletType { OutletType::Outlet } +#[async_trait] +impl DeviceConfig for IkeaOutletConfig { + async fn create( + self, + identifier: &str, + ext: &ConfigExternal, + ) -> Result, DeviceConfigError> { + trace!( + id = identifier, + name = self.info.name, + room = self.info.room, + "Setting up IkeaOutlet" + ); + + let device = IkeaOutlet { + identifier: identifier.to_owned(), + info: self.info, + mqtt: self.mqtt, + outlet_type: self.outlet_type, + timeout: self.timeout, + client: ext.client.clone(), + last_known_state: false, + handle: None, + }; + + Ok(Box::new(device)) + } +} + #[derive(Debug)] -pub struct IkeaOutlet { +struct IkeaOutlet { identifier: String, info: InfoConfig, mqtt: MqttDeviceConfig, @@ -58,38 +89,6 @@ pub struct IkeaOutlet { handle: Option>, } -#[async_trait] -impl CreateDevice for IkeaOutlet { - type Config = IkeaOutletConfig; - - async fn create( - identifier: &str, - config: Self::Config, - _event_channel: &EventChannel, - client: &AsyncClient, - _presence_topic: &str, - _device_manager: &DeviceManager, - ) -> Result { - trace!( - id = identifier, - name = config.info.name, - room = config.info.room, - "Setting up IkeaOutlet" - ); - - Ok(Self { - identifier: identifier.to_owned(), - info: config.info, - mqtt: config.mqtt, - outlet_type: config.outlet_type, - timeout: config.timeout.map(Duration::from_secs), - client: client.clone(), - last_known_state: false, - handle: None, - }) - } -} - async fn set_on(client: AsyncClient, topic: &str, on: bool) { let message = OnOffMessage::new(on); diff --git a/src/devices/kasa_outlet.rs b/src/devices/kasa_outlet.rs index dd2c679..deab941 100644 --- a/src/devices/kasa_outlet.rs +++ b/src/devices/kasa_outlet.rs @@ -9,7 +9,6 @@ use google_home::{ errors::{self, DeviceError}, traits, }; -use rumqttc::AsyncClient; use serde::{Deserialize, Serialize}; use thiserror::Error; use tokio::{ @@ -19,8 +18,8 @@ use tokio::{ use tracing::trace; use crate::{ - config::CreateDevice, device_manager::DeviceManager, error::CreateDeviceError, - event::EventChannel, + config::{ConfigExternal, DeviceConfig}, + error::DeviceConfigError, }; use super::Device; @@ -30,33 +29,30 @@ pub struct KasaOutletConfig { ip: Ipv4Addr, } -#[derive(Debug)] -pub struct KasaOutlet { - identifier: String, - addr: SocketAddr, -} - #[async_trait] -impl CreateDevice for KasaOutlet { - type Config = KasaOutletConfig; - +impl DeviceConfig for KasaOutletConfig { async fn create( + self, identifier: &str, - config: Self::Config, - _event_channel: &EventChannel, - _client: &AsyncClient, - _presence_topic: &str, - _device_manager: &DeviceManager, - ) -> Result { + _ext: &ConfigExternal, + ) -> Result, DeviceConfigError> { trace!(id = identifier, "Setting up KasaOutlet"); - Ok(Self { + let device = KasaOutlet { identifier: identifier.to_owned(), - addr: (config.ip, 9999).into(), - }) + addr: (self.ip, 9999).into(), + }; + + Ok(Box::new(device)) } } +#[derive(Debug)] +struct KasaOutlet { + identifier: String, + addr: SocketAddr, +} + impl Device for KasaOutlet { fn get_id(&self) -> &str { &self.identifier diff --git a/src/devices/mod.rs b/src/devices/mod.rs index 3729964..64ebf2a 100644 --- a/src/devices/mod.rs +++ b/src/devices/mod.rs @@ -10,17 +10,17 @@ mod presence; mod wake_on_lan; mod washer; -pub use self::audio_setup::AudioSetup; -pub use self::contact_sensor::ContactSensor; +pub use self::audio_setup::AudioSetupConfig; +pub use self::contact_sensor::ContactSensorConfig; pub use self::debug_bridge::{DebugBridge, DebugBridgeConfig}; -pub use self::hue_bridge::{HueBridge, HueBridgeConfig, HueLight}; -pub use self::ikea_outlet::IkeaOutlet; -pub use self::kasa_outlet::KasaOutlet; +pub use self::hue_bridge::{HueBridge, HueBridgeConfig, HueLightConfig}; +pub use self::ikea_outlet::IkeaOutletConfig; +pub use self::kasa_outlet::KasaOutletConfig; pub use self::light_sensor::{LightSensor, LightSensorConfig}; pub use self::ntfy::{Notification, Ntfy}; pub use self::presence::{Presence, PresenceConfig, DEFAULT_PRESENCE}; -pub use self::wake_on_lan::WakeOnLAN; -pub use self::washer::Washer; +pub use self::wake_on_lan::WakeOnLANConfig; +pub use self::washer::WasherConfig; use google_home::{device::AsGoogleHomeDevice, traits::OnOff}; diff --git a/src/devices/wake_on_lan.rs b/src/devices/wake_on_lan.rs index 937909b..410a2de 100644 --- a/src/devices/wake_on_lan.rs +++ b/src/devices/wake_on_lan.rs @@ -9,15 +9,13 @@ use google_home::{ types::Type, GoogleHomeDevice, }; -use rumqttc::{AsyncClient, Publish}; +use rumqttc::Publish; use serde::Deserialize; use tracing::{debug, error, trace}; use crate::{ - config::{CreateDevice, InfoConfig, MqttDeviceConfig}, - device_manager::DeviceManager, - error::CreateDeviceError, - event::EventChannel, + config::{ConfigExternal, DeviceConfig, InfoConfig, MqttDeviceConfig}, + error::DeviceConfigError, event::OnMqtt, messages::ActivateMessage, }; @@ -39,8 +37,34 @@ fn default_broadcast_ip() -> Ipv4Addr { Ipv4Addr::new(255, 255, 255, 255) } +#[async_trait] +impl DeviceConfig for WakeOnLANConfig { + async fn create( + self, + identifier: &str, + _ext: &ConfigExternal, + ) -> Result, DeviceConfigError> { + trace!( + id = identifier, + name = self.info.name, + room = self.info.room, + "Setting up WakeOnLAN" + ); + + let device = WakeOnLAN { + identifier: identifier.to_owned(), + info: self.info, + mqtt: self.mqtt, + mac_address: self.mac_address, + broadcast_ip: self.broadcast_ip, + }; + + Ok(Box::new(device)) + } +} + #[derive(Debug)] -pub struct WakeOnLAN { +struct WakeOnLAN { identifier: String, info: InfoConfig, mqtt: MqttDeviceConfig, @@ -48,35 +72,6 @@ pub struct WakeOnLAN { broadcast_ip: Ipv4Addr, } -#[async_trait] -impl CreateDevice for WakeOnLAN { - type Config = WakeOnLANConfig; - - async fn create( - identifier: &str, - config: Self::Config, - _event_channel: &EventChannel, - _client: &AsyncClient, - _presence_topic: &str, - _device_manager: &DeviceManager, - ) -> Result { - trace!( - id = identifier, - name = config.info.name, - room = config.info.room, - "Setting up WakeOnLAN" - ); - - Ok(Self { - identifier: identifier.to_owned(), - info: config.info, - mqtt: config.mqtt, - mac_address: config.mac_address, - broadcast_ip: config.broadcast_ip, - }) - } -} - impl Device for WakeOnLAN { fn get_id(&self) -> &str { &self.identifier diff --git a/src/devices/washer.rs b/src/devices/washer.rs index f62da95..6e25cda 100644 --- a/src/devices/washer.rs +++ b/src/devices/washer.rs @@ -1,12 +1,11 @@ use async_trait::async_trait; -use rumqttc::{AsyncClient, Publish}; +use rumqttc::Publish; use serde::Deserialize; use tracing::{debug, error, warn}; use crate::{ - config::{CreateDevice, MqttDeviceConfig}, - device_manager::DeviceManager, - error::CreateDeviceError, + config::{ConfigExternal, DeviceConfig, MqttDeviceConfig}, + error::DeviceConfigError, event::{Event, EventChannel, OnMqtt}, messages::PowerMessage, }; @@ -20,10 +19,29 @@ pub struct WasherConfig { threshold: f32, // Power in Watt } +#[async_trait] +impl DeviceConfig for WasherConfig { + async fn create( + self, + identifier: &str, + ext: &ConfigExternal, + ) -> Result, DeviceConfigError> { + let device = Washer { + identifier: identifier.to_owned(), + mqtt: self.mqtt, + event_channel: ext.event_channel.clone(), + threshold: self.threshold, + running: 0, + }; + + Ok(Box::new(device)) + } +} + // TODO: Add google home integration #[derive(Debug)] -pub struct Washer { +struct Washer { identifier: String, mqtt: MqttDeviceConfig, @@ -32,28 +50,6 @@ pub struct Washer { running: isize, } -#[async_trait] -impl CreateDevice for Washer { - type Config = WasherConfig; - - async fn create( - identifier: &str, - config: Self::Config, - event_channel: &EventChannel, - _client: &AsyncClient, - _presence_topic: &str, - _device_manager: &DeviceManager, - ) -> Result { - Ok(Self { - identifier: identifier.to_owned(), - mqtt: config.mqtt, - event_channel: event_channel.clone(), - threshold: config.threshold, - running: 0, - }) - } -} - impl Device for Washer { fn get_id(&self) -> &str { &self.identifier diff --git a/src/error.rs b/src/error.rs index b22fa9b..95b1dcb 100644 --- a/src/error.rs +++ b/src/error.rs @@ -90,11 +90,11 @@ impl MissingWildcard { } #[derive(Debug, Error)] -pub enum CreateDeviceError { - #[error("Child device '{0}' does not exist (yet?)")] - DeviceDoesNotExist(String), - #[error("Expected device '{0}' to implement OnOff trait")] - OnOffExpected(String), +pub enum DeviceConfigError { + #[error("Child '{1}' of device '{0}' does not exist")] + MissingChild(String, String), + #[error("Device '{0}' does not implement expected trait '{1}'")] + MissingTrait(String, String), #[error(transparent)] MissingWildcard(#[from] MissingWildcard), } diff --git a/src/main.rs b/src/main.rs index f0cab4b..bd6a366 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,7 +10,7 @@ use tracing::{debug, error, info}; use automation::{ auth::{OpenIDConfig, User}, - config::Config, + config::{Config, ConfigExternal, DeviceConfig}, device_manager::DeviceManager, devices::{DebugBridge, HueBridge, LightSensor, Ntfy, Presence}, error::ApiError, @@ -61,16 +61,15 @@ async fn app() -> anyhow::Result<()> { let event_channel = device_manager.start(); // Create all the devices specified in the config + let ext = ConfigExternal { + client: &client, + device_manager: &device_manager, + presence_topic: &config.presence.mqtt.topic, + event_channel: &event_channel, + }; + for (id, device_config) in config.devices { - let device = device_config - .create( - &id, - &event_channel, - &client, - &config.presence.mqtt.topic, - &device_manager, - ) - .await?; + let device = device_config.create(&id, &ext).await?; device_manager.add(device).await; } @@ -95,7 +94,7 @@ async fn app() -> anyhow::Result<()> { // Start the debug bridge if it is configured if let Some(config) = config.debug_bridge { - let debug_bridge = DebugBridge::new(config, &client)?; + let debug_bridge = DebugBridge::new(config, &client); device_manager.add(Box::new(debug_bridge)).await; }