Compare commits

..

3 Commits

Author SHA1 Message Date
6504b1239e feat: WIP
Some checks failed
Build and deploy / Deploy container (push) Blocked by required conditions
Build and deploy / build (push) Has been cancelled
2025-10-10 03:56:34 +02:00
ad8db00c8f feat: Add proper type definition for devices
Depending on the implemented traits the lua class will inherit from the
associated interface class.

It also specifies the constructor function for each of the devices.
2025-10-10 03:56:34 +02:00
8010f7a404 feat: Added Typed impl for all automation devices
To accomplish this a basic implementation was also provided for some
types in automation_lib
2025-10-10 03:56:33 +02:00
32 changed files with 817 additions and 63 deletions

38
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",
@@ -153,6 +154,7 @@ dependencies = [
"hostname", "hostname",
"indexmap", "indexmap",
"inventory", "inventory",
"lua_typed",
"mlua", "mlua",
"rumqttc", "rumqttc",
"serde", "serde",
@@ -345,6 +347,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 +1105,27 @@ dependencies = [
"cc", "cc",
] ]
[[package]]
name = "lua_typed"
version = "0.1.0"
source = "git+https://git.huizinga.dev/Dreaded_X/lua_typed#513b36afdbc23efcf10847f070faff7fcd30f29d"
dependencies = [
"eui48",
"lua_typed_macro",
]
[[package]]
name = "lua_typed_macro"
version = "0.1.0"
source = "git+https://git.huizinga.dev/Dreaded_X/lua_typed#513b36afdbc23efcf10847f070faff7fcd30f29d"
dependencies = [
"convert_case",
"itertools",
"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 +2288,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

@@ -9,15 +9,19 @@ use google_home::traits::{
TemperatureUnit, TemperatureUnit,
}; };
use google_home::types::Type; use google_home::types::Type;
use lua_typed::Typed;
use thiserror::Error; use thiserror::Error;
use tracing::{debug, trace}; use tracing::{debug, trace};
#[derive(Debug, Clone, LuaDeviceConfig)] #[derive(Debug, Clone, LuaDeviceConfig, Typed)]
#[typed(as = "AirFilterConfig")]
pub struct Config { pub struct Config {
#[device_config(flatten)] #[device_config(flatten)]
#[serde(flatten)]
pub info: InfoConfig, pub info: InfoConfig,
pub url: String, pub url: String,
} }
crate::register_type!(Config);
#[derive(Debug, Clone, Device)] #[derive(Debug, Clone, Device)]
#[device(traits(OnOff))] #[device(traits(OnOff))]

View File

@@ -13,35 +13,45 @@ use google_home::device;
use google_home::errors::{DeviceError, ErrorCode}; use google_home::errors::{DeviceError, ErrorCode};
use google_home::traits::OpenClose; use google_home::traits::OpenClose;
use google_home::types::Type; use google_home::types::Type;
use lua_typed::Typed;
use serde::Deserialize; use serde::Deserialize;
use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard}; use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
use tracing::{debug, error, trace}; use tracing::{debug, error, trace};
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Copy)] #[derive(Debug, Clone, Deserialize, PartialEq, Eq, Copy, Typed)]
pub enum SensorType { pub enum SensorType {
Door, Door,
Drawer, Drawer,
Window, Window,
} }
crate::register_type!(SensorType);
#[derive(Debug, Clone, LuaDeviceConfig)] #[derive(Debug, Clone, LuaDeviceConfig, Typed)]
#[typed(as = "ContactSensorConfig")]
pub struct Config { pub struct Config {
#[device_config(flatten)] #[device_config(flatten)]
#[serde(flatten)]
pub info: InfoConfig, pub info: InfoConfig,
#[device_config(flatten)] #[device_config(flatten)]
#[serde(flatten)]
pub mqtt: MqttDeviceConfig, pub mqtt: MqttDeviceConfig,
#[device_config(default(SensorType::Window))] #[device_config(default(SensorType::Window))]
#[serde(default)]
pub sensor_type: SensorType, pub sensor_type: SensorType,
#[device_config(from_lua, default)] #[device_config(from_lua, default)]
#[serde(default)]
pub callback: ActionCallback<(ContactSensor, bool)>, pub callback: ActionCallback<(ContactSensor, bool)>,
#[device_config(from_lua, default)] #[device_config(from_lua, default)]
#[serde(default)]
pub battery_callback: ActionCallback<(ContactSensor, f32)>, pub battery_callback: ActionCallback<(ContactSensor, f32)>,
#[device_config(from_lua)] #[device_config(from_lua)]
#[serde(default)]
pub client: WrappedAsyncClient, pub client: WrappedAsyncClient,
} }
crate::register_type!(Config);
#[derive(Debug)] #[derive(Debug)]
struct State { struct State {

View File

@@ -4,6 +4,7 @@ use std::net::SocketAddr;
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 tracing::{error, trace, warn}; use tracing::{error, trace, warn};
@@ -15,20 +16,24 @@ pub enum Flag {
Darkness, Darkness,
} }
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize, Typed)]
pub struct FlagIDs { pub struct FlagIDs {
presence: isize, presence: isize,
darkness: isize, darkness: isize,
} }
crate::register_type!(FlagIDs);
#[derive(Debug, LuaDeviceConfig, Clone)] #[derive(Debug, LuaDeviceConfig, Clone, Typed)]
#[typed(as = "HueBridgeConfig")]
pub struct Config { pub struct Config {
pub identifier: String, pub identifier: String,
#[device_config(rename("ip"), with(|ip| SocketAddr::new(ip, 80)))] #[device_config(rename("ip"), with(|ip| SocketAddr::new(ip, 80)))]
#[typed(as = "ip")]
pub addr: SocketAddr, pub addr: SocketAddr,
pub login: String, pub login: String,
pub flags: FlagIDs, pub flags: FlagIDs,
} }
crate::register_type!(Config);
#[derive(Debug, Clone, Device)] #[derive(Debug, Clone, Device)]
#[device(add_methods(Self::add_methods))] #[device(add_methods(Self::add_methods))]

View File

