@camstack/addon-provider-bosch 0.1.45 → 0.1.47

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.
@@ -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-DAqkAsRO.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-Bie4aS5e.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;
801
859
  return [
802
- reading.silent,
803
- reading.additionalHeater,
804
- reading.thermalW,
805
- reading.electricalW,
806
- reading.compressorPct
807
- ].some((v) => v !== null);
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;
930
+ return [
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;
@@ -1598,6 +1806,129 @@ function buildHomeComGatewayPersister(deps) {
1598
1806
  return (integrationId, gatewayId) => patch(integrationId, { gatewayId });
1599
1807
  }
1600
1808
  //#endregion
1809
+ //#region src/homecom/homecom-gateway.ts
1810
+ /** Stable id of one HomeCom device. Adoption matches an existing row on this. */
1811
+ function homeComStableId(integrationId, nativeId) {
1812
+ return `homecom:${integrationId}:${nativeId}`;
1813
+ }
1814
+ /** The hot-water child of a heat pump. A boiler's own row keeps its longer name. */
1815
+ var HOMECOM_HOT_WATER_CHILD_NAME = "Hot water";
1816
+ var HomeComConnectionResolver = class {
1817
+ #surfaces = /* @__PURE__ */ new Map();
1818
+ set(integrationId, surface) {
1819
+ if (surface === null) {
1820
+ this.#surfaces.delete(integrationId);
1821
+ return;
1822
+ }
1823
+ this.#surfaces.set(integrationId, surface);
1824
+ }
1825
+ has(integrationId) {
1826
+ return this.#surfaces.has(integrationId);
1827
+ }
1828
+ list() {
1829
+ return Array.from(this.#surfaces.keys()).map((id) => ({ id }));
1830
+ }
1831
+ discoverables(integrationId) {
1832
+ return this.#surfaces.get(integrationId)?.discoverables ?? [];
1833
+ }
1834
+ getClient(integrationId) {
1835
+ return this.#surfaces.get(integrationId)?.client ?? null;
1836
+ }
1837
+ getSnapshot(integrationId) {
1838
+ return this.#surfaces.get(integrationId)?.snapshot ?? null;
1839
+ }
1840
+ updateSnapshot(integrationId, snapshot) {
1841
+ const existing = this.#surfaces.get(integrationId);
1842
+ if (!existing) return;
1843
+ this.#surfaces.set(integrationId, {
1844
+ ...existing,
1845
+ snapshot,
1846
+ discoverables: buildDiscoverables(snapshot)
1847
+ });
1848
+ }
1849
+ clear() {
1850
+ this.#surfaces.clear();
1851
+ }
1852
+ };
1853
+ var BOILER_DHW_NAME = "Domestic hot water";
1854
+ function circuitDiscoverable(circuit) {
1855
+ return {
1856
+ nativeId: `hc:${circuit.id}`,
1857
+ name: circuit.name,
1858
+ kind: "heating-circuit",
1859
+ circuitId: circuit.id
1860
+ };
1861
+ }
1862
+ function dhwDiscoverable(name) {
1863
+ return {
1864
+ nativeId: "dhw:0",
1865
+ name,
1866
+ kind: "dhw"
1867
+ };
1868
+ }
1869
+ /**
1870
+ * A gateway with a heat pump is one pick. A boiler still offers each circuit
1871
+ * and the hot-water row on its own.
1872
+ */
1873
+ function buildDiscoverables(snapshot) {
1874
+ const circuits = snapshot.circuits.map(circuitDiscoverable);
1875
+ if (snapshot.heatPump !== void 0) return [{
1876
+ nativeId: "hp:0",
1877
+ name: "Heat pump",
1878
+ kind: "heat-pump",
1879
+ children: [...circuits, ...snapshot.dhw === null ? [] : [dhwDiscoverable(HOMECOM_HOT_WATER_CHILD_NAME)]]
1880
+ }];
1881
+ return [...circuits, ...snapshot.dhw === null ? [] : [dhwDiscoverable(BOILER_DHW_NAME)]];
1882
+ }
1883
+ var homeComConnections = new HomeComConnectionResolver();
1884
+ //#endregion
1885
+ //#region src/homecom/homecom-adoption-plan.ts
1886
+ function createOf(device, parentNativeId) {
1887
+ return {
1888
+ nativeId: device.nativeId,
1889
+ name: device.name,
1890
+ kind: device.kind,
1891
+ circuitId: device.circuitId,
1892
+ parentNativeId
1893
+ };
1894
+ }
1895
+ /**
1896
+ * The devices one pick creates. A heat pump also creates its children, except
1897
+ * a circuit or hot-water row that already exists: a top-level one is reported
1898
+ * so the caller can log why it stayed there, and any existing row is not
1899
+ * created again.
1900
+ */
1901
+ function planHomeComAdoption(device, integrationId, adopted) {
1902
+ const root = createOf(device, null);
1903
+ if (device.kind !== "heat-pump") return {
1904
+ create: [root],
1905
+ stayedTopLevel: [],
1906
+ alreadyPresent: []
1907
+ };
1908
+ const byStableId = new Map(adopted.map((row) => [row.stableId, row]));
1909
+ const create = [root];
1910
+ const stayedTopLevel = [];
1911
+ const alreadyPresent = [];
1912
+ for (const child of device.children ?? []) {
1913
+ const existing = byStableId.get(homeComStableId(integrationId, child.nativeId));
1914
+ if (existing === void 0) {
1915
+ create.push(createOf(child, device.nativeId));
1916
+ continue;
1917
+ }
1918
+ const skip = {
1919
+ nativeId: child.nativeId,
1920
+ deviceId: existing.id
1921
+ };
1922
+ if (existing.parentDeviceId === null) stayedTopLevel.push(skip);
1923
+ else alreadyPresent.push(skip);
1924
+ }
1925
+ return {
1926
+ create,
1927
+ stayedTopLevel,
1928
+ alreadyPresent
1929
+ };
1930
+ }
1931
+ //#endregion
1601
1932
  //#region src/homecom/homecom-discovery.ts
