iobroker.zigbee 3.1.6 → 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
@@ -94,12 +94,12 @@ function keepAlive(callback) {
94
94
  }
95
95
 
96
96
  function startKeepalive() {
97
- return setInterval(keepAlive, 10000);
97
+ return setInterval(() => UpdateAdapterAlive(false), 120000);
98
98
  }
99
99
 
100
100
  function UpdateAdapterAlive(state) {
101
- if (connectionStatus.connected === state) return;
102
101
  connectionStatus.time = Date.now();
102
+ if (connectionStatus.connected === state) return;
103
103
  if (state) {
104
104
  $('#adapterStopped_btn').addClass('hide');
105
105
  $('#code_pairing').removeClass('disabled');
@@ -185,7 +185,7 @@ function sanitizeModelParameter(parameter) {
185
185
  try {
186
186
  return parameter.replace(replaceByUnderscore, '_');
187
187
  }
188
- catch {}
188
+ catch { /* intentionally empty*/ }
189
189
  return parameter;
190
190
  }
191
191
 
@@ -196,29 +196,41 @@ function sanitizeModelParameter(parameter) {
196
196
  ////
197
197
 
198
198
  const LocalDataDisplayValues = {
199
- unfoldedModels : {}, // { plug01: {devices: true/false, options: true/false}}
200
- unfoldedDevices : {}, // { 0xdeadbeefdeadbeef: true/false}
199
+ unfoldedModels : [], // [{ model:plug01, devices: true/false, options: true/false}]
200
+ unfoldedDevices : [], // [{ device:0xdeadbeefdeadbeef, show: true/false}]
201
201
  buttonSet: new Set(),
202
202
  showModels: true,
203
+ sortedKeys : [],
204
+ sortMethod: function (a, b) { return 0 },
205
+ filterMethod: function (a) { return true },
206
+ }
207
+
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;;
214
+ }
215
+ if (devices) m.devices = !m.devices;
216
+ if (options) m.options = !m.options;
217
+ return m;
203
218
  }
204
219
 
205
220
 
206
- function getModelData(data, models) {
221
+ function getModelData(data, models, keys) {
207
222
  const Html = [];
208
- // Html.push(`<ul class="collapsible">`);
209
223
  const s = new Set();
210
- for (const k of Object.keys(models)) {
224
+ for (const k of keys) {
211
225
  const model = models[k];
212
226
  const key = model.model.model;
213
- console.warn(`getmodeldata: model is ${key}, sO: ${JSON.stringify(model.setOptions)}`);
214
- const numOptions = Object.keys(model.setOptions).length;
215
- const foldData = LocalDataDisplayValues.unfoldedModels[k] || { devices:false, options:false};
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);
216
230
  let numrows = 1;
217
231
  if (foldData.devices) numrows += model.devices.length;
218
232
  if (numOptions > 0) numrows += 1;
219
233
  if (foldData.options) numrows += numOptions;
220
- //const numrows = (foldData.devices ? model.devices.length : 0) + (foldData.options ? numOptions : 0) + numOptions > 0 ? 2 : 1;
221
- console.warn(`numrows is ${numrows} with ${model.devices.length} ${foldData.devices ? 'shown' : 'hidden'} devices and ${numOptions} options ${foldData.options ? 'shown' : 'hidden'}`);
222
234
  const d_btn_name = `d_toggle_${k}`;
223
235
  const e_btn_name = `m_edit_${k}`;
224
236
  const d_btn_tip = `fold / unfold devices of ${key}`;
@@ -227,25 +239,61 @@ function getModelData(data, models) {
227
239
  const e_btn = btnParam(e_btn_name, e_btn_tip, 'edit', 'green', false)
228
240
  LocalDataDisplayValues.buttonSet.add(d_btn_name);
229
241
  LocalDataDisplayValues.buttonSet.add(e_btn_name);
230
- Html.push(`<tr id="datarowodd"><td rowspan="${numrows}"><img src = ${model.icon} alt="" width="80" height="auto"></td><td colspan="2">Devices of Model ${key}</td><td>${d_btn}&nbsp;${e_btn}</td></tr>`)
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>`)
231
247
  let cnt = 0;
232
248
  if (foldData.devices) {
249
+ let isOdd = false;
233
250
  for (const dev of model.devices) {
234
251
  let devieee = dev._id.replace(`${namespace}.`, '');
235
252
 
236
253
  if (devieee == undefined) devieee = 'unknown' + cnt++;
237
- const bn = `d_edit_${devieee}`
238
- Html.push(`<tr id="datarowopt"><td>${devieee}</td><td>${dev.common.name}</td><td>${btnParam(bn, 'edit '+ devieee, 'edit', 'lime', true)}</td></tr>`)
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;
239
263
  }
240
264
  }
241
265
  if (numOptions > 0) {
242
266
  const o_btn_name = `o_toggle_${k}`;
243
267
  const o_btn_tip = `fold / unfold options for Model ${key}`;
244
268
  LocalDataDisplayValues.buttonSet.add(o_btn_name);
245
- Html.push(`<tr id="dataroweven"></td><td colspan="2">Options for ${key}</td><td>${btnParam(o_btn_name, o_btn_tip, foldData.options ? 'expand_less' : 'expand_more')}</td></tr>`)
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>`)
246
273
  if (foldData.options) {
274
+ let isOdd = false;
247
275
  for (const key of Object.keys(model.setOptions)) {
248
- Html.push(`<tr id="dataroweven"><td>${key}</td><td ${model.setOptions[key] === undefined ? 'id="datared">"not set on model"' : '>'+model.setOptions[key]}</td><td>&nbsp;</td></tr>`)
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
+ }
249
297
  }
250
298
  }
251
299
  }
@@ -261,7 +309,8 @@ function btnParam(id, tooltip, icon, color, disabled) {
261
309
 
262
310
  function getDeviceData(deviceList, withIcon) {
263
311
  const Html = [];
264
- for (const dev of deviceList) {
312
+ return Html;
313
+ /*for (const dev of deviceList) {
265
314
  const rowspan = dev.options ? Object.keys(dev.options).length + 2 : 2;
266
315
  const iconLink = `<img src=${dev.common.icon} class="dev_list">`;
267
316
  const devieee = dev._id.replace(`${namespace}.`, '');
@@ -282,58 +331,81 @@ function getDeviceData(deviceList, withIcon) {
282
331
  }
283
332
  }
284
333
  }
285
- return Html;
334
+ return Html;*/
335
+ }
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;
286
362
  }
287
363
 
288
364
  function showLocalData() {
289
365
  LocalDataDisplayValues.buttonSet.clear();
290
- const ModelHtml = getModelData(devices, models);
366
+ ;
367
+ const ModelHtml = getModelData(devices, models, sortAndFilter(undefined, undefined));
291
368
  const DeviceHtml = getDeviceData(devices);
292
369
  const sm = LocalDataDisplayValues.showModels;
293
- const dmtoggle = btnParam('t_all_models', sm ? 'fold Models / show Devices' : 'fold Devices / show Models', !sm ? 'developer_board' : 'devices_other')
370
+ //const dmtoggle = btnParam('t_all_models', 'Refresh models', 'developer_board');
294
371
 
295
372
  const RowSpan = sm ? ModelHtml.length +2 : DeviceHtml.length + 2;
296
373
  const Html = [];
374
+
297
375
  if (sm) {
298
- Html.push(`<table style="width:100%"><tr id="datatable"><th rowspan="${RowSpan}">&nbsp;</th><th colspan=3>Model Data</th><th>${dmtoggle}</th><th rowspan="${RowSpan}">&nbsp;</th></tr>`)
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>`);
299
377
  Html.push(ModelHtml.join(''));
300
378
  }
301
- else {
302
- Html.push(`<table style="width:100%"><tr id="datatable"><th rowspan="${RowSpan}">&nbsp;</th><th colspan=3>Device Data</th><th>${dmtoggle}</th><th rowspan="${RowSpan}">&nbsp;</th></tr>`)
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>`)
303
381
  Html.push(DeviceHtml.join(''));
304
- }
305
- Html.push(`<tr id="datatable"><td colspan="4">Statistics</td></tr>`)
382
+ }*/
383
+ Html.push(`<tr id="datatable"><td colspan="5"></td></tr>`)
306
384
  Html.push('</table>');
307
- $('#tab-overrides').html(Html.join(''));
385
+ //Html.push('</div></div>');
386
+ $('#tab-overrides-content').html(Html.join(''));
308
387
 
309
- $('#t_all_models').click(function () {
310
- LocalDataDisplayValues.showModels = !LocalDataDisplayValues.showModels;
311
- showLocalData();
312
- });
388
+ /*$('#t_all_models').click(function () {
389
+ //LocalDataDisplayValues.showModels = !LocalDataDisplayValues.showModels;
390
+ getDevices();
391
+ });*/
313
392
 
314
- console.warn(`lddv is ${JSON.stringify(LocalDataDisplayValues)}`)
393
+ //console.warn(`lddv is ${JSON.stringify(LocalDataDisplayValues)}`)
315
394
  for (const item of LocalDataDisplayValues.buttonSet) {
316
- console.warn(`adding click to ${item}`)
317
395
  if (item.startsWith('d_toggle_')) $(`#${item}`).click(function () {
318
- const key = item.substring(9);
319
- console.warn(`clicked ${item}`);
320
- if (LocalDataDisplayValues.unfoldedModels.hasOwnProperty(key))
321
- LocalDataDisplayValues.unfoldedModels[key].devices =! LocalDataDisplayValues.unfoldedModels[key].devices;
322
- else
323
- LocalDataDisplayValues.unfoldedModels[key] = { devices:true, options: false };
396
+ const key = item.replace('d_toggle_', '');
397
+ //console.warn(`clicked ${item}`);
398
+ updateFoldModel(models[key].model.model, true, false)
324
399
  showLocalData();
325
400
  });
326
401
  if (item.startsWith('o_toggle_')) $(`#${item}`).click(function () {
327
- console.warn(`clicked ${item}`);
402
+ //console.warn(`clicked ${item}`);
328
403
  const key = item.substring(9);
329
- if (LocalDataDisplayValues.unfoldedModels.hasOwnProperty(key))
330
- LocalDataDisplayValues.unfoldedModels[key].options = !LocalDataDisplayValues.unfoldedModels[key].options;
331
- else
332
- LocalDataDisplayValues.unfoldedModels[key] = { devices:false, options: true };
404
+ updateFoldModel(models[key].model.model, false, true)
333
405
  showLocalData();
334
406
  })
335
407
  if (item.startsWith('do_toggle_')) $(`#${item}`).click(function () {
336
- console.warn(`clicked ${item}`);
408
+ //console.warn(`clicked ${item}`);
337
409
  const key = item.substring(10);
338
410
  if (LocalDataDisplayValues.unfoldedDevices.hasOwnProperty(key))
339
411
  LocalDataDisplayValues.unfoldedDevices[key] =! LocalDataDisplayValues.unfoldedDevices[key];
@@ -341,10 +413,63 @@ function showLocalData() {
341
413
  LocalDataDisplayValues.unfoldedDevices[key] = true;
342
414
  showLocalData();
343
415
  })
344
-
345
- }
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();
346
458
 
347
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
+ }
348
473
  }
349
474
 
350
475
  /////
@@ -372,6 +497,7 @@ function getCard(dev) {
372
497
  rooms.push(dev.rooms[r]);
373
498
  }
374
499
  }
