Compare commits

...

6 Commits

Author SHA1 Message Date
f7ba602762 chore: Removed dotenvy
All checks were successful
Build and deploy / build (push) Successful in 10m21s
Build and deploy / Deploy container (push) Has been skipped
Since secrets can now be set from automation.toml the .env file was no
longer used, so dotenvy can be removed.
2025-10-15 01:18:09 +02:00
929007d8c2 feat: Use Typed type_name for registering proxy 2025-10-15 01:01:10 +02:00
b191d67ae2 feat: Expanded add_methods to extra_user_data
All checks were successful
Build and deploy / build (push) Successful in 10m29s
Build and deploy / Deploy container (push) Has been skipped
Instead of being a function it now expects a struct with the
PartialUserData trait implemented. This in part ensures the correct
function signature.

It also adds another optional function to PartialUserData that returns
definitions for the added methods.
diff --git a/automation_devices/src/hue_bridge.rs b/automation_devices/src/hue_bridge.rs
index b08ab51..0c548c8 100644
--- a/automation_devices/src/hue_bridge.rs
+++ b/automation_devices/src/hue_bridge.rs
@@ -3,18 +3,21 @@ use std::net::SocketAddr;

 use async_trait::async_trait;
 use automation_lib::device::{Device, LuaDeviceCreate};
+use automation_lib::lua::traits::PartialUserData;
 use automation_macro::{Device, LuaDeviceConfig};
 use lua_typed::Typed;
 use mlua::LuaSerdeExt;
 use serde::{Deserialize, Serialize};
 use tracing::{error, trace, warn};

-#[derive(Debug, Deserialize)]
+#[derive(Debug, Deserialize, Typed)]
 #[serde(rename_all = "snake_case")]
+#[typed(rename_all = "snake_case")]
 pub enum Flag {
     Presence,
     Darkness,
 }
