From fb455b4e4c6676e24d1d3469e57a00f897aaa4af Mon Sep 17 00:00:00 2001 From: Dreaded_X Date: Tue, 27 Dec 2022 22:26:42 +0100 Subject: [PATCH] Config is passed directly to IkeaOutlet and now supports turning off automatically after a specified amount of time --- config/zeus.dev.toml | 2 +- src/config.rs | 2 +- src/devices/ikea_outlet.rs | 97 +++++++++++++++++++++++++++----------- src/lib.rs | 1 + src/main.rs | 8 ++-- 5 files changed, 76 insertions(+), 34 deletions(-) diff --git a/config/zeus.dev.toml b/config/zeus.dev.toml index 6a16ed0..fa01713 100644 --- a/config/zeus.dev.toml +++ b/config/zeus.dev.toml @@ -11,7 +11,7 @@ username="Dreaded_X" type = "IkeaOutlet" info = { name = "Kettle", room = "Kitchen" } zigbee = { topic = "zigbee2mqtt/kitchen/kettle" } -kettle = {} # This is for future config +kettle = { timeout = 5 } [devices.living_workbench] type = "IkeaOutlet" diff --git a/src/config.rs b/src/config.rs index 20a952c..1e2b7ce 100644 --- a/src/config.rs +++ b/src/config.rs @@ -38,7 +38,7 @@ pub struct ZigbeeDeviceConfig { #[derive(Debug, Deserialize)] pub struct KettleConfig { - // @TODO Add options for the kettle + pub timeout: Option, // Timeout in seconds } #[derive(Debug, Deserialize)] diff --git a/src/devices/ikea_outlet.rs b/src/devices/ikea_outlet.rs index 8136b9c..00e9018 100644 --- a/src/devices/ikea_outlet.rs +++ b/src/devices/ikea_outlet.rs @@ -1,37 +1,53 @@ +use std::time::Duration; + use google_home::errors::ErrorCode; use google_home::{GoogleHomeDevice, device, types::Type, traits}; use rumqttc::{AsyncClient, Publish}; use serde::{Deserialize, Serialize}; -use log::debug; +use log::{debug, trace}; +use tokio::task::JoinHandle; +use crate::config::{KettleConfig, InfoConfig, ZigbeeDeviceConfig}; use crate::devices::Device; use crate::mqtt::Listener; pub struct IkeaOutlet { identifier: String, - name: String, - room: Option, - topic: String, - - kettle: bool, + info: InfoConfig, + zigbee: ZigbeeDeviceConfig, + kettle: Option, client: AsyncClient, last_known_state: bool, + handle: Option>, } impl IkeaOutlet { - pub fn new(identifier: String, name: String, room: Option, kettle: bool, topic: String, client: AsyncClient) -> Self { + pub fn new(identifier: String, info: InfoConfig, zigbee: ZigbeeDeviceConfig, kettle: Option, client: AsyncClient) -> Self { let c = client.clone(); - let t = topic.clone(); + let t = zigbee.topic.clone(); // @TODO Handle potential errors here tokio::spawn(async move { c.subscribe(t, rumqttc::QoS::AtLeastOnce).await.unwrap(); }); - Self{ identifier, name, room, kettle, topic, client, last_known_state: false } + Self{ identifier, info, zigbee, kettle, client, last_known_state: false, handle: None } } } +async fn set_on(client: AsyncClient, topic: String, on: bool) { + let message = StateMessage{ + state: if on { + "ON".to_owned() + } else { + "OFF".to_owned() + } + }; + + // @TODO Handle potential errors here + client.publish(topic + "/set", rumqttc::QoS::AtLeastOnce, false, serde_json::to_string(&message).unwrap()).await.unwrap(); +} + impl Device for IkeaOutlet { fn get_id(&self) -> String { self.identifier.clone() @@ -59,19 +75,55 @@ impl From<&Publish> for StateMessage { impl Listener for IkeaOutlet { fn notify(&mut self, message: &Publish) { // Update the internal state based on what the device has reported - if message.topic == self.topic { - let state = StateMessage::from(message); + if message.topic == self.zigbee.topic { + let new_state = StateMessage::from(message).state == "ON"; - let new_state = state.state == "ON"; - debug!("Updating state: {} => {}", self.last_known_state, new_state); + // No need to do anything if the state has not changed + if new_state == self.last_known_state { + return; + } + + // Abort any timer that is currently running + if let Some(handle) = self.handle.take() { + handle.abort(); + } + + trace!("Updating state: {} => {}", self.last_known_state, new_state); self.last_known_state = new_state; + + // If this is a kettle start a timeout for turning it of again + if new_state { + if let Some(kettle) = &self.kettle { + if let Some(timeout) = kettle.timeout.clone() { + let client = self.client.clone(); + let topic = self.zigbee.topic.clone(); + + // Turn the kettle of after the specified timeout + // @TODO Impl Drop for IkeaOutlet that will abort the handle if the IkeaOutlet + // get dropped + self.handle = Some( + tokio::spawn(async move { + debug!("Starting timeout ({timeout}s) for kettle..."); + tokio::time::sleep(Duration::from_secs(timeout)).await; + // @TODO We need to call set_on(false) in order to turn the device off + // again, how are we going to do this? + debug!("Turning kettle off!"); + set_on(client, topic, false).await; + }) + ); + } else { + trace!("Outlet is a kettle without timeout"); + } + + } + } } } } impl GoogleHomeDevice for IkeaOutlet { fn get_device_type(&self) -> Type { - if self.kettle { + if self.kettle.is_some() { Type::Kettle } else { Type::Outlet @@ -79,7 +131,7 @@ impl GoogleHomeDevice for IkeaOutlet { } fn get_device_name(&self) -> device::Name { - device::Name::new(&self.name) + device::Name::new(&self.info.name) } fn get_id(&self) -> String { @@ -91,7 +143,7 @@ impl GoogleHomeDevice for IkeaOutlet { } fn get_room_hint(&self) -> Option { - self.room.clone() + self.info.room.clone() } } @@ -101,19 +153,10 @@ impl traits::OnOff for IkeaOutlet { } fn set_on(&mut self, on: bool) -> Result<(), ErrorCode> { - let message = StateMessage{ - state: if on { - "ON".to_owned() - } else { - "OFF".to_owned() - } - }; - - // @TODO Handle potential errors here let client = self.client.clone(); - let topic = self.topic.to_owned(); + let topic = self.zigbee.topic.clone(); tokio::spawn(async move { - client.publish(topic + "/set", rumqttc::QoS::AtLeastOnce, false, serde_json::to_string(&message).unwrap()).await.unwrap(); + set_on(client, topic, on).await; }); Ok(()) diff --git a/src/lib.rs b/src/lib.rs index 7b65e19..b97b8bd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ #![feature(specialization)] pub mod devices; pub mod mqtt; +pub mod config; diff --git a/src/main.rs b/src/main.rs index c42789f..6d7dff3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,6 @@ -mod config; - use std::{time::Duration, sync::{Arc, RwLock}, process, net::SocketAddr}; -use config::Config; +use automation::config::{Config, Device}; use dotenv::dotenv; use warp::Filter; use rumqttc::{MqttOptions, Transport, AsyncClient}; @@ -59,9 +57,9 @@ async fn main() { debug!("Adding device {identifier}"); let device: automation::devices::DeviceBox = match device_config { - config::Device::IkeaOutlet { info, zigbee, kettle } => { + Device::IkeaOutlet { info, zigbee, kettle } => { trace!("\tIkeaOutlet [{} in {:?}]", info.name, info.room); - Box::new(IkeaOutlet::new(identifier, info.name, info.room, kettle.is_some(), zigbee.topic, client.clone())) + Box::new(IkeaOutlet::new(identifier, info, zigbee, kettle, client.clone())) }, };