homebridge-deconz 0.1.16 → 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.
- package/cli/deconz.js +4 -975
- package/homebridge-ui/server.js +2 -2
- package/lib/Deconz/Resource.js +67 -14
- package/lib/Deconz/index.js +0 -5
- package/lib/DeconzAccessory/Gateway.js +44 -7
- package/lib/DeconzAccessory/index.js +3 -3
- package/lib/DeconzPlatform.js +2 -2
- package/lib/DeconzService/AirQuality.js +2 -2
- package/lib/DeconzService/Battery.js +2 -2
- package/lib/DeconzService/Consumption.js +2 -2
- package/lib/DeconzService/Daylight.js +2 -2
- package/lib/DeconzService/Gateway.js +10 -0
- package/lib/DeconzService/LightLevel.js +2 -2
- package/lib/DeconzService/LightsResource.js +2 -2
- package/lib/DeconzService/Power.js +2 -2
- package/lib/DeconzService/Schedule.js +69 -0
- package/lib/DeconzService/SensorsResource.js +2 -2
- package/lib/DeconzService/index.js +3 -2
- package/package.json +2 -1
- package/lib/Deconz/ApiClient.js +0 -442
- package/lib/Deconz/ApiError.js +0 -49
- package/lib/Deconz/ApiResponse.js +0 -57
- package/lib/Deconz/Discovery.js +0 -238
- package/lib/Deconz/WsClient.js +0 -205
package/lib/Deconz/Discovery.js
DELETED
@@ -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
|
package/lib/Deconz/WsClient.js
DELETED
@@ -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
|