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