Skip to content

Commit

Permalink
feat(custom_commands): add support for custom commands exposed as but…
Browse files Browse the repository at this point in the history
…tons to home assistant
  • Loading branch information
maxjoehnk committed Sep 27, 2022
1 parent 85b0863 commit ef79125
Show file tree
Hide file tree
Showing 8 changed files with 283 additions and 21 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ repository = "https://github.com/maxjoehnk/desktop2mqtt"
[dependencies]
log = "0.4"
env_logger = "0.8"
tokio = { version = "1", features = ["fs", "rt-multi-thread", "sync", "signal", "time"] }
tokio = { version = "1", features = ["fs", "rt-multi-thread", "sync", "signal", "time", "process"] }
mqtt-async-client = "0.2"
user-idle = "0.4"
serde = { version = "1", features = ["derive"] }
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,8 @@ modules:
- /
- /mnt/games
- type: battery
custom_commands: # exposed as buttons to home assistant
- name: Disable HDMI
command: xrandr --output HDMI-0 --off
icon: mdi:television-off # optional
```
19 changes: 18 additions & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ pub struct Config {
pub mqtt: MqttConfig,
pub hass: HomeAssistantConfig,
#[serde(default)]
pub modules: Modules
pub modules: Modules,
}

#[derive(Default, Debug, Clone, Deserialize)]
Expand All @@ -25,6 +25,8 @@ pub struct Modules {
// TODO: add configuration options for icon and app name
pub notifications: Option<bool>,
#[serde(default)]
pub custom_commands: Vec<CustomCommandConfig>,
#[serde(default)]
pub sensors: SensorsConfig,
}

Expand Down Expand Up @@ -61,6 +63,21 @@ pub struct IdleModuleConfig {
pub poll_rate: Duration,
}

#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
pub struct CustomCommandConfig {
pub name: String,
pub command: String,
pub icon: Option<String>,
pub button_type: Option<ButtonType>,
}

#[derive(Debug, Copy, Clone, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum ButtonType {
Restart,
Update,
}

#[derive(Debug, Clone, Deserialize)]
pub struct MqttConfig {
pub url: String,
Expand Down
146 changes: 131 additions & 15 deletions src/core/home_assistant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ use futures_util::FutureExt;
use serde::Serialize;
use tokio::sync::mpsc::UnboundedSender;

use crate::config::{Config, HomeAssistantConfig, SensorType};
use crate::config::{Config, CustomCommandConfig, HomeAssistantConfig, SensorType};
use crate::core::mqtt::MqttCommand;
use crate::core::worker::Worker;
use crate::modules::{SensorsModule, SensorClass};
use crate::modules::{ButtonClass, CustomCommandsModule, SensorClass, SensorsModule};

pub struct HomeAssistantWorker {
mqtt_sender: UnboundedSender<MqttCommand>,
Expand All @@ -24,13 +24,31 @@ impl Worker for HomeAssistantWorker {
async move {
if let Some(idle) = modules_config.idle {
let expire_after = idle.poll_rate * 2;
self.announce_occupancy(&hass_config, topic.clone(), device.clone(), expire_after.as_secs())?;
self.announce_occupancy(
&hass_config,
topic.clone(),
device.clone(),
expire_after.as_secs(),
)?;
}
if modules_config.backlight.is_some() {
self.announce_backlight(&hass_config, topic.clone(), device.clone())?;
}
if modules_config.sensors.types.len() > 0 {
self.announce_sensors(&hass_config, topic.clone(), device.clone(), &modules_config.sensors.types)?;
self.announce_sensors(
&hass_config,
topic.clone(),
device.clone(),
&modules_config.sensors.types,
)?;
}
if modules_config.custom_commands.len() > 0 {
self.announce_custom_commands(
&hass_config,
topic.clone(),
device.clone(),
&modules_config.custom_commands,
)?;
}

Ok(())
Expand All @@ -56,8 +74,9 @@ impl HomeAssistantWorker {
format!("{} Backlight", &config.name),
format!("{}_backlight_desktop2mqtt", config.entity_id),
device,
topic,
topic.clone(),
LightConfig {
state_topic: topic,
command_topic: command_topic.clone(),
brightness: true,
schema: "json".to_string(),
Expand Down Expand Up @@ -87,8 +106,9 @@ impl HomeAssistantWorker {
format!("{} Occupancy", &config.name),
format!("{}_occupancy_desktop2mqtt", config.entity_id),
device,
topic,
topic.clone(),
BinarySensorConfig {
state_topic: topic,
device_class: "occupancy".to_string().into(),
value_template: "{{ value_json.occupancy }}".to_string(),
expire_after: Some(expire_after),
Expand All @@ -107,25 +127,61 @@ impl HomeAssistantWorker {
config: &HomeAssistantConfig,
topic: String,
device: Device,
enabled_sensors: &[SensorType]
enabled_sensors: &[SensorType],
) -> anyhow::Result<()> {
for sensor in SensorsModule::get_sensors(enabled_sensors)? {
let config_topic = format!("homeassistant/sensor/{}/{}/config", config.entity_id, sensor.id);
let config_topic = format!(
"homeassistant/sensor/{}/{}/config",
config.entity_id, sensor.id
);
let msg = ConfigMessage::sensor(
format!("{} {}", &config.name, sensor.name),
format!("{}_{}_desktop2mqtt", config.entity_id, sensor.id),
device.clone(),
topic.clone(),
SensorConfig {
state_topic: topic.clone(),
device_class: sensor.class.to_hass_class(),
value_template: format!("{{{{ value_json.sensors.{} }}}}", sensor.id),
unit_of_measurement: sensor.class.to_unit(),
icon: sensor.icon,
..Default::default()
}
},
);

self.mqtt_sender
.send(MqttCommand::new_json(config_topic, &msg)?)?;
}
Ok(())
}

fn announce_custom_commands(
&self,
config: &HomeAssistantConfig,
topic: String,
device: Device,
custom_commands: &[CustomCommandConfig],
) -> anyhow::Result<()> {
for command in CustomCommandsModule::get_commands(&config.entity_id, custom_commands) {
let config_topic = format!(
"homeassistant/button/{}/{}/config",
config.entity_id, command.id
);
let msg = ConfigMessage::button(
format!("{} {}", &config.name, command.name),
format!("{}_{}_desktop2mqtt", config.entity_id, command.id),
device.clone(),
topic.clone(),
ButtonConfig {
device_class: command.class.to_hass_class(),
icon: command.icon,
command_topic: command.topic,
..Default::default()
},
);

self.mqtt_sender.send(MqttCommand::new_json(config_topic, &msg)?)?;
self.mqtt_sender
.send(MqttCommand::new_json(config_topic, &msg)?)?;
}
Ok(())
}
Expand Down Expand Up @@ -155,12 +211,25 @@ impl ToHassClass for SensorClass {
}
}

impl ToHassClass for ButtonClass {
fn to_hass_class(&self) -> Option<String> {
match self {
Self::Generic => None,
Self::Restart => Some("restart".to_string()),
Self::Update => Some("update".to_string()),
}
}

fn to_unit(&self) -> Option<String> {
None
}
}

#[derive(Debug, Clone, Serialize)]
pub struct ConfigMessage {
pub availability_topic: String,
pub name: String,
pub unique_id: String,
pub state_topic: String,
pub device: Device,
pub json_attributes_topic: String,
#[serde(flatten)]
Expand All @@ -169,6 +238,8 @@ pub struct ConfigMessage {
pub sensor: Option<SensorConfig>,
#[serde(flatten)]
pub light: Option<LightConfig>,
#[serde(flatten)]
pub button: Option<ButtonConfig>,
}

impl ConfigMessage {
Expand All @@ -184,11 +255,11 @@ impl ConfigMessage {
name,
unique_id: id,
device,
state_topic: topic.clone(),
json_attributes_topic: topic,
binary_sensor: Some(config),
sensor: None,
light: None,
button: None,
}
}

Expand All @@ -204,11 +275,11 @@ impl ConfigMessage {
name,
unique_id: id,
device,
state_topic: topic.clone(),
json_attributes_topic: topic,
binary_sensor: None,
sensor: Some(config),
light: None,
button: None,
}
}

Expand All @@ -218,17 +289,38 @@ impl ConfigMessage {
name,
unique_id: id,
device,
state_topic: topic.clone(),
json_attributes_topic: topic,
binary_sensor: None,
sensor: None,
light: Some(config),
button: None,
}
}

fn button(
name: String,
id: String,
device: Device,
topic: String,
config: ButtonConfig,
) -> Self {
ConfigMessage {
availability_topic: format!("{}/availability", topic),
name,
unique_id: id,
device,
json_attributes_topic: topic,
binary_sensor: None,
button: Some(config),
light: None,
sensor: None,
}
}
}

#[derive(Debug, Clone, Serialize)]
pub struct BinarySensorConfig {
pub state_topic: String,
pub device_class: Option<String>,
pub value_template: String,
pub payload_off: bool,
Expand All @@ -240,6 +332,7 @@ pub struct BinarySensorConfig {
impl Default for BinarySensorConfig {
fn default() -> Self {
BinarySensorConfig {
state_topic: Default::default(),
device_class: None,
value_template: String::new(),
payload_on: true,
Expand All @@ -251,6 +344,7 @@ impl Default for BinarySensorConfig {

#[derive(Debug, Clone, Serialize)]
pub struct SensorConfig {
pub state_topic: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub device_class: Option<String>,
pub value_template: String,
Expand All @@ -260,12 +354,13 @@ pub struct SensorConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub unit_of_measurement: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub icon: Option<String>
pub icon: Option<String>,
}

impl Default for SensorConfig {
fn default() -> Self {
SensorConfig {
state_topic: Default::default(),
device_class: None,
value_template: String::new(),
expire_after: None,
Expand All @@ -277,11 +372,32 @@ impl Default for SensorConfig {

#[derive(Debug, Clone, Serialize)]
pub struct LightConfig {
pub state_topic: String,
pub command_topic: String,
pub brightness: bool,
pub schema: String,
}

#[derive(Debug, Clone, Serialize)]
pub struct ButtonConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub device_class: Option<String>,
/// Defines the number of seconds after the sensor’s state expires, if it’s not updated.
#[serde(skip_serializing_if = "Option::is_none")]
pub icon: Option<String>,
pub command_topic: String,
}

impl Default for ButtonConfig {
fn default() -> Self {
Self {
device_class: None,
icon: None,
command_topic: Default::default(),
}
}
}

#[derive(Debug, Clone, Serialize)]
pub struct Device {
pub identifiers: String,
Expand Down
9 changes: 9 additions & 0 deletions src/extensions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
pub trait StringExt {
fn to_slug(&self) -> String;
}

impl StringExt for String {
fn to_slug(&self) -> String {
self.to_lowercase().replace(" ", "-")
}
}
Loading

0 comments on commit ef79125

Please sign in to comment.