feat: Added low battery notification and made mqtt message parsing more robust
Resolves: #1
This commit is contained in:
@@ -36,6 +36,9 @@ pub struct Config {
|
||||
|
||||
#[device_config(from_lua, default)]
|
||||
pub callback: ActionCallback<ContactSensor, bool>,
|
||||
#[device_config(from_lua, default)]
|
||||
pub battery_callback: ActionCallback<ContactSensor, f32>,
|
||||
|
||||
#[device_config(from_lua)]
|
||||
pub client: WrappedAsyncClient,
|
||||
}
|
||||
@@ -149,21 +152,27 @@ impl OnMqtt for ContactSensor {
|
||||
return;
|
||||
}
|
||||
|
||||
let is_closed = match ContactMessage::try_from(message) {
|
||||
Ok(state) => state.is_closed(),
|
||||
let message = match ContactMessage::try_from(message) {
|
||||
Ok(message) => message,
|
||||
Err(err) => {
|
||||
error!(id = self.get_id(), "Failed to parse message: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if is_closed == self.state().await.is_closed {
|
||||
return;
|
||||
if let Some(is_closed) = message.contact {
|
||||
if is_closed == self.state().await.is_closed {
|
||||
return;
|
||||
}
|
||||
|
||||
self.config.callback.call(self, &!is_closed).await;
|
||||
|
||||
debug!(id = self.get_id(), "Updating state to {is_closed}");
|
||||
self.state_mut().await.is_closed = is_closed;
|
||||
}
|
||||
|
||||
self.config.callback.call(self, &!is_closed).await;
|
||||
|
||||
debug!(id = self.get_id(), "Updating state to {is_closed}");
|
||||
self.state_mut().await.is_closed = is_closed;
|
||||
if let Some(battery) = message.battery {
|
||||
self.config.battery_callback.call(self, &battery).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,9 +31,12 @@ pub struct Config {
|
||||
|
||||
#[device_config(from_lua, default)]
|
||||
pub right_hold_callback: ActionCallback<HueSwitch, ()>,
|
||||
|
||||
#[device_config(from_lua, default)]
|
||||
pub battery_callback: ActionCallback<HueSwitch, f32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[derive(Debug, Copy, Clone, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
enum Action {
|
||||
LeftPress,
|
||||
@@ -48,7 +51,8 @@ enum Action {
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
struct State {
|
||||
action: Action,
|
||||
action: Option<Action>,
|
||||
battery: Option<f32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, LuaDevice)]
|
||||
@@ -84,32 +88,43 @@ impl OnMqtt for HueSwitch {
|
||||
async fn on_mqtt(&self, message: Publish) {
|
||||
// Check if the message is from the device itself or from a remote
|
||||
if matches(&message.topic, &self.config.mqtt.topic) {
|
||||
let action = match serde_json::from_slice::<State>(&message.payload) {
|
||||
Ok(message) => message.action,
|
||||
let message = match serde_json::from_slice::<State>(&message.payload) {
|
||||
Ok(message) => message,
|
||||
Err(err) => {
|
||||
warn!(id = Device::get_id(self), "Failed to parse message: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
debug!(id = Device::get_id(self), "Remote action = {:?}", action);
|
||||
|
||||
match action {
|
||||
Action::LeftPressRelease => self.config.left_callback.call(self, &()).await,
|
||||
Action::RightPressRelease => self.config.right_callback.call(self, &()).await,
|
||||
Action::LeftHold => self.config.left_hold_callback.call(self, &()).await,
|
||||
Action::RightHold => self.config.right_hold_callback.call(self, &()).await,
|
||||
// If there is no hold action, the switch will act like a normal release
|
||||
Action::RightHoldRelease => {
|
||||
if !self.config.right_hold_callback.is_set() {
|
||||
self.config.right_callback.call(self, &()).await
|
||||
if let Some(action) = message.action {
|
||||
debug!(
|
||||
id = Device::get_id(self),
|
||||
?message.action,
|
||||
"Action received",
|
||||
);
|
||||
|
||||
match action {
|
||||
Action::LeftPressRelease => self.config.left_callback.call(self, &()).await,
|
||||
Action::RightPressRelease => self.config.right_callback.call(self, &()).await,
|
||||
Action::LeftHold => self.config.left_hold_callback.call(self, &()).await,
|
||||
Action::RightHold => self.config.right_hold_callback.call(self, &()).await,
|
||||
// If there is no hold action, the switch will act like a normal release
|
||||
Action::RightHoldRelease => {
|
||||
if !self.config.right_hold_callback.is_set() {
|
||||
self.config.right_callback.call(self, &()).await
|
||||
}
|
||||
}
|
||||
}
|
||||
Action::LeftHoldRelease => {
|
||||
if !self.config.left_hold_callback.is_set() {
|
||||
self.config.left_callback.call(self, &()).await
|
||||
Action::LeftHoldRelease => {
|
||||
if !self.config.left_hold_callback.is_set() {
|
||||
self.config.left_callback.call(self, &()).await
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if let Some(battery) = message.battery {
|
||||
self.config.battery_callback.call(self, &battery).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,8 @@ pub struct Config {
|
||||
|
||||
#[device_config(from_lua)]
|
||||
pub callback: ActionCallback<IkeaRemote, bool>,
|
||||
#[device_config(from_lua, default)]
|
||||
pub battery_callback: ActionCallback<IkeaRemote, f32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, LuaDevice)]
|
||||
@@ -60,31 +62,38 @@ impl OnMqtt for IkeaRemote {
|
||||
async fn on_mqtt(&self, message: Publish) {
|
||||
// Check if the message is from the deviec itself or from a remote
|
||||
if matches(&message.topic, &self.config.mqtt.topic) {
|
||||
let action = match RemoteMessage::try_from(message) {
|
||||
Ok(message) => message.action(),
|
||||
let message = match RemoteMessage::try_from(message) {
|
||||
Ok(message) => message,
|
||||
Err(err) => {
|
||||
error!(id = Device::get_id(self), "Failed to parse message: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
debug!(id = Device::get_id(self), "Remote action = {:?}", action);
|
||||
|
||||
let on = if self.config.single_button {
|
||||
match action {
|
||||
RemoteAction::On => Some(true),
|
||||
RemoteAction::BrightnessMoveUp => Some(false),
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
match action {
|
||||
RemoteAction::On => Some(true),
|
||||
RemoteAction::Off => Some(false),
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
if let Some(action) = message.action {
|
||||
debug!(id = Device::get_id(self), "Remote action = {:?}", action);
|
||||
|
||||
if let Some(on) = on {
|
||||
self.config.callback.call(self, &on).await;
|
||||
let on = if self.config.single_button {
|
||||
match action {
|
||||
RemoteAction::On => Some(true),
|
||||
RemoteAction::BrightnessMoveUp => Some(false),
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
match action {
|
||||
RemoteAction::On => Some(true),
|
||||
RemoteAction::Off => Some(false),
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(on) = on {
|
||||
self.config.callback.call(self, &on).await;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(battery) = message.battery {
|
||||
self.config.battery_callback.call(self, &battery).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,13 +68,8 @@ pub enum RemoteAction {
|
||||
// Message used to report the action performed by a remote
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct RemoteMessage {
|
||||
action: RemoteAction,
|
||||
}
|
||||
|
||||
impl RemoteMessage {
|
||||
pub fn action(&self) -> RemoteAction {
|
||||
self.action
|
||||
}
|
||||
pub action: Option<RemoteAction>,
|
||||
pub battery: Option<f32>,
|
||||
}
|
||||
|
||||
impl TryFrom<Publish> for RemoteMessage {
|
||||
@@ -144,13 +139,8 @@ impl TryFrom<Publish> for BrightnessMessage {
|
||||
// Message to report the state of a contact sensor
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ContactMessage {
|
||||
contact: bool,
|
||||
}
|
||||
|
||||
impl ContactMessage {
|
||||
pub fn is_closed(&self) -> bool {
|
||||
self.contact
|
||||
}
|
||||
pub contact: Option<bool>,
|
||||
pub battery: Option<f32>,
|
||||
}
|
||||
|
||||
impl TryFrom<Publish> for ContactMessage {
|
||||
|
||||
47
config.lua
47
config.lua
@@ -34,6 +34,37 @@ local ntfy = Ntfy.new({
|
||||
})
|
||||
automation.device_manager:add(ntfy)
|
||||
|
||||
local low_battery = {}
|
||||
local function check_battery(device, battery)
|
||||
local id = device:get_id()
|
||||
if battery < 15 then
|
||||
print("Device '" .. id .. "' has low battery: " .. tostring(battery))
|
||||
low_battery[id] = battery
|
||||
else
|
||||
low_battery[id] = nil
|
||||
end
|
||||
end
|
||||
automation.device_manager:schedule("0 0 21 */1 * *", function()
|
||||
-- Don't send notifications if there are now devices with low battery
|
||||
if next(low_battery) == nil then
|
||||
print("No devices with low battery")
|
||||
return
|
||||
end
|
||||
|
||||
local lines = {}
|
||||
for name, battery in pairs(low_battery) do
|
||||
table.insert(lines, name .. ": " .. tostring(battery) .. "%")
|
||||
end
|
||||
local message = table.concat(lines, "\n")
|
||||
|
||||
ntfy:send_notification({
|
||||
title = "Low battery",
|
||||
message = message,
|
||||
tags = { "battery" },
|
||||
priority = "default",
|
||||
})
|
||||
end)
|
||||
|
||||
local on_presence = {
|
||||
add = function(self, f)
|
||||
self[#self + 1] = f
|
||||
@@ -171,6 +202,7 @@ automation.device_manager:add(HueSwitch.new({
|
||||
right_hold_callback = function()
|
||||
living_lights_relax:set_on(true)
|
||||
end,
|
||||
battery_callback = check_battery,
|
||||
}))
|
||||
|
||||
automation.device_manager:add(WakeOnLAN.new({
|
||||
@@ -222,6 +254,7 @@ automation.device_manager:add(IkeaRemote.new({
|
||||
end
|
||||
end
|
||||
end,
|
||||
battery_callback = check_battery,
|
||||
}))
|
||||
|
||||
local function kettle_timeout()
|
||||
@@ -260,6 +293,7 @@ automation.device_manager:add(IkeaRemote.new({
|
||||
topic = mqtt_z2m("bedroom/remote"),
|
||||
single_button = true,
|
||||
callback = set_kettle,
|
||||
battery_callback = check_battery,
|
||||
}))
|
||||
|
||||
automation.device_manager:add(IkeaRemote.new({
|
||||
@@ -269,6 +303,7 @@ automation.device_manager:add(IkeaRemote.new({
|
||||
topic = mqtt_z2m("kitchen/remote"),
|
||||
single_button = true,
|
||||
callback = set_kettle,
|
||||
battery_callback = check_battery,
|
||||
}))
|
||||
|
||||
local function off_timeout(duration)
|
||||
@@ -356,6 +391,7 @@ automation.device_manager:add(IkeaRemote.new({
|
||||
workbench_light:set_on(false)
|
||||
end
|
||||
end,
|
||||
battery_callback = check_battery,
|
||||
}))
|
||||
|
||||
local hallway_top_light = HueGroup.new({
|
||||
@@ -373,6 +409,7 @@ automation.device_manager:add(HueSwitch.new({
|
||||
left_callback = function()
|
||||
hallway_top_light:set_on(not hallway_top_light:on())
|
||||
end,
|
||||
battery_callback = check_battery,
|
||||
}))
|
||||
automation.device_manager:add(HueSwitch.new({
|
||||
name = "SwitchTop",
|
||||
@@ -382,6 +419,7 @@ automation.device_manager:add(HueSwitch.new({
|
||||
left_callback = function()
|
||||
hallway_top_light:set_on(not hallway_top_light:on())
|
||||
end,
|
||||
battery_callback = check_battery,
|
||||
}))
|
||||
|
||||
local hallway_light_automation = {
|
||||
@@ -488,6 +526,7 @@ automation.device_manager:add(IkeaRemote.new({
|
||||
callback = function(_, on)
|
||||
hallway_light_automation:switch_callback(on)
|
||||
end,
|
||||
battery_callback = check_battery,
|
||||
}))
|
||||
local hallway_frontdoor = ContactSensor.new({
|
||||
name = "Frontdoor",
|
||||
@@ -503,6 +542,7 @@ local hallway_frontdoor = ContactSensor.new({
|
||||
hallway_light_automation:door_callback(open)
|
||||
frontdoor_presence(open)
|
||||
end,
|
||||
battery_callback = check_battery,
|
||||
})
|
||||
automation.device_manager:add(hallway_frontdoor)
|
||||
hallway_light_automation.door = hallway_frontdoor
|
||||
@@ -516,6 +556,7 @@ local hallway_trash = ContactSensor.new({
|
||||
callback = function(_, open)
|
||||
hallway_light_automation:trash_callback(open)
|
||||
end,
|
||||
battery_callback = check_battery,
|
||||
})
|
||||
automation.device_manager:add(hallway_trash)
|
||||
hallway_light_automation.trash = hallway_trash
|
||||
@@ -564,6 +605,7 @@ automation.device_manager:add(HueSwitch.new({
|
||||
left_hold_callback = function()
|
||||
bedroom_lights_relax:set_on(true)
|
||||
end,
|
||||
battery_callback = check_battery,
|
||||
}))
|
||||
|
||||
automation.device_manager:add(ContactSensor.new({
|
||||
@@ -572,24 +614,28 @@ automation.device_manager:add(ContactSensor.new({
|
||||
sensor_type = "Door",
|
||||
topic = mqtt_z2m("living/balcony"),
|
||||
client = mqtt_client,
|
||||
battery_callback = check_battery,
|
||||
}))
|
||||
automation.device_manager:add(ContactSensor.new({
|
||||
name = "Window",
|
||||
room = "Living Room",
|
||||
topic = mqtt_z2m("living/window"),
|
||||
client = mqtt_client,
|
||||
battery_callback = check_battery,
|
||||
}))
|
||||
automation.device_manager:add(ContactSensor.new({
|
||||
name = "Window",
|
||||
room = "Bedroom",
|
||||
topic = mqtt_z2m("bedroom/window"),
|
||||
client = mqtt_client,
|
||||
battery_callback = check_battery,
|
||||
}))
|
||||
automation.device_manager:add(ContactSensor.new({
|
||||
name = "Window",
|
||||
room = "Guest Room",
|
||||
topic = mqtt_z2m("guest/window"),
|
||||
client = mqtt_client,
|
||||
battery_callback = check_battery,
|
||||
}))
|
||||
|
||||
local storage_light = LightBrightness.new({
|
||||
@@ -614,6 +660,7 @@ automation.device_manager:add(ContactSensor.new({
|
||||
storage_light:set_on(false)
|
||||
end
|
||||
end,
|
||||
battery_callback = check_battery,
|
||||
}))
|
||||
|
||||
automation.device_manager:schedule("0 0 19 * * *", function()
|
||||
|
||||
Reference in New Issue
Block a user