Skip to content

Repository files navigation

Wake TV - Decky Loader Plugin

A Decky Loader plugin that automatically wakes your TV and switches to the correct HDMI input when your Steam Deck, steamMachine or any device running steamos resumes from sleep or when you press the gamepad Guide button.

Works with any TV that supports Wake-on-LAN (WOL) and the LG webOS SSAP protocol for HDMI switching (LG TVs from 2018+).

Features

  • Wake-on-LAN - Send magic packets to wake your TV
  • Auto HDMI switch - Automatically switch to the correct HDMI input after waking
  • Gamepad Guide button wake - Press the Guide/Home button on your gamepad to wake the TV
  • Sleep/resume wake - Automatically wake the TV when your device resumes from sleep
  • Auto-disable built-in controller - Automatically disables the built-in gamepad (e.g. ROG Ally controller) when an external controller is connected, and re-enables it when disconnected — eliminates the duplicate controller problem. External controllers are emulated as Xbox Elite for back paddle support via InputPlumber.
  • External gamepad fix - Automatically rebinds external USB gamepads after resume when InputPlumber is not present (defers to InputPlumber's suspend service when available to avoid duplicate notifications)
  • Bluetooth controller wake - Enables the internal Bluetooth adapter's USB wakeup flag so Bluetooth gamepads (e.g. DualSense) can wake the device from sleep. Required on non-Steam Deck hardware (ROG Ally, Legion Go) where SteamOS doesn't enable this by default.
  • USB wakeup chain repair - Re-enables BT adapter and USB3 hub wakeup flags after each resume so Bluetooth gamepads and Wake-on-LAN always work from the next sleep
  • Pair & forget - One-time pairing with your LG TV, credentials persist across reboots

Installation

  1. Install Decky Loader on your device
  2. Copy the plugin to ~/homebrew/plugins/waketv/
  3. Restart Decky Loader: sudo systemctl restart plugin_loader

Plugin Setup

  1. Open the Decky sidebar (... button) and find Wake TV
  2. Enter your TV's IP address
  3. Click Pair with TV - accept the pairing prompt on your TV screen
  4. Select the correct HDMI input
  5. Click Save

The MAC address is auto-discovered during pairing.

System Setup (One-Time)

The following system-level configuration enables advanced features like gamepad wake-from-sleep, external gamepad fix, and Wake-on-LAN to the device itself. These commands require SSH access to your device.

How Persistence Works

SteamOS uses an A/B rootfs with an /etc overlay. Updates reset this overlay, but SteamOS 3.6+ provides an official whitelist mechanism: files listed in /etc/atomic-update.conf.d/*.conf are preserved across updates. The keeplist directory itself is preserved by the system keeplist at /usr/lib/rauc/atomic-update-keep.conf, so custom entries survive too.

The setup below registers all WakeTV config files with this mechanism. Zero runtime overhead — the OS preserves them natively during the update process. The only boot-time service is for volatile USB/ACPI wakeup flags (sysfs entries that the kernel resets on every reboot).

1. Config Files (requires sudo)

All config files are installed once and persist across SteamOS updates via the atomic update keeplist.

ssh -t deck@<DEVICE_IP> "sudo /bin/sh -c '

# Register all WakeTV files with the SteamOS atomic update keeplist
mkdir -p /etc/atomic-update.conf.d
cat > /etc/atomic-update.conf.d/waketv.conf << KEEPLIST
/etc/sudoers.d/zz-waketv-usb
/etc/sudoers.d/zz-waketv-suspend
/etc/udev/rules.d/99-waketv-usb-wake.rules
/etc/polkit-1/rules.d/zz-waketv-inputplumber.rules
/etc/inputplumber/devices.d/50-external-gamepad.yaml
KEEPLIST

# Sudoers: USB unbind/bind for external gamepad fix + wakeup sysfs writes
echo \"deck ALL=(ALL) NOPASSWD: /usr/bin/tee /sys/bus/usb/drivers/usb/unbind, /usr/bin/tee /sys/bus/usb/drivers/usb/bind, /usr/bin/tee /sys/bus/usb/devices/*/power/wakeup\" > /etc/sudoers.d/zz-waketv-usb
chmod 440 /etc/sudoers.d/zz-waketv-usb

# Sudoers: remote suspend (needed for Homebridge/Apple Home)
echo \"deck ALL=(ALL) NOPASSWD: /usr/bin/systemctl suspend\" > /etc/sudoers.d/zz-waketv-suspend
chmod 440 /etc/sudoers.d/zz-waketv-suspend

# Udev: enable wakeup on dock USB3 hub and BT adapter on hot-plug
# Note: USB2 hub (05e3:0610) is intentionally excluded — its 2.4GHz gamepad
# dongle causes spurious wakes. BT controllers are the preferred wake source.
cat > /etc/udev/rules.d/99-waketv-usb-wake.rules << UDEV
ACTION==\"add\", SUBSYSTEM==\"usb\", ATTR{idVendor}==\"05e3\", ATTR{idProduct}==\"0626\", RUN+=\"/bin/sh -c '\\''echo enabled > /sys%p/power/wakeup'\\''\"
ACTION==\"add\", SUBSYSTEM==\"usb\", ATTR{idVendor}==\"0489\", ATTR{idProduct}==\"e0f5\", RUN+=\"/bin/sh -c '\\''echo enabled > /sys%p/power/wakeup'\\''\"
UDEV
udevadm control --reload-rules

# Polkit: allow plugin to toggle InputPlumber D-Bus API
mkdir -p /etc/polkit-1/rules.d
cat > /etc/polkit-1/rules.d/zz-waketv-inputplumber.rules << POLKIT
polkit.addRule(function(action, subject) {
    if (action.id === \"org.shadowblip.Input.CompositeDevice.SetTargetDevices\" &&
        subject.user === \"deck\") {
        return polkit.Result.YES;
    }
});
POLKIT
chmod 644 /etc/polkit-1/rules.d/zz-waketv-inputplumber.rules
'"

2. (Optional) External Controller as Xbox Elite

If you use an external gamepad (e.g. GameSir via 2.4GHz dongle), this InputPlumber device config makes it appear as an Xbox Elite controller. Adjust vendor_id, product_id, and name to match your controller (run lsusb and check /sys/class/input/event*/device/name).

ssh -t deck@<DEVICE_IP> "sudo /bin/sh -c '
mkdir -p /etc/inputplumber/devices.d
cat > /etc/inputplumber/devices.d/50-external-gamepad.yaml << EOF
version: 1
kind: CompositeDevice
name: External Controller

single_source: true
matches: []

source_devices:
  - group: gamepad
    evdev:
      name: Generic X-Box pad
      vendor_id: 3537
      product_id: 1098
      handler: event*

options:
  auto_manage: true

target_devices:
  - xbox-elite
EOF
systemctl restart inputplumber
'"

Note: The plugin will automatically correct the target to xbox-elite if steamos-manager overrides it on startup.

3. USB Wake: Boot Service

The boot service enables wakeup on the internal Bluetooth adapter, dock USB3 hub (for Ethernet WOL), root hubs, and ACPI wakeup sources. These are volatile kernel flags that reset every reboot — the service is the only runtime component.

Note: Port 1-4 is the internal MediaTek Bluetooth adapter — enabling its wakeup allows Bluetooth controllers (DualSense, DualShock 4, Switch Pro, etc.) to wake the device from s2idle sleep. The USB3 hub (05e3:0626 at 6-1) is needed for Wake-on-LAN via the dock Ethernet adapter. The USB2 hub (05e3:0610 at 5-1) is intentionally excluded — 2.4GHz gamepad dongles on this hub cause spurious wakes because the dongle firmware cannot distinguish a button press from a gamepad power-off event. The ACPI wakeup sources (XHC0-XHC4) and root hubs (usb5, usb6) are for the ROG Ally. Adjust for your hardware if needed — run lsusb and check /proc/acpi/wakeup.

ssh -t deck@<DEVICE_IP> "sudo /bin/sh -c '
cat > /etc/systemd/system/usb-wake.service << SVC
[Unit]
Description=Enable USB and Bluetooth wake for gamepad and Ethernet WOL
After=multi-user.target

[Service]
Type=oneshot
ExecStart=/bin/sh -c \"\
  for hub in usb5 usb6; do \
    [ -f /sys/bus/usb/devices/\\\$hub/power/wakeup ] && echo enabled > /sys/bus/usb/devices/\\\$hub/power/wakeup; \
  done; \
  for port in 1-4 6-1; do \
    [ -f /sys/bus/usb/devices/\\\$port/power/wakeup ] && echo enabled > /sys/bus/usb/devices/\\\$port/power/wakeup; \
  done; \
  for xhc in XHC0 XHC1 XHC2 XHC3 XHC4; do \
    grep -q \\\"^\\\$xhc.*disabled\\\" /proc/acpi/wakeup 2>/dev/null && echo \\\$xhc > /proc/acpi/wakeup; \
  done; \
  true\"
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target
SVC
systemctl daemon-reload
systemctl enable --now usb-wake.service
'"

4. InputPlumber Suspend Service

Enables proper input device cleanup/restoration on sleep/resume. When this service is enabled, the plugin automatically skips its own USB rebind (InputPlumber handles it), which avoids duplicate controller notifications on wake.

ssh -t deck@<DEVICE_IP> "sudo systemctl enable inputplumber-suspend.service"

5. (Optional) Prioritize Wired LAN over WiFi

If your device is connected to both WiFi and a wired dock/hub, NetworkManager may prefer WiFi. These commands set the wired connection to higher priority so traffic always routes through LAN. Changes persist across reboots and SteamOS updates.

ssh -t deck@<DEVICE_IP> "\
  sudo nmcli connection modify 'LAN' ipv4.route-metric 100 ipv6.route-metric 100 && \
  sudo nmcli connection modify 'LAN' connection.autoconnect-priority 100"

Then lower the priority on each WiFi network you use:

ssh -t deck@<DEVICE_IP> "\
  sudo nmcli connection modify '<WIFI_NAME>' ipv4.route-metric 700 ipv6.route-metric 700"

Note: Replace LAN with your wired connection name and <WIFI_NAME> with each WiFi network name. Run nmcli connection show on the device to see all connection names. Lower metric = higher priority.

How It Works

On Resume from Sleep

  1. System wakes up (power button, gamepad disconnect, or WOL packet)
  2. Plugin detects the D-Bus PrepareForSleep(false) signal
  3. If inputplumber-suspend.service is not enabled, external USB gamepads are rebound (unbind + bind) to fix the xpad driver resume bug. Otherwise InputPlumber handles device restoration.
  4. BT adapter and USB3 hub wakeup flags are re-enabled (some drivers reset them on suspend)
  5. Plugin waits for the network to come up (polls every 1s)
  6. WOL magic packet is sent to the TV
  7. Plugin connects to the TV via WebSocket and switches to the configured HDMI input

Auto-Disable Built-in Controller

When enabled in the plugin settings, the plugin continuously monitors connected gamepads:

  1. Polls /dev/input/event* every 2 seconds for gamepad-capable devices
  2. Classifies each device as built-in or external by resolving its sysfs path to a USB bus port
  3. When an external gamepad is detected, the built-in controller's virtual gamepad is removed via InputPlumber's D-Bus API (SetTargetDevices empty on the built-in CompositeDevice)
  4. If an InputPlumber device config exists for the external controller (section 2 of System Setup), it is emulated as Xbox Elite. The plugin corrects the target if steamos-manager overrides it.
  5. When all external gamepads are disconnected, the built-in controller is restored
  6. On plugin startup/shutdown, the built-in controller is always re-enabled as a safety net

Requires the polkit rule from section 1. Works with USB dongles, wired controllers, and Bluetooth gamepads.

On Gamepad Guide Button Press (While Awake)

  1. Plugin watches /dev/input/event* devices for BTN_MODE (code 316) events
  2. On press, sends WOL + HDMI switch to the TV
  3. 5-second cooldown prevents duplicate triggers

Optional: Apple Home / Siri Integration

If you have a Homebridge setup, you can control the entire gaming setup from Apple Home or Siri.

Install the WOL Plugin

In the Homebridge UI, install Homebridge WoL (homebridge-wol).

Configuration

Field Value
Name Gaming Setup (or whatever you like)
MAC address Your device's Ethernet MAC (not WiFi)
Broadcast address 192.168.1.255 (adjust for your subnet)
Ping command ping -c 1 -W 2 <DEVICE_IP>
Wake grace time 30
Shutdown command see below

Find your Ethernet MAC:

ssh deck@<DEVICE_IP> "cat /sys/class/net/enp*/address"

Shutdown Command (with Steam animation)

To suspend with the proper Steam sleep animation, the shutdown command injects a fake power button event:

ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 deck@<DEVICE_IP> "python3 -c \"import struct,time;fd=open('/dev/input/event0','wb');t=time.time();s=int(t);u=int((t-s)*1e6);p=struct.pack;fd.write(p('llHHi',s,u,1,116,1)+p('llHHi',s,u,0,0,0));fd.flush();time.sleep(0.1);t=time.time();s=int(t);u=int((t-s)*1e6);fd.write(p('llHHi',s,u,1,116,0)+p('llHHi',s,u,0,0,0));fd.close()\""

Note: Homebridge must have SSH key-based auth to the device. Run ssh-keygen && ssh-copy-id deck@<DEVICE_IP> from the Homebridge terminal.

Usage

  • "Hey Siri, turn on Gaming Setup" - Sends WOL to device, device wakes, plugin wakes TV + switches HDMI
  • "Hey Siri, turn off Gaming Setup" - SSHs to device, triggers Steam sleep animation
  • Works with HomeKit scenes and automations

Wake Methods Summary

Method How Animation
Power button Press the device's power button Yes
Bluetooth controller Press PS/Guide button on paired BT gamepad (e.g. DualSense, GameSir) — requires 1-4 wakeup enabled Yes
Wake-on-LAN Send WOL packet to device's Ethernet MAC from any device on the network Yes
Apple Home / Siri "Hey Siri, turn on Gaming Setup" via Homebridge WOL plugin Yes

Note: USB 2.4GHz gamepad dongles (on dock hub 5-1) are intentionally excluded from wake sources. The dongle firmware sends identical USB wakeup signals for button presses and gamepad power-off events, causing spurious wakes. Use Bluetooth controllers for gamepad wake instead.

All methods trigger the plugin's auto-wake flow: WOL to TV + HDMI switch.

Development

Prerequisites

  • Node.js 18+
  • pnpm

Build

pnpm install
pnpm run build

Deploy to Device

# Configure .env.deck with your device's IP
./deploy.sh

Project Structure

wake-tv/
├── main.py                    # Backend: lifecycle, watchers, settings, TV control
├── py_modules/
│   ├── tv_client.py           # LG webOS SSAP protocol (WebSocket, WOL, MAC discovery)
│   ├── input_watcher.py       # Gamepad Guide button detection via /dev/input
│   ├── sleep_watcher.py       # D-Bus PrepareForSleep signal monitoring
│   ├── usb_rebind.py          # USB rebind + wakeup chain repair after resume
│   └── controller_manager.py  # Auto-disable built-in controller on external connect
├── src/
│   ├── index.tsx              # Frontend plugin entry point
│   └── components/
│       └── WakeTVPanel.tsx    # Settings UI (IP, HDMI, toggles, pairing)
├── plugin.json                # Plugin metadata
├── deploy.sh                  # Build & deploy script
└── .env.deck                  # Device SSH config (not committed)

Troubleshooting

TV doesn't wake

  • Ensure WOL is enabled in your TV's settings (Network > Wake on LAN)
  • Verify the MAC address is correct in the plugin settings
  • Check that the TV and device are on the same network/subnet

HDMI doesn't switch

  • Make sure you've paired with the TV (accept the prompt on the TV screen)
  • Check the HDMI input number matches the port your device is connected to

Device doesn't wake from sleep via gamepad

  • Gamepad wake uses Bluetooth only (not USB 2.4GHz dongles — see note below)
  • Verify the BT adapter and USB3 hub wakeup flags:
    # BT adapter (1-4) must be enabled for controller wake
    # USB2 dock hub (5-1) should be DISABLED (causes spurious wakes)
    for d in 1-4 5-1 6-1 usb5 usb6; do
      echo "$d: $(cat /sys/bus/usb/devices/$d/power/wakeup 2>/dev/null || echo N/A)"
    done
    # Check ACPI wakeup sources (XHC controllers should show *enabled)
    grep XHC /proc/acpi/wakeup
  • Ensure usb-wake.service is running: systemctl status usb-wake.service
  • Ensure the udev rule is installed: cat /etc/udev/rules.d/99-waketv-usb-wake.rules
  • The inputplumber-suspend.service should be enabled
  • Verify the controller has WakeAllowed: yes in bluetoothctl info <MAC> and that the BT adapter wakeup is enabled (cat /sys/bus/usb/devices/1-4/power/wakeup should show enabled). On non-Steam Deck hardware, SteamOS does not enable BT adapter wakeup automatically — the usb-wake.service handles this.

Device wakes up spuriously (wakes immediately after sleep)

  • USB 2.4GHz gamepad dongles cause spurious wakes because the dongle firmware cannot distinguish between a button press and a gamepad power-off event. Ensure dock USB2 hub wakeup is disabled:
    cat /sys/bus/usb/devices/5-1/power/wakeup  # should show "disabled"
  • For Bluetooth spurious wakes, ensure only game controllers have WakeAllowed: yes (not headphones or other peripherals): bluetoothctl info <MAC> | grep WakeAllowed

External gamepad doesn't work after sleep

  • If inputplumber-suspend.service is enabled, InputPlumber handles device restoration. Check its logs: journalctl -u inputplumber -n 50
  • If InputPlumber is not installed, verify the sudoers rule is in place: ls -la /etc/sudoers.d/zz-waketv-usb
  • Check plugin logs for rebind results (look for "Post-resume:" and "inputplumber-suspend")

Things stopped working after a SteamOS update

Config files should persist natively via the atomic update keeplist. Verify:

cat /etc/atomic-update.conf.d/waketv.conf    # should list 5 paths
ls /etc/sudoers.d/zz-waketv-usb /etc/udev/rules.d/99-waketv-usb-wake.rules /etc/polkit-1/rules.d/zz-waketv-inputplumber.rules 2>&1

If the keeplist file itself was removed (edge case in major OS restructuring), re-run section 1 of System Setup.

Plugin logs

# View latest log file
ls -t ~/homebrew/logs/waketv/*.log | head -1 | xargs tail -100

# Follow logs in real-time (via deploy script)
./deploy.sh --logs

About

Decky Loader plugin for Steam Deck, Steam Machine, and any SteamOS device to Wake LG TVs and Switch to selected HDMI Input

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages