WIP
All checks were successful
Build and deploy / build (push) Successful in 11m20s
Build and deploy / Deploy container (push) Has been skipped

This commit is contained in:
2025-09-18 01:45:31 +02:00
parent 06b3154733
commit df45d4c489
14 changed files with 464 additions and 36 deletions

35
Cargo.lock generated
View File

@@ -128,6 +128,7 @@ dependencies = [
"eui48", "eui48",
"google_home", "google_home",
"inventory", "inventory",
"lua_typed",
"mlua", "mlua",
"reqwest", "reqwest",
"rumqttc", "rumqttc",
@@ -345,6 +346,15 @@ dependencies = [
"winnow", "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]] [[package]]
name = "core-foundation" name = "core-foundation"
version = "0.9.4" version = "0.9.4"
@@ -1094,6 +1104,25 @@ dependencies = [
"cc", "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]] [[package]]
name = "luajit-src" name = "luajit-src"
version = "210.6.1+f9140a6" version = "210.6.1+f9140a6"
@@ -2256,6 +2285,12 @@ version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "unicode-segmentation"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]] [[package]]
name = "untrusted" name = "untrusted"
version = "0.9.0" version = "0.9.0"

View File

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

View File

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

View File

@@ -22,20 +22,22 @@ macro_rules! register_device {
stringify!($device), stringify!($device),
::mlua::Lua::create_proxy::<$device> ::mlua::Lua::create_proxy::<$device>
)); ));
crate::register_type!($device);
}; };
} }
pub(crate) use register_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 { pub struct RegisteredDevice {
name: &'static str, name: &'static str,
register_fn: RegisterFn, register_fn: RegisterDeviceFn,
} }
impl RegisteredDevice { 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 } Self { name, register_fn }
} }
@@ -64,3 +66,28 @@ pub fn create_module(lua: &mlua::Lua) -> mlua::Result<mlua::Table> {
} }
inventory::submit! {Module::new("devices", create_module)} 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

