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
parent
a2acdfd0dc
commit
28c0944130
|
@ -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"] }
|
||||
|
|
48
src/mqtt.rs
48
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<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(())
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
///
|
||||
|
|
Loading…
Reference in New Issue