From 28c0944130f970559e41ac87985d4e55c4c0aa31 Mon Sep 17 00:00:00 2001 From: "Dustin C. Hatch" Date: Mon, 9 Jan 2023 11:39:52 -0600 Subject: [PATCH] Add DPMS on/off support We now manage a switch entity in Home Assistant that can be used to turn the display on or off. On Linux, this is handled by the X DPMS extension; presumably there is similar functionality on other platforms that we can use if we decide to support those as well. --- Cargo.toml | 2 +- src/mqtt.rs | 48 ++++++++++++++++--- src/session.rs | 72 ++++++++++++++++++++++++++++- src/x11/dpms.rs | 120 ++++++++++++++++++++++++++++++++++++++++++++++++ src/x11/mod.rs | 3 ++ 5 files changed, 237 insertions(+), 8 deletions(-) create mode 100644 src/x11/dpms.rs diff --git a/Cargo.toml b/Cargo.toml index 624a23f..3002d94 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,4 +18,4 @@ tokio-stream = "0.1.11" toml = "0.5.10" tracing = "0.1.37" tracing-subscriber = { version = "0.3.16", features = ["env-filter", "fmt"] } -x11 = { version = "2.20.1", features = ["xlib", "xrandr"] } +x11 = { version = "2.20.1", features = ["dpms", "xlib", "xrandr"] } diff --git a/src/mqtt.rs b/src/mqtt.rs index ab57ac6..e332a2e 100644 --- a/src/mqtt.rs +++ b/src/mqtt.rs @@ -18,11 +18,13 @@ use crate::hass::{self, HassConfig}; #[async_trait] pub trait MessageHandler { async fn navigate(&mut self, publisher: &MqttPublisher, msg: &Message); + async fn power(&mut self, publisher: &MqttPublisher, msg: &Message); } #[derive(Debug)] pub enum MessageType { Navigate, + PowerState, } pub struct MqttClient<'a> { @@ -33,9 +35,7 @@ pub struct MqttClient<'a> { } impl<'a> MqttClient<'a> { - pub fn new( - config: &'a Configuration, - ) -> Result { + pub fn new(config: &'a Configuration) -> Result { let uri = format!( "{}://{}:{}", if config.mqtt.tls { "ssl" } else { "tcp" }, @@ -68,12 +68,16 @@ impl<'a> MqttClient<'a> { Ok(res) } - pub async fn subscribe(&mut self) -> Result { + pub async fn subscribe(&mut self) -> Result<(), Error> { + let client = self.client.lock().await; let prefix = &self.config.mqtt.topic_prefix; let t_nav = format!("{}/+/navigate", prefix); - let res = self.client.lock().await.subscribe(&t_nav, 0).await?; + let t_power = format!("{}/power", prefix); + client.subscribe(&t_nav, 0).await?; + client.subscribe(&t_power, 0).await?; self.topics.insert(t_nav, MessageType::Navigate); - Ok(res) + self.topics.insert(t_power, MessageType::PowerState); + Ok(()) } pub fn publisher(&mut self) -> MqttPublisher { @@ -118,6 +122,9 @@ impl<'a> MqttClient<'a> { MessageType::Navigate => { handler.navigate(&publisher, &msg).await; } + MessageType::PowerState => { + handler.power(&publisher, &msg).await; + } } } } @@ -198,6 +205,14 @@ impl<'a> MqttPublisher<'a> { Ok(()) } + pub async fn publish_power_state(&self, state: bool) -> Result<(), Error> { + let topic = format!("{}/power_state", self.config.mqtt.topic_prefix); + let msg = + Message::new_retained(topic, if state { "ON" } else { "OFF" }, 0); + self.client.lock().await.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; @@ -254,6 +269,27 @@ impl<'a> MqttPublisher<'a> { trace!("Publishing message: {:?}", msg); self.client.lock().await.publish(msg).await?; + let unique_id = format!("light.{}", key); + let object_id = unique_id.clone(); + let command_topic = Some(format!("{}/power", prefix)); + let state_topic = format!("{}/power_state", prefix); + let name = "Display Power".into(); + let config = HassConfig { + command_topic, + state_topic, + name, + unique_id, + object_id, + ..config + }; + let msg = Message::new_retained( + format!("homeassistant/light/{}/config", key), + serde_json::to_string(&config).unwrap(), + 0, + ); + trace!("Publishing message: {:?}", msg); + self.client.lock().await.publish(msg).await?; + info!("Succesfully published Home Assistant config"); Ok(()) } diff --git a/src/session.rs b/src/session.rs index 3f4d3a9..b8ccbda 100644 --- a/src/session.rs +++ b/src/session.rs @@ -13,7 +13,7 @@ use crate::marionette::{Marionette, MarionetteConnection}; use crate::monitor::Monitor; use crate::mqtt::{Message, MqttClient, MqttPublisher}; #[cfg(unix)] -use crate::x11::{xrandr, Display}; +use crate::x11::{dpms, xrandr, Display}; #[derive(Debug)] pub enum SessionError { @@ -230,6 +230,19 @@ impl<'a> crate::mqtt::MessageHandler for MessageHandler<'a> { Err(e) => error!("Error getting title: {}", e), } } + + async fn power(&mut self, publisher: &MqttPublisher, msg: &Message) { + match msg.payload_str().as_ref() { + "ON" => turn_screen_on(), + "OFF" => turn_screen_off(), + x => { + warn!("Received unexpected power state command: {}", x); + } + } + if let Err(e) = publisher.publish_power_state(is_screen_on()).await { + error!("Failed to publish power state: {}", e); + } + } } #[cfg(unix)] @@ -246,3 +259,60 @@ fn get_monitors() -> Vec { }) .collect() } + +fn turn_screen_on() { + let display = match Display::open() { + Ok(d) => d, + Err(_) => { + error!("unable to open display \"{}\"", Display::name()); + return; + } + }; + if !dpms::force_level(&display, dpms::DpmsPowerLevel::On) { + error!("Failed to turn on display \"{}\"", Display::name()); + } + if !dpms::disable(&display) { + error!( + "Failed to disable DPMS on display \"{}\"", + Display::name() + ); + } +} + +#[cfg(unix)] +fn turn_screen_off() { + let display = match Display::open() { + Ok(d) => d, + Err(_) => { + error!("unable to open display \"{}\"", Display::name()); + return; + } + }; + if !dpms::enable(&display) { + 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()); + } +} + +#[cfg(unix)] +fn is_screen_on() -> bool { + let display = match Display::open() { + Ok(d) => d, + Err(_) => { + error!("unable to open display \"{}\"", Display::name()); + return false; + } + }; + if dpms::query_extension(&display) && dpms::dpms_capable(&display) { + let info = dpms::get_info(&display); + if info.state && info.power_level != dpms::DpmsPowerLevel::On { + return false; + } + } + true +} diff --git a/src/x11/dpms.rs b/src/x11/dpms.rs new file mode 100644 index 0000000..f53c39d --- /dev/null +++ b/src/x11/dpms.rs @@ -0,0 +1,120 @@ +use x11::dpms::{ + DPMSCapable, DPMSDisable, DPMSEnable, DPMSForceLevel, DPMSGetTimeouts, + DPMSInfo, DPMSQueryExtension, +}; +use x11::dpms::{DPMSModeOff, DPMSModeOn, DPMSModeStandby, DPMSModeSuspend}; +use x11::xmd::{BOOL, CARD16}; + +use super::Display; + +/// DPMS Power Level +/// +/// There are four power levels specified by the Video Electronics Standards +/// Association (VESA) Display Power Management Signaling (DPMS) standard. +/// These are mapped onto the X DPMS Extension +#[derive(Eq, PartialEq)] +pub enum DpmsPowerLevel { + /// In use + On = DPMSModeOn as isize, + /// Blanked, low power + Standby = DPMSModeStandby as isize, + /// Blanked, lower power + Suspend = DPMSModeSuspend as isize, + /// Shut off, awaiting activity + Off = DPMSModeOff as isize, + Unknown = -1, +} + +impl From for DpmsPowerLevel { + fn from(v: u16) -> Self { + #[allow(non_snake_case)] + match v { + x if x == DpmsPowerLevel::On as u16 => Self::On, + x if x == DpmsPowerLevel::Standby as u16 => Self::Standby, + x if x == DpmsPowerLevel::Suspend as u16 => Self::Suspend, + x if x == DpmsPowerLevel::Off as u16 => Self::Off, + _ => Self::Unknown, + } + } +} + +/// Result from [`get_info`] function (`DPMSInfo`) +pub struct DpmsInfo { + /// Current power level + pub power_level: DpmsPowerLevel, + /// DPMS enabled/disabled state + pub state: bool, +} + +/// Result from [`get_timeouts`] function (`DPMSGetTimeouts`) +pub struct DpmsTimeouts { + /// Amount of time of inactivity in seconds before standby mode is invoked + pub standby: u16, + /// Amount of time of inactivity in seconds before the second level of power + /// savings is invoked + pub suspend: u16, + /// Amount of time of inactivity in seconds before the third and final level + /// of power savings is invoked + pub off: u16, +} + +/// Queries the X server to determine the availability of the DPMS Extension +pub fn query_extension(display: &Display) -> bool { + let mut event_base = 0; + let mut error_base = 0; + let r = unsafe { + DPMSQueryExtension(display.display, &mut event_base, &mut error_base) + }; + r != 0 +} + +/// Returns the DPMS capability of the X server, either TRUE (capable of DPMS) +/// or FALSE (incapable of DPMS) +pub fn dpms_capable(display: &Display) -> bool { + let r = unsafe { DPMSCapable(display.display) }; + r != 0 +} + +/// Returns information about the current DPMS state +pub fn get_info(display: &Display) -> DpmsInfo { + let mut power_level: CARD16 = 0; + let mut state: BOOL = 0; + unsafe { DPMSInfo(display.display, &mut power_level, &mut state) }; + DpmsInfo { + power_level: power_level.into(), + state: state != 0, + } +} + +/// Retrieves the timeout values used by the X server for DPMS timings +pub fn get_timeouts(display: &Display) -> DpmsTimeouts { + let mut standby: CARD16 = 0; + let mut suspend: CARD16 = 0; + let mut off: CARD16 = 0; + unsafe { + DPMSGetTimeouts(display.display, &mut standby, &mut suspend, &mut off) + }; + DpmsTimeouts { + standby, + suspend, + off, + } +} + +/// Forces a DPMS capable display into the specified power level +pub fn force_level(display: &Display, level: DpmsPowerLevel) -> bool { + let r = unsafe { DPMSForceLevel(display.display, level as u16) }; + r != 0 +} + +/// Enables DPMS on the specified display +pub fn enable(display: &Display) -> bool { + let r = unsafe { DPMSEnable(display.display) }; + r != 0 +} + +/// Disables DPMS on the specified display +pub fn disable(display: &Display) -> bool { + let r = unsafe { DPMSDisable(display.display) }; + r != 0 +} diff --git a/src/x11/mod.rs b/src/x11/mod.rs index 373ed86..d6576ca 100644 --- a/src/x11/mod.rs +++ b/src/x11/mod.rs @@ -1,3 +1,5 @@ +#[allow(dead_code)] +pub mod dpms; pub mod xrandr; use std::ffi::CStr; @@ -18,6 +20,7 @@ pub struct Display { display: *mut _XDisplay, } +#[allow(dead_code)] impl Display { /// Open a connection to the X server ///