diff --git a/Cargo.lock b/Cargo.lock index accc16c..4d9dcd9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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#d2de4b5830c53feebb0d06c987f93a22fa6f3b3d" +dependencies = [ + "eui48", + "lua_typed_macro", +] + +[[package]] +name = "lua_typed_macro" +version = "0.1.0" +source = "git+https://git.huizinga.dev/Dreaded_X/lua_typed#d2de4b5830c53feebb0d06c987f93a22fa6f3b3d" +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" 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/air_filter.rs b/automation_devices/src/air_filter.rs index 5cfe965..a95cf5f 100644 --- a/automation_devices/src/air_filter.rs +++ b/automation_devices/src/air_filter.rs @@ -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)] + #[serde(flatten)] pub info: InfoConfig, pub url: String, } +crate::register_type!(Config); #[derive(Debug, Clone, Device)] #[device(traits(OnOff))] diff --git a/automation_devices/src/contact_sensor.rs b/automation_devices/src/contact_sensor.rs index 3da27ae..1a86391 100644 --- a/automation_devices/src/contact_sensor.rs +++ b/automation_devices/src/contact_sensor.rs @@ -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)] + #[serde(flatten)] pub info: InfoConfig, #[device_config(flatten)] + #[serde(flatten)] pub mqtt: MqttDeviceConfig, #[device_config(default(SensorType::Window))] + #[serde(default)] pub sensor_type: SensorType, #[device_config(from_lua, default)] + #[serde(default)] pub callback: ActionCallback<(ContactSensor, bool)>, #[device_config(from_lua, default)] + #[serde(default)] pub battery_callback: ActionCallback<(ContactSensor, f32)>, #[device_config(from_lua)] + #[serde(default)] pub client: WrappedAsyncClient, } +crate::register_type!(Config); #[derive(Debug)] struct State { diff --git a/automation_devices/src/hue_bridge.rs b/automation_devices/src/hue_bridge.rs index d7d2e25..b2bc1e0 100644 --- a/automation_devices/src/hue_bridge.rs +++ b/automation_devices/src/hue_bridge.rs @@ -4,6 +4,7 @@ use std::net::SocketAddr; 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 tracing::{error, trace, warn}; @@ -15,20 +16,24 @@ pub enum Flag { Darkness, } -#[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))] diff --git a/automation_devices/src/hue_group.rs b/automation_devices/src/hue_group.rs index a6cdad8..e2ae5d2 100644 --- a/automation_devices/src/hue_group.rs +++ b/automation_devices/src/hue_group.rs @@ -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))] diff --git a/automation_devices/src/hue_switch.rs b/automation_devices/src/hue_switch.rs index e52a1e9..ee068d5 100644 --- a/automation_devices/src/hue_switch.rs +++ b/automation_devices/src/hue_switch.rs @@ -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)] + #[serde(flatten)] pub info: InfoConfig, #[device_config(flatten)] + #[serde(flatten)] pub mqtt: MqttDeviceConfig, #[device_config(from_lua)] pub client: WrappedAsyncClient, #[device_config(from_lua, default)] + #[serde(default)] pub left_callback: ActionCallback, #[device_config(from_lua, default)] + #[serde(default)] pub right_callback: ActionCallback, #[device_config(from_lua, default)] + #[serde(default)] pub left_hold_callback: ActionCallback, #[device_config(from_lua, default)] + #[serde(default)] pub right_hold_callback: ActionCallback, #[device_config(from_lua, default)] + #[serde(default)] pub battery_callback: ActionCallback<(HueSwitch, f32)>, } +crate::register_type!(Config); #[derive(Debug, Copy, Clone, Deserialize)] #[serde(rename_all = "snake_case")] diff --git a/automation_devices/src/ikea_remote.rs b/automation_devices/src/ikea_remote.rs index 2359a52..652008c 100644 --- a/automation_devices/src/ikea_remote.rs +++ b/automation_devices/src/ikea_remote.rs @@ -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)] + #[serde(flatten)] pub info: InfoConfig, #[device_config(default)] + #[serde(default)] pub single_button: bool, #[device_config(flatten)] + #[serde(flatten)] pub mqtt: MqttDeviceConfig, #[device_config(from_lua)] pub client: WrappedAsyncClient, #[device_config(from_lua, default)] + #[serde(default)] pub callback: ActionCallback<(IkeaRemote, bool)>, #[device_config(from_lua, default)] + #[serde(default)] pub battery_callback: ActionCallback<(IkeaRemote, f32)>, } +crate::register_type!(Config); #[derive(Debug, Clone, Device)] pub struct IkeaRemote { diff --git a/automation_devices/src/kasa_outlet.rs b/automation_devices/src/kasa_outlet.rs index e812cb1..02f480f 100644 --- a/automation_devices/src/kasa_outlet.rs +++ b/automation_devices/src/kasa_outlet.rs @@ -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))] 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/light_sensor.rs b/automation_devices/src/light_sensor.rs index d241c79..7fe667a 100644 --- a/automation_devices/src/light_sensor.rs +++ b/automation_devices/src/light_sensor.rs @@ -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)] + #[serde(flatten)] pub mqtt: MqttDeviceConfig, pub min: isize, pub max: isize, #[device_config(from_lua, default)] + #[serde(default)] pub callback: ActionCallback<(LightSensor, bool)>, #[device_config(from_lua)] pub client: WrappedAsyncClient, } +crate::register_type!(Config); const DEFAULT: bool = false; diff --git a/automation_devices/src/ntfy.rs b/automation_devices/src/ntfy.rs index 78c7ba8..fc3a915 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,34 +20,37 @@ 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 { #[serde(skip_serializing_if = "HashMap::is_empty")] + #[serde(default)] extras: HashMap, }, // View, // 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 +61,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 +72,15 @@ impl Notification { } } -#[derive(Debug, Clone, LuaDeviceConfig)] +#[derive(Debug, Clone, LuaDeviceConfig, Typed)] +#[typed(as = "NtfyConfig")] pub struct Config { #[device_config(default("https://ntfy.sh".into()))] + #[serde(default)] pub url: String, pub topic: String, } +crate::register_type!(Config); #[derive(Debug, Clone, Device)] #[device(add_methods(Self::add_methods))] @@ -96,6 +104,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_devices/src/presence.rs b/automation_devices/src/presence.rs index a765284..aeee3d2 100644 --- a/automation_devices/src/presence.rs +++ b/automation_devices/src/presence.rs @@ -9,21 +9,26 @@ use automation_lib::event::OnMqtt; 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)] + #[serde(flatten)] pub mqtt: MqttDeviceConfig, #[device_config(from_lua, default)] + #[serde(default)] pub callback: ActionCallback<(Presence, bool)>, #[device_config(from_lua)] pub client: WrappedAsyncClient, } +crate::register_type!(Config); pub const DEFAULT_PRESENCE: bool = false; diff --git a/automation_devices/src/wake_on_lan.rs b/automation_devices/src/wake_on_lan.rs index 0282111..e80fb62 100644 --- a/automation_devices/src/wake_on_lan.rs +++ b/automation_devices/src/wake_on_lan.rs @@ -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)] + #[serde(flatten)] pub info: InfoConfig, #[device_config(flatten)] + #[serde(flatten)] pub mqtt: MqttDeviceConfig, pub mac_address: MacAddress, #[device_config(default(Ipv4Addr::new(255, 255, 255, 255)))] + #[serde(default)] pub broadcast_ip: Ipv4Addr, #[device_config(from_lua)] pub client: WrappedAsyncClient, } +crate::register_type!(Config); #[derive(Debug, Clone, Device)] pub struct WakeOnLAN { diff --git a/automation_devices/src/washer.rs b/automation_devices/src/washer.rs index bf821a0..74d0373 100644 --- a/automation_devices/src/washer.rs +++ b/automation_devices/src/washer.rs @@ -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)] + #[serde(flatten)] pub mqtt: MqttDeviceConfig, // Power in Watt pub threshold: f32, #[device_config(from_lua, default)] + #[serde(default)] pub done_callback: ActionCallback, #[device_config(from_lua)] pub client: WrappedAsyncClient, } +crate::register_type!(Config); #[derive(Debug)] pub struct State { diff --git a/automation_devices/src/zigbee/light.rs b/automation_devices/src/zigbee/light.rs index d5ba0ed..bd3e850 100644 --- a/automation_devices/src/zigbee/light.rs +++ b/automation_devices/src/zigbee/light.rs @@ -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; @@ -40,6 +41,13 @@ pub struct Config { pub client: WrappedAsyncClient, } +// TODO: Fix the derive macro so this is supported +impl Typed for Config { + fn type_name() -> String { + "LightConfig".into() + } +} + #[derive(Debug, Clone, Default, Serialize, Deserialize, LuaSerialize)] pub struct StateOnOff { #[serde(deserialize_with = "state_deserializer")] diff --git a/automation_devices/src/zigbee/outlet.rs b/automation_devices/src/zigbee/outlet.rs index ddf640d..44cbe7a 100644 --- a/automation_devices/src/zigbee/outlet.rs +++ b/automation_devices/src/zigbee/outlet.rs @@ -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; @@ -57,6 +58,13 @@ pub struct Config { pub client: WrappedAsyncClient, } +// TODO: Fix the derive macro so this is supported +impl Typed for Config { + fn type_name() -> String { + "OutletConfig".into() + } +} + #[derive(Debug, Clone, Default, Serialize, Deserialize, LuaSerialize)] pub struct StateOnOff { #[serde(deserialize_with = "state_deserializer")] diff --git a/automation_lib/Cargo.toml b/automation_lib/Cargo.toml index cfc48ee..15b28aa 100644 --- a/automation_lib/Cargo.toml +++ b/automation_lib/Cargo.toml @@ -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 } diff --git a/automation_lib/src/action_callback.rs b/automation_lib/src/action_callback.rs index 2b9479e..d1eb384 100644 --- a/automation_lib/src/action_callback.rs +++ b/automation_lib/src/action_callback.rs @@ -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

