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