diff --git a/Cargo.lock b/Cargo.lock index 67162b2..a25ddfc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aho-corasick" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +dependencies = [ + "memchr", +] + [[package]] name = "async-channel" version = "1.8.0" @@ -244,6 +253,17 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hostname" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +dependencies = [ + "libc", + "match_cfg", + "winapi", +] + [[package]] name = "indexmap" version = "1.9.2" @@ -321,6 +341,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "match_cfg" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" + [[package]] name = "matchers" version = "0.1.0" @@ -375,10 +401,12 @@ name = "mqttmarionette" version = "0.1.0" dependencies = [ "async-trait", + "hostname", "inotify", "mozprofile", "mozrunner", "paho-mqtt", + "regex", "serde", "serde_json", "tokio", @@ -535,6 +563,8 @@ version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a" dependencies = [ + "aho-corasick", + "memchr", "regex-syntax", ] diff --git a/Cargo.toml b/Cargo.toml index fa4fadd..624a23f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,10 +5,12 @@ edition = "2021" [dependencies] async-trait = "0.1.60" +hostname = "0.3.1" inotify = "0.10.0" mozprofile = "0.9.0" mozrunner = "0.15.0" paho-mqtt = "0.11.1" +regex = "1.7.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" tokio = { version = "1.23.0", features = ["io-util", "macros", "net", "rt", "signal", "sync", "time"] } diff --git a/src/config.rs b/src/config.rs index 1a18303..72f206d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,6 +3,8 @@ use std::path::{Path, PathBuf}; use serde::Deserialize; +use crate::util; + #[derive(Debug)] pub enum ConfigError { Io(std::io::Error), @@ -73,6 +75,8 @@ impl Default for MqttConfig { #[derive(Debug, Default, Deserialize)] pub struct Configuration { + #[serde(default = "util::hostname")] + pub unique_id: String, pub mqtt: MqttConfig, } diff --git a/src/hass.rs b/src/hass.rs new file mode 100644 index 0000000..368d56c --- /dev/null +++ b/src/hass.rs @@ -0,0 +1,46 @@ +use regex::Regex; +use serde::Serialize; + +use crate::util; + +#[derive(Serialize)] +pub struct HassDevice { + pub identifiers: Vec, + pub manufacturer: String, + pub model: String, + pub name: String, + pub sw_version: String, +} + +impl Default for HassDevice { + fn default() -> Self { + Self { + identifiers: vec![util::hostname()], + manufacturer: "Dustin C. Hatch".into(), + model: env!("CARGO_PKG_VERSION").into(), + name: "Browser HUD".into(), + sw_version: env!("CARGO_PKG_VERSION").into(), + } + } +} + +#[derive(Serialize)] +pub struct HassConfig { + pub availability_topic: String, + pub command_topic: Option, + pub device: HassDevice, + pub name: String, + pub state_topic: String, + pub unique_id: String, + pub object_id: String, + pub icon: String, + pub retain: Option, +} + +pub fn slugify(s: impl AsRef) -> String { + let re = Regex::new("[^a-z0-9_]").unwrap(); + let s = s.as_ref().to_ascii_lowercase(); + let s = re.replace_all(&s, "_"); + let re = Regex::new("_+").unwrap(); + re.replace_all(s.trim_matches('_'), "_").into() +} diff --git a/src/main.rs b/src/main.rs index f5f5472..a5c877f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,11 @@ mod browser; mod config; +mod hass; mod marionette; mod monitor; mod mqtt; mod session; +mod util; #[cfg(unix)] mod x11; diff --git a/src/mqtt.rs b/src/mqtt.rs index bc16459..0bb5013 100644 --- a/src/mqtt.rs +++ b/src/mqtt.rs @@ -8,17 +8,14 @@ use paho_mqtt::{ CreateOptionsBuilder, ServerResponse, SslOptionsBuilder, }; use tokio_stream::StreamExt; -use tracing::{info, trace}; +use tracing::{debug, info, trace}; use crate::config::Configuration; +use crate::hass::{self, HassConfig}; #[async_trait] pub trait MessageHandler { - async fn navigate( - &mut self, - publisher: &MqttPublisher, - msg: &Message, - ); + async fn navigate(&mut self, publisher: &MqttPublisher, msg: &Message); } #[derive(Debug)] @@ -42,8 +39,10 @@ impl<'a> MqttClient<'a> { config.mqtt.port ); info!("Connecting to MQTT server {}", uri); - let client_opts = - CreateOptionsBuilder::new().server_uri(uri).finalize(); + let client_opts = CreateOptionsBuilder::new() + .client_id(&config.unique_id) + .server_uri(uri) + .finalize(); let mut client = AsyncClient::new(client_opts)?; let stream = client.get_stream(10); let topics = TopicMatcher::new(); @@ -56,7 +55,9 @@ impl<'a> MqttClient<'a> { } pub async fn connect(&mut self) -> Result { - let res = self.client.connect(self.conn_opts()?).await?; + let opts = self.conn_opts()?; + trace!("Connect options: {:?}", opts); + let res = self.client.connect(opts).await?; info!("Successfully connected to MQTT broker"); Ok(res) } @@ -69,6 +70,13 @@ impl<'a> MqttClient<'a> { Ok(res) } + pub fn publisher(&mut self) -> MqttPublisher { + MqttPublisher { + config: self.config, + client: &mut self.client, + } + } + pub async fn run(mut self, mut handler: H) where H: MessageHandler, @@ -92,6 +100,11 @@ impl<'a> MqttClient<'a> { fn conn_opts(&self) -> Result { let mut conn_opts = ConnectOptionsBuilder::new(); + conn_opts.will_message(Message::new_retained( + format!("{}/available", self.config.mqtt.topic_prefix), + "offline", + 0, + )); conn_opts.automatic_reconnect( Duration::from_millis(500), Duration::from_secs(30), @@ -140,4 +153,65 @@ impl<'a> MqttPublisher<'a> { self.client.publish(msg).await?; Ok(()) } + + pub async fn publish_config(&self, screen: &str) -> Result<(), Error> { + debug!("Publishing Home Assistant configuration"); + let prefix = &self.config.mqtt.topic_prefix; + + let availability_topic = format!("{}/available", prefix); + let command_topic = Some(format!("{}/{}/navigate", prefix, screen)); + let name = format!("{} URL", screen); + let state_topic = format!("{}/{}/url", prefix, screen); + let key = format!( + "browserhud_{}_{}", + self.config.unique_id, + hass::slugify(screen) + ); + let unique_id = format!("text.{}_url", key); + let object_id = unique_id.clone(); + let msg = Message::new_retained(&availability_topic, "online", 0); + trace!("Publishing message: {:?}", msg); + self.client.publish(msg).await?; + let config = HassConfig { + availability_topic, + command_topic, + name, + state_topic, + unique_id, + object_id, + device: Default::default(), + icon: "mdi:monitor".into(), + retain: Some(true), + }; + let msg = Message::new_retained( + format!("homeassistant/text/{}_url/config", key), + serde_json::to_string(&config).unwrap(), + 0, + ); + trace!("Publishing message: {:?}", msg); + self.client.publish(msg).await?; + + let unique_id = format!("sensor.{}_title", key); + let object_id = unique_id.clone(); + let config = HassConfig { + command_topic: None, + state_topic: format!("{}/{}/title", prefix, screen), + name: format!("{} Title", screen), + unique_id, + object_id, + retain: None, + ..config + }; + let msg = Message::new_retained( + format!("homeassistant/sensor/{}_title/config", key), + serde_json::to_string(&config).unwrap(), + 0, + ); + + trace!("Publishing message: {:?}", msg); + self.client.publish(msg).await?; + + info!("Succesfully published Home Assistant config"); + Ok(()) + } } diff --git a/src/session.rs b/src/session.rs index 9b87470..eae20f2 100644 --- a/src/session.rs +++ b/src/session.rs @@ -122,6 +122,16 @@ impl Session { break; } + let publisher = client.publisher(); + for w in windows.keys() { + if let Err(e) = publisher.publish_config(w).await { + error!( + "Failed to publish Home Assistant config for screen {}: {}", + w, e + ); + } + } + let handler = MessageHandler { marionette: &mut self.marionette, windows, diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..575544b --- /dev/null +++ b/src/util.rs @@ -0,0 +1,10 @@ +pub fn hostname() -> String { + if let Ok(h) = hostname::get() { + if let Some(h) = h.to_str() { + if let Some((h, _)) = h.split_once('.') { + return h.into(); + }; + }; + }; + "localhost".into() +}