Everything needed to construct a new device is passed in through lua

This commit is contained in:
Dreaded_X 2024-04-25 01:35:23 +02:00
parent bfc73c7bd3
commit f4a1b507e5
Signed by: Dreaded_X
GPG Key ID: FA5F485356B0D2D4
22 changed files with 493 additions and 259 deletions

1
Cargo.lock generated
View File

@ -114,6 +114,7 @@ version = "0.1.0"
name = "automation_macro"
version = "0.1.0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.60",
]

View File

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

View File

@ -1,17 +1,17 @@
use proc_macro::TokenStream;
use proc_macro2::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
use syn::punctuated::Punctuated;
use syn::{parse_macro_input, DeriveInput, Token};
#[proc_macro_derive(LuaDevice, attributes(config))]
pub fn lua_device_derive(input: TokenStream) -> TokenStream {
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)
impl_lua_device_macro(&ast).into()
}
fn impl_lua_device_macro(ast: &syn::DeriveInput) -> TokenStream {
let name = &ast.ident;
let name_string = name.to_string();
// TODO: Handle errors properly
// This includes making sure one, and only one config is specified
let config = if let syn::Data::Struct(syn::DataStruct {
@ -36,13 +36,13 @@ fn impl_lua_device_macro(ast: &syn::DeriveInput) -> TokenStream {
let gen = quote! {
impl #name {
pub fn register_with_lua(lua: &mlua::Lua) -> mlua::Result<()> {
lua.globals().set(#name_string, lua.create_proxy::<#name>()?)
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_function("new", |lua, config: mlua::Value| {
let config: #config = mlua::LuaSerdeExt::from_value(lua, config)?;
let config: #config = mlua::FromLua::from_lua(config, lua)?;
let config: Box<dyn crate::device_manager::DeviceConfig> = Box::new(config);
Ok(config)
});
@ -50,5 +50,249 @@ fn impl_lua_device_macro(ast: &syn::DeriveInput) -> TokenStream {
}
};
gen.into()
gen
}
#[derive(Debug)]
enum Arg {
Flatten,
UserData,
With(TokenStream),
Default(Option<syn::Ident>),
}
impl syn::parse::Parse for Arg {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let arg = match input.parse::<syn::Ident>()?.to_string().as_str() {
"flatten" => Arg::Flatten,
"user_data" => Arg::UserData,
"with" => {
input.parse::<Token![=]>()?;
let lit = input.parse::<syn::Lit>()?;
if let syn::Lit::Str(lit_str) = lit {
let token_stream: TokenStream = lit_str.parse()?;
Arg::With(token_stream)
} else {
panic!("Expected literal string");
}
}
"default" => {
if input.parse::<Token![=]>().is_ok() {
let func = input.parse::<syn::Ident>()?;
Arg::Default(Some(func))
} else {
Arg::Default(None)
}
}
name => todo!("Handle unknown arg: {name}"),
};
Ok(arg)
}
}
#[derive(Debug)]
struct ArgsParser {
args: Punctuated<Arg, Token![,]>,
}
impl syn::parse::Parse for ArgsParser {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let args = input.parse_terminated(Arg::parse, Token![,])?;
Ok(Self { args })
}
}
#[derive(Debug)]
struct Args {
flatten: bool,
user_data: bool,
with: Option<TokenStream>,
default: Option<Option<syn::Ident>>,
}
impl Args {
fn new(args: Vec<Arg>) -> Self {
let mut result = Args {
flatten: false,
user_data: false,
with: None,
default: None,
};
for arg in args {
match arg {
Arg::Flatten => {
if result.flatten {
panic!("Option 'flatten' is already set")
}
result.flatten = true
}
Arg::UserData => {
if result.flatten {
panic!("Option 'user_data' is already set")
}
result.user_data = true
}
Arg::With(ty) => {
if result.with.is_some() {
panic!("Option 'with' is already set")
}
result.with = Some(ty)
}
Arg::Default(func) => {
if result.default.is_some() {
panic!("Option 'default' is already set")
}
result.default = Some(func)
}
}
}
if result.flatten && result.user_data {
panic!("The options 'flatten' and 'user_data' conflict with each other")
}
if result.flatten && result.default.is_some() {
panic!("The options 'flatten' and 'default' conflict with each other")
}
result
}
}
#[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);
impl_lua_device_config_macro(&ast).into()
}
// struct Args
fn impl_lua_device_config_macro(ast: &syn::DeriveInput) -> TokenStream {
let name = &ast.ident;
// TODO: Handle errors properly
// This includes making sure one, and only one config is specified
let fields = if let syn::Data::Struct(syn::DataStruct {
fields: syn::Fields::Named(syn::FieldsNamed { ref named, .. }),
..
}) = ast.data
{
named
} else {
unimplemented!("Macro can only handle named structs");
};
let fields: Vec<_> = fields
.iter()
.map(|field| {
let field_name = field.ident.clone().unwrap();
let args: Vec<_> = field
.attrs
.iter()
.filter_map(|attr| {
if attr.path().is_ident("device_config") {
let args: ArgsParser = attr.parse_args().unwrap();
Some(args.args)
} else {
None
}
})
.flatten()
.collect();
let args = Args::new(args);
// TODO: Improve how optional fields are detected
let optional = if let syn::Type::Path(path) = field.ty.clone() {
path.path.segments.first().unwrap().ident == "Option"
} else {
false
};
let default = if optional {
quote! { None }
} else if let Some(func) = args.default {
if func.is_some() {
quote! { #func() }
} else {
quote! { Default::default() }
}
} else {
let missing = format!("Missing field '{field_name}'");
quote! { panic!(#missing) }
};
let value = if args.flatten {
// println!("ValueFlatten: {}", field_name);
quote! {
mlua::LuaSerdeExt::from_value_with(lua, value.clone(), mlua::DeserializeOptions::new().deny_unsupported_types(false))?
}
} else if args.user_data {
// println!("UserData: {}", field_name);
quote! {
if table.contains_key(stringify!(#field_name))? {
table.get(stringify!(#field_name))?
} else {
#default
}
}
} else {
// println!("Value: {}", field_name);
quote! {
{
let #field_name: mlua::Value = table.get(stringify!(#field_name))?;
if !#field_name.is_nil() {
mlua::LuaSerdeExt::from_value(lua, #field_name)?
} else {
#default
}
}
}
};
let value = if let Some(temp_type) = args.with {
if optional {
quote! {
{
let temp: #temp_type = #value;
temp.map(|v| v.into())
}
}
} else {
quote! {
{
let temp: #temp_type = #value;
temp.into()
}
}
}
} else {
value
};
quote! {
#field_name: #value
}
})
.collect();
let gen = quote! {
impl<'lua> mlua::FromLua<'lua> for #name {
fn from_lua(value: mlua::Value<'lua>, lua: &'lua mlua::Lua) -> mlua::Result<Self> {
if !value.is_table() {
panic!("Expected table");
}
let table = value.as_table().unwrap();
Ok(#name {
#(#fields,)*
})
}
}
};
gen
}

View File

@ -17,6 +17,7 @@ automation.device_manager:create(
"debug_bridge",
DebugBridge.new({
topic = mqtt_automation("debug"),
client = automation.mqtt_client,
})
)
@ -41,6 +42,7 @@ automation.device_manager:create(
topic = mqtt_z2m("living/light"),
min = 22000,
max = 23500,
event_channel = automation.event_channel,
})
)
@ -74,6 +76,7 @@ automation.device_manager:create(
name = "Kettle",
room = "Kitchen",
topic = mqtt_z2m("kitchen/kettle"),
client = automation.mqtt_client,
timeout = debug and 5 or 300,
remotes = {
{ topic = mqtt_z2m("bedroom/remote") },
@ -89,6 +92,7 @@ automation.device_manager:create(
name = "Light",
room = "Bathroom",
topic = mqtt_z2m("batchroom/light"),
client = automation.mqtt_client,
timeout = debug and 60 or 45 * 60,
})
)
@ -98,6 +102,7 @@ automation.device_manager:create(
Washer.new({
topic = mqtt_z2m("batchroom/washer"),
threshold = 1,
event_channel = automation.event_channel,
})
)
@ -108,6 +113,7 @@ automation.device_manager:create(
name = "Charger",
room = "Workbench",
topic = mqtt_z2m("workbench/charger"),
client = automation.mqtt_client,
timeout = debug and 5 or 20 * 3600,
})
)
@ -118,6 +124,7 @@ automation.device_manager:create(
name = "Outlet",
room = "Workbench",
topic = mqtt_z2m("workbench/outlet"),
client = automation.mqtt_client,
})
)
@ -139,6 +146,7 @@ automation.device_manager:create(
"hallway_frontdoor",
ContactSensor.new({
topic = mqtt_z2m("hallway/frontdoor"),
client = automation.mqtt_client,
presence = {
topic = mqtt_automation("presence/contact/frontdoor"),
timeout = debug and 10 or 15 * 60,
@ -156,5 +164,6 @@ automation.device_manager:create(
name = "Air Filter",
room = "Bedroom",
topic = "pico/filter/bedroom",
client = automation.mqtt_client,
})
)

View File

@ -1,10 +1,12 @@
use std::collections::HashMap;
use std::ops::{Deref, DerefMut};
use std::sync::Arc;
use async_trait::async_trait;
use enum_dispatch::enum_dispatch;
use futures::future::join_all;
use google_home::traits::OnOff;
use mlua::FromLua;
use rumqttc::{matches, AsyncClient, QoS};
use tokio::sync::{RwLock, RwLockReadGuard};
use tokio_cron_scheduler::{Job, JobScheduler};
@ -15,25 +17,38 @@ use crate::error::DeviceConfigError;
use crate::event::{Event, EventChannel, OnDarkness, OnMqtt, OnNotification, OnPresence};
use crate::schedule::{Action, Schedule};
pub struct ConfigExternal<'a> {
pub client: &'a AsyncClient,
pub device_manager: &'a DeviceManager,
pub event_channel: &'a EventChannel,
}
#[async_trait]
#[enum_dispatch]
pub trait DeviceConfig {
async fn create(
&self,
identifier: &str,
ext: &ConfigExternal,
) -> Result<Box<dyn Device>, DeviceConfigError>;
async fn create(&self, identifier: &str) -> Result<Box<dyn Device>, DeviceConfigError>;
}
impl mlua::UserData for Box<dyn DeviceConfig> {}
pub type WrappedDevice = Arc<RwLock<Box<dyn Device>>>;
pub type DeviceMap = HashMap<String, WrappedDevice>;
#[derive(Debug, FromLua, Clone)]
pub struct WrappedDevice(Arc<RwLock<Box<dyn Device>>>);
impl WrappedDevice {
fn new(device: Box<dyn Device>) -> Self {
Self(Arc::new(RwLock::new(device)))
}
}
impl Deref for WrappedDevice {
type Target = Arc<RwLock<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 {}
pub type DeviceMap = HashMap<String, Arc<RwLock<Box<dyn Device>>>>;
#[derive(Debug, Clone)]
pub struct DeviceManager {
@ -117,7 +132,7 @@ impl DeviceManager {
sched.start().await.unwrap();
}
pub async fn add(&self, device: Box<dyn Device>) {
pub async fn add(&self, device: Box<dyn Device>) -> WrappedDevice {
let id = device.get_id().into();
debug!(id, "Adding device");
@ -135,9 +150,11 @@ impl DeviceManager {
}
// Wrap the device
let device = Arc::new(RwLock::new(device));
let device = WrappedDevice::new(device);
self.devices.write().await.insert(id, device);
self.devices.write().await.insert(id, device.0.clone());
device
}
pub fn event_channel(&self) -> EventChannel {
@ -145,7 +162,12 @@ impl DeviceManager {
}
pub async fn get(&self, name: &str) -> Option<WrappedDevice> {
self.devices.read().await.get(name).cloned()
self.devices
.read()
.await
.get(name)
.cloned()
.map(WrappedDevice)
}
pub async fn devices(&self) -> RwLockReadGuard<DeviceMap> {
@ -232,22 +254,12 @@ impl mlua::UserData for DeviceManager {
// TODO: Handle the error here properly
let config: Box<dyn DeviceConfig> = config.as_userdata().unwrap().take()?;
let ext = ConfigExternal {
client: &this.client,
device_manager: this,
event_channel: &this.event_channel,
};
let device = config
.create(&identifier, &ext)
.create(&identifier)
.await
.map_err(mlua::ExternalError::into_lua_err)?;
let id = device.get_id().to_owned();
this.add(device).await;
Ok(id)
Ok(this.add(device).await)
},
)
}

View File

@ -1,40 +1,37 @@
use async_trait::async_trait;
use automation_macro::LuaDevice;
use automation_macro::{LuaDevice, LuaDeviceConfig};
use google_home::device::Name;
use google_home::errors::ErrorCode;
use google_home::traits::{AvailableSpeeds, FanSpeed, HumiditySetting, OnOff, Speed, SpeedValues};
use google_home::types::Type;
use google_home::GoogleHomeDevice;
use rumqttc::{AsyncClient, Publish};
use serde::Deserialize;
use rumqttc::Publish;
use tracing::{debug, error, warn};
use crate::config::{InfoConfig, MqttDeviceConfig};
use crate::device_manager::{ConfigExternal, DeviceConfig};
use crate::device_manager::DeviceConfig;
use crate::devices::Device;
use crate::error::DeviceConfigError;
use crate::event::OnMqtt;
use crate::messages::{AirFilterFanState, AirFilterState, SetAirFilterFanState};
use crate::mqtt::WrappedAsyncClient;
#[derive(Debug, Deserialize, Clone)]
#[derive(Debug, Clone, LuaDeviceConfig)]
pub struct AirFilterConfig {
#[serde(flatten)]
#[device_config(flatten)]
info: InfoConfig,
#[serde(flatten)]
#[device_config(flatten)]
mqtt: MqttDeviceConfig,
#[device_config(user_data)]
client: WrappedAsyncClient,
}
#[async_trait]
impl DeviceConfig for AirFilterConfig {
async fn create(
&self,
identifier: &str,
ext: &ConfigExternal,
) -> Result<Box<dyn Device>, DeviceConfigError> {
async fn create(&self, identifier: &str) -> Result<Box<dyn Device>, DeviceConfigError> {
let device = AirFilter {
identifier: identifier.into(),
config: self.clone(),
client: ext.client.clone(),
last_known_state: AirFilterState {
state: AirFilterFanState::Off,
humidity: 0.0,
@ -51,7 +48,6 @@ pub struct AirFilter {
#[config]
config: AirFilterConfig,
client: AsyncClient,
last_known_state: AirFilterState,
}
@ -61,7 +57,8 @@ impl AirFilter {
let topic = format!("{}/set", self.config.mqtt.topic);
// TODO: Handle potential errors here
self.client
self.config
.client
.publish(
topic.clone(),
rumqttc::QoS::AtLeastOnce,

View File

@ -1,78 +1,45 @@
use async_trait::async_trait;
use automation_macro::LuaDevice;
use automation_macro::{LuaDevice, LuaDeviceConfig};
use google_home::traits::OnOff;
use serde::Deserialize;
use tracing::{debug, error, trace, warn};
use super::Device;
use crate::config::MqttDeviceConfig;
use crate::device_manager::{ConfigExternal, DeviceConfig, WrappedDevice};
use crate::device_manager::{DeviceConfig, WrappedDevice};
use crate::error::DeviceConfigError;
use crate::event::{OnMqtt, OnPresence};
use crate::messages::{RemoteAction, RemoteMessage};
#[derive(Debug, Clone, Deserialize)]
#[derive(Debug, Clone, LuaDeviceConfig)]
pub struct AudioSetupConfig {
#[serde(flatten)]
#[device_config(flatten)]
mqtt: MqttDeviceConfig,
mixer: String,
speakers: String,
#[device_config(user_data)]
mixer: WrappedDevice,
#[device_config(user_data)]
speakers: WrappedDevice,
}
#[async_trait]
impl DeviceConfig for AudioSetupConfig {
async fn create(
&self,
identifier: &str,
ext: &ConfigExternal,
) -> Result<Box<dyn Device>, DeviceConfigError> {
async fn create(&self, identifier: &str) -> Result<Box<dyn Device>, DeviceConfigError> {
trace!(id = identifier, "Setting up AudioSetup");
// TODO: Make sure they implement OnOff?
let mixer = ext
.device_manager
.get(&self.mixer)
.await
// NOTE: We need to clone to make the compiler happy, how ever if this clone happens the next one can never happen...
.ok_or(DeviceConfigError::MissingChild(
identifier.into(),
self.mixer.clone(),
))?;
{
let mixer = mixer.read().await;
if (mixer.as_ref().cast() as Option<&dyn OnOff>).is_none() {
return Err(DeviceConfigError::MissingTrait(
self.mixer.clone(),
"OnOff".into(),
));
}
let mixer = self.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 =
ext.device_manager
.get(&self.speakers)
.await
.ok_or(DeviceConfigError::MissingChild(
identifier.into(),
self.speakers.clone(),
))?;
{
let speakers = speakers.read().await;
if (speakers.as_ref().cast() as Option<&dyn OnOff>).is_none() {
return Err(DeviceConfigError::MissingTrait(
self.mixer.clone(),
"OnOff".into(),
));
}
let speakers = self.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()));
}
let device = AudioSetup {
identifier: identifier.into(),
config: self.clone(),
mixer,
speakers,
};
Ok(Box::new(device))
@ -85,8 +52,6 @@ pub struct AudioSetup {
identifier: String,
#[config]
config: AudioSetupConfig,
mixer: WrappedDevice,
speakers: WrappedDevice,
}
impl Device for AudioSetup {
@ -110,8 +75,8 @@ impl OnMqtt for AudioSetup {
}
};
let mut mixer = self.mixer.write().await;
let mut speakers = self.speakers.write().await;
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>,
@ -145,8 +110,9 @@ impl OnMqtt for AudioSetup {
#[async_trait]
impl OnPresence for AudioSetup {
async fn on_presence(&mut self, presence: bool) {
let mut mixer = self.mixer.write().await;
let mut speakers = self.speakers.write().await;
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>,

View File

@ -1,86 +1,74 @@
use std::time::Duration;
use async_trait::async_trait;
use automation_macro::LuaDevice;
use automation_macro::{LuaDevice, LuaDeviceConfig};
use google_home::traits::OnOff;
use rumqttc::AsyncClient;
use serde::Deserialize;
use serde_with::{serde_as, DurationSeconds};
use tokio::task::JoinHandle;
use tracing::{debug, error, trace, warn};
use super::Device;
use crate::config::MqttDeviceConfig;
use crate::device_manager::{ConfigExternal, DeviceConfig, WrappedDevice};
use crate::device_manager::{DeviceConfig, WrappedDevice};
use crate::devices::DEFAULT_PRESENCE;
use crate::error::DeviceConfigError;
use crate::event::{OnMqtt, OnPresence};
use crate::helper::DurationSeconds;
use crate::messages::{ContactMessage, PresenceMessage};
use crate::mqtt::WrappedAsyncClient;
use crate::traits::Timeout;
// NOTE: If we add more presence devices we might need to move this out of here
#[serde_as]
#[derive(Debug, Clone, Deserialize)]
#[derive(Debug, Clone, LuaDeviceConfig)]
pub struct PresenceDeviceConfig {
#[serde(flatten)]
#[device_config(flatten)]
pub mqtt: MqttDeviceConfig,
#[serde_as(as = "DurationSeconds")]
#[device_config(with = "DurationSeconds")]
pub timeout: Duration,
}
#[serde_as]
#[derive(Debug, Clone, Deserialize)]
#[derive(Debug, Clone, LuaDeviceConfig)]
pub struct TriggerConfig {
devices: Vec<String>,
#[serde(default)]
#[serde_as(as = "DurationSeconds")]
pub timeout: Duration,
#[device_config(user_data)]
devices: Vec<WrappedDevice>,
#[device_config(with = "Option<DurationSeconds>")]
pub timeout: Option<Duration>,
}
#[derive(Debug, Clone, Deserialize)]
#[derive(Debug, Clone, LuaDeviceConfig)]
pub struct ContactSensorConfig {
#[serde(flatten)]
#[device_config(flatten)]
mqtt: MqttDeviceConfig,
#[device_config(user_data)]
presence: Option<PresenceDeviceConfig>,
#[device_config(user_data)]
trigger: Option<TriggerConfig>,
#[device_config(user_data)]
client: WrappedAsyncClient,
}
#[async_trait]
impl DeviceConfig for ContactSensorConfig {
async fn create(
&self,
identifier: &str,
ext: &ConfigExternal,
) -> Result<Box<dyn Device>, DeviceConfigError> {
async fn create(&self, identifier: &str) -> Result<Box<dyn Device>, DeviceConfigError> {
trace!(id = identifier, "Setting up ContactSensor");
let trigger = if let Some(trigger_config) = &self.trigger {
let mut devices = Vec::new();
for device_name in &trigger_config.devices {
let device = ext.device_manager.get(device_name).await.ok_or(
DeviceConfigError::MissingChild(device_name.into(), "OnOff".into()),
)?;
for device in &trigger_config.devices {
{
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(
device_name.into(),
"OnOff".into(),
));
return Err(DeviceConfigError::MissingTrait(id, "OnOff".into()));
}
if trigger_config.timeout.is_zero()
if trigger_config.timeout.is_none()
&& (device.as_ref().cast() as Option<&dyn Timeout>).is_none()
{
return Err(DeviceConfigError::MissingTrait(
device_name.into(),
"Timeout".into(),
));
return Err(DeviceConfigError::MissingTrait(id, "Timeout".into()));
}
}
devices.push((device, false));
devices.push((device.clone(), false));
}
Some(Trigger {
@ -94,7 +82,6 @@ impl DeviceConfig for ContactSensorConfig {
let device = ContactSensor {
identifier: identifier.into(),
config: self.clone(),
client: ext.client.clone(),
overall_presence: DEFAULT_PRESENCE,
is_closed: true,
handle: None,
@ -108,7 +95,7 @@ impl DeviceConfig for ContactSensorConfig {
#[derive(Debug)]
struct Trigger {
devices: Vec<(WrappedDevice, bool)>,
timeout: Duration, // Timeout in seconds
timeout: Option<Duration>,
}
#[derive(Debug, LuaDevice)]
@ -117,7 +104,6 @@ pub struct ContactSensor {
#[config]
config: ContactSensorConfig,
client: AsyncClient,
overall_presence: bool,
is_closed: bool,
handle: Option<JoinHandle<()>>,
@ -174,14 +160,15 @@ impl OnMqtt for ContactSensor {
let mut light = light.write().await;
if !previous {
// If the timeout is zero just turn the light off directly
if trigger.timeout.is_zero()
if trigger.timeout.is_none()
&& let Some(light) = light.as_mut().cast_mut() as Option<&mut dyn OnOff>
{
light.set_on(false).await.ok();
} else if let Some(light) =
light.as_mut().cast_mut() as Option<&mut dyn Timeout>
} else if let Some(timeout) = trigger.timeout
&& let Some(light) =
light.as_mut().cast_mut() as Option<&mut dyn Timeout>
{
light.start_timeout(trigger.timeout).await.unwrap();
light.start_timeout(timeout).await.unwrap();
}
// TODO: Put a warning/error on creation if either of this has to option to fail
}
@ -206,7 +193,8 @@ impl OnMqtt for ContactSensor {
// 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 {
self.client
self.config
.client
.publish(
presence.mqtt.topic.clone(),
rumqttc::QoS::AtLeastOnce,
@ -224,7 +212,7 @@ impl OnMqtt for ContactSensor {
}
} else {
// Once the door is closed again we start a timeout for removing the presence
let client = self.client.clone();
let client = self.config.client.clone();
let id = self.identifier.clone();
let timeout = presence.timeout;
let topic = presence.mqtt.topic.clone();

View File

@ -1,33 +1,29 @@
use async_trait::async_trait;
use automation_macro::LuaDevice;
use rumqttc::AsyncClient;
use serde::Deserialize;
use automation_macro::{LuaDevice, LuaDeviceConfig};
use tracing::warn;
use crate::config::MqttDeviceConfig;
use crate::device_manager::{ConfigExternal, DeviceConfig};
use crate::device_manager::DeviceConfig;
use crate::devices::Device;
use crate::error::DeviceConfigError;
use crate::event::{OnDarkness, OnPresence};
use crate::messages::{DarknessMessage, PresenceMessage};
use crate::mqtt::WrappedAsyncClient;
#[derive(Debug, Deserialize, Clone)]
#[derive(Debug, LuaDeviceConfig, Clone)]
pub struct DebugBridgeConfig {
#[serde(flatten)]
#[device_config(flatten)]
pub mqtt: MqttDeviceConfig,
#[device_config(user_data)]
client: WrappedAsyncClient,
}
#[async_trait]
impl DeviceConfig for DebugBridgeConfig {
async fn create(
&self,
identifier: &str,
ext: &ConfigExternal,
) -> Result<Box<dyn Device>, DeviceConfigError> {
async fn create(&self, identifier: &str) -> Result<Box<dyn Device>, DeviceConfigError> {
let device = DebugBridge {
identifier: identifier.into(),
config: self.clone(),
client: ext.client.clone(),
};
Ok(Box::new(device))
@ -39,7 +35,6 @@ pub struct DebugBridge {
identifier: String,
#[config]
config: DebugBridgeConfig,
client: AsyncClient,
}
impl Device for DebugBridge {
@ -53,7 +48,8 @@ impl OnPresence for DebugBridge {
async fn on_presence(&mut self, presence: bool) {
let message = PresenceMessage::new(presence);
let topic = format!("{}/presence", self.config.mqtt.topic);
self.client
self.config
.client
.publish(
topic,
rumqttc::QoS::AtLeastOnce,
@ -76,7 +72,8 @@ impl OnDarkness for DebugBridge {
async fn on_darkness(&mut self, dark: bool) {
let message = DarknessMessage::new(dark);
let topic = format!("{}/darkness", self.config.mqtt.topic);
self.client
self.config
.client
.publish(
topic,
rumqttc::QoS::AtLeastOnce,

View File

@ -1,11 +1,11 @@
use std::net::Ipv4Addr;
use async_trait::async_trait;
use automation_macro::LuaDevice;
use automation_macro::{LuaDevice, LuaDeviceConfig};
use serde::{Deserialize, Serialize};
use tracing::{error, trace, warn};
use crate::device_manager::{ConfigExternal, DeviceConfig};
use crate::device_manager::DeviceConfig;
use crate::devices::Device;
use crate::error::DeviceConfigError;
use crate::event::{OnDarkness, OnPresence};
@ -22,8 +22,9 @@ pub struct FlagIDs {
pub darkness: isize,
}
#[derive(Debug, Deserialize, Clone)]
#[derive(Debug, LuaDeviceConfig, Clone)]
pub struct HueBridgeConfig {
// TODO: Add helper type that converts this to a socketaddr automatically
pub ip: Ipv4Addr,
pub login: String,
pub flags: FlagIDs,
@ -31,11 +32,7 @@ pub struct HueBridgeConfig {
#[async_trait]
impl DeviceConfig for HueBridgeConfig {
async fn create(
&self,
identifier: &str,
_ext: &ConfigExternal,
) -> Result<Box<dyn Device>, DeviceConfigError> {
async fn create(&self, identifier: &str) -> Result<Box<dyn Device>, DeviceConfigError> {
let device = HueBridge {
identifier: identifier.into(),
config: self.clone(),

View File

@ -3,39 +3,35 @@ use std::time::Duration;
use anyhow::{anyhow, Context, Result};
use async_trait::async_trait;
use automation_macro::LuaDevice;
use automation_macro::{LuaDevice, LuaDeviceConfig};
use google_home::errors::ErrorCode;
use google_home::traits::OnOff;
use rumqttc::Publish;
use serde::Deserialize;
use tracing::{debug, error, warn};
use super::Device;
use crate::config::MqttDeviceConfig;
use crate::device_manager::{ConfigExternal, DeviceConfig};
use crate::device_manager::DeviceConfig;
use crate::error::DeviceConfigError;
use crate::event::OnMqtt;
use crate::messages::{RemoteAction, RemoteMessage};
use crate::traits::Timeout;
#[derive(Debug, Clone, Deserialize)]
#[derive(Debug, Clone, LuaDeviceConfig)]
pub struct HueGroupConfig {
// TODO: Add helper type that converts this to a socketaddr automatically
pub ip: Ipv4Addr,
pub login: String,
pub group_id: isize,
pub timer_id: isize,
pub scene_id: String,
#[serde(default)]
#[device_config(default)]
pub remotes: Vec<MqttDeviceConfig>,
}
#[async_trait]
impl DeviceConfig for HueGroupConfig {
async fn create(
&self,
identifier: &str,
_ext: &ConfigExternal,
) -> Result<Box<dyn Device>, DeviceConfigError> {
async fn create(&self, identifier: &str) -> Result<Box<dyn Device>, DeviceConfigError> {
let device = HueGroup {
identifier: identifier.into(),
config: self.clone(),

View File

@ -2,23 +2,24 @@ use std::time::Duration;
use anyhow::Result;
use async_trait::async_trait;
use automation_macro::LuaDevice;
use automation_macro::{LuaDevice, LuaDeviceConfig};
use google_home::errors::ErrorCode;
use google_home::traits::{self, OnOff};
use google_home::types::Type;
use google_home::{device, GoogleHomeDevice};
use rumqttc::{matches, AsyncClient, Publish};
use rumqttc::{matches, Publish};
use serde::Deserialize;
use serde_with::{serde_as, DurationSeconds};
use tokio::task::JoinHandle;
use tracing::{debug, error, trace, warn};
use crate::config::{InfoConfig, MqttDeviceConfig};
use crate::device_manager::{ConfigExternal, DeviceConfig};
use crate::device_manager::DeviceConfig;
use crate::devices::Device;
use crate::error::DeviceConfigError;
use crate::event::{OnMqtt, OnPresence};
use crate::helper::DurationSeconds;
use crate::messages::{OnOffMessage, RemoteAction, RemoteMessage};
use crate::mqtt::WrappedAsyncClient;
use crate::traits::Timeout;
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Copy)]
@ -29,19 +30,21 @@ pub enum OutletType {
Light,
}
#[serde_as]
#[derive(Debug, Clone, Deserialize)]
#[derive(Debug, Clone, LuaDeviceConfig)]
pub struct IkeaOutletConfig {
#[serde(flatten)]
#[device_config(flatten)]
info: InfoConfig,
#[serde(flatten)]
#[device_config(flatten)]
mqtt: MqttDeviceConfig,
#[serde(default = "default_outlet_type")]
#[device_config(default = default_outlet_type)]
outlet_type: OutletType,
#[serde_as(as = "Option<DurationSeconds>")]
timeout: Option<Duration>, // Timeout in seconds
#[serde(default)]
#[device_config(with = "Option<DurationSeconds>")]
timeout: Option<Duration>,
#[device_config(default)]
pub remotes: Vec<MqttDeviceConfig>,
#[device_config(user_data)]
client: WrappedAsyncClient,
}
fn default_outlet_type() -> OutletType {
@ -50,11 +53,7 @@ fn default_outlet_type() -> OutletType {
#[async_trait]
impl DeviceConfig for IkeaOutletConfig {
async fn create(
&self,
identifier: &str,
ext: &ConfigExternal,
) -> Result<Box<dyn Device>, DeviceConfigError> {
async fn create(&self, identifier: &str) -> Result<Box<dyn Device>, DeviceConfigError> {
trace!(
id = identifier,
name = self.info.name,
@ -65,7 +64,6 @@ impl DeviceConfig for IkeaOutletConfig {
let device = IkeaOutlet {
identifier: identifier.into(),
config: self.clone(),
client: ext.client.clone(),
last_known_state: false,
handle: None,
};
@ -80,12 +78,11 @@ pub struct IkeaOutlet {
#[config]
config: IkeaOutletConfig,
client: AsyncClient,
last_known_state: bool,
handle: Option<JoinHandle<()>>,
}
async fn set_on(client: AsyncClient, topic: &str, on: bool) {
async fn set_on(client: WrappedAsyncClient, topic: &str, on: bool) {
let message = OnOffMessage::new(on);
let topic = format!("{}/set", topic);
@ -219,7 +216,7 @@ impl traits::OnOff for IkeaOutlet {
}
async fn set_on(&mut self, on: bool) -> Result<(), ErrorCode> {
set_on(self.client.clone(), &self.config.mqtt.topic, on).await;
set_on(self.config.client.clone(), &self.config.mqtt.topic, on).await;
Ok(())
}
@ -234,7 +231,7 @@ impl crate::traits::Timeout for IkeaOutlet {
// 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.client.clone();
let client = self.config.client.clone();
let topic = self.config.mqtt.topic.clone();
let id = self.identifier.clone();
self.handle = Some(tokio::spawn(async move {

View File

@ -2,7 +2,7 @@ use std::net::{Ipv4Addr, SocketAddr};
use std::str::Utf8Error;
use async_trait::async_trait;
use automation_macro::LuaDevice;
use automation_macro::{LuaDevice, LuaDeviceConfig};
use bytes::{Buf, BufMut};
use google_home::errors::{self, DeviceError};
use google_home::traits;
@ -13,21 +13,18 @@ use tokio::net::TcpStream;
use tracing::trace;
use super::Device;
use crate::device_manager::{ConfigExternal, DeviceConfig};
use crate::device_manager::DeviceConfig;
use crate::error::DeviceConfigError;
#[derive(Debug, Clone, Deserialize)]
#[derive(Debug, Clone, LuaDeviceConfig)]
pub struct KasaOutletConfig {
// TODO: Add helper type that converts this to a socketaddr automatically
ip: Ipv4Addr,
}
#[async_trait]
impl DeviceConfig for KasaOutletConfig {
async fn create(
&self,
identifier: &str,
_ext: &ConfigExternal,
) -> Result<Box<dyn Device>, DeviceConfigError> {
async fn create(&self, identifier: &str) -> Result<Box<dyn Device>, DeviceConfigError> {
trace!(id = identifier, "Setting up KasaOutlet");
let device = KasaOutlet {

View File

@ -1,22 +1,23 @@
use async_trait::async_trait;
use automation_macro::LuaDevice;
use automation_macro::{LuaDevice, LuaDeviceConfig};
use rumqttc::Publish;
use serde::Deserialize;
use tracing::{debug, trace, warn};
use crate::config::MqttDeviceConfig;
use crate::device_manager::{ConfigExternal, DeviceConfig};
use crate::device_manager::DeviceConfig;
use crate::devices::Device;
use crate::error::DeviceConfigError;
use crate::event::{self, Event, OnMqtt};
use crate::event::{self, Event, EventChannel, OnMqtt};
use crate::messages::BrightnessMessage;
#[derive(Debug, Clone, Deserialize)]
#[derive(Debug, Clone, LuaDeviceConfig)]
pub struct LightSensorConfig {
#[serde(flatten)]
#[device_config(flatten)]
pub mqtt: MqttDeviceConfig,
pub min: isize,
pub max: isize,
#[device_config(user_data)]
pub event_channel: EventChannel,
}
pub const DEFAULT: bool = false;
@ -25,14 +26,11 @@ pub const DEFAULT: bool = false;
#[async_trait]
impl DeviceConfig for LightSensorConfig {
async fn create(
&self,
identifier: &str,
ext: &ConfigExternal,
) -> Result<Box<dyn Device>, DeviceConfigError> {
async fn create(&self, identifier: &str) -> Result<Box<dyn Device>, DeviceConfigError> {
let device = LightSensor {
identifier: identifier.into(),
tx: ext.event_channel.get_tx(),
// Add helper type that does this conversion for us
tx: self.event_channel.get_tx(),
config: self.clone(),
is_dark: DEFAULT,
};

View File

@ -26,7 +26,7 @@ pub use self::hue_bridge::*;
pub use self::hue_light::*;
pub use self::ikea_outlet::*;
pub use self::kasa_outlet::*;
pub use self::light_sensor::{LightSensor, LightSensorConfig};
pub use self::light_sensor::*;
pub use self::ntfy::{Notification, Ntfy};
pub use self::presence::{Presence, PresenceConfig, DEFAULT_PRESENCE};
pub use self::wake_on_lan::*;

View File

@ -1,31 +1,30 @@
use std::net::Ipv4Addr;
use async_trait::async_trait;
use automation_macro::LuaDevice;
use automation_macro::{LuaDevice, LuaDeviceConfig};
use eui48::MacAddress;
use google_home::errors::ErrorCode;
use google_home::traits::{self, Scene};
use google_home::types::Type;
use google_home::{device, GoogleHomeDevice};
use rumqttc::Publish;
use serde::Deserialize;
use tracing::{debug, error, trace};
use super::Device;
use crate::config::{InfoConfig, MqttDeviceConfig};
use crate::device_manager::{ConfigExternal, DeviceConfig};
use crate::device_manager::DeviceConfig;
use crate::error::DeviceConfigError;
use crate::event::OnMqtt;
use crate::messages::ActivateMessage;
#[derive(Debug, Clone, Deserialize)]
#[derive(Debug, Clone, LuaDeviceConfig)]
pub struct WakeOnLANConfig {
#[serde(flatten)]
#[device_config(flatten)]
info: InfoConfig,
#[serde(flatten)]
#[device_config(flatten)]
mqtt: MqttDeviceConfig,
mac_address: MacAddress,
#[serde(default = "default_broadcast_ip")]
#[device_config(default = default_broadcast_ip)]
broadcast_ip: Ipv4Addr,
}
@ -35,11 +34,7 @@ fn default_broadcast_ip() -> Ipv4Addr {
#[async_trait]
impl DeviceConfig for WakeOnLANConfig {
async fn create(
&self,
identifier: &str,
_ext: &ConfigExternal,
) -> Result<Box<dyn Device>, DeviceConfigError> {
async fn create(&self, identifier: &str) -> Result<Box<dyn Device>, DeviceConfigError> {
trace!(
id = identifier,
name = self.info.name,
@ -47,6 +42,8 @@ impl DeviceConfig for WakeOnLANConfig {
"Setting up WakeOnLAN"
);
debug!("broadcast_ip = {}", self.broadcast_ip);
let device = WakeOnLAN {
identifier: identifier.into(),
config: self.clone(),

View File

@ -1,35 +1,32 @@
use async_trait::async_trait;
use automation_macro::LuaDevice;
use automation_macro::{LuaDevice, LuaDeviceConfig};
use rumqttc::Publish;
use serde::Deserialize;
use tracing::{debug, error, warn};
use super::ntfy::Priority;
use super::{Device, Notification};
use crate::config::MqttDeviceConfig;
use crate::device_manager::{ConfigExternal, DeviceConfig};
use crate::device_manager::DeviceConfig;
use crate::error::DeviceConfigError;
use crate::event::{Event, EventChannel, OnMqtt};
use crate::messages::PowerMessage;
#[derive(Debug, Clone, Deserialize)]
#[derive(Debug, Clone, LuaDeviceConfig)]
pub struct WasherConfig {
#[serde(flatten)]
#[device_config(flatten)]
mqtt: MqttDeviceConfig,
threshold: f32, // Power in Watt
// Power in Watt
threshold: f32,
#[device_config(user_data)]
event_channel: EventChannel,
}
#[async_trait]
impl DeviceConfig for WasherConfig {
async fn create(
&self,
identifier: &str,
ext: &ConfigExternal,
) -> Result<Box<dyn Device>, DeviceConfigError> {
async fn create(&self, identifier: &str) -> Result<Box<dyn Device>, DeviceConfigError> {
let device = Washer {
identifier: identifier.into(),
config: self.clone(),
event_channel: ext.event_channel.clone(),
running: 0,
};
@ -45,7 +42,6 @@ pub struct Washer {
#[config]
config: WasherConfig,
event_channel: EventChannel,
running: isize,
}
@ -94,6 +90,7 @@ impl OnMqtt for Washer {
.set_priority(Priority::High);
if self
.config
.event_channel
.get_tx()
.send(Event::Ntfy(notification))

View File

@ -1,4 +1,5 @@
use async_trait::async_trait;
use mlua::FromLua;
use rumqttc::Publish;
use tokio::sync::mpsc;
@ -15,7 +16,7 @@ pub enum Event {
pub type Sender = mpsc::Sender<Event>;
pub type Receiver = mpsc::Receiver<Event>;
#[derive(Clone, Debug)]
#[derive(Clone, Debug, FromLua)]
pub struct EventChannel(Sender);
impl EventChannel {
@ -30,6 +31,8 @@ impl EventChannel {
}
}
impl mlua::UserData for EventChannel {}
#[async_trait]
pub trait OnMqtt: Sync + Send {
fn topics(&self) -> Vec<&str>;

12
src/helper.rs Normal file
View File

@ -0,0 +1,12 @@
use std::time::Duration;
use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub struct DurationSeconds(u64);
impl From<DurationSeconds> for Duration {
fn from(value: DurationSeconds) -> Self {
Self::from_secs(value.0)
}
}

View File

@ -7,6 +7,7 @@ pub mod device_manager;
pub mod devices;
pub mod error;
pub mod event;
pub mod helper;
pub mod messages;
pub mod mqtt;
pub mod schedule;

View File

@ -9,7 +9,7 @@ use automation::devices::{
LightSensor, Ntfy, Presence, WakeOnLAN, Washer,
};
use automation::error::ApiError;
use automation::mqtt;
use automation::mqtt::{self, WrappedAsyncClient};
use axum::extract::FromRef;
use axum::http::StatusCode;
use axum::response::IntoResponse;
@ -85,6 +85,8 @@ async fn app() -> anyhow::Result<()> {
let automation = lua.create_table()?;
automation.set("device_manager", device_manager.clone())?;
automation.set("mqtt_client", WrappedAsyncClient(client.clone()))?;
automation.set("event_channel", device_manager.event_channel())?;
let util = lua.create_table()?;
let get_env = lua.create_function(|_lua, name: String| {

View File

@ -1,8 +1,30 @@
use rumqttc::{Event, EventLoop, Incoming};
use std::ops::{Deref, DerefMut};
use mlua::FromLua;
use rumqttc::{AsyncClient, Event, EventLoop, Incoming};
use tracing::{debug, warn};
use crate::event::{self, EventChannel};
#[derive(Debug, Clone, FromLua)]
pub struct WrappedAsyncClient(pub AsyncClient);
impl Deref for WrappedAsyncClient {
type Target = AsyncClient;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for WrappedAsyncClient {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl mlua::UserData for WrappedAsyncClient {}
pub fn start(mut eventloop: EventLoop, event_channel: &EventChannel) {
let tx = event_channel.get_tx();