Some cleanup and added light sensor

This commit is contained in:
Dreaded_X 2023-01-03 20:46:37 +01:00
parent cfd10a7daf
commit c9b2127eed
Signed by: Dreaded_X
GPG Key ID: 76BDEC4E165D8AD9
10 changed files with 182 additions and 43 deletions

View File

@ -9,25 +9,33 @@ username="Dreaded_X"
[presence]
topic = "automation_dev/presence"
[light_sensor]
topic = "zigbee2mqtt/living/light"
min = 23_000
max = 25_000
[devices.kitchen_kettle]
type = "IkeaOutlet"
info = { name = "Kettle", room = "Kitchen" }
mqtt = { topic = "zigbee2mqtt/kitchen/kettle" }
name = "Kettle"
room = "Kitchen"
topic = "zigbee2mqtt/kitchen/kettle"
kettle = { timeout = 5 }
[devices.living_workbench]
type = "IkeaOutlet"
info = { name = "Workbench", room = "Living Room" }
mqtt = { topic = "zigbee2mqtt/living/workbench" }
name = "Workbench"
room = "Living Room"
topic = "zigbee2mqtt/living/workbench"
[devices.living_zeus]
type = "WakeOnLAN"
info = { name = "Zeus", room = "Living Room" }
mqtt = { topic = "automation/appliance/living_room/zeus" }
name = "Zeus"
room = "Living Room"
topic = "automation/appliance/living_room/zeus"
mac_address = "30:9c:23:60:9c:13"
[devices.audio]
type = "AudioSetup"
mqtt = { topic = "zigbee2mqtt/living/remote" }
topic = "zigbee2mqtt/living/remote"
mixer = [10, 0, 0, 49]
speakers = [10, 0, 0, 182]

View File

