homebridge-deconz 0.1.17 → 0.1.18

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.
@@ -1,238 +0,0 @@
1
- // homebridge-hue/lib/Deconz/Discovery.js
2
- //
3
- // Homebridge plug-in for Philips Hue and/or deCONZ.
4
- // Copyright © 2018-2023 Erik Baauw. All rights reserved.
5
-
6
- 'use strict'
7
-
8
- const events = require('events')
9
- const { HttpClient, OptionParser, UpnpClient } = require('homebridge-lib')
10
- const xml2js = require('xml2js')
11
-
12
- /** Class for discovery of deCONZ gateways.
13
- *
14
- * See the [deCONZ API](https://dresden-elektronik.github.io/deconz-rest-doc/)
15
- * documentation for a better understanding of the API.
16
- * @extends EventEmitter
17
- * @memberof Deconz
18
- */
19
- class Discovery extends events.EventEmitter {
20
- /** Create a new instance.
21
- * @param {object} params - Parameters.
22
- * @param {boolean} [params.forceHttp=false] - Use plain HTTP instead of HTTPS.
23
- * @param {integer} [params.timeout=5] - Timeout (in seconds) for requests.
24
- */
25
- constructor (params = {}) {
26
- super()
27
- this._options = {
28
- forceHttp: false,
29
- timeout: 5
30
- }
31
- const optionParser = new OptionParser(this._options)
32
- optionParser.boolKey('forceHttp')
33
- optionParser.intKey('timeout', 1, 60)
34
- optionParser.parse(params)
35
- }
36
-
37
- /** Issue an unauthenticated GET request of `/api/config` to given host.
38
- *
39
- * @param {string} host - The IP address or hostname and port of the deCONZ
40
- * gateway.
41
- * @return {object|null} response - The JSON response body converted to
42
- * JavaScript, or null when the response doesn't come from deCONZ.
43
- * @throws {HttpError} In case of error.
44
- */
45
- async config (host) {
46
- const client = new HttpClient({
47
- host,
48
- json: true,
49
- path: '/api',
50
- timeout: this._options.timeout
51
- })
52
- client
53
- .on('error', (error) => {
54
- /** Emitted when an error has occured.
55
- *
56
- * @event DeconzDiscovery#error
57
- * @param {HttpError} error - The error.
58
- */
59
- this.emit('error', error)
60
- })
61
- .on('request', (request) => {
62
- /** Emitted when request has been sent.
63
- *
64
- * @event DeconzDiscovery#request
65
- * @param {HttpRequest} request - The request.
66
- */
67
- this.emit('request', request)
68
- })
69
- .on('response', (response) => {
70
- /** Emitted when a valid response has been received.
71
- *
72
- * @event DeconzDiscovery#response
73
- * @param {HttpResponse} response - The response.
74
- */
75
- this.emit('response', response)
76
- })
77
- const { body } = await client.get('/config')
78
- if (
79
- body != null && typeof body === 'object' &&
80
- typeof body.apiversion === 'string' &&
81
- /[0-9A-Fa-f]{16}/.test(body.bridgeid) &&
82
- typeof body.devicename === 'string' &&
83
- typeof body.name === 'string' &&
84
- typeof body.swversion === 'string'
85
- ) {
86
- if (body.bridgeid.startsWith('00212E')) {
87
- return body
88
- }
89
- throw new Error(`${body.bridgeid}: not a RaspBee/ConBee mac address`)
90
- }
91
- throw new Error('not a deCONZ gateway')
92
- }
93
-
94
- /** Issue an unauthenticated GET request of `/description.xml` to given host.
95
- *
96
- * @param {string} host - The IP address or hostname and port of the deCONZ gateway.
97
- * @return {object} response - The description, converted to JavaScript.
98
- * @throws {Error} In case of error.
99
- */
100
- async description (host) {
101
- const options = {
102
- host,
103
- timeout: this._options.timeout
104
- }
105
- const client = new HttpClient(options)
106
- client
107
- .on('error', (error) => { this.emit('error', error) })
108
- .on('request', (request) => { this.emit('request', request) })
109
- .on('response', (response) => { this.emit('response', response) })
110
- const { body } = await client.get('/description.xml')
111
- const xmlOptions = { explicitArray: false }
112
- const result = await xml2js.parseStringPromise(body, xmlOptions)
113
- return result
114
- }
115
-
116
- /** Discover deCONZ gateways.
117
- *
118
- * Queries the Phoscon portal for known gateways and does a local search
119
- * over UPnP.
120
- * Calls {@link DeconzDiscovery#config config()} for each discovered gateway
121
- * for verification.
122
- * @param {boolean} [stealth=false] - Don't query discovery portals.
123
- * @return {object} response - Response object with a key/value pair per
124
- * found gateway. The key is the host (IP address or hostname and port),
125
- * the value is the return value of {@link DeconzDiscovery#config config()}.
126
- */
127
- async discover (stealth = false) {
128
- this.gatewayMap = {}
129
- this.jobs = []
130
- this.jobs.push(this._upnp())
131
- if (!stealth) {
132
- this.jobs.push(this._nupnp({
133
- name: 'phoscon.de',
134
- https: !this._options.forceHttp,
135
- host: 'phoscon.de',
136
- path: '/discover'
137
- }))
138
- }
139
- for (const job of this.jobs) {
140
- await job
141
- }
142
- return this.gatewayMap
143
- }
144
-
145
- _found (name, id, host) {
146
- /** Emitted when a potential gateway has been found.
147
- * @event DeconzDiscovery#found
148
- * @param {string} name - The name of the search method.
149
- * @param {string} bridgeid - The ID of the gateway.
150
- * @param {string} host - The IP address/hostname and port of the gateway
151
- * or gateway.
152
- */
153
- this.emit('found', name, id, host)
154
- if (this.gatewayMap[host] == null) {
155
- this.gatewayMap[host] = id
156
- this.jobs.push(
157
- this.config(host).then((config) => {
158
- this.gatewayMap[host] = config
159
- }).catch((error) => {
160
- delete this.gatewayMap[host]
161
- if (error.request == null) {
162
- this.emit('error', error)
163
- }
164
- })
165
- )
166
- }
167
- }
168
-
169
- async _upnp () {
170
- const upnpClient = new UpnpClient({
171
- filter: (message) => {
172
- return /^[0-9A-F]{16}$/.test(message['gwid.phoscon.de'])
173
- },
174
- timeout: this._options.timeout
175
- })
176
- upnpClient
177
- .on('error', (error) => { this.emit('error', error) })
178
- .on('searching', (host) => {
179
- /** Emitted when UPnP search has started.
180
- *
181
- * @event DeconzDiscovery#searching
182
- * @param {string} host - The IP address and port from which the
183
- * search was started.
184
- */
185
- this.emit('searching', host)
186
- })
187
- .on('request', (request) => {
188
- request.name = 'upnp'
189
- this.emit('request', request)
190
- })
191
- .on('deviceFound', (address, obj, message) => {
192
- let host
193
- const a = obj.location.split('/')
194
- if (a.length > 3 && a[2] != null) {
195
- host = a[2]
196
- const b = host.split(':')
197
- const port = parseInt(b[1])
198
- if (port === 80) {
199
- host = b[0]
200
- }
201
- this._found('upnp', obj['gwid.phoscon.de'], host)
202
- }
203
- })
204
- upnpClient.search()
205
- await events.once(upnpClient, 'searchDone')
206
- /** Emitted when UPnP search has concluded.
207
- *
208
- * @event DeconzDiscovery#searchDone
209
- */
210
- this.emit('searchDone')
211
- }
212
-
213
- async _nupnp (options) {
214
- options.json = true
215
- options.timeout = this._options.timeout
216
- const client = new HttpClient(options)
217
- client
218
- .on('error', (error) => { this.emit('error', error) })
219
- .on('request', (request) => { this.emit('request', request) })
220
- .on('response', (response) => { this.emit('response', response) })
221
- try {
222
- const { body } = await client.get()
223
- if (Array.isArray(body)) {
224
- for (const gateway of body) {
225
- let host = gateway.internalipaddress
226
- if (gateway.internalport != null && gateway.internalport !== 80) {
227
- host += ':' + gateway.internalport
228
- }
229
- this._found(options.name, gateway.id.toUpperCase(), host)
230
- }
231
- }
232
- } catch (error) {
233
- this.emit('error', error)
234
- }
235
- }
236
- }
237
-
238
- module.exports = Discovery
@@ -1,205 +0,0 @@
1
- // homebridge-deconz/lib/Deconz/WsClient.js
2
- //
3
- // Homebridge plug-in for deCONZ.
4
- // Copyright © 2018-2023 Erik Baauw. All rights reserved.
5
-
6
- 'use strict'
7
-
8
- const events = require('events')
9
- const { OptionParser, timeout } = require('homebridge-lib')
10
- const WebSocket = require('ws')
11
-
12
- /** Client for web socket notifications by a deCONZ gateway.
13
- *
14
- * See the
15
- * [deCONZ](https://dresden-elektronik.github.io/deconz-rest-doc/endpoints/websocket/)
16
- * documentation for a better understanding of the web socket notifications.
17
- * @memberof Deconz
18
- */
19
- class WsClient extends events.EventEmitter {
20
- /** Instantiate a new web socket client.
21
- * @param {object} params - Parameters.
22
- * @param {string} [params.host='localhost:443'] - IP address or hostname
23
- * and port of the web socket server.
24
- * @param {integer} [params.retryTime=10] - Time (in seconds) to try and
25
- * reconnect when the server connection has been closed.
26
- * @param {boolean} [params.raw=false] - Issue raw events instead of parsing
27
- * them.<br>
28
- * When specified, {@link DeconzWsClient#event:notification notification}
29
- * events are emitted, in lieu of {@link DeconzWsClient#event:changed changed},
30
- * {@link DeconzWsClient#event:added added}, and
31
- * {@link DeconzWsClient#event:sceneRecall sceneRecall} events.
32
- */
33
- constructor (params = {}) {
34
- super()
35
- this.config = {
36
- hostname: 'localhost',
37
- port: 443,
38
- retryTime: 15
39
- }
40
- const optionParser = new OptionParser(this.config)
41
- optionParser
42
- .hostKey()
43
- .intKey('retryTime', 0, 120)
44
- .boolKey('raw')
45
- .parse(params)
46
- }
47
-
48
- /** The hostname or IP address and port of the web socket server.
49
- * @type {string}
50
- */
51
- get host () { return this.config.hostname + ':' + this.config.port }
52
- set host (host) {
53
- const { hostname, port } = OptionParser.toHost('host', host)
54
- this.config.hostname = hostname
55
- this.config.port = port
56
- }
57
-
58
- /** Connect to the web socket server, and listen notifications.
59
- */
60
- listen () {
61
- this.reconnect = true
62
- const url = 'ws://' + this.config.hostname + ':' + this.config.port
63
- this.ws = new WebSocket(url, { family: 4 })
64
-
65
- this.ws
66
- .on('error', (error) => {
67
- /** Emitted on error.
68
- * @event DeconzWsClient#error
69
- * @param {Error} error - The error.
70
- */
71
- this.emit('error', error)
72
- })
73
- .on('open', () => {
74
- /** Emitted when connection to web socket server has been made.
75
- * @event DeconzWsClient#listening
76
- * @param {string} url - The URL of the web socket server.
77
- */
78
- this.emit('listening', url)
79
- })
80
- .on('message', (data, flags) => {
81
- try {
82
- const obj = JSON.parse(data.toString())
83
- if (!this.config.raw) {
84
- if (obj.r === 'groups' && obj.id === 0xFFF0.toString()) {
85
- // Workaround for deCONZ bug: events for `/groups/0` contain
86
- // the Zigbee group ID, instead of the resource ID.
87
- obj.id = '0'
88
- }
89
- if (obj.t === 'event') {
90
- switch (obj.e) {
91
- case 'changed':
92
- if (obj.r !== null && obj.id !== null) {
93
- let body
94
- if (obj.attr != null) {
95
- body = obj.attr
96
- } else if (obj.capabilities != null) {
97
- body = { capabilities: obj.capabilities }
98
- } else if (obj.config != null) {
99
- body = { config: obj.config }
100
- } else if (obj.state != null) {
101
- body = { state: obj.state }
102
- }
103
- /** Emitted when a `changed` notification has been received.
104
- *
105
- * Note that the deCONZ gateway sends different
106
- * notifications for top-level, `state`, and `config`
107
- * attributes.
108
- * Consequenly, the `body` only contains one of these.
109
- * @event DeconzWsClient#changed
110
- * @param {string} rtype - The resource type of the changed
111
- * resource.
112
- * @param {integer} rid - The resource ID of the changed
113
- * resource.
114
- * @param {object} body - The body of the changed resource.
115
- */
116
- this.emit('changed', obj.r, obj.id, body)
117
- return
118
- }
119
- break
120
- case 'added':
121
- if (obj.r !== null && obj.id !== null) {
122
- /** Emitted when an `added` notification has been received.
123
- * @event DeconzWsClient#added
124
- * @param {string} rtype - The resource type of the added
125
- * resource.
126
- * @param {integer} rid - The resource ID of the added
127
- * resource.
128
- * @param {object} body - The body of the added resource.
129
- */
130
- this.emit('added', obj.r, obj.id, obj[obj.r.slice(0, -1)])
131
- return
132
- }
133
- break
134
- case 'deleted':
135
- if (obj.r !== null && obj.id !== null) {
136
- /** Emitted when an `deleted` notification has been received.
137
- * @event DeconzWsClient#deleted
138
- * @param {string} rtype - The resource type of the deleted
139
- * resource.
140
- * @param {integer} rid - The resource ID of the deleted
141
- * resource.
142
- */
143
- this.emit('deleted', obj.r, obj.id)
144
- return
145
- }
146
- break
147
- case 'scene-called':
148
- if (obj.gid != null && obj.scid != null) {
149
- const resource = '/groups/' + obj.gid + '/scenes/' + obj.scid
150
- /** Emitted when an `sceneRecall` notification has been received.
151
- * @event DeconzWsClient#sceneRecall
152
- * @param {string} resource - The scene resource.
153
- */
154
- this.emit('sceneRecall', resource)
155
- return
156
- }
157
- break
158
- default:
159
- break
160
- }
161
- }
162
- }
163
- /** Emitted when an unknown notification has been received, or when
164
- * `params.raw` was specified to the
165
- * {@link DeconzWsClient constructor}.
166
- * @event DeconzWsClient#notification
167
- * @param {object} notification - The raw notification.
168
- */
169
- this.emit('notification', obj)
170
- } catch (error) {
171
- this.emit('error', error)
172
- }
173
- })
174
- .on('close', async () => {
175
- if (this.ws != null) {
176
- this.ws.removeAllListeners()
177
- delete this.ws
178
- }
179
- const retryTime = this.reconnect ? this.config.retryTime : 0
180
- /** Emitted when the connection to the web socket server has been closed.
181
- * @event DeconzWsClient#closed
182
- * @param {string} url - The URL of the web socket server.
183
- * @param {?int} retryTime - Time in seconds after which connection
184
- * will be retied automatically.
185
- */
186
- this.emit('closed', url, retryTime)
187
- if (retryTime > 0) {
188
- await timeout(retryTime * 1000)
189
- return this.listen()
190
- }
191
- })
192
- }
193
-
194
- /** Close the web socket connection.
195
- */
196
- async close () {
197
- if (this.ws != null) {
198
- this.reconnect = false
199
- this.ws.close()
200
- await events.once(this.ws, 'close')
201
- }
202
- }
203
- }
204
-
205
- module.exports = WsClient