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"
[device.hallway_light]
type = "HueLight"
[device.hallway_lights]
type = "HueGroup"
ip = "10.0.0.146"
login = "${HUE_TOKEN}"
light_id = 16
group_id = 81
scene_id = "3qWKxGVadXFFG4o"
timer_id = 1
[device.hallway_frontdoor]
type = "ContactSensor"
topic = "zigbee2mqtt/hallway/frontdoor"
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"
[device.hallway_light]
type = "HueLight"
[device.hallway_lights]
type = "HueGroup"
ip = "10.0.0.146"
login = "${HUE_TOKEN}"
light_id = 16
group_id = 81
scene_id = "3qWKxGVadXFFG4o"
timer_id = 1
[device.hallway_frontdoor]
type = "ContactSensor"
topic = "zigbee2mqtt/hallway/frontdoor"
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::{
auth::OpenIDConfig,
device_manager::DeviceConfigs,
devices::{DebugBridgeConfig, PresenceConfig},
devices::PresenceConfig,
error::{ConfigParseError, MissingEnv},
};
@ -26,7 +26,6 @@ pub struct Config {
pub fullfillment: FullfillmentConfig,
pub ntfy: Option<NtfyConfig>,
pub presence: PresenceConfig,
pub debug_bridge: Option<DebugBridgeConfig>,
#[serde(rename = "device")]
pub devices: IndexMap<String, DeviceConfigs>,
}

View File

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

View File

@ -167,7 +167,13 @@ impl OnMqtt for ContactSensor {
let mut light = light.write().await;
if let Some(light) = As::<dyn OnOff>::cast_mut(light.as_mut()) {
*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 {

View File

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

View File

@ -15,7 +15,7 @@ pub use self::audio_setup::AudioSetupConfig;
pub use self::contact_sensor::ContactSensorConfig;
pub use self::debug_bridge::DebugBridgeConfig;
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::kasa_outlet::KasaOutletConfig;
pub use self::light_sensor::{LightSensor, LightSensorConfig};