Compare commits
7 Commits
006320be18
...
88e31699ad
Author | SHA1 | Date | |
---|---|---|---|
88e31699ad | |||
23e78fe5a7 | |||
14e14ca479 | |||
3fd8dddeb2 | |||
6c797820dc | |||
2cf4e40ad5 | |||
98ab265fed |
|
@ -1,31 +0,0 @@
|
|||
name: Check
|
||||
on:
|
||||
push:
|
||||
branches: "**"
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Run checks
|
||||
runs-on: ubuntu-latest
|
||||
container: git.huizinga.dev/dreaded_x/pre-commit:master
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- uses: https://gitea.com/actions/go-hashfiles@v0.0.1
|
||||
id: get-hash
|
||||
with:
|
||||
patterns: |-
|
||||
.pre-commit-config.yaml
|
||||
|
||||
- name: set PY
|
||||
run: echo "PY=$(python -VV | sha256sum | cut -d ' ' -f1)" >> $GITHUB_ENV
|
||||
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pre-commit
|
||||
key: pre-commit|${{ env.PY }}|${{ steps.get-hash.outputs.hash }}
|
||||
|
||||
- name: Run pre-commit
|
||||
run: SKIP=sqlx-prepare pre-commit run --show-diff-on-failure --color=always --all-files
|
||||
shell: bash
|
7
Cargo.lock
generated
7
Cargo.lock
generated
|
@ -100,6 +100,7 @@ dependencies = [
|
|||
"bytes",
|
||||
"console-subscriber",
|
||||
"dotenvy",
|
||||
"dyn-clone",
|
||||
"enum_dispatch",
|
||||
"eui48",
|
||||
"futures",
|
||||
|
@ -428,6 +429,12 @@ version = "0.15.7"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
|
||||
|
||||
[[package]]
|
||||
name = "dyn-clone"
|
||||
version = "1.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125"
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.9.0"
|
||||
|
|
|
@ -55,6 +55,7 @@ once_cell = "1.19.0"
|
|||
hostname = "0.4.0"
|
||||
tokio-util = { version = "0.7.11", features = ["full"] }
|
||||
uuid = "1.8.0"
|
||||
dyn-clone = "1.0.17"
|
||||
|
||||
[patch.crates-io]
|
||||
wakey = { git = "https://git.huizinga.dev/Dreaded_X/wakey" }
|
||||
|
|
|
@ -6,7 +6,6 @@ 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
|
||||
|
@ -16,10 +15,6 @@ where
|
|||
default fn cast(&self) -> Option<&P> {
|
||||
None
|
||||
}
|
||||
|
||||
default fn cast_mut(&mut self) -> Option<&mut P> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl<D, P> Cast<P> for D
|
||||
|
@ -30,8 +25,4 @@ where
|
|||
fn cast(&self) -> Option<&P> {
|
||||
Some(self)
|
||||
}
|
||||
|
||||
fn cast_mut(&mut self) -> Option<&mut P> {
|
||||
Some(self)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,19 +1,10 @@
|
|||
#![feature(let_chains)]
|
||||
#![feature(iter_intersperse)]
|
||||
mod lua_device;
|
||||
mod lua_device_config;
|
||||
|
||||
use lua_device::impl_lua_device_macro;
|
||||
use lua_device_config::impl_lua_device_config_macro;
|
||||
use syn::{parse_macro_input, DeriveInput};
|
||||
|
||||
#[proc_macro_derive(LuaDevice, attributes(config))]
|
||||
pub fn lua_device_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
let ast = parse_macro_input!(input as DeriveInput);
|
||||
|
||||
impl_lua_device_macro(&ast).into()
|
||||
}
|
||||
|
||||
#[proc_macro_derive(LuaDeviceConfig, attributes(device_config))]
|
||||
pub fn lua_device_config_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
let ast = parse_macro_input!(input as DeriveInput);
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
use proc_macro2::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::DeriveInput;
|
||||
|
||||
pub fn impl_lua_device_macro(ast: &DeriveInput) -> TokenStream {
|
||||
let name = &ast.ident;
|
||||
let gen = quote! {
|
||||
impl #name {
|
||||
pub fn register_with_lua(lua: &mlua::Lua) -> mlua::Result<()> {
|
||||
lua.globals().set(stringify!(#name), lua.create_proxy::<#name>()?)
|
||||
}
|
||||
}
|
||||
impl mlua::UserData for #name {
|
||||
fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||
methods.add_async_function("new", |lua, config: mlua::Value| async {
|
||||
let config = mlua::FromLua::from_lua(config, lua)?;
|
||||
|
||||
// TODO: Using crate:: could cause issues
|
||||
let device: #name = crate::devices::LuaDeviceCreate::create(config).await.map_err(mlua::ExternalError::into_lua_err)?;
|
||||
|
||||
Ok(crate::device_manager::WrappedDevice::new(Box::new(device)))
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
gen
|
||||
}
|
|
@ -56,7 +56,7 @@ pub trait Device: DeviceFulfillment {
|
|||
device
|
||||
}
|
||||
|
||||
async fn execute(&mut self, command: Command) -> Result<(), ErrorCode> {
|
||||
async fn execute(&self, command: Command) -> Result<(), ErrorCode> {
|
||||
DeviceFulfillment::execute(self, command.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
|
|
|
@ -4,7 +4,7 @@ use std::sync::Arc;
|
|||
use automation_cast::Cast;
|
||||
use futures::future::{join_all, OptionFuture};
|
||||
use thiserror::Error;
|
||||
use tokio::sync::{Mutex, RwLock};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::errors::{DeviceError, ErrorCode};
|
||||
use crate::request::{self, Intent, Request};
|
||||
|
@ -33,7 +33,7 @@ impl GoogleHome {
|
|||
pub async fn handle_request<T: Cast<dyn Device> + ?Sized + 'static>(
|
||||
&self,
|
||||
request: Request,
|
||||
devices: &HashMap<String, Arc<RwLock<Box<T>>>>,
|
||||
devices: &HashMap<String, Box<T>>,
|
||||
) -> Result<Response, FulfillmentError> {
|
||||
// TODO: What do we do if we actually get more then one thing in the input array, right now
|
||||
// we only respond to the first thing
|
||||
|
@ -61,11 +61,11 @@ impl GoogleHome {
|
|||
|
||||
async fn sync<T: Cast<dyn Device> + ?Sized + 'static>(
|
||||
&self,
|
||||
devices: &HashMap<String, Arc<RwLock<Box<T>>>>,
|
||||
devices: &HashMap<String, Box<T>>,
|
||||
) -> sync::Payload {
|
||||
let mut resp_payload = sync::Payload::new(&self.user_id);
|
||||
let f = devices.iter().map(|(_, device)| async move {
|
||||
if let Some(device) = device.read().await.as_ref().cast() {
|
||||
if let Some(device) = device.as_ref().cast() {
|
||||
Some(Device::sync(device).await)
|
||||
} else {
|
||||
None
|
||||
|
@ -79,7 +79,7 @@ impl GoogleHome {
|
|||
async fn query<T: Cast<dyn Device> + ?Sized + 'static>(
|
||||
&self,
|
||||
payload: request::query::Payload,
|
||||
devices: &HashMap<String, Arc<RwLock<Box<T>>>>,
|
||||
devices: &HashMap<String, Box<T>>,
|
||||
) -> query::Payload {
|
||||
let mut resp_payload = query::Payload::new();
|
||||
let f = payload
|
||||
|
@ -89,7 +89,7 @@ impl GoogleHome {
|
|||
.map(|id| async move {
|
||||
// NOTE: Requires let_chains feature
|
||||
let device = if let Some(device) = devices.get(id.as_str())
|
||||
&& let Some(device) = device.read().await.as_ref().cast()
|
||||
&& let Some(device) = device.as_ref().cast()
|
||||
{
|
||||
Device::query(device).await
|
||||
} else {
|
||||
|
@ -111,7 +111,7 @@ impl GoogleHome {
|
|||
async fn execute<T: Cast<dyn Device> + ?Sized + 'static>(
|
||||
&self,
|
||||
payload: request::execute::Payload,
|
||||
devices: &HashMap<String, Arc<RwLock<Box<T>>>>,
|
||||
devices: &HashMap<String, Box<T>>,
|
||||
) -> execute::Payload {
|
||||
let resp_payload = Arc::new(Mutex::new(response::execute::Payload::new()));
|
||||
|
||||
|
@ -138,7 +138,7 @@ impl GoogleHome {
|
|||
let execution = command.execution.clone();
|
||||
async move {
|
||||
if let Some(device) = devices.get(id.as_str())
|
||||
&& let Some(device) = device.write().await.as_mut().cast_mut()
|
||||
&& let Some(device) = device.as_ref().cast()
|
||||
{
|
||||
if !device.is_online() {
|
||||
return (id, Ok(false));
|
||||
|
|
|
@ -12,35 +12,35 @@ traits! {
|
|||
command_only_on_off: Option<bool>,
|
||||
query_only_on_off: Option<bool>,
|
||||
async fn on(&self) -> Result<bool, ErrorCode>,
|
||||
"action.devices.commands.OnOff" => async fn set_on(&mut self, on: bool) -> Result<(), ErrorCode>,
|
||||
"action.devices.commands.OnOff" => async fn set_on(&self, on: bool) -> Result<(), ErrorCode>,
|
||||
},
|
||||
"action.devices.traits.Scene" => trait Scene {
|
||||
scene_reversible: Option<bool>,
|
||||
|
||||
"action.devices.commands.ActivateScene" => async fn set_active(&mut self, deactivate: bool) -> Result<(), ErrorCode>,
|
||||
"action.devices.commands.ActivateScene" => async fn set_active(&self, deactivate: bool) -> Result<(), ErrorCode>,
|
||||
},
|
||||
"action.devices.traits.FanSpeed" => trait FanSpeed {
|
||||
reversible: Option<bool>,
|
||||
command_only_fan_speed: Option<bool>,
|
||||
available_fan_speeds: AvailableSpeeds,
|
||||
|
||||
fn current_fan_speed_setting(&self) -> Result<String, ErrorCode>,
|
||||
async fn current_fan_speed_setting(&self) -> Result<String, ErrorCode>,
|
||||
|
||||
// TODO: Figure out some syntax for optional command?
|
||||
// Probably better to just force the user to always implement commands?
|
||||
"action.devices.commands.SetFanSpeed" => async fn set_fan_speed(&mut self, fan_speed: String) -> Result<(), ErrorCode>,
|
||||
"action.devices.commands.SetFanSpeed" => async fn set_fan_speed(&self, fan_speed: String) -> Result<(), ErrorCode>,
|
||||
},
|
||||
"action.devices.traits.HumiditySetting" => trait HumiditySetting {
|
||||
query_only_humidity_setting: Option<bool>,
|
||||
|
||||
fn humidity_ambient_percent(&self) -> Result<isize, ErrorCode>,
|
||||
async fn humidity_ambient_percent(&self) -> Result<isize, ErrorCode>,
|
||||
},
|
||||
"action.devices.traits.TemperatureControl" => trait TemperatureSetting {
|
||||
query_only_temperature_control: Option<bool>,
|
||||
// TODO: Add rename
|
||||
temperatureUnitForUX: TemperatureUnit,
|
||||
|
||||
fn temperature_ambient_celsius(&self) -> f32,
|
||||
async fn temperature_ambient_celsius(&self) -> f32,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -501,7 +501,7 @@ pub fn traits(item: TokenStream) -> TokenStream {
|
|||
|
||||
Some(quote! {
|
||||
Command::#command_name {#(#parameters,)*} => {
|
||||
if let Some(t) = self.cast_mut() as Option<&mut dyn #ident> {
|
||||
if let Some(t) = self.cast() as Option<&dyn #ident> {
|
||||
t.#f_name(#(#parameters,)*) #asyncness #errors;
|
||||
serde_json::to_value(t.get_state().await?)?
|
||||
} else {
|
||||
|
@ -528,7 +528,7 @@ pub fn traits(item: TokenStream) -> TokenStream {
|
|||
pub trait #fulfillment: Sync + Send {
|
||||
async fn sync(&self) -> Result<(Vec<Trait>, serde_json::Value), Box<dyn ::std::error::Error>>;
|
||||
async fn query(&self) -> Result<serde_json::Value, Box<dyn ::std::error::Error>>;
|
||||
async fn execute(&mut self, command: Command) -> Result<serde_json::Value, Box<dyn std::error::Error>>;
|
||||
async fn execute(&self, command: Command) -> Result<serde_json::Value, Box<dyn std::error::Error>>;
|
||||
}
|
||||
|
||||
#(#structs)*
|
||||
|
@ -556,7 +556,7 @@ pub fn traits(item: TokenStream) -> TokenStream {
|
|||
Ok(state)
|
||||
}
|
||||
|
||||
async fn execute(&mut self, command: Command) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
|
||||
async fn execute(&self, command: Command) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
|
||||
let value = match command {
|
||||
#(#execute)*
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
[toolchain]
|
||||
channel = "nightly-2023-11-15"
|
||||
components = ["rustfmt", "clippy"]
|
||||
channel = "nightly-2024-07-25"
|
||||
components = ["rustfmt", "clippy", "rust-analyzer"]
|
||||
profile = "minimal"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use std::collections::HashMap;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::ops::Deref;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
|
||||
|
@ -17,38 +17,28 @@ use crate::event::{Event, EventChannel, OnDarkness, OnMqtt, OnNotification, OnPr
|
|||
use crate::LUA;
|
||||
|
||||
#[derive(Debug, FromLua, Clone)]
|
||||
pub struct WrappedDevice(Arc<RwLock<Box<dyn Device>>>);
|
||||
pub struct WrappedDevice(Box<dyn Device>);
|
||||
|
||||
impl WrappedDevice {
|
||||
pub fn new(device: Box<dyn Device>) -> Self {
|
||||
Self(Arc::new(RwLock::new(device)))
|
||||
pub fn new(device: impl Device + 'static) -> Self {
|
||||
Self(Box::new(device))
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for WrappedDevice {
|
||||
type Target = Arc<RwLock<Box<dyn Device>>>;
|
||||
type Target = Box<dyn Device>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for WrappedDevice {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
impl mlua::UserData for WrappedDevice {
|
||||
fn add_methods<'lua, M: mlua::prelude::LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||
methods.add_async_method("get_id", |_lua, this, _: ()| async {
|
||||
Ok(crate::devices::Device::get_id(this.0.read().await.as_ref()))
|
||||
});
|
||||
methods.add_async_method("get_id", |_lua, this, _: ()| async { Ok(this.get_id()) });
|
||||
|
||||
methods.add_async_method("set_on", |_lua, this, on: bool| async move {
|
||||
let mut device = this.0.write().await;
|
||||
let device = device.as_mut();
|
||||
|
||||
if let Some(device) = device.cast_mut() as Option<&mut dyn OnOff> {
|
||||
if let Some(device) = this.cast() as Option<&dyn OnOff> {
|
||||
device.set_on(on).await.unwrap()
|
||||
};
|
||||
|
||||
|
@ -57,7 +47,7 @@ impl mlua::UserData for WrappedDevice {
|
|||
}
|
||||
}
|
||||
|
||||
pub type DeviceMap = HashMap<String, Arc<RwLock<Box<dyn Device>>>>;
|
||||
pub type DeviceMap = HashMap<String, Box<dyn Device>>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DeviceManager {
|
||||
|
@ -94,25 +84,20 @@ impl DeviceManager {
|
|||
device_manager
|
||||
}
|
||||
|
||||
pub async fn add(&self, device: &WrappedDevice) {
|
||||
let id = device.read().await.get_id().to_owned();
|
||||
pub async fn add(&self, device: Box<dyn Device>) {
|
||||
let id = device.get_id();
|
||||
|
||||
debug!(id, "Adding device");
|
||||
|
||||
self.devices.write().await.insert(id, device.0.clone());
|
||||
self.devices.write().await.insert(id, device);
|
||||
}
|
||||
|
||||
pub fn event_channel(&self) -> EventChannel {
|
||||
self.event_channel.clone()
|
||||
}
|
||||
|
||||
pub async fn get(&self, name: &str) -> Option<WrappedDevice> {
|
||||
self.devices
|
||||
.read()
|
||||
.await
|
||||
.get(name)
|
||||
.cloned()
|
||||
.map(WrappedDevice)
|
||||
pub async fn get(&self, name: &str) -> Option<Box<dyn Device>> {
|
||||
self.devices.read().await.get(name).cloned()
|
||||
}
|
||||
|
||||
pub async fn devices(&self) -> RwLockReadGuard<DeviceMap> {
|
||||
|
@ -127,8 +112,7 @@ impl DeviceManager {
|
|||
let iter = devices.iter().map(|(id, device)| {
|
||||
let message = message.clone();
|
||||
async move {
|
||||
let mut device = device.write().await;
|
||||
let device: Option<&mut dyn OnMqtt> = device.as_mut().cast_mut();
|
||||
let device: Option<&dyn OnMqtt> = device.cast();
|
||||
if let Some(device) = device {
|
||||
// let subscribed = device
|
||||
// .topics()
|
||||
|
@ -149,8 +133,7 @@ impl DeviceManager {
|
|||
Event::Darkness(dark) => {
|
||||
let devices = self.devices.read().await;
|
||||
let iter = devices.iter().map(|(id, device)| async move {
|
||||
let mut device = device.write().await;
|
||||
let device: Option<&mut dyn OnDarkness> = device.as_mut().cast_mut();
|
||||
let device: Option<&dyn OnDarkness> = device.cast();
|
||||
if let Some(device) = device {
|
||||
trace!(id, "Handling");
|
||||
device.on_darkness(dark).await;
|
||||
|
@ -163,8 +146,7 @@ impl DeviceManager {
|
|||
Event::Presence(presence) => {
|
||||
let devices = self.devices.read().await;
|
||||
let iter = devices.iter().map(|(id, device)| async move {
|
||||
let mut device = device.write().await;
|
||||
let device: Option<&mut dyn OnPresence> = device.as_mut().cast_mut();
|
||||
let device: Option<&dyn OnPresence> = device.cast();
|
||||
if let Some(device) = device {
|
||||
trace!(id, "Handling");
|
||||
device.on_presence(presence).await;
|
||||
|
@ -179,8 +161,7 @@ impl DeviceManager {
|
|||
let iter = devices.iter().map(|(id, device)| {
|
||||
let notification = notification.clone();
|
||||
async move {
|
||||
let mut device = device.write().await;
|
||||
let device: Option<&mut dyn OnNotification> = device.as_mut().cast_mut();
|
||||
let device: Option<&dyn OnNotification> = device.cast();
|
||||
if let Some(device) = device {
|
||||
trace!(id, "Handling");
|
||||
device.on_notification(notification).await;
|
||||
|
@ -215,7 +196,7 @@ fn run_schedule(
|
|||
impl mlua::UserData for DeviceManager {
|
||||
fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||
methods.add_async_method("add", |_lua, this, device: WrappedDevice| async move {
|
||||
this.add(&device).await;
|
||||
this.add(device.0).await;
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use automation_macro::{LuaDevice, LuaDeviceConfig};
|
||||
use automation_macro::LuaDeviceConfig;
|
||||
use google_home::device::Name;
|
||||
use google_home::errors::ErrorCode;
|
||||
use google_home::traits::{
|
||||
|
@ -8,6 +10,7 @@ use google_home::traits::{
|
|||
};
|
||||
use google_home::types::Type;
|
||||
use rumqttc::Publish;
|
||||
use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||
use tracing::{debug, error, trace, warn};
|
||||
|
||||
use super::LuaDeviceCreate;
|
||||
|
@ -18,7 +21,7 @@ use crate::messages::{AirFilterFanState, AirFilterState, SetAirFilterFanState};
|
|||
use crate::mqtt::WrappedAsyncClient;
|
||||
|
||||
#[derive(Debug, Clone, LuaDeviceConfig)]
|
||||
pub struct AirFilterConfig {
|
||||
pub struct Config {
|
||||
#[device_config(flatten)]
|
||||
pub info: InfoConfig,
|
||||
#[device_config(flatten)]
|
||||
|
@ -27,11 +30,10 @@ pub struct AirFilterConfig {
|
|||
pub client: WrappedAsyncClient,
|
||||
}
|
||||
|
||||
#[derive(Debug, LuaDevice)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AirFilter {
|
||||
config: AirFilterConfig,
|
||||
|
||||
last_known_state: AirFilterState,
|
||||
config: Config,
|
||||
state: Arc<RwLock<AirFilterState>>,
|
||||
}
|
||||
|
||||
impl AirFilter {
|
||||
|
@ -52,11 +54,19 @@ impl AirFilter {
|
|||
.map_err(|err| warn!("Failed to update state on {topic}: {err}"))
|
||||
.ok();
|
||||
}
|
||||
|
||||
async fn state(&self) -> RwLockReadGuard<AirFilterState> {
|
||||
self.state.read().await
|
||||
}
|
||||
|
||||
async fn state_mut(&self) -> RwLockWriteGuard<AirFilterState> {
|
||||
self.state.write().await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl LuaDeviceCreate for AirFilter {
|
||||
type Config = AirFilterConfig;
|
||||
type Config = Config;
|
||||
type Error = rumqttc::ClientError;
|
||||
|
||||
async fn create(config: Self::Config) -> Result<Self, Self::Error> {
|
||||
|
@ -67,14 +77,14 @@ impl LuaDeviceCreate for AirFilter {
|
|||
.subscribe(&config.mqtt.topic, rumqttc::QoS::AtLeastOnce)
|
||||
.await?;
|
||||
|
||||
Ok(Self {
|
||||
config,
|
||||
last_known_state: AirFilterState {
|
||||
let state = AirFilterState {
|
||||
state: AirFilterFanState::Off,
|
||||
humidity: 0.0,
|
||||
temperature: 0.0,
|
||||
},
|
||||
})
|
||||
};
|
||||
let state = Arc::new(RwLock::new(state));
|
||||
|
||||
Ok(Self { config, state })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -86,7 +96,7 @@ impl Device for AirFilter {
|
|||
|
||||
#[async_trait]
|
||||
impl OnMqtt for AirFilter {
|
||||
async fn on_mqtt(&mut self, message: Publish) {
|
||||
async fn on_mqtt(&self, message: Publish) {
|
||||
if !rumqttc::matches(&message.topic, &self.config.mqtt.topic) {
|
||||
return;
|
||||
}
|
||||
|
@ -99,13 +109,13 @@ impl OnMqtt for AirFilter {
|
|||
}
|
||||
};
|
||||
|
||||
if state == self.last_known_state {
|
||||
if state == *self.state().await {
|
||||
return;
|
||||
}
|
||||
|
||||
debug!(id = Device::get_id(self), "Updating state to {state:?}");
|
||||
|
||||
self.last_known_state = state;
|
||||
*self.state_mut().await = state;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -138,10 +148,10 @@ impl google_home::Device for AirFilter {
|
|||
#[async_trait]
|
||||
impl OnOff for AirFilter {
|
||||
async fn on(&self) -> Result<bool, ErrorCode> {
|
||||
Ok(self.last_known_state.state != AirFilterFanState::Off)
|
||||
Ok(self.state().await.state != AirFilterFanState::Off)
|
||||
}
|
||||
|
||||
async fn set_on(&mut self, on: bool) -> Result<(), ErrorCode> {
|
||||
async fn set_on(&self, on: bool) -> Result<(), ErrorCode> {
|
||||
debug!("Turning on air filter: {on}");
|
||||
|
||||
if on {
|
||||
|
@ -192,8 +202,8 @@ impl FanSpeed for AirFilter {
|
|||
}
|
||||
}
|
||||
|
||||
fn current_fan_speed_setting(&self) -> Result<String, ErrorCode> {
|
||||
let speed = match self.last_known_state.state {
|
||||
async fn current_fan_speed_setting(&self) -> Result<String, ErrorCode> {
|
||||
let speed = match self.state().await.state {
|
||||
AirFilterFanState::Off => "off",
|
||||
AirFilterFanState::Low => "low",
|
||||
AirFilterFanState::Medium => "medium",
|
||||
|
@ -203,7 +213,7 @@ impl FanSpeed for AirFilter {
|
|||
Ok(speed.into())
|
||||
}
|
||||
|
||||
async fn set_fan_speed(&mut self, fan_speed: String) -> Result<(), ErrorCode> {
|
||||
async fn set_fan_speed(&self, fan_speed: String) -> Result<(), ErrorCode> {
|
||||
let fan_speed = fan_speed.as_str();
|
||||
let state = if fan_speed == "off" {
|
||||
AirFilterFanState::Off
|
||||
|
@ -229,11 +239,12 @@ impl HumiditySetting for AirFilter {
|
|||
Some(true)
|
||||
}
|
||||
|
||||
fn humidity_ambient_percent(&self) -> Result<isize, ErrorCode> {
|
||||
Ok(self.last_known_state.humidity.round() as isize)
|
||||
async fn humidity_ambient_percent(&self) -> Result<isize, ErrorCode> {
|
||||
Ok(self.state().await.humidity.round() as isize)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl TemperatureSetting for AirFilter {
|
||||
fn query_only_temperature_control(&self) -> Option<bool> {
|
||||
Some(true)
|
||||
|
@ -244,8 +255,8 @@ impl TemperatureSetting for AirFilter {
|
|||
TemperatureUnit::Celsius
|
||||
}
|
||||
|
||||
fn temperature_ambient_celsius(&self) -> f32 {
|
||||
async fn temperature_ambient_celsius(&self) -> f32 {
|
||||
// HACK: Round to one decimal place
|
||||
(10.0 * self.last_known_state.temperature).round() / 10.0
|
||||
(10.0 * self.state().await.temperature).round() / 10.0
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use async_trait::async_trait;
|
||||
use automation_macro::{LuaDevice, LuaDeviceConfig};
|
||||
use automation_macro::LuaDeviceConfig;
|
||||
use google_home::traits::OnOff;
|
||||
use tracing::{debug, error, trace, warn};
|
||||
|
||||
|
@ -12,7 +12,7 @@ use crate::messages::{RemoteAction, RemoteMessage};
|
|||
use crate::mqtt::WrappedAsyncClient;
|
||||
|
||||
#[derive(Debug, Clone, LuaDeviceConfig)]
|
||||
pub struct AudioSetupConfig {
|
||||
pub struct Config {
|
||||
pub identifier: String,
|
||||
#[device_config(flatten)]
|
||||
pub mqtt: MqttDeviceConfig,
|
||||
|
@ -24,29 +24,27 @@ pub struct AudioSetupConfig {
|
|||
pub client: WrappedAsyncClient,
|
||||
}
|
||||
|
||||
#[derive(Debug, LuaDevice)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AudioSetup {
|
||||
config: AudioSetupConfig,
|
||||
config: Config,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl LuaDeviceCreate for AudioSetup {
|
||||
type Config = AudioSetupConfig;
|
||||
type Config = Config;
|
||||
type Error = DeviceConfigError;
|
||||
|
||||
async fn create(config: Self::Config) -> Result<Self, Self::Error> {
|
||||
trace!(id = config.identifier, "Setting up AudioSetup");
|
||||
|
||||
{
|
||||
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() {
|
||||
let mixer_id = config.mixer.get_id().to_owned();
|
||||
if (config.mixer.cast() as Option<&dyn OnOff>).is_none() {
|
||||
return Err(DeviceConfigError::MissingTrait(mixer_id, "OnOff".into()));
|
||||
}
|
||||
|
||||
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() {
|
||||
let speakers_id = config.speakers.get_id().to_owned();
|
||||
if (config.speakers.cast() as Option<&dyn OnOff>).is_none() {
|
||||
return Err(DeviceConfigError::MissingTrait(speakers_id, "OnOff".into()));
|
||||
}
|
||||
}
|
||||
|
@ -68,7 +66,7 @@ impl Device for AudioSetup {
|
|||
|
||||
#[async_trait]
|
||||
impl OnMqtt for AudioSetup {
|
||||
async fn on_mqtt(&mut self, message: rumqttc::Publish) {
|
||||
async fn on_mqtt(&self, message: rumqttc::Publish) {
|
||||
if !rumqttc::matches(&message.topic, &self.config.mqtt.topic) {
|
||||
return;
|
||||
}
|
||||
|
@ -76,19 +74,14 @@ impl OnMqtt for AudioSetup {
|
|||
let action = match RemoteMessage::try_from(message) {
|
||||
Ok(message) => message.action(),
|
||||
Err(err) => {
|
||||
error!(
|
||||
id = self.config.identifier,
|
||||
"Failed to parse message: {err}"
|
||||
);
|
||||
error!(id = self.get_id(), "Failed to parse message: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let mut mixer = self.config.mixer.write().await;
|
||||
let mut speakers = self.config.speakers.write().await;
|
||||
if let (Some(mixer), Some(speakers)) = (
|
||||
mixer.as_mut().cast_mut() as Option<&mut dyn OnOff>,
|
||||
speakers.as_mut().cast_mut() as Option<&mut dyn OnOff>,
|
||||
self.config.mixer.cast() as Option<&dyn OnOff>,
|
||||
self.config.speakers.cast() as Option<&dyn OnOff>,
|
||||
) {
|
||||
match action {
|
||||
RemoteAction::On => {
|
||||
|
@ -118,17 +111,14 @@ impl OnMqtt for AudioSetup {
|
|||
|
||||
#[async_trait]
|
||||
impl OnPresence for AudioSetup {
|
||||
async fn on_presence(&mut self, presence: bool) {
|
||||
let mut mixer = self.config.mixer.write().await;
|
||||
let mut speakers = self.config.speakers.write().await;
|
||||
|
||||
async fn on_presence(&self, presence: bool) {
|
||||
if let (Some(mixer), Some(speakers)) = (
|
||||
mixer.as_mut().cast_mut() as Option<&mut dyn OnOff>,
|
||||
speakers.as_mut().cast_mut() as Option<&mut dyn OnOff>,
|
||||
self.config.mixer.cast() as Option<&dyn OnOff>,
|
||||
self.config.speakers.cast() as Option<&dyn OnOff>,
|
||||
) {
|
||||
// Turn off the audio setup when we leave the house
|
||||
if !presence {
|
||||
debug!(id = self.config.identifier, "Turning devices off");
|
||||
debug!(id = self.get_id(), "Turning devices off");
|
||||
speakers.set_on(false).await.unwrap();
|
||||
mixer.set_on(false).await.unwrap();
|
||||
}
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use automation_macro::{LuaDevice, LuaDeviceConfig};
|
||||
use automation_macro::LuaDeviceConfig;
|
||||
use google_home::traits::OnOff;
|
||||
use mlua::FromLua;
|
||||
use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||
use tokio::task::JoinHandle;
|
||||
use tracing::{debug, error, trace, warn};
|
||||
|
||||
|
@ -26,31 +27,16 @@ pub struct PresenceDeviceConfig {
|
|||
pub timeout: Duration,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct TriggerDevicesHelper(Vec<WrappedDevice>);
|
||||
|
||||
impl<'lua> FromLua<'lua> for TriggerDevicesHelper {
|
||||
fn from_lua(value: mlua::Value<'lua>, lua: &'lua mlua::Lua) -> mlua::Result<Self> {
|
||||
Ok(TriggerDevicesHelper(mlua::FromLua::from_lua(value, lua)?))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TriggerDevicesHelper> for Vec<(WrappedDevice, bool)> {
|
||||
fn from(value: TriggerDevicesHelper) -> Self {
|
||||
value.0.into_iter().map(|device| (device, false)).collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, LuaDeviceConfig)]
|
||||
pub struct TriggerConfig {
|
||||
#[device_config(from_lua, from(TriggerDevicesHelper))]
|
||||
pub devices: Vec<(WrappedDevice, bool)>,
|
||||
#[device_config(from_lua)]
|
||||
pub devices: Vec<WrappedDevice>,
|
||||
#[device_config(default, with(|t: Option<_>| t.map(Duration::from_secs)))]
|
||||
pub timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, LuaDeviceConfig)]
|
||||
pub struct ContactSensorConfig {
|
||||
pub struct Config {
|
||||
pub identifier: String,
|
||||
#[device_config(flatten)]
|
||||
pub mqtt: MqttDeviceConfig,
|
||||
|
@ -62,40 +48,56 @@ pub struct ContactSensorConfig {
|
|||
pub client: WrappedAsyncClient,
|
||||
}
|
||||
|
||||
#[derive(Debug, LuaDevice)]
|
||||
pub struct ContactSensor {
|
||||
config: ContactSensorConfig,
|
||||
|
||||
#[derive(Debug)]
|
||||
struct State {
|
||||
overall_presence: bool,
|
||||
is_closed: bool,
|
||||
previous: Vec<bool>,
|
||||
handle: Option<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ContactSensor {
|
||||
config: Config,
|
||||
state: Arc<RwLock<State>>,
|
||||
}
|
||||
|
||||
impl ContactSensor {
|
||||
async fn state(&self) -> RwLockReadGuard<State> {
|
||||
self.state.read().await
|
||||
}
|
||||
|
||||
async fn state_mut(&self) -> RwLockWriteGuard<State> {
|
||||
self.state.write().await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl LuaDeviceCreate for ContactSensor {
|
||||
type Config = ContactSensorConfig;
|
||||
type Config = Config;
|
||||
type Error = DeviceConfigError;
|
||||
|
||||
async fn create(config: Self::Config) -> Result<Self, Self::Error> {
|
||||
trace!(id = config.identifier, "Setting up ContactSensor");
|
||||
|
||||
let mut previous = Vec::new();
|
||||
// Make sure the devices implement the required traits
|
||||
if let Some(trigger) = &config.trigger {
|
||||
for (device, _) in &trigger.devices {
|
||||
for device in &trigger.devices {
|
||||
{
|
||||
let device = device.read().await;
|
||||
let id = device.get_id().to_owned();
|
||||
if (device.as_ref().cast() as Option<&dyn OnOff>).is_none() {
|
||||
if (device.cast() as Option<&dyn OnOff>).is_none() {
|
||||
return Err(DeviceConfigError::MissingTrait(id, "OnOff".into()));
|
||||
}
|
||||
|
||||
if trigger.timeout.is_none()
|
||||
&& (device.as_ref().cast() as Option<&dyn Timeout>).is_none()
|
||||
&& (device.cast() as Option<&dyn Timeout>).is_none()
|
||||
{
|
||||
return Err(DeviceConfigError::MissingTrait(id, "Timeout".into()));
|
||||
}
|
||||
}
|
||||
}
|
||||
previous.resize(trigger.devices.len(), false);
|
||||
}
|
||||
|
||||
config
|
||||
|
@ -103,12 +105,15 @@ impl LuaDeviceCreate for ContactSensor {
|
|||
.subscribe(&config.mqtt.topic, rumqttc::QoS::AtLeastOnce)
|
||||
.await?;
|
||||
|
||||
Ok(Self {
|
||||
config: config.clone(),
|
||||
let state = State {
|
||||
overall_presence: DEFAULT_PRESENCE,
|
||||
is_closed: true,
|
||||
previous,
|
||||
handle: None,
|
||||
})
|
||||
};
|
||||
let state = Arc::new(RwLock::new(state));
|
||||
|
||||
Ok(Self { config, state })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -120,14 +125,14 @@ impl Device for ContactSensor {
|
|||
|
||||
#[async_trait]
|
||||
impl OnPresence for ContactSensor {
|
||||
async fn on_presence(&mut self, presence: bool) {
|
||||
self.overall_presence = presence;
|
||||
async fn on_presence(&self, presence: bool) {
|
||||
self.state_mut().await.overall_presence = presence;
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl OnMqtt for ContactSensor {
|
||||
async fn on_mqtt(&mut self, message: rumqttc::Publish) {
|
||||
async fn on_mqtt(&self, message: rumqttc::Publish) {
|
||||
if !rumqttc::matches(&message.topic, &self.config.mqtt.topic) {
|
||||
return;
|
||||
}
|
||||
|
@ -135,42 +140,44 @@ impl OnMqtt for ContactSensor {
|
|||
let is_closed = match ContactMessage::try_from(message) {
|
||||
Ok(state) => state.is_closed(),
|
||||
Err(err) => {
|
||||
error!(
|
||||
id = self.config.identifier,
|
||||
"Failed to parse message: {err}"
|
||||
);
|
||||
error!(id = self.get_id(), "Failed to parse message: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if is_closed == self.is_closed {
|
||||
if is_closed == self.state().await.is_closed {
|
||||
return;
|
||||
}
|
||||
|
||||
debug!(id = self.config.identifier, "Updating state to {is_closed}");
|
||||
self.is_closed = is_closed;
|
||||
debug!(id = self.get_id(), "Updating state to {is_closed}");
|
||||
self.state_mut().await.is_closed = is_closed;
|
||||
|
||||
if let Some(trigger) = &mut self.config.trigger {
|
||||
if !self.is_closed {
|
||||
for (light, previous) in &mut trigger.devices {
|
||||
let mut light = light.write().await;
|
||||
if let Some(light) = light.as_mut().cast_mut() as Option<&mut dyn OnOff> {
|
||||
if let Some(trigger) = &self.config.trigger {
|
||||
if !is_closed {
|
||||
for (light, previous) in trigger
|
||||
.devices
|
||||
.iter()
|
||||
.zip(self.state_mut().await.previous.iter_mut())
|
||||
{
|
||||
if let Some(light) = light.cast() as Option<&dyn OnOff> {
|
||||
*previous = light.on().await.unwrap();
|
||||
light.set_on(true).await.ok();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (light, previous) in &trigger.devices {
|
||||
let mut light = light.write().await;
|
||||
for (light, previous) in trigger
|
||||
.devices
|
||||
.iter()
|
||||
.zip(self.state_mut().await.previous.iter())
|
||||
{
|
||||
if !previous {
|
||||
// If the timeout is zero just turn the light off directly
|
||||
if trigger.timeout.is_none()
|
||||
&& let Some(light) = light.as_mut().cast_mut() as Option<&mut dyn OnOff>
|
||||
&& let Some(light) = light.cast() as Option<&dyn OnOff>
|
||||
{
|
||||
light.set_on(false).await.ok();
|
||||
} else if let Some(timeout) = trigger.timeout
|
||||
&& let Some(light) =
|
||||
light.as_mut().cast_mut() as Option<&mut dyn Timeout>
|
||||
&& let Some(light) = light.cast() as Option<&dyn Timeout>
|
||||
{
|
||||
light.start_timeout(timeout).await.unwrap();
|
||||
}
|
||||
|
@ -183,20 +190,20 @@ impl OnMqtt for ContactSensor {
|
|||
// Check if this contact sensor works as a presence device
|
||||
// If not we are done here
|
||||
let presence = match &self.config.presence {
|
||||
Some(presence) => presence,
|
||||
Some(presence) => presence.clone(),
|
||||
None => return,
|
||||
};
|
||||
|
||||
if !is_closed {
|
||||
// Activate presence and stop any timeout once we open the door
|
||||
if let Some(handle) = self.handle.take() {
|
||||
if let Some(handle) = self.state_mut().await.handle.take() {
|
||||
handle.abort();
|
||||
}
|
||||
|
||||
// Only use the door as an presence sensor if there the current presence is set false
|
||||
// This is to prevent the house from being marked as present for however long the
|
||||
// timeout is set when leaving the house
|
||||
if !self.overall_presence {
|
||||
if !self.state().await.overall_presence {
|
||||
self.config
|
||||
.client
|
||||
.publish(
|
||||
|
@ -216,18 +223,25 @@ impl OnMqtt for ContactSensor {
|
|||
}
|
||||
} else {
|
||||
// Once the door is closed again we start a timeout for removing the presence
|
||||
let client = self.config.client.clone();
|
||||
let id = self.config.identifier.clone();
|
||||
let timeout = presence.timeout;
|
||||
let topic = presence.mqtt.topic.clone();
|
||||
self.handle = Some(tokio::spawn(async move {
|
||||
debug!(id, "Starting timeout ({timeout:?}) for contact sensor...");
|
||||
tokio::time::sleep(timeout).await;
|
||||
debug!(id, "Removing door device!");
|
||||
client
|
||||
.publish(&topic, rumqttc::QoS::AtLeastOnce, false, "")
|
||||
let device = self.clone();
|
||||
self.state_mut().await.handle = Some(tokio::spawn(async move {
|
||||
debug!(
|
||||
id = device.get_id(),
|
||||
"Starting timeout ({:?}) for contact sensor...", presence.timeout
|
||||
);
|
||||
tokio::time::sleep(presence.timeout).await;
|
||||
debug!(id = device.get_id(), "Removing door device!");
|
||||
device
|
||||
.config
|
||||
.client
|
||||
.publish(&presence.mqtt.topic, rumqttc::QoS::AtLeastOnce, false, "")
|
||||
.await
|
||||
.map_err(|err| warn!("Failed to publish presence on {topic}: {err}"))
|
||||
.map_err(|err| {
|
||||
warn!(
|
||||
"Failed to publish presence on {}: {err}",
|
||||
presence.mqtt.topic
|
||||
)
|
||||
})
|
||||
.ok();
|
||||
}));
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use std::convert::Infallible;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use automation_macro::{LuaDevice, LuaDeviceConfig};
|
||||
use automation_macro::LuaDeviceConfig;
|
||||
use tracing::{trace, warn};
|
||||
|
||||
use super::LuaDeviceCreate;
|
||||
|
@ -12,7 +12,7 @@ use crate::messages::{DarknessMessage, PresenceMessage};
|
|||
use crate::mqtt::WrappedAsyncClient;
|
||||
|
||||
#[derive(Debug, LuaDeviceConfig, Clone)]
|
||||
pub struct DebugBridgeConfig {
|
||||
pub struct Config {
|
||||
pub identifier: String,
|
||||
#[device_config(flatten)]
|
||||
pub mqtt: MqttDeviceConfig,
|
||||
|
@ -20,14 +20,14 @@ pub struct DebugBridgeConfig {
|
|||
pub client: WrappedAsyncClient,
|
||||
}
|
||||
|
||||
#[derive(Debug, LuaDevice)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DebugBridge {
|
||||
config: DebugBridgeConfig,
|
||||
config: Config,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl LuaDeviceCreate for DebugBridge {
|
||||
type Config = DebugBridgeConfig;
|
||||
type Config = Config;
|
||||
type Error = Infallible;
|
||||
|
||||
async fn create(config: Self::Config) -> Result<Self, Self::Error> {
|
||||
|
@ -44,7 +44,7 @@ impl Device for DebugBridge {
|
|||
|
||||
#[async_trait]
|
||||
impl OnPresence for DebugBridge {
|
||||
async fn on_presence(&mut self, presence: bool) {
|
||||
async fn on_presence(&self, presence: bool) {
|
||||
let message = PresenceMessage::new(presence);
|
||||
let topic = format!("{}/presence", self.config.mqtt.topic);
|
||||
self.config
|
||||
|
@ -68,7 +68,7 @@ impl OnPresence for DebugBridge {
|
|||
|
||||
#[async_trait]
|
||||
impl OnDarkness for DebugBridge {
|
||||
async fn on_darkness(&mut self, dark: bool) {
|
||||
async fn on_darkness(&self, dark: bool) {
|
||||
let message = DarknessMessage::new(dark);
|
||||
let topic = format!("{}/darkness", self.config.mqtt.topic);
|
||||
self.config
|
||||
|
|
|
@ -2,7 +2,7 @@ use std::convert::Infallible;
|
|||
use std::net::SocketAddr;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use automation_macro::{LuaDevice, LuaDeviceConfig};
|
||||
use automation_macro::LuaDeviceConfig;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::{error, trace, warn};
|
||||
|
||||
|
@ -23,7 +23,7 @@ pub struct FlagIDs {
|
|||
}
|
||||
|
||||
#[derive(Debug, LuaDeviceConfig, Clone)]
|
||||
pub struct HueBridgeConfig {
|
||||
pub struct Config {
|
||||
pub identifier: String,
|
||||
#[device_config(rename("ip"), with(|ip| SocketAddr::new(ip, 80)))]
|
||||
pub addr: SocketAddr,
|
||||
|
@ -31,9 +31,9 @@ pub struct HueBridgeConfig {
|
|||
pub flags: FlagIDs,
|
||||
}
|
||||
|
||||
#[derive(Debug, LuaDevice)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HueBridge {
|
||||
config: HueBridgeConfig,
|
||||
config: Config,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
|
@ -43,7 +43,7 @@ struct FlagMessage {
|
|||
|
||||
#[async_trait]
|
||||
impl LuaDeviceCreate for HueBridge {
|
||||
type Config = HueBridgeConfig;
|
||||
type Config = Config;
|
||||
type Error = Infallible;
|
||||
|
||||
async fn create(config: Self::Config) -> Result<Self, Infallible> {
|
||||
|
@ -93,7 +93,7 @@ impl Device for HueBridge {
|
|||
|
||||
#[async_trait]
|
||||
impl OnPresence for HueBridge {
|
||||
async fn on_presence(&mut self, presence: bool) {
|
||||
async fn on_presence(&self, presence: bool) {
|
||||
trace!("Bridging presence to hue");
|
||||
self.set_flag(Flag::Presence, presence).await;
|
||||
}
|
||||
|
@ -101,7 +101,7 @@ impl OnPresence for HueBridge {
|
|||
|
||||
#[async_trait]
|
||||
impl OnDarkness for HueBridge {
|
||||
async fn on_darkness(&mut self, dark: bool) {
|
||||
async fn on_darkness(&self, dark: bool) {
|
||||
trace!("Bridging darkness to hue");
|
||||
self.set_flag(Flag::Darkness, dark).await;
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ use std::time::Duration;
|
|||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use async_trait::async_trait;
|
||||
use automation_macro::{LuaDevice, LuaDeviceConfig};
|
||||
use automation_macro::LuaDeviceConfig;
|
||||
use google_home::errors::ErrorCode;
|
||||
use google_home::traits::OnOff;
|
||||
use rumqttc::{Publish, SubscribeFilter};
|
||||
|
@ -17,7 +17,7 @@ use crate::mqtt::WrappedAsyncClient;
|
|||
use crate::traits::Timeout;
|
||||
|
||||
#[derive(Debug, Clone, LuaDeviceConfig)]
|
||||
pub struct HueGroupConfig {
|
||||
pub struct Config {
|
||||
pub identifier: String,
|
||||
#[device_config(rename("ip"), with(|ip| SocketAddr::new(ip, 80)))]
|
||||
pub addr: SocketAddr,
|
||||
|
@ -31,15 +31,15 @@ pub struct HueGroupConfig {
|
|||
pub client: WrappedAsyncClient,
|
||||
}
|
||||
|
||||
#[derive(Debug, LuaDevice)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HueGroup {
|
||||
config: HueGroupConfig,
|
||||
config: Config,
|
||||
}
|
||||
|
||||
// Couple of helper function to get the correct urls
|
||||
#[async_trait]
|
||||
impl LuaDeviceCreate for HueGroup {
|
||||
type Config = HueGroupConfig;
|
||||
type Config = Config;
|
||||
type Error = rumqttc::ClientError;
|
||||
|
||||
async fn create(config: Self::Config) -> Result<Self, Self::Error> {
|
||||
|
@ -85,7 +85,7 @@ impl Device for HueGroup {
|
|||
|
||||
#[async_trait]
|
||||
impl OnMqtt for HueGroup {
|
||||
async fn on_mqtt(&mut self, message: Publish) {
|
||||
async fn on_mqtt(&self, message: Publish) {
|
||||
if !self
|
||||
.config
|
||||
.remotes
|
||||
|
@ -98,10 +98,7 @@ impl OnMqtt for HueGroup {
|
|||
let action = match RemoteMessage::try_from(message) {
|
||||
Ok(message) => message.action(),
|
||||
Err(err) => {
|
||||
error!(
|
||||
id = self.config.identifier,
|
||||
"Failed to parse message: {err}"
|
||||
);
|
||||
error!(id = self.get_id(), "Failed to parse message: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
@ -120,7 +117,7 @@ impl OnMqtt for HueGroup {
|
|||
|
||||
#[async_trait]
|
||||
impl OnOff for HueGroup {
|
||||
async fn set_on(&mut self, on: bool) -> Result<(), ErrorCode> {
|
||||
async fn set_on(&self, on: bool) -> Result<(), ErrorCode> {
|
||||
// Abort any timer that is currently running
|
||||
self.stop_timeout().await.unwrap();
|
||||
|
||||
|
@ -140,13 +137,10 @@ impl OnOff for HueGroup {
|
|||
Ok(res) => {
|
||||
let status = res.status();
|
||||
if !status.is_success() {
|
||||
warn!(
|
||||
id = self.config.identifier,
|
||||
"Status code is not success: {status}"
|
||||
);
|
||||
warn!(id = self.get_id(), "Status code is not success: {status}");
|
||||
}
|
||||
}
|
||||
Err(err) => error!(id = self.config.identifier, "Error: {err}"),
|
||||
Err(err) => error!(id = self.get_id(), "Error: {err}"),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
@ -162,19 +156,13 @@ impl OnOff for HueGroup {
|
|||
Ok(res) => {
|
||||
let status = res.status();
|
||||
if !status.is_success() {
|
||||
warn!(
|
||||
id = self.config.identifier,
|
||||
"Status code is not success: {status}"
|
||||
);
|
||||
warn!(id = self.get_id(), "Status code is not success: {status}");
|
||||
}
|
||||
|
||||
let on = match res.json::<message::Info>().await {
|
||||
Ok(info) => info.any_on(),
|
||||
Err(err) => {
|
||||
error!(
|
||||
id = self.config.identifier,
|
||||
"Failed to parse message: {err}"
|
||||
);
|
||||
error!(id = self.get_id(), "Failed to parse message: {err}");
|
||||
// TODO: Error code
|
||||
return Ok(false);
|
||||
}
|
||||
|
@ -182,7 +170,7 @@ impl OnOff for HueGroup {
|
|||
|
||||
return Ok(on);
|
||||
}
|
||||
Err(err) => error!(id = self.config.identifier, "Error: {err}"),
|
||||
Err(err) => error!(id = self.get_id(), "Error: {err}"),
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
|
@ -191,7 +179,7 @@ impl OnOff for HueGroup {
|
|||
|
||||
#[async_trait]
|
||||
impl Timeout for HueGroup {
|
||||
async fn start_timeout(&mut self, timeout: Duration) -> Result<()> {
|
||||
async fn start_timeout(&self, timeout: Duration) -> Result<()> {
|
||||
// Abort any timer that is currently running
|
||||
self.stop_timeout().await?;
|
||||
|
||||
|
@ -214,7 +202,7 @@ impl Timeout for HueGroup {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
async fn stop_timeout(&mut self) -> Result<()> {
|
||||
async fn stop_timeout(&self) -> Result<()> {
|
||||
let message = message::Timeout::new(None);
|
||||
let res = reqwest::Client::new()
|
||||
.put(self.url_set_schedule())
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use automation_macro::{LuaDevice, LuaDeviceConfig};
|
||||
use automation_macro::LuaDeviceConfig;
|
||||
use google_home::device;
|
||||
use google_home::errors::ErrorCode;
|
||||
use google_home::traits::{self, OnOff};
|
||||
use google_home::types::Type;
|
||||
use rumqttc::{matches, Publish, SubscribeFilter};
|
||||
use serde::Deserialize;
|
||||
use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||
use tokio::task::JoinHandle;
|
||||
use tracing::{debug, error, trace, warn};
|
||||
|
||||
|
@ -29,7 +31,7 @@ pub enum OutletType {
|
|||
}
|
||||
|
||||
#[derive(Debug, Clone, LuaDeviceConfig)]
|
||||
pub struct IkeaOutletConfig {
|
||||
pub struct Config {
|
||||
#[device_config(flatten)]
|
||||
pub info: InfoConfig,
|
||||
#[device_config(flatten)]
|
||||
|
@ -45,34 +47,32 @@ pub struct IkeaOutletConfig {
|
|||
pub client: WrappedAsyncClient,
|
||||
}
|
||||
|
||||
#[derive(Debug, LuaDevice)]
|
||||
pub struct IkeaOutlet {
|
||||
config: IkeaOutletConfig,
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct State {
|
||||
last_known_state: bool,
|
||||
handle: Option<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
async fn set_on(client: WrappedAsyncClient, topic: &str, on: bool) {
|
||||
let message = OnOffMessage::new(on);
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct IkeaOutlet {
|
||||
config: Config,
|
||||
|
||||
let topic = format!("{}/set", topic);
|
||||
// TODO: Handle potential errors here
|
||||
client
|
||||
.publish(
|
||||
&topic,
|
||||
rumqttc::QoS::AtLeastOnce,
|
||||
false,
|
||||
serde_json::to_string(&message).unwrap(),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| warn!("Failed to update state on {topic}: {err}"))
|
||||
.ok();
|
||||
state: Arc<RwLock<State>>,
|
||||
}
|
||||
|
||||
impl IkeaOutlet {
|
||||
async fn state(&self) -> RwLockReadGuard<State> {
|
||||
self.state.read().await
|
||||
}
|
||||
|
||||
async fn state_mut(&self) -> RwLockWriteGuard<State> {
|
||||
self.state.write().await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl LuaDeviceCreate for IkeaOutlet {
|
||||
type Config = IkeaOutletConfig;
|
||||
type Config = Config;
|
||||
type Error = rumqttc::ClientError;
|
||||
|
||||
async fn create(config: Self::Config) -> Result<Self, Self::Error> {
|
||||
|
@ -93,11 +93,13 @@ impl LuaDeviceCreate for IkeaOutlet {
|
|||
.subscribe(&config.mqtt.topic, rumqttc::QoS::AtLeastOnce)
|
||||
.await?;
|
||||
|
||||
Ok(Self {
|
||||
config,
|
||||
let state = State {
|
||||
last_known_state: false,
|
||||
handle: None,
|
||||
})
|
||||
};
|
||||
let state = Arc::new(RwLock::new(state));
|
||||
|
||||
Ok(Self { config, state })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -109,7 +111,7 @@ impl Device for IkeaOutlet {
|
|||
|
||||
#[async_trait]
|
||||
impl OnMqtt for IkeaOutlet {
|
||||
async fn on_mqtt(&mut self, message: Publish) {
|
||||
async fn on_mqtt(&self, message: Publish) {
|
||||
// Check if the message is from the deviec itself or from a remote
|
||||
if matches(&message.topic, &self.config.mqtt.topic) {
|
||||
// Update the internal state based on what the device has reported
|
||||
|
@ -122,7 +124,7 @@ impl OnMqtt for IkeaOutlet {
|
|||
};
|
||||
|
||||
// No need to do anything if the state has not changed
|
||||
if state == self.last_known_state {
|
||||
if state == self.state().await.last_known_state {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -130,7 +132,7 @@ impl OnMqtt for IkeaOutlet {
|
|||
self.stop_timeout().await.unwrap();
|
||||
|
||||
debug!(id = Device::get_id(self), "Updating state to {state}");
|
||||
self.last_known_state = state;
|
||||
self.state_mut().await.last_known_state = state;
|
||||
|
||||
// If this is a kettle start a timeout for turning it of again
|
||||
if state && let Some(timeout) = self.config.timeout {
|
||||
|
@ -162,7 +164,7 @@ impl OnMqtt for IkeaOutlet {
|
|||
|
||||
#[async_trait]
|
||||
impl OnPresence for IkeaOutlet {
|
||||
async fn on_presence(&mut self, presence: bool) {
|
||||
async fn on_presence(&self, presence: bool) {
|
||||
// Turn off the outlet when we leave the house (Not if it is a battery charger)
|
||||
if !presence && self.config.outlet_type != OutletType::Charger {
|
||||
debug!(id = Device::get_id(self), "Turning device off");
|
||||
|
@ -206,11 +208,25 @@ impl google_home::Device for IkeaOutlet {
|
|||
#[async_trait]
|
||||
impl traits::OnOff for IkeaOutlet {
|
||||
async fn on(&self) -> Result<bool, ErrorCode> {
|
||||
Ok(self.last_known_state)
|
||||
Ok(self.state().await.last_known_state)
|
||||
}
|
||||
|
||||
async fn set_on(&mut self, on: bool) -> Result<(), ErrorCode> {
|
||||
set_on(self.config.client.clone(), &self.config.mqtt.topic, on).await;
|
||||
async fn set_on(&self, on: bool) -> Result<(), ErrorCode> {
|
||||
let message = OnOffMessage::new(on);
|
||||
|
||||
let topic = format!("{}/set", self.config.mqtt.topic);
|
||||
// TODO: Handle potential errors here
|
||||
self.config
|
||||
.client
|
||||
.publish(
|
||||
&topic,
|
||||
rumqttc::QoS::AtLeastOnce,
|
||||
false,
|
||||
serde_json::to_string(&message).unwrap(),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| warn!("Failed to update state on {topic}: {err}"))
|
||||
.ok();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -218,31 +234,23 @@ impl traits::OnOff for IkeaOutlet {
|
|||
|
||||
#[async_trait]
|
||||
impl crate::traits::Timeout for IkeaOutlet {
|
||||
async fn start_timeout(&mut self, timeout: Duration) -> Result<()> {
|
||||
async fn start_timeout(&self, timeout: Duration) -> Result<()> {
|
||||
// Abort any timer that is currently running
|
||||
self.stop_timeout().await?;
|
||||
|
||||
// Turn the kettle of after the specified timeout
|
||||
// TODO: Impl Drop for IkeaOutlet that will abort the handle if the IkeaOutlet
|
||||
// get dropped
|
||||
let client = self.config.client.clone();
|
||||
let topic = self.config.mqtt.topic.clone();
|
||||
let id = Device::get_id(self).clone();
|
||||
self.handle = Some(tokio::spawn(async move {
|
||||
debug!(id, "Starting timeout ({timeout:?})...");
|
||||
let device = self.clone();
|
||||
self.state_mut().await.handle = Some(tokio::spawn(async move {
|
||||
debug!(id = device.get_id(), "Starting timeout ({timeout:?})...");
|
||||
tokio::time::sleep(timeout).await;
|
||||
debug!(id, "Turning outlet off!");
|
||||
// TODO: Idealy we would call self.set_on(false), however since we want to do
|
||||
// it after a timeout we have to put it in a separate task.
|
||||
// I don't think we can really get around calling outside function
|
||||
set_on(client, &topic, false).await;
|
||||
debug!(id = device.get_id(), "Turning outlet off!");
|
||||
device.set_on(false).await.unwrap();
|
||||
}));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn stop_timeout(&mut self) -> Result<()> {
|
||||
if let Some(handle) = self.handle.take() {
|
||||
async fn stop_timeout(&self) -> Result<()> {
|
||||
if let Some(handle) = self.state_mut().await.handle.take() {
|
||||
handle.abort();
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ use std::net::SocketAddr;
|
|||
use std::str::Utf8Error;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use automation_macro::{LuaDevice, LuaDeviceConfig};
|
||||
use automation_macro::LuaDeviceConfig;
|
||||
use bytes::{Buf, BufMut};
|
||||
use google_home::errors::{self, DeviceError};
|
||||
use google_home::traits;
|
||||
|
@ -16,20 +16,20 @@ use tracing::trace;
|
|||
use super::{Device, LuaDeviceCreate};
|
||||
|
||||
#[derive(Debug, Clone, LuaDeviceConfig)]
|
||||
pub struct KasaOutletConfig {
|
||||
pub struct Config {
|
||||
pub identifier: String,
|
||||
#[device_config(rename("ip"), with(|ip| SocketAddr::new(ip, 9999)))]
|
||||
pub addr: SocketAddr,
|
||||
}
|
||||
|
||||
#[derive(Debug, LuaDevice)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct KasaOutlet {
|
||||
config: KasaOutletConfig,
|
||||
config: Config,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl LuaDeviceCreate for KasaOutlet {
|
||||
type Config = KasaOutletConfig;
|
||||
type Config = Config;
|
||||
type Error = Infallible;
|
||||
|
||||
async fn create(config: Self::Config) -> Result<Self, Self::Error> {
|
||||
|
@ -241,7 +241,7 @@ impl traits::OnOff for KasaOutlet {
|
|||
.or(Err(DeviceError::TransientError.into()))
|
||||
}
|
||||
|
||||
async fn set_on(&mut self, on: bool) -> Result<(), errors::ErrorCode> {
|
||||
async fn set_on(&self, on: bool) -> Result<(), errors::ErrorCode> {
|
||||
let mut stream = TcpStream::connect(self.config.addr)
|
||||
.await
|
||||
.or::<DeviceError>(Err(DeviceError::DeviceOffline))?;
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use automation_macro::{LuaDevice, LuaDeviceConfig};
|
||||
use automation_macro::LuaDeviceConfig;
|
||||
use rumqttc::Publish;
|
||||
use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||
use tracing::{debug, trace, warn};
|
||||
|
||||
use super::LuaDeviceCreate;
|
||||
|
@ -11,7 +14,7 @@ use crate::messages::BrightnessMessage;
|
|||
use crate::mqtt::WrappedAsyncClient;
|
||||
|
||||
#[derive(Debug, Clone, LuaDeviceConfig)]
|
||||
pub struct LightSensorConfig {
|
||||
pub struct Config {
|
||||
pub identifier: String,
|
||||
#[device_config(flatten)]
|
||||
pub mqtt: MqttDeviceConfig,
|
||||
|
@ -25,16 +28,30 @@ pub struct LightSensorConfig {
|
|||
|
||||
const DEFAULT: bool = false;
|
||||
|
||||
#[derive(Debug, LuaDevice)]
|
||||
pub struct LightSensor {
|
||||
config: LightSensorConfig,
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct State {
|
||||
is_dark: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LightSensor {
|
||||
config: Config,
|
||||
state: Arc<RwLock<State>>,
|
||||
}
|
||||
|
||||
impl LightSensor {
|
||||
async fn state(&self) -> RwLockReadGuard<State> {
|
||||
self.state.read().await
|
||||
}
|
||||
|
||||
async fn state_mut(&self) -> RwLockWriteGuard<State> {
|
||||
self.state.write().await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl LuaDeviceCreate for LightSensor {
|
||||
type Config = LightSensorConfig;
|
||||
type Config = Config;
|
||||
type Error = rumqttc::ClientError;
|
||||
|
||||
async fn create(config: Self::Config) -> Result<Self, Self::Error> {
|
||||
|
@ -45,10 +62,10 @@ impl LuaDeviceCreate for LightSensor {
|
|||
.subscribe(&config.mqtt.topic, rumqttc::QoS::AtLeastOnce)
|
||||
.await?;
|
||||
|
||||
Ok(Self {
|
||||
config,
|
||||
is_dark: DEFAULT,
|
||||
})
|
||||
let state = State { is_dark: DEFAULT };
|
||||
let state = Arc::new(RwLock::new(state));
|
||||
|
||||
Ok(Self { config, state })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -60,7 +77,7 @@ impl Device for LightSensor {
|
|||
|
||||
#[async_trait]
|
||||
impl OnMqtt for LightSensor {
|
||||
async fn on_mqtt(&mut self, message: Publish) {
|
||||
async fn on_mqtt(&self, message: Publish) {
|
||||
if !rumqttc::matches(&message.topic, &self.config.mqtt.topic) {
|
||||
return;
|
||||
}
|
||||
|
@ -81,18 +98,19 @@ impl OnMqtt for LightSensor {
|
|||
trace!("It is light");
|
||||
false
|
||||
} else {
|
||||
let is_dark = self.state().await.is_dark;
|
||||
trace!(
|
||||
"In between min ({}) and max ({}) value, keeping current state: {}",
|
||||
self.config.min,
|
||||
self.config.max,
|
||||
self.is_dark
|
||||
is_dark
|
||||
);
|
||||
self.is_dark
|
||||
is_dark
|
||||
};
|
||||
|
||||
if is_dark != self.is_dark {
|
||||
if is_dark != self.state().await.is_dark {
|
||||
debug!("Dark state has changed: {is_dark}");
|
||||
self.is_dark = is_dark;
|
||||
self.state_mut().await.is_dark = is_dark;
|
||||
|
||||
if self.config.tx.send(Event::Darkness(is_dark)).await.is_err() {
|
||||
warn!("There are no receivers on the event channel");
|
||||
|
|
|
@ -16,21 +16,22 @@ use std::fmt::Debug;
|
|||
|
||||
use async_trait::async_trait;
|
||||
use automation_cast::Cast;
|
||||
use dyn_clone::DynClone;
|
||||
use google_home::traits::OnOff;
|
||||
|
||||
pub use self::air_filter::*;
|
||||
pub use self::audio_setup::*;
|
||||
pub use self::contact_sensor::*;
|
||||
pub use self::debug_bridge::*;
|
||||
pub use self::hue_bridge::*;
|
||||
pub use self::hue_group::*;
|
||||
pub use self::ikea_outlet::*;
|
||||
pub use self::kasa_outlet::*;
|
||||
pub use self::light_sensor::*;
|
||||
pub use self::air_filter::AirFilter;
|
||||
pub use self::audio_setup::AudioSetup;
|
||||
pub use self::contact_sensor::ContactSensor;
|
||||
pub use self::debug_bridge::DebugBridge;
|
||||
pub use self::hue_bridge::HueBridge;
|
||||
pub use self::hue_group::HueGroup;
|
||||
pub use self::ikea_outlet::IkeaOutlet;
|
||||
pub use self::kasa_outlet::KasaOutlet;
|
||||
pub use self::light_sensor::LightSensor;
|
||||
pub use self::ntfy::{Notification, Ntfy};
|
||||
pub use self::presence::{Presence, PresenceConfig, DEFAULT_PRESENCE};
|
||||
pub use self::wake_on_lan::*;
|
||||
pub use self::washer::*;
|
||||
pub use self::presence::{Presence, DEFAULT_PRESENCE};
|
||||
pub use self::wake_on_lan::WakeOnLAN;
|
||||
pub use self::washer::Washer;
|
||||
use crate::event::{OnDarkness, OnMqtt, OnNotification, OnPresence};
|
||||
use crate::traits::Timeout;
|
||||
|
||||
|
@ -44,26 +45,67 @@ pub trait LuaDeviceCreate {
|
|||
Self: Sized;
|
||||
}
|
||||
|
||||
macro_rules! register_device {
|
||||
($lua:expr, $device:ty) => {
|
||||
$lua.globals()
|
||||
.set(stringify!($device), $lua.create_proxy::<$device>()?)?;
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! impl_device {
|
||||
($lua:expr, $device:ty) => {
|
||||
impl mlua::UserData for $device {
|
||||
fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||
methods.add_async_function("new", |lua, config: mlua::Value| async {
|
||||
let config = mlua::FromLua::from_lua(config, lua)?;
|
||||
|
||||
// TODO: Using crate:: could cause issues
|
||||
let device: $device = crate::devices::LuaDeviceCreate::create(config)
|
||||
.await
|
||||
.map_err(mlua::ExternalError::into_lua_err)?;
|
||||
|
||||
Ok(crate::device_manager::WrappedDevice::new(device))
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_device!(lua, AirFilter);
|
||||
impl_device!(lua, AudioSetup);
|
||||
impl_device!(lua, ContactSensor);
|
||||
impl_device!(lua, DebugBridge);
|
||||
impl_device!(lua, HueBridge);
|
||||
impl_device!(lua, HueGroup);
|
||||
impl_device!(lua, IkeaOutlet);
|
||||
impl_device!(lua, KasaOutlet);
|
||||
impl_device!(lua, LightSensor);
|
||||
impl_device!(lua, Ntfy);
|
||||
impl_device!(lua, Presence);
|
||||
impl_device!(lua, WakeOnLAN);
|
||||
impl_device!(lua, Washer);
|
||||
|
||||
pub fn register_with_lua(lua: &mlua::Lua) -> mlua::Result<()> {
|
||||
AirFilter::register_with_lua(lua)?;
|
||||
AudioSetup::register_with_lua(lua)?;
|
||||
ContactSensor::register_with_lua(lua)?;
|
||||
DebugBridge::register_with_lua(lua)?;
|
||||
HueBridge::register_with_lua(lua)?;
|
||||
HueGroup::register_with_lua(lua)?;
|
||||
IkeaOutlet::register_with_lua(lua)?;
|
||||
KasaOutlet::register_with_lua(lua)?;
|
||||
LightSensor::register_with_lua(lua)?;
|
||||
Ntfy::register_with_lua(lua)?;
|
||||
Presence::register_with_lua(lua)?;
|
||||
WakeOnLAN::register_with_lua(lua)?;
|
||||
Washer::register_with_lua(lua)?;
|
||||
register_device!(lua, AirFilter);
|
||||
register_device!(lua, AudioSetup);
|
||||
register_device!(lua, ContactSensor);
|
||||
register_device!(lua, DebugBridge);
|
||||
register_device!(lua, HueBridge);
|
||||
register_device!(lua, HueGroup);
|
||||
register_device!(lua, IkeaOutlet);
|
||||
register_device!(lua, KasaOutlet);
|
||||
register_device!(lua, LightSensor);
|
||||
register_device!(lua, Ntfy);
|
||||
register_device!(lua, Presence);
|
||||
register_device!(lua, WakeOnLAN);
|
||||
register_device!(lua, Washer);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub trait Device:
|
||||
Debug
|
||||
+ DynClone
|
||||
+ Sync
|
||||
+ Send
|
||||
+ Cast<dyn google_home::Device>
|
||||
|
@ -77,3 +119,5 @@ pub trait Device:
|
|||
{
|
||||
fn get_id(&self) -> String;
|
||||
}
|
||||
|
||||
dyn_clone::clone_trait_object!(Device);
|
||||
|
|
|
@ -2,7 +2,7 @@ use std::collections::HashMap;
|
|||
use std::convert::Infallible;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use automation_macro::{LuaDevice, LuaDeviceConfig};
|
||||
use automation_macro::LuaDeviceConfig;
|
||||
use serde::Serialize;
|
||||
use serde_repr::*;
|
||||
use tracing::{error, trace, warn};
|
||||
|
@ -111,8 +111,8 @@ impl Default for Notification {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, LuaDeviceConfig)]
|
||||
pub struct NtfyConfig {
|
||||
#[derive(Debug, Clone, LuaDeviceConfig)]
|
||||
pub struct Config {
|
||||
#[device_config(default("https://ntfy.sh".into()))]
|
||||
pub url: String,
|
||||
pub topic: String,
|
||||
|
@ -120,14 +120,14 @@ pub struct NtfyConfig {
|
|||
pub tx: event::Sender,
|
||||
}
|
||||
|
||||
#[derive(Debug, LuaDevice)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Ntfy {
|
||||
config: NtfyConfig,
|
||||
config: Config,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl LuaDeviceCreate for Ntfy {
|
||||
type Config = NtfyConfig;
|
||||
type Config = Config;
|
||||
type Error = Infallible;
|
||||
|
||||
async fn create(config: Self::Config) -> Result<Self, Self::Error> {
|
||||
|
@ -166,7 +166,7 @@ impl Ntfy {
|
|||
|
||||
#[async_trait]
|
||||
impl OnPresence for Ntfy {
|
||||
async fn on_presence(&mut self, presence: bool) {
|
||||
async fn on_presence(&self, presence: bool) {
|
||||
// Setup extras for the broadcast
|
||||
let extras = HashMap::from([
|
||||
("cmd".into(), "presence".into()),
|
||||
|
@ -202,7 +202,7 @@ impl OnPresence for Ntfy {
|
|||
|
||||
#[async_trait]
|
||||
impl OnNotification for Ntfy {
|
||||
async fn on_notification(&mut self, notification: Notification) {
|
||||
async fn on_notification(&self, notification: Notification) {
|
||||
self.send(notification).await;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use automation_macro::{LuaDevice, LuaDeviceConfig};
|
||||
use automation_macro::LuaDeviceConfig;
|
||||
use rumqttc::Publish;
|
||||
use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||
use tracing::{debug, trace, warn};
|
||||
|
||||
use super::LuaDeviceCreate;
|
||||
|
@ -12,8 +14,8 @@ use crate::event::{self, Event, EventChannel, OnMqtt};
|
|||
use crate::messages::PresenceMessage;
|
||||
use crate::mqtt::WrappedAsyncClient;
|
||||
|
||||
#[derive(Debug, LuaDeviceConfig)]
|
||||
pub struct PresenceConfig {
|
||||
#[derive(Debug, Clone, LuaDeviceConfig)]
|
||||
pub struct Config {
|
||||
#[device_config(flatten)]
|
||||
pub mqtt: MqttDeviceConfig,
|
||||
#[device_config(from_lua, rename("event_channel"), with(|ec: EventChannel| ec.get_tx()))]
|
||||
|
@ -24,31 +26,48 @@ pub struct PresenceConfig {
|
|||
|
||||
pub const DEFAULT_PRESENCE: bool = false;
|
||||
|
||||
#[derive(Debug, LuaDevice)]
|
||||
pub struct Presence {
|
||||
config: PresenceConfig,
|
||||
#[derive(Debug)]
|
||||
pub struct State {
|
||||
devices: HashMap<String, bool>,
|
||||
current_overall_presence: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Presence {
|
||||
config: Config,
|
||||
state: Arc<RwLock<State>>,
|
||||
}
|
||||
|
||||
impl Presence {
|
||||
async fn state(&self) -> RwLockReadGuard<State> {
|
||||
self.state.read().await
|
||||
}
|
||||
|
||||
async fn state_mut(&self) -> RwLockWriteGuard<State> {
|
||||
self.state.write().await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl LuaDeviceCreate for Presence {
|
||||
type Config = PresenceConfig;
|
||||
type Config = Config;
|
||||
type Error = rumqttc::ClientError;
|
||||
|
||||
async fn create(config: Self::Config) -> Result<Self, Self::Error> {
|
||||
trace!(id = "ntfy", "Setting up Presence");
|
||||
trace!(id = "presence", "Setting up Presence");
|
||||
|
||||
config
|
||||
.client
|
||||
.subscribe(&config.mqtt.topic, rumqttc::QoS::AtLeastOnce)
|
||||
.await?;
|
||||
|
||||
Ok(Self {
|
||||
config,
|
||||
let state = State {
|
||||
devices: HashMap::new(),
|
||||
current_overall_presence: DEFAULT_PRESENCE,
|
||||
})
|
||||
};
|
||||
let state = Arc::new(RwLock::new(state));
|
||||
|
||||
Ok(Self { config, state })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -60,7 +79,7 @@ impl Device for Presence {
|
|||
|
||||
#[async_trait]
|
||||
impl OnMqtt for Presence {
|
||||
async fn on_mqtt(&mut self, message: Publish) {
|
||||
async fn on_mqtt(&self, message: Publish) {
|
||||
if !rumqttc::matches(&message.topic, &self.config.mqtt.topic) {
|
||||
return;
|
||||
}
|
||||
|
@ -77,7 +96,7 @@ impl OnMqtt for Presence {
|
|||
if message.payload.is_empty() {
|
||||
// Remove the device from the map
|
||||
debug!("State of device [{device_name}] has been removed");
|
||||
self.devices.remove(&device_name);
|
||||
self.state_mut().await.devices.remove(&device_name);
|
||||
} else {
|
||||
let present = match PresenceMessage::try_from(message) {
|
||||
Ok(state) => state.presence(),
|
||||
|
@ -88,13 +107,13 @@ impl OnMqtt for Presence {
|
|||
};
|
||||
|
||||
debug!("State of device [{device_name}] has changed: {}", present);
|
||||
self.devices.insert(device_name, present);
|
||||
self.state_mut().await.devices.insert(device_name, present);
|
||||
}
|
||||
|
||||
let overall_presence = self.devices.iter().any(|(_, v)| *v);
|
||||
if overall_presence != self.current_overall_presence {
|
||||
let overall_presence = self.state().await.devices.iter().any(|(_, v)| *v);
|
||||
if overall_presence != self.state().await.current_overall_presence {
|
||||
debug!("Overall presence updated: {overall_presence}");
|
||||
self.current_overall_presence = overall_presence;
|
||||
self.state_mut().await.current_overall_presence = overall_presence;
|
||||
|
||||
if self
|
||||
.config
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use std::net::Ipv4Addr;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use automation_macro::{LuaDevice, LuaDeviceConfig};
|
||||
use automation_macro::LuaDeviceConfig;
|
||||
use eui48::MacAddress;
|
||||
use google_home::device;
|
||||
use google_home::errors::ErrorCode;
|
||||
|
@ -17,7 +17,7 @@ use crate::messages::ActivateMessage;
|
|||
use crate::mqtt::WrappedAsyncClient;
|
||||
|
||||
#[derive(Debug, Clone, LuaDeviceConfig)]
|
||||
pub struct WakeOnLANConfig {
|
||||
pub struct Config {
|
||||
#[device_config(flatten)]
|
||||
pub info: InfoConfig,
|
||||
#[device_config(flatten)]
|
||||
|
@ -29,14 +29,14 @@ pub struct WakeOnLANConfig {
|
|||
pub client: WrappedAsyncClient,
|
||||
}
|
||||
|
||||
#[derive(Debug, LuaDevice)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct WakeOnLAN {
|
||||
config: WakeOnLANConfig,
|
||||
config: Config,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl LuaDeviceCreate for WakeOnLAN {
|
||||
type Config = WakeOnLANConfig;
|
||||
type Config = Config;
|
||||
type Error = rumqttc::ClientError;
|
||||
|
||||
async fn create(config: Self::Config) -> Result<Self, Self::Error> {
|
||||
|
@ -59,7 +59,7 @@ impl Device for WakeOnLAN {
|
|||
|
||||
#[async_trait]
|
||||
impl OnMqtt for WakeOnLAN {
|
||||
async fn on_mqtt(&mut self, message: Publish) {
|
||||
async fn on_mqtt(&self, message: Publish) {
|
||||
if !rumqttc::matches(&message.topic, &self.config.mqtt.topic) {
|
||||
return;
|
||||
}
|
||||
|
@ -103,7 +103,7 @@ impl google_home::Device for WakeOnLAN {
|
|||
|
||||
#[async_trait]
|
||||
impl traits::Scene for WakeOnLAN {
|
||||
async fn set_active(&mut self, deactivate: bool) -> Result<(), ErrorCode> {
|
||||
async fn set_active(&self, deactivate: bool) -> Result<(), ErrorCode> {
|
||||
if deactivate {
|
||||
debug!(
|
||||
id = Device::get_id(self),
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use automation_macro::{LuaDevice, LuaDeviceConfig};
|
||||
use automation_macro::LuaDeviceConfig;
|
||||
use rumqttc::Publish;
|
||||
use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||
use tracing::{debug, error, trace, warn};
|
||||
|
||||
use super::ntfy::Priority;
|
||||
|
@ -11,7 +14,7 @@ use crate::messages::PowerMessage;
|
|||
use crate::mqtt::WrappedAsyncClient;
|
||||
|
||||
#[derive(Debug, Clone, LuaDeviceConfig)]
|
||||
pub struct WasherConfig {
|
||||
pub struct Config {
|
||||
pub identifier: String,
|
||||
#[device_config(flatten)]
|
||||
pub mqtt: MqttDeviceConfig,
|
||||
|
@ -23,17 +26,31 @@ pub struct WasherConfig {
|
|||
pub client: WrappedAsyncClient,
|
||||
}
|
||||
|
||||
// TODO: Add google home integration
|
||||
#[derive(Debug, LuaDevice)]
|
||||
pub struct Washer {
|
||||
config: WasherConfig,
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct State {
|
||||
running: isize,
|
||||
}
|
||||
|
||||
// TODO: Add google home integration
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Washer {
|
||||
config: Config,
|
||||
state: Arc<RwLock<State>>,
|
||||
}
|
||||
|
||||
impl Washer {
|
||||
async fn state(&self) -> RwLockReadGuard<State> {
|
||||
self.state.read().await
|
||||
}
|
||||
|
||||
async fn state_mut(&self) -> RwLockWriteGuard<State> {
|
||||
self.state.write().await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl LuaDeviceCreate for Washer {
|
||||
type Config = WasherConfig;
|
||||
type Config = Config;
|
||||
type Error = rumqttc::ClientError;
|
||||
|
||||
async fn create(config: Self::Config) -> Result<Self, Self::Error> {
|
||||
|
@ -44,7 +61,10 @@ impl LuaDeviceCreate for Washer {
|
|||
.subscribe(&config.mqtt.topic, rumqttc::QoS::AtLeastOnce)
|
||||
.await?;
|
||||
|
||||
Ok(Self { config, running: 0 })
|
||||
let state = State { running: 0 };
|
||||
let state = Arc::new(RwLock::new(state));
|
||||
|
||||
Ok(Self { config, state })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -61,7 +81,7 @@ const HYSTERESIS: isize = 10;
|
|||
|
||||
#[async_trait]
|
||||
impl OnMqtt for Washer {
|
||||
async fn on_mqtt(&mut self, message: Publish) {
|
||||
async fn on_mqtt(&self, message: Publish) {
|
||||
if !rumqttc::matches(&message.topic, &self.config.mqtt.topic) {
|
||||
return;
|
||||
}
|
||||
|
@ -79,7 +99,7 @@ impl OnMqtt for Washer {
|
|||
|
||||
// debug!(id = self.identifier, power, "Washer state update");
|
||||
|
||||
if power < self.config.threshold && self.running >= HYSTERESIS {
|
||||
if power < self.config.threshold && self.state().await.running >= HYSTERESIS {
|
||||
// The washer is done running
|
||||
debug!(
|
||||
id = self.config.identifier,
|
||||
|
@ -88,7 +108,7 @@ impl OnMqtt for Washer {
|
|||
"Washer is done"
|
||||
);
|
||||
|
||||
self.running = 0;
|
||||
self.state_mut().await.running = 0;
|
||||
let notification = Notification::new()
|
||||
.set_title("Laundy is done")
|
||||
.set_message("Don't forget to hang it!")
|
||||
|
@ -106,8 +126,8 @@ impl OnMqtt for Washer {
|
|||
}
|
||||
} else if power < self.config.threshold {
|
||||
// Prevent false positives
|
||||
self.running = 0;
|
||||
} else if power >= self.config.threshold && self.running < HYSTERESIS {
|
||||
self.state_mut().await.running = 0;
|
||||
} else if power >= self.config.threshold && self.state().await.running < HYSTERESIS {
|
||||
// Washer could be starting
|
||||
debug!(
|
||||
id = self.config.identifier,
|
||||
|
@ -116,7 +136,7 @@ impl OnMqtt for Washer {
|
|||
"Washer is starting"
|
||||
);
|
||||
|
||||
self.running += 1;
|
||||
self.state_mut().await.running += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,20 +36,20 @@ impl mlua::UserData for EventChannel {}
|
|||
#[async_trait]
|
||||
pub trait OnMqtt: Sync + Send {
|
||||
// fn topics(&self) -> Vec<&str>;
|
||||
async fn on_mqtt(&mut self, message: Publish);
|
||||
async fn on_mqtt(&self, message: Publish);
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait OnPresence: Sync + Send {
|
||||
async fn on_presence(&mut self, presence: bool);
|
||||
async fn on_presence(&self, presence: bool);
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait OnDarkness: Sync + Send {
|
||||
async fn on_darkness(&mut self, dark: bool);
|
||||
async fn on_darkness(&self, dark: bool);
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait OnNotification: Sync + Send {
|
||||
async fn on_notification(&mut self, notification: Notification);
|
||||
async fn on_notification(&self, notification: Notification);
|
||||
}
|
||||
|
|
46
src/main.rs
46
src/main.rs
|
@ -1,4 +1,3 @@
|
|||
#![feature(async_closure)]
|
||||
use std::path::Path;
|
||||
use std::process;
|
||||
|
||||
|
@ -9,13 +8,12 @@ use automation::device_manager::DeviceManager;
|
|||
use automation::error::ApiError;
|
||||
use automation::mqtt::{self, WrappedAsyncClient};
|
||||
use automation::{devices, LUA};
|
||||
use axum::extract::FromRef;
|
||||
use axum::extract::{FromRef, State};
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::IntoResponse;
|
||||
use axum::routing::post;
|
||||
use axum::{Json, Router};
|
||||
use dotenvy::dotenv;
|
||||
use google_home::{GoogleHome, Request};
|
||||
use google_home::{GoogleHome, Request, Response};
|
||||
use mlua::LuaSerdeExt;
|
||||
use rumqttc::AsyncClient;
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
@ -23,6 +21,7 @@ use tracing::{debug, error, info, warn};
|
|||
#[derive(Clone)]
|
||||
struct AppState {
|
||||
pub openid_url: String,
|
||||
pub device_manager: DeviceManager,
|
||||
}
|
||||
|
||||
impl FromRef<AppState> for String {
|
||||
|
@ -44,6 +43,24 @@ async fn main() {
|
|||
}
|
||||
}
|
||||
|
||||
async fn fulfillment(
|
||||
State(state): State<AppState>,
|
||||
user: User,
|
||||
Json(payload): Json<Request>,
|
||||
) -> Result<Json<Response>, ApiError> {
|
||||
debug!(username = user.preferred_username, "{payload:#?}");
|
||||
let gc = GoogleHome::new(&user.preferred_username);
|
||||
let devices = state.device_manager.devices().await;
|
||||
let result = gc
|
||||
.handle_request(payload, &devices)
|
||||
.await
|
||||
.map_err(|err| ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, err.into()))?;
|
||||
|
||||
debug!(username = user.preferred_username, "{result:#?}");
|
||||
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
async fn app() -> anyhow::Result<()> {
|
||||
dotenv().ok();
|
||||
|
||||
|
@ -119,31 +136,14 @@ async fn app() -> anyhow::Result<()> {
|
|||
};
|
||||
|
||||
// Create google home fulfillment route
|
||||
let fulfillment = Router::new().route(
|
||||
"/google_home",
|
||||
post(async move |user: User, Json(payload): Json<Request>| {
|
||||
debug!(username = user.preferred_username, "{payload:#?}");
|
||||
let gc = GoogleHome::new(&user.preferred_username);
|
||||
let devices = device_manager.devices().await;
|
||||
let result = match gc.handle_request(payload, &devices).await {
|
||||
Ok(result) => result,
|
||||
Err(err) => {
|
||||
return ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, err.into())
|
||||
.into_response()
|
||||
}
|
||||
};
|
||||
|
||||
debug!(username = user.preferred_username, "{result:#?}");
|
||||
|
||||
(StatusCode::OK, Json(result)).into_response()
|
||||
}),
|
||||
);
|
||||
let fulfillment = Router::new().route("/google_home", post(fulfillment));
|
||||
|
||||
// Combine together all the routes
|
||||
let app = Router::new()
|
||||
.nest("/fulfillment", fulfillment)
|
||||
.with_state(AppState {
|
||||
openid_url: fulfillment_config.openid_url.clone(),
|
||||
device_manager,
|
||||
});
|
||||
|
||||
// Start the web server
|
||||
|
|
|
@ -5,6 +5,6 @@ use async_trait::async_trait;
|
|||
|
||||
#[async_trait]
|
||||
pub trait Timeout: Sync + Send {
|
||||
async fn start_timeout(&mut self, _timeout: Duration) -> Result<()>;
|
||||
async fn stop_timeout(&mut self) -> Result<()>;
|
||||
async fn start_timeout(&self, _timeout: Duration) -> Result<()>;
|
||||
async fn stop_timeout(&self) -> Result<()>;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user