Compare commits

6 Commits

Author SHA1 Message Date
686801bd36 feat: Expanded add_methods to extra_user_data
Some checks failed
Build and deploy / Deploy container (push) Blocked by required conditions
Build and deploy / build (push) Has been cancelled
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:44:06 +02:00
8ce9c01228 feat: Specify (optional) interface name in PartialUserData 2025-10-11 02:37:13 +02:00
56d37a3570 feat: Use PartialUserData on proxy type to add trait methods 2025-10-11 02:37:13 +02:00
5731300153 feat: Improved attribute parsing in device macro 2025-10-11 02:37:13 +02:00
47816504e3 feat: Add proper type definition for devices
Depending on the implemented traits the lua class will inherit from the
associated interface class.

It also specifies the constructor function for each of the devices.
2025-10-11 02:37:13 +02:00
9fa2db8fe6 feat: Added Typed impl for all automation devices
To accomplish this a basic implementation was also provided for some
types in automation_lib
2025-10-11 02:37:13 +02:00
27 changed files with 549 additions and 158 deletions

38
Cargo.lock generated
View File

@@ -128,6 +128,7 @@ dependencies = [
"eui48",
"google_home",
"inventory",
"lua_typed",
"mlua",
"reqwest",
"rumqttc",
@@ -153,6 +154,7 @@ dependencies = [
"hostname",
"indexmap",
"inventory",
"lua_typed",
"mlua",
"rumqttc",
"serde",
@@ -345,6 +347,15 @@ dependencies = [
"winnow",
]
[[package]]
name = "convert_case"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f"
dependencies = [
"unicode-segmentation",
]
[[package]]
name = "core-foundation"
version = "0.9.4"
@@ -1094,6 +1105,27 @@ dependencies = [
"cc",
]
[[package]]
name = "lua_typed"
version = "0.1.0"
source = "git+https://git.huizinga.dev/Dreaded_X/lua_typed#d5d6fc1638bd108514899a792ee64335af50fc8b"
dependencies = [
"eui48",
"lua_typed_macro",
]
[[package]]
name = "lua_typed_macro"
version = "0.1.0"
source = "git+https://git.huizinga.dev/Dreaded_X/lua_typed#d5d6fc1638bd108514899a792ee64335af50fc8b"
dependencies = [
"convert_case",
"itertools",
"proc-macro2",
"quote",
"syn 2.0.106",
]
[[package]]
name = "luajit-src"
version = "210.6.1+f9140a6"
@@ -2256,6 +2288,12 @@ version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "unicode-segmentation"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]]
name = "untrusted"
version = "0.9.0"

View File

