@camstack/addon-provider-bosch 0.1.43 → 0.1.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_bosch_instances = require("../bosch-instances-CppS1lTA.js");
3
- const require_account_integration_offline = require("../account-integration-offline-Ctzpu6DI.js");
2
+ const require_bosch_instances = require("../bosch-instances-DdklwoKS.js");
3
+ const require_account_integration_offline = require("../account-integration-offline-BiXbzZsg.js");
4
4
  let node_crypto = require("node:crypto");
5
5
  //#region src/homecom/config.ts
6
6
  var HOMECOM_BRANDS = ["bosch", "buderus"];
@@ -46,23 +46,22 @@ var HIDDEN_UNTIL_ADVANCED = {
46
46
  field: "__hidden__",
47
47
  equals: "yes"
48
48
  };
49
- var HOMECOM_CODE_HELP = "If the page does not finish, copy the address it stops on (it starts with com.bosch.tt.dashtt.pointt://app/login?code=) or only the code, and paste it in the box under the sign-in button.";
49
+ var HOMECOM_SIGN_IN_GUIDE = {
50
+ i18nPrefix: "homecom.s_account",
51
+ keyPrefix: "__homecom",
52
+ providerName: "SingleKey ID",
53
+ redirectStart: `${require_account_integration_offline.HOMECOM_REDIRECT_URI}?code=`
54
+ };
50
55
  function buildHomeComFormSchema() {
51
56
  return { sections: [{
52
57
  id: "account",
53
58
  title: "HomeCom account",
54
59
  titleKey: "provider-bosch:homecom.s_account.title",
55
- description: "Sign in with your SingleKey ID. A new tab opens the sign-in page through this hub and finishes on its own.",
60
+ description: "Sign in with your SingleKey ID in your own browser, then paste the address the browser stops on.",
56
61
  descriptionKey: "provider-bosch:homecom.s_account.description",
57
62
  columns: 1,
58
63
  fields: [
59
- {
60
- type: "info",
61
- key: "__homecom-code-help",
62
- label: HOMECOM_CODE_HELP,
63
- labelKey: "provider-bosch:homecom.s_account.__homecom-code-help.label",
64
- content: ""
65
- },
64
+ ...require_account_integration_offline.buildSignInGuideFields(HOMECOM_SIGN_IN_GUIDE),
66
65
  {
67
66
  type: "account-sign-in",
68
67
  addonId: "bosch-homecom",
@@ -72,7 +71,6 @@ function buildHomeComFormSchema() {
72
71
  providerLabel: "SingleKey ID",
73
72
  beginAction: "beginSignIn",
74
73
  completeAction: "finishSignIn",
75
- claimAction: "claimSignIn",
76
74
  signOutAction: "signOut",
77
75
  accountKey: "accountLabel",
78
76
  countKey: "gatewayCount",
@@ -215,27 +213,6 @@ var homeComActions = require_bosch_instances.defineCustomActions({
215
213
  kind: "mutation",
216
214
  auth: "admin"
217
215
  }),
218
- /**
219
- * The form asks while the proxied login page is open: `pending` until the hub captured the
220
- * app-scheme redirect and finished the exchange, then the same fields `finishSignIn` returns.
221
- * Claimed once. A failed exchange answers with its operator-facing message as an error.
222
- */
223
- claimSignIn: require_bosch_instances.customAction(require_bosch_instances.object({ attemptId: require_bosch_instances.string().min(1) }), require_bosch_instances.object({
224
- pending: require_bosch_instances.boolean(),
225
- token: require_bosch_instances.string().optional().meta({ secret: true }),
226
- refresh: require_bosch_instances.string().optional().meta({ secret: true }),
227
- username: require_bosch_instances.string().optional().meta({ secret: true }),
228
- accountLabel: require_bosch_instances.string().optional(),
229
- gatewayCount: require_bosch_instances.number().optional(),
230
- summary: require_bosch_instances.string().optional(),
231
- gatewayId: require_bosch_instances.string().optional(),
232
- brand: HomeComBrandSchema.optional(),
233
- region: HomeComRegionSchema.optional(),
234
- choices: require_bosch_instances.array(gatewayChoice).optional()
235
- }), {
236
- kind: "mutation",
237
- auth: "admin"
238
- }),
239
216
  signOut: require_bosch_instances.customAction(require_bosch_instances.object({}), require_bosch_instances.object({
240
217
  signedOut: require_bosch_instances.literal(true),
241
218
  summary: require_bosch_instances.string()
@@ -290,37 +267,6 @@ function unsupportedDeviceTypeMessage(deviceType) {
290
267
  return new HomeComUnknownDeviceTypeError(deviceType ?? "(missing)").message;
291
268
  }
292
269
  //#endregion
293
- //#region src/homecom/homecom-oauth.ts
294
- /**
295
- * Public OAuth client of the HomeCom Easy app, signed in through SingleKey ID.
296
- *
297
- * A PKCE client has no secret. The values are the ones SingleKey accepts for this client id
298
- * today: the same authorize query the MIT-licensed `homecom_alt` client records
299
- * (`OAUTH_LOGIN_PARAMS` — parameters, not copied code) and that the Home Assistant integration
300
- * opens. `pointt.gateway.resource.dopair` is not a scope this client may request; SingleKey
301
- * answers `misconfigured-application` and never shows the login page. The scope it does accept
302
- * names `pointt.gateway.resource.dashapp`, and the Bosch app sends `style_id=tt_bsch`.
303
- */
304
- var HOMECOM_CLIENT_ID = "762162C0-FA2D-4540-AE66-6489F189FADC";
305
- var HOMECOM_REDIRECT_URI = "com.bosch.tt.dashtt.pointt://app/login";
306
- var HOMECOM_AUTHORIZE_URL = "https://singlekey-id.com/auth/connect/authorize";
307
- var HOMECOM_TOKEN_URL = "https://singlekey-id.com/auth/connect/token";
308
- var HOMECOM_STYLE_ID = "tt_bsch";
309
- var HOMECOM_SCOPE = [
310
- "openid",
311
- "email",
312
- "profile",
313
- "offline_access",
314
- "pointt.gateway.claiming",
315
- "pointt.gateway.removal",
316
- "pointt.gateway.list",
317
- "pointt.gateway.users",
318
- "pointt.gateway.resource.dashapp",
319
- "pointt.castt.flow.token-exchange",
320
- "bacon",
321
- "hcc.tariff.read"
322
- ].join(" ");
323
- //#endregion
324
270
  //#region src/homecom/homecom-domain-mapping.ts
325
271
  var MODE_MAP = {
326
272
  Off: "off",
@@ -741,6 +687,352 @@ function isRecord$2(value) {
741
687
  return typeof value === "object" && value !== null && !Array.isArray(value);
742
688
  }
743
689
  //#endregion
690
+ //#region src/homecom/homecom-heat-pump.ts
691
+ /**
692
+ * Heat-pump resources of a hydronic gateway (D1026).
693
+ *
694
+ * UNVERIFIED: the paths below come from the layout of the public HomeCom Easy
695
+ * tree as the MIT `homecom_alt` library lists it, and were not checked against a
696
+ * live heat pump. They live in this one table so a wrong path is a one-line
697
+ * fix, and a resource the gateway does not answer reads as `null` (never 0),
698
+ * which the hero then simply does not draw.
699
+ */
700
+ var HEAT_PUMP_PATHS = {
701
+ system: {
702
+ silent: "/heatSources/hs1/silentMode",
703
+ away: "/system/awayMode",
704
+ additionalHeater: "/heatSources/hs1/additionalHeaterMode",
705
+ holiday: "/system/holidayMode"
706
+ },
707
+ readings: {
708
+ thermalW: "/heatSources/hs1/heatPumpPower",
709
+ electricalW: "/heatSources/hs1/electricalPower",
710
+ compressorPct: "/heatSources/hs1/compressorModulation",
711
+ supply: "/heatSources/hs1/supplyTemperature",
712
+ return: "/heatSources/hs1/returnTemperature",
713
+ outdoor: "/system/sensors/temperatures/outdoor_t1",
714
+ thermalKwh: "/recordings/heatSources/total/thermalEnergy",
715
+ electricalKwh: "/recordings/heatSources/total/electricalEnergy",
716
+ pressureBar: "/heatSources/hs1/waterPressure",
717
+ starts: "/heatSources/hs1/numberOfStarts"
718
+ },
719
+ circuit: {
720
+ operationMode: "operationMode",
721
+ heatCoolMode: "heatCoolMode",
722
+ summerWinter: "suWiSwitchMode",
723
+ comfort: "temperatureLevels/comfort2",
724
+ eco: "temperatureLevels/eco",
725
+ maxFlow: "maxSupplyTemperature",
726
+ current: "roomtemperature",
727
+ target: "currentRoomSetpoint"
728
+ },
729
+ dhw: {
730
+ operationMode: "operationMode",
731
+ level: "currentTemperatureLevel",
732
+ chargeDuration: "charge/duration",
733
+ charge: "charge",
734
+ current: "actualTemp",
735
+ target: "currentSetpoint"
736
+ }
737
+ };
738
+ /** A write the gateway would refuse, caught before it is sent. */
739
+ var HomeComHeatPumpRefusalError = class extends Error {
740
+ name = "HomeComHeatPumpRefusalError";
741
+ };
742
+ async function readHeatPump(input) {
743
+ const get = safeGet(input.get, input.onFailure);
744
+ const circuits = await Promise.all([...input.circuitIds.hc.map((id) => readHc(get, id)), ...input.circuitIds.dhw.map((id) => readDhwCircuit(get, id))]);
745
+ const r = HEAT_PUMP_PATHS.readings;
746
+ const s = HEAT_PUMP_PATHS.system;
747
+ const [silent, away, additionalHeater, holiday] = await Promise.all([
748
+ setting(get, s.silent),
749
+ setting(get, s.away),
750
+ setting(get, s.additionalHeater),
751
+ setting(get, s.holiday)
752
+ ]);
753
+ const [thermalW, electricalW, compressorPct, supply, ret, outdoor] = await Promise.all([
754
+ num(get, r.thermalW),
755
+ num(get, r.electricalW),
756
+ num(get, r.compressorPct),
757
+ num(get, r.supply),
758
+ num(get, r.return),
759
+ num(get, r.outdoor)
760
+ ]);
761
+ const [thermalKwh, electricalKwh, pressureBar, starts] = await Promise.all([
762
+ num(get, r.thermalKwh),
763
+ num(get, r.electricalKwh),
764
+ num(get, r.pressureBar),
765
+ num(get, r.starts)
766
+ ]);
767
+ return {
768
+ circuits,
769
+ silent,
770
+ away,
771
+ additionalHeater,
772
+ holiday,
773
+ thermalW,
774
+ electricalW,
775
+ compressorPct,
776
+ supply,
777
+ return: ret,
778
+ outdoor,
779
+ thermalKwh,
780
+ electricalKwh,
781
+ pressureBar,
782
+ starts
783
+ };
784
+ }
785
+ /** Any failure of a heat-pump read (HTTP 4xx/5xx, a throw) is "not answered", never an error. */
786
+ function safeGet(get, onFailure) {
787
+ return async (path) => {
788
+ try {
789
+ return await get(path);
790
+ } catch (error) {
791
+ onFailure?.(path, error);
792
+ return null;
793
+ }
794
+ };
795
+ }
796
+ /**
797
+ * True when a HEAT PUMP answered: silent mode, the auxiliary heater, heat-pump
798
+ * power or compressor modulation. Flow temperatures, pressure and starts are left
799
+ * out on purpose: a gas boiler's heat source answers those too, and it is no heat pump.
800
+ */
801
+ function hasHeatSource(reading) {
802
+ return [
803
+ reading.silent,
804
+ reading.additionalHeater,
805
+ reading.thermalW,
806
+ reading.electricalW,
807
+ reading.compressorPct
808
+ ].some((v) => v !== null);
809
+ }
810
+ async function readHc(get, id) {
811
+ const base = `/heatingCircuits/${id}`;
812
+ const p = HEAT_PUMP_PATHS.circuit;
813
+ const [operationMode, heatCoolMode, summerWinter, comfort, eco, maxFlow, current, target] = await Promise.all([
814
+ setting(get, `${base}/${p.operationMode}`),
815
+ setting(get, `${base}/${p.heatCoolMode}`),
816
+ setting(get, `${base}/${p.summerWinter}`),
817
+ setting(get, `${base}/${p.comfort}`),
818
+ setting(get, `${base}/${p.eco}`),
819
+ setting(get, `${base}/${p.maxFlow}`),
820
+ num(get, `${base}/${p.current}`),
821
+ num(get, `${base}/${p.target}`)
822
+ ]);
823
+ return {
824
+ id,
825
+ kind: "hc",
826
+ currentTemp: current,
827
+ targetTemp: target,
828
+ operationMode,
829
+ heatCoolMode,
830
+ summerWinter,
831
+ comfort,
832
+ eco,
833
+ maxFlow,
834
+ level: null,
835
+ chargeDuration: null,
836
+ charge: null
837
+ };
838
+ }
839
+ async function readDhwCircuit(get, id) {
840
+ const base = `/dhwCircuits/${id}`;
841
+ const p = HEAT_PUMP_PATHS.dhw;
842
+ const [operationMode, level, chargeDuration, charge, current, target] = await Promise.all([
843
+ setting(get, `${base}/${p.operationMode}`),
844
+ setting(get, `${base}/${p.level}`),
845
+ setting(get, `${base}/${p.chargeDuration}`),
846
+ setting(get, `${base}/${p.charge}`),
847
+ num(get, `${base}/${p.current}`),
848
+ num(get, `${base}/${p.target}`)
849
+ ]);
850
+ return {
851
+ id,
852
+ kind: "dhw",
853
+ currentTemp: current,
854
+ targetTemp: target,
855
+ operationMode,
856
+ heatCoolMode: null,
857
+ summerWinter: null,
858
+ comfort: null,
859
+ eco: null,
860
+ maxFlow: null,
861
+ level,
862
+ chargeDuration,
863
+ charge
864
+ };
865
+ }
866
+ async function setting(get, path) {
867
+ const envelope = readEnvelope(await get(path));
868
+ return envelope === null ? null : settingOf(path, envelope);
869
+ }
870
+ async function num(get, path) {
871
+ const envelope = readEnvelope(await get(path));
872
+ return envelope !== null && typeof envelope.value === "number" && Number.isFinite(envelope.value) ? envelope.value : null;
873
+ }
874
+ function settingOf(path, envelope) {
875
+ return {
876
+ path,
877
+ value: envelope.value,
878
+ min: envelope.min,
879
+ max: envelope.max,
880
+ allowed: envelope.allowed
881
+ };
882
+ }
883
+ function heatPumpStatusFrom(reading, now) {
884
+ return {
885
+ circuits: reading.circuits.map(circuitOf),
886
+ system: {
887
+ silent: optionOf(reading.silent),
888
+ away: optionOf(reading.away),
889
+ additionalHeater: optionOf(reading.additionalHeater),
890
+ holiday: optionOf(reading.holiday)
891
+ },
892
+ power: {
893
+ thermalW: reading.thermalW,
894
+ electricalW: reading.electricalW,
895
+ compressorPct: reading.compressorPct
896
+ },
897
+ temperatures: {
898
+ supply: reading.supply,
899
+ return: reading.return,
900
+ outdoor: reading.outdoor
901
+ },
902
+ energy: {
903
+ thermalKwh: reading.thermalKwh,
904
+ electricalKwh: reading.electricalKwh
905
+ },
906
+ waterPressureBar: reading.pressureBar,
907
+ starts: reading.starts,
908
+ lastFetchedAt: now
909
+ };
910
+ }
911
+ function circuitOf(c) {
912
+ const kind = kindOf(c);
913
+ const mode = textOf$1(c.operationMode);
914
+ return {
915
+ id: c.id,
916
+ kind,
917
+ label: labelOf(c),
918
+ icon: kind === "dhw" ? "droplets" : kind === "cooling" ? "snowflake" : "flame",
919
+ currentTemp: c.currentTemp,
920
+ targetTemp: c.targetTemp,
921
+ mode,
922
+ active: null,
923
+ operationMode: optionOf(c.operationMode),
924
+ heatCoolMode: optionOf(c.heatCoolMode),
925
+ summerWinter: optionOf(c.summerWinter),
926
+ comfortSetpoint: rangeOf(c.comfort),
927
+ ecoSetpoint: rangeOf(c.eco),
928
+ maxFlowTemp: rangeOf(c.maxFlow),
929
+ temperatureLevel: optionOf(c.level),
930
+ chargeDuration: rangeOf(c.chargeDuration),
931
+ extraHotWater: c.charge === null ? null : textOf$1(c.charge) === "start"
932
+ };
933
+ }
934
+ function kindOf(c) {
935
+ if (c.kind === "dhw") return "dhw";
936
+ return textOf$1(c.heatCoolMode) === "cooling" ? "cooling" : "heating";
937
+ }
938
+ function labelOf(c) {
939
+ const n = c.id.replace(/^\D+/, "");
940
+ if (c.kind === "dhw") return n === "1" || n === "" ? "Hot water" : `Hot water ${n}`;
941
+ return n === "" ? "Circuit" : `Circuit ${n}`;
942
+ }
943
+ function textOf$1(s) {
944
+ return s !== null && typeof s.value === "string" && s.value.length > 0 ? s.value : null;
945
+ }
946
+ function optionOf(s) {
947
+ if (s === null) return null;
948
+ return {
949
+ value: textOf$1(s),
950
+ allowed: [...s.allowed]
951
+ };
952
+ }
953
+ function rangeOf(s) {
954
+ if (s === null) return null;
955
+ return {
956
+ value: typeof s.value === "number" && Number.isFinite(s.value) ? s.value : null,
957
+ min: s.min,
958
+ max: s.max,
959
+ step: null
960
+ };
961
+ }
962
+ function requireCircuit(reading, circuitId, want) {
963
+ const found = reading.circuits.find((c) => c.id === circuitId && c.kind === want);
964
+ if (found === void 0) throw new HomeComHeatPumpRefusalError(`HomeCom heat pump has no ${want} circuit ${circuitId}`);
965
+ return found;
966
+ }
967
+ function optionWrite(s, what, mode) {
968
+ if (s === null) throw new HomeComHeatPumpRefusalError(`HomeCom heat pump: ${what} is not available`);
969
+ if (s.allowed.length === 0) throw new HomeComHeatPumpRefusalError(`HomeCom heat pump: ${what} is read-only on this gateway`);
970
+ if (!s.allowed.includes(mode)) throw new HomeComHeatPumpRefusalError(`HomeCom heat pump: "${mode}" is not an accepted value of ${what} (${s.allowed.join(", ")})`);
971
+ return {
972
+ path: s.path,
973
+ value: mode
974
+ };
975
+ }
976
+ function rangeWrite(s, what, value) {
977
+ if (s === null) throw new HomeComHeatPumpRefusalError(`HomeCom heat pump: ${what} is not available`);
978
+ if (!Number.isFinite(value)) throw new HomeComHeatPumpRefusalError(`HomeCom heat pump: ${what} needs a finite number`);
979
+ if (s.min === null || s.max === null) throw new HomeComHeatPumpRefusalError(`HomeCom heat pump: ${what} has no limits from the gateway`);
980
+ if (value < s.min || value > s.max) throw new HomeComHeatPumpRefusalError(`HomeCom heat pump: ${value} is outside ${what} limits ${s.min}..${s.max}`);
981
+ return {
982
+ path: s.path,
983
+ value
984
+ };
985
+ }
986
+ function systemModeWrite(reading, which, mode) {
987
+ return optionWrite(reading[which], `${which} mode`, mode);
988
+ }
989
+ function circuitOptionWrite(reading, circuitId, which, mode) {
990
+ return optionWrite(requireCircuit(reading, circuitId, "hc")[which], `${circuitId} ${which}`, mode);
991
+ }
992
+ function dhwOptionWrite(reading, circuitId, which, mode) {
993
+ return optionWrite(requireCircuit(reading, circuitId, "dhw")[which], `${circuitId} ${which}`, mode);
994
+ }
995
+ function circuitRangeWrite(reading, circuitId, which, value) {
996
+ return rangeWrite(requireCircuit(reading, circuitId, "hc")[which], `${circuitId} ${which}`, value);
997
+ }
998
+ function dhwChargeDurationWrite(reading, circuitId, minutes) {
999
+ return rangeWrite(requireCircuit(reading, circuitId, "dhw").chargeDuration, `${circuitId} charge duration`, minutes);
1000
+ }
1001
+ function extraHotWaterWrite(reading, circuitId) {
1002
+ const charge = requireCircuit(reading, circuitId, "dhw").charge;
1003
+ return optionWrite(charge, `${circuitId} extra hot water`, "start");
1004
+ }
1005
+ //#endregion
1006
+ //#region src/homecom/homecom-oauth.ts
1007
+ /**
1008
+ * Public OAuth client of the HomeCom Easy app, signed in through SingleKey ID.
1009
+ *
1010
+ * A PKCE client has no secret. The values are the ones SingleKey accepts for this client id
1011
+ * today: the same authorize query the MIT-licensed `homecom_alt` client records
1012
+ * (`OAUTH_LOGIN_PARAMS` — parameters, not copied code) and that the Home Assistant integration
1013
+ * opens. `pointt.gateway.resource.dopair` is not a scope this client may request; SingleKey
1014
+ * answers `misconfigured-application` and never shows the login page. The scope it does accept
1015
+ * names `pointt.gateway.resource.dashapp`, and the Bosch app sends `style_id=tt_bsch`.
1016
+ */
1017
+ var HOMECOM_CLIENT_ID = "762162C0-FA2D-4540-AE66-6489F189FADC";
1018
+ var HOMECOM_AUTHORIZE_URL = "https://singlekey-id.com/auth/connect/authorize";
1019
+ var HOMECOM_TOKEN_URL = "https://singlekey-id.com/auth/connect/token";
1020
+ var HOMECOM_STYLE_ID = "tt_bsch";
1021
+ var HOMECOM_SCOPE = [
1022
+ "openid",
1023
+ "email",
1024
+ "profile",
1025
+ "offline_access",
1026
+ "pointt.gateway.claiming",
1027
+ "pointt.gateway.removal",
1028
+ "pointt.gateway.list",
1029
+ "pointt.gateway.users",
1030
+ "pointt.gateway.resource.dashapp",
1031
+ "pointt.castt.flow.token-exchange",
1032
+ "bacon",
1033
+ "hcc.tariff.read"
1034
+ ].join(" ");
1035
+ //#endregion
744
1036
  //#region src/homecom/homecom-api-client.ts
745
1037
  var HomeComAuthError = class extends Error {
746
1038
  name = "HomeComAuthError";
@@ -954,15 +1246,43 @@ function createHomeComApiClient(config, gatewayId, deps = {}) {
954
1246
  }
955
1247
  async function fetchSystem() {
956
1248
  const resolved = await resolveDeviceType();
957
- const snapshot = await readPointtSnapshot({
1249
+ const withHeatPump = await attachHeatPump(await readPointtSnapshot({
958
1250
  deviceType: resolved.deviceType,
959
1251
  family: resolved.family,
960
1252
  gatewayId,
961
1253
  get: getResource,
962
1254
  logDrop
1255
+ }));
1256
+ last = withHeatPump;
1257
+ return withHeatPump;
1258
+ }
1259
+ async function attachHeatPump(snapshot) {
1260
+ if (snapshot.family !== "hydronic" || snapshot.deviceType === "rrc2") return snapshot;
1261
+ const dhwId = snapshot.dhw?.circuitId;
1262
+ const heatPump = await readHeatPump({
1263
+ get: getResource,
1264
+ onFailure: logHeatPumpFailure,
1265
+ circuitIds: {
1266
+ hc: snapshot.circuits.map((circuit) => circuit.id),
1267
+ dhw: dhwId === void 0 ? [] : [dhwId]
1268
+ }
1269
+ });
1270
+ return hasHeatSource(heatPump) ? {
1271
+ ...snapshot,
1272
+ heatPump
1273
+ } : snapshot;
1274
+ }
1275
+ function logHeatPumpFailure(path, error) {
1276
+ const key = `heat-pump-read:${path}`;
1277
+ if (dropped.has(key)) return;
1278
+ dropped.add(key);
1279
+ logger?.warn("HomeCom heat-pump resource could not be read; it is skipped", {
1280
+ tags: { gatewayId },
1281
+ meta: {
1282
+ path,
1283
+ error: error instanceof Error ? error.message : String(error)
1284
+ }
963
1285
  });
964
- last = snapshot;
965
- return snapshot;
966
1286
  }
967
1287
  async function probePath(path) {
968
1288
  const res = await authed(homeComResourceUrl(gatewayId, path), {
@@ -1016,6 +1336,9 @@ function createHomeComApiClient(config, gatewayId, deps = {}) {
1016
1336
  const snapshot = await requireSnapshot();
1017
1337
  if (snapshot.family !== "rac") logDrop("swing is not a resource on this device type");
1018
1338
  await putWrite(swingWrite(snapshot, "horizontal", on));
1339
+ },
1340
+ async writeHeatPump(writes) {
1341
+ for (const write of writes) await putWrite(write);
1019
1342
  }
1020
1343
  };
1021
1344
  }
@@ -1093,7 +1416,7 @@ function beginHomeComSignIn() {
1093
1416
  const state = b64url((0, node_crypto.randomBytes)(16));
1094
1417
  const url = new URL(HOMECOM_AUTHORIZE_URL);
1095
1418
  url.searchParams.set("client_id", HOMECOM_CLIENT_ID);
1096
- url.searchParams.set("redirect_uri", HOMECOM_REDIRECT_URI);
1419
+ url.searchParams.set("redirect_uri", require_account_integration_offline.HOMECOM_REDIRECT_URI);
1097
1420
  url.searchParams.set("response_type", "code");
1098
1421
  url.searchParams.set("scope", HOMECOM_SCOPE);
1099
1422
  url.searchParams.set("code_challenge", challenge);
@@ -1141,7 +1464,7 @@ async function completeHomeComSignIn(input, fetchFn = globalThis.fetch.bind(glob
1141
1464
  client_id: HOMECOM_CLIENT_ID,
1142
1465
  code,
1143
1466
  code_verifier: input.verifier,
1144
- redirect_uri: HOMECOM_REDIRECT_URI
1467
+ redirect_uri: require_account_integration_offline.HOMECOM_REDIRECT_URI
1145
1468
  }).toString()
1146
1469
  });
1147
1470
  const body = await res.json().catch(() => null);
@@ -1226,42 +1549,6 @@ function finishedFromGateways(tokens, resolved, listFailure) {
1226
1549
  }
1227
1550
  var homeComSignInAttempts = require_account_integration_offline.createSignInAttemptStore();
1228
1551
  //#endregion
1229
- //#region src/homecom/homecom-sign-in-proxy.ts
1230
- /**
1231
- * SingleKey ID serves the HomeCom login page; its scripts and styles come from `cdn.singlekey-id.com`
1232
- * as `type=module` + `crossorigin` files, which the CDN does not CORS-enable for another origin —
1233
- * served from the hub origin through the proxy they load, loaded directly they are refused and the
1234
- * page has no script (only the email field, no way to continue).
1235
- */
1236
- var HOMECOM_LOGIN_HOSTS = ["singlekey-id.com", "cdn.singlekey-id.com"];
1237
- var HOMECOM_APP_SCHEME = HOMECOM_REDIRECT_URI.slice(0, HOMECOM_REDIRECT_URI.indexOf("://") + 3);
1238
- var homeComSignInClaims = require_account_integration_offline.createSignInClaims();
1239
- /** Start an attempt whose `authorizeUrl` is the hub-relative proxied login page. */
1240
- function beginHomeComProxiedSignIn(deps) {
1241
- const nowMs = deps.nowMs ?? (() => Date.now());
1242
- const published = publishHomeComSignIn(homeComSignInAttempts, nowMs());
1243
- if (deps.proxy === null) return published;
1244
- const entry = require_account_integration_offline.openSignInProxy({
1245
- proxy: deps.proxy,
1246
- claims: homeComSignInClaims,
1247
- addonId: deps.addonId,
1248
- attemptId: published.attemptId,
1249
- authorizeUrl: published.authorizeUrl,
1250
- hosts: HOMECOM_LOGIN_HOSTS,
1251
- appScheme: HOMECOM_APP_SCHEME,
1252
- finish: (callback) => finishHomeComSignIn({
1253
- attemptId: published.attemptId,
1254
- callback,
1255
- nowMs: nowMs()
1256
- }, homeComSignInAttempts, deps.fetchFn !== void 0 ? { fetchFn: deps.fetchFn } : {}),
1257
- nowMs
1258
- });
1259
- return {
1260
- attemptId: published.attemptId,
1261
- authorizeUrl: entry
1262
- };
1263
- }
1264
- //#endregion
1265
1552
  //#region src/homecom/homecom-token-persist.ts
1266
1553
  /**
1267
1554
  * Patch the integration's record, one write at a time per integration. Refuses (and says so,
@@ -1316,8 +1603,7 @@ function buildHomeComGatewayPersister(deps) {
1316
1603
  function buildHomeComCandidates(input) {
1317
1604
  const out = [];
1318
1605
  for (const dev of input.devices) {
1319
- const type = dev.kind === "heating-circuit" ? require_bosch_instances.DeviceType.Climate : require_bosch_instances.DeviceType.WaterHeater;
1320
- const caps = dev.kind === "heating-circuit" ? ["climate-control"] : ["water-heater"];
1606
+ const { type, caps } = shapeOf(dev.kind);
1321
1607
  const adoptedId = input.adopted.get(dev.nativeId) ?? null;
1322
1608
  out.push({
1323
1609
  childNativeId: dev.nativeId,
@@ -1332,6 +1618,22 @@ function buildHomeComCandidates(input) {
1332
1618
  }
1333
1619
  return out;
1334
1620
  }
1621
+ function shapeOf(kind) {
1622
+ switch (kind) {
1623
+ case "heating-circuit": return {
1624
+ type: require_bosch_instances.DeviceType.Climate,
1625
+ caps: ["climate-control"]
1626
+ };
1627
+ case "dhw": return {
1628
+ type: require_bosch_instances.DeviceType.WaterHeater,
1629
+ caps: ["water-heater"]
1630
+ };
1631
+ case "heat-pump": return {
1632
+ type: require_bosch_instances.DeviceType.HeatPump,
1633
+ caps: ["heat-pump-status", "heat-pump-control"]
1634
+ };
1635
+ }
1636
+ }
1335
1637
  //#endregion
1336
1638
  //#region src/homecom/homecom-adoption-provider.ts
1337
1639
  var DEVICES_FILTER = {
@@ -1516,6 +1818,11 @@ function buildDiscoverables(snapshot) {
1516
1818
  name: "Domestic hot water",
1517
1819
  kind: "dhw"
1518
1820
  });
1821
+ if (snapshot.heatPump !== void 0) out.push({
1822
+ nativeId: "hp:0",
1823
+ name: "Heat pump",
1824
+ kind: "heat-pump"
1825
+ });
1519
1826
  return out;
1520
1827
  }
1521
1828
  var homeComConnections = new HomeComConnectionResolver();
@@ -2002,7 +2309,7 @@ var homeComClimateSchema = require_bosch_instances.object({
2002
2309
  integrationId: require_bosch_instances.string().optional()
2003
2310
  });
2004
2311
  var CLIMATE_CAP = "climate-control";
2005
- var COLD_START$1 = {
2312
+ var COLD_START$2 = {
2006
2313
  mode: "off",
2007
2314
  availableModes: [],
2008
2315
  fanMode: "",
@@ -2037,14 +2344,14 @@ var HomeComClimateDevice = class extends require_bosch_instances.BaseDevice {
2037
2344
  async onActivate() {
2038
2345
  await super.onActivate();
2039
2346
  this.registerClimateCap();
2040
- this.runtimeState.setCapState(CLIMATE_CAP, COLD_START$1);
2347
+ this.runtimeState.setCapState(CLIMATE_CAP, COLD_START$2);
2041
2348
  this.refreshFromSnapshot();
2042
2349
  }
2043
2350
  refreshFromSnapshot() {
2044
2351
  const circuit = homeComConnections.getSnapshot(this.#brokerId)?.circuits.find((c) => c.id === this.#circuitId);
2045
2352
  if (!circuit) {
2046
2353
  this.runtimeState.setCapState(CLIMATE_CAP, {
2047
- ...COLD_START$1,
2354
+ ...COLD_START$2,
2048
2355
  lastFetchedAt: Date.now()
2049
2356
  });
2050
2357
  return;
@@ -2060,7 +2367,7 @@ var HomeComClimateDevice = class extends require_bosch_instances.BaseDevice {
2060
2367
  this.ctx.registerNativeCap(require_bosch_instances.climateControlCapability, {
2061
2368
  getStatus: async () => {
2062
2369
  this.refreshFromSnapshot();
2063
- return this.runtimeState.getCapState(CLIMATE_CAP) ?? COLD_START$1;
2370
+ return this.runtimeState.getCapState(CLIMATE_CAP) ?? COLD_START$2;
2064
2371
  },
2065
2372
  setMode: async ({ mode }) => {
2066
2373
  const apiMode = hvacModeToHomeCom(mode);
@@ -2112,7 +2419,7 @@ var homeComWaterHeaterSchema = require_bosch_instances.object({
2112
2419
  integrationId: require_bosch_instances.string().optional()
2113
2420
  });
2114
2421
  var WH_CAP = "water-heater";
2115
- var COLD_START = {
2422
+ var COLD_START$1 = {
2116
2423
  currentTemp: null,
2117
2424
  targetTemp: null,
2118
2425
  operationMode: null,
@@ -2138,14 +2445,14 @@ var HomeComWaterHeaterDevice = class extends require_bosch_instances.BaseDevice
2138
2445
  async onActivate() {
2139
2446
  await super.onActivate();
2140
2447
  this.registerWaterHeaterCap();
2141
- this.runtimeState.setCapState(WH_CAP, COLD_START);
2448
+ this.runtimeState.setCapState(WH_CAP, COLD_START$1);
2142
2449
  this.refreshFromSnapshot();
2143
2450
  }
2144
2451
  refreshFromSnapshot() {
2145
2452
  const dhw = homeComConnections.getSnapshot(this.#brokerId)?.dhw;
2146
2453
  if (!dhw) {
2147
2454
  this.runtimeState.setCapState(WH_CAP, {
2148
- ...COLD_START,
2455
+ ...COLD_START$1,
2149
2456
  lastChangedAt: Date.now()
2150
2457
  });
2151
2458
  return;
@@ -2161,7 +2468,7 @@ var HomeComWaterHeaterDevice = class extends require_bosch_instances.BaseDevice
2161
2468
  this.ctx.registerNativeCap(require_bosch_instances.waterHeaterCapability, {
2162
2469
  getStatus: async () => {
2163
2470
  this.refreshFromSnapshot();
2164
- return this.runtimeState.getCapState(WH_CAP) ?? COLD_START;
2471
+ return this.runtimeState.getCapState(WH_CAP) ?? COLD_START$1;
2165
2472
  },
2166
2473
  setTargetTemp: async ({ temp }) => {
2167
2474
  await this.requireClient().setDhwTarget(temp);
@@ -2178,18 +2485,160 @@ var HomeComWaterHeaterDevice = class extends require_bosch_instances.BaseDevice
2178
2485
  }
2179
2486
  };
2180
2487
  //#endregion
2488
+ //#region src/homecom/devices/homecom-heat-pump-device.ts
2489
+ var homeComHeatPumpSchema = require_bosch_instances.object({
2490
+ nativeId: require_bosch_instances.string().min(1),
2491
+ brokerId: require_bosch_instances.string().min(1),
2492
+ system: require_bosch_instances.literal("homecom").optional(),
2493
+ integrationId: require_bosch_instances.string().optional()
2494
+ });
2495
+ var STATUS_CAP = "heat-pump-status";
2496
+ var COLD_START = {
2497
+ circuits: [],
2498
+ system: {
2499
+ silent: null,
2500
+ away: null,
2501
+ additionalHeater: null,
2502
+ holiday: null
2503
+ },
2504
+ power: {
2505
+ thermalW: null,
2506
+ electricalW: null,
2507
+ compressorPct: null
2508
+ },
2509
+ temperatures: {
2510
+ supply: null,
2511
+ return: null,
2512
+ outdoor: null
2513
+ },
2514
+ energy: {
2515
+ thermalKwh: null,
2516
+ electricalKwh: null
2517
+ },
2518
+ waterPressureBar: null,
2519
+ starts: null,
2520
+ lastFetchedAt: 0
2521
+ };
2522
+ /**
2523
+ * A heat pump on a hydronic HomeCom gateway (D1026). The slice and every limit
2524
+ * come from the gateway reading the integration poll already holds; a write is
2525
+ * checked against those limits first and the reading is refreshed after it.
2526
+ */
2527
+ var HomeComHeatPumpDevice = class extends require_bosch_instances.BaseDevice {
2528
+ features = [];
2529
+ #brokerId;
2530
+ /** True while the gateway has no heat-pump reading, so the log marks the transitions only. */
2531
+ #noReading = false;
2532
+ constructor(ctx) {
2533
+ const persisted = homeComHeatPumpSchema.parse(ctx.persistedConfig ?? {});
2534
+ super(ctx, homeComHeatPumpSchema, { type: ctx.deviceMeta.type });
2535
+ this.#brokerId = persisted.brokerId;
2536
+ this.online = true;
2537
+ this.updateSourceInfo({
2538
+ id: persisted.nativeId,
2539
+ system: "homecom"
2540
+ });
2541
+ }
2542
+ async onActivate() {
2543
+ await super.onActivate();
2544
+ this.registerStatusCap();
2545
+ this.registerControlCap();
2546
+ this.runtimeState.setCapState(STATUS_CAP, COLD_START);
2547
+ this.refreshFromSnapshot();
2548
+ }
2549
+ reading() {
2550
+ return homeComConnections.getSnapshot(this.#brokerId)?.heatPump ?? null;
2551
+ }
2552
+ refreshFromSnapshot() {
2553
+ const reading = this.reading();
2554
+ if (reading === null) {
2555
+ if (!this.#noReading) {
2556
+ this.#noReading = true;
2557
+ this.ctx.logger.warn("HomeCom heat pump: the gateway returned no heat-pump reading", { tags: { deviceId: this.ctx.id } });
2558
+ }
2559
+ this.runtimeState.setCapState(STATUS_CAP, {
2560
+ ...COLD_START,
2561
+ lastFetchedAt: Date.now()
2562
+ });
2563
+ return;
2564
+ }
2565
+ if (this.#noReading) {
2566
+ this.#noReading = false;
2567
+ this.ctx.logger.info("HomeCom heat pump: the gateway answers with a heat-pump reading again", { tags: { deviceId: this.ctx.id } });
2568
+ }
2569
+ this.runtimeState.setCapState(STATUS_CAP, heatPumpStatusFrom(reading, Date.now()));
2570
+ }
2571
+ requireClient() {
2572
+ const client = homeComConnections.getClient(this.#brokerId);
2573
+ if (!client) throw new Error("HomeCom client unavailable (heat pump)");
2574
+ return client;
2575
+ }
2576
+ requireReading() {
2577
+ const reading = this.reading();
2578
+ if (reading === null) throw new Error("HomeCom heat pump: no reading from the gateway yet");
2579
+ return reading;
2580
+ }
2581
+ /** Validate against the reading, send, then re-read so the slice shows what the gateway holds. */
2582
+ async apply(build) {
2583
+ const write = build(this.requireReading());
2584
+ const client = this.requireClient();
2585
+ await client.writeHeatPump([write]);
2586
+ this.ctx.logger.info("HomeCom heat pump: value written", {
2587
+ tags: { deviceId: this.ctx.id },
2588
+ meta: { path: write.path }
2589
+ });
2590
+ try {
2591
+ const snapshot = await client.fetchSystem();
2592
+ homeComConnections.updateSnapshot(this.#brokerId, snapshot);
2593
+ } catch (error) {
2594
+ this.ctx.logger.warn("HomeCom heat pump: value written, the refresh failed", {
2595
+ tags: { deviceId: this.ctx.id },
2596
+ meta: {
2597
+ path: write.path,
2598
+ error: error instanceof Error ? error.message : String(error)
2599
+ }
2600
+ });
2601
+ return;
2602
+ }
2603
+ this.refreshFromSnapshot();
2604
+ }
2605
+ registerStatusCap() {
2606
+ this.ctx.registerNativeCap(require_bosch_instances.heatPumpStatusCapability, { getStatus: async () => {
2607
+ this.refreshFromSnapshot();
2608
+ return this.runtimeState.getCapState(STATUS_CAP) ?? COLD_START;
2609
+ } });
2610
+ }
2611
+ registerControlCap() {
2612
+ this.ctx.registerNativeCap(require_bosch_instances.heatPumpControlCapability, {
2613
+ setSilentMode: ({ mode }) => this.apply((r) => systemModeWrite(r, "silent", mode)),
2614
+ setAwayMode: ({ mode }) => this.apply((r) => systemModeWrite(r, "away", mode)),
2615
+ setAdditionalHeaterMode: ({ mode }) => this.apply((r) => systemModeWrite(r, "additionalHeater", mode)),
2616
+ setHolidayMode: ({ mode }) => this.apply((r) => systemModeWrite(r, "holiday", mode)),
2617
+ setCircuitOperationMode: ({ circuitId, mode }) => this.apply((r) => circuitOptionWrite(r, circuitId, "operationMode", mode)),
2618
+ setCircuitHeatCoolMode: ({ circuitId, mode }) => this.apply((r) => circuitOptionWrite(r, circuitId, "heatCoolMode", mode)),
2619
+ setCircuitSummerWinter: ({ circuitId, mode }) => this.apply((r) => circuitOptionWrite(r, circuitId, "summerWinter", mode)),
2620
+ setCircuitComfortSetpoint: ({ circuitId, temp }) => this.apply((r) => circuitRangeWrite(r, circuitId, "comfort", temp)),
2621
+ setCircuitEcoSetpoint: ({ circuitId, temp }) => this.apply((r) => circuitRangeWrite(r, circuitId, "eco", temp)),
2622
+ setCircuitMaxFlowTemp: ({ circuitId, temp }) => this.apply((r) => circuitRangeWrite(r, circuitId, "maxFlow", temp)),
2623
+ setDhwOperationMode: ({ circuitId, mode }) => this.apply((r) => dhwOptionWrite(r, circuitId, "operationMode", mode)),
2624
+ setDhwTemperatureLevel: ({ circuitId, level }) => this.apply((r) => dhwOptionWrite(r, circuitId, "level", level)),
2625
+ setDhwChargeDuration: ({ circuitId, minutes }) => this.apply((r) => dhwChargeDurationWrite(r, circuitId, minutes)),
2626
+ startExtraHotWater: ({ circuitId }) => this.apply((r) => extraHotWaterWrite(r, circuitId))
2627
+ });
2628
+ }
2629
+ };
2630
+ //#endregion
2181
2631
  //#region src/homecom/addon.ts
2182
2632
  var BoschHomeComProviderAddon = class extends require_bosch_instances.BaseDeviceProvider {
2183
2633
  addonId = "bosch-homecom";
2184
2634
  providerName = "Bosch HomeCom";
2185
2635
  deviceClasses = {
2186
2636
  [require_bosch_instances.DeviceType.Climate]: HomeComClimateDevice,
2187
- [require_bosch_instances.DeviceType.WaterHeater]: HomeComWaterHeaterDevice
2637
+ [require_bosch_instances.DeviceType.WaterHeater]: HomeComWaterHeaterDevice,
2638
+ [require_bosch_instances.DeviceType.HeatPump]: HomeComHeatPumpDevice
2188
2639
  };
2189
2640
  clients = null;
2190
2641
  instances = null;
2191
- loginProxy = null;
2192
- loginPlane = null;
2193
2642
  constructor() {
2194
2643
  super({});
2195
2644
  }
@@ -2208,7 +2657,6 @@ var BoschHomeComProviderAddon = class extends require_bosch_instances.BaseDevice
2208
2657
  tester: this.buildConnectionTestProvider()
2209
2658
  });
2210
2659
  this.instances = built;
2211
- await this.serveLoginProxy();
2212
2660
  const persistDeps = {
2213
2661
  read: (id) => homeComStoredOf(built.instances, id),
2214
2662
  patch: (id, patch) => built.handle.patch(id, patch),
@@ -2243,19 +2691,9 @@ var BoschHomeComProviderAddon = class extends require_bosch_instances.BaseDevice
2243
2691
  customActions: homeComActions,
2244
2692
  actionHandlers: {
2245
2693
  beginSignIn: async () => ({
2246
- ...beginHomeComProxiedSignIn({
2247
- proxy: this.loginProxy,
2248
- addonId: this.ctx.id
2249
- }),
2250
- summary: "Open the sign-in page and sign in."
2694
+ ...publishHomeComSignIn(homeComSignInAttempts, Date.now()),
2695
+ summary: "Open the sign-in page, sign in, then paste the address the browser stops on."
2251
2696
  }),
2252
- claimSignIn: async (input) => {
2253
- const claim = homeComSignInClaims.claim(input.attemptId, Date.now());
2254
- return claim.state === "pending" ? { pending: true } : {
2255
- pending: false,
2256
- ...claim.result
2257
- };
2258
- },
2259
2697
  finishSignIn: async (input) => finishHomeComSignIn({
2260
2698
  attemptId: input.attemptId,
2261
2699
  callback: input.callback,
@@ -2268,27 +2706,7 @@ var BoschHomeComProviderAddon = class extends require_bosch_instances.BaseDevice
2268
2706
  }
2269
2707
  };
2270
2708
  }
2271
- /** The hub's HTTPS face for the vendor's login pages (D955): admin session only. */
2272
- async serveLoginProxy() {
2273
- const proxy = new require_bosch_instances.VendorLoginProxy({
2274
- addonId: this.ctx.id,
2275
- logger: this.ctx.logger
2276
- });
2277
- try {
2278
- this.loginPlane = await this.ctx.dataPlane?.serve({
2279
- prefix: "vendor-login",
2280
- access: "admin",
2281
- handler: proxy.handler
2282
- }) ?? null;
2283
- if (this.loginPlane !== null) this.loginProxy = proxy;
2284
- } catch (err) {
2285
- this.ctx.logger.warn("HomeCom login proxy failed to serve — sign-in falls back to paste", { meta: { error: require_bosch_instances.errMsg(err) } });
2286
- }
2287
- }
2288
2709
  async onShutdown() {
2289
- await this.loginPlane?.dispose().catch(() => void 0);
2290
- this.loginPlane = null;
2291
- this.loginProxy = null;
2292
2710
  try {
2293
2711
  await this.clients?.shutdown();
2294
2712
  } catch (err) {