matterbridge-zigbee2mqtt 2.3.0-dev.4 → 2.3.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/dist/entity.js CHANGED
@@ -1,9 +1,37 @@
1
+ /**
2
+ * This file contains the classes ZigbeeEntity, ZigbeeDevice and ZigbeeGroup.
3
+ *
4
+ * @file entity.ts
5
+ * @author Luca Liguori
6
+ * @date 2023-12-29
7
+ * @version 3.1.0
8
+ *
9
+ * Copyright 2023, 2024, 2025 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
+ */
1
23
  import { MatterbridgeDevice, airQualitySensor, colorTemperatureSwitch, dimmableSwitch, onOffSwitch, OnOff, LevelControl, ColorControl, ColorControlCluster, TemperatureMeasurement, BooleanState, RelativeHumidityMeasurement, PressureMeasurement, OccupancySensing, IlluminanceMeasurement, PowerSource, ClusterId, WindowCovering, DoorLock, BridgedDeviceBasicInformation, ThermostatCluster, Thermostat, getClusterNameById, powerSource, bridgedNode, AirQuality, TotalVolatileOrganicCompoundsConcentrationMeasurement, CarbonDioxideConcentrationMeasurement, CarbonMonoxideConcentrationMeasurement, FormaldehydeConcentrationMeasurement, Pm1ConcentrationMeasurement, Pm25ConcentrationMeasurement, Pm10ConcentrationMeasurement, electricalSensor, ElectricalEnergyMeasurement, ElectricalPowerMeasurement, onOffLight, dimmableLight, colorTemperatureLight, onOffOutlet, SwitchesTag, NumberTag, coverDevice, thermostatDevice, MatterbridgeEndpoint, dimmableOutlet, doorLockDevice, occupancySensor, lightSensor, contactSensor, temperatureSensor, humiditySensor, pressureSensor, genericSwitch, OnOffCluster, LevelControlCluster, WindowCoveringCluster, DoorLockCluster, } from 'matterbridge';
2
24
  import { AnsiLogger, gn, dn, ign, idn, rs, db, debugStringify, hk, zb, or, nf, CYAN, er, YELLOW } from 'matterbridge/logger';
3
25
  import { deepCopy, deepEqual, isValidNumber } from 'matterbridge/utils';
4
26
  import * as color from 'matterbridge/utils';
5
27
  import EventEmitter from 'events';
6
28
  import { hostname } from 'os';