500
+ const NoInterviewIcon = dev.info?.device?.interviewstate != 'SUCCESSFUL' ? `<div class="col tool"><i class="material-icons icon-red">perm_device_information</i></div>` : ``;
375
501
  const paired = (dev.paired) ? '' : '<i class="material-icons right">leak_remove</i>';
376
502
  const rid = id.split('.').join('_');
377
503
  const modelUrl = (!type) ? '' : `<a href="https://www.zigbee2mqtt.io/devices/${type_url}.html" target="_blank" rel="noopener noreferrer">${type}</a>`;
@@ -395,9 +521,15 @@ function getCard(dev) {
395
521
  ${roomInfo}
396
522
  </ul>
397
523
  </div>`,
398
- 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>`,
399
- 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>`,
400
- 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>` : ``;
401
533
 
402
534
  const dashCard = getDashCard(dev);
403
535
  const card = `<div id="${id}" class="device">
@@ -407,6 +539,7 @@ function getCard(dev) {
407
539
  <div class="card-content zcard">
408
540
  <div class="flip" style="cursor: pointer">
409
541
  <span class="top right small" style="border-radius: 50%">
542
+ ${NoInterviewIcon}
410
543
  ${battery}
411
544
  <!--${lq}-->
412
545
  ${status}
@@ -421,20 +554,15 @@ function getCard(dev) {
421
554
  <div class="card-action">
422
555
  <div class="card-reveal-buttons">
423
556
  ${infoBtn}
424
-
425
557
  <span class="left fw_info"></span>
426
- <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">
427
559
  <i class="material-icons icon-red">delete</i>
428
560
  </button>
429
- <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">
430
563
  <i class="material-icons icon-black">edit</i>
431
564
  </button>
432
- <button name="swapimage" class="right btn-flat btn-small tooltipped" title="Select Image">
433
- <i class="material-icons icon-black">image</i>
434
- </button>
435
- <button name="reconfigure" class="right btn-flat btn-small tooltipped" title="Reconfigure">
436
- <i class="material-icons icon-red">sync</i>
437
- </button>
565
+ ${reconfigureButton}
438
566
  ${deactBtn}
439
567
  ${debugBtn}
440
568
  </div>
@@ -469,7 +597,7 @@ function getCoordinatorCard(dev) {
469
597
  <li><span class="label">ZHC / ZH:</span><span>${coordinatorinfo.converters} / ${coordinatorinfo.herdsman}</span></li>
470
598
  </ul>
471
599
  </div>`,
472
- 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>',
473
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>`,
474
602
  card = `<div id="${id}" class="device">
475
603
  <div class="card hoverable">
@@ -522,6 +650,7 @@ function getGroupCard(dev) {
522
650
  ;
523
651
  info = info.concat(` ${roomInfo}</ul>
524
652
  </div>`);
653
+ const infoBtn = `<button name="info" class="left btn-flat btn-small"><i class="material-icons icon-blue">info</i></button>`;
525
654
  const image = `<img src="${dev.common.icon}" width="64px" onerror="this.onerror=null;this.src='img/unavailable.png';">`;
526
655
  const dashCard = getDashCard(dev, dev.common.icon, memberCount > 0);
527
656
  const card = `<div id="${id}" class="device group">
@@ -543,14 +672,15 @@ function getGroupCard(dev) {
543
672
  </div>
544
673
  <div class="card-action">
545
674
  <div class="card-reveal-buttons">
546
- <button name="deletegrp" class="right btn-flat btn-small">
547
- <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>
548
678
  </button>
549
- <button name="editgrp" class="right btn-flat btn-small">
550
- <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>
551
681
  </button>
552
- <button name="swapimage" class="right btn-flat btn-small tooltipped" title="Edit">
553
- <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>
554
684
  </button>
555
685
  </div>
556
686
  </div>
@@ -580,10 +710,11 @@ function getDashCard(dev, groupImage, groupstatus) {
580
710
  rooms = [],
581
711
  lang = systemLang || 'en';
582
712
  const paired = (dev.paired) ? '' : '<i class="material-icons right">leak_remove</i>';
583
- 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>`;
584
- 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>`;
585
715
  const rid = id.split('.').join('_');
586
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>` : ``;
587
718
  const image = `<img src="${img_src}" width="64px" onerror="this.onerror=null;this.src='img/unavailable.png';">`,
588
719
  nwk = (dev.info && dev.info.device) ? dev.info.device.nwk : undefined,
589
720
  battery_cls = getBatteryCls(dev.battery),
@@ -648,6 +779,7 @@ function getDashCard(dev, groupImage, groupstatus) {
648
779
  </span>
649
780
  <div class="flip">
650
781
  <span class="top right small" style="border-radius: 50%">
782
+ ${NoInterviewIcon}
651
783
  ${idleTime}
652
784
  ${battery}
653
785
  ${lq}
@@ -793,7 +925,7 @@ function showDevInfo(id) {
793
925
  // section Confirmations
794
926
  //
795
927
  ////
796
- function deleteConfirmation(id, name) {
928
+ function deleteConfirmation(id, name, dev, model) {
797
929
  const text = translateWord('Do you really want to delete device') + ' "' + name + '" (' + id + ')?';
798
930
  $('#modaldelete').find('p').text(text);
799
931
  $('#force').prop('checked', false);
@@ -801,7 +933,7 @@ function deleteConfirmation(id, name) {
801
933
  $('#modaldelete a.btn[name=\'yes\']').unbind('click');
802
934
  $('#modaldelete a.btn[name=\'yes\']').click(() => {
803
935
  const force = $('#force').prop('checked');
804
- deleteZigbeeDevice(id, force);
936
+ deleteZigbeeDevice(id, force, dev, model);
805
937
  });
806
938
  $('#modaldelete').modal('open');
807
939
  Materialize.updateTextFields();
@@ -856,7 +988,7 @@ function EndPointIDfromEndPoint(ep) {
856
988
 
857
989
 
858
990
 
859
- function editName(id, name) {
991
+ function editGroupMembers(id, name) {
860
992
 
861
993
  function updateGroupables(groupables) {
862
994
  const html = [];
@@ -923,8 +1055,8 @@ function GenerateGroupChange(oldmembers, newmembers) {
923
1055
  return grpchng;
924
1056
  }
925
1057
 
926
- function deleteZigbeeDevice(id, force) {
927
- 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) {
928
1060
  closeWaitingDialog();
929
1061
  if (msg) {
930
1062
  if (msg.error) {
@@ -934,7 +1066,7 @@ function deleteZigbeeDevice(id, force) {
934
1066
  }
935
1067
  }
936
1068
  });
937
- showWaitingDialog('Device is being removed', 10);
1069
+ showWaitingDialog('Device is being removed', 30);
938
1070
  }
939
1071
 
940
1072
 
@@ -1040,7 +1172,7 @@ function showDevices() {
1040
1172
  return room;
1041
1173
  }
1042
1174
  }).filter((item) => item != undefined));
1043
- console.warn(`rooms is ${JSON.stringify(allRooms)}`);
1175
+ //console.warn(`rooms is ${JSON.stringify(allRooms)}`);
1044
1176
  const roomSelector = $('#room-filter');
1045
1177
  roomSelector.empty();
1046
1178
  roomSelector.append(`<li class="device-order-item" data-type="All" tabindex="0"><a class="translate" data-lang="All">All</a></li>`);
@@ -1092,7 +1224,7 @@ function showDevices() {
1092
1224
  const dev_block = $(this).parents('div.device');
1093
1225
  const id = getDevId(dev_block);
1094
1226
  const name = getDevName(dev_block);
1095
- editName(id, name);
1227
+ editGroupMembers(id, name);
1096
1228
  });
1097
1229
  $('.card-reveal-buttons button[name=\'swapdebug\']').click(function () {
1098
1230
  const dev_block = $(this).parents('div.device');
@@ -1104,7 +1236,7 @@ function showDevices() {
1104
1236
  $('.card-reveal-buttons button[name=\'swapimage\']').click(function () {
1105
1237
  const dev_block = $(this).parents('div.device');
1106
1238
  const id = getDevId(dev_block);
1107
- selectImageOverride(id);
1239
+ editDeviceOptions(id, false);
1108
1240
  });
1109
1241
 
1110
1242
  $('.card-reveal-buttons button[name=\'editgrp\']').click(function () {
@@ -1125,12 +1257,6 @@ function showDevices() {
1125
1257
  sendTo(namespace, 'setState', {id: `${getDevId(dev_block)}.device_query`, val: true}, function (data) {
1126
1258
  //console.log(data);
1127
1259
  }); });
1128
- $('#modalpairing a.btn[name=\'extendpairing\']').click(function () {
1129
- openNetwork();
1130
- });
1131
- $('#modalpairing a.btn[name=\'endpairing\']').click(function () {
1132
- stopPairing();
1133
- });
1134
1260
  $('.card-reveal-buttons button[name=\'info\']').click(function () {
1135
1261
  const dev_block = $(this).parents('div.device');
1136
1262
  showDevInfo(getDevId(dev_block));
@@ -1223,22 +1349,22 @@ function letsPairingWithCode(code) {
1223
1349
  }
1224
1350
 
1225
1351
  function openNetwork() {
1226
- messages = [];
1227
1352
  sendToWrapper(namespace, 'letsPairing', {stop:false}, function (msg) {
1228
1353
  if (msg && msg.error) {
1229
1354
  showMessage(msg.error, _('Error'));
1230
1355
  }
1231
- else showPairingProcess();
1356
+ //else showPairingProcess();
1232
1357
  });
1233
1358
  }
1234
1359
 
1235
1360
  function stopPairing() {
1236
- messages = [];
1237
1361
  sendToWrapper(namespace, 'letsPairing', {stop:true}, function (msg) {
1238
1362
  if (msg && msg.error) {
1239
1363
  showMessage(msg.error, _('Error'));
1240
1364
  }
1241
1365
  });
1366
+ $('#pairing').html('<i class="material-icons">leak_add</i>');
1367
+
1242
1368
  }
1243
1369
 
1244
1370
  function touchlinkReset() {
@@ -1303,21 +1429,24 @@ async function toggleDebugDevice(id) {
1303
1429
  }
1304
1430
 
1305
1431
  function updateLocalConfigItems(device, data, global) {
1306
- sendToWrapper(namespace, 'updateLocalConfigItems', {target: device, data:data, global:global}, function(msg) {
1307
- if (msg && msg.hasOwnProperty.error) {
1308
- showMessage(msg.error, _('Error'));
1309
- }
1310
- getDevices();
1311
- });
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
+ });
1312
1439
  }
1313
1440
 
1314
- async function selectImageOverride(id) {
1441
+ async function editDeviceOptions(id, isModel) {
1442
+ //console.warn(`selectImageOverride on ${JSON.stringify(id)}`);
1315
1443
 
1316
1444
  // start local functions
1317
1445
  function removeOption(k) {
1446
+ const model = dialogData.model;
1318
1447
  if (k && device_options.hasOwnProperty(k)) {
1319
- if (dev.info.mapped && dev.info.mapped.options && dev.info.mapped.options.includes(device_options[k].key))
1320
- 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)
1321
1450
  delete device_options[k];
1322
1451
  }
1323
1452
  }
@@ -1330,12 +1459,15 @@ async function selectImageOverride(id) {
1330
1459
  key = `o${idx++}`;
1331
1460
  }
1332
1461
  while (device_options.hasOwnProperty(key));
1333
- device_options[key] = { key:optionName, value:''};
1334
- idx = availableOptions.indexOf(optionName);
1335
- 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)}`)
1336
1466
  }
1337
1467
 
1468
+
1338
1469
  function updateOptions(candidates) {
1470
+ console.warn(`update Options with ${JSON.stringify(candidates)}`)
1339
1471
  if (candidates.length > 0) {
1340
1472
  $('#chooseimage').find('.new_options_available').removeClass('hide');
1341
1473
  list2select('#option_Selector', candidates, [], (key, val) => { return val; }, (key, val) => { return val; })
@@ -1344,54 +1476,123 @@ async function selectImageOverride(id) {
1344
1476
  $('#chooseimage').find('.new_options_available').addClass('hide');
1345
1477
  }
1346
1478
  const html_options=[];
1479
+ const checkboxButtons = [];
1347
1480
 
1348
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])}`);
1349
1485
  html_options.push(`<div class="row">`);
1350
- 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>`)
1351
- 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>`)
1352
- 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>`);
1353
1509
  html_options.push(`</div>`)
1354
1510
  }
1355
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
+
1356
1525
  if (html_options.length > 0) {
1357
1526
  for (const k of Object.keys(device_options)) {
1527
+ if (device_options[k].isCustom) $(`#option_key_${k}`).removeClass('disabled')
1358
1528
  $(`#option_key_${k}`).val(device_options[k].key);
1359
- $(`#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
+ }
1360
1538
  $(`#option_rem_${k}`).unbind('click');
1361
- $(`#option_rem_${k}`).click(() => { removeOption(k); updateOptions(availableOptions) });
1539
+ $(`#option_rem_${k}`).click(() => {
1540
+ removeOption(k);
1541
+ updateOptions(dialogData.availableOptions);
1542
+ });
1362
1543
  }
1363
1544
  }
1364
1545
  }
1365
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
+
1366
1554
  function getOptionsFromUI(_do, _so) {
1367
1555
  const _no = {};
1368
1556
  let changed = false;
1557
+ console.warn(`${changed} : ${JSON.stringify(_do)} - ${JSON.stringify(_no)}`)
1369
1558
  for (const k of Object.keys(_do)) {
1370
1559
  const key = $(`#option_key_${k}`).val();
1371
- _do[k].key = key;
1372
- const val = $(`#option_value_${k}`).val();
1373
- try {
1374
- _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;
1375
1564
  }
1376
- catch {
1377
- _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();
1568
+ }
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
+ }
1378
1578
  }
1379
- if (device_options[k].key.length > 0) {
1380
- _no[key] = device_options[k].value;
1381
- changed |= _no[key] != _so[key];
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]);
1382
1583
  }
1383
1584
  }
1384
1585
  changed |= (Object.keys(_no).length != Object.keys(_so).length);
1385
- console.warn(`${changed} : ${JSON.stringify(_do)} - ${JSON.stringify(_no)}`)
1586
+ console.warn(`${changed ? 'changed': 'unchanged'} : ${JSON.stringify(_so)} - ${JSON.stringify(_no)}`)
1386
1587
  if (changed) return _no;
1387
1588
  return undefined;
1388
1589
  }
1389
1590
 
1390
- function updateImageSelection(dev, imagedata) {
1391
- const default_icon = (dev.common.type === 'group' ? dev.common.modelIcon : `img/${dev.common.type.replace(/\//g, '-')}.png`);
1392
- if (dev.legacyIcon) imagedata.unshift( { file:dev.legacyIcon, name:'legacy', data:dev.legacyIcon});
1393
- imagedata.unshift( { file:'none', name:'default', data:default_icon});
1394
- 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});
1395
1596
 
1396
1597
  list2select('#images', imagedata, selectItems,
1397
1598
  function (key, image) {
@@ -1414,39 +1615,65 @@ async function selectImageOverride(id) {
1414
1615
  const device_options = {};
1415
1616
  const received_options = {};
1416
1617
 
1417
- const dev = devices.find((d) => d._id == id);
1418
- const availableOptions = (dev.info.mapped ? dev.info.mapped.options.slice() || []:[]);
1419
- 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">`;
1420
1646
  //console.error(imghtml)
1421
1647
  const selectItems= [''];
1422
- $('#chooseimage').find('input[id=\'d_name\']').val(dev.common.name);
1648
+ $('#chooseimage').find('input[id=\'d_name\']').val(dialogData.name);
1423
1649
  $('#chooseimage').find('.currentIcon').html(imghtml);
1424
1650
  $('#option_add_1084').unbind('click');
1425
1651
  $('#option_add_1084').click(() => {
1426
1652
  getOptionsFromUI(device_options, received_options);
1427
1653
  addOption();
1428
- updateOptions(availableOptions)
1654
+ updateOptions(dialogData.availableOptions);
1429
1655
  });
1430
1656
 
1431
1657
 
1432
1658
 
1433
1659
  sendToWrapper(namespace, 'getLocalImages', {}, function(msg) {
1434
1660
  if (msg && msg.imageData) {
1435
- updateImageSelection(dev, msg.imageData);
1661
+ updateImageSelection(dialogData , msg.imageData);
1436
1662
 
1437
1663
  $('#chooseimage a.btn[name=\'save\']').unbind('click');
1438
1664
  $('#chooseimage a.btn[name=\'save\']').click(() => {
1439
1665
  const image = $('#chooseimage').find('#images option:selected').val();
1440
- const global = $('#chooseimage').find('#globaloverride').prop('checked');
1666
+ //const global = $('#chooseimage').find('#globaloverride').prop('checked');
1441
1667
  const name = $('#chooseimage').find('input[id=\'d_name\']').val();
1442
1668
  const data = {};
1443
1669
  if (image != 'current') data.icon= image;
1444
- if (name != dev.common.name) data.name = name;
1445
- 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;
1446
1673
 
1447
- updateLocalConfigItems(id, data, global);
1674
+ updateLocalConfigItems(id, data, isModel);
1448
1675
  });
1449
- sendToWrapper(namespace, 'getLocalConfigItems', { target:id, global:false, key:'options' }, function (msg) {
1676
+ sendToWrapper(namespace, 'getLocalConfigItems', { target:id, global:isModel, key:'options' }, function (msg) {
1450
1677
  if (msg) {
1451
1678
  if (msg.error) showMessage(msg.error, '_Error');
1452
1679
  Object.keys(device_options).forEach(key => delete device_options[key]);
@@ -1455,15 +1682,15 @@ async function selectImageOverride(id) {
1455
1682
  let cnt = 1;
1456
1683
  for (const key in msg.options)
1457
1684
  {
1458
- const idx = availableOptions.indexOf(key);
1685
+ const idx = dialogData.availableOptions.indexOf(key);
1459
1686
  console.warn(`key ${key} : index : ${idx}`);
1460
- if (idx > -1) availableOptions.splice(idx,1);
1687
+ if (idx > -1) dialogData.availableOptions.splice(idx,1);
1461
1688
  received_options[key]=msg.options[key];
1462
1689
  device_options[`o${cnt}`] = { key:key, value:msg.options[key]}
1463
1690
  cnt++;
1464
1691
  }
1465
1692
  }
1466
- updateOptions(availableOptions);
1693
+ updateOptions(dialogData.availableOptions);
1467
1694
  } else showMessage('callback without message');
1468
1695
  $('#chooseimage').modal('open');
1469
1696
  Materialize.updateTextFields();
@@ -1580,6 +1807,7 @@ function HtmlFromOutDebugMessages(messages, devID, filter) {
1580
1807
  }
1581
1808
 
1582
1809
  function displayDebugMessages(msg) {
1810
+ console.warn('displayDebugMessages called with '+ JSON.stringify(msg));
1583
1811
  const buttonNames = [];
1584
1812
  const idButtons = [];
1585
1813
  if (msg.byId) {
@@ -1785,7 +2013,9 @@ function getDevices() {
1785
2013
  getBinding();
1786
2014
  }
1787
2015
  updateStartButton();
2016
+ displayDebugMessages(debugMessages);
1788
2017
  showDevices();
2018
+ LocalDataDisplayValues.sortedKeys = Object.keys(models);
1789
2019
  showLocalData();
1790
2020
  UpdateAdapterAlive(true)
1791
2021
  }
@@ -1826,9 +2056,10 @@ function extractDevicesData(msg) {
1826
2056
  }
1827
2057
  else
1828
2058
  debugDevices = [];
1829
- if (debugMessages.byId) {
1830
- debugMessages.byId = msg;
1831
- if (msg) displayDebugMessages(debugMessages);
2059
+
2060
+ if (msg.deviceDebugData) {
2061
+ debugMessages = { byId: msg.deviceDebugData };
2062
+ displayDebugMessages(debugMessages);
1832
2063
  }
1833
2064
  if (msg.models) models = msg.models;
1834
2065
  lockout.isActive = false;
@@ -1946,7 +2177,7 @@ function load(settings, onChange) {
1946
2177
  getComPorts(onChange);
1947
2178
 
1948
2179
  //dialog = new MatDialog({EndingTop: '50%'});
1949
- const keepAliveHandle = startKeepalive();
2180
+ //const keepAliveHandle = startKeepalive();
1950
2181
  keepAlive(() => {
1951
2182
  getDevices();
1952
2183
  getNamedColors();
@@ -1955,6 +2186,10 @@ function load(settings, onChange) {
1955
2186
  groups = data.groups || {};
1956
2187
  //showGroups();
1957
2188
  });
2189
+ sendToWrapper(namespace, 'getLibData', {key: 'cidList'}, function (data) {
2190
+ cidList = data.list;
2191
+ });
2192
+
1958
2193
  })
1959
2194
 
1960
2195
  //getDebugMessages();
@@ -2008,8 +2243,10 @@ function load(settings, onChange) {
2008
2243
  });
2009
2244
  $('#pairing').click(function () {
2010
2245
  if (!$('#pairing').hasClass('pulse')) {
2246
+ messages = [];
2011
2247
  openNetwork();
2012
- } else showPairingProcess();
2248
+ }
2249
+ showPairingProcess();
2013
2250
  });
2014
2251
 
2015
2252
  $('#refresh').click(function () {
@@ -2024,6 +2261,10 @@ function load(settings, onChange) {
2024
2261
  resetConfirmation();
2025
2262
  });
2026
2263
 
2264
+ $('#restore-backup-btn').click(function () {
2265
+ selectBackup();
2266
+ });
2267
+
2027
2268
  $('#deleteNVRam-btn').click(function () {
2028
2269
  deleteNvBackupConfirmation();
2029
2270
  });
@@ -2091,6 +2332,65 @@ function load(settings, onChange) {
2091
2332
  $('#device-filter-btn').text($(this).text());
2092
2333
  doFilter();
2093
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
+ });
2094
2394
  });
2095
2395
 
2096
2396
  const text = $('#pairing').attr('data-tooltip');
@@ -2142,6 +2442,14 @@ function showPairingProcess(noextrabuttons) {
2142
2442
  dismissible: false
2143
2443
  });
2144
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
+ });
2145
2453
  if (noextrabuttons) {
2146
2454
  $('#modalpairing').find('.endpairing').addClass('hide');
2147
2455
  $('#modalpairing').find('.extendpairing').addClass('hide');
@@ -2269,6 +2577,7 @@ socket.emit('subscribeObjects', namespace + '.*');
2269
2577
 
2270
2578
  // react to changes
2271
2579
  socket.on('stateChange', function (id, state) {
2580
+ UpdateAdapterAlive(true);
2272
2581
  // only watch our own states
2273
2582
  if (id.substring(0, namespaceLen) !== namespace) return;
2274
2583
  if (state) {
@@ -2304,6 +2613,9 @@ socket.on('stateChange', function (id, state) {
2304
2613
  isHerdsmanRunning = false;
2305
2614
  updateStartButton();
2306
2615
  }
2616
+ if (state.val === 'Closing network.') {
2617
+ getDevices();
2618
+ }
2307
2619
  }
2308
2620
  } else {
2309
2621
  const devId = getDevId(id);
@@ -2331,6 +2643,7 @@ socket.on('stateChange', function (id, state) {
2331
2643
 
2332
2644
 
2333
2645
  socket.on('objectChange', function (id, obj) {
2646
+ UpdateAdapterAlive(true);
2334
2647
  if (id.substring(0, namespaceLen) !== namespace) return;
2335
2648
  if (obj && obj.type == 'device') { // && obj.common.type !== 'group') {
2336
2649
  updateDevice(id);
@@ -2341,6 +2654,7 @@ socket.on('objectChange', function (id, obj) {
2341
2654
  if (elems.length === 3) {
2342
2655
  removeDevice(id);
2343
2656
  showDevices();
2657
+ showLocalData();
2344
2658
  }
2345
2659
  }
2346
2660
  });
@@ -2408,7 +2722,7 @@ function showNetworkMap(devices, map) {
2408
2722
  node.label = 'Coordinator';
2409
2723
  // delete node.color;
2410
2724
  }
2411
- console.warn(`node for device ${JSON.stringify(node)}`)
2725
+ //console.warn(`node for device ${JSON.stringify(node)}`)
2412
2726
  return node;
2413
2727
  };
2414
2728
 
@@ -3257,6 +3571,42 @@ function resetConfirmation() {
3257
3571
  });
3258
3572
  }
3259
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
+ }
3260
3610
  function showViewConfig() {
3261
3611
  $('#modalviewconfig').modal('open');
3262
3612
  }
@@ -3643,14 +3993,13 @@ function genDevInfo(device) {
3643
3993
  if (value === undefined) {
3644
3994
  return '';
3645
3995
  } else {
3646
- 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>`;
3647
3998
  }
3648
3999
  };
3649
4000
  const genRowValues = function (name, value) {
3650
- if (value === undefined) {
3651
- return '';
3652
- } else {
3653
- let label = `${name}:`;
4001
+ if (Array.isArray(value)) {
4002
+ let label = `${name=='' ? '&nbsp;' : name + ':'}`;
3654
4003
  try {
3655
4004
  return value.map((val) => {
3656
4005
  const row = `<li><span class="label">${label}</span><span>${val}</span></li>`;
@@ -3662,6 +4011,7 @@ function genDevInfo(device) {
3662
4011
  return `<li><span class="label">${label}</span><span>${JSON.stringify(value)}</span></li>`
3663
4012
  }
3664
4013
  }
4014
+ else return '';
3665
4015
  };
3666
4016
  const modelUrl = (!mapped) ? '' : `<a href="https://www.zigbee2mqtt.io/devices/${sanitizeModelParameter(mapped.model)}.html" target="_blank" rel="noopener noreferrer">${mapped.model}</a>`;
3667
4017
  const mappedInfo = [];
@@ -3673,7 +4023,7 @@ function genDevInfo(device) {
3673
4023
  if (item == 'model')
3674
4024
  mappedInfo.push(genRow(item,modelUrl));
3675
4025
  else
3676
- mappedInfo.push(genRow(item,mapped[item]));
4026
+ if (typeof mapped[item] != 'object') mappedInfo.push(genRow(item,mapped[item]));
3677
4027
  }
3678
4028
  mappedInfo.push(
3679
4029
  ` </ul>
@@ -3695,7 +4045,7 @@ function genDevInfo(device) {
3695
4045
  const imgSrc = device.icon || device.common.icon;
3696
4046
  const imgInfo = (imgSrc) ? `<img src=${imgSrc} width='150px' onerror="this.onerror=null;this.src='img/unavailable.png';"><div class="divider"></div>` : '';
3697
4047
  const info =[
3698
- `<div class="col s12 m6 l6 xl6">
4048
+ `<div class="col ${device.memberinfo != undefined ? 's12 m12 l12 xl12':'s12 m6 l6 xl6'}">
3699
4049
  ${imgInfo}
3700
4050
  ${mappedInfo.join('')}
3701
4051
  <div class="divider"></div>
@@ -3704,8 +4054,19 @@ function genDevInfo(device) {
3704
4054
  for (const item in dev) {
3705
4055
  info.push(genRow(item, dev[item]));
3706
4056
  }
3707
- info.push(` ${genRow('configured', (device.isConfigured), true)}
3708
- </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>
3709
4070
  </div>
3710
4071
  </div>
3711
4072
  <div class="col s12 m6 l6 xl6">
@@ -4064,7 +4425,8 @@ function removeDevice(id) {
4064
4425
  }
4065
4426
 
4066
4427
  function swapActive(id) {
4067
- const dev = getDeviceByID(id);
4428
+ const dev = getDeviceByID(id) || getDeviceByIEEE(`0x${id}`);
4429
+ console.warn(`swap_active for ${id} -> ${JSON.stringify(dev)}`);
4068
4430
  if (dev && dev.common) {
4069
4431
  dev.common.deactivated = !(dev.common.deactivated);
4070
4432
  sendToWrapper(namespace, 'setDeviceActivated', {id: id, deactivated: dev.common.deactivated}, function () {