From f735216dc4b9af0b2b2a74c9762c4df48c35a291 Mon Sep 17 00:00:00 2001 From: Dreaded_X Date: Tue, 27 Dec 2022 04:00:35 +0100 Subject: [PATCH] Devices and some settings are now loaded from the config file instead of hardcoded --- config/zeus.dev.toml | 15 ++++++++++++++ src/config.rs | 41 +++++++++++++++++++++++++++++++++++--- src/devices.rs | 11 +++++----- src/devices/ikea_outlet.rs | 34 ++++++++++++++++++++----------- src/devices/test_outlet.rs | 33 ------------------------------ src/lib.rs | 1 - src/main.rs | 28 ++++++++++++++++++-------- src/zigbee.rs | 24 ---------------------- 8 files changed, 100 insertions(+), 87 deletions(-) delete mode 100644 src/devices/test_outlet.rs delete mode 100644 src/zigbee.rs diff --git a/config/zeus.dev.toml b/config/zeus.dev.toml index 09adba5..6a16ed0 100644 --- a/config/zeus.dev.toml +++ b/config/zeus.dev.toml @@ -2,3 +2,18 @@ host="olympus.lan.huizinga.dev" port=8883 username="mqtt" + +[fullfillment] +port=7878 +username="Dreaded_X" + +[devices.kitchen_kettle] +type = "IkeaOutlet" +info = { name = "Kettle", room = "Kitchen" } +zigbee = { topic = "zigbee2mqtt/kitchen/kettle" } +kettle = {} # This is for future config + +[devices.living_workbench] +type = "IkeaOutlet" +info = { name = "Workbench", room = "Living Room" } +zigbee = { topic = "zigbee2mqtt/living/workbench" } diff --git a/src/config.rs b/src/config.rs index c0e3629..20a952c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,21 +1,56 @@ -use std::{fs, error::Error}; +use std::{fs, error::Error, collections::HashMap}; use log::debug; use serde::Deserialize; #[derive(Debug, Deserialize)] pub struct Config { - pub mqtt: MQTT, + pub mqtt: MQTTConfig, + pub fullfillment: FullfillmentConfig, + #[serde(default)] + pub devices: HashMap } #[derive(Debug, Deserialize)] -pub struct MQTT { +pub struct MQTTConfig { pub host: String, pub port: u16, pub username: String, pub password: Option, } +#[derive(Debug, Deserialize)] +pub struct FullfillmentConfig { + pub port: u16, + pub username: String, +} + +#[derive(Debug, Deserialize)] +pub struct InfoConfig { + pub name: String, + pub room: Option, +} + +#[derive(Debug, Deserialize)] +pub struct ZigbeeDeviceConfig { + pub topic: String, +} + +#[derive(Debug, Deserialize)] +pub struct KettleConfig { + // @TODO Add options for the kettle +} + +#[derive(Debug, Deserialize)] +#[serde(tag = "type")] +pub enum Device { + IkeaOutlet { + info: InfoConfig, + zigbee: ZigbeeDeviceConfig, + kettle: Option, + }, +} + impl Config { pub fn build(filename: &str) -> Result> { debug!("Loading config: {filename}"); diff --git a/src/devices.rs b/src/devices.rs index 77768a6..b6cda79 100644 --- a/src/devices.rs +++ b/src/devices.rs @@ -1,9 +1,6 @@ mod ikea_outlet; pub use self::ikea_outlet::IkeaOutlet; -mod test_outlet; -pub use self::test_outlet::TestOutlet; - use std::collections::HashMap; use google_home::{GoogleHomeDevice, traits::OnOff}; @@ -21,7 +18,7 @@ pub trait Device: AsGoogleHomeDevice + AsListener + AsOnOff { // @TODO Add an inner type that we can wrap with Arc> to make this type a little bit nicer // to work with pub struct Devices { - devices: HashMap>, + devices: HashMap, } macro_rules! get_cast { @@ -41,13 +38,15 @@ macro_rules! get_cast { }; } +pub type DeviceBox = Box; + impl Devices { pub fn new() -> Self { Self { devices: HashMap::new() } } - pub fn add_device(&mut self, device: T) { - self.devices.insert(device.get_id(), Box::new(device)); + pub fn add_device(&mut self, device: DeviceBox) { + self.devices.insert(device.get_id(), device); } get_cast!(Listener); diff --git a/src/devices/ikea_outlet.rs b/src/devices/ikea_outlet.rs index a70863a..8136b9c 100644 --- a/src/devices/ikea_outlet.rs +++ b/src/devices/ikea_outlet.rs @@ -6,31 +6,35 @@ use log::debug; use crate::devices::Device; use crate::mqtt::Listener; -use crate::zigbee::Zigbee; pub struct IkeaOutlet { + identifier: String, name: String, - zigbee: Zigbee, + room: Option, + topic: String, + + kettle: bool, + client: AsyncClient, last_known_state: bool, } impl IkeaOutlet { - pub fn new(name: String, zigbee: Zigbee, client: AsyncClient) -> Self { + pub fn new(identifier: String, name: String, room: Option, kettle: bool, topic: String, client: AsyncClient) -> Self { let c = client.clone(); - let topic = zigbee.get_topic().to_owned(); + let t = topic.clone(); // @TODO Handle potential errors here tokio::spawn(async move { - c.subscribe(topic, rumqttc::QoS::AtLeastOnce).await.unwrap(); + c.subscribe(t, rumqttc::QoS::AtLeastOnce).await.unwrap(); }); - Self{ name, zigbee, client, last_known_state: false } + Self{ identifier, name, room, kettle, topic, client, last_known_state: false } } } impl Device for IkeaOutlet { fn get_id(&self) -> String { - self.zigbee.get_friendly_name().into() + self.identifier.clone() } } @@ -55,7 +59,7 @@ 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.zigbee.get_topic() { + if message.topic == self.topic { let state = StateMessage::from(message); let new_state = state.state == "ON"; @@ -67,7 +71,11 @@ impl Listener for IkeaOutlet { impl GoogleHomeDevice for IkeaOutlet { fn get_device_type(&self) -> Type { - Type::Kettle + if self.kettle { + Type::Kettle + } else { + Type::Outlet + } } fn get_device_name(&self) -> device::Name { @@ -81,6 +89,10 @@ impl GoogleHomeDevice for IkeaOutlet { fn is_online(&self) -> bool { true } + + fn get_room_hint(&self) -> Option { + self.room.clone() + } } impl traits::OnOff for IkeaOutlet { @@ -89,7 +101,6 @@ impl traits::OnOff for IkeaOutlet { } fn set_on(&mut self, on: bool) -> Result<(), ErrorCode> { - let topic = self.zigbee.get_topic().to_owned(); let message = StateMessage{ state: if on { "ON".to_owned() @@ -99,9 +110,8 @@ impl traits::OnOff for IkeaOutlet { }; // @TODO Handle potential errors here - // @NOTE We are blocking here, ideally this function would just be async, however that is - // currently not really possible let client = self.client.clone(); + let topic = self.topic.to_owned(); tokio::spawn(async move { client.publish(topic + "/set", rumqttc::QoS::AtLeastOnce, false, serde_json::to_string(&message).unwrap()).await.unwrap(); }); diff --git a/src/devices/test_outlet.rs b/src/devices/test_outlet.rs deleted file mode 100644 index 07a5878..0000000 --- a/src/devices/test_outlet.rs +++ /dev/null @@ -1,33 +0,0 @@ -use log::debug; - -use google_home::{errors::ErrorCode, traits}; - -use super::Device; - -pub struct TestOutlet { - on: bool -} - -impl TestOutlet { - pub fn new() -> Self { - Self { on: false } - } -} - -impl Device for TestOutlet { - fn get_id(&self) -> String { - "test_device".into() - } -} - -impl traits::OnOff for TestOutlet { - fn is_on(&self) -> Result { - Ok(self.on) - } - - fn set_on(&mut self, on: bool) -> Result<(), ErrorCode> { - debug!("Setting on: {on}"); - self.on = on; - Ok(()) - } -} diff --git a/src/lib.rs b/src/lib.rs index 59dd5fa..7b65e19 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,3 @@ #![feature(specialization)] pub mod devices; -pub mod zigbee; pub mod mqtt; diff --git a/src/main.rs b/src/main.rs index a8d0397..c42789f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,9 +7,9 @@ use dotenv::dotenv; use warp::Filter; use rumqttc::{MqttOptions, Transport, AsyncClient}; use env_logger::Builder; -use log::{error, info, LevelFilter}; +use log::{error, info, debug, trace, LevelFilter}; -use automation::{devices::{Devices, IkeaOutlet, TestOutlet}, zigbee::Zigbee, mqtt::Notifier}; +use automation::{devices::{Devices, IkeaOutlet}, mqtt::Notifier}; use google_home::{GoogleHome, Request}; #[tokio::main] @@ -28,6 +28,8 @@ async fn main() { process::exit(1); }); + debug!("Config: {config:#?}"); + info!("Starting automation_rs..."); // Create device holder @@ -51,10 +53,20 @@ async fn main() { todo!("Error in MQTT (most likely lost connection to mqtt server), we need to handle these errors!"); }); - // @TODO Load these from a config - // Create a new device and add it to the holder - devices.write().unwrap().add_device(IkeaOutlet::new("Kettle".into(), Zigbee::new("kitchen/kettle", "zigbee2mqtt/kitchen/kettle"), client.clone())); - devices.write().unwrap().add_device(TestOutlet::new()); + // Create devices based on config + // @TODO Move out of main (config? or maybe devices?) + for (identifier, device_config) in config.devices { + debug!("Adding device {identifier}"); + + let device: automation::devices::DeviceBox = match device_config { + config::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())) + }, + }; + + devices.write().unwrap().add_device(device); + } // Google Home fullfillments let fullfillment_google_home = warp::path("google_home") @@ -63,7 +75,7 @@ async fn main() { .map(move |request: Request| { // @TODO Verify that we are actually logged in // Might also be smart to get the username from here - let gc = GoogleHome::new("Dreaded_X"); + let gc = GoogleHome::new(&config.fullfillment.username); let result = gc.handle_request(request, &mut devices.write().unwrap().as_google_home_devices()).unwrap(); warp::reply::json(&result) @@ -76,7 +88,7 @@ async fn main() { let routes = fullfillment; // Start the web server - let addr: SocketAddr = ([127, 0, 0, 1], 7878).into(); + let addr: SocketAddr = ([127, 0, 0, 1], config.fullfillment.port).into(); info!("Server started on http://{addr}"); warp::serve(routes) .run(addr) diff --git a/src/zigbee.rs b/src/zigbee.rs deleted file mode 100644 index e1570e3..0000000 --- a/src/zigbee.rs +++ /dev/null @@ -1,24 +0,0 @@ -#[derive(Debug)] -pub struct Zigbee { - friendly_name: String, - // manufacturer: String, - topic: String, -} - -impl Zigbee { - pub fn new(friendly_name: &str, topic: &str) -> Self { - Self { - friendly_name: friendly_name.to_owned(), - // manufacturer: String::from("IKEA"), - topic: topic.to_owned(), - } - } - - pub fn get_friendly_name(&self) -> &str { - &self.friendly_name - } - - pub fn get_topic(&self) -> &str { - &self.topic - } -}