@camstack/addon-provider-bosch 0.1.50 → 0.1.52

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,7 +1,11 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_bosch_instances = require("../bosch-instances-CYBnIcHg.js");
3
- const require_account_integration_offline = require("../account-integration-offline-7CH3r5jI.js");
2
+ const require_bosch_instances = require("../bosch-instances-LWbVnsaD.js");
3
+ const require_account_integration_offline = require("../account-integration-offline-BEzGeJcl.js");
4
4
  let node_crypto = require("node:crypto");
5
+ let node_child_process = require("node:child_process");
6
+ let node_fs = require("node:fs");
7
+ let node_path = require("node:path");
8
+ let node_url = require("node:url");
5
9
  //#region src/homecom/config.ts
6
10
  var HOMECOM_BRANDS = ["bosch", "buderus"];
7
11
  var HomeComBrandSchema = require_bosch_instances._enum(HOMECOM_BRANDS);
@@ -648,6 +652,210 @@ function unsupportedDeviceTypeMessage(deviceType) {
648
652
  return new HomeComUnknownDeviceTypeError(deviceType ?? "(missing)").message;
649
653
  }
650
654
  //#endregion
655
+ //#region src/homecom/homecom-bridge.ts
656
+ /**
657
+ * The HomeCom Python bridge: one `python/homecom_worker.py` run per poll or write, built on
658
+ * the MIT `homecom_alt` library (pinned in `python/requirements.txt`).
659
+ *
660
+ * The access token travels ONLY in the JSON written to the child's stdin — never in argv,
661
+ * never in the environment — and the worker never prints it. The OAuth refresh stays in
662
+ * TypeScript: the worker answers `auth` and the caller refreshes and runs it again.
663
+ */
664
+ var HOMECOM_BRIDGE_DEVICE_TYPES = [
665
+ "k40",
666
+ "k30",
667
+ "icom"
668
+ ];
669
+ function isBridgeDeviceType(value) {
670
+ return HOMECOM_BRIDGE_DEVICE_TYPES.some((type) => type === value);
671
+ }
672
+ var SettingSchema = require_bosch_instances.object({
673
+ value: require_bosch_instances.union([require_bosch_instances.string(), require_bosch_instances.number()]).nullable(),
674
+ min: require_bosch_instances.number().nullable(),
675
+ max: require_bosch_instances.number().nullable(),
676
+ allowed: require_bosch_instances.array(require_bosch_instances.string()),
677
+ writeable: require_bosch_instances.boolean().nullable()
678
+ });
679
+ var MaybeSetting = SettingSchema.nullable();
680
+ var HeatingCircuitSchema = require_bosch_instances.object({
681
+ id: require_bosch_instances.string(),
682
+ /** The name the gateway gave the circuit in its reference listing, when it gave one. */
683
+ name: require_bosch_instances.string().nullable().default(null),
684
+ operationMode: MaybeSetting,
685
+ heatCoolMode: MaybeSetting,
686
+ currentSuWiMode: MaybeSetting,
687
+ suWiSwitchMode: MaybeSetting,
688
+ roomTemp: require_bosch_instances.number().nullable(),
689
+ humidity: require_bosch_instances.number().nullable(),
690
+ currentRoomSetpoint: MaybeSetting,
691
+ manualRoomSetpoint: MaybeSetting,
692
+ coolingRoomSetpoint: MaybeSetting,
693
+ comfortSetpoint: MaybeSetting,
694
+ ecoSetpoint: MaybeSetting,
695
+ maxFlowTemp: MaybeSetting
696
+ });
697
+ var DhwCircuitSchema = require_bosch_instances.object({
698
+ id: require_bosch_instances.string(),
699
+ operationMode: MaybeSetting,
700
+ actualTemp: require_bosch_instances.number().nullable(),
701
+ currentTemperatureLevel: MaybeSetting,
702
+ charge: MaybeSetting,
703
+ chargeDuration: MaybeSetting,
704
+ singleChargeSetpoint: MaybeSetting,
705
+ currentSetpoint: MaybeSetting,
706
+ temperatureLevels: require_bosch_instances.record(require_bosch_instances.string(), SettingSchema)
707
+ });
708
+ /**
709
+ * One GET of a heat-pump-only heat-source resource: `refused` (403/500) still proves a heat
710
+ * pump the gateway will not let us read, `absent` (404) is a boiler, `transient` proves nothing.
711
+ */
712
+ var ProbeOutcomeSchema = require_bosch_instances._enum([
713
+ "answered",
714
+ "refused",
715
+ "absent",
716
+ "transient"
717
+ ]);
718
+ var ReadingSchema = require_bosch_instances.object({
719
+ heatSource: require_bosch_instances.object({
720
+ heatPumpType: require_bosch_instances.string().nullable(),
721
+ supplyTemp: require_bosch_instances.number().nullable(),
722
+ returnTemp: require_bosch_instances.number().nullable(),
723
+ modulationPct: require_bosch_instances.number().nullable(),
724
+ pressureBar: require_bosch_instances.number().nullable(),
725
+ starts: require_bosch_instances.number().nullable(),
726
+ outdoorTemp: require_bosch_instances.number().nullable(),
727
+ thermalKwh: require_bosch_instances.number().nullable(),
728
+ electricalKwh: require_bosch_instances.number().nullable()
729
+ }),
730
+ system: require_bosch_instances.object({
731
+ away: MaybeSetting,
732
+ silent: MaybeSetting,
733
+ additionalHeater: MaybeSetting,
734
+ holiday: MaybeSetting
735
+ }),
736
+ heatingCircuits: require_bosch_instances.array(HeatingCircuitSchema),
737
+ dhwCircuits: require_bosch_instances.array(DhwCircuitSchema),
738
+ /** Asked only when the bulk read left the heat-pump type unknown; path → outcome. */
739
+ heatSourceProbe: require_bosch_instances.record(require_bosch_instances.string(), ProbeOutcomeSchema).default({})
740
+ });
741
+ var FailureKind = require_bosch_instances._enum([
742
+ "auth",
743
+ "unreachable",
744
+ "refused",
745
+ "api",
746
+ "protocol",
747
+ "bad-request"
748
+ ]);
749
+ var AnswerSchema = require_bosch_instances.union([
750
+ require_bosch_instances.object({
751
+ ok: require_bosch_instances.literal(true),
752
+ reading: ReadingSchema
753
+ }),
754
+ require_bosch_instances.object({
755
+ ok: require_bosch_instances.literal(true),
756
+ written: require_bosch_instances.string()
757
+ }),
758
+ require_bosch_instances.object({
759
+ ok: require_bosch_instances.literal(false),
760
+ kind: FailureKind,
761
+ error: require_bosch_instances.string()
762
+ })
763
+ ]);
764
+ /** A failed bridge run. The message never carries more than a short stderr tail. */
765
+ var HomeComBridgeError = class extends Error {
766
+ name = "HomeComBridgeError";
767
+ kind;
768
+ constructor(kind, message) {
769
+ super(message);
770
+ this.kind = kind;
771
+ }
772
+ };
773
+ /** A K40 bulk read is several bulk POSTs plus per-circuit GETs, 15 s each at most. */
774
+ var DEFAULT_READ_TIMEOUT_MS = 5e4;
775
+ var DEFAULT_WRITE_TIMEOUT_MS = 3e4;
776
+ var MAX_STDOUT_BYTES = 2 * 1024 * 1024;
777
+ function runWorker(deps, request, timeoutMs) {
778
+ const spawnFn = deps.spawnFn ?? node_child_process.spawn;
779
+ return new Promise((resolve, reject) => {
780
+ const child = spawnFn(deps.pythonPath, [deps.scriptPath], { stdio: "pipe" });
781
+ let stdout = "";
782
+ let stderr = "";
783
+ let settled = false;
784
+ const finish = (action) => {
785
+ if (settled) return;
786
+ settled = true;
787
+ clearTimeout(timer);
788
+ action();
789
+ };
790
+ const timer = setTimeout(() => {
791
+ child.kill("SIGKILL");
792
+ finish(() => reject(new HomeComBridgeError("timeout", `the HomeCom bridge did not answer within ${timeoutMs / 1e3}s`)));
793
+ }, timeoutMs);
794
+ child.stdout.on("data", (chunk) => {
795
+ stdout += chunk.toString("utf8");
796
+ if (stdout.length > MAX_STDOUT_BYTES) {
797
+ child.kill("SIGKILL");
798
+ finish(() => reject(new HomeComBridgeError("protocol", "the HomeCom bridge wrote too much output")));
799
+ }
800
+ });
801
+ child.stderr.on("data", (chunk) => {
802
+ stderr = (stderr + chunk.toString("utf8")).slice(-16384);
803
+ });
804
+ child.on("error", (err) => {
805
+ finish(() => reject(new HomeComBridgeError("protocol", `cannot start the HomeCom bridge: ${err.message}`)));
806
+ });
807
+ child.on("close", (code) => {
808
+ finish(() => {
809
+ let json;
810
+ try {
811
+ json = JSON.parse(stdout.trim());
812
+ } catch {
813
+ const tail = stderr.trim().slice(-300) || "no stderr";
814
+ reject(new HomeComBridgeError("protocol", `the HomeCom bridge produced no JSON (exit ${code ?? "null"}): ${tail}`));
815
+ return;
816
+ }
817
+ const parsed = AnswerSchema.safeParse(json);
818
+ if (!parsed.success) {
819
+ reject(new HomeComBridgeError("protocol", "the HomeCom bridge answered an unknown shape"));
820
+ return;
821
+ }
822
+ resolve(parsed.data);
823
+ });
824
+ });
825
+ child.stdin.on("error", () => void 0);
826
+ child.stdin.end(JSON.stringify(request));
827
+ });
828
+ }
829
+ function failure(answer) {
830
+ return answer.ok ? null : new HomeComBridgeError(answer.kind, answer.error);
831
+ }
832
+ function createHomeComBridge(deps) {
833
+ const readTimeoutMs = deps.readTimeoutMs ?? DEFAULT_READ_TIMEOUT_MS;
834
+ const writeTimeoutMs = deps.writeTimeoutMs ?? DEFAULT_WRITE_TIMEOUT_MS;
835
+ return {
836
+ async read(target) {
837
+ const answer = await runWorker(deps, {
838
+ ...target,
839
+ action: "read"
840
+ }, readTimeoutMs);
841
+ const failed = failure(answer);
842
+ if (failed !== null) throw failed;
843
+ if (!("reading" in answer)) throw new HomeComBridgeError("protocol", "the HomeCom bridge answered a read with no reading");
844
+ return answer.reading;
845
+ },
846
+ async write(target, write) {
847
+ const answer = await runWorker(deps, {
848
+ ...target,
849
+ action: "write",
850
+ write
851
+ }, writeTimeoutMs);
852
+ const failed = failure(answer);
853
+ if (failed !== null) throw failed;
854
+ if (!("written" in answer) || answer.written !== write.op) throw new HomeComBridgeError("protocol", "the HomeCom bridge did not confirm the write");
855
+ }
856
+ };
857
+ }
858
+ //#endregion
651
859
  //#region src/homecom/homecom-domain-mapping.ts
