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