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"
|
toml = "0.5.10"
|
||||||
tracing = "0.1.37"
|
tracing = "0.1.37"
|
||||||
tracing-subscriber = { version = "0.3.16", features = ["env-filter", "fmt"] }
|
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]
|
#[async_trait]
|
||||||
pub trait MessageHandler {
|
pub trait MessageHandler {
|
||||||
async fn navigate(&mut self, publisher: &MqttPublisher, msg: &Message);
|
async fn navigate(&mut self, publisher: &MqttPublisher, msg: &Message);
|
||||||
|
async fn power(&mut self, publisher: &MqttPublisher, msg: &Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum MessageType {
|
pub enum MessageType {
|
||||||
Navigate,
|
Navigate,
|
||||||
|
PowerState,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct MqttClient<'a> {
|
pub struct MqttClient<'a> {
|
||||||
|
@ -33,9 +35,7 @@ pub struct MqttClient<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> MqttClient<'a> {
|
impl<'a> MqttClient<'a> {
|
||||||
pub fn new(
|
pub fn new(config: &'a Configuration) -> Result<Self, Error> {
|
||||||
config: &'a Configuration,
|
|
||||||
) -> Result<Self, Error> {
|
|
||||||
let uri = format!(
|
let uri = format!(
|
||||||
"{}://{}:{}",
|
"{}://{}:{}",
|
||||||
if config.mqtt.tls { "ssl" } else { "tcp" },
|
if config.mqtt.tls { "ssl" } else { "tcp" },
|
||||||
|
@ -68,12 +68,16 @@ impl<'a> MqttClient<'a> {
|
||||||
Ok(res)
|
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 prefix = &self.config.mqtt.topic_prefix;
|
||||||
let t_nav = format!("{}/+/navigate", 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);
|
self.topics.insert(t_nav, MessageType::Navigate);
|
||||||
Ok(res)
|
self.topics.insert(t_power, MessageType::PowerState);
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn publisher(&mut self) -> MqttPublisher {
|
pub fn publisher(&mut self) -> MqttPublisher {
|
||||||
|
@ -118,6 +122,9 @@ impl<'a> MqttClient<'a> {
|
||||||
MessageType::Navigate => {
|
MessageType::Navigate => {
|
||||||
handler.navigate(&publisher, &msg).await;
|
handler.navigate(&publisher, &msg).await;
|
||||||
}
|
}
|
||||||
|
MessageType::PowerState => {
|
||||||
|
handler.power(&publisher, &msg).await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -198,6 +205,14 @@ impl<'a> MqttPublisher<'a> {
|
||||||
Ok(())
|
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> {
|
pub async fn publish_config(&self, screen: &str) -> Result<(), Error> {
|
||||||
debug!("Publishing Home Assistant configuration");
|
debug!("Publishing Home Assistant configuration");
|
||||||
let prefix = &self.config.mqtt.topic_prefix;
|
let prefix = &self.config.mqtt.topic_prefix;
|
||||||
|
@ -254,6 +269,27 @@ impl<'a> MqttPublisher<'a> {
|
||||||
trace!("Publishing message: {:?}", msg);
|
trace!("Publishing message: {:?}", msg);
|
||||||
self.client.lock().await.publish(msg).await?;
|
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");
|
info!("Succesfully published Home Assistant config");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ use crate::marionette::{Marionette, MarionetteConnection};
|
||||||
use crate::monitor::Monitor;
|
use crate::monitor::Monitor;
|
||||||
use crate::mqtt::{Message, MqttClient, MqttPublisher};
|
use crate::mqtt::{Message, MqttClient, MqttPublisher};
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
use crate::x11::{xrandr, Display};
|
use crate::x11::{dpms, xrandr, Display};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum SessionError {
|
pub enum SessionError {
|
||||||
|
@ -230,6 +230,19 @@ impl<'a> crate::mqtt::MessageHandler for MessageHandler<'a> {
|
||||||
Err(e) => error!("Error getting title: {}", e),
|
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)]
|
#[cfg(unix)]
|
||||||
|
@ -246,3 +259,60 @@ fn get_monitors() -> Vec<Monitor> {
|
||||||
})
|
})
|
||||||
.collect()
|
.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;
|
pub mod xrandr;
|
||||||
|
|
||||||
use std::ffi::CStr;
|
use std::ffi::CStr;
|
||||||
|
@ -18,6 +20,7 @@ pub struct Display {
|
||||||
display: *mut _XDisplay,
|
display: *mut _XDisplay,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
impl Display {
|
impl Display {
|
||||||
/// Open a connection to the X server
|
/// Open a connection to the X server
|
||||||
///
|
///
|
||||||
|
|
Loading…
Reference in New Issue