From c9b2127eed325b42c5115ce1318e950d9b3c341d Mon Sep 17 00:00:00 2001 From: Dreaded_X Date: Tue, 3 Jan 2023 20:46:37 +0100 Subject: [PATCH] Some cleanup and added light sensor --- config/zeus.dev.toml | 22 +++++++---- src/config.rs | 14 +++++++ src/devices.rs | 16 +++++++- src/devices/ikea_outlet.rs | 12 ++---- src/devices/wake_on_lan.rs | 4 +- src/lib.rs | 1 + src/light_sensor.rs | 80 ++++++++++++++++++++++++++++++++++++++ src/main.rs | 8 +++- src/mqtt.rs | 41 +++++++++++++++++++ src/presence.rs | 27 +++---------- 10 files changed, 182 insertions(+), 43 deletions(-) create mode 100644 src/light_sensor.rs diff --git a/config/zeus.dev.toml b/config/zeus.dev.toml index b064ab1..8e3b8d0 100644 --- a/config/zeus.dev.toml +++ b/config/zeus.dev.toml @@ -9,25 +9,33 @@ username="Dreaded_X" [presence] topic = "automation_dev/presence" +[light_sensor] +topic = "zigbee2mqtt/living/light" +min = 23_000 +max = 25_000 + [devices.kitchen_kettle] type = "IkeaOutlet" -info = { name = "Kettle", room = "Kitchen" } -mqtt = { topic = "zigbee2mqtt/kitchen/kettle" } +name = "Kettle" +room = "Kitchen" +topic = "zigbee2mqtt/kitchen/kettle" kettle = { timeout = 5 } [devices.living_workbench] type = "IkeaOutlet" -info = { name = "Workbench", room = "Living Room" } -mqtt = { topic = "zigbee2mqtt/living/workbench" } +name = "Workbench" +room = "Living Room" +topic = "zigbee2mqtt/living/workbench" [devices.living_zeus] type = "WakeOnLAN" -info = { name = "Zeus", room = "Living Room" } -mqtt = { topic = "automation/appliance/living_room/zeus" } +name = "Zeus" +room = "Living Room" +topic = "automation/appliance/living_room/zeus" mac_address = "30:9c:23:60:9c:13" [devices.audio] type = "AudioSetup" -mqtt = { topic = "zigbee2mqtt/living/remote" } +topic = "zigbee2mqtt/living/remote" mixer = [10, 0, 0, 49] speakers = [10, 0, 0, 182] diff --git a/src/config.rs b/src/config.rs index e6935fd..2dabbe1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -15,6 +15,7 @@ pub struct Config { #[serde(default)] pub ntfy: NtfyConfig, pub presence: MqttDeviceConfig, + pub light_sensor: LightSensorConfig, #[serde(default)] pub devices: HashMap } @@ -55,6 +56,14 @@ impl Default for NtfyConfig { } } +#[derive(Debug, Deserialize)] +pub struct LightSensorConfig { + #[serde(flatten)] + pub mqtt: MqttDeviceConfig, + pub min: isize, + pub max: isize, +} + #[derive(Debug, Deserialize)] pub struct InfoConfig { pub name: String, @@ -75,16 +84,21 @@ pub struct KettleConfig { #[serde(tag = "type")] pub enum Device { IkeaOutlet { + #[serde(flatten)] info: InfoConfig, + #[serde(flatten)] mqtt: MqttDeviceConfig, kettle: Option, }, WakeOnLAN { + #[serde(flatten)] info: InfoConfig, + #[serde(flatten)] mqtt: MqttDeviceConfig, mac_address: String, }, AudioSetup { + #[serde(flatten)] mqtt: MqttDeviceConfig, mixer: [u8; 4], speakers: [u8; 4], diff --git a/src/devices.rs b/src/devices.rs index 52e9a91..fb78d2a 100644 --- a/src/devices.rs +++ b/src/devices.rs @@ -12,14 +12,15 @@ use std::collections::HashMap; use google_home::{GoogleHomeDevice, traits::OnOff}; use tracing::{trace, debug, span, Level}; -use crate::{mqtt::OnMqtt, presence::OnPresence}; +use crate::{mqtt::OnMqtt, presence::OnPresence, light_sensor::OnDarkness}; impl_cast::impl_cast!(Device, OnMqtt); impl_cast::impl_cast!(Device, OnPresence); +impl_cast::impl_cast!(Device, OnDarkness); impl_cast::impl_cast!(Device, GoogleHomeDevice); impl_cast::impl_cast!(Device, OnOff); -pub trait Device: AsGoogleHomeDevice + AsOnMqtt + AsOnPresence + AsOnOff { +pub trait Device: AsGoogleHomeDevice + AsOnMqtt + AsOnPresence + AsOnDarkness + AsOnOff { fn get_id(&self) -> String; } @@ -60,6 +61,7 @@ impl Devices { get_cast!(OnMqtt); get_cast!(OnPresence); + get_cast!(OnDarkness); get_cast!(GoogleHomeDevice); get_cast!(OnOff); @@ -90,3 +92,13 @@ impl OnPresence for Devices { }) } } + +impl OnDarkness for Devices { + fn on_darkness(&mut self, dark: bool) { + self.as_on_darknesss().iter_mut().for_each(|(id, device)| { + let _span = span!(Level::TRACE, "on_darkness").entered(); + trace!(id, "Handling"); + device.on_darkness(dark); + }) + } +} diff --git a/src/devices/ikea_outlet.rs b/src/devices/ikea_outlet.rs index a82a751..b1d8077 100644 --- a/src/devices/ikea_outlet.rs +++ b/src/devices/ikea_outlet.rs @@ -25,10 +25,8 @@ pub struct IkeaOutlet { impl IkeaOutlet { pub fn new(identifier: String, info: InfoConfig, mqtt: MqttDeviceConfig, kettle: Option, client: AsyncClient) -> Self { - let c = client.clone(); - let t = mqtt.topic.clone(); // @TODO Handle potential errors here - c.subscribe(t, rumqttc::QoS::AtLeastOnce).block_on().unwrap(); + client.subscribe(mqtt.topic.clone(), rumqttc::QoS::AtLeastOnce).block_on().unwrap(); Self{ identifier, info, mqtt, kettle, client, last_known_state: false, handle: None } } @@ -115,9 +113,7 @@ impl OnPresence for IkeaOutlet { // Turn off the outlet when we leave the house if !presence { debug!(id = self.identifier, "Turning device off"); - let client = self.client.clone(); - let topic = self.mqtt.topic.clone(); - set_on(client, topic, false).block_on(); + set_on(self.client.clone(), self.mqtt.topic.clone(), false).block_on(); } } } @@ -159,9 +155,7 @@ impl traits::OnOff for IkeaOutlet { } fn set_on(&mut self, on: bool) -> Result<(), ErrorCode> { - let client = self.client.clone(); - let topic = self.mqtt.topic.clone(); - set_on(client, topic, on).block_on(); + set_on(self.client.clone(), self.mqtt.topic.clone(), on).block_on(); Ok(()) } diff --git a/src/devices/wake_on_lan.rs b/src/devices/wake_on_lan.rs index a4edb55..4b62565 100644 --- a/src/devices/wake_on_lan.rs +++ b/src/devices/wake_on_lan.rs @@ -16,9 +16,8 @@ pub struct WakeOnLAN { impl WakeOnLAN { pub fn new(identifier: String, info: InfoConfig, mqtt: MqttDeviceConfig, mac_address: String, client: AsyncClient) -> Self { - let t = mqtt.topic.clone(); // @TODO Handle potential errors here - client.subscribe(t, rumqttc::QoS::AtLeastOnce).block_on().unwrap(); + client.subscribe(mqtt.topic.clone(), rumqttc::QoS::AtLeastOnce).block_on().unwrap(); Self { identifier, info, mqtt, mac_address } } @@ -32,7 +31,6 @@ impl Device for WakeOnLAN { impl OnMqtt for WakeOnLAN { fn on_mqtt(&mut self, message: &Publish) { - if message.topic != self.mqtt.topic { return; } diff --git a/src/lib.rs b/src/lib.rs index 54d61f3..8007f50 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,3 +4,4 @@ pub mod mqtt; pub mod config; pub mod presence; pub mod ntfy; +pub mod light_sensor; diff --git a/src/light_sensor.rs b/src/light_sensor.rs new file mode 100644 index 0000000..97e0e69 --- /dev/null +++ b/src/light_sensor.rs @@ -0,0 +1,80 @@ +use std::sync::{Weak, RwLock}; + +use pollster::FutureExt as _; +use rumqttc::AsyncClient; +use tracing::{span, Level, log::{warn, trace}, debug}; + +use crate::{config::{MqttDeviceConfig, LightSensorConfig}, mqtt::{OnMqtt, BrightnessMessage}}; + + +pub trait OnDarkness { + fn on_darkness(&mut self, dark: bool); +} + +pub struct LightSensor { + listeners: Vec>>, + is_dark: bool, + mqtt: MqttDeviceConfig, + min: isize, + max: isize, +} + +impl LightSensor { + pub fn new(config: LightSensorConfig, client: AsyncClient) -> Self { + client.subscribe(config.mqtt.topic.clone(), rumqttc::QoS::AtLeastOnce).block_on().unwrap(); + + Self { listeners: Vec::new(), is_dark: false, mqtt: config.mqtt, min: config.min, max: config.max } + } + + pub fn add_listener(&mut self, listener: Weak>) { + self.listeners.push(listener); + } + + pub fn notify(dark: bool, listeners: Vec>>) { + let _span = span!(Level::TRACE, "darkness_update").entered(); + listeners.into_iter().for_each(|listener| { + if let Some(listener) = listener.upgrade() { + listener.write().unwrap().on_darkness(dark); + } + }) + } +} + +impl OnMqtt for LightSensor { + fn on_mqtt(&mut self, message: &rumqttc::Publish) { + if message.topic != self.mqtt.topic { + return; + } + + let illuminance = match BrightnessMessage::try_from(message) { + Ok(state) => state.illuminance(), + Err(err) => { + warn!("Failed to parse message: {err}"); + return; + } + }; + + debug!("Illuminance: {illuminance}"); + let is_dark = if illuminance <= self.min { + trace!("It is dark"); + true + } else if illuminance >= self.max { + trace!("It is light"); + false + } else { + trace!("In between min ({}) and max ({}) value, keeping current state: {}", self.min, self.max, self.is_dark); + self.is_dark + }; + + if is_dark != self.is_dark { + debug!("Dark state has changed: {is_dark}"); + self.is_dark = is_dark; + self.listeners.retain(|listener| listener.strong_count() > 0); + let listeners = self.listeners.clone(); + + tokio::task::spawn_blocking(move || { + LightSensor::notify(is_dark, listeners) + }); + } + } +} diff --git a/src/main.rs b/src/main.rs index c622405..c20b6d7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,7 @@ use std::{time::Duration, sync::{Arc, RwLock}, process, net::SocketAddr}; use axum::{Router, Json, routing::post, http::StatusCode}; -use automation::{config::Config, presence::Presence, ntfy::Ntfy}; +use automation::{config::Config, presence::Presence, ntfy::Ntfy, light_sensor::{self, LightSensor}}; use dotenv::dotenv; use rumqttc::{MqttOptions, Transport, AsyncClient}; use tracing::{error, info, metadata::LevelFilter}; @@ -58,6 +58,12 @@ async fn main() { let presence = Arc::new(RwLock::new(presence)); mqtt.add_listener(Arc::downgrade(&presence)); + let mut light_sensor = LightSensor::new(config.light_sensor, client.clone()); + light_sensor.add_listener(Arc::downgrade(&devices)); + + let light_sensor = Arc::new(RwLock::new(light_sensor)); + mqtt.add_listener(Arc::downgrade(&light_sensor)); + // Start mqtt, this spawns a seperate async task mqtt.start(); diff --git a/src/mqtt.rs b/src/mqtt.rs index 849b46f..bde9aec 100644 --- a/src/mqtt.rs +++ b/src/mqtt.rs @@ -136,3 +136,44 @@ impl TryFrom<&Publish> for RemoteMessage { .or(Err(anyhow::anyhow!("Invalid message payload received: {:?}", message.payload))) } } + +#[derive(Debug, Deserialize)] +pub struct PresenceMessage { + state: bool +} + +impl PresenceMessage { + pub fn present(&self) -> bool { + self.state + } +} + +impl TryFrom<&Publish> for PresenceMessage { + type Error = anyhow::Error; + + fn try_from(message: &Publish) -> Result { + serde_json::from_slice(&message.payload) + .or(Err(anyhow::anyhow!("Invalid message payload received: {:?}", message.payload))) + } +} + +#[derive(Debug, Deserialize)] +pub struct BrightnessMessage { + illuminance: isize, +} + +impl BrightnessMessage { + pub fn illuminance(&self) -> isize { + self.illuminance + } +} + +impl TryFrom<&Publish> for BrightnessMessage { + type Error = anyhow::Error; + + fn try_from(message: &Publish) -> Result { + serde_json::from_slice(&message.payload) + .or(Err(anyhow::anyhow!("Invalid message payload received: {:?}", message.payload))) + } +} + diff --git a/src/presence.rs b/src/presence.rs index 774e669..1ede13b 100644 --- a/src/presence.rs +++ b/src/presence.rs @@ -1,11 +1,10 @@ use std::{sync::{Weak, RwLock}, collections::HashMap}; use tracing::{debug, warn, span, Level}; -use rumqttc::{AsyncClient, Publish}; -use serde::{Serialize, Deserialize}; +use rumqttc::AsyncClient; use pollster::FutureExt as _; -use crate::{mqtt::OnMqtt, config::MqttDeviceConfig}; +use crate::{mqtt::{OnMqtt, PresenceMessage}, config::MqttDeviceConfig}; pub trait OnPresence { fn on_presence(&mut self, presence: bool); @@ -41,20 +40,6 @@ impl Presence { } } -#[derive(Debug, Serialize, Deserialize)] -struct StateMessage { - state: bool -} - -impl TryFrom<&Publish> for StateMessage { - type Error = anyhow::Error; - - fn try_from(message: &Publish) -> Result { - serde_json::from_slice(&message.payload) - .or(Err(anyhow::anyhow!("Invalid message payload received: {:?}", message.payload))) - } -} - impl OnMqtt for Presence { fn on_mqtt(&mut self, message: &rumqttc::Publish) { if message.topic.starts_with(&(self.mqtt.topic.clone() + "/")) { @@ -66,16 +51,16 @@ impl OnMqtt for Presence { self.devices.remove(device_name); return; } else { - let state = match StateMessage::try_from(message) { - Ok(state) => state, + let present = match PresenceMessage::try_from(message) { + Ok(state) => state.present(), Err(err) => { warn!("Failed to parse message: {err}"); return; } }; - debug!("State of device [{device_name}] has changed: {}", state.state); - self.devices.insert(device_name.to_owned(), state.state); + debug!("State of device [{device_name}] has changed: {}", present); + self.devices.insert(device_name.to_owned(), present); } let overall_presence = self.devices.iter().any(|(_, v)| *v);