Compare commits

...

2 Commits

Author SHA1 Message Date
2540b32902 feat: WIP
All checks were successful
Build and deploy / build (push) Successful in 9m48s
Build and deploy / Deploy container (push) Has been skipped
2025-09-17 00:38:23 +02:00
06b3154733 feat!: Use type alias instead of generic parameters in device macro
All checks were successful
Build and deploy / build (push) Successful in 10m11s
Build and deploy / Deploy container (push) Successful in 2m8s
This enforced the idea that all generics must be specified for the type
when using the device macro. It will also come into play later when the
Typed macro gets introduced, as the name will be used when generating
definitions.
2025-09-17 00:35:30 +02:00
15 changed files with 188 additions and 39 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#cef241546e679190472b1ea7e5904cf8102f7f4b"
dependencies = [
"lua_typed_macro",
]
[[package]]
name = "lua_typed_macro"
version = "0.1.0"
source = "git+https://git.huizinga.dev/Dreaded_X/lua_typed#cef241546e679190472b1ea7e5904cf8102f7f4b"
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

@@ -14,6 +14,7 @@ mod zigbee;
use automation_lib::Module; use automation_lib::Module;
use automation_lib::device::{Device, LuaDeviceCreate}; use automation_lib::device::{Device, LuaDeviceCreate};
use lua_typed::Typed;
use tracing::debug; use tracing::debug;
macro_rules! register_device { macro_rules! register_device {
@@ -64,3 +65,12 @@ 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)}
pub fn generate_definitions() {
println!("{}", ntfy::Priority::generate_full().unwrap());
println!("{}", ntfy::ActionType::generate_full().unwrap());
println!("{}", ntfy::Action::generate_full().unwrap());
println!("{}", ntfy::Notification::generate_full().unwrap());
println!("{}", ntfy::Config::generate_full().unwrap());
println!("{}", ntfy::Ntfy::generate_full().unwrap());
}

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 {
@@ -20,7 +21,7 @@ pub enum Priority {
Max, Max,
} }
#[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,7 +32,7 @@ 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,
@@ -39,14 +40,14 @@ pub struct Action {
pub clear: Option<bool>, pub clear: Option<bool>,
} }
#[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>,
@@ -67,7 +68,7 @@ impl Notification {
} }
} }
#[derive(Debug, Clone, LuaDeviceConfig)] #[derive(Debug, Clone, LuaDeviceConfig, Typed)]
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,
@@ -96,6 +97,21 @@ impl Ntfy {
} }
} }
impl Typed for Ntfy {
fn type_name() -> String {
"Ntfy".into()
}
fn generate_header() -> Option<String> {
Some("---@class Ntfy\n".into())
}
fn generate_members() -> Option<String> {
Some("---@async\n---@param notification Notification\nfunction Ntfy:send_notification(notification) end".into(),
)
}
}
#[async_trait] #[async_trait]
impl LuaDeviceCreate for Ntfy { impl LuaDeviceCreate for Ntfy {
type Config = Config; type Config = Config;

View File

@@ -89,9 +89,9 @@ impl From<StateColorTemperature> for StateBrightness {
} }
#[derive(Debug, Clone, Device)] #[derive(Debug, Clone, Device)]
#[device(traits(OnOff for <StateOnOff>, <StateBrightness>, <StateColorTemperature>))] #[device(traits(OnOff for LightOnOff, LightBrightness, LightColorTemperature))]
#[device(traits(Brightness for <StateBrightness>, <StateColorTemperature>))] #[device(traits(Brightness for LightBrightness, LightColorTemperature))]
#[device(traits(ColorSetting for <StateColorTemperature>))] #[device(traits(ColorSetting for LightColorTemperature))]
pub struct Light<T: LightState> { pub struct Light<T: LightState> {
config: Config<T>, config: Config<T>,
@@ -144,7 +144,7 @@ impl<T: LightState> Device for Light<T> {
} }
#[async_trait] #[async_trait]
impl OnMqtt for Light<StateOnOff> { impl OnMqtt for LightOnOff {
async fn on_mqtt(&self, message: Publish) { async fn on_mqtt(&self, message: Publish) {
// Check if the message is from the device itself or from a remote // Check if the message is from the device itself or from a remote
if matches(&message.topic, &self.config.mqtt.topic) { if matches(&message.topic, &self.config.mqtt.topic) {
@@ -177,7 +177,7 @@ impl OnMqtt for Light<StateOnOff> {
} }
#[async_trait] #[async_trait]
impl OnMqtt for Light<StateBrightness> { impl OnMqtt for LightBrightness {
async fn on_mqtt(&self, message: Publish) { async fn on_mqtt(&self, message: Publish) {
// Check if the message is from the deviec itself or from a remote // Check if the message is from the deviec itself or from a remote
if matches(&message.topic, &self.config.mqtt.topic) { if matches(&message.topic, &self.config.mqtt.topic) {
@@ -216,7 +216,7 @@ impl OnMqtt for Light<StateBrightness> {
} }
#[async_trait] #[async_trait]
impl OnMqtt for Light<StateColorTemperature> { impl OnMqtt for LightColorTemperature {
async fn on_mqtt(&self, message: Publish) { async fn on_mqtt(&self, message: Publish) {
// Check if the message is from the deviec itself or from a remote // Check if the message is from the deviec itself or from a remote
if matches(&message.topic, &self.config.mqtt.topic) { if matches(&message.topic, &self.config.mqtt.topic) {

View File

@@ -81,7 +81,7 @@ impl From<StatePower> for StateOnOff {
} }
#[derive(Debug, Clone, Device)] #[derive(Debug, Clone, Device)]
#[device(traits(OnOff for <StateOnOff>, <StatePower>))] #[device(traits(OnOff for OutletOnOff, OutletPower))]
pub struct Outlet<T: OutletState> { pub struct Outlet<T: OutletState> {
config: Config<T>, config: Config<T>,
@@ -131,7 +131,7 @@ impl<T: OutletState> Device for Outlet<T> {
} }
#[async_trait] #[async_trait]
impl OnMqtt for Outlet<StateOnOff> { impl OnMqtt for OutletOnOff {
async fn on_mqtt(&self, message: Publish) { async fn on_mqtt(&self, message: Publish) {
// Check if the message is from the device itself or from a remote // Check if the message is from the device itself or from a remote
if matches(&message.topic, &self.config.mqtt.topic) { if matches(&message.topic, &self.config.mqtt.topic) {
@@ -164,7 +164,7 @@ impl OnMqtt for Outlet<StateOnOff> {
} }
#[async_trait] #[async_trait]
impl OnMqtt for Outlet<StatePower> { impl OnMqtt for OutletPower {
async fn on_mqtt(&self, message: Publish) { async fn on_mqtt(&self, message: Publish) {
// Check if the message is from the deviec itself or from a remote // Check if the message is from the deviec itself or from a remote
if matches(&message.topic, &self.config.mqtt.topic) { if matches(&message.topic, &self.config.mqtt.topic) {

View File

@@ -35,14 +35,14 @@ impl Parse for Attr {
struct TraitAttr { struct TraitAttr {
traits: Traits, traits: Traits,
generics: Generics, aliases: Aliases,
} }
impl Parse for TraitAttr { impl Parse for TraitAttr {
fn parse(input: ParseStream) -> syn::Result<Self> { fn parse(input: ParseStream) -> syn::Result<Self> {
Ok(Self { Ok(Self {
traits: input.parse()?, traits: input.parse()?,
generics: input.parse()?, aliases: input.parse()?,
}) })
} }
} }
@@ -78,15 +78,15 @@ impl ToTokens for Traits {
} }
#[derive(Default)] #[derive(Default)]
struct Generics(Vec<syn::AngleBracketedGenericArguments>); struct Aliases(Vec<syn::Ident>);
impl Generics { impl Aliases {
fn has_generics(&self) -> bool { fn has_aliases(&self) -> bool {
!self.0.is_empty() !self.0.is_empty()
} }
} }
impl Parse for Generics { impl Parse for Aliases {
fn parse(input: ParseStream) -> syn::Result<Self> { fn parse(input: ParseStream) -> syn::Result<Self> {
if !input.peek(Token![for]) { if !input.peek(Token![for]) {
if input.is_empty() { if input.is_empty() {
@@ -100,7 +100,7 @@ impl Parse for Generics {
input input
.call(Punctuated::<_, Token![,]>::parse_separated_nonempty) .call(Punctuated::<_, Token![,]>::parse_separated_nonempty)
.map(|generics| generics.into_iter().collect()) .map(|aliases| aliases.into_iter().collect())
.map(Self) .map(Self)
} }
} }
@@ -125,7 +125,7 @@ impl ToTokens for AddMethodsAttr {
} }
struct Implementation { struct Implementation {
generics: Option<syn::AngleBracketedGenericArguments>, name: syn::Ident,
traits: Traits, traits: Traits,
add_methods: Vec<AddMethodsAttr>, add_methods: Vec<AddMethodsAttr>,
} }
@@ -133,13 +133,13 @@ struct Implementation {
impl quote::ToTokens for Implementation { impl quote::ToTokens for Implementation {
fn to_tokens(&self, tokens: &mut TokenStream2) { fn to_tokens(&self, tokens: &mut TokenStream2) {
let Self { let Self {
generics, name,
traits, traits,
add_methods, add_methods,
} = &self; } = &self;
tokens.extend(quote! { tokens.extend(quote! {
#generics { 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)
@@ -169,18 +169,18 @@ impl quote::ToTokens for Implementation {
struct Implementations(Vec<Implementation>); struct Implementations(Vec<Implementation>);
impl From<Vec<Attr>> for Implementations { impl Implementations {
fn from(attributes: Vec<Attr>) -> Self { fn from_attr(attributes: Vec<Attr>, name: syn::Ident) -> Self {
let mut add_methods = Vec::new(); let mut add_methods = Vec::new();
let mut all = Traits::default(); let mut all = Traits::default();
let mut implementations: HashMap<_, Traits> = HashMap::new(); let mut implementations: HashMap<_, Traits> = HashMap::new();
for attribute in attributes { for attribute in attributes {
match attribute { match attribute {
Attr::Trait(attribute) => { Attr::Trait(attribute) => {
if attribute.generics.has_generics() { if attribute.aliases.has_aliases() {
for generic in &attribute.generics.0 { for alias in &attribute.aliases.0 {
implementations implementations
.entry(Some(generic.clone())) .entry(Some(alias.clone()))
.or_default() .or_default()
.extend(&attribute.traits); .extend(&attribute.traits);
} }
@@ -203,8 +203,8 @@ impl From<Vec<Attr>> for Implementations {
Self( Self(
implementations implementations
.into_iter() .into_iter()
.map(|(generics, traits)| Implementation { .map(|(alias, traits)| Implementation {
generics, name: alias.unwrap_or(name.clone()),
traits, traits,
add_methods: add_methods.clone(), add_methods: add_methods.clone(),
}) })
@@ -213,9 +213,7 @@ impl From<Vec<Attr>> for Implementations {
} }
} }
pub fn device(input: &DeriveInput) -> TokenStream2 { pub fn device(input: DeriveInput) -> TokenStream2 {
let name = &input.ident;
let Implementations(imp) = match input let Implementations(imp) = match input
.attrs .attrs
.iter() .iter()
@@ -223,13 +221,13 @@ pub fn device(input: &DeriveInput) -> TokenStream2 {
.map(Attribute::parse_args) .map(Attribute::parse_args)
.try_collect::<Vec<_>>() .try_collect::<Vec<_>>()
{ {
Ok(result) => result.into(), Ok(attr) => Implementations::from_attr(attr, input.ident),
Err(err) => return err.into_compile_error(), Err(err) => return err.into_compile_error(),
}; };
quote! { quote! {
#( #(
impl mlua::UserData for #name #imp #imp
)* )*
} }
} }

View File

@@ -69,5 +69,5 @@ pub fn lua_serialize(input: proc_macro::TokenStream) -> proc_macro::TokenStream
#[proc_macro_derive(Device, attributes(device))] #[proc_macro_derive(Device, attributes(device))]
pub fn device(input: proc_macro::TokenStream) -> proc_macro::TokenStream { pub fn device(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let ast = parse_macro_input!(input as DeriveInput); let ast = parse_macro_input!(input as DeriveInput);
device::device(&ast).into() device::device(ast).into()
} }

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

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,3 @@
fn main() {
automation_devices::generate_definitions()
}