@camstack/addon-provider-bosch 0.1.44 → 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 +6 -0
- package/dist/{account-integration-offline-CFyuKmsG.js → account-integration-offline-BiXbzZsg.js} +1 -1
- package/dist/{account-integration-offline-IHodWmhD.mjs → account-integration-offline-DAEciV_Q.mjs} +1 -1
- package/dist/{bosch-instances-C8FRjQzC.mjs → bosch-instances-DXID-jMD.mjs} +1196 -174
- package/dist/{bosch-instances-CXMUquuA.js → bosch-instances-DdklwoKS.js} +1207 -173
- package/dist/home-connect/addon.js +2 -2
- package/dist/home-connect/addon.mjs +2 -2
- package/dist/homecom/addon.js +557 -46
- package/dist/homecom/addon.mjs +557 -46
- package/dist/shc/addon.js +1 -1
- package/dist/shc/addon.mjs +1 -1
- package/package.json +8 -2
package/dist/homecom/addon.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { A as
|
|
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-
|
|
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"];
|
|
@@ -266,36 +266,6 @@ function unsupportedDeviceTypeMessage(deviceType) {
|
|
|
266
266
|
return new HomeComUnknownDeviceTypeError(deviceType ?? "(missing)").message;
|
|
267
267
|
}
|
|
268
268
|
//#endregion
|
|
269
|
-
//#region src/homecom/homecom-oauth.ts
|
|
270
|
-
/**
|
|
271
|
-
* Public OAuth client of the HomeCom Easy app, signed in through SingleKey ID.
|
|
272
|
-
*
|
|
273
|
-
* A PKCE client has no secret. The values are the ones SingleKey accepts for this client id
|
|
274
|
-
* today: the same authorize query the MIT-licensed `homecom_alt` client records
|
|
275
|
-
* (`OAUTH_LOGIN_PARAMS` — parameters, not copied code) and that the Home Assistant integration
|
|
276
|
-
* opens. `pointt.gateway.resource.dopair` is not a scope this client may request; SingleKey
|
|
277
|
-
* answers `misconfigured-application` and never shows the login page. The scope it does accept
|
|
278
|
-
* names `pointt.gateway.resource.dashapp`, and the Bosch app sends `style_id=tt_bsch`.
|
|
279
|
-
*/
|
|
280
|
-
var HOMECOM_CLIENT_ID = "762162C0-FA2D-4540-AE66-6489F189FADC";
|
|
281
|
-
var HOMECOM_AUTHORIZE_URL = "https://singlekey-id.com/auth/connect/authorize";
|
|
282
|
-
var HOMECOM_TOKEN_URL = "https://singlekey-id.com/auth/connect/token";
|
|
283
|
-
var HOMECOM_STYLE_ID = "tt_bsch";
|
|
284
|
-
var HOMECOM_SCOPE = [
|
|
285
|
-
"openid",
|
|
286
|
-
"email",
|
|
287
|
-
"profile",
|
|
288
|
-
"offline_access",
|
|
289
|
-
"pointt.gateway.claiming",
|
|
290
|
-
"pointt.gateway.removal",
|
|
291
|
-
"pointt.gateway.list",
|
|
292
|
-
"pointt.gateway.users",
|
|
293
|
-
"pointt.gateway.resource.dashapp",
|
|
294
|
-
"pointt.castt.flow.token-exchange",
|
|
295
|
-
"bacon",
|
|
296
|
-
"hcc.tariff.read"
|
|
297
|
-
].join(" ");
|
|
298
|
-
//#endregion
|
|
299
269
|
//#region src/homecom/homecom-domain-mapping.ts
|
|
300
270
|
var MODE_MAP = {
|
|
301
271
|
Off: "off",
|
|
@@ -716,6 +686,352 @@ function isRecord$2(value) {
|
|
|
716
686
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
717
687
|
}
|
|
718
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
|
|
719
1035
|
//#region src/homecom/homecom-api-client.ts
|
|
720
1036
|
var HomeComAuthError = class extends Error {
|
|
721
1037
|
name = "HomeComAuthError";
|
|
@@ -929,15 +1245,43 @@ function createHomeComApiClient(config, gatewayId, deps = {}) {
|
|
|
929
1245
|
}
|
|
930
1246
|
async function fetchSystem() {
|
|
931
1247
|
const resolved = await resolveDeviceType();
|
|
932
|
-
const
|
|
1248
|
+
const withHeatPump = await attachHeatPump(await readPointtSnapshot({
|
|
933
1249
|
deviceType: resolved.deviceType,
|
|
934
1250
|
family: resolved.family,
|
|
935
1251
|
gatewayId,
|
|
936
1252
|
get: getResource,
|
|
937
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
|
+
}
|
|
938
1284
|
});
|
|
939
|
-
last = snapshot;
|
|
940
|
-
return snapshot;
|
|
941
1285
|
}
|
|
942
1286
|
async function probePath(path) {
|
|
943
1287
|
const res = await authed(homeComResourceUrl(gatewayId, path), {
|
|
@@ -991,6 +1335,9 @@ function createHomeComApiClient(config, gatewayId, deps = {}) {
|
|
|
991
1335
|
const snapshot = await requireSnapshot();
|
|
992
1336
|
if (snapshot.family !== "rac") logDrop("swing is not a resource on this device type");
|
|
993
1337
|
await putWrite(swingWrite(snapshot, "horizontal", on));
|
|
1338
|
+
},
|
|
1339
|
+
async writeHeatPump(writes) {
|
|
1340
|
+
for (const write of writes) await putWrite(write);
|
|
994
1341
|
}
|
|
995
1342
|
};
|
|
996
1343
|
}
|
|
@@ -1255,8 +1602,7 @@ function buildHomeComGatewayPersister(deps) {
|
|
|
1255
1602
|
function buildHomeComCandidates(input) {
|
|
1256
1603
|
const out = [];
|
|
1257
1604
|
for (const dev of input.devices) {
|
|
1258
|
-
const type = dev.kind
|
|
1259
|
-
const caps = dev.kind === "heating-circuit" ? ["climate-control"] : ["water-heater"];
|
|
1605
|
+
const { type, caps } = shapeOf(dev.kind);
|
|
1260
1606
|
const adoptedId = input.adopted.get(dev.nativeId) ?? null;
|
|
1261
1607
|
out.push({
|
|
1262
1608
|
childNativeId: dev.nativeId,
|
|
@@ -1271,6 +1617,22 @@ function buildHomeComCandidates(input) {
|
|
|
1271
1617
|
}
|
|
1272
1618
|
return out;
|
|
1273
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
|
+
}
|
|
1274
1636
|
//#endregion
|
|
1275
1637
|
//#region src/homecom/homecom-adoption-provider.ts
|
|
1276
1638
|
var DEVICES_FILTER = {
|
|
@@ -1455,6 +1817,11 @@ function buildDiscoverables(snapshot) {
|
|
|
1455
1817
|
name: "Domestic hot water",
|
|
1456
1818
|
kind: "dhw"
|
|
1457
1819
|
});
|
|
1820
|
+
if (snapshot.heatPump !== void 0) out.push({
|
|
1821
|
+
nativeId: "hp:0",
|
|
1822
|
+
name: "Heat pump",
|
|
1823
|
+
kind: "heat-pump"
|
|
1824
|
+
});
|
|
1458
1825
|
return out;
|
|
1459
1826
|
}
|
|
1460
1827
|
var homeComConnections = new HomeComConnectionResolver();
|
|
@@ -1941,7 +2308,7 @@ var homeComClimateSchema = object({
|
|
|
1941
2308
|
integrationId: string().optional()
|
|
1942
2309
|
});
|
|
1943
2310
|
var CLIMATE_CAP = "climate-control";
|
|
1944
|
-
var COLD_START$
|
|
2311
|
+
var COLD_START$2 = {
|
|
1945
2312
|
mode: "off",
|
|
1946
2313
|
availableModes: [],
|
|
1947
2314
|
fanMode: "",
|
|
@@ -1976,14 +2343,14 @@ var HomeComClimateDevice = class extends BaseDevice {
|
|
|
1976
2343
|
async onActivate() {
|
|
1977
2344
|
await super.onActivate();
|
|
1978
2345
|
this.registerClimateCap();
|
|
1979
|
-
this.runtimeState.setCapState(CLIMATE_CAP, COLD_START$
|
|
2346
|
+
this.runtimeState.setCapState(CLIMATE_CAP, COLD_START$2);
|
|
1980
2347
|
this.refreshFromSnapshot();
|
|
1981
2348
|
}
|
|
1982
2349
|
refreshFromSnapshot() {
|
|
1983
2350
|
const circuit = homeComConnections.getSnapshot(this.#brokerId)?.circuits.find((c) => c.id === this.#circuitId);
|
|
1984
2351
|
if (!circuit) {
|
|
1985
2352
|
this.runtimeState.setCapState(CLIMATE_CAP, {
|
|
1986
|
-
...COLD_START$
|
|
2353
|
+
...COLD_START$2,
|
|
1987
2354
|
lastFetchedAt: Date.now()
|
|
1988
2355
|
});
|
|
1989
2356
|
return;
|
|
@@ -1999,7 +2366,7 @@ var HomeComClimateDevice = class extends BaseDevice {
|
|
|
1999
2366
|
this.ctx.registerNativeCap(climateControlCapability, {
|
|
2000
2367
|
getStatus: async () => {
|
|
2001
2368
|
this.refreshFromSnapshot();
|
|
2002
|
-
return this.runtimeState.getCapState(CLIMATE_CAP) ?? COLD_START$
|
|
2369
|
+
return this.runtimeState.getCapState(CLIMATE_CAP) ?? COLD_START$2;
|
|
2003
2370
|
},
|
|
2004
2371
|
setMode: async ({ mode }) => {
|
|
2005
2372
|
const apiMode = hvacModeToHomeCom(mode);
|
|
@@ -2051,7 +2418,7 @@ var homeComWaterHeaterSchema = object({
|
|
|
2051
2418
|
integrationId: string().optional()
|
|
2052
2419
|
});
|
|
2053
2420
|
var WH_CAP = "water-heater";
|
|
2054
|
-
var COLD_START = {
|
|
2421
|
+
var COLD_START$1 = {
|
|
2055
2422
|
currentTemp: null,
|
|
2056
2423
|
targetTemp: null,
|
|
2057
2424
|
operationMode: null,
|
|
@@ -2077,14 +2444,14 @@ var HomeComWaterHeaterDevice = class extends BaseDevice {
|
|
|
2077
2444
|
async onActivate() {
|
|
2078
2445
|
await super.onActivate();
|
|
2079
2446
|
this.registerWaterHeaterCap();
|
|
2080
|
-
this.runtimeState.setCapState(WH_CAP, COLD_START);
|
|
2447
|
+
this.runtimeState.setCapState(WH_CAP, COLD_START$1);
|
|
2081
2448
|
this.refreshFromSnapshot();
|
|
2082
2449
|
}
|
|
2083
2450
|
refreshFromSnapshot() {
|
|
2084
2451
|
const dhw = homeComConnections.getSnapshot(this.#brokerId)?.dhw;
|
|
2085
2452
|
if (!dhw) {
|
|
2086
2453
|
this.runtimeState.setCapState(WH_CAP, {
|
|
2087
|
-
...COLD_START,
|
|
2454
|
+
...COLD_START$1,
|
|
2088
2455
|
lastChangedAt: Date.now()
|
|
2089
2456
|
});
|
|
2090
2457
|
return;
|
|
@@ -2100,7 +2467,7 @@ var HomeComWaterHeaterDevice = class extends BaseDevice {
|
|
|
2100
2467
|
this.ctx.registerNativeCap(waterHeaterCapability, {
|
|
2101
2468
|
getStatus: async () => {
|
|
2102
2469
|
this.refreshFromSnapshot();
|
|
2103
|
-
return this.runtimeState.getCapState(WH_CAP) ?? COLD_START;
|
|
2470
|
+
return this.runtimeState.getCapState(WH_CAP) ?? COLD_START$1;
|
|
2104
2471
|
},
|
|
2105
2472
|
setTargetTemp: async ({ temp }) => {
|
|
2106
2473
|
await this.requireClient().setDhwTarget(temp);
|
|
@@ -2117,13 +2484,157 @@ var HomeComWaterHeaterDevice = class extends BaseDevice {
|
|
|
2117
2484
|
}
|
|
2118
2485
|
};
|
|
2119
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
|
|
2120
2630
|
//#region src/homecom/addon.ts
|
|
2121
2631
|
var BoschHomeComProviderAddon = class extends BaseDeviceProvider {
|
|
2122
2632
|
addonId = "bosch-homecom";
|
|
2123
2633
|
providerName = "Bosch HomeCom";
|
|
2124
2634
|
deviceClasses = {
|
|
2125
2635
|
[DeviceType.Climate]: HomeComClimateDevice,
|
|
2126
|
-
[DeviceType.WaterHeater]: HomeComWaterHeaterDevice
|
|
2636
|
+
[DeviceType.WaterHeater]: HomeComWaterHeaterDevice,
|
|
2637
|
+
[DeviceType.HeatPump]: HomeComHeatPumpDevice
|
|
2127
2638
|
};
|
|
2128
2639
|
clients = null;
|
|
2129
2640
|
instances = null;
|