diff --git a/src/mqtt.rs b/src/mqtt.rs index e332a2e..54d29b1 100644 --- a/src/mqtt.rs +++ b/src/mqtt.rs @@ -15,16 +15,26 @@ use tracing::{debug, error, info, trace, warn}; use crate::config::Configuration; use crate::hass::{self, HassConfig}; +/// Callback methods invoked to hanle incoming MQTT messages #[async_trait] pub trait MessageHandler { + /// Navigate to the specified URL async fn navigate(&mut self, publisher: &MqttPublisher, msg: &Message); + /// Turn on or off the display(s) async fn power(&mut self, publisher: &MqttPublisher, msg: &Message); + /// Refresh the current page + async fn refresh(&mut self, publisher: &MqttPublisher, msg: &Message); } +/// MQTT message types #[derive(Debug)] pub enum MessageType { + /// Navigate to the specified URL Navigate, + /// Turn on or off the display(s) PowerState, + /// Refresh the current page + Refresh, } pub struct MqttClient<'a> { @@ -72,10 +82,13 @@ impl<'a> MqttClient<'a> { let client = self.client.lock().await; let prefix = &self.config.mqtt.topic_prefix; let t_nav = format!("{}/+/navigate", prefix); + let t_ref = format!("{}/+/refresh", prefix); let t_power = format!("{}/power", prefix); client.subscribe(&t_nav, 0).await?; + client.subscribe(&t_ref, 0).await?; client.subscribe(&t_power, 0).await?; self.topics.insert(t_nav, MessageType::Navigate); + self.topics.insert(t_ref, MessageType::Refresh); self.topics.insert(t_power, MessageType::PowerState); Ok(()) } @@ -125,6 +138,9 @@ impl<'a> MqttClient<'a> { MessageType::PowerState => { handler.power(&publisher, &msg).await; } + MessageType::Refresh => { + handler.refresh(&publisher, &msg).await; + } } } } diff --git a/src/session.rs b/src/session.rs index b8ccbda..04bb9f2 100644 --- a/src/session.rs +++ b/src/session.rs @@ -181,38 +181,36 @@ pub struct MessageHandler<'a> { windows: HashMap, } -#[async_trait::async_trait] -impl<'a> crate::mqtt::MessageHandler for MessageHandler<'a> { - async fn navigate(&mut self, publisher: &MqttPublisher, msg: &Message) { - let url = msg.payload_str(); - let parts: Vec<&str> = msg.topic().split('/').rev().collect(); - let screen = match parts.get(1) { - Some(&"") | None => { - warn!("Invalid navigate request: no screen"); - return; - } - Some(s) => s, +impl<'a> MessageHandler<'a> { + /// Switch to the window shown on the specified screen + /// + /// This method switches to the active window of the Marionette + /// session to the one displayed on the specified screen. It must + /// be called before performing any screen-specific operation, + /// such as navigating to a URL or refreshing the page. + async fn switch_window(&mut self, screen: &str) -> Result<(), String> { + let Some(window) = self.windows.get(screen) else { + return Err(format!("Unknown screen {}", screen)); }; - if let Some(window) = self.windows.get(*screen) { - debug!("Switching to window {}", window); - if let Err(e) = - self.marionette.switch_to_window(window.into(), false).await - { - error!( - "Failed to switch to window on screen {}: {}", - screen, e - ); - return; - } - } else { - error!("Invalid navigate request: unknown screen {}", screen); - return; - } - debug!("Handling navigate request: {}", url); - info!("Navigate screen {} to {}", screen, url); - if let Err(e) = self.marionette.navigate(url.to_string()).await { - error!("Failed to navigate: {}", e); + debug!("Switching to window {}", window); + if let Err(e) = + self.marionette.switch_to_window(window.into(), false).await + { + return Err(e.to_string()); } + Ok(()) + } + + /// Publish the current page info via MQTT + /// + /// This method retrieves the current title and URL of the page + /// displayed in the window visible on the specified screen, then + /// publishes the values to their respective MQTT topics. + async fn publish_info( + &mut self, + screen: &str, + publisher: &MqttPublisher<'_>, + ) { match self.marionette.get_current_url().await { Ok(u) => { if let Err(e) = publisher.publish_url(screen, &u).await { @@ -230,6 +228,27 @@ impl<'a> crate::mqtt::MessageHandler for MessageHandler<'a> { Err(e) => error!("Error getting title: {}", e), } } +} + +#[async_trait::async_trait] +impl<'a> crate::mqtt::MessageHandler for MessageHandler<'a> { + async fn navigate(&mut self, publisher: &MqttPublisher, msg: &Message) { + let url = msg.payload_str(); + let Some(screen) = screen_from_topic(msg.topic()) else { + warn!("Invalid navigate request: no screen"); + return; + }; + if let Err(e) = self.switch_window(screen).await { + error!("Failed to switch to window on screen {}: {}", screen, e); + return; + } + debug!("Handling navigate request: {}", url); + info!("Navigate screen {} to {}", screen, url); + if let Err(e) = self.marionette.navigate(url.to_string()).await { + error!("Failed to navigate: {}", e); + } + self.publish_info(screen, publisher).await; + } async fn power(&mut self, publisher: &MqttPublisher, msg: &Message) { match msg.payload_str().as_ref() { @@ -243,6 +262,30 @@ impl<'a> crate::mqtt::MessageHandler for MessageHandler<'a> { error!("Failed to publish power state: {}", e); } } + + /// Refresh the page currently displayd on the specified screen + /// + /// This method is called by the MQTT receiver when a + /// [`crate::mqtt::MessageType::Refresh`] message is received from + /// the broker. It calls `WebDriver:Refresh` for the window + /// displayed on the screen specified in the message topic, then + /// publishes the page URL and title, after the page load completes. + async fn refresh(&mut self, publisher: &MqttPublisher, msg: &Message) { + let Some(screen) = screen_from_topic(msg.topic()) else { + warn!("Invalid refresh request: no screen"); + return; + }; + if let Err(e) = self.switch_window(screen).await { + error!("Failed to switch to window on screen {}: {}", screen, e); + return; + } + info!("Refreshing screen: {}", screen); + if let Err(e) = self.marionette.refresh().await { + error!("Failed to refresh page: {}", e); + return; + } + self.publish_info(screen, publisher).await; + } } #[cfg(unix)] @@ -272,10 +315,7 @@ fn turn_screen_on() { error!("Failed to turn on display \"{}\"", Display::name()); } if !dpms::disable(&display) { - error!( - "Failed to disable DPMS on display \"{}\"", - Display::name() - ); + error!("Failed to disable DPMS on display \"{}\"", Display::name()); } } @@ -289,10 +329,7 @@ fn turn_screen_off() { } }; if !dpms::enable(&display) { - error!( - "Failed to enable DPMS on display \"{}\"", - Display::name() - ); + error!("Failed to enable DPMS on display \"{}\"", Display::name()); } if !dpms::force_level(&display, dpms::DpmsPowerLevel::Off) { error!("Failed to turn off display \"{}\"", Display::name()); @@ -316,3 +353,12 @@ fn is_screen_on() -> bool { } true } + +/// Extract the screen name from the topic +/// +/// The screen name is the penultimate segment of the topic path, e.g. +/// .../HDMI-1/navigate (HDMI-1) or .../eDP-1/refresh (eDP-1). +fn screen_from_topic(topic: &str) -> Option<&str> { + let topic = topic.get(..topic.rfind('/')?)?; + topic.get(topic.rfind('/')? + 1..) +}