@camstack/addon-provider-bosch 0.1.44 → 0.1.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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-DkUo6BfT.mjs";
2
+ import { a as loginFailureForAttempt, i as createSignInAttemptStore, n as cascadeRemoveDevicesForIntegration, o as parseOAuthCallback, r as buildSignInGuideFields, s as HOMECOM_REDIRECT_URI, t as setIntegrationDevicesOnline } from "../account-integration-offline-BGH1q3tj.mjs";
3
3
  import { createHash, randomBytes } from "node:crypto";
4
4
  //#region src/homecom/config.ts
5
5
  var HOMECOM_BRANDS = ["bosch", "buderus"];
@@ -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",
@@ -478,7 +448,7 @@ async function readHydronic(input) {
478
448
  const heating = await input.get("/heatingCircuits");
479
449
  const dhwBody = await input.get("/dhwCircuits");
480
450
  const circuits = [];
481
- 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));
482
452
  const dhwIds = referenceKeys(dhwBody, "dhw");
483
453
  const first = dhwIds[0];
484
454
  const dhw = first === void 0 ? null : await readDhw(input.get, first);
@@ -538,7 +508,7 @@ async function readRac(input) {
538
508
  setpointResource: "/airConditioning/temperatureSetpoint"
539
509
  }], null);
540
510
  }
