Compare commits

...

4 Commits
v1.3.1 ... main

9 changed files with 665 additions and 9 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
/target
/mqttAutomation-*

210
Cargo.lock generated
View File

@ -17,6 +17,21 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "autocfg"
version = "1.4.0"
@ -44,6 +59,12 @@ version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36"
[[package]]
name = "bumpalo"
version = "3.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
[[package]]
name = "bytes"
version = "1.9.0"
@ -65,6 +86,20 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"wasm-bindgen",
"windows-link",
]
[[package]]
name = "core-foundation"
version = "0.9.4"
@ -208,6 +243,30 @@ version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
[[package]]
name = "iana-time-zone"
version = "0.1.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"log",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "indexmap"
version = "2.7.1"
@ -224,6 +283,16 @@ version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
[[package]]
name = "js-sys"
version = "0.3.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
dependencies = [
"once_cell",
"wasm-bindgen",
]
[[package]]
name = "json"
version = "0.12.4"
@ -289,8 +358,9 @@ dependencies = [
[[package]]
name = "mqttAutomation"
version = "1.3.1"
version = "1.3.2"
dependencies = [
"chrono",
"crossbeam",
"json",
"mqtt-client",
@ -299,6 +369,15 @@ dependencies = [
"serde_yaml",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "object"
version = "0.36.7"
@ -308,6 +387,12 @@ dependencies = [
"memchr",
]
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "openssl-probe"
version = "0.1.5"
@ -436,6 +521,12 @@ dependencies = [
"untrusted",
]
[[package]]
name = "rustversion"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
[[package]]
name = "ryu"
version = "1.0.18"
@ -646,6 +737,123 @@ version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
dependencies = [
"bumpalo",
"log",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
dependencies = [
"unicode-ident",
]
[[package]]
name = "windows-core"
version = "0.61.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-implement"
version = "0.60.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.59.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-link"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
[[package]]
name = "windows-result"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.52.0"

View File

@ -1,6 +1,6 @@
[package]
name = "mqttAutomation"
version = "1.3.1"
version = "1.3.2"
edition = "2021"
[dependencies]
@ -10,3 +10,4 @@ rumqttc = "0.24.0"
serde = { version = "1.0.217", features = ["derive"] }
serde_yaml = "0.9.34"
mqtt-client = { tag = "v4.0.0", git = "https://gitea.finnvanreenen.nl/LailaTheElf/mqttClient.git" }
chrono = "0.4.41"

View File

@ -1,5 +1,7 @@
#!/bin/sh
set -e
cross build --target aarch64-unknown-linux-gnu --release
cargo build --release

View File

@ -8,3 +8,18 @@ automation:
base_topic: "/kees/automation/"
alarm_hour: 7
alarm_minute: 0
# 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

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

@ -0,0 +1,39 @@
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)
}
// ===== 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,4 +1,5 @@
mod json;
//AUTO mod auto;
use std::{thread, time::Duration};
use serde::Deserialize;
@ -7,13 +8,16 @@ use mqtt_client::{MqttMessage, MqttEvent, Sender, Receiver, QoS};
use mqtt_client::mqtt_client;
use crate::automation::json::json_parser;
//AUTO use crate::automation::auto::automation::{Automation as AutoConfig, Generator};
#[derive(Deserialize)]
pub struct SettingsConf {
base_topic: String,
alarm_hour: u8,
alarm_minute: u8
alarm_minute: u8,
//AUTO auto: Vec<AutoConfig>
}
struct PingStats {
@ -53,7 +57,9 @@ pub struct Automation {
ping_elfdesktop: PingStats,
config: SettingsConf
config: SettingsConf,
//AUTO generator: Generator
}
@ -80,10 +86,10 @@ impl Automation {
}});
}
fn lamp01_set(&self, state: bool) {
self.tx_set(String::from("/cool/devices/lamp-01/set"), state);
self.tx_set(String::from("/kees/devices/lamp-01/set"), state);
}
fn pc_sw_set(&self, state: bool) {
self.tx_set(String::from("/cool/devices/sw-01/set"), state);
self.tx_set(String::from("/kees/devices/sw-01/set"), state);
}
fn get_current_time(&self) -> u32 {
@ -125,7 +131,7 @@ impl Automation {
if topic.starts_with("alarm/set") {
let mut time = message.payload.split(':');
if time.clone().count() != 2 {
if time.clone().count() != 2 && time.clone().count() != 3 {
println!("ERROR: config_message_in: alarm/set has invalid payload. incorect number of slices ({})", message.payload)
}
else {
@ -241,7 +247,7 @@ impl Automation {
else if message.topic.starts_with("clock/") {
self.clock_message_in(message);
}
else if message.topic.eq("/cool/devices/KNMITemp/values") {
else if message.topic.eq("/kees/devices/KNMITemp/values") {
let payload_json = json_parser::Json::Text(message.payload);
let path = Vec::from([String::from("gr")]);
@ -307,7 +313,7 @@ impl Automation {
println!("ERROR: main: faild to subscribe to automation/alarm/# ({})", e),
Ok(_) => {}
}
match self.client.subscribe("/cool/devices/KNMITemp/values", QoS::AtMostOnce) {
match self.client.subscribe("/kees/devices/KNMITemp/values", QoS::AtMostOnce) {
Err(e) =>
println!("ERROR: main: faild to subscribe to KNMITemp/values ({})", e),
Ok(_) => {}
@ -340,6 +346,8 @@ impl mqtt_client::MqttTool<SettingsConf> for Automation {
ping_elfdesktop: { PingStats { total: 0, fails: 0, avg: 0.0 } },
config: config,
//AUTO generator: Generator::new()
}
}