@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
@@ -0,0 +1,476 @@
1
+ const crypto = require('crypto');
2
+ const upgradeFileIdentifier = Buffer.from([0x1E, 0xF1, 0xEE, 0x0B]);
3
+ const HttpsProxyAgent = require('https-proxy-agent');
4
+ const assert = require('assert');
5
+ const crc32 = require('buffer-crc32');
6
+ const maxTimeout = 2147483647; // +- 24 days
7
+ const imageBlockResponseDelay = 250;
8
+ const endRequestCodeLookup = {
9
+ 0x00: 'success',
10
+ 0x95: 'aborted by device',
11
+ 0x7E: 'not authorized',
12
+ 0x96: 'invalid image',
13
+ 0x97: 'no data available',
14
+ 0x98: 'no image available',
15
+ 0x80: 'malformed command',
16
+ 0x81: 'unsupported cluster command',
17
+ 0x99: 'requires more image files',
18
+ };
19
+
20
+ const validSilabsCrc = 0x2144DF1C;
21
+
22
+ const eblTagHeader = 0x0;
23
+ const eblTagEncHeader = 0xfb05;
24
+ const eblTagEnd = 0xfc04;
25
+ const eblPadding = 0xff;
26
+ const eblImageSignature = 0xe350;
27
+
28
+ const gblTagHeader = 0xeb17a603;
29
+ const gblTagEnd = 0xfc0404fc;
30
+
31
+ function getOTAEndpoint(device) {
32
+ return device.endpoints.find((e) => e.supportsOutputCluster('genOta'));
33
+ }
34
+
35
+ function parseSubElement(buffer, position) {
36
+ const tagID = buffer.readUInt16LE(position);
37
+ const length = buffer.readUInt32LE(position + 2);
38
+ const data = buffer.slice(position + 6, position + 6 + length);
39
+ return {tagID, length, data};
40
+ }
41
+
42
+ function parseImage(buffer) {
43
+ const header = {
44
+ otaUpgradeFileIdentifier: buffer.subarray(0, 4),
45
+ otaHeaderVersion: buffer.readUInt16LE(4),
46
+ otaHeaderLength: buffer.readUInt16LE(6),
47
+ otaHeaderFieldControl: buffer.readUInt16LE(8),
48
+ manufacturerCode: buffer.readUInt16LE(10),
49
+ imageType: buffer.readUInt16LE(12),
50
+ fileVersion: buffer.readUInt32LE(14),
51
+ zigbeeStackVersion: buffer.readUInt16LE(18),
52
+ otaHeaderString: buffer.toString('utf8', 20, 52),
53
+ totalImageSize: buffer.readUInt32LE(52),
54
+ };
55
+ let headerPos = 56;
56
+ if (header.otaHeaderFieldControl & 1) {
57
+ header.securityCredentialVersion = buffer.readUInt8(headerPos);
58
+ headerPos += 1;
59
+ }
60
+ if (header.otaHeaderFieldControl & 2) {
61
+ header.upgradeFileDestination = buffer.subarray(headerPos, headerPos + 8);
62
+ headerPos += 8;
63
+ }
64
+ if (header.otaHeaderFieldControl & 4) {
65
+ header.minimumHardwareVersion = buffer.readUInt16LE(headerPos);
66
+ headerPos += 2;
67
+ header.maximumHardwareVersion = buffer.readUInt16LE(headerPos);
68
+ headerPos += 2;
69
+ }
70
+
71
+ const raw = buffer.slice(0, header.totalImageSize);
72
+
73
+ assert(Buffer.compare(header.otaUpgradeFileIdentifier, upgradeFileIdentifier) === 0, 'Not an OTA file');
74
+
75
+ let position = header.otaHeaderLength;
76
+ const elements = [];
77
+ while (position < header.totalImageSize) {
78
+ const element = parseSubElement(buffer, position);
79
+ elements.push(element);
80
+ position += element.data.length + 6;
81
+ }
82
+
83
+ assert(position === header.totalImageSize, 'Size mismatch');
84
+ return {header, elements, raw};
85
+ }
86
+
87
+ function validateImageData(image) {
88
+ for (const element of image.elements) {
89
+ const {data} = element;
90
+
91
+ if (data.readUInt32BE(0) === gblTagHeader) {
92
+ validateSilabsGbl(data);
93
+ } else {
94
+ const tag = data.readUInt16BE(0);
95
+
96
+ if ((tag === eblTagHeader && data.readUInt16BE(6) === eblImageSignature) || tag === eblTagEncHeader ) {
97
+ validateSilabsEbl(data);
98
+ }
99
+ }
100
+ }
101
+ }
102
+
103
+ function validateSilabsEbl(data) {
104
+ const dataLength = data.length;
105
+
106
+ let position = 0;
107
+
108
+ while (position + 4 <= dataLength) {
109
+ const tag = data.readUInt16BE(position);
110
+ const len = data.readUInt16BE(position + 2);
111
+
112
+ position += 4 + len;
113
+
114
+ if (tag !== eblTagEnd) {
115
+ continue;
116
+ }
117
+
118
+ for (let position2 = position; position2 < dataLength; position2++) {
119
+ assert(data.readUInt8(position2) === eblPadding, `Image padding contains invalid bytes`);
120
+ }
121
+
122
+ const calculatedCrc32 = crc32.unsigned(data.slice(0, position));
123
+
124
+ assert(calculatedCrc32 === validSilabsCrc, `Image CRC-32 is invalid`);
125
+
126
+ return;
127
+ }
128
+
129
+ throw new Error(`Image is truncated: not long enough to contain a valid tag`);
130
+ }
131
+
132
+ function validateSilabsGbl(data) {
133
+ const dataLength = data.length;
134
+
135
+ let position = 0;
136
+
137
+ while (position + 8 <= dataLength) {
138
+ const tag = data.readUInt32BE(position);
139
+ const len = data.readUInt32LE(position + 4);
140
+
141
+ position += 8 + len;
142
+
143
+ if (tag !== gblTagEnd) {
144
+ continue;
145
+ }
146
+
147
+ const calculatedCrc32 = crc32.unsigned(data.slice(0, position));
148
+
149
+ assert(calculatedCrc32 === validSilabsCrc, `Image CRC-32 is invalid`);
150
+
151
+ return;
152
+ }
153
+
154
+ throw new Error(`Image is truncated: not long enough to contain a valid tag`);
155
+ }
156
+
157
+ function cancelWaiters(waiters) {
158
+ for (const waiter of Object.values(waiters)) {
159
+ if (waiter) {
160
+ waiter.cancel();
161
+ }
162
+ }
163
+ }
164
+
165
+ function sendQueryNextImageResponse(endpoint, image, logger) {
166
+ const payload = {
167
+ status: 0,
168
+ manufacturerCode: image.header.manufacturerCode,
169
+ imageType: image.header.imageType,
170
+ fileVersion: image.header.fileVersion,
171
+ imageSize: image.header.totalImageSize,
172
+ };
173
+
174
+ endpoint.commandResponse('genOta', 'queryNextImageResponse', payload).catch((e) => {
175
+ logger.debug(`Failed to send queryNextImageResponse (${e.message})`);
176
+ });
177
+ }
178
+
179
+ function imageNotify(endpoint) {
180
+ return endpoint.commandResponse('genOta', 'imageNotify', {payloadType: 0, queryJitter: 100}, {sendWhen: 'immediate'});
181
+ }
182
+
183
+ async function requestOTA(endpoint) {
184
+ // Some devices (e.g. Insta) take very long trying to discover the correct coordinator EP for OTA.
185
+ const queryNextImageRequest = endpoint.waitForCommand('genOta', 'queryNextImageRequest', null, 60000);
186
+ try {
187
+ await imageNotify(endpoint);
188
+ return await queryNextImageRequest.promise;
189
+ } catch (e) {
190
+ queryNextImageRequest.cancel();
191
+ throw new Error(`Device didn't respond to OTA request`);
192
+ }
193
+ }
194
+
195
+ function getImageBlockResponsePayload(image, imageBlockRequest, pageOffset, pageSize) {
196
+ const start = imageBlockRequest.payload.fileOffset + pageOffset;
197
+ // When the data size is too big, OTA gets unstable, so default it to 50 bytes maximum.
198
+ // For Insta devices, OTA only works for data sizes 40 and smaller (= manufacturerCode 4474).
199
+ const maximumDataSize = imageBlockRequest.payload.manufacturerCode === 4474 ? 40 : 50;
200
+ let dataSize = Math.min(maximumDataSize, imageBlockRequest.payload.maximumDataSize);
201
+ if (pageSize) {
202
+ dataSize = Math.min(dataSize, pageSize - pageOffset);
203
+ }
204
+ let end = start + dataSize;
205
+ if (end > image.raw.length) {
206
+ end = image.raw.length;
207
+ }
208
+
209
+ return {
210
+ status: 0,
211
+ manufacturerCode: imageBlockRequest.payload.manufacturerCode,
212
+ imageType: imageBlockRequest.payload.imageType,
213
+ fileVersion: imageBlockRequest.payload.fileVersion,
214
+ fileOffset: start,
215
+ dataSize: end - start,
216
+ data: image.raw.slice(start, end),
217
+ };
218
+ }
219
+
220
+ function callOnProgress(startTime, lastUpdate, imageBlockRequest, image, logger, onProgress) {
221
+ const now = Date.now();
222
+
223
+ // Call on progress every +- 30 seconds
224
+ if (lastUpdate === null || (now - lastUpdate) > 30000) {
225
+ const totalDuration = (now - startTime) / 1000; // in seconds
226
+ const bytesPerSecond = imageBlockRequest.payload.fileOffset / totalDuration;
227
+ const remaining = (image.header.totalImageSize - imageBlockRequest.payload.fileOffset) / bytesPerSecond;
228
+ let percentage = imageBlockRequest.payload.fileOffset / image.header.totalImageSize;
229
+ percentage = Math.round(percentage * 10000) / 100;
230
+ logger.debug(`OTA update at ${percentage}%, remaining ${remaining} seconds`);
231
+ onProgress(percentage, remaining === Infinity ? null : remaining);
232
+ return now;
233
+ } else {
234
+ return lastUpdate;
235
+ }
236
+ }
237
+
238
+ async function isUpdateAvailable(device, logger, isNewImageAvailable, requestPayload, getImageMeta = null) {
239
+ logger.debug(`Check if update available for '${device.ieeeAddr}' (${device.modelID})`);
240
+
241
+ if (requestPayload === null) {
242
+ const endpoint = getOTAEndpoint(device);
243
+ assert(endpoint != null, `Failed to find endpoint which support OTA cluster`);
244
+ logger.debug(`Using endpoint '${endpoint.ID}'`);
245
+
246
+ const request = await requestOTA(endpoint);
247
+ logger.debug(`Got OTA request '${JSON.stringify(request.payload)}'`);
248
+ requestPayload = request.payload;
249
+ }
250
+
251
+ const availableResult = await isNewImageAvailable(requestPayload, logger, device, getImageMeta);
252
+ logger.debug(`Update available for '${device.ieeeAddr}': ${availableResult.available < 0 ? 'YES' : 'NO'}`);
253
+ if (availableResult.available > 0) {
254
+ logger.warn(`Firmware on '${device.ieeeAddr}' is newer than latest firmware online.`);
255
+ }
256
+ return {...availableResult, available: availableResult.available < 0};
257
+ }
258
+
259
+ async function isNewImageAvailable(current, logger, device, getImageMeta) {
260
+ const meta = await getImageMeta(current, logger, device);
261
+ const [currentS, metaS] = [JSON.stringify(current), JSON.stringify(meta)];
262
+ logger.debug(`Is new image available for '${device.ieeeAddr}', current '${currentS}', latest meta '${metaS}'`);
263
+
264
+ // Negative number means the new firmware is 'newer' than current one
265
+ return {
266
+ available: meta.force ? -1 : Math.sign(current.fileVersion - meta.fileVersion),
267
+ currentFileVersion: current.fileVersion,
268
+ otaFileVersion: meta.fileVersion,
269
+ };
270
+ }
271
+
272
+ async function updateToLatest(device, logger, onProgress, getNewImage, getImageMeta = null, downloadImage = null) {
273
+ logger.debug(`Updating to latest '${device.ieeeAddr}' (${device.modelID})`);
274
+
275
+ const endpoint = getOTAEndpoint(device);
276
+ assert(endpoint != null, `Failed to find endpoint which support OTA cluster`);
277
+ logger.debug(`Using endpoint '${endpoint.ID}'`);
278
+
279
+ const request = await requestOTA(endpoint);
280
+ logger.debug(`Got OTA request '${JSON.stringify(request.payload)}'`);
281
+
282
+ const image = await getNewImage(request.payload, logger, device, getImageMeta, downloadImage);
283
+ logger.debug(`Got new image for '${device.ieeeAddr}'`);
284
+
285
+ const waiters = {};
286
+ let lastUpdate = null;
287
+ let lastImageBlockResponse = null;
288
+ const startTime = Date.now();
289
+
290
+ return new Promise((resolve, reject) => {
291
+ const answerNextImageBlockOrPageRequest = () => {
292
+ const imageBlockRequest = endpoint.waitForCommand('genOta', 'imageBlockRequest', null, 150000);
293
+ const imagePageRequest = endpoint.waitForCommand('genOta', 'imagePageRequest', null, 150000);
294
+ waiters.imageBlockOrPageRequest = {
295
+ promise: Promise.race([imageBlockRequest.promise, imagePageRequest.promise]),
296
+ cancel: () => {
297
+ imageBlockRequest.cancel();
298
+ imagePageRequest.cancel();
299
+ },
300
+ };
301
+
302
+ waiters.imageBlockOrPageRequest.promise.then(
303
+ (imageBlockOrPageRequest) => {
304
+ let pageOffset = 0;
305
+ let pageSize = 0;
306
+
307
+ const sendImageBlockResponse = (imageBlockRequest, thenCallback, transactionSequenceNumber) => {
308
+ const payload = getImageBlockResponsePayload(image, imageBlockRequest, pageOffset, pageSize);
309
+ const now = Date.now();
310
+ const timeSinceLastImageBlockResponse = now - lastImageBlockResponse;
311
+
312
+ // Reduce network congestion by only sending imageBlockResponse min every 250ms.
313
+ const cooldownTime = Math.max(imageBlockResponseDelay - timeSinceLastImageBlockResponse, 0);
314
+ setTimeout(() => {
315
+ endpoint.commandResponse(
316
+ 'genOta', 'imageBlockResponse', payload, null, transactionSequenceNumber,
317
+ ).then(
318
+ () => {
319
+ pageOffset += payload.dataSize;
320
+ lastImageBlockResponse = Date.now();
321
+ thenCallback();
322
+ },
323
+ (e) => {
324
+ // Shit happens, device will probably do a new imageBlockRequest so don't care.
325
+ lastImageBlockResponse = Date.now();
326
+ thenCallback();
327
+ logger.debug(`Image block response failed (${e.message})`);
328
+ },
329
+ );
330
+ }, cooldownTime);
331
+
332
+ lastUpdate = callOnProgress(startTime, lastUpdate, imageBlockRequest, image, logger,
333
+ onProgress);
334
+ };
335
+
336
+ if ('pageSize' in imageBlockOrPageRequest.payload) {
337
+ // imagePageRequest
338
+ pageSize = imageBlockOrPageRequest.payload.pageSize;
339
+ const handleImagePageRequestBlocks = (imagePageRequest) => {
340
+ if (pageOffset < pageSize) {
341
+ sendImageBlockResponse(imagePageRequest,
342
+ () => handleImagePageRequestBlocks(imagePageRequest));
343
+ } else {
344
+ answerNextImageBlockOrPageRequest();
345
+ }
346
+ };
347
+ handleImagePageRequestBlocks(imageBlockOrPageRequest);
348
+ } else {
349
+ // imageBlockRequest
350
+ sendImageBlockResponse(imageBlockOrPageRequest, answerNextImageBlockOrPageRequest,
351
+ imageBlockOrPageRequest.header.transactionSequenceNumber);
352
+ }
353
+ },
354
+ () => {
355
+ cancelWaiters(waiters);
356
+ reject(new Error('Timeout: device did not request any image blocks'));
357
+ },
358
+ );
359
+ };
360
+
361
+ const answerNextImageRequest = () => {
362
+ waiters.nextImageRequest = endpoint.waitForCommand('genOta', 'queryNextImageRequest', null, maxTimeout);
363
+ waiters.nextImageRequest.promise.then(() => {
364
+ answerNextImageRequest();
365
+ sendQueryNextImageResponse(endpoint, image, logger);
366
+ });
367
+ };
368
+
369
+ // No need to timeout here, will already be done in answerNextImageBlockRequest
370
+ waiters.upgradeEndRequest = endpoint.waitForCommand('genOta', 'upgradeEndRequest', null, maxTimeout);
371
+ waiters.upgradeEndRequest.promise.then((data) => {
372
+ logger.debug(`Got upgrade end request for '${device.ieeeAddr}': ${JSON.stringify(data.payload)}`);
373
+ cancelWaiters(waiters);
374
+
375
+ if (data.payload.status === 0) {
376
+ const payload = {
377
+ manufacturerCode: image.header.manufacturerCode, imageType: image.header.imageType,
378
+ fileVersion: image.header.fileVersion, imageSize: image.header.totalImageSize,
379
+ currentTime: 0, upgradeTime: 1,
380
+ };
381
+
382
+ endpoint.commandResponse('genOta', 'upgradeEndResponse', payload).then(
383
+ () => {
384
+ logger.debug(`Update succeeded, waiting for device announce`);
385
+ onProgress(100, null);
386
+
387
+ let timer = null;
388
+ const cb = () => {
389
+ logger.debug('Got device announce or timed out, call resolve');
390
+ clearInterval(timer);
391
+ device.removeListener('deviceAnnounce', cb);
392
+ resolve(image.header.fileVersion);
393
+ };
394
+ timer = setTimeout(cb, 120 * 1000); // timeout after 2 minutes
395
+ device.once('deviceAnnounce', cb);
396
+ },
397
+ (e) => {
398
+ const message = `Upgrade end reponse failed (${e.message})`;
399
+ logger.debug(message);
400
+ reject(new Error(message));
401
+ },
402
+ );
403
+ } else {
404
+ const error = `Update failed with reason: '${endRequestCodeLookup[data.payload.status]}'`;
405
+ logger.debug(error);
406
+ reject(new Error(error));
407
+ }
408
+ });
409
+
410
+ logger.debug('Starting upgrade');
411
+ answerNextImageBlockOrPageRequest();
412
+ answerNextImageRequest();
413
+
414
+ // Notify client once more about new image, client should start sending queryNextImageRequest now
415
+ imageNotify(endpoint).catch((e) => logger.debug(`Image notify failed (${e})`));
416
+ });
417
+ }
418
+
419
+ async function getNewImage(current, logger, device, getImageMeta, downloadImage) {
420
+ const meta = await getImageMeta(current, logger, device);
421
+ logger.debug(`getNewImage for '${device.ieeeAddr}', meta ${JSON.stringify(meta)}`);
422
+ assert(meta.fileVersion > current.fileVersion || meta.force, 'No new image available');
423
+
424
+ const download = downloadImage ? await downloadImage(meta, logger) :
425
+ await getAxios().get(meta.url, {responseType: 'arraybuffer'});
426
+
427
+ const checksum = (meta.sha512 || meta.sha256);
428
+ if (checksum) {
429
+ const hash = crypto.createHash(meta.sha512 ? 'sha512' : 'sha256');
430
+ hash.update(download.data);
431
+ assert(hash.digest('hex') === checksum, 'File checksum validation failed');
432
+ logger.debug(`OTA update checksum validation succeeded for '${device.ieeeAddr}'`);
433
+ }
434
+
435
+ const start = download.data.indexOf(upgradeFileIdentifier);
436
+ const image = parseImage(download.data.slice(start));
437
+ logger.debug(`getNewImage for '${device.ieeeAddr}', image header ${JSON.stringify(image.header)}`);
438
+ assert(image.header.fileVersion === meta.fileVersion, 'File version mismatch');
439
+ assert(!meta.fileSize || image.header.totalImageSize === meta.fileSize, 'Image size mismatch');
440
+ assert(image.header.manufacturerCode === current.manufacturerCode, 'Manufacturer code mismatch');
441
+ assert(image.header.imageType === current.imageType, 'Image type mismatch');
442
+ if ('minimumHardwareVersion' in image.header && 'maximumHardwareVersion' in image.header) {
443
+ assert(image.header.minimumHardwareVersion <= device.hardwareVersion &&
444
+ device.hardwareVersion <= image.header.maximumHardwareVersion, 'Hardware version mismatch');
445
+ }
446
+ validateImageData(image);
447
+ return image;
448
+ }
449
+
450
+ function getAxios() {
451
+ let config = {};
452
+ const proxy = process.env.HTTPS_PROXY;
453
+ if (proxy) {
454
+ config = {
455
+ proxy: false,
456
+ httpsAgent: new HttpsProxyAgent(proxy),
457
+ headers: {
458
+ 'Accept-Encoding': '*',
459
+ },
460
+ };
461
+ }
462
+
463
+ const axios = require('axios').create(config);
464
+ return axios;
465
+ }
466
+
467
+ module.exports = {
468
+ upgradeFileIdentifier,
469
+ isUpdateAvailable,
470
+ parseImage,
471
+ validateImageData,
472
+ isNewImageAvailable,
473
+ updateToLatest,
474
+ getNewImage,
475
+ getAxios,
476
+ };
@@ -0,0 +1,10 @@
1
+ module.exports = {
2
+ inovelli: require('./inovelli'),
3
+ ledvance: require('./ledvance'),
4
+ salus: require('./salus'),
5
+ lixee: require('./lixee'),
6
+ securifi: require('./securifi'),
7
+ tradfri: require('./tradfri'),
8
+ ubisys: require('./ubisys'),
9
+ zigbeeOTA: require('./zigbeeOTA'),
10
+ };
@@ -0,0 +1,72 @@
1
+ const common = require('./common');
2
+ const axios = common.getAxios();
3
+ /*
4
+ * Helper functions
5
+ */
6
+
7
+ async function getImageMeta(current, logger, device) {
8
+ const images = (await axios.get('https://files.inovelli.com/firmware/firmware.json')).data;
9
+
10
+ if (Object.keys(images).indexOf(device.modelID) === -1) {
11
+ throw new Error(`The device '${device.modelID}' is not supported for OTA at this time.`);
12
+ }
13
+
14
+ // Force for now. There is only beta firmware at the moment.
15
+ const useBetaChannel = true;
16
+ const image = images[device.modelID]
17
+ .filter((i) => (useBetaChannel ? true : i.channel === 'production'))
18
+ .sort((a, b) => {
19
+ const aRadix = a.version.match(/[A-F]/) ? 16 : 10;
20
+ const bRadix = b.version.match(/[A-F]/) ? 16 : 10;
21
+ const aVersion = parseFloat(a.version, aRadix);
22
+ const bVersion = parseFloat(b.version, bRadix);
23
+ // doesn't matter which order they are in
24
+ if (aVersion < bVersion) {
25
+ return -1;
26
+ }
27
+ if (aVersion > bVersion) {
28
+ return 1;
29
+ }
30
+ return 0;
31
+ })
32
+ .pop();
33
+
34
+ if (!image) {
35
+ throw new Error(`No images found in the ${useBetaChannel ? 'beta' : 'production'} channel for the device '${device.modelID}'`,
36
+ );
37
+ }
38
+ // version in the firmare removes the zero padding and support hex versioning
39
+ return {
40
+ fileVersion: parseFloat(image.version, image.version.match(/[A-F]/) ? 16 : 10),
41
+ url: image.firmware,
42
+ };
43
+ }
44
+
45
+ /**
46
+ * Interface implementation
47
+ */
48
+
49
+ async function isUpdateAvailable(device, logger, requestPayload = null) {
50
+ return common.isUpdateAvailable(
51
+ device,
52
+ logger,
53
+ common.isNewImageAvailable,
54
+ requestPayload,
55
+ getImageMeta,
56
+ );
57
+ }
58
+
59
+ async function updateToLatest(device, logger, onProgress) {
60
+ return common.updateToLatest(
61
+ device,
62
+ logger,
63
+ onProgress,
64
+ common.getNewImage,
65
+ getImageMeta,
66
+ );
67
+ }
68
+
69
+ module.exports = {
70
+ isUpdateAvailable,
71
+ updateToLatest,
72
+ };
@@ -0,0 +1,52 @@
1
+ const updateCheckUrl = 'https://api.update.ledvance.com/v1/zigbee/firmwares/newer';
2
+ const updateDownloadUrl = 'https://api.update.ledvance.com/v1/zigbee/firmwares/download';
3
+ const assert = require('assert');
4
+ const common = require('./common');
5
+ const axios = common.getAxios();
6
+
7
+ /**
8
+ * Helper functions
9
+ */
10
+
11
+ async function getImageMeta(current, logger, device) {
12
+ const manufacturerCode = current.manufacturerCode;
13
+ const imageType = current.imageType;
14
+ const {data} = await axios.get(updateCheckUrl +
15
+ `?company=${manufacturerCode}&product=${imageType}&version=0.0.0`);
16
+
17
+ assert(data && data.firmwares && data.firmwares.length > 0,
18
+ `No image available for manufacturerCode '${manufacturerCode}' imageType '${imageType}'`);
19
+
20
+ // Ledvance's API docs state the checksum should be `sha_256` but it is actually `shA256`
21
+ const {identity, fullName, length, shA256: sha256} = data.firmwares[0];
22
+
23
+ const fileVersionMatch = /\/(\d+)\//.exec(fullName);
24
+ const fileVersion = parseInt(`0x${fileVersionMatch[1]}`, 16);
25
+
26
+ const versionString = `${identity.version.major}.${identity.version.minor}.${identity.version.build}.${identity.version.revision}`;
27
+
28
+ return {
29
+ fileVersion,
30
+ fileSize: length,
31
+ url: updateDownloadUrl +
32
+ `?company=${identity.company}&product=${identity.product}&version=${versionString}`,
33
+ sha256,
34
+ };
35
+ }
36
+
37
+ /**
38
+ * Interface implementation
39
+ */
40
+
41
+ async function isUpdateAvailable(device, logger, requestPayload=null) {
42
+ return common.isUpdateAvailable(device, logger, common.isNewImageAvailable, requestPayload, getImageMeta);
43
+ }
44
+
45
+ async function updateToLatest(device, logger, onProgress) {
46
+ return common.updateToLatest(device, logger, onProgress, common.getNewImage, getImageMeta);
47
+ }
48
+
49
+ module.exports = {
50
+ isUpdateAvailable,
51
+ updateToLatest,
52
+ };
@@ -0,0 +1,57 @@
1
+ const firmwareOrigin = 'https://api.github.com/repos/fairecasoimeme/Zlinky_TIC/releases';
2
+ const assert = require('assert');
3
+ const common = require('./common');
4
+ const axios = common.getAxios();
5
+
6
+ /**
7
+ * Helper functions
8
+ */
9
+
10
+ async function getImageMeta(current, logger, device) {
11
+ const manufacturerCode = current.manufacturerCode;
12
+ const imageType = current.imageType;
13
+ const releasesLIST = (await axios.get(firmwareOrigin)).data;
14
+
15
+ let firmURL;
16
+
17
+ // Find the most recent OTA file available
18
+ for (const e of releasesLIST.sort((a, b) => a.published_at - a.published_at)) {
19
+ if (e.assets) {
20
+ const targetObj = e.assets
21
+ .find((a) => a.name.endsWith('.ota'));
22
+ if (targetObj && targetObj.browser_download_url) {
23
+ firmURL = targetObj;
24
+ break;
25
+ }
26
+ }
27
+ }
28
+
29
+ assert(firmURL,
30
+ `No image available for manufacturerCode '${manufacturerCode}' imageType '${imageType} on Github repo'`);
31
+
32
+ logger.info(`Using firmware file ` + firmURL.name);
33
+ const image = common.parseImage((await common.getAxios().get(firmURL.browser_download_url, {responseType: 'arraybuffer'})).data);
34
+
35
+ return {
36
+ fileVersion: image.header.fileVersion,
37
+ fileSize: firmURL.size,
38
+ url: firmURL.browser_download_url,
39
+ };
40
+ }
41
+
42
+ /**
43
+ * Interface implementation
44
+ */
45
+
46
+ async function isUpdateAvailable(device, logger, requestPayload=null) {
47
+ return common.isUpdateAvailable(device, logger, common.isNewImageAvailable, requestPayload, getImageMeta);
48
+ }
49
+
50
+ async function updateToLatest(device, logger, onProgress) {
51
+ return common.updateToLatest(device, logger, onProgress, common.getNewImage, getImageMeta);
52
+ }
53
+
54
+ module.exports = {
55
+ isUpdateAvailable,
56
+ updateToLatest,
57
+ };