hb-zp-tools 0.0.1
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/LICENSE +201 -0
- package/README.md +29 -0
- package/cli/zp.js +15 -0
- package/index.js +9 -0
- package/jsdoc.json +21 -0
- package/lib/ZpClient.js +1736 -0
- package/lib/ZpListener.js +176 -0
- package/lib/ZpTool.js +1262 -0
- package/lib/ZpXmlParser.js +291 -0
- package/package.json +50 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// hb-zp-tools/lib/ZpListener.js
|
|
2
|
+
// Copyright © 2019-2024 Erik Baauw. All rights reserved.
|
|
3
|
+
//
|
|
4
|
+
// Homebridge ZP Tools.
|
|
5
|
+
|
|
6
|
+
import { EventEmitter, once } from 'node:events'
|
|
7
|
+
import { createServer } from 'node:http'
|
|
8
|
+
|
|
9
|
+
/** Listener for events from Sonos ZonePlayer.
|
|
10
|
+
* <br>See {@link ZpListener}.
|
|
11
|
+
* @name ZpListener
|
|
12
|
+
* @type {Class}
|
|
13
|
+
* @memberof module:hb-zp-tools
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** Listener class for receiving notifications by Sonos zone players.
|
|
17
|
+
*
|
|
18
|
+
* This class implements a web server to receive notifications from Sonos
|
|
19
|
+
* zone players.
|
|
20
|
+
* The web server can handle notifications from multiple zone players.
|
|
21
|
+
*
|
|
22
|
+
* Use {@link ZpListener#addClient addClient()} to register a zone player.
|
|
23
|
+
* When a notification for a registered zone player is recevied, a
|
|
24
|
+
* {@link ZpListener#event:notify notify} event
|
|
25
|
+
* is issued, using the zone player ID as event name.
|
|
26
|
+
*
|
|
27
|
+
* Use {@link ZpListener#removeClient removeClient()} to unregister a
|
|
28
|
+
* zone player.
|
|
29
|
+
* @extends EventEmitter
|
|
30
|
+
*/
|
|
31
|
+
class ZpListener extends EventEmitter {
|
|
32
|
+
/** Create a new listener instance.
|
|
33
|
+
* @param {integer} [port=0] - The port for the web server.
|
|
34
|
+
*/
|
|
35
|
+
constructor (port = 0) {
|
|
36
|
+
super()
|
|
37
|
+
this._myPort = port
|
|
38
|
+
this._clients = {}
|
|
39
|
+
this._server = createServer((request, response) => {
|
|
40
|
+
let buffer = ''
|
|
41
|
+
request.on('data', (data) => {
|
|
42
|
+
buffer += data
|
|
43
|
+
})
|
|
44
|
+
request.on('end', async () => {
|
|
45
|
+
try {
|
|
46
|
+
request.body = buffer
|
|
47
|
+
if (request.method === 'GET' && request.url === '/notify') {
|
|
48
|
+
// Provide an easy way to check that listener is reachable.
|
|
49
|
+
response.writeHead(200, { 'Content-Type': 'text/html' })
|
|
50
|
+
response.write('<table>')
|
|
51
|
+
response.write(`<caption><h3>Listening to ${Object.keys(this._clients).length} clients</h3></caption>`)
|
|
52
|
+
response.write('<tr><th scope="col">ZonePlayer</th>')
|
|
53
|
+
// response.write('<th scope="col">ID</th>')
|
|
54
|
+
response.write('<th scope="col">IP Address</th>')
|
|
55
|
+
response.write('<th scope="col">Local IP Address</th>')
|
|
56
|
+
response.write('<th scope="col">Subscriptions</th></tr>')
|
|
57
|
+
const names = {}
|
|
58
|
+
for (const id of Object.keys(this._clients)) {
|
|
59
|
+
const zpClient = this._clients[id]
|
|
60
|
+
const name = zpClient.name == null ? zpClient.id : zpClient.name
|
|
61
|
+
names[name] = zpClient
|
|
62
|
+
}
|
|
63
|
+
for (const name of Object.keys(names).sort()) {
|
|
64
|
+
const zpClient = names[name]
|
|
65
|
+
response.write(`<tr><td>${name}</td>`)
|
|
66
|
+
// response.write(`<td>${zpClient.id}</td>`)
|
|
67
|
+
response.write(`<td>${zpClient.address}</td>`)
|
|
68
|
+
response.write(`<td>${zpClient.localAddress}</td>`)
|
|
69
|
+
const subs = zpClient.subscriptions.map((sub) => {
|
|
70
|
+
return sub.slice(0, -6)
|
|
71
|
+
}).join(', ')
|
|
72
|
+
response.write(`<td>${subs}</td></tr>`)
|
|
73
|
+
}
|
|
74
|
+
response.write('</table>')
|
|
75
|
+
} else if (request.method === 'NOTIFY') {
|
|
76
|
+
const array = request.url.split('/')
|
|
77
|
+
if (array.length === 5) {
|
|
78
|
+
array.splice(3, 0, 'ZonePlayer')
|
|
79
|
+
}
|
|
80
|
+
if (
|
|
81
|
+
array[1] === 'notify' && this._clients[array[2]] !== null &&
|
|
82
|
+
array[3] != null && array[4] != null && array[5] === 'Event'
|
|
83
|
+
) {
|
|
84
|
+
/** Emitted when receiving a notification from a registered
|
|
85
|
+
* zone player.
|
|
86
|
+
*
|
|
87
|
+
* Note: the actual event name is the ID of the zone player.
|
|
88
|
+
* @event ZpListener#notify
|
|
89
|
+
* @param {object} params - The notification paramaters.
|
|
90
|
+
* @param {string} params.device - The device that issued the
|
|
91
|
+
* notification or `ZonePlayer` for the default device.
|
|
92
|
+
* @param {string} params.service - The service that issued the
|
|
93
|
+
* notification.
|
|
94
|
+
* @param {string} params.body - The body of the notification
|
|
95
|
+
* (in XML).
|
|
96
|
+
*/
|
|
97
|
+
this.emit(array[2], {
|
|
98
|
+
device: array[3],
|
|
99
|
+
service: array[4],
|
|
100
|
+
body: request.body
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
response.end()
|
|
105
|
+
} catch (error) {
|
|
106
|
+
/** Emitted when the web server encounters an error.
|
|
107
|
+
* @event ZpListener#error
|
|
108
|
+
* @param {Error} error - The error.
|
|
109
|
+
*/
|
|
110
|
+
this.emit('error', error)
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
this._server
|
|
115
|
+
.on('error', (error) => { this.emit('error', error) })
|
|
116
|
+
.on('close', () => {
|
|
117
|
+
/** Emitted when the web server is closed.
|
|
118
|
+
* @event ZpListener#close
|
|
119
|
+
* @param {string} url - The url the web server was listening on.
|
|
120
|
+
*/
|
|
121
|
+
this.emit('close', this._callbackUrl)
|
|
122
|
+
delete this._callbackUrl
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Start the web server.
|
|
127
|
+
async _listen () {
|
|
128
|
+
if (this._server.listening) {
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
this._server.listen(this._myPort, '0.0.0.0')
|
|
132
|
+
await once(this._server, 'listening')
|
|
133
|
+
const address = this._server.address()
|
|
134
|
+
this._myIp = address.address
|
|
135
|
+
this._myPort = address.port
|
|
136
|
+
this._callbackUrl = 'http://' + this._myIp + ':' + this._myPort + '/notify'
|
|
137
|
+
/** Emitted when the web server has started.
|
|
138
|
+
* @event ZpListener#listening
|
|
139
|
+
* @param {string} url - The url the web server is listening on.
|
|
140
|
+
*/
|
|
141
|
+
this.emit('listening', this._callbackUrl)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Registers a zone player for notifications.
|
|
145
|
+
*
|
|
146
|
+
* Starts the web server if not already started.
|
|
147
|
+
* @param {ZpClient} zpClient - The {@link ZpClient} instance for the zone
|
|
148
|
+
* player.
|
|
149
|
+
* @return {string} callbackUrl - The callback url to pass to the zone
|
|
150
|
+
* player when subscribing to notifications.
|
|
151
|
+
* See {@link ZpClient#subscribe subscribe()}.
|
|
152
|
+
*/
|
|
153
|
+
async addClient (zpClient) {
|
|
154
|
+
this._clients[zpClient.id] = zpClient
|
|
155
|
+
await this._listen()
|
|
156
|
+
const callbackUrl = 'http://' + zpClient.localAddress + ':' + this._myPort +
|
|
157
|
+
'/notify/' + zpClient.id
|
|
158
|
+
return callbackUrl
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Deregisters a zone player for notifications.
|
|
162
|
+
*
|
|
163
|
+
* Stops the web server when no more clients remain.
|
|
164
|
+
* @param {ZpClient} zpClient - The {@link ZpClient} instance for the zone
|
|
165
|
+
* player.
|
|
166
|
+
*/
|
|
167
|
+
async removeClient (zpClient) {
|
|
168
|
+
this.removeAllListeners(zpClient.id)
|
|
169
|
+
delete this._clients[zpClient.id] // FIXME: this doesn't work?!
|
|
170
|
+
if (Object.keys(this._clients).length === 0) {
|
|
171
|
+
await this._server.close()
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export { ZpListener }
|