homey-lib 2.45.3 → 2.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/.eslintignore +2 -1
  2. package/assets/app/schema.d.ts +546 -0
  3. package/assets/app/schema.json +136 -0
  4. package/assets/capability/schema.d.ts +53 -0
  5. package/helpers/index.js +21 -0
  6. package/index.js +48 -24
  7. package/lib/App/index.js +184 -5
  8. package/lib/Capability/index.js +42 -0
  9. package/lib/Device/index.js +10 -0
  10. package/lib/Energy/index.js +6 -0
  11. package/lib/Media/index.js +3 -0
  12. package/lib/Signal/index.js +25 -0
  13. package/lib/Signal/validators.js +21 -0
  14. package/lib/Util/file.js +84 -0
  15. package/lib/Util/index.js +72 -0
  16. package/lib/Util/zigbee.js +225 -0
  17. package/package.json +11 -3
  18. package/tsconfig.types.json +15 -0
  19. package/types/assets/app/schema.d.ts +546 -0
  20. package/types/assets/capability/schema.d.ts +53 -0
  21. package/types/helpers/index.d.ts +12 -0
  22. package/types/helpers/index.d.ts.map +1 -0
  23. package/types/index.d.ts +24 -0
  24. package/types/index.d.ts.map +1 -0
  25. package/types/lib/App/index.d.ts +140 -0
  26. package/types/lib/App/index.d.ts.map +1 -0
  27. package/types/lib/Capability/index.d.ts +61 -0
  28. package/types/lib/Capability/index.d.ts.map +1 -0
  29. package/types/lib/Device/index.d.ts +18 -0
  30. package/types/lib/Device/index.d.ts.map +1 -0
  31. package/types/lib/Energy/index.d.ts +12 -0
  32. package/types/lib/Energy/index.d.ts.map +1 -0
  33. package/types/lib/Media/index.d.ts +8 -0
  34. package/types/lib/Media/index.d.ts.map +1 -0
  35. package/types/lib/Signal/index.d.ts +52 -0
  36. package/types/lib/Signal/index.d.ts.map +1 -0
  37. package/types/lib/Signal/validators.d.ts +37 -0
  38. package/types/lib/Signal/validators.d.ts.map +1 -0
  39. package/types/lib/Util/file.d.ts +27 -0
  40. package/types/lib/Util/file.d.ts.map +1 -0
  41. package/types/lib/Util/index.d.ts +76 -0
  42. package/types/lib/Util/index.d.ts.map +1 -0
  43. package/types/lib/Util/zigbee.d.ts +88 -0
  44. package/types/lib/Util/zigbee.d.ts.map +1 -0
  45. package/webpack/index.js +1 -1
  46. package/webpack.config.js +1 -0
@@ -2,6 +2,9 @@
2
2
 
