homebridge-bose-soundtouch 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.json +26 -0
- package/README.md +195 -0
- package/config.schema.json +247 -0
- package/dist/discovery.d.ts +20 -0
- package/dist/discovery.js +93 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/platform.d.ts +43 -0
- package/dist/platform.js +153 -0
- package/dist/settings.d.ts +8 -0
- package/dist/settings.js +11 -0
- package/dist/soundtouchAccessory.d.ts +37 -0
- package/dist/soundtouchAccessory.js +442 -0
- package/dist/soundtouchClient.d.ts +135 -0
- package/dist/soundtouchClient.js +498 -0
- package/dist/soundtouchWebSocket.d.ts +71 -0
- package/dist/soundtouchWebSocket.js +190 -0
- package/homebridge-ui/public/index.html +600 -0
- package/homebridge-ui/server.js +88 -0
- package/nodemon.json +9 -0
- package/package.json +52 -0
- package/src/discovery.ts +113 -0
- package/src/index.ts +10 -0
- package/src/platform.ts +213 -0
- package/src/settings.ts +9 -0
- package/src/soundtouchAccessory.ts +543 -0
- package/src/soundtouchClient.ts +751 -0
- package/src/soundtouchWebSocket.ts +262 -0
- package/tsconfig.json +24 -0
package/.eslintrc.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"parser": "@typescript-eslint/parser",
|
|
3
|
+
"extends": [
|
|
4
|
+
"eslint:recommended",
|
|
5
|
+
"plugin:@typescript-eslint/eslint-recommended",
|
|
6
|
+
"plugin:@typescript-eslint/recommended"
|
|
7
|
+
],
|
|
8
|
+
"parserOptions": {
|
|
9
|
+
"ecmaVersion": 2022,
|
|
10
|
+
"sourceType": "module"
|
|
11
|
+
},
|
|
12
|
+
"rules": {
|
|
13
|
+
"quotes": ["warn", "single"],
|
|
14
|
+
"indent": ["warn", 2, { "SwitchCase": 1 }],
|
|
15
|
+
"semi": ["error", "always"],
|
|
16
|
+
"comma-dangle": ["warn", "always-multiline"],
|
|
17
|
+
"dot-notation": "off",
|
|
18
|
+
"eqeqeq": "warn",
|
|
19
|
+
"curly": ["warn", "all"],
|
|
20
|
+
"brace-style": ["warn"],
|
|
21
|
+
"prefer-arrow-callback": ["warn"],
|
|
22
|
+
"max-len": ["warn", 140],
|
|
23
|
+
"no-console": ["warn"],
|
|
24
|
+
"@typescript-eslint/no-non-null-assertion": "off"
|
|
25
|
+
}
|
|
26
|
+
}
|
package/README.md
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# Homebridge Bose SoundTouch
|
|
2
|
+
|
|
3
|
+
Ein Homebridge Plugin zur Steuerung von Bose SoundTouch Lautsprechern via Apple HomeKit.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Automatische Erkennung** - Findet SoundTouch-Geräte automatisch im Netzwerk via mDNS
|
|
8
|
+
- **Lautstärkesteuerung** - Regeln der Lautstärke über HomeKit
|
|
9
|
+
- **Wiedergabesteuerung** - Play, Pause, Stop, Nächster/Vorheriger Track
|
|
10
|
+
- **Presets/Favoriten** - Schnellzugriff auf gespeicherte Sender als Schalter
|
|
11
|
+
- **Stummschaltung** - Mute-Funktion
|
|
12
|
+
- **Echtzeit-Updates** - WebSocket-Verbindung für sofortige Statusänderungen
|
|
13
|
+
- **Multi-Room** - Unterstützung für mehrere Geräte
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
### Über Homebridge UI
|
|
18
|
+
|
|
19
|
+
1. Suche nach `homebridge-bose-soundtouch` in der Plugin-Suche
|
|
20
|
+
2. Klicke auf "Installieren"
|
|
21
|
+
|
|
22
|
+
### Manuell via npm
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install -g homebridge-bose-soundtouch
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Aus dem Quellcode
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
cd homebridge-bose-soundtouch
|
|
32
|
+
npm install
|
|
33
|
+
npm run build
|
|
34
|
+
npm link
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Konfiguration
|
|
38
|
+
|
|
39
|
+
### Minimale Konfiguration (Auto-Discovery)
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"platforms": [
|
|
44
|
+
{
|
|
45
|
+
"platform": "BoseSoundTouch",
|
|
46
|
+
"name": "Bose SoundTouch"
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Vollständige Konfiguration
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"platforms": [
|
|
57
|
+
{
|
|
58
|
+
"platform": "BoseSoundTouch",
|
|
59
|
+
"name": "Bose SoundTouch",
|
|
60
|
+
"autoDiscover": true,
|
|
61
|
+
"discoveryTimeout": 10000,
|
|
62
|
+
"enableSmartSpeaker": true,
|
|
63
|
+
"volumeAsLightbulb": false,
|
|
64
|
+
"enablePresets": true,
|
|
65
|
+
"devices": [
|
|
66
|
+
{
|
|
67
|
+
"name": "Wohnzimmer Speaker",
|
|
68
|
+
"host": "192.168.1.100",
|
|
69
|
+
"room": "Wohnzimmer"
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"name": "Schlafzimmer Speaker",
|
|
73
|
+
"host": "192.168.1.101",
|
|
74
|
+
"room": "Schlafzimmer"
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Konfigurationsoptionen
|
|
83
|
+
|
|
84
|
+
| Option | Typ | Standard | Beschreibung |
|
|
85
|
+
|--------|-----|----------|--------------|
|
|
86
|
+
| `platform` | string | **Pflicht** | Muss `"BoseSoundTouch"` sein |
|
|
87
|
+
| `name` | string | **Pflicht** | Name der Plattform |
|
|
88
|
+
| `autoDiscover` | boolean | `true` | Automatische Geräteerkennung |
|
|
89
|
+
| `discoveryTimeout` | number | `10000` | Timeout für Discovery in ms |
|
|
90
|
+
| `enableSmartSpeaker` | boolean | `true` | SmartSpeaker-Service aktivieren |
|
|
91
|
+
| `volumeAsLightbulb` | boolean | `false` | Lautstärke als Lampe darstellen (besserer Slider) |
|
|
92
|
+
| `enablePresets` | boolean | `true` | Preset-Schalter aktivieren |
|
|
93
|
+
| `devices` | array | `[]` | Manuell konfigurierte Geräte |
|
|
94
|
+
|
|
95
|
+
### Geräte-Konfiguration
|
|
96
|
+
|
|
97
|
+
| Option | Typ | Beschreibung |
|
|
98
|
+
|--------|-----|--------------|
|
|
99
|
+
| `name` | string | Anzeigename des Geräts |
|
|
100
|
+
| `host` | string | IP-Adresse des SoundTouch-Geräts |
|
|
101
|
+
| `room` | string | Raum-Zuordnung (optional) |
|
|
102
|
+
|
|
103
|
+
## HomeKit-Funktionen
|
|
104
|
+
|
|
105
|
+
### Speaker Service
|
|
106
|
+
- **Mute** - Stummschaltung ein/aus
|
|
107
|
+
- **Volume** - Lautstärke (0-100%)
|
|
108
|
+
|
|
109
|
+
### Smart Speaker Service (optional)
|
|
110
|
+
- **CurrentMediaState** - Aktueller Status (Play/Pause/Stop)
|
|
111
|
+
- **TargetMediaState** - Wiedergabe steuern
|
|
112
|
+
- **Volume** - Lautstärke
|
|
113
|
+
- **Mute** - Stummschaltung
|
|
114
|
+
|
|
115
|
+
### Lightbulb Service (optional)
|
|
116
|
+
- **On** - Mute/Unmute
|
|
117
|
+
- **Brightness** - Lautstärke als Slider
|
|
118
|
+
|
|
119
|
+
### Preset Switches
|
|
120
|
+
- Schalter für jeden gespeicherten Preset (1-6)
|
|
121
|
+
- Momentary-Verhalten (schaltet automatisch aus)
|
|
122
|
+
|
|
123
|
+
## Unterstützte Geräte
|
|
124
|
+
|
|
125
|
+
Dieses Plugin funktioniert mit allen Bose SoundTouch-Geräten:
|
|
126
|
+
- SoundTouch 10
|
|
127
|
+
- SoundTouch 20
|
|
128
|
+
- SoundTouch 30
|
|
129
|
+
- SoundTouch 300
|
|
130
|
+
- SoundTouch Portable
|
|
131
|
+
- SoundTouch SA-5 Amplifier
|
|
132
|
+
- Wave SoundTouch
|
|
133
|
+
|
|
134
|
+
**Hinweis:** Neuere Bose-Geräte (Home Speaker 500, 700, etc.) werden NICHT unterstützt, da sie eine andere API verwenden.
|
|
135
|
+
|
|
136
|
+
## API-Funktionen
|
|
137
|
+
|
|
138
|
+
Das Plugin nutzt die vollständige [Bose SoundTouch Web API](https://assets.bosecreative.com/m/496577402d128874/original/SoundTouch-Web-API.pdf):
|
|
139
|
+
|
|
140
|
+
- `/info` - Geräteinformationen
|
|
141
|
+
- `/now_playing` - Aktuelle Wiedergabe
|
|
142
|
+
- `/volume` - Lautstärke get/set
|
|
143
|
+
- `/key` - Tastendrücke (Play, Pause, etc.)
|
|
144
|
+
- `/presets` - Gespeicherte Favoriten
|
|
145
|
+
- `/sources` - Verfügbare Quellen
|
|
146
|
+
- `/bass` - Bass-Einstellungen
|
|
147
|
+
- `/getZone` / `/setZone` - Multi-Room-Zonen
|
|
148
|
+
- WebSocket auf Port 8080 für Echtzeit-Updates
|
|
149
|
+
|
|
150
|
+
## Troubleshooting
|
|
151
|
+
|
|
152
|
+
### Gerät wird nicht gefunden
|
|
153
|
+
|
|
154
|
+
1. Stelle sicher, dass das SoundTouch-Gerät eingeschaltet ist
|
|
155
|
+
2. Prüfe, ob sich das Gerät im gleichen Netzwerk befindet
|
|
156
|
+
3. Versuche, die IP-Adresse manuell in der Konfiguration anzugeben
|
|
157
|
+
4. Teste die Verbindung: `curl http://IP_ADRESSE:8090/info`
|
|
158
|
+
|
|
159
|
+
### Keine Steuerung möglich
|
|
160
|
+
|
|
161
|
+
1. Prüfe die Homebridge-Logs auf Fehler
|
|
162
|
+
2. Starte Homebridge neu
|
|
163
|
+
3. Entferne das Gerät aus HomeKit und füge es erneut hinzu
|
|
164
|
+
|
|
165
|
+
### WebSocket-Verbindung fehlerhaft
|
|
166
|
+
|
|
167
|
+
Der WebSocket verbindet sich automatisch erneut. Falls Echtzeit-Updates nicht funktionieren, prüfe ob Port 8080 erreichbar ist.
|
|
168
|
+
|
|
169
|
+
## Entwicklung
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
# Clone und installieren
|
|
173
|
+
git clone https://github.com/daniel/homebridge-bose-soundtouch.git
|
|
174
|
+
cd homebridge-bose-soundtouch
|
|
175
|
+
npm install
|
|
176
|
+
|
|
177
|
+
# Build
|
|
178
|
+
npm run build
|
|
179
|
+
|
|
180
|
+
# Watch mode für Entwicklung
|
|
181
|
+
npm run watch
|
|
182
|
+
|
|
183
|
+
# Linting
|
|
184
|
+
npm run lint
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Lizenz
|
|
188
|
+
|
|
189
|
+
MIT
|
|
190
|
+
|
|
191
|
+
## Credits
|
|
192
|
+
|
|
193
|
+
- [Bose SoundTouch Web API](https://assets.bosecreative.com/m/496577402d128874/original/SoundTouch-Web-API.pdf)
|
|
194
|
+
- [libsoundtouch](https://github.com/CharlesBlonde/libsoundtouch) - Inspiration für API-Implementierung
|
|
195
|
+
- [bosesoundtouchapi](https://github.com/thlucas1/bosesoundtouchapi) - Referenz für WebSocket-Events
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
{
|
|
2
|
+
"pluginAlias": "BoseSoundTouch",
|
|
3
|
+
"pluginType": "platform",
|
|
4
|
+
"singular": true,
|
|
5
|
+
"customUi": true,
|
|
6
|
+
"headerDisplay": "Homebridge Plugin für Bose SoundTouch Lautsprecher - Mit Custom Radio & Streaming Stationen.",
|
|
7
|
+
"footerDisplay": "Weitere Informationen: [GitHub](https://github.com/Dress13/homebridge-bose-soundtouch)",
|
|
8
|
+
"schema": {
|
|
9
|
+
"type": "object",
|
|
10
|
+
"properties": {
|
|
11
|
+
"name": {
|
|
12
|
+
"title": "Name",
|
|
13
|
+
"type": "string",
|
|
14
|
+
"default": "Bose SoundTouch",
|
|
15
|
+
"required": true
|
|
16
|
+
},
|
|
17
|
+
"autoDiscover": {
|
|
18
|
+
"title": "Automatische Erkennung",
|
|
19
|
+
"type": "boolean",
|
|
20
|
+
"default": true,
|
|
21
|
+
"description": "SoundTouch-Geräte automatisch im Netzwerk finden"
|
|
22
|
+
},
|
|
23
|
+
"discoveryTimeout": {
|
|
24
|
+
"title": "Discovery Timeout (ms)",
|
|
25
|
+
"type": "integer",
|
|
26
|
+
"default": 10000,
|
|
27
|
+
"minimum": 1000,
|
|
28
|
+
"maximum": 60000,
|
|
29
|
+
"description": "Timeout für die automatische Geräteerkennung"
|
|
30
|
+
},
|
|
31
|
+
"devices": {
|
|
32
|
+
"title": "Geräte",
|
|
33
|
+
"description": "Geräte werden automatisch erkannt. Hier kannst du pro Gerät Icon und Presets konfigurieren.",
|
|
34
|
+
"type": "array",
|
|
35
|
+
"items": {
|
|
36
|
+
"type": "object",
|
|
37
|
+
"properties": {
|
|
38
|
+
"host": {
|
|
39
|
+
"title": "IP-Adresse",
|
|
40
|
+
"type": "string",
|
|
41
|
+
"required": true,
|
|
42
|
+
"format": "ipv4",
|
|
43
|
+
"description": "IP-Adresse des SoundTouch-Geräts"
|
|
44
|
+
},
|
|
45
|
+
"name": {
|
|
46
|
+
"title": "Name",
|
|
47
|
+
"type": "string",
|
|
48
|
+
"description": "Anzeigename (optional, wird automatisch vom Gerät gelesen)"
|
|
49
|
+
},
|
|
50
|
+
"room": {
|
|
51
|
+
"title": "Raum",
|
|
52
|
+
"type": "string",
|
|
53
|
+
"description": "Raum-Zuordnung (optional)"
|
|
54
|
+
},
|
|
55
|
+
"deviceIcon": {
|
|
56
|
+
"title": "Geräte-Icon",
|
|
57
|
+
"type": "integer",
|
|
58
|
+
"default": 26,
|
|
59
|
+
"oneOf": [
|
|
60
|
+
{ "title": "Speaker (Lautsprecher)", "enum": [26] },
|
|
61
|
+
{ "title": "Audio Receiver", "enum": [34] },
|
|
62
|
+
{ "title": "Television (TV)", "enum": [31] },
|
|
63
|
+
{ "title": "TV Set Top Box", "enum": [35] },
|
|
64
|
+
{ "title": "TV Streaming Stick", "enum": [36] },
|
|
65
|
+
{ "title": "AirPlay Speaker", "enum": [38] },
|
|
66
|
+
{ "title": "HomePod", "enum": [39] },
|
|
67
|
+
{ "title": "Airport", "enum": [27] }
|
|
68
|
+
],
|
|
69
|
+
"description": "Icon für dieses Gerät in der Home App"
|
|
70
|
+
},
|
|
71
|
+
"presets": {
|
|
72
|
+
"title": "Preset-Belegung",
|
|
73
|
+
"description": "Belege die 6 Preset-Tasten mit eigenen Sendern",
|
|
74
|
+
"type": "array",
|
|
75
|
+
"maxItems": 6,
|
|
76
|
+
"items": {
|
|
77
|
+
"type": "object",
|
|
78
|
+
"properties": {
|
|
79
|
+
"slot": {
|
|
80
|
+
"title": "Preset-Taste",
|
|
81
|
+
"type": "integer",
|
|
82
|
+
"minimum": 1,
|
|
83
|
+
"maximum": 6,
|
|
84
|
+
"required": true,
|
|
85
|
+
"description": "Welche Preset-Taste (1-6)"
|
|
86
|
+
},
|
|
87
|
+
"name": {
|
|
88
|
+
"title": "Name",
|
|
89
|
+
"type": "string",
|
|
90
|
+
"required": true,
|
|
91
|
+
"description": "Anzeigename in HomeKit"
|
|
92
|
+
},
|
|
93
|
+
"type": {
|
|
94
|
+
"title": "Typ",
|
|
95
|
+
"type": "string",
|
|
96
|
+
"required": true,
|
|
97
|
+
"default": "radio",
|
|
98
|
+
"oneOf": [
|
|
99
|
+
{ "title": "Internet Radio (HTTP Stream)", "enum": ["radio"] },
|
|
100
|
+
{ "title": "Spotify", "enum": ["spotify"] },
|
|
101
|
+
{ "title": "Amazon Music", "enum": ["amazon"] },
|
|
102
|
+
{ "title": "Deezer", "enum": ["deezer"] },
|
|
103
|
+
{ "title": "TuneIn", "enum": ["tunein"] }
|
|
104
|
+
],
|
|
105
|
+
"description": "Art der Quelle"
|
|
106
|
+
},
|
|
107
|
+
"url": {
|
|
108
|
+
"title": "Stream URL",
|
|
109
|
+
"type": "string",
|
|
110
|
+
"description": "HTTP Stream URL für Radio"
|
|
111
|
+
},
|
|
112
|
+
"spotifyUri": {
|
|
113
|
+
"title": "Spotify URI",
|
|
114
|
+
"type": "string",
|
|
115
|
+
"description": "Spotify URI (z.B. spotify:playlist:37i9dQZF1DXcBWIGoYBM5M)"
|
|
116
|
+
},
|
|
117
|
+
"contentId": {
|
|
118
|
+
"title": "Content ID",
|
|
119
|
+
"type": "string",
|
|
120
|
+
"description": "Content ID für TuneIn, Amazon oder Deezer"
|
|
121
|
+
},
|
|
122
|
+
"sourceAccount": {
|
|
123
|
+
"title": "Account",
|
|
124
|
+
"type": "string",
|
|
125
|
+
"description": "Account-Identifier für Spotify/Amazon/Deezer"
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
"layout": [
|
|
136
|
+
"name",
|
|
137
|
+
{
|
|
138
|
+
"type": "fieldset",
|
|
139
|
+
"title": "Automatische Erkennung",
|
|
140
|
+
"description": "Geräte werden beim Start automatisch im Netzwerk gefunden. Du kannst sie dann hier konfigurieren.",
|
|
141
|
+
"items": [
|
|
142
|
+
"autoDiscover",
|
|
143
|
+
"discoveryTimeout"
|
|
144
|
+
]
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
"type": "fieldset",
|
|
148
|
+
"title": "Geräte-Konfiguration",
|
|
149
|
+
"description": "Füge hier deine SoundTouch-Geräte hinzu (IP-Adresse). Pro Gerät kannst du das Icon und die Preset-Tasten individuell konfigurieren.",
|
|
150
|
+
"expandable": true,
|
|
151
|
+
"expanded": true,
|
|
152
|
+
"items": [
|
|
153
|
+
{
|
|
154
|
+
"key": "devices",
|
|
155
|
+
"type": "array",
|
|
156
|
+
"buttonText": "Gerät hinzufügen",
|
|
157
|
+
"items": [
|
|
158
|
+
{
|
|
159
|
+
"type": "div",
|
|
160
|
+
"displayFlex": true,
|
|
161
|
+
"items": [
|
|
162
|
+
{
|
|
163
|
+
"key": "devices[].host",
|
|
164
|
+
"flex": "1 1 150px"
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
"key": "devices[].name",
|
|
168
|
+
"flex": "1 1 150px"
|
|
169
|
+
}
|
|
170
|
+
]
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
"type": "div",
|
|
174
|
+
"displayFlex": true,
|
|
175
|
+
"items": [
|
|
176
|
+
{
|
|
177
|
+
"key": "devices[].room",
|
|
178
|
+
"flex": "1 1 150px"
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
"key": "devices[].deviceIcon",
|
|
182
|
+
"flex": "1 1 150px"
|
|
183
|
+
}
|
|
184
|
+
]
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
"type": "fieldset",
|
|
188
|
+
"title": "Preset-Tasten belegen",
|
|
189
|
+
"expandable": true,
|
|
190
|
+
"items": [
|
|
191
|
+
{
|
|
192
|
+
"key": "devices[].presets",
|
|
193
|
+
"type": "array",
|
|
194
|
+
"buttonText": "Preset hinzufügen",
|
|
195
|
+
"items": [
|
|
196
|
+
{
|
|
197
|
+
"type": "div",
|
|
198
|
+
"displayFlex": true,
|
|
199
|
+
"items": [
|
|
200
|
+
{
|
|
201
|
+
"key": "devices[].presets[].slot",
|
|
202
|
+
"flex": "0 0 80px"
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
"key": "devices[].presets[].name",
|
|
206
|
+
"flex": "1 1 150px"
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
"key": "devices[].presets[].type",
|
|
210
|
+
"flex": "1 1 150px"
|
|
211
|
+
}
|
|
212
|
+
]
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
"key": "devices[].presets[].url",
|
|
216
|
+
"condition": {
|
|
217
|
+
"functionBody": "try { return model.devices[arrayIndices[0]].presets[arrayIndices[1]].type === 'radio'; } catch(e) { return false; }"
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
"key": "devices[].presets[].spotifyUri",
|
|
222
|
+
"condition": {
|
|
223
|
+
"functionBody": "try { return model.devices[arrayIndices[0]].presets[arrayIndices[1]].type === 'spotify'; } catch(e) { return false; }"
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
"key": "devices[].presets[].contentId",
|
|
228
|
+
"condition": {
|
|
229
|
+
"functionBody": "try { return ['tunein', 'amazon', 'deezer'].includes(model.devices[arrayIndices[0]].presets[arrayIndices[1]].type); } catch(e) { return false; }"
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
"key": "devices[].presets[].sourceAccount",
|
|
234
|
+
"condition": {
|
|
235
|
+
"functionBody": "try { return ['spotify', 'amazon', 'deezer'].includes(model.devices[arrayIndices[0]].presets[arrayIndices[1]].type); } catch(e) { return false; }"
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
]
|
|
239
|
+
}
|
|
240
|
+
]
|
|
241
|
+
}
|
|
242
|
+
]
|
|
243
|
+
}
|
|
244
|
+
]
|
|
245
|
+
}
|
|
246
|
+
]
|
|
247
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface DiscoveredDevice {
|
|
2
|
+
name: string;
|
|
3
|
+
host: string;
|
|
4
|
+
port: number;
|
|
5
|
+
mac?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare class SoundTouchDiscovery {
|
|
8
|
+
private bonjour;
|
|
9
|
+
private browser;
|
|
10
|
+
private devices;
|
|
11
|
+
private onDeviceFound?;
|
|
12
|
+
private onDeviceLost?;
|
|
13
|
+
constructor();
|
|
14
|
+
start(onDeviceFound?: (device: DiscoveredDevice) => void, onDeviceLost?: (device: DiscoveredDevice) => void): void;
|
|
15
|
+
stop(): void;
|
|
16
|
+
private handleServiceUp;
|
|
17
|
+
private handleServiceDown;
|
|
18
|
+
getDevices(): DiscoveredDevice[];
|
|
19
|
+
discoverOnce(timeout?: number): Promise<DiscoveredDevice[]>;
|
|
20
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.SoundTouchDiscovery = void 0;
|
|
7
|
+
const bonjour_service_1 = __importDefault(require("bonjour-service"));
|
|
8
|
+
class SoundTouchDiscovery {
|
|
9
|
+
bonjour;
|
|
10
|
+
browser = null;
|
|
11
|
+
devices = new Map();
|
|
12
|
+
onDeviceFound;
|
|
13
|
+
onDeviceLost;
|
|
14
|
+
constructor() {
|
|
15
|
+
this.bonjour = new bonjour_service_1.default();
|
|
16
|
+
}
|
|
17
|
+
start(onDeviceFound, onDeviceLost) {
|
|
18
|
+
this.onDeviceFound = onDeviceFound;
|
|
19
|
+
this.onDeviceLost = onDeviceLost;
|
|
20
|
+
// SoundTouch devices advertise via mDNS as _soundtouch._tcp
|
|
21
|
+
this.browser = this.bonjour.find({ type: 'soundtouch' }, (service) => {
|
|
22
|
+
this.handleServiceUp(service);
|
|
23
|
+
});
|
|
24
|
+
this.browser.on('down', (service) => {
|
|
25
|
+
this.handleServiceDown(service);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
stop() {
|
|
29
|
+
if (this.browser) {
|
|
30
|
+
this.browser.stop();
|
|
31
|
+
this.browser = null;
|
|
32
|
+
}
|
|
33
|
+
this.bonjour.destroy();
|
|
34
|
+
}
|
|
35
|
+
handleServiceUp(service) {
|
|
36
|
+
if (!service.addresses || service.addresses.length === 0) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
// Prefer IPv4 address
|
|
40
|
+
const host = service.addresses.find((addr) => !addr.includes(':')) || service.addresses[0];
|
|
41
|
+
const device = {
|
|
42
|
+
name: service.name,
|
|
43
|
+
host: host,
|
|
44
|
+
port: service.port || 8090,
|
|
45
|
+
mac: service.txt?.['MAC'],
|
|
46
|
+
};
|
|
47
|
+
const key = `${device.host}:${device.port}`;
|
|
48
|
+
if (!this.devices.has(key)) {
|
|
49
|
+
this.devices.set(key, device);
|
|
50
|
+
this.onDeviceFound?.(device);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
handleServiceDown(service) {
|
|
54
|
+
const addresses = service.addresses || [];
|
|
55
|
+
for (const addr of addresses) {
|
|
56
|
+
const key = `${addr}:${service.port || 8090}`;
|
|
57
|
+
const device = this.devices.get(key);
|
|
58
|
+
if (device) {
|
|
59
|
+
this.devices.delete(key);
|
|
60
|
+
this.onDeviceLost?.(device);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
getDevices() {
|
|
65
|
+
return Array.from(this.devices.values());
|
|
66
|
+
}
|
|
67
|
+
async discoverOnce(timeout = 5000) {
|
|
68
|
+
return new Promise((resolve) => {
|
|
69
|
+
const devices = [];
|
|
70
|
+
const browser = this.bonjour.find({ type: 'soundtouch' }, (service) => {
|
|
71
|
+
if (!service.addresses || service.addresses.length === 0) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const host = service.addresses.find((addr) => !addr.includes(':')) || service.addresses[0];
|
|
75
|
+
const device = {
|
|
76
|
+
name: service.name,
|
|
77
|
+
host: host,
|
|
78
|
+
port: service.port || 8090,
|
|
79
|
+
mac: service.txt?.['MAC'],
|
|
80
|
+
};
|
|
81
|
+
const exists = devices.some((d) => d.host === device.host && d.port === device.port);
|
|
82
|
+
if (!exists) {
|
|
83
|
+
devices.push(device);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
setTimeout(() => {
|
|
87
|
+
browser.stop();
|
|
88
|
+
resolve(devices);
|
|
89
|
+
}, timeout);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
exports.SoundTouchDiscovery = SoundTouchDiscovery;
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { API, DynamicPlatformPlugin, Logger, PlatformAccessory, PlatformConfig, Service, Characteristic } from 'homebridge';
|
|
2
|
+
import { SoundTouchAccessory } from './soundtouchAccessory';
|
|
3
|
+
export interface PresetConfig {
|
|
4
|
+
slot: number;
|
|
5
|
+
name: string;
|
|
6
|
+
type: 'radio' | 'spotify' | 'amazon' | 'deezer' | 'tunein';
|
|
7
|
+
url?: string;
|
|
8
|
+
spotifyUri?: string;
|
|
9
|
+
contentId?: string;
|
|
10
|
+
sourceAccount?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface DeviceConfig {
|
|
13
|
+
name?: string;
|
|
14
|
+
host: string;
|
|
15
|
+
room?: string;
|
|
16
|
+
deviceIcon?: number;
|
|
17
|
+
presets?: PresetConfig[];
|
|
18
|
+
}
|
|
19
|
+
export interface SoundTouchPlatformConfig extends PlatformConfig {
|
|
20
|
+
devices?: DeviceConfig[];
|
|
21
|
+
autoDiscover?: boolean;
|
|
22
|
+
discoveryTimeout?: number;
|
|
23
|
+
}
|
|
24
|
+
export declare class SoundTouchPlatform implements DynamicPlatformPlugin {
|
|
25
|
+
readonly log: Logger;
|
|
26
|
+
readonly config: SoundTouchPlatformConfig;
|
|
27
|
+
readonly api: API;
|
|
28
|
+
readonly Service: typeof Service;
|
|
29
|
+
readonly Characteristic: typeof Characteristic;
|
|
30
|
+
readonly accessories: PlatformAccessory[];
|
|
31
|
+
private readonly soundTouchAccessories;
|
|
32
|
+
private readonly externalAccessories;
|
|
33
|
+
private discovery?;
|
|
34
|
+
constructor(log: Logger, config: SoundTouchPlatformConfig, api: API);
|
|
35
|
+
configureAccessory(accessory: PlatformAccessory): void;
|
|
36
|
+
private discoverDevices;
|
|
37
|
+
private registerDevice;
|
|
38
|
+
private cleanup;
|
|
39
|
+
getAllAccessories(): SoundTouchAccessory[];
|
|
40
|
+
getAccessoryByHost(host: string): SoundTouchAccessory | undefined;
|
|
41
|
+
getAccessoryByName(name: string): SoundTouchAccessory | undefined;
|
|
42
|
+
getDeviceConfig(host: string): DeviceConfig | undefined;
|
|
43
|
+
}
|