Compare commits
No commits in common. "main" and "v1.0.1" have entirely different histories.
10 changed files with 677 additions and 1336 deletions
1887
Cargo.lock
generated
1887
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
31
Cargo.toml
31
Cargo.toml
|
@ -1,34 +1,35 @@
|
|||
[package]
|
||||
name = "hassliebe"
|
||||
version = "1.1.1"
|
||||
version = "1.0.1"
|
||||
edition = "2021"
|
||||
license = "BlueOak-1.0.0"
|
||||
authors = ["Moritz Ruth <dev@moritzruth.de>"]
|
||||
homepage = "https://git.moritzruth.de/moritzruth/Hassliebe"
|
||||
repository = "https://git.moritzruth.de/moritzruth/Hassliebe"
|
||||
|
||||
[features]
|
||||
dry_run = [] # will prevent some actions like shutting down
|
||||
dry_run = [] # will prevent some actions like shutting down
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.88"
|
||||
base64 = "0.22.1"
|
||||
env_logger = "0.11.5"
|
||||
anyhow = "1.0.69"
|
||||
base64 = "0.21.0"
|
||||
battery = "0.7.8"
|
||||
env_logger = "0.10.0"
|
||||
exitcode = "1.1.2"
|
||||
image = "0.25.2"
|
||||
image = "0.24.5"
|
||||
json = "0.12.4"
|
||||
lazy_static = "1.4.0"
|
||||
log = "0.4.17"
|
||||
notify-rust = { version = "4.11.3", features = ["images"] }
|
||||
notify-rust = { version = "4.8.0", features = ["images"] }
|
||||
rand = "0.8.5"
|
||||
regex = "1.7.1"
|
||||
rumqttc = "0.24.0"
|
||||
rumqttc = "0.20.0"
|
||||
serde = { version = "1.0.152", features = ["derive"] }
|
||||
serde_json = "1.0.128"
|
||||
sysinfo = { version = "0.31.4", default-features = false, features = ["system"] }
|
||||
serde_json = "1.0.93"
|
||||
sysinfo = { version = "0.28.2", default-features = false }
|
||||
tokio = { version = "1.25.0", features = ["full"] }
|
||||
toml = "0.8.19"
|
||||
toml = "0.7.2"
|
||||
users = "0.11.0"
|
||||
validator = { version = "0.18.1", features = ["derive"] }
|
||||
|
||||
[profile.release]
|
||||
strip = true # Automatically strip symbols from the binary.
|
||||
validator = { version = "0.16.0", features = ["derive"] }
|
||||
void = "1.0.2"
|
||||
zbus = { version = "3.10.0", default-features = false, features = ["tokio"] }
|
||||
|
|
36
README.md
36
README.md
|
@ -2,31 +2,27 @@
|
|||
|
||||
> Integrates any Linux machine into your Home Assistant ecosystem.
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
- [x] Command buttons
|
||||
- [x] Notifications
|
||||
- [x] Actions
|
||||
- [x] System information reporting (CPU usage, RAM usage, battery status)
|
||||
|
||||
The following sound like interesting ideas but as I don’t have a use case for them at the moment, I haven’t implemented them yet.
|
||||
Feel free to [open an issue and describe your use-case](https://git.moritzruth.de/moritzruth/Hassliebe/issues/new).
|
||||
|
||||
- [x] System information reporting (CPU usage, battery status, …)
|
||||
- [ ] Media control (MPRIS)
|
||||
- [ ] PipeWire control
|
||||
- [ ] Exposing file contents as sensors
|
||||
- [ ] File watcher
|
||||
|
||||
Ideas:
|
||||
|
||||
- Camera video stream
|
||||
- Idle time (→ libseat)
|
||||
|
||||
## Installation
|
||||
|
||||
Hassliebe is a [single executable](https://git.moritzruth.de/moritzruth/Hassliebe/releases). Copy it somewhere and execute it.
|
||||
|
||||
For systemd users, a unit file is provided.
|
||||
|
||||
### As a systemd _system_ service
|
||||
|
||||
- Download [the latest binary](https://git.moritzruth.de/moritzruth/Hassliebe/releases) and put it into `/usr/bin`.
|
||||
- Download [the unit file](contrib/systemd/system/hassliebe.service) and put it into `/etc/systemd/system`.
|
||||
- Download [the unit file](distrib/systemd/system/hassliebe.service) and put it into `/etc/systemd/system`.
|
||||
- Create the configuration file (see below) at `/etc/hassliebe/config.toml`.
|
||||
- Enable and start the unit:
|
||||
|
||||
|
@ -34,11 +30,10 @@ For systemd users, a unit file is provided.
|
|||
systemctl enable hassliebe && systemctl start hassliebe
|
||||
```
|
||||
|
||||
|
||||
### As a systemd _user_ service
|
||||
|
||||
- Download [the latest binary](https://git.moritzruth.de/moritzruth/Hassliebe/releases) and put it into `~/.local/bin`.
|
||||
- Download [the unit file](contrib/systemd/user/hassliebe.service) and put it into `~/.local/share/systemd/user`.
|
||||
- Download [the unit file](distrib/systemd/user/hassliebe.service) and put it into `~/.local/share/systemd/user`.
|
||||
- Create the configuration file (see below) at `$XDG_CONFIG_HOME/hassliebe/config.toml`.
|
||||
- Enable and start the unit:
|
||||
|
||||
|
@ -46,13 +41,11 @@ systemctl enable hassliebe && systemctl start hassliebe
|
|||
systemctl --user enable hassliebe && systemctl --user start hassliebe
|
||||
```
|
||||
|
||||
|
||||
## Configuration
|
||||
|
||||
Depending on whether Hassliebe is run as root or as a regular user, the configuration is read from
|
||||
`/etc/hassliebe/config.toml` or `$XDG_CONFIG_HOME/hassliebe/config.toml`.
|
||||
|
||||
|
||||
### Example
|
||||
|
||||
```toml
|
||||
|
@ -63,9 +56,6 @@ display_name = "My PC"
|
|||
host = "127.0.0.1" # You probably need to change this
|
||||
port = 1883
|
||||
|
||||
# You can remove the following line if your MQTT broker allows unauthenticated access
|
||||
credentials = { user = "user", password = "password" }
|
||||
|
||||
[modules.buttons]
|
||||
enabled = true
|
||||
|
||||
|
@ -84,7 +74,6 @@ battery = 60
|
|||
enabled = true
|
||||
```
|
||||
|
||||
|
||||
### Machine identification
|
||||
|
||||
Hassliebe needs a way to uniquely identify a machine.
|
||||
|
@ -94,7 +83,6 @@ If `/etc/machine-id` exists (as is the case with systemd-based systems), it will
|
|||
Otherwise, a random ID will be generated on the first run and stored in `[data]/machine_id`.
|
||||
The latter always takes precedence.
|
||||
|
||||
|
||||
## Modules
|
||||
|
||||
### Buttons
|
||||
|
@ -111,7 +99,6 @@ 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:
|
||||
|
@ -130,7 +117,6 @@ ram_usage = 0 # disabled
|
|||
battery = 60 # updates every 60 seconds
|
||||
```
|
||||
|
||||
|
||||
### Notifications
|
||||
|
||||
**Not available when running as a system service.**
|
||||
|
@ -166,7 +152,6 @@ Complex messages have these properties:
|
|||
- `encoded_image` (optional) — Padded Base64-encoded image attached to the notification.
|
||||
- `actions` (optional) — Object with the keys being IDs and the values being labels.
|
||||
|
||||
|
||||
#### Actions
|
||||
|
||||
When a notification action is invoked, the ID of the action is sent to the MQTT topic with the following name:
|
||||
|
@ -174,7 +159,6 @@ When a notification action is invoked, the ID of the action is sent to the MQTT
|
|||
|
||||
When a notification is dismissed, `closed` is sent into the topic.
|
||||
|
||||
|
||||
## License
|
||||
|
||||
This project is available under the permissive [Blue Oak Model License 1.0.0](https://blueoakcouncil.org/license/1.0.0).
|
||||
Hassliebe is licensed under the [Blue Oak Model License 1.0.0](./LICENSE.md).
|
|
@ -15,7 +15,7 @@ pub struct Mqtt {
|
|||
pub port: u16,
|
||||
|
||||
#[serde(default)]
|
||||
#[validate(nested)]
|
||||
#[validate]
|
||||
pub credentials: Option<MqttCredentials>,
|
||||
}
|
||||
|
||||
|
@ -33,16 +33,16 @@ pub struct Internal {
|
|||
|
||||
#[derive(Deserialize, Validate)]
|
||||
pub struct Config {
|
||||
#[validate(custom(function = "crate::util::validate_hass_id"))]
|
||||
#[validate(custom = "crate::util::validate_hass_id")]
|
||||
pub friendly_id: String,
|
||||
|
||||
#[validate(length(min = 1))]
|
||||
pub display_name: String,
|
||||
|
||||
#[validate(nested)]
|
||||
#[validate]
|
||||
pub mqtt: Mqtt,
|
||||
|
||||
#[validate(nested)]
|
||||
#[validate]
|
||||
pub modules: modules::Config,
|
||||
}
|
||||
|
||||
|
@ -55,7 +55,7 @@ pub fn load(config_file_path: &Path) -> Result<Option<Config>> {
|
|||
file.read_to_string(&mut string_content).context("while reading the configuration file")?;
|
||||
|
||||
let parsed = toml::from_str::<Config>(string_content.as_str()).context("while parsing the configuration file")?;
|
||||
Validate::validate(&parsed).context("while validating the configuration file")?;
|
||||
parsed.validate().context("while validating the configuration file")?;
|
||||
|
||||
Ok(Some(parsed))
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ const BUTTON_TRIGGER_TEXT: &str = "press";
|
|||
|
||||
#[derive(Deserialize, Validate, Clone)]
|
||||
pub struct ButtonConfig {
|
||||
#[validate(custom(function = "crate::util::validate_hass_id"))]
|
||||
#[validate(custom = "crate::util::validate_hass_id")]
|
||||
pub id: String,
|
||||
|
||||
#[validate(length(min = 1))]
|
||||
|
|
|
@ -3,10 +3,10 @@ use std::time::Duration;
|
|||
use anyhow::{anyhow, Result};
|
||||
use rumqttc::QoS;
|
||||
use serde::Deserialize;
|
||||
use sysinfo::{CpuExt, System, SystemExt};
|
||||
use tokio::task::spawn_blocking;
|
||||
use tokio::time::MissedTickBehavior;
|
||||
use validator::Validate;
|
||||
use sysinfo::System;
|
||||
|
||||
use crate::modules::InitializationContext;
|
||||
|
||||
|
@ -43,16 +43,14 @@ pub async fn init(context: &mut InitializationContext) -> Result<()> {
|
|||
sub_id: "ram_usage",
|
||||
display_name: "RAM Usage",
|
||||
icon: "mdi:memory",
|
||||
suggested_precision: Some(0),
|
||||
suggested_precision: 0,
|
||||
unit: Some("%"),
|
||||
force_update: true,
|
||||
device_class: None
|
||||
},
|
||||
context,
|
||||
config.ram_usage,
|
||||
move || {
|
||||
sys.refresh_memory();
|
||||
Ok(format!("{:.2}", (sys.used_memory() as f64) / (sys.total_memory() as f64) * 100.0))
|
||||
Ok(format!("{:.2}", (sys.available_memory() as f64) / (sys.total_memory() as f64) * 100.0))
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
@ -65,16 +63,14 @@ pub async fn init(context: &mut InitializationContext) -> Result<()> {
|
|||
sub_id: "cpu_usage",
|
||||
display_name: "CPU Usage",
|
||||
icon: "mdi:memory",
|
||||
suggested_precision: Some(0),
|
||||
suggested_precision: 0,
|
||||
unit: Some("%"),
|
||||
force_update: true,
|
||||
device_class: None
|
||||
},
|
||||
context,
|
||||
config.cpu_usage,
|
||||
move || {
|
||||
sys.refresh_cpu_usage();
|
||||
Ok(format!("{:.2}", sys.global_cpu_usage()))
|
||||
sys.refresh_cpu();
|
||||
Ok(format!("{:.2}", sys.global_cpu_info().cpu_usage()))
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
@ -90,7 +86,8 @@ pub async fn init(context: &mut InitializationContext) -> Result<()> {
|
|||
})
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.await?;
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
if let Some(dir) = battery_dirs.first() {
|
||||
log::debug!(
|
||||
|
@ -109,10 +106,8 @@ pub async fn init(context: &mut InitializationContext) -> Result<()> {
|
|||
sub_id: "battery_level",
|
||||
display_name: "Battery Level",
|
||||
icon: "mdi:battery",
|
||||
suggested_precision: Some(0),
|
||||
suggested_precision: 0,
|
||||
unit: Some("%"),
|
||||
force_update: true,
|
||||
device_class: Some("battery")
|
||||
},
|
||||
context,
|
||||
config.battery,
|
||||
|
@ -125,10 +120,8 @@ pub async fn init(context: &mut InitializationContext) -> Result<()> {
|
|||
sub_id: "battery_state",
|
||||
display_name: "Battery State",
|
||||
icon: "mdi:battery",
|
||||
suggested_precision: None,
|
||||
suggested_precision: 0,
|
||||
unit: None,
|
||||
force_update: false,
|
||||
device_class: Some("enum")
|
||||
},
|
||||
context,
|
||||
config.battery,
|
||||
|
@ -147,10 +140,8 @@ struct InfoEntityOptions<'a> {
|
|||
sub_id: &'a str,
|
||||
display_name: &'a str,
|
||||
icon: &'a str,
|
||||
suggested_precision: Option<u64>,
|
||||
suggested_precision: u64,
|
||||
unit: Option<&'a str>,
|
||||
force_update: bool,
|
||||
device_class: Option<&'a str>
|
||||
}
|
||||
|
||||
async fn init_info_value(
|
||||
|
@ -176,10 +167,10 @@ async fn init_info_value(
|
|||
let mut object = json::object! {
|
||||
"availability_topic": context.full.mqtt.availability_topic.clone(),
|
||||
"device": context.full.mqtt.discovery_device_object.clone(),
|
||||
"device_class": options.device_class,
|
||||
"force_update": options.force_update,
|
||||
"force_update": true,
|
||||
"icon": options.icon,
|
||||
"name": options.display_name,
|
||||
"suggested_display_precision": options.suggested_precision,
|
||||
"state_class": "measurement",
|
||||
"state_topic": state_topic.as_str(),
|
||||
"object_id": entity_id.as_str(),
|
||||
|
@ -190,10 +181,6 @@ async fn init_info_value(
|
|||
object.insert("unit_of_measurement", unit).unwrap();
|
||||
}
|
||||
|
||||
if let Some(suggested_precision) = options.suggested_precision {
|
||||
object.insert("suggested_display_precision", suggested_precision).unwrap();
|
||||
}
|
||||
|
||||
object
|
||||
}),
|
||||
)
|
||||
|
|
|
@ -13,13 +13,13 @@ mod notifications;
|
|||
|
||||
#[derive(Deserialize, Validate, Default)]
|
||||
pub struct Config {
|
||||
#[validate(nested)]
|
||||
#[validate]
|
||||
pub buttons: Option<buttons::Config>,
|
||||
|
||||
#[validate(nested)]
|
||||
#[validate]
|
||||
pub info: Option<info::Config>,
|
||||
|
||||
#[validate(nested)]
|
||||
#[validate]
|
||||
pub notifications: Option<notifications::Config>,
|
||||
}
|
||||
|
||||
|
|
|
@ -28,8 +28,6 @@ pub async fn create_client(config: &config::Config, machine_id: &str, availabili
|
|||
usize::MAX,
|
||||
);
|
||||
|
||||
config.mqtt.credentials.as_ref().map(|c| options.set_credentials(c.user.clone(), c.password.clone()));
|
||||
|
||||
let (mqtt_client, event_loop) = MqttClient::new(options, 30);
|
||||
Ok((mqtt_client, event_loop))
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue