From f4c1ac5c9b828b4a5baa4ed00c6d0d1a2034e39f Mon Sep 17 00:00:00 2001 From: Dreaded_X Date: Thu, 13 Apr 2023 05:12:39 +0200 Subject: [PATCH] Moved most config structs to be in the same file as what they are for --- Cargo.lock | 12 -- Cargo.toml | 1 - src/auth.rs | 10 +- src/config.rs | 256 +++++----------------------------- src/debug_bridge.rs | 15 +- src/devices.rs | 10 +- src/devices/audio_setup.rs | 47 ++++--- src/devices/contact_sensor.rs | 71 ++++++++-- src/devices/ikea_outlet.rs | 56 ++++++-- src/devices/kasa_outlet.rs | 18 ++- src/devices/wake_on_lan.rs | 46 ++++-- src/error.rs | 8 +- src/hue_bridge.rs | 32 +++-- src/light_sensor.rs | 11 +- src/main.rs | 44 +++--- src/mqtt.rs | 11 ++ 16 files changed, 305 insertions(+), 343 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 962830e..24187c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,17 +29,6 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" -[[package]] -name = "async-recursion" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cda8f4bcc10624c4e85bc66b3f452cca98cfa5ca002dc83a16aad2367641bea" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.105", -] - [[package]] name = "async-stream" version = "0.3.5" @@ -84,7 +73,6 @@ name = "automation" version = "0.1.0" dependencies = [ "anyhow", - "async-recursion", "async-trait", "axum", "bytes", diff --git a/Cargo.toml b/Cargo.toml index 2c694cf..d6a7c98 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,6 @@ bytes = "1.3.0" pollster = "0.2.5" regex = "1.7.0" async-trait = "0.1.61" -async-recursion = "1.0.0" futures = "0.3.25" eui48 = { version = "1.1.0", default-features = false, features = [ "disp_hexstring", diff --git a/src/auth.rs b/src/auth.rs index 58b98bc..87bd17d 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -5,10 +5,12 @@ use axum::{ }; use serde::Deserialize; -use crate::{ - config::OpenIDConfig, - error::{ApiError, ApiErrorJson}, -}; +use crate::error::{ApiError, ApiErrorJson}; + +#[derive(Debug, Clone, Deserialize)] +pub struct OpenIDConfig { + pub base_url: String, +} #[derive(Debug, Deserialize)] pub struct User { diff --git a/src/config.rs b/src/config.rs index a2ea570..290d013 100644 --- a/src/config.rs +++ b/src/config.rs @@ -5,16 +5,21 @@ use std::{ time::Duration, }; -use async_recursion::async_recursion; -use eui48::MacAddress; use regex::{Captures, Regex}; -use rumqttc::{has_wildcards, AsyncClient, MqttOptions, Transport}; +use rumqttc::{AsyncClient, MqttOptions, Transport}; use serde::{Deserialize, Deserializer}; -use tracing::{debug, trace}; +use tracing::debug; use crate::{ - devices::{self, AudioSetup, ContactSensor, IkeaOutlet, KasaOutlet, WakeOnLAN}, - error::{ConfigParseError, DeviceCreationError, MissingEnv, MissingWildcard}, + auth::OpenIDConfig, + debug_bridge::DebugBridgeConfig, + devices::{ + self, AudioSetup, AudioSetupConfig, ContactSensor, ContactSensorConfig, IkeaOutlet, + IkeaOutletConfig, KasaOutlet, KasaOutletConfig, WakeOnLAN, WakeOnLANConfig, + }, + error::{ConfigParseError, DeviceCreateError, MissingEnv}, + hue_bridge::HueBridgeConfig, + light_sensor::LightSensorConfig, }; #[derive(Debug, Deserialize)] @@ -33,11 +38,6 @@ pub struct Config { pub devices: HashMap, } -#[derive(Debug, Clone, Deserialize)] -pub struct OpenIDConfig { - pub base_url: String, -} - #[derive(Debug, Clone, Deserialize)] pub struct MqttConfig { pub host: String, @@ -49,13 +49,6 @@ pub struct MqttConfig { pub tls: bool, } -fn deserialize_mqtt_options<'de, D>(deserializer: D) -> Result -where - D: Deserializer<'de>, -{ - Ok(MqttOptions::from(MqttConfig::deserialize(deserializer)?)) -} - impl From for MqttOptions { fn from(value: MqttConfig) -> Self { let mut mqtt_options = MqttOptions::new(value.client_name, value.host, value.port); @@ -70,6 +63,13 @@ impl From for MqttOptions { } } +fn deserialize_mqtt_options<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + Ok(MqttOptions::from(MqttConfig::deserialize(deserializer)?)) +} + #[derive(Debug, Deserialize)] pub struct FullfillmentConfig { #[serde(default = "default_fullfillment_ip")] @@ -112,32 +112,6 @@ fn default_ntfy_url() -> String { "https://ntfy.sh".into() } -#[derive(Debug, Clone, Deserialize)] -pub struct LightSensorConfig { - #[serde(flatten)] - pub mqtt: MqttDeviceConfig, - pub min: isize, - pub max: isize, -} - -#[derive(Debug, Clone, Deserialize)] -pub struct Flags { - pub presence: isize, - pub darkness: isize, -} - -#[derive(Debug, Deserialize)] -pub struct HueBridgeConfig { - pub ip: Ipv4Addr, - pub login: String, - pub flags: Flags, -} - -#[derive(Debug, Deserialize)] -pub struct DebugBridgeConfig { - pub topic: String, -} - #[derive(Debug, Clone, Deserialize)] pub struct InfoConfig { pub name: String, @@ -149,102 +123,14 @@ pub struct MqttDeviceConfig { pub topic: String, } -#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Copy)] -pub enum OutletType { - Outlet, - Kettle, - Charger, -} - -#[derive(Debug, Clone, Deserialize)] -pub struct KettleConfig { - pub timeout: Option, // Timeout in seconds -} - -#[derive(Debug, Clone, Deserialize)] -pub struct PresenceDeviceConfig { - #[serde(flatten)] - pub mqtt: Option, - // TODO: Maybe make this an option? That way if no timeout is set it will immediately turn the - // device off again? - pub timeout: u64, // Timeout in seconds -} - -impl PresenceDeviceConfig { - /// Set the mqtt topic to an appropriate value if it is not already set - fn generate_topic( - mut self, - class: &str, - identifier: &str, - config: &Config, - ) -> Result { - if self.mqtt.is_none() { - if !has_wildcards(&config.presence.topic) { - return Err(MissingWildcard::new(&config.presence.topic)); - } - - // TODO: This is not perfect, if the topic is some/+/thing/# this will fail - let offset = config - .presence - .topic - .find('+') - .or(config.presence.topic.find('#')) - .unwrap(); - let topic = format!( - "{}/{class}/{identifier}", - &config.presence.topic[..offset - 1] - ); - trace!("Setting presence mqtt topic: {topic}"); - self.mqtt = Some(MqttDeviceConfig { topic }); - } - - Ok(self) - } -} - #[derive(Debug, Clone, Deserialize)] #[serde(tag = "type")] pub enum Device { - IkeaOutlet { - #[serde(flatten)] - info: InfoConfig, - #[serde(flatten)] - mqtt: MqttDeviceConfig, - #[serde(default = "default_outlet_type")] - outlet_type: OutletType, - timeout: Option, // Timeout in seconds - }, - WakeOnLAN { - #[serde(flatten)] - info: InfoConfig, - #[serde(flatten)] - mqtt: MqttDeviceConfig, - mac_address: MacAddress, - #[serde(default = "default_broadcast_ip")] - broadcast_ip: Ipv4Addr, - }, - KasaOutlet { - ip: Ipv4Addr, - }, - AudioSetup { - #[serde(flatten)] - mqtt: MqttDeviceConfig, - mixer: Box, - speakers: Box, - }, - ContactSensor { - #[serde(flatten)] - mqtt: MqttDeviceConfig, - presence: Option, - }, -} - -fn default_outlet_type() -> OutletType { - OutletType::Outlet -} - -fn default_broadcast_ip() -> Ipv4Addr { - Ipv4Addr::new(255, 255, 255, 255) + IkeaOutlet(IkeaOutletConfig), + WakeOnLAN(WakeOnLANConfig), + KasaOutlet(KasaOutletConfig), + AudioSetup(AudioSetupConfig), + ContactSensor(ContactSensorConfig), } impl Config { @@ -275,94 +161,26 @@ impl Config { } } -// Quick helper function to box up the devices, -// passing in Box::new would be ideal, however the return type is incorrect -// Maybe there is a better way to solve this? -// fn device_box(device: T) -> DeviceBox { -// let a: DeviceBox = Box::new(device); -// a -// } - impl Device { - #[async_recursion] - pub async fn create( + pub fn create( self, identifier: &str, - config: &Config, client: AsyncClient, - ) -> Result, DeviceCreationError> { + presence_topic: &str, + ) -> Result, DeviceCreateError> { let device: Box = match self { - Device::IkeaOutlet { - info, - mqtt, - outlet_type, - timeout, - } => { - trace!( - id = identifier, - "IkeaOutlet [{} in {:?}]", - info.name, - info.room - ); - Box::new(IkeaOutlet::new( - identifier, - info, - mqtt, - outlet_type, - timeout, - client, - )) - } - Device::WakeOnLAN { - info, - mqtt, - mac_address, - broadcast_ip, - } => { - trace!( - id = identifier, - "WakeOnLan [{} in {:?}]", - info.name, - info.room - ); - Box::new(WakeOnLAN::new( - identifier, - info, - mqtt, - mac_address, - broadcast_ip, - )) - } - Device::KasaOutlet { ip } => { - trace!(id = identifier, "KasaOutlet [{}]", identifier); - Box::new(KasaOutlet::new(identifier, ip)) - } - Device::AudioSetup { - mqtt, - mixer, - speakers, - } => { - trace!(id = identifier, "AudioSetup [{}]", identifier); - // Create the child devices - let mixer_id = format!("{}.mixer", identifier); - let mixer = (*mixer).create(&mixer_id, config, client.clone()).await?; - let speakers_id = format!("{}.speakers", identifier); - let speakers = (*speakers) - .create(&speakers_id, config, client.clone()) - .await?; - - AudioSetup::build(identifier, mqtt, mixer, speakers) - .await - .map(Box::new)? - } - Device::ContactSensor { mqtt, presence } => { - trace!(id = identifier, "ContactSensor [{}]", identifier); - let presence = presence - .map(|p| p.generate_topic("contact", identifier, config)) - .transpose()?; - - Box::new(ContactSensor::new(identifier, mqtt, presence, client)) + Device::IkeaOutlet(c) => Box::new(IkeaOutlet::create(identifier, c, client)?), + Device::WakeOnLAN(c) => Box::new(WakeOnLAN::create(identifier, c)?), + Device::KasaOutlet(c) => Box::new(KasaOutlet::create(identifier, c)?), + Device::AudioSetup(c) => { + Box::new(AudioSetup::create(identifier, c, client, presence_topic)?) } + Device::ContactSensor(c) => Box::new(ContactSensor::create( + identifier, + c, + client, + presence_topic, + )?), }; Ok(device) diff --git a/src/debug_bridge.rs b/src/debug_bridge.rs index d946132..4891647 100644 --- a/src/debug_bridge.rs +++ b/src/debug_bridge.rs @@ -1,23 +1,28 @@ use async_trait::async_trait; use rumqttc::AsyncClient; +use serde::Deserialize; use tracing::warn; use crate::{ - config::DebugBridgeConfig, light_sensor::{self, OnDarkness}, mqtt::{DarknessMessage, PresenceMessage}, presence::{self, OnPresence}, }; +#[derive(Debug, Deserialize)] +pub struct DebugBridgeConfig { + pub topic: String, +} + struct DebugBridge { topic: String, client: AsyncClient, } impl DebugBridge { - pub fn new(topic: &str, client: AsyncClient) -> Self { + pub fn new(config: DebugBridgeConfig, client: AsyncClient) -> Self { Self { - topic: topic.to_owned(), + topic: config.topic, client, } } @@ -26,10 +31,10 @@ impl DebugBridge { pub fn start( mut presence_rx: presence::Receiver, mut light_sensor_rx: light_sensor::Receiver, - config: &DebugBridgeConfig, + config: DebugBridgeConfig, client: AsyncClient, ) { - let mut debug_bridge = DebugBridge::new(&config.topic, client); + let mut debug_bridge = DebugBridge::new(config, client); tokio::spawn(async move { loop { diff --git a/src/devices.rs b/src/devices.rs index 311c345..b366980 100644 --- a/src/devices.rs +++ b/src/devices.rs @@ -4,11 +4,11 @@ mod ikea_outlet; mod kasa_outlet; mod wake_on_lan; -pub use self::audio_setup::AudioSetup; -pub use self::contact_sensor::ContactSensor; -pub use self::ikea_outlet::IkeaOutlet; -pub use self::kasa_outlet::KasaOutlet; -pub use self::wake_on_lan::WakeOnLAN; +pub use self::audio_setup::{AudioSetup, AudioSetupConfig}; +pub use self::contact_sensor::{ContactSensor, ContactSensorConfig}; +pub use self::ikea_outlet::{IkeaOutlet, IkeaOutletConfig}; +pub use self::kasa_outlet::{KasaOutlet, KasaOutletConfig}; +pub use self::wake_on_lan::{WakeOnLAN, WakeOnLANConfig}; use std::collections::HashMap; diff --git a/src/devices/audio_setup.rs b/src/devices/audio_setup.rs index 3e1eccb..ea097d7 100644 --- a/src/devices/audio_setup.rs +++ b/src/devices/audio_setup.rs @@ -1,16 +1,25 @@ use async_trait::async_trait; use google_home::traits; -use tracing::{debug, error, warn}; +use rumqttc::AsyncClient; +use serde::Deserialize; +use tracing::{debug, error, trace, warn}; -use crate::config::MqttDeviceConfig; -use crate::error::DeviceError; +use crate::config::{self, MqttDeviceConfig}; +use crate::error::DeviceCreateError; use crate::mqtt::{OnMqtt, RemoteAction, RemoteMessage}; use crate::presence::OnPresence; use super::{As, Device}; -// TODO: Ideally we store am Arc to the childern devices, -// that way they hook into everything just like all other devices +#[derive(Debug, Clone, Deserialize)] +pub struct AudioSetupConfig { + #[serde(flatten)] + mqtt: MqttDeviceConfig, + mixer: Box, + speakers: Box, +} + +// TODO: We need a better way to store the children devices #[derive(Debug)] pub struct AudioSetup { identifier: String, @@ -20,22 +29,28 @@ pub struct AudioSetup { } impl AudioSetup { - pub async fn build( + pub fn create( identifier: &str, - mqtt: MqttDeviceConfig, - mixer: Box, - speakers: Box, - ) -> Result { - // We expect the children devices to implement the OnOff trait - let mixer_id = mixer.get_id().to_owned(); - let mixer = As::consume(mixer).ok_or(DeviceError::OnOffExpected(mixer_id))?; + config: AudioSetupConfig, + client: AsyncClient, + // We only need to pass this in because constructing children + presence_topic: &str, // Not a big fan of passing in the global config + ) -> Result { + trace!(id = identifier, "Setting up AudioSetup"); - let speakers_id = speakers.get_id().to_owned(); - let speakers = As::consume(speakers).ok_or(DeviceError::OnOffExpected(speakers_id))?; + // Create the child devices + let mixer_id = format!("{}.mixer", identifier); + let mixer = (*config.mixer).create(&mixer_id, client.clone(), presence_topic)?; + let mixer = As::consume(mixer).ok_or(DeviceCreateError::OnOffExpected(mixer_id))?; + + let speakers_id = format!("{}.speakers", identifier); + let speakers = (*config.speakers).create(&speakers_id, client, presence_topic)?; + let speakers = + As::consume(speakers).ok_or(DeviceCreateError::OnOffExpected(speakers_id))?; Ok(Self { identifier: identifier.to_owned(), - mqtt, + mqtt: config.mqtt, mixer, speakers, }) diff --git a/src/devices/contact_sensor.rs b/src/devices/contact_sensor.rs index 7e51ace..c693643 100644 --- a/src/devices/contact_sensor.rs +++ b/src/devices/contact_sensor.rs @@ -1,18 +1,62 @@ use std::time::Duration; use async_trait::async_trait; -use rumqttc::AsyncClient; +use rumqttc::{has_wildcards, AsyncClient}; +use serde::Deserialize; use tokio::task::JoinHandle; -use tracing::{debug, error, warn}; +use tracing::{debug, error, trace, warn}; use crate::{ - config::{MqttDeviceConfig, PresenceDeviceConfig}, + config::MqttDeviceConfig, + error::{DeviceCreateError, MissingWildcard}, mqtt::{ContactMessage, OnMqtt, PresenceMessage}, presence::OnPresence, }; use super::Device; +// NOTE: If we add more presence devices we might need to move this out of here +#[derive(Debug, Clone, Deserialize)] +pub struct PresenceDeviceConfig { + #[serde(flatten)] + pub mqtt: Option, + pub timeout: u64, // Timeout in seconds +} + +impl PresenceDeviceConfig { + /// Set the mqtt topic to an appropriate value if it is not already set + fn generate_topic( + mut self, + class: &str, + identifier: &str, + presence_topic: &str, + ) -> Result { + if self.mqtt.is_none() { + if !has_wildcards(presence_topic) { + return Err(MissingWildcard::new(presence_topic)); + } + + // TODO: This is not perfect, if the topic is some/+/thing/# this will fail + let offset = presence_topic + .find('+') + .or(presence_topic.find('#')) + .unwrap(); + let topic = format!("{}/{class}/{identifier}", &presence_topic[..offset - 1]); + trace!("Setting presence mqtt topic: {topic}"); + self.mqtt = Some(MqttDeviceConfig { topic }); + } + + Ok(self) + } +} + +#[derive(Debug, Clone, Deserialize)] +pub struct ContactSensorConfig { + #[serde(flatten)] + mqtt: MqttDeviceConfig, + presence: Option, +} + #[derive(Debug)] pub struct ContactSensor { identifier: String, @@ -26,21 +70,28 @@ pub struct ContactSensor { } impl ContactSensor { - pub fn new( + pub fn create( identifier: &str, - mqtt: MqttDeviceConfig, - presence: Option, + config: ContactSensorConfig, client: AsyncClient, - ) -> Self { - Self { + presence_topic: &str, + ) -> Result { + trace!(id = identifier, "Setting up ContactSensor"); + + let presence = config + .presence + .map(|p| p.generate_topic("contact", identifier, presence_topic)) + .transpose()?; + + Ok(Self { identifier: identifier.to_owned(), - mqtt, + mqtt: config.mqtt, presence, client, overall_presence: false, is_closed: true, handle: None, - } + }) } } diff --git a/src/devices/ikea_outlet.rs b/src/devices/ikea_outlet.rs index 2c254e2..a4f39ad 100644 --- a/src/devices/ikea_outlet.rs +++ b/src/devices/ikea_outlet.rs @@ -8,15 +8,39 @@ use google_home::{ }; use pollster::FutureExt as _; use rumqttc::{AsyncClient, Publish}; +use serde::Deserialize; use std::time::Duration; use tokio::task::JoinHandle; -use tracing::{debug, error, warn}; +use tracing::{debug, error, trace, warn}; -use crate::config::{InfoConfig, MqttDeviceConfig, OutletType}; +use crate::config::{InfoConfig, MqttDeviceConfig}; use crate::devices::Device; +use crate::error::DeviceCreateError; use crate::mqtt::{OnMqtt, OnOffMessage}; use crate::presence::OnPresence; +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Copy)] +pub enum OutletType { + Outlet, + Kettle, + Charger, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct IkeaOutletConfig { + #[serde(flatten)] + info: InfoConfig, + #[serde(flatten)] + mqtt: MqttDeviceConfig, + #[serde(default = "default_outlet_type")] + outlet_type: OutletType, + timeout: Option, // Timeout in seconds +} + +fn default_outlet_type() -> OutletType { + OutletType::Outlet +} + #[derive(Debug)] pub struct IkeaOutlet { identifier: String, @@ -31,24 +55,28 @@ pub struct IkeaOutlet { } impl IkeaOutlet { - pub fn new( + pub fn create( identifier: &str, - info: InfoConfig, - mqtt: MqttDeviceConfig, - outlet_type: OutletType, - timeout: Option, + config: IkeaOutletConfig, client: AsyncClient, - ) -> Self { - Self { + ) -> Result { + trace!( + id = identifier, + name = config.info.name, + room = config.info.room, + "Setting up IkeaOutlet" + ); + + Ok(Self { identifier: identifier.to_owned(), - info, - mqtt, - outlet_type, - timeout, + info: config.info, + mqtt: config.mqtt, + outlet_type: config.outlet_type, + timeout: config.timeout, client, last_known_state: false, handle: None, - } + }) } } diff --git a/src/devices/kasa_outlet.rs b/src/devices/kasa_outlet.rs index 5a4f8f9..ee8dd26 100644 --- a/src/devices/kasa_outlet.rs +++ b/src/devices/kasa_outlet.rs @@ -11,9 +11,17 @@ use google_home::{ }; use serde::{Deserialize, Serialize}; use thiserror::Error; +use tracing::trace; + +use crate::error::DeviceCreateError; use super::Device; +#[derive(Debug, Clone, Deserialize)] +pub struct KasaOutletConfig { + ip: Ipv4Addr, +} + #[derive(Debug)] pub struct KasaOutlet { identifier: String, @@ -21,11 +29,13 @@ pub struct KasaOutlet { } impl KasaOutlet { - pub fn new(identifier: &str, ip: Ipv4Addr) -> Self { - Self { + pub fn create(identifier: &str, config: KasaOutletConfig) -> Result { + trace!(id = identifier, "Setting up KasaOutlet"); + + Ok(Self { identifier: identifier.to_owned(), - addr: (ip, 9999).into(), - } + addr: (config.ip, 9999).into(), + }) } } diff --git a/src/devices/wake_on_lan.rs b/src/devices/wake_on_lan.rs index a670638..5987aca 100644 --- a/src/devices/wake_on_lan.rs +++ b/src/devices/wake_on_lan.rs @@ -10,15 +10,32 @@ use google_home::{ GoogleHomeDevice, }; use rumqttc::Publish; -use tracing::{debug, error}; +use serde::Deserialize; +use tracing::{debug, error, trace}; use crate::{ config::{InfoConfig, MqttDeviceConfig}, + error::DeviceCreateError, mqtt::{ActivateMessage, OnMqtt}, }; use super::Device; +#[derive(Debug, Clone, Deserialize)] +pub struct WakeOnLANConfig { + #[serde(flatten)] + info: InfoConfig, + #[serde(flatten)] + mqtt: MqttDeviceConfig, + mac_address: MacAddress, + #[serde(default = "default_broadcast_ip")] + broadcast_ip: Ipv4Addr, +} + +fn default_broadcast_ip() -> Ipv4Addr { + Ipv4Addr::new(255, 255, 255, 255) +} + #[derive(Debug)] pub struct WakeOnLAN { identifier: String, @@ -29,20 +46,21 @@ pub struct WakeOnLAN { } impl WakeOnLAN { - pub fn new( - identifier: &str, - info: InfoConfig, - mqtt: MqttDeviceConfig, - mac_address: MacAddress, - broadcast_ip: Ipv4Addr, - ) -> Self { - Self { + pub fn create(identifier: &str, config: WakeOnLANConfig) -> Result { + trace!( + id = identifier, + name = config.info.name, + room = config.info.room, + "Setting up WakeOnLAN" + ); + + Ok(Self { identifier: identifier.to_owned(), - info, - mqtt, - mac_address, - broadcast_ip, - } + info: config.info, + mqtt: config.mqtt, + mac_address: config.mac_address, + broadcast_ip: config.broadcast_ip, + }) } } diff --git a/src/error.rs b/src/error.rs index 13c81f9..8f85ee1 100644 --- a/src/error.rs +++ b/src/error.rs @@ -83,15 +83,9 @@ impl MissingWildcard { } #[derive(Debug, Error)] -pub enum DeviceError { +pub enum DeviceCreateError { #[error("Expected device '{0}' to implement OnOff trait")] OnOffExpected(String), -} - -#[derive(Debug, Error)] -pub enum DeviceCreationError { - #[error(transparent)] - DeviceError(#[from] DeviceError), #[error(transparent)] MissingWildcard(#[from] MissingWildcard), } diff --git a/src/hue_bridge.rs b/src/hue_bridge.rs index c5c7af7..37e836e 100644 --- a/src/hue_bridge.rs +++ b/src/hue_bridge.rs @@ -1,11 +1,10 @@ -use std::net::SocketAddr; +use std::net::{Ipv4Addr, SocketAddr}; use async_trait::async_trait; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use tracing::{error, trace, warn}; use crate::{ - config::{Flags, HueBridgeConfig}, light_sensor::{self, OnDarkness}, presence::{self, OnPresence}, }; @@ -15,10 +14,22 @@ pub enum Flag { Darkness, } +#[derive(Debug, Clone, Deserialize)] +pub struct FlagIDs { + pub presence: isize, + pub darkness: isize, +} + +#[derive(Debug, Deserialize)] +pub struct HueBridgeConfig { + pub ip: Ipv4Addr, + pub login: String, + pub flags: FlagIDs, +} struct HueBridge { addr: SocketAddr, login: String, - flags: Flags, + flags: FlagIDs, } #[derive(Debug, Serialize)] @@ -27,11 +38,11 @@ struct FlagMessage { } impl HueBridge { - pub fn new(addr: SocketAddr, login: &str, flags: Flags) -> Self { + pub fn new(config: HueBridgeConfig) -> Self { Self { - addr, - login: login.to_owned(), - flags, + addr: (config.ip, 80).into(), + login: config.login, + flags: config.flags, } } @@ -68,10 +79,9 @@ impl HueBridge { pub fn start( mut presence_rx: presence::Receiver, mut light_sensor_rx: light_sensor::Receiver, - config: &HueBridgeConfig, + config: HueBridgeConfig, ) { - let mut hue_bridge = - HueBridge::new((config.ip, 80).into(), &config.login, config.flags.clone()); + let mut hue_bridge = HueBridge::new(config); tokio::spawn(async move { loop { diff --git a/src/light_sensor.rs b/src/light_sensor.rs index ddfb415..d581199 100644 --- a/src/light_sensor.rs +++ b/src/light_sensor.rs @@ -1,10 +1,11 @@ use async_trait::async_trait; use rumqttc::{matches, AsyncClient}; +use serde::Deserialize; use tokio::sync::watch; use tracing::{debug, error, trace}; use crate::{ - config::{LightSensorConfig, MqttDeviceConfig}, + config::MqttDeviceConfig, error::LightSensorError, mqtt::{self, BrightnessMessage, OnMqtt}, }; @@ -17,6 +18,14 @@ pub trait OnDarkness: Sync + Send + 'static { pub type Receiver = watch::Receiver; type Sender = watch::Sender; +#[derive(Debug, Clone, Deserialize)] +pub struct LightSensorConfig { + #[serde(flatten)] + pub mqtt: MqttDeviceConfig, + pub min: isize, + pub max: isize, +} + #[derive(Debug)] struct LightSensor { mqtt: MqttDeviceConfig, diff --git a/src/main.rs b/src/main.rs index b23ca9e..0203db6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,8 +6,8 @@ use axum::{ }; use automation::{ - auth::User, - config::{Config, OpenIDConfig}, + auth::{OpenIDConfig, User}, + config::Config, debug_bridge, devices, error::ApiError, hue_bridge, light_sensor, @@ -26,7 +26,7 @@ struct AppState { pub openid: OpenIDConfig, } -impl FromRef for automation::config::OpenIDConfig { +impl FromRef for OpenIDConfig { fn from_ref(input: &AppState) -> Self { input.openid.clone() } @@ -74,12 +74,12 @@ async fn app() -> anyhow::Result<()> { } // Start the hue bridge if it is configured - if let Some(config) = &config.hue_bridge { + if let Some(config) = config.hue_bridge { hue_bridge::start(presence.clone(), light_sensor.clone(), config); } // Start the debug bridge if it is configured - if let Some(config) = &config.debug_bridge { + if let Some(config) = config.debug_bridge { debug_bridge::start( presence.clone(), light_sensor.clone(), @@ -88,27 +88,31 @@ async fn app() -> anyhow::Result<()> { ); } - let devices = devices::start( + // Setup the device handler + let device_handler = devices::start( mqtt.subscribe(), presence.clone(), light_sensor.clone(), client.clone(), ); + + // Create all the devices specified in the config + let devices = config + .devices + .into_iter() + .map(|(identifier, device_config)| { + device_config.create(&identifier, client.clone(), &config.presence.topic) + }) + .collect::, _>>()?; + + // Can even add some more devices here + // devices.push(device) + + // Register all the devices to the device_handler join_all( - config - .devices - .clone() + devices .into_iter() - .map(|(identifier, device_config)| async { - // Force the async block to move identifier - let identifier = identifier; - let device = device_config - .create(&identifier, &config, client.clone()) - .await?; - devices.add_device(device).await?; - // We don't need a seperate error type in main - anyhow::Ok(()) - }), + .map(|device| async { device_handler.add_device(device).await }), ) .await .into_iter() @@ -124,7 +128,7 @@ async fn app() -> anyhow::Result<()> { post(async move |user: User, Json(payload): Json| { debug!(username = user.preferred_username, "{payload:#?}"); let gc = GoogleHome::new(&user.preferred_username); - let result = match devices.fullfillment(gc, payload).await { + let result = match device_handler.fullfillment(gc, payload).await { Ok(result) => result, Err(err) => { return ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, err.into()) diff --git a/src/mqtt.rs b/src/mqtt.rs index d9f50b0..0db1d86 100644 --- a/src/mqtt.rs +++ b/src/mqtt.rs @@ -19,6 +19,17 @@ pub trait OnMqtt { pub type Receiver = broadcast::Receiver; type Sender = broadcast::Sender; +#[derive(Debug, Clone, Deserialize)] +pub struct MqttConfig { + pub host: String, + pub port: u16, + pub client_name: String, + pub username: String, + pub password: String, + #[serde(default)] + pub tls: bool, +} + pub struct Mqtt { tx: Sender, eventloop: EventLoop,