matterbridge-zigbee2mqtt 2.0.0 → 2.0.2

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.
@@ -1,1202 +0,0 @@
1
- /**
2
- * This file contains the class Zigbee2MQTT and all the interfaces to communicate with zigbee2MQTT.
3
- *
4
- * @file zigbee2mqtt.ts
5
- * @author Luca Liguori
6
- * @date 2023-06-30
7
- * @version 2.2.12
8
- *
9
- * Copyright 2023, 2024 Luca Liguori.
10
- *
11
- * Licensed under the Apache License, Version 2.0 (the "License");
12
- * you may not use this file except in compliance with the License.
13
- * You may obtain a copy of the License at
14
- *
15
- * http://www.apache.org/licenses/LICENSE-2.0
16
- *
17
- * Unless required by applicable law or agreed to in writing, software
18
- * distributed under the License is distributed on an "AS IS" BASIS,
19
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20
- * See the License for the specific language governing permissions and
21
- * limitations under the License. *
22
- */
23
-
24
- import fs from 'fs';
25
- import path from 'path';
26
- import * as util from 'util';
27
- import * as crypto from 'crypto';
28
- import { MqttClient, IClientOptions, connectAsync, ErrorWithReasonCode, IConnackPacket, IDisconnectPacket, IPublishPacket, Packet } from 'mqtt';
29
- import { EventEmitter } from 'events';
30
- import { AnsiLogger, TimestampFormat, rs, db, dn, gn, er, zb, hk, id, idn, ign, REVERSE, REVERSEOFF } from 'node-ansi-logger';
31
- import { BridgeExtension, KeyValue, Topology } from './zigbee2mqttTypes.js';
32
- import { mkdir } from 'fs/promises';
33
-
34
- const writeFile = util.promisify(fs.writeFile);
35
-
36
- interface Group {
37
- friendly_name: string;
38
- id: number;
39
- members: Member[];
40
- scenes: Scene[];
41
- }
42
-
43
- interface Member {
44
- endpoint: number;
45
- ieee_address: string;
46
- }
47
-
48
- interface Scene {
49
- id: number;
50
- name: string;
51
- }
52
-
53
- interface Preset {
54
- description: string;
55
- name: string;
56
- value: number;
57
- }
58
-
59
- interface Features {
60
- category: string;
61
- access: number;
62
- description: string;
63
- name: string;
64
- property: string;
65
- label: string;
66
- type: string;
67
- endpoint: string;
68
- value_off: string;
69
- value_on: string;
70
- value_toggle: string;
71
- unit: string;
72
- value_max: number;
73
- value_min: number;
74
- value_step: number;
75
- values: string[];
76
- presets: Preset[];
77
- }
78
-
79
- interface Exposes {
80
- category: string;
81
- type: string;
82
- endpoint: string;
83
- name: string;
84
- property: string;
85
- label: string;
86
- description: string;
87
- access: number;
88
- value_off: string;
89
- value_on: string;
90
- value_toggle: string;
91
- unit: string;
92
- value_max: number;
93
- value_min: number;
94
- value_step: number;
95
- values: string[];
96
- presets: Preset[];
97
- features: Features[];
98
- }
99
-
100
- interface Definition {
101
- model: string;
102
- vendor: string;
103
- description: string;
104
- exposes: Exposes[];
105
- options: Exposes[];
106
- supports_ota: boolean;
107
- }
108
-
109
- interface Target {
110
- endpoint: number;
111
- ieee_address: string;
112
- type: string;
113
- }
114
-
115
- interface Binding {
116
- cluster: string;
117
- target: Target;
118
- }
119
-
120
- interface Reporting {
121
- attribute: string;
122
- cluster: string;
123
- maximum_report_interval: number;
124
- minimum_report_interval: number;
125
- reportable_change: number;
126
- }
127
-
128
- interface Scenes {
129
- id: number;
130
- name: string;
131
- }
132
-
133
- interface Endpoint {
134
- bindings: Binding[];
135
- clusters: {
136
- input: string[];
137
- output: string[];
138
- };
139
- configured_reportings: Reporting[];
140
- scenes: Scenes[];
141
- }
142
-
143
- interface z2mEndpoints {
144
- endpoint?: string;
145
- bindings: Binding[];
146
- clusters: {
147
- input: string[];
148
- output: string[];
149
- };
150
- configured_reportings: Reporting[];
151
- scenes: Scenes[];
152
- }
153
-
154
- interface Device {
155
- date_code: string;
156
- definition: Definition;
157
- disabled: boolean;
158
- endpoints: {
159
- [key: number]: Endpoint;
160
- };
161
- friendly_name: string;
162
- ieee_address: string;
163
- interview_completed: boolean;
164
- interviewing: boolean;
165
- manufacturer: string;
166
- model_id: string;
167
- network_address: number;
168
- power_source: string;
169
- software_build_id: string;
170
- supported: boolean;
171
- type: string;
172
- }
173
-
174
- export interface z2mFeature {
175
- category: string;
176
- access: number;
177
- description: string;
178
- name: string;
179
- property: string;
180
- label: string;
181
- type: string;
182
- endpoint: string;
183
- value_off: string; // TODO boolean or string
184
- value_on: string; // TODO boolean or string
185
- value_toggle: string;
186
- unit: string;
187
- value_max: number;
188
- value_min: number;
189
- value_step: number;
190
- values: string[];
191
- presets: Preset[];
192
- }
193
-
194
- export interface z2mDevice {
195
- index: number;
196
- logName: string;
197
- ieee_address: string;
198
- friendly_name: string;
199
- getPayload: KeyValue | undefined;
200
- description: string;
201
- manufacturer: string;
202
- model_id: string;
203
- vendor: string;
204
- model: string;
205
- date_code: string;
206
- software_build_id: string;
207
- power_source: string;
208
- isAvailabilityEnabled: boolean;
209
- isOnline: boolean;
210
- category: string; // light or switch
211
- hasEndpoints: boolean;
212
- exposes: z2mFeature[]; // Exposes specific and generic
213
- options: z2mFeature[]; // Exposes options like state_action
214
- endpoints: z2mEndpoints[];
215
- }
216
-
217
- export interface z2mGroup {
218
- index: number;
219
- logName: string;
220
- id: number;
221
- friendly_name: string;
222
- getPayload: KeyValue | undefined;
223
- isAvailabilityEnabled: boolean;
224
- isOnline: boolean;
225
- members: Member[];
226
- scenes: Scene[];
227
- }
228
-
229
- interface PublishQueue {
230
- topic: string;
231
- message: string;
232
- }
233
-
234
- export class Zigbee2MQTT extends EventEmitter {
235
- // Logger
236
- private log: AnsiLogger;
237
-
238
- // Instance properties
239
- public mqttHost: string;
240
- public mqttPort: number;
241
- public mqttTopic: string;
242
- private mqttClient: MqttClient | undefined;
243
- private mqttIsConnected = false;
244
- private mqttIsReconnecting = false;
245
- private mqttIsEnding = false;
246
- private mqttDataPath = '';
247
- private mqttPublishQueue: PublishQueue[] = [];
248
- private mqttPublishQueueTimeout: NodeJS.Timeout | undefined = undefined;
249
- private mqttPublishInflights: number = 0;
250
-
251
- private z2mIsAvailabilityEnabled: boolean;
252
- private z2mIsOnline: boolean;
253
- private z2mPermitJoin: boolean;
254
- private z2mPermitJoinTimeout: number;
255
- private z2mVersion: string;
256
- public z2mDevices: z2mDevice[];
257
- public z2mGroups: z2mGroup[];
258
-
259
- // Define our MQTT options
260
- private options: IClientOptions = {
261
- clientId: 'classZigbee2MQTT_' + crypto.randomBytes(8).toString('hex'),
262
- keepalive: 60,
263
- protocolId: 'MQTT',
264
- protocolVersion: 5,
265
- reconnectPeriod: 1000,
266
- connectTimeout: 30 * 1000,
267
- username: '',
268
- password: '',
269
- clean: true,
270
- };
271
-
272
- // Constructor
273
- constructor(mqttHost: string, mqttPort: number, mqttTopic: string) {
274
- super();
275
-
276
- this.mqttHost = mqttHost;
277
- this.mqttPort = mqttPort;
278
- this.mqttTopic = mqttTopic;
279
-
280
- this.z2mIsAvailabilityEnabled = false;
281
- this.z2mIsOnline = false;
282
- this.z2mPermitJoin = false;
283
- this.z2mPermitJoinTimeout = 0;
284
- this.z2mVersion = '';
285
- this.z2mDevices = [];
286
- this.z2mGroups = [];
287
-
288
- this.log = new AnsiLogger({ logName: 'Zigbee2MQTT', logTimestampFormat: TimestampFormat.TIME_MILLIS });
289
- this.log.debug(`Created new instance with host: ${mqttHost} port: ${mqttPort} topic: ${mqttTopic}`);
290
- }
291
-
292
- public async setDataPath(dataPath: string): Promise<void> {
293
- try {
294
- await mkdir(dataPath, { recursive: true });
295
- this.mqttDataPath = dataPath;
296
- this.log.debug(`Data directory ${this.mqttDataPath} created successfully.`);
297
- } catch (e) {
298
- const error = e as NodeJS.ErrnoException;
299
- if (error.code === 'EEXIST') {
300
- this.log.debug('Data directory already exists');
301
- } else {
302
- this.log.error('Error creating data directory:', error);
303
- }
304
- }
305
- }
306
-
307
- // Get the URL for connect
308
- private getUrl(): string {
309
- return 'mqtt://' + this.mqttHost + ':' + this.mqttPort.toString();
310
- }
311
-
312
- public async start() {
313
- this.log.debug('Starting...');
314
-
315
- connectAsync(this.getUrl(), this.options)
316
- .then((client) => {
317
- this.log.debug('Connection established');
318
- this.mqttClient = client;
319
-
320
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
321
- this.mqttClient.on('connect', (packet: IConnackPacket) => {
322
- this.log.debug(`Event connect to ${this.getUrl()}${rs}` /*, connack*/);
323
- this.mqttIsConnected = true;
324
- this.mqttIsReconnecting = false;
325
- this.mqttIsEnding = false;
326
- this.emit('mqtt_connect'); // Never emitted at the start cause we connect async
327
- });
328
-
329
- this.mqttClient.on('reconnect', () => {
330
- this.log.debug(`Event reconnect to ${this.getUrl()}${rs}`);
331
- this.mqttIsReconnecting = true;
332
- this.emit('mqtt_reconnect');
333
- });
334
-
335
- this.mqttClient.on('disconnect', (packet: IDisconnectPacket) => {
336
- this.log.debug('Event diconnect', this.getUrl(), packet);
337
- this.emit('mqtt_disconnect');
338
- });
339
-
340
- this.mqttClient.on('close', () => {
341
- this.log.debug('Event close');
342
- this.mqttIsConnected = false;
343
- this.mqttIsReconnecting = false;
344
- this.emit('mqtt_close');
345
- });
346
-
347
- this.mqttClient.on('end', () => {
348
- this.log.debug('Event end');
349
- this.mqttIsConnected = false;
350
- this.mqttIsReconnecting = false;
351
- this.emit('mqtt_end');
352
- });
353
-
354
- this.mqttClient.on('offline', () => {
355
- this.log.error('Event offline');
356
- this.emit('mqtt_offline');
357
- });
358
-
359
- this.mqttClient.on('error', (error: Error | ErrorWithReasonCode) => {
360
- this.log.error('Event error', error);
361
- this.emit('mqtt_error', error);
362
- });
363
-
364
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
365
- this.mqttClient.on('packetsend', (packet: Packet) => {
366
- //this.log.debug('classZigbee2MQTT=>Event packetsend');
367
- });
368
-
369
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
370
- this.mqttClient.on('packetreceive', (packet: Packet) => {
371
- //this.log.debug('classZigbee2MQTT=>Event packetreceive');
372
- });
373
-
374
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
375
- this.mqttClient.on('message', (topic: string, payload: Buffer, packet: IPublishPacket) => {
376
- //this.log.debug(`classZigbee2MQTT=>Event message topic: ${topic} payload: ${payload.toString()} packet: ${stringify(packet, true)}`);
377
- this.messageHandler(topic, payload);
378
- });
379
-
380
- this.log.debug('Started');
381
-
382
- this.mqttIsConnected = true;
383
- this.mqttIsReconnecting = false;
384
- this.mqttIsEnding = false;
385
- this.emit('mqtt_connect');
386
- })
387
- .catch((error) => {
388
- this.log.error(`Error connecting to ${this.getUrl()}: ${error.message}`);
389
- });
390
- }
391
-
392
- public async stop() {
393
- if (!this.mqttClient || this.mqttIsEnding) {
394
- this.log.debug('Already stopped!');
395
- } else {
396
- this.mqttIsEnding = true;
397
- this.log.debug('Ending connection...');
398
- this.mqttClient
399
- .endAsync(false)
400
- .then(() => {
401
- this.mqttClient?.removeAllListeners();
402
- this.mqttIsConnected = false;
403
- this.mqttIsReconnecting = false;
404
- this.mqttIsEnding = false;
405
- this.mqttClient = undefined;
406
- this.log.debug('Connection closed');
407
- })
408
- .catch((error) => {
409
- this.log.error(`Error closing connection: ${error.message}`);
410
- });
411
- }
412
- }
413
-
414
- public async subscribe(topic: string) {
415
- if (this.mqttClient && this.mqttIsConnected) {
416
- this.log.debug(`Subscribing topic: ${topic}`);
417
- // Use subscribeAsync for promise-based handling
418
- this.mqttClient
419
- .subscribeAsync(topic, { qos: 2 })
420
- .then(() => {
421
- this.log.debug(`Subscribe success on topic: ${topic}`);
422
- this.emit('mqtt_subscribed');
423
- })
424
- .catch((error) => {
425
- this.log.error(`Subscribe error: ${error} on topic: ${topic}`);
426
- });
427
- } else {
428
- this.log.error('Unable to subscribe, client not connected or unavailable');
429
- }
430
- }
431
-
432
- public async publish(topic: string, message: string, queue: boolean = false) {
433
- const startInterval = () => {
434
- if (this.mqttPublishQueueTimeout) {
435
- return;
436
- }
437
- this.log.debug(`**Start publish ${REVERSE}[${this.mqttPublishQueue.length}-${this.mqttPublishInflights}]${REVERSEOFF} interval`);
438
- this.mqttPublishQueueTimeout = setInterval(async () => {
439
- if (this.mqttClient && this.mqttPublishQueue.length > 0) {
440
- this.log.debug(
441
- // eslint-disable-next-line max-len
442
- `**Publish ${REVERSE}[${this.mqttPublishQueue.length}-${this.mqttPublishInflights}]${REVERSEOFF} topic: ${this.mqttPublishQueue[0].topic} message: ${this.mqttPublishQueue[0].message}${rs}`,
443
- );
444
- //this.publish(this.mqttPublishQueue[0].topic, this.mqttPublishQueue[0].message);
445
-
446
- try {
447
- this.mqttPublishInflights++;
448
- await this.mqttClient.publishAsync(this.mqttPublishQueue[0].topic, this.mqttPublishQueue[0].message, { qos: 2 });
449
- this.log.debug(`***Publish ${REVERSE}[${this.mqttPublishQueue.length}-${this.mqttPublishInflights}]${REVERSEOFF} success on topic: ${topic} message: ${message} inflights: ${this.mqttPublishInflights}`);
450
- this.emit('mqtt_published');
451
- this.mqttPublishInflights--;
452
- } catch (error) {
453
- this.mqttPublishInflights--;
454
- this.log.error(
455
- // eslint-disable-next-line max-len
456
- `****Publish ${REVERSE}[${this.mqttPublishQueue.length}-${this.mqttPublishInflights}]${REVERSEOFF} error: ${error} on topic: ${topic} message: ${message} inflights: ${this.mqttPublishInflights}`,
457
- );
458
- }
459
-
460
- this.mqttPublishQueue.splice(0, 1);
461
- } else {
462
- stopInterval();
463
- }
464
- }, 50);
465
- };
466
-
467
- const stopInterval = () => {
468
- if (this.mqttPublishQueueTimeout) {
469
- this.log.debug(`**Stop publish ${REVERSE}[${this.mqttPublishQueue.length}-${this.mqttPublishInflights}]${REVERSEOFF} interval`);
470
- clearInterval(this.mqttPublishQueueTimeout);
471
- this.mqttPublishQueueTimeout = undefined;
472
- }
473
- };
474
-
475
- if (this.mqttClient && this.mqttIsConnected) {
476
- if (queue) {
477
- startInterval();
478
- this.mqttPublishQueue.push({ topic: topic, message: message });
479
- this.log.debug(`**Add to publish ${REVERSE}[${this.mqttPublishQueue.length}-${this.mqttPublishInflights}]${REVERSEOFF} topic: ${topic} message: ${message}${rs}`);
480
- return;
481
- }
482
-
483
- this.log.debug(`**Publishing ${REVERSE}[${this.mqttPublishInflights}]${REVERSEOFF} topic: ${topic} message: ${message}`);
484
- try {
485
- this.mqttPublishInflights++;
486
- await this.mqttClient.publishAsync(topic, message, { qos: 2 });
487
- this.log.debug(`***Publish ${REVERSE}[${this.mqttPublishInflights}]${REVERSEOFF} success on topic: ${topic} message: ${message}`);
488
- this.emit('mqtt_published');
489
- this.mqttPublishInflights--;
490
- } catch (error) {
491
- this.mqttPublishInflights--;
492
- this.log.error(`****Publish ${REVERSE}[${this.mqttPublishInflights}]${REVERSEOFF} error: ${error} on topic: ${topic} message: ${message}`);
493
- }
494
- } else {
495
- this.log.error('Unable to publish, client not connected or unavailable.');
496
- }
497
- }
498
-
499
- private async writeBufferJSON(file: string, buffer: Buffer) {
500
- const filePath = path.join(this.mqttDataPath, file);
501
- let jsonData;
502
-
503
- // Parse the buffer to JSON
504
- try {
505
- jsonData = JSON.parse(buffer.toString());
506
- } catch (error) {
507
- this.log.error('writeBufferJSON: parsing error:', error);
508
- return; // Stop execution if parsing fails
509
- }
510
-
511
- // Write the JSON data to a file
512
- writeFile(`${filePath}.json`, JSON.stringify(jsonData, null, 2))
513
- .then(() => {
514
- this.log.debug(`Successfully wrote to ${filePath}.json`);
515
- })
516
- .catch((error) => {
517
- this.log.error(`Error writing to ${filePath}.json:`, error);
518
- });
519
- }
520
-
521
- private async writeFile(file: string, data: string) {
522
- const filePath = path.join(this.mqttDataPath, file);
523
-
524
- // Write the data to a file
525
- writeFile(`${filePath}`, data)
526
- .then(() => {
527
- this.log.debug(`Successfully wrote to ${filePath}`);
528
- })
529
- .catch((error) => {
530
- this.log.error(`Error writing to ${filePath}:`, error);
531
- });
532
- }
533
-
534
- private messageHandler(topic: string, payload: Buffer) {
535
- if (topic.startsWith(this.mqttTopic + '/bridge/state')) {
536
- const data = JSON.parse(payload.toString());
537
- //this.log.debug('classZigbee2MQTT=>Message bridge/state', data);
538
- if (data.state === 'online') {
539
- this.z2mIsOnline = true;
540
- this.emit('online');
541
- } else if (data.state === 'offline') {
542
- this.z2mIsOnline = false;
543
- this.emit('offline');
544
- }
545
- this.log.debug(`Message bridge/state online => ${this.z2mIsOnline}`);
546
- } else if (topic.startsWith(this.mqttTopic + '/bridge/info')) {
547
- const data = JSON.parse(payload.toString());
548
- //this.log.debug('classZigbee2MQTT=>Message bridge/info', data);
549
- this.z2mPermitJoin = data.permit_join ? data.permit_join : false;
550
- this.z2mPermitJoinTimeout = data.permit_join_timeout ? data.permit_join_timeout : 0;
551
- this.z2mVersion = data.version ? data.version : '';
552
- this.z2mIsAvailabilityEnabled = data.config.availability ? true : false;
553
- this.log.debug(`Message bridge/info availability => ${this.z2mIsAvailabilityEnabled}`);
554
- this.log.debug(`Message bridge/info version => ${this.z2mVersion}`);
555
- this.log.debug(`Message bridge/info permit_join => ${this.z2mPermitJoin} timeout => ${this.z2mPermitJoinTimeout}`);
556
- this.emit('info', this.z2mVersion, this.z2mIsAvailabilityEnabled, this.z2mPermitJoin, this.z2mPermitJoinTimeout);
557
- this.writeBufferJSON('bridge-info', payload);
558
- this.emit('bridge-info', data);
559
- } else if (topic.startsWith(this.mqttTopic + '/bridge/devices')) {
560
- this.z2mDevices.splice(0, this.z2mDevices.length);
561
- const devices: Device[] = JSON.parse(payload.toString());
562
- const data = JSON.parse(payload.toString());
563
- this.writeBufferJSON('bridge-devices', payload);
564
- this.emit('bridge-devices', data);
565
- let index = 1;
566
- for (const device of devices) {
567
- if (device.type === 'Coordinator' && device.supported === true && device.disabled === false && device.interview_completed === true && device.interviewing === false) {
568
- const z2m: z2mDevice = {
569
- logName: 'Coordinator',
570
- index: 0,
571
- ieee_address: device.ieee_address,
572
- friendly_name: device.friendly_name,
573
- getPayload: undefined,
574
- description: '',
575
- manufacturer: '',
576
- model_id: '',
577
- vendor: 'zigbee2MQTT',
578
- model: 'coordinator',
579
- date_code: '',
580
- software_build_id: '',
581
- power_source: 'Mains (single phase)',
582
- isAvailabilityEnabled: false,
583
- isOnline: false,
584
- category: '',
585
- hasEndpoints: false,
586
- exposes: [],
587
- options: [],
588
- endpoints: [],
589
- };
590
- this.z2mDevices.push(z2m);
591
- }
592
- if (device.type !== 'Coordinator' && device.supported === true && device.disabled === false && device.interview_completed === true && device.interviewing === false) {
593
- const z2m: z2mDevice = {
594
- logName: 'Dev#' + index.toString().padStart(2, '0'),
595
- index: index++,
596
- ieee_address: device.ieee_address,
597
- friendly_name: device.friendly_name,
598
- getPayload: undefined,
599
- description: device.definition.description || '',
600
- manufacturer: device.manufacturer || '',
601
- model_id: device.model_id || '',
602
- vendor: device.definition.vendor || '',
603
- model: device.definition.model || '',
604
- date_code: device.date_code || '',
605
- software_build_id: device.software_build_id || '',
606
- power_source: device.power_source,
607
- isAvailabilityEnabled: false,
608
- isOnline: false,
609
- category: '',
610
- hasEndpoints: false,
611
- exposes: [],
612
- options: [],
613
- endpoints: [],
614
- };
615
- for (const expose of device.definition.exposes) {
616
- if (!expose.property && !expose.name && expose.features && expose.type) {
617
- // Specific expose https://www.zigbee2mqtt.io/guide/usage/exposes.html
618
- if (z2m.category === '') {
619
- // Only the first type: light, switch ...
620
- z2m.category = expose.type;
621
- }
622
- for (const feature of expose.features) {
623
- // Exposes nested inside features
624
- feature.category = expose.type;
625
- z2m.exposes.push(feature);
626
- if (feature.endpoint) {
627
- z2m.hasEndpoints = true;
628
- }
629
- }
630
- } else {
631
- // Generic expose https://www.zigbee2mqtt.io/guide/usage/exposes.html
632
- expose.category = '';
633
- z2m.exposes.push(expose);
634
- }
635
- }
636
- for (const option of device.definition.options) {
637
- const feature = option as z2mFeature;
638
- z2m.options.push(feature);
639
- }
640
- for (const key in device.endpoints) {
641
- interface EndpointWithKey extends Endpoint {
642
- endpoint: string;
643
- }
644
- const endpoint: Endpoint = device.endpoints[key];
645
- const endpointWithKey: EndpointWithKey = {
646
- ...endpoint,
647
- endpoint: key,
648
- };
649
- z2m.endpoints.push(endpointWithKey);
650
- //this.log.debug('classZigbee2MQTT=>Message bridge/devices endpoints=>', device.friendly_name, key, endpoint);
651
- }
652
- this.z2mDevices.push(z2m);
653
- }
654
- }
655
- this.log.debug(`Received ${this.z2mDevices.length} devices`);
656
- this.emit('devices');
657
- //this.printDevices();
658
- } else if (topic.startsWith(this.mqttTopic + '/bridge/groups')) {
659
- this.z2mGroups.splice(0, this.z2mGroups.length);
660
- const groups: Group[] = JSON.parse(payload.toString());
661
- const data = JSON.parse(payload.toString());
662
- this.writeBufferJSON('bridge-groups', payload);
663
- this.emit('bridge-groups', data);
664
- let index = 1;
665
- for (const group of groups) {
666
- const z2m: z2mGroup = {
667
- logName: 'Grp#' + index.toString().padStart(2, '0'),
668
- index: index++,
669
- id: group.id,
670
- friendly_name: group.friendly_name,
671
- getPayload: undefined,
672
- isAvailabilityEnabled: false,
673
- isOnline: false,
674
- members: [],
675
- scenes: [],
676
- };
677
- for (const member of group.members) {
678
- z2m.members.push(member);
679
- }
680
- for (const scene of group.scenes) {
681
- z2m.scenes.push(scene);
682
- }
683
- this.z2mGroups.push(z2m);
684
- }
685
- this.log.debug(`Received ${this.z2mGroups.length} groups`);
686
- this.emit('groups');
687
- //this.printGroups();
688
- } else if (topic.startsWith(this.mqttTopic + '/bridge/extensions')) {
689
- const extensions = JSON.parse(payload.toString()) as BridgeExtension[];
690
- for (const extension of extensions) {
691
- this.log.warn(`Message topic: ${topic} extension: ${extension.name}`);
692
- }
693
- } else if (topic.startsWith(this.mqttTopic + '/bridge/event')) {
694
- this.handleEvent(payload);
695
- } else if (topic.startsWith(this.mqttTopic + '/bridge/request')) {
696
- const data = JSON.parse(payload.toString());
697
- this.log.warn(`Message topic: ${topic} payload:${rs}`, data);
698
- } else if (topic.startsWith(this.mqttTopic + '/bridge/response')) {
699
- if (topic.startsWith(this.mqttTopic + '/bridge/response/networkmap')) {
700
- this.handleResponseNetworkmap(payload);
701
- return;
702
- }
703
- if (topic.startsWith(this.mqttTopic + '/bridge/response/permit_join')) {
704
- this.handleResponsePermitJoin(payload);
705
- return;
706
- }
707
- if (topic.startsWith(this.mqttTopic + '/bridge/response/device/rename')) {
708
- this.handleResponseDeviceRename(payload);
709
- return;
710
- }
711
- const data = JSON.parse(payload.toString());
712
- this.log.warn(`Message topic: ${topic} payload:${rs}`, data);
713
- /*
714
- [05/09/2023, 20:35:26] [z2m] classZigbee2MQTT=>Message bridge/response zigbee2mqtt/bridge/response/group/add {
715
- data: { friendly_name: 'Guest', id: 1 },
716
- status: 'ok',
717
- transaction: '1nqux-2'
718
- }
719
- [11/09/2023, 15:13:54] [z2m] classZigbee2MQTT=>Message bridge/response zigbee2mqtt/bridge/response/group/members/add {
720
- data: { device: '0x84fd27fffe83066f/1', group: 'Master Guest room' },
721
- status: 'ok',
722
- transaction: '2ww7l-5'
723
- }
724
- */
725
- } else if (topic.startsWith(this.mqttTopic + '/bridge/logging')) {
726
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
727
- //const data = JSON.parse(payload.toString());
728
- //this.log.debug('classZigbee2MQTT=>Message bridge/logging', data);
729
- } else {
730
- let entity = topic.replace(this.mqttTopic + '/', '');
731
- let service = '';
732
- if (entity.search('/')) {
733
- // set get availability or unknown TODO
734
- const parts = entity.split('/');
735
- entity = parts[0];
736
- service = parts[1];
737
- }
738
- if (entity === 'Coordinator') {
739
- const data = JSON.parse(payload.toString()); // TODO crash on device rename
740
- if (service === 'availability') {
741
- if (data.state === 'online') {
742
- this.log.debug(`Received ONLINE for ${id}Coordinator${rs}`, data);
743
- } else if (data.state === 'offline') {
744
- this.log.debug(`Received OFFLINE for ${id}Coordinator${rs}`, data);
745
- }
746
- }
747
- return;
748
- }
749
- if (entity.includes('_-_')) {
750
- // Eve app test mode!
751
- const foundDevice = this.z2mDevices.find((device) => device.friendly_name.includes(entity));
752
- entity = foundDevice ? foundDevice.friendly_name : entity;
753
- }
754
- const foundDevice = this.z2mDevices.findIndex((device) => device.ieee_address === entity || device.friendly_name === entity);
755
- if (foundDevice !== -1) {
756
- this.handleDeviceMessage(foundDevice, entity, service, payload);
757
- } else {
758
- const foundGroup = this.z2mGroups.findIndex((group) => group.friendly_name === entity);
759
- if (foundGroup !== -1) {
760
- this.handleGroupMessage(foundGroup, entity, service, payload);
761
- } else {
762
- try {
763
- const data = JSON.parse(payload.toString());
764
- this.log.warn('Message for ***unknown*** entity:', entity, 'service:', service, 'payload:', data);
765
- } catch {
766
- this.log.error('Message for ***unknown*** entity:', entity, 'service:', service, 'payload: error');
767
- }
768
- }
769
- }
770
- }
771
- }
772
-
773
- public getDevice(name: string): z2mDevice | undefined {
774
- return this.z2mDevices.find((device) => device.ieee_address === name || device.friendly_name === name);
775
- }
776
-
777
- public getGroup(name: string): z2mGroup | undefined {
778
- return this.z2mGroups.find((group) => group.friendly_name === name);
779
- }
780
-
781
- private handleDeviceMessage(deviceIndex: number, entity: string, service: string, payload: Buffer) {
782
- //this.log.debug(`classZigbee2MQTT=>handleDeviceMessage ${id}#${deviceIndex + 1}${rs} entity ${dn}${entity}${rs} service ${zb}${service}${rs} payload ${pl}${payload}${rs}`);
783
- if (payload.length === 0 || payload === null) {
784
- this.log.warn(`handleDeviceMessage ${id}#${deviceIndex + 1}${rs} entity ${dn}${entity}${rs} service ${zb}${service}${rs} payload null`);
785
- return;
786
- }
787
- const data = JSON.parse(payload.toString()); // TODO crash on device rename
788
- if (service === 'availability') {
789
- if (data.state === 'online') {
790
- this.z2mDevices[deviceIndex].isAvailabilityEnabled = true;
791
- this.z2mDevices[deviceIndex].isOnline = true;
792
- this.emit('ONLINE-' + entity);
793
- } else if (data.state === 'offline') {
794
- this.z2mDevices[deviceIndex].isOnline = false;
795
- this.emit('OFFLINE-' + entity);
796
- }
797
- } else if (service === 'get') {
798
- // Do nothing
799
- //this.log.warn(`handleDeviceMessage entity ${dn}${entity}${wr} service ${service} payload ${pl}${payload}${rs}`);
800
- } else if (service === 'set') {
801
- // Do nothing
802
- //this.log.warn(`handleDeviceMessage entity ${dn}${entity}${wr} service ${service} payload ${pl}${payload}${rs}`);
803
- } else {
804
- //this.log.debug(`classZigbee2MQTT=>emitting message for device ${dn}${entity}${rs} payload ${pl}${payload}${rs}`);
805
- this.emit('MESSAGE-' + entity, data);
806
- }
807
- }
808
-
809
- private handleGroupMessage(groupIndex: number, entity: string, service: string, payload: Buffer) {
810
- //this.log.debug(`classZigbee2MQTT=>handleGroupMessage ${id}#${groupIndex+1}${rs} entity ${gn}${entity}${rs} service ${zb}${service}${rs} payload ${pl}${payload}${rs}`);
811
- if (payload.length === 0 || payload === null) {
812
- this.log.warn(`handleGroupMessage ${id}#${groupIndex + 1}${rs} entity ${gn}${entity}${rs} service ${zb}${service}${rs} payload null`);
813
- return;
814
- }
815
- const data = JSON.parse(payload.toString());
816
- data['last_seen'] = new Date().toISOString();
817
- if (service === 'availability') {
818
- if (data.state === 'online') {
819
- this.z2mGroups[groupIndex].isAvailabilityEnabled = true;
820
- this.z2mGroups[groupIndex].isOnline = true;
821
- this.emit('ONLINE-' + entity);
822
- } else if (data.state === 'offline') {
823
- this.z2mGroups[groupIndex].isOnline = false;
824
- this.emit('OFFLINE-' + entity);
825
- }
826
- } else if (service === 'get') {
827
- // Do nothing
828
- } else if (service === 'set') {
829
- // Do nothing
830
- } else {
831
- //this.log.debug(`classZigbee2MQTT=>emitting message for group ${gn}${entity}${rs} payload ${pl}${payload}${rs}`);
832
- this.emit('MESSAGE-' + entity, data);
833
- }
834
- }
835
-
836
- private handleResponseNetworkmap(payload: Buffer) {
837
- /*
838
- "routes": [
839
- {
840
- "destinationAddress": 31833,
841
- "nextHop": 31833,
842
- "status": "ACTIVE"
843
- }
844
- ],
845
- */
846
- const data = JSON.parse(payload.toString());
847
-
848
- const topology: Topology = data.data.value;
849
- const lqi = (lqi: number) => {
850
- if (lqi < 50) {
851
- return `\x1b[31m${lqi.toString().padStart(3, ' ')}${db}`;
852
- } else if (lqi > 200) {
853
- return `\x1b[32m${lqi.toString().padStart(3, ' ')}${db}`;
854
- } else {
855
- return `\x1b[38;5;251m${lqi.toString().padStart(3, ' ')}${db}`;
856
- }
857
- };
858
- const depth = (depth: number) => {
859
- if (depth === 255) {
860
- return `\x1b[32m${depth.toString().padStart(3, ' ')}${db}`;
861
- } else {
862
- return `\x1b[38;5;251m${depth.toString().padStart(3, ' ')}${db}`;
863
- }
864
- };
865
- const relationship = (relationship: number): string => {
866
- if (relationship === 0) {
867
- return `${zb}${relationship}-IsParent ${db}`;
868
- } else if (relationship === 1) {
869
- return `${hk}${relationship}-IsAChild ${db}`;
870
- } else {
871
- return `${relationship}-IsASibling`;
872
- }
873
- };
874
- const friendlyName = (ieeeAddr: string): string => {
875
- const node = topology.nodes.find((node) => node.ieeeAddr === ieeeAddr);
876
- if (node) {
877
- if (node.type === 'Coordinator') {
878
- return `\x1b[48;5;1m\x1b[38;5;255m${node.friendlyName} [C]${rs}${db}`;
879
- } else if (node.type === 'Router') {
880
- return `${dn}${node.friendlyName} [R]${db}`;
881
- } else if (node.type === 'EndDevice') {
882
- return `${gn}${node.friendlyName} [E]${db}`;
883
- }
884
- }
885
- return `${er}${ieeeAddr}${db}`;
886
- };
887
- const timePassedSince = (lastSeen: number): string => {
888
- const now = Date.now();
889
- const difference = now - lastSeen; // difference in milliseconds
890
-
891
- const days = Math.floor(difference / (1000 * 60 * 60 * 24));
892
- if (days > 0) {
893
- return `${days} days ago`;
894
- }
895
-
896
- const hours = Math.floor((difference % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
897
- if (hours > 0) {
898
- return `${hours} hours ago`;
899
- }
900
-
901
- const minutes = Math.floor((difference % (1000 * 60 * 60)) / (1000 * 60));
902
- if (minutes > 0) {
903
- return `${minutes} minutes ago`;
904
- }
905
-
906
- const seconds = Math.floor((difference % (1000 * 60)) / 1000);
907
- return `${seconds} seconds ago`;
908
- };
909
- this.writeBufferJSON('networkmap_' + data.data.type, payload);
910
-
911
- if (data.data.type === 'graphviz') {
912
- this.writeFile('networkmap_' + data.data.type + '.txt', data.data.value);
913
- }
914
- if (data.data.type === 'plantuml') {
915
- this.writeFile('networkmap_' + data.data.type + '.txt', data.data.value);
916
- }
917
- if (data.data.type === 'raw') {
918
- // Log nodes with links
919
- this.log.warn('Network map nodes:');
920
- topology.nodes.sort((a, b) => a.friendlyName.localeCompare(b.friendlyName));
921
- topology.nodes.forEach((node, index) => {
922
- this.log.debug(
923
- // eslint-disable-next-line max-len
924
- `Node [${index.toString().padStart(3, ' ')}] ${node.type === 'EndDevice' ? ign : node.type === 'Router' ? idn : '\x1b[48;5;1m\x1b[38;5;255m'}${node.friendlyName}${rs}${db} addr: ${node.ieeeAddr}-0x${node.networkAddress.toString(16)} type: ${node.type} lastseen: ${timePassedSince(node.lastSeen)}`,
925
- );
926
- // SourceAddr
927
- const sourceLinks = topology.links.filter((link) => link.sourceIeeeAddr === node.ieeeAddr); // Filter
928
- sourceLinks.sort((a, b) => a.lqi - b.lqi); // Sort by lqi
929
- sourceLinks.forEach((link, index) => {
930
- //const targetNode = topology.nodes.find((node) => node.ieeeAddr === link.target.ieeeAddr);
931
- this.log.debug(` link [${index.toString().padStart(4, ' ')}] lqi: ${lqi(link.lqi)} depth: ${depth(link.depth)} relation: ${relationship(link.relationship)} > > > ${friendlyName(link.target.ieeeAddr)}`);
932
- });
933
- // TargetAddr
934
- const targetLinks = topology.links.filter((link) => link.targetIeeeAddr === node.ieeeAddr); // Filter
935
- targetLinks.sort((a, b) => a.lqi - b.lqi); // Sort by lqi
936
- targetLinks.forEach((link, index) => {
937
- //const sourceNode = topology.nodes.find((node) => node.ieeeAddr === link.source.ieeeAddr);
938
- this.log.debug(` link [${index.toString().padStart(4, ' ')}] lqi: ${lqi(link.lqi)} depth: ${depth(link.depth)} relation: ${relationship(link.relationship)} < < < ${friendlyName(link.source.ieeeAddr)}`);
939
- });
940
- });
941
- // Log links
942
- /*
943
- this.log.warn('Network map links:');
944
- map.links.sort((a, b) => a.sourceIeeeAddr.localeCompare(b.sourceIeeeAddr));
945
- map.links.forEach( (link, index) => {
946
- const sourceNode = map.nodes.find(node => node.ieeeAddr === link.source.ieeeAddr);
947
- assert(sourceNode, `${wr}NwkAddr error node ${link.sourceIeeeAddr} not found${db}`);
948
- const targetNode = map.nodes.find(node => node.ieeeAddr === link.target.ieeeAddr);
949
- assert(targetNode, `${wr}NwkAddr error node ${link.targetIeeeAddr} not found${db}`);
950
- this.log.debug(`- link[${index}]: ${link.source.ieeeAddr}-${link.source.networkAddress.toString(16)} (${sourceNode?.friendlyName})
951
- Lqi: ${link.lqi} Depth: ${link.depth} Relation: ${link.relationship} => ${link.target.ieeeAddr}-${link.target.networkAddress.toString(16)} (${targetNode?.friendlyName})`);
952
- } );
953
- */
954
- }
955
- }
956
-
957
- private handleResponseDeviceRename(payload: Buffer) {
958
- /*
959
- {
960
- data: {
961
- from: '0xcc86ecfffe4e9d25',
962
- homeassistant_rename: false,
963
- to: 'Double switch'
964
- },
965
- status: 'ok',
966
- transaction: 'smeo0-8'
967
- }
968
- */
969
- const json = JSON.parse(payload.toString());
970
- this.log.warn(`handleResponseDeviceRename from ${json.data.from} to ${json.data.to} status ${json.status}`);
971
- const device = this.z2mDevices.find((device) => device.friendly_name === json.data.to);
972
- if (device && json.status === 'ok') {
973
- this.emit('rename', device.ieee_address, json.data.from, json.data.to);
974
- }
975
- }
976
-
977
- private handleResponsePermitJoin(payload: Buffer) {
978
- /*
979
- {
980
- data: { device?: 'Coordinator', time: 254, value: true },
981
- status: 'ok',
982
- transaction: 'adeis-5'
983
- }
984
- */
985
- const json = JSON.parse(payload.toString());
986
- this.log.warn(`handleResponsePermitJoin() device: ${json.data.device ? json.data.device : 'All'} time: ${json.data.time} value: ${json.data.value} status: ${json.status}`);
987
- if (json.status === 'ok') {
988
- this.emit('permit_join', json.data.device, json.data.time, json.data.value);
989
- }
990
- }
991
-
992
- private handleEvent(payload: Buffer) {
993
- /*
994
- {
995
- data: { friendly_name: 'Light sensor', ieee_address: '0x54ef44100085c321' },
996
- type: 'device_leave'
997
- }
998
- {
999
- data: {
1000
- friendly_name: 'Kitchen Dishwasher water leak sensor',
1001
- ieee_address: '0x00158d0007c2b057'
1002
- },
1003
- type: 'device_joined'
1004
- }
1005
- {
1006
- data: {
1007
- friendly_name: 'Kitchen Sink water leak sensor',
1008
- ieee_address: '0x00158d0008f1099b',
1009
- status: 'started'
1010
- },
1011
- type: 'device_interview'
1012
- }
1013
- {
1014
- data: {
1015
- friendly_name: 'Kitchen Sink water leak sensor',
1016
- ieee_address: '0x00158d0008f1099b'
1017
- },
1018
- type: 'device_announce'
1019
- }
1020
- {
1021
- data: {
1022
- definition: {
1023
- description: 'Aqara water leak sensor',
1024
- exposes: [Array],
1025
- model: 'SJCGQ11LM',
1026
- options: [Array],
1027
- supports_ota: false,
1028
- vendor: 'Xiaomi'
1029
- },
1030
- friendly_name: 'Kitchen Sink water leak sensor',
1031
- ieee_address: '0x00158d0008f1099b',
1032
- status: 'successful',
1033
- supported: true
1034
- },
1035
- type: 'device_interview'
1036
- }
1037
- */
1038
- const json = JSON.parse(payload.toString());
1039
- switch (json.type) {
1040
- case undefined:
1041
- this.log.error('handleEvent() undefined type', json);
1042
- break;
1043
- case 'device_leave':
1044
- this.log.warn(`handleEvent() type: device_leave name: ${json.data.friendly_name} address: ${json.data.ieee_address}`);
1045
- break;
1046
- case 'device_joined':
1047
- this.log.warn(`handleEvent() type: device_joined name: ${json.data.friendly_name} address: ${json.data.ieee_address}`);
1048
- break;
1049
- case 'device_interview':
1050
- this.log.warn(`handleEvent() type: device_interview name: ${json.data.friendly_name} address: ${json.data.ieee_address} status: ${json.data.status} supported: ${json.data.supported}`);
1051
- break;
1052
- case 'device_announce':
1053
- this.log.warn(`handleEvent() type: device_announce name: ${json.data.friendly_name} address: ${json.data.ieee_address}`);
1054
- break;
1055
- }
1056
- }
1057
-
1058
- // Function to read JSON config from a file
1059
- private readConfig(filename: string) {
1060
- try {
1061
- const rawdata = fs.readFileSync(filename, 'utf-8');
1062
- const data = JSON.parse(rawdata);
1063
- return data;
1064
- } catch (err) {
1065
- this.log.error('readConfig error', err);
1066
- return null;
1067
- }
1068
- }
1069
-
1070
- // Function to write JSON config to a file
1071
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1072
- private writeConfig(filename: string, data: any): boolean {
1073
- try {
1074
- const jsonString = JSON.stringify(data, null, 2);
1075
- fs.writeFileSync(filename, jsonString);
1076
- return true;
1077
- } catch (err) {
1078
- this.log.error('writeConfig error', err);
1079
- return true;
1080
- }
1081
- }
1082
-
1083
- private printDevice(device: z2mDevice) {
1084
- this.log.debug(`Device - ${dn}${device.friendly_name}${rs}`);
1085
- this.log.debug(`IEEE Address: ${device.ieee_address}`);
1086
- this.log.debug(`Description: ${device.description}`);
1087
- this.log.debug(`Manufacturer: ${device.manufacturer}`);
1088
- this.log.debug(`Model ID: ${device.model_id}`);
1089
- this.log.debug(`Date Code: ${device.date_code}`);
1090
- this.log.debug(`Software Build ID: ${device.software_build_id}`);
1091
- this.log.debug(`Power Source: ${device.power_source}`);
1092
- this.log.debug(`Availability Enabled: ${device.isAvailabilityEnabled}`);
1093
- this.log.debug(`Online: ${device.isOnline}`);
1094
- this.log.debug(`Type: ${device.category}`);
1095
-
1096
- const printFeatures = (features: z2mFeature[], featureType: string) => {
1097
- this.log.debug(`${featureType}:`);
1098
- features.forEach((feature) => {
1099
- this.log.debug(` Name: ${zb}${feature.name}${rs}`);
1100
- this.log.debug(` Description: ${feature.description}`);
1101
- this.log.debug(` Property: ${zb}${feature.property}${rs}`);
1102
- this.log.debug(` Type: ${feature.type}`);
1103
- this.log.debug(` Access: ${feature.access}`);
1104
- if (feature.endpoint) {
1105
- this.log.debug(` Endpoint: ${feature.endpoint}`);
1106
- }
1107
- if (feature.unit) {
1108
- this.log.debug(` Unit: ${feature.unit}`);
1109
- }
1110
- if (feature.value_max) {
1111
- this.log.debug(` Value Max: ${feature.value_max}`);
1112
- }
1113
- if (feature.value_min) {
1114
- this.log.debug(` Value Min: ${feature.value_min}`);
1115
- }
1116
- if (feature.value_step) {
1117
- this.log.debug(` Value Step: ${feature.value_step}`);
1118
- }
1119
- if (feature.value_on) {
1120
- this.log.debug(` Value On: ${feature.value_on}`);
1121
- }
1122
- if (feature.value_off) {
1123
- this.log.debug(` Value Off: ${feature.value_off}`);
1124
- }
1125
- if (feature.value_toggle) {
1126
- this.log.debug(` Value Toggle: ${feature.value_toggle}`);
1127
- }
1128
- if (feature.values) {
1129
- this.log.debug(` Values: ${feature.values.join(', ')}`);
1130
- }
1131
- if (feature.presets) {
1132
- this.log.debug(` Presets: ${feature.presets.join(', ')}`);
1133
- }
1134
- this.log.debug('');
1135
- });
1136
- };
1137
-
1138
- const printEndpoints = (endpoints: z2mEndpoints[]) => {
1139
- endpoints.forEach((endpoint) => {
1140
- this.log.debug(`--Endpoint ${endpoint.endpoint}`);
1141
- endpoint.bindings.forEach((binding) => {
1142
- this.log.debug(`----Bindings: ${binding.cluster}`, binding.target);
1143
- });
1144
- endpoint.clusters.input.forEach((input) => {
1145
- this.log.debug(`----Clusters input: ${input}`);
1146
- });
1147
- endpoint.clusters.output.forEach((output) => {
1148
- this.log.debug(`----Clusters output: ${output}`);
1149
- });
1150
- endpoint.configured_reportings.forEach((reporting) => {
1151
- // eslint-disable-next-line max-len
1152
- this.log.debug(`----Reportings: ${reporting.attribute} ${reporting.cluster} ${reporting.minimum_report_interval} ${reporting.maximum_report_interval} ${reporting.reportable_change}`);
1153
- });
1154
- endpoint.scenes.forEach((scene) => {
1155
- this.log.debug(`----Scenes: ID ${scene.id} Name ${scene.name}`);
1156
- });
1157
- this.log.debug('');
1158
- });
1159
- };
1160
-
1161
- printFeatures(device.exposes, 'Exposes');
1162
- printFeatures(device.options, 'Options');
1163
- printEndpoints(device.endpoints);
1164
-
1165
- this.log.debug('');
1166
- }
1167
-
1168
- private printDevices() {
1169
- this.z2mDevices.forEach((device) => {
1170
- this.printDevice(device);
1171
- });
1172
- }
1173
-
1174
- private printGroup(group: z2mGroup) {
1175
- this.log.debug(`Group - ${dn}${group.friendly_name}${rs}`);
1176
- this.log.debug(`ID: ${group.id}`);
1177
- const printMembers = (members: Member[]) => {
1178
- this.log.debug('Members:');
1179
- members.forEach((member) => {
1180
- this.log.debug(`--Endpoint ${member.endpoint}`);
1181
- this.log.debug(`--IEEE Address ${member.ieee_address}`);
1182
- });
1183
- };
1184
- printMembers(group.members);
1185
- const printScenes = (scenes: Scene[]) => {
1186
- this.log.debug('Scenes:');
1187
- scenes.forEach((scene) => {
1188
- this.log.debug(`--ID ${scene.id}`);
1189
- this.log.debug(`--Name ${scene.name}`);
1190
- });
1191
- };
1192
- printScenes(group.scenes);
1193
- this.log.debug(`Availability Enabled: ${group.isAvailabilityEnabled}`);
1194
- this.log.debug(`Online: ${group.isOnline}`);
1195
- }
1196
-
1197
- private printGroups() {
1198
- this.z2mGroups.forEach((group) => {
1199
- this.printGroup(group);
1200
- });
1201
- }
1202
- }