From df45d4c489606025c4d35159b9f668d4a4e0ce4e Mon Sep 17 00:00:00 2001 From: Dreaded_X Date: Thu, 18 Sep 2025 01:45:31 +0200 Subject: [PATCH] WIP --- Cargo.lock | 35 +++++ Cargo.toml | 1 + automation_devices/Cargo.toml | 1 + automation_devices/src/lib.rs | 33 +++- automation_devices/src/ntfy.rs | 36 ++++- automation_lib/src/lua/traits.rs | 95 ++++++++--- automation_macro/src/device.rs | 51 +++++- config.lua | 8 +- definitions.bak/automation:devices.lua.bak | 42 +++++ definitions.bak/automation:secrets.lua.bak | 6 + definitions.bak/automation:utils.lua.bak | 27 ++++ definitions.bak/automation:variables.lua.bak | 6 + definitions/automation:devices.lua | 156 +++++++++++++++++++ src/bin/generate_definitions.rs | 3 + 14 files changed, 464 insertions(+), 36 deletions(-) create mode 100644 definitions.bak/automation:devices.lua.bak create mode 100644 definitions.bak/automation:secrets.lua.bak create mode 100644 definitions.bak/automation:utils.lua.bak create mode 100644 definitions.bak/automation:variables.lua.bak create mode 100644 definitions/automation:devices.lua create mode 100644 src/bin/generate_definitions.rs diff --git a/Cargo.lock b/Cargo.lock index accc16c..0bd599f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -128,6 +128,7 @@ dependencies = [ "eui48", "google_home", "inventory", + "lua_typed", "mlua", "reqwest", "rumqttc", @@ -345,6 +346,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 +1104,25 @@ dependencies = [ "cc", ] +[[package]] +name = "lua_typed" +version = "0.1.0" +source = "git+https://git.huizinga.dev/Dreaded_X/lua_typed#a5f4b672167335c60d9e50e0e7afc932a755b4cf" +dependencies = [ + "lua_typed_macro", +] + +[[package]] +name = "lua_typed_macro" +version = "0.1.0" +source = "git+https://git.huizinga.dev/Dreaded_X/lua_typed#a5f4b672167335c60d9e50e0e7afc932a755b4cf" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "luajit-src" version = "210.6.1+f9140a6" @@ -2256,6 +2285,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" diff --git a/Cargo.toml b/Cargo.toml index 952c0c4..5f401f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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", diff --git a/automation_devices/Cargo.toml b/automation_devices/Cargo.toml index 4bfee79..5aba41f 100644 --- a/automation_devices/Cargo.toml +++ b/automation_devices/Cargo.toml @@ -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 } diff --git a/automation_devices/src/lib.rs b/automation_devices/src/lib.rs index 80f3f17..efd168f 100644 --- a/automation_devices/src/lib.rs +++ b/automation_devices/src/lib.rs @@ -22,20 +22,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; +type RegisterDeviceFn = fn(lua: &mlua::Lua) -> mlua::Result; 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 +66,28 @@ pub fn create_module(lua: &mlua::Lua) -> mlua::Result { } 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; + +pub struct RegisteredType(RegisterTypeFn); + +inventory::collect!(RegisteredType); + +pub fn generate_definitions() { + println!("---@meta\n\nlocal devices\n"); + for ty in inventory::iter:: { + let def = ty.0().unwrap(); + println!("{def}"); + } + println!("return devices") +} diff --git a/automation_devices/src/ntfy.rs b/automation_devices/src/ntfy.rs index 78c7ba8..93f2782 100644 --- a/automation_devices/src/ntfy.rs +++ b/automation_devices/src/ntfy.rs @@ -4,12 +4,13 @@ use std::convert::Infallible; use async_trait::async_trait; use automation_lib::device::{Device, LuaDeviceCreate}; 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")] pub enum Priority { @@ -19,8 +20,9 @@ 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")] pub enum ActionType { Broadcast { @@ -31,22 +33,23 @@ pub enum ActionType { // Http } -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, Typed)] pub struct Action { #[serde(flatten)] pub action: ActionType, pub label: String, pub clear: Option, } +crate::register_type!(Action); -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Typed)] struct NotificationFinal { topic: String, #[serde(flatten)] inner: Notification, } -#[derive(Debug, Serialize, Clone, Deserialize)] +#[derive(Debug, Serialize, Clone, Deserialize, Typed)] pub struct Notification { title: String, message: Option, @@ -57,6 +60,7 @@ pub struct Notification { #[serde(skip_serializing_if = "Vec::is_empty", default = "Default::default")] actions: Vec, } +crate::register_type!(Notification); impl Notification { fn finalize(self, topic: &str) -> NotificationFinal { @@ -67,12 +71,14 @@ impl Notification { } } -#[derive(Debug, Clone, LuaDeviceConfig)] +#[derive(Debug, Clone, LuaDeviceConfig, Typed)] +#[typed(as = "NtfyConfig")] pub struct Config { #[device_config(default("https://ntfy.sh".into()))] pub url: String, pub topic: String, } +crate::register_type!(Config); #[derive(Debug, Clone, Device)] #[device(add_methods(Self::add_methods))] @@ -96,6 +102,24 @@ impl Ntfy { } } +// impl Typed for Ntfy { +// fn type_name() -> String { +// "Ntfy".into() +// } +// +// fn generate_header() -> Option { +// let type_name = ::type_name(); +// Some(format!("---@class {type_name}\nlocal {type_name}\n")) +// } +// +// fn generate_members() -> Option { +// Some(format!( +// "---@async\n---@param notification Notification\nfunction {}:send_notification(notification) end\n", +// ::type_name(), +// )) +// } +// } + #[async_trait] impl LuaDeviceCreate for Ntfy { type Config = Config; diff --git a/automation_lib/src/lua/traits.rs b/automation_lib/src/lua/traits.rs index 4e1c724..713a1db 100644 --- a/automation_lib/src/lua/traits.rs +++ b/automation_lib/src/lua/traits.rs @@ -1,11 +1,16 @@ +use std::marker::PhantomData; use std::ops::Deref; // TODO: Enable and disable functions based on query_only and command_only -pub trait OnOff { - fn add_methods>(methods: &mut M) +pub struct OnOff { + _phantom: PhantomData, +} + +impl OnOff { + pub fn add_methods>(methods: &mut M) where - Self: Sized + google_home::traits::OnOff + 'static, + T: Sized + google_home::traits::OnOff + 'static, { methods.add_async_method("set_on", async |_lua, this, on: bool| { this.deref().set_on(on).await.unwrap(); @@ -17,13 +22,25 @@ pub trait OnOff { Ok(this.deref().on().await.unwrap()) }); } -} -impl OnOff for T where T: google_home::traits::OnOff {} -pub trait Brightness { - fn add_methods>(methods: &mut M) + pub fn generate_definitions(name: &str) -> String { + let mut output = String::new(); + + output += &format!("---@async\n---@param on boolean\nfunction {name}:set_on(on) end\n"); + output += &format!("---@async\n---@return boolean\nfunction {name}:on() end\n"); + + output + } +} + +pub struct Brightness { + _phantom: PhantomData, +} + +impl Brightness { + pub fn add_methods>(methods: &mut M) where - Self: Sized + google_home::traits::Brightness + 'static, + T: Sized + google_home::traits::Brightness + 'static, { methods.add_async_method("set_brightness", async |_lua, this, brightness: u8| { this.set_brightness(brightness).await.unwrap(); @@ -35,13 +52,27 @@ pub trait Brightness { Ok(this.brightness().await.unwrap()) }); } -} -impl Brightness for T where T: google_home::traits::Brightness {} -pub trait ColorSetting { - fn add_methods>(methods: &mut M) + pub fn generate_definitions(name: &str) -> String { + let mut output = String::new(); + + output += &format!( + "---@async\n---@param brightness integer\nfunction {name}:set_brightness(brightness) end\n" + ); + output += &format!("---@async\n---@return integer\nfunction {name}:brightness() end\n"); + + output + } +} + +pub struct ColorSetting { + _phantom: PhantomData, +} + +impl ColorSetting { + pub fn add_methods>(methods: &mut M) where - Self: Sized + google_home::traits::ColorSetting + 'static, + T: Sized + google_home::traits::ColorSetting + 'static, { methods.add_async_method( "set_color_temperature", @@ -58,13 +89,28 @@ pub trait ColorSetting { Ok(this.color().await.temperature) }); } -} -impl ColorSetting for T where T: google_home::traits::ColorSetting {} -pub trait OpenClose { - fn add_methods>(methods: &mut M) + pub fn generate_definitions(name: &str) -> String { + let mut output = String::new(); + + output += &format!( + "---@async\n---@param temperature integer\nfunction {name}:set_color_temperature(temperature) end\n" + ); + output += + &format!("---@async\n---@return integer\nfunction {name}:color_temperature() end\n"); + + output + } +} + +pub struct OpenClose { + _phantom: PhantomData, +} + +impl OpenClose { + pub fn add_methods>(methods: &mut M) where - Self: Sized + google_home::traits::OpenClose + 'static, + T: 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(); @@ -76,5 +122,16 @@ pub trait OpenClose { Ok(this.open_percent().await.unwrap()) }); } + + pub fn generate_definitions(type_name: &str) -> String { + let mut output = String::new(); + + output += &format!( + "---@async\n---@param open_percent integer\nfunction {type_name}:set_open_percent(open_percent) end\n" + ); + output += + &format!("---@async\n---@return integer\nfunction {type_name}:open_percent() end\n"); + + output + } } -impl OpenClose for T where T: google_home::traits::OpenClose {} diff --git a/automation_macro/src/device.rs b/automation_macro/src/device.rs index b886436..aa83759 100644 --- a/automation_macro/src/device.rs +++ b/automation_macro/src/device.rs @@ -47,7 +47,7 @@ impl Parse for TraitAttr { } } -#[derive(Default)] +#[derive(Default, Clone)] struct Traits(Vec); impl Traits { @@ -65,13 +65,27 @@ impl Parse for Traits { } } -impl ToTokens for Traits { +struct TraitsAddMethods(Traits); +impl ToTokens for TraitsAddMethods { fn to_tokens(&self, tokens: &mut TokenStream2) { - let Self(traits) = &self; + let Self(Traits(traits)) = &self; tokens.extend(quote! { #( - ::automation_lib::lua::traits::#traits::add_methods(methods); + ::automation_lib::lua::traits::#traits::::add_methods(methods); + )* + }); + } +} + +struct TraitsGenerateDefinitions(Traits); +impl ToTokens for TraitsGenerateDefinitions { + fn to_tokens(&self, tokens: &mut TokenStream2) { + let Self(Traits(traits)) = &self; + + tokens.extend(quote! { + #( + output += &::automation_lib::lua::traits::#traits::::generate_definitions(&type_name); )* }); } @@ -138,9 +152,12 @@ impl quote::ToTokens for Implementation { add_methods, } = &self; + let traits_add_methods = TraitsAddMethods(traits.clone()); + let traits_generate_definitions = TraitsGenerateDefinitions(traits.clone()); + tokens.extend(quote! { - impl mlua::UserData for #name { - fn add_methods>(methods: &mut M) { + impl ::mlua::UserData for #name { + fn add_methods>(methods: &mut M) { methods.add_async_function("new", async |_lua, config| { let device: Self = LuaDeviceCreate::create(config) .await @@ -156,13 +173,33 @@ impl quote::ToTokens for Implementation { methods.add_async_method("get_id", async |_lua, this, _: ()| { Ok(this.get_id()) }); - #traits + #traits_add_methods #( #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 = ::type_name(); + Some(format!("---@class {type_name}\nlocal {type_name}\n")) + } + + fn generate_members() -> Option { + let type_name = ::type_name(); + let mut output = String::new(); + + #traits_generate_definitions + + Some(output) + } + } }); } } diff --git a/config.lua b/config.lua index f3cdc5b..b31737b 100644 --- a/config.lua +++ b/config.lua @@ -2,17 +2,21 @@ local devices = require("automation:devices") local device_manager = require("automation:device_manager") local utils = require("automation:utils") local secrets = require("automation:secrets") -local debug = require("automation:variables").debug or false +local debug = require("automation:variables").debug and true or false print(_VERSION) local host = utils.get_hostname() print("Running @" .. host) +--- @param topic string +--- @return string local function mqtt_z2m(topic) return "zigbee2mqtt/" .. topic end +--- @param topic string +--- @return string local function mqtt_automation(topic) return "automation/" .. topic end @@ -30,6 +34,7 @@ local mqtt_client = require("automation:mqtt").new({ tls = host == "zeus" or host == "hephaestus", }) +--- @type Ntfy local ntfy = devices.Ntfy.new({ topic = secrets.ntfy_topic, }) @@ -362,6 +367,7 @@ local workbench_outlet = devices.OutletOnOff.new({ turn_off_when_away(workbench_outlet) device_manager:add(workbench_outlet) +--- @type LightColorTemperature local workbench_light = devices.LightColorTemperature.new({ name = "Light", room = "Workbench", diff --git a/definitions.bak/automation:devices.lua.bak b/definitions.bak/automation:devices.lua.bak new file mode 100644 index 0000000..d629c6c --- /dev/null +++ b/definitions.bak/automation:devices.lua.bak @@ -0,0 +1,42 @@ +---@meta + +local devices + +---@class Action +---@field action +---| "broadcast" +---| "view" +---@field extras table | nil +---@field label string | nil +---@field clear boolean|nil + +---@alias Priority +---| "min" +---| "low" +---| "default" +---| "high" +---| "max" + +---@class Notification +---@field title string +---@field message string | nil +-- NOTE: It might be possible to specify this down to the actual possible values +---@field tags string[] | nil +---@field priority Priority | nil +---@field actions Action[] | nil + +---@class Ntfy +local Ntfy +---@async +---@param notification Notification +function Ntfy:send_notification(notification) end + +---@class NtfyConfig +---@field topic string + +devices.Ntfy = {} +---@param config NtfyConfig +---@return Ntfy +function devices.Ntfy.new(config) end + +return devices diff --git a/definitions.bak/automation:secrets.lua.bak b/definitions.bak/automation:secrets.lua.bak new file mode 100644 index 0000000..6494943 --- /dev/null +++ b/definitions.bak/automation:secrets.lua.bak @@ -0,0 +1,6 @@ +---@meta + +---@type table +local secrets + +return secrets diff --git a/definitions.bak/automation:utils.lua.bak b/definitions.bak/automation:utils.lua.bak new file mode 100644 index 0000000..d8b380c --- /dev/null +++ b/definitions.bak/automation:utils.lua.bak @@ -0,0 +1,27 @@ +---@meta + +local utils + +---@class Timeout +local Timeout +---@async +---@param timeout number +---@param callback fun() +function Timeout:start(timeout, callback) end +---@async +function Timeout:cancel() end +---@async +---@return boolean +function Timeout:is_waiting() end + +utils.Timeout = {} +---@return Timeout +function utils.Timeout.new() end + +--- @return string hostname +function utils.get_hostname() end + +--- @return number epoch +function utils.get_epoch() end + +return utils diff --git a/definitions.bak/automation:variables.lua.bak b/definitions.bak/automation:variables.lua.bak new file mode 100644 index 0000000..6f09c6d --- /dev/null +++ b/definitions.bak/automation:variables.lua.bak @@ -0,0 +1,6 @@ +---@meta + +---@type table +local variables + +return variables diff --git a/definitions/automation:devices.lua b/definitions/automation:devices.lua new file mode 100644 index 0000000..2ee533f --- /dev/null +++ b/definitions/automation:devices.lua @@ -0,0 +1,156 @@ +---@meta + +local devices + +---@class Washer +local Washer + +---@class IkeaRemote +local IkeaRemote + +---@class OutletPower +local OutletPower +---@async +---@param on boolean +function OutletPower:set_on(on) end +---@async +---@return boolean +function OutletPower:on() end + +---@class OutletOnOff +local OutletOnOff +---@async +---@param on boolean +function OutletOnOff:set_on(on) end +---@async +---@return boolean +function OutletOnOff:on() end + +---@class KasaOutlet +local KasaOutlet +---@async +---@param on boolean +function KasaOutlet:set_on(on) end +---@async +---@return boolean +function KasaOutlet:on() end + +---@class Presence +local Presence + +---@class HueSwitch +local HueSwitch + +---@class ContactSensor +local ContactSensor +---@async +---@param open_percent integer +function ContactSensor:set_open_percent(open_percent) end +---@async +---@return integer +function ContactSensor:open_percent() end + +---@class HueGroup +local HueGroup +---@async +---@param on boolean +function HueGroup:set_on(on) end +---@async +---@return boolean +function HueGroup:on() end + +---@class Ntfy +local Ntfy + +---@alias Priority +---| "min" +---| "low" +---| "default" +---| "high" +---| "max" + +---@class Notification +---@field title string +---@field message string? +---@field tags string[]? +---@field priority Priority? +---@field actions Action[]? +local Notification + +---@class NtfyConfig +---@field url string +---@field topic string +local NtfyConfig + +---@class Action +---@field action +---| "broadcast" +---@field extras table? +---@field label string +---@field clear boolean? +local Action + +---@class LightSensor +local LightSensor + +---@class LightColorTemperature +local LightColorTemperature +---@async +---@param on boolean +function LightColorTemperature:set_on(on) end +---@async +---@return boolean +function LightColorTemperature:on() end +---@async +---@param brightness integer +function LightColorTemperature:set_brightness(brightness) end +---@async +---@return integer +function LightColorTemperature:brightness() end +---@async +---@param temperature integer +function LightColorTemperature:set_color_temperature(temperature) end +---@async +---@return integer +function LightColorTemperature:color_temperature() end + +---@class LightOnOff +local LightOnOff +---@async +---@param on boolean +function LightOnOff:set_on(on) end +---@async +---@return boolean +function LightOnOff:on() end + +---@class LightBrightness +local LightBrightness +---@async +---@param on boolean +function LightBrightness:set_on(on) end +---@async +---@return boolean +function LightBrightness:on() end +---@async +---@param brightness integer +function LightBrightness:set_brightness(brightness) end +---@async +---@return integer +function LightBrightness:brightness() end + +---@class HueBridge +local HueBridge + +---@class AirFilter +local AirFilter +---@async +---@param on boolean +function AirFilter:set_on(on) end +---@async +---@return boolean +function AirFilter:on() end + +---@class WakeOnLAN +local WakeOnLAN + +return devices diff --git a/src/bin/generate_definitions.rs b/src/bin/generate_definitions.rs new file mode 100644 index 0000000..eee496d --- /dev/null +++ b/src/bin/generate_definitions.rs @@ -0,0 +1,3 @@ +fn main() { + automation_devices::generate_definitions() +}