Compare commits

..

21 Commits

Author SHA1 Message Date
09853e00f4
Make sure the generated lua definitions are up to date
All checks were successful
Build and deploy automation_rs / Build automation_rs (push) Successful in 4m53s
Build and deploy automation_rs / Build Docker image (push) Successful in 44s
Build and deploy automation_rs / Deploy Docker container (push) Has been skipped
2024-04-30 02:25:30 +02:00
9ab16b3dcd
Further work on automatically generating lua type definitions
All checks were successful
Build and deploy automation_rs / Build automation_rs (push) Successful in 5m21s
Build and deploy automation_rs / Build Docker image (push) Successful in 28s
Build and deploy automation_rs / Deploy Docker container (push) Has been skipped
2024-04-30 02:21:34 +02:00
cf6253f14b
Started work on generating definitions 2024-04-30 02:21:34 +02:00
af2a8bc35c
Fixed typo in README.md and added mosquitto as word
All checks were successful
Build and deploy automation_rs / Build automation_rs (push) Successful in 5m50s
Build and deploy automation_rs / Build Docker image (push) Successful in 52s
Build and deploy automation_rs / Deploy Docker container (push) Has been skipped
2024-04-30 02:20:51 +02:00
67ed13463a
Started work on reimplementing schedules
All checks were successful
Build and deploy automation_rs / Build Docker image (push) Successful in 40s
Build and deploy automation_rs / Deploy Docker container (push) Has been skipped
Build and deploy automation_rs / Build automation_rs (push) Successful in 5m22s
2024-04-29 04:55:39 +02:00
b16f2ae420
Fixed spelling mistakes 2024-04-29 04:55:39 +02:00
96f260492b
Moved last config items to lua + small cleanup 2024-04-29 04:55:30 +02:00
0b31b2e443
Fixed visibility of device configs
All checks were successful
Build and deploy automation_rs / Build automation_rs (push) Successful in 5m14s
Build and deploy automation_rs / Build Docker image (push) Successful in 51s
Build and deploy automation_rs / Deploy Docker container (push) Has been skipped
2024-04-29 03:03:42 +02:00
2b62aca78a
LuaDevice macro now uses LuaDeviceCreate trait to create devices from configs
All checks were successful
Build and deploy automation_rs / Build automation_rs (push) Successful in 4m53s
Build and deploy automation_rs / Build Docker image (push) Successful in 59s
Build and deploy automation_rs / Deploy Docker container (push) Has been skipped
2024-04-29 02:53:21 +02:00
40426862e5
mqtt client is now created in lua 2024-04-29 02:19:52 +02:00
c3bd05434c
DeviceManager no longer handles subscribing and filtering topics, each device has to do this themselves now 2024-04-29 02:12:47 +02:00
9385f27125
Improved how devices are created, ntfy and presence are now treated like any other device
All checks were successful
Build and deploy automation_rs / Build automation_rs (push) Successful in 5m30s
Build and deploy automation_rs / Build Docker image (push) Successful in 55s
Build and deploy automation_rs / Deploy Docker container (push) Has been skipped
2024-04-27 02:55:53 +02:00
8c327095fd
Moved schedule config from yml to lua 2024-04-26 23:16:39 +02:00
57596ae531
Set lua warning function 2024-04-26 21:54:55 +02:00
8762a680a8
Slight macro cleanup
All checks were successful
Build and deploy automation_rs / Build automation_rs (push) Successful in 4m26s
Build and deploy automation_rs / Build Docker image (push) Successful in 34s
Build and deploy automation_rs / Deploy Docker container (push) Has been skipped
2024-04-26 06:03:54 +02:00
e7fb8bfb8d
Improved the internals of the LuaDeviceConfig macro and improve the
usability of the macro
2024-04-26 06:03:54 +02:00
dc3a7e5407
Use helper types to process config input into the right type 2024-04-26 06:03:54 +02:00
20606c6356
Added helper type to convert from ip addr to socketaddr with the correct port 2024-04-26 06:03:54 +02:00
d8198bf5b0
Added rename option to macro 2024-04-26 06:03:54 +02:00
9449a83f61
Everything needed to construct a new device is passed in through lua 2024-04-26 06:03:54 +02:00
2bc2dc6be1
Device config is now done through lua 2024-04-26 06:03:54 +02:00
18 changed files with 313 additions and 206 deletions

49
Cargo.lock generated
View File

