@camstack/addon-provider-bosch 0.1.44 → 0.1.45

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-CXMUquuA.js");
3
- const require_account_integration_offline = require("../account-integration-offline-CFyuKmsG.js");
2
+ const require_bosch_instances = require("../bosch-instances-DdklwoKS.js");
3
+ const require_account_integration_offline = require("../account-integration-offline-BiXbzZsg.js");
4
4
  let node_crypto = require("node:crypto");
5
5
  //#region src/homecom/config.ts
6
6
  var HOMECOM_BRANDS = ["bosch", "buderus"];
@@ -267,36 +267,6 @@ function unsupportedDeviceTypeMessage(deviceType) {
267
267
  return new HomeComUnknownDeviceTypeError(deviceType ?? "(missing)").message;
268
268
  }
269
269
  //#endregion
270
- //#region src/homecom/homecom-oauth.ts
271
- /**
272
- * Public OAuth client of the HomeCom Easy app, signed in through SingleKey ID.
273
- *
274
- * A PKCE client has no secret. The values are the ones SingleKey accepts for this client id
275
- * today: the same authorize query the MIT-licensed `homecom_alt` client records
276
- * (`OAUTH_LOGIN_PARAMS` — parameters, not copied code) and that the Home Assistant integration
277
- * opens. `pointt.gateway.resource.dopair` is not a scope this client may request; SingleKey
278
- * answers `misconfigured-application` and never shows the login page. The scope it does accept
279
- * names `pointt.gateway.resource.dashapp`, and the Bosch app sends `style_id=tt_bsch`.
280
- */
281
- var HOMECOM_CLIENT_ID = "762162C0-FA2D-4540-AE66-6489F189FADC";
282
- var HOMECOM_AUTHORIZE_URL = "https://singlekey-id.com/auth/connect/authorize";
283
- var HOMECOM_TOKEN_URL = "https://singlekey-id.com/auth/connect/token";
284
- var HOMECOM_STYLE_ID = "tt_bsch";
285
- var HOMECOM_SCOPE = [
286
- "openid",
287
- "email",
288
- "profile",
289
- "offline_access",
290
- "pointt.gateway.claiming",
291
- "pointt.gateway.removal",
292
- "pointt.gateway.list",
293
- "pointt.gateway.users",
294
- "pointt.gateway.resource.dashapp",
295
- "pointt.castt.flow.token-exchange",
296
- "bacon",
297
- "hcc.tariff.read"
298
- ].join(" ");
299
- //#endregion
300
270
  //#region src/homecom/homecom-domain-mapping.ts
