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
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
c584fa014c
commit
bb131f2b1a
|
@ -79,10 +79,18 @@ topic = "zigbee2mqtt/living/remote"
|
||||||
mixer = "living_mixer"
|
mixer = "living_mixer"
|
||||||
speakers = "living_speakers"
|
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]
|
[devices.hallway_frontdoor]
|
||||||
type = "ContactSensor"
|
type = "ContactSensor"
|
||||||
topic = "zigbee2mqtt/hallway/frontdoor"
|
topic = "zigbee2mqtt/hallway/frontdoor"
|
||||||
presence = { timeout = 900 }
|
presence = { timeout = 900 }
|
||||||
|
lights = { lights = ["hallway_light"], timeout = 60 }
|
||||||
|
|
||||||
[devices.bathroom_washer]
|
[devices.bathroom_washer]
|
||||||
type = "Washer"
|
type = "Washer"
|
||||||
|
|
|
@ -79,11 +79,18 @@ topic = "zigbee2mqtt/living/remote"
|
||||||
mixer = "living_mixer"
|
mixer = "living_mixer"
|
||||||
speakers = "living_speakers"
|
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]
|
[devices.hallway_frontdoor]
|
||||||
type = "ContactSensor"
|
type = "ContactSensor"
|
||||||
topic = "zigbee2mqtt/hallway/frontdoor"
|
topic = "zigbee2mqtt/hallway/frontdoor"
|
||||||
presence = { timeout = 10 }
|
presence = { timeout = 10 }
|
||||||
lights = { lights = ["bathroom_light"], timeout = 10 }
|
lights = { lights = ["hallway_light"], timeout = 10 }
|
||||||
|
|
||||||
[devices.bathroom_washer]
|
[devices.bathroom_washer]
|
||||||
type = "Washer"
|
type = "Washer"
|
||||||
|
|
|
@ -14,8 +14,8 @@ use crate::{
|
||||||
auth::OpenIDConfig,
|
auth::OpenIDConfig,
|
||||||
device_manager::DeviceManager,
|
device_manager::DeviceManager,
|
||||||
devices::{
|
devices::{
|
||||||
AudioSetup, ContactSensor, DebugBridgeConfig, Device, HueBridgeConfig, IkeaOutlet,
|
AudioSetup, ContactSensor, DebugBridgeConfig, Device, HueBridgeConfig, HueLight,
|
||||||
KasaOutlet, LightSensorConfig, PresenceConfig, WakeOnLAN, Washer,
|
IkeaOutlet, KasaOutlet, LightSensorConfig, PresenceConfig, WakeOnLAN, Washer,
|
||||||
},
|
},
|
||||||
error::{ConfigParseError, CreateDeviceError, MissingEnv},
|
error::{ConfigParseError, CreateDeviceError, MissingEnv},
|
||||||
event::EventChannel,
|
event::EventChannel,
|
||||||
|
@ -131,6 +131,7 @@ pub enum DeviceConfig {
|
||||||
KasaOutlet(<KasaOutlet as CreateDevice>::Config),
|
KasaOutlet(<KasaOutlet as CreateDevice>::Config),
|
||||||
WakeOnLAN(<WakeOnLAN as CreateDevice>::Config),
|
WakeOnLAN(<WakeOnLAN as CreateDevice>::Config),
|
||||||
Washer(<Washer as CreateDevice>::Config),
|
Washer(<Washer as CreateDevice>::Config),
|
||||||
|
HueLight(<HueLight as CreateDevice>::Config),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
|
@ -202,7 +203,8 @@ impl DeviceConfig {
|
||||||
IkeaOutlet,
|
IkeaOutlet,
|
||||||
KasaOutlet,
|
KasaOutlet,
|
||||||
WakeOnLAN,
|
WakeOnLAN,
|
||||||
Washer
|
Washer,
|
||||||
|
HueLight
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -201,8 +201,9 @@ impl OnMqtt for ContactSensor {
|
||||||
if lights.timeout.is_zero() && let Some(light) = As::<dyn OnOff>::cast_mut(light.as_mut()) {
|
if lights.timeout.is_zero() && let Some(light) = As::<dyn OnOff>::cast_mut(light.as_mut()) {
|
||||||
light.set_on(false).await.ok();
|
light.set_on(false).await.ok();
|
||||||
} else if let Some(light) = As::<dyn Timeout>::cast_mut(light.as_mut()) {
|
} 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,19 @@
|
||||||
use std::net::{Ipv4Addr, SocketAddr};
|
use std::{
|
||||||
|
net::{Ipv4Addr, SocketAddr},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use google_home::{errors::ErrorCode, traits::OnOff};
|
||||||
|
use rumqttc::AsyncClient;
|
||||||
use serde::{Deserialize, Serialize};
|
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)]
|
#[derive(Debug)]
|
||||||
pub enum Flag {
|
pub enum Flag {
|
||||||
|
@ -68,9 +77,7 @@ impl HueBridge {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl HueBridge {
|
|
||||||
pub fn new(config: HueBridgeConfig) -> Self {
|
pub fn new(config: HueBridgeConfig) -> Self {
|
||||||
Self {
|
Self {
|
||||||
addr: (config.ip, 80).into(),
|
addr: (config.ip, 80).into(),
|
||||||
|
@ -101,3 +108,163 @@ impl OnDarkness for HueBridge {
|
||||||
self.set_flag(Flag::Darkness, dark).await;
|
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}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -135,16 +135,14 @@ impl OnMqtt for IkeaOutlet {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Abort any timer that is currently running
|
// Abort any timer that is currently running
|
||||||
if let Some(handle) = self.handle.take() {
|
self.stop_timeout().await;
|
||||||
handle.abort();
|
|
||||||
}
|
|
||||||
|
|
||||||
debug!(id = self.identifier, "Updating state to {state}");
|
debug!(id = self.identifier, "Updating state to {state}");
|
||||||
self.last_known_state = state;
|
self.last_known_state = state;
|
||||||
|
|
||||||
// If this is a kettle start a timeout for turning it of again
|
// If this is a kettle start a timeout for turning it of again
|
||||||
if state && let Some(timeout) = self.timeout {
|
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 {
|
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
|
// Abort any timer that is currently running
|
||||||
if let Some(handle) = self.handle.take() {
|
self.stop_timeout().await;
|
||||||
handle.abort();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Turn the kettle of after the specified timeout
|
// Turn the kettle of after the specified timeout
|
||||||
// TODO: Impl Drop for IkeaOutlet that will abort the handle if the IkeaOutlet
|
// 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;
|
set_on(client, &topic, false).await;
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn stop_timeout(&mut self) {
|
||||||
|
if let Some(handle) = self.handle.take() {
|
||||||
|
handle.abort();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ mod washer;
|
||||||
pub use self::audio_setup::AudioSetup;
|
pub use self::audio_setup::AudioSetup;
|
||||||
pub use self::contact_sensor::ContactSensor;
|
pub use self::contact_sensor::ContactSensor;
|
||||||
pub use self::debug_bridge::{DebugBridge, DebugBridgeConfig};
|
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::ikea_outlet::IkeaOutlet;
|
||||||
pub use self::kasa_outlet::KasaOutlet;
|
pub use self::kasa_outlet::KasaOutlet;
|
||||||
pub use self::light_sensor::{LightSensor, LightSensorConfig};
|
pub use self::light_sensor::{LightSensor, LightSensorConfig};
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
use impl_cast::device_trait;
|
use impl_cast::device_trait;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
#[device_trait]
|
#[device_trait]
|
||||||
pub trait Timeout {
|
pub trait Timeout {
|
||||||
fn start_timeout(&mut self, _timeout: Duration) {}
|
async fn start_timeout(&mut self, _timeout: Duration);
|
||||||
|
async fn stop_timeout(&mut self);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user