diff --git a/Cargo.toml b/Cargo.toml index fa1cb84..13c9440 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,7 +54,7 @@ rumqttc = "0.24.0" serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.143" serde_repr = "0.1.20" -syn = { version = "2.0.106", features = ["extra-traits", "full"] } +syn = { version = "2.0.106" } thiserror = "2.0.16" tokio = { version = "1", features = ["rt-multi-thread"] } tokio-cron-scheduler = "0.14.0" diff --git a/automation_devices/src/air_filter.rs b/automation_devices/src/air_filter.rs index d3593ae..7038223 100644 --- a/automation_devices/src/air_filter.rs +++ b/automation_devices/src/air_filter.rs @@ -1,7 +1,7 @@ use async_trait::async_trait; use automation_lib::config::InfoConfig; use automation_lib::device::{Device, LuaDeviceCreate}; -use automation_macro::{LuaDevice, LuaDeviceConfig}; +use automation_macro::{Device, LuaDeviceConfig}; use google_home::device::Name; use google_home::errors::ErrorCode; use google_home::traits::{ @@ -19,8 +19,8 @@ pub struct Config { pub url: String, } -#[derive(Debug, Clone, LuaDevice)] -#[traits(OnOff)] +#[derive(Debug, Clone, Device)] +#[device(traits(OnOff))] pub struct AirFilter { config: Config, } diff --git a/automation_devices/src/contact_sensor.rs b/automation_devices/src/contact_sensor.rs index b84df81..809fcc1 100644 --- a/automation_devices/src/contact_sensor.rs +++ b/automation_devices/src/contact_sensor.rs @@ -8,7 +8,7 @@ use automation_lib::error::DeviceConfigError; use automation_lib::event::OnMqtt; use automation_lib::messages::ContactMessage; use automation_lib::mqtt::WrappedAsyncClient; -use automation_macro::{LuaDevice, LuaDeviceConfig}; +use automation_macro::{Device, LuaDeviceConfig}; use google_home::device; use google_home::errors::{DeviceError, ErrorCode}; use google_home::traits::OpenClose; @@ -48,8 +48,8 @@ struct State { is_closed: bool, } -#[derive(Debug, Clone, LuaDevice)] -#[traits(OpenClose)] +#[derive(Debug, Clone, Device)] +#[device(traits(OpenClose))] pub struct ContactSensor { config: Config, state: Arc>, diff --git a/automation_devices/src/hue_bridge.rs b/automation_devices/src/hue_bridge.rs index c0e6187..3f03480 100644 --- a/automation_devices/src/hue_bridge.rs +++ b/automation_devices/src/hue_bridge.rs @@ -4,7 +4,7 @@ use std::net::SocketAddr; use async_trait::async_trait; use automation_lib::device::{Device, LuaDeviceCreate}; use automation_lib::lua::traits::AddAdditionalMethods; -use automation_macro::{LuaDevice, LuaDeviceConfig}; +use automation_macro::{Device, LuaDeviceConfig}; use mlua::LuaSerdeExt; use serde::{Deserialize, Serialize}; use tracing::{error, trace, warn}; @@ -31,8 +31,8 @@ pub struct Config { pub flags: FlagIDs, } -#[derive(Debug, Clone, LuaDevice)] -#[traits(AddAdditionalMethods)] +#[derive(Debug, Clone, Device)] +#[device(traits(AddAdditionalMethods))] pub struct HueBridge { config: Config, } diff --git a/automation_devices/src/hue_group.rs b/automation_devices/src/hue_group.rs index 836776b..8483e23 100644 --- a/automation_devices/src/hue_group.rs +++ b/automation_devices/src/hue_group.rs @@ -2,7 +2,7 @@ use std::net::SocketAddr; use anyhow::Result; use async_trait::async_trait; -use automation_macro::{LuaDevice, LuaDeviceConfig}; +use automation_macro::{Device, LuaDeviceConfig}; use google_home::errors::ErrorCode; use google_home::traits::OnOff; use tracing::{error, trace, warn}; @@ -19,8 +19,8 @@ pub struct Config { pub scene_id: String, } -#[derive(Debug, Clone, LuaDevice)] -#[traits(OnOff)] +#[derive(Debug, Clone, Device)] +#[device(traits(OnOff))] pub struct HueGroup { config: Config, } diff --git a/automation_devices/src/hue_switch.rs b/automation_devices/src/hue_switch.rs index b7a153c..e1f62cc 100644 --- a/automation_devices/src/hue_switch.rs +++ b/automation_devices/src/hue_switch.rs @@ -4,7 +4,7 @@ use automation_lib::config::{InfoConfig, MqttDeviceConfig}; use automation_lib::device::{Device, LuaDeviceCreate}; use automation_lib::event::OnMqtt; use automation_lib::mqtt::WrappedAsyncClient; -use automation_macro::{LuaDevice, LuaDeviceConfig}; +use automation_macro::{Device, LuaDeviceConfig}; use rumqttc::{Publish, matches}; use serde::Deserialize; use tracing::{debug, trace, warn}; @@ -55,7 +55,7 @@ struct State { battery: Option, } -#[derive(Debug, Clone, LuaDevice)] +#[derive(Debug, Clone, Device)] pub struct HueSwitch { config: Config, } diff --git a/automation_devices/src/ikea_remote.rs b/automation_devices/src/ikea_remote.rs index 5f8782d..b03eedb 100644 --- a/automation_devices/src/ikea_remote.rs +++ b/automation_devices/src/ikea_remote.rs @@ -5,7 +5,7 @@ use automation_lib::device::{Device, LuaDeviceCreate}; use automation_lib::event::OnMqtt; use automation_lib::messages::{RemoteAction, RemoteMessage}; use automation_lib::mqtt::WrappedAsyncClient; -use automation_macro::{LuaDevice, LuaDeviceConfig}; +use automation_macro::{Device, LuaDeviceConfig}; use rumqttc::{Publish, matches}; use tracing::{debug, error, trace}; @@ -29,7 +29,7 @@ pub struct Config { pub battery_callback: ActionCallback<(IkeaRemote, f32)>, } -#[derive(Debug, Clone, LuaDevice)] +#[derive(Debug, Clone, Device)] pub struct IkeaRemote { config: Config, } diff --git a/automation_devices/src/kasa_outlet.rs b/automation_devices/src/kasa_outlet.rs index 301c728..3e02bdf 100644 --- a/automation_devices/src/kasa_outlet.rs +++ b/automation_devices/src/kasa_outlet.rs @@ -4,7 +4,7 @@ use std::str::Utf8Error; use async_trait::async_trait; use automation_lib::device::{Device, LuaDeviceCreate}; -use automation_macro::{LuaDevice, LuaDeviceConfig}; +use automation_macro::{Device, LuaDeviceConfig}; use bytes::{Buf, BufMut}; use google_home::errors::{self, DeviceError}; use google_home::traits::OnOff; @@ -21,8 +21,8 @@ pub struct Config { pub addr: SocketAddr, } -#[derive(Debug, Clone, LuaDevice)] -#[traits(OnOff)] +#[derive(Debug, Clone, Device)] +#[device(traits(OnOff))] pub struct KasaOutlet { config: Config, } diff --git a/automation_devices/src/light_sensor.rs b/automation_devices/src/light_sensor.rs index e3cc259..4ef85ab 100644 --- a/automation_devices/src/light_sensor.rs +++ b/automation_devices/src/light_sensor.rs @@ -7,7 +7,7 @@ use automation_lib::device::{Device, LuaDeviceCreate}; use automation_lib::event::OnMqtt; use automation_lib::messages::BrightnessMessage; use automation_lib::mqtt::WrappedAsyncClient; -use automation_macro::{LuaDevice, LuaDeviceConfig}; +use automation_macro::{Device, LuaDeviceConfig}; use rumqttc::Publish; use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard}; use tracing::{debug, trace, warn}; @@ -34,7 +34,7 @@ pub struct State { is_dark: bool, } -#[derive(Debug, Clone, LuaDevice)] +#[derive(Debug, Clone, Device)] pub struct LightSensor { config: Config, state: Arc>, diff --git a/automation_devices/src/ntfy.rs b/automation_devices/src/ntfy.rs index 565969b..5e8f329 100644 --- a/automation_devices/src/ntfy.rs +++ b/automation_devices/src/ntfy.rs @@ -4,7 +4,7 @@ use std::convert::Infallible; use async_trait::async_trait; use automation_lib::device::{Device, LuaDeviceCreate}; use automation_lib::lua::traits::AddAdditionalMethods; -use automation_macro::{LuaDevice, LuaDeviceConfig}; +use automation_macro::{Device, LuaDeviceConfig}; use mlua::LuaSerdeExt; use serde::{Deserialize, Serialize}; use serde_repr::*; @@ -118,8 +118,8 @@ pub struct Config { pub topic: String, } -#[derive(Debug, Clone, LuaDevice)] -#[traits(AddAdditionalMethods)] +#[derive(Debug, Clone, Device)] +#[device(traits(AddAdditionalMethods))] pub struct Ntfy { config: Config, } diff --git a/automation_devices/src/presence.rs b/automation_devices/src/presence.rs index 77b8718..bc36324 100644 --- a/automation_devices/src/presence.rs +++ b/automation_devices/src/presence.rs @@ -9,7 +9,7 @@ use automation_lib::event::OnMqtt; use automation_lib::lua::traits::AddAdditionalMethods; use automation_lib::messages::PresenceMessage; use automation_lib::mqtt::WrappedAsyncClient; -use automation_macro::{LuaDevice, LuaDeviceConfig}; +use automation_macro::{Device, LuaDeviceConfig}; use rumqttc::Publish; use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard}; use tracing::{debug, trace, warn}; @@ -34,8 +34,8 @@ pub struct State { current_overall_presence: bool, } -#[derive(Debug, Clone, LuaDevice)] -#[traits(AddAdditionalMethods)] +#[derive(Debug, Clone, Device)] +#[device(traits(AddAdditionalMethods))] pub struct Presence { config: Config, state: Arc>, diff --git a/automation_devices/src/wake_on_lan.rs b/automation_devices/src/wake_on_lan.rs index 5eb2f00..a73c3cf 100644 --- a/automation_devices/src/wake_on_lan.rs +++ b/automation_devices/src/wake_on_lan.rs @@ -6,7 +6,7 @@ use automation_lib::device::{Device, LuaDeviceCreate}; use automation_lib::event::OnMqtt; use automation_lib::messages::ActivateMessage; use automation_lib::mqtt::WrappedAsyncClient; -use automation_macro::{LuaDevice, LuaDeviceConfig}; +use automation_macro::{Device, LuaDeviceConfig}; use eui48::MacAddress; use google_home::device; use google_home::errors::ErrorCode; @@ -28,7 +28,7 @@ pub struct Config { pub client: WrappedAsyncClient, } -#[derive(Debug, Clone, LuaDevice)] +#[derive(Debug, Clone, Device)] pub struct WakeOnLAN { config: Config, } diff --git a/automation_devices/src/washer.rs b/automation_devices/src/washer.rs index 8163844..c135b81 100644 --- a/automation_devices/src/washer.rs +++ b/automation_devices/src/washer.rs @@ -7,7 +7,7 @@ use automation_lib::device::{Device, LuaDeviceCreate}; use automation_lib::event::OnMqtt; use automation_lib::messages::PowerMessage; use automation_lib::mqtt::WrappedAsyncClient; -use automation_macro::{LuaDevice, LuaDeviceConfig}; +use automation_macro::{Device, LuaDeviceConfig}; use rumqttc::Publish; use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard}; use tracing::{debug, error, trace}; @@ -33,7 +33,7 @@ pub struct State { } // TODO: Add google home integration -#[derive(Debug, Clone, LuaDevice)] +#[derive(Debug, Clone, Device)] pub struct Washer { config: Config, state: Arc>, diff --git a/automation_devices/src/zigbee/light.rs b/automation_devices/src/zigbee/light.rs index 643c572..a41aa4e 100644 --- a/automation_devices/src/zigbee/light.rs +++ b/automation_devices/src/zigbee/light.rs @@ -10,7 +10,7 @@ use automation_lib::device::{Device, LuaDeviceCreate}; use automation_lib::event::OnMqtt; use automation_lib::helpers::serialization::state_deserializer; use automation_lib::mqtt::WrappedAsyncClient; -use automation_macro::{LuaDevice, LuaDeviceConfig, LuaSerialize}; +use automation_macro::{Device, LuaDeviceConfig, LuaSerialize}; use google_home::device; use google_home::errors::ErrorCode; use google_home::traits::{Brightness, Color, ColorSetting, ColorTemperatureRange, OnOff}; @@ -88,10 +88,10 @@ impl From for StateBrightness { } } -#[derive(Debug, Clone, LuaDevice)] -#[traits(: OnOff)] -#[traits(: OnOff, Brightness)] -#[traits(: OnOff, Brightness, ColorSetting)] +#[derive(Debug, Clone, Device)] +#[device(traits(OnOff for , , ))] +#[device(traits(Brightness for , ))] +#[device(traits(ColorSetting for ))] pub struct Light { config: Config, diff --git a/automation_devices/src/zigbee/outlet.rs b/automation_devices/src/zigbee/outlet.rs index 3006a47..2d10759 100644 --- a/automation_devices/src/zigbee/outlet.rs +++ b/automation_devices/src/zigbee/outlet.rs @@ -10,7 +10,7 @@ use automation_lib::device::{Device, LuaDeviceCreate}; use automation_lib::event::OnMqtt; use automation_lib::helpers::serialization::state_deserializer; use automation_lib::mqtt::WrappedAsyncClient; -use automation_macro::{LuaDevice, LuaDeviceConfig, LuaSerialize}; +use automation_macro::{Device, LuaDeviceConfig, LuaSerialize}; use google_home::device; use google_home::errors::ErrorCode; use google_home::traits::OnOff; @@ -80,9 +80,8 @@ impl From for StateOnOff { } } -#[derive(Debug, Clone, LuaDevice)] -#[traits(: OnOff)] -#[traits(: OnOff)] +#[derive(Debug, Clone, Device)] +#[device(traits(OnOff for , ))] pub struct Outlet { config: Config, diff --git a/automation_macro/src/device.rs b/automation_macro/src/device.rs new file mode 100644 index 0000000..6fa8014 --- /dev/null +++ b/automation_macro/src/device.rs @@ -0,0 +1,198 @@ +use std::collections::HashMap; + +use proc_macro2::TokenStream as TokenStream2; +use quote::{ToTokens, quote}; +use syn::parse::{Parse, ParseStream}; +use syn::punctuated::Punctuated; +use syn::{Attribute, DeriveInput, Token, parenthesized}; + +enum Attr { + Trait(TraitAttr), +} + +impl Parse for Attr { + fn parse(input: ParseStream) -> syn::Result { + let ident: syn::Ident = input.parse()?; + + let attr; + _ = parenthesized!(attr in input); + + let attr = match ident.to_string().as_str() { + "traits" => Attr::Trait(attr.parse()?), + _ => return Err(syn::Error::new(ident.span(), "Expected 'traits'")), + }; + + Ok(attr) + } +} + +struct TraitAttr { + traits: Traits, + generics: Generics, +} + +impl Parse for TraitAttr { + fn parse(input: ParseStream) -> syn::Result { + Ok(Self { + traits: input.parse()?, + generics: input.parse()?, + }) + } +} + +#[derive(Default)] +struct Traits(Vec); + +impl Traits { + fn extend(&mut self, other: &Traits) { + self.0.extend_from_slice(&other.0); + } +} + +impl Parse for Traits { + fn parse(input: ParseStream) -> syn::Result { + input + .call(Punctuated::<_, Token![,]>::parse_separated_nonempty) + .map(|traits| traits.into_iter().collect()) + .map(Self) + } +} + +impl ToTokens for Traits { + fn to_tokens(&self, tokens: &mut TokenStream2) { + let Self(traits) = &self; + + tokens.extend(quote! { + #( + ::automation_lib::lua::traits::#traits::add_methods(methods); + )* + }); + } +} + +#[derive(Default)] +struct Generics(Vec); + +impl Generics { + fn has_generics(&self) -> bool { + !self.0.is_empty() + } +} + +impl Parse for Generics { + fn parse(input: ParseStream) -> syn::Result { + if !input.peek(Token![for]) { + if input.is_empty() { + return Ok(Default::default()); + } else { + return Err(input.error("Expected ')' or 'for'")); + } + } + + _ = input.parse::()?; + + input + .call(Punctuated::<_, Token![,]>::parse_separated_nonempty) + .map(|generics| generics.into_iter().collect()) + .map(Self) + } +} + +struct Implementation { + generics: Option, + traits: Traits, +} + +impl From<(Option, Traits)> for Implementation { + fn from(value: (Option, Traits)) -> Self { + Self { + generics: value.0, + traits: value.1, + } + } +} + +impl quote::ToTokens for Implementation { + fn to_tokens(&self, tokens: &mut TokenStream2) { + let Self { generics, traits } = &self; + + tokens.extend(quote! { + #generics { + fn add_methods>(methods: &mut M) { + methods.add_async_function("new", async |_lua, config| { + let device: Self = LuaDeviceCreate::create(config) + .await + .map_err(mlua::ExternalError::into_lua_err)?; + + Ok(device) + }); + + methods.add_method("__box", |_lua, this, _: ()| { + let b: Box = Box::new(this.clone()); + Ok(b) + }); + + methods.add_async_method("get_id", async |_lua, this, _: ()| { Ok(this.get_id()) }); + + #traits + } + } + }); + } +} + +struct Implementations(Vec); + +impl From> for Implementations { + fn from(attributes: Vec) -> Self { + let mut all = Traits::default(); + let mut implementations: HashMap<_, Traits> = HashMap::new(); + for attribute in attributes { + match attribute { + Attr::Trait(attribute) => { + if attribute.generics.has_generics() { + for generic in &attribute.generics.0 { + implementations + .entry(Some(generic.clone())) + .or_default() + .extend(&attribute.traits); + } + } else { + all.extend(&attribute.traits); + } + } + } + } + + if implementations.is_empty() { + implementations.entry(None).or_default().extend(&all); + } else { + for traits in implementations.values_mut() { + traits.extend(&all); + } + } + + Self(implementations.into_iter().map(Into::into).collect()) + } +} + +pub fn device(input: &DeriveInput) -> TokenStream2 { + let name = &input.ident; + + let Implementations(imp) = match input + .attrs + .iter() + .filter(|attr| attr.path().is_ident("device")) + .map(Attribute::parse_args) + .try_collect::>() + { + Ok(result) => result.into(), + Err(err) => return err.into_compile_error(), + }; + + quote! { + #( + impl mlua::UserData for #name #imp + )* + } +} diff --git a/automation_macro/src/impl_device.rs b/automation_macro/src/impl_device.rs deleted file mode 100644 index 3c001b2..0000000 --- a/automation_macro/src/impl_device.rs +++ /dev/null @@ -1,88 +0,0 @@ -use proc_macro2::TokenStream; -use quote::{ToTokens, quote}; -use syn::parse::Parse; -use syn::punctuated::Punctuated; -use syn::{AngleBracketedGenericArguments, Attribute, DeriveInput, Ident, Path, Token}; - -#[derive(Debug, Default)] -struct Impl { - generics: Option, - traits: Vec, -} - -impl Parse for Impl { - fn parse(input: syn::parse::ParseStream) -> syn::Result { - let generics = if input.peek(Token![<]) { - let generics = input.parse()?; - input.parse::()?; - - Some(generics) - } else { - None - }; - - let traits: Punctuated<_, _> = input.parse_terminated(Path::parse, Token![,])?; - let traits = traits.into_iter().collect(); - - Ok(Impl { generics, traits }) - } -} - -impl Impl { - fn generate(&self, name: &Ident) -> TokenStream { - let generics = &self.generics; - - // If an identifier is specified, assume it is placed in ::automation_lib::lua::traits, - // otherwise use the provided path - let traits = self.traits.iter().map(|t| { - if let Some(ident) = t.get_ident() { - quote! {::automation_lib::lua::traits::#ident } - } else { - t.to_token_stream() - } - }); - - quote! { - impl mlua::UserData for #name #generics { - fn add_methods>(methods: &mut M) { - methods.add_async_function("new", async |_lua, config| { - let device: Self = LuaDeviceCreate::create(config) - .await - .map_err(mlua::ExternalError::into_lua_err)?; - - Ok(device) - }); - - methods.add_method("__box", |_lua, this, _: ()| { - let b: Box = Box::new(this.clone()); - Ok(b) - }); - - methods.add_async_method("get_id", async |_lua, this, _: ()| { Ok(this.get_id()) }); - - #( - #traits::add_methods(methods); - )* - } - } - } - } -} - -pub fn impl_device_macro(ast: &DeriveInput) -> TokenStream { - let name = &ast.ident; - - let impls: TokenStream = ast - .attrs - .iter() - .filter(|attr| attr.path().is_ident("traits")) - .flat_map(Attribute::parse_args::) - .map(|im| im.generate(name)) - .collect(); - - if impls.is_empty() { - Impl::default().generate(name) - } else { - impls - } -} diff --git a/automation_macro/src/lib.rs b/automation_macro/src/lib.rs index 733422c..2268de7 100644 --- a/automation_macro/src/lib.rs +++ b/automation_macro/src/lib.rs @@ -1,13 +1,12 @@ #![feature(iter_intersperse)] -mod impl_device; +#![feature(iterator_try_collect)] +mod device; mod lua_device_config; use lua_device_config::impl_lua_device_config_macro; use quote::quote; use syn::{DeriveInput, parse_macro_input}; -use crate::impl_device::impl_device_macro; - #[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); @@ -15,13 +14,6 @@ pub fn lua_device_config_derive(input: proc_macro::TokenStream) -> proc_macro::T impl_lua_device_config_macro(&ast).into() } -#[proc_macro_derive(LuaDevice, attributes(traits))] -pub fn impl_device(input: proc_macro::TokenStream) -> proc_macro::TokenStream { - let ast = parse_macro_input!(input as DeriveInput); - - impl_device_macro(&ast).into() -} - #[proc_macro_derive(LuaSerialize, attributes(traits))] pub fn lua_serialize(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let ast = parse_macro_input!(input as DeriveInput); @@ -37,3 +29,31 @@ pub fn lua_serialize(input: proc_macro::TokenStream) -> proc_macro::TokenStream } .into() } + +/// Derive macro generating an impl for the trait `::mlua::UserData` +/// +/// The `device(traits)` attribute can be used to tell the macro what traits are implemented so that +/// the appropriate methods can automatically be registered. +/// If the struct does not have any type parameters the syntax is very simple: +/// ``` +/// #[device(traits(TraitA, TraitB))] +/// ``` +/// +/// If the type does have type parameters you will have to manually specify all variations that +/// have the trait available: +/// ``` +/// #[device(traits(TraitA, TraitB for , ))] +/// ``` +/// If multiple of these attributes are specified they will all combined appropriately. +/// +/// +/// # NOTE +/// If your type _has_ type parameters any instance of the traits attribute that does not specify +/// any type parameters will have the traits applied to _all_ other type parameter variations +/// listed in the other trait attributes. This behavior only applies if there is at least one +/// instance with type parameters specified. +#[proc_macro_derive(Device, attributes(device))] +pub fn device(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let ast = parse_macro_input!(input as DeriveInput); + device::device(&ast).into() +} diff --git a/src/main.rs b/src/main.rs index 4eb3bb9..405a255 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,7 @@ mod web; use std::net::SocketAddr; use std::path::Path; use std::process; -use std::time::{SystemTime, UNIX_EPOCH}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; use ::config::{Environment, File}; use automation_lib::config::{FulfillmentConfig, MqttConfig}; @@ -172,6 +172,11 @@ async fn app() -> anyhow::Result<()> { .as_millis()) })?; utils.set("get_epoch", get_epoch)?; + let sleep = lua.create_async_function(async |_lua, duration: u64| { + tokio::time::sleep(Duration::from_millis(duration)).await; + Ok(()) + })?; + utils.set("sleep", sleep)?; lua.register_module("utils", utils)?; automation_devices::register_with_lua(&lua)?;