@ -76,7 +76,6 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
"automation_cast",
"automation_macro", "automation_macro",
"axum", "axum",
"bytes", "bytes",
@ -86,7 +85,7 @@ dependencies = [
"eui48", "eui48",
"futures", "futures",
"google-home", "google-home",
"hostname", "impl_cast",
"indexmap 2.0.0", "indexmap 2.0.0",
"mlua", "mlua",
"once_cell", "once_cell",
@ -108,10 +107,6 @@ dependencies = [
"wakey", "wakey",
] ]
[[package]]
name = "automation_cast"
version = "0.1.0"
[[package]] [[package]]
name = "automation_macro" name = "automation_macro"
version = "0.1.0" version = "0.1.0"
@ -621,8 +616,8 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
"automation_cast",
"futures", "futures",
"impl_cast",
"serde", "serde",
"serde_json", "serde_json",
"thiserror", "thiserror",
@ -694,17 +689,6 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "hostname"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9c7c7c8ac16c798734b8a24560c1362120597c40d5e1459f09498f8f6c8f2ba"
dependencies = [
"cfg-if",
"libc",
"windows 0.52.0",
]
[[package]] [[package]]
name = "http" name = "http"
version = "0.2.9" version = "0.2.9"
@ -806,7 +790,7 @@ dependencies = [
"iana-time-zone-haiku", "iana-time-zone-haiku",
"js-sys", "js-sys",
"wasm-bindgen", "wasm-bindgen",
"windows 0.48.0", "windows",
] ]
[[package]] [[package]]
@ -834,6 +818,14 @@ dependencies = [
"unicode-normalization", "unicode-normalization",
] ]
[[package]]
name = "impl_cast"
version = "0.1.0"
dependencies = [
"quote",
"syn 2.0.60",
]
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "1.9.3" version = "1.9.3"
@ -2312,25 +2304,6 @@ dependencies = [
"windows-targets 0.48.1", "windows-targets 0.48.1",
] ]
[[package]]
name = "windows"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be"
dependencies = [
"windows-core",
"windows-targets 0.52.5",
]
[[package]]
name = "windows-core"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
dependencies = [
"windows-targets 0.52.5",
]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.48.0" version = "0.48.0"

View File

