diff --git a/config/config.toml b/config/config.toml index 31bf74a..473dcce 100644 --- a/config/config.toml +++ b/config/config.toml @@ -83,3 +83,7 @@ speakers = "living_speakers" type = "ContactSensor" topic = "zigbee2mqtt/hallway/frontdoor" presence = { timeout = 900 } + +[devices.bathroom_washer] +type = "Washer" +topic = "zigbee2mqtt/bathroom/washer" diff --git a/config/zeus.dev.toml b/config/zeus.dev.toml index 1c1a6ee..fec6064 100644 --- a/config/zeus.dev.toml +++ b/config/zeus.dev.toml @@ -84,3 +84,7 @@ type = "ContactSensor" topic = "zigbee2mqtt/hallway/frontdoor" presence = { timeout = 10 } lights = { lights = ["bathroom_light"], timeout = 10 } + +[devices.bathroom_washer] +type = "Washer" +topic = "zigbee2mqtt/bathroom/washer" diff --git a/src/config.rs b/src/config.rs index f5098cc..8a1431e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -15,7 +15,7 @@ use crate::{ device_manager::DeviceManager, devices::{ AudioSetup, ContactSensor, DebugBridgeConfig, Device, HueBridgeConfig, IkeaOutlet, - KasaOutlet, LightSensorConfig, PresenceConfig, WakeOnLAN, + KasaOutlet, LightSensorConfig, PresenceConfig, WakeOnLAN, Washer, }, error::{ConfigParseError, CreateDeviceError, MissingEnv}, event::EventChannel, @@ -130,6 +130,7 @@ pub enum DeviceConfig { IkeaOutlet(::Config), KasaOutlet(::Config), WakeOnLAN(::Config), + Washer(::Config), } impl Config { @@ -200,7 +201,8 @@ impl DeviceConfig { ContactSensor, IkeaOutlet, KasaOutlet, - WakeOnLAN + WakeOnLAN, + Washer ] }) } diff --git a/src/devices/mod.rs b/src/devices/mod.rs index 6cd05ec..36bd5b0 100644 --- a/src/devices/mod.rs +++ b/src/devices/mod.rs @@ -8,6 +8,7 @@ mod light_sensor; mod ntfy; mod presence; mod wake_on_lan; +mod washer; pub use self::audio_setup::AudioSetup; pub use self::contact_sensor::ContactSensor; @@ -19,6 +20,7 @@ pub use self::light_sensor::{LightSensor, LightSensorConfig}; pub use self::ntfy::{Notification, Ntfy}; pub use self::presence::{Presence, PresenceConfig, DEFAULT_PRESENCE}; pub use self::wake_on_lan::WakeOnLAN; +pub use self::washer::Washer; use google_home::{device::AsGoogleHomeDevice, traits::OnOff}; diff --git a/src/devices/washer.rs b/src/devices/washer.rs new file mode 100644 index 0000000..c518e49 --- /dev/null +++ b/src/devices/washer.rs @@ -0,0 +1,98 @@ +use async_trait::async_trait; +use rumqttc::{AsyncClient, Publish}; +use serde::Deserialize; +use tracing::{error, warn}; + +use crate::{ + config::{CreateDevice, MqttDeviceConfig}, + device_manager::DeviceManager, + error::CreateDeviceError, + event::{Event, EventChannel, OnMqtt}, + messages::PowerMessage, +}; + +use super::{ntfy::Priority, Device, Notification}; + +#[derive(Debug, Clone, Deserialize)] +pub struct WasherConfig { + #[serde(flatten)] + mqtt: MqttDeviceConfig, +} + +// TODO: Add google home integration + +#[derive(Debug)] +pub struct Washer { + identifier: String, + mqtt: MqttDeviceConfig, + + event_channel: EventChannel, + running: bool, +} + +#[async_trait] +impl CreateDevice for Washer { + type Config = WasherConfig; + + async fn create( + identifier: &str, + config: Self::Config, + event_channel: &EventChannel, + _client: &AsyncClient, + _presence_topic: &str, + _device_manager: &DeviceManager, + ) -> Result { + Ok(Self { + identifier: identifier.to_owned(), + mqtt: config.mqtt, + event_channel: event_channel.clone(), + running: false, + }) + } +} + +impl Device for Washer { + fn get_id(&self) -> &str { + &self.identifier + } +} + +#[async_trait] +impl OnMqtt for Washer { + fn topics(&self) -> Vec<&str> { + vec![&self.mqtt.topic] + } + + async fn on_mqtt(&mut self, message: Publish) { + let power = match PowerMessage::try_from(message) { + Ok(state) => state.power(), + Err(err) => { + error!(id = self.identifier, "Failed to parse message: {err}"); + return; + } + }; + + if self.running && power < 1.0 { + // The washer is done running + self.running = false; + let notification = Notification::new() + .set_title("Laundy is done") + .set_message("Don't forget to hang it!") + .add_tag("womans_clothes") + .set_priority(Priority::High); + + if self + .event_channel + .get_tx() + .send(Event::Ntfy(notification)) + .await + .is_err() + { + warn!("There are no receivers on the event channel"); + } + } else if !self.running && power >= 1.0 { + // We just started washing + self.running = true + } + } +} diff --git a/src/event.rs b/src/event.rs index 46fa367..879f210 100644 --- a/src/event.rs +++ b/src/event.rs @@ -17,7 +17,7 @@ pub enum Event { pub type Sender = mpsc::Sender; pub type Receiver = mpsc::Receiver; -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct EventChannel(Sender); impl EventChannel { diff --git a/src/messages.rs b/src/messages.rs index cc220dd..f190f3b 100644 --- a/src/messages.rs +++ b/src/messages.rs @@ -119,7 +119,7 @@ impl TryFrom for PresenceMessage { } } -// Message use to report the state of a light sensor +// Message used to report the state of a light sensor #[derive(Debug, Deserialize)] pub struct BrightnessMessage { illuminance: isize, @@ -194,3 +194,24 @@ impl TryFrom for DarknessMessage { .or(Err(ParseError::InvalidPayload(message.payload.clone()))) } } + +// Message used to report the power draw a smart plug +#[derive(Debug, Deserialize)] +pub struct PowerMessage { + power: f32, +} + +impl PowerMessage { + pub fn power(&self) -> f32 { + self.power + } +} + +impl TryFrom for PowerMessage { + type Error = ParseError; + + fn try_from(message: Publish) -> Result { + serde_json::from_slice(&message.payload) + .or(Err(ParseError::InvalidPayload(message.payload.clone()))) + } +}