Devices and some settings are now loaded from the config file instead of hardcoded
This commit is contained in:
parent
c45ef583b1
commit
f735216dc4
|
@ -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" }
|
||||
|
|
|
@ -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<String, Device>
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct MQTT {
|
||||
pub struct MQTTConfig {
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
pub username: 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 {
|
||||
pub fn build(filename: &str) -> Result<Self, Box<dyn Error>> {
|
||||
debug!("Loading config: {filename}");
|
||||
|
|
|
@ -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<RwLock<>> to make this type a little bit nicer
|
||||
// to work with
|
||||
pub struct Devices {
|
||||
devices: HashMap<String, Box<dyn Device + Sync + Send>>,
|
||||
devices: HashMap<String, DeviceBox>,
|
||||
}
|
||||
|
||||
macro_rules! get_cast {
|
||||
|
@ -41,13 +38,15 @@ macro_rules! get_cast {
|
|||
};
|
||||
}
|
||||
|
||||
pub type DeviceBox = Box<dyn Device + Sync + Send>;
|
||||
|
||||
impl Devices {
|
||||
pub fn new() -> Self {
|
||||
Self { devices: HashMap::new() }
|
||||
}
|
||||
|
||||
pub fn add_device<T: Device + Sync + Send + 'static>(&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);
|
||||
|
|
|
@ -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<String>,
|
||||
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<String>, 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<String> {
|
||||
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();
|
||||
});
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
|
@ -1,4 +1,3 @@
|
|||
#![feature(specialization)]
|
||||
pub mod devices;
|
||||
pub mod zigbee;
|
||||
pub mod mqtt;
|
||||
|
|
28
src/main.rs
28
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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user