Some cleanup and added light sensor
This commit is contained in:
parent
cfd10a7daf
commit
c9b2127eed
|
@ -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]
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
80
src/light_sensor.rs
Normal 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)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
||||
|
|
41
src/mqtt.rs
41
src/mqtt.rs
|
@ -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)))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue
Block a user