From f115e0e6e8105c9140a478b3d29aab5b384d2c3a Mon Sep 17 00:00:00 2001 From: Dreaded_X Date: Thu, 25 Apr 2024 01:35:23 +0200 Subject: [PATCH] Everything needed to construct a new device is passed in through lua --- Cargo.lock | 1 + automation_macro/Cargo.toml | 3 +- automation_macro/src/lib.rs | 260 ++++++++++++++++++++++++++++++++-- config.lua | 9 ++ src/device_manager.rs | 70 +++++---- src/devices/air_filter.rs | 27 ++-- src/devices/audio_setup.rs | 78 +++------- src/devices/contact_sensor.rs | 80 +++++------ src/devices/debug_bridge.rs | 27 ++-- src/devices/hue_bridge.rs | 13 +- src/devices/hue_light.rs | 16 +-- src/devices/ikea_outlet.rs | 41 +++--- src/devices/kasa_outlet.rs | 13 +- src/devices/light_sensor.rs | 22 ++- src/devices/mod.rs | 2 +- src/devices/wake_on_lan.rs | 21 ++- src/devices/washer.rs | 23 ++- src/event.rs | 5 +- src/helper.rs | 12 ++ src/lib.rs | 1 + src/main.rs | 4 +- src/mqtt.rs | 24 +++- 22 files changed, 493 insertions(+), 259 deletions(-) create mode 100644 src/helper.rs diff --git a/Cargo.lock b/Cargo.lock index 82078b8..2404fcc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -114,6 +114,7 @@ version = "0.1.0" name = "automation_macro" version = "0.1.0" dependencies = [ + "proc-macro2", "quote", "syn 2.0.60", ] diff --git a/automation_macro/Cargo.toml b/automation_macro/Cargo.toml index 4ae24b5..50f0fd7 100644 --- a/automation_macro/Cargo.toml +++ b/automation_macro/Cargo.toml @@ -7,5 +7,6 @@ edition = "2021" proc-macro = true [dependencies] +proc-macro2 = "1.0.81" quote = "1.0.36" -syn = "2.0.60" +syn = { version = "2.0.60", features = ["extra-traits"] } diff --git a/automation_macro/src/lib.rs b/automation_macro/src/lib.rs index a42e6cb..2bdf53d 100644 --- a/automation_macro/src/lib.rs +++ b/automation_macro/src/lib.rs @@ -1,17 +1,17 @@ -use proc_macro::TokenStream; +use proc_macro2::TokenStream; use quote::quote; -use syn::{parse_macro_input, DeriveInput}; +use syn::punctuated::Punctuated; +use syn::{parse_macro_input, DeriveInput, Token}; #[proc_macro_derive(LuaDevice, attributes(config))] -pub fn lua_device_derive(input: TokenStream) -> TokenStream { +pub fn lua_device_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let ast = parse_macro_input!(input as DeriveInput); - impl_lua_device_macro(&ast) + impl_lua_device_macro(&ast).into() } fn impl_lua_device_macro(ast: &syn::DeriveInput) -> TokenStream { let name = &ast.ident; - let name_string = name.to_string(); // TODO: Handle errors properly // This includes making sure one, and only one config is specified let config = if let syn::Data::Struct(syn::DataStruct { @@ -36,13 +36,13 @@ fn impl_lua_device_macro(ast: &syn::DeriveInput) -> TokenStream { let gen = quote! { impl #name { pub fn register_with_lua(lua: &mlua::Lua) -> mlua::Result<()> { - lua.globals().set(#name_string, lua.create_proxy::<#name>()?) + lua.globals().set(stringify!(#name), lua.create_proxy::<#name>()?) } } impl mlua::UserData for #name { fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) { methods.add_function("new", |lua, config: mlua::Value| { - let config: #config = mlua::LuaSerdeExt::from_value(lua, config)?; + let config: #config = mlua::FromLua::from_lua(config, lua)?; let config: Box = Box::new(config); Ok(config) }); @@ -50,5 +50,249 @@ fn impl_lua_device_macro(ast: &syn::DeriveInput) -> TokenStream { } }; - gen.into() + gen +} + +#[derive(Debug)] +enum Arg { + Flatten, + UserData, + With(TokenStream), + Default(Option), +} + +impl syn::parse::Parse for Arg { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let arg = match input.parse::()?.to_string().as_str() { + "flatten" => Arg::Flatten, + "user_data" => Arg::UserData, + "with" => { + input.parse::()?; + let lit = input.parse::()?; + if let syn::Lit::Str(lit_str) = lit { + let token_stream: TokenStream = lit_str.parse()?; + Arg::With(token_stream) + } else { + panic!("Expected literal string"); + } + } + "default" => { + if input.parse::().is_ok() { + let func = input.parse::()?; + Arg::Default(Some(func)) + } else { + Arg::Default(None) + } + } + name => todo!("Handle unknown arg: {name}"), + }; + + Ok(arg) + } +} + +#[derive(Debug)] +struct ArgsParser { + args: Punctuated, +} + +impl syn::parse::Parse for ArgsParser { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let args = input.parse_terminated(Arg::parse, Token![,])?; + + Ok(Self { args }) + } +} + +#[derive(Debug)] +struct Args { + flatten: bool, + user_data: bool, + with: Option, + default: Option>, +} + +impl Args { + fn new(args: Vec) -> Self { + let mut result = Args { + flatten: false, + user_data: false, + with: None, + default: None, + }; + for arg in args { + match arg { + Arg::Flatten => { + if result.flatten { + panic!("Option 'flatten' is already set") + } + result.flatten = true + } + Arg::UserData => { + if result.flatten { + panic!("Option 'user_data' is already set") + } + result.user_data = true + } + Arg::With(ty) => { + if result.with.is_some() { + panic!("Option 'with' is already set") + } + result.with = Some(ty) + } + Arg::Default(func) => { + if result.default.is_some() { + panic!("Option 'default' is already set") + } + result.default = Some(func) + } + } + } + + if result.flatten && result.user_data { + panic!("The options 'flatten' and 'user_data' conflict with each other") + } + + if result.flatten && result.default.is_some() { + panic!("The options 'flatten' and 'default' conflict with each other") + } + + result + } +} + +#[proc_macro_derive(LuaDeviceConfig, attributes(device_config))] +pub fn lua_device_config_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let ast = parse_macro_input!(input as DeriveInput); + + impl_lua_device_config_macro(&ast).into() +} + +// struct Args + +fn impl_lua_device_config_macro(ast: &syn::DeriveInput) -> TokenStream { + let name = &ast.ident; + // TODO: Handle errors properly + // This includes making sure one, and only one config is specified + let fields = if let syn::Data::Struct(syn::DataStruct { + fields: syn::Fields::Named(syn::FieldsNamed { ref named, .. }), + .. + }) = ast.data + { + named + } else { + unimplemented!("Macro can only handle named structs"); + }; + + let fields: Vec<_> = fields + .iter() + .map(|field| { + let field_name = field.ident.clone().unwrap(); + let args: Vec<_> = field + .attrs + .iter() + .filter_map(|attr| { + if attr.path().is_ident("device_config") { + let args: ArgsParser = attr.parse_args().unwrap(); + Some(args.args) + } else { + None + } + }) + .flatten() + .collect(); + + let args = Args::new(args); + + // TODO: Improve how optional fields are detected + let optional = if let syn::Type::Path(path) = field.ty.clone() { + path.path.segments.first().unwrap().ident == "Option" + } else { + false + }; + + let default = if optional { + quote! { None } + } else if let Some(func) = args.default { + if func.is_some() { + quote! { #func() } + } else { + quote! { Default::default() } + } + } else { + let missing = format!("Missing field '{field_name}'"); + quote! { panic!(#missing) } + }; + + let value = if args.flatten { + // println!("ValueFlatten: {}", field_name); + quote! { + mlua::LuaSerdeExt::from_value_with(lua, value.clone(), mlua::DeserializeOptions::new().deny_unsupported_types(false))? + } + } else if args.user_data { + // println!("UserData: {}", field_name); + quote! { + if table.contains_key(stringify!(#field_name))? { + table.get(stringify!(#field_name))? + } else { + #default + } + } + } else { + // println!("Value: {}", field_name); + quote! { + { + let #field_name: mlua::Value = table.get(stringify!(#field_name))?; + if !#field_name.is_nil() { + mlua::LuaSerdeExt::from_value(lua, #field_name)? + } else { + #default + } + } + } + }; + + let value = if let Some(temp_type) = args.with { + if optional { + quote! { + { + let temp: #temp_type = #value; + temp.map(|v| v.into()) + } + } + } else { + quote! { + { + let temp: #temp_type = #value; + temp.into() + } + } + } + } else { + value + }; + + quote! { + #field_name: #value + } + }) + .collect(); + + let gen = quote! { + impl<'lua> mlua::FromLua<'lua> for #name { + fn from_lua(value: mlua::Value<'lua>, lua: &'lua mlua::Lua) -> mlua::Result { + if !value.is_table() { + panic!("Expected table"); + } + let table = value.as_table().unwrap(); + + Ok(#name { + #(#fields,)* + }) + + } + } + }; + + gen } diff --git a/config.lua b/config.lua index 1890ba4..c39fd87 100644 --- a/config.lua +++ b/config.lua @@ -17,6 +17,7 @@ automation.device_manager:create( "debug_bridge", DebugBridge.new({ topic = mqtt_automation("debug"), + client = automation.mqtt_client, }) ) @@ -41,6 +42,7 @@ automation.device_manager:create( topic = mqtt_z2m("living/light"), min = 22000, max = 23500, + event_channel = automation.event_channel, }) ) @@ -74,6 +76,7 @@ automation.device_manager:create( name = "Kettle", room = "Kitchen", topic = mqtt_z2m("kitchen/kettle"), + client = automation.mqtt_client, timeout = debug and 5 or 300, remotes = { { topic = mqtt_z2m("bedroom/remote") }, @@ -89,6 +92,7 @@ automation.device_manager:create( name = "Light", room = "Bathroom", topic = mqtt_z2m("batchroom/light"), + client = automation.mqtt_client, timeout = debug and 60 or 45 * 60, }) ) @@ -98,6 +102,7 @@ automation.device_manager:create( Washer.new({ topic = mqtt_z2m("batchroom/washer"), threshold = 1, + event_channel = automation.event_channel, }) ) @@ -108,6 +113,7 @@ automation.device_manager:create( name = "Charger", room = "Workbench", topic = mqtt_z2m("workbench/charger"), + client = automation.mqtt_client, timeout = debug and 5 or 20 * 3600, }) ) @@ -118,6 +124,7 @@ automation.device_manager:create( name = "Outlet", room = "Workbench", topic = mqtt_z2m("workbench/outlet"), + client = automation.mqtt_client, }) ) @@ -139,6 +146,7 @@ automation.device_manager:create( "hallway_frontdoor", ContactSensor.new({ topic = mqtt_z2m("hallway/frontdoor"), + client = automation.mqtt_client, presence = { topic = mqtt_automation("presence/contact/frontdoor"), timeout = debug and 10 or 15 * 60, @@ -156,5 +164,6 @@ automation.device_manager:create( name = "Air Filter", room = "Bedroom", topic = "pico/filter/bedroom", + client = automation.mqtt_client, }) ) diff --git a/src/device_manager.rs b/src/device_manager.rs index 75b1caa..e132e33 100644 --- a/src/device_manager.rs +++ b/src/device_manager.rs @@ -1,10 +1,12 @@ use std::collections::HashMap; +use std::ops::{Deref, DerefMut}; use std::sync::Arc; use async_trait::async_trait; use enum_dispatch::enum_dispatch; use futures::future::join_all; use google_home::traits::OnOff; +use mlua::FromLua; use rumqttc::{matches, AsyncClient, QoS}; use tokio::sync::{RwLock, RwLockReadGuard}; use tokio_cron_scheduler::{Job, JobScheduler}; @@ -15,25 +17,38 @@ use crate::error::DeviceConfigError; use crate::event::{Event, EventChannel, OnDarkness, OnMqtt, OnNotification, OnPresence}; use crate::schedule::{Action, Schedule}; -pub struct ConfigExternal<'a> { - pub client: &'a AsyncClient, - pub device_manager: &'a DeviceManager, - pub event_channel: &'a EventChannel, -} - #[async_trait] #[enum_dispatch] pub trait DeviceConfig { - async fn create( - &self, - identifier: &str, - ext: &ConfigExternal, - ) -> Result, DeviceConfigError>; + async fn create(&self, identifier: &str) -> Result, DeviceConfigError>; } impl mlua::UserData for Box {} -pub type WrappedDevice = Arc>>; -pub type DeviceMap = HashMap; +#[derive(Debug, FromLua, Clone)] +pub struct WrappedDevice(Arc>>); + +impl WrappedDevice { + fn new(device: Box) -> Self { + Self(Arc::new(RwLock::new(device))) + } +} + +impl Deref for WrappedDevice { + type Target = Arc>>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for WrappedDevice { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} +impl mlua::UserData for WrappedDevice {} + +pub type DeviceMap = HashMap>>>; #[derive(Debug, Clone)] pub struct DeviceManager { @@ -117,7 +132,7 @@ impl DeviceManager { sched.start().await.unwrap(); } - pub async fn add(&self, device: Box) { + pub async fn add(&self, device: Box) -> WrappedDevice { let id = device.get_id().into(); debug!(id, "Adding device"); @@ -135,9 +150,11 @@ impl DeviceManager { } // Wrap the device - let device = Arc::new(RwLock::new(device)); + let device = WrappedDevice::new(device); - self.devices.write().await.insert(id, device); + self.devices.write().await.insert(id, device.0.clone()); + + device } pub fn event_channel(&self) -> EventChannel { @@ -145,7 +162,12 @@ impl DeviceManager { } pub async fn get(&self, name: &str) -> Option { - self.devices.read().await.get(name).cloned() + self.devices + .read() + .await + .get(name) + .cloned() + .map(WrappedDevice) } pub async fn devices(&self) -> RwLockReadGuard { @@ -232,22 +254,12 @@ impl mlua::UserData for DeviceManager { // TODO: Handle the error here properly let config: Box = config.as_userdata().unwrap().take()?; - let ext = ConfigExternal { - client: &this.client, - device_manager: this, - event_channel: &this.event_channel, - }; - let device = config - .create(&identifier, &ext) + .create(&identifier) .await .map_err(mlua::ExternalError::into_lua_err)?; - let id = device.get_id().to_owned(); - - this.add(device).await; - - Ok(id) + Ok(this.add(device).await) }, ) } diff --git a/src/devices/air_filter.rs b/src/devices/air_filter.rs index efc7675..b2b7576 100644 --- a/src/devices/air_filter.rs +++ b/src/devices/air_filter.rs @@ -1,40 +1,37 @@ use async_trait::async_trait; -use automation_macro::LuaDevice; +use automation_macro::{LuaDevice, LuaDeviceConfig}; use google_home::device::Name; use google_home::errors::ErrorCode; use google_home::traits::{AvailableSpeeds, FanSpeed, HumiditySetting, OnOff, Speed, SpeedValues}; use google_home::types::Type; use google_home::GoogleHomeDevice; -use rumqttc::{AsyncClient, Publish}; -use serde::Deserialize; +use rumqttc::Publish; use tracing::{debug, error, warn}; use crate::config::{InfoConfig, MqttDeviceConfig}; -use crate::device_manager::{ConfigExternal, DeviceConfig}; +use crate::device_manager::DeviceConfig; use crate::devices::Device; use crate::error::DeviceConfigError; use crate::event::OnMqtt; use crate::messages::{AirFilterFanState, AirFilterState, SetAirFilterFanState}; +use crate::mqtt::WrappedAsyncClient; -#[derive(Debug, Deserialize, Clone)] +#[derive(Debug, Clone, LuaDeviceConfig)] pub struct AirFilterConfig { - #[serde(flatten)] + #[device_config(flatten)] info: InfoConfig, - #[serde(flatten)] + #[device_config(flatten)] mqtt: MqttDeviceConfig, + #[device_config(user_data)] + client: WrappedAsyncClient, } #[async_trait] impl DeviceConfig for AirFilterConfig { - async fn create( - &self, - identifier: &str, - ext: &ConfigExternal, - ) -> Result, DeviceConfigError> { + async fn create(&self, identifier: &str) -> Result, DeviceConfigError> { let device = AirFilter { identifier: identifier.into(), config: self.clone(), - client: ext.client.clone(), last_known_state: AirFilterState { state: AirFilterFanState::Off, humidity: 0.0, @@ -51,7 +48,6 @@ pub struct AirFilter { #[config] config: AirFilterConfig, - client: AsyncClient, last_known_state: AirFilterState, } @@ -61,7 +57,8 @@ impl AirFilter { let topic = format!("{}/set", self.config.mqtt.topic); // TODO: Handle potential errors here - self.client + self.config + .client .publish( topic.clone(), rumqttc::QoS::AtLeastOnce, diff --git a/src/devices/audio_setup.rs b/src/devices/audio_setup.rs index 8bfc01a..072558a 100644 --- a/src/devices/audio_setup.rs +++ b/src/devices/audio_setup.rs @@ -1,78 +1,45 @@ use async_trait::async_trait; -use automation_macro::LuaDevice; +use automation_macro::{LuaDevice, LuaDeviceConfig}; use google_home::traits::OnOff; -use serde::Deserialize; use tracing::{debug, error, trace, warn}; use super::Device; use crate::config::MqttDeviceConfig; -use crate::device_manager::{ConfigExternal, DeviceConfig, WrappedDevice}; +use crate::device_manager::{DeviceConfig, WrappedDevice}; use crate::error::DeviceConfigError; use crate::event::{OnMqtt, OnPresence}; use crate::messages::{RemoteAction, RemoteMessage}; -#[derive(Debug, Clone, Deserialize)] +#[derive(Debug, Clone, LuaDeviceConfig)] pub struct AudioSetupConfig { - #[serde(flatten)] + #[device_config(flatten)] mqtt: MqttDeviceConfig, - mixer: String, - speakers: String, + #[device_config(user_data)] + mixer: WrappedDevice, + #[device_config(user_data)] + speakers: WrappedDevice, } #[async_trait] impl DeviceConfig for AudioSetupConfig { - async fn create( - &self, - identifier: &str, - ext: &ConfigExternal, - ) -> Result, DeviceConfigError> { + async fn create(&self, identifier: &str) -> 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(), - ))?; - - { - let mixer = mixer.read().await; - if (mixer.as_ref().cast() as Option<&dyn OnOff>).is_none() { - return Err(DeviceConfigError::MissingTrait( - self.mixer.clone(), - "OnOff".into(), - )); - } + let mixer = self.mixer.read().await; + let mixer_id = mixer.get_id().to_owned(); + if (mixer.as_ref().cast() as Option<&dyn OnOff>).is_none() { + return Err(DeviceConfigError::MissingTrait(mixer_id, "OnOff".into())); } - let speakers = - ext.device_manager - .get(&self.speakers) - .await - .ok_or(DeviceConfigError::MissingChild( - identifier.into(), - self.speakers.clone(), - ))?; - - { - let speakers = speakers.read().await; - if (speakers.as_ref().cast() as Option<&dyn OnOff>).is_none() { - return Err(DeviceConfigError::MissingTrait( - self.mixer.clone(), - "OnOff".into(), - )); - } + let speakers = self.speakers.read().await; + let speakers_id = speakers.get_id().to_owned(); + if (speakers.as_ref().cast() as Option<&dyn OnOff>).is_none() { + return Err(DeviceConfigError::MissingTrait(speakers_id, "OnOff".into())); } let device = AudioSetup { identifier: identifier.into(), config: self.clone(), - mixer, - speakers, }; Ok(Box::new(device)) @@ -85,8 +52,6 @@ pub struct AudioSetup { identifier: String, #[config] config: AudioSetupConfig, - mixer: WrappedDevice, - speakers: WrappedDevice, } impl Device for AudioSetup { @@ -110,8 +75,8 @@ impl OnMqtt for AudioSetup { } }; - let mut mixer = self.mixer.write().await; - let mut speakers = self.speakers.write().await; + let mut mixer = self.config.mixer.write().await; + let mut speakers = self.config.speakers.write().await; if let (Some(mixer), Some(speakers)) = ( mixer.as_mut().cast_mut() as Option<&mut dyn OnOff>, speakers.as_mut().cast_mut() as Option<&mut dyn OnOff>, @@ -145,8 +110,9 @@ impl OnMqtt for AudioSetup { #[async_trait] impl OnPresence for AudioSetup { async fn on_presence(&mut self, presence: bool) { - let mut mixer = self.mixer.write().await; - let mut speakers = self.speakers.write().await; + let mut mixer = self.config.mixer.write().await; + let mut speakers = self.config.speakers.write().await; + if let (Some(mixer), Some(speakers)) = ( mixer.as_mut().cast_mut() as Option<&mut dyn OnOff>, speakers.as_mut().cast_mut() as Option<&mut dyn OnOff>, diff --git a/src/devices/contact_sensor.rs b/src/devices/contact_sensor.rs index c7b03fb..c9952b2 100644 --- a/src/devices/contact_sensor.rs +++ b/src/devices/contact_sensor.rs @@ -1,86 +1,74 @@ use std::time::Duration; use async_trait::async_trait; -use automation_macro::LuaDevice; +use automation_macro::{LuaDevice, LuaDeviceConfig}; use google_home::traits::OnOff; -use rumqttc::AsyncClient; -use serde::Deserialize; -use serde_with::{serde_as, DurationSeconds}; use tokio::task::JoinHandle; use tracing::{debug, error, trace, warn}; use super::Device; use crate::config::MqttDeviceConfig; -use crate::device_manager::{ConfigExternal, DeviceConfig, WrappedDevice}; +use crate::device_manager::{DeviceConfig, WrappedDevice}; use crate::devices::DEFAULT_PRESENCE; use crate::error::DeviceConfigError; use crate::event::{OnMqtt, OnPresence}; +use crate::helper::DurationSeconds; use crate::messages::{ContactMessage, PresenceMessage}; +use crate::mqtt::WrappedAsyncClient; use crate::traits::Timeout; // NOTE: If we add more presence devices we might need to move this out of here -#[serde_as] -#[derive(Debug, Clone, Deserialize)] +#[derive(Debug, Clone, LuaDeviceConfig)] pub struct PresenceDeviceConfig { - #[serde(flatten)] + #[device_config(flatten)] pub mqtt: MqttDeviceConfig, - #[serde_as(as = "DurationSeconds")] + #[device_config(with = "DurationSeconds")] pub timeout: Duration, } -#[serde_as] -#[derive(Debug, Clone, Deserialize)] +#[derive(Debug, Clone, LuaDeviceConfig)] pub struct TriggerConfig { - devices: Vec, - #[serde(default)] - #[serde_as(as = "DurationSeconds")] - pub timeout: Duration, + #[device_config(user_data)] + devices: Vec, + #[device_config(with = "Option")] + pub timeout: Option, } -#[derive(Debug, Clone, Deserialize)] +#[derive(Debug, Clone, LuaDeviceConfig)] pub struct ContactSensorConfig { - #[serde(flatten)] + #[device_config(flatten)] mqtt: MqttDeviceConfig, + #[device_config(user_data)] presence: Option, + #[device_config(user_data)] trigger: Option, + #[device_config(user_data)] + client: WrappedAsyncClient, } #[async_trait] impl DeviceConfig for ContactSensorConfig { - async fn create( - &self, - identifier: &str, - ext: &ConfigExternal, - ) -> Result, DeviceConfigError> { + async fn create(&self, identifier: &str) -> Result, DeviceConfigError> { trace!(id = identifier, "Setting up ContactSensor"); let trigger = if let Some(trigger_config) = &self.trigger { let mut devices = Vec::new(); - for device_name in &trigger_config.devices { - let device = ext.device_manager.get(device_name).await.ok_or( - DeviceConfigError::MissingChild(device_name.into(), "OnOff".into()), - )?; - + for device in &trigger_config.devices { { let device = device.read().await; + let id = device.get_id().to_owned(); if (device.as_ref().cast() as Option<&dyn OnOff>).is_none() { - return Err(DeviceConfigError::MissingTrait( - device_name.into(), - "OnOff".into(), - )); + return Err(DeviceConfigError::MissingTrait(id, "OnOff".into())); } - if trigger_config.timeout.is_zero() + if trigger_config.timeout.is_none() && (device.as_ref().cast() as Option<&dyn Timeout>).is_none() { - return Err(DeviceConfigError::MissingTrait( - device_name.into(), - "Timeout".into(), - )); + return Err(DeviceConfigError::MissingTrait(id, "Timeout".into())); } } - devices.push((device, false)); + devices.push((device.clone(), false)); } Some(Trigger { @@ -94,7 +82,6 @@ impl DeviceConfig for ContactSensorConfig { let device = ContactSensor { identifier: identifier.into(), config: self.clone(), - client: ext.client.clone(), overall_presence: DEFAULT_PRESENCE, is_closed: true, handle: None, @@ -108,7 +95,7 @@ impl DeviceConfig for ContactSensorConfig { #[derive(Debug)] struct Trigger { devices: Vec<(WrappedDevice, bool)>, - timeout: Duration, // Timeout in seconds + timeout: Option, } #[derive(Debug, LuaDevice)] @@ -117,7 +104,6 @@ pub struct ContactSensor { #[config] config: ContactSensorConfig, - client: AsyncClient, overall_presence: bool, is_closed: bool, handle: Option>, @@ -174,14 +160,15 @@ impl OnMqtt for ContactSensor { let mut light = light.write().await; if !previous { // If the timeout is zero just turn the light off directly - if trigger.timeout.is_zero() + if trigger.timeout.is_none() && let Some(light) = light.as_mut().cast_mut() as Option<&mut dyn OnOff> { light.set_on(false).await.ok(); - } else if let Some(light) = - light.as_mut().cast_mut() as Option<&mut dyn Timeout> + } else if let Some(timeout) = trigger.timeout + && let Some(light) = + light.as_mut().cast_mut() as Option<&mut dyn Timeout> { - light.start_timeout(trigger.timeout).await.unwrap(); + light.start_timeout(timeout).await.unwrap(); } // TODO: Put a warning/error on creation if either of this has to option to fail } @@ -206,7 +193,8 @@ impl OnMqtt for ContactSensor { // This is to prevent the house from being marked as present for however long the // timeout is set when leaving the house if !self.overall_presence { - self.client + self.config + .client .publish( presence.mqtt.topic.clone(), rumqttc::QoS::AtLeastOnce, @@ -224,7 +212,7 @@ impl OnMqtt for ContactSensor { } } else { // Once the door is closed again we start a timeout for removing the presence - let client = self.client.clone(); + let client = self.config.client.clone(); let id = self.identifier.clone(); let timeout = presence.timeout; let topic = presence.mqtt.topic.clone(); diff --git a/src/devices/debug_bridge.rs b/src/devices/debug_bridge.rs index f5383a8..a13d101 100644 --- a/src/devices/debug_bridge.rs +++ b/src/devices/debug_bridge.rs @@ -1,33 +1,29 @@ use async_trait::async_trait; -use automation_macro::LuaDevice; -use rumqttc::AsyncClient; -use serde::Deserialize; +use automation_macro::{LuaDevice, LuaDeviceConfig}; use tracing::warn; use crate::config::MqttDeviceConfig; -use crate::device_manager::{ConfigExternal, DeviceConfig}; +use crate::device_manager::DeviceConfig; use crate::devices::Device; use crate::error::DeviceConfigError; use crate::event::{OnDarkness, OnPresence}; use crate::messages::{DarknessMessage, PresenceMessage}; +use crate::mqtt::WrappedAsyncClient; -#[derive(Debug, Deserialize, Clone)] +#[derive(Debug, LuaDeviceConfig, Clone)] pub struct DebugBridgeConfig { - #[serde(flatten)] + #[device_config(flatten)] pub mqtt: MqttDeviceConfig, + #[device_config(user_data)] + client: WrappedAsyncClient, } #[async_trait] impl DeviceConfig for DebugBridgeConfig { - async fn create( - &self, - identifier: &str, - ext: &ConfigExternal, - ) -> Result, DeviceConfigError> { + async fn create(&self, identifier: &str) -> Result, DeviceConfigError> { let device = DebugBridge { identifier: identifier.into(), config: self.clone(), - client: ext.client.clone(), }; Ok(Box::new(device)) @@ -39,7 +35,6 @@ pub struct DebugBridge { identifier: String, #[config] config: DebugBridgeConfig, - client: AsyncClient, } impl Device for DebugBridge { @@ -53,7 +48,8 @@ impl OnPresence for DebugBridge { async fn on_presence(&mut self, presence: bool) { let message = PresenceMessage::new(presence); let topic = format!("{}/presence", self.config.mqtt.topic); - self.client + self.config + .client .publish( topic, rumqttc::QoS::AtLeastOnce, @@ -76,7 +72,8 @@ impl OnDarkness for DebugBridge { async fn on_darkness(&mut self, dark: bool) { let message = DarknessMessage::new(dark); let topic = format!("{}/darkness", self.config.mqtt.topic); - self.client + self.config + .client .publish( topic, rumqttc::QoS::AtLeastOnce, diff --git a/src/devices/hue_bridge.rs b/src/devices/hue_bridge.rs index 90cf78e..12ffda7 100644 --- a/src/devices/hue_bridge.rs +++ b/src/devices/hue_bridge.rs @@ -1,11 +1,11 @@ use std::net::Ipv4Addr; use async_trait::async_trait; -use automation_macro::LuaDevice; +use automation_macro::{LuaDevice, LuaDeviceConfig}; use serde::{Deserialize, Serialize}; use tracing::{error, trace, warn}; -use crate::device_manager::{ConfigExternal, DeviceConfig}; +use crate::device_manager::DeviceConfig; use crate::devices::Device; use crate::error::DeviceConfigError; use crate::event::{OnDarkness, OnPresence}; @@ -22,8 +22,9 @@ pub struct FlagIDs { pub darkness: isize, } -#[derive(Debug, Deserialize, Clone)] +#[derive(Debug, LuaDeviceConfig, Clone)] pub struct HueBridgeConfig { + // TODO: Add helper type that converts this to a socketaddr automatically pub ip: Ipv4Addr, pub login: String, pub flags: FlagIDs, @@ -31,11 +32,7 @@ pub struct HueBridgeConfig { #[async_trait] impl DeviceConfig for HueBridgeConfig { - async fn create( - &self, - identifier: &str, - _ext: &ConfigExternal, - ) -> Result, DeviceConfigError> { + async fn create(&self, identifier: &str) -> Result, DeviceConfigError> { let device = HueBridge { identifier: identifier.into(), config: self.clone(), diff --git a/src/devices/hue_light.rs b/src/devices/hue_light.rs index 4431018..3f7f13c 100644 --- a/src/devices/hue_light.rs +++ b/src/devices/hue_light.rs @@ -3,39 +3,35 @@ use std::time::Duration; use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; -use automation_macro::LuaDevice; +use automation_macro::{LuaDevice, LuaDeviceConfig}; use google_home::errors::ErrorCode; use google_home::traits::OnOff; use rumqttc::Publish; -use serde::Deserialize; use tracing::{debug, error, warn}; use super::Device; use crate::config::MqttDeviceConfig; -use crate::device_manager::{ConfigExternal, DeviceConfig}; +use crate::device_manager::DeviceConfig; use crate::error::DeviceConfigError; use crate::event::OnMqtt; use crate::messages::{RemoteAction, RemoteMessage}; use crate::traits::Timeout; -#[derive(Debug, Clone, Deserialize)] +#[derive(Debug, Clone, LuaDeviceConfig)] pub struct HueGroupConfig { + // TODO: Add helper type that converts this to a socketaddr automatically pub ip: Ipv4Addr, pub login: String, pub group_id: isize, pub timer_id: isize, pub scene_id: String, - #[serde(default)] + #[device_config(default)] pub remotes: Vec, } #[async_trait] impl DeviceConfig for HueGroupConfig { - async fn create( - &self, - identifier: &str, - _ext: &ConfigExternal, - ) -> Result, DeviceConfigError> { + async fn create(&self, identifier: &str) -> Result, DeviceConfigError> { let device = HueGroup { identifier: identifier.into(), config: self.clone(), diff --git a/src/devices/ikea_outlet.rs b/src/devices/ikea_outlet.rs index cba7dfb..7c5cd7a 100644 --- a/src/devices/ikea_outlet.rs +++ b/src/devices/ikea_outlet.rs @@ -2,23 +2,24 @@ use std::time::Duration; use anyhow::Result; use async_trait::async_trait; -use automation_macro::LuaDevice; +use automation_macro::{LuaDevice, LuaDeviceConfig}; use google_home::errors::ErrorCode; use google_home::traits::{self, OnOff}; use google_home::types::Type; use google_home::{device, GoogleHomeDevice}; -use rumqttc::{matches, AsyncClient, Publish}; +use rumqttc::{matches, Publish}; use serde::Deserialize; -use serde_with::{serde_as, DurationSeconds}; use tokio::task::JoinHandle; use tracing::{debug, error, trace, warn}; use crate::config::{InfoConfig, MqttDeviceConfig}; -use crate::device_manager::{ConfigExternal, DeviceConfig}; +use crate::device_manager::DeviceConfig; use crate::devices::Device; use crate::error::DeviceConfigError; use crate::event::{OnMqtt, OnPresence}; +use crate::helper::DurationSeconds; use crate::messages::{OnOffMessage, RemoteAction, RemoteMessage}; +use crate::mqtt::WrappedAsyncClient; use crate::traits::Timeout; #[derive(Debug, Clone, Deserialize, PartialEq, Eq, Copy)] @@ -29,19 +30,21 @@ pub enum OutletType { Light, } -#[serde_as] -#[derive(Debug, Clone, Deserialize)] +#[derive(Debug, Clone, LuaDeviceConfig)] pub struct IkeaOutletConfig { - #[serde(flatten)] + #[device_config(flatten)] info: InfoConfig, - #[serde(flatten)] + #[device_config(flatten)] mqtt: MqttDeviceConfig, - #[serde(default = "default_outlet_type")] + #[device_config(default = default_outlet_type)] outlet_type: OutletType, - #[serde_as(as = "Option")] - timeout: Option, // Timeout in seconds - #[serde(default)] + #[device_config(with = "Option")] + timeout: Option, + #[device_config(default)] pub remotes: Vec, + + #[device_config(user_data)] + client: WrappedAsyncClient, } fn default_outlet_type() -> OutletType { @@ -50,11 +53,7 @@ fn default_outlet_type() -> OutletType { #[async_trait] impl DeviceConfig for IkeaOutletConfig { - async fn create( - &self, - identifier: &str, - ext: &ConfigExternal, - ) -> Result, DeviceConfigError> { + async fn create(&self, identifier: &str) -> Result, DeviceConfigError> { trace!( id = identifier, name = self.info.name, @@ -65,7 +64,6 @@ impl DeviceConfig for IkeaOutletConfig { let device = IkeaOutlet { identifier: identifier.into(), config: self.clone(), - client: ext.client.clone(), last_known_state: false, handle: None, }; @@ -80,12 +78,11 @@ pub struct IkeaOutlet { #[config] config: IkeaOutletConfig, - client: AsyncClient, last_known_state: bool, handle: Option>, } -async fn set_on(client: AsyncClient, topic: &str, on: bool) { +async fn set_on(client: WrappedAsyncClient, topic: &str, on: bool) { let message = OnOffMessage::new(on); let topic = format!("{}/set", topic); @@ -219,7 +216,7 @@ impl traits::OnOff for IkeaOutlet { } async fn set_on(&mut self, on: bool) -> Result<(), ErrorCode> { - set_on(self.client.clone(), &self.config.mqtt.topic, on).await; + set_on(self.config.client.clone(), &self.config.mqtt.topic, on).await; Ok(()) } @@ -234,7 +231,7 @@ impl crate::traits::Timeout for IkeaOutlet { // Turn the kettle of after the specified timeout // TODO: Impl Drop for IkeaOutlet that will abort the handle if the IkeaOutlet // get dropped - let client = self.client.clone(); + let client = self.config.client.clone(); let topic = self.config.mqtt.topic.clone(); let id = self.identifier.clone(); self.handle = Some(tokio::spawn(async move { diff --git a/src/devices/kasa_outlet.rs b/src/devices/kasa_outlet.rs index dab2462..82699c0 100644 --- a/src/devices/kasa_outlet.rs +++ b/src/devices/kasa_outlet.rs @@ -2,7 +2,7 @@ use std::net::{Ipv4Addr, SocketAddr}; use std::str::Utf8Error; use async_trait::async_trait; -use automation_macro::LuaDevice; +use automation_macro::{LuaDevice, LuaDeviceConfig}; use bytes::{Buf, BufMut}; use google_home::errors::{self, DeviceError}; use google_home::traits; @@ -13,21 +13,18 @@ use tokio::net::TcpStream; use tracing::trace; use super::Device; -use crate::device_manager::{ConfigExternal, DeviceConfig}; +use crate::device_manager::DeviceConfig; use crate::error::DeviceConfigError; -#[derive(Debug, Clone, Deserialize)] +#[derive(Debug, Clone, LuaDeviceConfig)] pub struct KasaOutletConfig { + // TODO: Add helper type that converts this to a socketaddr automatically ip: Ipv4Addr, } #[async_trait] impl DeviceConfig for KasaOutletConfig { - async fn create( - &self, - identifier: &str, - _ext: &ConfigExternal, - ) -> Result, DeviceConfigError> { + async fn create(&self, identifier: &str) -> Result, DeviceConfigError> { trace!(id = identifier, "Setting up KasaOutlet"); let device = KasaOutlet { diff --git a/src/devices/light_sensor.rs b/src/devices/light_sensor.rs index 2dc086f..6f060ce 100644 --- a/src/devices/light_sensor.rs +++ b/src/devices/light_sensor.rs @@ -1,22 +1,23 @@ use async_trait::async_trait; -use automation_macro::LuaDevice; +use automation_macro::{LuaDevice, LuaDeviceConfig}; use rumqttc::Publish; -use serde::Deserialize; use tracing::{debug, trace, warn}; use crate::config::MqttDeviceConfig; -use crate::device_manager::{ConfigExternal, DeviceConfig}; +use crate::device_manager::DeviceConfig; use crate::devices::Device; use crate::error::DeviceConfigError; -use crate::event::{self, Event, OnMqtt}; +use crate::event::{self, Event, EventChannel, OnMqtt}; use crate::messages::BrightnessMessage; -#[derive(Debug, Clone, Deserialize)] +#[derive(Debug, Clone, LuaDeviceConfig)] pub struct LightSensorConfig { - #[serde(flatten)] + #[device_config(flatten)] pub mqtt: MqttDeviceConfig, pub min: isize, pub max: isize, + #[device_config(user_data)] + pub event_channel: EventChannel, } pub const DEFAULT: bool = false; @@ -25,14 +26,11 @@ pub const DEFAULT: bool = false; #[async_trait] impl DeviceConfig for LightSensorConfig { - async fn create( - &self, - identifier: &str, - ext: &ConfigExternal, - ) -> Result, DeviceConfigError> { + async fn create(&self, identifier: &str) -> Result, DeviceConfigError> { let device = LightSensor { identifier: identifier.into(), - tx: ext.event_channel.get_tx(), + // Add helper type that does this conversion for us + tx: self.event_channel.get_tx(), config: self.clone(), is_dark: DEFAULT, }; diff --git a/src/devices/mod.rs b/src/devices/mod.rs index 0e6c8f3..35eff8b 100644 --- a/src/devices/mod.rs +++ b/src/devices/mod.rs @@ -26,7 +26,7 @@ pub use self::hue_bridge::*; pub use self::hue_light::*; pub use self::ikea_outlet::*; pub use self::kasa_outlet::*; -pub use self::light_sensor::{LightSensor, LightSensorConfig}; +pub use self::light_sensor::*; pub use self::ntfy::{Notification, Ntfy}; pub use self::presence::{Presence, PresenceConfig, DEFAULT_PRESENCE}; pub use self::wake_on_lan::*; diff --git a/src/devices/wake_on_lan.rs b/src/devices/wake_on_lan.rs index afece56..0162957 100644 --- a/src/devices/wake_on_lan.rs +++ b/src/devices/wake_on_lan.rs @@ -1,31 +1,30 @@ use std::net::Ipv4Addr; use async_trait::async_trait; -use automation_macro::LuaDevice; +use automation_macro::{LuaDevice, LuaDeviceConfig}; use eui48::MacAddress; use google_home::errors::ErrorCode; use google_home::traits::{self, Scene}; use google_home::types::Type; use google_home::{device, GoogleHomeDevice}; use rumqttc::Publish; -use serde::Deserialize; use tracing::{debug, error, trace}; use super::Device; use crate::config::{InfoConfig, MqttDeviceConfig}; -use crate::device_manager::{ConfigExternal, DeviceConfig}; +use crate::device_manager::DeviceConfig; use crate::error::DeviceConfigError; use crate::event::OnMqtt; use crate::messages::ActivateMessage; -#[derive(Debug, Clone, Deserialize)] +#[derive(Debug, Clone, LuaDeviceConfig)] pub struct WakeOnLANConfig { - #[serde(flatten)] + #[device_config(flatten)] info: InfoConfig, - #[serde(flatten)] + #[device_config(flatten)] mqtt: MqttDeviceConfig, mac_address: MacAddress, - #[serde(default = "default_broadcast_ip")] + #[device_config(default = default_broadcast_ip)] broadcast_ip: Ipv4Addr, } @@ -35,11 +34,7 @@ fn default_broadcast_ip() -> Ipv4Addr { #[async_trait] impl DeviceConfig for WakeOnLANConfig { - async fn create( - &self, - identifier: &str, - _ext: &ConfigExternal, - ) -> Result, DeviceConfigError> { + async fn create(&self, identifier: &str) -> Result, DeviceConfigError> { trace!( id = identifier, name = self.info.name, @@ -47,6 +42,8 @@ impl DeviceConfig for WakeOnLANConfig { "Setting up WakeOnLAN" ); + debug!("broadcast_ip = {}", self.broadcast_ip); + let device = WakeOnLAN { identifier: identifier.into(), config: self.clone(), diff --git a/src/devices/washer.rs b/src/devices/washer.rs index e5212c4..fdff148 100644 --- a/src/devices/washer.rs +++ b/src/devices/washer.rs @@ -1,35 +1,32 @@ use async_trait::async_trait; -use automation_macro::LuaDevice; +use automation_macro::{LuaDevice, LuaDeviceConfig}; use rumqttc::Publish; -use serde::Deserialize; use tracing::{debug, error, warn}; use super::ntfy::Priority; use super::{Device, Notification}; use crate::config::MqttDeviceConfig; -use crate::device_manager::{ConfigExternal, DeviceConfig}; +use crate::device_manager::DeviceConfig; use crate::error::DeviceConfigError; use crate::event::{Event, EventChannel, OnMqtt}; use crate::messages::PowerMessage; -#[derive(Debug, Clone, Deserialize)] +#[derive(Debug, Clone, LuaDeviceConfig)] pub struct WasherConfig { - #[serde(flatten)] + #[device_config(flatten)] mqtt: MqttDeviceConfig, - threshold: f32, // Power in Watt + // Power in Watt + threshold: f32, + #[device_config(user_data)] + event_channel: EventChannel, } #[async_trait] impl DeviceConfig for WasherConfig { - async fn create( - &self, - identifier: &str, - ext: &ConfigExternal, - ) -> Result, DeviceConfigError> { + async fn create(&self, identifier: &str) -> Result, DeviceConfigError> { let device = Washer { identifier: identifier.into(), config: self.clone(), - event_channel: ext.event_channel.clone(), running: 0, }; @@ -45,7 +42,6 @@ pub struct Washer { #[config] config: WasherConfig, - event_channel: EventChannel, running: isize, } @@ -94,6 +90,7 @@ impl OnMqtt for Washer { .set_priority(Priority::High); if self + .config .event_channel .get_tx() .send(Event::Ntfy(notification)) diff --git a/src/event.rs b/src/event.rs index a9e69b1..39ea779 100644 --- a/src/event.rs +++ b/src/event.rs @@ -1,4 +1,5 @@ use async_trait::async_trait; +use mlua::FromLua; use rumqttc::Publish; use tokio::sync::mpsc; @@ -15,7 +16,7 @@ pub enum Event { pub type Sender = mpsc::Sender; pub type Receiver = mpsc::Receiver; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, FromLua)] pub struct EventChannel(Sender); impl EventChannel { @@ -30,6 +31,8 @@ impl EventChannel { } } +impl mlua::UserData for EventChannel {} + #[async_trait] pub trait OnMqtt: Sync + Send { fn topics(&self) -> Vec<&str>; diff --git a/src/helper.rs b/src/helper.rs new file mode 100644 index 0000000..68dde86 --- /dev/null +++ b/src/helper.rs @@ -0,0 +1,12 @@ +use std::time::Duration; + +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub struct DurationSeconds(u64); + +impl From for Duration { + fn from(value: DurationSeconds) -> Self { + Self::from_secs(value.0) + } +} diff --git a/src/lib.rs b/src/lib.rs index 931fcb5..8d8b7e2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,7 @@ pub mod device_manager; pub mod devices; pub mod error; pub mod event; +pub mod helper; pub mod messages; pub mod mqtt; pub mod schedule; diff --git a/src/main.rs b/src/main.rs index 511efb7..1a96652 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,7 +9,7 @@ use automation::devices::{ LightSensor, Ntfy, Presence, WakeOnLAN, Washer, }; use automation::error::ApiError; -use automation::mqtt; +use automation::mqtt::{self, WrappedAsyncClient}; use axum::extract::FromRef; use axum::http::StatusCode; use axum::response::IntoResponse; @@ -85,6 +85,8 @@ async fn app() -> anyhow::Result<()> { let automation = lua.create_table()?; automation.set("device_manager", device_manager.clone())?; + automation.set("mqtt_client", WrappedAsyncClient(client.clone()))?; + automation.set("event_channel", device_manager.event_channel())?; let util = lua.create_table()?; let get_env = lua.create_function(|_lua, name: String| { diff --git a/src/mqtt.rs b/src/mqtt.rs index a743321..b610fb1 100644 --- a/src/mqtt.rs +++ b/src/mqtt.rs @@ -1,8 +1,30 @@ -use rumqttc::{Event, EventLoop, Incoming}; +use std::ops::{Deref, DerefMut}; + +use mlua::FromLua; +use rumqttc::{AsyncClient, Event, EventLoop, Incoming}; use tracing::{debug, warn}; use crate::event::{self, EventChannel}; +#[derive(Debug, Clone, FromLua)] +pub struct WrappedAsyncClient(pub AsyncClient); + +impl Deref for WrappedAsyncClient { + type Target = AsyncClient; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for WrappedAsyncClient { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl mlua::UserData for WrappedAsyncClient {} + pub fn start(mut eventloop: EventLoop, event_channel: &EventChannel) { let tx = event_channel.get_tx();