more work on configurable automation

This commit is contained in:
Laila van Reenen 2025-05-19 01:07:18 +02:00
parent 31e0664aa6
commit 4419442101
Signed by: LailaTheElf
GPG Key ID: 8A3EF0226518C12D
9 changed files with 429 additions and 287 deletions

View File

@ -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"

View File

@ -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<String, config::Automation>,
time_triggers: HashMap<String, TimeTrigger>,
}
impl Generator {
pub fn new() -> Generator {
Generator {
mqtt_triggers: HashMap::new(),
time_triggers: HashMap::new()
}
}
pub fn read_automations(&mut self, automations: Vec<config::Automation>) {
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);
},,
}
}
}
}

View File

@ -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<String>,
retain: Option<bool>
}
#[derive(Deserialize, Clone)]
pub enum Action {
Publish(Publish),
}
}
#[derive(Deserialize, Clone)]
pub struct Automation {
pub trigger: triggers::Trigger,
pub actions: Vec<actions::Action>
}
}

View File

@ -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<String>
}
impl TriggerBase for Mqtt {
fn get_next_trigger(&self) -> &String {
&self.topic
}
}
#[derive(Deserialize, Clone)]
pub enum Trigger {
Mqtt(Mqtt),
Time(Datetime),
}
}

View File

@ -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<u16> {
match u16::try_from(value) {
Ok(n) => Some(n),
Err(_) => None
}
}
fn get_u16_from_i32(value: i32) -> Option<u16> {
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<StringOrU16>,
month: Option<StringOrU16>,
date: Option<StringOrU16>,
weekday: Option<StringOrU16>
}
#[derive(Deserialize, Clone)]
struct Time {
hour: Option<StringOrU16>,
minute: Option<StringOrU16>,
second: Option<StringOrU16>
}
#[derive(Deserialize, Clone)]
pub struct Datetime {
date: Date,
time: Time
}
impl Datetime {
pub fn first_match(condition: String, start: u16, max: u16) -> Result<Option<u16>, CompareError> {
if condition == "*" {
return Ok(Some(start));
}
if condition.starts_with("*/") {
let divs = condition.split('/');
let mut options: Vec<u16> = (start..max).collect();
for div in divs {
match div.parse::<u16>() {
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<u16> = [].to_vec();
for part in parts {
match part.parse::<u16>() {
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<Local>) -> DateTime<Local> {
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())
}
}
}

View File

@ -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<String>,
retain: Option<bool>
}
#[derive(Deserialize, Clone)]
pub enum Action {
Publish(Publish),
}
}
#[derive(Deserialize, Clone)]
pub struct Automation {
pub trigger: triggers::Trigger,
pub actions: Vec<actions::Action>
}
pub struct Generator {
mqtt_triggers: HashMap<String, triggers::Mqtt>,
time_triggers: HashMap<String, triggers::Datetime>,
}
impl Generator {
pub fn new() -> Generator {
Generator {
mqtt_triggers: HashMap::new(),
time_triggers: HashMap::new()
}
}
pub fn read_automations(&mut self, automations: Vec<Automation>) {
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!()
}
}
}
}
}

View File

@ -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<String>
}
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),
}
}

View File

@ -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<u16> {
match u16::try_from(value) {
Ok(n) => Some(n),
Err(_) => None
}
}
fn get_u16_from_i32(value: i32) -> Option<u16> {
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<String>,
month: Option<String>,
date: Option<String>,
weekday: Option<String>
}
#[derive(Deserialize, Clone)]
struct Time {
hour: Option<String>,
minute: Option<String>,
second: Option<String>
}
#[derive(Deserialize, Clone)]
pub struct Datetime {
date: Date,
time: Time
}
impl Datetime {
pub fn first_match(condition: String, start: u16, end: u16) -> Result<Option<u16>, CompareError> {
if condition == "*" {
return Ok(Some(start));
}
if condition.starts_with("*/") {
let div = condition[2..].to_string();
let mut options: Vec<u16> = (start..end).collect();
match div.parse::<u16>() {
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<u16> = [].to_vec();
for part in parts {
match part.parse::<u16>() {
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::<u16>() {
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<u16> {
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<Local>) -> 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<Local>) -> Option<DateTime<Local>> {
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<DateTime<Local>> = 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);
}
}
}

View File

@ -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<AutoConfig>
}
struct PingStats {