652
860
  var MODE_MAP = {
653
861
  Off: "off",
@@ -723,6 +931,280 @@ function waterHeaterSliceFromDhw(dhw, nowMs) {
723
931
  lastChangedAt: nowMs
724
932
  };
725
933
  }
934
+ //#endregion
935
+ //#region src/homecom/homecom-heat-pump.ts
936
+ /** A write the gateway would refuse, caught before it is sent. */
937
+ var HomeComHeatPumpRefusalError = class extends Error {
938
+ name = "HomeComHeatPumpRefusalError";
939
+ };
940
+ var COOLING_VALUES = ["cool", "cooling"];
941
+ /** The current level's own temperature, or the gateway's current setpoint (icom). */
942
+ function dhwTargetSetting(dhw) {
943
+ const level = dhw.currentTemperatureLevel?.value;
944
+ if (typeof level === "string" && level !== "off") {
945
+ const own = dhw.temperatureLevels[level];
946
+ if (own !== void 0) return own;
947
+ }
948
+ return dhw.currentSetpoint;
949
+ }
950
+ /** True when the circuit is cooling now: heat/cool mode or the season says so. */
951
+ function circuitIsCooling(hc) {
952
+ const mode = hc.heatCoolMode?.value;
953
+ const season = hc.currentSuWiMode?.value;
954
+ return typeof mode === "string" && COOLING_VALUES.includes(mode) || typeof season === "string" && COOLING_VALUES.includes(season);
955
+ }
956
+ function numberValue$1(s) {
957
+ return s !== null && typeof s.value === "number" ? s.value : null;
958
+ }
959
+ function hcReading(hc) {
960
+ const target = circuitIsCooling(hc) ? numberValue$1(hc.coolingRoomSetpoint) ?? numberValue$1(hc.currentRoomSetpoint) : numberValue$1(hc.currentRoomSetpoint) ?? numberValue$1(hc.manualRoomSetpoint);
961
+ return {
962
+ id: hc.id,
963
+ kind: "hc",
964
+ currentTemp: hc.roomTemp,
965
+ targetTemp: target,
966
+ operationMode: hc.operationMode,
967
+ heatCoolMode: hc.heatCoolMode,
968
+ summerWinter: hc.suWiSwitchMode,
969
+ comfort: hc.comfortSetpoint,
970
+ eco: hc.ecoSetpoint,
971
+ maxFlow: hc.maxFlowTemp,
972
+ level: null,
973
+ chargeDuration: null,
974
+ charge: null
975
+ };
976
+ }
977
+ function dhwReading(dhw) {
978
+ return {
979
+ id: dhw.id,
980
+ kind: "dhw",
981
+ currentTemp: dhw.actualTemp,
982
+ targetTemp: numberValue$1(dhwTargetSetting(dhw)),
983
+ operationMode: dhw.operationMode,
984
+ heatCoolMode: null,
985
+ summerWinter: null,
986
+ comfort: null,
987
+ eco: null,
988
+ maxFlow: null,
989
+ level: dhw.currentTemperatureLevel,
990
+ chargeDuration: dhw.chargeDuration,
991
+ charge: dhw.charge
992
+ };
993
+ }
994
+ function heatPumpReadingFrom(bridge) {
995
+ const hs = bridge.heatSource;
996
+ return {
997
+ circuits: [...bridge.heatingCircuits.map(hcReading), ...bridge.dhwCircuits.map(dhwReading)],
998
+ heatPumpType: hs.heatPumpType,
999
+ heatSourceRefused: Object.values(bridge.heatSourceProbe).some((outcome) => outcome === "answered" || outcome === "refused"),
1000
+ silent: bridge.system.silent,
1001
+ away: bridge.system.away,
1002
+ additionalHeater: bridge.system.additionalHeater,
1003
+ holiday: bridge.system.holiday,
1004
+ compressorPct: hs.modulationPct,
1005
+ supply: hs.supplyTemp,
1006
+ return: hs.returnTemp,
1007
+ outdoor: hs.outdoorTemp,
1008
+ thermalKwh: hs.thermalKwh,
1009
+ electricalKwh: hs.electricalKwh,
1010
+ pressureBar: hs.pressureBar,
1011
+ starts: hs.starts
1012
+ };
1013
+ }
1014
+ /**
1015
+ * True when the gateway should be offered as a system device (`hp:0`).
1016
+ *
1017
+ * Only evidence a gas boiler on the same controller family cannot give counts: a heat-pump
1018
+ * type on the heat source, a heat-pump-only heat-source resource the gateway refuses (403/500,
1019
+ * never a 404 or a transient failure), the silent mode or the additional heater
1020
+ * (heat-pump-only resources), or a heating circuit that can cool. Away, holiday, flow, return, modulation,
1021
+ * pressure and starts are answered by a boiler too.
1022
+ */
1023
+ function hasSystemDevice(reading) {
1024
+ if (reading.heatPumpType !== null || reading.heatSourceRefused) return true;
1025
+ if (reading.silent !== null || reading.additionalHeater !== null) return true;
1026
+ return reading.circuits.some((c) => c.kind === "hc" && offersCooling(c.heatCoolMode));
1027
+ }
1028
+ function offersCooling(setting) {
1029
+ if (setting === null) return false;
1030
+ const value = setting.value;
1031
+ if (typeof value === "string" && COOLING_VALUES.includes(value)) return true;
1032
+ return setting.allowed.some((allowed) => COOLING_VALUES.includes(allowed));
1033
+ }
1034
+ function heatPumpStatusFrom(reading, now) {
1035
+ return {
1036
+ circuits: reading.circuits.map(circuitOf),
1037
+ system: {
1038
+ silent: optionOf(reading.silent),
1039
+ away: optionOf(reading.away),
1040
+ additionalHeater: optionOf(reading.additionalHeater),
1041
+ holiday: optionOf(reading.holiday)
1042
+ },
1043
+ power: {
1044
+ thermalW: null,
1045
+ electricalW: null,
1046
+ compressorPct: reading.compressorPct
1047
+ },
1048
+ temperatures: {
1049
+ supply: reading.supply,
1050
+ return: reading.return,
1051
+ outdoor: reading.outdoor
1052
+ },
1053
+ energy: {
1054
+ thermalKwh: reading.thermalKwh,
1055
+ electricalKwh: reading.electricalKwh
1056
+ },
1057
+ waterPressureBar: reading.pressureBar,
1058
+ starts: reading.starts,
1059
+ lastFetchedAt: now
1060
+ };
1061
+ }
1062
+ function circuitOf(c) {
1063
+ const kind = kindOf(c);
1064
+ return {
1065
+ id: c.id,
1066
+ kind,
1067
+ label: labelOf(c),
1068
+ icon: kind === "dhw" ? "droplets" : kind === "cooling" ? "snowflake" : "flame",
1069
+ currentTemp: c.currentTemp,
1070
+ targetTemp: c.targetTemp,
1071
+ mode: modeValue(c.operationMode),
1072
+ active: null,
1073
+ operationMode: optionOf(c.operationMode),
1074
+ heatCoolMode: optionOf(c.heatCoolMode),
1075
+ summerWinter: optionOf(c.summerWinter),
1076
+ comfortSetpoint: rangeOf(c.comfort),
1077
+ ecoSetpoint: rangeOf(c.eco),
1078
+ maxFlowTemp: rangeOf(c.maxFlow),
1079
+ temperatureLevel: optionOf(c.level),
1080
+ chargeDuration: rangeOf(c.chargeDuration),
1081
+ extraHotWater: c.charge === null ? null : textOf$1(c.charge) === "start"
1082
+ };
1083
+ }
1084
+ function kindOf(c) {
1085
+ if (c.kind === "dhw") return "dhw";
1086
+ const mode = textOf$1(c.heatCoolMode);
1087
+ return mode !== null && COOLING_VALUES.includes(mode) ? "cooling" : "heating";
1088
+ }
1089
+ function labelOf(c) {
1090
+ const n = c.id.replace(/^\D+/, "");
1091
+ if (c.kind === "dhw") return n === "1" || n === "" ? "Hot water" : `Hot water ${n}`;
1092
+ return n === "" ? "Heating circuit" : `Heating circuit ${n}`;
1093
+ }
1094
+ function textOf$1(s) {
1095
+ return s !== null && typeof s.value === "string" && s.value.length > 0 ? s.value : null;
1096
+ }
1097
+ /**
1098
+ * A mode the gateway answered with an empty value is off (or "none" when the
1099
+ * gateway did not list off). A mode it did not answer at all stays null.
1100
+ */
1101
+ function modeValue(s) {
1102
+ if (s === null || typeof s.value !== "string") return null;
1103
+ if (s.value.length > 0) return s.value;
1104
+ return s.allowed.includes("off") ? "off" : "none";
1105
+ }
1106
+ function optionOf(s) {
1107
+ if (s === null) return null;
1108
+ return {
1109
+ value: modeValue(s),
1110
+ allowed: [...s.allowed]
1111
+ };
1112
+ }
1113
+ function rangeOf(s) {
1114
+ if (s === null) return null;
1115
+ return {
1116
+ value: numberValue$1(s),
1117
+ min: s.min,
1118
+ max: s.max,
1119
+ step: null
1120
+ };
1121
+ }
1122
+ function requireCircuit(reading, circuitId, want) {
1123
+ const found = reading.circuits.find((c) => c.id === circuitId && c.kind === want);
1124
+ if (found === void 0) throw new HomeComHeatPumpRefusalError(`HomeCom heat pump has no ${want} circuit ${circuitId}`);
1125
+ return found;
1126
+ }
1127
+ /** The accepted value of an option setting, or a refusal naming why the gateway would refuse. */
1128
+ function acceptedOption(s, what, mode) {
1129
+ if (s === null) throw new HomeComHeatPumpRefusalError(`HomeCom heat pump: ${what} is not available`);
1130
+ if (s.writeable === false || s.allowed.length === 0) throw new HomeComHeatPumpRefusalError(`HomeCom heat pump: ${what} is read-only on this gateway`);
1131
+ if (!s.allowed.includes(mode)) throw new HomeComHeatPumpRefusalError(`HomeCom heat pump: "${mode}" is not an accepted value of ${what} (${s.allowed.join(", ")})`);
1132
+ return mode;
1133
+ }
1134
+ /** The value of a range write, within the limits the gateway gave, or a refusal. */
1135
+ function acceptedRange(s, what, value) {
1136
+ if (s === null) throw new HomeComHeatPumpRefusalError(`HomeCom heat pump: ${what} is not available`);
1137
+ if (s.writeable === false) throw new HomeComHeatPumpRefusalError(`HomeCom heat pump: ${what} is read-only on this gateway`);
1138
+ if (!Number.isFinite(value)) throw new HomeComHeatPumpRefusalError(`HomeCom heat pump: ${what} needs a finite number`);
1139
+ if (s.min === null || s.max === null) throw new HomeComHeatPumpRefusalError(`HomeCom heat pump: ${what} has no limits from the gateway`);
1140
+ if (value < s.min || value > s.max) throw new HomeComHeatPumpRefusalError(`HomeCom heat pump: ${value} is outside ${what} limits ${s.min}..${s.max}`);
1141
+ return value;
1142
+ }
1143
+ var SYSTEM_OPS = {
1144
+ silent: "silentMode",
1145
+ away: "awayMode",
1146
+ additionalHeater: "additionalHeaterMode",
1147
+ holiday: "holidayMode"
1148
+ };
1149
+ function systemModeWrite(reading, which, mode) {
1150
+ return {
1151
+ op: SYSTEM_OPS[which],
1152
+ value: acceptedOption(reading[which], `${which} mode`, mode)
1153
+ };
1154
+ }
1155
+ var CIRCUIT_OPTION_OPS = {
1156
+ operationMode: "hcOperationMode",
1157
+ heatCoolMode: "hcHeatCoolMode",
1158
+ summerWinter: "hcSuWiSwitchMode"
1159
+ };
1160
+ function circuitOptionWrite(reading, circuitId, which, mode) {
1161
+ const circuit = requireCircuit(reading, circuitId, "hc");
1162
+ return {
1163
+ op: CIRCUIT_OPTION_OPS[which],
1164
+ circuitId,
1165
+ value: acceptedOption(circuit[which], `${circuitId} ${which}`, mode)
1166
+ };
1167
+ }
1168
+ var DHW_OPTION_OPS = {
1169
+ operationMode: "dhwOperationMode",
1170
+ level: "dhwTemperatureLevel"
1171
+ };
1172
+ function dhwOptionWrite(reading, circuitId, which, mode) {
1173
+ const circuit = requireCircuit(reading, circuitId, "dhw");
1174
+ return {
1175
+ op: DHW_OPTION_OPS[which],
1176
+ circuitId,
1177
+ value: acceptedOption(circuit[which], `${circuitId} ${which}`, mode)
1178
+ };
1179
+ }
1180
+ var CIRCUIT_RANGE_OPS = {
1181
+ comfort: "hcComfortSetpoint",
1182
+ eco: "hcEcoSetpoint",
1183
+ maxFlow: "hcMaxFlowTemp"
1184
+ };
1185
+ function circuitRangeWrite(reading, circuitId, which, value) {
1186
+ const circuit = requireCircuit(reading, circuitId, "hc");
1187
+ return {
1188
+ op: CIRCUIT_RANGE_OPS[which],
1189
+ circuitId,
1190
+ value: acceptedRange(circuit[which], `${circuitId} ${which}`, value)
1191
+ };
1192
+ }
1193
+ function dhwChargeDurationWrite(reading, circuitId, minutes) {
1194
+ return {
1195
+ op: "dhwChargeDuration",
1196
+ circuitId,
1197
+ value: acceptedRange(requireCircuit(reading, circuitId, "dhw").chargeDuration, `${circuitId} charge duration`, minutes)
1198
+ };
1199
+ }
1200
+ function extraHotWaterWrite(reading, circuitId) {
1201
+ const charge = requireCircuit(reading, circuitId, "dhw").charge;
1202
+ return {
1203
+ op: "dhwCharge",
1204
+ circuitId,
1205
+ value: acceptedOption(charge, `${circuitId} extra hot water`, "start")
1206
+ };
1207
+ }
726
1208
  var HOMECOM_PROBE_CLOCK = "/gateway/DateTime";