541
- async function readCircuit(get, id) {
511
+ async function readCircuit(get, id, givenName) {
542
512
  const base = `/heatingCircuits/${id}`;
543
513
  const loaded = await loadEnvelopes(get, [
544
514
  `${base}/operationMode`,
@@ -561,7 +531,7 @@ async function readCircuit(get, id) {
561
531
  const humidity = numberOf(loaded.get(`${base}/actualHumidity`) ?? null);
562
532
  return {
563
533
  id,
564
- name: id,
534
+ name: heatingCircuitName(id, givenName),
565
535
  operationMode: stringOf(operation) ?? "off",
566
536
  setpoint: cooling ? numberOf(coolingSet) ?? numberOf(current) ?? numberOf(manual) : numberOf(current) ?? numberOf(manual),
567
537
  currentRoomTemp: numberOf(loaded.get(`${base}/roomtemperature`) ?? null),
@@ -663,18 +633,37 @@ function pointtMode(mode) {
663
633
  default: return mode;
664
634
  }
665
635
  }
666
- 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) {
667
643
  if (!isRecord$2(body) || !Array.isArray(body["references"])) return [];
668
644
  const pattern = prefix === "hc" ? /^hc\d+$/ : /^dhw\d+$/;
669
645
  const out = [];
670
646
  for (const ref of body["references"]) {
671
647
  if (!isRecord$2(ref) || typeof ref["id"] !== "string") continue;
672
648
  const tail = ref["id"].split("/").filter((part) => part.length > 0).at(-1);
673
- if (tail === void 0 || !pattern.test(tail) || out.includes(tail)) continue;
674
- 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
+ });
675
654
  }
676
655
  return out;
677
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
+ }
678
667
  function indexStandard(body) {
679
668
  const map = /* @__PURE__ */ new Map();
680
669
  if (!isRecord$2(body) || !Array.isArray(body["references"])) return map;
@@ -716,6 +705,527 @@ function isRecord$2(value) {
716
705
  return typeof value === "object" && value !== null && !Array.isArray(value);
717
706
  }
718
707
  //#endregion
708
+ //#region src/homecom/homecom-heat-pump.ts
709
+ /**
710
+ * Heat-pump resources of a hydronic gateway (D1026).
711
+ *
712
+ * UNVERIFIED: the paths below come from the layout of the public HomeCom Easy
713
+ * tree as the MIT `homecom_alt` library lists it, and were not checked against a
714
+ * live heat pump. They live in this one table so a wrong path is a one-line
715
+ * fix, and a resource the gateway does not answer reads as `null` (never 0),
716
+ * which the hero then simply does not draw.
717
+ */
718
+ var HEAT_PUMP_PATHS = {
719
+ system: {
720
+ silent: "/heatSources/hs1/silentMode",
721
+ away: "/system/awayMode",
722
+ additionalHeater: "/heatSources/hs1/additionalHeaterMode",
723
+ holiday: "/system/holidayMode"
724
+ },
725
+ readings: {
726
+ thermalW: "/heatSources/hs1/heatPumpPower",
727
+ electricalW: "/heatSources/hs1/electricalPower",
728
+ compressorPct: "/heatSources/hs1/compressorModulation",
729
+ supply: "/heatSources/hs1/supplyTemperature",
730
+ return: "/heatSources/hs1/returnTemperature",
731
+ outdoor: "/system/sensors/temperatures/outdoor_t1",
732
+ thermalKwh: "/recordings/heatSources/total/thermalEnergy",
733
+ electricalKwh: "/recordings/heatSources/total/electricalEnergy",
734
+ pressureBar: "/heatSources/hs1/waterPressure",
735
+ starts: "/heatSources/hs1/numberOfStarts"
736
+ },
737
+ circuit: {
738
+ operationMode: "operationMode",
739
+ heatCoolMode: "heatCoolMode",
740
+ summerWinter: "suWiSwitchMode",
741
+ comfort: "temperatureLevels/comfort2",
742
+ eco: "temperatureLevels/eco",
743
+ maxFlow: "maxSupplyTemperature",
744
+ current: "roomtemperature",
745
+ target: "currentRoomSetpoint"
746
+ },
747
+ dhw: {
748
+ operationMode: "operationMode",
749
+ level: "currentTemperatureLevel",
750
+ chargeDuration: "charge/duration",
751
+ charge: "charge",
752
+ current: "actualTemp",
753
+ target: "currentSetpoint"
754
+ }
755
+ };
756
+ /** A write the gateway would refuse, caught before it is sent. */
757
+ var HomeComHeatPumpRefusalError = class extends Error {
758
+ name = "HomeComHeatPumpRefusalError";
759
+ };
760
+ async function readHeatPump(input) {
761
+ const get = safeGet(input.get, input.onFailure);
762
+ const circuits = await Promise.all([...input.circuitIds.hc.map((id) => readHc(get, id)), ...input.circuitIds.dhw.map((id) => readDhwCircuit(get, id))]);
763
+ const r = HEAT_PUMP_PATHS.readings;
764
+ const s = HEAT_PUMP_PATHS.system;
765
+ const [silent, away, additionalHeater, holiday] = await Promise.all([
766
+ setting(get, s.silent),
767
+ setting(get, s.away),
768
+ setting(get, s.additionalHeater),
769
+ setting(get, s.holiday)
770
+ ]);
771
+ const [thermalW, electricalW, compressorPct, supply, ret, outdoor] = await Promise.all([
772
+ num(get, r.thermalW),
773
+ num(get, r.electricalW),
774
+ num(get, r.compressorPct),
775
+ num(get, r.supply),
776
+ num(get, r.return),
777
+ num(get, r.outdoor)
778
+ ]);
779
+ const [thermalKwh, electricalKwh, pressureBar, starts] = await Promise.all([
780
+ num(get, r.thermalKwh),
781
+ num(get, r.electricalKwh),
782
+ num(get, r.pressureBar),
783
+ num(get, r.starts)
784
+ ]);
785
+ return {
786
+ circuits,
787
+ silent,
788
+ away,
789
+ additionalHeater,
790
+ holiday,
791
+ thermalW,
792
+ electricalW,
793
+ compressorPct,
794
+ supply,
795
+ return: ret,
796
+ outdoor,
797
+ thermalKwh,
798
+ electricalKwh,
799
+ pressureBar,
800
+ starts
801
+ };
802
+ }
803
+ /** Any failure of a heat-pump read (HTTP 4xx/5xx, a throw) is "not answered", never an error. */
804
+ function safeGet(get, onFailure) {
805
+ return async (path) => {
806
+ try {
807
+ return await get(path);
808
+ } catch (error) {
809
+ onFailure?.(path, error);
810
+ return null;
811
+ }
812
+ };
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
+ ];
825
+ /**
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.
836
+ */
837
+ function hasSystemDevice(reading) {
838
+ if (SYSTEM_MODE_KEYS.some((key) => reading[key] !== null)) return true;
839
+ return HEAT_PUMP_ONLY_READINGS.some((key) => reading[key] !== null);
840
+ }
841
+ /** Which heat-pump paths this gateway answered. One log per gateway corrects the path table. */
842
+ function heatPumpPathCensus(reading) {
843
+ const rows = [
844
+ ...systemPathRows(reading),
845
+ ...readingPathRows(reading),
846
+ ...reading.circuits.flatMap(circuitPathRows)
847
+ ];
848
+ const answered = [];
849
+ const unanswered = [];
850
+ for (const row of rows) if (row.present) answered.push(row.path);
851
+ else unanswered.push(row.path);
852
+ return {
853
+ answered,
854
+ unanswered
855
+ };
856
+ }
857
+ function systemPathRows(reading) {
858
+ const s = HEAT_PUMP_PATHS.system;
859
+ return [
860
+ {
861
+ path: s.silent,
862
+ present: reading.silent !== null
863
+ },
864
+ {
865
+ path: s.away,
866
+ present: reading.away !== null
867
+ },
868
+ {
869
+ path: s.additionalHeater,
870
+ present: reading.additionalHeater !== null
871
+ },
872
+ {
873
+ path: s.holiday,
874
+ present: reading.holiday !== null
875
+ }
876
+ ];
877
+ }
878
+ function readingPathRows(reading) {
879
+ const r = HEAT_PUMP_PATHS.readings;
880
+ return [
881
+ {
882
+ path: r.thermalW,
883
+ present: reading.thermalW !== null
884
+ },
885
+ {
886
+ path: r.electricalW,
887
+ present: reading.electricalW !== null
888
+ },
889
+ {
890
+ path: r.compressorPct,
891
+ present: reading.compressorPct !== null
892
+ },
893
+ {
894
+ path: r.supply,
895
+ present: reading.supply !== null
896
+ },
897
+ {
898
+ path: r.return,
899
+ present: reading.return !== null
900
+ },
901
+ {
902
+ path: r.outdoor,
903
+ present: reading.outdoor !== null
904
+ },
905
+ {
906
+ path: r.thermalKwh,
907
+ present: reading.thermalKwh !== null
908
+ },
909
+ {
910
+ path: r.electricalKwh,
911
+ present: reading.electricalKwh !== null
912
+ },
913
+ {
914
+ path: r.pressureBar,
915
+ present: reading.pressureBar !== null
916
+ },
917
+ {
918
+ path: r.starts,
919
+ present: reading.starts !== null
920
+ }
921
+ ];
922
+ }
923
+ function circuitPathRows(circuit) {
924
+ if (circuit.kind === "dhw") return dhwPathRows(circuit);
925
+ return hcPathRows(circuit);
926
+ }
927
+ function hcPathRows(circuit) {
928
+ const base = `/heatingCircuits/${circuit.id}`;
929
+ const p = HEAT_PUMP_PATHS.circuit;
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
+ ];
994
+ }
995
+ async function readHc(get, id) {
996
+ const base = `/heatingCircuits/${id}`;
997
+ const p = HEAT_PUMP_PATHS.circuit;
998
+ const [operationMode, heatCoolMode, summerWinter, comfort, eco, maxFlow, current, target] = await Promise.all([
999
+ setting(get, `${base}/${p.operationMode}`),
1000
+ setting(get, `${base}/${p.heatCoolMode}`),
1001
+ setting(get, `${base}/${p.summerWinter}`),
1002
+ setting(get, `${base}/${p.comfort}`),
1003
+ setting(get, `${base}/${p.eco}`),
1004
+ setting(get, `${base}/${p.maxFlow}`),
1005
+ num(get, `${base}/${p.current}`),
1006
+ num(get, `${base}/${p.target}`)
1007
+ ]);
1008
+ return {
1009
+ id,
1010
+ kind: "hc",
1011
+ currentTemp: current,
1012
+ targetTemp: target,
1013
+ operationMode,
1014
+ heatCoolMode,
1015
+ summerWinter,
1016
+ comfort,
1017
+ eco,
1018
+ maxFlow,
1019
+ level: null,
1020
+ chargeDuration: null,
1021
+ charge: null
1022
+ };
1023
+ }
1024
+ async function readDhwCircuit(get, id) {
1025
+ const base = `/dhwCircuits/${id}`;
1026
+ const p = HEAT_PUMP_PATHS.dhw;
1027
+ const [operationMode, level, chargeDuration, charge, current, target] = await Promise.all([
1028
+ setting(get, `${base}/${p.operationMode}`),
1029
+ setting(get, `${base}/${p.level}`),
1030
+ setting(get, `${base}/${p.chargeDuration}`),
1031
+ setting(get, `${base}/${p.charge}`),
1032
+ num(get, `${base}/${p.current}`),
1033
+ num(get, `${base}/${p.target}`)
1034
+ ]);
1035
+ return {
1036
+ id,
1037
+ kind: "dhw",
1038
+ currentTemp: current,
1039
+ targetTemp: target,
1040
+ operationMode,
1041
+ heatCoolMode: null,
1042
+ summerWinter: null,
1043
+ comfort: null,
1044
+ eco: null,
1045
+ maxFlow: null,
1046
+ level,
1047
+ chargeDuration,
1048
+ charge
1049
+ };
1050
+ }
1051
+ async function setting(get, path) {
1052
+ const envelope = readEnvelope(await get(path));
1053
+ return envelope === null ? null : settingOf(path, envelope);
1054
+ }
1055
+ async function num(get, path) {
1056
+ const envelope = readEnvelope(await get(path));
1057
+ return envelope !== null && typeof envelope.value === "number" && Number.isFinite(envelope.value) ? envelope.value : null;
1058
+ }
1059
+ function settingOf(path, envelope) {
1060
+ return {
1061
+ path,
1062
+ value: envelope.value,
1063
+ min: envelope.min,
1064
+ max: envelope.max,
1065
+ allowed: envelope.allowed
1066
+ };
1067
+ }
1068
+ function heatPumpStatusFrom(reading, now) {
1069
+ return {
1070
+ circuits: reading.circuits.map(circuitOf),
1071
+ system: {
1072
+ silent: optionOf(reading.silent),
1073
+ away: optionOf(reading.away),
1074
+ additionalHeater: optionOf(reading.additionalHeater),
1075
+ holiday: optionOf(reading.holiday)
1076
+ },
1077
+ power: {
1078
+ thermalW: reading.thermalW,
1079
+ electricalW: reading.electricalW,
1080
+ compressorPct: reading.compressorPct
1081
+ },
1082
+ temperatures: {
1083
+ supply: reading.supply,
1084
+ return: reading.return,
1085
+ outdoor: reading.outdoor
1086
+ },
1087
+ energy: {
1088
+ thermalKwh: reading.thermalKwh,
1089
+ electricalKwh: reading.electricalKwh
1090
+ },
1091
+ waterPressureBar: reading.pressureBar,
1092
+ starts: reading.starts,
1093
+ lastFetchedAt: now
1094
+ };
1095
+ }
1096
+ function circuitOf(c) {
1097
+ const kind = kindOf(c);
1098
+ return {
1099
+ id: c.id,
1100
+ kind,
1101
+ label: labelOf(c),
1102
+ icon: kind === "dhw" ? "droplets" : kind === "cooling" ? "snowflake" : "flame",
1103
+ currentTemp: c.currentTemp,
1104
+ targetTemp: c.targetTemp,
1105
+ mode: modeValue(c.operationMode),
1106
+ active: null,
1107
+ operationMode: optionOf(c.operationMode),
1108
+ heatCoolMode: optionOf(c.heatCoolMode),
1109
+ summerWinter: optionOf(c.summerWinter),
1110
+ comfortSetpoint: rangeOf(c.comfort),
1111
+ ecoSetpoint: rangeOf(c.eco),
1112
+ maxFlowTemp: rangeOf(c.maxFlow),
1113
+ temperatureLevel: optionOf(c.level),
1114
+ chargeDuration: rangeOf(c.chargeDuration),
1115
+ extraHotWater: c.charge === null ? null : textOf$1(c.charge) === "start"
1116
+ };
1117
+ }
1118
+ function kindOf(c) {
1119
+ if (c.kind === "dhw") return "dhw";
1120
+ return textOf$1(c.heatCoolMode) === "cooling" ? "cooling" : "heating";
1121
+ }
1122
+ function labelOf(c) {
1123
+ const n = c.id.replace(/^\D+/, "");
1124
+ if (c.kind === "dhw") return n === "1" || n === "" ? "Hot water" : `Hot water ${n}`;
1125
+ return n === "" ? "Heating circuit" : `Heating circuit ${n}`;
1126
+ }
1127
+ function textOf$1(s) {
1128
+ return s !== null && typeof s.value === "string" && s.value.length > 0 ? s.value : null;
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
+ }
1139
+ function optionOf(s) {
1140
+ if (s === null) return null;
1141
+ return {
1142
+ value: modeValue(s),
1143
+ allowed: [...s.allowed]
1144
+ };
1145
+ }
1146
+ function rangeOf(s) {
1147
+ if (s === null) return null;
1148
+ return {
1149
+ value: typeof s.value === "number" && Number.isFinite(s.value) ? s.value : null,
1150
+ min: s.min,
1151
+ max: s.max,
1152
+ step: null
1153
+ };
1154
+ }
1155
+ function requireCircuit(reading, circuitId, want) {
1156
+ const found = reading.circuits.find((c) => c.id === circuitId && c.kind === want);
1157
+ if (found === void 0) throw new HomeComHeatPumpRefusalError(`HomeCom heat pump has no ${want} circuit ${circuitId}`);
1158
+ return found;
1159
+ }
1160
+ function optionWrite(s, what, mode) {
1161
+ if (s === null) throw new HomeComHeatPumpRefusalError(`HomeCom heat pump: ${what} is not available`);
1162
+ if (s.allowed.length === 0) throw new HomeComHeatPumpRefusalError(`HomeCom heat pump: ${what} is read-only on this gateway`);
1163
+ if (!s.allowed.includes(mode)) throw new HomeComHeatPumpRefusalError(`HomeCom heat pump: "${mode}" is not an accepted value of ${what} (${s.allowed.join(", ")})`);
1164
+ return {
1165
+ path: s.path,
1166
+ value: mode
1167
+ };
1168
+ }
1169
+ function rangeWrite(s, what, value) {
1170
+ if (s === null) throw new HomeComHeatPumpRefusalError(`HomeCom heat pump: ${what} is not available`);
1171
+ if (!Number.isFinite(value)) throw new HomeComHeatPumpRefusalError(`HomeCom heat pump: ${what} needs a finite number`);
1172
+ if (s.min === null || s.max === null) throw new HomeComHeatPumpRefusalError(`HomeCom heat pump: ${what} has no limits from the gateway`);
1173
+ if (value < s.min || value > s.max) throw new HomeComHeatPumpRefusalError(`HomeCom heat pump: ${value} is outside ${what} limits ${s.min}..${s.max}`);
1174
+ return {
1175
+ path: s.path,
1176
+ value
1177
+ };
1178
+ }
1179
+ function systemModeWrite(reading, which, mode) {
1180
+ return optionWrite(reading[which], `${which} mode`, mode);
1181
+ }
1182
+ function circuitOptionWrite(reading, circuitId, which, mode) {
1183
+ return optionWrite(requireCircuit(reading, circuitId, "hc")[which], `${circuitId} ${which}`, mode);
1184
+ }
1185
+ function dhwOptionWrite(reading, circuitId, which, mode) {
1186
+ return optionWrite(requireCircuit(reading, circuitId, "dhw")[which], `${circuitId} ${which}`, mode);
1187
+ }
1188
+ function circuitRangeWrite(reading, circuitId, which, value) {
1189
+ return rangeWrite(requireCircuit(reading, circuitId, "hc")[which], `${circuitId} ${which}`, value);
1190
+ }
1191
+ function dhwChargeDurationWrite(reading, circuitId, minutes) {
1192
+ return rangeWrite(requireCircuit(reading, circuitId, "dhw").chargeDuration, `${circuitId} charge duration`, minutes);
1193
+ }
1194
+ function extraHotWaterWrite(reading, circuitId) {
1195
+ const charge = requireCircuit(reading, circuitId, "dhw").charge;
1196
+ return optionWrite(charge, `${circuitId} extra hot water`, "start");
1197
+ }
1198
+ //#endregion
1199
+ //#region src/homecom/homecom-oauth.ts
1200
+ /**
1201
+ * Public OAuth client of the HomeCom Easy app, signed in through SingleKey ID.
1202
+ *
1203
+ * A PKCE client has no secret. The values are the ones SingleKey accepts for this client id
1204
+ * today: the same authorize query the MIT-licensed `homecom_alt` client records
1205
+ * (`OAUTH_LOGIN_PARAMS` — parameters, not copied code) and that the Home Assistant integration
1206
+ * opens. `pointt.gateway.resource.dopair` is not a scope this client may request; SingleKey
1207
+ * answers `misconfigured-application` and never shows the login page. The scope it does accept
1208
+ * names `pointt.gateway.resource.dashapp`, and the Bosch app sends `style_id=tt_bsch`.
1209
+ */
1210
+ var HOMECOM_CLIENT_ID = "762162C0-FA2D-4540-AE66-6489F189FADC";
1211
+ var HOMECOM_AUTHORIZE_URL = "https://singlekey-id.com/auth/connect/authorize";
1212
+ var HOMECOM_TOKEN_URL = "https://singlekey-id.com/auth/connect/token";
1213
+ var HOMECOM_STYLE_ID = "tt_bsch";
1214
+ var HOMECOM_SCOPE = [
1215
+ "openid",
1216
+ "email",
1217
+ "profile",
1218
+ "offline_access",
1219
+ "pointt.gateway.claiming",
1220
+ "pointt.gateway.removal",
1221
+ "pointt.gateway.list",
1222
+ "pointt.gateway.users",
1223
+ "pointt.gateway.resource.dashapp",
1224
+ "pointt.castt.flow.token-exchange",
1225
+ "bacon",
1226
+ "hcc.tariff.read"
1227
+ ].join(" ");
1228
+ //#endregion
719
1229
  //#region src/homecom/homecom-api-client.ts
720
1230
  var HomeComAuthError = class extends Error {
721
1231
  name = "HomeComAuthError";
@@ -796,6 +1306,7 @@ function createHomeComApiClient(config, gatewayId, deps = {}) {
796
1306
  let last = null;
797
1307
  let refreshTail = Promise.resolve();
798
1308
  const dropped = /* @__PURE__ */ new Set();
1309
+ let heatPumpPathsLogged = false;
799
1310
  const base = homeComApiBase(config.region);
800
1311
  function authHeaders() {
801
1312
  return {
@@ -929,15 +1440,56 @@ function createHomeComApiClient(config, gatewayId, deps = {}) {
929
1440
  }
930
1441
  async function fetchSystem() {
931
1442
  const resolved = await resolveDeviceType();
932
- const snapshot = await readPointtSnapshot({
1443
+ const withHeatPump = await attachHeatPump(await readPointtSnapshot({
933
1444
  deviceType: resolved.deviceType,
934
1445
  family: resolved.family,
935
1446
  gatewayId,
936
1447
  get: getResource,
937
1448
  logDrop
1449
+ }));
1450
+ last = withHeatPump;
1451
+ return withHeatPump;
1452
+ }
1453
+ async function attachHeatPump(snapshot) {
1454
+ if (snapshot.family !== "hydronic" || snapshot.deviceType === "rrc2") return snapshot;
1455
+ const dhwId = snapshot.dhw?.circuitId;
1456
+ const heatPump = await readHeatPump({
1457
+ get: getResource,
1458
+ onFailure: logHeatPumpFailure,
1459
+ circuitIds: {
1460
+ hc: snapshot.circuits.map((circuit) => circuit.id),
1461
+ dhw: dhwId === void 0 ? [] : [dhwId]
1462
+ }
1463
+ });
1464
+ logHeatPumpPaths(heatPump);
1465
+ return hasSystemDevice(heatPump) ? {
1466
+ ...snapshot,
1467
+ heatPump
1468
+ } : snapshot;
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
+ }
1482
+ function logHeatPumpFailure(path, error) {
1483
+ const key = `heat-pump-read:${path}`;
1484
+ if (dropped.has(key)) return;
1485
+ dropped.add(key);
1486
+ logger?.warn("HomeCom heat-pump resource could not be read; it is skipped", {
1487
+ tags: { gatewayId },
1488
+ meta: {
1489
+ path,
1490
+ error: error instanceof Error ? error.message : String(error)
1491
+ }
938
1492
  });
939
- last = snapshot;
940
- return snapshot;
941
1493
  }
942
1494
  async function probePath(path) {
943
1495
  const res = await authed(homeComResourceUrl(gatewayId, path), {
@@ -991,6 +1543,9 @@ function createHomeComApiClient(config, gatewayId, deps = {}) {
991
1543
  const snapshot = await requireSnapshot();
992
1544
  if (snapshot.family !== "rac") logDrop("swing is not a resource on this device type");
993
1545
  await putWrite(swingWrite(snapshot, "horizontal", on));
1546
+ },
1547
+ async writeHeatPump(writes) {
1548
+ for (const write of writes) await putWrite(write);
994
1549
  }
995
1550
  };
996
1551
  }
@@ -1255,8 +1810,7 @@ function buildHomeComGatewayPersister(deps) {
1255
1810
  function buildHomeComCandidates(input) {
1256
1811
  const out = [];
1257
1812
  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"];
1813
+ const { type, caps } = shapeOf(dev.kind);
1260
1814
  const adoptedId = input.adopted.get(dev.nativeId) ?? null;
1261
1815
  out.push({
1262
1816
  childNativeId: dev.nativeId,
@@ -1271,6 +1825,22 @@ function buildHomeComCandidates(input) {
1271
1825
  }
1272
1826
  return out;
1273
1827
  }
1828
+ function shapeOf(kind) {
1829
+ switch (kind) {
1830
+ case "heating-circuit": return {
1831
+ type: DeviceType.Climate,
1832
+ caps: ["climate-control"]
1833
+ };
1834
+ case "dhw": return {
1835
+ type: DeviceType.WaterHeater,
1836
+ caps: ["water-heater"]
1837
+ };
1838
+ case "heat-pump": return {
1839
+ type: DeviceType.HeatPump,
1840
+ caps: ["heat-pump-status", "heat-pump-control"]
1841
+ };
1842
+ }
1843
+ }
1274
1844
  //#endregion
1275
1845
  //#region src/homecom/homecom-adoption-provider.ts
1276
1846
  var DEVICES_FILTER = {
@@ -1455,6 +2025,11 @@ function buildDiscoverables(snapshot) {
1455
2025
  name: "Domestic hot water",
1456
2026
  kind: "dhw"
1457
2027
  });
2028
+ if (snapshot.heatPump !== void 0) out.push({
2029
+ nativeId: "hp:0",
2030
+ name: "Heat pump",
2031
+ kind: "heat-pump"
2032
+ });
1458
2033
  return out;
1459
2034
  }
1460
2035
  var homeComConnections = new HomeComConnectionResolver();
@@ -1941,7 +2516,7 @@ var homeComClimateSchema = object({
1941
2516
  integrationId: string().optional()
1942
2517
  });
1943
2518
  var CLIMATE_CAP = "climate-control";
1944
- var COLD_START$1 = {
2519
+ var COLD_START$2 = {
1945
2520
  mode: "off",
1946
2521
  availableModes: [],
1947
2522
  fanMode: "",
@@ -1976,14 +2551,14 @@ var HomeComClimateDevice = class extends BaseDevice {
1976
2551
  async onActivate() {
1977
2552
  await super.onActivate();
1978
2553
  this.registerClimateCap();
1979
- this.runtimeState.setCapState(CLIMATE_CAP, COLD_START$1);
2554
+ this.runtimeState.setCapState(CLIMATE_CAP, COLD_START$2);
1980
2555
  this.refreshFromSnapshot();
1981
2556
  }
1982
2557
  refreshFromSnapshot() {
1983
2558
  const circuit = homeComConnections.getSnapshot(this.#brokerId)?.circuits.find((c) => c.id === this.#circuitId);
1984
2559
  if (!circuit) {
1985
2560
  this.runtimeState.setCapState(CLIMATE_CAP, {
1986
- ...COLD_START$1,
2561
+ ...COLD_START$2,
1987
2562
  lastFetchedAt: Date.now()
1988
2563
  });
1989
2564
  return;
@@ -1999,7 +2574,7 @@ var HomeComClimateDevice = class extends BaseDevice {
1999
2574
  this.ctx.registerNativeCap(climateControlCapability, {
2000
2575
  getStatus: async () => {
2001
2576
  this.refreshFromSnapshot();
2002
- return this.runtimeState.getCapState(CLIMATE_CAP) ?? COLD_START$1;
2577
+ return this.runtimeState.getCapState(CLIMATE_CAP) ?? COLD_START$2;
2003
2578
  },
2004
2579
  setMode: async ({ mode }) => {
2005
2580
  const apiMode = hvacModeToHomeCom(mode);
@@ -2051,7 +2626,7 @@ var homeComWaterHeaterSchema = object({
2051
2626
  integrationId: string().optional()
2052
2627
  });
2053
2628
  var WH_CAP = "water-heater";
2054
- var COLD_START = {
2629
+ var COLD_START$1 = {
2055
2630
  currentTemp: null,
2056
2631
  targetTemp: null,
2057
2632
  operationMode: null,
@@ -2077,14 +2652,14 @@ var HomeComWaterHeaterDevice = class extends BaseDevice {
2077
2652
  async onActivate() {
2078
2653
  await super.onActivate();
2079
2654
  this.registerWaterHeaterCap();
2080
- this.runtimeState.setCapState(WH_CAP, COLD_START);
2655
+ this.runtimeState.setCapState(WH_CAP, COLD_START$1);
2081
2656
  this.refreshFromSnapshot();
2082
2657
  }
2083
2658
  refreshFromSnapshot() {
2084
2659
  const dhw = homeComConnections.getSnapshot(this.#brokerId)?.dhw;
2085
2660
  if (!dhw) {
2086
2661
  this.runtimeState.setCapState(WH_CAP, {
2087
- ...COLD_START,
2662
+ ...COLD_START$1,
2088
2663
  lastChangedAt: Date.now()
2089
2664
  });
2090
2665
  return;
@@ -2100,7 +2675,7 @@ var HomeComWaterHeaterDevice = class extends BaseDevice {
2100
2675
  this.ctx.registerNativeCap(waterHeaterCapability, {
2101
2676
  getStatus: async () => {
2102
2677
  this.refreshFromSnapshot();
2103
- return this.runtimeState.getCapState(WH_CAP) ?? COLD_START;
2678
+ return this.runtimeState.getCapState(WH_CAP) ?? COLD_START$1;
2104
2679
  },
2105
2680
  setTargetTemp: async ({ temp }) => {
2106
2681
  await this.requireClient().setDhwTarget(temp);
@@ -2117,13 +2692,157 @@ var HomeComWaterHeaterDevice = class extends BaseDevice {
2117
2692
  }
2118
2693
  };
2119
2694
  //#endregion
2695
+ //#region src/homecom/devices/homecom-heat-pump-device.ts
2696
+ var homeComHeatPumpSchema = object({
2697
+ nativeId: string().min(1),
2698
+ brokerId: string().min(1),
2699
+ system: literal("homecom").optional(),
2700
+ integrationId: string().optional()
2701
+ });
2702
+ var STATUS_CAP = "heat-pump-status";
2703
+ var COLD_START = {
2704
+ circuits: [],
2705
+ system: {
2706
+ silent: null,
2707
+ away: null,
2708
+ additionalHeater: null,
2709
+ holiday: null
2710
+ },
2711
+ power: {
2712
+ thermalW: null,
2713
+ electricalW: null,
2714
+ compressorPct: null
2715
+ },
2716
+ temperatures: {
2717
+ supply: null,
2718
+ return: null,
2719
+ outdoor: null
2720
+ },
2721
+ energy: {
2722
+ thermalKwh: null,
2723
+ electricalKwh: null
2724
+ },
2725
+ waterPressureBar: null,
2726
+ starts: null,
2727
+ lastFetchedAt: 0
2728
+ };
2729
+ /**
2730
+ * A heat pump on a hydronic HomeCom gateway (D1026). The slice and every limit
2731
+ * come from the gateway reading the integration poll already holds; a write is
2732
+ * checked against those limits first and the reading is refreshed after it.
2733
+ */
2734
+ var HomeComHeatPumpDevice = class extends BaseDevice {
2735
+ features = [];
2736
+ #brokerId;
2737
+ /** True while the gateway has no heat-pump reading, so the log marks the transitions only. */
2738
+ #noReading = false;
2739
+ constructor(ctx) {
2740
+ const persisted = homeComHeatPumpSchema.parse(ctx.persistedConfig ?? {});
2741
+ super(ctx, homeComHeatPumpSchema, { type: ctx.deviceMeta.type });
2742
+ this.#brokerId = persisted.brokerId;
2743
+ this.online = true;
2744
+ this.updateSourceInfo({
2745
+ id: persisted.nativeId,
2746
+ system: "homecom"
2747
+ });
2748
+ }
2749
+ async onActivate() {
2750
+ await super.onActivate();
2751
+ this.registerStatusCap();
2752
+ this.registerControlCap();
2753
+ this.runtimeState.setCapState(STATUS_CAP, COLD_START);
2754
+ this.refreshFromSnapshot();
2755
+ }
2756
+ reading() {
2757
+ return homeComConnections.getSnapshot(this.#brokerId)?.heatPump ?? null;
2758
+ }
2759
+ refreshFromSnapshot() {
2760
+ const reading = this.reading();
2761
+ if (reading === null) {
2762
+ if (!this.#noReading) {
2763
+ this.#noReading = true;
2764
+ this.ctx.logger.warn("HomeCom heat pump: the gateway returned no heat-pump reading", { tags: { deviceId: this.ctx.id } });
2765
+ }
2766
+ this.runtimeState.setCapState(STATUS_CAP, {
2767
+ ...COLD_START,
2768
+ lastFetchedAt: Date.now()
2769
+ });
2770
+ return;
2771
+ }
2772
+ if (this.#noReading) {
2773
+ this.#noReading = false;
2774
+ this.ctx.logger.info("HomeCom heat pump: the gateway answers with a heat-pump reading again", { tags: { deviceId: this.ctx.id } });
2775
+ }
2776
+ this.runtimeState.setCapState(STATUS_CAP, heatPumpStatusFrom(reading, Date.now()));
2777
+ }
2778
+ requireClient() {
2779
+ const client = homeComConnections.getClient(this.#brokerId);
2780
+ if (!client) throw new Error("HomeCom client unavailable (heat pump)");
2781
+ return client;
2782
+ }
2783
+ requireReading() {
2784
+ const reading = this.reading();
2785
+ if (reading === null) throw new Error("HomeCom heat pump: no reading from the gateway yet");
2786
+ return reading;
2787
+ }
2788
+ /** Validate against the reading, send, then re-read so the slice shows what the gateway holds. */
2789
+ async apply(build) {
2790
+ const write = build(this.requireReading());
2791
+ const client = this.requireClient();
2792
+ await client.writeHeatPump([write]);
2793
+ this.ctx.logger.info("HomeCom heat pump: value written", {
2794
+ tags: { deviceId: this.ctx.id },
2795
+ meta: { path: write.path }
2796
+ });
2797
+ try {
2798
+ const snapshot = await client.fetchSystem();
2799
+ homeComConnections.updateSnapshot(this.#brokerId, snapshot);
2800
+ } catch (error) {
2801
+ this.ctx.logger.warn("HomeCom heat pump: value written, the refresh failed", {
2802
+ tags: { deviceId: this.ctx.id },
2803
+ meta: {
2804
+ path: write.path,
2805
+ error: error instanceof Error ? error.message : String(error)
2806
+ }
2807
+ });
2808
+ return;
2809
+ }
2810
+ this.refreshFromSnapshot();
2811
+ }
2812
+ registerStatusCap() {
2813
+ this.ctx.registerNativeCap(heatPumpStatusCapability, { getStatus: async () => {
2814
+ this.refreshFromSnapshot();
2815
+ return this.runtimeState.getCapState(STATUS_CAP) ?? COLD_START;
2816
+ } });
2817
+ }
2818
+ registerControlCap() {
2819
+ this.ctx.registerNativeCap(heatPumpControlCapability, {
2820
+ setSilentMode: ({ mode }) => this.apply((r) => systemModeWrite(r, "silent", mode)),
2821
+ setAwayMode: ({ mode }) => this.apply((r) => systemModeWrite(r, "away", mode)),
2822
+ setAdditionalHeaterMode: ({ mode }) => this.apply((r) => systemModeWrite(r, "additionalHeater", mode)),
2823
+ setHolidayMode: ({ mode }) => this.apply((r) => systemModeWrite(r, "holiday", mode)),
2824
+ setCircuitOperationMode: ({ circuitId, mode }) => this.apply((r) => circuitOptionWrite(r, circuitId, "operationMode", mode)),
2825
+ setCircuitHeatCoolMode: ({ circuitId, mode }) => this.apply((r) => circuitOptionWrite(r, circuitId, "heatCoolMode", mode)),
2826
+ setCircuitSummerWinter: ({ circuitId, mode }) => this.apply((r) => circuitOptionWrite(r, circuitId, "summerWinter", mode)),
2827
+ setCircuitComfortSetpoint: ({ circuitId, temp }) => this.apply((r) => circuitRangeWrite(r, circuitId, "comfort", temp)),
2828
+ setCircuitEcoSetpoint: ({ circuitId, temp }) => this.apply((r) => circuitRangeWrite(r, circuitId, "eco", temp)),
2829
+ setCircuitMaxFlowTemp: ({ circuitId, temp }) => this.apply((r) => circuitRangeWrite(r, circuitId, "maxFlow", temp)),
2830
+ setDhwOperationMode: ({ circuitId, mode }) => this.apply((r) => dhwOptionWrite(r, circuitId, "operationMode", mode)),
2831
+ setDhwTemperatureLevel: ({ circuitId, level }) => this.apply((r) => dhwOptionWrite(r, circuitId, "level", level)),
2832
+ setDhwChargeDuration: ({ circuitId, minutes }) => this.apply((r) => dhwChargeDurationWrite(r, circuitId, minutes)),
2833
+ startExtraHotWater: ({ circuitId }) => this.apply((r) => extraHotWaterWrite(r, circuitId))
2834
+ });
2835
+ }
2836
+ };
2837
+ //#endregion
2120
2838
  //#region src/homecom/addon.ts
2121
2839
  var BoschHomeComProviderAddon = class extends BaseDeviceProvider {
2122
2840
  addonId = "bosch-homecom";
2123
2841
  providerName = "Bosch HomeCom";
2124
2842
  deviceClasses = {
2125
2843
  [DeviceType.Climate]: HomeComClimateDevice,
2126
- [DeviceType.WaterHeater]: HomeComWaterHeaterDevice
2844
+ [DeviceType.WaterHeater]: HomeComWaterHeaterDevice,
2845
+ [DeviceType.HeatPump]: HomeComHeatPumpDevice
2127
2846
  };
2128
2847
  clients = null;
2129
2848
  instances = null;