1602
1933
  function buildHomeComCandidates(input) {
1603
1934
  const out = [];
@@ -1617,6 +1948,9 @@ function buildHomeComCandidates(input) {
1617
1948
  }
1618
1949
  return out;
1619
1950
  }
1951
+ function homeComDeviceType(kind) {
1952
+ return shapeOf(kind).type;
1953
+ }
1620
1954
  function shapeOf(kind) {
1621
1955
  switch (kind) {
1622
1956
  case "heating-circuit": return {
@@ -1640,6 +1974,62 @@ var DEVICES_FILTER = {
1640
1974
  label: "Devices",
1641
1975
  isDefault: true
1642
1976
  };
1977
+ var STAYED_TOP_LEVEL = "HomeCom device stayed top-level: it was already adopted, so it was not created again under the heat pump";
1978
+ var ALREADY_PRESENT = "HomeCom device was not created again: a device with this stable id already exists";
1979
+ async function adoptOne(input) {
1980
+ const { dev, integrationId, name, known, adoptDevice, logger, logOnce } = input;
1981
+ const plan = planHomeComAdoption(dev, integrationId, known);
1982
+ for (const skip of plan.stayedTopLevel) logOnce(skip.deviceId, STAYED_TOP_LEVEL);
1983
+ for (const skip of plan.alreadyPresent) logOnce(skip.deviceId, ALREADY_PRESENT);
1984
+ const root = plan.create[0];
1985
+ if (root === void 0) return { ok: false };
1986
+ let parentDeviceId;
1987
+ try {
1988
+ parentDeviceId = (await adoptDevice({
1989
+ nativeId: root.nativeId,
1990
+ integrationId,
1991
+ type: homeComDeviceType(root.kind),
1992
+ name,
1993
+ kind: root.kind,
1994
+ ...root.circuitId === void 0 ? {} : { circuitId: root.circuitId },
1995
+ parentDeviceId: null
1996
+ })).deviceId;
1997
+ } catch (err) {
1998
+ logger.warn("homecom adopt failed", { meta: {
1999
+ nativeId: root.nativeId,
2000
+ integrationId,
2001
+ error: errMsg(err)
2002
+ } });
2003
+ return { ok: false };
2004
+ }
2005
+ const accessoryDeviceIds = [];
2006
+ for (const child of plan.create.slice(1)) try {
2007
+ const created = await adoptDevice({
2008
+ nativeId: child.nativeId,
2009
+ integrationId,
2010
+ type: homeComDeviceType(child.kind),
2011
+ name: child.name,
2012
+ kind: child.kind,
2013
+ ...child.circuitId === void 0 ? {} : { circuitId: child.circuitId },
2014
+ parentDeviceId
2015
+ });
2016
+ accessoryDeviceIds.push(created.deviceId);
2017
+ } catch (err) {
2018
+ logger.warn("homecom adopt child failed", {
2019
+ tags: { deviceId: parentDeviceId },
2020
+ meta: {
2021
+ nativeId: child.nativeId,
2022
+ integrationId,
2023
+ error: errMsg(err)
2024
+ }
2025
+ });
2026
+ }
2027
+ return {
2028
+ ok: true,
2029
+ parentDeviceId,
2030
+ accessoryDeviceIds
2031
+ };
2032
+ }
1643
2033
  async function adoptedMapForIntegration(integrationId, listAdopted) {
1644
2034
  const all = await listAdopted();
1645
2035
  const map = /* @__PURE__ */ new Map();
@@ -1651,6 +2041,12 @@ async function adoptedMapForIntegration(integrationId, listAdopted) {
1651
2041
  }
1652
2042
  function buildHomeComAdoptionProvider(deps) {
1653
2043
  const { registry, hasIntegration, listIntegrations, listAdopted, adoptDevice, removeDevice, findDeviceConfig, logger } = deps;
2044
+ const logged = /* @__PURE__ */ new Set();
2045
+ const logOnce = (deviceId, message) => {
2046
+ if (logged.has(deviceId)) return;
2047
+ logged.add(deviceId);
2048
+ logger.info(message, { tags: { deviceId } });
2049
+ };
1654
2050
  async function allCandidatesForIntegration(integrationId) {
1655
2051
  return buildHomeComCandidates({
1656
2052
  devices: registry.discoverables(integrationId),
@@ -1705,48 +2101,36 @@ function buildHomeComAdoptionProvider(deps) {
1705
2101
  adopt: async ({ integrationId, childNativeIds, perCandidate }) => {
1706
2102
  if (!registry.has(integrationId)) throw new Error(`homecom adopt: integration ${integrationId} not connected`);
1707
2103
  const devices = registry.discoverables(integrationId);
2104
+ const known = await listAdopted();
1708
2105
  const adopted = [];
1709
2106
  let failures = 0;
1710
- for (const nativeId of childNativeIds) try {
2107
+ for (const nativeId of childNativeIds) {
1711
2108
  const dev = devices.find((d) => d.nativeId === nativeId);
1712
2109
  if (!dev) {
1713
2110
  failures++;
1714
2111
  continue;
1715
2112
  }
1716
- const candidate = buildHomeComCandidates({
1717
- devices: [dev],
1718
- adopted: /* @__PURE__ */ new Map()
1719
- })[0];
1720
- if (!candidate) {
2113
+ const outcome = await adoptOne({
2114
+ dev,
2115
+ integrationId,
2116
+ name: perCandidate?.[nativeId]?.name ?? dev.name,
2117
+ known,
2118
+ adoptDevice,
2119
+ logger,
2120
+ logOnce
2121
+ });
2122
+ if (!outcome.ok) {
1721
2123
  failures++;
1722
2124
  continue;
1723
2125
  }
1724
- const name = perCandidate?.[nativeId]?.name ?? dev.name;
1725
- const { deviceId } = await adoptDevice({
1726
- nativeId,
1727
- integrationId,
1728
- type: candidate.type,
1729
- name,
1730
- kind: dev.kind,
1731
- circuitId: dev.circuitId
1732
- });
1733
2126
  adopted.push({
1734
2127
  childNativeId: nativeId,
1735
- parentDeviceId: deviceId
2128
+ parentDeviceId: outcome.parentDeviceId,
2129
+ accessoryDeviceIds: outcome.accessoryDeviceIds
1736
2130
  });
1737
- } catch (err) {
1738
- logger.warn("homecom adopt failed", { meta: {
1739
- nativeId,
1740
- integrationId,
1741
- error: errMsg(err)
1742
- } });
1743
- failures++;
1744
2131
  }
1745
2132
  if (adopted.length === 0 && failures > 0) throw new Error(`homecom adopt: all ${failures} adopt(s) failed`);
1746
- return { adopted: adopted.map((a) => ({
1747
- ...a,
1748
- accessoryDeviceIds: []
1749
- })) };
2133
+ return { adopted };
1750
2134
  },
1751
2135
  release: async ({ camDeviceId }) => {
1752
2136
  await removeDevice(camDeviceId);
@@ -1766,66 +2150,6 @@ function buildHomeComAdoptionProvider(deps) {
1766
2150
  };
1767
2151
  }
1768
2152
  //#endregion
1769
- //#region src/homecom/homecom-gateway.ts
1770
- var HomeComConnectionResolver = class {
1771
- #surfaces = /* @__PURE__ */ new Map();
1772
- set(integrationId, surface) {
1773
- if (surface === null) {
1774
- this.#surfaces.delete(integrationId);
1775
- return;
1776
- }
1777
- this.#surfaces.set(integrationId, surface);
1778
- }
1779
- has(integrationId) {
1780
- return this.#surfaces.has(integrationId);
1781
- }
1782
- list() {
1783
- return Array.from(this.#surfaces.keys()).map((id) => ({ id }));
1784
- }
1785
- discoverables(integrationId) {
1786
- return this.#surfaces.get(integrationId)?.discoverables ?? [];
1787
- }
1788
- getClient(integrationId) {
1789
- return this.#surfaces.get(integrationId)?.client ?? null;
1790
- }
1791
- getSnapshot(integrationId) {
1792
- return this.#surfaces.get(integrationId)?.snapshot ?? null;
1793
- }
1794
- updateSnapshot(integrationId, snapshot) {
1795
- const existing = this.#surfaces.get(integrationId);
1796
- if (!existing) return;
1797
- this.#surfaces.set(integrationId, {
1798
- ...existing,
1799
- snapshot,
1800
- discoverables: buildDiscoverables(snapshot)
1801
- });
1802
- }
1803
- clear() {
1804
- this.#surfaces.clear();
1805
- }
1806
- };
1807
- function buildDiscoverables(snapshot) {
1808
- const out = [];
1809
- for (const circuit of snapshot.circuits) out.push({
1810
- nativeId: `hc:${circuit.id}`,
1811
- name: circuit.name,
1812
- kind: "heating-circuit",
1813
- circuitId: circuit.id
1814
- });
1815
- if (snapshot.dhw) out.push({
1816
- nativeId: "dhw:0",
1817
- name: "Domestic hot water",
1818
- kind: "dhw"
1819
- });
1820
- if (snapshot.heatPump !== void 0) out.push({
1821
- nativeId: "hp:0",
1822
- name: "Heat pump",
1823
- kind: "heat-pump"
1824
- });
1825
- return out;
1826
- }
1827
- var homeComConnections = new HomeComConnectionResolver();
1828
- //#endregion
1829
2153
  //#region src/homecom/homecom-integration-manager.ts
1830
2154
  var HomeComIntegrationManager = class {
1831
2155
  #id;
@@ -2724,7 +3048,7 @@ var BoschHomeComProviderAddon = class extends BaseDeviceProvider {
2724
3048
  if (typeof integrationId !== "string" || integrationId.length === 0) throw new Error("homecom generateStableId: brokerId required");
2725
3049
  const nativeId = config?.["nativeId"];
2726
3050
  if (typeof nativeId !== "string" || nativeId.length === 0) throw new Error("homecom generateStableId: nativeId required");
2727
- return `homecom:${integrationId}:${nativeId}`;
3051
+ return homeComStableId(integrationId, nativeId);
2728
3052
  }
2729
3053
  async reconcileIntegrations() {
2730
3054
  const reg = this.requireClients();
@@ -2786,12 +3110,14 @@ var BoschHomeComProviderAddon = class extends BaseDeviceProvider {
2786
3110
  const config = await devices.loadConfig(d.id).catch(() => ({}));
2787
3111
  out.push({
2788
3112
  id: d.id,
3113
+ stableId: d.stableId,
3114
+ parentDeviceId: d.parentDeviceId,
2789
3115
  config
2790
3116
  });
2791
3117
  }
2792
3118
  return out;
2793
3119
  },
2794
- adoptDevice: async ({ nativeId, integrationId, type, name, kind, circuitId }) => {
3120
+ adoptDevice: async ({ nativeId, integrationId, type, name, kind, circuitId, parentDeviceId }) => {
2795
3121
  const devices = this.ctx.kernel.devices;
2796
3122
  if (!devices) throw new Error("homecom adopt: devices unavailable");
2797
3123
  const config = {
@@ -2805,7 +3131,7 @@ var BoschHomeComProviderAddon = class extends BaseDeviceProvider {
2805
3131
  const stableId = this.generateStableId(type, config);
2806
3132
  const DeviceClass = this.deviceClasses[type];
2807
3133
  if (!DeviceClass) throw new Error(`homecom adopt: unsupported type ${type}`);
2808
- return { deviceId: (await devices.create(stableId, DeviceClass, config, null, {
3134
+ return { deviceId: (await devices.create(stableId, DeviceClass, config, parentDeviceId, {
2809
3135
  type,
2810
3136
  name,
2811
3137
  integrationId