homebridge-deconz 0.0.15 → 0.0.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.
@@ -6,501 +6,290 @@ Copyright © 2022 Erik Baauw. All rights reserved.
6
6
  -->
7
7
 
8
8
  <link rel="stylesheet" href="style.css">
9
+ <script src="https://unpkg.com/vue@3"></script>
10
+
9
11
  <p align="center">
10
12
  <a href="https://github.com/ebaauw/homebridge-deconz/wiki/Configuration" target="_blank">
11
13
  <img src="homebridge-deconz.png" height="200px">
12
14
  </a>
13
15
  </p>
14
16
 
15
- <script>
17
+ <div id="app">
18
+ <!-- v-if can be used on any element, it will only render the element if the condition is true -->
19
+ <div v-if="view === 'gateways'">
20
+ <div class="w-100 d-flex justify-content-between align-items-center mb-1">
21
+ <h4 class="mb-0">Gateways</h4>
22
+ <!-- @click will call the addGateway method -->
23
+ <button class="btn btn-primary mr-0" @click="addGateway">Add <i class="fas fa-plus" ></i></button>
24
+ </div>
16
25
 
17
- async function showFormPluginConfig () {
18
- homebridge.showSpinner()
19
- const pluginConfig = await homebridge.getPluginConfig()
20
- console.log('pluginConfig: %o', pluginConfig)
21
- // const pluginConfigSchema = await homebridge.getPluginConfigSchema()
22
- // console.log('pluginConfigSchema: %o', pluginConfigSchema)
23
- // const cachedAccessories = await homebridge.getCachedAccessories()
24
- // console.log('cachedAccessories: %o', cachedAccessories)
25
- // const discoveredGateways = await homebridge.request('discover')
26
- // console.log('discovered gateways: %o', discoveredGateways)
27
- for (const config of pluginConfig) {
28
- if (config._bridge != null) {
29
- const cachedAccessories = await homebridge.request('cachedAccessories', {
30
- username: config._bridge.username
31
- })
32
- console.log('%s: cachedAccessories: %o', config.name, cachedAccessories)
33
- const cachedGateways = cachedAccessories.filter((accessory) => {
34
- return accessory.plugin === 'homebridge-deconz' &&
35
- accessory.context != null &&
36
- accessory.context.className === 'Gateway'
37
- })
38
- const result = {}
39
- for (const gateway of cachedGateways) {
40
- if (gateway.context.uiPort == null) {
41
- continue
42
- }
43
- const pong = await homebridge.request(
44
- 'get', { uiPort: gateway.context.uiPort, path: '/ping' }
45
- )
46
- if (pong === 'pong') {
47
- result[gateway.context.host] = gateway.context
48
- }
49
- }
50
- const gateways = Object.keys(result).sort()
51
- console.log('%s: gateways: %j',config.name, gateways)
52
- }
53
- }
54
- homebridge.hideSpinner()
26
+ <ul class="list-group">
27
+ <!-- here we are looping over data.pluginConfig.gateways using v-for -->
28
+ <li class="list-group-item d-flex justify-content-between align-items-center" v-for="(gateway, $index) in pluginConfig.gateways" :key="$index">
29
+ <div>
30
+ {{ gateway.name }}
31
+ <div class="grey-text">
32
+ {{ gateway.host }}
33
+ </div>
34
+ </div>
35
+ <div>
36
+ <div class="btn-group" role="group" aria-label="Basic example">
37
+ <!-- @click will call the editGateway(gateway) method with the selected gateway as the first argument -->
38
+ <button class="btn btn-primary" @click="editGateway(gateway)">
39
+ <i class="fas fa-cog"></i>
40
+ </button>
41
+ <!-- @click will call the deleteGateway($index) method with its position in the array / index as the first argument -->
42
+ <button class="btn btn-danger" @click="deleteGateway($index)">
43
+ <i class="fas fa-trash"></i>
44
+ </button>
45
+ </div>
46
+ </div>
47
+ </li>
48
+ </ul>
49
+ </div>
55
50
 
56
- const form = homebridge.createForm(
57
- {
58
- schema: {
59
- type: 'object',
60
- properties: {
61
- config: {
62
- title: 'Gateways',
63
- description: 'Configure a child bridge per deCONZ gateway. See <a href="https://github.com/ebaauw/homebridge-deconz/wiki/Configuration" target="_blank">wiki</a> for details.',
64
- type: 'array',
65
- disabled: true,
66
- items: {
67
- type: 'object',
68
- properties: {
69
- host: {
70
- description: 'Gateway hostname and port.',
71
- default: 'localhost:80',
72
- type: 'string',
73
- required: true
74
- },
75
- name: {
76
- description: 'Homebridge log plugin name.',
77
- default: 'deCONZ',
78
- type: 'string'
79
- },
80
- _bridge: {
81
- type: 'object',
82
- required: true,
83
- properties: {
84
- name: {
85
- type: 'string',
86
- required: true
87
- },
88
- username: {
89
- type: 'string',
90
- pattern: '^([A-F0-9]{2}:){5}[A-F0-9]{2}$',
91
- placeholder: 'AA:BB:CC:DD:EE:FF',
92
- required: true
93
- },
94
- port: {
95
- type: 'integer',
96
- minimum: 1025,
97
- // maximum: 65535,
98
- required: true
99
- },
100
- manufacturer: {
101
- type: 'string',
102
- enabled: false,
103
- required: true
104
- },
105
- model: {
106
- type: 'string',
107
- required: true
108
- }
109
- }
110
- }
111
- }
112
- }
113
- }
114
- }
115
- },
116
- // layout: null
117
- layout: [
118
- {
119
- type: 'tabarray',
120
- title: '{{ value.name }}',
121
- items: [
122
- {
123
- type: 'fieldset',
124
- title: 'Gateway Settings',
125
- key: 'config[]',
126
- items: [
127
- {
128
- type: 'flex',
129
- 'flex-flow': 'row',
130
- items: [
131
- 'config[].host',
132
- 'config[].name',
133
- ]
134
- }
135
- ]
136
- },
137
- {
138
- type: 'flex',
139
- 'flex-flow': 'row',
140
- key: 'config[]',
141
- items: [
142
- {
143
- type: 'button',
144
- title: 'Connect',
145
- key: 'config[].connect'
146
- },
147
- {
148
- type: 'button',
149
- title: 'Get API Key',
150
- key: 'config[].getApiKey'
151
- },
152
- {
153
- type: 'submit',
154
- title: 'Configure',
155
- key: 'config[].configure'
156
- }
157
- ]
158
- },
159
- {
160
- type: 'fieldset',
161
- key: 'config[]._bridge',
162
- // expandable: true,
163
- title: 'Child Bridge Accessory Settings',
164
- items: [
165
- {
166
- type: 'flex',
167
- 'flex-flow': 'row',
168
- items: [
169
- 'config[]._bridge.username',
170
- 'config[]._bridge.port'
171
- ]
172
- },
173
- 'config[]._bridge.name',
174
- {
175
- type: 'flex',
176
- 'flex-flow': 'row',
177
- items: [
178
- 'config[]._bridge.manufacturer',
179
- 'config[]._bridge.model'
180
- ]
181
- }
182
- ]
183
- }
184
- ]
185
- }
186
- ]
187
- }, {
188
- config: pluginConfig,
189
- },
190
- 'Gateway Settings',
191
- 'Homebridge Settings'
192
- )
193
- form.onChange(async (form) => {
194
- console.log('change: %o', form)
195
- })
196
- form.onSubmit(async (form) => {
197
- console.log('submit: %o', form)
198
- })
199
- form.onCancel(async (form) => {
200
- console.log('cancel: %o', form)
201
- })
51
+ <div v-if="view === 'edit-gateway'">
52
+ <h4 class="mb-2">
53
+ Configure Gateway
54
+ <span v-if="selectedGateway">
55
+ - {{ selectedGateway.name }}
56
+ </span>
57
+ </h4>
202
58
 
203
- }
59
+ <!-- @click will call the connect method -->
60
+ <button class="btn btn-primary ml-0" @click="connect">Connect</button>
204
61
 
