Devices and some settings are now loaded from the config file instead of hardcoded

This commit is contained in:
Dreaded_X 2022-12-27 04:00:35 +01:00
parent c45ef583b1
commit f735216dc4
Signed by: Dreaded_X
GPG Key ID: 76BDEC4E165D8AD9
8 changed files with 100 additions and 87 deletions

View File

@ -2,3 +2,18 @@
host="olympus.lan.huizinga.dev" host="olympus.lan.huizinga.dev"
port=8883 port=8883
username="mqtt" 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" }

View File

@ -1,21 +1,56 @@
use std::{fs, error::Error}; use std::{fs, error::Error, collections::HashMap};
use log::debug; use log::debug;
use serde::Deserialize; use serde::Deserialize;
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct Config { pub struct Config {
pub mqtt: MQTT, pub mqtt: MQTTConfig,
pub fullfillment: FullfillmentConfig,
#[serde(default)]
pub devices: HashMap<String, Device>
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct MQTT { pub struct MQTTConfig {
pub host: String, pub host: String,
pub port: u16, pub port: u16,
pub username: String, pub username: String,
pub password: Option<String>, pub password: Option<String>,
} }
#[derive(Debug, Deserialize)]
pub struct FullfillmentConfig {
pub port: u16,
pub username: String,
}
#[derive(Debug, Deserialize)]
pub struct InfoConfig {
pub name: String,
pub room: Option<String>,
}
#[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<KettleConfig>,
},
}
impl Config { impl Config {
pub fn build(filename: &str) -> Result<Self, Box<dyn Error>> { pub fn build(filename: &str) -> Result<Self, Box<dyn Error>> {
debug!("Loading config: {filename}"); debug!("Loading config: {filename}");

View File

@ -1,9 +1,6 @@
mod ikea_outlet; mod ikea_outlet;
pub use self::ikea_outlet::IkeaOutlet; pub use self::ikea_outlet::IkeaOutlet;
mod test_outlet;
pub use self::test_outlet::TestOutlet;
use std::collections::HashMap; use std::collections::HashMap;
use google_home::{GoogleHomeDevice, traits::OnOff}; 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<RwLock<>> to make this type a little bit nicer // @TODO Add an inner type that we can wrap with Arc<RwLock<>> to make this type a little bit nicer
// to work with // to work with
pub struct Devices { pub struct Devices {
devices: HashMap<String, Box<dyn Device + Sync + Send>>, devices: HashMap<String, DeviceBox>,
} }
macro_rules! get_cast { macro_rules! get_cast {
@ -41,13 +38,15 @@ macro_rules! get_cast {
}; };
} }
pub type DeviceBox = Box<dyn Device + Sync + Send>;
impl Devices { impl Devices {
pub fn new() -> Self { pub fn new() -> Self {
Self { devices: HashMap::new() } Self { devices: HashMap::new() }
} }
pub fn add_device<T: Device + Sync + Send + 'static>(&mut self, device: T) { pub fn add_device(&mut self, device: DeviceBox) {
self.devices.insert(device.get_id(), Box::new(device)); self.devices.insert(device.get_id(), device);
} }
get_cast!(Listener); get_cast!(Listener);

View File

@ -6,31 +6,35 @@ use log::debug;
use crate::devices::Device; use crate::devices::Device;
use crate::mqtt::Listener; use crate::mqtt::Listener;
use crate::zigbee::Zigbee;
pub struct IkeaOutlet { pub struct IkeaOutlet {
identifier: String,
name: String, name: String,
zigbee: Zigbee, room: Option<String>,
topic: String,
kettle: bool,
client: AsyncClient, client: AsyncClient,
last_known_state: bool, last_known_state: bool,
} }
impl IkeaOutlet { impl IkeaOutlet {
pub fn new(name: String, zigbee: Zigbee, client: AsyncClient) -> Self { pub fn new(identifier: String, name: String, room: Option<String>, kettle: bool, topic: String, client: AsyncClient) -> Self {
let c = client.clone(); let c = client.clone();
let topic = zigbee.get_topic().to_owned(); let t = topic.clone();
// @TODO Handle potential errors here // @TODO Handle potential errors here
tokio::spawn(async move { 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 { impl Device for IkeaOutlet {
fn get_id(&self) -> String { 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 { 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.zigbee.get_topic() { if message.topic == self.topic {
let state = StateMessage::from(message); let state = StateMessage::from(message);
let new_state = state.state == "ON"; let new_state = state.state == "ON";
@ -67,7 +71,11 @@ impl Listener for IkeaOutlet {
impl GoogleHomeDevice for IkeaOutlet { impl GoogleHomeDevice for IkeaOutlet {
fn get_device_type(&self) -> Type { fn get_device_type(&self) -> Type {
Type::Kettle if self.kettle {
Type::Kettle
} else {
Type::Outlet
}
} }
fn get_device_name(&self) -> device::Name { fn get_device_name(&self) -> device::Name {
@ -81,6 +89,10 @@ impl GoogleHomeDevice for IkeaOutlet {
fn is_online(&self) -> bool { fn is_online(&self) -> bool {
true true
} }
fn get_room_hint(&self) -> Option<String> {
self.room.clone()
}
} }
impl traits::OnOff for IkeaOutlet { impl traits::OnOff for IkeaOutlet {
@ -89,7 +101,6 @@ impl traits::OnOff for IkeaOutlet {
} }
fn set_on(&mut self, on: bool) -> Result<(), ErrorCode> { fn set_on(&mut self, on: bool) -> Result<(), ErrorCode> {
let topic = self.zigbee.get_topic().to_owned();
let message = StateMessage{ let message = StateMessage{
state: if on { state: if on {
"ON".to_owned() "ON".to_owned()
@ -99,9 +110,8 @@ impl traits::OnOff for IkeaOutlet {
}; };
// @TODO Handle potential errors here // @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 client = self.client.clone();
let topic = self.topic.to_owned();
tokio::spawn(async move { tokio::spawn(async move {
client.publish(topic + "/set", rumqttc::QoS::AtLeastOnce, false, serde_json::to_string(&message).unwrap()).await.unwrap(); client.publish(topic + "/set", rumqttc::QoS::AtLeastOnce, false, serde_json::to_string(&message).unwrap()).await.unwrap();
}); });

View File

@ -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<bool, ErrorCode> {
Ok(self.on)
}
fn set_on(&mut self, on: bool) -> Result<(), ErrorCode> {
debug!("Setting on: {on}");
self.on = on;
Ok(())
}
}

View File

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

View File

@ -7,9 +7,9 @@ use dotenv::dotenv;
use warp::Filter; use warp::Filter;
use rumqttc::{MqttOptions, Transport, AsyncClient}; use rumqttc::{MqttOptions, Transport, AsyncClient};
use env_logger::Builder; 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}; use google_home::{GoogleHome, Request};
#[tokio::main] #[tokio::main]
@ -28,6 +28,8 @@ async fn main() {
process::exit(1); process::exit(1);
}); });
debug!("Config: {config:#?}");
info!("Starting automation_rs..."); info!("Starting automation_rs...");
// Create device holder // 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!("Error in MQTT (most likely lost connection to mqtt server), we need to handle these errors!");
}); });
// @TODO Load these from a config // Create devices based on config
// Create a new device and add it to the holder // @TODO Move out of main (config? or maybe devices?)
devices.write().unwrap().add_device(IkeaOutlet::new("Kettle".into(), Zigbee::new("kitchen/kettle", "zigbee2mqtt/kitchen/kettle"), client.clone())); for (identifier, device_config) in config.devices {
devices.write().unwrap().add_device(TestOutlet::new()); 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 // Google Home fullfillments
let fullfillment_google_home = warp::path("google_home") let fullfillment_google_home = warp::path("google_home")
@ -63,7 +75,7 @@ async fn main() {
.map(move |request: Request| { .map(move |request: Request| {
// @TODO Verify that we are actually logged in // @TODO Verify that we are actually logged in
// Might also be smart to get the username from here // 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(); let result = gc.handle_request(request, &mut devices.write().unwrap().as_google_home_devices()).unwrap();
warp::reply::json(&result) warp::reply::json(&result)
@ -76,7 +88,7 @@ async fn main() {
let routes = fullfillment; let routes = fullfillment;
// Start the web server // 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}"); info!("Server started on http://{addr}");
warp::serve(routes) warp::serve(routes)
.run(addr) .run(addr)

View File

@ -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
}
}