first consept for configurable automation
This commit is contained in:
parent
642e02949f
commit
6d5340e9ca
208
Cargo.lock
generated
208
Cargo.lock
generated
@ -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"
|
||||
@ -291,6 +360,7 @@ dependencies = [
|
||||
name = "mqttAutomation"
|
||||
version = "1.3.1"
|
||||
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"
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -8,3 +8,17 @@ automation:
|
||||
base_topic: "/kees/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"
|
||||
|
||||
48
src/automation/auto.rs
Normal file
48
src/automation/auto.rs
Normal file
@ -0,0 +1,48 @@
|
||||
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);
|
||||
},,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
30
src/automation/auto/config.rs
Normal file
30
src/automation/auto/config.rs
Normal file
@ -0,0 +1,30 @@
|
||||
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>
|
||||
}
|
||||
|
||||
}
|
||||
33
src/automation/auto/config/triggers.rs
Normal file
33
src/automation/auto/config/triggers.rs
Normal file
@ -0,0 +1,33 @@
|
||||
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),
|
||||
}
|
||||
}
|
||||
153
src/automation/auto/config/triggers/datetime.rs
Normal file
153
src/automation/auto/config/triggers/datetime.rs
Normal file
@ -0,0 +1,153 @@
|
||||
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
15
src/automation/auto/triggers.rs
Normal file
15
src/automation/auto/triggers.rs
Normal file
@ -0,0 +1,15 @@
|
||||
|
||||
use
|
||||
|
||||
pub trait TriggerBase {
|
||||
fn key()
|
||||
}
|
||||
pub mod triggers {
|
||||
}
|
||||
|
||||
|
||||
pub impl clock {
|
||||
|
||||
|
||||
|
||||
}
|
||||
@ -1,5 +1,7 @@
|
||||
mod json;
|
||||
//AUTO mod auto;
|
||||
|
||||
//AUTO use std::collections::HashMap;
|
||||
use std::{thread, time::Duration};
|
||||
use serde::Deserialize;
|
||||
|
||||
@ -7,13 +9,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::auto_generator::{config::Automation as AutoConfig, Generator};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct SettingsConf {
|
||||
base_topic: String,
|
||||
|
||||
alarm_hour: u8,
|
||||
alarm_minute: u8
|
||||
alarm_minute: u8,
|
||||
|
||||
// auto: AutoConfig
|
||||
}
|
||||
|
||||
struct PingStats {
|
||||
@ -53,7 +58,9 @@ pub struct Automation {
|
||||
|
||||
ping_elfdesktop: PingStats,
|
||||
|
||||
config: SettingsConf
|
||||
config: SettingsConf,
|
||||
|
||||
//AUTO generator: Generator
|
||||
}
|
||||
|
||||
|
||||
@ -340,6 +347,8 @@ impl mqtt_client::MqttTool<SettingsConf> for Automation {
|
||||
ping_elfdesktop: { PingStats { total: 0, fails: 0, avg: 0.0 } },
|
||||
|
||||
config: config,
|
||||
|
||||
//AUTO generator: Generator::new()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user