@camstack/addon-provider-bosch 0.1.49 → 0.1.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/dist/{account-integration-offline-Dcx0sq_v.js → account-integration-offline-7CH3r5jI.js} +1 -1
- package/dist/{account-integration-offline-BKDol6kz.mjs → account-integration-offline-B1Wgr_rT.mjs} +1 -1
- package/dist/{bosch-instances-BHFcR1TK.js → bosch-instances-CYBnIcHg.js} +12 -0
- package/dist/{bosch-instances-B2e4DAI1.mjs → bosch-instances-CdvcBYye.mjs} +1 -1
- package/dist/home-connect/addon.js +2 -2
- package/dist/home-connect/addon.mjs +2 -2
- package/dist/homecom/addon.js +397 -3
- package/dist/homecom/addon.mjs +397 -3
- package/dist/shc/addon.js +1 -1
- package/dist/shc/addon.mjs +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.1.50] - 2026-11-11
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- HomeCom: a read-only resource-tree explorer action for diagnosing what a gateway serves
|
|
15
|
+
|
|
10
16
|
## [0.1.49] - 2026-11-11
|
|
11
17
|
|
|
12
18
|
### Changed
|
package/dist/{account-integration-offline-Dcx0sq_v.js → account-integration-offline-7CH3r5jI.js}
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const require_bosch_instances = require("./bosch-instances-
|
|
1
|
+
const require_bosch_instances = require("./bosch-instances-CYBnIcHg.js");
|
|
2
2
|
let node_crypto = require("node:crypto");
|
|
3
3
|
//#region src/shared/sign-in-redirects.ts
|
|
4
4
|
/** The addresses each vendor sends the browser to after a sign-in; no browser can open them. Kept free of imports so the form schema can name them. */
|
package/dist/{account-integration-offline-BKDol6kz.mjs → account-integration-offline-B1Wgr_rT.mjs}
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Q as errMsg } from "./bosch-instances-CdvcBYye.mjs";
|
|
2
2
|
import { randomBytes } from "node:crypto";
|
|
3
3
|
//#region src/shared/sign-in-redirects.ts
|
|
4
4
|
/** The addresses each vendor sends the browser to after a sign-in; no browser can open them. Kept free of imports so the form schema can name them. */
|
|
@@ -102380,6 +102380,12 @@ Object.defineProperty(exports, "_enum", {
|
|
|
102380
102380
|
return _enum;
|
|
102381
102381
|
}
|
|
102382
102382
|
});
|
|
102383
|
+
Object.defineProperty(exports, "_null", {
|
|
102384
|
+
enumerable: true,
|
|
102385
|
+
get: function() {
|
|
102386
|
+
return _null;
|
|
102387
|
+
}
|
|
102388
|
+
});
|
|
102383
102389
|
Object.defineProperty(exports, "array", {
|
|
102384
102390
|
enumerable: true,
|
|
102385
102391
|
get: function() {
|
|
@@ -102398,6 +102404,12 @@ Object.defineProperty(exports, "binaryCapability", {
|
|
|
102398
102404
|
return binaryCapability;
|
|
102399
102405
|
}
|
|
102400
102406
|
});
|
|
102407
|
+
Object.defineProperty(exports, "boolean", {
|
|
102408
|
+
enumerable: true,
|
|
102409
|
+
get: function() {
|
|
102410
|
+
return boolean;
|
|
102411
|
+
}
|
|
102412
|
+
});
|
|
102401
102413
|
Object.defineProperty(exports, "boschRowsMigration", {
|
|
102402
102414
|
enumerable: true,
|
|
102403
102415
|
get: function() {
|
|
@@ -102326,4 +102326,4 @@ function keepBodyKeys(provider, keys) {
|
|
|
102326
102326
|
};
|
|
102327
102327
|
}
|
|
102328
102328
|
//#endregion
|
|
102329
|
-
export { RecordIntegrationInstances as A,
|
|
102329
|
+
export { EventCategory as $, RecordIntegrationInstances as A, _null as B, powerMeterCapability as C, switchCapability as D, runningStateCapability as E, entryBody as F, object as G, boolean as H, isSecretMarker as I, union as J, record as K, isSecretMarkerLike as L, customAction as M, defineCustomActions as N, temperatureSensorCapability as O, defineEntryKind as P, errMsg as Q, rowsDepsOver as R, numericSensorCapability as S, runningFromDishwasherOperation as T, literal as U, array as V, number$1 as W, DeviceRole as X, unknown as Y, DeviceType as Z, heatPumpControlCapability as _, BaseDevice as a, ingestReading as b, RUNNING_STATE_COLD as c, climateControlCapability as d, consumablesCapability as f, eventEmitterCapability as g, enumSensorCapability as h, testBoschInstance as i, createEvent as j, waterHeaterCapability as k, batteryCapability as l, dishwasherCapability as m, boschRowsPorts as n, BaseDeviceProvider as o, deviceAdoptionCapability as p, string as q, keepBodyKeys as r, DishwasherOperationStateSchema as s, boschRowsMigration as t, binaryCapability as u, heatPumpStatusCapability as v, quantityFromUnit as w, integrationInstancesCapability as x, heatingValveCapability as y, _enum as z };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_bosch_instances = require("../bosch-instances-
|
|
3
|
-
const require_account_integration_offline = require("../account-integration-offline-
|
|
2
|
+
const require_bosch_instances = require("../bosch-instances-CYBnIcHg.js");
|
|
3
|
+
const require_account_integration_offline = require("../account-integration-offline-7CH3r5jI.js");
|
|
4
4
|
let node_crypto = require("node:crypto");
|
|
5
5
|
let node_tls = require("node:tls");
|
|
6
6
|
let node_zlib = require("node:zlib");
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { a as loginFailureForAttempt, c as HOME_CONNECT_REDIRECT_URI, i as createSignInAttemptStore, n as cascadeRemoveDevicesForIntegration, o as parseOAuthCallback, r as buildSignInGuideFields, t as setIntegrationDevicesOnline } from "../account-integration-offline-
|
|
1
|
+
import { $ as EventCategory, A as RecordIntegrationInstances, C as powerMeterCapability, E as runningStateCapability, F as entryBody, G as object, I as isSecretMarker, J as union, K as record, L as isSecretMarkerLike, M as customAction, N as defineCustomActions, P as defineEntryKind, Q as errMsg, R as rowsDepsOver, S as numericSensorCapability, T as runningFromDishwasherOperation, U as literal, V as array, X as DeviceRole, Y as unknown, Z as DeviceType, a as BaseDevice, b as ingestReading, c as RUNNING_STATE_COLD, f as consumablesCapability, g as eventEmitterCapability, h as enumSensorCapability, i as testBoschInstance, j as createEvent, m as dishwasherCapability, n as boschRowsPorts, o as BaseDeviceProvider, p as deviceAdoptionCapability, q as string, r as keepBodyKeys, s as DishwasherOperationStateSchema, t as boschRowsMigration, u as binaryCapability, w as quantityFromUnit, x as integrationInstancesCapability, z as _enum } from "../bosch-instances-CdvcBYye.mjs";
|
|
2
|
+
import { a as loginFailureForAttempt, c as HOME_CONNECT_REDIRECT_URI, i as createSignInAttemptStore, n as cascadeRemoveDevicesForIntegration, o as parseOAuthCallback, r as buildSignInGuideFields, t as setIntegrationDevicesOnline } from "../account-integration-offline-B1Wgr_rT.mjs";
|
|
3
3
|
import { createHash, randomBytes } from "node:crypto";
|
|
4
4
|
import { connect } from "node:tls";
|
|
5
5
|
import { inflateRawSync } from "node:zlib";
|
package/dist/homecom/addon.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_bosch_instances = require("../bosch-instances-
|
|
3
|
-
const require_account_integration_offline = require("../account-integration-offline-
|
|
2
|
+
const require_bosch_instances = require("../bosch-instances-CYBnIcHg.js");
|
|
3
|
+
const require_account_integration_offline = require("../account-integration-offline-7CH3r5jI.js");
|
|
4
4
|
let node_crypto = require("node:crypto");
|
|
5
5
|
//#region src/homecom/config.ts
|
|
6
6
|
var HOMECOM_BRANDS = ["bosch", "buderus"];
|
|
@@ -178,6 +178,280 @@ function buildHomeComFormSchema() {
|
|
|
178
178
|
]
|
|
179
179
|
}] };
|
|
180
180
|
}
|
|
181
|
+
var EXPLORER_MAX_NODES_CAP = 1500;
|
|
182
|
+
var VALUE_MAX_CHARS = 200;
|
|
183
|
+
/** Roots walked when `/` does not list its children. */
|
|
184
|
+
var EXPLORER_SEED_ROOTS = [
|
|
185
|
+
"/system",
|
|
186
|
+
"/heatSources",
|
|
187
|
+
"/heatingCircuits",
|
|
188
|
+
"/dhwCircuits",
|
|
189
|
+
"/gateway",
|
|
190
|
+
"/notifications",
|
|
191
|
+
"/recordings",
|
|
192
|
+
"/devices",
|
|
193
|
+
"/solarCircuits",
|
|
194
|
+
"/ventilation"
|
|
195
|
+
];
|
|
196
|
+
/** Under the 60 s UDS request timeout a forked addon's action answers within. */
|
|
197
|
+
var EXPLORER_MAX_DEADLINE_MS = 5e4;
|
|
198
|
+
var DROPPED_SAMPLE_MAX = 20;
|
|
199
|
+
var UNVISITED_SAMPLE_MAX = 100;
|
|
200
|
+
var PATH_SHAPE = /^\/(?:[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]+)*)?$/;
|
|
201
|
+
/**
|
|
202
|
+
* A resource path this walker will request: absolute, plain segments, and no `.` or `..`
|
|
203
|
+
* segment — URL resolution would otherwise lift the request out of the gateway's tree.
|
|
204
|
+
*/
|
|
205
|
+
function isResourcePath(path) {
|
|
206
|
+
if (!PATH_SHAPE.test(path)) return false;
|
|
207
|
+
return path.split("/").slice(1).every((segment) => segment !== "." && segment !== "..");
|
|
208
|
+
}
|
|
209
|
+
var explorerOutcomeSchema = require_bosch_instances._enum([
|
|
210
|
+
"ok",
|
|
211
|
+
"forbidden",
|
|
212
|
+
"not-found",
|
|
213
|
+
"rate-limited",
|
|
214
|
+
"error"
|
|
215
|
+
]);
|
|
216
|
+
var exploredResourceSchema = require_bosch_instances.object({
|
|
217
|
+
path: require_bosch_instances.string(),
|
|
218
|
+
httpStatus: require_bosch_instances.number(),
|
|
219
|
+
outcome: explorerOutcomeSchema,
|
|
220
|
+
type: require_bosch_instances.string().optional(),
|
|
221
|
+
value: require_bosch_instances.union([
|
|
222
|
+
require_bosch_instances.string(),
|
|
223
|
+
require_bosch_instances.number(),
|
|
224
|
+
require_bosch_instances.boolean(),
|
|
225
|
+
require_bosch_instances._null()
|
|
226
|
+
]).optional(),
|
|
227
|
+
unit: require_bosch_instances.string().optional(),
|
|
228
|
+
writeable: require_bosch_instances.boolean().optional(),
|
|
229
|
+
recordable: require_bosch_instances.boolean().optional(),
|
|
230
|
+
allowedValues: require_bosch_instances.array(require_bosch_instances.string()).optional(),
|
|
231
|
+
minValue: require_bosch_instances.number().optional(),
|
|
232
|
+
maxValue: require_bosch_instances.number().optional(),
|
|
233
|
+
childCount: require_bosch_instances.number().optional(),
|
|
234
|
+
/** References of this container that were not a walkable path. */
|
|
235
|
+
droppedReferences: require_bosch_instances.number().optional(),
|
|
236
|
+
error: require_bosch_instances.string().optional()
|
|
237
|
+
});
|
|
238
|
+
var explorerSummarySchema = require_bosch_instances.object({
|
|
239
|
+
ok: require_bosch_instances.number(),
|
|
240
|
+
forbidden: require_bosch_instances.number(),
|
|
241
|
+
"not-found": require_bosch_instances.number(),
|
|
242
|
+
"rate-limited": require_bosch_instances.number(),
|
|
243
|
+
error: require_bosch_instances.number()
|
|
244
|
+
});
|
|
245
|
+
var explorerStopReasonSchema = require_bosch_instances._enum([
|
|
246
|
+
"complete",
|
|
247
|
+
"max-nodes",
|
|
248
|
+
"rate-limited",
|
|
249
|
+
"deadline"
|
|
250
|
+
]);
|
|
251
|
+
function isRecord$3(value) {
|
|
252
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
253
|
+
}
|
|
254
|
+
function finite$1(value) {
|
|
255
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
256
|
+
}
|
|
257
|
+
function text$1(value) {
|
|
258
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
259
|
+
}
|
|
260
|
+
function truncate(value) {
|
|
261
|
+
return value.length > VALUE_MAX_CHARS ? `${value.slice(0, VALUE_MAX_CHARS - 1)}…` : value;
|
|
262
|
+
}
|
|
263
|
+
function shownValue(value) {
|
|
264
|
+
if (value === null || value === void 0) return null;
|
|
265
|
+
if (typeof value === "number" || typeof value === "boolean") return value;
|
|
266
|
+
if (typeof value === "string") return truncate(value);
|
|
267
|
+
return truncate(JSON.stringify(value));
|
|
268
|
+
}
|
|
269
|
+
function outcomeOf(status) {
|
|
270
|
+
if (status >= 200 && status < 300) return "ok";
|
|
271
|
+
if (status === 403) return "forbidden";
|
|
272
|
+
if (status === 404) return "not-found";
|
|
273
|
+
if (status === 429) return "rate-limited";
|
|
274
|
+
return "error";
|
|
275
|
+
}
|
|
276
|
+
/** What a reference names: its `id`, else the part of its `uri` after `/resource`. */
|
|
277
|
+
function referenceText(ref) {
|
|
278
|
+
if (!isRecord$3(ref)) return null;
|
|
279
|
+
const id = text$1(ref["id"]);
|
|
280
|
+
if (id !== void 0 && id.startsWith("/")) return id;
|
|
281
|
+
const uri = text$1(ref["uri"]);
|
|
282
|
+
if (uri === void 0) return id ?? null;
|
|
283
|
+
const at = uri.indexOf("/resource/");
|
|
284
|
+
if (at < 0) return uri;
|
|
285
|
+
return uri.slice(at + 9);
|
|
286
|
+
}
|
|
287
|
+
/** The walkable resource path a reference names, or null. */
|
|
288
|
+
function referencePath(ref) {
|
|
289
|
+
const path = referenceText(ref);
|
|
290
|
+
return path !== null && isResourcePath(path) ? path : null;
|
|
291
|
+
}
|
|
292
|
+
function referencesOf(body) {
|
|
293
|
+
if (!isRecord$3(body)) return null;
|
|
294
|
+
const refs = body["references"];
|
|
295
|
+
if (!Array.isArray(refs)) return null;
|
|
296
|
+
const paths = [];
|
|
297
|
+
const dropped = [];
|
|
298
|
+
for (const ref of refs) {
|
|
299
|
+
const path = referencePath(ref);
|
|
300
|
+
if (path !== null) paths.push(path);
|
|
301
|
+
else dropped.push(truncate(referenceText(ref) ?? JSON.stringify(ref) ?? "undefined"));
|
|
302
|
+
}
|
|
303
|
+
return {
|
|
304
|
+
total: refs.length,
|
|
305
|
+
paths,
|
|
306
|
+
dropped
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
function describeLeaf(path, status, body) {
|
|
310
|
+
if (!isRecord$3(body)) return {
|
|
311
|
+
path,
|
|
312
|
+
httpStatus: status,
|
|
313
|
+
outcome: "ok"
|
|
314
|
+
};
|
|
315
|
+
const allowed = body["allowedValues"];
|
|
316
|
+
const allowedValues = Array.isArray(allowed) ? allowed.map((v) => truncate(String(v))) : void 0;
|
|
317
|
+
const type = text$1(body["type"]);
|
|
318
|
+
const unit = text$1(body["unitOfMeasure"]);
|
|
319
|
+
const minValue = finite$1(body["minValue"]);
|
|
320
|
+
const maxValue = finite$1(body["maxValue"]);
|
|
321
|
+
const writeable = body["writeable"];
|
|
322
|
+
const recordable = body["recordable"];
|
|
323
|
+
return {
|
|
324
|
+
path,
|
|
325
|
+
httpStatus: status,
|
|
326
|
+
outcome: "ok",
|
|
327
|
+
...type !== void 0 ? { type } : {},
|
|
328
|
+
..."value" in body ? { value: shownValue(body["value"]) } : {},
|
|
329
|
+
...unit !== void 0 ? { unit } : {},
|
|
330
|
+
...typeof writeable === "number" || typeof writeable === "boolean" ? { writeable: Boolean(writeable) } : {},
|
|
331
|
+
...typeof recordable === "number" || typeof recordable === "boolean" ? { recordable: Boolean(recordable) } : {},
|
|
332
|
+
...allowedValues !== void 0 ? { allowedValues } : {},
|
|
333
|
+
...minValue !== void 0 ? { minValue } : {},
|
|
334
|
+
...maxValue !== void 0 ? { maxValue } : {}
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
function emptySummary() {
|
|
338
|
+
return {
|
|
339
|
+
ok: 0,
|
|
340
|
+
forbidden: 0,
|
|
341
|
+
"not-found": 0,
|
|
342
|
+
"rate-limited": 0,
|
|
343
|
+
error: 0
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
function clampDeadline(requested) {
|
|
347
|
+
return Math.min(Math.max(Math.floor(requested ?? 45e3), 1), EXPLORER_MAX_DEADLINE_MS);
|
|
348
|
+
}
|
|
349
|
+
function clampNodes(requested) {
|
|
350
|
+
return Math.min(Math.max(Math.floor(requested ?? 400), 1), EXPLORER_MAX_NODES_CAP);
|
|
351
|
+
}
|
|
352
|
+
var defaultSleep = (ms) => new Promise((resolve) => {
|
|
353
|
+
setTimeout(resolve, ms);
|
|
354
|
+
});
|
|
355
|
+
async function exploreHomeComResources(input) {
|
|
356
|
+
const root = input.root ?? "/";
|
|
357
|
+
if (!isResourcePath(root)) throw new Error(`Not a resource path: ${root}`);
|
|
358
|
+
const maxNodes = clampNodes(input.maxNodes);
|
|
359
|
+
const delayMs = Math.max(input.delayMs ?? 150, 150);
|
|
360
|
+
const sleep = input.sleep ?? defaultSleep;
|
|
361
|
+
const now = input.now ?? Date.now;
|
|
362
|
+
const deadlineAt = now() + clampDeadline(input.deadlineMs);
|
|
363
|
+
const queue = [root];
|
|
364
|
+
const queued = new Set([root]);
|
|
365
|
+
const resources = [];
|
|
366
|
+
const summary = emptySummary();
|
|
367
|
+
let calls = 0;
|
|
368
|
+
let stoppedBy = "complete";
|
|
369
|
+
let droppedReferences = 0;
|
|
370
|
+
const droppedSamples = [];
|
|
371
|
+
const enqueue = (paths) => {
|
|
372
|
+
for (const path of paths) {
|
|
373
|
+
if (queued.has(path)) continue;
|
|
374
|
+
queued.add(path);
|
|
375
|
+
queue.push(path);
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
while (queue.length > 0) {
|
|
379
|
+
if (calls >= maxNodes) {
|
|
380
|
+
stoppedBy = "max-nodes";
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
if (now() + (calls > 0 ? delayMs : 0) >= deadlineAt) {
|
|
384
|
+
stoppedBy = "deadline";
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
const path = queue.shift();
|
|
388
|
+
if (path === void 0) break;
|
|
389
|
+
if (calls > 0) await sleep(delayMs);
|
|
390
|
+
calls += 1;
|
|
391
|
+
let answer;
|
|
392
|
+
try {
|
|
393
|
+
answer = await input.read(path);
|
|
394
|
+
} catch (err) {
|
|
395
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
396
|
+
resources.push({
|
|
397
|
+
path,
|
|
398
|
+
httpStatus: 0,
|
|
399
|
+
outcome: "error",
|
|
400
|
+
error
|
|
401
|
+
});
|
|
402
|
+
summary.error += 1;
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
const outcome = outcomeOf(answer.status);
|
|
406
|
+
summary[outcome] += 1;
|
|
407
|
+
if (outcome === "rate-limited") {
|
|
408
|
+
resources.push({
|
|
409
|
+
path,
|
|
410
|
+
httpStatus: answer.status,
|
|
411
|
+
outcome
|
|
412
|
+
});
|
|
413
|
+
stoppedBy = "rate-limited";
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
if (outcome !== "ok") {
|
|
417
|
+
resources.push({
|
|
418
|
+
path,
|
|
419
|
+
httpStatus: answer.status,
|
|
420
|
+
outcome
|
|
421
|
+
});
|
|
422
|
+
if (path === "/" && root === "/") enqueue(EXPLORER_SEED_ROOTS);
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
const children = referencesOf(answer.body);
|
|
426
|
+
if (children !== null) {
|
|
427
|
+
const type = isRecord$3(answer.body) ? text$1(answer.body["type"]) : void 0;
|
|
428
|
+
resources.push({
|
|
429
|
+
path,
|
|
430
|
+
httpStatus: answer.status,
|
|
431
|
+
outcome,
|
|
432
|
+
type: type ?? "refEnum",
|
|
433
|
+
childCount: children.total,
|
|
434
|
+
...children.dropped.length > 0 ? { droppedReferences: children.dropped.length } : {}
|
|
435
|
+
});
|
|
436
|
+
droppedReferences += children.dropped.length;
|
|
437
|
+
for (const ref of children.dropped) if (droppedSamples.length < DROPPED_SAMPLE_MAX) droppedSamples.push(`${path} -> ${ref}`);
|
|
438
|
+
enqueue(children.paths);
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
resources.push(describeLeaf(path, answer.status, answer.body));
|
|
442
|
+
if (path === "/" && root === "/") enqueue(EXPLORER_SEED_ROOTS);
|
|
443
|
+
}
|
|
444
|
+
return {
|
|
445
|
+
root,
|
|
446
|
+
resources,
|
|
447
|
+
summary,
|
|
448
|
+
stoppedBy,
|
|
449
|
+
unvisited: queue.length,
|
|
450
|
+
unvisitedPaths: queue.slice(0, UNVISITED_SAMPLE_MAX),
|
|
451
|
+
droppedReferences,
|
|
452
|
+
droppedSamples
|
|
453
|
+
};
|
|
454
|
+
}
|
|
181
455
|
//#endregion
|
|
182
456
|
//#region src/homecom/custom-actions.ts
|
|
183
457
|
var gatewayChoice = require_bosch_instances.object({
|
|
@@ -186,6 +460,27 @@ var gatewayChoice = require_bosch_instances.object({
|
|
|
186
460
|
brand: HomeComBrandSchema,
|
|
187
461
|
region: HomeComRegionSchema
|
|
188
462
|
});
|
|
463
|
+
var exploreResourcesInput = require_bosch_instances.object({
|
|
464
|
+
/** The HomeCom integration instance; may be omitted when exactly one is connected. */
|
|
465
|
+
integrationId: require_bosch_instances.string().min(1).optional(),
|
|
466
|
+
root: require_bosch_instances.string().refine(isResourcePath, "an absolute resource path such as /heatSources, no . or .. segment").optional(),
|
|
467
|
+
maxNodes: require_bosch_instances.number().int().min(1).max(EXPLORER_MAX_NODES_CAP).optional(),
|
|
468
|
+
/** Wall-clock budget; the walk returns what it has, with `stoppedBy: 'deadline'`. */
|
|
469
|
+
deadlineMs: require_bosch_instances.number().int().min(1e3).max(EXPLORER_MAX_DEADLINE_MS).optional()
|
|
470
|
+
});
|
|
471
|
+
var exploreResourcesOutput = require_bosch_instances.object({
|
|
472
|
+
integrationId: require_bosch_instances.string(),
|
|
473
|
+
/** Null when the integration has no snapshot yet: unknown, not empty. */
|
|
474
|
+
gatewayId: require_bosch_instances.string().nullable(),
|
|
475
|
+
root: require_bosch_instances.string(),
|
|
476
|
+
stoppedBy: explorerStopReasonSchema,
|
|
477
|
+
unvisited: require_bosch_instances.number(),
|
|
478
|
+
unvisitedPaths: require_bosch_instances.array(require_bosch_instances.string()),
|
|
479
|
+
droppedReferences: require_bosch_instances.number(),
|
|
480
|
+
droppedSamples: require_bosch_instances.array(require_bosch_instances.string()),
|
|
481
|
+
summary: explorerSummarySchema,
|
|
482
|
+
resources: require_bosch_instances.array(exploredResourceSchema)
|
|
483
|
+
});
|
|
189
484
|
var homeComActions = require_bosch_instances.defineCustomActions({
|
|
190
485
|
beginSignIn: require_bosch_instances.customAction(require_bosch_instances.object({}), require_bosch_instances.object({
|
|
191
486
|
attemptId: require_bosch_instances.string().min(1),
|
|
@@ -219,9 +514,95 @@ var homeComActions = require_bosch_instances.defineCustomActions({
|
|
|
219
514
|
}), {
|
|
220
515
|
kind: "mutation",
|
|
221
516
|
auth: "admin"
|
|
517
|
+
}),
|
|
518
|
+
/** Read-only walk of the gateway's resource tree, for diagnosing what it serves. */
|
|
519
|
+
exploreResources: require_bosch_instances.customAction(exploreResourcesInput, exploreResourcesOutput, {
|
|
520
|
+
kind: "query",
|
|
521
|
+
auth: "admin"
|
|
222
522
|
})
|
|
223
523
|
});
|
|
224
524
|
//#endregion
|
|
525
|
+
//#region src/homecom/homecom-explore-action.ts
|
|
526
|
+
/** The action refused before it walked anything; `reason` says why. */
|
|
527
|
+
var HomeComExploreRefusedError = class extends Error {
|
|
528
|
+
reason;
|
|
529
|
+
constructor(reason, message) {
|
|
530
|
+
super(message);
|
|
531
|
+
this.name = "HomeComExploreRefusedError";
|
|
532
|
+
this.reason = reason;
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
function pickIntegration(input, connected) {
|
|
536
|
+
if (input.integrationId !== void 0) return input.integrationId;
|
|
537
|
+
if (connected.length === 1 && connected[0] !== void 0) return connected[0];
|
|
538
|
+
if (connected.length === 0) throw new HomeComExploreRefusedError("not-connected", "No HomeCom integration is connected.");
|
|
539
|
+
throw new HomeComExploreRefusedError("ambiguous", `Several HomeCom integrations are connected; name one with integrationId (${connected.join(", ")}).`);
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* The `exploreResources` action: a read-only walk of one integration's gateway. One walk
|
|
543
|
+
* per integration at a time — a second call while one runs is refused, never queued.
|
|
544
|
+
*/
|
|
545
|
+
function createHomeComExploreAction(deps) {
|
|
546
|
+
const running = /* @__PURE__ */ new Set();
|
|
547
|
+
return async (input) => {
|
|
548
|
+
const integrationId = pickIntegration(input, deps.listConnected());
|
|
549
|
+
if (running.has(integrationId)) throw new HomeComExploreRefusedError("already-running", `A resource walk is already running for HomeCom integration ${integrationId}.`);
|
|
550
|
+
const client = deps.getClient(integrationId);
|
|
551
|
+
if (client === null) throw new HomeComExploreRefusedError("not-connected", `HomeCom integration ${integrationId} is not connected.`);
|
|
552
|
+
running.add(integrationId);
|
|
553
|
+
try {
|
|
554
|
+
return await walk(input, integrationId, client, deps);
|
|
555
|
+
} finally {
|
|
556
|
+
running.delete(integrationId);
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
async function walk(input, integrationId, client, deps) {
|
|
561
|
+
const result = await exploreHomeComResources({
|
|
562
|
+
read: (path) => client.readResource(path),
|
|
563
|
+
...input.root !== void 0 ? { root: input.root } : {},
|
|
564
|
+
...input.maxNodes !== void 0 ? { maxNodes: input.maxNodes } : {},
|
|
565
|
+
...input.deadlineMs !== void 0 ? { deadlineMs: input.deadlineMs } : {},
|
|
566
|
+
...deps.sleep !== void 0 ? { sleep: deps.sleep } : {},
|
|
567
|
+
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
568
|
+
});
|
|
569
|
+
const gatewayId = deps.getGatewayId(integrationId);
|
|
570
|
+
if (result.droppedReferences > 0) deps.logger.warn("HomeCom resource walk dropped references that are not a walkable path", {
|
|
571
|
+
tags: { integrationId },
|
|
572
|
+
meta: {
|
|
573
|
+
gatewayId,
|
|
574
|
+
droppedReferences: result.droppedReferences,
|
|
575
|
+
samples: result.droppedSamples
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
deps.logger.info("HomeCom resource tree explored", {
|
|
579
|
+
tags: { integrationId },
|
|
580
|
+
meta: {
|
|
581
|
+
gatewayId,
|
|
582
|
+
root: result.root,
|
|
583
|
+
visited: result.resources.length,
|
|
584
|
+
stoppedBy: result.stoppedBy,
|
|
585
|
+
unvisited: result.unvisited,
|
|
586
|
+
droppedReferences: result.droppedReferences,
|
|
587
|
+
...result.summary
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
return {
|
|
591
|
+
integrationId,
|
|
592
|
+
gatewayId,
|
|
593
|
+
...result
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
/** The addon's deps: registered integrations that hold a live client; gateway from the snapshot. */
|
|
597
|
+
function homeComExploreDeps(wiring) {
|
|
598
|
+
return {
|
|
599
|
+
listConnected: () => wiring.registry.list().filter((id) => wiring.connections.getClient(id) !== null),
|
|
600
|
+
getClient: (id) => wiring.connections.getClient(id),
|
|
601
|
+
getGatewayId: (id) => wiring.connections.getSnapshot(id)?.gateway.gatewayId ?? null,
|
|
602
|
+
logger: wiring.logger
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
//#endregion
|
|
225
606
|
//#region src/homecom/homecom-device-type.ts
|
|
226
607
|
/**
|
|
227
608
|
* Device types the PointT gateway list reports. The set is the one the MIT
|
|
@@ -1592,6 +1973,13 @@ function createHomeComApiClient(config, gatewayId, deps = {}) {
|
|
|
1592
1973
|
},
|
|
1593
1974
|
async writeHeatPump(writes) {
|
|
1594
1975
|
for (const write of writes) await putWrite(write);
|
|
1976
|
+
},
|
|
1977
|
+
async readResource(path) {
|
|
1978
|
+
const res = await authed(homeComResourceUrl(gatewayId, path), { method: "GET" });
|
|
1979
|
+
return {
|
|
1980
|
+
status: res.status,
|
|
1981
|
+
body: res.body
|
|
1982
|
+
};
|
|
1595
1983
|
}
|
|
1596
1984
|
};
|
|
1597
1985
|
}
|
|
@@ -3095,6 +3483,11 @@ var BoschHomeComProviderAddon = class extends require_bosch_instances.BaseDevice
|
|
|
3095
3483
|
});
|
|
3096
3484
|
await this.reconcileIntegrations();
|
|
3097
3485
|
this.subscribeIntegrationLifecycle();
|
|
3486
|
+
const explore = createHomeComExploreAction(homeComExploreDeps({
|
|
3487
|
+
registry: this.clients,
|
|
3488
|
+
connections: homeComConnections,
|
|
3489
|
+
logger: this.ctx.logger
|
|
3490
|
+
}));
|
|
3098
3491
|
return {
|
|
3099
3492
|
providers: [
|
|
3100
3493
|
...regs,
|
|
@@ -3121,7 +3514,8 @@ var BoschHomeComProviderAddon = class extends require_bosch_instances.BaseDevice
|
|
|
3121
3514
|
signOut: async () => ({
|
|
3122
3515
|
signedOut: true,
|
|
3123
3516
|
summary: "Signed out."
|
|
3124
|
-
})
|
|
3517
|
+
}),
|
|
3518
|
+
exploreResources: explore
|
|
3125
3519
|
}
|
|
3126
3520
|
};
|
|
3127
3521
|
}
|
package/dist/homecom/addon.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { A as RecordIntegrationInstances, B as
|
|
2
|
-
import { a as loginFailureForAttempt, i as createSignInAttemptStore, n as cascadeRemoveDevicesForIntegration, o as parseOAuthCallback, r as buildSignInGuideFields, s as HOMECOM_REDIRECT_URI, t as setIntegrationDevicesOnline } from "../account-integration-offline-
|
|
1
|
+
import { $ as EventCategory, A as RecordIntegrationInstances, B as _null, F as entryBody, G as object, H as boolean, J as union, M as customAction, N as defineCustomActions, P as defineEntryKind, Q as errMsg, R as rowsDepsOver, U as literal, V as array, W as number, Z as DeviceType, _ as heatPumpControlCapability, a as BaseDevice, d as climateControlCapability, i as testBoschInstance, k as waterHeaterCapability, n as boschRowsPorts, o as BaseDeviceProvider, p as deviceAdoptionCapability, q as string, r as keepBodyKeys, t as boschRowsMigration, v as heatPumpStatusCapability, x as integrationInstancesCapability, z as _enum } from "../bosch-instances-CdvcBYye.mjs";
|
|
2
|
+
import { a as loginFailureForAttempt, i as createSignInAttemptStore, n as cascadeRemoveDevicesForIntegration, o as parseOAuthCallback, r as buildSignInGuideFields, s as HOMECOM_REDIRECT_URI, t as setIntegrationDevicesOnline } from "../account-integration-offline-B1Wgr_rT.mjs";
|
|
3
3
|
import { createHash, randomBytes } from "node:crypto";
|
|
4
4
|
//#region src/homecom/config.ts
|
|
5
5
|
var HOMECOM_BRANDS = ["bosch", "buderus"];
|
|
@@ -177,6 +177,280 @@ function buildHomeComFormSchema() {
|
|
|
177
177
|
]
|
|
178
178
|
}] };
|
|
179
179
|
}
|
|
180
|
+
var EXPLORER_MAX_NODES_CAP = 1500;
|
|
181
|
+
var VALUE_MAX_CHARS = 200;
|
|
182
|
+
/** Roots walked when `/` does not list its children. */
|
|
183
|
+
var EXPLORER_SEED_ROOTS = [
|
|
184
|
+
"/system",
|
|
185
|
+
"/heatSources",
|
|
186
|
+
"/heatingCircuits",
|
|
187
|
+
"/dhwCircuits",
|
|
188
|
+
"/gateway",
|
|
189
|
+
"/notifications",
|
|
190
|
+
"/recordings",
|
|
191
|
+
"/devices",
|
|
192
|
+
"/solarCircuits",
|
|
193
|
+
"/ventilation"
|
|
194
|
+
];
|
|
195
|
+
/** Under the 60 s UDS request timeout a forked addon's action answers within. */
|
|
196
|
+
var EXPLORER_MAX_DEADLINE_MS = 5e4;
|
|
197
|
+
var DROPPED_SAMPLE_MAX = 20;
|
|
198
|
+
var UNVISITED_SAMPLE_MAX = 100;
|
|
199
|
+
var PATH_SHAPE = /^\/(?:[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]+)*)?$/;
|
|
200
|
+
/**
|
|
201
|
+
* A resource path this walker will request: absolute, plain segments, and no `.` or `..`
|
|
202
|
+
* segment — URL resolution would otherwise lift the request out of the gateway's tree.
|
|
203
|
+
*/
|
|
204
|
+
function isResourcePath(path) {
|
|
205
|
+
if (!PATH_SHAPE.test(path)) return false;
|
|
206
|
+
return path.split("/").slice(1).every((segment) => segment !== "." && segment !== "..");
|
|
207
|
+
}
|
|
208
|
+
var explorerOutcomeSchema = _enum([
|
|
209
|
+
"ok",
|
|
210
|
+
"forbidden",
|
|
211
|
+
"not-found",
|
|
212
|
+
"rate-limited",
|
|
213
|
+
"error"
|
|
214
|
+
]);
|
|
215
|
+
var exploredResourceSchema = object({
|
|
216
|
+
path: string(),
|
|
217
|
+
httpStatus: number(),
|
|
218
|
+
outcome: explorerOutcomeSchema,
|
|
219
|
+
type: string().optional(),
|
|
220
|
+
value: union([
|
|
221
|
+
string(),
|
|
222
|
+
number(),
|
|
223
|
+
boolean(),
|
|
224
|
+
_null()
|
|
225
|
+
]).optional(),
|
|
226
|
+
unit: string().optional(),
|
|
227
|
+
writeable: boolean().optional(),
|
|
228
|
+
recordable: boolean().optional(),
|
|
229
|
+
allowedValues: array(string()).optional(),
|
|
230
|
+
minValue: number().optional(),
|
|
231
|
+
maxValue: number().optional(),
|
|
232
|
+
childCount: number().optional(),
|
|
233
|
+
/** References of this container that were not a walkable path. */
|
|
234
|
+
droppedReferences: number().optional(),
|
|
235
|
+
error: string().optional()
|
|
236
|
+
});
|
|
237
|
+
var explorerSummarySchema = object({
|
|
238
|
+
ok: number(),
|
|
239
|
+
forbidden: number(),
|
|
240
|
+
"not-found": number(),
|
|
241
|
+
"rate-limited": number(),
|
|
242
|
+
error: number()
|
|
243
|
+
});
|
|
244
|
+
var explorerStopReasonSchema = _enum([
|
|
245
|
+
"complete",
|
|
246
|
+
"max-nodes",
|
|
247
|
+
"rate-limited",
|
|
248
|
+
"deadline"
|
|
249
|
+
]);
|
|
250
|
+
function isRecord$3(value) {
|
|
251
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
252
|
+
}
|
|
253
|
+
function finite$1(value) {
|
|
254
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
255
|
+
}
|
|
256
|
+
function text$1(value) {
|
|
257
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
258
|
+
}
|
|
259
|
+
function truncate(value) {
|
|
260
|
+
return value.length > VALUE_MAX_CHARS ? `${value.slice(0, VALUE_MAX_CHARS - 1)}…` : value;
|
|
261
|
+
}
|
|
262
|
+
function shownValue(value) {
|
|
263
|
+
if (value === null || value === void 0) return null;
|
|
264
|
+
if (typeof value === "number" || typeof value === "boolean") return value;
|
|
265
|
+
if (typeof value === "string") return truncate(value);
|
|
266
|
+
return truncate(JSON.stringify(value));
|
|
267
|
+
}
|
|
268
|
+
function outcomeOf(status) {
|
|
269
|
+
if (status >= 200 && status < 300) return "ok";
|
|
270
|
+
if (status === 403) return "forbidden";
|
|
271
|
+
if (status === 404) return "not-found";
|
|
272
|
+
if (status === 429) return "rate-limited";
|
|
273
|
+
return "error";
|
|
274
|
+
}
|
|
275
|
+
/** What a reference names: its `id`, else the part of its `uri` after `/resource`. */
|
|
276
|
+
function referenceText(ref) {
|
|
277
|
+
if (!isRecord$3(ref)) return null;
|
|
278
|
+
const id = text$1(ref["id"]);
|
|
279
|
+
if (id !== void 0 && id.startsWith("/")) return id;
|
|
280
|
+
const uri = text$1(ref["uri"]);
|
|
281
|
+
if (uri === void 0) return id ?? null;
|
|
282
|
+
const at = uri.indexOf("/resource/");
|
|
283
|
+
if (at < 0) return uri;
|
|
284
|
+
return uri.slice(at + 9);
|
|
285
|
+
}
|
|
286
|
+
/** The walkable resource path a reference names, or null. */
|
|
287
|
+
function referencePath(ref) {
|
|
288
|
+
const path = referenceText(ref);
|
|
289
|
+
return path !== null && isResourcePath(path) ? path : null;
|
|
290
|
+
}
|
|
291
|
+
function referencesOf(body) {
|
|
292
|
+
if (!isRecord$3(body)) return null;
|
|
293
|
+
const refs = body["references"];
|
|
294
|
+
if (!Array.isArray(refs)) return null;
|
|
295
|
+
const paths = [];
|
|
296
|
+
const dropped = [];
|
|
297
|
+
for (const ref of refs) {
|
|
298
|
+
const path = referencePath(ref);
|
|
299
|
+
if (path !== null) paths.push(path);
|
|
300
|
+
else dropped.push(truncate(referenceText(ref) ?? JSON.stringify(ref) ?? "undefined"));
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
total: refs.length,
|
|
304
|
+
paths,
|
|
305
|
+
dropped
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
function describeLeaf(path, status, body) {
|
|
309
|
+
if (!isRecord$3(body)) return {
|
|
310
|
+
path,
|
|
311
|
+
httpStatus: status,
|
|
312
|
+
outcome: "ok"
|
|
313
|
+
};
|
|
314
|
+
const allowed = body["allowedValues"];
|
|
315
|
+
const allowedValues = Array.isArray(allowed) ? allowed.map((v) => truncate(String(v))) : void 0;
|
|
316
|
+
const type = text$1(body["type"]);
|
|
317
|
+
const unit = text$1(body["unitOfMeasure"]);
|
|
318
|
+
const minValue = finite$1(body["minValue"]);
|
|
319
|
+
const maxValue = finite$1(body["maxValue"]);
|
|
320
|
+
const writeable = body["writeable"];
|
|
321
|
+
const recordable = body["recordable"];
|
|
322
|
+
return {
|
|
323
|
+
path,
|
|
324
|
+
httpStatus: status,
|
|
325
|
+
outcome: "ok",
|
|
326
|
+
...type !== void 0 ? { type } : {},
|
|
327
|
+
..."value" in body ? { value: shownValue(body["value"]) } : {},
|
|
328
|
+
...unit !== void 0 ? { unit } : {},
|
|
329
|
+
...typeof writeable === "number" || typeof writeable === "boolean" ? { writeable: Boolean(writeable) } : {},
|
|
330
|
+
...typeof recordable === "number" || typeof recordable === "boolean" ? { recordable: Boolean(recordable) } : {},
|
|
331
|
+
...allowedValues !== void 0 ? { allowedValues } : {},
|
|
332
|
+
...minValue !== void 0 ? { minValue } : {},
|
|
333
|
+
...maxValue !== void 0 ? { maxValue } : {}
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
function emptySummary() {
|
|
337
|
+
return {
|
|
338
|
+
ok: 0,
|
|
339
|
+
forbidden: 0,
|
|
340
|
+
"not-found": 0,
|
|
341
|
+
"rate-limited": 0,
|
|
342
|
+
error: 0
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
function clampDeadline(requested) {
|
|
346
|
+
return Math.min(Math.max(Math.floor(requested ?? 45e3), 1), EXPLORER_MAX_DEADLINE_MS);
|
|
347
|
+
}
|
|
348
|
+
function clampNodes(requested) {
|
|
349
|
+
return Math.min(Math.max(Math.floor(requested ?? 400), 1), EXPLORER_MAX_NODES_CAP);
|
|
350
|
+
}
|
|
351
|
+
var defaultSleep = (ms) => new Promise((resolve) => {
|
|
352
|
+
setTimeout(resolve, ms);
|
|
353
|
+
});
|
|
354
|
+
async function exploreHomeComResources(input) {
|
|
355
|
+
const root = input.root ?? "/";
|
|
356
|
+
if (!isResourcePath(root)) throw new Error(`Not a resource path: ${root}`);
|
|
357
|
+
const maxNodes = clampNodes(input.maxNodes);
|
|
358
|
+
const delayMs = Math.max(input.delayMs ?? 150, 150);
|
|
359
|
+
const sleep = input.sleep ?? defaultSleep;
|
|
360
|
+
const now = input.now ?? Date.now;
|
|
361
|
+
const deadlineAt = now() + clampDeadline(input.deadlineMs);
|
|
362
|
+
const queue = [root];
|
|
363
|
+
const queued = new Set([root]);
|
|
364
|
+
const resources = [];
|
|
365
|
+
const summary = emptySummary();
|
|
366
|
+
let calls = 0;
|
|
367
|
+
let stoppedBy = "complete";
|
|
368
|
+
let droppedReferences = 0;
|
|
369
|
+
const droppedSamples = [];
|
|
370
|
+
const enqueue = (paths) => {
|
|
371
|
+
for (const path of paths) {
|
|
372
|
+
if (queued.has(path)) continue;
|
|
373
|
+
queued.add(path);
|
|
374
|
+
queue.push(path);
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
while (queue.length > 0) {
|
|
378
|
+
if (calls >= maxNodes) {
|
|
379
|
+
stoppedBy = "max-nodes";
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
if (now() + (calls > 0 ? delayMs : 0) >= deadlineAt) {
|
|
383
|
+
stoppedBy = "deadline";
|
|
384
|
+
break;
|
|
385
|
+
}
|
|
386
|
+
const path = queue.shift();
|
|
387
|
+
if (path === void 0) break;
|
|
388
|
+
if (calls > 0) await sleep(delayMs);
|
|
389
|
+
calls += 1;
|
|
390
|
+
let answer;
|
|
391
|
+
try {
|
|
392
|
+
answer = await input.read(path);
|
|
393
|
+
} catch (err) {
|
|
394
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
395
|
+
resources.push({
|
|
396
|
+
path,
|
|
397
|
+
httpStatus: 0,
|
|
398
|
+
outcome: "error",
|
|
399
|
+
error
|
|
400
|
+
});
|
|
401
|
+
summary.error += 1;
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
const outcome = outcomeOf(answer.status);
|
|
405
|
+
summary[outcome] += 1;
|
|
406
|
+
if (outcome === "rate-limited") {
|
|
407
|
+
resources.push({
|
|
408
|
+
path,
|
|
409
|
+
httpStatus: answer.status,
|
|
410
|
+
outcome
|
|
411
|
+
});
|
|
412
|
+
stoppedBy = "rate-limited";
|
|
413
|
+
break;
|
|
414
|
+
}
|
|
415
|
+
if (outcome !== "ok") {
|
|
416
|
+
resources.push({
|
|
417
|
+
path,
|
|
418
|
+
httpStatus: answer.status,
|
|
419
|
+
outcome
|
|
420
|
+
});
|
|
421
|
+
if (path === "/" && root === "/") enqueue(EXPLORER_SEED_ROOTS);
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
const children = referencesOf(answer.body);
|
|
425
|
+
if (children !== null) {
|
|
426
|
+
const type = isRecord$3(answer.body) ? text$1(answer.body["type"]) : void 0;
|
|
427
|
+
resources.push({
|
|
428
|
+
path,
|
|
429
|
+
httpStatus: answer.status,
|
|
430
|
+
outcome,
|
|
431
|
+
type: type ?? "refEnum",
|
|
432
|
+
childCount: children.total,
|
|
433
|
+
...children.dropped.length > 0 ? { droppedReferences: children.dropped.length } : {}
|
|
434
|
+
});
|
|
435
|
+
droppedReferences += children.dropped.length;
|
|
436
|
+
for (const ref of children.dropped) if (droppedSamples.length < DROPPED_SAMPLE_MAX) droppedSamples.push(`${path} -> ${ref}`);
|
|
437
|
+
enqueue(children.paths);
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
resources.push(describeLeaf(path, answer.status, answer.body));
|
|
441
|
+
if (path === "/" && root === "/") enqueue(EXPLORER_SEED_ROOTS);
|
|
442
|
+
}
|
|
443
|
+
return {
|
|
444
|
+
root,
|
|
445
|
+
resources,
|
|
446
|
+
summary,
|
|
447
|
+
stoppedBy,
|
|
448
|
+
unvisited: queue.length,
|
|
449
|
+
unvisitedPaths: queue.slice(0, UNVISITED_SAMPLE_MAX),
|
|
450
|
+
droppedReferences,
|
|
451
|
+
droppedSamples
|
|
452
|
+
};
|
|
453
|
+
}
|
|
180
454
|
//#endregion
|
|
181
455
|
//#region src/homecom/custom-actions.ts
|
|
182
456
|
var gatewayChoice = object({
|
|
@@ -185,6 +459,27 @@ var gatewayChoice = object({
|
|
|
185
459
|
brand: HomeComBrandSchema,
|
|
186
460
|
region: HomeComRegionSchema
|
|
187
461
|
});
|
|
462
|
+
var exploreResourcesInput = object({
|
|
463
|
+
/** The HomeCom integration instance; may be omitted when exactly one is connected. */
|
|
464
|
+
integrationId: string().min(1).optional(),
|
|
465
|
+
root: string().refine(isResourcePath, "an absolute resource path such as /heatSources, no . or .. segment").optional(),
|
|
466
|
+
maxNodes: number().int().min(1).max(EXPLORER_MAX_NODES_CAP).optional(),
|
|
467
|
+
/** Wall-clock budget; the walk returns what it has, with `stoppedBy: 'deadline'`. */
|
|
468
|
+
deadlineMs: number().int().min(1e3).max(EXPLORER_MAX_DEADLINE_MS).optional()
|
|
469
|
+
});
|
|
470
|
+
var exploreResourcesOutput = object({
|
|
471
|
+
integrationId: string(),
|
|
472
|
+
/** Null when the integration has no snapshot yet: unknown, not empty. */
|
|
473
|
+
gatewayId: string().nullable(),
|
|
474
|
+
root: string(),
|
|
475
|
+
stoppedBy: explorerStopReasonSchema,
|
|
476
|
+
unvisited: number(),
|
|
477
|
+
unvisitedPaths: array(string()),
|
|
478
|
+
droppedReferences: number(),
|
|
479
|
+
droppedSamples: array(string()),
|
|
480
|
+
summary: explorerSummarySchema,
|
|
481
|
+
resources: array(exploredResourceSchema)
|
|
482
|
+
});
|
|
188
483
|
var homeComActions = defineCustomActions({
|
|
189
484
|
beginSignIn: customAction(object({}), object({
|
|
190
485
|
attemptId: string().min(1),
|
|
@@ -218,9 +513,95 @@ var homeComActions = defineCustomActions({
|
|
|
218
513
|
}), {
|
|
219
514
|
kind: "mutation",
|
|
220
515
|
auth: "admin"
|
|
516
|
+
}),
|
|
517
|
+
/** Read-only walk of the gateway's resource tree, for diagnosing what it serves. */
|
|
518
|
+
exploreResources: customAction(exploreResourcesInput, exploreResourcesOutput, {
|
|
519
|
+
kind: "query",
|
|
520
|
+
auth: "admin"
|
|
221
521
|
})
|
|
222
522
|
});
|
|
223
523
|
//#endregion
|
|
524
|
+
//#region src/homecom/homecom-explore-action.ts
|
|
525
|
+
/** The action refused before it walked anything; `reason` says why. */
|
|
526
|
+
var HomeComExploreRefusedError = class extends Error {
|
|
527
|
+
reason;
|
|
528
|
+
constructor(reason, message) {
|
|
529
|
+
super(message);
|
|
530
|
+
this.name = "HomeComExploreRefusedError";
|
|
531
|
+
this.reason = reason;
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
function pickIntegration(input, connected) {
|
|
535
|
+
if (input.integrationId !== void 0) return input.integrationId;
|
|
536
|
+
if (connected.length === 1 && connected[0] !== void 0) return connected[0];
|
|
537
|
+
if (connected.length === 0) throw new HomeComExploreRefusedError("not-connected", "No HomeCom integration is connected.");
|
|
538
|
+
throw new HomeComExploreRefusedError("ambiguous", `Several HomeCom integrations are connected; name one with integrationId (${connected.join(", ")}).`);
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* The `exploreResources` action: a read-only walk of one integration's gateway. One walk
|
|
542
|
+
* per integration at a time — a second call while one runs is refused, never queued.
|
|
543
|
+
*/
|
|
544
|
+
function createHomeComExploreAction(deps) {
|
|
545
|
+
const running = /* @__PURE__ */ new Set();
|
|
546
|
+
return async (input) => {
|
|
547
|
+
const integrationId = pickIntegration(input, deps.listConnected());
|
|
548
|
+
if (running.has(integrationId)) throw new HomeComExploreRefusedError("already-running", `A resource walk is already running for HomeCom integration ${integrationId}.`);
|
|
549
|
+
const client = deps.getClient(integrationId);
|
|
550
|
+
if (client === null) throw new HomeComExploreRefusedError("not-connected", `HomeCom integration ${integrationId} is not connected.`);
|
|
551
|
+
running.add(integrationId);
|
|
552
|
+
try {
|
|
553
|
+
return await walk(input, integrationId, client, deps);
|
|
554
|
+
} finally {
|
|
555
|
+
running.delete(integrationId);
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
async function walk(input, integrationId, client, deps) {
|
|
560
|
+
const result = await exploreHomeComResources({
|
|
561
|
+
read: (path) => client.readResource(path),
|
|
562
|
+
...input.root !== void 0 ? { root: input.root } : {},
|
|
563
|
+
...input.maxNodes !== void 0 ? { maxNodes: input.maxNodes } : {},
|
|
564
|
+
...input.deadlineMs !== void 0 ? { deadlineMs: input.deadlineMs } : {},
|
|
565
|
+
...deps.sleep !== void 0 ? { sleep: deps.sleep } : {},
|
|
566
|
+
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
567
|
+
});
|
|
568
|
+
const gatewayId = deps.getGatewayId(integrationId);
|
|
569
|
+
if (result.droppedReferences > 0) deps.logger.warn("HomeCom resource walk dropped references that are not a walkable path", {
|
|
570
|
+
tags: { integrationId },
|
|
571
|
+
meta: {
|
|
572
|
+
gatewayId,
|
|
573
|
+
droppedReferences: result.droppedReferences,
|
|
574
|
+
samples: result.droppedSamples
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
deps.logger.info("HomeCom resource tree explored", {
|
|
578
|
+
tags: { integrationId },
|
|
579
|
+
meta: {
|
|
580
|
+
gatewayId,
|
|
581
|
+
root: result.root,
|
|
582
|
+
visited: result.resources.length,
|
|
583
|
+
stoppedBy: result.stoppedBy,
|
|
584
|
+
unvisited: result.unvisited,
|
|
585
|
+
droppedReferences: result.droppedReferences,
|
|
586
|
+
...result.summary
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
return {
|
|
590
|
+
integrationId,
|
|
591
|
+
gatewayId,
|
|
592
|
+
...result
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
/** The addon's deps: registered integrations that hold a live client; gateway from the snapshot. */
|
|
596
|
+
function homeComExploreDeps(wiring) {
|
|
597
|
+
return {
|
|
598
|
+
listConnected: () => wiring.registry.list().filter((id) => wiring.connections.getClient(id) !== null),
|
|
599
|
+
getClient: (id) => wiring.connections.getClient(id),
|
|
600
|
+
getGatewayId: (id) => wiring.connections.getSnapshot(id)?.gateway.gatewayId ?? null,
|
|
601
|
+
logger: wiring.logger
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
//#endregion
|
|
224
605
|
//#region src/homecom/homecom-device-type.ts
|
|
225
606
|
/**
|
|
226
607
|
* Device types the PointT gateway list reports. The set is the one the MIT
|
|
@@ -1591,6 +1972,13 @@ function createHomeComApiClient(config, gatewayId, deps = {}) {
|
|
|
1591
1972
|
},
|
|
1592
1973
|
async writeHeatPump(writes) {
|
|
1593
1974
|
for (const write of writes) await putWrite(write);
|
|
1975
|
+
},
|
|
1976
|
+
async readResource(path) {
|
|
1977
|
+
const res = await authed(homeComResourceUrl(gatewayId, path), { method: "GET" });
|
|
1978
|
+
return {
|
|
1979
|
+
status: res.status,
|
|
1980
|
+
body: res.body
|
|
1981
|
+
};
|
|
1594
1982
|
}
|
|
1595
1983
|
};
|
|
1596
1984
|
}
|
|
@@ -3094,6 +3482,11 @@ var BoschHomeComProviderAddon = class extends BaseDeviceProvider {
|
|
|
3094
3482
|
});
|
|
3095
3483
|
await this.reconcileIntegrations();
|
|
3096
3484
|
this.subscribeIntegrationLifecycle();
|
|
3485
|
+
const explore = createHomeComExploreAction(homeComExploreDeps({
|
|
3486
|
+
registry: this.clients,
|
|
3487
|
+
connections: homeComConnections,
|
|
3488
|
+
logger: this.ctx.logger
|
|
3489
|
+
}));
|
|
3097
3490
|
return {
|
|
3098
3491
|
providers: [
|
|
3099
3492
|
...regs,
|
|
@@ -3120,7 +3513,8 @@ var BoschHomeComProviderAddon = class extends BaseDeviceProvider {
|
|
|
3120
3513
|
signOut: async () => ({
|
|
3121
3514
|
signedOut: true,
|
|
3122
3515
|
summary: "Signed out."
|
|
3123
|
-
})
|
|
3516
|
+
}),
|
|
3517
|
+
exploreResources: explore
|
|
3124
3518
|
}
|
|
3125
3519
|
};
|
|
3126
3520
|
}
|
package/dist/shc/addon.js
CHANGED
|
@@ -2,7 +2,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
|
2
2
|
//#region \0rolldown/runtime.js
|
|
3
3
|
var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
|
|
4
4
|
//#endregion
|
|
5
|
-
const require_bosch_instances = require("../bosch-instances-
|
|
5
|
+
const require_bosch_instances = require("../bosch-instances-CYBnIcHg.js");
|
|
6
6
|
let node_crypto = require("node:crypto");
|
|
7
7
|
let node_https = require("node:https");
|
|
8
8
|
let node_child_process = require("node:child_process");
|
package/dist/shc/addon.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { $ as EventCategory, A as RecordIntegrationInstances, D as switchCapability, F as entryBody, G as object, M as customAction, N as defineCustomActions, O as temperatureSensorCapability, P as defineEntryKind, Q as errMsg, R as rowsDepsOver, U as literal, V as array, W as number, Z as DeviceType, a as BaseDevice, d as climateControlCapability, i as testBoschInstance, l as batteryCapability, n as boschRowsPorts, o as BaseDeviceProvider, p as deviceAdoptionCapability, q as string, r as keepBodyKeys, t as boschRowsMigration, x as integrationInstancesCapability, y as heatingValveCapability, z as _enum } from "../bosch-instances-CdvcBYye.mjs";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import { request } from "node:https";
|
package/package.json
CHANGED