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.
dev/ci
Dustin 2023-01-09 11:39:52 -06:00
parent a2acdfd0dc
commit 28c0944130
5 changed files with 237 additions and 8 deletions

View File

@ -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"] }

View File

@ -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<Self, Error> {
pub fn new(config: &'a Configuration) -> Result<Self, Error> {
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<ServerResponse, Error> {
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(())
}

View File

@ -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<Monitor> {
})
.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
}

120
src/x11/dpms.rs Normal file
View File

@ -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<u16> 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
}

View File

@ -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
///