@camstack/addon-provider-bosch 0.1.44 → 0.1.46
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-IHodWmhD.mjs → account-integration-offline-BGH1q3tj.mjs} +1 -1
- package/dist/{account-integration-offline-CFyuKmsG.js → account-integration-offline-BfsMnlJo.js} +1 -1
- package/dist/{bosch-instances-CXMUquuA.js → bosch-instances-BD-Valeo.js} +1355 -187
- package/dist/{bosch-instances-C8FRjQzC.mjs → bosch-instances-DkUo6BfT.mjs} +1344 -188
- package/dist/home-connect/addon.js +2 -2
- package/dist/home-connect/addon.mjs +2 -2
- package/dist/homecom/addon.js +771 -52
- package/dist/homecom/addon.mjs +771 -52
- 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-DkUo6BfT.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-BGH1q3tj.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",
|
|
@@ -478,7 +448,7 @@ async function readHydronic(input) {
|
|
|
478
448
|
const heating = await input.get("/heatingCircuits");
|
|
479
449
|
const dhwBody = await input.get("/dhwCircuits");
|
|
480
450
|
const circuits = [];
|
|
481
|
-
for (const
|
|
451
|
+
for (const ref of circuitRefs(heating, "hc")) circuits.push(await readCircuit(input.get, ref.id, ref.name));
|
|
482
452
|
const dhwIds = referenceKeys(dhwBody, "dhw");
|
|
483
453
|
const first = dhwIds[0];
|
|
484
454
|
const dhw = first === void 0 ? null : await readDhw(input.get, first);
|
|
@@ -538,7 +508,7 @@ async function readRac(input) {
|
|
|
538
508
|
setpointResource: "/airConditioning/temperatureSetpoint"
|
|
539
509
|
}], null);
|
|
540
510
|
}
|
|
541
|
-
async function readCircuit(get, id) {
|
|
511
|
+
async function readCircuit(get, id, givenName) {
|
|
542
512
|
const base = `/heatingCircuits/${id}`;
|
|
543
513
|
const loaded = await loadEnvelopes(get, [
|
|
544
514
|
`${base}/operationMode`,
|
|
@@ -561,7 +531,7 @@ async function readCircuit(get, id) {
|
|
|
561
531
|
const humidity = numberOf(loaded.get(`${base}/actualHumidity`) ?? null);
|
|
562
532
|
return {
|
|
563
533
|
id,
|
|
564
|
-
name: id,
|
|
534
|
+
name: heatingCircuitName(id, givenName),
|
|
565
535
|
operationMode: stringOf(operation) ?? "off",
|
|
566
536
|
setpoint: cooling ? numberOf(coolingSet) ?? numberOf(current) ?? numberOf(manual) : numberOf(current) ?? numberOf(manual),
|
|
567
537
|
currentRoomTemp: numberOf(loaded.get(`${base}/roomtemperature`) ?? null),
|
|
@@ -663,18 +633,37 @@ function pointtMode(mode) {
|
|
|
663
633
|
default: return mode;
|
|
664
634
|
}
|
|
665
635
|
}
|
|
666
|
-
|
|
636
|
+
/** `hc1` → "Heating circuit 1", unless the gateway already named the circuit. */
|
|
637
|
+
function heatingCircuitName(id, given) {
|
|
638
|
+
if (given !== null && given.length > 0 && given !== id) return given;
|
|
639
|
+
const n = id.replace(/^hc/, "");
|
|
640
|
+
return /^\d+$/.test(n) ? `Heating circuit ${n}` : "Heating circuit";
|
|
641
|
+
}
|
|
642
|
+
function circuitRefs(body, prefix) {
|
|
667
643
|
if (!isRecord$2(body) || !Array.isArray(body["references"])) return [];
|
|
668
644
|
const pattern = prefix === "hc" ? /^hc\d+$/ : /^dhw\d+$/;
|
|
669
645
|
const out = [];
|
|
670
646
|
for (const ref of body["references"]) {
|
|
671
647
|
if (!isRecord$2(ref) || typeof ref["id"] !== "string") continue;
|
|
672
648
|
const tail = ref["id"].split("/").filter((part) => part.length > 0).at(-1);
|
|
673
|
-
if (tail === void 0 || !pattern.test(tail) || out.
|
|
674
|
-
out.push(
|
|
649
|
+
if (tail === void 0 || !pattern.test(tail) || out.some((item) => item.id === tail)) continue;
|
|
650
|
+
out.push({
|
|
651
|
+
id: tail,
|
|
652
|
+
name: refName(ref, tail)
|
|
653
|
+
});
|
|
675
654
|
}
|
|
676
655
|
return out;
|
|
677
656
|
}
|
|
657
|
+
function refName(ref, id) {
|
|
658
|
+
const raw = ref["name"];
|
|
659
|
+
if (typeof raw !== "string") return null;
|
|
660
|
+
const trimmed = raw.trim();
|
|
661
|
+
if (trimmed.length === 0 || trimmed === id) return null;
|
|
662
|
+
return trimmed;
|
|
663
|
+
}
|
|
664
|
+
function referenceKeys(body, prefix) {
|
|
665
|
+
return circuitRefs(body, prefix).map((ref) => ref.id);
|
|
666
|
+
}
|
|
678
667
|
function indexStandard(body) {
|
|
679
668
|
const map = /* @__PURE__ */ new Map();
|
|
680
669
|
if (!isRecord$2(body) || !Array.isArray(body["references"])) return map;
|
|
@@ -716,6 +705,527 @@ function isRecord$2(value) {
|
|
|
716
705
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
717
706
|
}
|
|
718
707
|
//#endregion
|
|
708
|
+
//#region src/homecom/homecom-heat-pump.ts
|
|
709
|
+
/**
|
|
710
|
+
* Heat-pump resources of a hydronic gateway (D1026).
|
|
711
|
+
*
|
|
712
|
+
* UNVERIFIED: the paths below come from the layout of the public HomeCom Easy
|
|
713
|
+
* tree as the MIT `homecom_alt` library lists it, and were not checked against a
|
|
714
|
+
* live heat pump. They live in this one table so a wrong path is a one-line
|
|
715
|
+
* fix, and a resource the gateway does not answer reads as `null` (never 0),
|
|
716
|
+
* which the hero then simply does not draw.
|
|
717
|
+
*/
|
|
718
|
+
var HEAT_PUMP_PATHS = {
|
|
719
|
+
system: {
|
|
720
|
+
silent: "/heatSources/hs1/silentMode",
|
|
721
|
+
away: "/system/awayMode",
|
|
722
|
+
additionalHeater: "/heatSources/hs1/additionalHeaterMode",
|
|
723
|
+
holiday: "/system/holidayMode"
|
|
724
|
+
},
|
|
725
|
+
readings: {
|
|
726
|
+
thermalW: "/heatSources/hs1/heatPumpPower",
|
|
727
|
+
electricalW: "/heatSources/hs1/electricalPower",
|
|
728
|
+
compressorPct: "/heatSources/hs1/compressorModulation",
|
|
729
|
+
supply: "/heatSources/hs1/supplyTemperature",
|
|
730
|
+
return: "/heatSources/hs1/returnTemperature",
|
|
731
|
+
outdoor: "/system/sensors/temperatures/outdoor_t1",
|
|
732
|
+
thermalKwh: "/recordings/heatSources/total/thermalEnergy",
|
|
733
|
+
electricalKwh: "/recordings/heatSources/total/electricalEnergy",
|
|
734
|
+
pressureBar: "/heatSources/hs1/waterPressure",
|
|
735
|
+
starts: "/heatSources/hs1/numberOfStarts"
|
|
736
|
+
},
|
|
737
|
+
circuit: {
|
|
738
|
+
operationMode: "operationMode",
|
|
739
|
+
heatCoolMode: "heatCoolMode",
|
|
740
|
+
summerWinter: "suWiSwitchMode",
|
|
741
|
+
comfort: "temperatureLevels/comfort2",
|
|
742
|
+
eco: "temperatureLevels/eco",
|
|
743
|
+
maxFlow: "maxSupplyTemperature",
|
|
744
|
+
current: "roomtemperature",
|
|
745
|
+
target: "currentRoomSetpoint"
|
|
746
|
+
},
|
|
747
|
+
dhw: {
|
|
748
|
+
operationMode: "operationMode",
|
|
749
|
+
level: "currentTemperatureLevel",
|
|
750
|
+
chargeDuration: "charge/duration",
|
|
751
|
+
charge: "charge",
|
|
752
|
+
current: "actualTemp",
|
|
753
|
+
target: "currentSetpoint"
|
|
754
|
+
}
|
|
755
|
+
};
|
|
756
|
+
/** A write the gateway would refuse, caught before it is sent. */
|
|
757
|
+
var HomeComHeatPumpRefusalError = class extends Error {
|
|
758
|
+
name = "HomeComHeatPumpRefusalError";
|
|
759
|
+
};
|
|
760
|
+
async function readHeatPump(input) {
|
|
761
|
+
const get = safeGet(input.get, input.onFailure);
|
|
762
|
+
const circuits = await Promise.all([...input.circuitIds.hc.map((id) => readHc(get, id)), ...input.circuitIds.dhw.map((id) => readDhwCircuit(get, id))]);
|
|
763
|
+
const r = HEAT_PUMP_PATHS.readings;
|
|
764
|
+
const s = HEAT_PUMP_PATHS.system;
|
|
765
|
+
const [silent, away, additionalHeater, holiday] = await Promise.all([
|
|
766
|
+
setting(get, s.silent),
|
|
767
|
+
setting(get, s.away),
|
|
768
|
+
setting(get, s.additionalHeater),
|
|
769
|
+
setting(get, s.holiday)
|
|
770
|
+
]);
|
|
771
|
+
const [thermalW, electricalW, compressorPct, supply, ret, outdoor] = await Promise.all([
|
|
772
|
+
num(get, r.thermalW),
|
|
773
|
+
num(get, r.electricalW),
|
|
774
|
+
num(get, r.compressorPct),
|
|
775
|
+
num(get, r.supply),
|
|
776
|
+
num(get, r.return),
|
|
777
|
+
num(get, r.outdoor)
|
|
778
|
+
]);
|
|
779
|
+
const [thermalKwh, electricalKwh, pressureBar, starts] = await Promise.all([
|
|
780
|
+
num(get, r.thermalKwh),
|
|
781
|
+
num(get, r.electricalKwh),
|
|
782
|
+
num(get, r.pressureBar),
|
|
783
|
+
num(get, r.starts)
|
|
784
|
+
]);
|
|
785
|
+
return {
|
|
786
|
+
circuits,
|
|
787
|
+
silent,
|
|
788
|
+
away,
|
|
789
|
+
additionalHeater,
|
|
790
|
+
holiday,
|
|
791
|
+
thermalW,
|
|
792
|
+
electricalW,
|
|
793
|
+
compressorPct,
|
|
794
|
+
supply,
|
|
795
|
+
return: ret,
|
|
796
|
+
outdoor,
|
|
797
|
+
thermalKwh,
|
|
798
|
+
electricalKwh,
|
|
799
|
+
pressureBar,
|
|
800
|
+
starts
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
/** Any failure of a heat-pump read (HTTP 4xx/5xx, a throw) is "not answered", never an error. */
|
|
804
|
+
function safeGet(get, onFailure) {
|
|
805
|
+
return async (path) => {
|
|
806
|
+
try {
|
|
807
|
+
return await get(path);
|
|
808
|
+
} catch (error) {
|
|
809
|
+
onFailure?.(path, error);
|
|
810
|
+
return null;
|
|
811
|
+
}
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
var SYSTEM_MODE_KEYS = [
|
|
815
|
+
"silent",
|
|
816
|
+
"away",
|
|
817
|
+
"additionalHeater",
|
|
818
|
+
"holiday"
|
|
819
|
+
];
|
|
820
|
+
var HEAT_PUMP_ONLY_READINGS = [
|
|
821
|
+
"thermalW",
|
|
822
|
+
"electricalW",
|
|
823
|
+
"compressorPct"
|
|
824
|
+
];
|
|
825
|
+
/**
|
|
826
|
+
* True when the gateway should be offered as a system device (`hp:0`).
|
|
827
|
+
*
|
|
828
|
+
* A system mode counts when the gateway answered it: silent, away, the
|
|
829
|
+
* additional heater, or holiday. An empty string is an answer (holiday often
|
|
830
|
+
* comes back empty); a path that did not answer is not. HomeCom returns no
|
|
831
|
+
* value for silent mode and the auxiliary heater on some heat-pump firmware,
|
|
832
|
+
* so away and holiday alone are enough. A heat-pump-only reading counts too
|
|
833
|
+
* (thermal power, electrical power, compressor modulation). Supply, return,
|
|
834
|
+
* outdoor, pressure, starts and the energy meters do not: a gas boiler's heat
|
|
835
|
+
* source answers flow, pressure and starts, and that is not a heat pump.
|
|
836
|
+
*/
|
|
837
|
+
function hasSystemDevice(reading) {
|
|
838
|
+
if (SYSTEM_MODE_KEYS.some((key) => reading[key] !== null)) return true;
|
|
839
|
+
return HEAT_PUMP_ONLY_READINGS.some((key) => reading[key] !== null);
|
|
840
|
+
}
|
|
841
|
+
/** Which heat-pump paths this gateway answered. One log per gateway corrects the path table. */
|
|
842
|
+
function heatPumpPathCensus(reading) {
|
|
843
|
+
const rows = [
|
|
844
|
+
...systemPathRows(reading),
|
|
845
|
+
...readingPathRows(reading),
|
|
846
|
+
...reading.circuits.flatMap(circuitPathRows)
|
|
847
|
+
];
|
|
848
|
+
const answered = [];
|
|
849
|
+
const unanswered = [];
|
|
850
|
+
for (const row of rows) if (row.present) answered.push(row.path);
|
|
851
|
+
else unanswered.push(row.path);
|
|
852
|
+
return {
|
|
853
|
+
answered,
|
|
854
|
+
unanswered
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
function systemPathRows(reading) {
|
|
858
|
+
const s = HEAT_PUMP_PATHS.system;
|
|
859
|
+
return [
|
|
860
|
+
{
|
|
861
|
+
path: s.silent,
|
|
862
|
+
present: reading.silent !== null
|
|
863
|
+
},
|
|
864
|
+
{
|
|
865
|
+
path: s.away,
|
|
866
|
+
present: reading.away !== null
|
|
867
|
+
},
|
|
868
|
+
{
|
|
869
|
+
path: s.additionalHeater,
|
|
870
|
+
present: reading.additionalHeater !== null
|
|
871
|
+
},
|
|
872
|
+
{
|
|
873
|
+
path: s.holiday,
|
|
874
|
+
present: reading.holiday !== null
|
|
875
|
+
}
|
|
876
|
+
];
|
|
877
|
+
}
|
|
878
|
+
function readingPathRows(reading) {
|
|
879
|
+
const r = HEAT_PUMP_PATHS.readings;
|
|
880
|
+
return [
|
|
881
|
+
{
|
|
882
|
+
path: r.thermalW,
|
|
883
|
+
present: reading.thermalW !== null
|
|
884
|
+
},
|
|
885
|
+
{
|
|
886
|
+
path: r.electricalW,
|
|
887
|
+
present: reading.electricalW !== null
|
|
888
|
+
},
|
|
889
|
+
{
|
|
890
|
+
path: r.compressorPct,
|
|
891
|
+
present: reading.compressorPct !== null
|
|
892
|
+
},
|
|
893
|
+
{
|
|
894
|
+
path: r.supply,
|
|
895
|
+
present: reading.supply !== null
|
|
896
|
+
},
|
|
897
|
+
{
|
|
898
|
+
path: r.return,
|
|
899
|
+
present: reading.return !== null
|
|
900
|
+
},
|
|
901
|
+
{
|
|
902
|
+
path: r.outdoor,
|
|
903
|
+
present: reading.outdoor !== null
|
|
904
|
+
},
|
|
905
|
+
{
|
|
906
|
+
path: r.thermalKwh,
|
|
907
|
+
present: reading.thermalKwh !== null
|
|
908
|
+
},
|
|
909
|
+
{
|
|
910
|
+
path: r.electricalKwh,
|
|
911
|
+
present: reading.electricalKwh !== null
|
|
912
|
+
},
|
|
913
|
+
{
|
|
914
|
+
path: r.pressureBar,
|
|
915
|
+
present: reading.pressureBar !== null
|
|
916
|
+
},
|
|
917
|
+
{
|
|
918
|
+
path: r.starts,
|
|
919
|
+
present: reading.starts !== null
|
|
920
|
+
}
|
|
921
|
+
];
|
|
922
|
+
}
|
|
923
|
+
function circuitPathRows(circuit) {
|
|
924
|
+
if (circuit.kind === "dhw") return dhwPathRows(circuit);
|
|
925
|
+
return hcPathRows(circuit);
|
|
926
|
+
}
|
|
927
|
+
function hcPathRows(circuit) {
|
|
928
|
+
const base = `/heatingCircuits/${circuit.id}`;
|
|
929
|
+
const p = HEAT_PUMP_PATHS.circuit;
|
|
930
|
+
return [
|
|
931
|
+
{
|
|
932
|
+
path: `${base}/${p.operationMode}`,
|
|
933
|
+
present: circuit.operationMode !== null
|
|
934
|
+
},
|
|
935
|
+
{
|
|
936
|
+
path: `${base}/${p.heatCoolMode}`,
|
|
937
|
+
present: circuit.heatCoolMode !== null
|
|
938
|
+
},
|
|
939
|
+
{
|
|
940
|
+
path: `${base}/${p.summerWinter}`,
|
|
941
|
+
present: circuit.summerWinter !== null
|
|
942
|
+
},
|
|
943
|
+
{
|
|
944
|
+
path: `${base}/${p.comfort}`,
|
|
945
|
+
present: circuit.comfort !== null
|
|
946
|
+
},
|
|
947
|
+
{
|
|
948
|
+
path: `${base}/${p.eco}`,
|
|
949
|
+
present: circuit.eco !== null
|
|
950
|
+
},
|
|
951
|
+
{
|
|
952
|
+
path: `${base}/${p.maxFlow}`,
|
|
953
|
+
present: circuit.maxFlow !== null
|
|
954
|
+
},
|
|
955
|
+
{
|
|
956
|
+
path: `${base}/${p.current}`,
|
|
957
|
+
present: circuit.currentTemp !== null
|
|
958
|
+
},
|
|
959
|
+
{
|
|
960
|
+
path: `${base}/${p.target}`,
|
|
961
|
+
present: circuit.targetTemp !== null
|
|
962
|
+
}
|
|
963
|
+
];
|
|
964
|
+
}
|
|
965
|
+
function dhwPathRows(circuit) {
|
|
966
|
+
const base = `/dhwCircuits/${circuit.id}`;
|
|
967
|
+
const p = HEAT_PUMP_PATHS.dhw;
|
|
968
|
+
return [
|
|
969
|
+
{
|
|
970
|
+
path: `${base}/${p.operationMode}`,
|
|
971
|
+
present: circuit.operationMode !== null
|
|
972
|
+
},
|
|
973
|
+
{
|
|
974
|
+
path: `${base}/${p.level}`,
|
|
975
|
+
present: circuit.level !== null
|
|
976
|
+
},
|
|
977
|
+
{
|
|
978
|
+
path: `${base}/${p.chargeDuration}`,
|
|
979
|
+
present: circuit.chargeDuration !== null
|
|
980
|
+
},
|
|
981
|
+
{
|
|
982
|
+
path: `${base}/${p.charge}`,
|
|
983
|
+
present: circuit.charge !== null
|
|
984
|
+
},
|
|
985
|
+
{
|
|
986
|
+
path: `${base}/${p.current}`,
|
|
987
|
+
present: circuit.currentTemp !== null
|
|
988
|
+
},
|
|
989
|
+
{
|
|
990
|
+
path: `${base}/${p.target}`,
|
|
991
|
+
present: circuit.targetTemp !== null
|
|
992
|
+
}
|
|
993
|
+
];
|
|
994
|
+
}
|
|
995
|
+
async function readHc(get, id) {
|
|
996
|
+
const base = `/heatingCircuits/${id}`;
|
|
997
|
+
const p = HEAT_PUMP_PATHS.circuit;
|
|
998
|
+
const [operationMode, heatCoolMode, summerWinter, comfort, eco, maxFlow, current, target] = await Promise.all([
|
|
999
|
+
setting(get, `${base}/${p.operationMode}`),
|
|
1000
|
+
setting(get, `${base}/${p.heatCoolMode}`),
|
|
1001
|
+
setting(get, `${base}/${p.summerWinter}`),
|
|
1002
|
+
setting(get, `${base}/${p.comfort}`),
|
|
1003
|
+
setting(get, `${base}/${p.eco}`),
|
|
1004
|
+
setting(get, `${base}/${p.maxFlow}`),
|
|
1005
|
+
num(get, `${base}/${p.current}`),
|
|
1006
|
+
num(get, `${base}/${p.target}`)
|
|
1007
|
+
]);
|
|
1008
|
+
return {
|
|
1009
|
+
id,
|
|
1010
|
+
kind: "hc",
|
|
1011
|
+
currentTemp: current,
|
|
1012
|
+
targetTemp: target,
|
|
1013
|
+
operationMode,
|
|
1014
|
+
heatCoolMode,
|
|
1015
|
+
summerWinter,
|
|
1016
|
+
comfort,
|
|
1017
|
+
eco,
|
|
1018
|
+
maxFlow,
|
|
1019
|
+
level: null,
|
|
1020
|
+
chargeDuration: null,
|
|
1021
|
+
charge: null
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
async function readDhwCircuit(get, id) {
|
|
1025
|
+
const base = `/dhwCircuits/${id}`;
|
|
1026
|
+
const p = HEAT_PUMP_PATHS.dhw;
|
|
1027
|
+
const [operationMode, level, chargeDuration, charge, current, target] = await Promise.all([
|
|
1028
|
+
setting(get, `${base}/${p.operationMode}`),
|
|
1029
|
+
setting(get, `${base}/${p.level}`),
|
|
1030
|
+
setting(get, `${base}/${p.chargeDuration}`),
|
|
1031
|
+
setting(get, `${base}/${p.charge}`),
|
|
1032
|
+
num(get, `${base}/${p.current}`),
|
|
1033
|
+
num(get, `${base}/${p.target}`)
|
|
1034
|
+
]);
|
|
1035
|
+
return {
|
|
1036
|
+
id,
|
|
1037
|
+
kind: "dhw",
|
|
1038
|
+
currentTemp: current,
|
|
1039
|
+
targetTemp: target,
|
|
1040
|
+
operationMode,
|
|
1041
|
+
heatCoolMode: null,
|
|
1042
|
+
summerWinter: null,
|
|
1043
|
+
comfort: null,
|
|
1044
|
+
eco: null,
|
|
1045
|
+
maxFlow: null,
|
|
1046
|
+
level,
|
|
1047
|
+
chargeDuration,
|
|
1048
|
+
charge
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
async function setting(get, path) {
|
|
1052
|
+
const envelope = readEnvelope(await get(path));
|
|
1053
|
+
return envelope === null ? null : settingOf(path, envelope);
|
|
1054
|
+
}
|
|
1055
|
+
async function num(get, path) {
|
|
1056
|
+
const envelope = readEnvelope(await get(path));
|
|
1057
|
+
return envelope !== null && typeof envelope.value === "number" && Number.isFinite(envelope.value) ? envelope.value : null;
|
|
1058
|
+
}
|
|
1059
|
+
function settingOf(path, envelope) {
|
|
1060
|
+
return {
|
|
1061
|
+
path,
|
|
1062
|
+
value: envelope.value,
|
|
1063
|
+
min: envelope.min,
|
|
1064
|
+
max: envelope.max,
|
|
1065
|
+
allowed: envelope.allowed
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
function heatPumpStatusFrom(reading, now) {
|
|
1069
|
+
return {
|
|
1070
|
+
circuits: reading.circuits.map(circuitOf),
|
|
1071
|
+
system: {
|
|
1072
|
+
silent: optionOf(reading.silent),
|
|
1073
|
+
away: optionOf(reading.away),
|
|
1074
|
+
additionalHeater: optionOf(reading.additionalHeater),
|
|
1075
|
+
holiday: optionOf(reading.holiday)
|
|
1076
|
+
},
|
|
1077
|
+
power: {
|
|
1078
|
+
thermalW: reading.thermalW,
|
|
1079
|
+
electricalW: reading.electricalW,
|
|
1080
|
+
compressorPct: reading.compressorPct
|
|
1081
|
+
},
|
|
1082
|
+
temperatures: {
|
|
1083
|
+
supply: reading.supply,
|
|
1084
|
+
return: reading.return,
|
|
1085
|
+
outdoor: reading.outdoor
|
|
1086
|
+
},
|
|
1087
|
+
energy: {
|
|
1088
|
+
thermalKwh: reading.thermalKwh,
|
|
1089
|
+
electricalKwh: reading.electricalKwh
|
|
1090
|
+
},
|
|
1091
|
+
waterPressureBar: reading.pressureBar,
|
|
1092
|
+
starts: reading.starts,
|
|
1093
|
+
lastFetchedAt: now
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
function circuitOf(c) {
|
|
1097
|
+
const kind = kindOf(c);
|
|
1098
|
+
return {
|
|
1099
|
+
id: c.id,
|
|
1100
|
+
kind,
|
|
1101
|
+
label: labelOf(c),
|
|
1102
|
+
icon: kind === "dhw" ? "droplets" : kind === "cooling" ? "snowflake" : "flame",
|
|
1103
|
+
currentTemp: c.currentTemp,
|
|
1104
|
+
targetTemp: c.targetTemp,
|
|
1105
|
+
mode: modeValue(c.operationMode),
|
|
1106
|
+
active: null,
|
|
1107
|
+
operationMode: optionOf(c.operationMode),
|
|
1108
|
+
heatCoolMode: optionOf(c.heatCoolMode),
|
|
1109
|
+
summerWinter: optionOf(c.summerWinter),
|
|
1110
|
+
comfortSetpoint: rangeOf(c.comfort),
|
|
1111
|
+
ecoSetpoint: rangeOf(c.eco),
|
|
1112
|
+
maxFlowTemp: rangeOf(c.maxFlow),
|
|
1113
|
+
temperatureLevel: optionOf(c.level),
|
|
1114
|
+
chargeDuration: rangeOf(c.chargeDuration),
|
|
1115
|
+
extraHotWater: c.charge === null ? null : textOf$1(c.charge) === "start"
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
function kindOf(c) {
|
|
1119
|
+
if (c.kind === "dhw") return "dhw";
|
|
1120
|
+
return textOf$1(c.heatCoolMode) === "cooling" ? "cooling" : "heating";
|
|
1121
|
+
}
|
|
1122
|
+
function labelOf(c) {
|
|
1123
|
+
const n = c.id.replace(/^\D+/, "");
|
|
1124
|
+
if (c.kind === "dhw") return n === "1" || n === "" ? "Hot water" : `Hot water ${n}`;
|
|
1125
|
+
return n === "" ? "Heating circuit" : `Heating circuit ${n}`;
|
|
1126
|
+
}
|
|
1127
|
+
function textOf$1(s) {
|
|
1128
|
+
return s !== null && typeof s.value === "string" && s.value.length > 0 ? s.value : null;
|
|
1129
|
+
}
|
|
1130
|
+
/**
|
|
1131
|
+
* A mode the gateway answered with an empty value is off (or "none" when the
|
|
1132
|
+
* gateway did not list off). A mode it did not answer at all stays null.
|
|
1133
|
+
*/
|
|
1134
|
+
function modeValue(s) {
|
|
1135
|
+
if (s === null || typeof s.value !== "string") return null;
|
|
1136
|
+
if (s.value.length > 0) return s.value;
|
|
1137
|
+
return s.allowed.includes("off") ? "off" : "none";
|
|
1138
|
+
}
|
|
1139
|
+
function optionOf(s) {
|
|
1140
|
+
if (s === null) return null;
|
|
1141
|
+
return {
|
|
1142
|
+
value: modeValue(s),
|
|
1143
|
+
allowed: [...s.allowed]
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
function rangeOf(s) {
|
|
1147
|
+
if (s === null) return null;
|
|
1148
|
+
return {
|
|
1149
|
+
value: typeof s.value === "number" && Number.isFinite(s.value) ? s.value : null,
|
|
1150
|
+
min: s.min,
|
|
1151
|
+
max: s.max,
|
|
1152
|
+
step: null
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
function requireCircuit(reading, circuitId, want) {
|
|
1156
|
+
const found = reading.circuits.find((c) => c.id === circuitId && c.kind === want);
|
|
1157
|
+
if (found === void 0) throw new HomeComHeatPumpRefusalError(`HomeCom heat pump has no ${want} circuit ${circuitId}`);
|
|
1158
|
+
return found;
|
|
1159
|
+
}
|
|
1160
|
+
function optionWrite(s, what, mode) {
|
|
1161
|
+
if (s === null) throw new HomeComHeatPumpRefusalError(`HomeCom heat pump: ${what} is not available`);
|
|
1162
|
+
if (s.allowed.length === 0) throw new HomeComHeatPumpRefusalError(`HomeCom heat pump: ${what} is read-only on this gateway`);
|
|
1163
|
+
if (!s.allowed.includes(mode)) throw new HomeComHeatPumpRefusalError(`HomeCom heat pump: "${mode}" is not an accepted value of ${what} (${s.allowed.join(", ")})`);
|
|
1164
|
+
return {
|
|
1165
|
+
path: s.path,
|
|
1166
|
+
value: mode
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
function rangeWrite(s, what, value) {
|
|
1170
|
+
if (s === null) throw new HomeComHeatPumpRefusalError(`HomeCom heat pump: ${what} is not available`);
|
|
1171
|
+
if (!Number.isFinite(value)) throw new HomeComHeatPumpRefusalError(`HomeCom heat pump: ${what} needs a finite number`);
|
|
1172
|
+
if (s.min === null || s.max === null) throw new HomeComHeatPumpRefusalError(`HomeCom heat pump: ${what} has no limits from the gateway`);
|
|
1173
|
+
if (value < s.min || value > s.max) throw new HomeComHeatPumpRefusalError(`HomeCom heat pump: ${value} is outside ${what} limits ${s.min}..${s.max}`);
|
|
1174
|
+
return {
|
|
1175
|
+
path: s.path,
|
|
1176
|
+
value
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
function systemModeWrite(reading, which, mode) {
|
|
1180
|
+
return optionWrite(reading[which], `${which} mode`, mode);
|
|
1181
|
+
}
|
|
1182
|
+
function circuitOptionWrite(reading, circuitId, which, mode) {
|
|
1183
|
+
return optionWrite(requireCircuit(reading, circuitId, "hc")[which], `${circuitId} ${which}`, mode);
|
|
1184
|
+
}
|
|
1185
|
+
function dhwOptionWrite(reading, circuitId, which, mode) {
|
|
1186
|
+
return optionWrite(requireCircuit(reading, circuitId, "dhw")[which], `${circuitId} ${which}`, mode);
|
|
1187
|
+
}
|
|
1188
|
+
function circuitRangeWrite(reading, circuitId, which, value) {
|
|
1189
|
+
return rangeWrite(requireCircuit(reading, circuitId, "hc")[which], `${circuitId} ${which}`, value);
|
|
1190
|
+
}
|
|
1191
|
+
function dhwChargeDurationWrite(reading, circuitId, minutes) {
|
|
1192
|
+
return rangeWrite(requireCircuit(reading, circuitId, "dhw").chargeDuration, `${circuitId} charge duration`, minutes);
|
|
1193
|
+
}
|
|
1194
|
+
function extraHotWaterWrite(reading, circuitId) {
|
|
1195
|
+
const charge = requireCircuit(reading, circuitId, "dhw").charge;
|
|
1196
|
+
return optionWrite(charge, `${circuitId} extra hot water`, "start");
|
|
1197
|
+
}
|
|
1198
|
+
//#endregion
|
|
1199
|
+
//#region src/homecom/homecom-oauth.ts
|
|
1200
|
+
/**
|
|
1201
|
+
* Public OAuth client of the HomeCom Easy app, signed in through SingleKey ID.
|
|
1202
|
+
*
|
|
1203
|
+
* A PKCE client has no secret. The values are the ones SingleKey accepts for this client id
|
|
1204
|
+
* today: the same authorize query the MIT-licensed `homecom_alt` client records
|
|
1205
|
+
* (`OAUTH_LOGIN_PARAMS` — parameters, not copied code) and that the Home Assistant integration
|
|
1206
|
+
* opens. `pointt.gateway.resource.dopair` is not a scope this client may request; SingleKey
|
|
1207
|
+
* answers `misconfigured-application` and never shows the login page. The scope it does accept
|
|
1208
|
+
* names `pointt.gateway.resource.dashapp`, and the Bosch app sends `style_id=tt_bsch`.
|
|
1209
|
+
*/
|
|
1210
|
+
var HOMECOM_CLIENT_ID = "762162C0-FA2D-4540-AE66-6489F189FADC";
|
|
1211
|
+
var HOMECOM_AUTHORIZE_URL = "https://singlekey-id.com/auth/connect/authorize";
|
|
1212
|
+
var HOMECOM_TOKEN_URL = "https://singlekey-id.com/auth/connect/token";
|
|
1213
|
+
var HOMECOM_STYLE_ID = "tt_bsch";
|
|
1214
|
+
var HOMECOM_SCOPE = [
|
|
1215
|
+
"openid",
|
|
1216
|
+
"email",
|
|
1217
|
+
"profile",
|
|
1218
|
+
"offline_access",
|
|
1219
|
+
"pointt.gateway.claiming",
|
|
1220
|
+
"pointt.gateway.removal",
|
|
1221
|
+
"pointt.gateway.list",
|
|
1222
|
+
"pointt.gateway.users",
|
|
1223
|
+
"pointt.gateway.resource.dashapp",
|
|
1224
|
+
"pointt.castt.flow.token-exchange",
|
|
1225
|
+
"bacon",
|
|
1226
|
+
"hcc.tariff.read"
|
|
1227
|
+
].join(" ");
|
|
1228
|
+
//#endregion
|
|
719
1229
|
//#region src/homecom/homecom-api-client.ts
|
|
720
1230
|
var HomeComAuthError = class extends Error {
|
|
721
1231
|
name = "HomeComAuthError";
|
|
@@ -796,6 +1306,7 @@ function createHomeComApiClient(config, gatewayId, deps = {}) {
|
|
|
796
1306
|
let last = null;
|
|
797
1307
|
let refreshTail = Promise.resolve();
|
|
798
1308
|
const dropped = /* @__PURE__ */ new Set();
|
|
1309
|
+
let heatPumpPathsLogged = false;
|
|
799
1310
|
const base = homeComApiBase(config.region);
|
|
800
1311
|
function authHeaders() {
|
|
801
1312
|
return {
|
|
@@ -929,15 +1440,56 @@ function createHomeComApiClient(config, gatewayId, deps = {}) {
|
|
|
929
1440
|
}
|
|
930
1441
|
async function fetchSystem() {
|
|
931
1442
|
const resolved = await resolveDeviceType();
|
|
932
|
-
const
|
|
1443
|
+
const withHeatPump = await attachHeatPump(await readPointtSnapshot({
|
|
933
1444
|
deviceType: resolved.deviceType,
|
|
934
1445
|
family: resolved.family,
|
|
935
1446
|
gatewayId,
|
|
936
1447
|
get: getResource,
|
|
937
1448
|
logDrop
|
|
1449
|
+
}));
|
|
1450
|
+
last = withHeatPump;
|
|
1451
|
+
return withHeatPump;
|
|
1452
|
+
}
|
|
1453
|
+
async function attachHeatPump(snapshot) {
|
|
1454
|
+
if (snapshot.family !== "hydronic" || snapshot.deviceType === "rrc2") return snapshot;
|
|
1455
|
+
const dhwId = snapshot.dhw?.circuitId;
|
|
1456
|
+
const heatPump = await readHeatPump({
|
|
1457
|
+
get: getResource,
|
|
1458
|
+
onFailure: logHeatPumpFailure,
|
|
1459
|
+
circuitIds: {
|
|
1460
|
+
hc: snapshot.circuits.map((circuit) => circuit.id),
|
|
1461
|
+
dhw: dhwId === void 0 ? [] : [dhwId]
|
|
1462
|
+
}
|
|
1463
|
+
});
|
|
1464
|
+
logHeatPumpPaths(heatPump);
|
|
1465
|
+
return hasSystemDevice(heatPump) ? {
|
|
1466
|
+
...snapshot,
|
|
1467
|
+
heatPump
|
|
1468
|
+
} : snapshot;
|
|
1469
|
+
}
|
|
1470
|
+
function logHeatPumpPaths(reading) {
|
|
1471
|
+
if (heatPumpPathsLogged) return;
|
|
1472
|
+
heatPumpPathsLogged = true;
|
|
1473
|
+
const census = heatPumpPathCensus(reading);
|
|
1474
|
+
logger?.info("HomeCom heat-pump paths", {
|
|
1475
|
+
tags: { gatewayId },
|
|
1476
|
+
meta: {
|
|
1477
|
+
answered: census.answered,
|
|
1478
|
+
unanswered: census.unanswered
|
|
1479
|
+
}
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
1482
|
+
function logHeatPumpFailure(path, error) {
|
|
1483
|
+
const key = `heat-pump-read:${path}`;
|
|
1484
|
+
if (dropped.has(key)) return;
|
|
1485
|
+
dropped.add(key);
|
|
1486
|
+
logger?.warn("HomeCom heat-pump resource could not be read; it is skipped", {
|
|
1487
|
+
tags: { gatewayId },
|
|
1488
|
+
meta: {
|
|
1489
|
+
path,
|
|
1490
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1491
|
+
}
|
|
938
1492
|
});
|
|
939
|
-
last = snapshot;
|
|
940
|
-
return snapshot;
|
|
941
1493
|
}
|
|
942
1494
|
async function probePath(path) {
|
|
943
1495
|
const res = await authed(homeComResourceUrl(gatewayId, path), {
|
|
@@ -991,6 +1543,9 @@ function createHomeComApiClient(config, gatewayId, deps = {}) {
|
|
|
991
1543
|
const snapshot = await requireSnapshot();
|
|
992
1544
|
if (snapshot.family !== "rac") logDrop("swing is not a resource on this device type");
|
|
993
1545
|
await putWrite(swingWrite(snapshot, "horizontal", on));
|
|
1546
|
+
},
|
|
1547
|
+
async writeHeatPump(writes) {
|
|
1548
|
+
for (const write of writes) await putWrite(write);
|
|
994
1549
|
}
|
|
995
1550
|
};
|
|
996
1551
|
}
|
|
@@ -1255,8 +1810,7 @@ function buildHomeComGatewayPersister(deps) {
|
|
|
1255
1810
|
function buildHomeComCandidates(input) {
|
|
1256
1811
|
const out = [];
|
|
1257
1812
|
for (const dev of input.devices) {
|
|
1258
|
-
const type = dev.kind
|
|
1259
|
-
const caps = dev.kind === "heating-circuit" ? ["climate-control"] : ["water-heater"];
|
|
1813
|
+
const { type, caps } = shapeOf(dev.kind);
|
|
1260
1814
|
const adoptedId = input.adopted.get(dev.nativeId) ?? null;
|
|
1261
1815
|
out.push({
|
|
1262
1816
|
childNativeId: dev.nativeId,
|
|
@@ -1271,6 +1825,22 @@ function buildHomeComCandidates(input) {
|
|
|
1271
1825
|
}
|
|
1272
1826
|
return out;
|
|
1273
1827
|
}
|
|
1828
|
+
function shapeOf(kind) {
|
|
1829
|
+
switch (kind) {
|
|
1830
|
+
case "heating-circuit": return {
|
|
1831
|
+
type: DeviceType.Climate,
|
|
1832
|
+
caps: ["climate-control"]
|
|
1833
|
+
};
|
|
1834
|
+
case "dhw": return {
|
|
1835
|
+
type: DeviceType.WaterHeater,
|
|
1836
|
+
caps: ["water-heater"]
|
|
1837
|
+
};
|
|
1838
|
+
case "heat-pump": return {
|
|
1839
|
+
type: DeviceType.HeatPump,
|
|
1840
|
+
caps: ["heat-pump-status", "heat-pump-control"]
|
|
1841
|
+
};
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1274
1844
|
//#endregion
|
|
1275
1845
|
//#region src/homecom/homecom-adoption-provider.ts
|
|
1276
1846
|
var DEVICES_FILTER = {
|
|
@@ -1455,6 +2025,11 @@ function buildDiscoverables(snapshot) {
|
|
|
1455
2025
|
name: "Domestic hot water",
|
|
1456
2026
|
kind: "dhw"
|
|
1457
2027
|
});
|
|
2028
|
+
if (snapshot.heatPump !== void 0) out.push({
|
|
2029
|
+
nativeId: "hp:0",
|
|
2030
|
+
name: "Heat pump",
|
|
2031
|
+
kind: "heat-pump"
|
|
2032
|
+
});
|
|
1458
2033
|
return out;
|
|
1459
2034
|
}
|
|
1460
2035
|
var homeComConnections = new HomeComConnectionResolver();
|
|
@@ -1941,7 +2516,7 @@ var homeComClimateSchema = object({
|
|
|
1941
2516
|
integrationId: string().optional()
|
|
1942
2517
|
});
|
|
1943
2518
|
var CLIMATE_CAP = "climate-control";
|
|
1944
|
-
var COLD_START$
|
|
2519
|
+
var COLD_START$2 = {
|
|
1945
2520
|
mode: "off",
|
|
1946
2521
|
availableModes: [],
|
|
1947
2522
|
fanMode: "",
|
|
@@ -1976,14 +2551,14 @@ var HomeComClimateDevice = class extends BaseDevice {
|
|
|
1976
2551
|
async onActivate() {
|
|
1977
2552
|
await super.onActivate();
|
|
1978
2553
|
this.registerClimateCap();
|
|
1979
|
-
this.runtimeState.setCapState(CLIMATE_CAP, COLD_START$
|
|
2554
|
+
this.runtimeState.setCapState(CLIMATE_CAP, COLD_START$2);
|
|
1980
2555
|
this.refreshFromSnapshot();
|
|
1981
2556
|
}
|
|
1982
2557
|
refreshFromSnapshot() {
|
|
1983
2558
|
const circuit = homeComConnections.getSnapshot(this.#brokerId)?.circuits.find((c) => c.id === this.#circuitId);
|
|
1984
2559
|
if (!circuit) {
|
|
1985
2560
|
this.runtimeState.setCapState(CLIMATE_CAP, {
|
|
1986
|
-
...COLD_START$
|
|
2561
|
+
...COLD_START$2,
|
|
1987
2562
|
lastFetchedAt: Date.now()
|
|
1988
2563
|
});
|
|
1989
2564
|
return;
|
|
@@ -1999,7 +2574,7 @@ var HomeComClimateDevice = class extends BaseDevice {
|
|
|
1999
2574
|
this.ctx.registerNativeCap(climateControlCapability, {
|
|
2000
2575
|
getStatus: async () => {
|
|
2001
2576
|
this.refreshFromSnapshot();
|
|
2002
|
-
return this.runtimeState.getCapState(CLIMATE_CAP) ?? COLD_START$
|
|
2577
|
+
return this.runtimeState.getCapState(CLIMATE_CAP) ?? COLD_START$2;
|
|
2003
2578
|
},
|
|
2004
2579
|
setMode: async ({ mode }) => {
|
|
2005
2580
|
const apiMode = hvacModeToHomeCom(mode);
|
|
@@ -2051,7 +2626,7 @@ var homeComWaterHeaterSchema = object({
|
|
|
2051
2626
|
integrationId: string().optional()
|
|
2052
2627
|
});
|
|
2053
2628
|
var WH_CAP = "water-heater";
|
|
2054
|
-
var COLD_START = {
|
|
2629
|
+
var COLD_START$1 = {
|
|
2055
2630
|
currentTemp: null,
|
|
2056
2631
|
targetTemp: null,
|
|
2057
2632
|
operationMode: null,
|
|
@@ -2077,14 +2652,14 @@ var HomeComWaterHeaterDevice = class extends BaseDevice {
|
|
|
2077
2652
|
async onActivate() {
|
|
2078
2653
|
await super.onActivate();
|
|
2079
2654
|
this.registerWaterHeaterCap();
|
|
2080
|
-
this.runtimeState.setCapState(WH_CAP, COLD_START);
|
|
2655
|
+
this.runtimeState.setCapState(WH_CAP, COLD_START$1);
|
|
2081
2656
|
this.refreshFromSnapshot();
|
|
2082
2657
|
}
|
|
2083
2658
|
refreshFromSnapshot() {
|
|
2084
2659
|
const dhw = homeComConnections.getSnapshot(this.#brokerId)?.dhw;
|
|
2085
2660
|
if (!dhw) {
|
|
2086
2661
|
this.runtimeState.setCapState(WH_CAP, {
|
|
2087
|
-
...COLD_START,
|
|
2662
|
+
...COLD_START$1,
|
|
2088
2663
|
lastChangedAt: Date.now()
|
|
2089
2664
|
});
|
|
2090
2665
|
return;
|
|
@@ -2100,7 +2675,7 @@ var HomeComWaterHeaterDevice = class extends BaseDevice {
|
|
|
2100
2675
|
this.ctx.registerNativeCap(waterHeaterCapability, {
|
|
2101
2676
|
getStatus: async () => {
|
|
2102
2677
|
this.refreshFromSnapshot();
|
|
2103
|
-
return this.runtimeState.getCapState(WH_CAP) ?? COLD_START;
|
|
2678
|
+
return this.runtimeState.getCapState(WH_CAP) ?? COLD_START$1;
|
|
2104
2679
|
},
|
|
2105
2680
|
setTargetTemp: async ({ temp }) => {
|
|
2106
2681
|
await this.requireClient().setDhwTarget(temp);
|
|
@@ -2117,13 +2692,157 @@ var HomeComWaterHeaterDevice = class extends BaseDevice {
|
|
|
2117
2692
|
}
|
|
2118
2693
|
};
|
|
2119
2694
|
//#endregion
|
|
2695
|
+
//#region src/homecom/devices/homecom-heat-pump-device.ts
|
|
2696
|
+
var homeComHeatPumpSchema = object({
|
|
2697
|
+
nativeId: string().min(1),
|
|
2698
|
+
brokerId: string().min(1),
|
|
2699
|
+
system: literal("homecom").optional(),
|
|
2700
|
+
integrationId: string().optional()
|
|
2701
|
+
});
|
|
2702
|
+
var STATUS_CAP = "heat-pump-status";
|
|
2703
|
+
var COLD_START = {
|
|
2704
|
+
circuits: [],
|
|
2705
|
+
system: {
|
|
2706
|
+
silent: null,
|
|
2707
|
+
away: null,
|
|
2708
|
+
additionalHeater: null,
|
|
2709
|
+
holiday: null
|
|
2710
|
+
},
|
|
2711
|
+
power: {
|
|
2712
|
+
thermalW: null,
|
|
2713
|
+
electricalW: null,
|
|
2714
|
+
compressorPct: null
|
|
2715
|
+
},
|
|
2716
|
+
temperatures: {
|
|
2717
|
+
supply: null,
|
|
2718
|
+
return: null,
|
|
2719
|
+
outdoor: null
|
|
2720
|
+
},
|
|
2721
|
+
energy: {
|
|
2722
|
+
thermalKwh: null,
|
|
2723
|
+
electricalKwh: null
|
|
2724
|
+
},
|
|
2725
|
+
waterPressureBar: null,
|
|
2726
|
+
starts: null,
|
|
2727
|
+
lastFetchedAt: 0
|
|
2728
|
+
};
|
|
2729
|
+
/**
|
|
2730
|
+
* A heat pump on a hydronic HomeCom gateway (D1026). The slice and every limit
|
|
2731
|
+
* come from the gateway reading the integration poll already holds; a write is
|
|
2732
|
+
* checked against those limits first and the reading is refreshed after it.
|
|
2733
|
+
*/
|
|
2734
|
+
var HomeComHeatPumpDevice = class extends BaseDevice {
|
|
2735
|
+
features = [];
|
|
2736
|
+
#brokerId;
|
|
2737
|
+
/** True while the gateway has no heat-pump reading, so the log marks the transitions only. */
|
|
2738
|
+
#noReading = false;
|
|
2739
|
+
constructor(ctx) {
|
|
2740
|
+
const persisted = homeComHeatPumpSchema.parse(ctx.persistedConfig ?? {});
|
|
2741
|
+
super(ctx, homeComHeatPumpSchema, { type: ctx.deviceMeta.type });
|
|
2742
|
+
this.#brokerId = persisted.brokerId;
|
|
2743
|
+
this.online = true;
|
|
2744
|
+
this.updateSourceInfo({
|
|
2745
|
+
id: persisted.nativeId,
|
|
2746
|
+
system: "homecom"
|
|
2747
|
+
});
|
|
2748
|
+
}
|
|
2749
|
+
async onActivate() {
|
|
2750
|
+
await super.onActivate();
|
|
2751
|
+
this.registerStatusCap();
|
|
2752
|
+
this.registerControlCap();
|
|
2753
|
+
this.runtimeState.setCapState(STATUS_CAP, COLD_START);
|
|
2754
|
+
this.refreshFromSnapshot();
|
|
2755
|
+
}
|
|
2756
|
+
reading() {
|
|
2757
|
+
return homeComConnections.getSnapshot(this.#brokerId)?.heatPump ?? null;
|
|
2758
|
+
}
|
|
2759
|
+
refreshFromSnapshot() {
|
|
2760
|
+
const reading = this.reading();
|
|
2761
|
+
if (reading === null) {
|
|
2762
|
+
if (!this.#noReading) {
|
|
2763
|
+
this.#noReading = true;
|
|
2764
|
+
this.ctx.logger.warn("HomeCom heat pump: the gateway returned no heat-pump reading", { tags: { deviceId: this.ctx.id } });
|
|
2765
|
+
}
|
|
2766
|
+
this.runtimeState.setCapState(STATUS_CAP, {
|
|
2767
|
+
...COLD_START,
|
|
2768
|
+
lastFetchedAt: Date.now()
|
|
2769
|
+
});
|
|
2770
|
+
return;
|
|
2771
|
+
}
|
|
2772
|
+
if (this.#noReading) {
|
|
2773
|
+
this.#noReading = false;
|
|
2774
|
+
this.ctx.logger.info("HomeCom heat pump: the gateway answers with a heat-pump reading again", { tags: { deviceId: this.ctx.id } });
|
|
2775
|
+
}
|
|
2776
|
+
this.runtimeState.setCapState(STATUS_CAP, heatPumpStatusFrom(reading, Date.now()));
|
|
2777
|
+
}
|
|
2778
|
+
requireClient() {
|
|
2779
|
+
const client = homeComConnections.getClient(this.#brokerId);
|
|
2780
|
+
if (!client) throw new Error("HomeCom client unavailable (heat pump)");
|
|
2781
|
+
return client;
|
|
2782
|
+
}
|
|
2783
|
+
requireReading() {
|
|
2784
|
+
const reading = this.reading();
|
|
2785
|
+
if (reading === null) throw new Error("HomeCom heat pump: no reading from the gateway yet");
|
|
2786
|
+
return reading;
|
|
2787
|
+
}
|
|
2788
|
+
/** Validate against the reading, send, then re-read so the slice shows what the gateway holds. */
|
|
2789
|
+
async apply(build) {
|
|
2790
|
+
const write = build(this.requireReading());
|
|
2791
|
+
const client = this.requireClient();
|
|
2792
|
+
await client.writeHeatPump([write]);
|
|
2793
|
+
this.ctx.logger.info("HomeCom heat pump: value written", {
|
|
2794
|
+
tags: { deviceId: this.ctx.id },
|
|
2795
|
+
meta: { path: write.path }
|
|
2796
|
+
});
|
|
2797
|
+
try {
|
|
2798
|
+
const snapshot = await client.fetchSystem();
|
|
2799
|
+
homeComConnections.updateSnapshot(this.#brokerId, snapshot);
|
|
2800
|
+
} catch (error) {
|
|
2801
|
+
this.ctx.logger.warn("HomeCom heat pump: value written, the refresh failed", {
|
|
2802
|
+
tags: { deviceId: this.ctx.id },
|
|
2803
|
+
meta: {
|
|
2804
|
+
path: write.path,
|
|
2805
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2806
|
+
}
|
|
2807
|
+
});
|
|
2808
|
+
return;
|
|
2809
|
+
}
|
|
2810
|
+
this.refreshFromSnapshot();
|
|
2811
|
+
}
|
|
2812
|
+
registerStatusCap() {
|
|
2813
|
+
this.ctx.registerNativeCap(heatPumpStatusCapability, { getStatus: async () => {
|
|
2814
|
+
this.refreshFromSnapshot();
|
|
2815
|
+
return this.runtimeState.getCapState(STATUS_CAP) ?? COLD_START;
|
|
2816
|
+
} });
|
|
2817
|
+
}
|
|
2818
|
+
registerControlCap() {
|
|
2819
|
+
this.ctx.registerNativeCap(heatPumpControlCapability, {
|
|
2820
|
+
setSilentMode: ({ mode }) => this.apply((r) => systemModeWrite(r, "silent", mode)),
|
|
2821
|
+
setAwayMode: ({ mode }) => this.apply((r) => systemModeWrite(r, "away", mode)),
|
|
2822
|
+
setAdditionalHeaterMode: ({ mode }) => this.apply((r) => systemModeWrite(r, "additionalHeater", mode)),
|
|
2823
|
+
setHolidayMode: ({ mode }) => this.apply((r) => systemModeWrite(r, "holiday", mode)),
|
|
2824
|
+
setCircuitOperationMode: ({ circuitId, mode }) => this.apply((r) => circuitOptionWrite(r, circuitId, "operationMode", mode)),
|
|
2825
|
+
setCircuitHeatCoolMode: ({ circuitId, mode }) => this.apply((r) => circuitOptionWrite(r, circuitId, "heatCoolMode", mode)),
|
|
2826
|
+
setCircuitSummerWinter: ({ circuitId, mode }) => this.apply((r) => circuitOptionWrite(r, circuitId, "summerWinter", mode)),
|
|
2827
|
+
setCircuitComfortSetpoint: ({ circuitId, temp }) => this.apply((r) => circuitRangeWrite(r, circuitId, "comfort", temp)),
|
|
2828
|
+
setCircuitEcoSetpoint: ({ circuitId, temp }) => this.apply((r) => circuitRangeWrite(r, circuitId, "eco", temp)),
|
|
2829
|
+
setCircuitMaxFlowTemp: ({ circuitId, temp }) => this.apply((r) => circuitRangeWrite(r, circuitId, "maxFlow", temp)),
|
|
2830
|
+
setDhwOperationMode: ({ circuitId, mode }) => this.apply((r) => dhwOptionWrite(r, circuitId, "operationMode", mode)),
|
|
2831
|
+
setDhwTemperatureLevel: ({ circuitId, level }) => this.apply((r) => dhwOptionWrite(r, circuitId, "level", level)),
|
|
2832
|
+
setDhwChargeDuration: ({ circuitId, minutes }) => this.apply((r) => dhwChargeDurationWrite(r, circuitId, minutes)),
|
|
2833
|
+
startExtraHotWater: ({ circuitId }) => this.apply((r) => extraHotWaterWrite(r, circuitId))
|
|
2834
|
+
});
|
|
2835
|
+
}
|
|
2836
|
+
};
|
|
2837
|
+
//#endregion
|
|
2120
2838
|
//#region src/homecom/addon.ts
|
|
2121
2839
|
var BoschHomeComProviderAddon = class extends BaseDeviceProvider {
|
|
2122
2840
|
addonId = "bosch-homecom";
|
|
2123
2841
|
providerName = "Bosch HomeCom";
|
|
2124
2842
|
deviceClasses = {
|
|
2125
2843
|
[DeviceType.Climate]: HomeComClimateDevice,
|
|
2126
|
-
[DeviceType.WaterHeater]: HomeComWaterHeaterDevice
|
|
2844
|
+
[DeviceType.WaterHeater]: HomeComWaterHeaterDevice,
|
|
2845
|
+
[DeviceType.HeatPump]: HomeComHeatPumpDevice
|
|
2127
2846
|
};
|
|
2128
2847
|
clients = null;
|
|
2129
2848
|
instances = null;
|