mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-19 00:56:16 +00:00
Fix WebSocket connection issues and add special message parsing
- Fix WebSocket URL construction by properly extracting hostname from base URL - Add 'gabbo' protocol requirement as specified in SoundTouch API docs - Add parsing for SoundTouchSdkInfo and UserActivityUpdate messages - Add proper filtering support for special message types (sdkInfo, userActivity) - Fix nil pointer dereference by ensuring WebSocket client always has a logger - Add SilentLogger for non-verbose mode to prevent crashes - Update README and help text to include new special message types - Clean up logging to only show unknown message types, not known special messages Fixes the original WebSocket connection error: 'parse "ws://http:%2F%2F192.168.178.28:8090:8080/": invalid URL escape "%2F"'
This commit is contained in:
@@ -145,7 +145,7 @@ go run ./cmd/websocket-demo -host 192.168.1.10 -filter volume,nowPlaying
|
||||
go run ./cmd/websocket-demo -host 192.168.1.10 -duration 5m -verbose
|
||||
|
||||
# Available event types for filtering:
|
||||
# nowPlaying, volume, connection, preset, zone, bass
|
||||
# nowPlaying, volume, connection, preset, zone, bass, sdkInfo, userActivity
|
||||
```
|
||||
|
||||
**Supported WebSocket Events:**
|
||||
@@ -155,6 +155,8 @@ go run ./cmd/websocket-demo -host 192.168.1.10 -duration 5m -verbose
|
||||
- 📻 **Preset**: Preset configuration updates
|
||||
- 🏠 **Zone**: Multiroom zone membership changes
|
||||
- 🎚️ **Bass**: Bass equalizer level adjustments
|
||||
- 📡 **SDK Info**: Server version and build information (sent on connection)
|
||||
- 👤 **User Activity**: User interaction notifications
|
||||
|
||||
See [docs/websocket-events.md](docs/websocket-events.md) for complete WebSocket documentation.
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ func parseFilters(eventFilter string) map[string]bool {
|
||||
validFilters := map[string]bool{
|
||||
"nowPlaying": true, "volume": true, "connection": true,
|
||||
"preset": true, "zone": true, "bass": true,
|
||||
"sdkInfo": true, "userActivity": true,
|
||||
}
|
||||
|
||||
if eventFilter == "" {
|
||||
@@ -59,7 +60,7 @@ func parseFilters(eventFilter string) map[string]bool {
|
||||
for _, f := range filterList {
|
||||
f = strings.TrimSpace(f)
|
||||
if !validFilters[f] {
|
||||
fmt.Printf("Invalid filter '%s'. Valid filters: nowPlaying, volume, connection, preset, zone, bass\n", f)
|
||||
fmt.Printf("Invalid filter '%s'. Valid filters: nowPlaying, volume, connection, preset, zone, bass, sdkInfo, userActivity\n", f)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@@ -116,6 +117,9 @@ func setupWebSocket(soundTouchClient *client.Client, reconnect, verbose bool) *c
|
||||
|
||||
if verbose {
|
||||
wsConfig.Logger = &VerboseLogger{}
|
||||
} else {
|
||||
// Use a silent logger when not verbose
|
||||
wsConfig.Logger = &SilentLogger{}
|
||||
}
|
||||
|
||||
if !reconnect {
|
||||
@@ -187,6 +191,11 @@ func main() {
|
||||
// Set up event handlers
|
||||
setupEventHandlers(wsClient, filters, *verbose)
|
||||
|
||||
// Set up special message handler
|
||||
wsClient.OnSpecialMessage(func(message *models.SpecialMessage) {
|
||||
handleSpecialMessage(message, filters, *verbose)
|
||||
})
|
||||
|
||||
// Connect to WebSocket
|
||||
fmt.Println("Connecting to WebSocket...")
|
||||
|
||||
@@ -382,6 +391,43 @@ func handleBass(event *models.BassUpdatedEvent) {
|
||||
fmt.Printf(" 📊 %s\n", levelDesc)
|
||||
}
|
||||
|
||||
func handleSpecialMessage(message *models.SpecialMessage, filters map[string]bool, verbose bool) {
|
||||
// Check if we should filter this message type
|
||||
if filters != nil {
|
||||
switch message.Type {
|
||||
case models.MessageTypeSdkInfo:
|
||||
if !filters["sdkInfo"] {
|
||||
return
|
||||
}
|
||||
case models.MessageTypeUserActivity:
|
||||
if !filters["userActivity"] {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch message.Type {
|
||||
case models.MessageTypeSdkInfo:
|
||||
if sdkInfo := message.GetSdkInfo(); sdkInfo != nil {
|
||||
fmt.Printf("\n📡 SDK Info:\n")
|
||||
fmt.Printf(" 📋 Server Version: %s\n", sdkInfo.ServerVersion)
|
||||
fmt.Printf(" 🔧 Server Build: %s\n", sdkInfo.ServerBuild)
|
||||
}
|
||||
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"))
|
||||
}
|
||||
default:
|
||||
fmt.Printf("\n❓ Unknown Special Message: %s\n", message.String())
|
||||
|
||||
if verbose {
|
||||
fmt.Printf(" 📱 Raw data: %s\n", string(message.RawData))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setupEventHandlers(wsClient *client.WebSocketClient, filters map[string]bool, verbose bool) {
|
||||
// Now Playing events
|
||||
if filters == nil || filters["nowPlaying"] {
|
||||
@@ -477,7 +523,7 @@ func printHelp() {
|
||||
fmt.Println(" Enable verbose logging")
|
||||
fmt.Println(" -filter string")
|
||||
fmt.Println(" Filter events by type (comma-separated):")
|
||||
fmt.Println(" nowPlaying, volume, connection, preset, zone, bass")
|
||||
fmt.Println(" nowPlaying, volume, connection, preset, zone, bass, sdkInfo, userActivity")
|
||||
fmt.Println(" -help")
|
||||
fmt.Println(" Show this help message")
|
||||
fmt.Println()
|
||||
@@ -501,6 +547,8 @@ func printHelp() {
|
||||
fmt.Println(" 📻 preset - Preset configuration changes")
|
||||
fmt.Println(" 🏠 zone - Multiroom zone changes")
|
||||
fmt.Println(" 🎚️ bass - Bass level changes")
|
||||
fmt.Println(" 📡 sdkInfo - SDK version information")
|
||||
fmt.Println(" 👤 userActivity - User interaction notifications")
|
||||
fmt.Println()
|
||||
fmt.Println("The tool will automatically reconnect if the connection is lost.")
|
||||
fmt.Println("Press Ctrl+C to stop monitoring.")
|
||||
@@ -513,3 +561,10 @@ func (v *VerboseLogger) Printf(format string, args ...interface{}) {
|
||||
timestamp := time.Now().Format("15:04:05")
|
||||
fmt.Printf("[%s] [WebSocket] %s\n", timestamp, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// SilentLogger provides no-op WebSocket logging
|
||||
type SilentLogger struct{}
|
||||
|
||||
func (s *SilentLogger) Printf(_ string, _ ...interface{}) {
|
||||
// Do nothing - silent logging
|
||||
}
|
||||
|
||||
+52
-2
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -153,6 +154,14 @@ func (ws *WebSocketClient) OnUnknownEvent(handler models.EventHandler) {
|
||||
ws.handlers.OnUnknownEvent = handler
|
||||
}
|
||||
|
||||
// OnSpecialMessage sets a handler for special (non-updates) messages
|
||||
func (ws *WebSocketClient) OnSpecialMessage(handler models.SpecialMessageHandler) {
|
||||
ws.mu.Lock()
|
||||
defer ws.mu.Unlock()
|
||||
|
||||
ws.handlers.OnSpecialMessage = handler
|
||||
}
|
||||
|
||||
// Connect establishes a WebSocket connection to the SoundTouch device
|
||||
func (ws *WebSocketClient) Connect() error {
|
||||
return ws.connectWithConfig(DefaultWebSocketConfig())
|
||||
@@ -172,19 +181,26 @@ func (ws *WebSocketClient) connectWithConfig(config *WebSocketConfig) error {
|
||||
}
|
||||
|
||||
// Build WebSocket URL
|
||||
// Parse the base URL to extract just the hostname
|
||||
baseURL, err := url.Parse(ws.client.BaseURL())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse base URL: %w", err)
|
||||
}
|
||||
|
||||
wsURL := url.URL{
|
||||
Scheme: "ws",
|
||||
Host: fmt.Sprintf("%s:%d", ws.client.Host(), 8080), // SoundTouch WebSocket port is typically 8080
|
||||
Host: fmt.Sprintf("%s:8080", baseURL.Hostname()), // SoundTouch WebSocket port is typically 8080
|
||||
Path: "/",
|
||||
}
|
||||
|
||||
ws.logger.Printf("Connecting to %s", wsURL.String())
|
||||
|
||||
// Create dialer with custom buffer sizes
|
||||
// Create dialer with custom buffer sizes and "gabbo" protocol
|
||||
dialer := websocket.Dialer{
|
||||
HandshakeTimeout: 10 * time.Second,
|
||||
ReadBufferSize: config.ReadBufferSize,
|
||||
WriteBufferSize: config.WriteBufferSize,
|
||||
Subprotocols: []string{"gabbo"}, // Required by SoundTouch API
|
||||
}
|
||||
|
||||
// Establish connection
|
||||
@@ -357,6 +373,12 @@ func (ws *WebSocketClient) attemptReconnect(config *WebSocketConfig) {
|
||||
|
||||
// handleMessage processes incoming WebSocket messages
|
||||
func (ws *WebSocketClient) handleMessage(data []byte) {
|
||||
// Check if this is a SoundTouchSdkInfo or other non-updates message
|
||||
if !ws.isUpdatesMessage(data) {
|
||||
ws.handleSpecialMessage(data)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the WebSocket event
|
||||
event, err := models.ParseWebSocketEvent(data)
|
||||
if err != nil {
|
||||
@@ -368,6 +390,34 @@ func (ws *WebSocketClient) handleMessage(data []byte) {
|
||||
ws.handleEvent(event)
|
||||
}
|
||||
|
||||
// handleSpecialMessage processes special (non-updates) WebSocket messages
|
||||
func (ws *WebSocketClient) handleSpecialMessage(data []byte) {
|
||||
specialMessage, err := models.ParseSpecialMessage(data)
|
||||
if err != nil {
|
||||
ws.logger.Printf("Unknown special message type: %v", err)
|
||||
ws.logger.Printf("Raw message: %s", string(data))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Call handler if set
|
||||
ws.mu.RLock()
|
||||
handler := ws.handlers.OnSpecialMessage
|
||||
ws.mu.RUnlock()
|
||||
|
||||
if handler != nil {
|
||||
handler(specialMessage)
|
||||
}
|
||||
}
|
||||
|
||||
// isUpdatesMessage checks if the message contains an <updates> element
|
||||
func (ws *WebSocketClient) isUpdatesMessage(data []byte) bool {
|
||||
// Simple check for <updates> element - this avoids full XML parsing
|
||||
// for messages we want to ignore like <SoundTouchSdkInfo>
|
||||
dataStr := string(data)
|
||||
return strings.Contains(dataStr, "<updates") && strings.Contains(dataStr, "deviceID=")
|
||||
}
|
||||
|
||||
func (ws *WebSocketClient) dispatchTypedEvent(handlers *models.WebSocketEventHandlers, eventType models.WebSocketEventType, event *models.WebSocketEvent) bool {
|
||||
switch eventType {
|
||||
case models.EventTypeNowPlaying:
|
||||
|
||||
+114
-6
@@ -3,6 +3,7 @@ package models
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -298,13 +299,47 @@ type Language struct {
|
||||
Value string `xml:",chardata"`
|
||||
}
|
||||
|
||||
// SpecialMessageType represents message types that are not part of <updates>
|
||||
type SpecialMessageType string
|
||||
|
||||
// Constants for special message types
|
||||
const (
|
||||
MessageTypeSdkInfo SpecialMessageType = "sdkInfo"
|
||||
MessageTypeUserActivity SpecialMessageType = "userActivity"
|
||||
)
|
||||
|
||||
// SoundTouchSdkInfo represents the SDK info message sent on connection
|
||||
type SoundTouchSdkInfo struct {
|
||||
XMLName xml.Name `xml:"SoundTouchSdkInfo"`
|
||||
ServerVersion string `xml:"serverVersion,attr"`
|
||||
ServerBuild string `xml:"serverBuild,attr"`
|
||||
}
|
||||
|
||||
// UserActivityUpdate represents user activity notifications
|
||||
type UserActivityUpdate struct {
|
||||
XMLName xml.Name `xml:"userActivityUpdate"`
|
||||
DeviceID string `xml:"deviceID,attr"`
|
||||
}
|
||||
|
||||
// SpecialMessage represents non-updates WebSocket messages
|
||||
type SpecialMessage struct {
|
||||
Type SpecialMessageType
|
||||
DeviceID string
|
||||
Data interface{}
|
||||
RawData []byte
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
// SpecialMessageHandler defines the signature for special message handlers
|
||||
type SpecialMessageHandler func(message *SpecialMessage)
|
||||
|
||||
// EventHandler represents a function that handles WebSocket events
|
||||
type EventHandler func(event *WebSocketEvent)
|
||||
|
||||
// TypedEventHandler represents a function that handles specific event types
|
||||
type TypedEventHandler[T any] func(event T)
|
||||
|
||||
// WebSocketEventHandlers holds typed event handlers for different event types
|
||||
// WebSocketEventHandlers contains handlers for different types of WebSocket events
|
||||
type WebSocketEventHandlers struct {
|
||||
OnNowPlaying TypedEventHandler[*NowPlayingUpdatedEvent]
|
||||
OnVolumeUpdated TypedEventHandler[*VolumeUpdatedEvent]
|
||||
@@ -319,6 +354,7 @@ type WebSocketEventHandlers struct {
|
||||
OnRecentsUpdated TypedEventHandler[*RecentsUpdatedEvent]
|
||||
OnLanguageUpdated TypedEventHandler[*LanguageUpdatedEvent]
|
||||
OnUnknownEvent EventHandler
|
||||
OnSpecialMessage SpecialMessageHandler
|
||||
}
|
||||
|
||||
// ParseWebSocketEvent attempts to parse a WebSocket message into a specific event type
|
||||
@@ -521,16 +557,88 @@ func (e *WebSocketEvent) GetEventTypes() []WebSocketEventType {
|
||||
|
||||
// String returns a human-readable string representation of the WebSocket event
|
||||
func (e *WebSocketEvent) String() string {
|
||||
events := e.GetEvents()
|
||||
eventTypes := e.GetEventTypes()
|
||||
|
||||
if len(events) == 0 {
|
||||
if len(eventTypes) == 0 {
|
||||
return fmt.Sprintf("WebSocket Event [Device: %s] - No events", e.DeviceID)
|
||||
}
|
||||
|
||||
if len(events) == 1 {
|
||||
if len(eventTypes) == 1 {
|
||||
return fmt.Sprintf("WebSocket Event [Device: %s] - %s", e.DeviceID, eventTypes[0].String())
|
||||
}
|
||||
|
||||
return fmt.Sprintf("WebSocket Event [Device: %s] - %d events", e.DeviceID, len(events))
|
||||
return fmt.Sprintf("WebSocket Event [Device: %s] - %d events", e.DeviceID, len(eventTypes))
|
||||
}
|
||||
|
||||
// ParseSpecialMessage parses non-updates WebSocket messages
|
||||
func ParseSpecialMessage(data []byte) (*SpecialMessage, error) {
|
||||
dataStr := string(data)
|
||||
|
||||
// Check for SoundTouchSdkInfo
|
||||
if strings.Contains(dataStr, "<SoundTouchSdkInfo") {
|
||||
var sdkInfo SoundTouchSdkInfo
|
||||
if err := xml.Unmarshal(data, &sdkInfo); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse SoundTouchSdkInfo: %w", err)
|
||||
}
|
||||
|
||||
return &SpecialMessage{
|
||||
Type: MessageTypeSdkInfo,
|
||||
Data: &sdkInfo,
|
||||
RawData: data,
|
||||
Timestamp: time.Now(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Check for userActivityUpdate
|
||||
if strings.Contains(dataStr, "<userActivityUpdate") {
|
||||
var userActivity UserActivityUpdate
|
||||
if err := xml.Unmarshal(data, &userActivity); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse userActivityUpdate: %w", err)
|
||||
}
|
||||
|
||||
return &SpecialMessage{
|
||||
Type: MessageTypeUserActivity,
|
||||
DeviceID: userActivity.DeviceID,
|
||||
Data: &userActivity,
|
||||
RawData: data,
|
||||
Timestamp: time.Now(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unknown special message type: %s", dataStr)
|
||||
}
|
||||
|
||||
// GetSdkInfo returns the parsed SdkInfo data if the message is of that type
|
||||
func (sm *SpecialMessage) GetSdkInfo() *SoundTouchSdkInfo {
|
||||
if sm.Type == MessageTypeSdkInfo {
|
||||
if sdkInfo, ok := sm.Data.(*SoundTouchSdkInfo); ok {
|
||||
return sdkInfo
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetUserActivity returns the parsed UserActivity data if the message is of that type
|
||||
func (sm *SpecialMessage) GetUserActivity() *UserActivityUpdate {
|
||||
if sm.Type == MessageTypeUserActivity {
|
||||
if userActivity, ok := sm.Data.(*UserActivityUpdate); ok {
|
||||
return userActivity
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// String returns a string representation of the special message
|
||||
func (sm *SpecialMessage) String() string {
|
||||
switch sm.Type {
|
||||
case MessageTypeSdkInfo:
|
||||
if sdkInfo := sm.GetSdkInfo(); sdkInfo != nil {
|
||||
return fmt.Sprintf("SoundTouch SDK Info - Version: %s, Build: %s", sdkInfo.ServerVersion, sdkInfo.ServerBuild)
|
||||
}
|
||||
case MessageTypeUserActivity:
|
||||
return fmt.Sprintf("User Activity [Device: %s]", sm.DeviceID)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("Unknown Special Message - Type: %s", sm.Type)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user