@willieee802/zigbee-herdsman-converters 15.0.8-4.1

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 (312) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +7 -0
  3. package/converters/fromZigbee.js +8439 -0
  4. package/converters/toZigbee.js +7162 -0
  5. package/devices/ITCommander.js +37 -0
  6. package/devices/RMC002.js +20 -0
  7. package/devices/acova.js +101 -0
  8. package/devices/acuity_brands_lighting.js +11 -0
  9. package/devices/adeo.js +256 -0
  10. package/devices/adurosmart.js +130 -0
  11. package/devices/aeotec.js +13 -0
  12. package/devices/airam.js +59 -0
  13. package/devices/ajax_online.js +49 -0
  14. package/devices/akuvox.js +28 -0
  15. package/devices/alchemy.js +18 -0
  16. package/devices/aldi.js +53 -0
  17. package/devices/alecto.js +98 -0
  18. package/devices/anchor.js +17 -0
  19. package/devices/atlantic.js +110 -0
  20. package/devices/atsmart.js +28 -0
  21. package/devices/aubess.js +21 -0
  22. package/devices/aurora_lighting.js +292 -0
  23. package/devices/automaton.js +44 -0
  24. package/devices/awox.js +184 -0
  25. package/devices/axis.js +22 -0
  26. package/devices/bankamp.js +11 -0
  27. package/devices/bega.js +22 -0
  28. package/devices/belkin.js +11 -0
  29. package/devices/bitron.js +334 -0
  30. package/devices/blaupunkt.js +27 -0
  31. package/devices/blitzwolf.js +47 -0
  32. package/devices/bosch.js +702 -0
  33. package/devices/brimate.js +15 -0
  34. package/devices/bseed.js +17 -0
  35. package/devices/bticino.js +118 -0
  36. package/devices/busch-jaeger.js +140 -0
  37. package/devices/byun.js +24 -0
  38. package/devices/calex.js +37 -0
  39. package/devices/candeo.js +49 -0
  40. package/devices/casaia.js +52 -0
  41. package/devices/centralite.js +359 -0
  42. package/devices/cleode.js +20 -0
  43. package/devices/cleverio.js +36 -0
  44. package/devices/climax.js +138 -0
  45. package/devices/commercial_electric.js +16 -0
  46. package/devices/connecte.js +46 -0
  47. package/devices/cree.js +11 -0
  48. package/devices/ctm.js +999 -0
  49. package/devices/current_products_corp.js +24 -0
  50. package/devices/custom_devices_diy.js +850 -0
  51. package/devices/cy-lighting.js +11 -0
  52. package/devices/danalock.js +26 -0
  53. package/devices/danfoss.js +340 -0
  54. package/devices/databyte.ch.js +55 -0
  55. package/devices/datek.js +246 -0
  56. package/devices/dawon_dns.js +338 -0
  57. package/devices/develco.js +868 -0
  58. package/devices/digi.js +14 -0
  59. package/devices/diyruz.js +305 -0
  60. package/devices/dlink.js +37 -0
  61. package/devices/dnake.js +11 -0
  62. package/devices/dresden_elektronik.js +47 -0
  63. package/devices/easyaccess.js +27 -0
  64. package/devices/eatonhalo_led.js +11 -0
  65. package/devices/echostar.js +25 -0
  66. package/devices/ecodim.js +145 -0
  67. package/devices/ecolink.js +21 -0
  68. package/devices/ecosmart.js +64 -0
  69. package/devices/ecozy.js +31 -0
  70. package/devices/edp.js +39 -0
  71. package/devices/eglo.js +25 -0
  72. package/devices/elko.js +176 -0
  73. package/devices/enbrighten.js +163 -0
  74. package/devices/enocean.js +52 -0
  75. package/devices/envilar.js +73 -0
  76. package/devices/essentialb.js +87 -0
  77. package/devices/eurotronic.js +48 -0
  78. package/devices/evanell.js +29 -0
  79. package/devices/evn.js +31 -0
  80. package/devices/evology.js +24 -0
  81. package/devices/evvr.js +17 -0
  82. package/devices/ewelink.js +184 -0
  83. package/devices/ezex.js +24 -0
  84. package/devices/fantem.js +77 -0
  85. package/devices/feibit.js +172 -0
  86. package/devices/fireangel.js +15 -0
  87. package/devices/frankever.js +23 -0
  88. package/devices/garza.js +11 -0
  89. package/devices/ge.js +111 -0
  90. package/devices/gewiss.js +48 -0
  91. package/devices/gidealed.js +11 -0
  92. package/devices/giderwel.js +11 -0
  93. package/devices/giex.js +222 -0
  94. package/devices/girier.js +19 -0
  95. package/devices/gledopto.js +756 -0
  96. package/devices/gmy.js +11 -0
  97. package/devices/gs.js +29 -0
  98. package/devices/halemeier.js +18 -0
  99. package/devices/hampton_bay.js +37 -0
  100. package/devices/heiman.js +805 -0
  101. package/devices/hej.js +107 -0
  102. package/devices/hfh.js +11 -0
  103. package/devices/hgkg.js +54 -0
  104. package/devices/hilux.js +11 -0
  105. package/devices/hive.js +524 -0
  106. package/devices/home_control_as.js +27 -0
  107. package/devices/hommyn.js +24 -0
  108. package/devices/honyar.js +29 -0
  109. package/devices/hornbach.js +53 -0
  110. package/devices/hzc.js +21 -0
  111. package/devices/hzc_electric.js +42 -0
  112. package/devices/icasa.js +121 -0
  113. package/devices/idinio.js +19 -0
  114. package/devices/ihorn.js +61 -0
  115. package/devices/ikea.js +1168 -0
  116. package/devices/ilightsin.js +11 -0
  117. package/devices/iluminize.js +262 -0
  118. package/devices/ilux.js +11 -0
  119. package/devices/immax.js +203 -0
  120. package/devices/innr.js +705 -0
  121. package/devices/inovelli.js +1315 -0
  122. package/devices/insta.js +110 -0
  123. package/devices/iolloi.js +20 -0
  124. package/devices/iotperfect.js +20 -0
  125. package/devices/iris.js +180 -0
  126. package/devices/istar.js +20 -0
  127. package/devices/jasco.js +55 -0
  128. package/devices/javis.js +37 -0
  129. package/devices/jethome.js +48 -0
  130. package/devices/jiawen.js +18 -0
  131. package/devices/jumitech.js +18 -0
  132. package/devices/jxuan.js +49 -0
  133. package/devices/kami.js +15 -0
  134. package/devices/keen_home.js +80 -0
  135. package/devices/klikaanklikuit.js +17 -0
  136. package/devices/kmpcil.js +148 -0
  137. package/devices/konke.js +141 -0
  138. package/devices/ksentry.js +17 -0
  139. package/devices/kurvia.js +17 -0
  140. package/devices/kwikset.js +120 -0
  141. package/devices/lanesto.js +11 -0
  142. package/devices/lds.js +11 -0
  143. package/devices/led_trading.js +69 -0
  144. package/devices/ledvance.js +353 -0
  145. package/devices/leedarson.js +130 -0
  146. package/devices/legrand.js +554 -0
  147. package/devices/lellki.js +146 -0
  148. package/devices/letsled.js +11 -0
  149. package/devices/letv.js +18 -0
  150. package/devices/leviton.js +142 -0
  151. package/devices/lg.js +18 -0
  152. package/devices/lidl.js +1000 -0
  153. package/devices/lifecontrol.js +92 -0
  154. package/devices/lightsolutions.js +37 -0
  155. package/devices/linkind.js +205 -0
  156. package/devices/livingwise.js +59 -0
  157. package/devices/livolo.js +286 -0
  158. package/devices/lixee.js +779 -0
  159. package/devices/lonsonho.js +218 -0
  160. package/devices/lubeez.js +18 -0
  161. package/devices/lupus.js +74 -0
  162. package/devices/lutron.js +32 -0
  163. package/devices/lux.js +40 -0
  164. package/devices/m-elec.js +18 -0
  165. package/devices/makegood.js +51 -0
  166. package/devices/matcall_bv.js +18 -0
  167. package/devices/meazon.js +47 -0
  168. package/devices/mercator.js +225 -0
  169. package/devices/miboxer.js +72 -0
  170. package/devices/micromatic.js +29 -0
  171. package/devices/moes.js +400 -0
  172. package/devices/mycket.js +11 -0
  173. package/devices/m/303/274ller_licht.js +220 -0
  174. package/devices/namron.js +774 -0
  175. package/devices/nanoleaf.js +11 -0
  176. package/devices/neo.js +86 -0
  177. package/devices/net2grid.js +29 -0
  178. package/devices/netvox.js +35 -0
  179. package/devices/niko.js +314 -0
  180. package/devices/ninja_blocks.js +24 -0
  181. package/devices/niviss.js +11 -0
  182. package/devices/nodon.js +109 -0
  183. package/devices/nordtronic.js +30 -0
  184. package/devices/nous.js +88 -0
  185. package/devices/novo.js +17 -0
  186. package/devices/nue_3a.js +417 -0
  187. package/devices/nyce.js +67 -0
  188. package/devices/onesti.js +59 -0
  189. package/devices/openlumi.js +22 -0
  190. package/devices/orvibo.js +515 -0
  191. package/devices/osram.js +519 -0
  192. package/devices/oujiabao.js +15 -0
  193. package/devices/owon.js +352 -0
  194. package/devices/ozsmartthings.js +11 -0
  195. package/devices/paul_neuhaus.js +96 -0
  196. package/devices/paulmann.js +158 -0
  197. package/devices/peq.js +24 -0
  198. package/devices/perenio.js +413 -0
  199. package/devices/philips.js +3118 -0
  200. package/devices/plaid.js +25 -0
  201. package/devices/plugwise.js +173 -0
  202. package/devices/popp.js +10 -0
  203. package/devices/profalux.js +47 -0
  204. package/devices/prolight.js +54 -0
  205. package/devices/qmotion.js +33 -0
  206. package/devices/qoto.js +113 -0
  207. package/devices/quotra.js +18 -0
  208. package/devices/rademacher.js +18 -0
  209. package/devices/rgb_genie.js +119 -0
  210. package/devices/robb.js +335 -0
  211. package/devices/roome.js +15 -0
  212. package/devices/rtx.js +90 -0
  213. package/devices/salus_controls.js +137 -0
  214. package/devices/samotech.js +93 -0
  215. package/devices/saswell.js +58 -0
  216. package/devices/scanproducts.js +32 -0
  217. package/devices/schlage.js +24 -0
  218. package/devices/schneider_electric.js +1039 -0
  219. package/devices/schwaiger.js +62 -0
  220. package/devices/seastar_intelligence.js +16 -0
  221. package/devices/securifi.js +39 -0
  222. package/devices/sengled.js +332 -0
  223. package/devices/sercomm.js +140 -0
  224. package/devices/shenzhen_homa.js +53 -0
  225. package/devices/shinasystem.js +686 -0
  226. package/devices/siglis.js +440 -0
  227. package/devices/sinope.js +1257 -0
  228. package/devices/siterwell.js +46 -0
  229. package/devices/skydance.js +112 -0
  230. package/devices/slv.js +27 -0
  231. package/devices/smart9.js +27 -0
  232. package/devices/smart_home_pty.js +18 -0
  233. package/devices/smartenit.js +42 -0
  234. package/devices/smartthings.js +495 -0
  235. package/devices/smartwings.js +24 -0
  236. package/devices/sohan_electric.js +13 -0
  237. package/devices/solaredge.js +11 -0
  238. package/devices/somgoms.js +47 -0
  239. package/devices/sonoff.js +262 -0
  240. package/devices/spotmau.js +36 -0
  241. package/devices/sprut.js +317 -0
  242. package/devices/stelpro.js +216 -0
  243. package/devices/sunricher.js +625 -0
  244. package/devices/swann.js +33 -0
  245. package/devices/sylvania.js +200 -0
  246. package/devices/tci.js +25 -0
  247. package/devices/technicolor.js +47 -0
  248. package/devices/terncy.js +64 -0
  249. package/devices/the_light_group.js +70 -0
  250. package/devices/third_reality.js +195 -0
  251. package/devices/titan_products.js +22 -0
  252. package/devices/tplink.js +42 -0
  253. package/devices/trust.js +114 -0
  254. package/devices/tubeszb.js +20 -0
  255. package/devices/tuya.js +4215 -0
  256. package/devices/ubisys.js +938 -0
  257. package/devices/uhome.js +25 -0
  258. package/devices/universal_electronics_inc.js +119 -0
  259. package/devices/urlighting.js +12 -0
  260. package/devices/useelink.js +52 -0
  261. package/devices/vbled.js +18 -0
  262. package/devices/vesternet.js +188 -0
  263. package/devices/viessmann.js +51 -0
  264. package/devices/villeroy_boch.js +18 -0
  265. package/devices/vimar.js +78 -0
  266. package/devices/visonic.js +80 -0
  267. package/devices/vrey.js +18 -0
  268. package/devices/wally.js +25 -0
  269. package/devices/waxman.js +45 -0
  270. package/devices/weiser.js +67 -0
  271. package/devices/weten.js +13 -0
  272. package/devices/wisdom.js +11 -0
  273. package/devices/woox.js +177 -0
  274. package/devices/wyze.js +23 -0
  275. package/devices/xiaomi.js +3223 -0
  276. package/devices/xinghuoyuan.js +11 -0
  277. package/devices/yale.js +227 -0
  278. package/devices/ynoa.js +68 -0
  279. package/devices/yookee.js +23 -0
  280. package/devices/ysrsai.js +26 -0
  281. package/devices/zemismart.js +254 -0
  282. package/devices/zen.js +34 -0
  283. package/devices/zipato.js +11 -0
  284. package/index.js +242 -0
  285. package/lib/color.js +784 -0
  286. package/lib/configureKey.js +944 -0
  287. package/lib/constants.js +316 -0
  288. package/lib/exposes.js +677 -0
  289. package/lib/extend.js +180 -0
  290. package/lib/kelvinToXy.js +1912 -0
  291. package/lib/legacy.js +2223 -0
  292. package/lib/light.js +111 -0
  293. package/lib/ota/OTA_URLs.md +119 -0
  294. package/lib/ota/common.js +476 -0
  295. package/lib/ota/index.js +10 -0
  296. package/lib/ota/inovelli.js +72 -0
  297. package/lib/ota/ledvance.js +52 -0
  298. package/lib/ota/lixee.js +57 -0
  299. package/lib/ota/salus.js +82 -0
  300. package/lib/ota/securifi.js +25 -0
  301. package/lib/ota/tradfri.js +45 -0
  302. package/lib/ota/ubisys.js +61 -0
  303. package/lib/ota/zigbeeOTA.js +161 -0
  304. package/lib/philips.js +667 -0
  305. package/lib/reporting.js +234 -0
  306. package/lib/store.js +57 -0
  307. package/lib/tuya.js +2027 -0
  308. package/lib/utils.js +527 -0
  309. package/lib/xiaomi.d.ts +11 -0
  310. package/lib/xiaomi.js +1288 -0
  311. package/lib/zosung.js +243 -0
  312. package/package.json +39 -0
