Started work on rust rewrite of automation system

This commit is contained in:
2022-12-10 18:03:01 +01:00
commit 68cbccd72c
10 changed files with 1084 additions and 0 deletions

46
src/devices.rs Normal file
View 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);
})
}
}

View 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
View File

@@ -0,0 +1,4 @@
pub mod devices;
pub mod zigbee;
mod state;
pub mod mqtt;

46
src/main.rs Normal file
View 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
View 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
View 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
View 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
}
}