How to upgrade odio and check for releases
From Home Assistant and the web UI
Section titled “From Home Assistant and the web UI”The simplest way to check for and apply upgrades is from Home Assistant or the embedded web UI, no SSH required. Both are backed by go-odio-api (v0.15.1), which exposes the odio-check-upgrade and odio-upgrade units, and /var/cache/odio/upgrades.json, over its API. A full odios install wires this up automatically since 2026.6.0b2.
In the embedded web UI, a status badge sits in the header:
- a check mark when the node is up to date, click to re-check;
- an up arrow when an upgrade is available, click to start it (with a confirmation);
- a progress ring while a run is in flight;
- a red alert badge when the last run failed, hover to reveal the up arrow and retry; it persists until the next run.

In Home Assistant, the node carries a native Firmware update entity (update.odio_remote_<hostname>_firmware). It tracks the installed and latest versions, shows an Install button when the server allows starting an upgrade, and reports live progress during a run. Detection is server-driven over SSE, so HA never polls.

Both reflect whatever the node’s detector reports and trigger the same units, see the upgrade API for the contract behind them.
Check for upgrades from the command line
Section titled “Check for upgrades from the command line”Each node shows its update status in the MOTD when you SSH in:

The notice lists the overall version bump and the roles whose version actually changed. If nothing changed, the notice is absent.
A systemd user timer (odio-check-upgrade.timer) reruns the check daily. Run it on demand to see the result on stdout:
odio@raspodio:~ $ odio-upgrade checkUpgrades available: dev-8e978895 → 2026.5.0b1Or trigger the user unit, with the result landing in the user journal:
systemctl --user start odio-check-upgrade.serviceApply an upgrade
Section titled “Apply an upgrade”Trigger the systemd user unit:
systemctl --user start odio-upgrade.serviceIt re-runs the installer with the feature selection from the previous run. Since 2026.5.0b1, it only re-runs the roles that actually changed in the target release (smart upgrade), bringing most upgrades down to a handful of roles.
For flags, call the script directly:
odio-upgrade apply # same as the unitodio-upgrade apply --version 2026.5.0b1 # pin a release tagodio-upgrade apply --dry-run --force # print the derived env, no changesOpen PRs are published as pr-<N> pre-releases (see CI/CD), so testing a PR before merge is the same call:
odio-upgrade apply --version pr-52The installer is idempotent and safe to re-run. When a config file has been locally modified, it creates a <config>.bak before applying changes; if the file ends up identical, no backup is kept (Bluetooth, MPD, mpd-discplayer, odio-api, PipeWire, shairport-sync, spotifyd, upmpdcli).
Bootstrap (cold install)
Section titled “Bootstrap (cold install)”Fresh install:
curl -fsSL https://beta.odio.love/install | bashPin a release tag:
curl -fsSL https://github.com/b0bbywan/odios/releases/download/2026.5.0b1/install.sh | ODIOS_VERSION=2026.5.0b1 bashIf the node was installed before 2026.4.2b1, it lacks the odio-upgrade helper. Each release ships it as a standalone asset, so it can run before being installed:
curl -fsSL https://odio.love/upgrade -o /tmp/odio-upgradechmod +x /tmp/odio-upgrade/tmp/odio-upgradeThe next run finds the helper installed in /usr/local/bin and the state file at /var/lib/odio/state.json.
Opt out of a role or feature
Section titled “Opt out of a role or feature”odio-upgrade is opt-out: only entries listed in state.json map to INSTALL_*=N. Everything else, including roles or features added in a later release, is installed.
Edit /var/lib/odio/state.json and add the entry to the matching _excluded list, for example to keep branding and upnpwebradios off:
"roles_excluded": [ "snapclient", "spotifyd" "snapclient", "spotifyd", "branding"],"features_excluded": [ "tidal" "tidal", "upnpwebradios"]Verify and apply:
odio-upgrade apply --dry-run --force # print the derived INSTALL_* flagsodio-upgrade applyRemoving an entry opts back in.
State files behind the upgrade flow
Three files back the detection and upgrade flow:
-
Local state —
/var/lib/odio/state.json, written by the installer after each successful run and read byodio-upgrade apply. Tracks the odios version, install mode, target user, per-role versions, opt-in features, explicit opt-outs, and arelease_historyof the last odios versions installed on this node. The file is0640 root:odio:{"features": ["tidal","qobuz","upnpwebradios","mympd"],"features_excluded": [],"install_mode": "live","odios": "2026.5.0b1","release_history": ["2026.4.2b2","2026.5.0b1"],"roles": {"bluetooth": "2026.5.0b1","branding": "2026.5.0b1","common": "2026.5.0b1","mpd": "2026.5.0b1","mpd_discplayer": "2026.5.0b1","odio_api": "2026.5.0b1","pulseaudio": "2026.4.2b1","shairport_sync": "2026.4.1rc1","snapclient": "2026.4.0rc5","spotifyd": "2026.5.0b1","upgrade": "2026.5.0b1","upmpdcli": "2026.4.2b2"},"roles_excluded": [],"target_user": "odio"} -
Published manifest — odio.love/manifest.json, generated by CI on each release with the same shape. Each role version is the last odios release that touched that role.
-
Check result —
/var/cache/odio/upgrades.json, written byodio-upgrade check(the daily timer’s job) and read by the MOTD:{"current": "2026.4.2b2","latest": "2026.4.2b2","upgrade_available": false,"roles": [],"manifest": {"odios": "2026.4.2b2","roles": {"bluetooth": "2026.4.0rc5","branding": "2026.4.2b1","common": "2026.4.2b2","mpd": "2026.4.2b2","mpd_discplayer": "2026.4.2b1","odio_api": "2026.4.2b2","pipewire": "2026.4.0rc6","pulseaudio": "2026.4.2b1","shairport_sync": "2026.4.1rc1","snapclient": "2026.4.0rc5","spotifyd": "2026.4.2b1","upgrade": "2026.4.2b2","upmpdcli": "2026.4.2b2"}},"checked_at": "2026-05-07T19:32:34Z"}