fix, fmt, refactor, document
This commit is contained in:
parent
a171f9ffed
commit
603fff0094
10 changed files with 167 additions and 81 deletions
24
Cargo.lock
generated
24
Cargo.lock
generated
|
@ -688,7 +688,6 @@ dependencies = [
|
||||||
"json",
|
"json",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"log",
|
"log",
|
||||||
"mac_address",
|
|
||||||
"notify-rust",
|
"notify-rust",
|
||||||
"rand",
|
"rand",
|
||||||
"regex",
|
"regex",
|
||||||
|
@ -918,16 +917,6 @@ dependencies = [
|
||||||
"time",
|
"time",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "mac_address"
|
|
||||||
version = "1.1.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b238e3235c8382b7653c6408ed1b08dd379bdb9fdf990fb0bbae3db2cc0ae963"
|
|
||||||
dependencies = [
|
|
||||||
"nix 0.23.2",
|
|
||||||
"winapi",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mach"
|
name = "mach"
|
||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
|
@ -1018,19 +1007,6 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "nix"
|
|
||||||
version = "0.23.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags",
|
|
||||||
"cc",
|
|
||||||
"cfg-if",
|
|
||||||
"libc",
|
|
||||||
"memoffset 0.6.5",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nix"
|
name = "nix"
|
||||||
version = "0.25.1"
|
version = "0.25.1"
|
||||||
|
|
|
@ -19,7 +19,6 @@ image = "0.24.5"
|
||||||
json = "0.12.4"
|
json = "0.12.4"
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
log = "0.4.17"
|
log = "0.4.17"
|
||||||
mac_address = "1.1.4"
|
|
||||||
notify-rust = { version = "4.8.0", features = ["images"] }
|
notify-rust = { version = "4.8.0", features = ["images"] }
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
regex = "1.7.1"
|
regex = "1.7.1"
|
||||||
|
|
101
README.md
101
README.md
|
@ -4,35 +4,116 @@
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- [ ] Fallback MQTT broker address
|
|
||||||
- [x] Command buttons
|
- [x] Command buttons
|
||||||
- [x] Notifications
|
- [x] Notifications
|
||||||
- [x] Actions
|
- [x] Actions
|
||||||
- [x] System stats
|
- [x] System information reporting (CPU usage, battery status, …)
|
||||||
- [x] CPU usage
|
|
||||||
- [x] RAM usage
|
|
||||||
- [x] Battery: level, charging status
|
|
||||||
- [ ] Media control (MPRIS)
|
- [ ] Media control (MPRIS)
|
||||||
- [ ] PipeWire
|
- [ ] PipeWire control
|
||||||
- [ ] File watcher
|
- [ ] File watcher
|
||||||
|
- [ ] Fallback MQTT broker address
|
||||||
|
|
||||||
Ideas:
|
Ideas:
|
||||||
|
|
||||||
- Camera video stream
|
- Camera video stream
|
||||||
- Idle time (→ libseat)
|
- Idle time (→ libseat)
|
||||||
|
|
||||||
## Installation
|
## Installation and configuration
|
||||||
|
|
||||||
TBD
|
TBD
|
||||||
|
|
||||||
|
If the configuration file does not exist, an example file is created and the program exits.
|
||||||
|
|
||||||
|
The default path is `$XDG_CONFIG_HOME/hassliebe/config.toml`.
|
||||||
|
It can be changed using the `HASSLIEBE_CONFIG` environment variable.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
friendly_id = "my_pc" # Used as prefix for MQTT topics and Home Assistant IDs
|
||||||
|
display_name = "My PC"
|
||||||
|
|
||||||
|
[mqtt]
|
||||||
|
host = "127.0.0.1" # You probably need to change this
|
||||||
|
port = 1883
|
||||||
|
|
||||||
|
[modules.buttons]
|
||||||
|
enabled = true
|
||||||
|
|
||||||
|
[[modules.buttons.buttons]]
|
||||||
|
id = "reboot"
|
||||||
|
name = "Reboot"
|
||||||
|
command = "shutdown -r now"
|
||||||
|
|
||||||
|
[modules.info]
|
||||||
|
enabled = true
|
||||||
|
cpu_usage = 10 # update interval in seconds
|
||||||
|
ram_usage = 0 # 0 disables reporting
|
||||||
|
battery = 60
|
||||||
|
|
||||||
|
[modules.notifications]
|
||||||
|
enabled = true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Machine identification
|
||||||
|
|
||||||
|
Hassliebe needs a way to uniquely identify a machine.
|
||||||
|
`friendly_id` is not suited well for this because the user may want to change it from time to time.
|
||||||
|
|
||||||
|
If `/etc/machine-id` exists (as is the case with systemd-based systems), it will be used.
|
||||||
|
Otherwise, a random ID will be generated on the first run and stored in `$XDG_DATA_HOME/hassliebe/machine_id`.
|
||||||
|
The latter always takes precedence.
|
||||||
|
|
||||||
## Modules
|
## Modules
|
||||||
|
|
||||||
|
### Buttons
|
||||||
|
|
||||||
|
You can define MQTT buttons in your configuration file like this:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[[modules.buttons.buttons]]
|
||||||
|
id = "shutdown" # must be unique among all the buttons in the file
|
||||||
|
name = "Shutdown"
|
||||||
|
command = "shutdown -h now"
|
||||||
|
run_in_shell = true # defaults to false
|
||||||
|
```
|
||||||
|
|
||||||
|
When `run_in_shell` is set to `true`, the command will be run with `/bin/sh`.
|
||||||
|
|
||||||
|
### Info
|
||||||
|
|
||||||
|
Hassliebe currently supports reporting the following system stats:
|
||||||
|
|
||||||
|
- CPU usage (%)
|
||||||
|
- RAM usage (%)
|
||||||
|
- Battery level (%) and status (`/sys/class/power_supply/[name]/status`)
|
||||||
|
|
||||||
|
For each of these, the update interval can be set to a number of seconds or `0` to disable it:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[modules.info]
|
||||||
|
enabled = true
|
||||||
|
cpu_usage = 10 # updates every 10 seconds
|
||||||
|
ram_usage = 0 # disabled
|
||||||
|
battery = 60 # updates every 60 seconds
|
||||||
|
```
|
||||||
|
|
||||||
### Notifications
|
### Notifications
|
||||||
|
|
||||||
|
The notifications module allows sending notifications according to the
|
||||||
|
[Desktop Notifications Specification](https://specifications.freedesktop.org/notification-spec/notification-spec-latest.html#markup).
|
||||||
|
|
||||||
|
Remember to enable the module:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[modules.notifications]
|
||||||
|
enabled = true
|
||||||
|
```
|
||||||
|
|
||||||
Two MQTT topics can be used for sending notifications:
|
Two MQTT topics can be used for sending notifications:
|
||||||
|
|
||||||
- `[unique_id]/notifications/simple` — for plain-text messages
|
- `[friendly_id]/notifications/simple` — for plain-text messages
|
||||||
- `[unique_id]/notifications/json` — for JSON-encoded messages matching the schema described below
|
- `[friendly_id]/notifications/json` — for JSON-encoded messages matching the schema described below
|
||||||
|
|
||||||
Complex messages have these properties:
|
Complex messages have these properties:
|
||||||
|
|
||||||
|
@ -53,7 +134,7 @@ Complex messages have these properties:
|
||||||
#### Actions
|
#### Actions
|
||||||
|
|
||||||
When a notification action is invoked, the ID of the action is sent to the MQTT topic with the following name:
|
When a notification action is invoked, the ID of the action is sent to the MQTT topic with the following name:
|
||||||
`[unique_id]/notifications/actions/[notification_id]`.
|
`[friendly_id]/notifications/actions/[notification_id]`.
|
||||||
|
|
||||||
When a notification is dismissed, `closed` is sent into the topic.
|
When a notification is dismissed, `closed` is sent into the topic.
|
||||||
|
|
||||||
|
|
|
@ -5,12 +5,10 @@ use std::io::{ErrorKind, Read, Write};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
use regex::Regex;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use validator::{Validate, ValidationError};
|
use validator::Validate;
|
||||||
|
|
||||||
use crate::modules;
|
use crate::modules;
|
||||||
use crate::util::generate_alphanumeric_id;
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Validate)]
|
#[derive(Serialize, Deserialize, Validate)]
|
||||||
pub struct Mqtt {
|
pub struct Mqtt {
|
||||||
|
@ -37,45 +35,28 @@ pub struct Internal {
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Validate)]
|
#[derive(Serialize, Deserialize, Validate)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
#[validate(custom = "validate_unique_id")]
|
#[validate(custom = "crate::util::validate_hass_id")]
|
||||||
pub unique_id: String,
|
pub friendly_id: String,
|
||||||
|
|
||||||
#[validate(length(min = 1))]
|
#[validate(length(min = 1))]
|
||||||
pub display_name: String,
|
pub display_name: String,
|
||||||
pub announce_mac_address: bool,
|
|
||||||
|
|
||||||
#[validate]
|
#[validate]
|
||||||
pub mqtt: Mqtt,
|
pub mqtt: Mqtt,
|
||||||
|
|
||||||
#[validate]
|
#[validate]
|
||||||
pub modules: modules::Config,
|
pub modules: modules::Config,
|
||||||
|
|
||||||
#[serde(rename = "DO_NOT_CHANGE")]
|
|
||||||
#[validate]
|
|
||||||
pub internal: Internal,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn validate_unique_id(value: &str) -> Result<(), ValidationError> {
|
|
||||||
if Regex::new(r"^[a-zA-Z0-9]+(_[a-zA-Z0-9]+)*$").unwrap().is_match(value) {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(ValidationError::new("does not match regex"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_example_config() -> Config {
|
fn create_example_config() -> Config {
|
||||||
Config {
|
Config {
|
||||||
unique_id: "my_pc".to_owned(),
|
friendly_id: "my_pc".to_owned(),
|
||||||
display_name: "My PC".to_owned(),
|
display_name: "My PC".to_owned(),
|
||||||
announce_mac_address: true,
|
|
||||||
mqtt: Mqtt {
|
mqtt: Mqtt {
|
||||||
host: "".to_owned(),
|
host: "".to_owned(),
|
||||||
port: 1883,
|
port: 1883,
|
||||||
credentials: None,
|
credentials: None,
|
||||||
},
|
},
|
||||||
internal: Internal {
|
|
||||||
stable_id: generate_alphanumeric_id(12),
|
|
||||||
},
|
|
||||||
modules: modules::Config::default(),
|
modules: modules::Config::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
45
src/main.rs
45
src/main.rs
|
@ -7,6 +7,7 @@ use anyhow::{anyhow, Result};
|
||||||
|
|
||||||
use crate::modules::{InitializationContext, ModuleContext, ModuleContextMqtt};
|
use crate::modules::{InitializationContext, ModuleContext, ModuleContextMqtt};
|
||||||
use crate::mqtt::OwnedTopicsService;
|
use crate::mqtt::OwnedTopicsService;
|
||||||
|
use crate::util::generate_alphanumeric_id;
|
||||||
|
|
||||||
mod config;
|
mod config;
|
||||||
mod modules;
|
mod modules;
|
||||||
|
@ -44,19 +45,54 @@ async fn get_paths_and_create_directories() -> Result<Paths> {
|
||||||
Ok(paths)
|
Ok(paths)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn initialize_logger() {
|
||||||
|
let mut builder = env_logger::builder();
|
||||||
|
builder.parse_filters("warn,hassliebe=info");
|
||||||
|
builder.parse_default_env();
|
||||||
|
builder.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_machine_id(paths: &Paths) -> Result<String> {
|
||||||
|
let overwrite_path = paths.data_directory.join("machine_id");
|
||||||
|
let mut value = tokio::fs::read_to_string(&overwrite_path).await.unwrap_or("".to_owned()).trim().to_owned();
|
||||||
|
|
||||||
|
if value.is_empty() {
|
||||||
|
log::debug!("{} does not exist or is empty", overwrite_path.to_string_lossy());
|
||||||
|
|
||||||
|
match tokio::fs::read_to_string("/etc/machine-id").await {
|
||||||
|
Ok(id) => {
|
||||||
|
value = id.trim().to_owned();
|
||||||
|
log::debug!("Found machine ID in /etc/machine-id: {}", value);
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
log::debug!("Could not read /etc/machine-id");
|
||||||
|
|
||||||
|
value = generate_alphanumeric_id(32);
|
||||||
|
log::info!("Generated new machine ID: {}", value);
|
||||||
|
tokio::fs::write(overwrite_path, &value).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log::debug!("Found machine ID in {}: {}", overwrite_path.to_string_lossy(), value);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(value)
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
env_logger::init();
|
initialize_logger();
|
||||||
|
|
||||||
let paths = get_paths_and_create_directories().await?;
|
let paths = get_paths_and_create_directories().await?;
|
||||||
|
let machine_id = load_machine_id(&paths).await?;
|
||||||
|
|
||||||
let Some(config) = config::load(&paths.config)? else {
|
let Some(config) = config::load(&paths.config)? else {
|
||||||
exit(exitcode::CONFIG);
|
exit(exitcode::CONFIG);
|
||||||
};
|
};
|
||||||
|
|
||||||
let availability_topic = config.unique_id.to_owned() + "/availability";
|
let availability_topic = config.friendly_id.to_owned() + "/availability";
|
||||||
let (mqtt_client, event_loop) = mqtt::create_client(&config, &availability_topic).await?;
|
let (mqtt_client, event_loop) = mqtt::create_client(&config, &machine_id, &availability_topic).await?;
|
||||||
let discovery_device_object = mqtt::create_discovery_device_object(&config);
|
let discovery_device_object = mqtt::create_discovery_device_object(&config, &machine_id);
|
||||||
|
|
||||||
let owned_topics_service = OwnedTopicsService::new(&paths.data_directory).await?;
|
let owned_topics_service = OwnedTopicsService::new(&paths.data_directory).await?;
|
||||||
|
|
||||||
|
@ -66,6 +102,7 @@ async fn main() -> Result<()> {
|
||||||
|
|
||||||
full: Arc::new(ModuleContext {
|
full: Arc::new(ModuleContext {
|
||||||
config,
|
config,
|
||||||
|
machine_id,
|
||||||
mqtt: ModuleContextMqtt {
|
mqtt: ModuleContextMqtt {
|
||||||
client: mqtt_client,
|
client: mqtt_client,
|
||||||
availability_topic,
|
availability_topic,
|
||||||
|
|
|
@ -3,7 +3,6 @@ use serde::{Deserialize, Serialize};
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
use validator::Validate;
|
use validator::Validate;
|
||||||
|
|
||||||
use crate::config::validate_unique_id;
|
|
||||||
use crate::modules::InitializationContext;
|
use crate::modules::InitializationContext;
|
||||||
|
|
||||||
const MODULE_ID: &str = "buttons";
|
const MODULE_ID: &str = "buttons";
|
||||||
|
@ -11,7 +10,7 @@ const BUTTON_TRIGGER_TEXT: &str = "press";
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Validate, Clone)]
|
#[derive(Serialize, Deserialize, Validate, Clone)]
|
||||||
pub struct ButtonConfig {
|
pub struct ButtonConfig {
|
||||||
#[validate(custom = "validate_unique_id")]
|
#[validate(custom = "crate::util::validate_hass_id")]
|
||||||
pub id: String,
|
pub id: String,
|
||||||
|
|
||||||
#[validate(length(min = 1))]
|
#[validate(length(min = 1))]
|
||||||
|
|
|
@ -63,12 +63,13 @@ impl InitializationContext {
|
||||||
pub struct ModuleContext {
|
pub struct ModuleContext {
|
||||||
pub config: super::config::Config,
|
pub config: super::config::Config,
|
||||||
pub mqtt: ModuleContextMqtt,
|
pub mqtt: ModuleContextMqtt,
|
||||||
|
pub machine_id: String,
|
||||||
// pub dbus_session_connection: zbus::Connection,
|
// pub dbus_session_connection: zbus::Connection,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ModuleContext {
|
impl ModuleContext {
|
||||||
fn get_entity_id(&self, module_id: &str, sub_id: &str) -> String {
|
fn get_entity_id(&self, module_id: &str, sub_id: &str) -> String {
|
||||||
format!("{}_{}_{}", self.config.unique_id, module_id, sub_id)
|
format!("{}_{}_{}", self.config.friendly_id, module_id, sub_id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -92,7 +92,7 @@ pub async fn init(context: &mut InitializationContext) -> Result<()> {
|
||||||
log::info!("Initializing…");
|
log::info!("Initializing…");
|
||||||
|
|
||||||
let full_context = context.get_full();
|
let full_context = context.get_full();
|
||||||
context.subscribe_mqtt_topic(format!("{}/{}/simple", context.full.config.unique_id, MODULE_ID), move |text| {
|
context.subscribe_mqtt_topic(format!("{}/{}/simple", context.full.config.friendly_id, MODULE_ID), move |text| {
|
||||||
let (summary_text, long_content) = text.split_once('\n').unwrap_or((text, ""));
|
let (summary_text, long_content) = text.split_once('\n').unwrap_or((text, ""));
|
||||||
|
|
||||||
tokio::spawn(handle_notification_message(
|
tokio::spawn(handle_notification_message(
|
||||||
|
@ -113,7 +113,7 @@ pub async fn init(context: &mut InitializationContext) -> Result<()> {
|
||||||
});
|
});
|
||||||
|
|
||||||
let full_context = context.get_full();
|
let full_context = context.get_full();
|
||||||
context.subscribe_mqtt_topic(format!("{}/{}/json", context.full.config.unique_id, MODULE_ID), move |text| {
|
context.subscribe_mqtt_topic(format!("{}/{}/json", context.full.config.friendly_id, MODULE_ID), move |text| {
|
||||||
let message = serde_json::from_str::<NotificationMessage>(text);
|
let message = serde_json::from_str::<NotificationMessage>(text);
|
||||||
|
|
||||||
match message {
|
match message {
|
||||||
|
@ -127,7 +127,7 @@ pub async fn init(context: &mut InitializationContext) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
|
|
||||||
context.subscribe_mqtt_topic(format!("{}/{}/close", context.full.config.unique_id, MODULE_ID), move |id| {
|
context.subscribe_mqtt_topic(format!("{}/{}/close", context.full.config.friendly_id, MODULE_ID), move |id| {
|
||||||
spawn_nonessential(close_notification(id.to_owned()));
|
spawn_nonessential(close_notification(id.to_owned()));
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -210,7 +210,7 @@ async fn send_notification_action_message(context: Arc<ModuleContext>, notificat
|
||||||
.mqtt
|
.mqtt
|
||||||
.client
|
.client
|
||||||
.publish(
|
.publish(
|
||||||
format!("{}/{}/action/{}", context.config.unique_id, MODULE_ID, notification_id),
|
format!("{}/{}/action/{}", context.config.friendly_id, MODULE_ID, notification_id),
|
||||||
QoS::ExactlyOnce,
|
QoS::ExactlyOnce,
|
||||||
false,
|
false,
|
||||||
action_id,
|
action_id,
|
||||||
|
|
26
src/mqtt.rs
26
src/mqtt.rs
|
@ -16,27 +16,27 @@ use crate::modules::InitializationContext;
|
||||||
|
|
||||||
use super::config;
|
use super::config;
|
||||||
|
|
||||||
pub async fn create_client(config: &config::Config, availability_topic: &str) -> Result<(MqttClient, EventLoop)> {
|
pub async fn create_client(config: &config::Config, machine_id: &str, availability_topic: &str) -> Result<(MqttClient, EventLoop)> {
|
||||||
let mut options = MqttOptions::new(&config.internal.stable_id, config.mqtt.host.to_owned(), config.mqtt.port);
|
let mut options = MqttOptions::new(machine_id, config.mqtt.host.to_owned(), config.mqtt.port);
|
||||||
options.set_clean_session(true);
|
options.set_clean_session(true);
|
||||||
options.set_keep_alive(Duration::from_secs(5));
|
options.set_keep_alive(Duration::from_secs(5));
|
||||||
options.set_last_will(LastWill::new(availability_topic, "offline", QoS::AtLeastOnce, true));
|
options.set_last_will(LastWill::new(availability_topic, "offline", QoS::AtLeastOnce, true));
|
||||||
|
|
||||||
options.set_max_packet_size(usize::MAX, usize::MAX);
|
options.set_inflight(30);
|
||||||
|
options.set_max_packet_size(
|
||||||
|
30 * 1000 * 1000, // 30 MB
|
||||||
|
usize::MAX,
|
||||||
|
);
|
||||||
|
|
||||||
let (mqtt_client, event_loop) = MqttClient::new(options, 10);
|
let (mqtt_client, event_loop) = MqttClient::new(options, 100);
|
||||||
mqtt_client.publish(availability_topic, QoS::AtLeastOnce, true, "online").await?;
|
mqtt_client.publish(availability_topic, QoS::AtLeastOnce, true, "online").await?;
|
||||||
|
|
||||||
Ok((mqtt_client, event_loop))
|
Ok((mqtt_client, event_loop))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_discovery_device_object(config: &config::Config) -> JsonValue {
|
pub fn create_discovery_device_object(config: &config::Config, machine_id: &str) -> JsonValue {
|
||||||
json::object! {
|
json::object! {
|
||||||
"connections": if config.announce_mac_address {
|
"identifiers": [machine_id],
|
||||||
mac_address::get_mac_address().unwrap_or(None).map(|a| json::array![["mac", a.to_string()]]).unwrap_or(json::array![])
|
|
||||||
} else {
|
|
||||||
json::array![]
|
|
||||||
},
|
|
||||||
"name": config.display_name.as_str()
|
"name": config.display_name.as_str()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -75,6 +75,8 @@ impl OwnedTopicsService {
|
||||||
mqtt_client.publish(topic, QoS::AtLeastOnce, true, Vec::new()).await?;
|
mqtt_client.publish(topic, QoS::AtLeastOnce, true, Vec::new()).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log::trace!("Writing owned_topics file");
|
||||||
|
|
||||||
let mut new_content = String::new();
|
let mut new_content = String::new();
|
||||||
new_content.push_str("# DO NOT EDIT THIS FILE. It is automatically generated at each run of Hassliebe.\n");
|
new_content.push_str("# DO NOT EDIT THIS FILE. It is automatically generated at each run of Hassliebe.\n");
|
||||||
|
|
||||||
|
@ -99,8 +101,8 @@ enum ConnectionState {
|
||||||
}
|
}
|
||||||
|
|
||||||
const FAST_RETRYING_INTERVAL_MS: u64 = 500;
|
const FAST_RETRYING_INTERVAL_MS: u64 = 500;
|
||||||
const FAST_RETRYING_LIMIT_SECONDS: u64 = 15;
|
const FAST_RETRYING_LIMIT_SECONDS: u64 = 10;
|
||||||
const SLOW_RETRYING_INTERVAL_SECONDS: u64 = 5;
|
const SLOW_RETRYING_INTERVAL_SECONDS: u64 = 10;
|
||||||
|
|
||||||
pub async fn start_communication(context: &InitializationContext, mut event_loop: EventLoop, owned_topics_service: OwnedTopicsService) -> Result<()> {
|
pub async fn start_communication(context: &InitializationContext, mut event_loop: EventLoop, owned_topics_service: OwnedTopicsService) -> Result<()> {
|
||||||
log::info!(
|
log::info!(
|
||||||
|
|
10
src/util.rs
10
src/util.rs
|
@ -5,7 +5,9 @@ use std::hash::{Hash, Hasher};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use rand::distributions::Alphanumeric;
|
use rand::distributions::Alphanumeric;
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
|
use regex::Regex;
|
||||||
use tokio::task::JoinHandle;
|
use tokio::task::JoinHandle;
|
||||||
|
use validator::ValidationError;
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn spawn_nonessential(future: impl Future<Output=Result<()>> + Send + 'static) -> JoinHandle<()> {
|
pub fn spawn_nonessential(future: impl Future<Output=Result<()>> + Send + 'static) -> JoinHandle<()> {
|
||||||
|
@ -28,6 +30,14 @@ pub fn log_error(error: anyhow::Error) {
|
||||||
log::error!("{:#}", error);
|
log::error!("{:#}", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn validate_hass_id(value: &str) -> Result<(), ValidationError> {
|
||||||
|
if Regex::new(r"^[a-zA-Z0-9]+(_[a-zA-Z0-9]+)*$").unwrap().is_match(value) {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(ValidationError::new("does not match regex"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn generate_alphanumeric_id(length: usize) -> String {
|
pub fn generate_alphanumeric_id(length: usize) -> String {
|
||||||
rand::thread_rng().sample_iter(&Alphanumeric).take(length).map(char::from).collect()
|
rand::thread_rng().sample_iter(&Alphanumeric).take(length).map(char::from).collect()
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue