Wireless Link Adapter on firmware 20: pair-account binds the account, but the device stays filed under default

#780 · open · 0 comments

View on GitHub ↗

bitranox

Follow-up to #660, as suggested there. **Setup.** AfterTouch v0.138.0 in Docker with host networking. At one site there is a SoundTouch Wireless Link Adapter on firmware `20.0.6.44144.3075410` and a SoundTouch 20 on `27.0.6`. Both were migrated over telnet and both play radio. **What happens.** The adapter was bound with `pair-account` (on v0.137.1 the HTTP call answered 500 and the telnet fallback did the binding). It uses the account from then on: it fetches `/streaming/account/{id}/full`. Its `/info` still leaves `margeAccountUUID` empty: ```xml <info deviceID="..."> <name>...</name> <type>SoundTouch Wireless Link Adapter</type> <margeAccountUUID></margeAccountUUID> <components> <component> <componentCategory>SCM</componentCategory> <softwareVersion>20.0.6.44144.3075410 epdbuild.trunk.hepdswbld05.2018-11-06T16:50:30</softwareVersion> ... </component> ... </components> <margeURL>http://<service>:8000</margeURL> ... <moduleType>sm2</moduleType> <variant>binky</variant> <variantMode>normal</variantMode> </info> ``` So the service files it under `default`. `/api/setup/devices` reports `"account_id": "default"` for the adapter and the real account id for the SoundTouch 20, and on disk the adapter sits in `accounts/default/devices/`. The adapter also never PUTs presets or POSTs recents, so there is no stored `Presets.xml` for it and preset sharing skips it. **Where it comes from (v0.138.0).** The only place that moves a device between accounts is the discovery path in `pkg/service/handlers/server.go`, around line 1767: ```go if liveInfo.MargeAccountUUID != "" && storedAccount != "" && liveInfo.MargeAccountUUID != storedAccount { if err := s.ds.MoveDevice(storedAccount, accountID, deviceID); err != nil { ``` With `margeAccountUUID` empty that condition never holds, so a device that was first seen under `default` stays there. `PairAccount` (called from `handlers_pairing.go:103` and `:170`) changes the speaker and does not touch the datastore. **What firmware 20 fetches.** These are the request lines recorded with `User-Agent: Bose_Lisa/20.0.6` across 27 recording sessions (2026-09-05 to 2026-09-29). The recorder does not keep every request, so read this as the set of paths the box uses, not as counts: ``` GET /bmx/registry/v1/services POST /streaming/support/power_on GET /streaming/sourceproviders GET /streaming/device/{device_id}/streaming_token GET /streaming/account/{accountId}/full GET /bmx/tunein/ GET /core02/svc-bmx-adapter-orion/prod/orion/ GET /core02/svc-bmx-adapter-orion/prod/orion/station GET /core02/svc-bmx-adapter-siriusxm-everest-eco1/prod/live-adapter/ GET /custom/v1/playback/<base64url> GET /media/bmx-icons/{tunein,orion,siriusxm-everest}/... ``` No `PUT .../presets` and no `POST .../recents` in any of them. Its `/sources` lists `TUNEIN` and `LOCAL_INTERNET_RADIO` as READY and has no `RADIO_BROWSER` entry at all. **Relative presets work on it.** I stored a preset in the v0.138.0 relative form (`/station?data=...`) with `storePreset` and pressed it: the adapter fetched `/core02/svc-bmx-adapter-orion/prod/orion/station` and reached `PLAY_STATE` on the right station. So the relative form resolves on firmware 20 as well, at least for stored presets. **Possible fix.** `pair-account` knows both the device and the account, so filing the device under that account once pairing succeeds would cover this box, as you suggested. A second signal would also catch devices paired before such a change: a device filed under `default` that fetches `/streaming/account/{id}/full` has just told the service which account it uses. I can test a build on this adapter.

Comments