Started work on rust rewrite of automation system
This commit is contained in:
46
src/devices.rs
Normal file
46
src/devices.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
mod ikea_outlet;
|
||||
|
||||
use crate::{mqtt::Listener, state::StateOnOff};
|
||||
|
||||
pub use self::ikea_outlet::IkeaOutlet;
|
||||
|
||||
pub trait Device {
|
||||
fn get_identifier(&self) -> &str;
|
||||
|
||||
fn as_state_on_off(&mut self) -> Option<&mut dyn StateOnOff>;
|
||||
|
||||
fn as_listener(&mut self) -> Option<&mut dyn Listener>;
|
||||
}
|
||||
|
||||
pub struct Devices {
|
||||
devices: Vec<Box<dyn Device>>,
|
||||
}
|
||||
|
||||
impl Devices {
|
||||
pub fn new() -> Self {
|
||||
Self { devices: Vec::new() }
|
||||
}
|
||||
|
||||
pub fn add_device<T: Device + 'static>(&mut self, device: T) {
|
||||
self.devices.push(Box::new(device));
|
||||
}
|
||||
|
||||
pub fn as_listeners(&mut self) -> Vec<&mut dyn Listener> {
|
||||
self.devices.iter_mut().filter_map(|device| device.as_listener()).collect()
|
||||
}
|
||||
|
||||
pub fn get_device(&mut self, index: usize) -> Option<&mut dyn Device> {
|
||||
if let Some(device) = self.devices.get_mut(index) {
|
||||
return Some(device.as_mut());
|
||||
}
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
impl Listener for Devices {
|
||||
fn notify(&mut self, message: &rumqttc::Publish) {
|
||||
self.as_listeners().iter_mut().for_each(|listener| {
|
||||
listener.notify(message);
|
||||
})
|
||||
}
|
||||
}
|
||||
86
src/devices/ikea_outlet.rs
Normal file
86
src/devices/ikea_outlet.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
use rumqttc::{Client, Publish};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::devices::Device;
|
||||
use crate::mqtt::Listener;
|
||||
use crate::state::StateOnOff;
|
||||
use crate::zigbee::Zigbee;
|
||||
|
||||
pub struct IkeaOutlet {
|
||||
zigbee: Zigbee,
|
||||
client: Client,
|
||||
last_known_state: bool,
|
||||
}
|
||||
|
||||
impl IkeaOutlet {
|
||||
pub fn new(zigbee: Zigbee, mut client: Client) -> Self {
|
||||
client.subscribe(zigbee.get_topic(), rumqttc::QoS::AtLeastOnce).unwrap();
|
||||
Self{ zigbee, client, last_known_state: false }
|
||||
}
|
||||
}
|
||||
|
||||
impl Device for IkeaOutlet {
|
||||
fn get_identifier(& self) -> &str {
|
||||
&self.zigbee.get_friendly_name()
|
||||
}
|
||||
|
||||
fn as_state_on_off(&mut self) -> Option<&mut dyn StateOnOff> {
|
||||
Some(self)
|
||||
}
|
||||
|
||||
fn as_listener(&mut self) -> Option<&mut dyn Listener> {
|
||||
Some(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct StateMessage {
|
||||
state: String
|
||||
}
|
||||
|
||||
impl From<&Publish> for StateMessage {
|
||||
fn from(p: &Publish) -> Self {
|
||||
let parsed = match serde_json::from_slice(&p.payload) {
|
||||
Ok(outlet) => outlet,
|
||||
Err(err) => {
|
||||
panic!("{}", err);
|
||||
}
|
||||
};
|
||||
|
||||
parsed
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
let state = StateMessage::from(message);
|
||||
|
||||
print!("Updating state: {} => ", self.last_known_state);
|
||||
self.last_known_state = state.state == "ON";
|
||||
println!("{}", self.last_known_state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StateOnOff for IkeaOutlet {
|
||||
// This will send a message over mqtt to update change the state of the device
|
||||
// It does not change the internal state, that gets updated when the device responds
|
||||
fn set_state(&mut self, state: bool) {
|
||||
let topic = self.zigbee.get_topic().to_owned();
|
||||
let message = StateMessage{
|
||||
state: if state {
|
||||
"ON".to_owned()
|
||||
} else {
|
||||
"OFF".to_owned()
|
||||
}
|
||||
};
|
||||
|
||||
self.client.publish(topic + "/set", rumqttc::QoS::AtLeastOnce, false, serde_json::to_string(&message).unwrap()).unwrap();
|
||||
}
|
||||
|
||||
fn get_state(&self) -> bool {
|
||||
self.last_known_state
|
||||
}
|
||||
}
|
||||
4
src/lib.rs
Normal file
4
src/lib.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub mod devices;
|
||||
pub mod zigbee;
|
||||
mod state;
|
||||
pub mod mqtt;
|
||||
46
src/main.rs
Normal file
46
src/main.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
use std::{time::Duration, rc::Rc, cell::RefCell, process::exit};
|
||||
|
||||
use dotenv::dotenv;
|
||||
|
||||
use automation::{devices::{Devices, IkeaOutlet}, zigbee::Zigbee, mqtt::Notifier};
|
||||
use rumqttc::{MqttOptions, Transport, Client};
|
||||
|
||||
fn get_required_env(name: &str) -> String {
|
||||
match std::env::var(name) {
|
||||
Ok(value) => value,
|
||||
_ => {
|
||||
eprintln!("Environment variable ${name} is not set!");
|
||||
exit(-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
dotenv().ok();
|
||||
|
||||
// Setup MQTT
|
||||
let mut mqttoptions = MqttOptions::new("rust-test", get_required_env("MQTT_HOST"), 8883);
|
||||
mqttoptions.set_credentials(get_required_env("MQTT_USERNAME"), get_required_env("MQTT_PASSWORD"));
|
||||
mqttoptions.set_keep_alive(Duration::from_secs(5));
|
||||
mqttoptions.set_transport(Transport::tls_with_default_config());
|
||||
|
||||
let (client, connection) = Client::new(mqttoptions, 10);
|
||||
|
||||
// Create device holder
|
||||
let devices = Rc::new(RefCell::new(Devices::new()));
|
||||
|
||||
// Create a new device and add it to the holder
|
||||
devices.borrow_mut().add_device(IkeaOutlet::new(Zigbee::new("kitchen/kettle", "zigbee2mqtt/kitchen/kettle"), client.clone()));
|
||||
|
||||
let mut notifier = Notifier::new();
|
||||
|
||||
{
|
||||
let mut temp = devices.borrow_mut();
|
||||
let a = temp.get_device(0);
|
||||
a.unwrap().as_state_on_off().unwrap().set_state(false);
|
||||
}
|
||||
|
||||
notifier.add_listener(Rc::downgrade(&devices));
|
||||
|
||||
notifier.start(connection);
|
||||
}
|
||||
48
src/mqtt.rs
Normal file
48
src/mqtt.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
use std::{rc::Weak, cell::RefCell};
|
||||
|
||||
use rumqttc::{Publish, Connection, Event, Incoming};
|
||||
|
||||
pub trait Listener {
|
||||
fn notify(&mut self, message: &Publish);
|
||||
}
|
||||
|
||||
pub struct Notifier {
|
||||
listeners: Vec<Weak<RefCell<dyn Listener>>>,
|
||||
}
|
||||
|
||||
impl Notifier {
|
||||
pub fn new() -> Self {
|
||||
return Self { listeners: Vec::new() }
|
||||
}
|
||||
|
||||
fn notify(&mut self, message: Publish) {
|
||||
self.listeners.retain(|listener| {
|
||||
if let Some(listener) = listener.upgrade() {
|
||||
listener.borrow_mut().notify(&message);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
})
|
||||
}
|
||||
|
||||
pub fn add_listener<T: Listener + 'static>(&mut self, listener: Weak<RefCell<T>>) {
|
||||
self.listeners.push(listener);
|
||||
}
|
||||
|
||||
pub fn start(&mut self, mut connection: Connection) {
|
||||
for notification in connection.iter() {
|
||||
match notification {
|
||||
Ok(Event::Incoming(Incoming::Publish(p))) => {
|
||||
println!("{:?}", p);
|
||||
self.notify(p);
|
||||
},
|
||||
Ok(..) => continue,
|
||||
Err(err) => {
|
||||
eprintln!("{}", err);
|
||||
break
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
4
src/state.rs
Normal file
4
src/state.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub trait StateOnOff {
|
||||
fn set_state(&mut self, state: bool);
|
||||
fn get_state(&self) -> bool;
|
||||
}
|
||||
24
src/zigbee.rs
Normal file
24
src/zigbee.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
#[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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user