homey-api 3.17.0 → 3.17.2
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/lib/HomeyAPI/HomeyAPIV3/DiscoveryManager.js +294 -0
- package/lib/HomeyAPI/HomeyAPIV3.js +20 -325
- package/lib/Util.js +280 -14
- package/package.json +3 -6
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const APIErrorHomeyOffline = require('../../APIErrorHomeyOffline');
|
|
4
|
+
const APIErrorHomeySubscriptionInactive = require('../../APIErrorHomeySubscriptionInactive');
|
|
5
|
+
const APIErrorHomeyInvalidSerialNumber = require('../../APIErrorHomeyInvalidSerialNumber');
|
|
6
|
+
const Util = require('../../Util');
|
|
7
|
+
|
|
8
|
+
class DiscoveryManager {
|
|
9
|
+
constructor(homey) {
|
|
10
|
+
this.homey = homey;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async discoverBaseUrl() {
|
|
14
|
+
const DISCOVERY_STRATEGIES = this.homey.constructor.DISCOVERY_STRATEGIES;
|
|
15
|
+
const urls = {};
|
|
16
|
+
|
|
17
|
+
if (this.homey.__strategies.includes(DISCOVERY_STRATEGIES.MDNS)) {
|
|
18
|
+
if (Util.isHTTPUnsecureSupported()) {
|
|
19
|
+
if (this.homey.__properties.mdnsFqdn) {
|
|
20
|
+
urls[DISCOVERY_STRATEGIES.MDNS] = `http://${this.homey.__properties.mdnsFqdn}`;
|
|
21
|
+
} else {
|
|
22
|
+
urls[DISCOVERY_STRATEGIES.MDNS] = `http://homey-${this.homey.id}.local`;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (this.homey.__strategies.includes(DISCOVERY_STRATEGIES.LOCAL)) {
|
|
28
|
+
if (Util.isHTTPUnsecureSupported() && this.homey.__properties.localUrl) {
|
|
29
|
+
urls[DISCOVERY_STRATEGIES.LOCAL] = `${this.homey.__properties.localUrl}`;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (this.homey.__strategies.includes(DISCOVERY_STRATEGIES.LOCAL_SECURE)) {
|
|
34
|
+
if (this.homey.__properties.localUrlSecure) {
|
|
35
|
+
urls[DISCOVERY_STRATEGIES.LOCAL_SECURE] = `${this.homey.__properties.localUrlSecure}`;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (this.homey.__strategies.includes(DISCOVERY_STRATEGIES.CLOUD)) {
|
|
40
|
+
if (this.homey.__properties.remoteUrl) {
|
|
41
|
+
urls[DISCOVERY_STRATEGIES.CLOUD] = `${this.homey.__properties.remoteUrl}`;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (this.homey.__strategies.includes(DISCOVERY_STRATEGIES.REMOTE_FORWARDED)) {
|
|
46
|
+
if (this.homey.__properties.remoteUrlForwarded) {
|
|
47
|
+
urls[DISCOVERY_STRATEGIES.REMOTE_FORWARDED] =
|
|
48
|
+
`${this.homey.__properties.remoteUrlForwarded}`;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!Object.keys(urls).length) {
|
|
53
|
+
throw new Error('No Discovery Strategies Available');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
this.homey.__debug(`Discovery Strategies: ${Object.keys(urls).join(',')}`);
|
|
57
|
+
|
|
58
|
+
let resolve;
|
|
59
|
+
let reject;
|
|
60
|
+
|
|
61
|
+
const promise = new Promise((resolve_, reject_) => {
|
|
62
|
+
resolve = resolve_;
|
|
63
|
+
reject = reject_;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const pingAbortControllers = {};
|
|
67
|
+
|
|
68
|
+
const abortPendingPings = ({ exceptStrategyId } = {}) => {
|
|
69
|
+
Object.entries(pingAbortControllers).forEach(([strategyId, abortController]) => {
|
|
70
|
+
if (strategyId === exceptStrategyId) return;
|
|
71
|
+
if (abortController.signal.aborted) return;
|
|
72
|
+
abortController.abort('Discovery Completed');
|
|
73
|
+
});
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const resolveAndAbortPendingPings = (result) => {
|
|
77
|
+
abortPendingPings({ exceptStrategyId: result.strategyId });
|
|
78
|
+
resolve(result);
|
|
79
|
+
return result;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const rejectAndAbortPendingPings = (error) => {
|
|
83
|
+
abortPendingPings();
|
|
84
|
+
reject(error);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const isSubscriptionError = (error) => {
|
|
88
|
+
return (
|
|
89
|
+
error instanceof APIErrorHomeySubscriptionInactive ||
|
|
90
|
+
error instanceof APIErrorHomeyInvalidSerialNumber
|
|
91
|
+
);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const normalizeDiscoveryError = (error) => {
|
|
95
|
+
if (error instanceof AggregateError) {
|
|
96
|
+
for (const err of error.errors) {
|
|
97
|
+
if (isSubscriptionError(err)) {
|
|
98
|
+
return err;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return new APIErrorHomeyOffline();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (isSubscriptionError(error) || error instanceof APIErrorHomeyOffline) {
|
|
106
|
+
return error;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return new APIErrorHomeyOffline(error);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const ping = async (strategyId, timeout) => {
|
|
113
|
+
const baseUrl = urls[strategyId];
|
|
114
|
+
const abortController = new AbortController();
|
|
115
|
+
pingAbortControllers[strategyId] = abortController;
|
|
116
|
+
|
|
117
|
+
const response = await Util.fetch(
|
|
118
|
+
`${baseUrl}/api/manager/system/ping?id=${this.homey.id}`,
|
|
119
|
+
{
|
|
120
|
+
headers: {
|
|
121
|
+
'X-Homey-ID': this.homey.id,
|
|
122
|
+
},
|
|
123
|
+
signal: abortController.signal,
|
|
124
|
+
},
|
|
125
|
+
timeout
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const text = await response.text();
|
|
129
|
+
|
|
130
|
+
if (!response.ok) {
|
|
131
|
+
const responseContentType = response.headers.get('Content-Type');
|
|
132
|
+
let parsed;
|
|
133
|
+
|
|
134
|
+
if (responseContentType && responseContentType.toLowerCase().includes('application/json')) {
|
|
135
|
+
try {
|
|
136
|
+
parsed = JSON.parse(text);
|
|
137
|
+
// eslint-disable-next-line no-empty
|
|
138
|
+
} catch (err) {}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (typeof parsed === 'object' && parsed !== null) {
|
|
142
|
+
if (
|
|
143
|
+
parsed.error &&
|
|
144
|
+
parsed.error === 'Subscription Inactive' &&
|
|
145
|
+
parsed.statusCode === 403
|
|
146
|
+
) {
|
|
147
|
+
throw new APIErrorHomeySubscriptionInactive(parsed.error, parsed.statusCode);
|
|
148
|
+
} else if (
|
|
149
|
+
parsed.error &&
|
|
150
|
+
parsed.error === 'Invalid Serial Number' &&
|
|
151
|
+
parsed.statusCode === 403
|
|
152
|
+
) {
|
|
153
|
+
throw new APIErrorHomeyInvalidSerialNumber(parsed.error, parsed.statusCode);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
throw new Error(text || response.statusText);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (text === 'false') {
|
|
161
|
+
throw new Error('Invalid Homey ID');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const homeyId = response.headers.get('X-Homey-ID');
|
|
165
|
+
|
|
166
|
+
if (homeyId && homeyId !== this.homey.id) {
|
|
167
|
+
throw new Error('Invalid Homey ID');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const homeyVersion = response.headers.get('X-Homey-Version');
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
baseUrl,
|
|
174
|
+
strategyId,
|
|
175
|
+
homeyVersion,
|
|
176
|
+
};
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const pings = {};
|
|
180
|
+
|
|
181
|
+
if (urls[DISCOVERY_STRATEGIES.LOCAL_SECURE]) {
|
|
182
|
+
pings[DISCOVERY_STRATEGIES.LOCAL_SECURE] = ping(
|
|
183
|
+
DISCOVERY_STRATEGIES.LOCAL_SECURE,
|
|
184
|
+
1200
|
|
185
|
+
);
|
|
186
|
+
pings[DISCOVERY_STRATEGIES.LOCAL_SECURE].catch((err) => {
|
|
187
|
+
this.homey.__debug(
|
|
188
|
+
`Ping ${DISCOVERY_STRATEGIES.LOCAL_SECURE} Error:`,
|
|
189
|
+
err && err.message
|
|
190
|
+
);
|
|
191
|
+
this.homey.__debug(urls[DISCOVERY_STRATEGIES.LOCAL_SECURE]);
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (urls[DISCOVERY_STRATEGIES.LOCAL]) {
|
|
196
|
+
pings[DISCOVERY_STRATEGIES.LOCAL] = ping(DISCOVERY_STRATEGIES.LOCAL, 1000);
|
|
197
|
+
pings[DISCOVERY_STRATEGIES.LOCAL].catch((err) =>
|
|
198
|
+
this.homey.__debug(`Ping ${DISCOVERY_STRATEGIES.LOCAL} Error:`, err && err.message)
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (urls[DISCOVERY_STRATEGIES.MDNS]) {
|
|
203
|
+
pings[DISCOVERY_STRATEGIES.MDNS] = ping(DISCOVERY_STRATEGIES.MDNS, 3000);
|
|
204
|
+
pings[DISCOVERY_STRATEGIES.MDNS].catch((err) =>
|
|
205
|
+
this.homey.__debug(`Ping ${DISCOVERY_STRATEGIES.MDNS} Error:`, err && err.message)
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (urls[DISCOVERY_STRATEGIES.CLOUD]) {
|
|
210
|
+
pings[DISCOVERY_STRATEGIES.CLOUD] = ping(DISCOVERY_STRATEGIES.CLOUD, 5000);
|
|
211
|
+
pings[DISCOVERY_STRATEGIES.CLOUD].catch((err) =>
|
|
212
|
+
this.homey.__debug(`Ping ${DISCOVERY_STRATEGIES.CLOUD} Error:`, err && err.message)
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (urls[DISCOVERY_STRATEGIES.REMOTE_FORWARDED]) {
|
|
217
|
+
pings[DISCOVERY_STRATEGIES.REMOTE_FORWARDED] = ping(
|
|
218
|
+
DISCOVERY_STRATEGIES.REMOTE_FORWARDED,
|
|
219
|
+
2000
|
|
220
|
+
);
|
|
221
|
+
pings[DISCOVERY_STRATEGIES.REMOTE_FORWARDED].catch((err) =>
|
|
222
|
+
this.homey.__debug(
|
|
223
|
+
`Ping ${DISCOVERY_STRATEGIES.REMOTE_FORWARDED} Error:`,
|
|
224
|
+
err && err.message
|
|
225
|
+
)
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const withCloudFallback = (primaryPromise) => {
|
|
230
|
+
return primaryPromise.catch((error) => {
|
|
231
|
+
if (pings[DISCOVERY_STRATEGIES.CLOUD]) {
|
|
232
|
+
return pings[DISCOVERY_STRATEGIES.CLOUD];
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
throw new APIErrorHomeyOffline(error);
|
|
236
|
+
});
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
let selectedRoutePromise;
|
|
240
|
+
|
|
241
|
+
if (pings[DISCOVERY_STRATEGIES.LOCAL_SECURE]) {
|
|
242
|
+
selectedRoutePromise = pings[DISCOVERY_STRATEGIES.LOCAL_SECURE]
|
|
243
|
+
.catch((error) => {
|
|
244
|
+
const fallbackPromises = [];
|
|
245
|
+
|
|
246
|
+
if (pings[DISCOVERY_STRATEGIES.LOCAL]) {
|
|
247
|
+
fallbackPromises.push(pings[DISCOVERY_STRATEGIES.LOCAL]);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (pings[DISCOVERY_STRATEGIES.REMOTE_FORWARDED]) {
|
|
251
|
+
fallbackPromises.push(pings[DISCOVERY_STRATEGIES.REMOTE_FORWARDED]);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (pings[DISCOVERY_STRATEGIES.MDNS]) {
|
|
255
|
+
fallbackPromises.push(pings[DISCOVERY_STRATEGIES.MDNS]);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (pings[DISCOVERY_STRATEGIES.CLOUD]) {
|
|
259
|
+
fallbackPromises.push(pings[DISCOVERY_STRATEGIES.CLOUD]);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (isSubscriptionError(error)) {
|
|
263
|
+
throw error;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (!fallbackPromises.length) {
|
|
267
|
+
throw new APIErrorHomeyOffline();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return Promise.any(fallbackPromises);
|
|
271
|
+
});
|
|
272
|
+
} else if (pings[DISCOVERY_STRATEGIES.LOCAL]) {
|
|
273
|
+
selectedRoutePromise = withCloudFallback(pings[DISCOVERY_STRATEGIES.LOCAL]);
|
|
274
|
+
} else if (pings[DISCOVERY_STRATEGIES.MDNS]) {
|
|
275
|
+
selectedRoutePromise = withCloudFallback(pings[DISCOVERY_STRATEGIES.MDNS]);
|
|
276
|
+
} else if (pings[DISCOVERY_STRATEGIES.REMOTE_FORWARDED]) {
|
|
277
|
+
selectedRoutePromise = withCloudFallback(pings[DISCOVERY_STRATEGIES.REMOTE_FORWARDED]);
|
|
278
|
+
} else if (pings[DISCOVERY_STRATEGIES.CLOUD]) {
|
|
279
|
+
selectedRoutePromise = pings[DISCOVERY_STRATEGIES.CLOUD];
|
|
280
|
+
} else {
|
|
281
|
+
selectedRoutePromise = Promise.reject(new APIErrorHomeyOffline());
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
selectedRoutePromise
|
|
285
|
+
.then((result) => resolveAndAbortPendingPings(result))
|
|
286
|
+
.catch((error) => {
|
|
287
|
+
rejectAndAbortPendingPings(normalizeDiscoveryError(error));
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
return promise;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
module.exports = DiscoveryManager;
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const SocketIOClient = require('socket.io-client');
|
|
4
|
-
const APIErrorHomeyOffline = require('../APIErrorHomeyOffline');
|
|
5
|
-
const APIErrorHomeySubscriptionInactive = require('../APIErrorHomeySubscriptionInactive');
|
|
6
|
-
const APIErrorHomeyInvalidSerialNumber = require('../APIErrorHomeyInvalidSerialNumber');
|
|
7
4
|
const Util = require('../Util');
|
|
8
5
|
const HomeyAPI = require('./HomeyAPI');
|
|
9
6
|
const HomeyAPIError = require('./HomeyAPIError');
|
|
@@ -15,6 +12,7 @@ const ManagerFlowToken = require('./HomeyAPIV3/ManagerFlowToken');
|
|
|
15
12
|
const ManagerInsights = require('./HomeyAPIV3/ManagerInsights');
|
|
16
13
|
const ManagerUsers = require('./HomeyAPIV3/ManagerUsers');
|
|
17
14
|
const ManagerZones = require('./HomeyAPIV3/ManagerZones');
|
|
15
|
+
const DiscoveryManager = require('./HomeyAPIV3/DiscoveryManager');
|
|
18
16
|
const Manager = require('./HomeyAPIV3/Manager');
|
|
19
17
|
|
|
20
18
|
/**
|
|
@@ -119,6 +117,8 @@ class HomeyAPIV3 extends HomeyAPI {
|
|
|
119
117
|
writable: true,
|
|
120
118
|
});
|
|
121
119
|
|
|
120
|
+
this.__discoveryManager = new DiscoveryManager(this);
|
|
121
|
+
|
|
122
122
|
this.generateManagersFromSpecification();
|
|
123
123
|
}
|
|
124
124
|
|
|
@@ -128,9 +128,10 @@ class HomeyAPIV3 extends HomeyAPI {
|
|
|
128
128
|
get baseUrl() {
|
|
129
129
|
return (async () => {
|
|
130
130
|
if (!this.__baseUrlPromise) {
|
|
131
|
-
this.__baseUrlPromise =
|
|
131
|
+
this.__baseUrlPromise = (async () => {
|
|
132
|
+
const { baseUrl } = await this.discoverBaseUrl();
|
|
132
133
|
return baseUrl;
|
|
133
|
-
});
|
|
134
|
+
})();
|
|
134
135
|
this.__baseUrlPromise.catch(() => {});
|
|
135
136
|
}
|
|
136
137
|
|
|
@@ -191,331 +192,25 @@ class HomeyAPIV3 extends HomeyAPI {
|
|
|
191
192
|
*/
|
|
192
193
|
|
|
193
194
|
async discoverBaseUrl() {
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
urls[HomeyAPI.DISCOVERY_STRATEGIES.MDNS] = `http://homey-${this.id}.local`;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
if (this.__strategies.includes(HomeyAPI.DISCOVERY_STRATEGIES.LOCAL)) {
|
|
203
|
-
if (Util.isHTTPUnsecureSupported() && this.__properties.localUrl) {
|
|
204
|
-
urls[HomeyAPI.DISCOVERY_STRATEGIES.LOCAL] = `${this.__properties.localUrl}`;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
if (this.__strategies.includes(HomeyAPI.DISCOVERY_STRATEGIES.LOCAL_SECURE)) {
|
|
209
|
-
if (this.__properties.localUrlSecure) {
|
|
210
|
-
urls[HomeyAPI.DISCOVERY_STRATEGIES.LOCAL_SECURE] = `${this.__properties.localUrlSecure}`;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
if (this.__strategies.includes(HomeyAPI.DISCOVERY_STRATEGIES.CLOUD)) {
|
|
215
|
-
if (this.__properties.remoteUrl) {
|
|
216
|
-
urls[HomeyAPI.DISCOVERY_STRATEGIES.CLOUD] = `${this.__properties.remoteUrl}`;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
195
|
+
const discoveryResult = await this.__discoveryManager.discoverBaseUrl();
|
|
196
|
+
const result = this.__applyDiscoveryResult(discoveryResult);
|
|
197
|
+
return result;
|
|
198
|
+
}
|
|
219
199
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
}
|
|
225
|
-
}
|
|
200
|
+
__applyDiscoveryResult({ baseUrl, strategyId, homeyVersion }) {
|
|
201
|
+
this.__baseUrl = baseUrl;
|
|
202
|
+
this.__baseUrlPromise = Promise.resolve(baseUrl);
|
|
203
|
+
this.__strategyId = strategyId;
|
|
226
204
|
|
|
227
|
-
if (
|
|
228
|
-
|
|
205
|
+
if (homeyVersion != null) {
|
|
206
|
+
this.version = homeyVersion;
|
|
229
207
|
}
|
|
230
208
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
let reject;
|
|
236
|
-
|
|
237
|
-
const promise = new Promise((resolve_, reject_) => {
|
|
238
|
-
resolve = resolve_;
|
|
239
|
-
reject = reject_;
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
promise
|
|
243
|
-
.then(({ baseUrl, strategyId }) => {
|
|
244
|
-
this.__baseUrl = baseUrl;
|
|
245
|
-
this.__strategyId = strategyId;
|
|
246
|
-
})
|
|
247
|
-
.catch(() => {});
|
|
248
|
-
|
|
249
|
-
// Ping method
|
|
250
|
-
const ping = async (strategyId, timeout) => {
|
|
251
|
-
const baseUrl = urls[strategyId];
|
|
252
|
-
|
|
253
|
-
const response = await Util.fetch(
|
|
254
|
-
`${baseUrl}/api/manager/system/ping?id=${this.id}`,
|
|
255
|
-
{
|
|
256
|
-
headers: {
|
|
257
|
-
'X-Homey-ID': this.id,
|
|
258
|
-
},
|
|
259
|
-
},
|
|
260
|
-
timeout
|
|
261
|
-
);
|
|
262
|
-
|
|
263
|
-
const text = await response.text();
|
|
264
|
-
|
|
265
|
-
if (!response.ok) {
|
|
266
|
-
const responseContentType = response.headers.get('Content-Type');
|
|
267
|
-
let parsed;
|
|
268
|
-
|
|
269
|
-
if (responseContentType && responseContentType.toLowerCase().includes('application/json')) {
|
|
270
|
-
try {
|
|
271
|
-
parsed = JSON.parse(text);
|
|
272
|
-
// eslint-disable-next-line no-empty
|
|
273
|
-
} catch (err) {}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
if (typeof parsed === 'object' && parsed !== null) {
|
|
277
|
-
if (
|
|
278
|
-
parsed.error &&
|
|
279
|
-
parsed.error === 'Subscription Inactive' &&
|
|
280
|
-
parsed.statusCode === 403
|
|
281
|
-
) {
|
|
282
|
-
throw new APIErrorHomeySubscriptionInactive(parsed.error, parsed.statusCode);
|
|
283
|
-
} else if (
|
|
284
|
-
parsed.error &&
|
|
285
|
-
parsed.error === 'Invalid Serial Number' &&
|
|
286
|
-
parsed.statusCode === 403
|
|
287
|
-
) {
|
|
288
|
-
throw new APIErrorHomeyInvalidSerialNumber(parsed.error, parsed.statusCode);
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
throw new Error(text || response.statusText);
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
if (text === 'false') {
|
|
296
|
-
throw new Error('Invalid Homey ID');
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
const homeyId = response.headers.get('X-Homey-ID');
|
|
300
|
-
|
|
301
|
-
if (homeyId && homeyId !== this.id) {
|
|
302
|
-
throw new Error('Invalid Homey ID'); // TODO: Add to Homey Connect
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// Set the version that Homey told us.
|
|
306
|
-
// It's the absolute truth, because the Cloud API may be behind.
|
|
307
|
-
const homeyVersion = response.headers.get('X-Homey-Version');
|
|
308
|
-
|
|
309
|
-
if (homeyVersion !== this.version) {
|
|
310
|
-
this.version = homeyVersion;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
return {
|
|
314
|
-
baseUrl,
|
|
315
|
-
strategyId,
|
|
316
|
-
};
|
|
209
|
+
return {
|
|
210
|
+
baseUrl,
|
|
211
|
+
strategyId,
|
|
212
|
+
homeyVersion,
|
|
317
213
|
};
|
|
318
|
-
|
|
319
|
-
const pings = {};
|
|
320
|
-
|
|
321
|
-
// Ping localSecure (https://xxx-xxx-xxx-xx.homey.homeylocal.com)
|
|
322
|
-
if (urls[HomeyAPI.DISCOVERY_STRATEGIES.LOCAL_SECURE]) {
|
|
323
|
-
pings[HomeyAPI.DISCOVERY_STRATEGIES.LOCAL_SECURE] = ping(
|
|
324
|
-
HomeyAPI.DISCOVERY_STRATEGIES.LOCAL_SECURE,
|
|
325
|
-
1200
|
|
326
|
-
);
|
|
327
|
-
pings[HomeyAPI.DISCOVERY_STRATEGIES.LOCAL_SECURE].catch((err) => {
|
|
328
|
-
this.__debug(
|
|
329
|
-
`Ping ${HomeyAPI.DISCOVERY_STRATEGIES.LOCAL_SECURE} Error:`,
|
|
330
|
-
err && err.message
|
|
331
|
-
);
|
|
332
|
-
this.__debug(urls[HomeyAPI.DISCOVERY_STRATEGIES.LOCAL_SECURE]);
|
|
333
|
-
});
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
// Ping local (http://xxx-xxx-xxx-xxx)
|
|
337
|
-
if (urls[HomeyAPI.DISCOVERY_STRATEGIES.LOCAL]) {
|
|
338
|
-
pings[HomeyAPI.DISCOVERY_STRATEGIES.LOCAL] = ping(HomeyAPI.DISCOVERY_STRATEGIES.LOCAL, 1000);
|
|
339
|
-
pings[HomeyAPI.DISCOVERY_STRATEGIES.LOCAL].catch((err) =>
|
|
340
|
-
this.__debug(`Ping ${HomeyAPI.DISCOVERY_STRATEGIES.LOCAL} Error:`, err && err.message)
|
|
341
|
-
);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
// Ping mdns (http://homey-<homeyId>.local)
|
|
345
|
-
if (urls[HomeyAPI.DISCOVERY_STRATEGIES.MDNS]) {
|
|
346
|
-
pings[HomeyAPI.DISCOVERY_STRATEGIES.MDNS] = ping(HomeyAPI.DISCOVERY_STRATEGIES.MDNS, 3000);
|
|
347
|
-
pings[HomeyAPI.DISCOVERY_STRATEGIES.MDNS].catch((err) =>
|
|
348
|
-
this.__debug(`Ping ${HomeyAPI.DISCOVERY_STRATEGIES.MDNS} Error:`, err && err.message)
|
|
349
|
-
);
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// Ping cloud (https://<homeyId>.connect.athom.com)
|
|
353
|
-
if (urls[HomeyAPI.DISCOVERY_STRATEGIES.CLOUD]) {
|
|
354
|
-
pings[HomeyAPI.DISCOVERY_STRATEGIES.CLOUD] = ping(HomeyAPI.DISCOVERY_STRATEGIES.CLOUD, 5000);
|
|
355
|
-
pings[HomeyAPI.DISCOVERY_STRATEGIES.CLOUD].catch((err) =>
|
|
356
|
-
this.__debug(`Ping ${HomeyAPI.DISCOVERY_STRATEGIES.CLOUD} Error:`, err && err.message)
|
|
357
|
-
);
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// Ping Direct (https://xxx-xxx-xxx-xx.homey.homeylocal.com:12345)
|
|
361
|
-
if (urls[HomeyAPI.DISCOVERY_STRATEGIES.REMOTE_FORWARDED]) {
|
|
362
|
-
pings[HomeyAPI.DISCOVERY_STRATEGIES.REMOTE_FORWARDED] = ping(
|
|
363
|
-
HomeyAPI.DISCOVERY_STRATEGIES.REMOTE_FORWARDED,
|
|
364
|
-
2000
|
|
365
|
-
);
|
|
366
|
-
pings[HomeyAPI.DISCOVERY_STRATEGIES.REMOTE_FORWARDED].catch((err) =>
|
|
367
|
-
this.__debug(
|
|
368
|
-
`Ping ${HomeyAPI.DISCOVERY_STRATEGIES.REMOTE_FORWARDED} Error:`,
|
|
369
|
-
err && err.message
|
|
370
|
-
)
|
|
371
|
-
);
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
// Select the best route
|
|
375
|
-
if (pings[HomeyAPI.DISCOVERY_STRATEGIES.LOCAL_SECURE]) {
|
|
376
|
-
pings[HomeyAPI.DISCOVERY_STRATEGIES.LOCAL_SECURE]
|
|
377
|
-
.then((result) => resolve(result))
|
|
378
|
-
.catch((error) => {
|
|
379
|
-
const promises = [];
|
|
380
|
-
|
|
381
|
-
if (pings[HomeyAPI.DISCOVERY_STRATEGIES.LOCAL]) {
|
|
382
|
-
promises.push(pings[HomeyAPI.DISCOVERY_STRATEGIES.LOCAL]);
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
if (pings[HomeyAPI.DISCOVERY_STRATEGIES.REMOTE_FORWARDED]) {
|
|
386
|
-
promises.push(pings[HomeyAPI.DISCOVERY_STRATEGIES.REMOTE_FORWARDED]);
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
if (pings[HomeyAPI.DISCOVERY_STRATEGIES.MDNS]) {
|
|
390
|
-
promises.push(pings[HomeyAPI.DISCOVERY_STRATEGIES.MDNS]);
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
if (pings[HomeyAPI.DISCOVERY_STRATEGIES.CLOUD]) {
|
|
394
|
-
promises.push(pings[HomeyAPI.DISCOVERY_STRATEGIES.CLOUD]);
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
if (
|
|
398
|
-
error instanceof APIErrorHomeySubscriptionInactive ||
|
|
399
|
-
error instanceof APIErrorHomeyInvalidSerialNumber
|
|
400
|
-
) {
|
|
401
|
-
throw error;
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
if (!promises.length) {
|
|
405
|
-
throw new APIErrorHomeyOffline();
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
return Promise.any(promises);
|
|
409
|
-
})
|
|
410
|
-
.then((result) => resolve(result))
|
|
411
|
-
.catch((error) => {
|
|
412
|
-
if (error instanceof AggregateError) {
|
|
413
|
-
for (const err of error.errors) {
|
|
414
|
-
if (
|
|
415
|
-
err instanceof APIErrorHomeySubscriptionInactive ||
|
|
416
|
-
err instanceof APIErrorHomeyInvalidSerialNumber
|
|
417
|
-
) {
|
|
418
|
-
reject(err);
|
|
419
|
-
return;
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
if (
|
|
425
|
-
error instanceof APIErrorHomeySubscriptionInactive ||
|
|
426
|
-
error instanceof APIErrorHomeyInvalidSerialNumber
|
|
427
|
-
) {
|
|
428
|
-
reject(error);
|
|
429
|
-
return;
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
reject(new APIErrorHomeyOffline());
|
|
433
|
-
});
|
|
434
|
-
} else if (pings[HomeyAPI.DISCOVERY_STRATEGIES.LOCAL]) {
|
|
435
|
-
pings[HomeyAPI.DISCOVERY_STRATEGIES.LOCAL]
|
|
436
|
-
.then((result) => resolve(result))
|
|
437
|
-
.catch((err) => {
|
|
438
|
-
if (pings[HomeyAPI.DISCOVERY_STRATEGIES.CLOUD]) {
|
|
439
|
-
pings[HomeyAPI.DISCOVERY_STRATEGIES.CLOUD]
|
|
440
|
-
.then((result) => resolve(result))
|
|
441
|
-
.catch((err) => {
|
|
442
|
-
if (
|
|
443
|
-
err instanceof APIErrorHomeySubscriptionInactive ||
|
|
444
|
-
err instanceof APIErrorHomeyInvalidSerialNumber
|
|
445
|
-
) {
|
|
446
|
-
reject(err);
|
|
447
|
-
return;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
reject(new APIErrorHomeyOffline(err));
|
|
451
|
-
});
|
|
452
|
-
} else {
|
|
453
|
-
reject(new APIErrorHomeyOffline(err));
|
|
454
|
-
}
|
|
455
|
-
});
|
|
456
|
-
} else if (pings[HomeyAPI.DISCOVERY_STRATEGIES.MDNS]) {
|
|
457
|
-
pings[HomeyAPI.DISCOVERY_STRATEGIES.MDNS]
|
|
458
|
-
.then((result) => resolve(result))
|
|
459
|
-
.catch((err) => {
|
|
460
|
-
if (pings[HomeyAPI.DISCOVERY_STRATEGIES.CLOUD]) {
|
|
461
|
-
pings[HomeyAPI.DISCOVERY_STRATEGIES.CLOUD]
|
|
462
|
-
.then((result) => resolve(result))
|
|
463
|
-
.catch((err) => {
|
|
464
|
-
if (
|
|
465
|
-
err instanceof APIErrorHomeySubscriptionInactive ||
|
|
466
|
-
err instanceof APIErrorHomeyInvalidSerialNumber
|
|
467
|
-
) {
|
|
468
|
-
reject(err);
|
|
469
|
-
return;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
reject(new APIErrorHomeyOffline(err));
|
|
473
|
-
});
|
|
474
|
-
} else {
|
|
475
|
-
reject(new APIErrorHomeyOffline(err));
|
|
476
|
-
}
|
|
477
|
-
});
|
|
478
|
-
} else if (pings[HomeyAPI.DISCOVERY_STRATEGIES.REMOTE_FORWARDED]) {
|
|
479
|
-
pings[HomeyAPI.DISCOVERY_STRATEGIES.REMOTE_FORWARDED]
|
|
480
|
-
.then((result) => resolve(result))
|
|
481
|
-
.catch((err) => {
|
|
482
|
-
if (pings[HomeyAPI.DISCOVERY_STRATEGIES.CLOUD]) {
|
|
483
|
-
pings[HomeyAPI.DISCOVERY_STRATEGIES.CLOUD]
|
|
484
|
-
.then((result) => resolve(result))
|
|
485
|
-
.catch((err) => {
|
|
486
|
-
if (
|
|
487
|
-
err instanceof APIErrorHomeySubscriptionInactive ||
|
|
488
|
-
err instanceof APIErrorHomeyInvalidSerialNumber
|
|
489
|
-
) {
|
|
490
|
-
reject(err);
|
|
491
|
-
return;
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
reject(new APIErrorHomeyOffline(err));
|
|
495
|
-
});
|
|
496
|
-
} else {
|
|
497
|
-
reject(new APIErrorHomeyOffline(err));
|
|
498
|
-
}
|
|
499
|
-
});
|
|
500
|
-
} else if (pings[HomeyAPI.DISCOVERY_STRATEGIES.CLOUD]) {
|
|
501
|
-
pings[HomeyAPI.DISCOVERY_STRATEGIES.CLOUD]
|
|
502
|
-
.then((result) => resolve(result))
|
|
503
|
-
.catch((err) => {
|
|
504
|
-
if (
|
|
505
|
-
err instanceof APIErrorHomeySubscriptionInactive ||
|
|
506
|
-
err instanceof APIErrorHomeyInvalidSerialNumber
|
|
507
|
-
) {
|
|
508
|
-
reject(err);
|
|
509
|
-
return;
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
reject(new APIErrorHomeyOffline(err));
|
|
513
|
-
});
|
|
514
|
-
} else {
|
|
515
|
-
reject(new APIErrorHomeyOffline());
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
return promise;
|
|
519
214
|
}
|
|
520
215
|
|
|
521
216
|
async call({
|
package/lib/Util.js
CHANGED
|
@@ -2,6 +2,244 @@
|
|
|
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
|
+
|
|
5
243
|
/**
|
|
6
244
|
* Helper Utility Class
|
|
7
245
|
* @class
|
|
@@ -19,16 +257,41 @@ class Util {
|
|
|
19
257
|
*/
|
|
20
258
|
static async fetch(url, options, timeoutDuration, timeoutMessage) {
|
|
21
259
|
options = { ...options };
|
|
22
|
-
let
|
|
260
|
+
let timeoutTimer = null;
|
|
261
|
+
let composedAbortController = null;
|
|
262
|
+
let didTimeoutTriggerAbort = false;
|
|
263
|
+
let externalSignal = null;
|
|
264
|
+
let handleExternalAbort = null;
|
|
23
265
|
|
|
24
266
|
if (timeoutDuration != null) {
|
|
25
|
-
|
|
267
|
+
composedAbortController = new AbortController();
|
|
268
|
+
externalSignal = options.signal;
|
|
269
|
+
|
|
270
|
+
if (externalSignal) {
|
|
271
|
+
if (externalSignal.aborted) {
|
|
272
|
+
composedAbortController.abort(externalSignal.reason);
|
|
273
|
+
} else {
|
|
274
|
+
handleExternalAbort = () => {
|
|
275
|
+
composedAbortController.abort(externalSignal.reason);
|
|
276
|
+
};
|
|
277
|
+
externalSignal.addEventListener(
|
|
278
|
+
'abort',
|
|
279
|
+
handleExternalAbort,
|
|
280
|
+
{ once: true }
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
26
284
|
|
|
27
|
-
|
|
28
|
-
|
|
285
|
+
timeoutTimer = setTimeout(() => {
|
|
286
|
+
if (composedAbortController.signal.aborted) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
didTimeoutTriggerAbort = true;
|
|
291
|
+
composedAbortController.abort('Timeout');
|
|
29
292
|
}, timeoutDuration);
|
|
30
293
|
|
|
31
|
-
options.signal =
|
|
294
|
+
options.signal = composedAbortController.signal;
|
|
32
295
|
}
|
|
33
296
|
|
|
34
297
|
let responsePromise = null;
|
|
@@ -39,6 +302,13 @@ class Util {
|
|
|
39
302
|
responsePromise = window.fetch(url, options);
|
|
40
303
|
} else if (this.isNodeJS()) {
|
|
41
304
|
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
|
+
|
|
42
312
|
responsePromise = fetch(url, options);
|
|
43
313
|
} else if (typeof fetch !== 'undefined') {
|
|
44
314
|
responsePromise = fetch(url, options);
|
|
@@ -51,20 +321,16 @@ class Util {
|
|
|
51
321
|
|
|
52
322
|
return result;
|
|
53
323
|
} catch (err) {
|
|
54
|
-
if (err.name === 'AbortError') {
|
|
55
|
-
if (options.signal && options.signal.aborted && options.signal.reason === 'Timeout') {
|
|
56
|
-
throw new APIErrorTimeout(timeoutMessage ?? `Timeout after ${timeoutDuration}ms`);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (err.type === 'aborted') {
|
|
61
|
-
// https://github.com/node-fetch/node-fetch/blob/2.x/src/abort-error.js
|
|
324
|
+
if ((err.name === 'AbortError' || err.type === 'aborted') && didTimeoutTriggerAbort) {
|
|
62
325
|
throw new APIErrorTimeout(timeoutMessage ?? `Timeout after ${timeoutDuration}ms`);
|
|
63
326
|
}
|
|
64
327
|
|
|
65
328
|
throw err;
|
|
66
329
|
} finally {
|
|
67
|
-
clearTimeout(
|
|
330
|
+
clearTimeout(timeoutTimer);
|
|
331
|
+
if (externalSignal && handleExternalAbort) {
|
|
332
|
+
externalSignal.removeEventListener('abort', handleExternalAbort);
|
|
333
|
+
}
|
|
68
334
|
}
|
|
69
335
|
}
|
|
70
336
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "homey-api",
|
|
3
|
-
"version": "3.17.
|
|
3
|
+
"version": "3.17.2",
|
|
4
4
|
"description": "Homey API",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"license": "SEE LICENSE",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
],
|
|
14
14
|
"types": "assets/types/homey-api.d.ts",
|
|
15
15
|
"scripts": {
|
|
16
|
-
"test": "
|
|
16
|
+
"test": "node --test",
|
|
17
17
|
"lint": "eslint .",
|
|
18
18
|
"serve": "concurrently \"serve jsdoc/\" \"npm run jsdoc:watch\"",
|
|
19
19
|
"build": "npm run build:specs && npm run build:jsdoc && npm run build:types && npm run build:webpack;",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
},
|
|
47
47
|
"homepage": "https://github.com/athombv/node-homey-api#readme",
|
|
48
48
|
"engines": {
|
|
49
|
-
"node": ">=
|
|
49
|
+
"node": ">=22"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"engine.io-client": "^3.5.5",
|
|
@@ -59,7 +59,6 @@
|
|
|
59
59
|
"@babel/core": "^7.16.0",
|
|
60
60
|
"@babel/plugin-proposal-class-properties": "^7.16.0",
|
|
61
61
|
"@babel/preset-env": "^7.16.0",
|
|
62
|
-
"@types/jest": "^29.5.12",
|
|
63
62
|
"@typescript-eslint/eslint-plugin": "^6.1.0",
|
|
64
63
|
"@typescript-eslint/parser": "^6.1.0",
|
|
65
64
|
"babel-loader": "^8.2.3",
|
|
@@ -67,12 +66,10 @@
|
|
|
67
66
|
"ejs": "^3.1.6",
|
|
68
67
|
"eslint": "^7.32.0",
|
|
69
68
|
"eslint-config-athom": "^2.1.1",
|
|
70
|
-
"eslint-plugin-jest": "^27.4.0",
|
|
71
69
|
"express": "^4.18.2",
|
|
72
70
|
"fs-extra": "^10.0.0",
|
|
73
71
|
"homey-api": "^3.0.8",
|
|
74
72
|
"http-server": "^0.12.3",
|
|
75
|
-
"jest": "^30.0.0-alpha.2",
|
|
76
73
|
"jsdoc": "4.0.5",
|
|
77
74
|
"jsdoc-to-markdown": "^8.0.0",
|
|
78
75
|
"jsdoc-ts-utils": "^4.0.0",
|