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

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

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();