@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.
- package/CHANGELOG.md +12 -0
- package/dist/{account-integration-offline-Ctzpu6DI.js → account-integration-offline-BiXbzZsg.js} +65 -122
- package/dist/{account-integration-offline-BrrnCsrl.mjs → account-integration-offline-DAEciV_Q.mjs} +50 -113
- package/dist/{bosch-instances-BtKXLOoG.mjs → bosch-instances-DXID-jMD.mjs} +20264 -16673
- package/dist/{bosch-instances-CppS1lTA.js → bosch-instances-DdklwoKS.js} +20276 -16691
- package/dist/home-connect/addon.js +13 -98
- package/dist/home-connect/addon.mjs +12 -97
- package/dist/homecom/addon.js +569 -151
- package/dist/homecom/addon.mjs +567 -149
- package/dist/shc/addon.js +1 -1
- package/dist/shc/addon.mjs +1 -1
- package/i18n/en.json +19 -4
- package/i18n/it.json +19 -4
- package/package.json +8 -2
package/dist/homecom/addon.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_bosch_instances = require("../bosch-instances-
|
|
3
|
-
const require_account_integration_offline = require("../account-integration-offline-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
-
...
|
|
2247
|
-
|
|
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) {
|