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