package/lib/xiaomi.js ADDED
@@ -0,0 +1,1288 @@
1
+ 'use strict';
2
+
3
+ const {
4
+ batteryVoltageToPercentage,
5
+ calibrateAndPrecisionRoundOptions,
6
+ calibrateAndPrecisionRoundOptionsIsPercentual,
7
+ postfixWithEndpointName,
8
+ precisionRound,
9
+ getKey,
10
+ } = require('./utils');
11
+
12
+ const exposes = require('../lib/exposes');
13
+ const globalStore = require('../lib/store');
14
+
15
+ const buffer2DataObject = (meta, model, buffer) => {
16
+ const dataObject = {};
17
+
18
+ if (buffer !== null && Buffer.isBuffer(buffer)) {
19
+ // Xiaomi struct parsing
20
+ for (let i = 0; i < buffer.length - 1; i++) {
21
+ const index = buffer[i];
22
+ let value = null;
23
+
24
+ switch (buffer[i + 1]) {
25
+ case 16:
26
+ case 32:
27
+ // 0x10 ZclBoolean
28
+ // 0x20 Zcl8BitUint
29
+ value = buffer.readUInt8(i + 2);
30
+ i += 2;
31
+ break;
32
+ case 33:
33
+ // 0x21 Zcl16BitUint
34
+ value = buffer.readUInt16LE(i + 2);
35
+ i += 3;
36
+ break;
37
+ case 34:
38
+ // 0x22 Zcl24BitUint
39
+ value = buffer.readUIntLE(i + 2, 3);
40
+ i += 4;
41
+ break;
42
+ case 35:
43
+ // 0x23 Zcl32BitUint
44
+ value = buffer.readUInt32LE(i + 2);
45
+ i += 5;
46
+ break;
47
+ case 36:
48
+ // 0x24 Zcl40BitUint
49
+ value = buffer.readUIntLE(i + 2, 5);
50
+ i += 6;
51
+ break;
52
+ case 37:
53
+ // 0x25 Zcl48BitUint
54
+ value = buffer.readUIntLE(i + 2, 6);
55
+ i += 7;
56
+ break;
57
+ case 38:
58
+ // 0x26 Zcl56BitUint
59
+ value = buffer.readUIntLE(i + 2, 7);
60
+ i += 8;
61
+ break;
62
+ case 39:
63
+ // 0x27 Zcl64BitUint
64
+ value = buffer.readBigUInt64BE(i + 2);
65
+ i += 9;
66
+ break;
67
+ case 40:
68
+ // 0x28 Zcl8BitInt
69
+ value = buffer.readInt8(i + 2);
70
+ i += 2;
71
+ break;
72
+ case 41:
73
+ // 0x29 Zcl16BitInt
74
+ value = buffer.readInt16LE(i + 2);
75
+ i += 3;
76
+ break;
77
+ case 42:
78
+ // 0x2A Zcl24BitInt
79
+ value = buffer.readIntLE(i + 2, 3);
80
+ i += 4;
81
+ break;
82
+ case 43:
83
+ // 0x2B Zcl32BitInt
84
+ value = buffer.readInt32LE(i+2);
85
+ i += 5;
86
+ break;
87
+ case 44:
88
+ // 0x2C Zcl40BitInt
89
+ value = buffer.readIntLE(i + 2, 5);
90
+ i += 6;
91
+ break;
92
+ case 45:
93
+ // 0x2D Zcl48BitInt
94
+ value = buffer.readIntLE(i + 2, 6);
95
+ i += 7;
96
+ break;
97
+ case 46:
98
+ // 0x2E Zcl56BitInt
99
+ value = buffer.readIntLE(i + 2, 7);
100
+ i += 8;
101
+ break;
102
+ case 47:
103
+ // 0x2F Zcl64BitInt
104
+ value = buffer.readBigInt64BE(i + 2);
105
+ i += 9;
106
+ break;
107
+ case 57:
108
+ // 0x39 ZclSingleFloat
109
+ value = buffer.readFloatLE(i + 2);
110
+ i += 5;
111
+ break;
112
+ case 58:
113
+ // 0x3a ZclDoubleFloat
114
+ value = buffer.readDoubleLE(i + 2);
115
+ i += 5;
116
+ break;
117
+ case 66:
118
+ // 0x42 unknown, length taken from what seems correct in the logs, maybe is wrong
119
+ if (meta.logger) meta.logger.debug(`${model.zigbeeModel}: unknown vtype=${buffer[i+1]}, pos=${i+1}, moving length 1`);
120
+ i += 2;
121
+ break;
122
+ case 95:
123
+ // 0x5f unknown, length taken from what seems correct in the logs, maybe is wrong
124
+ if (meta.logger) meta.logger.debug(`${model.zigbeeModel}: unknown vtype=${buffer[i+1]}, pos=${i+1}, moving length 4`);
125
+ i += 5;
126
+ break;
127
+ default:
128
+ if (meta.logger) meta.logger.debug(`${model.zigbeeModel}: unknown vtype=${buffer[i + 1]}, pos=${i + 1}`);
129
+ }
130
+
131
+ if (value != null) {
132
+ dataObject[index] = value;
133
+ }
134
+ }
135
+ }
136
+
137
+ if (meta.logger) {
138
+ meta.logger.debug(`${model.zigbeeModel}: Processed buffer into data ${JSON.stringify(dataObject,
139
+ (key, value) => typeof value === 'bigint' ? value.toString() : value)}`);
140
+ }
141
+
142
+
143
+ return dataObject;
144
+ };
145
+
146
+ const numericAttributes2Payload = async (msg, meta, model, options, dataObject) => {
147
+ let payload = {};
148
+
149
+ for (const [key, value] of Object.entries(dataObject)) {
150
+ switch (key) {
151
+ case '0':
152
+ payload.detection_period = value;
153
+ break;
154
+ case '1':
155
+ payload.voltage = value;
156
+ if (model.meta && model.meta.battery && model.meta.battery.voltageToPercentage) {
157
+ payload.battery = batteryVoltageToPercentage(value, model.meta.battery.voltageToPercentage);
158
+ }
159
+ break;
160
+ case '2':
161
+ if (['JT-BZ-01AQ/A'].includes(model.model)) {
162
+ payload.power_outage_count = value - 1;
163
+ }
164
+ break;
165
+ case '3':
166
+ if (['WXCJKG11LM', 'WXCJKG12LM', 'WXCJKG13LM', 'MCCGQ14LM', 'GZCGQ01LM', 'JY-GZ-01AQ', 'CTP-R01'].includes(model.model)) {
167
+ // The temperature value is constant 25 °C and does not change, so we ignore it
168
+ // https://github.com/Koenkk/zigbee2mqtt/issues/11126
169
+ // https://github.com/Koenkk/zigbee-herdsman-converters/pull/3585
170
+ // https://github.com/Koenkk/zigbee2mqtt/issues/13253
171
+ } else {
172
+ payload.device_temperature = calibrateAndPrecisionRoundOptions(value, options, 'device_temperature'); // 0x03
173
+ }
174
+ break;
175
+ case '4':
176
+ payload.mode_switch = {4: 'anti_flicker_mode', 1: 'quick_mode'}[value];
177
+ break;
178
+ case '5':
179
+ payload.power_outage_count = value - 1;
180
+ break;
181
+ case '8':
182
+ if (['ZNLDP13LM'].includes(model.model)) {
183
+ // We don't know what the value means for these devices.
184
+ }
185
+ break;
186
+ case '9':
187
+ if (['ZNLDP13LM'].includes(model.model)) {
188
+ // We don't know what the value means for these devices.
189
+ }
190
+ break;
191
+ case '10':
192
+ if (['ZNLDP13LM', 'CTP-R01'].includes(model.model)) {
193
+ // We don't know what the value means for these devices.
194
+ } else {
195
+ payload.switch_type = {1: 'toggle', 2: 'momentary'}[value];
196
+ }
197
+ break;
198
+ case '11':
199
+ if (['RTCGQ11LM'].includes(model.model)) {
200
+ payload.illuminance = calibrateAndPrecisionRoundOptions(value, options, 'illuminance');
201
+ // DEPRECATED: remove illuminance_lux here.
202
+ payload.illuminance_lux = calibrateAndPrecisionRoundOptions(value, options, 'illuminance_lux');
203
+ }
204
+ break;
205
+ case '12':
206
+ if (['ZNLDP13LM'].includes(model.model)) {
207
+ // We don't know what the value means for these devices.
208
+ }
209
+ break;
210
+ case '100':
211
+ if (['QBKG20LM', 'QBKG31LM', 'QBKG39LM', 'QBKG41LM', 'QBCZ15LM', 'LLKZMK11LM', 'QBKG12LM', 'QBKG03LM', 'QBKG25LM']
212
+ .includes(model.model)) {
213
+ let mapping;
214
+ switch (model.model) {
215
+ case 'QBCZ15LM':
216
+ mapping = 'relay';
217
+ break;
218
+ case 'LLKZMK11LM':
219
+ mapping = 'l1';
220
+ break;
221
+ default:
222
+ mapping = 'left';
223
+ }
224
+ payload[`state_${mapping}`] = value === 1 ? 'ON' : 'OFF';
225
+ } else if (['WXKG14LM', 'WXKG16LM', 'WXKG17LM'].includes(model.model)) {
226
+ payload.click_mode = {1: 'fast', 2: 'multi'}[value];
227
+ } else if (['WXCJKG11LM', 'WXCJKG12LM', 'WXCJKG13LM', 'ZNMS12LM', 'ZNCLBL01LM', 'RTCGQ12LM', 'RTCGQ13LM', 'RTCGQ14LM',
228
+ 'RTCGQ15LM'].includes(model.model)) {
229
+ // We don't know what the value means for these devices.
230
+ // https://github.com/Koenkk/zigbee2mqtt/issues/11126
231
+ // https://github.com/Koenkk/zigbee2mqtt/issues/12279
232
+ } else if (['WSDCGQ01LM', 'WSDCGQ11LM', 'WSDCGQ12LM', 'VOCKQJK11LM'].includes(model.model)) {
233
+ // https://github.com/Koenkk/zigbee2mqtt/issues/798
234
+ // Sometimes the sensor publishes non-realistic vales, filter these
235
+ const temperature = parseFloat(value) / 100.0;
236
+ if (temperature > -65 && temperature < 65) {
237
+ payload.temperature = calibrateAndPrecisionRoundOptions(temperature, options, 'temperature');
238
+ }
239
+ } else if (['RTCGQ11LM'].includes(model.model)) {
240
+ // It contains the occupancy, but in z2m we use a custom timer to do it, so we ignore it
241
+ // payload.occupancy = value === 1;
242
+ } else if (['MCCGQ11LM', 'MCCGQ14LM'].includes(model.model)) {
243
+ payload.contact = value === 0;
244
+ } else if (['SJCGQ11LM'].includes(model.model)) {
245
+ // Ignore the message. It seems not reliable. See discussion here https://github.com/Koenkk/zigbee2mqtt/issues/12018
246
+ // payload.water_leak = value === 1;
247
+ } else if (['SJCGQ13LM'].includes(model.model)) {
248
+ payload.water_leak = value === 1;
249
+ } else if (['JTYJ-GD-01LM/BW'].includes(model.model)) {
250
+ payload.smoke_density = value;
251
+ } else if (['GZCGQ01LM'].includes(model.model)) {
252
+ // DEPRECATED: change illuminance_lux -> illuminance
253
+ payload.illuminance_lux = calibrateAndPrecisionRoundOptions(value, options, 'illuminance_lux');
254
+ } else {
255
+ payload.state = value === 1 ? 'ON' : 'OFF';
256
+ }
257
+ break;
258
+ case '101':
259
+ if (['QBKG20LM', 'QBKG31LM', 'QBKG39LM', 'QBKG41LM', 'QBCZ15LM', 'QBKG25LM', 'QBKG34LM', 'LLKZMK11LM', 'QBKG12LM', 'QBKG03LM']
260
+ .includes(model.model)) {
261
+ let mapping;
262
+ switch (model.model) {
263
+ case 'QBCZ15LM':
264
+ mapping = 'usb';
265
+ break;
266
+ case 'QBKG25LM':
267
+ case 'QBKG34LM':
268
+ mapping = 'center';
269
+ break;
270
+ case 'LLKZMK11LM':
271
+ mapping = 'l2';
272
+ break;
273
+ default:
274
+ mapping = 'right';
275
+ }
276
+ payload[`state_${mapping}`] = value === 1 ? 'ON' : 'OFF';
277
+ } else if (['RTCGQ12LM', 'RTCGQ14LM', 'RTCGQ15LM'].includes(model.model)) {
278
+ // Sometimes RTCGQ14LM reports high illuminance values in the dark
279
+ // https://github.com/Koenkk/zigbee2mqtt/issues/12596
280
+ const illuminance = value > 65000 ? 0 : value;
281
+ payload.illuminance = calibrateAndPrecisionRoundOptions(illuminance, options, 'illuminance');
282
+ } else if (['WSDCGQ01LM', 'WSDCGQ11LM', 'WSDCGQ12LM', 'VOCKQJK11LM'].includes(model.model)) {
283
+ // https://github.com/Koenkk/zigbee2mqtt/issues/798
284
+ // Sometimes the sensor publishes non-realistic vales, filter these
285
+ const humidity = parseFloat(value) / 100.0;
286
+ if (humidity >= 0 && humidity <= 100) {
287
+ payload.humidity = calibrateAndPrecisionRoundOptions(humidity, options, 'humidity');
288
+ }
289
+ } else if (['ZNJLBL01LM', 'ZNCLDJ12LM'].includes(model.model)) {
290
+ payload.battery = value;
291
+ } else if (['ZNCLBL01LM'].includes(model.model)) {
292
+ const battery = value / 2;
293
+ payload.battery = precisionRound(battery, 2);
294
+ } else if (['RTCZCGQ11LM'].includes(model.model)) {
295
+ payload.presence = {0: false, 1: true, 255: null}[value];
296
+ }
297
+ break;
298
+ case '102':
299
+ if (['QBKG25LM', 'QBKG34LM'].includes(model.model)) {
300
+ payload.state_right = value === 1 ? 'ON' : 'OFF';
301
+ } else if (['WSDCGQ01LM', 'WSDCGQ11LM'].includes(model.model)) {
302
+ payload.pressure = calibrateAndPrecisionRoundOptions(value/100.0, options, 'pressure');
303
+ } else if (['WSDCGQ12LM'].includes(model.model)) {
304
+ // This pressure value is ignored because it is less accurate than reported in the 'scaledValue' attribute
305
+ // of the 'msPressureMeasurement' cluster
306
+ } else if (['RTCZCGQ11LM'].includes(model.model)) {
307
+ if (meta.device.applicationVersion < 50) {
308
+ payload.presence_event = {0: 'enter', 1: 'leave', 2: 'left_enter', 3: 'right_leave', 4: 'right_enter',
309
+ 5: 'left_leave', 6: 'approach', 7: 'away', 255: null}[value];
310
+ } else {
311
+ payload.motion_sensitivity = {1: 'low', 2: 'medium', 3: 'high'}[value];
312
+ }
313
+ }
314
+ break;
315
+ case '103':
316
+ if (['RTCZCGQ11LM'].includes(model.model)) {
317
+ payload.monitoring_mode = {0: 'undirected', 1: 'left_right'}[value];
318
+ }
319
+ break;
320
+ case '105':
321
+ if (['RTCGQ13LM'].includes(model.model)) {
322
+ payload.motion_sensitivity = {1: 'low', 2: 'medium', 3: 'high'}[value];
323
+ } else if (['RTCZCGQ11LM'].includes(model.model)) {
324
+ payload.approach_distance = {0: 'far', 1: 'medium', 2: 'near'}[value];
325
+ } else if (['RTCGQ14LM'].includes(model.model)) {
326
+ payload.detection_interval = value;
327
+ }
328
+ break;
329
+ case '106':
330
+ if (['RTCGQ14LM'].includes(model.model)) {
331
+ payload.motion_sensitivity = {1: 'low', 2: 'medium', 3: 'high'}[value];
332
+ }
333
+ break;
334
+ case '107':
335
+ if (['RTCGQ14LM'].includes(model.model)) {
336
+ payload.trigger_indicator = value === 1;
337
+ } else if (['ZNCLBL01LM'].includes(model.model)) {
338
+ const position = options.invert_cover ? 100 - value : value;
339
+ payload.position = position;
340
+ payload.state = options.invert_cover ? (position > 0 ? 'CLOSE' : 'OPEN') : (position > 0 ? 'OPEN' : 'CLOSE');
341
+ }
342
+ break;
343
+ case '149':
344
+ payload.energy = calibrateAndPrecisionRoundOptions(value, options, 'energy'); // 0x95
345
+ // Consumption is deprecated
346
+ payload.consumption = payload.energy;
347
+ break;
348
+ case '150':
349
+ if (!['JTYJ-GD-01LM/BW'].includes(model.model)) {
350
+ payload.voltage = calibrateAndPrecisionRoundOptions(value * 0.1, options, 'voltage'); // 0x96
351
+ }
352
+ break;
353
+ case '151':
354
+ if (['LLKZMK11LM'].includes(model.model)) {
355
+ payload.current = calibrateAndPrecisionRoundOptions(value, options, 'current');
356
+ } else {
357
+ payload.current = calibrateAndPrecisionRoundOptions(value * 0.001, options, 'current');
358
+ }
359
+ break;
360
+ case '152':
361
+ if (['DJT11LM'].includes(model.model)) {
362
+ // We don't know what implies for this device, it contains values like 30, 50,... that don't seem to change
363
+ } else {
364
+ payload.power = calibrateAndPrecisionRoundOptions(value, options, 'power'); // 0x98
365
+ }
366
+ break;
367
+ case '154':
368
+ if (['ZNLDP13LM'].includes(model.model)) {
369
+ // We don't know what the value means for these devices.
370
+ }
371
+ break;
372
+ case '159':
373
+ if (['JT-BZ-01AQ/A'].includes(model.model)) {
374
+ payload.gas_sensitivity = {1: '15%LEL', 2: '10%LEL'}[value];
375
+ }
376
+ break;
377
+ case '160':
378
+ if (['JT-BZ-01AQ/A'].includes(model.model)) {
379
+ payload.gas = value === 1;
380
+ } else if (['JY-GZ-01AQ'].includes(model.model)) {
381
+ payload.smoke = value === 1;
382
+ }
383
+ break;
384
+ case '161':
385
+ if (['JT-BZ-01AQ/A'].includes(model.model)) {
386
+ payload.gas_density = value;
387
+ } else if (['JY-GZ-01AQ'].includes(model.model)) {
388
+ payload.smoke_density = value;
389
+ payload.smoke_density_dbm = {0: 0, 1: 0.085, 2: 0.088, 3: 0.093, 4: 0.095, 5: 0.100, 6: 0.105, 7: 0.110,
390
+ 8: 0.115, 9: 0.120, 10: 0.125}[value];
391
+ }
392
+ break;
393
+ case '162':
394
+ if (['JT-BZ-01AQ/A', 'JY-GZ-01AQ'].includes(model.model)) {
395
+ payload.test = value === 1;
396
+ }
397
+ break;
398
+ case '163':
399
+ if (['JT-BZ-01AQ/A', 'JY-GZ-01AQ'].includes(model.model)) {
400
+ payload.buzzer_manual_mute = value === 1;
401
+ }
402
+ break;
403
+ case '164':
404
+ if (['JT-BZ-01AQ/A'].includes(model.model)) {
405
+ payload.state = {0: 'work', 1: 'preparation'}[value];
406
+ } else if (['JY-GZ-01AQ'].includes(model.model)) {
407
+ payload.heartbeat_indicator = value === 1;
408
+ }
409
+ break;
410
+ case '165':
411
+ if (['JY-GZ-01AQ'].includes(model.model)) {
412
+ payload.linkage_alarm = value === 1;
413
+ }
414
+ break;
415
+ case '166':
416
+ if (['JT-BZ-01AQ/A'].includes(model.model)) {
417
+ payload.linkage_alarm = value === 1;
418
+ }
419
+ break;
420
+ case '240':
421
+ payload.flip_indicator_light = value === 1 ? 'ON' : 'OFF';
422
+ break;
423
+ case '247':
424
+ {
425
+ const dataObject247 = buffer2DataObject(meta, model, value);
426
+ if (['CTP-R01'].includes(model.model)) {
427
+ // execute pending soft switch of operation_mode, if exists
428
+ const opModeSwitchTask = globalStore.getValue(meta.device, 'opModeSwitchTask');
429
+ if (opModeSwitchTask) {
430
+ const {callback, newMode} = opModeSwitchTask;
431
+ try {
432
+ await callback();
433
+ payload.operation_mode = newMode;
434
+ globalStore.putValue(meta.device, 'opModeSwitchTask', null);
435
+ } catch (error) {
436
+ // do nothing when callback fails
437
+ }
438
+ } else {
439
+ payload.operation_mode = {0: 'action_mode', 1: 'scene_mode'}[dataObject247[155]];
440
+ }
441
+ }
442
+ const payload247 = await numericAttributes2Payload(msg, meta, model, options, dataObject247);
443
+ payload = {...payload, ...payload247};
444
+ }
445
+ break;
446
+ case '258':
447
+ payload.detection_interval = value;
448
+ break;
449
+ case '268':
450
+ if (['RTCGQ13LM', 'RTCGQ14LM', 'RTCZCGQ11LM'].includes(model.model)) {
451
+ payload.motion_sensitivity = {1: 'low', 2: 'medium', 3: 'high'}[value];
452
+ } else if (['JT-BZ-01AQ/A'].includes(model.model)) {
453
+ payload.gas_sensitivity = {1: '15%LEL', 2: '10%LEL'}[value];
454
+ }
455
+ break;
456
+ case '276':
457
+ if (['VOCKQJK11LM'].includes(model.model)) {
458
+ payload.display_unit = getKey(VOCKQJK11LMDisplayUnit, value);
459
+ }
460
+ break;
461
+ case '293':
462
+ payload.click_mode = {1: 'fast', 2: 'multi'}[value];
463
+ break;
464
+ case '294':
465
+ if (['JT-BZ-01AQ/A', 'JY-GZ-01AQ'].includes(model.model)) {
466
+ payload.buzzer_manual_mute = value === 1;
467
+ }
468
+ break;
469
+ case '295':
470
+ if (['JT-BZ-01AQ/A', 'JY-GZ-01AQ'].includes(model.model)) {
471
+ payload.test = value === 1;
472
+ }
473
+ break;
474
+ case '313':
475
+ if (['JT-BZ-01AQ/A'].includes(model.model)) {
476
+ payload.state = {0: 'work', 1: 'preparation'}[value];
477
+ }
478
+ break;
479
+ case '314':
480
+ if (['JT-BZ-01AQ/A'].includes(model.model)) {
481
+ payload.gas = value === 1;
482
+ } else if (['JY-GZ-01AQ'].includes(model.model)) {
483
+ payload.smoke = value === 1;
484
+ }
485
+ break;
486
+ case '315':
487
+ if (['JT-BZ-01AQ/A'].includes(model.model)) {
488
+ payload.gas_density = value;
489
+ } else if (['JY-GZ-01AQ'].includes(model.model)) {
490
+ payload.smoke_density = value;
491
+ payload.smoke_density_dbm = {0: 0, 1: 0.085, 2: 0.088, 3: 0.093, 4: 0.095, 5: 0.100, 6: 0.105, 7: 0.110,
492
+ 8: 0.115, 9: 0.120, 10: 0.125}[value];
493
+ }
494
+ break;
495
+ case '316':
496
+ if (['JY-GZ-01AQ'].includes(model.model)) {
497
+ payload.heartbeat_indicator = value === 1;
498
+ }
499
+ break;
500
+ case '317':
501
+ if (['JT-BZ-01AQ/A', 'JY-GZ-01AQ'].includes(model.model)) {
502
+ payload.buzzer_manual_alarm = value === 1;
503
+ }
504
+ break;
505
+ case '320':
506
+ if (['MCCGQ13LM'].includes(model.model)) {
507
+ payload.battery_cover = {0: 'CLOSE', 1: 'OPEN'}[value];
508
+ }
509
+ break;
510
+ case '322':
511
+ if (['RTCZCGQ11LM'].includes(model.model)) {
512
+ payload.presence = {0: false, 1: true, 255: null}[value];
513
+ }
514
+ break;
515
+ case '323':
516
+ if (['RTCZCGQ11LM'].includes(model.model)) {
517
+ payload.presence_event = {0: 'enter', 1: 'leave', 2: 'left_enter', 3: 'right_leave', 4: 'right_enter',
518
+ 5: 'left_leave', 6: 'approach', 7: 'away'}[value];
519
+ }
520
+ break;
521
+ case '324':
522
+ if (['RTCZCGQ11LM'].includes(model.model)) {
523
+ payload.monitoring_mode = {0: 'undirected', 1: 'left_right'}[value];
524
+ }
525
+ break;
526
+ case '326':
527
+ if (['RTCZCGQ11LM'].includes(model.model)) {
528
+ payload.approach_distance = {0: 'far', 1: 'medium', 2: 'near'}[value];
529
+ }
530
+ break;
531
+ case '328':
532
+ if (['CTP-R01'].includes(model.model)) {
533
+ // detected hard switch of operation_mode (attribute 0x148[328])
534
+ payload.operation_mode = {0: 'action_mode', 1: 'scene_mode'}[msg.data[328]];
535
+ }
536
+ break;
537
+ case '329':
538
+ if (['CTP-R01'].includes(model.model)) {
539
+ // side_up attribute report (attribute 0x149[329])
540
+ payload.action = 'side_up';
541
+ payload.side = msg.data[329] + 1;
542
+ }
543
+ break;
544
+ case '331':
545
+ if (['JT-BZ-01AQ/A', 'JY-GZ-01AQ'].includes(model.model)) {
546
+ payload.linkage_alarm = value === 1;
547
+ }
548
+ break;
549
+ case '332':
550
+ if (['JT-BZ-01AQ/A', 'JY-GZ-01AQ'].includes(model.model)) {
551
+ payload.linkage_alarm_state = value === 1;
552
+ }
553
+ break;
554
+ case '338':
555
+ if (['RTCGQ14LM'].includes(model.model)) {
556
+ payload.trigger_indicator = value === 1;
557
+ }
558
+ break;
559
+ case '512':
560
+ if (['ZNCZ15LM', 'QBCZ14LM', 'QBCZ15LM', 'SP-EUC01'].includes(model.model)) {
561
+ payload.button_lock = value === 1 ? 'OFF' : 'ON';
562
+ } else {
563
+ const mode = {0x01: 'control_relay', 0x00: 'decoupled'}[value];
564
+ payload[postfixWithEndpointName('operation_mode', msg, model, meta)] = mode;
565
+ }
566
+ break;
567
+ case '513':
568
+ payload.power_outage_memory = value === 1;
569
+ break;
570
+ case '514':
571
+ payload.auto_off = value === 1;
572
+ break;
573
+ case '515':
574
+ payload.led_disabled_night = value === 1;
575
+ break;
576
+ case '519':
577
+ payload.consumer_connected = value === 1;
578
+ break;
579
+ case '523':
580
+ payload.overload_protection = precisionRound(value, 2);
581
+ break;
582
+ case '550':
583
+ payload.button_switch_mode = value === 1 ? 'relay_and_usb' : 'relay';
584
+ break;
585
+ case '1025':
586
+ if (['ZNCLBL01LM'].includes(model.model)) {
587
+ payload.hand_open = !value;
588
+ } else {
589
+ // next values update only when curtain finished initial setup and knows current position
590
+ payload.options = {...payload.options,
591
+ reverse_direction: value[2] == '\u0001',
592
+ hand_open: value[5] == '\u0000',
593
+ };
594
+ }
595
+ break;
596
+ case '1028':
597
+ payload = {...payload,
598
+ motor_state: (options.invert_cover ? {0: 'stopped', 1: 'closing', 2: 'opening'} :
599
+ {0: 'stopped', 1: 'opening', 2: 'closing'})[value],
600
+ running: !!value,
601
+ };
602
+ break;
603
+ case '1033':
604
+ if (['ZNJLBL01LM'].includes(model.model)) {
605
+ payload.charging_status = value === 1;
606
+ }
607
+ break;
608
+ case '1034':
609
+ if (['ZNJLBL01LM'].includes(model.model)) {
610
+ payload.battery = value;
611
+ }
612
+ break;
613
+ case '1035':
614
+ if (['ZNCLBL01LM'].includes(model.model)) {
615
+ payload.voltage = value;
616
+ }
617
+ break;
618
+ case '1055':
619
+ if (['ZNCLBL01LM'].includes(model.model)) {
620
+ payload.target_position = options.invert_cover ? 100 - value : value;
621
+ }
622
+ break;
623
+ case '1056':
624
+ if (['ZNCLBL01LM'].includes(model.model)) {
625
+ // This is the "target_state" attribute, which takes the following values: 0: 'OPEN', 1: 'CLOSE', 2: 'STOP'.
626
+ // It is not used because the values 0 and 1 are not always reported.
627
+ // https://github.com/Koenkk/zigbee-herdsman-converters/pull/4307
628
+ }
629
+ break;
630
+ case '1057':
631
+ if (['ZNCLBL01LM'].includes(model.model)) {
632
+ payload.motor_state = (options.invert_cover ? {0: 'opening', 1: 'closing', 2: 'stopped'} :
633
+ {0: 'closing', 1: 'opening', 2: 'stopped'})[value];
634
+ payload.running = value < 2 ? true : false;
635
+ }
636
+ break;
637
+ case '1061':
638
+ if (['ZNCLBL01LM'].includes(model.model)) {
639
+ payload.action = (options.invert_cover ? {1: 'manual_close', 2: 'manual_open'} :
640
+ {1: 'manual_open', 2: 'manual_close'})[value];
641
+ }
642
+ break;
643
+ case '1063':
644
+ if (['ZNCLBL01LM'].includes(model.model)) {
645
+ payload.hooks_lock = {0: 'UNLOCK', 1: 'LOCK'}[value];
646
+ }
647
+ break;
648
+ case '1064':
649
+ if (['ZNCLBL01LM'].includes(model.model)) {
650
+ payload.hooks_state = {0: 'unlocked', 1: 'locked', 2: 'locking', 3: 'unlocking'}[value];
651
+ payload.hooks_lock = {0: 'UNLOCK', 1: 'LOCK', 2: 'UNLOCK', 3: 'LOCK'}[value];
652
+ }
653
+ break;
654
+ case '1289':
655
+ payload.dimmer_mode = {3: 'rgbw', 1: 'dual_ct'}[value];
656
+ break;
657
+ case '65281':
658
+ {
659
+ const payload65281 = await numericAttributes2Payload(msg, meta, model, options, value);
660
+ payload = {...payload, ...payload65281};
661
+ }
662
+ break;
663
+ case '65282':
664
+ // This is a a complete structure with attributes, like element 0 for state, element 1 for voltage...
665
+ // At this moment we only extract what we are sure, for example, position 0 seems to be always 1 for a
666
+ // occupancy sensor, so we ignore it at this moment
667
+ if (['MCCGQ01LM'].includes(model.model)) {
668
+ payload.contact = value[0].elmVal === 0;
669
+ }
670
+ payload.voltage = value[1].elmVal;
671
+ if (model.meta && model.meta.battery && model.meta.battery.voltageToPercentage) {
672
+ payload.battery = batteryVoltageToPercentage(payload.voltage, model.meta.battery.voltageToPercentage);
673
+ }
674
+ payload.power_outage_count = value[4].elmVal - 1;
675
+ break;
676
+ case 'mode':
677
+ payload.operation_mode = ['command', 'event'][value];
678
+ break;
679
+ case 'modelId':
680
+ // We ignore it, but we add it here to not shown an unknown key in the log
681
+ break;
682
+ case 'illuminance':
683
+ // It contains the illuminance and occupancy, but in z2m we use a custom timer to do it, so we ignore it
684
+ break;
685
+ default:
686
+ if (meta.logger) meta.logger.debug(`${model.zigbeeModel}: unknown key ${key} with value ${value}`);
687
+ }
688
+ }
689
+
690
+ if (meta.logger) meta.logger.debug(`${model.zigbeeModel}: Processed data into payload ${JSON.stringify(payload)}`);
691
+
692
+ return payload;
693
+ };
694
+
695
+ const VOCKQJK11LMDisplayUnit = {
696
+ 'mgm3_celsius': 0x00, // mg/m³, °C (default)
697
+ 'ppb_celsius': 0x01, // ppb, °C
698
+ 'mgm3_fahrenheit': 0x10, // mg/m³, °F
699
+ 'ppb_fahrenheit': 0x11, // ppb, °F
700
+ };
701
+
702
+ const numericAttributes2Options = (definition) => {
703
+ const supported = ['temperature', 'device_temperature', 'illuminance', 'illuminance_lux',
704
+ 'pressure', 'power', 'current', 'voltage', 'energy', 'power'];
705
+ const precisionSupported = ['temperature', 'humidity', 'pressure', 'power', 'current', 'voltage', 'energy', 'power'];
706
+ const result = [];
707
+ for (const expose of definition.exposes) {
708
+ // only eletrical measurement voltage is supported, not battery
709
+ const isBatteryVoltage = expose.name === 'voltage' && definition.meta && definition.meta.battery;
710
+ if (supported.includes(expose.name) && !isBatteryVoltage) {
711
+ const type = calibrateAndPrecisionRoundOptionsIsPercentual(expose.name) ? 'percentual' : 'absolute';
712
+ result.push(exposes.options.calibration(expose.name, type));
713
+ if (precisionSupported.includes(expose.name)) {
714
+ result.push(exposes.options.precision(expose.name));
715
+ }
716
+ }
717
+ }
718
+
719
+ return result;
720
+ };
721
+
722
+ // For RTCZCGQ11LM
723
+ /**
724
+ * @typedef {{
725
+ * x: number,
726
+ * y: number,
727
+ * }} AqaraFP1RegionZone
728
+ */
729
+ const fp1Constants = {
730
+ region_event_key: 0x0151,
731
+ region_event_types: {
732
+ Enter: 1,
733
+ Leave: 2,
734
+ Occupied: 4,
735
+ Unoccupied: 8,
736
+ },
737
+ region_config_write_attribute: 0x0150,
738
+ region_config_write_attribute_type: 0x41,
739
+ region_config_cmds: {
740
+ /**
741
+ * Creates new region (or force replaces existing one)
742
+ * with new zones definition.
743
+ */
744
+ create: 1,
745
+ /**
746
+ * Modifies existing region.
747
+ * Note: unused, as it seems to break existing regions
748
+ * (region stops reporting new detection events).
749
+ * Use "create" instead, as it replaces existing region with new one.
750
+ */
751
+ modify: 2,
752
+ /**
753
+ * Deletes existing region.
754
+ */
755
+ delete: 3,
756
+ },
757
+ region_config_regionId_min: 1,
758
+ region_config_regionId_max: 10,
759
+ region_config_zoneY_min: 1,
760
+ region_config_zoneY_max: 7,
761
+ region_config_zoneX_min: 1,
762
+ region_config_zoneX_max: 4,
763
+ region_config_cmd_suffix_upsert: 0xff,
764
+ region_config_cmd_suffix_delete: 0x00,
765
+ };
766
+ const fp1Mappers = {
767
+ aqara_fp1: {
768
+ region_event_type_names: {
769
+ [fp1Constants.region_event_types.Enter]: 'enter',
770
+ [fp1Constants.region_event_types.Leave]: 'leave',
771
+ [fp1Constants.region_event_types.Occupied]: 'occupied',
772
+ [fp1Constants.region_event_types.Unoccupied]: 'unoccupied',
773
+ },
774
+ },
775
+ };
776
+ const fp1 = {
777
+ constants: fp1Constants,
778
+ mappers: fp1Mappers,
779
+ /**
780
+ * @param {undefined | Set<number>} xCells
781
+ * @return {number}
782
+ */
783
+ encodeXCellsDefinition: (xCells) => {
784
+ if (!xCells || !xCells.size) {
785
+ return 0;
786
+ }
787
+ return [...xCells.values()].reduce((accumulator, marker) => accumulator + fp1.encodeXCellIdx(marker), 0);
788
+ },
789
+ /**
790
+ * @param {number} cellXIdx
791
+ * @return {number}
792
+ */
793
+ encodeXCellIdx: (cellXIdx) => {
794
+ return 2 ** (cellXIdx - 1);
795
+ },
796
+ // Note: let TypeScript infer the return type to enable union discrimination
797
+ // eslint-disable-next-line valid-jsdoc
798
+ /**
799
+ * @param {unknown} input
800
+ */
801
+ parseAqaraFp1RegionDeleteInput: (input) => {
802
+ if (!input || typeof input !== 'object') {
803
+ return fp1.failure({reason: 'NOT_OBJECT'});
804
+ }
805
+
806
+ if (!('region_id' in input) || !fp1.isAqaraFp1RegionId(input.region_id)) {
807
+ return fp1.failure({reason: 'INVALID_REGION_ID'});
808
+ }
809
+
810
+ return {
811
+ /** @type true */
812
+ isSuccess: true,
813
+ payload: {
814
+ command: {
815
+ region_id: input.region_id,
816
+ },
817
+ },
818
+ };
819
+ },
820
+ // Note: let TypeScript infer the return type to enable union discrimination
821
+ // eslint-disable-next-line valid-jsdoc
822
+ /**
823
+ * @param {unknown} input
824
+ */
825
+ parseAqaraFp1RegionUpsertInput: (input) => {
826
+ if (!input || typeof input !== 'object') {
827
+ return fp1.failure({reason: 'NOT_OBJECT'});
828
+ }
829
+
830
+ if (!('region_id' in input) || !fp1.isAqaraFp1RegionId(input.region_id)) {
831
+ return fp1.failure({reason: 'INVALID_REGION_ID'});
832
+ }
833
+
834
+ if (!('zones' in input) || !Array.isArray(input.zones) || !input.zones.length) {
835
+ return fp1.failure({reason: 'ZONES_LIST_EMPTY'});
836
+ }
837
+
838
+ if (!input.zones.every(fp1.isAqaraFp1RegionZoneDefinition)) {
839
+ return fp1.failure({reason: 'INVALID_ZONES'});
840
+ }
841
+
842
+ return {
843
+ /** @type true */
844
+ isSuccess: true,
845
+ payload: {
846
+ command: {
847
+ region_id: input.region_id,
848
+ zones: input.zones,
849
+ },
850
+ },
851
+ };
852
+ },
853
+ // Note: this is valid typescript JSDoc
854
+ // eslint-disable-next-line valid-jsdoc
855
+ /**
856
+ * @param {unknown} value
857
+ * @returns {value is number}
858
+ */
859
+ isAqaraFp1RegionId: (value) => {
860
+ return (
861
+ typeof value === 'number' &&
862
+ value >= fp1.constants.region_config_regionId_min &&
863
+ value <= fp1.constants.region_config_regionId_max
864
+ );
865
+ },
866
+ // Note: this is valid typescript JSDoc
867
+ // eslint-disable-next-line valid-jsdoc
868
+ /**
869
+ * @param {unknown} value
870
+ * @returns {value is AqaraFP1RegionZone}
871
+ */
872
+ isAqaraFp1RegionZoneDefinition: (value) => {
873
+ return (
874
+ value &&
875
+ typeof value === 'object' &&
876
+ 'x' in value &&
877
+ 'y' in value &&
878
+ typeof value.x === 'number' &&
879
+ typeof value.y === 'number' &&
880
+ value.x >= fp1.constants.region_config_zoneX_min &&
881
+ value.x <= fp1.constants.region_config_zoneX_max &&
882
+ value.y >= fp1.constants.region_config_zoneY_min &&
883
+ value.y <= fp1.constants.region_config_zoneY_max
884
+ );
885
+ },
886
+ /**
887
+ * @template {Record<string, unknown>} ErrorType
888
+ * @param {ErrorType} error
889
+ * @return { { isSuccess: false, error: ErrorType } }
890
+ */
891
+ failure: (error) => {
892
+ return {
893
+ isSuccess: false,
894
+ error,
895
+ };
896
+ },
897
+ };
898
+
899
+ /**
900
+ * @param {Buffer} buffer
901
+ * @param {number} offset
902
+ * @return {number}
903
+ */
904
+ function readTemperature(buffer, offset) {
905
+ return buffer.readUint16BE(offset) / 100;
906
+ }
907
+
908
+ /**
909
+ * @param {Buffer} buffer
910
+ * @param {number} offset
911
+ * @param {number} temperature
912
+ * @return {void}
913
+ */
914
+ function writeTemperature(buffer, offset, temperature) {
915
+ buffer.writeUint16BE(temperature * 100, offset);
916
+ }
917
+
918
+ /**
919
+ * @type {Day[]}
920
+ */
921
+ const dayNames = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'];
922
+
923
+ /**
924
+ * @param {Buffer} buffer
925
+ * @param {number} offset
926
+ * @return {Day[]}
927
+ */
928
+ function readDaySelection(buffer, offset) {
929
+ const selectedDays = [];
930
+
931
+ dayNames.forEach((day, index) => {
932
+ if ((buffer[offset] >> index + 1) % 2 !== 0) {
933
+ selectedDays.push(day);
934
+ }
935
+ });
936
+
937
+ return selectedDays;
938
+ }
939
+
940
+ /**
941
+ * @param {Day[]} selectedDays
942
+ */
943
+ function validateDaySelection(selectedDays) {
944
+ selectedDays.filter((selectedDay) => !dayNames.includes(selectedDay)).forEach((invalidValue) => {
945
+ throw new Error(`The value "${invalidValue}" is not a valid day (available values: ${dayNames.join(', ')})`);
946
+ });
947
+ }
948
+
949
+ /**
950
+ * @param {Buffer} buffer
951
+ * @param {number} offset
952
+ * @param {Day[]} selectedDays
953
+ */
954
+ function writeDaySelection(buffer, offset, selectedDays) {
955
+ validateDaySelection(selectedDays);
956
+
957
+ const bitMap = dayNames.reduce((repeat, dayName, index) => {
958
+ const isDaySelected = selectedDays.includes(dayName);
959
+ return repeat | isDaySelected << index + 1;
960
+ }, 0);
961
+
962
+ buffer.writeUInt8(bitMap, offset);
963
+ }
964
+
965
+ const timeNextDayFlag = 1 << 15;
966
+
967
+ /**
968
+ * @param {Buffer} buffer
969
+ * @param {number} offset
970
+ * @return {number}
971
+ */
972
+ function readTime(buffer, offset) {
973
+ const minutesWithDayFlag = buffer.readUint16BE(offset);
974
+ return minutesWithDayFlag & ~timeNextDayFlag;
975
+ }
976
+
977
+ /**
978
+ * @param {number} time
979
+ * @return {void}
980
+ */
981
+ function validateTime(time) {
982
+ const isPositiveInteger = (value) => typeof value === 'number' && Number.isInteger(value) && value >= 0;
983
+
984
+ if (!isPositiveInteger(time)) {
985
+ throw new Error(`Time must be a positive integer number`);
986
+ }
987
+
988
+ if (time >= 24 * 60) {
989
+ throw new Error(`Time must be between 00:00 and 23:59`);
990
+ }
991
+ }
992
+
993
+ /**
994
+ * @param {Buffer} buffer
995
+ * @param {number} offset
996
+ * @param {number} time
997
+ * @param {boolean} isNextDay
998
+ * @return {void}
999
+ */
1000
+ function writeTime(buffer, offset, time, isNextDay) {
1001
+ validateTime(time);
1002
+
1003
+ let minutesWithDayFlag = time;
1004
+
1005
+ if (isNextDay) {
1006
+ minutesWithDayFlag = minutesWithDayFlag | timeNextDayFlag;
1007
+ }
1008
+
1009
+ buffer.writeUint16BE(minutesWithDayFlag, offset);
1010
+ }
1011
+
1012
+ /**
1013
+ * Formats a number of minutes into a user-readable 24-hour time notation in the form hh:mm.
1014
+ * @param {number} timeMinutes
1015
+ * @return {string}
1016
+ */
1017
+ function formatTime(timeMinutes) {
1018
+ const hours = Math.floor(timeMinutes / 60);
1019
+ const minutes = timeMinutes % 60;
1020
+ return `${hours}:${String(minutes).padStart(2, '0')}`;
1021
+ }
1022
+
1023
+ /**
1024
+ * Parses a 24-hour time notation string in the form hh:mm into a number of minutes.
1025
+ * @param {string} timeString
1026
+ * @return {number}
1027
+ */
1028
+ function parseTime(timeString) {
1029
+ const parts = timeString.split(':');
1030
+
1031
+ if (parts.length !== 2) {
1032
+ throw new Error(`Cannot parse time string ${timeString}`);
1033
+ }
1034
+
1035
+ const hours = parseInt(parts[0]);
1036
+ const minutes = parseInt(parts[1]);
1037
+
1038
+ return hours * 60 + minutes;
1039
+ }
1040
+
1041
+ const stringifiedScheduleFragmentSeparator = '|';
1042
+ const stringifiedScheduleValueSeparator = ',';
1043
+
1044
+ const trv = {
1045
+ decodeFirmwareVersionString(value) {
1046
+ // Add prefix to follow Aqara's versioning schema: https://www.aqara.com/en/version/radiator-thermostat-e1
1047
+ const firmwareVersionPrefix = '0.0.0_';
1048
+
1049
+ // Reinterpret from LE integer to byte sequence(e.g., `[25,8,0,0]` corresponds to 0.0.0_0825)
1050
+ const buffer = Buffer.alloc(4);
1051
+ buffer.writeUInt32LE(value);
1052
+ const firmwareVersionNumber = buffer.reverse().subarray(1).join('');
1053
+
1054
+ return firmwareVersionPrefix + firmwareVersionNumber;
1055
+ },
1056
+
1057
+ decodePreset(value) {
1058
+ // Setup mode is the initial device state after powering it ("F11" on display) and not a real preset that can be deliberately
1059
+ // set by users, therefore it is exposed as a separate flag.
1060
+ return {
1061
+ setup: value === 3,
1062
+ preset: {2: 'away', 1: 'auto', 0: 'manual'}[value],
1063
+ };
1064
+ },
1065
+
1066
+ decodeHeartbeat(meta, model, messageBuffer) {
1067
+ const data = buffer2DataObject(meta, model, messageBuffer);
1068
+ const payload = {};
1069
+
1070
+ Object.entries(data).forEach(([key, value]) => {
1071
+ switch (parseInt(key)) {
1072
+ case 3:
1073
+ payload.device_temperature = value;
1074
+ break;
1075
+ case 5:
1076
+ payload.power_outage_count = value - 1;
1077
+ break;
1078
+ case 10:
1079
+ // unidentified number, e.g. 32274, 3847
1080
+ break;
1081
+ case 13:
1082
+ payload.firmware_version = trv.decodeFirmwareVersionString(value);
1083
+ break;
1084
+ case 17:
1085
+ // unidentified flag/enum, e.g. 1
1086
+ break;
1087
+ case 101:
1088
+ Object.assign(payload, trv.decodePreset(value));
1089
+ break;
1090
+ case 102:
1091
+ payload.local_temperature = value / 100;
1092
+ break;
1093
+ case 103:
1094
+ // This takes the following values:
1095
+ // - `occupied_heating_setpoint` if `system_mode` is `heat` and `preset` is `manual`
1096
+ // - `away_preset_temperature` if `system_mode` is `heat` and `preset` is `away`
1097
+ // - `5` if `system_mode` is `off`
1098
+ // It thus behaves similar to `occupied_heating_setpoint` except in `off` mode. Due to this difference,
1099
+ // this value is written to another property to avoid an inconsistency of the `occupied_heating_setpoint`.
1100
+ // TODO How to handle this value? Find better name?
1101
+ payload.internal_heating_setpoint = value / 100;
1102
+ break;
1103
+ case 104:
1104
+ payload.valve_alarm = value === 1;
1105
+ break;
1106
+ case 105:
1107
+ payload.battery = value;
1108
+ break;
1109
+ case 106:
1110
+ // unidentified flag/enum, e.g. 0
1111
+ break;
1112
+ }
1113
+ });
1114
+
1115
+ return payload;
1116
+ },
1117
+
1118
+ /**
1119
+ * Decode a Zigbee schedule configuration message into a schedule configuration object.
1120
+ * @param {Buffer} buffer
1121
+ * @return {TrvScheduleConfig}
1122
+ */
1123
+ decodeSchedule(buffer) {
1124
+ return {
1125
+ days: readDaySelection(buffer, 1),
1126
+ events: [
1127
+ {time: readTime(buffer, 2), temperature: readTemperature(buffer, 6)},
1128
+ {time: readTime(buffer, 8), temperature: readTemperature(buffer, 12)},
1129
+ {time: readTime(buffer, 14), temperature: readTemperature(buffer, 18)},
1130
+ {time: readTime(buffer, 20), temperature: readTemperature(buffer, 24)},
1131
+ ],
1132
+ };
1133
+ },
1134
+
1135
+ /**
1136
+ * @param {TrvScheduleConfig} schedule
1137
+ * @return {void}
1138
+ */
1139
+ validateSchedule(schedule) {
1140
+ const eventCount = 4;
1141
+
1142
+ if (typeof schedule !== 'object') {
1143
+ throw new Error('The provided value must be a schedule object');
1144
+ }
1145
+
1146
+ if (schedule.days == null || !Array.isArray(schedule.days) || schedule.days.length === 0) {
1147
+ throw new Error(`The schedule object must contain an array of days with at least one entry`);
1148
+ }
1149
+
1150
+ validateDaySelection(schedule.days);
1151
+
1152
+ if (schedule.events == null || !Array.isArray(schedule.events) || schedule.events.length !== eventCount) {
1153
+ throw new Error(`The schedule object must contain an array of ${eventCount} time/temperature events`);
1154
+ }
1155
+
1156
+ schedule.events.forEach((event) => {
1157
+ if (typeof event !== 'object') {
1158
+ throw new Error('The provided time/temperature event must be an object');
1159
+ }
1160
+
1161
+ validateTime(event.time);
1162
+
1163
+ if (typeof event.temperature !== 'number') {
1164
+ throw new Error(`The provided time/temperature entry must contain a numeric temperature`);
1165
+ }
1166
+
1167
+ if (event.temperature < 5 || event.temperature > 30) {
1168
+ throw new Error(`The temperature must be between 5 and 30 °C`);
1169
+ }
1170
+ });
1171
+
1172
+ // Calculate time durations between events
1173
+ const durations = schedule.events
1174
+ .map((entry, index, entries) => {
1175
+ if (index === 0) {
1176
+ return 0;
1177
+ }
1178
+
1179
+ const time = entry.time;
1180
+ const fullDay = 24 * 60;
1181
+ const previousTime = entries[index - 1].time;
1182
+ const isNextDay = time < previousTime;
1183
+
1184
+ if (isNextDay) {
1185
+ return (fullDay - previousTime) + time;
1186
+ } else {
1187
+ return time - previousTime;
1188
+ }
1189
+ })
1190
+ // Remove first entry which is not a duration
1191
+ .slice(1);
1192
+
1193
+ const minDuration = 60;
1194
+ const hasInvalidDurations = durations.some((duration) => duration < minDuration);
1195
+
1196
+ if (hasInvalidDurations) {
1197
+ throw new Error(`The individual times must be at least 1 hour apart`);
1198
+ }
1199
+
1200
+ const maxTotalDuration = 24 * 60;
1201
+ const totalDuration = durations.reduce((total, duration) => total + duration, 0);
1202
+
1203
+ if (totalDuration > maxTotalDuration) {
1204
+ // this implicitly also makes sure that there is at most one "next day" switch
1205
+ throw new Error(`The start and end times must be at most 24 hours apart`);
1206
+ }
1207
+ },
1208
+
1209
+ /**
1210
+ * Encodes a schedule object into Zigbee message format.
1211
+ * @param {TrvScheduleConfig} schedule
1212
+ * @return {Buffer}
1213
+ */
1214
+ encodeSchedule(schedule) {
1215
+ const buffer = Buffer.alloc(26);
1216
+ buffer.writeUInt8(0x04);
1217
+
1218
+ writeDaySelection(buffer, 1, schedule.days);
1219
+
1220
+ schedule.events.forEach((event, index, events) => {
1221
+ const offset = 2 + index * 6;
1222
+ const isNextDay = index > 0 && event.time < events[index - 1].time;
1223
+
1224
+ writeTime(buffer, offset, event.time, isNextDay);
1225
+ writeTemperature(buffer, offset + 4, event.temperature);
1226
+ });
1227
+
1228
+ return buffer;
1229
+ },
1230
+
1231
+ /**
1232
+ * Converts a schedule config object into a configuration string.
1233
+ * @param {TrvScheduleConfig} schedule
1234
+ * @return {string}
1235
+ */
1236
+ stringifySchedule(schedule) {
1237
+ const stringifiedScheduleFragments = [schedule.days.join(stringifiedScheduleValueSeparator)];
1238
+
1239
+ for (const event of schedule.events) {
1240
+ const formattedTemperature = Number.isInteger(event.temperature) ?
1241
+ event.temperature.toFixed(1) : // add ".0" for usability to signal that floats can be used
1242
+ String(event.temperature);
1243
+
1244
+ const entryFragments = [formatTime(event.time), formattedTemperature];
1245
+
1246
+ stringifiedScheduleFragments.push(entryFragments.join(stringifiedScheduleValueSeparator));
1247
+ }
1248
+
1249
+ return stringifiedScheduleFragments.join(stringifiedScheduleFragmentSeparator);
1250
+ },
1251
+
1252
+ /**
1253
+ * Parses a schedule configuration string into a configuration object.
1254
+ * @param {string} stringifiedSchedule
1255
+ * @return {TrvScheduleConfig}
1256
+ */
1257
+ parseSchedule(stringifiedSchedule) {
1258
+ const schedule = {days: [], events: []};
1259
+
1260
+ if (!stringifiedSchedule) {
1261
+ return schedule;
1262
+ }
1263
+
1264
+ const stringifiedScheduleFragments = stringifiedSchedule.split(stringifiedScheduleFragmentSeparator);
1265
+
1266
+ stringifiedScheduleFragments.forEach((fragment, index) => {
1267
+ if (index === 0) {
1268
+ schedule.days.push(...fragment.split(stringifiedScheduleValueSeparator));
1269
+ } else {
1270
+ const entryFragments = fragment.split(stringifiedScheduleValueSeparator);
1271
+ const entry = {time: parseTime(entryFragments[0]), temperature: parseFloat(entryFragments[1])};
1272
+ schedule.events.push(entry);
1273
+ }
1274
+ });
1275
+
1276
+ return schedule;
1277
+ },
1278
+ };
1279
+
1280
+ module.exports = {
1281
+ buffer2DataObject,
1282
+ numericAttributes2Payload,
1283
+ numericAttributes2Options,
1284
+ VOCKQJK11LMDisplayUnit,
1285
+ fp1,
1286
+ manufacturerCode: 0x115f,
1287
+ trv,
1288
+ };