+crate::register_type!(Flag);

 #[derive(Debug, Clone, Deserialize, Typed)]
 pub struct FlagIDs {
@@ -36,12 +39,36 @@ pub struct Config {
 crate::register_type!(Config);

 #[derive(Debug, Clone, Device)]
-#[device(add_methods = Self::add_methods)]
+#[device(extra_user_data = SetFlag)]
 pub struct HueBridge {
     config: Config,
 }
 crate::register_device!(HueBridge);

+struct SetFlag;
+impl PartialUserData<HueBridge> for SetFlag {
+    fn add_methods<M: mlua::UserDataMethods<HueBridge>>(methods: &mut M) {
+        methods.add_async_method(
+            "set_flag",
+            async |lua, this, (flag, value): (mlua::Value, bool)| {
+                let flag: Flag = lua.from_value(flag)?;
+
+                this.set_flag(flag, value).await;
+
+                Ok(())
+            },
+        );
+    }
+
+    fn definitions() -> Option<String> {
+        Some(format!(
+            "---@async\n---@param flag {}\n---@param value boolean\nfunction {}:set_flag(flag, value) end\n",
+            <Flag as Typed>::type_name(),
+            <HueBridge as Typed>::type_name(),
+        ))
+    }
+}
+
 #[derive(Debug, Serialize)]
 struct FlagMessage {
     flag: bool,
@@ -89,19 +116,6 @@ impl HueBridge {
             }
         }
     }
-
-    fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
-        methods.add_async_method(
-            "set_flag",
-            async |lua, this, (flag, value): (mlua::Value, bool)| {
-                let flag: Flag = lua.from_value(flag)?;
-
-                this.set_flag(flag, value).await;
-
-                Ok(())
-            },
-        );
-    }
 }

 impl Device for HueBridge {
diff --git a/automation_devices/src/ntfy.rs b/automation_devices/src/ntfy.rs
index 8060ced..1be2874 100644
--- a/automation_devices/src/ntfy.rs
+++ b/automation_devices/src/ntfy.rs
@@ -3,6 +3,7 @@ use std::convert::Infallible;

 use async_trait::async_trait;
 use automation_lib::device::{Device, LuaDeviceCreate};
+use automation_lib::lua::traits::PartialUserData;
 use automation_macro::{Device, LuaDeviceConfig};
 use lua_typed::Typed;
 use mlua::LuaSerdeExt;
@@ -90,14 +91,15 @@ pub struct Config {
 crate::register_type!(Config);

 #[derive(Debug, Clone, Device)]
-#[device(add_methods = Self::add_methods)]
+#[device(extra_user_data = SendNotification)]
 pub struct Ntfy {
     config: Config,
 }
 crate::register_device!(Ntfy);

-impl Ntfy {
-    fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
+struct SendNotification;
+impl PartialUserData<Ntfy> for SendNotification {
+    fn add_methods<M: mlua::UserDataMethods<Ntfy>>(methods: &mut M) {
         methods.add_async_method(
             "send_notification",
             async |lua, this, notification: mlua::Value| {
@@ -109,6 +111,14 @@ impl Ntfy {
             },
         );
     }
+
+    fn definitions() -> Option<String> {
+        Some(format!(
+            "---@async\n---@param notification {}\nfunction {}:send_notification(notification) end\n",
+            <Notification as Typed>::type_name(),
+            <Ntfy as Typed>::type_name(),
+        ))
+    }
 }

 #[async_trait]
diff --git a/automation_devices/src/presence.rs b/automation_devices/src/presence.rs
index 72391ab..a77327c 100644
--- a/automation_devices/src/presence.rs
+++ b/automation_devices/src/presence.rs
@@ -6,6 +6,7 @@ use automation_lib::action_callback::ActionCallback;
 use automation_lib::config::MqttDeviceConfig;
 use automation_lib::device::{Device, LuaDeviceCreate};
 use automation_lib::event::OnMqtt;
+use automation_lib::lua::traits::PartialUserData;
 use automation_lib::messages::PresenceMessage;
 use automation_lib::mqtt::WrappedAsyncClient;
 use automation_macro::{Device, LuaDeviceConfig};
@@ -39,13 +40,29 @@ pub struct State {
 }

 #[derive(Debug, Clone, Device)]
-#[device(add_methods = Self::add_methods)]
+#[device(extra_user_data = OverallPresence)]
 pub struct Presence {
     config: Config,
     state: Arc<RwLock<State>>,
 }
 crate::register_device!(Presence);

+struct OverallPresence;
+impl PartialUserData<Presence> for OverallPresence {
+    fn add_methods<M: mlua::UserDataMethods<Presence>>(methods: &mut M) {
+        methods.add_async_method("overall_presence", async |_lua, this, ()| {
+            Ok(this.state().await.current_overall_presence)
+        });
+    }
+
+    fn definitions() -> Option<String> {
+        Some(format!(
+            "---@async\n---@return boolean\nfunction {}:overall_presence() end\n",
+            <Presence as Typed>::type_name(),
+        ))
+    }
+}
+
 impl Presence {
     async fn state(&self) -> RwLockReadGuard<'_, State> {
         self.state.read().await
@@ -54,12 +71,6 @@ impl Presence {
     async fn state_mut(&self) -> RwLockWriteGuard<'_, State> {
         self.state.write().await
     }
-
-    fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
-        methods.add_async_method("overall_presence", async |_lua, this, ()| {
-            Ok(this.state().await.current_overall_presence)
-        });
-    }
 }

 #[async_trait]
diff --git a/automation_lib/src/lua/traits.rs b/automation_lib/src/lua/traits.rs
index 6f61841..adae7df 100644
--- a/automation_lib/src/lua/traits.rs
+++ b/automation_lib/src/lua/traits.rs
@@ -8,6 +8,10 @@ pub trait PartialUserData<T> {
     fn interface_name() -> Option<&'static str> {
         None
     }
+
+    fn definitions() -> Option<String> {
+        None
+    }
 }

 pub struct Device;
diff --git a/automation_macro/src/device.rs b/automation_macro/src/device.rs
index 874765f..d66e0bd 100644
--- a/automation_macro/src/device.rs
+++ b/automation_macro/src/device.rs
@@ -1,7 +1,7 @@
 use std::collections::HashMap;

 use proc_macro2::TokenStream as TokenStream2;
-use quote::{ToTokens, quote};
+use quote::quote;
 use syn::parse::{Parse, ParseStream};
 use syn::punctuated::Punctuated;
 use syn::spanned::Spanned;
@@ -9,7 +9,7 @@ use syn::{Attribute, DeriveInput, Token, parenthesized};

 enum Attr {
     Trait(TraitAttr),
-    AddMethods(AddMethodsAttr),
+    ExtraUserData(ExtraUserDataAttr),
 }

 impl Attr {
@@ -20,9 +20,9 @@ impl Attr {
                 let input;
                 _ = parenthesized!(input in meta.input);
                 parsed = Some(Attr::Trait(input.parse()?));
-            } else if meta.path.is_ident("add_methods") {
+            } else if meta.path.is_ident("extra_user_data") {
                 let value = meta.value()?;
-                parsed = Some(Attr::AddMethods(value.parse()?));
+                parsed = Some(Attr::ExtraUserData(value.parse()?));
             } else {
                 return Err(syn::Error::new(meta.path.span(), "Unknown attribute"));
             }
@@ -95,28 +95,18 @@ impl Parse for Aliases {
 }

 #[derive(Clone)]
-struct AddMethodsAttr(syn::Path);
+struct ExtraUserDataAttr(syn::Ident);

-impl Parse for AddMethodsAttr {
+impl Parse for ExtraUserDataAttr {
     fn parse(input: ParseStream) -> syn::Result<Self> {
         Ok(Self(input.parse()?))
     }
 }

-impl ToTokens for AddMethodsAttr {
-    fn to_tokens(&self, tokens: &mut TokenStream2) {
-        let Self(path) = self;
-
-        tokens.extend(quote! {
-            #path
-        });
-    }
-}
-
 struct Implementation {
     name: syn::Ident,
     traits: Traits,
-    add_methods: Vec<AddMethodsAttr>,
+    extra_user_data: Vec<ExtraUserDataAttr>,
 }

 impl quote::ToTokens for Implementation {
@@ -124,9 +114,10 @@ impl quote::ToTokens for Implementation {
         let Self {
             name,
             traits,
-            add_methods,
+            extra_user_data,
         } = &self;
         let Traits(traits) = traits;
+        let extra_user_data: Vec<_> = extra_user_data.iter().map(|tr| tr.0.clone()).collect();

         tokens.extend(quote! {
             impl mlua::UserData for #name {
@@ -151,7 +142,7 @@ impl quote::ToTokens for Implementation {
                     )*

                     #(
-                        #add_methods(methods);
+                        <#extra_user_data as ::automation_lib::lua::traits::PartialUserData<#name>>::add_methods(methods);
                     )*
                 }
             }
@@ -178,7 +169,7 @@ impl quote::ToTokens for Implementation {
                         format!(": {interfaces}")
                     };

-                    Some(format!("---@class {type_name}{interfaces}\nlocal {type_name}"))
+                    Some(format!("---@class {type_name}{interfaces}\nlocal {type_name}\n"))
                 }

                 fn generate_members() -> Option<String> {
@@ -191,6 +182,15 @@ impl quote::ToTokens for Implementation {
                     output += &format!("---@return {type_name}\n");
                     output += &format!("function devices.{type_name}.new(config) end\n");

+                    output += &<::automation_lib::lua::traits::Device as ::automation_lib::lua::traits::PartialUserData<#name>>::definitions().unwrap_or("".into());
+
+                    #(
+                        output += &<::automation_lib::lua::traits::#traits as ::automation_lib::lua::traits::PartialUserData<#name>>::definitions().unwrap_or("".into());
+                    )*
+                    #(
+                        output += &<#extra_user_data as ::automation_lib::lua::traits::PartialUserData<#name>>::definitions().unwrap_or("".into());
+                    )*
+

                     Some(output)
                 }
@@ -220,7 +220,7 @@ impl Implementations {
                         all.extend(&attribute.traits);
                     }
                 }
-                Attr::AddMethods(attribute) => add_methods.push(attribute),
+                Attr::ExtraUserData(attribute) => add_methods.push(attribute),
             }
         }

