homebridge-deconz 0.0.6 → 0.0.11
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/README.md +8 -0
- package/cli/deconz.js +980 -0
- package/config.schema.json +1 -1
- package/lib/Client/ApiError.js +42 -0
- package/lib/{DeconzClient.js → Deconz/ApiClient.js} +82 -158
- package/lib/Deconz/ApiError.js +42 -0
- package/lib/Deconz/ApiResponse.js +54 -0
- package/lib/Deconz/Device.js +100 -0
- package/lib/{DeconzDiscovery.js → Deconz/Discovery.js} +4 -3
- package/lib/Deconz/Resource.js +1206 -0
- package/lib/{DeconzWsClient.js → Deconz/WsClient.js} +59 -44
- package/lib/Deconz/index.js +21 -0
- package/lib/DeconzAccessory/Contact.js +54 -0
- package/lib/DeconzAccessory/Gateway.js +316 -374
- package/lib/DeconzAccessory/Light.js +72 -0
- package/lib/DeconzAccessory/Motion.js +51 -0
- package/lib/DeconzAccessory/Sensor.js +35 -0
- package/lib/DeconzAccessory/Temperature.js +63 -0
- package/lib/DeconzAccessory/Thermostat.js +50 -0
- package/lib/DeconzAccessory/WarningDevice.js +56 -0
- package/lib/DeconzAccessory/WindowCovering.js +47 -0
- package/lib/DeconzAccessory/index.js +216 -0
- package/lib/DeconzPlatform.js +8 -3
- package/lib/DeconzService/AirPressure.js +43 -0
- package/lib/DeconzService/AirQuality.js +20 -10
- package/lib/DeconzService/Alarm.js +16 -9
- package/lib/DeconzService/Battery.js +43 -0
- package/lib/DeconzService/Button.js +12 -2
- package/lib/DeconzService/CarbonMonoxide.js +38 -0
- package/lib/DeconzService/Consumption.js +65 -0
- package/lib/DeconzService/Contact.js +60 -0
- package/lib/DeconzService/Daylight.js +132 -0
- package/lib/DeconzService/DeviceSettings.js +13 -5
- package/lib/DeconzService/Flag.js +52 -0
- package/lib/DeconzService/GatewaySettings.js +8 -58
- package/lib/DeconzService/Humidity.js +37 -0
- package/lib/DeconzService/Leak.js +38 -0
- package/lib/DeconzService/Light.js +376 -0
- package/lib/DeconzService/LightLevel.js +54 -0
- package/lib/DeconzService/LightsResource.js +112 -0
- package/lib/DeconzService/Motion.js +101 -0
- package/lib/DeconzService/Outlet.js +76 -0
- package/lib/DeconzService/Power.js +83 -0
- package/lib/DeconzService/SensorsResource.js +96 -0
- package/lib/DeconzService/Smoke.js +38 -0
- package/lib/DeconzService/Status.js +53 -0
- package/lib/DeconzService/Switch.js +93 -0
- package/lib/DeconzService/Temperature.js +63 -0
- package/lib/DeconzService/Thermostat.js +175 -0
- package/lib/DeconzService/WarningDevice.js +68 -0
- package/lib/DeconzService/WindowCovering.js +139 -0
- package/lib/DeconzService/index.js +94 -0
- package/package.json +7 -4
- package/lib/DeconzAccessory/Device.js +0 -91
- package/lib/DeconzAccessory.js +0 -16
- package/lib/DeconzDevice.js +0 -245
- package/lib/DeconzService/Sensor.js +0 -58
- package/lib/DeconzService.js +0 -43
package/cli/deconz.js
ADDED
@@ -0,0 +1,980 @@
|
|
1
|
+
#!/usr/bin/env node
|
2
|
+
|
3
|
+
// homebridge-deconz/cli/deconz.js
|
4
|
+
// Copyright © 2018-2022 Erik Baauw. All rights reserved.
|
5
|
+
//
|
6
|
+
// Command line interface to deCONZ gateway.
|
7
|
+
|
8
|
+
'use strict'
|
9
|
+
|
10
|
+
const fs = require('fs')
|
11
|
+
const Deconz = require('../lib/Deconz')
|
12
|
+
const homebridgeLib = require('homebridge-lib')
|
13
|
+
const packageJson = require('../package.json')
|
14
|
+
|
15
|
+
const { b, u } = homebridgeLib.CommandLineTool
|
16
|
+
const { UsageError } = homebridgeLib.CommandLineParser
|
17
|
+
|
18
|
+
const usage = {
|
19
|
+
deconz: `${b('deconz')} [${b('-hVDp')}] [${b('-H')} ${u('hostname')}[${b(':')}${u('port')}]] [${b('-K')} ${u('api key')}] [${b('-t')} ${u('timeout')}] ${u('command')} [${u('argument')} ...]`,
|
20
|
+
|
21
|
+
get: `${b('get')} [${b('-hsnjuatlkv')}] [${u('path')}]`,
|
22
|
+
put: `${b('put')} [${b('-hv')}] ${u('resource')} [${u('body')}]`,
|
23
|
+
post: `${b('post')} [${b('-hv')}] ${u('resource')} [${u('body')}]`,
|
24
|
+
delete: `${b('delete')} [${b('-hv')}] ${u('resource')} [${u('body')}]`,
|
25
|
+
|
26
|
+
eventlog: `${b('eventlog')} [${b('-hnrs')}]`,
|
27
|
+
|
28
|
+
discover: `${b('discover')} [${b('-hS')}]`,
|
29
|
+
config: `${b('config')} [${b('-hs')}]`,
|
30
|
+
description: `${b('description')} [${b('-hs')}]`,
|
31
|
+
getApiKey: `${b('getApiKey')} [${b('-hv')}]`,
|
32
|
+
unlock: `${b('unlock')} [${b('-hv')}]`,
|
33
|
+
search: `${b('search')} [${b('-hv')}]`,
|
34
|
+
|
35
|
+
probe: `${b('probe')} [${b('-hv')}] [${b('-t')} ${u('timeout')}] ${u('light')}`,
|
36
|
+
restart: `${b('restart')} [${b('-hv')}]`
|
37
|
+
}
|
38
|
+
const description = {
|
39
|
+
deconz: 'Command line interface to deCONZ gateway.',
|
40
|
+
|
41
|
+
get: `Retrieve ${u('path')} from gateway.`,
|
42
|
+
put: `Update ${u('resource')} on gateway with ${u('body')}.`,
|
43
|
+
post: `Create ${u('resource')} on gateway with ${u('body')}.`,
|
44
|
+
delete: `Delete ${u('resource')} from gateway with ${u('body')}.`,
|
45
|
+
|
46
|
+
eventlog: 'Log web socket notifications by the gateway.',
|
47
|
+
|
48
|
+
discover: 'Discover gateways.',
|
49
|
+
config: 'Retrieve gateway configuration (unauthenticated).',
|
50
|
+
description: 'Retrieve gateway description.',
|
51
|
+
|
52
|
+
getApiKey: 'Obtain an API key for the gateway.',
|
53
|
+
unlock: 'Unlock the gateway so new clients can obtain an API key.',
|
54
|
+
search: 'Initiate a seach for new devices.',
|
55
|
+
|
56
|
+
probe: `Probe ${u('light')} for supported colour (temperature) range.`,
|
57
|
+
restart: 'Restart the gateway.'
|
58
|
+
}
|
59
|
+
const help = {
|
60
|
+
deconz: `${description.deconz}
|
61
|
+
|
62
|
+
Usage: ${usage.deconz}
|
63
|
+
|
64
|
+
Parameters:
|
65
|
+
${b('-h')}, ${b('--help')}
|
66
|
+
Print this help and exit.
|
67
|
+
|
68
|
+
${b('-V')}, ${b('--version')}
|
69
|
+
Print version and exit.
|
70
|
+
|
71
|
+
${b('-D')}, ${b('--debug')}
|
72
|
+
Print debug messages for communication with the gateway.
|
73
|
+
|
74
|
+
${b('-p')}, ${b('--phoscon')}
|
75
|
+
Imitate the Phoscon app.
|
76
|
+
|
77
|
+
${b('-H')} ${u('hostname')}[${b(':')}${u('port')}], ${b('--host=')}${u('hostname')}[${b(':')}${u('port')}]
|
78
|
+
Connect to ${u('hostname')}${b(':80')} or ${u('hostname')}${b(':')}${u('port')} instead of the default ${b('localhost:80')}.
|
79
|
+
The hostname and port can also be specified by setting ${b('DECONZ_HOST')}.
|
80
|
+
|
81
|
+
${b('-K')} ${u('API key')}, ${b('--apiKey=')}${u('API key')}
|
82
|
+
Use ${u('API key')} instead of the API key saved in ${b('~/.deconz')}.
|
83
|
+
The API key can also be specified by setting ${b('DECONZ_API_KEY')}.
|
84
|
+
|
85
|
+
${b('-t')} ${u('timeout')}, ${b('--timeout=')}${u('timeout')}
|
86
|
+
Set timeout to ${u('timeout')} seconds instead of default ${b(5)}.
|
87
|
+
|
88
|
+
Commands:
|
89
|
+
${usage.get}
|
90
|
+
${description.get}
|
91
|
+
|
92
|
+
${usage.put}
|
93
|
+
${description.put}
|
94
|
+
|
95
|
+
${usage.post}
|
96
|
+
${description.post}
|
97
|
+
|
98
|
+
${usage.delete}
|
99
|
+
${description.delete}
|
100
|
+
|
101
|
+
${usage.eventlog}
|
102
|
+
${description.eventlog}
|
103
|
+
|
104
|
+
${usage.discover}
|
105
|
+
${description.discover}
|
106
|
+
|
107
|
+
${usage.config}
|
108
|
+
${description.config}
|
109
|
+
|
110
|
+
${usage.description}
|
111
|
+
${description.description}
|
112
|
+
|
113
|
+
${usage.getApiKey}
|
114
|
+
${description.getApiKey}
|
115
|
+
|
116
|
+
${usage.unlock}
|
117
|
+
${description.unlock}
|
118
|
+
|
119
|
+
${usage.search}
|
120
|
+
${description.search}
|
121
|
+
|
122
|
+
${usage.probe}
|
123
|
+
${description.probe}
|
124
|
+
|
125
|
+
${usage.restart}
|
126
|
+
${description.restart}
|
127
|
+
|
128
|
+
For more help, issue: ${b('deconz')} ${u('command')} ${b('-h')}`,
|
129
|
+
get: `${description.deconz}
|
130
|
+
|
131
|
+
Usage: ${b('deconz')} ${usage.get}
|
132
|
+
|
133
|
+
${description.get}
|
134
|
+
|
135
|
+
Parameters:
|
136
|
+
${b('-h')}, ${b('--help')}
|
137
|
+
Print this help and exit.
|
138
|
+
|
139
|
+
${b('-s')}, ${b('--sortKeys')}
|
140
|
+
Sort object key/value pairs alphabetically on key.
|
141
|
+
|
142
|
+
${b('-n')}, ${b('-noWhiteSpace')}
|
143
|
+
Do not include spaces nor newlines in the output.
|
144
|
+
|
145
|
+
${b('-j')}, ${b('--jsonArray')}
|
146
|
+
Output a JSON array of objects for each key/value pair.
|
147
|
+
Each object contains two key/value pairs: key "keys" with an array
|
148
|
+
of keys as value and key "value" with the value as value.
|
149
|
+
|
150
|
+
${b('-u')}, ${b('--joinKeys')}
|
151
|
+
Output JSON array of objects for each key/value pair.
|
152
|
+
Each object contains one key/value pair: the path (concatenated
|
153
|
+
keys separated by '/') as key and the value as value.
|
154
|
+
|
155
|
+
${b('-a')}, ${b('--ascii')}
|
156
|
+
Output path:value in plain text instead of JSON.
|
157
|
+
|
158
|
+
${b('-t')}, ${b('--topOnly')}
|
159
|
+
Limit output to top-level key/values.
|
160
|
+
|
161
|
+
${b('-l')}, ${b('--leavesOnly')}
|
162
|
+
Limit output to leaf (non-array, non-object) key/values.
|
163
|
+
|
164
|
+
${b('-k')}, ${b('--keysOnly')}
|
165
|
+
Limit output to keys. With ${b('-u')}, output a JSON array of paths.
|
166
|
+
|
167
|
+
${b('-v')}, ${b('--valuesOnly')}
|
168
|
+
Limit output to values. With ${b('-u')}, output a JSON array of values.
|
169
|
+
|
170
|
+
${u('path')}
|
171
|
+
Path to retrieve from the gateway.`,
|
172
|
+
put: `${description.deconz}
|
173
|
+
|
174
|
+
Usage: ${b('deconz')} ${usage.put}
|
175
|
+
|
176
|
+
${description.put}
|
177
|
+
|
178
|
+
Parameters:
|
179
|
+
${b('-h')}, ${b('--help')}
|
180
|
+
Print this help and exit.
|
181
|
+
|
182
|
+
${b('-v')}, ${b('--verbose')}
|
183
|
+
Print full API output.
|
184
|
+
|
185
|
+
${u('resource')}
|
186
|
+
Resource to update.
|
187
|
+
|
188
|
+
${u('body')}
|
189
|
+
Body in JSON.`,
|
190
|
+
post: `${description.deconz}
|
191
|
+
|
192
|
+
Usage: ${b('deconz')} ${usage.post}
|
193
|
+
|
194
|
+
${description.post}
|
195
|
+
|
196
|
+
Parameters:
|
197
|
+
${b('-h')}, ${b('--help')}
|
198
|
+
Print this help and exit.
|
199
|
+
|
200
|
+
${b('-v')}, ${b('--verbose')}
|
201
|
+
Print full API output.
|
202
|
+
|
203
|
+
${u('resource')}
|
204
|
+
Resource to create.
|
205
|
+
|
206
|
+
${u('body')}
|
207
|
+
Body in JSON.`,
|
208
|
+
delete: `${description.deconz}
|
209
|
+
|
210
|
+
Usage: ${b('deconz')} ${usage.delete}
|
211
|
+
|
212
|
+
${description.delete}
|
213
|
+
|
214
|
+
Parameters:
|
215
|
+
${b('-h')}, ${b('--help')}
|
216
|
+
Print this help and exit.
|
217
|
+
|
218
|
+
${b('-v')}, ${b('--verbose')}
|
219
|
+
Print full API output.
|
220
|
+
|
221
|
+
${u('resource')}
|
222
|
+
Resource to delete.
|
223
|
+
|
224
|
+
${u('body')}
|
225
|
+
Body in JSON.`,
|
226
|
+
eventlog: `${description.deconz}
|
227
|
+
|
228
|
+
Usage: ${b('deconz')} ${usage.eventlog}
|
229
|
+
|
230
|
+
${description.eventlog}
|
231
|
+
|
232
|
+
Parameters:
|
233
|
+
${b('-h')}, ${b('--help')}
|
234
|
+
Print this help and exit.
|
235
|
+
|
236
|
+
${b('-v')}, ${b('--verbose')}
|
237
|
+
Print full API output.
|
238
|
+
|
239
|
+
${b('-n')}, ${b('--noRetry')}
|
240
|
+
Do not retry when connection is closed.
|
241
|
+
|
242
|
+
${b('-r')}, ${b('--raw')}
|
243
|
+
Do not parse events, output raw event data.
|
244
|
+
|
245
|
+
${b('-s')}, ${b('--service')}
|
246
|
+
Do not output timestamps (useful when running as service).`,
|
247
|
+
discover: `${description.deconz}
|
248
|
+
|
249
|
+
Usage: ${b('deconz')} ${usage.discover}
|
250
|
+
|
251
|
+
${description.discover}
|
252
|
+
|
253
|
+
Parameters:
|
254
|
+
${b('-h')}, ${b('--help')}
|
255
|
+
Print this help and exit.
|
256
|
+
|
257
|
+
${b('-S')}, ${b('--stealth')}
|
258
|
+
Stealth mode, only use local discovery.`,
|
259
|
+
config: `${description.deconz}
|
260
|
+
|
261
|
+
Usage: ${b('deconz')} ${usage.config}
|
262
|
+
|
263
|
+
${description.config}
|
264
|
+
|
265
|
+
Parameters:
|
266
|
+
${b('-h')}, ${b('--help')}
|
267
|
+
Print this help and exit.
|
268
|
+
|
269
|
+
${b('-s')}, ${b('--sortKeys')}
|
270
|
+
Sort object key/value pairs alphabetically on key.`,
|
271
|
+
description: `${description.deconz}
|
272
|
+
|
273
|
+
Usage: ${b('deconz')} ${usage.description}
|
274
|
+
|
275
|
+
${description.description}
|
276
|
+
|
277
|
+
Parameters:
|
278
|
+
${b('-h')}, ${b('--help')}
|
279
|
+
Print this help and exit.
|
280
|
+
|
281
|
+
${b('-s')}, ${b('--sortKeys')}
|
282
|
+
Sort object key/value pairs alphabetically on key.`,
|
283
|
+
getApiKey: `${description.deconz}
|
284
|
+
|
285
|
+
Usage: ${b('deconz')} ${usage.getApiKey}
|
286
|
+
|
287
|
+
${description.getApiKey}
|
288
|
+
You need to unlock the deCONZ gateway prior to issuing this command,
|
289
|
+
unless you're running it on the gateway's local host.
|
290
|
+
The API key is saved to ${b('~/.deconz')}.
|
291
|
+
|
292
|
+
Parameters:
|
293
|
+
${b('-h')}, ${b('--help')}
|
294
|
+
Print this help and exit.
|
295
|
+
|
296
|
+
${b('-v')}, ${b('--verbose')}
|
297
|
+
Print full API output.`,
|
298
|
+
unlock: `${description.deconz}
|
299
|
+
|
300
|
+
Usage: ${b('deconz')} ${usage.unlock}
|
301
|
+
|
302
|
+
${description.unlock}
|
303
|
+
|
304
|
+
Parameters:
|
305
|
+
${b('-h')}, ${b('--help')}
|
306
|
+
Print this help and exit.
|
307
|
+
|
308
|
+
${b('-v')}, ${b('--verbose')}
|
309
|
+
Print full API output.`,
|
310
|
+
search: `${description.search}
|
311
|
+
|
312
|
+
Usage: ${b('deconz')} ${usage.search}
|
313
|
+
|
314
|
+
${description.search}
|
315
|
+
|
316
|
+
Parameters:
|
317
|
+
${b('-h')}, ${b('--help')}
|
318
|
+
Print this help and exit.
|
319
|
+
|
320
|
+
${b('-v')}, ${b('--verbose')}
|
321
|
+
Print full API output.`,
|
322
|
+
probe: `${description.deconz}
|
323
|
+
|
324
|
+
Usage: ${b('deconz')} ${usage.probe}
|
325
|
+
|
326
|
+
${description.probe}
|
327
|
+
|
328
|
+
Parameters:
|
329
|
+
${b('-h')}, ${b('--help')}
|
330
|
+
Print this help and exit.
|
331
|
+
|
332
|
+
${b('-v')}, ${b('--verbose')}
|
333
|
+
Print full API output.
|
334
|
+
|
335
|
+
${b('-t')} ${u('timeout')}, ${b('--timeout=')}${u('timeout')}
|
336
|
+
Timeout after ${u('timeout')} minutes (default: 5).
|
337
|
+
|
338
|
+
${u('light')}
|
339
|
+
Lights resource to probe.`,
|
340
|
+
restart: `${description.deconz}
|
341
|
+
|
342
|
+
Usage: ${b('deconz')} ${usage.restart}
|
343
|
+
|
344
|
+
${description.restart}
|
345
|
+
|
346
|
+
Parameters:
|
347
|
+
${b('-h')}, ${b('--help')}
|
348
|
+
Print this help and exit.
|
349
|
+
|
350
|
+
${b('-v')}, ${b('--verbose')}
|
351
|
+
Print full API output.`
|
352
|
+
}
|
353
|
+
|
354
|
+
class Main extends homebridgeLib.CommandLineTool {
|
355
|
+
constructor () {
|
356
|
+
super({ mode: 'command', debug: false })
|
357
|
+
this.usage = usage.deconz
|
358
|
+
try {
|
359
|
+
this.readGateways()
|
360
|
+
} catch (error) {
|
361
|
+
if (error.code !== 'ENOENT') {
|
362
|
+
this.error(error)
|
363
|
+
}
|
364
|
+
this.gateways = {}
|
365
|
+
}
|
366
|
+
}
|
367
|
+
|
368
|
+
// ===========================================================================
|
369
|
+
|
370
|
+
readGateways () {
|
371
|
+
const text = fs.readFileSync(process.env.HOME + '/.deconz')
|
372
|
+
try {
|
373
|
+
this.gateways = JSON.parse(text)
|
374
|
+
} catch (error) {
|
375
|
+
this.warn('%s/.deconz: file corrupted', process.env.HOME)
|
376
|
+
this.gateways = {}
|
377
|
+
}
|
378
|
+
}
|
379
|
+
|
380
|
+
writeGateways () {
|
381
|
+
const jsonFormatter = new homebridgeLib.JsonFormatter(
|
382
|
+
{ noWhiteSpace: true, sortKeys: true }
|
383
|
+
)
|
384
|
+
const text = jsonFormatter.stringify(this.gateways)
|
385
|
+
fs.writeFileSync(process.env.HOME + '/.deconz', text, { mode: 0o600 })
|
386
|
+
}
|
387
|
+
|
388
|
+
parseArguments () {
|
389
|
+
const parser = new homebridgeLib.CommandLineParser(packageJson)
|
390
|
+
const clargs = {
|
391
|
+
options: {
|
392
|
+
host: process.env.DECONZ_HOST || 'localhost',
|
393
|
+
timeout: 5
|
394
|
+
}
|
395
|
+
}
|
396
|
+
parser
|
397
|
+
.help('h', 'help', help.deconz)
|
398
|
+
.version('V', 'version')
|
399
|
+
.option('H', 'host', (value) => {
|
400
|
+
homebridgeLib.OptionParser.toHost('host', value, false, true)
|
401
|
+
clargs.options.host = value
|
402
|
+
})
|
403
|
+
.option('K', 'apiKey', (value) => {
|
404
|
+
clargs.options.apiKey = homebridgeLib.OptionParser.toString(
|
405
|
+
'apiKey', value, true, true
|
406
|
+
)
|
407
|
+
})
|
408
|
+
.flag('p', 'phoscon', () => {
|
409
|
+
clargs.options.phoscon = true
|
410
|
+
})
|
411
|
+
.flag('D', 'debug', () => {
|
412
|
+
if (this.debugEnabled) {
|
413
|
+
this.setOptions({ vdebug: true })
|
414
|
+
} else {
|
415
|
+
this.setOptions({ debug: true, chalk: true })
|
416
|
+
}
|
417
|
+
})
|
418
|
+
.option('t', 'timeout', (value) => {
|
419
|
+
clargs.options.timeout = homebridgeLib.OptionParser.toInt(
|
420
|
+
'timeout', value, 1, 60, true
|
421
|
+
)
|
422
|
+
})
|
423
|
+
.parameter('command', (value) => {
|
424
|
+
if (usage[value] == null || typeof this[value] !== 'function') {
|
425
|
+
throw new UsageError(`${value}: unknown command`)
|
426
|
+
}
|
427
|
+
clargs.command = value
|
428
|
+
})
|
429
|
+
.remaining((list) => { clargs.args = list })
|
430
|
+
parser
|
431
|
+
.parse()
|
432
|
+
return clargs
|
433
|
+
}
|
434
|
+
|
435
|
+
async main () {
|
436
|
+
try {
|
437
|
+
await this._main()
|
438
|
+
} catch (error) {
|
439
|
+
if (error.request == null) {
|
440
|
+
this.error(error)
|
441
|
+
}
|
442
|
+
}
|
443
|
+
}
|
444
|
+
|
445
|
+
async _main () {
|
446
|
+
this.clargs = this.parseArguments()
|
447
|
+
this.deconzDiscovery = new Deconz.Discovery({
|
448
|
+
timeout: this.clargs.options.timeout
|
449
|
+
})
|
450
|
+
this.deconzDiscovery
|
451
|
+
.on('error', (error) => {
|
452
|
+
this.log(
|
453
|
+
'%s: request %d: %s %s', error.request.name,
|
454
|
+
error.request.id, error.request.method, error.request.resource
|
455
|
+
)
|
456
|
+
this.warn(
|
457
|
+
'%s: request %d: %s', error.request.name, error.request.id, error
|
458
|
+
)
|
459
|
+
})
|
460
|
+
.on('request', (request) => {
|
461
|
+
this.debug(
|
462
|
+
'%s: request %d: %s %s', request.name,
|
463
|
+
request.id, request.method, request.resource
|
464
|
+
)
|
465
|
+
this.vdebug(
|
466
|
+
'%s: request %d: %s %s', request.name,
|
467
|
+
request.id, request.method, request.url
|
468
|
+
)
|
469
|
+
})
|
470
|
+
.on('response', (response) => {
|
471
|
+
this.vdebug(
|
472
|
+
'%s: request %d: response: %j', response.request.name,
|
473
|
+
response.request.id, response.body
|
474
|
+
)
|
475
|
+
this.debug(
|
476
|
+
'%s: request %d: %d %s', response.request.name,
|
477
|
+
response.request.id, response.statusCode, response.statusMessage
|
478
|
+
)
|
479
|
+
})
|
480
|
+
.on('found', (name, id, address) => {
|
481
|
+
this.debug('%s: found %s at %s', name, id, address)
|
482
|
+
})
|
483
|
+
.on('searching', (host) => {
|
484
|
+
this.debug('upnp: listening on %s', host)
|
485
|
+
})
|
486
|
+
.on('searchDone', () => { this.debug('upnp: search done') })
|
487
|
+
|
488
|
+
if (this.clargs.command === 'discover') {
|
489
|
+
return this.discover(this.clargs.args)
|
490
|
+
}
|
491
|
+
try {
|
492
|
+
this.gatewayConfig = await this.deconzDiscovery.config(
|
493
|
+
this.clargs.options.host
|
494
|
+
)
|
495
|
+
} catch (error) {
|
496
|
+
if (error.request == null) {
|
497
|
+
await this.fatal('%s: %s', this.clargs.options.host, error)
|
498
|
+
}
|
499
|
+
await this.fatal('%s: deCONZ gateway not found', this.clargs.options.host)
|
500
|
+
}
|
501
|
+
|
502
|
+
this.name = 'deconz ' + this.clargs.command
|
503
|
+
this.usage = `${b('deconz')} ${usage[this.clargs.command]}`
|
504
|
+
|
505
|
+
if (this.clargs.command === 'config' || this.clargs.command === 'description') {
|
506
|
+
return this[this.clargs.command](this.clargs.args)
|
507
|
+
}
|
508
|
+
|
509
|
+
this.bridgeid = this.gatewayConfig.bridgeid
|
510
|
+
if (this.clargs.options.apiKey == null) {
|
511
|
+
if (
|
512
|
+
this.gateways[this.bridgeid] != null &&
|
513
|
+
this.gateways[this.bridgeid].apiKey != null
|
514
|
+
) {
|
515
|
+
this.clargs.options.apiKey = this.gateways[this.bridgeid].apiKey
|
516
|
+
} else if (process.env.DECONZ_API_KEY != null) {
|
517
|
+
this.clargs.options.apiKey = process.env.DECONZ_API_KEY
|
518
|
+
}
|
519
|
+
}
|
520
|
+
if (this.clargs.options.apiKey == null && this.clargs.command !== 'getApiKey') {
|
521
|
+
let args = ''
|
522
|
+
if (
|
523
|
+
this.clargs.options.host !== 'localhost' &&
|
524
|
+
this.clargs.options.host !== process.env.DECONZ_HOST
|
525
|
+
) {
|
526
|
+
args += ' -H ' + this.clargs.options.host
|
527
|
+
}
|
528
|
+
await this.fatal(
|
529
|
+
'missing API key - unlock gateway and run "deconz%s getApiKey"', args
|
530
|
+
)
|
531
|
+
}
|
532
|
+
this.client = new Deconz.ApiClient(this.clargs.options)
|
533
|
+
this.client
|
534
|
+
.on('error', (error) => {
|
535
|
+
if (error.request.id !== this.requestId) {
|
536
|
+
if (error.request.body == null) {
|
537
|
+
this.log(
|
538
|
+
'request %d: %s %s', error.request.id,
|
539
|
+
error.request.method, error.request.resource
|
540
|
+
)
|
541
|
+
} else {
|
542
|
+
this.log(
|
543
|
+
'request %d: %s %s %s', error.request.id,
|
544
|
+
error.request.method, error.request.resource, error.request.body
|
545
|
+
)
|
546
|
+
}
|
547
|
+
this.requestId = error.request.id
|
548
|
+
}
|
549
|
+
if (error.nonCritical) {
|
550
|
+
this.warn('request %d: %s', error.request.id, error)
|
551
|
+
} else {
|
552
|
+
this.error('request %d: %s', error.request.id, error)
|
553
|
+
}
|
554
|
+
})
|
555
|
+
.on('request', (request) => {
|
556
|
+
if (request.body == null) {
|
557
|
+
this.debug(
|
558
|
+
'request %d: %s %s', request.id, request.method, request.resource
|
559
|
+
)
|
560
|
+
this.vdebug(
|
561
|
+
'request %d: %s %s', request.id, request.method, request.url
|
562
|
+
)
|
563
|
+
} else {
|
564
|
+
this.debug(
|
565
|
+
'request %d: %s %s %s', request.id,
|
566
|
+
request.method, request.resource, request.body
|
567
|
+
)
|
568
|
+
this.vdebug(
|
569
|
+
'request %d: %s %s %s', request.id,
|
570
|
+
request.method, request.url, request.body
|
571
|
+
)
|
572
|
+
}
|
573
|
+
})
|
574
|
+
.on('response', (response) => {
|
575
|
+
this.vdebug(
|
576
|
+
'request %d: response: %j', response.request.id, response.body
|
577
|
+
)
|
578
|
+
this.debug(
|
579
|
+
'request %d: %d %s', response.request.id,
|
580
|
+
response.statusCode, response.statusMessage
|
581
|
+
)
|
582
|
+
})
|
583
|
+
return this[this.clargs.command](this.clargs.args)
|
584
|
+
}
|
585
|
+
|
586
|
+
// ===== GET =================================================================
|
587
|
+
|
588
|
+
async get (...args) {
|
589
|
+
const parser = new homebridgeLib.CommandLineParser(packageJson)
|
590
|
+
const clargs = {
|
591
|
+
options: {}
|
592
|
+
}
|
593
|
+
parser
|
594
|
+
.help('h', 'help', help.get)
|
595
|
+
.flag('s', 'sortKeys', () => { clargs.options.sortKeys = true })
|
596
|
+
.flag('n', 'noWhiteSpace', () => {
|
597
|
+
clargs.options.noWhiteSpace = true
|
598
|
+
})
|
599
|
+
.flag('j', 'jsonArray', () => { clargs.options.noWhiteSpace = true })
|
600
|
+
.flag('u', 'joinKeys', () => { clargs.options.joinKeys = true })
|
601
|
+
.flag('a', 'ascii', () => { clargs.options.ascii = true })
|
602
|
+
.flag('t', 'topOnly', () => { clargs.options.topOnly = true })
|
603
|
+
.flag('l', 'leavesOnly', () => { clargs.options.leavesOnly = true })
|
604
|
+
.flag('k', 'keysOnly', () => { clargs.options.keysOnly = true })
|
605
|
+
.flag('v', 'valuesOnly', () => { clargs.options.valuesOnly = true })
|
606
|
+
.remaining((list) => {
|
607
|
+
if (list.length > 1) {
|
608
|
+
throw new UsageError('too many paramters')
|
609
|
+
}
|
610
|
+
clargs.resource = list.length === 0
|
611
|
+
? '/'
|
612
|
+
: homebridgeLib.OptionParser.toPath('resource', list[0])
|
613
|
+
})
|
614
|
+
.parse(...args)
|
615
|
+
const jsonFormatter = new homebridgeLib.JsonFormatter(clargs.options)
|
616
|
+
const response = await this.client.get(clargs.resource)
|
617
|
+
this.print(jsonFormatter.stringify(response))
|
618
|
+
}
|
619
|
+
|
620
|
+
// ===== PUT, POST, DELETE ===================================================
|
621
|
+
|
622
|
+
async resourceCommand (command, ...args) {
|
623
|
+
const parser = new homebridgeLib.CommandLineParser(packageJson)
|
624
|
+
const clargs = {
|
625
|
+
options: {}
|
626
|
+
}
|
627
|
+
parser
|
628
|
+
.help('h', 'help', help[command])
|
629
|
+
.flag('v', 'verbose', () => { clargs.options.verbose = true })
|
630
|
+
.parameter('resource', (resource) => {
|
631
|
+
clargs.resource = homebridgeLib.OptionParser.toPath('resource', resource)
|
632
|
+
if (clargs.resource === '/') {
|
633
|
+
// deCONZ will crash otherwise, see deconz-rest-plugin#2520.
|
634
|
+
throw new UsageError(`/: invalid resource for ${command}`)
|
635
|
+
}
|
636
|
+
})
|
637
|
+
.remaining((list) => {
|
638
|
+
if (list.length > 1) {
|
639
|
+
throw new Error('too many paramters')
|
640
|
+
}
|
641
|
+
if (list.length === 1) {
|
642
|
+
try {
|
643
|
+
clargs.body = JSON.parse(list[0])
|
644
|
+
} catch (error) {
|
645
|
+
throw new Error(error.message) // Covert TypeError to Error.
|
646
|
+
}
|
647
|
+
}
|
648
|
+
})
|
649
|
+
.parse(...args)
|
650
|
+
const response = await this.client[command](clargs.resource, clargs.body)
|
651
|
+
const jsonFormatter = new homebridgeLib.JsonFormatter()
|
652
|
+
if (clargs.options.verbose || response.success == null) {
|
653
|
+
this.print(jsonFormatter.stringify(response.body))
|
654
|
+
return
|
655
|
+
}
|
656
|
+
if (command !== 'put') {
|
657
|
+
if (response.success.id != null) {
|
658
|
+
this.print(jsonFormatter.stringify(response.success.id))
|
659
|
+
} else {
|
660
|
+
this.print(jsonFormatter.stringify(response.success))
|
661
|
+
}
|
662
|
+
return
|
663
|
+
}
|
664
|
+
this.print(jsonFormatter.stringify(response.success))
|
665
|
+
}
|
666
|
+
|
667
|
+
async put (...args) {
|
668
|
+
return this.resourceCommand('put', ...args)
|
669
|
+
}
|
670
|
+
|
671
|
+
async post (...args) {
|
672
|
+
return this.resourceCommand('post', ...args)
|
673
|
+
}
|
674
|
+
|
675
|
+
async delete (...args) {
|
676
|
+
return this.resourceCommand('delete', ...args)
|
677
|
+
}
|
678
|
+
|
679
|
+
// ===========================================================================
|
680
|
+
|
681
|
+
async destroy () {
|
682
|
+
if (this.wsMonitor != null) {
|
683
|
+
await this.wsMonitor.close()
|
684
|
+
}
|
685
|
+
if (this.eventStream != null) {
|
686
|
+
await this.eventStream.close()
|
687
|
+
}
|
688
|
+
}
|
689
|
+
|
690
|
+
async eventlog (...args) {
|
691
|
+
const parser = new homebridgeLib.CommandLineParser(packageJson)
|
692
|
+
let mode = 'daemon'
|
693
|
+
const options = {}
|
694
|
+
parser
|
695
|
+
.help('h', 'help', help.eventlog)
|
696
|
+
.flag('n', 'noRetry', () => { options.retryTime = 0 })
|
697
|
+
.flag('r', 'raw', () => { options.raw = true })
|
698
|
+
.flag('s', 'service', () => { mode = 'service' })
|
699
|
+
.parse(...args)
|
700
|
+
this.jsonFormatter = new homebridgeLib.JsonFormatter(
|
701
|
+
mode === 'service' ? { noWhiteSpace: true } : {}
|
702
|
+
)
|
703
|
+
const { websocketport } = await this.client.get('/config')
|
704
|
+
options.host = this.client.host + ':' + websocketport
|
705
|
+
this.wsMonitor = new Deconz.WsClient(options)
|
706
|
+
this.setOptions({ mode: mode })
|
707
|
+
this.wsMonitor
|
708
|
+
.on('error', (error) => { this.error(error) })
|
709
|
+
.on('listening', (url) => { this.log('listening on %s', url) })
|
710
|
+
.on('closed', (url) => { this.log('connection to %s closed', url) })
|
711
|
+
.on('changed', (rtype, rid, body) => {
|
712
|
+
let resource = '/' + rtype + '/' + rid
|
713
|
+
if (Object.keys(body).length === 1) {
|
714
|
+
if (body.state != null) {
|
715
|
+
resource += '/state'
|
716
|
+
body = body.state
|
717
|
+
} else if (body.config != null) {
|
718
|
+
resource += '/config'
|
719
|
+
body = body.config
|
720
|
+
}
|
721
|
+
}
|
722
|
+
this.log('%s: %s', resource, this.jsonFormatter.stringify(body))
|
723
|
+
})
|
724
|
+
.on('added', (rtype, rid, body) => {
|
725
|
+
this.log(
|
726
|
+
'/%s/%d: added: %s', rtype, rid, this.jsonFormatter.stringify(body)
|
727
|
+
)
|
728
|
+
})
|
729
|
+
.on('sceneRecall', (resource) => {
|
730
|
+
this.log('%s: recall', resource)
|
731
|
+
})
|
732
|
+
.on('notification', (body) => {
|
733
|
+
this.log(this.jsonFormatter.stringify(body))
|
734
|
+
})
|
735
|
+
.listen()
|
736
|
+
}
|
737
|
+
|
738
|
+
// ===========================================================================
|
739
|
+
|
740
|
+
async simpleCommand (command, ...args) {
|
741
|
+
const parser = new homebridgeLib.CommandLineParser(packageJson)
|
742
|
+
const clargs = {
|
743
|
+
options: {}
|
744
|
+
}
|
745
|
+
parser
|
746
|
+
.help('h', 'help', help[command])
|
747
|
+
.flag('v', 'verbose', () => { clargs.options.verbose = true })
|
748
|
+
.parse(...args)
|
749
|
+
const response = await this.client[command]()
|
750
|
+
const jsonFormatter = new homebridgeLib.JsonFormatter()
|
751
|
+
for (const error of response.errors) {
|
752
|
+
this.warn('api error %d: %s', error.type, error.description)
|
753
|
+
}
|
754
|
+
if (clargs.options.verbose || response.success == null) {
|
755
|
+
this.print(jsonFormatter.stringify(response.body))
|
756
|
+
return
|
757
|
+
}
|
758
|
+
if (response.success.id != null) {
|
759
|
+
this.print(jsonFormatter.stringify(response.success.id))
|
760
|
+
return
|
761
|
+
}
|
762
|
+
if (response.success != null) {
|
763
|
+
this.print(jsonFormatter.stringify(response.success))
|
764
|
+
return
|
765
|
+
}
|
766
|
+
this.print(jsonFormatter.stringify(response.body))
|
767
|
+
}
|
768
|
+
|
769
|
+
async discover (...args) {
|
770
|
+
const parser = new homebridgeLib.CommandLineParser(packageJson)
|
771
|
+
let stealth = false
|
772
|
+
parser
|
773
|
+
.help('h', 'help', help.discover)
|
774
|
+
.flag('S', 'stealth', () => { stealth = true })
|
775
|
+
.parse(...args)
|
776
|
+
const jsonFormatter = new homebridgeLib.JsonFormatter({ sortKeys: true })
|
777
|
+
const bridges = await this.deconzDiscovery.discover(stealth)
|
778
|
+
this.print(jsonFormatter.stringify(bridges))
|
779
|
+
}
|
780
|
+
|
781
|
+
async config (...args) {
|
782
|
+
const parser = new homebridgeLib.CommandLineParser(packageJson)
|
783
|
+
const options = {}
|
784
|
+
parser
|
785
|
+
.help('h', 'help', help.config)
|
786
|
+
.flag('s', 'sortKeys', () => { options.sortKeys = true })
|
787
|
+
.parse(...args)
|
788
|
+
const jsonFormatter = new homebridgeLib.JsonFormatter(options)
|
789
|
+
const json = jsonFormatter.stringify(this.gatewayConfig)
|
790
|
+
this.print(json)
|
791
|
+
}
|
792
|
+
|
793
|
+
async description (...args) {
|
794
|
+
const parser = new homebridgeLib.CommandLineParser(packageJson)
|
795
|
+
const options = {}
|
796
|
+
parser
|
797
|
+
.help('h', 'help', help.description)
|
798
|
+
.flag('s', 'sortKeys', () => { options.sortKeys = true })
|
799
|
+
.parse(...args)
|
800
|
+
const response = await this.deconzDiscovery.description(this.clargs.options.host)
|
801
|
+
const jsonFormatter = new homebridgeLib.JsonFormatter(options)
|
802
|
+
const json = jsonFormatter.stringify(response)
|
803
|
+
this.print(json)
|
804
|
+
}
|
805
|
+
|
806
|
+
async getApiKey (...args) {
|
807
|
+
const parser = new homebridgeLib.CommandLineParser(packageJson)
|
808
|
+
const jsonFormatter = new homebridgeLib.JsonFormatter(
|
809
|
+
{ noWhiteSpace: true, sortKeys: true }
|
810
|
+
)
|
811
|
+
parser
|
812
|
+
.help('h', 'help', help.getApiKey)
|
813
|
+
.parse(...args)
|
814
|
+
const apiKey = await this.client.getApiKey('deconz')
|
815
|
+
this.print(jsonFormatter.stringify(apiKey))
|
816
|
+
this.gateways[this.bridgeid] = { apiKey: apiKey }
|
817
|
+
if (this.client.fingerprint != null) {
|
818
|
+
this.gateways[this.bridgeid].fingerprint = this.client.fingerprint
|
819
|
+
}
|
820
|
+
this.writeGateways()
|
821
|
+
}
|
822
|
+
|
823
|
+
async unlock (...args) {
|
824
|
+
return this.simpleCommand('unlock', ...args)
|
825
|
+
}
|
826
|
+
|
827
|
+
async search (...args) {
|
828
|
+
return this.simpleCommand('search', ...args)
|
829
|
+
}
|
830
|
+
|
831
|
+
// ===== LIGHTVALUES =========================================================
|
832
|
+
|
833
|
+
async probe (...args) {
|
834
|
+
const parser = new homebridgeLib.CommandLineParser(packageJson)
|
835
|
+
const clargs = {
|
836
|
+
maxCount: 60
|
837
|
+
}
|
838
|
+
parser
|
839
|
+
.help('h', 'help', help.probe)
|
840
|
+
.flag('v', 'verbose', () => { clargs.verbose = true })
|
841
|
+
.option('t', 'timeout', (value, key) => {
|
842
|
+
homebridgeLib.OptionParser.toInt(
|
843
|
+
'timeout', value, 1, 10, true
|
844
|
+
)
|
845
|
+
clargs.maxCount = value * 12
|
846
|
+
})
|
847
|
+
.parameter('light', (value) => {
|
848
|
+
if (value.substring(0, 8) !== '/lights/') {
|
849
|
+
throw new UsageError(`${value}: invalid light`)
|
850
|
+
}
|
851
|
+
clargs.light = value
|
852
|
+
})
|
853
|
+
.parse(...args)
|
854
|
+
const light = await this.client.get(clargs.light)
|
855
|
+
|
856
|
+
async function probeCt (name, value) {
|
857
|
+
clargs.verbose && this.log(`${clargs.light}: ${name} ...\\c`)
|
858
|
+
await this.client.put(clargs.light + '/state', { ct: value })
|
859
|
+
let count = 0
|
860
|
+
return new Promise((resolve, reject) => {
|
861
|
+
const interval = setInterval(async () => {
|
862
|
+
const ct = await this.client.get(clargs.light + '/state/ct')
|
863
|
+
if (ct !== value || ++count > clargs.maxCount) {
|
864
|
+
clearInterval(interval)
|
865
|
+
clargs.verbose && this.logc(
|
866
|
+
count > clargs.maxCount ? ' timeout' : ' done'
|
867
|
+
)
|
868
|
+
return resolve(ct)
|
869
|
+
}
|
870
|
+
clargs.verbose && this.logc('.\\c')
|
871
|
+
}, 5000)
|
872
|
+
})
|
873
|
+
}
|
874
|
+
|
875
|
+
function round (f) {
|
876
|
+
return Math.round(f * 10000) / 10000
|
877
|
+
}
|
878
|
+
|
879
|
+
async function probeXy (name, value) {
|
880
|
+
clargs.verbose && this.log(`${clargs.light}: ${name} ...\\c`)
|
881
|
+
await this.client.put(clargs.light + '/state', { xy: value })
|
882
|
+
let count = 0
|
883
|
+
return new Promise((resolve, reject) => {
|
884
|
+
const interval = setInterval(async () => {
|
885
|
+
let xy = await this.client.get(clargs.light + '/state/xy')
|
886
|
+
if (this.client.isDeconz) {
|
887
|
+
xy = [round(xy[0]), round(xy[1])]
|
888
|
+
}
|
889
|
+
if (
|
890
|
+
xy[0] !== value[0] || xy[1] !== value[1] ||
|
891
|
+
++count > clargs.maxCount
|
892
|
+
) {
|
893
|
+
clearInterval(interval)
|
894
|
+
clargs.verbose && this.logc(
|
895
|
+
count > clargs.maxCount ? ' timeout' : ' done'
|
896
|
+
)
|
897
|
+
return resolve(xy)
|
898
|
+
}
|
899
|
+
clargs.verbose && this.logc('.\\c')
|
900
|
+
}, 5000)
|
901
|
+
})
|
902
|
+
}
|
903
|
+
|
904
|
+
this.verbose && this.log(
|
905
|
+
'%s: %s %s %s "%s"', clargs.light, light.manufacturername,
|
906
|
+
light.modelid, light.type, light.name
|
907
|
+
)
|
908
|
+
const response = {
|
909
|
+
manufacturername: light.manufacturername,
|
910
|
+
modelid: light.modelid,
|
911
|
+
type: light.type,
|
912
|
+
bri: light.state.bri != null
|
913
|
+
}
|
914
|
+
await this.client.put(clargs.light + '/state', { on: true })
|
915
|
+
if (light.state.ct != null) {
|
916
|
+
response.ct = {}
|
917
|
+
response.ct.min = await probeCt.call(this, 'cool', 1)
|
918
|
+
response.ct.max = await probeCt.call(this, 'warm', 1000)
|
919
|
+
}
|
920
|
+
if (light.state.xy != null) {
|
921
|
+
const zero = 0.0001
|
922
|
+
const one = 0.9961
|
923
|
+
response.xy = {}
|
924
|
+
response.xy.r = await probeXy.call(this, 'red', [one, zero])
|
925
|
+
response.xy.g = await probeXy.call(this, 'green', [zero, one])
|
926
|
+
response.xy.b = await probeXy.call(this, 'blue', [zero, zero])
|
927
|
+
}
|
928
|
+
await this.client.put(clargs.light + '/state', { on: light.state.on })
|
929
|
+
this.jsonFormatter = new homebridgeLib.JsonFormatter()
|
930
|
+
const json = this.jsonFormatter.stringify(response)
|
931
|
+
this.print(json)
|
932
|
+
}
|
933
|
+
|
934
|
+
// ===== BRIDGE/GATEWAY DISCOVERY ==============================================
|
935
|
+
|
936
|
+
async restart (...args) {
|
937
|
+
const parser = new homebridgeLib.CommandLineParser(packageJson)
|
938
|
+
const clargs = {}
|
939
|
+
parser
|
940
|
+
.help('h', 'help', help.restart)
|
941
|
+
.flag('v', 'verbose', () => { clargs.verbose = true })
|
942
|
+
.parse(...args)
|
943
|
+
if (this.client.isHue) {
|
944
|
+
const response = await this.client.put('/config', { reboot: true })
|
945
|
+
if (!response.success.reboot) {
|
946
|
+
return false
|
947
|
+
}
|
948
|
+
} else if (this.client.isDeconz) {
|
949
|
+
const response = await this.client.post('/config/restartapp')
|
950
|
+
if (!response.success.restartapp) {
|
951
|
+
return false
|
952
|
+
}
|
953
|
+
} else {
|
954
|
+
await this.fatal('restart: only supported for Hue bridge or deCONZ gateway')
|
955
|
+
}
|
956
|
+
clargs.verbose && this.log('restarting ...\\c')
|
957
|
+
return new Promise((resolve, reject) => {
|
958
|
+
let busy = false
|
959
|
+
const interval = setInterval(async () => {
|
960
|
+
try {
|
961
|
+
if (!busy) {
|
962
|
+
busy = true
|
963
|
+
const bridgeid = await this.client.get('/config/bridgeid')
|
964
|
+
if (bridgeid === this.bridgeid) {
|
965
|
+
clearInterval(interval)
|
966
|
+
clargs.verbose && this.logc(' done')
|
967
|
+
return resolve(true)
|
968
|
+
}
|
969
|
+
busy = false
|
970
|
+
}
|
971
|
+
} catch (error) {
|
972
|
+
busy = false
|
973
|
+
}
|
974
|
+
clargs.verbose && this.logc('.\\c')
|
975
|
+
}, 2500)
|
976
|
+
})
|
977
|
+
}
|
978
|
+
}
|
979
|
+
|
980
|
+
new Main().main()
|