Compare commits
23 Commits
09853e00f4
...
9f9b8eb2dd
Author | SHA1 | Date | |
---|---|---|---|
9f9b8eb2dd | |||
7a18f0f29e | |||
1b9e36ce02 | |||
96006631c3 | |||
3720e1b8e8 | |||
c355322549 | |||
8cc6629c77 | |||
0cfcbeca90 | |||
0fd3133357 | |||
1714dd8a20 | |||
4f19ae4f68 | |||
5beda5c4b7 | |||
be259fa99d | |||
144f5bb911 | |||
1a7cbf4f86 | |||
bf7e023a35 | |||
d4f6dd20f0 | |||
acdf15f32d | |||
5e4a9ee4c9 | |||
dd9c2d3983 | |||
675761ba55 | |||
3689a52afd | |||
cde9654a78 |
49
Cargo.lock
generated
49
Cargo.lock
generated
|
@ -76,6 +76,7 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"automation_cast",
|
||||
"automation_macro",
|
||||
"axum",
|
||||
"bytes",
|
||||
|
@ -85,7 +86,7 @@ dependencies = [
|
|||
"eui48",
|
||||
"futures",
|
||||
"google-home",
|
||||
"impl_cast",
|
||||
"hostname",
|
||||
"indexmap 2.0.0",
|
||||
"mlua",
|
||||
"once_cell",
|
||||
|
@ -107,6 +108,10 @@ dependencies = [
|
|||
"wakey",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "automation_cast"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "automation_macro"
|
||||
version = "0.1.0"
|
||||
|
@ -616,8 +621,8 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"automation_cast",
|
||||
"futures",
|
||||
"impl_cast",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
|
@ -689,6 +694,17 @@ dependencies = [
|
|||
"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]]
|
||||
name = "http"
|
||||
version = "0.2.9"
|
||||
|
@ -790,7 +806,7 @@ dependencies = [
|
|||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"windows",
|
||||
"windows 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -818,14 +834,6 @@ dependencies = [
|
|||
"unicode-normalization",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "impl_cast"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.60",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.9.3"
|
||||
|
@ -2304,6 +2312,25 @@ dependencies = [
|
|||
"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]]
|
||||
name = "windows-sys"
|
||||
version = "0.48.0"
|
||||
|
|
15
Cargo.toml
15
Cargo.toml
|
@ -4,14 +4,15 @@ version = "0.1.0"
|
|||
edition = "2021"
|
||||
|
||||
[workspace]
|
||||
members = ["impl_cast", "google-home", "automation_macro"]
|
||||
members = ["google-home", "automation_macro", "automation_cast"]
|
||||
|
||||
|
||||
[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"
|
||||
impl_cast = { path = "./impl_cast", features = ["debug"] }
|
||||
google-home = { path = "./google-home" }
|
||||
paste = "1.0.10"
|
||||
tokio = { version = "1", features = ["rt-multi-thread"] }
|
||||
|
@ -42,8 +43,16 @@ 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.9.7", features = ["lua54", "vendored", "macros", "serialize", "async", "send"] }
|
||||
mlua = { version = "0.9.7", features = [
|
||||
"lua54",
|
||||
"vendored",
|
||||
"macros",
|
||||
"serialize",
|
||||
"async",
|
||||
"send",
|
||||
] }
|
||||
once_cell = "1.19.0"
|
||||
hostname = "0.4.0"
|
||||
|
||||
[patch.crates-io]
|
||||
wakey = { git = "https://git.huizinga.dev/Dreaded_X/wakey" }
|
||||
|
|
8
automation_cast/Cargo.toml
Normal file
8
automation_cast/Cargo.toml
Normal file
|
@ -0,0 +1,8 @@
|
|||
[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]
|
37
automation_cast/src/lib.rs
Normal file
37
automation_cast/src/lib.rs
Normal file
|
@ -0,0 +1,37 @@
|
|||
#![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)
|
||||
}
|
||||
}
|
17
config.lua
17
config.lua
|
@ -1,8 +1,7 @@
|
|||
print("Hello from lua")
|
||||
|
||||
automation.fulfillment = {
|
||||
openid_url = "https://login.huizinga.dev/api/oidc",
|
||||
}
|
||||
local host = automation.util.get_hostname()
|
||||
print("Running @" .. host)
|
||||
|
||||
local debug, value = pcall(automation.util.get_env, "DEBUG")
|
||||
if debug and value ~= "true" then
|
||||
|
@ -17,13 +16,19 @@ local function mqtt_automation(topic)
|
|||
return "automation/" .. topic
|
||||
end
|
||||
|
||||
automation.fulfillment = {
|
||||
openid_url = "https://login.huizinga.dev/api/oidc",
|
||||
}
|
||||
|
||||
local mqtt_client = automation.new_mqtt_client({
|
||||
host = debug and "olympus.lan.huizinga.dev" or "mosquitto",
|
||||
host = (host == "zeus" and "olympus.lan.huizinga.dev")
|
||||
or (host == "hephaestus" and "olympus.vpn.huizinga.dev")
|
||||
or "mosquitto",
|
||||
port = 8883,
|
||||
client_name = debug and "automation-debug" or "automation_rs",
|
||||
client_name = "automation-" .. host,
|
||||
username = "mqtt",
|
||||
password = automation.util.get_env("MQTT_PASSWORD"),
|
||||
tls = debug and true or false,
|
||||
tls = host == "zeus" or host == "hephaestus",
|
||||
})
|
||||
|
||||
automation.device_manager:add(Ntfy.new({
|
||||
|
|
|
@ -6,7 +6,7 @@ edition = "2021"
|
|||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
impl_cast = { path = "../impl_cast" }
|
||||
automation_cast = { path = "../automation_cast/" }
|
||||
serde = { version = "1.0.149", features = ["derive"] }
|
||||
serde_json = "1.0.89"
|
||||
thiserror = "1.0.37"
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use async_trait::async_trait;
|
||||
use automation_cast::Cast;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::errors::{DeviceError, ErrorCode};
|
||||
|
@ -7,43 +8,10 @@ use crate::response;
|
|||
use crate::traits::{FanSpeed, HumiditySetting, OnOff, Scene, Trait};
|
||||
use crate::types::Type;
|
||||
|
||||
// TODO: Find a more elegant way to do this
|
||||
pub trait AsGoogleHomeDevice {
|
||||
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 {
|
||||
pub trait GoogleHomeDevice:
|
||||
Sync + Send + Cast<dyn OnOff> + Cast<dyn Scene> + Cast<dyn FanSpeed> + Cast<dyn HumiditySetting>
|
||||
{
|
||||
fn get_device_type(&self) -> Type;
|
||||
fn get_device_name(&self) -> Name;
|
||||
fn get_id(&self) -> String;
|
||||
|
@ -76,26 +44,26 @@ pub trait GoogleHomeDevice: AsGoogleHomeDevice + Sync + Send + 'static {
|
|||
let mut traits = Vec::new();
|
||||
|
||||
// OnOff
|
||||
if let Some(on_off) = As::<dyn OnOff>::cast(self) {
|
||||
if let Some(on_off) = self.cast() as Option<&dyn OnOff> {
|
||||
traits.push(Trait::OnOff);
|
||||
device.attributes.command_only_on_off = on_off.is_command_only();
|
||||
device.attributes.query_only_on_off = on_off.is_query_only();
|
||||
}
|
||||
|
||||
// Scene
|
||||
if let Some(scene) = As::<dyn Scene>::cast(self) {
|
||||
if let Some(scene) = self.cast() as Option<&dyn Scene> {
|
||||
traits.push(Trait::Scene);
|
||||
device.attributes.scene_reversible = scene.is_scene_reversible();
|
||||
}
|
||||
|
||||
// FanSpeed
|
||||
if let Some(fan_speed) = As::<dyn FanSpeed>::cast(self) {
|
||||
if let Some(fan_speed) = self.cast() as Option<&dyn FanSpeed> {
|
||||
traits.push(Trait::FanSpeed);
|
||||
device.attributes.command_only_fan_speed = fan_speed.command_only_fan_speed();
|
||||
device.attributes.available_fan_speeds = Some(fan_speed.available_speeds());
|
||||
}
|
||||
|
||||
if let Some(humidity_setting) = As::<dyn HumiditySetting>::cast(self) {
|
||||
if let Some(humidity_setting) = self.cast() as Option<&dyn HumiditySetting> {
|
||||
traits.push(Trait::HumiditySetting);
|
||||
device.attributes.query_only_humidity_setting =
|
||||
humidity_setting.query_only_humidity_setting();
|
||||
|
@ -113,7 +81,7 @@ pub trait GoogleHomeDevice: AsGoogleHomeDevice + Sync + Send + 'static {
|
|||
}
|
||||
|
||||
// OnOff
|
||||
if let Some(on_off) = As::<dyn OnOff>::cast(self) {
|
||||
if let Some(on_off) = self.cast() as Option<&dyn OnOff> {
|
||||
device.state.on = on_off
|
||||
.is_on()
|
||||
.await
|
||||
|
@ -122,11 +90,11 @@ pub trait GoogleHomeDevice: AsGoogleHomeDevice + Sync + Send + 'static {
|
|||
}
|
||||
|
||||
// FanSpeed
|
||||
if let Some(fan_speed) = As::<dyn FanSpeed>::cast(self) {
|
||||
if let Some(fan_speed) = self.cast() as Option<&dyn FanSpeed> {
|
||||
device.state.current_fan_speed_setting = Some(fan_speed.current_speed().await);
|
||||
}
|
||||
|
||||
if let Some(humidity_setting) = As::<dyn HumiditySetting>::cast(self) {
|
||||
if let Some(humidity_setting) = self.cast() as Option<&dyn HumiditySetting> {
|
||||
device.state.humidity_ambient_percent =
|
||||
Some(humidity_setting.humidity_ambient_percent().await);
|
||||
}
|
||||
|
@ -137,21 +105,21 @@ pub trait GoogleHomeDevice: AsGoogleHomeDevice + Sync + Send + 'static {
|
|||
async fn execute(&mut self, command: &CommandType) -> Result<(), ErrorCode> {
|
||||
match command {
|
||||
CommandType::OnOff { on } => {
|
||||
if let Some(t) = As::<dyn OnOff>::cast_mut(self) {
|
||||
if let Some(t) = self.cast_mut() as Option<&mut dyn OnOff> {
|
||||
t.set_on(*on).await?;
|
||||
} else {
|
||||
return Err(DeviceError::ActionNotAvailable.into());
|
||||
}
|
||||
}
|
||||
CommandType::ActivateScene { deactivate } => {
|
||||
if let Some(t) = As::<dyn Scene>::cast(self) {
|
||||
if let Some(t) = self.cast_mut() as Option<&mut dyn Scene> {
|
||||
t.set_active(!deactivate).await?;
|
||||
} else {
|
||||
return Err(DeviceError::ActionNotAvailable.into());
|
||||
}
|
||||
}
|
||||
CommandType::SetFanSpeed { fan_speed } => {
|
||||
if let Some(t) = As::<dyn FanSpeed>::cast(self) {
|
||||
if let Some(t) = self.cast_mut() as Option<&mut dyn FanSpeed> {
|
||||
t.set_speed(fan_speed).await?;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use automation_cast::Cast;
|
||||
use futures::future::{join_all, OptionFuture};
|
||||
use thiserror::Error;
|
||||
use tokio::sync::{Mutex, RwLock};
|
||||
|
||||
use crate::device::AsGoogleHomeDevice;
|
||||
use crate::errors::{DeviceError, ErrorCode};
|
||||
use crate::request::{self, Intent, Request};
|
||||
use crate::response::{self, execute, query, sync, Response, ResponsePayload, State};
|
||||
use crate::GoogleHomeDevice;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct GoogleHome {
|
||||
|
@ -29,7 +30,7 @@ impl GoogleHome {
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn handle_request<T: AsGoogleHomeDevice + ?Sized + 'static>(
|
||||
pub async fn handle_request<T: Cast<dyn GoogleHomeDevice> + ?Sized + 'static>(
|
||||
&self,
|
||||
request: Request,
|
||||
devices: &HashMap<String, Arc<RwLock<Box<T>>>>,
|
||||
|
@ -58,7 +59,7 @@ impl GoogleHome {
|
|||
.map(|payload| Response::new(&request.request_id, payload))
|
||||
}
|
||||
|
||||
async fn sync<T: AsGoogleHomeDevice + ?Sized + 'static>(
|
||||
async fn sync<T: Cast<dyn GoogleHomeDevice> + ?Sized + 'static>(
|
||||
&self,
|
||||
devices: &HashMap<String, Arc<RwLock<Box<T>>>>,
|
||||
) -> sync::Payload {
|
||||
|
@ -75,7 +76,7 @@ impl GoogleHome {
|
|||
resp_payload
|
||||
}
|
||||
|
||||
async fn query<T: AsGoogleHomeDevice + ?Sized + 'static>(
|
||||
async fn query<T: Cast<dyn GoogleHomeDevice> + ?Sized + 'static>(
|
||||
&self,
|
||||
payload: request::query::Payload,
|
||||
devices: &HashMap<String, Arc<RwLock<Box<T>>>>,
|
||||
|
@ -107,7 +108,7 @@ impl GoogleHome {
|
|||
resp_payload
|
||||
}
|
||||
|
||||
async fn execute<T: AsGoogleHomeDevice + ?Sized + 'static>(
|
||||
async fn execute<T: Cast<dyn GoogleHomeDevice> + ?Sized + 'static>(
|
||||
&self,
|
||||
payload: request::execute::Payload,
|
||||
devices: &HashMap<String, Arc<RwLock<Box<T>>>>,
|
||||
|
|
|
@ -16,8 +16,7 @@ pub enum Trait {
|
|||
}
|
||||
|
||||
#[async_trait]
|
||||
#[impl_cast::device_trait]
|
||||
pub trait OnOff {
|
||||
pub trait OnOff: Sync + Send {
|
||||
fn is_command_only(&self) -> Option<bool> {
|
||||
None
|
||||
}
|
||||
|
@ -32,8 +31,7 @@ pub trait OnOff {
|
|||
}
|
||||
|
||||
#[async_trait]
|
||||
#[impl_cast::device_trait]
|
||||
pub trait Scene {
|
||||
pub trait Scene: Sync + Send {
|
||||
fn is_scene_reversible(&self) -> Option<bool> {
|
||||
None
|
||||
}
|
||||
|
@ -60,8 +58,7 @@ pub struct AvailableSpeeds {
|
|||
}
|
||||
|
||||
#[async_trait]
|
||||
#[impl_cast::device_trait]
|
||||
pub trait FanSpeed {
|
||||
pub trait FanSpeed: Sync + Send {
|
||||
fn reversible(&self) -> Option<bool> {
|
||||
None
|
||||
}
|
||||
|
@ -76,8 +73,7 @@ pub trait FanSpeed {
|
|||
}
|
||||
|
||||
#[async_trait]
|
||||
#[impl_cast::device_trait]
|
||||
pub trait HumiditySetting {
|
||||
pub trait HumiditySetting: Sync + Send {
|
||||
// TODO: This implementation is not complete, I have only implemented what I need right now
|
||||
fn query_only_humidity_setting(&self) -> Option<bool> {
|
||||
None
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
[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
|
|
@ -1,165 +0,0 @@
|
|||
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()
|
||||
}
|
|
@ -8,7 +8,7 @@ use tokio::sync::{RwLock, RwLockReadGuard};
|
|||
use tokio_cron_scheduler::{Job, JobScheduler};
|
||||
use tracing::{debug, instrument, trace};
|
||||
|
||||
use crate::devices::{As, Device};
|
||||
use crate::devices::Device;
|
||||
use crate::event::{Event, EventChannel, OnDarkness, OnMqtt, OnNotification, OnPresence};
|
||||
use crate::LUA;
|
||||
|
||||
|
@ -80,7 +80,7 @@ impl DeviceManager {
|
|||
}
|
||||
|
||||
pub async fn add(&self, device: &WrappedDevice) {
|
||||
let id = device.read().await.get_id();
|
||||
let id = device.read().await.get_id().to_owned();
|
||||
|
||||
debug!(id, "Adding device");
|
||||
|
||||
|
@ -113,8 +113,8 @@ impl DeviceManager {
|
|||
let message = message.clone();
|
||||
async move {
|
||||
let mut device = device.write().await;
|
||||
let device = device.as_mut();
|
||||
if let Some(device) = As::<dyn OnMqtt>::cast_mut(device) {
|
||||
let device: Option<&mut dyn OnMqtt> = device.as_mut().cast_mut();
|
||||
if let Some(device) = device {
|
||||
// let subscribed = device
|
||||
// .topics()
|
||||
// .iter()
|
||||
|
@ -135,8 +135,8 @@ impl DeviceManager {
|
|||
let devices = self.devices.read().await;
|
||||
let iter = devices.iter().map(|(id, device)| async move {
|
||||
let mut device = device.write().await;
|
||||
let device = device.as_mut();
|
||||
if let Some(device) = As::<dyn OnDarkness>::cast_mut(device) {
|
||||
let device: Option<&mut dyn OnDarkness> = device.as_mut().cast_mut();
|
||||
if let Some(device) = device {
|
||||
trace!(id, "Handling");
|
||||
device.on_darkness(dark).await;
|
||||
trace!(id, "Done");
|
||||
|
@ -149,8 +149,8 @@ impl DeviceManager {
|
|||
let devices = self.devices.read().await;
|
||||
let iter = devices.iter().map(|(id, device)| async move {
|
||||
let mut device = device.write().await;
|
||||
let device = device.as_mut();
|
||||
if let Some(device) = As::<dyn OnPresence>::cast_mut(device) {
|
||||
let device: Option<&mut dyn OnPresence> = device.as_mut().cast_mut();
|
||||
if let Some(device) = device {
|
||||
trace!(id, "Handling");
|
||||
device.on_presence(presence).await;
|
||||
trace!(id, "Done");
|
||||
|
@ -165,8 +165,8 @@ impl DeviceManager {
|
|||
let notification = notification.clone();
|
||||
async move {
|
||||
let mut device = device.write().await;
|
||||
let device = device.as_mut();
|
||||
if let Some(device) = As::<dyn OnNotification>::cast_mut(device) {
|
||||
let device: Option<&mut dyn OnNotification> = device.as_mut().cast_mut();
|
||||
if let Some(device) = device {
|
||||
trace!(id, "Handling");
|
||||
device.on_notification(notification).await;
|
||||
trace!(id, "Done");
|
||||
|
|
|
@ -6,7 +6,6 @@ use tracing::{debug, error, trace, warn};
|
|||
use super::{Device, LuaDeviceCreate};
|
||||
use crate::config::MqttDeviceConfig;
|
||||
use crate::device_manager::WrappedDevice;
|
||||
use crate::devices::As;
|
||||
use crate::error::DeviceConfigError;
|
||||
use crate::event::{OnMqtt, OnPresence};
|
||||
use crate::messages::{RemoteAction, RemoteMessage};
|
||||
|
@ -38,15 +37,19 @@ impl LuaDeviceCreate for AudioSetup {
|
|||
async fn create(config: Self::Config) -> Result<Self, Self::Error> {
|
||||
trace!(id = config.identifier, "Setting up AudioSetup");
|
||||
|
||||
let mixer_id = config.mixer.read().await.get_id().to_owned();
|
||||
if !As::<dyn OnOff>::is(config.mixer.read().await.as_ref()) {
|
||||
{
|
||||
let mixer = config.mixer.read().await;
|
||||
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()));
|
||||
}
|
||||
|
||||
let speakers_id = config.speakers.read().await.get_id().to_owned();
|
||||
if !As::<dyn OnOff>::is(config.speakers.read().await.as_ref()) {
|
||||
let speakers = config.speakers.read().await;
|
||||
let speakers_id = speakers.get_id().to_owned();
|
||||
if (speakers.as_ref().cast() as Option<&dyn OnOff>).is_none() {
|
||||
return Err(DeviceConfigError::MissingTrait(speakers_id, "OnOff".into()));
|
||||
}
|
||||
}
|
||||
|
||||
config
|
||||
.client
|
||||
|
@ -84,8 +87,8 @@ impl OnMqtt for AudioSetup {
|
|||
let mut mixer = self.config.mixer.write().await;
|
||||
let mut speakers = self.config.speakers.write().await;
|
||||
if let (Some(mixer), Some(speakers)) = (
|
||||
As::<dyn OnOff>::cast_mut(mixer.as_mut()),
|
||||
As::<dyn OnOff>::cast_mut(speakers.as_mut()),
|
||||
mixer.as_mut().cast_mut() as Option<&mut dyn OnOff>,
|
||||
speakers.as_mut().cast_mut() as Option<&mut dyn OnOff>,
|
||||
) {
|
||||
match action {
|
||||
RemoteAction::On => {
|
||||
|
@ -120,8 +123,8 @@ impl OnPresence for AudioSetup {
|
|||
let mut speakers = self.config.speakers.write().await;
|
||||
|
||||
if let (Some(mixer), Some(speakers)) = (
|
||||
As::<dyn OnOff>::cast_mut(mixer.as_mut()),
|
||||
As::<dyn OnOff>::cast_mut(speakers.as_mut()),
|
||||
mixer.as_mut().cast_mut() as Option<&mut dyn OnOff>,
|
||||
speakers.as_mut().cast_mut() as Option<&mut dyn OnOff>,
|
||||
) {
|
||||
// Turn off the audio setup when we leave the house
|
||||
if !presence {
|
||||
|
|
|
@ -10,7 +10,7 @@ use tracing::{debug, error, trace, warn};
|
|||
use super::{Device, LuaDeviceCreate};
|
||||
use crate::config::MqttDeviceConfig;
|
||||
use crate::device_manager::WrappedDevice;
|
||||
use crate::devices::{As, DEFAULT_PRESENCE};
|
||||
use crate::devices::DEFAULT_PRESENCE;
|
||||
use crate::error::DeviceConfigError;
|
||||
use crate::event::{OnMqtt, OnPresence};
|
||||
use crate::messages::{ContactMessage, PresenceMessage};
|
||||
|
@ -82,17 +82,21 @@ impl LuaDeviceCreate for ContactSensor {
|
|||
// Make sure the devices implement the required traits
|
||||
if let Some(trigger) = &config.trigger {
|
||||
for (device, _) in &trigger.devices {
|
||||
let id = device.read().await.get_id().to_owned();
|
||||
if !As::<dyn OnOff>::is(device.read().await.as_ref()) {
|
||||
{
|
||||
let device = device.read().await;
|
||||
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()));
|
||||
}
|
||||
|
||||
if trigger.timeout.is_none() && !As::<dyn Timeout>::is(device.read().await.as_ref())
|
||||
if trigger.timeout.is_none()
|
||||
&& (device.as_ref().cast() as Option<&dyn Timeout>).is_none()
|
||||
{
|
||||
return Err(DeviceConfigError::MissingTrait(id, "Timeout".into()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
config
|
||||
.client
|
||||
|
@ -150,7 +154,7 @@ impl OnMqtt for ContactSensor {
|
|||
if !self.is_closed {
|
||||
for (light, previous) in &mut trigger.devices {
|
||||
let mut light = light.write().await;
|
||||
if let Some(light) = As::<dyn OnOff>::cast_mut(light.as_mut()) {
|
||||
if let Some(light) = light.as_mut().cast_mut() as Option<&mut dyn OnOff> {
|
||||
*previous = light.is_on().await.unwrap();
|
||||
light.set_on(true).await.ok();
|
||||
}
|
||||
|
@ -161,11 +165,12 @@ impl OnMqtt for ContactSensor {
|
|||
if !previous {
|
||||
// If the timeout is zero just turn the light off directly
|
||||
if trigger.timeout.is_none()
|
||||
&& let Some(light) = As::<dyn OnOff>::cast_mut(light.as_mut())
|
||||
&& let Some(light) = light.as_mut().cast_mut() as Option<&mut dyn OnOff>
|
||||
{
|
||||
light.set_on(false).await.ok();
|
||||
} else if let Some(timeout) = trigger.timeout
|
||||
&& let Some(light) = As::<dyn Timeout>::cast_mut(light.as_mut())
|
||||
&& let Some(light) =
|
||||
light.as_mut().cast_mut() as Option<&mut dyn Timeout>
|
||||
{
|
||||
light.start_timeout(timeout).await.unwrap();
|
||||
}
|
||||
|
|
|
@ -12,9 +12,12 @@ mod presence;
|
|||
mod wake_on_lan;
|
||||
mod washer;
|
||||
|
||||
use std::fmt::Debug;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use google_home::device::AsGoogleHomeDevice;
|
||||
use automation_cast::Cast;
|
||||
use google_home::traits::OnOff;
|
||||
use google_home::GoogleHomeDevice;
|
||||
|
||||
pub use self::air_filter::*;
|
||||
pub use self::audio_setup::*;
|
||||
|
@ -60,7 +63,18 @@ pub fn register_with_lua(lua: &mlua::Lua) -> mlua::Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[impl_cast::device(As: OnMqtt + OnPresence + OnDarkness + OnNotification + OnOff + Timeout)]
|
||||
pub trait Device: AsGoogleHomeDevice + std::fmt::Debug + Sync + Send {
|
||||
pub trait Device:
|
||||
Debug
|
||||
+ 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;
|
||||
}
|
||||
|
|
13
src/event.rs
13
src/event.rs
|
@ -1,5 +1,4 @@
|
|||
use async_trait::async_trait;
|
||||
use impl_cast::device_trait;
|
||||
use mlua::FromLua;
|
||||
use rumqttc::Publish;
|
||||
use tokio::sync::mpsc;
|
||||
|
@ -35,26 +34,22 @@ impl EventChannel {
|
|||
impl mlua::UserData for EventChannel {}
|
||||
|
||||
#[async_trait]
|
||||
#[device_trait]
|
||||
pub trait OnMqtt {
|
||||
pub trait OnMqtt: Sync + Send {
|
||||
// fn topics(&self) -> Vec<&str>;
|
||||
async fn on_mqtt(&mut self, message: Publish);
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
#[device_trait]
|
||||
pub trait OnPresence {
|
||||
pub trait OnPresence: Sync + Send {
|
||||
async fn on_presence(&mut self, presence: bool);
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
#[device_trait]
|
||||
pub trait OnDarkness {
|
||||
pub trait OnDarkness: Sync + Send {
|
||||
async fn on_darkness(&mut self, dark: bool);
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
#[device_trait]
|
||||
pub trait OnNotification {
|
||||
pub trait OnNotification: Sync + Send {
|
||||
async fn on_notification(&mut self, notification: Notification);
|
||||
}
|
||||
|
|
|
@ -47,7 +47,8 @@ async fn main() {
|
|||
async fn app() -> anyhow::Result<()> {
|
||||
dotenv().ok();
|
||||
|
||||
console_subscriber::init();
|
||||
tracing_subscriber::fmt::init();
|
||||
// console_subscriber::init();
|
||||
|
||||
info!("Starting automation_rs...");
|
||||
|
||||
|
@ -83,6 +84,12 @@ async fn app() -> anyhow::Result<()> {
|
|||
std::env::var(name).map_err(mlua::ExternalError::into_lua_err)
|
||||
})?;
|
||||
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)?;
|
||||
|
||||
lua.globals().set("automation", automation)?;
|
||||
|
|
|
@ -2,11 +2,9 @@ use std::time::Duration;
|
|||
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use impl_cast::device_trait;
|
||||
|
||||
#[async_trait]
|
||||
#[device_trait]
|
||||
pub trait Timeout {
|
||||
pub trait Timeout: Sync + Send {
|
||||
async fn start_timeout(&mut self, _timeout: Duration) -> Result<()>;
|
||||
async fn stop_timeout(&mut self) -> Result<()>;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user