Compare commits

9 Commits

Author SHA1 Message Date
295491c5fc feat(config)!: Move mqtt module to actual separate module
Some checks failed
Build and deploy / Deploy container (push) Blocked by required conditions
Build and deploy / build (push) Has been cancelled
The automation:mqtt module now gets loaded in a similar way as the
automation:devices and automation:utils modules.
This leads to a breaking change where instantiating a new mqtt client
the device manager needs to be explicitly passed in.
2025-10-15 03:41:39 +02:00
9f244b3475 feat: Added/expanded Typed impls 2025-10-15 03:41:38 +02:00
15a6e83ad8 feat: Remove automatic automation: module prefix 2025-10-15 03:41:38 +02:00
c727579290 chore: Removed dotenvy
Since secrets can now be set from automation.toml the .env file was no
longer used, so dotenvy can be removed.
2025-10-15 03:41:38 +02:00
19e8663f26 feat: Use Typed type_name for registering proxy 2025-10-15 03:41:38 +02:00
f5c4495cad feat!: Expanded add_methods to extra_user_data
Instead of being a function it now expects a struct with the
PartialUserData trait implemented. This in part ensures the correct
function signature.

It also adds another optional function to PartialUserData that returns
definitions for the added methods.
2025-10-15 03:41:25 +02:00
9bbf0a5422 feat: Specify (optional) interface name in PartialUserData 2025-10-15 00:45:38 +02:00
85e3c7b877 feat: Use PartialUserData on proxy type to add trait methods 2025-10-15 00:45:38 +02:00
a2130005de feat: Improved attribute parsing in device macro 2025-10-15 00:45:37 +02:00
19 changed files with 347 additions and 185 deletions

11
Cargo.lock generated
View File