@ -4,15 +4,14 @@ version = "0.1.0"
edition = "2021" edition = "2021"
[workspace] [workspace]
members = ["google-home", "automation_macro", "automation_cast"] members = ["impl_cast", "google-home", "automation_macro"]
[dependencies] [dependencies]
automation_macro = { path = "./automation_macro" } automation_macro = { path = "./automation_macro" }
automation_cast = { path = "./automation_cast/" }
rumqttc = "0.18" rumqttc = "0.18"
serde = { version = "1.0.149", features = ["derive"] } serde = { version = "1.0.149", features = ["derive"] }
serde_json = "1.0.89" serde_json = "1.0.89"
impl_cast = { path = "./impl_cast", features = ["debug"] }
google-home = { path = "./google-home" } google-home = { path = "./google-home" }
paste = "1.0.10" paste = "1.0.10"
tokio = { version = "1", features = ["rt-multi-thread"] } tokio = { version = "1", features = ["rt-multi-thread"] }
@ -43,16 +42,8 @@ enum_dispatch = "0.3.12"
indexmap = { version = "2.0.0", features = ["serde"] } indexmap = { version = "2.0.0", features = ["serde"] }
serde_yaml = "0.9.27" serde_yaml = "0.9.27"
tokio-cron-scheduler = "0.9.4" tokio-cron-scheduler = "0.9.4"
mlua = { version = "0.9.7", features = [ mlua = { version = "0.9.7", features = ["lua54", "vendored", "macros", "serialize", "async", "send"] }
"lua54",
"vendored",
"macros",
"serialize",
"async",
"send",
] }
once_cell = "1.19.0" once_cell = "1.19.0"
hostname = "0.4.0"
[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

@ -1,8 +0,0 @@
[package]
name = "automation_cast"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

View File

@ -1,37 +0,0 @@
#![allow(incomplete_features)]
#![feature(specialization)]
#![feature(unsize)]
use std::marker::Unsize;
pub trait Cast<P: ?Sized> {
fn cast(&self) -> Option<&P>;
fn cast_mut(&mut self) -> Option<&mut P>;
}
impl<D, P> Cast<P> for D
where
P: ?Sized,
{
default fn cast(&self) -> Option<&P> {
None
}
default fn cast_mut(&mut self) -> Option<&mut P> {
None
}
}
impl<D, P> Cast<P> for D
where
D: Unsize<P>,
P: ?Sized,
{
fn cast(&self) -> Option<&P> {
Some(self)
}
fn cast_mut(&mut self) -> Option<&mut P> {
Some(self)
}
}

View File

@ -1,7 +1,8 @@
print("Hello from lua") print("Hello from lua")
local host = automation.util.get_hostname() automation.fulfillment = {
print("Running @" .. host) openid_url = "https://login.huizinga.dev/api/oidc",
}
local debug, value = pcall(automation.util.get_env, "DEBUG") local debug, value = pcall(automation.util.get_env, "DEBUG")
if debug and value ~= "true" then if debug and value ~= "true" then
@ -16,19 +17,13 @@ local function mqtt_automation(topic)
return "automation/" .. topic return "automation/" .. topic
end end
automation.fulfillment = {
openid_url = "https://login.huizinga.dev/api/oidc",
}
local mqtt_client = automation.new_mqtt_client({ local mqtt_client = automation.new_mqtt_client({
host = (host == "zeus" and "olympus.lan.huizinga.dev") host = debug and "olympus.lan.huizinga.dev" or "mosquitto",
or (host == "hephaestus" and "olympus.vpn.huizinga.dev")
or "mosquitto",
port = 8883, port = 8883,
client_name = "automation-" .. host, client_name = debug and "automation-debug" or "automation_rs",
username = "mqtt", username = "mqtt",
password = automation.util.get_env("MQTT_PASSWORD"), password = automation.util.get_env("MQTT_PASSWORD"),
tls = host == "zeus" or host == "hephaestus", tls = debug and true or false,
}) })
automation.device_manager:add(Ntfy.new({ automation.device_manager:add(Ntfy.new({

View File

@ -6,7 +6,7 @@ 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/" } impl_cast = { path = "../impl_cast" }
serde = { version = "1.0.149", features = ["derive"] } serde = { version = "1.0.149", features = ["derive"] }
serde_json = "1.0.89" serde_json = "1.0.89"
thiserror = "1.0.37" thiserror = "1.0.37"

View File

@ -1,5 +1,4 @@
use async_trait::async_trait; use async_trait::async_trait;
use automation_cast::Cast;
use serde::Serialize; use serde::Serialize;
use crate::errors::{DeviceError, ErrorCode}; use crate::errors::{DeviceError, ErrorCode};
@ -8,10 +7,43 @@ use crate::response;
use crate::traits::{FanSpeed, HumiditySetting, OnOff, Scene, Trait}; use crate::traits::{FanSpeed, HumiditySetting, OnOff, Scene, Trait};
use crate::types::Type; use crate::types::Type;
#[async_trait] // TODO: Find a more elegant way to do this
pub trait GoogleHomeDevice: pub trait AsGoogleHomeDevice {
Sync + Send + Cast<dyn OnOff> + Cast<dyn Scene> + Cast<dyn FanSpeed> + Cast<dyn HumiditySetting> fn cast(&self) -> Option<&dyn GoogleHomeDevice>;
fn cast_mut(&mut self) -> Option<&mut dyn GoogleHomeDevice>;
}
// Default impl
impl<T> AsGoogleHomeDevice for T
where
T: 'static,
{ {
default fn cast(&self) -> Option<&(dyn GoogleHomeDevice + 'static)> {
None
}
default fn cast_mut(&mut self) -> Option<&mut (dyn GoogleHomeDevice + 'static)> {
None
}
}
// Specialization
impl<T> AsGoogleHomeDevice for T
where
T: GoogleHomeDevice + 'static,
{
fn cast(&self) -> Option<&(dyn GoogleHomeDevice + 'static)> {
Some(self)
}
fn cast_mut(&mut self) -> Option<&mut (dyn GoogleHomeDevice + 'static)> {
Some(self)
}
}
#[async_trait]
#[impl_cast::device(As: OnOff + Scene + FanSpeed + HumiditySetting)]
pub trait GoogleHomeDevice: AsGoogleHomeDevice + Sync + Send + 'static {
fn get_device_type(&self) -> Type; fn get_device_type(&self) -> Type;
fn get_device_name(&self) -> Name; fn get_device_name(&self) -> Name;
fn get_id(&self) -> String; fn get_id(&self) -> String;
@ -44,26 +76,26 @@ pub trait GoogleHomeDevice:
let mut traits = Vec::new(); let mut traits = Vec::new();
// OnOff // OnOff
if let Some(on_off) = self.cast() as Option<&dyn OnOff> { if let Some(on_off) = As::<dyn OnOff>::cast(self) {
traits.push(Trait::OnOff); traits.push(Trait::OnOff);
device.attributes.command_only_on_off = on_off.is_command_only(); device.attributes.command_only_on_off = on_off.is_command_only();
device.attributes.query_only_on_off = on_off.is_query_only(); device.attributes.query_only_on_off = on_off.is_query_only();
} }
// Scene // Scene
if let Some(scene) = self.cast() as Option<&dyn Scene> { if let Some(scene) = As::<dyn Scene>::cast(self) {
traits.push(Trait::Scene); traits.push(Trait::Scene);
device.attributes.scene_reversible = scene.is_scene_reversible(); device.attributes.scene_reversible = scene.is_scene_reversible();
} }
// FanSpeed // FanSpeed
if let Some(fan_speed) = self.cast() as Option<&dyn FanSpeed> { if let Some(fan_speed) = As::<dyn FanSpeed>::cast(self) {
traits.push(Trait::FanSpeed); traits.push(Trait::FanSpeed);
device.attributes.command_only_fan_speed = fan_speed.command_only_fan_speed(); device.attributes.command_only_fan_speed = fan_speed.command_only_fan_speed();
device.attributes.available_fan_speeds = Some(fan_speed.available_speeds()); device.attributes.available_fan_speeds = Some(fan_speed.available_speeds());
} }
if let Some(humidity_setting) = self.cast() as Option<&dyn HumiditySetting> { if let Some(humidity_setting) = As::<dyn HumiditySetting>::cast(self) {
traits.push(Trait::HumiditySetting); traits.push(Trait::HumiditySetting);
device.attributes.query_only_humidity_setting = device.attributes.query_only_humidity_setting =
humidity_setting.query_only_humidity_setting(); humidity_setting.query_only_humidity_setting();
@ -81,7 +113,7 @@ pub trait GoogleHomeDevice:
} }
// OnOff // OnOff
if let Some(on_off) = self.cast() as Option<&dyn OnOff> { if let Some(on_off) = As::<dyn OnOff>::cast(self) {
device.state.on = on_off device.state.on = on_off
.is_on() .is_on()
.await .await
@ -90,11 +122,11 @@ pub trait GoogleHomeDevice:
} }
// FanSpeed // FanSpeed
if let Some(fan_speed) = self.cast() as Option<&dyn FanSpeed> { if let Some(fan_speed) = As::<dyn FanSpeed>::cast(self) {
device.state.current_fan_speed_setting = Some(fan_speed.current_speed().await); device.state.current_fan_speed_setting = Some(fan_speed.current_speed().await);
} }
if let Some(humidity_setting) = self.cast() as Option<&dyn HumiditySetting> { if let Some(humidity_setting) = As::<dyn HumiditySetting>::cast(self) {
device.state.humidity_ambient_percent = device.state.humidity_ambient_percent =
Some(humidity_setting.humidity_ambient_percent().await); Some(humidity_setting.humidity_ambient_percent().await);
} }
@ -105,21 +137,21 @@ pub trait GoogleHomeDevice:
async fn execute(&mut self, command: &CommandType) -> Result<(), ErrorCode> { async fn execute(&mut self, command: &CommandType) -> Result<(), ErrorCode> {
match command { match command {
CommandType::OnOff { on } => { CommandType::OnOff { on } => {
if let Some(t) = self.cast_mut() as Option<&mut dyn OnOff> { if let Some(t) = As::<dyn OnOff>::cast_mut(self) {
t.set_on(*on).await?; t.set_on(*on).await?;
} else { } else {
return Err(DeviceError::ActionNotAvailable.into()); return Err(DeviceError::ActionNotAvailable.into());
} }
} }
CommandType::ActivateScene { deactivate } => { CommandType::ActivateScene { deactivate } => {
if let Some(t) = self.cast_mut() as Option<&mut dyn Scene> { if let Some(t) = As::<dyn Scene>::cast(self) {
t.set_active(!deactivate).await?; t.set_active(!deactivate).await?;
} else { } else {
return Err(DeviceError::ActionNotAvailable.into()); return Err(DeviceError::ActionNotAvailable.into());
} }
} }
CommandType::SetFanSpeed { fan_speed } => { CommandType::SetFanSpeed { fan_speed } => {
if let Some(t) = self.cast_mut() as Option<&mut dyn FanSpeed> { if let Some(t) = As::<dyn FanSpeed>::cast(self) {
t.set_speed(fan_speed).await?; t.set_speed(fan_speed).await?;
} }
} }

View File

@ -1,15 +1,14 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use automation_cast::Cast;
use futures::future::{join_all, OptionFuture}; use futures::future::{join_all, OptionFuture};
use thiserror::Error; use thiserror::Error;
use tokio::sync::{Mutex, RwLock}; use tokio::sync::{Mutex, RwLock};
use crate::device::AsGoogleHomeDevice;
use crate::errors::{DeviceError, ErrorCode}; use crate::errors::{DeviceError, ErrorCode};
use crate::request::{self, Intent, Request}; use crate::request::{self, Intent, Request};
use crate::response::{self, execute, query, sync, Response, ResponsePayload, State}; use crate::response::{self, execute, query, sync, Response, ResponsePayload, State};
use crate::GoogleHomeDevice;
#[derive(Debug)] #[derive(Debug)]
pub struct GoogleHome { pub struct GoogleHome {
@ -30,7 +29,7 @@ impl GoogleHome {
} }
} }
pub async fn handle_request<T: Cast<dyn GoogleHomeDevice> + ?Sized + 'static>( pub async fn handle_request<T: AsGoogleHomeDevice + ?Sized + 'static>(
&self, &self,
request: Request, request: Request,
devices: &HashMap<String, Arc<RwLock<Box<T>>>>, devices: &HashMap<String, Arc<RwLock<Box<T>>>>,
@ -59,7 +58,7 @@ impl GoogleHome {
.map(|payload| Response::new(&request.request_id, payload)) .map(|payload| Response::new(&request.request_id, payload))
} }
async fn sync<T: Cast<dyn GoogleHomeDevice> + ?Sized + 'static>( async fn sync<T: AsGoogleHomeDevice + ?Sized + 'static>(
&self, &self,
devices: &HashMap<String, Arc<RwLock<Box<T>>>>, devices: &HashMap<String, Arc<RwLock<Box<T>>>>,
) -> sync::Payload { ) -> sync::Payload {
@ -76,7 +75,7 @@ impl GoogleHome {
resp_payload resp_payload
} }
async fn query<T: Cast<dyn GoogleHomeDevice> + ?Sized + 'static>( async fn query<T: AsGoogleHomeDevice + ?Sized + 'static>(
&self, &self,
payload: request::query::Payload, payload: request::query::Payload,
devices: &HashMap<String, Arc<RwLock<Box<T>>>>, devices: &HashMap<String, Arc<RwLock<Box<T>>>>,
@ -108,7 +107,7 @@ impl GoogleHome {
resp_payload resp_payload
} }
async fn execute<T: Cast<dyn GoogleHomeDevice> + ?Sized + 'static>( async fn execute<T: AsGoogleHomeDevice + ?Sized + 'static>(
&self, &self,
payload: request::execute::Payload, payload: request::execute::Payload,
devices: &HashMap<String, Arc<RwLock<Box<T>>>>, devices: &HashMap<String, Arc<RwLock<Box<T>>>>,

View File

@ -16,7 +16,8 @@ pub enum Trait {
} }
#[async_trait] #[async_trait]
pub trait OnOff: Sync + Send { #[impl_cast::device_trait]
pub trait OnOff {
fn is_command_only(&self) -> Option<bool> { fn is_command_only(&self) -> Option<bool> {
None None
} }
@ -31,7 +32,8 @@ pub trait OnOff: Sync + Send {
} }
#[async_trait] #[async_trait]
pub trait Scene: Sync + Send { #[impl_cast::device_trait]
pub trait Scene {
fn is_scene_reversible(&self) -> Option<bool> { fn is_scene_reversible(&self) -> Option<bool> {
None None
} }
@ -58,7 +60,8 @@ pub struct AvailableSpeeds {
} }
#[async_trait] #[async_trait]
pub trait FanSpeed: Sync + Send { #[impl_cast::device_trait]
pub trait FanSpeed {
fn reversible(&self) -> Option<bool> { fn reversible(&self) -> Option<bool> {
None None
} }
@ -73,7 +76,8 @@ pub trait FanSpeed: Sync + Send {
} }
#[async_trait] #[async_trait]
pub trait HumiditySetting: Sync + Send { #[impl_cast::device_trait]
pub trait HumiditySetting {
// TODO: This implementation is not complete, I have only implemented what I need right now // TODO: This implementation is not complete, I have only implemented what I need right now
fn query_only_humidity_setting(&self) -> Option<bool> { fn query_only_humidity_setting(&self) -> Option<bool> {
None None

15
impl_cast/Cargo.toml Normal file
View File

@ -0,0 +1,15 @@
[package]
name = "impl_cast"
version = "0.1.0"
edition = "2021"
[lib]
proc-macro = true
[dependencies]
syn = { version = "2.0", features = ["extra-traits", "full"] }
quote = "1.0"
[features]
debug = [
] # If enabled it will add std::fmt::Debug as a trait bound to device_traits

165
impl_cast/src/lib.rs Normal file
View File

@ -0,0 +1,165 @@
use proc_macro::TokenStream;
use quote::{format_ident, quote, ToTokens};
use syn::parse::Parse;
use syn::{parse_macro_input, Ident, ItemTrait, Path, Token, TypeParamBound};
struct Attr {
name: Ident,
traits: Vec<Path>,
}
impl Parse for Attr {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let mut traits = Vec::new();
let name = input.parse::<Ident>()?;
input.parse::<Token![:]>()?;
loop {
let ty = input.parse()?;
traits.push(ty);
if input.is_empty() {
break;
}
input.parse::<Token![+]>()?;
}
Ok(Attr { name, traits })
}
}
/// This macro enables optional trait bounds on a trait with an appropriate cast trait to convert
/// to the optional traits
/// # Example
///
/// ```
/// #![feature(specialization)]
///
/// // Create some traits
/// #[impl_cast::device_trait]
/// trait OnOff {}
/// #[impl_cast::device_trait]
/// trait Brightness {}
///
/// // Create the main device trait
/// #[impl_cast::device(As: OnOff + Brightness)]
/// trait Device {}
///
/// // Create an implementation
/// struct ExampleDevice {}
/// impl Device for ExampleDevice {}
/// impl OnOff for ExampleDevice {}
///
/// // Creates a boxed instance of the example device
/// let example_device: Box<dyn Device> = Box::new(ExampleDevice {});
///
/// // Cast to the OnOff trait, which is implemented
/// let as_on_off = As::<dyn OnOff>::cast(example_device.as_ref());
/// assert!(as_on_off.is_some());
///
/// // Cast to the Brightness trait, which is not implemented
/// let as_on_off = As::<dyn Brightness>::cast(example_device.as_ref());
/// assert!(as_on_off.is_none());
///
/// // Finally we are going to consume the example device into an instance of the OnOff trait
/// let consumed = As::<dyn OnOff>::consume(example_device);
/// assert!(consumed.is_some())
/// ```
#[proc_macro_attribute]
pub fn device(attr: TokenStream, item: TokenStream) -> TokenStream {
let Attr { name, traits } = parse_macro_input!(attr);
let mut interface: ItemTrait = parse_macro_input!(item);
let prefix = quote! {
pub trait #name<T: ?Sized + 'static> {
fn is(&self) -> bool;
fn cast(&self) -> Option<&T>;
fn cast_mut(&mut self) -> Option<&mut T>;
}
};
traits.iter().for_each(|device_trait| {
interface.supertraits.push(TypeParamBound::Verbatim(quote! {
#name<dyn #device_trait>
}));
});
let interface_ident = format_ident!("{}", interface.ident);
let impls = traits
.iter()
.map(|device_trait| {
quote! {
// Default impl
impl<T> #name<dyn #device_trait> for T
where
T: #interface_ident + 'static,
{
default fn is(&self) -> bool {
false
}
default fn cast(&self) -> Option<&(dyn #device_trait + 'static)> {
None
}
default fn cast_mut(&mut self) -> Option<&mut (dyn #device_trait + 'static)> {
None
}
}
// Specialization, should not cause any unsoundness as we dispatch based on
// #device_trait
impl<T> #name<dyn #device_trait> for T
where
T: #interface_ident + #device_trait + 'static,
{
fn is(&self) -> bool {
true
}
fn cast(&self) -> Option<&(dyn #device_trait + 'static)> {
Some(self)
}
fn cast_mut(&mut self) -> Option<&mut (dyn #device_trait + 'static)> {
Some(self)
}
}
}
})
.fold(quote! {}, |acc, x| {
quote! {
// Not sure if this is the right way to do this
#acc
#x
}
});
let tokens = quote! {
#interface
#prefix
#impls
};
tokens.into()
}
// TODO: Not sure if this makes sense to have?
/// This macro ensures that the device traits have the correct trait bounds
#[proc_macro_attribute]
pub fn device_trait(_attr: TokenStream, item: TokenStream) -> TokenStream {
let mut interface: ItemTrait = parse_macro_input!(item);
interface.supertraits.push(TypeParamBound::Verbatim(quote! {
::core::marker::Sync + ::core::marker::Send
}));
#[cfg(feature = "debug")]
interface.supertraits.push(TypeParamBound::Verbatim(quote! {
::std::fmt::Debug
}));
interface.into_token_stream().into()
}

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::devices::{As, Device};
use crate::event::{Event, EventChannel, OnDarkness, OnMqtt, OnNotification, OnPresence}; use crate::event::{Event, EventChannel, OnDarkness, OnMqtt, OnNotification, OnPresence};
use crate::LUA; use crate::LUA;
@ -80,7 +80,7 @@ impl DeviceManager {
} }
pub async fn add(&self, device: &WrappedDevice) { pub async fn add(&self, device: &WrappedDevice) {
let id = device.read().await.get_id().to_owned(); let id = device.read().await.get_id();
debug!(id, "Adding device"); debug!(id, "Adding device");
@ -113,8 +113,8 @@ impl DeviceManager {
let message = message.clone(); let message = message.clone();
async move { async move {
let mut device = device.write().await; let mut device = device.write().await;
let device: Option<&mut dyn OnMqtt> = device.as_mut().cast_mut(); let device = device.as_mut();
if let Some(device) = device { if let Some(device) = As::<dyn OnMqtt>::cast_mut(device) {
// let subscribed = device // let subscribed = device
// .topics() // .topics()
// .iter() // .iter()
@ -135,8 +135,8 @@ impl DeviceManager {
let devices = self.devices.read().await; let devices = self.devices.read().await;
let iter = devices.iter().map(|(id, device)| async move { let iter = devices.iter().map(|(id, device)| async move {
let mut device = device.write().await; let mut device = device.write().await;
let device: Option<&mut dyn OnDarkness> = device.as_mut().cast_mut(); let device = device.as_mut();
if let Some(device) = device { if let Some(device) = As::<dyn OnDarkness>::cast_mut(device) {
trace!(id, "Handling"); trace!(id, "Handling");
device.on_darkness(dark).await; device.on_darkness(dark).await;
trace!(id, "Done"); trace!(id, "Done");
@ -149,8 +149,8 @@ impl DeviceManager {
let devices = self.devices.read().await; let devices = self.devices.read().await;
let iter = devices.iter().map(|(id, device)| async move { let iter = devices.iter().map(|(id, device)| async move {
let mut device = device.write().await; let mut device = device.write().await;
let device: Option<&mut dyn OnPresence> = device.as_mut().cast_mut(); let device = device.as_mut();
if let Some(device) = device { if let Some(device) = As::<dyn OnPresence>::cast_mut(device) {
trace!(id, "Handling"); trace!(id, "Handling");
device.on_presence(presence).await; device.on_presence(presence).await;
trace!(id, "Done"); trace!(id, "Done");
@ -165,8 +165,8 @@ impl DeviceManager {
let notification = notification.clone(); let notification = notification.clone();
async move { async move {
let mut device = device.write().await; let mut device = device.write().await;
let device: Option<&mut dyn OnNotification> = device.as_mut().cast_mut(); let device = device.as_mut();
if let Some(device) = device { if let Some(device) = As::<dyn OnNotification>::cast_mut(device) {
trace!(id, "Handling"); trace!(id, "Handling");
device.on_notification(notification).await; device.on_notification(notification).await;
trace!(id, "Done"); trace!(id, "Done");

View File

@ -6,6 +6,7 @@ use tracing::{debug, error, trace, warn};
use super::{Device, LuaDeviceCreate}; use super::{Device, LuaDeviceCreate};
use crate::config::MqttDeviceConfig; use crate::config::MqttDeviceConfig;
use crate::device_manager::WrappedDevice; use crate::device_manager::WrappedDevice;
use crate::devices::As;
use crate::error::DeviceConfigError; use crate::error::DeviceConfigError;
use crate::event::{OnMqtt, OnPresence}; use crate::event::{OnMqtt, OnPresence};
use crate::messages::{RemoteAction, RemoteMessage}; use crate::messages::{RemoteAction, RemoteMessage};
@ -37,19 +38,15 @@ impl LuaDeviceCreate for AudioSetup {
async fn create(config: Self::Config) -> Result<Self, Self::Error> { async fn create(config: Self::Config) -> Result<Self, Self::Error> {
trace!(id = config.identifier, "Setting up AudioSetup"); trace!(id = config.identifier, "Setting up AudioSetup");
{ let mixer_id = config.mixer.read().await.get_id().to_owned();
let mixer = config.mixer.read().await; if !As::<dyn OnOff>::is(config.mixer.read().await.as_ref()) {
let mixer_id = mixer.get_id().to_owned();
if (mixer.as_ref().cast() as Option<&dyn OnOff>).is_none() {
return Err(DeviceConfigError::MissingTrait(mixer_id, "OnOff".into())); return Err(DeviceConfigError::MissingTrait(mixer_id, "OnOff".into()));
} }
let speakers = config.speakers.read().await; let speakers_id = config.speakers.read().await.get_id().to_owned();
let speakers_id = speakers.get_id().to_owned(); if !As::<dyn OnOff>::is(config.speakers.read().await.as_ref()) {
if (speakers.as_ref().cast() as Option<&dyn OnOff>).is_none() {
return Err(DeviceConfigError::MissingTrait(speakers_id, "OnOff".into())); return Err(DeviceConfigError::MissingTrait(speakers_id, "OnOff".into()));
} }
}
config config
.client .client
@ -87,8 +84,8 @@ impl OnMqtt for AudioSetup {
let mut mixer = self.config.mixer.write().await; let mut mixer = self.config.mixer.write().await;
let mut speakers = self.config.speakers.write().await; let mut speakers = self.config.speakers.write().await;
if let (Some(mixer), Some(speakers)) = ( if let (Some(mixer), Some(speakers)) = (
mixer.as_mut().cast_mut() as Option<&mut dyn OnOff>, As::<dyn OnOff>::cast_mut(mixer.as_mut()),
speakers.as_mut().cast_mut() as Option<&mut dyn OnOff>, As::<dyn OnOff>::cast_mut(speakers.as_mut()),
) { ) {
match action { match action {
RemoteAction::On => { RemoteAction::On => {
@ -123,8 +120,8 @@ impl OnPresence for AudioSetup {
let mut speakers = self.config.speakers.write().await; let mut speakers = self.config.speakers.write().await;
if let (Some(mixer), Some(speakers)) = ( if let (Some(mixer), Some(speakers)) = (
mixer.as_mut().cast_mut() as Option<&mut dyn OnOff>, As::<dyn OnOff>::cast_mut(mixer.as_mut()),
speakers.as_mut().cast_mut() as Option<&mut dyn OnOff>, As::<dyn OnOff>::cast_mut(speakers.as_mut()),
) { ) {
// Turn off the audio setup when we leave the house // Turn off the audio setup when we leave the house
if !presence { if !presence {

View File

@ -10,7 +10,7 @@ use tracing::{debug, error, trace, warn};
use super::{Device, LuaDeviceCreate}; use super::{Device, LuaDeviceCreate};
use crate::config::MqttDeviceConfig; use crate::config::MqttDeviceConfig;
use crate::device_manager::WrappedDevice; use crate::device_manager::WrappedDevice;
use crate::devices::DEFAULT_PRESENCE; use crate::devices::{As, DEFAULT_PRESENCE};
use crate::error::DeviceConfigError; use crate::error::DeviceConfigError;
use crate::event::{OnMqtt, OnPresence}; use crate::event::{OnMqtt, OnPresence};
use crate::messages::{ContactMessage, PresenceMessage}; use crate::messages::{ContactMessage, PresenceMessage};
@ -82,21 +82,17 @@ impl LuaDeviceCreate for ContactSensor {
// Make sure the devices implement the required traits // Make sure the devices implement the required traits
if let Some(trigger) = &config.trigger { if let Some(trigger) = &config.trigger {
for (device, _) in &trigger.devices { for (device, _) in &trigger.devices {
{ let id = device.read().await.get_id().to_owned();
let device = device.read().await; if !As::<dyn OnOff>::is(device.read().await.as_ref()) {
let id = device.get_id().to_owned();
if (device.as_ref().cast() as Option<&dyn OnOff>).is_none() {
return Err(DeviceConfigError::MissingTrait(id, "OnOff".into())); return Err(DeviceConfigError::MissingTrait(id, "OnOff".into()));
} }
if trigger.timeout.is_none() if trigger.timeout.is_none() && !As::<dyn Timeout>::is(device.read().await.as_ref())
&& (device.as_ref().cast() as Option<&dyn Timeout>).is_none()
{ {
return Err(DeviceConfigError::MissingTrait(id, "Timeout".into())); return Err(DeviceConfigError::MissingTrait(id, "Timeout".into()));
} }
} }
} }
}
config config
.client .client
@ -154,7 +150,7 @@ impl OnMqtt for ContactSensor {
if !self.is_closed { if !self.is_closed {
for (light, previous) in &mut trigger.devices { for (light, previous) in &mut trigger.devices {
let mut light = light.write().await; let mut light = light.write().await;
if let Some(light) = light.as_mut().cast_mut() as Option<&mut dyn OnOff> { if let Some(light) = As::<dyn OnOff>::cast_mut(light.as_mut()) {
*previous = light.is_on().await.unwrap(); *previous = light.is_on().await.unwrap();
light.set_on(true).await.ok(); light.set_on(true).await.ok();
} }
@ -165,12 +161,11 @@ impl OnMqtt for ContactSensor {
if !previous { if !previous {
// If the timeout is zero just turn the light off directly // If the timeout is zero just turn the light off directly
if trigger.timeout.is_none() if trigger.timeout.is_none()
&& let Some(light) = light.as_mut().cast_mut() as Option<&mut dyn OnOff> && let Some(light) = As::<dyn OnOff>::cast_mut(light.as_mut())
{ {
light.set_on(false).await.ok(); light.set_on(false).await.ok();
} else if let Some(timeout) = trigger.timeout } else if let Some(timeout) = trigger.timeout
&& let Some(light) = && let Some(light) = As::<dyn Timeout>::cast_mut(light.as_mut())
light.as_mut().cast_mut() as Option<&mut dyn Timeout>
{ {
light.start_timeout(timeout).await.unwrap(); light.start_timeout(timeout).await.unwrap();
} }

View File

@ -12,12 +12,9 @@ mod presence;
mod wake_on_lan; mod wake_on_lan;
mod washer; mod washer;
use std::fmt::Debug;
use async_trait::async_trait; use async_trait::async_trait;
use automation_cast::Cast; use google_home::device::AsGoogleHomeDevice;
use google_home::traits::OnOff; use google_home::traits::OnOff;
use google_home::GoogleHomeDevice;
pub use self::air_filter::*; pub use self::air_filter::*;
pub use self::audio_setup::*; pub use self::audio_setup::*;
@ -63,18 +60,7 @@ pub fn register_with_lua(lua: &mlua::Lua) -> mlua::Result<()> {
Ok(()) Ok(())
} }
pub trait Device: #[impl_cast::device(As: OnMqtt + OnPresence + OnDarkness + OnNotification + OnOff + Timeout)]
Debug pub trait Device: AsGoogleHomeDevice + std::fmt::Debug + Sync + Send {
+ Sync
+ Send
+ Cast<dyn GoogleHomeDevice>
+ Cast<dyn OnMqtt>
+ Cast<dyn OnMqtt>
+ Cast<dyn OnPresence>
+ Cast<dyn OnDarkness>
+ Cast<dyn OnNotification>
+ Cast<dyn OnOff>
+ Cast<dyn Timeout>
{
fn get_id(&self) -> String; fn get_id(&self) -> String;
} }

View File

@ -1,4 +1,5 @@
use async_trait::async_trait; use async_trait::async_trait;
use impl_cast::device_trait;
use mlua::FromLua; use mlua::FromLua;
use rumqttc::Publish; use rumqttc::Publish;
use tokio::sync::mpsc; use tokio::sync::mpsc;
@ -34,22 +35,26 @@ impl EventChannel {
impl mlua::UserData for EventChannel {} impl mlua::UserData for EventChannel {}
#[async_trait] #[async_trait]
pub trait OnMqtt: Sync + Send { #[device_trait]
pub trait OnMqtt {
// fn topics(&self) -> Vec<&str>; // fn topics(&self) -> Vec<&str>;
async fn on_mqtt(&mut self, message: Publish); async fn on_mqtt(&mut self, message: Publish);
} }
#[async_trait] #[async_trait]
pub trait OnPresence: Sync + Send { #[device_trait]
pub trait OnPresence {
async fn on_presence(&mut self, presence: bool); async fn on_presence(&mut self, presence: bool);
} }
#[async_trait] #[async_trait]
pub trait OnDarkness: Sync + Send { #[device_trait]
pub trait OnDarkness {
async fn on_darkness(&mut self, dark: bool); async fn on_darkness(&mut self, dark: bool);
} }
#[async_trait] #[async_trait]
pub trait OnNotification: Sync + Send { #[device_trait]
pub trait OnNotification {
async fn on_notification(&mut self, notification: Notification); async fn on_notification(&mut self, notification: Notification);
} }

View File

@ -47,8 +47,7 @@ async fn main() {
async fn app() -> anyhow::Result<()> { async fn app() -> anyhow::Result<()> {
dotenv().ok(); dotenv().ok();
tracing_subscriber::fmt::init(); console_subscriber::init();
// console_subscriber::init();
info!("Starting automation_rs..."); info!("Starting automation_rs...");
@ -84,12 +83,6 @@ async fn app() -> anyhow::Result<()> {
std::env::var(name).map_err(mlua::ExternalError::into_lua_err) std::env::var(name).map_err(mlua::ExternalError::into_lua_err)
})?; })?;
util.set("get_env", get_env)?; util.set("get_env", get_env)?;
let get_hostname = lua.create_function(|_lua, ()| {
hostname::get()
.map(|name| name.to_str().unwrap_or("unknown").to_owned())
.map_err(mlua::ExternalError::into_lua_err)
})?;
util.set("get_hostname", get_hostname)?;
automation.set("util", util)?; automation.set("util", util)?;
lua.globals().set("automation", automation)?; lua.globals().set("automation", automation)?;

View File

@ -2,9 +2,11 @@ use std::time::Duration;
use anyhow::Result; use anyhow::Result;
use async_trait::async_trait; use async_trait::async_trait;
use impl_cast::device_trait;
#[async_trait] #[async_trait]
pub trait Timeout: Sync + Send { #[device_trait]
pub trait Timeout {
async fn start_timeout(&mut self, _timeout: Duration) -> Result<()>; async fn start_timeout(&mut self, _timeout: Duration) -> Result<()>;
async fn stop_timeout(&mut self) -> Result<()>; async fn stop_timeout(&mut self) -> Result<()>;
} }