205
- async function showFormGateways (gateway) {
206
- homebridge.showSpinner()
207
- const cachedAccessories = await homebridge.getCachedAccessories()
208
- const cachedGateways = cachedAccessories.filter((accessory) => {
209
- return accessory.plugin === 'homebridge-deconz' &&
210
- accessory.context != null &&
211
- accessory.context.className === 'Gateway'
212
- })
213
- const result = {}
214
- for (const gateway of cachedGateways) {
215
- if (gateway.context.uiPort == null) {
216
- continue
217
- }
218
- const pong = await homebridge.request(
219
- 'get', { uiPort: gateway.context.uiPort, path: '/ping' }
220
- )
221
- if (pong === 'pong') {
222
- result[gateway.context.host] = gateway.context
223
- }
224
- }
225
- const gateways = Object.keys(result).sort()
226
- homebridge.hideSpinner()
227
- if (gateways.length === 0) {
228
- homebridge.showSchemaForm()
229
- return
230
- }
231
- // const form = homebridge.createForm({
232
- // schema: {
233
- // type: 'object',
234
- // properties: {
235
- // gateway: {
236
- // title: 'Connected Gateways',
237
- // type: 'string',
238
- // oneOf: gateways.map((name) => {
239
- // const config = result[name].context.config
240
- // return {
241
- // title: `${name}: dresden elektronik ${config.modelid} gateway v${config.swversion} / ${config.devicename} ${config.bridgeid}`,
242
- // enum: [name]
243
- // }
244
- // }),
245
- // required: true
246
- // }
247
- // }
248
- // },
249
- // layout: null,
250
- // form: null
251
- // }, {
252
- // gateway: gateway != null ? gateway : gateways[0]
253
- // }, 'Gateway Settings', 'Homebridge Settings')
254
- const form = homebridge.createForm({
255
- footerDisplay: 'For a detailed description, see the [wiki](https://github.com/ebaauw/homebridge-deconz/wiki/Configuration).',
256
- schema: {
257
- type: 'object',
258
- properties: {
259
- name: {
260
- description: 'Plugin name as displayed in the Homebridge log.',
261
- type: 'string',
262
- required: true,
263
- default: 'deCONZ'
264
- },
265
- gateways: {
266
- title: 'Gateways',
267
- type: 'array',
268
- disabled: true,
269
- items: {
270
- type: 'object',
271
- properties: {
272
- host: {
273
- description: 'Hostname and port of the deCONZ gateway.',
274
- type: 'string'
275
- },
276
- expose: {
277
- description: 'Expose gateway to HomeKit.',
278
- type: 'boolean'
279
- }
280
- }
281
- }
62
+ <!-- @click will call the getApiKey method -->
63
+ <button class="btn btn-primary" @click="getApiKey">Get API Key</button>
64
+ </div>
65
+ </div>
282
66
 
283
- }
284
- }
285
- } //,
286
- // layout: [
287
- // 'name',
288
- // {
289
- // key: 'gateways',
290
- // type: 'array',
291
- // buttonText: 'Add Gateway',
292
- // items: [
293
- // {
294
- // type: 'section',
295
- // htmlClass: 'row',
296
- // items: [
297
- // {
298
- // type: 'section',
299
- // htmlClass: 'col',
300
- // items: [
301
- // 'gateways[].host'
302
- // ]
303
- // },
304
- // {
305
- // type: 'section',
306
- // htmlClass: 'col',
307
- // items: [
308
- // {
309
- // key: 'gateways[].expose',
310
- // disabled: true
311
- // }
312
- // ]
313
- // }
314
- // ]
315
- // }
316
- // ]
317
- // }
318
- // ]
319
- }, {
320
-
321
- }, 'Gateway Settings', 'Homebridge Settings')
322
- form.onChange(async (form) => {
323
- // showFormGatewaySettings(result[form.gateway])
324
- })
325
- form.onSubmit(async (form) => {
326
- await showFormGatewaySettings(result[form.gateways])
327
- })
328
- form.onCancel(() => { homebridge.showSchemaForm() })
329
- }
67
+ <script>
68
+ const { createApp } = Vue;
330
69
 
331
- async function showFormGatewaySettings (gateway, device) {
332
- homebridge.showSpinner()
333
- const data = await homebridge.request(
334
- 'get', {
335
- uiPort: gateway.uiPort,
336
- path: '/gateways/' + gateway.id
337
- }
338
- )
339
- const values = {}
340
- for (const rtype in data.deviceByRidByRtype) {
341
- values[rtype] = []
342
- for (const rid in data.deviceByRidByRtype[rtype]) {
343
- const device = data.deviceByRidByRtype[rtype][rid]
344
- values[rtype].push({
345
- title: ['', rtype, rid].join('/') + ': ' +
346
- device.resourceBySubtype[device.primary].body.name,
347
- enum: [device.id]
348
- })
349
- }
350
- }
351
- data.lightsDevice = values.lights[0].enum[0]
352
- data.sensorsDevice = values.sensors[0].enum[0]
353
- data.groupsDevice = values.groups[0].enum[0]
354
- homebridge.hideSpinner()
355
- const form = homebridge.createForm({
70
+ const gatewaySchema = {
356
71
  schema: {
357
72
  type: 'object',
358
73
  properties: {
359
- expose: {
360
- title: 'Expose',
361
- type: 'boolean'
74
+ host: {
75
+ description: 'Gateway hostname and port.',
76
+ default: 'localhost:80',
77
+ type: 'string',
78
+ required: true
79
+ },
80
+ name: {
81
+ description: 'Homebridge log plugin name.',
82
+ default: 'deCONZ',
83
+ type: 'string'
362
84
  },
363
- lights: {
364
- title: 'Lights',
365
- type: 'boolean',
85
+ forceHttp: {
86
+ description: "Use plain http instead of https.",
87
+ type: "boolean"
366
88
  },
367
- sensors: {
368
- title: 'Sensors',
369
- type: 'boolean',
89
+ noResponse: {
90
+ description: "Report unreachable lights as <i>No Response</i> in HomeKit.",
91
+ type: "boolean"
370
92
  },
371
- groups: {
372
- title: 'Groups',
373
- type: 'boolean',
93
+ parallelRequests: {
94
+ description:" The number of ansynchronous requests Homebridge deCONZ sends in parallel to a deCONZ gateway. Default: 10.",
95
+ type: "integer",
96
+ minimum: 1,
97
+ maximum: 30
374
98
  },
375
- schedules: {
376
- title: 'Schedules',
377
- type: 'boolean',
99
+ stealth: {
100
+ description: "Stealth mode: don't make any calls to the Internet. Default: false.",
101
+ type: "boolean"
378
102
  },
379
- logLevel: {
380
- title: 'Log Level',
381
- type: 'string',
382
- oneOf: ['0', '1', '2', '3'].map((level) => { return { title: level, enum: [level] } }),
383
- required: true,
384
- condition: {
385
- functionBody: 'return model.expose'
386
- }
103
+ timeout: {
104
+ description: "The timeout in seconds to wait for a response from a deCONZ gateway. Default: 5.",
105
+ type: "integer",
106
+ minimum: 1,
107
+ maximum: 30
387
108
  },
388
- lightsDevice: {
389
- title: 'Device',
390
- type: 'string',
391
- oneOf: values.lights,
392
- required: true
109
+ waitTimePut: {
110
+ description: "The time, in milliseconds, to wait after sending a PUT request, before sending the next PUT request. Default: 50.",
111
+ type: "integer",
112
+ minimum: 0,
113
+ maximum: 50
393
114
  },
394
- sensorsDevice: {
395
- title: 'Device',
396
- type: 'string',
397
- oneOf: values.sensors,
398
- required: true
115
+ waitTimePutGroup: {
116
+ description: "The time, in milliseconds, to wait after sending a PUT request to a group, before sending the next PUT request. Default: 1000.",
117
+ type: "integer",
118
+ minimum: 0,
119
+ maximum: 1000
399
120
  },
400
- groupsDevice: {
401
- title: 'Device',
402
- type: 'string',
403
- oneOf: values.groups,
404
- required: true
121
+ waitTimeResend: {
122
+ description: "The time, in milliseconds, to wait before resending a request after an ECONNRESET or http status 503 error. Default: 300.",
123
+ type: "integer",
124
+ minimum: 100,
125
+ maximum: 1000
126
+ },
127
+ waitTimeReset: {
128
+ description: "The timeout in milliseconds, to wait before resetting a characteristic value. Default: 500.",
129
+ type: "integer",
130
+ minimum: 10,
131
+ maximum: 2000
132
+ },
133
+ waitTimeUpdate: {
134
+ description: "The time, in milliseconds, to wait for a change from HomeKit to another characteristic for the same light or group, before updating the deCONZ gateway. Default: 100.",
135
+ type: "integer",
136
+ minimum: 0,
137
+ maximum: 500
405
138
  }
406
139
  }
407
140
  },
408
141
  layout: [
142
+ "host",
143
+ "name",
409
144
  {
410
- type: 'fieldset',
411
- title: `${gateway.context.host} Gateway Settings`
412
- },
413
- 'expose',
414
- 'logLevel',
415
- {
416
- type: 'flex',
417
- 'flex-flow': 'row',
418
- title: 'Automatically Expose New',
419
- items: [
420
- 'lights',
421
- 'sensors',
422
- 'groups',
423
- 'schedules'
424
- ],
425
- condition: {
426
- functionBody: 'return model.expose'
427
- }
428
- },
429
- {
430
- type: 'fieldset',
145
+ type: "fieldset",
146
+ expandable: true,
147
+ title: "Advanced Settings",
148
+ description: "Don't change these, unless you understand what you're doing.",
431
149
  items: [
432
- {
433
- type: 'tabs',
434
- tabs: [
435
- {
436
- title: 'Lights',
437
- items: [
438
- 'lightsDevice'
439
- ]
440
- },
441
- {
442
- title: 'Sensors',
443
- items: [
444
- 'sensorsDevice'
445
- ]
446
- },
447
- {
448
- title: 'Groups',
449
- items: [
450
- 'groupsDevice'
451
- ]
452
- }
453
- ]
454
- }
455
- ],
456
- condition: {
457
- functionBody: 'return model.expose'
458
- }
150
+ "forceHttp",
151
+ "parallelRequests",
152
+ "stealth",
153
+ "timeout",
154
+ "waitTimePut",
155
+ "waitTimePutGroup",
156
+ "waitTimeResend",
157
+ "waitTimeReset",
158
+ "waitTimeUpdate"
159
+ ]
459
160
  }
460
161
  ]
461
- }, data, 'Device Settings', 'Done')
462
- form.onChange((form) => {})
463
- form.onSubmit((form) => {
464
- showFormDeviceSettings(gateway, form.lightsDevice)
465
- })
466
- form.onCancel((form) => {
467
- showFormGateways(gateway.context.host)
468
- })
469
- }
162
+ };
470
163
 
471
- async function showFormDeviceSettings (gateway, device) {
472
- homebridge.showSpinner()
473
- homebridge.hideSpinner()
474
- const form = homebridge.createForm({
475
- schema: {
476
- type: 'object',
477
- properties: {
478
- gateway: {
479
- type: 'string'
164
+ const myApp = createApp({
165
+ /**
166
+ * This is called when the app is loaded. It's the entry point.
167
+ */
168
+ async created() {
169
+ const config = await homebridge.getPluginConfig();
170
+
171
+ if (!config.length) {
172
+ // if no config yet, create the basic config required
173
+ this.pluginConfig = {
174
+ gateways: [],
175
+ };
176
+ } else {
177
+ // if config does exist, we pretty safely assume only one config block and take this out
178
+ this.pluginConfig = config[0];
179
+ if (!Array.isArray(this.pluginConfig.gateways)) {
180
+ this.pluginConfig.gateways = [];
181
+ }
182
+ }
183
+ },
184
+ data() {
185
+ /**
186
+ * This is reactive data - it can be accessed in any of the methods via this.key
187
+ * It can be used in the HTML directly via {{ key }}
188
+ */
189
+ return {
190
+ view: 'gateways',
191
+ selectedGateway: null,
192
+ pluginConfig: {
193
+ gateways: [],
480
194
  },
481
- device: {
482
- type: 'string'
195
+ }
196
+ },
197
+ watch: {
198
+ /**
199
+ * This handler will be called whenever the object of data.pluginConfig changes
200
+ * Doing it likes this means we don't need to worry about keeping the UI in sync with plugin changes
201
+ * This will take care of everything as long as we keep data.pluginConfig correct
202
+ */
203
+ pluginConfig: {
204
+ deep: true,
205
+ handler(newValue, oldValue) {
206
+ // need to do a deep copy to clean the object before sending it to the Homebridge UI
207
+ const config = JSON.parse(JSON.stringify(newValue))
208
+ homebridge.updatePluginConfig([config]);
483
209
  }
210
+ },
211
+ },
212
+ methods: {
213
+ /**
214
+ * These are methods that can be called from the HTML.
215
+ * eg. <button @click="methodName"> or <button @click="methodName(someArg)">
216
+ */
217
+ addGateway() {
218
+ // create an empty selected gateway
219
+ this.selectedGateway = {};
220
+
221
+ // set the view to edit-gateway
222
+ this.view = "edit-gateway";
223
+
224
+ // start the form
225
+ const gatewayForm = homebridge.createForm(gatewaySchema, {}, 'OK', 'Cancel');
226
+
227
+ gatewayForm.onChange((form) => {
228
+ // push changes as they happen into the selectedGateway object
229
+ this.selectedGateway = form;
230
+ });
231
+
232
+ gatewayForm.onSubmit((form) => {
233
+ // on save, push the new gateway object into the pluginConfig.gateways array
234
+ this.pluginConfig.gateways.push(form);
235
+ this.view = "gateways";
236
+ gatewayForm.end();
237
+ });
238
+
239
+ gatewayForm.onCancel((form) => {
240
+ this.view = "gateways";
241
+ gatewayForm.end();
242
+ });
243
+ },
244
+ editGateway(gateway) {
245
+ this.view = "edit-gateway";
246
+
247
+ // create a copy of the current gateway object
248
+ const source = JSON.parse(JSON.stringify(gateway))
249
+
250
+ // set the selectedGateway so we can access it in the template easily
251
+ this.selectedGateway = source;
252
+
253
+ // load the form
254
+ const gatewayForm = homebridge.createForm(gatewaySchema, source, 'OK', 'Cancel');
255
+
256
+ // on changes, update the selectedGateway
257
+ gatewayForm.onChange((form) => {
258
+ this.selectedGateway = form;
259
+ });
260
+
261
+ // on save, update the gateway object in the pluginConfig.gateways array
262
+ gatewayForm.onSubmit((form) => {
263
+ Object.assign(gateway, form);
264
+ this.view = "gateways";
265
+ gatewayForm.end();
266
+ });
267
+
268
+ // on cancel, just go back to the gateways view, not updating the pluginConfig.gateways array with any changes
269
+ gatewayForm.onCancel((form) => {
270
+ this.view = "gateways";
271
+ gatewayForm.end();
272
+ });
273
+ },
274
+ deleteGateway(index) {
275
+ // on delete, just remove it from the gateways array
276
+ this.pluginConfig.gateways.splice(index, 1);
277
+ },
278
+ connect() {
279
+ console.log('connect clicked for ', this.selectedGateway);
280
+ },
281
+ getApiKey() {
282
+ console.log('get api key clicked for ', this.selectedGateway);
484
283
  }
284
+
485
285
  }
486
- }, {
487
- gateway: gateway.context.host,
488
- device: device
489
- }, 'OK', 'Cancel')
490
- form.onChange((form) => {})
491
- form.onSubmit((form) => {
492
- showFormGatewaySettings(gateway)
493
- })
494
- form.onCancel((form) => {
495
- showFormGatewaySettings(gateway)
496
- })
497
- }
286
+ });
498
287
 
499
- (async () => {
500
- try {
501
- await showFormPluginConfig()
502
- } catch (error) {
503
- console.error(error)
504
- }
505
- })()
288
+ /**
289
+ * Watch for the ready event, then start the vue app
290
+ */
291
+ homebridge.addEventListener('ready', async () => {
292
+ console.log('ready')
293
+ myApp.mount('#app')
294
+ });
506
295
  </script>