Everything is now implemented as a Device using device_traits with all events going through a single place
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Dreaded_X 2023-04-14 05:46:04 +02:00
parent 88e9b8f409
commit b7329b58ee
Signed by: Dreaded_X
GPG Key ID: FA5F485356B0D2D4
15 changed files with 487 additions and 366 deletions

View File

@ -15,6 +15,7 @@ use crate::{
debug_bridge::DebugBridgeConfig, debug_bridge::DebugBridgeConfig,
devices::{self, AudioSetup, ContactSensor, IkeaOutlet, KasaOutlet, WakeOnLAN}, devices::{self, AudioSetup, ContactSensor, IkeaOutlet, KasaOutlet, WakeOnLAN},
error::{ConfigParseError, CreateDeviceError, MissingEnv}, error::{ConfigParseError, CreateDeviceError, MissingEnv},
event::EventChannel,
hue_bridge::HueBridgeConfig, hue_bridge::HueBridgeConfig,
light_sensor::LightSensorConfig, light_sensor::LightSensorConfig,
presence::PresenceConfig, presence::PresenceConfig,
@ -165,8 +166,10 @@ pub trait CreateDevice {
fn create( fn create(
identifier: &str, identifier: &str,
config: Self::Config, config: Self::Config,
client: AsyncClient, event_channel: &EventChannel,
presence_topic: &str, // Not a big fan of passing in the global config client: &AsyncClient,
// TODO: Not a big fan of passing in the global config
presence_topic: &str,
) -> Result<Self, CreateDeviceError> ) -> Result<Self, CreateDeviceError>
where where
Self: Sized; Self: Sized;
@ -176,16 +179,31 @@ impl Device {
pub fn create( pub fn create(
self, self,
id: &str, id: &str,
client: AsyncClient, event_channel: &EventChannel,
client: &AsyncClient,
presence: &str, presence: &str,
) -> Result<Box<dyn devices::Device>, CreateDeviceError> { ) -> Result<Box<dyn devices::Device>, CreateDeviceError> {
let device: Box<dyn devices::Device> = match self { let device: Box<dyn devices::Device> = match self {
// TODO: It would be nice if this would be more automatic, not sure how to do that... // TODO: It would be nice if this would be more automatic, not sure how to do that...
Device::IkeaOutlet(c) => Box::new(IkeaOutlet::create(id, c, client, presence)?), Device::IkeaOutlet(c) => {
Device::WakeOnLAN(c) => Box::new(WakeOnLAN::create(id, c, client, presence)?), Box::new(IkeaOutlet::create(id, c, event_channel, client, presence)?)
Device::KasaOutlet(c) => Box::new(KasaOutlet::create(id, c, client, presence)?), }
Device::AudioSetup(c) => Box::new(AudioSetup::create(id, c, client, presence)?), Device::WakeOnLAN(c) => {
Device::ContactSensor(c) => Box::new(ContactSensor::create(id, c, client, presence)?), Box::new(WakeOnLAN::create(id, c, event_channel, client, presence)?)
}
Device::KasaOutlet(c) => {
Box::new(KasaOutlet::create(id, c, event_channel, client, presence)?)
}
Device::AudioSetup(c) => {
Box::new(AudioSetup::create(id, c, event_channel, client, presence)?)
}
Device::ContactSensor(c) => Box::new(ContactSensor::create(
id,
c,
event_channel,
client,
presence,
)?),
}; };
Ok(device) Ok(device)

View File

@ -1,27 +1,52 @@
use async_trait::async_trait;
use rumqttc::AsyncClient; use rumqttc::AsyncClient;
use serde::Deserialize; use serde::Deserialize;
use tracing::warn; use tracing::warn;
use crate::{ use crate::{
event::{Event, EventChannel}, config::MqttDeviceConfig,
devices::Device,
light_sensor::OnDarkness,
mqtt::{DarknessMessage, PresenceMessage}, mqtt::{DarknessMessage, PresenceMessage},
presence::OnPresence,
}; };
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct DebugBridgeConfig { pub struct DebugBridgeConfig {
pub topic: String, #[serde(flatten)]
pub mqtt: MqttDeviceConfig,
} }
pub fn start(config: DebugBridgeConfig, event_channel: &EventChannel, client: AsyncClient) { #[derive(Debug)]
let mut rx = event_channel.get_rx(); pub struct DebugBridge {
mqtt: MqttDeviceConfig,
client: AsyncClient,
}
tokio::spawn(async move { impl DebugBridge {
loop { pub fn new(
match rx.recv().await { config: DebugBridgeConfig,
Ok(Event::Presence(presence)) => { client: &AsyncClient,
) -> Result<Self, crate::error::CreateDeviceError> {
Ok(Self {
mqtt: config.mqtt,
client: client.clone(),
})
}
}
impl Device for DebugBridge {
fn get_id(&self) -> &str {
"debug_bridge"
}
}
#[async_trait]
impl OnPresence for DebugBridge {
async fn on_presence(&mut self, presence: bool) {
let message = PresenceMessage::new(presence); let message = PresenceMessage::new(presence);
let topic = format!("{}/presence", config.topic); let topic = format!("{}/presence", self.mqtt.topic);
client self.client
.publish( .publish(
topic, topic,
rumqttc::QoS::AtLeastOnce, rumqttc::QoS::AtLeastOnce,
@ -32,15 +57,19 @@ pub fn start(config: DebugBridgeConfig, event_channel: &EventChannel, client: As
.map_err(|err| { .map_err(|err| {
warn!( warn!(
"Failed to update presence on {}/presence: {err}", "Failed to update presence on {}/presence: {err}",
config.topic self.mqtt.topic
) )
}) })
.ok(); .ok();
} }
Ok(Event::Darkness(dark)) => { }
#[async_trait]
impl OnDarkness for DebugBridge {
async fn on_darkness(&mut self, dark: bool) {
let message = DarknessMessage::new(dark); let message = DarknessMessage::new(dark);
let topic = format!("{}/darkness", config.topic); let topic = format!("{}/darkness", self.mqtt.topic);
client self.client
.publish( .publish(
topic, topic,
rumqttc::QoS::AtLeastOnce, rumqttc::QoS::AtLeastOnce,
@ -51,14 +80,9 @@ pub fn start(config: DebugBridgeConfig, event_channel: &EventChannel, client: As
.map_err(|err| { .map_err(|err| {
warn!( warn!(
"Failed to update presence on {}/presence: {err}", "Failed to update presence on {}/presence: {err}",
config.topic self.mqtt.topic
) )
}) })
.ok(); .ok();
} }
Ok(_) => {}
Err(_) => todo!("Handle errors with the event channel properly"),
}
}
});
} }

View File

@ -12,21 +12,22 @@ pub use self::wake_on_lan::WakeOnLAN;
use std::collections::HashMap; use std::collections::HashMap;
use futures::future::join_all;
use google_home::{traits::OnOff, FullfillmentError, GoogleHome, GoogleHomeDevice}; use google_home::{traits::OnOff, FullfillmentError, GoogleHome, GoogleHomeDevice};
use pollster::FutureExt;
use rumqttc::{matches, AsyncClient, QoS}; use rumqttc::{matches, AsyncClient, QoS};
use thiserror::Error; use thiserror::Error;
use tokio::sync::{mpsc, oneshot}; use tokio::sync::{mpsc, oneshot};
use tracing::{debug, error, trace}; use tracing::{debug, error, instrument, trace};
use crate::{ use crate::{
event::{Event, EventChannel}, event::{Event, EventChannel},
light_sensor::OnDarkness, light_sensor::OnDarkness,
mqtt::OnMqtt, mqtt::OnMqtt,
ntfy::OnNotification,
presence::OnPresence, presence::OnPresence,
}; };
#[impl_cast::device(As: OnMqtt + OnPresence + OnDarkness + GoogleHomeDevice + OnOff)] #[impl_cast::device(As: OnMqtt + OnPresence + OnDarkness + OnNotification + GoogleHomeDevice + OnOff)]
pub trait Device: std::fmt::Debug + Sync + Send { pub trait Device: std::fmt::Debug + Sync + Send {
fn get_id(&self) -> &str; fn get_id(&self) -> &str;
} }
@ -85,6 +86,20 @@ impl DevicesHandle {
Ok(rx.await??) Ok(rx.await??)
} }
// TODO: Finish implementing this
// pub fn create_device<T>(&self, identifier: &str, config: T::Config, presence_topic: &str) -> Result<T, CreateDeviceError>
// where
// T: CreateDevice,
// {
// T::create(
// identifier,
// config,
// self.event_channel,
// self.client,
// presence_topic: presence_topic.to_owned(),
// )
// }
pub async fn add_device(&self, device: Box<dyn Device>) -> Result<(), DevicesError> { pub async fn add_device(&self, device: Box<dyn Device>) -> Result<(), DevicesError> {
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
self.tx.send(Command::AddDevice { device, tx }).await?; self.tx.send(Command::AddDevice { device, tx }).await?;
@ -92,21 +107,21 @@ impl DevicesHandle {
} }
} }
pub fn start(event_channel: &EventChannel, client: AsyncClient) -> DevicesHandle { pub fn start(client: AsyncClient) -> (DevicesHandle, EventChannel) {
let mut devices = Devices { let mut devices = Devices {
devices: HashMap::new(), devices: HashMap::new(),
client, client,
}; };
let (event_channel, mut event_rx) = EventChannel::new();
let (tx, mut rx) = mpsc::channel(100); let (tx, mut rx) = mpsc::channel(100);
let mut event_rx = event_channel.get_rx();
tokio::spawn(async move { tokio::spawn(async move {
// TODO: Handle error better // TODO: Handle error better
loop { loop {
tokio::select! { tokio::select! {
event = event_rx.recv() => { event = event_rx.recv() => {
if event.is_err() { if event.is_none() {
todo!("Handle errors with the event channel properly") todo!("Handle errors with the event channel properly")
} }
devices.handle_event(event.unwrap()).await; devices.handle_event(event.unwrap()).await;
@ -123,7 +138,7 @@ pub fn start(event_channel: &EventChannel, client: AsyncClient) -> DevicesHandle
} }
}); });
DevicesHandle { tx } (DevicesHandle { tx }, event_channel)
} }
impl Devices { impl Devices {
@ -165,12 +180,13 @@ impl Devices {
self.devices.insert(device.get_id().to_owned(), device); self.devices.insert(device.get_id().to_owned(), device);
} }
#[instrument(skip(self))]
async fn handle_event(&mut self, event: Event) { async fn handle_event(&mut self, event: Event) {
match event { match event {
Event::MqttMessage(message) => { Event::MqttMessage(message) => {
self.get::<dyn OnMqtt>() let iter = self.get::<dyn OnMqtt>().into_iter().map(|(id, listener)| {
.iter_mut() let message = message.clone();
.for_each(|(id, listener)| { async move {
let subscribed = listener let subscribed = listener
.topics() .topics()
.iter() .iter()
@ -178,27 +194,49 @@ impl Devices {
if subscribed { if subscribed {
trace!(id, "Handling"); trace!(id, "Handling");
listener.on_mqtt(&message).block_on(); listener.on_mqtt(message).await;
} }
}) }
});
join_all(iter).await;
} }
Event::Darkness(dark) => { Event::Darkness(dark) => {
let iter =
self.get::<dyn OnDarkness>() self.get::<dyn OnDarkness>()
.iter_mut() .into_iter()
.for_each(|(id, device)| { .map(|(id, device)| async move {
trace!(id, "Handling"); trace!(id, "Handling");
device.on_darkness(dark).block_on(); device.on_darkness(dark).await;
}) });
join_all(iter).await;
} }
Event::Presence(presence) => { Event::Presence(presence) => {
let iter =
self.get::<dyn OnPresence>() self.get::<dyn OnPresence>()
.iter_mut() .into_iter()
.for_each(|(id, device)| { .map(|(id, device)| async move {
trace!(id, "Handling"); trace!(id, "Handling");
device.on_presence(presence).block_on(); device.on_presence(presence).await;
}) });
join_all(iter).await;
}
Event::Ntfy(notification) => {
let iter = self
.get::<dyn OnNotification>()
.into_iter()
.map(|(id, device)| {
let notification = notification.clone();
async move {
trace!(id, "Handling");
device.on_notification(notification).await;
}
});
join_all(iter).await;
} }
Event::Ntfy(_) => {}
} }
} }

View File

@ -6,6 +6,7 @@ use tracing::{debug, error, trace, warn};
use crate::config::{self, CreateDevice, MqttDeviceConfig}; use crate::config::{self, CreateDevice, MqttDeviceConfig};
use crate::error::CreateDeviceError; use crate::error::CreateDeviceError;
use crate::event::EventChannel;
use crate::mqtt::{OnMqtt, RemoteAction, RemoteMessage}; use crate::mqtt::{OnMqtt, RemoteAction, RemoteMessage};
use crate::presence::OnPresence; use crate::presence::OnPresence;
@ -34,18 +35,20 @@ impl CreateDevice for AudioSetup {
fn create( fn create(
identifier: &str, identifier: &str,
config: Self::Config, config: Self::Config,
client: AsyncClient, event_channel: &EventChannel,
client: &AsyncClient,
presence_topic: &str, presence_topic: &str,
) -> Result<Self, CreateDeviceError> { ) -> Result<Self, CreateDeviceError> {
trace!(id = identifier, "Setting up AudioSetup"); trace!(id = identifier, "Setting up AudioSetup");
// Create the child devices // Create the child devices
let mixer_id = format!("{}.mixer", identifier); let mixer_id = format!("{}.mixer", identifier);
let mixer = (*config.mixer).create(&mixer_id, client.clone(), presence_topic)?; let mixer = (*config.mixer).create(&mixer_id, event_channel, client, presence_topic)?;
let mixer = As::consume(mixer).ok_or(CreateDeviceError::OnOffExpected(mixer_id))?; let mixer = As::consume(mixer).ok_or(CreateDeviceError::OnOffExpected(mixer_id))?;
let speakers_id = format!("{}.speakers", identifier); let speakers_id = format!("{}.speakers", identifier);
let speakers = (*config.speakers).create(&speakers_id, client, presence_topic)?; let speakers =
(*config.speakers).create(&speakers_id, event_channel, client, presence_topic)?;
let speakers = let speakers =
As::consume(speakers).ok_or(CreateDeviceError::OnOffExpected(speakers_id))?; As::consume(speakers).ok_or(CreateDeviceError::OnOffExpected(speakers_id))?;
@ -70,7 +73,7 @@ impl OnMqtt for AudioSetup {
vec![&self.mqtt.topic] vec![&self.mqtt.topic]
} }
async fn on_mqtt(&mut self, message: &rumqttc::Publish) { async fn on_mqtt(&mut self, message: rumqttc::Publish) {
let action = match RemoteMessage::try_from(message) { let action = match RemoteMessage::try_from(message) {
Ok(message) => message.action(), Ok(message) => message.action(),
Err(err) => { Err(err) => {

View File

@ -9,8 +9,9 @@ use tracing::{debug, error, trace, warn};
use crate::{ use crate::{
config::{CreateDevice, MqttDeviceConfig}, config::{CreateDevice, MqttDeviceConfig},
error::{CreateDeviceError, MissingWildcard}, error::{CreateDeviceError, MissingWildcard},
event::EventChannel,
mqtt::{ContactMessage, OnMqtt, PresenceMessage}, mqtt::{ContactMessage, OnMqtt, PresenceMessage},
presence::OnPresence, presence::{self, OnPresence},
}; };
use super::Device; use super::Device;
@ -75,7 +76,8 @@ impl CreateDevice for ContactSensor {
fn create( fn create(
identifier: &str, identifier: &str,
config: Self::Config, config: Self::Config,
client: AsyncClient, _event_channel: &EventChannel,
client: &AsyncClient,
presence_topic: &str, presence_topic: &str,
) -> Result<Self, CreateDeviceError> { ) -> Result<Self, CreateDeviceError> {
trace!(id = identifier, "Setting up ContactSensor"); trace!(id = identifier, "Setting up ContactSensor");
@ -89,8 +91,8 @@ impl CreateDevice for ContactSensor {
identifier: identifier.to_owned(), identifier: identifier.to_owned(),
mqtt: config.mqtt, mqtt: config.mqtt,
presence, presence,
client, client: client.clone(),
overall_presence: false, overall_presence: presence::DEFAULT,
is_closed: true, is_closed: true,
handle: None, handle: None,
}) })
@ -116,7 +118,7 @@ impl OnMqtt for ContactSensor {
vec![&self.mqtt.topic] vec![&self.mqtt.topic]
} }
async fn on_mqtt(&mut self, message: &rumqttc::Publish) { async fn on_mqtt(&mut self, message: rumqttc::Publish) {
let is_closed = match ContactMessage::try_from(message) { let is_closed = match ContactMessage::try_from(message) {
Ok(state) => state.is_closed(), Ok(state) => state.is_closed(),
Err(err) => { Err(err) => {

View File

@ -16,6 +16,7 @@ use tracing::{debug, error, trace, warn};
use crate::config::{CreateDevice, InfoConfig, MqttDeviceConfig}; use crate::config::{CreateDevice, InfoConfig, MqttDeviceConfig};
use crate::devices::Device; use crate::devices::Device;
use crate::error::CreateDeviceError; use crate::error::CreateDeviceError;
use crate::event::EventChannel;
use crate::mqtt::{OnMqtt, OnOffMessage}; use crate::mqtt::{OnMqtt, OnOffMessage};
use crate::presence::OnPresence; use crate::presence::OnPresence;
@ -60,8 +61,9 @@ impl CreateDevice for IkeaOutlet {
fn create( fn create(
identifier: &str, identifier: &str,
config: Self::Config, config: Self::Config,
client: AsyncClient, _event_channel: &EventChannel,
_presence_topic: &str, // Not a big fan of passing in the global config client: &AsyncClient,
_presence_topic: &str,
) -> Result<Self, CreateDeviceError> { ) -> Result<Self, CreateDeviceError> {
trace!( trace!(
id = identifier, id = identifier,
@ -76,7 +78,7 @@ impl CreateDevice for IkeaOutlet {
mqtt: config.mqtt, mqtt: config.mqtt,
outlet_type: config.outlet_type, outlet_type: config.outlet_type,
timeout: config.timeout, timeout: config.timeout,
client, client: client.clone(),
last_known_state: false, last_known_state: false,
handle: None, handle: None,
}) })
@ -112,7 +114,7 @@ impl OnMqtt for IkeaOutlet {
vec![&self.mqtt.topic] vec![&self.mqtt.topic]
} }
async fn on_mqtt(&mut self, message: &Publish) { async fn on_mqtt(&mut self, message: Publish) {
// Update the internal state based on what the device has reported // Update the internal state based on what the device has reported
let state = match OnOffMessage::try_from(message) { let state = match OnOffMessage::try_from(message) {
Ok(state) => state.state(), Ok(state) => state.state(),

View File

@ -14,7 +14,7 @@ use serde::{Deserialize, Serialize};
use thiserror::Error; use thiserror::Error;
use tracing::trace; use tracing::trace;
use crate::{config::CreateDevice, error::CreateDeviceError}; use crate::{config::CreateDevice, error::CreateDeviceError, event::EventChannel};
use super::Device; use super::Device;
@ -35,7 +35,8 @@ impl CreateDevice for KasaOutlet {
fn create( fn create(
identifier: &str, identifier: &str,
config: Self::Config, config: Self::Config,
_client: AsyncClient, _event_channel: &EventChannel,
_client: &AsyncClient,
_presence_topic: &str, _presence_topic: &str,
) -> Result<Self, CreateDeviceError> { ) -> Result<Self, CreateDeviceError> {
trace!(id = identifier, "Setting up KasaOutlet"); trace!(id = identifier, "Setting up KasaOutlet");

View File

@ -16,6 +16,7 @@ use tracing::{debug, error, trace};
use crate::{ use crate::{
config::{CreateDevice, InfoConfig, MqttDeviceConfig}, config::{CreateDevice, InfoConfig, MqttDeviceConfig},
error::CreateDeviceError, error::CreateDeviceError,
event::EventChannel,
mqtt::{ActivateMessage, OnMqtt}, mqtt::{ActivateMessage, OnMqtt},
}; };
@ -51,7 +52,8 @@ impl CreateDevice for WakeOnLAN {
fn create( fn create(
identifier: &str, identifier: &str,
config: Self::Config, config: Self::Config,
_client: AsyncClient, _event_channel: &EventChannel,
_client: &AsyncClient,
_presence_topic: &str, _presence_topic: &str,
) -> Result<Self, CreateDeviceError> { ) -> Result<Self, CreateDeviceError> {
trace!( trace!(
@ -83,7 +85,7 @@ impl OnMqtt for WakeOnLAN {
vec![&self.mqtt.topic] vec![&self.mqtt.topic]
} }
async fn on_mqtt(&mut self, message: &Publish) { async fn on_mqtt(&mut self, message: Publish) {
let activate = match ActivateMessage::try_from(message) { let activate = match ActivateMessage::try_from(message) {
Ok(message) => message.activate(), Ok(message) => message.activate(),
Err(err) => { Err(err) => {

View File

@ -1,9 +1,9 @@
use rumqttc::Publish; use rumqttc::Publish;
use tokio::sync::broadcast; use tokio::sync::mpsc;
use crate::ntfy; use crate::ntfy;
#[derive(Clone)] #[derive(Debug, Clone)]
pub enum Event { pub enum Event {
MqttMessage(Publish), MqttMessage(Publish),
Darkness(bool), Darkness(bool),
@ -11,29 +11,19 @@ pub enum Event {
Ntfy(ntfy::Notification), Ntfy(ntfy::Notification),
} }
pub type Sender = broadcast::Sender<Event>; pub type Sender = mpsc::Sender<Event>;
pub type Receiver = broadcast::Receiver<Event>; pub type Receiver = mpsc::Receiver<Event>;
pub struct EventChannel(Sender); pub struct EventChannel(Sender);
impl EventChannel { impl EventChannel {
pub fn new() -> Self { pub fn new() -> (Self, Receiver) {
let (tx, _) = broadcast::channel(100); let (tx, rx) = mpsc::channel(100);
Self(tx) (Self(tx), rx)
}
pub fn get_rx(&self) -> Receiver {
self.0.subscribe()
} }
pub fn get_tx(&self) -> Sender { pub fn get_tx(&self) -> Sender {
self.0.clone() self.0.clone()
} }
} }
impl Default for EventChannel {
fn default() -> Self {
Self::new()
}
}

View File

@ -1,10 +1,12 @@
use std::net::{Ipv4Addr, SocketAddr}; use std::net::{Ipv4Addr, SocketAddr};
use async_trait::async_trait;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tracing::{error, trace, warn}; use tracing::{error, trace, warn};
use crate::event::{Event, EventChannel}; use crate::{devices::Device, light_sensor::OnDarkness, presence::OnPresence};
#[derive(Debug)]
pub enum Flag { pub enum Flag {
Presence, Presence,
Darkness, Darkness,
@ -23,7 +25,8 @@ pub struct HueBridgeConfig {
pub flags: FlagIDs, pub flags: FlagIDs,
} }
struct HueBridge { #[derive(Debug)]
pub struct HueBridge {
addr: SocketAddr, addr: SocketAddr,
login: String, login: String,
flag_ids: FlagIDs, flag_ids: FlagIDs,
@ -35,14 +38,6 @@ struct FlagMessage {
} }
impl HueBridge { impl HueBridge {
pub fn new(config: HueBridgeConfig) -> Self {
Self {
addr: (config.ip, 80).into(),
login: config.login,
flag_ids: config.flags,
}
}
pub async fn set_flag(&self, flag: Flag, value: bool) { pub async fn set_flag(&self, flag: Flag, value: bool) {
let flag_id = match flag { let flag_id = match flag {
Flag::Presence => self.flag_ids.presence, Flag::Presence => self.flag_ids.presence,
@ -53,6 +48,8 @@ impl HueBridge {
"http://{}/api/{}/sensors/{flag_id}/state", "http://{}/api/{}/sensors/{flag_id}/state",
self.addr, self.login self.addr, self.login
); );
trace!(?flag, flag_id, value, "Sending request to change flag");
let res = reqwest::Client::new() let res = reqwest::Client::new()
.put(url) .put(url)
.json(&FlagMessage { flag: value }) .json(&FlagMessage { flag: value })
@ -73,25 +70,34 @@ impl HueBridge {
} }
} }
pub fn start(config: HueBridgeConfig, event_channel: &EventChannel) { impl HueBridge {
let hue_bridge = HueBridge::new(config); pub fn new(config: HueBridgeConfig) -> Self {
Self {
addr: (config.ip, 80).into(),
login: config.login,
flag_ids: config.flags,
}
}
}
let mut rx = event_channel.get_rx(); impl Device for HueBridge {
fn get_id(&self) -> &str {
"hue_bridge"
}
}
tokio::spawn(async move { #[async_trait]
loop { impl OnPresence for HueBridge {
match rx.recv().await { async fn on_presence(&mut self, presence: bool) {
Ok(Event::Presence(presence)) => {
trace!("Bridging presence to hue"); trace!("Bridging presence to hue");
hue_bridge.set_flag(Flag::Presence, presence).await; self.set_flag(Flag::Presence, presence).await;
} }
Ok(Event::Darkness(dark)) => { }
#[async_trait]
impl OnDarkness for HueBridge {
async fn on_darkness(&mut self, dark: bool) {
trace!("Bridging darkness to hue"); trace!("Bridging darkness to hue");
hue_bridge.set_flag(Flag::Darkness, dark).await; self.set_flag(Flag::Darkness, dark).await;
}
Ok(_) => {}
Err(_) => todo!("Handle errors with the event channel properly"),
} }
} }
});
}

View File

@ -1,13 +1,13 @@
use async_trait::async_trait; use async_trait::async_trait;
use rumqttc::{matches, AsyncClient}; use rumqttc::Publish;
use serde::Deserialize; use serde::Deserialize;
use tracing::{debug, error, trace, warn}; use tracing::{debug, trace, warn};
use crate::{ use crate::{
config::MqttDeviceConfig, config::MqttDeviceConfig,
error::LightSensorError, devices::Device,
event::{Event, EventChannel}, event::{self, Event, EventChannel},
mqtt::BrightnessMessage, mqtt::{BrightnessMessage, OnMqtt},
}; };
#[async_trait] #[async_trait]
@ -23,72 +23,74 @@ pub struct LightSensorConfig {
pub max: isize, pub max: isize,
} }
const DEFAULT: bool = false; pub const DEFAULT: bool = false;
pub async fn start( #[derive(Debug)]
config: LightSensorConfig, pub struct LightSensor {
event_channel: &EventChannel, tx: event::Sender,
client: AsyncClient, mqtt: MqttDeviceConfig,
) -> Result<(), LightSensorError> { min: isize,
// Subscrive to the mqtt topic max: isize,
client is_dark: bool,
.subscribe(config.mqtt.topic.clone(), rumqttc::QoS::AtLeastOnce)
.await?;
// Create the channels
let mut rx = event_channel.get_rx();
let tx = event_channel.get_tx();
// Setup default value, this is needed for hysteresis
let mut current_is_dark = DEFAULT;
tokio::spawn(async move {
loop {
match rx.recv().await {
Ok(Event::MqttMessage(message)) => {
if !matches(&message.topic, &config.mqtt.topic) {
continue;
} }
impl LightSensor {
pub fn new(config: LightSensorConfig, event_channel: &EventChannel) -> Self {
Self {
tx: event_channel.get_tx(),
mqtt: config.mqtt,
min: config.min,
max: config.max,
is_dark: DEFAULT,
}
}
}
impl Device for LightSensor {
fn get_id(&self) -> &str {
"light_sensor"
}
}
#[async_trait]
impl OnMqtt for LightSensor {
fn topics(&self) -> Vec<&str> {
vec![&self.mqtt.topic]
}
async fn on_mqtt(&mut self, message: Publish) {
let illuminance = match BrightnessMessage::try_from(message) { let illuminance = match BrightnessMessage::try_from(message) {
Ok(state) => state.illuminance(), Ok(state) => state.illuminance(),
Err(err) => { Err(err) => {
error!("Failed to parse message: {err}"); warn!("Failed to parse message: {err}");
continue; return;
} }
}; };
debug!("Illuminance: {illuminance}"); debug!("Illuminance: {illuminance}");
let is_dark = if illuminance <= config.min { let is_dark = if illuminance <= self.min {
trace!("It is dark"); trace!("It is dark");
true true
} else if illuminance >= config.max { } else if illuminance >= self.max {
trace!("It is light"); trace!("It is light");
false false
} else { } else {
trace!( trace!(
"In between min ({}) and max ({}) value, keeping current state: {}", "In between min ({}) and max ({}) value, keeping current state: {}",
config.min, self.min,
config.max, self.max,
current_is_dark self.is_dark
); );
current_is_dark self.is_dark
}; };
if is_dark != current_is_dark { if is_dark != self.is_dark {
debug!("Dark state has changed: {is_dark}"); debug!("Dark state has changed: {is_dark}");
current_is_dark = is_dark; self.is_dark = is_dark;
if tx.send(Event::Darkness(is_dark)).is_err() { if self.tx.send(Event::Darkness(is_dark)).await.is_err() {
warn!("There are no receivers on the event channel"); warn!("There are no receivers on the event channel");
} }
} }
} }
Ok(_) => {}
Err(_) => todo!("Handle errors with the event channel properly"),
}
}
});
Ok(())
} }

View File

@ -8,10 +8,14 @@ use axum::{
use automation::{ use automation::{
auth::{OpenIDConfig, User}, auth::{OpenIDConfig, User},
config::Config, config::Config,
debug_bridge, devices, debug_bridge::DebugBridge,
devices,
error::ApiError, error::ApiError,
event::EventChannel, hue_bridge::HueBridge,
hue_bridge, light_sensor, mqtt, ntfy, presence, light_sensor::LightSensor,
mqtt,
ntfy::Ntfy,
presence::Presence,
}; };
use dotenvy::dotenv; use dotenvy::dotenv;
use futures::future::join_all; use futures::future::join_all;
@ -55,41 +59,55 @@ async fn app() -> anyhow::Result<()> {
std::env::var("AUTOMATION_CONFIG").unwrap_or("./config/config.toml".to_owned()); std::env::var("AUTOMATION_CONFIG").unwrap_or("./config/config.toml".to_owned());
let config = Config::parse_file(&config_filename)?; let config = Config::parse_file(&config_filename)?;
let event_channel = EventChannel::new();
// Create a mqtt client // Create a mqtt client
let (client, eventloop) = AsyncClient::new(config.mqtt.clone(), 10); let (client, eventloop) = AsyncClient::new(config.mqtt.clone(), 10);
let presence_topic = config.presence.mqtt.topic.to_owned(); // Setup the device handler
presence::start(config.presence, &event_channel, client.clone()).await?; let (device_handler, event_channel) = devices::start(client.clone());
light_sensor::start(config.light_sensor, &event_channel, client.clone()).await?;
// Start the ntfy service if it is configured // Create all the devices specified in the config
if let Some(config) = config.ntfy { let mut devices = config
ntfy::start(config, &event_channel); .devices
.into_iter()
.map(|(identifier, device_config)| {
device_config.create(
&identifier,
&event_channel,
&client,
&config.presence.mqtt.topic,
)
})
.collect::<Result<Vec<_>, _>>()?;
// Create and add the light sensor
{
let light_sensor = LightSensor::new(config.light_sensor, &event_channel);
devices.push(Box::new(light_sensor));
} }
// Start the hue bridge if it is configured // Create and add the presence system
{
let presence = Presence::new(config.presence, &event_channel);
devices.push(Box::new(presence));
}
// If configured, create and add the hue bridge
if let Some(config) = config.hue_bridge { if let Some(config) = config.hue_bridge {
hue_bridge::start(config, &event_channel); let hue_bridge = HueBridge::new(config);
devices.push(Box::new(hue_bridge));
} }
// Start the debug bridge if it is configured // Start the debug bridge if it is configured
if let Some(config) = config.debug_bridge { if let Some(config) = config.debug_bridge {
debug_bridge::start(config, &event_channel, client.clone()); let debug_bridge = DebugBridge::new(config, &client)?;
devices.push(Box::new(debug_bridge));
} }
// Setup the device handler // Start the ntfy service if it is configured
let device_handler = devices::start(&event_channel, client.clone()); if let Some(config) = config.ntfy {
let ntfy = Ntfy::new(config, &event_channel);
// Create all the devices specified in the config devices.push(Box::new(ntfy));
let devices = config }
.devices
.into_iter()
.map(|(identifier, device_config)| {
device_config.create(&identifier, client.clone(), &presence_topic)
})
.collect::<Result<Vec<_>, _>>()?;
// Can even add some more devices here // Can even add some more devices here
// devices.push(device) // devices.push(device)

View File

@ -14,7 +14,7 @@ use crate::event::{self, EventChannel};
#[impl_cast::device_trait] #[impl_cast::device_trait]
pub trait OnMqtt { pub trait OnMqtt {
fn topics(&self) -> Vec<&str>; fn topics(&self) -> Vec<&str>;
async fn on_mqtt(&mut self, message: &Publish); async fn on_mqtt(&mut self, message: Publish);
} }
#[derive(Debug, Error)] #[derive(Debug, Error)]
@ -32,7 +32,7 @@ pub fn start(mut eventloop: EventLoop, event_channel: &EventChannel) {
let notification = eventloop.poll().await; let notification = eventloop.poll().await;
match notification { match notification {
Ok(Event::Incoming(Incoming::Publish(p))) => { Ok(Event::Incoming(Incoming::Publish(p))) => {
tx.send(event::Event::MqttMessage(p)).ok(); tx.send(event::Event::MqttMessage(p)).await.ok();
} }
Ok(..) => continue, Ok(..) => continue,
Err(err) => { Err(err) => {
@ -62,10 +62,10 @@ impl OnOffMessage {
} }
} }
impl TryFrom<&Publish> for OnOffMessage { impl TryFrom<Publish> for OnOffMessage {
type Error = ParseError; type Error = ParseError;
fn try_from(message: &Publish) -> Result<Self, Self::Error> { fn try_from(message: Publish) -> Result<Self, Self::Error> {
serde_json::from_slice(&message.payload) serde_json::from_slice(&message.payload)
.or(Err(ParseError::InvalidPayload(message.payload.clone()))) .or(Err(ParseError::InvalidPayload(message.payload.clone())))
} }
@ -82,10 +82,10 @@ impl ActivateMessage {
} }
} }
impl TryFrom<&Publish> for ActivateMessage { impl TryFrom<Publish> for ActivateMessage {
type Error = ParseError; type Error = ParseError;
fn try_from(message: &Publish) -> Result<Self, Self::Error> { fn try_from(message: Publish) -> Result<Self, Self::Error> {
serde_json::from_slice(&message.payload) serde_json::from_slice(&message.payload)
.or(Err(ParseError::InvalidPayload(message.payload.clone()))) .or(Err(ParseError::InvalidPayload(message.payload.clone())))
} }
@ -112,10 +112,10 @@ impl RemoteMessage {
} }
} }
impl TryFrom<&Publish> for RemoteMessage { impl TryFrom<Publish> for RemoteMessage {
type Error = ParseError; type Error = ParseError;
fn try_from(message: &Publish) -> Result<Self, Self::Error> { fn try_from(message: Publish) -> Result<Self, Self::Error> {
serde_json::from_slice(&message.payload) serde_json::from_slice(&message.payload)
.or(Err(ParseError::InvalidPayload(message.payload.clone()))) .or(Err(ParseError::InvalidPayload(message.payload.clone())))
} }
@ -185,10 +185,10 @@ impl ContactMessage {
} }
} }
impl TryFrom<&Publish> for ContactMessage { impl TryFrom<Publish> for ContactMessage {
type Error = ParseError; type Error = ParseError;
fn try_from(message: &Publish) -> Result<Self, Self::Error> { fn try_from(message: Publish) -> Result<Self, Self::Error> {
serde_json::from_slice(&message.payload) serde_json::from_slice(&message.payload)
.or(Err(ParseError::InvalidPayload(message.payload.clone()))) .or(Err(ParseError::InvalidPayload(message.payload.clone())))
} }
@ -218,10 +218,10 @@ impl DarknessMessage {
} }
} }
impl TryFrom<&Publish> for DarknessMessage { impl TryFrom<Publish> for DarknessMessage {
type Error = ParseError; type Error = ParseError;
fn try_from(message: &Publish) -> Result<Self, Self::Error> { fn try_from(message: Publish) -> Result<Self, Self::Error> {
serde_json::from_slice(&message.payload) serde_json::from_slice(&message.payload)
.or(Err(ParseError::InvalidPayload(message.payload.clone()))) .or(Err(ParseError::InvalidPayload(message.payload.clone())))
} }

View File

@ -1,5 +1,7 @@
use std::collections::HashMap; use std::collections::HashMap;
use async_trait::async_trait;
use impl_cast::device_trait;
use serde::Serialize; use serde::Serialize;
use serde_repr::*; use serde_repr::*;
use tokio::sync::mpsc; use tokio::sync::mpsc;
@ -7,18 +9,28 @@ use tracing::{debug, error, warn};
use crate::{ use crate::{
config::NtfyConfig, config::NtfyConfig,
event::{Event, EventChannel}, devices::Device,
event::{self, Event, EventChannel},
presence::OnPresence,
}; };
pub type Sender = mpsc::Sender<Notification>; pub type Sender = mpsc::Sender<Notification>;
pub type Receiver = mpsc::Receiver<Notification>; pub type Receiver = mpsc::Receiver<Notification>;
struct Ntfy { #[async_trait]
base_url: String, #[device_trait]
topic: String, pub trait OnNotification {
async fn on_notification(&mut self, notification: Notification);
} }
#[derive(Serialize_repr, Clone, Copy)] #[derive(Debug)]
pub struct Ntfy {
base_url: String,
topic: String,
tx: event::Sender,
}
#[derive(Debug, Serialize_repr, Clone, Copy)]
#[repr(u8)] #[repr(u8)]
pub enum Priority { pub enum Priority {
Min = 1, Min = 1,
@ -28,7 +40,7 @@ pub enum Priority {
Max, Max,
} }
#[derive(Serialize, Clone)] #[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "snake_case", tag = "action")] #[serde(rename_all = "snake_case", tag = "action")]
pub enum ActionType { pub enum ActionType {
Broadcast { Broadcast {
@ -39,7 +51,7 @@ pub enum ActionType {
// Http // Http
} }
#[derive(Serialize, Clone)] #[derive(Debug, Serialize, Clone)]
pub struct Action { pub struct Action {
#[serde(flatten)] #[serde(flatten)]
action: ActionType, action: ActionType,
@ -54,7 +66,7 @@ struct NotificationFinal {
inner: Notification, inner: Notification,
} }
#[derive(Serialize, Clone)] #[derive(Debug, Serialize, Clone)]
pub struct Notification { pub struct Notification {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
title: Option<String>, title: Option<String>,
@ -119,10 +131,11 @@ impl Default for Notification {
} }
impl Ntfy { impl Ntfy {
fn new(base_url: &str, topic: &str) -> Self { pub fn new(config: NtfyConfig, event_channel: &EventChannel) -> Self {
Self { Self {
base_url: base_url.to_owned(), base_url: config.url,
topic: topic.to_owned(), topic: config.topic,
tx: event_channel.get_tx(),
} }
} }
@ -148,16 +161,15 @@ impl Ntfy {
} }
} }
pub fn start(config: NtfyConfig, event_channel: &EventChannel) { impl Device for Ntfy {
let mut rx = event_channel.get_rx(); fn get_id(&self) -> &str {
let tx = event_channel.get_tx(); "ntfy"
}
}
let ntfy = Ntfy::new(&config.url, &config.topic); #[async_trait]
impl OnPresence for Ntfy {
tokio::spawn(async move { async fn on_presence(&mut self, presence: bool) {
loop {
match rx.recv().await {
Ok(Event::Presence(presence)) => {
// Setup extras for the broadcast // Setup extras for the broadcast
let extras = HashMap::from([ let extras = HashMap::from([
("cmd".into(), "presence".into()), ("cmd".into(), "presence".into()),
@ -179,14 +191,15 @@ pub fn start(config: NtfyConfig, event_channel: &EventChannel) {
.add_action(action) .add_action(action)
.set_priority(Priority::Low); .set_priority(Priority::Low);
if tx.send(Event::Ntfy(notification)).is_err() { if self.tx.send(Event::Ntfy(notification)).await.is_err() {
warn!("There are no receivers on the event channel"); warn!("There are no receivers on the event channel");
} }
} }
Ok(Event::Ntfy(notification)) => ntfy.send(notification).await, }
Ok(_) => {}
Err(_) => todo!("Handle errors with the event channel properly"), #[async_trait]
impl OnNotification for Ntfy {
async fn on_notification(&mut self, notification: Notification) {
self.send(notification).await;
} }
} }
});
}

View File

@ -1,18 +1,15 @@
use std::collections::HashMap; use std::collections::HashMap;
use async_trait::async_trait; use async_trait::async_trait;
use rumqttc::{has_wildcards, matches, AsyncClient}; use rumqttc::Publish;
use serde::Deserialize; use serde::Deserialize;
use tracing::{debug, warn}; use tracing::{debug, warn};
use crate::{ use crate::{
config::MqttDeviceConfig, config::MqttDeviceConfig,
error::{MissingWildcard, PresenceError}, devices::Device,
event::{ event::{self, Event, EventChannel},
Event::{self, MqttMessage}, mqtt::{OnMqtt, PresenceMessage},
EventChannel,
},
mqtt::PresenceMessage,
}; };
#[async_trait] #[async_trait]
@ -26,73 +23,78 @@ pub struct PresenceConfig {
pub mqtt: MqttDeviceConfig, pub mqtt: MqttDeviceConfig,
} }
const DEFAULT: bool = false; pub const DEFAULT: bool = false;
pub async fn start( #[derive(Debug)]
config: PresenceConfig, pub struct Presence {
event_channel: &EventChannel, tx: event::Sender,
client: AsyncClient, mqtt: MqttDeviceConfig,
) -> Result<(), PresenceError> { devices: HashMap<String, bool>,
if !has_wildcards(&config.mqtt.topic) { current_overall_presence: bool,
return Err(MissingWildcard::new(&config.mqtt.topic).into());
} }
// Subscribe to the relevant topics on mqtt impl Presence {
client pub fn new(config: PresenceConfig, event_channel: &EventChannel) -> Self {
.subscribe(config.mqtt.topic.clone(), rumqttc::QoS::AtLeastOnce) Self {
.await?; tx: event_channel.get_tx(),
mqtt: config.mqtt,
let mut rx = event_channel.get_rx(); devices: HashMap::new(),
let tx = event_channel.get_tx(); current_overall_presence: DEFAULT,
}
let mut devices = HashMap::<String, bool>::new(); }
let mut current_overall_presence = DEFAULT;
tokio::spawn(async move {
loop {
// TODO: Handle errors, warn if lagging
if let Ok(MqttMessage(message)) = rx.recv().await {
if !matches(&message.topic, &config.mqtt.topic) {
continue;
} }
let offset = config impl Device for Presence {
fn get_id(&self) -> &str {
"presence"
}
}
#[async_trait]
impl OnMqtt for Presence {
fn topics(&self) -> Vec<&str> {
vec![&self.mqtt.topic]
}
async fn on_mqtt(&mut self, message: Publish) {
let offset = self
.mqtt .mqtt
.topic .topic
.find('+') .find('+')
.or(config.mqtt.topic.find('#')) .or(self.mqtt.topic.find('#'))
.expect("Presence::new fails if it does not contain wildcards"); .expect("Presence::create fails if it does not contain wildcards");
let device_name = message.topic[offset..].to_owned(); let device_name = message.topic[offset..].to_owned();
if message.payload.is_empty() { if message.payload.is_empty() {
// Remove the device from the map // Remove the device from the map
debug!("State of device [{device_name}] has been removed"); debug!("State of device [{device_name}] has been removed");
devices.remove(&device_name); self.devices.remove(&device_name);
} else { } else {
let present = match PresenceMessage::try_from(message) { let present = match PresenceMessage::try_from(message) {
Ok(state) => state.present(), Ok(state) => state.present(),
Err(err) => { Err(err) => {
warn!("Failed to parse message: {err}"); warn!("Failed to parse message: {err}");
continue; return;
} }
}; };
debug!("State of device [{device_name}] has changed: {}", present); debug!("State of device [{device_name}] has changed: {}", present);
devices.insert(device_name, present); self.devices.insert(device_name, present);
} }
let overall_presence = devices.iter().any(|(_, v)| *v); let overall_presence = self.devices.iter().any(|(_, v)| *v);
if overall_presence != current_overall_presence { if overall_presence != self.current_overall_presence {
debug!("Overall presence updated: {overall_presence}"); debug!("Overall presence updated: {overall_presence}");
current_overall_presence = overall_presence; self.current_overall_presence = overall_presence;
if tx.send(Event::Presence(overall_presence)).is_err() { if self
.tx
.send(Event::Presence(overall_presence))
.await
.is_err()
{
warn!("There are no receivers on the event channel"); warn!("There are no receivers on the event channel");
} }
} }
} }
} }
});
Ok(())
}