Config is passed directly to IkeaOutlet and now supports turning off automatically after a specified amount of time

This commit is contained in:
Dreaded_X 2022-12-27 22:26:42 +01:00
parent f735216dc4
commit fb455b4e4c
Signed by: Dreaded_X
GPG Key ID: 76BDEC4E165D8AD9
5 changed files with 76 additions and 34 deletions

View File

@ -11,7 +11,7 @@ username="Dreaded_X"
type = "IkeaOutlet" type = "IkeaOutlet"
info = { name = "Kettle", room = "Kitchen" } info = { name = "Kettle", room = "Kitchen" }
zigbee = { topic = "zigbee2mqtt/kitchen/kettle" } zigbee = { topic = "zigbee2mqtt/kitchen/kettle" }
kettle = {} # This is for future config kettle = { timeout = 5 }
[devices.living_workbench] [devices.living_workbench]
type = "IkeaOutlet" type = "IkeaOutlet"

View File

@ -38,7 +38,7 @@ pub struct ZigbeeDeviceConfig {
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct KettleConfig { pub struct KettleConfig {
// @TODO Add options for the kettle pub timeout: Option<u64>, // Timeout in seconds
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]

View File

@ -1,37 +1,53 @@
use std::time::Duration;
use google_home::errors::ErrorCode; use google_home::errors::ErrorCode;
use google_home::{GoogleHomeDevice, device, types::Type, traits}; use google_home::{GoogleHomeDevice, device, types::Type, traits};
use rumqttc::{AsyncClient, Publish}; use rumqttc::{AsyncClient, Publish};
use serde::{Deserialize, Serialize}; 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::devices::Device;
use crate::mqtt::Listener; use crate::mqtt::Listener;
pub struct IkeaOutlet { pub struct IkeaOutlet {
identifier: String, identifier: String,
name: String, info: InfoConfig,
room: Option<String>, zigbee: ZigbeeDeviceConfig,
topic: String, kettle: Option<KettleConfig>,
kettle: bool,
client: AsyncClient, client: AsyncClient,
last_known_state: bool, last_known_state: bool,
handle: Option<JoinHandle<()>>,
} }
impl IkeaOutlet { impl IkeaOutlet {
pub fn new(identifier: String, name: String, room: Option<String>, kettle: bool, topic: String, client: AsyncClient) -> Self { pub fn new(identifier: String, info: InfoConfig, zigbee: ZigbeeDeviceConfig, kettle: Option<KettleConfig>, client: AsyncClient) -> Self {
let c = client.clone(); let c = client.clone();
let t = topic.clone(); let t = zigbee.topic.clone();
// @TODO Handle potential errors here // @TODO Handle potential errors here
tokio::spawn(async move { tokio::spawn(async move {
c.subscribe(t, rumqttc::QoS::AtLeastOnce).await.unwrap(); 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 { impl Device for IkeaOutlet {
fn get_id(&self) -> String { fn get_id(&self) -> String {
self.identifier.clone() self.identifier.clone()
@ -59,19 +75,55 @@ impl From<&Publish> for StateMessage {
impl Listener for IkeaOutlet { impl Listener for IkeaOutlet {
fn notify(&mut self, message: &Publish) { fn notify(&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
if message.topic == self.topic { if message.topic == self.zigbee.topic {
let state = StateMessage::from(message); let new_state = StateMessage::from(message).state == "ON";
let new_state = state.state == "ON"; // No need to do anything if the state has not changed
debug!("Updating state: {} => {}", self.last_known_state, new_state); 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; 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 { impl GoogleHomeDevice for IkeaOutlet {
fn get_device_type(&self) -> Type { fn get_device_type(&self) -> Type {
if self.kettle { if self.kettle.is_some() {
Type::Kettle Type::Kettle
} else { } else {
Type::Outlet Type::Outlet
@ -79,7 +131,7 @@ impl GoogleHomeDevice for IkeaOutlet {
} }
fn get_device_name(&self) -> device::Name { fn get_device_name(&self) -> device::Name {
device::Name::new(&self.name) device::Name::new(&self.info.name)
} }
fn get_id(&self) -> String { fn get_id(&self) -> String {
@ -91,7 +143,7 @@ impl GoogleHomeDevice for IkeaOutlet {
} }
fn get_room_hint(&self) -> Option<String> { fn get_room_hint(&self) -> Option<String> {
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> { 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 client = self.client.clone();
let topic = self.topic.to_owned(); let topic = self.zigbee.topic.clone();
tokio::spawn(async move { 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(()) Ok(())

View File

@ -1,3 +1,4 @@
#![feature(specialization)] #![feature(specialization)]
pub mod devices; pub mod devices;
pub mod mqtt; pub mod mqtt;
pub mod config;

View File

@ -1,8 +1,6 @@
mod config;
use std::{time::Duration, sync::{Arc, RwLock}, process, net::SocketAddr}; use std::{time::Duration, sync::{Arc, RwLock}, process, net::SocketAddr};
use config::Config; use automation::config::{Config, Device};
use dotenv::dotenv; use dotenv::dotenv;
use warp::Filter; use warp::Filter;
use rumqttc::{MqttOptions, Transport, AsyncClient}; use rumqttc::{MqttOptions, Transport, AsyncClient};
@ -59,9 +57,9 @@ async fn main() {
debug!("Adding device {identifier}"); debug!("Adding device {identifier}");
let device: automation::devices::DeviceBox = match device_config { 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); 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()))
}, },
}; };