Everything needed to construct a new device is passed in through lua

This commit is contained in:
Dreaded_X 2024-04-25 01:35:23 +02:00
parent 0d1e15c676
commit 6e1b2a8935
Signed by: Dreaded_X
GPG Key ID: FA5F485356B0D2D4
22 changed files with 490 additions and 252 deletions

1
Cargo.lock generated
View File

@ -110,6 +110,7 @@ dependencies = [
name = "automation_macro" name = "automation_macro"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"proc-macro2",
"quote", "quote",
"syn 2.0.60", "syn 2.0.60",
] ]

View File

@ -7,5 +7,6 @@ edition = "2021"
proc-macro = true proc-macro = true
[dependencies] [dependencies]
proc-macro2 = "1.0.81"
quote = "1.0.36" quote = "1.0.36"
syn = "2.0.60" syn = { version = "2.0.60", features = ["extra-traits"] }

View File

@ -1,17 +1,17 @@
use proc_macro::TokenStream; use proc_macro2::TokenStream;
use quote::quote; 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))] #[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); 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 { fn impl_lua_device_macro(ast: &syn::DeriveInput) -> TokenStream {
let name = &ast.ident; let name = &ast.ident;
let name_string = name.to_string();
// TODO: Handle errors properly // TODO: Handle errors properly
// This includes making sure one, and only one config is specified // This includes making sure one, and only one config is specified
let config = if let syn::Data::Struct(syn::DataStruct { 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! { let gen = quote! {
impl #name { impl #name {
pub fn register_with_lua(lua: &mlua::Lua) -> mlua::Result<()> { 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 { impl mlua::UserData for #name {
fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) { fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_function("new", |lua, config: mlua::Value| { 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<dyn crate::device_manager::DeviceConfig> = Box::new(config); let config: Box<dyn crate::device_manager::DeviceConfig> = Box::new(config);
Ok(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<syn::Ident>),
}
impl syn::parse::Parse for Arg {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let arg = match input.parse::<syn::Ident>()?.to_string().as_str() {
"flatten" => Arg::Flatten,
"user_data" => Arg::UserData,
"with" => {
input.parse::<Token![=]>()?;
let lit = input.parse::<syn::Lit>()?;
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::<Token![=]>().is_ok() {
let func = input.parse::<syn::Ident>()?;
Arg::Default(Some(func))
} else {
Arg::Default(None)
}
}
name => todo!("Handle unknown arg: {name}"),
};
Ok(arg)
}
}
#[derive(Debug)]
struct ArgsParser {
args: Punctuated<Arg, Token![,]>,
}
impl syn::parse::Parse for ArgsParser {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let args = input.parse_terminated(Arg::parse, Token![,])?;
Ok(Self { args })
}
}
#[derive(Debug)]
struct Args {
flatten: bool,
user_data: bool,
with: Option<TokenStream>,
default: Option<Option<syn::Ident>>,
}
impl Args {
fn new(args: Vec<Arg>) -> 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<Self> {
if !value.is_table() {
panic!("Expected table");
}
let table = value.as_table().unwrap();
Ok(#name {
#(#fields,)*
})
}
}
};
gen
} }

View File

@ -17,6 +17,7 @@ automation.device_manager:create(
"debug_bridge", "debug_bridge",
DebugBridge.new({ DebugBridge.new({
topic = mqtt_automation("debug"), topic = mqtt_automation("debug"),
client = automation.mqtt_client,
}) })
) )
@ -41,6 +42,7 @@ automation.device_manager:create(
topic = mqtt_z2m("living/light"), topic = mqtt_z2m("living/light"),
min = 22000, min = 22000,
max = 23500, max = 23500,
event_channel = automation.event_channel,
}) })
) )
@ -74,6 +76,7 @@ automation.device_manager:create(
name = "Kettle", name = "Kettle",
room = "Kitchen", room = "Kitchen",
topic = mqtt_z2m("kitchen/kettle"), topic = mqtt_z2m("kitchen/kettle"),
client = automation.mqtt_client,
timeout = debug and 5 or 300, timeout = debug and 5 or 300,
remotes = { remotes = {
{ topic = mqtt_z2m("bedroom/remote") }, { topic = mqtt_z2m("bedroom/remote") },
@ -89,6 +92,7 @@ automation.device_manager:create(
name = "Light", name = "Light",
room = "Bathroom", room = "Bathroom",
topic = mqtt_z2m("batchroom/light"), topic = mqtt_z2m("batchroom/light"),
client = automation.mqtt_client,
timeout = debug and 60 or 45 * 60, timeout = debug and 60 or 45 * 60,
}) })
) )
@ -98,6 +102,7 @@ automation.device_manager:create(
Washer.new({ Washer.new({
topic = mqtt_z2m("batchroom/washer"), topic = mqtt_z2m("batchroom/washer"),
threshold = 1, threshold = 1,
event_channel = automation.event_channel,
}) })
) )
@ -108,6 +113,7 @@ automation.device_manager:create(
name = "Charger", name = "Charger",
room = "Workbench", room = "Workbench",
topic = mqtt_z2m("workbench/charger"), topic = mqtt_z2m("workbench/charger"),
client = automation.mqtt_client,
timeout = debug and 5 or 20 * 3600, timeout = debug and 5 or 20 * 3600,
}) })
) )
@ -118,6 +124,7 @@ automation.device_manager:create(
name = "Outlet", name = "Outlet",
room = "Workbench", room = "Workbench",
topic = mqtt_z2m("workbench/outlet"), topic = mqtt_z2m("workbench/outlet"),
client = automation.mqtt_client,
}) })
) )
@ -139,6 +146,7 @@ automation.device_manager:create(
"hallway_frontdoor", "hallway_frontdoor",
ContactSensor.new({ ContactSensor.new({
topic = mqtt_z2m("hallway/frontdoor"), topic = mqtt_z2m("hallway/frontdoor"),
client = automation.mqtt_client,
presence = { presence = {
topic = mqtt_automation("presence/contact/frontdoor"), topic = mqtt_automation("presence/contact/frontdoor"),
timeout = debug and 10 or 15 * 60, timeout = debug and 10 or 15 * 60,
@ -156,5 +164,6 @@ automation.device_manager:create(
name = "Air Filter", name = "Air Filter",
room = "Bedroom", room = "Bedroom",
topic = "pico/filter/bedroom", topic = "pico/filter/bedroom",
client = automation.mqtt_client,
}) })
) )

View File

@ -1,10 +1,12 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::ops::{Deref, DerefMut};
use std::sync::Arc; use std::sync::Arc;
use async_trait::async_trait; use async_trait::async_trait;
use enum_dispatch::enum_dispatch; use enum_dispatch::enum_dispatch;
use futures::future::join_all; use futures::future::join_all;
use google_home::traits::OnOff; use google_home::traits::OnOff;
use mlua::FromLua;
use rumqttc::{matches, AsyncClient, QoS}; use rumqttc::{matches, AsyncClient, QoS};
use tokio::sync::{RwLock, RwLockReadGuard}; use tokio::sync::{RwLock, RwLockReadGuard};
use tokio_cron_scheduler::{Job, JobScheduler}; 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::event::{Event, EventChannel, OnDarkness, OnMqtt, OnNotification, OnPresence};
use crate::schedule::{Action, Schedule}; 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] #[async_trait]
#[enum_dispatch] #[enum_dispatch]
pub trait DeviceConfig { pub trait DeviceConfig {
async fn create( async fn create(&self, identifier: &str) -> Result<Box<dyn Device>, DeviceConfigError>;
&self,
identifier: &str,
ext: &ConfigExternal,
) -> Result<Box<dyn Device>, DeviceConfigError>;
} }
impl mlua::UserData for Box<dyn DeviceConfig> {} impl mlua::UserData for Box<dyn DeviceConfig> {}
pub type WrappedDevice = Arc<RwLock<Box<dyn Device>>>; #[derive(Debug, FromLua, Clone)]
pub type DeviceMap = HashMap<String, WrappedDevice>; pub struct WrappedDevice(Arc<RwLock<Box<dyn Device>>>);
impl WrappedDevice {
fn new(device: Box<dyn Device>) -> Self {
Self(Arc::new(RwLock::new(device)))
}
}
impl Deref for WrappedDevice {
type Target = Arc<RwLock<Box<dyn Device>>>;
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<String, Arc<RwLock<Box<dyn Device>>>>;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct DeviceManager { pub struct DeviceManager {
@ -117,7 +132,7 @@ impl DeviceManager {
sched.start().await.unwrap(); sched.start().await.unwrap();
} }
pub async fn add(&self, device: Box<dyn Device>) { pub async fn add(&self, device: Box<dyn Device>) -> WrappedDevice {
let id = device.get_id().into(); let id = device.get_id().into();
debug!(id, "Adding device"); debug!(id, "Adding device");
@ -135,9 +150,11 @@ impl DeviceManager {
} }
// Wrap the device // 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 { pub fn event_channel(&self) -> EventChannel {
@ -145,7 +162,12 @@ impl DeviceManager {
} }
pub async fn get(&self, name: &str) -> Option<WrappedDevice> { pub async fn get(&self, name: &str) -> Option<WrappedDevice> {
self.devices.read().await.get(name).cloned() self.devices
.read()
.await
.get(name)
.cloned()
.map(WrappedDevice)
} }
pub async fn devices(&self) -> RwLockReadGuard<DeviceMap> { pub async fn devices(&self) -> RwLockReadGuard<DeviceMap> {
@ -232,22 +254,12 @@ impl mlua::UserData for DeviceManager {
// TODO: Handle the error here properly // TODO: Handle the error here properly
let config: Box<dyn DeviceConfig> = config.as_userdata().unwrap().take()?; let config: Box<dyn DeviceConfig> = config.as_userdata().unwrap().take()?;
let ext = ConfigExternal {
client: &this.client,
device_manager: this,
event_channel: &this.event_channel,
};
let device = config let device = config
.create(&identifier, &ext) .create(&identifier)
.await .await
.map_err(mlua::ExternalError::into_lua_err)?; .map_err(mlua::ExternalError::into_lua_err)?;
let id = device.get_id().to_owned(); Ok(this.add(device).await)
this.add(device).await;
Ok(id)
}, },
) )
} }

