homey-api 3.16.1 → 3.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/types/homey-api.private.d.ts +0 -24
- package/lib/HomeyAPI/HomeyAPIV3/DiscoveryManager.js +290 -0
- package/lib/HomeyAPI/HomeyAPIV3/Item.js +2 -32
- package/lib/HomeyAPI/HomeyAPIV3/Manager.js +2 -13
- package/lib/HomeyAPI/HomeyAPIV3.js +20 -325
- package/lib/Util.js +280 -100
- package/package.json +3 -6
|
@@ -5782,12 +5782,6 @@ export class Util {
|
|
|
5782
5782
|
|
|
5783
5783
|
static encodeUrlSearchParams(params: object): string;
|
|
5784
5784
|
|
|
5785
|
-
static deepEqual(
|
|
5786
|
-
a: any,
|
|
5787
|
-
|
|
5788
|
-
b: any,
|
|
5789
|
-
): boolean;
|
|
5790
|
-
|
|
5791
5785
|
static fetch(
|
|
5792
5786
|
args: string,
|
|
5793
5787
|
|
|
@@ -5829,12 +5823,6 @@ export class Util {
|
|
|
5829
5823
|
static serializeQueryObject(queryObject: object): string;
|
|
5830
5824
|
|
|
5831
5825
|
static encodeUrlSearchParams(params: object): string;
|
|
5832
|
-
|
|
5833
|
-
static deepEqual(
|
|
5834
|
-
a: any,
|
|
5835
|
-
|
|
5836
|
-
b: any,
|
|
5837
|
-
): boolean;
|
|
5838
5826
|
}
|
|
5839
5827
|
|
|
5840
5828
|
export class APIDefinition {}
|
|
@@ -9212,12 +9200,6 @@ export class Util {
|
|
|
9212
9200
|
|
|
9213
9201
|
static encodeUrlSearchParams(params: object): string;
|
|
9214
9202
|
|
|
9215
|
-
static deepEqual(
|
|
9216
|
-
a: any,
|
|
9217
|
-
|
|
9218
|
-
b: any,
|
|
9219
|
-
): boolean;
|
|
9220
|
-
|
|
9221
9203
|
static fetch(
|
|
9222
9204
|
args: string,
|
|
9223
9205
|
|
|
@@ -9259,12 +9241,6 @@ export class Util {
|
|
|
9259
9241
|
static serializeQueryObject(queryObject: object): string;
|
|
9260
9242
|
|
|
9261
9243
|
static encodeUrlSearchParams(params: object): string;
|
|
9262
|
-
|
|
9263
|
-
static deepEqual(
|
|
9264
|
-
a: any,
|
|
9265
|
-
|
|
9266
|
-
b: any,
|
|
9267
|
-
): boolean;
|
|
9268
9244
|
}
|
|
9269
9245
|
|
|
9270
9246
|
export namespace HomeyAPIV3Cloud {
|
|
@@ -0,0 +1,290 @@
|
|
|
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
|
+
urls[DISCOVERY_STRATEGIES.MDNS] = `http://homey-${this.homey.id}.local`;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (this.homey.__strategies.includes(DISCOVERY_STRATEGIES.LOCAL)) {
|
|
24
|
+
if (Util.isHTTPUnsecureSupported() && this.homey.__properties.localUrl) {
|
|
25
|
+
urls[DISCOVERY_STRATEGIES.LOCAL] = `${this.homey.__properties.localUrl}`;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (this.homey.__strategies.includes(DISCOVERY_STRATEGIES.LOCAL_SECURE)) {
|
|
30
|
+
if (this.homey.__properties.localUrlSecure) {
|
|
31
|
+
urls[DISCOVERY_STRATEGIES.LOCAL_SECURE] = `${this.homey.__properties.localUrlSecure}`;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (this.homey.__strategies.includes(DISCOVERY_STRATEGIES.CLOUD)) {
|
|
36
|
+
if (this.homey.__properties.remoteUrl) {
|
|
37
|
+
urls[DISCOVERY_STRATEGIES.CLOUD] = `${this.homey.__properties.remoteUrl}`;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (this.homey.__strategies.includes(DISCOVERY_STRATEGIES.REMOTE_FORWARDED)) {
|
|
42
|
+
if (this.homey.__properties.remoteUrlForwarded) {
|
|
43
|
+
urls[DISCOVERY_STRATEGIES.REMOTE_FORWARDED] =
|
|
44
|
+
`${this.homey.__properties.remoteUrlForwarded}`;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!Object.keys(urls).length) {
|
|
49
|
+
throw new Error('No Discovery Strategies Available');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
this.homey.__debug(`Discovery Strategies: ${Object.keys(urls).join(',')}`);
|
|
53
|
+
|
|
54
|
+
let resolve;
|
|
55
|
+
let reject;
|
|
56
|
+
|
|
57
|
+
const promise = new Promise((resolve_, reject_) => {
|
|
58
|
+
resolve = resolve_;
|
|
59
|
+
reject = reject_;
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const pingAbortControllers = {};
|
|
63
|
+
|
|
64
|
+
const abortPendingPings = ({ exceptStrategyId } = {}) => {
|
|
65
|
+
Object.entries(pingAbortControllers).forEach(([strategyId, abortController]) => {
|
|
66
|
+
if (strategyId === exceptStrategyId) return;
|
|
67
|
+
if (abortController.signal.aborted) return;
|
|
68
|
+
abortController.abort('Discovery Completed');
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const resolveAndAbortPendingPings = (result) => {
|
|
73
|
+
abortPendingPings({ exceptStrategyId: result.strategyId });
|
|
74
|
+
resolve(result);
|
|
75
|
+
return result;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const rejectAndAbortPendingPings = (error) => {
|
|
79
|
+
abortPendingPings();
|
|
80
|
+
reject(error);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const isSubscriptionError = (error) => {
|
|
84
|
+
return (
|
|
85
|
+
error instanceof APIErrorHomeySubscriptionInactive ||
|
|
86
|
+
error instanceof APIErrorHomeyInvalidSerialNumber
|
|
87
|
+
);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const normalizeDiscoveryError = (error) => {
|
|
91
|
+
if (error instanceof AggregateError) {
|
|
92
|
+
for (const err of error.errors) {
|
|
93
|
+
if (isSubscriptionError(err)) {
|
|
94
|
+
return err;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return new APIErrorHomeyOffline();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (isSubscriptionError(error) || error instanceof APIErrorHomeyOffline) {
|
|
102
|
+
return error;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return new APIErrorHomeyOffline(error);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const ping = async (strategyId, timeout) => {
|
|
109
|
+
const baseUrl = urls[strategyId];
|
|
110
|
+
const abortController = new AbortController();
|
|
111
|
+
pingAbortControllers[strategyId] = abortController;
|
|
112
|
+
|
|
113
|
+
const response = await Util.fetch(
|
|
114
|
+
`${baseUrl}/api/manager/system/ping?id=${this.homey.id}`,
|
|
115
|
+
{
|
|
116
|
+
headers: {
|
|
117
|
+
'X-Homey-ID': this.homey.id,
|
|
118
|
+
},
|
|
119
|
+
signal: abortController.signal,
|
|
120
|
+
},
|
|
121
|
+
timeout
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const text = await response.text();
|
|
125
|
+
|
|
126
|
+
if (!response.ok) {
|
|
127
|
+
const responseContentType = response.headers.get('Content-Type');
|
|
128
|
+
let parsed;
|
|
129
|
+
|
|
130
|
+
if (responseContentType && responseContentType.toLowerCase().includes('application/json')) {
|
|
131
|
+
try {
|
|
132
|
+
parsed = JSON.parse(text);
|
|
133
|
+
// eslint-disable-next-line no-empty
|
|
134
|
+
} catch (err) {}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (typeof parsed === 'object' && parsed !== null) {
|
|
138
|
+
if (
|
|
139
|
+
parsed.error &&
|
|
140
|
+
parsed.error === 'Subscription Inactive' &&
|
|
141
|
+
parsed.statusCode === 403
|
|
142
|
+
) {
|
|
143
|
+
throw new APIErrorHomeySubscriptionInactive(parsed.error, parsed.statusCode);
|
|
144
|
+
} else if (
|
|
145
|
+
parsed.error &&
|
|
146
|
+
parsed.error === 'Invalid Serial Number' &&
|
|
147
|
+
parsed.statusCode === 403
|
|
148
|
+
) {
|
|
149
|
+
throw new APIErrorHomeyInvalidSerialNumber(parsed.error, parsed.statusCode);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
throw new Error(text || response.statusText);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (text === 'false') {
|
|
157
|
+
throw new Error('Invalid Homey ID');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const homeyId = response.headers.get('X-Homey-ID');
|
|
161
|
+
|
|
162
|
+
if (homeyId && homeyId !== this.homey.id) {
|
|
163
|
+
throw new Error('Invalid Homey ID');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const homeyVersion = response.headers.get('X-Homey-Version');
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
baseUrl,
|
|
170
|
+
strategyId,
|
|
171
|
+
homeyVersion,
|
|
172
|
+
};
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const pings = {};
|
|
176
|
+
|
|
177
|
+
if (urls[DISCOVERY_STRATEGIES.LOCAL_SECURE]) {
|
|
178
|
+
pings[DISCOVERY_STRATEGIES.LOCAL_SECURE] = ping(
|
|
179
|
+
DISCOVERY_STRATEGIES.LOCAL_SECURE,
|
|
180
|
+
1200
|
|
181
|
+
);
|
|
182
|
+
pings[DISCOVERY_STRATEGIES.LOCAL_SECURE].catch((err) => {
|
|
183
|
+
this.homey.__debug(
|
|
184
|
+
`Ping ${DISCOVERY_STRATEGIES.LOCAL_SECURE} Error:`,
|
|
185
|
+
err && err.message
|
|
186
|
+
);
|
|
187
|
+
this.homey.__debug(urls[DISCOVERY_STRATEGIES.LOCAL_SECURE]);
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (urls[DISCOVERY_STRATEGIES.LOCAL]) {
|
|
192
|
+
pings[DISCOVERY_STRATEGIES.LOCAL] = ping(DISCOVERY_STRATEGIES.LOCAL, 1000);
|
|
193
|
+
pings[DISCOVERY_STRATEGIES.LOCAL].catch((err) =>
|
|
194
|
+
this.homey.__debug(`Ping ${DISCOVERY_STRATEGIES.LOCAL} Error:`, err && err.message)
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (urls[DISCOVERY_STRATEGIES.MDNS]) {
|
|
199
|
+
pings[DISCOVERY_STRATEGIES.MDNS] = ping(DISCOVERY_STRATEGIES.MDNS, 3000);
|
|
200
|
+
pings[DISCOVERY_STRATEGIES.MDNS].catch((err) =>
|
|
201
|
+
this.homey.__debug(`Ping ${DISCOVERY_STRATEGIES.MDNS} Error:`, err && err.message)
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (urls[DISCOVERY_STRATEGIES.CLOUD]) {
|
|
206
|
+
pings[DISCOVERY_STRATEGIES.CLOUD] = ping(DISCOVERY_STRATEGIES.CLOUD, 5000);
|
|
207
|
+
pings[DISCOVERY_STRATEGIES.CLOUD].catch((err) =>
|
|
208
|
+
this.homey.__debug(`Ping ${DISCOVERY_STRATEGIES.CLOUD} Error:`, err && err.message)
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (urls[DISCOVERY_STRATEGIES.REMOTE_FORWARDED]) {
|
|
213
|
+
pings[DISCOVERY_STRATEGIES.REMOTE_FORWARDED] = ping(
|
|
214
|
+
DISCOVERY_STRATEGIES.REMOTE_FORWARDED,
|
|
215
|
+
2000
|
|
216
|
+
);
|
|
217
|
+
pings[DISCOVERY_STRATEGIES.REMOTE_FORWARDED].catch((err) =>
|
|
218
|
+
this.homey.__debug(
|
|
219
|
+
`Ping ${DISCOVERY_STRATEGIES.REMOTE_FORWARDED} Error:`,
|
|
220
|
+
err && err.message
|
|
221
|
+
)
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const withCloudFallback = (primaryPromise) => {
|
|
226
|
+
return primaryPromise.catch((error) => {
|
|
227
|
+
if (pings[DISCOVERY_STRATEGIES.CLOUD]) {
|
|
228
|
+
return pings[DISCOVERY_STRATEGIES.CLOUD];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
throw new APIErrorHomeyOffline(error);
|
|
232
|
+
});
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
let selectedRoutePromise;
|
|
236
|
+
|
|
237
|
+
if (pings[DISCOVERY_STRATEGIES.LOCAL_SECURE]) {
|
|
238
|
+
selectedRoutePromise = pings[DISCOVERY_STRATEGIES.LOCAL_SECURE]
|
|
239
|
+
.catch((error) => {
|
|
240
|
+
const fallbackPromises = [];
|
|
241
|
+
|
|
242
|
+
if (pings[DISCOVERY_STRATEGIES.LOCAL]) {
|
|
243
|
+
fallbackPromises.push(pings[DISCOVERY_STRATEGIES.LOCAL]);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (pings[DISCOVERY_STRATEGIES.REMOTE_FORWARDED]) {
|
|
247
|
+
fallbackPromises.push(pings[DISCOVERY_STRATEGIES.REMOTE_FORWARDED]);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (pings[DISCOVERY_STRATEGIES.MDNS]) {
|
|
251
|
+
fallbackPromises.push(pings[DISCOVERY_STRATEGIES.MDNS]);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (pings[DISCOVERY_STRATEGIES.CLOUD]) {
|
|
255
|
+
fallbackPromises.push(pings[DISCOVERY_STRATEGIES.CLOUD]);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (isSubscriptionError(error)) {
|
|
259
|
+
throw error;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (!fallbackPromises.length) {
|
|
263
|
+
throw new APIErrorHomeyOffline();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return Promise.any(fallbackPromises);
|
|
267
|
+
});
|
|
268
|
+
} else if (pings[DISCOVERY_STRATEGIES.LOCAL]) {
|
|
269
|
+
selectedRoutePromise = withCloudFallback(pings[DISCOVERY_STRATEGIES.LOCAL]);
|
|
270
|
+
} else if (pings[DISCOVERY_STRATEGIES.MDNS]) {
|
|
271
|
+
selectedRoutePromise = withCloudFallback(pings[DISCOVERY_STRATEGIES.MDNS]);
|
|
272
|
+
} else if (pings[DISCOVERY_STRATEGIES.REMOTE_FORWARDED]) {
|
|
273
|
+
selectedRoutePromise = withCloudFallback(pings[DISCOVERY_STRATEGIES.REMOTE_FORWARDED]);
|
|
274
|
+
} else if (pings[DISCOVERY_STRATEGIES.CLOUD]) {
|
|
275
|
+
selectedRoutePromise = pings[DISCOVERY_STRATEGIES.CLOUD];
|
|
276
|
+
} else {
|
|
277
|
+
selectedRoutePromise = Promise.reject(new APIErrorHomeyOffline());
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
selectedRoutePromise
|
|
281
|
+
.then((result) => resolveAndAbortPendingPings(result))
|
|
282
|
+
.catch((error) => {
|
|
283
|
+
rejectAndAbortPendingPings(normalizeDiscoveryError(error));
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
return promise;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
module.exports = DiscoveryManager;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const EventEmitter = require('../../EventEmitter');
|
|
4
|
-
const Util = require('../../Util');
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
6
|
* A superclass for all CRUD Items.
|
|
@@ -95,43 +94,14 @@ class Item extends EventEmitter {
|
|
|
95
94
|
}
|
|
96
95
|
|
|
97
96
|
__update(properties) {
|
|
98
|
-
const oldValues = {};
|
|
99
|
-
const newValues = {};
|
|
100
|
-
const changedKeys = [];
|
|
101
|
-
|
|
102
97
|
for (const [key, value] of Object.entries(properties)) {
|
|
103
98
|
if (key === 'id') continue;
|
|
104
|
-
|
|
105
|
-
if (!Util.deepEqual(this[key], value)) {
|
|
106
|
-
oldValues[key] = Util.deepClone(this[key]);
|
|
107
|
-
newValues[key] = Util.deepClone(value);
|
|
108
|
-
changedKeys.push(key);
|
|
109
|
-
|
|
110
|
-
this[key] = value;
|
|
111
|
-
}
|
|
99
|
+
this[key] = value;
|
|
112
100
|
}
|
|
113
101
|
|
|
114
102
|
this.__lastUpdated = new Date();
|
|
115
103
|
|
|
116
|
-
|
|
117
|
-
return {
|
|
118
|
-
oldValues,
|
|
119
|
-
newValues,
|
|
120
|
-
changedKeys,
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
this.emit('update', properties, {
|
|
125
|
-
oldValues,
|
|
126
|
-
newValues,
|
|
127
|
-
changedKeys,
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
return {
|
|
131
|
-
oldValues,
|
|
132
|
-
newValues,
|
|
133
|
-
changedKeys,
|
|
134
|
-
};
|
|
104
|
+
this.emit('update', properties);
|
|
135
105
|
}
|
|
136
106
|
|
|
137
107
|
__delete() {
|
|
@@ -531,19 +531,8 @@ class Manager extends EventEmitter {
|
|
|
531
531
|
|
|
532
532
|
if (this.__cache[ItemClass.ID][props.id]) {
|
|
533
533
|
const item = this.__cache[ItemClass.ID][props.id];
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
newValues,
|
|
537
|
-
changedKeys,
|
|
538
|
-
} = item.__update(props);
|
|
539
|
-
|
|
540
|
-
if (changedKeys.length === 0) return;
|
|
541
|
-
|
|
542
|
-
return this.emit(event, item, {
|
|
543
|
-
oldValues,
|
|
544
|
-
newValues,
|
|
545
|
-
changedKeys,
|
|
546
|
-
});
|
|
534
|
+
item.__update(props);
|
|
535
|
+
return this.emit(event, item);
|
|
547
536
|
}
|
|
548
537
|
|
|
549
538
|
break;
|
|
@@ -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
|
+
}
|
|
284
|
+
|
|
285
|
+
timeoutTimer = setTimeout(() => {
|
|
286
|
+
if (composedAbortController.signal.aborted) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
26
289
|
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
|
@@ -338,92 +604,6 @@ class Util {
|
|
|
338
604
|
return encodedPairs.join('&');
|
|
339
605
|
}
|
|
340
606
|
|
|
341
|
-
/**
|
|
342
|
-
* Deep equality check between two values.
|
|
343
|
-
* @param {any} a
|
|
344
|
-
* @param {any} b
|
|
345
|
-
* @returns {boolean}
|
|
346
|
-
*/
|
|
347
|
-
static deepEqual(a, b) {
|
|
348
|
-
if (a === b) return true;
|
|
349
|
-
|
|
350
|
-
if (
|
|
351
|
-
typeof a !== 'object' ||
|
|
352
|
-
typeof b !== 'object' ||
|
|
353
|
-
a === null ||
|
|
354
|
-
b === null
|
|
355
|
-
) {
|
|
356
|
-
return false;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
const keysA = Object.keys(a);
|
|
360
|
-
const keysB = Object.keys(b);
|
|
361
|
-
|
|
362
|
-
if (keysA.length !== keysB.length) return false;
|
|
363
|
-
|
|
364
|
-
for (const key of keysA) {
|
|
365
|
-
if (!keysB.includes(key) || !this.deepEqual(a[key], b[key])) {
|
|
366
|
-
return false;
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
return true;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
static deepClone(value) {
|
|
374
|
-
// Use native structuredClone when available
|
|
375
|
-
if (typeof structuredClone === 'function') {
|
|
376
|
-
return structuredClone(value);
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
const seen = new WeakMap();
|
|
380
|
-
|
|
381
|
-
function clone(val) {
|
|
382
|
-
if (val === null || typeof val !== 'object') {
|
|
383
|
-
return val;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
if (seen.has(val)) {
|
|
387
|
-
return seen.get(val);
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
if (val instanceof Date) {
|
|
391
|
-
return new Date(val);
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
if (val instanceof Map) {
|
|
395
|
-
const map = new Map();
|
|
396
|
-
seen.set(val, map);
|
|
397
|
-
val.forEach((v, k) => map.set(clone(k), clone(v)));
|
|
398
|
-
return map;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
if (val instanceof Set) {
|
|
402
|
-
const set = new Set();
|
|
403
|
-
seen.set(val, set);
|
|
404
|
-
val.forEach(v => set.add(clone(v)));
|
|
405
|
-
return set;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
if (Array.isArray(val)) {
|
|
409
|
-
const arr = [];
|
|
410
|
-
seen.set(val, arr);
|
|
411
|
-
val.forEach((v, i) => arr[i] = clone(v));
|
|
412
|
-
return arr;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
const obj = {};
|
|
416
|
-
seen.set(val, obj);
|
|
417
|
-
Object.keys(val).forEach(key => {
|
|
418
|
-
obj[key] = clone(val[key]);
|
|
419
|
-
});
|
|
420
|
-
|
|
421
|
-
return obj;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
return clone(value);
|
|
425
|
-
}
|
|
426
|
-
|
|
427
607
|
}
|
|
428
608
|
|
|
429
609
|
module.exports = Util;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "homey-api",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.17.1",
|
|
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",
|