@@ -238,7 +238,7 @@ impl Implementations {
                 .map(|(alias, traits)| Implementation {
                     name: alias.unwrap_or(name.clone()),
                     traits,
-                    add_methods: add_methods.clone(),
+                    extra_user_data: add_methods.clone(),
                 })
                 .collect(),
         )
2025-10-15 00:45:38 +02:00
9bbf0a5422 feat: Specify (optional) interface name in PartialUserData 2025-10-15 00:45:38 +02:00
85e3c7b877 feat: Use PartialUserData on proxy type to add trait methods 2025-10-15 00:45:38 +02:00
a2130005de feat: Improved attribute parsing in device macro 2025-10-15 00:45:37 +02:00
11 changed files with 225 additions and 158 deletions

7
Cargo.lock generated
View File

@@ -96,7 +96,6 @@ dependencies = [
"automation_lib", "automation_lib",
"axum", "axum",
"config", "config",
"dotenvy",
"git-version", "git-version",
"google_home", "google_home",
"mlua", "mlua",
@@ -433,12 +432,6 @@ dependencies = [
"syn 2.0.106", "syn 2.0.106",
] ]
[[package]]
name = "dotenvy"
version = "0.15.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
[[package]] [[package]]
name = "dyn-clone" name = "dyn-clone"
version = "1.0.20" version = "1.0.20"

View File

