Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

654 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

dormant

Blank OLED screens when the room empties. Wake them the moment you return.

CI MSRV 1.88 License: MIT OR Apache-2.0 Linux and macOS Latest release


OLED panels burn in when they hold a static image. OS idle timers are a blunt fix — they blank while you're reading and linger after you leave. Presence sensors know the difference: they can tell an empty room from a still one.

dormant is a Rust daemon that reads those sensors and blanks your displays only when the room is actually empty, then wakes them the instant someone walks back in. It runs your PC monitors and your TVs, over local buses and over the network, on rules you write per display.

What it does

  • Blank displays when a room is empty instead of on a fixed idle timer — driven by MQTT, Home Assistant WebSocket, or USB mmWave radar sensors
  • Blank without killing audio — DDC/CI, Samsung Tizen, and gamma-table paths leave the OS output intact
  • Soft KVM — two machines, one monitor; pull the panel by hotkey, CLI, tray, or web.
  • Escalate through a render ladder: black Wayland overlay → muted mpv screensaver → power-off, on configurable dwell timers
  • Track brightness-weighted panel on-hours per display, stored locally — no telemetry
  • Wear-even screensavers — order media and bias bounded pixel shifts toward cooler estimated panel regions
  • Active wear sampling — opt in to content-weighted OLED wear tracking from the KDE Wayland compositor.
  • Configure and control through a loopback web dashboard with a validated config editor that preserves comments
  • Config section rail — sticky per-tab section navigation with scroll-spy in the web UI
  • Web UI v3 — full config-schema parity in the web editor (coordination, keymaps, input filters, hooks), a new shared-display switching view with live pull/push feedback, event history across reloads, doctor checks grouped by subject, panel-wear detail split by seeded/measured time, and a security-posture card with honest boundary notes.
  • Drive the daemon from a desktop tray with global hotkeys (Linux KDE · macOS menu bar)
  • Probe every sensor and display against live hardware with dormantctl doctor
  • Recover from a bad config boot: automatic last-known-good rollback, a systemd watchdog, and dormantctl emergency-wake
  • MQTT state publishing — opt-in retained state + Home Assistant discovery for sensors, zones, and displays

dormant web dashboard — displays, sensors, zones, signal flow, and panel-exposure summary

dormant switching view — shared-panel ownership, pull and push actions, and the per-display hooks

dormant KDE tray applet — status tooltip and pause/blank/wake menu

Why not just DPMS?

Protecting OLEDs without the usual trade-offs — no black bars burned into the panel, no audio cutting out when the TV screen goes dark, no three-second wait to see your desktop again. Every blank mode makes a different bargain:

Mode OLED protection Audio survives Wake
screen_off_audio_on Full Yes Fast
power_off Full No ~1s standby
brightness_zero Partial — pixels stay lit Yes Instant

Three display paths leave the OS output alone, so audio keeps playing:

  • ddcci power_off writes VCP 0xD6 straight to the monitor's internal controller. The panel blanks; the OS output — and its audio sink — stay up.
  • samsung-tizen screen_off_audio_on sends KEY_PICTURE_OFF over WebSocket. The TV cuts its backlight; the HDMI source keeps running and the audio plays on.
  • samsung-tizen brightness_zero drives the TV's backlight to zero over Samsung IP Control G2 on port 1516 — a near-black dim rather than a true off, but the only mode that leaves the source's audio uninterrupted on a 2024 Samsung OLED.

Every DPMS path (kwin-dpms included) tears the output down and takes the audio with it, which is why it's a documented fallback, not a default.

What's in the box

Three binaries: dormantd (the daemon), dormantctl (the CLI), and dormant-tray (a desktop tray applet).

Sensors

Source
MQTT — Zigbee2MQTT, ESPHome, any broker (auth supported) Ready
Home Assistant WebSocket Ready
USB-serial LD2410 mmWave radar Ready

Zones fuse multiple sensors with any / all / quorum / weighted logic. A sensor that goes quiet — broker down, USB unplugged, a stale reading — resolves as present, never absent. dormant will not blank a room it can't see.

dormantctl doctor checks the configured sensors and displays. --report-issue and --draft-feature turn the full offline probe run into a ready-to-file GitHub issue draft, with known config and credential values redacted.

Display controllers

Controller
ddcci — DDC/CI power-off and brightness (audio-safe, Linux and macOS) Ready
samsung-tizen — Samsung Tizen TVs over WebSocket Ready
kwin-dpms — KDE Wayland DPMS (audio-unsafe fallback, Linux only) Ready
ha-passthrough — any Home Assistant service call Ready
command — arbitrary blank/wake shell commands Ready
macos-gamma-black — Quartz gamma-table black (audio-safe, macOS only) Ready
macos-display-sleeppmset display sleep (whole-machine fallback, macOS only) Ready

Each display gets an ordered controller chain with automatic fallback and bounded wake retry. A wake that fails on one controller escalates to the next. Repeated failures surface through desktop notifications, the tray, and the web dashboard.

A display referenced by no rule is manual-only: the daemon builds it, dormantctl status / the web UI / the tray show it, and it responds to hand-issued blank / wake commands — but no zone or rule ever drives it. That lets a TV join dormant without a keep-awake dummy zone.

Panel-wear tracking

