HueLight is now HueGroup and uses a scene to turn the light on, the contact sensor will also not override the current light state if it is already on
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Dreaded_X 2023-08-18 04:08:15 +02:00
parent 044c38ba86
commit 9628b8a94b
Signed by: Dreaded_X
GPG Key ID: FA5F485356B0D2D4
7 changed files with 77 additions and 38 deletions

View File

@ -93,15 +93,16 @@ room = "Workbench"
topic = "zigbee2mqtt/workbench/outlet" topic = "zigbee2mqtt/workbench/outlet"
[device.hallway_light] [device.hallway_lights]
type = "HueLight" type = "HueGroup"
ip = "10.0.0.146" ip = "10.0.0.146"
login = "${HUE_TOKEN}" login = "${HUE_TOKEN}"
light_id = 16 group_id = 81
scene_id = "3qWKxGVadXFFG4o"
timer_id = 1 timer_id = 1
[device.hallway_frontdoor] [device.hallway_frontdoor]
type = "ContactSensor" type = "ContactSensor"
topic = "zigbee2mqtt/hallway/frontdoor" topic = "zigbee2mqtt/hallway/frontdoor"
presence = { topic = "automation/presence/contact/frontdoor", timeout = 900 } presence = { topic = "automation/presence/contact/frontdoor", timeout = 900 }
trigger = { devices = ["hallway_light"], timeout = 60 } trigger = { devices = ["hallway_lights"], timeout = 60 }

View File

@ -95,15 +95,16 @@ room = "Workbench"
topic = "zigbee2mqtt/workbench/outlet" topic = "zigbee2mqtt/workbench/outlet"
[device.hallway_light] [device.hallway_lights]
type = "HueLight" type = "HueGroup"
ip = "10.0.0.146" ip = "10.0.0.146"
login = "${HUE_TOKEN}" login = "${HUE_TOKEN}"
light_id = 16 group_id = 81
scene_id = "3qWKxGVadXFFG4o"
timer_id = 1 timer_id = 1
[device.hallway_frontdoor] [device.hallway_frontdoor]
type = "ContactSensor" type = "ContactSensor"
topic = "zigbee2mqtt/hallway/frontdoor" topic = "zigbee2mqtt/hallway/frontdoor"
presence = { topic = "automation_dev/presence/contact/frontdoor", timeout = 10 } presence = { topic = "automation_dev/presence/contact/frontdoor", timeout = 10 }
trigger = { devices = ["hallway_light"], timeout = 10 } trigger = { devices = ["hallway_lights"], timeout = 10 }

View File

@ -13,7 +13,7 @@ use tracing::debug;
use crate::{ use crate::{
auth::OpenIDConfig, auth::OpenIDConfig,
device_manager::DeviceConfigs, device_manager::DeviceConfigs,
devices::{DebugBridgeConfig, PresenceConfig}, devices::PresenceConfig,
error::{ConfigParseError, MissingEnv}, error::{ConfigParseError, MissingEnv},
}; };
@ -26,7 +26,6 @@ pub struct Config {
pub fullfillment: FullfillmentConfig, pub fullfillment: FullfillmentConfig,
pub ntfy: Option<NtfyConfig>, pub ntfy: Option<NtfyConfig>,
pub presence: PresenceConfig, pub presence: PresenceConfig,
pub debug_bridge: Option<DebugBridgeConfig>,
#[serde(rename = "device")] #[serde(rename = "device")]
pub devices: IndexMap<String, DeviceConfigs>, pub devices: IndexMap<String, DeviceConfigs>,
} }

View File

