Compare commits

..

2 Commits

Author SHA1 Message Date
5aaa8b43e9 feat: Add proper type definition for devices
All checks were successful
Build and deploy / build (push) Successful in 10m19s
Build and deploy / Deploy container (push) Has been skipped
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 00:44:18 +02:00
c1475370a9 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 00:44:00 +02:00
9 changed files with 136 additions and 213 deletions

4
Cargo.lock generated
View File

@@ -1108,7 +1108,7 @@ dependencies = [
[[package]]
name = "lua_typed"
version = "0.1.0"
source = "git+https://git.huizinga.dev/Dreaded_X/lua_typed#d5d6fc1638bd108514899a792ee64335af50fc8b"
source = "git+https://git.huizinga.dev/Dreaded_X/lua_typed#d30a01aada8f5fc49ad3a296ddbf6e369e08d1f4"
dependencies = [
"eui48",
"lua_typed_macro",
@@ -1117,7 +1117,7 @@ dependencies = [
[[package]]
name = "lua_typed_macro"
version = "0.1.0"
source = "git+https://git.huizinga.dev/Dreaded_X/lua_typed#d5d6fc1638bd108514899a792ee64335af50fc8b"
source = "git+https://git.huizinga.dev/Dreaded_X/lua_typed#d30a01aada8f5fc49ad3a296ddbf6e369e08d1f4"
dependencies = [
"convert_case",
"itertools",

View File

@@ -3,21 +3,18 @@ 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, Typed)]
#[derive(Debug, Deserialize)]
#[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 {
@@ -39,36 +36,12 @@ pub struct Config {
crate::register_type!(Config);
#[derive(Debug, Clone, Device)]
#[device(extra_user_data = SetFlag)]
#[device(add_methods(Self::add_methods))]
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,
@@ -116,6 +89,19 @@ 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

@@ -1,4 +1,3 @@
#![feature(iter_intersperse)]
mod air_filter;
mod contact_sensor;
mod hue_bridge;

View File

@@ -3,7 +3,6 @@ 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;
@@ -14,7 +13,6 @@ use tracing::{error, trace, warn};
#[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,
@@ -61,11 +59,9 @@ 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>,
}
@@ -91,15 +87,14 @@ pub struct Config {
crate::register_type!(Config);
#[derive(Debug, Clone, Device)]
#[device(extra_user_data = SendNotification)]
#[device(add_methods(Self::add_methods))]
pub struct Ntfy {
config: Config,
}
crate::register_device!(Ntfy);
struct SendNotification;
impl PartialUserData<Ntfy> for SendNotification {
fn add_methods<M: mlua::UserDataMethods<Ntfy>>(methods: &mut M) {
impl Ntfy {
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
methods.add_async_method(
"send_notification",
async |lua, this, notification: mlua::Value| {
@@ -111,14 +106,6 @@ impl PartialUserData<Ntfy> for SendNotification {
},
);
}
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,7 +6,6 @@ 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};
@@ -40,29 +39,13 @@ pub struct State {
}
#[derive(Debug, Clone, Device)]
#[device(extra_user_data = OverallPresence)]
#[device(add_methods(Self::add_methods))]
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
@@ -71,6 +54,12 @@ 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

@@ -2,40 +2,21 @@ use std::ops::Deref;
// TODO: Enable and disable functions based on query_only and command_only
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
T: crate::device::Device + 'static,
{
fn add_methods<M: mlua::UserDataMethods<T>>(methods: &mut M) {
pub trait Device {
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M)
where
Self: Sized + crate::device::Device + 'static,
{
methods.add_async_method("get_id", async |_lua, this, _: ()| Ok(this.get_id()));
}
fn interface_name() -> Option<&'static str> {
Some("DeviceInterface")
}
}
impl<T> Device for T where T: crate::device::Device {}
pub struct OnOff;
impl<T> PartialUserData<T> for OnOff
where
T: google_home::traits::OnOff + 'static,
{
fn add_methods<M: mlua::UserDataMethods<T>>(methods: &mut M) {
pub trait OnOff {
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M)
where
Self: Sized + google_home::traits::OnOff + 'static,
{
methods.add_async_method("set_on", async |_lua, this, on: bool| {
this.deref().set_on(on).await.unwrap();
@@ -46,19 +27,14 @@ where
Ok(this.deref().on().await.unwrap())
});
}
fn interface_name() -> Option<&'static str> {
Some("OnOffInterface")
}
}
impl<T> OnOff for T where T: google_home::traits::OnOff {}
pub struct Brightness;
impl<T> PartialUserData<T> for Brightness
where
T: google_home::traits::Brightness + 'static,
{
fn add_methods<M: mlua::UserDataMethods<T>>(methods: &mut M) {
pub trait Brightness {
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M)
where
Self: Sized + google_home::traits::Brightness + 'static,
{
methods.add_async_method("set_brightness", async |_lua, this, brightness: u8| {
this.set_brightness(brightness).await.unwrap();
@@ -69,19 +45,14 @@ where
Ok(this.brightness().await.unwrap())
});
}
fn interface_name() -> Option<&'static str> {
Some("BrightnessInterface")
}
}
impl<T> Brightness for T where T: google_home::traits::Brightness {}
pub struct ColorSetting;
impl<T> PartialUserData<T> for ColorSetting
where
T: google_home::traits::ColorSetting + 'static,
{
fn add_methods<M: mlua::UserDataMethods<T>>(methods: &mut M) {
pub trait ColorSetting {
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M)
where
Self: Sized + google_home::traits::ColorSetting + 'static,
{
methods.add_async_method(
"set_color_temperature",
async |_lua, this, temperature: u32| {
@@ -97,19 +68,14 @@ where
Ok(this.color().await.temperature)
});
}
fn interface_name() -> Option<&'static str> {
Some("ColorSettingInterface")
}
}
impl<T> ColorSetting for T where T: google_home::traits::ColorSetting {}
pub struct OpenClose;
impl<T> PartialUserData<T> for OpenClose
where
T: google_home::traits::OpenClose + 'static,
{
fn add_methods<M: mlua::UserDataMethods<T>>(methods: &mut M) {
pub trait OpenClose {
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M)
where
Self: Sized + google_home::traits::OpenClose + 'static,
{
methods.add_async_method("set_open_percent", async |_lua, this, open_percent: u8| {
this.set_open_percent(open_percent).await.unwrap();
@@ -120,8 +86,5 @@ where
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,36 +1,35 @@
use std::collections::HashMap;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use quote::{ToTokens, quote};
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::spanned::Spanned;
use syn::{Attribute, DeriveInput, Token, parenthesized};
enum Attr {
Trait(TraitAttr),
ExtraUserData(ExtraUserDataAttr),
AddMethods(AddMethodsAttr),
}
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"));
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'",
));
}
};
Ok(())
})?;
Ok(parsed.expect("Parsed should be set"))
Ok(attr)
}
}
@@ -66,6 +65,18 @@ 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>);
@@ -95,18 +106,28 @@ impl Parse for Aliases {
}
#[derive(Clone)]
struct ExtraUserDataAttr(syn::Ident);
struct AddMethodsAttr(syn::Path);
impl Parse for ExtraUserDataAttr {
impl Parse for AddMethodsAttr {
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,
extra_user_data: Vec<ExtraUserDataAttr>,
add_methods: Vec<AddMethodsAttr>,
}
impl quote::ToTokens for Implementation {
@@ -114,10 +135,14 @@ impl quote::ToTokens for Implementation {
let Self {
name,
traits,
extra_user_data,
add_methods,
} = &self;
let Traits(traits) = traits;
let extra_user_data: Vec<_> = extra_user_data.iter().map(|tr| tr.0.clone()).collect();
let interfaces: String = traits
.0
.iter()
.map(|tr| format!(", Interface{tr}"))
.collect();
tokens.extend(quote! {
impl mlua::UserData for #name {
@@ -135,14 +160,12 @@ impl quote::ToTokens for Implementation {
Ok(b)
});
<::automation_lib::lua::traits::Device as ::automation_lib::lua::traits::PartialUserData<#name>>::add_methods(methods);
::automation_lib::lua::traits::Device::add_methods(methods);
#traits
#(
<::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);
#add_methods(methods);
)*
}
}
@@ -154,22 +177,7 @@ impl quote::ToTokens for Implementation {
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"))
Some(format!("---@class {type_name}: InterfaceDevice{}\nlocal {type_name}\n", #interfaces))
}
fn generate_members() -> Option<String> {
@@ -182,15 +190,6 @@ 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 +219,7 @@ impl Implementations {
all.extend(&attribute.traits);
}
}
Attr::ExtraUserData(attribute) => add_methods.push(attribute),
Attr::AddMethods(attribute) => add_methods.push(attribute),
}
}
@@ -238,7 +237,7 @@ impl Implementations {
.map(|(alias, traits)| Implementation {
name: alias.unwrap_or(name.clone()),
traits,
extra_user_data: add_methods.clone(),
add_methods: add_methods.clone(),
})
.collect(),
)
@@ -250,7 +249,7 @@ pub fn device(input: DeriveInput) -> TokenStream2 {
.attrs
.iter()
.filter(|attr| attr.path().is_ident("device"))
.map(Attr::parse)
.map(Attribute::parse_args)
.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

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