feat: WIP
All checks were successful
Build and deploy / build (push) Successful in 10m39s
Build and deploy / Deploy container (push) Has been skipped

This commit is contained in:
2025-09-18 01:45:31 +02:00
parent b784cfed4a
commit 2763690bc0
32 changed files with 778 additions and 52 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -22,20 +22,22 @@ macro_rules! register_device {
stringify!($device),
::mlua::Lua::create_proxy::<$device>
));
crate::register_type!($device);
};
}
pub(crate) use register_device;
type RegisterFn = fn(lua: &mlua::Lua) -> mlua::Result<mlua::AnyUserData>;
type RegisterDeviceFn = fn(lua: &mlua::Lua) -> mlua::Result<mlua::AnyUserData>;
pub struct RegisteredDevice {
name: &'static str,
register_fn: RegisterFn,
register_fn: RegisterDeviceFn,
}
impl RegisteredDevice {
pub const fn new(name: &'static str, register_fn: RegisterFn) -> Self {
pub const fn new(name: &'static str, register_fn: RegisterDeviceFn) -> Self {
Self { name, register_fn }
}
@@ -64,3 +66,28 @@ pub fn create_module(lua: &mlua::Lua) -> mlua::Result<mlua::Table> {
}
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::mqtt::WrappedAsyncClient;
use automation_macro::{Device, LuaDeviceConfig};
use lua_typed::Typed;
use rumqttc::Publish;
use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
use tracing::{debug, trace, warn};
#[derive(Debug, Clone, LuaDeviceConfig)]
#[derive(Debug, Clone, LuaDeviceConfig, Typed)]
#[typed(as = "LightSensorConfig")]
pub struct Config {
pub identifier: String,
#[device_config(flatten)]
#[serde(flatten)]
pub mqtt: MqttDeviceConfig,
pub min: isize,
pub max: isize,
#[device_config(from_lua, default)]
#[serde(default)]
pub callback: ActionCallback<(LightSensor, bool)>,
#[device_config(from_lua)]
pub client: WrappedAsyncClient,
}
crate::register_type!(Config);
const DEFAULT: bool = false;

View File

@@ -4,12 +4,13 @@ use std::convert::Infallible;
use async_trait::async_trait;
use automation_lib::device::{Device, LuaDeviceCreate};
use automation_macro::{Device, LuaDeviceConfig};
use lua_typed::Typed;
use mlua::LuaSerdeExt;
use serde::{Deserialize, Serialize};
use serde_repr::*;
use tracing::{error, trace, warn};
#[derive(Debug, Serialize_repr, Deserialize, Clone, Copy)]
#[derive(Debug, Serialize_repr, Deserialize, Clone, Copy, Typed)]
#[repr(u8)]
#[serde(rename_all = "snake_case")]
pub enum Priority {
@@ -19,34 +20,37 @@ pub enum Priority {
High,
Max,
}
crate::register_type!(Priority);
#[derive(Debug, Serialize, Deserialize, Clone)]
#[derive(Debug, Serialize, Deserialize, Clone, Typed)]
#[serde(rename_all = "snake_case", tag = "action")]
pub enum ActionType {
Broadcast {
#[serde(skip_serializing_if = "HashMap::is_empty")]
#[serde(default)]
extras: HashMap<String, String>,
},
// View,
// Http
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[derive(Debug, Serialize, Deserialize, Clone, Typed)]
pub struct Action {
#[serde(flatten)]
pub action: ActionType,
pub label: String,
pub clear: Option<bool>,
}
crate::register_type!(Action);
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, Typed)]
struct NotificationFinal {
topic: String,
#[serde(flatten)]
inner: Notification,
}
#[derive(Debug, Serialize, Clone, Deserialize)]
#[derive(Debug, Serialize, Clone, Deserialize, Typed)]
pub struct Notification {
title: String,
message: Option<String>,
@@ -57,6 +61,7 @@ pub struct Notification {
#[serde(skip_serializing_if = "Vec::is_empty", default = "Default::default")]
actions: Vec<Action>,
}
crate::register_type!(Notification);
impl Notification {
fn finalize(self, topic: &str) -> NotificationFinal {
@@ -67,12 +72,15 @@ impl Notification {
}
}
#[derive(Debug, Clone, LuaDeviceConfig)]
#[derive(Debug, Clone, LuaDeviceConfig, Typed)]
#[typed(as = "NtfyConfig")]
pub struct Config {
#[device_config(default("https://ntfy.sh".into()))]
#[serde(default)]
pub url: String,
pub topic: String,
}
crate::register_type!(Config);
#[derive(Debug, Clone, Device)]
#[device(add_methods(Self::add_methods))]
@@ -96,6 +104,24 @@ impl Ntfy {
}
}
// impl Typed for Ntfy {
// fn type_name() -> String {
// "Ntfy".into()
// }
//
// fn generate_header() -> Option<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]
impl LuaDeviceCreate for Ntfy {
type Config = Config;

View File

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

View File

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

View File

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

View File

@@ -15,6 +15,7 @@ use google_home::device;
use google_home::errors::ErrorCode;
use google_home::traits::{Brightness, Color, ColorSetting, ColorTemperatureRange, OnOff};
use google_home::types::Type;
use lua_typed::Typed;
use rumqttc::{Publish, matches};
use serde::{Deserialize, Serialize};
use serde_json::json;
@@ -40,6 +41,13 @@ pub struct Config<T: LightState> {
pub client: WrappedAsyncClient,
}
// TODO: Fix the derive macro so this is supported
impl<T: LightState> Typed for Config<T> {
fn type_name() -> String {
"LightConfig".into()
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, LuaSerialize)]
pub struct StateOnOff {
#[serde(deserialize_with = "state_deserializer")]

View File

@@ -15,6 +15,7 @@ use google_home::device;
use google_home::errors::ErrorCode;
use google_home::traits::OnOff;
use google_home::types::Type;
use lua_typed::Typed;
use rumqttc::{Publish, matches};
use serde::{Deserialize, Serialize};
use serde_json::json;
@@ -57,6 +58,13 @@ pub struct Config<T: OutletState> {
pub client: WrappedAsyncClient,
}
// TODO: Fix the derive macro so this is supported
impl<T: OutletState> Typed for Config<T> {
fn type_name() -> String {
"OutletConfig".into()
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, LuaSerialize)]
pub struct StateOnOff {
#[serde(deserialize_with = "state_deserializer")]