Added basic hue light bridge, improved Timeout trait and setup frontdoor to turn on hallway ligh temporarily
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Dreaded_X 2023-08-15 04:45:35 +02:00
parent c584fa014c
commit bb131f2b1a
Signed by: Dreaded_X
GPG Key ID: FA5F485356B0D2D4
8 changed files with 211 additions and 20 deletions

View File

@ -79,10 +79,18 @@ topic = "zigbee2mqtt/living/remote"
mixer = "living_mixer"
speakers = "living_speakers"
[devices.hallway_light]
type = "HueLight"
ip = "10.0.0.146"
login = "${HUE_TOKEN}"
light_id = 16
timer_id = 1
[devices.hallway_frontdoor]
type = "ContactSensor"
topic = "zigbee2mqtt/hallway/frontdoor"
presence = { timeout = 900 }
lights = { lights = ["hallway_light"], timeout = 60 }
[devices.bathroom_washer]
type = "Washer"

View File

@ -79,11 +79,18 @@ topic = "zigbee2mqtt/living/remote"
mixer = "living_mixer"
speakers = "living_speakers"
[devices.hallway_light]
type = "HueLight"
ip = "10.0.0.146"
login = "${HUE_TOKEN}"
light_id = 16
timer_id = 1
[devices.hallway_frontdoor]
type = "ContactSensor"
topic = "zigbee2mqtt/hallway/frontdoor"
presence = { timeout = 10 }
lights = { lights = ["bathroom_light"], timeout = 10 }
lights = { lights = ["hallway_light"], timeout = 10 }
[devices.bathroom_washer]
type = "Washer"

View File

@ -14,8 +14,8 @@ use crate::{
auth::OpenIDConfig,
device_manager::DeviceManager,
devices::{
AudioSetup, ContactSensor, DebugBridgeConfig, Device, HueBridgeConfig, IkeaOutlet,
KasaOutlet, LightSensorConfig, PresenceConfig, WakeOnLAN, Washer,
AudioSetup, ContactSensor, DebugBridgeConfig, Device, HueBridgeConfig, HueLight,
IkeaOutlet, KasaOutlet, LightSensorConfig, PresenceConfig, WakeOnLAN, Washer,
},
error::{ConfigParseError, CreateDeviceError, MissingEnv},
event::EventChannel,
@ -131,6 +131,7 @@ pub enum DeviceConfig {
KasaOutlet(<KasaOutlet as CreateDevice>::Config),
WakeOnLAN(<WakeOnLAN as CreateDevice>::Config),
Washer(<Washer as CreateDevice>::Config),
HueLight(<HueLight as CreateDevice>::Config),
}
impl Config {
@ -202,7 +203,8 @@ impl DeviceConfig {
IkeaOutlet,
KasaOutlet,
WakeOnLAN,
Washer
Washer,
HueLight
]
})
}

View File

@ -201,8 +201,9 @@ impl OnMqtt for ContactSensor {
if lights.timeout.is_zero() && let Some(light) = As::<dyn OnOff>::cast_mut(light.as_mut()) {
light.set_on(false).await.ok();
} else if let Some(light) = As::<dyn Timeout>::cast_mut(light.as_mut()) {
light.start_timeout(lights.timeout);
light.start_timeout(lights.timeout).await;
}
// TODO: Put a warning/error on creation if either of this has to option to fail
}
}
}

View File