@@ -96,7 +96,6 @@ dependencies = [
"automation_lib",
"axum",
"config",
"dotenvy",
"git-version",
"google_home",
"mlua",
@@ -433,12 +432,6 @@ dependencies = [
"syn 2.0.106",
]
[[package]]
name = "dotenvy"
version = "0.15.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
[[package]]
name = "dyn-clone"
version = "1.0.20"
@@ -1108,7 +1101,7 @@ dependencies = [
[[package]]
name = "lua_typed"
version = "0.1.0"
source = "git+https://git.huizinga.dev/Dreaded_X/lua_typed#d5d6fc1638bd108514899a792ee64335af50fc8b"
source = "git+https://git.huizinga.dev/Dreaded_X/lua_typed#08f5c4533a93131e8eda6702c062fb841d14d4e1"
dependencies = [
"eui48",
"lua_typed_macro",
@@ -1117,7 +1110,7 @@ dependencies = [
[[package]]
name = "lua_typed_macro"
version = "0.1.0"
source = "git+https://git.huizinga.dev/Dreaded_X/lua_typed#d5d6fc1638bd108514899a792ee64335af50fc8b"
source = "git+https://git.huizinga.dev/Dreaded_X/lua_typed#08f5c4533a93131e8eda6702c062fb841d14d4e1"
dependencies = [
"convert_case",
"itertools",

View File

@@ -23,7 +23,6 @@ automation_lib = { path = "./automation_lib" }
automation_macro = { path = "./automation_macro" }
axum = "0.8.4"
bytes = "1.10.1"
dotenvy = "0.15.7"
dyn-clone = "1.0.20"
eui48 = { version = "1.1.0", features = [
"disp_hexstring",
@@ -75,7 +74,6 @@ config = { version = "0.15.15", default-features = false, features = [
"async",
"toml",
] }
dotenvy = { workspace = true }
git-version = "0.3.9"
google_home = { workspace = true }
mlua = { workspace = true }

View File

@@ -3,18 +3,21 @@ use std::net::SocketAddr;
use async_trait::async_trait;
use automation_lib::device::{Device, LuaDeviceCreate};
use automation_lib::lua::traits::PartialUserData;
use automation_macro::{Device, LuaDeviceConfig};
use lua_typed::Typed;
use mlua::LuaSerdeExt;
use serde::{Deserialize, Serialize};
use tracing::{error, trace, warn};
#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, Typed)]
#[serde(rename_all = "snake_case")]
#[typed(rename_all = "snake_case")]
pub enum Flag {
Presence,
Darkness,
}
crate::register_type!(Flag);
#[derive(Debug, Clone, Deserialize, Typed)]
pub struct FlagIDs {
@@ -36,12 +39,36 @@ pub struct Config {
crate::register_type!(Config);
#[derive(Debug, Clone, Device)]
#[device(add_methods(Self::add_methods))]
#[device(extra_user_data = SetFlag)]
pub struct HueBridge {
config: Config,
}
crate::register_device!(HueBridge);
struct SetFlag;
impl PartialUserData<HueBridge> for SetFlag {
fn add_methods<M: mlua::UserDataMethods<HueBridge>>(methods: &mut M) {
methods.add_async_method(
"set_flag",
async |lua, this, (flag, value): (mlua::Value, bool)| {
let flag: Flag = lua.from_value(flag)?;
this.set_flag(flag, value).await;
Ok(())
},
);
}
fn definitions() -> Option<String> {
Some(format!(
"---@async\n---@param flag {}\n---@param value boolean\nfunction {}:set_flag(flag, value) end\n",
<Flag as Typed>::type_name(),
<HueBridge as Typed>::type_name(),
))
}
}
#[derive(Debug, Serialize)]
struct FlagMessage {
flag: bool,
@@ -89,19 +116,6 @@ impl HueBridge {
}
}
}
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
methods.add_async_method(
"set_flag",
async |lua, this, (flag, value): (mlua::Value, bool)| {
let flag: Flag = lua.from_value(flag)?;
this.set_flag(flag, value).await;
Ok(())
},
);
}
}
impl Device for HueBridge {

View File

@@ -1,3 +1,4 @@
#![feature(iter_intersperse)]
mod air_filter;
mod contact_sensor;
mod hue_bridge;
@@ -19,7 +20,7 @@ use tracing::debug;
macro_rules! register_device {
($device:ty) => {
::inventory::submit!(crate::RegisteredDevice::new(
stringify!($device),
<$device as ::lua_typed::Typed>::type_name,
::mlua::Lua::create_proxy::<$device>
));
@@ -29,20 +30,24 @@ macro_rules! register_device {
pub(crate) use register_device;
type DeviceNameFn = fn() -> String;
type RegisterDeviceFn = fn(lua: &mlua::Lua) -> mlua::Result<mlua::AnyUserData>;
pub struct RegisteredDevice {
name: &'static str,
name_fn: DeviceNameFn,
register_fn: RegisterDeviceFn,
}
impl RegisteredDevice {
pub const fn new(name: &'static str, register_fn: RegisterDeviceFn) -> Self {
Self { name, register_fn }
pub const fn new(name_fn: DeviceNameFn, register_fn: RegisterDeviceFn) -> Self {
Self {
name_fn,
register_fn,
}
}
pub const fn get_name(&self) -> &'static str {
self.name
pub fn get_name(&self) -> String {
(self.name_fn)()
}
pub fn register(&self, lua: &mlua::Lua) -> mlua::Result<mlua::AnyUserData> {
@@ -57,15 +62,16 @@ pub fn create_module(lua: &mlua::Lua) -> mlua::Result<mlua::Table> {
debug!("Loading devices...");
for device in inventory::iter::<RegisteredDevice> {
debug!(name = device.get_name(), "Registering device");
let name = device.get_name();
debug!(name, "Registering device");
let proxy = device.register(lua)?;
devices.set(device.get_name(), proxy)?;
devices.set(name, proxy)?;
}
Ok(devices)
}
inventory::submit! {Module::new("devices", create_module)}
inventory::submit! {Module::new("automation:devices", create_module)}
macro_rules! register_type {
($ty:ty) => {

View File

@@ -3,6 +3,7 @@ use std::convert::Infallible;
use async_trait::async_trait;
use automation_lib::device::{Device, LuaDeviceCreate};
use automation_lib::lua::traits::PartialUserData;
use automation_macro::{Device, LuaDeviceConfig};
use lua_typed::Typed;
use mlua::LuaSerdeExt;
@@ -90,14 +91,15 @@ pub struct Config {
crate::register_type!(Config);
#[derive(Debug, Clone, Device)]
#[device(add_methods(Self::add_methods))]
#[device(extra_user_data = SendNotification)]
pub struct Ntfy {
config: Config,
}
crate::register_device!(Ntfy);
impl Ntfy {
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
struct SendNotification;
impl PartialUserData<Ntfy> for SendNotification {
fn add_methods<M: mlua::UserDataMethods<Ntfy>>(methods: &mut M) {
methods.add_async_method(
"send_notification",
async |lua, this, notification: mlua::Value| {
@@ -109,6 +111,14 @@ impl Ntfy {
},
);
}
fn definitions() -> Option<String> {
Some(format!(
"---@async\n---@param notification {}\nfunction {}:send_notification(notification) end\n",
<Notification as Typed>::type_name(),
<Ntfy as Typed>::type_name(),
))
}
}
#[async_trait]

View File

@@ -6,6 +6,7 @@ use automation_lib::action_callback::ActionCallback;
use automation_lib::config::MqttDeviceConfig;
use automation_lib::device::{Device, LuaDeviceCreate};
use automation_lib::event::OnMqtt;
use automation_lib::lua::traits::PartialUserData;
use automation_lib::messages::PresenceMessage;
use automation_lib::mqtt::WrappedAsyncClient;
use automation_macro::{Device, LuaDeviceConfig};
@@ -39,13 +40,29 @@ pub struct State {
}
#[derive(Debug, Clone, Device)]
#[device(add_methods(Self::add_methods))]
#[device(extra_user_data = OverallPresence)]
pub struct Presence {
config: Config,
state: Arc<RwLock<State>>,
}
crate::register_device!(Presence);
struct OverallPresence;
impl PartialUserData<Presence> for OverallPresence {
fn add_methods<M: mlua::UserDataMethods<Presence>>(methods: &mut M) {
methods.add_async_method("overall_presence", async |_lua, this, ()| {
Ok(this.state().await.current_overall_presence)
});
}
fn definitions() -> Option<String> {
Some(format!(
"---@async\n---@return boolean\nfunction {}:overall_presence() end\n",
<Presence as Typed>::type_name(),
))
}
}
impl Presence {
async fn state(&self) -> RwLockReadGuard<'_, State> {
self.state.read().await
@@ -54,12 +71,6 @@ impl Presence {
async fn state_mut(&self) -> RwLockWriteGuard<'_, State> {
self.state.write().await
}
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
methods.add_async_method("overall_presence", async |_lua, this, ()| {
Ok(this.state().await.current_overall_presence)
});
}
}
#[async_trait]

View File

@@ -10,6 +10,12 @@ pub struct ActionCallback<P> {
_parameters: PhantomData<P>,
}
impl Typed for ActionCallback<()> {
fn type_name() -> String {
"fun() | fun()[]".into()
}
}
impl<A: Typed> Typed for ActionCallback<A> {
fn type_name() -> String {
let type_name = A::type_name();

View File

@@ -5,7 +5,7 @@ use lua_typed::Typed;
use rumqttc::{MqttOptions, Transport};
use serde::Deserialize;
#[derive(Debug, Clone, Deserialize)]
#[derive(Debug, Clone, Deserialize, Typed)]
pub struct MqttConfig {
pub host: String,
pub port: u16,

View File

@@ -4,6 +4,8 @@ use std::sync::Arc;
use futures::Future;
use futures::future::join_all;
use lua_typed::Typed;
use mlua::FromLua;
use tokio::sync::{RwLock, RwLockReadGuard};
use tokio_cron_scheduler::{Job, JobScheduler};
use tracing::{debug, instrument, trace};
@@ -13,7 +15,7 @@ use crate::event::{Event, EventChannel, OnMqtt};
pub type DeviceMap = HashMap<String, Box<dyn Device>>;
#[derive(Clone)]
#[derive(Clone, FromLua)]
pub struct DeviceManager {
devices: Arc<RwLock<DeviceMap>>,
event_channel: EventChannel,
@@ -142,3 +144,9 @@ impl mlua::UserData for DeviceManager {
methods.add_method("event_channel", |_lua, this, ()| Ok(this.event_channel()))
}
}
impl Typed for DeviceManager {
fn type_name() -> String {
"DeviceManager".into()
}
}

View File

@@ -41,7 +41,7 @@ pub fn load_modules(lua: &mlua::Lua) -> mlua::Result<()> {
for module in inventory::iter::<Module> {
debug!(name = module.get_name(), "Loading module");
let table = module.register(lua)?;
lua.register_module(&format!("automation:{}", module.get_name()), table)?;
lua.register_module(module.get_name(), table)?;
}
Ok(())

View File

@@ -2,21 +2,40 @@ use std::ops::Deref;
// TODO: Enable and disable functions based on query_only and command_only
pub trait Device {
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M)
where
Self: Sized + crate::device::Device + 'static,
{
methods.add_async_method("get_id", async |_lua, this, _: ()| Ok(this.get_id()));
pub trait PartialUserData<T> {
fn add_methods<M: mlua::UserDataMethods<T>>(methods: &mut M);
fn interface_name() -> Option<&'static str> {
None
}
fn definitions() -> Option<String> {
None
}
}
impl<T> Device for T where T: crate::device::Device {}
pub trait OnOff {
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M)
where
Self: Sized + google_home::traits::OnOff + 'static,
{
pub struct Device;
impl<T> PartialUserData<T> for Device
where
T: crate::device::Device + 'static,
{
fn add_methods<M: mlua::UserDataMethods<T>>(methods: &mut M) {
methods.add_async_method("get_id", async |_lua, this, _: ()| Ok(this.get_id()));
}
fn interface_name() -> Option<&'static str> {
Some("DeviceInterface")
}
}
pub struct OnOff;
impl<T> PartialUserData<T> for OnOff
where
T: google_home::traits::OnOff + 'static,
{
fn add_methods<M: mlua::UserDataMethods<T>>(methods: &mut M) {
methods.add_async_method("set_on", async |_lua, this, on: bool| {
this.deref().set_on(on).await.unwrap();
@@ -27,14 +46,19 @@ pub trait OnOff {
Ok(this.deref().on().await.unwrap())
});
}
}
impl<T> OnOff for T where T: google_home::traits::OnOff {}
pub trait Brightness {
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M)
where
Self: Sized + google_home::traits::Brightness + 'static,
{
fn interface_name() -> Option<&'static str> {
Some("OnOffInterface")
}
}
pub struct Brightness;
impl<T> PartialUserData<T> for Brightness
where
T: google_home::traits::Brightness + 'static,
{
fn add_methods<M: mlua::UserDataMethods<T>>(methods: &mut M) {
methods.add_async_method("set_brightness", async |_lua, this, brightness: u8| {
this.set_brightness(brightness).await.unwrap();
@@ -45,14 +69,19 @@ pub trait Brightness {
Ok(this.brightness().await.unwrap())
});
}
}
impl<T> Brightness for T where T: google_home::traits::Brightness {}
pub trait ColorSetting {
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M)
where
Self: Sized + google_home::traits::ColorSetting + 'static,
{
fn interface_name() -> Option<&'static str> {
Some("BrightnessInterface")
}
}
pub struct ColorSetting;
impl<T> PartialUserData<T> for ColorSetting
where
T: google_home::traits::ColorSetting + 'static,
{
fn add_methods<M: mlua::UserDataMethods<T>>(methods: &mut M) {
methods.add_async_method(
"set_color_temperature",
async |_lua, this, temperature: u32| {
@@ -68,14 +97,19 @@ pub trait ColorSetting {
Ok(this.color().await.temperature)
});
}
}
impl<T> ColorSetting for T where T: google_home::traits::ColorSetting {}
pub trait OpenClose {
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M)
where
Self: Sized + google_home::traits::OpenClose + 'static,
{
fn interface_name() -> Option<&'static str> {
Some("ColorSettingInterface")
}
}
pub struct OpenClose;
impl<T> PartialUserData<T> for OpenClose
where
T: google_home::traits::OpenClose + 'static,
{
fn add_methods<M: mlua::UserDataMethods<T>>(methods: &mut M) {
methods.add_async_method("set_open_percent", async |_lua, this, open_percent: u8| {
this.set_open_percent(open_percent).await.unwrap();
@@ -86,5 +120,8 @@ pub trait OpenClose {
Ok(this.open_percent().await.unwrap())
});
}
fn interface_name() -> Option<&'static str> {
Some("OpenCloseInterface")
}
}
impl<T> OpenClose for T where T: google_home::traits::OpenClose {}

View File

@@ -28,4 +28,4 @@ fn create_module(lua: &mlua::Lua) -> mlua::Result<mlua::Table> {
Ok(utils)
}
inventory::submit! {Module::new("utils", create_module)}
inventory::submit! {Module::new("automation:utils", create_module)}

View File

@@ -1,6 +1,7 @@
use std::sync::Arc;
use std::time::Duration;
use lua_typed::Typed;
use tokio::sync::RwLock;
use tokio::task::JoinHandle;
use tracing::debug;
@@ -74,3 +75,44 @@ impl mlua::UserData for Timeout {
});
}
}
impl Typed for Timeout {
fn type_name() -> String {
"Timeout".into()
}
fn generate_header() -> Option<String> {
let type_name = Self::type_name();
Some(format!("---@class {type_name}\nlocal {type_name}\n"))
}
fn generate_members() -> Option<String> {
let mut output = String::new();
let type_name = Self::type_name();
output += &format!(
"---@async\n---@param timeout number\n---@param callback {}\nfunction {type_name}:start(timeout, callback) end\n",
ActionCallback::<()>::type_name()
);
output += &format!("---@async\nfunction {type_name}:cancel() end\n",);
output +=
&format!("---@async\n---@return boolean\nfunction {type_name}:is_waiting() end\n",);
Some(output)
}
fn generate_footer() -> Option<String> {
let mut output = String::new();
let type_name = Self::type_name();
output += &format!("utils.{type_name} = {{}}\n");
output += &format!("---@return {type_name}\n");
output += &format!("function utils.{type_name}.new() end\n");
Some(output)
}
}

View File

@@ -1,10 +1,13 @@
use std::ops::{Deref, DerefMut};
use lua_typed::Typed;
use mlua::FromLua;
use mlua::{FromLua, LuaSerdeExt};
use rumqttc::{AsyncClient, Event, EventLoop, Incoming};
use tracing::{debug, warn};
use crate::Module;
use crate::config::MqttConfig;
use crate::device_manager::DeviceManager;
use crate::event::{self, EventChannel};
#[derive(Debug, Clone, FromLua)]
@@ -14,6 +17,37 @@ impl Typed for WrappedAsyncClient {
fn type_name() -> String {
"AsyncClient".into()
}
fn generate_header() -> Option<String> {
let type_name = Self::type_name();
Some(format!("---@class {type_name}\nlocal {type_name}\n"))
}
fn generate_members() -> Option<String> {
let mut output = String::new();
let type_name = Self::type_name();
output += &format!(
"---@async\n---@param topic string\n---@param message table?\nfunction {type_name}:send_message(topic, message) end\n"
);
Some(output)
}
fn generate_footer() -> Option<String> {
let mut output = String::new();
let type_name = Self::type_name();
output += &format!("mqtt.{type_name} = {{}}\n");
output += &format!("---@param device_manager {}\n", DeviceManager::type_name());
output += &format!("---@param config {}\n", MqttConfig::type_name());
output += &format!("---@return {type_name}\n");
output += "function mqtt.new(device_manager, config) end\n";
Some(output)
}
}
impl Deref for WrappedAsyncClient {
@@ -77,3 +111,25 @@ pub fn start(mut eventloop: EventLoop, event_channel: &EventChannel) {
}
});
}
fn create_module(lua: &mlua::Lua) -> mlua::Result<mlua::Table> {
let mqtt = lua.create_table()?;
let mqtt_new = lua.create_function(
move |lua, (device_manager, config): (DeviceManager, mlua::Value)| {
let event_channel = device_manager.event_channel();
let config: MqttConfig = lua.from_value(config)?;
// Create a mqtt client
// TODO: When starting up, the devices are not yet created, this could lead to a device being out of sync
let (client, eventloop) = AsyncClient::new(config.into(), 100);
start(eventloop, &event_channel);
Ok(WrappedAsyncClient(client))
},
)?;
mqtt.set("new", mqtt_new)?;
Ok(mqtt)
}
inventory::submit! {Module::new("automation:mqtt", create_module)}

View File

@@ -1,35 +1,36 @@
use std::collections::HashMap;
use proc_macro2::TokenStream as TokenStream2;
use quote::{ToTokens, quote};
use quote::quote;
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::spanned::Spanned;
use syn::{Attribute, DeriveInput, Token, parenthesized};
enum Attr {
Trait(TraitAttr),
AddMethods(AddMethodsAttr),
ExtraUserData(ExtraUserDataAttr),
}
impl Parse for Attr {
fn parse(input: ParseStream) -> syn::Result<Self> {
let ident: syn::Ident = input.parse()?;
let attr;
_ = parenthesized!(attr in input);
let attr = match ident.to_string().as_str() {
"traits" => Attr::Trait(attr.parse()?),
"add_methods" => Attr::AddMethods(attr.parse()?),
_ => {
return Err(syn::Error::new(
ident.span(),
"Expected 'traits' or 'add_methods'",
));
impl Attr {
fn parse(attr: &Attribute) -> syn::Result<Self> {
let mut parsed = None;
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("traits") {
let input;
_ = parenthesized!(input in meta.input);
parsed = Some(Attr::Trait(input.parse()?));
} else if meta.path.is_ident("extra_user_data") {
let value = meta.value()?;
parsed = Some(Attr::ExtraUserData(value.parse()?));
} else {
return Err(syn::Error::new(meta.path.span(), "Unknown attribute"));
}
};
Ok(attr)
Ok(())
})?;
Ok(parsed.expect("Parsed should be set"))
}
}
@@ -65,18 +66,6 @@ impl Parse for Traits {
}
}
impl ToTokens for Traits {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let Self(traits) = &self;
tokens.extend(quote! {
#(
::automation_lib::lua::traits::#traits::add_methods(methods);
)*
});
}
}
#[derive(Default)]
struct Aliases(Vec<syn::Ident>);
@@ -106,28 +95,18 @@ impl Parse for Aliases {
}
#[derive(Clone)]
struct AddMethodsAttr(syn::Path);
struct ExtraUserDataAttr(syn::Ident);
impl Parse for AddMethodsAttr {
impl Parse for ExtraUserDataAttr {
fn parse(input: ParseStream) -> syn::Result<Self> {
Ok(Self(input.parse()?))
}
}
impl ToTokens for AddMethodsAttr {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let Self(path) = self;
tokens.extend(quote! {
#path
});
}
}
struct Implementation {
name: syn::Ident,
traits: Traits,
add_methods: Vec<AddMethodsAttr>,
extra_user_data: Vec<ExtraUserDataAttr>,
}
impl quote::ToTokens for Implementation {
@@ -135,14 +114,10 @@ impl quote::ToTokens for Implementation {
let Self {
name,
traits,
add_methods,
extra_user_data,
} = &self;
let interfaces: String = traits
.0
.iter()
.map(|tr| format!(", Interface{tr}"))
.collect();
let Traits(traits) = traits;
let extra_user_data: Vec<_> = extra_user_data.iter().map(|tr| tr.0.clone()).collect();
tokens.extend(quote! {
impl mlua::UserData for #name {
@@ -160,12 +135,14 @@ impl quote::ToTokens for Implementation {
Ok(b)
});
::automation_lib::lua::traits::Device::add_methods(methods);
#traits
<::automation_lib::lua::traits::Device as ::automation_lib::lua::traits::PartialUserData<#name>>::add_methods(methods);
#(
#add_methods(methods);
<::automation_lib::lua::traits::#traits as ::automation_lib::lua::traits::PartialUserData<#name>>::add_methods(methods);
)*
#(
<#extra_user_data as ::automation_lib::lua::traits::PartialUserData<#name>>::add_methods(methods);
)*
}
}
@@ -177,7 +154,22 @@ impl quote::ToTokens for Implementation {
fn generate_header() -> std::option::Option<::std::string::String> {
let type_name = <Self as ::lua_typed::Typed>::type_name();
Some(format!("---@class {type_name}: InterfaceDevice{}\nlocal {type_name}\n", #interfaces))
let mut output = String::new();
let interfaces: String = [
<::automation_lib::lua::traits::Device as ::automation_lib::lua::traits::PartialUserData<#name>>::interface_name(),
#(
<::automation_lib::lua::traits::#traits as ::automation_lib::lua::traits::PartialUserData<#name>>::interface_name(),
)*
].into_iter().flatten().intersperse(", ").collect();
let interfaces = if interfaces.is_empty() {
"".into()
} else {
format!(": {interfaces}")
};
Some(format!("---@class {type_name}{interfaces}\nlocal {type_name}\n"))
}
fn generate_members() -> Option<String> {
@@ -190,6 +182,15 @@ impl quote::ToTokens for Implementation {
output += &format!("---@return {type_name}\n");
output += &format!("function devices.{type_name}.new(config) end\n");
output += &<::automation_lib::lua::traits::Device as ::automation_lib::lua::traits::PartialUserData<#name>>::definitions().unwrap_or("".into());
#(
output += &<::automation_lib::lua::traits::#traits as ::automation_lib::lua::traits::PartialUserData<#name>>::definitions().unwrap_or("".into());
)*
#(
output += &<#extra_user_data as ::automation_lib::lua::traits::PartialUserData<#name>>::definitions().unwrap_or("".into());
)*
Some(output)
}
@@ -219,7 +220,7 @@ impl Implementations {
all.extend(&attribute.traits);
}
}
Attr::AddMethods(attribute) => add_methods.push(attribute),
Attr::ExtraUserData(attribute) => add_methods.push(attribute),
}
}
@@ -237,7 +238,7 @@ impl Implementations {
.map(|(alias, traits)| Implementation {
name: alias.unwrap_or(name.clone()),
traits,
add_methods: add_methods.clone(),
extra_user_data: add_methods.clone(),
})
.collect(),
)
@@ -249,7 +250,7 @@ pub fn device(input: DeriveInput) -> TokenStream2 {
.attrs
.iter()
.filter(|attr| attr.path().is_ident("device"))
.map(Attribute::parse_args)
.map(Attr::parse)
.try_collect::<Vec<_>>()
{
Ok(attr) => Implementations::from_attr(attr, input.ident),

View File

@@ -64,7 +64,7 @@ pub fn lua_serialize(input: proc_macro::TokenStream) -> proc_macro::TokenStream
/// ```
/// It can then be registered with:
/// ```rust
/// #[device(add_methods(top_secret))]
/// #[device(add_methods = top_secret)]
/// ```
#[proc_macro_derive(Device, attributes(device))]
pub fn device(input: proc_macro::TokenStream) -> proc_macro::TokenStream {

View File

@@ -21,7 +21,7 @@ local fulfillment = {
openid_url = "https://login.huizinga.dev/api/oidc",
}
local mqtt_client = require("automation:mqtt").new({
local mqtt_client = require("automation:mqtt").new(device_manager, {
host = ((host == "zeus" or host == "hephaestus") and "olympus.lan.huizinga.dev") or "mosquitto",
port = 8883,
client_name = "automation-" .. host,

View File

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

View File

@@ -9,18 +9,15 @@ use std::path::Path;
use std::process;
use ::config::{Environment, File};
use automation_lib::config::{FulfillmentConfig, MqttConfig};
use automation_lib::config::FulfillmentConfig;
use automation_lib::device_manager::DeviceManager;
use automation_lib::mqtt::{self, WrappedAsyncClient};
use axum::extract::{FromRef, State};
use axum::http::StatusCode;
use axum::routing::post;
use axum::{Json, Router};
use config::Config;
use dotenvy::dotenv;
use google_home::{GoogleHome, Request, Response};
use mlua::LuaSerdeExt;
use rumqttc::AsyncClient;
use tokio::net::TcpListener;
use tracing::{debug, error, info, warn};
use web::{ApiError, User};
@@ -75,8 +72,6 @@ async fn fulfillment(
}
async fn app() -> anyhow::Result<()> {
dotenv().ok();
tracing_subscriber::fmt::init();
info!(version = VERSION, "automation_rs");
@@ -141,21 +136,6 @@ async fn app() -> anyhow::Result<()> {
automation_lib::load_modules(&lua)?;
let mqtt = lua.create_table()?;
let event_channel = device_manager.event_channel();
let mqtt_new = lua.create_function(move |lua, config: mlua::Value| {
let config: MqttConfig = lua.from_value(config)?;
// Create a mqtt client
// TODO: When starting up, the devices are not yet created, this could lead to a device being out of sync
let (client, eventloop) = AsyncClient::new(config.into(), 100);
mqtt::start(eventloop, &event_channel);
Ok(WrappedAsyncClient(client))
})?;
mqtt.set("new", mqtt_new)?;
lua.register_module("automation:mqtt", mqtt)?;
lua.register_module("automation:device_manager", device_manager.clone())?;
lua.register_module("automation:variables", lua.to_value(&config.variables)?)?;