zigbee-herdsman-converters 22.0.0 → 22.1.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.
Files changed (96) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/converters/fromZigbee.d.ts +7 -0
  3. package/converters/fromZigbee.d.ts.map +1 -1
  4. package/converters/fromZigbee.js +10 -1
  5. package/converters/fromZigbee.js.map +1 -1
  6. package/converters/toZigbee.d.ts +9 -0
  7. package/converters/toZigbee.d.ts.map +1 -1
  8. package/converters/toZigbee.js +11 -1
  9. package/converters/toZigbee.js.map +1 -1
  10. package/devices/custom_devices_diy.d.ts.map +1 -1
  11. package/devices/custom_devices_diy.js +9 -0
  12. package/devices/custom_devices_diy.js.map +1 -1
  13. package/devices/ewelink.d.ts.map +1 -1
  14. package/devices/ewelink.js +14 -1
  15. package/devices/ewelink.js.map +1 -1
  16. package/devices/hampton_bay.js +1 -1
  17. package/devices/hampton_bay.js.map +1 -1
  18. package/devices/inovelli.d.ts.map +1 -1
  19. package/devices/inovelli.js +9 -4
  20. package/devices/inovelli.js.map +1 -1
  21. package/devices/lellki.js +1 -1
  22. package/devices/lellki.js.map +1 -1
  23. package/devices/mercator.js +1 -1
  24. package/devices/mercator.js.map +1 -1
  25. package/devices/multiterm.js +1 -1
  26. package/devices/multiterm.js.map +1 -1
  27. package/devices/orvibo.js +1 -1
  28. package/devices/orvibo.js.map +1 -1
  29. package/devices/owon.js +2 -2
  30. package/devices/owon.js.map +1 -1
  31. package/devices/schneider_electric.js +1 -1
  32. package/devices/schneider_electric.js.map +1 -1
  33. package/devices/somfy.d.ts.map +1 -1
  34. package/devices/somfy.js +7 -0
  35. package/devices/somfy.js.map +1 -1
  36. package/devices/sonoff.d.ts.map +1 -1
  37. package/devices/sonoff.js +30 -4
  38. package/devices/sonoff.js.map +1 -1
  39. package/devices/sunricher.d.ts.map +1 -1
  40. package/devices/sunricher.js +917 -352
  41. package/devices/sunricher.js.map +1 -1
  42. package/devices/third_reality.d.ts.map +1 -1
  43. package/devices/third_reality.js +14 -9
  44. package/devices/third_reality.js.map +1 -1
  45. package/devices/tuya.d.ts.map +1 -1
  46. package/devices/tuya.js +8 -7
  47. package/devices/tuya.js.map +1 -1
  48. package/devices/ubisys.d.ts.map +1 -1
  49. package/devices/ubisys.js +1 -0
  50. package/devices/ubisys.js.map +1 -1
  51. package/index.d.ts +1 -1
  52. package/index.d.ts.map +1 -1
  53. package/index.js.map +1 -1
  54. package/lib/color.d.ts +1 -2
  55. package/lib/color.d.ts.map +1 -1
  56. package/lib/color.js +2 -6
  57. package/lib/color.js.map +1 -1
  58. package/lib/configureKey.js +0 -1
  59. package/lib/configureKey.js.map +1 -1
  60. package/lib/constants.js +0 -33
  61. package/lib/constants.js.map +1 -1
  62. package/lib/exposes.d.ts +14 -0
  63. package/lib/exposes.d.ts.map +1 -1
  64. package/lib/exposes.js +38 -13
  65. package/lib/exposes.js.map +1 -1
  66. package/lib/ikea.js +1 -1
  67. package/lib/ikea.js.map +1 -1
  68. package/lib/kelvinToXy.js +0 -1
  69. package/lib/kelvinToXy.js.map +1 -1
  70. package/lib/legacy.d.ts +2 -76
  71. package/lib/legacy.d.ts.map +1 -1
  72. package/lib/legacy.js +1 -64
  73. package/lib/legacy.js.map +1 -1
  74. package/lib/light.d.ts +2 -0
  75. package/lib/light.d.ts.map +1 -1
  76. package/lib/light.js +2 -6
  77. package/lib/light.js.map +1 -1
  78. package/lib/lumi.d.ts.map +1 -1
  79. package/lib/lumi.js +2 -17
  80. package/lib/lumi.js.map +1 -1
  81. package/lib/philips.d.ts +166 -1
  82. package/lib/philips.d.ts.map +1 -1
  83. package/lib/philips.js +12 -15
  84. package/lib/philips.js.map +1 -1
  85. package/lib/reporting.js +0 -51
  86. package/lib/reporting.js.map +1 -1
  87. package/lib/store.js +0 -5
  88. package/lib/store.js.map +1 -1
  89. package/lib/tuya.js +0 -19
  90. package/lib/tuya.js.map +1 -1
  91. package/lib/utils.js +0 -36
  92. package/lib/utils.js.map +1 -1
  93. package/lib/zosung.js +0 -3
  94. package/lib/zosung.js.map +1 -1
  95. package/models-index.json +1 -1
  96. package/package.json +1 -1
@@ -41,383 +41,694 @@ const zigbee_herdsman_1 = require("zigbee-herdsman");
41
41
  const fromZigbee_1 = __importDefault(require("../converters/fromZigbee"));
42
42
  const toZigbee_1 = __importDefault(require("../converters/toZigbee"));
43
43
  const constants = __importStar(require("../lib/constants"));
44
+ const constants_1 = require("../lib/constants");
44
45
  const exposes = __importStar(require("../lib/exposes"));
45
46
  const logger_1 = require("../lib/logger");
46
47
  const m = __importStar(require("../lib/modernExtend"));
47
48
  const reporting = __importStar(require("../lib/reporting"));
49
+ const reporting_1 = require("../lib/reporting");
48
50
  const globalStore = __importStar(require("../lib/store"));
49
51
  const sunricher = __importStar(require("../lib/sunricher"));
50
52
  const utils = __importStar(require("../lib/utils"));
53
+ const utils_1 = require("../lib/utils");
51
54
  const NS = 'zhc:sunricher';
52
55
  const e = exposes.presets;
53
56
  const ea = exposes.access;
54
57
  const sunricherManufacturerCode = 0x1224;
