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
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
88e9b8f409
commit
b7329b58ee
|
@ -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)
|
||||||
|
|
|
@ -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"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(_) => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
24
src/event.rs
24
src/event.rs
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
|
@ -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(())
|
|
||||||
}
|
}
|
||||||
|
|
68
src/main.rs
68
src/main.rs
|
@ -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)
|
||||||
|
|
24
src/mqtt.rs
24
src/mqtt.rs
|
@ -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())))
|
||||||
}
|
}
|
||||||
|
|
65
src/ntfy.rs
65
src/ntfy.rs
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
|
@ -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(())
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user