Compare commits

...

2 Commits

Author SHA1 Message Date
f577e9bfa5
Added window sensors, updated room names, and improved hallway automation
All checks were successful
Build and deploy / Build application (push) Successful in 3m10s
Build and deploy / Build container (push) Successful in 59s
Build and deploy / Deploy container (push) Successful in 33s
2024-12-11 22:20:54 +01:00
90a94934fb
Added open close trait and google home support for contact sensor 2024-12-11 22:19:31 +01:00
5 changed files with 160 additions and 26 deletions

View File

@ -3,7 +3,7 @@ use std::time::Duration;
use async_trait::async_trait; use async_trait::async_trait;
use automation_lib::action_callback::ActionCallback; use automation_lib::action_callback::ActionCallback;
use automation_lib::config::MqttDeviceConfig; use automation_lib::config::{InfoConfig, MqttDeviceConfig};
use automation_lib::device::{Device, LuaDeviceCreate}; use automation_lib::device::{Device, LuaDeviceCreate};
use automation_lib::error::DeviceConfigError; use automation_lib::error::DeviceConfigError;
use automation_lib::event::{OnMqtt, OnPresence}; use automation_lib::event::{OnMqtt, OnPresence};
@ -11,10 +11,22 @@ use automation_lib::messages::{ContactMessage, PresenceMessage};
use automation_lib::mqtt::WrappedAsyncClient; use automation_lib::mqtt::WrappedAsyncClient;
use automation_lib::presence::DEFAULT_PRESENCE; use automation_lib::presence::DEFAULT_PRESENCE;
use automation_macro::LuaDeviceConfig; use automation_macro::LuaDeviceConfig;
use google_home::device;
use google_home::errors::{DeviceError, ErrorCode};
use google_home::traits::OpenClose;
use google_home::types::Type;
use serde::Deserialize;
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};
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Copy)]
pub enum SensorType {
Door,
Drawer,
Window,
}
// 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)]
pub struct PresenceDeviceConfig { pub struct PresenceDeviceConfig {
@ -26,11 +38,16 @@ pub struct PresenceDeviceConfig {
#[derive(Debug, Clone, LuaDeviceConfig)] #[derive(Debug, Clone, LuaDeviceConfig)]
pub struct Config { pub struct Config {
pub identifier: String, #[device_config(flatten)]
pub info: InfoConfig,
#[device_config(flatten)] #[device_config(flatten)]
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(default(SensorType::Window))]
pub sensor_type: SensorType,
#[device_config(from_lua, default)] #[device_config(from_lua, default)]
pub callback: ActionCallback<ContactSensor, bool>, pub callback: ActionCallback<ContactSensor, bool>,
#[device_config(from_lua)] #[device_config(from_lua)]
@ -66,7 +83,7 @@ impl LuaDeviceCreate for ContactSensor {
type Error = DeviceConfigError; type Error = DeviceConfigError;
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.info.identifier(), "Setting up ContactSensor");
config config
.client .client
@ -86,7 +103,60 @@ impl LuaDeviceCreate for ContactSensor {
impl Device for ContactSensor { impl Device for ContactSensor {
fn get_id(&self) -> String { fn get_id(&self) -> String {
self.config.identifier.clone() self.config.info.identifier()
}
}
impl google_home::Device for ContactSensor {
fn get_device_type(&self) -> google_home::types::Type {
match self.config.sensor_type {
SensorType::Door => Type::Door,
SensorType::Drawer => Type::Drawer,
SensorType::Window => Type::Window,
}
}
fn get_id(&self) -> String {
Device::get_id(self)
}
fn get_device_name(&self) -> google_home::device::Name {
device::Name::new(&self.config.info.name)
}
fn get_room_hint(&self) -> Option<&str> {
self.config.info.room.as_deref()
}
fn will_report_state(&self) -> bool {
false
}
fn is_online(&self) -> bool {
true
}
}
#[async_trait]
impl OpenClose for ContactSensor {
fn discrete_only_open_close(&self) -> Option<bool> {
Some(true)
}
fn query_only_open_close(&self) -> Option<bool> {
Some(true)
}
async fn open_percent(&self) -> Result<u8, ErrorCode> {
if self.state().await.is_closed {
Ok(0)
} else {
Ok(100)
}
}
async fn set_open_percent(&self, _open_percent: u8) -> Result<(), ErrorCode> {
Err(DeviceError::ActionNotAvailable.into())
} }
} }

View File

@ -96,6 +96,28 @@ macro_rules! impl_device {
.unwrap()) .unwrap())
}); });
} }
if impls::impls!($device: google_home::traits::OpenClose) {
// TODO: Make discrete_only_open_close and query_only_open_close static, that way we can
// add only the supported functions and drop _percet if discrete is true
methods.add_async_method("set_open_percent", |_lua, this, open_percent: u8| async move {
(this.deref().cast() as Option<&dyn google_home::traits::OpenClose>)
.expect("Cast should be valid")
.set_open_percent(open_percent)
.await
.unwrap();
Ok(())
});
methods.add_async_method("open_percent", |_lua, this, _: ()| async move {
Ok((this.deref().cast() as Option<&dyn google_home::traits::OpenClose>)
.expect("Cast should be valid")
.open_percent()
.await
.unwrap())
});
}
} }
} }
}; };