@@ -23,7 +23,6 @@ automation_lib = { path = "./automation_lib" }
automation_macro = { path = "./automation_macro" } automation_macro = { path = "./automation_macro" }
axum = "0.8.4" axum = "0.8.4"
bytes = "1.10.1" bytes = "1.10.1"
dotenvy = "0.15.7"
dyn-clone = "1.0.20" dyn-clone = "1.0.20"
eui48 = { version = "1.1.0", features = [ eui48 = { version = "1.1.0", features = [
"disp_hexstring", "disp_hexstring",
@@ -75,7 +74,6 @@ config = { version = "0.15.15", default-features = false, features = [
"async", "async",
"toml", "toml",
] } ] }
dotenvy = { workspace = true }
git-version = "0.3.9" git-version = "0.3.9"
google_home = { workspace = true } google_home = { workspace = true }
mlua = { workspace = true } mlua = { workspace = true }

View File

@@ -3,18 +3,21 @@ use std::net::SocketAddr;
use async_trait::async_trait; use async_trait::async_trait;
use automation_lib::device::{Device, LuaDeviceCreate}; use automation_lib::device::{Device, LuaDeviceCreate};
use automation_lib::lua::traits::PartialUserData;
use automation_macro::{Device, LuaDeviceConfig}; use automation_macro::{Device, LuaDeviceConfig};
use lua_typed::Typed; use lua_typed::Typed;
use mlua::LuaSerdeExt; use mlua::LuaSerdeExt;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tracing::{error, trace, warn}; use tracing::{error, trace, warn};
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize, Typed)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
#[typed(rename_all = "snake_case")]
pub enum Flag { pub enum Flag {
Presence, Presence,
Darkness, Darkness,
} }
crate::register_type!(Flag);
#[derive(Debug, Clone, Deserialize, Typed)] #[derive(Debug, Clone, Deserialize, Typed)]
pub struct FlagIDs { pub struct FlagIDs {
@@ -36,12 +39,36 @@ pub struct Config {
crate::register_type!(Config); crate::register_type!(Config);
#[derive(Debug, Clone, Device)] #[derive(Debug, Clone, Device)]
#[device(add_methods(Self::add_methods))] #[device(extra_user_data = SetFlag)]
pub struct HueBridge { pub struct HueBridge {
config: Config, config: Config,
} }
crate::register_device!(HueBridge); crate::register_device!(HueBridge);
struct SetFlag;
impl PartialUserData<HueBridge> for SetFlag {
fn add_methods<M: mlua::UserDataMethods<HueBridge>>(methods: &mut M) {
methods.add_async_method(
"set_flag",
async |lua, this, (flag, value): (mlua::Value, bool)| {
let flag: Flag = lua.from_value(flag)?;
this.set_flag(flag, value).await;
Ok(())
},
);
}
fn definitions() -> Option<String> {
Some(format!(
"---@async\n---@param flag {}\n---@param value boolean\nfunction {}:set_flag(flag, value) end\n",
<Flag as Typed>::type_name(),
<HueBridge as Typed>::type_name(),
))
}
}
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
struct FlagMessage { struct FlagMessage {
flag: bool, flag: bool,
@@ -89,19 +116,6 @@ impl HueBridge {
} }
} }
} }
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
methods.add_async_method(
"set_flag",
async |lua, this, (flag, value): (mlua::Value, bool)| {
let flag: Flag = lua.from_value(flag)?;
this.set_flag(flag, value).await;
Ok(())
},
);
}
} }
impl Device for HueBridge { impl Device for HueBridge {

View File

@@ -1,3 +1,4 @@
#![feature(iter_intersperse)]
mod air_filter; mod air_filter;
mod contact_sensor; mod contact_sensor;
mod hue_bridge; mod hue_bridge;
@@ -19,7 +20,7 @@ use tracing::debug;
macro_rules! register_device { macro_rules! register_device {
($device:ty) => { ($device:ty) => {
::inventory::submit!(crate::RegisteredDevice::new( ::inventory::submit!(crate::RegisteredDevice::new(
stringify!($device), <$device as ::lua_typed::Typed>::type_name,
::mlua::Lua::create_proxy::<$device> ::mlua::Lua::create_proxy::<$device>
)); ));
@@ -29,20 +30,24 @@ macro_rules! register_device {
pub(crate) use register_device; pub(crate) use register_device;
type DeviceNameFn = fn() -> String;
type RegisterDeviceFn = fn(lua: &mlua::Lua) -> mlua::Result<mlua::AnyUserData>; type RegisterDeviceFn = fn(lua: &mlua::Lua) -> mlua::Result<mlua::AnyUserData>;
pub struct RegisteredDevice { pub struct RegisteredDevice {
name: &'static str, name_fn: DeviceNameFn,
register_fn: RegisterDeviceFn, register_fn: RegisterDeviceFn,
} }
impl RegisteredDevice { impl RegisteredDevice {
pub const fn new(name: &'static str, register_fn: RegisterDeviceFn) -> Self { pub const fn new(name_fn: DeviceNameFn, register_fn: RegisterDeviceFn) -> Self {
Self { name, register_fn } Self {
name_fn,
register_fn,
}
} }
pub const fn get_name(&self) -> &'static str { pub fn get_name(&self) -> String {
self.name (self.name_fn)()
} }
pub fn register(&self, lua: &mlua::Lua) -> mlua::Result<mlua::AnyUserData> { pub fn register(&self, lua: &mlua::Lua) -> mlua::Result<mlua::AnyUserData> {
@@ -57,9 +62,10 @@ pub fn create_module(lua: &mlua::Lua) -> mlua::Result<mlua::Table> {
debug!("Loading devices..."); debug!("Loading devices...");
for device in inventory::iter::<RegisteredDevice> { for device in inventory::iter::<RegisteredDevice> {
debug!(name = device.get_name(), "Registering device"); let name = device.get_name();
debug!(name, "Registering device");
let proxy = device.register(lua)?; let proxy = device.register(lua)?;
devices.set(device.get_name(), proxy)?; devices.set(name, proxy)?;
} }
Ok(devices) Ok(devices)

View File

@@ -3,6 +3,7 @@ use std::convert::Infallible;
use async_trait::async_trait; use async_trait::async_trait;
use automation_lib::device::{Device, LuaDeviceCreate}; use automation_lib::device::{Device, LuaDeviceCreate};
use automation_lib::lua::traits::PartialUserData;
use automation_macro::{Device, LuaDeviceConfig}; use automation_macro::{Device, LuaDeviceConfig};
use lua_typed::Typed; use lua_typed::Typed;
use mlua::LuaSerdeExt; use mlua::LuaSerdeExt;
@@ -90,14 +91,15 @@ pub struct Config {
crate::register_type!(Config); crate::register_type!(Config);
#[derive(Debug, Clone, Device)] #[derive(Debug, Clone, Device)]
#[device(add_methods(Self::add_methods))] #[device(extra_user_data = SendNotification)]
pub struct Ntfy { pub struct Ntfy {
config: Config, config: Config,
} }
crate::register_device!(Ntfy); crate::register_device!(Ntfy);
impl Ntfy { struct SendNotification;
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) { impl PartialUserData<Ntfy> for SendNotification {
fn add_methods<M: mlua::UserDataMethods<Ntfy>>(methods: &mut M) {
methods.add_async_method( methods.add_async_method(
"send_notification", "send_notification",
async |lua, this, notification: mlua::Value| { async |lua, this, notification: mlua::Value| {
@@ -109,6 +111,14 @@ impl Ntfy {
}, },
); );
} }
fn definitions() -> Option<String> {
Some(format!(
"---@async\n---@param notification {}\nfunction {}:send_notification(notification) end\n",
<Notification as Typed>::type_name(),
<Ntfy as Typed>::type_name(),
))
}
} }
#[async_trait] #[async_trait]

View File

@@ -6,6 +6,7 @@ use automation_lib::action_callback::ActionCallback;
use automation_lib::config::MqttDeviceConfig; use automation_lib::config::MqttDeviceConfig;
use automation_lib::device::{Device, LuaDeviceCreate}; use automation_lib::device::{Device, LuaDeviceCreate};
use automation_lib::event::OnMqtt; use automation_lib::event::OnMqtt;
use automation_lib::lua::traits::PartialUserData;
use automation_lib::messages::PresenceMessage; use automation_lib::messages::PresenceMessage;
use automation_lib::mqtt::WrappedAsyncClient; use automation_lib::mqtt::WrappedAsyncClient;
use automation_macro::{Device, LuaDeviceConfig}; use automation_macro::{Device, LuaDeviceConfig};
@@ -39,13 +40,29 @@ pub struct State {
} }
#[derive(Debug, Clone, Device)] #[derive(Debug, Clone, Device)]
#[device(add_methods(Self::add_methods))] #[device(extra_user_data = OverallPresence)]
pub struct Presence { pub struct Presence {
config: Config, config: Config,
state: Arc<RwLock<State>>, state: Arc<RwLock<State>>,
} }
crate::register_device!(Presence); crate::register_device!(Presence);
struct OverallPresence;
impl PartialUserData<Presence> for OverallPresence {
fn add_methods<M: mlua::UserDataMethods<Presence>>(methods: &mut M) {
methods.add_async_method("overall_presence", async |_lua, this, ()| {
Ok(this.state().await.current_overall_presence)
});
}
fn definitions() -> Option<String> {
Some(format!(
"---@async\n---@return boolean\nfunction {}:overall_presence() end\n",
<Presence as Typed>::type_name(),
))
}
}
impl Presence { impl Presence {
async fn state(&self) -> RwLockReadGuard<'_, State> { async fn state(&self) -> RwLockReadGuard<'_, State> {
self.state.read().await self.state.read().await
@@ -54,12 +71,6 @@ impl Presence {
async fn state_mut(&self) -> RwLockWriteGuard<'_, State> { async fn state_mut(&self) -> RwLockWriteGuard<'_, State> {
self.state.write().await self.state.write().await
} }
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
methods.add_async_method("overall_presence", async |_lua, this, ()| {
Ok(this.state().await.current_overall_presence)
});
}
} }
#[async_trait] #[async_trait]

View File

@@ -2,21 +2,40 @@ use std::ops::Deref;
// TODO: Enable and disable functions based on query_only and command_only // TODO: Enable and disable functions based on query_only and command_only
pub trait Device { pub trait PartialUserData<T> {
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) fn add_methods<M: mlua::UserDataMethods<T>>(methods: &mut M);
fn interface_name() -> Option<&'static str> {
None
}
fn definitions() -> Option<String> {
None
}
}
pub struct Device;
impl<T> PartialUserData<T> for Device
where where
Self: Sized + crate::device::Device + 'static, T: crate::device::Device + 'static,
{ {
fn add_methods<M: mlua::UserDataMethods<T>>(methods: &mut M) {
methods.add_async_method("get_id", async |_lua, this, _: ()| Ok(this.get_id())); methods.add_async_method("get_id", async |_lua, this, _: ()| Ok(this.get_id()));
} }
}
impl<T> Device for T where T: crate::device::Device {}
pub trait OnOff { fn interface_name() -> Option<&'static str> {
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) Some("DeviceInterface")
}
}
pub struct OnOff;
impl<T> PartialUserData<T> for OnOff
where where
Self: Sized + google_home::traits::OnOff + 'static, T: google_home::traits::OnOff + 'static,
{ {
fn add_methods<M: mlua::UserDataMethods<T>>(methods: &mut M) {
methods.add_async_method("set_on", async |_lua, this, on: bool| { methods.add_async_method("set_on", async |_lua, this, on: bool| {
this.deref().set_on(on).await.unwrap(); this.deref().set_on(on).await.unwrap();
@@ -27,14 +46,19 @@ pub trait OnOff {
Ok(this.deref().on().await.unwrap()) Ok(this.deref().on().await.unwrap())
}); });
} }
}
impl<T> OnOff for T where T: google_home::traits::OnOff {}
pub trait Brightness { fn interface_name() -> Option<&'static str> {
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) Some("OnOffInterface")
}
}
pub struct Brightness;
impl<T> PartialUserData<T> for Brightness
where where
Self: Sized + google_home::traits::Brightness + 'static, T: google_home::traits::Brightness + 'static,
{ {
fn add_methods<M: mlua::UserDataMethods<T>>(methods: &mut M) {
methods.add_async_method("set_brightness", async |_lua, this, brightness: u8| { methods.add_async_method("set_brightness", async |_lua, this, brightness: u8| {
this.set_brightness(brightness).await.unwrap(); this.set_brightness(brightness).await.unwrap();
@@ -45,14 +69,19 @@ pub trait Brightness {
Ok(this.brightness().await.unwrap()) Ok(this.brightness().await.unwrap())
}); });
} }
}
impl<T> Brightness for T where T: google_home::traits::Brightness {}
pub trait ColorSetting { fn interface_name() -> Option<&'static str> {
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) Some("BrightnessInterface")
}
}
pub struct ColorSetting;
impl<T> PartialUserData<T> for ColorSetting
where where
Self: Sized + google_home::traits::ColorSetting + 'static, T: google_home::traits::ColorSetting + 'static,
{ {
fn add_methods<M: mlua::UserDataMethods<T>>(methods: &mut M) {
methods.add_async_method( methods.add_async_method(
"set_color_temperature", "set_color_temperature",
async |_lua, this, temperature: u32| { async |_lua, this, temperature: u32| {
@@ -68,14 +97,19 @@ pub trait ColorSetting {
Ok(this.color().await.temperature) Ok(this.color().await.temperature)
}); });
} }
}
impl<T> ColorSetting for T where T: google_home::traits::ColorSetting {}
pub trait OpenClose { fn interface_name() -> Option<&'static str> {
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) Some("ColorSettingInterface")
}
}
pub struct OpenClose;
impl<T> PartialUserData<T> for OpenClose
where where
Self: Sized + google_home::traits::OpenClose + 'static, T: google_home::traits::OpenClose + 'static,
{ {
fn add_methods<M: mlua::UserDataMethods<T>>(methods: &mut M) {
methods.add_async_method("set_open_percent", async |_lua, this, open_percent: u8| { methods.add_async_method("set_open_percent", async |_lua, this, open_percent: u8| {
this.set_open_percent(open_percent).await.unwrap(); this.set_open_percent(open_percent).await.unwrap();
@@ -86,5 +120,8 @@ pub trait OpenClose {
Ok(this.open_percent().await.unwrap()) Ok(this.open_percent().await.unwrap())
}); });
} }
fn interface_name() -> Option<&'static str> {
Some("OpenCloseInterface")
}
} }
impl<T> OpenClose for T where T: google_home::traits::OpenClose {}

View File

@@ -1,35 +1,36 @@
use std::collections::HashMap; use std::collections::HashMap;
use proc_macro2::TokenStream as TokenStream2; use proc_macro2::TokenStream as TokenStream2;
use quote::{ToTokens, quote}; use quote::quote;
use syn::parse::{Parse, ParseStream}; use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated; use syn::punctuated::Punctuated;
use syn::spanned::Spanned;
use syn::{Attribute, DeriveInput, Token, parenthesized}; use syn::{Attribute, DeriveInput, Token, parenthesized};
enum Attr { enum Attr {
Trait(TraitAttr), Trait(TraitAttr),
AddMethods(AddMethodsAttr), ExtraUserData(ExtraUserDataAttr),
} }
impl Parse for Attr { impl Attr {
fn parse(input: ParseStream) -> syn::Result<Self> { fn parse(attr: &Attribute) -> syn::Result<Self> {
let ident: syn::Ident = input.parse()?; let mut parsed = None;
attr.parse_nested_meta(|meta| {
let attr; if meta.path.is_ident("traits") {
_ = parenthesized!(attr in input); let input;
_ = parenthesized!(input in meta.input);
let attr = match ident.to_string().as_str() { parsed = Some(Attr::Trait(input.parse()?));
"traits" => Attr::Trait(attr.parse()?), } else if meta.path.is_ident("extra_user_data") {
"add_methods" => Attr::AddMethods(attr.parse()?), let value = meta.value()?;
_ => { parsed = Some(Attr::ExtraUserData(value.parse()?));
return Err(syn::Error::new( } else {
ident.span(), return Err(syn::Error::new(meta.path.span(), "Unknown attribute"));
"Expected 'traits' or 'add_methods'",
));
} }
};
Ok(attr) Ok(())
})?;
Ok(parsed.expect("Parsed should be set"))
} }
} }
@@ -65,18 +66,6 @@ impl Parse for Traits {
} }
} }
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)] #[derive(Default)]
struct Aliases(Vec<syn::Ident>); struct Aliases(Vec<syn::Ident>);
@@ -106,28 +95,18 @@ impl Parse for Aliases {
} }
#[derive(Clone)] #[derive(Clone)]
struct AddMethodsAttr(syn::Path); struct ExtraUserDataAttr(syn::Ident);
impl Parse for AddMethodsAttr { impl Parse for ExtraUserDataAttr {
fn parse(input: ParseStream) -> syn::Result<Self> { fn parse(input: ParseStream) -> syn::Result<Self> {
Ok(Self(input.parse()?)) Ok(Self(input.parse()?))
} }
} }
impl ToTokens for AddMethodsAttr {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let Self(path) = self;
tokens.extend(quote! {
#path
});
}
}
struct Implementation { struct Implementation {
name: syn::Ident, name: syn::Ident,
traits: Traits, traits: Traits,
add_methods: Vec<AddMethodsAttr>, extra_user_data: Vec<ExtraUserDataAttr>,
} }
impl quote::ToTokens for Implementation { impl quote::ToTokens for Implementation {
@@ -135,14 +114,10 @@ impl quote::ToTokens for Implementation {
let Self { let Self {
name, name,
traits, traits,
add_methods, extra_user_data,
} = &self; } = &self;
let Traits(traits) = traits;
let interfaces: String = traits let extra_user_data: Vec<_> = extra_user_data.iter().map(|tr| tr.0.clone()).collect();
.0
.iter()
.map(|tr| format!(", Interface{tr}"))
.collect();
tokens.extend(quote! { tokens.extend(quote! {
impl mlua::UserData for #name { impl mlua::UserData for #name {
@@ -160,12 +135,14 @@ impl quote::ToTokens for Implementation {
Ok(b) Ok(b)
}); });
::automation_lib::lua::traits::Device::add_methods(methods); <::automation_lib::lua::traits::Device as ::automation_lib::lua::traits::PartialUserData<#name>>::add_methods(methods);
#traits
#( #(
#add_methods(methods); <::automation_lib::lua::traits::#traits as ::automation_lib::lua::traits::PartialUserData<#name>>::add_methods(methods);
)*
#(
<#extra_user_data as ::automation_lib::lua::traits::PartialUserData<#name>>::add_methods(methods);
)* )*
} }
} }
@@ -177,7 +154,22 @@ impl quote::ToTokens for Implementation {
fn generate_header() -> std::option::Option<::std::string::String> { fn generate_header() -> std::option::Option<::std::string::String> {
let type_name = <Self as ::lua_typed::Typed>::type_name(); let type_name = <Self as ::lua_typed::Typed>::type_name();
Some(format!("---@class {type_name}: InterfaceDevice{}\nlocal {type_name}\n", #interfaces)) let mut output = String::new();
let interfaces: String = [
<::automation_lib::lua::traits::Device as ::automation_lib::lua::traits::PartialUserData<#name>>::interface_name(),
#(
<::automation_lib::lua::traits::#traits as ::automation_lib::lua::traits::PartialUserData<#name>>::interface_name(),
)*
].into_iter().flatten().intersperse(", ").collect();
let interfaces = if interfaces.is_empty() {
"".into()
} else {
format!(": {interfaces}")
};
Some(format!("---@class {type_name}{interfaces}\nlocal {type_name}\n"))
} }
fn generate_members() -> Option<String> { fn generate_members() -> Option<String> {
@@ -190,6 +182,15 @@ impl quote::ToTokens for Implementation {
output += &format!("---@return {type_name}\n"); output += &format!("---@return {type_name}\n");
output += &format!("function devices.{type_name}.new(config) end\n"); output += &format!("function devices.{type_name}.new(config) end\n");
output += &<::automation_lib::lua::traits::Device as ::automation_lib::lua::traits::PartialUserData<#name>>::definitions().unwrap_or("".into());
#(
output += &<::automation_lib::lua::traits::#traits as ::automation_lib::lua::traits::PartialUserData<#name>>::definitions().unwrap_or("".into());
)*
#(
output += &<#extra_user_data as ::automation_lib::lua::traits::PartialUserData<#name>>::definitions().unwrap_or("".into());
)*
Some(output) Some(output)
} }
@@ -219,7 +220,7 @@ impl Implementations {
all.extend(&attribute.traits); all.extend(&attribute.traits);
} }
} }
Attr::AddMethods(attribute) => add_methods.push(attribute), Attr::ExtraUserData(attribute) => add_methods.push(attribute),
} }
} }
@@ -237,7 +238,7 @@ impl Implementations {
.map(|(alias, traits)| Implementation { .map(|(alias, traits)| Implementation {
name: alias.unwrap_or(name.clone()), name: alias.unwrap_or(name.clone()),
traits, traits,
add_methods: add_methods.clone(), extra_user_data: add_methods.clone(),
}) })
.collect(), .collect(),
) )
@@ -249,7 +250,7 @@ pub fn device(input: DeriveInput) -> TokenStream2 {
.attrs .attrs
.iter() .iter()
.filter(|attr| attr.path().is_ident("device")) .filter(|attr| attr.path().is_ident("device"))
.map(Attribute::parse_args) .map(Attr::parse)
.try_collect::<Vec<_>>() .try_collect::<Vec<_>>()
{ {
Ok(attr) => Implementations::from_attr(attr, input.ident), Ok(attr) => Implementations::from_attr(attr, input.ident),

View File

@@ -64,7 +64,7 @@ pub fn lua_serialize(input: proc_macro::TokenStream) -> proc_macro::TokenStream
/// ``` /// ```
/// It can then be registered with: /// It can then be registered with:
/// ```rust /// ```rust
/// #[device(add_methods(top_secret))] /// #[device(add_methods = top_secret)]
/// ``` /// ```
#[proc_macro_derive(Device, attributes(device))] #[proc_macro_derive(Device, attributes(device))]
pub fn device(input: proc_macro::TokenStream) -> proc_macro::TokenStream { pub fn device(input: proc_macro::TokenStream) -> proc_macro::TokenStream {

View File

@@ -1,42 +1,42 @@
--- @meta --- @meta
---@class InterfaceDevice ---@class DeviceInterface
local InterfaceDevice local DeviceInterface
---@return string ---@return string
function InterfaceDevice:get_id() end function DeviceInterface:get_id() end
---@class InterfaceOnOff: InterfaceDevice ---@class OnOffInterface: DeviceInterface
local InterfaceOnOff local OnOffInterface
---@async ---@async
---@param on boolean ---@param on boolean
function InterfaceOnOff:set_on(on) end function OnOffInterface:set_on(on) end
---@async ---@async
---@return boolean ---@return boolean
function InterfaceOnOff:on() end function OnOffInterface:on() end
---@class InterfaceBrightness: InterfaceDevice ---@class BrightnessInterface: DeviceInterface
local InterfaceBrightness local BrightnessInterface
---@async ---@async
---@param brightness integer ---@param brightness integer
function InterfaceBrightness:set_brightness(brightness) end function BrightnessInterface:set_brightness(brightness) end
---@async ---@async
---@return integer ---@return integer
function InterfaceBrightness:brightness() end function BrightnessInterface:brightness() end
---@class InterfaceColorSetting: InterfaceDevice ---@class ColorSettingInterface: DeviceInterface
local InterfaceColorSetting local ColorSettingInterface
---@async ---@async
---@param temperature integer ---@param temperature integer
function InterfaceColorSetting:set_color_temperature(temperature) end function ColorSettingInterface:set_color_temperature(temperature) end
---@async ---@async
---@return integer ---@return integer
function InterfaceColorSetting:color_temperature() end function ColorSettingInterface:color_temperature() end
---@class InterfaceOpenClose: InterfaceDevice ---@class OpenCloseInterface: DeviceInterface
local InterfaceOpenClose local OpenCloseInterface
---@async ---@async
---@param open_percent integer ---@param open_percent integer
function InterfaceOpenClose:set_open_percent(open_percent) end function OpenCloseInterface:set_open_percent(open_percent) end
---@async ---@async
---@return integer ---@return integer
function InterfaceOpenClose:open_percent() end function OpenCloseInterface:open_percent() end

View File

@@ -17,7 +17,6 @@ use axum::http::StatusCode;
use axum::routing::post; use axum::routing::post;
use axum::{Json, Router}; use axum::{Json, Router};
use config::Config; use config::Config;
use dotenvy::dotenv;
use google_home::{GoogleHome, Request, Response}; use google_home::{GoogleHome, Request, Response};
use mlua::LuaSerdeExt; use mlua::LuaSerdeExt;
use rumqttc::AsyncClient; use rumqttc::AsyncClient;
@@ -75,8 +74,6 @@ async fn fulfillment(
} }
async fn app() -> anyhow::Result<()> { async fn app() -> anyhow::Result<()> {
dotenv().ok();
tracing_subscriber::fmt::init(); tracing_subscriber::fmt::init();
info!(version = VERSION, "automation_rs"); info!(version = VERSION, "automation_rs");