727
1209
  var HOMECOM_PROBE_FIRMWARE = "/gateway/versionFirmware";
728
1210
  var HYDRONIC_DROPPED = "heat-source, ventilation, pool and energy resources are not published on a cap";
@@ -1087,529 +1569,181 @@ function isRecord$2(value) {
1087
1569
  return typeof value === "object" && value !== null && !Array.isArray(value);
1088
1570
  }
1089
1571
  //#endregion
1090
- //#region src/homecom/homecom-heat-pump.ts
1572
+ //#region src/homecom/homecom-bridge-snapshot.ts
1091
1573
  /**
1092
- * Heat-pump resources of a hydronic gateway (D1026).
1093
- *
1094
- * UNVERIFIED: the paths below come from the layout of the public HomeCom Easy
1095
- * tree as the MIT `homecom_alt` library lists it, and were not checked against a
1096
- * live heat pump. They live in this one table so a wrong path is a one-line
1097
- * fix, and a resource the gateway does not answer reads as `null` (never 0),
1098
- * which the hero then simply does not draw.
1574
+ * The climate and water-heater view of a heat-pump gateway (k40, k30, icom), built from the
1575
+ * bridge reading, and the climate writes it accepts. The heat-pump view rides along when
1576
+ * the gateway has one (D1026).
1099
1577
  */