View File

@ -1,40 +1,37 @@
use async_trait::async_trait; use async_trait::async_trait;
use automation_macro::LuaDevice; use automation_macro::{LuaDevice, LuaDeviceConfig};
use google_home::device::Name; use google_home::device::Name;
use google_home::errors::ErrorCode; use google_home::errors::ErrorCode;
use google_home::traits::{AvailableSpeeds, FanSpeed, HumiditySetting, OnOff, Speed, SpeedValues}; use google_home::traits::{AvailableSpeeds, FanSpeed, HumiditySetting, OnOff, Speed, SpeedValues};
use google_home::types::Type; use google_home::types::Type;
use google_home::GoogleHomeDevice; use google_home::GoogleHomeDevice;
use rumqttc::{AsyncClient, Publish}; use rumqttc::Publish;
use serde::Deserialize;
use tracing::{debug, error, warn}; use tracing::{debug, error, warn};
use crate::config::{InfoConfig, MqttDeviceConfig}; use crate::config::{InfoConfig, MqttDeviceConfig};
use crate::device_manager::{ConfigExternal, DeviceConfig}; use crate::device_manager::DeviceConfig;
use crate::devices::Device; use crate::devices::Device;
use crate::error::DeviceConfigError; use crate::error::DeviceConfigError;
use crate::event::OnMqtt; use crate::event::OnMqtt;
use crate::messages::{AirFilterFanState, AirFilterState, SetAirFilterFanState}; use crate::messages::{AirFilterFanState, AirFilterState, SetAirFilterFanState};
use crate::mqtt::WrappedAsyncClient;
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Clone, LuaDeviceConfig)]
pub struct AirFilterConfig { pub struct AirFilterConfig {
#[serde(flatten)] #[device_config(flatten)]
info: InfoConfig, info: InfoConfig,
#[serde(flatten)] #[device_config(flatten)]
mqtt: MqttDeviceConfig, mqtt: MqttDeviceConfig,
#[device_config(user_data)]
client: WrappedAsyncClient,
} }
#[async_trait] #[async_trait]
impl DeviceConfig for AirFilterConfig { impl DeviceConfig for AirFilterConfig {
async fn create( async fn create(&self, identifier: &str) -> Result<Box<dyn Device>, DeviceConfigError> {
&self,
identifier: &str,
ext: &ConfigExternal,
) -> Result<Box<dyn Device>, DeviceConfigError> {
let device = AirFilter { let device = AirFilter {
identifier: identifier.into(), identifier: identifier.into(),
config: self.clone(), config: self.clone(),
client: ext.client.clone(),
last_known_state: AirFilterState { last_known_state: AirFilterState {
state: AirFilterFanState::Off, state: AirFilterFanState::Off,
humidity: 0.0, humidity: 0.0,
@ -51,7 +48,6 @@ pub struct AirFilter {
#[config] #[config]
config: AirFilterConfig, config: AirFilterConfig,
client: AsyncClient,
last_known_state: AirFilterState, last_known_state: AirFilterState,
} }
@ -61,7 +57,8 @@ impl AirFilter {
let topic = format!("{}/set", self.config.mqtt.topic); let topic = format!("{}/set", self.config.mqtt.topic);
// TODO: Handle potential errors here // TODO: Handle potential errors here
self.client self.config
.client
.publish( .publish(
topic.clone(), topic.clone(),
rumqttc::QoS::AtLeastOnce, rumqttc::QoS::AtLeastOnce,

View File

@ -1,73 +1,44 @@
use async_trait::async_trait; use async_trait::async_trait;
use automation_macro::LuaDevice; use automation_macro::{LuaDevice, LuaDeviceConfig};
use google_home::traits::OnOff; use google_home::traits::OnOff;
use serde::Deserialize;
use tracing::{debug, error, trace, warn}; use tracing::{debug, error, trace, warn};
use super::Device; use super::Device;
use crate::config::MqttDeviceConfig; use crate::config::MqttDeviceConfig;
use crate::device_manager::{ConfigExternal, DeviceConfig, WrappedDevice}; use crate::device_manager::{DeviceConfig, WrappedDevice};
use crate::devices::As; use crate::devices::As;
use crate::error::DeviceConfigError; use crate::error::DeviceConfigError;
use crate::event::{OnMqtt, OnPresence}; use crate::event::{OnMqtt, OnPresence};
use crate::messages::{RemoteAction, RemoteMessage}; use crate::messages::{RemoteAction, RemoteMessage};
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, LuaDeviceConfig)]
pub struct AudioSetupConfig { pub struct AudioSetupConfig {
#[serde(flatten)] #[device_config(flatten)]
mqtt: MqttDeviceConfig, mqtt: MqttDeviceConfig,
mixer: String, #[device_config(user_data)]
speakers: String, mixer: WrappedDevice,
#[device_config(user_data)]
speakers: WrappedDevice,
} }
#[async_trait] #[async_trait]
impl DeviceConfig for AudioSetupConfig { impl DeviceConfig for AudioSetupConfig {
async fn create( async fn create(&self, identifier: &str) -> Result<Box<dyn Device>, DeviceConfigError> {
&self,
identifier: &str,
ext: &ConfigExternal,
) -> Result<Box<dyn Device>, DeviceConfigError> {
trace!(id = identifier, "Setting up AudioSetup"); trace!(id = identifier, "Setting up AudioSetup");
// TODO: Make sure they implement OnOff? let mixer_id = self.mixer.read().await.get_id().to_owned();
let mixer = ext if !As::<dyn OnOff>::is(self.mixer.read().await.as_ref()) {
.device_manager return Err(DeviceConfigError::MissingTrait(mixer_id, "OnOff".into()));
.get(&self.mixer)
.await
// NOTE: We need to clone to make the compiler happy, how ever if this clone happens the next one can never happen...
.ok_or(DeviceConfigError::MissingChild(
identifier.into(),
self.mixer.clone(),
))?;
if !As::<dyn OnOff>::is(mixer.read().await.as_ref()) {
return Err(DeviceConfigError::MissingTrait(
self.mixer.clone(),
"OnOff".into(),
));
} }
let speakers = let speakers_id = self.speakers.read().await.get_id().to_owned();
ext.device_manager if !As::<dyn OnOff>::is(self.speakers.read().await.as_ref()) {
.get(&self.speakers) return Err(DeviceConfigError::MissingTrait(speakers_id, "OnOff".into()));
.await
.ok_or(DeviceConfigError::MissingChild(
identifier.into(),
self.speakers.clone(),
))?;
if !As::<dyn OnOff>::is(speakers.read().await.as_ref()) {
return Err(DeviceConfigError::MissingTrait(
self.speakers.clone(),
"OnOff".into(),
));
} }
let device = AudioSetup { let device = AudioSetup {
identifier: identifier.into(), identifier: identifier.into(),
config: self.clone(), config: self.clone(),
mixer,
speakers,
}; };
Ok(Box::new(device)) Ok(Box::new(device))
@ -80,8 +51,6 @@ pub struct AudioSetup {
identifier: String, identifier: String,
#[config] #[config]
config: AudioSetupConfig, config: AudioSetupConfig,
mixer: WrappedDevice,
speakers: WrappedDevice,
} }
impl Device for AudioSetup { impl Device for AudioSetup {
@ -105,8 +74,8 @@ impl OnMqtt for AudioSetup {
} }
}; };
let mut mixer = self.mixer.write().await; let mut mixer = self.config.mixer.write().await;
let mut speakers = self.speakers.write().await; let mut speakers = self.config.speakers.write().await;
if let (Some(mixer), Some(speakers)) = ( if let (Some(mixer), Some(speakers)) = (
As::<dyn OnOff>::cast_mut(mixer.as_mut()), As::<dyn OnOff>::cast_mut(mixer.as_mut()),
As::<dyn OnOff>::cast_mut(speakers.as_mut()), As::<dyn OnOff>::cast_mut(speakers.as_mut()),
@ -140,8 +109,8 @@ impl OnMqtt for AudioSetup {
#[async_trait] #[async_trait]
impl OnPresence for AudioSetup { impl OnPresence for AudioSetup {
async fn on_presence(&mut self, presence: bool) { async fn on_presence(&mut self, presence: bool) {
let mut mixer = self.mixer.write().await; let mut mixer = self.config.mixer.write().await;
let mut speakers = self.speakers.write().await; let mut speakers = self.config.speakers.write().await;
if let (Some(mixer), Some(speakers)) = ( if let (Some(mixer), Some(speakers)) = (
As::<dyn OnOff>::cast_mut(mixer.as_mut()), As::<dyn OnOff>::cast_mut(mixer.as_mut()),

View File

@ -1,83 +1,71 @@
use std::time::Duration; use std::time::Duration;
use async_trait::async_trait; use async_trait::async_trait;
use automation_macro::LuaDevice; use automation_macro::{LuaDevice, LuaDeviceConfig};
use google_home::traits::OnOff; use google_home::traits::OnOff;
use rumqttc::AsyncClient;
use serde::Deserialize;
use serde_with::{serde_as, DurationSeconds};
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
use tracing::{debug, error, trace, warn}; use tracing::{debug, error, trace, warn};
use super::Device; use super::Device;
use crate::config::MqttDeviceConfig; use crate::config::MqttDeviceConfig;
use crate::device_manager::{ConfigExternal, DeviceConfig, WrappedDevice}; use crate::device_manager::{DeviceConfig, WrappedDevice};
use crate::devices::{As, DEFAULT_PRESENCE}; use crate::devices::{As, DEFAULT_PRESENCE};
use crate::error::DeviceConfigError; use crate::error::DeviceConfigError;
use crate::event::{OnMqtt, OnPresence}; use crate::event::{OnMqtt, OnPresence};
use crate::helper::DurationSeconds;
use crate::messages::{ContactMessage, PresenceMessage}; use crate::messages::{ContactMessage, PresenceMessage};
use crate::mqtt::WrappedAsyncClient;
use crate::traits::Timeout; use crate::traits::Timeout;
// NOTE: If we add more presence devices we might need to move this out of here // NOTE: If we add more presence devices we might need to move this out of here
#[serde_as] #[derive(Debug, Clone, LuaDeviceConfig)]
#[derive(Debug, Clone, Deserialize)]
pub struct PresenceDeviceConfig { pub struct PresenceDeviceConfig {
#[serde(flatten)] #[device_config(flatten)]
pub mqtt: MqttDeviceConfig, pub mqtt: MqttDeviceConfig,
#[serde_as(as = "DurationSeconds")] #[device_config(with = "DurationSeconds")]
pub timeout: Duration, pub timeout: Duration,
} }
#[serde_as] #[derive(Debug, Clone, LuaDeviceConfig)]
#[derive(Debug, Clone, Deserialize)]
pub struct TriggerConfig { pub struct TriggerConfig {
devices: Vec<String>, #[device_config(user_data)]
#[serde(default)] devices: Vec<WrappedDevice>,
#[serde_as(as = "DurationSeconds")] #[device_config(with = "Option<DurationSeconds>")]
pub timeout: Duration, pub timeout: Option<Duration>,
} }
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, LuaDeviceConfig)]
pub struct ContactSensorConfig { pub struct ContactSensorConfig {
#[serde(flatten)] #[device_config(flatten)]
mqtt: MqttDeviceConfig, mqtt: MqttDeviceConfig,
#[device_config(user_data)]
presence: Option<PresenceDeviceConfig>, presence: Option<PresenceDeviceConfig>,
#[device_config(user_data)]
trigger: Option<TriggerConfig>, trigger: Option<TriggerConfig>,
#[device_config(user_data)]
client: WrappedAsyncClient,
} }
#[async_trait] #[async_trait]
impl DeviceConfig for ContactSensorConfig { impl DeviceConfig for ContactSensorConfig {
async fn create( async fn create(&self, identifier: &str) -> Result<Box<dyn Device>, DeviceConfigError> {
&self,
identifier: &str,
ext: &ConfigExternal,
) -> Result<Box<dyn Device>, DeviceConfigError> {
trace!(id = identifier, "Setting up ContactSensor"); trace!(id = identifier, "Setting up ContactSensor");
let trigger = if let Some(trigger_config) = &self.trigger { let trigger = if let Some(trigger_config) = &self.trigger {
let mut devices = Vec::new(); let mut devices = Vec::new();
for device_name in &trigger_config.devices { for device in &trigger_config.devices {
let device = ext.device_manager.get(device_name).await.ok_or( let id = device.read().await.get_id().to_owned();
DeviceConfigError::MissingChild(device_name.into(), "OnOff".into()),
)?;
if !As::<dyn OnOff>::is(device.read().await.as_ref()) { if !As::<dyn OnOff>::is(device.read().await.as_ref()) {
return Err(DeviceConfigError::MissingTrait( return Err(DeviceConfigError::MissingTrait(id, "OnOff".into()));
device_name.into(),
"OnOff".into(),
));
} }
if !trigger_config.timeout.is_zero() if trigger_config.timeout.is_none()
&& !As::<dyn Timeout>::is(device.read().await.as_ref()) && !As::<dyn Timeout>::is(device.read().await.as_ref())
{ {
return Err(DeviceConfigError::MissingTrait( return Err(DeviceConfigError::MissingTrait(id, "Timeout".into()));
device_name.into(),
"Timeout".into(),
));
} }
devices.push((device, false)); devices.push((device.clone(), false));
} }
Some(Trigger { Some(Trigger {
@ -91,7 +79,6 @@ impl DeviceConfig for ContactSensorConfig {
let device = ContactSensor { let device = ContactSensor {
identifier: identifier.into(), identifier: identifier.into(),
config: self.clone(), config: self.clone(),
client: ext.client.clone(),
overall_presence: DEFAULT_PRESENCE, overall_presence: DEFAULT_PRESENCE,
is_closed: true, is_closed: true,
handle: None, handle: None,
@ -105,7 +92,7 @@ impl DeviceConfig for ContactSensorConfig {
#[derive(Debug)] #[derive(Debug)]
struct Trigger { struct Trigger {
devices: Vec<(WrappedDevice, bool)>, devices: Vec<(WrappedDevice, bool)>,
timeout: Duration, // Timeout in seconds timeout: Option<Duration>,
} }
#[derive(Debug, LuaDevice)] #[derive(Debug, LuaDevice)]
@ -114,7 +101,6 @@ pub struct ContactSensor {
#[config] #[config]
config: ContactSensorConfig, config: ContactSensorConfig,
client: AsyncClient,
overall_presence: bool, overall_presence: bool,
is_closed: bool, is_closed: bool,
handle: Option<JoinHandle<()>>, handle: Option<JoinHandle<()>>,
@ -170,12 +156,14 @@ impl OnMqtt for ContactSensor {
let mut light = light.write().await; let mut light = light.write().await;
if !previous { if !previous {
// If the timeout is zero just turn the light off directly // If the timeout is zero just turn the light off directly
if trigger.timeout.is_zero() if trigger.timeout.is_none()
&& let Some(light) = As::<dyn OnOff>::cast_mut(light.as_mut()) && let Some(light) = As::<dyn OnOff>::cast_mut(light.as_mut())
{ {
light.set_on(false).await.ok(); light.set_on(false).await.ok();
} else if let Some(light) = As::<dyn Timeout>::cast_mut(light.as_mut()) { } else if let Some(timeout) = trigger.timeout
light.start_timeout(trigger.timeout).await.unwrap(); && let Some(light) = As::<dyn Timeout>::cast_mut(light.as_mut())
{
light.start_timeout(timeout).await.unwrap();
} }
// TODO: Put a warning/error on creation if either of this has to option to fail // TODO: Put a warning/error on creation if either of this has to option to fail
} }
@ -200,7 +188,8 @@ impl OnMqtt for ContactSensor {
// This is to prevent the house from being marked as present for however long the // This is to prevent the house from being marked as present for however long the
// timeout is set when leaving the house // timeout is set when leaving the house
if !self.overall_presence { if !self.overall_presence {
self.client self.config
.client
.publish( .publish(
presence.mqtt.topic.clone(), presence.mqtt.topic.clone(),
rumqttc::QoS::AtLeastOnce, rumqttc::QoS::AtLeastOnce,
@ -218,7 +207,7 @@ impl OnMqtt for ContactSensor {
} }
} else { } else {
// Once the door is closed again we start a timeout for removing the presence // 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 id = self.identifier.clone();
let timeout = presence.timeout; let timeout = presence.timeout;
let topic = presence.mqtt.topic.clone(); let topic = presence.mqtt.topic.clone();

View File

@ -1,33 +1,29 @@
use async_trait::async_trait; use async_trait::async_trait;
use automation_macro::LuaDevice; use automation_macro::{LuaDevice, LuaDeviceConfig};
use rumqttc::AsyncClient;
use serde::Deserialize;
use tracing::warn; use tracing::warn;
use crate::config::MqttDeviceConfig; use crate::config::MqttDeviceConfig;
use crate::device_manager::{ConfigExternal, DeviceConfig}; use crate::device_manager::DeviceConfig;
use crate::devices::Device; use crate::devices::Device;
use crate::error::DeviceConfigError; use crate::error::DeviceConfigError;
use crate::event::{OnDarkness, OnPresence}; use crate::event::{OnDarkness, OnPresence};
use crate::messages::{DarknessMessage, PresenceMessage}; use crate::messages::{DarknessMessage, PresenceMessage};
use crate::mqtt::WrappedAsyncClient;
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, LuaDeviceConfig, Clone)]
pub struct DebugBridgeConfig { pub struct DebugBridgeConfig {
#[serde(flatten)] #[device_config(flatten)]
pub mqtt: MqttDeviceConfig, pub mqtt: MqttDeviceConfig,
#[device_config(user_data)]
client: WrappedAsyncClient,
} }
#[async_trait] #[async_trait]
impl DeviceConfig for DebugBridgeConfig { impl DeviceConfig for DebugBridgeConfig {
async fn create( async fn create(&self, identifier: &str) -> Result<Box<dyn Device>, DeviceConfigError> {
&self,
identifier: &str,
ext: &ConfigExternal,
) -> Result<Box<dyn Device>, DeviceConfigError> {
let device = DebugBridge { let device = DebugBridge {
identifier: identifier.into(), identifier: identifier.into(),
config: self.clone(), config: self.clone(),
client: ext.client.clone(),
}; };
Ok(Box::new(device)) Ok(Box::new(device))
@ -39,7 +35,6 @@ pub struct DebugBridge {
identifier: String, identifier: String,
#[config] #[config]
config: DebugBridgeConfig, config: DebugBridgeConfig,
client: AsyncClient,
} }
impl Device for DebugBridge { impl Device for DebugBridge {
@ -53,7 +48,8 @@ impl OnPresence for DebugBridge {
async fn on_presence(&mut self, presence: bool) { async fn on_presence(&mut self, presence: bool) {
let message = PresenceMessage::new(presence); let message = PresenceMessage::new(presence);
let topic = format!("{}/presence", self.config.mqtt.topic); let topic = format!("{}/presence", self.config.mqtt.topic);
self.client self.config
.client
.publish( .publish(
topic, topic,
rumqttc::QoS::AtLeastOnce, rumqttc::QoS::AtLeastOnce,
@ -76,7 +72,8 @@ impl OnDarkness for DebugBridge {
async fn on_darkness(&mut self, dark: bool) { async fn on_darkness(&mut self, dark: bool) {
let message = DarknessMessage::new(dark); let message = DarknessMessage::new(dark);
let topic = format!("{}/darkness", self.config.mqtt.topic); let topic = format!("{}/darkness", self.config.mqtt.topic);
self.client self.config
.client
.publish( .publish(
topic, topic,
rumqttc::QoS::AtLeastOnce, rumqttc::QoS::AtLeastOnce,

View File

@ -1,11 +1,11 @@
use std::net::Ipv4Addr; use std::net::Ipv4Addr;
use async_trait::async_trait; use async_trait::async_trait;
use automation_macro::LuaDevice; use automation_macro::{LuaDevice, LuaDeviceConfig};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tracing::{error, trace, warn}; use tracing::{error, trace, warn};
use crate::device_manager::{ConfigExternal, DeviceConfig}; use crate::device_manager::DeviceConfig;
use crate::devices::Device; use crate::devices::Device;
use crate::error::DeviceConfigError; use crate::error::DeviceConfigError;
use crate::event::{OnDarkness, OnPresence}; use crate::event::{OnDarkness, OnPresence};
@ -22,8 +22,9 @@ pub struct FlagIDs {
pub darkness: isize, pub darkness: isize,
} }
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, LuaDeviceConfig, Clone)]
pub struct HueBridgeConfig { pub struct HueBridgeConfig {
// TODO: Add helper type that converts this to a socketaddr automatically
pub ip: Ipv4Addr, pub ip: Ipv4Addr,
pub login: String, pub login: String,
pub flags: FlagIDs, pub flags: FlagIDs,
@ -31,11 +32,7 @@ pub struct HueBridgeConfig {
#[async_trait] #[async_trait]
impl DeviceConfig for HueBridgeConfig { impl DeviceConfig for HueBridgeConfig {
async fn create( async fn create(&self, identifier: &str) -> Result<Box<dyn Device>, DeviceConfigError> {
&self,
identifier: &str,
_ext: &ConfigExternal,
) -> Result<Box<dyn Device>, DeviceConfigError> {
let device = HueBridge { let device = HueBridge {
identifier: identifier.into(), identifier: identifier.into(),
config: self.clone(), config: self.clone(),

View File

@ -3,39 +3,35 @@ use std::time::Duration;
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use async_trait::async_trait; use async_trait::async_trait;
use automation_macro::LuaDevice; use automation_macro::{LuaDevice, LuaDeviceConfig};
use google_home::errors::ErrorCode; use google_home::errors::ErrorCode;
use google_home::traits::OnOff; use google_home::traits::OnOff;
use rumqttc::Publish; use rumqttc::Publish;
use serde::Deserialize;
use tracing::{debug, error, warn}; use tracing::{debug, error, warn};
use super::Device; use super::Device;
use crate::config::MqttDeviceConfig; use crate::config::MqttDeviceConfig;
use crate::device_manager::{ConfigExternal, DeviceConfig}; use crate::device_manager::DeviceConfig;
use crate::error::DeviceConfigError; use crate::error::DeviceConfigError;
use crate::event::OnMqtt; use crate::event::OnMqtt;
use crate::messages::{RemoteAction, RemoteMessage}; use crate::messages::{RemoteAction, RemoteMessage};
use crate::traits::Timeout; use crate::traits::Timeout;
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, LuaDeviceConfig)]
pub struct HueGroupConfig { pub struct HueGroupConfig {
// TODO: Add helper type that converts this to a socketaddr automatically
pub ip: Ipv4Addr, pub ip: Ipv4Addr,
pub login: String, pub login: String,
pub group_id: isize, pub group_id: isize,
pub timer_id: isize, pub timer_id: isize,
pub scene_id: String, pub scene_id: String,
#[serde(default)] #[device_config(default)]
pub remotes: Vec<MqttDeviceConfig>, pub remotes: Vec<MqttDeviceConfig>,
} }
#[async_trait] #[async_trait]
impl DeviceConfig for HueGroupConfig { impl DeviceConfig for HueGroupConfig {
async fn create( async fn create(&self, identifier: &str) -> Result<Box<dyn Device>, DeviceConfigError> {
&self,
identifier: &str,
_ext: &ConfigExternal,
) -> Result<Box<dyn Device>, DeviceConfigError> {
let device = HueGroup { let device = HueGroup {
identifier: identifier.into(), identifier: identifier.into(),
config: self.clone(), config: self.clone(),

View File

@ -2,23 +2,24 @@ use std::time::Duration;
use anyhow::Result; use anyhow::Result;
use async_trait::async_trait; use async_trait::async_trait;
use automation_macro::LuaDevice; use automation_macro::{LuaDevice, LuaDeviceConfig};
use google_home::errors::ErrorCode; use google_home::errors::ErrorCode;
use google_home::traits::{self, OnOff}; use google_home::traits::{self, OnOff};
use google_home::types::Type; use google_home::types::Type;
use google_home::{device, GoogleHomeDevice}; use google_home::{device, GoogleHomeDevice};
use rumqttc::{matches, AsyncClient, Publish}; use rumqttc::{matches, Publish};
use serde::Deserialize; use serde::Deserialize;
use serde_with::{serde_as, DurationSeconds};
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
use tracing::{debug, error, trace, warn}; use tracing::{debug, error, trace, warn};
use crate::config::{InfoConfig, MqttDeviceConfig}; use crate::config::{InfoConfig, MqttDeviceConfig};
use crate::device_manager::{ConfigExternal, DeviceConfig}; use crate::device_manager::DeviceConfig;
use crate::devices::Device; use crate::devices::Device;
use crate::error::DeviceConfigError; use crate::error::DeviceConfigError;
use crate::event::{OnMqtt, OnPresence}; use crate::event::{OnMqtt, OnPresence};
use crate::helper::DurationSeconds;
use crate::messages::{OnOffMessage, RemoteAction, RemoteMessage}; use crate::messages::{OnOffMessage, RemoteAction, RemoteMessage};
use crate::mqtt::WrappedAsyncClient;
use crate::traits::Timeout; use crate::traits::Timeout;
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Copy)] #[derive(Debug, Clone, Deserialize, PartialEq, Eq, Copy)]
@ -29,19 +30,21 @@ pub enum OutletType {
Light, Light,
} }
#[serde_as] #[derive(Debug, Clone, LuaDeviceConfig)]
#[derive(Debug, Clone, Deserialize)]
pub struct IkeaOutletConfig { pub struct IkeaOutletConfig {
#[serde(flatten)] #[device_config(flatten)]
info: InfoConfig, info: InfoConfig,
#[serde(flatten)] #[device_config(flatten)]
mqtt: MqttDeviceConfig, mqtt: MqttDeviceConfig,
#[serde(default = "default_outlet_type")] #[device_config(default = default_outlet_type)]
outlet_type: OutletType, outlet_type: OutletType,
#[serde_as(as = "Option<DurationSeconds>")] #[device_config(with = "Option<DurationSeconds>")]
timeout: Option<Duration>, // Timeout in seconds timeout: Option<Duration>,
#[serde(default)] #[device_config(default)]
pub remotes: Vec<MqttDeviceConfig>, pub remotes: Vec<MqttDeviceConfig>,
#[device_config(user_data)]
client: WrappedAsyncClient,
} }
fn default_outlet_type() -> OutletType { fn default_outlet_type() -> OutletType {
@ -50,11 +53,7 @@ fn default_outlet_type() -> OutletType {
#[async_trait] #[async_trait]
impl DeviceConfig for IkeaOutletConfig { impl DeviceConfig for IkeaOutletConfig {
async fn create( async fn create(&self, identifier: &str) -> Result<Box<dyn Device>, DeviceConfigError> {
&self,
identifier: &str,
ext: &ConfigExternal,
) -> Result<Box<dyn Device>, DeviceConfigError> {
trace!( trace!(
id = identifier, id = identifier,
name = self.info.name, name = self.info.name,
@ -65,7 +64,6 @@ impl DeviceConfig for IkeaOutletConfig {
let device = IkeaOutlet { let device = IkeaOutlet {
identifier: identifier.into(), identifier: identifier.into(),
config: self.clone(), config: self.clone(),
client: ext.client.clone(),
last_known_state: false, last_known_state: false,
handle: None, handle: None,
}; };
@ -80,12 +78,11 @@ pub struct IkeaOutlet {
#[config] #[config]
config: IkeaOutletConfig, config: IkeaOutletConfig,
client: AsyncClient,
last_known_state: bool, last_known_state: bool,
handle: Option<JoinHandle<()>>, handle: Option<JoinHandle<()>>,
} }
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 message = OnOffMessage::new(on);
let topic = format!("{}/set", topic); let topic = format!("{}/set", topic);
@ -219,7 +216,7 @@ impl traits::OnOff for IkeaOutlet {
} }
async fn set_on(&mut self, on: bool) -> Result<(), ErrorCode> { 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(()) Ok(())
} }
@ -234,7 +231,7 @@ impl crate::traits::Timeout for IkeaOutlet {
// Turn the kettle of after the specified timeout // Turn the kettle of after the specified timeout
// TODO: Impl Drop for IkeaOutlet that will abort the handle if the IkeaOutlet // TODO: Impl Drop for IkeaOutlet that will abort the handle if the IkeaOutlet
// get dropped // get dropped
let client = self.client.clone(); let client = self.config.client.clone();
let topic = self.config.mqtt.topic.clone(); let topic = self.config.mqtt.topic.clone();
let id = self.identifier.clone(); let id = self.identifier.clone();
self.handle = Some(tokio::spawn(async move { self.handle = Some(tokio::spawn(async move {

View File

@ -2,7 +2,7 @@ use std::net::{Ipv4Addr, SocketAddr};
use std::str::Utf8Error; use std::str::Utf8Error;
use async_trait::async_trait; use async_trait::async_trait;
use automation_macro::LuaDevice; use automation_macro::{LuaDevice, LuaDeviceConfig};
use bytes::{Buf, BufMut}; use bytes::{Buf, BufMut};
use google_home::errors::{self, DeviceError}; use google_home::errors::{self, DeviceError};
use google_home::traits; use google_home::traits;
@ -13,21 +13,18 @@ use tokio::net::TcpStream;
use tracing::trace; use tracing::trace;
use super::Device; use super::Device;
use crate::device_manager::{ConfigExternal, DeviceConfig}; use crate::device_manager::DeviceConfig;
use crate::error::DeviceConfigError; use crate::error::DeviceConfigError;
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, LuaDeviceConfig)]
pub struct KasaOutletConfig { pub struct KasaOutletConfig {
// TODO: Add helper type that converts this to a socketaddr automatically
ip: Ipv4Addr, ip: Ipv4Addr,
} }
#[async_trait] #[async_trait]
impl DeviceConfig for KasaOutletConfig { impl DeviceConfig for KasaOutletConfig {
async fn create( async fn create(&self, identifier: &str) -> Result<Box<dyn Device>, DeviceConfigError> {
&self,
identifier: &str,
_ext: &ConfigExternal,
) -> Result<Box<dyn Device>, DeviceConfigError> {
trace!(id = identifier, "Setting up KasaOutlet"); trace!(id = identifier, "Setting up KasaOutlet");
let device = KasaOutlet { let device = KasaOutlet {

View File

@ -1,22 +1,23 @@
use async_trait::async_trait; use async_trait::async_trait;
use automation_macro::LuaDevice; use automation_macro::{LuaDevice, LuaDeviceConfig};
use rumqttc::Publish; use rumqttc::Publish;
use serde::Deserialize;
use tracing::{debug, trace, warn}; use tracing::{debug, trace, warn};
use crate::config::MqttDeviceConfig; use crate::config::MqttDeviceConfig;
use crate::device_manager::{ConfigExternal, DeviceConfig}; use crate::device_manager::DeviceConfig;
use crate::devices::Device; use crate::devices::Device;
use crate::error::DeviceConfigError; use crate::error::DeviceConfigError;
use crate::event::{self, Event, OnMqtt}; use crate::event::{self, Event, EventChannel, OnMqtt};
use crate::messages::BrightnessMessage; use crate::messages::BrightnessMessage;
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, LuaDeviceConfig)]
pub struct LightSensorConfig { pub struct LightSensorConfig {
#[serde(flatten)] #[device_config(flatten)]
pub mqtt: MqttDeviceConfig, pub mqtt: MqttDeviceConfig,
pub min: isize, pub min: isize,
pub max: isize, pub max: isize,
#[device_config(user_data)]
pub event_channel: EventChannel,
} }
pub const DEFAULT: bool = false; pub const DEFAULT: bool = false;
@ -25,14 +26,11 @@ pub const DEFAULT: bool = false;
#[async_trait] #[async_trait]
impl DeviceConfig for LightSensorConfig { impl DeviceConfig for LightSensorConfig {
async fn create( async fn create(&self, identifier: &str) -> Result<Box<dyn Device>, DeviceConfigError> {
&self,
identifier: &str,
ext: &ConfigExternal,
) -> Result<Box<dyn Device>, DeviceConfigError> {
let device = LightSensor { let device = LightSensor {
identifier: identifier.into(), 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(), config: self.clone(),
is_dark: DEFAULT, is_dark: DEFAULT,
}; };

View File

@ -23,7 +23,7 @@ pub use self::hue_bridge::*;
pub use self::hue_light::*; pub use self::hue_light::*;
pub use self::ikea_outlet::*; pub use self::ikea_outlet::*;
pub use self::kasa_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::ntfy::{Notification, Ntfy};
pub use self::presence::{Presence, PresenceConfig, DEFAULT_PRESENCE}; pub use self::presence::{Presence, PresenceConfig, DEFAULT_PRESENCE};
pub use self::wake_on_lan::*; pub use self::wake_on_lan::*;

View File

@ -1,31 +1,30 @@
use std::net::Ipv4Addr; use std::net::Ipv4Addr;
use async_trait::async_trait; use async_trait::async_trait;
use automation_macro::LuaDevice; use automation_macro::{LuaDevice, LuaDeviceConfig};
use eui48::MacAddress; use eui48::MacAddress;
use google_home::errors::ErrorCode; use google_home::errors::ErrorCode;
use google_home::traits::{self, Scene}; use google_home::traits::{self, Scene};
use google_home::types::Type; use google_home::types::Type;
use google_home::{device, GoogleHomeDevice}; use google_home::{device, GoogleHomeDevice};
use rumqttc::Publish; use rumqttc::Publish;
use serde::Deserialize;
use tracing::{debug, error, trace}; use tracing::{debug, error, trace};
use super::Device; use super::Device;
use crate::config::{InfoConfig, MqttDeviceConfig}; use crate::config::{InfoConfig, MqttDeviceConfig};
use crate::device_manager::{ConfigExternal, DeviceConfig}; use crate::device_manager::DeviceConfig;
use crate::error::DeviceConfigError; use crate::error::DeviceConfigError;
use crate::event::OnMqtt; use crate::event::OnMqtt;
use crate::messages::ActivateMessage; use crate::messages::ActivateMessage;
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, LuaDeviceConfig)]
pub struct WakeOnLANConfig { pub struct WakeOnLANConfig {
#[serde(flatten)] #[device_config(flatten)]
info: InfoConfig, info: InfoConfig,
#[serde(flatten)] #[device_config(flatten)]
mqtt: MqttDeviceConfig, mqtt: MqttDeviceConfig,
mac_address: MacAddress, mac_address: MacAddress,
#[serde(default = "default_broadcast_ip")] #[device_config(default = default_broadcast_ip)]
broadcast_ip: Ipv4Addr, broadcast_ip: Ipv4Addr,
} }
@ -35,11 +34,7 @@ fn default_broadcast_ip() -> Ipv4Addr {
#[async_trait] #[async_trait]
impl DeviceConfig for WakeOnLANConfig { impl DeviceConfig for WakeOnLANConfig {
async fn create( async fn create(&self, identifier: &str) -> Result<Box<dyn Device>, DeviceConfigError> {
&self,
identifier: &str,
_ext: &ConfigExternal,
) -> Result<Box<dyn Device>, DeviceConfigError> {
trace!( trace!(
id = identifier, id = identifier,
name = self.info.name, name = self.info.name,
@ -47,6 +42,8 @@ impl DeviceConfig for WakeOnLANConfig {
"Setting up WakeOnLAN" "Setting up WakeOnLAN"
); );
debug!("broadcast_ip = {}", self.broadcast_ip);
let device = WakeOnLAN { let device = WakeOnLAN {
identifier: identifier.into(), identifier: identifier.into(),
config: self.clone(), config: self.clone(),

View File

@ -1,35 +1,32 @@
use async_trait::async_trait; use async_trait::async_trait;
use automation_macro::LuaDevice; use automation_macro::{LuaDevice, LuaDeviceConfig};
use rumqttc::Publish; use rumqttc::Publish;
use serde::Deserialize;
use tracing::{debug, error, warn}; use tracing::{debug, error, warn};
use super::ntfy::Priority; use super::ntfy::Priority;
use super::{Device, Notification}; use super::{Device, Notification};
use crate::config::MqttDeviceConfig; use crate::config::MqttDeviceConfig;
use crate::device_manager::{ConfigExternal, DeviceConfig}; use crate::device_manager::DeviceConfig;
use crate::error::DeviceConfigError; use crate::error::DeviceConfigError;
use crate::event::{Event, EventChannel, OnMqtt}; use crate::event::{Event, EventChannel, OnMqtt};
use crate::messages::PowerMessage; use crate::messages::PowerMessage;
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, LuaDeviceConfig)]
pub struct WasherConfig { pub struct WasherConfig {
#[serde(flatten)] #[device_config(flatten)]
mqtt: MqttDeviceConfig, mqtt: MqttDeviceConfig,
threshold: f32, // Power in Watt // Power in Watt
threshold: f32,
#[device_config(user_data)]
event_channel: EventChannel,
} }
#[async_trait] #[async_trait]
impl DeviceConfig for WasherConfig { impl DeviceConfig for WasherConfig {
async fn create( async fn create(&self, identifier: &str) -> Result<Box<dyn Device>, DeviceConfigError> {
&self,
identifier: &str,
ext: &ConfigExternal,
) -> Result<Box<dyn Device>, DeviceConfigError> {
let device = Washer { let device = Washer {
identifier: identifier.into(), identifier: identifier.into(),
config: self.clone(), config: self.clone(),
event_channel: ext.event_channel.clone(),
running: 0, running: 0,
}; };
@ -45,7 +42,6 @@ pub struct Washer {
#[config] #[config]
config: WasherConfig, config: WasherConfig,
event_channel: EventChannel,
running: isize, running: isize,
} }
@ -94,6 +90,7 @@ impl OnMqtt for Washer {
.set_priority(Priority::High); .set_priority(Priority::High);
if self if self
.config
.event_channel .event_channel
.get_tx() .get_tx()
.send(Event::Ntfy(notification)) .send(Event::Ntfy(notification))

View File

@ -1,5 +1,6 @@
use async_trait::async_trait; use async_trait::async_trait;
use impl_cast::device_trait; use impl_cast::device_trait;
use mlua::FromLua;
use rumqttc::Publish; use rumqttc::Publish;
use tokio::sync::mpsc; use tokio::sync::mpsc;
@ -16,7 +17,7 @@ pub enum Event {
pub type Sender = mpsc::Sender<Event>; pub type Sender = mpsc::Sender<Event>;
pub type Receiver = mpsc::Receiver<Event>; pub type Receiver = mpsc::Receiver<Event>;
#[derive(Clone, Debug)] #[derive(Clone, Debug, FromLua)]
pub struct EventChannel(Sender); pub struct EventChannel(Sender);
impl EventChannel { impl EventChannel {
@ -31,6 +32,8 @@ impl EventChannel {
} }
} }
impl mlua::UserData for EventChannel {}
#[async_trait] #[async_trait]
#[device_trait] #[device_trait]
pub trait OnMqtt { pub trait OnMqtt {

12
src/helper.rs Normal file
View File

@ -0,0 +1,12 @@
use std::time::Duration;
use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub struct DurationSeconds(u64);
impl From<DurationSeconds> for Duration {
fn from(value: DurationSeconds) -> Self {
Self::from_secs(value.0)
}
}

View File

@ -7,6 +7,7 @@ pub mod device_manager;
pub mod devices; pub mod devices;
pub mod error; pub mod error;
pub mod event; pub mod event;
pub mod helper;
pub mod messages; pub mod messages;
pub mod mqtt; pub mod mqtt;
pub mod schedule; pub mod schedule;

View File

@ -9,7 +9,7 @@ use automation::devices::{
LightSensor, Ntfy, Presence, WakeOnLAN, Washer, LightSensor, Ntfy, Presence, WakeOnLAN, Washer,
}; };
use automation::error::ApiError; use automation::error::ApiError;
use automation::mqtt; use automation::mqtt::{self, WrappedAsyncClient};
use axum::extract::FromRef; use axum::extract::FromRef;
use axum::http::StatusCode; use axum::http::StatusCode;
use axum::response::IntoResponse; use axum::response::IntoResponse;
@ -84,6 +84,8 @@ async fn app() -> anyhow::Result<()> {
let automation = lua.create_table()?; let automation = lua.create_table()?;
automation.set("device_manager", device_manager.clone())?; 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 util = lua.create_table()?;
let get_env = lua.create_function(|_lua, name: String| { let get_env = lua.create_function(|_lua, name: String| {

View File

@ -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 tracing::{debug, warn};
use crate::event::{self, EventChannel}; 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) { pub fn start(mut eventloop: EventLoop, event_channel: &EventChannel) {
let tx = event_channel.get_tx(); let tx = event_channel.get_tx();