@@ -4,12 +4,13 @@ use std::convert::Infallible;
use async_trait::async_trait; use async_trait::async_trait;
use automation_lib::device::{Device, LuaDeviceCreate}; use automation_lib::device::{Device, LuaDeviceCreate};
use automation_macro::{Device, LuaDeviceConfig}; use automation_macro::{Device, LuaDeviceConfig};
use lua_typed::Typed;
use mlua::LuaSerdeExt; use mlua::LuaSerdeExt;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_repr::*; use serde_repr::*;
use tracing::{error, trace, warn}; use tracing::{error, trace, warn};
#[derive(Debug, Serialize_repr, Deserialize, Clone, Copy)] #[derive(Debug, Serialize_repr, Deserialize, Clone, Copy, Typed)]
#[repr(u8)] #[repr(u8)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum Priority { pub enum Priority {
@@ -19,8 +20,9 @@ pub enum Priority {
High, High,
Max, Max,
} }
crate::register_type!(Priority);
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone, Typed)]
#[serde(rename_all = "snake_case", tag = "action")] #[serde(rename_all = "snake_case", tag = "action")]
pub enum ActionType { pub enum ActionType {
Broadcast { Broadcast {
@@ -31,22 +33,23 @@ pub enum ActionType {
// Http // Http
} }
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone, Typed)]
pub struct Action { pub struct Action {
#[serde(flatten)] #[serde(flatten)]
pub action: ActionType, pub action: ActionType,
pub label: String, pub label: String,
pub clear: Option<bool>, pub clear: Option<bool>,
} }
crate::register_type!(Action);
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize, Typed)]
struct NotificationFinal { struct NotificationFinal {
topic: String, topic: String,
#[serde(flatten)] #[serde(flatten)]
inner: Notification, inner: Notification,
} }
#[derive(Debug, Serialize, Clone, Deserialize)] #[derive(Debug, Serialize, Clone, Deserialize, Typed)]
pub struct Notification { pub struct Notification {
title: String, title: String,
message: Option<String>, message: Option<String>,
@@ -57,6 +60,7 @@ pub struct Notification {
#[serde(skip_serializing_if = "Vec::is_empty", default = "Default::default")] #[serde(skip_serializing_if = "Vec::is_empty", default = "Default::default")]
actions: Vec<Action>, actions: Vec<Action>,
} }
crate::register_type!(Notification);
impl Notification { impl Notification {
fn finalize(self, topic: &str) -> NotificationFinal { 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 { pub struct Config {
#[device_config(default("https://ntfy.sh".into()))] #[device_config(default("https://ntfy.sh".into()))]
pub url: String, pub url: String,
pub topic: String, pub topic: String,
} }
crate::register_type!(Config);
#[derive(Debug, Clone, Device)] #[derive(Debug, Clone, Device)]
#[device(add_methods(Self::add_methods))] #[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<String> {
// let type_name = <Self as Typed>::type_name();
// Some(format!("---@class {type_name}\nlocal {type_name}\n"))
// }
//
// fn generate_members() -> Option<String> {
// Some(format!(
// "---@async\n---@param notification Notification\nfunction {}:send_notification(notification) end\n",
// <Self as Typed>::type_name(),
// ))
// }
// }
#[async_trait] #[async_trait]
impl LuaDeviceCreate for Ntfy { impl LuaDeviceCreate for Ntfy {
type Config = Config; type Config = Config;

View File

@@ -1,11 +1,16 @@
use std::marker::PhantomData;
use std::ops::Deref; use std::ops::Deref;
// TODO: Enable and disable functions based on query_only and command_only // TODO: Enable and disable functions based on query_only and command_only
pub trait OnOff { pub struct OnOff<T: google_home::traits::OnOff> {
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) _phantom: PhantomData<T>,
}
impl<T: google_home::traits::OnOff> OnOff<T> {
pub fn add_methods<M: mlua::UserDataMethods<T>>(methods: &mut M)
where 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| { methods.add_async_method("set_on", async |_lua, this, on: bool| {
this.deref().set_on(on).await.unwrap(); this.deref().set_on(on).await.unwrap();
@@ -17,13 +22,25 @@ pub trait OnOff {
Ok(this.deref().on().await.unwrap()) Ok(this.deref().on().await.unwrap())
}); });
} }
}
impl<T> OnOff for T where T: google_home::traits::OnOff {}
pub trait Brightness { pub fn generate_definitions(name: &str) -> String {
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) 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<T: google_home::traits::Brightness> {
_phantom: PhantomData<T>,
}
impl<T: google_home::traits::Brightness> Brightness<T> {
pub fn add_methods<M: mlua::UserDataMethods<T>>(methods: &mut M)
where 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| { methods.add_async_method("set_brightness", async |_lua, this, brightness: u8| {
this.set_brightness(brightness).await.unwrap(); this.set_brightness(brightness).await.unwrap();
@@ -35,13 +52,27 @@ pub trait Brightness {
Ok(this.brightness().await.unwrap()) Ok(this.brightness().await.unwrap())
}); });
} }
}
impl<T> Brightness for T where T: google_home::traits::Brightness {}
pub trait ColorSetting { pub fn generate_definitions(name: &str) -> String {
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) 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<T: google_home::traits::ColorSetting> {
_phantom: PhantomData<T>,
}
impl<T: google_home::traits::ColorSetting> ColorSetting<T> {
pub fn add_methods<M: mlua::UserDataMethods<T>>(methods: &mut M)
where where
Self: Sized + google_home::traits::ColorSetting + 'static, T: Sized + google_home::traits::ColorSetting + 'static,
{ {
methods.add_async_method( methods.add_async_method(
"set_color_temperature", "set_color_temperature",
@@ -58,13 +89,28 @@ pub trait ColorSetting {
Ok(this.color().await.temperature) Ok(this.color().await.temperature)
}); });
} }
}
impl<T> ColorSetting for T where T: google_home::traits::ColorSetting {}
pub trait OpenClose { pub fn generate_definitions(name: &str) -> String {
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) 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<T: google_home::traits::OpenClose> {
_phantom: PhantomData<T>,
}
impl<T: google_home::traits::OpenClose> OpenClose<T> {
pub fn add_methods<M: mlua::UserDataMethods<T>>(methods: &mut M)
where 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| { methods.add_async_method("set_open_percent", async |_lua, this, open_percent: u8| {
this.set_open_percent(open_percent).await.unwrap(); this.set_open_percent(open_percent).await.unwrap();
@@ -76,5 +122,16 @@ pub trait OpenClose {
Ok(this.open_percent().await.unwrap()) 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<T> OpenClose for T where T: google_home::traits::OpenClose {}

View File

@@ -47,7 +47,7 @@ impl Parse for TraitAttr {
} }
} }
#[derive(Default)] #[derive(Default, Clone)]
struct Traits(Vec<syn::Ident>); struct Traits(Vec<syn::Ident>);
impl Traits { 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) { fn to_tokens(&self, tokens: &mut TokenStream2) {
let Self(traits) = &self; let Self(Traits(traits)) = &self;
tokens.extend(quote! { tokens.extend(quote! {
#( #(
::automation_lib::lua::traits::#traits::add_methods(methods); ::automation_lib::lua::traits::#traits::<Self>::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::<Self>::generate_definitions(&type_name);
)* )*
}); });
} }
@@ -138,9 +152,12 @@ impl quote::ToTokens for Implementation {
add_methods, add_methods,
} = &self; } = &self;
let traits_add_methods = TraitsAddMethods(traits.clone());
let traits_generate_definitions = TraitsGenerateDefinitions(traits.clone());
tokens.extend(quote! { tokens.extend(quote! {
impl mlua::UserData for #name { impl ::mlua::UserData for #name {
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) { fn add_methods<M: ::mlua::UserDataMethods<Self>>(methods: &mut M) {
methods.add_async_function("new", async |_lua, config| { methods.add_async_function("new", async |_lua, config| {
let device: Self = LuaDeviceCreate::create(config) let device: Self = LuaDeviceCreate::create(config)
.await .await
@@ -156,13 +173,33 @@ impl quote::ToTokens for Implementation {
methods.add_async_method("get_id", async |_lua, this, _: ()| { Ok(this.get_id()) }); methods.add_async_method("get_id", async |_lua, this, _: ()| { Ok(this.get_id()) });
#traits #traits_add_methods
#( #(
#add_methods(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 = <Self as ::lua_typed::Typed>::type_name();
Some(format!("---@class {type_name}\nlocal {type_name}\n"))
}
fn generate_members() -> Option<String> {
let type_name = <Self as ::lua_typed::Typed>::type_name();
let mut output = String::new();
#traits_generate_definitions
Some(output)
}
}
}); });
} }
} }

View File

@@ -2,17 +2,21 @@ local devices = require("automation:devices")
local device_manager = require("automation:device_manager") local device_manager = require("automation:device_manager")
local utils = require("automation:utils") local utils = require("automation:utils")
local secrets = require("automation:secrets") 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) print(_VERSION)
local host = utils.get_hostname() local host = utils.get_hostname()
print("Running @" .. host) print("Running @" .. host)
--- @param topic string
--- @return string
local function mqtt_z2m(topic) local function mqtt_z2m(topic)
return "zigbee2mqtt/" .. topic return "zigbee2mqtt/" .. topic
end end
--- @param topic string
--- @return string
local function mqtt_automation(topic) local function mqtt_automation(topic)
return "automation/" .. topic return "automation/" .. topic
end end
@@ -30,6 +34,7 @@ local mqtt_client = require("automation:mqtt").new({
tls = host == "zeus" or host == "hephaestus", tls = host == "zeus" or host == "hephaestus",
}) })
--- @type Ntfy
local ntfy = devices.Ntfy.new({ local ntfy = devices.Ntfy.new({
topic = secrets.ntfy_topic, topic = secrets.ntfy_topic,
}) })
@@ -362,6 +367,7 @@ local workbench_outlet = devices.OutletOnOff.new({
turn_off_when_away(workbench_outlet) turn_off_when_away(workbench_outlet)
device_manager:add(workbench_outlet) device_manager:add(workbench_outlet)
--- @type LightColorTemperature
local workbench_light = devices.LightColorTemperature.new({ local workbench_light = devices.LightColorTemperature.new({
name = "Light", name = "Light",
room = "Workbench", room = "Workbench",

View File

@@ -0,0 +1,42 @@
---@meta
local devices
---@class Action
---@field action
---| "broadcast"
---| "view"
---@field extras table<string, string> | 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

View File

@@ -0,0 +1,6 @@
---@meta
---@type table<string, string|nil>
local secrets
return secrets

View File

@@ -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

View File

@@ -0,0 +1,6 @@
---@meta
---@type table<string, string|nil>
local variables
return variables

View File

@@ -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<string, string>?
---@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

View File

@@ -0,0 +1,3 @@
fn main() {
automation_devices::generate_definitions()
}