@camstack/addon-provider-bosch 0.1.48 → 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 +12 -0
- package/dist/{account-integration-offline-CQ7UB6U_.js → account-integration-offline-7CH3r5jI.js} +1 -1
- package/dist/{account-integration-offline-DLCUxCxS.mjs → account-integration-offline-B1Wgr_rT.mjs} +1 -1
- package/dist/{bosch-instances-CRvxRbDk.js → bosch-instances-CYBnIcHg.js} +493 -248
- package/dist/{bosch-instances-t9x1fGVW.mjs → bosch-instances-CdvcBYye.mjs} +482 -249
- package/dist/home-connect/addon.js +2 -2
- package/dist/home-connect/addon.mjs +2 -2
- package/dist/homecom/addon.js +508 -19
- package/dist/homecom/addon.mjs +508 -19
- package/dist/shc/addon.js +1 -1
- package/dist/shc/addon.mjs +1 -1
- package/package.json +2 -2
|
@@ -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
|
|
@@ -735,6 +1116,14 @@ var HEAT_PUMP_PATHS = {
|
|
|
735
1116
|
pressureBar: "/heatSources/hs1/waterPressure",
|
|
736
1117
|
starts: "/heatSources/hs1/numberOfStarts"
|
|
737
1118
|
},
|
|
1119
|
+
/**
|
|
1120
|
+
* Resources only a heat pump's heat source has. A gateway that answers one of them with
|
|
1121
|
+
* anything but 404 (a 403 or a 500 included) has a heat pump it will not let us read.
|
|
1122
|
+
*/
|
|
1123
|
+
identity: {
|
|
1124
|
+
heatPumpType: "/heatSources/hs1/heatPumpType",
|
|
1125
|
+
heatPumpPower: "/heatSources/hs1/heatPumpPower"
|
|
1126
|
+
},
|
|
738
1127
|
circuit: {
|
|
739
1128
|
operationMode: "operationMode",
|
|
740
1129
|
heatCoolMode: "heatCoolMode",
|
|
@@ -763,6 +1152,7 @@ async function readHeatPump(input) {
|
|
|
763
1152
|
const circuits = await Promise.all([...input.circuitIds.hc.map((id) => readHc(get, id)), ...input.circuitIds.dhw.map((id) => readDhwCircuit(get, id))]);
|
|
764
1153
|
const r = HEAT_PUMP_PATHS.readings;
|
|
765
1154
|
const s = HEAT_PUMP_PATHS.system;
|
|
1155
|
+
const heatSourceAnswered = await anyAnswers(input.answers, input.onFailure, [HEAT_PUMP_PATHS.identity.heatPumpType, HEAT_PUMP_PATHS.identity.heatPumpPower]);
|
|
766
1156
|
const [silent, away, additionalHeater, holiday] = await Promise.all([
|
|
767
1157
|
setting(get, s.silent),
|
|
768
1158
|
setting(get, s.away),
|
|
@@ -785,6 +1175,7 @@ async function readHeatPump(input) {
|
|
|
785
1175
|
]);
|
|
786
1176
|
return {
|
|
787
1177
|
circuits,
|
|
1178
|
+
heatSourceAnswered,
|
|
788
1179
|
silent,
|
|
789
1180
|
away,
|
|
790
1181
|
additionalHeater,
|
|
@@ -801,6 +1192,17 @@ async function readHeatPump(input) {
|
|
|
801
1192
|
starts
|
|
802
1193
|
};
|
|
803
1194
|
}
|
|
1195
|
+
async function anyAnswers(answers, onFailure, paths) {
|
|
1196
|
+
if (answers === void 0) return false;
|
|
1197
|
+
return (await Promise.all(paths.map(async (path) => {
|
|
1198
|
+
try {
|
|
1199
|
+
return await answers(path);
|
|
1200
|
+
} catch (error) {
|
|
1201
|
+
onFailure?.(path, error);
|
|
1202
|
+
return false;
|
|
1203
|
+
}
|
|
1204
|
+
}))).some(Boolean);
|
|
1205
|
+
}
|
|
804
1206
|
/** Any failure of a heat-pump read (HTTP 4xx/5xx, a throw) is "not answered", never an error. */
|
|
805
1207
|
function safeGet(get, onFailure) {
|
|
806
1208
|
return async (path) => {
|
|
@@ -828,16 +1230,28 @@ var HEAT_PUMP_ONLY_READINGS = [
|
|
|
828
1230
|
*
|
|
829
1231
|
* A system mode counts when the gateway answered it: silent, away, the
|
|
830
1232
|
* additional heater, or holiday. An empty string is an answer (holiday often
|
|
831
|
-
* comes back empty); a path that did not answer is not.
|
|
832
|
-
*
|
|
833
|
-
*
|
|
834
|
-
*
|
|
835
|
-
*
|
|
836
|
-
*
|
|
1233
|
+
* comes back empty); a path that did not answer is not. A heat-pump-only
|
|
1234
|
+
* reading counts too (thermal power, electrical power, compressor modulation).
|
|
1235
|
+
* So does a circuit whose `heatCoolMode` can be cooling, and a heat source that
|
|
1236
|
+
* answers a heat-pump-only resource with any status but 404: some gateways
|
|
1237
|
+
* refuse (403) or fail (500) the whole system tree and still serve the
|
|
1238
|
+
* circuits. Supply, return, outdoor, pressure, starts and the energy meters do
|
|
1239
|
+
* not count: a gas boiler's heat source answers flow, pressure and starts, and
|
|
1240
|
+
* that is not a heat pump.
|
|
837
1241
|
*/
|
|
838
1242
|
function hasSystemDevice(reading) {
|
|
839
1243
|
if (SYSTEM_MODE_KEYS.some((key) => reading[key] !== null)) return true;
|
|
840
|
-
|
|
1244
|
+
if (HEAT_PUMP_ONLY_READINGS.some((key) => reading[key] !== null)) return true;
|
|
1245
|
+
if (reading.heatSourceAnswered) return true;
|
|
1246
|
+
return reading.circuits.some((c) => c.kind === "hc" && offersCooling(c.heatCoolMode));
|
|
1247
|
+
}
|
|
1248
|
+
/**
|
|
1249
|
+
* A boiler circuit can carry a `heatCoolMode` that only ever says heating, so the mode alone
|
|
1250
|
+
* is no proof. A circuit that can switch to (or already is in) cooling is a heat pump's.
|
|
1251
|
+
*/
|
|
1252
|
+
function offersCooling(setting) {
|
|
1253
|
+
if (setting === null) return false;
|
|
1254
|
+
return setting.value === "cooling" || setting.allowed.includes("cooling");
|
|
841
1255
|
}
|
|
842
1256
|
/** Which heat-pump paths this gateway answered. One log per gateway corrects the path table. */
|
|
843
1257
|
function heatPumpPathCensus(reading) {
|
|
@@ -1376,6 +1790,17 @@ function createHomeComApiClient(config, gatewayId, deps = {}) {
|
|
|
1376
1790
|
if (!res.ok) throw new Error(`HomeCom API ${path} failed: HTTP ${res.status}`);
|
|
1377
1791
|
return res.body;
|
|
1378
1792
|
}
|
|
1793
|
+
/**
|
|
1794
|
+
* Evidence that the resource exists: a 2xx, or the 403 the gateway gives a resource it
|
|
1795
|
+
* serves but will not show. 404 is "absent". Anything else (429, 5xx) is a transient
|
|
1796
|
+
* failure, so it is NOT evidence either way: it is logged and counts as unknown.
|
|
1797
|
+
*/
|
|
1798
|
+
async function resourceAnswers(path) {
|
|
1799
|
+
const res = await authed(homeComResourceUrl(gatewayId, path), { method: "GET" });
|
|
1800
|
+
if (res.ok || res.status === 403) return true;
|
|
1801
|
+
if (res.status !== 404) logHeatPumpFailure(path, /* @__PURE__ */ new Error(`HTTP ${res.status}`));
|
|
1802
|
+
return false;
|
|
1803
|
+
}
|
|
1379
1804
|
async function putWrite(write) {
|
|
1380
1805
|
const res = await authed(homeComResourceUrl(gatewayId, write.path), {
|
|
1381
1806
|
method: "PUT",
|
|
@@ -1457,6 +1882,7 @@ function createHomeComApiClient(config, gatewayId, deps = {}) {
|
|
|
1457
1882
|
const heatPump = await readHeatPump({
|
|
1458
1883
|
get: getResource,
|
|
1459
1884
|
onFailure: logHeatPumpFailure,
|
|
1885
|
+
answers: resourceAnswers,
|
|
1460
1886
|
circuitIds: {
|
|
1461
1887
|
hc: snapshot.circuits.map((circuit) => circuit.id),
|
|
1462
1888
|
dhw: dhwId === void 0 ? [] : [dhwId]
|
|
@@ -1547,6 +1973,13 @@ function createHomeComApiClient(config, gatewayId, deps = {}) {
|
|
|
1547
1973
|
},
|
|
1548
1974
|
async writeHeatPump(writes) {
|
|
1549
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
|
+
};
|
|
1550
1983
|
}
|
|
1551
1984
|
};
|
|
1552
1985
|
}
|
|
@@ -1851,6 +2284,15 @@ var HomeComConnectionResolver = class {
|
|
|
1851
2284
|
this.#surfaces.clear();
|
|
1852
2285
|
}
|
|
1853
2286
|
};
|
|
2287
|
+
/** Native id of the gateway pick: a Gateway device, sibling of the circuits. */
|
|
2288
|
+
var HOMECOM_GATEWAY_NATIVE_ID = "gateway:0";
|
|
2289
|
+
function gatewayDiscoverable(snapshot) {
|
|
2290
|
+
return {
|
|
2291
|
+
nativeId: HOMECOM_GATEWAY_NATIVE_ID,
|
|
2292
|
+
name: snapshot.gateway.name.length > 0 ? snapshot.gateway.name : "HomeCom gateway",
|
|
2293
|
+
kind: "gateway"
|
|
2294
|
+
};
|
|
2295
|
+
}
|
|
1854
2296
|
var BOILER_DHW_NAME = "Domestic hot water";
|
|
1855
2297
|
function circuitDiscoverable(circuit) {
|
|
1856
2298
|
return {
|
|
@@ -1868,18 +2310,25 @@ function dhwDiscoverable(name) {
|
|
|
1868
2310
|
};
|
|
1869
2311
|
}
|
|
1870
2312
|
/**
|
|
1871
|
-
* A gateway with a heat pump is one pick. A boiler still offers each circuit
|
|
2313
|
+
* The gateway itself is always the first pick. A gateway with a heat pump is one more pick. A boiler still offers each circuit
|
|
1872
2314
|
* and the hot-water row on its own.
|
|
1873
2315
|
*/
|
|
1874
2316
|
function buildDiscoverables(snapshot) {
|
|
1875
2317
|
const circuits = snapshot.circuits.map(circuitDiscoverable);
|
|
1876
|
-
if (snapshot.heatPump !== void 0)
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
2318
|
+
if (snapshot.heatPump !== void 0) {
|
|
2319
|
+
const children = [...circuits, ...snapshot.dhw === null ? [] : [dhwDiscoverable(HOMECOM_HOT_WATER_CHILD_NAME)]];
|
|
2320
|
+
return [gatewayDiscoverable(snapshot), {
|
|
2321
|
+
nativeId: "hp:0",
|
|
2322
|
+
name: "Heat pump",
|
|
2323
|
+
kind: "heat-pump",
|
|
2324
|
+
children
|
|
2325
|
+
}];
|
|
2326
|
+
}
|
|
2327
|
+
return [
|
|
2328
|
+
gatewayDiscoverable(snapshot),
|
|
2329
|
+
...circuits,
|
|
2330
|
+
...snapshot.dhw === null ? [] : [dhwDiscoverable(BOILER_DHW_NAME)]
|
|
2331
|
+
];
|
|
1883
2332
|
}
|
|
1884
2333
|
var homeComConnections = new HomeComConnectionResolver();
|
|
1885
2334
|
//#endregion
|
|
@@ -1962,6 +2411,10 @@ function shapeOf(kind) {
|
|
|
1962
2411
|
type: require_bosch_instances.DeviceType.WaterHeater,
|
|
1963
2412
|
caps: ["water-heater"]
|
|
1964
2413
|
};
|
|
2414
|
+
case "gateway": return {
|
|
2415
|
+
type: require_bosch_instances.DeviceType.Gateway,
|
|
2416
|
+
caps: []
|
|
2417
|
+
};
|
|
1965
2418
|
case "heat-pump": return {
|
|
1966
2419
|
type: require_bosch_instances.DeviceType.HeatPump,
|
|
1967
2420
|
caps: ["heat-pump-status", "heat-pump-control"]
|
|
@@ -2809,6 +3262,35 @@ var HomeComWaterHeaterDevice = class extends require_bosch_instances.BaseDevice
|
|
|
2809
3262
|
}
|
|
2810
3263
|
};
|
|
2811
3264
|
//#endregion
|
|
3265
|
+
//#region src/homecom/devices/homecom-gateway-device.ts
|
|
3266
|
+
var homeComGatewaySchema = require_bosch_instances.object({
|
|
3267
|
+
nativeId: require_bosch_instances.string().min(1),
|
|
3268
|
+
brokerId: require_bosch_instances.string().min(1),
|
|
3269
|
+
system: require_bosch_instances.literal("homecom").optional(),
|
|
3270
|
+
integrationId: require_bosch_instances.string().optional()
|
|
3271
|
+
});
|
|
3272
|
+
/**
|
|
3273
|
+
* The HomeCom gateway, as a {@link DeviceType.Gateway}. Its connection lives on the
|
|
3274
|
+
* INTEGRATION; this device is the gateway's presence in the device list and follows the
|
|
3275
|
+
* integration's online state (its connect and disconnect callbacks flip it).
|
|
3276
|
+
*
|
|
3277
|
+
* It registers no action: the cloud account offers no reboot, no update and no local page,
|
|
3278
|
+
* so there is nothing real to expose. The circuits, the hot water and the heat pump are NOT
|
|
3279
|
+
* children of it; each is adopted as its own top-level device of the same integration.
|
|
3280
|
+
*/
|
|
3281
|
+
var HomeComGatewayDevice = class extends require_bosch_instances.BaseDevice {
|
|
3282
|
+
features = [];
|
|
3283
|
+
constructor(ctx) {
|
|
3284
|
+
const cfg = homeComGatewaySchema.parse(ctx.persistedConfig ?? {});
|
|
3285
|
+
super(ctx, homeComGatewaySchema, { type: require_bosch_instances.DeviceType.Gateway });
|
|
3286
|
+
this.online = homeComConnections.has(cfg.brokerId);
|
|
3287
|
+
this.updateSourceInfo({
|
|
3288
|
+
id: cfg.nativeId,
|
|
3289
|
+
system: "homecom"
|
|
3290
|
+
});
|
|
3291
|
+
}
|
|
3292
|
+
};
|
|
3293
|
+
//#endregion
|
|
2812
3294
|
//#region src/homecom/devices/homecom-heat-pump-device.ts
|
|
2813
3295
|
var homeComHeatPumpSchema = require_bosch_instances.object({
|
|
2814
3296
|
nativeId: require_bosch_instances.string().min(1),
|
|
@@ -2959,7 +3441,8 @@ var BoschHomeComProviderAddon = class extends require_bosch_instances.BaseDevice
|
|
|
2959
3441
|
deviceClasses = {
|
|
2960
3442
|
[require_bosch_instances.DeviceType.Climate]: HomeComClimateDevice,
|
|
2961
3443
|
[require_bosch_instances.DeviceType.WaterHeater]: HomeComWaterHeaterDevice,
|
|
2962
|
-
[require_bosch_instances.DeviceType.HeatPump]: HomeComHeatPumpDevice
|
|
3444
|
+
[require_bosch_instances.DeviceType.HeatPump]: HomeComHeatPumpDevice,
|
|
3445
|
+
[require_bosch_instances.DeviceType.Gateway]: HomeComGatewayDevice
|
|
2963
3446
|
};
|
|
2964
3447
|
clients = null;
|
|
2965
3448
|
instances = null;
|
|
@@ -3000,6 +3483,11 @@ var BoschHomeComProviderAddon = class extends require_bosch_instances.BaseDevice
|
|
|
3000
3483
|
});
|
|
3001
3484
|
await this.reconcileIntegrations();
|
|
3002
3485
|
this.subscribeIntegrationLifecycle();
|
|
3486
|
+
const explore = createHomeComExploreAction(homeComExploreDeps({
|
|
3487
|
+
registry: this.clients,
|
|
3488
|
+
connections: homeComConnections,
|
|
3489
|
+
logger: this.ctx.logger
|
|
3490
|
+
}));
|
|
3003
3491
|
return {
|
|
3004
3492
|
providers: [
|
|
3005
3493
|
...regs,
|
|
@@ -3026,7 +3514,8 @@ var BoschHomeComProviderAddon = class extends require_bosch_instances.BaseDevice
|
|
|
3026
3514
|
signOut: async () => ({
|
|
3027
3515
|
signedOut: true,
|
|
3028
3516
|
summary: "Signed out."
|
|
3029
|
-
})
|
|
3517
|
+
}),
|
|
3518
|
+
exploreResources: explore
|
|
3030
3519
|
}
|
|
3031
3520
|
};
|
|
3032
3521
|
}
|