homey-api 3.17.6 → 3.17.7

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.
@@ -5748,6 +5748,8 @@ export class Util {
5748
5748
  timeoutDuration?: number,
5749
5749
 
5750
5750
  timeoutMessage?: string,
5751
+
5752
+ patchOptions?: Function,
5751
5753
  ): Promise<any>;
5752
5754
 
5753
5755
  static wait(ms: number): Promise<void>;
@@ -5790,6 +5792,8 @@ export class Util {
5790
5792
  timeoutDuration?: number,
5791
5793
 
5792
5794
  timeoutMessage?: string,
5795
+
5796
+ patchOptions?: Function,
5793
5797
  ): Promise<any>;
5794
5798
 
5795
5799
  static wait(ms: number): Promise<void>;
@@ -9166,6 +9170,8 @@ export class Util {
9166
9170
  timeoutDuration?: number,
9167
9171
 
9168
9172
  timeoutMessage?: string,
9173
+
9174
+ patchOptions?: Function,
9169
9175
  ): Promise<any>;
9170
9176
 
9171
9177
  static wait(ms: number): Promise<void>;
@@ -9208,6 +9214,8 @@ export class Util {
9208
9214
  timeoutDuration?: number,
9209
9215
 
9210
9216
  timeoutMessage?: string,
9217
+
9218
+ patchOptions?: Function,
9211
9219
  ): Promise<any>;
9212
9220
 
9213
9221
  static wait(ms: number): Promise<void>;