@@ -37,6 +37,7 @@ indexmap = { version = "2.11.0", features = ["serde"] }
inventory = "0.3.21"
itertools = "0.14.0"
json_value_merge = "2.0.1"
lua_typed = { git = "https://git.huizinga.dev/Dreaded_X/lua_typed" }
mlua = { version = "0.11.3", features = [
"lua54",
"vendored",

View File

@@ -14,6 +14,7 @@ dyn-clone = { workspace = true }
eui48 = { workspace = true }
google_home = { workspace = true }
inventory = { workspace = true }
lua_typed = { workspace = true }
mlua = { workspace = true }
reqwest = { workspace = true }
rumqttc = { workspace = true }

View File

@@ -9,15 +9,19 @@ use google_home::traits::{
TemperatureUnit,
};
use google_home::types::Type;
use lua_typed::Typed;
use thiserror::Error;
use tracing::{debug, trace};
#[derive(Debug, Clone, LuaDeviceConfig)]
#[derive(Debug, Clone, LuaDeviceConfig, Typed)]
#[typed(as = "AirFilterConfig")]
pub struct Config {
#[device_config(flatten)]
#[typed(flatten)]
pub info: InfoConfig,
pub url: String,
}
crate::register_type!(Config);
#[derive(Debug, Clone, Device)]
#[device(traits(OnOff))]

View File

@@ -13,35 +13,45 @@ use google_home::device;
use google_home::errors::{DeviceError, ErrorCode};
use google_home::traits::OpenClose;
use google_home::types::Type;
use lua_typed::Typed;
use serde::Deserialize;
use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
use tracing::{debug, error, trace};
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Copy)]
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Copy, Typed)]
pub enum SensorType {
Door,
Drawer,
Window,
}
crate::register_type!(SensorType);
#[derive(Debug, Clone, LuaDeviceConfig)]
#[derive(Debug, Clone, LuaDeviceConfig, Typed)]
#[typed(as = "ContactSensorConfig")]
pub struct Config {
#[device_config(flatten)]
#[typed(flatten)]
pub info: InfoConfig,
#[device_config(flatten)]
#[typed(flatten)]
pub mqtt: MqttDeviceConfig,
#[device_config(default(SensorType::Window))]
#[typed(default)]
pub sensor_type: SensorType,
#[device_config(from_lua, default)]
#[typed(default)]
pub callback: ActionCallback<(ContactSensor, bool)>,
#[device_config(from_lua, default)]
#[typed(default)]
pub battery_callback: ActionCallback<(ContactSensor, f32)>,
#[device_config(from_lua)]
#[typed(default)]
pub client: WrappedAsyncClient,
}
crate::register_type!(Config);
#[derive(Debug)]
struct State {

View File

@@ -3,40 +3,72 @@ 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)]
#[derive(Debug, Clone, Deserialize, Typed)]
pub struct FlagIDs {
presence: isize,
darkness: isize,
}
crate::register_type!(FlagIDs);
#[derive(Debug, LuaDeviceConfig, Clone)]
#[derive(Debug, LuaDeviceConfig, Clone, Typed)]
#[typed(as = "HueBridgeConfig")]
pub struct Config {
pub identifier: String,
#[device_config(rename("ip"), with(|ip| SocketAddr::new(ip, 80)))]
#[typed(as = "ip")]
pub addr: SocketAddr,
pub login: String,
pub flags: FlagIDs,
}
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,
@@ -84,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 {

View File

@@ -5,19 +5,23 @@ use async_trait::async_trait;
use automation_macro::{Device, LuaDeviceConfig};
use google_home::errors::ErrorCode;
use google_home::traits::OnOff;
use lua_typed::Typed;
use tracing::{error, trace, warn};
use super::{Device, LuaDeviceCreate};
#[derive(Debug, Clone, LuaDeviceConfig)]
#[derive(Debug, Clone, LuaDeviceConfig, Typed)]
#[typed(as = "HueGroupConfig")]
pub struct Config {
pub identifier: String,
#[device_config(rename("ip"), with(|ip| SocketAddr::new(ip, 80)))]
#[typed(as = "ip")]
pub addr: SocketAddr,
pub login: String,
pub group_id: isize,
pub scene_id: String,
}
crate::register_type!(Config);
#[derive(Debug, Clone, Device)]
#[device(traits(OnOff))]

View File

@@ -5,36 +5,46 @@ use automation_lib::device::{Device, LuaDeviceCreate};
use automation_lib::event::OnMqtt;
use automation_lib::mqtt::WrappedAsyncClient;
use automation_macro::{Device, LuaDeviceConfig};
use lua_typed::Typed;
use rumqttc::{Publish, matches};
use serde::Deserialize;
use tracing::{debug, trace, warn};
#[derive(Debug, Clone, LuaDeviceConfig)]
#[derive(Debug, Clone, LuaDeviceConfig, Typed)]
#[typed(as = "HueSwitchConfig")]
pub struct Config {
#[device_config(flatten)]
#[typed(flatten)]
pub info: InfoConfig,
#[device_config(flatten)]
#[typed(flatten)]
pub mqtt: MqttDeviceConfig,
#[device_config(from_lua)]
pub client: WrappedAsyncClient,
#[device_config(from_lua, default)]
#[typed(default)]
pub left_callback: ActionCallback<HueSwitch>,
#[device_config(from_lua, default)]
#[typed(default)]
pub right_callback: ActionCallback<HueSwitch>,
#[device_config(from_lua, default)]
#[typed(default)]
pub left_hold_callback: ActionCallback<HueSwitch>,
#[device_config(from_lua, default)]
#[typed(default)]
pub right_hold_callback: ActionCallback<HueSwitch>,
#[device_config(from_lua, default)]
#[typed(default)]
pub battery_callback: ActionCallback<(HueSwitch, f32)>,
}
crate::register_type!(Config);
#[derive(Debug, Copy, Clone, Deserialize)]
#[serde(rename_all = "snake_case")]

View File

@@ -6,28 +6,36 @@ use automation_lib::event::OnMqtt;
use automation_lib::messages::{RemoteAction, RemoteMessage};
use automation_lib::mqtt::WrappedAsyncClient;
use automation_macro::{Device, LuaDeviceConfig};
use lua_typed::Typed;
use rumqttc::{Publish, matches};
use tracing::{debug, error, trace};
#[derive(Debug, Clone, LuaDeviceConfig)]
#[derive(Debug, Clone, LuaDeviceConfig, Typed)]
#[typed(as = "IkeaRemoteConfig")]
pub struct Config {
#[device_config(flatten)]
#[typed(flatten)]
pub info: InfoConfig,
#[device_config(default)]
#[typed(default)]
pub single_button: bool,
#[device_config(flatten)]
#[typed(flatten)]
pub mqtt: MqttDeviceConfig,
#[device_config(from_lua)]
pub client: WrappedAsyncClient,
#[device_config(from_lua, default)]
#[typed(default)]
pub callback: ActionCallback<(IkeaRemote, bool)>,
#[device_config(from_lua, default)]
#[typed(default)]
pub battery_callback: ActionCallback<(IkeaRemote, f32)>,
}
crate::register_type!(Config);
#[derive(Debug, Clone, Device)]
pub struct IkeaRemote {

View File

@@ -8,18 +8,22 @@ use automation_macro::{Device, LuaDeviceConfig};
use bytes::{Buf, BufMut};
use google_home::errors::{self, DeviceError};
use google_home::traits::OnOff;
use lua_typed::Typed;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tracing::trace;
#[derive(Debug, Clone, LuaDeviceConfig)]
#[derive(Debug, Clone, LuaDeviceConfig, Typed)]
#[typed(as = "KasaOutletConfig")]
pub struct Config {
pub identifier: String,
#[device_config(rename("ip"), with(|ip| SocketAddr::new(ip, 9999)))]
#[typed(as = "ip")]
pub addr: SocketAddr,
}
crate::register_type!(Config);
#[derive(Debug, Clone, Device)]
#[device(traits(OnOff))]

View File

@@ -1,3 +1,4 @@
#![feature(iter_intersperse)]
mod air_filter;
mod contact_sensor;
mod hue_bridge;
@@ -22,20 +23,22 @@ macro_rules! register_device {
stringify!($device),
::mlua::Lua::create_proxy::<$device>
));
crate::register_type!($device);
};
}
pub(crate) use register_device;
type RegisterFn = fn(lua: &mlua::Lua) -> mlua::Result<mlua::AnyUserData>;
type RegisterDeviceFn = fn(lua: &mlua::Lua) -> mlua::Result<mlua::AnyUserData>;
pub struct RegisteredDevice {
name: &'static str,
register_fn: RegisterFn,
register_fn: RegisterDeviceFn,
}
impl RegisteredDevice {
pub const fn new(name: &'static str, register_fn: RegisterFn) -> Self {
pub const fn new(name: &'static str, register_fn: RegisterDeviceFn) -> Self {
Self { name, register_fn }
}
@@ -64,3 +67,28 @@ pub fn create_module(lua: &mlua::Lua) -> mlua::Result<mlua::Table> {
}
inventory::submit! {Module::new("devices", create_module)}
macro_rules! register_type {
($ty:ty) => {
::inventory::submit!(crate::RegisteredType(
<$ty as ::lua_typed::Typed>::generate_full
));
};
}
pub(crate) use register_type;
type RegisterTypeFn = fn() -> Option<String>;
pub struct RegisteredType(RegisterTypeFn);
inventory::collect!(RegisteredType);
pub fn generate_definitions() {
println!("---@meta\n\nlocal devices\n");
for ty in inventory::iter::<RegisteredType> {
let def = ty.0().unwrap();
println!("{def}");
}
println!("return devices")
}

View File

@@ -8,24 +8,29 @@ use automation_lib::event::OnMqtt;
use automation_lib::messages::BrightnessMessage;
use automation_lib::mqtt::WrappedAsyncClient;
use automation_macro::{Device, LuaDeviceConfig};
use lua_typed::Typed;
use rumqttc::Publish;
use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
use tracing::{debug, trace, warn};
#[derive(Debug, Clone, LuaDeviceConfig)]
#[derive(Debug, Clone, LuaDeviceConfig, Typed)]
#[typed(as = "LightSensorConfig")]
pub struct Config {
pub identifier: String,
#[device_config(flatten)]
#[typed(flatten)]
pub mqtt: MqttDeviceConfig,
pub min: isize,
pub max: isize,
#[device_config(from_lua, default)]
#[typed(default)]
pub callback: ActionCallback<(LightSensor, bool)>,
#[device_config(from_lua)]
pub client: WrappedAsyncClient,
}
crate::register_type!(Config);
const DEFAULT: bool = false;

View File

@@ -3,15 +3,18 @@ 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;
use serde::{Deserialize, Serialize};
use serde_repr::*;
use tracing::{error, trace, warn};
#[derive(Debug, Serialize_repr, Deserialize, Clone, Copy)]
#[derive(Debug, Serialize_repr, Deserialize, Clone, Copy, Typed)]
#[repr(u8)]
#[serde(rename_all = "snake_case")]
#[typed(rename_all = "snake_case")]
pub enum Priority {
Min = 1,
Low,
@@ -19,44 +22,54 @@ pub enum Priority {
High,
Max,
}
crate::register_type!(Priority);
#[derive(Debug, Serialize, Deserialize, Clone)]
#[derive(Debug, Serialize, Deserialize, Clone, Typed)]
#[serde(rename_all = "snake_case", tag = "action")]
#[typed(rename_all = "snake_case", tag = "action")]
pub enum ActionType {
Broadcast {
#[serde(skip_serializing_if = "HashMap::is_empty")]
#[serde(default)]
#[typed(default)]
extras: HashMap<String, String>,
},
// View,
// Http
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[derive(Debug, Serialize, Deserialize, Clone, Typed)]
pub struct Action {
#[serde(flatten)]
#[typed(flatten)]
pub action: ActionType,
pub label: String,
pub clear: Option<bool>,
}
crate::register_type!(Action);
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, Typed)]
struct NotificationFinal {
topic: String,
#[serde(flatten)]
#[typed(flatten)]
inner: Notification,
}
#[derive(Debug, Serialize, Clone, Deserialize)]
#[derive(Debug, Serialize, Clone, Deserialize, Typed)]
pub struct Notification {
title: String,
message: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty", default = "Default::default")]
#[typed(default)]
tags: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
priority: Option<Priority>,
#[typed(default)]
#[serde(skip_serializing_if = "Vec::is_empty", default = "Default::default")]
actions: Vec<Action>,
}
crate::register_type!(Notification);
impl Notification {
fn finalize(self, topic: &str) -> NotificationFinal {
@@ -67,22 +80,26 @@ impl Notification {
}
}
#[derive(Debug, Clone, LuaDeviceConfig)]
#[derive(Debug, Clone, LuaDeviceConfig, Typed)]
#[typed(as = "NtfyConfig")]
pub struct Config {
#[device_config(default("https://ntfy.sh".into()))]
#[typed(default)]
pub url: String,
pub topic: String,
}
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| {
@@ -94,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]

View File

@@ -6,24 +6,30 @@ 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};
use lua_typed::Typed;
use rumqttc::Publish;
use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
use tracing::{debug, trace, warn};
#[derive(Debug, Clone, LuaDeviceConfig)]
#[derive(Debug, Clone, LuaDeviceConfig, Typed)]
#[typed(as = "PresenceConfig")]
pub struct Config {
#[device_config(flatten)]
#[typed(flatten)]
pub mqtt: MqttDeviceConfig,
#[device_config(from_lua, default)]
#[typed(default)]
pub callback: ActionCallback<(Presence, bool)>,
#[device_config(from_lua)]
pub client: WrappedAsyncClient,
}
crate::register_type!(Config);
pub const DEFAULT_PRESENCE: bool = false;
@@ -34,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
@@ -49,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]

View File

@@ -12,21 +12,27 @@ use google_home::device;
use google_home::errors::ErrorCode;
use google_home::traits::{self, Scene};
use google_home::types::Type;
use lua_typed::Typed;
use rumqttc::Publish;
use tracing::{debug, error, trace};
#[derive(Debug, Clone, LuaDeviceConfig)]
#[derive(Debug, Clone, LuaDeviceConfig, Typed)]
#[typed(as = "WolConfig")]
pub struct Config {
#[device_config(flatten)]
#[typed(flatten)]
pub info: InfoConfig,
#[device_config(flatten)]
#[typed(flatten)]
pub mqtt: MqttDeviceConfig,
pub mac_address: MacAddress,
#[device_config(default(Ipv4Addr::new(255, 255, 255, 255)))]
#[typed(default)]
pub broadcast_ip: Ipv4Addr,
#[device_config(from_lua)]
pub client: WrappedAsyncClient,
}
crate::register_type!(Config);
#[derive(Debug, Clone, Device)]
pub struct WakeOnLAN {

View File

@@ -8,24 +8,29 @@ use automation_lib::event::OnMqtt;
use automation_lib::messages::PowerMessage;
use automation_lib::mqtt::WrappedAsyncClient;
use automation_macro::{Device, LuaDeviceConfig};
use lua_typed::Typed;
use rumqttc::Publish;
use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
use tracing::{debug, error, trace};
#[derive(Debug, Clone, LuaDeviceConfig)]
#[derive(Debug, Clone, LuaDeviceConfig, Typed)]
#[typed(as = "WasherConfig")]
pub struct Config {
pub identifier: String,
#[device_config(flatten)]
#[typed(flatten)]
pub mqtt: MqttDeviceConfig,
// Power in Watt
pub threshold: f32,
#[device_config(from_lua, default)]
#[typed(default)]
pub done_callback: ActionCallback<Washer>,
#[device_config(from_lua)]
pub client: WrappedAsyncClient,
}
crate::register_type!(Config);
#[derive(Debug)]
pub struct State {

View File

@@ -15,6 +15,7 @@ use google_home::device;
use google_home::errors::ErrorCode;
use google_home::traits::{Brightness, Color, ColorSetting, ColorTemperatureRange, OnOff};
use google_home::types::Type;
use lua_typed::Typed;
use rumqttc::{Publish, matches};
use serde::{Deserialize, Serialize};
use serde_json::json;
@@ -22,33 +23,47 @@ use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
use tracing::{debug, trace, warn};
pub trait LightState:
Debug + Clone + Default + Sync + Send + Serialize + Into<StateOnOff> + 'static
Debug + Clone + Default + Sync + Send + Serialize + Into<StateOnOff> + Typed + 'static
{
}
#[derive(Debug, Clone, LuaDeviceConfig)]
pub struct Config<T: LightState> {
#[derive(Debug, Clone, LuaDeviceConfig, Typed)]
#[typed(as = "ConfigLight")]
pub struct Config<T: LightState>
where
Light<T>: Typed,
{
#[device_config(flatten)]
#[typed(flatten)]
pub info: InfoConfig,
#[device_config(flatten)]
#[typed(flatten)]
pub mqtt: MqttDeviceConfig,
#[device_config(from_lua, default)]
#[typed(default)]
pub callback: ActionCallback<(Light<T>, T)>,
#[device_config(from_lua)]
#[typed(default)]
pub client: WrappedAsyncClient,
}
crate::register_type!(Config<StateOnOff>);
crate::register_type!(Config<StateBrightness>);
crate::register_type!(Config<StateColorTemperature>);
#[derive(Debug, Clone, Default, Serialize, Deserialize, LuaSerialize)]
#[derive(Debug, Clone, Default, Serialize, Deserialize, LuaSerialize, Typed)]
#[typed(as = "LightStateOnOff")]
pub struct StateOnOff {
#[serde(deserialize_with = "state_deserializer")]
state: bool,
}
impl LightState for StateOnOff {}
crate::register_type!(StateOnOff);
#[derive(Debug, Clone, Default, Serialize, Deserialize, LuaSerialize)]
#[derive(Debug, Clone, Default, Serialize, Deserialize, LuaSerialize, Typed)]
#[typed(as = "LightStateBrightness")]
pub struct StateBrightness {
#[serde(deserialize_with = "state_deserializer")]
state: bool,
@@ -56,6 +71,7 @@ pub struct StateBrightness {
}
impl LightState for StateBrightness {}
crate::register_type!(StateBrightness);
impl From<StateBrightness> for StateOnOff {
fn from(state: StateBrightness) -> Self {
@@ -63,13 +79,15 @@ impl From<StateBrightness> for StateOnOff {
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, LuaSerialize)]
#[derive(Debug, Clone, Default, Serialize, Deserialize, LuaSerialize, Typed)]
#[typed(as = "LightStateColorTemperature")]
pub struct StateColorTemperature {
#[serde(deserialize_with = "state_deserializer")]
state: bool,
brightness: f32,
color_temp: u32,
}
crate::register_type!(StateColorTemperature);
impl LightState for StateColorTemperature {}
@@ -92,7 +110,10 @@ impl From<StateColorTemperature> for StateBrightness {
#[device(traits(OnOff for LightOnOff, LightBrightness, LightColorTemperature))]
#[device(traits(Brightness for LightBrightness, LightColorTemperature))]
#[device(traits(ColorSetting for LightColorTemperature))]
pub struct Light<T: LightState> {
pub struct Light<T: LightState>
where
Light<T>: Typed,
{
config: Config<T>,
state: Arc<RwLock<T>>,
@@ -107,7 +128,10 @@ crate::register_device!(LightBrightness);
pub type LightColorTemperature = Light<StateColorTemperature>;
crate::register_device!(LightColorTemperature);
impl<T: LightState> Light<T> {
impl<T: LightState> Light<T>
where
Light<T>: Typed,
{
async fn state(&self) -> RwLockReadGuard<'_, T> {
self.state.read().await
}
@@ -118,7 +142,10 @@ impl<T: LightState> Light<T> {
}
#[async_trait]
impl<T: LightState> LuaDeviceCreate for Light<T> {
impl<T: LightState> LuaDeviceCreate for Light<T>
where
Light<T>: Typed,
{
type Config = Config<T>;
type Error = rumqttc::ClientError;
@@ -137,7 +164,10 @@ impl<T: LightState> LuaDeviceCreate for Light<T> {
}
}
impl<T: LightState> Device for Light<T> {
impl<T: LightState> Device for Light<T>
where
Light<T>: Typed,
{
fn get_id(&self) -> String {
self.config.info.identifier()
}
@@ -257,7 +287,10 @@ impl OnMqtt for LightColorTemperature {
}
#[async_trait]
impl<T: LightState> google_home::Device for Light<T> {
impl<T: LightState> google_home::Device for Light<T>
where
Light<T>: Typed,
{
fn get_device_type(&self) -> Type {
Type::Light
}
@@ -288,6 +321,7 @@ impl<T: LightState> google_home::Device for Light<T> {
impl<T> OnOff for Light<T>
where
T: LightState,
Light<T>: Typed,
{
async fn on(&self) -> Result<bool, ErrorCode> {
let state = self.state().await;
@@ -327,6 +361,7 @@ impl<T> Brightness for Light<T>
where
T: LightState,
T: Into<StateBrightness>,
Light<T>: Typed,
{
async fn brightness(&self) -> Result<u8, ErrorCode> {
let state = self.state().await;
@@ -368,6 +403,7 @@ impl<T> ColorSetting for Light<T>
where
T: LightState,
T: Into<StateColorTemperature>,
Light<T>: Typed,
{
fn color_temperature_range(&self) -> ColorTemperatureRange {
ColorTemperatureRange {

View File

@@ -15,6 +15,7 @@ use google_home::device;
use google_home::errors::ErrorCode;
use google_home::traits::OnOff;
use google_home::types::Type;
use lua_typed::Typed;
use rumqttc::{Publish, matches};
use serde::{Deserialize, Serialize};
use serde_json::json;
@@ -22,15 +23,16 @@ use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
use tracing::{debug, trace, warn};
pub trait OutletState:
Debug + Clone + Default + Sync + Send + Serialize + Into<StateOnOff> + 'static
Debug + Clone + Default + Sync + Send + Serialize + Into<StateOnOff> + Typed + 'static
{
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Copy)]
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Copy, Typed)]
pub enum OutletType {
Outlet,
Kettle,
}
crate::register_type!(OutletType);
impl From<OutletType> for Type {
fn from(outlet: OutletType) -> Self {
@@ -41,36 +43,50 @@ impl From<OutletType> for Type {
}
}
#[derive(Debug, Clone, LuaDeviceConfig)]
pub struct Config<T: OutletState> {
#[derive(Debug, Clone, LuaDeviceConfig, Typed)]
#[typed(as = "ConfigOutlet")]
pub struct Config<T: OutletState>
where
Outlet<T>: Typed,
{
#[device_config(flatten)]
#[typed(flatten)]
pub info: InfoConfig,
#[device_config(flatten)]
#[typed(flatten)]
pub mqtt: MqttDeviceConfig,
#[device_config(default(OutletType::Outlet))]
#[typed(default)]
pub outlet_type: OutletType,
#[device_config(from_lua, default)]
#[typed(default)]
pub callback: ActionCallback<(Outlet<T>, T)>,
#[device_config(from_lua)]
pub client: WrappedAsyncClient,
}
crate::register_type!(Config<StateOnOff>);
crate::register_type!(Config<StatePower>);
#[derive(Debug, Clone, Default, Serialize, Deserialize, LuaSerialize)]
#[derive(Debug, Clone, Default, Serialize, Deserialize, LuaSerialize, Typed)]
#[typed(as = "OutletStateOnOff")]
pub struct StateOnOff {
#[serde(deserialize_with = "state_deserializer")]
state: bool,
}
crate::register_type!(StateOnOff);
impl OutletState for StateOnOff {}
#[derive(Debug, Clone, Default, Serialize, Deserialize, LuaSerialize)]
#[derive(Debug, Clone, Default, Serialize, Deserialize, LuaSerialize, Typed)]
#[typed(as = "OutletStatePower")]
pub struct StatePower {
#[serde(deserialize_with = "state_deserializer")]
state: bool,
power: f64,
}
crate::register_type!(StatePower);
impl OutletState for StatePower {}
@@ -82,7 +98,10 @@ impl From<StatePower> for StateOnOff {
#[derive(Debug, Clone, Device)]
#[device(traits(OnOff for OutletOnOff, OutletPower))]
pub struct Outlet<T: OutletState> {
pub struct Outlet<T: OutletState>
where
Outlet<T>: Typed,
{
config: Config<T>,
state: Arc<RwLock<T>>,
@@ -94,7 +113,10 @@ crate::register_device!(OutletOnOff);
pub type OutletPower = Outlet<StatePower>;
crate::register_device!(OutletPower);
impl<T: OutletState> Outlet<T> {
impl<T: OutletState> Outlet<T>
where
Outlet<T>: Typed,
{
async fn state(&self) -> RwLockReadGuard<'_, T> {
self.state.read().await
}
@@ -105,7 +127,10 @@ impl<T: OutletState> Outlet<T> {
}
#[async_trait]
impl<T: OutletState> LuaDeviceCreate for Outlet<T> {
impl<T: OutletState> LuaDeviceCreate for Outlet<T>
where
Outlet<T>: Typed,
{
type Config = Config<T>;
type Error = rumqttc::ClientError;
@@ -124,7 +149,10 @@ impl<T: OutletState> LuaDeviceCreate for Outlet<T> {
}
}
impl<T: OutletState> Device for Outlet<T> {
impl<T: OutletState> Device for Outlet<T>
where
Outlet<T>: Typed,
{
fn get_id(&self) -> String {
self.config.info.identifier()
}
@@ -201,7 +229,10 @@ impl OnMqtt for OutletPower {
}
#[async_trait]
impl<T: OutletState> google_home::Device for Outlet<T> {
impl<T: OutletState> google_home::Device for Outlet<T>
where
Outlet<T>: Typed,
{
fn get_device_type(&self) -> Type {
self.config.outlet_type.into()
}
@@ -232,6 +263,7 @@ impl<T: OutletState> google_home::Device for Outlet<T> {
impl<T> OnOff for Outlet<T>
where
T: OutletState,
Outlet<T>: Typed,
{
async fn on(&self) -> Result<bool, ErrorCode> {
let state = self.state().await;

View File

@@ -13,6 +13,7 @@ google_home = { workspace = true }
hostname = { workspace = true }
indexmap = { workspace = true }
inventory = { workspace = true }
lua_typed = { workspace = true }
mlua = { workspace = true }
rumqttc = { workspace = true }
serde = { workspace = true }

View File

@@ -1,6 +1,7 @@
use std::marker::PhantomData;
use futures::future::try_join_all;
use lua_typed::Typed;
use mlua::{FromLua, IntoLuaMulti};
#[derive(Debug, Clone)]
@@ -9,6 +10,23 @@ pub struct ActionCallback<P> {
_parameters: PhantomData<P>,
}
impl<A: Typed> Typed for ActionCallback<A> {
fn type_name() -> String {
let type_name = A::type_name();
format!("fun(_: {type_name}) | fun(_: {type_name})[]")
}
}
impl<A: Typed, B: Typed> Typed for ActionCallback<(A, B)> {
fn type_name() -> String {
let type_name_a = A::type_name();
let type_name_b = B::type_name();
format!(
"fun(_: {type_name_a}, _: {type_name_b}) | fun(_: {type_name_a}, _: {type_name_b})[]"
)
}
}
// NOTE: For some reason the derive macro combined with PhantomData leads to issues where it
// requires all types part of P to implement default, even if they never actually get constructed.
// By manually implemented Default it works fine.

View File

@@ -1,6 +1,7 @@
use std::net::{Ipv4Addr, SocketAddr};
use std::time::Duration;
use lua_typed::Typed;
use rumqttc::{MqttOptions, Transport};
use serde::Deserialize;
@@ -52,7 +53,7 @@ fn default_fulfillment_port() -> u16 {
7878
}
#[derive(Debug, Clone, Deserialize)]
#[derive(Debug, Clone, Deserialize, Typed)]
pub struct InfoConfig {
pub name: String,
pub room: Option<String>,
@@ -68,7 +69,7 @@ impl InfoConfig {
}
}
#[derive(Debug, Clone, Deserialize)]
#[derive(Debug, Clone, Deserialize, Typed)]
pub struct MqttDeviceConfig {
pub topic: String,
}

View File

@@ -1,5 +1,6 @@
#![allow(incomplete_features)]
#![feature(iterator_try_collect)]
#![feature(with_negative_coherence)]
use tracing::debug;

View File

@@ -2,21 +2,40 @@ use std::ops::Deref;
// TODO: Enable and disable functions based on query_only and command_only
pub trait Device {
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M)
pub trait PartialUserData<T> {
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
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()));
}
}
impl<T> Device for T where T: crate::device::Device {}
pub trait OnOff {
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M)
fn interface_name() -> Option<&'static str> {
Some("DeviceInterface")
}
}
pub struct OnOff;
impl<T> PartialUserData<T> for OnOff
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| {
this.deref().set_on(on).await.unwrap();
@@ -27,14 +46,19 @@ pub trait OnOff {
Ok(this.deref().on().await.unwrap())
});
}
}
impl<T> OnOff for T where T: google_home::traits::OnOff {}
pub trait Brightness {
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M)
fn interface_name() -> Option<&'static str> {
Some("OnOffInterface")
}
}
pub struct Brightness;
impl<T> PartialUserData<T> for Brightness
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| {
this.set_brightness(brightness).await.unwrap();
@@ -45,14 +69,19 @@ pub trait Brightness {
Ok(this.brightness().await.unwrap())
});
}
}
impl<T> Brightness for T where T: google_home::traits::Brightness {}
pub trait ColorSetting {
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M)
fn interface_name() -> Option<&'static str> {
Some("BrightnessInterface")
}
}
pub struct ColorSetting;
impl<T> PartialUserData<T> for ColorSetting
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(
"set_color_temperature",
async |_lua, this, temperature: u32| {
@@ -68,14 +97,19 @@ pub trait ColorSetting {
Ok(this.color().await.temperature)
});
}
}
impl<T> ColorSetting for T where T: google_home::traits::ColorSetting {}
pub trait OpenClose {
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M)
fn interface_name() -> Option<&'static str> {
Some("ColorSettingInterface")
}
}
pub struct OpenClose;
impl<T> PartialUserData<T> for OpenClose
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| {
this.set_open_percent(open_percent).await.unwrap();
@@ -86,5 +120,8 @@ pub trait OpenClose {
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,5 +1,6 @@
use std::ops::{Deref, DerefMut};
use lua_typed::Typed;
use mlua::FromLua;
use rumqttc::{AsyncClient, Event, EventLoop, Incoming};
use tracing::{debug, warn};
@@ -9,6 +10,12 @@ use crate::event::{self, EventChannel};
#[derive(Debug, Clone, FromLua)]
pub struct WrappedAsyncClient(pub AsyncClient);
impl Typed for WrappedAsyncClient {
fn type_name() -> String {
"AsyncClient".into()
}
}
impl Deref for WrappedAsyncClient {
type Target = AsyncClient;

View File

@@ -1,35 +1,36 @@
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;
use syn::{Attribute, DeriveInput, Token, parenthesized};
enum Attr {
Trait(TraitAttr),
AddMethods(AddMethodsAttr),
ExtraUserData(ExtraUserDataAttr),
}
impl Parse for Attr {
fn parse(input: ParseStream) -> syn::Result<Self> {
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()?),
"add_methods" => Attr::AddMethods(attr.parse()?),
_ => {
return Err(syn::Error::new(
ident.span(),
"Expected 'traits' or 'add_methods'",
));
impl Attr {
fn parse(attr: &Attribute) -> syn::Result<Self> {
let mut parsed = None;
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("traits") {
let input;
_ = parenthesized!(input in meta.input);
parsed = Some(Attr::Trait(input.parse()?));
} else if meta.path.is_ident("extra_user_data") {
let value = meta.value()?;
parsed = Some(Attr::ExtraUserData(value.parse()?));
} else {
return Err(syn::Error::new(meta.path.span(), "Unknown attribute"));
}
};
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)]
struct Aliases(Vec<syn::Ident>);
@@ -106,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 {
@@ -135,8 +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 {
@@ -154,13 +135,64 @@ impl quote::ToTokens for Implementation {
Ok(b)
});
::automation_lib::lua::traits::Device::add_methods(methods);
#traits
<::automation_lib::lua::traits::Device as ::automation_lib::lua::traits::PartialUserData<#name>>::add_methods(methods);
#(
#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);
)*
}
}
impl ::lua_typed::Typed for #name {
fn type_name() -> String {
stringify!(#name).into()
}
fn generate_header() -> std::option::Option<::std::string::String> {
let type_name = <Self as ::lua_typed::Typed>::type_name();
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> {
let mut output = String::new();
let type_name = <Self as ::lua_typed::Typed>::type_name();
output += &format!("devices.{type_name} = {{}}\n");
let config_name = <<Self as ::automation_lib::device::LuaDeviceCreate>::Config as ::lua_typed::Typed>::type_name();
output += &format!("---@param config {config_name}\n");
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)
}
}
});
@@ -188,7 +220,7 @@ impl Implementations {
all.extend(&attribute.traits);
}
}
Attr::AddMethods(attribute) => add_methods.push(attribute),
Attr::ExtraUserData(attribute) => add_methods.push(attribute),
}
}
@@ -206,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(),
)
@@ -218,7 +250,7 @@ pub fn device(input: DeriveInput) -> TokenStream2 {
.attrs
.iter()
.filter(|attr| attr.path().is_ident("device"))
.map(Attribute::parse_args)
.map(Attr::parse)
.try_collect::<Vec<_>>()
{
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:
/// ```rust
/// #[device(add_methods(top_secret))]
/// #[device(add_methods = top_secret)]
/// ```
#[proc_macro_derive(Device, attributes(device))]
pub fn device(input: proc_macro::TokenStream) -> proc_macro::TokenStream {

View File

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