@camstack/addon-provider-bosch 0.1.45 → 0.1.47
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-DAEciV_Q.mjs → account-integration-offline-Bie4aS5e.mjs} +1 -1
- package/dist/{account-integration-offline-BiXbzZsg.js → account-integration-offline-C6tzFB-H.js} +1 -1
- package/dist/{bosch-instances-DdklwoKS.js → bosch-instances-D0eF-Jjy.js} +181 -30
- package/dist/{bosch-instances-DXID-jMD.mjs → bosch-instances-DAqkAsRO.mjs} +181 -30
- package/dist/home-connect/addon.js +2 -2
- package/dist/home-connect/addon.mjs +2 -2
- package/dist/homecom/addon.js +439 -113
- package/dist/homecom/addon.mjs +439 -113
- package/dist/shc/addon.js +1 -1
- package/dist/shc/addon.mjs +1 -1
- package/package.json +2 -2
package/dist/homecom/addon.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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-
|
|
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-DAqkAsRO.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-Bie4aS5e.mjs";
|
|
3
3
|
import { createHash, randomBytes } from "node:crypto";
|
|
4
4
|
//#region src/homecom/config.ts
|
|
5
5
|
var HOMECOM_BRANDS = ["bosch", "buderus"];
|
|
@@ -448,7 +448,7 @@ async function readHydronic(input) {
|
|
|
448
448
|
const heating = await input.get("/heatingCircuits");
|
|
449
449
|
const dhwBody = await input.get("/dhwCircuits");
|
|
450
450
|
const circuits = [];
|
|
451
|
-
for (const
|
|
451
|
+
for (const ref of circuitRefs(heating, "hc")) circuits.push(await readCircuit(input.get, ref.id, ref.name));
|
|
452
452
|
const dhwIds = referenceKeys(dhwBody, "dhw");
|
|
453
453
|
const first = dhwIds[0];
|
|
454
454
|
const dhw = first === void 0 ? null : await readDhw(input.get, first);
|
|
@@ -508,7 +508,7 @@ async function readRac(input) {
|
|
|
508
508
|
setpointResource: "/airConditioning/temperatureSetpoint"
|
|
509
509
|
}], null);
|
|
510
510
|
}
|
|
511
|
-
async function readCircuit(get, id) {
|
|
511
|
+
async function readCircuit(get, id, givenName) {
|
|
512
512
|
const base = `/heatingCircuits/${id}`;
|
|
513
513
|
const loaded = await loadEnvelopes(get, [
|
|
514
514
|
`${base}/operationMode`,
|
|
@@ -531,7 +531,7 @@ async function readCircuit(get, id) {
|
|
|
531
531
|
const humidity = numberOf(loaded.get(`${base}/actualHumidity`) ?? null);
|
|
532
532
|
return {
|
|
533
533
|
id,
|
|
534
|
-
name: id,
|
|
534
|
+
name: heatingCircuitName(id, givenName),
|
|
535
535
|
operationMode: stringOf(operation) ?? "off",
|
|
536
536
|
setpoint: cooling ? numberOf(coolingSet) ?? numberOf(current) ?? numberOf(manual) : numberOf(current) ?? numberOf(manual),
|
|
537
537
|
currentRoomTemp: numberOf(loaded.get(`${base}/roomtemperature`) ?? null),
|
|
@@ -633,18 +633,37 @@ function pointtMode(mode) {
|
|
|
633
633
|
default: return mode;
|
|
634
634
|
}
|
|
635
635
|
}
|
|
636
|
-
|
|
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) {
|
|
637
643
|
if (!isRecord$2(body) || !Array.isArray(body["references"])) return [];
|
|
638
644
|
const pattern = prefix === "hc" ? /^hc\d+$/ : /^dhw\d+$/;
|
|
639
645
|
const out = [];
|
|
640
646
|
for (const ref of body["references"]) {
|
|
641
647
|
if (!isRecord$2(ref) || typeof ref["id"] !== "string") continue;
|
|
642
648
|
const tail = ref["id"].split("/").filter((part) => part.length > 0).at(-1);
|
|
643
|
-
if (tail === void 0 || !pattern.test(tail) || out.
|
|
644
|
-
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
|
+
});
|
|
645
654
|
}
|
|
646
655
|
return out;
|
|
647
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
|
+
}
|
|
648
667
|
function indexStandard(body) {
|
|
649
668
|
const map = /* @__PURE__ */ new Map();
|
|
650
669
|
if (!isRecord$2(body) || !Array.isArray(body["references"])) return map;
|
|
@@ -792,19 +811,186 @@ function safeGet(get, onFailure) {
|
|
|
792
811
|
}
|
|
793
812
|
};
|
|
794
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
|
+
];
|
|
795
825
|
/**
|
|
796
|
-
* True when
|
|
797
|
-
*
|
|
798
|
-
*
|
|
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.
|
|
799
836
|
*/
|
|
800
|
-
function
|
|
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;
|
|
801
859
|
return [
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
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
|
+
];
|
|
808
994
|
}
|
|
809
995
|
async function readHc(get, id) {
|
|
810
996
|
const base = `/heatingCircuits/${id}`;
|
|
@@ -909,7 +1095,6 @@ function heatPumpStatusFrom(reading, now) {
|
|
|
909
1095
|
}
|
|
910
1096
|
function circuitOf(c) {
|
|
911
1097
|
const kind = kindOf(c);
|
|
912
|
-
const mode = textOf$1(c.operationMode);
|
|
913
1098
|
return {
|
|
914
1099
|
id: c.id,
|
|
915
1100
|
kind,
|
|
@@ -917,7 +1102,7 @@ function circuitOf(c) {
|
|
|
917
1102
|
icon: kind === "dhw" ? "droplets" : kind === "cooling" ? "snowflake" : "flame",
|
|
918
1103
|
currentTemp: c.currentTemp,
|
|
919
1104
|
targetTemp: c.targetTemp,
|
|
920
|
-
mode,
|
|
1105
|
+
mode: modeValue(c.operationMode),
|
|
921
1106
|
active: null,
|
|
922
1107
|
operationMode: optionOf(c.operationMode),
|
|
923
1108
|
heatCoolMode: optionOf(c.heatCoolMode),
|
|
@@ -937,15 +1122,24 @@ function kindOf(c) {
|
|
|
937
1122
|
function labelOf(c) {
|
|
938
1123
|
const n = c.id.replace(/^\D+/, "");
|
|
939
1124
|
if (c.kind === "dhw") return n === "1" || n === "" ? "Hot water" : `Hot water ${n}`;
|
|
940
|
-
return n === "" ? "
|
|
1125
|
+
return n === "" ? "Heating circuit" : `Heating circuit ${n}`;
|
|
941
1126
|
}
|
|
942
1127
|
function textOf$1(s) {
|
|
943
1128
|
return s !== null && typeof s.value === "string" && s.value.length > 0 ? s.value : null;
|
|
944
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
|
+
}
|
|
945
1139
|
function optionOf(s) {
|
|
946
1140
|
if (s === null) return null;
|
|
947
1141
|
return {
|
|
948
|
-
value:
|
|
1142
|
+
value: modeValue(s),
|
|
949
1143
|
allowed: [...s.allowed]
|
|
950
1144
|
};
|
|
951
1145
|
}
|
|
@@ -1112,6 +1306,7 @@ function createHomeComApiClient(config, gatewayId, deps = {}) {
|
|
|
1112
1306
|
let last = null;
|
|
1113
1307
|
let refreshTail = Promise.resolve();
|
|
1114
1308
|
const dropped = /* @__PURE__ */ new Set();
|
|
1309
|
+
let heatPumpPathsLogged = false;
|
|
1115
1310
|
const base = homeComApiBase(config.region);
|
|
1116
1311
|
function authHeaders() {
|
|
1117
1312
|
return {
|
|
@@ -1266,11 +1461,24 @@ function createHomeComApiClient(config, gatewayId, deps = {}) {
|
|
|
1266
1461
|
dhw: dhwId === void 0 ? [] : [dhwId]
|
|
1267
1462
|
}
|
|
1268
1463
|
});
|
|
1269
|
-
|
|
1464
|
+
logHeatPumpPaths(heatPump);
|
|
1465
|
+
return hasSystemDevice(heatPump) ? {
|
|
1270
1466
|
...snapshot,
|
|
1271
1467
|
heatPump
|
|
1272
1468
|
} : snapshot;
|
|
1273
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
|
+
}
|
|
1274
1482
|
function logHeatPumpFailure(path, error) {
|
|
1275
1483
|
const key = `heat-pump-read:${path}`;
|
|
1276
1484
|
if (dropped.has(key)) return;
|
|
@@ -1598,6 +1806,129 @@ function buildHomeComGatewayPersister(deps) {
|
|
|
1598
1806
|
return (integrationId, gatewayId) => patch(integrationId, { gatewayId });
|
|
1599
1807
|
}
|
|
1600
1808
|
//#endregion
|
|
1809
|
+
//#region src/homecom/homecom-gateway.ts
|
|
1810
|
+
/** Stable id of one HomeCom device. Adoption matches an existing row on this. */
|
|
1811
|
+
function homeComStableId(integrationId, nativeId) {
|
|
1812
|
+
return `homecom:${integrationId}:${nativeId}`;
|
|
1813
|
+
}
|
|
1814
|
+
/** The hot-water child of a heat pump. A boiler's own row keeps its longer name. */
|
|
1815
|
+
var HOMECOM_HOT_WATER_CHILD_NAME = "Hot water";
|
|
1816
|
+
var HomeComConnectionResolver = class {
|
|
1817
|
+
#surfaces = /* @__PURE__ */ new Map();
|
|
1818
|
+
set(integrationId, surface) {
|
|
1819
|
+
if (surface === null) {
|
|
1820
|
+
this.#surfaces.delete(integrationId);
|
|
1821
|
+
return;
|
|
1822
|
+
}
|
|
1823
|
+
this.#surfaces.set(integrationId, surface);
|
|
1824
|
+
}
|
|
1825
|
+
has(integrationId) {
|
|
1826
|
+
return this.#surfaces.has(integrationId);
|
|
1827
|
+
}
|
|
1828
|
+
list() {
|
|
1829
|
+
return Array.from(this.#surfaces.keys()).map((id) => ({ id }));
|
|
1830
|
+
}
|
|
1831
|
+
discoverables(integrationId) {
|
|
1832
|
+
return this.#surfaces.get(integrationId)?.discoverables ?? [];
|
|
1833
|
+
}
|
|
1834
|
+
getClient(integrationId) {
|
|
1835
|
+
return this.#surfaces.get(integrationId)?.client ?? null;
|
|
1836
|
+
}
|
|
1837
|
+
getSnapshot(integrationId) {
|
|
1838
|
+
return this.#surfaces.get(integrationId)?.snapshot ?? null;
|
|
1839
|
+
}
|
|
1840
|
+
updateSnapshot(integrationId, snapshot) {
|
|
1841
|
+
const existing = this.#surfaces.get(integrationId);
|
|
1842
|
+
if (!existing) return;
|
|
1843
|
+
this.#surfaces.set(integrationId, {
|
|
1844
|
+
...existing,
|
|
1845
|
+
snapshot,
|
|
1846
|
+
discoverables: buildDiscoverables(snapshot)
|
|
1847
|
+
});
|
|
1848
|
+
}
|
|
1849
|
+
clear() {
|
|
1850
|
+
this.#surfaces.clear();
|
|
1851
|
+
}
|
|
1852
|
+
};
|
|
1853
|
+
var BOILER_DHW_NAME = "Domestic hot water";
|
|
1854
|
+
function circuitDiscoverable(circuit) {
|
|
1855
|
+
return {
|
|
1856
|
+
nativeId: `hc:${circuit.id}`,
|
|
1857
|
+
name: circuit.name,
|
|
1858
|
+
kind: "heating-circuit",
|
|
1859
|
+
circuitId: circuit.id
|
|
1860
|
+
};
|
|
1861
|
+
}
|
|
1862
|
+
function dhwDiscoverable(name) {
|
|
1863
|
+
return {
|
|
1864
|
+
nativeId: "dhw:0",
|
|
1865
|
+
name,
|
|
1866
|
+
kind: "dhw"
|
|
1867
|
+
};
|
|
1868
|
+
}
|
|
1869
|
+
/**
|
|
1870
|
+
* A gateway with a heat pump is one pick. A boiler still offers each circuit
|
|
1871
|
+
* and the hot-water row on its own.
|
|
1872
|
+
*/
|
|
1873
|
+
function buildDiscoverables(snapshot) {
|
|
1874
|
+
const circuits = snapshot.circuits.map(circuitDiscoverable);
|
|
1875
|
+
if (snapshot.heatPump !== void 0) return [{
|
|
1876
|
+
nativeId: "hp:0",
|
|
1877
|
+
name: "Heat pump",
|
|
1878
|
+
kind: "heat-pump",
|
|
1879
|
+
children: [...circuits, ...snapshot.dhw === null ? [] : [dhwDiscoverable(HOMECOM_HOT_WATER_CHILD_NAME)]]
|
|
1880
|
+
}];
|
|
1881
|
+
return [...circuits, ...snapshot.dhw === null ? [] : [dhwDiscoverable(BOILER_DHW_NAME)]];
|
|
1882
|
+
}
|
|
1883
|
+
var homeComConnections = new HomeComConnectionResolver();
|
|
1884
|
+
//#endregion
|
|
1885
|
+
//#region src/homecom/homecom-adoption-plan.ts
|
|
1886
|
+
function createOf(device, parentNativeId) {
|
|
1887
|
+
return {
|
|
1888
|
+
nativeId: device.nativeId,
|
|
1889
|
+
name: device.name,
|
|
1890
|
+
kind: device.kind,
|
|
1891
|
+
circuitId: device.circuitId,
|
|
1892
|
+
parentNativeId
|
|
1893
|
+
};
|
|
1894
|
+
}
|
|
1895
|
+
/**
|
|
1896
|
+
* The devices one pick creates. A heat pump also creates its children, except
|
|
1897
|
+
* a circuit or hot-water row that already exists: a top-level one is reported
|
|
1898
|
+
* so the caller can log why it stayed there, and any existing row is not
|
|
1899
|
+
* created again.
|
|
1900
|
+
*/
|
|
1901
|
+
function planHomeComAdoption(device, integrationId, adopted) {
|
|
1902
|
+
const root = createOf(device, null);
|
|
1903
|
+
if (device.kind !== "heat-pump") return {
|
|
1904
|
+
create: [root],
|
|
1905
|
+
stayedTopLevel: [],
|
|
1906
|
+
alreadyPresent: []
|
|
1907
|
+
};
|
|
1908
|
+
const byStableId = new Map(adopted.map((row) => [row.stableId, row]));
|
|
1909
|
+
const create = [root];
|
|
1910
|
+
const stayedTopLevel = [];
|
|
1911
|
+
const alreadyPresent = [];
|
|
1912
|
+
for (const child of device.children ?? []) {
|
|
1913
|
+
const existing = byStableId.get(homeComStableId(integrationId, child.nativeId));
|
|
1914
|
+
if (existing === void 0) {
|
|
1915
|
+
create.push(createOf(child, device.nativeId));
|
|
1916
|
+
continue;
|
|
1917
|
+
}
|
|
1918
|
+
const skip = {
|
|
1919
|
+
nativeId: child.nativeId,
|
|
1920
|
+
deviceId: existing.id
|
|
1921
|
+
};
|
|
1922
|
+
if (existing.parentDeviceId === null) stayedTopLevel.push(skip);
|
|
1923
|
+
else alreadyPresent.push(skip);
|
|
1924
|
+
}
|
|
1925
|
+
return {
|
|
1926
|
+
create,
|
|
1927
|
+
stayedTopLevel,
|
|
1928
|
+
alreadyPresent
|
|
1929
|
+
};
|
|
1930
|
+
}
|
|
1931
|
+
//#endregion
|
|
1601
1932
|
//#region src/homecom/homecom-discovery.ts
|
|
1602
1933
|
function buildHomeComCandidates(input) {
|
|
1603
1934
|
const out = [];
|
|
@@ -1617,6 +1948,9 @@ function buildHomeComCandidates(input) {
|
|
|
1617
1948
|
}
|
|
1618
1949
|
return out;
|
|
1619
1950
|
}
|
|
1951
|
+
function homeComDeviceType(kind) {
|
|
1952
|
+
return shapeOf(kind).type;
|
|
1953
|
+
}
|
|
1620
1954
|
function shapeOf(kind) {
|
|
1621
1955
|
switch (kind) {
|
|
1622
1956
|
case "heating-circuit": return {
|
|
@@ -1640,6 +1974,62 @@ var DEVICES_FILTER = {
|
|
|
1640
1974
|
label: "Devices",
|
|
1641
1975
|
isDefault: true
|
|
1642
1976
|
};
|
|
1977
|
+
var STAYED_TOP_LEVEL = "HomeCom device stayed top-level: it was already adopted, so it was not created again under the heat pump";
|
|
1978
|
+
var ALREADY_PRESENT = "HomeCom device was not created again: a device with this stable id already exists";
|
|
1979
|
+
async function adoptOne(input) {
|
|
1980
|
+
const { dev, integrationId, name, known, adoptDevice, logger, logOnce } = input;
|
|
1981
|
+
const plan = planHomeComAdoption(dev, integrationId, known);
|
|
1982
|
+
for (const skip of plan.stayedTopLevel) logOnce(skip.deviceId, STAYED_TOP_LEVEL);
|
|
1983
|
+
for (const skip of plan.alreadyPresent) logOnce(skip.deviceId, ALREADY_PRESENT);
|
|
1984
|
+
const root = plan.create[0];
|
|
1985
|
+
if (root === void 0) return { ok: false };
|
|
1986
|
+
let parentDeviceId;
|
|
1987
|
+
try {
|
|
1988
|
+
parentDeviceId = (await adoptDevice({
|
|
1989
|
+
nativeId: root.nativeId,
|
|
1990
|
+
integrationId,
|
|
1991
|
+
type: homeComDeviceType(root.kind),
|
|
1992
|
+
name,
|
|
1993
|
+
kind: root.kind,
|
|
1994
|
+
...root.circuitId === void 0 ? {} : { circuitId: root.circuitId },
|
|
1995
|
+
parentDeviceId: null
|
|
1996
|
+
})).deviceId;
|
|
1997
|
+
} catch (err) {
|
|
1998
|
+
logger.warn("homecom adopt failed", { meta: {
|
|
1999
|
+
nativeId: root.nativeId,
|
|
2000
|
+
integrationId,
|
|
2001
|
+
error: errMsg(err)
|
|
2002
|
+
} });
|
|
2003
|
+
return { ok: false };
|
|
2004
|
+
}
|
|
2005
|
+
const accessoryDeviceIds = [];
|
|
2006
|
+
for (const child of plan.create.slice(1)) try {
|
|
2007
|
+
const created = await adoptDevice({
|
|
2008
|
+
nativeId: child.nativeId,
|
|
2009
|
+
integrationId,
|
|
2010
|
+
type: homeComDeviceType(child.kind),
|
|
2011
|
+
name: child.name,
|
|
2012
|
+
kind: child.kind,
|
|
2013
|
+
...child.circuitId === void 0 ? {} : { circuitId: child.circuitId },
|
|
2014
|
+
parentDeviceId
|
|
2015
|
+
});
|
|
2016
|
+
accessoryDeviceIds.push(created.deviceId);
|
|
2017
|
+
} catch (err) {
|
|
2018
|
+
logger.warn("homecom adopt child failed", {
|
|
2019
|
+
tags: { deviceId: parentDeviceId },
|
|
2020
|
+
meta: {
|
|
2021
|
+
nativeId: child.nativeId,
|
|
2022
|
+
integrationId,
|
|
2023
|
+
error: errMsg(err)
|
|
2024
|
+
}
|
|
2025
|
+
});
|
|
2026
|
+
}
|
|
2027
|
+
return {
|
|
2028
|
+
ok: true,
|
|
2029
|
+
parentDeviceId,
|
|
2030
|
+
accessoryDeviceIds
|
|
2031
|
+
};
|
|
2032
|
+
}
|
|
1643
2033
|
async function adoptedMapForIntegration(integrationId, listAdopted) {
|
|
1644
2034
|
const all = await listAdopted();
|
|
1645
2035
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -1651,6 +2041,12 @@ async function adoptedMapForIntegration(integrationId, listAdopted) {
|
|
|
1651
2041
|
}
|
|
1652
2042
|
function buildHomeComAdoptionProvider(deps) {
|
|
1653
2043
|
const { registry, hasIntegration, listIntegrations, listAdopted, adoptDevice, removeDevice, findDeviceConfig, logger } = deps;
|
|
2044
|
+
const logged = /* @__PURE__ */ new Set();
|
|
2045
|
+
const logOnce = (deviceId, message) => {
|
|
2046
|
+
if (logged.has(deviceId)) return;
|
|
2047
|
+
logged.add(deviceId);
|
|
2048
|
+
logger.info(message, { tags: { deviceId } });
|
|
2049
|
+
};
|
|
1654
2050
|
async function allCandidatesForIntegration(integrationId) {
|
|
1655
2051
|
return buildHomeComCandidates({
|
|
1656
2052
|
devices: registry.discoverables(integrationId),
|
|
@@ -1705,48 +2101,36 @@ function buildHomeComAdoptionProvider(deps) {
|
|
|
1705
2101
|
adopt: async ({ integrationId, childNativeIds, perCandidate }) => {
|
|
1706
2102
|
if (!registry.has(integrationId)) throw new Error(`homecom adopt: integration ${integrationId} not connected`);
|
|
1707
2103
|
const devices = registry.discoverables(integrationId);
|
|
2104
|
+
const known = await listAdopted();
|
|
1708
2105
|
const adopted = [];
|
|
1709
2106
|
let failures = 0;
|
|
1710
|
-
for (const nativeId of childNativeIds)
|
|
2107
|
+
for (const nativeId of childNativeIds) {
|
|
1711
2108
|
const dev = devices.find((d) => d.nativeId === nativeId);
|
|
1712
2109
|
if (!dev) {
|
|
1713
2110
|
failures++;
|
|
1714
2111
|
continue;
|
|
1715
2112
|
}
|
|
1716
|
-
const
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
2113
|
+
const outcome = await adoptOne({
|
|
2114
|
+
dev,
|
|
2115
|
+
integrationId,
|
|
2116
|
+
name: perCandidate?.[nativeId]?.name ?? dev.name,
|
|
2117
|
+
known,
|
|
2118
|
+
adoptDevice,
|
|
2119
|
+
logger,
|
|
2120
|
+
logOnce
|
|
2121
|
+
});
|
|
2122
|
+
if (!outcome.ok) {
|
|
1721
2123
|
failures++;
|
|
1722
2124
|
continue;
|
|
1723
2125
|
}
|
|
1724
|
-
const name = perCandidate?.[nativeId]?.name ?? dev.name;
|
|
1725
|
-
const { deviceId } = await adoptDevice({
|
|
1726
|
-
nativeId,
|
|
1727
|
-
integrationId,
|
|
1728
|
-
type: candidate.type,
|
|
1729
|
-
name,
|
|
1730
|
-
kind: dev.kind,
|
|
1731
|
-
circuitId: dev.circuitId
|
|
1732
|
-
});
|
|
1733
2126
|
adopted.push({
|
|
1734
2127
|
childNativeId: nativeId,
|
|
1735
|
-
parentDeviceId:
|
|
2128
|
+
parentDeviceId: outcome.parentDeviceId,
|
|
2129
|
+
accessoryDeviceIds: outcome.accessoryDeviceIds
|
|
1736
2130
|
});
|
|
1737
|
-
} catch (err) {
|
|
1738
|
-
logger.warn("homecom adopt failed", { meta: {
|
|
1739
|
-
nativeId,
|
|
1740
|
-
integrationId,
|
|
1741
|
-
error: errMsg(err)
|
|
1742
|
-
} });
|
|
1743
|
-
failures++;
|
|
1744
2131
|
}
|
|
1745
2132
|
if (adopted.length === 0 && failures > 0) throw new Error(`homecom adopt: all ${failures} adopt(s) failed`);
|
|
1746
|
-
return { adopted
|
|
1747
|
-
...a,
|
|
1748
|
-
accessoryDeviceIds: []
|
|
1749
|
-
})) };
|
|
2133
|
+
return { adopted };
|
|
1750
2134
|
},
|
|
1751
2135
|
release: async ({ camDeviceId }) => {
|
|
1752
2136
|
await removeDevice(camDeviceId);
|
|
@@ -1766,66 +2150,6 @@ function buildHomeComAdoptionProvider(deps) {
|
|
|
1766
2150
|
};
|
|
1767
2151
|
}
|
|
1768
2152
|
//#endregion
|
|
1769
|
-
//#region src/homecom/homecom-gateway.ts
|
|
1770
|
-
var HomeComConnectionResolver = class {
|
|
1771
|
-
#surfaces = /* @__PURE__ */ new Map();
|
|
1772
|
-
set(integrationId, surface) {
|
|
1773
|
-
if (surface === null) {
|
|
1774
|
-
this.#surfaces.delete(integrationId);
|
|
1775
|
-
return;
|
|
1776
|
-
}
|
|
1777
|
-
this.#surfaces.set(integrationId, surface);
|
|
1778
|
-
}
|
|
1779
|
-
has(integrationId) {
|
|
1780
|
-
return this.#surfaces.has(integrationId);
|
|
1781
|
-
}
|
|
1782
|
-
list() {
|
|
1783
|
-
return Array.from(this.#surfaces.keys()).map((id) => ({ id }));
|
|
1784
|
-
}
|
|
1785
|
-
discoverables(integrationId) {
|
|
1786
|
-
return this.#surfaces.get(integrationId)?.discoverables ?? [];
|
|
1787
|
-
}
|
|
1788
|
-
getClient(integrationId) {
|
|
1789
|
-
return this.#surfaces.get(integrationId)?.client ?? null;
|
|
1790
|
-
}
|
|
1791
|
-
getSnapshot(integrationId) {
|
|
1792
|
-
return this.#surfaces.get(integrationId)?.snapshot ?? null;
|
|
1793
|
-
}
|
|
1794
|
-
updateSnapshot(integrationId, snapshot) {
|
|
1795
|
-
const existing = this.#surfaces.get(integrationId);
|
|
1796
|
-
if (!existing) return;
|
|
1797
|
-
this.#surfaces.set(integrationId, {
|
|
1798
|
-
...existing,
|
|
1799
|
-
snapshot,
|
|
1800
|
-
discoverables: buildDiscoverables(snapshot)
|
|
1801
|
-
});
|
|
1802
|
-
}
|
|
1803
|
-
clear() {
|
|
1804
|
-
this.#surfaces.clear();
|
|
1805
|
-
}
|
|
1806
|
-
};
|
|
1807
|
-
function buildDiscoverables(snapshot) {
|
|
1808
|
-
const out = [];
|
|
1809
|
-
for (const circuit of snapshot.circuits) out.push({
|
|
1810
|
-
nativeId: `hc:${circuit.id}`,
|
|
1811
|
-
name: circuit.name,
|
|
1812
|
-
kind: "heating-circuit",
|
|
1813
|
-
circuitId: circuit.id
|
|
1814
|
-
});
|
|
1815
|
-
if (snapshot.dhw) out.push({
|
|
1816
|
-
nativeId: "dhw:0",
|
|
1817
|
-
name: "Domestic hot water",
|
|
1818
|
-
kind: "dhw"
|
|
1819
|
-
});
|
|
1820
|
-
if (snapshot.heatPump !== void 0) out.push({
|
|
1821
|
-
nativeId: "hp:0",
|
|
1822
|
-
name: "Heat pump",
|
|
1823
|
-
kind: "heat-pump"
|
|
1824
|
-
});
|
|
1825
|
-
return out;
|
|
1826
|
-
}
|
|
1827
|
-
var homeComConnections = new HomeComConnectionResolver();
|
|
1828
|
-
//#endregion
|
|
1829
2153
|
//#region src/homecom/homecom-integration-manager.ts
|
|
1830
2154
|
var HomeComIntegrationManager = class {
|
|
1831
2155
|
#id;
|
|
@@ -2724,7 +3048,7 @@ var BoschHomeComProviderAddon = class extends BaseDeviceProvider {
|
|
|
2724
3048
|
if (typeof integrationId !== "string" || integrationId.length === 0) throw new Error("homecom generateStableId: brokerId required");
|
|
2725
3049
|
const nativeId = config?.["nativeId"];
|
|
2726
3050
|
if (typeof nativeId !== "string" || nativeId.length === 0) throw new Error("homecom generateStableId: nativeId required");
|
|
2727
|
-
return
|
|
3051
|
+
return homeComStableId(integrationId, nativeId);
|
|
2728
3052
|
}
|
|
2729
3053
|
async reconcileIntegrations() {
|
|
2730
3054
|
const reg = this.requireClients();
|
|
@@ -2786,12 +3110,14 @@ var BoschHomeComProviderAddon = class extends BaseDeviceProvider {
|
|
|
2786
3110
|
const config = await devices.loadConfig(d.id).catch(() => ({}));
|
|
2787
3111
|
out.push({
|
|
2788
3112
|
id: d.id,
|
|
3113
|
+
stableId: d.stableId,
|
|
3114
|
+
parentDeviceId: d.parentDeviceId,
|
|
2789
3115
|
config
|
|
2790
3116
|
});
|
|
2791
3117
|
}
|
|
2792
3118
|
return out;
|
|
2793
3119
|
},
|
|
2794
|
-
adoptDevice: async ({ nativeId, integrationId, type, name, kind, circuitId }) => {
|
|
3120
|
+
adoptDevice: async ({ nativeId, integrationId, type, name, kind, circuitId, parentDeviceId }) => {
|
|
2795
3121
|
const devices = this.ctx.kernel.devices;
|
|
2796
3122
|
if (!devices) throw new Error("homecom adopt: devices unavailable");
|
|
2797
3123
|
const config = {
|
|
@@ -2805,7 +3131,7 @@ var BoschHomeComProviderAddon = class extends BaseDeviceProvider {
|
|
|
2805
3131
|
const stableId = this.generateStableId(type, config);
|
|
2806
3132
|
const DeviceClass = this.deviceClasses[type];
|
|
2807
3133
|
if (!DeviceClass) throw new Error(`homecom adopt: unsupported type ${type}`);
|
|
2808
|
-
return { deviceId: (await devices.create(stableId, DeviceClass, config,
|
|
3134
|
+
return { deviceId: (await devices.create(stableId, DeviceClass, config, parentDeviceId, {
|
|
2809
3135
|
type,
|
|
2810
3136
|
name,
|
|
2811
3137
|
integrationId
|