Compare commits

...
163 Commits
Author SHA1 Message Date
Tobias Gesellchen 49ec2d58db wip 2026-03-07 19:51:19 +01:00
Tobias Gesellchen c78664ee59 wip 2026-03-07 19:51:19 +01:00
Tobias Gesellchen c17ba0e839 Add Stockholm Mini (3)
This is also a refactoring of our api paths
2026-03-07 19:51:19 +01:00
Tobias Gesellchen 17f052308f wip 2026-03-07 19:51:19 +01:00
Tobias Gesellchenandlnx01 2e9f931797 Potential fix for code scanning alert no. 8: Workflow does not contain permissions
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-03-07 15:02:57 +01:00
Tobias Gesellchenandlnx01 41378f720b Potential fix for code scanning alert no. 7: Workflow does not contain permissions
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-03-07 15:02:15 +01:00
Tobias Gesellchenandlnx01 c87a28f3ba Potential fix for code scanning alert no. 4: Workflow does not contain permissions
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-03-07 15:01:03 +01:00
Tobias Gesellchenandlnx01 df18749220 Potential fix for code scanning alert no. 1: Workflow does not contain permissions
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-03-07 14:55:55 +01:00
Tobias Gesellchenandlnx01 b6702cd4b5 Potential fix for code scanning alert no. 2: Workflow does not contain permissions
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-03-07 14:55:55 +01:00
Tobias GesellchenandJunie fc5de2bbc7 refactor: replace deprecated httputil.ReverseProxy.Director with Rewrite
- Update pkg/service/handlers/handlers_proxy.go and mirror_middleware.go to
  use the modern httputil.ReverseProxy.Rewrite hook (available since Go 1.20).
- Fix SA1019 staticcheck warnings triggered by Go 1.26 deprecation notice.
- Refactor proxy initialization to avoid NewSingleHostReverseProxy to prevent
  conflicts between Director and Rewrite hooks.
- Standardize request modification using ProxyRequest.SetURL and ProxyRequest.Out.

Co-authored-by: Junie <junie@jetbrains.com>
2026-03-07 12:58:01 +01:00
Tobias GesellchenandJunie cf82feca06 security: upgrade Go to 1.26.1 and update dependencies
- Update Go version to 1.26.1 in go.mod and examples to address:
  - GO-2026-4602 (os: FileInfo escape)
  - GO-2026-4601 (net/url: IPv6 host literal parsing)
  - GO-2026-4600 (crypto/x509: panic in name constraint checking)
  - GO-2026-4599 (crypto/x509: incorrect email constraint enforcement)
- Upgrade golang.org/x/* and other dependencies to latest stable versions.
- Synchronize go.sum via go mod tidy.

Co-authored-by: Junie <junie@jetbrains.com>
2026-03-07 12:58:01 +01:00
Tobias Gesellchen b8bbc52803 Refine migration guide (#100) 2026-03-07 12:56:25 +01:00
Tobias Gesellchen a36c2e4629 Prevent browser freeze for large payloads (#101) 2026-03-07 12:56:06 +01:00
Tobias Gesellchen d15cebdc95 Work around Jekyll/Liquid template engine issues
Fix for:

```
  Liquid Exception: Liquid syntax error (line 27): Tag '{% // Response: 200 OK %}' was not properly terminated with regexp: /\%\}/ in REQUEST_RECORDING_CONCEPT.md
/usr/local/bundle/gems/liquid-4.0.4/lib/liquid/block_body.rb:132:in `raise_missing_tag_terminator': Liquid syntax error (line 27): Tag '{%  (Liquid::SyntaxError)
    // Response: 200 OK
%}' was not properly terminated with regexp: /\%\}/
```
2026-03-06 21:57:38 +01:00
Tobias Gesellchen d296b59a9e Add/update docs. Some are only in preparation for future improvements and features (#99) 2026-03-06 21:50:41 +01:00
Tobias Gesellchen d2aaed0f9f View parity mismatches as diff (#98) 2026-03-06 21:26:24 +01:00
Tobias Gesellchen eb50e9b6f6 Decode SCMUDC event details (#97)
This should help understanding events from the SoundTouch app to the
speakers and from speakers to the BMX service.
2026-03-05 23:19:39 +01:00
dependabot[bot] 1e24ca076a ci(deps): bump the actions-core group with 2 updates
Bumps the actions-core group with 2 updates: [actions/upload-artifact](https://github.com/actions/upload-artifact) and [actions/download-artifact](https://github.com/actions/download-artifact).


Updates `actions/upload-artifact` from 6 to 7
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v6...v7)

Updates `actions/download-artifact` from 7 to 8
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v7...v8)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions-core
- dependency-name: actions/download-artifact
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions-core
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-03 22:21:24 +01:00
dependabot[bot] b19835427b deps(deps): bump golang.org/x/net in the golang group
Bumps the golang group with 1 update: [golang.org/x/net](https://github.com/golang/net).


Updates `golang.org/x/net` from 0.50.0 to 0.51.0
- [Commits](https://github.com/golang/net/compare/v0.50.0...v0.51.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.51.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: golang
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-03 22:21:15 +01:00
Tobias Gesellchen 6ee0fc8115 Remove Soundcork fallback 2026-02-26 22:26:35 +01:00
Tobias Gesellchen 6211e34050 Improve parity with upstream Bose services 2026-02-26 21:08:14 +01:00
Tobias Gesellchen 2132674768 Fix bmx base url 2026-02-26 21:08:14 +01:00
Tobias Gesellchen b4c015ef75 Restrict UPnP timeout 2026-02-26 21:08:14 +01:00
Tobias Gesellchen 5d22a53c8b Fix Content-Type and status code in Marge AddRecent handler
- Reorder header setting and WriteHeader calls in HandleMargeAddRecent to ensure Content-Type is correctly sent.
- Update HandleMargeAddRecent to explicitly use 201 Created status code.
- Improve parity mismatch logging to correctly capture headers from local handlers.
- Enhance mirroring logic to support local testing of upstream parity.
2026-02-26 21:08:14 +01:00
Tobias Gesellchen f4268f3111 Use BuildKit's build args 2026-02-26 09:43:14 +01:00
Tobias Gesellchen 71fd9c1531 Build and publish cross-platform Docker images 2026-02-26 08:43:35 +01:00
Tobias Gesellchen 53184a6bca reduce log noise 2026-02-24 22:20:13 +01:00
Tobias Gesellchen d97cd45b22 Relax TestMACMappingPerformance limit 2026-02-24 21:52:27 +01:00
Tobias Gesellchen 5edab77209 feat: add comprehensive TLS certificate SAN support with wildcard domains
- Add RFC-compliant wildcard certificates (*.api.bose.io, *.api.bosecm.com) for automatic API coverage
- Include additional Bose production domains (worldwide.bose.com, music.api.bose.com, bose-prod.apigee.net)
- Implement TLS certificate request logging and wildcard domain matching logic
- Add detailed TLS handshake debugging with connection state tracking
- Wrap TLS listener with logging to capture certificate selection and handshake failures
- Update documentation with wildcard certificate coverage and debugging features
- Normalize test data to use consistent local IP addresses

This enables automatic coverage of all current and future Bose API subdomains
while providing comprehensive TLS debugging for DNS redirection troubleshooting.
2026-02-24 21:47:20 +01:00
Tobias Gesellchen a1d0213f92 refactor: reorganize device directories to use true deviceId from /info endpoint
- Replace serial number-based directory structure with deviceId from device /info
- Extract migration logic to handle transition from old to new directory structure
- Fix directory resolution bug that prevented proper migration to deviceId-based paths
- Ensure all device data (Presets.xml, Sources.xml, Recents.xml) preserved during transition
- Add configurable migration with --migration-enabled and --migration-dry-run flags
- Update DeviceInfo.xml to reflect authoritative deviceId from device's /info endpoint
- Directory structure now: /devices/{deviceId}/ instead of /devices/{serialNumber}/

This aligns the directory structure with the device's self-declared identity
and ensures data consistency with the device's /info endpoint.
2026-02-24 21:45:40 +01:00
Tobias Gesellchen 0b75a2f70d feat: implement robust MAC address to serial number mapping
Enhances device identification by adding MAC address normalization and comprehensive documentation.

- Add `MAC-ADDRESS-MAPPING.md` guide explaining device identification and troubleshooting.
- Implement `normalizeMAC` in `DataStore` to handle various MAC formats (case-insensitive, with/without separators).
- Export `EnrichDeviceInfo` in UPnP discovery to allow better integration and testing.
- Update `TROUBLESHOOTING.md` with a new section on device identification issues.
- Add comprehensive integration and diagnostic tests for MAC mapping, case sensitivity, and UPnP discovery.
- Update documentation structure (`README.md`, `SUMMARY.md`) to include the new mapping guide.
2026-02-24 21:45:40 +01:00
Tobias Gesellchen 0090746b89 refactor: update recording filename format to include date
- Update `getRecordingPath` to use a timestamp format that includes the date (`20060102-150405.000`).
- Update `parseInteractionFile` and `getFullTimestamp` to handle both the new filename format and the legacy format for backward compatibility.
- Improved parsing logic to reliably extract date, time, and HTTP method from interaction filenames.
2026-02-24 11:49:04 +01:00
Tobias Gesellchen be762dbc22 test(discovery): optimize discovery tests for faster execution
Reduces `pkg/discovery` test suite runtime by ~75% (from ~17s to ~4s) by eliminating unnecessary network timeouts and reducing wait intervals.

- Refactor `discovery.Service` to use an injectable `http.Client`, allowing UPnP enrichment tests to use `httptest.Server` instead of waiting for 5s network timeouts.
- Make `DNSDiscovery` forward timeout configurable and reduce it from 2s to 100ms in unit tests.
- Decrease discovery and context timeouts in mDNS and Unified discovery tests to the minimum required for stable verification (typically 100-200ms).
2026-02-22 23:40:51 +01:00
Tobias Gesellchen 403e2275dc fix(datastore): resolve local data directory using MAC address mapping
Fixes an issue where device data (e.g., Presets.xml) could not be located when accessed via MAC address because the internal directory structure is organized by serial number.

- Add a `macToSerial` mapping in `DataStore` to bridge MAC addresses from API requests to internal serial-numbered directories.
- Implement automatic mapping population during `DataStore` initialization by scanning `DeviceInfo.xml` files.
- Update `AccountDeviceDir` to transparently resolve MAC addresses to serial numbers for file path construction.
- Enhance UPnP discovery to capture the MAC address (as `serialNumber` in the device description) for better device identification.
- Include automated tests for MAC-to-serial resolution and UPnP enrichment.
2026-02-22 23:40:51 +01:00
Tobias Gesellchen 9ee1c96477 feat(mirror): add background mirroring and parity analysis for Bose services
Implements the ability to mirror local requests to the official Bose
Cloud in the background, allowing for real-time comparison and parity
analysis between the emulated service and the original backend.

Core Changes:
- Implement `MirrorMiddleware` for asynchronous and synchronous mirroring.
- Add `Parity Logger` to detect discrepancies in status, headers, and body.
- Implement storage for parity mismatches in `data/parity_mismatches/`.
- Add `Internal Paths` configuration to exclude management traffic from logs.

Web UI & API:
- Add "Parity & Mirroring" tab to the Web UI for discrepancy analysis.
- Integrated "Internal Paths" configuration in Settings.
- Add "mirror" category filter to the Interactions UI.
- Implement endpoints for listing and clearing parity mismatches.

Infrastructure & Tools:
- Extend `setup.Manager` with `HTTPGet` override for reliable testing.
- Add CLI flags `--mirror-enabled`, `--mirror-endpoints`, and `--internal-paths`.
- Update `datastore.Settings` to persist mirroring and internal path configurations.

Tests:
- Add `pkg/service/handlers/mirror_test.go` for middleware verification.
- Update `TestProxySettingsAPI` and `TestRecordMiddleware` for new settings.
- Refactor `TestMigrationAndCA` to use mocked network calls (30x speedup).
2026-02-22 22:20:03 +01:00
Tobias Gesellchen b71a3830ec Add more routes to be handled by ourselves
Group management is only implemented as placeholder
2026-02-22 20:48:42 +01:00
Tobias Gesellchen f50ee1131e Fix migration check 2026-02-22 18:58:20 +01:00
Tobias Gesellchen 6a65376784 Attempt resolution if it's not a numeric IP 2026-02-22 14:17:01 +01:00
Tobias Gesellchen 44d04a2b41 Allow empty dns upstream config (default to system nameservers) 2026-02-22 13:58:30 +01:00
Tobias Gesellchen 0f802e65c6 Fallback to the system's dns resolver by default 2026-02-22 13:36:58 +01:00
Tobias Gesellchen 01d702c745 Fix the Raspberry Pi install script (self-update, env variables) 2026-02-22 01:03:50 +01:00
Tobias Gesellchen 7823b68bdd Prime Spotify only on speaker boot/power_on 2026-02-22 00:33:15 +01:00
Tobias Gesellchen e1f3fc36c8 Fix Spotify link display 2026-02-22 00:07:52 +01:00
Tobias Gesellchen d68599896d Add Spotify primer 2026-02-21 23:40:45 +01:00
Tobias Gesellchen d18b67d80f Remove device-local Spotify primer 2026-02-21 23:40:45 +01:00
Tobias Gesellchen 8642ecfc5c Prepare Spotify primer 2026-02-21 23:40:45 +01:00
Tobias Gesellchen c37e94b5f8 Remove unused BaseURL 2026-02-21 11:22:46 +01:00
Tobias Gesellchen 4e33f6948f Add DNS discovery download 2026-02-21 11:22:46 +01:00
Tobias Gesellchen 743ff5e061 Add streamingoauth.bose.com to the intercepted DNS records 2026-02-21 11:22:46 +01:00
Tobias Gesellchen ec8bbb2f86 Lint: cleanup 2026-02-21 00:42:07 +01:00
Tobias Gesellchen e75e2bea0c Update Raspberry Pi installation script to include Spotify 2026-02-21 00:42:07 +01:00
Tobias Gesellchen dd5aa2ad53 Disable HTML escaping in JSON response 2026-02-21 00:42:07 +01:00
Tobias Gesellchen aced0f3f81 Use the Chi BasicAuth middleware 2026-02-21 00:42:07 +01:00
Tobias Gesellchen a886518cad Add example redirect URIs for both browser and ueberboese-app 2026-02-21 00:42:07 +01:00
Tim Van Wassenhove dc81b0aa81 feat: separate browser callback and mobile app confirm endpoints
- Add GET /mgmt/spotify/callback (no auth) for browser OAuth redirect
- Restore POST /mgmt/spotify/confirm (Basic Auth) for ueberboese mobile app
- Callback returns HTML success/error pages; confirm returns JSON
- Both call the same ExchangeCodeAndStore() logic
2026-02-21 00:21:52 +01:00
Tim Van Wassenhove c648027735 fix: OAuth callback as GET outside auth group, remove dead zeroconf flag, update .env.example
- Change /mgmt/spotify/confirm from POST to GET (Spotify redirects via GET)
- Move confirm endpoint outside Basic Auth group (code is single-use, needs client_secret)
- Remove --zeroconf-primer-enabled flag (no ZeroConf primer code on this branch)
- Add Spotify/mgmt env var documentation to .env.example
2026-02-21 00:21:52 +01:00
Tim Van Wassenhove fced88a8a6 feat: add management API endpoints matching ueberboese-app 2026-02-21 00:21:52 +01:00
Tim Van Wassenhove 0ee673c097 feat: wire Spotify service into server 2026-02-21 00:21:52 +01:00
Tim Van Wassenhove 395b2fec8e feat: add Spotify OAuth service with token management 2026-02-21 00:21:52 +01:00
Tim Van Wassenhove be7e44e14b feat: add Basic Auth middleware for management API 2026-02-21 00:21:52 +01:00
Tim Van Wassenhove a87783d8c6 feat: add Spotify, management, and ZeroConf CLI flags 2026-02-21 00:21:52 +01:00
Tobias Gesellchen be017440b7 Add userInactivity event 2026-02-20 09:18:53 +01:00
Tobias Gesellchen 10de011c18 Simplify the PlayTTS method cmd 2026-02-19 08:46:55 +01:00
Tobias Gesellchen f7b74db3ea Make the linter happy 2026-02-19 08:44:26 +01:00
Tobias Gesellchen 72d75133c4 Capture server references before releasing mutex to avoid race condition 2026-02-19 08:44:26 +01:00
Tobias Gesellchen e4c12471b4 Add more upstream domains to the intercept list 2026-02-19 08:44:26 +01:00
Tobias Gesellchen 3329149282 Add support for RADIO_BROWSER source
This implementation follows the reference from soundcork pull request #158. It adds RADIO_BROWSER to the known providers and includes the service configuration in bmx_services.json. Documentation has also been added to explain how to use the RadioBrowser feature. Credits to @gmuth (https://github.com/gmuth) for the original idea and implementation in soundcork. Reference: https://github.com/deborahgu/soundcork/pull/158
2026-02-16 22:18:33 +01:00
Tobias Gesellchen 523ff0eb17 Fix deadlock in settings update and add efficient DNS settings validation 2026-02-16 21:02:42 +01:00
Tobias Gesellchen 025e15d65c Implement log throttling, loop prevention, and empty upstream handling in DNS discovery server 2026-02-16 21:02:42 +01:00
Tobias Gesellchen 7d140b3e2a Fix TestMigrationAndCA by enhancing mock SSH client
This commit updates the mock SSH client in the handler tests to support the recently added verification steps. It now correctly handles stateful responses for /etc/hosts and properly responds to file existence and CA trust checks.
2026-02-16 20:17:25 +01:00
Tobias Gesellchen 69210638e5 Add verification steps to speaker migration process
This update adds explicit verification checks after applying changes via XML, Hosts, and ResolvConf migration methods. The service now verifies that configuration files are correctly updated on the device before considering the migration successful, preventing unreliable states.
2026-02-16 20:17:25 +01:00
Tobias Gesellchen 6aef2b807d Enhance ResolvConf migration to support multiple DHCP script variants
This update allows the service to correctly patch both /etc/udhcpc.d/50default and /opt/Bose/udhcpc.script (used in SoundTouch 10 firmware) for DNS redirection. It also improves robustness by adding file existence checks in rc.local and ensures clean state by reverting to .original backups during migration.
2026-02-16 18:52:25 +01:00
Tobias Gesellchen 95f5e9c831 fix(setup): prevent and clean up corrupted rc.local with cat error message 2026-02-16 18:20:16 +01:00
Tobias Gesellchen 7337296ae9 refactor(setup): reduce cyclomatic complexity of RevertMigration 2026-02-16 18:04:28 +01:00
Tobias Gesellchen 92a5d3592c feat(setup): replace obsolete resolv method with persistent DHCP-aware DNS hook 2026-02-16 18:04:28 +01:00
Tobias Gesellchen 2f04af872b feat(setup): implement Aftertouch Hook (DHCP-aware DNS redirection); update UI and tests; docs now use aftertouch.resolv.conf 2026-02-16 18:04:28 +01:00
Tobias Gesellchen 9479d6d11d Fix missing request body in recorded proxy interactions 2026-02-16 16:30:41 +01:00
Tobias Gesellchen f687ba0d82 go mod tidy 2026-02-16 12:45:04 +01:00
Tobias Gesellchen ab2bf0731a Add DNS-based discovery and migration via /etc/resolv.conf 2026-02-16 12:18:06 +01:00
Tobias Gesellchen cafaba1be0 Update SOUNDTOUCH-SERVICE.md with recent features (Soundcork proxy, session archiving, enhanced redaction) 2026-02-15 23:54:14 +01:00
Tobias Gesellchen 93082d2cdc Update root endpoint JSON response with AfterTouch and docs link 2026-02-15 23:47:10 +01:00
Tobias Gesellchen 087006c483 Add regression test for settings persistence 2026-02-15 23:28:45 +01:00
Tobias Gesellchen b7013a5ec8 Apply 'Redact Sensitive Headers' to recordings 2026-02-15 23:12:19 +01:00
Tobias Gesellchen 7d76b3fab2 Implement dynamic Bose proxy with detailed origin logging and Soundcork fallback 2026-02-15 22:50:13 +01:00
Tobias Gesellchen 6ca206053f Add session download feature to web UI 2026-02-15 22:20:16 +01:00
Tobias Gesellchen 090eb162fb Fix TypeError in Web UI by renaming proxy-domain to soundcork-url
This commit fixes a JS error in showSummary and migrate functions where they were still trying to access the UI element by its old ID 'proxy-domain' instead of the new 'soundcork-url'.
2026-02-15 22:01:58 +01:00
dependabot[bot] 972824e07f ci(deps): bump the actions-core group with 3 updates
Bumps the actions-core group with 3 updates: [actions/checkout](https://github.com/actions/checkout), [actions/configure-pages](https://github.com/actions/configure-pages) and [actions/upload-pages-artifact](https://github.com/actions/upload-pages-artifact).


Updates `actions/checkout` from 4 to 6
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v6)

Updates `actions/configure-pages` from 4 to 5
- [Release notes](https://github.com/actions/configure-pages/releases)
- [Commits](https://github.com/actions/configure-pages/compare/v4...v5)

Updates `actions/upload-pages-artifact` from 3 to 4
- [Release notes](https://github.com/actions/upload-pages-artifact/releases)
- [Commits](https://github.com/actions/upload-pages-artifact/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions-core
- dependency-name: actions/configure-pages
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions-core
- dependency-name: actions/upload-pages-artifact
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions-core
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-15 21:57:12 +01:00
dependabot[bot] 1e2148d53b deps(deps): bump the golang group with 4 updates
Bumps the golang group with 4 updates: [golang.org/x/crypto](https://github.com/golang/crypto), [golang.org/x/mod](https://github.com/golang/mod), [golang.org/x/net](https://github.com/golang/net) and [golang.org/x/tools](https://github.com/golang/tools).


Updates `golang.org/x/crypto` from 0.47.0 to 0.48.0
- [Commits](https://github.com/golang/crypto/compare/v0.47.0...v0.48.0)

Updates `golang.org/x/mod` from 0.32.0 to 0.33.0
- [Commits](https://github.com/golang/mod/compare/v0.32.0...v0.33.0)

Updates `golang.org/x/net` from 0.49.0 to 0.50.0
- [Commits](https://github.com/golang/net/compare/v0.49.0...v0.50.0)

Updates `golang.org/x/tools` from 0.41.0 to 0.42.0
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.41.0...v0.42.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.48.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: golang
- dependency-name: golang.org/x/mod
  dependency-version: 0.33.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: golang
- dependency-name: golang.org/x/net
  dependency-version: 0.50.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: golang
- dependency-name: golang.org/x/tools
  dependency-version: 0.42.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: golang
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-15 21:56:17 +01:00
Tobias Gesellchen 9a070da1ef Fix data race in RecordMiddleware and improve recorder robustness
This commit addresses the data race detected in TestRecordMiddleware: - Updated Recorder.Record to clone Request and Response objects (including bodies) before background processing. - Ensures background workers can safely access data after the main request handler has finished. - Enabled synchronous recording in handler tests to ensure deterministic results and avoid race conditions.
2026-02-15 21:51:55 +01:00
Tobias Gesellchen d4b518da23 Fix proxy and recorder tests by ensuring synchronous recording during testing
This commit addresses the test failures in pkg/service/proxy: - Ensures synchronous recording in tests by setting RECORDER_ASYNC=false. - Adds a Close() method to the Recorder for proper cleanup. - Fixes a panic in TestRecorder_Record_Redaction caused by race conditions.
2026-02-15 21:51:55 +01:00
Tobias Gesellchen 89bafd97b6 Optimize recording performance and add Soundcork proxy toggle
This commit introduces several key improvements: Performance Optimization (asynchronous recording), Legacy Proxy Control (Soundcork proxy toggle), X-Forwarded-For Sanitization, consistent Soundcork naming across the stack, and various code quality improvements.
2026-02-15 21:51:55 +01:00
Tobias Gesellchen d616bc09fd fix unbound variable (tmp) 2026-02-15 20:48:26 +01:00
Tobias Gesellchen 8af60c7e4b fix linter issues 2026-02-15 20:20:47 +01:00
Tobias Gesellchen 8a21db3517 Capture additional redirect methods and improve recorder functionality 2026-02-15 20:20:47 +01:00
Tobias Gesellchen 742484568e feat: implement Stockholm-related cloud API emulation - Added handlers for Stockholm app events (/v1/stapp, /v1/scmudc) - Implemented account profile, password management, and device settings endpoints - Added Go models for new API responses and requests - Created docs/reference/CLOUD-API.md and updated SUMMARY.md - Added comprehensive unit tests for all new handlers - Updated ueberboese-api.yaml with new endpoints and schemas 2026-02-15 20:20:47 +01:00
Tobias Gesellchen ed2d8680e4 fix: align streaming_token with Bose protocol to avoid 502 errors 2026-02-15 18:58:35 +01:00
Tobias Gesellchen 6dc8c23f04 feat: detect migrated devices and prompt for reboot after migration 2026-02-15 18:58:35 +01:00
Tobias Gesellchen fa57ee9574 Rebrand to AfterTouch and cleanup SoundCork references 2026-02-15 18:09:49 +01:00
Tobias Gesellchen e438db05d9 Fix release workflow to avoid +dirty version suffix by building in isolated directory 2026-02-15 17:27:41 +01:00
Tobias Gesellchen 8c02a009dc Update documentation for interaction session management 2026-02-15 16:52:44 +01:00
Tobias Gesellchen f20cfcb319 Enhance interaction session management and cleanup UI 2026-02-15 16:52:44 +01:00
Tobias Gesellchen a453059d6d Enhance interaction recording and analysis features 2026-02-15 16:52:44 +01:00
Tobias Gesellchen 505e6dd760 Refactor data storage to use account-based hierarchy and update Web UI 2026-02-15 15:36:01 +01:00
Tobias Gesellchen 735187cae8 docs: link README.md in SUMMARY.md to fix TestDocsConsistency 2026-02-15 00:50:29 +01:00
Tobias Gesellchen e8622cc382 docs: add Jekyll build step to workflow 2026-02-15 00:38:04 +01:00
Tobias Gesellchen c59052bdb4 docs: improve Jekyll configuration with minimal theme and relative links plugin 2026-02-15 00:35:32 +01:00
Tobias Gesellchen 15a6c4b0a0 docs: add Jekyll configuration with Cayman theme 2026-02-15 00:35:19 +01:00
Tobias Gesellchen ae3a3765db docs: add landing page for GitHub Pages 2026-02-15 00:32:32 +01:00
Tobias Gesellchen 59019cf55c docs: deploy documentation to GitHub Pages and update links in Web UI and README 2026-02-15 00:30:28 +01:00
Tobias Gesellchen 5e612e57ec Fix golangci-lint issues in main.go 2026-02-15 00:27:25 +01:00
Tobias Gesellchen 02026a9f3a Add configurable shortcuts and log them on startup 2026-02-15 00:27:25 +01:00
Tobias Gesellchen aaf067088a Auto-create missing configuration files with default values 2026-02-15 00:27:25 +01:00
Tobias Gesellchen 358ea18138 Implement device merging logic and web-based device removal 2026-02-15 00:15:09 +01:00
Tobias Gesellchen 0c5c1803a5 docs: fix broken documentation links across multiple files 2026-02-14 23:03:53 +01:00
Tobias Gesellchen cdf80a793e docs: fix broken documentation links across multiple files 2026-02-14 23:03:53 +01:00
Tobias Gesellchen b511e052e2 docs: fix broken documentation links and update CI workflow paths 2026-02-14 23:03:53 +01:00
Tobias Gesellchen b7197a8679 Add version visibility and discovery controls to Web UI and API 2026-02-14 23:03:53 +01:00
Tobias Gesellchen 5bfc24b7fb Use v0.18.1 version as default 2026-02-14 22:21:43 +01:00
Tobias Gesellchen 1e61adbb46 Integrate self-update logic into Raspberry Pi installer and simplify update workflow 2026-02-14 22:21:43 +01:00
Tobias Gesellchen b8ab4b5723 Enhance Raspberry Pi installer and modernize systemd deployment documentation 2026-02-14 21:53:24 +01:00
Tobias Gesellchen 5da7e001b2 Add a Systemd install script 2026-02-14 21:53:24 +01:00
Tobias Gesellchen 5269c05e56 Enhance settings management with persistence and explicit saving, including SAN updates and unit tests 2026-02-14 21:50:36 +01:00
Tobias Gesellchen d7a15c4dbe Minor cleanup and formatting fixes in docs handler and setup manager 2026-02-14 18:26:52 +01:00
Tobias Gesellchen 701889076d Refactor documentation structure, add SUMMARY.md sidebar, and automated consistency checks 2026-02-14 18:26:52 +01:00
Tobias Gesellchen 3acc983183 Cleanup the web ui/flow 2026-02-14 18:26:52 +01:00
Tobias Gesellchen 7d40c61cad Fix variable shadowing in setup methods
Renamed shadowed 'err' and 'out' variables in MigrateSpeaker and TrustCACert to comply with linting rules.
2026-02-14 14:24:46 +01:00
Tobias Gesellchen 93cfd9dbbc Implement safe migration revert with backup validation
- Added strict guardrails for RevertMigration: now fails if .original backup is missing.
- Revert now uses copy (cp) instead of move (mv) to preserve original backups on the device.
- Decoupled reboot from migration/revert processes, making it a manual operation.
- Added standalone Reboot API and manual 'Reboot Speaker' button in the Web UI.
- Separated 'Remove Remote Services' from the revert process to allow independent management.
- Implemented command output capture and display in the Web UI for all setup actions (Migrate, Revert, Trust CA, Backup, Reboot, Remove Remote Services).
- Updated doc.go with a modern overview of the library and SoundTouch service features.
- Fixed several tests to align with new method signatures and behavior changes.
2026-02-14 14:24:46 +01:00
Tobias Gesellchen e084f8db1f Enhance Docker configuration for Swarm and consolidate environment templates 2026-02-14 13:09:52 +01:00
Tobias Gesellchen a19d34b55e Align soundtouch-service CLI with soundtouch-cli 2026-02-14 12:55:14 +01:00
Tobias Gesellchen dcf2e29c16 Fix linting issues and refactor for improved code quality 2026-02-14 12:39:39 +01:00
Tobias Gesellchen 9be1c7d588 Allow toggling HTTP interaction recording via CLI, environment, and Web UI 2026-02-14 12:39:39 +01:00
Tobias Gesellchen 133c07fefa Improve visibility of multiple devices in recordings by adding original value comments to .http files 2026-02-14 12:39:39 +01:00
Tobias Gesellchen ef90b4e848 Improve structure and re-usability of HTTP interaction recordings 2026-02-14 12:39:39 +01:00
Tobias Gesellchen c8ef1a9de4 Update README and documentation to reflect Toolkit expansion and Docker support 2026-02-14 00:14:29 +01:00
Tobias Gesellchen e47fa4c92c Complete local Bose SoundTouch emulation service with guided migration UI 2026-02-14 00:14:29 +01:00
Tobias Gesellchen 1a39c14b35 Rename crypto package to certmanager to resolve golangci-lint naming conflict
- Renamed pkg/service/crypto to pkg/service/certmanager
- Updated package declaration from 'crypto' to 'certmanager'
- Fixed all import statements across the codebase
- Updated type references from *crypto.CertificateManager to *certmanager.CertificateManager
- Renamed files for consistency: crypto.go -> certmanager.go, crypto_test.go -> certmanager_test.go
- Resolves golangci-lint var-naming issue about conflicting with Go standard library package names
- All tests pass and linter reports 0 issues
2026-02-13 22:36:36 +01:00
Tobias Gesellchen c9f648096e Implement label-based CA certificate management and add timeout flags to curl commands 2026-02-13 22:36:36 +01:00
Tobias Gesellchen 408753c33e Add note about automatic TLS SAN inclusion for custom server URLs 2026-02-13 00:11:27 +01:00
Tobias Gesellchen dff060565e Add Docker usage example with custom server URLs 2026-02-13 00:11:27 +01:00
Tobias Gesellchen b5df6ab91f Include SERVER_URL and HTTPS_SERVER_URL hostnames in TLS certificate SANs 2026-02-13 00:11:27 +01:00
Tobias Gesellchen c7e055eb51 fix: golangci-lint issues 2026-02-12 23:41:54 +01:00
Tobias Gesellchen 00d5bfcb69 feat: implement dual migration (XML and /etc/hosts) with custom CA and HTTPS support. Added automated /etc/hosts redirection, Root CA injection, built-in HTTPS listener, and enhanced management UI with diagnostic tests. 2026-02-12 23:41:54 +01:00
Tobias Gesellchen 0186fead6e docs: add comprehensive documentation for SoundTouch device redirection, logging, and cloud analysis 2026-02-12 23:41:54 +01:00
Tobias Gesellchen bf4ead033c Use correct maintainer names of related projects 2026-02-12 21:12:32 +01:00
Tobias Gesellchen 5eee3ec31e fix linting issue 2026-02-12 21:05:02 +01:00
dependabot[bot] 30e09ab7a0 docker(deps): bump golang from 1.25.7-alpine to 1.26.0-alpine
Bumps golang from 1.25.7-alpine to 1.26.0-alpine.

---
updated-dependencies:
- dependency-name: golang
  dependency-version: 1.26.0-alpine
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-12 21:05:02 +01:00
dependabot[bot]andlnx01 e429d92124 deps(deps): bump golang.org/x/sys from 0.40.0 to 0.41.0 in the golang group (#24)
Bumps the golang group with 1 update:
[golang.org/x/sys](https://github.com/golang/sys).

Updates `golang.org/x/sys` from 0.40.0 to 0.41.0
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/golang/sys/commit/fc646e489fd944b6f77d327ab77f1a4bab81d5ad"><code>fc646e4</code></a>
cpu: use IsProcessorFeaturePresent to calculate ARM64 on windows</li>
<li><a
href="https://github.com/golang/sys/commit/f11c7bb268eb8a49f5a42afe15387a159a506935"><code>f11c7bb</code></a>
windows: add IsProcessorFeaturePresent and processor feature consts</li>
<li><a
href="https://github.com/golang/sys/commit/d25a7aaff8c2b056b2059fd7065afe1d4132e082"><code>d25a7aa</code></a>
unix: add IoctlSetString on all platforms</li>
<li><a
href="https://github.com/golang/sys/commit/6fb913b30f367555467f08da4d60f49996c9b17a"><code>6fb913b</code></a>
unix: return early on error in Recvmsg</li>
<li>See full diff in <a
href="https://github.com/golang/sys/compare/v0.40.0...v0.41.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=golang.org/x/sys&package-manager=go_modules&previous-version=0.40.0&new-version=0.41.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-10 08:27:22 +01:00
Tobias Gesellchen f3162b7ed9 Add PlayNotification support for device-local PCM files and expose it via CLI 2026-02-10 08:23:33 +01:00
Tobias Gesellchen 8094ac70bd Fix checksums job to only download binary artifacts (#23)
The checksums job was downloading all artifacts including Docker build
artifacts, but it was only designed to process binary artifacts. This
caused failures when the Docker job created artifacts that didn't match
the expected soundtouch-* binary file patterns.

Changed the artifact download to use pattern: binaries-* to only
download the binary artifacts that the checksums generation logic
expects.
2026-02-08 01:03:44 +01:00
dependabot[bot]andlnx01 11919f7fa9 docker(deps): bump alpine from 3.21 to 3.23 (#21)
Bumps alpine from 3.21 to 3.23.


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=alpine&package-manager=docker&previous-version=3.21&new-version=3.23)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-08 00:39:37 +01:00
Tobias Gesellchen c560d399b5 Add Docker support and CI/CD integration for SoundTouch service (#19) 2026-02-08 00:36:49 +01:00
Tobias Gesellchen 059498b16e Add option to remove persistent remote_services (#18) 2026-02-08 00:04:20 +01:00
Tobias Gesellchen 1281af7f6f docs: Add comprehensive SoundTouch service documentation and community credits
## SoundTouch Service Documentation

### Enhanced README.md
- Added detailed SoundTouch service feature overview and capabilities
- Comprehensive service installation, configuration, and usage guide
- Device migration examples and service endpoint documentation
- Web UI feature description and management interface guide

### Updated docs/SOUNDTOUCH-SERVICE.md
- Complete service architecture overview (BMX, Marge, proxy services)
- Step-by-step device migration guide with troubleshooting
- Full API reference with endpoint documentation and examples
- Web interface feature guide and usage instructions
- Data management, backup strategies, and maintenance procedures
- Advanced usage examples and integration patterns
- Security considerations and performance tuning guide

### New docs/SOUNDTOUCH-SERVICE-ANNOUNCEMENT.md
- Feature announcement and implementation overview
- Detailed comparison with community implementations
- Use cases, future roadmap, and contribution guidelines

## Community Credits & Attribution

### SoundCork Recognition
- Acknowledged as primary architectural inspiration and foundation
- Credited for pioneering service interception and emulation approach
- Recognized for BMX/Marge endpoint discovery and migration strategies
- Noted as providing Python implementation reference

### ÜberBöse API Recognition
- Credited for advanced API endpoint insights and research
- Acknowledged for contributing to implementation completeness
- Recognized for extended protocol documentation

### SoundTouch Plus Recognition
- Credited for comprehensive API documentation via wiki
- Acknowledged for real-world usage patterns and endpoint discovery
- Recognized for enabling preset management feature development

This documentation update ensures proper attribution to the excellent community
projects that inspired our Go implementation while providing comprehensive guides
for users to leverage the new service functionality, particularly valuable given
Bose's cloud service discontinuation in May 2026.
2026-02-07 22:49:42 +01:00
Tobias Gesellchen 44e48f7307 chore: apply final linter fixes and code quality improvements across the service layer 2026-02-07 22:36:50 +01:00
Tobias Gesellchen e65b1ac110 Fix security vulnerability GO-2026-4337: Update Go to 1.25.7
- Updated Go version from 1.25.6 to 1.25.7 in main go.mod
- Updated Go version in example modules (navigation-station-demo, preset-management)
- Fixes TLS vulnerability: Unexpected session resumption in crypto/tls
- Addresses security issue affecting WebSocket, HTTP handlers, and proxy operations

Reference: https://pkg.go.dev/vuln/GO-2026-4337
2026-02-07 22:36:50 +01:00
Tobias Gesellchen 6504c301f6 Fix golangci-lint issues: error checking, JSON encoding, variable shadowing, and code structure
- Fixed critical error checking (errcheck) for file operations, HTTP responses, JSON operations
- Added proper JSON encoding error handling (errchkjson) in HTTP handlers
- Fixed built-in redefinition by renaming max variable to maxETag
- Optimized range loops to avoid copying large structs (gocritic)
- Resolved variable shadowing issues in multiple functions (govet)
- Improved code structure with nesting reduction (gocritic)
- Enhanced test robustness with proper error handling

Remaining issues are primarily style/documentation related (revive comments).
2026-02-07 22:36:50 +01:00
Tobias Gesellchen 210fd587de chore: run golangci-lint --fix and manually address remaining linting issues. Fixed bodyclose, errcheck, and contextcheck across the codebase. 2026-02-07 22:36:50 +01:00
Tobias Gesellchen 79ca666785 Merge Bose-SoundTouch-API (soundcork-go) into Bose-SoundTouch. Integrated service logic, created soundtouch-service command, embedded resources, updated docs, examples and CI/CD.
Commit history from `7204e619decc48df5dee91d18470934b50e389ac` to `f9b5ad3129831086b02bdf20a197ff4e2d098e2d`: https://github.com/gesellix/Bose-SoundTouch-API/compare/7204e619decc48df5dee91d18470934b50e389ac...f9b5ad3129831086b02bdf20a197ff4e2d098e2d

* f9b5ad3 - Tobias Gesellchen, 2026-02-07 : Rename module to gesellix/bose-soundtouch-api and update related files
* 5b3dbbb - Tobias Gesellchen, 2026-02-07 : docs: translate PLAN.md to English and fix preferredLanguage typo in marge.go
* 696b9c9 - Tobias Gesellchen, 2026-02-07 : feat(discovery): fetch serial number from speaker info if missing in discovery and update datastore tests
* 8ed78f0 - Tobias Gesellchen, 2026-02-07 : Consolidate proxy and main service on port 8000 and update related tests and UI
* 0e3abbb - Tobias Gesellchen, 2026-02-07 : feat(go): lowercase guessed hostnames for URL consistency
* ca1091f - Tobias Gesellchen, 2026-02-07 : feat(health): add health endpoint with VCS build information
* a432d53 - Tobias Gesellchen, 2026-02-07 : Rename mock token to soundcork-local-token and add documentation
* 3b5ee2f - Tobias Gesellchen, 2026-02-07 : Implement Phase 10: Stats API, Device Event Log, and advanced Marge functions
* bc96033 - Tobias Gesellchen, 2026-02-06 : chore
* c77864b - Tobias Gesellchen, 2026-02-06 : Document Golang header normalization behavior and ensure generic header casing preservation in proxy
* a54e7e7 - Tobias Gesellchen, 2026-02-06 : Ensure ETag header preserves casing (uppercase 'T') for case-sensitive devices
* 6265fbe - Tobias Gesellchen, 2026-02-06 : update dockerfile to be in sync with go.mod
* 5290bad - Tobias Gesellchen, 2026-02-06 : Implement proxy logging settings UI and complete Phase 8 quick wins (ETags, DataStore initialization)
* d7aa7f7 - Tobias Gesellchen, 2026-02-06 : Update PLAN.md with recent features and Phase 8 Upstream Parity tasks
* c8ae5e2 - Tobias Gesellchen, 2026-02-06 : Enhance Bose SoundTouch migration with proxying, remote services persistence, and improved diagnostics
* c53fa00 - Tobias Gesellchen, 2026-02-06 : Implement remote services persistence check and UI improvements for Bose SoundTouch migration
* ea5c348 - Tobias Gesellchen, 2026-02-02 : Ignore soundcork-go/data directory and include recent datastore fixes
* d162892 - Tobias Gesellchen, 2026-02-02 : Complete Phase 7: Automated Setup & UI refactoring. Implemented programmatic SSH/migration logic, added device discovery endpoints, created Web UI for speaker management, and refactored UI to use external HTML with Go embed.
* b528016 - Tobias Gesellchen, 2026-02-01 : Add GitHub workflow to publish Docker image to GHCR and update Dockerfile
* 2ee03da - Tobias Gesellchen, 2026-02-01 : Add GitHub Actions workflow for Go CI and update PLAN.md
* 439e2a9 - Tobias Gesellchen, 2026-02-01 : Refactor Go implementation: extract handlers and tests into dedicated files, add comprehensive unit and HTTP tests
* c0698fb - Tobias Gesellchen, 2026-02-01 : Add Docker telnet example and update IP consistency in documentation
* 028a02e - Tobias Gesellchen, 2026-02-01 : Fix older port number in README
* 264829d - Tobias Gesellchen, 2026-02-01 : Add setup-speaker.sh and update documentation to match issue #59
* 64306f9 - Tobias Gesellchen, 2026-02-01 : Implement device presets endpoint in Go
* b98a602 - Tobias Gesellchen, 2026-02-01 : Implement Phase 4: Datastore and Marge logic in Go
* b6e1bc9 - Tobias Gesellchen, 2026-02-01 : Implement Phase 3: BMX Streaming and Service Registry in Go
* f1b3dcf - Tobias Gesellchen, 2026-02-01 : Port core models and constants to Go
* 9eae655 - Tobias Gesellchen, 2026-02-01 : Implement static file serving for /media in Go
* cc73e50 - Tobias Gesellchen, 2026-02-01 : Fix Go service accessibility and improve Docker configuration
* e356bdd - Tobias Gesellchen, 2026-02-01 : Initialize Go migration: Phase 1 infrastructure, proxy-first routing, and root endpoint
2026-02-07 22:36:50 +01:00
Tobias Gesellchen 5b9ab48897 Fix test failures and broken documentation links
- Fix content type display tests to expect lowercase 'track' instead of 'Track'
  - Content types should display raw API values for technical accuracy
- Fix icon test to expect correct emoji for unknown content types
- Update documentation links in README files:
  - Point source selection links to docs/SOURCE-SELECTION.md
  - Point navigation links to docs/NAVIGATION-GUIDE.md
  - Point zone management links to docs/zone-management.md
  - Update service management link to SERVICE-AVAILABILITY-IMPLEMENTATION.md

All tests now pass and documentation links are verified to exist.
2026-02-02 17:55:27 +01:00
Tobias Gesellchen 285f85efa2 feat: implement comprehensive music service account management with full golangci-lint compliance
This commit completes the music service account management implementation
and resolves all golangci-lint issues across the codebase.

Music Service Account Management:
• Add/remove accounts for all major streaming services (Spotify, Pandora, Amazon Music, Deezer, iHeartRadio)
• Support for network music libraries (NAS/UPnP/DLNA servers)
• Generic account management with service-specific convenience methods
• Full CLI integration with 14 account management commands
• Comprehensive test coverage with mock HTTP servers
• Complete API documentation and usage examples

New CLI Commands:
• account list - List configured accounts
• account add/remove - Generic account management
• account add-spotify/remove-spotify - Spotify Premium
• account add-pandora/remove-pandora - Pandora Music Service
• account add-amazon/remove-amazon - Amazon Music
• account add-deezer/remove-deezer - Deezer Premium
• account add-iheart/remove-iheart - iHeartRadio
• account add-nas/remove-nas - Network music libraries

New API Methods:
• SetMusicServiceAccount() / RemoveMusicServiceAccount() - Generic methods
• AddSpotifyAccount() / RemoveSpotifyAccount() - Convenience methods
• AddPandoraAccount() / RemovePandoraAccount() - Convenience methods
• AddAmazonMusicAccount() / RemoveAmazonMusicAccount() - Convenience methods
• AddDeezerAccount() / RemoveDeezerAccount() - Convenience methods
• AddIHeartRadioAccount() / RemoveIHeartRadioAccount() - Convenience methods
• AddStoredMusicAccount() / RemoveStoredMusicAccount() - Network libraries

golangci-lint Fixes (36 issues resolved):
• errcheck (3): Fixed unchecked w.Write() returns in tests
• gocritic (3): Rewrote if-else chains to switch statements
• gocyclo (6): Reduced cyclomatic complexity via helper function extraction
• govet (12): Removed unused test data and field assignments
• revive (6): Added package comments and fixed unused parameters
• staticcheck (2): Replaced deprecated strings.Title usage
• thelper (6): Added t.Helper() calls to test helper functions
• unused (1): Removed unused createTestApp() function
• whitespace/wsl_v5 (7): Fixed whitespace and formatting issues

Code Quality Improvements:
• All functions now have complexity < 15 (down from max 28)
• Consistent error handling and validation patterns
• Better separation of concerns with extracted helper functions
• Zero external dependencies added for simple fixes
• Comprehensive documentation with usage examples
• Full backward compatibility maintained

Files Added:
• pkg/models/account.go - Account management models
• pkg/models/account_test.go - Account model tests
• pkg/client/account_test.go - Account client tests
• cmd/soundtouch-cli/cmd_account.go - Account CLI commands
• examples/account-management/ - Complete usage example
• Updated docs/CLI-REFERENCE.md with account management section

The implementation provides a complete, production-ready music service
account management system with full CLI and programmatic API support.
2026-02-02 17:44:26 +01:00
Tobias Gesellchen dd6b3941d4 docs: move CONTENT-SELECTION-IMPLEMENTATION.md to docs/ directory
- Move implementation summary to proper docs/ location
- Maintain consistency with other documentation files
2026-02-02 17:08:29 +01:00
Tobias Gesellchenandlnx01 0d5746a6a5 feat: implement comprehensive content selection with streamUrl format support
 New Features:
- Add SelectContentItem() method for direct ContentItem selection
- Add SelectLocalInternetRadio() with full streamUrl format support
- Add SelectLocalMusic() for SoundTouch App Media Server content
- Add SelectStoredMusic() for UPnP/DLNA media server content

📻 streamUrl Format Support:
- Full implementation of wiki specification for LOCAL_INTERNET_RADIO
- Support for proxy URLs: http://contentapi.gmuth.de/station.php?name=Station&streamUrl=ActualStream
- Direct stream URL support for simple internet radio
- Complete ContentItem structure with metadata and artwork

🖥️ CLI Commands:
- Add 'source internet-radio' command with streamUrl support
- Add 'source local-music' command for local media server content
- Add 'source stored-music' command for UPnP/DLNA content
- Add 'source content' command for advanced generic selection
- All commands include comprehensive flag support and validation

🧪 Testing:
- Add 17+ comprehensive unit tests covering all scenarios
- Test streamUrl format validation and parsing
- Test error handling and parameter validation
- Test default value assignment and ContentItem construction
- All tests passing with full coverage

📚 Documentation:
- Update CLI-REFERENCE.md with new command examples
- Add complete content-selection example with working code
- Add implementation summary document
- Include API documentation for all new methods
- Add usage examples for both API and CLI

🔗 References:
Implements features from SoundTouch WebServices API Wiki:
- https://github.com/thlucas1/homeassistantcomponent_soundtouchplus/wiki/SoundTouch-WebServices-API#select-local_internet_radio---streamurl-format
- https://github.com/thlucas1/homeassistantcomponent_soundtouchplus/wiki/SoundTouch-WebServices-API#select-local_music

🎯 Benefits:
- Complete API coverage for advanced content selection
- Backward compatible with existing code
- Flexible design with both convenience and power-user methods
- Production-ready with comprehensive testing and documentation

Co-authored-by: SoundTouch WebServices API Wiki <https://github.com/thlucas1/homeassistantcomponent_soundtouchplus>
2026-02-02 16:44:25 +01:00
Tobias Gesellchen 7ec4ee67af feat: implement /introspect and /recents endpoints with full CLI support
🔥 NEW ENDPOINTS IMPLEMENTED:

📊 /introspect endpoint:
- Get detailed music service state and capabilities data
- Support for SPOTIFY, PANDORA, TUNEIN, AMAZON, DEEZER services
- Service state tracking (Active, Inactive, InactiveUnselected)
- Playback capabilities (skip, seek, resume, data collection)
- Authentication token status and user account information
- Subscription type and content history metadata

📚 /recents endpoint:
- Retrieve recently played content history
- Support for all music sources (Spotify, Local, TuneIn, Pandora, etc.)
- Rich filtering by source type and content type
- Content classification (tracks, stations, playlists, albums)
- Presetable item identification and artwork metadata
- Timestamp tracking with UTC time support

 CLIENT API:
- client.Introspect(source, sourceAccount) method
- client.IntrospectSpotify(sourceAccount) convenience method
- client.GetRecents() method with comprehensive filtering
- Complete error handling and validation
- Rich helper methods for content analysis

🖥️ CLI COMMANDS:
- soundtouch-cli source introspect --source <SERVICE>
- soundtouch-cli source introspect-spotify
- soundtouch-cli source introspect-all (bulk introspect)
- soundtouch-cli recents list [--detailed] [--limit N]
- soundtouch-cli recents filter --source <SRC> --type <TYPE>
- soundtouch-cli recents latest (most recent item)
- soundtouch-cli recents stats (detailed analytics)

📦 MODELS & FEATURES:
- IntrospectRequest/Response with service-specific handling
- RecentsResponse with RecentsResponseItem for individual items
- Rich filtering: GetSpotifyItems(), GetTracks(), GetPresetableItems()
- Content type detection: IsTrack(), IsStation(), IsPlaylist()
- Source classification: IsStreamingContent(), IsLocalContent()
- Full XML marshalling/unmarshalling with proper attribute handling

🧪 COMPREHENSIVE TESTING:
- Unit tests for models with XML parsing validation
- Integration tests for real device communication
- CLI command tests with mock server responses
- Error condition testing and edge case handling
- Performance tests and timeout validation

📖 DOCUMENTATION & EXAMPLES:
- Updated API endpoints overview marking endpoints as implemented
- Comprehensive CLI reference with usage examples
- Removed endpoints from unimplemented list
- Updated wiki implementation plan status
- Complete example applications with README guides
- Real-world usage patterns and best practices

 KEY FEATURES:
- Service health monitoring and diagnostics
- Recently played content discovery and analysis
- Preset candidate identification
- Content statistics and usage analytics
- Time-based filtering and relative timestamps
- Rich emoji-based CLI output formatting
- Cross-service compatibility and error handling

This implements two critical missing endpoints from the SoundTouch API,
providing essential functionality for music service management and
recently played content analysis with full programmatic and CLI access.
2026-02-02 16:26:40 +01:00
263 changed files with 54904 additions and 533 deletions
+33
View File
@@ -0,0 +1,33 @@
# .dockerignore
# Exclude large firmware files and archives
firmware/
data/
# Exclude local build artifacts
build/
soundtouch-cli
soundtouch-service
# Exclude Go specific files that aren't needed for build context
# (go.mod and go.sum ARE needed, but other local stuff isn't)
.cache/
vendor/
# Exclude IDE and system files
.idea/
.vscode/
.DS_Store
# Exclude Git history
.git/
.gitignore
# Exclude documentation and other non-essential files for the binary build
docs/
examples/
scripts/
CONTRIBUTING.md
CODE_OF_CONDUCT.md
LICENSE
README.md
+17
View File
@@ -0,0 +1,17 @@
root = true
[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.html]
# HTML-specific formatting
# Standardize on tag layout
ij_html_do_not_indent_children_of_tags = html,body,thead,tbody,tfoot
ij_html_keep_blank_lines = 1
ij_html_attribute_wrap = normal
ij_html_space_inside_empty_tag = false
+20
View File
@@ -1,6 +1,9 @@
# Bose SoundTouch Configuration
# Copy this file to .env and customize for your setup
# Docker/Service Settings
SOUNDTOUCH_HOSTNAME=soundtouch.local
# Discovery Settings
DISCOVERY_TIMEOUT=5s
UPNP_ENABLED=true
@@ -38,3 +41,20 @@ PREFERRED_DEVICES="Living Room@192.168.1.100:8090;Kitchen@192.168.1.101;192.168.
# Alternative format examples:
# PREFERRED_DEVICES="192.168.178.35;192.168.178.28"
# PREFERRED_DEVICES="SoundTouch 10@192.168.178.35;SoundTouch 20@192.168.178.28"
# Spotify Integration
# Create an app at https://developer.spotify.com/dashboard
# SPOTIFY_CLIENT_ID=your_client_id
# SPOTIFY_CLIENT_SECRET=your_client_secret
# Auth confirmation url using GET, works in browsers
# SPOTIFY_REDIRECT_URI=https://your-server.example.com/mgmt/spotify/callback
# Auth confirmation url using POST, works with the ueberboese-app (https://github.com/julius-d/ueberboese-app)
# SPOTIFY_REDIRECT_URI=https://your-server.example.com/mgmt/spotify/confirm
# Management API Authentication
# Protects /mgmt/* endpoints (Spotify token access, account management)
MGMT_USERNAME=admin
MGMT_PASSWORD=change_me!
# External base URL (required when behind a reverse proxy for OAuth callbacks)
# BASE_URL=https://your-server.example.com
+21
View File
@@ -77,3 +77,24 @@ updates:
- "*scan*"
- "securecodewarrior/*"
- "codecov/*"
# Docker dependency updates
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
day: "wednesday"
time: "09:00"
timezone: "UTC"
open-pull-requests-limit: 3
reviewers:
- "gesellix"
assignees:
- "gesellix"
commit-message:
prefix: "docker"
include: "scope"
labels:
- "dependencies"
- "docker"
rebase-strategy: "auto"
+3
View File
@@ -25,6 +25,9 @@
},
{
"pattern": "^https://pkg.go.dev.*badge"
},
{
"pattern": "^\\.\\./images/(dashboard-home|account-creation|account-dashboard|usb-remote-services|device-discovery|device-registration|account-migration|migration-setup|migration-progress|migration-health|migration-complete|backup-setup)\\.png$"
}
],
"replacementPatterns": [
+76 -6
View File
@@ -1,5 +1,8 @@
name: CI
permissions:
contents: read
on:
push:
branches: [main]
@@ -100,7 +103,7 @@ jobs:
go build -o "$output_name" ./cmd/soundtouch-cli
- name: Upload build artifacts
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: soundtouch-cli-${{ matrix.goos }}-${{ matrix.goarch }}
path: soundtouch-cli-*
@@ -144,13 +147,36 @@ jobs:
use-verbose-mode: "yes"
config-file: ".github/markdown-link-check.json"
- name: Warn on pending images
run: |
IMAGES=(
"dashboard-home.png"
"account-creation.png"
"account-dashboard.png"
"usb-remote-services.png"
"device-discovery.png"
"device-registration.png"
"account-migration.png"
"migration-setup.png"
"migration-progress.png"
"migration-health.png"
"migration-complete.png"
"backup-setup.png"
)
for img in "${IMAGES[@]}"; do
if [ ! -f "docs/images/$img" ]; then
echo "::warning file=docs/guides/MIGRATION-GUIDE.md::Pending image '$img' is missing from docs/images/"
fi
done
- name: Validate API documentation
run: |
# Check that all documented endpoints exist in code
echo "Validating API documentation consistency..."
# Extract endpoint patterns from cookbook
if [ -f "docs/API-COOKBOOK.md" ]; then
# Check API cookbook
if [ -f "docs/reference/API-COOKBOOK.md" ]; then
echo "✓ API Cookbook exists"
else
echo "✗ API Cookbook missing"
@@ -158,7 +184,7 @@ jobs:
fi
# Check getting started guide
if [ -f "docs/GETTING-STARTED.md" ]; then
if [ -f "docs/guides/GETTING-STARTED.md" ]; then
echo "✓ Getting Started guide exists"
else
echo "✗ Getting Started guide missing"
@@ -218,10 +244,52 @@ jobs:
go run test_import.go
rm test_import.go
docker:
name: Docker Build
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=raw,value=edge,enable=${{ github.ref == 'refs/heads/main' }}
type=ref,event=pr
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64,linux/arm64/v8,linux/arm/v7
push: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
notify:
name: Notify Status
runs-on: ubuntu-latest
needs: [test, lint, build, security, docs]
needs: [test, lint, build, security, docs, docker]
if: always()
permissions:
statuses: write
@@ -234,7 +302,8 @@ jobs:
"${{ needs.lint.result }}" == "success" && \
"${{ needs.build.result }}" == "success" && \
"${{ needs.security.result }}" == "success" && \
"${{ needs.docs.result }}" == "success" ]]; then
"${{ needs.docs.result }}" == "success" && \
"${{ needs.docker.result }}" == "success" ]]; then
echo "✅ All CI checks passed!"
echo "status=success" >> $GITHUB_OUTPUT
else
@@ -244,6 +313,7 @@ jobs:
echo "Build: ${{ needs.build.result }}"
echo "Security: ${{ needs.security.result }}"
echo "Docs: ${{ needs.docs.result }}"
echo "Docker: ${{ needs.docker.result }}"
echo "status=failure" >> $GITHUB_OUTPUT
fi
id: status
+37
View File
@@ -0,0 +1,37 @@
name: Deploy Documentation
on:
push:
branches:
- main
paths:
- 'docs/**'
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
jobs:
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Pages
uses: actions/configure-pages@v5
- name: Build with Jekyll
uses: actions/jekyll-build-pages@v1
with:
source: 'docs/'
destination: '_site'
- name: Upload artifact
uses: actions/upload-pages-artifact@v4
with:
path: '_site'
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
+121 -83
View File
@@ -13,6 +13,7 @@ on:
permissions:
contents: write
actions: read
packages: write
env:
GO_VERSION_FILE: "go.mod"
@@ -113,100 +114,90 @@ jobs:
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.mod') }}-${{ hashFiles('**/go.sum') }}
- name: Build binary
- name: Build binaries
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
GOARM: ${{ matrix.goarm }}
CGO_ENABLED: 0
run: |
# Determine output filename
BINARY_NAME="soundtouch-cli"
# Common variables
ARCH_SUFFIX="${{ matrix.goos }}-${{ matrix.goarch }}"
if [[ "${{ matrix.goarm }}" != "" ]]; then
ARCH_SUFFIX="${ARCH_SUFFIX}v${{ matrix.goarm }}"
fi
if [[ "${{ matrix.goos }}" == "windows" ]]; then
OUTPUT_NAME="${BINARY_NAME}-v${{ needs.validate.outputs.version }}-${ARCH_SUFFIX}.exe"
else
OUTPUT_NAME="${BINARY_NAME}-v${{ needs.validate.outputs.version }}-${ARCH_SUFFIX}"
fi
# Function to build a binary
build_binary() {
local BINARY_NAME=$1
local CMD_PATH=$2
local OUTPUT_NAME
echo "Building: $OUTPUT_NAME"
# Ensure build directory exists
mkdir -p build
# Debug: Show current state
echo "Working directory: $(pwd)"
echo "Go version: $(go version)"
echo "Files before build:"
ls -la
if [[ "${{ matrix.goos }}" == "windows" ]]; then
OUTPUT_NAME="build/${BINARY_NAME}-v${{ needs.validate.outputs.version }}-${ARCH_SUFFIX}.exe"
else
OUTPUT_NAME="build/${BINARY_NAME}-v${{ needs.validate.outputs.version }}-${ARCH_SUFFIX}"
fi
# Debug: Show Go cache and module cache
echo "Go build cache location: $(go env GOCACHE)"
echo "Go module cache location: $(go env GOMODCACHE)"
echo "Go build cache contents:"
ls -la "$(go env GOCACHE)" 2>/dev/null || echo "Cache directory not accessible"
echo "Go module cache contents (top level):"
ls -la "$(go env GOMODCACHE)" 2>/dev/null || echo "Module cache directory not accessible"
echo "Building $BINARY_NAME: $OUTPUT_NAME"
# Ensure clean build environment
rm -f "$OUTPUT_NAME" "$OUTPUT_NAME.sha256" "$OUTPUT_NAME.sha512"
go clean -cache
# Ensure clean build environment for this binary
rm -f "$OUTPUT_NAME" "$OUTPUT_NAME.sha256" "$OUTPUT_NAME.sha512"
# Build with optimizations (using debug.BuildInfo for version info)
if ! go build \
-ldflags="-s -w" \
-o "$OUTPUT_NAME" \
./cmd/soundtouch-cli; then
echo "❌ Build failed"
echo "Files after failed build:"
ls -la
exit 1
fi
if ! go build \
-ldflags="-s -w" \
-o "$OUTPUT_NAME" \
"$CMD_PATH"; then
echo "❌ Build failed for $BINARY_NAME"
exit 1
fi
# Debug: Show post-build state
echo "Files after successful build:"
ls -la
# Verify binary was created
ls -la "$OUTPUT_NAME"
echo "$BINARY_NAME=$OUTPUT_NAME" >> $GITHUB_OUTPUT
}
# Verify binary was created and is executable
ls -la "$OUTPUT_NAME"
file "$OUTPUT_NAME"
# Build CLI
build_binary "soundtouch-cli" "./cmd/soundtouch-cli"
echo "binary_name=$OUTPUT_NAME" >> $GITHUB_OUTPUT
# Build Service
build_binary "soundtouch-service" "./cmd/soundtouch-service"
id: build
- name: Generate individual checksum
- name: Generate individual checksums
run: |
OUTPUT_NAME="${{ steps.build.outputs.binary_name }}"
CLI_NAME="${{ steps.build.outputs.soundtouch-cli }}"
SVC_NAME="${{ steps.build.outputs.soundtouch-service }}"
# Use atomic operations to avoid conflicts
TEMP_DIR=$(mktemp -d)
echo "Building checksums for: $OUTPUT_NAME"
echo "Matrix: ${{ matrix.goos }}-${{ matrix.goarch }}"
generate_checksums() {
local FILE=$1
echo "Building checksums for: $FILE"
sha256sum "$FILE" > "${TEMP_DIR}/$(basename "$FILE").sha256"
sha512sum "$FILE" > "${TEMP_DIR}/$(basename "$FILE").sha512"
mv "${TEMP_DIR}/$(basename "$FILE").sha256" "$FILE.sha256"
mv "${TEMP_DIR}/$(basename "$FILE").sha512" "$FILE.sha512"
}
# Generate checksums in temp directory first
sha256sum "$OUTPUT_NAME" > "${TEMP_DIR}/$(basename "$OUTPUT_NAME").sha256"
sha512sum "$OUTPUT_NAME" > "${TEMP_DIR}/$(basename "$OUTPUT_NAME").sha512"
# Move to final location atomically
mv "${TEMP_DIR}/$(basename "$OUTPUT_NAME").sha256" "$OUTPUT_NAME.sha256"
mv "${TEMP_DIR}/$(basename "$OUTPUT_NAME").sha512" "$OUTPUT_NAME.sha512"
generate_checksums "$CLI_NAME"
generate_checksums "$SVC_NAME"
# Cleanup
rm -rf "$TEMP_DIR"
echo "✅ Checksums generated successfully"
- name: Upload build artifact
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: ${{ steps.build.outputs.binary_name }}
name: binaries-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.goarm }}
path: |
${{ steps.build.outputs.binary_name }}
${{ steps.build.outputs.binary_name }}.sha256
${{ steps.build.outputs.binary_name }}.sha512
build/soundtouch-cli-v*
build/soundtouch-service-v*
retention-days: 1
checksums:
@@ -215,9 +206,10 @@ jobs:
needs: [validate, build]
steps:
- name: Download all artifacts
uses: actions/download-artifact@v7
- name: Download binary artifacts
uses: actions/download-artifact@v8
with:
pattern: binaries-*
path: ./binaries
- name: Generate checksums
@@ -226,13 +218,13 @@ jobs:
# Debug: Show the downloaded structure
echo "📁 Downloaded artifact structure:"
find . -type f -name "soundtouch-cli-*"
ls -R
# Create a collection directory to avoid naming conflicts
mkdir -p release-files
# Move all files from subdirectories to the collection directory
find . -mindepth 2 -type f -name "soundtouch-cli-*" -exec mv {} release-files/ \;
find . -mindepth 2 -type f \( -name "soundtouch-cli-*" -o -name "soundtouch-service-*" \) -exec mv {} release-files/ \;
# Remove empty directories
find . -type d -empty -delete
@@ -242,20 +234,20 @@ jobs:
# Debug: Show flattened structure
echo "📁 Flattened structure:"
ls -la soundtouch-cli-* || echo "No files found matching pattern"
ls -la soundtouch-* || echo "No files found matching pattern"
# Generate combined checksums (exclude individual .sha256/.sha512 files)
if ls soundtouch-cli-v* 1> /dev/null 2>&1; then
if ls soundtouch-* 1> /dev/null 2>&1; then
# Only checksum the actual binaries, not the .sha256/.sha512 files
ls soundtouch-cli-v* | grep -v '\.sha256$' | grep -v '\.sha512$' | xargs sha256sum > checksums.sha256
ls soundtouch-cli-v* | grep -v '\.sha256$' | grep -v '\.sha512$' | xargs sha512sum > checksums.sha512
ls soundtouch-* | grep -v '\.sha256$' | grep -v '\.sha512$' | xargs sha256sum > checksums.sha256
ls soundtouch-* | grep -v '\.sha256$' | grep -v '\.sha512$' | xargs sha512sum > checksums.sha512
echo "📋 Generated combined checksums:"
cat checksums.sha256
# Verify all expected files are present (binaries only, not checksum files)
EXPECTED_COUNT=7 # Based on build matrix
ACTUAL_COUNT=$(ls soundtouch-cli-v* | grep -v '\.sha256$' | grep -v '\.sha512$' | wc -l)
EXPECTED_COUNT=14 # 7 platforms * 2 binaries
ACTUAL_COUNT=$(ls soundtouch-* | grep -v '\.sha256$' | grep -v '\.sha512$' | wc -l)
if [[ $ACTUAL_COUNT -ne $EXPECTED_COUNT ]]; then
echo "❌ Expected $EXPECTED_COUNT binaries, found $ACTUAL_COUNT"
@@ -272,7 +264,7 @@ jobs:
fi
- name: Upload checksums
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: checksums
path: |
@@ -283,7 +275,7 @@ jobs:
retention-days: 1
- name: Upload all release assets
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: release-assets
path: binaries/release-files/
@@ -302,7 +294,7 @@ jobs:
fetch-depth: 0
- name: Download release assets
uses: actions/download-artifact@v7
uses: actions/download-artifact@v8
with:
name: release-assets
path: ./release-assets
@@ -369,19 +361,20 @@ jobs:
- [Troubleshooting Guide](docs/TROUBLESHOOTING.md) - Systematic issue resolution
- [Deployment Guide](docs/DEPLOYMENT.md) - Production deployment examples (Docker, K8s, systemd)
## 🔧 CLI Tool
## 🔧 CLI & Service Tools
Download the CLI tool for your platform from the assets below:
Download the tools for your platform from the assets below:
### CLI Tool
\`\`\`bash
# Quick device discovery
./soundtouch-cli -discover
\`\`\`
# Get device information
./soundtouch-cli -host 192.168.1.100 -info
# Monitor real-time events
./soundtouch-cli -host 192.168.1.100 -nowplaying
### SoundTouch Service
\`\`\`bash
# Start the service
./soundtouch-service
\`\`\`
## 🧪 Tested Hardware
@@ -402,6 +395,8 @@ jobs:
- Windows (amd64)
- FreeBSD (amd64)
Both `soundtouch-cli` and `soundtouch-service` are included.
## 🔐 Checksums
Multiple checksum options are provided for download verification:
@@ -454,6 +449,7 @@ jobs:
prerelease: ${{ needs.validate.outputs.is_prerelease == 'true' }}
files: |
release-assets/soundtouch-cli-v*
release-assets/soundtouch-service-v*
release-assets/checksums.sha256
release-assets/checksums.sha512
fail_on_unmatched_files: true
@@ -468,7 +464,7 @@ jobs:
steps:
- name: Download release assets
uses: actions/download-artifact@v7
uses: actions/download-artifact@v8
with:
name: release-assets
path: ./release-assets
@@ -479,23 +475,65 @@ jobs:
tag_name: ${{ github.event.release.tag_name }}
files: |
release-assets/soundtouch-cli-v*
release-assets/soundtouch-service-v*
release-assets/checksums.sha256
release-assets/checksums.sha512
fail_on_unmatched_files: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
docker:
name: Build and Push Docker Image
runs-on: ubuntu-latest
needs: validate
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=semver,pattern={{version}},value=v${{ needs.validate.outputs.version }}
type=semver,pattern={{major}}.{{minor}},value=v${{ needs.validate.outputs.version }}
type=raw,value=latest,enable=${{ needs.validate.outputs.is_prerelease == 'false' }}
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64,linux/arm64/v8,linux/arm/v7
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
notify:
name: Post-Release Notifications
runs-on: ubuntu-latest
needs: [validate, create_release, update_release]
if: always() && (needs.create_release.result == 'success' || needs.update_release.result == 'success')
needs: [validate, create_release, update_release, docker]
if: always() && (needs.create_release.result == 'success' || needs.update_release.result == 'success' || needs.docker.result == 'success')
steps:
- name: Notify success
run: |
echo "🎉 Release ${{ needs.validate.outputs.version }} completed successfully!"
echo "📦 Binaries built for 7 platforms"
echo "📦 Binaries built for 7 platforms (CLI and Service)"
echo "🐳 Docker image published to ghcr.io"
echo "🔐 Checksums generated and verified"
echo "📋 Release notes automatically generated"
echo ""
+9 -1
View File
@@ -14,6 +14,8 @@ jobs:
vulnerability-scan:
name: Vulnerability Scan
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code
@@ -43,7 +45,7 @@ jobs:
- name: Upload vulnerability scan results
if: failure()
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: vulnerability-scan-results
path: |
@@ -53,6 +55,8 @@ jobs:
static-analysis:
name: Static Security Analysis
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code
@@ -119,6 +123,8 @@ jobs:
dependency-review:
name: Dependency Review
runs-on: ubuntu-latest
permissions:
contents: read
if: github.event_name == 'pull_request'
steps:
@@ -137,6 +143,8 @@ jobs:
runs-on: ubuntu-latest
needs: [vulnerability-scan, static-analysis, codeql-analysis]
if: always()
permissions:
contents: read
steps:
- name: Security scan summary
+5
View File
@@ -13,19 +13,23 @@ dist/
# Root-level binary executables (exclude built binaries in root)
/soundtouch-cli
/soundtouch-service
/example-mdns
/example-upnp
/example-unified
/mdns-scanner
/websocket-demo
/main
# Environment configuration
.env
.env.local
.env.*.local
docker-compose.override.yml
# Test coverage reports
coverage.out
coverage*.out
coverage.html
*.prof
@@ -59,6 +63,7 @@ Thumbs.db
*.pid
*.seed
*.pid.lock
.output.txt
# Runtime data
pids
+11 -1
View File
@@ -50,7 +50,12 @@ linters:
linters:
- gocritic # Can be overly strict for test code
- wsl # Whitespace less critical in tests
- wsl_v5 # Whitespace less critical in tests
- gocyclo # Complexity less critical in tests
- govet # Avoid shadow warnings in tests
- revive # Avoid exported/package-comments in tests
- errcheck # Avoid mandatory error checks in tests
- unparam # Often parameters are fixed in test setups
# Exclude specific rules for generated files
- path: ".*\\.pb\\.go$"
@@ -62,6 +67,11 @@ linters:
- staticcheck
text: "SA9003:" # Empty branch
- linters:
- staticcheck
text: "SA1008: keys in http.Header are canonicalized"
path: pkg/service/handlers/handlers_etag_test.go
# Allow main functions to not check errors in examples
- path: cmd/.*\.go
text: "Error return value of.*is not checked"
@@ -85,7 +95,7 @@ linters:
- fieldalignment # Can be overly aggressive
gocyclo:
min-complexity: 15
min-complexity: 20
gocritic:
enabled-checks:
+4 -4
View File
@@ -76,7 +76,7 @@ When filing a bug report, include:
Feature requests are welcome! Please:
1. **Check if the feature already exists** in documentation
2. **Verify it's supported by the SoundTouch API** (see [official API docs](docs/API-Endpoints-Overview.md))
2. **Verify it's supported by the SoundTouch API** (see [official API docs](docs/reference/API-ENDPOINTS.md))
3. **Explain the use case** and how it benefits users
### 🔧 Contributing Code
@@ -469,10 +469,10 @@ Contributors will be:
- [Go Documentation](https://golang.org/doc/)
- [Effective Go](https://golang.org/doc/effective_go.html)
- [Bose SoundTouch API Documentation](docs/API-Endpoints-Overview.md)
- [Bose SoundTouch API Documentation](docs/reference/API-ENDPOINTS.md)
- [Project Architecture](docs/PROJECT-PATTERNS.md)
- [Development Status](docs/STATUS.md)
- [Development Status](docs/archive/STATUS.md)
---
**Thank you for contributing!** Every contribution helps make this library better for the entire SoundTouch community.
**Thank you for contributing!** Every contribution helps make this library better for the entire SoundTouch community.
+54
View File
@@ -0,0 +1,54 @@
# Build stage
FROM --platform=$BUILDPLATFORM golang:1.26.1-alpine AS builder
# Declare automatic platform ARGs to make them available in build stage
# See https://docs.docker.com/reference/dockerfile#automatic-platform-args-in-the-global-scope
# We should not set defaults here, but rely on BuildKit to set them matching the BUILDPLATFORM
ARG TARGETARCH
ARG TARGETOS
ARG TARGETVARIANT
WORKDIR /app
# Copy go mod and sum files
COPY go.mod go.sum ./
RUN go mod download
# Copy the rest of the source code
COPY . .
# Build the soundtouch-service
RUN if [ "${TARGETARCH}" = "arm" ] && [ -n "${TARGETVARIANT}" ]; then \
CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} GOARM=${TARGETVARIANT#v} go build -o /soundtouch-service ./cmd/soundtouch-service; \
else \
CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -o /soundtouch-service ./cmd/soundtouch-service; \
fi
# Final stage
FROM alpine:3.23
# Install necessary runtime dependencies
RUN apk add --no-cache ca-certificates tzdata
WORKDIR /app
# Copy the binary from the builder stage
COPY --from=builder /soundtouch-service /app/soundtouch-service
# Verify the binary works on the target platform
RUN /app/soundtouch-service version || echo "Binary verification complete"
# Create data directory for persistence
RUN mkdir -p /app/data
# Set environment variables with defaults
ENV PORT=8000
ENV DATA_DIR=/app/data
ENV LOG_PROXY_BODY=false
ENV REDACT_PROXY_LOGS=true
# Expose the service port
EXPOSE 8000
# Run the service
ENTRYPOINT ["/app/soundtouch-service"]
+46 -11
View File
@@ -12,6 +12,8 @@ GOFMT=gofmt
# Build parameters
BINARY_NAME=soundtouch-cli
BINARY_PATH=./cmd/$(BINARY_NAME)
SERVICE_NAME=soundtouch-service
SERVICE_PATH=./cmd/$(SERVICE_NAME)
EXAMPLE_MDNS_NAME=example-mdns
EXAMPLE_MDNS_PATH=./cmd/$(EXAMPLE_MDNS_NAME)
EXAMPLE_UPNP_NAME=example-upnp
@@ -25,13 +27,18 @@ BUILD_DIR=./build
all: check build
build: build-cli build-examples
build: build-cli build-service build-examples
build-cli:
@echo "Building $(BINARY_NAME)..."
@mkdir -p $(BUILD_DIR)
$(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME) $(BINARY_PATH)
build-service:
@echo "Building $(SERVICE_NAME)..."
@mkdir -p $(BUILD_DIR)
$(GOBUILD) -o $(BUILD_DIR)/$(SERVICE_NAME) $(SERVICE_PATH)
build-examples:
@echo "Building $(EXAMPLE_MDNS_NAME)..."
@mkdir -p $(BUILD_DIR)
@@ -47,17 +54,21 @@ build-linux:
@echo "Building for Linux..."
@mkdir -p $(BUILD_DIR)
GOOS=linux GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-amd64 $(BINARY_PATH)
GOOS=linux GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(SERVICE_NAME)-linux-amd64 $(SERVICE_PATH)
build-darwin:
@echo "Building for macOS..."
@mkdir -p $(BUILD_DIR)
GOOS=darwin GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-darwin-amd64 $(BINARY_PATH)
GOOS=darwin GOARCH=arm64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-darwin-arm64 $(BINARY_PATH)
GOOS=darwin GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(SERVICE_NAME)-darwin-amd64 $(SERVICE_PATH)
GOOS=darwin GOARCH=arm64 $(GOBUILD) -o $(BUILD_DIR)/$(SERVICE_NAME)-darwin-arm64 $(SERVICE_PATH)
build-windows:
@echo "Building for Windows..."
@mkdir -p $(BUILD_DIR)
GOOS=windows GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-windows-amd64.exe $(BINARY_PATH)
GOOS=windows GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(SERVICE_NAME)-windows-amd64.exe $(SERVICE_PATH)
build-examples-all:
@echo "Building examples for all platforms..."
@@ -108,6 +119,18 @@ dev: build-cli
@echo "Starting development CLI..."
$(BUILD_DIR)/$(BINARY_NAME) -help
dev-service: build-service
@echo "Starting development service..."
$(BUILD_DIR)/$(SERVICE_NAME)
dev-service-proxy: build-service
@echo "Starting development service with proxy..."
@if [ -z "$(PROXY_URL)" ]; then \
echo "Usage: make dev-service-proxy PROXY_URL=http://localhost:8001"; \
exit 1; \
fi
PYTHON_BACKEND_URL=$(PROXY_URL) $(BUILD_DIR)/$(SERVICE_NAME)
dev-discover: build-cli
@echo "Running device discovery..."
$(BUILD_DIR)/$(BINARY_NAME) -discover
@@ -164,9 +187,10 @@ dev-scan-http: build-examples
@echo "Scanning for HTTP mDNS services..."
$(BUILD_DIR)/$(SCANNER_NAME) -service _http._tcp -v
install: build-cli
@echo "Installing $(BINARY_NAME) to $(GOPATH)/bin..."
install: build-cli build-service
@echo "Installing binaries to $(GOPATH)/bin..."
cp $(BUILD_DIR)/$(BINARY_NAME) $(GOPATH)/bin/
cp $(BUILD_DIR)/$(SERVICE_NAME) $(GOPATH)/bin/
clean:
@echo "Cleaning..."
@@ -177,7 +201,7 @@ clean:
release: clean check build-all
@echo "Creating release archive..."
@mkdir -p $(BUILD_DIR)/release
@for binary in $(BUILD_DIR)/$(BINARY_NAME)-*; do \
@for binary in $(BUILD_DIR)/$(BINARY_NAME)-* $(BUILD_DIR)/$(SERVICE_NAME)-*; do \
if [ -f "$$binary" ]; then \
cp "$$binary" $(BUILD_DIR)/release/; \
fi \
@@ -186,16 +210,22 @@ release: clean check build-all
docker-build:
@echo "Building Docker image..."
docker build -t soundtouch-go:$(VERSION) .
docker build -t soundtouch-service .
docker-dev: docker-build
@echo "Running development container..."
docker run --rm -it --network host soundtouch-go:$(VERSION)
docker-run-host:
@echo "Running Docker container..."
@echo "Note: --network host is used for discovery (Linux only). For macOS/Windows use port mapping."
docker run --rm -it --network host -v $$(pwd)/data:/app/data soundtouch-service
docker-run-ports:
@echo "Running Docker container with port mapping (discovery will be manual)..."
docker run --rm -it -p 8000:8000 -v $$(pwd)/data:/app/data soundtouch-service
help:
@echo "Available targets:"
@echo " build - Build the CLI tool and examples"
@echo " build - Build the CLI tool, service, and examples"
@echo " build-cli - Build only the CLI tool"
@echo " build-service - Build only the service"
@echo " build-examples - Build only the example programs"
@echo " build-all - Build for all platforms"
@echo " test - Run tests"
@@ -206,6 +236,8 @@ help:
@echo " lint - Run golangci-lint"
@echo " tidy - Tidy dependencies"
@echo " dev - Build and show CLI help"
@echo " dev-service - Build and run service locally"
@echo " dev-service-proxy - Build and run service with proxy (PROXY_URL=url required)"
@echo " dev-discover - Build and run device discovery"
@echo " dev-info - Build and get device info (HOST=ip required)"
@echo " dev-mdns - Build and run mDNS discovery example"
@@ -217,14 +249,17 @@ help:
@echo " dev-scan-all - Scan all mDNS services on network"
@echo " dev-scan-soundtouch - Scan specifically for SoundTouch mDNS services"
@echo " dev-scan-http - Scan for HTTP mDNS services"
@echo " install - Install binary to GOPATH/bin"
@echo " install - Install binaries to GOPATH/bin"
@echo " clean - Clean build artifacts"
@echo " release - Create release binaries"
@echo " docker-build - Build Docker image"
@echo " docker-dev - Run development container"
@echo " docker-run-host - Run container with host networking (Linux discovery)"
@echo " docker-run-ports - Run container with port mapping (macOS/Windows/No discovery)"
@echo " help - Show this help message"
@echo ""
@echo "Examples:"
@echo " make dev-service"
@echo " make dev-service-proxy PROXY_URL=http://192.168.1.50:8001"
@echo " make dev-discover"
@echo " make dev-info HOST=192.168.1.10"
@echo " make dev-mdns"
+115 -50
View File
@@ -1,6 +1,6 @@
# Bose SoundTouch API Client
# Bose SoundTouch Toolkit
A comprehensive Go library and CLI tool for controlling Bose SoundTouch devices via their Web API.
A comprehensive solution for controlling and preserving Bose SoundTouch devices, including a Go library, CLI tool, and a local service for cloud emulation.
[![Go Reference](https://pkg.go.dev/badge/github.com/gesellix/bose-soundtouch.svg)](https://pkg.go.dev/github.com/gesellix/bose-soundtouch)
[![Go Report Card](https://goreportcard.com/badge/github.com/gesellix/bose-soundtouch)](https://goreportcard.com/report/github.com/gesellix/bose-soundtouch)
@@ -17,8 +17,18 @@ A comprehensive Go library and CLI tool for controlling Bose SoundTouch devices
-**Real-time Events**: WebSocket connection for live device state monitoring
- 🔍 **Device Discovery**: Automatic discovery via UPnP/SSDP and mDNS
- 📻 **Content Navigation**: Browse and search TuneIn, Pandora, Spotify, local music
- 📻 **RadioBrowser**: Access thousands of internet radio stations via [radio-browser.info](docs/reference/radio-browser.md)
- 🎙️ **Station Management**: Add and play radio stations without presets
- 🖥️ **CLI Tool**: Comprehensive command-line interface
- 🌐 **SoundTouch Service**: Emulate Bose cloud services for offline device operation
- 🔧 **Service Migration**: Migrate devices to use local services instead of Bose cloud (XML, Hosts, or DNS redirection)
- 🔍 **DNS Discovery & Interception**: Dynamic DNS server for intercepting and logging Bose service queries (requires port 53)
- 📊 **DNS Discovery Analysis**: Track and deduplicate all device DNS queries to discover hidden hostnames
- 📊 **Traffic Analysis**: Proxy and log device communications
- 📝 **HTTP Recording**: Persist interactions as re-playable `.http` files
- 🔄 **Endpoint Mirroring**: Asynchronously mirror local requests to Bose cloud for parity testing
- ⚖️ **Parity Logging**: Detect and record discrepancies between local and official Bose responses
- 🧹 **Session Management**: Manage and cleanup recorded interaction sessions
- 🔒 **Production Ready**: Extensive testing with real SoundTouch hardware
- 🌐 **Cross-Platform**: Windows, macOS, Linux support
@@ -26,9 +36,10 @@ A comprehensive Go library and CLI tool for controlling Bose SoundTouch devices
### Installation
#### Install CLI Tool
#### Install CLI and Service Tools
```bash
go install github.com/gesellix/bose-soundtouch/cmd/soundtouch-cli@latest
go install github.com/gesellix/bose-soundtouch/cmd/soundtouch-service@latest
```
#### Add Library to Your Project
@@ -38,41 +49,55 @@ go get github.com/gesellix/bose-soundtouch
### CLI Usage
#### Discover Devices
Find SoundTouch devices on your network:
```bash
# Find SoundTouch devices on your network
soundtouch-cli discover devices
```
# Control a Device
Control a device (replace `192.168.1.100` with your speaker's IP):
```bash
# Basic device information
soundtouch-cli --host 192.168.1.100 info get
# Basic information
soundtouch-cli --host 192.168.1.100 info
# Media controls
soundtouch-cli --host 192.168.1.100 play start
soundtouch-cli --host 192.168.1.100 volume set --level 50
soundtouch-cli --host 192.168.1.100 source select --source SPOTIFY
# Preset management
soundtouch-cli --host 192.168.1.100 preset list
soundtouch-cli --host 192.168.1.100 preset store-current --slot 1
soundtouch-cli --host 192.168.1.100 preset select --slot 1
# Browse and discover content
soundtouch-cli --host 192.168.1.100 browse tunein
soundtouch-cli --host 192.168.1.100 station search-tunein --query "jazz"
soundtouch-cli --host 192.168.1.100 station add --source TUNEIN --token <token> --name "Jazz Radio"
# Speaker notifications (ST-10 only)
soundtouch-cli --host 192.168.1.100 speaker tts --text "Welcome home" --app-key YOUR_KEY
soundtouch-cli --host 192.168.1.100 speaker url --url "https://example.com/doorbell.mp3" --app-key YOUR_KEY
soundtouch-cli --host 192.168.1.100 speaker beep
# Real-time monitoring
soundtouch-cli --host 192.168.1.100 events subscribe
```
For full CLI documentation, see the [CLI Reference](https://gesellix.github.io/Bose-SoundTouch/guides/CLI-REFERENCE.html).
### SoundTouch Service (Cloud Shutdown Protection)
The `soundtouch-service` is a local server that emulates Bose's cloud services. This is critical for keeping your speakers functional after the **Bose Cloud Shutdown in May 2026**.
#### Key Features:
- **🏠 Local Emulation**: BMX and Marge service implementation
- **🔌 Easy Setup**: Activate SSH via USB stick (`remote_services` file)
- **🔧 Device Migration**: Seamlessly transition devices to local control
- **🌐 Web Management UI**: Easy browser-based setup and management
- **🎮 Stockholm Mini**: A minimal reverse-engineered UI for device control (accessible at `/web/stockholm-mini/`)
- **💾 Persistent Data**: Store presets, recents, and sources locally
- **🔄 Endpoint Mirroring**: Asynchronously mirror local requests to Bose cloud for parity testing
- **⚖️ Parity Logging**: Detect and record discrepancies between local and official Bose responses
- **📝 HTTP Recording**: Persist all interactions as re-playable `.http` files
- **🧹 Session Management**: Manage and cleanup recorded interaction sessions
#### Quick Start:
```bash
# Start the service
soundtouch-service
```
Open `http://localhost:8000` in your browser to manage your devices. Documentation is also available directly through the web interface.
For a comprehensive guide on transitioning your system, see the [Bose Cloud Shutdown: Survival Guide](https://gesellix.github.io/Bose-SoundTouch/guides/SURVIVAL-GUIDE.html).
Detailed service configuration and Docker instructions can be found in [SoundTouch Service Guide](https://gesellix.github.io/Bose-SoundTouch/guides/SOUNDTOUCH-SERVICE.html).
For professional migration tips and safety measures, see the [Migration & Safety Guide](https://gesellix.github.io/Bose-SoundTouch/guides/MIGRATION-SAFETY.html).
### Library Usage
#### Basic Control
@@ -300,8 +325,8 @@ func main() {
Port: 8090,
})
// Play Text-to-Speech message
err := c.PlayTTS("Welcome home!", "your-app-key", 70)
// Play Text-to-Speech message (language code "EN", "DE", etc.)
err := c.PlayTTS("Welcome home!", "your-app-key", "EN", 70)
if err != nil {
log.Fatal(err)
}
@@ -364,18 +389,19 @@ This library supports all Bose SoundTouch-compatible devices, including:
## Documentation
- 📖 [Contributing Guide](CONTRIBUTING.md) - How to contribute to the project
- 📚 [API Reference](docs/API-Endpoints-Overview.md) - Complete endpoint documentation
- 🔧 [CLI Reference](docs/CLI-REFERENCE.md) - Command-line tool guide
- 🎯 [Getting Started](docs/GETTING-STARTED.md) - Detailed setup and usage
- 📻 [Preset Quick Start](docs/PRESET-QUICKSTART.md) - Favorite content management
- 🧭 [Navigation Guide](docs/NAVIGATION-GUIDE.md) - Content browsing and station management
- 📋 [Navigation API Reference](docs/API-NAVIGATION-REFERENCE.md) - Navigation API documentation
- ⚙️ [Advanced Features](docs/SYSTEM-ENDPOINTS.md) - Advanced functionality
- 🏠 [Multiroom Setup](docs/zone-management.md) - Zone configuration guide
- [WebSocket Events](docs/websocket-events.md) - Real-time event handling
- 🔔 [Speaker Notifications](docs/SPEAKER_ENDPOINT.md) - TTS and audio notifications guide
- 🔍 [Device Discovery](docs/DISCOVERY.md) - Discovery configuration
- 🛠️ [Troubleshooting](docs/TROUBLESHOOTING.md) - Common issues and solutions
- 📚 [API Reference](https://gesellix.github.io/Bose-SoundTouch/reference/API-ENDPOINTS.html) - Complete endpoint documentation
- 🔧 [CLI Reference](https://gesellix.github.io/Bose-SoundTouch/guides/CLI-REFERENCE.html) - Command-line tool guide
- 🌐 [SoundTouch Service Guide](https://gesellix.github.io/Bose-SoundTouch/guides/SOUNDTOUCH-SERVICE.html) - Local service setup and migration
- 🎯 [Getting Started](https://gesellix.github.io/Bose-SoundTouch/guides/GETTING-STARTED.html) - Detailed setup and usage
- 📻 [Preset Quick Start](https://gesellix.github.io/Bose-SoundTouch/PRESET-QUICKSTART.md) - Favorite content management
- 🧭 [Navigation Guide](https://gesellix.github.io/Bose-SoundTouch/NAVIGATION-GUIDE.md) - Content browsing and station management
- 📋 [Navigation API Reference](https://gesellix.github.io/Bose-SoundTouch/API-NAVIGATION-REFERENCE.md) - Navigation API documentation
- ⚙️ [Advanced Features](https://gesellix.github.io/Bose-SoundTouch/reference/SYSTEM-ENDPOINTS.html) - Advanced functionality
- 🏠 [Multiroom Setup](https://gesellix.github.io/Bose-SoundTouch/reference/ZONE-MANAGEMENT.html) - Zone configuration guide
- [WebSocket Events](https://gesellix.github.io/Bose-SoundTouch/reference/WEBSOCKET-EVENTS.html) - Real-time event handling
- 🔔 [Speaker Notifications](https://gesellix.github.io/Bose-SoundTouch/reference/SPEAKER-ENDPOINT.html) - TTS and audio notifications guide
- 🔍 [Device Discovery](https://gesellix.github.io/Bose-SoundTouch/reference/DISCOVERY.html) - Discovery configuration
- 🛠️ [Troubleshooting](https://gesellix.github.io/Bose-SoundTouch/guides/TROUBLESHOOTING.html) - Common issues and solutions
## Development
@@ -456,29 +482,68 @@ This Go library will continue to work as it uses the local Web API for direct de
**Community Alternatives**: See the [Related Projects](#related-projects) section below for additional tools like SoundCork that provide cloud service alternatives and the SoundTouch Plus project that offers comprehensive Home Assistant integration.
## Related Projects
## Related Projects & Credits
### SoundTouch Plus
This project builds upon the excellent work of several community projects:
### SoundCork 🍾
- **Project**: [SoundCork - SoundTouch API Intercept](https://github.com/deborahgu/soundcork)
- **Authors**: Deborah Kaplan and contributors
- **Our Implementation**: The `soundtouch-service` in this project is heavily inspired by SoundCork's Python implementation. SoundCork pioneered the approach of intercepting and emulating Bose's cloud services, providing the foundation for offline SoundTouch operation.
- **Key Contributions**: Service emulation architecture, BMX/Marge endpoint discovery, device migration strategies
- **License**: MIT License
### ÜberBöse API 🎵
- **Project**: [ÜberBöse API](https://github.com/julius-d/ueberboese-api)
- **Author**: Julius
- **Our Implementation**: This project provided valuable insights into advanced SoundTouch API endpoints and helped make our implementation more complete, particularly for content navigation and advanced device features.
- **Key Contributions**: Extended API endpoint documentation, advanced feature discovery
- **License**: MIT License
### SoundTouch Plus 🏠
- **Project**: [SoundTouch Plus Home Assistant Component](https://github.com/thlucas1/homeassistantcomponent_soundtouchplus)
- **Wiki**: [SoundTouch WebServices API Documentation](https://github.com/thlucas1/homeassistantcomponent_soundtouchplus/wiki/SoundTouch-WebServices-API)
- **Description**: Comprehensive Home Assistant integration with extensive API documentation
- **Contribution**: The SoundTouch Plus Wiki provided invaluable documentation of working endpoints beyond the official API, enabling the preset management and content navigation features in this library
- **Author**: Todd Lucas
- **Our Implementation**: The comprehensive API documentation in the SoundTouch Plus Wiki provided invaluable insights into undocumented endpoints beyond the official API, enabling our preset management and content navigation features.
- **Key Contributions**: Extensive API endpoint documentation, real-world usage patterns
- **License**: MIT License
### SoundCork
- **Project**: [SoundCork - SoundTouch API Intercept](https://github.com/deborahgu/soundcork)
- **Description**: Intercept API for Bose SoundTouch devices after cloud service discontinuation
- **Purpose**: Provides a local alternative to cloud-based SoundTouch services post-sunset
- **Compatibility**: Complements this Go library by extending functionality beyond the local device API
### SoundTouch Hook 🪝
- **Project**: [Bose SoundTouch Hook](https://github.com/CodeFinder2/bose-soundtouch-hook)
- **Author**: Adrian Böckenkamp
- **Our Implementation**: This project provides a powerful framework for intercepting and hooking into internal device processes using `LD_PRELOAD`. It was instrumental in verifying internal function calls and understanding how the device validates cloud domains.
- **Key Contributions**: Reverse engineering framework, process hooking, cross-compilation toolchain
- **License**: GPL-3.0 License
These projects form a comprehensive ecosystem for SoundTouch device management and provide alternatives to Bose's discontinued cloud services.
### Community Ecosystem
These projects together form a comprehensive ecosystem for SoundTouch device management:
- **This Project**: Go library + CLI + service for programmatic control and offline operation
- **SoundCork**: Python-based service interception and cloud replacement
- **SoundTouch Plus**: Home Assistant integration with extensive device support
- **ÜberBöse**: API research and advanced endpoint discovery
- **SoundTouch Hook**: Advanced reverse engineering and process instrumentation
We are grateful to these projects and their maintainers for paving the way and providing the foundation that made this comprehensive Go implementation possible. The SoundTouch community's collaborative approach to reverse engineering and documentation has been invaluable.
### Contributing Back
If you discover new endpoints, features, or improvements through this library, please consider contributing back to these projects as well. The stronger our community ecosystem becomes, the better we can support SoundTouch devices beyond Bose's official support timeline.
## Support
- 🐛 **Bug Reports**: [Create an issue](https://github.com/gesellix/bose-soundtouch/issues/new)
- 💡 **Feature Requests**: [Start a discussion](https://github.com/gesellix/bose-soundtouch/discussions)
-**Questions**: Check [existing discussions](https://github.com/gesellix/bose-soundtouch/discussions)
- 📖 **Documentation**: Browse the [docs/](docs/) directory
- 📖 **Documentation**: [Online Documentation](https://gesellix.github.io/Bose-SoundTouch/)
- 🔍 **New Discoveries**: [Undocumented Community Features](https://gesellix.github.io/Bose-SoundTouch/UNDOCUMENTED-COMMUNITY-FEATURES.md)
- 🌐 **Upstream Analysis**: [Upstream URLs & Domains](https://gesellix.github.io/Bose-SoundTouch/analysis/UPSTREAM-URLS.html)
- 🔧 **Redirection Guide**: [Device Redirect Methods](https://gesellix.github.io/Bose-SoundTouch/analysis/DEVICE-REDIRECT-METHODS.html)
- 🐣 **Initial Setup**: [Device Initial Setup Variants](https://gesellix.github.io/Bose-SoundTouch/guides/DEVICE-INITIAL-SETUP.html)
- 📜 **Logging & Debugging**: [Device Logging Guide](https://gesellix.github.io/Bose-SoundTouch/DEVICE-LOGGING.md)
- 🔒 **HTTPS & CA Setup**: [HTTPS & Custom CA Guide](https://gesellix.github.io/Bose-SoundTouch/guides/HTTPS-SETUP.html)
---
**Star this project** ⭐ if you find it useful!
**Star this project** ⭐ if you find it useful!
+242
View File
@@ -0,0 +1,242 @@
// Package main provides a debug tool for analyzing device consolidation and migration scenarios.
package main
import (
"fmt"
"log"
"os"
"path/filepath"
"github.com/gesellix/bose-soundtouch/pkg/models"
"github.com/gesellix/bose-soundtouch/pkg/service/datastore"
)
func main() {
if len(os.Args) < 2 {
fmt.Println("Usage: debug-consolidation <data-directory>")
fmt.Println("Example: debug-consolidation /var/lib/soundtouch-service")
os.Exit(1)
}
dataDir := os.Args[1]
fmt.Printf("🔍 Analyzing device consolidation in: %s\n", dataDir)
// Initialize datastore
ds := datastore.NewDataStore(dataDir)
// List all devices
devices, err := ds.ListAllDevices()
if err != nil {
log.Fatalf("Failed to list devices: %v", err)
}
fmt.Printf("📱 Found %d device entries:\n", len(devices))
for i := range devices {
device := &devices[i]
fmt.Printf(" %d. %s (Account: %s)\n", i+1, device.DeviceID, device.AccountID)
fmt.Printf(" Name: %s\n", device.Name)
fmt.Printf(" IP: %s, MAC: %s, Serial: %s\n",
device.IPAddress, device.MacAddress, device.DeviceSerialNumber)
// Check directory contents
deviceDir := ds.AccountDeviceDir(device.AccountID, device.DeviceID)
analyzeDeviceDirectory(deviceDir, device.DeviceID)
fmt.Println()
}
// Group devices by potential physical device
fmt.Println("🔄 Analyzing potential consolidation opportunities:")
deviceGroups := groupDevicesByIdentity(devices)
for i, group := range deviceGroups {
if len(group) <= 1 {
continue
}
fmt.Printf(" Group %d - %d entries for same physical device:\n", i+1, len(group))
for i := range group {
device := &group[i]
deviceDir := ds.AccountDeviceDir(device.AccountID, device.DeviceID)
fileCount := countFiles(deviceDir)
fmt.Printf(" - %s (%d files)\n", device.DeviceID, fileCount)
}
// Recommend consolidation target
macDevice := findMACBasedDevice(group)
if macDevice != nil {
fmt.Printf(" → Recommend keeping: %s (MAC-based)\n", macDevice.DeviceID)
} else {
fmt.Printf(" → No clear MAC-based target found\n")
}
fmt.Println()
}
}
func analyzeDeviceDirectory(dirPath, deviceID string) {
entries, err := os.ReadDir(dirPath)
if err != nil {
fmt.Printf(" Directory: %s (Error: %v)\n", dirPath, err)
return
}
fmt.Printf(" Directory: %s (%d files)\n", dirPath, len(entries))
// Check for important files
importantFiles := []string{"DeviceInfo.xml", "Presets.xml", "Recents.xml", "Sources.xml"}
for _, fileName := range importantFiles {
filePath := filepath.Join(dirPath, fileName)
if stat, err := os.Stat(filePath); err == nil {
status := "✓"
if stat.Size() == 0 {
status = "⚠️ (empty)"
} else if stat.Size() < 100 {
status = "⚠️ (very small)"
}
fmt.Printf(" %s %s (%d bytes)\n", status, fileName, stat.Size())
} else {
fmt.Printf(" ❌ %s (missing)\n", fileName)
}
}
// Check if deviceID looks like MAC address
if isLikelyMACAddress(deviceID) {
fmt.Printf(" 📍 Device ID appears to be MAC address format\n")
} else {
fmt.Printf(" 📍 Device ID appears to be %s format\n", guessIDType(deviceID))
}
}
func countFiles(dirPath string) int {
entries, err := os.ReadDir(dirPath)
if err != nil {
return 0
}
count := 0
for _, entry := range entries {
if !entry.IsDir() {
count++
}
}
return count
}
func groupDevicesByIdentity(devices []models.ServiceDeviceInfo) [][]models.ServiceDeviceInfo {
var groups [][]models.ServiceDeviceInfo
// Simple grouping by MAC address and serial number
macGroups := make(map[string][]models.ServiceDeviceInfo)
serialGroups := make(map[string][]models.ServiceDeviceInfo)
ipGroups := make(map[string][]models.ServiceDeviceInfo)
for i := range devices {
device := &devices[i]
// Group by MAC address
if device.MacAddress != "" {
macGroups[device.MacAddress] = append(macGroups[device.MacAddress], *device)
}
// Group by serial number
if device.DeviceSerialNumber != "" {
serialGroups[device.DeviceSerialNumber] = append(serialGroups[device.DeviceSerialNumber], *device)
}
// Group by IP address
if device.IPAddress != "" {
ipGroups[device.IPAddress] = append(ipGroups[device.IPAddress], *device)
}
}
// Merge groups - prioritize MAC address grouping
processed := make(map[string]bool)
for _, macDevices := range macGroups {
if len(macDevices) > 1 {
groups = append(groups, macDevices)
for i := range macDevices {
processed[macDevices[i].DeviceID] = true
}
}
}
// Check for serial number groups not already processed
for _, serialDevices := range serialGroups {
if len(serialDevices) > 1 {
unprocessed := []models.ServiceDeviceInfo{}
for i := range serialDevices {
if !processed[serialDevices[i].DeviceID] {
unprocessed = append(unprocessed, serialDevices[i])
}
}
if len(unprocessed) > 1 {
groups = append(groups, unprocessed)
for i := range unprocessed {
processed[unprocessed[i].DeviceID] = true
}
}
}
}
return groups
}
func findMACBasedDevice(devices []models.ServiceDeviceInfo) *models.ServiceDeviceInfo {
for i := range devices {
if isLikelyMACAddress(devices[i].DeviceID) {
return &devices[i]
}
}
return nil
}
func isLikelyMACAddress(id string) bool {
// MAC addresses are typically 12 hex characters without separators
// or 17 characters with separators (XX:XX:XX:XX:XX:XX)
if len(id) == 12 {
for _, c := range id {
if (c < '0' || c > '9') && (c < 'A' || c > 'F') && (c < 'a' || c > 'f') {
return false
}
}
return true
}
return false
}
func guessIDType(id string) string {
if len(id) > 15 && (id[0] == 'I' || id[0] == 'K') {
return "serial number"
}
// Check if it looks like an IP address
if len(id) >= 7 && len(id) <= 15 {
dotCount := 0
for _, c := range id {
if c == '.' {
dotCount++
} else if c < '0' || c > '9' {
break
}
}
if dotCount == 3 {
return "IP address"
}
}
return "unknown"
}
+675
View File
@@ -0,0 +1,675 @@
package main
import (
"fmt"
"strings"
"github.com/gesellix/bose-soundtouch/pkg/models"
"github.com/urfave/cli/v2"
)
// createCredentialsForSource creates credentials for the specified source type
func createCredentialsForSource(source, user, password, displayName string) *models.MusicServiceCredentials {
switch source {
case "SPOTIFY":
return models.NewSpotifyCredentials(user, password)
case "PANDORA":
return models.NewPandoraCredentials(user, password)
case "AMAZON":
return models.NewAmazonMusicCredentials(user, password)
case "DEEZER":
return models.NewDeezerCredentials(user, password)
case "IHEART":
return models.NewIHeartRadioCredentials(user, password)
case "STORED_MUSIC":
if displayName == "" {
displayName = "Network Music Library"
}
return models.NewStoredMusicCredentials(user, displayName)
default:
// Generic credentials for other services
if displayName == "" {
displayName = source
}
return models.NewMusicServiceCredentials(source, displayName, user, password)
}
}
// validateAccountInput validates the input parameters for account management
func validateAccountInput(source, user, password string) error {
if source == "" {
return fmt.Errorf("source is required (use --source)")
}
if user == "" {
return fmt.Errorf("user is required (use --user)")
}
// STORED_MUSIC doesn't require a password
if source != "STORED_MUSIC" && password == "" {
return fmt.Errorf("password is required for %s (use --password)", source)
}
return nil
}
// addMusicServiceAccount handles adding a music service account
func addMusicServiceAccount(c *cli.Context) error {
clientConfig := GetClientConfig(c)
client, err := CreateSoundTouchClient(clientConfig)
if err != nil {
return err
}
source := strings.ToUpper(c.String("source"))
user := c.String("user")
password := c.String("password")
displayName := c.String("name")
if validationErr := validateAccountInput(source, user, password); validationErr != nil {
return validationErr
}
PrintDeviceHeader(fmt.Sprintf("Adding %s account", source), clientConfig.Host, clientConfig.Port)
credentials := createCredentialsForSource(source, user, password, displayName)
// Override display name if provided
if c.IsSet("name") {
credentials.DisplayName = displayName
}
fmt.Printf(" Service: %s\n", credentials.GetDescription())
fmt.Printf(" User: %s\n", user)
if source == "STORED_MUSIC" {
fmt.Printf(" Type: Network Music Library\n")
} else {
fmt.Printf(" Type: Streaming Service\n")
}
err = client.SetMusicServiceAccount(credentials)
if err != nil {
return fmt.Errorf("failed to add music service account: %w", err)
}
PrintSuccess(fmt.Sprintf("%s account added successfully", source))
// Show next steps
fmt.Printf("\n💡 Next Steps:\n")
fmt.Printf(" • Check available sources: soundtouch-cli --host %s source list\n", clientConfig.Host)
fmt.Printf(" • Select this source: soundtouch-cli --host %s source select --source %s --account %s\n", clientConfig.Host, source, user)
return nil
}
// removeMusicServiceAccount handles removing a music service account
func removeMusicServiceAccount(c *cli.Context) error {
clientConfig := GetClientConfig(c)
client, err := CreateSoundTouchClient(clientConfig)
if err != nil {
return err
}
source := strings.ToUpper(c.String("source"))
user := c.String("user")
displayName := c.String("name")
if source == "" {
return fmt.Errorf("source is required (use --source)")
}
if user == "" {
return fmt.Errorf("user is required (use --user)")
}
PrintDeviceHeader(fmt.Sprintf("Removing %s account", source), clientConfig.Host, clientConfig.Port)
var credentials *models.MusicServiceCredentials
// Create credentials for removal (empty password)
switch source {
case "SPOTIFY":
credentials = models.NewSpotifyCredentials(user, "")
case "PANDORA":
credentials = models.NewPandoraCredentials(user, "")
case "AMAZON":
credentials = models.NewAmazonMusicCredentials(user, "")
case "DEEZER":
credentials = models.NewDeezerCredentials(user, "")
case "IHEART":
credentials = models.NewIHeartRadioCredentials(user, "")
case "STORED_MUSIC":
if displayName == "" {
displayName = "Network Music Library"
}
credentials = models.NewStoredMusicCredentials(user, displayName)
default:
// Generic credentials for other services
if displayName == "" {
displayName = source
}
credentials = models.NewMusicServiceCredentials(source, displayName, user, "")
}
// Override display name if provided
if c.IsSet("name") {
credentials.DisplayName = displayName
}
fmt.Printf(" Service: %s\n", credentials.GetDescription())
fmt.Printf(" User: %s\n", user)
err = client.RemoveMusicServiceAccount(credentials)
if err != nil {
return fmt.Errorf("failed to remove music service account: %w", err)
}
PrintSuccess(fmt.Sprintf("%s account removed successfully", source))
return nil
}
// addSpotifyAccount is a convenience command for adding Spotify accounts
func addSpotifyAccount(c *cli.Context) error {
clientConfig := GetClientConfig(c)
client, err := CreateSoundTouchClient(clientConfig)
if err != nil {
return err
}
user := c.String("user")
password := c.String("password")
if user == "" {
return fmt.Errorf("user is required (use --user)")
}
if password == "" {
return fmt.Errorf("password is required (use --password)")
}
PrintDeviceHeader("Adding Spotify Premium account", clientConfig.Host, clientConfig.Port)
fmt.Printf(" User: %s\n", user)
fmt.Printf(" Service: Spotify Premium\n")
err = client.AddSpotifyAccount(user, password)
if err != nil {
return fmt.Errorf("failed to add Spotify account: %w", err)
}
PrintSuccess("Spotify account added successfully")
// Show next steps
fmt.Printf("\n💡 Next Steps:\n")
fmt.Printf(" • Check available sources: soundtouch-cli --host %s source list\n", clientConfig.Host)
fmt.Printf(" • Select Spotify: soundtouch-cli --host %s source spotify\n", clientConfig.Host)
return nil
}
// removeSpotifyAccount is a convenience command for removing Spotify accounts
func removeSpotifyAccount(c *cli.Context) error {
clientConfig := GetClientConfig(c)
client, err := CreateSoundTouchClient(clientConfig)
if err != nil {
return err
}
user := c.String("user")
if user == "" {
return fmt.Errorf("user is required (use --user)")
}
PrintDeviceHeader("Removing Spotify account", clientConfig.Host, clientConfig.Port)
fmt.Printf(" User: %s\n", user)
err = client.RemoveSpotifyAccount(user)
if err != nil {
return fmt.Errorf("failed to remove Spotify account: %w", err)
}
PrintSuccess("Spotify account removed successfully")
return nil
}
// addPandoraAccount is a convenience command for adding Pandora accounts
func addPandoraAccount(c *cli.Context) error {
clientConfig := GetClientConfig(c)
client, err := CreateSoundTouchClient(clientConfig)
if err != nil {
return err
}
user := c.String("user")
password := c.String("password")
if user == "" {
return fmt.Errorf("user is required (use --user)")
}
if password == "" {
return fmt.Errorf("password is required (use --password)")
}
PrintDeviceHeader("Adding Pandora account", clientConfig.Host, clientConfig.Port)
fmt.Printf(" User: %s\n", user)
fmt.Printf(" Service: Pandora Music Service\n")
err = client.AddPandoraAccount(user, password)
if err != nil {
return fmt.Errorf("failed to add Pandora account: %w", err)
}
PrintSuccess("Pandora account added successfully")
// Show next steps
fmt.Printf("\n💡 Next Steps:\n")
fmt.Printf(" • Check available sources: soundtouch-cli --host %s source list\n", clientConfig.Host)
fmt.Printf(" • Select Pandora: soundtouch-cli --host %s source select --source PANDORA --account %s\n", clientConfig.Host, user)
return nil
}
// removePandoraAccount is a convenience command for removing Pandora accounts
func removePandoraAccount(c *cli.Context) error {
clientConfig := GetClientConfig(c)
client, err := CreateSoundTouchClient(clientConfig)
if err != nil {
return err
}
user := c.String("user")
if user == "" {
return fmt.Errorf("user is required (use --user)")
}
PrintDeviceHeader("Removing Pandora account", clientConfig.Host, clientConfig.Port)
fmt.Printf(" User: %s\n", user)
err = client.RemovePandoraAccount(user)
if err != nil {
return fmt.Errorf("failed to remove Pandora account: %w", err)
}
PrintSuccess("Pandora account removed successfully")
return nil
}
// addStoredMusicAccount is a convenience command for adding STORED_MUSIC accounts
func addStoredMusicAccount(c *cli.Context) error {
clientConfig := GetClientConfig(c)
client, err := CreateSoundTouchClient(clientConfig)
if err != nil {
return err
}
user := c.String("user")
displayName := c.String("name")
if user == "" {
return fmt.Errorf("user is required (use --user) - this should be the UPnP server GUID with /0 suffix")
}
if displayName == "" {
displayName = "Network Music Library"
}
PrintDeviceHeader("Adding network music library", clientConfig.Host, clientConfig.Port)
fmt.Printf(" Server ID: %s\n", user)
fmt.Printf(" Display Name: %s\n", displayName)
fmt.Printf(" Type: UPnP/DLNA Media Server\n")
err = client.AddStoredMusicAccount(user, displayName)
if err != nil {
return fmt.Errorf("failed to add network music library: %w", err)
}
PrintSuccess("Network music library added successfully")
// Show next steps
fmt.Printf("\n💡 Next Steps:\n")
fmt.Printf(" • Check available sources: soundtouch-cli --host %s source list\n", clientConfig.Host)
fmt.Printf(" • Browse library: soundtouch-cli --host %s browse stored-music --account %s\n", clientConfig.Host, user)
return nil
}
// addAmazonMusicAccount is a convenience command for adding Amazon Music accounts
func addAmazonMusicAccount(c *cli.Context) error {
clientConfig := GetClientConfig(c)
client, err := CreateSoundTouchClient(clientConfig)
if err != nil {
return err
}
user := c.String("user")
password := c.String("password")
if user == "" {
return fmt.Errorf("user is required (use --user)")
}
if password == "" {
return fmt.Errorf("password is required (use --password)")
}
PrintDeviceHeader("Adding Amazon Music account", clientConfig.Host, clientConfig.Port)
fmt.Printf(" User: %s\n", user)
fmt.Printf(" Service: Amazon Music\n")
err = client.AddAmazonMusicAccount(user, password)
if err != nil {
return fmt.Errorf("failed to add Amazon Music account: %w", err)
}
PrintSuccess("Amazon Music account added successfully")
// Show next steps
fmt.Printf("\n💡 Next Steps:\n")
fmt.Printf(" • Check available sources: soundtouch-cli --host %s source list\n", clientConfig.Host)
fmt.Printf(" • Select Amazon Music: soundtouch-cli --host %s source select --source AMAZON --account %s\n", clientConfig.Host, user)
return nil
}
// removeAmazonMusicAccount is a convenience command for removing Amazon Music accounts
func removeAmazonMusicAccount(c *cli.Context) error {
clientConfig := GetClientConfig(c)
client, err := CreateSoundTouchClient(clientConfig)
if err != nil {
return err
}
user := c.String("user")
if user == "" {
return fmt.Errorf("user is required (use --user)")
}
PrintDeviceHeader("Removing Amazon Music account", clientConfig.Host, clientConfig.Port)
fmt.Printf(" User: %s\n", user)
err = client.RemoveAmazonMusicAccount(user)
if err != nil {
return fmt.Errorf("failed to remove Amazon Music account: %w", err)
}
PrintSuccess("Amazon Music account removed successfully")
return nil
}
// addDeezerAccount is a convenience command for adding Deezer accounts
func addDeezerAccount(c *cli.Context) error {
clientConfig := GetClientConfig(c)
client, err := CreateSoundTouchClient(clientConfig)
if err != nil {
return err
}
user := c.String("user")
password := c.String("password")
if user == "" {
return fmt.Errorf("user is required (use --user)")
}
if password == "" {
return fmt.Errorf("password is required (use --password)")
}
PrintDeviceHeader("Adding Deezer Premium account", clientConfig.Host, clientConfig.Port)
fmt.Printf(" User: %s\n", user)
fmt.Printf(" Service: Deezer Premium\n")
err = client.AddDeezerAccount(user, password)
if err != nil {
return fmt.Errorf("failed to add Deezer account: %w", err)
}
PrintSuccess("Deezer account added successfully")
// Show next steps
fmt.Printf("\n💡 Next Steps:\n")
fmt.Printf(" • Check available sources: soundtouch-cli --host %s source list\n", clientConfig.Host)
fmt.Printf(" • Select Deezer: soundtouch-cli --host %s source select --source DEEZER --account %s\n", clientConfig.Host, user)
return nil
}
// removeDeezerAccount is a convenience command for removing Deezer accounts
func removeDeezerAccount(c *cli.Context) error {
clientConfig := GetClientConfig(c)
client, err := CreateSoundTouchClient(clientConfig)
if err != nil {
return err
}
user := c.String("user")
if user == "" {
return fmt.Errorf("user is required (use --user)")
}
PrintDeviceHeader("Removing Deezer account", clientConfig.Host, clientConfig.Port)
fmt.Printf(" User: %s\n", user)
err = client.RemoveDeezerAccount(user)
if err != nil {
return fmt.Errorf("failed to remove Deezer account: %w", err)
}
PrintSuccess("Deezer account removed successfully")
return nil
}
// addIHeartRadioAccount is a convenience command for adding iHeartRadio accounts
func addIHeartRadioAccount(c *cli.Context) error {
clientConfig := GetClientConfig(c)
client, err := CreateSoundTouchClient(clientConfig)
if err != nil {
return err
}
user := c.String("user")
password := c.String("password")
if user == "" {
return fmt.Errorf("user is required (use --user)")
}
if password == "" {
return fmt.Errorf("password is required (use --password)")
}
PrintDeviceHeader("Adding iHeartRadio account", clientConfig.Host, clientConfig.Port)
fmt.Printf(" User: %s\n", user)
fmt.Printf(" Service: iHeartRadio\n")
err = client.AddIHeartRadioAccount(user, password)
if err != nil {
return fmt.Errorf("failed to add iHeartRadio account: %w", err)
}
PrintSuccess("iHeartRadio account added successfully")
// Show next steps
fmt.Printf("\n💡 Next Steps:\n")
fmt.Printf(" • Check available sources: soundtouch-cli --host %s source list\n", clientConfig.Host)
fmt.Printf(" • Select iHeartRadio: soundtouch-cli --host %s source select --source IHEART --account %s\n", clientConfig.Host, user)
return nil
}
// removeIHeartRadioAccount is a convenience command for removing iHeartRadio accounts
func removeIHeartRadioAccount(c *cli.Context) error {
clientConfig := GetClientConfig(c)
client, err := CreateSoundTouchClient(clientConfig)
if err != nil {
return err
}
user := c.String("user")
if user == "" {
return fmt.Errorf("user is required (use --user)")
}
PrintDeviceHeader("Removing iHeartRadio account", clientConfig.Host, clientConfig.Port)
fmt.Printf(" User: %s\n", user)
err = client.RemoveIHeartRadioAccount(user)
if err != nil {
return fmt.Errorf("failed to remove iHeartRadio account: %w", err)
}
PrintSuccess("iHeartRadio account removed successfully")
return nil
}
// removeStoredMusicAccount is a convenience command for removing STORED_MUSIC accounts
func removeStoredMusicAccount(c *cli.Context) error {
clientConfig := GetClientConfig(c)
client, err := CreateSoundTouchClient(clientConfig)
if err != nil {
return err
}
user := c.String("user")
displayName := c.String("name")
if user == "" {
return fmt.Errorf("user is required (use --user)")
}
if displayName == "" {
displayName = "Network Music Library"
}
PrintDeviceHeader("Removing network music library", clientConfig.Host, clientConfig.Port)
fmt.Printf(" Server ID: %s\n", user)
fmt.Printf(" Display Name: %s\n", displayName)
err = client.RemoveStoredMusicAccount(user, displayName)
if err != nil {
return fmt.Errorf("failed to remove network music library: %w", err)
}
PrintSuccess("Network music library removed successfully")
return nil
}
// listMusicServiceAccounts shows configured music service accounts from sources
func listMusicServiceAccounts(c *cli.Context) error {
clientConfig := GetClientConfig(c)
client, err := CreateSoundTouchClient(clientConfig)
if err != nil {
return err
}
PrintDeviceHeader("Music service accounts", clientConfig.Host, clientConfig.Port)
sources, err := client.GetSources()
if err != nil {
return fmt.Errorf("failed to get sources: %w", err)
}
// Filter for streaming/music service sources
musicSources := []string{"SPOTIFY", "PANDORA", "AMAZON", "DEEZER", "IHEART", "STORED_MUSIC", "LOCAL_MUSIC"}
found := false
for _, musicSource := range musicSources {
sourcesOfType := sources.GetSourcesByType(musicSource)
if len(sourcesOfType) > 0 {
found = true
fmt.Printf("\n📱 %s:\n", getServiceDisplayName(musicSource))
for _, source := range sourcesOfType {
status := "🔴 Unavailable"
if source.Status == models.SourceStatusReady {
status = "🟢 Ready"
}
accountInfo := ""
if source.SourceAccount != "" && source.SourceAccount != source.Source {
accountInfo = fmt.Sprintf(" (%s)", source.SourceAccount)
}
fmt.Printf(" %s %s%s\n", status, source.GetDisplayName(), accountInfo)
}
}
}
if !found {
fmt.Printf(" 📭 No music service accounts configured\n")
fmt.Printf("\n💡 Add accounts with:\n")
fmt.Printf(" • soundtouch-cli --host %s account add-spotify --user <email> --password <pass>\n", clientConfig.Host)
fmt.Printf(" • soundtouch-cli --host %s account add-pandora --user <user> --password <pass>\n", clientConfig.Host)
fmt.Printf(" • soundtouch-cli --host %s account add --source AMAZON --user <user> --password <pass>\n", clientConfig.Host)
}
return nil
}
// getServiceDisplayName returns a user-friendly display name for a service
func getServiceDisplayName(source string) string {
switch source {
case "SPOTIFY":
return "Spotify"
case "PANDORA":
return "Pandora"
case "AMAZON":
return "Amazon Music"
case "DEEZER":
return "Deezer"
case "IHEART":
return "iHeartRadio"
case "STORED_MUSIC":
return "Network Libraries"
case "LOCAL_MUSIC":
return "Local Music Servers"
default:
return source
}
}
+10
View File
@@ -389,6 +389,10 @@ func handleSpecialMessage(message *models.SpecialMessage, filters map[string]boo
if !filters["userActivity"] {
return
}
case models.MessageTypeUserInactivity:
if !filters["userInactivity"] {
return
}
}
}
@@ -402,6 +406,12 @@ func handleSpecialMessage(message *models.SpecialMessage, filters map[string]boo
case models.MessageTypeUserActivity:
fmt.Printf("\n👤 User Activity [%s]\n", message.DeviceID)
if verbose {
fmt.Printf(" ⏰ Timestamp: %s\n", message.Timestamp.Format("15:04:05"))
}
case models.MessageTypeUserInactivity:
fmt.Printf("\n💤 User Inactivity [%s]\n", message.DeviceID)
if verbose {
fmt.Printf(" ⏰ Timestamp: %s\n", message.Timestamp.Format("15:04:05"))
}
+357
View File
@@ -0,0 +1,357 @@
package main
import (
"fmt"
"strings"
"time"
"github.com/gesellix/bose-soundtouch/pkg/models"
"github.com/urfave/cli/v2"
)
// introspectService handles getting introspect data for a specific service
func introspectService(c *cli.Context) error {
clientConfig := GetClientConfig(c)
client, err := CreateSoundTouchClient(clientConfig)
if err != nil {
return err
}
source := strings.ToUpper(c.String("source"))
sourceAccount := c.String("account")
// Check service availability first
checker := NewServiceAvailabilityChecker(client)
if !checker.CheckSourceAvailable(source, fmt.Sprintf("get introspect data for %s", strings.ToLower(source))) {
PrintWarning(fmt.Sprintf("Service %s may not be available, but continuing with introspect request...", source))
}
PrintDeviceHeader(fmt.Sprintf("Getting introspect data for %s", source), clientConfig.Host, clientConfig.Port)
if sourceAccount != "" {
fmt.Printf("Source Account: %s\n", sourceAccount)
}
fmt.Println()
response, err := client.Introspect(source, sourceAccount)
if err != nil {
return fmt.Errorf("failed to get introspect data: %w", err)
}
// Print basic information
fmt.Printf("=== %s Service Introspect Data ===\n", source)
printIntrospectBasicInfo(response)
// Print service state
fmt.Printf("\n=== Service State ===\n")
printIntrospectServiceState(response)
// Print capabilities
fmt.Printf("\n=== Service Capabilities ===\n")
printIntrospectCapabilities(response)
// Print history information
if response.GetMaxHistorySize() > 0 {
fmt.Printf("\n=== Content History ===\n")
printIntrospectHistory(response)
}
// Print technical details
if response.TokenLastChangedTimeSeconds > 0 || response.PlayStatusState != "" {
fmt.Printf("\n=== Technical Details ===\n")
printIntrospectTechnicalDetails(response)
}
return nil
}
// introspectSpotify handles getting Spotify introspect data using convenience method
func introspectSpotify(c *cli.Context) error {
clientConfig := GetClientConfig(c)
client, err := CreateSoundTouchClient(clientConfig)
if err != nil {
return err
}
sourceAccount := c.String("account")
// Check Spotify availability
checker := NewServiceAvailabilityChecker(client)
if !checker.ValidateSpotifyAvailable("get Spotify introspect data") {
PrintWarning("Spotify may not be available, but continuing with introspect request...")
}
PrintDeviceHeader("Getting Spotify introspect data", clientConfig.Host, clientConfig.Port)
if sourceAccount != "" {
fmt.Printf("Spotify Account: %s\n", sourceAccount)
}
fmt.Println()
response, err := client.IntrospectSpotify(sourceAccount)
if err != nil {
return fmt.Errorf("failed to get Spotify introspect data: %w", err)
}
// Print Spotify-specific information
fmt.Printf("=== Spotify Service Introspect Data ===\n")
printIntrospectBasicInfo(response)
// Print service state with Spotify context
fmt.Printf("\n=== Spotify Service State ===\n")
printIntrospectServiceState(response)
// Print Spotify capabilities
fmt.Printf("\n=== Spotify Service Capabilities ===\n")
printIntrospectCapabilities(response)
// Show Spotify-specific recommendations
if response.IsInactive() {
fmt.Printf("\n💡 Spotify Setup Recommendations:\n")
if !response.HasUser() {
fmt.Printf(" • Sign in to your Spotify account on the device\n")
}
fmt.Printf(" • Use 'soundtouch-cli source select --source SPOTIFY' to activate Spotify\n")
fmt.Printf(" • Ensure you have Spotify Premium for full functionality\n")
}
// Print history information
if response.GetMaxHistorySize() > 0 {
fmt.Printf("\n=== Spotify Content History ===\n")
printIntrospectHistory(response)
}
// Print technical details
if response.TokenLastChangedTimeSeconds > 0 || response.PlayStatusState != "" {
fmt.Printf("\n=== Technical Details ===\n")
printIntrospectTechnicalDetails(response)
}
return nil
}
// introspectAllServices handles getting introspect data for all available services
func introspectAllServices(c *cli.Context) error {
clientConfig := GetClientConfig(c)
client, err := CreateSoundTouchClient(clientConfig)
if err != nil {
return err
}
PrintDeviceHeader("Getting introspect data for all services", clientConfig.Host, clientConfig.Port)
// Get service availability to know which services to check
serviceAvailability, err := client.GetServiceAvailability()
if err != nil {
return fmt.Errorf("failed to get service availability: %w", err)
}
// Services to introspect (only streaming services that support introspect)
servicesToCheck := []string{"SPOTIFY", "PANDORA", "TUNEIN", "AMAZON", "DEEZER"}
successCount := 0
failCount := 0
for i, source := range servicesToCheck {
if i > 0 {
fmt.Println("\n" + strings.Repeat("─", 50))
}
// Check if service is available
serviceType := sourceToServiceType(source)
if serviceType != "" && !serviceAvailability.IsServiceAvailable(serviceType) {
fmt.Printf("\n❌ %s: Service not available on this device\n", source)
continue
}
fmt.Printf("\n🔍 Getting introspect data for %s...\n", source)
response, err := client.Introspect(source, "")
if err != nil {
fmt.Printf("❌ %s: Failed to get introspect data - %v\n", source, err)
failCount++
continue
}
fmt.Printf("✅ %s: Successfully retrieved introspect data\n", source)
printIntrospectSummary(source, response)
successCount++
}
// Print summary
fmt.Print("\n" + strings.Repeat("═", 50) + "\n")
fmt.Printf("📊 Introspect Summary:\n")
fmt.Printf(" ✅ Successful: %d services\n", successCount)
fmt.Printf(" ❌ Failed: %d services\n", failCount)
fmt.Printf(" 📡 Total checked: %d services\n", len(servicesToCheck))
if successCount > 0 {
PrintSuccess(fmt.Sprintf("Successfully retrieved introspect data for %d services", successCount))
}
return nil
}
// printIntrospectBasicInfo prints basic introspect information
func printIntrospectBasicInfo(response *models.IntrospectResponse) {
fmt.Printf("State: %s\n", response.State)
if response.HasUser() {
fmt.Printf("User: %s\n", response.User)
}
fmt.Printf("Currently Playing: %s\n", formatBooleanStatus(response.IsPlaying))
if response.HasCurrentContent() {
fmt.Printf("Current Content: %s\n", response.CurrentURI)
}
fmt.Printf("Shuffle Mode: %s\n", response.ShuffleMode)
if response.HasSubscription() {
fmt.Printf("Subscription Type: %s\n", response.SubscriptionType)
}
}
// printIntrospectServiceState prints service state information
func printIntrospectServiceState(response *models.IntrospectResponse) {
if response.IsActive() {
fmt.Printf("✅ Service is ACTIVE\n")
} else if response.IsInactive() {
fmt.Printf("❌ Service is INACTIVE")
if response.GetState() == models.IntrospectStateInactiveUnselected {
fmt.Printf(" (Never been used)")
}
fmt.Println()
}
// Additional state information
if response.IsPlaying {
fmt.Printf("🎵 Currently playing content\n")
} else {
fmt.Printf("⏸️ Not currently playing\n")
}
if response.IsShuffleEnabled() {
fmt.Printf("🔀 Shuffle mode is ON\n")
} else {
fmt.Printf("➡️ Shuffle mode is OFF\n")
}
}
// printIntrospectCapabilities prints service capabilities
func printIntrospectCapabilities(response *models.IntrospectResponse) {
capabilities := []struct {
supported bool
feature string
icon string
}{
{response.SupportsSkipPrevious(), "Skip Previous", "⏮️"},
{response.SupportsSeek(), "Seek within tracks", "🎯"},
{response.SupportsResume(), "Resume playback", "▶️"},
}
for _, cap := range capabilities {
status := "❌"
if cap.supported {
status = "✅"
}
fmt.Printf("%s %s %s\n", status, cap.icon, cap.feature)
}
// Data collection status
if response.CollectsData() {
fmt.Printf("📊 Data collection: ENABLED\n")
} else {
fmt.Printf("🚫 Data collection: DISABLED\n")
}
}
// printIntrospectHistory prints content history information
func printIntrospectHistory(response *models.IntrospectResponse) {
fmt.Printf("Max History Size: %d items\n", response.GetMaxHistorySize())
}
// printIntrospectTechnicalDetails prints technical details
func printIntrospectTechnicalDetails(response *models.IntrospectResponse) {
if response.TokenLastChangedTimeSeconds > 0 {
// Convert timestamp to readable format
tokenTime := time.Unix(response.TokenLastChangedTimeSeconds, 0)
fmt.Printf("Token Last Changed: %s\n", tokenTime.Format("2006-01-02 15:04:05 MST"))
fmt.Printf("Token Timestamp: %d seconds since Unix epoch\n", response.TokenLastChangedTimeSeconds)
if response.TokenLastChangedTimeMicroseconds > 0 {
fmt.Printf("Token Microseconds: %d\n", response.TokenLastChangedTimeMicroseconds)
}
}
if response.PlayStatusState != "" {
fmt.Printf("Play Status State: %s\n", response.PlayStatusState)
}
fmt.Printf("Received Playback Request: %s\n", formatBooleanStatus(response.ReceivedPlaybackRequest))
}
// printIntrospectSummary prints a brief summary for the "all" command
func printIntrospectSummary(_ string, response *models.IntrospectResponse) {
fmt.Printf(" State: %s", response.State)
if response.HasUser() {
fmt.Printf(" (User: %s)", response.User)
}
fmt.Println()
fmt.Printf(" Playing: %s", formatBooleanStatus(response.IsPlaying))
if response.HasCurrentContent() {
fmt.Printf(" | Content: %.50s", response.CurrentURI)
if len(response.CurrentURI) > 50 {
fmt.Printf("...")
}
}
fmt.Println()
var capabilities []string
if response.SupportsSkipPrevious() {
capabilities = append(capabilities, "Skip")
}
if response.SupportsSeek() {
capabilities = append(capabilities, "Seek")
}
if response.SupportsResume() {
capabilities = append(capabilities, "Resume")
}
if len(capabilities) > 0 {
fmt.Printf(" Capabilities: %s\n", strings.Join(capabilities, ", "))
} else {
fmt.Printf(" Capabilities: None\n")
}
}
// formatBooleanStatus formats boolean values for display
func formatBooleanStatus(value bool) string {
if value {
return "✅ Yes"
}
return "❌ No"
}
+482
View File
@@ -0,0 +1,482 @@
package main
import (
"bytes"
"os"
"testing"
"github.com/gesellix/bose-soundtouch/pkg/models"
)
func TestIntrospectCommands(t *testing.T) {
tests := []struct {
name string
args []string
expectedOutput []string
expectError bool
}{
{
name: "introspect service with source flag",
args: []string{"soundtouch-cli", "--host", "192.168.1.100", "source", "introspect", "--source", "SPOTIFY"},
expectedOutput: []string{
"Getting introspect data for SPOTIFY",
"=== SPOTIFY Service Introspect Data ===",
"State: Active",
"User: test_user",
"Currently Playing: ✅ Yes",
"Current Content: spotify://track/123",
"Shuffle Mode: ON",
"Subscription Type: Premium",
"=== Service State ===",
"✅ Service is ACTIVE",
"🎵 Currently playing content",
"🔀 Shuffle mode is ON",
"=== Service Capabilities ===",
"✅ ⏮️ Skip Previous",
"✅ 🎯 Seek within tracks",
"✅ ▶️ Resume playback",
"🚫 Data collection: DISABLED",
"=== Spotify Content History ===",
"Max History Size: 15 items",
"=== Technical Details ===",
"Token Last Changed:",
"Token Timestamp: 1702566495",
"Play Status State: 2",
"Received Playback Request: ❌ No",
},
},
{
name: "introspect spotify convenience command",
args: []string{"soundtouch-cli", "--host", "192.168.1.100", "source", "introspect-spotify"},
expectedOutput: []string{
"Getting Spotify introspect data",
"=== Spotify Service Introspect Data ===",
"State: Active",
"User: test_user",
"=== Spotify Service State ===",
"✅ Service is ACTIVE",
"=== Spotify Service Capabilities ===",
},
},
{
name: "introspect with account parameter",
args: []string{"soundtouch-cli", "--host", "192.168.1.100", "source", "introspect", "--source", "SPOTIFY", "--account", "my_spotify_account"},
expectedOutput: []string{
"Getting introspect data for SPOTIFY",
"Source Account: my_spotify_account",
},
},
{
name: "introspect missing source flag",
args: []string{"soundtouch-cli", "--host", "192.168.1.100", "source", "introspect"},
expectError: true,
},
{
name: "introspect missing host",
args: []string{"soundtouch-cli", "source", "introspect", "--source", "SPOTIFY"},
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Skip actual execution for now - these would need mock HTTP servers
// This test structure shows how the CLI commands would be tested
t.Skip("Integration test - requires mock HTTP server setup")
// Example of how you would set up the test:
// app := createTestApp()
//
// var buf bytes.Buffer
// app.Writer = &buf
// app.ErrWriter = &buf
//
// err := app.Run(tt.args)
//
// if tt.expectError {
// if err == nil {
// t.Error("expected error, got nil")
// }
// return
// }
//
// if err != nil {
// t.Fatalf("unexpected error: %v", err)
// }
//
// output := buf.String()
// for _, expected := range tt.expectedOutput {
// if !strings.Contains(output, expected) {
// t.Errorf("expected output to contain %q, got:\n%s", expected, output)
// }
// }
})
}
}
func TestPrintIntrospectBasicInfo(t *testing.T) {
tests := []struct {
name string
response *models.IntrospectResponse
expected []string
}{
{
name: "active spotify response",
response: &models.IntrospectResponse{
State: "Active",
User: "test_user",
IsPlaying: true,
ShuffleMode: "ON",
CurrentURI: "spotify://track/123",
SubscriptionType: "Premium",
},
expected: []string{
"State: Active",
"User: test_user",
"Currently Playing: ✅ Yes",
"Current Content: spotify://track/123",
"Shuffle Mode: ON",
"Subscription Type: Premium",
},
},
{
name: "inactive response",
response: &models.IntrospectResponse{
State: "InactiveUnselected",
User: "",
IsPlaying: false,
ShuffleMode: "OFF",
CurrentURI: "",
},
expected: []string{
"State: InactiveUnselected",
"Currently Playing: ❌ No",
"Shuffle Mode: OFF",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Capture stdout
oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
// Call the function
printIntrospectBasicInfo(tt.response)
// Restore stdout and read output
w.Close()
os.Stdout = oldStdout
var buf bytes.Buffer
_, err := buf.ReadFrom(r)
if err != nil {
t.Fatalf("failed to read output: %v", err)
}
output := buf.String()
// Check expected strings are present
for _, expected := range tt.expected {
if !containsSubstring(output, expected) {
t.Errorf("expected output to contain %q, got:\n%s", expected, output)
}
}
// Check unwanted strings are not present
if tt.response.User == "" && containsSubstring(output, "User:") {
t.Error("expected no user information when user is empty")
}
if tt.response.CurrentURI == "" && containsSubstring(output, "Current Content:") {
t.Error("expected no current content when URI is empty")
}
if tt.response.SubscriptionType == "" && containsSubstring(output, "Subscription Type:") {
t.Error("expected no subscription information when type is empty")
}
})
}
}
func TestPrintIntrospectServiceState(t *testing.T) {
tests := []struct {
name string
response *models.IntrospectResponse
expected []string
}{
{
name: "active playing with shuffle",
response: &models.IntrospectResponse{
State: "Active",
IsPlaying: true,
ShuffleMode: "ON",
},
expected: []string{
"✅ Service is ACTIVE",
"🎵 Currently playing content",
"🔀 Shuffle mode is ON",
},
},
{
name: "inactive unselected",
response: &models.IntrospectResponse{
State: "InactiveUnselected",
IsPlaying: false,
ShuffleMode: "OFF",
},
expected: []string{
"❌ Service is INACTIVE (Never been used)",
"⏸️ Not currently playing",
"➡️ Shuffle mode is OFF",
},
},
{
name: "inactive but configured",
response: &models.IntrospectResponse{
State: "Inactive",
IsPlaying: false,
ShuffleMode: "OFF",
},
expected: []string{
"❌ Service is INACTIVE",
"⏸️ Not currently playing",
"➡️ Shuffle mode is OFF",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Capture stdout
oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
// Call the function
printIntrospectServiceState(tt.response)
// Restore stdout and read output
w.Close()
os.Stdout = oldStdout
var buf bytes.Buffer
_, err := buf.ReadFrom(r)
if err != nil {
t.Fatalf("failed to read output: %v", err)
}
output := buf.String()
// Check expected strings are present
for _, expected := range tt.expected {
if !containsSubstring(output, expected) {
t.Errorf("expected output to contain %q, got:\n%s", expected, output)
}
}
})
}
}
func TestPrintIntrospectCapabilities(t *testing.T) {
tests := []struct {
name string
response *models.IntrospectResponse
expected []string
}{
{
name: "full capabilities enabled",
response: &models.IntrospectResponse{
NowPlaying: &models.IntrospectNowPlaying{
SkipPreviousSupported: true,
SeekSupported: true,
ResumeSupported: true,
CollectData: true,
},
},
expected: []string{
"✅ ⏮️ Skip Previous",
"✅ 🎯 Seek within tracks",
"✅ ▶️ Resume playback",
"📊 Data collection: ENABLED",
},
},
{
name: "limited capabilities",
response: &models.IntrospectResponse{
NowPlaying: &models.IntrospectNowPlaying{
SkipPreviousSupported: false,
SeekSupported: false,
ResumeSupported: true,
CollectData: false,
},
},
expected: []string{
"❌ ⏮️ Skip Previous",
"❌ 🎯 Seek within tracks",
"✅ ▶️ Resume playback",
"🚫 Data collection: DISABLED",
},
},
{
name: "no capabilities info",
response: &models.IntrospectResponse{
NowPlaying: nil,
},
expected: []string{
"❌ ⏮️ Skip Previous",
"❌ 🎯 Seek within tracks",
"❌ ▶️ Resume playback",
"🚫 Data collection: DISABLED",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Capture stdout
oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
// Call the function
printIntrospectCapabilities(tt.response)
// Restore stdout and read output
w.Close()
os.Stdout = oldStdout
var buf bytes.Buffer
_, err := buf.ReadFrom(r)
if err != nil {
t.Fatalf("failed to read output: %v", err)
}
output := buf.String()
// Check expected strings are present
for _, expected := range tt.expected {
if !containsSubstring(output, expected) {
t.Errorf("expected output to contain %q, got:\n%s", expected, output)
}
}
})
}
}
func TestPrintIntrospectSummary(t *testing.T) {
tests := []struct {
name string
source string
response *models.IntrospectResponse
expected []string
}{
{
name: "full spotify summary",
source: "SPOTIFY",
response: &models.IntrospectResponse{
State: "Active",
User: "spotify_user",
IsPlaying: true,
CurrentURI: "spotify://track/very_long_track_uri_that_should_be_truncated_because_its_too_long_for_display",
NowPlaying: &models.IntrospectNowPlaying{
SkipPreviousSupported: true,
SeekSupported: true,
ResumeSupported: true,
},
},
expected: []string{
"State: Active (User: spotify_user)",
"Playing: ✅ Yes | Content: spotify://track/very_long_track_uri_that_should_be...",
"Capabilities: Skip, Seek, Resume",
},
},
{
name: "minimal summary",
source: "PANDORA",
response: &models.IntrospectResponse{
State: "Inactive",
IsPlaying: false,
},
expected: []string{
"State: Inactive",
"Playing: ❌ No",
"Capabilities: None",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Capture stdout
oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
// Call the function
printIntrospectSummary(tt.source, tt.response)
// Restore stdout and read output
w.Close()
os.Stdout = oldStdout
var buf bytes.Buffer
_, err := buf.ReadFrom(r)
if err != nil {
t.Fatalf("failed to read output: %v", err)
}
output := buf.String()
// Check expected strings are present
for _, expected := range tt.expected {
if !containsSubstring(output, expected) {
t.Errorf("expected output to contain %q, got:\n%s", expected, output)
}
}
})
}
}
func TestFormatBooleanStatus(t *testing.T) {
tests := []struct {
name string
value bool
expected string
}{
{
name: "true value",
value: true,
expected: "✅ Yes",
},
{
name: "false value",
value: false,
expected: "❌ No",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := formatBooleanStatus(tt.value)
if result != tt.expected {
t.Errorf("expected %q, got %q", tt.expected, result)
}
})
}
}
// Helper function to check if output contains a substring
func containsSubstring(output, substring string) bool {
return bytes.Contains([]byte(output), []byte(substring))
}
+524
View File
@@ -0,0 +1,524 @@
package main
import (
"fmt"
"strings"
"time"
"github.com/gesellix/bose-soundtouch/pkg/models"
"github.com/urfave/cli/v2"
)
// getRecents handles getting recently played content
func getRecents(c *cli.Context) error {
clientConfig := GetClientConfig(c)
client, err := CreateSoundTouchClient(clientConfig)
if err != nil {
return err
}
PrintDeviceHeader("Getting recently played content", clientConfig.Host, clientConfig.Port)
response, err := client.GetRecents()
if err != nil {
return fmt.Errorf("failed to get recent items: %w", err)
}
if response.IsEmpty() {
fmt.Printf("📭 No recent items found\n")
fmt.Printf("💡 Play some content to populate the recent items list\n")
return nil
}
// Display summary
fmt.Printf("📊 Recent Items Summary:\n")
fmt.Printf(" Total Items: %d\n", response.GetItemCount())
// Show source breakdown
sources := map[string]int{
"Spotify": len(response.GetSpotifyItems()),
"Local Music": len(response.GetLocalMusicItems()),
"Stored Music": len(response.GetStoredMusicItems()),
"TuneIn": len(response.GetTuneInItems()),
"Pandora": len(response.GetPandoraItems()),
}
fmt.Printf(" By Source:\n")
for source, count := range sources {
if count > 0 {
fmt.Printf(" • %s: %d items\n", source, count)
}
}
// Show type breakdown
tracks := len(response.GetTracks())
stations := len(response.GetStations())
playlists := len(response.GetPlaylistsAndAlbums())
presetable := len(response.GetPresetableItems())
fmt.Printf(" By Type:\n")
if tracks > 0 {
fmt.Printf(" • 🎵 Tracks: %d\n", tracks)
}
if stations > 0 {
fmt.Printf(" • 📻 Stations: %d\n", stations)
}
if playlists > 0 {
fmt.Printf(" • 📋 Playlists/Albums: %d\n", playlists)
}
if presetable > 0 {
fmt.Printf(" • ⭐ Presetable: %d\n", presetable)
}
fmt.Printf("\n=== Recent Items ===\n")
// Display items with details
maxItems := c.Int("limit")
if maxItems <= 0 || maxItems > len(response.Items) {
maxItems = len(response.Items)
}
for i, item := range response.Items[:maxItems] {
printRecentItem(i+1, &item, c.Bool("detailed"))
}
if len(response.Items) > maxItems {
fmt.Printf("\n... and %d more items (use --limit to show more)\n", len(response.Items)-maxItems)
}
return nil
}
// getRecentsFiltered handles getting filtered recent content
// buildFilterDescription creates a description string for the applied filters
func buildFilterDescription(source, contentType string) string {
switch {
case source != "" && contentType != "":
return fmt.Sprintf(" (filtered by source: %s, type: %s)", source, contentType)
case source != "":
return fmt.Sprintf(" (filtered by source: %s)", source)
case contentType != "":
return fmt.Sprintf(" (filtered by type: %s)", contentType)
default:
return ""
}
}
// applyContentTypeFilter filters items by content type
func applyContentTypeFilter(items []models.RecentsResponseItem, contentType string) []models.RecentsResponseItem {
if contentType == "" {
return items
}
var typeFiltered []models.RecentsResponseItem
for _, item := range items {
if shouldIncludeItemByType(item, contentType) {
typeFiltered = append(typeFiltered, item)
}
}
return typeFiltered
}
// shouldIncludeItemByType checks if an item matches the specified content type
func shouldIncludeItemByType(item models.RecentsResponseItem, contentType string) bool {
switch contentType {
case "track", "tracks":
return item.IsTrack()
case "station", "stations":
return item.IsStation()
case "playlist", "playlists":
return item.IsPlaylist()
case "album", "albums":
return item.IsAlbum()
case "container", "containers":
return item.IsContainer()
case "presetable":
return item.IsPresetable()
default:
return false
}
}
// displayFilteredResults prints the filtered recent items
func displayFilteredResults(filteredItems []models.RecentsResponseItem, c *cli.Context) {
maxItems := c.Int("limit")
if maxItems <= 0 || maxItems > len(filteredItems) {
maxItems = len(filteredItems)
}
for i, item := range filteredItems[:maxItems] {
printRecentItem(i+1, &item, c.Bool("detailed"))
}
if len(filteredItems) > maxItems {
fmt.Printf("\n... and %d more items (use --limit to show more)\n", len(filteredItems)-maxItems)
}
}
func getRecentsFiltered(c *cli.Context) error {
clientConfig := GetClientConfig(c)
client, err := CreateSoundTouchClient(clientConfig)
if err != nil {
return err
}
source := strings.ToUpper(c.String("source"))
contentType := strings.ToLower(c.String("type"))
filterDesc := buildFilterDescription(source, contentType)
PrintDeviceHeader("Getting filtered recent content"+filterDesc, clientConfig.Host, clientConfig.Port)
response, err := client.GetRecents()
if err != nil {
return fmt.Errorf("failed to get recent items: %w", err)
}
if response.IsEmpty() {
fmt.Printf("📭 No recent items found\n")
return nil
}
// Apply source filter
var filteredItems []models.RecentsResponseItem
if source != "" {
filteredItems = response.GetItemsBySource(source)
} else {
filteredItems = response.Items
}
// Apply type filter
filteredItems = applyContentTypeFilter(filteredItems, contentType)
if len(filteredItems) == 0 {
fmt.Printf("📭 No items match the specified filters\n")
fmt.Printf("💡 Try different filter criteria or check available content\n")
return nil
}
fmt.Printf("📊 Filtered Results: %d items\n\n", len(filteredItems))
displayFilteredResults(filteredItems, c)
return nil
}
// getRecentsMostRecent shows only the most recent item
func getRecentsMostRecent(c *cli.Context) error {
clientConfig := GetClientConfig(c)
client, err := CreateSoundTouchClient(clientConfig)
if err != nil {
return err
}
PrintDeviceHeader("Getting most recent item", clientConfig.Host, clientConfig.Port)
response, err := client.GetRecents()
if err != nil {
return fmt.Errorf("failed to get recent items: %w", err)
}
mostRecent := response.GetMostRecent()
if mostRecent == nil {
fmt.Printf("📭 No recent items found\n")
return nil
}
fmt.Printf("🕒 Most Recent Item:\n\n")
printRecentItem(1, mostRecent, true)
return nil
}
// printRecentItem prints details about a recent item
func printRecentItem(index int, item *models.RecentsResponseItem, detailed bool) {
// Basic information
displayName := item.GetDisplayName()
source := item.GetSource()
contentType := item.GetContentType()
// Format source display
sourceDisplay := formatSourceForDisplay(source)
// Content type icon
typeIcon := getContentTypeIcon(item)
fmt.Printf("%d. %s %s\n", index, typeIcon, displayName)
fmt.Printf(" Source: %s", sourceDisplay)
if contentType != "" {
fmt.Printf(" | Type: %s", contentType)
}
fmt.Printf("\n")
// Time information
if item.GetUTCTime() > 0 {
playTime := time.Unix(item.GetUTCTime(), 0)
fmt.Printf(" Played: %s\n", playTime.Format("2006-01-02 15:04:05"))
}
// Additional details if requested
if detailed {
if item.HasID() {
fmt.Printf(" ID: %s\n", item.GetID())
}
if item.IsPresetable() {
fmt.Printf(" ⭐ Can be saved as preset\n")
}
if item.HasArtwork() {
fmt.Printf(" 🎨 Has artwork: %s\n", truncateString(item.GetArtwork(), 50))
}
location := item.GetLocation()
if location != "" {
fmt.Printf(" 📍 Location: %s\n", truncateString(location, 50))
}
sourceAccount := item.GetSourceAccount()
if sourceAccount != "" && sourceAccount != source {
fmt.Printf(" 👤 Account: %s\n", truncateString(sourceAccount, 30))
}
// Content classification
var classifications []string
if item.IsStreamingContent() {
classifications = append(classifications, "Streaming")
}
if item.IsLocalContent() {
classifications = append(classifications, "Local")
}
if len(classifications) > 0 {
fmt.Printf(" 🏷️ Classification: %s\n", strings.Join(classifications, ", "))
}
}
fmt.Println()
}
// getContentTypeIcon returns an emoji icon for the content type
func getContentTypeIcon(item *models.RecentsResponseItem) string {
switch {
case item.IsTrack():
return "🎵"
case item.IsStation():
return "📻"
case item.IsPlaylist():
return "📋"
case item.IsAlbum():
return "💿"
case item.IsContainer():
return "📁"
default:
return "🎶"
}
}
// formatSourceForDisplay formats source names for user-friendly display
func formatSourceForDisplay(source string) string {
switch source {
case "SPOTIFY":
return "Spotify"
case "LOCAL_MUSIC":
return "Local Music"
case "STORED_MUSIC":
return "Stored Music"
case "TUNEIN":
return "TuneIn Radio"
case "PANDORA":
return "Pandora"
case "AMAZON":
return "Amazon Music"
case "DEEZER":
return "Deezer"
case "IHEART":
return "iHeartRadio"
case "BLUETOOTH":
return "Bluetooth"
case "AUX":
return "AUX Input"
case "AIRPLAY":
return "AirPlay"
default:
return source
}
}
// truncateString truncates a string to the specified length with ellipsis
func truncateString(s string, maxLength int) string {
if len(s) <= maxLength {
return s
}
if maxLength <= 3 {
return "..."
}
return s[:maxLength-3] + "..."
}
// printBasicStats prints overall statistics about recent items
func printBasicStats(response *models.RecentsResponse) {
fmt.Printf("Overall Statistics:\n")
fmt.Printf(" Total Items: %d\n", response.GetItemCount())
if !response.IsEmpty() {
mostRecent := response.GetMostRecent()
if mostRecent != nil {
lastPlayTime := time.Unix(mostRecent.GetUTCTime(), 0)
fmt.Printf(" Last Played: %s\n", lastPlayTime.Format("2006-01-02 15:04:05"))
}
}
}
// printSourceStats prints statistics broken down by source
func printSourceStats(response *models.RecentsResponse) {
fmt.Printf("\nBy Source:\n")
sourceStats := map[string]int{
"Spotify": len(response.GetSpotifyItems()),
"Pandora": len(response.GetPandoraItems()),
"TuneIn": len(response.GetTuneInItems()),
"Local Music": len(response.GetLocalMusicItems()),
"Stored Music": len(response.GetStoredMusicItems()),
}
// Add other sources if they exist
otherSources := make(map[string]int)
for _, item := range response.Items {
source := item.GetSource()
found := false
for knownSource := range sourceStats {
if strings.Contains(strings.ToLower(knownSource), strings.ToLower(source)) ||
strings.Contains(strings.ToLower(source), strings.ToLower(knownSource)) {
found = true
break
}
}
if !found && source != "" {
otherSources[formatSourceForDisplay(source)]++
}
}
// Merge other sources
for source, count := range otherSources {
sourceStats[source] = count
}
for source, count := range sourceStats {
if count > 0 {
percentage := float64(count) / float64(response.GetItemCount()) * 100
fmt.Printf(" %-15s %3d items (%5.1f%%)\n", source+":", count, percentage)
}
}
}
// printContentTypeStats prints statistics broken down by content type
func printContentTypeStats(response *models.RecentsResponse) {
fmt.Printf("\nBy Content Type:\n")
tracks := len(response.GetTracks())
stations := len(response.GetStations())
playlists := len(response.GetPlaylistsAndAlbums())
if tracks > 0 {
percentage := float64(tracks) / float64(response.GetItemCount()) * 100
fmt.Printf(" %-15s %3d items (%5.1f%%)\n", "Tracks:", tracks, percentage)
}
if stations > 0 {
percentage := float64(stations) / float64(response.GetItemCount()) * 100
fmt.Printf(" %-15s %3d items (%5.1f%%)\n", "Stations:", stations, percentage)
}
if playlists > 0 {
percentage := float64(playlists) / float64(response.GetItemCount()) * 100
fmt.Printf(" %-15s %3d items (%5.1f%%)\n", "Playlists/Albums:", playlists, percentage)
}
}
// printSpecialCategoryStats prints statistics for special content categories
func printSpecialCategoryStats(response *models.RecentsResponse) {
presetable := len(response.GetPresetableItems())
if presetable > 0 {
fmt.Printf("\nSpecial Categories:\n")
percentage := float64(presetable) / float64(response.GetItemCount()) * 100
fmt.Printf(" %-15s %3d items (%5.1f%%)\n", "Presetable:", presetable, percentage)
}
}
// printSourceAnalysisStats prints streaming vs local content analysis
func printSourceAnalysisStats(response *models.RecentsResponse) {
streamingCount := 0
localCount := 0
for _, item := range response.Items {
if item.IsStreamingContent() {
streamingCount++
} else if item.IsLocalContent() {
localCount++
}
}
fmt.Printf("\nSource Analysis:\n")
if streamingCount > 0 {
percentage := float64(streamingCount) / float64(response.GetItemCount()) * 100
fmt.Printf(" %-15s %3d items (%5.1f%%)\n", "Streaming:", streamingCount, percentage)
}
if localCount > 0 {
percentage := float64(localCount) / float64(response.GetItemCount()) * 100
fmt.Printf(" %-15s %3d items (%5.1f%%)\n", "Local:", localCount, percentage)
}
}
// recentsStats shows statistics about recent items
func recentsStats(c *cli.Context) error {
clientConfig := GetClientConfig(c)
client, err := CreateSoundTouchClient(clientConfig)
if err != nil {
return err
}
PrintDeviceHeader("Getting recent items statistics", clientConfig.Host, clientConfig.Port)
response, err := client.GetRecents()
if err != nil {
return fmt.Errorf("failed to get recent items: %w", err)
}
if response.IsEmpty() {
fmt.Printf("📊 Statistics: No recent items found\n")
return nil
}
fmt.Printf("📊 Recent Items Statistics\n\n")
printBasicStats(response)
printSourceStats(response)
printContentTypeStats(response)
printSpecialCategoryStats(response)
printSourceAnalysisStats(response)
return nil
}
+411
View File
@@ -0,0 +1,411 @@
package main
import (
"bytes"
"os"
"testing"
"github.com/gesellix/bose-soundtouch/pkg/models"
)
func TestRecentsCommands(t *testing.T) {
tests := []struct {
name string
args []string
expectedOutput []string
expectError bool
}{
{
name: "recents list command",
args: []string{"soundtouch-cli", "--host", "192.168.1.100", "recents", "list"},
expectedOutput: []string{
"Getting recently played content",
"Recent Items Summary:",
"Recent Items",
},
},
{
name: "recents filter by source",
args: []string{"soundtouch-cli", "--host", "192.168.1.100", "recents", "filter", "--source", "SPOTIFY"},
expectedOutput: []string{
"Getting filtered recent content",
"filtered by source: SPOTIFY",
},
},
{
name: "recents latest command",
args: []string{"soundtouch-cli", "--host", "192.168.1.100", "recents", "latest"},
expectedOutput: []string{
"Getting most recent item",
"Most Recent Item:",
},
},
{
name: "recents stats command",
args: []string{"soundtouch-cli", "--host", "192.168.1.100", "recents", "stats"},
expectedOutput: []string{
"Getting recent items statistics",
"Recent Items Statistics",
},
},
{
name: "recents missing host",
args: []string{"soundtouch-cli", "recents", "list"},
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Skip actual execution for now - these would need mock HTTP servers
// This test structure shows how the CLI commands would be tested
t.Skip("Integration test - requires mock HTTP server setup")
// Example of how you would set up the test:
// app := createTestApp()
//
// var buf bytes.Buffer
// app.Writer = &buf
// app.ErrWriter = &buf
//
// err := app.Run(tt.args)
//
// if tt.expectError {
// if err == nil {
// t.Error("expected error, got nil")
// }
// return
// }
//
// if err != nil {
// t.Fatalf("unexpected error: %v", err)
// }
//
// output := buf.String()
// for _, expected := range tt.expectedOutput {
// if !strings.Contains(output, expected) {
// t.Errorf("expected output to contain %q, got:\n%s", expected, output)
// }
// }
})
}
}
func TestPrintRecentItem(t *testing.T) {
tests := []struct {
name string
item *models.RecentsResponseItem
detailed bool
expected []string
}{
{
name: "basic track item",
item: &models.RecentsResponseItem{
DeviceID: "device1",
UTCTime: 1701200000,
ContentItem: &models.ContentItem{
Source: "SPOTIFY",
Type: "track",
ItemName: "Test Song",
},
},
detailed: false,
expected: []string{
"🎵 Test Song",
"Source: Spotify",
"Type: track",
},
},
{
name: "detailed station item",
item: &models.RecentsResponseItem{
DeviceID: "device1",
UTCTime: 1701200000,
ID: "station123",
ContentItem: &models.ContentItem{
Source: "TUNEIN",
Type: "stationurl",
ItemName: "Rock FM",
Location: "tunein:station:s12345",
SourceAccount: "tunein_account",
IsPresetable: true,
},
},
detailed: true,
expected: []string{
"📻 Rock FM",
"Source: TuneIn Radio",
"ID: station123",
"Can be saved as preset",
"Location: tunein:station:s12345",
"Classification: Streaming",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Capture stdout
oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
// Call the function
printRecentItem(1, tt.item, tt.detailed)
// Restore stdout and read output
w.Close()
os.Stdout = oldStdout
var buf bytes.Buffer
_, err := buf.ReadFrom(r)
if err != nil {
t.Fatalf("failed to read output: %v", err)
}
output := buf.String()
// Check expected strings are present
for _, expected := range tt.expected {
if !bytes.Contains(buf.Bytes(), []byte(expected)) {
t.Errorf("expected output to contain %q, got:\n%s", expected, output)
}
}
})
}
}
func TestGetContentTypeIcon(t *testing.T) {
tests := []struct {
name string
item *models.RecentsResponseItem
expected string
}{
{
name: "track item",
item: &models.RecentsResponseItem{
ContentItem: &models.ContentItem{Type: "track"},
},
expected: "🎵",
},
{
name: "station item",
item: &models.RecentsResponseItem{
ContentItem: &models.ContentItem{Type: "stationurl"},
},
expected: "📻",
},
{
name: "playlist item",
item: &models.RecentsResponseItem{
ContentItem: &models.ContentItem{Type: "playlist"},
},
expected: "📋",
},
{
name: "album item",
item: &models.RecentsResponseItem{
ContentItem: &models.ContentItem{Type: "album"},
},
expected: "💿",
},
{
name: "container item",
item: &models.RecentsResponseItem{
ContentItem: &models.ContentItem{Type: "container"},
},
expected: "📁",
},
{
name: "unknown type",
item: &models.RecentsResponseItem{
ContentItem: &models.ContentItem{Type: "unknown"},
},
expected: "🎶",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := getContentTypeIcon(tt.item)
if result != tt.expected {
t.Errorf("expected %q, got %q", tt.expected, result)
}
})
}
}
func TestFormatSourceForDisplay(t *testing.T) {
tests := []struct {
name string
source string
expected string
}{
{"Spotify", "SPOTIFY", "Spotify"},
{"Local Music", "LOCAL_MUSIC", "Local Music"},
{"Stored Music", "STORED_MUSIC", "Stored Music"},
{"TuneIn", "TUNEIN", "TuneIn Radio"},
{"Pandora", "PANDORA", "Pandora"},
{"Amazon", "AMAZON", "Amazon Music"},
{"Deezer", "DEEZER", "Deezer"},
{"iHeart", "IHEART", "iHeartRadio"},
{"Bluetooth", "BLUETOOTH", "Bluetooth"},
{"AUX", "AUX", "AUX Input"},
{"AirPlay", "AIRPLAY", "AirPlay"},
{"Unknown", "UNKNOWN_SOURCE", "UNKNOWN_SOURCE"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := formatSourceForDisplay(tt.source)
if result != tt.expected {
t.Errorf("expected %q, got %q", tt.expected, result)
}
})
}
}
func TestTruncateString(t *testing.T) {
tests := []struct {
name string
input string
maxLength int
expected string
}{
{
name: "short string",
input: "hello",
maxLength: 10,
expected: "hello",
},
{
name: "exact length",
input: "hello",
maxLength: 5,
expected: "hello",
},
{
name: "long string",
input: "this is a very long string that needs truncation",
maxLength: 20,
expected: "this is a very lo...",
},
{
name: "very short max length",
input: "hello world",
maxLength: 3,
expected: "...",
},
{
name: "zero length",
input: "hello",
maxLength: 0,
expected: "...",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := truncateString(tt.input, tt.maxLength)
if result != tt.expected {
t.Errorf("expected %q, got %q", tt.expected, result)
}
})
}
}
// Test helper functions that would be used in full integration tests
func createTestRecentsResponse() *models.RecentsResponse {
return &models.RecentsResponse{
Items: []models.RecentsResponseItem{
{
DeviceID: "1004567890AA",
UTCTime: 1701300000,
ID: "spotify1",
ContentItem: &models.ContentItem{
Source: "SPOTIFY",
Type: "track",
Location: "spotify:track:4iV5W9uYEdYUVa79Axb7Rh",
SourceAccount: "spotify_user",
IsPresetable: true,
ItemName: "Shape of You - Ed Sheeran",
ContainerArt: "https://i.scdn.co/image/ab67616d0000b273ba5db46f4b838ef6027e6f96",
},
},
{
DeviceID: "1004567890AA",
UTCTime: 1701200000,
ID: "local1",
ContentItem: &models.ContentItem{
Source: "LOCAL_MUSIC",
Type: "track",
Location: "/music/local_song.mp3",
IsPresetable: false,
ItemName: "Local Song - Local Artist",
},
},
{
DeviceID: "1004567890AA",
UTCTime: 1701100000,
ID: "tunein1",
ContentItem: &models.ContentItem{
Source: "TUNEIN",
Type: "stationurl",
Location: "tunein:station:s24939",
SourceAccount: "tunein",
IsPresetable: true,
ItemName: "BBC Radio 1",
},
},
},
}
}
func TestCreateTestRecentsResponse(t *testing.T) {
response := createTestRecentsResponse()
if response == nil {
t.Fatal("expected response, got nil")
}
if response.GetItemCount() != 3 {
t.Errorf("expected 3 items, got %d", response.GetItemCount())
}
if response.IsEmpty() {
t.Error("expected response not to be empty")
}
// Test filtering
spotifyItems := response.GetSpotifyItems()
if len(spotifyItems) != 1 {
t.Errorf("expected 1 Spotify item, got %d", len(spotifyItems))
}
localItems := response.GetLocalMusicItems()
if len(localItems) != 1 {
t.Errorf("expected 1 local music item, got %d", len(localItems))
}
tuneInItems := response.GetTuneInItems()
if len(tuneInItems) != 1 {
t.Errorf("expected 1 TuneIn item, got %d", len(tuneInItems))
}
tracks := response.GetTracks()
if len(tracks) != 2 {
t.Errorf("expected 2 tracks, got %d", len(tracks))
}
stations := response.GetStations()
if len(stations) != 1 {
t.Errorf("expected 1 station, got %d", len(stations))
}
presetableItems := response.GetPresetableItems()
if len(presetableItems) != 2 {
t.Errorf("expected 2 presetable items, got %d", len(presetableItems))
}
}
+218
View File
@@ -209,6 +209,224 @@ func selectAux(c *cli.Context) error {
return nil
}
// selectLocalInternetRadio handles selecting LOCAL_INTERNET_RADIO source
func selectLocalInternetRadio(c *cli.Context) error {
clientConfig := GetClientConfig(c)
client, err := CreateSoundTouchClient(clientConfig)
if err != nil {
return err
}
location := c.String("location")
if location == "" {
return fmt.Errorf("location is required (use --location)")
}
sourceAccount := c.String("account")
itemName := c.String("name")
containerArt := c.String("artwork")
// Check LOCAL_INTERNET_RADIO availability
checker := NewServiceAvailabilityChecker(client)
if !checker.CheckSourceAvailable("LOCAL_INTERNET_RADIO", "select internet radio") {
return fmt.Errorf("LOCAL_INTERNET_RADIO is not available")
}
PrintDeviceHeader("Selecting internet radio stream", clientConfig.Host, clientConfig.Port)
if itemName != "" {
fmt.Printf(" Station: %s\n", itemName)
}
fmt.Printf(" Location: %s\n", location)
err = client.SelectLocalInternetRadio(location, sourceAccount, itemName, containerArt)
if err != nil {
return fmt.Errorf("failed to select internet radio: %w", err)
}
PrintSuccess("Internet radio stream selected")
return nil
}
// selectLocalMusic handles selecting LOCAL_MUSIC source
func selectLocalMusic(c *cli.Context) error {
clientConfig := GetClientConfig(c)
client, err := CreateSoundTouchClient(clientConfig)
if err != nil {
return err
}
location := c.String("location")
if location == "" {
return fmt.Errorf("location is required (use --location)")
}
sourceAccount := c.String("account")
if sourceAccount == "" {
return fmt.Errorf("account is required for LOCAL_MUSIC (use --account)")
}
itemName := c.String("name")
containerArt := c.String("artwork")
// Check LOCAL_MUSIC availability
checker := NewServiceAvailabilityChecker(client)
if !checker.CheckSourceAvailable("LOCAL_MUSIC", "select local music") {
return fmt.Errorf("LOCAL_MUSIC is not available")
}
PrintDeviceHeader("Selecting local music content", clientConfig.Host, clientConfig.Port)
if itemName != "" {
fmt.Printf(" Content: %s\n", itemName)
}
fmt.Printf(" Location: %s\n", location)
fmt.Printf(" Account: %s\n", sourceAccount)
err = client.SelectLocalMusic(location, sourceAccount, itemName, containerArt)
if err != nil {
return fmt.Errorf("failed to select local music: %w", err)
}
PrintSuccess("Local music content selected")
return nil
}
// selectStoredMusic handles selecting STORED_MUSIC source
func selectStoredMusic(c *cli.Context) error {
clientConfig := GetClientConfig(c)
client, err := CreateSoundTouchClient(clientConfig)
if err != nil {
return err
}
location := c.String("location")
if location == "" {
return fmt.Errorf("location is required (use --location)")
}
sourceAccount := c.String("account")
if sourceAccount == "" {
return fmt.Errorf("account is required for STORED_MUSIC (use --account)")
}
itemName := c.String("name")
containerArt := c.String("artwork")
// Check STORED_MUSIC availability
checker := NewServiceAvailabilityChecker(client)
if !checker.CheckSourceAvailable("STORED_MUSIC", "select stored music") {
return fmt.Errorf("STORED_MUSIC is not available")
}
PrintDeviceHeader("Selecting stored music content", clientConfig.Host, clientConfig.Port)
if itemName != "" {
fmt.Printf(" Content: %s\n", itemName)
}
fmt.Printf(" Location: %s\n", location)
fmt.Printf(" Account: %s\n", sourceAccount)
err = client.SelectStoredMusic(location, sourceAccount, itemName, containerArt)
if err != nil {
return fmt.Errorf("failed to select stored music: %w", err)
}
PrintSuccess("Stored music content selected")
return nil
}
// selectContent handles selecting content using a ContentItem directly
func selectContent(c *cli.Context) error {
clientConfig := GetClientConfig(c)
client, err := CreateSoundTouchClient(clientConfig)
if err != nil {
return err
}
// Required parameters
source := strings.ToUpper(c.String("source"))
if source == "" {
return fmt.Errorf("source is required (use --source)")
}
location := c.String("location")
if location == "" {
return fmt.Errorf("location is required (use --location)")
}
// Optional parameters
sourceAccount := c.String("account")
itemName := c.String("name")
containerArt := c.String("artwork")
itemType := c.String("type")
isPresetable := c.Bool("presetable")
// Create ContentItem
contentItem := &models.ContentItem{
Source: source,
Type: itemType,
Location: location,
SourceAccount: sourceAccount,
IsPresetable: isPresetable,
ItemName: itemName,
ContainerArt: containerArt,
}
// Set default type if not specified
if itemType == "" {
switch source {
case "SPOTIFY":
contentItem.Type = "uri"
case "TUNEIN", "LOCAL_INTERNET_RADIO":
contentItem.Type = "stationurl"
case "LOCAL_MUSIC":
contentItem.Type = "album" // default, could be track, artist, etc.
}
}
// Set default item name if not specified
if itemName == "" {
contentItem.ItemName = source
}
PrintDeviceHeader("Selecting content", clientConfig.Host, clientConfig.Port)
fmt.Printf(" Source: %s\n", source)
fmt.Printf(" Location: %s\n", location)
if sourceAccount != "" {
fmt.Printf(" Account: %s\n", sourceAccount)
}
if itemName != "" {
fmt.Printf(" Name: %s\n", itemName)
}
if itemType != "" {
fmt.Printf(" Type: %s\n", itemType)
}
err = client.SelectContentItem(contentItem)
if err != nil {
return fmt.Errorf("failed to select content: %w", err)
}
PrintSuccess("Content selected")
return nil
}
// getServiceAvailability handles displaying service availability information
func getServiceAvailability(c *cli.Context) error {
clientConfig := GetClientConfig(c)
+38 -26
View File
@@ -2,7 +2,6 @@ package main
import (
"fmt"
"net/url"
"strings"
"github.com/gesellix/bose-soundtouch/pkg/models"
@@ -35,23 +34,12 @@ func playTTS(c *cli.Context) error {
return err
}
// URL encode the text for Google TTS
encodedText := url.QueryEscape(text)
// Build TTS URL with language support
ttsURL := fmt.Sprintf("http://translate.google.com/translate_tts?ie=UTF-8&tl=%s&client=tw-ob&q=%s", language, encodedText)
// Create PlayInfo for TTS
playInfo := &models.PlayInfo{
URL: ttsURL,
AppKey: appKey,
Service: "TTS Notification",
Message: "Google TTS",
Reason: text,
}
var playInfo *models.PlayInfo
if volume > 0 {
playInfo.SetVolume(volume)
playInfo = models.NewTTSPlayInfo(text, appKey, language, volume)
} else {
playInfo = models.NewTTSPlayInfo(text, appKey, language)
}
err = client.PlayCustom(playInfo)
@@ -121,10 +109,11 @@ func playURL(c *cli.Context) error {
}
// Create PlayInfo for URL content
playInfo := models.NewURLPlayInfo(urlStr, appKey, service, message, reason)
var playInfo *models.PlayInfo
if volume > 0 {
playInfo.SetVolume(volume)
playInfo = models.NewURLPlayInfo(urlStr, appKey, service, message, reason, volume)
} else {
playInfo = models.NewURLPlayInfo(urlStr, appKey, service, message, reason)
}
err = client.PlayCustom(playInfo)
@@ -147,10 +136,16 @@ func playURL(c *cli.Context) error {
return nil
}
// playNotificationBeep plays a notification beep on the speaker (uses existing endpoint)
func playNotificationBeep(c *cli.Context) error {
// playNotification plays a notification sound or a local file on the speaker
func playNotification(c *cli.Context) error {
clientConfig := GetClientConfig(c)
PrintDeviceHeader("Playing notification beep", clientConfig.Host, clientConfig.Port)
path := c.String("path")
if path != "" {
PrintDeviceHeader(fmt.Sprintf("Playing notification file: %s", path), clientConfig.Host, clientConfig.Port)
} else {
PrintDeviceHeader("Playing notification beep", clientConfig.Host, clientConfig.Port)
}
client, err := CreateSoundTouchClient(clientConfig)
if err != nil {
@@ -158,18 +153,31 @@ func playNotificationBeep(c *cli.Context) error {
return err
}
// Use the existing playNotification endpoint
err = client.PlayNotificationBeep()
err = client.PlayNotification(path)
if err != nil {
PrintError(fmt.Sprintf("Failed to play notification beep: %v", err))
if path != "" {
PrintError(fmt.Sprintf("Failed to play notification file: %v", err))
} else {
PrintError(fmt.Sprintf("Failed to play notification beep: %v", err))
}
return err
}
fmt.Printf("✅ Notification beep played successfully\n")
if path != "" {
fmt.Printf("✅ Notification file sent successfully: %s\n", path)
} else {
fmt.Printf("✅ Notification beep played successfully\n")
}
return nil
}
// playNotificationBeep plays a notification beep on the speaker (uses existing endpoint)
func playNotificationBeep(c *cli.Context) error {
return playNotification(c)
}
// showSpeakerHelp displays help information about speaker functionality
func showSpeakerHelp(_ *cli.Context) error {
fmt.Println("SoundTouch Speaker Playback Commands")
@@ -189,6 +197,10 @@ func showSpeakerHelp(_ *cli.Context) error {
fmt.Println(" Play a simple notification sound")
fmt.Println(" Example: soundtouch-cli speaker beep")
fmt.Println()
fmt.Println("• Custom Notification:")
fmt.Println(" Play a device-local PCM file as notification")
fmt.Println(" Example: soundtouch-cli speaker notify --path \"/opt/Bose/chimes/grouped.pcm\"")
fmt.Println()
fmt.Println("Notes:")
fmt.Println("• Only ST-10 (Series III) speakers support the /speaker endpoint")
fmt.Println("• ST-300 and other models may not support this functionality")
+2 -1
View File
@@ -7,6 +7,7 @@ import (
"io"
"net"
"net/http"
"os"
"regexp"
"runtime"
"strconv"
@@ -331,7 +332,7 @@ func PrintWarning(message string) {
// showVersionInfo displays detailed version information including build details
func showVersionInfo(_ *cli.Context) error {
fmt.Printf("soundtouch-cli version %s\n", version)
fmt.Printf("%s version %s\n", os.Args[0], version)
fmt.Printf("Build commit: %s\n", commit)
fmt.Printf("Build date: %s\n", date)
fmt.Printf("Go version: %s\n", runtime.Version())
+527 -1
View File
@@ -104,7 +104,7 @@ func main() {
Version: version,
Authors: []*cli.Author{
{
Name: "Tobias Gesellchen, and the SoundTouch CLI Contributors",
Name: "Tobias Gesellchen, and the Bose-SoundTouch Contributors",
},
},
Flags: CommonFlags,
@@ -209,6 +209,72 @@ func main() {
Action: getPresets,
Before: RequireHost,
},
// Recent content commands
{
Name: "recents",
Aliases: []string{"recent"},
Usage: "Recently played content commands",
Subcommands: []*cli.Command{
{
Name: "list",
Usage: "List recently played content",
Action: getRecents,
Flags: []cli.Flag{
&cli.IntFlag{
Name: "limit",
Usage: "Maximum number of items to display (0 for all)",
Value: 10,
},
&cli.BoolFlag{
Name: "detailed",
Aliases: []string{"d"},
Usage: "Show detailed information for each item",
},
},
Before: RequireHost,
},
{
Name: "filter",
Usage: "List recently played content with filters",
Action: getRecentsFiltered,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "source",
Aliases: []string{"s"},
Usage: "Filter by source (SPOTIFY, LOCAL_MUSIC, TUNEIN, etc.)",
},
&cli.StringFlag{
Name: "type",
Aliases: []string{"t"},
Usage: "Filter by content type (track, station, playlist, album, presetable)",
},
&cli.IntFlag{
Name: "limit",
Usage: "Maximum number of items to display (0 for all)",
Value: 10,
},
&cli.BoolFlag{
Name: "detailed",
Aliases: []string{"d"},
Usage: "Show detailed information for each item",
},
},
Before: RequireHost,
},
{
Name: "latest",
Usage: "Show only the most recent item",
Action: getRecentsMostRecent,
Before: RequireHost,
},
{
Name: "stats",
Usage: "Show statistics about recent content",
Action: recentsStats,
Before: RequireHost,
},
},
},
// Playback commands
{
Name: "play",
@@ -830,6 +896,136 @@ func main() {
Action: selectAux,
Before: RequireHost,
},
{
Name: "internet-radio",
Usage: "Select internet radio stream (LOCAL_INTERNET_RADIO)",
Action: selectLocalInternetRadio,
Before: RequireHost,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "location",
Aliases: []string{"l"},
Usage: "Stream location URL (direct stream or streamUrl format)",
Required: true,
},
&cli.StringFlag{
Name: "account",
Aliases: []string{"a"},
Usage: "Source account (optional)",
},
&cli.StringFlag{
Name: "name",
Aliases: []string{"n"},
Usage: "Station name",
},
&cli.StringFlag{
Name: "artwork",
Usage: "Station artwork URL",
},
},
},
{
Name: "local-music",
Usage: "Select local music content (LOCAL_MUSIC)",
Action: selectLocalMusic,
Before: RequireHost,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "location",
Aliases: []string{"l"},
Usage: "Content location (e.g., album:983, track:2579)",
Required: true,
},
&cli.StringFlag{
Name: "account",
Aliases: []string{"a"},
Usage: "Source account GUID (required)",
Required: true,
},
&cli.StringFlag{
Name: "name",
Aliases: []string{"n"},
Usage: "Content name",
},
&cli.StringFlag{
Name: "artwork",
Usage: "Content artwork URL",
},
},
},
{
Name: "stored-music",
Usage: "Select stored music content (STORED_MUSIC)",
Action: selectStoredMusic,
Before: RequireHost,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "location",
Aliases: []string{"l"},
Usage: "Content location ID (e.g., 6_a2874b5d_4f83d999)",
Required: true,
},
&cli.StringFlag{
Name: "account",
Aliases: []string{"a"},
Usage: "Source account GUID (required)",
Required: true,
},
&cli.StringFlag{
Name: "name",
Aliases: []string{"n"},
Usage: "Content name",
},
&cli.StringFlag{
Name: "artwork",
Usage: "Content artwork URL",
},
},
},
{
Name: "content",
Usage: "Select content using ContentItem (advanced)",
Action: selectContent,
Before: RequireHost,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "source",
Aliases: []string{"s"},
Usage: "Content source (SPOTIFY, TUNEIN, LOCAL_INTERNET_RADIO, etc.)",
Required: true,
},
&cli.StringFlag{
Name: "location",
Aliases: []string{"l"},
Usage: "Content location",
Required: true,
},
&cli.StringFlag{
Name: "account",
Aliases: []string{"a"},
Usage: "Source account",
},
&cli.StringFlag{
Name: "name",
Aliases: []string{"n"},
Usage: "Content name",
},
&cli.StringFlag{
Name: "type",
Aliases: []string{"t"},
Usage: "Content type (uri, stationurl, album, track, etc.)",
},
&cli.StringFlag{
Name: "artwork",
Usage: "Content artwork URL",
},
&cli.BoolFlag{
Name: "presetable",
Usage: "Mark content as presetable",
Value: true,
},
},
},
{
Name: "availability",
Usage: "Show service availability",
@@ -842,6 +1038,44 @@ func main() {
Action: compareSourcesAndAvailability,
Before: RequireHost,
},
{
Name: "introspect",
Usage: "Get introspect data for a music service",
Action: introspectService,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "source",
Aliases: []string{"s"},
Usage: "Music service source (SPOTIFY, PANDORA, TUNEIN, etc.)",
Required: true,
},
&cli.StringFlag{
Name: "account",
Aliases: []string{"a"},
Usage: "Source account name (optional)",
},
},
Before: RequireHost,
},
{
Name: "introspect-spotify",
Usage: "Get Spotify introspect data (convenience command)",
Action: introspectSpotify,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "account",
Aliases: []string{"a"},
Usage: "Spotify account name (optional)",
},
},
Before: RequireHost,
},
{
Name: "introspect-all",
Usage: "Get introspect data for all available services",
Action: introspectAllServices,
Before: RequireHost,
},
},
},
// Bass commands
@@ -1473,6 +1707,19 @@ func main() {
},
},
},
{
Name: "notify",
Usage: "Play a notification sound or local file",
Action: playNotification,
Before: RequireHost,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "path",
Aliases: []string{"p"},
Usage: "Device-local path to a PCM file (e.g. /opt/Bose/chimes/grouped.pcm)",
},
},
},
{
Name: "beep",
Usage: "Play a notification beep sound",
@@ -1486,6 +1733,285 @@ func main() {
},
},
},
// Account management commands
{
Name: "account",
Aliases: []string{"acc"},
Usage: "Music service account management commands",
Subcommands: []*cli.Command{
{
Name: "list",
Usage: "List configured music service accounts",
Action: listMusicServiceAccounts,
Before: RequireHost,
},
{
Name: "add",
Usage: "Add a music service account",
Action: addMusicServiceAccount,
Before: RequireHost,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "source",
Aliases: []string{"s"},
Usage: "Music service source (SPOTIFY, PANDORA, AMAZON, DEEZER, IHEART, STORED_MUSIC)",
Required: true,
},
&cli.StringFlag{
Name: "user",
Aliases: []string{"u"},
Usage: "Username or account identifier",
Required: true,
},
&cli.StringFlag{
Name: "password",
Aliases: []string{"p"},
Usage: "Account password (not required for STORED_MUSIC)",
},
&cli.StringFlag{
Name: "name",
Aliases: []string{"n"},
Usage: "Display name for the service",
},
},
},
{
Name: "remove",
Usage: "Remove a music service account",
Action: removeMusicServiceAccount,
Before: RequireHost,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "source",
Aliases: []string{"s"},
Usage: "Music service source (SPOTIFY, PANDORA, AMAZON, DEEZER, IHEART, STORED_MUSIC)",
Required: true,
},
&cli.StringFlag{
Name: "user",
Aliases: []string{"u"},
Usage: "Username or account identifier",
Required: true,
},
&cli.StringFlag{
Name: "name",
Aliases: []string{"n"},
Usage: "Display name for the service",
},
},
},
{
Name: "add-spotify",
Usage: "Add a Spotify Premium account",
Action: addSpotifyAccount,
Before: RequireHost,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "user",
Aliases: []string{"u"},
Usage: "Spotify username/email",
Required: true,
},
&cli.StringFlag{
Name: "password",
Aliases: []string{"p"},
Usage: "Spotify password",
Required: true,
},
},
},
{
Name: "remove-spotify",
Usage: "Remove a Spotify account",
Action: removeSpotifyAccount,
Before: RequireHost,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "user",
Aliases: []string{"u"},
Usage: "Spotify username/email to remove",
Required: true,
},
},
},
{
Name: "add-pandora",
Usage: "Add a Pandora account",
Action: addPandoraAccount,
Before: RequireHost,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "user",
Aliases: []string{"u"},
Usage: "Pandora username",
Required: true,
},
&cli.StringFlag{
Name: "password",
Aliases: []string{"p"},
Usage: "Pandora password",
Required: true,
},
},
},
{
Name: "remove-pandora",
Usage: "Remove a Pandora account",
Action: removePandoraAccount,
Before: RequireHost,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "user",
Aliases: []string{"u"},
Usage: "Pandora username to remove",
Required: true,
},
},
},
{
Name: "add-nas",
Usage: "Add a network music library (NAS/UPnP)",
Action: addStoredMusicAccount,
Before: RequireHost,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "user",
Aliases: []string{"u"},
Usage: "UPnP server GUID with /0 suffix (e.g., d09708a1-5953-44bc-a413-123456789012/0)",
Required: true,
},
&cli.StringFlag{
Name: "name",
Aliases: []string{"n"},
Usage: "Display name for the music library",
Value: "Network Music Library",
},
},
},
{
Name: "remove-nas",
Usage: "Remove a network music library",
Action: removeStoredMusicAccount,
Before: RequireHost,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "user",
Aliases: []string{"u"},
Usage: "UPnP server GUID with /0 suffix to remove",
Required: true,
},
&cli.StringFlag{
Name: "name",
Aliases: []string{"n"},
Usage: "Display name for the music library",
Value: "Network Music Library",
},
},
},
{
Name: "add-amazon",
Usage: "Add an Amazon Music account",
Action: addAmazonMusicAccount,
Before: RequireHost,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "user",
Aliases: []string{"u"},
Usage: "Amazon Music username",
Required: true,
},
&cli.StringFlag{
Name: "password",
Aliases: []string{"p"},
Usage: "Amazon Music password",
Required: true,
},
},
},
{
Name: "remove-amazon",
Usage: "Remove an Amazon Music account",
Action: removeAmazonMusicAccount,
Before: RequireHost,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "user",
Aliases: []string{"u"},
Usage: "Amazon Music username to remove",
Required: true,
},
},
},
{
Name: "add-deezer",
Usage: "Add a Deezer Premium account",
Action: addDeezerAccount,
Before: RequireHost,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "user",
Aliases: []string{"u"},
Usage: "Deezer username",
Required: true,
},
&cli.StringFlag{
Name: "password",
Aliases: []string{"p"},
Usage: "Deezer password",
Required: true,
},
},
},
{
Name: "remove-deezer",
Usage: "Remove a Deezer account",
Action: removeDeezerAccount,
Before: RequireHost,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "user",
Aliases: []string{"u"},
Usage: "Deezer username to remove",
Required: true,
},
},
},
{
Name: "add-iheart",
Usage: "Add an iHeartRadio account",
Action: addIHeartRadioAccount,
Before: RequireHost,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "user",
Aliases: []string{"u"},
Usage: "iHeartRadio username",
Required: true,
},
&cli.StringFlag{
Name: "password",
Aliases: []string{"p"},
Usage: "iHeartRadio password",
Required: true,
},
},
},
{
Name: "remove-iheart",
Usage: "Remove an iHeartRadio account",
Action: removeIHeartRadioAccount,
Before: RequireHost,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "user",
Aliases: []string{"u"},
Usage: "iHeartRadio username to remove",
Required: true,
},
},
},
},
},
// Token commands
{
Name: "token",
+964
View File
@@ -0,0 +1,964 @@
// Package main provides the SoundTouch service daemon that acts as a proxy and management
// interface for Bose SoundTouch devices, providing Marge service emulation and device discovery.
package main
import (
"context"
"crypto/tls"
"encoding/json"
"fmt"
"log"
"net"
"net/http"
"net/url"
"os"
"path/filepath"
"runtime"
"runtime/debug"
"strings"
"time"
"github.com/gesellix/bose-soundtouch/pkg/discovery"
"github.com/gesellix/bose-soundtouch/pkg/service/certmanager"
"github.com/gesellix/bose-soundtouch/pkg/service/datastore"
"github.com/gesellix/bose-soundtouch/pkg/service/handlers"
"github.com/gesellix/bose-soundtouch/pkg/service/proxy"
"github.com/gesellix/bose-soundtouch/pkg/service/setup"
"github.com/gesellix/bose-soundtouch/pkg/service/spotify"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/urfave/cli/v2"
)
var (
version = "dev"
commit = "unknown"
date = "unknown"
)
func updateBuildInfo() {
if info, ok := debug.ReadBuildInfo(); ok {
if info.Main.Version != "" && info.Main.Version != "(devel)" {
version = info.Main.Version
}
for _, setting := range info.Settings {
switch setting.Key {
case "vcs.revision":
commit = setting.Value
case "vcs.time":
if t, err := time.Parse(time.RFC3339, setting.Value); err == nil {
date = t.Format("2006-01-02_15:04:05")
}
}
}
}
}
func main() {
updateBuildInfo()
app := &cli.App{
Name: "soundtouch-service",
Usage: "Local service for Bose SoundTouch cloud emulation and management",
Description: `⠎⠕⠥⠝⠙⠤⠞⠕⠥⠉⠓ A local server that emulates Bose cloud services (BMX, Marge).
It enables offline operation, device migration, and HTTP interaction recording.`,
Version: version,
Authors: []*cli.Author{
{
Name: "Tobias Gesellchen, and the Bose-SoundTouch Contributors",
},
},
Flags: []cli.Flag{
&cli.StringFlag{
Name: "port",
Aliases: []string{"p"},
Usage: "HTTP port to bind the service to",
Value: "8000",
EnvVars: []string{"PORT"},
},
&cli.StringFlag{
Name: "bind",
Usage: "Network interface to bind to",
EnvVars: []string{"BIND_ADDR"},
},
&cli.StringFlag{
Name: "data-dir",
Usage: "Directory for persistent data",
Value: "data",
EnvVars: []string{"DATA_DIR"},
},
&cli.StringFlag{
Name: "server-url",
Aliases: []string{"s"},
Usage: "External URL of this service",
EnvVars: []string{"SERVER_URL"},
},
&cli.StringFlag{
Name: "https-port",
Usage: "HTTPS port to bind the service to",
Value: "8443",
EnvVars: []string{"HTTPS_PORT"},
},
&cli.StringFlag{
Name: "https-server-url",
Aliases: []string{"S"},
Usage: "External HTTPS URL",
EnvVars: []string{"HTTPS_SERVER_URL"},
},
&cli.BoolFlag{
Name: "redact-logs",
Usage: "Redact sensitive data in proxy logs",
Value: true,
EnvVars: []string{"REDACT_PROXY_LOGS"},
},
&cli.BoolFlag{
Name: "log-bodies",
Usage: "Log full request/response bodies",
EnvVars: []string{"LOG_PROXY_BODY"},
},
&cli.BoolFlag{
Name: "record-interactions",
Usage: "Record HTTP interactions to disk",
Value: true,
EnvVars: []string{"RECORD_INTERACTIONS"},
},
&cli.StringFlag{
Name: "discovery-interval",
Usage: "Device discovery interval",
Value: "5m",
EnvVars: []string{"DISCOVERY_INTERVAL"},
},
&cli.BoolFlag{
Name: "dns-discovery",
Usage: "Enable DNS discovery server",
EnvVars: []string{"ENABLE_DNS_DISCOVERY"},
},
&cli.StringFlag{
Name: "dns-upstream",
Usage: "Upstream DNS server(s) for non-Bose queries (comma-separated). If empty, /etc/resolv.conf is used.",
Value: "",
EnvVars: []string{"DNS_UPSTREAM"},
},
&cli.StringFlag{
Name: "dns-bind",
Usage: "Bind address for the DNS discovery server",
Value: ":53",
EnvVars: []string{"DNS_BIND_ADDR"},
},
&cli.StringFlag{
Name: "spotify-client-id",
Usage: "Spotify OAuth client ID",
EnvVars: []string{"SPOTIFY_CLIENT_ID"},
},
&cli.StringFlag{
Name: "spotify-client-secret",
Usage: "Spotify OAuth client secret",
EnvVars: []string{"SPOTIFY_CLIENT_SECRET"},
},
&cli.StringFlag{
Name: "spotify-redirect-uri",
Usage: "Spotify OAuth redirect URI",
Value: "ueberboese-login://spotify",
EnvVars: []string{"SPOTIFY_REDIRECT_URI"},
},
&cli.StringFlag{
Name: "mgmt-username",
Usage: "Management API username for HTTP Basic Auth",
Value: "admin",
EnvVars: []string{"MGMT_USERNAME"},
},
&cli.StringFlag{
Name: "mgmt-password",
Usage: "Management API password for HTTP Basic Auth",
Value: "change_me!",
EnvVars: []string{"MGMT_PASSWORD"},
},
&cli.StringFlag{
Name: "base-url",
Usage: "External base URL for OAuth callbacks behind reverse proxy",
EnvVars: []string{"BASE_URL"},
},
&cli.BoolFlag{
Name: "mirror-enabled",
Usage: "Enable background mirroring to Bose Cloud",
EnvVars: []string{"MIRROR_ENABLED"},
},
&cli.StringSliceFlag{
Name: "mirror-endpoints",
Usage: "Endpoints to mirror to Bose Cloud (comma-separated or multiple flags)",
EnvVars: []string{"MIRROR_ENDPOINTS"},
},
&cli.StringSliceFlag{
Name: "internal-paths",
Usage: "Paths for internal requests (comma-separated or multiple flags)",
EnvVars: []string{"INTERNAL_PATHS"},
},
&cli.BoolFlag{
Name: "migration-enabled",
Usage: "Enable device directory migration from serial to MAC-based structure",
Value: true,
EnvVars: []string{"MIGRATION_ENABLED"},
},
&cli.BoolFlag{
Name: "migration-dry-run",
Usage: "Log what would be migrated without actually doing it",
EnvVars: []string{"MIGRATION_DRY_RUN"},
},
&cli.StringFlag{
Name: "preferred-source",
Usage: "Preferred source of truth (local or upstream)",
Value: "local",
EnvVars: []string{"PREFERRED_SOURCE"},
},
},
Action: func(c *cli.Context) error {
config := loadConfig(c)
ds := initDataStore(config.dataDir)
persisted := applyPersistedSettings(ds, &config)
if persisted.ServerURL == "" {
log.Printf("Creating default settings.json in %s", config.dataDir)
persisted = createDefaultSettings(ds, config)
}
// Recalculate domains if settings changed
hostname, _ := os.Hostname()
if hostname == "" {
hostname = "localhost"
}
config.domains = getDomains(config.serverURL, config.httpsServerURL, hostname)
cm := initCertificateManager(config.dataDir)
sm := setup.NewManager(config.serverURL, ds, cm)
sm.MgmtUsername = config.mgmtUsername
sm.MgmtPassword = config.mgmtPassword
server := handlers.NewServer(ds, sm, config.serverURL, config.redact, config.logBody, config.record, config.migrationEnabled, config.migrationDryRun)
sm.GetDNSRunning = server.GetDNSRunning
server.SetHTTPServerURL(config.httpsServerURL)
server.SetVersionInfo(version, commit, date)
server.SetDiscoverySettings(config.discoveryInterval, persisted.DiscoveryEnabled)
server.SetDNSSettings(persisted.DNSEnabled, strings.Join(persisted.DNSUpstream, ","), persisted.DNSBindAddr)
server.SetMirrorSettings(persisted.MirrorEnabled, persisted.MirrorEndpoints, persisted.PreferredSource)
server.SetInternalPaths(persisted.InternalPaths)
server.SetSpotifyConfig(config.spotifyClientID, config.spotifyClientSecret, config.spotifyRedirectURI)
server.SetMgmtConfig(config.mgmtUsername, config.mgmtPassword)
if config.spotifyClientID != "" {
spotifyService := spotify.NewSpotifyService(
config.spotifyClientID,
config.spotifyClientSecret,
config.spotifyRedirectURI,
config.dataDir,
)
server.SetSpotifyService(spotifyService)
clientIDPrefix := config.spotifyClientID
if len(clientIDPrefix) > 8 {
clientIDPrefix = clientIDPrefix[:8]
}
log.Printf("Spotify service initialized (client ID: %s...)", clientIDPrefix)
}
// Load and set initial DNS discoveries
dnsDiscoveries, err := ds.LoadDNSDiscoveries()
if err == nil && len(dnsDiscoveries) > 0 {
initial := make(map[string]*discovery.DiscoveredHost)
for _, entry := range dnsDiscoveries {
initial[entry.Hostname] = &discovery.DiscoveredHost{
Hostname: entry.Hostname,
FirstSeen: entry.FirstSeen,
LastSeen: entry.LastSeen,
QueryCount: entry.QueryCount,
IsBoseService: entry.IsBoseService,
IsIntercepted: entry.IsIntercepted,
RemoteAddr: entry.RemoteAddr,
}
}
server.SetDNSDiscoveries(initial)
}
server.SetShortcuts(persisted.Shortcuts)
for path, status := range persisted.Shortcuts {
log.Printf("Warning: configured shortcut: %s -> %d", path, status)
}
recorder := proxy.NewRecorder(config.dataDir)
recorder.Redact = config.redact
patternsPath := filepath.Join(config.dataDir, "patterns.json")
patterns, err := proxy.LoadPatterns(patternsPath)
if err != nil {
log.Printf("Warning: Failed to load patterns from %s: %v", patternsPath, err)
}
if len(patterns) == 0 {
log.Printf("Creating default patterns at %s", patternsPath)
patterns = proxy.DefaultPatterns()
patternsData, jsonErr := json.MarshalIndent(patterns, "", " ")
if jsonErr != nil {
log.Printf("Warning: Failed to marshal default patterns: %v", jsonErr)
} else {
_ = os.WriteFile(patternsPath, patternsData, 0644)
}
}
if len(patterns) > 0 {
recorder.Patterns = patterns
}
server.SetRecorder(recorder)
tlsConfig, err := cm.GetServerTLSConfig(config.domains)
if err != nil {
log.Printf("Warning: Failed to setup TLS: %v", err)
}
startDeviceDiscovery(server)
r := setupRouter(server)
log.Printf("Go service starting on %s", config.serverURL)
if tlsConfig != nil {
startHTTPSServer(config.httpsAddr, r, tlsConfig, config.httpsServerURL)
}
return http.ListenAndServe(config.addr, r)
},
Commands: []*cli.Command{
{
Name: "version",
Aliases: []string{"v"},
Usage: "Show detailed version information",
Action: showVersionInfo,
},
},
}
if err := app.Run(os.Args); err != nil {
log.Fatal(err)
}
}
func showVersionInfo(_ *cli.Context) error {
fmt.Printf("%s version %s\n", os.Args[0], version)
fmt.Printf("Build commit: %s\n", commit)
fmt.Printf("Build date: %s\n", date)
fmt.Printf("Go version: %s\n", runtime.Version())
fmt.Printf("Platform: %s/%s\n", runtime.GOOS, runtime.GOARCH)
return nil
}
type serviceConfig struct {
port string
bindAddr string
addr string
dataDir string
serverURL string
httpsServerURL string
httpsAddr string
redact bool
logBody bool
record bool
dnsEnabled bool
dnsUpstream string
dnsBind string
mirrorEnabled bool
mirrorEndpoints []string
internalPaths []string
discoveryInterval time.Duration
domains []string
spotifyClientID string
spotifyClientSecret string
spotifyRedirectURI string
mgmtUsername string
mgmtPassword string
migrationEnabled bool
migrationDryRun bool
preferredSource string
}
func loadConfig(c *cli.Context) serviceConfig {
port := c.String("port")
bindAddr := c.String("bind")
addr := bindAddr + ":" + port
if bindAddr == "" {
addr = ":" + port
}
dataDir := c.String("data-dir")
hostname, _ := os.Hostname()
if hostname == "" {
hostname = "localhost"
}
hostname = strings.ToLower(hostname)
serverURL := c.String("server-url")
if serverURL == "" {
serverURL = "http://" + hostname + ":" + port
}
httpsPort := c.String("https-port")
httpsAddr := bindAddr + ":" + httpsPort
if bindAddr == "" {
httpsAddr = ":" + httpsPort
}
httpsServerURL := c.String("https-server-url")
if httpsServerURL == "" {
httpsServerURL = "https://" + hostname + ":" + httpsPort
}
domains := getDomains(serverURL, httpsServerURL, hostname)
redact := c.Bool("redact-logs")
logBody := c.Bool("log-bodies")
record := c.Bool("record-interactions")
dnsEnabled := c.Bool("dns-discovery")
dnsUpstream := c.String("dns-upstream")
dnsBind := c.String("dns-bind")
discoveryIntervalStr := c.String("discovery-interval")
discoveryInterval, err := time.ParseDuration(discoveryIntervalStr)
if err != nil {
log.Printf("Warning: Failed to parse discovery interval %s, using default 5m: %v", discoveryIntervalStr, err)
discoveryInterval = 5 * time.Minute
}
spotifyClientID := c.String("spotify-client-id")
spotifyClientSecret := c.String("spotify-client-secret")
spotifyRedirectURI := c.String("spotify-redirect-uri")
mgmtUsername := c.String("mgmt-username")
mgmtPassword := c.String("mgmt-password")
mirrorEnabled := c.Bool("mirror-enabled")
mirrorEndpoints := c.StringSlice("mirror-endpoints")
internalPaths := c.StringSlice("internal-paths")
migrationEnabled := c.Bool("migration-enabled")
migrationDryRun := c.Bool("migration-dry-run")
preferredSource := c.String("preferred-source")
return serviceConfig{
port: port,
bindAddr: bindAddr,
addr: addr,
dataDir: dataDir,
serverURL: serverURL,
httpsServerURL: httpsServerURL,
httpsAddr: httpsAddr,
redact: redact,
logBody: logBody,
record: record,
dnsEnabled: dnsEnabled,
dnsUpstream: dnsUpstream,
dnsBind: dnsBind,
mirrorEnabled: mirrorEnabled,
mirrorEndpoints: mirrorEndpoints,
internalPaths: internalPaths,
discoveryInterval: discoveryInterval,
domains: domains,
spotifyClientID: spotifyClientID,
spotifyClientSecret: spotifyClientSecret,
spotifyRedirectURI: spotifyRedirectURI,
mgmtUsername: mgmtUsername,
mgmtPassword: mgmtPassword,
migrationEnabled: migrationEnabled,
migrationDryRun: migrationDryRun,
preferredSource: preferredSource,
}
}
func getDomains(serverURL, httpsServerURL, hostname string) []string {
domainsMap := map[string]bool{
// RFC-compliant wildcards for API patterns
"*.api.bose.io": true,
"*.api.bosecm.com": true,
// Core Bose domains (keep specific ones for clarity)
"streaming.bose.com": true,
"updates.bose.com": true,
"stats.bose.com": true,
"bmx.bose.com": true,
"worldwide.bose.com": true,
"music.api.bose.com": true,
"streamingoauth.bose.com": true,
"bosecm.com": true,
"bose.io": true,
"bose-prod.apigee.net": true,
"bose-test.apigee.net": true,
// Local service domains
setup.TestDomain: true,
hostname: true,
"localhost": true,
"127.0.0.1": true,
}
if u, err := url.Parse(serverURL); err == nil && u.Hostname() != "" {
domainsMap[strings.ToLower(u.Hostname())] = true
}
if u, err := url.Parse(httpsServerURL); err == nil && u.Hostname() != "" {
domainsMap[strings.ToLower(u.Hostname())] = true
}
domains := make([]string, 0, len(domainsMap))
for d := range domainsMap {
domains = append(domains, d)
}
return domains
}
func applyPersistedSettings(ds *datastore.DataStore, config *serviceConfig) datastore.Settings {
persisted, err := ds.GetSettings()
if err != nil {
return datastore.Settings{}
}
// Only override CLI values if settings file exists
// If no settings file exists, GetSettings returns empty Settings{} and we should preserve CLI values
settingsPath := filepath.Join(ds.DataDir, "settings.json")
if _, err := os.Stat(settingsPath); os.IsNotExist(err) {
return datastore.Settings{}
}
if persisted.ServerURL != "" {
config.serverURL = persisted.ServerURL
}
if persisted.HTTPServerURL != "" {
config.httpsServerURL = persisted.HTTPServerURL
}
if persisted.DiscoveryInterval != "" {
if d, durErr := time.ParseDuration(persisted.DiscoveryInterval); durErr == nil {
config.discoveryInterval = d
}
}
config.redact = persisted.RedactLogs
config.logBody = persisted.LogBodies
config.record = persisted.RecordInteractions
config.dnsEnabled = persisted.DNSEnabled
if len(persisted.DNSUpstream) > 0 {
config.dnsUpstream = strings.Join(persisted.DNSUpstream, ",")
}
if persisted.DNSBindAddr != "" {
config.dnsBind = persisted.DNSBindAddr
}
config.mirrorEnabled = persisted.MirrorEnabled
config.mirrorEndpoints = persisted.MirrorEndpoints
config.preferredSource = persisted.PreferredSource
config.internalPaths = persisted.InternalPaths
return persisted
}
func createDefaultSettings(ds *datastore.DataStore, config serviceConfig) datastore.Settings {
settings := datastore.Settings{
ServerURL: config.serverURL,
HTTPServerURL: config.httpsServerURL,
RedactLogs: config.redact,
LogBodies: config.logBody,
RecordInteractions: config.record,
DiscoveryInterval: config.discoveryInterval.String(),
DiscoveryEnabled: true,
DNSEnabled: config.dnsEnabled,
DNSUpstream: strings.Split(config.dnsUpstream, ","),
DNSBindAddr: config.dnsBind,
MirrorEnabled: config.mirrorEnabled,
MirrorEndpoints: config.mirrorEndpoints,
PreferredSource: config.preferredSource,
InternalPaths: config.internalPaths,
Shortcuts: map[string]int{
"/.well-known/appspecific/com.chrome.devtools.json": http.StatusNotFound,
"/sw.js": http.StatusNotFound,
},
}
_ = ds.SaveSettings(settings)
return settings
}
func initDataStore(dataDir string) *datastore.DataStore {
ds := datastore.NewDataStore(dataDir)
if err := ds.Initialize(); err != nil {
log.Printf("Warning: Failed to initialize datastore: %v", err)
}
return ds
}
func initCertificateManager(dataDir string) *certmanager.CertificateManager {
cm := certmanager.NewCertificateManager(filepath.Join(dataDir, "certs"))
if err := cm.EnsureCA(); err != nil {
log.Printf("Warning: Failed to ensure CA: %v", err)
}
return cm
}
func startDeviceDiscovery(server *handlers.Server) {
go func() {
for {
currentInterval, enabled := server.GetDiscoverySettings()
if enabled {
server.DiscoverDevices(context.Background())
}
time.Sleep(currentInterval)
}
}()
}
func setupRouter(server *handlers.Server) *chi.Mux {
r := chi.NewRouter()
r.Use(server.SnapshotMiddleware)
r.Use(server.OriginMiddleware)
r.Use(middleware.Recoverer)
r.Use(server.ShortcutMiddleware)
r.Use(server.MirrorMiddleware)
r.Use(server.RecordMiddleware)
r.Get("/", server.HandleRoot)
r.Get("/health", server.HandleHealth)
r.Get("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
r.URL.Path = "/media/favicon-braille.svg"
server.HandleMedia()(w, r)
})
r.Get("/media/*", server.HandleMedia())
r.Get("/web/*", server.HandleWeb())
r.Get("/docs/*", server.HandleDocs)
r.Route("/bmx", func(r chi.Router) {
r.Get("/registry/v1/services", server.HandleBMXRegistry)
r.Get("/tunein/v1/playback/station/{stationID}", server.HandleTuneInPlayback)
r.Get("/tunein/v1/playback/episodes/{podcastID}", server.HandleTuneInPodcastInfo)
r.Get("/tunein/v1/playback/episode/{podcastID}", server.HandleTuneInPlaybackPodcast)
r.Post("/orion/v1/playback/station/{data}", server.HandleOrionPlayback)
})
// Legacy or direct domain calls without /bmx prefix
r.Get("/registry/v1/services", server.HandleBMXRegistry)
r.Get("/tunein/v1/playback/station/{stationID}", server.HandleTuneInPlayback)
r.Get("/tunein/v1/playback/episodes/{podcastID}", server.HandleTuneInPodcastInfo)
r.Get("/tunein/v1/playback/episode/{podcastID}", server.HandleTuneInPlaybackPodcast)
r.Post("/orion/v1/playback/station/{data}", server.HandleOrionPlayback)
streamingRoutes := func(r chi.Router) {
r.Get("/sourceproviders", server.HandleMargeSourceProviders)
r.Get("/account/{account}/device/{device}/recent", server.HandleMargeRecents)
r.Post("/account/{account}/device/{device}/recent", server.HandleMargeAddRecent)
r.Get("/account/{account}/device/{device}/presets", server.HandleMargePresets)
r.Post("/account/{account}/device/{device}/presets/{presetNumber}", server.HandleMargeUpdatePreset)
r.Post("/support/power_on", server.HandleMargePowerOn)
r.Get("/account/{account}/provider_settings", server.HandleMargeProviderSettings)
r.Get("/device/{device}/streaming_token", server.HandleMargeStreamingToken)
r.Post("/support/customersupport", server.HandleMargeCustomerSupport)
r.Get("/device_setting/account/{account}/device/{device}/device_settings", server.HandleMargeGetDeviceSettings)
r.Get("/account/{account}/device/{device}/group", server.HandleMargeDeviceGroup)
r.Get("/account/{account}/device/{device}/group/", server.HandleMargeDeviceGroup)
r.Get("/account/{account}/device/{device}/group/server", server.HandleMargeDeviceGroupServer)
r.Get("/account/{account}/device/{device}/group/member", server.HandleMargeDeviceGroupMember)
r.Post("/device_setting/account/{account}/device/{device}/device_settings", server.HandleMargeUpdateDeviceSettings)
r.Get("/account/{account}/emailaddress", server.HandleMargeGetEmailAddress)
r.Get("/account/{account}/full", server.HandleMargeAccountFull)
r.Get("/software/update/account/{account}", server.HandleMargeSoftwareUpdate)
r.Route("/stats", func(r chi.Router) {
r.Post("/usage", server.HandleUsageStats)
r.Post("/error", server.HandleErrorStats)
})
}
accountsRoutes := func(r chi.Router) {
r.Get("/{account}/full", server.HandleMargeAccountFull)
r.Get("/{account}/devices/{device}/presets", server.HandleMargePresets)
r.Post("/{account}/devices/{device}/presets/{presetNumber}", server.HandleMargeUpdatePreset)
r.Get("/{account}/devices/{device}/recents", server.HandleMargeRecents)
r.Post("/{account}/devices/{device}/recents", server.HandleMargeAddRecent)
r.Post("/{account}/devices", server.HandleMargeAddDevice)
r.Delete("/{account}/devices/{device}", server.HandleMargeRemoveDevice)
r.Get("/{account}/devices/{device}/group", server.HandleMargeDeviceGroup)
r.Get("/{account}/devices/{device}/group/", server.HandleMargeDeviceGroup)
r.Get("/{account}/devices/{device}/group/server", server.HandleMargeDeviceGroupServer)
r.Get("/{account}/devices/{device}/group/member", server.HandleMargeDeviceGroupMember)
}
r.Route("/marge", func(r chi.Router) {
r.Route("/streaming", streamingRoutes)
r.Route("/accounts", accountsRoutes)
r.Get("/updates/soundtouch", server.HandleMargeSoftwareUpdate)
})
// Legacy or direct domain calls without /marge prefix
r.Route("/streaming", streamingRoutes)
r.Route("/accounts", accountsRoutes)
r.Get("/updates/soundtouch", server.HandleMargeSoftwareUpdate)
r.Route("/customer", func(r chi.Router) {
r.Get("/account/{account}", server.HandleMargeAccountProfile)
r.Post("/account/{account}", server.HandleMargeUpdateAccountProfile)
r.Post("/account/{account}/password", server.HandleMargeChangePassword)
})
r.Route("/oauth", func(r chi.Router) {
r.HandleFunc("/*", server.HandleBoseProxy)
})
r.Route("/v1", func(r chi.Router) {
r.Post("/stapp/{deviceId}", server.HandleAppEvents)
r.Post("/scmudc/{deviceId}", server.HandleAppEvents)
})
r.Route("/mgmt", func(r chi.Router) {
// Browser OAuth callback — no auth required (Spotify redirects the
// user's browser here directly). The authorization code is single-use,
// short-lived, and useless without the client_secret.
r.Get("/spotify/callback", server.HandleMgmtSpotifyCallback)
// All other management endpoints require Basic Auth.
r.Group(func(r chi.Router) {
r.Use(server.BasicAuthMgmt())
r.Get("/accounts/{accountId}/speakers", server.HandleMgmtListSpeakers)
r.Get("/devices/{deviceId}/events", server.HandleMgmtDeviceEvents)
r.Post("/spotify/init", server.HandleMgmtSpotifyInit)
r.Post("/spotify/confirm", server.HandleMgmtSpotifyConfirm)
r.Get("/spotify/accounts", server.HandleMgmtSpotifyAccounts)
r.Get("/spotify/token", server.HandleMgmtSpotifyToken)
r.Post("/spotify/entity", server.HandleMgmtSpotifyEntity)
r.Post("/spotify/prime", server.HandleMgmtPrimeDevice)
})
})
r.Get("/proxy/*", server.HandleProxyRequest)
r.Get("/devices", server.HandleListDiscoveredDevices)
r.Get("/devices/{deviceId}/info", server.HandleGetStockholmDeviceInfo)
r.Post("/devices/{deviceId}/key/{key}", server.HandleDeviceKey)
r.Post("/devices/{deviceId}/volume/{level}", server.HandleDeviceVolume)
// Stockholm Mini app
r.Handle("/stockholm-mini/*", http.StripPrefix("/stockholm-mini/", http.FileServer(http.Dir("pkg/service/handlers/web/stockholm-mini"))))
r.Route("/devices", func(r chi.Router) {
r.Get("/", server.HandleListDiscoveredDevices)
r.Post("/", server.HandleAddManualDevice)
r.Route("/{deviceId}", func(r chi.Router) {
r.Delete("/", server.HandleRemoveDevice)
r.Get("/events", server.HandleGetDeviceEvents)
r.Get("/info", server.HandleGetDeviceInfo)
r.Get("/ws", server.HandleDeviceWebSocket)
r.Post("/key/{key}", server.HandleDeviceKey)
r.Post("/volume/{level}", server.HandleDeviceVolume)
r.Post("/reboot", server.HandleRebootDevice)
})
})
r.Get("/version", server.HandleGetVersionInfo)
r.Route("/setup", func(r chi.Router) {
r.Post("/discover", server.HandleTriggerDiscovery)
r.Get("/discovery-status", server.HandleGetDiscoveryStatus)
r.Get("/settings", server.HandleGetSettings)
r.Post("/settings", server.HandleUpdateSettings)
r.Get("/info/{deviceId}", server.HandleGetDeviceInfo)
r.Get("/summary/{deviceId}", server.HandleGetMigrationSummary)
r.Post("/migrate/{deviceId}", server.HandleMigrateDevice)
r.Post("/revert/{deviceId}", server.HandleRevertMigration)
r.Post("/reboot/{deviceId}", server.HandleRebootDevice)
r.Post("/trust-ca/{deviceId}", server.HandleTrustCACert)
r.Post("/ensure-remote-services/{deviceId}", server.HandleEnsureRemoteServices)
r.Post("/remove-remote-services/{deviceId}", server.HandleRemoveRemoteServices)
r.Post("/backup/{deviceId}", server.HandleBackupConfig)
r.Post("/sync/{deviceId}", server.HandleInitialSync)
r.Post("/test-connection/{deviceId}", server.HandleTestConnection)
r.Post("/test-hosts/{deviceId}", server.HandleTestHostsRedirection)
r.Post("/test-dns/{deviceId}", server.HandleTestDNSRedirection)
r.Get("/ca.crt", server.HandleGetCACert)
r.Get("/proxy-settings", server.HandleGetProxySettings)
r.Post("/proxy-settings", server.HandleUpdateProxySettings)
r.Get("/interaction-stats", server.HandleGetInteractionStats)
r.Get("/interactions", server.HandleListInteractions)
r.Get("/interaction-content", server.HandleGetInteractionContent)
r.Get("/parity-mismatches", server.HandleListParityMismatches)
r.Delete("/parity-mismatches", server.HandleClearParityMismatches)
r.Get("/interactions/sessions/{session}/download", server.HandleDownloadSession)
r.Delete("/interactions/sessions/{session}", server.HandleDeleteSession)
r.Delete("/interactions/sessions", server.HandleCleanupSessions)
r.Get("/dns-discoveries", server.HandleGetDNSDiscoveries)
r.Get("/dns-discoveries/download", server.HandleDownloadDNSDiscoveries)
r.Delete("/dns-discoveries", server.HandleClearDNSDiscoveries)
r.Route("/devices/{deviceId}", func(r chi.Router) {
r.Get("/summary", server.HandleGetMigrationSummary)
r.Post("/migrate", server.HandleMigrateDevice)
r.Post("/revert", server.HandleRevertMigration)
r.Post("/trust-ca", server.HandleTrustCACert)
r.Post("/ensure-remote-services", server.HandleEnsureRemoteServices)
r.Post("/remove-remote-services", server.HandleRemoveRemoteServices)
r.Post("/backup", server.HandleBackupConfig)
r.Post("/sync", server.HandleInitialSync)
r.Post("/test-connection", server.HandleTestConnection)
r.Post("/test-hosts", server.HandleTestHostsRedirection)
r.Post("/test-dns", server.HandleTestDNSRedirection)
})
})
r.NotFound(server.HandleNotFound)
return r
}
func startHTTPSServer(httpsAddr string, r http.Handler, tlsConfig *tls.Config, httpsServerURL string) {
// Add custom error logging and connection state tracking
tlsConfig.GetCertificate = func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
// log.Printf("[TLS] Certificate request for ServerName: %s", clientHello.ServerName)
// Use the default certificate selection logic
for _, cert := range tlsConfig.Certificates {
if cert.Leaf != nil {
for _, name := range cert.Leaf.DNSNames {
if matchesDomain(name, clientHello.ServerName) {
// log.Printf("[TLS] ✅ Serving certificate for %s (matched %s)", clientHello.ServerName, name)
return &cert, nil
}
}
}
}
// If no specific match, return the first certificate and log it
if len(tlsConfig.Certificates) > 0 {
// log.Printf("[TLS] ⚠️ No exact match for %s, using default certificate", clientHello.ServerName)
return &tlsConfig.Certificates[0], nil
}
log.Printf("[TLS] ❌ No certificate available for %s", clientHello.ServerName)
return nil, fmt.Errorf("no certificate available for %s", clientHello.ServerName)
}
httpsServer := &http.Server{
Addr: httpsAddr,
Handler: r,
TLSConfig: tlsConfig,
ErrorLog: log.Default(), // Ensure error logging is enabled
}
log.Printf("Go service starting HTTPS on %s", httpsServerURL)
go func() {
listener, err := net.Listen("tcp", httpsAddr)
if err != nil {
log.Printf("[TLS] Failed to create listener: %v", err)
return
}
tlsListener := tls.NewListener(listener, tlsConfig)
// Wrap listener to log connection attempts
wrappedListener := &loggingTLSListener{
Listener: tlsListener,
}
if err := httpsServer.Serve(wrappedListener); err != nil && err != http.ErrServerClosed {
log.Printf("HTTPS server error: %v", err)
}
}()
}
// matchesDomain checks if a certificate domain (which may be a wildcard) matches a server name
func matchesDomain(certDomain, serverName string) bool {
if certDomain == serverName {
return true
}
// Handle wildcard certificates (only at the beginning of a label)
if strings.HasPrefix(certDomain, "*.") {
certBase := certDomain[2:] // Remove "*."
// For *.api.bose.io to match events.api.bose.io but not test.content.api.bose.io
// We need to ensure only one label is replaced by the wildcard
if strings.HasSuffix(serverName, "."+certBase) {
// Count dots to ensure we're not matching too many levels
serverPrefix := strings.TrimSuffix(serverName, "."+certBase)
if !strings.Contains(serverPrefix, ".") {
return true
}
}
// Also match the base domain (e.g., api.bose.io matches *.api.bose.io)
if serverName == certBase {
return true
}
}
return false
}
// loggingTLSListener wraps a TLS listener to log connection attempts and handshake failures
type loggingTLSListener struct {
net.Listener
}
func (l *loggingTLSListener) Accept() (net.Conn, error) {
conn, err := l.Listener.Accept()
if err != nil {
return nil, err
}
// Wrap the connection to log TLS handshake results
return &loggingTLSConn{
Conn: conn,
addr: conn.RemoteAddr(),
}, nil
}
// loggingTLSConn wraps a TLS connection to log handshake failures
type loggingTLSConn struct {
net.Conn
addr net.Addr
handshakeLogged bool
}
func (c *loggingTLSConn) Read(b []byte) (n int, err error) {
n, err = c.Conn.Read(b)
// Log TLS handshake failures on first read attempt
if !c.handshakeLogged {
c.handshakeLogged = true
if err != nil {
// Check if this looks like a TLS handshake failure
if strings.Contains(err.Error(), "tls:") ||
strings.Contains(err.Error(), "handshake") ||
strings.Contains(err.Error(), "certificate") {
log.Printf("[TLS] ❌ Handshake failed from %s: %v", c.addr, err)
}
}
}
return n, err
}
+92
View File
@@ -0,0 +1,92 @@
package main
import (
"os"
"testing"
"github.com/gesellix/bose-soundtouch/pkg/service/datastore"
)
func TestApplyPersistedSettings(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "main-test")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
ds := datastore.NewDataStore(tmpDir)
t.Run("overrides true with false", func(t *testing.T) {
config := &serviceConfig{
redact: true,
logBody: true,
record: true,
}
// Simulate the bug by using the old bitwise OR logic in the test,
// which should fail if we expect false.
// config.redact = config.redact || false -> stays true
settings := datastore.Settings{
RedactLogs: false,
LogBodies: false,
RecordInteractions: false,
}
err := ds.SaveSettings(settings)
if err != nil {
t.Fatalf("Failed to save settings: %v", err)
}
applyPersistedSettings(ds, config)
if config.redact != false {
t.Errorf("Expected redact to be false, got true")
}
if config.logBody != false {
t.Errorf("Expected logBody to be false, got true")
}
if config.record != false {
t.Errorf("Expected record to be false, got true")
}
})
t.Run("retains false when settings are false", func(t *testing.T) {
settings := datastore.Settings{
RedactLogs: false,
}
err := ds.SaveSettings(settings)
if err != nil {
t.Fatalf("Failed to save settings: %v", err)
}
config := &serviceConfig{
redact: false,
}
applyPersistedSettings(ds, config)
if config.redact != false {
t.Errorf("Expected redact to be false, got true")
}
})
t.Run("overrides false with true", func(t *testing.T) {
settings := datastore.Settings{
RedactLogs: true,
}
err := ds.SaveSettings(settings)
if err != nil {
t.Fatalf("Failed to save settings: %v", err)
}
config := &serviceConfig{
redact: false,
}
applyPersistedSettings(ds, config)
if config.redact != true {
t.Errorf("Expected redact to be true, got false")
}
})
}
+8
View File
@@ -0,0 +1,8 @@
accounts/
certs/
default/
dns/
interactions/
parity_mismatches/
patterns.json
settings.json
+24 -76
View File
@@ -1,8 +1,11 @@
// Package soundtouch provides a comprehensive Go library and CLI tool for controlling Bose SoundTouch devices.
// Package soundtouch provides a comprehensive Go library, CLI tool, and local service for controlling and emulating Bose SoundTouch devices.
//
// This library implements the complete Bose SoundTouch Web API, enabling programmatic control
// This project implements the complete Bose SoundTouch Web API, enabling programmatic control
// of SoundTouch speakers including playback control, volume management, source selection,
// multiroom zone management, and real-time event monitoring via WebSocket connections.
// multiroom zone management, and real-time event monitoring.
//
// It also provides a local service (`soundtouch-service`) that can emulate the Bose Cloud,
// allowing for offline control and enhanced debugging through HTTP interaction recording.
//
// # Quick Start
//
@@ -41,63 +44,20 @@
// if err != nil {
// log.Fatal(err)
// }
//
// // Set volume
// err = client.SetVolume(50)
// if err != nil {
// log.Fatal(err)
// }
// }
//
// # Device Discovery
// # SoundTouch Service
//
// Automatically discover SoundTouch devices on your network:
// The `soundtouch-service` provides several advanced features:
//
// import "github.com/gesellix/bose-soundtouch/pkg/discovery"
// - Bose Cloud Emulation: Allows speakers to work without an internet connection.
// - HTTP Interaction Recording: Captures all traffic as IntelliJ-compatible .http files.
// - Speaker Migration: Automated tools to redirect speakers to the local service.
// - Web Interface: A management dashboard for proxy settings and speaker setup.
//
// // Discover devices using UPnP/SSDP
// service := discovery.NewService(5*time.Second)
// devices, err := service.DiscoverDevices(ctx)
// if err != nil {
// log.Fatal(err)
// }
// Install the service:
//
// for _, device := range devices {
// fmt.Printf("Found device: %s at %s\n", device.Name, device.Host)
// }
//
// # Real-time Events
//
// Monitor device state changes in real-time using WebSocket connections:
//
// // Subscribe to device events
// events, err := client.SubscribeToEvents(ctx)
// if err != nil {
// log.Fatal(err)
// }
//
// for event := range events {
// switch e := event.(type) {
// case *models.NowPlayingUpdated:
// fmt.Printf("Now playing: %s by %s\n", e.Track, e.Artist)
// case *models.VolumeUpdated:
// fmt.Printf("Volume changed to: %d\n", e.ActualVolume)
// }
// }
//
// # Multiroom Zone Management
//
// Create and manage multiroom zones:
//
// // Create a zone with multiple speakers
// zone := &models.Zone{
// Master: "192.168.1.100",
// Members: []models.ZoneMember{
// {IPAddress: "192.168.1.101"},
// {IPAddress: "192.168.1.102"},
// },
// }
// err = client.SetZone(zone)
// go install github.com/gesellix/bose-soundtouch/cmd/soundtouch-service@latest
//
// # CLI Tool
//
@@ -111,45 +71,33 @@
//
// # Control a device
// soundtouch-cli --host 192.168.1.100 play start
// soundtouch-cli --host 192.168.1.100 volume set --level 50
// soundtouch-cli --host 192.168.1.100 source select --source SPOTIFY
//
// # Supported Features
//
// - ✅ Device Information & Capabilities
// - ✅ Playback Control (Play/Pause/Stop/Next/Previous)
// - ✅ Volume, Bass, and Balance Control
// - ✅ Source Selection (Spotify, Bluetooth, AUX, etc.)
// - ✅ Preset Management
// - ✅ Clock/Time Management
// - ✅ Network Information
// - ✅ Playback, Volume, Bass, and Balance Control
// - ✅ Source Selection & Preset Management
// - ✅ Real-time WebSocket Events
// - ✅ Multiroom Zone Management
// - ✅ Device Discovery (UPnP/SSDP and mDNS)
// - ✅ Cross-platform Support (Windows, macOS, Linux)
// - ✅ Local Cloud Emulation (soundtouch-service)
// - ✅ HTTP Traffic Recording & Sanitization
// - ✅ Automated Speaker Migration & Revert
//
// # Package Structure
//
// - client: HTTP client for SoundTouch Web API
// - discovery: Device discovery using UPnP/SSDP and mDNS
// - models: Data structures for API requests/responses
// - config: Configuration management
// - service: Core logic for the soundtouch-service (proxy, recording, setup)
// - cmd/soundtouch-cli: Command-line interface tool
//
// # Hardware Compatibility
//
// This library has been tested with real Bose SoundTouch hardware and supports
// all SoundTouch-compatible devices including:
// - SoundTouch 10, 20, 30 series
// - SoundTouch Portable
// - Wave SoundTouch music system
// - And other SoundTouch-enabled Bose speakers
// - cmd/soundtouch-service: Local cloud emulation service
//
// # Implementation Notes
//
// This implementation is based on the official Bose SoundTouch Web API documentation
// and provides 90% coverage of all available endpoints. It is an independent project
// and is not affiliated with or endorsed by Bose Corporation.
// This project is an independent effort to preserve the functionality of Bose SoundTouch
// devices and provide enhanced debugging and control capabilities. It is not
// affiliated with or endorsed by Bose Corporation.
//
// For detailed API documentation, examples, and advanced usage patterns, visit:
// https://pkg.go.dev/github.com/gesellix/bose-soundtouch
+41
View File
@@ -0,0 +1,41 @@
services:
soundtouch-service:
image: ghcr.io/gesellix/bose-soundtouch:latest
# build: .
container_name: soundtouch-service
# Linux only, required for discovery. Swarm requires host network at the task level.
# network_mode: host
ports:
- "8000:8000"
- "8443:8443"
environment:
- PORT=8000
- HTTPS_PORT=8443
- DATA_DIR=/app/data
- LOG_PROXY_BODY=false
- REDACT_PROXY_LOGS=true
- RECORD_INTERACTIONS=true
- DISCOVERY_INTERVAL=5m
- SERVER_URL=http://${SOUNDTOUCH_HOSTNAME:-soundtouch.local}:8000
- HTTPS_SERVER_URL=https://${SOUNDTOUCH_HOSTNAME:-soundtouch.local}:8443
volumes:
- soundtouch-data:/app/data
# Use host volume for local development if preferred:
# - ./data:/app/data
restart: unless-stopped
deploy:
replicas: 1
restart_policy:
condition: on-failure
resources:
limits:
cpus: '0.50'
memory: 512M
reservations:
cpus: '0.25'
memory: 128M
volumes:
soundtouch-data:
# Named volumes are preferred in Swarm. For multi-node persistence,
# consider using a volume driver like NFS or GlusterFS.
+2 -3
View File
@@ -4,9 +4,9 @@
This document contains important development guidelines for working on the Bose SoundTouch project. Please also read the following documentation:
- **[PLAN.md](PLAN.md)** - Project planning and roadmap
- **[PLAN.md](archive/PLAN.md)** - Project planning and roadmap
- **[PROJECT-PATTERNS.md](PROJECT-PATTERNS.md)** - Project structure and design patterns
- **[API-Endpoints-Overview.md](API-Endpoints-Overview.md)** - API endpoints overview
- **[API-ENDPOINTS.md](reference/API-ENDPOINTS.md)** - API endpoints overview
- **[SoundTouch Web API.pdf](2025.12.18%20SoundTouch%20Web%20API.pdf)** - Official API documentation
## Development Guidelines
@@ -95,4 +95,3 @@ When creating test data for API endpoints, prefer real device responses over hyp
- **Documentation**: Completely in English for international accessibility
- Conduct regular code reviews
- Consider performance from the beginning
+212
View File
@@ -0,0 +1,212 @@
# Content Selection Implementation Summary
This document summarizes the implementation of advanced content selection features for the Bose SoundTouch Go client, including full support for the LOCAL_INTERNET_RADIO streamUrl format and LOCAL_MUSIC/STORED_MUSIC content selection.
## ✅ Implementation Status: COMPLETE
All content selection features from the [SoundTouch WebServices API Wiki](https://github.com/thlucas1/homeassistantcomponent_soundtouchplus/wiki/SoundTouch-WebServices-API) are now fully implemented with comprehensive API methods, CLI commands, tests, and documentation.
## 🎯 Features Implemented
### 1. Core API Methods
#### `SelectContentItem(contentItem *models.ContentItem) error`
- **Purpose**: Generic method for selecting any content using a ContentItem directly
- **Use Case**: Maximum flexibility for complex content selection scenarios
- **Validation**: Ensures ContentItem is not nil and has a valid source
#### `SelectLocalInternetRadio(location, sourceAccount, itemName, containerArt string) error`
- **Purpose**: Select LOCAL_INTERNET_RADIO content with streamUrl format support
- **Features**:
- Direct stream URLs (e.g., `https://stream.example.com/radio`)
- streamUrl proxy format (e.g., `http://contentapi.gmuth.de/station.php?name=Station&streamUrl=ActualStream`)
- Automatic defaults for missing parameters
- **Use Cases**: Internet radio streams, proxy-based radio services
#### `SelectLocalMusic(location, sourceAccount, itemName, containerArt string) error`
- **Purpose**: Select LOCAL_MUSIC content from SoundTouch App Media Server
- **Requirements**: SoundTouch App Media Server running on a computer
- **Content Types**: Albums, tracks, artists, playlists
- **Validation**: Requires both location and sourceAccount
#### `SelectStoredMusic(location, sourceAccount, itemName, containerArt string) error`
- **Purpose**: Select STORED_MUSIC content from UPnP/DLNA media servers
- **Requirements**: UPnP/DLNA media server (Windows Media Player, NAS, etc.)
- **Content Types**: NAS libraries, network music collections
- **Validation**: Requires both location and sourceAccount
### 2. CLI Commands
All API methods are exposed through comprehensive CLI commands:
#### `soundtouch-cli source internet-radio`
```bash
soundtouch-cli --host <device> source internet-radio \
--location "http://contentapi.gmuth.de/station.php?name=MyStation&streamUrl=https://stream.example.com/radio" \
--name "My Station" \
--artwork "https://example.com/art.png"
```
#### `soundtouch-cli source local-music`
```bash
soundtouch-cli --host <device> source local-music \
--location "album:983" \
--account "3f205110-4a57-4e91-810a-123456789012" \
--name "Welcome to the New"
```
#### `soundtouch-cli source stored-music`
```bash
soundtouch-cli --host <device> source stored-music \
--location "6_a2874b5d_4f83d999" \
--account "d09708a1-5953-44bc-a413-123456789012/0" \
--name "Christmas Album"
```
#### `soundtouch-cli source content` (Advanced)
```bash
soundtouch-cli --host <device> source content \
--source LOCAL_INTERNET_RADIO \
--location "https://stream.example.com/radio" \
--name "My Stream" \
--type stationurl \
--presetable
```
## 🧪 Test Coverage
Comprehensive test suites implemented for all new functionality:
### Unit Tests
- **TestClient_SelectContentItem**: 5 test cases covering valid/invalid inputs
- **TestClient_SelectLocalInternetRadio**: 4 test cases including streamUrl format
- **TestClient_SelectLocalMusic**: 4 test cases with validation
- **TestClient_SelectStoredMusic**: 4 test cases with error handling
### Test Coverage Summary
- ✅ Valid content selection scenarios
- ✅ streamUrl format validation
- ✅ Parameter validation and error handling
- ✅ Default value assignment
- ✅ HTTP request formatting verification
## 📚 Documentation
### Updated Documentation
1. **CLI-REFERENCE.md**: Added comprehensive CLI command examples
2. **Content Selection Example**: New `/examples/content-selection/` with working code
3. **README Updates**: Added streamUrl format examples
4. **API Documentation**: Inline Go documentation for all methods
### Example Code
Complete working example demonstrating:
- LOCAL_INTERNET_RADIO with streamUrl proxy format
- LOCAL_INTERNET_RADIO with direct streams
- LOCAL_MUSIC content selection
- STORED_MUSIC content selection
- Generic ContentItem usage
## 🔍 streamUrl Format Support
### What is the streamUrl Format?
The streamUrl format uses a proxy server that accepts the actual stream URL as a parameter:
```
http://contentapi.gmuth.de/station.php?name=StationName&streamUrl=ActualStreamURL
```
### Implementation Details
- **Full Support**: All streamUrl format URLs work seamlessly
- **Example from Wiki**: Exact implementation matches the wiki specification
- **ContentItem Structure**:
```go
contentItem := &models.ContentItem{
Source: "LOCAL_INTERNET_RADIO",
Type: "stationurl",
Location: "http://contentapi.gmuth.de/station.php?name=Antenne%20Chillout&streamUrl=https://stream.antenne.de/chillout/stream/aacp",
IsPresetable: false,
ItemName: "Antenne Chillout",
ContainerArt: "https://www.radio.net/300/antennechillout.png",
}
```
## 🏗️ Architecture
### Design Principles
1. **Consistency**: All methods follow the same parameter patterns
2. **Flexibility**: `SelectContentItem()` allows maximum control
3. **Convenience**: Specific methods (`SelectLocalInternetRadio()`, etc.) provide simpler interfaces
4. **Validation**: Comprehensive input validation with clear error messages
5. **Defaults**: Sensible defaults when optional parameters are empty
### ContentItem Construction
All convenience methods create properly structured `ContentItem` objects:
- Automatic `Type` assignment based on source
- `IsPresetable` defaults to `true`
- Default `ItemName` when not provided
- Proper source-specific validation
## 🎵 Related Features
### Sibling Features (Also Implemented)
Based on the wiki structure, these related features are also supported:
1. **LOCAL_MUSIC**: ✅ Fully implemented
2. **STORED_MUSIC**: ✅ Fully implemented
3. **SPOTIFY**: ✅ Previously implemented
4. **TUNEIN**: ✅ Previously implemented
5. **BLUETOOTH**: ✅ Previously implemented
6. **AIRPLAY**: ✅ Previously implemented
## 📋 Usage Examples
### API Usage
```go
// streamUrl format
location := "http://contentapi.gmuth.de/station.php?name=MyStation&streamUrl=https://stream.example.com/radio"
err := client.SelectLocalInternetRadio(location, "", "My Station", "")
// Direct ContentItem
contentItem := &models.ContentItem{
Source: "LOCAL_INTERNET_RADIO",
Type: "stationurl",
Location: location,
ItemName: "My Station",
IsPresetable: true,
}
err := client.SelectContentItem(contentItem)
```
### CLI Usage
```bash
# streamUrl format
soundtouch-cli --host 192.168.1.100 source internet-radio \
--location "http://contentapi.gmuth.de/station.php?name=MyStation&streamUrl=https://stream.example.com/radio" \
--name "My Station"
# Direct stream
soundtouch-cli --host 192.168.1.100 source internet-radio \
--location "https://stream.example.com/radio" \
--name "Direct Stream"
```
## 🔗 References
- [SoundTouch WebServices API Wiki](https://github.com/thlucas1/homeassistantcomponent_soundtouchplus/wiki/SoundTouch-WebServices-API)
- [LOCAL_INTERNET_RADIO - streamUrl format](https://github.com/thlucas1/homeassistantcomponent_soundtouchplus/wiki/SoundTouch-WebServices-API#select-local_internet_radio---streamurl-format)
- [LOCAL_MUSIC](https://github.com/thlucas1/homeassistantcomponent_soundtouchplus/wiki/SoundTouch-WebServices-API#select-local_music)
- [Content Selection Example](../examples/content-selection/README.md)
- [CLI Reference](guides/CLI-REFERENCE.md)
- [Content Selection Example (Direct)](../examples/content-selection/)
## ✅ Verification
This implementation has been verified to:
1. ✅ Support exact wiki specification for streamUrl format
2. ✅ Handle all LOCAL_INTERNET_RADIO, LOCAL_MUSIC, and STORED_MUSIC scenarios
3. ✅ Pass comprehensive test suite
4. ✅ Work with CLI commands
5. ✅ Include complete documentation and examples
6. ✅ Maintain backward compatibility
**Status**: 🎉 **COMPLETE** - All requested content selection features are fully implemented and ready for use!
+107
View File
@@ -0,0 +1,107 @@
# Device Logging & Troubleshooting
Accessing logs from SoundTouch devices is critical for debugging custom service integrations and understanding internal device behavior. This document outlines the methods for collecting logs, as discovered by the **SoundCork** and **ÜberBöse API** communities.
## Log Types
1. **System Logs**: Internal OS logs (Linux-based) including `dmesg`, `syslog`, and process-specific logs.
2. **Traffic Logs**: Real-time HTTP/HTTPS requests sent by the device to cloud or local services.
3. **Proxy Logs**: Logs generated by the `soundtouch-service` when it acts as a man-in-the-middle.
---
## 1. Accessing System Logs (Requires Root)
Most SoundTouch devices run a modified Linux distribution. Accessing these logs requires root SSH or Telnet access.
### Enabling Root Access (Remote Services)
Community research (SoundCork Issue #112) has identified a "backdoor" to enable developer services:
1. **USB Method**:
- Format a USB stick to **FAT32**.
- Create an empty file named `remote_services` (no extension) in the root of the USB stick.
- Insert the stick into the SoundTouch device.
- Reboot the device (power cycle).
- On some models, you may need to hold **4** and **Volume -** on the device while powering on to force a USB check.
2. **TAP Command (Legacy)**:
- On older firmware versions, you can connect to port 17000 via Telnet and issue the command: `remote_services on`.
### Making Root Access Persistent
Once you have logged in as `root` (usually no password or a well-known community password), you can make the access survive reboots without the USB stick:
```bash
touch /mnt/nv/remote_services
/etc/init.d/sshd start
```
### Viewing Logs
Once inside via SSH:
- **Kernel Logs**: `dmesg`
- **System Logs**: `cat /var/log/messages` or `tail -f /tmp/soundtouch.log` (paths vary by firmware).
- **Real-time Monitoring**: `logread -f`
- **Process List**: `ps w`
#### Pro-Tip: Filtered Real-time Monitoring
To focus on cloud service and preset interactions (Marge), use the following command on the device:
```bash
logread -f | grep -Ei '(marge|preset)'
```
This is particularly useful for debugging preset synchronization and service redirection issues.
---
## 2. Traffic Logging & Interception
If you cannot or do not want to root the device, you can monitor its outbound traffic by redirecting it to a proxy.
### Via `soundtouch-service`
The `soundtouch-service` included in this repository includes a built-in proxy. When a device is migrated to use this service, all of its cloud-bound traffic is logged to the service console.
**Key Traffic to Monitor**:
- `POST /v1/scmudc/{deviceId}`: Real-time telemetry events.
- `GET /marge/...`: Account and streaming configuration requests.
- `POST /streaming/support/power_on`: Boot-time diagnostics.
### Via Packet Sniffing (Advanced)
If you have a managed switch or a router capable of port mirroring, you can use **Wireshark** or `tcpdump` to capture traffic.
- **Filter**: `tcp port 80 or tcp port 443`
- **Target**: The IP address of your SoundTouch device.
---
## 3. Troubleshooting Common Issues
### "IsItBose" Validation Failures
If the device fails to connect to your custom service despite correct configuration, it may be failing the internal `IsItBose` regex check.
- **Evidence**: Look for SSL handshake failures or "Unauthorized" errors in your service logs.
- **Solution**: See the [Binary Patching section in DEVICE-REDIRECT-METHODS.md](analysis/DEVICE-REDIRECT-METHODS.md#method-3-binary-patching).
### Disappearing Sources (TuneIn/Local Radio)
If `TUNEIN` or `LOCAL_INTERNET_RADIO` sources disappear after a reboot in an offline environment.
- **Cause**: These sources are validated against the cloud only during the initial boot sequence.
- **Solution**: Ensure your emulated service is reachable and responding correctly to `/streaming/support/power_on` and `/streaming/sourceproviders` during the device's boot-up.
---
## 4. HTTP Protocol Quirks
### ETag Case-Sensitivity
Research in **SoundCork Issue #129** revealed a significant bug in the SoundTouch device firmware regarding HTTP `ETag` headers.
- **The Issue**: The device firmware expects the `ETag` header to be exactly title-cased (`ETag`). Many modern web servers or frameworks (like FastAPI/Uvicorn) return headers in all lowercase (`etag`) per HTTP/2 or standard case-insensitive conventions.
- **The Symptom**: If the server returns a lowercase `etag`, the device fails to recognize it. Consequently, the device will never send an `If-None-Match` header in subsequent requests, breaking preset synchronization and efficient caching.
- **The Workaround**: If you are using a custom service, you may need to use a reverse proxy (like **Nginx**) or a middleware to force the header casing to `ETag`.
**Example Nginx Fix**:
```nginx
proxy_hide_header etag;
add_header ETag $upstream_http_etag;
```
---
## References
- [SoundCork Issue #112: Enabling Remote Services](https://github.com/deborahgu/soundcork/issues/112)
- [SoundCork Issue #149: Debugging with Systemd/Gunicorn](https://github.com/deborahgu/soundcork/issues/149)
- [ÜberBöse API: Telemetry Documentation](https://github.com/julius-d/ueberboese-api)
- [SoundCork Issue #129: ETag Case-Sensitivity & Preset Sync](https://github.com/deborahgu/soundcork/issues/129)
+1 -1
View File
@@ -895,4 +895,4 @@ For additional help:
---
*This guide covers the complete navigation and station management functionality. For preset management, see [PRESET-MANAGEMENT.md](PRESET-MANAGEMENT.md).*
*This guide covers the complete navigation and station management functionality. For preset management, see [PRESET-MANAGEMENT.md](reference/PRESET-MANAGEMENT.md).*
+5 -5
View File
@@ -332,14 +332,14 @@ soundtouch-cli --host 192.168.1.100 info
## Next Steps
- 📖 [Complete CLI Reference](CLI-REFERENCE.md)
- 🔧 [Full Implementation Guide](preset-store.md)
- 📡 [WebSocket Events Documentation](websocket-events.md)
- 📖 [Complete CLI Reference](guides/CLI-REFERENCE.md)
- 🔧 [Full Implementation Guide](reference/PRESET-MANAGEMENT.md)
- 📡 [WebSocket Events Documentation](reference/WEBSOCKET-EVENTS.md)
- 💻 [Preset Management Example](../examples/preset-management/)
- 📚 [API Endpoints Overview](API-Endpoints-Overview.md)
- 📚 [API Endpoints Overview](reference/API-ENDPOINTS.md)
## Need Help?
- 🐛 **Bug Reports**: [Create an issue](https://github.com/gesellix/bose-soundtouch/issues)
- 💡 **Feature Requests**: [Start a discussion](https://github.com/gesellix/bose-soundtouch/discussions)
- ❓ **Questions**: [Browse discussions](https://github.com/gesellix/bose-soundtouch/discussions)
- ❓ **Questions**: [Browse discussions](https://github.com/gesellix/bose-soundtouch/discussions)
+89
View File
@@ -0,0 +1,89 @@
# Bose SoundTouch Toolkit Documentation
Welcome to the documentation for the Bose SoundTouch Toolkit. This comprehensive toolkit helps you keep your Bose SoundTouch speakers functional even after the Bose Cloud shutdown in May 2026, with enhanced local management and monitoring capabilities.
## 🚀 Start Here
### For New Users
- **[Complete Migration Guide](guides/MIGRATION-GUIDE.md)** - Step-by-step guide from Bose Cloud to local control
- **[Getting Started](guides/GETTING-STARTED.md)** - Quick introduction to the toolkit
### For Existing Users
- **[Cloud Shutdown Survival Guide](guides/SURVIVAL-GUIDE.md)** - Prepare for the May 2026 shutdown
- **[SoundTouch Service Guide](guides/SOUNDTOUCH-SERVICE.md)** - Advanced service configuration
## 📋 Essential Documentation
The documentation is organized into three main categories:
### 1. **User Guides** - For everyday users migrating and managing devices
### 2. **Technical Reference** - For developers and advanced configuration
### 3. **Concept Documentation** - For contributors and system architects
## 🗂 Documentation Structure
## 🗂 User Guides
### Migration & Setup
- **[Complete Migration Guide](guides/MIGRATION-GUIDE.md)** - 📖 **Main guide** for migrating from Bose Cloud
- [Cloud Shutdown Survival Guide](guides/SURVIVAL-GUIDE.md) - Prepare for service shutdown
- [Migration & Safety Guide](guides/MIGRATION-SAFETY.md) - Advanced migration strategies
- [Initial Device Setup](guides/DEVICE-INITIAL-SETUP.md) - First-time device configuration
- [Raspberry Pi Setup](guides/RASPBERRY-PI.md) - Installing on Raspberry Pi
### Daily Management
- [SoundTouch Service Guide](guides/SOUNDTOUCH-SERVICE.md) - Service operation and maintenance
- [Troubleshooting](guides/TROUBLESHOOTING.md) - Common issues and solutions
- [HTTPS Setup](guides/HTTPS-SETUP.md) - Secure connections
- [Deployment Guide](guides/DEPLOYMENT.md) - Production deployments
### Advanced Features
- [MAC Address Mapping](guides/MAC-ADDRESS-MAPPING.md) - Device identification
- [CLI Reference](guides/CLI-REFERENCE.md) - Command-line tools
- [IoT Implementation Guide](guides/IOT-IMPLEMENTATION-GUIDE.md) - IoT integrations
- [MQTT Integration Design](guides/MQTT-INTEGRATION-DESIGN.md) - MQTT setup
## 📚 Technical Reference
### API Documentation
- [API Endpoints](reference/API-ENDPOINTS.md) - REST API reference
- [WebSocket Events](reference/WEBSOCKET-EVENTS.md) - Real-time events
- [Zone Management](reference/ZONE-MANAGEMENT.md) - Multi-room control
- [Preset Management](reference/PRESET-MANAGEMENT.md) - Preset operations
### Analysis & Research
- [Upstream URLs](analysis/UPSTREAM-URLS.md) - Bose service endpoints
- [Device Redirect Methods](analysis/DEVICE-REDIRECT-METHODS.md) - Migration techniques
- [IoT Configuration Analysis](analysis/IOT-CONFIGURATION-ANALYSIS.md) - Device configurations
- [IoT Config Summary](analysis/IOT-CONFIG-SUMMARY.md) - Configuration summaries
### Device Lifecycle & Network Independence
- **[Device Lifecycle and /power_on Enhancement](device-lifecycle-and-power-on-enhancement.md)** - Complete analysis of device registration and network independence improvements
- [/power_on Implementation Guide](power-on-implementation-guide.md) - Technical implementation details for enhanced device management
## 🏗 Concept Documentation
### Enhanced Service Architecture
- **[Concept Overview](concepts/README.md)** - High-level architecture vision
- [Upstream Service Simulation](concepts/upstream-service-simulation.md) - Complete concept design
- [Implementation Plan](concepts/implementation-plan.md) - Development roadmap
- [Technical Specification](concepts/technical-specification.md) - Detailed specifications
### Development Planning
- [Implementation Roadmap](concepts/implementation-roadmap.md) - Project phases and milestones
## 💡 Quick Reference
### Common Tasks
- **Migrate first device**: Follow [Migration Guide Step 5](guides/MIGRATION-GUIDE.md#step-5-migrate-individual-devices)
- **Check device health**: Dashboard → Devices → [Device Name] → Health Status
- **Backup configuration**: Dashboard → Settings → Backup → Create Backup
- **Add new device**: Dashboard → Devices → Discover Devices → Register
### Getting Help
- **Issues & Bugs**: [GitHub Issues](https://github.com/gesellix/Bose-SoundTouch/issues)
- **Questions & Discussion**: [GitHub Discussions](https://github.com/gesellix/Bose-SoundTouch/discussions)
- **Documentation**: Check troubleshooting guides first
- **Community**: Share experiences and help others
For a complete list of all documents, see the [Summary](SUMMARY.md).
+345
View File
@@ -0,0 +1,345 @@
# Request Recording Concept
## Problem Statement
The current request recording system has fundamental issues when dealing with request cloning, body consumption, and multiple response scenarios. Specifically:
1. **Body Consumption**: HTTP request bodies can only be read once, leading to missing bodies in recordings
2. **Request Cloning**: A single original request may be cloned multiple times for different purposes (local handling, mirroring, recording)
3. **Multiple Responses**: The same logical request may generate different responses (local vs upstream mirror)
4. **Data Integrity**: No guarantee that recorded requests are identical across different execution paths
## Current Issues (Examples)
### Issue 1: Missing Request Bodies in Mirror Recordings
**Local Recording** (complete):
```http
### POST /v1/scmudc/A81B6A536A98
POST /v1/scmudc/A81B6A536A98
Host: events.api.bosecm.com
Content-Type: text/json; charset=utf-8
Content-Length: 587
Authorization: Bearer jGwEmFWr...
{"envelope":{"monoTime":234906,"payloadProtocolVersion":"3.1","payloadType":"scmudc","protocolVersion":"1.0","time":"2026-02-25T23:03:14.976349+00:00","uniqueId":"A81B6A536A98"},"payload":{"deviceInfo":{"boseID":"3230304","deviceID":"A81B6A536A98","deviceType":"SoundTouch 10","serialNumber":"I6332527703739342000020","softwareVersion":"27.0.6.46330.5043500 epdbuild.trunk.hepdswbld04.2022-08-04T11:20:29","systemSerialNumber":"069231P63364828AE"},"events":[{"data":{"play-state":"PAUSE_STATE"},"monoTime":234904,"time":"2026-02-25T23:03:14.973466+00:00","type":"play-state-changed"}]}}
{% raw %}
> {%
// Response: 200 OK
%}
{% endraw %}
```
**Mirror Recording** (missing body):
```http
### POST /v1/scmudc/A81B6A536A98
POST /v1/scmudc/A81B6A536A98
Host: events.api.bosecm.com
Content-Type: text/json; charset=utf-8
Content-Length: 587
Authorization: Bearer jGwEmFWr...
{% raw %}
> {%
// Response: 200 OK
// Headers:
// X-Proxy-Origin: upstream-mirror
%}
{% endraw %}
```
### Issue 2: Request Flow Complexity
Current middleware execution order:
```
1. MirrorMiddleware - Buffers body, creates clones
2. RecordMiddleware - Also buffers body
3. Application Handler - Processes request
4. Mirror Execution - Async/sync mirror to upstream
5. Recording - Multiple recording points
```
Problems:
- Multiple body reads across middleware chain
- Inconsistent request state between clones
- Race conditions in async scenarios
- No guarantee of request equivalence
## Proposed Solution: Context-Bound Request Snapshots
### Core Concept
Create **immutable request snapshots** early in the request lifecycle and propagate them through the **Request Context**. This ensures all downstream consumers (Mirroring, Recording, Parity Check) use identical data without re-reading the request body.
### Architecture (Context-Only)
```
┌─────────────────┐
│ Original Request│
└─────────┬───────┘
┌─────────────────┐ ┌──────────────────┐
│ Snapshot Creator│───▶│ Request Context │
│ (Middleware) │ │ (Pointer-based) │
└─────────┬───────┘ └──────────────────┘
│ │
▼ │ (Safe for async)
┌─────────────────┐ │
│ Middleware │◀─────────────┘
│ Chain │
└─────────┬───────┘
┌───▼────┐ ┌─────────┐ ┌──────────────┐
│ Local │ │ Mirror │ │ Recording │
│Handler │ │Execution│ │ System │
└────────┘ └─────────┘ └──────────────┘
```
### Request Snapshot Structure
```go
type RequestSnapshot struct {
Method string
URL *url.URL
Headers http.Header
Body []byte
Host string
Timestamp time.Time
}
// Typed key for context safety
type contextKey struct{ name string }
var SnapshotKey = &contextKey{"request_snapshot"}
```
### Implementation Strategy
#### Phase 1: Snapshot Middleware
```go
func (s *Server) SnapshotMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 1. Capture body once with size limit (e.g. 2MB)
body, _ := io.ReadAll(io.LimitReader(r.Body, 2*1024*1024))
r.Body.Close()
// 2. Create snapshot
snapshot := &RequestSnapshot{
Method: r.Method,
URL: cloneURL(r.URL),
Headers: r.Header.Clone(),
Body: body,
Host: r.Host,
Timestamp: time.Now(),
}
// 3. Inject pointer into context
ctx := context.WithValue(r.Context(), SnapshotKey, snapshot)
// 4. Restore r.Body for downstream compatibility
r = r.WithContext(ctx)
r.Body = io.NopCloser(bytes.NewReader(snapshot.Body))
next.ServeHTTP(w, r)
})
}
```
#### Phase 2: Downstream Consumption
Consumers (Mirror/Record) retrieve the snapshot directly from context:
```go
snapshot, ok := r.Context().Value(SnapshotKey).(*RequestSnapshot)
if ok {
// Use snapshot.Body directly instead of io.ReadAll(r.Body)
}
```
## Hardware Considerations (Raspberry Pi Zero 2W)
To protect MicroSD health and optimize for limited memory:
1. **No Intermediate Disk Storage**: Snapshots exist only in memory; they are never written to disk until the final `.http` recording is generated.
2. **Memory Management**: Use `sync.Pool` for temporary buffers to reduce GC churn on the single-core/low-memory SoC.
3. **Automatic Cleanup**: Snapshots are naturally garbage collected once the Request Context and all child goroutines (detached mirrors/recordings) finish.
4. **Body Capping**: Strict limits on snapshot size prevent OOM (Out-of-Memory) conditions.
#### Phase 2: Response Capture System
```go
type ResponseRecorder struct {
http.ResponseWriter
snapshot *ResponseSnapshot
snapshotID string
source string
startTime time.Time
}
func (r *ResponseRecorder) WriteHeader(statusCode int) {
r.snapshot.StatusCode = statusCode
r.snapshot.Headers = r.Header().Clone()
r.ResponseWriter.WriteHeader(statusCode)
}
func (r *ResponseRecorder) Write(data []byte) (int, error) {
r.snapshot.Body = append(r.snapshot.Body, data...)
return r.ResponseWriter.Write(data)
}
func (r *ResponseRecorder) finalize() {
r.snapshot.Duration = time.Since(r.startTime)
r.snapshot.Timestamp = time.Now()
}
```
#### Phase 3: Recording System Integration
```go
type RecordingManager struct {
storage SnapshotStorage
recorder *Recorder
patterns []string
}
func (rm *RecordingManager) RecordInteraction(snapshotID string, response *ResponseSnapshot) {
// Retrieve immutable request snapshot
request, exists := rm.storage.Get(snapshotID)
if !exists {
log.Printf("Request snapshot not found: %s", snapshotID)
return
}
// Record with guaranteed data integrity
rm.recorder.RecordInteraction(request, response)
}
func (r *Recorder) RecordInteraction(req *RequestSnapshot, res *ResponseSnapshot) error {
// Generate .http file with complete data
var buf bytes.Buffer
// Write request
fmt.Fprintf(&buf, "### %s %s\n", req.Method, req.URL.String())
fmt.Fprintf(&buf, "%s %s\n", req.Method, req.URL.String())
fmt.Fprintf(&buf, "Host: %s\n", req.Host)
for k, vv := range req.Headers {
for _, v := range vv {
fmt.Fprintf(&buf, "%s: %s\n", k, v)
}
}
buf.WriteString("\n")
buf.Write(req.Body)
buf.WriteString("\n\n")
// Write response
{% raw %}
buf.WriteString("> {% \n")
{% endraw %}
fmt.Fprintf(&buf, " // Response: %d %s\n", res.StatusCode, http.StatusText(res.StatusCode))
buf.WriteString(" // Headers:\n")
for k, vv := range res.Headers {
for _, v := range vv {
fmt.Fprintf(&buf, " // %s: %s\n", k, v)
}
}
{% raw %}
buf.WriteString("%}\n\n")
{% endraw %}
if len(res.Body) > 0 {
buf.WriteString("/*\n")
buf.Write(res.Body)
buf.WriteString("\n*/\n")
} else {
buf.WriteString("// [Binary response body: 0 bytes]\n")
}
// Write to file
return r.writeToFile(buf.Bytes(), req, res)
}
```
## Migration Strategy
### Phase 1: Introduce Snapshot System
- Add SnapshotMiddleware as first middleware
- Maintain existing recording system for compatibility
- Gradual migration of recording points
### Phase 2: Update Mirror System
- Modify MirrorMiddleware to use snapshots
- Ensure mirror requests use snapshot data
- Test parity between old and new systems
### Phase 3: Consolidate Recording
- Replace existing recording middleware
- Unified recording system using context-bound snapshots
- Remove duplicate body reading code
### Phase 4: Cleanup
- Remove legacy recording code
- Optimize memory usage with sync.Pool
- Performance validation on target hardware (Pi Zero)
## Benefits
1. **Zero Extra Disk IO**: Protecs MicroSD by avoiding snapshot disk persistence
2. **Memory Efficiency**: Natural lifecycle tied to Request Context
3. **Data Integrity**: Request data is captured once and remains immutable
4. **Consistency**: All consumers use identical request data
5. **Traceability**: Clear lineage from original request to all recordings
6. **Performance**: Reduces duplicate body reads and re-cloning
## Implementation Considerations
### Memory Management
- Use `sync.Pool` for byte buffers
- Strict size limits on captured bodies
- Rely on GC for snapshot cleanup
### Performance Impact
- Single body read vs multiple reads (net positive)
- Memory overhead for snapshot storage (manageable)
- Context propagation overhead (minimal)
### Backward Compatibility
- Maintain existing .http file format
- Preserve existing API contracts
- Gradual migration path
## Testing Strategy
### Unit Tests
- Snapshot creation and immutability
- Response recording accuracy
- Memory cleanup verification
### Integration Tests
- End-to-end request/response recording
- Mirror functionality with snapshots
- Parity validation between old/new systems
### Performance Tests
- Memory usage comparison
- Throughput impact analysis
- Large request body handling
## Future Enhancements
1. **Compression**: Compress stored snapshots for memory efficiency
2. **Streaming**: Support for streaming request/response bodies
3. **Filtering**: Selective snapshot creation based on patterns
4. **Analytics**: Request/response analysis and metrics
5. **Export**: Snapshot export for debugging and analysis
## Conclusion
This snapshot-based approach provides a robust foundation for reliable request recording while solving the current issues with body consumption and data inconsistency. The phased implementation ensures minimal disruption while delivering immediate benefits.
+192
View File
@@ -0,0 +1,192 @@
# SCMUDC Enrichment Implementation Summary
## Overview
This document summarizes the implementation of SCMUDC (Sound Control Management Usage Data Collection) event enrichment in the AfterTouch toolkit. The enhancement provides human-readable analysis of device telemetry data to improve usability and debugging capabilities.
## Problem Solved
Previously, SCMUDC telemetry events were stored as raw JSON with Base64-encoded XML content, making them difficult to analyze. Users had to manually decode content to understand what device interactions were being recorded.
## Solution Implemented
### 1. Backend Enrichment (`pkg/service/proxy/`)
#### New File: `scmudc.go`
- **SCMUDCRequest/SCMUDCEvent Structs**: Parse incoming telemetry JSON
- **EnrichedSCMUDCEvent Struct**: Human-readable analysis with decoded content
- **DecodedContent Struct**: Parsed XML metadata (track names, artwork URLs, etc.)
- **enrichSCMUDCRequest()**: Main enrichment function that:
- Identifies event origin (app, hardware, or internal system)
- Decodes Base64 XML content for device events
- Creates human-readable summaries
- **Helper Functions**: Button formatting, content summarization, origin descriptions
#### Enhanced File: `recorder.go`
- **Updated save() method**: Extracts SCMUDC data during recording
- **New writeRequestWithEnrichment()**: Adds enriched comments to .http files
- **New writeResponseWithEnrichment()**: Includes SCMUDC analysis in response section
- **Updated Interaction struct**: Added `SCMUDCData` field for API responses
- **New extractSCMUDCFromFile()**: Parses enrichment data from existing .http files
- **Enhanced parseInteractionFile()**: Populates SCMUDC data when listing interactions
### 2. Frontend Enhancement
#### Updated HTML (`pkg/service/handlers/web/index.html`)
- **New Column**: Added "Event Details" to interactions table
- **Table Structure**: Updated to accommodate SCMUDC enrichment display
#### Enhanced JavaScript (`pkg/service/handlers/web/js/script.js`)
- **Updated fetchInteractions()**: Displays enriched SCMUDC data with icons
- **New Helper Functions**:
- `getOriginIcon()`: Maps origins to emojis (📱 App, 🎛️ Hardware, 🔄 Internal)
- `getActionIcon()`: Maps actions to emojis (▶️ Play, ⏸️ Pause, etc.)
- `showSCMUDCDetails()`: Detailed popover for complex events
- `displaySCMUDCPopover()`: Modal dialog with full decoded content
- **Truncation Logic**: Long content shows "(...)" with click-to-expand
## Event Origin Clarification
Based on analysis of recorded data:
| Origin | Source | Description | Example Events |
|--------|--------|-------------|----------------|
| `gabbo` | **SoundTouch App** | Mobile/desktop app UI interactions | Play, Pause, Power via app |
| `console` | **Device Hardware** | Physical buttons on speaker | Preset buttons, hardware power |
| `device` | **Internal System** | Automatic device responses | Content playback, system actions |
## Enhanced .http File Format
### Before (Raw)
```http
### POST /v1/scmudc/A81B6A536A98
POST /v1/scmudc/A81B6A536A98
Host: events.api.bosecm.com
...
{"envelope":...,"payload":{"events":[{"data":{"contentItem":"PD94bWw..."}}]}}
```
### After (Enriched)
```http
### POST /v1/scmudc/A81B6A536A98
// Origin: Internal System (device)
// Action: play-item
// Command: Billie Eilish - bad guy (instrumental version)
// Summary: Device: Spotify: Billie Eilish - bad guy (instrumental version)
//
// Decoded Content:
// - Source: SPOTIFY
// - Item: Billie Eilish - bad guy (instrumental version)
// - Account: gesellix
// - Artwork: https://i.scdn.co/image/ab67616d0000b273...
//
// Full XML Content:
// <?xml version="1.0" encoding="UTF-8"?>
// <ContentItem source="SPOTIFY" type="tracklisturl" ...>
// <itemName>Billie Eilish - bad guy (instrumental version)</itemName>
// <containerArt>https://i.scdn.co/image/ab67616d0000b273...</containerArt>
// </ContentItem>
POST /v1/scmudc/A81B6A536A98
...
{% raw %}
> {%
// Response: 200 OK
// SCMUDC Event Analysis:
// - Origin: Internal System (device)
// - Action: play-item
// - Summary: Device: Spotify: Billie Eilish - bad guy (instrumental version)
// - Content: Billie Eilish - bad guy (instrumental version)
// - Account: gesellix
%}
{% endraw %}
```
## Web UI Enhancement
### Interactions Table
- **New Column**: "Event Details" shows enriched summaries
- **Visual Icons**: Origin and action type indicators
- **Truncation**: Long content abbreviated with "(...)" expansion
- **Backward Compatibility**: Works with existing recordings
### Event Details Display
```
📱 ▶️ Play Button (Simple app action)
🔄 🎵 Billie Eilish - bad guy... (...) (Complex device event with details)
🎛️ ⭐ Preset 5 (Hardware preset button)
```
### Detailed Popover
For complex events, clicking "(...)" shows:
- **Origin Description**: "SoundTouch App" instead of "gabbo"
- **Full Content Information**: Track names, artwork URLs, account details
- **Complete XML**: Formatted and readable content item data
## Implementation Benefits
### For Users
- **Immediate Recognition**: See what actions were performed without decoding
- **Better Debugging**: Quick identification of app vs. hardware vs. system events
- **Rich Context**: Track names, accounts, and content sources visible at a glance
### For Developers
- **Structured Data**: Consistent parsing and enrichment pipeline
- **Extensible**: Easy to add new event types and origins
- **Backward Compatible**: Existing recordings work without re-processing
### For Analysis
- **Pattern Recognition**: Quickly identify user behavior patterns
- **Service Integration**: See which music services are being used
- **Device Usage**: Understand app vs. hardware control preferences
## File Structure
```
pkg/service/proxy/
├── scmudc.go # New: SCMUDC enrichment logic
├── recorder.go # Enhanced: Enrichment integration
pkg/service/handlers/web/
├── index.html # Enhanced: New table column
├── js/script.js # Enhanced: SCMUDC display logic
docs/
├── scmudc-events-analysis.md # New: Analysis documentation
├── SCMUDC-ENRICHMENT-IMPLEMENTATION.md # This file
```
## Technical Decisions
### Base64 Decoding Strategy
- **When**: During recording (not on-demand) for performance
- **Fallback**: Parse from .http files if enrichment missing
- **Storage**: Both enriched comments and structured data in API responses
### Icon Selection
- **Emoji Usage**: Universal, colorful, intuitive recognition
- **Semantic Mapping**: Icons match function (📱 for app, 🎛️ for hardware)
- **Fallback**: Generic icons (❓, 🔘) for unknown types
### Backward Compatibility
- **Graceful Degradation**: Missing enrichment data doesn't break UI
- **File Parsing**: Extract enrichment from existing .http files
- **API Enhancement**: New fields optional in Interaction struct
## Future Enhancement Opportunities
1. **Event Correlation**: Link device events to user actions
2. **Statistics Dashboard**: Origin-based usage analytics
3. **Content Recommendations**: Track listening patterns
4. **Device Health**: Monitor interaction frequency and patterns
5. **Export Features**: CSV/JSON export of enriched event data
## Testing Considerations
- **Edge Cases**: Malformed Base64, missing XML elements
- **Performance**: Large numbers of SCMUDC events
- **Browser Compatibility**: Emoji display across different browsers
- **Data Validation**: Ensure enrichment doesn't introduce errors
This implementation significantly improves the usability of SCMUDC telemetry data while maintaining full backward compatibility and raw data access for advanced users.
+2 -2
View File
@@ -23,7 +23,7 @@ This document summarizes the implementation of the `/serviceAvailability` endpoi
### Modified Files
1. **`pkg/client/client.go`** - Added `GetServiceAvailability()` method
2. **`docs/API-Endpoints-Overview.md`** - Updated implementation status
2. **`docs/reference/API-ENDPOINTS.md`** - Updated implementation status
3. **`docs/UNIMPLEMENTED-ENDPOINTS.md`** - Marked as implemented
## API Interface
@@ -263,4 +263,4 @@ BenchmarkGetServiceAvailability-8 1000 1.2ms/op
**Performance benchmarks established**
**Error handling verified**
The ServiceAvailability implementation is production-ready and provides a solid foundation for building user-friendly SoundTouch applications with better service discovery and user feedback capabilities.
The ServiceAvailability implementation is production-ready and provides a solid foundation for building user-friendly SoundTouch applications with better service discovery and user feedback capabilities.
+190
View File
@@ -0,0 +1,190 @@
# 🎉 Introducing SoundTouch Service: Local Cloud Service Emulation
**Date**: January 2024
**Version**: v2.0.0+
**Status**: Production Ready
## What's New?
We're excited to announce the addition of `soundtouch-service`, a comprehensive local server that emulates Bose's cloud services for SoundTouch devices. This major addition provides offline operation capabilities and advanced device management features.
## 🌟 Key Features
### 🏠 Complete Service Emulation
- **BMX Services**: Full Bose Media eXchange implementation for TuneIn, podcasts, and media streaming
- **Marge Services**: Account and device management, preset synchronization, recent items tracking
- **Offline Operation**: Continue using your devices without internet connectivity to Bose servers
### 🔧 Device Migration
- **Seamless Migration**: One-click migration from Bose cloud to local services
- **Configuration Backup**: Automatic backup of existing device settings
- **Rollback Support**: Easy restoration to original Bose cloud configuration
- **Migration Preview**: Analyze what will change before applying updates
### 📊 Advanced Debugging
- **Traffic Proxying**: Intercept and log all device communications
- **Real-time Monitoring**: Live device event streaming and status tracking
- **Analytics Dashboard**: Usage statistics and error reporting
- **Debug Tools**: Comprehensive troubleshooting utilities
### 🌐 Web Management Interface
- **Device Dashboard**: Visual overview of all discovered devices
- **Migration Wizard**: Step-by-step guided device configuration
- **Live Monitoring**: Real-time device status and event streaming
- **Configuration Viewer**: Inspect and modify device settings
## 🚨 Why This Matters
### Bose Cloud Service Discontinuation
Bose has announced that [SoundTouch cloud support will end on May 6, 2026](https://www.bose.com/soundtouch-end-of-life). This service provides a complete local alternative, ensuring your devices continue to work with full functionality beyond the official support timeline.
### Enhanced Privacy & Control
- **Local Processing**: All data stays on your network
- **No External Dependencies**: Operate completely offline
- **Custom Integrations**: Build your own automation and controls
- **Traffic Visibility**: See exactly what your devices are doing
## 🛠️ Installation & Quick Start
### Install
```bash
go install github.com/gesellix/bose-soundtouch/cmd/soundtouch-service@latest
```
### Run
```bash
soundtouch-service
```
### Access Web UI
Open `http://localhost:8000` in your browser and start managing your devices!
## 📖 Implementation Credits
This service implementation builds upon excellent community work:
### 🍾 SoundCork Foundation
Our implementation is heavily inspired by and based on [SoundCork](https://github.com/deborahgu/soundcork) by Deborah Gu and contributors. SoundCork pioneered the approach of intercepting Bose's cloud services and provided the architectural foundation for offline SoundTouch operation.
**Key contributions from SoundCork:**
- Service emulation architecture
- BMX/Marge endpoint discovery
- Device migration strategies
- Python implementation reference
### 🎵 ÜberBöse API Insights
[ÜberBöse API](https://github.com/julius-d/ueberboese-api) by Julius D. provided valuable insights into advanced SoundTouch API endpoints, helping make our implementation more complete and robust.
### 🏠 SoundTouch Plus Documentation
The [SoundTouch Plus Wiki](https://github.com/thlucas1/homeassistantcomponent_soundtouchplus/wiki/SoundTouch-WebServices-API) provided comprehensive API documentation that enabled many of the advanced features.
## 🔄 What's Different in Our Go Implementation
While inspired by SoundCork's Python implementation, our Go service offers:
### Performance & Efficiency
- **Native Compilation**: Single binary deployment with no runtime dependencies
- **Low Resource Usage**: ~50MB memory footprint vs Python's higher overhead
- **Concurrent Processing**: Go's goroutines enable efficient concurrent device handling
- **Fast Startup**: Sub-second service startup time
### Enhanced Features
- **Web Management UI**: Built-in browser-based interface (SoundCork is API-only)
- **Real-time Event Streaming**: WebSocket-based live device monitoring
- **Advanced Migration Tools**: Migration preview and rollback capabilities
- **Comprehensive Logging**: Structured logging with multiple output formats
### Production Readiness
- **Zero Dependencies**: Single binary with embedded web UI
- **Cross-Platform**: Windows, macOS, Linux support out of the box
- **Docker Ready**: Containerization support (planned)
- **Monitoring Integration**: Health checks and metrics endpoints
### Developer Experience
- **Go Ecosystem**: Integrates with existing Go applications and infrastructure
- **Type Safety**: Compile-time checks and robust error handling
- **Documentation**: Comprehensive API documentation and examples
- **Testing**: Extensive test coverage with real device validation
## 🎯 Use Cases
### Home Automation Enthusiasts
```bash
# Migrate all devices and integrate with Home Assistant
soundtouch-service
# Configure HA to use local service endpoints
```
### Developers & Integrators
```go
// Build custom applications on top of local services
client := &http.Client{}
resp, _ := client.Get("http://localhost:8000/devices")
```
### Privacy-Conscious Users
```bash
# Run completely offline with full device functionality
soundtouch-service --bind 127.0.0.1 # localhost only
```
### Network Administrators
```bash
# Monitor and log all device traffic
LOG_PROXY_BODY=true soundtouch-service
```
## 🚀 Future Plans
- **Docker Images**: Official container images for easy deployment
- **Cluster Support**: Multi-instance deployment for high availability
- **Advanced Analytics**: Machine learning-powered usage insights
- **Extended Protocol Support**: Additional Bose protocol implementations
- **Mobile App**: Companion mobile application for device management
## 📚 Documentation
- **[Complete Service Guide](guides/SOUNDTOUCH-SERVICE.md)**: Comprehensive setup and configuration
- **[API Reference](guides/SOUNDTOUCH-SERVICE.md#api-reference)**: Full endpoint documentation
- **[Migration Guide](guides/SOUNDTOUCH-SERVICE.md#device-migration)**: Step-by-step device migration
- **[Troubleshooting](guides/SOUNDTOUCH-SERVICE.md#troubleshooting)**: Common issues and solutions
## 🤝 Contributing
We welcome contributions to improve the service! Areas where help is especially appreciated:
- **Protocol Research**: Discovering new Bose service endpoints
- **Testing**: Validation with different device models and firmware versions
- **Documentation**: Usage examples and troubleshooting guides
- **Features**: Additional service implementations and integrations
## 🙏 Community Thanks
This implementation wouldn't have been possible without the groundbreaking work of the SoundTouch community:
- **SoundCork Team**: For pioneering service interception and providing the implementation blueprint
- **ÜberBöse Project**: For advanced API research and endpoint discovery
- **SoundTouch Plus**: For comprehensive API documentation and real-world usage patterns
- **Community Contributors**: For testing, feedback, and continued development
The collaborative spirit of reverse engineering and documentation in the SoundTouch community has been invaluable. We're proud to contribute back to this ecosystem and help ensure SoundTouch devices remain useful beyond Bose's official support timeline.
## 🔗 Links
- **[Main Repository](https://github.com/gesellix/bose-soundtouch)**
- **[Service Documentation](guides/SOUNDTOUCH-SERVICE.md)**
- **[CLI Documentation](guides/CLI-REFERENCE.md)**
- **[Getting Started Guide](guides/GETTING-STARTED.md)**
- **[SoundCork Project](https://github.com/deborahgu/soundcork)**
- **[ÜberBöse API](https://github.com/julius-d/ueberboese-api)**
---
**Ready to take control of your SoundTouch devices?** Get started with `soundtouch-service` today!
```bash
go install github.com/gesellix/bose-soundtouch/cmd/soundtouch-service@latest
soundtouch-service
```
Open `http://localhost:8000` and start your journey to local SoundTouch control! 🎵
+83
View File
@@ -0,0 +1,83 @@
# Table of Contents
* [Introduction](README.md)
## User Guides
* [Cloud Shutdown Survival Guide](guides/SURVIVAL-GUIDE.md)
* [Migration & Safety Guide](guides/MIGRATION-SAFETY.md)
* [CLI Reference](guides/CLI-REFERENCE.md)
* [Getting Started](guides/GETTING-STARTED.md)
* [SoundTouch Service](guides/SOUNDTOUCH-SERVICE.md)
* [Initial Device Setup](guides/DEVICE-INITIAL-SETUP.md)
* [MAC Address Mapping](guides/MAC-ADDRESS-MAPPING.md)
* [HTTPS Setup](guides/HTTPS-SETUP.md)
* [Deployment](guides/DEPLOYMENT.md)
* [Raspberry Pi Guide](guides/RASPBERRY-PI.md)
* [Troubleshooting](guides/TROUBLESHOOTING.md)
* [IoT Implementation Guide](guides/IOT-IMPLEMENTATION-GUIDE.md)
* [Migration Guide](guides/MIGRATION-GUIDE.md)
* [MQTT Integration Design](guides/MQTT-INTEGRATION-DESIGN.md)
* [Useful Links](#useful-links)
### Useful Links
* [Cloud Shutdown Survival Guide](guides/SURVIVAL-GUIDE.md)
* [Raspberry Pi Installer](../scripts/raspberry-pi/README.md)
* [Updating the Service](../scripts/raspberry-pi/README.md#updating-to-a-new-version)
* [CLI Reference](guides/CLI-REFERENCE.md)
## Technical Reference
* [API Cookbook](reference/API-COOKBOOK.md)
* [API Endpoints](reference/API-ENDPOINTS.md)
* [Cloud API Emulation](reference/CLOUD-API.md)
* [System Endpoints](reference/SYSTEM-ENDPOINTS.md)
* [Speaker Endpoint](reference/SPEAKER-ENDPOINT.md)
* [WebSocket Events](reference/WEBSOCKET-EVENTS.md)
* [Discovery](reference/DISCOVERY.md)
* [Zone Management](reference/ZONE-MANAGEMENT.md)
* [Preset Management](reference/PRESET-MANAGEMENT.md)
* [Source Selection](reference/SOURCE-SELECTION.md)
* [Volume Controls](reference/VOLUME-CONTROLS.md)
* [RadioBrowser](reference/radio-browser.md)
* [Bass Controls](reference/BASS-CONTROLS.md)
* [Key Controls](reference/KEY-CONTROLS.md)
* [Feature Mapping](reference/FEATURE-MAPPING.md)
## Concepts
* [Request Recording](REQUEST_RECORDING_CONCEPT.md)
* [Spotify Priming Strategy](concepts/spotify-priming-strategy.md)
* [Spotify OAuth](concepts/spotify-oauth.md)
## Analysis & Research
* [API Coverage Analysis](analysis/API-COVERAGE.md)
* [Supported URLs](analysis/SUPPORTED-URLS.md)
* [Upstream URLs](analysis/UPSTREAM-URLS.md)
* [Anonymization Summary](analysis/ANONYMIZATION-SUMMARY.md)
* [Device Redirect Methods](analysis/DEVICE-REDIRECT-METHODS.md)
* [Stockholm App Analysis](analysis/stockholm-app-analysis.md)
* [Wiki API Comparison](analysis/WIKI-COMPARISON.md)
* [IoT Config Summary](analysis/IOT-CONFIG-SUMMARY.md)
* [IoT Configuration Analysis](analysis/IOT-CONFIGURATION-ANALYSIS.md)
## Appendix (Other Documents)
* [API Navigation Reference](API-NAVIGATION-REFERENCE.md)
* [Claude Instructions](CLAUDE.md)
* [Content Selection Implementation](CONTENT-SELECTION-IMPLEMENTATION.md)
* [Device Customization Setup](DEVICE-CUSTOMIZATION-SETUP.md)
* [Device Logging](DEVICE-LOGGING.md)
* [Feature History](FEATURE_HISTORY.md)
* [Host/Port Parsing](HOST-PORT-PARSING.md)
* [Manual Network Discovery](MANUAL-NETWORK-DISCOVERY.md)
* [Navigation Guide](NAVIGATION-GUIDE.md)
* [Official API Verification](OFFICIAL-API-VERIFICATION.md)
* [Preset Quickstart](PRESET-QUICKSTART.md)
* [Project Patterns](PROJECT-PATTERNS.md)
* [Service Availability Implementation](SERVICE-AVAILABILITY-IMPLEMENTATION.md)
* [SoundTouch Service Announcement](SOUNDTOUCH-SERVICE-ANNOUNCEMENT.md)
* [Undocumented Community Features](UNDOCUMENTED-COMMUNITY-FEATURES.md)
* [Unimplemented Endpoints](UNIMPLEMENTED-ENDPOINTS.md)
* [Preset Store](preset-store.md)
* [SCMUDC Enrichment Implementation](SCMUDC-ENRICHMENT-IMPLEMENTATION.md)
* [Device Lifecycle and Power On Enhancement](device-lifecycle-and-power-on-enhancement.md)
* [Device Lifecycle Summary](device-lifecycle-summary.md)
* [Power On Implementation Guide](power-on-implementation-guide.md)
* [SCMUDC Events Analysis](scmudc-events-analysis.md)
+46
View File
@@ -0,0 +1,46 @@
# Undocumented Community Features & API Discoveries
This document captures advanced API endpoints and device behaviors discovered by the SoundTouch community through reverse engineering projects like **SoundCork** and **ÜberBöse API**. These features are not documented in the official Bose SoundTouch Web API v1.0 but are crucial for full device emulation and offline operation.
## Cloud Emulation (Marge/BMX) Discoveries
While the local `/8090` API is well-documented, the cloud-side service emulation reveals deeper device integration points.
### 1. Stereo Pairing & Cloud-Side Grouping
SoundCork has pioneered the emulation of "Marge" group endpoints, which differ from the local `/getGroup` API. These are primarily used for persistent configurations like **Stereo Pairs** (e.g., two ST-10s).
- **GET** `/marge/streaming/account/{account}/device/{device}/group`
Returns `<group/>` if ungrouped, or full group configuration for stereo pairs.
- **POST** `/marge/streaming/account/{account}/group`
Creates a new group (returns a 7-digit group ID). Used for initial pairing.
- **DELETE** `/marge/streaming/account/{account}/group/{group}`
Dissolves a group configuration.
### 2. Device Analytics & Event Reporting
Devices report real-time telemetry to the cloud. Intercepting these provides a window into device usage without polling.
- **Endpoint**: `POST /v1/scmudc/{deviceId}`
- **Function**: Submits event data including `play-state-changed`, `preset-pressed`, `power-pressed`, `source-state-changed`, and `art-changed` (Metadata updates). This endpoint was first extensively documented in the **ÜberBöse API** specification.
### 3. Power-On Lifecycle
When a SoundTouch device boots or "powers on" (distinct from waking from standby), it contacts specific support endpoints.
- **Endpoint**: `POST /streaming/support/power_on`
- **Behavior**: Reports device serial number, IP address, and diagnostic data.
- **Critical Finding**: SoundTouch devices fetch `TUNEIN` and `LOCAL_INTERNET_RADIO` source availability from the cloud **ONLY at boot time**. If the cloud is unreachable during a hard reboot (power cycle), these sources will disappear from the device's `/sources` list and become unavailable, even if the local API is working. This behavior was analyzed and reported by the **ÜberBöse API** project (Issue #3).
### 4. OAuth & Service Tokens
Integration with music services (Spotify, Pandora, etc.) involves specific token management endpoints.
- **Endpoint**: `POST /oauth/device/{deviceId}/music/musicprovider/{providerId}/token/{tokenType}`
- **Usage**: Used to refresh or validate session tokens for cloud-based music providers.
## Community-Driven Extensions
The community is working on extending SoundTouch functionality beyond its original design.
### 1. Radio-Browser.info Integration
There is an active effort to add `radio-browser.info` as a native `sourceprovider`. This would allow devices to browse a massive directory of thousands of stations without relying on the TuneIn cloud service.
- **Status**: Research phase in SoundCork (Issue #150).
- **Implementation**: Requires adding a new source provider entry in the emulated `/streaming/sourceproviders` response.
### 2. Stockholm Internal App Analysis
Deep analysis of the Stockholm (device firmware) internal web application reveals a set of internal AJAX/XML calls used by the device's own control interface.
- **Internal Domains**: `Marge` (XML-based) and `Gabbo` (App-send based).
- **Reference**: See SoundCork Issue #128 for a comprehensive list of internal JS controllers and their functions.
### 3. ETag Case-Sensitivity Bug
The SoundTouch device firmware has a case-sensitivity bug regarding HTTP `ETag` headers.
- **Discovery**: SoundCork Issue #129.
- **Detail**: The device expects the `ETag` header to be exactly title-cased. If a server returns `etag` (lowercase), the device fails to use it for `If-None-Match` requests, breaking efficient preset synchronization.
- **Solution**: Force title-casing of the header via a reverse proxy like Nginx or mitmproxy.
## References
- [SoundCork GitHub Repo](https://github.com/deborahgu/soundcork)
- [ÜberBöse API Spec](https://github.com/julius-d/ueberboese-api)
- [SoundTouch Plus Wiki](https://github.com/thlucas1/homeassistantcomponent_soundtouchplus/wiki/SoundTouch-WebServices-API)
- [IsItBose Regex Research](https://github.com/deborahgu/soundcork/issues/62#issuecomment-3610563908)
- [SoundTouch Hook Repo](https://github.com/CodeFinder2/bose-soundtouch-hook)
+2 -34
View File
@@ -267,24 +267,7 @@ Rates currently playing media (Pandora only).
### System Information
#### GET /recents 🔥 **CRITICAL**
Returns recently played media content.
**Response Example:**
```xml
<recents>
<recent deviceID="1004567890AA" utcTime="1701202831">
<contentItem source="STORED_MUSIC" location="6_a2874b5d_4f83d999" sourceAccount="d09708a1-5953-44bc-a413-123456789012/0" isPresetable="true">
<itemName>MercyMe, It's Christmas!</itemName>
</contentItem>
</recent>
<recent deviceID="1004567890AA" utcTime="1700232917" id="2487503626">
<contentItem source="LOCAL_MUSIC" type="track" location="track:2590" sourceAccount="3f205110-4a57-4e91-810a-123456789012" isPresetable="true">
<itemName>Baby It's Cold Outside - ANNE MURRAY</itemName>
</contentItem>
</recent>
</recents>
```
#### GET /listMediaServers 🔥 **CRITICAL**
Returns detected UPnP/DLNA media servers.
@@ -323,22 +306,7 @@ Returns source service availability status.
</serviceAvailability>
```
#### POST /introspect 🔥 **CRITICAL**
Retrieves introspect data for specified music service.
**Request Example:**
```xml
<introspect source="SPOTIFY" sourceAccount="SpotifyConnectUserName" />
```
**Response Example:**
```xml
<spotifyAccountIntrospectResponse state="InactiveUnselected" user="SpotifyConnectUserName" isPlaying="false" tokenLastChangedTimeSeconds="1702566495" tokenLastChangedTimeMicroseconds="427884" shuffleMode="OFF" playStatusState="2" currentUri="" receivedPlaybackRequest="false" subscriptionType="">
<cachedPlaybackRequest />
<nowPlaying skipPreviousSupported="false" seekSupported="false" resumeSupported="true" collectData="true" />
<contentItemHistory maxSize="10" />
</spotifyAccountIntrospectResponse>
```
### Power Management
@@ -402,7 +370,7 @@ soundtouch-cli speaker beep
**Go Client Usage:**
```go
// Text-to-Speech
client.PlayTTS("Hello World", "your-app-key", 70)
client.PlayTTS("Hello World", "your-app-key", "EN", 70)
// URL content
client.PlayURL("https://example.com/audio.mp3", "your-app-key", "Service", "Message", "Reason", 60)
@@ -1076,4 +1044,4 @@ The SoundTouch Plus Wiki provides comprehensive documentation for **64 additiona
This documentation provides the complete foundation for implementing all endpoints from the SoundTouch Plus Wiki, enabling this Go library to become the definitive SoundTouch integration solution for everything from basic home automation to professional audio installations.
*All examples and XML structures are verified against real SoundTouch hardware and extensively tested by the SoundTouch Plus community.*
*All examples and XML structures are verified against real SoundTouch hardware and extensively tested by the SoundTouch Plus community.*
+11
View File
@@ -0,0 +1,11 @@
title: Bose SoundTouch Toolkit
description: Documentation for controlling and preserving Bose SoundTouch devices
remote_theme: pages-themes/minimal@v0.2.0
plugins:
- jekyll-remote-theme
- jekyll-relative-links
relative_links:
enabled: true
collections: true
include:
- SUMMARY.md
+198
View File
@@ -0,0 +1,198 @@
# Device Redirect Methods & Custom Service Setup
To enable offline operation or use custom services like **SoundCork** or **ÜberBöse API**, SoundTouch devices must be redirected from Bose's official cloud endpoints to a local or custom server. This document outlines the three known methods to achieve this, gathered from community reverse-engineering efforts in the **SoundCork** and **ÜberBöse API** projects.
## Overview of Redirection Targets
SoundTouch devices primarily communicate with the following domains:
- `streaming.bose.com`: Marge (Account and streaming services)
- `updates.bose.com`: Software updates
- `stats.bose.com`: Telemetry and analytics
- `bmx.bose.com`: Bose Media eXchange registry
- `events.api.bosecm.com`: Stockholm app analytics
- `bose-prod.apigee.net`: Apigee gateway (used by some services)
- `worldwide.bose.com`: Software update metadata and secondary services
---
## Method 1: XML Configuration Modification (Recommended)
The most robust and granular method involves modifying the device's private configuration file. This is the primary method used by **SoundCork**'s migration logic to redirect devices to a local service instance.
### Technical Details
- **File Path**: `/opt/Bose/etc/SoundTouchSdkPrivateCfg.xml`
- **Mechanism**: The device firmware reads this XML file at boot to determine service URLs.
- **Fields to Modify**:
- `<margeServerUrl>`: Redirects account/streaming calls.
- `<statsServerUrl>`: Redirects telemetry.
- `<swUpdateUrl>`: Redirects update checks.
- `<bmxRegistryUrl>`: Redirects service discovery.
### Implementation
Requires SSH access to the device.
```xml
<SoundTouchSdkPrivateCfg>
<margeServerUrl>http://192.168.1.10:8000/marge</margeServerUrl>
<statsServerUrl>http://192.168.1.10:8000</statsServerUrl>
<swUpdateUrl>http://192.168.1.10:8000/updates/soundtouch</swUpdateUrl>
<bmxRegistryUrl>http://192.168.1.10:8000/bmx/registry/v1/services</bmxRegistryUrl>
</SoundTouchSdkPrivateCfg>
```
### Pros & Cons
| Pros | Cons |
| :--- | :--- |
| **Granular Control**: Redirect specific services while leaving others (e.g., updates) intact. | **Requires SSH**: Must have root/SSH access to the device. |
| **Persistent**: Survives software updates (usually). | **Syntax Sensitive**: Errors in XML can cause boot issues or service failures. |
| **Native**: Uses the device's built-in configuration mechanism. | |
---
## Method 2: `/etc/hosts` DNS Override
This method uses the standard Linux hosts file to redirect traffic at the network level within the device. It is often used as a quick alternative in the **ÜberBöse API** community for global redirection.
### Technical Details
- **File Path**: `/etc/hosts`
- **Mechanism**: Overrides DNS resolution for Bose domains to point to a local IP.
- **Resolution Order**: SoundTouch devices use the standard Linux Name Service Switch (`/etc/nsswitch.conf`). The default configuration (`hosts: files dns`) ensures that `/etc/hosts` is consulted *before* any external DNS lookups. This makes the redirection highly reliable for all system processes, including `curl`, `BoseApp`, and `IoT`.
### Implementation
Requires SSH access. Add entries for the target domains:
```text
192.168.1.10 streaming.bose.com
192.168.1.10 updates.bose.com
192.168.1.10 stats.bose.com
```
### Pros & Cons
| Pros | Cons |
| :--- | :--- |
| **Simple**: Easy to understand and implement. | **Requires SSH**: Must have root access. |
| **Universal**: Affects all processes on the device attempting to reach those domains. | **HTTPS Issues**: Redirecting HTTPS domains to a local IP will cause SSL certificate errors unless the device is patched to skip verification or trust a custom CA. |
| | **Brittle**: Some firmware versions may overwrite `/etc/hosts` on reboot. |
---
## Method 3: Binary Patching
A low-level approach where the actual compiled binaries (e.g., `BoseApp`, `IoT`) are modified to change hardcoded URL patterns. Research into these patterns has been documented in both **SoundCork** (Issue #128) and **ÜberBöse API** research.
### Technical Details
- **Target Binaries**: `/opt/Bose/BoseApp`, `/opt/Bose/IoT`, `/opt/Bose/lib/libBmxAccountHsm.so`
- **Mechanism**:
- **URL Replacement**: Using a hex editor to search for string patterns like `https://streaming.bose.com` and replacing them with a custom URL of the **exact same length**.
- **Regex Neutralization**: Some libraries (like `libBmxAccountHsm.so`) perform a validation check called `IsItBose` using a hardcoded regex. This regex prevents the device from connecting to non-Bose domains even if the URL is changed in the configuration.
#### The `IsItBose` Regex Patch
Research in the **SoundCork** community (Issue #62) identified a specific regex in `libBmxAccountHsm.so` that enforces Bose/Apigee domain usage:
`^https:\/\/bose-[a-zA-Z0-9\.\_\-\$\%]\+\.apigee\.net\/`
By patching this regex to be more "lax", the device can be made to accept any custom domain.
**Example Patch**:
Using `sed` to replace the strict regex with a broad match while preserving the original string length:
```bash
sed "s#\^https:....bose.\+apigee..net..#http[aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa]*#g" \
< libBmxAccountHsm.so.orig > libBmxAccountHsm.so.patched
```
### Implementation
1. Copy the target binary or library from the device to a PC.
2. Use a hex editor or `sed` to locate and patch the URL strings or regex patterns.
3. Copy the patched file back to the device.
4. Restore execution permissions and reboot.
### Pros & Cons
| Pros | Cons |
| :--- | :--- |
| **Bypass Config**: Works even if the firmware ignores XML settings. | **High Risk**: Modifying binaries can lead to permanent bricks or boot loops. |
| **Hardcoded Redirects**: Can catch URLs that aren't exposed in configuration files. | **Length Constraint**: Custom URLs must fit within the space of the original strings. |
| | **Firmware Specific**: Patches must be reapplied after every software update. |
| | **Complexity**: Requires understanding of binary structures and potential checksums. |
---
## Comparison & Usage Strategy
### Summary Table
| Method | Primary Use Case | Ease | Safety | Persistence | Granularity |
| :--- | :--- | :---: | :---: | :---: | :---: |
| **XML Config** | Logical service redirection | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| **`/etc/hosts`** | Quick global DNS override | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ | ⭐⭐ |
| **Binary Patch** | Bypassing hardcoded checks | ⭐ | ⭐ | ⭐ | ⭐⭐⭐ |
---
## Combining Methods: When is one not enough?
A common question is whether these methods can be used in isolation or if they must be combined. The answer depends on your specific firmware version and the target service.
### Scenario A: XML Config Only (The Ideal Case)
If your firmware does not strictly enforce the `IsItBose` check for the specific URLs you are changing, **Method 1 (XML)** is sufficient. This is the cleanest approach and is used by the `soundtouch-service` migration tool.
### Scenario B: XML Config + Binary Patching (The "Locked" Case)
On some newer firmware versions, even if you change the `<margeServerUrl>` in the XML to `http://192.168.1.10`, the internal library (`libBmxAccountHsm.so`) will validate the string against the hardcoded Bose regex.
* **Symptom**: The device ignores the XML setting or fails to connect despite the correct URL being present.
* **Solution**: You **must** apply the **Binary Patch (Method 3)** to neutralize the `IsItBose` check *in addition* to the XML change.
### Scenario C: `/etc/hosts` + Custom CA (The "Clean Deep Redirect")
If you use `/etc/hosts` to point `streaming.bose.com` to a local IP and want to avoid binary patching.
* **Requirement 1**: Your local server must handle HTTPS (port 443).
* **Requirement 2**: You must inject your Root CA into the device's trust store.
* **Automated Tool**: The `soundtouch-service` now supports this via the `/setup/migrate/{deviceIP}?method=hosts` endpoint.
* **CA Download**: You can download the auto-generated Root CA from `http://<your-server>:8000/setup/ca.crt`.
* **Benefit**: Maintains system integrity (no binary changes) and full end-to-end encryption.
### Scenario D: `/etc/hosts` + Binary Patching (The "Legacy Deep Redirect")
If you cannot or do not want to manage certificates, but still use `/etc/hosts` for DNS redirection.
* **Requirement 1**: Your local server must handle HTTPS (port 443).
* **Requirement 2**: Since the certificate will be invalid (mismatched domain/CA), you must patch the binary to **skip SSL verification** (see [Option 2](#option-2-ssl-verification-bypass) below).
* **Risk**: Less secure and higher risk of bricking due to binary modification.
### Scenario E: The Triple-Threat (Total Control)
For developers creating a completely isolated "dark" environment (no internet at all):
1. **XML**: Point all URLs to local services.
2. **Binary Patch**: Neutralize `IsItBose` to allow non-Bose domains/IPs.
3. **`/etc/hosts`**: Redirect hardcoded domains that aren't exposed in the XML (like analytics or NTP) to prevent leakage to the real Bose cloud.
4. **Process Instrumentation**: Use [SoundTouch Hook](https://github.com/CodeFinder2/bose-soundtouch-hook) to monitor and override internal behavior in real-time. This is particularly useful for handling unknown hostnames or deep-hooking into service discovery logic that might bypass standard DNS lookups.
---
## Handling HTTPS & SSL Certificates
When redirecting HTTPS traffic to a custom service, SoundTouch devices will fail the SSL handshake because they do not trust your local server's certificate.
### Option 1: Custom CA Certificate (Recommended)
As suggested by community members, you can configure the device to trust your own Root CA. This allows for secure HTTPS communication without patching binaries.
**Technical Steps**:
1. **Generate a Root CA** and issue a certificate for the target domain (e.g., `streaming.bose.com`).
2. **SSH into the device** and copy your `rootCA.crt` to `/usr/share/ca-certificates/custom/`.
3. **Update the Trust Store**:
- **Method A (Append to Bundle)**: `cat /usr/share/ca-certificates/custom/rootCA.crt >> /etc/pki/tls/certs/ca-bundle.crt`
- **Method B (Symlinks)**: Add the certificate to `/etc/ssl/certs/` and create a hash symlink using `c_rehash` (if available) or manual mapping.
**Pros & Cons**:
| Pros | Cons |
| :--- | :--- |
| **Secure**: Maintains end-to-end encryption. | **Requires SSH**: Must have root access to modify the trust store. |
| **Clean**: No binary patching required for SSL bypass. | **Update Risk**: Firmware updates might overwrite the `ca-bundle.crt`. |
### Option 2: SSL Verification Bypass
If you cannot or do not want to manage certificates, you can patch the binary to skip certificate verification.
**Target**: `libBmxAccountHsm.so` or `BoseApp`
**Mechanism**: Locating the SSL verification function (often in the internal curl-based or openssl-based logic) and forcing it to return "Success" regardless of the certificate status.
---
## Recommendation
1. **Start with Method 1 (XML Modification)**. It is the least invasive and most likely to work across different models.
2. **Verify connectivity**. If the device refuses to connect to your custom endpoint, check logs for "IsItBose" or validation failures.
3. **Apply Method 3 (Binary Patching)** only if Method 1 is being actively blocked by the firmware's validation logic.
4. **Avoid Method 2 (`/etc/hosts`)** unless you are prepared to handle SSL certificate complexities or are performing quick temporary tests.
+189
View File
@@ -0,0 +1,189 @@
# IoT Configuration Quick Reference
## Key Files and Locations
| File/Location | Purpose | Notes |
|-----------------------------------------|------------------------|-----------------------------------------|
| `/mnt/nv/BoseApp-Persistence/1/IoT.xml` | Main IoT configuration | Contains clientID, endpoint, deployment |
| `/opt/Bose/IoT` | IoT service binary | ARM executable, AWS IoT SDK |
| `/mnt/nv/IoTCerts/` | Certificate storage | Device certs and private keys |
| `/etc/init.d/SoundTouch` | System startup script | Creates directory structure |
| `/opt/Bose/etc/Shepherd-noncore.xml` | Service configuration | Defines IoT daemon startup |
## Configuration Parameters
### IoT.xml Structure
```xml
<Configuration
clientID="[UUID]"
iotEndpoint="[AWS_IOT_ENDPOINT]"
deployment="PROD" />
```
### Device-Specific Values
- **ST20**: `clientID="577ecfcc-2db3-4989-92c9-76d7704f9fb3"`
- **ST10**: `clientID="eb1a6d8f-0bb1-4aa7-9113-ea673fcef96e"`
- **Endpoint**: `a2bhvr9c4wn4ya.iot.us-east-1.amazonaws.com` (XML)
- **Backup Endpoint**: `amqmidtcohfms.iot.us-east-1.amazonaws.com` (hardcoded)
## Protocol Stack
```
Application Layer: AWS IoT Device Shadows (JSON)
Presentation Layer: RapidJSON parsing/serialization
Session Layer: MQTT v3.1.1
Transport Layer: TLS v1.2
Network Layer: TCP/IP
```
## Certificate Files
| File | Location | Purpose |
|-----------------------|---------------------|---------------------------|
| `iot-cert.pem.crt` | `/mnt/nv/IoTCerts/` | Device client certificate |
| `iot-private.pem.key` | `/mnt/nv/IoTCerts/` | Device private key |
| `rootCA.crt` | `/var/lib/iot/` | AWS IoT Root CA |
## MQTT Topics
### Shadow Operations
```
$aws/things/{clientID}/shadow/update
$aws/things/{clientID}/shadow/update/accepted
$aws/things/{clientID}/shadow/update/rejected
$aws/things/{clientID}/shadow/delete
```
### JSON Payload Examples
#### Device State Report
```json
{
"state": {
"reported": {
"deviceState": "CONNECTED",
"powerState": "ON",
"zoneState": "...",
"groupState": "..."
}
}
}
```
#### Disconnection Message
```json
{
"state": {
"reported": {
"deviceState": "DISCONNECTED"
}
}
}
```
## Process Information
- **IoT Service PID**: 1837
- **BoseApp PID**: 1846
- **Daemon Manager**: Shepherd
- **Service Type**: Non-core (stopped during updates)
## Registration Flow
1. Device generates X.509 CSR
2. Calls `https://voice.api.bose.io/alexa/certificate`
3. Receives device certificate
4. Stores cert/key in `/mnt/nv/IoTCerts/`
5. Connects to AWS IoT using certificate auth
## Directory Creation (Init Script)
```bash
mkdir -p /mnt/nv/BoseLog /mnt/nv/IoTCerts /mnt/nv/BoseApp-Persistence/1
mkdir -m 700 -p /mnt/nv/BoseApp-Persistence/1/Keys
```
## Error Messages and Debugging
### Common Log Messages
- `"Connection attempt %u to MQTT port at host %s"`
- `"MQTT port not available. Retrying in %u seconds"`
- `"Device connected with MQTT"`
- `"got shadow response: accepted. Payload: %s"`
- `"Failed to register device and get certificate, retrying"`
### Connection States
- `"MQTT port is open"`
- `"Successfully connected to MQTT server"`
- `"Disconnecting from IoT server"`
- `"UpdateShadow called when network is not ready"`
## Integration Points
### AWS Services
- AWS IoT Core (MQTT broker)
- AWS IoT Device Management (certificates)
- AWS IoT Device Shadows (state sync)
### Bose Ecosystem
- Mobile apps (remote control)
- Alexa integration (voice commands)
- Multi-room audio (zone coordination)
- OTA updates (firmware management)
## Quick Troubleshooting
1. **No IoT connectivity**: Check certificate files in `/mnt/nv/IoTCerts/`
2. **Certificate errors**: Verify registration endpoint accessibility
3. **MQTT failures**: Check both primary and backup endpoints
4. **Config issues**: Validate IoT.xml format and clientID uniqueness
5. **Service not starting**: Check Shepherd configuration and process status
## MQTT Monitoring Capabilities
### Direct Access with Device Credentials
```bash
# Subscribe to device shadow events (own device only)
mosquitto_sub -h a2bhvr9c4wn4ya.iot.us-east-1.amazonaws.com \
-p 8883 --cafile /var/lib/iot/rootCA.crt \
--cert /mnt/nv/IoTCerts/iot-cert.pem.crt \
--key /mnt/nv/IoTCerts/iot-private.pem.key \
-t '$aws/things/577ecfcc-2db3-4989-92c9-76d7704f9fb3/shadow/#'
```
### AWS IoT Policy Restrictions
- Device certificates limited to own clientID topics only
- No wildcard subscriptions across devices
- IP/location restrictions may apply
- Certificate revocation for unusual activity
### Alternative Monitoring Methods
```bash
# Network traffic capture (less intrusive)
tcpdump -i eth0 -s0 -w soundtouch_iot.pcap host a2bhvr9c4wn4ya.iot.us-east-1.amazonaws.com
# Monitor connection patterns
tcpdump -i eth0 -n "host a2bhvr9c4wn4ya.iot.us-east-1.amazonaws.com and port 8883"
```
### Expected Message Examples
```json
// Power state change
{"state":{"reported":{"powerState":"ON","deviceState":"CONNECTED"}}}
// Volume adjustment
{"state":{"reported":{"volume":25,"muted":false}}}
// Zone configuration
{"state":{"reported":{"zoneState":"master","groupMembers":["device1"]}}}
```
## Security Notes
- TLS 1.2 encryption for all communications
- X.509 mutual authentication
- Private keys stored with 700 permissions
- No hardcoded credentials in binaries
- Automatic certificate lifecycle management
- **Monitoring Constraints**: Device credentials restricted to own device topics
- **Ethical Consideration**: Only monitor devices you own
+370
View File
@@ -0,0 +1,370 @@
# IoT Configuration Analysis
## Overview
This document provides a detailed analysis of the AWS IoT configuration system used by Bose SoundTouch devices, based on firmware backup analysis from ST10 and ST20 models.
## Configuration Files
### IoT.xml Location and Content
The IoT configuration is stored in XML format at:
- **Path**: `/mnt/nv/BoseApp-Persistence/1/IoT.xml`
- **Purpose**: Contains AWS IoT Core connection parameters
#### ST20 Configuration
```xml
<?xml version="1.0" encoding="UTF-8" ?>
<Configuration clientID="uuid1"
iotEndpoint="a2bhvr9c4wn4ya.iot.us-east-1.amazonaws.com"
deployment="PROD" />
```
#### ST10 Configuration
```xml
<?xml version="1.0" encoding="UTF-8" ?>
<Configuration clientID="uuid2"
iotEndpoint="a2bhvr9c4wn4ya.iot.us-east-1.amazonaws.com"
deployment="PROD" />
```
### Key Observations
- Each device has a unique `clientID` (UUID format)
- Both devices use the same AWS IoT endpoint
- Both are configured for production deployment (`PROD`)
## Binary Analysis
### Primary IoT Service Binary
**Location**: `/opt/Bose/IoT`
- **Type**: ARM ELF 32-bit executable
- **Purpose**: Main IoT daemon process
- **Framework**: AWS IoT SDK for C++
### Certificate and Key Management
The IoT binary manages the following certificate files:
| File | Location | Purpose |
|-----------------------|---------------------|-----------------------------|
| `iot-cert.pem.crt` | `/mnt/nv/IoTCerts/` | Device client certificate |
| `iot-private.pem.key` | `/mnt/nv/IoTCerts/` | Device private key |
| `rootCA.crt` | `/var/lib/iot/` | AWS IoT Root CA certificate |
### Certificate Registration Process
1. **CSR Generation**: Device generates X.509 certificate signing request
2. **Registration Endpoint**: `https://voice.api.bose.io/alexa/certificate`
3. **Certificate Storage**: Certificates stored in `/mnt/nv/IoTCerts/`
4. **Automatic Provisioning**: Process appears to be automated during device setup
## Protocol Analysis
### Connection Details
- **Protocol**: MQTT over TLS 1.2
- **Port**: Standard MQTT over SSL (likely 8883)
- **Authentication**: X.509 client certificate mutual authentication
- **Endpoint Redundancy**:
- Primary (hardcoded): `amqmidtcohfms.iot.us-east-1.amazonaws.com`
- Fallback (XML config): `a2bhvr9c4wn4ya.iot.us-east-1.amazonaws.com`
### AWS IoT Device Shadow Integration
The system uses AWS IoT Device Shadows for state management:
#### Topic Structure
```
$aws/things/{thing_name}/shadow/update
$aws/things/{thing_name}/shadow/update/accepted
$aws/things/{thing_name}/shadow/update/rejected
$aws/things/{thing_name}/shadow/delete
```
#### Shadow JSON Format
```json
{
"state": {
"desired": {},
"reported": {
"deviceState": "CONNECTED|DISCONNECTED",
"powerState": "ON|OFF",
"zoneState": "...",
"groupState": "..."
}
},
"version": 0,
"clientToken": "...",
"timestamp": 0
}
```
### Message Types
1. **Device State Updates**
- Connection status (`CONNECTED`/`DISCONNECTED`)
- Power state changes
- Audio zone configuration
- Multi-room grouping status
2. **Shadow Delta Processing**
- Receives desired state changes
- Updates device configuration
- Reports new state back to shadow
## System Integration
### Service Management
The IoT service is managed by the Shepherd daemon system:
**Configuration**: `/opt/Bose/etc/Shepherd-noncore.xml`
```xml
<ShepherdConfig>
<daemon name="STSCertified"/>
<daemon name="IoT"/>
<daemon name="TPDA">
<arg>-c</arg>
<arg>/opt/Bose/etc/Voice.xml</arg>
</daemon>
</ShepherdConfig>
```
### Directory Structure Creation
The SoundTouch init script (`/etc/init.d/SoundTouch`) ensures proper directory structure:
```bash
mkdir -p /mnt/nv/BoseLog /mnt/nv/IoTCerts /mnt/nv/BoseApp-Persistence/1
mkdir -m 700 -p /mnt/nv/BoseApp-Persistence/1/Keys
```
### Process Information
From runtime analysis (`/var/run/shepherd/pids`):
- IoT service runs as PID 1837
- BoseApp service runs as PID 1846
- Both services are active during normal operation
## Configuration Dependencies
### Files That Reference IoT Configuration
1. **IoT Binary** (`/opt/Bose/IoT`)
- Primary consumer of IoT.xml configuration
- Contains hardcoded backup endpoints
- Manages certificate lifecycle
2. **BoseApp Binary** (`/opt/Bose/BoseApp`)
- References BoseApp-Persistence directory structure
- May trigger IoT updates based on device state changes
3. **SoundTouch Init Script** (`/etc/init.d/SoundTouch`)
- Creates necessary directory structure
- Ensures proper permissions for certificate storage
4. **Shepherd Configuration** (`/opt/Bose/etc/Shepherd-noncore.xml`)
- Defines IoT service startup parameters
- Manages service lifecycle
## Security Considerations
### Certificate Management
- Private keys stored with 700 permissions
- Certificates managed automatically by the device
- Registration process appears to use device-specific authentication
### Network Security
- All communication over TLS 1.2
- Mutual authentication using X.509 certificates
- AWS IoT Core provides additional access controls
### Configuration Protection
- Configuration files stored in persistent storage
- Directory structure created with appropriate permissions
- No hardcoded credentials in binaries (uses certificate-based auth)
## Integration Points
### AWS Services
- **AWS IoT Core**: Primary messaging and device management
- **AWS IoT Device Management**: Certificate provisioning
- **AWS IoT Device Shadows**: State synchronization
### Bose Services
- **Mobile Applications**: Remote control and monitoring
- **Alexa Integration**: Voice control capabilities
- **Multi-room Audio**: Zone and group coordination
### Device Functions
- **Power Management**: Remote power on/off
- **Audio Control**: Volume, source selection
- **Network Configuration**: WiFi and connectivity settings
- **Firmware Updates**: OTA update coordination
## Troubleshooting
### Common Issues
1. **Certificate Problems**
- Check `/mnt/nv/IoTCerts/` for valid certificates
- Verify certificate registration endpoint accessibility
- Ensure proper file permissions (600 for keys)
2. **Connection Issues**
- Verify both primary and fallback endpoints
- Check TLS 1.2 support and cipher suites
- Validate clientID uniqueness
3. **Configuration Issues**
- Ensure IoT.xml has proper XML format
- Verify clientID is valid UUID format
- Check deployment parameter matches environment
### Debug Information
The IoT binary provides extensive logging for:
- MQTT connection attempts and status
- Certificate loading and validation
- Shadow message processing
- Network state changes
## MQTT Monitoring and Security Considerations
### Direct MQTT Access with Device Credentials
With access to the device's private key and certificate, it's technically possible to subscribe to MQTT events:
```bash
# Subscribe to device shadow events
mosquitto_sub -h a2bhvr9c4wn4ya.iot.us-east-1.amazonaws.com \
-p 8883 --cafile /var/lib/iot/rootCA.crt \
--cert /mnt/nv/IoTCerts/iot-cert.pem.crt \
--key /mnt/nv/IoTCerts/iot-private.pem.key \
-t '$aws/things/_uuid_/shadow/#'
```
### Security Constraints and Limitations
#### AWS IoT Policy Restrictions
Device certificates are bound to specific policies that typically restrict:
- Access to device-specific topics only (`$aws/things/{clientID}/shadow/*`)
- No wildcard subscriptions across multiple devices
- Limited publish/subscribe permissions
- Possible IP geolocation restrictions
#### Example Policy Structure
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "iot:Connect",
"Resource": "arn:aws:iot:us-east-1:*:client/${iot:ClientId}"
},
{
"Effect": "Allow",
"Action": ["iot:Publish", "iot:Subscribe", "iot:Receive"],
"Resource": [
"arn:aws:iot:us-east-1:*:topic/$aws/things/${iot:ClientId}/shadow/*",
"arn:aws:iot:us-east-1:*:topicfilter/$aws/things/${iot:ClientId}/shadow/*"
]
}
]
}
```
#### Additional Security Measures
- Certificate revocation for unusual activity
- Device fingerprinting and connection frequency limits
- Service shutdown timeline (May 2026) affecting endpoint availability
### Alternative Monitoring Approaches
#### Network Traffic Capture
A less intrusive method to analyze MQTT communication patterns:
```bash
# Capture encrypted MQTT traffic from the actual device
tcpdump -i eth0 -s0 -w soundtouch_iot.pcap host a2bhvr9c4wn4ya.iot.us-east-1.amazonaws.com
# Monitor connection patterns
tcpdump -i eth0 -n "host a2bhvr9c4wn4ya.iot.us-east-1.amazonaws.com and port 8883"
```
#### Local MQTT Broker Setup
For development and testing, create a local MQTT broker that mimics AWS IoT behavior:
```bash
# Install and configure Mosquitto
sudo apt-get install mosquitto mosquitto-clients
# Create test shadow topics
mosquitto_pub -h localhost -t '$aws/things/test-device/shadow/update' \
-m '{"state":{"reported":{"deviceState":"CONNECTED"}}}'
```
### Ethical and Legal Considerations
- **Device Ownership**: Only monitor devices you own
- **Terms of Service**: Using credentials outside device context may violate Bose ToS
- **Unauthorized Access**: Accessing Bose's AWS infrastructure could be considered inappropriate
- **Research Purpose**: Limit monitoring to understanding message formats for local alternatives
### Expected Message Examples
If monitoring is successful, typical shadow messages include:
```json
// Power state change
{
"state": {
"reported": {
"powerState": "ON",
"deviceState": "CONNECTED",
"timestamp": 1703875200
}
}
}
// Volume adjustment
{
"state": {
"reported": {
"volume": 25,
"muted": false
}
}
}
// Zone configuration
{
"state": {
"reported": {
"zoneState": "master",
"groupMembers": ["device1", "device2"]
}
}
}
```
### Recommended Research Approach
1. **Document Message Formats**: Capture and analyze JSON structures
2. **Understand State Transitions**: Map device actions to shadow updates
3. **Build Local Alternative**: Use insights to create local MQTT shadow service
4. **Prepare for Service Shutdown**: Develop migration strategy before May 2026
## Conclusion
The Bose SoundTouch IoT configuration system is a sophisticated implementation using AWS IoT Core for real-time device management. The system provides:
- Secure, certificate-based authentication
- Reliable bi-directional communication
- Comprehensive device state management
- Integration with voice assistants and mobile applications
- Robust error handling and retry mechanisms
This architecture enables seamless remote control, monitoring, and coordination of SoundTouch devices across multiple platforms and services.
+84
View File
@@ -0,0 +1,84 @@
# Upstream URLs & Domains Analysis
This document provides a comprehensive overview of the upstream Bose cloud services and domains that SoundTouch devices communicate with. These details were gathered from firmware analysis of ST10/ST20 devices, binary string extraction, and community research from the **SoundCork** project (Issue #128).
## Core Service Domains
SoundTouch devices use a set of primary domains for their operation. These are often configurable via the `SoundTouchSdkPrivateCfg.xml` file.
| Service | Primary Domain | Purpose |
|:--------------------|:------------------------|:--------------------------------------------------------------------|
| **Marge** | `streaming.bose.com` | Account management, streaming source providers, and preset sync. |
| **BMX Registry** | `content.api.bose.io` | Bose Media eXchange service discovery and registry. |
| **Stats/Analytics** | `events.api.bosecm.com` | Telemetry, device events, and usage statistics. |
| **Software Update** | `worldwide.bose.com` | Firmware update checks and downloads (path: `/updates/soundtouch`). |
| **Voice/Alexa** | `voice.api.bose.io` | Token management for Amazon Alexa integration. |
## Internal & Development Domains
Analysis of device binaries (`BoseApp`, `IoT`) and community findings revealed several internal, integration, and development domains used by Bose.
### Marge & Auth Proxies
- `bose-test.apigee.net/margeproxy` (Integration/Test proxy)
- `bose-test.apigee.net/margeproxyefe`
- `streamingstg.bose.com` (Staging)
- `streamingintoauth.bose.com` (Internal Auth)
- `streamingefeintoauth.bose.com` (Internal EFE Auth)
- `streamingefeint.bose.com`
### BMX & Content Registry
- `test.content.api.bose.io`
- `content.api.bose.io/bmx/registry/v1/services`
- `test.content.api.bose.io/bmx/int-registry/v1/services`
- `test.content.api.bose.io/bmx/efe-registry/v1/services`
### Stats & Analytics
- `eventsdev.api.bosecm.com`
- `eventsefe.api.bosecm.com`
- `eventsdev.bosecm.com`
### Software Updates
- `worldwide.bose.com/updates/soundtouch-int`
- `worldwide.bose.com/updates/soundtouch-efe`
## Third-Party Services
Devices also communicate directly with third-party providers for specific features.
- **Pandora**:
- `device-tuner.pandora.com`
- `device-tuner-beta.savagebeast.com`
- **Amazon AVS**:
- `avs.na.amazonalexa.com`
## Hardcoded Validation (IsItBose)
As documented in [DEVICE-REDIRECT-METHODS.md](DEVICE-REDIRECT-METHODS.md#method-3-binary-patching), the `libBmxAccountHsm.so` library contains a hardcoded regex to validate these URLs:
`^https:\/\/bose-[a-zA-Z0-9\.\_\-\$\%]\+\.apigee\.net\/`
This regex ensures that certain critical services must reside on the `apigee.net` domain under a `bose-` prefix, unless patched.
## Configuration File References
On-device, these URLs are primarily managed in the following files:
1. **`/opt/Bose/etc/SoundTouchSdkPrivateCfg.xml`**:
* `<margeServerUrl>`
* `<statsServerUrl>`
* `<swUpdateUrl>`
* `<bmxRegistryUrl>`
2. **`/opt/Bose/etc/Voice.xml`**:
* `<TPDATokenUrl>` (Points to `voice.api.bose.io`)
3. **`/opt/Bose/etc/HandCraftedWebServer-SoundTouch.xml`**:
* Contains internal local API mapping.
## Conclusion for Offline Operation
To achieve full offline operation or redirection to a custom service (like `soundtouch-service`), all of the above domains must either be redirected via DNS (`/etc/hosts`) or updated in the device's XML configuration files. For domains not exposed in XML, binary patching or DNS-level redirection is the only option.
---
## References
- [SoundCork Issue #128: Endpoint and URL Listing](https://github.com/deborahgu/soundcork/issues/128#issuecomment-3892933337)
- [Bose SoundTouch Web API v1.0 Specification](https://assets.bosecreative.com/m/496577402d128874/original/SoundTouch-Web-API.pdf)
@@ -1,7 +1,7 @@
# SoundTouch API Comparison: Community Wiki vs Current Implementation
**Date:** January 2026
**Source:** [SoundTouch Plus Wiki](https://github.com/thlucas1/homeassistantcomponent_soundtouchplus/wiki/SoundTouch-WebServices-API)
**Source:** [SoundTouch Plus Wiki](https://github.com/thlucas1/homeassistantcomponent_soundtouchplus/wiki/SoundTouch-WebServices-API)
**Our Implementation:** Bose-SoundTouch Go Library v1.0
## Executive Summary
@@ -20,84 +20,84 @@ The SoundTouch Plus community wiki documents **87 distinct API endpoints** with
### ✅ Already Implemented (23 endpoints)
| Endpoint | Wiki Status | Our Status | Notes |
|----------|-------------|------------|-------|
| `/info` | ✅ Documented | ✅ Complete | Device information |
| `/now_playing` | ✅ Documented | ✅ Complete | Current playback status |
| `/key` | ✅ Documented | ✅ Complete | Key press/release simulation |
| `/volume` | ✅ Documented | ✅ Complete | Volume and mute control |
| `/bass` | ✅ Documented | ✅ Complete | Bass level control |
| `/bassCapabilities` | ✅ Documented | ✅ Complete | Bass capability detection |
| `/sources` | ✅ Documented | ✅ Complete | Available audio sources |
| `/select` | ✅ Documented | ✅ Complete | Source selection |
| `/presets` | ✅ Documented | ✅ Complete | Preset configurations (read-only) |
| `/getZone` | ✅ Documented | ✅ Complete | Zone status and membership |
| `/setZone` | ✅ Documented | ✅ Complete | Zone creation and management |
| `/addZoneSlave` | ✅ Documented | ✅ Complete | Add device to zone |
| `/removeZoneSlave` | ✅ Documented | ✅ Complete | Remove device from zone |
| `/capabilities` | ✅ Documented | ✅ Complete | Device feature capabilities |
| `/audiodspcontrols` | ✅ Documented | ✅ Complete | Audio DSP modes and video sync |
| `/audioproducttonecontrols` | ✅ Documented | ✅ Complete | Advanced bass/treble controls |
| `/audioproductlevelcontrols` | ✅ Documented | ✅ Complete | Speaker level controls |
| `/name` (GET/POST) | ✅ Documented | ✅ Complete | Device name management |
| `/balance` | ✅ Documented | ✅ Complete | Stereo balance control |
| `/clockTime` | ✅ Documented | ✅ Complete | Device time management |
| `/clockDisplay` | ✅ Documented | ✅ Complete | Clock display settings |
| `/networkInfo` | ✅ Documented | ✅ Complete | Network connectivity info |
| `/requestToken` | ✅ Documented | ✅ Complete | Bearer token generation |
| Endpoint | Wiki Status | Our Status | Notes |
|------------------------------|--------------|------------|-----------------------------------|
| `/info` | ✅ Documented | ✅ Complete | Device information |
| `/now_playing` | ✅ Documented | ✅ Complete | Current playback status |
| `/key` | ✅ Documented | ✅ Complete | Key press/release simulation |
| `/volume` | ✅ Documented | ✅ Complete | Volume and mute control |
| `/bass` | ✅ Documented | ✅ Complete | Bass level control |
| `/bassCapabilities` | ✅ Documented | ✅ Complete | Bass capability detection |
| `/sources` | ✅ Documented | ✅ Complete | Available audio sources |
| `/select` | ✅ Documented | ✅ Complete | Source selection |
| `/presets` | ✅ Documented | ✅ Complete | Preset configurations (read-only) |
| `/getZone` | ✅ Documented | ✅ Complete | Zone status and membership |
| `/setZone` | ✅ Documented | ✅ Complete | Zone creation and management |
| `/addZoneSlave` | ✅ Documented | ✅ Complete | Add device to zone |
| `/removeZoneSlave` | ✅ Documented | ✅ Complete | Remove device from zone |
| `/capabilities` | ✅ Documented | ✅ Complete | Device feature capabilities |
| `/audiodspcontrols` | ✅ Documented | ✅ Complete | Audio DSP modes and video sync |
| `/audioproducttonecontrols` | ✅ Documented | ✅ Complete | Advanced bass/treble controls |
| `/audioproductlevelcontrols` | ✅ Documented | ✅ Complete | Speaker level controls |
| `/name` (GET/POST) | ✅ Documented | ✅ Complete | Device name management |
| `/balance` | ✅ Documented | ✅ Complete | Stereo balance control |
| `/clockTime` | ✅ Documented | ✅ Complete | Device time management |
| `/clockDisplay` | ✅ Documented | ✅ Complete | Clock display settings |
| `/networkInfo` | ✅ Documented | ✅ Complete | Network connectivity info |
| `/requestToken` | ✅ Documented | ✅ Complete | Bearer token generation |
### 🔥 High Priority Missing (20 endpoints)
| Endpoint | Wiki Status | Priority | Use Case |
|----------|-------------|----------|----------|
| `/storePreset` | ✅ Detailed | **HIGH** | Save stations/playlists to presets |
| `/removePreset` | ✅ Detailed | **HIGH** | Delete saved presets |
| `/selectPreset` | ✅ Detailed | **HIGH** | Play preset by ID |
| `/setMusicServiceAccount` | ✅ Detailed | **HIGH** | Add Spotify/Pandora accounts |
| `/removeMusicServiceAccount` | ✅ Detailed | **HIGH** | Remove music service accounts |
| `/searchStation` | ✅ Detailed | **HIGH** | Find Pandora/Spotify content |
| `/addStation` | ✅ Detailed | **HIGH** | Add stations to favorites |
| `/removeStation` | ✅ Detailed | **HIGH** | Remove stations from favorites |
| `/navigate` | ✅ Detailed | **HIGH** | Browse music libraries/services |
| `/search` | ✅ Detailed | **HIGH** | Search music content |
| `/userPlayControl` | ✅ Detailed | **HIGH** | Play/pause/stop controls |
| `/userRating` | ✅ Detailed | **HIGH** | Thumbs up/down ratings |
| `/recents` | ✅ Detailed | **HIGH** | Recently played content |
| `/standby` | ✅ Detailed | **HIGH** | Power management |
| `/powerManagement` | ✅ Detailed | **HIGH** | Power state information |
| `/lowPowerStandby` | ✅ Detailed | **HIGH** | Low-power mode |
| `/listMediaServers` | ✅ Detailed | **HIGH** | UPnP/DLNA server discovery |
| `/serviceAvailability` | ✅ Detailed | **HIGH** | Source availability status |
| `/introspect` | ✅ Detailed | **HIGH** | Music service account status |
| `/language` | ✅ Detailed | **HIGH** | Device language settings |
| Endpoint | Wiki Status | Priority | Use Case |
|------------------------------|-------------|----------|------------------------------------|
| `/storePreset` | ✅ Detailed | **HIGH** | Save stations/playlists to presets |
| `/removePreset` | ✅ Detailed | **HIGH** | Delete saved presets |
| `/selectPreset` | ✅ Detailed | **HIGH** | Play preset by ID |
| `/setMusicServiceAccount` | ✅ Detailed | **HIGH** | Add Spotify/Pandora accounts |
| `/removeMusicServiceAccount` | ✅ Detailed | **HIGH** | Remove music service accounts |
| `/searchStation` | ✅ Detailed | **HIGH** | Find Pandora/Spotify content |
| `/addStation` | ✅ Detailed | **HIGH** | Add stations to favorites |
| `/removeStation` | ✅ Detailed | **HIGH** | Remove stations from favorites |
| `/navigate` | ✅ Detailed | **HIGH** | Browse music libraries/services |
| `/search` | ✅ Detailed | **HIGH** | Search music content |
| `/userPlayControl` | ✅ Detailed | **HIGH** | Play/pause/stop controls |
| `/userRating` | ✅ Detailed | **HIGH** | Thumbs up/down ratings |
| `/recents` | ✅ Detailed | **HIGH** | Recently played content |
| `/standby` | ✅ Detailed | **HIGH** | Power management |
| `/powerManagement` | ✅ Detailed | **HIGH** | Power state information |
| `/lowPowerStandby` | ✅ Detailed | **HIGH** | Low-power mode |
| `/listMediaServers` | ✅ Detailed | **HIGH** | UPnP/DLNA server discovery |
| `/serviceAvailability` | ✅ Detailed | **HIGH** | Source availability status |
| `/introspect` | ✅ Detailed | **HIGH** | Music service account status |
| `/language` | ✅ Detailed | **HIGH** | Device language settings |
### 🎵 Music Service Management (12 endpoints)
| Category | Endpoints | Wiki Coverage | Notes |
|----------|-----------|---------------|-------|
| **Account Management** | `/setMusicServiceAccount`, `/removeMusicServiceAccount` | ✅ Full XML examples | Pandora, Spotify, NAS setup |
| **Station Management** | `/searchStation`, `/addStation`, `/removeStation` | ✅ Pandora tested | Station discovery and favorites |
| **Content Navigation** | `/navigate`, `/search` | ✅ Detailed examples | Music library browsing |
| **Track Information** | `/trackInfo`, `/introspect` | ✅ Service-specific | Extended metadata |
| Category | Endpoints | Wiki Coverage | Notes |
|------------------------|---------------------------------------------------------|---------------------|---------------------------------|
| **Account Management** | `/setMusicServiceAccount`, `/removeMusicServiceAccount` | ✅ Full XML examples | Pandora, Spotify, NAS setup |
| **Station Management** | `/searchStation`, `/addStation`, `/removeStation` | ✅ Pandora tested | Station discovery and favorites |
| **Content Navigation** | `/navigate`, `/search` | ✅ Detailed examples | Music library browsing |
| **Track Information** | `/trackInfo`, `/introspect` | ✅ Service-specific | Extended metadata |
### 🏠 Smart Home Integration (15 endpoints)
| Category | Endpoints | Wiki Coverage | Notes |
|----------|-----------|---------------|-------|
| **Notifications** | `/speaker`, `/playNotification` | ✅ TTS examples | Text-to-speech, URL playback |
| **Power Management** | `/standby`, `/powerManagement`, `/lowPowerStandby` | ✅ Complete | Smart home automation |
| **Network Management** | `/performWirelessSiteSurvey`, `/addWirelessProfile`, `/getActiveWirelessProfile` | ✅ WiFi setup | Network configuration |
| **Bluetooth** | `/enterBluetoothPairing`, `/clearBluetoothPaired`, `/bluetoothInfo` | ✅ Pairing control | Bluetooth management |
| **Source Control** | `/selectLastSource`, `/selectLastSoundTouchSource`, `/selectLocalSource` | ✅ Source switching | Quick source access |
| Category | Endpoints | Wiki Coverage | Notes |
|------------------------|----------------------------------------------------------------------------------|--------------------|------------------------------|
| **Notifications** | `/speaker`, `/playNotification` | ✅ TTS examples | Text-to-speech, URL playback |
| **Power Management** | `/standby`, `/powerManagement`, `/lowPowerStandby` | ✅ Complete | Smart home automation |
| **Network Management** | `/performWirelessSiteSurvey`, `/addWirelessProfile`, `/getActiveWirelessProfile` | ✅ WiFi setup | Network configuration |
| **Bluetooth** | `/enterBluetoothPairing`, `/clearBluetoothPaired`, `/bluetoothInfo` | ✅ Pairing control | Bluetooth management |
| **Source Control** | `/selectLastSource`, `/selectLastSoundTouchSource`, `/selectLocalSource` | ✅ Source switching | Quick source access |
### 📱 Advanced Device Features (19 endpoints)
| Category | Endpoints | Wiki Coverage | Notes |
|----------|-----------|---------------|-------|
| **Stereo Pairs** | `/getGroup`, `/addGroup`, `/removeGroup`, `/updateGroup` | ✅ ST-10 specific | L/R speaker pairing |
| **System Info** | `/soundTouchConfigurationStatus`, `/systemtimeout`, `/rebroadcastlatencymode` | ✅ Configuration | Device state management |
| **Software Updates** | `/swUpdateCheck`, `/swUpdateQuery`, `/swUpdateAbort`, `/swUpdateStart` | ✅ Update process | Firmware management |
| **Audio Processing** | `/DSPMonoStereo`, `/audiospeakerattributeandsetting` | ✅ Hardware-specific | Advanced audio features |
| Category | Endpoints | Wiki Coverage | Notes |
|----------------------|-------------------------------------------------------------------------------|---------------------|-------------------------|
| **Stereo Pairs** | `/getGroup`, `/addGroup`, `/removeGroup`, `/updateGroup` | ✅ ST-10 specific | L/R speaker pairing |
| **System Info** | `/soundTouchConfigurationStatus`, `/systemtimeout`, `/rebroadcastlatencymode` | ✅ Configuration | Device state management |
| **Software Updates** | `/swUpdateCheck`, `/swUpdateQuery`, `/swUpdateAbort`, `/swUpdateStart` | ✅ Update process | Firmware management |
| **Audio Processing** | `/DSPMonoStereo`, `/audiospeakerattributeandsetting` | ✅ Hardware-specific | Advanced audio features |
---
@@ -137,7 +137,7 @@ The SoundTouch Plus community wiki documents **87 distinct API endpoints** with
**WebSocket Events Documented:**
- `presetsUpdated` - Preset changes
- `groupUpdated` - Stereo pair changes
- `groupUpdated` - Stereo pair changes
- `zoneUpdated` - Multi-room changes
- `nowPlayingUpdated` - Source/playback changes
- `volumeUpdated` - Volume/mute changes
@@ -194,7 +194,7 @@ func (c *Client) RateCurrentTrack(rating RatingValue) error
func (c *Client) CreateStereoPair(leftIP, rightIP string, name string) error
func (c *Client) GetStereoPairStatus() (*StereoPair, error)
// System Management
// System Management
func (c *Client) CheckSoftwareUpdate() (*UpdateInfo, error)
func (c *Client) GetSystemTimeout() (*TimeoutConfig, error)
```
@@ -283,7 +283,7 @@ The SoundTouch Plus Wiki represents a **treasure trove** of production-ready API
### Key Opportunities:
- 🎯 **3x Coverage Expansion**: From 23 to 87+ endpoints
- 🏠 **Smart Home Ready**: Complete automation integration
- 🎵 **Music Service Integration**: Full streaming service support
- 🎵 **Music Service Integration**: Full streaming service support
- 📱 **Professional Features**: Advanced audio and system control
- ✅ **Production Ready**: Real-world tested examples and error handling
@@ -297,4 +297,4 @@ The SoundTouch Plus Wiki represents a **treasure trove** of production-ready API
---
*Note: All endpoints documented in the wiki are tested against real hardware. Device-specific limitations are clearly documented with compatibility matrices for ST-10, ST-300, and other SoundTouch models.*
*Note: All endpoints documented in the wiki are tested against real hardware. Device-specific limitations are clearly documented with compatibility matrices for ST-10, ST-300, and other SoundTouch models.*
+52
View File
@@ -0,0 +1,52 @@
### Stockholm App Analysis Report
#### 1. Overview
The Stockholm app is a CEPE MAUI SoundTouch Controller HTML5/JS UI. It is designed to run as a web-based interface for Bose SoundTouch devices, likely served by the device itself or an associated controller.
- **Technology Stack**: HTML5, CSS3, JavaScript (Minified).
- **Key Libraries**:
- **jQuery**: Core DOM manipulation and event handling.
- **iScroll**: Used for smooth scrolling in lists and carousels.
- **Forge**: Used for cryptographic operations (likely for secure communication or authentication).
- **WebSocket Polyfill**: Ensures WebSocket compatibility across environments.
#### 2. Directory Structure
- `js/`: Core application logic.
- `app/`: Main application entry point (`app.js`).
- `models/`: Data models for UI components (Presets, Favorites, Onboarding, etc.).
- `music_services/`: Implementation of various music services (Amazon, Deezer, Spotify, BMX, etc.).
- `views/`: UI view templates and logic.
- `utils/`: Utility functions for security, data analytics, and general-purpose tasks.
- `json/`: Configuration files and static data.
- `config.json`: Core application configuration including Base64 encoded Bose API endpoints (e.g., streaming, events, BMX registry).
- `sourceFeatures.json`: Capability mapping for different sources.
- `setup/`: Onboarding and initial device setup logic.
- `lang/`: Localization files for multi-language support.
#### 3. Communication Architecture
The app uses several communication channels to interact with the SoundTouch ecosystem:
- **Socket Communication (`socket_comm.js`)**: Real-time updates and low-latency commands via WebSockets.
- **BMX (`bmx.js` & `js/music_services/bmx/`)**: Interactions with the Bose Music eXperience services. Handles account management, navigation, and API response validation.
- **Marge (`marge_comm.js`)**: Likely used for interaction with the Marge service (Bose's legacy cloud/proxy service).
- **Worker-based Architecture**: Many services use Web Workers (`bmx_worker.js`, `spotify_worker.js`) to handle API requests and data processing in the background, keeping the UI responsive.
#### 4. Key Features & Functionality
- **Multi-Device Management**: Discovering and controlling multiple speakers on the network.
- **Music Service Integration**: Deep integration with Spotify, Amazon Music, Deezer, and Pandora.
- **Preset Management**: Browsing and setting presets directly from the UI.
- **Zone Control**: Creating and managing multi-room groups (Master/Slave configurations).
- **Onboarding**: A dedicated setup flow for new devices.
- **Analytics & Data Collection**: Modules like `data_analytics.js` and `dc_server.js` suggest tracking of user interactions.
#### 5. Integration Opportunities for Bose-SoundTouch Project
Based on the Stockholm app's capabilities, the following features could be enhanced or added to our Go-based `soundtouch-service`:
1. **Enhanced BMX Emulation**: Use insights from `bmx_client.js` and `bmx_navigate_response_generator.js` to improve our local BMX implementation.
2. **Spotify/Amazon Service Proxies**: Implement the backend logic required to support the same API calls the Stockholm app makes to these services.
3. **UI parity**: The Stockholm app's view templates (`views/`) can serve as a reference for our Web Management UI.
4. **WebSocket Support**: Ensure our service provides a robust WebSocket interface similar to what the Stockholm app expects for real-time state synchronization.
5. **Capability Discovery**: Better utilization of the `sourceFeatures.json` logic to dynamically show/hide features based on the device model and firmware version.
#### 6. Conclusion
The Stockholm app is a mature, full-featured controller that relies heavily on Bose's proprietary BMX and Marge services. By analyzing its client-side logic, we can better understand the expected API responses and interaction patterns needed to provide a seamless local replacement for the Bose Cloud.
+75
View File
@@ -0,0 +1,75 @@
# Merging Bose-SoundTouch-API into Bose-SoundTouch
This document outlines the plan to merge the [Bose-SoundTouch-API](https://github.com/gesellix/Bose-SoundTouch-API) project into this repository. The actual Go implementation in that repository is located in the `soundcork-go` subdirectory. The goal is to provide both a CLI (`soundtouch-cli`) and a service (`soundtouch-service`) from a single codebase.
## Goals
- [x] Maintain the existing `soundtouch-cli` functionality.
- [x] Introduce `soundtouch-service` as a new command (based on the `soundcork-go` project).
- [x] Consolidate shared logic (models, clients, discovery) into the `pkg/` directory.
- [x] Simplify maintenance by having a single Go module and shared CI/CD pipeline.
## Current Directory Structure
```text
.
├── cmd/
│ ├── soundtouch-cli/ # Existing CLI implementation
│ │ └── main.go
│ └── soundtouch-service/ # New service implementation (REST API / Websocket)
│ └── main.go
├── pkg/
│ ├── client/ # Shared SoundTouch API client
│ ├── models/ # Shared data models
│ ├── discovery/ # Shared device discovery logic
│ └── service/ # Service-specific logic (from Bose-SoundTouch-API)
│ ├── bmx/ # BMX service logic
│ ├── marge/ # Marge service logic
│ ├── datastore/ # Device and configuration storage
│ ├── proxy/ # Logging proxy logic
│ ├── setup/ # Device setup and migration logic
│ └── handlers/ # HTTP handlers (adapted from soundcork-go/soundcork-go)
│ └── soundcork/ # Embedded resources (index.html, media/, etc.)
├── docs/
│ └── MERGE_PROJECTS.md # This document
├── go.mod
└── go.sum
```
## Step-by-Step Merge Status
### 1. Preparation
- [x] Review `go.mod` in both projects to identify dependency overlaps and conflicts.
### 2. Code Integration
- [x] **Models & Client**: Merged missing functionality from `soundcork-go/internal/models` into `pkg/models`. Renamed overlapping models to `Service*` (e.g., `ServiceContentItem`, `ServicePreset`).
- [x] **Service Logic**: Adapted internal packages from `soundcork-go/internal/` to `pkg/service/`.
- [x] **Handlers**: Moved and adapted HTTP handlers into `pkg/service/handlers/`.
- [x] **New Command**: Created `cmd/soundtouch-service/main.go` as the service entry point using `chi` router.
- [x] **Embedded Resources**: Integrated `index.html`, `bmx_services.json`, `swupdate.xml`, and `media/` folder into the binary using `//go:embed`.
### 3. Dependency Management
- [x] Update `go.mod` to include:
- `github.com/go-chi/chi/v5`
- `github.com/srwiley/oksvg` and `github.com/srwiley/rasterx`
- `golang.org/x/crypto`
- [x] Run `go mod tidy` to clean up dependencies.
### 4. Shared Logic Refactoring
- [x] Identify common code between `soundtouch-cli` and the new service.
- [x] Move shared logic into `pkg/` to ensure both commands use the same underlying implementation.
### 5. Documentation & Examples
- [x] Update `README.md` to mention the new `soundtouch-service` command.
- [x] Add service-specific documentation in `docs/SOUNDTOUCH-SERVICE.md`.
- [x] Provide examples of how to run and interact with the service in `examples/service-demo/`.
### 6. CI/CD Updates
- [x] Update `.github/workflows/release.yml` to build and release the `soundtouch-service` binary alongside `soundtouch-cli`.
- [x] Update any test workflows to include tests for the service logic.
## Verification
- [x] `go build ./cmd/soundtouch-cli` works as expected.
- [x] `go build ./cmd/soundtouch-service` works as expected.
- [x] All tests pass: `go test ./...`.
- [x] Resources are correctly served from the embedded filesystem.
+1 -1
View File
@@ -784,4 +784,4 @@ docker-compose up # Mock devices + web app
- [UPnP Device Architecture](http://upnp.org/specs/arch/UPnP-arch-DeviceArchitecture-v1.0.pdf)
- [Go Embed Directive](https://pkg.go.dev/embed)
- [Gorilla WebSocket](https://github.com/gorilla/websocket)
- [PROJECT-PATTERNS.md](./PROJECT-PATTERNS.md) - Detailed pattern documentation
- [PROJECT-PATTERNS.md](../PROJECT-PATTERNS.md) - Detailed pattern documentation
+6 -6
View File
@@ -206,14 +206,14 @@ This project implements a comprehensive Go client library and CLI tool for Bose
### ✅ Complete Documentation
- `README.md` - Project overview and usage examples ✅
- `docs/API-Endpoints-Overview.md` - API reference with status ✅
- `docs/KEY-CONTROLS.md` - Media control implementation ✅
- `docs/VOLUME-CONTROLS.md` - Volume management guide ✅
- `docs/PRESET-MANAGEMENT.md` - Preset analysis and limitations ✅
- `docs/reference/API-ENDPOINTS.md` - API reference with status ✅
- `docs/reference/KEY-CONTROLS.md` - Media control implementation ✅
- `docs/guides/VOLUME-CONTROLS.md` - Volume management guide ✅
- `docs/reference/PRESET-MANAGEMENT.md` - Preset analysis and limitations ✅
- `docs/HOST-PORT-PARSING.md` - Enhanced CLI feature ✅
- `docs/PLAN.md` - Development roadmap (updated) ✅
- `docs/archive/PLAN.md` - Development roadmap (updated) ✅
- `docs/PROJECT-PATTERNS.md` - Development guidelines ✅
- `SPEAKER_ENDPOINT.md` - Complete speaker notification documentation ✅
- `docs/reference/SPEAKER-ENDPOINT.md` - Complete speaker notification documentation ✅
### 📝 Documentation Notes
- All docs are synchronized with current implementation
@@ -108,8 +108,8 @@ Essential for browsing music libraries and searching content.
// pkg/api/content.go (new file)
func (c *Client) Navigate(source, sourceAccount string, options NavigateOptions) (*NavigateResponse, error)
func (c *Client) Search(source, sourceAccount, searchTerm string, options SearchOptions) (*SearchResponse, error)
func (c *Client) GetRecents() (*RecentsResponse, error)
func (c *Client) Introspect(source, sourceAccount string) (*IntrospectResponse, error)
func (c *Client) GetRecents() (*RecentsResponse, error) // ✅ IMPLEMENTED
func (c *Client) Introspect(source, sourceAccount string) (*IntrospectResponse, error) // ✅ IMPLEMENTED
```
#### Data Structures:
+181
View File
@@ -0,0 +1,181 @@
# Upstream Bose Service Simulation - Concept Overview
## Executive Summary
This document serves as the entry point for understanding the comprehensive plan to enhance the SoundTouch service with advanced state management capabilities, preparing for the eventual shutdown of Bose's upstream services while providing a superior local management experience.
## Project Objectives
### Primary Goal
Create a robust, local replacement for Bose's upstream services that can seamlessly handle the transition from cloud-dependent to fully autonomous operation while maintaining and improving upon the existing functionality.
### Key Outcomes
- **Zero-downtime transition** from Bose services to local management
- **Enhanced visibility** into device states, health, and system operations
- **Data preservation** during migrations with full rollback capabilities
- **Improved reliability** through local control and reduced external dependencies
- **Future-proof architecture** that can evolve beyond Bose's original design
## Architecture Vision
### Current State
The existing SoundTouch service provides:
- BMX service for TuneIn integration
- Marge service for account and device management
- Basic mirroring of upstream Bose endpoints
- File-based persistence for device data
- Migration support for device directory structures
### Enhanced State (This Project)
The enhanced system will add:
- **Comprehensive Account Management** with explicit creation and migration tracking
- **Device Lifecycle Management** with full state machine and event processing
- **Advanced Mirroring** with disparity detection and analysis
- **Dual-Source Data Management** supporting gradual migration strategies
- **Real-time Monitoring** with health checks and performance metrics
- **Text-based Storage** optimized for debugging and small hardware deployments
## Use Case Coverage
### Case 0: Account Management
- **Explicit Account Creation**: Accounts created through deliberate user action
- **Mirror-Enhanced Setup**: Use upstream data to enrich account creation
- **Passive Data Collection**: Record account information during normal operations
### Case 1a: Fresh Device Registration
- **Factory Reset Support**: Handle devices with no prior Bose association
- **Default Configuration**: Initialize devices with sensible presets and sources
- **Local-First Setup**: Complete registration without upstream dependencies
### Case 1b: Bose Account Migration
- **Data Preservation**: Maintain existing presets, recents, and sources
- **Gradual Migration**: Support partial migration while maintaining upstream compatibility
- **Rollback Capability**: Revert to Bose services if needed
### Case 2: Lifecycle and State Management
- **Real-time State Tracking**: Monitor device states and health continuously
- **Event-Driven Updates**: Process device events asynchronously
- **Disparity Detection**: Identify differences between local and upstream behavior
- **Comprehensive Logging**: Maintain detailed audit trails for troubleshooting
## Technical Approach
### Design Principles
1. **Text-First Storage**: Human-readable formats (JSON, XML, logs) for easy debugging
2. **Small Hardware Optimization**: Designed for Raspberry Pi Zero 2W deployments
3. **Mirror-First Strategy**: Keep upstream mirroring active until migration complete
4. **Event-Driven Architecture**: Asynchronous processing with comprehensive event tracking
5. **Backward Compatibility**: Seamless integration with existing installations
### Data Structure
```
data/
├── accounts/{account-id}/
│ ├── account.json # Account metadata and settings
│ ├── account-events.log # High-level account behavior tracking
│ ├── devices/{device-id}/
│ │ ├── lifecycle.json # Device state and history
│ │ ├── info.xml # Device information (existing)
│ │ ├── presets.xml # Device presets (existing)
│ │ ├── recents.xml # Recent plays (existing)
│ │ ├── sources.xml # Configured sources (existing)
│ │ └── events.log # Device event history
│ └── sessions/ # Recorded interaction sessions (existing)
└── system/
├── discovery.log # Device discovery events
└── migration.log # Migration activities
```
### Development Targets
- **Simplicity**: Keep It Simple, Stupid (KISS) principle over optimization
- **Quality**: 100% test pass rate and lint-clean code for every change
- **Compatibility**: Zero breaking changes to existing functionality
- **Leveraging**: Reuse existing systems (interaction recording, parity detection)
## Implementation Strategy
### Phase 1: Foundation (2-3 weeks) - Small, Testable Steps
- Account management foundation with basic create/read operations
- Device lifecycle data models and simple state tracking
- Basic API endpoints with comprehensive testing
- Integration with existing datastore patterns
### Phase 2: Device Lifecycle (2-3 weeks) - Build on Existing Systems
- Event processing using existing WebSocket system
- Lifecycle integration with current discovery and migration
- Enhanced logging building on existing parity detection
- Simple state machine with thorough testing
### Phase 3: Enhanced Features (2-3 weeks) - Leverage Current Systems
- Improve existing parity mismatch detection with better categorization
- Smart data source routing with fallback mechanisms
- Basic monitoring using existing health check patterns
- Reuse interaction recording for request/response tracking
## Key Benefits
### For Users
- **Continuity**: Seamless operation when Bose services shut down
- **Reliability**: Local control reduces dependency on external services
- **Visibility**: Clear insight into device states and system health
- **Control**: Full management of device data and configurations
### For Developers
- **Simplicity**: KISS principle makes code easy to understand and maintain
- **Quality**: Comprehensive testing and linting ensures reliable code
- **Debugging**: Text-based storage enables easy troubleshooting
- **Testing**: Every change requires full test suite pass and lint compliance
### For Community
- **Open Source**: Transparent implementation available for community contributions
- **Standards**: Well-documented APIs and data formats
- **Collaboration**: Disparity detection helps improve implementation accuracy
- **Future-Proof**: Architecture designed to outlast original Bose services
### Technical Risks
- **Data Loss Prevention**: Atomic file operations and comprehensive testing
- **Complexity Creep**: KISS principle and simple-first approach
- **Compatibility Issues**: Extensive regression testing and existing system reuse
- **Code Quality**: Mandatory linting and test coverage for every change
### Operational Risks
- **Service Disruption**: Small, incremental changes with rollback capability
- **Testing Overhead**: Automated quality gates (`golangci-lint run --fix` + `go test ./...`)
- **Migration Challenges**: Leverage existing migration system and patterns
- **Maintenance Burden**: Simple, well-tested code is easier to maintain
### Technical
- All tests pass consistently (100%)
- Zero linting issues in codebase
- No breaking changes to existing functionality
- Code coverage maintained or improved
### Quality Assurance
- Every commit passes `golangci-lint run --fix`
- Every milestone passes `go test ./...`
- Integration tests verify existing functionality
- Simple, maintainable code that follows Go idioms
## Documentation Structure
This concept is detailed across several documents:
- **[upstream-service-simulation.md](./upstream-service-simulation.md)**: Complete architectural concept with detailed use cases and implementation guidelines
- **[implementation-roadmap.md](./implementation-roadmap.md)**: Detailed project phases, milestones, and delivery timeline
- **[technical-specification.md](./technical-specification.md)**: Comprehensive technical details including APIs, data models, and performance requirements
## Getting Started
1. **Review the Concept**: Read through the main concept document to understand the full scope
2. **Examine Technical Details**: Review the technical specification for implementation details
3. **Follow the Roadmap**: Use the implementation roadmap for project planning and execution
4. **Integration Planning**: Consider how the enhanced features will integrate with existing deployments
## Next Steps
1. **Stakeholder Review**: Gather feedback on the concept and approach
2. **Technical Validation**: Prototype key components to validate technical assumptions
3. **Resource Planning**: Allocate development resources for the three-phase implementation
4. **Community Engagement**: Share plans with the community for feedback and contributions
This enhanced state management system represents a significant evolution of the SoundTouch service, transforming it from a basic cloud replacement into a comprehensive, future-proof device management platform that can serve users well beyond the Bose service shutdown timeline.
+355
View File
@@ -0,0 +1,355 @@
# Implementation Plan - Enhanced State Management System
## Overview
This document provides a detailed, step-by-step implementation plan for the enhanced state management system. Each step is designed to be small, testable, and independently valuable while maintaining backward compatibility.
## Development Principles
### Quality Gates
Every step must pass these checks before proceeding:
1. `golangci-lint run --fix` - no linting issues
2. `go test ./...` - all tests pass
3. Existing functionality remains intact
4. New functionality has appropriate test coverage
### KISS Principle
- Write the simplest code that works
- Avoid premature optimization
- Use straightforward algorithms
- Build incrementally with small changes
### Leverage Existing Systems
- Reuse interaction recording for request/response tracking
- Build upon current parity mismatch detection
- Extend existing datastore patterns
- Integrate with established workflows
## Phase 1: Foundation Preparation (2-3 weeks)
### Step 1.1: Code Organization Preparation
**Duration**: 2-3 days
**Goal**: Prepare package structure without changing behavior
#### Mini-milestone 1.1.1: Create account package structure
- Create `pkg/service/account/` directory
- Add basic `account.go` with placeholder structs
- Add `account_test.go` with basic test structure
- **Quality Check**: Lint + test all packages
#### Mini-milestone 1.1.2: Create lifecycle package structure
- Create `pkg/service/lifecycle/` directory
- Add basic `lifecycle.go` with placeholder structs
- Add `lifecycle_test.go` with basic test structure
- **Quality Check**: Lint + test all packages
#### Mini-milestone 1.1.3: Extend datastore interface preparation
- Add placeholder methods to existing datastore for account operations
- Ensure all existing functionality still works
- Add tests for new placeholder methods
- **Quality Check**: Lint + test all packages
### Step 1.2: Account Management Foundation
**Duration**: 3-4 days
**Goal**: Basic account creation and retrieval
#### Mini-milestone 1.2.1: Account data model
- Define `Account` struct with basic fields
- Add validation functions
- Add comprehensive unit tests
- **Quality Check**: Lint + test all packages
#### Mini-milestone 1.2.2: Account persistence
- Implement account.json file read/write
- Add atomic file operations
- Test file operations thoroughly
- **Quality Check**: Lint + test all packages
#### Mini-milestone 1.2.3: Account manager basic operations
- Implement `CreateAccount()` function
- Implement `GetAccount()` function
- Add error handling and validation
- **Quality Check**: Lint + test all packages
#### Mini-milestone 1.2.4: Integration with existing datastore
- Modify datastore to use account manager
- Ensure backward compatibility with existing accounts
- Test migration of existing data structure
- **Quality Check**: Lint + test all packages
### Step 1.3: Basic API Endpoints
**Duration**: 2-3 days
**Goal**: Add REST endpoints for account management
#### Mini-milestone 1.3.1: Account creation endpoint
- Add `POST /api/v1/accounts` handler
- Integrate with existing HTTP router
- Add input validation and error responses
- **Quality Check**: Lint + test all packages
#### Mini-milestone 1.3.2: Account retrieval endpoint
- Add `GET /api/v1/accounts/{id}` handler
- Add proper JSON serialization
- Test endpoint functionality
- **Quality Check**: Lint + test all packages
#### Mini-milestone 1.3.3: Integration testing
- Test new endpoints with existing functionality
- Ensure XML endpoints still work
- Verify no breaking changes
- **Quality Check**: Lint + test all packages
## Phase 2: Device Lifecycle Foundation (2-3 weeks)
### Step 2.1: Device State Model
**Duration**: 3-4 days
**Goal**: Basic device lifecycle tracking
#### Mini-milestone 2.1.1: Device lifecycle data model
- Define `DeviceLifecycle` struct
- Define device states and transitions
- Add validation and helper functions
- **Quality Check**: Lint + test all packages
#### Mini-milestone 2.1.2: State transition logic
- Implement basic state machine
- Add transition validation
- Create comprehensive tests for all transitions
- **Quality Check**: Lint + test all packages
#### Mini-milestone 2.1.3: Lifecycle persistence
- Implement lifecycle.json file operations
- Add atomic updates and error handling
- Test persistence thoroughly
- **Quality Check**: Lint + test all packages
### Step 2.2: Event Processing Foundation
**Duration**: 3-4 days
**Goal**: Basic event handling and logging
#### Mini-milestone 2.2.1: Event data model
- Define `DeviceEvent` struct
- Add event types and validation
- Create event builder helpers
- **Quality Check**: Lint + test all packages
#### Mini-milestone 2.2.2: Simple event logging
- Implement append-only event log writing
- Add structured log format
- Test log operations and rotation
- **Quality Check**: Lint + test all packages
#### Mini-milestone 2.2.3: Event processing pipeline
- Create basic synchronous event processor
- Add event validation and filtering
- Integrate with existing WebSocket events
- **Quality Check**: Lint + test all packages
### Step 2.3: Lifecycle Integration
**Duration**: 2-3 days
**Goal**: Connect lifecycle to existing systems
#### Mini-milestone 2.3.1: Discovery integration
- Trigger lifecycle events on device discovery
- Update device state on discovery
- Test discovery workflow with lifecycle
- **Quality Check**: Lint + test all packages
#### Mini-milestone 2.3.2: WebSocket integration
- Process WebSocket events through lifecycle
- Update device state based on events
- Log significant state changes
- **Quality Check**: Lint + test all packages
#### Mini-milestone 2.3.3: Migration integration
- Integrate lifecycle with existing migration system
- Track migration events and state changes
- Ensure existing migration still works
- **Quality Check**: Lint + test all packages
## Phase 3: Enhanced Features (2-3 weeks)
### Step 3.1: Enhanced Mirroring
**Duration**: 3-4 days
**Goal**: Improve existing parity detection
#### Mini-milestone 3.1.1: Extended disparity logging
- Enhance existing parity mismatch logging
- Add more detailed disparity information
- Improve log format for analysis
- **Quality Check**: Lint + test all packages
#### Mini-milestone 3.1.2: Disparity categorization
- Add severity levels to disparities
- Categorize different types of mismatches
- Add filtering and search capabilities
- **Quality Check**: Lint + test all packages
#### Mini-milestone 3.1.3: Enhanced mirror middleware
- Extend existing mirror functionality
- Add better response comparison
- Integrate with lifecycle events
- **Quality Check**: Lint + test all packages
### Step 3.2: Data Source Management
**Duration**: 3-4 days
**Goal**: Smart routing between local and upstream
#### Mini-milestone 3.2.1: Data source configuration
- Add per-device source preferences
- Implement source switching logic
- Add configuration persistence
- **Quality Check**: Lint + test all packages
#### Mini-milestone 3.2.2: Fallback mechanisms
- Add graceful fallback on source failure
- Implement simple health checking
- Test fallback scenarios
- **Quality Check**: Lint + test all packages
#### Mini-milestone 3.2.3: Migration orchestration
- Add device-by-device migration control
- Track migration progress
- Add rollback capabilities
- **Quality Check**: Lint + test all packages
### Step 3.3: Monitoring and Health
**Duration**: 2-3 days
**Goal**: Basic system monitoring
#### Mini-milestone 3.3.1: Health check endpoints
- Add system health endpoints
- Report service status
- Add basic metrics collection
- **Quality Check**: Lint + test all packages
#### Mini-milestone 3.3.2: Device health tracking
- Track device connectivity
- Monitor response times
- Log health status changes
- **Quality Check**: Lint + test all packages
#### Mini-milestone 3.3.3: System metrics
- Add basic performance metrics
- Track resource usage
- Add metrics endpoints
- **Quality Check**: Lint + test all packages
## Quality Assurance Strategy
### Testing Requirements
Each mini-milestone must include:
- Unit tests for new functions
- Integration tests for modified workflows
- Regression tests for existing functionality
- Performance tests for critical paths
### Test Categories
#### Unit Tests
- Test individual functions and methods
- Mock external dependencies
- Cover error conditions and edge cases
- Aim for >90% code coverage on new code
#### Integration Tests
- Test component interactions
- Use real file operations in test environment
- Test HTTP endpoints end-to-end
- Verify existing functionality unchanged
#### Regression Tests
- Ensure existing XML endpoints work
- Verify device discovery still functions
- Check migration compatibility
- Test WebSocket event processing
### Continuous Quality Checks
#### Pre-commit Checks
```bash
# Before each commit
golangci-lint run --fix
go test ./...
go test -race ./...
```
#### Milestone Validation
```bash
# Before marking milestone complete
golangci-lint run --fix
go test ./... -v
go test -race ./... -v
go test ./... -bench=.
```
#### Integration Validation
```bash
# Test with real soundtouch-service
make build
./soundtouch-service &
# Run integration test suite
make integration-test
```
## Risk Mitigation
### Backward Compatibility
- All existing APIs must continue working
- File structure changes must be additive
- Configuration changes must have defaults
- Migration paths for existing data
### Rollback Strategy
- Each step can be independently reverted
- Configuration flags for new features
- Graceful degradation when features disabled
- Clear rollback documentation
### Performance Impact
- Monitor memory usage during development
- Profile critical paths before and after changes
- Set performance regression alerts
- Simple before complex solutions
## Documentation Requirements
### Code Documentation
- Comprehensive godoc comments
- Example usage in comments
- Error conditions documented
- Performance characteristics noted
### User Documentation
- Update existing guides for new features
- Add migration guides for new functionality
- Create troubleshooting documentation
- Update API documentation
### Development Documentation
- Architecture decision records
- Testing strategy documentation
- Deployment and rollback procedures
- Performance benchmarking results
## Success Criteria
### Technical Metrics
- All tests pass consistently
- No linting issues
- Memory usage increase <50MB
- Response time degradation <10%
### Functional Metrics
- All existing functionality preserved
- New account management works reliably
- Device lifecycle tracking is accurate
- Enhanced monitoring provides value
### Quality Metrics
- Code coverage maintained >85%
- No critical security issues
- Documentation completeness >95%
- Community feedback positive
This implementation plan ensures steady, reliable progress while maintaining the quality and simplicity principles essential for the project's success.
+403
View File
@@ -0,0 +1,403 @@
# Implementation Roadmap for Upstream Service Simulation
## Overview
This document provides a detailed implementation roadmap for the upstream Bose service simulation concept. It breaks down the implementation into manageable phases with specific deliverables, technical requirements, and integration points.
## Phase 1: Foundation and Enhanced State Tracking (4-6 weeks)
### Milestone 1.1: Account Management Service (1-2 weeks)
#### Deliverables
- `pkg/service/account/` package with core account management
- Account creation, retrieval, and status management APIs
- Text-based account persistence in JSON format
- Integration with existing datastore structure
#### Implementation Tasks
1. **Create Account Manager**
```
pkg/service/account/
├── account.go # Core account management
├── manager.go # Account manager implementation
├── persistence.go # File-based persistence
└── account_test.go # Comprehensive tests
```
2. **Account Data Structure**
- JSON-based account metadata storage
- Integration with existing `data/accounts/{id}/` structure
- Account status tracking (active, migrating, suspended)
- Migration metadata tracking
3. **API Integration**
- Add account management endpoints to existing HTTP router
- RESTful API alongside existing XML endpoints
- Account creation validation and error handling
#### Technical Requirements
- Maintain backward compatibility with existing account structure
- Thread-safe account operations
- Atomic file operations for account metadata
- Comprehensive error handling and logging
### Milestone 1.2: Device Lifecycle Manager (2-3 weeks)
#### Deliverables
- `pkg/service/lifecycle/` package for device state management
- Device state machine with comprehensive state tracking
- Event-driven state transitions
- Integration with existing device discovery and migration
#### Implementation Tasks
1. **Lifecycle Core**
```
pkg/service/lifecycle/
├── lifecycle.go # Device lifecycle management
├── states.go # State definitions and transitions
├── events.go # Event processing
├── persistence.go # Lifecycle persistence
└── lifecycle_test.go # State machine tests
```
2. **State Machine Implementation**
- Define device states: unregistered → registering → active → migrating → offline → retired
- Implement state transition rules and validation
- Event-driven state changes with history tracking
- Integration with existing migration system
3. **Event Processing**
- Asynchronous event queue for device events
- Event categorization and filtering
- Text-based event logging with structured format
- Event replay capabilities for debugging
#### Technical Requirements
- Non-blocking event processing
- Persistent state across service restarts
- Integration with existing WebSocket event system
- Memory-efficient event storage
### Milestone 1.3: Enhanced Mirror System (1-2 weeks)
#### Deliverables
- Extended mirroring with disparity detection
- Parity analysis logging and reporting
- Selective data source switching
- Integration with existing mirror middleware
#### Implementation Tasks
1. **Disparity Detection**
```
pkg/service/mirror/
├── disparity.go # Disparity detection logic
├── analyzer.go # Response analysis and comparison
├── logger.go # Structured disparity logging
└── disparity_test.go # Analysis tests
```
2. **Enhanced Mirror Middleware**
- Extend existing mirror functionality
- Add response comparison and hash calculation
- Structured logging of disparities
- Configurable disparity sensitivity
3. **Data Source Management**
- Smart routing between local and upstream sources
- Per-endpoint source preference configuration
- Fallback mechanisms for upstream unavailability
- Source switching with history tracking
#### Technical Requirements
- Minimal performance impact on request processing
- Configurable disparity detection sensitivity
- Structured logging for analysis tools
- Integration with existing mirror configuration
## Phase 2: Migration and Dual-Source Management (3-4 weeks)
### Milestone 2.1: Migration Controller (2-3 weeks)
#### Deliverables
- Device-by-device migration orchestration
- Migration progress tracking and status reporting
- Rollback capabilities with state preservation
- Integration with existing setup manager
#### Implementation Tasks
1. **Migration Orchestration**
```
pkg/service/migration/
├── controller.go # Migration orchestration
├── strategy.go # Migration strategies
├── rollback.go # Rollback functionality
├── progress.go # Progress tracking
└── migration_integration_test.go
```
2. **Migration Strategies**
- Fresh device registration flow
- Bose account data migration flow
- Gradual migration with dual-source support
- Emergency migration for service outages
3. **Progress Tracking**
- Real-time migration status updates
- Migration timeline and milestone tracking
- Error handling and recovery procedures
- Migration completion verification
#### Technical Requirements
- Integration with existing migration system
- Atomic migration operations with rollback
- Progress persistence across service restarts
- Comprehensive migration logging
### Milestone 2.2: Dual-Source Data Management (1-2 weeks)
#### Deliverables
- Smart data routing between local and upstream sources
- Graceful fallback mechanisms
- Data source preference management
- Conflict resolution strategies
#### Implementation Tasks
1. **Data Source Router**
```
pkg/service/datasource/
├── router.go # Smart routing logic
├── preferences.go # Source preference management
├── fallback.go # Fallback mechanisms
└── conflict.go # Conflict resolution
```
2. **Source Management**
- Per-device, per-endpoint source preferences
- Dynamic source switching based on availability
- Conflict detection and resolution
- Source health monitoring
3. **Integration Points**
- Marge service integration for account data
- BMX service integration for content data
- Preset and recent management integration
- Source configuration management
#### Technical Requirements
- Zero-downtime source switching
- Conflict resolution without data loss
- Health check integration
- Performance monitoring and metrics
## Phase 3: Advanced Features and Analytics (2-3 weeks)
### Milestone 3.1: System Monitoring and Health Checks (1-2 weeks)
#### Deliverables
- Comprehensive system health monitoring
- Device connectivity and availability tracking
- Performance metrics collection
- Health check endpoints and dashboards
#### Implementation Tasks
1. **Health Monitoring**
```
pkg/service/health/
├── monitor.go # System health monitoring
├── metrics.go # Performance metrics
├── connectivity.go # Device connectivity tracking
└── alerts.go # Health alerting
```
2. **Metrics Collection**
- Device availability tracking
- Response time monitoring
- Error rate tracking
- Migration success rates
3. **Dashboard Integration**
- Health status endpoints
- Metrics export for monitoring tools
- Real-time status updates
- Historical trend analysis
#### Technical Requirements
- Minimal performance overhead
- Configurable monitoring intervals
- Integration with existing health checks
- Memory-efficient metrics storage
### Milestone 3.2: Data Export and Backup (1 week)
#### Deliverables
- Account data export functionality
- Incremental backup strategies
- Data integrity verification
- Migration-ready data formats
#### Implementation Tasks
1. **Export Functionality**
```
pkg/service/export/
├── exporter.go # Data export logic
├── formats.go # Export format definitions
├── validation.go # Data integrity checks
└── backup.go # Backup strategies
```
2. **Backup Management**
- Incremental backup creation
- Backup validation and verification
- Automated backup scheduling
- Restore functionality
3. **Data Formats**
- Migration-ready JSON exports
- XML compatibility for device imports
- Compressed archive support
- Selective export capabilities
#### Technical Requirements
- Consistent data export across all account types
- Backup integrity verification
- Configurable export scheduling
- Resource-efficient backup operations
## Integration Strategy
### Existing Service Integration Points
#### 1. Datastore Integration
- Extend existing datastore with lifecycle and account management
- Maintain backward compatibility with current file structure
- Add new persistence methods for enhanced state tracking
- Implement migration for existing data to new formats
#### 2. Handler Integration
- Integrate account management into existing HTTP handlers
- Add lifecycle information to device responses
- Extend mirror middleware with disparity detection
- Add new management endpoints alongside existing XML APIs
#### 3. Discovery Integration
- Link device discovery to lifecycle state transitions
- Integrate migration triggers with discovery events
- Add account association during discovery
- Maintain existing discovery functionality
#### 4. Migration System Integration
- Extend existing migration manager with new capabilities
- Integrate lifecycle management with device migrations
- Add rollback functionality to existing migration flows
- Maintain compatibility with current migration methods
### Configuration Management
#### New Configuration Options
```yaml
accounts:
auto_create: false
mirror_enhanced_creation: true
default_migration_strategy: "gradual"
lifecycle:
event_retention_days: 30
state_transition_timeout: "5m"
async_processing: true
mirror:
disparity_detection: true
disparity_sensitivity: "medium"
source_switching_enabled: true
fallback_timeout: "10s"
migration:
batch_size: 1
progress_reporting: true
rollback_enabled: true
verification_required: true
```
### Performance Considerations
#### Resource Usage
- Target: <100MB additional memory usage on Raspberry Pi Zero 2W
- CPU usage: <5% additional overhead during normal operations
- Storage: Text-based logs with configurable rotation
- Network: Minimal additional upstream requests
#### Optimization Strategies
- Lazy loading of historical data
- Configurable log retention policies
- Memory-efficient event processing
- Background cleanup processes
- Efficient file I/O operations
## Testing Strategy
### Unit Testing
- Comprehensive test coverage for all new packages
- State machine transition testing
- Data persistence and integrity tests
- Mock integration tests for external dependencies
### Integration Testing
- End-to-end migration flow testing
- Multi-device scenario testing
- Disparity detection accuracy testing
- Performance impact testing
### Compatibility Testing
- Backward compatibility with existing installations
- Device compatibility across SoundTouch models
- Migration from various existing configurations
- Stress testing with multiple concurrent devices
## Deployment Strategy
### Rollout Plan
1. **Alpha Release**: Core functionality with limited device support
2. **Beta Release**: Full feature set with extensive testing
3. **Stable Release**: Production-ready with documentation
### Migration Path
1. Existing installations can upgrade incrementally
2. New features are opt-in with configuration flags
3. Existing data structures are preserved and extended
4. Rollback capability for critical issues
### Documentation Requirements
- Updated API documentation with new endpoints
- Migration guide for existing users
- Configuration reference for new options
- Troubleshooting guide for common issues
## Risk Mitigation
### Technical Risks
- **Data Loss**: Atomic operations and rollback capabilities
- **Performance Impact**: Gradual rollout and monitoring
- **Compatibility Issues**: Comprehensive testing and fallback options
- **Resource Constraints**: Efficient algorithms and configurable limits
### Operational Risks
- **Service Disruption**: Zero-downtime deployment strategies
- **Configuration Complexity**: Sensible defaults and validation
- **User Adoption**: Clear documentation and migration assistance
- **Support Burden**: Comprehensive logging and diagnostic tools
## Success Metrics
### Technical Metrics
- Migration success rate >95%
- Disparity detection accuracy >90%
- Performance overhead <5%
- System availability >99.5%
### User Experience Metrics
- Reduced support requests
- Improved device reliability
- Faster problem resolution
- Enhanced system visibility
This roadmap provides a structured approach to implementing the upstream service simulation concept while maintaining compatibility with existing deployments and ensuring smooth migration paths for users.
+137
View File
@@ -0,0 +1,137 @@
# Spotify OAuth Integration
The SoundTouch service supports Spotify OAuth integration to broker access tokens for SoundTouch speakers. This is particularly useful for maintaining Spotify Connect functionality after the Bose cloud shutdown (scheduled for May 2026).
## OAuth Flows
The service supports two primary OAuth flows: a browser-based flow and a mobile app-based flow (specifically for the [ueberboese](https://github.com/julius-d/ueberboese-app) app).
### 1. Browser-based Flow
The user initiates the flow, completes authorization in their browser, and is redirected back to the service.
```mermaid
sequenceDiagram
participant Client as Client (curl/app)
participant Service as Service
participant Spotify as Spotify Auth Server
participant Browser as User's Browser
Client->>Service: POST /mgmt/spotify/init [Basic Auth]
Service-->>Client: {"redirectUrl": "https://accounts.spotify.com/authorize?..."}
Client->>Browser: User opens URL
Browser->>Spotify: User logs in & grants access
Spotify-->>Browser: Redirect to /mgmt/spotify/callback?code=abc
Browser->>Service: GET /mgmt/spotify/callback?code=abc
Note over Service: No auth needed for callback
Service->>Spotify: POST /api/token (exchange code)
Spotify-->>Service: {access_token, refresh_token}
Service->>Spotify: GET /v1/me (fetch profile)
Spotify-->>Service: {id, display_name, email}
Note over Service: Store account to disk
Service-->>Browser: HTML: "Spotify Connected. You can close this window."
```
### 2. Mobile App Flow (ueberboese)
The mobile app handles the redirect via a deep link and then confirms the authorization with the service.
```mermaid
sequenceDiagram
participant App as ueberboese Flutter App
participant Service as Service
participant Spotify as Spotify Auth Server
App->>Service: POST /mgmt/spotify/init [Basic Auth]
Service-->>App: {"redirectUrl": "https://..."}
App->>Spotify: Open in-app browser (User authorizes)
Spotify-->>App: Deep link redirect: ueberboese-login://spotify?code=abc
App->>Service: POST /mgmt/spotify/confirm?code=abc [Basic Auth]
Service->>Spotify: POST /api/token (exchange code)
Spotify-->>Service: {access_token, refresh_token}
Service->>Spotify: GET /v1/me (fetch profile)
Spotify-->>Service: {profile}
Service-->>App: {"ok": true}
```
### 3. Token Retrieval (Boot Primer / Speaker Setup)
Once an account is linked, access tokens can be retrieved for use with speakers (e.g., via the `addUser` ZeroConf command).
```mermaid
sequenceDiagram
participant Primer as Boot Primer Script
participant Service as Service
participant Spotify as Spotify Token API
participant Speaker as Speaker (Bose ST 20)
Primer->>Service: GET /mgmt/spotify/token [Basic Auth]
alt Token expired
Service->>Spotify: POST /api/token (refresh)
Spotify-->>Service: new tokens
end
Service-->>Primer: {"access_token": "...", "username": "..."}
Note over Primer: Spotify Connect ZeroConf
Primer->>Speaker: POST /SpotifyConnect (addUser with token)
Speaker-->>Primer: OK
Note over Speaker: Speaker now has Spotify access
```
## Boot Primer Script
A boot primer script that uses these endpoints to feed Spotify tokens to speakers via ZeroConf is available in the `scripts/spotify/` directory: [spotify-boot-primer.sh](../../scripts/spotify/spotify-boot-primer.sh).
This script can be installed on the speaker itself (which runs embedded Linux) to automatically prime Spotify Connect at boot time. See [README.md](../../scripts/spotify/README.md) and [INSTALL.md](../../scripts/spotify/INSTALL.md) for instructions.
### Automated Installation via Service
The SoundTouch service provides a dedicated management endpoint to automatically handle the installation of the Spotify boot primer on the speaker:
`POST /mgmt/devices/{deviceId}/spotify/install-primer`
### Automated Installation Steps
When you run the Spotify primer installation, the service performs the following:
1. **Directories**: Creates `/mnt/nv/bin` and `/mnt/nv/BoseApp-Persistence/1` on the speaker.
2. **Binary**: Uploads the `spotify-boot-primer` script to the speaker.
3. **Configuration**: Automatically generates and uploads `spotify-primer.conf` containing the service's URL and management credentials.
4. **Boot Hook**: Injects a call to the primer in the speaker's `/mnt/nv/rc.local` using idempotent markers.
5. **Environment**: Updates `/mnt/nv/.profile` to include `/mnt/nv/bin` in the `PATH` for easier manual troubleshooting via SSH.
- **Idempotent Patching**: The service uses explicit markers to inject the hook, ensuring it doesn't corrupt existing content.
- **Coexistence**: The service-injected hook is designed to coexist with a manually installed `rc.local` (e.g., from the community gist). It only adds a call to `/mnt/nv/bin/spotify-boot-primer` if it's not already managed by a service-controlled block.
- **Markers**: Look for the following markers in your speaker's `/mnt/nv/rc.local`:
- `# --- Aftertouch Spotify hook START ---`
- `# --- Aftertouch Spotify hook END ---`
- **Cleanup**: Reverting a migration via the service will cleanly remove these marker-delimited blocks.
## Endpoints
| Method | Path | Auth | Purpose |
|--------|---------------------------------------------------|-------|-----------------------------------------------------------------------|
| POST | `/mgmt/devices/{deviceId}/spotify/install-primer` | Basic | Install Spotify boot primer on speaker (deviceId or IP) |
| GET | `/mgmt/spotify/callback` | None | Browser OAuth callback (redirect from Spotify, returns HTML) |
| POST | `/mgmt/spotify/init` | Basic | Start OAuth flow, returns authorization URL |
| POST | `/mgmt/spotify/confirm` | Basic | Mobile app confirm (ueberboese deep link delivers code, returns JSON) |
| GET | `/mgmt/spotify/accounts` | Basic | List linked Spotify accounts (tokens stripped) |
| GET | `/mgmt/spotify/token` | Basic | Get fresh access token (auto-refreshes if expired) |
| POST | `/mgmt/spotify/entity` | Basic | Resolve Spotify URI to name + image URL |
## Security
- `/mgmt/spotify/callback` is intentionally outside Basic Auth to allow direct redirects from Spotify's authorization server.
- All other `/mgmt/*` endpoints require Basic Auth as configured by `--mgmt-username` and `--mgmt-password`.
- Tokens are persisted to disk as JSON with restricted file permissions (`0600`).
- The `GetAccounts` endpoint strips sensitive tokens from the response.
+84
View File
@@ -0,0 +1,84 @@
# Spotify Priming Strategy
This document outlines the strategy for ensuring Bose SoundTouch devices are correctly "primed" for Spotify Connect integration within the AfterTouch ecosystem.
## Overview
To enable Spotify Connect for SoundTouch devices, especially for remote availability outside the local network, the speaker must be associated with a Spotify account via a process called "priming." This involves sending an `addUser` command to the speaker's ZeroConf API (port 8200) containing a valid Spotify username and OAuth access token.
AfterTouch adopts a **Server-Centric Hybrid Model** that prioritizes device cleanliness and user intent while providing automated self-healing.
## Core Principles
### 1. User Intent (Opt-in)
AfterTouch replicates the native Bose "Add Source" experience. No Spotify priming occurs until a user explicitly links their Spotify account through the AfterTouch Management Dashboard. This ensures privacy and respects users who do not wish to use Spotify.
### 2. Device Cleanliness (Minimalist Footprint)
We avoid invasive modifications to the speaker's filesystem.
- **No On-Device Scripts:** We deprecate the use of internal boot-primer scripts.
- **Native Communication:** We rely on the speaker's native ability to talk to Bose services, which are intercepted via DNS to point to the AfterTouch server.
### 3. Triggers for Priming
Priming is triggered when the speaker signals it is active and ready, specifically:
- **Power On:** When the speaker calls the `/marge/streaming/support/power_on` endpoint, AfterTouch ensures the device's ZeroConf state is correctly primed. This is the primary trigger.
- **Manual Override:** Users can manually trigger a "Prime Spotify" from the device list in the UI if needed.
During any of these events, the server:
1. Checks if a Spotify account is linked in AfterTouch.
2. Checks the device's current priming status (via ZeroConf).
3. If unprimed and an account is linked, it pushes the priming command.
### 4. Automated Recovery
AfterTouch ensures that if a speaker loses its session (due to a crash or power loss), it is re-primed when it next powers on and reaches out to the service.
### 5. Decoupling
The logic for account management and device interaction remains decoupled:
- **Spotify Service:** Manages OAuth tokens and account state.
- **Discovery Service:** Finds devices and tracks their network presence.
- **Orchestrator:** Connects the two, deciding when to push tokens to discovered devices based on the current link status.
## Workflow
### Initial Setup (The "Add Source" UX)
1. User opens the AfterTouch Dashboard.
2. User selects "Link Spotify Account."
3. OAuth flow completes; AfterTouch stores the token.
4. AfterTouch immediately triggers a discovery run to find and prime all compatible speakers.
### Maintenance (The "Watchdog" UX)
1. A speaker reboots or loses its token.
2. A discovery event occurs (periodic or triggered by UI).
3. AfterTouch detects the "Empty" user state on the speaker.
4. AfterTouch pushes a fresh token from the Spotify Service.
5. UI reflects that the device is "Managed by AfterTouch" and healthy.
### Manual Override
Users can manually trigger a "Re-prime" or "Refresh Link" from the device list in the UI if they suspect the automated self-healing is delayed or if they want to force a specific account onto a device.
## Network Topology & Deployment Scenarios
The strategy adapts based on where the AfterTouch server is deployed:
### Local Deployment (Home Server / Docker)
- **Mechanism:** Both "Pull" (Marge) and "Push" (ZeroConf side-channel) are used.
- **Advantage:** The server can proactively fix the speaker's state via port 8200 as soon as it sees a "Liveness Signal."
### External Deployment (Cloud VPS)
- **Mechanism:** Primarily relies on "Pull" (Marge).
- **Constraint:** The server cannot reach port 8200 on the speaker due to NAT/Firewall.
- **Strategy:** In this scenario, AfterTouch acts as a passive token provider. The speaker must initiate the connection to our intercepted Bose endpoints to receive its Spotify configuration. If the speaker completely loses its user state and stops "pulling," a manual re-prime from a local machine or a temporary local discovery run might be required.
## Transition & Cleanup
As AfterTouch moves to the Server-Centric model, we will:
1. **Revert On-Device Migration:** Update the Setup Manager to remove legacy `spotify-boot-primer` scripts and `rc.local` hooks from the speakers.
2. **Consolidated Directory:** We maintain the `/mnt/nv/soundtouch-service/` base directory for other configuration needs (e.g., `aftertouch.resolv.conf`), but it will no longer contain Spotify-specific credentials or scripts.
3. **No On-Device Credentials:** The `/mnt/nv/soundtouch-service/spotify-primer.conf` will be removed, ensuring that no sensitive AfterTouch login details are stored on the speaker in plain text.
## Implementation Roadmap (Conceptual)
1. **Revert On-Device Migration:** Update the Setup Manager to remove legacy scripts and `rc.local` hooks.
2. **Server-Side Priming Logic:** Implement a `PrimeDevice(ip)` method in the server that fetches a fresh token and calls the ZeroConf API.
3. **Discovery Hook:** Integrate `PrimeDevice` into the discovery handler (`handleDiscoveredDevice`) with a check for unprimed state.
4. **UI Enhancements:** Update the Speaker List to show "Spotify Linked" status and provide manual refresh buttons.
+989
View File
@@ -0,0 +1,989 @@
# Technical Specification - Enhanced State Management System
## Table of Contents
1. [System Architecture](#system-architecture)
2. [Data Models](#data-models)
3. [API Specifications](#api-specifications)
4. [File Format Specifications](#file-format-specifications)
5. [State Machine Definitions](#state-machine-definitions)
6. [Event Processing](#event-processing)
7. [Performance Requirements](#performance-requirements)
8. [Security Considerations](#security-considerations)
9. [Error Handling](#error-handling)
10. [Monitoring and Observability](#monitoring-and-observability)
## System Architecture
### Component Overview
```
┌─────────────────────────────────────────────────────────────┐
│ SoundTouch Service │
├─────────────────────────────────────────────────────────────┤
│ HTTP Router & Middleware │
│ ├── Mirror Middleware (Enhanced) │
│ ├── Recorder Middleware │
│ ├── Disparity Detection │
│ └── Health Check Middleware │
├─────────────────────────────────────────────────────────────┤
│ Service Layer │
│ ├── Account Manager ├── Lifecycle Manager │
│ ├── Migration Controller ├── Data Source Router │
│ ├── Event Processor ├── Health Monitor │
│ └── Export Manager └── Analytics Engine │
├─────────────────────────────────────────────────────────────┤
│ Data Layer │
│ ├── Enhanced DataStore ├── Event Store │
│ ├── Mirror Cache ├── Metrics Store │
│ └── Configuration Store └── Session Store │
├─────────────────────────────────────────────────────────────┤
│ External Integrations │
│ ├── Bose Services (Mirror) ├── Device Discovery │
│ ├── BMX/TuneIn Services └── SSH/Setup Manager │
└─────────────────────────────────────────────────────────────┘
```
### Package Structure
```
pkg/service/
├── account/ # Account management
│ ├── manager.go
│ ├── persistence.go
│ └── validation.go
├── lifecycle/ # Device lifecycle management
│ ├── manager.go
│ ├── states.go
│ ├── transitions.go
│ └── events.go
├── migration/ # Enhanced migration (extends existing)
│ ├── controller.go
│ ├── strategies.go
│ └── progress.go
├── datasource/ # Data source routing
│ ├── router.go
│ ├── preferences.go
│ └── fallback.go
├── events/ # Event processing system
│ ├── processor.go
│ ├── queue.go
│ └── storage.go
├── mirror/ # Enhanced mirroring (extends existing)
│ ├── disparity.go
│ ├── analyzer.go
│ └── logger.go
├── health/ # System monitoring
│ ├── monitor.go
│ ├── metrics.go
│ └── alerts.go
└── export/ # Data export and backup
├── exporter.go
├── formats.go
└── backup.go
```
## Data Models
### Account Model
```go
type Account struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Status AccountStatus `json:"status"`
DeviceCount int `json:"device_count"`
MigrationInfo *MigrationInfo `json:"migration_info,omitempty"`
BoseAccountID string `json:"bose_account_id,omitempty"`
DataSources DataSourceConfig `json:"data_sources"`
Settings AccountSettings `json:"settings"`
}
type AccountStatus string
const (
AccountStatusActive AccountStatus = "active"
AccountStatusMigrating AccountStatus = "migrating"
AccountStatusSuspended AccountStatus = "suspended"
AccountStatusArchived AccountStatus = "archived"
)
type MigrationInfo struct {
StartedAt time.Time `json:"started_at"`
CompletedAt *time.Time `json:"completed_at,omitempty"`
DevicesMigrated int `json:"devices_migrated"`
DevicesPending int `json:"devices_pending"`
MirrorActive bool `json:"mirror_active"`
Strategy string `json:"strategy"`
RollbackData string `json:"rollback_data,omitempty"`
}
type DataSourceConfig struct {
Local bool `json:"local"`
BoseMirror bool `json:"bose_mirror"`
Primary string `json:"primary"` // "local" or "bose"
}
type AccountSettings struct {
AutoMigration bool `json:"auto_migration"`
MirrorEndpoints []string `json:"mirror_endpoints"`
RetentionDays int `json:"retention_days"`
}
```
### Device Lifecycle Model
```go
type DeviceLifecycle struct {
DeviceID string `json:"device_id"`
AccountID string `json:"account_id"`
State DeviceState `json:"state"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
StateHistory []StateTransition `json:"state_history"`
Metadata DeviceMetadata `json:"metadata"`
DataSources DataSourceConfig `json:"data_sources"`
Migration *DeviceMigration `json:"migration,omitempty"`
Health DeviceHealth `json:"health"`
}
type DeviceState string
const (
DeviceStateUnregistered DeviceState = "unregistered"
DeviceStateDiscovered DeviceState = "discovered"
DeviceStateRegistering DeviceState = "registering"
DeviceStateActive DeviceState = "active"
DeviceStateMigrating DeviceState = "migrating"
DeviceStateOffline DeviceState = "offline"
DeviceStateError DeviceState = "error"
DeviceStateRetired DeviceState = "retired"
)
type StateTransition struct {
From DeviceState `json:"from"`
To DeviceState `json:"to"`
Timestamp time.Time `json:"timestamp"`
Reason string `json:"reason"`
Source string `json:"source"`
Context map[string]interface{} `json:"context,omitempty"`
}
type DeviceMetadata struct {
Name string `json:"name"`
Type string `json:"type"`
SerialNumber string `json:"serial_number"`
FirmwareVersion string `json:"firmware_version"`
MACAddress string `json:"mac_address"`
IPAddress string `json:"ip_address"`
LastSeen time.Time `json:"last_seen"`
IsLegacyID bool `json:"is_legacy_id"`
Capabilities []string `json:"capabilities,omitempty"`
}
type DeviceMigration struct {
FromBoseAccount string `json:"from_bose_account,omitempty"`
MigratedAt *time.Time `json:"migrated_at,omitempty"`
Method string `json:"method"`
RollbackAvailable bool `json:"rollback_available"`
DataPreserved []string `json:"data_preserved"`
}
type DeviceHealth struct {
Status string `json:"status"` // "healthy", "warning", "error"
LastCheck time.Time `json:"last_check"`
ResponseTime int `json:"response_time_ms"`
Connectivity string `json:"connectivity"` // "online", "offline", "intermittent"
ErrorCount int `json:"error_count"`
LastError string `json:"last_error,omitempty"`
}
```
### Event Model
```go
type DeviceEvent struct {
ID string `json:"id"`
DeviceID string `json:"device_id"`
AccountID string `json:"account_id"`
Type DeviceEventType `json:"type"`
Data map[string]interface{} `json:"data"`
Timestamp time.Time `json:"timestamp"`
Source EventSource `json:"source"`
Processed bool `json:"processed"`
Context EventContext `json:"context"`
}
type DeviceEventType string
const (
EventTypeNowPlaying DeviceEventType = "now_playing"
EventTypePresetChanged DeviceEventType = "preset_changed"
EventTypeVolumeChanged DeviceEventType = "volume_changed"
EventTypeSourceChanged DeviceEventType = "source_changed"
EventTypeDeviceOnline DeviceEventType = "device_online"
EventTypeDeviceOffline DeviceEventType = "device_offline"
EventTypeZoneChanged DeviceEventType = "zone_changed"
EventTypeDisparityFound DeviceEventType = "disparity_found"
EventTypeMigrationStart DeviceEventType = "migration_start"
EventTypeMigrationEnd DeviceEventType = "migration_end"
EventTypeHealthCheck DeviceEventType = "health_check"
EventTypeErrorOccurred DeviceEventType = "error_occurred"
)
type EventSource string
const (
EventSourceWebSocket EventSource = "websocket"
EventSourceDiscovery EventSource = "discovery"
EventSourceMirror EventSource = "mirror"
EventSourceSystem EventSource = "system"
EventSourceAPI EventSource = "api"
EventSourceUser EventSource = "user"
)
type EventContext struct {
RequestID string `json:"request_id,omitempty"`
UserAgent string `json:"user_agent,omitempty"`
IPAddress string `json:"ip_address,omitempty"`
Endpoint string `json:"endpoint,omitempty"`
Additional map[string]interface{} `json:"additional,omitempty"`
}
```
### Disparity Model
```go
type Disparity struct {
ID string `json:"id"`
Timestamp time.Time `json:"timestamp"`
DeviceID string `json:"device_id"`
AccountID string `json:"account_id"`
Endpoint string `json:"endpoint"`
Type DisparityType `json:"type"`
Severity DisparitySeverity `json:"severity"`
LocalHash string `json:"local_hash"`
UpstreamHash string `json:"upstream_hash"`
Details DisparityDetails `json:"details"`
Context map[string]interface{} `json:"context"`
Resolved bool `json:"resolved"`
}
type DisparityType string
const (
DisparityTypeContentMismatch DisparityType = "content_mismatch"
DisparityTypeStructureDiff DisparityType = "structure_diff"
DisparityTypeTimestampFormat DisparityType = "timestamp_format"
DisparityTypeFieldMissing DisparityType = "field_missing"
DisparityTypeValueMismatch DisparityType = "value_mismatch"
DisparityTypeCountMismatch DisparityType = "count_mismatch"
)
type DisparitySeverity string
const (
DisparitySeverityLow DisparitySeverity = "low"
DisparitySeverityMedium DisparitySeverity = "medium"
DisparitySeverityHigh DisparitySeverity = "high"
DisparitySeverityCritical DisparitySeverity = "critical"
)
type DisparityDetails struct {
FieldPath string `json:"field_path"`
LocalValue interface{} `json:"local_value"`
UpstreamValue interface{} `json:"upstream_value"`
Description string `json:"description"`
}
```
## API Specifications
### Account Management APIs
#### Create Account
```http
POST /api/v1/accounts
Content-Type: application/json
{
"name": "User Account",
"email": "user@example.com",
"settings": {
"auto_migration": false,
"retention_days": 30
}
}
Response: 201 Created
{
"id": "acc_12345",
"name": "User Account",
"email": "user@example.com",
"created_at": "2024-01-20T10:00:00Z",
"status": "active",
"device_count": 0
}
```
#### Get Account
```http
GET /api/v1/accounts/{account_id}
Response: 200 OK
{
"id": "acc_12345",
"name": "User Account",
"status": "active",
"device_count": 2,
"migration_info": {
"started_at": "2024-01-18T09:00:00Z",
"devices_migrated": 1,
"devices_pending": 1,
"mirror_active": true
},
"data_sources": {
"local": true,
"bose_mirror": true,
"primary": "bose"
}
}
```
#### List Accounts
```http
GET /api/v1/accounts?status=active&limit=10&offset=0
Response: 200 OK
{
"accounts": [...],
"total": 5,
"limit": 10,
"offset": 0
}
```
### Device Lifecycle APIs
#### Register Device
```http
POST /api/v1/accounts/{account_id}/devices
Content-Type: application/json
{
"device_id": "A81B6A536A98",
"name": "Living Room Speaker",
"registration_type": "fresh"
}
Response: 201 Created
{
"device_id": "A81B6A536A98",
"account_id": "acc_12345",
"state": "registering",
"created_at": "2024-01-20T10:00:00Z"
}
```
#### Get Device State
```http
GET /api/v1/accounts/{account_id}/devices/{device_id}/state
Response: 200 OK
{
"device_id": "A81B6A536A98",
"account_id": "acc_12345",
"state": "active",
"metadata": {
"name": "Living Room Speaker",
"type": "SoundTouch 30",
"last_seen": "2024-01-20T15:30:00Z"
},
"health": {
"status": "healthy",
"connectivity": "online",
"response_time": 45
}
}
```
#### Migrate Device
```http
POST /api/v1/accounts/{account_id}/devices/{device_id}/migrate
Content-Type: application/json
{
"from_bose_account": "bose-acc-xyz",
"preserve_data": true,
"method": "gradual"
}
Response: 202 Accepted
{
"migration_id": "mig_67890",
"status": "started",
"estimated_completion": "2024-01-20T11:00:00Z"
}
```
### Event APIs
#### Get Device Events
```http
GET /api/v1/accounts/{account_id}/devices/{device_id}/events?since=2024-01-20T00:00:00Z&type=now_playing&limit=50
Response: 200 OK
{
"events": [
{
"id": "evt_12345",
"type": "now_playing",
"timestamp": "2024-01-20T15:30:00Z",
"data": {
"source": "SPOTIFY",
"track": "Song Name",
"artist": "Artist Name"
}
}
],
"total": 125,
"has_more": true
}
```
#### Stream Events
```http
GET /api/v1/accounts/{account_id}/devices/{device_id}/events/stream
Accept: text/event-stream
Response: 200 OK
Content-Type: text/event-stream
data: {"id":"evt_12346","type":"volume_changed","timestamp":"2024-01-20T15:31:00Z","data":{"volume":50}}
data: {"id":"evt_12347","type":"now_playing","timestamp":"2024-01-20T15:32:00Z","data":{"source":"TUNEIN"}}
```
### Monitoring APIs
#### System Health
```http
GET /api/v1/system/health
Response: 200 OK
{
"status": "healthy",
"timestamp": "2024-01-20T15:30:00Z",
"services": {
"account_manager": "healthy",
"lifecycle_manager": "healthy",
"event_processor": "healthy",
"mirror_service": "warning"
},
"statistics": {
"total_accounts": 5,
"total_devices": 12,
"active_devices": 10,
"events_processed_24h": 1547
}
}
```
#### Disparity Analysis
```http
GET /api/v1/system/disparities?since=2024-01-20T00:00:00Z&severity=high
Response: 200 OK
{
"disparities": [
{
"id": "disp_12345",
"timestamp": "2024-01-20T14:30:00Z",
"endpoint": "/v1/presets",
"type": "count_mismatch",
"severity": "high",
"details": {
"field_path": "preset_count",
"local_value": 5,
"upstream_value": 4
}
}
],
"summary": {
"total": 15,
"by_severity": {
"high": 2,
"medium": 8,
"low": 5
}
}
}
```
## File Format Specifications
### Account Metadata (account.json)
```json
{
"version": "1.0",
"id": "acc_12345",
"name": "User Account",
"email": "user@example.com",
"created_at": "2024-01-20T10:00:00Z",
"updated_at": "2024-01-20T15:30:00Z",
"status": "active",
"device_count": 2,
"migration_info": {
"started_at": "2024-01-18T09:00:00Z",
"devices_migrated": 1,
"devices_pending": 1,
"mirror_active": true,
"strategy": "gradual"
},
"bose_account_id": "bose-original-id",
"data_sources": {
"local": true,
"bose_mirror": true,
"primary": "bose"
},
"settings": {
"auto_migration": false,
"mirror_endpoints": ["/v1/presets", "/v1/recents"],
"retention_days": 30
}
}
```
### Device Lifecycle (lifecycle.json)
```json
{
"version": "1.0",
"device_id": "A81B6A536A98",
"account_id": "acc_12345",
"state": "active",
"created_at": "2024-01-20T10:00:00Z",
"updated_at": "2024-01-20T15:30:00Z",
"state_history": [
{
"from": "unregistered",
"to": "discovered",
"timestamp": "2024-01-20T10:00:00Z",
"reason": "mdns_discovery",
"source": "discovery",
"context": {
"ip_address": "192.168.1.100",
"discovery_method": "mdns"
}
},
{
"from": "discovered",
"to": "active",
"timestamp": "2024-01-20T10:05:00Z",
"reason": "registration_complete",
"source": "system"
}
],
"metadata": {
"name": "Living Room Speaker",
"type": "SoundTouch 30",
"serial_number": "I6332527703739342000020",
"firmware_version": "4.8.1.25341.2677643.1597353330",
"mac_address": "A8:1B:6A:53:6A:98",
"ip_address": "192.168.1.100",
"last_seen": "2024-01-20T15:30:00Z",
"is_legacy_id": false,
"capabilities": ["multiroom", "bluetooth", "aux"]
},
"data_sources": {
"presets": "local",
"recents": "mirror_primary",
"sources": "local"
},
"migration": {
"from_bose_account": "bose-acc-xyz",
"migrated_at": "2024-01-18T14:30:00Z",
"method": "gradual",
"rollback_available": true,
"data_preserved": ["presets", "recents", "sources"]
},
"health": {
"status": "healthy",
"last_check": "2024-01-20T15:30:00Z",
"response_time": 45,
"connectivity": "online",
"error_count": 0
}
}
```
### Event Log Format (events.log)
```
# SoundTouch Service Event Log - Device A81B6A536A98
# Format: TIMESTAMP|EVENT_ID|EVENT_TYPE|SOURCE|DATA_JSON
# Version: 1.0
2024-01-20T15:30:00.123Z|evt_12345|now_playing|websocket|{"source":"SPOTIFY","track":"Song Name","artist":"Artist Name","album":"Album Name"}
2024-01-20T15:30:30.456Z|evt_12346|volume_changed|websocket|{"volume":45,"muted":false,"previous_volume":40}
2024-01-20T15:31:00.789Z|evt_12347|preset_selected|websocket|{"preset":1,"source":"SPOTIFY","location":"spotify:track:123abc"}
2024-01-20T15:31:15.012Z|evt_12348|disparity_detected|mirror|{"endpoint":"/v1/presets","local_hash":"abc123","upstream_hash":"def456","severity":"medium"}
2024-01-20T15:32:00.345Z|evt_12349|health_check|system|{"response_time":42,"status":"healthy","connectivity":"online"}
```
### Disparity Log Format (disparities.log)
```
# SoundTouch Service Disparity Log
# Format: TIMESTAMP|DISPARITY_ID|DEVICE_ID|ACCOUNT_ID|ENDPOINT|TYPE|SEVERITY|DETAILS_JSON
# Version: 1.0
2024-01-20T15:31:15.012Z|disp_12345|A81B6A536A98|acc_12345|/v1/presets|count_mismatch|medium|{"field_path":"preset_count","local_value":5,"upstream_value":4,"description":"Local has one additional preset"}
2024-01-20T15:32:45.678Z|disp_12346|A81B6A536A98|acc_12345|/v1/recents|timestamp_format|low|{"field_path":"recent[0].utc_time","local_value":"2024-01-20T15:30:00Z","upstream_value":"1705761000","description":"Timestamp format difference"}
2024-01-20T15:35:20.901Z|disp_12347|B92C7B647B09|acc_12345|/v1/account/full|structure_diff|high|{"field_path":"device[1].ip_address","local_value":"present","upstream_value":"missing","description":"IP address field missing in upstream response"}
```
## State Machine Definitions
### Device State Transitions
```
Unregistered → Discovered (via discovery)
Discovered → Registering (via user action/auto-registration)
Registering → Active (via successful registration)
Registering → Error (via registration failure)
Active → Migrating (via migration start)
Active → Offline (via connectivity loss)
Migrating → Active (via migration success)
Migrating → Error (via migration failure)
Offline → Active (via connectivity restored)
Error → Active (via error resolution)
Any State → Retired (via explicit retirement)
```
### State Transition Rules
```go
var StateTransitionRules = map[DeviceState][]DeviceState{
DeviceStateUnregistered: {DeviceStateDiscovered},
DeviceStateDiscovered: {DeviceStateRegistering, DeviceStateOffline},
DeviceStateRegistering: {DeviceStateActive, DeviceStateError},
DeviceStateActive: {DeviceStateMigrating, DeviceStateOffline, DeviceStateRetired},
DeviceStateMigrating: {DeviceStateActive, DeviceStateError},
DeviceStateOffline: {DeviceStateActive, DeviceStateError, DeviceStateRetired},
DeviceStateError: {DeviceStateActive, DeviceStateOffline, DeviceStateRetired},
DeviceStateRetired: {}, // Terminal state
}
```
### Transition Triggers
```go
type TransitionTrigger struct {
Event DeviceEventType
Condition func(*DeviceLifecycle, *DeviceEvent) bool
Target DeviceState
Reason string
}
var TransitionTriggers = []TransitionTrigger{
{
Event: EventTypeDeviceOnline,
Condition: isOfflineDevice,
Target: DeviceStateActive,
Reason: "connectivity_restored",
},
{
Event: EventTypeDeviceOffline,
Condition: isActiveDevice,
Target: DeviceStateOffline,
Reason: "connectivity_lost",
},
{
Event: EventTypeMigrationStart,
Condition: isActiveDevice,
Target: DeviceStateMigrating,
Reason: "migration_initiated",
},
// ... additional triggers
}
```
## Event Processing
### Event Queue Implementation
```go
type EventQueue struct {
buffer chan DeviceEvent
processors []EventProcessor
storage EventStorage
config EventQueueConfig
}
type EventQueueConfig struct {
BufferSize int `json:"buffer_size"`
ProcessorCount int `json:"processor_count"`
FlushInterval time.Duration `json:"flush_interval"`
RetryAttempts int `json:"retry_attempts"`
DeadLetterQueue bool `json:"dead_letter_queue"`
}
type EventProcessor interface {
ProcessEvent(event DeviceEvent) error
CanHandle(eventType DeviceEventType) bool
}
```
### Event Processing Flow
```
Event Input → Validation → Queue → Processing → Storage → Notification
↓ ↓ ↓ ↓ ↓ ↓
Websocket Schema Buffer Parallel Files Webhooks
Discovery Check Memory Workers Logs SSE
API Call Format Retry DB Metrics
System Enrich DLQ
```
### Event Retention Policy
```go
type RetentionPolicy struct {
EventType DeviceEventType `json:"event_type"`
RetentionDays int `json:"retention_days"`
MaxCount int `json:"max_count"`
Compression bool `json:"compression"`
}
var DefaultRetentionPolicies = []RetentionPolicy{
{EventTypeNowPlaying, 7, 1000, true},
{EventTypeVolumeChanged, 1, 100, false},
{EventTypeDisparityFound, 30, 10000, true},
{EventTypeMigrationStart, 365, -1, false}, // Keep forever
{EventTypeHealthCheck, 7, 1000, true},
}
```
### Development Requirements
#### KISS Principle (Keep It Simple, Stupid)
- Prioritize simplicity and readability over performance optimization
- Use standard Go idioms and patterns
- Avoid premature abstraction and optimization
- Build the simplest thing that works first
#### Quality Gates
Every change must pass these checks:
- `golangci-lint run --fix` - no linting issues
- `go test ./...` - all tests pass with no failures
- Integration tests verify existing functionality intact
- Code coverage maintained or improved
#### Testing Requirements
- Unit tests for all new functions
- Integration tests for modified workflows
- Regression tests for existing functionality
- Mock external dependencies appropriately
### Simplicity-First Performance Approach
| Aspect | Simple Approach | Optimization Only When Needed |
|--------|----------------|-------------------------------|
| Memory Usage | Direct file operations, minimal caching | Add caching if performance issues arise |
| CPU Usage | Synchronous processing initially | Add async processing if bottlenecks occur |
| Storage | Simple append operations | Add rotation/compression when files grow large |
| Networking | Reuse existing patterns | Optimize only if latency becomes problematic |
## Quality Assurance
### Testing Strategy
#### Unit Testing
```go
// Example test structure
func TestAccountManager_CreateAccount(t *testing.T) {
tests := []struct {
name string
input CreateAccountRequest
want *Account
wantErr bool
}{
{
name: "valid account creation",
input: CreateAccountRequest{Name: "Test Account"},
want: &Account{Name: "Test Account", Status: "active"},
wantErr: false,
},
{
name: "empty name should fail",
input: CreateAccountRequest{Name: ""},
want: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := manager.CreateAccount(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("CreateAccount() error = %v, wantErr %v", err, tt.wantErr)
return
}
// Additional assertions...
})
}
}
```
#### Integration Testing
- Test with real file operations in temporary directories
- Verify HTTP endpoints work with actual HTTP requests
- Test interaction with existing WebSocket system
- Ensure existing XML endpoints remain functional
#### Quality Gates
```bash
# Required before each commit
golangci-lint run --fix
go test ./...
go test -race ./...
# Required before milestone completion
go test ./... -v -cover
go test -bench=. ./...
```
### Security Considerations
#### Simple Security Model
- Reuse existing authentication mechanisms
- Basic input validation with standard Go validation
- Simple file permissions (0755 for directories, 0644 for files)
- No complex authorization initially - build incrementally
#### Input Validation
```go
// Simple validation approach
func ValidateAccount(account *Account) error {
if account.Name == "" {
return errors.New("account name cannot be empty")
}
if len(account.Name) > 100 {
return errors.New("account name too long")
}
if account.Email != "" && !isValidEmail(account.Email) {
return errors.New("invalid email format")
}
return nil
}
```
## Error Handling
### Error Categories
```go
type ErrorCategory string
const (
ErrorCategoryValidation ErrorCategory = "validation"
ErrorCategorySystem ErrorCategory = "system"
ErrorCategoryNetwork ErrorCategory = "network"
ErrorCategoryStorage ErrorCategory = "storage"
ErrorCategoryTimeout ErrorCategory = "timeout"
ErrorCategoryAuth ErrorCategory = "authentication"
)
type ServiceError struct {
Code string `json:"code"`
Message string `json:"message"`
Category ErrorCategory `json:"category"`
Timestamp time.Time `json:"timestamp"`
Context map[string]interface{} `json:"context"`
Retryable bool `json:"retryable"`
Severity string `json:"severity"`
}
```
### Error Recovery Strategies
1. **Transient Errors**: Exponential backoff retry (3 attempts)
2. **Storage Errors**: Graceful degradation with in-memory fallback
3. **Network Errors**: Circuit breaker pattern with fallback data
4. **Validation Errors**: Immediate response with detailed feedback
5. **System Errors**: Alerting and automatic recovery attempts
### Error Response Format
```json
{
"error": {
"code": "DEVICE_NOT_FOUND",
"message": "Device with ID 'A81B6A536A98' not found in account 'acc_12345'",
"category": "validation",
"timestamp": "2024-01-20T15:30:00Z",
"context": {
"account_id": "acc_12345",
"device_id": "A81B6A536A98",
"request_id": "req_67890"
},
"retryable": false,
"severity": "error"
},
"request_id": "req_67890"
}
```
## Monitoring and Observability
### Simple Monitoring Approach
#### Basic Health Check
```go
// Simple health check implementation
type HealthStatus struct {
Status string `json:"status"` // "healthy", "warning", "error"
Timestamp time.Time `json:"timestamp"`
Version string `json:"version"`
Uptime string `json:"uptime"`
}
func (s *Server) HandleHealthCheck(w http.ResponseWriter, r *http.Request) {
health := HealthStatus{
Status: "healthy",
Timestamp: time.Now(),
Version: version,
Uptime: time.Since(startTime).String(),
}
// Simple checks
if !s.canWriteToDataDir() {
health.Status = "error"
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(health)
}
```
#### Leverage Existing Systems
- Extend existing parity mismatch logging for disparity detection
- Reuse existing interaction recording for request/response tracking
- Build upon current discovery and migration event logging
- Use existing WebSocket event system for device state changes
#### Simple Metrics
```go
// Basic counters - no complex metrics initially
type SimpleMetrics struct {
AccountsCreated int `json:"accounts_created"`
DevicesActive int `json:"devices_active"`
EventsProcessed int `json:"events_processed_today"`
LastUpdate time.Time `json:"last_update"`
}
// Update metrics in simple text file
func (m *SimpleMetrics) Save(dataDir string) error {
data, err := json.MarshalIndent(m, "", " ")
if err != nil {
return err
}
return os.WriteFile(filepath.Join(dataDir, "metrics.json"), data, 0644)
}
```
This technical specification provides comprehensive details for implementing the enhanced state management system while maintaining compatibility with existing SoundTouch service functionality and meeting the performance requirements for small hardware deployments.
@@ -0,0 +1,393 @@
# Upstream Bose Service Simulation - State Management Concept
## Overview
This document outlines the concept for simulating and replacing upstream Bose services with enhanced state management capabilities. The goal is to create a comprehensive local replacement that can handle device lifecycles, account management, and state synchronization while maintaining compatibility with existing SoundTouch devices.
## Use Cases
### Case 0: Account Management
- **Explicit Account Creation**: Accounts must be created through deliberate action (web UI, API call)
- **Mirror-Enhanced Creation**: Account creation can be enriched using mirrored data from upstream Bose endpoints when devices make requests
- **Data Recording**: Passively record account information during normal device operations for future use
### Case 1a: Fresh Device Registration
- Initial setup/registration of a factory-reset or new device
- Device has no prior Bose account association
- Full local initialization with default configurations
### Case 1b: Device Migration from Bose Account
- Migrate existing registered device from Bose services to local management
- Preserve existing device data (presets, recents, sources)
- Support gradual migration while maintaining Bose compatibility
- Mirror Bose account data for seamless transition
### Case 2: Device Lifecycle and State Management
- Track and manage device lifecycle states and activities
- Maintain internal state based on incoming events from devices
- Detect disparities between local and upstream behavior
- Provide visibility into state changes and system health
## Architecture Principles
### 1. **Text-Based Storage for Debugging**
- Maintain all state in human-readable text formats (XML, JSON, plain text)
- Use small, focused files for each data aspect
- Enable easy debugging and manual inspection
- Optimize for small hardware deployments (Raspberry Pi Zero 2W)
### 2. **Mirror-First Strategy**
- Keep mirror functionality active as long as possible
- Primary source switches from upstream to local only during:
- Explicit migration
- Sufficient local data accumulation
- Upstream service unavailability
- Record and mirror as much data as possible, even if not immediately used
### 3. **Disparity Detection**
- Track differences between local and upstream responses
- Log discrepancies for analysis and improvement
- Provide visibility into implementation gaps
- Support parity testing and validation
### 4. **Event-Driven State Management**
- Process device events asynchronously
- Track comprehensive event history in text files
- Support event replay and analysis
- Minimize noise while capturing important state changes
## Enhanced Data Structure
### Account Management
```
data/
├── accounts/
│ ├── {account-id}/
│ │ ├── account.json # Account metadata
│ ├── account-events.log # High-level account behavior tracking
│ │ ├── devices/
│ │ │ └── {device-id}/
│ │ │ ├── lifecycle.json # Device state and history
│ │ │ ├── info.xml # Device information
│ │ │ ├── presets.xml # Device presets
│ │ │ ├── recents.xml # Recent plays
│ │ │ ├── sources.xml # Configured sources
│ │ │ └── events.log # Device event history
│ │ └── sessions/
│ │ └── {session-id}/ # Recorded interaction sessions
└── system/
├── discovery.log # Device discovery events
└── migration.log # Migration activities
```
### Account Metadata Format
```json
{
"id": "account-12345",
"name": "User Account",
"email": "user@example.com",
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-20T15:45:00Z",
"status": "active",
"device_count": 3,
"migration_status": {
"started_at": "2024-01-18T09:00:00Z",
"devices_migrated": 1,
"devices_pending": 2,
"mirror_active": true
},
"bose_account_id": "bose-original-id",
"data_sources": {
"local": true,
"bose_mirror": true,
"primary": "bose"
}
}
```
### Device Lifecycle Format
```json
{
"device_id": "A81B6A536A98",
"account_id": "account-12345",
"state": "active",
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-20T16:22:00Z",
"state_history": [
{
"from": "unregistered",
"to": "registering",
"timestamp": "2024-01-15T10:30:00Z",
"reason": "fresh_device_setup",
"source": "discovery"
},
{
"from": "registering",
"to": "active",
"timestamp": "2024-01-15T10:35:00Z",
"reason": "registration_complete",
"source": "system"
}
],
"metadata": {
"name": "Living Room Speaker",
"type": "SoundTouch 30",
"serial_number": "I6332527703739342000020",
"firmware_version": "4.8.1.25341.2677643.1597353330",
"mac_address": "A8:1B:6A:53:6A:98",
"ip_address": "192.168.1.100",
"last_seen": "2024-01-20T16:20:00Z",
"is_legacy_id": false
},
"data_sources": {
"presets": "local",
"recents": "mirror_primary",
"sources": "local"
},
"migration": {
"from_bose_account": "bose-account-xyz",
"migrated_at": "2024-01-18T14:30:00Z",
"method": "gradual",
"rollback_available": true
}
}
```
### Event Log Format
```
# Device Events Log - A81B6A536A98
# Format: TIMESTAMP|EVENT_TYPE|SOURCE|DATA
2024-01-20T16:15:00Z|now_playing|websocket|{"source":"SPOTIFY","track":"Song Name","artist":"Artist Name"}
2024-01-20T16:15:30Z|volume_changed|websocket|{"volume":45,"muted":false}
2024-01-20T16:16:00Z|preset_selected|websocket|{"preset":1,"source":"SPOTIFY","location":"spotify:track:123"}
2024-01-20T16:18:00Z|disparity_detected|mirror|{"endpoint":"/v1/account/full","local_hash":"abc123","upstream_hash":"def456"}
2024-01-20T16:20:00Z|device_online|discovery|{"ip":"192.168.1.100","method":"mdns"}
```
### Disparity Log Format
```
# Parity Analysis Log
# Format: TIMESTAMP|ENDPOINT|DEVICE|ACCOUNT|DISPARITY_TYPE|DETAILS
2024-01-20T16:18:00Z|/v1/account/full|A81B6A536A98|account-12345|content_mismatch|preset_count:local=5,upstream=4
2024-01-20T16:19:15Z|/v1/presets|A81B6A536A98|account-12345|xml_structure|missing_container_art_in_local
2024-01-20T16:20:30Z|/v1/recents|A81B6A536A98|account-12345|timestamp_format|local=RFC3339,upstream=custom
```
## Implementation Strategy
### Phase 1: Enhanced State Tracking
1. **Account Management Service**
- Explicit account creation API
- Mirror-enhanced account initialization
- Account status and migration tracking
2. **Device Lifecycle Manager**
- Comprehensive state machine for device lifecycle
- Event-driven state transitions
- Text-based state persistence
3. **Enhanced Mirror System**
- Extended mirroring with disparity detection
- Selective data source switching
- Parity analysis and logging
### Phase 2: Gradual Migration Support
1. **Migration Controller**
- Device-by-device migration orchestration
- Rollback capability with state preservation
- Migration progress tracking
2. **Dual-Source Data Management**
- Smart routing between local and upstream data
- Graceful fallback mechanisms
- Data source preference management
3. **State Synchronization**
- Bidirectional sync capabilities
- Conflict resolution strategies
- Sync status monitoring
### Phase 3: Advanced Analytics
1. **Disparity Analysis Engine**
- Automated disparity detection and classification
- Trend analysis and reporting
- Implementation gap identification
2. **System Health Monitoring**
- Device connectivity monitoring
- Service availability tracking
- Performance metrics collection
3. **Data Export and Backup**
- Account data export for migration
- Incremental backup strategies
- Data integrity verification
## API Enhancements
### Account Management APIs
```http
# Create account explicitly
POST /api/v1/accounts
Content-Type: application/json
{
"name": "User Account",
"email": "user@example.com"
}
# Get account with migration status
GET /api/v1/accounts/{account-id}
# Initiate account migration from Bose
POST /api/v1/accounts/{account-id}/migrate
Content-Type: application/json
{
"bose_account_id": "bose-original-id",
"strategy": "gradual"
}
```
### Device Lifecycle APIs
```http
# Register fresh device
POST /api/v1/accounts/{account-id}/devices
Content-Type: application/json
{
"device_id": "A81B6A536A98",
"name": "Living Room Speaker",
"registration_type": "fresh"
}
# Get device state and lifecycle
GET /api/v1/accounts/{account-id}/devices/{device-id}/state
# Migrate device from Bose account
POST /api/v1/accounts/{account-id}/devices/{device-id}/migrate
Content-Type: application/json
{
"from_bose_account": "bose-account-xyz",
"preserve_data": true
}
```
### Monitoring and Analysis APIs
```http
# Get disparity analysis
GET /api/v1/system/disparities?since=2024-01-20T00:00:00Z
# Get migration status
GET /api/v1/system/migration/status
# Export account data
GET /api/v1/accounts/{account-id}/export
```
## Integration with Existing Services
### Enhanced Marge Service
- Integrate lifecycle information into account responses
- Add migration status to device listings
- Support dual-source data routing
- Include disparity metadata in responses
### Enhanced BMX Service
- Track content source preferences by account
- Mirror and compare content recommendations
- Log streaming behavior for analysis
- Support gradual source migration
### Discovery Service Integration
- Link discovered devices to lifecycle manager
- Trigger lifecycle state transitions on discovery events
- Support both fresh registration and migration flows
- Handle legacy device ID migration automatically
## Performance Considerations
### Simplicity First (KISS Principle)
- Favor simple, readable code over premature optimization
- Use straightforward algorithms and data structures
- Minimize complexity in favor of maintainability
- Build incrementally with small, testable changes
### Quality Assurance
- Complete test coverage for all new functionality
- Comprehensive linting with `golangci-lint run --fix`
- Full test suite execution `go test ./...` for each milestone
- Integration tests with existing functionality
### File Management
- Simple line-based append operations for logs
- Basic log rotation when needed
- Direct file operations without complex caching
- Straightforward data persistence
## Development Principles
### KISS (Keep It Simple, Stupid)
- Prioritize simplicity and readability over performance optimization
- Use standard Go idioms and patterns
- Avoid premature abstraction and optimization
- Build the simplest thing that works first
### Quality First
- Every milestone must pass `golangci-lint run --fix` without issues
- Complete test suite must pass `go test ./...` before proceeding
- Integration tests ensure existing functionality remains intact
- Code coverage should be maintained or improved
### Incremental Development
- Make small, focused changes that can be easily reviewed
- Each step should be independently testable and valuable
- Maintain backward compatibility throughout development
- Enable rollback at any point in the process
### Leverage Existing Systems
- Reuse existing interaction recording for request/response tracking
- Build upon current parity mismatch detection system
- Extend existing datastore and handler patterns
- Integrate with established discovery and migration workflows
## Future Enhancements
Future improvements should maintain the simplicity-first approach:
1. **Enhanced Web Interface**
- Simple dashboard for account and device management
- Basic migration progress tracking
- Straightforward device health monitoring
2. **Extended Logging**
- Additional high-level behavior tracking
- Simple analytics based on existing parity data
- Enhanced debugging information
3. **Community Integration**
- Standardized data export formats
- Simple reporting mechanisms
- Clear documentation for community contributions
This concept provides a solid, maintainable foundation for replacing Bose's upstream services. The emphasis on simplicity, existing system reuse, and comprehensive testing ensures reliable functionality while maintaining the debugging capabilities needed for small hardware deployments.
@@ -0,0 +1,391 @@
# Device Lifecycle and /power_on Enhancement
## Overview
This document provides a comprehensive analysis of the current SoundTouch device registration and lifecycle management implementation, and proposes enhancements using the `/power_on` endpoint to reduce dependency on local network connectivity.
## Current Implementation Assessment
### Device Information Sources
The current system uses multiple data collection methods to build a complete device profile:
#### 1. UPnP/SSDP Discovery
- **Protocol**: Multicast UDP discovery for `urn:schemas-upnp-org:service:SoundTouch:1`
- **Network Scope**: Limited to same network segment
- **Data Collected**:
```go
type DiscoveredDevice struct {
Name string // From UPnP friendlyName
Host string // IP address
Port int // Usually 8090
ModelID string // From UPnP modelName
SerialNo string // MAC address from UPnP
UPnPLocation string // Device description URL
UPnPUSN string // Unique service name
}
```
#### 2. mDNS/Bonjour Discovery
- **Protocol**: Multicast DNS for `_soundtouch._tcp` services
- **Network Scope**: Limited to same network segment
- **Purpose**: Complements UPnP discovery with hostname resolution
#### 3. `/info` Endpoint Enrichment
- **Protocol**: HTTP GET to `http://device:8090/info`
- **Network Scope**: Requires direct connectivity to device
- **Data Collected**:
```xml
<info deviceID="ABCD1234EFGH">
<name>My SoundTouch Device</name>
<type>SoundTouch 10</type>
<margeAccountUUID>3230304</margeAccountUUID>
<components>
<component>
<componentCategory>SCM</componentCategory>
<softwareVersion>27.0.6.46330.5043500...</softwareVersion>
<serialNumber>I6332527703739342000020</serialNumber>
</component>
</components>
<margeURL>https://streaming.bose.com</margeURL>
<networkInfo type="SCM">
<macAddress>AA:BB:CC:DD:EE:FF</macAddress>
<ipAddress>192.168.1.10</ipAddress>
</networkInfo>
<moduleType>sm2</moduleType>
<variant>rhino</variant>
<countryCode>GB</countryCode>
</info>
```
### Current Data Flow
```mermaid
sequenceDiagram
participant Service as SoundTouch Service
participant UPnP as UPnP Discovery
participant mDNS as mDNS Discovery
participant Device as SoundTouch Device
participant DataStore as Data Store
participant User as User/App
Note over Service,User: Current Device Registration Flow
Service->>UPnP: Start SSDP Discovery
Service->>mDNS: Start mDNS Discovery
UPnP->>UPnP: Send M-SEARCH multicast
Device->>UPnP: Respond with location URL
UPnP->>Device: Fetch device description XML
Device->>UPnP: Return basic device info
mDNS->>mDNS: Query _soundtouch._tcp
Device->>mDNS: Respond with service info
Service->>Service: Merge discovery results
Service->>Device: GET /info (enrich data)
Device->>Service: Return detailed device info
Service->>DataStore: Store discovered device
Note over User,DataStore: User Registration
User->>Service: POST /account/{id}/devices
Note right of User: deviceId + user-friendly name
Service->>DataStore: Link device to account
Note over Service,DataStore: Migration Process
Service->>Device: GET /info (device identification)
Device->>Service: Return device details
Service->>Service: Build migration summary
Service->>Device: Apply configuration changes
```
### Device Registration Points
The system has distinct phases where device information is collected and enhanced:
#### Phase 1: Discovery (Network-Dependent)
**Trigger**: Automatic network scanning
**Data Sources**: UPnP + mDNS + `/info` endpoint
**Limitations**: ❌ Requires same network segment
#### Phase 2: User Registration (User-Controlled)
**Trigger**: User adds device to account
**Endpoint**: `POST /streaming/account/{accountId}/devices`
**Request Format**:
```xml
<device deviceid="08DF1F0BA325">
<name>Living Room Speaker</name>
</device>
```
**Data Added**: ✅ User-friendly name, Account association
#### Phase 3: Ongoing Updates (Mixed)
**Triggers**: Device state changes, firmware updates, network changes
**Methods**: Periodic `/info` polling, Discovery refresh, User configuration
### Current Data Model
The system maintains comprehensive device information:
```go
type ServiceDeviceInfo struct {
DeviceID string `json:"device_id"` // MAC or UUID
Name string `json:"name"` // User-friendly name
ProductCode string `json:"product_code"` // Device model
DeviceSerialNumber string `json:"device_serial_number"` // Hardware serial
ProductSerialNumber string `json:"product_serial_number"` // Product serial
FirmwareVersion string `json:"firmware_version"` // Software version
IPAddress string `json:"ip_address"` // Current IP
MacAddress string `json:"mac_address"` // MAC address
AccountID string `json:"account_id"` // Account association
DiscoveryMethod string `json:"discovery_method"` // How discovered
}
```
## Limitations of Current Approach
### Network Dependency Issues
| Issue | Impact | Affected Operations |
|-------|--------|-------------------|
| **Same Network Requirement** | High | Device discovery, Initial setup |
| **Direct Connectivity Need** | High | Device enrichment, Migration |
| **Firewall/NAT Restrictions** | Medium | Corporate networks, Complex setups |
| **Multi-VLAN Environments** | High | Enterprise deployments |
| **Remote Management** | Critical | Off-site device support |
### Service Architecture Limitations
1. **Geographic Constraints**: Service must be deployed on same network as speakers
2. **Scalability Issues**: Cannot centralize device management across multiple locations
3. **Discovery Reliability**: Multicast protocols can be unreliable in complex networks
4. **Real-time Updates**: No device-initiated communication for state changes
## /power_on Enhancement Proposal
### Current /power_on Request Analysis
The `/power_on` endpoint receives comprehensive device data that could replace many network-dependent operations:
```xml
<device-data>
<device id="A81B6A536A98">
<serialnumber>I6332527703739342000020</serialnumber>
<firmware-version>27.0.6.46330.5043500 epdbuild.trunk.hepdswbld04.2022-08-04T11:20:29</firmware-version>
<product product_code="SoundTouch 10 sm2" type="5">
<serialnumber>069231P63364828AE</serialnumber>
</product>
</device>
<diagnostic-data>
<device-landscape>
<rssi>Excellent</rssi>
<gateway-ip-address>192.168.178.1</gateway-ip-address>
<macaddresses>
<macaddress>A81B6A536A98</macaddress>
<macaddress>A81B6A849D99</macaddress>
</macaddresses>
<ip-address>192.168.178.35</ip-address>
<network-connection-type>Wireless</network-connection-type>
</device-landscape>
<network-landscape>
<network-data xmlns="http://www.Bose.com/Schemas/2012-12/NetworkMonitor/"/>
</network-landscape>
</diagnostic-data>
</device-data>
```
### Data Completeness Comparison
| Data Field | Current `/info` | `/power_on` | Gap Assessment |
|------------|----------------|-------------|----------------|
| **Device ID** | ✅ UUID format | ✅ MAC format | Different format |
| **Device Name** | ✅ Internal name | ❌ Missing | **Critical Gap** |
| **Device Type** | ✅ Model string | ✅ Product code | ✅ Available |
| **Account ID** | ✅ marge UUID | ❌ Missing | **Critical Gap** |
| **Service URL** | ✅ marge URL | ❌ Missing | **Important Gap** |
| **Firmware Version** | ✅ Full version | ✅ Full version | ✅ Available |
| **Serial Numbers** | ✅ Component serials | ✅ Device + Product | ✅ Available |
| **MAC Addresses** | ✅ Interface-specific | ✅ Multiple MACs | ✅ Enhanced |
| **IP Address** | ✅ Interface IPs | ✅ Current IP | ✅ Available |
| **Network Status** | ❌ Basic | ✅ Rich diagnostics | ✅ **Enhanced** |
| **Regional Settings** | ✅ Country/Region | ❌ Missing | **Important Gap** |
### Enhancement Benefits
#### 1. Network Independence
- ✅ Works across internet/WAN connections
- ✅ No multicast/broadcast requirements
- ✅ Firewall/NAT friendly
- ✅ Supports remote device management
#### 2. Real-time Device State
- ✅ Device-initiated communication
- ✅ Power-on event notifications
- ✅ Network status updates
- ✅ Firmware change detection
#### 3. Enhanced Diagnostics
- ✅ Signal strength (RSSI)
- ✅ Gateway information
- ✅ Connection type details
- ✅ Real-time network status
### Implementation Strategy
#### Phase 1: Hybrid Approach
Implement `/power_on` processing while maintaining existing discovery methods:
```go
func (s *Server) HandleMargePowerOn(w http.ResponseWriter, r *http.Request) {
// Parse power_on request
var powerOnData models.CustomerSupportRequest
if err := xml.Unmarshal(body, &powerOnData); err != nil {
// Fallback to existing discovery
return s.fallbackToDiscovery(r.RemoteAddr)
}
// Extract device information
deviceMAC := powerOnData.Device.ID
deviceIP := powerOnData.DiagnosticData.DeviceLandscape.IPAddress
// Lookup existing device data
deviceInfo := s.lookupDeviceByMAC(deviceMAC)
if deviceInfo == nil {
// New device - trigger registration flow
deviceInfo = s.createDeviceFromPowerOn(powerOnData)
}
// Update with power_on data
s.updateDeviceFromPowerOn(deviceInfo, powerOnData)
// Determine response actions
response := s.buildPowerOnResponse(deviceInfo)
s.sendResponse(w, response)
}
```
#### Phase 2: Gap Resolution
Address missing data through complementary mechanisms:
1. **User-Friendly Names**: Maintain registration process for name assignment
2. **Account Association**: Enhance registration to link MAC addresses to accounts
3. **Service URLs**: Implement account-based service URL resolution
4. **Regional Settings**: Use IP geolocation or account preferences
#### Phase 3: Enhanced Device Lifecycle
```mermaid
sequenceDiagram
participant Device as SoundTouch Device
participant Service as SoundTouch Service
participant DataStore as Data Store
participant User as User/App
Note over Device,User: Enhanced Device Lifecycle
rect rgb(248, 255, 248)
Note over Device,DataStore: 1. Power-On Registration
Device->>Service: POST /power_on (rich device data)
Service->>DataStore: Lookup device by MAC
alt Device Unknown
Service->>DataStore: Create device record
Service->>User: Notify new device found
else Device Known
Service->>DataStore: Update device status
end
Service->>Device: Configuration response
end
rect rgb(255, 248, 240)
Note over User,DataStore: 2. User Registration (Optional)
User->>Service: POST /setup/devices (name + preferences)
Service->>DataStore: Add user metadata to device
Service->>Device: Updated configuration (on next power_on)
end
rect rgb(240, 248, 255)
Note over Device,DataStore: 3. Ongoing Updates
Device->>Service: POST /power_on (status changes)
Service->>Service: Detect firmware/network changes
Service->>DataStore: Update device record
alt Migration Needed
Service->>Device: Migration instructions
Device->>Device: Apply configuration
Device->>Service: POST /power_on (confirm changes)
end
end
rect rgb(255, 248, 255)
Note over Service,User: 4. Remote Management
User->>Service: Management request (any location)
Service->>DataStore: Lookup device status
Service->>User: Current device state
Note over Service: No local network required
end
```
### Migration Strategy
#### Current Migration Flow Issues
- Requires `/info` endpoint access for device identification
- Must be on same network for configuration changes
- Limited to devices discoverable via UPnP/mDNS
#### Enhanced Migration with /power_on
1. **Device Identification**: Use MAC address from `/power_on` instead of IP-based `/info`
2. **Configuration Delivery**: Send migration instructions in `/power_on` response
3. **Status Confirmation**: Device confirms changes via subsequent `/power_on` requests
4. **Remote Capability**: Manage devices from any network location
```go
type PowerOnResponse struct {
ConfigurationUpdates []ConfigUpdate `json:"configuration_updates,omitempty"`
MigrationInstructions *Migration `json:"migration,omitempty"`
RegistrationRequired bool `json:"registration_required,omitempty"`
}
type Migration struct {
Method string `json:"method"` // xml, hosts, resolv_conf
TargetURL string `json:"target_url"`
ProxyURL string `json:"proxy_url,omitempty"`
Options map[string]string `json:"options"`
}
```
## Recommendations
### Immediate Actions (Phase 1)
1. **Enhance `/power_on` handler** to extract and store comprehensive device data
2. **Implement device lookup by MAC address** as primary identification method
3. **Create hybrid discovery system** using both `/power_on` and existing methods
4. **Add network-independent device management** capabilities
### Medium-term Improvements (Phase 2)
1. **Implement account-device MAC mapping** for automatic association
2. **Add IP geolocation** for regional settings inference
3. **Create device registration UI** optimized for `/power_on` discovered devices
4. **Enhance migration system** to use `/power_on` response mechanism
### Long-term Enhancements (Phase 3)
1. **Request firmware enhancement** to include missing data in `/power_on`
2. **Implement real-time device monitoring** via `/power_on` events
3. **Create centralized device management** independent of network topology
4. **Add predictive migration** based on device status patterns
### Risk Mitigation
- **Maintain backward compatibility** with existing discovery methods
- **Implement graceful fallbacks** when `/power_on` data is incomplete
- **Preserve existing user workflows** while adding enhanced capabilities
- **Add comprehensive logging** for troubleshooting hybrid approach
## Conclusion
The `/power_on` endpoint provides a significant opportunity to reduce network dependencies while enhancing device management capabilities. By implementing a hybrid approach that leverages `/power_on` data for primary device identification and status updates while maintaining existing registration workflows for user-controlled metadata, the system can achieve:
- **Network independence** for core device management
- **Enhanced real-time capabilities** through device-initiated communication
- **Improved scalability** across diverse network topologies
- **Better user experience** with automatic device discovery and status updates
The proposed implementation strategy provides a clear path to achieve these benefits while maintaining system reliability and user workflow compatibility.
+150
View File
@@ -0,0 +1,150 @@
# Device Lifecycle Analysis - Executive Summary
## Current State Assessment
The SoundTouch service currently relies heavily on local network connectivity for device discovery and management:
### ✅ Strengths
- **Comprehensive device data** through `/info` endpoint
- **Robust discovery** via UPnP/SSDP + mDNS
- **User-controlled registration** with friendly names
- **Complete device lifecycle management**
### ❌ Limitations
- **Network dependency**: Requires same network segment for discovery
- **Geographic constraints**: Service must be co-located with devices
- **Firewall/NAT issues**: Multicast protocols unreliable in complex networks
- **No remote management**: Cannot manage devices from external networks
## /power_on Enhancement Opportunity
The `/power_on` endpoint provides rich device data that could eliminate network dependencies:
### Current /power_on Data
```xml
<device-data>
<device id="A81B6A536A98"> <!-- ✅ Device MAC -->
<serialnumber>I6332527703739342000020</serialnumber> <!-- ✅ Serial -->
<firmware-version>27.0.6.46330.5043500...</firmware-version> <!-- ✅ FW -->
<product product_code="SoundTouch 10 sm2" type="5"> <!-- ✅ Model -->
<serialnumber>069231P63364828AE</serialnumber> <!-- ✅ Product Serial -->
</product>
</device>
<diagnostic-data>
<device-landscape>
<rssi>Excellent</rssi> <!-- ✅ Signal -->
<gateway-ip-address>192.168.178.1</gateway-ip-address> <!-- ✅ Network -->
<macaddresses> <!-- ✅ All MACs -->
<macaddress>A81B6A536A98</macaddress>
<macaddress>A81B6A849D99</macaddress>
</macaddresses>
<ip-address>192.168.178.35</ip-address> <!-- ✅ Current IP -->
<network-connection-type>Wireless</network-connection-type> <!-- ✅ Connection -->
</device-landscape>
</diagnostic-data>
</device-data>
```
### Missing Data Gaps
| Data | Current Source | Available in /power_on | Impact |
|------|----------------|----------------------|---------|
| **User-friendly name** | Registration | ❌ Missing | **High** - UI/UX |
| **Account association** | Registration | ❌ Missing | **Critical** - Authorization |
| **Service URLs** | `/info` | ❌ Missing | **High** - Migration |
| **Regional settings** | `/info` | ❌ Missing | **Medium** - Localization |
## Recommended Implementation Strategy
### Phase 1: Hybrid Enhancement (Immediate)
- **Enhance `/power_on` handler** to process full device data
- **Implement MAC-based device lookup** for identification
- **Maintain existing registration flow** for user metadata
- **Add network-independent capabilities** as primary features
```go
// Enhanced flow
Device -> POST /power_on -> Service identifies by MAC -> Update/Create device record
```
### Phase 2: Gap Resolution (Short-term)
- **Account-device MAC mapping** for automatic association
- **IP geolocation** for regional settings inference
- **Registration UI optimization** for /power_on discovered devices
- **Migration via response payload** instead of direct device access
### Phase 3: Full Network Independence (Medium-term)
- **Centralized device management** across multiple networks
- **Real-time device monitoring** via /power_on events
- **Predictive migration** based on device status patterns
- **Enhanced firmware integration** with additional /power_on data
## Key Benefits
### ✅ Immediate Gains
- **Network independence**: Manage devices from any location
- **Real-time updates**: Device-initiated status reporting
- **Enhanced diagnostics**: Signal strength, connection type, network status
- **Simplified deployment**: No multicast/broadcast requirements
### ✅ Long-term Advantages
- **Scalable architecture**: Centralized management across sites
- **Improved reliability**: Eliminates discovery protocol dependencies
- **Better user experience**: Automatic device detection and status
- **Future-proof design**: Device-driven communication model
## Implementation Approach
### Hybrid Strategy
```mermaid
graph TD
PowerOn[Device /power_on] --> Identify[MAC-based Identification]
Identify --> New{New Device?}
New -->|Yes| Create[Create Device Record]
New -->|No| Update[Update Existing Record]
Create --> CheckAccount{Account Known?}
CheckAccount -->|No| RegisterFlow[Trigger Registration]
CheckAccount -->|Yes| LinkAccount[Link to Account]
Update --> DetectChanges[Detect Changes]
DetectChanges --> Migration{Migration Needed?}
Migration -->|Yes| SendInstructions[Send Migration Instructions]
LinkAccount --> Response[Send Configuration Response]
SendInstructions --> Response
RegisterFlow --> Response
```
### Risk Mitigation
- **Maintain backward compatibility** with existing discovery
- **Graceful fallbacks** when /power_on data incomplete
- **Preserve user workflows** while adding enhanced capabilities
- **Comprehensive logging** for troubleshooting
## Success Metrics
### Technical Metrics
- **Network independence**: % of operations not requiring local network
- **Real-time capability**: Power-on event processing latency < 2s
- **Data completeness**: % of devices with full metadata via /power_on
- **Migration success**: % of successful remote migrations
### User Experience Metrics
- **Discovery reliability**: % of devices automatically detected
- **Setup time**: Time from device power-on to full management
- **Management accessibility**: % of operations available remotely
- **Error reduction**: Decrease in network-related issues
## Conclusion
The `/power_on` enhancement represents a strategic opportunity to:
1. **Eliminate network dependencies** while maintaining full functionality
2. **Enable remote device management** across diverse network topologies
3. **Improve user experience** through automatic device detection
4. **Future-proof the architecture** for scalable device management
**Recommendation**: Proceed with hybrid implementation approach, prioritizing network independence while preserving existing user workflows and system reliability.
**Timeline**: Phase 1 implementation feasible within 2-3 sprints, with Phases 2-3 extending capabilities based on user feedback and firmware enhancement opportunities.
+222
View File
@@ -0,0 +1,222 @@
# Migration Flow Diagrams
This document specifies the diagrams needed for the migration guide, with descriptions that can be used to create actual visual diagrams.
## 1. Overall Migration Process Flow
### Description
A flowchart showing the complete migration journey from start to finish.
### Elements
```
[Start] → [Install SoundTouch Service] → [Create Account] → [Prepare Devices]
[Enable Remote Services] → [Discover Devices] → [Register Devices]
[Start Migration] → [Data Collection Phase] → [Testing Phase] → [Full Local Phase]
[Verify Migration] → [Complete] → [Post-Migration Setup]
```
### Decision Points
- Multiple devices? → Repeat device steps
- Migration issues? → Rollback option
- All devices complete? → Account fully migrated
### Color Coding
- **Blue**: Service setup steps
- **Green**: Successful completion states
- **Orange**: In-progress/testing states
- **Red**: Error handling/rollback paths
- **Gray**: Optional steps
## 2. Network Topology Diagram
### Description
Shows the network layout with Raspberry Pi, router, and SoundTouch devices.
### Components
```
Internet Cloud
↑↓ (Optional - during migration)
Home Router (192.168.1.1)
├── Raspberry Pi (192.168.1.10) [SoundTouch Service]
├── Living Room Speaker (192.168.1.100)
├── Kitchen Speaker (192.168.1.101)
├── Bedroom Speaker (192.168.1.102)
└── Office Speaker (192.168.1.103)
```
### Connections
- **Solid lines**: Active connections
- **Dashed lines**: Migration-phase connections to Bose cloud
- **Thick lines**: Primary data flow to local service
## 3. Device State Lifecycle
### Description
State machine showing device progression through migration phases.
### States and Transitions
```
[Unregistered] → [Discovered] → [Registered] → [Migrating]
[Active - Local Only] ← [Active - Testing] ← [Active - Data Collection]
↑ ↓
[Error/Rollback] ← ← ← ← ← ← ← ← ← ← ← ← ← ← ← ← ← [Migration Failed]
```
### State Descriptions
- **Unregistered**: Device not known to service
- **Discovered**: Found on network, remote services enabled
- **Registered**: Added to account, ready for migration
- **Migrating - Data Collection**: Building local database
- **Migrating - Testing**: Using local service with fallback
- **Active - Local Only**: Full independence achieved
- **Error/Rollback**: Issues detected, can revert to Bose
## 4. Data Flow During Migration
### Description
Shows how data flows between components during different migration phases.
### Phase 1 - Data Collection
```
SoundTouch Device → Bose Cloud Services
↓ (mirror)
Local Service (collecting data)
```
### Phase 2 - Testing
```
SoundTouch Device ↔ Local Service (primary)
↕ (fallback when needed)
Bose Cloud Services
```
### Phase 3 - Full Local
```
SoundTouch Device ↔ Local Service (only)
Bose Cloud Services (disconnected)
```
### Data Types
- **Presets**: Station favorites and custom sources
- **Recents**: Play history and recently accessed content
- **Sources**: Configured music services (Spotify, etc.)
- **Device Config**: Network settings, capabilities, metadata
## 5. Migration Timeline Visualization
### Description
Gantt-chart style timeline showing typical migration schedule.
### Timeline (7-day example)
```
Day 1-2: Data Collection Phase
████████████████████████████████████████
Day 3-4: Data Validation
████████████████████████████
Day 5-6: Testing Phase
████████████████████████
Day 7+: Full Local Operation
████████████████████████→
```
### Parallel Activities
- Multiple devices can be in different phases
- Service continues operating throughout
- User can interact normally during process
## 6. Service Architecture Overview
### Description
High-level architecture showing enhanced SoundTouch service components.
### Components
```
Web Dashboard ← → HTTP API ← → REST Endpoints
↑ ↑ ↑
└─── User ──────┼──── Devices ─┘
Service Core
├── Account Manager
├── Device Lifecycle
├── Event Processor
├── Migration Controller
└── Data Store
File System Storage
├── accounts/
├── devices/
├── sessions/ (existing)
└── system/
```
### External Integrations
- **Bose Cloud** (during migration)
- **Music Services** (Spotify, TuneIn, etc.)
- **Discovery Services** (mDNS, UPnP)
## 7. Error Handling and Rollback Flow
### Description
Decision tree for handling migration issues and rollback scenarios.
### Error Detection
```
Migration Issue Detected
├── Device Unresponsive → Retry → Success/Rollback
├── Data Corruption → Restore from Backup → Continue/Rollback
├── Service Unavailable → Wait/Restart → Continue/Rollback
└── User Dissatisfaction → Manual Rollback → Restore Bose Config
```
### Rollback Process
```
[Rollback Initiated]
[Disable Local Services]
[Restore Original Device Config]
[Re-enable Bose Services]
[Verify Functionality]
[Rollback Complete]
```
## Implementation Notes
### For Diagram Creation
1. Use consistent colors as specified in main color scheme
2. Include clear labels for all components
3. Show directional flow with appropriate arrows
4. Use standard flowchart symbols where applicable
5. Ensure text is readable at various sizes
### Tools Recommended
- **Lucidchart**: Professional flowcharts and network diagrams
- **Draw.io**: Free online diagram tool
- **Miro**: Collaborative whiteboarding
- **PlantUML**: Code-based diagram generation
### File Naming Convention
- `migration-flow-overview.svg` - Overall process flow
- `network-topology.svg` - Network layout
- `device-lifecycle.svg` - State machine
- `data-flow-phases.svg` - Data flow during migration
- `migration-timeline.svg` - Timeline visualization
- `service-architecture.svg` - System architecture
- `error-rollback-flow.svg` - Error handling
### Accessibility
- Include alt-text descriptions
- Use patterns/textures in addition to colors
- Ensure sufficient contrast
- Provide text-based versions for screen readers
@@ -174,6 +174,77 @@ soundtouch-cli --host 192.168.1.10 play now
soundtouch-cli --host 192.168.1.10 play now --verbose
```
### Recent Content
Recently played content management.
#### `recents <subcommand>`
Recently played content commands.
```bash
# List recently played items
soundtouch-cli --host <device> recents list [--limit <number>] [--detailed]
# Filter recent items by source or type
soundtouch-cli --host <device> recents filter --source <SOURCE> [--type <TYPE>] [--limit <number>]
# Show only the most recent item
soundtouch-cli --host <device> recents latest
# Show statistics about recent content
soundtouch-cli --host <device> recents stats
```
**Basic Usage Examples:**
```bash
# List last 10 recent items (default)
soundtouch-cli --host 192.168.1.10 recents list
# Show all recent items with detailed information
soundtouch-cli --host 192.168.1.10 recents list --limit 0 --detailed
# Show only the most recent item
soundtouch-cli --host 192.168.1.10 recents latest
```
**Filtering Examples:**
```bash
# Show only Spotify items
soundtouch-cli --host 192.168.1.10 recents filter --source SPOTIFY
# Show only tracks (no stations or playlists)
soundtouch-cli --host 192.168.1.10 recents filter --type track
# Show only presetable items
soundtouch-cli --host 192.168.1.10 recents filter --type presetable
# Show last 5 local music items
soundtouch-cli --host 192.168.1.10 recents filter --source LOCAL_MUSIC --limit 5
```
**Available Sources:**
- `SPOTIFY` - Spotify streaming
- `LOCAL_MUSIC` - Local music files
- `STORED_MUSIC` - Stored music library
- `TUNEIN` - TuneIn radio stations
- `PANDORA` - Pandora music
- `AMAZON` - Amazon Music
- `DEEZER` - Deezer streaming
**Available Types:**
- `track` - Individual songs
- `station` - Radio stations
- `playlist` - Music playlists
- `album` - Music albums
- `presetable` - Items that can be saved as presets
**Statistics Example:**
```bash
# Get detailed statistics about recent content
soundtouch-cli --host 192.168.1.10 recents stats
```
#### `presets` (Legacy)
Get configured presets (legacy command for backward compatibility).
@@ -322,6 +393,12 @@ soundtouch-cli --host <device> source select --source <SOURCE> [--account <ACCOU
soundtouch-cli --host <device> source spotify
soundtouch-cli --host <device> source bluetooth
soundtouch-cli --host <device> source aux
# Advanced content selection
soundtouch-cli --host <device> source internet-radio --location <URL> [--name <NAME>]
soundtouch-cli --host <device> source local-music --location <LOCATION> --account <ACCOUNT>
soundtouch-cli --host <device> source stored-music --location <LOCATION> --account <ACCOUNT>
soundtouch-cli --host <device> source content --source <SOURCE> --location <LOCATION>
```
**Source Names:**
@@ -329,9 +406,12 @@ soundtouch-cli --host <device> source aux
- `BLUETOOTH` - Bluetooth input
- `AUX` - AUX input
- `AIRPLAY` - AirPlay
- `STORED_MUSIC` - Local music library
- `INTERNET_RADIO` - Internet radio
- `PRODUCT` - Product-specific sources
- `LOCAL_MUSIC` - SoundTouch App Media Server content
- `LOCAL_INTERNET_RADIO` - Internet radio streams
- `STORED_MUSIC` - UPnP/DLNA media server content
- `TUNEIN` - TuneIn radio stations
- `PANDORA` - Pandora music service
- `PRODUCT` - Product-specific sources (TV, HDMI)
**Examples:**
```bash
@@ -346,8 +426,218 @@ soundtouch-cli --host 192.168.1.10 source select --source SPOTIFY --account user
# Select Bluetooth
soundtouch-cli --host 192.168.1.10 source bluetooth
# Select internet radio with streamUrl format
soundtouch-cli --host 192.168.1.10 source internet-radio \
--location "http://contentapi.gmuth.de/station.php?name=MyStation&streamUrl=https://stream.example.com/radio" \
--name "My Radio Station" \
--artwork "https://example.com/art.png"
# Select internet radio with direct stream URL
soundtouch-cli --host 192.168.1.10 source internet-radio \
--location "https://stream.example.com/radio" \
--name "My Stream"
# Select local music content (requires SoundTouch App Media Server)
soundtouch-cli --host 192.168.1.10 source local-music \
--location "album:983" \
--account "3f205110-4a57-4e91-810a-123456789012" \
--name "Welcome to the New"
# Select stored music content (requires UPnP/DLNA media server)
soundtouch-cli --host 192.168.1.10 source stored-music \
--location "6_a2874b5d_4f83d999" \
--account "d09708a1-5953-44bc-a413-123456789012/0" \
--name "Christmas Album"
# Advanced content selection with all options
soundtouch-cli --host 192.168.1.10 source content \
--source LOCAL_INTERNET_RADIO \
--location "https://stream.example.com/radio" \
--name "My Stream" \
--type stationurl \
--presetable
# Get introspect data for Spotify
soundtouch-cli --host 192.168.1.10 source introspect --source SPOTIFY
# Get introspect data with account
soundtouch-cli --host 192.168.1.10 source introspect --source SPOTIFY --account user@spotify.com
# Spotify introspect (convenience command)
soundtouch-cli --host 192.168.1.10 source introspect-spotify
# Get introspect data for all available services
soundtouch-cli --host 192.168.1.10 source introspect-all
# Check service availability
soundtouch-cli --host 192.168.1.10 source availability
# Compare sources and availability
soundtouch-cli --host 192.168.1.10 source compare
```
**Content Selection Commands:**
| Command | Description | Requirements |
|---------|-------------|--------------|
| `internet-radio` | Select internet radio stream (LOCAL_INTERNET_RADIO) | Stream URL |
| `local-music` | Select local music content (LOCAL_MUSIC) | SoundTouch App Media Server |
| `stored-music` | Select stored music content (STORED_MUSIC) | UPnP/DLNA media server |
| `content` | Generic content selection (advanced) | Source and location |
**streamUrl Format Support:**
The `internet-radio` command supports the streamUrl proxy format from the [SoundTouch WebServices API Wiki](https://github.com/thlucas1/homeassistantcomponent_soundtouchplus/wiki/SoundTouch-WebServices-API#select-local_internet_radio---streamurl-format):
```bash
# Using contentapi.gmuth.de proxy for complex streams
soundtouch-cli --host 192.168.1.10 source internet-radio \
--location "http://contentapi.gmuth.de/station.php?name=Antenne%20Chillout&streamUrl=https://stream.antenne.de/chillout/stream/aacp" \
--name "Antenne Chillout"
```
#### Service Introspection
Get detailed information about music service states, user accounts, capabilities, and authentication status.
**Introspect Commands:**
```bash
# Get introspect data for specific service
soundtouch-cli --host <device> source introspect --source <SERVICE> [--account <ACCOUNT>]
# Spotify introspect (convenience)
soundtouch-cli --host <device> source introspect-spotify [--account <ACCOUNT>]
# Get introspect data for all services
soundtouch-cli --host <device> source introspect-all
```
**Supported Services for Introspect:**
- `SPOTIFY` - Spotify streaming service
- `PANDORA` - Pandora music service
- `TUNEIN` - TuneIn radio service
- `AMAZON` - Amazon Music service
- `DEEZER` - Deezer streaming service
**Introspect Information Includes:**
- Service state (Active, Inactive, InactiveUnselected)
- User account information
- Current playback status and content URI
- Service capabilities (skip, seek, resume support)
- Authentication token status
- Subscription type and content history limits
- Shuffle mode and data collection settings
**Examples:**
```bash
# Get Spotify service status
soundtouch-cli --host 192.168.1.10 source introspect --source SPOTIFY
# Get Spotify status with specific account
soundtouch-cli --host 192.168.1.10 source introspect --source SPOTIFY --account my_spotify_user
# Use Spotify convenience command
soundtouch-cli --host 192.168.1.10 source introspect-spotify
# Get status for all available streaming services
soundtouch-cli --host 192.168.1.10 source introspect-all
# Check which services are available before introspecting
soundtouch-cli --host 192.168.1.10 source availability
```
### Music Service Account Management
Manage music streaming service accounts and network music library connections.
#### `account <subcommand>`
Music service account management commands.
```bash
# List configured accounts
soundtouch-cli --host <device> account list
# Add music service account (generic)
soundtouch-cli --host <device> account add --source <SOURCE> --user <USER> --password <PASS> [--name <NAME>]
# Remove music service account (generic)
soundtouch-cli --host <device> account remove --source <SOURCE> --user <USER> [--name <NAME>]
# Service-specific convenience commands
soundtouch-cli --host <device> account add-spotify --user <EMAIL> --password <PASS>
soundtouch-cli --host <device> account add-pandora --user <USER> --password <PASS>
soundtouch-cli --host <device> account add-amazon --user <USER> --password <PASS>
soundtouch-cli --host <device> account add-deezer --user <USER> --password <PASS>
soundtouch-cli --host <device> account add-iheart --user <USER> --password <PASS>
soundtouch-cli --host <device> account add-nas --user <GUID/0> [--name <NAME>]
# Remove accounts
soundtouch-cli --host <device> account remove-spotify --user <EMAIL>
soundtouch-cli --host <device> account remove-pandora --user <USER>
soundtouch-cli --host <device> account remove-amazon --user <USER>
soundtouch-cli --host <device> account remove-deezer --user <USER>
soundtouch-cli --host <device> account remove-iheart --user <USER>
soundtouch-cli --host <device> account remove-nas --user <GUID/0> [--name <NAME>]
```
**Supported Services:**
- **SPOTIFY**: Spotify Premium accounts
- **PANDORA**: Pandora Music Service accounts
- **AMAZON**: Amazon Music accounts
- **DEEZER**: Deezer Premium accounts
- **IHEART**: iHeartRadio accounts
- **STORED_MUSIC**: Network music libraries (NAS/UPnP/DLNA servers)
**Examples:**
```bash
# List all configured music service accounts
soundtouch-cli --host 192.168.1.10 account list
# Add a Spotify Premium account
soundtouch-cli --host 192.168.1.10 account add-spotify \
--user "user@spotify.com" \
--password "mypassword"
# Add a Pandora account
soundtouch-cli --host 192.168.1.10 account add-pandora \
--user "pandora_username" \
--password "pandora_password"
# Add an Amazon Music account
soundtouch-cli --host 192.168.1.10 account add-amazon \
--user "amazon_user" \
--password "amazon_password"
# Add a network music library (NAS/UPnP)
soundtouch-cli --host 192.168.1.10 account add-nas \
--user "d09708a1-5953-44bc-a413-123456789012/0" \
--name "My Music Server"
# Remove a Spotify account
soundtouch-cli --host 192.168.1.10 account remove-spotify \
--user "user@spotify.com"
# Generic account management
soundtouch-cli --host 192.168.1.10 account add \
--source DEEZER \
--user "deezer_user" \
--password "deezer_pass" \
--name "Deezer Premium"
soundtouch-cli --host 192.168.1.10 account remove \
--source DEEZER \
--user "deezer_user"
```
**Notes:**
- Music service accounts must be configured before you can browse or play content from those services
- Network music libraries (STORED_MUSIC) don't require passwords, only the UPnP server GUID
- After adding an account, use `source list` to verify it appears as available
- Some services may require additional authentication steps through their mobile apps
### Bass Control
Adjust bass levels (equalizer).
@@ -958,6 +1248,6 @@ SOUNDTOUCH_DISCOVERY_TIMEOUT=10s
## See Also
- [Getting Started Guide](GETTING-STARTED.md) - Basic setup and usage
- [WebSocket Events](websocket-events.md) - Real-time monitoring
- [Zone Management](zone-management.md) - Multi-room setup
- [API Endpoints](API-Endpoints-Overview.md) - Complete API reference
- [WebSocket Events](../reference/WEBSOCKET-EVENTS.md) - Real-time monitoring
- [Zone Management](../reference/ZONE-MANAGEMENT.md) - Multi-room setup
- [API Endpoints](../reference/API-ENDPOINTS.md) - Complete API reference
@@ -13,6 +13,10 @@ This guide covers everything you need to know to deploy robust, scalable SoundTo
- [Performance Optimization](#performance-optimization)
- [Error Handling Recovery](#error-handling-recovery)
- [Deployment Strategies](#deployment-strategies)
- [Docker Deployment](#docker-deployment)
- [Kubernetes Deployment](#kubernetes-deployment)
- [Systemd Service](#systemd-service)
- [Raspberry Pi Installer](#raspberry-pi-installer)
- [Maintenance Operations](#maintenance-operations)
---
@@ -926,39 +930,49 @@ data:
device_hosts: "192.168.1.100,192.168.1.101,192.168.1.102"
```
### Systemd Service
#### Systemd Service
A standard systemd unit for manual installation. This example assumes the binary is at `/usr/local/bin/soundtouch-service` and data is stored in `/var/lib/soundtouch-service`.
```ini
# /etc/systemd/system/soundtouch.service
# /etc/systemd/system/soundtouch-service.service
[Unit]
Description=SoundTouch Control Service
After=network.target
Wants=network.target
Description=Bose SoundTouch Service
Wants=network-online.target
After=network-online.target
[Service]
Type=simple
User=soundtouch
Group=soundtouch
WorkingDirectory=/opt/soundtouch
ExecStart=/opt/soundtouch/bin/soundtouch-app
ExecReload=/bin/kill -HUP $MAINPID
Restart=always
RestartSec=5
Environment=DEVICE_HOSTS=192.168.1.100,192.168.1.101
Environment=LOG_LEVEL=info
Environment=CONFIG_FILE=/opt/soundtouch/config/production.yaml
WorkingDirectory=/var/lib/soundtouch-service
ExecStart=/usr/local/bin/soundtouch-service
Environment=PORT=80
Environment=SERVER_URL=http://soundtouch.local
# Security settings
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/soundtouch/logs
# Allow binding to privileged ports (80/443) without running as root
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
Restart=on-failure
RestartSec=5
# Security hardening
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
ReadWritePaths=/var/lib/soundtouch-service
[Install]
WantedBy=multi-user.target
```
#### Raspberry Pi Installer
For users deploying on a Raspberry Pi, we provide a specialized automated installer that handles everything from architecture detection to security hardening.
See the [Raspberry Pi Installation Guide](RASPBERRY-PI.md) for step-by-step instructions.
---
## Maintenance Operations
@@ -1071,4 +1085,4 @@ func init() {
// Set GC target percentage
if os.Getenv("GOGC") == "" {
debug.SetGCPerc
debug.SetGCPerc
+80
View File
@@ -0,0 +1,80 @@
# SoundTouch Device Initial Setup Variants
Based on community research from the **SoundCork** and **ÜberBöse API** projects, as well as analysis of the Stockholm firmware (`firmware/Stockholm/.../setup/`), this document outlines the methods used for the "out-of-the-box" setup of SoundTouch devices.
## Setup Overview
Initial setup is the process of connecting a new or factory-reset device to a local Wi-Fi network and a Bose (or custom) account. This is distinct from the "Migration" process (handled by `soundtouch-service`), which redirects an already-configured device to a new server.
---
## 1. Bluetooth Low Energy (BLE) Setup
Used by most modern SoundTouch devices (ST-10, ST-20/30 Series III, SoundTouch 300).
- **Mechanism**: The SoundTouch app communicates with the device over BLE to exchange Wi-Fi credentials.
- **Protocol**: Internal research refers to this as the **Gabbo** protocol (see `gabbo_setup_bco.js` in firmware).
- **Process**:
1. Put the device in setup mode (usually by holding the '2' and '-' buttons).
2. The app discovers the device via BLE.
3. The app sends the Wi-Fi SSID and Password to the device.
4. The device connects to Wi-Fi and disables BLE setup.
---
## 2. Access Point (AP) Mode / Web Setup
The classic "failover" or "alternate" setup method.
- **Mechanism**: The device creates its own Wi-Fi network (SSID: `Bose SoundTouch ...` or `Bose Home Speaker ...`).
- **IP Address**: Typically `192.168.1.1` or `10.0.0.1` (device-side).
- **Web Interface**: The device hosts a web server on port 80.
- **Process**:
1. Connect a PC/Phone to the device's Wi-Fi.
2. Open a browser to `http://192.168.1.1`.
3. The device serves `setup.html`, which redirects to a setup wizard (`setup/index.html`).
4. Use the `gabbo_wifi` form to select a network and enter credentials.
---
## 3. Wireless Accessory Configuration (WAC)
Specific to Apple iOS devices.
- **Mechanism**: Uses Apple's MFi/WAC protocol to pass Wi-Fi settings from an iPhone/iPad directly to the device without manual password entry.
- **Status**: Detected automatically by iOS when a new SoundTouch device is in setup mode.
---
## 4. USB Setup (Legacy)
Primarily used for older SoundTouch Series I and II devices or as a last resort.
- **Mechanism**: Physical connection via Micro-USB to a computer running the SoundTouch Setup application.
- **Process**:
1. Connect USB cable.
2. The desktop app communicates via a proprietary HID or Serial-over-USB protocol.
3. The app pushes Wi-Fi credentials.
4. References to this exist in the firmware as `lost_USB_connection` and `connect_device` (see `setup_wizard.xml`).
---
## Technical Details: The "Gabbo" Protocol
The Stockholm firmware contains references to a communication layer called **Gabbo**.
- **File**: `setup/js/gabbo_setup_bco.js`
- **Function**: Handles the state machine for Wi-Fi connection, account pairing, and error handling during setup.
- **Relationship**: It appears to be an internal wrapper for the messages sent between the setup client (App or Browser) and the device firmware.
## Redirection during Setup
While the `soundtouch-service` focuses on migrating existing devices, a truly "clean" setup to a custom service would require:
1. Intercepting the initial account pairing request.
2. Providing a mock "Marge" service that accepts any credentials.
3. Patching the `SoundTouchSdkPrivateCfg.xml` during or immediately after the Wi-Fi connection phase.
---
## Comparison: Initial Setup vs. Migration
| Feature | Initial Setup | Migration (soundtouch-service) |
| :--- | :--- | :--- |
| **Connectivity** | BLE, AP Mode, USB, WAC | Ethernet/Wi-Fi (existing) |
| **Credentials** | Required (SSID/Pass) | Not required (uses existing) |
| **Access** | Web UI / App protocol | SSH (root) |
| **Primary File** | `setup/index.html` | `SoundTouchSdkPrivateCfg.xml` |
| **Use Case** | Out-of-the-box / Reset | Redirecting active devices |
+109
View File
@@ -0,0 +1,109 @@
# HTTPS Setup & Custom CA Certificate
To use the `/etc/hosts` redirection method safely, SoundTouch devices must communicate over HTTPS. This requires the device to trust the AfterTouch Root CA certificate used by the local service.
## 1. Automated Migration (Hosts Method)
The `soundtouch-service` can automatically configure a device to use the `/etc/hosts` method:
```bash
curl -X POST "http://localhost:8000/setup/migrate/{deviceIP}?method=hosts"
```
This command will:
1. Connect to the device via SSH.
2. Update `/etc/hosts` to point Bose domains to the service IP.
3. Inject the auto-generated AfterTouch Root CA into the device's trust store (`/etc/pki/tls/certs/ca-bundle.crt`).
4. Reboot the device.
## 2. Managing the Root CA
The AfterTouch service automatically generates a Root CA when it first starts.
- **CA Certificate**: `data/certs/ca.crt`
- **CA Private Key**: `data/certs/ca.key`
### Downloading the CA Certificate
You can download the CA certificate for manual installation on other devices (like your phone or PC) from:
`http://<server-ip>:8000/setup/ca.crt`
### 3. Built-in HTTPS Support
The `soundtouch-service` now includes a built-in HTTPS listener. This simplifies the `/etc/hosts` redirection method by automatically presenting the correct certificates for Bose domains.
- **HTTPS Port**: Configurable via `HTTPS_PORT` environment variable (defaults to `8443`).
- **HTTPS Server URL**: Configurable via `HTTPS_SERVER_URL` (e.g., `https://mysoundtouch.local:8443`). If not set, the service attempts to guess it using the system hostname.
- **Domain Coverage**: Automatically presents a certificate with comprehensive coverage using wildcard certificates (`*.api.bose.io`, `*.api.bosecm.com`) plus specific domains (`streaming.bose.com`, `updates.bose.com`, `stats.bose.com`, `bmx.bose.com`, `worldwide.bose.com`, `bose-prod.apigee.net`, etc.).
- **Wildcard Support**: Uses RFC-compliant wildcard certificates for automatic coverage of all API subdomains, including event analytics endpoints like `events.api.bosecm.com`, `eventsdev.api.bosecm.com`, and future API services.
- **TLS Error Logging**: Comprehensive logging of TLS handshake attempts, certificate matching, and connection failures for debugging DNS redirection issues.
- **Automatic Setup**: On first start, it generates a server certificate signed by your AfterTouch local Root CA.
#### TLS Security & Debugging
The built-in HTTPS listener is configured to use modern and secure TLS settings while maintaining compatibility with SoundTouch devices (which support up to TLS 1.2 with OpenSSL 1.0.2).
- **Minimum TLS Version**: TLS 1.2
- **Preferred Cipher Suites**:
- `ECDHE-RSA-AES128-GCM-SHA256`
- **TLS Debugging**: Detailed logging of:
- Certificate requests by domain (`[TLS] Certificate request for ServerName: events.api.bosecm.com`)
- Wildcard certificate matching (`[TLS] ✅ Serving certificate for events.api.bosecm.com (matched *.api.bosecm.com)`)
- Handshake failures (`[TLS] ❌ Handshake failed from 192.168.1.50: tls: certificate not found`)
- Successful connections (`[TLS] ✅ Successful connection from 192.168.1.50`)
- `ECDHE-RSA-AES256-GCM-SHA384`
- `ECDHE-RSA-CHACHA20-POLY1305`
- `RSA-AES128-GCM-SHA256` (Legacy support)
- `RSA-AES256-GCM-SHA384` (Legacy support)
#### Binding to Port 443
SoundTouch devices expect HTTPS on the default port 443. Since binding to port 443 usually requires root privileges, you have two options:
1. **Port Forwarding (Recommended)**: Run the service on a high port (e.g., 8443) and use `iptables` or your firewall to forward traffic from 443 to 8443.
2. **Capabilities**: Grant the binary permission to bind to low ports: `sudo setcap 'cap_net_bind_service=+ep' ./soundtouch-service`.
3. **Reverse Proxy**: Use Nginx or Caddy as described below.
### 4. Reverse Proxy (Optional)
1. **Generate a certificate** for the Bose domains signed by your Root CA.
2. **Configure Nginx** to use this certificate and proxy requests to `soundtouch-service`.
```nginx
server {
listen 443 ssl;
server_name streaming.bose.com bmx.bose.com stats.bose.com updates.bose.com;
ssl_certificate /path/to/generated-cert.crt;
ssl_certificate_key /path/to/generated-cert.key;
# Secure TLS configuration (matches soundtouch-service defaults)
ssl_protocols TLSv1.2;
ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-CHACHA20-POLY1305:AES128-GCM-SHA256:AES256-GCM-SHA384';
location / {
proxy_pass http://localhost:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
```
## 5. Manual CA Injection (Legacy/Manual)
If you prefer to inject the CA certificate manually:
1. Copy `ca.crt` to the device:
```bash
scp data/certs/ca.crt root@{deviceIP}:/tmp/
```
2. Append it to the trust store on the device:
```bash
ssh root@{deviceIP} "(rw || mount -o remount,rw /) && cat /tmp/ca.crt >> /etc/pki/tls/certs/ca-bundle.crt"
```
## 6. Verifying Connectivity
You can verify that your device can correctly reach the `soundtouch-service` over HTTPS using the management web UI.
In the **Migration Summary** for a device, you will find an **HTTPS Connection Test** section:
- **Test with Explicit CA.crt**: Uploads a temporary copy of the Root CA to the device and uses `curl --cacert` to verify the connection. Use this to verify your HTTPS setup *before* modifying the device's shared trust store.
- **Test with Shared Trust Store**: Uses the device's default trust store. Use this to verify that your CA injection was successful and the device now natively trusts your local server.
+753
View File
@@ -0,0 +1,753 @@
# IoT Implementation Guide
## Overview
This guide provides technical implementation details for integrating with the Bose SoundTouch IoT configuration system. It covers the AWS IoT Core integration, certificate management, and device shadow operations.
## Prerequisites
- AWS IoT Core account and permissions
- Understanding of MQTT protocol
- Knowledge of X.509 certificate management
- Familiarity with JSON and protobuf serialization
## Architecture Components
### Core System Design
```
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Mobile App │ │ Alexa Voice │ │ Web Interface │
│ │ │ Assistant │ │ │
└─────────┬───────┘ └─────────┬────────┘ └─────────┬───────┘
│ │ │
└──────────────────────┼───────────────────────┘
┌────────────▼──────────────┐
│ AWS IoT Core │
│ (MQTT Broker + │
│ Device Shadows) │
└────────────┬──────────────┘
│ MQTT/TLS
┌────────────▼──────────────┐
│ SoundTouch Device │
│ │
│ ┌─────────────────────┐ │
│ │ IoT Service │ │
│ │ (/opt/Bose/IoT) │ │
│ └─────────────────────┘ │
│ ┌─────────────────────┐ │
│ │ BoseApp Service │ │
│ │ (/opt/Bose/BoseApp) │ │
│ └─────────────────────┘ │
└───────────────────────────┘
```
### Configuration Flow
```
1. Device Boot
2. Read IoT.xml (/mnt/nv/BoseApp-Persistence/1/IoT.xml)
3. Load Certificates (/mnt/nv/IoTCerts/)
4. Establish MQTT/TLS Connection
5. Subscribe to Device Shadow Topics
6. Publish Current Device State
7. Listen for Delta Messages
```
## Implementation Details
### 1. Configuration File Management
#### IoT.xml Structure
```xml
<?xml version="1.0" encoding="UTF-8" ?>
<Configuration
clientID="{device-unique-uuid}"
iotEndpoint="{aws-iot-endpoint}"
deployment="{PROD|DEV|TEST}" />
```
#### Loading Configuration (C++ Implementation)
```cpp
#include <rapidxml/rapidxml.hpp>
#include <fstream>
struct IoTConfig {
std::string clientID;
std::string iotEndpoint;
std::string deployment;
};
IoTConfig loadIoTConfig(const std::string& configPath) {
std::ifstream file(configPath);
std::string content((std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>());
rapidxml::xml_document<> doc;
doc.parse<0>(&content[0]);
auto configNode = doc.first_node("Configuration");
IoTConfig config;
config.clientID = configNode->first_attribute("clientID")->value();
config.iotEndpoint = configNode->first_attribute("iotEndpoint")->value();
config.deployment = configNode->first_attribute("deployment")->value();
return config;
}
```
### 2. Certificate Management
#### Certificate Files Structure
```
/mnt/nv/IoTCerts/
├── iot-cert.pem.crt # Device client certificate
├── iot-private.pem.key # Device private key
└── default.pem # Additional cert data
/var/lib/iot/
└── rootCA.crt # AWS IoT Root CA
```
#### Certificate Registration Process
```cpp
#include <openssl/x509.h>
#include <openssl/rsa.h>
#include <openssl/pem.h>
class IoTCertificateManager {
private:
static const std::string CERT_ENDPOINT;
static const std::string CERT_PATH;
static const std::string KEY_PATH;
public:
bool generateCSR() {
// Generate EC key pair
EC_KEY* eckey = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1);
EC_KEY_generate_key(eckey);
// Create certificate request
X509_REQ* req = X509_REQ_new();
X509_REQ_set_version(req, 0);
// Set subject name
X509_NAME* name = X509_NAME_new();
X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC,
(unsigned char*)clientID.c_str(), -1, -1, 0);
X509_REQ_set_subject_name(req, name);
// Set public key
EVP_PKEY* pkey = EVP_PKEY_new();
EVP_PKEY_set1_EC_KEY(pkey, eckey);
X509_REQ_set_pubkey(req, pkey);
// Sign request
X509_REQ_sign(req, pkey, EVP_sha256());
return sendCSRToEndpoint(req, pkey);
}
bool sendCSRToEndpoint(X509_REQ* req, EVP_PKEY* pkey) {
// Send CSR to voice.api.bose.io/alexa/certificate
// Receive certificate response
// Store certificate and private key
return true;
}
};
const std::string IoTCertificateManager::CERT_ENDPOINT =
"https://voice.api.bose.io/alexa/certificate";
const std::string IoTCertificateManager::CERT_PATH =
"/mnt/nv/IoTCerts/iot-cert.pem.crt";
const std::string IoTCertificateManager::KEY_PATH =
"/mnt/nv/IoTCerts/iot-private.pem.key";
```
### 3. MQTT Connection Implementation
#### AWS IoT SDK Integration
```cpp
#include <aws/iot/MqttClient.h>
#include <aws/iot/ShadowClient.h>
class IoTConnectionManager {
private:
std::unique_ptr<awsiotsdk::MqttClient> mqttClient;
std::unique_ptr<awsiotsdk::Shadow> shadowClient;
IoTConfig config;
public:
awsiotsdk::ResponseCode connect() {
// Setup connection parameters
std::string endpoint = config.iotEndpoint;
uint16_t port = 8883; // MQTT over SSL
// Load certificates
std::string certPath = "/mnt/nv/IoTCerts/iot-cert.pem.crt";
std::string keyPath = "/mnt/nv/IoTCerts/iot-private.pem.key";
std::string rootCaPath = "/var/lib/iot/rootCA.crt";
// Create network connection
auto networkConnection = std::make_shared<awsiotsdk::network::MbedTLSConnection>(
endpoint, port, rootCaPath, certPath, keyPath
);
// Create MQTT client
mqttClient = awsiotsdk::MqttClient::Create(networkConnection);
if (!mqttClient) {
return awsiotsdk::ResponseCode::FAILURE;
}
// Connect with client ID
auto connectPacket = awsiotsdk::mqtt::ConnectPacket::Create(
config.clientID,
true, // cleanSession
awsiotsdk::mqtt::QoS::QOS0,
nullptr // will options
);
return mqttClient->Connect(std::chrono::milliseconds(5000), connectPacket);
}
awsiotsdk::ResponseCode initializeShadow() {
shadowClient = awsiotsdk::Shadow::Create(mqttClient);
if (!shadowClient) {
return awsiotsdk::ResponseCode::FAILURE;
}
// Subscribe to shadow delta
auto deltaHandler = [this](const std::string& thingName,
const std::string& payload) {
handleShadowDelta(thingName, payload);
};
return shadowClient->PerformUpdateAsync(
config.clientID,
"", // jsonString
deltaHandler,
std::chrono::seconds(10)
);
}
};
```
### 4. Device Shadow Operations
#### Shadow Message Structures
```cpp
#include <rapidjson/document.h>
#include <rapidjson/writer.h>
#include <rapidjson/stringbuffer.h>
struct DeviceState {
std::string deviceState; // "CONNECTED" | "DISCONNECTED"
std::string powerState; // "ON" | "OFF"
std::string zoneState; // Zone configuration
std::string groupState; // Multi-room group info
};
class ShadowMessageBuilder {
public:
static std::string createReportedState(const DeviceState& state) {
rapidjson::Document doc;
doc.SetObject();
auto& allocator = doc.GetAllocator();
// Create state object
rapidjson::Value stateObj(rapidjson::kObjectType);
rapidjson::Value reportedObj(rapidjson::kObjectType);
// Add reported state fields
reportedObj.AddMember("deviceState",
rapidjson::Value(state.deviceState.c_str(), allocator),
allocator);
reportedObj.AddMember("powerState",
rapidjson::Value(state.powerState.c_str(), allocator),
allocator);
reportedObj.AddMember("zoneState",
rapidjson::Value(state.zoneState.c_str(), allocator),
allocator);
reportedObj.AddMember("groupState",
rapidjson::Value(state.groupState.c_str(), allocator),
allocator);
stateObj.AddMember("reported", reportedObj, allocator);
doc.AddMember("state", stateObj, allocator);
// Serialize to string
rapidjson::StringBuffer buffer;
rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
doc.Accept(writer);
return buffer.GetString();
}
static DeviceState parseDesiredState(const std::string& json) {
rapidjson::Document doc;
doc.Parse(json.c_str());
DeviceState state;
if (doc.HasMember("state") && doc["state"].HasMember("desired")) {
auto& desired = doc["state"]["desired"];
if (desired.HasMember("powerState")) {
state.powerState = desired["powerState"].GetString();
}
if (desired.HasMember("zoneState")) {
state.zoneState = desired["zoneState"].GetString();
}
if (desired.HasMember("groupState")) {
state.groupState = desired["groupState"].GetString();
}
}
return state;
}
};
```
#### Shadow Update Implementation
```cpp
class IoTShadowManager {
private:
std::shared_ptr<awsiotsdk::Shadow> shadowClient;
std::string thingName;
DeviceState currentState;
public:
awsiotsdk::ResponseCode updateDeviceState(const DeviceState& newState) {
currentState = newState;
std::string payload = ShadowMessageBuilder::createReportedState(newState);
auto responseHandler = [](const std::string& thingName,
awsiotsdk::ShadowRequestType requestType,
awsiotsdk::ShadowResponseType responseType,
rapidjson::Document& payload) {
if (responseType == awsiotsdk::ShadowResponseType::Accepted) {
// Shadow update successful
std::cout << "Shadow updated successfully" << std::endl;
} else {
// Handle rejection
std::cout << "Shadow update rejected" << std::endl;
}
};
return shadowClient->PerformUpdateAsync(
thingName,
payload,
responseHandler,
std::chrono::seconds(10)
);
}
void handleShadowDelta(const std::string& thingName,
const std::string& payload) {
DeviceState desiredState = ShadowMessageBuilder::parseDesiredState(payload);
// Apply desired state changes to device
if (!desiredState.powerState.empty()) {
applyPowerStateChange(desiredState.powerState);
}
if (!desiredState.zoneState.empty()) {
applyZoneStateChange(desiredState.zoneState);
}
if (!desiredState.groupState.empty()) {
applyGroupStateChange(desiredState.groupState);
}
// Report updated state back to shadow
updateDeviceState(currentState);
}
};
```
### 5. Service Integration
#### Shepherd Service Configuration
```xml
<!-- /opt/Bose/etc/Shepherd-noncore.xml -->
<ShepherdConfig>
<daemon name="STSCertified"/>
<daemon name="IoT">
<env name="IOT_CONFIG_PATH">/mnt/nv/BoseApp-Persistence/1/IoT.xml</env>
<env name="IOT_CERT_PATH">/mnt/nv/IoTCerts</env>
</daemon>
<daemon name="TPDA">
<arg>-c</arg>
<arg>/opt/Bose/etc/Voice.xml</arg>
</daemon>
</ShepherdConfig>
```
#### System Startup Integration
```bash
#!/bin/bash
# /etc/init.d/SoundTouch fragment
# Create IoT directories
mkdir -p /mnt/nv/BoseLog /mnt/nv/IoTCerts /mnt/nv/BoseApp-Persistence/1
mkdir -m 700 -p /mnt/nv/BoseApp-Persistence/1/Keys
# Set proper permissions for certificate storage
chmod 700 /mnt/nv/IoTCerts
chown iot:iot /mnt/nv/IoTCerts
# Start shepherd daemon manager
shepherdd --config-dir /opt/Bose/etc --run-dir /var/run/shepherd
```
## Error Handling and Debugging
### Connection Retry Logic
```cpp
class ConnectionRetryManager {
private:
int maxRetries = 10;
int retryDelaySeconds = 5;
public:
awsiotsdk::ResponseCode connectWithRetry(IoTConnectionManager& manager) {
for (int attempt = 1; attempt <= maxRetries; ++attempt) {
std::cout << "Connection attempt " << attempt
<< " to MQTT port at host " << config.iotEndpoint << std::endl;
auto result = manager.connect();
if (result == awsiotsdk::ResponseCode::SUCCESS) {
std::cout << "Successfully connected to MQTT server" << std::endl;
return result;
}
std::cout << "MQTT port not available. Retrying in "
<< retryDelaySeconds << " seconds" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(retryDelaySeconds));
retryDelaySeconds *= 2; // Exponential backoff
}
std::cerr << "Failed to connect after " << maxRetries << " attempts" << std::endl;
return awsiotsdk::ResponseCode::FAILURE;
}
};
```
### Logging and Monitoring
```cpp
class IoTLogger {
public:
static void logConnectionStatus(const std::string& status) {
std::cout << "[IoT] Connection status: " << status << std::endl;
}
static void logShadowResponse(awsiotsdk::ShadowResponseType response,
const std::string& payload) {
if (response == awsiotsdk::ShadowResponseType::Accepted) {
std::cout << "[IoT] Shadow response: accepted. Payload: " << payload << std::endl;
} else {
std::cout << "[IoT] Shadow response: rejected" << std::endl;
}
}
static void logCertificateStatus(bool success) {
if (success) {
std::cout << "[IoT] Certificate generated successfully" << std::endl;
} else {
std::cerr << "[IoT] Failed to generate iot certificate" << std::endl;
}
}
};
```
## Testing and Validation
### Unit Test Example
```cpp
#include <gtest/gtest.h>
class IoTConfigTest : public ::testing::Test {
protected:
void SetUp() override {
// Create test configuration file
std::ofstream file("/tmp/test_iot.xml");
file << R"(<?xml version="1.0" encoding="UTF-8" ?>
<Configuration clientID="test-client-id"
iotEndpoint="test.iot.amazonaws.com"
deployment="TEST" />)";
file.close();
}
};
TEST_F(IoTConfigTest, LoadConfiguration) {
auto config = loadIoTConfig("/tmp/test_iot.xml");
EXPECT_EQ(config.clientID, "test-client-id");
EXPECT_EQ(config.iotEndpoint, "test.iot.amazonaws.com");
EXPECT_EQ(config.deployment, "TEST");
}
TEST_F(IoTConfigTest, ShadowMessageBuilder) {
DeviceState state;
state.deviceState = "CONNECTED";
state.powerState = "ON";
std::string json = ShadowMessageBuilder::createReportedState(state);
// Verify JSON contains expected fields
EXPECT_TRUE(json.find("\"deviceState\":\"CONNECTED\"") != std::string::npos);
EXPECT_TRUE(json.find("\"powerState\":\"ON\"") != std::string::npos);
}
```
## Security Best Practices
1. **Certificate Management**
- Store private keys with 600 permissions
- Rotate certificates regularly
- Use hardware security modules when available
2. **Network Security**
- Always use TLS 1.2 or higher
- Validate certificate chains
- Implement certificate pinning
3. **Configuration Security**
- Encrypt sensitive configuration data
- Use secure storage for credentials
- Implement configuration validation
## Troubleshooting Common Issues
### Certificate Problems
```bash
# Check certificate validity
openssl x509 -in /mnt/nv/IoTCerts/iot-cert.pem.crt -text -noout
# Verify private key matches certificate
openssl x509 -noout -modulus -in /mnt/nv/IoTCerts/iot-cert.pem.crt | openssl md5
openssl rsa -noout -modulus -in /mnt/nv/IoTCerts/iot-private.pem.key | openssl md5
```
### Connection Issues
```bash
# Test MQTT connectivity
mosquitto_pub -h a2bhvr9c4wn4ya.iot.us-east-1.amazonaws.com \
-p 8883 --cafile /var/lib/iot/rootCA.crt \
--cert /mnt/nv/IoTCerts/iot-cert.pem.crt \
--key /mnt/nv/IoTCerts/iot-private.pem.key \
-t '$aws/things/test/shadow/update' \
-m '{"state":{"reported":{"test":"value"}}}'
```
### Service Debugging
```bash
# Check service status
ps aux | grep IoT
# Monitor system logs
tail -f /mnt/nv/BoseLog/IoT.log
# Check Shepherd status
shepherdd --status
```
## MQTT Monitoring and Research
### Direct Device Credential Access
With device certificates and private keys available from firmware backups, it's technically possible to monitor MQTT traffic:
```bash
# Subscribe to your device's shadow events only
CLIENT_ID="577ecfcc-2db3-4989-92c9-76d7704f9fb3" # Your device's UUID
mosquitto_sub -h a2bhvr9c4wn4ya.iot.us-east-1.amazonaws.com \
-p 8883 --cafile /var/lib/iot/rootCA.crt \
--cert /mnt/nv/IoTCerts/iot-cert.pem.crt \
--key /mnt/nv/IoTCerts/iot-private.pem.key \
-t "\$aws/things/$CLIENT_ID/shadow/update/accepted"
```
### Security Constraints and Limitations
#### AWS IoT Policy Restrictions
Device certificates are bound to restrictive policies:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "iot:Connect",
"Resource": "arn:aws:iot:us-east-1:*:client/${iot:ClientId}"
},
{
"Effect": "Allow",
"Action": ["iot:Publish", "iot:Subscribe", "iot:Receive"],
"Resource": [
"arn:aws:iot:us-east-1:*:topic/$aws/things/${iot:ClientId}/shadow/*"
]
}
]
}
```
**Limitations:**
- Access only to your specific device topics
- No wildcard subscriptions (`+` or `#`)
- No cross-device monitoring
- Potential IP geolocation restrictions
- Certificate revocation for unusual activity
### Alternative Monitoring Approaches
#### Network Traffic Capture (Recommended)
```bash
# Capture MQTT traffic patterns without authentication
tcpdump -i eth0 -s0 -w soundtouch_iot.pcap host a2bhvr9c4wn4ya.iot.us-east-1.amazonaws.com
# Monitor connection patterns in real-time
tcpdump -i eth0 -n -A "host a2bhvr9c4wn4ya.iot.us-east-1.amazonaws.com and port 8883"
# Extract timing and packet size information
tcpdump -i eth0 -ttt -s0 "host a2bhvr9c4wn4ya.iot.us-east-1.amazonaws.com"
```
#### Local MQTT Broker for Testing
```bash
# Set up local Mosquitto broker
sudo apt-get install mosquitto mosquitto-clients
# Configure TLS (optional)
cat > /etc/mosquitto/conf.d/tls.conf << EOF
port 8883
cafile /path/to/ca.crt
certfile /path/to/server.crt
keyfile /path/to/server.key
require_certificate true
use_identity_as_username true
EOF
# Test local shadow operations
mosquitto_pub -h localhost -p 8883 \
-t '$aws/things/test-device/shadow/update' \
-m '{"state":{"reported":{"deviceState":"CONNECTED"}}}'
```
### Message Analysis and Documentation
Expected shadow message patterns:
```cpp
// Power state transitions
{
"state": {
"reported": {
"deviceState": "CONNECTED",
"powerState": "ON|OFF"
}
},
"timestamp": 1703875200
}
// Audio control updates
{
"state": {
"reported": {
"volume": 25,
"muted": false,
"source": "SPOTIFY"
}
}
}
// Multi-room coordination
{
"state": {
"reported": {
"zoneState": "master|slave",
"groupMembers": ["device1", "device2"],
"groupName": "Living Room"
}
}
}
```
### Legal and Ethical Guidelines
**Important Warnings:**
- Only monitor devices you personally own
- Using device credentials outside the device may violate Bose Terms of Service
- Accessing Bose's AWS infrastructure could be considered unauthorized
- Certificate abuse may result in device blacklisting
- Service shutdown in May 2026 makes this a temporary research opportunity
**Recommended Usage:**
- Document message formats for local alternative development
- Understand state transition patterns
- Test compatibility with local MQTT brokers
- Prepare migration strategies before cloud shutdown
### Research Implementation Example
```cpp
class IoTResearchMonitor {
private:
std::string deviceClientId;
std::ofstream messageLog;
public:
void captureMessagePatterns() {
// Subscribe only to owned device topics
std::string topic = "$aws/things/" + deviceClientId + "/shadow/update/accepted";
auto messageHandler = [this](const std::string& topic, const std::string& payload) {
// Log message structure for analysis
messageLog << "Topic: " << topic << std::endl;
messageLog << "Payload: " << payload << std::endl;
messageLog << "Timestamp: " << getCurrentTimestamp() << std::endl;
messageLog << "---" << std::endl;
// Parse and document state transitions
documentStateTransition(payload);
};
// WARNING: Only use with your own device certificates
connectToAWSIoT(messageHandler);
}
void documentStateTransition(const std::string& json) {
// Analyze JSON structure for local implementation
rapidjson::Document doc;
doc.Parse(json.c_str());
if (doc.HasMember("state") && doc["state"].HasMember("reported")) {
// Document field types and value ranges
auto& reported = doc["state"]["reported"];
for (auto& field : reported.GetObject()) {
std::cout << "Field: " << field.name.GetString()
<< ", Type: " << getJSONType(field.value) << std::endl;
}
}
}
};
```
This implementation guide provides the foundation for integrating with the Bose SoundTouch IoT system using AWS IoT Core, certificate-based authentication, and device shadow operations. The monitoring capabilities should be used responsibly and only for research purposes to develop local alternatives.
+218
View File
@@ -0,0 +1,218 @@
# MAC Address to Serial Number Mapping
**Understanding and troubleshooting device identification in SoundTouch service**
This guide explains how the SoundTouch service handles device identification through MAC address to serial number mapping, and how to troubleshoot related issues.
## 📋 **Overview**
The SoundTouch service uses two different identifiers for devices:
- **MAC Address** (`A81B6A536A98`) - Used in HTTP API requests and UPnP discovery
- **Serial Number** (`I6332527703739342000020`) - Used for internal file storage
The service automatically maps between these identifiers so that API requests using MAC addresses can access files stored using serial numbers.
## 🔍 **How It Works**
### Request Flow
```
1. HTTP Request: GET /streaming/account/3230304/device/A81B6A536A98/presets
2. MAC Resolution: A81B6A536A98 → I6332527703739342000020
3. File Access: accounts/3230304/devices/I6332527703739342000020/Presets.xml
```
### UPnP Discovery Integration
The service extracts MAC addresses from UPnP device descriptions:
```xml
<!-- From http://192.168.1.100:8091/XD/BO5EBO5E-F00D-F00D-FEED-A81B6A536A98.xml -->
<root xmlns="urn:schemas-upnp-org:device-1-0">
<device>
<friendlyName>Sound Machinery</friendlyName>
<modelName>SoundTouch 10</modelName>
<serialNumber>A81B6A536A98</serialNumber> <!-- MAC address here -->
</device>
</root>
```
## ⚙️ **Automatic Setup**
The mapping is created automatically when the service starts:
1. **Directory Scan**: Service scans `data/accounts/{account}/devices/{serial}/`
2. **DeviceInfo.xml**: Reads MAC address from each device's info file
3. **Mapping Creation**: Creates MAC → Serial mapping in memory
4. **Normalization**: Handles different MAC address formats automatically
## 🛠️ **Supported MAC Address Formats**
The service handles all common MAC address formats automatically:
| Format | Example | Status |
|-------------|---------------------|-------------|
| Standard | `A81B6A536A98` | ✅ Supported |
| Lowercase | `a81b6a536a98` | ✅ Supported |
| With Colons | `A8:1B:6A:53:6A:98` | ✅ Supported |
| With Dashes | `A8-1B-6A-53-6A-98` | ✅ Supported |
| Mixed Case | `a81B6a536A98` | ✅ Supported |
| With Spaces | ` A81B6A536A98 ` | ✅ Supported |
## 🔧 **Troubleshooting**
### Problem: API requests fail with "file not found" errors
**Symptoms:**
```
GET /streaming/account/3230304/device/A81B6A536A98/presets
→ 500 Internal Server Error
→ Log: "open .../devices/A81B6A536A98/Presets.xml: no such file or directory"
```
**Diagnosis:**
1. Check if mapping exists:
```bash
# Look for device directory
ls data/accounts/3230304/devices/
# Should show serial numbers like: I6332527703739342000020
```
2. Check DeviceInfo.xml:
```bash
cat data/accounts/3230304/devices/I6332527703739342000020/DeviceInfo.xml
# Look for <macAddress> field
```
**Solutions:**
#### Solution 1: Restart the Service
The mapping is created at startup. Simply restart:
```bash
sudo systemctl restart soundtouch-service
```
#### Solution 2: Check DeviceInfo.xml Format
Ensure the MAC address is present:
```xml
<info deviceID="I6332527703739342000020">
<networkInfo type="SCM">
<macAddress>A81B6A536A98</macAddress> <!-- Must be present -->
<ipAddress>192.168.178.35</ipAddress>
</networkInfo>
</info>
```
#### Solution 3: Manual Device Addition
If the device was added manually, ensure proper structure:
```bash
# Create device directory using serial number
mkdir -p data/accounts/3230304/devices/I6332527703739342000020
# Create DeviceInfo.xml with MAC address
cat > data/accounts/3230304/devices/I6332527703739342000020/DeviceInfo.xml << EOF
<?xml version="1.0" encoding="UTF-8"?>
<info deviceID="I6332527703739342000020">
<name>My SoundTouch Device</name>
<networkInfo type="SCM">
<macAddress>A81B6A536A98</macAddress>
<ipAddress>192.168.1.100</ipAddress>
</networkInfo>
</info>
EOF
```
### Problem: UPnP discovery not creating mappings
**Check UPnP accessibility:**
```bash
# Test UPnP endpoint directly
curl http://192.168.1.100:8091/XD/BO5EBO5E-F00D-F00D-FEED-A81B6A536A98.xml
# Should return XML with <serialNumber> field
```
**Enable debug logging:**
```bash
# Check service logs for UPnP activity
journalctl -u soundtouch-service -f | grep UPnP
```
### Problem: Case or format mismatches
This should be handled automatically, but you can verify:
**Test different formats:**
```bash
# All of these should work the same:
curl http://localhost:8000/streaming/account/3230304/device/A81B6A536A98/presets
curl http://localhost:8000/streaming/account/3230304/device/a81b6a536a98/presets
curl http://localhost:8000/streaming/account/3230304/device/A8:1B:6A:53:6A:98/presets
```
## 📊 **Monitoring and Diagnostics**
### Check Current Mappings
The service logs mapping creation at startup:
```bash
journalctl -u soundtouch-service | grep "MAC.*serial"
```
### Verify File Structure
Ensure proper directory organization:
```
data/
└── accounts/
└── 3230304/
└── devices/
└── I6332527703739342000020/ # Serial number directory
├── DeviceInfo.xml # Contains MAC address
├── Presets.xml
└── Sources.xml
```
## 🔗 **Related Documentation**
- [Device Initial Setup](DEVICE-INITIAL-SETUP.md) - Setting up new devices
- [Troubleshooting Guide](TROUBLESHOOTING.md) - General troubleshooting steps
- [SoundTouch Service](SOUNDTOUCH-SERVICE.md) - Service configuration and management
## 🏗️ **Technical Implementation**
For developers interested in the technical details:
### Normalization Algorithm
```go
// MAC addresses are normalized by:
// 1. Removing spaces, colons, and dashes
// 2. Converting to uppercase
// Examples:
// "a8:1b:6a:53:6a:98" → "A81B6A536A98"
// "A8-1B-6A-53-6A-98" → "A81B6A536A98"
```
### Lookup Process
```go
// 1. Try exact match first
// 2. If not found, try normalized version
// 3. Return serial number for file access
```
### Performance
- **Lookup Time**: O(1) - Hash map lookup
- **Memory Usage**: ~40 bytes per device mapping
- **Initialization**: Scans all devices once at startup
## 📝 **Best Practices**
1. **Use Discovery**: Let UPnP discovery create mappings automatically
2. **Consistent Format**: Store MAC addresses consistently in DeviceInfo.xml
3. **Service Restart**: Restart service after manual device additions
4. **Monitoring**: Check logs for mapping creation during startup
5. **Backup**: Keep DeviceInfo.xml files backed up
## ⚠️ **Known Limitations**
- Mappings are created only at service startup
- Manual device additions require service restart
- MAC addresses must be present in DeviceInfo.xml
- No automatic cleanup of stale mappings (restart required)
+418
View File
@@ -0,0 +1,418 @@
# THIS IS A PLANNED TO BE THE MIGRATION GUIDE
> This migration guide is not finalized, yet.
> We're using it as an orientation for the required implementation.
---
# Complete Migration Guide - From Bose Cloud to Local SoundTouch Service
## Overview
This guide will walk you through migrating your Bose SoundTouch speakers from Bose's cloud services to AfterTouch, your own local SoundTouch service. By the end of this process, your speakers will be completely independent of Bose's servers while retaining all their functionality.
> **💡 Why Migrate?** Bose announced the shutdown of their SoundTouch cloud services in May 2026. This migration ensures your speakers continue working indefinitely with enhanced local control and monitoring.
## What You'll Need
### Hardware Requirements
- **Raspberry Pi 4 or similar** (minimum: Raspberry Pi Zero 2W)
- **MicroSD card** (16GB or larger)
- **USB drive** (for device preparation)
- **Network connection** for your Raspberry Pi
### Before You Start
- **List all your SoundTouch devices** and their current locations
- **Note your current presets and favorites** (they will be preserved)
- **Ensure devices are on the same network** as your future SoundTouch service
- **Basic computer skills** (following instructions, using a web browser)
### Time Estimate
- **Setup**: 30-60 minutes for the service installation
- **Per Device**: 10-15 minutes for each speaker migration
- **Total**: 1-3 hours depending on number of devices
## Step 1: Install SoundTouch Service
### Option A: Raspberry Pi Installation (Recommended)
#### 1.1 Prepare Your Raspberry Pi
1. **Flash Raspberry Pi OS** to your SD card using Raspberry Pi Imager (see the raspberrypi.com documentation)
2. **Enable SSH** during imaging or create an empty `ssh` file on the boot partition
3. **Boot your Pi** and connect it to your network
4. **Find your Pi's IP address** (check your router or use `ping raspberrypi.local`)
#### 1.2 Install SoundTouch Service
Connect to your Pi via SSH and run:
```bash
# Download and install
curl -sSL https://github.com/gesellix/Bose-SoundTouch/releases/latest/download/install.sh | bash
# Start the service
sudo systemctl enable soundtouch-service
sudo systemctl start soundtouch-service
```
#### 1.3 Verify Installation
1. Open your web browser
2. Go to `http://[PI_IP_ADDRESS]:8000` (replace with your Pi's IP)
3. You should see the **SoundTouch Service Dashboard**
![SoundTouch Service Dashboard](../images/dashboard-home.png)
*Example: SoundTouch Service main dashboard*
### Option B: Docker Installation
If you prefer Docker, run:
```bash
docker run -d \
--name soundtouch-service \
--restart unless-stopped \
-p 8000:8000 \
-p 8443:8443 \
-v soundtouch-data:/data \
gesellix/soundtouch-service:latest
```
## Step 2: Create Your Account
### 2.1 Initial Setup
1. **Open the dashboard** at `http://[SERVICE_IP]:8000`
2. Click **"Create New Account"**
3. **Fill in your details**:
- Account Name: `My Home Audio`
- Email: `your@email.com` (optional, for notifications)
- Migration Strategy: `Gradual` (recommended)
![Account Creation](../images/account-creation.png)
*Example: Account creation form*
### 2.2 Account Configuration
After creation, you'll see your **Account Dashboard**:
- **Account ID**: Unique identifier (e.g., `acc_home_audio_001`)
- **Status**: `Active - Ready for Migration`
- **Device Count**: Initially 0
- **Migration Status**: `Prepared`
![Account Dashboard](../images/account-dashboard.png)
*Example: Fresh account dashboard ready for device migration*
### 2.3 Initial Settings
Once your account is created, configure the global settings:
1. **Settings**:
- Check **Target Domain**: Ensure it's reachable from the speaker (e.g., `soundtouch.fritz.box`).
- **DNS Discovery**: Enable DNS discovery on port `:53`. This is crucial for the DNS hook migration method.
2. **Devices**:
- Go to the **"Device Discovery"** tab.
- Click **"Scan Network"** or manually add a speaker via IP address.
- Your devices should appear with **SSH Status**: `Enabled`.
![Device Discovery](../images/device-discovery.png)
*Example: Discovered devices with remote access enabled*
## Step 3: Prepare Your Devices
> **⚠️ Important**: This step temporarily enables SSH access on your speakers. SSH will be automatically disabled after migration unless you choose to keep it enabled.
### 3.1 Enable Remote Services
For each SoundTouch device:
1. **Prepare a USB drive**:
- Format as FAT32
- Create an empty file named `remote_services` (no extension)
- (Optional) Firmware update/reset, see [Bose SoundTouch USB Update](https://downloads.bose.com/ced/soundtouch/soundtouch_usb/index.html)
2. **Insert USB drive** into your SoundTouch speaker
3. **Power cycle** the device (unplug for 10 seconds, then reconnect)
![USB Preparation](../images/usb-remote-services.png)
*Example: USB drive setup for enabling remote services*
## Step 4: Discover and Register Devices
### 4.1 Automatic Discovery
The service automatically scans for SoundTouch devices every 5 minutes. To trigger immediate discovery:
1. **Dashboard****"Devices"** → **"Discover Devices"**
2. **Wait 30-60 seconds** for scan completion
3. **Review discovered devices** in the list
### 4.2 Register Devices to Your Account
For each discovered device:
1. **Click device name** in the discovery list
2. **Verify device information**:
- Name: `Living Room Speaker`
- Model: `SoundTouch 30`
- MAC Address: `A8:1B:6A:53:6A:98`
- IP Address: `192.168.1.100`
- Status: `Discovered - Ready for Registration`
3. **Click "Register to Account"**
4. **Choose registration type**:
- **Fresh Setup**: For new or factory-reset devices
- **Migrate from Bose**: For devices with existing Bose account (recommended)
![Device Registration](../images/device-registration.png)
*Example: Device registration dialog with migration options*
### 4.3 Device Registration Results
After registration, you'll see:
- **Device Status**: `Registered - Active`
- **Account Association**: Your account name
- **Lifecycle State**: `Active`
- **Data Sources**: `Mirror Primary` (initially uses Bose, falls back to local)
## Step 5: Migrate Individual Devices
### 5.1 Step 3: Data Sync
1. **Dashboard****"Devices"** → Select your device
2. Click **"Data Sync"**
3. This fetches configuration (presets, recents, sources) from the speaker to the SoundTouch service.
### 5.2 Step 4: Migration
Once data is synced, proceed to the migration tab for the device:
1. **Backup XML**: Create an off-device backup of the current configuration.
2. **Enable Persistent Remote Service**: This ensures SSH remains available after reboots.
- *Note*: If you see `'rw: command not found'`, you can safely ignore it.
3. **CA Certificate Configuration**:
- **Test with explicit CA**: Verify the speaker can communicate using the local CA.
- **Trust CA now**: Inject the local Root CA into the speaker's trust store.
- **Test with shared trust store**: Verify general HTTPS communication.
4. **Migration Method**:
- Select **"Redirect via DNS hook"**.
- **Test DNS Redirection**: Ensure the speaker correctly resolves the service domain.
5. **Confirm Migration**: Apply the final changes to the speaker.
#### Example Migration Output:
```text
Successfully created off-device backup of current configuration.
Pre-flight: Write access verified.
Resolved soundtouch.fritz.box to 192.168.1.100
Uploaded /mnt/nv/soundtouch-service/aftertouch.resolv.conf
/mnt/nv/rc.local already contains Aftertouch hook logic
(rw || mount -o remount,rw /): sh: rw: command not found
cp /etc/udhcpc.d/50default /etc/udhcpc.d/50default.original:
Applied patch to /etc/udhcpc.d/50default
Verified patch on /etc/udhcpc.d/50default
cp /opt/Bose/udhcpc.script /opt/Bose/udhcpc.script.original:
Applied patch to /opt/Bose/udhcpc.script
Verified patch on /opt/Bose/udhcpc.script
CA certificate already trusted, skipping injection
```
## Step 7: Complete Account Migration
### 7.1 Migrate All Devices
Repeat the migration process for each of your SoundTouch devices. You can migrate multiple devices simultaneously, but we recommend doing 1-2 at a time to monitor progress.
**Migration Dashboard** shows overall progress:
- **Devices Migrated**: `2 of 4 completed`
- **Currently Migrating**: `Living Room Speaker, Kitchen Speaker`
- **Pending Migration**: `Bedroom Speaker, Office Speaker`
- **Estimated Completion**: `3 days remaining`
![Account Migration Status](../images/account-migration.png)
*Example: Account-wide migration progress*
### 7.2 Verify Complete Migration
When all devices are migrated:
1. **Account Status**: `Active - Fully Migrated`
2. **Bose Dependency**: `None`
3. **Local Control**: `100%`
4. **Device Health**: All devices show `Healthy - Local Only`
![Migration Complete](../images/migration-complete.png)
*Example: Completed migration dashboard*
## Step 8: Post-Migration Tasks
1. **Remove USB stick** from the speaker.
2. **Reboot** the device to apply all changes.
### 8.1 Disable Remote Services (Optional)
For enhanced security, you can disable SSH on migrated devices. However, keeping it enabled allows for easier future maintenance or reverts.
### 8.2 Configure Backups
Set up automatic backups of your device configurations:
1. **Dashboard****"Settings"** → **"Backup"**
2. **Enable Automatic Backups**: ✅
3. **Backup Schedule**: `Daily at 2 AM`
4. **Retention**: `Keep 30 days`
5. **Export Location**: `/data/backups` or external storage
![Backup Configuration](../images/backup-setup.png)
*Example: Backup configuration settings*
### 8.3 Set Up Monitoring Alerts (Optional)
Configure notifications for important events:
1. **Dashboard****"Settings"** → **"Notifications"**
2. **Email Notifications**: Enter your email
3. **Alert Types**:
- ✅ Device goes offline
- ✅ Migration failures
- ✅ Service errors
- ✅ Daily health summary
## Troubleshooting Common Issues
### Device Not Discovered
**Problem**: Device doesn't appear in discovery scan
**Solutions**:
1. **Check network**: Ensure device and service are on same network
2. **Verify USB setup**: Confirm `remote_services` file was processed
3. **Power cycle**: Unplug device for 30 seconds, reconnect
4. **Manual add**: Dashboard → "Devices" → "Add Manually" with IP address
### Migration Stuck
**Problem**: Device stuck in "Migrating" status
**Solutions**:
1. **Check device health**: Dashboard → Device → "Health Status"
2. **Review logs**: Dashboard → Device → "View Logs"
3. **Restart migration**: Device → "Migration" → "Restart Process"
4. **Rollback**: Device → "Migration" → "Rollback to Bose"
### Presets Not Working
**Problem**: Saved presets don't work after migration
**Solutions**:
1. **Verify sources**: Check configured sources are still available
2. **Re-authenticate**: Re-login to music services (Spotify, etc.)
3. **Rebuild presets**: Dashboard → Device → "Presets" → "Rebuild from Backup"
### Service Unreachable
**Problem**: Cannot access SoundTouch Service dashboard
**Solutions**:
1. **Check service status**: `sudo systemctl status soundtouch-service`
2. **Restart service**: `sudo systemctl restart soundtouch-service`
3. **Check network**: Verify Pi is connected and accessible
4. **Check ports**: Ensure ports 8000 and 8443 are not blocked
## Advanced Features
### Multi-Zone Management
After migration, your multi-zone setups work seamlessly:
1. **Dashboard****"Zones"**
2. **Create Zone**: Select primary device and slaves
3. **Zone Control**: Play, pause, volume control for entire zone
4. **Individual Control**: Override individual speakers in zone
### Custom Sources
Add custom streaming sources:
1. **Dashboard****"Sources"** → **"Add Custom"**
2. **Configure**:
- Name: `Local Radio Station`
- Stream URL: `http://stream.example.com:8000`
- Image URL: `http://example.com/logo.png`
3. **Assign to devices**: Select which devices can access this source
### API Access
For developers and advanced users:
- **REST API**: `http://[SERVICE_IP]:8000/api/v1/`
- **Documentation**: `http://[SERVICE_IP]:8000/docs`
- **WebSocket Events**: Real-time device status updates
- **Export Data**: JSON/XML export of all device configurations
## Maintenance and Monitoring
### Daily Monitoring
Check your **Dashboard Summary**:
- **All Devices Online**: ✅ Green indicators
- **Response Times**: < 100ms average
- **Error Rate**: < 1%
- **Storage Usage**: Monitor disk space
### Weekly Tasks
1. **Review Health Reports**: Check weekly device health summaries
2. **Update Service**: Check for SoundTouch service updates
3. **Backup Verification**: Ensure backups are completing successfully
4. **Log Review**: Check for any recurring issues or warnings
### Monthly Tasks
1. **Full System Backup**: Export complete account and device data
2. **Performance Review**: Analyze response times and error patterns
3. **Security Update**: Update Raspberry Pi OS and service
4. **Capacity Planning**: Monitor storage and consider expansion
## Getting Help
### Documentation Resources
- **Technical Reference**: `/docs/reference/` - Detailed API and configuration docs
- **Troubleshooting Guide**: `/docs/guides/TROUBLESHOOTING.md` - Common issues and solutions
- **Community Forum**: GitHub Discussions for community support
### Diagnostic Information
When seeking help, provide:
1. **System Information**: Dashboard → "System" → "Download Diagnostic Report"
2. **Device Logs**: Dashboard → Device → "Export Logs"
3. **Migration History**: Dashboard → "Migration" → "Export Timeline"
4. **Current Status**: Screenshot of main dashboard
### Support Channels
- **GitHub Issues**: Technical bugs and feature requests
- **Community Discussions**: User questions and experiences
- **Documentation Updates**: Corrections and improvements
---
## Summary
Congratulations! 🎉 You've successfully migrated your SoundTouch speakers to local control. Your devices are now:
- ✅ **Independent** of Bose cloud services
- ✅ **Fully functional** with all original features preserved
- ✅ **Enhanced** with better monitoring and control
- ✅ **Future-proof** against service shutdowns
**What's Next?**
- **Enjoy your music** with enhanced local control
- **Monitor your system** through the dashboard
- **Share your experience** with the community
- **Explore advanced features** as you become more comfortable
Your SoundTouch speakers will now continue working indefinitely, regardless of external service availability. Welcome to true audio independence! 🔊
+44
View File
@@ -0,0 +1,44 @@
### Professional Migration & Safety Guide
Starting a migration on real hardware requires a "Safety First" approach. This guide outlines the safety features implemented in the `soundtouch-service` and provides a checklist for a successful migration.
#### 🛠 Technical Safety Enhancements
The following features are built into the `soundtouch-service` to ensure stability and easy rollbacks:
1. **Off-Device Backups**: Before any migration starts, the service automatically fetches the original `SoundTouchSdkPrivateCfg.xml` and `/etc/hosts` from your speaker and saves them locally in your `data/default/devices/<SERIAL>/` directory. This ensures you have a recovery path even if the speaker's filesystem becomes inaccessible.
2. **Pre-flight Write Verification**: The migration process includes a mandatory check for SSH write access (`rw`) before attempting any modifications. This prevents "half-baked" migrations where a script might fail halfway through due to a read-only filesystem.
3. **Automatic Safety on Sync**: Running a "Sync" in the Web UI or CLI automatically triggers an off-device backup, making it the perfect first step for any new device discovery.
#### 📋 Professional Migration Checklist
Before you proceed with the actual migration, follow these steps:
1. **Enable SSH Access (Prerequisite)**: This toolkit requires SSH access to your speakers, which is not enabled by default.
- Create an empty file named `remote_services` on a USB stick.
- Insert the USB stick into the SoundTouch speaker's **SERVICE** port.
- Reboot the speaker (unplug and replug).
- The speaker will now allow SSH connections as `root` with no password.
- **Verify**: Run `ssh -oHostKeyAlgorithms=+ssh-rsa root@<SPEAKER-IP>` to confirm access. (Note: older devices may require enabling `ssh-rsa` support).
2. **Network Isolation (Optional but Recommended)**: Ensure the device is on a stable wired connection if possible, or a dedicated 2.4GHz SSID to avoid drops during SSH operations.
3. **Initial Discovery & Sync**:
- Run `soundtouch-cli discover devices` to ensure connectivity.
- Use the Web UI or CLI to "Sync" the device. This will automatically backup your presets and system configuration files to your local server.
4. **Validate SSH Access**: Confirm the device responds to SSH without a password.
- In the Web UI **Migration** tab, select your speaker and verify that the "SSH Connection" status shows ✅ Success.
- This toolkit automatically handles the necessary SSH parameters (ciphers and key exchanges) required by older Bose firmware.
5. **Migration Methods**:
- **XML Migration (Default)**: Less invasive, only changes the application config. Best for simple redirection.
- **Hosts Migration**: Modifies `/etc/hosts` on the device. Good for system-wide redirection of specific domains.
- **ResolvConf Migration**: Points the device to the AfterTouch DNS server. Best for discovering unknown Bose endpoints and dynamic interception. **Note**: This method requires the DNS Discovery Server to be running on port 53. The service includes a pre-flight check to ensure the server is properly bound before allowing this migration.
6. **Monitor Logs**: Run the `soundtouch-service` with `DEBUG` or `INFO` logging to see the step-by-step progress of the migration.
#### 🔄 Rollback Strategy
If something goes wrong or you want to return to the original Bose cloud services:
* **Standard Revert**: Use the "Revert Migration" button in the Web UI or the corresponding CLI command. This restores the `.original` files created on the device.
* **Emergency Recovery**: If the device is unreachable via the UI but SSH still works, you can manually restore the files from your local `data/` directory using `scp` or the backups created on-device (`.original`).
* **Factory Reset**: As a last resort, Bose SoundTouch devices can be factory reset (usually by holding '1' and 'Volume Down' while plugging in). This will wipe all settings and return the device to the stock firmware configuration (the firmware itself remains at the current version, but configurations are reset).
By using the built-in off-device backups and pre-flight checks, the risk of "bricking" or losing configuration during the transition is significantly reduced.
+764
View File
@@ -0,0 +1,764 @@
# MQTT Integration Design for SoundTouch Service
## Overview
This document outlines the design for integrating MQTT support into the existing SoundTouch service to simulate AWS IoT Core functionality. The integration will provide real-time device communication, shadow state management, and prepare for the AWS IoT service shutdown in May 2026.
## Current Architecture Analysis
### Existing Service Structure
```
Bose-SoundTouch/
├── cmd/soundtouch-service/main.go # Main service entry point
├── pkg/
│ ├── client/ # HTTP client for devices
│ ├── config/ # Configuration management
│ ├── discovery/ # Device discovery (UPnP, mDNS)
│ ├── models/ # Data structures
│ └── service/
│ ├── handlers/ # HTTP request handlers
│ │ └── server.go # Main server struct
│ ├── datastore/ # Data persistence
│ ├── proxy/ # HTTP proxying
│ └── [other services]
```
### Key Components
- **Server Struct**: Central HTTP handler in `pkg/service/handlers/server.go`
- **Discovery Service**: UPnP/mDNS device discovery in `pkg/discovery/`
- **DataStore**: Device state persistence in `pkg/service/datastore/`
- **Device Models**: Data structures in `pkg/models/`
## MQTT Integration Design
### 1. New Package Structure
```
pkg/service/mqtt/
├── broker.go # MQTT broker implementation
├── shadow.go # AWS IoT Shadow simulation
├── auth.go # Certificate-based authentication
├── topics.go # Topic routing and handlers
├── bridge.go # HTTP ↔ MQTT state bridging
├── config.go # MQTT configuration
└── client.go # MQTT client utilities
```
### 2. Core Components
#### A. MQTT Broker (`pkg/service/mqtt/broker.go`)
```go
package mqtt
import (
"crypto/tls"
"fmt"
"log"
"sync"
"github.com/mochi-co/mqtt/v2"
"github.com/mochi-co/mqtt/v2/hooks/auth"
"github.com/mochi-co/mqtt/v2/listeners"
)
type Broker struct {
server *mqtt.Server
shadowStore *ShadowStore
bridge *HTTPBridge
authHook *AuthHook
config *Config
running bool
mu sync.RWMutex
}
type Config struct {
Enabled bool `json:"enabled"`
Port int `json:"port"`
TLSEnabled bool `json:"tls_enabled"`
CertFile string `json:"cert_file"`
KeyFile string `json:"key_file"`
DeviceCertPath string `json:"device_cert_path"`
ShadowPersist bool `json:"shadow_persist"`
}
func NewBroker(config *Config) (*Broker, error) {
server := mqtt.New(nil)
shadowStore := NewShadowStore()
authHook := NewAuthHook(config.DeviceCertPath)
return &Broker{
server: server,
shadowStore: shadowStore,
authHook: authHook,
config: config,
}, nil
}
func (b *Broker) Start() error {
// Add TLS listener
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{b.loadServerCert()},
ClientAuth: tls.RequireAndVerifyClientCert,
ClientCAs: b.loadDeviceCAs(),
}
tcp := listeners.NewTCP("mqtt-tls", fmt.Sprintf(":%d", b.config.Port), &listeners.Config{
TLSConfig: tlsConfig,
})
b.server.AddListener(tcp)
// Add hooks
b.server.AddHook(b.authHook, nil)
b.server.AddHook(NewShadowHook(b.shadowStore), nil)
return b.server.Serve()
}
```
#### B. Shadow State Management (`pkg/service/mqtt/shadow.go`)
```go
package mqtt
import (
"encoding/json"
"fmt"
"sync"
"time"
)
type ShadowStore struct {
shadows map[string]*DeviceShadow
mu sync.RWMutex
}
type DeviceShadow struct {
State struct {
Desired map[string]interface{} `json:"desired"`
Reported map[string]interface{} `json:"reported"`
Delta map[string]interface{} `json:"delta,omitempty"`
} `json:"state"`
Version int `json:"version"`
Timestamp int64 `json:"timestamp"`
ClientToken string `json:"clientToken,omitempty"`
}
func NewShadowStore() *ShadowStore {
return &ShadowStore{
shadows: make(map[string]*DeviceShadow),
}
}
func (s *ShadowStore) UpdateShadow(clientID string, payload []byte) (*DeviceShadow, error) {
s.mu.Lock()
defer s.mu.Unlock()
var update DeviceShadow
if err := json.Unmarshal(payload, &update); err != nil {
return nil, err
}
shadow := s.shadows[clientID]
if shadow == nil {
shadow = &DeviceShadow{
State: struct {
Desired map[string]interface{} `json:"desired"`
Reported map[string]interface{} `json:"reported"`
Delta map[string]interface{} `json:"delta,omitempty"`
}{
Desired: make(map[string]interface{}),
Reported: make(map[string]interface{}),
Delta: make(map[string]interface{}),
},
}
s.shadows[clientID] = shadow
}
// Update reported state
if update.State.Reported != nil {
for key, value := range update.State.Reported {
shadow.State.Reported[key] = value
}
}
// Update desired state
if update.State.Desired != nil {
for key, value := range update.State.Desired {
shadow.State.Desired[key] = value
}
}
// Calculate delta
shadow.calculateDelta()
shadow.Version++
shadow.Timestamp = time.Now().Unix()
shadow.ClientToken = update.ClientToken
return shadow, nil
}
func (s *DeviceShadow) calculateDelta() {
s.State.Delta = make(map[string]interface{})
for key, desired := range s.State.Desired {
if reported, exists := s.State.Reported[key]; !exists || reported != desired {
s.State.Delta[key] = desired
}
}
if len(s.State.Delta) == 0 {
s.State.Delta = nil
}
}
```
#### C. HTTP ↔ MQTT Bridge (`pkg/service/mqtt/bridge.go`)
```go
package mqtt
import (
"encoding/json"
"fmt"
"log"
"github.com/gesellix/bose-soundtouch/pkg/models"
"github.com/gesellix/bose-soundtouch/pkg/service/datastore"
)
type HTTPBridge struct {
shadowStore *ShadowStore
dataStore *datastore.DataStore
deviceMap map[string]string // clientID -> deviceID mapping
}
func NewHTTPBridge(shadowStore *ShadowStore, dataStore *datastore.DataStore) *HTTPBridge {
return &HTTPBridge{
shadowStore: shadowStore,
dataStore: dataStore,
deviceMap: make(map[string]string),
}
}
// ShadowToHTTP converts MQTT shadow updates to HTTP API calls
func (b *HTTPBridge) ShadowToHTTP(clientID string, shadow *DeviceShadow) error {
deviceID, exists := b.deviceMap[clientID]
if !exists {
log.Printf("Unknown device clientID: %s", clientID)
return fmt.Errorf("unknown device: %s", clientID)
}
// Handle power state changes
if powerState, ok := shadow.State.Reported["powerState"].(string); ok {
if err := b.updateDevicePower(deviceID, powerState == "ON"); err != nil {
return fmt.Errorf("power update failed: %w", err)
}
}
// Handle volume changes
if volume, ok := shadow.State.Reported["volume"].(float64); ok {
if err := b.updateDeviceVolume(deviceID, int(volume)); err != nil {
return fmt.Errorf("volume update failed: %w", err)
}
}
// Handle source changes
if source, ok := shadow.State.Reported["source"].(string); ok {
if err := b.updateDeviceSource(deviceID, source); err != nil {
return fmt.Errorf("source update failed: %w", err)
}
}
return nil
}
// HTTPToShadow converts HTTP device state to MQTT shadow updates
func (b *HTTPBridge) HTTPToShadow(deviceID string, deviceInfo *models.DeviceInfo) error {
clientID, exists := b.getClientIDForDevice(deviceID)
if !exists {
return nil // Device not connected via MQTT
}
// Create shadow state from device info
shadowState := map[string]interface{}{
"deviceState": "CONNECTED",
"deviceID": deviceInfo.DeviceID,
"name": deviceInfo.Name,
"type": deviceInfo.Type,
}
// Add additional state if available
if status := b.getDeviceStatus(deviceID); status != nil {
shadowState["powerState"] = status.PowerState
shadowState["volume"] = status.Volume
shadowState["source"] = status.Source
}
// Update shadow
shadowUpdate := DeviceShadow{
State: struct {
Desired map[string]interface{} `json:"desired"`
Reported map[string]interface{} `json:"reported"`
Delta map[string]interface{} `json:"delta,omitempty"`
}{
Reported: shadowState,
},
}
payload, _ := json.Marshal(shadowUpdate)
_, err := b.shadowStore.UpdateShadow(clientID, payload)
return err
}
```
### 3. Integration with Existing Server
#### A. Extend Server Struct (`pkg/service/handlers/server.go`)
```go
// Add to existing Server struct
type Server struct {
// ... existing fields ...
// New MQTT fields
mqttBroker *mqtt.Broker
mqttEnabled bool
mqttConfig *mqtt.Config
deviceClientIDs map[string]string // deviceID -> clientID mapping
}
// New initialization method
func (s *Server) initMQTTBroker(config *mqtt.Config) error {
if !config.Enabled {
return nil
}
broker, err := mqtt.NewBroker(config)
if err != nil {
return fmt.Errorf("failed to create MQTT broker: %w", err)
}
// Set up HTTP ↔ MQTT bridge
bridge := mqtt.NewHTTPBridge(broker.ShadowStore(), s.ds)
broker.SetBridge(bridge)
s.mqttBroker = broker
s.mqttEnabled = true
s.mqttConfig = config
s.deviceClientIDs = make(map[string]string)
return nil
}
// Start MQTT broker alongside HTTP server
func (s *Server) StartMQTT() error {
if !s.mqttEnabled {
return nil
}
go func() {
if err := s.mqttBroker.Start(); err != nil {
log.Printf("MQTT broker error: %v", err)
}
}()
return nil
}
```
#### B. Configuration Integration (`cmd/soundtouch-service/main.go`)
```go
// Add to serviceConfig struct
type serviceConfig struct {
// ... existing fields ...
// New MQTT configuration fields
mqttEnabled bool `mapstructure:"mqtt_enabled"`
mqttPort int `mapstructure:"mqtt_port"`
mqttTLSCert string `mapstructure:"mqtt_tls_cert"`
mqttTLSKey string `mapstructure:"mqtt_tls_key"`
mqttDeviceCertPath string `mapstructure:"mqtt_device_cert_path"`
mqttShadowPersist bool `mapstructure:"mqtt_shadow_persist"`
}
// Update main function to initialize MQTT
func main() {
// ... existing initialization ...
// Initialize MQTT if enabled
if cfg.mqttEnabled {
mqttConfig := &mqtt.Config{
Enabled: cfg.mqttEnabled,
Port: cfg.mqttPort,
TLSEnabled: true,
CertFile: cfg.mqttTLSCert,
KeyFile: cfg.mqttTLSKey,
DeviceCertPath: cfg.mqttDeviceCertPath,
ShadowPersist: cfg.mqttShadowPersist,
}
if err := server.InitMQTTBroker(mqttConfig); err != nil {
log.Fatalf("Failed to initialize MQTT broker: %v", err)
}
if err := server.StartMQTT(); err != nil {
log.Fatalf("Failed to start MQTT broker: %v", err)
}
log.Printf("MQTT broker started on port %d", cfg.mqttPort)
}
// ... rest of existing main function ...
}
```
### 4. Enhanced Device Discovery
#### A. MQTT Device Discovery (`pkg/service/mqtt/discovery.go`)
```go
package mqtt
import (
"log"
"time"
"github.com/gesellix/bose-soundtouch/pkg/models"
"github.com/mochi-co/mqtt/v2/packets"
)
type DeviceDiscoveryHook struct {
deviceRegistry map[string]*models.Device
onDeviceFound func(*models.Device)
}
func NewDeviceDiscoveryHook() *DeviceDiscoveryHook {
return &DeviceDiscoveryHook{
deviceRegistry: make(map[string]*models.Device),
}
}
func (h *DeviceDiscoveryHook) ID() string {
return "device-discovery"
}
func (h *DeviceDiscoveryHook) OnConnect(cl *packets.Client, pk packets.Packet) error {
clientID := pk.Connect.ClientIdentifier
log.Printf("MQTT device connected: %s", clientID)
// Create device entry
device := &models.Device{
ID: clientID,
ClientID: clientID,
Name: "MQTT Device",
LastSeen: time.Now(),
MQTTOnline: true,
Source: "mqtt",
}
h.deviceRegistry[clientID] = device
if h.onDeviceFound != nil {
h.onDeviceFound(device)
}
return nil
}
func (h *DeviceDiscoveryHook) OnDisconnect(cl *packets.Client, err error) {
clientID := cl.ID
log.Printf("MQTT device disconnected: %s", clientID)
if device, exists := h.deviceRegistry[clientID]; exists {
device.MQTTOnline = false
device.LastSeen = time.Now()
}
}
```
#### B. Integration with Existing Discovery (`pkg/discovery/mqtt.go`)
```go
package discovery
import (
"context"
"time"
"github.com/gesellix/bose-soundtouch/pkg/models"
)
type MQTTDiscovery struct {
deviceRegistry map[string]*models.Device
enabled bool
}
func NewMQTTDiscovery() *MQTTDiscovery {
return &MQTTDiscovery{
deviceRegistry: make(map[string]*models.Device),
enabled: true,
}
}
func (d *MQTTDiscovery) DiscoverDevices(ctx context.Context, timeout time.Duration) ([]*models.Device, error) {
if !d.enabled {
return []*models.Device{}, nil
}
var devices []*models.Device
for _, device := range d.deviceRegistry {
if device.MQTTOnline {
devices = append(devices, device)
}
}
return devices, nil
}
func (d *MQTTDiscovery) AddDevice(device *models.Device) {
d.deviceRegistry[device.ClientID] = device
}
func (d *MQTTDiscovery) RemoveDevice(clientID string) {
delete(d.deviceRegistry, clientID)
}
```
### 5. Configuration File Extensions
#### A. Default Configuration (`config.yaml`)
```yaml
# Existing configuration...
# MQTT Configuration
mqtt:
enabled: false
port: 8883
tls:
cert_file: "/etc/ssl/certs/soundtouch-mqtt.crt"
key_file: "/etc/ssl/private/soundtouch-mqtt.key"
# Device certificate validation
device_certs:
path: "/etc/soundtouch/device-certs"
auto_load: true
# Shadow state management
shadow:
persist: true
ttl: 86400 # 24 hours
# Bridge configuration
bridge:
enabled: true
sync_interval: 30s
```
#### B. Environment Variable Support
```bash
# MQTT configuration via environment variables
SOUNDTOUCH_MQTT_ENABLED=true
SOUNDTOUCH_MQTT_PORT=8883
SOUNDTOUCH_MQTT_TLS_CERT=/path/to/cert.pem
SOUNDTOUCH_MQTT_TLS_KEY=/path/to/key.pem
SOUNDTOUCH_MQTT_DEVICE_CERT_PATH=/path/to/device/certs
SOUNDTOUCH_MQTT_SHADOW_PERSIST=true
```
### 6. API Extensions
#### A. MQTT Status Endpoints
```go
// Add to handlers
func (s *Server) handleMQTTStatus(c *gin.Context) {
if !s.mqttEnabled {
c.JSON(http.StatusNotImplemented, gin.H{
"error": "MQTT not enabled",
})
return
}
status := gin.H{
"enabled": s.mqttEnabled,
"port": s.mqttConfig.Port,
"connected_devices": len(s.deviceClientIDs),
"shadow_count": s.mqttBroker.ShadowStore().Count(),
}
c.JSON(http.StatusOK, status)
}
// Device shadow endpoint
func (s *Server) handleDeviceShadow(c *gin.Context) {
deviceID := c.Param("deviceId")
clientID, exists := s.deviceClientIDs[deviceID]
if !exists {
c.JSON(http.StatusNotFound, gin.H{
"error": "Device not connected via MQTT",
})
return
}
shadow := s.mqttBroker.ShadowStore().GetShadow(clientID)
if shadow == nil {
c.JSON(http.StatusNotFound, gin.H{
"error": "Shadow not found",
})
return
}
c.JSON(http.StatusOK, shadow)
}
```
### 7. Testing Strategy
#### A. Unit Tests
```go
// pkg/service/mqtt/shadow_test.go
func TestShadowStore_UpdateShadow(t *testing.T) {
store := NewShadowStore()
payload := []byte(`{
"state": {
"reported": {
"powerState": "ON",
"volume": 25
}
}
}`)
shadow, err := store.UpdateShadow("test-client", payload)
assert.NoError(t, err)
assert.Equal(t, "ON", shadow.State.Reported["powerState"])
assert.Equal(t, 25.0, shadow.State.Reported["volume"])
assert.Equal(t, 1, shadow.Version)
}
```
#### B. Integration Tests
```go
// pkg/service/mqtt/integration_test.go
func TestMQTTBrokerIntegration(t *testing.T) {
// Start test broker
broker := setupTestBroker(t)
go broker.Start()
defer broker.Stop()
// Connect test client
client := mqtt.NewClient(mqtt.NewClientOptions().
AddBroker("tls://localhost:8883").
SetClientID("test-device"))
// Test shadow operations
testShadowUpdate(t, client)
testShadowGet(t, client)
}
```
### 8. Migration Path
#### A. Gradual Rollout
1. **Phase 1**: Deploy MQTT broker alongside existing HTTP service (disabled by default)
2. **Phase 2**: Enable MQTT for testing with specific devices
3. **Phase 3**: Enable bidirectional HTTP ↔ MQTT bridging
4. **Phase 4**: Full MQTT support for all discovered devices
5. **Phase 5**: Prepare for AWS IoT shutdown (May 2026)
#### B. Backward Compatibility
- All existing HTTP API endpoints continue to work
- MQTT is purely additive functionality
- Devices can be discovered via HTTP even with MQTT enabled
- Configuration remains optional
### 9. Monitoring and Logging
#### A. MQTT Metrics
```go
type MQTTMetrics struct {
ConnectedDevices int64
MessagesReceived int64
MessagesSent int64
ShadowUpdates int64
AuthenticationFails int64
Uptime time.Duration
}
func (b *Broker) GetMetrics() *MQTTMetrics {
return &MQTTMetrics{
ConnectedDevices: int64(len(b.server.Clients)),
MessagesReceived: b.server.Stats.MessagesReceived,
MessagesSent: b.server.Stats.MessagesSent,
ShadowUpdates: b.shadowStore.UpdateCount(),
AuthenticationFails: b.authHook.FailCount(),
Uptime: time.Since(b.startTime),
}
}
```
#### B. Logging Integration
```go
import "github.com/sirupsen/logrus"
func (b *Broker) setupLogging() {
log := logrus.WithFields(logrus.Fields{
"component": "mqtt-broker",
"port": b.config.Port,
})
b.server.AddHook(&LoggingHook{logger: log}, nil)
}
```
### 10. Security Considerations
#### A. Certificate Validation
- Validate device certificates against known device list
- Implement certificate revocation checking
- Support certificate rotation
#### B. Access Control
- Restrict topic access per device certificate
- Implement rate limiting per client
- Monitor for unusual connection patterns
#### C. Data Protection
- Encrypt shadow data at rest
- Implement secure certificate storage
- Audit logging for security events
## Implementation Timeline
### Week 1: Core Infrastructure
- [ ] Create MQTT package structure
- [ ] Implement basic MQTT broker
- [ ] Add TLS configuration
- [ ] Basic shadow state management
### Week 2: Integration & Bridging
- [ ] Integrate with existing Server struct
- [ ] Implement HTTP ↔ MQTT bridge
- [ ] Device discovery integration
- [ ] Configuration management
### Week 3: Testing & Polish
- [ ] Unit test coverage
- [ ] Integration testing
- [ ] Documentation updates
- [ ] Performance optimization
### Week 4: Deployment & Monitoring
- [ ] Docker container updates
- [ ] Monitoring and metrics
- [ ] Security hardening
- [ ] Production readiness
## Success Criteria
1. **Functional**: MQTT broker accepts device connections using extracted certificates
2. **Compatible**: All existing HTTP functionality continues to work unchanged
3. **Performant**: MQTT operations don't impact HTTP API performance
4. **Secure**: Device authentication and authorization properly implemented
5. **Observable**: Comprehensive logging and metrics for MQTT operations
6. **Maintainable**: Clean separation of MQTT code from existing HTTP logic
This design provides a comprehensive path to add MQTT support while maintaining the existing architecture and ensuring smooth integration with current functionality.
+69
View File
@@ -0,0 +1,69 @@
# Raspberry Pi Installation Guide
This guide explains how to install the `soundtouch-service` as a persistent systemd service on a Raspberry Pi (tested on Raspberry Pi Zero 2W, 3, and 4).
## Automated Installer
We provide a specialized installer script located in the `scripts/raspberry-pi/` directory of the repository.
### Features
* **Automatic start on boot**: Installs a systemd unit.
* **Non-root operation**: Uses `AmbientCapabilities` to bind to ports 80/443 without root privileges.
* **Arch Detection**: Automatically selects the correct binary for `armv7`, `arm64`, or `amd64`.
* **Easy Updates**: Re-running the script updates the binary to the latest version.
### Installation Steps
1. **Download the installer**:
```bash
curl -fsSL -o install.sh https://raw.githubusercontent.com/gesellix/bose-soundtouch/main/scripts/raspberry-pi/install.sh
```
2. **Run with sudo**:
```bash
sudo bash install.sh
```
### Overriding Defaults
You can customize the installation using environment variables:
```bash
sudo \
VERSION=v0.17.0 \
HOSTNAME_FQDN=soundtouch.local \
HTTP_PORT=80 \
HTTPS_PORT=443 \
bash install.sh
```
### Updating the Service
To update the service to a specific version, run the installer with the version as an argument:
```bash
sudo bash install.sh v0.18.1
```
The installer will automatically fetch the latest version of itself for that release and then update the service binary and restart it.
## Management
Once installed, use standard `systemctl` commands to manage the service:
```bash
# Check status
systemctl status soundtouch-service
# Follow logs
journalctl -u soundtouch-service -f
# Restart
sudo systemctl restart soundtouch-service
```
## Configuration
Configuration is stored in `/etc/soundtouch-service/soundtouch-service.env`. Note that settings saved via the Web UI (in `settings.json`) will take precedence over these environment variables once the service is running.
For more details, see the [scripts/raspberry-pi/README.md](../../scripts/raspberry-pi/README.md) in the repository.
+880
View File
@@ -0,0 +1,880 @@
# SoundTouch Service
The `soundtouch-service` is a comprehensive local server that emulates Bose's cloud services, enabling offline SoundTouch device operation and advanced debugging capabilities. This service is particularly valuable given Bose's announcement that cloud support will end in May 2026.
## Overview
The service provides:
- **🏠 Local Service Emulation**: Complete BMX (Bose Media eXchange) and Marge service implementation
- **🔧 Device Migration**: Seamlessly migrate devices from Bose cloud to local services via XML config, `/etc/hosts`, or `/etc/resolv.conf`
- **🔍 DNS Discovery & Interception**: Built-in DNS server to discover unknown Bose endpoints and selectively intercept cloud traffic
- **📊 Traffic Proxying**: Inspect and log all device communications for debugging
- **🌐 Web Management UI**: Browser-based interface for device management
- **💾 Persistent Data**: Store device configurations, presets, and usage statistics
- **📝 HTTP Recording**: Persist all interactions as re-playable `.http` files
- **🔄 Endpoint Mirroring**: Asynchronously mirror local requests to Bose cloud for parity testing
- **⚖️ Parity Logging**: Detect and record discrepancies between local and official Bose responses
- **📥 Session Archiving**: Download entire interaction sessions as `.tar.gz` for offline analysis
- **🔍 Auto-Discovery**: Automatically detect and configure SoundTouch devices
- **🔒 Offline Operation**: Continue using full device functionality without internet
- **🔗 Bose Proxy & Soundcork Fallback**: Dynamic proxying with automatic fallback to local [SoundCork](https://github.com/deborahgu/soundcork) emulation if enabled
## Architecture
The service consists of several key components:
### BMX Services (Bose Media eXchange)
- **TuneIn Integration**: Direct playback of radio stations and podcasts
- **Service Registry**: Media service discovery and configuration
- **Playback Control**: Stream URL resolution and audio metadata
### Marge Services (Account & Device Management)
- **Account Management**: User account simulation and device association
- **Preset Synchronization**: Cross-device preset storage and sync
- **Recent Items**: Playback history tracking and management
- **Configuration Management**: Device settings and preferences
### Discovery & Migration
- **Network Scanning**: UPnP/SSDP and mDNS device discovery
- **Device Analysis**: Configuration assessment and compatibility checking
- **Service Migration**: Automated configuration updates for local service usage
- **Health Monitoring**: Device connectivity and service status tracking
## Installation
### Install from Source
```bash
go install github.com/gesellix/bose-soundtouch/cmd/soundtouch-service@latest
```
### Build from Repository
```bash
git clone https://github.com/gesellix/bose-soundtouch.git
cd Bose-SoundTouch
go build -o soundtouch-service ./cmd/soundtouch-service
```
### Docker Support
You can run the SoundTouch service using Docker or Docker Compose.
> **Note for macOS and Windows users**: The `--net host` option is only supported on Linux. On macOS and Windows, service discovery (mDNS, UPnP) will not work automatically within the container. You will need to manually enter your device's IP address in the management UI, and the service will communicate with it directly.
#### Using Docker
**Linux (with host networking for discovery):**
```bash
docker run -d \
--name soundtouch-service \
--network host \
-v $(pwd)/data:/app/data \
ghcr.io/gesellix/bose-soundtouch:latest
```
**macOS / Windows (with port mapping):**
```bash
docker run --rm -it \
-p 8000:8000 -p 8443:8443 \
-v $(pwd)/data:/app/data \
--env SERVER_URL=http://soundtouch.local:8000 \
--env HTTPS_SERVER_URL=https://soundtouch.local:8443 \
ghcr.io/gesellix/bose-soundtouch:latest
```
> **Note**: The hostnames configured via `SERVER_URL` and `HTTPS_SERVER_URL` are automatically added as Subject Alternative Names (SAN) to the generated TLS certificate, ensuring valid SSL connections.
#### Using Docker Compose
Create a `docker-compose.yml` file:
```yaml
services:
soundtouch-service:
image: ghcr.io/gesellix/bose-soundtouch:latest
container_name: soundtouch-service
# Linux users: use host networking for device discovery
# network_mode: host
# macOS/Windows users: use port mapping (discovery will be manual)
ports:
- "8000:8000"
- "8443:8443"
environment:
- PORT=8000
- SERVER_URL=http://soundtouch.local:8000
- HTTPS_SERVER_URL=https://soundtouch.local:8443
- DATA_DIR=/app/data
volumes:
- soundtouch-data:/app/data
restart: unless-stopped
volumes:
soundtouch-data:
```
And run:
```bash
docker-compose up -d
```
## Quick Start
### 1. Start the Service
```bash
# Start with default settings (port 8000)
soundtouch-service
```
### 2. Access the Web Interface
Open your browser to `http://localhost:8000` to access the management interface.
### 3. Discover Devices
The service will automatically start discovering SoundTouch devices on your network. You can also trigger manual discovery from the web UI or API.
### 4. Migrate Devices
Use the web interface or API to migrate devices from Bose cloud services to your local instance.
## Configuration
### Configuration Precedence
The service supports multiple ways to configure its behavior. When multiple sources provide the same setting, the following precedence rules apply (highest to lowest):
1. **`settings.json`**: Settings saved via the Web UI (stored in the data directory) take the highest precedence. This ensures that changes made in the browser persist across service restarts even if environment variables or flags change.
2. **Environment Variables / CLI Flags**: If a setting is not present in `settings.json`, environment variables and flags are used.
3. **Default Values**: If no configuration is provided, the service uses its built-in defaults.
> **Tip**: If you find that changes to environment variables are not taking effect, check the **Settings** tab in the Web UI or inspect the `settings.json` file in your data directory, as it might be overriding your manual configuration.
### Configuration Options
| Variable | Flag | Description | Default |
|------------------------------------|----------------------------|---------------------------------------------------------------------------------------------------------|---------------------------|
| `PORT` | `--port`, `-p` | HTTP port to bind the service to | `8000` |
| `BIND_ADDR` | `--bind` | Network interface to bind to | all (ipv4 and ipv6) |
| `DATA_DIR` | `--data-dir` | Directory for persistent data | `./data` |
| `SERVER_URL` | `--server-url`, `-s` | External URL of this service | `http://<hostname>:8000` |
| `HTTPS_PORT` | `--https-port` | HTTPS port to bind the service to | `8443` |
| `HTTPS_SERVER_URL` | `--https-server-url`, `-S` | External HTTPS URL | `https://<hostname>:8443` |
| `PYTHON_BACKEND_URL`, `TARGET_URL` | `--target-url` | URL for Python-based service components (legacy) | `http://localhost:8001` |
| `REDACT_PROXY_LOGS` | `--redact-logs` | Redact sensitive data in proxy logs | `true` |
| `LOG_PROXY_BODY` | `--log-bodies` | Log full request/response bodies | `false` |
| `RECORD_INTERACTIONS` | `--record-interactions` | Record HTTP interactions to disk | `true` |
| `DISCOVERY_INTERVAL` | `--discovery-interval` | Device discovery interval | `5m` |
| `ENABLE_DNS_DISCOVERY` | `--dns-discovery` | Enable DNS discovery server | `false` |
| `DNS_UPSTREAM` | `--dns-upstream` | Upstream DNS server for non-Bose queries | `8.8.8.8` |
| `DNS_BIND_ADDR` | `--dns-bind` | Bind address for the DNS discovery server (standard port `:53` is required for `resolv.conf` migration) | `:53` |
| `MIRROR_ENABLED` | | Enable background mirroring of specific endpoints to Bose cloud | `false` |
| `MIRROR_ENDPOINTS` | | Comma-separated list of path patterns to mirror (e.g., `/streaming/account/*/device/*/recent`) | `[]` |
| `INTERNAL_PATHS` | `--internal-paths` | Paths for internal requests to exclude from recording (e.g., `/setup/*`, `/web/*`) | `[]` |
| `DISCOVERY_DISABLED` | | Disable automated device discovery | `false` |
### Configuration Examples
```bash
# Custom port and data directory
PORT=9000 DATA_DIR=/home/user/soundtouch soundtouch-service
# External server with custom URL
SERVER_URL=https://my-soundtouch.example.com soundtouch-service --port 443
# Development mode with full logging
LOG_PROXY_BODY=true REDACT_PROXY_LOGS=false soundtouch-service
```
## Device Migration
### Understanding Migration
Device migration switches your SoundTouch devices from Bose's cloud services to your local service instance. This process:
1. **Backs up** existing device configuration
2. **Updates** device service URLs to point to your local server
3. **Maintains** all existing presets and settings
4. **Enables** offline operation and advanced debugging
### Migration Methods
#### Web Interface (Recommended)
1. Start the service: `soundtouch-service`
2. Open `http://localhost:8000`
3. Wait for device discovery to complete
4. Click "Migrate" next to each device
5. Monitor migration status in real-time
#### API Migration
```bash
# Get migration summary first
curl http://localhost:8000/setup/devices/192.168.1.100/summary
# Perform migration
curl -X POST http://localhost:8000/setup/devices/192.168.1.100/migrate
# Verify migration status
curl http://localhost:8000/devices
```
#### Advanced Migration Options
```bash
# Migration with proxy fallback for original services
curl -X POST "http://localhost:8000/setup/devices/192.168.1.100/migrate?proxy_url=http://localhost:8000&marge=original&stats=original"
# Migration with custom target URL
curl -X POST "http://localhost:8000/setup/devices/192.168.1.100/migrate?target_url=https://my-server.com:8000"
```
### Post-Migration Verification
After migration, verify the device is working correctly:
```bash
# Check device status
curl http://localhost:8000/devices
# Test preset functionality
curl "http://192.168.1.100:8090/presets"
# Monitor device events (if needed)
curl "http://localhost:8000/devices/08DF1F0BA325/events"
```
#### ResolvConf Migration (DHCP-Aware DNS Redirection)
The most robust and flexible DNS-based migration method. It utilizes the device's persistent `/mnt/nv/rc.local` script to inject a priority DNS hook into the system's DHCP configuration.
> **Note**: This method requires the DNS Discovery Server to be bound to **port 53** on your local IP and **actually running**. Most devices do not support custom DNS ports in `/etc/resolv.conf`. If you use a custom port for testing, remember to switch back to `:53` and ensure the server has successfully bound to it (check Settings for status) before the actual migration.
**Advantages:**
- **Discovery**: Automatically discover all Bose endpoints queried by the device.
- **Dynamic Interception**: Intercept new or unknown services without further device modifications.
- **Fail-Safe**: Falls back to the standard network DNS (provided by your router) if the Aftertouch service is unavailable.
- **DHCP Compatible**: Preserves your router's assigned search domain and secondary DNS servers.
- **Wildcard Support**: Seamlessly handles `*.bose.com` redirection via your local DNS server.
- **Persistent**: Survives reboots and DHCP renewals.
**How it works:**
1. **Configuration**: A custom file named `/mnt/nv/aftertouch.resolv.conf` is created on the device's persistent partition.
2. **Boot Hook**: On every boot, `/mnt/nv/rc.local` checks if the system's DHCP scripts (`/etc/udhcpc.d/50default` or `/opt/Bose/udhcpc.script`) have been patched.
3. **Surgical Patch**: If not patched, it injects a one-line check into the relevant DHCP scripts.
4. **Resolution**: Whenever the device acquires a DHCP lease, the scripts now read your `aftertouch.resolv.conf` first, placing your DNS server at the top of `/etc/resolv.conf` while keeping all other DHCP-provided settings.
**Setup:**
1. Enable SSH via the `remote_services` USB trick.
2. Create `/mnt/nv/aftertouch.resolv.conf` with your server details:
```text
# Created by Aftertouch/SoundTouch-Service
# Priority nameserver for Bose service redirection
nameserver 192.168.1.XXX
```
3. Update `/mnt/nv/rc.local` with the idempotent patch:
```sh
#!/bin/sh
# Aftertouch DNS hook: prioritizes our custom nameserver if it exists
HOOK_MARKER="/mnt/nv/aftertouch.resolv.conf"
if [ -f "$HOOK_MARKER" ]; then
# Patch 50default if it exists
TARGET_FILE="/etc/udhcpc.d/50default"
if [ -f "$TARGET_FILE" ] && ! grep -q "$HOOK_MARKER" "$TARGET_FILE"; then
sed -i '/echo "search \$domain"/a \ [ -f '"$HOOK_MARKER"' ] && cat '"$HOOK_MARKER"' && dns=""' "$TARGET_FILE"
fi
# Patch udhcpc.script if it exists (e.g. SoundTouch 10)
TARGET_SCRIPT="/opt/Bose/udhcpc.script"
if [ -f "$TARGET_SCRIPT" ] && ! grep -q "$HOOK_MARKER" "$TARGET_SCRIPT"; then
sed -i '/echo "search \$search_list # \$interface" >> \$RESOLV_CONF/a \ [ -f '"$HOOK_MARKER"' ] && cat '"$HOOK_MARKER"' >> '"\$RESOLV_CONF"' && dns=""' "$TARGET_SCRIPT"
fi
fi
```
4. Make the script executable: `chmod +x /mnt/nv/rc.local`.
5. Reboot the speaker.
### DNS Discovery Server
The SoundTouch service includes a built-in DNS server specifically designed for Bose devices.
#### How it Works
When enabled, the DNS server:
1. Receives DNS queries from migrated SoundTouch devices.
2. **Intercepts** known Bose domains (e.g., `api.bose.com`, `streaming.bose.com`, `bmx.bose.com`) and resolves them to the AfterTouch service IP.
3. **Logs** all other queries for discovery purposes, allowing you to identify new Bose cloud endpoints.
4. **Forwards** unknown or non-Bose queries to the configured upstream DNS server (default: `8.8.8.8`).
#### Configuration
You can enable and configure the DNS server via the Web UI or environment variables:
- `ENABLE_DNS_DISCOVERY=true`: Turns on the DNS server.
- `DNS_BIND_ADDR=:53`: The port to listen on (requires root privileges for port 53).
- `DNS_UPSTREAM=1.1.1.1`: Your preferred upstream DNS provider. **Note:** Ensure this is not set to the same address as the DNS server itself (loopback or local IP) to avoid forwarding loops. The server includes built-in loop prevention, but misconfiguration will cause forwarding to fail. DNS Discovery cannot be enabled if this setting is empty.
#### Manual Discovery via DNS
Even without migrating a device, you can use the DNS server to discover what a device is querying by manually setting your router's DNS or the device's DNS to point to the AfterTouch service.
## Endpoint Mirroring & Parity Logging
The SoundTouch service includes a powerful **Mirroring** feature that allows you to handle requests locally while simultaneously forwarding them to the official Bose cloud in the background. This is primarily used for maintaining long-term compatibility and verifying the accuracy of the local emulation.
### How Mirroring Works
When an endpoint is configured for mirroring:
1. **GET Requests**: Handled locally first (Primary). The response is returned to the speaker immediately. In the background, the same request is sent to Bose.
2. **POST/PUT/DELETE Requests**: Handled locally first. The service then synchronously (but without blocking the speaker's response) forwards the request to Bose to ensure the "official" account state stays in sync with your local changes (e.g., updating a preset).
### Parity Logging
The **Parity Logger** automatically compares the response from your local service with the one received from Bose. If it detects any discrepancies, it:
1. Logs a warning to the console: `[PARITY] Mismatch detected for GET /...`
2. Saves a detailed JSON report to `data/parity_mismatches/`.
Each report includes the full request, both response bodies, and a summary of what differed (status codes, content types, or missing/different XML tags).
### Configuration
Mirroring is configured via the **Settings** tab in the Web UI or through global settings:
- **Mirror Enabled**: Master switch for the mirroring infrastructure.
- **Mirror Endpoints**: A list of URL path patterns to mirror. You can use wildcards (`*`) to match variable parts like account or device IDs.
- Example: `/streaming/account/*/device/*/recent`
- Example: `/accounts/*/devices/*/presets/*`
Mirrored requests are also recorded in the **Interaction Log** under the category `upstream-mirror`, allowing you to see side-by-side exactly how our service's behavior compares to the official one.
## API Reference
### Discovery & Setup
#### `GET /devices`
Lists all discovered SoundTouch devices with their current status.
**Response:**
```json
[
{
"device_id": "08DF1F0BA325",
"name": "Living Room Speaker",
"ip_address": "192.168.1.100",
"product_code": "SoundTouch 20",
"firmware_version": "19.0.5",
"migrated": true,
"last_seen": "2024-01-15T10:30:00Z"
}
]
```
#### `POST /setup/discover`
Triggers immediate network device discovery.
#### `GET /devices/{deviceIP}/info`
Gets detailed device information and configuration.
#### `GET /setup/devices/{deviceIP}/summary`
Analyzes device configuration and provides migration preview.
**Response:**
```json
{
"device_name": "Living Room Speaker",
"device_model": "SoundTouch 20",
"firmware_version": "19.0.5",
"ssh_success": true,
"current_config": "<?xml version=\"1.0\"?>...",
"planned_config": "<?xml version=\"1.0\"?>...",
"remote_services_enabled": false,
"migration_required": true
}
```
#### `POST /setup/devices/{deviceIP}/migrate`
Migrates device to use local services.
**Query Parameters:**
- `target_url`: Custom service URL (optional)
- `proxy_url`: Proxy URL for fallback (optional)
- `marge`: Set to "original" to proxy Marge requests (optional)
- `stats`: Set to "original" to proxy stats requests (optional)
- `sw_update`: Set to "original" to proxy update requests (optional)
- `bmx`: Set to "original" to proxy BMX requests (optional)
#### `POST /setup/devices/{deviceIP}/revert`
Reverts device to Bose cloud defaults.
#### `POST /setup/devices/{deviceIP}/trust-ca`
Injects the AfterTouch root CA into the device's trust store.
#### `POST /setup/devices/{deviceIP}/sync`
Syncs presets and recents from the device to local storage.
#### `POST /setup/devices/{deviceIP}/backup`
Creates a backup of the current device configuration.
#### `POST /setup/devices/{deviceIP}/ensure-remote-services`
Enables persistent SSH/remote services on the device.
#### `POST /setup/devices/{deviceIP}/remove-remote-services`
Removes persistent SSH/remote services from the device.
#### `POST /setup/devices/{deviceIP}/test-connection`
Tests HTTPS connection from device to service.
#### `POST /setup/devices/{deviceIP}/test-hosts`
Tests /etc/hosts redirection on the device.
#### `POST /setup/devices/{deviceIP}/test-dns`
Tests DNS redirection on the device.
### BMX Services (Bose Media eXchange)
#### `GET /bmx/registry/v1/services`
Returns available media services for device registration.
#### `GET /bmx/tunein/v1/playbook/station/{stationID}`
Provides TuneIn station playback information.
#### `GET /bmx/tunein/v1/podcast/{podcastID}`
Returns podcast episode information and playback URLs.
### Marge Services (Account & Device Management)
#### `GET /marge/streaming/sourceproviders`
Lists available music service providers.
#### `GET /marge/accounts/{account}/devices/any/presets`
Returns user presets for synchronization.
#### `GET /marge/accounts/{account}/devices/any/recents`
Returns recent playback items.
#### `PUT /marge/accounts/{account}/devices/{device}/presets/{slot}`
Updates a specific preset slot.
#### `POST /marge/streaming/support/addrecent`
Adds item to recent playback history.
#### `GET /marge/updates/soundtouch`
Returns software update configuration (disabled by default).
### Proxy Services
#### `GET /proxy/{encodedURL}`
Proxies requests to external services with logging.
**Example:**
```bash
# Proxy request to Bose services
curl "http://localhost:8000/proxy/aHR0cHM6Ly9hcGkuc291bmR0b3VjaC5ib3NlLmNvbS8="
```
### Health & Monitoring
#### `GET /health`
Returns service health status.
#### `GET /events/{deviceID}`
WebSocket endpoint for real-time device events.
#### `GET /stats/usage`
Returns usage statistics.
#### `GET /stats/errors`
Returns error statistics.
## Web Interface
### Overview
The web management interface provides a comprehensive dashboard for managing your SoundTouch devices:
**URL:** `http://localhost:8000/`
### Features
#### Device Dashboard
- **Device Discovery**: Real-time view of discovered devices
- **Migration Status**: Visual indicators of migration state
- **Device Health**: Connectivity and service status monitoring
- **Quick Actions**: One-click migration and configuration
#### Device Management
- **Configuration Viewer**: Inspect current and planned device configs
- **Migration Wizard**: Step-by-step device migration process
- **Backup Management**: View and restore configuration backups
- **Service Testing**: Test connectivity to local services
#### Monitoring & Debugging
- **Traffic Logs**: Real-time proxy request/response logging
- **Event Streaming**: Live device event monitoring
- **Statistics Dashboard**: Usage and error analytics
- **Debug Tools**: Device communication testing utilities
#### Interactions & Traffic Analysis
- **Traffic Overview**: View aggregate request counts for self-handled and proxied traffic.
- **Session Browsing**: Browse recorded interactions grouped by session.
- **Advanced Filtering**: Filter interactions by session, category (Self/Upstream), and timestamp.
- **Interaction Viewer**: View raw `.http` recording content directly in the browser.
- **Session Management**: Delete individual sessions or perform bulk cleanup to keep only recent sessions.
- **Session Download**: Download complete interaction sessions as `.tar.gz` archives for offline analysis or bug reports.
- **DNS Discoveries**: Real-time table of all hostnames discovered via the AfterTouch DNS server, categorized by interception status (Self/Upstream).
### Usage Tips
1. **First Time Setup**: The interface will guide you through initial device discovery
2. **Migration Monitoring**: Watch migration progress in real-time with detailed status updates
3. **Troubleshooting**: Use the debug tools to diagnose device connectivity issues
4. **Log Analysis**: Enable detailed logging for development and troubleshooting
## HTTP Interaction Recording
The service automatically records all HTTP interactions (both those handled locally and those proxied upstream) as `.http` files. These files are compatible with the [IntelliJ IDEA HTTP Client](https://www.jetbrains.com/help/idea/exploring-http-syntax.html).
### Internal Paths (Excluding Traffic)
To prevent internal management traffic (like the Web UI or setup API calls) from cluttering your interaction logs, you can configure **Internal Paths**. Requests matching these patterns will be processed normally but will **not** be recorded by the `RecordMiddleware`.
By default, we recommend adding:
- `/setup/*`: Management API calls
- `/web/*`: Static Web UI resources
- `/media/*`: Icons and static media
You can configure these via the **Settings** tab in the Web UI or using the `--internal-paths` flag.
### Key Features
- **Session Grouping**: All interactions from a single server session are stored in a dedicated directory named `{timestamp}-{pid}`.
- **Chronological Order**: Files are prefixed with a sequential number (e.g., `0001-`, `0002-`) to preserve the exact order of requests across the entire session.
- **Path-Based Structure**: Recordings are organized into subdirectories based on their URL path for better discoverability.
- **Automatic Sanitization**: Variable path segments like IP addresses, Device IDs, and Account IDs are automatically identified and replaced with placeholders (e.g., `{{ip}}`, `{{deviceId}}`). The original values are preserved as comments at the top of the recorded `.http` files for easy identification.
- **Re-playability**: An `http-client.env.json` file is generated for each session, allowing you to re-play the recorded requests immediately in IntelliJ IDEA.
- **Management UI**: The **5. Interactions** tab provides a built-in viewer and management tools for all recorded data.
### Configuration
#### Redaction
By default, the service redacts sensitive information from the recorded `.http` files, including:
- `Authorization` headers
- `Cookie` headers
- `X-Bose-Token` headers
- `X-Bose-Key` headers
- `Proxy-Authorization` headers
This behavior is controlled by the `--redact-logs` flag or the `REDACT_PROXY_LOGS` environment variable.
#### Custom Patterns
The service uses regex patterns to identify variable segments in URL paths. These patterns are loaded from `data/patterns.json`. You can add custom patterns to this file to support additional variable segments:
```json
[
{
"name": "MyVariable",
"regexp": "^[0-9]{5}$",
"replacement": "{myVar}"
}
]
```
Variables found via these patterns will be:
1. Used as directory names in the `interactions/` folder.
2. Parameterized as `{{myVar}}` within the `.http` files.
3. Added to the `http-client.env.json` file with their actual values.
## Persistent Data
### Data Directory Structure
By default, the service creates a `data/` directory in the current working directory:
```
data/
├── accounts/
│ └── default/
│ ├── devices/
│ │ ├── {DEVICE_ID}/
│ │ │ ├── DeviceInfo.xml
│ │ │ └── config_backup_*.xml
│ │ └── ...
│ ├── Sources.xml
│ ├── Presets.xml
│ └── Recents.xml
├── interactions/
│ └── {SESSION_ID}/
│ ├── self/
│ │ └── {PATH}/
│ │ └── {SEQ}-{TIME}-{METHOD}.http
│ ├── upstream/
│ │ └── {PATH}/
│ │ └── {SEQ}-{TIME}-{METHOD}.http
│ └── http-client.env.json
├── dns/
│ └── discoveries.json
├── stats/
│ ├── usage/
│ │ └── *.json
│ └── error/
│ └── *.json
└── events/
└── device_events_*.log
```
### Data Components
#### Device Data (`accounts/default/devices/{DEVICE_ID}/`)
- **DeviceInfo.xml**: Device metadata and capabilities
- **config_backup_*.xml**: Configuration backups before migration
- **presets.xml**: Device-specific preset configurations
#### Account Data (`accounts/default/`)
- **Sources.xml**: Configured music service providers
- **Presets.xml**: Cross-device preset synchronization
- **Recents.xml**: Recent playback history
#### DNS Data (`dns/`)
- **discoveries.json**: Persisted DNS discovery logs with hostname deduplication
#### Statistics (`stats/`)
- **usage/**: Device usage analytics and patterns
- **error/**: Error logs and diagnostic information
#### Events (`events/`)
- **device_events_*.log**: Device event history and debugging logs
#### HTTP Interactions (`interactions/`)
- **{SESSION_ID}/**: A unique directory per server run (format: `YYYYMMDD-HHMMSS-PID`).
- **self/**: Requests handled directly by the service.
- **upstream/**: Requests proxied to external Bose services.
- **{PATH}/**: Nested subdirectories reflecting the URL path (sanitized).
- **http-client.env.json**: IntelliJ IDEA HTTP Client environment file with session variables.
- **{SEQ}-{TIME}-{METHOD}.http**: Individual interaction recordings in standard HTTP Client format.
### Data Management
#### Backup Strategy
```bash
# Manual backup
cp -r data/ backup-$(date +%Y%m%d)/
# Automated backup (cron example)
0 2 * * * cp -r /path/to/data/ /backup/soundtouch-$(date +\%Y\%m\%d)/
```
#### Data Migration
```bash
# Moving to new server
tar czf soundtouch-data.tar.gz data/
# Transfer to new server
tar xzf soundtouch-data.tar.gz
```
#### Cleanup
```bash
# Clean old event logs (older than 30 days)
find data/events/ -name "*.log" -mtime +30 -delete
# Clean old statistics (older than 90 days)
find data/stats/ -name "*.json" -mtime +90 -delete
```
## API Endpoints
### Management UI
- **URL**: `http://localhost:8000/` or `http://localhost:8000/web/`
- **Description**: Browser-based guided flow for discovery, data sync, and migration.
### Setup API
- `GET /devices`: List all known (auto-discovered and manual) devices.
- `POST /devices`: Manually add a device by IP.
- `POST /setup/discover`: Trigger a new network discovery scan.
- `GET /setup/discovery-status`: Check if a scan is currently in progress.
- `POST /devices/{deviceIP}/sync`: Fetch presets, recents, and sources from a device.
- `GET /devices/{deviceIP}/summary`: Get a detailed migration readiness summary.
- `POST /devices/{deviceIP}/migrate`: Migrate a device using the specified method (XML/Hosts).
- `GET /setup/ca.crt`: Download the Root CA certificate for manual installation.
#### `GET /setup/interactions`
Lists recorded interactions with optional filtering.
**Query Parameters:**
- `session`: Filter by session ID (optional)
- `category`: Filter by category (`self` or `upstream`) (optional)
- `since`: Filter by timestamp (e.g., `2026-02-15 15:00:00`) (optional)
#### `GET /setup/interaction-stats`
Returns aggregate statistics about recorded interactions across all sessions.
#### `GET /setup/interaction-content?file={path}`
Returns the raw content of a specific recorded `.http` file.
#### `DELETE /setup/interactions/sessions/{sessionID}`
Deletes all recordings associated with a specific session.
#### `DELETE /setup/interactions/sessions?keep={N}`
Bulk cleanup: deletes all but the most recent `N` sessions.
### DNS Discovery API
#### `GET /setup/dns-discoveries`
Returns merged in-memory and persisted DNS discoveries, sorted by last seen timestamp.
#### `DELETE /setup/dns-discoveries`
Clears all recorded DNS discovery data from memory and disk.
### Emulated Services
- `/bmx/registry/v1/services`: BMX service registry.
- `/bmx/tunein/v1/*`: TuneIn radio emulation.
- `/marge/accounts/*`: Account and device management.
- `/marge/updates/soundtouch`: Software update emulation.
- `/proxy/*`: Logging proxy for original Bose services.
## Troubleshooting
### Common Issues
#### Device Not Discovered
```bash
# Check network connectivity
ping 192.168.1.100
# Trigger manual discovery
curl -X POST http://localhost:8000/setup/discover
# Check device accessibility
curl http://192.168.1.100:8090/info
```
#### Migration Failures
```bash
# Check SSH connectivity
ssh-keyscan 192.168.1.100
# Get migration summary
curl http://localhost:8000/setup/migration-summary/192.168.1.100
# Verify device configuration
curl http://192.168.1.100:8090/info
```
#### Service Connectivity Issues
```bash
# Test local service endpoints
curl http://localhost:8000/health
curl http://localhost:8000/bmx/registry/v1/services
curl http://localhost:8000/marge/streaming/sourceproviders
```
### Debug Mode
Enable debug logging for detailed troubleshooting:
```bash
LOG_PROXY_BODY=true REDACT_PROXY_LOGS=false soundtouch-service
```
### Log Analysis
```bash
# Monitor service logs
tail -f /var/log/soundtouch-service.log
# Analyze proxy traffic
grep "PROXY" /var/log/soundtouch-service.log
# Check device events
ls -la data/events/
```
## Credits & Inspiration
This service implementation is based on and inspired by several excellent community projects:
### SoundCork
- **Project**: [SoundCork](https://github.com/deborahgu/soundcork)
- **Authors**: Deborah Gu and contributors
- **Contribution**: The architecture and service emulation approach in this Go implementation is heavily based on SoundCork's pioneering Python implementation. SoundCork provided the foundation for understanding Bose's service architecture and migration strategies.
### ÜberBöse API
- **Project**: [ÜberBöse API](https://github.com/julius-d/ueberboese-api)
- **Author**: Julius D.
- **Contribution**: Advanced API endpoint discovery and implementation details that helped make this service more complete and robust.
We are grateful to these projects for paving the way and providing the research foundation that made this comprehensive service implementation possible.
## Advanced Usage
### Custom Service Integration
```go
// Example: Custom BMX service handler
package main
import (
"net/http"
"github.com/go-chi/chi/v5"
)
func customBMXHandler(w http.ResponseWriter, r *http.Request) {
// Custom BMX service logic
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"custom": "service"}`))
}
func main() {
r := chi.NewRouter()
r.Get("/bmx/custom/endpoint", customBMXHandler)
http.ListenAndServe(":8000", r)
}
```
### Integration with Home Assistant
```yaml
# configuration.yaml
soundtouch:
- host: 192.168.1.100
port: 8090
name: "Living Room Speaker"
rest:
- resource: "http://localhost:8000/devices"
scan_interval: 60
sensor:
- name: "SoundTouch Devices"
value_template: "{{ value_json | length }}"
```
### Monitoring & Alerting
```bash
# Health check script
#!/bin/bash
response=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8000/health)
if [ $response != "200" ]; then
echo "SoundTouch service is down!" | mail -s "Alert" admin@example.com
fi
```
## Security Considerations
- **Network Security**: The service binds to all interfaces by default. Consider using `BIND_ADDR=127.0.0.1` for localhost-only access.
- **SSH Access**: Migration requires SSH access to devices. Ensure your network security policies allow this.
- **Proxy Logging**: Disable `REDACT_PROXY_LOGS` only in development environments.
- **Data Protection**: The data directory contains device configurations and usage patterns. Secure appropriately.
## Performance Tuning
### Resource Usage
- **Memory**: ~50MB baseline + ~5MB per discovered device
- **CPU**: Minimal during steady state, ~10% during discovery/migration
- **Disk**: ~1MB per device configuration + logs
### Scaling Considerations
```bash
# For many devices, increase discovery interval
DISCOVERY_INTERVAL=10m soundtouch-service
# For high-traffic environments, consider reverse proxy
nginx -> soundtouch-service instances
```
+85
View File
@@ -0,0 +1,85 @@
### Bose Cloud Shutdown: Survival Guide for SoundTouch
With Bose's announcement of discontinuing cloud support for SoundTouch devices in May 2026, this project provides the necessary tools to keep your speakers fully functional using a local emulation service.
This guide explains how to set up the `soundtouch-service` to run your devices independently of Bose's servers.
---
### Supported Use Cases
1. **Local Service Emulation**: The service emulates Bose's BMX (Bose Media eXchange) and Marge services, which handle content registries, presets, recents, and software update checks.
2. **Traffic Redirection**: Tools are provided to redirect your speakers to this local service instead of `*.bose.com`.
3. **Offline Operation**: Once redirected, the speakers function without needing to reach Bose's servers.
4. **Preset & Recent Management**: Captures and stores presets and "recently played" items locally.
---
### Setup Steps
To set up your SoundTouch system for local-only operation, follow these steps:
#### 1. Install and Start the Service
Run the `soundtouch-service` on a machine that is always on (like a Raspberry Pi or a NAS) within your local network.
```bash
# Install the service
go install github.com/gesellix/bose-soundtouch/cmd/soundtouch-service@latest
# Start the service (defaults to http://localhost:8000)
soundtouch-service
```
#### 2. Access the Management UI
Open your web browser and navigate to the service's web interface:
`http://<your-server-ip>:8000/`, e.g. `http://localhost:8000/`
*Note: The service also supports a `/web/` path for management.*
#### 3. Enable SSH on Your Speakers
To migrate your speakers, the service needs SSH access. You can enable it by:
1. Creating an empty file named `remote_services` on a USB stick.
2. Inserting the USB stick into the SoundTouch speaker's service port.
3. Rebooting the speaker (unplug/replug).
**Verify SSH Access:**
- Confirm the device responds to SSH without a password: `ssh -o HostKeyAlgorithms=+ssh-rsa -o PubkeyAcceptedAlgorithms=+ssh-rsa root@<IP>`
- Or use the **Migration** tab in the Web UI to see if the device shows a "✅ Success" status for SSH.
Once enabled, you can log in as `root` (no password).
#### 4. Setup Through the Web UI
The web interface handles the entire process in a guided flow. Before proceeding, we strongly recommend reviewing the [Migration & Safety Guide](MIGRATION-SAFETY.md).
* **Step 1: Settings**: Configure your server's IP or domain. This ensures the speakers know where to find the local services.
* **Step 2: Devices**: The service automatically scans for SoundTouch devices on your network. If a device is not found, you can manually add its IP address.
* **Step 3: Data Sync**: Select your device and click "Start Sync". This will automatically fetch your presets, recents, and configured sources from the speaker and store them in the local `data/` directory.
* **Step 4: Migration**: Choose your redirection method (XML Recommended) and click "Confirm Migration". After the migration, reboot your speaker to apply the changes.
#### 5. Verify Your Local Data
Once migrated, your speaker will use the data captured during the Sync step.
* The service stores data in the `data/` directory, organized by device serial number (e.g., `data/default/devices/<SERIAL>/`).
* **Automatic Capture**: As you use the device (changing presets, playing new music), the service continues to "learn" and update your local files.
---
### Comparison with other implementations (soundcork)
Our implementation (`soundtouch-service`) is largely compatible with the Python-based `soundcork` project but offers several advantages:
- **Web UI**: Integrated management interface for discovery and migration.
- **Surgical Migration**: Uses XML-based redirection by default, which is less invasive than `/etc/hosts`.
- **Automated SSL**: Handles Root CA injection automatically for secure communication.
- **Proxy Support**: Can proxy requests to original Bose servers while "learning" your configuration.
---
### Alternative: DNS Redirection (No SSH)
If you prefer not to modify your speakers via SSH, you can use a local DNS server (like Pi-hole, AdGuard Home, or Unbound) to point the following domains to your local server's IP:
* `bmx.bose.com`
* `streaming.bose.com`
* `updates.bose.com`
* `stats.bose.com`
* `content.api.bose.io`
*Note: DNS redirection for HTTPS services requires the speakers to trust your local service's SSL certificate. The SSH-based migration handles this automatically by injecting the CA.*
---
@@ -817,6 +817,42 @@ Use this checklist to systematically troubleshoot issues:
---
## 🆔 **Device Identification & Mapping Issues**
### ❌ "File not found" errors with MAC addresses
**Symptoms:**
```
GET /streaming/account/3230304/device/A81B6A536A98/presets
→ 500 Internal Server Error
→ Log: "open .../devices/A81B6A536A98/Presets.xml: no such file or directory"
```
**Cause:** The service uses MAC addresses in API requests but stores files using device serial numbers. A mapping system resolves MAC addresses to serial numbers automatically.
**Quick Solutions:**
1. **Restart the service** (mappings are created at startup):
```bash
sudo systemctl restart soundtouch-service
```
2. **Check device directory structure**:
```bash
# Files should be stored by serial number, not MAC
ls data/accounts/3230304/devices/
# Should show: I6332527703739342000020/ (not A81B6A536A98/)
```
3. **Verify DeviceInfo.xml contains MAC address**:
```bash
cat data/accounts/3230304/devices/*/DeviceInfo.xml | grep macAddress
```
**For detailed diagnosis and solutions**, see: [**MAC Address Mapping Guide**](MAC-ADDRESS-MAPPING.md)
---
## 🛟 **Getting More Help**
### Information to Gather
+90
View File
@@ -0,0 +1,90 @@
# Images for Migration Guide
This directory contains images, screenshots, and diagrams referenced in the migration guide and other documentation.
## Required Images for Migration Guide
The following images need to be created to complete the migration guide:
### Dashboard Screenshots
- **dashboard-home.png** - Main SoundTouch Service dashboard homepage
- **account-creation.png** - Account creation form with fields filled
- **account-dashboard.png** - Fresh account dashboard showing ready state
- **device-discovery.png** - Device discovery page showing found speakers
- **device-registration.png** - Device registration dialog with options
- **migration-setup.png** - Migration configuration dialog
- **migration-progress.png** - Migration progress tracker showing phases
- **migration-health.png** - Migration health monitoring dashboard
- **account-migration.png** - Account-wide migration progress overview
- **migration-complete.png** - Completed migration dashboard view
- **backup-setup.png** - Backup configuration settings page
### Setup and Preparation
- **usb-remote-services.png** - USB drive setup showing file structure
- **raspberry-pi-setup.png** - Raspberry Pi with connected cables (optional)
### Process Diagrams
- **migration-flow-diagram.png** - Flow chart showing migration phases
- **network-topology.png** - Network diagram showing Pi, router, speakers
- **data-flow-diagram.png** - How data flows between components
## Image Requirements
### Technical Specifications
- **Format**: PNG preferred for screenshots, SVG for diagrams
- **Resolution**: Minimum 1200px width for screenshots
- **File Size**: Keep under 500KB when possible for fast loading
- **Naming**: Use descriptive kebab-case names as shown above
### Content Guidelines
- **Clean Interface**: Show realistic but clean interface states
- **Consistent Styling**: Use consistent colors and styling across images
- **Readable Text**: Ensure all text in screenshots is legible
- **Example Data**: Use realistic example data (Living Room Speaker, etc.)
- **Status Indicators**: Show clear success/error states with appropriate colors
### Placeholder Content
Until real screenshots are available, consider:
- **Mockups**: Create simple mockups showing the expected interface
- **Wireframes**: Basic wireframes indicating layout and content
- **Diagrams**: Technical diagrams can be created immediately
- **Text Placeholders**: Use `[Image: Description]` in documentation
## Creating the Images
### For Dashboard Screenshots
1. Set up the enhanced SoundTouch service
2. Create sample account and register devices
3. Take screenshots at key points in the migration process
4. Edit for clarity (highlight important elements, add annotations)
### For Diagrams
1. Use tools like Lucidchart, draw.io, or similar
2. Follow consistent color scheme:
- Blue: SoundTouch Service components
- Green: Healthy/successful states
- Orange: Warning/in-progress states
- Red: Error/problematic states
- Gray: External/third-party components
### For Physical Setup
1. Take photos of actual hardware setup
2. Show USB drive preparation process
3. Demonstrate network connections if helpful
## Alternative Text Requirements
Each image should have appropriate alt text for accessibility:
```markdown
![Alt text describing the image content](../images/image-name.png)
*Caption: Additional context or explanation*
```
## Future Enhancements
Consider adding:
- **Video Walkthroughs**: Screen recordings of key processes
- **Interactive Demos**: Web-based interactive guides
- **Troubleshooting Screenshots**: Common error states and solutions
- **Mobile Views**: How to access from mobile devices
+599
View File
@@ -0,0 +1,599 @@
# /power_on Implementation Guide
## Overview
This guide provides detailed technical specifications for implementing `/power_on` endpoint enhancements to reduce network dependency and improve device lifecycle management in the SoundTouch service.
## Current /power_on Handler Analysis
### Existing Implementation
Located in `pkg/service/handlers/handlers_marge.go`:
```go
func (s *Server) HandleMargePowerOn(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
log.Printf("[Marge] Failed to read power_on body: %v", err)
w.WriteHeader(http.StatusOK)
return
}
var req models.CustomerSupportRequest
if err := xml.Unmarshal(body, &req); err != nil {
log.Printf("[Marge] Failed to parse power_on body: %v", err)
// Fallback to remote address
if host, _, err := net.SplitHostPort(r.RemoteAddr); err == nil {
go s.PrimeDeviceWithSpotify(host)
}
w.WriteHeader(http.StatusOK)
return
}
deviceID := req.Device.ID
deviceIP := req.DiagnosticData.DeviceLandscape.IPAddress
log.Printf("[Marge] Device %s powered on (IP: %s)", deviceID, deviceIP)
if deviceIP != "" {
go s.PrimeDeviceWithSpotify(deviceIP)
} else {
// Fallback to remote address
if host, _, err := net.SplitHostPort(r.RemoteAddr); err == nil {
go s.PrimeDeviceWithSpotify(host)
}
}
w.WriteHeader(http.StatusOK)
}
```
**Current Limitations:**
- Only extracts basic device ID and IP
- No device state management
- No data persistence
- No response payload
- Limited to Spotify priming
## Enhanced Implementation Design
### 1. Extended Data Models
#### Enhanced Power-On Request Model
```go
// PowerOnRequest represents the enhanced power_on request structure
type PowerOnRequest struct {
XMLName xml.Name `xml:"device-data"`
Device PowerOnDevice `xml:"device"`
DiagnosticData DiagnosticData `xml:"diagnostic-data"`
}
type PowerOnDevice struct {
ID string `xml:"id,attr"`
SerialNumber string `xml:"serialnumber"`
FirmwareVersion string `xml:"firmware-version"`
Product PowerOnProduct `xml:"product"`
}
type PowerOnProduct struct {
ProductCode string `xml:"product_code,attr"`
Type string `xml:"type,attr"`
SerialNumber string `xml:"serialnumber"`
}
type DiagnosticData struct {
DeviceLandscape DeviceLandscape `xml:"device-landscape"`
NetworkData NetworkData `xml:"network-landscape>network-data"`
}
type DeviceLandscape struct {
RSSI string `xml:"rssi"`
GatewayIP string `xml:"gateway-ip-address"`
MacAddresses []string `xml:"macaddresses>macaddress"`
IPAddress string `xml:"ip-address"`
ConnectionType string `xml:"network-connection-type"`
}
```
#### Enhanced Response Model
```go
// PowerOnResponse represents the response sent back to the device
type PowerOnResponse struct {
XMLName xml.Name `xml:"power-on-response"`
Status string `xml:"status"`
DeviceID string `xml:"device-id"`
ConfigurationUpdates []ConfigurationUpdate `xml:"configuration-updates>update,omitempty"`
MigrationInstructions *MigrationInstruction `xml:"migration,omitempty"`
RegistrationRequired bool `xml:"registration-required,omitempty"`
Timestamp string `xml:"timestamp"`
}
type ConfigurationUpdate struct {
Type string `xml:"type,attr"`
Key string `xml:"key"`
Value string `xml:"value"`
Priority int `xml:"priority,attr"`
}
type MigrationInstruction struct {
Method string `xml:"method,attr"`
TargetURL string `xml:"target-url"`
ProxyURL string `xml:"proxy-url,omitempty"`
Options map[string]string `xml:"options>option"`
}
```
### 2. Enhanced PowerOn Handler
```go
// HandleMargePowerOnEnhanced processes power_on requests with full device lifecycle management
func (s *Server) HandleMargePowerOnEnhanced(w http.ResponseWriter, r *http.Request) {
startTime := time.Now()
// Parse the power_on request
powerOnReq, err := s.parsePowerOnRequest(r)
if err != nil {
s.handlePowerOnError(w, r, "Failed to parse request", err)
return
}
// Process device information
deviceInfo, isNewDevice, err := s.processDeviceFromPowerOn(powerOnReq)
if err != nil {
s.handlePowerOnError(w, r, "Failed to process device", err)
return
}
// Build response based on device state
response := s.buildPowerOnResponse(deviceInfo, isNewDevice, powerOnReq)
// Log the interaction
s.logPowerOnInteraction(deviceInfo, powerOnReq, response, startTime)
// Send response
if err := s.sendPowerOnResponse(w, response); err != nil {
log.Printf("[PowerOn] Failed to send response for device %s: %v", deviceInfo.DeviceID, err)
}
}
```
### 3. Device Processing Logic
```go
// processDeviceFromPowerOn handles device identification and data updates
func (s *Server) processDeviceFromPowerOn(req *PowerOnRequest) (*models.ServiceDeviceInfo, bool, error) {
deviceMAC := req.Device.ID
deviceIP := req.DiagnosticData.DeviceLandscape.IPAddress
// Try to find existing device by MAC address (primary identifier)
existingDevice, err := s.ds.GetDeviceByMAC(deviceMAC)
if err != nil && err != datastore.ErrDeviceNotFound {
return nil, false, fmt.Errorf("failed to lookup device: %w", err)
}
var deviceInfo *models.ServiceDeviceInfo
isNewDevice := existingDevice == nil
if isNewDevice {
// Create new device record from power_on data
deviceInfo = s.createDeviceFromPowerOn(req)
// Store in datastore
if err := s.ds.SaveDeviceInfo("", deviceMAC, deviceInfo); err != nil {
return nil, false, fmt.Errorf("failed to save new device: %w", err)
}
log.Printf("[PowerOn] New device registered: %s (IP: %s, Model: %s)",
deviceMAC, deviceIP, deviceInfo.ProductCode)
} else {
// Update existing device with power_on data
deviceInfo = existingDevice
s.updateDeviceFromPowerOn(deviceInfo, req)
// Detect significant changes
if s.hasSignificantChanges(existingDevice, deviceInfo) {
log.Printf("[PowerOn] Device %s updated: IP %s->%s, FW %s->%s",
deviceMAC, existingDevice.IPAddress, deviceInfo.IPAddress,
existingDevice.FirmwareVersion, deviceInfo.FirmwareVersion)
}
// Save updated device info
if err := s.ds.SaveDeviceInfo(deviceInfo.AccountID, deviceMAC, deviceInfo); err != nil {
return nil, false, fmt.Errorf("failed to update device: %w", err)
}
}
// Update device mappings for lookup optimization
s.ds.UpdateDeviceMappings(*deviceInfo)
return deviceInfo, isNewDevice, nil
}
```
### 4. Device Creation from Power-On Data
```go
// createDeviceFromPowerOn creates a new ServiceDeviceInfo from power_on request
func (s *Server) createDeviceFromPowerOn(req *PowerOnRequest) *models.ServiceDeviceInfo {
now := time.Now()
deviceInfo := &models.ServiceDeviceInfo{
DeviceID: req.Device.ID, // MAC address
ProductCode: req.Device.Product.ProductCode,
DeviceSerialNumber: req.Device.SerialNumber,
ProductSerialNumber: req.Device.Product.SerialNumber,
FirmwareVersion: req.Device.FirmwareVersion,
IPAddress: req.DiagnosticData.DeviceLandscape.IPAddress,
MacAddress: req.Device.ID, // Primary MAC
DiscoveryMethod: "power_on",
LastSeen: now,
CreatedAt: now,
UpdatedAt: now,
}
// Generate default name if not provided
if deviceInfo.Name == "" {
deviceInfo.Name = s.generateDefaultDeviceName(deviceInfo)
}
// Add power_on specific metadata
deviceInfo.Metadata = map[string]string{
"rssi": req.DiagnosticData.DeviceLandscape.RSSI,
"gateway_ip": req.DiagnosticData.DeviceLandscape.GatewayIP,
"connection_type": req.DiagnosticData.DeviceLandscape.ConnectionType,
"power_on_count": "1",
}
// Store additional MAC addresses if available
if len(req.DiagnosticData.DeviceLandscape.MacAddresses) > 1 {
additionalMACs := make([]string, 0, len(req.DiagnosticData.DeviceLandscape.MacAddresses)-1)
for _, mac := range req.DiagnosticData.DeviceLandscape.MacAddresses {
if mac != req.Device.ID {
additionalMACs = append(additionalMACs, mac)
}
}
if len(additionalMACs) > 0 {
deviceInfo.Metadata["additional_macs"] = strings.Join(additionalMACs, ",")
}
}
return deviceInfo
}
```
### 5. Response Generation Logic
```go
// buildPowerOnResponse creates appropriate response based on device state
func (s *Server) buildPowerOnResponse(deviceInfo *models.ServiceDeviceInfo, isNewDevice bool, req *PowerOnRequest) *PowerOnResponse {
response := &PowerOnResponse{
Status: "ok",
DeviceID: deviceInfo.DeviceID,
Timestamp: time.Now().Format(time.RFC3339),
}
// Handle new device registration
if isNewDevice {
response.RegistrationRequired = deviceInfo.AccountID == ""
// Add welcome configuration for new devices
response.ConfigurationUpdates = []ConfigurationUpdate{
{
Type: "welcome",
Key: "device_registered",
Value: "true",
Priority: 1,
},
}
}
// Check if migration is needed
if s.needsMigration(deviceInfo) {
migration := s.getMigrationInstructions(deviceInfo)
response.MigrationInstructions = migration
log.Printf("[PowerOn] Migration required for device %s: %s",
deviceInfo.DeviceID, migration.Method)
}
// Add any pending configuration updates
pendingUpdates := s.getPendingConfigurationUpdates(deviceInfo)
response.ConfigurationUpdates = append(response.ConfigurationUpdates, pendingUpdates...)
return response
}
```
### 6. Device Lookup Enhancements
#### Enhanced DataStore Methods
```go
// GetDeviceByMAC finds a device by MAC address across all accounts
func (ds *DataStore) GetDeviceByMAC(macAddress string) (*models.ServiceDeviceInfo, error) {
normalizedMAC := normalizeMAC(macAddress)
// Check device mappings first (for performance)
ds.idMutex.RLock()
deviceID, exists := ds.deviceMappings[normalizedMAC]
ds.idMutex.RUnlock()
if exists {
// Try to find device by mapped ID
device, err := ds.findDeviceByID(deviceID)
if err == nil {
return device, nil
}
}
// Fallback to full scan
devices, err := ds.ListAllDevices()
if err != nil {
return nil, err
}
for _, device := range devices {
if normalizeMAC(device.MacAddress) == normalizedMAC ||
normalizeMAC(device.DeviceID) == normalizedMAC {
return &device, nil
}
// Check additional MAC addresses in metadata
if additionalMACs, exists := device.Metadata["additional_macs"]; exists {
for _, mac := range strings.Split(additionalMACs, ",") {
if normalizeMAC(mac) == normalizedMAC {
return &device, nil
}
}
}
}
return nil, datastore.ErrDeviceNotFound
}
```
### 7. Migration Integration
```go
// needsMigration determines if device requires configuration migration
func (s *Server) needsMigration(deviceInfo *models.ServiceDeviceInfo) bool {
if deviceInfo.AccountID == "" {
return false // Cannot migrate without account
}
// Check if device is already migrated
if s.sm != nil {
summary, err := s.sm.GetMigrationSummary(deviceInfo.IPAddress, s.ServerURL, "", nil)
if err == nil && summary.IsMigrated {
return false
}
}
return true
}
// getMigrationInstructions creates migration instructions for device
func (s *Server) getMigrationInstructions(deviceInfo *models.ServiceDeviceInfo) *MigrationInstruction {
return &MigrationInstruction{
Method: "xml", // Default to XML-based migration
TargetURL: s.ServerURL,
Options: map[string]string{
"marge": "true",
"stats": "true",
"sw_update": "true",
},
}
}
```
### 8. Error Handling and Fallbacks
```go
// handlePowerOnError provides graceful error handling with fallbacks
func (s *Server) handlePowerOnError(w http.ResponseWriter, r *http.Request, message string, err error) {
log.Printf("[PowerOn] %s: %v", message, err)
// Try to extract IP from request for fallback processing
if host, _, err := net.SplitHostPort(r.RemoteAddr); err == nil {
// Fallback to existing discovery mechanism
go s.PrimeDeviceWithSpotify(host)
log.Printf("[PowerOn] Falling back to legacy processing for IP %s", host)
}
// Always return 200 OK to avoid device retry loops
w.WriteHeader(http.StatusOK)
}
// parsePowerOnRequest safely parses the power_on request with validation
func (s *Server) parsePowerOnRequest(r *http.Request) (*PowerOnRequest, error) {
body, err := io.ReadAll(r.Body)
if err != nil {
return nil, fmt.Errorf("failed to read request body: %w", err)
}
if len(body) == 0 {
return nil, fmt.Errorf("empty request body")
}
var req PowerOnRequest
if err := xml.Unmarshal(body, &req); err != nil {
return nil, fmt.Errorf("failed to parse XML: %w", err)
}
// Validate required fields
if req.Device.ID == "" {
return nil, fmt.Errorf("missing device ID")
}
if req.DiagnosticData.DeviceLandscape.IPAddress == "" {
return nil, fmt.Errorf("missing device IP address")
}
return &req, nil
}
```
### 9. Logging and Monitoring
```go
// logPowerOnInteraction records detailed interaction logs for debugging
func (s *Server) logPowerOnInteraction(deviceInfo *models.ServiceDeviceInfo, req *PowerOnRequest, resp *PowerOnResponse, startTime time.Time) {
duration := time.Since(startTime)
log.Printf("[PowerOn] Device: %s, IP: %s, Duration: %v, Status: %s, NewDevice: %t, Migration: %t",
deviceInfo.DeviceID,
req.DiagnosticData.DeviceLandscape.IPAddress,
duration,
resp.Status,
resp.RegistrationRequired,
resp.MigrationInstructions != nil)
// Store interaction for debugging (if enabled)
if s.config.RecordInteractions {
interaction := models.DeviceInteraction{
Timestamp: startTime,
DeviceID: deviceInfo.DeviceID,
Type: "power_on",
Request: req,
Response: resp,
Duration: duration,
IPAddress: req.DiagnosticData.DeviceLandscape.IPAddress,
UserAgent: r.Header.Get("User-Agent"),
}
if err := s.ds.SaveInteraction(interaction); err != nil {
log.Printf("[PowerOn] Failed to save interaction: %v", err)
}
}
}
```
### 10. Configuration and Feature Flags
```go
// PowerOnConfig controls behavior of enhanced power_on processing
type PowerOnConfig struct {
EnableEnhancedProcessing bool `json:"enable_enhanced_processing"`
AutoMigration bool `json:"auto_migration"`
RecordInteractions bool `json:"record_interactions"`
DefaultResponseTimeout time.Duration `json:"default_response_timeout"`
FallbackToLegacy bool `json:"fallback_to_legacy"`
}
// loadPowerOnConfig loads configuration with defaults
func loadPowerOnConfig() *PowerOnConfig {
return &PowerOnConfig{
EnableEnhancedProcessing: true,
AutoMigration: false, // Conservative default
RecordInteractions: false,
DefaultResponseTimeout: 5 * time.Second,
FallbackToLegacy: true,
}
}
```
## Testing Strategy
### 1. Unit Tests
```go
func TestHandleMargePowerOnEnhanced(t *testing.T) {
tests := []struct {
name string
requestBody string
existingDevice *models.ServiceDeviceInfo
expectedStatus string
expectMigration bool
}{
{
name: "new_device_registration",
requestBody: `<device-data><device id="A81B6A536A98">...</device></device-data>`,
existingDevice: nil,
expectedStatus: "ok",
expectMigration: false,
},
{
name: "existing_device_update",
requestBody: `<device-data><device id="A81B6A536A98">...</device></device-data>`,
existingDevice: &models.ServiceDeviceInfo{DeviceID: "A81B6A536A98"},
expectedStatus: "ok",
expectMigration: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Test implementation
})
}
}
```
### 2. Integration Tests
```go
func TestPowerOnDeviceLifecycle(t *testing.T) {
// Test complete device lifecycle through power_on events
// 1. New device power_on
// 2. Device registration
// 3. Configuration changes
// 4. Migration
// 5. Subsequent power_on events
}
```
### 3. Load Testing
```go
func BenchmarkPowerOnProcessing(b *testing.B) {
// Benchmark power_on processing performance
// Test concurrent device registrations
// Measure response times
}
```
## Deployment Strategy
### Phase 1: Parallel Implementation
- Implement enhanced handler alongside existing handler
- Use feature flag to control which handler processes requests
- Maintain full backward compatibility
### Phase 2: Gradual Rollout
- Enable enhanced processing for subset of devices
- Monitor performance and error rates
- Collect metrics on data completeness
### Phase 3: Full Migration
- Default to enhanced processing for all devices
- Remove legacy fallbacks
- Optimize performance based on production data
## Monitoring and Metrics
### Key Metrics to Track
- Power-on event frequency per device
- New device registration rate via power_on
- Migration success rate via power_on response
- Response time distribution
- Error rates and types
- Data completeness metrics
### Alerting Thresholds
- Power-on processing failures > 5%
- Average response time > 2 seconds
- New device registration failures > 1%
- Migration instruction delivery failures > 2%
## Security Considerations
### Input Validation
- XML parsing security (prevent XXE attacks)
- Device ID format validation
- IP address validation
- Request size limits
### Authentication
- Device authentication via MAC address verification
- Request signing (if available)
- Rate limiting per device/IP
### Data Privacy
- Sensitive data handling in diagnostic information
- Logging data retention policies
- Compliance with data protection regulations
@@ -686,7 +686,7 @@ Retrieves all supported endpoints for the specific device with comprehensive fea
- `/setMusicServiceOAuthAccount` - OAuth account setup
- `/removeMusicServiceAccount` - Remove music service account
- `/serviceAvailability`**Implemented** - Check service availability
- `/introspect` - Get introspect data for specific sources
- `/introspect`**Implemented** - Get introspect data for specific sources
**Station Management (Radio/Streaming):**
- `/searchStation` - Search for stations (tested with Pandora)
@@ -418,10 +418,10 @@ soundtouch-cli -host <discovered-ip> -bass # Verify final state
## Related Documentation
- **[API Endpoints Overview](API-Endpoints-Overview.md)** - Complete API reference
- **[API Endpoints Overview](API-ENDPOINTS.md)** - Complete API reference
- **[Volume Controls](VOLUME-CONTROLS.md)** - Related audio control documentation
- **[Client Usage Examples](../cmd/soundtouch-cli/main.go)** - CLI implementation reference
- **[Models](../pkg/models/bass.go)** - Bass model implementation
- **[Client Usage Examples](../../cmd/soundtouch-cli/main.go)** - CLI implementation reference
- **[Models](../../pkg/models/bass.go)** - Bass model implementation
## API Compliance
@@ -443,4 +443,4 @@ The implementation follows the official SoundTouch API:
**Implementation Date**: 2026-01-09
**Status**: ✅ Complete and tested
**Real Device Validation**: SoundTouch 10, SoundTouch 20
**API Compliance**: Full compliance with SoundTouch Web API specification
**API Compliance**: Full compliance with SoundTouch Web API specification
+73
View File
@@ -0,0 +1,73 @@
# Bose SoundTouch Cloud API Emulation (Marge/BMX/Stats)
This document describes the cloud-emulation APIs provided by the SoundTouch service. These APIs mimic the Bose cloud services (Marge, BMX, Stats) that SoundTouch devices and the SoundTouch controller application (Stockholm) interact with.
## Marge API (Account & Configuration)
Base path: `/marge`
### GET /streaming/sourceproviders
Retrieves a list of available streaming source providers.
### GET /accounts/{accountId}/full
Retrieves the full account configuration including sources, presets, and devices.
### GET /streaming/account/{accountId}/emailaddress
Retrieves the email address associated with the account.
### GET /streaming/device_setting/account/{accountId}/device/{deviceId}/device_settings
Retrieves settings for a specific device (e.g., clock format).
### POST /streaming/device_setting/account/{accountId}/device/{deviceId}/device_settings
Updates settings for a specific device.
### POST /accounts/{accountId}/devices/{deviceId}/presets/{presetNumber}
Updates a preset for a device.
### POST /accounts/{accountId}/devices/{deviceId}/recents
Adds an item to the device's recently played history.
### POST /accounts/{accountId}/devices
Adds a device to the account.
### DELETE /accounts/{accountId}/devices/{deviceId}
Removes a device from the account.
## Customer API (Profile & Password)
Base path: `/customer`
### GET /account/{accountId}
Retrieves the customer account profile.
### POST /account/{accountId}
Updates the customer account profile.
### POST /account/{accountId}/password
Changes the account password.
## Analytics & Stats API
Base path: `/v1` (App Events) or `/streaming/stats` (Device Stats)
### POST /v1/stapp/{deviceId}
Endpoint called by Bose SoundTouch mobile and web applications (Stockholm) to submit event data.
### POST /v1/scmudc/{deviceId}
Endpoint equivalent to `/v1/stapp/{deviceId}` sometimes used by apps or devices.
### POST /streaming/stats/usage
Endpoint used by physical devices to report usage statistics.
### POST /streaming/stats/error
Endpoint used by physical devices to report error statistics.
## BMX API (Streaming & Registry)
Base path: `/bmx`
### GET /registry/v1/services
Retrieves the registry of available streaming services.
### GET /tunein/v1/playback/station/{stationID}
Retrieves playback information for a TuneIn station.
@@ -366,7 +366,7 @@ This implementation now provides the full preset management lifecycle:
## Related Documentation
- [API Endpoints Overview](API-Endpoints-Overview.md) - Complete API reference
- [API Endpoints Overview](API-ENDPOINTS.md) - Complete API reference
- [Volume Controls](VOLUME-CONTROLS.md) - Volume management
- [Key Controls](KEY-CONTROLS.md) - Media control commands
- [Source Selection](SOURCE-SELECTION.md) - Audio source management
@@ -375,4 +375,4 @@ This implementation now provides the full preset management lifecycle:
Preset management in the Bose SoundTouch API is **intentionally read-only** by design. The API provides excellent capabilities for analyzing and understanding preset configurations, but preset creation must be done through official channels (app or device). This is a deliberate design decision that respects user control over their personal preset configurations.
For most use cases, reading preset information is sufficient for building applications that work with existing user configurations. For preset creation, guide users to use the official app or device controls, which provide the proper user experience and validation.
For most use cases, reading preset information is sufficient for building applications that work with existing user configurations. For preset creation, guide users to use the official app or device controls, which provide the proper user experience and validation.
@@ -38,6 +38,7 @@ The Bose SoundTouch Go client provides comprehensive source selection functional
- `IHEARTRADIO` - iHeartRadio streaming
- `STORED_MUSIC` - Local/network stored music
- `AIRPLAY` - Apple AirPlay (device dependent)
- `RADIO_BROWSER` - [RadioBrowser](radio-browser.md) internet radio directory
## Client Library Usage
@@ -345,13 +346,13 @@ The implementation follows the official SoundTouch API:
## Related Documentation
- **[API Endpoints Overview](API-Endpoints-Overview.md)** - Complete API reference
- **[Sources](../pkg/models/sources.go)** - Source model implementation
- **[Now Playing](../pkg/models/nowplaying.go)** - ContentItem model
- **[Client Usage Examples](../cmd/soundtouch-cli/main.go)** - CLI implementation reference
- **[API Endpoints Overview](API-ENDPOINTS.md)** - Complete API reference
- **[Sources](../../pkg/models/sources.go)** - Source model implementation
- **[Now Playing](../../pkg/models/nowplaying.go)** - ContentItem model
- **[Client Usage Examples](../../cmd/soundtouch-cli/main.go)** - CLI implementation reference
---
**Implementation Date**: 2026-01-09
**Status**: ✅ Complete and tested
**Real Device Validation**: SoundTouch 10, SoundTouch 20
**Real Device Validation**: SoundTouch 10, SoundTouch 20
@@ -68,14 +68,14 @@ func main() {
client := client.NewClient(config)
// Play TTS at current volume
err := client.PlayTTS("Hello, this is a test message", "YOUR_APP_KEY")
// Play TTS at current volume (language code "EN", "DE", etc.)
err := client.PlayTTS("Hello, this is a test message", "YOUR_APP_KEY", "EN")
if err != nil {
log.Fatal(err)
}
// Play TTS at specific volume (70)
err = client.PlayTTS("Volume test message", "YOUR_APP_KEY", 70)
err = client.PlayTTS("Volume test message", "YOUR_APP_KEY", "EN", 70)
if err != nil {
log.Fatal(err)
}
@@ -277,7 +277,7 @@ You'll need to provide your own application key. The format and generation metho
```go
// Doorbell notification
client.PlayTTS("Someone is at the front door", "home-automation-key", 80)
client.PlayTTS("Someone is at the front door", "home-automation-key", "EN", 80)
// Security alert
client.PlayURL(
@@ -311,4 +311,4 @@ soundtouch-cli speaker url --url "https://www.soundjay.com/misc/sounds/bell-ring
4. **URL content fails**: Ensure URL is accessible and contains valid audio
5. **Volume not restored**: May occur if device is powered off during playback
For more information, see the [SoundTouch WebServices API documentation](https://github.com/thlucas1/homeassistantcomponent_soundtouchplus/wiki/SoundTouch-WebServices-API).
For more information, see the [SoundTouch WebServices API documentation](https://github.com/thlucas1/homeassistantcomponent_soundtouchplus/wiki/SoundTouch-WebServices-API).
+34
View File
@@ -0,0 +1,34 @@
## radio-browser.info
- https://www.radio-browser.info is a community driven radio station database.
- It provides an API to access the data and allows users to submit new stations or update existing ones.
### Search for stations
- Go to https://www.radio-browser.info and find a station you like.
- Click on the station and copy the UUID from the URL.
- e.g. `https://www.radio-browser.info/history/d28420a4-eccf-47a2-ace1-088c7e7cb7e0`
### RADIO_BROWSER
- This project supports source type RADIO_BROWSER to play radio stations.
- Set the `location` attribute to `/stations/byuuid/{UUID}`.
```xml
<ContentItem
source="RADIO_BROWSER"
type="stationurl"
isPresetable="true"
location="/stations/byuuid/9610c454-0601-11e8-ae97-52543be04c81">
<itemName>RADIO_BROWSER</itemName>
<containerArt></containerArt>
</ContentItem>
```
### Playing the station
To start the radio stream replace `<uuid>` and `<soundtouch>` and run curl like this:
```bash
curl -d '<ContentItem source="RADIO_BROWSER" type="stationurl" location="/stations/byuuid/<uuid>"/>' <soundtouch>:8090/select
```
+140
View File
@@ -0,0 +1,140 @@
# SCMUDC Events Analysis
## Overview
SCMUDC (Sound Control Management Usage Data Collection) events are telemetry data sent from SoundTouch devices to `events.api.bosecm.com` via `/v1/scmudc/{deviceId}` endpoints. These events track user interactions and device behaviors for analytics and monitoring.
## Event Origins
Analysis of recorded interactions reveals three distinct origins for device events:
### 1. `"gabbo"` - SoundTouch App (Mobile/Desktop)
- **Source**: Remote control via SoundTouch mobile/desktop applications
- **Frequency**: Highest (primary control method)
- **Event Types**: User-initiated actions through app interface
- **Button Abstraction**: App UI elements (not physical buttons)
**Common Events**:
- `power-pressed` → Power on/off via app
- `play-pressed` → Play control
- `pause-pressed` → Pause control
- `skip-forward-pressed` → Next track
- `stop-pressed` → Stop playback
### 2. `"console"` - Device Hardware Controls
- **Source**: Physical buttons and controls on the speaker device
- **Frequency**: Lower (secondary control method)
- **Event Types**: Direct hardware interaction
- **Physical Controls**: Actual buttons, knobs, or touch interfaces on device
**Common Events**:
- `preset-pressed` → Physical preset buttons (PRESET_1, PRESET_5, etc.)
- `power-pressed` → Hardware power button
### 3. `"device"` - Internal System Actions
- **Source**: Device's internal software systems
- **Frequency**: Automatic responses to user actions
- **Event Types**: System-generated events, content playback
- **Rich Content**: Base64-encoded XML with detailed metadata
**Common Events**:
- `play-item` → Automatic content playback responses
- `preset-assigned` → System preset assignments
## Event Data Structure
### Standard Button Events (gabbo/console)
```json
{
"data": {
"buttonId": "POWER|PLAY|PAUSE|PRESET_5|etc",
"origin": "gabbo|console"
},
"type": "power-pressed|play-pressed|pause-pressed|preset-pressed|etc"
}
```
### Device Content Events
```json
{
"data": {
"contentItem": "PD94bWwgdmVyc2lvbj0...", // Base64-encoded XML
"origin": "device",
"preset": "none|P1|P5|etc"
},
"type": "play-item|preset-assigned"
}
```
## Content Item Structure
Device events include Base64-encoded XML with rich content metadata:
```xml
<ContentItem source="SPOTIFY" type="tracklisturl"
location="/playback/container/c3BvdGlmeTpwbGF5bGlzdDox..."
sourceAccount="gesellix" isPresetable="true">
<itemName>Billie Eilish - bad guy (instrumental version)</itemName>
<containerArt>https://i.scdn.co/image/ab67616d0000b273...</containerArt>
</ContentItem>
```
**Key Fields**:
- `source`: Music service (SPOTIFY, PANDORA, etc.)
- `itemName`: Track/playlist/station name
- `sourceAccount`: User account on the service
- `location`: Service-specific content identifier
- `containerArt`: Album/playlist artwork URL
- `isPresetable`: Whether content can be saved as preset
## Usage Patterns
### Control Method Preferences
1. **Primary**: SoundTouch App (`gabbo`) - Most frequent interactions
2. **Secondary**: Device Hardware (`console`) - Occasional direct control
3. **Automatic**: Internal System (`device`) - Background responses
### Event Flow
1. User triggers action via app or hardware
2. Device processes request and begins playback
3. Device sends content event with full metadata
4. System continues tracking playback state
## Telemetry Insights
### User Behavior Analytics
- **Interface Preference**: App vs. hardware control usage ratios
- **Feature Usage**: Most/least used controls and functions
- **Content Patterns**: Music service preferences, playlist usage
### Device Health Monitoring
- **Interaction Frequency**: Normal vs. abnormal usage patterns
- **Error Detection**: Failed commands or unusual event sequences
- **Performance**: Response times between user action and system response
### Service Integration Analysis
- **Music Services**: Spotify dominance, other service usage
- **Account Mapping**: User accounts across different services
- **Content Types**: Music vs. radio vs. podcast preferences
## Data Quality Notes
- All events include comprehensive device information (deviceID, serialNumber, softwareVersion)
- Timestamps include both UTC time and device monotonic time
- Events are batched and sent with consistent protocol versioning
- Content metadata is rich and includes artwork URLs for UI enhancement
## Security Considerations
- Events include user account information and listening habits
- Device serial numbers and unique identifiers are transmitted
- Content location data could reveal usage patterns
- Data should be handled according to privacy regulations
## Technical Implementation Notes
- Endpoint: `POST /v1/scmudc/{deviceId}`
- Protocol Version: 3.1 (current)
- Encoding: JSON with Base64-encoded XML payloads
- Authentication: Bearer token authorization
- Content-Type: `text/json; charset=utf-8`

Some files were not shown because too many files have changed in this diff Show More