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