mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-18 08:36:13 +00:00
refactor(setup): split high-complexity functions into per-axis helpers
Brings the five remaining gocyclo > 20 warnings to zero by extracting cohesive sub-functions; same observable behaviour, smaller surface to read at each call site. Bonus: the new helpers are individually testable. - pkg/models/clockdisplay.go: split ClockDisplay.UnmarshalXML attr handling into applyClockDisplayOuterAttrs (legacy flat shape) and applyClockConfigAttrs (current nested shape). - pkg/service/setup/ssh_probe_apply.go: split applyProbeToSummary into applyProbeCurrentConfig / applyProbeResolvConf / applyProbeRemoteServices / applyProbeCACert — one helper per MigrationSummary axis the probe populates. - pkg/service/setup/init_plan.go: split ExecuteInitPlan into applyInitPlanDefaults, runURLRewrite, resolveAccountID, and verifyPairing. Cleans up several shadowed err variables in the process. - cmd/soundtouch-cli/cmd_setup.go: split renderInspectReport into renderInspectIdentityAndPairing / renderInspectNetwork / renderInspectSources / renderInspectPresets / renderInspectRuntimeURLs, and buildPlanSteps into resetSteps + migrationSteps helpers. golangci-lint run ./pkg/service/setup/... ./pkg/models/... ./cmd/soundtouch-cli/... now reports zero findings. Tests green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
9e384840ba
commit
e3450ffd00
+236
-198
@@ -86,115 +86,156 @@ func renderInspectReport(r *setup.InspectReport) {
|
||||
fmt.Printf("Speaker @ %s\n", r.DeviceIP)
|
||||
fmt.Println(strings.Repeat("─", 40))
|
||||
|
||||
renderInspectIdentityAndPairing(r)
|
||||
renderInspectNetwork(r)
|
||||
renderInspectSources(r)
|
||||
renderInspectPresets(r)
|
||||
renderInspectRuntimeURLs(r)
|
||||
}
|
||||
|
||||
func renderInspectIdentityAndPairing(r *setup.InspectReport) {
|
||||
if r.InfoErr != nil {
|
||||
PrintError(fmt.Sprintf("/info: %v", r.InfoErr))
|
||||
} else if r.Info != nil {
|
||||
i := r.Info
|
||||
|
||||
fmt.Println("Identity")
|
||||
fmt.Printf(" deviceID : %s\n", i.DeviceID)
|
||||
|
||||
if suffix := deviceIDSuffix(i.DeviceID); suffix != "" {
|
||||
fmt.Printf(" → use as --match suffix for wait-online: %s\n", suffix)
|
||||
}
|
||||
|
||||
fmt.Printf(" name : %s\n", i.Name)
|
||||
fmt.Printf(" type : %s\n", i.Type)
|
||||
|
||||
for _, comp := range i.Components {
|
||||
if comp.SoftwareVersion != "" {
|
||||
fmt.Printf(" softwareVersion : %s (component %s)\n", comp.SoftwareVersion, comp.Category)
|
||||
}
|
||||
|
||||
if comp.SerialNumber != "" {
|
||||
fmt.Printf(" serialNumber : %s (component %s)\n", comp.SerialNumber, comp.Category)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println("Pairing")
|
||||
|
||||
if i.MargeAccountUUID == "" {
|
||||
PrintWarning("margeAccountUUID is empty — device is unpaired (factory-reset state)")
|
||||
} else {
|
||||
fmt.Printf(" margeAccountUUID : %s\n", i.MargeAccountUUID)
|
||||
}
|
||||
|
||||
fmt.Printf(" margeURL : %s\n", i.MargeURL)
|
||||
fmt.Println()
|
||||
return
|
||||
}
|
||||
|
||||
if r.Info == nil {
|
||||
return
|
||||
}
|
||||
|
||||
i := r.Info
|
||||
|
||||
fmt.Println("Identity")
|
||||
fmt.Printf(" deviceID : %s\n", i.DeviceID)
|
||||
|
||||
if suffix := deviceIDSuffix(i.DeviceID); suffix != "" {
|
||||
fmt.Printf(" → use as --match suffix for wait-online: %s\n", suffix)
|
||||
}
|
||||
|
||||
fmt.Printf(" name : %s\n", i.Name)
|
||||
fmt.Printf(" type : %s\n", i.Type)
|
||||
|
||||
for _, comp := range i.Components {
|
||||
if comp.SoftwareVersion != "" {
|
||||
fmt.Printf(" softwareVersion : %s (component %s)\n", comp.SoftwareVersion, comp.Category)
|
||||
}
|
||||
|
||||
if comp.SerialNumber != "" {
|
||||
fmt.Printf(" serialNumber : %s (component %s)\n", comp.SerialNumber, comp.Category)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println("Pairing")
|
||||
|
||||
if i.MargeAccountUUID == "" {
|
||||
PrintWarning("margeAccountUUID is empty — device is unpaired (factory-reset state)")
|
||||
} else {
|
||||
fmt.Printf(" margeAccountUUID : %s\n", i.MargeAccountUUID)
|
||||
}
|
||||
|
||||
fmt.Printf(" margeURL : %s\n", i.MargeURL)
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
func renderInspectNetwork(r *setup.InspectReport) {
|
||||
if r.NetworkErr != nil {
|
||||
PrintError(fmt.Sprintf("/networkInfo: %v", r.NetworkErr))
|
||||
} else if r.Network != nil {
|
||||
fmt.Println("Network")
|
||||
|
||||
for i := range r.Network.Interfaces.Interfaces {
|
||||
iface := &r.Network.Interfaces.Interfaces[i]
|
||||
|
||||
fmt.Printf(" %s\n", iface.Type)
|
||||
fmt.Printf(" state : %s\n", iface.State)
|
||||
|
||||
if iface.IPAddress != "" {
|
||||
fmt.Printf(" ipAddress : %s\n", iface.IPAddress)
|
||||
}
|
||||
|
||||
if iface.MacAddress != "" {
|
||||
fmt.Printf(" macAddress : %s\n", iface.MacAddress)
|
||||
}
|
||||
|
||||
if iface.SSID != "" {
|
||||
fmt.Printf(" ssid : %s\n", iface.SSID)
|
||||
fmt.Printf(" → use as --ssid for wifi-push: %s\n", iface.SSID)
|
||||
}
|
||||
|
||||
if iface.Signal != "" {
|
||||
fmt.Printf(" signal : %s\n", iface.Signal)
|
||||
}
|
||||
|
||||
if iface.FrequencyKHz != 0 {
|
||||
fmt.Printf(" frequency : %d kHz\n", iface.FrequencyKHz)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
return
|
||||
}
|
||||
|
||||
if r.Network == nil {
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("Network")
|
||||
|
||||
for i := range r.Network.Interfaces.Interfaces {
|
||||
iface := &r.Network.Interfaces.Interfaces[i]
|
||||
|
||||
fmt.Printf(" %s\n", iface.Type)
|
||||
fmt.Printf(" state : %s\n", iface.State)
|
||||
|
||||
if iface.IPAddress != "" {
|
||||
fmt.Printf(" ipAddress : %s\n", iface.IPAddress)
|
||||
}
|
||||
|
||||
if iface.MacAddress != "" {
|
||||
fmt.Printf(" macAddress : %s\n", iface.MacAddress)
|
||||
}
|
||||
|
||||
if iface.SSID != "" {
|
||||
fmt.Printf(" ssid : %s\n", iface.SSID)
|
||||
fmt.Printf(" → use as --ssid for wifi-push: %s\n", iface.SSID)
|
||||
}
|
||||
|
||||
if iface.Signal != "" {
|
||||
fmt.Printf(" signal : %s\n", iface.Signal)
|
||||
}
|
||||
|
||||
if iface.FrequencyKHz != 0 {
|
||||
fmt.Printf(" frequency : %d kHz\n", iface.FrequencyKHz)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
func renderInspectSources(r *setup.InspectReport) {
|
||||
if r.SourcesErr != nil {
|
||||
PrintError(fmt.Sprintf("/sources: %v", r.SourcesErr))
|
||||
} else if r.Sources != nil {
|
||||
fmt.Printf("Sources (%d)\n", len(r.Sources.SourceItem))
|
||||
renderSourceTable(r.Sources.SourceItem)
|
||||
fmt.Println()
|
||||
return
|
||||
}
|
||||
|
||||
if r.Sources == nil {
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Sources (%d)\n", len(r.Sources.SourceItem))
|
||||
renderSourceTable(r.Sources.SourceItem)
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
func renderInspectPresets(r *setup.InspectReport) {
|
||||
if r.PresetsErr != nil {
|
||||
PrintError(fmt.Sprintf("/presets: %v", r.PresetsErr))
|
||||
} else if r.Presets != nil {
|
||||
fmt.Printf("Presets (%d)\n", len(r.Presets.Presets))
|
||||
|
||||
for _, p := range r.Presets.Presets {
|
||||
fmt.Printf(" [%s] %s (source=%s)\n", p.ID, p.ContentItem.ItemName, p.ContentItem.Source)
|
||||
}
|
||||
|
||||
if len(r.Presets.Presets) == 0 {
|
||||
fmt.Println(" (none)")
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
return
|
||||
}
|
||||
|
||||
if r.Presets == nil {
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Presets (%d)\n", len(r.Presets.Presets))
|
||||
|
||||
for _, p := range r.Presets.Presets {
|
||||
fmt.Printf(" [%s] %s (source=%s)\n", p.ID, p.ContentItem.ItemName, p.ContentItem.Source)
|
||||
}
|
||||
|
||||
if len(r.Presets.Presets) == 0 {
|
||||
fmt.Println(" (none)")
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
func renderInspectRuntimeURLs(r *setup.InspectReport) {
|
||||
if r.RuntimeErr != nil {
|
||||
PrintError(fmt.Sprintf("telnet getpdo: %v", r.RuntimeErr))
|
||||
} else if r.RuntimeURLs != "" {
|
||||
fmt.Println("Runtime URL configuration (telnet getpdo)")
|
||||
|
||||
for _, line := range strings.Split(r.RuntimeURLs, "\n") {
|
||||
fmt.Printf(" %s\n", line)
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
return
|
||||
}
|
||||
|
||||
if r.RuntimeURLs == "" {
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("Runtime URL configuration (telnet getpdo)")
|
||||
|
||||
for _, line := range strings.Split(r.RuntimeURLs, "\n") {
|
||||
fmt.Printf(" %s\n", line)
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
// sourceLine is the per-source row before any width-padding decisions
|
||||
@@ -1092,60 +1133,7 @@ func buildPlanSteps(
|
||||
var steps []planStep
|
||||
|
||||
if reset {
|
||||
steps = append(steps, planStep{
|
||||
title: "Factory-reset the speaker",
|
||||
cmd: fmt.Sprintf("soundtouch-cli setup factory-reset --host=%s", host),
|
||||
reason: "Wipes account pairing, presets, Wi-Fi — gives a clean baseline for the SETUP state machine.",
|
||||
})
|
||||
|
||||
steps = append(steps, planStep{
|
||||
title: "Connect this host to the speaker's setup AP",
|
||||
cmd: `# macOS: networksetup -setairportnetwork en0 "Bose SoundTouch XXXX"`,
|
||||
reason: "After reset the speaker broadcasts its own Wi-Fi at 192.0.2.1.",
|
||||
manual: true,
|
||||
})
|
||||
|
||||
steps = append(steps, planStep{
|
||||
title: "Wait for the speaker's setup-mode IP to respond",
|
||||
cmd: "soundtouch-cli setup wait-ap",
|
||||
})
|
||||
|
||||
ssidArg := wifiSSID
|
||||
if ssidArg == "" {
|
||||
if currentSSID := inspectedSSID(inspect); currentSSID != "" {
|
||||
ssidArg = currentSSID
|
||||
} else {
|
||||
ssidArg = "<HOME_SSID>"
|
||||
}
|
||||
}
|
||||
|
||||
steps = append(steps, planStep{
|
||||
title: "Push home Wi-Fi credentials to the speaker",
|
||||
cmd: fmt.Sprintf(`soundtouch-cli setup wifi-push --ssid=%q --pass=<HOME_PASS>`, ssidArg),
|
||||
reason: "Speaker leaves AP mode and joins your home network within ~30 s.",
|
||||
})
|
||||
|
||||
steps = append(steps, planStep{
|
||||
title: "Switch this host back to home Wi-Fi",
|
||||
cmd: fmt.Sprintf(`# macOS: networksetup -setairportnetwork en0 %q <HOME_PASS>`, ssidArg),
|
||||
reason: "Required so wait-online's mDNS browse reaches the right network segment.",
|
||||
manual: true,
|
||||
})
|
||||
|
||||
match := ""
|
||||
if inspect.Info != nil {
|
||||
match = deviceIDSuffix(inspect.Info.DeviceID)
|
||||
}
|
||||
|
||||
if match == "" {
|
||||
match = "<deviceID-suffix>"
|
||||
}
|
||||
|
||||
steps = append(steps, planStep{
|
||||
title: "Discover the speaker's new IP via mDNS",
|
||||
cmd: fmt.Sprintf("soundtouch-cli setup wait-online --match=%s", match),
|
||||
})
|
||||
|
||||
steps = append(steps, resetSteps(host, wifiSSID, inspect)...)
|
||||
host = "<NEW_IP>" // subsequent commands target the discovered IP
|
||||
}
|
||||
|
||||
@@ -1153,60 +1141,8 @@ func buildPlanSteps(
|
||||
return steps
|
||||
}
|
||||
|
||||
if !reset && (summary == nil || !summary.IsMigrated) {
|
||||
method, methodReason := recommendMigrationMethod(serviceURL, summary)
|
||||
if method == "" {
|
||||
steps = append(steps, planStep{
|
||||
title: "Enable SSH on the speaker (USB-stick procedure)",
|
||||
cmd: "# See `soundtouch-cli setup ssh-check` output for the USB-stick steps.",
|
||||
reason: "Telnet won't respond and SSH is closed — no transport available to apply a migration.",
|
||||
manual: true,
|
||||
})
|
||||
} else {
|
||||
if method == setup.MigrationMethodResolvConf || method == setup.MigrationMethodHosts {
|
||||
if summary != nil && !summary.CACertTrusted {
|
||||
steps = append(steps, planStep{
|
||||
title: "Install AfterTouch's CA cert on the speaker",
|
||||
cmd: fmt.Sprintf("soundtouch-cli setup install-ca --host=%s --service-url=%s",
|
||||
host, serviceURL),
|
||||
reason: "DNS-redirect methods keep using https://*.bose.com URLs — the device needs to trust AfterTouch's cert.",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
steps = append(steps, planStep{
|
||||
title: fmt.Sprintf("Apply URL migration using method=%s", method),
|
||||
cmd: fmt.Sprintf("soundtouch-cli setup migrate --host=%s --service-url=%s --method=%s",
|
||||
host, serviceURL, method),
|
||||
reason: methodReason,
|
||||
})
|
||||
|
||||
steps = append(steps, planStep{
|
||||
title: "Reboot the speaker",
|
||||
cmd: fmt.Sprintf("soundtouch-cli setup reboot --host=%s", host),
|
||||
reason: "The envswitch parallel-persistence layer only fully wins on next boot; reboot now to lock the new URLs in before pairing.",
|
||||
})
|
||||
}
|
||||
} else if reset {
|
||||
// Reset path always re-applies a migration after wifi-push.
|
||||
method, methodReason := recommendMigrationMethod(serviceURL, nil)
|
||||
if method == "" {
|
||||
method = setup.MigrationMethodTelnet
|
||||
methodReason = "Default: envswitch — works on most firmware-27 devices without SSH."
|
||||
}
|
||||
|
||||
steps = append(steps, planStep{
|
||||
title: fmt.Sprintf("Apply URL migration using method=%s", method),
|
||||
cmd: fmt.Sprintf("soundtouch-cli setup migrate --host=%s --service-url=%s --method=%s",
|
||||
host, serviceURL, method),
|
||||
reason: methodReason,
|
||||
})
|
||||
|
||||
steps = append(steps, planStep{
|
||||
title: "Reboot the speaker",
|
||||
cmd: fmt.Sprintf("soundtouch-cli setup reboot --host=%s", host),
|
||||
reason: "The envswitch parallel-persistence layer only fully wins on next boot; reboot now to lock the new URLs in before pairing.",
|
||||
})
|
||||
if reset || summary == nil || !summary.IsMigrated {
|
||||
steps = append(steps, migrationSteps(host, serviceURL, summary, reset)...)
|
||||
}
|
||||
|
||||
if includePair && (reset || (summary != nil && !summary.IsPaired)) {
|
||||
@@ -1220,6 +1156,108 @@ func buildPlanSteps(
|
||||
return steps
|
||||
}
|
||||
|
||||
// resetSteps composes the factory-reset → AP-switch → wait-ap →
|
||||
// wifi-push → home-switch → wait-online prefix that --reset adds.
|
||||
func resetSteps(host, wifiSSID string, inspect *setup.InspectReport) []planStep {
|
||||
ssidArg := wifiSSID
|
||||
if ssidArg == "" {
|
||||
if currentSSID := inspectedSSID(inspect); currentSSID != "" {
|
||||
ssidArg = currentSSID
|
||||
} else {
|
||||
ssidArg = "<HOME_SSID>"
|
||||
}
|
||||
}
|
||||
|
||||
match := ""
|
||||
if inspect != nil && inspect.Info != nil {
|
||||
match = deviceIDSuffix(inspect.Info.DeviceID)
|
||||
}
|
||||
|
||||
if match == "" {
|
||||
match = "<deviceID-suffix>"
|
||||
}
|
||||
|
||||
return []planStep{
|
||||
{
|
||||
title: "Factory-reset the speaker",
|
||||
cmd: fmt.Sprintf("soundtouch-cli setup factory-reset --host=%s", host),
|
||||
reason: "Wipes account pairing, presets, Wi-Fi — gives a clean baseline for the SETUP state machine.",
|
||||
},
|
||||
{
|
||||
title: "Connect this host to the speaker's setup AP",
|
||||
cmd: `# macOS: networksetup -setairportnetwork en0 "Bose SoundTouch XXXX"`,
|
||||
reason: "After reset the speaker broadcasts its own Wi-Fi at 192.0.2.1.",
|
||||
manual: true,
|
||||
},
|
||||
{
|
||||
title: "Wait for the speaker's setup-mode IP to respond",
|
||||
cmd: "soundtouch-cli setup wait-ap",
|
||||
},
|
||||
{
|
||||
title: "Push home Wi-Fi credentials to the speaker",
|
||||
cmd: fmt.Sprintf(`soundtouch-cli setup wifi-push --ssid=%q --pass=<HOME_PASS>`, ssidArg),
|
||||
reason: "Speaker leaves AP mode and joins your home network within ~30 s.",
|
||||
},
|
||||
{
|
||||
title: "Switch this host back to home Wi-Fi",
|
||||
cmd: fmt.Sprintf(`# macOS: networksetup -setairportnetwork en0 %q <HOME_PASS>`, ssidArg),
|
||||
reason: "Required so wait-online's mDNS browse reaches the right network segment.",
|
||||
manual: true,
|
||||
},
|
||||
{
|
||||
title: "Discover the speaker's new IP via mDNS",
|
||||
cmd: fmt.Sprintf("soundtouch-cli setup wait-online --match=%s", match),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// migrationSteps composes the migrate + reboot pair (plus optional
|
||||
// install-ca prelude for DNS-redirect methods). In --reset mode the
|
||||
// summary is nil and we default to method=telnet; otherwise we let the
|
||||
// recommender pick based on the speaker's current capabilities.
|
||||
func migrationSteps(host, serviceURL string, summary *setup.MigrationSummary, reset bool) []planStep {
|
||||
var steps []planStep
|
||||
|
||||
method, methodReason := recommendMigrationMethod(serviceURL, summary)
|
||||
|
||||
if method == "" {
|
||||
if reset {
|
||||
method = setup.MigrationMethodTelnet
|
||||
methodReason = "Default: envswitch — works on most firmware-27 devices without SSH."
|
||||
} else {
|
||||
return []planStep{{
|
||||
title: "Enable SSH on the speaker (USB-stick procedure)",
|
||||
cmd: "# See `soundtouch-cli setup ssh-check` output for the USB-stick steps.",
|
||||
reason: "Telnet won't respond and SSH is closed — no transport available to apply a migration.",
|
||||
manual: true,
|
||||
}}
|
||||
}
|
||||
}
|
||||
|
||||
dnsRedirect := method == setup.MigrationMethodResolvConf || method == setup.MigrationMethodHosts
|
||||
if dnsRedirect && summary != nil && !summary.CACertTrusted {
|
||||
steps = append(steps, planStep{
|
||||
title: "Install AfterTouch's CA cert on the speaker",
|
||||
cmd: fmt.Sprintf("soundtouch-cli setup install-ca --host=%s --service-url=%s", host, serviceURL),
|
||||
reason: "DNS-redirect methods keep using https://*.bose.com URLs — the device needs to trust AfterTouch's cert.",
|
||||
})
|
||||
}
|
||||
|
||||
steps = append(steps, planStep{
|
||||
title: fmt.Sprintf("Apply URL migration using method=%s", method),
|
||||
cmd: fmt.Sprintf("soundtouch-cli setup migrate --host=%s --service-url=%s --method=%s", host, serviceURL, method),
|
||||
reason: methodReason,
|
||||
})
|
||||
|
||||
steps = append(steps, planStep{
|
||||
title: "Reboot the speaker",
|
||||
cmd: fmt.Sprintf("soundtouch-cli setup reboot --host=%s", host),
|
||||
reason: "The envswitch parallel-persistence layer only fully wins on next boot; reboot now to lock the new URLs in before pairing.",
|
||||
})
|
||||
|
||||
return steps
|
||||
}
|
||||
|
||||
// recommendMigrationMethod picks a migration method from the speaker's
|
||||
// current capabilities. Returns "" when no transport is available.
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user