55
- function sunricherExternalSwitchType() {
56
- const attribute = 0x8803;
57
- const data_type = 0x20;
58
- const value_map = {
59
- 0: 'push_button',
60
- 1: 'normal_on_off',
61
- 2: 'three_way',
62
- };
63
- const value_lookup = {
64
- push_button: 0,
65
- normal_on_off: 1,
66
- three_way: 2,
67
- };
68
- const fromZigbee = [
69
- {
70
- cluster: 'genBasic',
71
- type: ['attributeReport', 'readResponse'],
72
- convert: (model, msg, publish, options, meta) => {
73
- if (Object.prototype.hasOwnProperty.call(msg.data, attribute)) {
74
- const value = msg.data[attribute];
75
- return {
76
- external_switch_type: value_map[value] || 'unknown',
77
- external_switch_type_numeric: value,
78
- };
58
+ const sunricherExtend = {
59
+ externalSwitchType: () => {
60
+ const attribute = 0x8803;
61
+ const data_type = 0x20;
62
+ const value_map = {
63
+ 0: 'push_button',
64
+ 1: 'normal_on_off',
65
+ 2: 'three_way',
66
+ };
67
+ const value_lookup = {
68
+ push_button: 0,
69
+ normal_on_off: 1,
70
+ three_way: 2,
71
+ };
72
+ const fromZigbee = [
73
+ {
74
+ cluster: 'genBasic',
75
+ type: ['attributeReport', 'readResponse'],
76
+ convert: (model, msg, publish, options, meta) => {
77
+ if (Object.prototype.hasOwnProperty.call(msg.data, attribute)) {
78
+ const value = msg.data[attribute];
79
+ return {
80
+ external_switch_type: value_map[value] || 'unknown',
81
+ external_switch_type_numeric: value,
82
+ };
83
+ }
84
+ return undefined;
85
+ },
86
+ },
87
+ ];
88
+ const toZigbee = [
89
+ {
90
+ key: ['external_switch_type'],
91
+ convertSet: async (entity, key, value, meta) => {
92
+ const numericValue = value_lookup[value] ?? parseInt(value, 10);
93
+ await entity.write('genBasic', { [attribute]: { value: numericValue, type: data_type } }, { manufacturerCode: sunricherManufacturerCode });
94
+ return { state: { external_switch_type: value } };
95
+ },
96
+ convertGet: async (entity, key, meta) => {
97
+ await entity.read('genBasic', [attribute], { manufacturerCode: sunricherManufacturerCode });
98
+ },
99
+ },
100
+ ];
101
+ const exposes = [
102
+ e.enum('external_switch_type', ea.ALL, ['push_button', 'normal_on_off', 'three_way']).withLabel('External switch type'),
103
+ ];
104
+ const configure = [
105
+ async (device, coordinatorEndpoint, definition) => {
106
+ const endpoint = device.getEndpoint(1);
107
+ try {
108
+ await endpoint.read('genBasic', [attribute], { manufacturerCode: sunricherManufacturerCode });
109
+ }
110
+ catch (error) {
111
+ console.warn(`Failed to read external switch type attribute: ${error}`);
79
112
  }
80
- return undefined;
81
113
  },
82
- },
83
- ];
84
- const toZigbee = [
85
- {
86
- key: ['external_switch_type'],
87
- convertSet: async (entity, key, value, meta) => {
88
- const numericValue = value_lookup[value] ?? parseInt(value, 10);
89
- await entity.write('genBasic', { [attribute]: { value: numericValue, type: data_type } }, { manufacturerCode: sunricherManufacturerCode });
90
- return { state: { external_switch_type: value } };
114
+ ];
115
+ return {
116
+ fromZigbee,
117
+ toZigbee,
118
+ exposes,
119
+ configure,
120
+ isModernExtend: true,
121
+ };
122
+ },
123
+ minimumPWM: () => {
124
+ const attribute = 0x7809;
125
+ const data_type = 0x20;
126
+ const fromZigbee = [
127
+ {
128
+ cluster: 'genBasic',
129
+ type: ['attributeReport', 'readResponse'],
130
+ convert: (model, msg, publish, options, meta) => {
131
+ if (Object.prototype.hasOwnProperty.call(msg.data, attribute)) {
132
+ console.log(`from `, msg.data[attribute]);
133
+ const value = Math.round(msg.data[attribute] / 5.1);
134
+ return {
135
+ minimum_pwm: value,
136
+ };
137
+ }
138
+ return undefined;
139
+ },
91
140
  },
92
- convertGet: async (entity, key, meta) => {
93
- await entity.read('genBasic', [attribute], { manufacturerCode: sunricherManufacturerCode });
141
+ ];
142
+ const toZigbee = [
143
+ {
144
+ key: ['minimum_pwm'],
145
+ convertSet: async (entity, key, value, meta) => {
146
+ console.log(`to `, value);
147
+ const numValue = typeof value === 'string' ? parseInt(value) : value;
148
+ const zgValue = Math.round(numValue * 5.1);
149
+ await entity.write('genBasic', { [attribute]: { value: zgValue, type: data_type } }, { manufacturerCode: sunricherManufacturerCode });
150
+ return { state: { minimum_pwm: numValue } };
151
+ },
152
+ convertGet: async (entity, key, meta) => {
153
+ await entity.read('genBasic', [attribute], { manufacturerCode: sunricherManufacturerCode });
154
+ },
94
155
  },
95
- },
96
- ];
97
- const exposes = [
98
- e.enum('external_switch_type', ea.ALL, ['push_button', 'normal_on_off', 'three_way']).withLabel('External switch type'),
99
- ];
100
- const configure = [
101
- async (device, coordinatorEndpoint, definition) => {
102
- const endpoint = device.getEndpoint(1);
103
- try {
104
- await endpoint.read('genBasic', [attribute], { manufacturerCode: sunricherManufacturerCode });
105
- }
106
- catch (error) {
107
- console.warn(`Failed to read external switch type attribute: ${error}`);
108
- }
109
- },
110
- ];
111
- return {
112
- fromZigbee,
113
- toZigbee,
114
- exposes,
115
- configure,
116
- isModernExtend: true,
117
- };
118
- }
119
- function sunricherMinimumPWM() {
120
- const attribute = 0x7809;
121
- const data_type = 0x20;
122
- const fromZigbee = [
123
- {
124
- cluster: 'genBasic',
125
- type: ['attributeReport', 'readResponse'],
126
- convert: (model, msg, publish, options, meta) => {
127
- if (Object.prototype.hasOwnProperty.call(msg.data, attribute)) {
128
- console.log(`from `, msg.data[attribute]);
129
- const value = Math.round(msg.data[attribute] / 5.1);
130
- return {
131
- minimum_pwm: value,
132
- };
156
+ ];
157
+ const exposes = [
158
+ e
159
+ .numeric('minimum_pwm', ea.ALL)
160
+ .withLabel('Minimum PWM')
161
+ .withDescription('Power off the device and wait for 3 seconds before reconnecting to apply the settings.')
162
+ .withValueMin(0)
163
+ .withValueMax(50)
164
+ .withUnit('%')
165
+ .withValueStep(1),
166
+ ];
167
+ const configure = [
168
+ async (device, coordinatorEndpoint, definition) => {
169
+ const endpoint = device.getEndpoint(1);
170
+ try {
171
+ await endpoint.read('genBasic', [attribute], { manufacturerCode: sunricherManufacturerCode });
172
+ }
173
+ catch (error) {
174
+ console.warn(`Failed to read external switch type attribute: ${error}`);
133
175
  }
134
- return undefined;
135
176
  },
136
- },
137
- ];
138
- const toZigbee = [
139
- {
140
- key: ['minimum_pwm'],
141
- convertSet: async (entity, key, value, meta) => {
142
- console.log(`to `, value);
143
- const numValue = typeof value === 'string' ? parseInt(value) : value;
144
- const zgValue = Math.round(numValue * 5.1);
145
- await entity.write('genBasic', { [attribute]: { value: zgValue, type: data_type } }, { manufacturerCode: sunricherManufacturerCode });
146
- return { state: { minimum_pwm: numValue } };
177
+ ];
178
+ return {
179
+ fromZigbee,
180
+ toZigbee,
181
+ exposes,
182
+ configure,
183
+ isModernExtend: true,
184
+ };
185
+ },
186
+ SRZG9002KR12Pro: () => {
187
+ const cluster = 0xff03;
188
+ const fromZigbee = [
189
+ {
190
+ cluster: 0xff03,
191
+ type: ['raw'],
192
+ convert: (model, msg, publish, options, meta) => {
193
+ const bytes = [...msg.data];
194
+ const messageType = bytes[3];
195
+ let action = 'unknown';
196
+ if (messageType === 0x01) {
197
+ const pressTypeMask = bytes[6];
198
+ const pressTypeLookup = {
199
+ 0x01: 'short_press',
200
+ 0x02: 'double_press',
201
+ 0x03: 'hold',
202
+ 0x04: 'hold_released',
203
+ };
204
+ action = pressTypeLookup[pressTypeMask] || 'unknown';
205
+ const buttonMask = (bytes[4] << 8) | bytes[5];
206
+ const specialButtonMap = {
207
+ 9: 'knob',
208
+ 11: 'k9',
209
+ 12: 'k10',
210
+ 15: 'k11',
211
+ 16: 'k12',
212
+ };
213
+ const actionButtons = [];
214
+ for (let i = 0; i < 16; i++) {
215
+ if ((buttonMask >> i) & 1) {
216
+ const button = i + 1;
217
+ actionButtons.push(specialButtonMap[button] ?? `k${button}`);
218
+ }
219
+ }
220
+ return { action, action_buttons: actionButtons };
221
+ }
222
+ else if (messageType === 0x03) {
223
+ const directionMask = bytes[4];
224
+ const actionSpeed = bytes[6];
225
+ const directionMap = {
226
+ 0x01: 'clockwise',
227
+ 0x02: 'anti_clockwise',
228
+ };
229
+ const direction = directionMap[directionMask] || 'unknown';
230
+ action = `${direction}_rotation`;
231
+ return { action, action_speed: actionSpeed };
232
+ }
233
+ return { action };
234
+ },
147
235
  },
148
- convertGet: async (entity, key, meta) => {
149
- await entity.read('genBasic', [attribute], { manufacturerCode: sunricherManufacturerCode });
236
+ ];
237
+ const exposes = [
238
+ e.action(['short_press', 'double_press', 'hold', 'hold_released', 'clockwise_rotation', 'anti_clockwise_rotation']),
239
+ ];
240
+ const configure = [
241
+ async (device, coordinatorEndpoint, definition) => {
242
+ const endpoint = device.getEndpoint(1);
243
+ await endpoint.bind(cluster, coordinatorEndpoint);
150
244
  },
151
- },
152
- ];
153
- const exposes = [
154
- e
155
- .numeric('minimum_pwm', ea.ALL)
156
- .withLabel('Minimum PWM')
157
- .withDescription('Power off the device and wait for 3 seconds before reconnecting to apply the settings.')
158
- .withValueMin(0)
159
- .withValueMax(50)
160
- .withUnit('%')
161
- .withValueStep(1),
162
- ];
163
- const configure = [
164
- async (device, coordinatorEndpoint, definition) => {
165
- const endpoint = device.getEndpoint(1);
166
- try {
167
- await endpoint.read('genBasic', [attribute], { manufacturerCode: sunricherManufacturerCode });
168
- }
169
- catch (error) {
170
- console.warn(`Failed to read external switch type attribute: ${error}`);
171
- }
172
- },
173
- ];
174
- return {
175
- fromZigbee,
176
- toZigbee,
177
- exposes,
178
- configure,
179
- isModernExtend: true,
180
- };
181
- }
182
- function sunricherSRZG9002KR12Pro() {
183
- const cluster = 0xff03;
184
- const fromZigbee = [
185
- {
186
- cluster: 0xff03,
187
- type: ['raw'],
188
- convert: (model, msg, publish, options, meta) => {
189
- const bytes = [...msg.data];
190
- const messageType = bytes[3];
191
- let action = 'unknown';
192
- if (messageType === 0x01) {
193
- const pressTypeMask = bytes[6];
194
- const pressTypeLookup = {
195
- 0x01: 'short_press',
196
- 0x02: 'double_press',
197
- 0x03: 'hold',
198
- 0x04: 'hold_released',
199
- };
200
- action = pressTypeLookup[pressTypeMask] || 'unknown';
201
- const buttonMask = (bytes[4] << 8) | bytes[5];
202
- const specialButtonMap = {
203
- 9: 'knob',
204
- 11: 'k9',
205
- 12: 'k10',
206
- 15: 'k11',
207
- 16: 'k12',
208
- };
209
- const actionButtons = [];
210
- for (let i = 0; i < 16; i++) {
211
- if ((buttonMask >> i) & 1) {
212
- const button = i + 1;
213
- actionButtons.push(specialButtonMap[button] ?? `k${button}`);
245
+ ];
246
+ return {
247
+ fromZigbee,
248
+ exposes,
249
+ configure,
250
+ isModernExtend: true,
251
+ };
252
+ },
253
+ SRZG2836D5Pro: () => {
254
+ const cluster = 0xff03;
255
+ const fromZigbee = [
256
+ {
257
+ cluster: 0xff03,
258
+ type: ['raw'],
259
+ convert: (model, msg, publish, options, meta) => {
260
+ const bytes = [...msg.data];
261
+ const messageType = bytes[3];
262
+ let action = 'unknown';
263
+ if (messageType === 0x01) {
264
+ const pressTypeMask = bytes[6];
265
+ const pressTypeLookup = {
266
+ 0x01: 'short_press',
267
+ 0x02: 'double_press',
268
+ 0x03: 'hold',
269
+ 0x04: 'hold_released',
270
+ };
271
+ action = pressTypeLookup[pressTypeMask] || 'unknown';
272
+ const buttonMask = bytes[5];
273
+ const specialButtonLookup = {
274
+ 0x01: 'top_left',
275
+ 0x02: 'top_right',
276
+ 0x03: 'bottom_left',
277
+ 0x04: 'bottom_right',
278
+ 0x05: 'center',
279
+ };
280
+ const actionButtons = [];
281
+ for (let i = 0; i < 5; i++) {
282
+ if ((buttonMask >> i) & 1) {
283
+ const button = i + 1;
284
+ actionButtons.push(specialButtonLookup[button] || `unknown_${button}`);
285
+ }
214
286
  }
287
+ return { action, action_buttons: actionButtons };
215
288
  }
216
- return { action, action_buttons: actionButtons };
217
- }
218
- else if (messageType === 0x03) {
219
- const directionMask = bytes[4];
220
- const actionSpeed = bytes[6];
221
- const directionMap = {
222
- 0x01: 'clockwise',
223
- 0x02: 'anti_clockwise',
224
- };
225
- const direction = directionMap[directionMask] || 'unknown';
226
- action = `${direction}_rotation`;
227
- return { action, action_speed: actionSpeed };
228
- }
229
- return { action };
289
+ else if (messageType === 0x03) {
290
+ const directionMask = bytes[4];
291
+ const actionSpeed = bytes[6];
292
+ const isStop = bytes[5] === 0x02;
293
+ const directionMap = {
294
+ 0x01: 'clockwise',
295
+ 0x02: 'anti_clockwise',
296
+ };
297
+ const direction = isStop ? 'stop' : directionMap[directionMask] || 'unknown';
298
+ action = `${direction}_rotation`;
299
+ return { action, action_speed: actionSpeed };
300
+ }
301
+ return { action };
302
+ },
230
303
  },
231
- },
232
- ];
233
- const exposes = [e.action(['short_press', 'double_press', 'hold', 'hold_released', 'clockwise_rotation', 'anti_clockwise_rotation'])];
234
- const configure = [
235
- async (device, coordinatorEndpoint, definition) => {
236
- const endpoint = device.getEndpoint(1);
237
- await endpoint.bind(cluster, coordinatorEndpoint);
238
- },
239
- ];
240
- return {
241
- fromZigbee,
242
- exposes,
243
- configure,
244
- isModernExtend: true,
245
- };
246
- }
247
- function sunricherSRZG2836D5Pro() {
248
- const cluster = 0xff03;
249
- const fromZigbee = [
250
- {
251
- cluster: 0xff03,
252
- type: ['raw'],
253
- convert: (model, msg, publish, options, meta) => {
254
- const bytes = [...msg.data];
255
- const messageType = bytes[3];
256
- let action = 'unknown';
257
- if (messageType === 0x01) {
258
- const pressTypeMask = bytes[6];
259
- const pressTypeLookup = {
260
- 0x01: 'short_press',
261
- 0x02: 'double_press',
262
- 0x03: 'hold',
263
- 0x04: 'hold_released',
264
- };
265
- action = pressTypeLookup[pressTypeMask] || 'unknown';
266
- const buttonMask = bytes[5];
267
- const specialButtonLookup = {
268
- 0x01: 'top_left',
269
- 0x02: 'top_right',
270
- 0x03: 'bottom_left',
271
- 0x04: 'bottom_right',
272
- 0x05: 'center',
273
- };
274
- const actionButtons = [];
275
- for (let i = 0; i < 5; i++) {
276
- if ((buttonMask >> i) & 1) {
277
- const button = i + 1;
278
- actionButtons.push(specialButtonLookup[button] || `unknown_${button}`);
304
+ ];
305
+ const exposes = [
306
+ e.action(['short_press', 'double_press', 'hold', 'hold_released', 'clockwise_rotation', 'anti_clockwise_rotation', 'stop_rotation']),
307
+ ];
308
+ const configure = [
309
+ async (device, coordinatorEndpoint, definition) => {
310
+ const endpoint = device.getEndpoint(1);
311
+ await endpoint.bind(cluster, coordinatorEndpoint);
312
+ },
313
+ ];
314
+ return {
315
+ fromZigbee,
316
+ exposes,
317
+ configure,
318
+ isModernExtend: true,
319
+ };
320
+ },
321
+ SRZG9002K16Pro: () => {
322
+ const cluster = 0xff03;
323
+ const fromZigbee = [
324
+ {
325
+ cluster,
326
+ type: ['raw'],
327
+ convert: (model, msg, publish, options, meta) => {
328
+ const bytes = [...msg.data];
329
+ const messageType = bytes[3];
330
+ let action = 'unknown';
331
+ if (messageType === 0x01) {
332
+ const pressTypeMask = bytes[6];
333
+ const pressTypeLookup = {
334
+ 0x01: 'short_press',
335
+ 0x02: 'double_press',
336
+ 0x03: 'hold',
337
+ 0x04: 'hold_released',
338
+ };
339
+ action = pressTypeLookup[pressTypeMask] || 'unknown';
340
+ const buttonMask = (bytes[4] << 8) | bytes[5];
341
+ const getButtonNumber = (input) => {
342
+ const row = Math.floor((input - 1) / 4);
343
+ const col = (input - 1) % 4;
344
+ return col * 4 + row + 1;
345
+ };
346
+ const actionButtons = [];
347
+ for (let i = 0; i < 16; i++) {
348
+ if ((buttonMask >> i) & 1) {
349
+ const button = i + 1;
350
+ actionButtons.push(`k${getButtonNumber(button)}`);
351
+ }
279
352
  }
353
+ return { action, action_buttons: actionButtons };
280
354
  }
281
- return { action, action_buttons: actionButtons };
282
- }
283
- else if (messageType === 0x03) {
284
- const directionMask = bytes[4];
285
- const actionSpeed = bytes[6];
286
- const isStop = bytes[5] === 0x02;
287
- const directionMap = {
288
- 0x01: 'clockwise',
289
- 0x02: 'anti_clockwise',
290
- };
291
- const direction = isStop ? 'stop' : directionMap[directionMask] || 'unknown';
292
- action = `${direction}_rotation`;
293
- return { action, action_speed: actionSpeed };
294
- }
295
- return { action };
355
+ return { action };
356
+ },
296
357
  },
297
- },
298
- ];
299
- const exposes = [
300
- e.action(['short_press', 'double_press', 'hold', 'hold_released', 'clockwise_rotation', 'anti_clockwise_rotation', 'stop_rotation']),
301
- ];
302
- const configure = [
303
- async (device, coordinatorEndpoint, definition) => {
304
- const endpoint = device.getEndpoint(1);
305
- await endpoint.bind(cluster, coordinatorEndpoint);
306
- },
307
- ];
308
- return {
309
- fromZigbee,
310
- exposes,
311
- configure,
312
- isModernExtend: true,
313
- };
314
- }
315
- function sunricherSRZG9002K16Pro() {
316
- const cluster = 0xff03;
317
- const fromZigbee = [
318
- {
319
- cluster,
320
- type: ['raw'],
321
- convert: (model, msg, publish, options, meta) => {
322
- const bytes = [...msg.data];
323
- const messageType = bytes[3];
324
- let action = 'unknown';
325
- if (messageType === 0x01) {
326
- const pressTypeMask = bytes[6];
327
- const pressTypeLookup = {
328
- 0x01: 'short_press',
329
- 0x02: 'double_press',
330
- 0x03: 'hold',
331
- 0x04: 'hold_released',
358
+ ];
359
+ const exposes = [e.action(['short_press', 'double_press', 'hold', 'hold_released'])];
360
+ const configure = [
361
+ async (device, coordinatorEndpoint, definition) => {
362
+ const endpoint = device.getEndpoint(1);
363
+ await endpoint.bind(cluster, coordinatorEndpoint);
364
+ },
365
+ ];
366
+ return {
367
+ fromZigbee,
368
+ exposes,
369
+ configure,
370
+ isModernExtend: true,
371
+ };
372
+ },
373
+ indicatorLight() {
374
+ const cluster = 0xfc8b;
375
+ const attribute = 0xf001;
376
+ const data_type = 0x20;
377
+ const manufacturerCode = 0x120b;
378
+ const exposes = [
379
+ e.enum('indicator_light', ea.ALL, ['on', 'off']).withDescription('Enable/disable the LED indicator').withCategory('config'),
380
+ ];
381
+ const fromZigbee = [
382
+ {
383
+ cluster,
384
+ type: ['attributeReport', 'readResponse'],
385
+ convert: (model, msg, publish, options, meta) => {
386
+ if (!Object.prototype.hasOwnProperty.call(msg.data, attribute))
387
+ return;
388
+ const indicatorLight = msg.data[attribute];
389
+ const firstBit = indicatorLight & 0x01;
390
+ return { indicator_light: firstBit === 1 ? 'on' : 'off' };
391
+ },
392
+ },
393
+ ];
394
+ const toZigbee = [
395
+ {
396
+ key: ['indicator_light'],
397
+ convertSet: async (entity, key, value, meta) => {
398
+ const attributeRead = await entity.read(cluster, [attribute]);
399
+ if (attributeRead === undefined)
400
+ return;
401
+ // @ts-expect-error ignore
402
+ const currentValue = attributeRead[attribute];
403
+ const newValue = value === 'on' ? currentValue | 0x01 : currentValue & ~0x01;
404
+ await entity.write(cluster, { [attribute]: { value: newValue, type: data_type } }, { manufacturerCode });
405
+ return { state: { indicator_light: value } };
406
+ },
407
+ convertGet: async (entity, key, meta) => {
408
+ await entity.read(cluster, [attribute], { manufacturerCode });
409
+ },
410
+ },
411
+ ];
412
+ const configure = [
413
+ async (device, coordinatorEndpoint, definition) => {
414
+ const endpoint = device.getEndpoint(1);
415
+ await endpoint.bind(cluster, coordinatorEndpoint);
416
+ await endpoint.read(cluster, [attribute], { manufacturerCode });
417
+ },
418
+ ];
419
+ return {
420
+ exposes,
421
+ configure,
422
+ fromZigbee,
423
+ toZigbee,
424
+ isModernExtend: true,
425
+ };
426
+ },
427
+ thermostatWeeklySchedule: () => {
428
+ const exposes = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'].map((day) => e
429
+ .text(`schedule_${day}`, ea.ALL)
430
+ .withDescription(`Schedule for ${day.charAt(0).toUpperCase() + day.slice(1)}, example: "06:00/21.0 12:00/21.0 18:00/21.0 22:00/16.0"`)
431
+ .withCategory('config'));
432
+ const fromZigbee = [
433
+ {
434
+ cluster: 'hvacThermostat',
435
+ type: ['commandGetWeeklyScheduleRsp'],
436
+ convert: (model, msg, publish, options, meta) => {
437
+ const day = Object.entries(constants.thermostatDayOfWeek).find((d) => msg.data.dayofweek & (1 << +d[0]))[1];
438
+ const transitions = msg.data.transitions
439
+ .map((t) => {
440
+ const hours = Math.floor(t.transitionTime / 60);
441
+ const minutes = t.transitionTime % 60;
442
+ return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}/${t.heatSetpoint / 100}`;
443
+ })
444
+ .sort()
445
+ .join(' ');
446
+ return {
447
+ ...meta.state.weekly_schedule,
448
+ [`schedule_${day}`]: transitions,
332
449
  };
333
- action = pressTypeLookup[pressTypeMask] || 'unknown';
334
- const buttonMask = (bytes[4] << 8) | bytes[5];
335
- const getButtonNumber = (input) => {
336
- const row = Math.floor((input - 1) / 4);
337
- const col = (input - 1) % 4;
338
- return col * 4 + row + 1;
450
+ },
451
+ },
452
+ ];
453
+ const toZigbee = [
454
+ {
455
+ key: [
456
+ 'schedule_sunday',
457
+ 'schedule_monday',
458
+ 'schedule_tuesday',
459
+ 'schedule_wednesday',
460
+ 'schedule_thursday',
461
+ 'schedule_friday',
462
+ 'schedule_saturday',
463
+ ],
464
+ convertSet: async (entity, key, value, meta) => {
465
+ const transitionRegex = /^(0[0-9]|1[0-9]|2[0-3]):([0-5][0-9])\/(\d+(\.\d+)?)$/;
466
+ const dayOfWeekName = key.replace('schedule_', '');
467
+ utils.assertString(value, dayOfWeekName);
468
+ const dayKey = utils.getKey(constants.thermostatDayOfWeek, dayOfWeekName.toLowerCase(), null);
469
+ if (!dayKey)
470
+ throw new Error(`Invalid schedule: invalid day name, found: ${dayOfWeekName}`);
471
+ const transitions = value.split(' ').sort();
472
+ if (transitions.length !== 4) {
473
+ throw new Error('Invalid schedule: days must have exactly 4 transitions');
474
+ }
475
+ const payload = {
476
+ dayofweek: 1 << Number(dayKey),
477
+ numoftrans: transitions.length,
478
+ mode: 1 << 0,
479
+ transitions: transitions.map((transition) => {
480
+ const matches = transition.match(transitionRegex);
481
+ if (!matches) {
482
+ throw new Error(`Invalid schedule: transitions must be in format HH:mm/temperature (e.g. 12:00/15.5), found: ${transition}`);
483
+ }
484
+ const [, hours, minutes, temp] = matches;
485
+ const temperature = parseFloat(temp);
486
+ if (temperature < 4 || temperature > 35) {
487
+ throw new Error(`Invalid schedule: temperature value must be between 4-35 (inclusive), found: ${temperature}`);
488
+ }
489
+ return {
490
+ transitionTime: parseInt(hours) * 60 + parseInt(minutes),
491
+ heatSetpoint: Math.round(temperature * 100),
492
+ };
493
+ }),
339
494
  };
340
- const actionButtons = [];
341
- for (let i = 0; i < 16; i++) {
342
- if ((buttonMask >> i) & 1) {
343
- const button = i + 1;
344
- actionButtons.push(`k${getButtonNumber(button)}`);
495
+ await entity.command('hvacThermostat', 'setWeeklySchedule', payload, utils.getOptions(meta.mapped, entity));
496
+ },
497
+ convertGet: async (entity, key, meta) => {
498
+ const dayOfWeekName = key.replace('schedule_', '');
499
+ const dayKey = utils.getKey(constants.thermostatDayOfWeek, dayOfWeekName.toLowerCase(), null);
500
+ await entity.command('hvacThermostat', 'getWeeklySchedule', {
501
+ daystoreturn: dayKey !== null ? 1 << Number(dayKey) : 0xff,
502
+ modetoreturn: 1,
503
+ }, utils.getOptions(meta.mapped, entity));
504
+ },
505
+ },
506
+ ];
507
+ const configure = [
508
+ async (device, coordinatorEndpoint, definition) => {
509
+ const endpoint = device.getEndpoint(1);
510
+ await endpoint.command('hvacThermostat', 'getWeeklySchedule', {
511
+ daystoreturn: 0xff,
512
+ modetoreturn: 1,
513
+ });
514
+ },
515
+ ];
516
+ return { exposes, fromZigbee, toZigbee, configure, isModernExtend: true };
517
+ },
518
+ thermostatChildLock: () => {
519
+ const exposes = [e.binary('child_lock', ea.ALL, 'LOCK', 'UNLOCK').withDescription('Enables/disables physical input on the device')];
520
+ const fromZigbee = [
521
+ {
522
+ cluster: 'hvacUserInterfaceCfg',
523
+ type: ['attributeReport', 'readResponse'],
524
+ convert: (model, msg, publish, options, meta) => {
525
+ if (Object.hasOwn(msg.data, 'keypadLockout')) {
526
+ return { child_lock: msg.data.keypadLockout === 0 ? 'UNLOCK' : 'LOCK' };
527
+ }
528
+ return {};
529
+ },
530
+ },
531
+ ];
532
+ const toZigbee = [
533
+ {
534
+ key: ['child_lock'],
535
+ convertSet: async (entity, key, value, meta) => {
536
+ const keypadLockout = Number(value === 'LOCK');
537
+ await entity.write('hvacUserInterfaceCfg', { keypadLockout });
538
+ return { state: { child_lock: value } };
539
+ },
540
+ convertGet: async (entity, key, meta) => {
541
+ await entity.read('hvacUserInterfaceCfg', ['keypadLockout']);
542
+ },
543
+ },
544
+ ];
545
+ const configure = [
546
+ async (device, coordinatorEndpoint, definition) => {
547
+ const endpoint = device.getEndpoint(1);
548
+ await reporting.bind(endpoint, coordinatorEndpoint, ['hvacUserInterfaceCfg']);
549
+ await endpoint.read('hvacUserInterfaceCfg', ['keypadLockout']);
550
+ await reporting.thermostatKeypadLockMode(endpoint);
551
+ },
552
+ ];
553
+ return { exposes, fromZigbee, toZigbee, configure, isModernExtend: true };
554
+ },
555
+ thermostatPreset: () => {
556
+ const systemModeLookup = {
557
+ 0: 'off',
558
+ 1: 'auto',
559
+ 3: 'cool',
560
+ 4: 'manual',
561
+ 5: 'emergency_heating',
562
+ 6: 'precooling',
563
+ 7: 'fan_only',
564
+ 8: 'dry',
565
+ 9: 'sleep',
566
+ };
567
+ const awayOrBoostModeLookup = { 0: 'normal', 1: 'away', 2: 'forced' };
568
+ const fromZigbee = [
569
+ {
570
+ cluster: 'hvacThermostat',
571
+ type: ['attributeReport', 'readResponse'],
572
+ convert: (model, msg, publish, options, meta) => {
573
+ if (!Object.hasOwn(msg.data, 'systemMode') && !Object.hasOwn(msg.data, 'awayOrBoostMode'))
574
+ return;
575
+ const systemMode = msg.data.systemMode ?? globalStore.getValue(msg.device, 'systemMode');
576
+ const awayOrBoostMode = msg.data.awayOrBoostMode ?? globalStore.getValue(msg.device, 'awayOrBoostMode');
577
+ globalStore.putValue(msg.device, 'systemMode', systemMode);
578
+ globalStore.putValue(msg.device, 'awayOrBoostMode', awayOrBoostMode);
579
+ const result = {};
580
+ if (awayOrBoostMode !== undefined && awayOrBoostMode !== 0) {
581
+ result.preset = utils.getFromLookup(awayOrBoostMode, awayOrBoostModeLookup);
582
+ result.away_or_boost_mode = utils.getFromLookup(awayOrBoostMode, awayOrBoostModeLookup);
583
+ if (systemMode !== undefined) {
584
+ result.system_mode = constants.thermostatSystemModes[systemMode];
345
585
  }
346
586
  }
347
- return { action, action_buttons: actionButtons };
348
- }
349
- return { action };
587
+ else if (systemMode !== undefined) {
588
+ result.preset = utils.getFromLookup(systemMode, systemModeLookup);
589
+ result.system_mode = constants.thermostatSystemModes[systemMode];
590
+ if (awayOrBoostMode !== undefined) {
591
+ result.away_or_boost_mode = utils.getFromLookup(awayOrBoostMode, awayOrBoostModeLookup);
592
+ }
593
+ }
594
+ return result;
595
+ },
350
596
  },
351
- },
352
- ];
353
- const exposes = [e.action(['short_press', 'double_press', 'hold', 'hold_released'])];
354
- const configure = [
355
- async (device, coordinatorEndpoint, definition) => {
356
- const endpoint = device.getEndpoint(1);
357
- await endpoint.bind(cluster, coordinatorEndpoint);
358
- },
359
- ];
360
- return {
361
- fromZigbee,
362
- exposes,
363
- configure,
364
- isModernExtend: true,
365
- };
366
- }
367
- function sunricherIndicatorLight() {
368
- const cluster = 0xfc8b;
369
- const attribute = 0xf001;
370
- const data_type = 0x20;
371
- const manufacturerCode = 0x120b;
372
- const exposes = [
373
- e.enum('indicator_light', ea.ALL, ['on', 'off']).withDescription('Enable/disable the LED indicator').withCategory('config'),
374
- ];
375
- const fromZigbee = [
376
- {
377
- cluster,
378
- type: ['attributeReport', 'readResponse'],
379
- convert: (model, msg, publish, options, meta) => {
380
- if (!Object.prototype.hasOwnProperty.call(msg.data, attribute))
381
- return;
382
- const indicatorLight = msg.data[attribute];
383
- const firstBit = indicatorLight & 0x01;
384
- return { indicator_light: firstBit === 1 ? 'on' : 'off' };
597
+ ];
598
+ const toZigbee = [
599
+ {
600
+ key: ['preset'],
601
+ convertSet: async (entity, key, value, meta) => {
602
+ if (value === 'away' || value === 'forced') {
603
+ const awayOrBoostMode = value === 'away' ? 1 : 2;
604
+ globalStore.putValue(entity, 'awayOrBoostMode', awayOrBoostMode);
605
+ if (value === 'away') {
606
+ await entity.read('hvacThermostat', ['unoccupiedHeatingSetpoint']);
607
+ }
608
+ await entity.write('hvacThermostat', { awayOrBoostMode });
609
+ return { state: { preset: value, away_or_boost_mode: value } };
610
+ }
611
+ else {
612
+ globalStore.putValue(entity, 'awayOrBoostMode', 0);
613
+ const systemMode = utils.getKey(systemModeLookup, value, undefined, Number);
614
+ await entity.write('hvacThermostat', { systemMode });
615
+ if (typeof systemMode === 'number') {
616
+ return {
617
+ state: {
618
+ // @ts-expect-error ignore
619
+ preset: systemModeLookup[systemMode],
620
+ system_mode: constants.thermostatSystemModes[systemMode],
621
+ },
622
+ };
623
+ }
624
+ }
625
+ },
385
626
  },
386
- },
387
- ];
388
- const toZigbee = [
389
- {
390
- key: ['indicator_light'],
391
- convertSet: async (entity, key, value, meta) => {
392
- const attributeRead = await entity.read(cluster, [attribute]);
393
- if (attributeRead === undefined)
394
- return;
627
+ {
628
+ key: ['system_mode'],
629
+ convertSet: async (entity, key, value, meta) => {
630
+ const systemMode = utils.getKey(constants.thermostatSystemModes, value, undefined, Number);
631
+ if (systemMode === undefined || typeof systemMode !== 'number') {
632
+ throw new Error(`Invalid system mode: ${value}`);
633
+ }
634
+ await entity.write('hvacThermostat', { systemMode });
635
+ return {
636
+ state: {
637
+ // @ts-expect-error ignore
638
+ preset: systemModeLookup[systemMode],
639
+ system_mode: constants.thermostatSystemModes[systemMode],
640
+ },
641
+ };
642
+ },
643
+ convertGet: async (entity, key, meta) => {
644
+ await entity.read('hvacThermostat', ['systemMode']);
645
+ },
646
+ },
647
+ ];
648
+ const configure = [
649
+ async (device, coordinatorEndpoint, definition) => {
650
+ const endpoint = device.getEndpoint(1);
651
+ await endpoint.read('hvacThermostat', ['systemMode']);
652
+ await endpoint.read('hvacThermostat', ['awayOrBoostMode']);
653
+ await reporting.bind(endpoint, coordinatorEndpoint, ['hvacThermostat']);
654
+ await reporting.thermostatSystemMode(endpoint);
655
+ await endpoint.configureReporting('hvacThermostat', (0, reporting_1.payload)('awayOrBoostMode', 10, constants_1.repInterval.HOUR, null));
656
+ },
657
+ ];
658
+ return { fromZigbee, toZigbee, configure, isModernExtend: true };
659
+ },
660
+ thermostatCurrentHeatingSetpoint: () => {
661
+ const getAwayOrBoostMode = async (entity) => {
662
+ let result = globalStore.getValue(entity, 'awayOrBoostMode');
663
+ if (result === undefined) {
664
+ const attributeRead = await entity.read('hvacThermostat', ['awayOrBoostMode']);
395
665
  // @ts-expect-error ignore
396
- const currentValue = attributeRead[attribute];
397
- const newValue = value === 'on' ? currentValue | 0x01 : currentValue & ~0x01;
398
- await entity.write(cluster, { [attribute]: { value: newValue, type: data_type } }, { manufacturerCode });
399
- return { state: { indicator_light: value } };
666
+ result = attributeRead['awayOrBoostMode'];
667
+ globalStore.putValue(entity, 'awayOrBoostMode', result);
668
+ }
669
+ return result;
670
+ };
671
+ const fromZigbee = [
672
+ {
673
+ cluster: 'hvacThermostat',
674
+ type: ['attributeReport', 'readResponse'],
675
+ convert: async (model, msg, publish, options, meta) => {
676
+ const hasHeatingSetpoints = Object.hasOwn(msg.data, 'occupiedHeatingSetpoint') || Object.hasOwn(msg.data, 'unoccupiedHeatingSetpoint');
677
+ if (!hasHeatingSetpoints)
678
+ return;
679
+ const processSetpoint = (value) => {
680
+ if (value === undefined)
681
+ return undefined;
682
+ return (0, utils_1.precisionRound)(value, 2) / 100;
683
+ };
684
+ const occupiedSetpoint = processSetpoint(msg.data.occupiedHeatingSetpoint);
685
+ const unoccupiedSetpoint = processSetpoint(msg.data.unoccupiedHeatingSetpoint);
686
+ const awayOrBoostMode = msg.data.awayOrBoostMode ?? (await getAwayOrBoostMode(msg.device.getEndpoint(1)));
687
+ const result = {};
688
+ if (awayOrBoostMode === 1 && unoccupiedSetpoint !== undefined) {
689
+ result.current_heating_setpoint = unoccupiedSetpoint;
690
+ }
691
+ else if (occupiedSetpoint !== undefined) {
692
+ result.current_heating_setpoint = occupiedSetpoint;
693
+ }
694
+ return result;
695
+ },
696
+ },
697
+ ];
698
+ const toZigbee = [
699
+ {
700
+ key: ['current_heating_setpoint'],
701
+ convertSet: async (entity, key, value, meta) => {
702
+ utils.assertNumber(value, key);
703
+ const awayOrBoostMode = await getAwayOrBoostMode(entity);
704
+ let convertedValue;
705
+ if (meta.options.thermostat_unit === 'fahrenheit') {
706
+ convertedValue = Math.round(utils.normalizeCelsiusVersionOfFahrenheit(value) * 100);
707
+ }
708
+ else {
709
+ convertedValue = Number((Math.round(Number((value * 2).toFixed(1))) / 2).toFixed(1)) * 100;
710
+ }
711
+ const attribute = awayOrBoostMode === 1 ? 'unoccupiedHeatingSetpoint' : 'occupiedHeatingSetpoint';
712
+ await entity.write('hvacThermostat', { [attribute]: convertedValue });
713
+ return { state: { current_heating_setpoint: value } };
714
+ },
715
+ convertGet: async (entity, key, meta) => {
716
+ await entity.read('hvacThermostat', ['occupiedHeatingSetpoint', 'unoccupiedHeatingSetpoint']);
717
+ },
400
718
  },
401
- convertGet: async (entity, key, meta) => {
402
- await entity.read(cluster, [attribute], { manufacturerCode });
719
+ ];
720
+ const configure = [
721
+ async (device, coordinatorEndpoint, definition) => {
722
+ const endpoint = device.getEndpoint(1);
723
+ await endpoint.read('hvacThermostat', ['occupiedHeatingSetpoint', 'unoccupiedHeatingSetpoint']);
724
+ await reporting.bind(endpoint, coordinatorEndpoint, ['hvacThermostat']);
725
+ await reporting.thermostatOccupiedHeatingSetpoint(endpoint);
726
+ await reporting.thermostatUnoccupiedHeatingSetpoint(endpoint);
403
727
  },
404
- },
405
- ];
406
- const configure = [
407
- async (device, coordinatorEndpoint, definition) => {
408
- const endpoint = device.getEndpoint(1);
409
- await endpoint.bind(cluster, coordinatorEndpoint);
410
- await endpoint.read(cluster, [attribute], { manufacturerCode });
411
- },
412
- ];
413
- return {
414
- exposes,
415
- configure,
416
- fromZigbee,
417
- toZigbee,
418
- isModernExtend: true,
419
- };
420
- }
728
+ ];
729
+ return { fromZigbee, toZigbee, configure, isModernExtend: true };
730
+ },
731
+ };
421
732
  const fzLocal = {
422
733
  sunricher_SRZGP2801K45C: {
423
734
  cluster: 'greenPower',
@@ -457,7 +768,261 @@ async function syncTime(endpoint) {
457
768
  /* Do nothing*/
458
769
  }
459
770
  }
771
+ async function syncTimeWithTimeZone(endpoint) {
772
+ try {
773
+ const time = Math.round((new Date().getTime() - constants.OneJanuary2000) / 1000);
774
+ const timeZone = new Date().getTimezoneOffset() * -1 * 60;
775
+ await endpoint.write('genTime', { time, timeZone });
776
+ }
777
+ catch {
778
+ logger_1.logger.error('Failed to sync time with time zone', NS);
779
+ }
780
+ }
460
781
  exports.definitions = [
782
+ {
783
+ zigbeeModel: ['ZG9340'],
784
+ model: 'SR-ZG9093TRV',
785
+ vendor: 'Sunricher',
786
+ description: 'Zigbee thermostatic radiator valve',
787
+ extend: [
788
+ m.deviceAddCustomCluster('hvacThermostat', {
789
+ ID: zigbee_herdsman_1.Zcl.Clusters.hvacThermostat.ID,
790
+ attributes: {
791
+ screenTimeout: {
792
+ ID: 0x100d,
793
+ type: zigbee_herdsman_1.Zcl.DataType.UINT8,
794
+ manufacturerCode: sunricherManufacturerCode,
795
+ },
796
+ antiFreezingTemp: {
797
+ ID: 0x1005,
798
+ type: zigbee_herdsman_1.Zcl.DataType.UINT8,
799
+ manufacturerCode: sunricherManufacturerCode,
800
+ },
801
+ temperatureDisplayMode: {
802
+ ID: 0x1008,
803
+ type: zigbee_herdsman_1.Zcl.DataType.ENUM8,
804
+ manufacturerCode: sunricherManufacturerCode,
805
+ },
806
+ windowOpenCheck: {
807
+ ID: 0x1009,
808
+ type: zigbee_herdsman_1.Zcl.DataType.UINT8,
809
+ manufacturerCode: sunricherManufacturerCode,
810
+ },
811
+ hysteresis: {
812
+ ID: 0x100a,
813
+ type: zigbee_herdsman_1.Zcl.DataType.UINT8,
814
+ manufacturerCode: sunricherManufacturerCode,
815
+ },
816
+ windowOpenFlag: {
817
+ ID: 0x100b,
818
+ type: zigbee_herdsman_1.Zcl.DataType.ENUM8,
819
+ manufacturerCode: sunricherManufacturerCode,
820
+ },
821
+ forcedHeatingTime: {
822
+ ID: 0x100e,
823
+ type: zigbee_herdsman_1.Zcl.DataType.UINT8,
824
+ manufacturerCode: sunricherManufacturerCode,
825
+ },
826
+ errorCode: {
827
+ ID: 0x2003,
828
+ type: zigbee_herdsman_1.Zcl.DataType.BITMAP8,
829
+ manufacturerCode: sunricherManufacturerCode,
830
+ },
831
+ awayOrBoostMode: {
832
+ ID: 0x2002,
833
+ type: zigbee_herdsman_1.Zcl.DataType.ENUM8,
834
+ manufacturerCode: sunricherManufacturerCode,
835
+ },
836
+ },
837
+ commands: {},
838
+ commandsResponse: {},
839
+ }),
840
+ sunricherExtend.thermostatPreset(),
841
+ sunricherExtend.thermostatCurrentHeatingSetpoint(),
842
+ m.battery(),
843
+ m.identify(),
844
+ m.numeric({
845
+ name: 'screen_timeout',
846
+ cluster: 'hvacThermostat',
847
+ attribute: 'screenTimeout',
848
+ valueMin: 10,
849
+ valueMax: 30,
850
+ unit: 's',
851
+ description: 'Screen Timeout for Inactivity (excluding gateway config). Range: 10-30s, Default: 10s',
852
+ access: 'ALL',
853
+ entityCategory: 'config',
854
+ }),
855
+ m.numeric({
856
+ name: 'anti_freezing_temp',
857
+ cluster: 'hvacThermostat',
858
+ attribute: 'antiFreezingTemp',
859
+ valueMin: 0,
860
+ valueMax: 10,
861
+ unit: '°C',
862
+ description: 'Anti Freezing(Low Temp) Mode Configuration. 0: disabled, 5~10: temperature (5°C by default)',
863
+ access: 'ALL',
864
+ entityCategory: 'config',
865
+ }),
866
+ m.enumLookup({
867
+ name: 'temperature_display_mode',
868
+ cluster: 'hvacThermostat',
869
+ attribute: 'temperatureDisplayMode',
870
+ lookup: {
871
+ set_temp: 1,
872
+ room_temp: 2,
873
+ },
874
+ description: 'Temperature Display Mode. 1: displays set temp, 2: displays room temp (default)',
875
+ access: 'ALL',
876
+ }),
877
+ m.numeric({
878
+ name: 'window_open_check',
879
+ cluster: 'hvacThermostat',
880
+ attribute: 'windowOpenCheck',
881
+ valueMin: 0,
882
+ valueMax: 10,
883
+ unit: '°C',
884
+ description: 'The temperature threshold for Window Open Detect, value range 0~10, unit is 1°C, 0 means disabled, default value is 5',
885
+ access: 'ALL',
886
+ entityCategory: 'config',
887
+ }),
888
+ m.numeric({
889
+ name: 'hysteresis',
890
+ cluster: 'hvacThermostat',
891
+ attribute: 'hysteresis',
892
+ valueMin: 5,
893
+ valueMax: 20,
894
+ valueStep: 0.1,
895
+ unit: '°C',
896
+ description: 'Control hysteresis setting, range is 5-20, unit is 0.1°C, default value is 10. Because the sensor accuracy is 0.5°C, it is recommended not to set this value below 1°C to avoid affecting the battery life.',
897
+ access: 'ALL',
898
+ entityCategory: 'config',
899
+ }),
900
+ m.binary({
901
+ name: 'window_open_flag',
902
+ cluster: 'hvacThermostat',
903
+ attribute: 'windowOpenFlag',
904
+ description: 'Window open flag',
905
+ valueOn: ['opened', 1],
906
+ valueOff: ['not_opened', 0],
907
+ access: 'STATE_GET',
908
+ }),
909
+ m.numeric({
910
+ name: 'forced_heating_time',
911
+ cluster: 'hvacThermostat',
912
+ attribute: 'forcedHeatingTime',
913
+ valueMin: 10,
914
+ valueMax: 90,
915
+ unit: '10s',
916
+ description: 'Forced heating time, range 10~90, unit is 10s, default value is 30(300s)',
917
+ access: 'ALL',
918
+ entityCategory: 'config',
919
+ }),
920
+ m.enumLookup({
921
+ name: 'error_code',
922
+ cluster: 'hvacThermostat',
923
+ attribute: 'errorCode',
924
+ lookup: {
925
+ no_error: 0,
926
+ motor_error: 4,
927
+ motor_timeout: 5,
928
+ },
929
+ description: 'Error code: 0=No hardware error, 4=Motor error (detected not running), 5=The motor runs exceeding the self-check time without finding the boundary',
930
+ access: 'STATE_GET',
931
+ }),
932
+ m.enumLookup({
933
+ name: 'temperature_display_unit',
934
+ cluster: 'hvacUserInterfaceCfg',
935
+ attribute: { ID: 0x0000, type: 0x30 },
936
+ lookup: {
937
+ celsius: 0x00,
938
+ fahrenheit: 0x01,
939
+ },
940
+ description: 'The temperature unit shown on the display',
941
+ access: 'ALL',
942
+ entityCategory: 'config',
943
+ }),
944
+ sunricherExtend.thermostatWeeklySchedule(),
945
+ sunricherExtend.thermostatChildLock(),
946
+ ],
947
+ fromZigbee: [fromZigbee_1.default.thermostat],
948
+ toZigbee: [
949
+ toZigbee_1.default.thermostat_local_temperature,
950
+ toZigbee_1.default.thermostat_local_temperature_calibration,
951
+ toZigbee_1.default.thermostat_running_state,
952
+ toZigbee_1.default.thermostat_temperature_display_mode,
953
+ ],
954
+ exposes: [
955
+ e
956
+ .climate()
957
+ .withLocalTemperature(ea.STATE_GET)
958
+ .withSetpoint('current_heating_setpoint', 5, 35, 0.1, ea.ALL)
959
+ .withLocalTemperatureCalibration(-30, 30, 0.5, ea.ALL)
960
+ .withPreset(['off', 'auto', 'away', 'sleep', 'manual', 'forced'], 'Preset of the thermostat. Manual: comfort temp (20°C), Auto: schedule temp (see schedule), ' +
961
+ 'Away: eco temp (6°C), Sleep: night temp (17°C), Forced: temporary heating with configurable duration (default 300s)')
962
+ .withSystemMode(['off', 'auto', 'heat', 'sleep'], ea.ALL)
963
+ .withRunningState(['idle', 'heat']),
964
+ ],
965
+ configure: async (device, coordinatorEndpoint) => {
966
+ const endpoint = device.getEndpoint(1);
967
+ const bindClusters = ['genBasic', 'genPowerCfg', 'hvacThermostat', 'hvacUserInterfaceCfg', 'genTime'];
968
+ const maxRetries = 3;
969
+ let retryCount = 0;
970
+ let bindSuccess = false;
971
+ while (retryCount < maxRetries) {
972
+ try {
973
+ if (!bindSuccess) {
974
+ await reporting.bind(endpoint, coordinatorEndpoint, bindClusters);
975
+ bindSuccess = true;
976
+ }
977
+ const configPromises = [
978
+ reporting.thermostatTemperature(endpoint),
979
+ reporting.thermostatOccupiedHeatingSetpoint(endpoint),
980
+ reporting.thermostatUnoccupiedHeatingSetpoint(endpoint),
981
+ reporting.thermostatRunningState(endpoint),
982
+ reporting.batteryPercentageRemaining(endpoint),
983
+ endpoint.configureReporting('hvacUserInterfaceCfg', (0, reporting_1.payload)('tempDisplayMode', 10, constants_1.repInterval.MINUTE, null)),
984
+ ];
985
+ await Promise.all(configPromises);
986
+ const customAttributes = [
987
+ 'screenTimeout',
988
+ 'antiFreezingTemp',
989
+ 'temperatureDisplayMode',
990
+ 'windowOpenCheck',
991
+ 'hysteresis',
992
+ 'windowOpenFlag',
993
+ 'forcedHeatingTime',
994
+ ];
995
+ await Promise.all(customAttributes.map((attr) => endpoint.configureReporting('hvacThermostat', (0, reporting_1.payload)(attr, 10, constants_1.repInterval.MINUTE, null))));
996
+ const readPromises = [
997
+ endpoint.read('hvacUserInterfaceCfg', ['tempDisplayMode']),
998
+ endpoint.read('hvacThermostat', ['localTemp', 'runningState']),
999
+ endpoint.read('hvacThermostat', [
1000
+ 'screenTimeout',
1001
+ 'antiFreezingTemp',
1002
+ 'temperatureDisplayMode',
1003
+ 'windowOpenCheck',
1004
+ 'hysteresis',
1005
+ 'windowOpenFlag',
1006
+ 'forcedHeatingTime',
1007
+ 'errorCode',
1008
+ ]),
1009
+ ];
1010
+ await Promise.all(readPromises);
1011
+ await syncTimeWithTimeZone(endpoint);
1012
+ break;
1013
+ }
1014
+ catch (e) {
1015
+ retryCount++;
1016
+ logger_1.logger.warning(`Configure attempt ${retryCount} failed: ${e}`, NS);
1017
+ if (retryCount === maxRetries) {
1018
+ logger_1.logger.error(`Failed to configure device after ${maxRetries} attempts`, NS);
1019
+ throw e;
1020
+ }
1021
+ await new Promise((resolve) => setTimeout(resolve, 2000 * retryCount));
1022
+ }
1023
+ }
1024
+ },
1025
+ },
461
1026
  {
462
1027
  zigbeeModel: ['HK-SENSOR-SMO'],
463
1028
  model: 'SR-ZG9070A-SS',
@@ -485,7 +1050,7 @@ exports.definitions = [
485
1050
  commands: {},
486
1051
  commandsResponse: {},
487
1052
  }),
488
- sunricherIndicatorLight(),
1053
+ sunricherExtend.indicatorLight(),
489
1054
  m.numeric({
490
1055
  name: 'detection_area',
491
1056
  cluster: 'sunricherSensor',
@@ -554,7 +1119,7 @@ exports.definitions = [
554
1119
  model: 'SR-ZG2835RAC-UK',
555
1120
  vendor: 'Sunricher',
556
1121
  description: 'Push compatible zigBee knob smart dimmer',
557
- extend: [m.light(), m.electricityMeter(), sunricherExternalSwitchType()],
1122
+ extend: [m.light(), m.electricityMeter(), sunricherExtend.externalSwitchType()],
558
1123
  },
559
1124
  {
560
1125
  zigbeeModel: ['ZG2837RAC-K4'],
@@ -630,7 +1195,7 @@ exports.definitions = [
630
1195
  model: 'SR-ZG9002K16-Pro',
631
1196
  vendor: 'Sunricher',
632
1197
  description: 'Zigbee smart wall panel remote',
633
- extend: [m.battery(), sunricherSRZG9002K16Pro()],
1198
+ extend: [m.battery(), sunricherExtend.SRZG9002K16Pro()],
634
1199
  },
635
1200
  {
636
1201
  zigbeeModel: ['ZG9030A-MW'],
@@ -843,14 +1408,14 @@ exports.definitions = [
843
1408
  model: 'SR-ZG2836D5-Pro',
844
1409
  vendor: 'Sunricher',
845
1410
  description: 'Zigbee smart remote',
846
- extend: [m.battery(), sunricherSRZG2836D5Pro()],
1411
+ extend: [m.battery(), sunricherExtend.SRZG2836D5Pro()],
847
1412
  },
848
1413
  {
849
1414
  zigbeeModel: ['HK-ZRC-K12&RS-E'],
850
1415
  model: 'SR-ZG9002KR12-Pro',
851
1416
  vendor: 'Sunricher',
852
1417
  description: 'Zigbee smart wall panel remote',
853
- extend: [m.battery(), sunricherSRZG9002KR12Pro()],
1418
+ extend: [m.battery(), sunricherExtend.SRZG9002KR12Pro()],
854
1419
  },
855
1420
  {
856
1421
  zigbeeModel: ['ZV9380A', 'ZG9380A'],
@@ -864,14 +1429,14 @@ exports.definitions = [
864
1429
  model: 'SR-ZG2835PAC-AU',
865
1430
  vendor: 'Sunricher',
866
1431
  description: 'Zigbee push button smart dimmer',
867
- extend: [m.light({ configureReporting: true }), sunricherExternalSwitchType(), m.electricityMeter()],
1432
+ extend: [m.light({ configureReporting: true }), sunricherExtend.externalSwitchType(), m.electricityMeter()],
868
1433
  },
869
1434
  {
870
1435
  zigbeeModel: ['HK-SL-DIM-CLN'],
871
1436
  model: 'SR-ZG9101SAC-HP-CLN',
872
1437
  vendor: 'Sunricher',
873
1438
  description: 'Zigbee micro smart dimmer',
874
- extend: [m.light({ configureReporting: true }), sunricherExternalSwitchType(), sunricherMinimumPWM()],
1439
+ extend: [m.light({ configureReporting: true }), sunricherExtend.externalSwitchType(), sunricherExtend.minimumPWM()],
875
1440
  },
876
1441
  {
877
1442
  zigbeeModel: ['HK-SENSOR-CT-MINI'],
@@ -1134,7 +1699,7 @@ exports.definitions = [
1134
1699
  model: 'ZG9101SAC-HP-Switch',
1135
1700
  vendor: 'Sunricher',
1136
1701
  description: 'Zigbee AC in wall switch',
1137
- extend: [m.onOff({ powerOnBehavior: false }), sunricherExternalSwitchType()],
1702
+ extend: [m.onOff({ powerOnBehavior: false }), sunricherExtend.externalSwitchType()],
1138
1703
  },
1139
1704
  {
1140
1705
  zigbeeModel: ['Micro Smart Dimmer', 'SM311', 'HK-SL-RDIM-A', 'HK-SL-DIM-EU-A'],
@@ -1168,7 +1733,7 @@ exports.definitions = [
1168
1733
  model: 'SR-ZG9040A/ZG9041A-D',
1169
1734
  vendor: 'Sunricher',
1170
1735
  description: 'Zigbee micro smart dimmer',
1171
- extend: [m.light({ configureReporting: true }), m.electricityMeter(), sunricherExternalSwitchType(), sunricherMinimumPWM()],
1736
+ extend: [m.light({ configureReporting: true }), m.electricityMeter(), sunricherExtend.externalSwitchType(), sunricherExtend.minimumPWM()],
1172
1737
  },
1173
1738
  {
1174
1739
  zigbeeModel: ['HK-ZD-DIM-A'],