dormant tracks brightness-weighted on-hours per display and shows them in the web dashboard as a panel-exposure card. An advisory appears after a long stretch without a rest window. Tracking does not change blank/wake timing. Ledgers stay under the daemon's local state directory; there is no telemetry. See Panel-wear tracking for the limits and the wear.* keys.

Web dashboard and tray

Build with --features web-ui for a loopback web dashboard: a live view of the sensor → zone → rule → display pipeline, force blank/wake, pause/resume, panel-wear tracking, failure state, and a config editor. The editor can create and delete sensors, zones, displays, and rules; its Samsung pairing wizard stores the granted token in credentials.toml. Config writes preserve comments and pass daemon-identical validation before they reach disk.

Build with --features render for blanking that never touches DPMS: a fullscreen black Wayland overlay, an escalation ladder (screensaver → black → power-off on your own dwell timers), and a muted streaming screensaver driven by mpv. The screensaver applies a 2 px pixel shift every 2 minutes by default; the uniform black overlay never shifts.

dormant-tray puts per-display status and blank/wake/pause controls in the KDE StatusNotifierItem tray on Linux and a native NSStatusItem menu-bar item on macOS. See the installation guide for systemd and launchd autostart.

Quickstart

Install through the channel that matches your platform:

Homebrew (macOS or Linuxbrew):

brew install legion-works/tap/dormantd
brew install legion-works/tap/dormantctl
brew install legion-works/tap/dormant-tray

Arch Linux (AUR):

yay -S dormant-bin

The AUR package includes the pre-built binaries and systemd user units.

Installer scripts (Linux x86_64 / aarch64, macOS arm64 / x86_64):

curl --proto '=https' --tlsv1.2 -LsSf https://github.com/legion-works/dormant/releases/download/v0.4.0/dormantd-installer.sh | sh
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/legion-works/dormant/releases/download/v0.4.0/dormantctl-installer.sh | sh

The dormant-tray-installer.sh script is in the same release directory. Checksums are published alongside every artifact; verify with sha256sum -c <artifact>.sha256.

Release tarballs include the matching systemd user units on Linux. Manual installation and source builds are covered in the installation guide.

On macOS, install the per-user LaunchAgents for the daemon and tray, then start them — see Installation: LaunchAgent (macOS):

dormantctl launchd install
launchctl bootstrap gui/$UID "$HOME/Library/LaunchAgents/com.legionworks.dormant.plist"
launchctl bootstrap gui/$UID "$HOME/Library/LaunchAgents/com.legionworks.dormant-tray.plist"

Build from source (Linux, Rust 1.88+)

git clone https://github.com/legion-works/dormant.git
cd dormant
sudo apt install libudev-dev libwayland-dev libmpv-dev pkg-config
cargo build --release --features web-ui,render
install -Dm755 target/release/dormantd  ~/.local/bin/dormantd
install -Dm755 target/release/dormantctl ~/.local/bin/dormantctl
install -Dm755 target/release/dormant-tray ~/.local/bin/dormant-tray

Write ~/.config/dormant/config.toml:

config_version = 1

[sensors.desk]
type = "mqtt"
broker_url = "mqtt://localhost:1883"
topic = "zigbee2mqtt/desk-presence"

[zones.office]
mode = "any"
members = ["desk"]

[displays.monitor]
controllers = ["ddcci"]
blank_mode = "power_off"

[rules.office]
zone = "office"
displays = ["monitor"]
grace_period = "60s"

Then:

dormantctl validate     # check the config
dormantd                # start the daemon
dormantctl status       # watch the pipeline
dormantctl doctor       # diagnose sensors and displays against your hardware
dormantctl doctor exercise monitor  # verify a real blank → wake control path

Run it as a user service:

mkdir -p ~/.config/systemd/user
cp crates/dormantd/systemd/dormant.service ~/.config/systemd/user/
systemctl --user enable --now dormant

Documentation

The rendered book for the latest release lives at legion-works.github.io/dormant. Configuration reference, sensor and controller guides, and the doctor command are also in docs/src/introduction.md. Hardware-specific findings — which DDC codes work, how Samsung standby behaves — live in docs/research/.

Status

Running in production on the author's hardware — an AOC AGON OLED monitor and a Samsung S90D — driven by real Zigbee and mmWave presence sensors. CI covers the full workspace on Linux, macOS, and Windows; macOS (M1) is a genuinely supported target as of this milestone — DDC/CI, Quartz-gamma black, and pmset display-sleep controllers, a CoreGraphics idle source, launchd LaunchAgents, and a native menu-bar tray item — while Windows remains portability-only (no native display control). The daemon caps Tokio at two workers, calls malloc_trim after screensaver teardown, and sets MALLOC_ARENA_MAX=2 in the systemd unit (Linux). The shipped watchdog restarts a wedged engine on Linux via systemd's WatchdogSec; macOS has no equivalent wedged-daemon detection (see Installation: LaunchAgent (macOS)). Last-known-good rollback can recover a bad boot config on both platforms — it is supervisor-agnostic.

It's a young project with one maintainer, aimed at homelabs and single-operator setups; interfaces can still shift before 1.0, and the web dashboard binds to loopback with no authentication by design.

License

MIT OR Apache-2.0, at your option.


Legion Works  A Legion Works fleet daemon. Many programs. One consensus.

About

Presence-aware display daemon — blanks OLED screens when the room is empty, wakes them instantly on return, tracks panel wear, and soft-KVM-switches shared monitors between machines over DDC/CI

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages