iobroker.zigbee 3.1.5 → 3.2.0

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/admin/admin.js CHANGED
@@ -9,6 +9,7 @@ const Materialize = (typeof M !== 'undefined') ? M : Materialize,
9
9
  namespace = 'zigbee.' + instance,
10
10
  namespaceLen = namespace.length;
11
11
  let devices = [],
12
+ models = [],
12
13
  debugDevices = [],
13
14
  messages = [],
14
15
  map = {},
@@ -17,6 +18,7 @@ let devices = [],
17
18
  network,
18
19
  networkEvents,
19
20
  responseCodes = false,
21
+ localConfigData = {},
20
22
  groups = {},
21
23
  devGroups = {}, // eslint-disable-line prefer-const
22
24
  binding = [],
@@ -92,12 +94,12 @@ function keepAlive(callback) {
92
94
  }
93
95
 
94
96
  function startKeepalive() {
95
- return setInterval(keepAlive, 10000);
97
+ return setInterval(() => UpdateAdapterAlive(false), 120000);
96
98
  }
97
99
 
98
100
  function UpdateAdapterAlive(state) {
99
- if (connectionStatus.connected === state) return;
100
101
  connectionStatus.time = Date.now();
102
+ if (connectionStatus.connected === state) return;
101
103
  if (state) {
102
104
  $('#adapterStopped_btn').addClass('hide');
103
105
  $('#code_pairing').removeClass('disabled');
@@ -180,7 +182,11 @@ function getLQICls(value) {
180
182
 
181
183
  function sanitizeModelParameter(parameter) {
182
184
  const replaceByUnderscore = /[\s/]/g;
183
- return parameter.replace(replaceByUnderscore, '_');
185
+ try {
186
+ return parameter.replace(replaceByUnderscore, '_');
187
+ }
188
+ catch { /* intentionally empty*/ }
189
+ return parameter;
184
190
  }
185
191
 
186
192
  /////
@@ -189,88 +195,281 @@ function sanitizeModelParameter(parameter) {
189
195
  //
190
196
  ////
191
197
 
198
+ const LocalDataDisplayValues = {
199
+ unfoldedModels : [], // [{ model:plug01, devices: true/false, options: true/false}]
200
+ unfoldedDevices : [], // [{ device:0xdeadbeefdeadbeef, show: true/false}]
201
+ buttonSet: new Set(),
202
+ showModels: true,
203
+ sortedKeys : [],
204
+ sortMethod: function (a, b) { return 0 },
205
+ filterMethod: function (a) { return true },
206
+ }
192
207
 
193
- function getModelData(data) {
194
- console.warn(JSON.stringify(data));
195
- const devicesByModel = {};
196
- for (const dev of data) {
197
- const modelID = dev.info?.mapped?.model || dev.info.device.modelZigbee || dev.info.device.name || 'unknown';
198
- if (devicesByModel[modelID])
199
- devicesByModel[modelID].devices.push(dev);
200
- else devicesByModel[modelID] = {devices:[dev], icon:dev.common.icon};
208
+ function updateFoldModel(model, devices, options) {
209
+ const m = LocalDataDisplayValues.unfoldedModels.find((c) => c.model === model);
210
+ if (!m) {
211
+ const ml = {model, devices: (devices === undefined ? false: devices), options: (options === undefined ? false : options)}
212
+ LocalDataDisplayValues.unfoldedModels.push(ml)
213
+ return ml;;
201
214
  }
202
- console.warn(JSON.stringify(devicesByModel));
215
+ if (devices) m.devices = !m.devices;
216
+ if (options) m.options = !m.options;
217
+ return m;
218
+ }
219
+
220
+
221
+ function getModelData(data, models, keys) {
203
222
  const Html = [];
204
- // Html.push(`<ul class="collapsible">`);
205
- Html.push(`<ul class="collection">`)
206
- for (const key of Object.keys(devicesByModel)) {
207
- const model = devicesByModel[key];
208
- Html.push(`<li class="collection-item avatar>`);
209
- //Html.push(`<li>`)
210
- //Html.push(`<div class="collapsible-header"><img src=${model.iccon} alt="" class="circle" width="40" height="auto">&nbsp;Paired Models</div>`);
211
- //Html.push(`<div class="collapsile-body"<span>${getDeviceData(model.devices)}</span></div>`);
212
- Html.push(`<img src = ${model.iccon} alt="" class="circle" width="40" height="auto">`);
213
- Html.push(`<span class=title></p>`);
214
- Html.push(getDeviceData(model.devices).join('<br>'))
215
- Html.push(`</p><a href="#!" class="secondary-content"><i class="material-icons">grade</i></a></li>`)
216
- }
217
- Html.push('</ul>');
223
+ const s = new Set();
224
+ for (const k of keys) {
225
+ const model = models[k];
226
+ const key = model.model.model;
227
+ //console.warn(`getmodeldata: model is ${key}, sO: ${JSON.stringify(model.setOptions)}`);
228
+ const numOptions = Object.keys(model.setOptions).length + ((typeof model.setOptions.options === 'object' && model.setOptions.options != null) ? Object.keys(model.setOptions.options).length-1 : 0);
229
+ const foldData = updateFoldModel(key, undefined, undefined);
230
+ let numrows = 1;
231
+ if (foldData.devices) numrows += model.devices.length;
232
+ if (numOptions > 0) numrows += 1;
233
+ if (foldData.options) numrows += numOptions;
234
+ const d_btn_name = `d_toggle_${k}`;
235
+ const e_btn_name = `m_edit_${k}`;
236
+ const d_btn_tip = `fold / unfold devices of ${key}`;
237
+ const e_btn_tip = `edit model ${key}`;
238
+ const d_btn = btnParam(d_btn_name, d_btn_tip, foldData.devices ? 'expand_less' : 'expand_more', false);
239
+ const e_btn = btnParam(e_btn_name, e_btn_tip, 'edit', 'green', false)
240
+ LocalDataDisplayValues.buttonSet.add(d_btn_name);
241
+ LocalDataDisplayValues.buttonSet.add(e_btn_name);
242
+ const devtxt = (model.devices.length && !foldData.devices) ? `${model.devices.length} ${model.model.type}${model.devices.length > 1 ? 's' : ''}` : '';
243
+ Html.push(`<tr id="datarowodd">
244
+ <td rowspan="${numrows}" width="15%"><img src=${model.model.icon} class="dev_list"></td>
245
+ <td colspan="2">Model ${key}</td><td>${devtxt}</td>
246
+ <td>${d_btn}&nbsp;${e_btn}</td></tr>`)
247
+ let cnt = 0;
248
+ if (foldData.devices) {
249
+ let isOdd = false;
250
+ for (const dev of model.devices) {
251
+ let devieee = dev._id.replace(`${namespace}.`, '');
252
+
253
+ if (devieee == undefined) devieee = 'unknown' + cnt++;
254
+ //LocalDataDisplayValues.buttonSet.add(`d_delete_${devieee}`);
255
+ LocalDataDisplayValues.buttonSet.add(`d_delall_${k}-${devieee}`);
256
+ LocalDataDisplayValues.buttonSet.add(`d_disen_${k}-${devieee}`);
257
+
258
+ //const bn = btnParam(`d_delete_${devieee}`, `delete device ${devieee}`, 'delete', 'red darken-4', false);
259
+ const bna = btnParam(`d_delall_${k}-${devieee}`, `completely delete device ${devieee}`, 'delete_forever', 'red accent-4', false);
260
+ const bta = !dev.common.deactivated ? btnParam(`d_disen_${k}-${devieee}`, `disable device ${devieee}`, 'power_settings_new', 'green accent-4', false) : btnParam(`d_disen_${k}-${devieee}`, `enable device ${devieee}`, 'power_settings_new', 'red accent-4', false);
261
+ Html.push(`<tr id="datarow${isOdd ? 'opt':'even'}${dev.common.deactivated ? '_red' : ''}"><td width="1%"><i class="material-icons small">devices</i></td><td width="25%">${devieee}</td><td width="45%">${dev.common.name}</td><td width="10%">${bna}${bta}<td></tr>`)
262
+ isOdd = !isOdd;
263
+ }
264
+ }
265
+ if (numOptions > 0) {
266
+ const o_btn_name = `o_toggle_${k}`;
267
+ const o_btn_tip = `fold / unfold options for Model ${key}`;
268
+ LocalDataDisplayValues.buttonSet.add(o_btn_name);
269
+ const opttxt = (numOptions > 0 && !(foldData.options)) ? `${numOptions} global option${numOptions > 1 ? 's' : ''}` :''
270
+ Html.push(`<tr id="datarowodd">
271
+ <td colspan="2">Model ${key}</td><td>${opttxt}</td>
272
+ <td>${btnParam(o_btn_name, o_btn_tip, foldData.options ? 'expand_less' : 'expand_more')}</td></tr>`)
273
+ if (foldData.options) {
274
+ let isOdd = false;
275
+ for (const key of Object.keys(model.setOptions)) {
276
+ if (typeof model.setOptions[key] === 'object') {
277
+ const oo = model.setOptions[key];
278
+ for (const ok of Object.keys(oo)) {
279
+ LocalDataDisplayValues.buttonSet.add(`o_delete_${k}-${ok}`);
280
+ const btn = btnParam(`o_delete_${k}-${ok}`, `delete option ${ok}`, 'delete', 'red darken-4', false);
281
+ Html.push(`<tr id="datarow${isOdd ? 'opt':'even'}"><td width="1%"><i class="material-icons small">blur_circular</i></td><td width="25%">${ok}</td><td width="45%" ${oo[ok] === undefined ? 'id="datared">"not set on model"' : '>'+oo[ok]}</td><td>${btn}</td></tr>`)
282
+ isOdd = !isOdd;
283
+ }
284
+ }
285
+ else {
286
+ LocalDataDisplayValues.buttonSet.add(`l_delete_${k}-${key}`);
287
+ const btn = btnParam(`l_delete_${k}-${key}`, `delete option ${key}`, 'delete', 'red darken-4', false);
288
+ if (key==='icon') {
289
+ const icontext = model.setOptions[key] === undefined ? 'id="datared">"not set on model"' : `>${model.setOptions[key]}`;
290
+ const icon = model.setOptions[key]=== undefined ? '' : `<img src=${model.setOptions[key]} height="32px" class="sml_list">`;
291
+ Html.push(`<tr id="datarow${isOdd ? 'opt':'even'}"><td width="1%"><i class="material-icons small">blur_circular</i></td><td width="25%">${key}</td><td valign="middle" width="45%" ${icontext}</td><td>${btn}${icon}</td></tr>`)
292
+ }
293
+ else
294
+ Html.push(`<tr id="datarow${isOdd ? 'opt':'even'}"><td width="1%"><i class="material-icons small">blur_circular</i></td><td width="25%">${key}</td><td width="45%" ${model.setOptions[key] === undefined ? 'id="datared">"not set on model"' : '>'+model.setOptions[key]}</td><td>${btn}</td></tr>`)
295
+ isOdd = !isOdd;
296
+ }
297
+ }
298
+ }
299
+ }
300
+
301
+ }
218
302
  return Html;
219
303
  }
304
+
305
+ function btnParam(id, tooltip, icon, color, disabled) {
306
+ return `<a id="${id}" class="btn-floating waves-effect waves-light right ${color ? color : 'blue'} ${disabled ? 'disabled ' : ''}tooltipped center-align hoverable translateT" title="${tooltip}"><i class="material-icons large">${icon}</i></a>`;
307
+ }
308
+
309
+
220
310
  function getDeviceData(deviceList, withIcon) {
221
- const Html = [`<div class="container">`];
222
- for (const dev of deviceList) {
223
- const iconLink = `<img src=${dev.common.icon} class="circle" width="40" height="auto">`;
224
- Html.push(`<div="row"><div class="col s4">${withIcon ? iconLink : ''}<br>${dev.info.device.ieee}<br>connectedInfo</div>`)
225
- Html.push(`<div class=col s4>Device Name:${dev.common.name}</div><div class=col s4>Connected: true</div></div>`);
226
- if (dev.options) {
227
- Html.push(`<div="row"><div class="col s3">Options</div>`)
311
+ const Html = [];
312
+ return Html;
313
+ /*for (const dev of deviceList) {
314
+ const rowspan = dev.options ? Object.keys(dev.options).length + 2 : 2;
315
+ const iconLink = `<img src=${dev.common.icon} class="dev_list">`;
316
+ const devieee = dev._id.replace(`${namespace}.`, '');
317
+ const o_btn_name = `do_toggle_${devieee}`;
318
+ const o_btn_tip = `fold / unfold options for ${devieee}`;
319
+ LocalDataDisplayValues.buttonSet.add(o_btn_name);
320
+ const bn = `f_edit_${devieee}`
321
+ LocalDataDisplayValues.buttonSet.add(bn);
322
+
323
+ Html.push(`<tr id="datarowodd"><td rowspan="${rowspan}">${iconLink}</td><td colspan="2">${dev.common.name} (${devieee})</td><td>${btnParam(o_btn_name, o_btn_tip, LocalDataDisplayValues.unfoldedDevices ? 'do_not_disturb' : 'add_circle')}</td></tr>`);
324
+ Html.push(`<tr id="dataroweven"><td colspan="2">Device flags</td><td>${btnParam(bn, 'edit flags','edit')}</td></tr>`);
325
+ //console.warn(`dev is ${JSON.stringify(dev)}`);
326
+ if (dev.options && LocalDataDisplayValues.unfoldedDevices[devieee]) {
228
327
  for (const o of dev.options) {
229
- Html.push(`<div class=col s4>${o.key}</div><div class=col s4>${o.value}</div><div>`);
328
+ const bn = `o_edit_${devieee}.${o.key}`
329
+ LocalDataDisplayValues.buttonSet.add(bn);
330
+ Html.push(`<tr id="datarowopt"><td>${o.key}></td><td>${o.value}</td><td>${btnParam(bn, 'edit flags','edit')}</td></tr>`);
230
331
  }
231
- Html.push(`</div>`);
232
332
  }
233
- Html.push(`</div>`)
234
333
  }
235
- Html.push(`</div>`)
236
- return Html;
334
+ return Html;*/
237
335
  }
238
- function getGlobalOptionData() {
239
- return ['No Data Yet'];
336
+
337
+ function sortAndFilter(filter, sort) {
338
+ const fFun = filter || LocalDataDisplayValues.filterMethod;
339
+ console.warn('once:='+JSON.stringify(models['m_0'].setOptions))
340
+ console.warn('twice:='+ JSON.stringify(models['m_1'].setOptions))
341
+ let filterMap = LocalDataDisplayValues.sortedKeys = Object.keys(models);
342
+ if (LocalDataDisplayValues.searchVal && LocalDataDisplayValues.searchVal.length) {
343
+ filterMap = filterMap.filter((a) => {
344
+ return models[a]?.model?.model?.toLowerCase().includes(LocalDataDisplayValues.searchVal)
345
+ });
346
+ console.warn(`${JSON.stringify(LocalDataDisplayValues.searchVal)} - ${JSON.stringify(models['m_1'].model)}`);
347
+ }
348
+ if (typeof fFun == 'function') {
349
+ console.warn(`${JSON.stringify(filterMap)} - ${JSON.stringify(models['m_1'].model)}`);
350
+ filterMap = filterMap.filter(fFun);
351
+ }
352
+ console.warn(JSON.stringify(filterMap));
353
+ const sFun = sort || LocalDataDisplayValues.sortMethod;
354
+ if (typeof sFun == 'function') {
355
+ console.warn(`${JSON.stringify(filterMap)} - ${JSON.stringify(models['m_1'].model)}`);
356
+ filterMap = filterMap.sort(sFun);
357
+ }
358
+ console.warn(JSON.stringify(filterMap));
359
+ if (typeof filter == 'function') LocalDataDisplayValues.filterMethod = filter;
360
+ if (typeof sort == 'function') LocalDataDisplayValues.sortMethod = sort;
361
+ return filterMap;
240
362
  }
241
363
 
242
364
  function showLocalData() {
243
- return;
244
- /*
365
+ LocalDataDisplayValues.buttonSet.clear();
366
+ ;
367
+ const ModelHtml = getModelData(devices, models, sortAndFilter(undefined, undefined));
368
+ const DeviceHtml = getDeviceData(devices);
369
+ const sm = LocalDataDisplayValues.showModels;
370
+ //const dmtoggle = btnParam('t_all_models', 'Refresh models', 'developer_board');
371
+
372
+ const RowSpan = sm ? ModelHtml.length +2 : DeviceHtml.length + 2;
245
373
  const Html = [];
246
374
 
247
- Html.push(`<ul class="collapsible">`);
248
- Html.push('<li>')
249
- Html.push (`<li class="active"><div class="collapsible-header">
250
- Paired Models
251
- </div>`);
252
- Html.push (`<div class="collapsible-body">
253
- <span>${getModelData(devices).join('')}</span>
254
- </div>`);
255
- Html.push ('</li><li>')
256
- Html.push (`<div class="collapsible-header">
257
- Paired Devices
258
- </div>`);
259
- Html.push (`<div class="collapsible-body">
260
- <span>${getDeviceData(devices, true).join('')}</span>
261
- </div>`);
262
- Html.push ('</li><li>')
263
- Html.push (`<div class="collapsible-header">
264
- Global Options
265
- </div>`);
266
- Html.push (`<div class="collapsible-body">
267
- <span>${getGlobalOptionData(devices).join('')}</span>
268
- </div>`);
269
- Html.push ('</li>')
270
- Html.push (`</ul>`);
271
- $('#tab-overrides').html(Html.join(''));
272
- $('.collapsible').collapsible();
273
- */
375
+ if (sm) {
376
+ Html.push(`<table style="width:100%"><tr id="datatable"><th rowspan="${RowSpan}">&nbsp;</th><th colspan=4></th><th></th><th rowspan="${RowSpan}">&nbsp;</th></tr>`);
377
+ Html.push(ModelHtml.join(''));
378
+ }
379
+ /*else {
380
+ Html.push(`<table style="width:100%"><tr id="datatable"><th rowspan="${RowSpan}">&nbsp;</th><th colspan=4>Device Data</th><th>${dmtoggle}</th><th rowspan="${RowSpan}">&nbsp;</th></tr>`)
381
+ Html.push(DeviceHtml.join(''));
382
+ }*/
383
+ Html.push(`<tr id="datatable"><td colspan="5"></td></tr>`)
384
+ Html.push('</table>');
385
+ //Html.push('</div></div>');
386
+ $('#tab-overrides-content').html(Html.join(''));
387
+
388
+ /*$('#t_all_models').click(function () {
389
+ //LocalDataDisplayValues.showModels = !LocalDataDisplayValues.showModels;
390
+ getDevices();
391
+ });*/
392
+
393
+ //console.warn(`lddv is ${JSON.stringify(LocalDataDisplayValues)}`)
394
+ for (const item of LocalDataDisplayValues.buttonSet) {
395
+ if (item.startsWith('d_toggle_')) $(`#${item}`).click(function () {
396
+ const key = item.replace('d_toggle_', '');
397
+ //console.warn(`clicked ${item}`);
398
+ updateFoldModel(models[key].model.model, true, false)
399
+ showLocalData();
400
+ });
401
+ if (item.startsWith('o_toggle_')) $(`#${item}`).click(function () {
402
+ //console.warn(`clicked ${item}`);
403
+ const key = item.substring(9);
404
+ updateFoldModel(models[key].model.model, false, true)
405
+ showLocalData();
406
+ })
407
+ if (item.startsWith('do_toggle_')) $(`#${item}`).click(function () {
408
+ //console.warn(`clicked ${item}`);
409
+ const key = item.substring(10);
410
+ if (LocalDataDisplayValues.unfoldedDevices.hasOwnProperty(key))
411
+ LocalDataDisplayValues.unfoldedDevices[key] =! LocalDataDisplayValues.unfoldedDevices[key];
412
+ else
413
+ LocalDataDisplayValues.unfoldedDevices[key] = true;
414
+ showLocalData();
415
+ })
416
+ if (item.startsWith('m_edit_')) $(`#${item}`).click(function () {
417
+ //console.warn(`clicked ${item}`);
418
+ const key = item.substring(7);
419
+ editDeviceOptions(models[key], true);
420
+ })
421
+ if (item.startsWith('o_delete_')) {
422
+ console.warn(`adding click to ${item}`)
423
+ $(`#${item}`).click(function () {
424
+ console.warn(`clicked ${item}`);
425
+ const keys = item.replace('o_delete_', '').split('-');
426
+ const model = models[keys[0]]?.model.model;
427
+ const option = keys[1];
428
+ const sOptions = models[keys[0]]?.setOptions || {};
429
+ const options = models[keys[0]]?.setOptions?.options || {};
430
+ //options[option] = '##REMOVE##';
431
+ console.warn(`clicked ${item} - options are ${JSON.stringify(options)}`);
432
+ delete options[option];
433
+ updateLocalConfigItems(model, sOptions || {}, true);
434
+ showLocalData();
435
+ })
436
+ }
437
+ if (item.startsWith('l_delete_')) $(`#${item}`).click(function () {
438
+ const keys = item.replace('l_delete_', '').split('-');
439
+ const model = models[keys[0]]?.model.model;
440
+ const option = keys[1];
441
+ const options = models[keys[0]].setOptions;
442
+ options[option] = '##REMOVE##';
443
+ console.warn(`clicked ${item} - options are ${JSON.stringify(options)}`);
444
+ updateLocalConfigItems(model, options || {}, true)
445
+ delete options[option];
446
+ showLocalData();
447
+ })
448
+ if (item.startsWith('d_disen_')) {
449
+ console.warn(`adding click to ${item}`)
450
+ $(`#${item}`).click(function () {
451
+ console.warn(`clicked ${item}`);
452
+ const keys = item.replace('d_disen_', '').split('-');
453
+ const model = models[keys[0]];
454
+ const device = model.devices.find( (d) => d.native.id === keys[1]);
455
+ swapActive(keys[1]);
456
+ device.common.deactivated = !device.common.deactivated
457
+ showLocalData();
458
+
459
+
460
+ });
461
+ }
462
+ if (item.startsWith('d_delall_')) {
463
+ console.warn(`adding click to ${item}`)
464
+ $(`#${item}`).click(function () {
465
+ console.warn(`clicked ${item}`);
466
+ const keys = item.replace('d_delall_', '').split('-');
467
+ const model = models[keys[0]];
468
+ const device = model.devices.find( (d) => d.native.id === keys[1]);
469
+ deleteConfirmation(keys[1], device.common.name, keys[1], models[keys[0]].devices.count <=1 ? models[keys[0]]?.model.model : undefined);
470
+ });
471
+ }
472
+ }
274
473
  }
275
474
 
276
475
  /////
@@ -298,6 +497,7 @@ function getCard(dev) {
298
497
  rooms.push(dev.rooms[r]);
299
498
  }
300
499
  }
500
+ const NoInterviewIcon = dev.info?.device?.interviewstate != 'SUCCESSFUL' ? `<div class="col tool"><i class="material-icons icon-red">perm_device_information</i></div>` : ``;
301
501
  const paired = (dev.paired) ? '' : '<i class="material-icons right">leak_remove</i>';
302
502
  const rid = id.split('.').join('_');
303
503
  const modelUrl = (!type) ? '' : `<a href="https://www.zigbee2mqtt.io/devices/${type_url}.html" target="_blank" rel="noopener noreferrer">${type}</a>`;
@@ -321,9 +521,15 @@ function getCard(dev) {
321
521
  ${roomInfo}
322
522
  </ul>
323
523
  </div>`,
324
- deactBtn = `<button name="swapactive" class="right btn-flat btn-small tooltipped" title="${(isActive ? 'Deactivate' : 'Activate')}"><i class="material-icons ${(isActive ? 'icon-green' : 'icon-red')}">power_settings_new</i></button>`,
325
- debugBtn = `<button name="swapdebug" class="right btn-flat btn-small tooltipped" title="${(isDebug > -1 ? (isDebug > 0) ?'Automatic by '+debugDevices[isDebug-1]: 'Disable Debug' : 'Enable Debug')}"><i class="material-icons icon-${(isDebug > -1 ? (isDebug > 0 ? 'orange' : 'green') : 'gray')}">bug_report</i></button>`,
326
- infoBtn = (nwk) ? `<button name="info" class="left btn-flat btn-small"><i class="material-icons icon-blue">info</i></button>` : '';
524
+ deactBtn = `<button name="swapactive" class="right btn-flat btn-small tooltipped" title="${(isActive ? 'deactivate' : 'activate')}"><i class="material-icons ${(isActive ? 'icon-green' : 'icon-red')}">power_settings_new</i></button>`,
525
+ debugBtn = `<button name="swapdebug" class="right btn-flat btn-small tooltipped" title="${(isDebug > -1 ? (isDebug > 0) ?'automatic by '+debugDevices[isDebug-1]: 'disable debug' : 'enable debug')}"><i class="material-icons icon-${(isDebug > -1 ? (isDebug > 0 ? 'orange' : 'green') : 'gray')}">bug_report</i></button>`,
526
+ infoBtn = (nwk) ? `<button name="info" class="left btn-flat btn-small"><i class="material-icons icon-blue">info</i></button>` : '',
527
+ reconfigureButton = dev.info.mapped.hasConfigure ? `<button name="reconfigure" class="right btn-flat btn-small tooltipped" title="reconfigure">
528
+ <i class="material-icons icon-red">sync</i>
529
+ </button>` : ``,
530
+ groupButton = dev.info?.device?.isGroupable ? ` <button name="edit" class="right btn-flat btn-small tooltipped" title="edit group membership">
531
+ <i class="material-icons icon-black">group_work</i>
532
+ </button>` : ``;
327
533
 
328
534
  const dashCard = getDashCard(dev);
329
535
  const card = `<div id="${id}" class="device">
@@ -333,6 +539,7 @@ function getCard(dev) {
333
539
  <div class="card-content zcard">
334
540
  <div class="flip" style="cursor: pointer">
335
541
  <span class="top right small" style="border-radius: 50%">
542
+ ${NoInterviewIcon}
336
543
  ${battery}
337
544
  <!--${lq}-->
338
545
  ${status}
@@ -347,20 +554,15 @@ function getCard(dev) {
347
554
  <div class="card-action">
348
555
  <div class="card-reveal-buttons">
349
556
  ${infoBtn}
350
-
351
557
  <span class="left fw_info"></span>
352
- <button name="delete" class="right btn-flat btn-small tooltipped" title="Delete">
558
+ <button name="delete" class="right btn-flat btn-small tooltipped" title="delete device">
353
559
  <i class="material-icons icon-red">delete</i>
354
560
  </button>
355
- <button name="edit" class="right btn-flat btn-small tooltipped" title="Edit">
561
+ ${groupButton}
562
+ <button name="swapimage" class="right btn-flat btn-small tooltipped" title="edit device options">
356
563
  <i class="material-icons icon-black">edit</i>
357
564
  </button>
358
- <button name="swapimage" class="right btn-flat btn-small tooltipped" title="Select Image">
359
- <i class="material-icons icon-black">image</i>
360
- </button>
361
- <button name="reconfigure" class="right btn-flat btn-small tooltipped" title="Reconfigure">
362
- <i class="material-icons icon-red">sync</i>
363
- </button>
565
+ ${reconfigureButton}
364
566
  ${deactBtn}
365
567
  ${debugBtn}
366
568
  </div>
@@ -395,7 +597,7 @@ function getCoordinatorCard(dev) {
395
597
  <li><span class="label">ZHC / ZH:</span><span>${coordinatorinfo.converters} / ${coordinatorinfo.herdsman}</span></li>
396
598
  </ul>
397
599
  </div>`,
398
- permitJoinBtn = '<div class="col tool"><button name="joinCard" class="waves-effect btn-small btn-flat right hoverable green"><i class="material-icons icon-green">leak_add</i></button></div>',
600
+ permitJoinBtn = '<div class="col tool"><button name="joinCard" class="waves-effect btn-small btn-flat right hoverable green tooltipped" title="open network"><i class="material-icons icon-green">leak_add</i></button></div>',
399
601
  //permitJoinBtn = `<div class="col tool"><button name="join" class="btn-floating-sml waves-effect waves-light right hoverable green><i class="material-icons">leak_add</i></button></div>`,
400
602
  card = `<div id="${id}" class="device">
401
603
  <div class="card hoverable">
@@ -448,6 +650,7 @@ function getGroupCard(dev) {
448
650
  ;
449
651
  info = info.concat(` ${roomInfo}</ul>
450
652
  </div>`);
653
+ const infoBtn = `<button name="info" class="left btn-flat btn-small"><i class="material-icons icon-blue">info</i></button>`;
451
654
  const image = `<img src="${dev.common.icon}" width="64px" onerror="this.onerror=null;this.src='img/unavailable.png';">`;
452
655
  const dashCard = getDashCard(dev, dev.common.icon, memberCount > 0);
453
656
  const card = `<div id="${id}" class="device group">
@@ -469,14 +672,15 @@ function getGroupCard(dev) {
469
672
  </div>
470
673
  <div class="card-action">
471
674
  <div class="card-reveal-buttons">
472
- <button name="deletegrp" class="right btn-flat btn-small">
473
- <i class="material-icons icon-black">delete</i>
675
+ ${infoBtn}
676
+ <button name="deletegrp" class="right btn-flat btn-small tooltipped" title="delete group">
677
+ <i class="material-icons icon-red">delete</i>
474
678
  </button>
475
- <button name="editgrp" class="right btn-flat btn-small">
476
- <i class="material-icons icon-green">edit</i>
679
+ <button name="editgrp" class="right btn-flat btn-small tooltipped" title="edit group members">
680
+ <i class="material-icons">group_work</i>
477
681
  </button>
478
- <button name="swapimage" class="right btn-flat btn-small tooltipped" title="Edit">
479
- <i class="material-icons icon-black">image</i>
682
+ <button name="swapimage" class="right btn-flat btn-small tooltipped" title="edit group options">
683
+ <i class="material-icons icon-black">edit</i>
480
684
  </button>
481
685
  </div>
482
686
  </div>
@@ -506,10 +710,11 @@ function getDashCard(dev, groupImage, groupstatus) {
506
710
  rooms = [],
507
711
  lang = systemLang || 'en';
508
712
  const paired = (dev.paired) ? '' : '<i class="material-icons right">leak_remove</i>';
509
- const permitJoinBtn = dev.battery || dev.common.type == 'group' ? '' : `<div class="col tool"><button name="joinCard" class="waves-effect btn-small btn-flat right hoverable green"><i class="material-icons icon-green">leak_add</i></button></div>`;
510
- const device_queryBtn = dev.battery || dev.common.type == 'group' ? '' : `<div class="col tool"><button name="deviceQuery" class="waves-effect btn-small btn-flat right hoverable green"><i class="material-icons icon-green">play_for_work</i></button></div>`;
713
+ const permitJoinBtn = dev.info?.device?.type == 'EndDevice' || dev.common.type == 'group' ? '' : `<div class="col tool"><button name="joinCard" class="waves-effect btn-small btn-flat right hoverable green tooltipped" title="open network on device"><i class="material-icons icon-green">leak_add</i></button></div>`;
714
+ const device_queryBtn = dev.info?.device?.type == 'EndDevice' || dev.common.type == 'group' ? '' : `<div class="col tool"><button name="deviceQuery" class="waves-effect btn-small btn-flat right hoverable green tooltipped" title="trigger device query"><i class="material-icons icon-green">play_for_work</i></button></div>`;
511
715
  const rid = id.split('.').join('_');
512
716
  const modelUrl = (!type) ? '' : `<a href="https://www.zigbee2mqtt.io/devices/${type}.html" target="_blank" rel="noopener noreferrer">${type}</a>`;
717
+ const NoInterviewIcon = (dev.info?.device?.interviewstate != 'SUCCESSFUL' && dev.common.type != 'group') ? `<div class="col tool"><i class="material-icons icon-red">perm_device_information</i></div>` : ``;
513
718
  const image = `<img src="${img_src}" width="64px" onerror="this.onerror=null;this.src='img/unavailable.png';">`,
514
719
  nwk = (dev.info && dev.info.device) ? dev.info.device.nwk : undefined,
515
720
  battery_cls = getBatteryCls(dev.battery),
@@ -574,6 +779,7 @@ function getDashCard(dev, groupImage, groupstatus) {
574
779
  </span>
575
780
  <div class="flip">
576
781
  <span class="top right small" style="border-radius: 50%">
782
+ ${NoInterviewIcon}
577
783
  ${idleTime}
578
784
  ${battery}
579
785
  ${lq}
@@ -719,7 +925,7 @@ function showDevInfo(id) {
719
925
  // section Confirmations
720
926
  //
721
927
  ////
722
- function deleteConfirmation(id, name) {
928
+ function deleteConfirmation(id, name, dev, model) {
723
929
  const text = translateWord('Do you really want to delete device') + ' "' + name + '" (' + id + ')?';
724
930
  $('#modaldelete').find('p').text(text);
725
931
  $('#force').prop('checked', false);
@@ -727,7 +933,7 @@ function deleteConfirmation(id, name) {
727
933
  $('#modaldelete a.btn[name=\'yes\']').unbind('click');
728
934
  $('#modaldelete a.btn[name=\'yes\']').click(() => {
729
935
  const force = $('#force').prop('checked');
730
- deleteZigbeeDevice(id, force);
936
+ deleteZigbeeDevice(id, force, dev, model);
731
937
  });
732
938
  $('#modaldelete').modal('open');
733
939
  Materialize.updateTextFields();
@@ -782,7 +988,7 @@ function EndPointIDfromEndPoint(ep) {
782
988
 
783
989
 
784
990
 
785
- function editName(id, name) {
991
+ function editGroupMembers(id, name) {
786
992
 
787
993
  function updateGroupables(groupables) {
788
994
  const html = [];
@@ -849,8 +1055,8 @@ function GenerateGroupChange(oldmembers, newmembers) {
849
1055
  return grpchng;
850
1056
  }
851
1057
 
852
- function deleteZigbeeDevice(id, force) {
853
- sendToWrapper(namespace, 'deleteZigbeeDevice', {id: id, force: force}, function (msg) {
1058
+ function deleteZigbeeDevice(id, force, devOpts, modelOpts) {
1059
+ sendToWrapper(namespace, 'deleteZigbeeDevice', {id: id, force: force, dev:devOpts, model:modelOpts}, function (msg) {
854
1060
  closeWaitingDialog();
855
1061
  if (msg) {
856
1062
  if (msg.error) {
@@ -860,7 +1066,7 @@ function deleteZigbeeDevice(id, force) {
860
1066
  }
861
1067
  }
862
1068
  });
863
- showWaitingDialog('Device is being removed', 10);
1069
+ showWaitingDialog('Device is being removed', 30);
864
1070
  }
865
1071
 
866
1072
 
@@ -966,6 +1172,7 @@ function showDevices() {
966
1172
  return room;
967
1173
  }
968
1174
  }).filter((item) => item != undefined));
1175
+ //console.warn(`rooms is ${JSON.stringify(allRooms)}`);
969
1176
  const roomSelector = $('#room-filter');
970
1177
  roomSelector.empty();
971
1178
  roomSelector.append(`<li class="device-order-item" data-type="All" tabindex="0"><a class="translate" data-lang="All">All</a></li>`);
@@ -988,12 +1195,14 @@ function showDevices() {
988
1195
 
989
1196
  const element = $('#devices');
990
1197
 
991
- if (element) {
1198
+ if ($('tab-main')) try {
992
1199
  shuffleInstance = devices && devices.length ? new Shuffle(element, {
993
1200
  itemSelector: '.device',
994
1201
  sizer: '.js-shuffle-sizer',
995
1202
  }) : undefined;
996
1203
  doFilter();
1204
+ } catch {
1205
+ // empty.
997
1206
  }
998
1207
 
999
1208
  const getDevName = function (dev_block) {
@@ -1015,7 +1224,7 @@ function showDevices() {
1015
1224
  const dev_block = $(this).parents('div.device');
1016
1225
  const id = getDevId(dev_block);
1017
1226
  const name = getDevName(dev_block);
1018
- editName(id, name);
1227
+ editGroupMembers(id, name);
1019
1228
  });
1020
1229
  $('.card-reveal-buttons button[name=\'swapdebug\']').click(function () {
1021
1230
  const dev_block = $(this).parents('div.device');
@@ -1027,7 +1236,7 @@ function showDevices() {
1027
1236
  $('.card-reveal-buttons button[name=\'swapimage\']').click(function () {
1028
1237
  const dev_block = $(this).parents('div.device');
1029
1238
  const id = getDevId(dev_block);
1030
- selectImageOverride(id);
1239
+ editDeviceOptions(id, false);
1031
1240
  });
1032
1241
 
1033
1242
  $('.card-reveal-buttons button[name=\'editgrp\']').click(function () {
@@ -1048,12 +1257,6 @@ function showDevices() {
1048
1257
  sendTo(namespace, 'setState', {id: `${getDevId(dev_block)}.device_query`, val: true}, function (data) {
1049
1258
  //console.log(data);
1050
1259
  }); });
1051
- $('#modalpairing a.btn[name=\'extendpairing\']').click(function () {
1052
- openNetwork();
1053
- });
1054
- $('#modalpairing a.btn[name=\'endpairing\']').click(function () {
1055
- stopPairing();
1056
- });
1057
1260
  $('.card-reveal-buttons button[name=\'info\']').click(function () {
1058
1261
  const dev_block = $(this).parents('div.device');
1059
1262
  showDevInfo(getDevId(dev_block));
@@ -1146,22 +1349,22 @@ function letsPairingWithCode(code) {
1146
1349
  }
1147
1350
 
1148
1351
  function openNetwork() {
1149
- messages = [];
1150
1352
  sendToWrapper(namespace, 'letsPairing', {stop:false}, function (msg) {
1151
1353
  if (msg && msg.error) {
1152
1354
  showMessage(msg.error, _('Error'));
1153
1355
  }
1154
- else showPairingProcess();
1356
+ //else showPairingProcess();
1155
1357
  });
1156
1358
  }
1157
1359
 
1158
1360
  function stopPairing() {
1159
- messages = [];
1160
1361
  sendToWrapper(namespace, 'letsPairing', {stop:true}, function (msg) {
1161
1362
  if (msg && msg.error) {
1162
1363
  showMessage(msg.error, _('Error'));
1163
1364
  }
1164
1365
  });
1366
+ $('#pairing').html('<i class="material-icons">leak_add</i>');
1367
+
1165
1368
  }
1166
1369
 
1167
1370
  function touchlinkReset() {
@@ -1226,21 +1429,24 @@ async function toggleDebugDevice(id) {
1226
1429
  }
1227
1430
 
1228
1431
  function updateLocalConfigItems(device, data, global) {
1229
- sendToWrapper(namespace, 'updateLocalConfigItems', {target: device, data:data, global:global}, function(msg) {
1230
- if (msg && msg.hasOwnProperty.error) {
1231
- showMessage(msg.error, _('Error'));
1232
- }
1233
- getDevices();
1234
- });
1432
+ if (data != {})
1433
+ sendToWrapper(namespace, 'updateLocalConfigItems', {target: device, data:data, global:global}, function(msg) {
1434
+ if (msg && msg.hasOwnProperty.error) {
1435
+ showMessage(msg.error, _('Error'));
1436
+ }
1437
+ getDevices();
1438
+ });
1235
1439
  }
1236
1440
 
1237
- async function selectImageOverride(id) {
1441
+ async function editDeviceOptions(id, isModel) {
1442
+ //console.warn(`selectImageOverride on ${JSON.stringify(id)}`);
1238
1443
 
1239
1444
  // start local functions
1240
1445
  function removeOption(k) {
1446
+ const model = dialogData.model;
1241
1447
  if (k && device_options.hasOwnProperty(k)) {
1242
- if (dev.info.mapped && dev.info.mapped.options && dev.info.mapped.options.includes(device_options[k].key))
1243
- availableOptions.push(device_options[k].key)
1448
+ if (device_options[k].key === 'legacy' || (model && model.options && model.options.includes(device_options[k].key)) && !device_options[k].isCustom)
1449
+ dialogData.availableOptions.push(device_options[k].key)
1244
1450
  delete device_options[k];
1245
1451
  }
1246
1452
  }
@@ -1253,12 +1459,15 @@ async function selectImageOverride(id) {
1253
1459
  key = `o${idx++}`;
1254
1460
  }
1255
1461
  while (device_options.hasOwnProperty(key));
1256
- device_options[key] = { key:optionName, value:''};
1257
- idx = availableOptions.indexOf(optionName);
1258
- if (idx > -1) availableOptions.splice(idx, 1);
1462
+ device_options[key] = { key:optionName, value:'', isCustom:optionName==='custom', expose:getExposeFromOptions(optionName)};
1463
+ idx = dialogData.availableOptions.indexOf(optionName);
1464
+ if (idx > -1 && !device_options[key].isCustom) dialogData.availableOptions.splice(idx, 1);
1465
+ console.warn(`addOption added ${JSON.stringify(device_options)}`)
1259
1466
  }
1260
1467
 
1468
+
1261
1469
  function updateOptions(candidates) {
1470
+ console.warn(`update Options with ${JSON.stringify(candidates)}`)
1262
1471
  if (candidates.length > 0) {
1263
1472
  $('#chooseimage').find('.new_options_available').removeClass('hide');
1264
1473
  list2select('#option_Selector', candidates, [], (key, val) => { return val; }, (key, val) => { return val; })
@@ -1267,54 +1476,123 @@ async function selectImageOverride(id) {
1267
1476
  $('#chooseimage').find('.new_options_available').addClass('hide');
1268
1477
  }
1269
1478
  const html_options=[];
1479
+ const checkboxButtons = [];
1270
1480
 
1271
1481
  for (const k of Object.keys(device_options)) {
1482
+ const expose = device_options[k].expose === undefined ? getExposeFromOptions(device_options[k].key) : device_options[k].expose;
1483
+ const disabled = device_options[k]?.isCustom ? '' : 'disabled ';
1484
+ console.warn(`option for ${k} is ${JSON.stringify(device_options[k])}`);
1272
1485
  html_options.push(`<div class="row">`);
1273
- html_options.push(`<div class="input-field suffix col s5 m5 l5"><input disabled id="option_key_${k}" type="text" class="value" /><label for="option_key_${k}">Option</label></div>`)
1274
- html_options.push(`<div class="input-field suffix col s5 m5 l5"><input id="option_value_${k}" type="text" class="value" /><label for="option_value_${k}">Value</label></div>`)
1275
- html_options.push(`<div class="col"><a id="option_rem_${k}" class="btn-large round red" ><i class="material-icons icon-red">remove_circle</i></a></div>`);
1486
+ switch (expose.type) {
1487
+ case 'numeric':
1488
+ html_options.push(`<div class="input-field col s5 m5 l5"><input ${disabled}id="option_key_${k}" type="text" class="value" /><label for="option_key_${k}">Option</label></div>`)
1489
+ html_options.push(`<div class="input-field col s5 m5 l5"><input id="option_value_${k}" type="number"${expose.value_min != undefined ? ' min="'+expose.value_min+'"' : ''}${expose.value_max != undefined ? ' max="'+expose.value_max+'"' : ''}${expose.value_step != undefined ? ' step="'+expose.value_step+'"' : ''} class="value" /><label>${expose.label ? expose.label : 'Value'}</label></div>`)
1490
+ break;
1491
+ case 'binary': {
1492
+ html_options.push(`<div class="input-field col s5 m5 l5">
1493
+ <input ${disabled}id="option_key_${k}" type="text" class="value" />
1494
+ <label for="option_key_${k}">Option</label></div>`);
1495
+ const dok = device_options[k];
1496
+ if (dok.vOn=== undefined) dok.vOn = (expose.value_on === undefined ? 'true' : String(expose.value_on));
1497
+ if (dok.vOff=== undefined) dok.vOff = (expose.value_off === undefined ? 'false' : String(expose.value_off));
1498
+ if (dok.value != dok.vOn) dok.value = dok.vOff;
1499
+ html_options.push(`<div class="col s5 m5 l5"><a id="option_value_${k}" class="btn-large value">${dok.value}</a></div>`);
1500
+ checkboxButtons.push(`option_value_${k}`);
1501
+ break;
1502
+ }
1503
+ default:
1504
+ html_options.push(`<div class="input-field col s5 m5 l5"><input ${disabled}id="option_key_${k}" type="text" class="value" /><label for="option_key_${k}">Option</label></div>`)
1505
+ html_options.push(`<div class="input-field col s5 m5 l5"><input id="option_value_${k}" type="text" class="value" /><label for="option_value_${k}">${expose.label ? expose.label : 'Value'}</label></div>`)
1506
+ break;
1507
+ }
1508
+ html_options.push(`<div class="col"><a id="option_rem_${k}" class="btn-large round red " ><i class="material-icons icon-red">remove_circle</i></a></div>`);
1276
1509
  html_options.push(`</div>`)
1277
1510
  }
1278
1511
  $('#chooseimage').find('.options_grid').html(html_options.join(''));
1512
+ for (const item of checkboxButtons) {
1513
+ $(`#${item}`).unbind('click');
1514
+ $(`#${item}`).click(() => {
1515
+ const key = item.replace('option_value_', '');
1516
+ const dok = device_options[key];
1517
+ const oval = $(`#option_value_${key}`).html();
1518
+ const val = $(`#option_value_${key}`).html()=== dok.vOn ? dok.vOff : dok.vOn;
1519
+ dok.value = val;
1520
+ console.warn(`${item} clicked: ${JSON.stringify(dok)} => ${val} from ${oval}`);
1521
+ $(`#${item}`).html(val);
1522
+ });
1523
+ }
1524
+
1279
1525
  if (html_options.length > 0) {
1280
1526
  for (const k of Object.keys(device_options)) {
1527
+ if (device_options[k].isCustom) $(`#option_key_${k}`).removeClass('disabled')
1281
1528
  $(`#option_key_${k}`).val(device_options[k].key);
1282
- $(`#option_value_${k}`).val(device_options[k].value);
1529
+ if (device_options[k].expose?.type != 'binary') {
1530
+ const value = $(`#option_value_${k}.value`);
1531
+ /* if (value.attr('type') === 'checkbox') {
1532
+ console.warn(`oval for ${k} : ${device_options[k].value}`);
1533
+ value.prop('checked', Boolean(device_options[k].value));
1534
+ }
1535
+ else*/
1536
+ value.val(device_options[k].value);
1537
+ }
1283
1538
  $(`#option_rem_${k}`).unbind('click');
1284
- $(`#option_rem_${k}`).click(() => { removeOption(k); updateOptions(availableOptions) });
1539
+ $(`#option_rem_${k}`).click(() => {
1540
+ removeOption(k);
1541
+ updateOptions(dialogData.availableOptions);
1542
+ });
1285
1543
  }
1286
1544
  }
1287
1545
  }
1288
1546
 
1547
+ function getExposeFromOptions(option) {
1548
+ const rv = dialogData.model.optionExposes.find((expose) => expose.name === option);
1549
+ console.warn(`GEFO: ${option} results in ${JSON.stringify(rv)}`);
1550
+ if (rv) return rv;
1551
+ return { type:option === 'legacy' ? 'binary' : 'string' };
1552
+ }
1553
+
1289
1554
  function getOptionsFromUI(_do, _so) {
1290
1555
  const _no = {};
1291
1556
  let changed = false;
1557
+ console.warn(`${changed} : ${JSON.stringify(_do)} - ${JSON.stringify(_no)}`)
1292
1558
  for (const k of Object.keys(_do)) {
1293
1559
  const key = $(`#option_key_${k}`).val();
1294
- _do[k].key = key;
1295
- const val = $(`#option_value_${k}`).val();
1296
- try {
1297
- _do[k].value = JSON.parse(val);
1560
+ if (_do[k].isCustom) _do[k].key = key;
1561
+ else if (_do[k].key != key) {
1562
+ console.warn(`_illegal Keys: ${key}, ${_do[k].key}`)
1563
+ continue;
1298
1564
  }
1299
- catch {
1300
- _do[k].value = val;
1565
+ console.warn(`_legal Keys: ${key}, ${_do[k].key}`)
1566
+ if (_do[k].expose?.type === 'binary') {
1567
+ _do[k].value = $(`#option_value_${k}`).html();
1301
1568
  }
1302
- if (device_options[k].key.length > 0) {
1303
- _no[key] = device_options[k].value;
1304
- changed |= _no[key] != _so[key];
1569
+ else
1570
+ {
1571
+ const val = $(`#option_value_${k}`).val();
1572
+ try {
1573
+ _do[k].value = JSON.parse(val);
1574
+ }
1575
+ catch {
1576
+ _do[k].value = val;
1577
+ }
1578
+ }
1579
+ if (_do[k].key.length > 0) {
1580
+ console.warn(`dok: ${_do[k].key} : ${_do[k].value}`);
1581
+ _no[key] = _do[k].value;
1582
+ changed |= (_no[key] != _so[key]);
1305
1583
  }
1306
1584
  }
1307
1585
  changed |= (Object.keys(_no).length != Object.keys(_so).length);
1308
- console.warn(`${changed} : ${JSON.stringify(_do)} - ${JSON.stringify(_no)}`)
1586
+ console.warn(`${changed ? 'changed': 'unchanged'} : ${JSON.stringify(_so)} - ${JSON.stringify(_no)}`)
1309
1587
  if (changed) return _no;
1310
1588
  return undefined;
1311
1589
  }
1312
1590
 
1313
- function updateImageSelection(dev, imagedata) {
1314
- const default_icon = (dev.common.type === 'group' ? dev.common.modelIcon : `img/${dev.common.type.replace(/\//g, '-')}.png`);
1315
- if (dev.legacyIcon) imagedata.unshift( { file:dev.legacyIcon, name:'legacy', data:dev.legacyIcon});
1316
- imagedata.unshift( { file:'none', name:'default', data:default_icon});
1317
- imagedata.unshift( { file:'current', name:'current', data:dev.common.icon || dev.icon});
1591
+ function updateImageSelection(dData, imagedata) {
1592
+ // const default_icon = (dev.common.type === 'group' ? dev.common.modelIcon : `img/${dev.common.type.replace(/\//g, '-')}.png`);
1593
+ if (dData.legacyIcon) imagedata.unshift( { file:dData.legacyIcon, name:'legacy', data:dData.legacyIcon});
1594
+ imagedata.unshift( { file:'none', name:'default', data:dData.defaultIcon});
1595
+ imagedata.unshift( { file:'current', name:'current', data:dData.icon});
1318
1596
 
1319
1597
  list2select('#images', imagedata, selectItems,
1320
1598
  function (key, image) {
@@ -1337,39 +1615,65 @@ async function selectImageOverride(id) {
1337
1615
  const device_options = {};
1338
1616
  const received_options = {};
1339
1617
 
1340
- const dev = devices.find((d) => d._id == id);
1341
- const availableOptions = (dev.info.mapped ? dev.info.mapped.options.slice() || []:[]);
1342
- const imghtml = `<img src="${dev.common.icon || dev.icon}" width="80px">`
1618
+ const dialogData = {};
1619
+
1620
+ if (isModel) {
1621
+ const model = id.model;
1622
+ dialogData.model = model;
1623
+ dialogData.availableOptions = model.options.slice() || [];
1624
+ dialogData.availableOptions.push('custom');
1625
+ if (model.hasLegacyDef) dialogData.availableOptions.push('legacy');
1626
+ dialogData.setOptions = {};
1627
+ for (const k in Object.keys(id.setOptions))
1628
+ if (k == 'icon' || k == 'name') continue;
1629
+ else dialogData.setOptions[k] = id.setOptions[k];
1630
+ dialogData.name = id.setOptions.name || id.name || 'unset';
1631
+ dialogData.icon = id.setOptions.icon || model.icon || 'img/dummyDevice.jpg';
1632
+ dialogData.legacyIcon = id.devices[0].legacyIcon;
1633
+ id = id.model.model;
1634
+ } else
1635
+ {
1636
+ const dev = devices.find((d) => d._id == id);
1637
+ dialogData.model = dev.info.mapped;
1638
+ dialogData.availableOptions = (dev.info.mapped ? dev.info.mapped.options.slice() || []:[]);
1639
+ dialogData.name = dev.common.name;
1640
+ dialogData.icon = dev.common.icon || dev.icon;
1641
+ dialogData.default_icon = (dev.common.type === 'group' ? dev.common.modelIcon : `img/${dev.common.type.replace(/\//g, '-')}.png`);
1642
+ dialogData.legacyIcon = dev.legacyIcon;
1643
+ }
1644
+
1645
+ const imghtml = `<img src="${dialogData.icon}" width="80px">`;
1343
1646
  //console.error(imghtml)
1344
1647
  const selectItems= [''];
1345
- $('#chooseimage').find('input[id=\'d_name\']').val(dev.common.name);
1648
+ $('#chooseimage').find('input[id=\'d_name\']').val(dialogData.name);
1346
1649
  $('#chooseimage').find('.currentIcon').html(imghtml);
1347
1650
  $('#option_add_1084').unbind('click');
1348
1651
  $('#option_add_1084').click(() => {
1349
1652
  getOptionsFromUI(device_options, received_options);
1350
1653
  addOption();
1351
- updateOptions(availableOptions)
1654
+ updateOptions(dialogData.availableOptions);
1352
1655
  });
1353
1656
 
1354
1657
 
1355
1658
 
1356
1659
  sendToWrapper(namespace, 'getLocalImages', {}, function(msg) {
1357
1660
  if (msg && msg.imageData) {
1358
- updateImageSelection(dev, msg.imageData);
1661
+ updateImageSelection(dialogData , msg.imageData);
1359
1662
 
1360
1663
  $('#chooseimage a.btn[name=\'save\']').unbind('click');
1361
1664
  $('#chooseimage a.btn[name=\'save\']').click(() => {
1362
1665
  const image = $('#chooseimage').find('#images option:selected').val();
1363
- const global = $('#chooseimage').find('#globaloverride').prop('checked');
1666
+ //const global = $('#chooseimage').find('#globaloverride').prop('checked');
1364
1667
  const name = $('#chooseimage').find('input[id=\'d_name\']').val();
1365
1668
  const data = {};
1366
1669
  if (image != 'current') data.icon= image;
1367
- if (name != dev.common.name) data.name = name;
1368
- data.options = getOptionsFromUI(device_options, received_options)
1670
+ if (name != dialogData.name) data.name = name;
1671
+ const changedOptions = getOptionsFromUI(device_options, received_options);
1672
+ if (changedOptions != undefined) data.options = changedOptions;
1369
1673
 
1370
- updateLocalConfigItems(id, data, global);
1674
+ updateLocalConfigItems(id, data, isModel);
1371
1675
  });
1372
- sendToWrapper(namespace, 'getLocalConfigItems', { target:id, global:false, key:'options' }, function (msg) {
1676
+ sendToWrapper(namespace, 'getLocalConfigItems', { target:id, global:isModel, key:'options' }, function (msg) {
1373
1677
  if (msg) {
1374
1678
  if (msg.error) showMessage(msg.error, '_Error');
1375
1679
  Object.keys(device_options).forEach(key => delete device_options[key]);
@@ -1378,15 +1682,15 @@ async function selectImageOverride(id) {
1378
1682
  let cnt = 1;
1379
1683
  for (const key in msg.options)
1380
1684
  {
1381
- const idx = availableOptions.indexOf(key);
1685
+ const idx = dialogData.availableOptions.indexOf(key);
1382
1686
  console.warn(`key ${key} : index : ${idx}`);
1383
- if (idx > -1) availableOptions.splice(idx,1);
1687
+ if (idx > -1) dialogData.availableOptions.splice(idx,1);
1384
1688
  received_options[key]=msg.options[key];
1385
1689
  device_options[`o${cnt}`] = { key:key, value:msg.options[key]}
1386
1690
  cnt++;
1387
1691
  }
1388
1692
  }
1389
- updateOptions(availableOptions);
1693
+ updateOptions(dialogData.availableOptions);
1390
1694
  } else showMessage('callback without message');
1391
1695
  $('#chooseimage').modal('open');
1392
1696
  Materialize.updateTextFields();
@@ -1503,6 +1807,7 @@ function HtmlFromOutDebugMessages(messages, devID, filter) {
1503
1807
  }
1504
1808
 
1505
1809
  function displayDebugMessages(msg) {
1810
+ console.warn('displayDebugMessages called with '+ JSON.stringify(msg));
1506
1811
  const buttonNames = [];
1507
1812
  const idButtons = [];
1508
1813
  if (msg.byId) {
@@ -1692,55 +1997,33 @@ function getDevices() {
1692
1997
  coordinatorinfo = msg;
1693
1998
  updateStartButton()
1694
1999
  }
1695
- sendToWrapper(namespace, 'getDevices', {}, function (msg) {
1696
- if (msg) {
1697
- devices = msg.devices ? msg.devices : [];
1698
- // check if stashed error messages are sent alongside
1699
- if (msg.clean)
1700
- $('#state_cleanup_btn').removeClass('hide');
1701
- else
1702
- $('#state_cleanup_btn').addClass('hide');
1703
- if (msg.errors && msg.errors.length > 0) {
1704
- $('#show_errors_btn').removeClass('hide');
1705
- errorData = msg.errors;
1706
- }
1707
- else {
1708
- $('#show_errors_btn').addClass('hide');
1709
- }
1710
- let newDebugMessages = false;
1711
-
1712
- //check if debug messages are sent alongside
1713
- if (msg && typeof (msg.debugDevices == 'array')) {
1714
- debugDevices = msg.debugDevices;
1715
- }
1716
- else
1717
- debugDevices = [];
1718
- if (debugMessages.byId) {
1719
- newDebugMessages = true;
1720
- debugMessages.byId = msg;
1721
- if (msg) displayDebugMessages(debugMessages)
1722
- }
1723
- lockout.isActive = false;
1724
- if (msg.error) {
1725
- errorData.push(msg.error);
1726
- isHerdsmanRunning = false;
1727
- } else {
1728
- isHerdsmanRunning = true;
1729
- if (!newDebugMessages) {
1730
- getDebugMessages();
1731
- }
1732
- //getExclude();
1733
- getBinding();
1734
- }
1735
- updateStartButton();
1736
- showDevices();
1737
- showLocalData();
1738
- UpdateAdapterAlive(true)
2000
+ });
2001
+ sendToWrapper(namespace, 'getLocalConfigItems', {getAllData:true}, function(msg) {
2002
+ if (msg.hasOwnProperty('by_id') && msg.hasOwnProperty('by_model'))
2003
+ localConfigData = msg;
2004
+ })
2005
+ sendToWrapper(namespace, 'getDevices', {}, function (msg) {
2006
+ if (msg) {
2007
+ extractDevicesData(msg);
2008
+ if (msg.error) {
2009
+ errorData.push(msg.error);
2010
+ isHerdsmanRunning = false;
2011
+ } else {
2012
+ isHerdsmanRunning = true;
2013
+ getBinding();
1739
2014
  }
1740
- });
2015
+ updateStartButton();
2016
+ displayDebugMessages(debugMessages);
2017
+ showDevices();
2018
+ LocalDataDisplayValues.sortedKeys = Object.keys(models);
2019
+ showLocalData();
2020
+ UpdateAdapterAlive(true)
2021
+ }
1741
2022
  });
1742
2023
  }
1743
2024
 
2025
+
2026
+
1744
2027
  if (lockout.timeoutid) {
1745
2028
  clearTimeout(lockout.timeoutid);
1746
2029
  }
@@ -1753,6 +2036,38 @@ function getDevices() {
1753
2036
 
1754
2037
  }
1755
2038
 
2039
+ function extractDevicesData(msg) {
2040
+ devices = msg.devices ? msg.devices : [];
2041
+ // check if stashed error messages are sent alongside
2042
+ if (msg.clean)
2043
+ $('#state_cleanup_btn').removeClass('hide');
2044
+ else
2045
+ $('#state_cleanup_btn').addClass('hide');
2046
+ if (msg.errors && msg.errors.length > 0) {
2047
+ $('#show_errors_btn').removeClass('hide');
2048
+ errorData = msg.errors;
2049
+ }
2050
+ else {
2051
+ $('#show_errors_btn').addClass('hide');
2052
+ }
2053
+ //check if debug messages are sent alongside
2054
+ if (msg && typeof (msg.debugDevices == 'array')) {
2055
+ debugDevices = msg.debugDevices;
2056
+ }
2057
+ else
2058
+ debugDevices = [];
2059
+
2060
+ if (msg.deviceDebugData) {
2061
+ debugMessages = { byId: msg.deviceDebugData };
2062
+ displayDebugMessages(debugMessages);
2063
+ }
2064
+ if (msg.models) models = msg.models;
2065
+ lockout.isActive = false;
2066
+
2067
+ }
2068
+
2069
+
2070
+
1756
2071
  function getNamedColors() {
1757
2072
  sendToWrapper(namespace, 'getNamedColors', {}, function(msg) {
1758
2073
  if (msg && typeof msg.colors) {
@@ -1862,7 +2177,7 @@ function load(settings, onChange) {
1862
2177
  getComPorts(onChange);
1863
2178
 
1864
2179
  //dialog = new MatDialog({EndingTop: '50%'});
1865
- const keepAliveHandle = startKeepalive();
2180
+ //const keepAliveHandle = startKeepalive();
1866
2181
  keepAlive(() => {
1867
2182
  getDevices();
1868
2183
  getNamedColors();
@@ -1871,6 +2186,10 @@ function load(settings, onChange) {
1871
2186
  groups = data.groups || {};
1872
2187
  //showGroups();
1873
2188
  });
2189
+ sendToWrapper(namespace, 'getLibData', {key: 'cidList'}, function (data) {
2190
+ cidList = data.list;
2191
+ });
2192
+
1874
2193
  })
1875
2194
 
1876
2195
  //getDebugMessages();
@@ -1924,8 +2243,10 @@ function load(settings, onChange) {
1924
2243
  });
1925
2244
  $('#pairing').click(function () {
1926
2245
  if (!$('#pairing').hasClass('pulse')) {
2246
+ messages = [];
1927
2247
  openNetwork();
1928
- } else showPairingProcess();
2248
+ }
2249
+ showPairingProcess();
1929
2250
  });
1930
2251
 
1931
2252
  $('#refresh').click(function () {
@@ -1940,6 +2261,10 @@ function load(settings, onChange) {
1940
2261
  resetConfirmation();
1941
2262
  });
1942
2263
 
2264
+ $('#restore-backup-btn').click(function () {
2265
+ selectBackup();
2266
+ });
2267
+
1943
2268
  $('#deleteNVRam-btn').click(function () {
1944
2269
  deleteNvBackupConfirmation();
1945
2270
  });
@@ -2007,6 +2332,65 @@ function load(settings, onChange) {
2007
2332
  $('#device-filter-btn').text($(this).text());
2008
2333
  doFilter();
2009
2334
  });
2335
+ $('#model-search').keyup(function (event) {
2336
+ LocalDataDisplayValues.searchVal = event.target.value.toLowerCase();
2337
+ if (!LocalDataDisplayValues.searchTimeout)
2338
+ LocalDataDisplayValues.searchTimeout = setTimeout(() => { LocalDataDisplayValues.searchTimeout = null; showLocalData(); }, 250);
2339
+ });
2340
+ $('#model-sort a').click(function () {
2341
+ const t = $(this).text();
2342
+ $('#model-sort-btn').text(t);
2343
+ switch (t) {
2344
+ case 'by type':
2345
+ LocalDataDisplayValues.sortMethod = function(a,b) {
2346
+ if (models[a].model?.type == models[b].model?.type) return (models[a].model?.model > models[b].model?.model ? 1 : -1);
2347
+ return (models[a].model?.type > models[b].model?.type ? 1 : -1);
2348
+ };
2349
+ break;
2350
+
2351
+ case 'by device count':
2352
+ LocalDataDisplayValues.sortMethod = function(a,b) {
2353
+ if (models[a].setOptions?.length == models[b].setOptions?.length) return (models[a].model?.model > models[b].model?.model ? 1 : -1);
2354
+ return (models[a].setOptions?.length > models[b].setOptions?.length?1:-1);
2355
+ };
2356
+ break;
2357
+ case 'by option count':
2358
+ LocalDataDisplayValues.sortMethod = function(a,b) {
2359
+ if (models[a].devices?.length == models[b].devices?.length) return (models[a].model?.model > models[b].model?.model ? 1 : -1);
2360
+ return (models[a].devices?.length > models[b].devices?.length ? 1 : -1);
2361
+ };
2362
+ break;
2363
+ default:
2364
+ LocalDataDisplayValues.sortMethod = undefined;
2365
+ }
2366
+ showLocalData();
2367
+ });
2368
+ $('#refresh_models_btn').click(function () {
2369
+ getDevices();
2370
+ });
2371
+ $('#model-filter a').click(function () {
2372
+ const t = $(this).text();
2373
+ $('#model-filter-btn').text(t);
2374
+ switch (t) {
2375
+ case 'Groups':
2376
+ LocalDataDisplayValues.filterMethod = function(a) { return models[a].model.model== 'group'};
2377
+ break;
2378
+ case 'Routers':
2379
+ LocalDataDisplayValues.filterMethod = function(a) { return models[a].model.type == 'Router'};
2380
+ break;
2381
+ case 'End Devices':
2382
+ LocalDataDisplayValues.filterMethod = function(a) { return models[a].model.type == 'EndDevice'};
2383
+ break;
2384
+ case 'with options':
2385
+ LocalDataDisplayValues.filterMethod = function(a) { return models[a].setOptions && Object.keys(models[a].setOptions).length > 0 };
2386
+ break;
2387
+ case 'without options':
2388
+ LocalDataDisplayValues.filterMethod = function(a) { return !(models[a].setOptions && Object.keys(models[a].setOptions).length > 0) };
2389
+ break;
2390
+ default: LocalDataDisplayValues.filterMethod = undefined;
2391
+ }
2392
+ showLocalData();
2393
+ });
2010
2394
  });
2011
2395
 
2012
2396
  const text = $('#pairing').attr('data-tooltip');
@@ -2058,6 +2442,14 @@ function showPairingProcess(noextrabuttons) {
2058
2442
  dismissible: false
2059
2443
  });
2060
2444
 
2445
+ $('#modalpairing a.btn[name=\'extendpairing\']').unbind('click');
2446
+ $('#modalpairing a.btn[name=\'extendpairing\']').click(function () {
2447
+ openNetwork();
2448
+ });
2449
+ $('#modalpairing a.btn[name=\'endpairing\']').unbind('click');
2450
+ $('#modalpairing a.btn[name=\'endpairing\']').click(function () {
2451
+ stopPairing();
2452
+ });
2061
2453
  if (noextrabuttons) {
2062
2454
  $('#modalpairing').find('.endpairing').addClass('hide');
2063
2455
  $('#modalpairing').find('.extendpairing').addClass('hide');
@@ -2185,6 +2577,7 @@ socket.emit('subscribeObjects', namespace + '.*');
2185
2577
 
2186
2578
  // react to changes
2187
2579
  socket.on('stateChange', function (id, state) {
2580
+ UpdateAdapterAlive(true);
2188
2581
  // only watch our own states
2189
2582
  if (id.substring(0, namespaceLen) !== namespace) return;
2190
2583
  if (state) {
@@ -2220,6 +2613,9 @@ socket.on('stateChange', function (id, state) {
2220
2613
  isHerdsmanRunning = false;
2221
2614
  updateStartButton();
2222
2615
  }
2616
+ if (state.val === 'Closing network.') {
2617
+ getDevices();
2618
+ }
2223
2619
  }
2224
2620
  } else {
2225
2621
  const devId = getDevId(id);
@@ -2247,6 +2643,7 @@ socket.on('stateChange', function (id, state) {
2247
2643
 
2248
2644
 
2249
2645
  socket.on('objectChange', function (id, obj) {
2646
+ UpdateAdapterAlive(true);
2250
2647
  if (id.substring(0, namespaceLen) !== namespace) return;
2251
2648
  if (obj && obj.type == 'device') { // && obj.common.type !== 'group') {
2252
2649
  updateDevice(id);
@@ -2257,6 +2654,7 @@ socket.on('objectChange', function (id, obj) {
2257
2654
  if (elems.length === 3) {
2258
2655
  removeDevice(id);
2259
2656
  showDevices();
2657
+ showLocalData();
2260
2658
  }
2261
2659
  }
2262
2660
  });
@@ -2324,7 +2722,7 @@ function showNetworkMap(devices, map) {
2324
2722
  node.label = 'Coordinator';
2325
2723
  // delete node.color;
2326
2724
  }
2327
- console.warn(`node for device ${JSON.stringify(node)}`)
2725
+ //console.warn(`node for device ${JSON.stringify(node)}`)
2328
2726
  return node;
2329
2727
  };
2330
2728
 
@@ -3173,6 +3571,42 @@ function resetConfirmation() {
3173
3571
  });
3174
3572
  }
3175
3573
 
3574
+
3575
+ function selectBackup() {
3576
+ sendToWrapper(namespace, 'listbackups', {}, function(msg) {
3577
+ const candidates = {};
3578
+ for (const fn of msg.files) {
3579
+ const m = fn.matchAll(/backup_([0-9]+)_([0-9]+)_([0-9]+)-([0-9]+)_([0-9]+)_([0-9]+)/gm);
3580
+ console.warn(`m is ${JSON.stringify(m)}`);
3581
+ if (m) {
3582
+ candidates[`${m[3]}.${m[2]}.${m[1]} ${m[4]}:${m[5]}`] = fn;
3583
+ }
3584
+ }
3585
+ console.warn('candidates is ' + JSON.stringify(candidates));
3586
+ list2select('#backup_Selector', msg.files, [], (key, val) => { return val; }, (key, val) => { return val; })
3587
+ $('#modalrestore').modal('open');
3588
+ const btn = $('#modalrestore .modal-content a.btn-large');
3589
+ btn.unbind('click')
3590
+ btn.click(function (e) {
3591
+ const name = $('#backup_Selector').val();
3592
+ console.warn(` filename is ${name}`);
3593
+ $('#modalrestore').modal('close');
3594
+ showWaitingDialog(`Attempting to restore the backup from ${name}`, 180000);
3595
+ const start = Date.now();
3596
+ sendToWrapper(namespace, 'restore', {name}, function(msg) {
3597
+ closeWaitingDialog();
3598
+ if (msg.error) {
3599
+ showMessage(msg.error, _('Error'))
3600
+ }
3601
+ else {
3602
+ const duration = Date.now() - start;
3603
+ showMessage(`Restored configuration from backup after ${duration / 1000} s`, 'Restore successful');
3604
+ }
3605
+ })
3606
+ })
3607
+ })
3608
+
3609
+ }
3176
3610
  function showViewConfig() {
3177
3611
  $('#modalviewconfig').modal('open');
3178
3612
  }
@@ -3559,14 +3993,13 @@ function genDevInfo(device) {
3559
3993
  if (value === undefined) {
3560
3994
  return '';
3561
3995
  } else {
3562
- return `<li><span class="label">${name.replace('_',' ')}:</span><span>${value}</span></li>`;
3996
+ const label = `${name=='' ? '&nbsp;' : name + ':'}`;
3997
+ return `<li><span class="label">${label.replace('_',' ')}</span><span>${value}</span></li>`;
3563
3998
  }
3564
3999
  };
3565
4000
  const genRowValues = function (name, value) {
3566
- if (value === undefined) {
3567
- return '';
3568
- } else {
3569
- let label = `${name}:`;
4001
+ if (Array.isArray(value)) {
4002
+ let label = `${name=='' ? '&nbsp;' : name + ':'}`;
3570
4003
  try {
3571
4004
  return value.map((val) => {
3572
4005
  const row = `<li><span class="label">${label}</span><span>${val}</span></li>`;
@@ -3578,6 +4011,7 @@ function genDevInfo(device) {
3578
4011
  return `<li><span class="label">${label}</span><span>${JSON.stringify(value)}</span></li>`
3579
4012
  }
3580
4013
  }
4014
+ else return '';
3581
4015
  };
3582
4016
  const modelUrl = (!mapped) ? '' : `<a href="https://www.zigbee2mqtt.io/devices/${sanitizeModelParameter(mapped.model)}.html" target="_blank" rel="noopener noreferrer">${mapped.model}</a>`;
3583
4017
  const mappedInfo = [];
@@ -3589,7 +4023,7 @@ function genDevInfo(device) {
3589
4023
  if (item == 'model')
3590
4024
  mappedInfo.push(genRow(item,modelUrl));
3591
4025
  else
3592
- mappedInfo.push(genRow(item,mapped[item]));
4026
+ if (typeof mapped[item] != 'object') mappedInfo.push(genRow(item,mapped[item]));
3593
4027
  }
3594
4028
  mappedInfo.push(
3595
4029
  ` </ul>
@@ -3611,7 +4045,7 @@ function genDevInfo(device) {
3611
4045
  const imgSrc = device.icon || device.common.icon;
3612
4046
  const imgInfo = (imgSrc) ? `<img src=${imgSrc} width='150px' onerror="this.onerror=null;this.src='img/unavailable.png';"><div class="divider"></div>` : '';
3613
4047
  const info =[
3614
- `<div class="col s12 m6 l6 xl6">
4048
+ `<div class="col ${device.memberinfo != undefined ? 's12 m12 l12 xl12':'s12 m6 l6 xl6'}">
3615
4049
  ${imgInfo}
3616
4050
  ${mappedInfo.join('')}
3617
4051
  <div class="divider"></div>
@@ -3620,8 +4054,19 @@ function genDevInfo(device) {
3620
4054
  for (const item in dev) {
3621
4055
  info.push(genRow(item, dev[item]));
3622
4056
  }
3623
- info.push(` ${genRow('configured', (device.isConfigured), true)}
3624
- </ul>
4057
+ if (device.memberinfo != undefined) {
4058
+ const memberCount = (device.memberinfo.length);
4059
+ if (memberCount != 1) info.push(genRow(`Members`, `${memberCount}`));
4060
+ for (let m = 0; m < device.memberinfo.length; m++) {
4061
+ const dev = getDeviceByIEEE(device.memberinfo[m].ieee);
4062
+ const epid = device.memberinfo[m].epid;
4063
+ const epname = Array.isArray(dev.info.endpoints) ? `:${dev.info.endpoints.find((item) => item.ID == epid)?.epName}` : undefined;
4064
+ info.push(genRow(`Member${memberCount > 1 ? ' ' + (m+1) : ''}`, `${device.memberinfo[m].device}${epname ? epname : ''} - ${device.memberinfo[m].ieee}.${epid}`));
4065
+ }
4066
+ info.push(`</div>
4067
+ </div>`);
4068
+ }
4069
+ else info.push(`${genRow('configured', (device.isConfigured), true)}</ul>
3625
4070
  </div>
3626
4071
  </div>
3627
4072
  <div class="col s12 m6 l6 xl6">
@@ -3951,20 +4396,22 @@ function sortByTitle(element) {
3951
4396
 
3952
4397
 
3953
4398
  function updateDevice(id) {
3954
- sendToWrapper(namespace, 'getDevice', {id: id}, function (msg) {
3955
- if (msg) {
3956
- const devs = msg.devices;
3957
- if (devs) {
3958
- if (devs.error) {
3959
- showMessage(devs.error, _('Error'));
3960
- } else {
3961
- removeDevice(id);
3962
- devs.forEach(dev => devices.push(dev));
3963
- showDevices();
4399
+ if (devices.length > 0)
4400
+ sendToWrapper(namespace, 'getDevice', {id: id}, function (msg) {
4401
+ if (msg) {
4402
+ const devs = msg.devices;
4403
+ if (devs) {
4404
+ if (devs.error) {
4405
+ showMessage(devs.error, _('Error'));
4406
+ } else {
4407
+ removeDevice(id);
4408
+ devs.forEach(dev => devices.push(dev));
4409
+ showDevices();
4410
+ }
3964
4411
  }
3965
4412
  }
3966
- }
3967
- });
4413
+ });
4414
+ else sendToWrapper(namespace, 'getDevices', {}, extractDevicesData)
3968
4415
  }
3969
4416
 
3970
4417
  function removeDevice(id) {
@@ -3978,7 +4425,8 @@ function removeDevice(id) {
3978
4425
  }
3979
4426
 
3980
4427
  function swapActive(id) {
3981
- const dev = getDeviceByID(id);
4428
+ const dev = getDeviceByID(id) || getDeviceByIEEE(`0x${id}`);
4429
+ console.warn(`swap_active for ${id} -> ${JSON.stringify(dev)}`);
3982
4430
  if (dev && dev.common) {
3983
4431
  dev.common.deactivated = !(dev.common.deactivated);
3984
4432
  sendToWrapper(namespace, 'setDeviceActivated', {id: id, deactivated: dev.common.deactivated}, function () {