Some cleanup and added light sensor
This commit is contained in:
parent
cfd10a7daf
commit
c9b2127eed
|
@ -9,25 +9,33 @@ username="Dreaded_X"
|
||||||
[presence]
|
[presence]
|
||||||
topic = "automation_dev/presence"
|
topic = "automation_dev/presence"
|
||||||
|
|
||||||
|
[light_sensor]
|
||||||
|
topic = "zigbee2mqtt/living/light"
|
||||||
|
min = 23_000
|
||||||
|
max = 25_000
|
||||||
|
|
||||||
[devices.kitchen_kettle]
|
[devices.kitchen_kettle]
|
||||||
type = "IkeaOutlet"
|
type = "IkeaOutlet"
|
||||||
info = { name = "Kettle", room = "Kitchen" }
|
name = "Kettle"
|
||||||
mqtt = { topic = "zigbee2mqtt/kitchen/kettle" }
|
room = "Kitchen"
|
||||||
|
topic = "zigbee2mqtt/kitchen/kettle"
|
||||||
kettle = { timeout = 5 }
|
kettle = { timeout = 5 }
|
||||||
|
|
||||||
[devices.living_workbench]
|
[devices.living_workbench]
|
||||||
type = "IkeaOutlet"
|
type = "IkeaOutlet"
|
||||||
info = { name = "Workbench", room = "Living Room" }
|
name = "Workbench"
|
||||||
mqtt = { topic = "zigbee2mqtt/living/workbench" }
|
room = "Living Room"
|
||||||
|
topic = "zigbee2mqtt/living/workbench"
|
||||||
|
|
||||||
[devices.living_zeus]
|
[devices.living_zeus]
|
||||||
type = "WakeOnLAN"
|
type = "WakeOnLAN"
|
||||||
info = { name = "Zeus", room = "Living Room" }
|
name = "Zeus"
|
||||||
mqtt = { topic = "automation/appliance/living_room/zeus" }
|
room = "Living Room"
|
||||||
|
topic = "automation/appliance/living_room/zeus"
|
||||||
mac_address = "30:9c:23:60:9c:13"
|
mac_address = "30:9c:23:60:9c:13"
|
||||||
|
|
||||||
[devices.audio]
|
[devices.audio]
|
||||||
type = "AudioSetup"
|
type = "AudioSetup"
|
||||||
mqtt = { topic = "zigbee2mqtt/living/remote" }
|
topic = "zigbee2mqtt/living/remote"
|
||||||
mixer = [10, 0, 0, 49]
|
mixer = [10, 0, 0, 49]
|
||||||
speakers = [10, 0, 0, 182]
|
speakers = [10, 0, 0, 182]
|
||||||
|
|
|
@ -15,6 +15,7 @@ pub struct Config {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub ntfy: NtfyConfig,
|
pub ntfy: NtfyConfig,
|
||||||
pub presence: MqttDeviceConfig,
|
pub presence: MqttDeviceConfig,
|
||||||
|
pub light_sensor: LightSensorConfig,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub devices: HashMap<String, Device>
|
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)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct InfoConfig {
|
pub struct InfoConfig {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
@ -75,16 +84,21 @@ pub struct KettleConfig {
|
||||||
#[serde(tag = "type")]
|
#[serde(tag = "type")]
|
||||||
pub enum Device {
|
pub enum Device {
|
||||||
IkeaOutlet {
|
IkeaOutlet {
|
||||||
|
#[serde(flatten)]
|
||||||
info: InfoConfig,
|
info: InfoConfig,
|
||||||
|
#[serde(flatten)]
|
||||||
mqtt: MqttDeviceConfig,
|
mqtt: MqttDeviceConfig,
|
||||||
kettle: Option<KettleConfig>,
|
kettle: Option<KettleConfig>,
|
||||||
},
|
},
|
||||||
WakeOnLAN {
|
WakeOnLAN {
|
||||||
|
#[serde(flatten)]
|
||||||
info: InfoConfig,
|
info: InfoConfig,
|
||||||
|
#[serde(flatten)]
|
||||||
mqtt: MqttDeviceConfig,
|
mqtt: MqttDeviceConfig,
|
||||||
mac_address: String,
|
mac_address: String,
|
||||||
},
|
},
|
||||||
AudioSetup {
|
AudioSetup {
|
||||||
|
#[serde(flatten)]
|
||||||
mqtt: MqttDeviceConfig,
|
mqtt: MqttDeviceConfig,
|
||||||
mixer: [u8; 4],
|
mixer: [u8; 4],
|
||||||
speakers: [u8; 4],
|
speakers: [u8; 4],
|
||||||
|
|
|
@ -12,14 +12,15 @@ use std::collections::HashMap;
|
||||||
use google_home::{GoogleHomeDevice, traits::OnOff};
|
use google_home::{GoogleHomeDevice, traits::OnOff};
|
||||||
use tracing::{trace, debug, span, Level};
|
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, OnMqtt);
|
||||||
impl_cast::impl_cast!(Device, OnPresence);
|
impl_cast::impl_cast!(Device, OnPresence);
|
||||||
|
impl_cast::impl_cast!(Device, OnDarkness);
|
||||||
impl_cast::impl_cast!(Device, GoogleHomeDevice);
|
impl_cast::impl_cast!(Device, GoogleHomeDevice);
|
||||||
impl_cast::impl_cast!(Device, OnOff);
|
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;
|
fn get_id(&self) -> String;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,6 +61,7 @@ impl Devices {
|
||||||
|
|
||||||
get_cast!(OnMqtt);
|
get_cast!(OnMqtt);
|
||||||
get_cast!(OnPresence);
|
get_cast!(OnPresence);
|
||||||
|
get_cast!(OnDarkness);
|
||||||
get_cast!(GoogleHomeDevice);
|
get_cast!(GoogleHomeDevice);
|
||||||
get_cast!(OnOff);
|
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 {
|
impl IkeaOutlet {
|
||||||
pub fn new(identifier: String, info: InfoConfig, mqtt: MqttDeviceConfig, kettle: Option<KettleConfig>, client: AsyncClient) -> Self {
|
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
|
// @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 }
|
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
|
// Turn off the outlet when we leave the house
|
||||||
if !presence {
|
if !presence {
|
||||||
debug!(id = self.identifier, "Turning device off");
|
debug!(id = self.identifier, "Turning device off");
|
||||||
let client = self.client.clone();
|
set_on(self.client.clone(), self.mqtt.topic.clone(), false).block_on();
|
||||||
let topic = self.mqtt.topic.clone();
|
|
||||||
set_on(client, topic, false).block_on();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -159,9 +155,7 @@ impl traits::OnOff for IkeaOutlet {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_on(&mut self, on: bool) -> Result<(), ErrorCode> {
|
fn set_on(&mut self, on: bool) -> Result<(), ErrorCode> {
|
||||||
let client = self.client.clone();
|
set_on(self.client.clone(), self.mqtt.topic.clone(), on).block_on();
|
||||||
let topic = self.mqtt.topic.clone();
|
|
||||||
set_on(client, topic, on).block_on();
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,9 +16,8 @@ pub struct WakeOnLAN {
|
||||||
|
|
||||||
impl WakeOnLAN {
|
impl WakeOnLAN {
|
||||||
pub fn new(identifier: String, info: InfoConfig, mqtt: MqttDeviceConfig, mac_address: String, client: AsyncClient) -> Self {
|
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
|
// @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 }
|
Self { identifier, info, mqtt, mac_address }
|
||||||
}
|
}
|
||||||
|
@ -32,7 +31,6 @@ impl Device for WakeOnLAN {
|
||||||
|
|
||||||
impl OnMqtt for WakeOnLAN {
|
impl OnMqtt for WakeOnLAN {
|
||||||
fn on_mqtt(&mut self, message: &Publish) {
|
fn on_mqtt(&mut self, message: &Publish) {
|
||||||
|
|
||||||
if message.topic != self.mqtt.topic {
|
if message.topic != self.mqtt.topic {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,3 +4,4 @@ pub mod mqtt;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod presence;
|
pub mod presence;
|
||||||
pub mod ntfy;
|
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 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 dotenv::dotenv;
|
||||||
use rumqttc::{MqttOptions, Transport, AsyncClient};
|
use rumqttc::{MqttOptions, Transport, AsyncClient};
|
||||||
use tracing::{error, info, metadata::LevelFilter};
|
use tracing::{error, info, metadata::LevelFilter};
|
||||||
|
@ -58,6 +58,12 @@ async fn main() {
|
||||||
let presence = Arc::new(RwLock::new(presence));
|
let presence = Arc::new(RwLock::new(presence));
|
||||||
mqtt.add_listener(Arc::downgrade(&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
|
// Start mqtt, this spawns a seperate async task
|
||||||
mqtt.start();
|
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)))
|
.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 std::{sync::{Weak, RwLock}, collections::HashMap};
|
||||||
|
|
||||||
use tracing::{debug, warn, span, Level};
|
use tracing::{debug, warn, span, Level};
|
||||||
use rumqttc::{AsyncClient, Publish};
|
use rumqttc::AsyncClient;
|
||||||
use serde::{Serialize, Deserialize};
|
|
||||||
use pollster::FutureExt as _;
|
use pollster::FutureExt as _;
|
||||||
|
|
||||||
use crate::{mqtt::OnMqtt, config::MqttDeviceConfig};
|
use crate::{mqtt::{OnMqtt, PresenceMessage}, config::MqttDeviceConfig};
|
||||||
|
|
||||||
pub trait OnPresence {
|
pub trait OnPresence {
|
||||||
fn on_presence(&mut self, presence: bool);
|
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 {
|
impl OnMqtt for Presence {
|
||||||
fn on_mqtt(&mut self, message: &rumqttc::Publish) {
|
fn on_mqtt(&mut self, message: &rumqttc::Publish) {
|
||||||
if message.topic.starts_with(&(self.mqtt.topic.clone() + "/")) {
|
if message.topic.starts_with(&(self.mqtt.topic.clone() + "/")) {
|
||||||
|
@ -66,16 +51,16 @@ impl OnMqtt for Presence {
|
||||||
self.devices.remove(device_name);
|
self.devices.remove(device_name);
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
let state = match StateMessage::try_from(message) {
|
let present = match PresenceMessage::try_from(message) {
|
||||||
Ok(state) => state,
|
Ok(state) => state.present(),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
warn!("Failed to parse message: {err}");
|
warn!("Failed to parse message: {err}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
debug!("State of device [{device_name}] has changed: {}", state.state);
|
debug!("State of device [{device_name}] has changed: {}", present);
|
||||||
self.devices.insert(device_name.to_owned(), state.state);
|
self.devices.insert(device_name.to_owned(), present);
|
||||||
}
|
}
|
||||||
|
|
||||||
let overall_presence = self.devices.iter().any(|(_, v)| *v);
|
let overall_presence = self.devices.iter().any(|(_, v)| *v);
|
||||||
|
|
Loading…
Reference in New Issue
Block a user