mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-19 17:16:16 +00:00
Add `soundtouch-cli setup` subcommand group covering the full reset →
re-provision → pair lifecycle as a scriptable alternative to the web UI:
inspect, verify, plan, factory-reset, wait-ap, wifi-push, wait-online,
ssh-check, install-ca, migrate, reboot, pair (bare | full state machine)
Supporting library code lives in pkg/service/setup: factory_reset.go,
wifi_provision.go, inspect.go, init_plan.go, setup_session.go.
Confirmed against ST10 firmware 27.0.6 that bare setMargeAccount over
WebSocket — no SETUP_START/SETUP_ENTER/SETUP_LEAVE bracket — is
sufficient to pair a factory-reset speaker; the firmware materializes
SystemConfigurationDB.xml and Sources.xml itself and the pairing
survives reboot. Result and field-by-field SystemConfigurationDB
comparison documented in docs/analysis/SETUP-WEBSOCKET-EXPERIMENT.md.
Captures the device's pre-reset DELETE-to-marge plus its LAN peer
notification flow in docs/analysis/FACTORY-RESET-PROTOCOL.md.
Perf: batch GetMigrationSummary's SSH probes into one Run() call via
ssh_probe.go / ssh_probe_apply.go — was ~8 sequential dials at
500-1000 ms each on FW 27 crypto, now one round-trip. Same data shape,
same MigrationSummary fields populated.
Fixes /clockTime and /clockDisplay wire formats — firmware 27 rejects
the legacy flat XML ("Error parsing request"). ClockTimeRequest now
uses utcTime attribute; ClockDisplayRequest emits the nested
<clockConfig> envelope with timezoneInfo/timeFormat/brightnessLevel.
Removes cmd/example-init-speaker (superseded by setup pair).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
77 lines
2.1 KiB
Go
77 lines
2.1 KiB
Go
package setup
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestFactoryReset_HappyPath(t *testing.T) {
|
|
f := &fakeTelnet{
|
|
banner: "BoseDebug>",
|
|
responses: map[string]string{"sys factorydefault": "Rebooting...\n"},
|
|
}
|
|
m := newFakeTelnetManager(f)
|
|
|
|
logs, err := m.FactoryReset("192.0.2.10")
|
|
if err != nil {
|
|
t.Fatalf("FactoryReset: %v", err)
|
|
}
|
|
|
|
if len(f.commands) != 1 || f.commands[0] != "sys factorydefault" {
|
|
t.Errorf("commands = %v, want [sys factorydefault]", f.commands)
|
|
}
|
|
|
|
if !strings.Contains(logs, "Rebooting") {
|
|
t.Errorf("logs missing reboot output: %s", logs)
|
|
}
|
|
}
|
|
|
|
func TestFactoryReset_DisconnectIsAcceptedAsSuccess(t *testing.T) {
|
|
// Some firmwares drop the socket as soon as the reset starts, before
|
|
// they finish writing a response. That's not a failure.
|
|
f := &fakeTelnet{
|
|
fail: map[string]error{"sys factorydefault": errors.New("read EOF")},
|
|
}
|
|
m := newFakeTelnetManager(f)
|
|
|
|
logs, err := m.FactoryReset("192.0.2.10")
|
|
if err != nil {
|
|
t.Fatalf("disconnect during reset should be treated as success, got: %v", err)
|
|
}
|
|
|
|
if !strings.Contains(logs, "device disconnected") {
|
|
t.Errorf("logs should mention the expected disconnect, got: %s", logs)
|
|
}
|
|
}
|
|
|
|
func TestFactoryReset_RejectsFirmwareWithoutCommand(t *testing.T) {
|
|
// Default fakeTelnet response is "Command not found\n" for unmapped commands.
|
|
f := &fakeTelnet{}
|
|
m := newFakeTelnetManager(f)
|
|
|
|
_, err := m.FactoryReset("192.0.2.10")
|
|
if err == nil || !strings.Contains(err.Error(), "firmware does not expose") {
|
|
t.Errorf("err = %v, want firmware-rejection error", err)
|
|
}
|
|
}
|
|
|
|
func TestFactoryReset_NoTelnetClient(t *testing.T) {
|
|
m := &Manager{} // NewTelnet nil
|
|
|
|
_, err := m.FactoryReset("192.0.2.10")
|
|
if err == nil || !strings.Contains(err.Error(), "NewTelnet") {
|
|
t.Errorf("err = %v, want NewTelnet-nil error", err)
|
|
}
|
|
}
|
|
|
|
func TestFactoryReset_DialFailurePropagates(t *testing.T) {
|
|
f := &fakeTelnet{dialErr: errors.New("connection refused")}
|
|
m := newFakeTelnetManager(f)
|
|
|
|
_, err := m.FactoryReset("192.0.2.10")
|
|
if err == nil || !strings.Contains(err.Error(), "connection refused") {
|
|
t.Errorf("err = %v, want dial error", err)
|
|
}
|
|
}
|