@@ -5,9 +5,16 @@ const APIErrorHomeySubscriptionInactive = require('../../APIErrorHomeySubscripti
5
5
  const APIErrorHomeyInvalidSerialNumber = require('../../APIErrorHomeyInvalidSerialNumber');
6
6
  const Util = require('../../Util');
7
7
 
8
+ function getRuntimeRequire() {
9
+ // Avoid webpack statically bundling Node-only discovery transport into the browser build.
10
+ // eslint-disable-next-line no-eval
11
+ return eval('require');
12
+ }
13
+
8
14
  class DiscoveryManager {
9
15
  constructor(homey) {
10
16
  this.homey = homey;
17
+ this.discoveryNodeTransport = null;
11
18
  }
12
19
 
13
20
  async discoverBaseUrl() {
@@ -17,7 +24,10 @@ class DiscoveryManager {
17
24
  if (this.homey.__strategies.includes(DISCOVERY_STRATEGIES.MDNS)) {
18
25
  if (Util.isHTTPUnsecureSupported()) {
19
26
  if (this.homey.__properties.mdnsFqdn) {
20
- const port = this.homey.__properties.portHttp && this.homey.__properties.portHttp !== 80 ? `:${this.homey.__properties.portHttp}` : '';
27
+ const port =
28
+ this.homey.__properties.portHttp && this.homey.__properties.portHttp !== 80
29
+ ? `:${this.homey.__properties.portHttp}`
30
+ : '';
21
31
  urls[DISCOVERY_STRATEGIES.MDNS] = `http://${this.homey.__properties.mdnsFqdn}${port}`;
22
32
  } else {
23
33
  urls[DISCOVERY_STRATEGIES.MDNS] = `http://homey-${this.homey.id}.local`;
@@ -112,18 +122,24 @@ class DiscoveryManager {
112
122
 
113
123
  const ping = async (strategyId, timeout) => {
114
124
  const baseUrl = urls[strategyId];
125
+ const pingUrl = `${baseUrl}/api/manager/system/ping?id=${this.homey.id}`;
115
126
  const abortController = new AbortController();
116
127
  pingAbortControllers[strategyId] = abortController;
117
-
118
- const response = await Util.fetch(
119
- `${baseUrl}/api/manager/system/ping?id=${this.homey.id}`,
128
+ const discoveryTransport = this.getDiscoveryNodeTransport();
129
+ const fetch =
130
+ discoveryTransport && discoveryTransport.shouldUseForUrl(pingUrl)
131
+ ? discoveryTransport.fetch.bind(discoveryTransport)
132
+ : Util.fetch.bind(Util);
133
+
134
+ const response = await fetch(
135
+ pingUrl,
120
136
  {
121
137
  headers: {
122
138
  'X-Homey-ID': this.homey.id,
123
139
  },
124
140
  signal: abortController.signal,
125
141
  },
126
- timeout
142
+ timeout,
127
143
  );
128
144
 
129
145
  const text = await response.text();
@@ -180,15 +196,9 @@ class DiscoveryManager {
180
196
  const pings = {};
181
197
 
182
198
  if (urls[DISCOVERY_STRATEGIES.LOCAL_SECURE]) {
183
- pings[DISCOVERY_STRATEGIES.LOCAL_SECURE] = ping(
184
- DISCOVERY_STRATEGIES.LOCAL_SECURE,
185
- 1200
186
- );
199
+ pings[DISCOVERY_STRATEGIES.LOCAL_SECURE] = ping(DISCOVERY_STRATEGIES.LOCAL_SECURE, 1200);
187
200
  pings[DISCOVERY_STRATEGIES.LOCAL_SECURE].catch((err) => {
188
- this.homey.__debug(
189
- `Ping ${DISCOVERY_STRATEGIES.LOCAL_SECURE} Error:`,
190
- err && err.message
191
- );
201
+ this.homey.__debug(`Ping ${DISCOVERY_STRATEGIES.LOCAL_SECURE} Error:`, err && err.message);
192
202
  this.homey.__debug(urls[DISCOVERY_STRATEGIES.LOCAL_SECURE]);
193
203
  });
194
204
  }
@@ -196,34 +206,34 @@ class DiscoveryManager {
196
206
  if (urls[DISCOVERY_STRATEGIES.LOCAL]) {
197
207
  pings[DISCOVERY_STRATEGIES.LOCAL] = ping(DISCOVERY_STRATEGIES.LOCAL, 1000);
198
208
  pings[DISCOVERY_STRATEGIES.LOCAL].catch((err) =>
199
- this.homey.__debug(`Ping ${DISCOVERY_STRATEGIES.LOCAL} Error:`, err && err.message)
209
+ this.homey.__debug(`Ping ${DISCOVERY_STRATEGIES.LOCAL} Error:`, err && err.message),
200
210
  );
201
211
  }
202
212
 
203
213
  if (urls[DISCOVERY_STRATEGIES.MDNS]) {
204
214
  pings[DISCOVERY_STRATEGIES.MDNS] = ping(DISCOVERY_STRATEGIES.MDNS, 3000);
205
215
  pings[DISCOVERY_STRATEGIES.MDNS].catch((err) =>
206
- this.homey.__debug(`Ping ${DISCOVERY_STRATEGIES.MDNS} Error:`, err && err.message)
216
+ this.homey.__debug(`Ping ${DISCOVERY_STRATEGIES.MDNS} Error:`, err && err.message),
207
217
  );
208
218
  }
209
219
 
210
220
  if (urls[DISCOVERY_STRATEGIES.CLOUD]) {
211
221
  pings[DISCOVERY_STRATEGIES.CLOUD] = ping(DISCOVERY_STRATEGIES.CLOUD, 5000);
212
222
  pings[DISCOVERY_STRATEGIES.CLOUD].catch((err) =>
213
- this.homey.__debug(`Ping ${DISCOVERY_STRATEGIES.CLOUD} Error:`, err && err.message)
223
+ this.homey.__debug(`Ping ${DISCOVERY_STRATEGIES.CLOUD} Error:`, err && err.message),
214
224
  );
215
225
  }
216
226
 
217
227
  if (urls[DISCOVERY_STRATEGIES.REMOTE_FORWARDED]) {
218
228
  pings[DISCOVERY_STRATEGIES.REMOTE_FORWARDED] = ping(
219
229
  DISCOVERY_STRATEGIES.REMOTE_FORWARDED,
220
- 2000
230
+ 2000,
221
231
  );
222
232
  pings[DISCOVERY_STRATEGIES.REMOTE_FORWARDED].catch((err) =>
223
233
  this.homey.__debug(
224
234
  `Ping ${DISCOVERY_STRATEGIES.REMOTE_FORWARDED} Error:`,
225
- err && err.message
226
- )
235
+ err && err.message,
236
+ ),
227
237
  );
228
238
  }
229
239
 
@@ -240,36 +250,35 @@ class DiscoveryManager {
240
250
  let selectedRoutePromise;
241
251
 
242
252
  if (pings[DISCOVERY_STRATEGIES.LOCAL_SECURE]) {
243
- selectedRoutePromise = pings[DISCOVERY_STRATEGIES.LOCAL_SECURE]
244
- .catch((error) => {
245
- const fallbackPromises = [];
253
+ selectedRoutePromise = pings[DISCOVERY_STRATEGIES.LOCAL_SECURE].catch((error) => {
254
+ const fallbackPromises = [];
246
255
 
247
- if (pings[DISCOVERY_STRATEGIES.LOCAL]) {
248
- fallbackPromises.push(pings[DISCOVERY_STRATEGIES.LOCAL]);
249
- }
256
+ if (pings[DISCOVERY_STRATEGIES.LOCAL]) {
257
+ fallbackPromises.push(pings[DISCOVERY_STRATEGIES.LOCAL]);
258
+ }
250
259
 
251
- if (pings[DISCOVERY_STRATEGIES.REMOTE_FORWARDED]) {
252
- fallbackPromises.push(pings[DISCOVERY_STRATEGIES.REMOTE_FORWARDED]);
253
- }
260
+ if (pings[DISCOVERY_STRATEGIES.REMOTE_FORWARDED]) {
261
+ fallbackPromises.push(pings[DISCOVERY_STRATEGIES.REMOTE_FORWARDED]);
262
+ }
254
263
 
255
- if (pings[DISCOVERY_STRATEGIES.MDNS]) {
256
- fallbackPromises.push(pings[DISCOVERY_STRATEGIES.MDNS]);
257
- }
264
+ if (pings[DISCOVERY_STRATEGIES.MDNS]) {
265
+ fallbackPromises.push(pings[DISCOVERY_STRATEGIES.MDNS]);
266
+ }
258
267
 
259
- if (pings[DISCOVERY_STRATEGIES.CLOUD]) {
260
- fallbackPromises.push(pings[DISCOVERY_STRATEGIES.CLOUD]);
261
- }
268
+ if (pings[DISCOVERY_STRATEGIES.CLOUD]) {
269
+ fallbackPromises.push(pings[DISCOVERY_STRATEGIES.CLOUD]);
270
+ }
262
271
 
263
- if (isSubscriptionError(error)) {
264
- throw error;
265
- }
272
+ if (isSubscriptionError(error)) {
273
+ throw error;
274
+ }
266
275
 
267
- if (!fallbackPromises.length) {
268
- throw new APIErrorHomeyOffline();
269
- }
276
+ if (!fallbackPromises.length) {
277
+ throw new APIErrorHomeyOffline();
278
+ }
270
279
 
271
- return Promise.any(fallbackPromises);
272
- });
280
+ return Promise.any(fallbackPromises);
281
+ });
273
282
  } else if (pings[DISCOVERY_STRATEGIES.LOCAL]) {
274
283
  selectedRoutePromise = withCloudFallback(pings[DISCOVERY_STRATEGIES.LOCAL]);
275
284
  } else if (pings[DISCOVERY_STRATEGIES.MDNS]) {
@@ -290,6 +299,17 @@ class DiscoveryManager {
290
299
 
291
300
  return promise;
292
301
  }
302
+
303
+ getDiscoveryNodeTransport() {
304
+ if (!Util.isNodeJS()) {
305
+ return null;
306
+ }
307
+
308
+ this.discoveryNodeTransport =
309
+ this.discoveryNodeTransport || getRuntimeRequire()('./DiscoveryNodeTransport');
310
+
311
+ return this.discoveryNodeTransport;
312
+ }
293
313
  }
294
314
 
295
- module.exports = DiscoveryManager;
315
+ module.exports = DiscoveryManager;
@@ -0,0 +1,589 @@
1
+ 'use strict';
2
+
3
+ const Util = require('../../Util');
4
+
5
+ const DNS_RECORD_TYPES = {
6
+ A: 0x0001,
7
+ AAAA: 0x001c,
8
+ };
9
+
10
+ const DNS_CLASS_IN = 0x0001;
11
+ const DNS_CLASS_QU = 0x8000;
12
+
13
+ function getNodeHttpModules() {
14
+ return {
15
+ http: require('node:http'),
16
+ https: require('node:https'),
17
+ };
18
+ }
19
+
20
+ function getDnsModule() {
21
+ return require('node:dns');
22
+ }
23
+
24
+ function getDgramModule() {
25
+ return require('node:dgram');
26
+ }
27
+
28
+ function getNetModule() {
29
+ return require('node:net');
30
+ }
31
+
32
+ function createAbortError(reason) {
33
+ const error = new Error(
34
+ typeof reason === 'string' && reason.length > 0 ? reason : 'The operation was aborted',
35
+ );
36
+ error.name = 'AbortError';
37
+ error.code = 'ABORT_ERR';
38
+ error.type = 'aborted';
39
+ return error;
40
+ }
41
+
42
+ function getHostname(url) {
43
+ try {
44
+ return new URL(url).hostname;
45
+ } catch (error) {
46
+ return null;
47
+ }
48
+ }
49
+
50
+ function normalizeHostname(hostname) {
51
+ return String(hostname || '')
52
+ .replace(/\.$/, '')
53
+ .toLowerCase();
54
+ }
55
+
56
+ function shouldUseResolverLookupForHostname(hostname) {
57
+ return normalizeHostname(hostname).endsWith('.homey.homeylocal.com');
58
+ }
59
+
60
+ function shouldUseMdnsLookupForHostname(hostname) {
61
+ return normalizeHostname(hostname).endsWith('.local');
62
+ }
63
+
64
+ function getMdnsQueryTypes(options) {
65
+ const family = Number(options?.family) || 0;
66
+
67
+ if (family === 6) {
68
+ return [DNS_RECORD_TYPES.AAAA];
69
+ }
70
+
71
+ return [DNS_RECORD_TYPES.A];
72
+ }
73
+
74
+ function getMdnsConfig() {
75
+ const host = process.env.HOMEY_DISCOVERY_MDNS_HOST || '224.0.0.251';
76
+ const port = Number(process.env.HOMEY_DISCOVERY_MDNS_PORT) || 5353;
77
+
78
+ return {
79
+ host,
80
+ port,
81
+ joinMulticast: host === '224.0.0.251',
82
+ };
83
+ }
84
+
85
+ function encodeDnsName(hostname) {
86
+ const labels = normalizeHostname(hostname).split('.');
87
+ const parts = [];
88
+
89
+ for (const label of labels) {
90
+ const encodedLabel = Buffer.from(label);
91
+ parts.push(Buffer.from([encodedLabel.length]));
92
+ parts.push(encodedLabel);
93
+ }
94
+
95
+ parts.push(Buffer.from([0]));
96
+
97
+ return Buffer.concat(parts);
98
+ }
99
+
100
+ function buildMdnsQuestion(hostname, type) {
101
+ const question = Buffer.alloc(4);
102
+ question.writeUInt16BE(type, 0);
103
+ question.writeUInt16BE(DNS_CLASS_IN | DNS_CLASS_QU, 2);
104
+
105
+ return Buffer.concat([encodeDnsName(hostname), question]);
106
+ }
107
+
108
+ function buildMdnsQuery(hostname, options) {
109
+ const types = getMdnsQueryTypes(options);
110
+ const header = Buffer.alloc(12);
111
+
112
+ header.writeUInt16BE(0, 0);
113
+ header.writeUInt16BE(0, 2);
114
+ header.writeUInt16BE(types.length, 4);
115
+
116
+ return Buffer.concat([
117
+ header,
118
+ ...types.map((type) => buildMdnsQuestion(hostname, type)),
119
+ ]);
120
+ }
121
+
122
+ function readDnsName(buffer, offset, visited = new Set()) {
123
+ const labels = [];
124
+ let cursor = offset;
125
+
126
+ while (cursor < buffer.length) {
127
+ const length = buffer[cursor];
128
+
129
+ if (length === 0) {
130
+ return {
131
+ name: labels.join('.'),
132
+ offset: cursor + 1,
133
+ };
134
+ }
135
+
136
+ if ((length & 0xc0) === 0xc0) {
137
+ if (cursor + 1 >= buffer.length) {
138
+ throw new Error('Invalid DNS name pointer');
139
+ }
140
+
141
+ const pointer = ((length & 0x3f) << 8) | buffer[cursor + 1];
142
+
143
+ if (visited.has(pointer)) {
144
+ throw new Error('Invalid DNS name compression loop');
145
+ }
146
+
147
+ visited.add(pointer);
148
+
149
+ const result = readDnsName(buffer, pointer, visited);
150
+
151
+ if (result.name) {
152
+ labels.push(result.name);
153
+ }
154
+
155
+ return {
156
+ name: labels.join('.'),
157
+ offset: cursor + 2,
158
+ };
159
+ }
160
+
161
+ const labelStart = cursor + 1;
162
+ const labelEnd = labelStart + length;
163
+
164
+ if (labelEnd > buffer.length) {
165
+ throw new Error('Invalid DNS label length');
166
+ }
167
+
168
+ labels.push(buffer.toString('utf8', labelStart, labelEnd));
169
+ cursor = labelEnd;
170
+ }
171
+
172
+ throw new Error('Invalid DNS name');
173
+ }
174
+
175
+ function decodeIPv6Address(buffer, offset) {
176
+ const segments = [];
177
+
178
+ for (let index = 0; index < 8; index++) {
179
+ segments.push(buffer.readUInt16BE(offset + index * 2).toString(16));
180
+ }
181
+
182
+ return segments.join(':');
183
+ }
184
+
185
+ function parseDnsAddresses(buffer, hostname) {
186
+ if (buffer.length < 12) {
187
+ return [];
188
+ }
189
+
190
+ const normalizedHostname = normalizeHostname(hostname);
191
+ const questionCount = buffer.readUInt16BE(4);
192
+ const answerCount = buffer.readUInt16BE(6);
193
+ const authorityCount = buffer.readUInt16BE(8);
194
+ const additionalCount = buffer.readUInt16BE(10);
195
+ const recordCount = answerCount + authorityCount + additionalCount;
196
+ const addresses = [];
197
+ let offset = 12;
198
+
199
+ for (let index = 0; index < questionCount; index++) {
200
+ const question = readDnsName(buffer, offset);
201
+ offset = question.offset + 4;
202
+
203
+ if (offset > buffer.length) {
204
+ return [];
205
+ }
206
+ }
207
+
208
+ for (let index = 0; index < recordCount; index++) {
209
+ const recordName = readDnsName(buffer, offset);
210
+ offset = recordName.offset;
211
+
212
+ if (offset + 10 > buffer.length) {
213
+ return [];
214
+ }
215
+
216
+ const type = buffer.readUInt16BE(offset);
217
+ const klass = buffer.readUInt16BE(offset + 2) & 0x7fff;
218
+ const dataLength = buffer.readUInt16BE(offset + 8);
219
+ const dataOffset = offset + 10;
220
+ const nextOffset = dataOffset + dataLength;
221
+
222
+ if (nextOffset > buffer.length) {
223
+ return [];
224
+ }
225
+
226
+ if (klass === DNS_CLASS_IN && normalizeHostname(recordName.name) === normalizedHostname) {
227
+ if (type === DNS_RECORD_TYPES.A && dataLength === 4) {
228
+ addresses.push({
229
+ address: Array.from(buffer.subarray(dataOffset, nextOffset)).join('.'),
230
+ family: 4,
231
+ });
232
+ } else if (type === DNS_RECORD_TYPES.AAAA && dataLength === 16) {
233
+ addresses.push({
234
+ address: decodeIPv6Address(buffer, dataOffset),
235
+ family: 6,
236
+ });
237
+ }
238
+ }
239
+
240
+ offset = nextOffset;
241
+ }
242
+
243
+ return addresses;
244
+ }
245
+
246
+ function selectLookupResult(addresses, options) {
247
+ const family = Number(options?.family) || 0;
248
+ let candidates = addresses;
249
+
250
+ if (family === 4 || family === 6) {
251
+ candidates = addresses.filter((address) => address.family === family);
252
+ }
253
+
254
+ if (!candidates.length) {
255
+ return null;
256
+ }
257
+
258
+ if (options?.all === true) {
259
+ return {
260
+ address: candidates.map((candidate) => ({
261
+ address: candidate.address,
262
+ family: candidate.family,
263
+ })),
264
+ };
265
+ }
266
+
267
+ return candidates[0];
268
+ }
269
+
270
+ function createResolverLookup(signal) {
271
+ const dns = getDnsModule();
272
+ const net = getNetModule();
273
+
274
+ return (hostname, options, callback) => {
275
+ let settled = false;
276
+ let handleAbort = null;
277
+ const resolver = new dns.Resolver();
278
+
279
+ const finish = (error, address, family) => {
280
+ if (settled) {
281
+ return;
282
+ }
283
+
284
+ settled = true;
285
+
286
+ if (signal && handleAbort) {
287
+ signal.removeEventListener('abort', handleAbort);
288
+ }
289
+
290
+ callback(error, address, family);
291
+ };
292
+
293
+ const ipFamily = net.isIP(hostname);
294
+
295
+ if (ipFamily) {
296
+ finish(null, hostname, ipFamily);
297
+ return;
298
+ }
299
+
300
+ if (signal) {
301
+ if (signal.aborted) {
302
+ finish(createAbortError(signal.reason));
303
+ return;
304
+ }
305
+
306
+ handleAbort = () => {
307
+ resolver.cancel();
308
+ finish(createAbortError(signal.reason));
309
+ };
310
+
311
+ signal.addEventListener('abort', handleAbort, { once: true });
312
+ }
313
+
314
+ const family = Number(options?.family) || 0;
315
+
316
+ if (family === 6) {
317
+ resolver.resolve6(hostname, (error, addresses) => {
318
+ if (error) {
319
+ finish(error);
320
+ return;
321
+ }
322
+
323
+ if (!Array.isArray(addresses) || addresses.length === 0) {
324
+ finish(new Error(`No DNS results for ${hostname}`));
325
+ return;
326
+ }
327
+
328
+ if (options?.all === true) {
329
+ finish(
330
+ null,
331
+ addresses.map((address) => ({
332
+ address,
333
+ family: 6,
334
+ })),
335
+ );
336
+ return;
337
+ }
338
+
339
+ finish(null, addresses[0], 6);
340
+ });
341
+
342
+ return;
343
+ }
344
+
345
+ resolver.resolve4(hostname, (resolve4Error, addresses) => {
346
+ if (settled) {
347
+ return;
348
+ }
349
+
350
+ if (!resolve4Error && Array.isArray(addresses) && addresses.length > 0) {
351
+ if (options?.all === true) {
352
+ finish(
353
+ null,
354
+ addresses.map((address) => ({
355
+ address,
356
+ family: 4,
357
+ })),
358
+ );
359
+ return;
360
+ }
361
+
362
+ finish(null, addresses[0], 4);
363
+ return;
364
+ }
365
+
366
+ resolver.resolve6(hostname, (resolve6Error, resolve6Addresses) => {
367
+ if (resolve6Error) {
368
+ finish(resolve4Error || resolve6Error);
369
+ return;
370
+ }
371
+
372
+ if (!Array.isArray(resolve6Addresses) || resolve6Addresses.length === 0) {
373
+ finish(resolve4Error || new Error(`No DNS results for ${hostname}`));
374
+ return;
375
+ }
376
+
377
+ if (options?.all === true) {
378
+ finish(
379
+ null,
380
+ resolve6Addresses.map((address) => ({
381
+ address,
382
+ family: 6,
383
+ })),
384
+ );
385
+ return;
386
+ }
387
+
388
+ finish(null, resolve6Addresses[0], 6);
389
+ });
390
+ });
391
+ };
392
+ }
393
+
394
+ function createMdnsLookup(signal) {
395
+ const dgram = getDgramModule();
396
+ const net = getNetModule();
397
+
398
+ return (hostname, options, callback) => {
399
+ let settled = false;
400
+ let handleAbort = null;
401
+ let sendTimers = [];
402
+ const mdnsConfig = getMdnsConfig();
403
+ const socket = mdnsConfig.joinMulticast
404
+ ? dgram.createSocket({ type: 'udp4', reuseAddr: true })
405
+ : dgram.createSocket('udp4');
406
+ const query = buildMdnsQuery(hostname, options);
407
+
408
+ const cleanup = () => {
409
+ sendTimers.forEach(clearTimeout);
410
+ sendTimers = [];
411
+
412
+ if (signal && handleAbort) {
413
+ signal.removeEventListener('abort', handleAbort);
414
+ }
415
+
416
+ socket.removeAllListeners('error');
417
+ socket.removeAllListeners('message');
418
+
419
+ try {
420
+ socket.close();
421
+ } catch (error) {
422
+ void error;
423
+ }
424
+ };
425
+
426
+ const finish = (error, address, family) => {
427
+ if (settled) {
428
+ return;
429
+ }
430
+
431
+ settled = true;
432
+ cleanup();
433
+ callback(error, address, family);
434
+ };
435
+
436
+ const ipFamily = net.isIP(hostname);
437
+
438
+ if (ipFamily) {
439
+ finish(null, hostname, ipFamily);
440
+ return;
441
+ }
442
+
443
+ if (signal) {
444
+ if (signal.aborted) {
445
+ finish(createAbortError(signal.reason));
446
+ return;
447
+ }
448
+
449
+ handleAbort = () => {
450
+ finish(createAbortError(signal.reason));
451
+ };
452
+
453
+ signal.addEventListener('abort', handleAbort, { once: true });
454
+ }
455
+
456
+ socket.on('error', (error) => {
457
+ finish(error);
458
+ });
459
+
460
+ socket.on('message', (message) => {
461
+ let addresses = [];
462
+
463
+ try {
464
+ addresses = parseDnsAddresses(message, hostname);
465
+ } catch (error) {
466
+ return;
467
+ }
468
+
469
+ const result = selectLookupResult(addresses, options);
470
+
471
+ if (!result) {
472
+ return;
473
+ }
474
+
475
+ if (options?.all === true) {
476
+ finish(null, result.address);
477
+ return;
478
+ }
479
+
480
+ finish(null, result.address, result.family);
481
+ });
482
+
483
+ socket.bind(mdnsConfig.joinMulticast ? mdnsConfig.port : 0, () => {
484
+ if (settled) {
485
+ return;
486
+ }
487
+
488
+ if (mdnsConfig.joinMulticast) {
489
+ try {
490
+ socket.addMembership(mdnsConfig.host);
491
+ } catch (error) {
492
+ void error;
493
+ }
494
+ }
495
+
496
+ const sendQuery = () => {
497
+ if (settled) {
498
+ return;
499
+ }
500
+
501
+ socket.send(query, mdnsConfig.port, mdnsConfig.host, (error) => {
502
+ if (error) {
503
+ finish(error);
504
+ }
505
+ });
506
+ };
507
+
508
+ sendQuery();
509
+ sendTimers = [250, 1000].map((delay) => setTimeout(sendQuery, delay));
510
+ });
511
+ };
512
+ }
513
+
514
+ function getLookup(url, signal) {
515
+ const hostname = getHostname(url);
516
+
517
+ if (typeof hostname !== 'string') {
518
+ return undefined;
519
+ }
520
+
521
+ if (shouldUseResolverLookupForHostname(hostname)) {
522
+ return createResolverLookup(signal);
523
+ }
524
+
525
+ if (shouldUseMdnsLookupForHostname(hostname)) {
526
+ return createMdnsLookup(signal);
527
+ }
528
+
529
+ return undefined;
530
+ }
531
+
532
+ function getAgent(url, signal) {
533
+ const { http, https } = getNodeHttpModules();
534
+ const lookup = getLookup(url, signal);
535
+
536
+ if (!lookup) {
537
+ return undefined;
538
+ }
539
+
540
+ if (typeof url !== 'string') {
541
+ return undefined;
542
+ }
543
+
544
+ if (url.startsWith('https://')) {
545
+ return new https.Agent({
546
+ keepAlive: false,
547
+ lookup,
548
+ });
549
+ }
550
+
551
+ if (url.startsWith('http://')) {
552
+ return new http.Agent({
553
+ keepAlive: false,
554
+ lookup,
555
+ });
556
+ }
557
+
558
+ return undefined;
559
+ }
560
+
561
+ class DiscoveryNodeTransport {
562
+ static shouldUseForUrl(url) {
563
+ const hostname = getHostname(url);
564
+
565
+ return (
566
+ typeof hostname === 'string' &&
567
+ (
568
+ shouldUseResolverLookupForHostname(hostname) ||
569
+ shouldUseMdnsLookupForHostname(hostname)
570
+ )
571
+ );
572
+ }
573
+
574
+ static async fetch(url, options, timeoutDuration, timeoutMessage) {
575
+ return Util.fetch(
576
+ url,
577
+ options,
578
+ timeoutDuration,
579
+ timeoutMessage,
580
+ (resolvedOptions) => {
581
+ if (typeof resolvedOptions.agent === 'undefined') {
582
+ resolvedOptions.agent = getAgent(url, resolvedOptions.signal);
583
+ }
584
+ },
585
+ );
586
+ }
587
+ }
588
+
589
+ module.exports = DiscoveryNodeTransport;
@@ -483,10 +483,16 @@ class HomeyAPIV3 extends HomeyAPI {
483
483
  .finally(() => {
484
484
  // Delete after 30 seconds some requests might still be pending and they should be able
485
485
  // to receive a rejected promise for this token.
486
- this.__refreshMap[token + 'timeout'] = setTimeout(() => {
486
+ const cleanupTimer = setTimeout(() => {
487
487
  delete this.__refreshMap[token];
488
488
  delete this.__refreshMap[token + 'timeout'];
489
489
  }, 30 * 1000);
490
+
491
+ if (typeof cleanupTimer.unref === 'function') {
492
+ cleanupTimer.unref();
493
+ }
494
+
495
+ this.__refreshMap[token + 'timeout'] = cleanupTimer;
490
496
  });
491
497
  }
492
498
 
@@ -536,6 +542,14 @@ class HomeyAPIV3 extends HomeyAPI {
536
542
 
537
543
  destroy() {
538
544
  this.__destroyed = true;
545
+
546
+ for (const [key, value] of Object.entries(this.__refreshMap)) {
547
+ if (key.endsWith('timeout')) {
548
+ clearTimeout(value);
549
+ }
550
+ }
551
+
552
+ this.__refreshMap = {};
539
553
  this.__subscriptionRegistry.destroy();
540
554
  this.__socketSession.destroy();
541
555
 
package/lib/Util.js CHANGED
@@ -2,244 +2,6 @@
2
2
 
3
3
  const APIErrorTimeout = require('./APIErrorTimeout');
4
4
 
5
- let httpAgent = null;
6
- let httpsAgent = null;
7
-
8
- function getNodeHttpModules() {
9
- return {
10
- http: require('node:http'),
11
- https: require('node:https'),
12
- };
13
- }
14
-
15
- function getDnsModule() {
16
- return require('node:dns');
17
- }
18
-
19
- function getNetModule() {
20
- return require('node:net');
21
- }
22
-
23
- function getSharedNodeFetchAgents() {
24
- const { http, https } = getNodeHttpModules();
25
-
26
- httpAgent = httpAgent || new http.Agent({
27
- keepAlive: false,
28
- });
29
- httpsAgent = httpsAgent || new https.Agent({
30
- keepAlive: false,
31
- });
32
-
33
- return {
34
- httpAgent,
35
- httpsAgent,
36
- };
37
- }
38
-
39
- function getNodeFetchAgent(url) {
40
- const { httpAgent, httpsAgent } = getSharedNodeFetchAgents();
41
-
42
- if (typeof url !== 'string') {
43
- return undefined;
44
- }
45
-
46
- if (url.startsWith('https://')) {
47
- return httpsAgent;
48
- }
49
-
50
- if (url.startsWith('http://')) {
51
- return httpAgent;
52
- }
53
-
54
- return undefined;
55
- }
56
-
57
- function createAbortError(reason) {
58
- const error = new Error(
59
- typeof reason === 'string' && reason.length > 0
60
- ? reason
61
- : 'The operation was aborted',
62
- );
63
- error.name = 'AbortError';
64
- error.code = 'ABORT_ERR';
65
- error.type = 'aborted';
66
- return error;
67
- }
68
-
69
- function shouldUseCancellableResolverLookup(hostname) {
70
- if (typeof hostname !== 'string') {
71
- return false;
72
- }
73
-
74
- return hostname.endsWith('.homey.homeylocal.com');
75
- }
76
-
77
- function resolveHostnameWithResolver({ dns, hostname, options, finish }) {
78
- const resolver = new dns.Resolver();
79
- const family = Number(options?.family) || 0;
80
- const wantsAll = options?.all === true;
81
-
82
- const resolve4 = (callback) => resolver.resolve4(hostname, callback);
83
- const resolve6 = (callback) => resolver.resolve6(hostname, callback);
84
-
85
- const finishWithAddresses = (addresses, addressFamily) => {
86
- if (!Array.isArray(addresses) || addresses.length === 0) {
87
- finish(new Error(`No DNS results for ${hostname}`));
88
- return;
89
- }
90
-
91
- if (wantsAll) {
92
- finish(
93
- null,
94
- addresses.map((address) => ({
95
- address,
96
- family: addressFamily,
97
- })),
98
- );
99
- return;
100
- }
101
-
102
- finish(null, addresses[0], addressFamily);
103
- };
104
-
105
- const maybeResolve6After4 = (error) => {
106
- if (family === 4) {
107
- finish(error);
108
- return;
109
- }
110
-
111
- resolve6((resolve6Error, addresses) => {
112
- if (resolve6Error) {
113
- finish(error || resolve6Error);
114
- return;
115
- }
116
-
117
- finishWithAddresses(addresses, 6);
118
- });
119
- };
120
-
121
- if (family === 6) {
122
- resolve6((error, addresses) => {
123
- if (error) {
124
- finish(error);
125
- return;
126
- }
127
-
128
- finishWithAddresses(addresses, 6);
129
- });
130
-
131
- return () => resolver.cancel();
132
- }
133
-
134
- resolve4((error, addresses) => {
135
- if (error) {
136
- maybeResolve6After4(error);
137
- return;
138
- }
139
-
140
- finishWithAddresses(addresses, 4);
141
- });
142
-
143
- return () => resolver.cancel();
144
- }
145
-
146
- function createTimedLookup(timeoutDuration, signal) {
147
- const dns = getDnsModule();
148
- const net = getNetModule();
149
-
150
- return (hostname, options, callback) => {
151
- let settled = false;
152
- let timer = null;
153
- let handleAbort = null;
154
- let cancelLookup = null;
155
-
156
- const finish = (error, address, family) => {
157
- if (settled) {
158
- return;
159
- }
160
-
161
- settled = true;
162
- clearTimeout(timer);
163
-
164
- if (signal && handleAbort) {
165
- signal.removeEventListener('abort', handleAbort);
166
- }
167
-
168
- if (cancelLookup) {
169
- cancelLookup();
170
- cancelLookup = null;
171
- }
172
-
173
- callback(error, address, family);
174
- };
175
-
176
- const ipFamily = net.isIP(hostname);
177
-
178
- if (ipFamily) {
179
- finish(null, hostname, ipFamily);
180
- return;
181
- }
182
-
183
- if (signal) {
184
- if (signal.aborted) {
185
- finish(createAbortError(signal.reason));
186
- return;
187
- }
188
-
189
- handleAbort = () => {
190
- finish(createAbortError(signal.reason));
191
- };
192
-
193
- signal.addEventListener('abort', handleAbort, { once: true });
194
- }
195
-
196
- timer = setTimeout(() => {
197
- const error = new APIErrorTimeout(`DNS lookup timeout after ${timeoutDuration}ms`);
198
- error.code = 'ETIMEDOUT';
199
- error.syscall = 'getaddrinfo';
200
- error.stage = 'dns-lookup';
201
- finish(error);
202
- }, timeoutDuration);
203
-
204
- if (shouldUseCancellableResolverLookup(hostname)) {
205
- cancelLookup = resolveHostnameWithResolver({
206
- dns,
207
- hostname,
208
- options,
209
- finish,
210
- });
211
- return;
212
- }
213
-
214
- dns.lookup(hostname, options, finish);
215
- };
216
- }
217
-
218
- function getTimedNodeFetchAgent(url, timeoutDuration, signal) {
219
- const { http, https } = getNodeHttpModules();
220
- const lookup = createTimedLookup(timeoutDuration, signal);
221
-
222
- if (typeof url !== 'string') {
223
- return undefined;
224
- }
225
-
226
- if (url.startsWith('https://')) {
227
- return new https.Agent({
228
- keepAlive: false,
229
- lookup,
230
- });
231
- }
232
-
233
- if (url.startsWith('http://')) {
234
- return new http.Agent({
235
- keepAlive: false,
236
- lookup,
237
- });
238
- }
239
-
240
- return undefined;
241
- }
242
-
243
5
  /**
244
6
  * Helper Utility Class
245
7
  * @class
@@ -253,9 +15,10 @@ class Util {
253
15
  * @param {{}=} options
254
16
  * @param {number=} timeoutDuration
255
17
  * @param {string=} timeoutMessage
18
+ * @param {Function=} patchOptions
256
19
  * @returns {Promise<any>}
257
20
  */
258
- static async fetch(url, options, timeoutDuration, timeoutMessage) {
21
+ static async fetch(url, options, timeoutDuration, timeoutMessage, patchOptions) {
259
22
  options = { ...options };
260
23
  let timeoutTimer = null;
261
24
  let composedAbortController = null;
@@ -274,11 +37,7 @@ class Util {
274
37
  handleExternalAbort = () => {
275
38
  composedAbortController.abort(externalSignal.reason);
276
39
  };
277
- externalSignal.addEventListener(
278
- 'abort',
279
- handleExternalAbort,
280
- { once: true }
281
- );
40
+ externalSignal.addEventListener('abort', handleExternalAbort, { once: true });
282
41
  }
283
42
  }
284
43
 
@@ -294,6 +53,14 @@ class Util {
294
53
  options.signal = composedAbortController.signal;
295
54
  }
296
55
 
56
+ if (typeof patchOptions === 'function') {
57
+ const patchedOptions = patchOptions(options, url);
58
+
59
+ if (patchedOptions && typeof patchedOptions === 'object') {
60
+ options = patchedOptions;
61
+ }
62
+ }
63
+
297
64
  let responsePromise = null;
298
65
 
299
66
  if (this.isReactNative()) {
@@ -302,13 +69,6 @@ class Util {
302
69
  responsePromise = window.fetch(url, options);
303
70
  } else if (this.isNodeJS()) {
304
71
  const fetch = require('node-fetch');
305
-
306
- if (typeof options.agent === 'undefined') {
307
- options.agent = timeoutDuration != null
308
- ? getTimedNodeFetchAgent(url, timeoutDuration, options.signal)
309
- : getNodeFetchAgent(url);
310
- }
311
-
312
72
  responsePromise = fetch(url, options);
313
73
  } else if (typeof fetch !== 'undefined') {
314
74
  responsePromise = fetch(url, options);
@@ -354,7 +114,7 @@ class Util {
354
114
  static async timeout(
355
115
  promise,
356
116
  timeoutMillis = 5000,
357
- message = `Timeout after ${timeoutMillis}ms`
117
+ message = `Timeout after ${timeoutMillis}ms`,
358
118
  ) {
359
119
  const timeoutError = new APIErrorTimeout(message);
360
120
  let timeoutRef;
@@ -370,7 +130,7 @@ class Util {
370
130
 
371
131
  returnPromise
372
132
  // eslint-disable-next-line no-unused-vars
373
- .catch((err) => { })
133
+ .catch((err) => {})
374
134
  .finally(() => {
375
135
  clearTimeout(timeoutRef);
376
136
  });
@@ -421,7 +181,12 @@ class Util {
421
181
  */
422
182
  static isNodeJS() {
423
183
  if (this.isReactNative()) return false;
424
- return typeof process !== 'undefined';
184
+ return (
185
+ typeof process !== 'undefined' &&
186
+ typeof process.versions === 'object' &&
187
+ process.versions !== null &&
188
+ typeof process.versions.node === 'string'
189
+ );
425
190
  }
426
191
 
427
192
  /**
@@ -560,7 +325,7 @@ class Util {
560
325
  buildParams(
561
326
  prefix + '[' + (typeof obj[index] === 'object' ? index : '') + ']',
562
327
  obj[index],
563
- add
328
+ add,
564
329
  );
565
330
  }
566
331
  }
@@ -603,7 +368,6 @@ class Util {
603
368
 
604
369
  return encodedPairs.join('&');
605
370
  }
606
-
607
371
  }
608
372
 
609
373
  module.exports = Util;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "homey-api",
3
- "version": "3.17.6",
3
+ "version": "3.17.7",
4
4
  "description": "Homey API",
5
5
  "main": "index.js",
6
6
  "license": "SEE LICENSE",