Moved and improved hallways logic with lua
All checks were successful
Build and deploy / Build application (push) Successful in 4m7s
Build and deploy / Build container (push) Successful in 1m18s
Build and deploy / Deploy container (push) Successful in 21s

This commit is contained in:
Dreaded_X 2024-12-06 01:27:35 +01:00
parent 9d4b52b511
commit e9f080ef19
Signed by: Dreaded_X
GPG Key ID: FA5F485356B0D2D4
7 changed files with 85 additions and 176 deletions

View File

@ -112,10 +112,10 @@ automation.device_manager:add(IkeaRemote.new({
local function off_timeout(duration) local function off_timeout(duration)
local timeout = Timeout.new() local timeout = Timeout.new()
return function(this, on) return function(self, on)
if on then if on then
timeout:start(duration, function() timeout:start(duration, function()
this:set_on(false) self:set_on(false)
end) end)
else else
timeout:cancel() timeout:cancel()
@ -188,26 +188,6 @@ automation.device_manager:add(IkeaOutlet.new({
client = mqtt_client, 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({ local hallway_top_light = HueGroup.new({
identifier = "hallway_top_light", identifier = "hallway_top_light",
ip = hue_ip, ip = hue_ip,
@ -235,6 +215,64 @@ automation.device_manager:add(HueSwitch.new({
end, 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({ automation.device_manager:add(ContactSensor.new({
identifier = "hallway_frontdoor", identifier = "hallway_frontdoor",
topic = mqtt_z2m("hallway/frontdoor"), topic = mqtt_z2m("hallway/frontdoor"),
@ -243,19 +281,17 @@ automation.device_manager:add(ContactSensor.new({
topic = mqtt_automation("presence/contact/frontdoor"), topic = mqtt_automation("presence/contact/frontdoor"),
timeout = debug and 10 or 15 * 60, timeout = debug and 10 or 15 * 60,
}, },
trigger = { callback = function(open)
devices = { hallway_bottom_lights }, hallway_light_automation:door_callback(open)
timeout = debug and 10 or 2 * 60, end,
},
})) }))
automation.device_manager:add(ContactSensor.new({ automation.device_manager:add(ContactSensor.new({
identifier = "hallway_trash", identifier = "hallway_trash",
topic = mqtt_z2m("hallway/trash"), topic = mqtt_z2m("hallway/trash"),
client = mqtt_client, client = mqtt_client,
trigger = { callback = function(open)
devices = { hallway_bottom_lights }, hallway_light_automation:trash_callback(open)
}, end,
})) }))
automation.device_manager:add(IkeaOutlet.new({ automation.device_manager:add(IkeaOutlet.new({

View File

@ -3,19 +3,18 @@ use std::time::Duration;
use async_trait::async_trait; use async_trait::async_trait;
use automation_macro::LuaDeviceConfig; use automation_macro::LuaDeviceConfig;
use google_home::traits::OnOff;
use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard}; use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
use tracing::{debug, error, trace, warn}; use tracing::{debug, error, trace, warn};
use super::{Device, LuaDeviceCreate}; use super::{Device, LuaDeviceCreate};
use crate::action_callback::ActionCallback;
use crate::config::MqttDeviceConfig; use crate::config::MqttDeviceConfig;
use crate::devices::DEFAULT_PRESENCE; use crate::devices::DEFAULT_PRESENCE;
use crate::error::DeviceConfigError; use crate::error::DeviceConfigError;
use crate::event::{OnMqtt, OnPresence}; use crate::event::{OnMqtt, OnPresence};
use crate::messages::{ContactMessage, PresenceMessage}; use crate::messages::{ContactMessage, PresenceMessage};
use crate::mqtt::WrappedAsyncClient; use crate::mqtt::WrappedAsyncClient;
use crate::traits::Timeout;
// NOTE: If we add more presence devices we might need to move this out of here // NOTE: If we add more presence devices we might need to move this out of here
#[derive(Debug, Clone, LuaDeviceConfig)] #[derive(Debug, Clone, LuaDeviceConfig)]
@ -26,14 +25,6 @@ pub struct PresenceDeviceConfig {
pub timeout: Duration, 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)] #[derive(Debug, Clone, LuaDeviceConfig)]
pub struct Config { pub struct Config {
pub identifier: String, pub identifier: String,
@ -41,8 +32,8 @@ pub struct Config {
pub mqtt: MqttDeviceConfig, pub mqtt: MqttDeviceConfig,
#[device_config(from_lua, default)] #[device_config(from_lua, default)]
pub presence: Option<PresenceDeviceConfig>, pub presence: Option<PresenceDeviceConfig>,
#[device_config(from_lua)] #[device_config(from_lua, default)]
pub trigger: Option<TriggerConfig>, pub callback: ActionCallback<bool>,
#[device_config(from_lua)] #[device_config(from_lua)]
pub client: WrappedAsyncClient, pub client: WrappedAsyncClient,
} }
@ -51,7 +42,6 @@ pub struct Config {
struct State { struct State {
overall_presence: bool, overall_presence: bool,
is_closed: bool, is_closed: bool,
previous: Vec<bool>,
handle: Option<JoinHandle<()>>, handle: Option<JoinHandle<()>>,
} }
@ -79,26 +69,6 @@ impl LuaDeviceCreate for ContactSensor {
async fn create(config: Self::Config) -> Result<Self, Self::Error> { async fn create(config: Self::Config) -> Result<Self, Self::Error> {
trace!(id = config.identifier, "Setting up ContactSensor"); 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 config
.client .client
.subscribe(&config.mqtt.topic, rumqttc::QoS::AtLeastOnce) .subscribe(&config.mqtt.topic, rumqttc::QoS::AtLeastOnce)
@ -107,7 +77,6 @@ impl LuaDeviceCreate for ContactSensor {
let state = State { let state = State {
overall_presence: DEFAULT_PRESENCE, overall_presence: DEFAULT_PRESENCE,
is_closed: true, is_closed: true,
previous,
handle: None, handle: None,
}; };
let state = Arc::new(RwLock::new(state)); let state = Arc::new(RwLock::new(state));
@ -148,44 +117,11 @@ impl OnMqtt for ContactSensor {
return; return;
} }
self.config.callback.call(!is_closed).await;
debug!(id = self.get_id(), "Updating state to {is_closed}"); debug!(id = self.get_id(), "Updating state to {is_closed}");
self.state_mut().await.is_closed = 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 // Check if this contact sensor works as a presence device
// If not we are done here // If not we are done here
let presence = match &self.config.presence { let presence = match &self.config.presence {

View File

@ -1,7 +1,6 @@
use std::net::SocketAddr; use std::net::SocketAddr;
use std::time::Duration;
use anyhow::{anyhow, Context, Result}; use anyhow::Result;
use async_trait::async_trait; use async_trait::async_trait;
use automation_macro::LuaDeviceConfig; use automation_macro::LuaDeviceConfig;
use google_home::errors::ErrorCode; use google_home::errors::ErrorCode;
@ -10,7 +9,6 @@ use tracing::{error, trace, warn};
use super::{Device, LuaDeviceCreate}; use super::{Device, LuaDeviceCreate};
use crate::mqtt::WrappedAsyncClient; use crate::mqtt::WrappedAsyncClient;
use crate::traits::Timeout;
#[derive(Debug, Clone, LuaDeviceConfig)] #[derive(Debug, Clone, LuaDeviceConfig)]
pub struct Config { pub struct Config {
@ -19,8 +17,6 @@ pub struct Config {
pub addr: SocketAddr, pub addr: SocketAddr,
pub login: String, pub login: String,
pub group_id: isize, pub group_id: isize,
#[device_config(default)]
pub timer_id: Option<isize>,
pub scene_id: String, pub scene_id: String,
#[device_config(from_lua)] #[device_config(from_lua)]
pub client: WrappedAsyncClient, pub client: WrappedAsyncClient,
@ -49,11 +45,6 @@ impl HueGroup {
format!("http://{}/api/{}", self.config.addr, self.config.login) 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 { fn url_set_action(&self) -> String {
format!("{}/groups/{}/action", self.url_base(), self.config.group_id) format!("{}/groups/{}/action", self.url_base(), self.config.group_id)
} }
@ -72,9 +63,6 @@ impl Device for HueGroup {
#[async_trait] #[async_trait]
impl OnOff for HueGroup { impl OnOff for HueGroup {
async fn set_on(&self, on: bool) -> Result<(), ErrorCode> { 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 { let message = if on {
message::Action::scene(self.config.scene_id.clone()) message::Action::scene(self.config.scene_id.clone())
} else { } 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 { mod message {
use std::time::Duration; use std::time::Duration;

View File

@ -37,7 +37,6 @@ pub use self::presence::{Presence, DEFAULT_PRESENCE};
pub use self::wake_on_lan::WakeOnLAN; pub use self::wake_on_lan::WakeOnLAN;
pub use self::washer::Washer; pub use self::washer::Washer;
use crate::event::{OnDarkness, OnMqtt, OnNotification, OnPresence}; use crate::event::{OnDarkness, OnMqtt, OnNotification, OnPresence};
use crate::traits::Timeout;
#[async_trait] #[async_trait]
pub trait LuaDeviceCreate { pub trait LuaDeviceCreate {
@ -145,7 +144,6 @@ pub trait Device:
+ Cast<dyn OnDarkness> + Cast<dyn OnDarkness>
+ Cast<dyn OnNotification> + Cast<dyn OnNotification>
+ Cast<dyn OnOff> + Cast<dyn OnOff>
+ Cast<dyn Timeout>
{ {
fn get_id(&self) -> String; fn get_id(&self) -> String;
} }

View File

@ -59,5 +59,18 @@ impl mlua::UserData for Timeout {
Ok(()) 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)
});
} }
} }

View File

@ -13,4 +13,3 @@ pub mod helpers;
pub mod messages; pub mod messages;
pub mod mqtt; pub mod mqtt;
pub mod schedule; pub mod schedule;
pub mod traits;

View File

@ -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<()>;
}