3
3
  class Media {
4
4
 
5
+ /**
6
+ * @returns {Record<string, Record<string, unknown>>}
7
+ */
5
8
  static getCodecs() {
6
9
  // eslint-disable-next-line global-require
7
10
  return require('../../assets/media/codecs.json');
@@ -11,8 +11,16 @@ const {
11
11
  prontoValidator,
12
12
  } = require('./validators');
13
13
 
14
+ /**
15
+ * @typedef {'433' | '868' | 'ir'} SignalFrequency
16
+ */
17
+
14
18
  class Signal {
15
19
 
20
+ /**
21
+ * @param {Record<string, unknown>} signal
22
+ * @param {{ frequency?: SignalFrequency }} [options]
23
+ */
16
24
  constructor(signal, { frequency = undefined } = {}) {
17
25
  this._signal = signal;
18
26
  this._frequency = frequency;
@@ -20,6 +28,10 @@ class Signal {
20
28
  this._check = this._check.bind(this);
21
29
  }
22
30
 
31
+ /**
32
+ * @param {...unknown} args
33
+ * @returns {void}
34
+ */
23
35
  debug(...args) {
24
36
  if (!this._debug) return;
25
37
 
@@ -27,12 +39,21 @@ class Signal {
27
39
  console.log('[dbg]', ...args);
28
40
  }
29
41
 
42
+ /**
43
+ * @param {string} message
44
+ * @param {boolean} result
45
+ * @returns {void}
46
+ */
30
47
  _check(message, result) {
31
48
  if (result !== true) {
32
49
  throw new Error(message);
33
50
  }
34
51
  }
35
52
 
53
+ /**
54
+ * @param {{ debug?: boolean }} [options]
55
+ * @returns {Promise<void>}
56
+ */
36
57
  async validate({
37
58
  debug = false,
38
59
  } = {}) {
@@ -65,6 +86,10 @@ class Signal {
65
86
  this.debug('Validated successfully');
66
87
  }
67
88
 
89
+ /**
90
+ * @param {Record<string, (value: unknown, signal: Record<string, unknown>) => { result: boolean, msg: string }>} validatorEngine
91
+ * @returns {void}
92
+ */
68
93
  _validateWithEngine(validatorEngine) {
69
94
  return validate(validatorEngine, this._check, this._signal);
70
95
  }
@@ -2,7 +2,21 @@
2
2
 
3
3
  'use strict';
4
4
 
5
+ /**
6
+ * @typedef {Record<string, unknown>} SignalDefinition
7
+ * @typedef {{ result: boolean, msg: string }} ValidationResult
8
+ * @typedef {(value: unknown, signal: SignalDefinition) => ValidationResult} Validator
9
+ * @typedef {Record<string, Validator>} ValidatorEngine
10
+ * @typedef {(message: string, result: boolean) => void} CheckFn
11
+ */
12
+
5
13
  /* static functions */
14
+ /**
15
+ * @param {ValidatorEngine} validator
16
+ * @param {CheckFn} check
17
+ * @param {SignalDefinition} signal
18
+ * @returns {void}
19
+ */
6
20
  function validate(validator, check, signal) {
7
21
  for (const propName in signal) {
8
22
  const property = signal[propName];
@@ -70,6 +84,7 @@ function _validateGenericData(data, signal) {
70
84
  return res;
71
85
  }
72
86
 
87
+ /** @type {ValidatorEngine} */
73
88
  const genericValidator = {
74
89
  words(words, signal) {
75
90
  const res = { result: true, msg: 'invalid_words' };
@@ -166,6 +181,7 @@ const rfBounds = {
166
181
  repetitions: { min: 1, max: 255 },
167
182
  };
168
183
 
184
+ /** @type {ValidatorEngine} */
169
185
  const rfValidator = {
170
186
 
171
187
  words(words, signal) {
@@ -222,6 +238,7 @@ const modulationBounds = {
222
238
  channelDeviation: { min: 5000, max: 50000 },
223
239
  };
224
240
 
241
+ /** @type {ValidatorEngine} */
225
242
  const modulationValidator = {
226
243
  modulation(modulation, signal) {
227
244
  // replace by default modulation props in homey-microcontroller?
@@ -256,6 +273,7 @@ const modulationValidator = {
256
273
  },
257
274
  };
258
275
 
276
+ /** @type {ValidatorEngine} */
259
277
  const prontoValidator = {
260
278
  cmds(cmds, signal) {
261
279
  const res = !Object.keys(cmds).some(cmd => {
@@ -279,6 +297,7 @@ const rf433Bounds = {
279
297
  carrier: { min: 433000000, max: 433990000 },
280
298
  };
281
299
 
300
+ /** @type {ValidatorEngine} */
282
301
  const rf433Validator = {
283
302
  carrier(carrier, signal) {
284
303
  const res = _valid_bounds(carrier, 433000000, 433990000);
@@ -290,6 +309,7 @@ const rf868Bounds = {
290
309
  carrier: { min: 868000000, max: 868900000 },
291
310
  };
292
311
 
312
+ /** @type {ValidatorEngine} */
293
313
  const rf868Validator = {
294
314
  carrier(carrier, signal) {
295
315
  const res = _valid_bounds(carrier, rf868Bounds.carrier.min, rf868Bounds.carrier.max);
@@ -302,6 +322,7 @@ const irBounds = {
302
322
  dutyCycle: { min: 30, max: 70 },
303
323
  };
304
324
 
325
+ /** @type {ValidatorEngine} */
305
326
  const irValidator = {
306
327
  carrier(carrier, signal) {
307
328
  const res = _valid_bounds(carrier, irBounds.carrier.min, irBounds.carrier.max);
@@ -0,0 +1,84 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ const ALLOWED_INTEGRITY_HASHES = new Set([
8
+ 'blake2b512',
9
+ 'blake2s256',
10
+ 'sha256',
11
+ 'sha384',
12
+ 'sha512',
13
+ 'sha512-256',
14
+ 'sha3-256',
15
+ 'sha3-384',
16
+ 'sha3-512',
17
+ ]);
18
+
19
+ /**
20
+ * Calculate a hex hash for the contents of a file.
21
+ * @param {string} filePath - Absolute or relative path to the file.
22
+ * @param {string} [hashName='sha256'] - Hash algorithm name (e.g. sha256).
23
+ * @returns {Promise<string>} Resolves with a hex-encoded digest.
24
+ */
25
+ async function hashFile(filePath, hashName = 'sha256') {
26
+ return new Promise((resolve, reject) => {
27
+ const hash = crypto.createHash(hashName);
28
+ const stream = fs.createReadStream(filePath);
29
+
30
+ stream.on('error', reject);
31
+ stream.on('data', chunk => hash.update(chunk));
32
+ stream.on('end', () => resolve(hash.digest('hex')));
33
+ });
34
+ }
35
+
36
+ /**
37
+ * Validate file integrity using a `hashName:hexDigest` string.
38
+ * @param {string} filePath - Absolute or relative path to the file.
39
+ * @param {string} integrity - Hash name and hex digest separated by `:`.
40
+ * @returns {Promise<boolean>} Resolves with true when hashes match.
41
+ */
42
+ async function validateIntegrity(filePath, integrity) {
43
+ if (typeof integrity !== 'string') {
44
+ throw new TypeError('Integrity must be a string');
45
+ }
46
+
47
+ const parts = integrity.split(':');
48
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
49
+ throw new Error('Integrity must be in the format "hashName:hexDigest"');
50
+ }
51
+
52
+ const [hashName, expectedHex] = parts;
53
+ const actualHex = await hashFile(filePath, hashName);
54
+
55
+ return actualHex === expectedHex.toLowerCase();
56
+ }
57
+
58
+ /**
59
+ * Create an integrity string for a file using an allowed hash algorithm.
60
+ * @param {string} filePath - Absolute or relative path to the file.
61
+ * @param {string} [hashName='sha256'] - Allowed hash algorithm name.
62
+ * @returns {Promise<string>} Resolves with `hashName:hexDigest`.
63
+ */
64
+ async function getIntegrity(filePath, hashName = 'sha256') {
65
+ const normalizedHash = String(hashName || '').toLowerCase();
66
+
67
+ if (!ALLOWED_INTEGRITY_HASHES.has(normalizedHash)) {
68
+ throw new Error('Hash algorithm is not allowed for integrity checks');
69
+ }
70
+
71
+ const digest = await hashFile(filePath, normalizedHash);
72
+ return `${normalizedHash}:${digest}`;
73
+ }
74
+
75
+ function getOTAFilePath({ appPath, driverId, fileName }) {
76
+ return path.join(appPath, 'drivers', driverId, 'assets', 'firmware', fileName);
77
+ }
78
+
79
+ module.exports = {
80
+ hashFile,
81
+ getIntegrity,
82
+ validateIntegrity,
83
+ getOTAFilePath,
84
+ };
package/lib/Util/index.js CHANGED
@@ -1,16 +1,87 @@
1
1
  'use strict';
2
2
 
3
+ const fileUtils = require('./file');
4
+ const zigbeeUtils = require('./zigbee');
5
+
3
6
  class Util {
4
7
 
8
+ /**
9
+ * @param {string} modelId
10
+ * @returns {string[]}
11
+ */
5
12
  static getPlatformLocalFeatures(modelId) {
6
13
  return Util._platformLocalFeatures[modelId] || [];
7
14
  }
8
15
 
16
+ /**
17
+ * @param {string} modelId
18
+ * @param {string[]} wantedFeatures
19
+ * @returns {string[]}
20
+ */
9
21
  static getMissingPlatformLocalFeatures(modelId, wantedFeatures) {
10
22
  const features = Util.getPlatformLocalFeatures(modelId);
11
23
  return wantedFeatures.filter(feature => !features.includes(feature));
12
24
  }
13
25
 
26
+ /**
27
+ * Create an integrity string for a file using an allowed hash algorithm.
28
+ * NOTE: This method is only available in Node.js environments.
29
+ * @param {string} filePath - Absolute or relative path to the file.
30
+ * @param {string} hashName - Allowed hash algorithm name.
31
+ * @returns {Promise<string>} Resolves with `hashName:hexDigest`.
32
+ */
33
+ static getIntegrity(filePath, hashName) {
34
+ return fileUtils.getIntegrity(filePath, hashName);
35
+ }
36
+
37
+ /**
38
+ * Validate file integrity using a `hashName:hexDigest` string.
39
+ * NOTE: This method is only available in Node.js environments.
40
+ * @param {string} filePath - Absolute or relative path to the file.
41
+ * @param {string} integrity - Hash name and hex digest separated by `:`.
42
+ * @returns {Promise<boolean>} Resolves with true when hashes match.
43
+ */
44
+ static validateIntegrity(filePath, integrity) {
45
+ return fileUtils.validateIntegrity(filePath, integrity);
46
+ }
47
+
48
+ /**
49
+ * Parse the Zigbee OTA header from a file.
50
+ * NOTE: This method is only available in Node.js environments.
51
+ * @param {string} filePath - Absolute or relative path to the OTA file.
52
+ * @returns {Promise<import('./zigbee').ZigbeeOTAHeader>} Parsed header values.
53
+ */
54
+ static parseZigbeeOTAHeader(filePath) {
55
+ return zigbeeUtils.parseZigbeeOTAHeader(filePath);
56
+ }
57
+
58
+ /**
59
+ * Validate a Zigbee OTA header against expected values.
60
+ * NOTE: This method is only available in Node.js environments.
61
+ * @param {object} options - Validation options.
62
+ * @param {string} options.filePath - Absolute or relative path to the OTA file.
63
+ * @param {number} [options.manufacturerCode] - Expected manufacturer code.
64
+ * @param {number} [options.fileVersion] - Expected file version.
65
+ * @param {number} [options.imageType] - Expected image type.
66
+ * @returns {Promise<import('./zigbee').ZigbeeOTAHeader>} Parsed header values.
67
+ */
68
+ static validateZigbeeOTAHeader(options) {
69
+ return zigbeeUtils.validateZigbeeOTAHeader(options);
70
+ }
71
+
72
+ /**
73
+ * Returns the expected file path for an OTA file based on app path, driver ID and file name.
74
+ * NOTE: This method is only available in Node.js environments.
75
+ * @param {object} options - File path options.
76
+ * @param {string} options.appPath - Absolute path to the app.
77
+ * @param {string} options.driverId - Driver ID.
78
+ * @param {string} options.fileName - OTA file name.
79
+ * @returns {string} Resolves with the expected file path.
80
+ */
81
+ static getOTAFilePath({ appPath, driverId, fileName }) {
82
+ return fileUtils.getOTAFilePath({ appPath, driverId, fileName });
83
+ }
84
+
14
85
  }
15
86
 
16
87
  Util.FEATURE_SPEAKER = 'speaker';
@@ -18,6 +89,7 @@ Util.FEATURE_LED_RING = 'ledring';
18
89
  Util.FEATURE_NFC = 'nfc';
19
90
  Util.FEATURE_MATTER = 'matter';
20
91
  Util.FEATURE_CAMERA_STREAMING = 'camera-streaming';
92
+ /** @type {Record<string, string[]>} */
21
93
  Util._platformLocalFeatures = {
22
94
  homey1s: [Util.FEATURE_SPEAKER, Util.FEATURE_LED_RING, Util.FEATURE_NFC],
23
95
  homey1d: [Util.FEATURE_SPEAKER, Util.FEATURE_LED_RING, Util.FEATURE_NFC],
@@ -0,0 +1,225 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+
5
+ const OTA_FILE_IDENTIFIER = 0x0BEEF11E;
6
+ const OTA_HEADER_VERSION = 0x0100;
7
+ const OTA_MIN_HEADER_LENGTH = 56;
8
+
9
+ const FIELD_CONTROL_BITS = {
10
+ securityCredentialVersion: 1 << 0,
11
+ deviceSpecificFile: 1 << 1,
12
+ hardwareVersionsPresent: 1 << 2,
13
+ };
14
+
15
+ /**
16
+ * @typedef {object} ZigbeeOTAHeader
17
+ * @property {number} fileSize - File size in bytes.
18
+ * @property {number} fileIdentifier - OTA file identifier.
19
+ * @property {number} headerVersion - OTA header version.
20
+ * @property {number} headerLength - OTA header length in bytes.
21
+ * @property {number} fieldControl - Field control bitmask.
22
+ * @property {number} manufacturerCode - Manufacturer code.
23
+ * @property {number} imageType - Image type.
24
+ * @property {number} fileVersion - File version.
25
+ * @property {number} zigbeeStackVersion - Zigbee stack version.
26
+ * @property {string} headerString - Header string (ASCII).
27
+ * @property {number} totalImageSize - Total image size in bytes.
28
+ * @property {number} [securityCredentialVersion] - Security credential version.
29
+ * @property {string} [upgradeFileDestination] - Destination EUI64 hex string.
30
+ * @property {number} [minimumHardwareVersion] - Minimum hardware version.
31
+ * @property {number} [maximumHardwareVersion] - Maximum hardware version.
32
+ * @property {number} remainingHeaderBytes - Unparsed header bytes.
33
+ */
34
+
35
+ /**
36
+ * Read a fixed-length slice from the start of a file.
37
+ * @param {string} filePath - Absolute or relative path to the OTA file.
38
+ * @param {number} length - Number of bytes to read.
39
+ * @returns {Promise<Buffer>} Buffer with the requested bytes.
40
+ */
41
+ async function readFileSlice(filePath, length) {
42
+ const fileHandle = await fs.promises.open(filePath, 'r');
43
+ try {
44
+ const buffer = Buffer.alloc(length);
45
+ const { bytesRead } = await fileHandle.read(buffer, 0, length, 0);
46
+ if (bytesRead < length) {
47
+ throw new Error('Unexpected end of file while reading Zigbee OTA header');
48
+ }
49
+ return buffer;
50
+ } finally {
51
+ await fileHandle.close();
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Parse the Zigbee OTA header from a file.
57
+ * @param {string} filePath - Absolute or relative path to the OTA file.
58
+ * @returns {Promise<ZigbeeOTAHeader>} Parsed header values.
59
+ */
60
+ async function parseZigbeeOTAHeader(filePath) {
61
+ if (typeof filePath !== 'string' || filePath.length === 0) {
62
+ throw new TypeError('filePath must be a non-empty string');
63
+ }
64
+
65
+ const stats = await fs.promises.stat(filePath);
66
+ if (stats.size < OTA_MIN_HEADER_LENGTH) {
67
+ throw new Error('File is too small to contain a Zigbee OTA header');
68
+ }
69
+
70
+ const baseHeader = await readFileSlice(filePath, OTA_MIN_HEADER_LENGTH);
71
+ const baseFileIdentifier = baseHeader.readUInt32LE(0);
72
+
73
+ if (baseFileIdentifier !== OTA_FILE_IDENTIFIER) {
74
+ throw new Error('Invalid Zigbee OTA header identifier');
75
+ }
76
+
77
+ const headerLength = baseHeader.readUInt16LE(6);
78
+
79
+ if (headerLength < OTA_MIN_HEADER_LENGTH) {
80
+ throw new Error('Zigbee OTA header length is shorter than the mandatory header size');
81
+ }
82
+ if (headerLength > stats.size) {
83
+ throw new Error('Zigbee OTA header length exceeds file size');
84
+ }
85
+
86
+ const headerBuffer = headerLength === OTA_MIN_HEADER_LENGTH
87
+ ? baseHeader
88
+ : await readFileSlice(filePath, headerLength);
89
+
90
+ let offset = 0;
91
+ const fileIdentifier = headerBuffer.readUInt32LE(offset);
92
+ offset += 4;
93
+ if (fileIdentifier !== OTA_FILE_IDENTIFIER) {
94
+ throw new Error('Invalid Zigbee OTA header identifier');
95
+ }
96
+ const headerVersion = headerBuffer.readUInt16LE(offset);
97
+ offset += 2;
98
+ const parsedHeaderLength = headerBuffer.readUInt16LE(offset);
99
+ offset += 2;
100
+ const fieldControl = headerBuffer.readUInt16LE(offset);
101
+ offset += 2;
102
+ const manufacturerCode = headerBuffer.readUInt16LE(offset);
103
+ offset += 2;
104
+ const imageType = headerBuffer.readUInt16LE(offset);
105
+ offset += 2;
106
+ const fileVersion = headerBuffer.readUInt32LE(offset);
107
+ offset += 4;
108
+ const zigbeeStackVersion = headerBuffer.readUInt16LE(offset);
109
+ offset += 2;
110
+ const headerString = headerBuffer
111
+ .toString('ascii', offset, offset + 32)
112
+ .replace(/\0+$/, '');
113
+ offset += 32;
114
+ const totalImageSize = headerBuffer.readUInt32LE(offset);
115
+ offset += 4;
116
+
117
+ const header = {
118
+ fileSize: stats.size,
119
+ fileIdentifier,
120
+ headerVersion,
121
+ headerLength: parsedHeaderLength,
122
+ fieldControl,
123
+ manufacturerCode,
124
+ imageType,
125
+ fileVersion,
126
+ zigbeeStackVersion,
127
+ headerString,
128
+ totalImageSize,
129
+ };
130
+
131
+ if (fieldControl & FIELD_CONTROL_BITS.securityCredentialVersion) {
132
+ header.securityCredentialVersion = headerBuffer.readUInt8(offset);
133
+ offset += 1;
134
+ }
135
+
136
+ if (fieldControl & FIELD_CONTROL_BITS.deviceSpecificFile) {
137
+ header.upgradeFileDestination = headerBuffer
138
+ .slice(offset, offset + 8)
139
+ .toString('hex');
140
+ offset += 8;
141
+ }
142
+
143
+ if (fieldControl & FIELD_CONTROL_BITS.hardwareVersionsPresent) {
144
+ header.minimumHardwareVersion = headerBuffer.readUInt16LE(offset);
145
+ offset += 2;
146
+ header.maximumHardwareVersion = headerBuffer.readUInt16LE(offset);
147
+ offset += 2;
148
+ }
149
+
150
+ if (parsedHeaderLength < offset) {
151
+ throw new Error('Zigbee OTA header length is shorter than parsed fields');
152
+ }
153
+
154
+ header.remainingHeaderBytes = parsedHeaderLength - offset;
155
+
156
+ return header;
157
+ }
158
+
159
+ /**
160
+ * Validate a Zigbee OTA header against expected values.
161
+ * @param {object} options - Validation options.
162
+ * @param {string} options.filePath - Absolute or relative path to the OTA file.
163
+ * @param {number} [options.manufacturerCode] - Expected manufacturer code.
164
+ * @param {number} [options.fileVersion] - Expected file version.
165
+ * @param {number} [options.imageType] - Expected image type.
166
+ * @returns {Promise<ZigbeeOTAHeader>} Parsed header values.
167
+ */
168
+ async function validateZigbeeOTAHeader({
169
+ filePath,
170
+ manufacturerCode,
171
+ fileVersion,
172
+ imageType,
173
+ } = {}) {
174
+ const header = await parseZigbeeOTAHeader(filePath);
175
+
176
+ if (header.fileIdentifier !== OTA_FILE_IDENTIFIER) {
177
+ throw new Error('Invalid Zigbee OTA header identifier');
178
+ }
179
+
180
+ if (header.headerVersion !== OTA_HEADER_VERSION) {
181
+ throw new Error('Unsupported Zigbee OTA header version');
182
+ }
183
+
184
+ if (header.headerLength > header.totalImageSize) {
185
+ throw new Error('Zigbee OTA header length exceeds total image size');
186
+ }
187
+
188
+ if (header.totalImageSize !== header.fileSize) {
189
+ throw new Error('Zigbee OTA total image size does not match file size');
190
+ }
191
+
192
+ if (manufacturerCode !== undefined) {
193
+ if (typeof manufacturerCode !== 'number') {
194
+ throw new TypeError('manufacturerCode must be a number');
195
+ }
196
+ if (header.manufacturerCode !== manufacturerCode) {
197
+ throw new Error('Zigbee OTA manufacturer code does not match');
198
+ }
199
+ }
200
+
201
+ if (imageType !== undefined) {
202
+ if (typeof imageType !== 'number') {
203
+ throw new TypeError('imageType must be a number');
204
+ }
205
+ if (header.imageType !== imageType) {
206
+ throw new Error('Zigbee OTA image type does not match');
207
+ }
208
+ }
209
+
210
+ if (fileVersion !== undefined) {
211
+ if (typeof fileVersion !== 'number') {
212
+ throw new TypeError('fileVersion must be a number');
213
+ }
214
+ if (header.fileVersion !== fileVersion) {
215
+ throw new Error('Zigbee OTA file version does not match');
216
+ }
217
+ }
218
+
219
+ return header;
220
+ }
221
+
222
+ module.exports = {
223
+ parseZigbeeOTAHeader,
224
+ validateZigbeeOTAHeader,
225
+ };
package/package.json CHANGED
@@ -1,15 +1,21 @@
1
1
  {
2
2
  "name": "homey-lib",
3
- "version": "2.45.3",
3
+ "version": "2.46.0",
4
4
  "description": "Shared Library for Homey",
5
+ "types": "types/index.d.ts",
5
6
  "main": "index.js",
6
7
  "scripts": {
7
- "build": "npm ci; npm run pack",
8
+ "build": "npm ci; npm run types:generate; npm run schema:generate; npm run schema:copy; npm run pack",
8
9
  "pack": "webpack",
9
10
  "lint": "eslint .",
10
11
  "test": "mocha --parallel",
11
12
  "locales:apply": "node scripts/apply-locale-files.js",
12
- "locales:generate": "node scripts/generate-locale-files.js"
13
+ "locales:generate": "node scripts/generate-locale-files.js",
14
+ "types:generate": "tsc --project tsconfig.types.json",
15
+ "schema:generate": "npm run capability-schema:generate && npm run app-schema:generate",
16
+ "schema:copy": "mkdir -p types/assets/app types/assets/capability && cp assets/app/schema.d.ts types/assets/app/schema.d.ts && cp assets/capability/schema.d.ts types/assets/capability/schema.d.ts",
17
+ "capability-schema:generate": "json2ts -i assets/capability/schema.json -o assets/capability/schema.d.ts --unknownAny",
18
+ "app-schema:generate": "json2ts -i assets/app/schema.json -o assets/app/schema.d.ts --unknownAny"
13
19
  },
14
20
  "engines": {
15
21
  "node": ">=12.13.0"
@@ -34,10 +40,12 @@
34
40
  "devDependencies": {
35
41
  "eslint": "^7.31.0",
36
42
  "eslint-config-athom": "^2.1.1",
43
+ "json-schema-to-typescript": "^15.0.4",
37
44
  "mocha": "^9.1.3",
38
45
  "mock-fs": "^5.1.2",
39
46
  "object-path": "^0.11.8",
40
47
  "set-value": "^4.1.0",
48
+ "typescript": "^5.9.3",
41
49
  "webpack": "^5.98.0",
42
50
  "webpack-cli": "^5.1.4"
43
51
  }
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "allowJs": true,
4
+ "checkJs": false,
5
+ "declaration": true,
6
+ "emitDeclarationOnly": true,
7
+ "declarationMap": true,
8
+ "outDir": "./types",
9
+ "module": "commonjs",
10
+ "target": "ES2020",
11
+ "strict": false
12
+ },
13
+ "include": ["index.js", "lib/**/*.js"],
14
+ "exclude": ["test/**", "node_modules/**"]
15
+ }