1100
- var HEAT_PUMP_PATHS = {
1101
- system: {
1102
- silent: "/heatSources/hs1/silentMode",
1103
- away: "/system/awayMode",
1104
- additionalHeater: "/heatSources/hs1/additionalHeaterMode",
1105
- holiday: "/system/holidayMode"
1106
- },
1107
- readings: {
1108
- thermalW: "/heatSources/hs1/heatPumpPower",
1109
- electricalW: "/heatSources/hs1/electricalPower",
1110
- compressorPct: "/heatSources/hs1/compressorModulation",
1111
- supply: "/heatSources/hs1/supplyTemperature",
1112
- return: "/heatSources/hs1/returnTemperature",
1113
- outdoor: "/system/sensors/temperatures/outdoor_t1",
1114
- thermalKwh: "/recordings/heatSources/total/thermalEnergy",
1115
- electricalKwh: "/recordings/heatSources/total/electricalEnergy",
1116
- pressureBar: "/heatSources/hs1/waterPressure",
1117
- starts: "/heatSources/hs1/numberOfStarts"
1118
- },
1119
- /**
1120
- * Resources only a heat pump's heat source has. A gateway that answers one of them with
1121
- * anything but 404 (a 403 or a 500 included) has a heat pump it will not let us read.
1122
- */
1123
- identity: {
1124
- heatPumpType: "/heatSources/hs1/heatPumpType",
1125
- heatPumpPower: "/heatSources/hs1/heatPumpPower"
1126
- },
1127
- circuit: {
1128
- operationMode: "operationMode",
1129
- heatCoolMode: "heatCoolMode",
1130
- summerWinter: "suWiSwitchMode",
1131
- comfort: "temperatureLevels/comfort2",
1132
- eco: "temperatureLevels/eco",
1133
- maxFlow: "maxSupplyTemperature",
1134
- current: "roomtemperature",
1135
- target: "currentRoomSetpoint"
1136
- },
1137
- dhw: {
1138
- operationMode: "operationMode",
1139
- level: "currentTemperatureLevel",
1140
- chargeDuration: "charge/duration",
1141
- charge: "charge",
1142
- current: "actualTemp",
1143
- target: "currentSetpoint"
1144
- }
1145
- };
1146
- /** A write the gateway would refuse, caught before it is sent. */
1147
- var HomeComHeatPumpRefusalError = class extends Error {
1148
- name = "HomeComHeatPumpRefusalError";
1149
- };
1150
- async function readHeatPump(input) {
1151
- const get = safeGet(input.get, input.onFailure);
1152
- const circuits = await Promise.all([...input.circuitIds.hc.map((id) => readHc(get, id)), ...input.circuitIds.dhw.map((id) => readDhwCircuit(get, id))]);
1153
- const r = HEAT_PUMP_PATHS.readings;
1154
- const s = HEAT_PUMP_PATHS.system;
1155
- const heatSourceAnswered = await anyAnswers(input.answers, input.onFailure, [HEAT_PUMP_PATHS.identity.heatPumpType, HEAT_PUMP_PATHS.identity.heatPumpPower]);
1156
- const [silent, away, additionalHeater, holiday] = await Promise.all([
1157
- setting(get, s.silent),
1158
- setting(get, s.away),
1159
- setting(get, s.additionalHeater),
1160
- setting(get, s.holiday)
1161
- ]);
1162
- const [thermalW, electricalW, compressorPct, supply, ret, outdoor] = await Promise.all([
1163
- num(get, r.thermalW),
1164
- num(get, r.electricalW),
1165
- num(get, r.compressorPct),
1166
- num(get, r.supply),
1167
- num(get, r.return),
1168
- num(get, r.outdoor)
1169
- ]);
1170
- const [thermalKwh, electricalKwh, pressureBar, starts] = await Promise.all([
1171
- num(get, r.thermalKwh),
1172
- num(get, r.electricalKwh),
1173
- num(get, r.pressureBar),
1174
- num(get, r.starts)
1175
- ]);
1176
- return {
1177
- circuits,
1178
- heatSourceAnswered,
1179
- silent,
1180
- away,
1181
- additionalHeater,
1182
- holiday,
1183
- thermalW,
1184
- electricalW,
1185
- compressorPct,
1186
- supply,
1187
- return: ret,
1188
- outdoor,
1189
- thermalKwh,
1190
- electricalKwh,
1191
- pressureBar,
1192
- starts
1193
- };
1194
- }
1195
- async function anyAnswers(answers, onFailure, paths) {
1196
- if (answers === void 0) return false;
1197
- return (await Promise.all(paths.map(async (path) => {
1198
- try {
1199
- return await answers(path);
1200
- } catch (error) {
1201
- onFailure?.(path, error);
1202
- return false;
1203
- }
1204
- }))).some(Boolean);
1205
- }
1206
- /** Any failure of a heat-pump read (HTTP 4xx/5xx, a throw) is "not answered", never an error. */
1207
- function safeGet(get, onFailure) {
1208
- return async (path) => {
1209
- try {
1210
- return await get(path);
1211
- } catch (error) {
1212
- onFailure?.(path, error);
1213
- return null;
1214
- }
1215
- };
1578
+ function numberValue(s) {
1579
+ return s !== null && typeof s.value === "number" ? s.value : null;
1216
1580
  }
1217
- var SYSTEM_MODE_KEYS = [
1218
- "silent",
1219
- "away",
1220
- "additionalHeater",
1221
- "holiday"
1222
- ];
1223
- var HEAT_PUMP_ONLY_READINGS = [
1224
- "thermalW",
1225
- "electricalW",
1226
- "compressorPct"
1227
- ];
1228
- /**
1229
- * True when the gateway should be offered as a system device (`hp:0`).
1230
- *
1231
- * A system mode counts when the gateway answered it: silent, away, the
1232
- * additional heater, or holiday. An empty string is an answer (holiday often
1233
- * comes back empty); a path that did not answer is not. A heat-pump-only
1234
- * reading counts too (thermal power, electrical power, compressor modulation).
1235
- * So does a circuit whose `heatCoolMode` can be cooling, and a heat source that
1236
- * answers a heat-pump-only resource with any status but 404: some gateways
1237
- * refuse (403) or fail (500) the whole system tree and still serve the
1238
- * circuits. Supply, return, outdoor, pressure, starts and the energy meters do
1239
- * not count: a gas boiler's heat source answers flow, pressure and starts, and
1240
- * that is not a heat pump.
1241
- */
1242
- function hasSystemDevice(reading) {
1243
- if (SYSTEM_MODE_KEYS.some((key) => reading[key] !== null)) return true;
1244
- if (HEAT_PUMP_ONLY_READINGS.some((key) => reading[key] !== null)) return true;
1245
- if (reading.heatSourceAnswered) return true;
1246
- return reading.circuits.some((c) => c.kind === "hc" && offersCooling(c.heatCoolMode));
1581
+ function textValue(s) {
1582
+ return s !== null && typeof s.value === "string" && s.value.length > 0 ? s.value : null;
1247
1583
  }
1248
1584
  /**
1249
- * A boiler circuit can carry a `heatCoolMode` that only ever says heating, so the mode alone
1250
- * is no proof. A circuit that can switch to (or already is in) cooling is a heat pump's.
1585
+ * Cooling writes the cooling setpoint; manual writes the manual room setpoint; auto writes the
1586
+ * temperature level (comfort or eco) the circuit runs on now. The library reads no active level
1587
+ * for a circuit, so it is the one level whose temperature equals the current room setpoint;
1588
+ * none or both matching is unknown, never a guess.
1251
1589
  */
1252
- function offersCooling(setting) {
1253
- if (setting === null) return false;
1254
- return setting.value === "cooling" || setting.allowed.includes("cooling");
1255
- }
1256
- /** Which heat-pump paths this gateway answered. One log per gateway corrects the path table. */
1257
- function heatPumpPathCensus(reading) {
1258
- const rows = [
1259
- ...systemPathRows(reading),
1260
- ...readingPathRows(reading),
1261
- ...reading.circuits.flatMap(circuitPathRows)
1262
- ];
1263
- const answered = [];
1264
- const unanswered = [];
1265
- for (const row of rows) if (row.present) answered.push(row.path);
1266
- else unanswered.push(row.path);
1267
- return {
1268
- answered,
1269
- unanswered
1590
+ function activeSetpoint(hc) {
1591
+ if (circuitIsCooling(hc)) return {
1592
+ kind: "setting",
1593
+ op: "hcCoolingRoomSetpoint",
1594
+ what: `${hc.id} cooling setpoint`,
1595
+ setting: hc.coolingRoomSetpoint
1270
1596
  };
1271
- }
1272
- function systemPathRows(reading) {
1273
- const s = HEAT_PUMP_PATHS.system;
1274
- return [
1275
- {
1276
- path: s.silent,
1277
- present: reading.silent !== null
1278
- },
1279
- {
1280
- path: s.away,
1281
- present: reading.away !== null
1282
- },
1283
- {
1284
- path: s.additionalHeater,
1285
- present: reading.additionalHeater !== null
1286
- },
1287
- {
1288
- path: s.holiday,
1289
- present: reading.holiday !== null
1290
- }
1291
- ];
1292
- }
1293
- function readingPathRows(reading) {
1294
- const r = HEAT_PUMP_PATHS.readings;
1295
- return [
1296
- {
1297
- path: r.thermalW,
1298
- present: reading.thermalW !== null
1299
- },
1300
- {
1301
- path: r.electricalW,
1302
- present: reading.electricalW !== null
1303
- },
1304
- {
1305
- path: r.compressorPct,
1306
- present: reading.compressorPct !== null
1307
- },
1308
- {
1309
- path: r.supply,
1310
- present: reading.supply !== null
1311
- },
1312
- {
1313
- path: r.return,
1314
- present: reading.return !== null
1315
- },
1316
- {
1317
- path: r.outdoor,
1318
- present: reading.outdoor !== null
1319
- },
1320
- {
1321
- path: r.thermalKwh,
1322
- present: reading.thermalKwh !== null
1323
- },
1324
- {
1325
- path: r.electricalKwh,
1326
- present: reading.electricalKwh !== null
1327
- },
1328
- {
1329
- path: r.pressureBar,
1330
- present: reading.pressureBar !== null
1331
- },
1332
- {
1333
- path: r.starts,
1334
- present: reading.starts !== null
1335
- }
1336
- ];
1337
- }
1338
- function circuitPathRows(circuit) {
1339
- if (circuit.kind === "dhw") return dhwPathRows(circuit);
1340
- return hcPathRows(circuit);
1341
- }
1342
- function hcPathRows(circuit) {
1343
- const base = `/heatingCircuits/${circuit.id}`;
1344
- const p = HEAT_PUMP_PATHS.circuit;
1345
- return [
1346
- {
1347
- path: `${base}/${p.operationMode}`,
1348
- present: circuit.operationMode !== null
1349
- },
1350
- {
1351
- path: `${base}/${p.heatCoolMode}`,
1352
- present: circuit.heatCoolMode !== null
1353
- },
1354
- {
1355
- path: `${base}/${p.summerWinter}`,
1356
- present: circuit.summerWinter !== null
1357
- },
1358
- {
1359
- path: `${base}/${p.comfort}`,
1360
- present: circuit.comfort !== null
1361
- },
1362
- {
1363
- path: `${base}/${p.eco}`,
1364
- present: circuit.eco !== null
1365
- },
1366
- {
1367
- path: `${base}/${p.maxFlow}`,
1368
- present: circuit.maxFlow !== null
1369
- },
1370
- {
1371
- path: `${base}/${p.current}`,
1372
- present: circuit.currentTemp !== null
1373
- },
1374
- {
1375
- path: `${base}/${p.target}`,
1376
- present: circuit.targetTemp !== null
1377
- }
1378
- ];
1379
- }
1380
- function dhwPathRows(circuit) {
1381
- const base = `/dhwCircuits/${circuit.id}`;
1382
- const p = HEAT_PUMP_PATHS.dhw;
1383
- return [
1384
- {
1385
- path: `${base}/${p.operationMode}`,
1386
- present: circuit.operationMode !== null
1387
- },
1388
- {
1389
- path: `${base}/${p.level}`,
1390
- present: circuit.level !== null
1391
- },
1392
- {
1393
- path: `${base}/${p.chargeDuration}`,
1394
- present: circuit.chargeDuration !== null
1395
- },
1396
- {
1397
- path: `${base}/${p.charge}`,
1398
- present: circuit.charge !== null
1399
- },
1400
- {
1401
- path: `${base}/${p.current}`,
1402
- present: circuit.currentTemp !== null
1403
- },
1404
- {
1405
- path: `${base}/${p.target}`,
1406
- present: circuit.targetTemp !== null
1407
- }
1408
- ];
1409
- }
1410
- async function readHc(get, id) {
1411
- const base = `/heatingCircuits/${id}`;
1412
- const p = HEAT_PUMP_PATHS.circuit;
1413
- const [operationMode, heatCoolMode, summerWinter, comfort, eco, maxFlow, current, target] = await Promise.all([
1414
- setting(get, `${base}/${p.operationMode}`),
1415
- setting(get, `${base}/${p.heatCoolMode}`),
1416
- setting(get, `${base}/${p.summerWinter}`),
1417
- setting(get, `${base}/${p.comfort}`),
1418
- setting(get, `${base}/${p.eco}`),
1419
- setting(get, `${base}/${p.maxFlow}`),
1420
- num(get, `${base}/${p.current}`),
1421
- num(get, `${base}/${p.target}`)
1422
- ]);
1423
- return {
1424
- id,
1425
- kind: "hc",
1426
- currentTemp: current,
1427
- targetTemp: target,
1428
- operationMode,
1429
- heatCoolMode,
1430
- summerWinter,
1431
- comfort,
1432
- eco,
1433
- maxFlow,
1434
- level: null,
1435
- chargeDuration: null,
1436
- charge: null
1597
+ if (textValue(hc.operationMode) !== "auto") return {
1598
+ kind: "setting",
1599
+ op: "hcManualRoomSetpoint",
1600
+ what: `${hc.id} setpoint`,
1601
+ setting: hc.manualRoomSetpoint
1437
1602
  };
1438
- }
1439
- async function readDhwCircuit(get, id) {
1440
- const base = `/dhwCircuits/${id}`;
1441
- const p = HEAT_PUMP_PATHS.dhw;
1442
- const [operationMode, level, chargeDuration, charge, current, target] = await Promise.all([
1443
- setting(get, `${base}/${p.operationMode}`),
1444
- setting(get, `${base}/${p.level}`),
1445
- setting(get, `${base}/${p.chargeDuration}`),
1446
- setting(get, `${base}/${p.charge}`),
1447
- num(get, `${base}/${p.current}`),
1448
- num(get, `${base}/${p.target}`)
1449
- ]);
1450
- return {
1451
- id,
1452
- kind: "dhw",
1453
- currentTemp: current,
1454
- targetTemp: target,
1455
- operationMode,
1456
- heatCoolMode: null,
1457
- summerWinter: null,
1458
- comfort: null,
1459
- eco: null,
1460
- maxFlow: null,
1461
- level,
1462
- chargeDuration,
1463
- charge
1603
+ const current = numberValue(hc.currentRoomSetpoint);
1604
+ const comfort = current !== null && numberValue(hc.comfortSetpoint) === current;
1605
+ if (comfort === (current !== null && numberValue(hc.ecoSetpoint) === current)) return {
1606
+ kind: "unknown",
1607
+ reason: `HomeCom ${hc.id}: the active level in auto mode cannot be told from the reading`
1608
+ };
1609
+ return comfort ? {
1610
+ kind: "setting",
1611
+ op: "hcComfortSetpoint",
1612
+ what: `${hc.id} comfort level`,
1613
+ setting: hc.comfortSetpoint
1614
+ } : {
1615
+ kind: "setting",
1616
+ op: "hcEcoSetpoint",
1617
+ what: `${hc.id} eco level`,
1618
+ setting: hc.ecoSetpoint
1464
1619
  };
1465
1620
  }
1466
- async function setting(get, path) {
1467
- const envelope = readEnvelope(await get(path));
1468
- return envelope === null ? null : settingOf(path, envelope);
1469
- }
1470
- async function num(get, path) {
1471
- const envelope = readEnvelope(await get(path));
1472
- return envelope !== null && typeof envelope.value === "number" && Number.isFinite(envelope.value) ? envelope.value : null;
1473
- }
1474
- function settingOf(path, envelope) {
1621
+ function climateCircuit(hc) {
1622
+ const cooling = circuitIsCooling(hc);
1623
+ const active = activeSetpoint(hc);
1624
+ const limits = active.kind === "setting" ? active.setting : null;
1625
+ const modes = hvacModesFromAllowed(hc.operationMode?.allowed ?? []);
1475
1626
  return {
1476
- path,
1477
- value: envelope.value,
1478
- min: envelope.min,
1479
- max: envelope.max,
1480
- allowed: envelope.allowed
1627
+ id: hc.id,
1628
+ name: heatingCircuitName(hc.id, hc.name),
1629
+ operationMode: textValue(hc.operationMode) ?? "off",
1630
+ setpoint: cooling ? numberValue(hc.coolingRoomSetpoint) ?? numberValue(hc.currentRoomSetpoint) : numberValue(hc.currentRoomSetpoint) ?? numberValue(hc.manualRoomSetpoint),
1631
+ currentRoomTemp: hc.roomTemp,
1632
+ minTemp: limits?.min ?? null,
1633
+ maxTemp: limits?.max ?? null,
1634
+ currentHumidity: hc.humidity,
1635
+ season: cooling ? "cooling" : textValue(hc.currentSuWiMode),
1636
+ fanMode: "",
1637
+ availableFanModes: [],
1638
+ swingVertical: null,
1639
+ swingHorizontal: null,
1640
+ availableModes: modes.length > 0 ? modes : [...HYDRONIC_MODES]
1481
1641
  };
1482
1642
  }
1483
- function heatPumpStatusFrom(reading, now) {
1643
+ function waterHeater(dhw) {
1644
+ const target = dhwTargetSetting(dhw);
1645
+ const modes = dhw.operationMode?.allowed ?? [];
1484
1646
  return {
1485
- circuits: reading.circuits.map(circuitOf),
1486
- system: {
1487
- silent: optionOf(reading.silent),
1488
- away: optionOf(reading.away),
1489
- additionalHeater: optionOf(reading.additionalHeater),
1490
- holiday: optionOf(reading.holiday)
1491
- },
1492
- power: {
1493
- thermalW: reading.thermalW,
1494
- electricalW: reading.electricalW,
1495
- compressorPct: reading.compressorPct
1496
- },
1497
- temperatures: {
1498
- supply: reading.supply,
1499
- return: reading.return,
1500
- outdoor: reading.outdoor
1501
- },
1502
- energy: {
1503
- thermalKwh: reading.thermalKwh,
1504
- electricalKwh: reading.electricalKwh
1505
- },
1506
- waterPressureBar: reading.pressureBar,
1507
- starts: reading.starts,
1508
- lastFetchedAt: now
1647
+ currentTemp: dhw.actualTemp,
1648
+ targetTemp: numberValue(target),
1649
+ operationMode: textValue(dhw.operationMode),
1650
+ availableModes: [...modes],
1651
+ minTemp: target?.min ?? null,
1652
+ maxTemp: target?.max ?? null,
1653
+ circuitId: dhw.id
1509
1654
  };
1510
1655
  }
1511
- function circuitOf(c) {
1512
- const kind = kindOf(c);
1656
+ function bridgeSnapshot(input) {
1657
+ const { reading } = input;
1658
+ const firstDhw = reading.dhwCircuits[0];
1659
+ if (reading.dhwCircuits.length > 1) {
1660
+ const extra = reading.dhwCircuits.slice(1).map((c) => c.id);
1661
+ input.logDrop(`extra DHW circuits are on the heat pump only: ${extra.join(", ")}`);
1662
+ }
1663
+ for (const [path, outcome] of Object.entries(reading.heatSourceProbe)) if (outcome === "transient") input.logTransientProbe(path);
1664
+ const heatPump = heatPumpReadingFrom(reading);
1513
1665
  return {
1514
- id: c.id,
1515
- kind,
1516
- label: labelOf(c),
1517
- icon: kind === "dhw" ? "droplets" : kind === "cooling" ? "snowflake" : "flame",
1518
- currentTemp: c.currentTemp,
1519
- targetTemp: c.targetTemp,
1520
- mode: modeValue(c.operationMode),
1521
- active: null,
1522
- operationMode: optionOf(c.operationMode),
1523
- heatCoolMode: optionOf(c.heatCoolMode),
1524
- summerWinter: optionOf(c.summerWinter),
1525
- comfortSetpoint: rangeOf(c.comfort),
1526
- ecoSetpoint: rangeOf(c.eco),
1527
- maxFlowTemp: rangeOf(c.maxFlow),
1528
- temperatureLevel: optionOf(c.level),
1529
- chargeDuration: rangeOf(c.chargeDuration),
1530
- extraHotWater: c.charge === null ? null : textOf$1(c.charge) === "start"
1666
+ gateway: {
1667
+ gatewayId: input.gatewayId,
1668
+ name: input.gatewayId,
1669
+ model: input.deviceType
1670
+ },
1671
+ circuits: reading.heatingCircuits.map(climateCircuit),
1672
+ dhw: firstDhw === void 0 ? null : waterHeater(firstDhw),
1673
+ deviceType: input.deviceType,
1674
+ family: "hydronic",
1675
+ bridge: reading,
1676
+ ...hasSystemDevice(heatPump) ? { heatPump } : {}
1531
1677
  };
1532
1678
  }
1533
- function kindOf(c) {
1534
- if (c.kind === "dhw") return "dhw";
1535
- return textOf$1(c.heatCoolMode) === "cooling" ? "cooling" : "heating";
1536
- }
1537
- function labelOf(c) {
1538
- const n = c.id.replace(/^\D+/, "");
1539
- if (c.kind === "dhw") return n === "1" || n === "" ? "Hot water" : `Hot water ${n}`;
1540
- return n === "" ? "Heating circuit" : `Heating circuit ${n}`;
1541
- }
1542
- function textOf$1(s) {
1543
- return s !== null && typeof s.value === "string" && s.value.length > 0 ? s.value : null;
1679
+ function findHc(reading, circuitId) {
1680
+ const found = reading.heatingCircuits.find((c) => c.id === circuitId);
1681
+ if (found === void 0) throw new HomeComHeatPumpRefusalError(`HomeCom has no heating circuit ${circuitId}`);
1682
+ return found;
1544
1683
  }
1545
- /**
1546
- * A mode the gateway answered with an empty value is off (or "none" when the
1547
- * gateway did not list off). A mode it did not answer at all stays null.
1548
- */
1549
- function modeValue(s) {
1550
- if (s === null || typeof s.value !== "string") return null;
1551
- if (s.value.length > 0) return s.value;
1552
- return s.allowed.includes("off") ? "off" : "none";
1684
+ function firstDhw(reading) {
1685
+ const found = reading.dhwCircuits[0];
1686
+ if (found === void 0) throw new HomeComHeatPumpRefusalError("HomeCom has no DHW circuit");
1687
+ return found;
1553
1688
  }
1554
- function optionOf(s) {
1555
- if (s === null) return null;
1689
+ function bridgeSetpointWrite(reading, circuitId, temp) {
1690
+ const active = activeSetpoint(findHc(reading, circuitId));
1691
+ if (active.kind === "unknown") throw new HomeComHeatPumpRefusalError(active.reason);
1692
+ const value = acceptedRange(active.setting, active.what, temp);
1556
1693
  return {
1557
- value: modeValue(s),
1558
- allowed: [...s.allowed]
1694
+ op: active.op,
1695
+ circuitId,
1696
+ value
1559
1697
  };
1560
1698
  }
1561
- function rangeOf(s) {
1562
- if (s === null) return null;
1699
+ /** The gateway's token for an HVAC mode: heat is `manual`; cool is a heat/cool mode. */
1700
+ function operationToken(mode) {
1701
+ switch (mode) {
1702
+ case "Heat":
1703
+ case "heat": return "manual";
1704
+ case "Auto": return "auto";
1705
+ case "Off": return "off";
1706
+ default: return mode;
1707
+ }
1708
+ }
1709
+ function bridgeModeWrite(reading, circuitId, mode) {
1710
+ const hc = findHc(reading, circuitId);
1711
+ if (mode === "cool" || mode === "Cool") {
1712
+ const token = (hc.heatCoolMode?.allowed ?? []).includes("cool") ? "cool" : "cooling";
1713
+ return {
1714
+ op: "hcHeatCoolMode",
1715
+ circuitId,
1716
+ value: acceptedOption(hc.heatCoolMode, `${circuitId} heat/cool mode`, token)
1717
+ };
1718
+ }
1563
1719
  return {
1564
- value: typeof s.value === "number" && Number.isFinite(s.value) ? s.value : null,
1565
- min: s.min,
1566
- max: s.max,
1567
- step: null
1720
+ op: "hcOperationMode",
1721
+ circuitId,
1722
+ value: acceptedOption(hc.operationMode, `${circuitId} mode`, operationToken(mode))
1568
1723
  };
1569
1724
  }
1570
- function requireCircuit(reading, circuitId, want) {
1571
- const found = reading.circuits.find((c) => c.id === circuitId && c.kind === want);
1572
- if (found === void 0) throw new HomeComHeatPumpRefusalError(`HomeCom heat pump has no ${want} circuit ${circuitId}`);
1573
- return found;
1574
- }
1575
- function optionWrite(s, what, mode) {
1576
- if (s === null) throw new HomeComHeatPumpRefusalError(`HomeCom heat pump: ${what} is not available`);
1577
- if (s.allowed.length === 0) throw new HomeComHeatPumpRefusalError(`HomeCom heat pump: ${what} is read-only on this gateway`);
1578
- if (!s.allowed.includes(mode)) throw new HomeComHeatPumpRefusalError(`HomeCom heat pump: "${mode}" is not an accepted value of ${what} (${s.allowed.join(", ")})`);
1725
+ /** The hot-water target is the temperature of the level the tank runs on now. */
1726
+ function bridgeDhwTargetWrite(reading, temp) {
1727
+ const dhw = firstDhw(reading);
1728
+ const level = textValue(dhw.currentTemperatureLevel);
1729
+ if (level === null || level === "off" || dhw.temperatureLevels[level] === void 0) throw new HomeComHeatPumpRefusalError("HomeCom DHW: setpoint not supported");
1730
+ const value = acceptedRange(dhw.temperatureLevels[level] ?? null, `${dhw.id} ${level}`, temp);
1579
1731
  return {
1580
- path: s.path,
1581
- value: mode
1732
+ op: "dhwLevelTemp",
1733
+ circuitId: dhw.id,
1734
+ level,
1735
+ value
1582
1736
  };
1583
1737
  }
1584
- function rangeWrite(s, what, value) {
1585
- if (s === null) throw new HomeComHeatPumpRefusalError(`HomeCom heat pump: ${what} is not available`);
1586
- if (!Number.isFinite(value)) throw new HomeComHeatPumpRefusalError(`HomeCom heat pump: ${what} needs a finite number`);
1587
- if (s.min === null || s.max === null) throw new HomeComHeatPumpRefusalError(`HomeCom heat pump: ${what} has no limits from the gateway`);
1588
- if (value < s.min || value > s.max) throw new HomeComHeatPumpRefusalError(`HomeCom heat pump: ${value} is outside ${what} limits ${s.min}..${s.max}`);
1738
+ function bridgeDhwModeWrite(reading, mode) {
1739
+ const dhw = firstDhw(reading);
1740
+ const value = acceptedOption(dhw.operationMode, `${dhw.id} mode`, mode);
1589
1741
  return {
1590
- path: s.path,
1742
+ op: "dhwOperationMode",
1743
+ circuitId: dhw.id,
1591
1744
  value
1592
1745
  };
1593
1746
  }
1594
- function systemModeWrite(reading, which, mode) {
1595
- return optionWrite(reading[which], `${which} mode`, mode);
1596
- }
1597
- function circuitOptionWrite(reading, circuitId, which, mode) {
1598
- return optionWrite(requireCircuit(reading, circuitId, "hc")[which], `${circuitId} ${which}`, mode);
1599
- }
1600
- function dhwOptionWrite(reading, circuitId, which, mode) {
1601
- return optionWrite(requireCircuit(reading, circuitId, "dhw")[which], `${circuitId} ${which}`, mode);
1602
- }
1603
- function circuitRangeWrite(reading, circuitId, which, value) {
1604
- return rangeWrite(requireCircuit(reading, circuitId, "hc")[which], `${circuitId} ${which}`, value);
1605
- }
1606
- function dhwChargeDurationWrite(reading, circuitId, minutes) {
1607
- return rangeWrite(requireCircuit(reading, circuitId, "dhw").chargeDuration, `${circuitId} charge duration`, minutes);
1608
- }
1609
- function extraHotWaterWrite(reading, circuitId) {
1610
- const charge = requireCircuit(reading, circuitId, "dhw").charge;
1611
- return optionWrite(charge, `${circuitId} extra hot water`, "start");
1612
- }
1613
1747
  //#endregion
1614
1748
  //#region src/homecom/homecom-oauth.ts
1615
1749
  /**
@@ -1721,7 +1855,8 @@ function createHomeComApiClient(config, gatewayId, deps = {}) {
1721
1855
  let last = null;
1722
1856
  let refreshTail = Promise.resolve();
1723
1857
  const dropped = /* @__PURE__ */ new Set();
1724
- let heatPumpPathsLogged = false;
1858
+ const bridgeFailuresLogged = /* @__PURE__ */ new Set();
1859
+ const bridge = deps.bridge;
1725
1860
  const base = homeComApiBase(config.region);
1726
1861
  function authHeaders() {
1727
1862
  return {
@@ -1790,17 +1925,6 @@ function createHomeComApiClient(config, gatewayId, deps = {}) {
1790
1925
  if (!res.ok) throw new Error(`HomeCom API ${path} failed: HTTP ${res.status}`);
1791
1926
  return res.body;
1792
1927
  }
1793
- /**
1794
- * Evidence that the resource exists: a 2xx, or the 403 the gateway gives a resource it
1795
- * serves but will not show. 404 is "absent". Anything else (429, 5xx) is a transient
1796
- * failure, so it is NOT evidence either way: it is logged and counts as unknown.
1797
- */
1798
- async function resourceAnswers(path) {
1799
- const res = await authed(homeComResourceUrl(gatewayId, path), { method: "GET" });
1800
- if (res.ok || res.status === 403) return true;
1801
- if (res.status !== 404) logHeatPumpFailure(path, /* @__PURE__ */ new Error(`HTTP ${res.status}`));
1802
- return false;
1803
- }
1804
1928
  async function putWrite(write) {
1805
1929
  const res = await authed(homeComResourceUrl(gatewayId, write.path), {
1806
1930
  method: "PUT",
@@ -1866,58 +1990,95 @@ function createHomeComApiClient(config, gatewayId, deps = {}) {
1866
1990
  }
1867
1991
  async function fetchSystem() {
1868
1992
  const resolved = await resolveDeviceType();
1869
- const withHeatPump = await attachHeatPump(await readPointtSnapshot({
1870
- deviceType: resolved.deviceType,
1871
- family: resolved.family,
1993
+ const snapshot = await readSnapshot(resolved.deviceType, resolved.family);
1994
+ last = snapshot;
1995
+ return snapshot;
1996
+ }
1997
+ async function readSnapshot(type, family) {
1998
+ if (isBridgeDeviceType(type)) if (bridge !== void 0) {
1999
+ const reading = await readThroughBridge(bridge, type);
2000
+ if (reading !== null) return bridgeSnapshot({
2001
+ gatewayId,
2002
+ deviceType: type,
2003
+ reading,
2004
+ logDrop,
2005
+ logTransientProbe
2006
+ });
2007
+ } else logDrop("the HomeCom bridge is not available (no Python): the heat pump is not read");
2008
+ return readPointtSnapshot({
2009
+ deviceType: type,
2010
+ family,
1872
2011
  gatewayId,
1873
2012
  get: getResource,
1874
2013
  logDrop
1875
- }));
1876
- last = withHeatPump;
1877
- return withHeatPump;
1878
- }
1879
- async function attachHeatPump(snapshot) {
1880
- if (snapshot.family !== "hydronic" || snapshot.deviceType === "rrc2") return snapshot;
1881
- const dhwId = snapshot.dhw?.circuitId;
1882
- const heatPump = await readHeatPump({
1883
- get: getResource,
1884
- onFailure: logHeatPumpFailure,
1885
- answers: resourceAnswers,
1886
- circuitIds: {
1887
- hc: snapshot.circuits.map((circuit) => circuit.id),
1888
- dhw: dhwId === void 0 ? [] : [dhwId]
1889
- }
1890
2014
  });
1891
- logHeatPumpPaths(heatPump);
1892
- return hasSystemDevice(heatPump) ? {
1893
- ...snapshot,
1894
- heatPump
1895
- } : snapshot;
1896
- }
1897
- function logHeatPumpPaths(reading) {
1898
- if (heatPumpPathsLogged) return;
1899
- heatPumpPathsLogged = true;
1900
- const census = heatPumpPathCensus(reading);
1901
- logger?.info("HomeCom heat-pump paths", {
1902
- tags: { gatewayId },
1903
- meta: {
1904
- answered: census.answered,
1905
- unanswered: census.unanswered
2015
+ }
2016
+ /**
2017
+ * A failed bridge read must not take the climate and water-heater devices down, nor change
2018
+ * the heat pump's shape: anything but an authentication failure keeps the last bridge
2019
+ * reading (so an adopted heat pump keeps its values and discovery its devices), or — before
2020
+ * any bridge read has succeeded — falls back to the plain climate resources. Named once per
2021
+ * failure kind until a read succeeds again.
2022
+ */
2023
+ async function readThroughBridge(from, type) {
2024
+ try {
2025
+ const reading = await bridgeCall((t) => from.read(bridgeTarget(t, type)));
2026
+ bridgeFailuresLogged.clear();
2027
+ return reading;
2028
+ } catch (error) {
2029
+ if (!(error instanceof HomeComBridgeError)) throw error;
2030
+ const kept = last?.bridge ?? null;
2031
+ if (!bridgeFailuresLogged.has(error.kind)) {
2032
+ bridgeFailuresLogged.add(error.kind);
2033
+ logger?.warn(kept === null ? "HomeCom bridge read failed; the climate resources are read until it answers" : "HomeCom bridge read failed; the last bridge reading is kept until it answers", {
2034
+ tags: { gatewayId },
2035
+ meta: {
2036
+ kind: error.kind,
2037
+ error: error.message
2038
+ }
2039
+ });
1906
2040
  }
1907
- });
2041
+ return kept;
2042
+ }
1908
2043
  }
1909
- function logHeatPumpFailure(path, error) {
1910
- const key = `heat-pump-read:${path}`;
2044
+ function logTransientProbe(path) {
2045
+ const key = `probe:${path}`;
1911
2046
  if (dropped.has(key)) return;
1912
2047
  dropped.add(key);
1913
- logger?.warn("HomeCom heat-pump resource could not be read; it is skipped", {
2048
+ logger?.warn("HomeCom heat-source probe failed transiently; it does not count as evidence", {
1914
2049
  tags: { gatewayId },
1915
- meta: {
1916
- path,
1917
- error: error instanceof Error ? error.message : String(error)
1918
- }
2050
+ meta: { path }
1919
2051
  });
1920
2052
  }
2053
+ function bridgeTarget(t, type) {
2054
+ return {
2055
+ token: t,
2056
+ gatewayId,
2057
+ deviceType: type
2058
+ };
2059
+ }
2060
+ /** One bridge run; an `auth` answer refreshes the token once and runs it again. */
2061
+ async function bridgeCall(run) {
2062
+ const used = token;
2063
+ try {
2064
+ return await run(used);
2065
+ } catch (error) {
2066
+ if (!(error instanceof HomeComBridgeError) || error.kind !== "auth") throw error;
2067
+ if (!refreshOnUnauthorized) throw new HomeComAuthError("token expired");
2068
+ }
2069
+ await refreshIfStale(used);
2070
+ try {
2071
+ return await run(token);
2072
+ } catch (error) {
2073
+ if (error instanceof HomeComBridgeError && error.kind === "auth") throw new HomeComAuthError("token expired after refresh");
2074
+ throw error;
2075
+ }
2076
+ }
2077
+ async function bridgeWrite(snapshot, write) {
2078
+ const type = snapshot.deviceType;
2079
+ if (bridge === void 0 || !isBridgeDeviceType(type)) throw new Error("HomeCom heat-pump writes need the HomeCom bridge (embedded Python).");
2080
+ await bridgeCall((t) => bridge.write(bridgeTarget(t, type), write));
2081
+ }
1921
2082
  async function probePath(path) {
1922
2083
  const res = await authed(homeComResourceUrl(gatewayId, path), {
1923
2084
  method: "GET",
@@ -1945,16 +2106,36 @@ function createHomeComApiClient(config, gatewayId, deps = {}) {
1945
2106
  throw new Error(`HomeCom gateway ${gatewayId} did not answer /gateway/DateTime or /gateway/versionFirmware.`);
1946
2107
  },
1947
2108
  async setCircuitSetpoint(circuitId, temp) {
1948
- await putWrite(circuitSetpointWrite(await requireSnapshot(), circuitId, temp));
2109
+ const snapshot = await requireSnapshot();
2110
+ if (snapshot.bridge !== void 0) {
2111
+ await bridgeWrite(snapshot, bridgeSetpointWrite(snapshot.bridge, circuitId, temp));
2112
+ return;
2113
+ }
2114
+ await putWrite(circuitSetpointWrite(snapshot, circuitId, temp));
1949
2115
  },
1950
2116
  async setCircuitMode(circuitId, mode) {
1951
- for (const write of circuitModeWrites(await requireSnapshot(), circuitId, mode)) await putWrite(write);
2117
+ const snapshot = await requireSnapshot();
2118
+ if (snapshot.bridge !== void 0) {
2119
+ await bridgeWrite(snapshot, bridgeModeWrite(snapshot.bridge, circuitId, mode));
2120
+ return;
2121
+ }
2122
+ for (const write of circuitModeWrites(snapshot, circuitId, mode)) await putWrite(write);
1952
2123
  },
1953
2124
  async setDhwTarget(temp) {
1954
- await putWrite(dhwTargetWrite(await requireSnapshot(), temp));
2125
+ const snapshot = await requireSnapshot();
2126
+ if (snapshot.bridge !== void 0) {
2127
+ await bridgeWrite(snapshot, bridgeDhwTargetWrite(snapshot.bridge, temp));
2128
+ return;
2129
+ }
2130
+ await putWrite(dhwTargetWrite(snapshot, temp));
1955
2131
  },
1956
2132
  async setDhwMode(mode) {
1957
- await putWrite(dhwModeWrite(await requireSnapshot(), mode));
2133
+ const snapshot = await requireSnapshot();
2134
+ if (snapshot.bridge !== void 0) {
2135
+ await bridgeWrite(snapshot, bridgeDhwModeWrite(snapshot.bridge, mode));
2136
+ return;
2137
+ }
2138
+ await putWrite(dhwModeWrite(snapshot, mode));
1958
2139
  },
1959
2140
  async setFanMode(_circuitId, fanMode) {
1960
2141
  const snapshot = await requireSnapshot();
@@ -1972,7 +2153,8 @@ function createHomeComApiClient(config, gatewayId, deps = {}) {
1972
2153
  await putWrite(swingWrite(snapshot, "horizontal", on));
1973
2154
  },
1974
2155
  async writeHeatPump(writes) {
1975
- for (const write of writes) await putWrite(write);
2156
+ const snapshot = await requireSnapshot();
2157
+ for (const write of writes) await bridgeWrite(snapshot, write);
1976
2158
  },
1977
2159
  async readResource(path) {
1978
2160
  const res = await authed(homeComResourceUrl(gatewayId, path), { method: "GET" });
@@ -2632,6 +2814,7 @@ var HomeComIntegrationManager = class {
2632
2814
  this.#onDisconnected = options.onDisconnected;
2633
2815
  this.#surfaceSink = options.surfaceSink;
2634
2816
  const fetchFn = options.fetchFn;
2817
+ const bridge = options.bridge;
2635
2818
  this.#onGatewayChosen = options.onGatewayChosen ?? (() => void 0);
2636
2819
  const onTokensRotated = async (tokens) => {
2637
2820
  this.#connection = {
@@ -2652,6 +2835,7 @@ var HomeComIntegrationManager = class {
2652
2835
  fetchFn,
2653
2836
  onTokensRotated,
2654
2837
  logger: this.#logger,
2838
+ ...bridge !== void 0 ? { bridge } : {},
2655
2839
  deviceType: this.#gateways.find((gateway) => gateway.gatewayId === gatewayId)?.deviceType
2656
2840
  }));
2657
2841
  this.#pollIntervalMs = options.pollIntervalMs ?? 6e4;
@@ -2775,6 +2959,7 @@ var HomeComClientRegistry = class {
2775
2959
  this.#onGatewayChosen = deps.onGatewayChosen ?? (() => void 0);
2776
2960
  const fetchFn = deps.fetchFn;
2777
2961
  const makeClient = deps.makeClient;
2962
+ const bridge = deps.bridge;
2778
2963
  this.#makeManager = deps.makeManager ?? ((opts) => new HomeComIntegrationManager({
2779
2964
  id: opts.integrationId,
2780
2965
  name: opts.name,
@@ -2787,6 +2972,7 @@ var HomeComClientRegistry = class {
2787
2972
  surfaceSink: homeComConnections,
2788
2973
  fetchFn,
2789
2974
  makeClient,
2975
+ ...bridge !== void 0 ? { bridge } : {},
2790
2976
  pollIntervalMs: 6e4
2791
2977
  }));
2792
2978
  }
@@ -2879,6 +3065,27 @@ var HomeComClientRegistry = class {
2879
3065
  }
2880
3066
  };
2881
3067
  //#endregion
3068
+ //#region src/homecom/homecom-python-paths.ts
3069
+ var WORKER = "homecom_worker.py";
3070
+ /**
3071
+ * Absolute path of the bundled `python/homecom_worker.py`: next to `dist/` in a deployed
3072
+ * package (one level up from a `dist/` chunk, two from `dist/homecom/`), two levels up from
3073
+ * `src/homecom/` in the workspace.
3074
+ */
3075
+ function resolveHomeComWorker(fromModuleUrl = require("url").pathToFileURL(__filename).href) {
3076
+ const here = (0, node_path.dirname)((0, node_url.fileURLToPath)(fromModuleUrl));
3077
+ const candidates = [
3078
+ (0, node_path.join)(here, "../python"),
3079
+ (0, node_path.join)(here, "../../python"),
3080
+ (0, node_path.join)(here, "python")
3081
+ ];
3082
+ for (const dir of candidates) {
3083
+ const file = (0, node_path.join)(dir, WORKER);
3084
+ if ((0, node_fs.existsSync)(file)) return file;
3085
+ }
3086
+ throw new Error(`provider-bosch: ${WORKER} not found. Searched:\n${candidates.join("\n")}`);
3087
+ }
3088
+ //#endregion
2882
3089
  //#region src/homecom/homecom-connection-test.ts
2883
3090
  var NO_GATEWAY = "This Bosch account has no HomeCom gateway.";
2884
3091
  function signInIncomplete(paths) {
@@ -3109,7 +3316,10 @@ var HomeComClimateDevice = class extends require_bosch_instances.BaseDevice {
3109
3316
  #circuitId;
3110
3317
  constructor(ctx) {
3111
3318
  const persisted = homeComClimateSchema.parse(ctx.persistedConfig ?? {});
3112
- super(ctx, homeComClimateSchema, { type: ctx.deviceMeta.type });
3319
+ super(ctx, homeComClimateSchema, {
3320
+ type: ctx.deviceMeta.type,
3321
+ role: require_bosch_instances.DeviceRole.ClimateHeatPumpCircuit
3322
+ });
3113
3323
  this.#brokerId = persisted.brokerId;
3114
3324
  this.#circuitId = persisted.circuitId;
3115
3325
  this.online = true;
@@ -3141,7 +3351,7 @@ var HomeComClimateDevice = class extends require_bosch_instances.BaseDevice {
3141
3351
  return client;
3142
3352
  }
3143
3353
  registerClimateCap() {
3144
- this.ctx.registerNativeCap(require_bosch_instances.climateControlCapability, {
3354
+ const provider = {
3145
3355
  getStatus: async () => {
3146
3356
  this.refreshFromSnapshot();
3147
3357
  return this.runtimeState.getCapState(CLIMATE_CAP) ?? COLD_START$2;
@@ -3176,7 +3386,9 @@ var HomeComClimateDevice = class extends require_bosch_instances.BaseDevice {
3176
3386
  await this.requireClient().setSwingHorizontal(this.#circuitId, on);
3177
3387
  this.refreshFromSnapshot();
3178
3388
  }
3179
- });
3389
+ };
3390
+ this.ctx.registerNativeCap(require_bosch_instances.climateControlCapability, provider);
3391
+ require_bosch_instances.registerPowerSwitch(this, require_bosch_instances.climateControlCapability, provider);
3180
3392
  }
3181
3393
  };
3182
3394
  function hvacModeToHomeCom(mode) {
@@ -3242,7 +3454,7 @@ var HomeComWaterHeaterDevice = class extends require_bosch_instances.BaseDevice
3242
3454
  return client;
3243
3455
  }
3244
3456
  registerWaterHeaterCap() {
3245
- this.ctx.registerNativeCap(require_bosch_instances.waterHeaterCapability, {
3457
+ const provider = {
3246
3458
  getStatus: async () => {
3247
3459
  this.refreshFromSnapshot();
3248
3460
  return this.runtimeState.getCapState(WH_CAP) ?? COLD_START$1;
@@ -3258,7 +3470,9 @@ var HomeComWaterHeaterDevice = class extends require_bosch_instances.BaseDevice
3258
3470
  setAway: async () => {
3259
3471
  throw new Error("HomeCom DHW: away mode not supported");
3260
3472
  }
3261
- });
3473
+ };
3474
+ this.ctx.registerNativeCap(require_bosch_instances.waterHeaterCapability, provider);
3475
+ require_bosch_instances.registerPowerSwitch(this, require_bosch_instances.waterHeaterCapability, provider);
3262
3476
  }
3263
3477
  };
3264
3478
  //#endregion
@@ -3391,7 +3605,7 @@ var HomeComHeatPumpDevice = class extends require_bosch_instances.BaseDevice {
3391
3605
  await client.writeHeatPump([write]);
3392
3606
  this.ctx.logger.info("HomeCom heat pump: value written", {
3393
3607
  tags: { deviceId: this.ctx.id },
3394
- meta: { path: write.path }
3608
+ meta: { op: write.op }
3395
3609
  });
3396
3610
  try {
3397
3611
  const snapshot = await client.fetchSystem();
@@ -3400,7 +3614,7 @@ var HomeComHeatPumpDevice = class extends require_bosch_instances.BaseDevice {
3400
3614
  this.ctx.logger.warn("HomeCom heat pump: value written, the refresh failed", {
3401
3615
  tags: { deviceId: this.ctx.id },
3402
3616
  meta: {
3403
- path: write.path,
3617
+ op: write.op,
3404
3618
  error: error instanceof Error ? error.message : String(error)
3405
3619
  }
3406
3620
  });
@@ -3471,7 +3685,9 @@ var BoschHomeComProviderAddon = class extends require_bosch_instances.BaseDevice
3471
3685
  };
3472
3686
  const persistTokens = buildHomeComTokenPersister(persistDeps);
3473
3687
  const persistGateway = buildHomeComGatewayPersister(persistDeps);
3688
+ const bridge = await this.createBridge();
3474
3689
  this.clients = new HomeComClientRegistry(this.ctx.logger, {
3690
+ ...bridge !== null ? { bridge } : {},
3475
3691
  onTokensRotated: (integrationId, tokens) => persistTokens(integrationId, tokens),
3476
3692
  onGatewayChosen: (integrationId, gatewayId) => void persistGateway(integrationId, gatewayId)
3477
3693
  });
@@ -3529,6 +3745,23 @@ var BoschHomeComProviderAddon = class extends require_bosch_instances.BaseDevice
3529
3745
  this.instances = null;
3530
3746
  await super.onShutdown();
3531
3747
  }
3748
+ /** The `homecom_alt` bridge on the embedded Python; null leaves k40/k30/icom on plain reads. */
3749
+ async createBridge() {
3750
+ try {
3751
+ const pythonPath = await this.ctx.deps.ensurePython();
3752
+ if (pythonPath === null) {
3753
+ this.ctx.logger.warn("HomeCom: embedded Python is unavailable, the heat pump is not read");
3754
+ return null;
3755
+ }
3756
+ return createHomeComBridge({
3757
+ pythonPath,
3758
+ scriptPath: resolveHomeComWorker()
3759
+ });
3760
+ } catch (err) {
3761
+ this.ctx.logger.warn("HomeCom: the Python bridge could not be prepared", { meta: { error: require_bosch_instances.errMsg(err) } });
3762
+ return null;
3763
+ }
3764
+ }
3532
3765
  requireClients() {
3533
3766
  if (!this.clients) throw new Error("HomeCom provider not initialised");
3534
3767
  return this.clients;