@@ -5,19 +5,23 @@ use async_trait::async_trait;
use automation_macro::{Device, LuaDeviceConfig}; use automation_macro::{Device, LuaDeviceConfig};
use google_home::errors::ErrorCode; use google_home::errors::ErrorCode;
use google_home::traits::OnOff; use google_home::traits::OnOff;
use lua_typed::Typed;
use tracing::{error, trace, warn}; use tracing::{error, trace, warn};
use super::{Device, LuaDeviceCreate}; use super::{Device, LuaDeviceCreate};
#[derive(Debug, Clone, LuaDeviceConfig)] #[derive(Debug, Clone, LuaDeviceConfig, Typed)]
#[typed(as = "HueGroupConfig")]
pub struct Config { pub struct Config {
pub identifier: String, pub identifier: String,
#[device_config(rename("ip"), with(|ip| SocketAddr::new(ip, 80)))] #[device_config(rename("ip"), with(|ip| SocketAddr::new(ip, 80)))]
#[typed(as = "ip")]
pub addr: SocketAddr, pub addr: SocketAddr,
pub login: String, pub login: String,
pub group_id: isize, pub group_id: isize,
pub scene_id: String, pub scene_id: String,
} }
crate::register_type!(Config);
#[derive(Debug, Clone, Device)] #[derive(Debug, Clone, Device)]
#[device(traits(OnOff))] #[device(traits(OnOff))]

View File

@@ -5,36 +5,46 @@ use automation_lib::device::{Device, LuaDeviceCreate};
use automation_lib::event::OnMqtt; use automation_lib::event::OnMqtt;
use automation_lib::mqtt::WrappedAsyncClient; use automation_lib::mqtt::WrappedAsyncClient;
use automation_macro::{Device, LuaDeviceConfig}; use automation_macro::{Device, LuaDeviceConfig};
use lua_typed::Typed;
use rumqttc::{Publish, matches}; use rumqttc::{Publish, matches};
use serde::Deserialize; use serde::Deserialize;
use tracing::{debug, trace, warn}; use tracing::{debug, trace, warn};
#[derive(Debug, Clone, LuaDeviceConfig)] #[derive(Debug, Clone, LuaDeviceConfig, Typed)]
#[typed(as = "HueSwitchConfig")]
pub struct Config { pub struct Config {
#[device_config(flatten)] #[device_config(flatten)]
#[serde(flatten)]
pub info: InfoConfig, pub info: InfoConfig,
#[device_config(flatten)] #[device_config(flatten)]
#[serde(flatten)]
pub mqtt: MqttDeviceConfig, pub mqtt: MqttDeviceConfig,
#[device_config(from_lua)] #[device_config(from_lua)]
pub client: WrappedAsyncClient, pub client: WrappedAsyncClient,
#[device_config(from_lua, default)] #[device_config(from_lua, default)]
#[serde(default)]
pub left_callback: ActionCallback<HueSwitch>, pub left_callback: ActionCallback<HueSwitch>,
#[device_config(from_lua, default)] #[device_config(from_lua, default)]
#[serde(default)]
pub right_callback: ActionCallback<HueSwitch>, pub right_callback: ActionCallback<HueSwitch>,
#[device_config(from_lua, default)] #[device_config(from_lua, default)]
#[serde(default)]
pub left_hold_callback: ActionCallback<HueSwitch>, pub left_hold_callback: ActionCallback<HueSwitch>,
#[device_config(from_lua, default)] #[device_config(from_lua, default)]
#[serde(default)]
pub right_hold_callback: ActionCallback<HueSwitch>, pub right_hold_callback: ActionCallback<HueSwitch>,
#[device_config(from_lua, default)] #[device_config(from_lua, default)]
#[serde(default)]
pub battery_callback: ActionCallback<(HueSwitch, f32)>, pub battery_callback: ActionCallback<(HueSwitch, f32)>,
} }
crate::register_type!(Config);
#[derive(Debug, Copy, Clone, Deserialize)] #[derive(Debug, Copy, Clone, Deserialize)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]

View File

