Home Assistant integration for SteamOS — voice-control your games, media, and system from anywhere.
Boiler Room bridges Home Assistant and SteamOS, letting you launch games, play Jellyfin media, switch between desktop and gaming mode, and manage your SteamOS device with voice commands, automations, and dashboard controls.
Two components that snap together:
boiler-room-agent— A Go binary that runs on your SteamOS device. It scans your game library, monitors system sensors, and exposes a REST API for control.boiler_roomintegration — A Home Assistant custom integration that discovers agents on the network and creates entities for games, media, sensors, and power controls.
Voice / HA Dashboard
|
Home Assistant
|
Boiler Room Integration (Python)
| HTTP REST + WebSocket
Boiler Room Agent (Go, on SteamOS)
|
Steam / Flatpak / steamosctl / Jellyfin API
- Launch any Steam game by voice or from the HA dashboard
- Launch Flatpak apps (Firefox, Jellyfin, etc.) the same way
- Fuzzy matching — handles partial names, abbreviations, and aliases
- Built-in aliases for common games (e.g., "bg3" = Baldur's Gate 3)
- Recent launches tracked and persisted across reboots
- Search and play movies, shows, music, and albums by voice
- Type hints to disambiguate ("play the movie Fargo" vs "play the show Fargo")
- Browse content without playing — navigates to the item's page in the Jellyfin app
- Auto-launches the Jellyfin app if not running, skips re-launch if already open
- Configured entirely from the HA UI (Settings > Integrations > Configure)
- Independent of the official Jellyfin integration — no entity sprawl
- Suspend, shutdown, reboot via voice or buttons
- Wake-on-LAN — works even when the device is off (MAC address persisted)
- Real-time power state tracking via WebSocket (on, sleep, shutdown, reboot)
- Gaming/Desktop mode toggle switch
- Volume control by voice or slider
- CPU and GPU temperature sensors
- Battery level and charging status
- Current game detection
- Installed game and app counts
- mDNS auto-discovery — HA finds your device automatically
- Supports multiple SteamOS devices on the same network
All voice commands work through Home Assistant Assist (voice satellites, the app, or the Assist text box).
"Launch Elden Ring"
"Play Vampire Survivors"
"Start Balatro on the steam machine"
"Fire up Cyberpunk"
"Put on Stardew Valley"
Aliases work too:
"Play bg3" → Baldur's Gate 3
"Launch cyberpunk" → Cyberpunk 2077
"Play red dead" → Red Dead Redemption 2
"Start civ" → Sid Meier's Civilization VI
"Open Firefox on the deck"
"Launch the Jellyfin app"
"Start the Chrome app"
"Play Robocop on Jellyfin"
"Watch Breaking Bad on Jellyfin"
"Listen to Dark Side of the Moon on Jellyfin"
With type hints (when names collide):
"Play the movie Fargo on Jellyfin"
"Play the show Fargo on Jellyfin"
"Play the album Fargo on Jellyfin"
"Listen to the song Bohemian Rhapsody on Jellyfin"
"Show me Taskmaster on Jellyfin"
"Browse Akira Kurosawa on Jellyfin"
"Look up the show Breaking Bad on Jellyfin"
"Show me the album Abbey Road on Jellyfin"
This navigates to the item's page in the Jellyfin app without starting playback.
"Suspend the steam machine"
"Put the deck to sleep"
"Shut down the gaming pc"
"Reboot the steam machine"
"Set deck volume to 50"
"Search YouTube for cat videos"
"Play lo-fi beats on YouTube"
SSH into your SteamOS device and run the installer:
ssh deck@<your-deck-ip>
curl -fsSL https://raw.githubusercontent.com/rappo/boiler-room/main/install.sh | bashThe installer will:
- Download the agent binary to
~/.local/bin/ - Create a default config at
~/.config/boiler-room/config.yaml - Set up and start a systemd user service
- Enable auto-start at boot (even before GUI login)
You should see output like:
Boiler Room is running!
API: http://<device-ip>:9451/api/v1/status
Config: /home/deck/.config/boiler-room/config.yaml
Service: systemctl --user status boiler-room
- Copy
integration/custom_components/boiler_room/to your HA config'scustom_components/directory - Copy
integration/custom_components/boiler_room/custom_sentences/en/boiler_room.yamlto<HA config>/custom_sentences/en/boiler_room.yaml - Restart Home Assistant
- Go to Settings > Devices & Services > Add Integration
- Search for Boiler Room
- If your device is on the same network, it may be auto-discovered. Otherwise, enter the IP address.
- Confirm the device — you'll see your game count and device name
- Go to Settings > Integrations > Boiler Room > Configure
- Enter your Jellyfin server URL (e.g.,
http://<jellyfin-ip>:8096) - Enter your Jellyfin API key (generate one in Jellyfin Dashboard > API Keys)
- Submit — the connection is validated before saving
| Entity | Type | Description |
|---|---|---|
| Media Player | media_player |
Shows current game, play/pause/stop controls |
| Quick Launch | select |
Dropdown to pick and launch a game |
| Gaming Mode | switch |
Toggle between gaming mode and desktop mode |
| Wake (WoL) | button |
Wake the device via Wake-on-LAN (always available) |
| Suspend | button |
Suspend the device |
| Shutdown | button |
Shut down the device |
| Reboot | button |
Reboot the device |
| Power State | sensor |
Real-time power state (on, sleep, shutdown, reboot) |
| CPU Temperature | sensor |
Current CPU temperature |
| GPU Temperature | sensor |
Current GPU temperature |
| Battery Level | sensor |
Battery percentage |
| Volume | number |
Volume slider (0-100) |
| Current Game | sensor |
Name of the running game |
# Check agent status
systemctl --user status boiler-room
# View logs
journalctl --user -u boiler-room -f
# Restart the agent
systemctl --user restart boiler-room
# Edit configuration
nano ~/.config/boiler-room/config.yamlLocated at ~/.config/boiler-room/config.yaml:
device_name: "steamdeck" # Name shown in Home Assistant
api_port: 9451 # API port (default: 9451)
log_level: "info" # Log level: debug, info, warn, error
preferred_browser: "firefox" # Browser for YouTube: firefox, chrome, or Flatpak ID
repo_url: "" # Forgejo/GitHub repo URL for self-updatesJellyfin configuration is managed entirely from the HA UI — no agent-side config needed.
The agent exposes a REST API at http://<device-ip>:9451/api/v1/.
| Endpoint | Method | Description |
|---|---|---|
/status |
GET | Device info, game counts, mode, MAC address |
/games |
GET | List installed Steam games |
/games?refresh=true |
GET | Re-scan and list games |
/apps |
GET | List installed Flatpak apps |
/shortcuts |
GET | List non-Steam shortcuts |
/launch |
POST | Launch a game, app, or URL |
/recent |
GET | Recently launched games and apps |
/games/{appid}/artwork/{type} |
GET | Game artwork (grid, hero, icon, logo) |
/system/sensors |
GET | CPU/GPU temp, battery, volume |
/system/volume |
POST | Set volume level or mute |
/system/power |
POST | Suspend, shutdown, or reboot |
/system/session |
POST | Switch between desktop and gaming mode |
/plugins |
GET | List registered plugins |
/plugins/{name}/action |
POST | Execute a plugin action |
/update/check |
GET | Check for agent updates |
/update/apply |
POST | Apply a pending update |
/ws |
WebSocket | Real-time state events (see below) |
/health |
GET | Health check |
Connect to /api/v1/ws for real-time push events. The agent sends server-side pings every 30 seconds to keep the connection alive.
power_state_changed — fires when the system power state changes (via any method: voice, Steam menu, power button, HDMI-CEC remote, etc.)
{
"type": "power_state_changed",
"timestamp": 1716566400,
"data": {
"power_state": "sleep"
}
}Power state values:
| Value | Meaning |
|---|---|
on |
System is running (also sent on wake from sleep) |
sleep |
System is entering suspend |
shutdown |
System is shutting down |
reboot |
System is rebooting |
The HA integration uses these events to update sensor.<device>_power_state instantly, enabling automations that react to power changes without polling delay (e.g., turning off a TV when the device sleeps).
Voice commands work through HA automations backed by Boiler Room services. The integration ships 3 pre-built automations you can create with one button press — fully editable in the HA UI.
Option A: One-click — Press the "Create Voice Automations" button on your Boiler Room device page. This writes 3 automations to your automations.yaml and reloads — they appear instantly in Settings → Automations, fully editable.
Option B: Copy-paste — Create automations manually using the YAML below. Go to Settings → Automations → Create Automation → ⋮ → Edit in YAML, paste, and save.
System Controls — wake, suspend, shutdown, reboot, volume
alias: "Boiler Room: System Controls"
description: "Voice commands for SteamOS device power and volume control."
mode: single
triggers:
- trigger: conversation
command:
- "(Wake up|Turn on|Power on) the (deck|steam machine|gaming pc)"
id: wake
- trigger: conversation
command:
- "(Suspend|Sleep) the (deck|steam machine|gaming pc)"
- "Put the (deck|steam machine|gaming pc) to sleep"
id: suspend
- trigger: conversation
command:
- "(Turn off|Shut down|Shutdown|Power off) the (deck|steam machine|gaming pc)"
id: shutdown
- trigger: conversation
command:
- "(Reboot|Restart) the (deck|steam machine|gaming pc)"
id: reboot
- trigger: conversation
command:
- "(Set|Change) [the] (deck|steam machine|gaming pc) volume to {volume}"
- "Volume {volume} [on] [the] (deck|steam machine|gaming pc)"
id: volume
actions:
- choose:
- conditions:
- condition: trigger
id: wake
sequence:
- action: boiler_room.wake
response_variable: result
- set_conversation_response: "{{ result.speech | default("Command sent.") }}"
- conditions:
- condition: trigger
id: suspend
sequence:
- action: boiler_room.power
data:
action: suspend
response_variable: result
- set_conversation_response: "{{ result.speech | default("Command sent.") }}"
- conditions:
- condition: trigger
id: shutdown
sequence:
- action: boiler_room.power
data:
action: shutdown
response_variable: result
- set_conversation_response: "{{ result.speech | default("Command sent.") }}"
- conditions:
- condition: trigger
id: reboot
sequence:
- action: boiler_room.power
data:
action: reboot
response_variable: result
- set_conversation_response: "{{ result.speech | default("Command sent.") }}"
- conditions:
- condition: trigger
id: volume
sequence:
- action: boiler_room.set_volume
data:
level: "{{ trigger.slots.volume | int(50) }}"
response_variable: result
- set_conversation_response: "{{ result.speech | default("Command sent.") }}"Games & Apps — launch games and Flatpak apps by voice
alias: "Boiler Room: Games & Apps"
description: "Voice commands for launching Steam games and apps on your SteamOS device."
mode: single
triggers:
- trigger: conversation
command:
- "(Launch|Play|Start|Open|Run|Boot up|Fire up) {game_name}"
- "Put on {game_name}"
- "(Launch|Play|Start|Open|Run) {game_name} on the (deck|steam machine|tv|living room|gaming pc)"
id: launch_game
- trigger: conversation
command:
- "Open {app_name} on the (deck|steam machine|tv|living room|gaming pc)"
- "(Launch|Start) the {app_name} app"
id: launch_app
actions:
- choose:
- conditions:
- condition: trigger
id: launch_game
sequence:
- action: boiler_room.launch_game
data:
name: "{{ trigger.slots.game_name }}"
response_variable: result
- set_conversation_response: "{{ result.speech | default("Command sent.") }}"
- conditions:
- condition: trigger
id: launch_app
sequence:
- action: boiler_room.launch_app
data:
name: "{{ trigger.slots.app_name }}"
response_variable: result
- set_conversation_response: "{{ result.speech | default("Command sent.") }}"Jellyfin Media — play, browse, control playback, YouTube
alias: "Boiler Room: Jellyfin Media"
description: "Voice commands for Jellyfin media playback, browsing, and YouTube."
mode: single
triggers:
- trigger: conversation
command:
- "(Play|Watch|Listen to|Put on) {query} (on|in) (Jellyfin|Jelly Fin|Jellyfish|jelly fin)"
id: jellyfin_play
- trigger: conversation
command:
- "(Play|Watch|Put on) the movie {query} (on|in) (Jellyfin|Jelly Fin|Jellyfish|jelly fin)"
id: jellyfin_play_movie
- trigger: conversation
command:
- "(Play|Watch|Put on) the (show|series|tv show) {query} (on|in) (Jellyfin|Jelly Fin|Jellyfish|jelly fin)"
id: jellyfin_play_series
- trigger: conversation
command:
- "(Play|Listen to|Put on) the album {query} (on|in) (Jellyfin|Jelly Fin|Jellyfish|jelly fin)"
id: jellyfin_play_album
- trigger: conversation
command:
- "(Play|Listen to|Put on) the song {query} (on|in) (Jellyfin|Jelly Fin|Jellyfish|jelly fin)"
id: jellyfin_play_song
- trigger: conversation
command:
- "(Show me|Browse|Look up) {query} (on|in) (Jellyfin|Jelly Fin|Jellyfish|jelly fin)"
- "(Show|Browse|Find|Look up|Go to) the movie {query} (on|in) (Jellyfin|Jelly Fin|Jellyfish|jelly fin)"
- "(Show|Browse|Find|Look up|Go to) the (show|series|tv show) {query} (on|in) (Jellyfin|Jelly Fin|Jellyfish|jelly fin)"
- "(Show|Browse|Find|Look up|Go to) the (album|artist|band) {query} (on|in) (Jellyfin|Jelly Fin|Jellyfish|jelly fin)"
id: jellyfin_browse
- trigger: conversation
command:
- "(Pause|Resume|Unpause) (Jellyfin|Jelly Fin|Jellyfish|jelly fin|the stream|playback)"
id: jellyfin_pause
- trigger: conversation
command:
- "(Stop) (Jellyfin|Jelly Fin|Jellyfish|jelly fin|the stream|playback)"
id: jellyfin_stop
- trigger: conversation
command:
- "(Rewind|Go back|Skip back) (Jellyfin|Jelly Fin|Jellyfish|jelly fin|the stream|playback)"
id: jellyfin_rewind
- trigger: conversation
command:
- "(Fast forward|Skip forward|Skip ahead) (Jellyfin|Jelly Fin|Jellyfish|jelly fin|the stream|playback)"
id: jellyfin_ff
- trigger: conversation
command:
- "Search YouTube for {query}"
- "(Open|Play|Watch) {query} on YouTube"
id: youtube
actions:
- choose:
- conditions:
- condition: trigger
id: jellyfin_play
sequence:
- action: boiler_room.jellyfin_play
data:
query: "{{ trigger.slots.query }}"
response_variable: result
- set_conversation_response: "{{ result.speech | default("Command sent.") }}"
- conditions:
- condition: trigger
id: jellyfin_play_movie
sequence:
- action: boiler_room.jellyfin_play
data:
query: "{{ trigger.slots.query }}"
type: Movie
response_variable: result
- set_conversation_response: "{{ result.speech | default("Command sent.") }}"
- conditions:
- condition: trigger
id: jellyfin_play_series
sequence:
- action: boiler_room.jellyfin_play
data:
query: "{{ trigger.slots.query }}"
type: Series
response_variable: result
- set_conversation_response: "{{ result.speech | default("Command sent.") }}"
- conditions:
- condition: trigger
id: jellyfin_play_album
sequence:
- action: boiler_room.jellyfin_play
data:
query: "{{ trigger.slots.query }}"
type: MusicAlbum
response_variable: result
- set_conversation_response: "{{ result.speech | default("Command sent.") }}"
- conditions:
- condition: trigger
id: jellyfin_play_song
sequence:
- action: boiler_room.jellyfin_play
data:
query: "{{ trigger.slots.query }}"
type: Audio
response_variable: result
- set_conversation_response: "{{ result.speech | default("Command sent.") }}"
- conditions:
- condition: trigger
id: jellyfin_browse
sequence:
- action: boiler_room.jellyfin_browse
data:
query: "{{ trigger.slots.query }}"
response_variable: result
- set_conversation_response: "{{ result.speech | default("Command sent.") }}"
- conditions:
- condition: trigger
id: jellyfin_pause
sequence:
- action: boiler_room.jellyfin_control
data:
action: pause
response_variable: result
- set_conversation_response: "{{ result.speech | default("Command sent.") }}"
- conditions:
- condition: trigger
id: jellyfin_stop
sequence:
- action: boiler_room.jellyfin_control
data:
action: stop
response_variable: result
- set_conversation_response: "{{ result.speech | default("Command sent.") }}"
- conditions:
- condition: trigger
id: jellyfin_rewind
sequence:
- action: boiler_room.jellyfin_control
data:
action: rewind
response_variable: result
- set_conversation_response: "{{ result.speech | default("Command sent.") }}"
- conditions:
- condition: trigger
id: jellyfin_ff
sequence:
- action: boiler_room.jellyfin_control
data:
action: fastforward
response_variable: result
- set_conversation_response: "{{ result.speech | default("Command sent.") }}"
- conditions:
- condition: trigger
id: youtube
sequence:
- action: boiler_room.youtube_search
data:
query: "{{ trigger.slots.query }}"
response_variable: result
- set_conversation_response: "{{ result.speech | default("Command sent.") }}"| Sentence | What It Does |
|---|---|
Wake up the steam machine |
Sends Wake-on-LAN magic packet |
Suspend / Sleep the deck |
Suspends the device |
Put the steam machine to sleep |
Suspends the device |
Turn off / Shut down the deck |
Shuts down the device |
Reboot / Restart the steam machine |
Reboots the device |
Set volume to 50 |
Sets volume (0–100) |
| Sentence | What It Does |
|---|---|
Launch Baldur's Gate 3 |
Fuzzy match + launch game |
Play / Start / Open / Run / Boot up {name} |
Aliases work too (e.g., "bg3") |
Launch {name} on the deck |
Target a specific device |
Open YouTube on the deck |
Launch a Flatpak app (checks aliases first) |
Launch the Firefox app |
Launch by app name |
| Sentence | What It Does |
|---|---|
Play {query} on Jellyfin |
Search + play top result |
Play the movie Fargo on Jellyfin |
With type hint to disambiguate |
Play the show / series {query} on Jellyfin |
Series-specific search |
Play the album {query} on Jellyfin |
Album-specific search |
Play the song {query} on Jellyfin |
Song-specific search |
Show me {query} on Jellyfin |
Navigate to item (no playback) |
Pause / Resume Jellyfin |
Toggle playback |
Stop Jellyfin |
Stop playback |
Rewind / Skip back |
Seek backward |
Fast forward / Skip ahead |
Seek forward |
Search YouTube for {query} |
Open YouTube search on device |
Play {query} on YouTube |
Open YouTube with query |
Device aliases:
deck,steam machine,tv,living room, andgaming pcare interchangeable in all commands. Edit the automation to add your own.
All voice logic is exposed as standard HA services, callable from automations, scripts, or Developer Tools:
| Service | Parameters |
|---|---|
boiler_room.launch_game |
name, optional device |
boiler_room.launch_app |
name, optional device |
boiler_room.jellyfin_play |
query, optional type |
boiler_room.jellyfin_browse |
query, optional type |
boiler_room.jellyfin_control |
action (pause/unpause/stop/rewind/fastforward) |
boiler_room.youtube_search |
query, optional device |
boiler_room.power |
action (suspend/shutdown/reboot) |
boiler_room.wake |
optional device |
boiler_room.set_volume |
level (0–100) |
All services return a speech response variable for use with set_conversation_response.
If Jellyfin voice caching is enabled (Settings → Integrations → Boiler Room → Configure), the integration caches your library and matches voice queries phonetically. This handles artists and albums with unusual spellings that STT engines mangle:
| You say | STT transcribes | Cache matches |
|---|---|---|
| "Play deadmau5" | "dead mouse" | deadmau5 (82%) |
| "Play Nine Inch Noize" | "nine inch noise" | Nine Inch Noize (93%) |
| "Play Gorillaz" | "gorillas" | Gorillaz (88%) |
| "Play Mötley Crüe" | "motley crew" | Mötley Crüe (91%) |
Make sure you imported the blueprint and created an automation from it. The blueprint alone doesn't do anything — you need to create an automation that uses it.
The WoL button should always be available. If it's disabled, restart HA — this was fixed in v0.5.0 by decoupling the WoL button from the coordinator.
The Jellyfin app needs to be running on the SteamOS device. The integration will auto-launch it, but if the app crashes or isn't installed, you'll see this error. Install Jellyfin Desktop via Flatpak: flatpak install org.jellyfin.JellyfinDesktop.
Try saying the full name. Partial matches work but can be ambiguous. Check journalctl --user -u boiler-room -f on the SteamOS device to see what the agent received.
MIT
Built with Opus