From 44194421012a723be1ae7b93c3330d8b5330195a Mon Sep 17 00:00:00 2001 From: LailaTheElf Date: Mon, 19 May 2025 01:07:18 +0200 Subject: [PATCH] more work on configurable automation --- mqttAutomation.yml | 27 +- src/automation/auto.rs | 48 --- src/automation/auto/config.rs | 30 -- src/automation/auto/config/triggers.rs | 33 -- .../auto/config/triggers/datetime.rs | 153 --------- src/automation/auto/mod.rs | 66 ++++ src/automation/auto/triggers.rs | 38 ++- src/automation/auto/triggers/datetime.rs | 316 ++++++++++++++++++ src/automation/mod.rs | 5 +- 9 files changed, 429 insertions(+), 287 deletions(-) delete mode 100644 src/automation/auto.rs delete mode 100644 src/automation/auto/config.rs delete mode 100644 src/automation/auto/config/triggers.rs delete mode 100644 src/automation/auto/config/triggers/datetime.rs create mode 100644 src/automation/auto/mod.rs create mode 100644 src/automation/auto/triggers/datetime.rs diff --git a/mqttAutomation.yml b/mqttAutomation.yml index 3124c0d..0bc2563 100644 --- a/mqttAutomation.yml +++ b/mqttAutomation.yml @@ -9,16 +9,17 @@ automation: alarm_hour: 7 alarm_minute: 0 -# automation: -# - trigger: -# - type: time -# date: -# weekday: [0,1,2,3,4] -# time: -# hour: 7 -# minute: 0 -# seconds: 0 -# action: -# - type: publish -# topic: "/cool/devices/lamp-01/set" -# pauload: "ON" + # auto: + # - trigger: + # !Time + # date: + # weekday: "0,1,2,3,4" + # time: + # hour: "7" + # minute: "0" + # seconds: "0" + # actions: + # - !Publish + # type: publish + # topic: "/cool/devices/lamp-01/set" + # pauload: "ON" diff --git a/src/automation/auto.rs b/src/automation/auto.rs deleted file mode 100644 index 458211a..0000000 --- a/src/automation/auto.rs +++ /dev/null @@ -1,48 +0,0 @@ -mod config; - - -pub mod auto_generator { - use std::collections::HashMap; - - pub use crate::automation::auto::config::automation_config as config; - - struct TimeTrigger { - next: u64, - time: config::triggers::Datetime - } - - pub struct Generator { - mqtt_triggers: HashMap, - time_triggers: HashMap, - } - impl Generator { - - pub fn new() -> Generator { - Generator { - mqtt_triggers: HashMap::new(), - time_triggers: HashMap::new() - } - } - - pub fn read_automations(&mut self, automations: Vec) { - for auto in automations { - self.read_automation_single(auto); - } - } - - fn read_automation_single(&mut self, auto: config::Automation) { - let auto_cloned = auto.clone(); - match auto.trigger { - config::triggers::Trigger::Mqtt(mqtt) => { - self.mqtt_triggers.insert(mqtt.topic, auto_cloned); - }, - config::triggers::Trigger::Time(datetime) => { - self.time_triggers.insert(mqtt.topic, auto_cloned); - },, - } - } - - } - - -} diff --git a/src/automation/auto/config.rs b/src/automation/auto/config.rs deleted file mode 100644 index aaeee48..0000000 --- a/src/automation/auto/config.rs +++ /dev/null @@ -1,30 +0,0 @@ -mod triggers; - -pub mod automation_config { - use serde::Deserialize; - - pub use crate::automation::auto::config::triggers::triggers; - - mod actions { - use serde::Deserialize; - - #[derive(Deserialize, Clone)] - struct Publish { - topic: String, - payload: Option, - retain: Option - } - - #[derive(Deserialize, Clone)] - pub enum Action { - Publish(Publish), - } - } - - #[derive(Deserialize, Clone)] - pub struct Automation { - pub trigger: triggers::Trigger, - pub actions: Vec - } - -} diff --git a/src/automation/auto/config/triggers.rs b/src/automation/auto/config/triggers.rs deleted file mode 100644 index 43f9c86..0000000 --- a/src/automation/auto/config/triggers.rs +++ /dev/null @@ -1,33 +0,0 @@ -mod datetime; - -pub mod triggers { - use serde::Deserialize; - - pub trait TriggerBase { - fn get_next_trigger(&self) -> &String; - } - - #[derive(Deserialize, Clone)] - enum StringOrU16 { - String(String), - U16(u16) - } - - #[derive(Deserialize, Clone)] - pub struct Mqtt { - pub topic: String, - pub payload: Option - } - impl TriggerBase for Mqtt { - fn get_next_trigger(&self) -> &String { - &self.topic - } - } - - - #[derive(Deserialize, Clone)] - pub enum Trigger { - Mqtt(Mqtt), - Time(Datetime), - } -} diff --git a/src/automation/auto/config/triggers/datetime.rs b/src/automation/auto/config/triggers/datetime.rs deleted file mode 100644 index 381d41a..0000000 --- a/src/automation/auto/config/triggers/datetime.rs +++ /dev/null @@ -1,153 +0,0 @@ - - -pub mod trigger_datetime { - use std::collections::btree_map::Range; - - use serde::Deserialize; - use crate::automation::auto::config::triggers::triggers::TriggerBase; - use chrono::{DateTime, Datelike, Local, Timelike}; - - fn get_u16_from_u32(value: u32) -> Option { - match u16::try_from(value) { - Ok(n) => Some(n), - Err(_) => None - } - } - fn get_u16_from_i32(value: i32) -> Option { - match u16::try_from(value) { - Ok(n) => Some(n), - Err(_) => None - } - } - - pub enum CompareError { - InvalidValue - DivNaN - } - - #[derive(Deserialize, Clone)] - enum StringOrU16 { - String(String), - U16(u16) - } - - #[derive(Deserialize, Clone)] - struct Date { - year: Option, - month: Option, - date: Option, - weekday: Option - } - #[derive(Deserialize, Clone)] - struct Time { - hour: Option, - minute: Option, - second: Option - } - - #[derive(Deserialize, Clone)] - pub struct Datetime { - date: Date, - time: Time - } - impl Datetime { - pub fn first_match(condition: String, start: u16, max: u16) -> Result, CompareError> { - if condition == "*" { - return Ok(Some(start)); - } - if condition.starts_with("*/") { - let divs = condition.split('/'); - let mut options: Vec = (start..max).collect(); - for div in divs { - match div.parse::() { - Ok(n) => { - options.retain(|x| x % n == 0); - }, - Err(_) => { - return Err(CompareError::DivNaN); - }, - } - } - let min = options.iter().min(); - match min { - Some(min) => { - return Ok(Some(min.clone())); - }, - None => { - return Ok(None); - }, - } - } - if condition.split(',').count() > 0 { - let parts = condition.split(','); - let mut options: Vec = [].to_vec(); - for part in parts { - match part.parse::() { - Ok(n) => { - options.push(n); - }, - Err(_) => { - return Err(CompareError::DivNaN); - }, - } - } - options.retain(|x| *x >= start && *x <= max); - let min = options.iter().min(); - match min { - Some(min) => { - return Ok(Some(min.clone())); - }, - None => { - return Ok(None); - }, - } - } - Err(CompareError::InvalidValue) - } - - fn find_next_trigger(&self, from: DateTime) -> DateTime { - let year: u16 = u16::MAX; - let month: u16 = u16::MAX; - let date: u16 = u16::MAX; - let weekday: u16 = u16::MAX; - let hour: u16 = u16::MAX; - let minute: u16 = u16::MAX; - let second: u16 = u16::MAX; - - let a_next_time: bool = true; - - // year - match self.date.year { - Some(y) => { - match y { - StringOrU16::String(y) => { - match self::Datetime::first_match(y, get_u16_from_i32(from.year()).unwrap(), u16::MAX) { - Ok(r) => { - match r { - Some(y) => year = y, - None => todo!(), - } - }, - Err(_) => todo!(), - } - }, - StringOrU16::U16(y) => { - if y == get_u16_from_u32(from.year()) { - year = y; - } - }, - } - - }, - None => todo!(), - } - - todo!() - } - } - impl TriggerBase for Datetime { - fn get_next_trigger(&self) -> &String { - let next_trigger = self.find_next_trigger(Local::now()) - } - } -} diff --git a/src/automation/auto/mod.rs b/src/automation/auto/mod.rs new file mode 100644 index 0000000..ab8faab --- /dev/null +++ b/src/automation/auto/mod.rs @@ -0,0 +1,66 @@ +mod triggers; + +pub mod automation { + use std::collections::HashMap; + + use serde::Deserialize; + + pub use crate::automation::auto::triggers::triggers; + + mod actions { + use serde::Deserialize; + + #[derive(Deserialize, Clone)] + struct Publish { + topic: String, + payload: Option, + retain: Option + } + + #[derive(Deserialize, Clone)] + pub enum Action { + Publish(Publish), + } + } + + #[derive(Deserialize, Clone)] + pub struct Automation { + pub trigger: triggers::Trigger, + pub actions: Vec + } + + pub struct Generator { + mqtt_triggers: HashMap, + time_triggers: HashMap, + } + impl Generator { + + pub fn new() -> Generator { + Generator { + mqtt_triggers: HashMap::new(), + time_triggers: HashMap::new() + } + } + + pub fn read_automations(&mut self, automations: Vec) { + for auto in automations { + self.read_automation_single(auto); + } + } + + fn read_automation_single(&mut self, auto: Automation) { + // let auto_cloned = auto.clone(); + match auto.trigger { + triggers::Trigger::Mqtt(mqtt) => { + self.mqtt_triggers.insert(mqtt.topic.clone(), mqtt.clone()); + }, + triggers::Trigger::Time(_datetime) => { + // self.time_triggers.insert(mqtt.topic, auto_cloned); + todo!() + } + } + } + + } + +} diff --git a/src/automation/auto/triggers.rs b/src/automation/auto/triggers.rs index bf4df50..6c56c6f 100644 --- a/src/automation/auto/triggers.rs +++ b/src/automation/auto/triggers.rs @@ -1,15 +1,39 @@ +mod datetime; -use - -pub trait TriggerBase { - fn key() -} pub mod triggers { -} + use serde::Deserialize; + pub trait TriggerBase { + fn get_next_trigger(&self) -> String; + } -pub impl clock { + #[derive(Deserialize, Clone)] + enum StringOrU16 { + String(String), + U16(u16) + } + // ===== mqtt + + #[derive(Deserialize, Clone)] + pub struct Mqtt { + pub topic: String, + pub payload: Option + } + impl TriggerBase for Mqtt { + fn get_next_trigger(&self) -> String { + self.topic.clone() + } + } + + // ===== datetime + + pub use crate::automation::auto::triggers::datetime::trigger_datetime::Datetime; + #[derive(Deserialize, Clone)] + pub enum Trigger { + Mqtt(Mqtt), + Time(Datetime), + } } diff --git a/src/automation/auto/triggers/datetime.rs b/src/automation/auto/triggers/datetime.rs new file mode 100644 index 0000000..b99bbe0 --- /dev/null +++ b/src/automation/auto/triggers/datetime.rs @@ -0,0 +1,316 @@ + + +pub mod trigger_datetime { + use serde::Deserialize; + use crate::automation::auto::triggers::triggers::TriggerBase; + use chrono::{DateTime, Datelike, Local, NaiveDate, TimeZone, Timelike}; + + fn get_u16_from_u32(value: u32) -> Option { + match u16::try_from(value) { + Ok(n) => Some(n), + Err(_) => None + } + } + fn get_u16_from_i32(value: i32) -> Option { + match u16::try_from(value) { + Ok(n) => Some(n), + Err(_) => None + } + } + + #[derive(Debug)] + pub enum CompareError { + InvalidValue, + DivNaN + } + + #[derive(Deserialize, Clone)] + struct Date { + year: Option, + month: Option, + date: Option, + weekday: Option + } + #[derive(Deserialize, Clone)] + struct Time { + hour: Option, + minute: Option, + second: Option + } + + #[derive(Deserialize, Clone)] + pub struct Datetime { + date: Date, + time: Time + } + impl Datetime { + pub fn first_match(condition: String, start: u16, end: u16) -> Result, CompareError> { + if condition == "*" { + return Ok(Some(start)); + } + if condition.starts_with("*/") { + let div = condition[2..].to_string(); + let mut options: Vec = (start..end).collect(); + match div.parse::() { + Ok(n) => { + options.retain(|x| x % n == 0); + }, + Err(_) => { + return Err(CompareError::DivNaN); + }, + } + let min = options.iter().min(); + match min { + Some(min) => { + return Ok(Some(min.clone())); + }, + None => { + return Ok(None); + }, + } + } + if condition.split(',').count() > 0 { + let parts = condition.split(','); + let mut options: Vec = [].to_vec(); + for part in parts { + match part.parse::() { + Ok(n) => { + options.push(n); + }, + Err(_) => { + return Err(CompareError::DivNaN); + }, + } + } + options.retain(|x| *x >= start && *x <= end); + let min = options.iter().min(); + match min { + Some(min) => { + return Ok(Some(min.clone())); + }, + None => { + return Ok(None); + }, + } + } + match condition.parse::() { + Ok(n) => { + if n >= start && n <= end { + return Ok(Some(n)) + } + else { + return Ok(None); + } + }, + Err(_) => { + return Err(CompareError::InvalidValue); + }, + } + } + + fn check_thing(&self, option: String, start: u16, end: u16) -> Option { + match self::Datetime::first_match(option, start, end) { + Ok(r) => { + return r + }, + Err(e) => { + println!("ERROR: trigger.datetime.find_next: failed to parse month: {:?}", e); + return None + }, + } + } + + fn get_days_in_month(date: &DateTime) -> u16 { + let year = date.year(); + let month = date.month(); + let date = match NaiveDate::from_ymd_opt(year, month + 1, 1) { + Some(d) => d, + None => NaiveDate::from_ymd_opt(year + 1, 1, 1).unwrap() + }; + let d = date.signed_duration_since(NaiveDate::from_ymd_opt(year, month, 1).unwrap()); + u16::try_from(d.num_days()).unwrap() + } + + fn find_next_trigger(&self, from: DateTime) -> Option> { + let mut next = from.clone(); + + // year + match &self.date.year { + Some(y) => { + match self::Datetime::first_match(y.to_string(), get_u16_from_i32(next.year()).unwrap(), u16::MAX) { + Ok(r) => { + match r { + Some(y) => { + if get_u16_from_i32(next.year()).unwrap() != y { + next = match Local::with_ymd_and_hms(&Local, i32::from(y), 1, 1, 0, 0, 0) { + chrono::offset::LocalResult::Single(d) => d, + chrono::offset::LocalResult::Ambiguous(d, _) => d, + chrono::offset::LocalResult::None => return None, + } + } + }, + None => return None, + } + }, + Err(e) => { + println!("ERROR: trigger.datetime.find_next: failed to parse year: {:?}", e); + return None + }, + } + }, + None => {}, + } + + // month + match &self.date.month { + Some(opt) => { + let now = get_u16_from_u32(next.month()).unwrap(); + match self.check_thing(opt.to_string(), now, 12) { + Some(r) => { + if r > now { + next = match Local::with_ymd_and_hms(&Local, next.year(), u32::from(r), 1, 0, 0, 0) { + chrono::offset::LocalResult::Single(d) => d, + chrono::offset::LocalResult::Ambiguous(d, _) => d, + chrono::offset::LocalResult::None => return None, + } + } + }, + None => { + next = match Local::with_ymd_and_hms(&Local, next.year()+1, 1, 1, 0, 0, 0) { + chrono::offset::LocalResult::Single(d) => d, + chrono::offset::LocalResult::Ambiguous(d, _) => d, + chrono::offset::LocalResult::None => return None + }; + if (next - Local::now()).num_days() < 2*365 { + return self.find_next_trigger(next); + } + else { + return None; + } + } + } + }, + None => {}, + } + + // date + match &self.date.date { + Some(opt) => { + let now = get_u16_from_u32(next.day()).unwrap(); + match self.check_thing(opt.to_string(), now, Datetime::get_days_in_month(&next)) { + Some(r) => { + if r > now { + next = match Local::with_ymd_and_hms(&Local, next.year(), next.month(), u32::from(r), 0, 0, 0) { + chrono::offset::LocalResult::Single(d) => d, + chrono::offset::LocalResult::Ambiguous(d, _) => d, + chrono::offset::LocalResult::None => return None, + } + } + }, + None => { + let m = match next.month() { + 12 => 1, + munth => munth + }; + let y = match m { + 1 => next.year()+1, + _ => next.year() + }; + next = match Local::with_ymd_and_hms(&Local, y, m, 1, 0, 0, 0) { + chrono::offset::LocalResult::Single(d) => d, + chrono::offset::LocalResult::Ambiguous(d, _) => d, + chrono::offset::LocalResult::None => return None + }; + if (next - Local::now()).num_days() < 2*365 { + return self.find_next_trigger(next); + } + else { + return None; + } + } + } + }, + None => {}, + } + + // hour + match &self.time.hour { + Some(opt) => { + let now = get_u16_from_u32(next.hour()).unwrap(); + match self.check_thing(opt.to_string(), now, 23) { + Some(r) => { + if r > now { + next = match Local::with_ymd_and_hms(&Local, next.year(), next.month(), next.hour(), u32::from(r), 0, 0) { + chrono::offset::LocalResult::Single(d) => d, + chrono::offset::LocalResult::Ambiguous(d, _) => d, + chrono::offset::LocalResult::None => return None, + } + } + }, + None => { + let mut d = next.day() + 1; + if d == u32::from(Datetime::get_days_in_month(&next)) { + d = 1; + } + let m = match d { + 1 => next.month()+1, + _ => next.month() + }; + let y = match m { + 1 => next.year()+1, + _ => next.year() + }; + next = match Local::with_ymd_and_hms(&Local, y, m, d, 0, 0, 0) { + chrono::offset::LocalResult::Single(d) => d, + chrono::offset::LocalResult::Ambiguous(d, _) => d, + chrono::offset::LocalResult::None => return None + }; + if (next - Local::now()).num_days() < 2*365 { + return self.find_next_trigger(next); + } + else { + return None; + } + } + } + }, + None => {}, + } + + Some(next) + } + } + impl TriggerBase for Datetime { + fn get_next_trigger(&self) -> String { + let next_trigger: Option> = self.find_next_trigger(Local::now()); + match next_trigger { + Some(next) => { + let str = next.format("%S").to_string(); + print!("datetime string: {str}"); + return str + }, + None => { + print!("ERROR: trigger.datetime.get_next: No next trigger found"); + return "None".to_string(); + } + } + } + } + + + + #[cfg(test)] + mod tests { + use super::*; + + #[test] + fn datetime_first_match() { + assert_eq!(Datetime::first_match("*".to_string(), 123, 128).unwrap(), Some(123)); + assert_eq!(Datetime::first_match("*/4".to_string(), 123, 128).unwrap(), Some(124)); + assert_eq!(Datetime::first_match("*/10".to_string(), 123, 128).unwrap(), None); + assert_eq!(Datetime::first_match("125".to_string(), 123, 128).unwrap(), Some(125)); + assert_eq!(Datetime::first_match("122,126".to_string(), 123, 128).unwrap(), Some(126)); + assert_eq!(Datetime::first_match("122,129".to_string(), 123, 128).unwrap(), None); + } + } +} diff --git a/src/automation/mod.rs b/src/automation/mod.rs index 1f17e93..ae5721f 100644 --- a/src/automation/mod.rs +++ b/src/automation/mod.rs @@ -1,7 +1,6 @@ mod json; //AUTO mod auto; -//AUTO use std::collections::HashMap; use std::{thread, time::Duration}; use serde::Deserialize; @@ -9,7 +8,7 @@ use mqtt_client::{MqttMessage, MqttEvent, Sender, Receiver, QoS}; use mqtt_client::mqtt_client; use crate::automation::json::json_parser; -//AUTO use crate::automation::auto::auto_generator::{config::Automation as AutoConfig, Generator}; +//AUTO use crate::automation::auto::automation::{Automation as AutoConfig, Generator}; #[derive(Deserialize)] pub struct SettingsConf { @@ -18,7 +17,7 @@ pub struct SettingsConf { alarm_hour: u8, alarm_minute: u8, - // auto: AutoConfig + //AUTO auto: Vec } struct PingStats {