Skip to content

Commit

Permalink
Merge pull request #224 from knuton/multiple-power-buttons
Browse files Browse the repository at this point in the history
Support for multiple 'Power Button's
  • Loading branch information
yfyf authored Feb 10, 2025
2 parents 3c0677a + fb777c6 commit 9de2b83
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 14 deletions.
2 changes: 1 addition & 1 deletion application/power-management/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ in {

systemd.services.power-button-shutdown = {
enable = true;
description = "Detect Power Button key presses, by Power Button device only and not remote controls, then shutdown computer";
description = "Handle Power Button device key presses.";
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = "${power-button-shutdown}/bin/power-button-shutdown";
Expand Down
63 changes: 50 additions & 13 deletions application/power-management/power-button-shutdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,59 @@

import evdev
import logging
import os
import subprocess
import sys
import threading

logging.basicConfig(level=logging.INFO)

devices = [evdev.InputDevice(path) for path in evdev.list_devices()]
device_names = ', '.join([d.name for d in devices])
logging.info(f'Found devices: {device_names}')
def handle_device(device):
"""Handle power key presses for the given input device by invoking poweroff."""

power_off_device = next((d for d in devices if d.name == 'Power Button'), None)
if power_off_device is None:
logging.error(f'Power Button device not found')
sys.exit(1)
logging.info(f'Listening to Power Button on device {device.path}')
try:
for event in device.read_loop():
if event.type == evdev.ecodes.EV_KEY and event.code == evdev.ecodes.KEY_POWER and event.value:
logging.info(f'KEY_POWER detected on {device.path}, shutting down')
subprocess.run(['systemctl', 'start', 'poweroff.target'], check=True)
except Exception as e:
logging.error(f'Error handling {device.path}: {e}')

def get_power_button_devices():
"""Get all input devices which identify as 'Power Button' and have a power key."""

devices = [evdev.InputDevice(path) for path in evdev.list_devices()]

power_button_devices = []
for device in devices:
try:
if device.name == 'Power Button':
ev_capabilities = device.capabilities().get(evdev.ecodes.EV_KEY, [])
if evdev.ecodes.KEY_POWER in ev_capabilities:
power_button_devices.append(device)
except Exception as e:
logging.warning(f'Failed to inspect {device.path}: {e}')

return power_button_devices

# Identify Power Button devices
power_button_devices = get_power_button_devices()
if not power_button_devices:
logging.error('No Power Button devices found')
sys.exit(1)
logging.info(f'Found {len(power_button_devices)} Power Button device(s)')

# Start a thread with a handler for each identified device
handler_threads = []
for device in power_button_devices:
thread = threading.Thread(target=handle_device, args=(device,))
thread.start()
handler_threads.append(thread)

# Exit gracefully if a handler executes
try:
for thread in handler_threads:
thread.join()
except KeyboardInterrupt:
sys.exit(0)

logging.info(f'Listening to Power Button on device {power_off_device.path}')
for event in power_off_device.read_loop():
if event.type == evdev.ecodes.EV_KEY and evdev.ecodes.KEY[event.code] == 'KEY_POWER' and event.value:
logging.info('KEY_POWER detected on Power Button device, shutting down')
os.system('systemctl start poweroff.target')
1 change: 1 addition & 0 deletions controller/Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
- controller: Suppress password prompt for open WiFi networks
- controller: Explicitly mark WiFi networks with unsupported authentication methods
- controller: Improve error messages when connecting to WiFi networks fails
- os: Extend Power Button handling to multiple devices

## Removed

Expand Down
44 changes: 44 additions & 0 deletions testing/integration/power-button-shutdown.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
let
pkgs = import ../../pkgs { };
in
pkgs.testers.runNixOSTest {
name = "ACPI power button handling";

nodes = {
machine = { config, pkgs, ... }: {
imports = [ ../../application/power-management ];
};
};

extraPythonPackages = ps: [
ps.colorama
ps.types-colorama
];

testScript = {nodes}:
''
${builtins.readFile ../helpers/nixos-test-script-helpers.py}
import time
with TestPrecondition("Power Button has been recognized"):
machine.start()
machine.wait_for_unit("multi-user.target")
machine.wait_for_console_text("Power Button")
print(machine.succeed("cat /proc/bus/input/devices"))
with TestCase("Short press on power and sleep from regular keyboard are ignored"):
# https://github.com/qemu/qemu/blob/master/pc-bios/keymaps/en-us
machine.send_monitor_command("sendkey 0xde") # XF86PowerOff
machine.send_monitor_command("sendkey 0xdf") # XF86Sleep
time.sleep(5)
machine.succeed("echo still alive", timeout=1)
with TestCase("ACPI shutdown command invokes shutdown"):
machine.send_monitor_command("system_powerdown")
machine.wait_for_console_text("Stopped target Multi-User System")
# Test script does not finish on its own after shutdown
exit(0)
'';
}

0 comments on commit 9de2b83

Please sign in to comment.