@ -12,7 +12,7 @@ use tracing::{debug, error, instrument, trace};
use crate::{ use crate::{
devices::{ devices::{
As, AudioSetupConfig, ContactSensorConfig, DebugBridgeConfig, Device, HueBridgeConfig, As, AudioSetupConfig, ContactSensorConfig, DebugBridgeConfig, Device, HueBridgeConfig,
HueLightConfig, IkeaOutletConfig, KasaOutletConfig, LightSensorConfig, WakeOnLANConfig, HueGroupConfig, IkeaOutletConfig, KasaOutletConfig, LightSensorConfig, WakeOnLANConfig,
WasherConfig, WasherConfig,
}, },
error::DeviceConfigError, error::DeviceConfigError,
@ -50,7 +50,7 @@ pub enum DeviceConfigs {
WakeOnLAN(WakeOnLANConfig), WakeOnLAN(WakeOnLANConfig),
Washer(WasherConfig), Washer(WasherConfig),
HueBridge(HueBridgeConfig), HueBridge(HueBridgeConfig),
HueLight(HueLightConfig), HueGroup(HueGroupConfig),
LightSensor(LightSensorConfig), LightSensor(LightSensorConfig),
} }

View File

@ -167,7 +167,13 @@ impl OnMqtt for ContactSensor {
let mut light = light.write().await; let mut light = light.write().await;
if let Some(light) = As::<dyn OnOff>::cast_mut(light.as_mut()) { if let Some(light) = As::<dyn OnOff>::cast_mut(light.as_mut()) {
*previous = light.is_on().await.unwrap(); *previous = light.is_on().await.unwrap();
light.set_on(true).await.ok(); // Only turn the light on when it is currently off
// This is done such that if the light is on but dimmed for example it
// won't suddenly blast at full brightness but instead retain the current
// state
if !*previous {
light.set_on(true).await.ok();
}
} }
} }
} else { } else {

View File

@ -18,25 +18,27 @@ use crate::{
use super::Device; use super::Device;
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub struct HueLightConfig { pub struct HueGroupConfig {
pub ip: Ipv4Addr, pub ip: Ipv4Addr,
pub login: String, pub login: String,
pub light_id: isize, pub group_id: isize,
pub timer_id: isize, pub timer_id: isize,
pub scene_id: String,
} }
#[async_trait] #[async_trait]
impl DeviceConfig for HueLightConfig { impl DeviceConfig for HueGroupConfig {
async fn create( async fn create(
self, self,
identifier: &str, identifier: &str,
_ext: &ConfigExternal, _ext: &ConfigExternal,
) -> Result<Box<dyn Device>, DeviceConfigError> { ) -> Result<Box<dyn Device>, DeviceConfigError> {
let device = HueLight { let device = HueGroup {
identifier: identifier.into(), identifier: identifier.into(),
addr: (self.ip, 80).into(), addr: (self.ip, 80).into(),
login: self.login, login: self.login,
light_id: self.light_id, group_id: self.group_id,
scene_id: self.scene_id,
timer_id: self.timer_id, timer_id: self.timer_id,
}; };
@ -45,16 +47,17 @@ impl DeviceConfig for HueLightConfig {
} }
#[derive(Debug)] #[derive(Debug)]
struct HueLight { struct HueGroup {
pub identifier: String, pub identifier: String,
pub addr: SocketAddr, pub addr: SocketAddr,
pub login: String, pub login: String,
pub light_id: isize, pub group_id: isize,
pub timer_id: isize, pub timer_id: isize,
pub scene_id: String,
} }
// Couple of helper function to get the correct urls // Couple of helper function to get the correct urls
impl HueLight { impl HueGroup {
fn url_base(&self) -> String { fn url_base(&self) -> String {
format!("http://{}/api/{}", self.addr, self.login) format!("http://{}/api/{}", self.addr, self.login)
} }
@ -63,30 +66,35 @@ impl HueLight {
format!("{}/schedules/{}", self.url_base(), self.timer_id) format!("{}/schedules/{}", self.url_base(), self.timer_id)
} }
fn url_set_state(&self) -> String { fn url_set_action(&self) -> String {
format!("{}/lights/{}/state", self.url_base(), self.light_id) format!("{}/groups/{}/action", self.url_base(), self.group_id)
} }
fn url_get_state(&self) -> String { fn url_get_state(&self) -> String {
format!("{}/lights/{}", self.url_base(), self.light_id) format!("{}/groups/{}", self.url_base(), self.group_id)
} }
} }
impl Device for HueLight { impl Device for HueGroup {
fn get_id(&self) -> &str { fn get_id(&self) -> &str {
&self.identifier &self.identifier
} }
} }
#[async_trait] #[async_trait]
impl OnOff for HueLight { impl OnOff for HueGroup {
async fn set_on(&mut self, on: bool) -> Result<(), ErrorCode> { async fn set_on(&mut self, on: bool) -> Result<(), ErrorCode> {
// Abort any timer that is currently running // Abort any timer that is currently running
self.stop_timeout().await.unwrap(); self.stop_timeout().await.unwrap();
let message = message::State::new(on); let message = if on {
message::Action::scene(self.scene_id.clone())
} else {
message::Action::on(true)
};
let res = reqwest::Client::new() let res = reqwest::Client::new()
.put(self.url_set_state()) .put(self.url_set_action())
.json(&message) .json(&message)
.send() .send()
.await; .await;
@ -118,7 +126,7 @@ impl OnOff for HueLight {
} }
let on = match res.json::<message::Info>().await { let on = match res.json::<message::Info>().await {
Ok(info) => info.is_on(), Ok(info) => info.any_on(),
Err(err) => { Err(err) => {
error!(id = self.identifier, "Failed to parse message: {err}"); error!(id = self.identifier, "Failed to parse message: {err}");
// TODO: Error code // TODO: Error code
@ -136,11 +144,12 @@ impl OnOff for HueLight {
} }
#[async_trait] #[async_trait]
impl Timeout for HueLight { impl Timeout for HueGroup {
async fn start_timeout(&mut self, timeout: Duration) -> Result<()> { async fn start_timeout(&mut self, timeout: Duration) -> Result<()> {
// Abort any timer that is currently running // Abort any timer that is currently running
self.stop_timeout().await?; self.stop_timeout().await?;
// NOTE: This uses an existing timer, as we are unable to cancel it on the hub otherwise
let message = message::Timeout::new(Some(timeout)); let message = message::Timeout::new(Some(timeout));
let res = reqwest::Client::new() let res = reqwest::Client::new()
.put(self.url_set_schedule()) .put(self.url_set_schedule())
@ -185,14 +194,33 @@ mod message {
use serde::{ser::SerializeStruct, Deserialize, Serialize}; use serde::{ser::SerializeStruct, Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct State { pub struct Action {
on: bool, #[serde(skip_serializing_if = "Option::is_none")]
on: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
scene: Option<String>,
} }
impl State { impl Action {
pub fn new(on: bool) -> Self { pub fn on(on: bool) -> Self {
Self { on } Self {
on: Some(on),
scene: None,
}
} }
pub fn scene(scene: String) -> Self {
Self {
on: None,
scene: Some(scene),
}
}
}
#[derive(Debug, Serialize, Deserialize)]
struct State {
all_on: bool,
any_on: bool,
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
@ -201,9 +229,13 @@ mod message {
} }
impl Info { impl Info {
pub fn is_on(&self) -> bool { pub fn any_on(&self) -> bool {
self.state.on self.state.any_on
} }
// pub fn all_on(&self) -> bool {
// self.state.all_on
// }
} }
#[derive(Debug)] #[derive(Debug)]

View File

@ -15,7 +15,7 @@ pub use self::audio_setup::AudioSetupConfig;
pub use self::contact_sensor::ContactSensorConfig; pub use self::contact_sensor::ContactSensorConfig;
pub use self::debug_bridge::DebugBridgeConfig; pub use self::debug_bridge::DebugBridgeConfig;
pub use self::hue_bridge::HueBridgeConfig; pub use self::hue_bridge::HueBridgeConfig;
pub use self::hue_light::HueLightConfig; pub use self::hue_light::HueGroupConfig;
pub use self::ikea_outlet::IkeaOutletConfig; pub use self::ikea_outlet::IkeaOutletConfig;
pub use self::kasa_outlet::KasaOutletConfig; pub use self::kasa_outlet::KasaOutletConfig;
pub use self::light_sensor::{LightSensor, LightSensorConfig}; pub use self::light_sensor::{LightSensor, LightSensorConfig};