diff --git a/config/ares.dev.toml b/config/ares.dev.toml index 53ac067..bdd25f5 100644 --- a/config/ares.dev.toml +++ b/config/ares.dev.toml @@ -2,12 +2,12 @@ base_url = "https://login.huizinga.dev/api/oidc" [mqtt] -host="olympus.vpn.huizinga.dev" -port=8883 -client_name="automation-ares" -username="mqtt" -password="${MQTT_PASSWORD}" -tls=true +host = "olympus.vpn.huizinga.dev" +port = 8883 +client_name = "automation-ares" +username = "mqtt" +password = "${MQTT_PASSWORD}" +tls = true [ntfy] topic = "${NTFY_TOPIC}" diff --git a/config/config.toml b/config/config.toml index ca138c0..31bf74a 100644 --- a/config/config.toml +++ b/config/config.toml @@ -65,15 +65,19 @@ topic = "automation/appliance/living_room/zeus" mac_address = "30:9c:23:60:9c:13" broadcast_ip = "10.0.0.255" +[devices.living_mixer] +type = "KasaOutlet" +ip = "10.0.0.49" + +[devices.living_speakers] +type = "KasaOutlet" +ip = "10.0.0.182" + [devices.living_audio] type = "AudioSetup" topic = "zigbee2mqtt/living/remote" -[devices.living_audio.mixer] -type = "KasaOutlet" -ip = "10.0.0.49" -[devices.living_audio.speakers] -type = "KasaOutlet" -ip = "10.0.0.182" +mixer = "living_mixer" +speakers = "living_speakers" [devices.hallway_frontdoor] type = "ContactSensor" diff --git a/config/zeus.dev.toml b/config/zeus.dev.toml index 792a285..66e1876 100644 --- a/config/zeus.dev.toml +++ b/config/zeus.dev.toml @@ -65,15 +65,19 @@ room = "Living Room" topic = "automation/appliance/living_room/zeus" mac_address = "30:9c:23:60:9c:13" +[devices.living_mixer] +type = "KasaOutlet" +ip = "10.0.0.49" + +[devices.living_speakers] +type = "KasaOutlet" +ip = "10.0.0.182" + [devices.living_audio] type = "AudioSetup" topic = "zigbee2mqtt/living/remote" -[devices.living_audio.mixer] -type = "KasaOutlet" -ip = "10.0.0.49" -[devices.living_audio.speakers] -type = "KasaOutlet" -ip = "10.0.0.182" +mixer = "light_sensor" +speakers = "living_speakers" [devices.hallway_frontdoor] type = "ContactSensor" diff --git a/src/config.rs b/src/config.rs index c4d4047..f5098cc 100644 --- a/src/config.rs +++ b/src/config.rs @@ -4,6 +4,7 @@ use std::{ time::Duration, }; +use async_trait::async_trait; use regex::{Captures, Regex}; use rumqttc::{AsyncClient, MqttOptions, Transport}; use serde::{Deserialize, Deserializer}; @@ -11,6 +12,7 @@ use tracing::debug; use crate::{ auth::OpenIDConfig, + device_manager::DeviceManager, devices::{ AudioSetup, ContactSensor, DebugBridgeConfig, Device, HueBridgeConfig, IkeaOutlet, KasaOutlet, LightSensorConfig, PresenceConfig, WakeOnLAN, @@ -158,39 +160,42 @@ impl Config { } } +#[async_trait] pub trait CreateDevice { type Config; - fn create( + 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), [ $( $Variant:ident ),* ]) => { + (($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)?),)* + $(DeviceConfig::$Variant(c) => Box::new($Variant::create($id, c, $event_channel, $client, $presence_topic, $device_manager).await?),)* } }; } impl DeviceConfig { - pub fn create( + 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), [ + (self, id, event_channel, client, presence_topic, device_manager), [ AudioSetup, ContactSensor, IkeaOutlet, diff --git a/src/device_manager.rs b/src/device_manager.rs index e0db3a7..60cb1c6 100644 --- a/src/device_manager.rs +++ b/src/device_manager.rs @@ -14,7 +14,8 @@ use crate::{ event::{Event, EventChannel, OnMqtt}, }; -pub type DeviceMap = HashMap>>>; +pub type WrappedDevice = Arc>>; +pub type DeviceMap = HashMap; #[derive(Debug, Clone)] pub struct DeviceManager { @@ -70,6 +71,10 @@ impl DeviceManager { self.devices.write().await.insert(id, device); } + pub async fn get(&self, name: &str) -> Option { + self.devices.read().await.get(name).cloned() + } + pub async fn devices(&self) -> RwLockReadGuard { self.devices.read().await } diff --git a/src/devices/audio_setup.rs b/src/devices/audio_setup.rs index 316d37a..908e515 100644 --- a/src/devices/audio_setup.rs +++ b/src/devices/audio_setup.rs @@ -1,11 +1,13 @@ use async_trait::async_trait; -use google_home::traits; +use google_home::traits::OnOff; use rumqttc::AsyncClient; use serde::Deserialize; use tracing::{debug, error, trace, warn}; use crate::{ - config::{self, CreateDevice, MqttDeviceConfig}, + config::{CreateDevice, MqttDeviceConfig}, + device_manager::{DeviceManager, WrappedDevice}, + devices::As, error::CreateDeviceError, event::EventChannel, event::OnMqtt, @@ -13,14 +15,14 @@ use crate::{ messages::{RemoteAction, RemoteMessage}, }; -use super::{As, Device}; +use super::Device; #[derive(Debug, Clone, Deserialize)] pub struct AudioSetupConfig { #[serde(flatten)] mqtt: MqttDeviceConfig, - mixer: Box, - speakers: Box, + mixer: String, + speakers: String, } // TODO: We need a better way to store the children devices @@ -28,32 +30,48 @@ pub struct AudioSetupConfig { pub struct AudioSetup { identifier: String, mqtt: MqttDeviceConfig, - mixer: Box, - speakers: Box, + mixer: WrappedDevice, + speakers: WrappedDevice, } +#[async_trait] impl CreateDevice for AudioSetup { type Config = AudioSetupConfig; - fn create( + async fn create( identifier: &str, config: Self::Config, - event_channel: &EventChannel, - client: &AsyncClient, - presence_topic: &str, + _event_channel: &EventChannel, + _client: &AsyncClient, + _presence_topic: &str, + device_manager: &DeviceManager, ) -> Result { trace!(id = identifier, "Setting up AudioSetup"); - // Create the child devices - let mixer_id = format!("{}.mixer", identifier); - let mixer = (*config.mixer).create(&mixer_id, event_channel, client, presence_topic)?; - let mixer = As::consume(mixer).ok_or(CreateDeviceError::OnOffExpected(mixer_id))?; + // 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 speakers_id = format!("{}.speakers", identifier); - let speakers = - (*config.speakers).create(&speakers_id, event_channel, client, presence_topic)?; - let speakers = - As::consume(speakers).ok_or(CreateDeviceError::OnOffExpected(speakers_id))?; + { + 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(), @@ -85,27 +103,34 @@ impl OnMqtt for AudioSetup { } }; - match action { - RemoteAction::On => { - if self.mixer.is_on().await.unwrap() { - self.speakers.set_on(false).await.unwrap(); - self.mixer.set_on(false).await.unwrap(); - } else { - self.speakers.set_on(true).await.unwrap(); - self.mixer.set_on(true).await.unwrap(); - } - }, - RemoteAction::BrightnessMoveUp => { - if !self.mixer.is_on().await.unwrap() { - self.mixer.set_on(true).await.unwrap(); - } else if self.speakers.is_on().await.unwrap() { - self.speakers.set_on(false).await.unwrap(); - } else { - self.speakers.set_on(true).await.unwrap(); - } - }, - RemoteAction::BrightnessStop => { /* Ignore this action */ }, - _ => warn!("Expected ikea shortcut button which only supports 'on' and 'brightness_move_up', got: {action:?}") + let mut mixer = self.mixer.write().await; + let mut speakers = self.speakers.write().await; + if let (Some(mixer), Some(speakers)) = ( + As::::cast_mut(mixer.as_mut()), + As::::cast_mut(speakers.as_mut()), + ) { + match action { + RemoteAction::On => { + if mixer.is_on().await.unwrap() { + speakers.set_on(false).await.unwrap(); + mixer.set_on(false).await.unwrap(); + } else { + speakers.set_on(true).await.unwrap(); + mixer.set_on(true).await.unwrap(); + } + }, + RemoteAction::BrightnessMoveUp => { + if !mixer.is_on().await.unwrap() { + mixer.set_on(true).await.unwrap(); + } else if speakers.is_on().await.unwrap() { + speakers.set_on(false).await.unwrap(); + } else { + speakers.set_on(true).await.unwrap(); + } + }, + RemoteAction::BrightnessStop => { /* Ignore this action */ }, + _ => warn!("Expected ikea shortcut button which only supports 'on' and 'brightness_move_up', got: {action:?}") + } } } } @@ -113,11 +138,19 @@ impl OnMqtt for AudioSetup { #[async_trait] impl OnPresence for AudioSetup { async fn on_presence(&mut self, presence: bool) { - // Turn off the audio setup when we leave the house - if !presence { - debug!(id = self.identifier, "Turning devices off"); - self.speakers.set_on(false).await.unwrap(); - self.mixer.set_on(false).await.unwrap(); + let mut mixer = self.mixer.write().await; + let mut speakers = self.speakers.write().await; + + if let (Some(mixer), Some(speakers)) = ( + As::::cast_mut(mixer.as_mut()), + As::::cast_mut(speakers.as_mut()), + ) { + // Turn off the audio setup when we leave the house + if !presence { + debug!(id = self.identifier, "Turning devices off"); + speakers.set_on(false).await.unwrap(); + mixer.set_on(false).await.unwrap(); + } } } } diff --git a/src/devices/contact_sensor.rs b/src/devices/contact_sensor.rs index 0505945..d79a415 100644 --- a/src/devices/contact_sensor.rs +++ b/src/devices/contact_sensor.rs @@ -8,6 +8,7 @@ use tracing::{debug, error, trace, warn}; use crate::{ config::{CreateDevice, MqttDeviceConfig}, + device_manager::DeviceManager, devices::DEFAULT_PRESENCE, error::{CreateDeviceError, MissingWildcard}, event::EventChannel, @@ -72,15 +73,17 @@ pub struct ContactSensor { handle: Option>, } +#[async_trait] impl CreateDevice for ContactSensor { type Config = ContactSensorConfig; - fn create( + 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"); diff --git a/src/devices/ikea_outlet.rs b/src/devices/ikea_outlet.rs index cd94f6d..89f941e 100644 --- a/src/devices/ikea_outlet.rs +++ b/src/devices/ikea_outlet.rs @@ -13,6 +13,7 @@ use tokio::task::JoinHandle; use tracing::{debug, error, trace, warn}; use crate::config::{CreateDevice, InfoConfig, MqttDeviceConfig}; +use crate::device_manager::DeviceManager; use crate::devices::Device; use crate::error::CreateDeviceError; use crate::event::EventChannel; @@ -56,15 +57,17 @@ pub struct IkeaOutlet { handle: Option>, } +#[async_trait] impl CreateDevice for IkeaOutlet { type Config = IkeaOutletConfig; - fn create( + async fn create( identifier: &str, config: Self::Config, _event_channel: &EventChannel, client: &AsyncClient, _presence_topic: &str, + _device_manager: &DeviceManager, ) -> Result { trace!( id = identifier, diff --git a/src/devices/kasa_outlet.rs b/src/devices/kasa_outlet.rs index df02467..dd2c679 100644 --- a/src/devices/kasa_outlet.rs +++ b/src/devices/kasa_outlet.rs @@ -18,7 +18,10 @@ use tokio::{ }; use tracing::trace; -use crate::{config::CreateDevice, error::CreateDeviceError, event::EventChannel}; +use crate::{ + config::CreateDevice, device_manager::DeviceManager, error::CreateDeviceError, + event::EventChannel, +}; use super::Device; @@ -33,15 +36,17 @@ pub struct KasaOutlet { addr: SocketAddr, } +#[async_trait] impl CreateDevice for KasaOutlet { type Config = KasaOutletConfig; - fn create( + 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 KasaOutlet"); diff --git a/src/devices/wake_on_lan.rs b/src/devices/wake_on_lan.rs index 9af59e1..937909b 100644 --- a/src/devices/wake_on_lan.rs +++ b/src/devices/wake_on_lan.rs @@ -15,6 +15,7 @@ use tracing::{debug, error, trace}; use crate::{ config::{CreateDevice, InfoConfig, MqttDeviceConfig}, + device_manager::DeviceManager, error::CreateDeviceError, event::EventChannel, event::OnMqtt, @@ -47,15 +48,17 @@ pub struct WakeOnLAN { broadcast_ip: Ipv4Addr, } +#[async_trait] impl CreateDevice for WakeOnLAN { type Config = WakeOnLANConfig; - fn create( + async fn create( identifier: &str, config: Self::Config, _event_channel: &EventChannel, _client: &AsyncClient, _presence_topic: &str, + _device_manager: &DeviceManager, ) -> Result { trace!( id = identifier, diff --git a/src/error.rs b/src/error.rs index 3c3ab6c..b22fa9b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -91,6 +91,8 @@ 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), #[error(transparent)] diff --git a/src/main.rs b/src/main.rs index e948ea3..f0cab4b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -62,8 +62,15 @@ async fn app() -> anyhow::Result<()> { // Create all the devices specified in the config for (id, device_config) in config.devices { - let device = - device_config.create(&id, &event_channel, &client, &config.presence.mqtt.topic)?; + let device = device_config + .create( + &id, + &event_channel, + &client, + &config.presence.mqtt.topic, + &device_manager, + ) + .await?; device_manager.add(device).await; }