29
+ /**
30
+ * Represents a Zigbee entity: a group or a device.
31
+ *
32
+ * @class
33
+ * @extends {EventEmitter}
34
+ */
7
35
  export class ZigbeeEntity extends EventEmitter {
8
36
  log;
9
37
  serial = '';
@@ -23,6 +51,7 @@ export class ZigbeeEntity extends EventEmitter {
23
51
  ignoreFeatures = [];
24
52
  transition = false;
25
53
  propertyMap = new Map();
54
+ // We save the tag list and device types and cluster servers and clients to avoid multiple lookups
26
55
  mutableDevice = new Map();
27
56
  colorTimeout = undefined;
28
57
  thermostatTimeout = undefined;
@@ -30,6 +59,12 @@ export class ZigbeeEntity extends EventEmitter {
30
59
  hasEndpoints = false;
31
60
  isRouter = false;
32
61
  noUpdate = false;
62
+ /**
63
+ * Creates an instance of ZigbeeEntity.
64
+ *
65
+ * @param {ZigbeePlatform} platform - The Zigbee platform instance.
66
+ * @param {BridgeDevice | BridgeGroup} entity - The bridge device or group instance received from zigbee2mqtt.
67
+ */
33
68
  constructor(platform, entity) {
34
69
  super();
35
70
  this.platform = platform;
@@ -47,22 +82,26 @@ export class ZigbeeEntity extends EventEmitter {
47
82
  this.en = gn;
48
83
  this.ien = ign;
49
84
  }
50
- this.log = new AnsiLogger({ logName: this.entityName, logTimestampFormat: 4, logLevel: platform.debugEnabled ? "debug" : "info" });
85
+ this.log = new AnsiLogger({ logName: this.entityName, logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */, logLevel: platform.debugEnabled ? "debug" /* LogLevel.DEBUG */ : "info" /* LogLevel.INFO */ });
51
86
  this.log.debug(`Created MatterEntity: ${this.entityName}`);
52
87
  this.platform.z2m.on('MESSAGE-' + this.entityName, (payload) => {
88
+ // Check if the message is a duplicate that can be ingored cause only linkquality and last_seen have changed (action is always passed)
53
89
  const now = Date.now();
54
90
  if (now - this.lastSeen < 1000 * 60 && deepEqual(this.lastPayload, payload, ['linkquality', 'last_seen', ...this.ignoreFeatures]) && !Object.prototype.hasOwnProperty.call(this.lastPayload, 'action')) {
55
91
  this.log.debug(`Skipping not changed ${platform.z2mDevicesRegistered ? 'MQTT message' : 'State update'} for accessory ${this.entityName}`);
56
92
  return;
57
93
  }
58
94
  this.lastSeen = Date.now();
95
+ // Check and deep copy the payload
59
96
  if (deepEqual(this.lastPayload, payload, this.ignoreFeatures))
60
97
  return;
61
98
  this.lastPayload = deepCopy(payload);
62
99
  if (Object.prototype.hasOwnProperty.call(this.lastPayload, 'action'))
63
100
  delete this.lastPayload.action;
101
+ // Remove each key in ignoreFeatures from the payload copy
64
102
  for (const key of this.ignoreFeatures) {
65
103
  if (Object.prototype.hasOwnProperty.call(payload, key)) {
104
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
66
105
  delete payload[key];
67
106
  this.log.debug(`Removed key ${CYAN}${key}${db} from payload`);
68
107
  }
@@ -76,19 +115,24 @@ export class ZigbeeEntity extends EventEmitter {
76
115
  return;
77
116
  }
78
117
  this.log.info(`${db}${platform.z2mDevicesRegistered ? 'MQTT message' : 'State update'} for device ${this.ien}${this.entityName}${rs}${db} payload: ${debugStringify(payload)}`);
118
+ // Parse the payload and update the accessory
79
119
  Object.entries(payload).forEach(([key, value]) => {
120
+ // Skip null and undefined values
80
121
  if (value === undefined || value === null)
81
122
  return;
82
123
  if (this.bridgedDevice === undefined || this.noUpdate)
83
124
  return;
125
+ // Modify voltage to battery_voltage
84
126
  if (key === 'voltage' && this.isDevice && this.device?.power_source === 'Battery')
85
127
  key = 'battery_voltage';
128
+ // Modify illuminance and illuminance_lux
86
129
  if (key === 'illuminance' && this.isDevice && this.device && this.device.definition && ['RTCGQ14LM'].includes(this.device.definition.model)) {
87
130
  key = 'illuminance_lux';
88
131
  }
89
132
  if (key === 'illuminance' && typeof value === 'number' && this.isDevice && this.device && this.device.definition && ['ZG-204ZL', 'ZG-205Z/A'].includes(this.device.definition.model)) {
90
133
  value = value * 10;
91
134
  }
135
+ // Lookup the property in the propertyMap and ZigbeeToMatter table
92
136
  const propertyMap = this.propertyMap.get(key);
93
137
  if (propertyMap) {
94
138
  this.log.debug(`Payload entry ${CYAN}${key}${db} => name: ${CYAN}${propertyMap.name}${db} type: ${CYAN}${propertyMap.type === '' ? 'generic' : propertyMap.type}${db} ` +
@@ -108,9 +152,11 @@ export class ZigbeeEntity extends EventEmitter {
108
152
  }
109
153
  else
110
154
  this.log.debug(`*Payload entry ${CYAN}${key}${db} not found in propertyMap`);
155
+ // Switch actions on the endpoints
111
156
  if (key === 'action' && value !== '') {
112
157
  const propertyMap = this.propertyMap.get(('action_' + value));
113
158
  if (propertyMap) {
159
+ // this.log.debug(`Payload entry ${CYAN}${value}${db} => name: ${CYAN}${propertyMap.name}${db} endpoint: ${CYAN}${propertyMap.endpoint}${db} action: ${CYAN}${propertyMap.action}${db}`);
114
160
  const child = this.bridgedDevice.getChildEndpointByName(propertyMap.endpoint);
115
161
  if (child && child.number)
116
162
  this.bridgedDevice.triggerSwitchEvent(propertyMap.action, this.log, child);
@@ -118,6 +164,9 @@ export class ZigbeeEntity extends EventEmitter {
118
164
  else
119
165
  this.log.debug(`*Payload entry ${CYAN}${('action_' + value)}${db} not found in propertyMap`);
120
166
  }
167
+ // WindowCovering
168
+ // Zigbee2MQTT cover: 0 = open, 100 = closed
169
+ // Matter WindowCovering: 0 = open 10000 = closed
121
170
  if (key === 'position' && this.isDevice && isValidNumber(value, 0, 100)) {
122
171
  this.updateAttributeIfChanged(this.bridgedDevice, undefined, WindowCovering.Cluster.id, 'currentPositionLiftPercent100ths', value * 100);
123
172
  }
@@ -138,10 +187,12 @@ export class ZigbeeEntity extends EventEmitter {
138
187
  this.updateAttributeIfChanged(this.bridgedDevice, undefined, WindowCovering.Cluster.id, 'targetPositionLiftPercent100ths', position);
139
188
  }
140
189
  }
190
+ // ColorControl colorTemperatureMired and colorMode
141
191
  if (key === 'color_temp' && 'color_mode' in payload && payload['color_mode'] === 'color_temp') {
142
192
  this.updateAttributeIfChanged(this.bridgedDevice, undefined, ColorControl.Cluster.id, 'colorTemperatureMireds', Math.max(147, Math.min(500, typeof value === 'number' ? value : 0)));
143
193
  this.updateAttributeIfChanged(this.bridgedDevice, undefined, ColorControl.Cluster.id, 'colorMode', ColorControl.ColorMode.ColorTemperatureMireds);
144
194
  }
195
+ // ColorControl currentHue, currentSaturation and colorMode
145
196
  if (key === 'color' && 'color_mode' in payload && payload['color_mode'] === 'xy') {
146
197
  const { x, y } = value;
147
198
  const hsl = color.xyToHsl(x, y);
@@ -169,6 +220,13 @@ export class ZigbeeEntity extends EventEmitter {
169
220
  }
170
221
  });
171
222
  }
223
+ /**
224
+ * Destroys the ZigbeeEntity instance by clearing any active timeouts.
225
+ *
226
+ * @remarks
227
+ * This method is used to clean up the ZigbeeEntity instance by clearing any active timeouts for color and thermostat operations.
228
+ * It ensures that no further actions are taken on these timeouts after the entity is destroyed.
229
+ */
172
230
  destroy() {
173
231
  if (this.colorTimeout)
174
232
  clearTimeout(this.colorTimeout);
@@ -177,6 +235,22 @@ export class ZigbeeEntity extends EventEmitter {
177
235
  clearTimeout(this.thermostatTimeout);
178
236
  this.thermostatTimeout = undefined;
179
237
  }
238
+ /**
239
+ * Creates a mutable device with the specified definition and includes the specified server list.
240
+ *
241
+ * @param {DeviceTypeDefinition | AtLeastOne<DeviceTypeDefinition>} definition - The device type definition.
242
+ * @param {ClusterId[]} [includeServerList=[]] - The list of server clusters to include.
243
+ * @param {EndpointOptions} [options] - Optional endpoint options.
244
+ * @param {boolean} [debug] - Optional debug flag.
245
+ * @returns {MatterbridgeDevice} The created mutable device.
246
+ *
247
+ * @remarks
248
+ * This method creates a mutable device based on the provided definition. It adds the specified server clusters
249
+ * to the device and configures the device with basic information and power source clusters. If the device is a
250
+ * coordinator, it sets up the basic information cluster with coordinator-specific details. If the device is a
251
+ * group, it sets up the basic information cluster with group-specific details. The method also configures the
252
+ * power source cluster based on the device's power source.
253
+ */
180
254
  async createMutableDevice(definition, options, debug) {
181
255
  if (this.platform.matterbridge.edge === true) {
182
256
  this.bridgedDevice = (await MatterbridgeEndpoint.loadInstance(definition, options, debug));
@@ -189,6 +263,7 @@ export class ZigbeeEntity extends EventEmitter {
189
263
  getBridgedDeviceBasicInformation() {
190
264
  if (!this.bridgedDevice)
191
265
  throw new Error('No bridged device');
266
+ // Add BridgedDeviceBasicInformation cluster and device type
192
267
  const softwareVersion = parseInt(this.platform.z2mBridgeInfo?.version || '1');
193
268
  const softwareVersionString = `${this.platform.z2mBridgeInfo?.version} (commit ${this.platform.z2mBridgeInfo?.commit})`;
194
269
  const hardwareVersion = parseInt(this.platform.matterbridge.matterbridgeVersion || '1');
@@ -206,6 +281,7 @@ export class ZigbeeEntity extends EventEmitter {
206
281
  addBridgedDeviceBasicInformation() {
207
282
  if (!this.bridgedDevice)
208
283
  throw new Error('No bridged device');
284
+ // Add BridgedDeviceBasicInformation cluster and device type
209
285
  this.bridgedDevice.addDeviceType(bridgedNode);
210
286
  this.bridgedDevice.addClusterServer(this.getBridgedDeviceBasicInformation());
211
287
  return this.bridgedDevice;
@@ -226,13 +302,26 @@ export class ZigbeeEntity extends EventEmitter {
226
302
  addPowerSource() {
227
303
  if (!this.bridgedDevice)
228
304
  throw new Error('No bridged device');
305
+ // Add PowerSource device type and cluster
229
306
  this.bridgedDevice.addDeviceType(powerSource);
230
307
  this.bridgedDevice.addClusterServer(this.getPowerSource());
231
308
  return this.bridgedDevice;
232
309
  }
310
+ /**
311
+ * Verifies that all required server clusters are present on the main endpoint and child endpoints.
312
+ *
313
+ * @param {MatterbridgeDevice} endpoint - The device endpoint to verify.
314
+ * @returns {boolean} True if all required server clusters are present, false otherwise.
315
+ *
316
+ * @remarks
317
+ * This method checks if all required server clusters are present on the main endpoint and its child endpoints.
318
+ * It logs an error message if any required server cluster is missing and returns false. If all required server
319
+ * clusters are present, it returns true.
320
+ */
233
321
  verifyMutableDevice(endpoint) {
234
322
  if (!endpoint)
235
323
  return false;
324
+ // Verify that all required server clusters are present in the main endpoint and in the child endpoints
236
325
  for (const deviceType of endpoint.getDeviceTypes()) {
237
326
  for (const clusterId of deviceType.requiredServerClusters) {
238
327
  if (!endpoint.getClusterServerById(clusterId)) {
@@ -241,6 +330,7 @@ export class ZigbeeEntity extends EventEmitter {
241
330
  }
242
331
  }
243
332
  }
333
+ // Verify that all required server clusters are present in the child endpoints
244
334
  for (const childEndpoint of endpoint.getChildEndpoints()) {
245
335
  for (const deviceType of childEndpoint.getDeviceTypes()) {
246
336
  for (const clusterId of deviceType.requiredServerClusters) {
@@ -253,6 +343,17 @@ export class ZigbeeEntity extends EventEmitter {
253
343
  }
254
344
  return true;
255
345
  }
346
+ /**
347
+ * Configures the device by setting up the WindowCovering and DoorLock clusters if they are present.
348
+ *
349
+ * @returns {Promise<void>} A promise that resolves when the configuration is complete.
350
+ *
351
+ * @remarks
352
+ * This method configures the device by checking for the presence of the WindowCovering and DoorLock clusters.
353
+ * If the WindowCovering cluster is present, it sets the target as the current position and stops any ongoing
354
+ * movement. If the DoorLock cluster is present, it retrieves the lock state and triggers the appropriate lock
355
+ * operation event based on the current state.
356
+ */
256
357
  async configure() {
257
358
  if (this.bridgedDevice?.getClusterServerById(WindowCovering.Cluster.id)) {
258
359
  this.log.info(`Configuring ${this.bridgedDevice?.deviceName} WindowCovering cluster`);
@@ -269,6 +370,22 @@ export class ZigbeeEntity extends EventEmitter {
269
370
  }
270
371
  }
271
372
  }
373
+ /**
374
+ * Updates the attribute of a cluster on a device endpoint if the value has changed.
375
+ *
376
+ * @param {Endpoint} deviceEndpoint - The device endpoint to update.
377
+ * @param {string | undefined} childEndpointName - The name of the child endpoint, if any.
378
+ * @param {number} clusterId - The ID of the cluster to update.
379
+ * @param {string} attributeName - The name of the attribute to update.
380
+ * @param {PayloadValue} value - The new value of the attribute.
381
+ * @param {string[]} [lookup] - Optional lookup array for converting string values to indices.
382
+ *
383
+ * @remarks
384
+ * This method checks if the specified attribute of a cluster on a device endpoint has changed. If the attribute
385
+ * has changed, it updates the attribute with the new value. If a lookup array is provided, it converts string
386
+ * values to their corresponding indices in the lookup array. The method logs the update process and handles any
387
+ * errors that occur during the update.
388
+ */
272
389
  updateAttributeIfChanged(deviceEndpoint, childEndpointName, clusterId, attributeName, value, lookup) {
273
390
  if (childEndpointName && childEndpointName !== '') {
274
391
  deviceEndpoint = this.bridgedDevice?.getChildEndpointByName(childEndpointName) ?? deviceEndpoint;
@@ -307,6 +424,18 @@ export class ZigbeeEntity extends EventEmitter {
307
424
  this.log.error(`Error setting attribute ${hk}${getClusterNameById(ClusterId(clusterId))}${er}.${hk}${attributeName}${er} to ${value}: ${error}`);
308
425
  }
309
426
  }
427
+ /**
428
+ * Publishes a command to the specified entity with the given payload.
429
+ *
430
+ * @param {string} command - The command to execute.
431
+ * @param {string} entityName - The name of the entity to publish the command to.
432
+ * @param {Payload} payload - The payload of the command.
433
+ *
434
+ * @remarks
435
+ * This method logs the execution of the command and publishes the command to the specified entity.
436
+ * If the entity name starts with 'bridge/request', it publishes the payload without a 'set' suffix.
437
+ * Otherwise, it publishes the payload with a 'set' suffix.
438
+ */
310
439
  publishCommand(command, entityName, payload) {
311
440
  this.log.debug(`executeCommand ${command} called for ${this.ien}${entityName}${rs}${db} payload: ${debugStringify(payload)}`);
312
441
  if (entityName.startsWith('bridge/request')) {
@@ -316,7 +445,16 @@ export class ZigbeeEntity extends EventEmitter {
316
445
  this.platform.publish(entityName, 'set', JSON.stringify(payload));
317
446
  }
318
447
  }
448
+ /**
449
+ * Logs the property map of the Zigbee entity.
450
+ *
451
+ * @remarks
452
+ * This method iterates over the property map of the Zigbee entity and logs each property's details,
453
+ * including its name, type, values, minimum and maximum values, unit, and endpoint.
454
+ */
455
+ // zigbeeDevice.propertyMap.set(property, { name, type, endpoint, category, description, label, unit, value_min, value_max, values: value });
319
456
  logPropertyMap() {
457
+ // Log properties
320
458
  this.propertyMap.forEach((value, key) => {
321
459
  this.log.debug(`Property ${CYAN}${key}${db} name ${CYAN}${value.name}${db} type ${CYAN}${value.type === '' ? 'generic' : value.type}${db} endpoint ${CYAN}${value.endpoint === '' ? 'main' : value.endpoint}${db} ` +
322
460
  `category ${CYAN}${value.category}${db} description ${CYAN}${value.description}${db} label ${CYAN}${value.label}${db} unit ${CYAN}${value.unit}${db} ` +
@@ -324,10 +462,33 @@ export class ZigbeeEntity extends EventEmitter {
324
462
  });
325
463
  }
326
464
  }
465
+ /**
466
+ * Represents a Zigbee group entity.
467
+ *
468
+ * @class
469
+ * @extends {ZigbeeEntity}
470
+ */
327
471
  export class ZigbeeGroup extends ZigbeeEntity {
472
+ /**
473
+ * Creates an instance of ZigbeeGroup.
474
+ *
475
+ * @param {ZigbeePlatform} platform - The Zigbee platform instance.
476
+ * @param {BridgeGroup} group - The bridge group instance.
477
+ */
328
478
  constructor(platform, group) {
329
479
  super(platform, group);
330
480
  }
481
+ /**
482
+ * Creates a new ZigbeeGroup instance.
483
+ *
484
+ * @param {ZigbeePlatform} platform - The Zigbee platform instance.
485
+ * @param {BridgeGroup} group - The bridge group instance.
486
+ * @returns {Promise<ZigbeeGroup>} A promise that resolves to the created ZigbeeGroup instance.
487
+ *
488
+ * @remarks
489
+ * This method initializes a new ZigbeeGroup instance, sets up its properties, and configures the device
490
+ * based on the group members. It also adds command handlers for the group.
491
+ */
331
492
  static async create(platform, group) {
332
493
  const zigbeeGroup = new ZigbeeGroup(platform, group);
333
494
  if (zigbeeGroup.platform.postfixHostname) {
@@ -347,19 +508,23 @@ export class ZigbeeGroup extends ZigbeeEntity {
347
508
  let isCover = false;
348
509
  let isThermostat = false;
349
510
  if (group.members.length === 0) {
511
+ // Create a virtual device for the empty group to use in automations
350
512
  zigbeeGroup.log.debug(`Group: ${gn}${group.friendly_name}${rs}${db} is a ${CYAN}virtual${db} group`);
351
- zigbeeGroup.bridgedDevice = await zigbeeGroup.createMutableDevice([onOffSwitch], { uniqueStorageKey: group.friendly_name }, zigbeeGroup.log.logLevel === "debug");
513
+ zigbeeGroup.bridgedDevice = await zigbeeGroup.createMutableDevice([onOffSwitch], { uniqueStorageKey: group.friendly_name }, zigbeeGroup.log.logLevel === "debug" /* LogLevel.DEBUG */);
352
514
  isSwitch = true;
353
515
  zigbeeGroup.propertyMap.set('state', { name: 'state', type: 'switch', endpoint: '' });
354
516
  }
355
517
  else {
518
+ // Create a switch or light or outlet device for the group
356
519
  group.members.forEach((member) => {
520
+ // const device = zigbeeGroup.platform.z2m.getDevice(member.ieee_address);
357
521
  const device = zigbeeGroup.platform.z2mBridgeDevices?.find((device) => device.ieee_address === member.ieee_address);
358
522
  if (!device)
359
523
  return;
360
524
  zigbeeGroup.log.debug(`Group ${gn}${group.friendly_name}${db}: member device ${dn}${device.friendly_name}${db}`);
361
525
  device.definition?.exposes.forEach((expose) => {
362
526
  if (expose.features) {
527
+ // Specific features with type
363
528
  expose.features?.forEach((feature) => {
364
529
  if (expose.type === 'lock' && feature.name === 'state' && feature.property === 'child_lock') {
365
530
  expose.type = 'child_lock';
@@ -389,6 +554,7 @@ export class ZigbeeGroup extends ZigbeeEntity {
389
554
  });
390
555
  }
391
556
  else {
557
+ // Generic features without type
392
558
  zigbeeGroup.log.debug(`- generic type ${CYAN}${expose.type}${db} expose name ${CYAN}${expose.name}${db} property ${CYAN}${expose.property}${db}`);
393
559
  }
394
560
  });
@@ -434,14 +600,17 @@ export class ZigbeeGroup extends ZigbeeEntity {
434
600
  }
435
601
  if (!deviceType)
436
602
  return zigbeeGroup;
437
- zigbeeGroup.bridgedDevice = await zigbeeGroup.createMutableDevice([deviceType], { uniqueStorageKey: group.friendly_name }, zigbeeGroup.log.logLevel === "debug");
603
+ zigbeeGroup.bridgedDevice = await zigbeeGroup.createMutableDevice([deviceType], { uniqueStorageKey: group.friendly_name }, zigbeeGroup.log.logLevel === "debug" /* LogLevel.DEBUG */);
438
604
  }
439
605
  zigbeeGroup.addBridgedDeviceBasicInformation();
440
606
  zigbeeGroup.addPowerSource();
441
607
  zigbeeGroup.bridgedDevice.addRequiredClusterServers(zigbeeGroup.bridgedDevice);
608
+ // Verify the device
442
609
  if (!zigbeeGroup.bridgedDevice || !zigbeeGroup.verifyMutableDevice(zigbeeGroup.bridgedDevice))
443
610
  return zigbeeGroup;
611
+ // Log properties
444
612
  zigbeeGroup.logPropertyMap();
613
+ // Add command handlers
445
614
  if (isSwitch || isLight) {
446
615
  if (isSwitch)
447
616
  await zigbeeGroup.bridgedDevice.addFixedLabel('type', 'switch');
@@ -449,6 +618,7 @@ export class ZigbeeGroup extends ZigbeeEntity {
449
618
  await zigbeeGroup.bridgedDevice.addFixedLabel('type', 'light');
450
619
  zigbeeGroup.bridgedDevice.addCommandHandler('identify', async ({ request: { identifyTime } }) => {
451
620
  zigbeeGroup.log.warn(`Command identify called for ${zigbeeGroup.ien}${group.friendly_name}${rs}${db} identifyTime:${identifyTime}`);
621
+ // logEndpoint(zigbeeGroup.bridgedDevice!);
452
622
  });
453
623
  zigbeeGroup.bridgedDevice.addCommandHandler('on', async () => {
454
624
  zigbeeGroup.log.debug(`Command on called for ${zigbeeGroup.ien}${group.friendly_name}${rs}${db}`);
@@ -540,6 +710,7 @@ export class ZigbeeGroup extends ZigbeeEntity {
540
710
  zigbeeGroup.bridgedDevice.subscribeAttribute(ThermostatCluster.id, 'systemMode', (newValue, oldValue) => {
541
711
  zigbeeGroup.bridgedDevice?.log.info(`Thermostat systemMode changed from ${oldValue} to ${newValue}`);
542
712
  if (oldValue !== newValue) {
713
+ // Thermostat.SystemMode.Heat && newValue === Thermostat.SystemMode.Off
543
714
  zigbeeGroup.bridgedDevice?.log.info(`Setting thermostat systemMode to ${newValue}`);
544
715
  if (newValue === Thermostat.SystemMode.Off) {
545
716
  zigbeeGroup.publishCommand('SystemMode', group.friendly_name, { system_mode: 'off' });
@@ -580,6 +751,7 @@ export class ZigbeeGroup extends ZigbeeEntity {
580
751
  return zigbeeGroup;
581
752
  }
582
753
  }
754
+ // prettier-ignore
583
755
  export const z2ms = [
584
756
  { type: 'switch', name: 'state', property: 'state', deviceType: onOffSwitch, cluster: OnOff.Cluster.id, attribute: 'onOff', converter: (value) => { return value === 'ON' ? true : false; } },
585
757
  { type: 'switch', name: 'brightness', property: 'brightness', deviceType: dimmableSwitch, cluster: LevelControl.Cluster.id, attribute: 'currentLevel', converter: (value) => { return Math.max(0, Math.min(254, value)); } },
@@ -639,19 +811,43 @@ export const z2ms = [
639
811
  { type: '', name: 'voltage', property: 'voltage', deviceType: electricalSensor, cluster: ElectricalPowerMeasurement.Cluster.id, attribute: 'voltage', converter: (value) => { return value * 1000; } },
640
812
  { type: '', name: 'current', property: 'current', deviceType: electricalSensor, cluster: ElectricalPowerMeasurement.Cluster.id, attribute: 'activeCurrent', converter: (value) => { return value * 1000; } },
641
813
  ];
814
+ /**
815
+ * Represents a Zigbee device entity.
816
+ *
817
+ * @class
818
+ * @extends {ZigbeeEntity}
819
+ */
642
820
  export class ZigbeeDevice extends ZigbeeEntity {
821
+ /**
822
+ * Represents a Zigbee device entity.
823
+ *
824
+ * @class
825
+ * @extends {ZigbeeEntity}
826
+ */
643
827
  constructor(platform, device) {
644
828
  super(platform, device);
645
829
  }
830
+ /**
831
+ * Creates a new ZigbeeDevice instance.
832
+ *
833
+ * @param {ZigbeePlatform} platform - The Zigbee platform instance.
834
+ * @param {BridgeDevice} device - The bridge device instance.
835
+ * @returns {Promise<ZigbeeDevice>} A promise that resolves to the created ZigbeeDevice instance.
836
+ *
837
+ * @remarks
838
+ * This method initializes a new ZigbeeDevice instance, sets up its properties, and configures the device
839
+ * based on the device definition and options. It also adds command handlers for the device.
840
+ */
646
841
  static async create(platform, device) {
647
842
  const zigbeeDevice = new ZigbeeDevice(platform, device);
648
843
  zigbeeDevice.serial = `${device.ieee_address}`;
649
844
  if (zigbeeDevice.platform.postfixHostname) {
650
845
  zigbeeDevice.serial = `${zigbeeDevice.serial}_${hostname}`.slice(0, 32);
651
846
  }
847
+ // Set Coordinator and dedicated routers
652
848
  if (device.friendly_name === 'Coordinator' || (device.model_id === 'ti.router' && device.manufacturer === 'TexasInstruments') || (device.model_id.startsWith('SLZB-') && device.manufacturer === 'SMLIGHT')) {
653
849
  zigbeeDevice.isRouter = true;
654
- zigbeeDevice.bridgedDevice = await zigbeeDevice.createMutableDevice([doorLockDevice], { uniqueStorageKey: device.friendly_name }, zigbeeDevice.log.logLevel === "debug");
850
+ zigbeeDevice.bridgedDevice = await zigbeeDevice.createMutableDevice([doorLockDevice], { uniqueStorageKey: device.friendly_name }, zigbeeDevice.log.logLevel === "debug" /* LogLevel.DEBUG */);
655
851
  zigbeeDevice.addBridgedDeviceBasicInformation();
656
852
  zigbeeDevice.addPowerSource();
657
853
  zigbeeDevice.bridgedDevice.addRequiredClusterServers(zigbeeDevice.bridgedDevice);
@@ -669,6 +865,7 @@ export class ZigbeeDevice extends ZigbeeEntity {
669
865
  });
670
866
  return zigbeeDevice;
671
867
  }
868
+ // Get types and properties
672
869
  const types = [];
673
870
  const endpoints = [];
674
871
  const names = [];
@@ -682,6 +879,7 @@ export class ZigbeeDevice extends ZigbeeEntity {
682
879
  const values = [];
683
880
  device.definition?.exposes.forEach((expose) => {
684
881
  if (expose.features) {
882
+ // Specific features with type
685
883
  expose.features?.forEach((feature) => {
686
884
  if (expose.type === 'lock' && feature.name === 'state' && feature.property === 'child_lock')
687
885
  feature.name = 'child_lock';
@@ -699,10 +897,17 @@ export class ZigbeeDevice extends ZigbeeEntity {
699
897
  });
700
898
  }
701
899
  else {
900
+ // Generic features without type
901
+ // Change voltage to battery_voltage for battery powered devices
702
902
  if (device.power_source === 'Battery' && expose.name === 'voltage')
703
903
  expose.name = 'battery_voltage';
704
904
  if (device.power_source === 'Battery' && expose.property === 'voltage')
705
905
  expose.property = 'battery_voltage';
906
+ // Fix illuminance and illuminance_lux for light sensors:
907
+ // illuminance is raw value (use like it is)
908
+ // illuminance_lux is in lux (convert with log10)
909
+ // illuminance has description "Raw measured illuminance"
910
+ // illuminance_lux has description "Measured illuminance in lux"
706
911
  if (expose.description === 'Raw measured illuminance') {
707
912
  expose.name = 'illuminance';
708
913
  expose.property = 'illuminance';
@@ -763,6 +968,16 @@ export class ZigbeeDevice extends ZigbeeEntity {
763
968
  zigbeeDevice.ignoreFeatures = [...zigbeeDevice.ignoreFeatures, ...platform.featureBlackList];
764
969
  if (platform.deviceFeatureBlackList[device.friendly_name])
765
970
  zigbeeDevice.ignoreFeatures = [...zigbeeDevice.ignoreFeatures, ...platform.deviceFeatureBlackList[device.friendly_name]];
971
+ /*
972
+ zigbeeDevice.log.debug(`Device ${zigbeeDevice.ien}${device.friendly_name}${rs}${db} - types[${types.length}]: ${debugStringify(types)}`);
973
+ zigbeeDevice.log.debug(`Device ${zigbeeDevice.ien}${device.friendly_name}${rs}${db} - endpoints[${endpoints.length}]: ${debugStringify(endpoints)}`);
974
+ zigbeeDevice.log.debug(`Device ${zigbeeDevice.ien}${device.friendly_name}${rs}${db} - names[${names.length}]: ${debugStringify(names)}`);
975
+ zigbeeDevice.log.debug(`Device ${zigbeeDevice.ien}${device.friendly_name}${rs}${db} - properties[${properties.length}]: ${debugStringify(properties)}`);
976
+ zigbeeDevice.log.debug(`Device ${zigbeeDevice.ien}${device.friendly_name}${rs}${db} - categories[${categories.length}]: ${debugStringify(categories)}`);
977
+ zigbeeDevice.log.debug(`Device ${zigbeeDevice.ien}${device.friendly_name}${rs}${db} - descriptions[${descriptions.length}]: ${debugStringify(descriptions)}`);
978
+ zigbeeDevice.log.debug(`Device ${zigbeeDevice.ien}${device.friendly_name}${rs}${db} - labels[${labels.length}]: ${debugStringify(labels)}`);
979
+ zigbeeDevice.log.debug(`Device ${zigbeeDevice.ien}${device.friendly_name}${rs}${db} - units[${units.length}]: ${debugStringify(units)}`);
980
+ */
766
981
  for (const [index, name] of names.entries()) {
767
982
  if (platform.featureBlackList.includes(name)) {
768
983
  zigbeeDevice.log.debug(`Device ${zigbeeDevice.en}${device.friendly_name}${db} feature ${name} is globally blacklisted`);
@@ -791,6 +1006,7 @@ export class ZigbeeDevice extends ZigbeeEntity {
791
1006
  zigbeeDevice.log.debug(`Device ${zigbeeDevice.ien}${device.friendly_name}${rs}${db} endpoint: ${zb}${endpoint}${db} type: ${zb}${type}${db} property: ${zb}${name}${db} => deviceType: ${z2m.deviceType?.name} cluster: ${z2m.cluster} attribute: ${z2m.attribute}`);
792
1007
  zigbeeDevice.propertyMap.set(property, { name, type, endpoint, category, description, label, unit, value_min, value_max, values: value });
793
1008
  if (endpoint === '') {
1009
+ /* prettier-ignore */
794
1010
  if (!zigbeeDevice.mutableDevice.has(endpoint)) {
795
1011
  zigbeeDevice.mutableDevice.set(endpoint, { tagList: [], deviceTypes: [z2m.deviceType], clusterServersIds: [...z2m.deviceType.requiredServerClusters, ClusterId(z2m.cluster)], clusterServersObjs: [], clusterClientsIds: [], clusterClientsObjs: [] });
796
1012
  }
@@ -814,6 +1030,7 @@ export class ZigbeeDevice extends ZigbeeEntity {
814
1030
  if (endpoint === 'l6')
815
1031
  tagList.push({ mfgCode: null, namespaceId: NumberTag.Six.namespaceId, tag: NumberTag.Six.tag, label: 'endpoint ' + endpoint });
816
1032
  tagList.push({ mfgCode: null, namespaceId: SwitchesTag.Custom.namespaceId, tag: SwitchesTag.Custom.tag, label: 'endpoint ' + endpoint });
1033
+ /* prettier-ignore */
817
1034
  if (!zigbeeDevice.mutableDevice.has(endpoint)) {
818
1035
  zigbeeDevice.mutableDevice.set(endpoint, { tagList, deviceTypes: [z2m.deviceType], clusterServersIds: [...z2m.deviceType.requiredServerClusters, ClusterId(z2m.cluster)], clusterServersObjs: [], clusterClientsIds: [], clusterClientsObjs: [] });
819
1036
  }
@@ -827,13 +1044,16 @@ export class ZigbeeDevice extends ZigbeeEntity {
827
1044
  }
828
1045
  }
829
1046
  else {
1047
+ // zigbeeDevice.log.debug(`Device ${zigbeeDevice.ien}${device.friendly_name}${rs}${db} endpoint: ${zb}${endpoint}${db} type: ${zb}${type}${db} property: ${zb}${name}${db} => no mapping found`);
830
1048
  }
1049
+ // Map actions to switches
831
1050
  if (name === 'action' && zigbeeDevice.actions.length) {
832
1051
  zigbeeDevice.log.info(`Device ${zigbeeDevice.ien}${device.friendly_name}${rs}${nf} has actions mapped to these switches on sub endpoints:`);
833
1052
  zigbeeDevice.log.info(' controller events <=> zigbee2mqtt actions');
834
1053
  if (!zigbeeDevice.bridgedDevice)
835
- zigbeeDevice.bridgedDevice = await zigbeeDevice.createMutableDevice([bridgedNode], { uniqueStorageKey: device.friendly_name }, zigbeeDevice.log.logLevel === "debug");
1054
+ zigbeeDevice.bridgedDevice = await zigbeeDevice.createMutableDevice([bridgedNode], { uniqueStorageKey: device.friendly_name }, zigbeeDevice.log.logLevel === "debug" /* LogLevel.DEBUG */);
836
1055
  zigbeeDevice.hasEndpoints = true;
1056
+ // Mapping actions
837
1057
  const switchMap = ['Single Press', 'Double Press', 'Long Press '];
838
1058
  const triggerMap = ['Single', 'Double', 'Long'];
839
1059
  let count = 1;
@@ -866,16 +1086,19 @@ export class ZigbeeDevice extends ZigbeeEntity {
866
1086
  zigbeeDevice.composedType = 'button';
867
1087
  }
868
1088
  }
1089
+ // Add battery properties
869
1090
  if (device.power_source === 'Battery') {
870
1091
  zigbeeDevice.propertyMap.set('battery', { name: 'battery', type: '', endpoint: '' });
871
1092
  zigbeeDevice.propertyMap.set('battery_low', { name: 'battery_low', type: '', endpoint: '' });
872
1093
  zigbeeDevice.propertyMap.set('battery_voltage', { name: 'battery_voltage', type: '', endpoint: '' });
873
1094
  }
1095
+ // Handle when the device has only child endpoints
874
1096
  if (!zigbeeDevice.mutableDevice.has(''))
875
1097
  zigbeeDevice.mutableDevice.set('', { tagList: [], deviceTypes: [bridgedNode, powerSource], clusterServersIds: [], clusterServersObjs: [], clusterClientsIds: [], clusterClientsObjs: [] });
876
1098
  const mainEndpoint = zigbeeDevice.mutableDevice.get('');
877
1099
  if (!mainEndpoint)
878
1100
  return zigbeeDevice;
1101
+ // Remove duplicates and superset device Types on all endpoints
879
1102
  for (const device of zigbeeDevice.mutableDevice.values()) {
880
1103
  const deviceTypesMap = new Map();
881
1104
  device.deviceTypes.forEach((deviceType) => {
@@ -891,15 +1114,20 @@ export class ZigbeeDevice extends ZigbeeEntity {
891
1114
  deviceTypesMap.delete(onOffLight.code);
892
1115
  if (deviceTypesMap.has(dimmableLight.code) && deviceTypesMap.has(colorTemperatureLight.code))
893
1116
  deviceTypesMap.delete(dimmableLight.code);
894
- device.deviceTypes = Array.from(deviceTypesMap.values());
1117
+ device.deviceTypes = Array.from(deviceTypesMap.values()); /* .sort((a, b) => b.code - a.code);*/
895
1118
  }
896
- zigbeeDevice.bridgedDevice = await zigbeeDevice.createMutableDevice(mainEndpoint.deviceTypes, { uniqueStorageKey: device.friendly_name }, zigbeeDevice.log.logLevel === "debug");
1119
+ // Create the mutable device for the main endpoint
1120
+ zigbeeDevice.bridgedDevice = await zigbeeDevice.createMutableDevice(mainEndpoint.deviceTypes, { uniqueStorageKey: device.friendly_name }, zigbeeDevice.log.logLevel === "debug" /* LogLevel.DEBUG */);
1121
+ // Configure BridgedDeviceBasicInformation cluster
897
1122
  mainEndpoint.clusterServersObjs.push(zigbeeDevice.getBridgedDeviceBasicInformation());
1123
+ // Configure PowerSource cluster
898
1124
  mainEndpoint.clusterServersObjs.push(zigbeeDevice.getPowerSource());
1125
+ // Configure ColorControlCluster
899
1126
  if (mainEndpoint.clusterServersIds.includes(ColorControl.Cluster.id)) {
900
1127
  zigbeeDevice.log.debug(`Configuring device ${zigbeeDevice.ien}${device.friendly_name}${rs}${db} ColorControlCluster cluster with HS: ${names.includes('color_hs')} XY: ${names.includes('color_xy')} CT: ${names.includes('color_temp')}`);
901
1128
  zigbeeDevice.bridgedDevice.configureColorControlCluster(names.includes('color_hs') || names.includes('color_xy'), false, names.includes('color_temp'));
902
1129
  }
1130
+ // Configure ThermostatCluster: Auto or Heating only or Cooling only. Set also min and max if available
903
1131
  if (mainEndpoint.clusterServersIds.includes(Thermostat.Cluster.id)) {
904
1132
  const heat = zigbeeDevice.propertyMap.get('occupied_heating_setpoint') || zigbeeDevice.propertyMap.get('current_heating_setpoint');
905
1133
  const cool = zigbeeDevice.propertyMap.get('occupied_cooling_setpoint') || zigbeeDevice.propertyMap.get('current_cooling_setpoint');
@@ -910,18 +1138,20 @@ export class ZigbeeDevice extends ZigbeeEntity {
910
1138
  zigbeeDevice.log.debug(`Configuring device ${zigbeeDevice.ien}${device.friendly_name}${rs}${db} Thermostat cluster with heating ${CYAN}${heat ? 'supported' : 'not supported'}${db} cooling ${CYAN}${cool ? 'supported' : 'not supported'}${db} ` +
911
1139
  `minHeating ${CYAN}${minHeating}${db} maxHeating ${CYAN}${maxHeating}${db} minCooling ${CYAN}${minCooling}${db} maxCooling ${CYAN}${maxCooling}${db}`);
912
1140
  if (heat && !cool) {
913
- zigbeeDevice.propertyMap.delete('running_state');
1141
+ zigbeeDevice.propertyMap.delete('running_state'); // Remove running_state if only heating is supported cause it's not supported by the cluster without AutoMode
914
1142
  mainEndpoint.clusterServersObjs.push(zigbeeDevice.bridgedDevice.getDefaultHeatingThermostatClusterServer(undefined, undefined, minHeating, maxHeating));
915
1143
  }
916
1144
  else if (!heat && cool) {
917
- zigbeeDevice.propertyMap.delete('running_state');
1145
+ zigbeeDevice.propertyMap.delete('running_state'); // Remove running_state if only cooling is supported cause it's not supported by the cluster without AutoMode
918
1146
  mainEndpoint.clusterServersObjs.push(zigbeeDevice.bridgedDevice.getDefaultCoolingThermostatClusterServer(undefined, undefined, minCooling, maxCooling));
919
1147
  }
920
1148
  else if (heat && cool) {
921
1149
  mainEndpoint.clusterServersObjs.push(zigbeeDevice.bridgedDevice.getDefaultThermostatClusterServer(undefined, undefined, undefined, undefined, minHeating, maxHeating, minCooling, maxCooling));
922
1150
  }
923
1151
  }
1152
+ // Filter out duplicate clusters and clusters objects
924
1153
  for (const [endpoint, device] of zigbeeDevice.mutableDevice) {
1154
+ // Filter out duplicate server clusters and server clusters objects. Remove the cluster server id when a cluster server object is present.
925
1155
  const deviceClusterServersMap = new Map();
926
1156
  device.clusterServersIds.forEach((clusterServer) => {
927
1157
  deviceClusterServersMap.set(clusterServer, clusterServer);
@@ -933,6 +1163,7 @@ export class ZigbeeDevice extends ZigbeeEntity {
933
1163
  });
934
1164
  device.clusterServersIds = Array.from(deviceClusterServersMap.values());
935
1165
  device.clusterServersObjs = Array.from(deviceClusterServersObjMap.values());
1166
+ // Filter out duplicate client clusters and client clusters objects. Remove the cluster client id when a cluster client object is present.
936
1167
  const deviceClusterClientsMap = new Map();
937
1168
  device.clusterClientsIds.forEach((clusterClient) => {
938
1169
  deviceClusterClientsMap.set(clusterClient, clusterClient);
@@ -947,27 +1178,36 @@ export class ZigbeeDevice extends ZigbeeEntity {
947
1178
  zigbeeDevice.log.debug(`Device ${zigbeeDevice.ien}${zigbeeDevice.device?.friendly_name}${rs}${db} endpoint: ${ign}${endpoint === '' ? 'main' : endpoint}${rs}${db} => ` +
948
1179
  `${nf}tagList: ${debugStringify(device.tagList)} deviceTypes: ${debugStringify(device.deviceTypes)} clusterServersIds: ${debugStringify(device.clusterServersIds)}`);
949
1180
  }
1181
+ // Add the cluster objects to the main endpoint
950
1182
  mainEndpoint.clusterServersObjs.forEach((clusterServerObj) => {
951
1183
  zigbeeDevice.bridgedDevice?.addClusterServer(clusterServerObj);
952
1184
  });
1185
+ // Add the cluster ids to the main endpoint
953
1186
  zigbeeDevice.bridgedDevice.addClusterServerFromList(zigbeeDevice.bridgedDevice, mainEndpoint.clusterServersIds);
954
1187
  zigbeeDevice.bridgedDevice.addRequiredClusterServers(zigbeeDevice.bridgedDevice);
1188
+ // Add the Fixed Label cluster to the main endpoint
955
1189
  if (zigbeeDevice.composedType !== '')
956
1190
  await zigbeeDevice.bridgedDevice.addFixedLabel('composed', zigbeeDevice.composedType);
1191
+ // Create the child endpoints
957
1192
  for (const [endpoint, device] of zigbeeDevice.mutableDevice) {
958
1193
  if (endpoint === '')
959
1194
  continue;
960
- const child = zigbeeDevice.bridgedDevice?.addChildDeviceTypeWithClusterServer(endpoint, device.deviceTypes, device.clusterServersIds, { tagList: device.tagList }, zigbeeDevice.log.logLevel === "debug");
1195
+ const child = zigbeeDevice.bridgedDevice?.addChildDeviceTypeWithClusterServer(endpoint, device.deviceTypes, device.clusterServersIds, { tagList: device.tagList }, zigbeeDevice.log.logLevel === "debug" /* LogLevel.DEBUG */);
961
1196
  device.clusterServersObjs.forEach((clusterServerObj) => {
962
1197
  child.addClusterServer(clusterServerObj);
963
1198
  });
964
1199
  }
1200
+ // Verify the device
965
1201
  if (!zigbeeDevice.verifyMutableDevice(zigbeeDevice.bridgedDevice))
966
1202
  return zigbeeDevice;
1203
+ // Clear the mutable device from memory
967
1204
  zigbeeDevice.mutableDevice.clear();
1205
+ // Log properties
968
1206
  zigbeeDevice.logPropertyMap();
1207
+ // Add command handlers
969
1208
  zigbeeDevice.bridgedDevice.addCommandHandler('identify', async (data) => {
970
1209
  zigbeeDevice.log.debug(`Command identify called for ${zigbeeDevice.ien}${device.friendly_name}${rs}${db} endpoint: ${data.endpoint.number} request identifyTime:${data.request.identifyTime} `);
1210
+ // logEndpoint(zigbeeDevice.bridgedDevice!);
971
1211
  });
972
1212
  if (zigbeeDevice.bridgedDevice.getClusterServerById(OnOffCluster.id) || zigbeeDevice.hasEndpoints) {
973
1213
  for (const child of zigbeeDevice.bridgedDevice.getChildEndpoints()) {
@@ -1229,3 +1469,4 @@ export class ZigbeeDevice extends ZigbeeEntity {
1229
1469
  return zigbeeDevice;
1230
1470
  }
1231
1471
  }
1472
+ //# sourceMappingURL=entity.js.map