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
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
044c38ba86
commit
9628b8a94b
|
@ -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 }
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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>,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)]
|
||||||
|
|
|
@ -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};
|
||||||
|
|
Loading…
Reference in New Issue
Block a user