301
271
  var MODE_MAP = {
302
272
  Off: "off",
@@ -717,6 +687,352 @@ function isRecord$2(value) {
717
687
  return typeof value === "object" && value !== null && !Array.isArray(value);
718
688
  }
719
689
  //#endregion
690
+ //#region src/homecom/homecom-heat-pump.ts
691
+ /**
692
+ * Heat-pump resources of a hydronic gateway (D1026).
693
+ *
694
+ * UNVERIFIED: the paths below come from the layout of the public HomeCom Easy
695
+ * tree as the MIT `homecom_alt` library lists it, and were not checked against a
696
+ * live heat pump. They live in this one table so a wrong path is a one-line
697
+ * fix, and a resource the gateway does not answer reads as `null` (never 0),
698
+ * which the hero then simply does not draw.
699
+ */
700
+ var HEAT_PUMP_PATHS = {
701
+ system: {
702
+ silent: "/heatSources/hs1/silentMode",
703
+ away: "/system/awayMode",
704
+ additionalHeater: "/heatSources/hs1/additionalHeaterMode",
705
+ holiday: "/system/holidayMode"
706
+ },
707
+ readings: {
708
+ thermalW: "/heatSources/hs1/heatPumpPower",
709
+ electricalW: "/heatSources/hs1/electricalPower",
710
+ compressorPct: "/heatSources/hs1/compressorModulation",
711
+ supply: "/heatSources/hs1/supplyTemperature",
712
+ return: "/heatSources/hs1/returnTemperature",
713
+ outdoor: "/system/sensors/temperatures/outdoor_t1",
714
+ thermalKwh: "/recordings/heatSources/total/thermalEnergy",
715
+ electricalKwh: "/recordings/heatSources/total/electricalEnergy",
716
+ pressureBar: "/heatSources/hs1/waterPressure",
717
+ starts: "/heatSources/hs1/numberOfStarts"
718
+ },
719
+ circuit: {
720
+ operationMode: "operationMode",
721
+ heatCoolMode: "heatCoolMode",
722
+ summerWinter: "suWiSwitchMode",
723
+ comfort: "temperatureLevels/comfort2",
724
+ eco: "temperatureLevels/eco",
725
+ maxFlow: "maxSupplyTemperature",
726
+ current: "roomtemperature",
727
+ target: "currentRoomSetpoint"
728
+ },
729
+ dhw: {
730
+ operationMode: "operationMode",
731
+ level: "currentTemperatureLevel",
732
+ chargeDuration: "charge/duration",
733
+ charge: "charge",
734
+ current: "actualTemp",
735
+ target: "currentSetpoint"
736
+ }
737
+ };
738
+ /** A write the gateway would refuse, caught before it is sent. */
739
+ var HomeComHeatPumpRefusalError = class extends Error {
740
+ name = "HomeComHeatPumpRefusalError";
741
+ };
742
+ async function readHeatPump(input) {
743
+ const get = safeGet(input.get, input.onFailure);
744
+ const circuits = await Promise.all([...input.circuitIds.hc.map((id) => readHc(get, id)), ...input.circuitIds.dhw.map((id) => readDhwCircuit(get, id))]);
745
+ const r = HEAT_PUMP_PATHS.readings;
746
+ const s = HEAT_PUMP_PATHS.system;
747
+ const [silent, away, additionalHeater, holiday] = await Promise.all([
748
+ setting(get, s.silent),
749
+ setting(get, s.away),
750
+ setting(get, s.additionalHeater),
751
+ setting(get, s.holiday)
752
+ ]);
753
+ const [thermalW, electricalW, compressorPct, supply, ret, outdoor] = await Promise.all([
754
+ num(get, r.thermalW),
755
+ num(get, r.electricalW),
756
+ num(get, r.compressorPct),
757
+ num(get, r.supply),
758
+ num(get, r.return),
759
+ num(get, r.outdoor)
760
+ ]);
761
+ const [thermalKwh, electricalKwh, pressureBar, starts] = await Promise.all([
762
+ num(get, r.thermalKwh),
763
+ num(get, r.electricalKwh),
764
+ num(get, r.pressureBar),
765
+ num(get, r.starts)
766
+ ]);
767
+ return {
768
+ circuits,
769
+ silent,
770
+ away,
771
+ additionalHeater,
772
+ holiday,
773
+ thermalW,
774
+ electricalW,
775
+ compressorPct,
776
+ supply,
777
+ return: ret,
778
+ outdoor,
779
+ thermalKwh,
780
+ electricalKwh,
781
+ pressureBar,
782
+ starts
783
+ };
784
+ }
785
+ /** Any failure of a heat-pump read (HTTP 4xx/5xx, a throw) is "not answered", never an error. */
786
+ function safeGet(get, onFailure) {
787
+ return async (path) => {
788
+ try {
789
+ return await get(path);
790
+ } catch (error) {
791
+ onFailure?.(path, error);
792
+ return null;
793
+ }
794
+ };
795
+ }
796
+ /**
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.
800
+ */
801
+ function hasHeatSource(reading) {
802
+ return [
803
+ reading.silent,
804
+ reading.additionalHeater,
805
+ reading.thermalW,
806
+ reading.electricalW,
807
+ reading.compressorPct
808
+ ].some((v) => v !== null);
809
+ }
810
+ async function readHc(get, id) {
811
+ const base = `/heatingCircuits/${id}`;
812
+ const p = HEAT_PUMP_PATHS.circuit;
813
+ const [operationMode, heatCoolMode, summerWinter, comfort, eco, maxFlow, current, target] = await Promise.all([
814
+ setting(get, `${base}/${p.operationMode}`),
815
+ setting(get, `${base}/${p.heatCoolMode}`),
816
+ setting(get, `${base}/${p.summerWinter}`),
817
+ setting(get, `${base}/${p.comfort}`),
818
+ setting(get, `${base}/${p.eco}`),
819
+ setting(get, `${base}/${p.maxFlow}`),
820
+ num(get, `${base}/${p.current}`),
821
+ num(get, `${base}/${p.target}`)
822
+ ]);
823
+ return {
824
+ id,
825
+ kind: "hc",
826
+ currentTemp: current,
827
+ targetTemp: target,
828
+ operationMode,
829
+ heatCoolMode,
830
+ summerWinter,
831
+ comfort,
832
+ eco,
833
+ maxFlow,
834
+ level: null,
835
+ chargeDuration: null,
836
+ charge: null
837
+ };
838
+ }
839
+ async function readDhwCircuit(get, id) {
840
+ const base = `/dhwCircuits/${id}`;
841
+ const p = HEAT_PUMP_PATHS.dhw;
842
+ const [operationMode, level, chargeDuration, charge, current, target] = await Promise.all([
843
+ setting(get, `${base}/${p.operationMode}`),
844
+ setting(get, `${base}/${p.level}`),
845
+ setting(get, `${base}/${p.chargeDuration}`),
846
+ setting(get, `${base}/${p.charge}`),
847
+ num(get, `${base}/${p.current}`),
848
+ num(get, `${base}/${p.target}`)
849
+ ]);
850
+ return {
851
+ id,
852
+ kind: "dhw",
853
+ currentTemp: current,
854
+ targetTemp: target,
855
+ operationMode,
856
+ heatCoolMode: null,
857
+ summerWinter: null,
858
+ comfort: null,
859
+ eco: null,
860
+ maxFlow: null,
861
+ level,
862
+ chargeDuration,
863
+ charge
864
+ };
865
+ }
866
+ async function setting(get, path) {
867
+ const envelope = readEnvelope(await get(path));
868
+ return envelope === null ? null : settingOf(path, envelope);
869
+ }
870
+ async function num(get, path) {
871
+ const envelope = readEnvelope(await get(path));
872
+ return envelope !== null && typeof envelope.value === "number" && Number.isFinite(envelope.value) ? envelope.value : null;
873
+ }
874
+ function settingOf(path, envelope) {
875
+ return {
876
+ path,
877
+ value: envelope.value,
878
+ min: envelope.min,
879
+ max: envelope.max,
880
+ allowed: envelope.allowed
881
+ };
882
+ }
883
+ function heatPumpStatusFrom(reading, now) {
884
+ return {
885
+ circuits: reading.circuits.map(circuitOf),
886
+ system: {
887
+ silent: optionOf(reading.silent),
888
+ away: optionOf(reading.away),
889
+ additionalHeater: optionOf(reading.additionalHeater),
890
+ holiday: optionOf(reading.holiday)
891
+ },
892
+ power: {
893
+ thermalW: reading.thermalW,
894
+ electricalW: reading.electricalW,
895
+ compressorPct: reading.compressorPct
896
+ },
897
+ temperatures: {
898
+ supply: reading.supply,
899
+ return: reading.return,
900
+ outdoor: reading.outdoor
901
+ },
902
+ energy: {
903
+ thermalKwh: reading.thermalKwh,
904
+ electricalKwh: reading.electricalKwh
905
+ },
906
+ waterPressureBar: reading.pressureBar,
907
+ starts: reading.starts,
908
+ lastFetchedAt: now
909
+ };
910
+ }
911
+ function circuitOf(c) {
912
+ const kind = kindOf(c);
913
+ const mode = textOf$1(c.operationMode);
914
+ return {
915
+ id: c.id,
916
+ kind,
917
+ label: labelOf(c),
918
+ icon: kind === "dhw" ? "droplets" : kind === "cooling" ? "snowflake" : "flame",
919
+ currentTemp: c.currentTemp,
920
+ targetTemp: c.targetTemp,
921
+ mode,
922
+ active: null,
923
+ operationMode: optionOf(c.operationMode),
924
+ heatCoolMode: optionOf(c.heatCoolMode),
925
+ summerWinter: optionOf(c.summerWinter),
926
+ comfortSetpoint: rangeOf(c.comfort),
927
+ ecoSetpoint: rangeOf(c.eco),
928
+ maxFlowTemp: rangeOf(c.maxFlow),
929
+ temperatureLevel: optionOf(c.level),
930
+ chargeDuration: rangeOf(c.chargeDuration),
931
+ extraHotWater: c.charge === null ? null : textOf$1(c.charge) === "start"
932
+ };
933
+ }
934
+ function kindOf(c) {
935
+ if (c.kind === "dhw") return "dhw";
936
+ return textOf$1(c.heatCoolMode) === "cooling" ? "cooling" : "heating";
937
+ }
938
+ function labelOf(c) {
939
+ const n = c.id.replace(/^\D+/, "");
940
+ if (c.kind === "dhw") return n === "1" || n === "" ? "Hot water" : `Hot water ${n}`;
941
+ return n === "" ? "Circuit" : `Circuit ${n}`;
942
+ }
943
+ function textOf$1(s) {
944
+ return s !== null && typeof s.value === "string" && s.value.length > 0 ? s.value : null;
945
+ }
946
+ function optionOf(s) {
947
+ if (s === null) return null;
948
+ return {
949
+ value: textOf$1(s),
950
+ allowed: [...s.allowed]
951
+ };
952
+ }
953
+ function rangeOf(s) {
954
+ if (s === null) return null;
955
+ return {
956
+ value: typeof s.value === "number" && Number.isFinite(s.value) ? s.value : null,
957
+ min: s.min,
958
+ max: s.max,
959
+ step: null
960
+ };
961
+ }
962
+ function requireCircuit(reading, circuitId, want) {
963
+ const found = reading.circuits.find((c) => c.id === circuitId && c.kind === want);
964
+ if (found === void 0) throw new HomeComHeatPumpRefusalError(`HomeCom heat pump has no ${want} circuit ${circuitId}`);
965
+ return found;
966
+ }
967
+ function optionWrite(s, what, mode) {
968
+ if (s === null) throw new HomeComHeatPumpRefusalError(`HomeCom heat pump: ${what} is not available`);
969
+ if (s.allowed.length === 0) throw new HomeComHeatPumpRefusalError(`HomeCom heat pump: ${what} is read-only on this gateway`);
970
+ if (!s.allowed.includes(mode)) throw new HomeComHeatPumpRefusalError(`HomeCom heat pump: "${mode}" is not an accepted value of ${what} (${s.allowed.join(", ")})`);
971
+ return {
972
+ path: s.path,
973
+ value: mode
974
+ };
975
+ }
976
+ function rangeWrite(s, what, value) {
977
+ if (s === null) throw new HomeComHeatPumpRefusalError(`HomeCom heat pump: ${what} is not available`);
978
+ if (!Number.isFinite(value)) throw new HomeComHeatPumpRefusalError(`HomeCom heat pump: ${what} needs a finite number`);
979
+ if (s.min === null || s.max === null) throw new HomeComHeatPumpRefusalError(`HomeCom heat pump: ${what} has no limits from the gateway`);
980
+ if (value < s.min || value > s.max) throw new HomeComHeatPumpRefusalError(`HomeCom heat pump: ${value} is outside ${what} limits ${s.min}..${s.max}`);
981
+ return {
982
+ path: s.path,
983
+ value
984
+ };
985
+ }
986
+ function systemModeWrite(reading, which, mode) {
987
+ return optionWrite(reading[which], `${which} mode`, mode);
988
+ }
989
+ function circuitOptionWrite(reading, circuitId, which, mode) {
990
+ return optionWrite(requireCircuit(reading, circuitId, "hc")[which], `${circuitId} ${which}`, mode);
991
+ }
992
+ function dhwOptionWrite(reading, circuitId, which, mode) {
993
+ return optionWrite(requireCircuit(reading, circuitId, "dhw")[which], `${circuitId} ${which}`, mode);
994
+ }
995
+ function circuitRangeWrite(reading, circuitId, which, value) {
996
+ return rangeWrite(requireCircuit(reading, circuitId, "hc")[which], `${circuitId} ${which}`, value);
997
+ }
998
+ function dhwChargeDurationWrite(reading, circuitId, minutes) {
999
+ return rangeWrite(requireCircuit(reading, circuitId, "dhw").chargeDuration, `${circuitId} charge duration`, minutes);
1000
+ }
1001
+ function extraHotWaterWrite(reading, circuitId) {
1002
+ const charge = requireCircuit(reading, circuitId, "dhw").charge;
1003
+ return optionWrite(charge, `${circuitId} extra hot water`, "start");
1004
+ }
1005
+ //#endregion
1006
+ //#region src/homecom/homecom-oauth.ts
1007
+ /**
1008
+ * Public OAuth client of the HomeCom Easy app, signed in through SingleKey ID.
1009
+ *
1010
+ * A PKCE client has no secret. The values are the ones SingleKey accepts for this client id
1011
+ * today: the same authorize query the MIT-licensed `homecom_alt` client records
1012
+ * (`OAUTH_LOGIN_PARAMS` — parameters, not copied code) and that the Home Assistant integration
1013
+ * opens. `pointt.gateway.resource.dopair` is not a scope this client may request; SingleKey
1014
+ * answers `misconfigured-application` and never shows the login page. The scope it does accept
1015
+ * names `pointt.gateway.resource.dashapp`, and the Bosch app sends `style_id=tt_bsch`.
1016
+ */
1017
+ var HOMECOM_CLIENT_ID = "762162C0-FA2D-4540-AE66-6489F189FADC";
1018
+ var HOMECOM_AUTHORIZE_URL = "https://singlekey-id.com/auth/connect/authorize";
1019
+ var HOMECOM_TOKEN_URL = "https://singlekey-id.com/auth/connect/token";
1020
+ var HOMECOM_STYLE_ID = "tt_bsch";
1021
+ var HOMECOM_SCOPE = [
1022
+ "openid",
1023
+ "email",
1024
+ "profile",
1025
+ "offline_access",
1026
+ "pointt.gateway.claiming",
1027
+ "pointt.gateway.removal",
1028
+ "pointt.gateway.list",
1029
+ "pointt.gateway.users",
1030
+ "pointt.gateway.resource.dashapp",
1031
+ "pointt.castt.flow.token-exchange",
1032
+ "bacon",
1033
+ "hcc.tariff.read"
1034
+ ].join(" ");
1035
+ //#endregion
720
1036
  //#region src/homecom/homecom-api-client.ts
721
1037
  var HomeComAuthError = class extends Error {
722
1038
  name = "HomeComAuthError";
@@ -930,15 +1246,43 @@ function createHomeComApiClient(config, gatewayId, deps = {}) {
930
1246
  }
931
1247
  async function fetchSystem() {
932
1248
  const resolved = await resolveDeviceType();
933
- const snapshot = await readPointtSnapshot({
1249
+ const withHeatPump = await attachHeatPump(await readPointtSnapshot({
934
1250
  deviceType: resolved.deviceType,
935
1251
  family: resolved.family,
936
1252
  gatewayId,
937
1253
  get: getResource,
938
1254
  logDrop
1255
+ }));
1256
+ last = withHeatPump;
1257
+ return withHeatPump;
1258
+ }
1259
+ async function attachHeatPump(snapshot) {
1260
+ if (snapshot.family !== "hydronic" || snapshot.deviceType === "rrc2") return snapshot;
1261
+ const dhwId = snapshot.dhw?.circuitId;
1262
+ const heatPump = await readHeatPump({
1263
+ get: getResource,
1264
+ onFailure: logHeatPumpFailure,
1265
+ circuitIds: {
1266
+ hc: snapshot.circuits.map((circuit) => circuit.id),
1267
+ dhw: dhwId === void 0 ? [] : [dhwId]
1268
+ }
1269
+ });
1270
+ return hasHeatSource(heatPump) ? {
1271
+ ...snapshot,
1272
+ heatPump
1273
+ } : snapshot;
1274
+ }
1275
+ function logHeatPumpFailure(path, error) {
1276
+ const key = `heat-pump-read:${path}`;
1277
+ if (dropped.has(key)) return;
1278
+ dropped.add(key);
1279
+ logger?.warn("HomeCom heat-pump resource could not be read; it is skipped", {
1280
+ tags: { gatewayId },
1281
+ meta: {
1282
+ path,
1283
+ error: error instanceof Error ? error.message : String(error)
1284
+ }
939
1285
  });
940
- last = snapshot;
941
- return snapshot;
942
1286
  }
943
1287
  async function probePath(path) {
944
1288
  const res = await authed(homeComResourceUrl(gatewayId, path), {
@@ -992,6 +1336,9 @@ function createHomeComApiClient(config, gatewayId, deps = {}) {
992
1336
  const snapshot = await requireSnapshot();
993
1337
  if (snapshot.family !== "rac") logDrop("swing is not a resource on this device type");
994
1338
  await putWrite(swingWrite(snapshot, "horizontal", on));
1339
+ },
1340
+ async writeHeatPump(writes) {
1341
+ for (const write of writes) await putWrite(write);
995
1342
  }
996
1343
  };
997
1344
  }
@@ -1256,8 +1603,7 @@ function buildHomeComGatewayPersister(deps) {
1256
1603
  function buildHomeComCandidates(input) {
1257
1604
  const out = [];
1258
1605
  for (const dev of input.devices) {
1259
- const type = dev.kind === "heating-circuit" ? require_bosch_instances.DeviceType.Climate : require_bosch_instances.DeviceType.WaterHeater;
1260
- const caps = dev.kind === "heating-circuit" ? ["climate-control"] : ["water-heater"];
1606
+ const { type, caps } = shapeOf(dev.kind);
1261
1607
  const adoptedId = input.adopted.get(dev.nativeId) ?? null;
1262
1608
  out.push({
1263
1609
  childNativeId: dev.nativeId,
@@ -1272,6 +1618,22 @@ function buildHomeComCandidates(input) {
1272
1618
  }
1273
1619
  return out;
1274
1620
  }
1621
+ function shapeOf(kind) {
1622
+ switch (kind) {
1623
+ case "heating-circuit": return {
1624
+ type: require_bosch_instances.DeviceType.Climate,
1625
+ caps: ["climate-control"]
1626
+ };
1627
+ case "dhw": return {
1628
+ type: require_bosch_instances.DeviceType.WaterHeater,
1629
+ caps: ["water-heater"]
1630
+ };
1631
+ case "heat-pump": return {
1632
+ type: require_bosch_instances.DeviceType.HeatPump,
1633
+ caps: ["heat-pump-status", "heat-pump-control"]
1634
+ };
1635
+ }
1636
+ }
1275
1637
  //#endregion
1276
1638
  //#region src/homecom/homecom-adoption-provider.ts
1277
1639
  var DEVICES_FILTER = {
@@ -1456,6 +1818,11 @@ function buildDiscoverables(snapshot) {
1456
1818
  name: "Domestic hot water",
1457
1819
  kind: "dhw"
1458
1820
  });
1821
+ if (snapshot.heatPump !== void 0) out.push({
1822
+ nativeId: "hp:0",
1823
+ name: "Heat pump",
1824
+ kind: "heat-pump"
1825
+ });
1459
1826
  return out;
1460
1827
  }
1461
1828
  var homeComConnections = new HomeComConnectionResolver();
@@ -1942,7 +2309,7 @@ var homeComClimateSchema = require_bosch_instances.object({
1942
2309
  integrationId: require_bosch_instances.string().optional()
1943
2310
  });
1944
2311
  var CLIMATE_CAP = "climate-control";
1945
- var COLD_START$1 = {
2312
+ var COLD_START$2 = {
1946
2313
  mode: "off",
1947
2314
  availableModes: [],
1948
2315
  fanMode: "",
@@ -1977,14 +2344,14 @@ var HomeComClimateDevice = class extends require_bosch_instances.BaseDevice {
1977
2344
  async onActivate() {
1978
2345
  await super.onActivate();
1979
2346
  this.registerClimateCap();
1980
- this.runtimeState.setCapState(CLIMATE_CAP, COLD_START$1);
2347
+ this.runtimeState.setCapState(CLIMATE_CAP, COLD_START$2);
1981
2348
  this.refreshFromSnapshot();
1982
2349
  }
1983
2350
  refreshFromSnapshot() {
1984
2351
  const circuit = homeComConnections.getSnapshot(this.#brokerId)?.circuits.find((c) => c.id === this.#circuitId);
1985
2352
  if (!circuit) {
1986
2353
  this.runtimeState.setCapState(CLIMATE_CAP, {
1987
- ...COLD_START$1,
2354
+ ...COLD_START$2,
1988
2355
  lastFetchedAt: Date.now()
1989
2356
  });
1990
2357
  return;
@@ -2000,7 +2367,7 @@ var HomeComClimateDevice = class extends require_bosch_instances.BaseDevice {
2000
2367
  this.ctx.registerNativeCap(require_bosch_instances.climateControlCapability, {
2001
2368
  getStatus: async () => {
2002
2369
  this.refreshFromSnapshot();
2003
- return this.runtimeState.getCapState(CLIMATE_CAP) ?? COLD_START$1;
2370
+ return this.runtimeState.getCapState(CLIMATE_CAP) ?? COLD_START$2;
2004
2371
  },
2005
2372
  setMode: async ({ mode }) => {
2006
2373
  const apiMode = hvacModeToHomeCom(mode);
@@ -2052,7 +2419,7 @@ var homeComWaterHeaterSchema = require_bosch_instances.object({
2052
2419
  integrationId: require_bosch_instances.string().optional()
2053
2420
  });
2054
2421
  var WH_CAP = "water-heater";
2055
- var COLD_START = {
2422
+ var COLD_START$1 = {
2056
2423
  currentTemp: null,
2057
2424
  targetTemp: null,
2058
2425
  operationMode: null,
@@ -2078,14 +2445,14 @@ var HomeComWaterHeaterDevice = class extends require_bosch_instances.BaseDevice
2078
2445
  async onActivate() {
2079
2446
  await super.onActivate();
2080
2447
  this.registerWaterHeaterCap();
2081
- this.runtimeState.setCapState(WH_CAP, COLD_START);
2448
+ this.runtimeState.setCapState(WH_CAP, COLD_START$1);
2082
2449
  this.refreshFromSnapshot();
2083
2450
  }
2084
2451
  refreshFromSnapshot() {
2085
2452
  const dhw = homeComConnections.getSnapshot(this.#brokerId)?.dhw;
2086
2453
  if (!dhw) {
2087
2454
  this.runtimeState.setCapState(WH_CAP, {
2088
- ...COLD_START,
2455
+ ...COLD_START$1,
2089
2456
  lastChangedAt: Date.now()
2090
2457
  });
2091
2458
  return;
@@ -2101,7 +2468,7 @@ var HomeComWaterHeaterDevice = class extends require_bosch_instances.BaseDevice
2101
2468
  this.ctx.registerNativeCap(require_bosch_instances.waterHeaterCapability, {
2102
2469
  getStatus: async () => {
2103
2470
  this.refreshFromSnapshot();
2104
- return this.runtimeState.getCapState(WH_CAP) ?? COLD_START;
2471
+ return this.runtimeState.getCapState(WH_CAP) ?? COLD_START$1;
2105
2472
  },
2106
2473
  setTargetTemp: async ({ temp }) => {
2107
2474
  await this.requireClient().setDhwTarget(temp);
@@ -2118,13 +2485,157 @@ var HomeComWaterHeaterDevice = class extends require_bosch_instances.BaseDevice
2118
2485
  }
2119
2486
  };
2120
2487
  //#endregion
2488
+ //#region src/homecom/devices/homecom-heat-pump-device.ts
2489
+ var homeComHeatPumpSchema = require_bosch_instances.object({
2490
+ nativeId: require_bosch_instances.string().min(1),
2491
+ brokerId: require_bosch_instances.string().min(1),
2492
+ system: require_bosch_instances.literal("homecom").optional(),
2493
+ integrationId: require_bosch_instances.string().optional()
2494
+ });
2495
+ var STATUS_CAP = "heat-pump-status";
2496
+ var COLD_START = {
2497
+ circuits: [],
2498
+ system: {
2499
+ silent: null,
2500
+ away: null,
2501
+ additionalHeater: null,
2502
+ holiday: null
2503
+ },
2504
+ power: {
2505
+ thermalW: null,
2506
+ electricalW: null,
2507
+ compressorPct: null
2508
+ },
2509
+ temperatures: {
2510
+ supply: null,
2511
+ return: null,
2512
+ outdoor: null
2513
+ },
2514
+ energy: {
2515
+ thermalKwh: null,
2516
+ electricalKwh: null
2517
+ },
2518
+ waterPressureBar: null,
2519
+ starts: null,
2520
+ lastFetchedAt: 0
2521
+ };
2522
+ /**
2523
+ * A heat pump on a hydronic HomeCom gateway (D1026). The slice and every limit
2524
+ * come from the gateway reading the integration poll already holds; a write is
2525
+ * checked against those limits first and the reading is refreshed after it.
2526
+ */
2527
+ var HomeComHeatPumpDevice = class extends require_bosch_instances.BaseDevice {
2528
+ features = [];
2529
+ #brokerId;
2530
+ /** True while the gateway has no heat-pump reading, so the log marks the transitions only. */
2531
+ #noReading = false;
2532
+ constructor(ctx) {
2533
+ const persisted = homeComHeatPumpSchema.parse(ctx.persistedConfig ?? {});
2534
+ super(ctx, homeComHeatPumpSchema, { type: ctx.deviceMeta.type });
2535
+ this.#brokerId = persisted.brokerId;
2536
+ this.online = true;
2537
+ this.updateSourceInfo({
2538
+ id: persisted.nativeId,
2539
+ system: "homecom"
2540
+ });
2541
+ }
2542
+ async onActivate() {
2543
+ await super.onActivate();
2544
+ this.registerStatusCap();
2545
+ this.registerControlCap();
2546
+ this.runtimeState.setCapState(STATUS_CAP, COLD_START);
2547
+ this.refreshFromSnapshot();
2548
+ }
2549
+ reading() {
2550
+ return homeComConnections.getSnapshot(this.#brokerId)?.heatPump ?? null;
2551
+ }
2552
+ refreshFromSnapshot() {
2553
+ const reading = this.reading();
2554
+ if (reading === null) {
2555
+ if (!this.#noReading) {
2556
+ this.#noReading = true;
2557
+ this.ctx.logger.warn("HomeCom heat pump: the gateway returned no heat-pump reading", { tags: { deviceId: this.ctx.id } });
2558
+ }
2559
+ this.runtimeState.setCapState(STATUS_CAP, {
2560
+ ...COLD_START,
2561
+ lastFetchedAt: Date.now()
2562
+ });
2563
+ return;
2564
+ }
2565
+ if (this.#noReading) {
2566
+ this.#noReading = false;
2567
+ this.ctx.logger.info("HomeCom heat pump: the gateway answers with a heat-pump reading again", { tags: { deviceId: this.ctx.id } });
2568
+ }
2569
+ this.runtimeState.setCapState(STATUS_CAP, heatPumpStatusFrom(reading, Date.now()));
2570
+ }
2571
+ requireClient() {
2572
+ const client = homeComConnections.getClient(this.#brokerId);
2573
+ if (!client) throw new Error("HomeCom client unavailable (heat pump)");
2574
+ return client;
2575
+ }
2576
+ requireReading() {
2577
+ const reading = this.reading();
2578
+ if (reading === null) throw new Error("HomeCom heat pump: no reading from the gateway yet");
2579
+ return reading;
2580
+ }
2581
+ /** Validate against the reading, send, then re-read so the slice shows what the gateway holds. */
2582
+ async apply(build) {
2583
+ const write = build(this.requireReading());
2584
+ const client = this.requireClient();
2585
+ await client.writeHeatPump([write]);
2586
+ this.ctx.logger.info("HomeCom heat pump: value written", {
2587
+ tags: { deviceId: this.ctx.id },
2588
+ meta: { path: write.path }
2589
+ });
2590
+ try {
2591
+ const snapshot = await client.fetchSystem();
2592
+ homeComConnections.updateSnapshot(this.#brokerId, snapshot);
2593
+ } catch (error) {
2594
+ this.ctx.logger.warn("HomeCom heat pump: value written, the refresh failed", {
2595
+ tags: { deviceId: this.ctx.id },
2596
+ meta: {
2597
+ path: write.path,
2598
+ error: error instanceof Error ? error.message : String(error)
2599
+ }
2600
+ });
2601
+ return;
2602
+ }
2603
+ this.refreshFromSnapshot();
2604
+ }
2605
+ registerStatusCap() {
2606
+ this.ctx.registerNativeCap(require_bosch_instances.heatPumpStatusCapability, { getStatus: async () => {
2607
+ this.refreshFromSnapshot();
2608
+ return this.runtimeState.getCapState(STATUS_CAP) ?? COLD_START;
2609
+ } });
2610
+ }
2611
+ registerControlCap() {
2612
+ this.ctx.registerNativeCap(require_bosch_instances.heatPumpControlCapability, {
2613
+ setSilentMode: ({ mode }) => this.apply((r) => systemModeWrite(r, "silent", mode)),
2614
+ setAwayMode: ({ mode }) => this.apply((r) => systemModeWrite(r, "away", mode)),
2615
+ setAdditionalHeaterMode: ({ mode }) => this.apply((r) => systemModeWrite(r, "additionalHeater", mode)),
2616
+ setHolidayMode: ({ mode }) => this.apply((r) => systemModeWrite(r, "holiday", mode)),
2617
+ setCircuitOperationMode: ({ circuitId, mode }) => this.apply((r) => circuitOptionWrite(r, circuitId, "operationMode", mode)),
2618
+ setCircuitHeatCoolMode: ({ circuitId, mode }) => this.apply((r) => circuitOptionWrite(r, circuitId, "heatCoolMode", mode)),
2619
+ setCircuitSummerWinter: ({ circuitId, mode }) => this.apply((r) => circuitOptionWrite(r, circuitId, "summerWinter", mode)),
2620
+ setCircuitComfortSetpoint: ({ circuitId, temp }) => this.apply((r) => circuitRangeWrite(r, circuitId, "comfort", temp)),
2621
+ setCircuitEcoSetpoint: ({ circuitId, temp }) => this.apply((r) => circuitRangeWrite(r, circuitId, "eco", temp)),
2622
+ setCircuitMaxFlowTemp: ({ circuitId, temp }) => this.apply((r) => circuitRangeWrite(r, circuitId, "maxFlow", temp)),
2623
+ setDhwOperationMode: ({ circuitId, mode }) => this.apply((r) => dhwOptionWrite(r, circuitId, "operationMode", mode)),
2624
+ setDhwTemperatureLevel: ({ circuitId, level }) => this.apply((r) => dhwOptionWrite(r, circuitId, "level", level)),
2625
+ setDhwChargeDuration: ({ circuitId, minutes }) => this.apply((r) => dhwChargeDurationWrite(r, circuitId, minutes)),
2626
+ startExtraHotWater: ({ circuitId }) => this.apply((r) => extraHotWaterWrite(r, circuitId))
2627
+ });
2628
+ }
2629
+ };
2630
+ //#endregion
2121
2631
  //#region src/homecom/addon.ts
2122
2632
  var BoschHomeComProviderAddon = class extends require_bosch_instances.BaseDeviceProvider {
2123
2633
  addonId = "bosch-homecom";
2124
2634
  providerName = "Bosch HomeCom";
2125
2635
  deviceClasses = {
2126
2636
  [require_bosch_instances.DeviceType.Climate]: HomeComClimateDevice,
2127
- [require_bosch_instances.DeviceType.WaterHeater]: HomeComWaterHeaterDevice
2637
+ [require_bosch_instances.DeviceType.WaterHeater]: HomeComWaterHeaterDevice,
2638
+ [require_bosch_instances.DeviceType.HeatPump]: HomeComHeatPumpDevice
2128
2639
  };
2129
2640
  clients = null;
2130
2641
  instances = null;