Compare commits

...

4 Commits

Author SHA1 Message Date
14aabe202d
Updated rust toolchain
All checks were successful
Build and deploy / Build application (push) Successful in 4m7s
Build and deploy / Build container (push) Successful in 1m2s
Build and deploy / Deploy container (push) Successful in 35s
2024-12-08 00:57:57 +01:00
e8d5698835
Updated dependencies 2024-12-08 00:53:31 +01:00
8877b24e84
Reorganized project 2024-12-08 00:15:03 +01:00
42f391cde6
Removed duplicate OnMqtt entry 2024-12-07 22:33:52 +01:00
37 changed files with 3918 additions and 1153 deletions

4314
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -9,45 +9,11 @@ members = [
"automation_cast", "automation_cast",
"google_home/google_home", "google_home/google_home",
"google_home/google_home_macro", "google_home/google_home_macro",
"automation_devices",
"automation_lib",
] ]
[workspace.dependencies]
[dependencies]
automation_macro = { path = "./automation_macro" }
automation_cast = { path = "./automation_cast/" }
rumqttc = "0.18"
serde = { version = "1.0.149", features = ["derive"] }
serde_json = "1.0.89"
google_home = { path = "./google_home/google_home/" }
paste = "1.0.10"
tokio = { version = "1", features = ["rt-multi-thread"] }
dotenvy = "0.15.0"
reqwest = { version = "0.11.13", features = [
"json",
"rustls-tls",
], default-features = false } # Use rustls, since the other packages also use rustls
axum = "0.6.1"
serde_repr = "0.1.10"
tracing = "0.1.37"
bytes = "1.3.0"
pollster = "0.2.5"
regex = "1.7.0"
async-trait = "0.1.61"
futures = "0.3.25"
eui48 = { version = "1.1.0", default-features = false, features = [
"disp_hexstring",
"serde",
] }
thiserror = "1.0.38"
anyhow = "1.0.68"
wakey = "0.3.0"
console-subscriber = "0.1.8"
tracing-subscriber = "0.3.16"
serde_with = "3.2.0"
enum_dispatch = "0.3.12"
indexmap = { version = "2.0.0", features = ["serde"] }
serde_yaml = "0.9.27"
tokio-cron-scheduler = "0.9.4"
mlua = { version = "0.10.1", features = [ mlua = { version = "0.10.1", features = [
"lua54", "lua54",
"vendored", "vendored",
@ -56,12 +22,67 @@ mlua = { version = "0.10.1", features = [
"async", "async",
"send", "send",
] } ] }
hostname = "0.4.0" automation_macro = { path = "./automation_macro" }
tokio-util = { version = "0.7.11", features = ["full"] } automation_cast = { path = "./automation_cast" }
uuid = "1.8.0" automation_lib = { path = "./automation_lib" }
automation_devices = { path = "./automation_devices" }
google_home = { path = "./google_home/google_home" }
google_home_macro = { path = "./google_home/google_home_macro" }
tokio = { version = "1", features = ["rt-multi-thread"] }
rumqttc = "0.24.0"
tracing = "0.1.37"
anyhow = "1.0.68"
async-trait = "0.1.83"
axum = "0.7.9"
bytes = "1.3.0"
dotenvy = "0.15.0"
dyn-clone = "1.0.17" dyn-clone = "1.0.17"
eui48 = { version = "1.1.0", features = [
"disp_hexstring",
"serde",
], default-features = false }
futures = "0.3.25"
hostname = "0.4.0"
impls = "1.0.3" impls = "1.0.3"
zigbee2mqtt-types = { version = "0.2.0", features = ["debug", "philips"] } indexmap = { version = "2.0.0", features = ["serde"] }
itertools = "0.13.0"
json_value_merge = "2.0.0"
pollster = "0.4.0"
proc-macro2 = "1.0.81"
quote = "1.0.36"
reqwest = { version = "0.12.9", features = [
"json",
"rustls-tls",
], default-features = false } # Use rustls, since the other packages also use rustls
serde = { version = "1.0.149", features = ["derive"] }
serde_json = "1.0.89"
serde_repr = "0.1.10"
syn = { version = "2.0.60", features = ["extra-traits", "full"] }
thiserror = "2.0.5"
tokio-cron-scheduler = "0.13.0"
tokio-util = { version = "0.7.11", features = ["full"] }
tracing-subscriber = "0.3.16"
uuid = "1.8.0"
wakey = "0.3.0"
zigbee2mqtt-types = { version = "0.4.0", features = ["debug", "philips"] }
[dependencies]
automation_lib = { workspace = true }
automation_devices = { workspace = true }
google_home = { workspace = true }
mlua = { workspace = true }
tokio = { workspace = true }
hostname = { workspace = true }
rumqttc = { workspace = true }
axum = { workspace = true }
tracing = { workspace = true }
anyhow = { workspace = true }
dotenvy = { workspace = true }
tracing-subscriber = { workspace = true }
serde = { workspace = true }
thiserror = { workspace = true }
serde_json = { workspace = true }
reqwest = { workspace = true }
[patch.crates-io] [patch.crates-io]
wakey = { git = "https://git.huizinga.dev/Dreaded_X/wakey" } wakey = { git = "https://git.huizinga.dev/Dreaded_X/wakey" }

View File

@ -0,0 +1,27 @@
[package]
name = "automation_devices"
version = "0.1.0"
edition = "2021"
[dependencies]
automation_lib = { workspace = true }
automation_macro = { workspace = true }
automation_cast = { workspace = true }
google_home = { workspace = true }
mlua = { workspace = true }
async-trait = { workspace = true }
dyn-clone = { workspace = true }
rumqttc = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }
serde_json = { workspace = true }
impls = { workspace = true }
serde = { workspace = true }
reqwest = { workspace = true } # Use rustls, since the other packages also use rustls
anyhow = { workspace = true }
zigbee2mqtt-types = { workspace = true }
axum = { workspace = true }
bytes = { workspace = true }
thiserror = { workspace = true }
eui48 = { workspace = true }
wakey = { workspace = true }

View File

@ -1,6 +1,11 @@
use std::sync::Arc; use std::sync::Arc;
use async_trait::async_trait; use async_trait::async_trait;
use automation_lib::config::{InfoConfig, MqttDeviceConfig};
use automation_lib::device::{Device, LuaDeviceCreate};
use automation_lib::event::OnMqtt;
use automation_lib::messages::{AirFilterFanState, AirFilterState, SetAirFilterFanState};
use automation_lib::mqtt::WrappedAsyncClient;
use automation_macro::LuaDeviceConfig; use automation_macro::LuaDeviceConfig;
use google_home::device::Name; use google_home::device::Name;
use google_home::errors::ErrorCode; use google_home::errors::ErrorCode;
@ -13,13 +18,6 @@ use rumqttc::Publish;
use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard}; use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
use tracing::{debug, error, trace, warn}; use tracing::{debug, error, trace, warn};
use super::LuaDeviceCreate;
use crate::config::{InfoConfig, MqttDeviceConfig};
use crate::devices::Device;
use crate::event::OnMqtt;
use crate::messages::{AirFilterFanState, AirFilterState, SetAirFilterFanState};
use crate::mqtt::WrappedAsyncClient;
#[derive(Debug, Clone, LuaDeviceConfig)] #[derive(Debug, Clone, LuaDeviceConfig)]
pub struct Config { pub struct Config {
#[device_config(flatten)] #[device_config(flatten)]

View File

@ -2,20 +2,19 @@ use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use async_trait::async_trait; use async_trait::async_trait;
use automation_lib::action_callback::ActionCallback;
use automation_lib::config::MqttDeviceConfig;
use automation_lib::device::{Device, LuaDeviceCreate};
use automation_lib::error::DeviceConfigError;
use automation_lib::event::{OnMqtt, OnPresence};
use automation_lib::messages::{ContactMessage, PresenceMessage};
use automation_lib::mqtt::WrappedAsyncClient;
use automation_lib::presence::DEFAULT_PRESENCE;
use automation_macro::LuaDeviceConfig; use automation_macro::LuaDeviceConfig;
use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard}; use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
use tracing::{debug, error, trace, warn}; use tracing::{debug, error, trace, warn};
use super::{Device, LuaDeviceCreate};
use crate::action_callback::ActionCallback;
use crate::config::MqttDeviceConfig;
use crate::devices::DEFAULT_PRESENCE;
use crate::error::DeviceConfigError;
use crate::event::{OnMqtt, OnPresence};
use crate::messages::{ContactMessage, PresenceMessage};
use crate::mqtt::WrappedAsyncClient;
// NOTE: If we add more presence devices we might need to move this out of here // NOTE: If we add more presence devices we might need to move this out of here
#[derive(Debug, Clone, LuaDeviceConfig)] #[derive(Debug, Clone, LuaDeviceConfig)]
pub struct PresenceDeviceConfig { pub struct PresenceDeviceConfig {

View File

@ -1,16 +1,14 @@
use std::convert::Infallible; use std::convert::Infallible;
use async_trait::async_trait; use async_trait::async_trait;
use automation_lib::config::MqttDeviceConfig;
use automation_lib::device::{Device, LuaDeviceCreate};
use automation_lib::event::{OnDarkness, OnPresence};
use automation_lib::messages::{DarknessMessage, PresenceMessage};
use automation_lib::mqtt::WrappedAsyncClient;
use automation_macro::LuaDeviceConfig; use automation_macro::LuaDeviceConfig;
use tracing::{trace, warn}; use tracing::{trace, warn};
use super::LuaDeviceCreate;
use crate::config::MqttDeviceConfig;
use crate::devices::Device;
use crate::event::{OnDarkness, OnPresence};
use crate::messages::{DarknessMessage, PresenceMessage};
use crate::mqtt::WrappedAsyncClient;
#[derive(Debug, LuaDeviceConfig, Clone)] #[derive(Debug, LuaDeviceConfig, Clone)]
pub struct Config { pub struct Config {
pub identifier: String, pub identifier: String,

View File

@ -2,14 +2,12 @@ use std::convert::Infallible;
use std::net::SocketAddr; use std::net::SocketAddr;
use async_trait::async_trait; use async_trait::async_trait;
use automation_lib::device::{Device, LuaDeviceCreate};
use automation_lib::event::{OnDarkness, OnPresence};
use automation_macro::LuaDeviceConfig; use automation_macro::LuaDeviceConfig;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tracing::{error, trace, warn}; use tracing::{error, trace, warn};
use super::LuaDeviceCreate;
use crate::devices::Device;
use crate::event::{OnDarkness, OnPresence};
#[derive(Debug)] #[derive(Debug)]
pub enum Flag { pub enum Flag {
Presence, Presence,

View File

@ -2,13 +2,13 @@ use std::net::SocketAddr;
use anyhow::Result; use anyhow::Result;
use async_trait::async_trait; use async_trait::async_trait;
use automation_lib::mqtt::WrappedAsyncClient;
use automation_macro::LuaDeviceConfig; use automation_macro::LuaDeviceConfig;
use google_home::errors::ErrorCode; use google_home::errors::ErrorCode;
use google_home::traits::OnOff; use google_home::traits::OnOff;
use tracing::{error, trace, warn}; use tracing::{error, trace, warn};
use super::{Device, LuaDeviceCreate}; use super::{Device, LuaDeviceCreate};
use crate::mqtt::WrappedAsyncClient;
#[derive(Debug, Clone, LuaDeviceConfig)] #[derive(Debug, Clone, LuaDeviceConfig)]
pub struct Config { pub struct Config {
@ -120,9 +120,6 @@ impl OnOff for HueGroup {
} }
mod message { mod message {
use std::time::Duration;
use serde::ser::SerializeStruct;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
@ -164,46 +161,5 @@ mod message {
pub fn any_on(&self) -> bool { pub fn any_on(&self) -> bool {
self.state.any_on self.state.any_on
} }
// pub fn all_on(&self) -> bool {
// self.state.all_on
// }
}
#[derive(Debug)]
pub struct Timeout {
timeout: Option<Duration>,
}
impl Timeout {
pub fn new(timeout: Option<Duration>) -> Self {
Self { timeout }
}
}
impl Serialize for Timeout {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let len = if self.timeout.is_some() { 2 } else { 1 };
let mut state = serializer.serialize_struct("TimerMessage", len)?;
if self.timeout.is_some() {
state.serialize_field("status", "enabled")?;
} else {
state.serialize_field("status", "disabled")?;
}
if let Some(timeout) = self.timeout {
let seconds = timeout.as_secs() % 60;
let minutes = (timeout.as_secs() / 60) % 60;
let hours = timeout.as_secs() / 3600;
let time = format!("PT{hours:<02}:{minutes:<02}:{seconds:<02}");
state.serialize_field("localtime", &time)?;
};
state.end()
}
} }
} }

View File

@ -1,15 +1,13 @@
use async_trait::async_trait;
use automation_lib::action_callback::ActionCallback;
use automation_lib::config::{InfoConfig, MqttDeviceConfig};
use automation_lib::device::{Device, LuaDeviceCreate};
use automation_lib::event::OnMqtt;
use automation_lib::mqtt::WrappedAsyncClient;
use automation_macro::LuaDeviceConfig; use automation_macro::LuaDeviceConfig;
use axum::async_trait;
use rumqttc::{matches, Publish}; use rumqttc::{matches, Publish};
use tracing::{debug, trace, warn}; use tracing::{debug, trace, warn};
use zigbee2mqtt_types::vendors::philips::Zigbee929003017102; use zigbee2mqtt_types::philips::{Zigbee929003017102, Zigbee929003017102Action};
use super::LuaDeviceCreate;
use crate::action_callback::ActionCallback;
use crate::config::{InfoConfig, MqttDeviceConfig};
use crate::devices::Device;
use crate::event::OnMqtt;
use crate::mqtt::WrappedAsyncClient;
#[derive(Debug, Clone, LuaDeviceConfig)] #[derive(Debug, Clone, LuaDeviceConfig)]
pub struct Config { pub struct Config {
@ -72,12 +70,8 @@ impl OnMqtt for HueSwitch {
debug!(id = Device::get_id(self), "Remote action = {:?}", action); debug!(id = Device::get_id(self), "Remote action = {:?}", action);
match action { match action {
zigbee2mqtt_types::vendors::philips::Zigbee929003017102Action::Leftpress => { Zigbee929003017102Action::LeftPress => self.config.left_callback.call(()).await,
self.config.left_callback.call(()).await Zigbee929003017102Action::RightPress => self.config.right_callback.call(()).await,
}
zigbee2mqtt_types::vendors::philips::Zigbee929003017102Action::Rightpress => {
self.config.right_callback.call(()).await
}
_ => {} _ => {}
} }
} }

View File

@ -2,6 +2,12 @@ use std::sync::Arc;
use anyhow::Result; use anyhow::Result;
use async_trait::async_trait; use async_trait::async_trait;
use automation_lib::action_callback::ActionCallback;
use automation_lib::config::{InfoConfig, MqttDeviceConfig};
use automation_lib::device::{Device, LuaDeviceCreate};
use automation_lib::event::{OnMqtt, OnPresence};
use automation_lib::messages::OnOffMessage;
use automation_lib::mqtt::WrappedAsyncClient;
use automation_macro::LuaDeviceConfig; use automation_macro::LuaDeviceConfig;
use google_home::device; use google_home::device;
use google_home::errors::ErrorCode; use google_home::errors::ErrorCode;
@ -12,14 +18,6 @@ use serde::Deserialize;
use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard}; use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
use tracing::{debug, error, trace, warn}; use tracing::{debug, error, trace, warn};
use super::LuaDeviceCreate;
use crate::action_callback::ActionCallback;
use crate::config::{InfoConfig, MqttDeviceConfig};
use crate::devices::Device;
use crate::event::{OnMqtt, OnPresence};
use crate::messages::OnOffMessage;
use crate::mqtt::WrappedAsyncClient;
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Copy)] #[derive(Debug, Clone, Deserialize, PartialEq, Eq, Copy)]
pub enum OutletType { pub enum OutletType {
Outlet, Outlet,

View File

@ -1,16 +1,14 @@
use automation_lib::action_callback::ActionCallback;
use automation_lib::config::{InfoConfig, MqttDeviceConfig};
use automation_lib::device::{Device, LuaDeviceCreate};
use automation_lib::event::OnMqtt;
use automation_lib::messages::{RemoteAction, RemoteMessage};
use automation_lib::mqtt::WrappedAsyncClient;
use automation_macro::LuaDeviceConfig; use automation_macro::LuaDeviceConfig;
use axum::async_trait; use axum::async_trait;
use rumqttc::{matches, Publish}; use rumqttc::{matches, Publish};
use tracing::{debug, error, trace}; use tracing::{debug, error, trace};
use super::LuaDeviceCreate;
use crate::action_callback::ActionCallback;
use crate::config::{InfoConfig, MqttDeviceConfig};
use crate::devices::Device;
use crate::event::OnMqtt;
use crate::messages::RemoteMessage;
use crate::mqtt::WrappedAsyncClient;
#[derive(Debug, Clone, LuaDeviceConfig)] #[derive(Debug, Clone, LuaDeviceConfig)]
pub struct Config { pub struct Config {
#[device_config(flatten)] #[device_config(flatten)]
@ -73,14 +71,14 @@ impl OnMqtt for IkeaRemote {
let on = if self.config.single_button { let on = if self.config.single_button {
match action { match action {
crate::messages::RemoteAction::On => Some(true), RemoteAction::On => Some(true),
crate::messages::RemoteAction::BrightnessMoveUp => Some(false), RemoteAction::BrightnessMoveUp => Some(false),
_ => None, _ => None,
} }
} else { } else {
match action { match action {
crate::messages::RemoteAction::On => Some(true), RemoteAction::On => Some(true),
crate::messages::RemoteAction::Off => Some(false), RemoteAction::Off => Some(false),
_ => None, _ => None,
} }
}; };

View File

@ -3,6 +3,8 @@ use std::net::SocketAddr;
use std::str::Utf8Error; use std::str::Utf8Error;
use async_trait::async_trait; use async_trait::async_trait;
use automation_lib::device::{Device, LuaDeviceCreate};
use automation_lib::event::OnPresence;
use automation_macro::LuaDeviceConfig; use automation_macro::LuaDeviceConfig;
use bytes::{Buf, BufMut}; use bytes::{Buf, BufMut};
use google_home::errors::{self, DeviceError}; use google_home::errors::{self, DeviceError};
@ -13,9 +15,6 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream; use tokio::net::TcpStream;
use tracing::{debug, trace}; use tracing::{debug, trace};
use super::{Device, LuaDeviceCreate};
use crate::event::OnPresence;
#[derive(Debug, Clone, LuaDeviceConfig)] #[derive(Debug, Clone, LuaDeviceConfig)]
pub struct Config { pub struct Config {
pub identifier: String, pub identifier: String,

View File

@ -8,19 +8,13 @@ mod ikea_outlet;
mod ikea_remote; mod ikea_remote;
mod kasa_outlet; mod kasa_outlet;
mod light_sensor; mod light_sensor;
mod ntfy;
mod presence;
mod wake_on_lan; mod wake_on_lan;
mod washer; mod washer;
use std::fmt::Debug;
use std::ops::Deref; use std::ops::Deref;
use async_trait::async_trait;
use automation_cast::Cast; use automation_cast::Cast;
use dyn_clone::DynClone; use automation_lib::device::{Device, LuaDeviceCreate};
use google_home::traits::OnOff;
use mlua::ObjectLike;
pub use self::air_filter::AirFilter; pub use self::air_filter::AirFilter;
pub use self::contact_sensor::ContactSensor; pub use self::contact_sensor::ContactSensor;
@ -32,21 +26,8 @@ pub use self::ikea_outlet::IkeaOutlet;
pub use self::ikea_remote::IkeaRemote; pub use self::ikea_remote::IkeaRemote;
pub use self::kasa_outlet::KasaOutlet; pub use self::kasa_outlet::KasaOutlet;
pub use self::light_sensor::LightSensor; pub use self::light_sensor::LightSensor;
pub use self::ntfy::{Notification, Ntfy};
pub use self::presence::{Presence, DEFAULT_PRESENCE};
pub use self::wake_on_lan::WakeOnLAN; pub use self::wake_on_lan::WakeOnLAN;
pub use self::washer::Washer; pub use self::washer::Washer;
use crate::event::{OnDarkness, OnMqtt, OnNotification, OnPresence};
#[async_trait]
pub trait LuaDeviceCreate {
type Config;
type Error;
async fn create(config: Self::Config) -> Result<Self, Self::Error>
where
Self: Sized;
}
macro_rules! register_device { macro_rules! register_device {
($lua:expr, $device:ty) => { ($lua:expr, $device:ty) => {
@ -60,7 +41,7 @@ macro_rules! impl_device {
impl mlua::UserData for $device { impl mlua::UserData for $device {
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) { fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
methods.add_async_function("new", |_lua, config| async { methods.add_async_function("new", |_lua, config| async {
let device: $device = crate::devices::LuaDeviceCreate::create(config) let device: $device = LuaDeviceCreate::create(config)
.await .await
.map_err(mlua::ExternalError::into_lua_err)?; .map_err(mlua::ExternalError::into_lua_err)?;
@ -74,9 +55,9 @@ macro_rules! impl_device {
methods.add_async_method("get_id", |_lua, this, _: ()| async move { Ok(this.get_id()) }); methods.add_async_method("get_id", |_lua, this, _: ()| async move { Ok(this.get_id()) });
if impls::impls!($device: OnOff) { if impls::impls!($device: google_home::traits::OnOff) {
methods.add_async_method("set_on", |_lua, this, on: bool| async move { methods.add_async_method("set_on", |_lua, this, on: bool| async move {
(this.deref().cast() as Option<&dyn OnOff>) (this.deref().cast() as Option<&dyn google_home::traits::OnOff>)
.expect("Cast should be valid") .expect("Cast should be valid")
.set_on(on) .set_on(on)
.await .await
@ -86,7 +67,7 @@ macro_rules! impl_device {
}); });
methods.add_async_method("is_on", |_lua, this, _: ()| async move { methods.add_async_method("is_on", |_lua, this, _: ()| async move {
Ok((this.deref().cast() as Option<&dyn OnOff>) Ok((this.deref().cast() as Option<&dyn google_home::traits::OnOff>)
.expect("Cast should be valid") .expect("Cast should be valid")
.on() .on()
.await .await
@ -108,8 +89,6 @@ impl_device!(IkeaOutlet);
impl_device!(IkeaRemote); impl_device!(IkeaRemote);
impl_device!(KasaOutlet); impl_device!(KasaOutlet);
impl_device!(LightSensor); impl_device!(LightSensor);
impl_device!(Ntfy);
impl_device!(Presence);
impl_device!(WakeOnLAN); impl_device!(WakeOnLAN);
impl_device!(Washer); impl_device!(Washer);
@ -124,47 +103,8 @@ pub fn register_with_lua(lua: &mlua::Lua) -> mlua::Result<()> {
register_device!(lua, IkeaRemote); register_device!(lua, IkeaRemote);
register_device!(lua, KasaOutlet); register_device!(lua, KasaOutlet);
register_device!(lua, LightSensor); register_device!(lua, LightSensor);
register_device!(lua, Ntfy);
register_device!(lua, Presence);
register_device!(lua, WakeOnLAN); register_device!(lua, WakeOnLAN);
register_device!(lua, Washer); register_device!(lua, Washer);
Ok(()) Ok(())
} }
pub trait Device:
Debug
+ DynClone
+ Sync
+ Send
+ Cast<dyn google_home::Device>
+ Cast<dyn OnMqtt>
+ Cast<dyn OnMqtt>
+ Cast<dyn OnPresence>
+ Cast<dyn OnDarkness>
+ Cast<dyn OnNotification>
+ Cast<dyn OnOff>
{
fn get_id(&self) -> String;
}
impl mlua::FromLua for Box<dyn Device> {
fn from_lua(value: mlua::Value, _lua: &mlua::Lua) -> mlua::Result<Self> {
match value {
mlua::Value::UserData(ud) => {
let ud = if ud.is::<Box<dyn Device>>() {
ud
} else {
ud.call_method::<_>("__box", ())?
};
let b = ud.borrow::<Self>()?.clone();
Ok(b)
}
_ => Err(mlua::Error::RuntimeError("Expected user data".into())),
}
}
}
impl mlua::UserData for Box<dyn Device> {}
dyn_clone::clone_trait_object!(Device);

View File

@ -1,18 +1,16 @@
use std::sync::Arc; use std::sync::Arc;
use async_trait::async_trait; use async_trait::async_trait;
use automation_lib::config::MqttDeviceConfig;
use automation_lib::device::{Device, LuaDeviceCreate};
use automation_lib::event::{self, Event, EventChannel, OnMqtt};
use automation_lib::messages::BrightnessMessage;
use automation_lib::mqtt::WrappedAsyncClient;
use automation_macro::LuaDeviceConfig; use automation_macro::LuaDeviceConfig;
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};
use super::LuaDeviceCreate;
use crate::config::MqttDeviceConfig;
use crate::devices::Device;
use crate::event::{self, Event, EventChannel, OnMqtt};
use crate::messages::BrightnessMessage;
use crate::mqtt::WrappedAsyncClient;
#[derive(Debug, Clone, LuaDeviceConfig)] #[derive(Debug, Clone, LuaDeviceConfig)]
pub struct Config { pub struct Config {
pub identifier: String, pub identifier: String,

View File

@ -1,6 +1,11 @@
use std::net::Ipv4Addr; use std::net::Ipv4Addr;
use async_trait::async_trait; use async_trait::async_trait;
use automation_lib::config::{InfoConfig, MqttDeviceConfig};
use automation_lib::device::{Device, LuaDeviceCreate};
use automation_lib::event::OnMqtt;
use automation_lib::messages::ActivateMessage;
use automation_lib::mqtt::WrappedAsyncClient;
use automation_macro::LuaDeviceConfig; use automation_macro::LuaDeviceConfig;
use eui48::MacAddress; use eui48::MacAddress;
use google_home::device; use google_home::device;
@ -10,12 +15,6 @@ use google_home::types::Type;
use rumqttc::Publish; use rumqttc::Publish;
use tracing::{debug, error, trace}; use tracing::{debug, error, trace};
use super::{Device, LuaDeviceCreate};
use crate::config::{InfoConfig, MqttDeviceConfig};
use crate::event::OnMqtt;
use crate::messages::ActivateMessage;
use crate::mqtt::WrappedAsyncClient;
#[derive(Debug, Clone, LuaDeviceConfig)] #[derive(Debug, Clone, LuaDeviceConfig)]
pub struct Config { pub struct Config {
#[device_config(flatten)] #[device_config(flatten)]

View File

@ -1,18 +1,17 @@
use std::sync::Arc; use std::sync::Arc;
use async_trait::async_trait; use async_trait::async_trait;
use automation_lib::config::MqttDeviceConfig;
use automation_lib::device::{Device, LuaDeviceCreate};
use automation_lib::event::{self, Event, EventChannel, OnMqtt};
use automation_lib::messages::PowerMessage;
use automation_lib::mqtt::WrappedAsyncClient;
use automation_lib::ntfy::{Notification, Priority};
use automation_macro::LuaDeviceConfig; use automation_macro::LuaDeviceConfig;
use rumqttc::Publish; use rumqttc::Publish;
use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard}; use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
use tracing::{debug, error, trace, warn}; use tracing::{debug, error, trace, warn};
use super::ntfy::Priority;
use super::{Device, LuaDeviceCreate, Notification};
use crate::config::MqttDeviceConfig;
use crate::event::{self, Event, EventChannel, OnMqtt};
use crate::messages::PowerMessage;
use crate::mqtt::WrappedAsyncClient;
#[derive(Debug, Clone, LuaDeviceConfig)] #[derive(Debug, Clone, LuaDeviceConfig)]
pub struct Config { pub struct Config {
pub identifier: String, pub identifier: String,

28
automation_lib/Cargo.toml Normal file
View File

@ -0,0 +1,28 @@
[package]
name = "automation_lib"
version = "0.1.0"
edition = "2021"
[dependencies]
automation_macro = { workspace = true }
automation_cast = { workspace = true }
google_home = { workspace = true }
rumqttc = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
tokio = { workspace = true }
reqwest = { workspace = true }
serde_repr = { workspace = true }
tracing = { workspace = true }
bytes = { workspace = true }
pollster = { workspace = true }
async-trait = { workspace = true }
futures = { workspace = true }
thiserror = { workspace = true }
indexmap = { workspace = true }
tokio-cron-scheduler = { workspace = true }
mlua = { workspace = true }
tokio-util = { workspace = true }
uuid = { workspace = true }
dyn-clone = { workspace = true }
impls = { workspace = true }

View File

@ -0,0 +1,99 @@
use std::fmt::Debug;
use automation_cast::Cast;
use dyn_clone::DynClone;
use google_home::traits::OnOff;
use mlua::ObjectLike;
use crate::event::{OnDarkness, OnMqtt, OnNotification, OnPresence};
// TODO: Make this a proper macro
macro_rules! impl_device {
($device:ty) => {
impl mlua::UserData for $device {
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
methods.add_async_function("new", |_lua, config| async {
let device: $device = LuaDeviceCreate::create(config)
.await
.map_err(mlua::ExternalError::into_lua_err)?;
Ok(device)
});
methods.add_method("__box", |_lua, this, _: ()| {
let b: Box<dyn Device> = Box::new(this.clone());
Ok(b)
});
methods.add_async_method("get_id", |_lua, this, _: ()| async move { Ok(this.get_id()) });
if impls::impls!($device: google_home::traits::OnOff) {
methods.add_async_method("set_on", |_lua, this, on: bool| async move {
(this.deref().cast() as Option<&dyn google_home::traits::OnOff>)
.expect("Cast should be valid")
.set_on(on)
.await
.unwrap();
Ok(())
});
methods.add_async_method("is_on", |_lua, this, _: ()| async move {
Ok((this.deref().cast() as Option<&dyn google_home::traits::OnOff>)
.expect("Cast should be valid")
.on()
.await
.unwrap())
});
}
}
}
};
}
pub(crate) use impl_device;
#[async_trait::async_trait]
pub trait LuaDeviceCreate {
type Config;
type Error;
async fn create(config: Self::Config) -> Result<Self, Self::Error>
where
Self: Sized;
}
pub trait Device:
Debug
+ DynClone
+ Sync
+ Send
+ Cast<dyn google_home::Device>
+ Cast<dyn OnMqtt>
+ Cast<dyn OnPresence>
+ Cast<dyn OnDarkness>
+ Cast<dyn OnNotification>
+ Cast<dyn OnOff>
{
fn get_id(&self) -> String;
}
impl mlua::FromLua for Box<dyn Device> {
fn from_lua(value: mlua::Value, _lua: &mlua::Lua) -> mlua::Result<Self> {
match value {
mlua::Value::UserData(ud) => {
let ud = if ud.is::<Box<dyn Device>>() {
ud
} else {
ud.call_method::<_>("__box", ())?
};
let b = ud.borrow::<Self>()?.clone();
Ok(b)
}
_ => Err(mlua::Error::RuntimeError("Expected user data".into())),
}
}
}
impl mlua::UserData for Box<dyn Device> {}
dyn_clone::clone_trait_object!(Device);

View File

@ -8,7 +8,7 @@ use tokio::sync::{RwLock, RwLockReadGuard};
use tokio_cron_scheduler::{Job, JobScheduler}; use tokio_cron_scheduler::{Job, JobScheduler};
use tracing::{debug, instrument, trace}; use tracing::{debug, instrument, trace};
use crate::devices::Device; use crate::device::Device;
use crate::event::{Event, EventChannel, OnDarkness, OnMqtt, OnNotification, OnPresence}; use crate::event::{Event, EventChannel, OnDarkness, OnMqtt, OnNotification, OnPresence};
pub type DeviceMap = HashMap<String, Box<dyn Device>>; pub type DeviceMap = HashMap<String, Box<dyn Device>>;

View File

@ -1,10 +1,7 @@
use std::{error, fmt, result}; use std::{error, fmt, result};
use axum::http::status::InvalidStatusCode;
use axum::response::IntoResponse;
use bytes::Bytes; use bytes::Bytes;
use rumqttc::ClientError; use rumqttc::ClientError;
use serde::{Deserialize, Serialize};
use thiserror::Error; use thiserror::Error;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -101,68 +98,3 @@ pub enum LightSensorError {
#[error(transparent)] #[error(transparent)]
SubscribeError(#[from] ClientError), SubscribeError(#[from] ClientError),
} }
#[derive(Debug, Error)]
#[error("{source}")]
pub struct ApiError {
status_code: axum::http::StatusCode,
source: Box<dyn std::error::Error>,
}
impl ApiError {
pub fn new(status_code: axum::http::StatusCode, source: Box<dyn std::error::Error>) -> Self {
Self {
status_code,
source,
}
}
}
impl From<ApiError> for ApiErrorJson {
fn from(value: ApiError) -> Self {
let error = ApiErrorJsonError {
code: value.status_code.as_u16(),
status: value.status_code.to_string(),
reason: value.source.to_string(),
};
Self { error }
}
}
impl IntoResponse for ApiError {
fn into_response(self) -> axum::response::Response {
(
self.status_code,
serde_json::to_string::<ApiErrorJson>(&self.into())
.expect("Serialization should not fail"),
)
.into_response()
}
}
#[derive(Debug, Serialize, Deserialize)]
struct ApiErrorJsonError {
code: u16,
status: String,
reason: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ApiErrorJson {
error: ApiErrorJsonError,
}
impl TryFrom<ApiErrorJson> for ApiError {
type Error = InvalidStatusCode;
fn try_from(value: ApiErrorJson) -> result::Result<Self, Self::Error> {
let status_code = axum::http::StatusCode::from_u16(value.error.code)?;
let source = value.error.reason.into();
Ok(Self {
status_code,
source,
})
}
}

View File

@ -3,7 +3,7 @@ use mlua::FromLua;
use rumqttc::Publish; use rumqttc::Publish;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use crate::devices::Notification; use crate::ntfy::Notification;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum Event { pub enum Event {

View File

@ -3,13 +3,14 @@
#![feature(let_chains)] #![feature(let_chains)]
pub mod action_callback; pub mod action_callback;
pub mod auth;
pub mod config; pub mod config;
pub mod device;
pub mod device_manager; pub mod device_manager;
pub mod devices;
pub mod error; pub mod error;
pub mod event; pub mod event;
pub mod helpers; pub mod helpers;
pub mod messages; pub mod messages;
pub mod mqtt; pub mod mqtt;
pub mod ntfy;
pub mod presence;
pub mod schedule; pub mod schedule;

View File

@ -1,14 +1,15 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::convert::Infallible; use std::convert::Infallible;
use std::ops::Deref;
use async_trait::async_trait; use async_trait::async_trait;
use automation_cast::Cast;
use automation_macro::LuaDeviceConfig; use automation_macro::LuaDeviceConfig;
use serde::Serialize; use serde::Serialize;
use serde_repr::*; use serde_repr::*;
use tracing::{error, trace, warn}; use tracing::{error, trace, warn};
use super::LuaDeviceCreate; use crate::device::{impl_device, Device, LuaDeviceCreate};
use crate::devices::Device;
use crate::event::{self, Event, EventChannel, OnNotification, OnPresence}; use crate::event::{self, Event, EventChannel, OnNotification, OnPresence};
#[derive(Debug, Serialize_repr, Clone, Copy)] #[derive(Debug, Serialize_repr, Clone, Copy)]
@ -125,6 +126,8 @@ pub struct Ntfy {
config: Config, config: Config,
} }
impl_device!(Ntfy);
#[async_trait] #[async_trait]
impl LuaDeviceCreate for Ntfy { impl LuaDeviceCreate for Ntfy {
type Config = Config; type Config = Config;

View File

@ -1,15 +1,16 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::ops::Deref;
use std::sync::Arc; use std::sync::Arc;
use async_trait::async_trait; use async_trait::async_trait;
use automation_cast::Cast;
use automation_macro::LuaDeviceConfig; use automation_macro::LuaDeviceConfig;
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};
use super::LuaDeviceCreate;
use crate::config::MqttDeviceConfig; use crate::config::MqttDeviceConfig;
use crate::devices::Device; use crate::device::{impl_device, Device, LuaDeviceCreate};
use crate::event::{self, Event, EventChannel, OnMqtt}; use crate::event::{self, Event, EventChannel, OnMqtt};
use crate::messages::PresenceMessage; use crate::messages::PresenceMessage;
use crate::mqtt::WrappedAsyncClient; use crate::mqtt::WrappedAsyncClient;
@ -48,6 +49,8 @@ impl Presence {
} }
} }
impl_device!(Presence);
#[async_trait] #[async_trait]
impl LuaDeviceCreate for Presence { impl LuaDeviceCreate for Presence {
type Config = Config; type Config = Config;

View File

@ -7,14 +7,7 @@ edition = "2021"
proc-macro = true proc-macro = true
[dependencies] [dependencies]
automation_cast = { path = "../automation_cast" } itertools = { workspace = true }
async-trait = "0.1.80" proc-macro2 = { workspace = true }
itertools = "0.12.1" quote = { workspace = true }
proc-macro2 = "1.0.81" syn = { workspace = true }
quote = "1.0.36"
serde = { version = "1.0.202", features = ["derive"] }
syn = { version = "2.0.60", features = ["extra-traits", "full"] }
serde_json = "1.0.118"
[dev-dependencies]
serde = { version = "1.0.202", features = ["derive"] }

View File

@ -6,13 +6,12 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
automation_cast = { path = "../../automation_cast/" } automation_cast = { workspace = true }
google_home_macro = { path = "../google_home_macro/" } google_home_macro = { workspace = true }
serde = { version = "1.0.149", features = ["derive"] } serde = { workspace = true }
serde_json = "1.0.89" serde_json = { workspace = true }
thiserror = "1.0.37" thiserror = { workspace = true }
tokio = { version = "1", features = ["sync", "full"] } tokio = { workspace = true }
async-trait = "0.1.61" async-trait = { workspace = true }
futures = "0.3.25" futures = { workspace = true }
anyhow = "1.0.75" json_value_merge = { workspace = true }
json_value_merge = "2.0.0"

View File

@ -7,6 +7,6 @@ edition = "2021"
proc-macro = true proc-macro = true
[dependencies] [dependencies]
proc-macro2 = "1.0.81" proc-macro2 = { workspace = true }
quote = "1.0.36" quote = { workspace = true }
syn = { version = "2.0.60", features = ["extra-traits", "full"] } syn = { workspace = true }

View File

@ -1,4 +1,4 @@
[toolchain] [toolchain]
channel = "nightly-2024-07-25" channel = "nightly-2024-12-06"
components = ["rustfmt", "clippy", "rust-analyzer"] components = ["rustfmt", "clippy", "rust-analyzer"]
profile = "minimal" profile = "minimal"

View File

@ -1,13 +1,16 @@
mod web;
use std::net::SocketAddr;
use std::path::Path; use std::path::Path;
use std::process; use std::process;
use anyhow::anyhow; use anyhow::anyhow;
use automation::auth::User; use automation_lib::config::{FulfillmentConfig, MqttConfig};
use automation::config::{FulfillmentConfig, MqttConfig}; use automation_lib::device_manager::DeviceManager;
use automation::device_manager::DeviceManager; use automation_lib::helpers;
use automation::error::ApiError; use automation_lib::mqtt::{self, WrappedAsyncClient};
use automation::mqtt::{self, WrappedAsyncClient}; use automation_lib::ntfy::Ntfy;
use automation::{devices, helpers}; use automation_lib::presence::Presence;
use axum::extract::{FromRef, State}; use axum::extract::{FromRef, State};
use axum::http::StatusCode; use axum::http::StatusCode;
use axum::routing::post; use axum::routing::post;
@ -16,7 +19,9 @@ use dotenvy::dotenv;
use google_home::{GoogleHome, Request, Response}; use google_home::{GoogleHome, Request, Response};
use mlua::LuaSerdeExt; use mlua::LuaSerdeExt;
use rumqttc::AsyncClient; use rumqttc::AsyncClient;
use tokio::net::TcpListener;
use tracing::{debug, error, info, warn}; use tracing::{debug, error, info, warn};
use web::{ApiError, User};
#[derive(Clone)] #[derive(Clone)]
struct AppState { struct AppState {
@ -111,8 +116,11 @@ async fn app() -> anyhow::Result<()> {
lua.globals().set("automation", automation)?; lua.globals().set("automation", automation)?;
devices::register_with_lua(&lua)?; automation_devices::register_with_lua(&lua)?;
helpers::register_with_lua(&lua)?; helpers::register_with_lua(&lua)?;
lua.globals().set("Ntfy", lua.create_proxy::<Ntfy>()?)?;
lua.globals()
.set("Presence", lua.create_proxy::<Presence>()?)?;
// TODO: Make this not hardcoded // TODO: Make this not hardcoded
let config_filename = std::env::var("AUTOMATION_CONFIG").unwrap_or("./config.lua".into()); let config_filename = std::env::var("AUTOMATION_CONFIG").unwrap_or("./config.lua".into());
@ -148,11 +156,10 @@ async fn app() -> anyhow::Result<()> {
}); });
// Start the web server // Start the web server
let addr = fulfillment_config.into(); let addr: SocketAddr = fulfillment_config.into();
info!("Server started on http://{addr}"); info!("Server started on http://{addr}");
axum::Server::try_bind(&addr)? let listener = TcpListener::bind(addr).await?;
.serve(app.into_make_service()) axum::serve(listener, app).await?;
.await?;
Ok(()) Ok(())
} }

View File

@ -1,10 +1,78 @@
use std::result;
use axum::async_trait; use axum::async_trait;
use axum::extract::{FromRef, FromRequestParts}; use axum::extract::{FromRef, FromRequestParts};
use axum::http::request::Parts; use axum::http::request::Parts;
use axum::http::status::InvalidStatusCode;
use axum::http::StatusCode; use axum::http::StatusCode;
use serde::Deserialize; use axum::response::IntoResponse;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::error::{ApiError, ApiErrorJson}; #[derive(Debug, Error)]
#[error("{source}")]
pub struct ApiError {
status_code: axum::http::StatusCode,
source: Box<dyn std::error::Error>,
}
impl ApiError {
pub fn new(status_code: axum::http::StatusCode, source: Box<dyn std::error::Error>) -> Self {
Self {
status_code,
source,
}
}
}
impl From<ApiError> for ApiErrorJson {
fn from(value: ApiError) -> Self {
let error = ApiErrorJsonError {
code: value.status_code.as_u16(),
status: value.status_code.to_string(),
reason: value.source.to_string(),
};
Self { error }
}
}
impl IntoResponse for ApiError {
fn into_response(self) -> axum::response::Response {
(
self.status_code,
serde_json::to_string::<ApiErrorJson>(&self.into())
.expect("Serialization should not fail"),
)
.into_response()
}
}
#[derive(Debug, Serialize, Deserialize)]
struct ApiErrorJsonError {
code: u16,
status: String,
reason: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ApiErrorJson {
error: ApiErrorJsonError,
}
impl TryFrom<ApiErrorJson> for ApiError {
type Error = InvalidStatusCode;
fn try_from(value: ApiErrorJson) -> result::Result<Self, Self::Error> {
let status_code = axum::http::StatusCode::from_u16(value.error.code)?;
let source = value.error.reason.into();
Ok(Self {
status_code,
source,
})
}
}
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct User { pub struct User {
@ -25,6 +93,8 @@ where
// Create a request to the auth server // Create a request to the auth server
// TODO: Do some discovery to find the correct url for this instead of assuming // TODO: Do some discovery to find the correct url for this instead of assuming
// TODO: I think we can also just run Authlia in front of the endpoint instead
// This would then give us a header containing the logged in user info?
let mut req = reqwest::Client::new().get(format!("{}/userinfo", openid_url)); let mut req = reqwest::Client::new().get(format!("{}/userinfo", openid_url));
// Add auth header to the request if it exists // Add auth header to the request if it exists