@@ -6,28 +6,36 @@ use automation_lib::event::OnMqtt;
use automation_lib::messages::{RemoteAction, RemoteMessage}; use automation_lib::messages::{RemoteAction, RemoteMessage};
use automation_lib::mqtt::WrappedAsyncClient; use automation_lib::mqtt::WrappedAsyncClient;
use automation_macro::{Device, LuaDeviceConfig}; use automation_macro::{Device, LuaDeviceConfig};
use lua_typed::Typed;
use rumqttc::{Publish, matches}; use rumqttc::{Publish, matches};
use tracing::{debug, error, trace}; use tracing::{debug, error, trace};
#[derive(Debug, Clone, LuaDeviceConfig)] #[derive(Debug, Clone, LuaDeviceConfig, Typed)]
#[typed(as = "IkeaRemoteConfig")]
pub struct Config { pub struct Config {
#[device_config(flatten)] #[device_config(flatten)]
#[serde(flatten)]
pub info: InfoConfig, pub info: InfoConfig,
#[device_config(default)] #[device_config(default)]
#[serde(default)]
pub single_button: bool, pub single_button: bool,
#[device_config(flatten)] #[device_config(flatten)]
#[serde(flatten)]
pub mqtt: MqttDeviceConfig, pub mqtt: MqttDeviceConfig,
#[device_config(from_lua)] #[device_config(from_lua)]
pub client: WrappedAsyncClient, pub client: WrappedAsyncClient,
#[device_config(from_lua, default)] #[device_config(from_lua, default)]
#[serde(default)]
pub callback: ActionCallback<(IkeaRemote, bool)>, pub callback: ActionCallback<(IkeaRemote, bool)>,
#[device_config(from_lua, default)] #[device_config(from_lua, default)]
#[serde(default)]
pub battery_callback: ActionCallback<(IkeaRemote, f32)>, pub battery_callback: ActionCallback<(IkeaRemote, f32)>,
} }
crate::register_type!(Config);
#[derive(Debug, Clone, Device)] #[derive(Debug, Clone, Device)]
pub struct IkeaRemote { pub struct IkeaRemote {

View File

@@ -8,18 +8,22 @@ use automation_macro::{Device, LuaDeviceConfig};
use bytes::{Buf, BufMut}; use bytes::{Buf, BufMut};
use google_home::errors::{self, DeviceError}; use google_home::errors::{self, DeviceError};
use google_home::traits::OnOff; use google_home::traits::OnOff;
use lua_typed::Typed;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use thiserror::Error; use thiserror::Error;
use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream; use tokio::net::TcpStream;
use tracing::trace; use tracing::trace;
#[derive(Debug, Clone, LuaDeviceConfig)] #[derive(Debug, Clone, LuaDeviceConfig, Typed)]
#[typed(as = "KasaOutletConfig")]
pub struct Config { pub struct Config {
pub identifier: String, pub identifier: String,
#[device_config(rename("ip"), with(|ip| SocketAddr::new(ip, 9999)))] #[device_config(rename("ip"), with(|ip| SocketAddr::new(ip, 9999)))]
#[typed(as = "ip")]
pub addr: SocketAddr, pub addr: SocketAddr,
} }
crate::register_type!(Config);
#[derive(Debug, Clone, Device)] #[derive(Debug, Clone, Device)]
#[device(traits(OnOff))] #[device(traits(OnOff))]

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

@@ -8,24 +8,29 @@ use automation_lib::event::OnMqtt;
use automation_lib::messages::BrightnessMessage; use automation_lib::messages::BrightnessMessage;
use automation_lib::mqtt::WrappedAsyncClient; use automation_lib::mqtt::WrappedAsyncClient;
use automation_macro::{Device, LuaDeviceConfig}; use automation_macro::{Device, LuaDeviceConfig};
use lua_typed::Typed;
use rumqttc::Publish; use rumqttc::Publish;
use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard}; use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
use tracing::{debug, trace, warn}; use tracing::{debug, trace, warn};
#[derive(Debug, Clone, LuaDeviceConfig)] #[derive(Debug, Clone, LuaDeviceConfig, Typed)]
#[typed(as = "LightSensorConfig")]
pub struct Config { pub struct Config {
pub identifier: String, pub identifier: String,
#[device_config(flatten)] #[device_config(flatten)]
#[serde(flatten)]
pub mqtt: MqttDeviceConfig, pub mqtt: MqttDeviceConfig,
pub min: isize, pub min: isize,
pub max: isize, pub max: isize,
#[device_config(from_lua, default)] #[device_config(from_lua, default)]
#[serde(default)]
pub callback: ActionCallback<(LightSensor, bool)>, pub callback: ActionCallback<(LightSensor, bool)>,
#[device_config(from_lua)] #[device_config(from_lua)]
pub client: WrappedAsyncClient, pub client: WrappedAsyncClient,
} }
crate::register_type!(Config);
const DEFAULT: bool = false; const DEFAULT: bool = false;

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,34 +20,37 @@ 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 {
#[serde(skip_serializing_if = "HashMap::is_empty")] #[serde(skip_serializing_if = "HashMap::is_empty")]
#[serde(default)]
extras: HashMap<String, String>, extras: HashMap<String, String>,
}, },
// View, // View,
// 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 +61,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 +72,15 @@ 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()))]
#[serde(default)]
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 +104,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

@@ -9,21 +9,26 @@ use automation_lib::event::OnMqtt;
use automation_lib::messages::PresenceMessage; use automation_lib::messages::PresenceMessage;
use automation_lib::mqtt::WrappedAsyncClient; use automation_lib::mqtt::WrappedAsyncClient;
use automation_macro::{Device, LuaDeviceConfig}; use automation_macro::{Device, LuaDeviceConfig};
use lua_typed::Typed;
use rumqttc::Publish; use rumqttc::Publish;
use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard}; use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
use tracing::{debug, trace, warn}; use tracing::{debug, trace, warn};
#[derive(Debug, Clone, LuaDeviceConfig)] #[derive(Debug, Clone, LuaDeviceConfig, Typed)]
#[typed(as = "PresenceConfig")]
pub struct Config { pub struct Config {
#[device_config(flatten)] #[device_config(flatten)]
#[serde(flatten)]
pub mqtt: MqttDeviceConfig, pub mqtt: MqttDeviceConfig,
#[device_config(from_lua, default)] #[device_config(from_lua, default)]
#[serde(default)]
pub callback: ActionCallback<(Presence, bool)>, pub callback: ActionCallback<(Presence, bool)>,
#[device_config(from_lua)] #[device_config(from_lua)]
pub client: WrappedAsyncClient, pub client: WrappedAsyncClient,
} }
crate::register_type!(Config);
pub const DEFAULT_PRESENCE: bool = false; pub const DEFAULT_PRESENCE: bool = false;

View File