@ -15,6 +15,7 @@ pub struct Config {
#[serde(default)]
pub ntfy: NtfyConfig,
pub presence: MqttDeviceConfig,
pub light_sensor: LightSensorConfig,
#[serde(default)]
pub devices: HashMap<String, Device>
}
@ -55,6 +56,14 @@ impl Default for NtfyConfig {
}
}
#[derive(Debug, Deserialize)]
pub struct LightSensorConfig {
#[serde(flatten)]
pub mqtt: MqttDeviceConfig,
pub min: isize,
pub max: isize,
}
#[derive(Debug, Deserialize)]
pub struct InfoConfig {
pub name: String,
@ -75,16 +84,21 @@ pub struct KettleConfig {
#[serde(tag = "type")]
pub enum Device {
IkeaOutlet {
#[serde(flatten)]
info: InfoConfig,
#[serde(flatten)]
mqtt: MqttDeviceConfig,
kettle: Option<KettleConfig>,
},
WakeOnLAN {
#[serde(flatten)]
info: InfoConfig,
#[serde(flatten)]
mqtt: MqttDeviceConfig,
mac_address: String,
},
AudioSetup {
#[serde(flatten)]
mqtt: MqttDeviceConfig,
mixer: [u8; 4],
speakers: [u8; 4],

View File

@ -12,14 +12,15 @@ use std::collections::HashMap;
use google_home::{GoogleHomeDevice, traits::OnOff};
use tracing::{trace, debug, span, Level};
use crate::{mqtt::OnMqtt, presence::OnPresence};
use crate::{mqtt::OnMqtt, presence::OnPresence, light_sensor::OnDarkness};
impl_cast::impl_cast!(Device, OnMqtt);
impl_cast::impl_cast!(Device, OnPresence);
impl_cast::impl_cast!(Device, OnDarkness);
impl_cast::impl_cast!(Device, GoogleHomeDevice);
impl_cast::impl_cast!(Device, OnOff);
pub trait Device: AsGoogleHomeDevice + AsOnMqtt + AsOnPresence + AsOnOff {
pub trait Device: AsGoogleHomeDevice + AsOnMqtt + AsOnPresence + AsOnDarkness + AsOnOff {
fn get_id(&self) -> String;
}
@ -60,6 +61,7 @@ impl Devices {
get_cast!(OnMqtt);
get_cast!(OnPresence);
get_cast!(OnDarkness);
get_cast!(GoogleHomeDevice);
get_cast!(OnOff);
@ -90,3 +92,13 @@ impl OnPresence for Devices {
})
}
}
impl OnDarkness for Devices {
fn on_darkness(&mut self, dark: bool) {
self.as_on_darknesss().iter_mut().for_each(|(id, device)| {
let _span = span!(Level::TRACE, "on_darkness").entered();
trace!(id, "Handling");
device.on_darkness(dark);
})
}
}

View File

@ -25,10 +25,8 @@ pub struct IkeaOutlet {
impl IkeaOutlet {
pub fn new(identifier: String, info: InfoConfig, mqtt: MqttDeviceConfig, kettle: Option<KettleConfig>, client: AsyncClient) -> Self {
let c = client.clone();
let t = mqtt.topic.clone();
// @TODO Handle potential errors here
c.subscribe(t, rumqttc::QoS::AtLeastOnce).block_on().unwrap();
client.subscribe(mqtt.topic.clone(), rumqttc::QoS::AtLeastOnce).block_on().unwrap();
Self{ identifier, info, mqtt, kettle, client, last_known_state: false, handle: None }
}
@ -115,9 +113,7 @@ impl OnPresence for IkeaOutlet {
// Turn off the outlet when we leave the house
if !presence {
debug!(id = self.identifier, "Turning device off");
let client = self.client.clone();
let topic = self.mqtt.topic.clone();
set_on(client, topic, false).block_on();
set_on(self.client.clone(), self.mqtt.topic.clone(), false).block_on();
}
}
}
@ -159,9 +155,7 @@ impl traits::OnOff for IkeaOutlet {
}
fn set_on(&mut self, on: bool) -> Result<(), ErrorCode> {
let client = self.client.clone();
let topic = self.mqtt.topic.clone();
set_on(client, topic, on).block_on();
set_on(self.client.clone(), self.mqtt.topic.clone(), on).block_on();
Ok(())
}

View File

@ -16,9 +16,8 @@ pub struct WakeOnLAN {
impl WakeOnLAN {
pub fn new(identifier: String, info: InfoConfig, mqtt: MqttDeviceConfig, mac_address: String, client: AsyncClient) -> Self {
let t = mqtt.topic.clone();
// @TODO Handle potential errors here
client.subscribe(t, rumqttc::QoS::AtLeastOnce).block_on().unwrap();
client.subscribe(mqtt.topic.clone(), rumqttc::QoS::AtLeastOnce).block_on().unwrap();
Self { identifier, info, mqtt, mac_address }
}
@ -32,7 +31,6 @@ impl Device for WakeOnLAN {
impl OnMqtt for WakeOnLAN {
fn on_mqtt(&mut self, message: &Publish) {
if message.topic != self.mqtt.topic {
return;
}

View File

@ -4,3 +4,4 @@ pub mod mqtt;
pub mod config;
pub mod presence;
pub mod ntfy;
pub mod light_sensor;

80
src/light_sensor.rs Normal file
View File

@ -0,0 +1,80 @@
use std::sync::{Weak, RwLock};
use pollster::FutureExt as _;
use rumqttc::AsyncClient;
use tracing::{span, Level, log::{warn, trace}, debug};
use crate::{config::{MqttDeviceConfig, LightSensorConfig}, mqtt::{OnMqtt, BrightnessMessage}};
pub trait OnDarkness {
fn on_darkness(&mut self, dark: bool);
}
pub struct LightSensor {
listeners: Vec<Weak<RwLock<dyn OnDarkness + Sync + Send>>>,
is_dark: bool,
mqtt: MqttDeviceConfig,
min: isize,
max: isize,
}
impl LightSensor {
pub fn new(config: LightSensorConfig, client: AsyncClient) -> Self {
client.subscribe(config.mqtt.topic.clone(), rumqttc::QoS::AtLeastOnce).block_on().unwrap();
Self { listeners: Vec::new(), is_dark: false, mqtt: config.mqtt, min: config.min, max: config.max }
}
pub fn add_listener<T: OnDarkness + Sync + Send + 'static>(&mut self, listener: Weak<RwLock<T>>) {
self.listeners.push(listener);
}
pub fn notify(dark: bool, listeners: Vec<Weak<RwLock<dyn OnDarkness + Sync + Send>>>) {
let _span = span!(Level::TRACE, "darkness_update").entered();
listeners.into_iter().for_each(|listener| {
if let Some(listener) = listener.upgrade() {
listener.write().unwrap().on_darkness(dark);
}
})
}
}
impl OnMqtt for LightSensor {
fn on_mqtt(&mut self, message: &rumqttc::Publish) {
if message.topic != self.mqtt.topic {
return;
}
let illuminance = match BrightnessMessage::try_from(message) {
Ok(state) => state.illuminance(),
Err(err) => {
warn!("Failed to parse message: {err}");
return;
}
};
debug!("Illuminance: {illuminance}");
let is_dark = if illuminance <= self.min {
trace!("It is dark");
true
} else if illuminance >= self.max {
trace!("It is light");
false
} else {
trace!("In between min ({}) and max ({}) value, keeping current state: {}", self.min, self.max, self.is_dark);
self.is_dark
};
if is_dark != self.is_dark {
debug!("Dark state has changed: {is_dark}");
self.is_dark = is_dark;
self.listeners.retain(|listener| listener.strong_count() > 0);
let listeners = self.listeners.clone();
tokio::task::spawn_blocking(move || {
LightSensor::notify(is_dark, listeners)
});
}
}
}

View File

@ -3,7 +3,7 @@ use std::{time::Duration, sync::{Arc, RwLock}, process, net::SocketAddr};
use axum::{Router, Json, routing::post, http::StatusCode};
use automation::{config::Config, presence::Presence, ntfy::Ntfy};
use automation::{config::Config, presence::Presence, ntfy::Ntfy, light_sensor::{self, LightSensor}};
use dotenv::dotenv;
use rumqttc::{MqttOptions, Transport, AsyncClient};
use tracing::{error, info, metadata::LevelFilter};
@ -58,6 +58,12 @@ async fn main() {
let presence = Arc::new(RwLock::new(presence));
mqtt.add_listener(Arc::downgrade(&presence));
let mut light_sensor = LightSensor::new(config.light_sensor, client.clone());
light_sensor.add_listener(Arc::downgrade(&devices));
let light_sensor = Arc::new(RwLock::new(light_sensor));
mqtt.add_listener(Arc::downgrade(&light_sensor));
// Start mqtt, this spawns a seperate async task
mqtt.start();

View File

@ -136,3 +136,44 @@ impl TryFrom<&Publish> for RemoteMessage {
.or(Err(anyhow::anyhow!("Invalid message payload received: {:?}", message.payload)))
}
}
#[derive(Debug, Deserialize)]
pub struct PresenceMessage {
state: bool
}
impl PresenceMessage {
pub fn present(&self) -> bool {
self.state
}
}
impl TryFrom<&Publish> for PresenceMessage {
type Error = anyhow::Error;
fn try_from(message: &Publish) -> Result<Self, Self::Error> {
serde_json::from_slice(&message.payload)
.or(Err(anyhow::anyhow!("Invalid message payload received: {:?}", message.payload)))
}
}
#[derive(Debug, Deserialize)]
pub struct BrightnessMessage {
illuminance: isize,
}
impl BrightnessMessage {
pub fn illuminance(&self) -> isize {
self.illuminance
}
}
impl TryFrom<&Publish> for BrightnessMessage {
type Error = anyhow::Error;
fn try_from(message: &Publish) -> Result<Self, Self::Error> {
serde_json::from_slice(&message.payload)
.or(Err(anyhow::anyhow!("Invalid message payload received: {:?}", message.payload)))
}
}

View File

@ -1,11 +1,10 @@
use std::{sync::{Weak, RwLock}, collections::HashMap};
use tracing::{debug, warn, span, Level};
use rumqttc::{AsyncClient, Publish};
use serde::{Serialize, Deserialize};
use rumqttc::AsyncClient;
use pollster::FutureExt as _;
use crate::{mqtt::OnMqtt, config::MqttDeviceConfig};
use crate::{mqtt::{OnMqtt, PresenceMessage}, config::MqttDeviceConfig};
pub trait OnPresence {
fn on_presence(&mut self, presence: bool);
@ -41,20 +40,6 @@ impl Presence {
}
}
#[derive(Debug, Serialize, Deserialize)]
struct StateMessage {
state: bool
}
impl TryFrom<&Publish> for StateMessage {
type Error = anyhow::Error;
fn try_from(message: &Publish) -> Result<Self, Self::Error> {
serde_json::from_slice(&message.payload)
.or(Err(anyhow::anyhow!("Invalid message payload received: {:?}", message.payload)))
}
}
impl OnMqtt for Presence {
fn on_mqtt(&mut self, message: &rumqttc::Publish) {
if message.topic.starts_with(&(self.mqtt.topic.clone() + "/")) {
@ -66,16 +51,16 @@ impl OnMqtt for Presence {
self.devices.remove(device_name);
return;
} else {
let state = match StateMessage::try_from(message) {
Ok(state) => state,
let present = match PresenceMessage::try_from(message) {
Ok(state) => state.present(),
Err(err) => {
warn!("Failed to parse message: {err}");
return;
}
};
debug!("State of device [{device_name}] has changed: {}", state.state);
self.devices.insert(device_name.to_owned(), state.state);
debug!("State of device [{device_name}] has changed: {}", present);
self.devices.insert(device_name.to_owned(), present);
}
let overall_presence = self.devices.iter().any(|(_, v)| *v);