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