@ -1,10 +1,19 @@
use std::net::{Ipv4Addr, SocketAddr};
use std::{
net::{Ipv4Addr, SocketAddr},
time::Duration,
};
use async_trait::async_trait;
use google_home::{errors::ErrorCode, traits::OnOff};
use rumqttc::AsyncClient;
use serde::{Deserialize, Serialize};
use tracing::{error, trace, warn};
use serde_json::Value;
use tracing::{debug, error, trace, warn};
use crate::{devices::Device, event::OnDarkness, event::OnPresence};
use crate::{
config::CreateDevice, device_manager::DeviceManager, devices::Device, error::CreateDeviceError,
event::EventChannel, event::OnDarkness, event::OnPresence, traits::Timeout,
};
#[derive(Debug)]
pub enum Flag {
@ -68,9 +77,7 @@ impl HueBridge {
}
}
}
}
impl HueBridge {
pub fn new(config: HueBridgeConfig) -> Self {
Self {
addr: (config.ip, 80).into(),
@ -101,3 +108,163 @@ impl OnDarkness for HueBridge {
self.set_flag(Flag::Darkness, dark).await;
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct HueLightConfig {
pub ip: Ipv4Addr,
pub login: String,
pub light_id: isize,
pub timer_id: isize,
}
#[derive(Debug)]
pub struct HueLight {
pub identifier: String,
pub addr: SocketAddr,
pub login: String,
pub light_id: isize,
pub timer_id: isize,
}
#[async_trait]
impl CreateDevice for HueLight {
type Config = HueLightConfig;
async fn create(
identifier: &str,
config: Self::Config,
_event_channel: &EventChannel,
_client: &AsyncClient,
_presence_topic: &str,
_devices: &DeviceManager,
) -> Result<Self, CreateDeviceError> {
Ok(Self {
identifier: identifier.to_owned(),
addr: (config.ip, 80).into(),
login: config.login,
light_id: config.light_id,
timer_id: config.timer_id,
})
}
}
impl Device for HueLight {
fn get_id(&self) -> &str {
&self.identifier
}
}
#[async_trait]
impl OnOff for HueLight {
async fn set_on(&mut self, on: bool) -> Result<(), ErrorCode> {
// Abort any timer that is currently running
self.stop_timeout().await;
let url = format!(
"http://{}/api/{}/lights/{}/state",
self.addr, self.login, self.light_id
);
let res = reqwest::Client::new()
.put(url)
.body(format!(r#"{{"on": {}}}"#, on))
.send()
.await;
match res {
Ok(res) => {
let status = res.status();
if !status.is_success() {
warn!(self.identifier, "Status code is not success: {status}");
}
}
Err(err) => error!(self.identifier, "Error: {err}"),
}
Ok(())
}
async fn is_on(&self) -> Result<bool, ErrorCode> {
let url = format!(
"http://{}/api/{}/lights/{}",
self.addr, self.login, self.light_id
);
let res = reqwest::Client::new().get(url).send().await;
match res {
Ok(res) => {
let status = res.status();
if !status.is_success() {
warn!(self.identifier, "Status code is not success: {status}");
}
let v: Value = serde_json::from_slice(res.bytes().await.unwrap().as_ref()).unwrap();
// TODO: This is not very nice
return Ok(v["state"]["on"].as_bool().unwrap());
}
Err(err) => error!(self.identifier, "Error: {err}"),
}
Ok(false)
}
}
#[async_trait]
impl Timeout for HueLight {
async fn start_timeout(&mut self, timeout: Duration) {
// Abort any timer that is currently running
self.stop_timeout().await;
let url = format!(
"http://{}/api/{}/schedules/{}",
self.addr, self.login, self.timer_id
);
let seconds = timeout.as_secs() % 60;
let minutes = (timeout.as_secs() / 60) % 60;
let hours = timeout.as_secs() / 3600;
let time = format!("PT{hours:<02}:{minutes:<02}:{seconds:<02}");
debug!(self.identifier, "Starting timeout ({time})...");
let res = reqwest::Client::new()
.put(url)
.body(format!(r#"{{"status": "enabled", "localtime": "{time}"}}"#))
.send()
.await;
match res {
Ok(res) => {
let status = res.status();
if !status.is_success() {
warn!(self.identifier, "Status code is not success: {status}");
}
}
Err(err) => error!(self.identifier, "Error: {err}"),
}
}
async fn stop_timeout(&mut self) {
let url = format!(
"http://{}/api/{}/schedules/{}",
self.addr, self.login, self.timer_id
);
let res = reqwest::Client::new()
.put(url)
.body(format!(r#"{{"status": "disabled"}}"#))
.send()
.await;
match res {
Ok(res) => {
let status = res.status();
if !status.is_success() {
warn!(self.identifier, "Status code is not success: {status}");
}
}
Err(err) => error!(self.identifier, "Error: {err}"),
}
}
}

View File

@ -135,16 +135,14 @@ impl OnMqtt for IkeaOutlet {
}
// Abort any timer that is currently running
if let Some(handle) = self.handle.take() {
handle.abort();
}
self.stop_timeout().await;
debug!(id = self.identifier, "Updating state to {state}");
self.last_known_state = state;
// If this is a kettle start a timeout for turning it of again
if state && let Some(timeout) = self.timeout {
self.start_timeout(timeout);
self.start_timeout(timeout).await;
}
}
}
@ -205,12 +203,11 @@ impl traits::OnOff for IkeaOutlet {
}
}
#[async_trait]
impl crate::traits::Timeout for IkeaOutlet {
fn start_timeout(&mut self, timeout: Duration) {
async fn start_timeout(&mut self, timeout: Duration) {
// Abort any timer that is currently running
if let Some(handle) = self.handle.take() {
handle.abort();
}
self.stop_timeout().await;
// Turn the kettle of after the specified timeout
// TODO: Impl Drop for IkeaOutlet that will abort the handle if the IkeaOutlet
@ -228,4 +225,10 @@ impl crate::traits::Timeout for IkeaOutlet {
set_on(client, &topic, false).await;
}));
}
async fn stop_timeout(&mut self) {
if let Some(handle) = self.handle.take() {
handle.abort();
}
}
}

View File

@ -13,7 +13,7 @@ mod washer;
pub use self::audio_setup::AudioSetup;
pub use self::contact_sensor::ContactSensor;
pub use self::debug_bridge::{DebugBridge, DebugBridgeConfig};
pub use self::hue_bridge::{HueBridge, HueBridgeConfig};
pub use self::hue_bridge::{HueBridge, HueBridgeConfig, HueLight};
pub use self::ikea_outlet::IkeaOutlet;
pub use self::kasa_outlet::KasaOutlet;
pub use self::light_sensor::{LightSensor, LightSensorConfig};

View File

@ -1,8 +1,11 @@
use std::time::Duration;
use async_trait::async_trait;
use impl_cast::device_trait;
#[async_trait]
#[device_trait]
pub trait Timeout {
fn start_timeout(&mut self, _timeout: Duration) {}
async fn start_timeout(&mut self, _timeout: Duration);
async fn stop_timeout(&mut self);
}