Music Assistant isn't finding Sendspin (ESP32 / HA Voice PE) players.
MA persists publish_ip (the literal IP advertised to players) and bind_ip in /data/settings.json. A prior hotfix pinned both to 10.2.30.137, because MA's auto-detect picks the mgmt default-route IP (eth1/v30iot carries routeMetric 4096, so it's never the default route) — which IoT players can't reach.
MA now runs on k8s-2, whose DHCP eth1 lease is 10.2.30.193, so the stored .137 is no longer a local address. On startup:
the stream server falls back to all-interfaces and survives;
the Sendspin server (aiosendspin, :8927) has no fallback — it fails to bind and dies, and sendspin_proxy.py then targets the dead .137 instead of 127.0.0.1. Result, on every player request:
ERROR [aiosendspin.server.server] Failed to start server on 10.2.30.137:8927: could not bind on any address
ERROR [music_assistant.sendspin_proxy] Failed to connect to internal Sendspin server
eth1 is DHCP and MA is not node-pinned, so any stored IP goes stale on the next reschedule or lease change — pinning an IP just recreates this bug.
Fix
A boot-time initContainer that, before MA starts:
discovers this node's current10.2.30.0/24 address from the live interface (hostNetwork sees eth1 directly; selects by subnet, not a fixed IP or interface name);
writes publish_ip=<that IP> and bind_ip=0.0.0.0 into core/streams/values in settings.json (idempotent; preserves all other keys; atomic replace).
bind_ip=0.0.0.0 is MA's own default and makes the Sendspin server bind all interfaces + its proxy use loopback, so it can never die on a bad bind again. publish_ip is the only genuinely dynamic value, recomputed every boot — no static IP, no node pinning, no DHCP reservation.
Reuses the MA image (Python stdlib only; the $imagepolicy marker keeps the init tag in lockstep with the main container) and fails closed if no v30iot address is found, so MA never advertises a wrong IP.
Single deployment, Recreate strategy. After merge/reconcile, confirm the reconcile-publish-ip initContainer logs the discovered IP and the change, and that Sendspin players reappear, before considering it done.
## Problem
Music Assistant isn't finding Sendspin (ESP32 / HA Voice PE) players.
MA persists `publish_ip` (the literal IP advertised to players) and `bind_ip` in `/data/settings.json`. A prior hotfix pinned both to `10.2.30.137`, because MA's auto-detect picks the **mgmt default-route IP** (eth1/v30iot carries `routeMetric 4096`, so it's never the default route) — which IoT players can't reach.
MA now runs on **k8s-2**, whose DHCP `eth1` lease is `10.2.30.193`, so the stored `.137` is no longer a local address. On startup:
- the **stream server** falls back to all-interfaces and survives;
- the **Sendspin server** (`aiosendspin`, `:8927`) has **no fallback** — it fails to bind and dies, and `sendspin_proxy.py` then targets the dead `.137` instead of `127.0.0.1`. Result, on every player request:
```
ERROR [aiosendspin.server.server] Failed to start server on 10.2.30.137:8927: could not bind on any address
ERROR [music_assistant.sendspin_proxy] Failed to connect to internal Sendspin server
```
`eth1` is DHCP and MA is **not** node-pinned, so any stored IP goes stale on the next reschedule or lease change — pinning an IP just recreates this bug.
## Fix
A boot-time `initContainer` that, before MA starts:
1. discovers this node's **current** `10.2.30.0/24` address from the live interface (hostNetwork sees `eth1` directly; selects by *subnet*, not a fixed IP or interface name);
2. writes `publish_ip=<that IP>` and `bind_ip=0.0.0.0` into `core/streams/values` in `settings.json` (idempotent; preserves all other keys; atomic replace).
`bind_ip=0.0.0.0` is MA's own default and makes the Sendspin server bind all interfaces + its proxy use loopback, so it can never die on a bad bind again. `publish_ip` is the only genuinely dynamic value, recomputed every boot — no static IP, no node pinning, no DHCP reservation.
Reuses the MA image (Python stdlib only; the `$imagepolicy` marker keeps the init tag in lockstep with the main container) and **fails closed** if no v30iot address is found, so MA never advertises a wrong IP.
## Validation
- `kubectl kustomize` renders cleanly (ConfigMap + Service + Deployment + Ingress).
- `reconcile.py` syntax-checked; fail-closed path exercised; JSON patch + idempotency tested against a mock `settings.json` (sibling keys survive, `.137` -> host IP, `bind_ip` -> `0.0.0.0`).
## Rollout note
Single deployment, `Recreate` strategy. After merge/reconcile, confirm the `reconcile-publish-ip` initContainer logs the discovered IP and the change, and that Sendspin players reappear, before considering it done.
Music Assistant persists publish_ip (the literal IP advertised to players)
and bind_ip in /data/settings.json. A prior hotfix pinned both to
10.2.30.137 because MA's auto-detect picks the mgmt default-route IP (eth1
carries routeMetric 4096, so v30iot is never the default route), which IoT
players can't reach.
After MA moved to k8s-2, whose DHCP eth1 lease is 10.2.30.193, the stored
.137 is no longer local. The stream server falls back to all-interfaces and
survives, but the Sendspin server (aiosendspin :8927) has no fallback: it
fails to bind and dies, and its proxy then targets the dead .137 instead of
loopback -> Sendspin/Voice PE players never register.
eth1 is DHCP and MA is not node-pinned, so any stored IP goes stale on the
next reschedule or lease change. Instead of pinning an IP, an initContainer
recomputes the current v30iot (10.2.30.0/24) address from the live interface
on every boot and writes publish_ip=<that IP> and bind_ip=0.0.0.0 into
settings.json before MA reads it. hostNetwork means it sees eth1 directly;
it reuses the MA image (stdlib only) and fails closed if no v30iot address
is present, so MA never comes up advertising a wrong IP.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Problem
Music Assistant isn't finding Sendspin (ESP32 / HA Voice PE) players.
MA persists
publish_ip(the literal IP advertised to players) andbind_ipin/data/settings.json. A prior hotfix pinned both to10.2.30.137, because MA's auto-detect picks the mgmt default-route IP (eth1/v30iot carriesrouteMetric 4096, so it's never the default route) — which IoT players can't reach.MA now runs on k8s-2, whose DHCP
eth1lease is10.2.30.193, so the stored.137is no longer a local address. On startup:aiosendspin,:8927) has no fallback — it fails to bind and dies, andsendspin_proxy.pythen targets the dead.137instead of127.0.0.1. Result, on every player request:eth1is DHCP and MA is not node-pinned, so any stored IP goes stale on the next reschedule or lease change — pinning an IP just recreates this bug.Fix
A boot-time
initContainerthat, before MA starts:10.2.30.0/24address from the live interface (hostNetwork seeseth1directly; selects by subnet, not a fixed IP or interface name);publish_ip=<that IP>andbind_ip=0.0.0.0intocore/streams/valuesinsettings.json(idempotent; preserves all other keys; atomic replace).bind_ip=0.0.0.0is MA's own default and makes the Sendspin server bind all interfaces + its proxy use loopback, so it can never die on a bad bind again.publish_ipis the only genuinely dynamic value, recomputed every boot — no static IP, no node pinning, no DHCP reservation.Reuses the MA image (Python stdlib only; the
$imagepolicymarker keeps the init tag in lockstep with the main container) and fails closed if no v30iot address is found, so MA never advertises a wrong IP.Validation
kubectl kustomizerenders cleanly (ConfigMap + Service + Deployment + Ingress).reconcile.pysyntax-checked; fail-closed path exercised; JSON patch + idempotency tested against a mocksettings.json(sibling keys survive,.137-> host IP,bind_ip->0.0.0.0).Rollout note
Single deployment,
Recreatestrategy. After merge/reconcile, confirm thereconcile-publish-ipinitContainer logs the discovered IP and the change, and that Sendspin players reappear, before considering it done.