@@ -12,21 +12,27 @@ use google_home::device;
use google_home::errors::ErrorCode; use google_home::errors::ErrorCode;
use google_home::traits::{self, Scene}; use google_home::traits::{self, Scene};
use google_home::types::Type; use google_home::types::Type;
use lua_typed::Typed;
use rumqttc::Publish; use rumqttc::Publish;
use tracing::{debug, error, trace}; use tracing::{debug, error, trace};
#[derive(Debug, Clone, LuaDeviceConfig)] #[derive(Debug, Clone, LuaDeviceConfig, Typed)]
#[typed(as = "WolConfig")]
pub struct Config { pub struct Config {
#[device_config(flatten)] #[device_config(flatten)]
#[serde(flatten)]
pub info: InfoConfig, pub info: InfoConfig,
#[device_config(flatten)] #[device_config(flatten)]
#[serde(flatten)]
pub mqtt: MqttDeviceConfig, pub mqtt: MqttDeviceConfig,
pub mac_address: MacAddress, pub mac_address: MacAddress,
#[device_config(default(Ipv4Addr::new(255, 255, 255, 255)))] #[device_config(default(Ipv4Addr::new(255, 255, 255, 255)))]
#[serde(default)]
pub broadcast_ip: Ipv4Addr, pub broadcast_ip: Ipv4Addr,
#[device_config(from_lua)] #[device_config(from_lua)]
pub client: WrappedAsyncClient, pub client: WrappedAsyncClient,
} }
crate::register_type!(Config);
#[derive(Debug, Clone, Device)] #[derive(Debug, Clone, Device)]
pub struct WakeOnLAN { pub struct WakeOnLAN {

View File

@@ -8,24 +8,29 @@ use automation_lib::event::OnMqtt;
use automation_lib::messages::PowerMessage; use automation_lib::messages::PowerMessage;
use automation_lib::mqtt::WrappedAsyncClient; use automation_lib::mqtt::WrappedAsyncClient;
use automation_macro::{Device, LuaDeviceConfig}; use automation_macro::{Device, LuaDeviceConfig};
use lua_typed::Typed;
use rumqttc::Publish; use rumqttc::Publish;
use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard}; use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
use tracing::{debug, error, trace}; use tracing::{debug, error, trace};
#[derive(Debug, Clone, LuaDeviceConfig)] #[derive(Debug, Clone, LuaDeviceConfig, Typed)]
#[typed(as = "WasherConfig")]
pub struct Config { pub struct Config {
pub identifier: String, pub identifier: String,
#[device_config(flatten)] #[device_config(flatten)]
#[serde(flatten)]
pub mqtt: MqttDeviceConfig, pub mqtt: MqttDeviceConfig,
// Power in Watt // Power in Watt
pub threshold: f32, pub threshold: f32,
#[device_config(from_lua, default)] #[device_config(from_lua, default)]
#[serde(default)]
pub done_callback: ActionCallback<Washer>, pub done_callback: ActionCallback<Washer>,
#[device_config(from_lua)] #[device_config(from_lua)]
pub client: WrappedAsyncClient, pub client: WrappedAsyncClient,
} }
crate::register_type!(Config);
#[derive(Debug)] #[derive(Debug)]
pub struct State { pub struct State {

View File

@@ -15,6 +15,7 @@ use google_home::device;
use google_home::errors::ErrorCode; use google_home::errors::ErrorCode;
use google_home::traits::{Brightness, Color, ColorSetting, ColorTemperatureRange, OnOff}; use google_home::traits::{Brightness, Color, ColorSetting, ColorTemperatureRange, OnOff};
use google_home::types::Type; use google_home::types::Type;
use lua_typed::Typed;
use rumqttc::{Publish, matches}; use rumqttc::{Publish, matches};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::json; use serde_json::json;
@@ -22,33 +23,47 @@ use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
use tracing::{debug, trace, warn}; use tracing::{debug, trace, warn};
pub trait LightState: pub trait LightState:
Debug + Clone + Default + Sync + Send + Serialize + Into<StateOnOff> + 'static Debug + Clone + Default + Sync + Send + Serialize + Into<StateOnOff> + Typed + 'static
{ {
} }
#[derive(Debug, Clone, LuaDeviceConfig)] #[derive(Debug, Clone, LuaDeviceConfig, Typed)]
pub struct Config<T: LightState> { #[typed(as = "ConfigLight")]
pub struct Config<T: LightState>
where
Light<T>: Typed,
{
#[device_config(flatten)] #[device_config(flatten)]
#[serde(flatten)]
pub info: InfoConfig, pub info: InfoConfig,
#[device_config(flatten)] #[device_config(flatten)]
#[serde(flatten)]
pub mqtt: MqttDeviceConfig, pub mqtt: MqttDeviceConfig,
#[device_config(from_lua, default)] #[device_config(from_lua, default)]
#[serde(default)]
pub callback: ActionCallback<(Light<T>, T)>, pub callback: ActionCallback<(Light<T>, T)>,
#[serde(default)]
#[device_config(from_lua)] #[device_config(from_lua)]
pub client: WrappedAsyncClient, pub client: WrappedAsyncClient,
} }
crate::register_type!(Config<StateOnOff>);
crate::register_type!(Config<StateBrightness>);
crate::register_type!(Config<StateColorTemperature>);
#[derive(Debug, Clone, Default, Serialize, Deserialize, LuaSerialize)] #[derive(Debug, Clone, Default, Serialize, Deserialize, LuaSerialize, Typed)]
#[typed(as = "LightStateOnOff")]
pub struct StateOnOff { pub struct StateOnOff {
#[serde(deserialize_with = "state_deserializer")] #[serde(deserialize_with = "state_deserializer")]
state: bool, state: bool,
} }
impl LightState for StateOnOff {} impl LightState for StateOnOff {}
crate::register_type!(StateOnOff);
#[derive(Debug, Clone, Default, Serialize, Deserialize, LuaSerialize)] #[derive(Debug, Clone, Default, Serialize, Deserialize, LuaSerialize, Typed)]
#[typed(as = "LightStateBrightness")]
pub struct StateBrightness { pub struct StateBrightness {
#[serde(deserialize_with = "state_deserializer")] #[serde(deserialize_with = "state_deserializer")]
state: bool, state: bool,
@@ -56,6 +71,7 @@ pub struct StateBrightness {
} }
impl LightState for StateBrightness {} impl LightState for StateBrightness {}
crate::register_type!(StateBrightness);
impl From<StateBrightness> for StateOnOff { impl From<StateBrightness> for StateOnOff {
fn from(state: StateBrightness) -> Self { fn from(state: StateBrightness) -> Self {
@@ -63,13 +79,15 @@ impl From<StateBrightness> for StateOnOff {
} }
} }
#[derive(Debug, Clone, Default, Serialize, Deserialize, LuaSerialize)] #[derive(Debug, Clone, Default, Serialize, Deserialize, LuaSerialize, Typed)]
#[typed(as = "LightStateColorTemperature")]
pub struct StateColorTemperature { pub struct StateColorTemperature {
#[serde(deserialize_with = "state_deserializer")] #[serde(deserialize_with = "state_deserializer")]
state: bool, state: bool,
brightness: f32, brightness: f32,
color_temp: u32, color_temp: u32,
} }
crate::register_type!(StateColorTemperature);
impl LightState for StateColorTemperature {} impl LightState for StateColorTemperature {}
@@ -92,7 +110,10 @@ impl From<StateColorTemperature> for StateBrightness {
#[device(traits(OnOff for LightOnOff, LightBrightness, LightColorTemperature))] #[device(traits(OnOff for LightOnOff, LightBrightness, LightColorTemperature))]
#[device(traits(Brightness for LightBrightness, LightColorTemperature))] #[device(traits(Brightness for LightBrightness, LightColorTemperature))]
#[device(traits(ColorSetting for LightColorTemperature))] #[device(traits(ColorSetting for LightColorTemperature))]
pub struct Light<T: LightState> { pub struct Light<T: LightState>
where
Light<T>: Typed,
{
config: Config<T>, config: Config<T>,
state: Arc<RwLock<T>>, state: Arc<RwLock<T>>,
@@ -107,7 +128,10 @@ crate::register_device!(LightBrightness);
pub type LightColorTemperature = Light<StateColorTemperature>; pub type LightColorTemperature = Light<StateColorTemperature>;
crate::register_device!(LightColorTemperature); crate::register_device!(LightColorTemperature);
impl<T: LightState> Light<T> { impl<T: LightState> Light<T>
where
Light<T>: Typed,
{
async fn state(&self) -> RwLockReadGuard<'_, T> { async fn state(&self) -> RwLockReadGuard<'_, T> {
self.state.read().await self.state.read().await
} }
@@ -118,7 +142,10 @@ impl<T: LightState> Light<T> {
} }
#[async_trait] #[async_trait]
impl<T: LightState> LuaDeviceCreate for Light<T> { impl<T: LightState> LuaDeviceCreate for Light<T>
where
Light<T>: Typed,
{
type Config = Config<T>; type Config = Config<T>;
type Error = rumqttc::ClientError; type Error = rumqttc::ClientError;
@@ -137,7 +164,10 @@ impl<T: LightState> LuaDeviceCreate for Light<T> {
} }
} }
impl<T: LightState> Device for Light<T> { impl<T: LightState> Device for Light<T>
where
Light<T>: Typed,
{
fn get_id(&self) -> String { fn get_id(&self) -> String {
self.config.info.identifier() self.config.info.identifier()
} }
@@ -257,7 +287,10 @@ impl OnMqtt for LightColorTemperature {
} }
#[async_trait] #[async_trait]
impl<T: LightState> google_home::Device for Light<T> { impl<T: LightState> google_home::Device for Light<T>
where
Light<T>: Typed,
{
fn get_device_type(&self) -> Type { fn get_device_type(&self) -> Type {
Type::Light Type::Light
} }
@@ -288,6 +321,7 @@ impl<T: LightState> google_home::Device for Light<T> {
impl<T> OnOff for Light<T> impl<T> OnOff for Light<T>
where where
T: LightState, T: LightState,
Light<T>: Typed,
{ {
async fn on(&self) -> Result<bool, ErrorCode> { async fn on(&self) -> Result<bool, ErrorCode> {
let state = self.state().await; let state = self.state().await;
@@ -327,6 +361,7 @@ impl<T> Brightness for Light<T>
where where
T: LightState, T: LightState,
T: Into<StateBrightness>, T: Into<StateBrightness>,
Light<T>: Typed,
{ {
async fn brightness(&self) -> Result<u8, ErrorCode> { async fn brightness(&self) -> Result<u8, ErrorCode> {
let state = self.state().await; let state = self.state().await;
@@ -368,6 +403,7 @@ impl<T> ColorSetting for Light<T>
where where
T: LightState, T: LightState,
T: Into<StateColorTemperature>, T: Into<StateColorTemperature>,
Light<T>: Typed,
{ {
fn color_temperature_range(&self) -> ColorTemperatureRange { fn color_temperature_range(&self) -> ColorTemperatureRange {
ColorTemperatureRange { ColorTemperatureRange {

View File

@@ -15,6 +15,7 @@ use google_home::device;
use google_home::errors::ErrorCode; use google_home::errors::ErrorCode;
use google_home::traits::OnOff; use google_home::traits::OnOff;
use google_home::types::Type; use google_home::types::Type;
use lua_typed::Typed;
use rumqttc::{Publish, matches}; use rumqttc::{Publish, matches};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::json; use serde_json::json;
@@ -22,15 +23,16 @@ use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
use tracing::{debug, trace, warn}; use tracing::{debug, trace, warn};
pub trait OutletState: pub trait OutletState:
Debug + Clone + Default + Sync + Send + Serialize + Into<StateOnOff> + 'static Debug + Clone + Default + Sync + Send + Serialize + Into<StateOnOff> + Typed + 'static
{ {
} }
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Copy)] #[derive(Debug, Clone, Deserialize, PartialEq, Eq, Copy, Typed)]
pub enum OutletType { pub enum OutletType {
Outlet, Outlet,
Kettle, Kettle,
} }
crate::register_type!(OutletType);
impl From<OutletType> for Type { impl From<OutletType> for Type {
fn from(outlet: OutletType) -> Self { fn from(outlet: OutletType) -> Self {
@@ -41,36 +43,50 @@ impl From<OutletType> for Type {
} }
} }
#[derive(Debug, Clone, LuaDeviceConfig)] #[derive(Debug, Clone, LuaDeviceConfig, Typed)]
pub struct Config<T: OutletState> { #[typed(as = "ConfigOutlet")]
pub struct Config<T: OutletState>
where
Outlet<T>: Typed,
{
#[device_config(flatten)] #[device_config(flatten)]
#[serde(flatten)]
pub info: InfoConfig, pub info: InfoConfig,
#[device_config(flatten)] #[device_config(flatten)]
#[serde(flatten)]
pub mqtt: MqttDeviceConfig, pub mqtt: MqttDeviceConfig,
#[device_config(default(OutletType::Outlet))] #[device_config(default(OutletType::Outlet))]
#[serde(default)]
pub outlet_type: OutletType, pub outlet_type: OutletType,
#[device_config(from_lua, default)] #[device_config(from_lua, default)]
#[serde(default)]
pub callback: ActionCallback<(Outlet<T>, T)>, pub callback: ActionCallback<(Outlet<T>, T)>,
#[device_config(from_lua)] #[device_config(from_lua)]
pub client: WrappedAsyncClient, pub client: WrappedAsyncClient,
} }
crate::register_type!(Config<StateOnOff>);
crate::register_type!(Config<StatePower>);
#[derive(Debug, Clone, Default, Serialize, Deserialize, LuaSerialize)] #[derive(Debug, Clone, Default, Serialize, Deserialize, LuaSerialize, Typed)]
#[typed(as = "OutletStateOnOff")]
pub struct StateOnOff { pub struct StateOnOff {
#[serde(deserialize_with = "state_deserializer")] #[serde(deserialize_with = "state_deserializer")]
state: bool, state: bool,
} }
crate::register_type!(StateOnOff);
impl OutletState for StateOnOff {} impl OutletState for StateOnOff {}
#[derive(Debug, Clone, Default, Serialize, Deserialize, LuaSerialize)] #[derive(Debug, Clone, Default, Serialize, Deserialize, LuaSerialize, Typed)]
#[typed(as = "OutletStatePower")]
pub struct StatePower { pub struct StatePower {
#[serde(deserialize_with = "state_deserializer")] #[serde(deserialize_with = "state_deserializer")]
state: bool, state: bool,
power: f64, power: f64,
} }
crate::register_type!(StatePower);
impl OutletState for StatePower {} impl OutletState for StatePower {}
@@ -82,7 +98,10 @@ impl From<StatePower> for StateOnOff {
#[derive(Debug, Clone, Device)] #[derive(Debug, Clone, Device)]
#[device(traits(OnOff for OutletOnOff, OutletPower))] #[device(traits(OnOff for OutletOnOff, OutletPower))]
pub struct Outlet<T: OutletState> { pub struct Outlet<T: OutletState>
where
Outlet<T>: Typed,
{
config: Config<T>, config: Config<T>,
state: Arc<RwLock<T>>, state: Arc<RwLock<T>>,
@@ -94,7 +113,10 @@ crate::register_device!(OutletOnOff);
pub type OutletPower = Outlet<StatePower>; pub type OutletPower = Outlet<StatePower>;
crate::register_device!(OutletPower); crate::register_device!(OutletPower);
impl<T: OutletState> Outlet<T> { impl<T: OutletState> Outlet<T>
where
Outlet<T>: Typed,
{
async fn state(&self) -> RwLockReadGuard<'_, T> { async fn state(&self) -> RwLockReadGuard<'_, T> {
self.state.read().await self.state.read().await
} }
@@ -105,7 +127,10 @@ impl<T: OutletState> Outlet<T> {
} }
#[async_trait] #[async_trait]
impl<T: OutletState> LuaDeviceCreate for Outlet<T> { impl<T: OutletState> LuaDeviceCreate for Outlet<T>
where
Outlet<T>: Typed,
{
type Config = Config<T>; type Config = Config<T>;
type Error = rumqttc::ClientError; type Error = rumqttc::ClientError;
@@ -124,7 +149,10 @@ impl<T: OutletState> LuaDeviceCreate for Outlet<T> {
} }
} }
impl<T: OutletState> Device for Outlet<T> { impl<T: OutletState> Device for Outlet<T>
where
Outlet<T>: Typed,
{
fn get_id(&self) -> String { fn get_id(&self) -> String {
self.config.info.identifier() self.config.info.identifier()
} }
@@ -201,7 +229,10 @@ impl OnMqtt for OutletPower {
} }
#[async_trait] #[async_trait]
impl<T: OutletState> google_home::Device for Outlet<T> { impl<T: OutletState> google_home::Device for Outlet<T>
where
Outlet<T>: Typed,
{
fn get_device_type(&self) -> Type { fn get_device_type(&self) -> Type {
self.config.outlet_type.into() self.config.outlet_type.into()
} }
@@ -232,6 +263,7 @@ impl<T: OutletState> google_home::Device for Outlet<T> {
impl<T> OnOff for Outlet<T> impl<T> OnOff for Outlet<T>
where where
T: OutletState, T: OutletState,
Outlet<T>: Typed,
{ {
async fn on(&self) -> Result<bool, ErrorCode> { async fn on(&self) -> Result<bool, ErrorCode> {
let state = self.state().await; let state = self.state().await;

View File

@@ -13,6 +13,7 @@ google_home = { workspace = true }
hostname = { workspace = true } hostname = { workspace = true }
indexmap = { workspace = true } indexmap = { workspace = true }
inventory = { workspace = true } inventory = { workspace = true }
lua_typed = { workspace = true }
mlua = { workspace = true } mlua = { workspace = true }
rumqttc = { workspace = true } rumqttc = { workspace = true }
serde = { workspace = true } serde = { workspace = true }

View File

@@ -1,6 +1,7 @@
use std::marker::PhantomData; use std::marker::PhantomData;
use futures::future::try_join_all; use futures::future::try_join_all;
use lua_typed::Typed;
use mlua::{FromLua, IntoLuaMulti}; use mlua::{FromLua, IntoLuaMulti};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -9,6 +10,23 @@ pub struct ActionCallback<P> {
_parameters: PhantomData<P>, _parameters: PhantomData<P>,
} }
impl<A: Typed> Typed for ActionCallback<A> {
fn type_name() -> String {
let type_name = A::type_name();
format!("fun(_: {type_name}) | fun(_: {type_name})[]")
}
}
impl<A: Typed, B: Typed> 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 // 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. // requires all types part of P to implement default, even if they never actually get constructed.
// By manually implemented Default it works fine. // By manually implemented Default it works fine.

View File

@@ -1,6 +1,7 @@
use std::net::{Ipv4Addr, SocketAddr}; use std::net::{Ipv4Addr, SocketAddr};
use std::time::Duration; use std::time::Duration;
use lua_typed::Typed;
use rumqttc::{MqttOptions, Transport}; use rumqttc::{MqttOptions, Transport};
use serde::Deserialize; use serde::Deserialize;
@@ -52,7 +53,7 @@ fn default_fulfillment_port() -> u16 {
7878 7878
} }
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize, Typed)]
pub struct InfoConfig { pub struct InfoConfig {
pub name: String, pub name: String,
pub room: Option<String>, pub room: Option<String>,
@@ -68,7 +69,7 @@ impl InfoConfig {
} }
} }
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize, Typed)]
pub struct MqttDeviceConfig { pub struct MqttDeviceConfig {
pub topic: String, pub topic: String,
} }

View File

@@ -1,5 +1,5 @@
#![allow(incomplete_features)]
#![feature(iterator_try_collect)] #![feature(iterator_try_collect)]
#![feature(with_negative_coherence)]
use tracing::debug; use tracing::debug;

View File

@@ -1,5 +1,6 @@
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
use lua_typed::Typed;
use mlua::FromLua; use mlua::FromLua;
use rumqttc::{AsyncClient, Event, EventLoop, Incoming}; use rumqttc::{AsyncClient, Event, EventLoop, Incoming};
use tracing::{debug, warn}; use tracing::{debug, warn};
@@ -9,6 +10,12 @@ use crate::event::{self, EventChannel};
#[derive(Debug, Clone, FromLua)] #[derive(Debug, Clone, FromLua)]
pub struct WrappedAsyncClient(pub AsyncClient); pub struct WrappedAsyncClient(pub AsyncClient);
impl Typed for WrappedAsyncClient {
fn type_name() -> String {
"AsyncClient".into()
}
}
impl Deref for WrappedAsyncClient { impl Deref for WrappedAsyncClient {
type Target = AsyncClient; type Target = AsyncClient;

View File

@@ -138,6 +138,12 @@ impl quote::ToTokens for Implementation {
add_methods, add_methods,
} = &self; } = &self;
let interfaces: String = traits
.0
.iter()
.map(|tr| format!(", Interface{tr}"))
.collect();
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) {
@@ -163,6 +169,31 @@ impl quote::ToTokens for Implementation {
)* )*
} }
} }
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}: InterfaceDevice{}\nlocal {type_name}\n", #interfaces))
}
fn generate_members() -> Option<String> {
let mut output = String::new();
let type_name = <Self as ::lua_typed::Typed>::type_name();
output += &format!("devices.{type_name} = {{}}\n");
let config_name = <<Self as ::automation_lib::device::LuaDeviceCreate>::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)
}
}
}); });
} }
} }

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
@@ -35,7 +39,10 @@ local ntfy = devices.Ntfy.new({
}) })
device_manager:add(ntfy) device_manager:add(ntfy)
--- @type {[string]: number}
local low_battery = {} local low_battery = {}
--- @param device InterfaceDevice
--- @param battery number
local function check_battery(device, battery) local function check_battery(device, battery)
local id = device:get_id() local id = device:get_id()
if battery < 15 then if battery < 15 then
@@ -66,11 +73,13 @@ device_manager:schedule("0 0 21 */1 * *", function()
}) })
end) end)
local on_presence = { --- @class OnPresence
add = function(self, f) --- @field [integer] fun(presence: boolean)
local on_presence = {}
--- @param f fun(presence: boolean)
function on_presence:add(f)
self[#self + 1] = f self[#self + 1] = f
end, end
}
local presence_system = devices.Presence.new({ local presence_system = devices.Presence.new({
topic = mqtt_automation("presence/+/#"), topic = mqtt_automation("presence/+/#"),
@@ -110,11 +119,13 @@ on_presence:add(function(presence)
}) })
end) end)
local window_sensors = { --- @class WindowSensor
add = function(self, f) --- @field [integer] InterfaceOpenClose
self[#self + 1] = f local window_sensors = {}
end, --- @param sensor InterfaceOpenClose
} function window_sensors:add(sensor)
self[#self + 1] = sensor
end
on_presence:add(function(presence) on_presence:add(function(presence)
if not presence then if not presence then
local open = {} local open = {}
@@ -139,6 +150,7 @@ on_presence:add(function(presence)
end end
end) end)
--- @param device InterfaceOnOff
local function turn_off_when_away(device) local function turn_off_when_away(device)
on_presence:add(function(presence) on_presence:add(function(presence)
if not presence then if not presence then
@@ -147,11 +159,13 @@ local function turn_off_when_away(device)
end) end)
end end
local on_light = { --- @class OnLight
add = function(self, f) --- @field [integer] fun(light: boolean)
local on_light = {}
--- @param f fun(light: boolean)
function on_light:add(f)
self[#self + 1] = f self[#self + 1] = f
end, end
}
device_manager:add(devices.LightSensor.new({ device_manager:add(devices.LightSensor.new({
identifier = "living_light_sensor", identifier = "living_light_sensor",
topic = mqtt_z2m("living/light"), topic = mqtt_z2m("living/light"),
@@ -290,6 +304,8 @@ device_manager:add(devices.IkeaRemote.new({
local function kettle_timeout() local function kettle_timeout()
local timeout = utils.Timeout.new() local timeout = utils.Timeout.new()
--- @param self InterfaceOnOff
--- @param state StatePower
return function(self, state) return function(self, state)
if state.state and state.power < 100 then if state.state and state.power < 100 then
timeout:start(3, function() timeout:start(3, function()
@@ -301,6 +317,7 @@ local function kettle_timeout()
end end
end end
--- @type OutletPower
local kettle = devices.OutletPower.new({ local kettle = devices.OutletPower.new({
outlet_type = "Kettle", outlet_type = "Kettle",
name = "Kettle", name = "Kettle",
@@ -312,6 +329,7 @@ local kettle = devices.OutletPower.new({
turn_off_when_away(kettle) turn_off_when_away(kettle)
device_manager:add(kettle) device_manager:add(kettle)
--- @param on boolean
local function set_kettle(_, on) local function set_kettle(_, on)
kettle:set_on(on) kettle:set_on(on)
end end
@@ -336,9 +354,12 @@ device_manager:add(devices.IkeaRemote.new({
battery_callback = check_battery, battery_callback = check_battery,
})) }))
--- @param duration number
local function off_timeout(duration) local function off_timeout(duration)
local timeout = utils.Timeout.new() local timeout = utils.Timeout.new()
--- @param self InterfaceOnOff
--- @param state {state: boolean}
return function(self, state) return function(self, state)
if state.state then if state.state then
timeout:start(duration, function() timeout:start(duration, function()

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,322 @@
---@meta
local devices
---@class LightSensor: InterfaceDevice
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
---@alias Priority
---| "min"
---| "low"
---| "default"
---| "high"
---| "max"
---@class Action
---@field action
---| "broadcast"
---@field extras table<string, string>?
---@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 NtfyConfig
---@field url string?
---@field topic string
local NtfyConfig
---@class Ntfy: InterfaceDevice
local Ntfy
devices.Ntfy = {}
---@param config NtfyConfig
---@return Ntfy
function devices.Ntfy.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 HueSwitch: InterfaceDevice
local HueSwitch
devices.HueSwitch = {}
---@param config HueSwitchConfig
---@return HueSwitch
function devices.HueSwitch.new(config) end
---@class AirFilterConfig
---@field name string
---@field room string?
---@field url string
local AirFilterConfig
---@class AirFilter: InterfaceDevice, InterfaceOnOff
local AirFilter
devices.AirFilter = {}
---@param config AirFilterConfig
---@return AirFilter
function devices.AirFilter.new(config) end
---@class HueBridge: InterfaceDevice
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 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: InterfaceDevice
local WakeOnLAN
devices.WakeOnLAN = {}
---@param config WolConfig
---@return WakeOnLAN
function devices.WakeOnLAN.new(config) end
---@class OutletStateOnOff
---@field state boolean
local OutletStateOnOff
---@class OutletOnOff: InterfaceDevice, InterfaceOnOff
local OutletOnOff
devices.OutletOnOff = {}
---@param config ConfigOutletOutletStateOnOff
---@return OutletOnOff
function devices.OutletOnOff.new(config) end
---@class OutletPower: InterfaceDevice, InterfaceOnOff
local OutletPower
devices.OutletPower = {}
---@param config ConfigOutletOutletStatePower
---@return OutletPower
function devices.OutletPower.new(config) end
---@class ConfigOutletOutletStatePower
---@field name string
---@field room string?
---@field topic string
---@field outlet_type OutletType?
---@field callback fun(_: OutletPower, _: OutletStatePower) | fun(_: OutletPower, _: OutletStatePower)[]?
---@field client AsyncClient
local ConfigOutletOutletStatePower
---@alias OutletType
---| "Outlet"
---| "Kettle"
---@class ConfigOutletOutletStateOnOff
---@field name string
---@field room string?
---@field topic string
---@field outlet_type OutletType?
---@field callback fun(_: OutletOnOff, _: OutletStateOnOff) | fun(_: OutletOnOff, _: OutletStateOnOff)[]?
---@field client AsyncClient
local ConfigOutletOutletStateOnOff
---@class OutletStatePower
---@field state boolean
---@field power number
local OutletStatePower
---@class ConfigLightLightStateBrightness
---@field name string
---@field room string?
---@field topic string
---@field callback fun(_: LightBrightness, _: LightStateBrightness) | fun(_: LightBrightness, _: LightStateBrightness)[]
---@field client AsyncClient
local ConfigLightLightStateBrightness
---@class ConfigLightLightStateColorTemperature
---@field name string
---@field room string?
---@field topic string
---@field callback fun(_: LightColorTemperature, _: LightStateColorTemperature) | fun(_: LightColorTemperature, _: LightStateColorTemperature)[]
---@field client AsyncClient
local ConfigLightLightStateColorTemperature
---@class LightOnOff: InterfaceDevice, InterfaceOnOff
local LightOnOff
devices.LightOnOff = {}
---@param config ConfigLightLightStateOnOff
---@return LightOnOff
function devices.LightOnOff.new(config) end
---@class LightColorTemperature: InterfaceDevice, InterfaceOnOff, InterfaceBrightness, InterfaceColorSetting
local LightColorTemperature
devices.LightColorTemperature = {}
---@param config ConfigLightLightStateColorTemperature
---@return LightColorTemperature
function devices.LightColorTemperature.new(config) end
---@class LightBrightness: InterfaceDevice, InterfaceOnOff, InterfaceBrightness
local LightBrightness
devices.LightBrightness = {}
---@param config ConfigLightLightStateBrightness
---@return LightBrightness
function devices.LightBrightness.new(config) end
---@class LightStateBrightness
---@field state boolean
---@field brightness number
local LightStateBrightness
---@class ConfigLightLightStateOnOff
---@field name string
---@field room string?
---@field topic string
---@field callback fun(_: LightOnOff, _: LightStateOnOff) | fun(_: LightOnOff, _: LightStateOnOff)[]
---@field client AsyncClient
local ConfigLightLightStateOnOff
---@class LightStateOnOff
---@field state boolean
local LightStateOnOff
---@class LightStateColorTemperature
---@field state boolean
---@field brightness number
---@field color_temp integer
local LightStateColorTemperature
---@class PresenceConfig
---@field topic string
---@field callback fun(_: Presence, _: boolean) | fun(_: Presence, _: boolean)[]?
---@field client AsyncClient
local PresenceConfig
---@class Presence: InterfaceDevice
local Presence
devices.Presence = {}
---@param config PresenceConfig
---@return Presence
function devices.Presence.new(config) end
---@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: InterfaceDevice
local IkeaRemote
devices.IkeaRemote = {}
---@param config IkeaRemoteConfig
---@return IkeaRemote
function devices.IkeaRemote.new(config) end
---@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: InterfaceDevice
local Washer
devices.Washer = {}
---@param config WasherConfig
---@return Washer
function devices.Washer.new(config) end
---@class HueGroup: InterfaceDevice, InterfaceOnOff
local HueGroup
devices.HueGroup = {}
---@param config HueGroupConfig
---@return HueGroup
function devices.HueGroup.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 KasaOutletConfig
---@field identifier string
---@field ip string
local KasaOutletConfig
---@class KasaOutlet: InterfaceDevice, InterfaceOnOff
local KasaOutlet
devices.KasaOutlet = {}
---@param config KasaOutletConfig
---@return KasaOutlet
function devices.KasaOutlet.new(config) end
---@class ContactSensor: InterfaceDevice, InterfaceOpenClose
local ContactSensor
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
return devices

View File

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

View File

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