@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/tuya.js ADDED
@@ -0,0 +1,2027 @@
1
+ 'use strict';
2
+
3
+ const constants = require('./constants');
4
+ const globalStore = require('./store');
5
+ const exposes = require('./exposes');
6
+ const utils = require('./utils');
7
+ const e = exposes.presets;
8
+ const ea = exposes.access;
9
+
10
+ const dataTypes = {
11
+ raw: 0, // [ bytes ]
12
+ bool: 1, // [0/1]
13
+ value: 2, // [ 4 byte value ]
14
+ string: 3, // [ N byte string ]
15
+ enum: 4, // [ 0-255 ]
16
+ bitmap: 5, // [ 1,2,4 bytes ] as bits
17
+ };
18
+
19
+ const convertMultiByteNumberPayloadToSingleDecimalNumber = (chunks) => {
20
+ // Destructuring "chunks" is needed because it's a Buffer
21
+ // and we need a simple array.
22
+ let value = 0;
23
+ for (let i = 0; i < chunks.length; i++) {
24
+ value = value << 8;
25
+ value += chunks[i];
26
+ }
27
+ return value;
28
+ };
29
+
30
+ function firstDpValue(msg, meta, converterName) {
31
+ const dpValues = msg.data.dpValues;
32
+ for (let index = 1; index < dpValues.length; index++) {
33
+ meta.logger.warn(`zigbee-herdsman-converters:${converterName}: Additional DP #${
34
+ dpValues[index].dp} with data ${JSON.stringify(dpValues[index])} will be ignored! ` +
35
+ 'Use a for loop in the fromZigbee converter (see ' +
36
+ 'https://www.zigbee2mqtt.io/advanced/support-new-devices/02_support_new_tuya_devices.html)');
37
+ }
38
+ return dpValues[0];
39
+ }
40
+
41
+ function getDataValue(dpValue) {
42
+ switch (dpValue.datatype) {
43
+ case dataTypes.raw:
44
+ return dpValue.data;
45
+ case dataTypes.bool:
46
+ return dpValue.data[0] === 1;
47
+ case dataTypes.value:
48
+ return convertMultiByteNumberPayloadToSingleDecimalNumber(dpValue.data);
49
+ case dataTypes.string:
50
+ // eslint-disable-next-line
51
+ let dataString = '';
52
+ // Don't use .map here, doesn't work: https://github.com/Koenkk/zigbee-herdsman-converters/pull/1799/files#r530377091
53
+ for (let i = 0; i < dpValue.data.length; ++i) {
54
+ dataString += String.fromCharCode(dpValue.data[i]);
55
+ }
56
+ return dataString;
57
+ case dataTypes.enum:
58
+ return dpValue.data[0];
59
+ case dataTypes.bitmap:
60
+ return convertMultiByteNumberPayloadToSingleDecimalNumber(dpValue.data);
61
+ }
62
+ }
63
+
64
+ function getTypeName(dpValue) {
65
+ const entry = Object.entries(dataTypes).find(([typeName, typeId]) => typeId === dpValue.datatype);
66
+ return (entry ? entry[0] : 'unknown');
67
+ }
68
+
69
+ function getDataPointNames(dpValue) {
70
+ const entries = Object.entries(dataPoints).filter(([dpName, dpId]) => dpId === dpValue.dp);
71
+ return entries.map(([dpName, dpId]) => dpName);
72
+ }
73
+
74
+ function logDataPoint(where, msg, dpValue, meta) {
75
+ meta.logger.info(`zigbee-herdsman-converters:${where}: Received Tuya DataPoint #${
76
+ dpValue.dp} from ${meta.device.ieeeAddr} with raw data '${JSON.stringify(dpValue)}': type='${
77
+ msg.type}', datatype='${getTypeName(dpValue)}', value='${
78
+ getDataValue(dpValue)}', known DP# usage: ${JSON.stringify(getDataPointNames(dpValue))}`);
79
+ }
80
+
81
+ function logUnexpectedDataPoint(where, msg, dpValue, meta) {
82
+ meta.logger.warn(`zigbee-herdsman-converters:${where}: Received unexpected Tuya DataPoint #${
83
+ dpValue.dp} from ${meta.device.ieeeAddr} with raw data '${JSON.stringify(dpValue)}': type='${
84
+ msg.type}', datatype='${getTypeName(dpValue)}', value='${
85
+ getDataValue(dpValue)}', known DP# usage: ${JSON.stringify(getDataPointNames(dpValue))}`);
86
+ }
87
+
88
+ function logUnexpectedDataType(where, msg, dpValue, meta, expectedDataType) {
89
+ meta.logger.warn(`zigbee-herdsman-converters:${where}: Received Tuya DataPoint #${
90
+ dpValue.dp} with unexpected datatype from ${meta.device.ieeeAddr} with raw data '${
91
+ JSON.stringify(dpValue)}': type='${msg.type}', datatype='${
92
+ getTypeName(dpValue)}' (instead of '${expectedDataType}'), value='${
93
+ getDataValue(dpValue)}', known DP# usage: ${JSON.stringify(getDataPointNames(dpValue))}`);
94
+ }
95
+
96
+ function logUnexpectedDataValue(where, msg, dpValue, meta, valueKind, expectedMinValue=null, expectedMaxValue=null) {
97
+ if (expectedMinValue === null) {
98
+ if (expectedMaxValue === null) {
99
+ meta.logger.warn(`zigbee-herdsman-converters:${where}: Received Tuya DataPoint #${dpValue.dp
100
+ } with invalid value ${getDataValue(dpValue)} for ${valueKind} from ${meta.device.ieeeAddr}`);
101
+ } else {
102
+ meta.logger.warn(`zigbee-herdsman-converters:${where}: Received Tuya DataPoint #${dpValue.dp
103
+ } with invalid value ${getDataValue(dpValue)} for ${valueKind} from ${meta.device.ieeeAddr
104
+ } which is higher than the expected maximum of ${expectedMaxValue}`);
105
+ }
106
+ } else {
107
+ if (expectedMaxValue === null) {
108
+ meta.logger.warn(`zigbee-herdsman-converters:${where}: Received Tuya DataPoint #${dpValue.dp
109
+ } with invalid value ${getDataValue(dpValue)} for ${valueKind} from ${meta.device.ieeeAddr
110
+ } which is lower than the expected minimum of ${expectedMinValue}`);
111
+ } else {
112
+ meta.logger.warn(`zigbee-herdsman-converters:${where}: Received Tuya DataPoint #${dpValue.dp
113
+ } with invalid value ${getDataValue(dpValue)} for ${valueKind} from ${meta.device.ieeeAddr
114
+ } which is outside the expected range from ${expectedMinValue} to ${expectedMaxValue}`);
115
+ }
116
+ }
117
+ }
118
+
119
+ function convertRawToCycleTimer(value) {
120
+ let timernr = 0;
121
+ let starttime = '00:00';
122
+ let endtime = '00:00';
123
+ let irrigationDuration = 0;
124
+ let pauseDuration = 0;
125
+ let weekdays = 'once';
126
+ let timeractive = 0;
127
+ if (value.length > 11) {
128
+ timernr = value[1];
129
+ timeractive = value[2];
130
+ if (value[3] > 0) {
131
+ weekdays = (value[3] & 0x40 ? 'Sa' : '') +
132
+ (value[3] & 0x20 ? 'Fr' : '') +
133
+ (value[3] & 0x10 ? 'Th' : '') +
134
+ (value[3] & 0x08 ? 'We' : '') +
135
+ (value[3] & 0x04 ? 'Tu' : '') +
136
+ (value[3] & 0x02 ? 'Mo' : '') +
137
+ (value[3] & 0x01 ? 'Su' : '');
138
+ } else {
139
+ weekdays = 'once';
140
+ }
141
+ let minsincemidnight = value[4] * 256 + value[5];
142
+ starttime = String(parseInt(minsincemidnight / 60)).padStart(2, '0') + ':' + String(minsincemidnight % 60).padStart(2, '0');
143
+ minsincemidnight = value[6] * 256 + value[7];
144
+ endtime = String(parseInt(minsincemidnight / 60)).padStart(2, '0') + ':' + String(minsincemidnight % 60).padStart(2, '0');
145
+ irrigationDuration = value[8] * 256 + value[9];
146
+ pauseDuration = value[10] * 256 + value[11];
147
+ }
148
+ return {
149
+ timernr: timernr,
150
+ starttime: starttime,
151
+ endtime: endtime,
152
+ irrigationDuration: irrigationDuration,
153
+ pauseDuration: pauseDuration,
154
+ weekdays: weekdays,
155
+ active: timeractive,
156
+ };
157
+ }
158
+
159
+ function convertRawToTimer(value) {
160
+ let timernr = 0;
161
+ let starttime = '00:00';
162
+ let duration = 0;
163
+ let weekdays = 'once';
164
+ let timeractive = '';
165
+ if (value.length > 12) {
166
+ timernr = value[1];
167
+ const minsincemidnight = value[2] * 256 + value[3];
168
+ starttime = String(parseInt(minsincemidnight / 60)).padStart(2, '0') + ':' + String(minsincemidnight % 60).padStart(2, '0');
169
+ duration = value[4] * 256 + value[5];
170
+ if (value[6] > 0) {
171
+ weekdays = (value[6] & 0x40 ? 'Sa' : '') +
172
+ (value[6] & 0x20 ? 'Fr' : '') +
173
+ (value[6] & 0x10 ? 'Th' : '') +
174
+ (value[6] & 0x08 ? 'We' : '') +
175
+ (value[6] & 0x04 ? 'Tu' : '') +
176
+ (value[6] & 0x02 ? 'Mo' : '') +
177
+ (value[6] & 0x01 ? 'Su' : '');
178
+ } else {
179
+ weekdays = 'once';
180
+ }
181
+ timeractive = value[8];
182
+ }
183
+ return {timernr: timernr, time: starttime, duration: duration, weekdays: weekdays, active: timeractive};
184
+ }
185
+
186
+ function convertTimeTo2ByteHexArray(time) {
187
+ const timeArray = time.split(':');
188
+ if (timeArray.length != 2) {
189
+ throw new Error('Time format incorrect');
190
+ }
191
+ const timeHour = parseInt(timeArray[0]);
192
+ const timeMinute = parseInt(timeArray[1]);
193
+
194
+ if (timeHour > 23 || timeMinute > 59) {
195
+ throw new Error('Time incorrect');
196
+ }
197
+ return convertDecimalValueTo2ByteHexArray(timeHour * 60 + timeMinute);
198
+ }
199
+
200
+ function convertWeekdaysTo1ByteHexArray(weekdays) {
201
+ let nr = 0;
202
+ if (weekdays == 'once') {
203
+ return nr;
204
+ }
205
+ if (weekdays.includes('Mo')) {
206
+ nr |= 0x40;
207
+ }
208
+ if (weekdays.includes('Tu')) {
209
+ nr |= 0x20;
210
+ }
211
+ if (weekdays.includes('We')) {
212
+ nr |= 0x10;
213
+ }
214
+ if (weekdays.includes('Th')) {
215
+ nr |= 0x08;
216
+ }
217
+ if (weekdays.includes('Fr')) {
218
+ nr |= 0x04;
219
+ }
220
+ if (weekdays.includes('Sa')) {
221
+ nr |= 0x02;
222
+ }
223
+ if (weekdays.includes('Su')) {
224
+ nr |= 0x01;
225
+ }
226
+ return [nr];
227
+ }
228
+
229
+ function convertDecimalValueTo4ByteHexArray(value) {
230
+ const hexValue = Number(value).toString(16).padStart(8, '0');
231
+ const chunk1 = hexValue.substr(0, 2);
232
+ const chunk2 = hexValue.substr(2, 2);
233
+ const chunk3 = hexValue.substr(4, 2);
234
+ const chunk4 = hexValue.substr(6);
235
+ return [chunk1, chunk2, chunk3, chunk4].map((hexVal) => parseInt(hexVal, 16));
236
+ }
237
+
238
+ function convertDecimalValueTo2ByteHexArray(value) {
239
+ const hexValue = Number(value).toString(16).padStart(4, '0');
240
+ const chunk1 = hexValue.substr(0, 2);
241
+ const chunk2 = hexValue.substr(2);
242
+ return [chunk1, chunk2].map((hexVal) => parseInt(hexVal, 16));
243
+ }
244
+
245
+ async function onEventMeasurementPoll(type, data, device, options, electricalMeasurement=true, metering=false) {
246
+ const endpoint = device.getEndpoint(1);
247
+ if (type === 'stop') {
248
+ clearTimeout(globalStore.getValue(device, 'measurement_poll'));
249
+ globalStore.clearValue(device, 'measurement_poll');
250
+ } else if (!globalStore.hasValue(device, 'measurement_poll')) {
251
+ const seconds = options && options.measurement_poll_interval ? options.measurement_poll_interval : 60;
252
+ if (seconds === -1) return;
253
+ const setTimer = () => {
254
+ const timer = setTimeout(async () => {
255
+ try {
256
+ if (electricalMeasurement) {
257
+ await endpoint.read('haElectricalMeasurement', ['rmsVoltage', 'rmsCurrent', 'activePower']);
258
+ }
259
+ if (metering) {
260
+ await endpoint.read('seMetering', ['currentSummDelivered']);
261
+ }
262
+ } catch (error) {/* Do nothing*/}
263
+ setTimer();
264
+ }, seconds * 1000);
265
+ globalStore.putValue(device, 'measurement_poll', timer);
266
+ };
267
+ setTimer();
268
+ }
269
+ }
270
+
271
+ async function onEventSetTime(type, data, device) {
272
+ // FIXME: Need to join onEventSetTime/onEventSetLocalTime to one command
273
+
274
+ if (data.type === 'commandMcuSyncTime' && data.cluster === 'manuSpecificTuya') {
275
+ try {
276
+ const utcTime = Math.round(((new Date()).getTime() - constants.OneJanuary2000) / 1000);
277
+ const localTime = utcTime - (new Date()).getTimezoneOffset() * 60;
278
+ const endpoint = device.getEndpoint(1);
279
+
280
+ const payload = {
281
+ payloadSize: 8,
282
+ payload: [
283
+ ...convertDecimalValueTo4ByteHexArray(utcTime),
284
+ ...convertDecimalValueTo4ByteHexArray(localTime),
285
+ ],
286
+ };
287
+ await endpoint.command('manuSpecificTuya', 'mcuSyncTime', payload, {});
288
+ } catch (error) {
289
+ // endpoint.command can throw an error which needs to
290
+ // be caught or the zigbee-herdsman may crash
291
+ // Debug message is handled in the zigbee-herdsman
292
+ }
293
+ }
294
+ }
295
+
296
+ // set UTC and Local Time as total number of seconds from 00: 00: 00 on January 01, 1970
297
+ // force to update every device time every hour due to very poor clock
298
+ async function onEventSetLocalTime(type, data, device) {
299
+ // FIXME: What actually nextLocalTimeUpdate/forceTimeUpdate do?
300
+ // I did not find any timers or something else where it was used.
301
+ // Actually, there are two ways to set time on TuYa MCU devices:
302
+ // 1. Respond to the `commandMcuSyncTime` event
303
+ // 2. Just send `mcuSyncTime` anytime (by 1-hour timer or something else)
304
+
305
+ const nextLocalTimeUpdate = globalStore.getValue(device, 'nextLocalTimeUpdate');
306
+ const forceTimeUpdate = nextLocalTimeUpdate == null || nextLocalTimeUpdate < new Date().getTime();
307
+
308
+ if ((data.type === 'commandMcuSyncTime' && data.cluster === 'manuSpecificTuya') || forceTimeUpdate) {
309
+ globalStore.putValue(device, 'nextLocalTimeUpdate', new Date().getTime() + 3600 * 1000);
310
+
311
+ try {
312
+ const utcTime = Math.round(((new Date()).getTime()) / 1000);
313
+ const localTime = utcTime - (new Date()).getTimezoneOffset() * 60;
314
+ const endpoint = device.getEndpoint(1);
315
+
316
+ const payload = {
317
+ payloadSize: 8,
318
+ payload: [
319
+ ...convertDecimalValueTo4ByteHexArray(utcTime),
320
+ ...convertDecimalValueTo4ByteHexArray(localTime),
321
+ ],
322
+ };
323
+ await endpoint.command('manuSpecificTuya', 'mcuSyncTime', payload, {});
324
+ } catch (error) {
325
+ // endpoint.command can throw an error which needs to
326
+ // be caught or the zigbee-herdsman may crash
327
+ // Debug message is handled in the zigbee-herdsman
328
+ }
329
+ }
330
+ }
331
+
332
+ function convertStringToHexArray(value) {
333
+ const asciiKeys = [];
334
+ for (let i = 0; i < value.length; i ++) {
335
+ asciiKeys.push(value[i].charCodeAt(0));
336
+ }
337
+ return asciiKeys;
338
+ }
339
+
340
+ // Contains all covers which need their position inverted by default
341
+ // Default is 100 = open, 0 = closed; Devices listed here will use 0 = open, 100 = closed instead
342
+ // Use manufacturerName to identify device!
343
+ // Dont' invert _TZE200_cowvfni3: https://github.com/Koenkk/zigbee2mqtt/issues/6043
344
+ const coverPositionInvert = ['_TZE200_wmcdj3aq', '_TZE200_nogaemzt', '_TZE200_xuzcvlku', '_TZE200_xaabybja', '_TZE200_rmymn92d',
345
+ '_TZE200_gubdgai2', '_TZE200_r0jdjrvi'];
346
+
347
+ // Gets a boolean indicating whether the cover by this manufacturerName needs reversed positions
348
+ function isCoverInverted(manufacturerName) {
349
+ // Return true if cover is listed in coverPositionInvert
350
+ // Return false by default, not inverted
351
+ return coverPositionInvert.includes(manufacturerName);
352
+ }
353
+
354
+ const coverStateOverride = {
355
+ // Contains all covers which differentiate from the default enum states
356
+ // Use manufacturerName to identify device!
357
+ // https://github.com/Koenkk/zigbee2mqtt/issues/5596#issuecomment-759408189
358
+ '_TZE200_rddyvrci': {close: 1, open: 2, stop: 0},
359
+ '_TZE200_wmcdj3aq': {close: 0, open: 2, stop: 1},
360
+ '_TZE200_cowvfni3': {close: 0, open: 2, stop: 1},
361
+ '_TYST11_cowvfni3': {close: 0, open: 2, stop: 1},
362
+ };
363
+
364
+ // Gets an array containing which enums have to be used in order for the correct close/open/stop commands to be sent
365
+ function getCoverStateEnums(manufacturerName) {
366
+ if (manufacturerName in coverStateOverride) {
367
+ return coverStateOverride[manufacturerName];
368
+ } else {
369
+ return {close: 2, open: 0, stop: 1}; // defaults
370
+ }
371
+ }
372
+
373
+ const thermostatSystemModes = {
374
+ 0: 'off',
375
+ 1: 'auto',
376
+ 2: 'manual',
377
+ 3: 'comfort',
378
+ 4: 'eco',
379
+ 5: 'boost',
380
+ 6: 'complex',
381
+ };
382
+
383
+ const thermostatSystemModes2 = {
384
+ 0: 'auto',
385
+ 1: 'cool',
386
+ 2: 'heat',
387
+ 3: 'dry',
388
+ 4: 'fan',
389
+ };
390
+
391
+ const thermostatSystemModes3 = {
392
+ 0: 'auto',
393
+ 1: 'heat',
394
+ 2: 'off',
395
+ };
396
+
397
+ const thermostatSystemModes4 = {
398
+ 0: 'off',
399
+ 1: 'auto',
400
+ 2: 'heat',
401
+ };
402
+
403
+ const dataPoints = {
404
+ // Common data points
405
+ // Below data points are usually shared between devices
406
+ state: 1,
407
+ heatingSetpoint: 2,
408
+ coverPosition: 2,
409
+ dimmerLevel: 3,
410
+ dimmerMinLevel: 3,
411
+ localTemp: 3,
412
+ coverArrived: 3,
413
+ occupancy: 3,
414
+ mode: 4,
415
+ fanMode: 5,
416
+ dimmerMaxLevel: 5,
417
+ motorDirection: 5,
418
+ config: 5,
419
+ childLock: 7,
420
+ coverChange: 7,
421
+ runningState: 14,
422
+ valveDetection: 20,
423
+ battery: 21,
424
+ tempCalibration: 44,
425
+ // Data points above 100 are usually custom function data points
426
+ waterLeak: 101,
427
+ minTemp: 102,
428
+ maxTemp: 103,
429
+ windowDetection: 104,
430
+ boostTime: 105,
431
+ coverSpeed: 105,
432
+ forceMode: 106,
433
+ comfortTemp: 107,
434
+ ecoTemp: 108,
435
+ valvePos: 109,
436
+ batteryLow: 110,
437
+ weekFormat: 111,
438
+ scheduleWorkday: 112,
439
+ scheduleHoliday: 113,
440
+ awayTemp: 114,
441
+ windowOpen: 115,
442
+ autoLock: 116,
443
+ awayDays: 117,
444
+ // Manufacturer specific
445
+ // Earda
446
+ eardaDimmerLevel: 2,
447
+ // Siterwell Thermostat
448
+ siterwellWindowDetection: 18,
449
+ // Moes Thermostat
450
+ moesHold: 2,
451
+ moesScheduleEnable: 3,
452
+ moesHeatingSetpoint: 16,
453
+ moesMaxTempLimit: 18,
454
+ moesMaxTemp: 19,
455
+ moesDeadZoneTemp: 20,
456
+ moesLocalTemp: 24,
457
+ moesMinTempLimit: 26,
458
+ moesTempCalibration: 27,
459
+ moesValve: 36,
460
+ moesChildLock: 40,
461
+ moesSensor: 43,
462
+ moesSchedule: 101,
463
+ etopErrorStatus: 13,
464
+ // MoesS Thermostat
465
+ moesSsystemMode: 1,
466
+ moesSheatingSetpoint: 2,
467
+ moesSlocalTemp: 3,
468
+ moesSboostHeating: 4,
469
+ moesSboostHeatingCountdown: 5,
470
+ moesSreset: 7,
471
+ moesSwindowDetectionFunktion_A2: 8,
472
+ moesSwindowDetection: 9,
473
+ moesSchildLock: 13,
474
+ moesSbattery: 14,
475
+ moesSschedule: 101,
476
+ moesSvalvePosition: 104,
477
+ moesSboostHeatingCountdownTimeSet: 103,
478
+ moesScompensationTempSet: 105,
479
+ moesSecoMode: 106,
480
+ moesSecoModeTempSet: 107,
481
+ moesSmaxTempSet: 108,
482
+ moesSminTempSet: 109,
483
+ moesCoverCalibration: 3,
484
+ moesCoverBacklight: 7,
485
+ moesCoverMotorReversal: 8,
486
+ // Neo T&H
487
+ neoOccupancy: 101,
488
+ neoPowerType: 101,
489
+ neoMelody: 102,
490
+ neoDuration: 103,
491
+ neoTamper: 103,
492
+ neoAlarm: 104,
493
+ neoTemp: 105,
494
+ neoTempScale: 106,
495
+ neoHumidity: 106,
496
+ neoMinTemp: 107,
497
+ neoMaxTemp: 108,
498
+ neoMinHumidity: 109,
499
+ neoMaxHumidity: 110,
500
+ neoUnknown2: 112,
501
+ neoTempAlarm: 113,
502
+ neoTempHumidityAlarm: 113,
503
+ neoHumidityAlarm: 114,
504
+ neoUnknown3: 115,
505
+ neoVolume: 116,
506
+ // Neo AlarmOnly
507
+ neoAOBattPerc: 15,
508
+ neoAOMelody: 21,
509
+ neoAODuration: 7,
510
+ neoAOAlarm: 13,
511
+ neoAOVolume: 5,
512
+ // Saswell TRV
513
+ saswellHeating: 3,
514
+ saswellWindowDetection: 8,
515
+ saswellFrostDetection: 10,
516
+ saswellTempCalibration: 27,
517
+ saswellChildLock: 40,
518
+ saswellState: 101,
519
+ saswellLocalTemp: 102,
520
+ saswellHeatingSetpoint: 103,
521
+ saswellValvePos: 104,
522
+ saswellBatteryLow: 105,
523
+ saswellAwayMode: 106,
524
+ saswellScheduleMode: 107,
525
+ saswellScheduleEnable: 108,
526
+ saswellScheduleSet: 109,
527
+ saswellSetpointHistoryDay: 110,
528
+ saswellTimeSync: 111,
529
+ saswellSetpointHistoryWeek: 112,
530
+ saswellSetpointHistoryMonth: 113,
531
+ saswellSetpointHistoryYear: 114,
532
+ saswellLocalHistoryDay: 115,
533
+ saswellLocalHistoryWeek: 116,
534
+ saswellLocalHistoryMonth: 117,
535
+ saswellLocalHistoryYear: 118,
536
+ saswellMotorHistoryDay: 119,
537
+ saswellMotorHistoryWeek: 120,
538
+ saswellMotorHistoryMonth: 121,
539
+ saswellMotorHistoryYear: 122,
540
+ saswellScheduleSunday: 123,
541
+ saswellScheduleMonday: 124,
542
+ saswellScheduleTuesday: 125,
543
+ saswellScheduleWednesday: 126,
544
+ saswellScheduleThursday: 127,
545
+ saswellScheduleFriday: 128,
546
+ saswellScheduleSaturday: 129,
547
+ saswellAntiScaling: 130,
548
+ // HY thermostat
549
+ hyHeating: 102,
550
+ hyExternalTemp: 103,
551
+ hyAwayDays: 104,
552
+ hyAwayTemp: 105,
553
+ hyMaxTempProtection: 106,
554
+ hyMinTempProtection: 107,
555
+ hyTempCalibration: 109,
556
+ hyHysteresis: 110,
557
+ hyProtectionHysteresis: 111,
558
+ hyProtectionMaxTemp: 112,
559
+ hyProtectionMinTemp: 113,
560
+ hyMaxTemp: 114,
561
+ hyMinTemp: 115,
562
+ hySensor: 116,
563
+ hyPowerOnBehavior: 117,
564
+ hyWeekFormat: 118,
565
+ hyWorkdaySchedule1: 119,
566
+ hyWorkdaySchedule2: 120,
567
+ hyHolidaySchedule1: 121,
568
+ hyHolidaySchedule2: 122,
569
+ hyState: 125,
570
+ hyHeatingSetpoint: 126,
571
+ hyLocalTemp: 127,
572
+ hyMode: 128,
573
+ hyChildLock: 129,
574
+ hyAlarm: 130,
575
+ // Silvercrest
576
+ silvercrestChangeMode: 2,
577
+ silvercrestSetBrightness: 3,
578
+ silvercrestSetColorTemp: 4,
579
+ silvercrestSetColor: 5,
580
+ silvercrestSetEffect: 6,
581
+ // Fantem
582
+ fantemPowerSupplyMode: 101,
583
+ fantemReportingTime: 102,
584
+ fantemExtSwitchType: 103,
585
+ fantemTempCalibration: 104,
586
+ fantemHumidityCalibration: 105,
587
+ fantemLoadDetectionMode: 105,
588
+ fantemLuxCalibration: 106,
589
+ fantemExtSwitchStatus: 106,
590
+ fantemTemp: 107,
591
+ fantemHumidity: 108,
592
+ fantemMotionEnable: 109,
593
+ fantemControlMode: 109,
594
+ fantemBattery: 110,
595
+ fantemLedEnable: 111,
596
+ fantemReportingEnable: 112,
597
+ fantemLoadType: 112,
598
+ fantemLoadDimmable: 113,
599
+ // Woox
600
+ wooxSwitch: 102,
601
+ wooxBattery: 14,
602
+ wooxSmokeTest: 8,
603
+ // FrankEver
604
+ frankEverTimer: 9,
605
+ frankEverTreshold: 101,
606
+ // Dinrail power meter switch
607
+ dinrailPowerMeterTotalEnergy: 17,
608
+ dinrailPowerMeterCurrent: 18,
609
+ dinrailPowerMeterPower: 19,
610
+ dinrailPowerMeterVoltage: 20,
611
+ dinrailPowerMeterTotalEnergy2: 101,
612
+ dinrailPowerMeterPower2: 103,
613
+ // tuya smart air box
614
+ tuyaSabCO2: 2,
615
+ tuyaSabTemp: 18,
616
+ tuyaSabHumidity: 19,
617
+ tuyaSabVOC: 21,
618
+ tuyaSabFormaldehyd: 22,
619
+ // tuya Smart Air House Keeper, Multifunctionale air quality detector.
620
+ // CO2, Temp, Humidity, VOC and Formaldehyd same as Smart Air Box
621
+ tuyaSahkMP25: 2,
622
+ tuyaSahkCO2: 22,
623
+ tuyaSahkFormaldehyd: 20,
624
+ // Tuya CO (carbon monoxide) smart air box
625
+ tuyaSabCOalarm: 1,
626
+ tuyaSabCO: 2,
627
+ lidlTimer: 5,
628
+ // Moes MS-105 Dimmer
629
+ moes105DimmerState1: 1,
630
+ moes105DimmerLevel1: 2,
631
+ moes105DimmerState2: 7,
632
+ moes105DimmerLevel2: 8,
633
+ // TuYa Radar Sensor
634
+ trsPresenceState: 1,
635
+ trsSensitivity: 2,
636
+ trsMotionState: 102,
637
+ trsIlluminanceLux: 103,
638
+ trsDetectionData: 104,
639
+ trsScene: 112,
640
+ trsMotionDirection: 114,
641
+ trsMotionSpeed: 115,
642
+ // TuYa Radar Sensor with fall function
643
+ trsfPresenceState: 1,
644
+ trsfSensitivity: 2,
645
+ trsfMotionState: 102,
646
+ trsfIlluminanceLux: 103,
647
+ trsfTumbleSwitch: 105,
648
+ trsfTumbleAlarmTime: 106,
649
+ trsfScene: 112,
650
+ trsfMotionDirection: 114,
651
+ trsfMotionSpeed: 115,
652
+ trsfFallDownStatus: 116,
653
+ trsfStaticDwellAlarm: 117,
654
+ trsfFallSensitivity: 118,
655
+ // Human Presence Sensor AIR
656
+ msVSensitivity: 101,
657
+ msOSensitivity: 102,
658
+ msVacancyDelay: 103,
659
+ msMode: 104,
660
+ msVacantConfirmTime: 105,
661
+ msReferenceLuminance: 106,
662
+ msLightOnLuminancePrefer: 107,
663
+ msLightOffLuminancePrefer: 108,
664
+ msLuminanceLevel: 109,
665
+ msLedStatus: 110,
666
+ // TV01 Moes Thermostat
667
+ tvMode: 2,
668
+ tvWindowDetection: 8,
669
+ tvFrostDetection: 10,
670
+ tvHeatingSetpoint: 16,
671
+ tvLocalTemp: 24,
672
+ tvTempCalibration: 27,
673
+ tvWorkingDay: 31,
674
+ tvHolidayTemp: 32,
675
+ tvBattery: 35,
676
+ tvChildLock: 40,
677
+ tvErrorStatus: 45,
678
+ tvHolidayMode: 46,
679
+ tvBoostTime: 101,
680
+ tvOpenWindowTemp: 102,
681
+ tvComfortTemp: 104,
682
+ tvEcoTemp: 105,
683
+ tvWeekSchedule: 106,
684
+ tvHeatingStop: 107,
685
+ tvMondaySchedule: 108,
686
+ tvWednesdaySchedule: 109,
687
+ tvFridaySchedule: 110,
688
+ tvSundaySchedule: 111,
689
+ tvTuesdaySchedule: 112,
690
+ tvThursdaySchedule: 113,
691
+ tvSaturdaySchedule: 114,
692
+ tvBoostMode: 115,
693
+ // HOCH / WDYK DIN Rail
694
+ hochCountdownTimer: 9,
695
+ hochFaultCode: 26,
696
+ hochRelayStatus: 27,
697
+ hochChildLock: 29,
698
+ hochVoltage: 101,
699
+ hochCurrent: 102,
700
+ hochActivePower: 103,
701
+ hochLeakageCurrent: 104,
702
+ hochTemperature: 105,
703
+ hochRemainingEnergy: 106,
704
+ hochRechargeEnergy: 107,
705
+ hochCostParameters: 108,
706
+ hochLeakageParameters: 109,
707
+ hochVoltageThreshold: 110,
708
+ hochCurrentThreshold: 111,
709
+ hochTemperatureThreshold: 112,
710
+ hochTotalActivePower: 113,
711
+ hochEquipmentNumberType: 114,
712
+ hochClearEnergy: 115,
713
+ hochLocking: 116,
714
+ hochTotalReverseActivePower: 117,
715
+ hochHistoricalVoltage: 118,
716
+ hochHistoricalCurrent: 119,
717
+ // NOUS SMart LCD Temperature and Humidity Sensor E6
718
+ nousTemperature: 1,
719
+ nousHumidity: 2,
720
+ nousBattery: 4,
721
+ nousTempUnitConvert: 9,
722
+ nousMaxTemp: 10,
723
+ nousMinTemp: 11,
724
+ nousMaxHumi: 12,
725
+ nousMinHumi: 13,
726
+ nousTempAlarm: 14,
727
+ nousHumiAlarm: 15,
728
+ nousHumiSensitivity: 20,
729
+ nousTempSensitivity: 19,
730
+ nousTempReportInterval: 17,
731
+ nousHumiReportInterval: 18,
732
+ // TUYA Temperature and Humidity Sensor
733
+ tthTemperature: 1,
734
+ tthHumidity: 2,
735
+ tthBatteryLevel: 3,
736
+ tthBattery: 4,
737
+ // TUYA / HUMIDITY/ILLUMINANCE/TEMPERATURE SENSOR
738
+ thitBatteryPercentage: 3,
739
+ thitIlluminanceLux: 7,
740
+ tIlluminanceLux: 2,
741
+ thitHumidity: 9,
742
+ thitTemperature: 8,
743
+ // TUYA SMART VIBRATION SENSOR
744
+ tuyaVibration: 10,
745
+ // TUYA WLS-100z Water Leak Sensor
746
+ wlsWaterLeak: 1,
747
+ wlsBatteryPercentage: 4,
748
+ // Evanell
749
+ evanellMode: 2,
750
+ evanellHeatingSetpoint: 4,
751
+ evanellLocalTemp: 5,
752
+ evanellBattery: 6,
753
+ evanellChildLock: 8,
754
+ // ZMAM02 Zemismart RF Courtain Converter
755
+ AM02Control: 1,
756
+ AM02PercentControl: 2,
757
+ AM02PercentState: 3,
758
+ AM02Mode: 4,
759
+ AM02Direction: 5,
760
+ AM02WorkState: 7,
761
+ AM02CountdownLeft: 9,
762
+ AM02TimeTotal: 10,
763
+ AM02SituationSet: 11,
764
+ AM02Fault: 12,
765
+ AM02Border: 16,
766
+ AM02MotorWorkingMode: 20,
767
+ AM02AddRemoter: 101,
768
+ // Matsee Tuya Garage Door Opener
769
+ garageDoorTrigger: 1,
770
+ garageDoorContact: 3,
771
+ garageDoorStatus: 12,
772
+ // Moes switch with optional neutral
773
+ moesSwitchPowerOnBehavior: 14,
774
+ moesSwitchIndicateLight: 15,
775
+ // X5H thermostat
776
+ x5hState: 1,
777
+ x5hMode: 2,
778
+ x5hWorkingStatus: 3,
779
+ x5hSound: 7,
780
+ x5hFrostProtection: 10,
781
+ x5hSetTemp: 16,
782
+ x5hSetTempCeiling: 19,
783
+ x5hCurrentTemp: 24,
784
+ x5hTempCorrection: 27,
785
+ x5hWeeklyProcedure: 30,
786
+ x5hWorkingDaySetting: 31,
787
+ x5hFactoryReset: 39,
788
+ x5hChildLock: 40,
789
+ x5hSensorSelection: 43,
790
+ x5hFaultAlarm: 45,
791
+ x5hTempDiff: 101,
792
+ x5hProtectionTempLimit: 102,
793
+ x5hOutputReverse: 103,
794
+ x5hBackplaneBrightness: 104,
795
+ // Connecte thermostat
796
+ connecteState: 1,
797
+ connecteMode: 2,
798
+ connecteHeatingSetpoint: 16,
799
+ connecteLocalTemp: 24,
800
+ connecteTempCalibration: 28,
801
+ connecteChildLock: 30,
802
+ connecteTempFloor: 101,
803
+ connecteSensorType: 102,
804
+ connecteHysteresis: 103,
805
+ connecteRunningState: 104,
806
+ connecteTempProgram: 105,
807
+ connecteOpenWindow: 106,
808
+ connecteMaxProtectTemp: 107,
809
+ // TuYa Smart Human Presense Sensor
810
+ tshpsPresenceState: 1,
811
+ tshpscSensitivity: 2,
812
+ tshpsMinimumRange: 3,
813
+ tshpsMaximumRange: 4,
814
+ tshpsTargetDistance: 9,
815
+ tshpsDetectionDelay: 101,
816
+ tshpsFadingTime: 102,
817
+ tshpsIlluminanceLux: 104,
818
+ tshpsCLI: 103, // not recognize
819
+ tshpsSelfTest: 6, // not recognize
820
+ // TuYa Luminance Motion sensor
821
+ lmsState: 1,
822
+ lmsBattery: 4,
823
+ lmsSensitivity: 9,
824
+ lmsKeepTime: 10,
825
+ lmsIlluminance: 12,
826
+ // Alecto SMART-SMOKE10
827
+ alectoSmokeState: 1,
828
+ alectoSmokeValue: 2,
829
+ alectoSelfChecking: 8,
830
+ alectoCheckingResult: 9,
831
+ alectoSmokeTest: 11,
832
+ alectoLifecycle: 12,
833
+ alectoBatteryState: 14,
834
+ alectoBatteryPercentage: 15,
835
+ alectoSilence: 16,
836
+ // BAC-002-ALZB - Moes like thermostat with Fan control
837
+ bacFanMode: 28,
838
+ // Human Presence Sensor Zigbee Radiowave Tuya
839
+ HPSZInductionState: 1,
840
+ HPSZPresenceTime: 101,
841
+ HPSZLeavingTime: 102,
842
+ HPSZLEDState: 103,
843
+ };
844
+
845
+ const thermostatWeekFormat = {
846
+ 0: '5+2',
847
+ 1: '6+1',
848
+ 2: '7',
849
+ };
850
+
851
+ const thermostatForceMode = {
852
+ 0: 'normal',
853
+ 1: 'open',
854
+ 2: 'close',
855
+ };
856
+
857
+ const thermostatPresets = {
858
+ 0: 'away',
859
+ 1: 'schedule',
860
+ 2: 'manual',
861
+ 3: 'comfort',
862
+ 4: 'eco',
863
+ 5: 'boost',
864
+ 6: 'complex',
865
+ };
866
+
867
+ const thermostatScheduleMode = {
868
+ 1: 'single', // One schedule for all days
869
+ 2: 'weekday/weekend', // Weekdays(2-5) and Holidays(6-1)
870
+ 3: 'weekday/sat/sun', // Weekdays(2-6), Saturday(7), Sunday(1)
871
+ 4: '7day', // 7 day schedule
872
+ };
873
+
874
+ const silvercrestModes = {
875
+ white: 0,
876
+ color: 1,
877
+ effect: 2,
878
+ };
879
+
880
+ const silvercrestEffects = {
881
+ steady: '00',
882
+ snow: '01',
883
+ rainbow: '02',
884
+ snake: '03',
885
+ twinkle: '04',
886
+ firework: '08',
887
+ horizontal_flag: '06',
888
+ waves: '07',
889
+ updown: '08',
890
+ vintage: '09',
891
+ fading: '0a',
892
+ collide: '0b',
893
+ strobe: '0c',
894
+ sparkles: '0d',
895
+ carnaval: '0e',
896
+ glow: '0f',
897
+ };
898
+
899
+ const fanModes = {
900
+ 0: 'low',
901
+ 1: 'medium',
902
+ 2: 'high',
903
+ 3: 'auto',
904
+ };
905
+
906
+ // Radar sensor lookups
907
+ const tuyaRadar = {
908
+ radarScene: {
909
+ 0: 'default',
910
+ 1: 'area',
911
+ 2: 'toilet',
912
+ 3: 'bedroom',
913
+ 4: 'parlour',
914
+ 5: 'office',
915
+ 6: 'hotel',
916
+ },
917
+ motionDirection: {
918
+ 0: 'standing_still',
919
+ 1: 'moving_forward',
920
+ 2: 'moving_backward',
921
+ },
922
+ fallDown: {
923
+ 0: 'none',
924
+ 1: 'maybe_fall',
925
+ 2: 'fall',
926
+ },
927
+ };
928
+
929
+ // Motion sensor lookups
930
+ const msLookups = {
931
+ OSensitivity: {
932
+ 0: 'sensitive',
933
+ 1: 'normal',
934
+ 2: 'cautious',
935
+ },
936
+ VSensitivity: {
937
+ 0: 'speed_priority',
938
+ 1: 'normal_priority',
939
+ 2: 'accuracy_priority',
940
+ },
941
+ Mode: {
942
+ 0: 'general_model',
943
+ 1: 'temporaty_stay',
944
+ 2: 'basic_detection',
945
+ 3: 'sensor_test',
946
+ },
947
+ };
948
+
949
+ const tvThermostatMode = {
950
+ 0: 'off',
951
+ 1: 'heat',
952
+ 2: 'auto',
953
+ };
954
+
955
+
956
+ const tvThermostatPreset = {
957
+ 0: 'auto',
958
+ 1: 'manual',
959
+ 3: 'holiday',
960
+ };
961
+ // Zemismart ZM_AM02 Roller Shade Converter
962
+ const ZMLookups = {
963
+ AM02Mode: {
964
+ 0: 'morning',
965
+ 1: 'night',
966
+ },
967
+ AM02Control: {
968
+ 0: 'open',
969
+ 1: 'stop',
970
+ 2: 'close',
971
+ 3: 'continue',
972
+ },
973
+ AM02Direction: {
974
+ 0: 'forward',
975
+ 1: 'back',
976
+ },
977
+ AM02WorkState: {
978
+ 0: 'opening',
979
+ 1: 'closing',
980
+ },
981
+ AM02Border: {
982
+ 0: 'up',
983
+ 1: 'down',
984
+ 2: 'down_delete',
985
+ },
986
+ AM02Situation: {
987
+ 0: 'fully_open',
988
+ 1: 'fully_close',
989
+ },
990
+ AM02MotorWorkingMode: {
991
+ 0: 'continuous',
992
+ 1: 'intermittently',
993
+ },
994
+ };
995
+
996
+ const moesSwitch = {
997
+ powerOnBehavior: {
998
+ 0: 'off',
999
+ 1: 'on',
1000
+ 2: 'previous',
1001
+ },
1002
+ indicateLight: {
1003
+ 0: 'off',
1004
+ 1: 'switch',
1005
+ 2: 'position',
1006
+ 3: 'freeze',
1007
+ },
1008
+ };
1009
+ const tuyaHPSCheckingResult = {
1010
+ 0: 'checking',
1011
+ 1: 'check_success',
1012
+ 2: 'check_failure',
1013
+ 3: 'others',
1014
+ 4: 'comm_fault',
1015
+ 5: 'radar_fault',
1016
+ };
1017
+
1018
+ // Return `seq` - transaction ID for handling concrete response
1019
+ async function sendDataPoints(entity, dpValues, cmd, seq=undefined) {
1020
+ if (seq === undefined) {
1021
+ if (sendDataPoints.seq === undefined) {
1022
+ sendDataPoints.seq = 0;
1023
+ } else {
1024
+ sendDataPoints.seq++;
1025
+ sendDataPoints.seq %= 0xFFFF;
1026
+ }
1027
+ seq = sendDataPoints.seq;
1028
+ }
1029
+
1030
+ await entity.command(
1031
+ 'manuSpecificTuya',
1032
+ cmd || 'dataRequest',
1033
+ {
1034
+ seq,
1035
+ dpValues,
1036
+ },
1037
+ {disableDefaultResponse: true},
1038
+ );
1039
+ return seq;
1040
+ }
1041
+
1042
+ function dpValueFromIntValue(dp, value) {
1043
+ return {dp, datatype: dataTypes.value, data: convertDecimalValueTo4ByteHexArray(value)};
1044
+ }
1045
+
1046
+ function dpValueFromBool(dp, value) {
1047
+ return {dp, datatype: dataTypes.bool, data: [value ? 1 : 0]};
1048
+ }
1049
+
1050
+ function dpValueFromEnum(dp, value) {
1051
+ return {dp, datatype: dataTypes.enum, data: [value]};
1052
+ }
1053
+
1054
+ function dpValueFromStringBuffer(dp, stringBuffer) {
1055
+ return {dp, datatype: dataTypes.string, data: stringBuffer};
1056
+ }
1057
+
1058
+ function dpValueFromRaw(dp, rawBuffer) {
1059
+ return {dp, datatype: dataTypes.raw, data: rawBuffer};
1060
+ }
1061
+
1062
+ function dpValueFromBitmap(dp, bitmapBuffer) {
1063
+ return {dp, datatype: dataTypes.bitmap, data: bitmapBuffer};
1064
+ }
1065
+
1066
+ // Return `seq` - transaction ID for handling concrete response
1067
+ async function sendDataPoint(entity, dpValue, cmd, seq=undefined) {
1068
+ return await sendDataPoints(entity, [dpValue], cmd, seq);
1069
+ }
1070
+
1071
+ async function sendDataPointValue(entity, dp, value, cmd, seq=undefined) {
1072
+ return await sendDataPoints(entity, [dpValueFromIntValue(dp, value)], cmd, seq);
1073
+ }
1074
+
1075
+ async function sendDataPointBool(entity, dp, value, cmd, seq=undefined) {
1076
+ return await sendDataPoints(entity, [dpValueFromBool(dp, value)], cmd, seq);
1077
+ }
1078
+
1079
+ async function sendDataPointEnum(entity, dp, value, cmd, seq=undefined) {
1080
+ return await sendDataPoints(entity, [dpValueFromEnum(dp, value)], cmd, seq);
1081
+ }
1082
+
1083
+ async function sendDataPointRaw(entity, dp, value, cmd, seq=undefined) {
1084
+ return await sendDataPoints(entity, [dpValueFromRaw(dp, value)], cmd, seq);
1085
+ }
1086
+
1087
+ async function sendDataPointBitmap(entity, dp, value, cmd, seq=undefined) {
1088
+ return await sendDataPoints(entity, [dpValueFromBitmap(dp, value)], cmd, seq);
1089
+ }
1090
+
1091
+ async function sendDataPointStringBuffer(entity, dp, value, cmd, seq=undefined) {
1092
+ return await sendDataPoints(entity, [dpValueFromStringBuffer(dp, value)], cmd, seq);
1093
+ }
1094
+
1095
+ const tuyaExposes = {
1096
+ lightType: () => exposes.enum('light_type', ea.STATE_SET, ['led', 'incandescent', 'halogen'])
1097
+ .withDescription('Type of light attached to the device'),
1098
+ lightBrightnessWithMinMax: () => e.light_brightness().withMinBrightness().withMaxBrightness()
1099
+ .setAccess('state', ea.STATE_SET)
1100
+ .setAccess('brightness', ea.STATE_SET)
1101
+ .setAccess('min_brightness', ea.STATE_SET)
1102
+ .setAccess('max_brightness', ea.STATE_SET),
1103
+ lightBrightness: () => e.light_brightness()
1104
+ .setAccess('state', ea.STATE_SET)
1105
+ .setAccess('brightness', ea.STATE_SET),
1106
+ countdown: () => exposes.numeric('countdown', ea.STATE_SET).withValueMin(0).withValueMax(43200).withValueStep(1).withUnit('s')
1107
+ .withDescription('Countdown to turn device off after a certain time'),
1108
+ switch: () => e.switch().setAccess('state', ea.STATE_SET),
1109
+ selfTest: () => exposes.binary('self_test', ea.STATE_SET, true, false)
1110
+ .withDescription('Indicates whether the device is being self-tested'),
1111
+ selfTestResult: () => exposes.enum('self_test_result', ea.STATE, ['checking', 'success', 'failure', 'others'])
1112
+ .withDescription('Result of the self-test'),
1113
+ faultAlarm: () => exposes.binary('fault_alarm', ea.STATE, true, false).withDescription('Indicates whether a fault was detected'),
1114
+ silence: () => exposes.binary('silence', ea.STATE_SET, true, false).withDescription('Silence the alarm'),
1115
+ frostProtection: (extraNote='') => exposes.binary('frost_protection', ea.STATE_SET, 'ON', 'OFF').withDescription(
1116
+ `When Anti-Freezing function is activated, the temperature in the house is kept at 8 °C.${extraNote}`),
1117
+ errorStatus: () => exposes.numeric('error_status', ea.STATE).withDescription('Error status'),
1118
+ scheduleAllDays: (access, format) => ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']
1119
+ .map((day) => exposes.text(`schedule_${day}`, access).withDescription(`Schedule for ${day}, format: "${format}"`)),
1120
+ temperatureUnit: () => exposes.enum('temperature_unit', ea.STATE_SET, ['celsius', 'fahrenheit']).withDescription('Temperature unit'),
1121
+ temperatureCalibration: () => exposes.numeric('temperature_calibration', ea.STATE_SET).withValueMin(-2.0).withValueMax(2.0)
1122
+ .withValueStep(0.1).withUnit('°C').withDescription('Temperature calibration'),
1123
+ humidityCalibration: () => exposes.numeric('humidity_calibration', ea.STATE_SET).withValueMin(-30).withValueMax(30)
1124
+ .withValueStep(1).withUnit('%').withDescription('Humidity calibration'),
1125
+ gasValue: () => exposes.numeric('gas_value', ea.STATE).withDescription('Measured gas concentration'),
1126
+ energyWithPhase: (phase) => exposes.numeric(`energy_${phase}`, ea.STATE).withUnit('kWh')
1127
+ .withDescription(`Sum of consumed energy (phase ${phase.toUpperCase()})`),
1128
+ voltageWithPhase: (phase) => exposes.numeric(`voltage_${phase}`, ea.STATE).withUnit('V')
1129
+ .withDescription(`Measured electrical potential value (phase ${phase.toUpperCase()})`),
1130
+ powerWithPhase: (phase) => exposes.numeric(`power_${phase}`, ea.STATE).withUnit('W')
1131
+ .withDescription(`Instantaneous measured power (phase ${phase.toUpperCase()})`),
1132
+ currentWithPhase: (phase) => exposes.numeric(`current_${phase}`, ea.STATE).withUnit('A')
1133
+ .withDescription(`Instantaneous measured electrical current (phase ${phase.toUpperCase()})`),
1134
+ powerFactorWithPhase: (phase) => exposes.numeric(`power_factor_${phase}`, ea.STATE).withUnit('%')
1135
+ .withDescription(`Instantaneous measured power factor (phase ${phase.toUpperCase()})`),
1136
+ switchType: () => exposes.enum('switch_type', ea.ALL, ['toggle', 'state', 'momentary']).withDescription('Type of the switch'),
1137
+ backlightModeLowMediumHigh: () => exposes.enum('backlight_mode', ea.ALL, ['low', 'medium', 'high'])
1138
+ .withDescription('Intensity of the backlight'),
1139
+ backlightModeOffNormalInverted: () => exposes.enum('backlight_mode', ea.ALL, ['off', 'normal', 'inverted'])
1140
+ .withDescription('Mode of the backlight'),
1141
+ backlightModeOffOn: () => exposes.binary('backlight_mode', ea.ALL, 'ON', 'OFF').withDescription(`Mode of the backlight`),
1142
+ indicatorMode: () => exposes.enum('indicator_mode', ea.ALL, ['off', 'off/on', 'on/off', 'on']).withDescription('LED indicator mode'),
1143
+ indicatorModeNoneRelayPos: () => exposes.enum('indicator_mode', ea.ALL, ['none', 'relay', 'pos'])
1144
+ .withDescription('Mode of the indicator light'),
1145
+ powerOutageMemory: () => exposes.enum('power_outage_memory', ea.ALL, ['on', 'off', 'restore'])
1146
+ .withDescription('Recover state after power outage'),
1147
+ batteryState: () => exposes.enum('battery_state', ea.STATE, ['low', 'medium', 'high']).withDescription('State of the battery'),
1148
+ doNotDisturb: () => exposes.binary('do_not_disturb', ea.STATE_SET, true, false)
1149
+ .withDescription('Do not disturb mode, when enabled this function will keep the light OFF after a power outage'),
1150
+ colorPowerOnBehavior: () => exposes.enum('color_power_on_behavior', ea.STATE_SET, ['initial', 'previous', 'cutomized'])
1151
+ .withDescription('Power on behavior state'),
1152
+ };
1153
+
1154
+ const skip = {
1155
+ // Prevent state from being published when already ON and brightness is also published.
1156
+ // This prevents 100% -> X% brightness jumps when the switch is already on
1157
+ // https://github.com/Koenkk/zigbee2mqtt/issues/13800#issuecomment-1263592783
1158
+ stateOnAndBrightnessPresent: (meta) => meta.message.hasOwnProperty('brightness') && meta.state.state === 'ON',
1159
+ };
1160
+
1161
+ const configureMagicPacket = async (device, coordinatorEndpoint, logger) => {
1162
+ try {
1163
+ const endpoint = device.endpoints[0];
1164
+ await endpoint.read('genBasic', ['manufacturerName', 'zclVersion', 'appVersion', 'modelId', 'powerSource', 0xfffe]);
1165
+ } catch (e) {
1166
+ // Fails for some TuYa devices with UNSUPPORTED_ATTRIBUTE, ignore that.
1167
+ // e.g. https://github.com/Koenkk/zigbee2mqtt/issues/14857
1168
+ if (e.message.includes('UNSUPPORTED_ATTRIBUTE')) {
1169
+ logger.debug('TuYa configureMagicPacket failed, ignoring...');
1170
+ } else {
1171
+ throw e;
1172
+ }
1173
+ }
1174
+ };
1175
+
1176
+ const fingerprint = (modelID, manufacturerNames) => {
1177
+ return manufacturerNames.map((manufacturerName) => {
1178
+ return {modelID, manufacturerName};
1179
+ });
1180
+ };
1181
+
1182
+ class Base {
1183
+ constructor(value) {
1184
+ this.value = value;
1185
+ }
1186
+
1187
+ valueOf() {
1188
+ return this.value;
1189
+ }
1190
+ }
1191
+
1192
+ class Enum extends Base {
1193
+ constructor(value) {
1194
+ super(value);
1195
+ }
1196
+ }
1197
+
1198
+ class Bitmap extends Base {
1199
+ constructor(value) {
1200
+ super(value);
1201
+ }
1202
+ }
1203
+
1204
+ const valueConverterBasic = {
1205
+ lookup: (map) => {
1206
+ return {
1207
+ to: (v) => {
1208
+ if (map[v] === undefined) throw new Error(`Value '${v}' is not allowed, expected one of ${Object.keys(map)}`);
1209
+ return map[v];
1210
+ },
1211
+ from: (v) => {
1212
+ const value = Object.entries(map).find((i) => i[1].valueOf() === v);
1213
+ if (!value) throw new Error(`Value '${v}' is not allowed, expected one of ${Object.values(map)}`);
1214
+ return value[0];
1215
+ },
1216
+ };
1217
+ },
1218
+ scale: (min1, max1, min2, max2) => {
1219
+ return {to: (v) => utils.mapNumberRange(v, min1, max1, min2, max2), from: (v) => utils.mapNumberRange(v, min2, max2, min1, max1)};
1220
+ },
1221
+ raw: () => {
1222
+ return {to: (v) => v, from: (v) => v};
1223
+ },
1224
+ divideBy: (value) => {
1225
+ return {to: (v) => v * value, from: (v) => v / value};
1226
+ },
1227
+ trueFalse: (valueTrue) => {
1228
+ return {from: (v) => v === valueTrue};
1229
+ },
1230
+ };
1231
+
1232
+ const valueConverter = {
1233
+ trueFalse0: valueConverterBasic.trueFalse(0),
1234
+ trueFalse1: valueConverterBasic.trueFalse(1),
1235
+ trueFalseEnum0: valueConverterBasic.trueFalse(new Enum(0)),
1236
+ onOff: valueConverterBasic.lookup({'ON': true, 'OFF': false}),
1237
+ powerOnBehavior: valueConverterBasic.lookup({'off': 0, 'on': 1, 'previous': 2}),
1238
+ lightType: valueConverterBasic.lookup({'led': 0, 'incandescent': 1, 'halogen': 2}),
1239
+ countdown: valueConverterBasic.raw(),
1240
+ scale0_254to0_1000: valueConverterBasic.scale(0, 254, 0, 1000),
1241
+ scale0_1to0_1000: valueConverterBasic.scale(0, 1, 0, 1000),
1242
+ divideBy100: valueConverterBasic.divideBy(100),
1243
+ temperatureUnit: valueConverterBasic.lookup({'celsius': 0, 'fahrenheit': 1}),
1244
+ batteryState: valueConverterBasic.lookup({'low': 0, 'medium': 1, 'high': 2}),
1245
+ divideBy10: valueConverterBasic.divideBy(10),
1246
+ divideBy1000: valueConverterBasic.divideBy(1000),
1247
+ raw: valueConverterBasic.raw(),
1248
+ coverPosition: {
1249
+ to: async (v, meta) => {
1250
+ return meta.options.invert_cover ? 100 - v : v;
1251
+ },
1252
+ from: (v, meta, options) => {
1253
+ return options.invert_cover ? 100 - v : v;
1254
+ },
1255
+ },
1256
+ plus1: {
1257
+ from: (v) => v + 1,
1258
+ to: (v) => v - 1,
1259
+ },
1260
+ static: (value) => {
1261
+ return {
1262
+ from: (v) => {
1263
+ return value;
1264
+ },
1265
+ };
1266
+ },
1267
+ phaseVariant1: {
1268
+ from: (v) => {
1269
+ const buffer = Buffer.from(v, 'base64');
1270
+ return {voltage: (buffer[14] | buffer[13] << 8) / 10, current: (buffer[12] | buffer[11] << 8) / 1000};
1271
+ },
1272
+ },
1273
+ phaseVariant2: {
1274
+ from: (v) => {
1275
+ const buf = Buffer.from(v, 'base64');
1276
+ return {voltage: (buf[1] | buf[0] << 8) / 10, current: (buf[4] | buf[3] << 8) / 1000, power: (buf[7] | buf[6] << 8)};
1277
+ },
1278
+ },
1279
+ phaseVariant2WithPhase: (phase) => {
1280
+ return {
1281
+ from: (v) => {
1282
+ const buf = Buffer.from(v, 'base64');
1283
+ return {
1284
+ [`voltage_${phase}`]: (buf[1] | buf[0] << 8) / 10,
1285
+ [`current_${phase}`]: (buf[4] | buf[3] << 8) / 1000,
1286
+ [`power_${phase}`]: (buf[7] | buf[6] << 8)};
1287
+ },
1288
+ };
1289
+ },
1290
+ threshold: {
1291
+ from: (v) => {
1292
+ const buffer = Buffer.from(v, 'base64');
1293
+ const stateLookup = {0: 'not_set', 1: 'over_current_threshold', 3: 'over_voltage_threshold'};
1294
+ const protectionLookup = {0: 'OFF', 1: 'ON'};
1295
+ return {
1296
+ threshold_1_protection: protectionLookup[buffer[1]],
1297
+ threshold_1: stateLookup[buffer[0]],
1298
+ threshold_1_value: (buffer[3] | buffer[2] << 8),
1299
+ threshold_2_protection: protectionLookup[buffer[5]],
1300
+ threshold_2: stateLookup[buffer[4]],
1301
+ threshold_2_value: (buffer[7] | buffer[6] << 8),
1302
+ };
1303
+ },
1304
+ },
1305
+ selfTestResult: valueConverterBasic.lookup({'checking': 0, 'success': 1, 'failure': 2, 'others': 3}),
1306
+ lockUnlock: valueConverterBasic.lookup({'LOCK': true, 'UNLOCK': false}),
1307
+ localTempCalibration1: {
1308
+ from: (v) => {
1309
+ if (v > 55) v -= 0x100000000;
1310
+ return v / 10;
1311
+ },
1312
+ to: (v) => {
1313
+ if (v > 0) return v * 10;
1314
+ if (v < 0) return v * 10 + 0x100000000;
1315
+ return v;
1316
+ },
1317
+ },
1318
+ localTempCalibration2: {
1319
+ from: (v) => v,
1320
+ to: (v) => {
1321
+ if (v < 0) return v + 0x100000000;
1322
+ return v;
1323
+ },
1324
+ },
1325
+ thermostatHolidayStartStop: {
1326
+ from: (v) => {
1327
+ const start = {
1328
+ year: v.slice(0, 4), month: v.slice(4, 6), day: v.slice(6, 8),
1329
+ hours: v.slice(8, 10), minutes: v.slice(10, 12),
1330
+ };
1331
+ const end = {
1332
+ year: v.slice(12, 16), month: v.slice(16, 18), day: v.slice(18, 20),
1333
+ hours: v.slice(20, 22), minutes: v.slice(22, 24),
1334
+ };
1335
+ const startStr = `${start.year}/${start.month}/${start.day} ${start.hours}:${start.minutes}`;
1336
+ const endStr = `${end.year}/${end.month}/${end.day} ${end.hours}:${end.minutes}`;
1337
+ return `${startStr} | ${endStr}`;
1338
+ },
1339
+ to: (v) => {
1340
+ const numberPattern = /\d+/g;
1341
+ return v.match(numberPattern).join([]).toString();
1342
+ },
1343
+ },
1344
+ thermostatScheduleDaySingleDP: {
1345
+ from: (v) => {
1346
+ // day splitted to 10 min segments = total 144 segments
1347
+ const maxPeriodsInDay = 10;
1348
+ const periodSize = 3;
1349
+ const schedule = [];
1350
+
1351
+ for (let i = 0; i < maxPeriodsInDay; i++) {
1352
+ const time = v[i * periodSize];
1353
+ const totalMinutes = time * 10;
1354
+ const hours = totalMinutes / 60;
1355
+ const rHours = Math.floor(hours);
1356
+ const minutes = (hours - rHours) * 60;
1357
+ const rMinutes = Math.round(minutes);
1358
+ const strHours = rHours.toString().padStart(2, '0');
1359
+ const strMinutes = rMinutes.toString().padStart(2, '0');
1360
+ const tempHexArray = [v[i * periodSize + 1], v[i * periodSize + 2]];
1361
+ const tempRaw = Buffer.from(tempHexArray).readUIntBE(0, tempHexArray.length);
1362
+ const temp = tempRaw / 10;
1363
+ schedule.push(`${strHours}:${strMinutes}/${temp}`);
1364
+ if (rHours === 24) break;
1365
+ }
1366
+
1367
+ return schedule.join(' ');
1368
+ },
1369
+ to: (v, meta) => {
1370
+ const dayByte = {
1371
+ monday: 1, tuesday: 2, wednesday: 4, thursday: 8,
1372
+ friday: 16, saturday: 32, sunday: 64,
1373
+ };
1374
+ const weekDay = v.week_day;
1375
+ if (Object.keys(dayByte).indexOf(weekDay) === -1) {
1376
+ throw new Error('Invalid "week_day" property value: ' + weekDay);
1377
+ }
1378
+ let weekScheduleType;
1379
+ if (meta.state && meta.state.working_day) weekScheduleType = meta.state.working_day;
1380
+ const payload = [];
1381
+
1382
+ switch (weekScheduleType) {
1383
+ case 'mon_sun':
1384
+ payload.push(127);
1385
+ break;
1386
+ case 'mon_fri+sat+sun':
1387
+ if (['saturday', 'sunday'].indexOf(weekDay) === -1) {
1388
+ payload.push(31);
1389
+ break;
1390
+ }
1391
+ payload.push(dayByte[weekDay]);
1392
+ break;
1393
+ case 'separate':
1394
+ payload.push(dayByte[weekDay]);
1395
+ break;
1396
+ default:
1397
+ throw new Error('Invalid "working_day" property, need to set it before');
1398
+ }
1399
+
1400
+ // day splitted to 10 min segments = total 144 segments
1401
+ const maxPeriodsInDay = 10;
1402
+ const schedule = v.schedule.split(' ');
1403
+ const schedulePeriods = schedule.length;
1404
+ if (schedulePeriods > 10) throw new Error('There cannot be more than 10 periods in the schedule: ' + v);
1405
+ if (schedulePeriods < 2) throw new Error('There cannot be less than 2 periods in the schedule: ' + v);
1406
+ let prevHour;
1407
+
1408
+ for (const period of schedule) {
1409
+ const timeTemp = period.split('/');
1410
+ const hm = timeTemp[0].split(':', 2);
1411
+ const h = parseInt(hm[0]);
1412
+ const m = parseInt(hm[1]);
1413
+ const temp = parseFloat(timeTemp[1]);
1414
+ if (h < 0 || h > 24 || m < 0 || m >= 60 || m % 10 !== 0 || temp < 5 || temp > 30 || temp % 0.5 !== 0) {
1415
+ throw new Error('Invalid hour, minute or temperature of: ' + period);
1416
+ } else if (prevHour > h) {
1417
+ throw new Error(`The hour of the next segment can't be less than the previous one: ${prevHour} > ${h}`);
1418
+ }
1419
+ prevHour = h;
1420
+ const segment = (h * 60 + m) / 10;
1421
+ const tempHexArray = convertDecimalValueTo2ByteHexArray(temp * 10);
1422
+ payload.push(segment, ...tempHexArray);
1423
+ }
1424
+
1425
+ // Add "technical" periods to be valid payload
1426
+ for (let i = 0; i < maxPeriodsInDay - schedulePeriods; i++) {
1427
+ // by default it sends 9000b2, it's 24 hours and 18 degrees
1428
+ payload.push(144, 0, 180);
1429
+ }
1430
+
1431
+ return payload;
1432
+ },
1433
+ },
1434
+ thermostatScheduleDayMultiDP: {
1435
+ from: (v) => {
1436
+ const schedule = [];
1437
+ for (let index = 1; index < 17; index = index + 4) {
1438
+ schedule.push(
1439
+ String(parseInt(v[index+0])).padStart(2, '0') + ':' +
1440
+ String(parseInt(v[index+1])).padStart(2, '0') + '/' +
1441
+ (parseFloat((v[index+2] << 8) + v[index+3]) / 10.0).toFixed(1),
1442
+ );
1443
+ }
1444
+ return schedule.join(' ');
1445
+ },
1446
+ to: (v) => {
1447
+ const payload = [0];
1448
+ const transitions = v.split(' ');
1449
+ if (transitions.length != 4) {
1450
+ throw new Error('Invalid schedule: there should be 4 transitions');
1451
+ }
1452
+ for (const transition of transitions) {
1453
+ const timeTemp = transition.split('/');
1454
+ if (timeTemp.length != 2) {
1455
+ throw new Error('Invalid schedule: wrong transition format: ' + transition);
1456
+ }
1457
+ const hourMin = timeTemp[0].split(':');
1458
+ const hour = hourMin[0];
1459
+ const min = hourMin[1];
1460
+ const temperature = Math.floor(timeTemp[1] *10);
1461
+ if (hour < 0 || hour > 24 || min < 0 || min > 60 || temperature < 50 || temperature > 300) {
1462
+ throw new Error('Invalid hour, minute or temperature of: ' + transition);
1463
+ }
1464
+ payload.push(
1465
+ hour,
1466
+ min,
1467
+ (temperature & 0xff00) >> 8,
1468
+ temperature & 0xff,
1469
+ );
1470
+ }
1471
+ return payload;
1472
+ },
1473
+ },
1474
+ thermostatScheduleDayMultiDPWithDayNumber: (dayNum) => {
1475
+ return {
1476
+ from: (v) => valueConverter.thermostatScheduleDayMultiDP.from(v),
1477
+ to: (v) => {
1478
+ const data = valueConverter.thermostatScheduleDayMultiDP.to(v);
1479
+ data[0] = dayNum;
1480
+ return data;
1481
+ },
1482
+ };
1483
+ },
1484
+ TV02SystemMode: {
1485
+ to: async (v, meta) => {
1486
+ const entity = meta.device.endpoints[0];
1487
+ if (meta.message.system_mode) {
1488
+ if (meta.message.system_mode === 'off') {
1489
+ await sendDataPointBool(entity, 107, true, 'dataRequest', 1);
1490
+ } else {
1491
+ await sendDataPointEnum(entity, 2, 1, 'dataRequest', 1); // manual
1492
+ }
1493
+ } else if (meta.message.heating_stop) {
1494
+ if (meta.message.heating_stop === 'ON') {
1495
+ await sendDataPointBool(entity, 107, true, 'dataRequest', 1);
1496
+ } else {
1497
+ await sendDataPointEnum(entity, 2, 1, 'dataRequest', 1); // manual
1498
+ }
1499
+ }
1500
+ },
1501
+ from: (v) => {
1502
+ return {system_mode: v === false ? 'heat' : 'off', heating_stop: v === false ? 'OFF' : 'ON'};
1503
+ },
1504
+ },
1505
+ TV02FrostProtection: {
1506
+ to: async (v, meta) => {
1507
+ const entity = meta.device.endpoints[0];
1508
+ if (v === 'ON') {
1509
+ await sendDataPointBool(entity, 10, true, 'dataRequest', 1);
1510
+ } else {
1511
+ await sendDataPointEnum(entity, 2, 1, 'dataRequest', 1); // manual
1512
+ }
1513
+ },
1514
+ from: (v) => {
1515
+ return {frost_protection: v === false ? 'OFF' : 'ON'};
1516
+ },
1517
+ },
1518
+ inverse: {to: (v) => !v, from: (v) => !v},
1519
+ onOffNotStrict: {from: (v) => v ? 'ON' : 'OFF', to: (v) => v === 'ON'},
1520
+ errorOrBatteryLow: {
1521
+ from: (v) => {
1522
+ if (v === 0) return {'battery_low': false};
1523
+ if (v === 1) return {'battery_low': true};
1524
+ return {'error': v};
1525
+ },
1526
+ },
1527
+ };
1528
+
1529
+ const tuyaTz = {
1530
+ power_on_behavior_1: {
1531
+ key: ['power_on_behavior', 'power_outage_memory'],
1532
+ convertSet: async (entity, key, value, meta) => {
1533
+ // Legacy: remove power_outage_memory
1534
+ const lookup = key === 'power_on_behavior' ? {'off': 0, 'on': 1, 'previous': 2} : {'off': 0x00, 'on': 0x01, 'restore': 0x02};
1535
+ value = value.toLowerCase();
1536
+ utils.validateValue(value, Object.keys(lookup));
1537
+ const pState = lookup[value];
1538
+ await entity.write('genOnOff', {moesStartUpOnOff: pState});
1539
+ return {state: {[key]: value}};
1540
+ },
1541
+ convertGet: async (entity, key, meta) => {
1542
+ await entity.read('genOnOff', ['moesStartUpOnOff']);
1543
+ },
1544
+ },
1545
+ power_on_behavior_2: {
1546
+ key: ['power_on_behavior'],
1547
+ convertSet: async (entity, key, value, meta) => {
1548
+ value = value.toLowerCase();
1549
+ const lookup = {'off': 0, 'on': 1, 'previous': 2};
1550
+ utils.validateValue(value, Object.keys(lookup));
1551
+ const pState = lookup[value];
1552
+ await entity.write('manuSpecificTuya_3', {'powerOnBehavior': pState});
1553
+ return {state: {power_on_behavior: value}};
1554
+ },
1555
+ convertGet: async (entity, key, meta) => {
1556
+ await entity.read('manuSpecificTuya_3', ['powerOnBehavior']);
1557
+ },
1558
+ },
1559
+ switch_type: {
1560
+ key: ['switch_type'],
1561
+ convertSet: async (entity, key, value, meta) => {
1562
+ value = value.toLowerCase();
1563
+ const lookup = {'toggle': 0, 'state': 1, 'momentary': 2};
1564
+ utils.validateValue(value, Object.keys(lookup));
1565
+ await entity.write('manuSpecificTuya_3', {'switchType': lookup[value]}, {disableDefaultResponse: true});
1566
+ return {state: {switch_type: value}};
1567
+ },
1568
+ convertGet: async (entity, key, meta) => {
1569
+ await entity.read('manuSpecificTuya_3', ['switchType']);
1570
+ },
1571
+ },
1572
+ backlight_indicator_mode_1: {
1573
+ key: ['backlight_mode', 'indicator_mode'],
1574
+ convertSet: async (entity, key, value, meta) => {
1575
+ const lookup = key === 'backlight_mode' ? {'low': 0, 'medium': 1, 'high': 2, 'off': 0, 'normal': 1, 'inverted': 2} :
1576
+ {'off': 0, 'off/on': 1, 'on/off': 2, 'on': 3};
1577
+ value = value.toLowerCase();
1578
+ utils.validateValue(value, Object.keys(lookup));
1579
+ await entity.write('genOnOff', {tuyaBacklightMode: lookup[value]});
1580
+ return {state: {[key]: value}};
1581
+ },
1582
+ convertGet: async (entity, key, meta) => {
1583
+ await entity.read('genOnOff', ['tuyaBacklightMode']);
1584
+ },
1585
+ },
1586
+ backlight_indicator_mode_2: {
1587
+ key: ['backlight_mode'],
1588
+ convertSet: async (entity, key, value, meta) => {
1589
+ const lookup = {'off': 0, 'on': 1};
1590
+ value = value.toLowerCase();
1591
+ utils.validateValue(value, Object.keys(lookup));
1592
+ await entity.write('genOnOff', {tuyaBacklightSwitch: lookup[value]});
1593
+ return {state: {[key]: value}};
1594
+ },
1595
+ convertGet: async (entity, key, meta) => {
1596
+ await entity.read('genOnOff', ['tuyaBacklightSwitch']);
1597
+ },
1598
+ },
1599
+ child_lock: {
1600
+ key: ['child_lock'],
1601
+ convertSet: async (entity, key, value, meta) => {
1602
+ await entity.write('genOnOff', {0x8000: {value: value === 'LOCK', type: 0x10}});
1603
+ },
1604
+ },
1605
+ min_brightness: {
1606
+ key: ['min_brightness'],
1607
+ convertSet: async (entity, key, value, meta) => {
1608
+ const minValueHex = value.toString(16);
1609
+ const maxValueHex = 'ff';
1610
+ const minMaxValue = parseInt(`${minValueHex}${maxValueHex}`, 16);
1611
+ const payload = {0xfc00: {value: minMaxValue, type: 0x21}};
1612
+ await entity.write('genLevelCtrl', payload, {disableDefaultResponse: true});
1613
+ return {state: {min_brightness: value}};
1614
+ },
1615
+ convertGet: async (entity, key, meta) => {
1616
+ await entity.read('genLevelCtrl', [0xfc00]);
1617
+ },
1618
+ },
1619
+ color_power_on_behavior: {
1620
+ key: ['color_power_on_behavior'],
1621
+ convertSet: async (entity, key, value, meta) => {
1622
+ const lookup = {'initial': 0, 'previous': 1, 'cutomized': 2};
1623
+ utils.validateValue(value, Object.keys(lookup));
1624
+ await entity.command('lightingColorCtrl', 'tuyaOnStartUp', {
1625
+ mode: lookup[value]*256, data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]});
1626
+ return {state: {color_power_on_behavior: value}};
1627
+ },
1628
+ },
1629
+ datapoints: {
1630
+ key: [
1631
+ 'temperature_unit', 'temperature_calibration', 'humidity_calibration', 'alarm_switch',
1632
+ 'state', 'brightness', 'min_brightness', 'max_brightness', 'power_on_behavior', 'position',
1633
+ 'countdown', 'light_type', 'silence', 'self_test', 'child_lock', 'open_window', 'open_window_temperature', 'frost_protection',
1634
+ 'system_mode', 'heating_stop', 'current_heating_setpoint', 'local_temperature_calibration', 'preset', 'boost_timeset_countdown',
1635
+ 'holiday_start_stop', 'holiday_temperature', 'comfort_temperature', 'eco_temperature', 'working_day',
1636
+ 'week_schedule_programming', 'online', 'holiday_mode_date', 'schedule', 'schedule_monday', 'schedule_tuesday',
1637
+ 'schedule_wednesday', 'schedule_thursday', 'schedule_friday', 'schedule_saturday', 'schedule_sunday', 'clear_fault',
1638
+ 'scale_protection', 'error', 'radar_scene', 'radar_sensitivity', 'tumble_alarm_time', 'tumble_switch', 'fall_sensitivity',
1639
+ 'min_temperature', 'max_temperature', 'window_detection', 'boost_heating', 'alarm_ringtone', 'alarm_time', 'fan_speed',
1640
+ ],
1641
+ convertSet: async (entity, key, value, meta) => {
1642
+ // A set converter is only called once; therefore we need to loop
1643
+ const state = {};
1644
+ const datapoints = utils.getMetaValue(entity, meta.mapped, 'tuyaDatapoints', undefined, undefined);
1645
+ if (!datapoints) throw new Error('No datapoints map defined');
1646
+ for (const [key, value] of Object.entries(meta.message)) {
1647
+ const convertedKey = meta.mapped.meta.multiEndpoint ? `${key}_${meta.endpoint_name}` : key;
1648
+ const dpEntry = datapoints.find((d) => d[1] === convertedKey);
1649
+ if (!dpEntry || !dpEntry[1]) {
1650
+ throw new Error(`No datapoint defined for '${key}'`);
1651
+ }
1652
+ if (dpEntry[3] && dpEntry[3].skip && dpEntry[3].skip(meta)) continue;
1653
+ const dpId = dpEntry[0];
1654
+ const convertedValue = await dpEntry[2].to(value, meta);
1655
+ const sendCommand = utils.getMetaValue(entity, meta.mapped, 'tuyaSendCommand', undefined, 'dataRequest');
1656
+ if (convertedValue === undefined) {
1657
+ // conversion done inside converter, ignore.
1658
+ } else if (typeof convertedValue === 'boolean') {
1659
+ await sendDataPointBool(entity, dpId, convertedValue, sendCommand, 1);
1660
+ } else if (typeof convertedValue === 'number') {
1661
+ await sendDataPointValue(entity, dpId, convertedValue, sendCommand, 1);
1662
+ } else if (typeof convertedValue === 'string') {
1663
+ await sendDataPointStringBuffer(entity, dpId, convertedValue, sendCommand, 1);
1664
+ } else if (Array.isArray(convertedValue)) {
1665
+ await sendDataPointRaw(entity, dpId, convertedValue, sendCommand, 1);
1666
+ } else if (convertedValue instanceof Enum) {
1667
+ await sendDataPointEnum(entity, dpId, convertedValue.valueOf(), sendCommand, 1);
1668
+ } else if (convertedValue instanceof Bitmap) {
1669
+ await sendDataPointBitmap(entity, dpId, convertedValue.valueOf(), sendCommand, 1);
1670
+ } else {
1671
+ throw new Error(`Don't know how to send type '${typeof convertedValue}'`);
1672
+ }
1673
+ state[key] = value;
1674
+ }
1675
+ return {state};
1676
+ },
1677
+ },
1678
+ do_not_disturb: {
1679
+ key: ['do_not_disturb'],
1680
+ convertSet: async (entity, key, value, meta) => {
1681
+ await entity.command('lightingColorCtrl', 'tuyaDoNotDisturb', {enable: value ? 1 : 0});
1682
+ return {state: {do_not_disturb: value}};
1683
+ },
1684
+ },
1685
+ };
1686
+
1687
+ const tuyaFz = {
1688
+ gateway_connection_status: {
1689
+ cluster: 'manuSpecificTuya',
1690
+ type: ['commandMcuGatewayConnectionStatus'],
1691
+ convert: async (model, msg, publish, options, meta) => {
1692
+ // "payload" can have the following values:
1693
+ // 0x00: The gateway is not connected to the internet.
1694
+ // 0x01: The gateway is connected to the internet.
1695
+ // 0x02: The request timed out after three seconds.
1696
+ const payload = {payloadSize: 1, payload: 1};
1697
+ await msg.endpoint.command('manuSpecificTuya', 'mcuGatewayConnectionStatus', payload, {});
1698
+ },
1699
+ },
1700
+ power_on_behavior_1: {
1701
+ cluster: 'genOnOff',
1702
+ type: ['attributeReport', 'readResponse'],
1703
+ convert: (model, msg, publish, options, meta) => {
1704
+ if (msg.data.hasOwnProperty('moesStartUpOnOff')) {
1705
+ const lookup = {0: 'off', 1: 'on', 2: 'previous'};
1706
+ const property = utils.postfixWithEndpointName('power_on_behavior', msg, model, meta);
1707
+ return {[property]: lookup[msg.data['moesStartUpOnOff']]};
1708
+ }
1709
+ },
1710
+ },
1711
+ power_on_behavior_2: {
1712
+ cluster: 'manuSpecificTuya_3',
1713
+ type: ['attributeReport', 'readResponse'],
1714
+ convert: (model, msg, publish, options, meta) => {
1715
+ const attribute = 'powerOnBehavior';
1716
+ const lookup = {0: 'off', 1: 'on', 2: 'previous'};
1717
+ if (msg.data.hasOwnProperty(attribute)) {
1718
+ const property = utils.postfixWithEndpointName('power_on_behavior', msg, model, meta);
1719
+ return {[property]: lookup[msg.data[attribute]]};
1720
+ }
1721
+ },
1722
+ },
1723
+ power_outage_memory: {
1724
+ cluster: 'genOnOff',
1725
+ type: ['attributeReport', 'readResponse'],
1726
+ convert: (model, msg, publish, options, meta) => {
1727
+ if (msg.data.hasOwnProperty('moesStartUpOnOff')) {
1728
+ const lookup = {0x00: 'off', 0x01: 'on', 0x02: 'restore'};
1729
+ const property = utils.postfixWithEndpointName('power_outage_memory', msg, model, meta);
1730
+ return {[property]: lookup[msg.data['moesStartUpOnOff']]};
1731
+ }
1732
+ },
1733
+ },
1734
+ switch_type: {
1735
+ cluster: 'manuSpecificTuya_3',
1736
+ type: ['attributeReport', 'readResponse'],
1737
+ convert: (model, msg, publish, options, meta) => {
1738
+ if (msg.data.hasOwnProperty('switchType')) {
1739
+ const lookup = {0: 'toggle', 1: 'state', 2: 'momentary'};
1740
+ return {switch_type: lookup[msg.data['switchType']]};
1741
+ }
1742
+ },
1743
+ },
1744
+ backlight_mode_low_medium_high: {
1745
+ cluster: 'genOnOff',
1746
+ type: ['attributeReport', 'readResponse'],
1747
+ convert: (model, msg, publish, options, meta) => {
1748
+ if (msg.data.hasOwnProperty('tuyaBacklightMode')) {
1749
+ const value = msg.data['tuyaBacklightMode'];
1750
+ const backlightLookup = {0: 'low', 1: 'medium', 2: 'high'};
1751
+ return {backlight_mode: backlightLookup[value]};
1752
+ }
1753
+ },
1754
+ },
1755
+ backlight_mode_off_normal_inverted: {
1756
+ cluster: 'genOnOff',
1757
+ type: ['attributeReport', 'readResponse'],
1758
+ convert: (model, msg, publish, options, meta) => {
1759
+ if (msg.data.hasOwnProperty('tuyaBacklightMode')) {
1760
+ const value = msg.data['tuyaBacklightMode'];
1761
+ const backlightLookup = {0: 'off', 1: 'normal', 2: 'inverted'};
1762
+ return {backlight_mode: backlightLookup[value]};
1763
+ }
1764
+ },
1765
+ },
1766
+ backlight_mode_off_on: {
1767
+ cluster: 'genOnOff',
1768
+ type: ['attributeReport', 'readResponse'],
1769
+ convert: (model, msg, publish, options, meta) => {
1770
+ if (msg.data.hasOwnProperty('tuyaBacklightSwitch')) {
1771
+ const value = msg.data['tuyaBacklightSwitch'];
1772
+ const backlightLookup = {0: 'off', 1: 'on'};
1773
+ return {backlight_mode: backlightLookup[value]};
1774
+ }
1775
+ },
1776
+ },
1777
+ indicator_mode: {
1778
+ cluster: 'genOnOff',
1779
+ type: ['attributeReport', 'readResponse'],
1780
+ convert: (model, msg, publish, options, meta) => {
1781
+ if (msg.data.hasOwnProperty('tuyaBacklightMode')) {
1782
+ const value = msg.data['tuyaBacklightMode'];
1783
+ const lookup = {0: 'off', 1: 'off/on', 2: 'on/off', 3: 'on'};
1784
+ if (lookup.hasOwnProperty(value)) {
1785
+ return {indicator_mode: lookup[value]};
1786
+ }
1787
+ }
1788
+ },
1789
+ },
1790
+ child_lock: {
1791
+ cluster: 'genOnOff',
1792
+ type: ['attributeReport', 'readResponse'],
1793
+ convert: (model, msg, publish, options, meta) => {
1794
+ if (msg.data.hasOwnProperty('32768')) {
1795
+ const value = msg.data['32768'];
1796
+ return {child_lock: value ? 'LOCK' : 'UNLOCK'};
1797
+ }
1798
+ },
1799
+ },
1800
+ min_brightness: {
1801
+ cluster: 'genLevelCtrl',
1802
+ type: ['attributeReport', 'readResponse'],
1803
+ convert: (model, msg, publish, options, meta) => {
1804
+ if (msg.data.hasOwnProperty(0xfc00)) {
1805
+ const property = utils.postfixWithEndpointName('min_brightness', msg, model, meta);
1806
+ const value = parseInt(msg.data[0xfc00].toString(16).slice(0, 2), 16);
1807
+ return {[property]: value};
1808
+ }
1809
+ },
1810
+ },
1811
+ datapoints: {
1812
+ cluster: 'manuSpecificTuya',
1813
+ type: ['commandDataResponse', 'commandDataReport', 'commandActiveStatusReport', 'commandActiveStatusReportAlt'],
1814
+ options: (definition) => {
1815
+ const result = [];
1816
+ for (const datapoint of definition.meta.tuyaDatapoints) {
1817
+ const dpKey = datapoint[1];
1818
+ if (dpKey in utils.calibrateAndPrecisionRoundOptionsDefaultPrecision) {
1819
+ const type = utils.calibrateAndPrecisionRoundOptionsIsPercentual(dpKey) ? 'percentual' : 'absolute';
1820
+ result.push(exposes.options.precision(dpKey), exposes.options.calibration(dpKey, type));
1821
+ }
1822
+ }
1823
+ return result;
1824
+ },
1825
+ convert: (model, msg, publish, options, meta) => {
1826
+ let result = {};
1827
+ if (!model.meta || !model.meta.tuyaDatapoints) throw new Error('No datapoints map defined');
1828
+ const datapoints = model.meta.tuyaDatapoints;
1829
+ for (const dpValue of msg.data.dpValues) {
1830
+ const dpId = dpValue.dp;
1831
+ const dpEntry = datapoints.find((d) => d[0] === dpId);
1832
+ if (dpEntry) {
1833
+ const value = getDataValue(dpValue);
1834
+ if (dpEntry[1]) {
1835
+ result[dpEntry[1]] = dpEntry[2].from(value, meta, options);
1836
+ } else if (dpEntry[2]) {
1837
+ result = {...result, ...dpEntry[2].from(value, meta, options)};
1838
+ }
1839
+ } else {
1840
+ meta.logger.debug(`Datapoint ${dpId} not defined for '${meta.device.manufacturerName}' ` +
1841
+ `with data ${JSON.stringify(dpValue)}`);
1842
+ }
1843
+ }
1844
+
1845
+ // Apply calibrateAndPrecisionRoundOptions
1846
+ const keys = Object.keys(utils.calibrateAndPrecisionRoundOptionsDefaultPrecision);
1847
+ for (const entry of Object.entries(result)) {
1848
+ if (keys.includes(entry[0])) {
1849
+ result[entry[0]] = utils.calibrateAndPrecisionRoundOptions(entry[1], options, entry[0]);
1850
+ }
1851
+ }
1852
+ return result;
1853
+ },
1854
+ },
1855
+ };
1856
+
1857
+ const tuyaExtend = {
1858
+ switch: (options={}) => {
1859
+ const tz = require('../converters/toZigbee');
1860
+ const fz = require('../converters/fromZigbee');
1861
+ const exposes = options.endpoints ? options.endpoints.map((ee) => e.switch().withEndpoint(ee)) : [e.switch()];
1862
+ const fromZigbee = [fz.on_off, fz.ignore_basic_report];
1863
+ const toZigbee = [tz.on_off];
1864
+ if (options.powerOutageMemory) {
1865
+ // Legacy, powerOnBehavior is preferred
1866
+ fromZigbee.push(tuyaFz.power_outage_memory);
1867
+ toZigbee.push(tuyaTz.power_on_behavior_1);
1868
+ exposes.push(tuyaExposes.powerOutageMemory());
1869
+ } else {
1870
+ fromZigbee.push(tuyaFz.power_on_behavior_1);
1871
+ toZigbee.push(tuyaTz.power_on_behavior_1);
1872
+ exposes.push(e.power_on_behavior());
1873
+ }
1874
+
1875
+ if (options.switchType) {
1876
+ fromZigbee.push(tuyaFz.switch_type);
1877
+ toZigbee.push(tuyaTz.switch_type);
1878
+ exposes.push(tuyaExposes.switchType());
1879
+ }
1880
+
1881
+ if (options.backlightModeLowMediumHigh) {
1882
+ fromZigbee.push(tuyaFz.backlight_mode_low_medium_high);
1883
+ exposes.push(tuyaExposes.backlightModeLowMediumHigh());
1884
+ toZigbee.push(tuyaTz.backlight_indicator_mode_1);
1885
+ }
1886
+ if (options.backlightModeOffNormalInverted) {
1887
+ fromZigbee.push(tuyaFz.backlight_mode_off_normal_inverted);
1888
+ exposes.push(tuyaExposes.backlightModeOffNormalInverted());
1889
+ toZigbee.push(tuyaTz.backlight_indicator_mode_1);
1890
+ }
1891
+ if (options.indicatorMode) {
1892
+ fromZigbee.push(tuyaFz.indicator_mode);
1893
+ exposes.push(tuyaExposes.indicatorMode());
1894
+ toZigbee.push(tuyaTz.backlight_indicator_mode_1);
1895
+ }
1896
+ if (options.backlightModeOffOn) {
1897
+ fromZigbee.push(tuyaFz.backlight_mode_off_on);
1898
+ exposes.push(tuyaExposes.backlightModeOffOn());
1899
+ toZigbee.push(tuyaTz.backlight_indicator_mode_2);
1900
+ }
1901
+
1902
+ if (options.electricalMeasurements) {
1903
+ fromZigbee.push(fz.electrical_measurement, fz.metering);
1904
+ exposes.push(e.power(), e.current(), e.voltage(), e.energy());
1905
+ }
1906
+ if (options.childLock) {
1907
+ fromZigbee.push(tuyaFz.child_lock);
1908
+ toZigbee.push(tuyaTz.child_lock);
1909
+ exposes.push(e.child_lock());
1910
+ }
1911
+ return {exposes, fromZigbee, toZigbee};
1912
+ },
1913
+ light_onoff_brightness_colortemp_color: (options={}) => {
1914
+ const extend = require('./extend');
1915
+ options = {
1916
+ disableColorTempStartup: true, disablePowerOnBehavior: true, toZigbee: [tuyaTz.do_not_disturb, tuyaTz.color_power_on_behavior],
1917
+ exposes: [tuyaExposes.doNotDisturb(), tuyaExposes.colorPowerOnBehavior()], ...options,
1918
+ };
1919
+ const meta = {applyRedFix: true, enhancedHue: false};
1920
+ return {...extend.light_onoff_brightness_colortemp_color(options), meta};
1921
+ },
1922
+ light_onoff_brightness_colortemp: (options={}) => {
1923
+ const extend = require('./extend');
1924
+ options = {
1925
+ disableColorTempStartup: true, disablePowerOnBehavior: true, toZigbee: [tuyaTz.do_not_disturb],
1926
+ exposes: [tuyaExposes.doNotDisturb()], ...options,
1927
+ };
1928
+ return extend.light_onoff_brightness_colortemp(options);
1929
+ },
1930
+ light_onoff_brightness_color: (options={}) => {
1931
+ const extend = require('./extend');
1932
+ options = {
1933
+ disablePowerOnBehavior: true, toZigbee: [tuyaTz.do_not_disturb, tuyaTz.color_power_on_behavior],
1934
+ exposes: [tuyaExposes.doNotDisturb(), tuyaExposes.colorPowerOnBehavior()], ...options,
1935
+ };
1936
+ const meta = {applyRedFix: true, enhancedHue: false};
1937
+ return {...extend.light_onoff_brightness_color(options), meta};
1938
+ },
1939
+ light_onoff_brightness: (options={}) => {
1940
+ const extend = require('./extend');
1941
+ options = {
1942
+ disablePowerOnBehavior: true, toZigbee: [tuyaTz.do_not_disturb], exposes: [tuyaExposes.doNotDisturb()],
1943
+ minBrightness: false, ...options,
1944
+ };
1945
+ const result = extend.light_onoff_brightness(options);
1946
+ result.exposes = options.endpoints ? options.endpoints.map((ee) => e.light_brightness()) : [e.light_brightness()];
1947
+ if (options.minBrightness) {
1948
+ result.fromZigbee.push(tuyaFz.min_brightness);
1949
+ result.toZigbee.push(tuyaTz.min_brightness);
1950
+ result.exposes = result.exposes.map((e) => e.withMinBrightness());
1951
+ }
1952
+ if (options.endpoints) {
1953
+ result.exposes = result.exposes.map((e, i) => e.withEndpoint(options.endpoints[i]));
1954
+ }
1955
+ return result;
1956
+ },
1957
+ };
1958
+
1959
+ module.exports = {
1960
+ exposes: tuyaExposes,
1961
+ extend: tuyaExtend,
1962
+ tz: tuyaTz,
1963
+ fz: tuyaFz,
1964
+ skip,
1965
+ configureMagicPacket,
1966
+ fingerprint,
1967
+ enum: (value) => new Enum(value),
1968
+ bitmap: (value) => new Bitmap(value),
1969
+ valueConverter,
1970
+ valueConverterBasic,
1971
+ tzDataPoints: tuyaTz.datapoints,
1972
+ fzDataPoints: tuyaFz.datapoints,
1973
+ sendDataPoint,
1974
+ sendDataPoints,
1975
+ sendDataPointValue,
1976
+ sendDataPointBool,
1977
+ sendDataPointEnum,
1978
+ sendDataPointBitmap,
1979
+ sendDataPointRaw,
1980
+ sendDataPointStringBuffer,
1981
+ firstDpValue,
1982
+ getDataValue,
1983
+ getTypeName,
1984
+ getDataPointNames,
1985
+ logDataPoint,
1986
+ logUnexpectedDataPoint,
1987
+ logUnexpectedDataType,
1988
+ logUnexpectedDataValue,
1989
+ dataTypes,
1990
+ dataPoints,
1991
+ dpValueFromIntValue,
1992
+ dpValueFromBool,
1993
+ dpValueFromEnum,
1994
+ dpValueFromStringBuffer,
1995
+ dpValueFromRaw,
1996
+ dpValueFromBitmap,
1997
+ convertRawToCycleTimer,
1998
+ convertRawToTimer,
1999
+ convertTimeTo2ByteHexArray,
2000
+ convertWeekdaysTo1ByteHexArray,
2001
+ convertDecimalValueTo4ByteHexArray,
2002
+ convertDecimalValueTo2ByteHexArray,
2003
+ onEventSetTime,
2004
+ onEventSetLocalTime,
2005
+ onEventMeasurementPoll,
2006
+ convertStringToHexArray,
2007
+ isCoverInverted,
2008
+ getCoverStateEnums,
2009
+ thermostatSystemModes4,
2010
+ thermostatSystemModes3,
2011
+ thermostatSystemModes2,
2012
+ thermostatSystemModes,
2013
+ thermostatWeekFormat,
2014
+ thermostatForceMode,
2015
+ thermostatPresets,
2016
+ thermostatScheduleMode,
2017
+ silvercrestModes,
2018
+ silvercrestEffects,
2019
+ fanModes,
2020
+ msLookups,
2021
+ tvThermostatMode,
2022
+ tvThermostatPreset,
2023
+ tuyaRadar,
2024
+ ZMLookups,
2025
+ moesSwitch,
2026
+ tuyaHPSCheckingResult,
2027
+ };