@camstack/addon-provider-bosch 0.1.45 → 0.1.46

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 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.46] - 2026-11-09
11
+
12
+ ### Changed
13
+
14
+ - A HomeCom heat pump whose gateway reports only its system modes is now offered for adoption.
15
+
10
16
  ## [0.1.45] - 2026-11-09
11
17
 
12
18
  ### Changed
@@ -1,4 +1,4 @@
1
- import { X as errMsg } from "./bosch-instances-DXID-jMD.mjs";
1
+ import { X as errMsg } from "./bosch-instances-DkUo6BfT.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. */
@@ -1,4 +1,4 @@
1
- const require_bosch_instances = require("./bosch-instances-DdklwoKS.js");
1
+ const require_bosch_instances = require("./bosch-instances-BD-Valeo.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. */
@@ -44553,21 +44553,44 @@ var AutomationCapStepBaseSchema = object({
44553
44553
  */
44554
44554
  argUnits: record(string(), string().min(1)).optional()
44555
44555
  });
44556
- var AutomationCapStepSchema = AutomationCapStepBaseSchema.refine((step) => step.target !== void 0 || step.deviceId !== void 0, {
44556
+ /**
44557
+ * The steps an internal Action may contain: cap and wait, and nothing that can
44558
+ * call back into an Action — so an Action can never reach itself (no cycles).
44559
+ * This is the LEAF shape: no condition of its own.
44560
+ */
44561
+ var LeafActionStepSchema = discriminatedUnion("kind", [AutomationWaitStepSchema, AutomationCapStepBaseSchema.refine((step) => step.target !== void 0 || step.deviceId !== void 0, {
44557
44562
  message: "a cap step needs a target",
44558
44563
  path: ["target"]
44559
44564
  }).refine((step) => step.target?.kind !== "domain" || step.actionId !== void 0, {
44560
44565
  message: "a domain target needs the vocabulary action it applies to (actionId)",
44561
44566
  path: ["actionId"]
44562
- });
44567
+ })]);
44563
44568
  /**
44564
- * The steps an internal Action may contain: cap and wait, and nothing that can
44565
- * call back into an Action — so an Action can never reach itself (no cycles).
44569
+ * The optional guard of a step (#472): `if` is the ONE `when` tree (D980), read
44570
+ * on the state mirror when the step is reached; `else` runs when it is false.
44571
+ * An `else` holds leaf steps only — the nesting is bounded at ONE level, so a
44572
+ * branch can never grow a tree the trace has to draw.
44566
44573
  */
44567
- var ActionStepSchema = discriminatedUnion("kind", [AutomationWaitStepSchema, AutomationCapStepSchema]);
44574
+ var stepGuardShape = {
44575
+ if: AutomationWhenSchema.optional(),
44576
+ else: array(LeafActionStepSchema).max(10).optional()
44577
+ };
44578
+ var elseNeedsIf = {
44579
+ message: "an else list needs the condition it is the other side of (if)",
44580
+ path: ["else"]
44581
+ };
44582
+ var GuardedWaitStepSchema = AutomationWaitStepSchema.extend(stepGuardShape).refine((step) => step.else === void 0 || step.if !== void 0, elseNeedsIf);
44583
+ var GuardedCapStepSchema = AutomationCapStepBaseSchema.extend(stepGuardShape).refine((step) => step.target !== void 0 || step.deviceId !== void 0, {
44584
+ message: "a cap step needs a target",
44585
+ path: ["target"]
44586
+ }).refine((step) => step.target?.kind !== "domain" || step.actionId !== void 0, {
44587
+ message: "a domain target needs the vocabulary action it applies to (actionId)",
44588
+ path: ["actionId"]
44589
+ }).refine((step) => step.else === void 0 || step.if !== void 0, elseNeedsIf);
44590
+ var ActionStepSchema = discriminatedUnion("kind", [GuardedWaitStepSchema, GuardedCapStepSchema]);
44568
44591
  var AutomationActionSchema = discriminatedUnion("kind", [
44569
- AutomationWaitStepSchema,
44570
- AutomationCapStepSchema,
44592
+ GuardedWaitStepSchema,
44593
+ GuardedCapStepSchema,
44571
44594
  object({
44572
44595
  kind: literal("action"),
44573
44596
  /** An entry of the `actions` surface (`internal:<uuid>` or `<addonId>:<localId>`). */
@@ -44947,7 +44970,7 @@ var StepDeviceTraceSchema = object({
44947
44970
  outcome: _enum(["ok", "error"]),
44948
44971
  message: string().optional()
44949
44972
  });
44950
- var AutomationStepTraceSchema = object({
44973
+ var StepTraceBaseSchema = object({
44951
44974
  /** `actions.<i>` */
44952
44975
  path: string().min(1),
44953
44976
  kind: string().min(1),
@@ -44966,15 +44989,35 @@ var AutomationStepTraceSchema = object({
44966
44989
  * is shown as {@link REDACTED_ARG}. Absent on traces written before it existed.
44967
44990
  */
44968
44991
  args: record(string(), TraceValueSchema).optional(),
44969
- /** `empty`: the target resolved to no device, so nothing was done — never silently `ok`. */
44992
+ /**
44993
+ * The device values a templated arg printed as `—`, one `ref: reason` each
44994
+ * (D1021): why a sentence says "—" where a number was expected. Bounded.
44995
+ */
44996
+ templates: array(string()).max(20).optional(),
44997
+ /**
44998
+ * What the step's own `if` read when the step was reached (#472): `true` the
44999
+ * step ran, `false` its `else` ran (or nothing did), `unknown` a state leaf
45000
+ * could not be read, so NEITHER side ran. Absent on an unguarded step.
45001
+ */
45002
+ condition: _enum([
45003
+ "true",
45004
+ "false",
45005
+ "unknown"
45006
+ ]).optional(),
45007
+ /** Why the condition read what it read, in the evaluator's own words. */
45008
+ conditionReason: string().optional(),
45009
+ /** `empty`: the target resolved to no device, so nothing was done — never silently `ok`. `skipped`: its `if` was false (or unknown) and nothing ran in its place. */
44970
45010
  outcome: _enum([
44971
45011
  "ok",
44972
45012
  "error",
44973
45013
  "not-run",
44974
- "empty"
45014
+ "empty",
45015
+ "skipped"
44975
45016
  ]),
44976
45017
  message: string().optional()
44977
45018
  });
45019
+ /** A step trace; the `else` steps that ran are one level of the same shape (the nesting is bounded at one). */
45020
+ var AutomationStepTraceSchema = StepTraceBaseSchema.extend({ else: array(StepTraceBaseSchema).optional() });
44978
45021
  var AutomationRunTraceSchema = object({
44979
45022
  runId: string().min(1),
44980
45023
  startedAt: number$1(),
@@ -45150,6 +45193,7 @@ _enum([
45150
45193
  "invalid-variables",
45151
45194
  "invalid-steps",
45152
45195
  "not-linked",
45196
+ "template-scope",
45153
45197
  "busy"
45154
45198
  ]);
45155
45199
  object({
@@ -52732,6 +52776,43 @@ method(InstancePinSchema, DeviceExportStatusSchema), method(_void(), array(Expor
52732
52776
  auth: "admin"
52733
52777
  });
52734
52778
  var DeviceAliasSchema = string().max(64).regex(/^[a-z][a-z0-9_]*$/);
52779
+ var DeviceTemplateSchema = string().min(1).max(4096);
52780
+ var DeviceTemplateRefusalSchema = object({
52781
+ reason: _enum([
52782
+ "too-long",
52783
+ "syntax",
52784
+ "forbidden-syntax",
52785
+ "forbidden-member",
52786
+ "unknown-filter",
52787
+ "ambiguous-field",
52788
+ "busy",
52789
+ "budget-exceeded",
52790
+ "timeout",
52791
+ "output-too-large",
52792
+ "render-failed",
52793
+ "out-of-scope",
52794
+ "unavailable"
52795
+ ]),
52796
+ message: string()
52797
+ });
52798
+ var DeviceTemplateTraceEntrySchema = object({
52799
+ ref: string(),
52800
+ reason: _enum([
52801
+ "unknown-alias",
52802
+ "unknown-field",
52803
+ "no-field",
52804
+ "unavailable"
52805
+ ])
52806
+ });
52807
+ var DeviceTemplateRenderSchema = discriminatedUnion("ok", [object({
52808
+ ok: literal(true),
52809
+ text: string(),
52810
+ trace: array(DeviceTemplateTraceEntrySchema)
52811
+ }), object({
52812
+ ok: literal(false),
52813
+ refusal: DeviceTemplateRefusalSchema,
52814
+ trace: array(DeviceTemplateTraceEntrySchema)
52815
+ })]);
52735
52816
  /**
52736
52817
  * A SITE ZONE — a named part of the property that several locations belong to.
52737
52818
  *
@@ -53563,6 +53644,22 @@ method(object({
53563
53644
  kind: "mutation",
53564
53645
  auth: "admin"
53565
53646
  }), method(object({
53647
+ templates: array(DeviceTemplateSchema).max(64),
53648
+ allowedDeviceIds: array(number$1().int()).optional()
53649
+ }), object({ results: array(DeviceTemplateRenderSchema) }), { auth: "admin" }), method(object({
53650
+ templates: array(string()).max(64),
53651
+ allowedDeviceIds: array(number$1().int()).optional()
53652
+ }), object({ results: array(discriminatedUnion("ok", [object({
53653
+ ok: literal(true),
53654
+ aliases: array(object({
53655
+ alias: string(),
53656
+ deviceId: number$1().int().nullable(),
53657
+ allowed: boolean()
53658
+ }))
53659
+ }), object({
53660
+ ok: literal(false),
53661
+ refusal: DeviceTemplateRefusalSchema
53662
+ })])) }), { auth: "admin" }), method(object({
53566
53663
  deviceId: number$1(),
53567
53664
  location: string().nullable()
53568
53665
  }), _void(), {
@@ -57531,7 +57628,13 @@ var HeroTimedRunSlotSchema = object({
57531
57628
  activeUntil: number$1().nullable(),
57532
57629
  quickRun: TimedRunQuickRunSchema,
57533
57630
  run: HeroActionSchema,
57534
- cancel: HeroActionSchema
57631
+ cancel: HeroActionSchema,
57632
+ /**
57633
+ * Whole units left (the limit's unit, ceil). `null` while nothing is armed.
57634
+ * A number, not words: the row formats it, and a text here would not fit the
57635
+ * hero-layout declaration.
57636
+ */
57637
+ remaining: number$1().nullable()
57535
57638
  });
57536
57639
  var HeroPowerSupplySlotSchema = object({
57537
57640
  /** The plug's state; `null` = unknown, never off. */
@@ -57650,18 +57753,31 @@ var HeroModelSchema = object({
57650
57753
  * 4. a trailing fact (power, else battery, else consumables).
57651
57754
  *
57652
57755
  * Selection rules, all taken from the model:
57653
- * - ONE control is ever shown: the device's own power; else the FIRST control of
57756
+ * - the primary control is the device's own power; else the FIRST control of
57654
57757
  * the FIRST row that has any (a later row's control is never promoted). When
57655
57758
  * the model lists that part in `gated`, the row has no inline control: the
57656
57759
  * projection never falls through to another control the caller could use.
57760
+ * - a timed run, when the model carries the slot, sits beside that primary:
57761
+ * Run while idle, Stop while armed. Gating the slot hides the control.
57657
57762
  * - the status line is the head (what plays, else the state) plus at most
57658
- * {@link ROW_STATUS_FACTS_MAX} facts, the trailing fact excluded;
57763
+ * {@link ROW_STATUS_FACTS_MAX} facts, the trailing fact excluded, then the
57764
+ * time left on an armed timed run;
57659
57765
  * - the trailing fact is the first of power, battery, consumables.
57660
57766
  *
57661
57767
  * `projectDeviceRow` is pure and computes nothing: it selects from a
57662
57768
  * {@link HeroModel}. A row that needed a value the hero lacks would mean the
57663
57769
  * model is missing it, so the fix goes in the model.
57664
57770
  */
57771
+ /**
57772
+ * The row's timed-run control. `id` is `timed-run.run` while idle and
57773
+ * `timed-run.cancel` while armed; `duration` and `unit` are the one-tap run
57774
+ * a Run press starts.
57775
+ */
57776
+ var HeroRowTimedRunSchema = object({
57777
+ id: _enum(["timed-run.run", "timed-run.cancel"]),
57778
+ duration: number$1(),
57779
+ unit: _enum(["min", "s"])
57780
+ });
57665
57781
  var HeroRowModelSchema = object({
57666
57782
  deviceId: number$1().int(),
57667
57783
  name: string(),
@@ -57674,6 +57790,11 @@ var HeroRowModelSchema = object({
57674
57790
  statusLine: array(HeroTextSchema),
57675
57791
  /** `null` = the type has no inline control, or the caller may not act on it. */
57676
57792
  primary: HeroControlSchema.nullable(),
57793
+ /**
57794
+ * Beside `primary` when the device offers a timed run: Run while idle, Stop
57795
+ * while armed. `null` when there is no slot, or the caller may not act.
57796
+ */
57797
+ timedRun: HeroRowTimedRunSchema.nullable(),
57677
57798
  trailing: HeroFactSchema.nullable()
57678
57799
  });
57679
57800
  /**
@@ -89025,6 +89146,13 @@ Object.freeze({
89025
89146
  access: "create",
89026
89147
  role: "admin"
89027
89148
  },
89149
+ "deviceManager.checkDeviceTemplates": {
89150
+ capName: "device-manager",
89151
+ capScope: "system",
89152
+ addonId: null,
89153
+ access: "view",
89154
+ role: "admin"
89155
+ },
89028
89156
  "deviceManager.cleanupDeletedDevicesData": {
89029
89157
  capName: "device-manager",
89030
89158
  capScope: "system",
@@ -89424,6 +89552,13 @@ Object.freeze({
89424
89552
  access: "create",
89425
89553
  role: "admin"
89426
89554
  },
89555
+ "deviceManager.renderDeviceTemplates": {
89556
+ capName: "device-manager",
89557
+ capScope: "system",
89558
+ addonId: null,
89559
+ access: "view",
89560
+ role: "admin"
89561
+ },
89427
89562
  "deviceManager.reportDiscoveryObservations": {
89428
89563
  capName: "device-manager",
89429
89564
  capScope: "system",
@@ -97340,7 +97475,6 @@ function offered(received) {
97340
97475
  };
97341
97476
  }
97342
97477
  offered(HA_AS), withheld(HOMEKIT_REASON), withheld(ALEXA_REASON), withheld(GOOGLE_REASON), withheld(MATTER_REASON);
97343
- string().min(1).max(4096);
97344
97478
  Object.freeze({
97345
97479
  "accessories.getStatus": [{
97346
97480
  name: "deviceId",
@@ -44553,21 +44553,44 @@ var AutomationCapStepBaseSchema = object({
44553
44553
  */
44554
44554
  argUnits: record(string(), string().min(1)).optional()
44555
44555
  });
44556
- var AutomationCapStepSchema = AutomationCapStepBaseSchema.refine((step) => step.target !== void 0 || step.deviceId !== void 0, {
44556
+ /**
44557
+ * The steps an internal Action may contain: cap and wait, and nothing that can
44558
+ * call back into an Action — so an Action can never reach itself (no cycles).
44559
+ * This is the LEAF shape: no condition of its own.
44560
+ */
44561
+ var LeafActionStepSchema = discriminatedUnion("kind", [AutomationWaitStepSchema, AutomationCapStepBaseSchema.refine((step) => step.target !== void 0 || step.deviceId !== void 0, {
44557
44562
  message: "a cap step needs a target",
44558
44563
  path: ["target"]
44559
44564
  }).refine((step) => step.target?.kind !== "domain" || step.actionId !== void 0, {
44560
44565
  message: "a domain target needs the vocabulary action it applies to (actionId)",
44561
44566
  path: ["actionId"]
44562
- });
44567
+ })]);
44563
44568
  /**
44564
- * The steps an internal Action may contain: cap and wait, and nothing that can
44565
- * call back into an Action — so an Action can never reach itself (no cycles).
44569
+ * The optional guard of a step (#472): `if` is the ONE `when` tree (D980), read
44570
+ * on the state mirror when the step is reached; `else` runs when it is false.
44571
+ * An `else` holds leaf steps only — the nesting is bounded at ONE level, so a
44572
+ * branch can never grow a tree the trace has to draw.
44566
44573
  */
44567
- var ActionStepSchema = discriminatedUnion("kind", [AutomationWaitStepSchema, AutomationCapStepSchema]);
44574
+ var stepGuardShape = {
44575
+ if: AutomationWhenSchema.optional(),
44576
+ else: array(LeafActionStepSchema).max(10).optional()
44577
+ };
44578
+ var elseNeedsIf = {
44579
+ message: "an else list needs the condition it is the other side of (if)",
44580
+ path: ["else"]
44581
+ };
44582
+ var GuardedWaitStepSchema = AutomationWaitStepSchema.extend(stepGuardShape).refine((step) => step.else === void 0 || step.if !== void 0, elseNeedsIf);
44583
+ var GuardedCapStepSchema = AutomationCapStepBaseSchema.extend(stepGuardShape).refine((step) => step.target !== void 0 || step.deviceId !== void 0, {
44584
+ message: "a cap step needs a target",
44585
+ path: ["target"]
44586
+ }).refine((step) => step.target?.kind !== "domain" || step.actionId !== void 0, {
44587
+ message: "a domain target needs the vocabulary action it applies to (actionId)",
44588
+ path: ["actionId"]
44589
+ }).refine((step) => step.else === void 0 || step.if !== void 0, elseNeedsIf);
44590
+ var ActionStepSchema = discriminatedUnion("kind", [GuardedWaitStepSchema, GuardedCapStepSchema]);
44568
44591
  var AutomationActionSchema = discriminatedUnion("kind", [
44569
- AutomationWaitStepSchema,
44570
- AutomationCapStepSchema,
44592
+ GuardedWaitStepSchema,
44593
+ GuardedCapStepSchema,
44571
44594
  object({
44572
44595
  kind: literal("action"),
44573
44596
  /** An entry of the `actions` surface (`internal:<uuid>` or `<addonId>:<localId>`). */
@@ -44947,7 +44970,7 @@ var StepDeviceTraceSchema = object({
44947
44970
  outcome: _enum(["ok", "error"]),
44948
44971
  message: string().optional()
44949
44972
  });
44950
- var AutomationStepTraceSchema = object({
44973
+ var StepTraceBaseSchema = object({
44951
44974
  /** `actions.<i>` */
44952
44975
  path: string().min(1),
44953
44976
  kind: string().min(1),
@@ -44966,15 +44989,35 @@ var AutomationStepTraceSchema = object({
44966
44989
  * is shown as {@link REDACTED_ARG}. Absent on traces written before it existed.
44967
44990
  */
44968
44991
  args: record(string(), TraceValueSchema).optional(),
44969
- /** `empty`: the target resolved to no device, so nothing was done — never silently `ok`. */
44992
+ /**
44993
+ * The device values a templated arg printed as `—`, one `ref: reason` each
44994
+ * (D1021): why a sentence says "—" where a number was expected. Bounded.
44995
+ */
44996
+ templates: array(string()).max(20).optional(),
44997
+ /**
44998
+ * What the step's own `if` read when the step was reached (#472): `true` the
44999
+ * step ran, `false` its `else` ran (or nothing did), `unknown` a state leaf
45000
+ * could not be read, so NEITHER side ran. Absent on an unguarded step.
45001
+ */
45002
+ condition: _enum([
45003
+ "true",
45004
+ "false",
45005
+ "unknown"
45006
+ ]).optional(),
45007
+ /** Why the condition read what it read, in the evaluator's own words. */
45008
+ conditionReason: string().optional(),
45009
+ /** `empty`: the target resolved to no device, so nothing was done — never silently `ok`. `skipped`: its `if` was false (or unknown) and nothing ran in its place. */
44970
45010
  outcome: _enum([
44971
45011
  "ok",
44972
45012
  "error",
44973
45013
  "not-run",
44974
- "empty"
45014
+ "empty",
45015
+ "skipped"
44975
45016
  ]),
44976
45017
  message: string().optional()
44977
45018
  });
45019
+ /** A step trace; the `else` steps that ran are one level of the same shape (the nesting is bounded at one). */
45020
+ var AutomationStepTraceSchema = StepTraceBaseSchema.extend({ else: array(StepTraceBaseSchema).optional() });
44978
45021
  var AutomationRunTraceSchema = object({
44979
45022
  runId: string().min(1),
44980
45023
  startedAt: number$1(),
@@ -45150,6 +45193,7 @@ _enum([
45150
45193
  "invalid-variables",
45151
45194
  "invalid-steps",
45152
45195
  "not-linked",
45196
+ "template-scope",
45153
45197
  "busy"
45154
45198
  ]);
45155
45199
  object({
@@ -52732,6 +52776,43 @@ method(InstancePinSchema, DeviceExportStatusSchema), method(_void(), array(Expor
52732
52776
  auth: "admin"
52733
52777
  });
52734
52778
  var DeviceAliasSchema = string().max(64).regex(/^[a-z][a-z0-9_]*$/);
52779
+ var DeviceTemplateSchema = string().min(1).max(4096);
52780
+ var DeviceTemplateRefusalSchema = object({
52781
+ reason: _enum([
52782
+ "too-long",
52783
+ "syntax",
52784
+ "forbidden-syntax",
52785
+ "forbidden-member",
52786
+ "unknown-filter",
52787
+ "ambiguous-field",
52788
+ "busy",
52789
+ "budget-exceeded",
52790
+ "timeout",
52791
+ "output-too-large",
52792
+ "render-failed",
52793
+ "out-of-scope",
52794
+ "unavailable"
52795
+ ]),
52796
+ message: string()
52797
+ });
52798
+ var DeviceTemplateTraceEntrySchema = object({
52799
+ ref: string(),
52800
+ reason: _enum([
52801
+ "unknown-alias",
52802
+ "unknown-field",
52803
+ "no-field",
52804
+ "unavailable"
52805
+ ])
52806
+ });
52807
+ var DeviceTemplateRenderSchema = discriminatedUnion("ok", [object({
52808
+ ok: literal(true),
52809
+ text: string(),
52810
+ trace: array(DeviceTemplateTraceEntrySchema)
52811
+ }), object({
52812
+ ok: literal(false),
52813
+ refusal: DeviceTemplateRefusalSchema,
52814
+ trace: array(DeviceTemplateTraceEntrySchema)
52815
+ })]);
52735
52816
  /**
52736
52817
  * A SITE ZONE — a named part of the property that several locations belong to.
52737
52818
  *
@@ -53563,6 +53644,22 @@ method(object({
53563
53644
  kind: "mutation",
53564
53645
  auth: "admin"
53565
53646
  }), method(object({
53647
+ templates: array(DeviceTemplateSchema).max(64),
53648
+ allowedDeviceIds: array(number$1().int()).optional()
53649
+ }), object({ results: array(DeviceTemplateRenderSchema) }), { auth: "admin" }), method(object({
53650
+ templates: array(string()).max(64),
53651
+ allowedDeviceIds: array(number$1().int()).optional()
53652
+ }), object({ results: array(discriminatedUnion("ok", [object({
53653
+ ok: literal(true),
53654
+ aliases: array(object({
53655
+ alias: string(),
53656
+ deviceId: number$1().int().nullable(),
53657
+ allowed: boolean()
53658
+ }))
53659
+ }), object({
53660
+ ok: literal(false),
53661
+ refusal: DeviceTemplateRefusalSchema
53662
+ })])) }), { auth: "admin" }), method(object({
53566
53663
  deviceId: number$1(),
53567
53664
  location: string().nullable()
53568
53665
  }), _void(), {
@@ -57531,7 +57628,13 @@ var HeroTimedRunSlotSchema = object({
57531
57628
  activeUntil: number$1().nullable(),
57532
57629
  quickRun: TimedRunQuickRunSchema,
57533
57630
  run: HeroActionSchema,
57534
- cancel: HeroActionSchema
57631
+ cancel: HeroActionSchema,
57632
+ /**
57633
+ * Whole units left (the limit's unit, ceil). `null` while nothing is armed.
57634
+ * A number, not words: the row formats it, and a text here would not fit the
57635
+ * hero-layout declaration.
57636
+ */
57637
+ remaining: number$1().nullable()
57535
57638
  });
57536
57639
  var HeroPowerSupplySlotSchema = object({
57537
57640
  /** The plug's state; `null` = unknown, never off. */
@@ -57650,18 +57753,31 @@ var HeroModelSchema = object({
57650
57753
  * 4. a trailing fact (power, else battery, else consumables).
57651
57754
  *
57652
57755
  * Selection rules, all taken from the model:
57653
- * - ONE control is ever shown: the device's own power; else the FIRST control of
57756
+ * - the primary control is the device's own power; else the FIRST control of
57654
57757
  * the FIRST row that has any (a later row's control is never promoted). When
57655
57758
  * the model lists that part in `gated`, the row has no inline control: the
57656
57759
  * projection never falls through to another control the caller could use.
57760
+ * - a timed run, when the model carries the slot, sits beside that primary:
57761
+ * Run while idle, Stop while armed. Gating the slot hides the control.
57657
57762
  * - the status line is the head (what plays, else the state) plus at most
57658
- * {@link ROW_STATUS_FACTS_MAX} facts, the trailing fact excluded;
57763
+ * {@link ROW_STATUS_FACTS_MAX} facts, the trailing fact excluded, then the
57764
+ * time left on an armed timed run;
57659
57765
  * - the trailing fact is the first of power, battery, consumables.
57660
57766
  *
57661
57767
  * `projectDeviceRow` is pure and computes nothing: it selects from a
57662
57768
  * {@link HeroModel}. A row that needed a value the hero lacks would mean the
57663
57769
  * model is missing it, so the fix goes in the model.
57664
57770
  */
57771
+ /**
57772
+ * The row's timed-run control. `id` is `timed-run.run` while idle and
57773
+ * `timed-run.cancel` while armed; `duration` and `unit` are the one-tap run
57774
+ * a Run press starts.
57775
+ */
57776
+ var HeroRowTimedRunSchema = object({
57777
+ id: _enum(["timed-run.run", "timed-run.cancel"]),
57778
+ duration: number$1(),
57779
+ unit: _enum(["min", "s"])
57780
+ });
57665
57781
  var HeroRowModelSchema = object({
57666
57782
  deviceId: number$1().int(),
57667
57783
  name: string(),
@@ -57674,6 +57790,11 @@ var HeroRowModelSchema = object({
57674
57790
  statusLine: array(HeroTextSchema),
57675
57791
  /** `null` = the type has no inline control, or the caller may not act on it. */
57676
57792
  primary: HeroControlSchema.nullable(),
57793
+ /**
57794
+ * Beside `primary` when the device offers a timed run: Run while idle, Stop
57795
+ * while armed. `null` when there is no slot, or the caller may not act.
57796
+ */
57797
+ timedRun: HeroRowTimedRunSchema.nullable(),
57677
57798
  trailing: HeroFactSchema.nullable()
57678
57799
  });
57679
57800
  /**
@@ -89025,6 +89146,13 @@ Object.freeze({
89025
89146
  access: "create",
89026
89147
  role: "admin"
89027
89148
  },
89149
+ "deviceManager.checkDeviceTemplates": {
89150
+ capName: "device-manager",
89151
+ capScope: "system",
89152
+ addonId: null,
89153
+ access: "view",
89154
+ role: "admin"
89155
+ },
89028
89156
  "deviceManager.cleanupDeletedDevicesData": {
89029
89157
  capName: "device-manager",
89030
89158
  capScope: "system",
@@ -89424,6 +89552,13 @@ Object.freeze({
89424
89552
  access: "create",
89425
89553
  role: "admin"
89426
89554
  },
89555
+ "deviceManager.renderDeviceTemplates": {
89556
+ capName: "device-manager",
89557
+ capScope: "system",
89558
+ addonId: null,
89559
+ access: "view",
89560
+ role: "admin"
89561
+ },
89427
89562
  "deviceManager.reportDiscoveryObservations": {
89428
89563
  capName: "device-manager",
89429
89564
  capScope: "system",
@@ -97340,7 +97475,6 @@ function offered(received) {
97340
97475
  };
97341
97476
  }
97342
97477
  offered(HA_AS), withheld(HOMEKIT_REASON), withheld(ALEXA_REASON), withheld(GOOGLE_REASON), withheld(MATTER_REASON);
97343
- string().min(1).max(4096);
97344
97478
  Object.freeze({
97345
97479
  "accessories.getStatus": [{
97346
97480
  name: "deviceId",
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_bosch_instances = require("../bosch-instances-DdklwoKS.js");
3
- const require_account_integration_offline = require("../account-integration-offline-BiXbzZsg.js");
2
+ const require_bosch_instances = require("../bosch-instances-BD-Valeo.js");
3
+ const require_account_integration_offline = require("../account-integration-offline-BfsMnlJo.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 { A as RecordIntegrationInstances, B as array, C as powerMeterCapability, E as runningStateCapability, F as entryBody, G as string, I as isSecretMarker, J as DeviceRole, K as union, L as isSecretMarkerLike, M as customAction, N as defineCustomActions, P as defineEntryKind, R as rowsDepsOver, S as numericSensorCapability, T as runningFromDishwasherOperation, U as object, V as literal, W as record, X as errMsg, Y as DeviceType, Z as EventCategory, 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 unknown, r as keepBodyKeys, s as DishwasherOperationStateSchema, t as boschRowsMigration, u as binaryCapability, w as quantityFromUnit, x as integrationInstancesCapability, z as _enum } from "../bosch-instances-DXID-jMD.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-DAEciV_Q.mjs";
1
+ import { A as RecordIntegrationInstances, B as array, C as powerMeterCapability, E as runningStateCapability, F as entryBody, G as string, I as isSecretMarker, J as DeviceRole, K as union, L as isSecretMarkerLike, M as customAction, N as defineCustomActions, P as defineEntryKind, R as rowsDepsOver, S as numericSensorCapability, T as runningFromDishwasherOperation, U as object, V as literal, W as record, X as errMsg, Y as DeviceType, Z as EventCategory, 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 unknown, r as keepBodyKeys, s as DishwasherOperationStateSchema, t as boschRowsMigration, u as binaryCapability, w as quantityFromUnit, x as integrationInstancesCapability, z as _enum } from "../bosch-instances-DkUo6BfT.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-BGH1q3tj.mjs";
3
3
  import { createHash, randomBytes } from "node:crypto";
4
4
  import { connect } from "node:tls";
5
5
  import { inflateRawSync } from "node:zlib";
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_bosch_instances = require("../bosch-instances-DdklwoKS.js");
3
- const require_account_integration_offline = require("../account-integration-offline-BiXbzZsg.js");
2
+ const require_bosch_instances = require("../bosch-instances-BD-Valeo.js");
3
+ const require_account_integration_offline = require("../account-integration-offline-BfsMnlJo.js");
4
4
  let node_crypto = require("node:crypto");
5
5
  //#region src/homecom/config.ts
6
6
  var HOMECOM_BRANDS = ["bosch", "buderus"];
@@ -449,7 +449,7 @@ async function readHydronic(input) {
449
449
  const heating = await input.get("/heatingCircuits");
450
450
  const dhwBody = await input.get("/dhwCircuits");
451
451
  const circuits = [];
452
- for (const id of referenceKeys(heating, "hc")) circuits.push(await readCircuit(input.get, id));
452
+ for (const ref of circuitRefs(heating, "hc")) circuits.push(await readCircuit(input.get, ref.id, ref.name));
453
453
  const dhwIds = referenceKeys(dhwBody, "dhw");
454
454
  const first = dhwIds[0];
455
455
  const dhw = first === void 0 ? null : await readDhw(input.get, first);
@@ -509,7 +509,7 @@ async function readRac(input) {
509
509
  setpointResource: "/airConditioning/temperatureSetpoint"
510
510
  }], null);
511
511
  }
512
- async function readCircuit(get, id) {
512
+ async function readCircuit(get, id, givenName) {
513
513
  const base = `/heatingCircuits/${id}`;
514
514
  const loaded = await loadEnvelopes(get, [
515
515
  `${base}/operationMode`,
@@ -532,7 +532,7 @@ async function readCircuit(get, id) {
532
532
  const humidity = numberOf(loaded.get(`${base}/actualHumidity`) ?? null);
533
533
  return {
534
534
  id,
535
- name: id,
535
+ name: heatingCircuitName(id, givenName),
536
536
  operationMode: stringOf(operation) ?? "off",
537
537
  setpoint: cooling ? numberOf(coolingSet) ?? numberOf(current) ?? numberOf(manual) : numberOf(current) ?? numberOf(manual),
538
538
  currentRoomTemp: numberOf(loaded.get(`${base}/roomtemperature`) ?? null),
@@ -634,18 +634,37 @@ function pointtMode(mode) {
634
634
  default: return mode;
635
635
  }
636
636
  }
637
- function referenceKeys(body, prefix) {
637
+ /** `hc1` → "Heating circuit 1", unless the gateway already named the circuit. */
638
+ function heatingCircuitName(id, given) {
639
+ if (given !== null && given.length > 0 && given !== id) return given;
640
+ const n = id.replace(/^hc/, "");
641
+ return /^\d+$/.test(n) ? `Heating circuit ${n}` : "Heating circuit";
642
+ }
643
+ function circuitRefs(body, prefix) {
638
644
  if (!isRecord$2(body) || !Array.isArray(body["references"])) return [];
639
645
  const pattern = prefix === "hc" ? /^hc\d+$/ : /^dhw\d+$/;
640
646
  const out = [];
641
647
  for (const ref of body["references"]) {
642
648
  if (!isRecord$2(ref) || typeof ref["id"] !== "string") continue;
643
649
  const tail = ref["id"].split("/").filter((part) => part.length > 0).at(-1);
644
- if (tail === void 0 || !pattern.test(tail) || out.includes(tail)) continue;
645
- out.push(tail);
650
+ if (tail === void 0 || !pattern.test(tail) || out.some((item) => item.id === tail)) continue;
651
+ out.push({
652
+ id: tail,
653
+ name: refName(ref, tail)
654
+ });
646
655
  }
647
656
  return out;
648
657
  }
658
+ function refName(ref, id) {
659
+ const raw = ref["name"];
660
+ if (typeof raw !== "string") return null;
661
+ const trimmed = raw.trim();
662
+ if (trimmed.length === 0 || trimmed === id) return null;
663
+ return trimmed;
664
+ }
665
+ function referenceKeys(body, prefix) {
666
+ return circuitRefs(body, prefix).map((ref) => ref.id);
667
+ }
649
668
  function indexStandard(body) {
650
669
  const map = /* @__PURE__ */ new Map();
651
670
  if (!isRecord$2(body) || !Array.isArray(body["references"])) return map;
@@ -793,19 +812,186 @@ function safeGet(get, onFailure) {
793
812
  }
794
813
  };
795
814
  }
815
+ var SYSTEM_MODE_KEYS = [
816
+ "silent",
817
+ "away",
818
+ "additionalHeater",
819
+ "holiday"
820
+ ];
821
+ var HEAT_PUMP_ONLY_READINGS = [
822
+ "thermalW",
823
+ "electricalW",
824
+ "compressorPct"
825
+ ];
796
826
  /**
797
- * True when a HEAT PUMP answered: silent mode, the auxiliary heater, heat-pump
798
- * power or compressor modulation. Flow temperatures, pressure and starts are left
799
- * out on purpose: a gas boiler's heat source answers those too, and it is no heat pump.
827
+ * True when the gateway should be offered as a system device (`hp:0`).
828
+ *
829
+ * A system mode counts when the gateway answered it: silent, away, the
830
+ * additional heater, or holiday. An empty string is an answer (holiday often
831
+ * comes back empty); a path that did not answer is not. HomeCom returns no
832
+ * value for silent mode and the auxiliary heater on some heat-pump firmware,
833
+ * so away and holiday alone are enough. A heat-pump-only reading counts too
834
+ * (thermal power, electrical power, compressor modulation). Supply, return,
835
+ * outdoor, pressure, starts and the energy meters do not: a gas boiler's heat
836
+ * source answers flow, pressure and starts, and that is not a heat pump.
800
837
  */
801
- function hasHeatSource(reading) {
838
+ function hasSystemDevice(reading) {
839
+ if (SYSTEM_MODE_KEYS.some((key) => reading[key] !== null)) return true;
840
+ return HEAT_PUMP_ONLY_READINGS.some((key) => reading[key] !== null);
841
+ }
842
+ /** Which heat-pump paths this gateway answered. One log per gateway corrects the path table. */
843
+ function heatPumpPathCensus(reading) {
844
+ const rows = [
845
+ ...systemPathRows(reading),
846
+ ...readingPathRows(reading),
847
+ ...reading.circuits.flatMap(circuitPathRows)
848
+ ];
849
+ const answered = [];
850
+ const unanswered = [];
851
+ for (const row of rows) if (row.present) answered.push(row.path);
852
+ else unanswered.push(row.path);
853
+ return {
854
+ answered,
855
+ unanswered
856
+ };
857
+ }
858
+ function systemPathRows(reading) {
859
+ const s = HEAT_PUMP_PATHS.system;
860
+ return [
861
+ {
862
+ path: s.silent,
863
+ present: reading.silent !== null
864
+ },
865
+ {
866
+ path: s.away,
867
+ present: reading.away !== null
868
+ },
869
+ {
870
+ path: s.additionalHeater,
871
+ present: reading.additionalHeater !== null
872
+ },
873
+ {
874
+ path: s.holiday,
875
+ present: reading.holiday !== null
876
+ }
877
+ ];
878
+ }
879
+ function readingPathRows(reading) {
880
+ const r = HEAT_PUMP_PATHS.readings;
881
+ return [
882
+ {
883
+ path: r.thermalW,
884
+ present: reading.thermalW !== null
885
+ },
886
+ {
887
+ path: r.electricalW,
888
+ present: reading.electricalW !== null
889
+ },
890
+ {
891
+ path: r.compressorPct,
892
+ present: reading.compressorPct !== null
893
+ },
894
+ {
895
+ path: r.supply,
896
+ present: reading.supply !== null
897
+ },
898
+ {
899
+ path: r.return,
900
+ present: reading.return !== null
901
+ },
902
+ {
903
+ path: r.outdoor,
904
+ present: reading.outdoor !== null
905
+ },
906
+ {
907
+ path: r.thermalKwh,
908
+ present: reading.thermalKwh !== null
909
+ },
910
+ {
911
+ path: r.electricalKwh,
912
+ present: reading.electricalKwh !== null
913
+ },
914
+ {
915
+ path: r.pressureBar,
916
+ present: reading.pressureBar !== null
917
+ },
918
+ {
919
+ path: r.starts,
920
+ present: reading.starts !== null
921
+ }
922
+ ];
923
+ }
924
+ function circuitPathRows(circuit) {
925
+ if (circuit.kind === "dhw") return dhwPathRows(circuit);
926
+ return hcPathRows(circuit);
927
+ }
928
+ function hcPathRows(circuit) {
929
+ const base = `/heatingCircuits/${circuit.id}`;
930
+ const p = HEAT_PUMP_PATHS.circuit;
802
931
  return [
803
- reading.silent,
804
- reading.additionalHeater,
805
- reading.thermalW,
806
- reading.electricalW,
807
- reading.compressorPct
808
- ].some((v) => v !== null);
932
+ {
933
+ path: `${base}/${p.operationMode}`,
934
+ present: circuit.operationMode !== null
935
+ },
936
+ {
937
+ path: `${base}/${p.heatCoolMode}`,
938
+ present: circuit.heatCoolMode !== null
939
+ },
940
+ {
941
+ path: `${base}/${p.summerWinter}`,
942
+ present: circuit.summerWinter !== null
943
+ },
944
+ {
945
+ path: `${base}/${p.comfort}`,
946
+ present: circuit.comfort !== null
947
+ },
948
+ {
949
+ path: `${base}/${p.eco}`,
950
+ present: circuit.eco !== null
951
+ },
952
+ {
953
+ path: `${base}/${p.maxFlow}`,
954
+ present: circuit.maxFlow !== null
955
+ },
956
+ {
957
+ path: `${base}/${p.current}`,
958
+ present: circuit.currentTemp !== null
959
+ },
960
+ {
961
+ path: `${base}/${p.target}`,
962
+ present: circuit.targetTemp !== null
963
+ }
964
+ ];
965
+ }
966
+ function dhwPathRows(circuit) {
967
+ const base = `/dhwCircuits/${circuit.id}`;
968
+ const p = HEAT_PUMP_PATHS.dhw;
969
+ return [
970
+ {
971
+ path: `${base}/${p.operationMode}`,
972
+ present: circuit.operationMode !== null
973
+ },
974
+ {
975
+ path: `${base}/${p.level}`,
976
+ present: circuit.level !== null
977
+ },
978
+ {
979
+ path: `${base}/${p.chargeDuration}`,
980
+ present: circuit.chargeDuration !== null
981
+ },
982
+ {
983
+ path: `${base}/${p.charge}`,
984
+ present: circuit.charge !== null
985
+ },
986
+ {
987
+ path: `${base}/${p.current}`,
988
+ present: circuit.currentTemp !== null
989
+ },
990
+ {
991
+ path: `${base}/${p.target}`,
992
+ present: circuit.targetTemp !== null
993
+ }
994
+ ];
809
995
  }
810
996
  async function readHc(get, id) {
811
997
  const base = `/heatingCircuits/${id}`;
@@ -910,7 +1096,6 @@ function heatPumpStatusFrom(reading, now) {
910
1096
  }
911
1097
  function circuitOf(c) {
912
1098
  const kind = kindOf(c);
913
- const mode = textOf$1(c.operationMode);
914
1099
  return {
915
1100
  id: c.id,
916
1101
  kind,
@@ -918,7 +1103,7 @@ function circuitOf(c) {
918
1103
  icon: kind === "dhw" ? "droplets" : kind === "cooling" ? "snowflake" : "flame",
919
1104
  currentTemp: c.currentTemp,
920
1105
  targetTemp: c.targetTemp,
921
- mode,
1106
+ mode: modeValue(c.operationMode),
922
1107
  active: null,
923
1108
  operationMode: optionOf(c.operationMode),
924
1109
  heatCoolMode: optionOf(c.heatCoolMode),
@@ -938,15 +1123,24 @@ function kindOf(c) {
938
1123
  function labelOf(c) {
939
1124
  const n = c.id.replace(/^\D+/, "");
940
1125
  if (c.kind === "dhw") return n === "1" || n === "" ? "Hot water" : `Hot water ${n}`;
941
- return n === "" ? "Circuit" : `Circuit ${n}`;
1126
+ return n === "" ? "Heating circuit" : `Heating circuit ${n}`;
942
1127
  }
943
1128
  function textOf$1(s) {
944
1129
  return s !== null && typeof s.value === "string" && s.value.length > 0 ? s.value : null;
945
1130
  }
1131
+ /**
1132
+ * A mode the gateway answered with an empty value is off (or "none" when the
1133
+ * gateway did not list off). A mode it did not answer at all stays null.
1134
+ */
1135
+ function modeValue(s) {
1136
+ if (s === null || typeof s.value !== "string") return null;
1137
+ if (s.value.length > 0) return s.value;
1138
+ return s.allowed.includes("off") ? "off" : "none";
1139
+ }
946
1140
  function optionOf(s) {
947
1141
  if (s === null) return null;
948
1142
  return {
949
- value: textOf$1(s),
1143
+ value: modeValue(s),
950
1144
  allowed: [...s.allowed]
951
1145
  };
952
1146
  }
@@ -1113,6 +1307,7 @@ function createHomeComApiClient(config, gatewayId, deps = {}) {
1113
1307
  let last = null;
1114
1308
  let refreshTail = Promise.resolve();
1115
1309
  const dropped = /* @__PURE__ */ new Set();
1310
+ let heatPumpPathsLogged = false;
1116
1311
  const base = homeComApiBase(config.region);
1117
1312
  function authHeaders() {
1118
1313
  return {
@@ -1267,11 +1462,24 @@ function createHomeComApiClient(config, gatewayId, deps = {}) {
1267
1462
  dhw: dhwId === void 0 ? [] : [dhwId]
1268
1463
  }
1269
1464
  });
1270
- return hasHeatSource(heatPump) ? {
1465
+ logHeatPumpPaths(heatPump);
1466
+ return hasSystemDevice(heatPump) ? {
1271
1467
  ...snapshot,
1272
1468
  heatPump
1273
1469
  } : snapshot;
1274
1470
  }
1471
+ function logHeatPumpPaths(reading) {
1472
+ if (heatPumpPathsLogged) return;
1473
+ heatPumpPathsLogged = true;
1474
+ const census = heatPumpPathCensus(reading);
1475
+ logger?.info("HomeCom heat-pump paths", {
1476
+ tags: { gatewayId },
1477
+ meta: {
1478
+ answered: census.answered,
1479
+ unanswered: census.unanswered
1480
+ }
1481
+ });
1482
+ }
1275
1483
  function logHeatPumpFailure(path, error) {
1276
1484
  const key = `heat-pump-read:${path}`;
1277
1485
  if (dropped.has(key)) return;
@@ -1,5 +1,5 @@
1
- import { A as RecordIntegrationInstances, B as array, F as entryBody, G as string, H as number, M as customAction, N as defineCustomActions, P as defineEntryKind, R as rowsDepsOver, U as object, V as literal, X as errMsg, Y as DeviceType, Z as EventCategory, _ as heatPumpControlCapability, a as BaseDevice, d as climateControlCapability, i as testBoschInstance, k as waterHeaterCapability, n as boschRowsPorts, o as BaseDeviceProvider, p as deviceAdoptionCapability, r as keepBodyKeys, t as boschRowsMigration, v as heatPumpStatusCapability, x as integrationInstancesCapability, z as _enum } from "../bosch-instances-DXID-jMD.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-DAEciV_Q.mjs";
1
+ import { A as RecordIntegrationInstances, B as array, F as entryBody, G as string, H as number, M as customAction, N as defineCustomActions, P as defineEntryKind, R as rowsDepsOver, U as object, V as literal, X as errMsg, Y as DeviceType, Z as EventCategory, _ as heatPumpControlCapability, a as BaseDevice, d as climateControlCapability, i as testBoschInstance, k as waterHeaterCapability, n as boschRowsPorts, o as BaseDeviceProvider, p as deviceAdoptionCapability, r as keepBodyKeys, t as boschRowsMigration, v as heatPumpStatusCapability, x as integrationInstancesCapability, z as _enum } from "../bosch-instances-DkUo6BfT.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-BGH1q3tj.mjs";
3
3
  import { createHash, randomBytes } from "node:crypto";
4
4
  //#region src/homecom/config.ts
5
5
  var HOMECOM_BRANDS = ["bosch", "buderus"];
@@ -448,7 +448,7 @@ async function readHydronic(input) {
448
448
  const heating = await input.get("/heatingCircuits");
449
449
  const dhwBody = await input.get("/dhwCircuits");
450
450
  const circuits = [];
451
- for (const id of referenceKeys(heating, "hc")) circuits.push(await readCircuit(input.get, id));
451
+ for (const ref of circuitRefs(heating, "hc")) circuits.push(await readCircuit(input.get, ref.id, ref.name));
452
452
  const dhwIds = referenceKeys(dhwBody, "dhw");
453
453
  const first = dhwIds[0];
454
454
  const dhw = first === void 0 ? null : await readDhw(input.get, first);
@@ -508,7 +508,7 @@ async function readRac(input) {
508
508
  setpointResource: "/airConditioning/temperatureSetpoint"
509
509
  }], null);
510
510
  }
511
- async function readCircuit(get, id) {
511
+ async function readCircuit(get, id, givenName) {
512
512
  const base = `/heatingCircuits/${id}`;
513
513
  const loaded = await loadEnvelopes(get, [
514
514
  `${base}/operationMode`,
@@ -531,7 +531,7 @@ async function readCircuit(get, id) {
531
531
  const humidity = numberOf(loaded.get(`${base}/actualHumidity`) ?? null);
532
532
  return {
533
533
  id,
534
- name: id,
534
+ name: heatingCircuitName(id, givenName),
535
535
  operationMode: stringOf(operation) ?? "off",
536
536
  setpoint: cooling ? numberOf(coolingSet) ?? numberOf(current) ?? numberOf(manual) : numberOf(current) ?? numberOf(manual),
537
537
  currentRoomTemp: numberOf(loaded.get(`${base}/roomtemperature`) ?? null),
@@ -633,18 +633,37 @@ function pointtMode(mode) {
633
633
  default: return mode;
634
634
  }
635
635
  }
636
- function referenceKeys(body, prefix) {
636
+ /** `hc1` → "Heating circuit 1", unless the gateway already named the circuit. */
637
+ function heatingCircuitName(id, given) {
638
+ if (given !== null && given.length > 0 && given !== id) return given;
639
+ const n = id.replace(/^hc/, "");
640
+ return /^\d+$/.test(n) ? `Heating circuit ${n}` : "Heating circuit";
641
+ }
642
+ function circuitRefs(body, prefix) {
637
643
  if (!isRecord$2(body) || !Array.isArray(body["references"])) return [];
638
644
  const pattern = prefix === "hc" ? /^hc\d+$/ : /^dhw\d+$/;
639
645
  const out = [];
640
646
  for (const ref of body["references"]) {
641
647
  if (!isRecord$2(ref) || typeof ref["id"] !== "string") continue;
642
648
  const tail = ref["id"].split("/").filter((part) => part.length > 0).at(-1);
643
- if (tail === void 0 || !pattern.test(tail) || out.includes(tail)) continue;
644
- out.push(tail);
649
+ if (tail === void 0 || !pattern.test(tail) || out.some((item) => item.id === tail)) continue;
650
+ out.push({
651
+ id: tail,
652
+ name: refName(ref, tail)
653
+ });
645
654
  }
646
655
  return out;
647
656
  }
657
+ function refName(ref, id) {
658
+ const raw = ref["name"];
659
+ if (typeof raw !== "string") return null;
660
+ const trimmed = raw.trim();
661
+ if (trimmed.length === 0 || trimmed === id) return null;
662
+ return trimmed;
663
+ }
664
+ function referenceKeys(body, prefix) {
665
+ return circuitRefs(body, prefix).map((ref) => ref.id);
666
+ }
648
667
  function indexStandard(body) {
649
668
  const map = /* @__PURE__ */ new Map();
650
669
  if (!isRecord$2(body) || !Array.isArray(body["references"])) return map;
@@ -792,19 +811,186 @@ function safeGet(get, onFailure) {
792
811
  }
793
812
  };
794
813
  }
814
+ var SYSTEM_MODE_KEYS = [
815
+ "silent",
816
+ "away",
817
+ "additionalHeater",
818
+ "holiday"
819
+ ];
820
+ var HEAT_PUMP_ONLY_READINGS = [
821
+ "thermalW",
822
+ "electricalW",
823
+ "compressorPct"
824
+ ];
795
825
  /**
796
- * True when a HEAT PUMP answered: silent mode, the auxiliary heater, heat-pump
797
- * power or compressor modulation. Flow temperatures, pressure and starts are left
798
- * out on purpose: a gas boiler's heat source answers those too, and it is no heat pump.
826
+ * True when the gateway should be offered as a system device (`hp:0`).
827
+ *
828
+ * A system mode counts when the gateway answered it: silent, away, the
829
+ * additional heater, or holiday. An empty string is an answer (holiday often
830
+ * comes back empty); a path that did not answer is not. HomeCom returns no
831
+ * value for silent mode and the auxiliary heater on some heat-pump firmware,
832
+ * so away and holiday alone are enough. A heat-pump-only reading counts too
833
+ * (thermal power, electrical power, compressor modulation). Supply, return,
834
+ * outdoor, pressure, starts and the energy meters do not: a gas boiler's heat
835
+ * source answers flow, pressure and starts, and that is not a heat pump.
799
836
  */
800
- function hasHeatSource(reading) {
837
+ function hasSystemDevice(reading) {
838
+ if (SYSTEM_MODE_KEYS.some((key) => reading[key] !== null)) return true;
839
+ return HEAT_PUMP_ONLY_READINGS.some((key) => reading[key] !== null);
840
+ }
841
+ /** Which heat-pump paths this gateway answered. One log per gateway corrects the path table. */
842
+ function heatPumpPathCensus(reading) {
843
+ const rows = [
844
+ ...systemPathRows(reading),
845
+ ...readingPathRows(reading),
846
+ ...reading.circuits.flatMap(circuitPathRows)
847
+ ];
848
+ const answered = [];
849
+ const unanswered = [];
850
+ for (const row of rows) if (row.present) answered.push(row.path);
851
+ else unanswered.push(row.path);
852
+ return {
853
+ answered,
854
+ unanswered
855
+ };
856
+ }
857
+ function systemPathRows(reading) {
858
+ const s = HEAT_PUMP_PATHS.system;
859
+ return [
860
+ {
861
+ path: s.silent,
862
+ present: reading.silent !== null
863
+ },
864
+ {
865
+ path: s.away,
866
+ present: reading.away !== null
867
+ },
868
+ {
869
+ path: s.additionalHeater,
870
+ present: reading.additionalHeater !== null
871
+ },
872
+ {
873
+ path: s.holiday,
874
+ present: reading.holiday !== null
875
+ }
876
+ ];
877
+ }
878
+ function readingPathRows(reading) {
879
+ const r = HEAT_PUMP_PATHS.readings;
880
+ return [
881
+ {
882
+ path: r.thermalW,
883
+ present: reading.thermalW !== null
884
+ },
885
+ {
886
+ path: r.electricalW,
887
+ present: reading.electricalW !== null
888
+ },
889
+ {
890
+ path: r.compressorPct,
891
+ present: reading.compressorPct !== null
892
+ },
893
+ {
894
+ path: r.supply,
895
+ present: reading.supply !== null
896
+ },
897
+ {
898
+ path: r.return,
899
+ present: reading.return !== null
900
+ },
901
+ {
902
+ path: r.outdoor,
903
+ present: reading.outdoor !== null
904
+ },
905
+ {
906
+ path: r.thermalKwh,
907
+ present: reading.thermalKwh !== null
908
+ },
909
+ {
910
+ path: r.electricalKwh,
911
+ present: reading.electricalKwh !== null
912
+ },
913
+ {
914
+ path: r.pressureBar,
915
+ present: reading.pressureBar !== null
916
+ },
917
+ {
918
+ path: r.starts,
919
+ present: reading.starts !== null
920
+ }
921
+ ];
922
+ }
923
+ function circuitPathRows(circuit) {
924
+ if (circuit.kind === "dhw") return dhwPathRows(circuit);
925
+ return hcPathRows(circuit);
926
+ }
927
+ function hcPathRows(circuit) {
928
+ const base = `/heatingCircuits/${circuit.id}`;
929
+ const p = HEAT_PUMP_PATHS.circuit;
801
930
  return [
802
- reading.silent,
803
- reading.additionalHeater,
804
- reading.thermalW,
805
- reading.electricalW,
806
- reading.compressorPct
807
- ].some((v) => v !== null);
931
+ {
932
+ path: `${base}/${p.operationMode}`,
933
+ present: circuit.operationMode !== null
934
+ },
935
+ {
936
+ path: `${base}/${p.heatCoolMode}`,
937
+ present: circuit.heatCoolMode !== null
938
+ },
939
+ {
940
+ path: `${base}/${p.summerWinter}`,
941
+ present: circuit.summerWinter !== null
942
+ },
943
+ {
944
+ path: `${base}/${p.comfort}`,
945
+ present: circuit.comfort !== null
946
+ },
947
+ {
948
+ path: `${base}/${p.eco}`,
949
+ present: circuit.eco !== null
950
+ },
951
+ {
952
+ path: `${base}/${p.maxFlow}`,
953
+ present: circuit.maxFlow !== null
954
+ },
955
+ {
956
+ path: `${base}/${p.current}`,
957
+ present: circuit.currentTemp !== null
958
+ },
959
+ {
960
+ path: `${base}/${p.target}`,
961
+ present: circuit.targetTemp !== null
962
+ }
963
+ ];
964
+ }
965
+ function dhwPathRows(circuit) {
966
+ const base = `/dhwCircuits/${circuit.id}`;
967
+ const p = HEAT_PUMP_PATHS.dhw;
968
+ return [
969
+ {
970
+ path: `${base}/${p.operationMode}`,
971
+ present: circuit.operationMode !== null
972
+ },
973
+ {
974
+ path: `${base}/${p.level}`,
975
+ present: circuit.level !== null
976
+ },
977
+ {
978
+ path: `${base}/${p.chargeDuration}`,
979
+ present: circuit.chargeDuration !== null
980
+ },
981
+ {
982
+ path: `${base}/${p.charge}`,
983
+ present: circuit.charge !== null
984
+ },
985
+ {
986
+ path: `${base}/${p.current}`,
987
+ present: circuit.currentTemp !== null
988
+ },
989
+ {
990
+ path: `${base}/${p.target}`,
991
+ present: circuit.targetTemp !== null
992
+ }
993
+ ];
808
994
  }
809
995
  async function readHc(get, id) {
810
996
  const base = `/heatingCircuits/${id}`;
@@ -909,7 +1095,6 @@ function heatPumpStatusFrom(reading, now) {
909
1095
  }
910
1096
  function circuitOf(c) {
911
1097
  const kind = kindOf(c);
912
- const mode = textOf$1(c.operationMode);
913
1098
  return {
914
1099
  id: c.id,
915
1100
  kind,
@@ -917,7 +1102,7 @@ function circuitOf(c) {
917
1102
  icon: kind === "dhw" ? "droplets" : kind === "cooling" ? "snowflake" : "flame",
918
1103
  currentTemp: c.currentTemp,
919
1104
  targetTemp: c.targetTemp,
920
- mode,
1105
+ mode: modeValue(c.operationMode),
921
1106
  active: null,
922
1107
  operationMode: optionOf(c.operationMode),
923
1108
  heatCoolMode: optionOf(c.heatCoolMode),
@@ -937,15 +1122,24 @@ function kindOf(c) {
937
1122
  function labelOf(c) {
938
1123
  const n = c.id.replace(/^\D+/, "");
939
1124
  if (c.kind === "dhw") return n === "1" || n === "" ? "Hot water" : `Hot water ${n}`;
940
- return n === "" ? "Circuit" : `Circuit ${n}`;
1125
+ return n === "" ? "Heating circuit" : `Heating circuit ${n}`;
941
1126
  }
942
1127
  function textOf$1(s) {
943
1128
  return s !== null && typeof s.value === "string" && s.value.length > 0 ? s.value : null;
944
1129
  }
1130
+ /**
1131
+ * A mode the gateway answered with an empty value is off (or "none" when the
1132
+ * gateway did not list off). A mode it did not answer at all stays null.
1133
+ */
1134
+ function modeValue(s) {
1135
+ if (s === null || typeof s.value !== "string") return null;
1136
+ if (s.value.length > 0) return s.value;
1137
+ return s.allowed.includes("off") ? "off" : "none";
1138
+ }
945
1139
  function optionOf(s) {
946
1140
  if (s === null) return null;
947
1141
  return {
948
- value: textOf$1(s),
1142
+ value: modeValue(s),
949
1143
  allowed: [...s.allowed]
950
1144
  };
951
1145
  }
@@ -1112,6 +1306,7 @@ function createHomeComApiClient(config, gatewayId, deps = {}) {
1112
1306
  let last = null;
1113
1307
  let refreshTail = Promise.resolve();
1114
1308
  const dropped = /* @__PURE__ */ new Set();
1309
+ let heatPumpPathsLogged = false;
1115
1310
  const base = homeComApiBase(config.region);
1116
1311
  function authHeaders() {
1117
1312
  return {
@@ -1266,11 +1461,24 @@ function createHomeComApiClient(config, gatewayId, deps = {}) {
1266
1461
  dhw: dhwId === void 0 ? [] : [dhwId]
1267
1462
  }
1268
1463
  });
1269
- return hasHeatSource(heatPump) ? {
1464
+ logHeatPumpPaths(heatPump);
1465
+ return hasSystemDevice(heatPump) ? {
1270
1466
  ...snapshot,
1271
1467
  heatPump
1272
1468
  } : snapshot;
1273
1469
  }
1470
+ function logHeatPumpPaths(reading) {
1471
+ if (heatPumpPathsLogged) return;
1472
+ heatPumpPathsLogged = true;
1473
+ const census = heatPumpPathCensus(reading);
1474
+ logger?.info("HomeCom heat-pump paths", {
1475
+ tags: { gatewayId },
1476
+ meta: {
1477
+ answered: census.answered,
1478
+ unanswered: census.unanswered
1479
+ }
1480
+ });
1481
+ }
1274
1482
  function logHeatPumpFailure(path, error) {
1275
1483
  const key = `heat-pump-read:${path}`;
1276
1484
  if (dropped.has(key)) return;
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-DdklwoKS.js");
5
+ const require_bosch_instances = require("../bosch-instances-BD-Valeo.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");
@@ -1,4 +1,4 @@
1
- import { A as RecordIntegrationInstances, B as array, D as switchCapability, F as entryBody, G as string, H as number, M as customAction, N as defineCustomActions, O as temperatureSensorCapability, P as defineEntryKind, R as rowsDepsOver, U as object, V as literal, X as errMsg, Y as DeviceType, Z as EventCategory, a as BaseDevice, d as climateControlCapability, i as testBoschInstance, l as batteryCapability, n as boschRowsPorts, o as BaseDeviceProvider, p as deviceAdoptionCapability, r as keepBodyKeys, t as boschRowsMigration, x as integrationInstancesCapability, y as heatingValveCapability, z as _enum } from "../bosch-instances-DXID-jMD.mjs";
1
+ import { A as RecordIntegrationInstances, B as array, D as switchCapability, F as entryBody, G as string, H as number, M as customAction, N as defineCustomActions, O as temperatureSensorCapability, P as defineEntryKind, R as rowsDepsOver, U as object, V as literal, X as errMsg, Y as DeviceType, Z as EventCategory, a as BaseDevice, d as climateControlCapability, i as testBoschInstance, l as batteryCapability, n as boschRowsPorts, o as BaseDeviceProvider, p as deviceAdoptionCapability, r as keepBodyKeys, t as boschRowsMigration, x as integrationInstancesCapability, y as heatingValveCapability, z as _enum } from "../bosch-instances-DkUo6BfT.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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-bosch",
3
- "version": "0.1.45",
3
+ "version": "0.1.46",
4
4
  "description": "Bosch smart-home device-provider addons for CamStack — SHC, Home Connect, and HomeCom cloud integrations (scaffold)",
5
5
  "keywords": [
6
6
  "camstack",
@@ -138,7 +138,7 @@
138
138
  }
139
139
  ],
140
140
  "requires": {
141
- "server": ">=1.2.821"
141
+ "server": ">=1.2.822"
142
142
  }
143
143
  },
144
144
  "files": [