zigbee-herdsman-converters 25.112.0 → 25.114.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/dist/devices/awox.d.ts.map +1 -1
- package/dist/devices/awox.js +10 -2
- package/dist/devices/awox.js.map +1 -1
- package/dist/devices/engo.js +1 -1
- package/dist/devices/engo.js.map +1 -1
- package/dist/devices/namron.d.ts.map +1 -1
- package/dist/devices/namron.js +1 -2
- package/dist/devices/namron.js.map +1 -1
- package/dist/devices/philips.d.ts.map +1 -1
- package/dist/devices/philips.js +34 -17
- package/dist/devices/philips.js.map +1 -1
- package/dist/devices/shelly.d.ts.map +1 -1
- package/dist/devices/shelly.js +85 -0
- package/dist/devices/shelly.js.map +1 -1
- package/dist/devices/smartthings.js +1 -1
- package/dist/devices/smartthings.js.map +1 -1
- package/dist/devices/sonoff.d.ts.map +1 -1
- package/dist/devices/sonoff.js +1 -0
- package/dist/devices/sonoff.js.map +1 -1
- package/dist/devices/tuya.d.ts.map +1 -1
- package/dist/devices/tuya.js +21 -17
- package/dist/devices/tuya.js.map +1 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/ledvance.d.ts.map +1 -1
- package/dist/lib/ledvance.js +2 -5
- package/dist/lib/ledvance.js.map +1 -1
- package/dist/lib/lumi.d.ts.map +1 -1
- package/dist/lib/lumi.js +32 -11
- package/dist/lib/lumi.js.map +1 -1
- package/dist/lib/types.d.ts +4 -78
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/models-index.json +1 -1
- package/package.json +2 -2
- package/dist/lib/ota.d.ts +0 -73
- package/dist/lib/ota.d.ts.map +0 -1
- package/dist/lib/ota.js +0 -697
- package/dist/lib/ota.js.map +0 -1
package/dist/lib/ota.js
DELETED
|
@@ -1,697 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.UPGRADE_FILE_IDENTIFIER = exports.DEFAULT_IMAGE_BLOCK_RESPONSE_DELAY = exports.DEFAULT_MAXIMUM_DATA_SIZE = exports.ZIGBEE_OTA_PREVIOUS_URL = exports.ZIGBEE_OTA_LATEST_URL = void 0;
|
|
7
|
-
exports.setConfiguration = setConfiguration;
|
|
8
|
-
exports.isValidUrl = isValidUrl;
|
|
9
|
-
exports.parseImage = parseImage;
|
|
10
|
-
exports.isUpdateAvailable = isUpdateAvailable;
|
|
11
|
-
exports.update = update;
|
|
12
|
-
const node_assert_1 = __importDefault(require("node:assert"));
|
|
13
|
-
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
14
|
-
const node_fs_1 = require("node:fs");
|
|
15
|
-
const node_path_1 = __importDefault(require("node:path"));
|
|
16
|
-
const node_zlib_1 = require("node:zlib");
|
|
17
|
-
const zigbee_herdsman_1 = require("zigbee-herdsman");
|
|
18
|
-
const logger_1 = require("./logger");
|
|
19
|
-
const NS = "zhc:ota";
|
|
20
|
-
exports.ZIGBEE_OTA_LATEST_URL = "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/index.json";
|
|
21
|
-
exports.ZIGBEE_OTA_PREVIOUS_URL = "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/index1.json";
|
|
22
|
-
/** +- 24 days */
|
|
23
|
-
const MAX_TIMEOUT = 2147483647;
|
|
24
|
-
/** When the data size is too big, OTA gets unstable, so default it to 50 bytes maximum. */
|
|
25
|
-
exports.DEFAULT_MAXIMUM_DATA_SIZE = 50;
|
|
26
|
-
/** Use to reduce network congestion by throttling response if necessary */
|
|
27
|
-
exports.DEFAULT_IMAGE_BLOCK_RESPONSE_DELAY = 250;
|
|
28
|
-
/** Consider update done after this amount of time without having seen a deviceAnnounce */
|
|
29
|
-
const UPDATE_END_FORCE_RESOLVE_TIME = 120 * 1000;
|
|
30
|
-
exports.UPGRADE_FILE_IDENTIFIER = Buffer.from([0x1e, 0xf1, 0xee, 0x0b]);
|
|
31
|
-
const VALID_SILABS_CRC = 0x2144df1c;
|
|
32
|
-
const EBL_TAG_HEADER = 0x0;
|
|
33
|
-
const EBL_TAG_ENC_HEADER = 0xfb05;
|
|
34
|
-
const EBL_TAG_END = 0xfc04;
|
|
35
|
-
const EBL_PADDING = 0xff;
|
|
36
|
-
const EBL_IMAGE_SIGNATURE = 0xe350;
|
|
37
|
-
const GBL_HEADER_TAG = Buffer.from([0xeb, 0x17, 0xa6, 0x03]);
|
|
38
|
-
/** Contains length+CRC32 and possibly padding after this. */
|
|
39
|
-
const GBL_END_TAG = Buffer.from([0xfc, 0x04, 0x04, 0xfc]);
|
|
40
|
-
// #region Configuration
|
|
41
|
-
let dataDir;
|
|
42
|
-
let overrideIndexFileName;
|
|
43
|
-
let imageBlockResponseDelay = exports.DEFAULT_IMAGE_BLOCK_RESPONSE_DELAY;
|
|
44
|
-
let initialMaximumDataSize = exports.DEFAULT_MAXIMUM_DATA_SIZE;
|
|
45
|
-
function setConfiguration(settings) {
|
|
46
|
-
dataDir = settings.dataDir;
|
|
47
|
-
overrideIndexFileName = settings.overrideIndexLocation;
|
|
48
|
-
// use || no zero values
|
|
49
|
-
imageBlockResponseDelay = settings.imageBlockResponseDelay || exports.DEFAULT_IMAGE_BLOCK_RESPONSE_DELAY;
|
|
50
|
-
initialMaximumDataSize = settings.defaultMaximumDataSize || exports.DEFAULT_MAXIMUM_DATA_SIZE;
|
|
51
|
-
}
|
|
52
|
-
// #endregion
|
|
53
|
-
// #region General Utils
|
|
54
|
-
function isValidUrl(url) {
|
|
55
|
-
try {
|
|
56
|
-
const parsed = new URL(url);
|
|
57
|
-
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
|
58
|
-
}
|
|
59
|
-
catch {
|
|
60
|
-
return false;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
async function getJson(pageUrl) {
|
|
64
|
-
const response = await fetch(pageUrl);
|
|
65
|
-
if (!response.ok || !response.body) {
|
|
66
|
-
throw new Error(`Invalid response from ${pageUrl} status=${response.status}.`);
|
|
67
|
-
}
|
|
68
|
-
return (await response.json());
|
|
69
|
-
}
|
|
70
|
-
function readLocalFile(fileName) {
|
|
71
|
-
// If the file name is not a full path, then treat it as a relative to the data directory
|
|
72
|
-
if (!node_path_1.default.isAbsolute(fileName) && dataDir) {
|
|
73
|
-
fileName = node_path_1.default.join(dataDir, fileName);
|
|
74
|
-
}
|
|
75
|
-
logger_1.logger.debug(`Getting local firmware file '${fileName}'`, NS);
|
|
76
|
-
return (0, node_fs_1.readFileSync)(fileName);
|
|
77
|
-
}
|
|
78
|
-
async function getFirmwareFile(meta) {
|
|
79
|
-
const urlOrName = meta.url;
|
|
80
|
-
// First try to download firmware file with the URL provided
|
|
81
|
-
if (isValidUrl(urlOrName)) {
|
|
82
|
-
logger_1.logger.debug(`Downloading firmware image from '${urlOrName}'`, NS);
|
|
83
|
-
const firmwareFileRsp = await fetch(urlOrName);
|
|
84
|
-
if (!firmwareFileRsp.ok || !firmwareFileRsp.body) {
|
|
85
|
-
throw new Error(`Invalid response from ${urlOrName} status=${firmwareFileRsp.status}.`);
|
|
86
|
-
}
|
|
87
|
-
return Buffer.from(await firmwareFileRsp.arrayBuffer());
|
|
88
|
-
}
|
|
89
|
-
logger_1.logger.debug(`Try to read firmware image from local file '${urlOrName}'`, NS);
|
|
90
|
-
return readLocalFile(urlOrName);
|
|
91
|
-
}
|
|
92
|
-
// #endregion
|
|
93
|
-
// #region OTA Utils
|
|
94
|
-
function parseSubElement(buffer, position) {
|
|
95
|
-
const tagID = buffer.readUInt16LE(position);
|
|
96
|
-
const length = buffer.readUInt32LE(position + 2);
|
|
97
|
-
const data = buffer.subarray(position + 6, position + 6 + length);
|
|
98
|
-
return { tagID, length, data };
|
|
99
|
-
}
|
|
100
|
-
function parseTelinkEncryptSubElement(buffer, position) {
|
|
101
|
-
const tagID = buffer.readUInt16LE(position);
|
|
102
|
-
const length = buffer.readUInt32LE(position + 2);
|
|
103
|
-
// const tagInfo = buffer.readUInt32LE(position + 4);
|
|
104
|
-
const data = buffer.subarray(position + 8, position + 8 + length);
|
|
105
|
-
return { tagID, length, data };
|
|
106
|
-
}
|
|
107
|
-
function parseImage(buffer, suppressElementImageParseFailure = false, customParseLogic = undefined) {
|
|
108
|
-
logger_1.logger.debug(`Parsing image, size=${buffer.length}, suppressElementImageParseFailure=${suppressElementImageParseFailure}, customParseLogic=${customParseLogic}`, NS);
|
|
109
|
-
const header = {
|
|
110
|
-
otaUpgradeFileIdentifier: buffer.subarray(0, 4),
|
|
111
|
-
otaHeaderVersion: buffer.readUInt16LE(4),
|
|
112
|
-
otaHeaderLength: buffer.readUInt16LE(6),
|
|
113
|
-
otaHeaderFieldControl: buffer.readUInt16LE(8),
|
|
114
|
-
manufacturerCode: buffer.readUInt16LE(10),
|
|
115
|
-
imageType: buffer.readUInt16LE(12),
|
|
116
|
-
fileVersion: buffer.readUInt32LE(14),
|
|
117
|
-
zigbeeStackVersion: buffer.readUInt16LE(18),
|
|
118
|
-
otaHeaderString: buffer.toString("utf8", 20, 52),
|
|
119
|
-
totalImageSize: buffer.readUInt32LE(52),
|
|
120
|
-
};
|
|
121
|
-
let headerPos = 56;
|
|
122
|
-
let didSuppressElementImageParseFailure = false;
|
|
123
|
-
/* istanbul ignore next */
|
|
124
|
-
if (header.otaHeaderFieldControl & 1) {
|
|
125
|
-
header.securityCredentialVersion = buffer.readUInt8(headerPos);
|
|
126
|
-
headerPos += 1;
|
|
127
|
-
}
|
|
128
|
-
/* istanbul ignore next */
|
|
129
|
-
if (header.otaHeaderFieldControl & 2) {
|
|
130
|
-
header.upgradeFileDestination = buffer.subarray(headerPos, headerPos + 8);
|
|
131
|
-
headerPos += 8;
|
|
132
|
-
}
|
|
133
|
-
if (header.otaHeaderFieldControl & 4) {
|
|
134
|
-
header.minimumHardwareVersion = buffer.readUInt16LE(headerPos);
|
|
135
|
-
headerPos += 2;
|
|
136
|
-
header.maximumHardwareVersion = buffer.readUInt16LE(headerPos);
|
|
137
|
-
headerPos += 2;
|
|
138
|
-
}
|
|
139
|
-
const raw = buffer.subarray(0, header.totalImageSize);
|
|
140
|
-
// Note: in the context of this file, this can never assert, since both callers of `parseImage` already subarray to `UPGRADE_FILE_IDENTIFIER`
|
|
141
|
-
(0, node_assert_1.default)(exports.UPGRADE_FILE_IDENTIFIER.equals(header.otaUpgradeFileIdentifier), "Not a valid OTA file");
|
|
142
|
-
let position = header.otaHeaderLength;
|
|
143
|
-
const elements = [];
|
|
144
|
-
try {
|
|
145
|
-
while (position < header.totalImageSize) {
|
|
146
|
-
// Use the selected parser function
|
|
147
|
-
let element;
|
|
148
|
-
let elementOffset = 6;
|
|
149
|
-
if (customParseLogic === "telinkEncrypted") {
|
|
150
|
-
element = parseTelinkEncryptSubElement(buffer, position);
|
|
151
|
-
elementOffset = 8;
|
|
152
|
-
}
|
|
153
|
-
else {
|
|
154
|
-
element = parseSubElement(buffer, position);
|
|
155
|
-
}
|
|
156
|
-
elements.push(element);
|
|
157
|
-
position += element.data.length + elementOffset;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
catch (error) {
|
|
161
|
-
if (!suppressElementImageParseFailure) {
|
|
162
|
-
throw error;
|
|
163
|
-
}
|
|
164
|
-
didSuppressElementImageParseFailure = true;
|
|
165
|
-
logger_1.logger.error("Partially failed to parse the image, continuing anyway...", NS);
|
|
166
|
-
}
|
|
167
|
-
if (!didSuppressElementImageParseFailure) {
|
|
168
|
-
(0, node_assert_1.default)(position === header.totalImageSize, "Size mismatch");
|
|
169
|
-
}
|
|
170
|
-
return { header, elements, raw };
|
|
171
|
-
}
|
|
172
|
-
function validateImageData(image) {
|
|
173
|
-
for (const element of image.elements) {
|
|
174
|
-
const { data } = element;
|
|
175
|
-
if (data.indexOf(GBL_HEADER_TAG) === 0) {
|
|
176
|
-
validateSilabsGbl(data);
|
|
177
|
-
}
|
|
178
|
-
else {
|
|
179
|
-
const tag = data.readUInt16BE(0);
|
|
180
|
-
/* istanbul ignore next */
|
|
181
|
-
if ((tag === EBL_TAG_HEADER && data.readUInt16BE(6) === EBL_IMAGE_SIGNATURE) || tag === EBL_TAG_ENC_HEADER) {
|
|
182
|
-
validateSilabsEbl(data);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
/* istanbul ignore next */
|
|
188
|
-
function validateSilabsEbl(data) {
|
|
189
|
-
const dataLength = data.length;
|
|
190
|
-
let position = 0;
|
|
191
|
-
while (position + 4 <= dataLength) {
|
|
192
|
-
const tag = data.readUInt16BE(position);
|
|
193
|
-
const len = data.readUInt16BE(position + 2);
|
|
194
|
-
position += 4 + len;
|
|
195
|
-
if (tag !== EBL_TAG_END) {
|
|
196
|
-
continue;
|
|
197
|
-
}
|
|
198
|
-
for (let position2 = position; position2 < dataLength; position2++) {
|
|
199
|
-
(0, node_assert_1.default)(data.readUInt8(position2) === EBL_PADDING, "Image padding contains invalid bytes");
|
|
200
|
-
}
|
|
201
|
-
const calculatedCrc32 = (0, node_zlib_1.crc32)(data.subarray(0, position));
|
|
202
|
-
(0, node_assert_1.default)(calculatedCrc32 === VALID_SILABS_CRC, "Image CRC-32 is invalid");
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
throw new Error("Image is truncated, not long enough to contain a valid tag");
|
|
206
|
-
}
|
|
207
|
-
function validateSilabsGbl(data) {
|
|
208
|
-
(0, node_assert_1.default)(data.indexOf(GBL_HEADER_TAG) === 0, "Not a valid GBL image");
|
|
209
|
-
const gblEndTagIndex = data.lastIndexOf(GBL_END_TAG);
|
|
210
|
-
(0, node_assert_1.default)(gblEndTagIndex > 16, "Not a valid GBL image"); // after HEADER, just because...
|
|
211
|
-
const gblEnd = gblEndTagIndex + 12; // tag + length + crc32 (4*3)
|
|
212
|
-
// ignore possible padding
|
|
213
|
-
const calculatedCrc32 = (0, node_zlib_1.crc32)(data.subarray(0, gblEnd));
|
|
214
|
-
(0, node_assert_1.default)(calculatedCrc32 === VALID_SILABS_CRC, "Image CRC-32 is invalid");
|
|
215
|
-
}
|
|
216
|
-
function fillImageInfo(meta) {
|
|
217
|
-
// Web-hosted images must come with all fields filled already
|
|
218
|
-
if (isValidUrl(meta.url)) {
|
|
219
|
-
return meta;
|
|
220
|
-
}
|
|
221
|
-
// Nothing to do if needed fields were filled already
|
|
222
|
-
if (meta.imageType !== undefined && meta.manufacturerCode !== undefined && meta.fileVersion !== undefined) {
|
|
223
|
-
return meta;
|
|
224
|
-
}
|
|
225
|
-
// If no fields provided - get them from the image file
|
|
226
|
-
const imageFile = readLocalFile(meta.url);
|
|
227
|
-
const otaIdentifier = imageFile.indexOf(exports.UPGRADE_FILE_IDENTIFIER);
|
|
228
|
-
(0, node_assert_1.default)(otaIdentifier !== -1, "Not a valid OTA file");
|
|
229
|
-
// allow bypass non-spec Ledvance OTA files if proper manufacturer set
|
|
230
|
-
const image = parseImage(imageFile.subarray(otaIdentifier), meta.manufacturerCode === zigbee_herdsman_1.Zcl.ManufacturerCode.LEDVANCE_GMBH, meta.customParseLogic);
|
|
231
|
-
// Will fill only those fields that were absent
|
|
232
|
-
if (meta.imageType === undefined) {
|
|
233
|
-
meta.imageType = image.header.imageType;
|
|
234
|
-
}
|
|
235
|
-
if (meta.manufacturerCode === undefined) {
|
|
236
|
-
meta.manufacturerCode = image.header.manufacturerCode;
|
|
237
|
-
}
|
|
238
|
-
if (meta.fileVersion === undefined) {
|
|
239
|
-
meta.fileVersion = image.header.fileVersion;
|
|
240
|
-
}
|
|
241
|
-
return meta;
|
|
242
|
-
}
|
|
243
|
-
async function getIndex(previous) {
|
|
244
|
-
const mainIndex = await getJson(previous ? exports.ZIGBEE_OTA_PREVIOUS_URL : exports.ZIGBEE_OTA_LATEST_URL);
|
|
245
|
-
logger_1.logger.debug("Downloaded main index", NS);
|
|
246
|
-
if (overrideIndexFileName) {
|
|
247
|
-
logger_1.logger.debug(`Loading override index '${overrideIndexFileName}'`, NS);
|
|
248
|
-
const localIndex = isValidUrl(overrideIndexFileName)
|
|
249
|
-
? await getJson(overrideIndexFileName)
|
|
250
|
-
: JSON.parse((0, node_fs_1.readFileSync)(overrideIndexFileName, "utf-8"));
|
|
251
|
-
// Resulting index will have overridden items first
|
|
252
|
-
return localIndex.map((image) => fillImageInfo(image)).concat(mainIndex);
|
|
253
|
-
}
|
|
254
|
-
return mainIndex;
|
|
255
|
-
}
|
|
256
|
-
function deviceLogString(device) {
|
|
257
|
-
return `[${device.ieeeAddr} | ${device.modelID}]`;
|
|
258
|
-
}
|
|
259
|
-
// #endregion
|
|
260
|
-
// #region OTA
|
|
261
|
-
function cancelWaiters(waiters) {
|
|
262
|
-
waiters.imageBlockOrPageRequest?.cancel();
|
|
263
|
-
waiters.upgradeEndRequest?.cancel();
|
|
264
|
-
}
|
|
265
|
-
function getOTAEndpoint(device) {
|
|
266
|
-
return device.endpoints.find((e) => e.supportsOutputCluster("genOta"));
|
|
267
|
-
}
|
|
268
|
-
async function sendQueryNextImageResponse(device, endpoint, image, requestTransactionSequenceNumber) {
|
|
269
|
-
const payload = image
|
|
270
|
-
? {
|
|
271
|
-
status: zigbee_herdsman_1.Zcl.Status.SUCCESS,
|
|
272
|
-
manufacturerCode: image.header.manufacturerCode,
|
|
273
|
-
imageType: image.header.imageType,
|
|
274
|
-
fileVersion: image.header.fileVersion,
|
|
275
|
-
imageSize: image.header.totalImageSize,
|
|
276
|
-
}
|
|
277
|
-
: { status: zigbee_herdsman_1.Zcl.Status.NO_IMAGE_AVAILABLE };
|
|
278
|
-
try {
|
|
279
|
-
await endpoint.commandResponse("genOta", "queryNextImageResponse", payload, undefined, requestTransactionSequenceNumber);
|
|
280
|
-
}
|
|
281
|
-
catch (error) {
|
|
282
|
-
logger_1.logger.debug(() => `${deviceLogString(device)} Failed to send queryNextImageResponse: ${error.message}`, NS);
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
async function imageNotify(endpoint) {
|
|
286
|
-
await endpoint.commandResponse("genOta", "imageNotify", { payloadType: 0, queryJitter: 100 }, { sendPolicy: "immediate" });
|
|
287
|
-
}
|
|
288
|
-
async function requestOTA(endpoint) {
|
|
289
|
-
// Some devices (e.g. Insta) take very long trying to discover the correct coordinator EP for OTA.
|
|
290
|
-
const queryNextImageRequest = endpoint.waitForCommand("genOta", "queryNextImageRequest", undefined, 60000);
|
|
291
|
-
try {
|
|
292
|
-
await imageNotify(endpoint);
|
|
293
|
-
const response = await queryNextImageRequest.promise;
|
|
294
|
-
return [response.header.transactionSequenceNumber, response.payload];
|
|
295
|
-
}
|
|
296
|
-
catch {
|
|
297
|
-
queryNextImageRequest.cancel();
|
|
298
|
-
throw new Error(`Device didn't respond to OTA request`);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
// this is not significant for tests, skipping coverage
|
|
302
|
-
/* istanbul ignore next */
|
|
303
|
-
function getInitialMaximumDataSize(imageBlockRequest) {
|
|
304
|
-
if (imageBlockRequest.payload.manufacturerCode === zigbee_herdsman_1.Zcl.ManufacturerCode.INSTA_GMBH) {
|
|
305
|
-
// Insta devices, OTA only works for data sizes 40 and smaller (= manufacturerCode 4474).
|
|
306
|
-
return 40;
|
|
307
|
-
}
|
|
308
|
-
if (imageBlockRequest.payload.manufacturerCode === zigbee_herdsman_1.Zcl.ManufacturerCode.LEGRAND_GROUP) {
|
|
309
|
-
// Legrand devices (newer firmware) require up to 64 bytes (= manufacturerCode 4129).
|
|
310
|
-
return Number.POSITIVE_INFINITY;
|
|
311
|
-
}
|
|
312
|
-
return initialMaximumDataSize;
|
|
313
|
-
}
|
|
314
|
-
function getImageBlockResponsePayload(device, image, imageBlockRequest, pageOffset, pageSize) {
|
|
315
|
-
const initialMaximumDataSize = getInitialMaximumDataSize(imageBlockRequest);
|
|
316
|
-
// Some devices send `imageBlockRequest.payload.maximumDataSize` 0xFF which is translated to `null` according to the spec.
|
|
317
|
-
// In that case default to the `initialMaximumDataSize`.
|
|
318
|
-
// e.g. Develco https://github.com/Koenkk/zigbee-OTA/issues/863#issuecomment-3736158829
|
|
319
|
-
let dataSize = Number.isFinite(imageBlockRequest.payload.maximumDataSize)
|
|
320
|
-
? Math.min(initialMaximumDataSize, imageBlockRequest.payload.maximumDataSize)
|
|
321
|
-
: initialMaximumDataSize;
|
|
322
|
-
let start = imageBlockRequest.payload.fileOffset + pageOffset;
|
|
323
|
-
// Hack for https://github.com/Koenkk/zigbee-OTA/issues/328 (Legrand OTA not working)
|
|
324
|
-
/* istanbul ignore next */
|
|
325
|
-
if (imageBlockRequest.payload.manufacturerCode === zigbee_herdsman_1.Zcl.ManufacturerCode.LEGRAND_GROUP &&
|
|
326
|
-
imageBlockRequest.payload.fileOffset === 50 &&
|
|
327
|
-
imageBlockRequest.payload.maximumDataSize === 12) {
|
|
328
|
-
logger_1.logger.info(() => `${deviceLogString(device)} Detected Legrand firmware issue, attempting to reset the OTA stack`, NS);
|
|
329
|
-
// The following vector seems to buffer overflow the device to reset the OTA stack!
|
|
330
|
-
start = 78;
|
|
331
|
-
dataSize = 64;
|
|
332
|
-
}
|
|
333
|
-
if (pageSize) {
|
|
334
|
-
dataSize = Math.min(dataSize, pageSize - pageOffset);
|
|
335
|
-
}
|
|
336
|
-
let end = start + dataSize;
|
|
337
|
-
if (end > image.raw.length) {
|
|
338
|
-
end = image.raw.length;
|
|
339
|
-
}
|
|
340
|
-
logger_1.logger.debug(() => `${deviceLogString(device)} Request offsets: fileOffset=${imageBlockRequest.payload.fileOffset} pageOffset=${pageOffset} maximumDataSize=${imageBlockRequest.payload.maximumDataSize}`, NS);
|
|
341
|
-
logger_1.logger.debug(() => `${deviceLogString(device)} Payload offsets: start=${start} end=${end} dataSize=${dataSize}`, NS);
|
|
342
|
-
return {
|
|
343
|
-
status: zigbee_herdsman_1.Zcl.Status.SUCCESS,
|
|
344
|
-
manufacturerCode: imageBlockRequest.payload.manufacturerCode,
|
|
345
|
-
imageType: imageBlockRequest.payload.imageType,
|
|
346
|
-
fileVersion: imageBlockRequest.payload.fileVersion,
|
|
347
|
-
fileOffset: start,
|
|
348
|
-
dataSize: end - start,
|
|
349
|
-
data: image.raw.subarray(start, end),
|
|
350
|
-
};
|
|
351
|
-
}
|
|
352
|
-
function callOnProgress(device, startTime, lastUpdate, imageBlockRequest, image, onProgress) {
|
|
353
|
-
const now = Date.now();
|
|
354
|
-
// Call on progress every +- 30 seconds
|
|
355
|
-
if (lastUpdate === undefined || now - lastUpdate > 30000) {
|
|
356
|
-
const totalDuration = (now - startTime) / 1000; // in seconds
|
|
357
|
-
const bytesPerSecond = imageBlockRequest.payload.fileOffset / totalDuration;
|
|
358
|
-
const remaining = (image.header.totalImageSize - imageBlockRequest.payload.fileOffset) / bytesPerSecond;
|
|
359
|
-
let percentage = imageBlockRequest.payload.fileOffset / image.header.totalImageSize;
|
|
360
|
-
percentage = Math.round(percentage * 10000) / 100;
|
|
361
|
-
logger_1.logger.debug(() => `${deviceLogString(device)} Update at ${percentage}%, remaining ${remaining} seconds`, NS);
|
|
362
|
-
onProgress(percentage, remaining === Number.POSITIVE_INFINITY ? undefined : remaining);
|
|
363
|
-
return now;
|
|
364
|
-
}
|
|
365
|
-
return lastUpdate;
|
|
366
|
-
}
|
|
367
|
-
async function getImageMeta(current, device, extraMetas, previous) {
|
|
368
|
-
logger_1.logger.debug(() => `${deviceLogString(device)} Getting image metadata...`, NS);
|
|
369
|
-
const images = await getIndex(previous);
|
|
370
|
-
// NOTE: Officially an image can be determined with a combination of manufacturerCode and imageType.
|
|
371
|
-
// However Gledopto pro products use the same imageType (0) for every device while the image is different.
|
|
372
|
-
// For this case additional identification through the modelId is done.
|
|
373
|
-
// In the case of Tuya and Moes, additional identification is carried out through the manufacturerName.
|
|
374
|
-
return images.find((i) => i.imageType === current.imageType &&
|
|
375
|
-
i.manufacturerCode === current.manufacturerCode &&
|
|
376
|
-
(i.minFileVersion === undefined || current.fileVersion >= i.minFileVersion) &&
|
|
377
|
-
(i.maxFileVersion === undefined || current.fileVersion <= i.maxFileVersion) &&
|
|
378
|
-
// let extra metas override the match from device.modelID, same for manufacturerName
|
|
379
|
-
(!i.modelId || i.modelId === device.modelID || i.modelId === extraMetas.modelId) &&
|
|
380
|
-
(!i.manufacturerName ||
|
|
381
|
-
// biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
|
|
382
|
-
i.manufacturerName.includes(device.manufacturerName) ||
|
|
383
|
-
// biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
|
|
384
|
-
i.manufacturerName.includes(extraMetas.manufacturerName)) &&
|
|
385
|
-
(!extraMetas.otaHeaderString || i.otaHeaderString === extraMetas.otaHeaderString) &&
|
|
386
|
-
(i.hardwareVersionMin === undefined ||
|
|
387
|
-
(current.hardwareVersion !== undefined && current.hardwareVersion >= i.hardwareVersionMin) ||
|
|
388
|
-
(extraMetas.hardwareVersionMin !== undefined && extraMetas.hardwareVersionMin >= i.hardwareVersionMin)) &&
|
|
389
|
-
(i.hardwareVersionMax === undefined ||
|
|
390
|
-
(current.hardwareVersion !== undefined && current.hardwareVersion <= i.hardwareVersionMax) ||
|
|
391
|
-
(extraMetas.hardwareVersionMax !== undefined && extraMetas.hardwareVersionMax <= i.hardwareVersionMax)));
|
|
392
|
-
}
|
|
393
|
-
async function isImageAvailable(current, device, extraMetas, previous) {
|
|
394
|
-
const imageSet = previous ? "previous" : "latest";
|
|
395
|
-
logger_1.logger.debug(() => `${deviceLogString(device)} Checking ${imageSet} image availability, current: ${JSON.stringify(current)}`, NS);
|
|
396
|
-
// biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
|
|
397
|
-
if (["lumi.airrtc.agl001", "lumi.curtain.acn003", "lumi.curtain.agl001"].includes(device.modelID)) {
|
|
398
|
-
// The current.fileVersion which comes from the device is wrong.
|
|
399
|
-
// Use the `lumiFileVersion` which comes from the manuSpecificLumi.attributeReport instead.
|
|
400
|
-
// https://github.com/Koenkk/zigbee2mqtt/issues/16345#issuecomment-1454835056
|
|
401
|
-
// https://github.com/Koenkk/zigbee2mqtt/issues/16345 doesn't seem to be needed for all
|
|
402
|
-
// https://github.com/Koenkk/zigbee2mqtt/issues/15745
|
|
403
|
-
if (device.meta.lumiFileVersion) {
|
|
404
|
-
current = { ...current, fileVersion: device.meta.lumiFileVersion };
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
const meta = await getImageMeta(current, device, extraMetas, previous);
|
|
408
|
-
// Soft-fail because no images in repo/URL for specified device
|
|
409
|
-
if (!meta) {
|
|
410
|
-
logger_1.logger.debug(() => `${deviceLogString(device)} No ${imageSet} image currently available, current: ${JSON.stringify(current)}'`, NS);
|
|
411
|
-
return {
|
|
412
|
-
available: 0,
|
|
413
|
-
currentFileVersion: current.fileVersion,
|
|
414
|
-
otaFileVersion: current.fileVersion,
|
|
415
|
-
};
|
|
416
|
-
}
|
|
417
|
-
logger_1.logger.debug(() => `${deviceLogString(device)} Result for ${imageSet} image availability, meta: '${JSON.stringify(meta)}'`, NS);
|
|
418
|
-
/* istanbul ignore next */
|
|
419
|
-
if (meta.releaseNotes) {
|
|
420
|
-
logger_1.logger.info(() => `${deviceLogString(device)} Firmware release notes: ${
|
|
421
|
-
// biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
|
|
422
|
-
meta.releaseNotes.replace(/[\r\n]/g, "")}`, NS);
|
|
423
|
-
}
|
|
424
|
-
// Negative number means the firmware is 'newer' than current one
|
|
425
|
-
// Positive number means the firmware is 'older' than current one
|
|
426
|
-
return {
|
|
427
|
-
available: meta.force ? -1 : Math.sign(current.fileVersion - meta.fileVersion),
|
|
428
|
-
currentFileVersion: current.fileVersion,
|
|
429
|
-
otaFileVersion: meta.fileVersion,
|
|
430
|
-
};
|
|
431
|
-
}
|
|
432
|
-
async function getImage(current, device, extraMetas, previous) {
|
|
433
|
-
const meta = await getImageMeta(current, device, extraMetas, previous);
|
|
434
|
-
if (!meta) {
|
|
435
|
-
throw new Error(`${deviceLogString(device)} No image currently available`);
|
|
436
|
-
}
|
|
437
|
-
const imageSet = previous ? "previous" : "latest";
|
|
438
|
-
logger_1.logger.info(() => `${deviceLogString(device)} Getting ${imageSet} image, meta: ${JSON.stringify(meta)}`, NS);
|
|
439
|
-
if (previous) {
|
|
440
|
-
(0, node_assert_1.default)(meta.fileVersion < current.fileVersion || meta.force, "No previous image available");
|
|
441
|
-
}
|
|
442
|
-
else {
|
|
443
|
-
(0, node_assert_1.default)(meta.fileVersion > current.fileVersion || meta.force, "No new image available");
|
|
444
|
-
}
|
|
445
|
-
const downloadedFile = await getFirmwareFile(meta);
|
|
446
|
-
if (meta.sha512) {
|
|
447
|
-
const hash = node_crypto_1.default.createHash("sha512");
|
|
448
|
-
hash.update(downloadedFile);
|
|
449
|
-
(0, node_assert_1.default)(hash.digest("hex") === meta.sha512, "File checksum validation failed");
|
|
450
|
-
logger_1.logger.debug(() => `${deviceLogString(device)} Image checksum validation succeeded.`, NS);
|
|
451
|
-
}
|
|
452
|
-
const otaIdentifier = downloadedFile.indexOf(exports.UPGRADE_FILE_IDENTIFIER);
|
|
453
|
-
(0, node_assert_1.default)(otaIdentifier !== -1, "Not a valid OTA file");
|
|
454
|
-
const image = parseImage(downloadedFile.subarray(otaIdentifier), extraMetas.suppressElementImageParseFailure || false, meta.customParseLogic);
|
|
455
|
-
logger_1.logger.debug(() => `${deviceLogString(device)} Got ${imageSet} image, header: ${JSON.stringify(image.header)}`, NS);
|
|
456
|
-
(0, node_assert_1.default)(image.header.fileVersion === meta.fileVersion, "File version mismatch");
|
|
457
|
-
(0, node_assert_1.default)(!meta.fileSize || image.header.totalImageSize === meta.fileSize, "Image size mismatch");
|
|
458
|
-
(0, node_assert_1.default)(image.header.manufacturerCode === current.manufacturerCode, "Manufacturer code mismatch");
|
|
459
|
-
(0, node_assert_1.default)(image.header.imageType === current.imageType, "Image type mismatch");
|
|
460
|
-
// this is only reachable if manifest is missing hardwareVersionMin/Max
|
|
461
|
-
if ("minimumHardwareVersion" in image.header &&
|
|
462
|
-
image.header.minimumHardwareVersion !== undefined &&
|
|
463
|
-
"maximumHardwareVersion" in image.header &&
|
|
464
|
-
image.header.maximumHardwareVersion !== undefined) {
|
|
465
|
-
(0, node_assert_1.default)(current.hardwareVersion !== undefined, "Hardware version required");
|
|
466
|
-
(0, node_assert_1.default)(image.header.minimumHardwareVersion <= current.hardwareVersion && current.hardwareVersion <= image.header.maximumHardwareVersion, "Hardware version mismatch");
|
|
467
|
-
}
|
|
468
|
-
validateImageData(image);
|
|
469
|
-
return image;
|
|
470
|
-
}
|
|
471
|
-
async function isUpdateAvailable(device, extraMetas, requestPayload, previous) {
|
|
472
|
-
logger_1.logger.debug(() => `${deviceLogString(device)} Checking if an update is available`, NS);
|
|
473
|
-
if (device.modelID === "PP-WHT-US") {
|
|
474
|
-
// see https://github.com/Koenkk/zigbee-OTA/pull/14
|
|
475
|
-
const scenesEndpoint = device.endpoints.find((e) => e.supportsOutputCluster("genScenes"));
|
|
476
|
-
if (scenesEndpoint) {
|
|
477
|
-
await scenesEndpoint.write("genScenes", { currentGroup: 49502 });
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
if (requestPayload === undefined) {
|
|
481
|
-
const endpoint = getOTAEndpoint(device);
|
|
482
|
-
(0, node_assert_1.default)(endpoint !== undefined, `${deviceLogString(device)} Failed to find an endpoint which supports the OTA cluster`);
|
|
483
|
-
logger_1.logger.debug(() => `${deviceLogString(device)} Using endpoint '${endpoint.ID}'`, NS);
|
|
484
|
-
const [, payload] = await requestOTA(endpoint);
|
|
485
|
-
logger_1.logger.debug(() => `${deviceLogString(device)} Got request '${JSON.stringify(payload)}'`, NS);
|
|
486
|
-
requestPayload = payload;
|
|
487
|
-
}
|
|
488
|
-
const availableResult = await isImageAvailable(requestPayload, device, extraMetas, previous);
|
|
489
|
-
let available = false;
|
|
490
|
-
if (previous) {
|
|
491
|
-
available = availableResult.available > 0;
|
|
492
|
-
logger_1.logger.debug(() => `${deviceLogString(device)} Downgrade available: ${available ? "YES" : "NO"}`, NS);
|
|
493
|
-
}
|
|
494
|
-
else {
|
|
495
|
-
available = availableResult.available < 0;
|
|
496
|
-
logger_1.logger.debug(() => `${deviceLogString(device)} Upgrade available: ${available ? "YES" : "NO"}`, NS);
|
|
497
|
-
if (availableResult.available > 0) {
|
|
498
|
-
logger_1.logger.warning(() => `${deviceLogString(device)} Firmware is newer than latest available firmware.`, NS);
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
return { ...availableResult, available };
|
|
502
|
-
}
|
|
503
|
-
// this is not significant for tests, skipping coverage
|
|
504
|
-
/* istanbul ignore next */
|
|
505
|
-
function getImageBlockOrPageRequestTimeoutMs(requestPayload) {
|
|
506
|
-
// increase the upgradeEndReq wait time to solve the problem of OTA timeout failure of Sonoff Devices
|
|
507
|
-
// (https://github.com/Koenkk/zigbee-herdsman-converters/issues/6657)
|
|
508
|
-
if (requestPayload.manufacturerCode === zigbee_herdsman_1.Zcl.ManufacturerCode.SHENZHEN_COOLKIT_TECHNOLOGY_CO_LTD && requestPayload.imageType === 8199) {
|
|
509
|
-
return 3600000;
|
|
510
|
-
}
|
|
511
|
-
// Bosch transmits the firmware updates in the background in their native implementation.
|
|
512
|
-
// According to the app, this can take up to 2 days. Therefore, we assume to get at least
|
|
513
|
-
// one package request per hour from the device here.
|
|
514
|
-
if (requestPayload.manufacturerCode === zigbee_herdsman_1.Zcl.ManufacturerCode.ROBERT_BOSCH_GMBH) {
|
|
515
|
-
return 60 * 60 * 1000;
|
|
516
|
-
}
|
|
517
|
-
// Increase the timeout for Legrand devices, so that they will re-initiate and update themselves
|
|
518
|
-
// Newer firmwares have awkward behaviours when it comes to the handling of the last bytes of OTA updates
|
|
519
|
-
if (requestPayload.manufacturerCode === zigbee_herdsman_1.Zcl.ManufacturerCode.LEGRAND_GROUP) {
|
|
520
|
-
return 30 * 60 * 1000;
|
|
521
|
-
}
|
|
522
|
-
return 150000;
|
|
523
|
-
}
|
|
524
|
-
async function update(device, extraMetas, previous, onProgress, requestPayload, reqTransNum) {
|
|
525
|
-
const imageSet = previous ? "previous" : "latest";
|
|
526
|
-
logger_1.logger.debug(() => `${deviceLogString(device)} Updating to ${imageSet}`, NS);
|
|
527
|
-
const endpoint = getOTAEndpoint(device);
|
|
528
|
-
(0, node_assert_1.default)(endpoint !== undefined, `${deviceLogString(device)} Failed to find an endpoint which supports the OTA cluster`);
|
|
529
|
-
logger_1.logger.debug(() => `${deviceLogString(device)} Using endpoint '${endpoint.ID}'`, NS);
|
|
530
|
-
if (device.modelID === "PP-WHT-US") {
|
|
531
|
-
// see https://github.com/Koenkk/zigbee-OTA/pull/14
|
|
532
|
-
const scenesEndpoint = device.endpoints.find((e) => e.supportsOutputCluster("genScenes"));
|
|
533
|
-
if (scenesEndpoint) {
|
|
534
|
-
await scenesEndpoint.write("genScenes", { currentGroup: 49502 });
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
if (!requestPayload) {
|
|
538
|
-
[reqTransNum, requestPayload] = await requestOTA(endpoint);
|
|
539
|
-
logger_1.logger.debug(() => `${deviceLogString(device)} Got request payload '${JSON.stringify(requestPayload)}'`, NS);
|
|
540
|
-
}
|
|
541
|
-
let image;
|
|
542
|
-
try {
|
|
543
|
-
image = await getImage(requestPayload, device, extraMetas, previous);
|
|
544
|
-
logger_1.logger.debug(() => `${deviceLogString(device)} Got ${imageSet} image`, NS);
|
|
545
|
-
}
|
|
546
|
-
catch (error) {
|
|
547
|
-
logger_1.logger.info(() => `${deviceLogString(device)} No image currently available (${error.message})`, NS);
|
|
548
|
-
}
|
|
549
|
-
// reply to `queryNextImageRequest` in `requestOTA` now that we have the data for it,
|
|
550
|
-
// should trigger image block/page request from device
|
|
551
|
-
await sendQueryNextImageResponse(device, endpoint, image, reqTransNum);
|
|
552
|
-
if (!image) {
|
|
553
|
-
return undefined;
|
|
554
|
-
}
|
|
555
|
-
const waiters = {};
|
|
556
|
-
let lastBlockResponseTime = 0;
|
|
557
|
-
let lastBlockTimeout;
|
|
558
|
-
let lastUpdate;
|
|
559
|
-
const startTime = Date.now();
|
|
560
|
-
const sendImageBlockResponse = async (imageBlockRequest, pageOffset, pageSize) => {
|
|
561
|
-
// Reduce network congestion by throttling response if necessary
|
|
562
|
-
{
|
|
563
|
-
clearTimeout(lastBlockTimeout);
|
|
564
|
-
const now = Date.now();
|
|
565
|
-
const timeSinceLast = now - lastBlockResponseTime;
|
|
566
|
-
const delay = imageBlockResponseDelay - timeSinceLast;
|
|
567
|
-
if (delay <= 0) {
|
|
568
|
-
lastBlockResponseTime = now;
|
|
569
|
-
}
|
|
570
|
-
else {
|
|
571
|
-
await new Promise((resolve) => {
|
|
572
|
-
lastBlockTimeout = setTimeout(() => {
|
|
573
|
-
lastBlockResponseTime = Date.now();
|
|
574
|
-
resolve();
|
|
575
|
-
}, delay);
|
|
576
|
-
});
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
try {
|
|
580
|
-
const blockPayload = getImageBlockResponsePayload(device, image, imageBlockRequest, pageOffset, pageSize);
|
|
581
|
-
await endpoint.commandResponse("genOta", "imageBlockResponse", blockPayload, undefined, imageBlockRequest.header.transactionSequenceNumber);
|
|
582
|
-
pageOffset += blockPayload.dataSize;
|
|
583
|
-
}
|
|
584
|
-
catch (error) {
|
|
585
|
-
// Shit happens, device will probably do a new imageBlockRequest so don't care.
|
|
586
|
-
logger_1.logger.debug(() => `${deviceLogString(device)} Image block response failed: ${error.message}`, NS);
|
|
587
|
-
}
|
|
588
|
-
lastUpdate = callOnProgress(device, startTime, lastUpdate, imageBlockRequest, image, onProgress);
|
|
589
|
-
return pageOffset;
|
|
590
|
-
};
|
|
591
|
-
let done = false;
|
|
592
|
-
const imageBlockOrPageRequestTimeoutMs = getImageBlockOrPageRequestTimeoutMs(requestPayload);
|
|
593
|
-
/** recursive, endless (expects `upgradeEndRequest` to stop it, or anything that sets done=true) */
|
|
594
|
-
const sendImageChunks = async () => {
|
|
595
|
-
while (!done) {
|
|
596
|
-
const imageBlockRequest = endpoint.waitForCommand("genOta", "imageBlockRequest", undefined, imageBlockOrPageRequestTimeoutMs);
|
|
597
|
-
const imagePageRequest = endpoint.waitForCommand("genOta", "imagePageRequest", undefined, imageBlockOrPageRequestTimeoutMs);
|
|
598
|
-
waiters.imageBlockOrPageRequest = {
|
|
599
|
-
promise: Promise.race([imageBlockRequest.promise, imagePageRequest.promise]),
|
|
600
|
-
cancel: () => {
|
|
601
|
-
imageBlockRequest.cancel();
|
|
602
|
-
imagePageRequest.cancel();
|
|
603
|
-
},
|
|
604
|
-
};
|
|
605
|
-
try {
|
|
606
|
-
const result = await waiters.imageBlockOrPageRequest.promise;
|
|
607
|
-
let pageSize = 0;
|
|
608
|
-
let pageOffset = 0;
|
|
609
|
-
if ("pageSize" in result.payload) {
|
|
610
|
-
// TODO: `result.payload.responseSpacing` support?
|
|
611
|
-
// imagePageRequest
|
|
612
|
-
pageSize = result.payload.pageSize;
|
|
613
|
-
while (pageOffset < pageSize) {
|
|
614
|
-
// in case upgradeEndRequest resolves, bail early (quirks)
|
|
615
|
-
if (done) {
|
|
616
|
-
return;
|
|
617
|
-
}
|
|
618
|
-
pageOffset = await sendImageBlockResponse(result, pageOffset, pageSize);
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
else {
|
|
622
|
-
// imageBlockRequest
|
|
623
|
-
pageOffset = await sendImageBlockResponse(result, pageOffset, pageSize);
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
catch (error) {
|
|
627
|
-
cancelWaiters(waiters);
|
|
628
|
-
throw new Error(`${deviceLogString(device)} Timeout. Device did not start/finish firmware download after being notified. (${error.message})`);
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
};
|
|
632
|
-
logger_1.logger.debug(() => `${deviceLogString(device)} Starting update`, NS);
|
|
633
|
-
waiters.upgradeEndRequest = endpoint.waitForCommand("genOta", "upgradeEndRequest", undefined, MAX_TIMEOUT);
|
|
634
|
-
await Promise.race([
|
|
635
|
-
sendImageChunks(),
|
|
636
|
-
waiters.upgradeEndRequest.promise.finally(() => {
|
|
637
|
-
clearTimeout(lastBlockResponseTime);
|
|
638
|
-
// always clear state
|
|
639
|
-
cancelWaiters(waiters);
|
|
640
|
-
done = true;
|
|
641
|
-
}),
|
|
642
|
-
]);
|
|
643
|
-
// already resolved when this is reached
|
|
644
|
-
const endResult = await waiters.upgradeEndRequest.promise;
|
|
645
|
-
logger_1.logger.debug(() => `${deviceLogString(device)} Got upgrade end request: ${JSON.stringify(endResult.payload)}`, NS);
|
|
646
|
-
if (endResult.payload.status === zigbee_herdsman_1.Zcl.Status.SUCCESS) {
|
|
647
|
-
const payload = {
|
|
648
|
-
manufacturerCode: image.header.manufacturerCode,
|
|
649
|
-
imageType: image.header.imageType,
|
|
650
|
-
fileVersion: image.header.fileVersion,
|
|
651
|
-
currentTime: 0,
|
|
652
|
-
upgradeTime: 1,
|
|
653
|
-
};
|
|
654
|
-
try {
|
|
655
|
-
await endpoint.commandResponse("genOta", "upgradeEndResponse", payload, undefined, endResult.header.transactionSequenceNumber);
|
|
656
|
-
logger_1.logger.debug(() => `${deviceLogString(device)} Update successful. Waiting for device announce...`, NS);
|
|
657
|
-
onProgress(100, undefined);
|
|
658
|
-
let timer;
|
|
659
|
-
const newFileVersion = image.header.fileVersion;
|
|
660
|
-
return await new Promise((resolve) => {
|
|
661
|
-
// XXX: annoying to test since using fake timers, same result anyway
|
|
662
|
-
/* istanbul ignore next */
|
|
663
|
-
const onDeviceAnnounce = () => {
|
|
664
|
-
clearTimeout(timer);
|
|
665
|
-
logger_1.logger.debug(() => `${deviceLogString(device)} Received device announce, update finished.`, NS);
|
|
666
|
-
resolve(newFileVersion);
|
|
667
|
-
};
|
|
668
|
-
// force "finished" after given time
|
|
669
|
-
timer = setTimeout(() => {
|
|
670
|
-
device.removeListener("deviceAnnounce", onDeviceAnnounce);
|
|
671
|
-
logger_1.logger.debug(() => `${deviceLogString(device)} Timed out waiting for device announce, update considered finished.`, NS);
|
|
672
|
-
resolve(newFileVersion);
|
|
673
|
-
}, UPDATE_END_FORCE_RESOLVE_TIME);
|
|
674
|
-
device.once("deviceAnnounce", onDeviceAnnounce);
|
|
675
|
-
});
|
|
676
|
-
}
|
|
677
|
-
catch (error) {
|
|
678
|
-
throw new Error(`Upgrade end response failed: ${error.message}`);
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
else {
|
|
682
|
-
/**
|
|
683
|
-
* For other status value received such as INVALID_IMAGE, REQUIRE_MORE_IMAGE, or ABORT,
|
|
684
|
-
* the upgrade server SHALL not send Upgrade End Response command but it SHALL send default
|
|
685
|
-
* response command with status of success and it SHALL wait for the client to reinitiate the upgrade process.
|
|
686
|
-
*/
|
|
687
|
-
try {
|
|
688
|
-
await endpoint.defaultResponse(zigbee_herdsman_1.Zcl.Clusters.genOta.commands.upgradeEndRequest.ID, zigbee_herdsman_1.Zcl.Status.SUCCESS, zigbee_herdsman_1.Zcl.Clusters.genOta.ID, endResult.header.transactionSequenceNumber);
|
|
689
|
-
}
|
|
690
|
-
catch (error) {
|
|
691
|
-
logger_1.logger.debug(() => `${deviceLogString(device)} Upgrade end request default response failed: ${error.message}`, NS);
|
|
692
|
-
}
|
|
693
|
-
throw new Error(`Update failed with reason: ${zigbee_herdsman_1.Zcl.Status[endResult.payload.status]}`);
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
// #endregion
|
|
697
|
-
//# sourceMappingURL=ota.js.map
|