Skip to content

Commit 9de2b83

Browse files
authored
Merge pull request #224 from knuton/multiple-power-buttons
Support for multiple 'Power Button's
2 parents 3c0677a + fb777c6 commit 9de2b83

File tree

4 files changed

+96
-14
lines changed

4 files changed

+96
-14
lines changed

application/power-management/default.nix

+1-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ in {
6060

6161
systemd.services.power-button-shutdown = {
6262
enable = true;
63-
description = "Detect Power Button key presses, by Power Button device only and not remote controls, then shutdown computer";
63+
description = "Handle Power Button device key presses.";
6464
wantedBy = [ "multi-user.target" ];
6565
serviceConfig = {
6666
ExecStart = "${power-button-shutdown}/bin/power-button-shutdown";

application/power-management/power-button-shutdown.py

+50-13
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,59 @@
22

33
import evdev
44
import logging
5-
import os
5+
import subprocess
66
import sys
7+
import threading
78

89
logging.basicConfig(level=logging.INFO)
910

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

14-
power_off_device = next((d for d in devices if d.name == 'Power Button'), None)
15-
if power_off_device is None:
16-
logging.error(f'Power Button device not found')
17-
sys.exit(1)
14+
logging.info(f'Listening to Power Button on device {device.path}')
15+
try:
16+
for event in device.read_loop():
17+
if event.type == evdev.ecodes.EV_KEY and event.code == evdev.ecodes.KEY_POWER and event.value:
18+
logging.info(f'KEY_POWER detected on {device.path}, shutting down')
19+
subprocess.run(['systemctl', 'start', 'poweroff.target'], check=True)
20+
except Exception as e:
21+
logging.error(f'Error handling {device.path}: {e}')
22+
23+
def get_power_button_devices():
24+
"""Get all input devices which identify as 'Power Button' and have a power key."""
25+
26+
devices = [evdev.InputDevice(path) for path in evdev.list_devices()]
27+
28+
power_button_devices = []
29+
for device in devices:
30+
try:
31+
if device.name == 'Power Button':
32+
ev_capabilities = device.capabilities().get(evdev.ecodes.EV_KEY, [])
33+
if evdev.ecodes.KEY_POWER in ev_capabilities:
34+
power_button_devices.append(device)
35+
except Exception as e:
36+
logging.warning(f'Failed to inspect {device.path}: {e}')
37+
38+
return power_button_devices
39+
40+
# Identify Power Button devices
41+
power_button_devices = get_power_button_devices()
42+
if not power_button_devices:
43+
logging.error('No Power Button devices found')
44+
sys.exit(1)
45+
logging.info(f'Found {len(power_button_devices)} Power Button device(s)')
46+
47+
# Start a thread with a handler for each identified device
48+
handler_threads = []
49+
for device in power_button_devices:
50+
thread = threading.Thread(target=handle_device, args=(device,))
51+
thread.start()
52+
handler_threads.append(thread)
53+
54+
# Exit gracefully if a handler executes
55+
try:
56+
for thread in handler_threads:
57+
thread.join()
58+
except KeyboardInterrupt:
59+
sys.exit(0)
1860

19-
logging.info(f'Listening to Power Button on device {power_off_device.path}')
20-
for event in power_off_device.read_loop():
21-
if event.type == evdev.ecodes.EV_KEY and evdev.ecodes.KEY[event.code] == 'KEY_POWER' and event.value:
22-
logging.info('KEY_POWER detected on Power Button device, shutting down')
23-
os.system('systemctl start poweroff.target')

controller/Changelog.md

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
- controller: Suppress password prompt for open WiFi networks
1818
- controller: Explicitly mark WiFi networks with unsupported authentication methods
1919
- controller: Improve error messages when connecting to WiFi networks fails
20+
- os: Extend Power Button handling to multiple devices
2021

2122
## Removed
2223

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
let
2+
pkgs = import ../../pkgs { };
3+
in
4+
pkgs.testers.runNixOSTest {
5+
name = "ACPI power button handling";
6+
7+
nodes = {
8+
machine = { config, pkgs, ... }: {
9+
imports = [ ../../application/power-management ];
10+
};
11+
};
12+
13+
extraPythonPackages = ps: [
14+
ps.colorama
15+
ps.types-colorama
16+
];
17+
18+
testScript = {nodes}:
19+
''
20+
${builtins.readFile ../helpers/nixos-test-script-helpers.py}
21+
import time
22+
23+
with TestPrecondition("Power Button has been recognized"):
24+
machine.start()
25+
machine.wait_for_unit("multi-user.target")
26+
machine.wait_for_console_text("Power Button")
27+
28+
print(machine.succeed("cat /proc/bus/input/devices"))
29+
30+
with TestCase("Short press on power and sleep from regular keyboard are ignored"):
31+
# https://github.com/qemu/qemu/blob/master/pc-bios/keymaps/en-us
32+
machine.send_monitor_command("sendkey 0xde") # XF86PowerOff
33+
machine.send_monitor_command("sendkey 0xdf") # XF86Sleep
34+
time.sleep(5)
35+
machine.succeed("echo still alive", timeout=1)
36+
37+
with TestCase("ACPI shutdown command invokes shutdown"):
38+
machine.send_monitor_command("system_powerdown")
39+
machine.wait_for_console_text("Stopped target Multi-User System")
40+
41+
# Test script does not finish on its own after shutdown
42+
exit(0)
43+
'';
44+
}

0 commit comments

Comments
 (0)