Moved and improved hallways logic with lua
This commit is contained in:
parent
9d4b52b511
commit
e9f080ef19
96
config.lua
96
config.lua
|
@ -112,10 +112,10 @@ automation.device_manager:add(IkeaRemote.new({
|
|||
local function off_timeout(duration)
|
||||
local timeout = Timeout.new()
|
||||
|
||||
return function(this, on)
|
||||
return function(self, on)
|
||||
if on then
|
||||
timeout:start(duration, function()
|
||||
this:set_on(false)
|
||||
self:set_on(false)
|
||||
end)
|
||||
else
|
||||
timeout:cancel()
|
||||
|
@ -188,26 +188,6 @@ automation.device_manager:add(IkeaOutlet.new({
|
|||
client = mqtt_client,
|
||||
}))
|
||||
|
||||
local hallway_bottom_lights = HueGroup.new({
|
||||
identifier = "hallway_bottom_lights",
|
||||
ip = hue_ip,
|
||||
login = hue_token,
|
||||
group_id = 81,
|
||||
scene_id = "3qWKxGVadXFFG4o",
|
||||
timer_id = 1,
|
||||
client = mqtt_client,
|
||||
})
|
||||
automation.device_manager:add(hallway_bottom_lights)
|
||||
automation.device_manager:add(IkeaRemote.new({
|
||||
name = "Remote",
|
||||
room = "Hallway",
|
||||
client = mqtt_client,
|
||||
topic = mqtt_z2m("hallway/remote"),
|
||||
callback = function(on)
|
||||
hallway_bottom_lights:set_on(on)
|
||||
end,
|
||||
}))
|
||||
|
||||
local hallway_top_light = HueGroup.new({
|
||||
identifier = "hallway_top_light",
|
||||
ip = hue_ip,
|
||||
|
@ -235,6 +215,64 @@ automation.device_manager:add(HueSwitch.new({
|
|||
end,
|
||||
}))
|
||||
|
||||
local hallway_bottom_lights = HueGroup.new({
|
||||
identifier = "hallway_bottom_lights",
|
||||
ip = hue_ip,
|
||||
login = hue_token,
|
||||
group_id = 81,
|
||||
scene_id = "3qWKxGVadXFFG4o",
|
||||
client = mqtt_client,
|
||||
})
|
||||
automation.device_manager:add(hallway_bottom_lights)
|
||||
|
||||
local hallway_light_automation = {
|
||||
group = hallway_bottom_lights,
|
||||
timeout = Timeout.new(),
|
||||
state = {
|
||||
door_open = false,
|
||||
trash_open = false,
|
||||
forced = false,
|
||||
},
|
||||
switch_callback = function(self, on)
|
||||
self.timeout:cancel()
|
||||
self.group:set_on(on)
|
||||
self.state.forced = on
|
||||
end,
|
||||
door_callback = function(self, open)
|
||||
self.state.door_open = open
|
||||
if open then
|
||||
self.timeout:cancel()
|
||||
|
||||
self.group:set_on(true)
|
||||
elseif not self.state.forced then
|
||||
self.timeout:start(debug and 10 or 60, function()
|
||||
if not self.state.trash_open then
|
||||
self.group:set_on(false)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end,
|
||||
trash_callback = function(self, open)
|
||||
self.state.trash_open = open
|
||||
if open then
|
||||
self.group:set_on(true)
|
||||
else
|
||||
if not self.timeout:is_waiting() and not self.state.door_open and not self.state.forced then
|
||||
self.group:set_on(false)
|
||||
end
|
||||
end
|
||||
end,
|
||||
}
|
||||
|
||||
automation.device_manager:add(IkeaRemote.new({
|
||||
name = "Remote",
|
||||
room = "Hallway",
|
||||
client = mqtt_client,
|
||||
topic = mqtt_z2m("hallway/remote"),
|
||||
callback = function(on)
|
||||
hallway_light_automation:switch_callback(on)
|
||||
end,
|
||||
}))
|
||||
automation.device_manager:add(ContactSensor.new({
|
||||
identifier = "hallway_frontdoor",
|
||||
topic = mqtt_z2m("hallway/frontdoor"),
|
||||
|
@ -243,19 +281,17 @@ automation.device_manager:add(ContactSensor.new({
|
|||
topic = mqtt_automation("presence/contact/frontdoor"),
|
||||
timeout = debug and 10 or 15 * 60,
|
||||
},
|
||||
trigger = {
|
||||
devices = { hallway_bottom_lights },
|
||||
timeout = debug and 10 or 2 * 60,
|
||||
},
|
||||
callback = function(open)
|
||||
hallway_light_automation:door_callback(open)
|
||||
end,
|
||||
}))
|
||||
|
||||
automation.device_manager:add(ContactSensor.new({
|
||||
identifier = "hallway_trash",
|
||||
topic = mqtt_z2m("hallway/trash"),
|
||||
client = mqtt_client,
|
||||
trigger = {
|
||||
devices = { hallway_bottom_lights },
|
||||
},
|
||||
callback = function(open)
|
||||
hallway_light_automation:trash_callback(open)
|
||||
end,
|
||||
}))
|
||||
|
||||
automation.device_manager:add(IkeaOutlet.new({
|
||||
|
|
|
@ -3,19 +3,18 @@ use std::time::Duration;
|
|||
|
||||
use async_trait::async_trait;
|
||||
use automation_macro::LuaDeviceConfig;
|
||||
use google_home::traits::OnOff;
|
||||
use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||
use tokio::task::JoinHandle;
|
||||
use tracing::{debug, error, trace, warn};
|
||||
|
||||
use super::{Device, LuaDeviceCreate};
|
||||
use crate::action_callback::ActionCallback;
|
||||
use crate::config::MqttDeviceConfig;
|
||||
use crate::devices::DEFAULT_PRESENCE;
|
||||
use crate::error::DeviceConfigError;
|
||||
use crate::event::{OnMqtt, OnPresence};
|
||||
use crate::messages::{ContactMessage, PresenceMessage};
|
||||
use crate::mqtt::WrappedAsyncClient;
|
||||
use crate::traits::Timeout;
|
||||
|
||||
// NOTE: If we add more presence devices we might need to move this out of here
|
||||
#[derive(Debug, Clone, LuaDeviceConfig)]
|
||||
|
@ -26,14 +25,6 @@ pub struct PresenceDeviceConfig {
|
|||
pub timeout: Duration,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, LuaDeviceConfig)]
|
||||
pub struct TriggerConfig {
|
||||
#[device_config(from_lua)]
|
||||
pub devices: Vec<Box<dyn Device>>,
|
||||
#[device_config(default, with(|t: Option<_>| t.map(Duration::from_secs)))]
|
||||
pub timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, LuaDeviceConfig)]
|
||||
pub struct Config {
|
||||
pub identifier: String,
|
||||
|
@ -41,8 +32,8 @@ pub struct Config {
|
|||
pub mqtt: MqttDeviceConfig,
|
||||
#[device_config(from_lua, default)]
|
||||
pub presence: Option<PresenceDeviceConfig>,
|
||||
#[device_config(from_lua)]
|
||||
pub trigger: Option<TriggerConfig>,
|
||||
#[device_config(from_lua, default)]
|
||||
pub callback: ActionCallback<bool>,
|
||||
#[device_config(from_lua)]
|
||||
pub client: WrappedAsyncClient,
|
||||
}
|
||||
|
@ -51,7 +42,6 @@ pub struct Config {
|
|||
struct State {
|
||||
overall_presence: bool,
|
||||
is_closed: bool,
|
||||
previous: Vec<bool>,
|
||||
handle: Option<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
|
@ -79,26 +69,6 @@ impl LuaDeviceCreate for ContactSensor {
|
|||
async fn create(config: Self::Config) -> Result<Self, Self::Error> {
|
||||
trace!(id = config.identifier, "Setting up ContactSensor");
|
||||
|
||||
let mut previous = Vec::new();
|
||||
// Make sure the devices implement the required traits
|
||||
if let Some(trigger) = &config.trigger {
|
||||
for device in &trigger.devices {
|
||||
{
|
||||
let id = device.get_id().to_owned();
|
||||
if (device.cast() as Option<&dyn OnOff>).is_none() {
|
||||
return Err(DeviceConfigError::MissingTrait(id, "OnOff".into()));
|
||||
}
|
||||
|
||||
if trigger.timeout.is_none()
|
||||
&& (device.cast() as Option<&dyn Timeout>).is_none()
|
||||
{
|
||||
return Err(DeviceConfigError::MissingTrait(id, "Timeout".into()));
|
||||
}
|
||||
}
|
||||
}
|
||||
previous.resize(trigger.devices.len(), false);
|
||||
}
|
||||
|
||||
config
|
||||
.client
|
||||
.subscribe(&config.mqtt.topic, rumqttc::QoS::AtLeastOnce)
|
||||
|
@ -107,7 +77,6 @@ impl LuaDeviceCreate for ContactSensor {
|
|||
let state = State {
|
||||
overall_presence: DEFAULT_PRESENCE,
|
||||
is_closed: true,
|
||||
previous,
|
||||
handle: None,
|
||||
};
|
||||
let state = Arc::new(RwLock::new(state));
|
||||
|
@ -148,44 +117,11 @@ impl OnMqtt for ContactSensor {
|
|||
return;
|
||||
}
|
||||
|
||||
self.config.callback.call(!is_closed).await;
|
||||
|
||||
debug!(id = self.get_id(), "Updating state to {is_closed}");
|
||||
self.state_mut().await.is_closed = is_closed;
|
||||
|
||||
if let Some(trigger) = &self.config.trigger {
|
||||
if !is_closed {
|
||||
for (light, previous) in trigger
|
||||
.devices
|
||||
.iter()
|
||||
.zip(self.state_mut().await.previous.iter_mut())
|
||||
{
|
||||
if let Some(light) = light.cast() as Option<&dyn OnOff> {
|
||||
*previous = light.on().await.unwrap();
|
||||
light.set_on(true).await.ok();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (light, previous) in trigger
|
||||
.devices
|
||||
.iter()
|
||||
.zip(self.state_mut().await.previous.iter())
|
||||
{
|
||||
if !previous {
|
||||
// If the timeout is zero just turn the light off directly
|
||||
if trigger.timeout.is_none()
|
||||
&& let Some(light) = light.cast() as Option<&dyn OnOff>
|
||||
{
|
||||
light.set_on(false).await.ok();
|
||||
} else if let Some(timeout) = trigger.timeout
|
||||
&& let Some(light) = light.cast() as Option<&dyn Timeout>
|
||||
{
|
||||
light.start_timeout(timeout).await.unwrap();
|
||||
}
|
||||
// TODO: Put a warning/error on creation if either of this has to option to fail
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this contact sensor works as a presence device
|
||||
// If not we are done here
|
||||
let presence = match &self.config.presence {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
use std::net::SocketAddr;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use automation_macro::LuaDeviceConfig;
|
||||
use google_home::errors::ErrorCode;
|
||||
|
@ -10,7 +9,6 @@ use tracing::{error, trace, warn};
|
|||
|
||||
use super::{Device, LuaDeviceCreate};
|
||||
use crate::mqtt::WrappedAsyncClient;
|
||||
use crate::traits::Timeout;
|
||||
|
||||
#[derive(Debug, Clone, LuaDeviceConfig)]
|
||||
pub struct Config {
|
||||
|
@ -19,8 +17,6 @@ pub struct Config {
|
|||
pub addr: SocketAddr,
|
||||
pub login: String,
|
||||
pub group_id: isize,
|
||||
#[device_config(default)]
|
||||
pub timer_id: Option<isize>,
|
||||
pub scene_id: String,
|
||||
#[device_config(from_lua)]
|
||||
pub client: WrappedAsyncClient,
|
||||
|
@ -49,11 +45,6 @@ impl HueGroup {
|
|||
format!("http://{}/api/{}", self.config.addr, self.config.login)
|
||||
}
|
||||
|
||||
fn url_set_schedule(&self) -> Option<String> {
|
||||
let timer_id = self.config.timer_id?;
|
||||
Some(format!("{}/schedules/{}", self.url_base(), timer_id))
|
||||
}
|
||||
|
||||
fn url_set_action(&self) -> String {
|
||||
format!("{}/groups/{}/action", self.url_base(), self.config.group_id)
|
||||
}
|
||||
|
@ -72,9 +63,6 @@ impl Device for HueGroup {
|
|||
#[async_trait]
|
||||
impl OnOff for HueGroup {
|
||||
async fn set_on(&self, on: bool) -> Result<(), ErrorCode> {
|
||||
// Abort any timer that is currently running
|
||||
self.stop_timeout().await.unwrap();
|
||||
|
||||
let message = if on {
|
||||
message::Action::scene(self.config.scene_id.clone())
|
||||
} else {
|
||||
|
@ -131,57 +119,6 @@ impl OnOff for HueGroup {
|
|||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Timeout for HueGroup {
|
||||
async fn start_timeout(&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 Some(url) = self.url_set_schedule() else {
|
||||
return Ok(());
|
||||
};
|
||||
let res = reqwest::Client::new()
|
||||
.put(url)
|
||||
.json(&message)
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to start timeout")?;
|
||||
|
||||
let status = res.status();
|
||||
if !status.is_success() {
|
||||
return Err(anyhow!(
|
||||
"Hue bridge returned unsuccessful status '{status}'"
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn stop_timeout(&self) -> Result<()> {
|
||||
let message = message::Timeout::new(None);
|
||||
let Some(url) = self.url_set_schedule() else {
|
||||
return Ok(());
|
||||
};
|
||||
let res = reqwest::Client::new()
|
||||
.put(url)
|
||||
.json(&message)
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to stop timeout")?;
|
||||
|
||||
let status = res.status();
|
||||
if !status.is_success() {
|
||||
return Err(anyhow!(
|
||||
"Hue bridge returned unsuccessful status '{status}'"
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
mod message {
|
||||
use std::time::Duration;
|
||||
|
||||
|
|
|
@ -37,7 +37,6 @@ pub use self::presence::{Presence, DEFAULT_PRESENCE};
|
|||
pub use self::wake_on_lan::WakeOnLAN;
|
||||
pub use self::washer::Washer;
|
||||
use crate::event::{OnDarkness, OnMqtt, OnNotification, OnPresence};
|
||||
use crate::traits::Timeout;
|
||||
|
||||
#[async_trait]
|
||||
pub trait LuaDeviceCreate {
|
||||
|
@ -145,7 +144,6 @@ pub trait Device:
|
|||
+ Cast<dyn OnDarkness>
|
||||
+ Cast<dyn OnNotification>
|
||||
+ Cast<dyn OnOff>
|
||||
+ Cast<dyn Timeout>
|
||||
{
|
||||
fn get_id(&self) -> String;
|
||||
}
|
||||
|
|
|
@ -59,5 +59,18 @@ impl mlua::UserData for Timeout {
|
|||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
methods.add_async_method("is_waiting", |_lua, this, ()| async move {
|
||||
debug!("Canceling timeout callback");
|
||||
|
||||
if let Some(handle) = this.state.read().await.handle.as_ref() {
|
||||
debug!("Join handle: {}", handle.is_finished());
|
||||
return Ok(!handle.is_finished());
|
||||
}
|
||||
|
||||
debug!("Join handle: None");
|
||||
|
||||
Ok(false)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,4 +13,3 @@ pub mod helpers;
|
|||
pub mod messages;
|
||||
pub mod mqtt;
|
||||
pub mod schedule;
|
||||
pub mod traits;
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
|
||||
#[async_trait]
|
||||
pub trait Timeout: Sync + Send {
|
||||
async fn start_timeout(&self, _timeout: Duration) -> Result<()>;
|
||||
async fn stop_timeout(&self) -> Result<()>;
|
||||
}
|
Loading…
Reference in New Issue
Block a user