{ _parameters: PhantomData

, } +impl Typed for ActionCallback { + fn type_name() -> String { + let type_name = A::type_name(); + format!("fun(_: {type_name}) | fun(_: {type_name})[]") + } +} + +impl 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. diff --git a/automation_lib/src/config.rs b/automation_lib/src/config.rs index 42bee56..4b3b334 100644 --- a/automation_lib/src/config.rs +++ b/automation_lib/src/config.rs @@ -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, @@ -68,7 +69,7 @@ impl InfoConfig { } } -#[derive(Debug, Clone, Deserialize)] +#[derive(Debug, Clone, Deserialize, Typed)] pub struct MqttDeviceConfig { pub topic: String, } diff --git a/automation_lib/src/lib.rs b/automation_lib/src/lib.rs index e2a3bbf..359d71a 100644 --- a/automation_lib/src/lib.rs +++ b/automation_lib/src/lib.rs @@ -1,5 +1,5 @@ -#![allow(incomplete_features)] #![feature(iterator_try_collect)] +#![feature(with_negative_coherence)] use tracing::debug; diff --git a/automation_lib/src/lua/traits.rs b/automation_lib/src/lua/traits.rs index 4e1c724..720cd6f 100644 --- a/automation_lib/src/lua/traits.rs +++ b/automation_lib/src/lua/traits.rs @@ -1,11 +1,18 @@ +use std::marker::PhantomData; use std::ops::Deref; +use lua_typed::Typed; + // 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 + 'static, { methods.add_async_method("set_on", async |_lua, this, on: bool| { this.deref().set_on(on).await.unwrap(); @@ -17,13 +24,27 @@ 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() -> String { + let type_name = T::type_name(); + let mut output = String::new(); + + output += + &format!("---@async\n---@param on boolean\nfunction {type_name}:set_on(on) end\n"); + output += &format!("---@async\n---@return boolean\nfunction {type_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 + 'static, { methods.add_async_method("set_brightness", async |_lua, this, brightness: u8| { this.set_brightness(brightness).await.unwrap(); @@ -35,13 +56,29 @@ 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() -> String { + let type_name = T::type_name(); + let mut output = String::new(); + + output += &format!( + "---@async\n---@param brightness integer\nfunction {type_name}:set_brightness(brightness) end\n" + ); + output += + &format!("---@async\n---@return integer\nfunction {type_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 + 'static, { methods.add_async_method( "set_color_temperature", @@ -58,13 +95,30 @@ 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() -> String { + let type_name = T::type_name(); + let mut output = String::new(); + + output += &format!( + "---@async\n---@param temperature integer\nfunction {type_name}:set_color_temperature(temperature) end\n" + ); + output += &format!( + "---@async\n---@return integer\nfunction {type_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 + 'static, { methods.add_async_method("set_open_percent", async |_lua, this, open_percent: u8| { this.set_open_percent(open_percent).await.unwrap(); @@ -76,5 +130,17 @@ pub trait OpenClose { Ok(this.open_percent().await.unwrap()) }); } + + pub fn generate_definitions() -> String { + let type_name = T::type_name(); + 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_lib/src/mqtt.rs b/automation_lib/src/mqtt.rs index fc19fba..3fb7d4c 100644 --- a/automation_lib/src/mqtt.rs +++ b/automation_lib/src/mqtt.rs @@ -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; diff --git a/automation_macro/src/device.rs b/automation_macro/src/device.rs index b886436..0fc78fb 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(); )* }); } @@ -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,39 @@ 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 mut output = String::new(); + + #traits_generate_definitions + + let type_name = ::type_name(); + output += &format!("devices.{type_name} = {{}}\n"); + let config_name = <::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"); + + Some(output) + } + } }); } } diff --git a/config.lua b/config.lua index f3cdc5b..ab5e903 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 @@ -72,6 +76,7 @@ local on_presence = { end, } +--- @type Presence local presence_system = devices.Presence.new({ topic = mqtt_automation("presence/+/#"), client = mqtt_client, @@ -272,6 +277,7 @@ local function kettle_timeout() end end +--- @type OutletPower local kettle = devices.OutletPower.new({ outlet_type = "Kettle", name = "Kettle", @@ -362,6 +368,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..34928a2 --- /dev/null +++ b/definitions/automation:devices.lua @@ -0,0 +1,324 @@ +---@meta + +local devices + +---@class OutletOnOff +local OutletOnOff +---@async +---@param on boolean +function OutletOnOff:set_on(on) end +---@async +---@return boolean +function OutletOnOff:on() end +devices.OutletOnOff = {} +---@param config OutletConfig +---@return OutletOnOff +function devices.OutletOnOff.new(config) end + +---@class OutletPower +local OutletPower +---@async +---@param on boolean +function OutletPower:set_on(on) end +---@async +---@return boolean +function OutletPower:on() end +devices.OutletPower = {} +---@param config OutletConfig +---@return OutletPower +function devices.OutletPower.new(config) end + +---@class AirFilterConfig +---@field name string +---@field room string? +---@field url string +local AirFilterConfig + +---@class AirFilter +local AirFilter +---@async +---@param on boolean +function AirFilter:set_on(on) end +---@async +---@return boolean +function AirFilter:on() end +devices.AirFilter = {} +---@param config AirFilterConfig +---@return AirFilter +function devices.AirFilter.new(config) end + +---@class PresenceConfig +---@field topic string +---@field callback fun(_: Presence, _: boolean) | fun(_: Presence, _: boolean)[]? +---@field client AsyncClient +local PresenceConfig + +---@class Presence +local Presence +devices.Presence = {} +---@param config PresenceConfig +---@return Presence +function devices.Presence.new(config) end + +---@class WolConfig +---@field name string +---@field room string? +---@field topic string +---@field mac_address string +---@field broadcast_ip string? +---@field client AsyncClient +local WolConfig + +---@class WakeOnLAN +local WakeOnLAN +devices.WakeOnLAN = {} +---@param config WolConfig +---@return WakeOnLAN +function devices.WakeOnLAN.new(config) end + +---@class LightSensor +local LightSensor +devices.LightSensor = {} +---@param config LightSensorConfig +---@return LightSensor +function devices.LightSensor.new(config) end + +---@class LightSensorConfig +---@field identifier string +---@field topic string +---@field min integer +---@field max integer +---@field callback fun(_: LightSensor, _: boolean) | fun(_: LightSensor, _: boolean)[]? +---@field client AsyncClient +local LightSensorConfig + +---@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 +devices.ContactSensor = {} +---@param config ContactSensorConfig +---@return ContactSensor +function devices.ContactSensor.new(config) end + +---@alias SensorType +---| "Door" +---| "Drawer" +---| "Window" + +---@class ContactSensorConfig +---@field name string +---@field room string? +---@field topic string +---@field sensor_type SensorType? +---@field callback fun(_: ContactSensor, _: boolean) | fun(_: ContactSensor, _: boolean)[]? +---@field battery_callback fun(_: ContactSensor, _: number) | fun(_: ContactSensor, _: number)[]? +---@field client AsyncClient? +local ContactSensorConfig + +---@class KasaOutlet +local KasaOutlet +---@async +---@param on boolean +function KasaOutlet:set_on(on) end +---@async +---@return boolean +function KasaOutlet:on() end +devices.KasaOutlet = {} +---@param config KasaOutletConfig +---@return KasaOutlet +function devices.KasaOutlet.new(config) end + +---@class KasaOutletConfig +---@field identifier string +---@field ip string +local KasaOutletConfig + +---@class LightOnOff +local LightOnOff +---@async +---@param on boolean +function LightOnOff:set_on(on) end +---@async +---@return boolean +function LightOnOff:on() end +devices.LightOnOff = {} +---@param config LightConfig +---@return LightOnOff +function devices.LightOnOff.new(config) end + +---@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 +devices.LightColorTemperature = {} +---@param config LightConfig +---@return LightColorTemperature +function devices.LightColorTemperature.new(config) 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 +devices.LightBrightness = {} +---@param config LightConfig +---@return LightBrightness +function devices.LightBrightness.new(config) end + +---@class HueSwitch +local HueSwitch +devices.HueSwitch = {} +---@param config HueSwitchConfig +---@return HueSwitch +function devices.HueSwitch.new(config) end + +---@class HueSwitchConfig +---@field name string +---@field room string? +---@field topic string +---@field client AsyncClient +---@field left_callback fun(_: HueSwitch) | fun(_: HueSwitch)[]? +---@field right_callback fun(_: HueSwitch) | fun(_: HueSwitch)[]? +---@field left_hold_callback fun(_: HueSwitch) | fun(_: HueSwitch)[]? +---@field right_hold_callback fun(_: HueSwitch) | fun(_: HueSwitch)[]? +---@field battery_callback fun(_: HueSwitch, _: number) | fun(_: HueSwitch, _: number)[]? +local HueSwitchConfig + +---@class HueBridge +local HueBridge +devices.HueBridge = {} +---@param config HueBridgeConfig +---@return HueBridge +function devices.HueBridge.new(config) end + +---@class HueBridgeConfig +---@field identifier string +---@field ip string +---@field login string +---@field flags FlagIDs +local HueBridgeConfig + +---@class FlagIDs +---@field presence integer +---@field darkness integer +local FlagIDs + +---@class IkeaRemoteConfig +---@field name string +---@field room string? +---@field single_button boolean? +---@field topic string +---@field client AsyncClient +---@field callback fun(_: IkeaRemote, _: boolean) | fun(_: IkeaRemote, _: boolean)[]? +---@field battery_callback fun(_: IkeaRemote, _: number) | fun(_: IkeaRemote, _: number)[]? +local IkeaRemoteConfig + +---@class IkeaRemote +local IkeaRemote +devices.IkeaRemote = {} +---@param config IkeaRemoteConfig +---@return IkeaRemote +function devices.IkeaRemote.new(config) end + +---@alias Priority +---| "min" +---| "low" +---| "default" +---| "high" +---| "max" + +---@class Ntfy +local Ntfy +devices.Ntfy = {} +---@param config NtfyConfig +---@return Ntfy +function devices.Ntfy.new(config) end + +---@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 Notification +---@field title string +---@field message string? +---@field tags string[]? +---@field priority Priority? +---@field actions Action[]? +local Notification + +---@class WasherConfig +---@field identifier string +---@field topic string +---@field threshold number +---@field done_callback fun(_: Washer) | fun(_: Washer)[]? +---@field client AsyncClient +local WasherConfig + +---@class Washer +local Washer +devices.Washer = {} +---@param config WasherConfig +---@return Washer +function devices.Washer.new(config) end + +---@class HueGroupConfig +---@field identifier string +---@field ip string +---@field login string +---@field group_id integer +---@field scene_id string +local HueGroupConfig + +---@class HueGroup +local HueGroup +---@async +---@param on boolean +function HueGroup:set_on(on) end +---@async +---@return boolean +function HueGroup:on() end +devices.HueGroup = {} +---@param config HueGroupConfig +---@return HueGroup +function devices.HueGroup.new(config) end + +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() +}