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