Skip to content

Configure the odio API with config.yaml drop-ins

The odio installer generates ~/.config/odio-api/config.yaml automatically. The configuration shown below documents all available options — you only need to edit the file if you want to customize the defaults.

For standalone installations, the config file can also be specified with --config <path>.

bind: lo
logLevel: info
api:
enabled: true
port: 8018
ui:
enabled: true
cors:
origins: ["https://pwa.odio.love", "https://odio-pwa.vercel.app"]

The api block configures the HTTP server itself: port, embedded UI, SSE stream, and browser CORS. Backends are configured separately, below.

api:
enabled: true
port: 8018
ui:
enabled: true
# adminSocket: /run/user/1000/odioctl-web.sock
sse:
enabled: true
cors:
origins:
- https://pwa.odio.love
- https://odio-pwa.vercel.app
KeyDefaultNotes
enabledtrueDisable to stop the server entirely.
port8018TCP port. Combined with bind to form the listen addresses.
ui.enabledtrueServes the embedded web UI at /ui. Auto-disabled at startup if bind does not include lo (the UI is loopback-only by design).
ui.adminunsetOptional link to an admin web app, shown as a gear icon in the UI header. Same forms as a service url in the systemd whitelist: ":8021" resolves against the node’s own address. A Unix socket here is ignored with a warning, see ui.adminSocket. Since v0.17.0.
ui.adminSocketunsetAn admin web app listening on a Unix socket, proxied at /ui/admin/ with X-Forwarded-Prefix. While the socket exists, the gear icon leads there rather than to ui.admin. odios points it at the settings page. Since v0.17.4.
sse.enabledtrueServes the SSE event stream at /events. The embedded UI and the PWA both rely on it for real-time updates.
cors.originsPWA defaultsBrowser origins allowed to call the API, see below.

The default cors.origins covers the two hosted PWA addresses:

  • https://pwa.odio.love
  • https://odio-pwa.vercel.app

bind controls which network interfaces the API listens on.

bind: lo # loopback only (default)
# bind: enp2s0 # single LAN interface
# bind: [lo, enp2s0] # loopback + LAN
# bind: [lo, enp2s0, wlan0] # loopback + ethernet + wifi
# bind: all # all interfaces (Docker, remote access)

Since odio-api v0.12.0, ~/.config/odio-api/conf.d/ is loaded automatically: any *.yml file in that directory is merged on top of config.yaml at startup, so you can layer overrides on top of a base config without editing it. Within the systemd block, the system and user arrays are replaced wholesale by an override, not merged item by item, so the override must list every entry you want exposed.

On odio streamer installs, the installer creates this directory and drops a 10-systemd.yml.default reference matching the host’s installed services, regenerated on every upgrade run. The .default extension keeps it ignored at runtime. To use it as a starting point, copy it under a .yml name and edit:

Terminal window
cd ~/.config/odio-api/conf.d
cp 10-systemd.yml.default 50-my-systemd.yml
$EDITOR 50-my-systemd.yml
systemctl --user restart odio-api

Each backend can be enabled or disabled independently. Disabling a backend removes all its routes from the API.

See Systemd for endpoint details.

systemd:
enabled: true
timeout: 90s
system:
- name: bluetooth.service
user:
- name: mpd.service
url: ":8080"
open: self
- name: shairport-sync.service
- name: snapclient.service
url: "http://<snapserver>:1780"
open: panel
- name: spotifyd.service
- name: upmpdcli.service

Since odio-api v0.12.0, each whitelist entry is a map with a name and an optional url. The URL is exposed on the /services response so clients (the embedded UI, PWA, Home Assistant) can render a direct link, which is how myMPD shows up alongside mpd.service on installs that ship it.

Since v0.17.4, an optional open tells clients where to open that URL:

openBehavior in the embedded UI
tab (default)New browser tab
panelPanel over the dashboard, a side panel on larger screens and a bottom sheet on phones
selfCurrent tab, replacing the dashboard

open only applies to an entry with a url: without one it is ignored with a warning. An unknown value also logs a warning and falls back to tab. The resolved value is exposed as open on /services.

See Bluetooth for endpoint details.

bluetooth:
enabled: true
powerOnStart: false
timeout: 5s
pairingTimeout: 60s
idleTimeout: 30m
scanTimeout: 60s

Since odio doesn’t run as root, a few system configuration steps are required.

The user running odio must belong to the bluetooth group:

Terminal window
sudo usermod -a -G bluetooth <username>

PulseAudio or PipeWire must be configured to handle Bluetooth audio:

Terminal window
# PulseAudio
sudo apt install pulseaudio-module-bluetooth
# PipeWire
sudo apt install libspa-0.2-bluetooth

Edit /etc/bluetooth/main.conf to identify the node as an audio receiver:

[General]
Name = odio
Class = 0x240428
[Policy]
AutoEnable=false

Class of Device (0x240428) breakdown:

  • 0x24 — Major Device Class: Audio/Video
  • 0x0428 — Minor + services: Audio Sink, Loudspeaker, Rendering device

This makes the node appear as a standard Bluetooth speaker. Phones and computers will show a headphone icon and route media audio to it.

AutoEnable=false keeps the adapter off at boot — power is managed by the API (or Home Assistant). To have the API power it on when the service starts, set bluetooth.powerOnStart: true (since v0.13.1).

After modifying the configuration, restart the Bluetooth service:

Terminal window
sudo systemctl restart bluetooth

BlueZ provides mpris-proxy, a systemd user service that creates an MPRIS player for each connected Bluetooth device. This is what allows the odio API to discover and control Bluetooth playback alongside all other sources.

Terminal window
systemctl --user status mpris-proxy.service

See Power for endpoint details.

power:
enabled: true
capabilities:
poweroff: true
reboot: true

On headless systems, logind requires a polkit rule to allow the odio user to reboot and power off without a graphical session:

/etc/polkit-1/rules.d/10-allow-shutdown.rules
polkit.addRule(function(action, subject) {
if ((action.id == "org.freedesktop.login1.power-off" ||
action.id == "org.freedesktop.login1.power-off-multiple-sessions" ||
action.id == "org.freedesktop.login1.reboot" ||
action.id == "org.freedesktop.login1.reboot-multiple-sessions") &&
subject.user == "odio") {
return polkit.Result.YES;
}
});

On desktop systems with a graphical session, this rule is not needed — logind already grants these permissions.

See Input for devices, bindings and actions. Since v0.18.0.

input:
enabled: true
devices:
- "flirc.tv flirc"
bindings:
- { key: KEY_POWER, action: power.poweroff }

The user running odio must belong to the input group, and the node must be rebooted for the group to apply:

Terminal window
sudo usermod -aG input <username>

See Zeroconf for details. Requires bind to be set to a real network interface.

bind: enp2s0
zeroconf:
enabled: true