@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.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { A as RecordIntegrationInstances, B as array, F as entryBody,
|
|
2
|
-
import { a as
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
-
...
|
|
2246
|
-
|
|
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) {
|