Moved most config structs to be in the same file as what they are for

This commit is contained in:
Dreaded_X 2023-04-13 05:12:39 +02:00
parent 2aa13e7706
commit f4c1ac5c9b
Signed by: Dreaded_X
GPG Key ID: FA5F485356B0D2D4
16 changed files with 305 additions and 343 deletions

12
Cargo.lock generated
View File

@ -29,17 +29,6 @@ version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" 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]] [[package]]
name = "async-stream" name = "async-stream"
version = "0.3.5" version = "0.3.5"
@ -84,7 +73,6 @@ name = "automation"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-recursion",
"async-trait", "async-trait",
"axum", "axum",
"bytes", "bytes",

View File

@ -27,7 +27,6 @@ bytes = "1.3.0"
pollster = "0.2.5" pollster = "0.2.5"
regex = "1.7.0" regex = "1.7.0"
async-trait = "0.1.61" async-trait = "0.1.61"
async-recursion = "1.0.0"
futures = "0.3.25" futures = "0.3.25"
eui48 = { version = "1.1.0", default-features = false, features = [ eui48 = { version = "1.1.0", default-features = false, features = [
"disp_hexstring", "disp_hexstring",

View File

@ -5,10 +5,12 @@ use axum::{
}; };
use serde::Deserialize; use serde::Deserialize;
use crate::{ use crate::error::{ApiError, ApiErrorJson};
config::OpenIDConfig,
error::{ApiError, ApiErrorJson}, #[derive(Debug, Clone, Deserialize)]
}; pub struct OpenIDConfig {
pub base_url: String,
}
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct User { pub struct User {

View File

@ -5,16 +5,21 @@ use std::{
time::Duration, time::Duration,
}; };
use async_recursion::async_recursion;
use eui48::MacAddress;
use regex::{Captures, Regex}; use regex::{Captures, Regex};
use rumqttc::{has_wildcards, AsyncClient, MqttOptions, Transport}; use rumqttc::{AsyncClient, MqttOptions, Transport};
use serde::{Deserialize, Deserializer}; use serde::{Deserialize, Deserializer};
use tracing::{debug, trace}; use tracing::debug;
use crate::{ use crate::{
devices::{self, AudioSetup, ContactSensor, IkeaOutlet, KasaOutlet, WakeOnLAN}, auth::OpenIDConfig,
error::{ConfigParseError, DeviceCreationError, MissingEnv, MissingWildcard}, 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)] #[derive(Debug, Deserialize)]
@ -33,11 +38,6 @@ pub struct Config {
pub devices: HashMap<String, Device>, pub devices: HashMap<String, Device>,
} }
#[derive(Debug, Clone, Deserialize)]
pub struct OpenIDConfig {
pub base_url: String,
}
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub struct MqttConfig { pub struct MqttConfig {
pub host: String, pub host: String,
@ -49,13 +49,6 @@ pub struct MqttConfig {
pub tls: bool, pub tls: bool,
} }
fn deserialize_mqtt_options<'de, D>(deserializer: D) -> Result<MqttOptions, D::Error>
where
D: Deserializer<'de>,
{
Ok(MqttOptions::from(MqttConfig::deserialize(deserializer)?))
}
impl From<MqttConfig> for MqttOptions { impl From<MqttConfig> for MqttOptions {
fn from(value: MqttConfig) -> Self { fn from(value: MqttConfig) -> Self {
let mut mqtt_options = MqttOptions::new(value.client_name, value.host, value.port); let mut mqtt_options = MqttOptions::new(value.client_name, value.host, value.port);
@ -70,6 +63,13 @@ impl From<MqttConfig> for MqttOptions {
} }
} }
fn deserialize_mqtt_options<'de, D>(deserializer: D) -> Result<MqttOptions, D::Error>
where
D: Deserializer<'de>,
{
Ok(MqttOptions::from(MqttConfig::deserialize(deserializer)?))
}
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct FullfillmentConfig { pub struct FullfillmentConfig {
#[serde(default = "default_fullfillment_ip")] #[serde(default = "default_fullfillment_ip")]
@ -112,32 +112,6 @@ fn default_ntfy_url() -> String {
"https://ntfy.sh".into() "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)] #[derive(Debug, Clone, Deserialize)]
pub struct InfoConfig { pub struct InfoConfig {
pub name: String, pub name: String,
@ -149,102 +123,14 @@ pub struct MqttDeviceConfig {
pub topic: String, 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<u64>, // Timeout in seconds
}
#[derive(Debug, Clone, Deserialize)]
pub struct PresenceDeviceConfig {
#[serde(flatten)]
pub mqtt: Option<MqttDeviceConfig>,
// 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<PresenceDeviceConfig, MissingWildcard> {
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)] #[derive(Debug, Clone, Deserialize)]
#[serde(tag = "type")] #[serde(tag = "type")]
pub enum Device { pub enum Device {
IkeaOutlet { IkeaOutlet(IkeaOutletConfig),
#[serde(flatten)] WakeOnLAN(WakeOnLANConfig),
info: InfoConfig, KasaOutlet(KasaOutletConfig),
#[serde(flatten)] AudioSetup(AudioSetupConfig),
mqtt: MqttDeviceConfig, ContactSensor(ContactSensorConfig),
#[serde(default = "default_outlet_type")]
outlet_type: OutletType,
timeout: Option<u64>, // 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<Device>,
speakers: Box<Device>,
},
ContactSensor {
#[serde(flatten)]
mqtt: MqttDeviceConfig,
presence: Option<PresenceDeviceConfig>,
},
}
fn default_outlet_type() -> OutletType {
OutletType::Outlet
}
fn default_broadcast_ip() -> Ipv4Addr {
Ipv4Addr::new(255, 255, 255, 255)
} }
impl Config { 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<T: devices::Device>(device: T) -> DeviceBox {
// let a: DeviceBox = Box::new(device);
// a
// }
impl Device { impl Device {
#[async_recursion] pub fn create(
pub async fn create(
self, self,
identifier: &str, identifier: &str,
config: &Config,
client: AsyncClient, client: AsyncClient,
) -> Result<Box<dyn devices::Device>, DeviceCreationError> { presence_topic: &str,
) -> Result<Box<dyn devices::Device>, DeviceCreateError> {
let device: Box<dyn devices::Device> = match self { let device: Box<dyn devices::Device> = match self {
Device::IkeaOutlet { Device::IkeaOutlet(c) => Box::new(IkeaOutlet::create(identifier, c, client)?),
info, Device::WakeOnLAN(c) => Box::new(WakeOnLAN::create(identifier, c)?),
mqtt, Device::KasaOutlet(c) => Box::new(KasaOutlet::create(identifier, c)?),
outlet_type, Device::AudioSetup(c) => {
timeout, Box::new(AudioSetup::create(identifier, c, client, presence_topic)?)
} => { }
trace!( Device::ContactSensor(c) => Box::new(ContactSensor::create(
id = identifier,
"IkeaOutlet [{} in {:?}]",
info.name,
info.room
);
Box::new(IkeaOutlet::new(
identifier, identifier,
info, c,
mqtt,
outlet_type,
timeout,
client, client,
)) presence_topic,
} )?),
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))
}
}; };
Ok(device) Ok(device)

View File

@ -1,23 +1,28 @@
use async_trait::async_trait; use async_trait::async_trait;
use rumqttc::AsyncClient; use rumqttc::AsyncClient;
use serde::Deserialize;
use tracing::warn; use tracing::warn;
use crate::{ use crate::{
config::DebugBridgeConfig,
light_sensor::{self, OnDarkness}, light_sensor::{self, OnDarkness},
mqtt::{DarknessMessage, PresenceMessage}, mqtt::{DarknessMessage, PresenceMessage},
presence::{self, OnPresence}, presence::{self, OnPresence},
}; };
#[derive(Debug, Deserialize)]
pub struct DebugBridgeConfig {
pub topic: String,
}
struct DebugBridge { struct DebugBridge {
topic: String, topic: String,
client: AsyncClient, client: AsyncClient,
} }
impl DebugBridge { impl DebugBridge {
pub fn new(topic: &str, client: AsyncClient) -> Self { pub fn new(config: DebugBridgeConfig, client: AsyncClient) -> Self {
Self { Self {
topic: topic.to_owned(), topic: config.topic,
client, client,
} }
} }
@ -26,10 +31,10 @@ impl DebugBridge {
pub fn start( pub fn start(
mut presence_rx: presence::Receiver, mut presence_rx: presence::Receiver,
mut light_sensor_rx: light_sensor::Receiver, mut light_sensor_rx: light_sensor::Receiver,
config: &DebugBridgeConfig, config: DebugBridgeConfig,
client: AsyncClient, client: AsyncClient,
) { ) {
let mut debug_bridge = DebugBridge::new(&config.topic, client); let mut debug_bridge = DebugBridge::new(config, client);
tokio::spawn(async move { tokio::spawn(async move {
loop { loop {

View File

@ -4,11 +4,11 @@ mod ikea_outlet;
mod kasa_outlet; mod kasa_outlet;
mod wake_on_lan; mod wake_on_lan;
pub use self::audio_setup::AudioSetup; pub use self::audio_setup::{AudioSetup, AudioSetupConfig};
pub use self::contact_sensor::ContactSensor; pub use self::contact_sensor::{ContactSensor, ContactSensorConfig};
pub use self::ikea_outlet::IkeaOutlet; pub use self::ikea_outlet::{IkeaOutlet, IkeaOutletConfig};
pub use self::kasa_outlet::KasaOutlet; pub use self::kasa_outlet::{KasaOutlet, KasaOutletConfig};
pub use self::wake_on_lan::WakeOnLAN; pub use self::wake_on_lan::{WakeOnLAN, WakeOnLANConfig};
use std::collections::HashMap; use std::collections::HashMap;

View File

@ -1,16 +1,25 @@
use async_trait::async_trait; use async_trait::async_trait;
use google_home::traits; 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::config::{self, MqttDeviceConfig};
use crate::error::DeviceError; use crate::error::DeviceCreateError;
use crate::mqtt::{OnMqtt, RemoteAction, RemoteMessage}; use crate::mqtt::{OnMqtt, RemoteAction, RemoteMessage};
use crate::presence::OnPresence; use crate::presence::OnPresence;
use super::{As, Device}; use super::{As, Device};
// TODO: Ideally we store am Arc to the childern devices, #[derive(Debug, Clone, Deserialize)]
// that way they hook into everything just like all other devices pub struct AudioSetupConfig {
#[serde(flatten)]
mqtt: MqttDeviceConfig,
mixer: Box<config::Device>,
speakers: Box<config::Device>,
}
// TODO: We need a better way to store the children devices
#[derive(Debug)] #[derive(Debug)]
pub struct AudioSetup { pub struct AudioSetup {
identifier: String, identifier: String,
@ -20,22 +29,28 @@ pub struct AudioSetup {
} }
impl AudioSetup { impl AudioSetup {
pub async fn build( pub fn create(
identifier: &str, identifier: &str,
mqtt: MqttDeviceConfig, config: AudioSetupConfig,
mixer: Box<dyn Device>, client: AsyncClient,
speakers: Box<dyn Device>, // We only need to pass this in because constructing children
) -> Result<Self, DeviceError> { presence_topic: &str, // Not a big fan of passing in the global config
// We expect the children devices to implement the OnOff trait ) -> Result<Self, DeviceCreateError> {
let mixer_id = mixer.get_id().to_owned(); trace!(id = identifier, "Setting up AudioSetup");
let mixer = As::consume(mixer).ok_or(DeviceError::OnOffExpected(mixer_id))?;
let speakers_id = speakers.get_id().to_owned(); // Create the child devices
let speakers = As::consume(speakers).ok_or(DeviceError::OnOffExpected(speakers_id))?; 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 { Ok(Self {
identifier: identifier.to_owned(), identifier: identifier.to_owned(),
mqtt, mqtt: config.mqtt,
mixer, mixer,
speakers, speakers,
}) })

View File

@ -1,18 +1,62 @@
use std::time::Duration; use std::time::Duration;
use async_trait::async_trait; use async_trait::async_trait;
use rumqttc::AsyncClient; use rumqttc::{has_wildcards, AsyncClient};
use serde::Deserialize;
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
use tracing::{debug, error, warn}; use tracing::{debug, error, trace, warn};
use crate::{ use crate::{
config::{MqttDeviceConfig, PresenceDeviceConfig}, config::MqttDeviceConfig,
error::{DeviceCreateError, MissingWildcard},
mqtt::{ContactMessage, OnMqtt, PresenceMessage}, mqtt::{ContactMessage, OnMqtt, PresenceMessage},
presence::OnPresence, presence::OnPresence,
}; };
use super::Device; 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<MqttDeviceConfig>,
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<PresenceDeviceConfig, MissingWildcard> {
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<PresenceDeviceConfig>,
}
#[derive(Debug)] #[derive(Debug)]
pub struct ContactSensor { pub struct ContactSensor {
identifier: String, identifier: String,
@ -26,21 +70,28 @@ pub struct ContactSensor {
} }
impl ContactSensor { impl ContactSensor {
pub fn new( pub fn create(
identifier: &str, identifier: &str,
mqtt: MqttDeviceConfig, config: ContactSensorConfig,
presence: Option<PresenceDeviceConfig>,
client: AsyncClient, client: AsyncClient,
) -> Self { presence_topic: &str,
Self { ) -> Result<Self, DeviceCreateError> {
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(), identifier: identifier.to_owned(),
mqtt, mqtt: config.mqtt,
presence, presence,
client, client,
overall_presence: false, overall_presence: false,
is_closed: true, is_closed: true,
handle: None, handle: None,
} })
} }
} }

View File

@ -8,15 +8,39 @@ use google_home::{
}; };
use pollster::FutureExt as _; use pollster::FutureExt as _;
use rumqttc::{AsyncClient, Publish}; use rumqttc::{AsyncClient, Publish};
use serde::Deserialize;
use std::time::Duration; use std::time::Duration;
use tokio::task::JoinHandle; 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::devices::Device;
use crate::error::DeviceCreateError;
use crate::mqtt::{OnMqtt, OnOffMessage}; use crate::mqtt::{OnMqtt, OnOffMessage};
use crate::presence::OnPresence; 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<u64>, // Timeout in seconds
}
fn default_outlet_type() -> OutletType {
OutletType::Outlet
}
#[derive(Debug)] #[derive(Debug)]
pub struct IkeaOutlet { pub struct IkeaOutlet {
identifier: String, identifier: String,
@ -31,24 +55,28 @@ pub struct IkeaOutlet {
} }
impl IkeaOutlet { impl IkeaOutlet {
pub fn new( pub fn create(
identifier: &str, identifier: &str,
info: InfoConfig, config: IkeaOutletConfig,
mqtt: MqttDeviceConfig,
outlet_type: OutletType,
timeout: Option<u64>,
client: AsyncClient, client: AsyncClient,
) -> Self { ) -> Result<Self, DeviceCreateError> {
Self { trace!(
id = identifier,
name = config.info.name,
room = config.info.room,
"Setting up IkeaOutlet"
);
Ok(Self {
identifier: identifier.to_owned(), identifier: identifier.to_owned(),
info, info: config.info,
mqtt, mqtt: config.mqtt,
outlet_type, outlet_type: config.outlet_type,
timeout, timeout: config.timeout,
client, client,
last_known_state: false, last_known_state: false,
handle: None, handle: None,
} })
} }
} }

View File

@ -11,9 +11,17 @@ use google_home::{
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use thiserror::Error; use thiserror::Error;
use tracing::trace;
use crate::error::DeviceCreateError;
use super::Device; use super::Device;
#[derive(Debug, Clone, Deserialize)]
pub struct KasaOutletConfig {
ip: Ipv4Addr,
}
#[derive(Debug)] #[derive(Debug)]
pub struct KasaOutlet { pub struct KasaOutlet {
identifier: String, identifier: String,
@ -21,11 +29,13 @@ pub struct KasaOutlet {
} }
impl KasaOutlet { impl KasaOutlet {
pub fn new(identifier: &str, ip: Ipv4Addr) -> Self { pub fn create(identifier: &str, config: KasaOutletConfig) -> Result<Self, DeviceCreateError> {
Self { trace!(id = identifier, "Setting up KasaOutlet");
Ok(Self {
identifier: identifier.to_owned(), identifier: identifier.to_owned(),
addr: (ip, 9999).into(), addr: (config.ip, 9999).into(),
} })
} }
} }

View File

@ -10,15 +10,32 @@ use google_home::{
GoogleHomeDevice, GoogleHomeDevice,
}; };
use rumqttc::Publish; use rumqttc::Publish;
use tracing::{debug, error}; use serde::Deserialize;
use tracing::{debug, error, trace};
use crate::{ use crate::{
config::{InfoConfig, MqttDeviceConfig}, config::{InfoConfig, MqttDeviceConfig},
error::DeviceCreateError,
mqtt::{ActivateMessage, OnMqtt}, mqtt::{ActivateMessage, OnMqtt},
}; };
use super::Device; 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)] #[derive(Debug)]
pub struct WakeOnLAN { pub struct WakeOnLAN {
identifier: String, identifier: String,
@ -29,20 +46,21 @@ pub struct WakeOnLAN {
} }
impl WakeOnLAN { impl WakeOnLAN {
pub fn new( pub fn create(identifier: &str, config: WakeOnLANConfig) -> Result<Self, DeviceCreateError> {
identifier: &str, trace!(
info: InfoConfig, id = identifier,
mqtt: MqttDeviceConfig, name = config.info.name,
mac_address: MacAddress, room = config.info.room,
broadcast_ip: Ipv4Addr, "Setting up WakeOnLAN"
) -> Self { );
Self {
Ok(Self {
identifier: identifier.to_owned(), identifier: identifier.to_owned(),
info, info: config.info,
mqtt, mqtt: config.mqtt,
mac_address, mac_address: config.mac_address,
broadcast_ip, broadcast_ip: config.broadcast_ip,
} })
} }
} }

View File

@ -83,15 +83,9 @@ impl MissingWildcard {
} }
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum DeviceError { pub enum DeviceCreateError {
#[error("Expected device '{0}' to implement OnOff trait")] #[error("Expected device '{0}' to implement OnOff trait")]
OnOffExpected(String), OnOffExpected(String),
}
#[derive(Debug, Error)]
pub enum DeviceCreationError {
#[error(transparent)]
DeviceError(#[from] DeviceError),
#[error(transparent)] #[error(transparent)]
MissingWildcard(#[from] MissingWildcard), MissingWildcard(#[from] MissingWildcard),
} }

View File

@ -1,11 +1,10 @@
use std::net::SocketAddr; use std::net::{Ipv4Addr, SocketAddr};
use async_trait::async_trait; use async_trait::async_trait;
use serde::Serialize; use serde::{Deserialize, Serialize};
use tracing::{error, trace, warn}; use tracing::{error, trace, warn};
use crate::{ use crate::{
config::{Flags, HueBridgeConfig},
light_sensor::{self, OnDarkness}, light_sensor::{self, OnDarkness},
presence::{self, OnPresence}, presence::{self, OnPresence},
}; };
@ -15,10 +14,22 @@ pub enum Flag {
Darkness, 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 { struct HueBridge {
addr: SocketAddr, addr: SocketAddr,
login: String, login: String,
flags: Flags, flags: FlagIDs,
} }
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
@ -27,11 +38,11 @@ struct FlagMessage {
} }
impl HueBridge { impl HueBridge {
pub fn new(addr: SocketAddr, login: &str, flags: Flags) -> Self { pub fn new(config: HueBridgeConfig) -> Self {
Self { Self {
addr, addr: (config.ip, 80).into(),
login: login.to_owned(), login: config.login,
flags, flags: config.flags,
} }
} }
@ -68,10 +79,9 @@ impl HueBridge {
pub fn start( pub fn start(
mut presence_rx: presence::Receiver, mut presence_rx: presence::Receiver,
mut light_sensor_rx: light_sensor::Receiver, mut light_sensor_rx: light_sensor::Receiver,
config: &HueBridgeConfig, config: HueBridgeConfig,
) { ) {
let mut hue_bridge = let mut hue_bridge = HueBridge::new(config);
HueBridge::new((config.ip, 80).into(), &config.login, config.flags.clone());
tokio::spawn(async move { tokio::spawn(async move {
loop { loop {

View File

@ -1,10 +1,11 @@
use async_trait::async_trait; use async_trait::async_trait;
use rumqttc::{matches, AsyncClient}; use rumqttc::{matches, AsyncClient};
use serde::Deserialize;
use tokio::sync::watch; use tokio::sync::watch;
use tracing::{debug, error, trace}; use tracing::{debug, error, trace};
use crate::{ use crate::{
config::{LightSensorConfig, MqttDeviceConfig}, config::MqttDeviceConfig,
error::LightSensorError, error::LightSensorError,
mqtt::{self, BrightnessMessage, OnMqtt}, mqtt::{self, BrightnessMessage, OnMqtt},
}; };
@ -17,6 +18,14 @@ pub trait OnDarkness: Sync + Send + 'static {
pub type Receiver = watch::Receiver<bool>; pub type Receiver = watch::Receiver<bool>;
type Sender = watch::Sender<bool>; type Sender = watch::Sender<bool>;
#[derive(Debug, Clone, Deserialize)]
pub struct LightSensorConfig {
#[serde(flatten)]
pub mqtt: MqttDeviceConfig,
pub min: isize,
pub max: isize,
}
#[derive(Debug)] #[derive(Debug)]
struct LightSensor { struct LightSensor {
mqtt: MqttDeviceConfig, mqtt: MqttDeviceConfig,

View File

@ -6,8 +6,8 @@ use axum::{
}; };
use automation::{ use automation::{
auth::User, auth::{OpenIDConfig, User},
config::{Config, OpenIDConfig}, config::Config,
debug_bridge, devices, debug_bridge, devices,
error::ApiError, error::ApiError,
hue_bridge, light_sensor, hue_bridge, light_sensor,
@ -26,7 +26,7 @@ struct AppState {
pub openid: OpenIDConfig, pub openid: OpenIDConfig,
} }
impl FromRef<AppState> for automation::config::OpenIDConfig { impl FromRef<AppState> for OpenIDConfig {
fn from_ref(input: &AppState) -> Self { fn from_ref(input: &AppState) -> Self {
input.openid.clone() input.openid.clone()
} }
@ -74,12 +74,12 @@ async fn app() -> anyhow::Result<()> {
} }
// Start the hue bridge if it is configured // 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); hue_bridge::start(presence.clone(), light_sensor.clone(), config);
} }
// Start the debug bridge if it is configured // 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( debug_bridge::start(
presence.clone(), presence.clone(),
light_sensor.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(), mqtt.subscribe(),
presence.clone(), presence.clone(),
light_sensor.clone(), light_sensor.clone(),
client.clone(), client.clone(),
); );
join_all(
config // Create all the devices specified in the config
let devices = config
.devices .devices
.clone()
.into_iter() .into_iter()
.map(|(identifier, device_config)| async { .map(|(identifier, device_config)| {
// Force the async block to move identifier device_config.create(&identifier, client.clone(), &config.presence.topic)
let identifier = identifier; })
let device = device_config .collect::<Result<Vec<_>, _>>()?;
.create(&identifier, &config, client.clone())
.await?; // Can even add some more devices here
devices.add_device(device).await?; // devices.push(device)
// We don't need a seperate error type in main
anyhow::Ok(()) // Register all the devices to the device_handler
}), join_all(
devices
.into_iter()
.map(|device| async { device_handler.add_device(device).await }),
) )
.await .await
.into_iter() .into_iter()
@ -124,7 +128,7 @@ async fn app() -> anyhow::Result<()> {
post(async move |user: User, Json(payload): Json<Request>| { post(async move |user: User, Json(payload): Json<Request>| {
debug!(username = user.preferred_username, "{payload:#?}"); debug!(username = user.preferred_username, "{payload:#?}");
let gc = GoogleHome::new(&user.preferred_username); 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, Ok(result) => result,
Err(err) => { Err(err) => {
return ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, err.into()) return ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, err.into())

View File

@ -19,6 +19,17 @@ pub trait OnMqtt {
pub type Receiver = broadcast::Receiver<Publish>; pub type Receiver = broadcast::Receiver<Publish>;
type Sender = broadcast::Sender<Publish>; type Sender = broadcast::Sender<Publish>;
#[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 { pub struct Mqtt {
tx: Sender, tx: Sender,
eventloop: EventLoop, eventloop: EventLoop,