View File

@ -86,7 +86,7 @@ automation.device_manager:add(living_speakers)
automation.device_manager:add(IkeaRemote.new({ automation.device_manager:add(IkeaRemote.new({
name = "Remote", name = "Remote",
room = "Living", room = "Living Room",
client = mqtt_client, client = mqtt_client,
topic = mqtt_z2m("living/remote"), topic = mqtt_z2m("living/remote"),
single_button = true, single_button = true,
@ -216,48 +216,42 @@ automation.device_manager:add(HueSwitch.new({
local hallway_light_automation = { local hallway_light_automation = {
timeout = Timeout.new(), timeout = Timeout.new(),
state = {
door_open = false,
trash_open = false,
forced = false, forced = false,
},
switch_callback = function(self, on) switch_callback = function(self, on)
self.timeout:cancel() self.timeout:cancel()
self.group.set_on(on) self.group.set_on(on)
self.state.forced = on self.forced = on
end, end,
door_callback = function(self, open) door_callback = function(self, open)
self.state.door_open = open
if open then if open then
self.timeout:cancel() self.timeout:cancel()
self.group.set_on(true) self.group.set_on(true)
elseif not self.state.forced then elseif not self.forced then
self.timeout:start(debug and 10 or 2 * 60, function() self.timeout:start(debug and 10 or 2 * 60, function()
if not self.state.trash_open then if self.trash:is_open_percent() == 0 then
self.group.set_on(false) self.group.set_on(false)
end end
end) end)
end end
end, end,
trash_callback = function(self, open) trash_callback = function(self, open)
self.state.trash_open = open
if open then if open then
self.group.set_on(true) self.group.set_on(true)
else else
if not self.timeout:is_waiting() and not self.state.door_open and not self.state.forced then if not self.timeout:is_waiting() and self.door:is_open_percent() == 0 and not self.forced then
self.group.set_on(false) self.group.set_on(false)
end end
end end
end, end,
light_callback = function(self, on) light_callback = function(self, on)
if on and not self.state.trash_open and not self.state.door_open then if on and self.trash:is_open_percent() == 0 and self.door:is_open_percent() == 0 then
-- If the door and trash are not open, that means the light got turned on manually -- If the door and trash are not open, that means the light got turned on manually
self.timeout:cancel() self.timeout:cancel()
self.state.forced = true self.orced = true
elseif not on then elseif not on then
-- The light is never forced when it is off -- The light is never forced when it is off
self.state.forced = false self.forced = false
end end
end, end,
} }
@ -303,8 +297,10 @@ automation.device_manager:add(IkeaRemote.new({
hallway_light_automation:switch_callback(on) hallway_light_automation:switch_callback(on)
end, end,
})) }))
automation.device_manager:add(ContactSensor.new({ local hallway_frontdoor = ContactSensor.new({
identifier = "hallway_frontdoor", name = "Frontdoor",
room = "Hallway",
sensor_type = "Door",
topic = mqtt_z2m("hallway/frontdoor"), topic = mqtt_z2m("hallway/frontdoor"),
client = mqtt_client, client = mqtt_client,
presence = { presence = {
@ -314,19 +310,26 @@ automation.device_manager:add(ContactSensor.new({
callback = function(_, open) callback = function(_, open)
hallway_light_automation:door_callback(open) hallway_light_automation:door_callback(open)
end, end,
})) })
automation.device_manager:add(ContactSensor.new({ automation.device_manager:add(hallway_frontdoor)
identifier = "hallway_trash", hallway_lights_automation.door = hallway_frontdoor
local hallway_trash = ContactSensor.new({
name = "Trash",
room = "Hallway",
sensor_type = "Drawer",
topic = mqtt_z2m("hallway/trash"), topic = mqtt_z2m("hallway/trash"),
client = mqtt_client, client = mqtt_client,
callback = function(_, open) callback = function(_, open)
hallway_light_automation:trash_callback(open) hallway_light_automation:trash_callback(open)
end, end,
})) })
automation.device_manager:add(hallway_trash)
hallway_lights_automation.trash = hallway_frontdoor
automation.device_manager:add(LightOnOff.new({ automation.device_manager:add(LightOnOff.new({
name = "Light", name = "Light",
room = "Guest", room = "Guest Room",
topic = mqtt_z2m("guest/light"), topic = mqtt_z2m("guest/light"),
client = mqtt_client, client = mqtt_client,
})) }))
@ -339,6 +342,32 @@ local bedroom_air_filter = AirFilter.new({
}) })
automation.device_manager:add(bedroom_air_filter) automation.device_manager:add(bedroom_air_filter)
automation.device_manager:add(ContactSensor.new({
name = "Balcony",
room = "Living Room",
sensor_type = "Door",
topic = mqtt_z2m("living/balcony"),
client = mqtt_client,
}))
automation.device_manager:add(ContactSensor.new({
name = "Window",
room = "Living Room",
topic = mqtt_z2m("living/window"),
client = mqtt_client,
}))
automation.device_manager:add(ContactSensor.new({
name = "Window",
room = "Bedroom",
topic = mqtt_z2m("bedroom/window"),
client = mqtt_client,
}))
automation.device_manager:add(ContactSensor.new({
name = "Window",
room = "Guest Room",
topic = mqtt_z2m("guest/window"),
client = mqtt_client,
}))
automation.device_manager:schedule("0 0 19 * * *", function() automation.device_manager:schedule("0 0 19 * * *", function()
bedroom_air_filter:set_on(true) bedroom_air_filter:set_on(true)
end) end)

View File

@ -14,6 +14,13 @@ traits! {
async fn on(&self) -> Result<bool, ErrorCode>, async fn on(&self) -> Result<bool, ErrorCode>,
"action.devices.commands.OnOff" => async fn set_on(&self, on: bool) -> Result<(), ErrorCode>, "action.devices.commands.OnOff" => async fn set_on(&self, on: bool) -> Result<(), ErrorCode>,
}, },
"action.devices.traits.OpenClose" => trait OpenClose {
discrete_only_open_close: Option<bool>,
command_only_open_close: Option<bool>,
query_only_open_close: Option<bool>,
async fn open_percent(&self) -> Result<u8, ErrorCode>,
"action.devices.commands.OpenClose" => async fn set_open_percent(&self, open_percent: u8) -> Result<(), ErrorCode>,
},
"action.devices.traits.Brightness" => trait Brightness { "action.devices.traits.Brightness" => trait Brightness {
command_only_brightness: Option<bool>, command_only_brightness: Option<bool>,
async fn brightness(&self) -> Result<u8, ErrorCode>, async fn brightness(&self) -> Result<u8, ErrorCode>,

View File

@ -12,4 +12,10 @@ pub enum Type {
Scene, Scene,
#[serde(rename = "action.devices.types.AIRPURIFIER")] #[serde(rename = "action.devices.types.AIRPURIFIER")]
AirPurifier, AirPurifier,
#[serde(rename = "action.devices.types.DOOR")]
Door,
#[serde(rename = "action.devices.types.WINDOW")]
Window,
#[serde(rename = "action.devices.types.DRAWER")]
Drawer,
} }