mirror of
https://codeberg.org/madmo/trvcontrol.git
synced 2024-12-04 11:02:17 +00:00
initial import
This commit is contained in:
commit
901b44284f
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/target
|
8
.idea/.gitignore
vendored
Normal file
8
.idea/.gitignore
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
8
.idea/modules.xml
Normal file
8
.idea/modules.xml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/space_heater.iml" filepath="$PROJECT_DIR$/.idea/space_heater.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
11
.idea/space_heater.iml
Normal file
11
.idea/space_heater.iml
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="CPP_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
6
.idea/vcs.xml
Normal file
6
.idea/vcs.xml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
1112
Cargo.lock
generated
Normal file
1112
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
15
Cargo.toml
Normal file
15
Cargo.toml
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
[package]
|
||||||
|
name = "trvcontrol"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
toml = "0.8.8"
|
||||||
|
rumqttc = "0.23.0"
|
||||||
|
clap = { version = "4.3.19", features = ["derive"] }
|
||||||
|
snafu = "0.7.5"
|
||||||
|
clokwerk = "0.4.0"
|
||||||
|
serde = { version = "1.0.190", features = ["derive"] }
|
||||||
|
chrono = "0.4.31"
|
||||||
|
log = "0.4.20"
|
||||||
|
env_logger = "0.10.0"
|
13
LICENSE.txt
Normal file
13
LICENSE.txt
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
Copyright 2023 Moritz Bitsch <moritz@h6t.eu>
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
purpose with or without fee is hereby granted, provided that the above
|
||||||
|
copyright notice and this permission notice appear in all copies.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
40
README.md
Normal file
40
README.md
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
# TRV Control
|
||||||
|
|
||||||
|
A small program to control thermostatic radiator valves using zigbee2mqtt. The idea is, that you write a configuration
|
||||||
|
file specifying profiles, which get activated on a specified schedule.
|
||||||
|
|
||||||
|
# Installation
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cargo install --locked --git https://github.com/madmo/trvcontrol
|
||||||
|
```
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
|
||||||
|
Sample config:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[mqtt]
|
||||||
|
host = "localhost"
|
||||||
|
port = 1883
|
||||||
|
user = "user"
|
||||||
|
password = "password"
|
||||||
|
|
||||||
|
[profiles.workday_morning]
|
||||||
|
alarm = { days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"], time = 04:00:00 }
|
||||||
|
temperature = { bath_trv = 23.0, bedroom_trv = 21.0 }
|
||||||
|
|
||||||
|
[profiles.workday_day]
|
||||||
|
alarm = { days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"], time = 08:00:00 }
|
||||||
|
temperature = { bath_trv = 20.0, office_trv = 23 }
|
||||||
|
```
|
||||||
|
|
||||||
|
Starting the program:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
trvcontrol config.toml
|
||||||
|
```
|
||||||
|
|
||||||
|
# License
|
||||||
|
|
||||||
|
Distributed under the ISC License. See LICENSE.txt for more information.
|
160
src/main.rs
Normal file
160
src/main.rs
Normal file
|
@ -0,0 +1,160 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use clap::Parser;
|
||||||
|
use clokwerk::Interval::{Friday, Saturday, Sunday, Thursday, Wednesday};
|
||||||
|
use clokwerk::{Interval, Job, Scheduler, SyncJob};
|
||||||
|
use log::{debug, error, warn};
|
||||||
|
use rumqttc::{Client, ClientError, MqttOptions, QoS};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use snafu::{ResultExt, Whatever};
|
||||||
|
use toml::value::Datetime;
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
struct Args {
|
||||||
|
/// path to config file
|
||||||
|
config: String,
|
||||||
|
|
||||||
|
#[clap(long)]
|
||||||
|
dump_config: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct Config {
|
||||||
|
mqtt: Mqtt,
|
||||||
|
profiles: HashMap<String, Profile>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct Mqtt {
|
||||||
|
host: String,
|
||||||
|
port: u16,
|
||||||
|
user: Option<String>,
|
||||||
|
password: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
type FriendlyName = String;
|
||||||
|
|
||||||
|
#[derive(Deserialize, Clone)]
|
||||||
|
pub struct Profile {
|
||||||
|
alarm: Option<Alarm>,
|
||||||
|
temperature: HashMap<FriendlyName, f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Clone)]
|
||||||
|
pub struct Alarm {
|
||||||
|
days: Vec<Days>,
|
||||||
|
time: Datetime,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Clone)]
|
||||||
|
pub enum Days {
|
||||||
|
Monday,
|
||||||
|
Tuesday,
|
||||||
|
Wednesday,
|
||||||
|
Thursday,
|
||||||
|
Friday,
|
||||||
|
Saturday,
|
||||||
|
Sunday,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Days> for Interval {
|
||||||
|
fn from(val: Days) -> Interval {
|
||||||
|
match val {
|
||||||
|
Days::Monday => Interval::Monday,
|
||||||
|
Days::Tuesday => Interval::Tuesday,
|
||||||
|
Days::Wednesday => Wednesday,
|
||||||
|
Days::Thursday => Thursday,
|
||||||
|
Days::Friday => Friday,
|
||||||
|
Days::Saturday => Saturday,
|
||||||
|
Days::Sunday => Sunday,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<(), Whatever> {
|
||||||
|
let args = Args::parse();
|
||||||
|
let config: Config = toml::from_str(std::fs::read_to_string(&args.config).unwrap().as_str())
|
||||||
|
.with_whatever_context(|_| format!("Could not parse config file {}", args.config))?;
|
||||||
|
|
||||||
|
let mut scheduler = Scheduler::new();
|
||||||
|
|
||||||
|
let mut mqttoptions = MqttOptions::new("space_heater", config.mqtt.host, config.mqtt.port);
|
||||||
|
mqttoptions.set_keep_alive(Duration::from_secs(5));
|
||||||
|
if let (Some(user), Some(password)) = (config.mqtt.user, config.mqtt.password) {
|
||||||
|
mqttoptions.set_credentials(user, password);
|
||||||
|
}
|
||||||
|
|
||||||
|
let (client, mut connection) = Client::new(mqttoptions, 10);
|
||||||
|
|
||||||
|
for (_, profile) in config.profiles {
|
||||||
|
handle_profile(&mut scheduler, profile, &client, args.dump_config);
|
||||||
|
}
|
||||||
|
|
||||||
|
let watch_thread = scheduler.watch_thread(Duration::from_millis(500));
|
||||||
|
|
||||||
|
for (_i, notification) in connection.iter().enumerate() {
|
||||||
|
match notification {
|
||||||
|
Ok(n) => debug!("mqtt notification: {:?}", n),
|
||||||
|
Err(e) => error!("mqtt error: {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch_thread.stop();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_profile(scheduler: &mut Scheduler, profile: Profile, client: &Client, dump: bool) {
|
||||||
|
if let Some(ref alarm) = profile.alarm {
|
||||||
|
let mut jobq: Option<&mut SyncJob> = Option::None;
|
||||||
|
|
||||||
|
let time = alarm.time.time.unwrap();
|
||||||
|
let time = chrono::naive::NaiveTime::from_hms_nano_opt(
|
||||||
|
time.hour as u32,
|
||||||
|
time.minute as u32,
|
||||||
|
time.second as u32,
|
||||||
|
time.nanosecond,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
for day in alarm.days.iter() {
|
||||||
|
let day: Interval = (*day).clone().into();
|
||||||
|
if let Some(prev) = jobq {
|
||||||
|
jobq = Some(prev.and_every(day).at_time(time))
|
||||||
|
} else {
|
||||||
|
jobq = Some(scheduler.every(day).at_time(time));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(jobq) = jobq {
|
||||||
|
if dump {
|
||||||
|
println!("{:#?}", jobq);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut client = client.clone();
|
||||||
|
jobq.run(move || {
|
||||||
|
for (room, temp) in profile.temperature.iter() {
|
||||||
|
match set_temperature(&mut client, room, *temp) {
|
||||||
|
Ok(_) => debug!("temperature in {} set to {}", room, temp),
|
||||||
|
Err(e) => warn!("could not set temperature: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_temperature(
|
||||||
|
client: &mut Client,
|
||||||
|
friendly_name: &str,
|
||||||
|
temperature: f32,
|
||||||
|
) -> Result<(), ClientError> {
|
||||||
|
let json = format!(
|
||||||
|
r#"{{"system_mode":"heat", "current_heating_setpoint":{}}}"#,
|
||||||
|
temperature
|
||||||
|
);
|
||||||
|
let topic = format!("zigbee2mqtt/{}/set", friendly_name);
|
||||||
|
println!("publish at {}: {}", topic, json);
|
||||||
|
client.publish(topic, QoS::AtMostOnce, false, json.as_bytes())
|
||||||
|
}
|
Loading…
Reference in a new issue