@camstack/addon-provider-homematic 1.2.164 → 1.2.165
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/dist/addon.js +943 -138
- package/dist/addon.mjs +941 -136
- package/package.json +2 -2
package/dist/addon.js
CHANGED
|
@@ -57010,6 +57010,12 @@ var TerminalInstanceInfoSchema = object({
|
|
|
57010
57010
|
instanceId: string(),
|
|
57011
57011
|
cameraStableId: string(),
|
|
57012
57012
|
nodeId: string(),
|
|
57013
|
+
/**
|
|
57014
|
+
* False while the node the instance is bound to is not in the cluster
|
|
57015
|
+
* topology (forgotten, renamed, re-paired). The instance is kept; the
|
|
57016
|
+
* operator re-points it with `updateInstance({ targetNodeId })`.
|
|
57017
|
+
*/
|
|
57018
|
+
nodeInCluster: boolean(),
|
|
57013
57019
|
profileId: string(),
|
|
57014
57020
|
profileLabel: string(),
|
|
57015
57021
|
name: string(),
|
|
@@ -57059,6 +57065,8 @@ method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }),
|
|
|
57059
57065
|
auth: "admin"
|
|
57060
57066
|
}), method(object({
|
|
57061
57067
|
instanceId: string().min(1),
|
|
57068
|
+
/** Re-point the instance to another node; the profile must exist there. */
|
|
57069
|
+
targetNodeId: string().min(1).max(256).optional(),
|
|
57062
57070
|
name: string().trim().min(1).max(160).optional(),
|
|
57063
57071
|
executable: string().max(1024).optional(),
|
|
57064
57072
|
args: array(string().max(2048)).max(64).optional(),
|
|
@@ -113228,6 +113236,19 @@ var CircuitBreakerOpenError = class extends BaseHomematicError {};
|
|
|
113228
113236
|
var AuthFailureError = class extends BaseHomematicError {};
|
|
113229
113237
|
var InternalBackendError = class extends BaseHomematicError {};
|
|
113230
113238
|
var CommandSupersededError = class extends BaseHomematicError {};
|
|
113239
|
+
var CcuManagementError = class extends BaseHomematicError {
|
|
113240
|
+
/** The XML-RPC / JSON-RPC method that failed. */
|
|
113241
|
+
method;
|
|
113242
|
+
/** The underlying typed error (or whatever was thrown). */
|
|
113243
|
+
original;
|
|
113244
|
+
constructor(method, original) {
|
|
113245
|
+
const detail = original instanceof Error ? original.message : String(original);
|
|
113246
|
+
super(`${method} failed: ${detail}`);
|
|
113247
|
+
this.method = method;
|
|
113248
|
+
this.original = original;
|
|
113249
|
+
}
|
|
113250
|
+
};
|
|
113251
|
+
var GroupWriteVerificationError = class extends BaseHomematicError {};
|
|
113231
113252
|
var DescriptionNotFoundError = class extends BaseHomematicError {};
|
|
113232
113253
|
var InvalidStateTransitionError = class extends BaseHomematicError {};
|
|
113233
113254
|
var TimeoutError = class extends BaseHomematicError {};
|
|
@@ -113981,6 +114002,54 @@ var InterfaceClient = class {
|
|
|
113981
114002
|
const result = await this.callGuarded("getInstallMode", []);
|
|
113982
114003
|
return typeof result === "number" ? result : 0;
|
|
113983
114004
|
}
|
|
114005
|
+
/**
|
|
114006
|
+
* `listBidcosInterfaces()` → the radio gateways of a BidCos interface.
|
|
114007
|
+
*
|
|
114008
|
+
* Source: HomeMatic XML-RPC interface spec (rfd), as consumed by the WebUI in
|
|
114009
|
+
* eq-3/occu `WebUI/www/api/methods/interface/listbidcosinterfaces.tcl`
|
|
114010
|
+
* (fields ADDRESS, DESCRIPTION, DUTY_CYCLE, CONNECTED, DEFAULT,
|
|
114011
|
+
* FIRMWARE_VERSION, TYPE). A field the CCU did not send is `null`, never 0.
|
|
114012
|
+
*/
|
|
114013
|
+
async listBidcosInterfaces() {
|
|
114014
|
+
const result = await this.managementCall("listBidcosInterfaces", []);
|
|
114015
|
+
if (!Array.isArray(result)) return [];
|
|
114016
|
+
const gateways = [];
|
|
114017
|
+
for (const entry of result) if (isStruct(entry)) gateways.push(toRadioGateway(entry));
|
|
114018
|
+
return gateways;
|
|
114019
|
+
}
|
|
114020
|
+
/**
|
|
114021
|
+
* `setInstallMode(on[, seconds[, mode]])` — open or close the pairing window.
|
|
114022
|
+
*
|
|
114023
|
+
* Source: HomeMatic XML-RPC interface spec; the WebUI forwards the same call
|
|
114024
|
+
* in eq-3/occu `WebUI/www/api/methods/interface/setinstallmode.tcl` and
|
|
114025
|
+
* `setinstallmodehmip.tcl` (`setInstallMode(bool on, int time)`). `mode`
|
|
114026
|
+
* (1 normal, 2 with the HomeMatic Secure key) is BidCos-only and is sent only
|
|
114027
|
+
* when given.
|
|
114028
|
+
*/
|
|
114029
|
+
async setInstallMode(request) {
|
|
114030
|
+
const params = [request.on];
|
|
114031
|
+
if (request.seconds !== void 0) params.push(request.seconds);
|
|
114032
|
+
if (request.mode !== void 0) params.push(request.mode);
|
|
114033
|
+
await this.managementCall("setInstallMode", params);
|
|
114034
|
+
}
|
|
114035
|
+
/**
|
|
114036
|
+
* `getInstallMode()` → seconds left of the pairing window, or `null` when the
|
|
114037
|
+
* CCU answered something that is not an integer (never a fake 0).
|
|
114038
|
+
*/
|
|
114039
|
+
async getInstallModeRemaining() {
|
|
114040
|
+
const result = await this.managementCall("getInstallMode", []);
|
|
114041
|
+
return typeof result === "number" && Number.isInteger(result) ? result : null;
|
|
114042
|
+
}
|
|
114043
|
+
/**
|
|
114044
|
+
* `deleteDevice(address, flags)` — remove a device from the interface.
|
|
114045
|
+
*
|
|
114046
|
+
* Source: eq-3/occu `WebUI/www/api/methods/interface/deletedevice.tcl`:
|
|
114047
|
+
* flags 0x01 reset to factory, 0x02 force (also when unreachable), 0x04 defer
|
|
114048
|
+
* until the device is next reachable.
|
|
114049
|
+
*/
|
|
114050
|
+
async deleteDevice(address, flags) {
|
|
114051
|
+
await this.managementCall("deleteDevice", [address, flags]);
|
|
114052
|
+
}
|
|
113984
114053
|
/** `getVersion()` (bypass) → backend version string. */
|
|
113985
114054
|
async getVersion() {
|
|
113986
114055
|
return scalarToString(await this.callGuarded("getVersion", [], { bypassBreaker: true }));
|
|
@@ -113991,6 +114060,18 @@ var InterfaceClient = class {
|
|
|
113991
114060
|
return Array.isArray(result) ? result.map(scalarToString) : [];
|
|
113992
114061
|
}
|
|
113993
114062
|
/**
|
|
114063
|
+
* A management call: guarded like any other, but a failure is rethrown as a
|
|
114064
|
+
* {@link CcuManagementError} naming the method and keeping the CCU fault text.
|
|
114065
|
+
*/
|
|
114066
|
+
async managementCall(method, params) {
|
|
114067
|
+
try {
|
|
114068
|
+
return await this.callGuarded(method, params);
|
|
114069
|
+
} catch (err) {
|
|
114070
|
+
if (err instanceof BaseHomematicError) throw new CcuManagementError(method, err);
|
|
114071
|
+
throw err;
|
|
114072
|
+
}
|
|
114073
|
+
}
|
|
114074
|
+
/**
|
|
113994
114075
|
* Invoke `method`, gating non-bypass calls behind the circuit breaker.
|
|
113995
114076
|
* Bypass methods skip the breaker entirely (no success/failure recorded).
|
|
113996
114077
|
*/
|
|
@@ -114045,17 +114126,48 @@ function asDeviceDescriptionArray(value) {
|
|
|
114045
114126
|
function asParamsetDescription(value) {
|
|
114046
114127
|
return asStruct(value);
|
|
114047
114128
|
}
|
|
114129
|
+
function stringOrNull(value) {
|
|
114130
|
+
return typeof value === "string" && value !== "" ? value : null;
|
|
114131
|
+
}
|
|
114132
|
+
function numberOrNull(value) {
|
|
114133
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
114134
|
+
}
|
|
114135
|
+
function flagOrNull(value) {
|
|
114136
|
+
if (typeof value === "boolean") return value;
|
|
114137
|
+
if (value === 1) return true;
|
|
114138
|
+
if (value === 0) return false;
|
|
114139
|
+
return null;
|
|
114140
|
+
}
|
|
114141
|
+
function toRadioGateway(entry) {
|
|
114142
|
+
const connected = flagOrNull(entry["CONNECTED"]);
|
|
114143
|
+
return {
|
|
114144
|
+
address: stringOrNull(entry["ADDRESS"]) ?? "",
|
|
114145
|
+
description: stringOrNull(entry["DESCRIPTION"]),
|
|
114146
|
+
dutyCycle: numberOrNull(entry["DUTY_CYCLE"]),
|
|
114147
|
+
connected,
|
|
114148
|
+
isDefault: flagOrNull(entry["DEFAULT"]) === true,
|
|
114149
|
+
firmware: stringOrNull(entry["FIRMWARE_VERSION"]),
|
|
114150
|
+
type: stringOrNull(entry["TYPE"])
|
|
114151
|
+
};
|
|
114152
|
+
}
|
|
114048
114153
|
var SESSION_ID_PARAM = "_session_id_";
|
|
114049
114154
|
var JsonRpcMethod = {
|
|
114050
114155
|
CCU_GET_AUTH_ENABLED: "CCU.getAuthEnabled",
|
|
114051
114156
|
CCU_GET_HTTPS_REDIRECT_ENABLED: "CCU.getHttpsRedirectEnabled",
|
|
114157
|
+
CCU_GET_VERSION: "CCU.getVersion",
|
|
114158
|
+
CCU_GET_SERIAL: "CCU.getSerial",
|
|
114159
|
+
CCU_GET_HEATING_GROUP_LIST: "CCU.getHeatingGroupList",
|
|
114052
114160
|
DEVICE_LIST_ALL_DETAIL: "Device.listAllDetail",
|
|
114161
|
+
DEVICE_GET_REGA_ID_BY_ADDRESS: "Device.getReGaIDByAddress",
|
|
114053
114162
|
DEVICE_SET_NAME: "Device.setName",
|
|
114054
114163
|
CHANNEL_SET_NAME: "Channel.setName",
|
|
114055
114164
|
INTERFACE_LIST_INTERFACES: "Interface.listInterfaces",
|
|
114056
114165
|
INTERFACE_LIST_DEVICES: "Interface.listDevices",
|
|
114166
|
+
INTERFACE_IS_PRESENT: "Interface.isPresent",
|
|
114167
|
+
INTERFACE_GET_DUTY_CYCLE: "Interface.getDutyCycle",
|
|
114057
114168
|
INTERFACE_GET_INSTALL_MODE: "Interface.getInstallMode",
|
|
114058
114169
|
INTERFACE_SET_INSTALL_MODE_HMIP: "Interface.setInstallModeHMIP",
|
|
114170
|
+
INTERFACE_SET_METADATA: "Interface.setMetadata",
|
|
114059
114171
|
PROGRAM_GET_ALL: "Program.getAll",
|
|
114060
114172
|
PROGRAM_EXECUTE: "Program.execute",
|
|
114061
114173
|
SYSVAR_GET_ALL: "SysVar.getAll",
|
|
@@ -114264,6 +114376,691 @@ var SessionManager = class {
|
|
|
114264
114376
|
return Math.min(exp, MAX_BACKOFF_MS);
|
|
114265
114377
|
}
|
|
114266
114378
|
};
|
|
114379
|
+
var FORM_CONTENT_TYPE = "application/x-www-form-urlencoded; charset=UTF-8";
|
|
114380
|
+
var ESCAPE_SAFE = /[A-Za-z0-9@*_+\-./]/;
|
|
114381
|
+
function jsEscape(text) {
|
|
114382
|
+
let out = "";
|
|
114383
|
+
for (const ch of text) {
|
|
114384
|
+
const unit = ch.charCodeAt(0);
|
|
114385
|
+
if (ESCAPE_SAFE.test(ch)) out += ch;
|
|
114386
|
+
else if (ch.length === 1 && unit < 256) out += `%${unit.toString(16).toUpperCase().padStart(2, "0")}`;
|
|
114387
|
+
else for (let i = 0; i < ch.length; i += 1) out += `%u${ch.charCodeAt(i).toString(16).toUpperCase().padStart(4, "0")}`;
|
|
114388
|
+
}
|
|
114389
|
+
return out;
|
|
114390
|
+
}
|
|
114391
|
+
var WebUiHttpClient = class {
|
|
114392
|
+
base;
|
|
114393
|
+
timeoutMs;
|
|
114394
|
+
fetchFn;
|
|
114395
|
+
dispatcher;
|
|
114396
|
+
constructor(options) {
|
|
114397
|
+
this.base = options.url.replace(/\/+$/, "");
|
|
114398
|
+
this.timeoutMs = options.timeoutMs ?? TIMEOUTS.rpc;
|
|
114399
|
+
this.dispatcher = options.rejectUnauthorized === false ? new import_undici.Agent({ connect: { rejectUnauthorized: false } }) : void 0;
|
|
114400
|
+
this.fetchFn = options.fetch ?? this.defaultFetch.bind(this);
|
|
114401
|
+
}
|
|
114402
|
+
get(path$2, sessionId) {
|
|
114403
|
+
return this.request("GET", path$2, sessionId, void 0);
|
|
114404
|
+
}
|
|
114405
|
+
post(path$3, sessionId, body) {
|
|
114406
|
+
return this.request("POST", path$3, sessionId, body);
|
|
114407
|
+
}
|
|
114408
|
+
async close() {
|
|
114409
|
+
await this.dispatcher?.close();
|
|
114410
|
+
}
|
|
114411
|
+
async request(method, path$4, sessionId, body) {
|
|
114412
|
+
const url = `${this.base}${path$4}?sid=${encodeURIComponent(sessionId)}`;
|
|
114413
|
+
const controller = new AbortController();
|
|
114414
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
114415
|
+
try {
|
|
114416
|
+
const response = await this.fetchFn(url, {
|
|
114417
|
+
method,
|
|
114418
|
+
headers: body === void 0 ? {} : { "Content-Type": FORM_CONTENT_TYPE },
|
|
114419
|
+
body,
|
|
114420
|
+
signal: controller.signal
|
|
114421
|
+
});
|
|
114422
|
+
const text = await response.text();
|
|
114423
|
+
if (response.status < 200 || response.status >= 300) throw new ClientError(`HTTP ${response.status}`);
|
|
114424
|
+
return text;
|
|
114425
|
+
} catch (err) {
|
|
114426
|
+
const mapped = err instanceof ClientError ? err : mapTransportError(err);
|
|
114427
|
+
throw new CcuManagementError(`${method} ${path$4}`, /* @__PURE__ */ new Error(`${mapped.name}: ${mapped.message.split(sessionId).join("<sid>")}`));
|
|
114428
|
+
} finally {
|
|
114429
|
+
clearTimeout(timer);
|
|
114430
|
+
}
|
|
114431
|
+
}
|
|
114432
|
+
async defaultFetch(url, init) {
|
|
114433
|
+
const req = {
|
|
114434
|
+
method: init.method,
|
|
114435
|
+
headers: init.headers,
|
|
114436
|
+
signal: init.signal
|
|
114437
|
+
};
|
|
114438
|
+
if (init.body !== void 0) req.body = init.body;
|
|
114439
|
+
if (this.dispatcher) req.dispatcher = this.dispatcher;
|
|
114440
|
+
return (0, import_undici.fetch)(url, req);
|
|
114441
|
+
}
|
|
114442
|
+
};
|
|
114443
|
+
function escapeRegaString(s) {
|
|
114444
|
+
return s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
114445
|
+
}
|
|
114446
|
+
var CONTROL_CHARS = /[\u0000-\u001F]/g;
|
|
114447
|
+
function sanitizeJsonControlChars(s) {
|
|
114448
|
+
return s.replace(CONTROL_CHARS, "");
|
|
114449
|
+
}
|
|
114450
|
+
function decodeRegaName(encoded) {
|
|
114451
|
+
const bytes = [];
|
|
114452
|
+
for (let i = 0; i < encoded.length; i += 1) {
|
|
114453
|
+
const ch = encoded[i];
|
|
114454
|
+
if (ch === "%" && i + 2 < encoded.length) {
|
|
114455
|
+
const hex = encoded.slice(i + 1, i + 3);
|
|
114456
|
+
const byte = Number.parseInt(hex, 16);
|
|
114457
|
+
if (!Number.isNaN(byte) && /^[0-9a-fA-F]{2}$/.test(hex)) {
|
|
114458
|
+
bytes.push(byte);
|
|
114459
|
+
i += 2;
|
|
114460
|
+
continue;
|
|
114461
|
+
}
|
|
114462
|
+
}
|
|
114463
|
+
if (ch === "+") {
|
|
114464
|
+
bytes.push(32);
|
|
114465
|
+
continue;
|
|
114466
|
+
}
|
|
114467
|
+
bytes.push(ch.charCodeAt(0) & 255);
|
|
114468
|
+
}
|
|
114469
|
+
return Buffer.from(bytes).toString("latin1");
|
|
114470
|
+
}
|
|
114471
|
+
function substitute(body, params) {
|
|
114472
|
+
let out = body;
|
|
114473
|
+
for (const [key, value] of Object.entries(params)) {
|
|
114474
|
+
const escaped = escapeRegaString(String(value));
|
|
114475
|
+
out = out.split(`##${key}##`).join(escaped);
|
|
114476
|
+
}
|
|
114477
|
+
return out;
|
|
114478
|
+
}
|
|
114479
|
+
async function runScript(client, sessionId, body, params) {
|
|
114480
|
+
const script = params ? substitute(body, params) : body;
|
|
114481
|
+
const result = await client.post(JsonRpcMethod.REGA_RUN_SCRIPT, { script }, sessionId !== void 0 ? { sessionId } : void 0);
|
|
114482
|
+
if (result === void 0 || result === null) throw new ClientError("ReGa.runScript returned no result");
|
|
114483
|
+
if (typeof result === "string") return JSON.parse(sanitizeJsonControlChars(result));
|
|
114484
|
+
return result;
|
|
114485
|
+
}
|
|
114486
|
+
var SET_SYSTEM_VARIABLE = `object sv = dom.GetObject("##name##");
|
|
114487
|
+
if (sv) {
|
|
114488
|
+
sv.State("##value##");
|
|
114489
|
+
}`;
|
|
114490
|
+
var SET_PROGRAM_STATE = `object prog = dom.GetObject(##id##);
|
|
114491
|
+
if (prog) {
|
|
114492
|
+
prog.Active(##state##);
|
|
114493
|
+
}`;
|
|
114494
|
+
var SYSVAR_DESCRIPTIONS = `string sSysVarId;
|
|
114495
|
+
string sSep = "";
|
|
114496
|
+
WriteLine("[");
|
|
114497
|
+
foreach(sSysVarId, dom.GetObject(ID_SYSTEM_VARIABLES).EnumUsedIDs()) {
|
|
114498
|
+
object oSysVar = dom.GetObject(sSysVarId);
|
|
114499
|
+
Write(sSep);
|
|
114500
|
+
Write("{");
|
|
114501
|
+
Write("\\"id\\":");
|
|
114502
|
+
Write(oSysVar.ID());
|
|
114503
|
+
Write(",\\"description\\":\\"");
|
|
114504
|
+
Write(oSysVar.DPInfo().UriEncode());
|
|
114505
|
+
Write("\\"}");
|
|
114506
|
+
sSep = ",";
|
|
114507
|
+
}
|
|
114508
|
+
WriteLine("]");`;
|
|
114509
|
+
var GET_ROOMS_FUNCTIONS = `string sChnId;
|
|
114510
|
+
string sRoomId;
|
|
114511
|
+
string sFuncId;
|
|
114512
|
+
string sChnSepR = "";
|
|
114513
|
+
string sChnSepF = "";
|
|
114514
|
+
Write("{\\"rooms\\":{");
|
|
114515
|
+
foreach(sChnId, dom.GetObject(ID_CHANNELS).EnumUsedIDs()) {
|
|
114516
|
+
object oChn = dom.GetObject(sChnId);
|
|
114517
|
+
if (oChn) {
|
|
114518
|
+
string sAddr = oChn.Address();
|
|
114519
|
+
object oRooms = oChn.ChnRoom();
|
|
114520
|
+
if (oRooms && oRooms.Count() > 0) {
|
|
114521
|
+
Write(sChnSepR);
|
|
114522
|
+
Write("\\"");
|
|
114523
|
+
Write(sAddr);
|
|
114524
|
+
Write("\\":[");
|
|
114525
|
+
string sRoomSep = "";
|
|
114526
|
+
foreach(sRoomId, oRooms) {
|
|
114527
|
+
object oRoom = dom.GetObject(sRoomId);
|
|
114528
|
+
Write(sRoomSep);
|
|
114529
|
+
Write("\\"");
|
|
114530
|
+
Write(oRoom.Name().UriEncode());
|
|
114531
|
+
Write("\\"");
|
|
114532
|
+
sRoomSep = ",";
|
|
114533
|
+
}
|
|
114534
|
+
Write("]");
|
|
114535
|
+
sChnSepR = ",";
|
|
114536
|
+
}
|
|
114537
|
+
}
|
|
114538
|
+
}
|
|
114539
|
+
Write("},\\"functions\\":{");
|
|
114540
|
+
foreach(sChnId, dom.GetObject(ID_CHANNELS).EnumUsedIDs()) {
|
|
114541
|
+
object oChn = dom.GetObject(sChnId);
|
|
114542
|
+
if (oChn) {
|
|
114543
|
+
string sAddr = oChn.Address();
|
|
114544
|
+
object oFuncs = oChn.ChnFunction();
|
|
114545
|
+
if (oFuncs && oFuncs.Count() > 0) {
|
|
114546
|
+
Write(sChnSepF);
|
|
114547
|
+
Write("\\"");
|
|
114548
|
+
Write(sAddr);
|
|
114549
|
+
Write("\\":[");
|
|
114550
|
+
string sFuncSep = "";
|
|
114551
|
+
foreach(sFuncId, oFuncs) {
|
|
114552
|
+
object oFunc = dom.GetObject(sFuncId);
|
|
114553
|
+
Write(sFuncSep);
|
|
114554
|
+
Write("\\"");
|
|
114555
|
+
Write(oFunc.Name().UriEncode());
|
|
114556
|
+
Write("\\"");
|
|
114557
|
+
sFuncSep = ",";
|
|
114558
|
+
}
|
|
114559
|
+
Write("]");
|
|
114560
|
+
sChnSepF = ",";
|
|
114561
|
+
}
|
|
114562
|
+
}
|
|
114563
|
+
}
|
|
114564
|
+
Write("}}");`;
|
|
114565
|
+
var GET_SERVICE_MESSAGES = `string sAID;
|
|
114566
|
+
string sSep = "";
|
|
114567
|
+
Write("[");
|
|
114568
|
+
foreach(sAID, dom.GetObject(ID_SERVICES).EnumIDs()) {
|
|
114569
|
+
object oAlarm = dom.GetObject(sAID);
|
|
114570
|
+
if (oAlarm) {
|
|
114571
|
+
if (oAlarm.IsTypeOf(OT_ALARMDP)) {
|
|
114572
|
+
if (oAlarm.AlState() == asOncoming) {
|
|
114573
|
+
string sAddr = "";
|
|
114574
|
+
string sType = "";
|
|
114575
|
+
string sAck = "true";
|
|
114576
|
+
object oLast = dom.GetObject(oAlarm.LastTriggerID());
|
|
114577
|
+
if (oLast) {
|
|
114578
|
+
sType = oLast.HSSID();
|
|
114579
|
+
object oChn = dom.GetObject(oLast.Channel());
|
|
114580
|
+
if (oChn) {
|
|
114581
|
+
sAddr = oChn.Address();
|
|
114582
|
+
}
|
|
114583
|
+
}
|
|
114584
|
+
object oTrig = dom.GetObject(oAlarm.AlTriggerDP());
|
|
114585
|
+
if (oTrig) {
|
|
114586
|
+
if (!(oTrig.Operations() & OPERATION_WRITE)) {
|
|
114587
|
+
sAck = "false";
|
|
114588
|
+
}
|
|
114589
|
+
}
|
|
114590
|
+
Write(sSep);
|
|
114591
|
+
Write("{\\"id\\":");
|
|
114592
|
+
Write(oAlarm.ID());
|
|
114593
|
+
Write(",\\"name\\":\\"");
|
|
114594
|
+
Write(oAlarm.Name().UriEncode());
|
|
114595
|
+
Write("\\",\\"address\\":\\"");
|
|
114596
|
+
Write(sAddr);
|
|
114597
|
+
Write("\\",\\"type\\":\\"");
|
|
114598
|
+
Write(sType);
|
|
114599
|
+
Write("\\",\\"timestamp\\":");
|
|
114600
|
+
Write(oAlarm.AlOccurrenceTime().ToInteger());
|
|
114601
|
+
Write(",\\"acknowledgeable\\":");
|
|
114602
|
+
Write(sAck);
|
|
114603
|
+
Write("}");
|
|
114604
|
+
sSep = ",";
|
|
114605
|
+
}
|
|
114606
|
+
}
|
|
114607
|
+
}
|
|
114608
|
+
}
|
|
114609
|
+
Write("]");`;
|
|
114610
|
+
var ACKNOWLEDGE_SERVICE_MESSAGE = `object oAlarm = dom.GetObject(##id##);
|
|
114611
|
+
if (oAlarm) {
|
|
114612
|
+
oAlarm.AlReceipt();
|
|
114613
|
+
Write("{\\"ok\\":true}");
|
|
114614
|
+
} else {
|
|
114615
|
+
Write("{\\"ok\\":false}");
|
|
114616
|
+
}`;
|
|
114617
|
+
var GET_CCU_PLATFORM = `string sRelease;
|
|
114618
|
+
string sHost;
|
|
114619
|
+
string sErr;
|
|
114620
|
+
system.Exec("cat /VERSION", &sRelease, &sErr);
|
|
114621
|
+
system.Exec("hostname", &sHost, &sErr);
|
|
114622
|
+
Write("{\\"release\\":\\"");
|
|
114623
|
+
Write(sRelease.UriEncode());
|
|
114624
|
+
Write("\\",\\"host\\":\\"");
|
|
114625
|
+
Write(sHost.UriEncode());
|
|
114626
|
+
Write("\\"}");`;
|
|
114627
|
+
var GROUP_CREATE_PATH = "/pages/jpages/group/create";
|
|
114628
|
+
var GROUP_SAVE_PATH = "/pages/jpages/group/save";
|
|
114629
|
+
var GROUP_DELETE_PATH = "/pages/jpages/group/delete";
|
|
114630
|
+
var DRAFT_GROUP_ID = /self\.groupId\s*=\s*(\d+)/;
|
|
114631
|
+
var KNOWN_GROUP_TYPES = ["hmip.heating.group", "HomeMatic.heating"];
|
|
114632
|
+
var CHANNEL_ADDRESS = /^[0-9A-Za-z_-]+:\d+$/;
|
|
114633
|
+
var MAX_GROUP_NAME = 64;
|
|
114634
|
+
var DEFAULT_INSTALL_SECONDS = 60;
|
|
114635
|
+
var MAX_INSTALL_SECONDS = 3600;
|
|
114636
|
+
var SERVICE_MESSAGE_ID = /^\d+$/;
|
|
114637
|
+
function isRecord(value) {
|
|
114638
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
114639
|
+
}
|
|
114640
|
+
function asRecordArray(value) {
|
|
114641
|
+
return Array.isArray(value) ? value.filter(isRecord) : [];
|
|
114642
|
+
}
|
|
114643
|
+
function strOrNull(value) {
|
|
114644
|
+
if (typeof value === "string") return value === "" ? null : value;
|
|
114645
|
+
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
114646
|
+
return null;
|
|
114647
|
+
}
|
|
114648
|
+
function numOrNull(value) {
|
|
114649
|
+
if (value === null || value === void 0 || value === "") return null;
|
|
114650
|
+
const num = typeof value === "number" ? value : Number(value);
|
|
114651
|
+
return Number.isFinite(num) ? num : null;
|
|
114652
|
+
}
|
|
114653
|
+
var INTERFACES_BY_NAME = new Map(Object.values(Interface).map((iface) => [iface.toString(), iface]));
|
|
114654
|
+
function toInterface(name) {
|
|
114655
|
+
const match = INTERFACES_BY_NAME.get(name);
|
|
114656
|
+
if (match === void 0) throw new ValidationError(`Unknown interface "${name}".`);
|
|
114657
|
+
return match;
|
|
114658
|
+
}
|
|
114659
|
+
function toGateway(info) {
|
|
114660
|
+
return { ...info };
|
|
114661
|
+
}
|
|
114662
|
+
function decodedOrNull(value) {
|
|
114663
|
+
if (typeof value !== "string") return null;
|
|
114664
|
+
const text = decodeRegaName(value).trim();
|
|
114665
|
+
return text === "" ? null : text;
|
|
114666
|
+
}
|
|
114667
|
+
function parseKeyValueLines(text) {
|
|
114668
|
+
const out = /* @__PURE__ */ new Map();
|
|
114669
|
+
for (const line of text.split(/\r?\n/)) {
|
|
114670
|
+
const eq = line.indexOf("=");
|
|
114671
|
+
if (eq > 0) out.set(line.slice(0, eq).trim(), line.slice(eq + 1).trim());
|
|
114672
|
+
}
|
|
114673
|
+
return out;
|
|
114674
|
+
}
|
|
114675
|
+
var OPENCCU_PRODUCTS = /* @__PURE__ */ new Set([
|
|
114676
|
+
"raspberrymatic",
|
|
114677
|
+
"openccu",
|
|
114678
|
+
"ova"
|
|
114679
|
+
]);
|
|
114680
|
+
function productFamily(raw) {
|
|
114681
|
+
if (raw === null) return "unknown";
|
|
114682
|
+
const key = raw.toLowerCase();
|
|
114683
|
+
if (key === "ccu3") return "ccu3";
|
|
114684
|
+
return OPENCCU_PRODUCTS.has(key) ? "openccu" : "unknown";
|
|
114685
|
+
}
|
|
114686
|
+
var CcuManagement = class {
|
|
114687
|
+
json;
|
|
114688
|
+
webUi;
|
|
114689
|
+
getSessionId;
|
|
114690
|
+
interfaceClient;
|
|
114691
|
+
configuredInterfaces;
|
|
114692
|
+
constructor(options) {
|
|
114693
|
+
this.json = options.json;
|
|
114694
|
+
this.webUi = options.webUi;
|
|
114695
|
+
this.getSessionId = options.getSessionId;
|
|
114696
|
+
this.interfaceClient = options.interfaceClient;
|
|
114697
|
+
this.configuredInterfaces = options.configuredInterfaces;
|
|
114698
|
+
}
|
|
114699
|
+
/**
|
|
114700
|
+
* Every interface the CCU offers (JSON-RPC `Interface.listInterfaces`), with
|
|
114701
|
+
* connection state, duty cycle and, for BidCos, the radio gateways. Without a
|
|
114702
|
+
* JSON-RPC client only the configured interfaces are listed and every probe
|
|
114703
|
+
* result is `null`.
|
|
114704
|
+
*/
|
|
114705
|
+
async listInterfaces() {
|
|
114706
|
+
const offered = await this.offeredInterfaces();
|
|
114707
|
+
const gatewaysByName = /* @__PURE__ */ new Map();
|
|
114708
|
+
for (const entry of offered) {
|
|
114709
|
+
const iface = INTERFACES_BY_NAME.get(entry.name);
|
|
114710
|
+
const client = iface === void 0 ? void 0 : this.interfaceClient(iface);
|
|
114711
|
+
if (client === void 0 || iface !== "BidCos-RF" && iface !== "HmIP-RF") continue;
|
|
114712
|
+
const gateways = await this.tryOrNull(() => client.listBidcosInterfaces());
|
|
114713
|
+
if (gateways !== null) gatewaysByName.set(entry.name, gateways.map(toGateway));
|
|
114714
|
+
}
|
|
114715
|
+
const fallbackDuty = offered.some((e) => e.name === "BidCos-RF".toString() && (gatewaysByName.get(e.name) ?? []).every((g) => g.dutyCycle === null)) ? await this.ownRadioDutyCycle() : null;
|
|
114716
|
+
const result = [];
|
|
114717
|
+
for (const entry of offered) {
|
|
114718
|
+
const gateways = gatewaysByName.get(entry.name) ?? [];
|
|
114719
|
+
const primary = gateways.find((g) => g.isDefault) ?? gateways[0];
|
|
114720
|
+
const connected = primary?.connected ?? await this.isPresent(entry.name);
|
|
114721
|
+
const dutyCycle = primary?.dutyCycle ?? (entry.name === "BidCos-RF".toString() ? fallbackDuty : null);
|
|
114722
|
+
result.push({
|
|
114723
|
+
name: entry.name,
|
|
114724
|
+
port: entry.port,
|
|
114725
|
+
info: entry.info,
|
|
114726
|
+
connected,
|
|
114727
|
+
dutyCycle,
|
|
114728
|
+
firmware: primary?.firmware ?? null,
|
|
114729
|
+
type: primary?.type ?? null,
|
|
114730
|
+
gateways
|
|
114731
|
+
});
|
|
114732
|
+
}
|
|
114733
|
+
return result;
|
|
114734
|
+
}
|
|
114735
|
+
/** Open (default 60 s) or close the pairing window of a configured interface. */
|
|
114736
|
+
async setInstallMode(request) {
|
|
114737
|
+
const client = this.requireClient(request.interface);
|
|
114738
|
+
if (!request.on) {
|
|
114739
|
+
await client.setInstallMode({ on: false });
|
|
114740
|
+
return;
|
|
114741
|
+
}
|
|
114742
|
+
const seconds = request.seconds ?? DEFAULT_INSTALL_SECONDS;
|
|
114743
|
+
if (!Number.isInteger(seconds) || seconds < 1 || seconds > MAX_INSTALL_SECONDS) throw new ValidationError(`seconds must be an integer between 1 and ${MAX_INSTALL_SECONDS}, got ${String(seconds)}.`);
|
|
114744
|
+
const call = request.mode === void 0 ? {
|
|
114745
|
+
on: true,
|
|
114746
|
+
seconds
|
|
114747
|
+
} : {
|
|
114748
|
+
on: true,
|
|
114749
|
+
seconds,
|
|
114750
|
+
mode: request.mode === "secure" ? 2 : 1
|
|
114751
|
+
};
|
|
114752
|
+
await client.setInstallMode(call);
|
|
114753
|
+
}
|
|
114754
|
+
/** Seconds left of the pairing window; `null` when the CCU's answer was unusable. */
|
|
114755
|
+
async getInstallMode(interfaceName) {
|
|
114756
|
+
return this.requireClient(interfaceName).getInstallModeRemaining();
|
|
114757
|
+
}
|
|
114758
|
+
/** Every active service message (ReGa listing of `ID_SERVICES`). */
|
|
114759
|
+
async serviceMessages() {
|
|
114760
|
+
return asRecordArray(await this.rega("ReGa.runScript(serviceMessages)", GET_SERVICE_MESSAGES)).flatMap((entry) => {
|
|
114761
|
+
const id = strOrNull(entry["id"]);
|
|
114762
|
+
if (id === null) return [];
|
|
114763
|
+
const address = strOrNull(entry["address"]);
|
|
114764
|
+
const colon = address === null ? -1 : address.lastIndexOf(":");
|
|
114765
|
+
const device = address === null ? null : colon === -1 ? address : address.slice(0, colon);
|
|
114766
|
+
const channel = address === null || colon === -1 ? null : numOrNull(address.slice(colon + 1));
|
|
114767
|
+
const seconds = numOrNull(entry["timestamp"]);
|
|
114768
|
+
return [{
|
|
114769
|
+
id,
|
|
114770
|
+
name: decodedOrNull(entry["name"]),
|
|
114771
|
+
address: device,
|
|
114772
|
+
channel,
|
|
114773
|
+
type: strOrNull(entry["type"]),
|
|
114774
|
+
timestamp: seconds === null || seconds <= 0 ? null : seconds * 1e3,
|
|
114775
|
+
acknowledgeable: entry["acknowledgeable"] === true
|
|
114776
|
+
}];
|
|
114777
|
+
});
|
|
114778
|
+
}
|
|
114779
|
+
/** Acknowledge one service message by its id. */
|
|
114780
|
+
async acknowledgeServiceMessage(id) {
|
|
114781
|
+
if (!SERVICE_MESSAGE_ID.test(id)) throw new ValidationError(`Invalid service message id "${id}".`);
|
|
114782
|
+
const raw = await this.rega("ReGa.runScript(acknowledgeServiceMessage)", ACKNOWLEDGE_SERVICE_MESSAGE, { id });
|
|
114783
|
+
if (!isRecord(raw) || raw["ok"] !== true) throw new ValidationError(`Unknown service message "${id}".`);
|
|
114784
|
+
}
|
|
114785
|
+
/** Version, product, serial and hostname; each `null` when unanswered. */
|
|
114786
|
+
async ccuInfo() {
|
|
114787
|
+
const [version, serial, platform] = await Promise.allSettled([
|
|
114788
|
+
this.call(JsonRpcMethod.CCU_GET_VERSION),
|
|
114789
|
+
this.call(JsonRpcMethod.CCU_GET_SERIAL),
|
|
114790
|
+
this.rega("ReGa.runScript(ccuPlatform)", GET_CCU_PLATFORM)
|
|
114791
|
+
]);
|
|
114792
|
+
if (version.status === "rejected" && serial.status === "rejected" && platform.status === "rejected") throw version.reason instanceof CcuManagementError ? version.reason : new CcuManagementError(JsonRpcMethod.CCU_GET_VERSION, version.reason);
|
|
114793
|
+
const release = platform.status === "fulfilled" && isRecord(platform.value) ? parseKeyValueLines(decodeRegaName(releaseText(platform.value))) : /* @__PURE__ */ new Map();
|
|
114794
|
+
const hostname = platform.status === "fulfilled" && isRecord(platform.value) ? decodedOrNull(platform.value["host"]) : null;
|
|
114795
|
+
const product = strOrNull(release.get("PRODUCT"));
|
|
114796
|
+
return {
|
|
114797
|
+
version: version.status === "fulfilled" ? strOrNull(version.value) : null,
|
|
114798
|
+
product,
|
|
114799
|
+
productFamily: productFamily(product),
|
|
114800
|
+
platform: strOrNull(release.get("PLATFORM")),
|
|
114801
|
+
serial: serial.status === "fulfilled" ? strOrNull(serial.value) : null,
|
|
114802
|
+
hostname
|
|
114803
|
+
};
|
|
114804
|
+
}
|
|
114805
|
+
/**
|
|
114806
|
+
* The heating groups of the CCU ("Gruppen"), read from `CCU.getHeatingGroupList`
|
|
114807
|
+
* (the HMServer's `/etc/config/groups.gson`). `[]` when the CCU has no groups
|
|
114808
|
+
* file. Writes (`createGroup` / `updateGroup` / `deleteGroup`) go through the
|
|
114809
|
+
* HMServer's WebUI pages instead, see the group-writes section below.
|
|
114810
|
+
*/
|
|
114811
|
+
async groups() {
|
|
114812
|
+
const raw = await this.call(JsonRpcMethod.CCU_GET_HEATING_GROUP_LIST);
|
|
114813
|
+
if (typeof raw !== "string" || raw.trim() === "") return [];
|
|
114814
|
+
let parsed;
|
|
114815
|
+
try {
|
|
114816
|
+
parsed = JSON.parse(raw);
|
|
114817
|
+
} catch (err) {
|
|
114818
|
+
throw new CcuManagementError(JsonRpcMethod.CCU_GET_HEATING_GROUP_LIST, err);
|
|
114819
|
+
}
|
|
114820
|
+
if (!isRecord(parsed)) return [];
|
|
114821
|
+
return Object.keys(parsed).sort().flatMap((key) => asRecordArray(parsed[key])).flatMap((entry) => {
|
|
114822
|
+
const group = toGroup(entry);
|
|
114823
|
+
return group === null ? [] : [group];
|
|
114824
|
+
});
|
|
114825
|
+
}
|
|
114826
|
+
/** Create a group; resolves with the group as read back from the CCU. */
|
|
114827
|
+
async createGroup(input) {
|
|
114828
|
+
const name = validateGroupName(input.name);
|
|
114829
|
+
const members = validateMembers(input.members);
|
|
114830
|
+
const before = await this.groups();
|
|
114831
|
+
this.validateType(input.type, before);
|
|
114832
|
+
const { webUi, sid } = await this.requireWebUi("group/create");
|
|
114833
|
+
const page = checkWebUiResponse(GROUP_CREATE_PATH, await webUi.get(GROUP_CREATE_PATH, sid));
|
|
114834
|
+
const draft = DRAFT_GROUP_ID.exec(page);
|
|
114835
|
+
if (draft === null || draft[1] === void 0) throw new CcuManagementError(GROUP_CREATE_PATH, new ClientError("no group id in the page"));
|
|
114836
|
+
const draftId = Number(draft[1]);
|
|
114837
|
+
const id = numOrNull(await this.saveGroup(webUi, sid, {
|
|
114838
|
+
groupId: draftId,
|
|
114839
|
+
groupName: jsEscape(name),
|
|
114840
|
+
groupTypeId: input.type,
|
|
114841
|
+
forbidSingleOperation: false,
|
|
114842
|
+
assignedDevicesIds: members,
|
|
114843
|
+
isNewGroup: true,
|
|
114844
|
+
groupDeviceName: `${name} `
|
|
114845
|
+
})) ?? draftId;
|
|
114846
|
+
await this.flagMembers(members, "true");
|
|
114847
|
+
return this.verifyPresent(id, {
|
|
114848
|
+
name,
|
|
114849
|
+
type: input.type,
|
|
114850
|
+
members
|
|
114851
|
+
});
|
|
114852
|
+
}
|
|
114853
|
+
/** Change a group's name and/or member list; resolves with the group read back. */
|
|
114854
|
+
async updateGroup(id, patch) {
|
|
114855
|
+
if (patch.name === void 0 && patch.members === void 0) throw new ValidationError("updateGroup needs a name and/or members.");
|
|
114856
|
+
const name = patch.name === void 0 ? void 0 : validateGroupName(patch.name);
|
|
114857
|
+
const members = patch.members === void 0 ? void 0 : validateMembers(patch.members);
|
|
114858
|
+
const current = await this.requireGroup(id);
|
|
114859
|
+
const { webUi, sid } = await this.requireWebUi("group/save");
|
|
114860
|
+
const nextName = name ?? current.name;
|
|
114861
|
+
const nextMembers = members ?? current.members.map((m) => m.address);
|
|
114862
|
+
await this.saveGroup(webUi, sid, {
|
|
114863
|
+
groupId: id,
|
|
114864
|
+
groupName: jsEscape(nextName),
|
|
114865
|
+
groupTypeId: current.type,
|
|
114866
|
+
forbidSingleOperation: current.forbidSingleOperation,
|
|
114867
|
+
assignedDevicesIds: nextMembers,
|
|
114868
|
+
isNewGroup: false,
|
|
114869
|
+
groupDeviceName: current.deviceAddress === null ? nextName : `${nextName} ${virtualDeviceSerial(id)}`
|
|
114870
|
+
});
|
|
114871
|
+
await this.flagMembers(nextMembers, "true");
|
|
114872
|
+
const kept = new Set(nextMembers);
|
|
114873
|
+
await this.flagMembers(current.members.map((m) => m.address).filter((a) => !kept.has(a)), "false");
|
|
114874
|
+
return this.verifyPresent(id, {
|
|
114875
|
+
name: nextName,
|
|
114876
|
+
type: current.type,
|
|
114877
|
+
members: nextMembers
|
|
114878
|
+
});
|
|
114879
|
+
}
|
|
114880
|
+
/** Delete a group; verified by the group being gone from the read-back. */
|
|
114881
|
+
async deleteGroup(id) {
|
|
114882
|
+
const current = await this.requireGroup(id);
|
|
114883
|
+
const { webUi, sid } = await this.requireWebUi("group/delete");
|
|
114884
|
+
checkWebUiResponse(GROUP_DELETE_PATH, await webUi.post(GROUP_DELETE_PATH, sid, JSON.stringify({ groupId: id })));
|
|
114885
|
+
await this.flagMembers(current.members.map((m) => m.address), "false");
|
|
114886
|
+
if ((await this.groups()).some((g) => g.id === id)) throw new GroupWriteVerificationError(`Group ${id} is still listed after deleteGroup.`);
|
|
114887
|
+
}
|
|
114888
|
+
async requireGroup(id) {
|
|
114889
|
+
if (!Number.isInteger(id)) throw new ValidationError(`Invalid group id ${String(id)}.`);
|
|
114890
|
+
const found = (await this.groups()).find((g) => g.id === id);
|
|
114891
|
+
if (found === void 0) throw new ValidationError(`Unknown group ${id}.`);
|
|
114892
|
+
return found;
|
|
114893
|
+
}
|
|
114894
|
+
validateType(type, existing) {
|
|
114895
|
+
const known = /* @__PURE__ */ new Set([...KNOWN_GROUP_TYPES, ...existing.map((g) => g.type)]);
|
|
114896
|
+
if (!known.has(type)) throw new ValidationError(`Unknown group type "${type}" (known: ${[...known].join(", ")}).`);
|
|
114897
|
+
}
|
|
114898
|
+
async requireWebUi(label) {
|
|
114899
|
+
if (this.webUi === void 0) throw new ValidationError(`${label} needs WebUI credentials (HMServer pages); none are configured.`);
|
|
114900
|
+
const sid = await this.getSessionId();
|
|
114901
|
+
if (sid === void 0) throw new ValidationError(`${label} needs a WebUI session.`);
|
|
114902
|
+
return {
|
|
114903
|
+
webUi: this.webUi,
|
|
114904
|
+
sid
|
|
114905
|
+
};
|
|
114906
|
+
}
|
|
114907
|
+
/** POST `group/save`; returns the response `content` (the group id as text). */
|
|
114908
|
+
async saveGroup(webUi, sid, payload) {
|
|
114909
|
+
return checkWebUiResponse(GROUP_SAVE_PATH, await webUi.post(GROUP_SAVE_PATH, sid, JSON.stringify(payload)));
|
|
114910
|
+
}
|
|
114911
|
+
/**
|
|
114912
|
+
* The WebUI marks every device it puts in / takes out of a heating group with
|
|
114913
|
+
* `Interface.setMetadata inHeatingGroup` (its pages use the flag to hide
|
|
114914
|
+
* grouped devices). Best effort: the hint is cosmetic and the group itself is
|
|
114915
|
+
* verified separately.
|
|
114916
|
+
*/
|
|
114917
|
+
async flagMembers(addresses, value) {
|
|
114918
|
+
const devices = [...new Set(addresses.map((a) => a.slice(0, a.lastIndexOf(":"))))];
|
|
114919
|
+
for (const address of devices) try {
|
|
114920
|
+
const objectId = await this.call(JsonRpcMethod.DEVICE_GET_REGA_ID_BY_ADDRESS, { address });
|
|
114921
|
+
if (typeof objectId !== "string" || objectId === "" || objectId === "noDeviceFound") continue;
|
|
114922
|
+
await this.call(JsonRpcMethod.INTERFACE_SET_METADATA, {
|
|
114923
|
+
objectId,
|
|
114924
|
+
dataId: "inHeatingGroup",
|
|
114925
|
+
value
|
|
114926
|
+
});
|
|
114927
|
+
} catch {}
|
|
114928
|
+
}
|
|
114929
|
+
async verifyPresent(id, want) {
|
|
114930
|
+
const found = (await this.groups()).find((g) => g.id === id);
|
|
114931
|
+
if (found === void 0) throw new GroupWriteVerificationError(`Group ${id} is not listed after the write.`);
|
|
114932
|
+
const have = found.members.map((m) => m.address).sort();
|
|
114933
|
+
const expected = [...want.members].sort();
|
|
114934
|
+
const sameMembers = have.length === expected.length && have.every((a, i) => a === expected[i]);
|
|
114935
|
+
if (found.name !== want.name || found.type !== want.type || !sameMembers) throw new GroupWriteVerificationError(`Group ${id} reads back as "${found.name}" (${found.type}, ${have.length} members), expected "${want.name}" (${want.type}, ${expected.length} members).`);
|
|
114936
|
+
return found;
|
|
114937
|
+
}
|
|
114938
|
+
requireClient(interfaceName) {
|
|
114939
|
+
const iface = toInterface(interfaceName);
|
|
114940
|
+
const client = this.interfaceClient(iface);
|
|
114941
|
+
if (client === void 0) throw new ValidationError(`Interface "${interfaceName}" is not configured.`);
|
|
114942
|
+
return client;
|
|
114943
|
+
}
|
|
114944
|
+
requireJson(method) {
|
|
114945
|
+
if (this.json === void 0) throw new ValidationError(`${method} needs WebUI credentials (JSON-RPC); none are configured.`);
|
|
114946
|
+
return this.json;
|
|
114947
|
+
}
|
|
114948
|
+
/** A session-bound JSON-RPC call whose failure names the method. */
|
|
114949
|
+
async call(method, params) {
|
|
114950
|
+
const json = this.requireJson(method);
|
|
114951
|
+
try {
|
|
114952
|
+
const sessionId = await this.getSessionId();
|
|
114953
|
+
return await json.post(method, params, sessionId !== void 0 ? { sessionId } : void 0);
|
|
114954
|
+
} catch (err) {
|
|
114955
|
+
throw err instanceof ValidationError ? err : new CcuManagementError(method, err);
|
|
114956
|
+
}
|
|
114957
|
+
}
|
|
114958
|
+
async rega(label, script, params) {
|
|
114959
|
+
const json = this.requireJson(label);
|
|
114960
|
+
try {
|
|
114961
|
+
return await runScript(json, await this.getSessionId(), script, params);
|
|
114962
|
+
} catch (err) {
|
|
114963
|
+
throw err instanceof ValidationError ? err : new CcuManagementError(label, err);
|
|
114964
|
+
}
|
|
114965
|
+
}
|
|
114966
|
+
async tryOrNull(fn) {
|
|
114967
|
+
try {
|
|
114968
|
+
return await fn();
|
|
114969
|
+
} catch {
|
|
114970
|
+
return null;
|
|
114971
|
+
}
|
|
114972
|
+
}
|
|
114973
|
+
async offeredInterfaces() {
|
|
114974
|
+
if (this.json === void 0) return this.configuredInterfaces().map((iface) => ({
|
|
114975
|
+
name: iface,
|
|
114976
|
+
port: null,
|
|
114977
|
+
info: null
|
|
114978
|
+
}));
|
|
114979
|
+
return asRecordArray(await this.call(JsonRpcMethod.INTERFACE_LIST_INTERFACES)).flatMap((entry) => {
|
|
114980
|
+
const name = strOrNull(entry["name"]);
|
|
114981
|
+
return name === null ? [] : [{
|
|
114982
|
+
name,
|
|
114983
|
+
port: numOrNull(entry["port"]),
|
|
114984
|
+
info: strOrNull(entry["info"])
|
|
114985
|
+
}];
|
|
114986
|
+
});
|
|
114987
|
+
}
|
|
114988
|
+
async isPresent(name) {
|
|
114989
|
+
if (this.json === void 0) return null;
|
|
114990
|
+
const answer = await this.tryOrNull(() => this.call(JsonRpcMethod.INTERFACE_IS_PRESENT, { interface: name }));
|
|
114991
|
+
if (typeof answer === "boolean") return answer;
|
|
114992
|
+
if (answer === "true") return true;
|
|
114993
|
+
if (answer === "false") return false;
|
|
114994
|
+
return null;
|
|
114995
|
+
}
|
|
114996
|
+
/**
|
|
114997
|
+
* Fallback when `listBidcosInterfaces` gave no answer: `Interface.getDutyCycle`
|
|
114998
|
+
* (RaspberryMatic / OpenCCU) reports the CCU's own radio module as ONE entry
|
|
114999
|
+
* with string numbers (`"7"`). It cannot be attributed to HmIP, so it only
|
|
115000
|
+
* ever stands in for BidCos-RF; non-numeric or absent is `null`.
|
|
115001
|
+
*/
|
|
115002
|
+
async ownRadioDutyCycle() {
|
|
115003
|
+
if (this.json === void 0) return null;
|
|
115004
|
+
const entries = asRecordArray(await this.tryOrNull(() => this.call(JsonRpcMethod.INTERFACE_GET_DUTY_CYCLE)));
|
|
115005
|
+
return entries.length === 1 ? numOrNull(entries[0]?.["dutyCycle"]) : null;
|
|
115006
|
+
}
|
|
115007
|
+
};
|
|
115008
|
+
function releaseText(value) {
|
|
115009
|
+
const release = value["release"];
|
|
115010
|
+
return typeof release === "string" ? release : "";
|
|
115011
|
+
}
|
|
115012
|
+
var DEVICE_ADDRESS_SUFFIX = /\b(INT\d{7})\s*$/;
|
|
115013
|
+
function toGroup(entry) {
|
|
115014
|
+
const id = numOrNull(entry["id"]);
|
|
115015
|
+
if (id === null) return null;
|
|
115016
|
+
const props = isRecord(entry["groupProperties"]) ? entry["groupProperties"] : {};
|
|
115017
|
+
const groupType = isRecord(entry["groupType"]) ? entry["groupType"] : {};
|
|
115018
|
+
const deviceName = strOrNull(props["GROUP_DEVICE_NAME"]);
|
|
115019
|
+
const deviceMatch = deviceName === null ? null : DEVICE_ADDRESS_SUFFIX.exec(deviceName);
|
|
115020
|
+
const members = asRecordArray(entry["groupMembers"]).flatMap((m) => {
|
|
115021
|
+
const address = strOrNull(m["id"]);
|
|
115022
|
+
if (address === null) return [];
|
|
115023
|
+
return [{
|
|
115024
|
+
address,
|
|
115025
|
+
type: (isRecord(m["memberType"]) ? strOrNull(m["memberType"]["id"]) : null) ?? ""
|
|
115026
|
+
}];
|
|
115027
|
+
});
|
|
115028
|
+
return {
|
|
115029
|
+
id,
|
|
115030
|
+
name: (strOrNull(props["NAME"]) ?? "").trim(),
|
|
115031
|
+
type: strOrNull(groupType["id"]) ?? "",
|
|
115032
|
+
typeLabel: strOrNull(groupType["label"]),
|
|
115033
|
+
deviceAddress: deviceMatch === null ? null : deviceMatch[1] ?? null,
|
|
115034
|
+
forbidSingleOperation: props["FORBID_SINGLE_OPERATION"] === true,
|
|
115035
|
+
members
|
|
115036
|
+
};
|
|
115037
|
+
}
|
|
115038
|
+
function virtualDeviceSerial(id) {
|
|
115039
|
+
return `INT${String(id).padStart(7, "0")}`;
|
|
115040
|
+
}
|
|
115041
|
+
function validateGroupName(raw) {
|
|
115042
|
+
const name = raw.replace(/"/g, "'").trim();
|
|
115043
|
+
if (name === "" || name.length > MAX_GROUP_NAME) throw new ValidationError(`Group name must be 1..${MAX_GROUP_NAME} characters.`);
|
|
115044
|
+
return name;
|
|
115045
|
+
}
|
|
115046
|
+
function validateMembers(members) {
|
|
115047
|
+
for (const address of members) if (!CHANNEL_ADDRESS.test(address)) throw new ValidationError(`Invalid member channel address "${address}".`);
|
|
115048
|
+
return [...new Set(members)];
|
|
115049
|
+
}
|
|
115050
|
+
function checkWebUiResponse(path$5, text) {
|
|
115051
|
+
let parsed;
|
|
115052
|
+
try {
|
|
115053
|
+
parsed = JSON.parse(text);
|
|
115054
|
+
} catch {
|
|
115055
|
+
throw new CcuManagementError(path$5, new ClientError("response was not JSON"));
|
|
115056
|
+
}
|
|
115057
|
+
if (!isRecord(parsed) || parsed["isSuccessful"] !== true) {
|
|
115058
|
+
const code = isRecord(parsed) ? strOrNull(parsed["errorCode"]) : null;
|
|
115059
|
+
throw new CcuManagementError(path$5, new ClientError(code === "42" ? "session expired (errorCode 42)" : `refused (errorCode ${code ?? "none"})`));
|
|
115060
|
+
}
|
|
115061
|
+
const content = parsed["content"];
|
|
115062
|
+
return typeof content === "string" || typeof content === "number" ? String(content) : "";
|
|
115063
|
+
}
|
|
114267
115064
|
var FAULT_METHOD_NOT_FOUND = -32601;
|
|
114268
115065
|
var FAULT_INTERNAL_ERROR = -32603;
|
|
114269
115066
|
var MethodNotFoundError = class extends BaseHomematicError {};
|
|
@@ -115171,128 +115968,6 @@ function asString(value) {
|
|
|
115171
115968
|
if (typeof value === "string" && value !== "") return value;
|
|
115172
115969
|
if (typeof value === "number") return String(value);
|
|
115173
115970
|
}
|
|
115174
|
-
function escapeRegaString(s) {
|
|
115175
|
-
return s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
115176
|
-
}
|
|
115177
|
-
var CONTROL_CHARS = /[\u0000-\u001F]/g;
|
|
115178
|
-
function sanitizeJsonControlChars(s) {
|
|
115179
|
-
return s.replace(CONTROL_CHARS, "");
|
|
115180
|
-
}
|
|
115181
|
-
function decodeRegaName(encoded) {
|
|
115182
|
-
const bytes = [];
|
|
115183
|
-
for (let i = 0; i < encoded.length; i += 1) {
|
|
115184
|
-
const ch = encoded[i];
|
|
115185
|
-
if (ch === "%" && i + 2 < encoded.length) {
|
|
115186
|
-
const hex = encoded.slice(i + 1, i + 3);
|
|
115187
|
-
const byte = Number.parseInt(hex, 16);
|
|
115188
|
-
if (!Number.isNaN(byte) && /^[0-9a-fA-F]{2}$/.test(hex)) {
|
|
115189
|
-
bytes.push(byte);
|
|
115190
|
-
i += 2;
|
|
115191
|
-
continue;
|
|
115192
|
-
}
|
|
115193
|
-
}
|
|
115194
|
-
if (ch === "+") {
|
|
115195
|
-
bytes.push(32);
|
|
115196
|
-
continue;
|
|
115197
|
-
}
|
|
115198
|
-
bytes.push(ch.charCodeAt(0) & 255);
|
|
115199
|
-
}
|
|
115200
|
-
return Buffer.from(bytes).toString("latin1");
|
|
115201
|
-
}
|
|
115202
|
-
function substitute(body, params) {
|
|
115203
|
-
let out = body;
|
|
115204
|
-
for (const [key, value] of Object.entries(params)) {
|
|
115205
|
-
const escaped = escapeRegaString(String(value));
|
|
115206
|
-
out = out.split(`##${key}##`).join(escaped);
|
|
115207
|
-
}
|
|
115208
|
-
return out;
|
|
115209
|
-
}
|
|
115210
|
-
async function runScript(client, sessionId, body, params) {
|
|
115211
|
-
const script = params ? substitute(body, params) : body;
|
|
115212
|
-
const result = await client.post(JsonRpcMethod.REGA_RUN_SCRIPT, { script }, sessionId !== void 0 ? { sessionId } : void 0);
|
|
115213
|
-
if (result === void 0 || result === null) throw new ClientError("ReGa.runScript returned no result");
|
|
115214
|
-
if (typeof result === "string") return JSON.parse(sanitizeJsonControlChars(result));
|
|
115215
|
-
return result;
|
|
115216
|
-
}
|
|
115217
|
-
var SET_SYSTEM_VARIABLE = `object sv = dom.GetObject("##name##");
|
|
115218
|
-
if (sv) {
|
|
115219
|
-
sv.State("##value##");
|
|
115220
|
-
}`;
|
|
115221
|
-
var SET_PROGRAM_STATE = `object prog = dom.GetObject(##id##);
|
|
115222
|
-
if (prog) {
|
|
115223
|
-
prog.Active(##state##);
|
|
115224
|
-
}`;
|
|
115225
|
-
var SYSVAR_DESCRIPTIONS = `string sSysVarId;
|
|
115226
|
-
string sSep = "";
|
|
115227
|
-
WriteLine("[");
|
|
115228
|
-
foreach(sSysVarId, dom.GetObject(ID_SYSTEM_VARIABLES).EnumUsedIDs()) {
|
|
115229
|
-
object oSysVar = dom.GetObject(sSysVarId);
|
|
115230
|
-
Write(sSep);
|
|
115231
|
-
Write("{");
|
|
115232
|
-
Write("\\"id\\":");
|
|
115233
|
-
Write(oSysVar.ID());
|
|
115234
|
-
Write(",\\"description\\":\\"");
|
|
115235
|
-
Write(oSysVar.DPInfo().UriEncode());
|
|
115236
|
-
Write("\\"}");
|
|
115237
|
-
sSep = ",";
|
|
115238
|
-
}
|
|
115239
|
-
WriteLine("]");`;
|
|
115240
|
-
var GET_ROOMS_FUNCTIONS = `string sChnId;
|
|
115241
|
-
string sRoomId;
|
|
115242
|
-
string sFuncId;
|
|
115243
|
-
string sChnSepR = "";
|
|
115244
|
-
string sChnSepF = "";
|
|
115245
|
-
Write("{\\"rooms\\":{");
|
|
115246
|
-
foreach(sChnId, dom.GetObject(ID_CHANNELS).EnumUsedIDs()) {
|
|
115247
|
-
object oChn = dom.GetObject(sChnId);
|
|
115248
|
-
if (oChn) {
|
|
115249
|
-
string sAddr = oChn.Address();
|
|
115250
|
-
object oRooms = oChn.ChnRoom();
|
|
115251
|
-
if (oRooms && oRooms.Count() > 0) {
|
|
115252
|
-
Write(sChnSepR);
|
|
115253
|
-
Write("\\"");
|
|
115254
|
-
Write(sAddr);
|
|
115255
|
-
Write("\\":[");
|
|
115256
|
-
string sRoomSep = "";
|
|
115257
|
-
foreach(sRoomId, oRooms) {
|
|
115258
|
-
object oRoom = dom.GetObject(sRoomId);
|
|
115259
|
-
Write(sRoomSep);
|
|
115260
|
-
Write("\\"");
|
|
115261
|
-
Write(oRoom.Name().UriEncode());
|
|
115262
|
-
Write("\\"");
|
|
115263
|
-
sRoomSep = ",";
|
|
115264
|
-
}
|
|
115265
|
-
Write("]");
|
|
115266
|
-
sChnSepR = ",";
|
|
115267
|
-
}
|
|
115268
|
-
}
|
|
115269
|
-
}
|
|
115270
|
-
Write("},\\"functions\\":{");
|
|
115271
|
-
foreach(sChnId, dom.GetObject(ID_CHANNELS).EnumUsedIDs()) {
|
|
115272
|
-
object oChn = dom.GetObject(sChnId);
|
|
115273
|
-
if (oChn) {
|
|
115274
|
-
string sAddr = oChn.Address();
|
|
115275
|
-
object oFuncs = oChn.ChnFunction();
|
|
115276
|
-
if (oFuncs && oFuncs.Count() > 0) {
|
|
115277
|
-
Write(sChnSepF);
|
|
115278
|
-
Write("\\"");
|
|
115279
|
-
Write(sAddr);
|
|
115280
|
-
Write("\\":[");
|
|
115281
|
-
string sFuncSep = "";
|
|
115282
|
-
foreach(sFuncId, oFuncs) {
|
|
115283
|
-
object oFunc = dom.GetObject(sFuncId);
|
|
115284
|
-
Write(sFuncSep);
|
|
115285
|
-
Write("\\"");
|
|
115286
|
-
Write(oFunc.Name().UriEncode());
|
|
115287
|
-
Write("\\"");
|
|
115288
|
-
sFuncSep = ",";
|
|
115289
|
-
}
|
|
115290
|
-
Write("]");
|
|
115291
|
-
sChnSepF = ",";
|
|
115292
|
-
}
|
|
115293
|
-
}
|
|
115294
|
-
}
|
|
115295
|
-
Write("}}");`;
|
|
115296
115971
|
var HubValueType = /* @__PURE__ */ ((HubValueType2) => {
|
|
115297
115972
|
HubValueType2["ALARM"] = "ALARM";
|
|
115298
115973
|
HubValueType2["FLOAT"] = "FLOAT";
|
|
@@ -115331,7 +116006,7 @@ function parseSysVarValue(type, raw) {
|
|
|
115331
116006
|
}
|
|
115332
116007
|
}
|
|
115333
116008
|
var WRITABLE_MARKER = "HAHM";
|
|
115334
|
-
function
|
|
116009
|
+
function isRecord2(value) {
|
|
115335
116010
|
return value !== null && typeof value === "object";
|
|
115336
116011
|
}
|
|
115337
116012
|
function asString2(value) {
|
|
@@ -115350,9 +116025,9 @@ function asOptionalNumber(value) {
|
|
|
115350
116025
|
const num = Number(value);
|
|
115351
116026
|
return Number.isFinite(num) ? num : void 0;
|
|
115352
116027
|
}
|
|
115353
|
-
function
|
|
116028
|
+
function asRecordArray2(value) {
|
|
115354
116029
|
if (!Array.isArray(value)) return [];
|
|
115355
|
-
return value.filter(
|
|
116030
|
+
return value.filter(isRecord2);
|
|
115356
116031
|
}
|
|
115357
116032
|
var HubFetcher = class {
|
|
115358
116033
|
client;
|
|
@@ -115371,7 +116046,7 @@ var HubFetcher = class {
|
|
|
115371
116046
|
*/
|
|
115372
116047
|
async fetchSystemVariables() {
|
|
115373
116048
|
const sid = this.sid();
|
|
115374
|
-
const entries =
|
|
116049
|
+
const entries = asRecordArray2(await this.client.post(JsonRpcMethod.SYSVAR_GET_ALL, void 0, sidOpts(sid)));
|
|
115375
116050
|
const descriptions = await this.fetchSysVarDescriptions();
|
|
115376
116051
|
return entries.map((entry) => this.toSystemVariable(entry, descriptions));
|
|
115377
116052
|
}
|
|
@@ -115413,7 +116088,7 @@ var HubFetcher = class {
|
|
|
115413
116088
|
/** Fetch all programs, coercing flags to booleans. */
|
|
115414
116089
|
async fetchPrograms() {
|
|
115415
116090
|
const sid = this.sid();
|
|
115416
|
-
return
|
|
116091
|
+
return asRecordArray2(await this.client.post(JsonRpcMethod.PROGRAM_GET_ALL, void 0, sidOpts(sid))).map((entry) => {
|
|
115417
116092
|
const lastExecuteTime = entry.lastExecuteTime;
|
|
115418
116093
|
return {
|
|
115419
116094
|
id: asString2(entry.id),
|
|
@@ -115445,8 +116120,8 @@ var HubFetcher = class {
|
|
|
115445
116120
|
const sid = this.sid();
|
|
115446
116121
|
const parsed = await runScript(this.client, sid, GET_ROOMS_FUNCTIONS);
|
|
115447
116122
|
return {
|
|
115448
|
-
rooms: toDecodedMap(
|
|
115449
|
-
functions: toDecodedMap(
|
|
116123
|
+
rooms: toDecodedMap(isRecord2(parsed) ? parsed.rooms : void 0),
|
|
116124
|
+
functions: toDecodedMap(isRecord2(parsed) ? parsed.functions : void 0)
|
|
115450
116125
|
};
|
|
115451
116126
|
}
|
|
115452
116127
|
/** Build the `SYSVAR_DESCRIPTIONS` map: sysvar id → description text. */
|
|
@@ -115455,7 +116130,7 @@ var HubFetcher = class {
|
|
|
115455
116130
|
const out = /* @__PURE__ */ new Map();
|
|
115456
116131
|
try {
|
|
115457
116132
|
const parsed = await runScript(this.client, sid, SYSVAR_DESCRIPTIONS);
|
|
115458
|
-
for (const entry of
|
|
116133
|
+
for (const entry of asRecordArray2(parsed)) {
|
|
115459
116134
|
const id = asString2(entry.id);
|
|
115460
116135
|
if (id !== "") out.set(id, asString2(entry.description));
|
|
115461
116136
|
}
|
|
@@ -115492,7 +116167,7 @@ function sidOpts(sid) {
|
|
|
115492
116167
|
}
|
|
115493
116168
|
function toDecodedMap(value) {
|
|
115494
116169
|
const map = /* @__PURE__ */ new Map();
|
|
115495
|
-
if (!
|
|
116170
|
+
if (!isRecord2(value)) return map;
|
|
115496
116171
|
for (const [addr, names] of Object.entries(value)) if (Array.isArray(names)) map.set(addr, names.filter((n) => typeof n === "string").map((n) => decodeRegaName(n)));
|
|
115497
116172
|
return map;
|
|
115498
116173
|
}
|
|
@@ -116021,6 +116696,7 @@ var CentralUnit = class {
|
|
|
116021
116696
|
ifaceById = /* @__PURE__ */ new Map();
|
|
116022
116697
|
jsonClient;
|
|
116023
116698
|
session;
|
|
116699
|
+
webUi;
|
|
116024
116700
|
callbackServer;
|
|
116025
116701
|
router;
|
|
116026
116702
|
recovery;
|
|
@@ -116088,6 +116764,30 @@ var CentralUnit = class {
|
|
|
116088
116764
|
});
|
|
116089
116765
|
}
|
|
116090
116766
|
/**
|
|
116767
|
+
* Build the {@link CcuManagement} bound to this central's interface clients and
|
|
116768
|
+
* (when credentialed) its JSON-RPC client. The session id is ensured per call
|
|
116769
|
+
* through the {@link SessionManager}, so renews apply. INTERNAL: the facade
|
|
116770
|
+
* exposes it; it is not part of the published surface.
|
|
116771
|
+
*/
|
|
116772
|
+
getManagement() {
|
|
116773
|
+
return new CcuManagement({
|
|
116774
|
+
json: this.jsonClient,
|
|
116775
|
+
webUi: this.webUi,
|
|
116776
|
+
getSessionId: () => this.session !== void 0 ? this.session.ensureSession() : Promise.resolve(void 0),
|
|
116777
|
+
interfaceClient: (iface) => {
|
|
116778
|
+
for (const runtime of this.runtimes.values()) if (runtime.iface === iface) return runtime.client;
|
|
116779
|
+
},
|
|
116780
|
+
configuredInterfaces: () => this.interfaces
|
|
116781
|
+
});
|
|
116782
|
+
}
|
|
116783
|
+
/**
|
|
116784
|
+
* Remove a device from the interface owning it, routing by `interfaceId`.
|
|
116785
|
+
* Throws if the interface is unknown.
|
|
116786
|
+
*/
|
|
116787
|
+
async deleteDevice(interfaceId2, address, flags) {
|
|
116788
|
+
await this.clientFor(interfaceId2).deleteDevice(address, flags);
|
|
116789
|
+
}
|
|
116790
|
+
/**
|
|
116091
116791
|
* Read the discovered paramset DESCRIPTION (the spec — types, ranges, flags)
|
|
116092
116792
|
* for a channel + paramset key from the in-memory {@link ParamsetDescriptionCache}.
|
|
116093
116793
|
* Returns `undefined` if the paramset has not been discovered. This is the
|
|
@@ -116186,14 +116886,14 @@ var CentralUnit = class {
|
|
|
116186
116886
|
};
|
|
116187
116887
|
const tls = this.tls;
|
|
116188
116888
|
const port = this.portFor(iface);
|
|
116189
|
-
const path$
|
|
116889
|
+
const path$6 = INTERFACE_REMOTE_PATH[iface];
|
|
116190
116890
|
return new InterfaceClient({
|
|
116191
116891
|
centralName: this.centralName,
|
|
116192
116892
|
interface: iface,
|
|
116193
116893
|
host: this.host,
|
|
116194
116894
|
port,
|
|
116195
116895
|
tls,
|
|
116196
|
-
...path$
|
|
116896
|
+
...path$6 !== void 0 ? { path: path$6 } : {},
|
|
116197
116897
|
...auth !== void 0 ? { auth } : {},
|
|
116198
116898
|
callbackUrlProvider: () => this.callbackUrl()
|
|
116199
116899
|
});
|
|
@@ -116219,10 +116919,14 @@ var CentralUnit = class {
|
|
|
116219
116919
|
}
|
|
116220
116920
|
buildJsonRpc() {
|
|
116221
116921
|
if (this.credentials === void 0) return;
|
|
116222
|
-
if (this.options.jsonClient !== void 0)
|
|
116223
|
-
|
|
116224
|
-
|
|
116922
|
+
if (this.options.jsonClient !== void 0) {
|
|
116923
|
+
this.jsonClient = this.options.jsonClient;
|
|
116924
|
+
this.webUi = this.options.webUiClient;
|
|
116925
|
+
} else {
|
|
116926
|
+
const scheme = this.tls ? "https" : "http";
|
|
116927
|
+
const client = new JsonRpcClient({ url: `${scheme}://${this.host}` });
|
|
116225
116928
|
this.jsonClient = client;
|
|
116929
|
+
this.webUi = this.options.webUiClient ?? new WebUiHttpClient({ url: `${scheme}://${this.host}` });
|
|
116226
116930
|
this.session = new SessionManager({
|
|
116227
116931
|
client,
|
|
116228
116932
|
username: this.credentials.username,
|
|
@@ -117700,7 +118404,7 @@ var TypedEventEmitter = class {
|
|
|
117700
118404
|
};
|
|
117701
118405
|
var DEFAULT_CENTRAL_NAME = "nodehomematic";
|
|
117702
118406
|
var pendingTestInjectables;
|
|
117703
|
-
function
|
|
118407
|
+
function toInterface2(name) {
|
|
117704
118408
|
const match = Object.values(Interface).includes(name) ? name : void 0;
|
|
117705
118409
|
if (match === void 0) throw new ValidationError(`Unknown interface "${name}".`);
|
|
117706
118410
|
return match;
|
|
@@ -117710,7 +118414,7 @@ function deviceAddressOf4(channelAddress2) {
|
|
|
117710
118414
|
return colon === -1 ? channelAddress2 : channelAddress2.slice(0, colon);
|
|
117711
118415
|
}
|
|
117712
118416
|
function buildCentral(options, injectables) {
|
|
117713
|
-
const interfaces = options.interfaces.map(
|
|
118417
|
+
const interfaces = options.interfaces.map(toInterface2);
|
|
117714
118418
|
if (interfaces.length === 0) throw new ValidationError("At least one interface is required.");
|
|
117715
118419
|
if (injectables.central !== void 0) return injectables.central;
|
|
117716
118420
|
return new CentralUnit({
|
|
@@ -117743,6 +118447,8 @@ var Homematic = class {
|
|
|
117743
118447
|
#started = false;
|
|
117744
118448
|
/** Bound hub fetcher, or undefined when no JSON-RPC client is available. */
|
|
117745
118449
|
#hub;
|
|
118450
|
+
/** CCU-management service, built on first use, cleared at stop(). */
|
|
118451
|
+
#management;
|
|
117746
118452
|
/** Snapshot of system variables, refreshed on start / refreshHub. */
|
|
117747
118453
|
#sysvars = [];
|
|
117748
118454
|
/** Snapshot of programs, refreshed on start / refreshHub. */
|
|
@@ -117800,6 +118506,7 @@ var Homematic = class {
|
|
|
117800
118506
|
this.#customEntityList = [];
|
|
117801
118507
|
this.#customEntitiesByKey.clear();
|
|
117802
118508
|
this.#hub = void 0;
|
|
118509
|
+
this.#management = void 0;
|
|
117803
118510
|
this.#sysvars = [];
|
|
117804
118511
|
this.#programs = [];
|
|
117805
118512
|
this.#roomsByDevice.clear();
|
|
@@ -117844,6 +118551,98 @@ var Homematic = class {
|
|
|
117844
118551
|
await this.#central.installFirmware(device.interfaceId, deviceAddress);
|
|
117845
118552
|
}
|
|
117846
118553
|
/**
|
|
118554
|
+
* Every interface the CCU offers (BidCos-RF, HmIP-RF, VirtualDevices, ...)
|
|
118555
|
+
* with connection state, duty cycle, firmware and type where the CCU reports
|
|
118556
|
+
* them. A value the CCU did not answer is `null`, never `0`. Needs a started
|
|
118557
|
+
* facade.
|
|
118558
|
+
*
|
|
118559
|
+
* CCU methods: JSON-RPC `Interface.listInterfaces` / `Interface.isPresent`
|
|
118560
|
+
* (eq-3/occu `WebUI/www/api/methods/interface/`), XML-RPC
|
|
118561
|
+
* `listBidcosInterfaces` on BidCos-RF, JSON-RPC `Interface.getDutyCycle`
|
|
118562
|
+
* (RaspberryMatic / OpenCCU only) for the HmIP access point.
|
|
118563
|
+
*/
|
|
118564
|
+
async listInterfaces() {
|
|
118565
|
+
return this.#requireManagement().listInterfaces();
|
|
118566
|
+
}
|
|
118567
|
+
/**
|
|
118568
|
+
* Open (default 60 s, 1..3600) or close the pairing window of a configured
|
|
118569
|
+
* interface. XML-RPC `setInstallMode`; `mode: 'secure'` (BidCos only) also
|
|
118570
|
+
* teaches the HomeMatic Secure key.
|
|
118571
|
+
*/
|
|
118572
|
+
async setInstallMode(request) {
|
|
118573
|
+
await this.#requireManagement().setInstallMode(request);
|
|
118574
|
+
}
|
|
118575
|
+
/** Seconds left of an interface's pairing window (`null` when the CCU's answer was unusable). XML-RPC `getInstallMode`. */
|
|
118576
|
+
async getInstallMode(interfaceName) {
|
|
118577
|
+
return this.#requireManagement().getInstallMode(interfaceName);
|
|
118578
|
+
}
|
|
118579
|
+
/**
|
|
118580
|
+
* Remove a device from its interface (XML-RPC `deleteDevice`). `flags` is an OR
|
|
118581
|
+
* of {@link HM_DELETE_FLAGS}: `RESET` (0x01, factory reset), `FORCE` (0x02,
|
|
118582
|
+
* remove even when unreachable), `DEFER` (0x04, remove when next reachable);
|
|
118583
|
+
* `0` is a plain removal. The model drops the device once the CCU reports it
|
|
118584
|
+
* through the `deleteDevices` callback. Throws {@link ValidationError} for an
|
|
118585
|
+
* unknown device or invalid flags.
|
|
118586
|
+
*/
|
|
118587
|
+
async deleteDevice(deviceAddress, flags = 0) {
|
|
118588
|
+
this.#requireManagement();
|
|
118589
|
+
const device = this.#devicesByAddress.get(deviceAddress);
|
|
118590
|
+
if (device === void 0) throw new ValidationError(`Unknown device "${deviceAddress}".`);
|
|
118591
|
+
if (!Number.isInteger(flags) || flags < 0 || flags > 7) throw new ValidationError(`Invalid delete flags ${String(flags)} (expected an integer 0..7).`);
|
|
118592
|
+
await this.#central.deleteDevice(device.interfaceId, deviceAddress, flags);
|
|
118593
|
+
}
|
|
118594
|
+
/**
|
|
118595
|
+
* The CCU's active service messages (UNREACH, LOWBAT, CONFIG_PENDING,
|
|
118596
|
+
* STICKY_UNREACH, ...). Needs WebUI credentials. ReGa script over
|
|
118597
|
+
* `ReGa.runScript` (eq-3/occu `WebUI/www/rega/esp/system.fn`, `ID_SERVICES`).
|
|
118598
|
+
*/
|
|
118599
|
+
async serviceMessages() {
|
|
118600
|
+
return this.#requireManagement().serviceMessages();
|
|
118601
|
+
}
|
|
118602
|
+
/** Acknowledge a service message by its `id` (ReGa `AlReceipt`, as the WebUI does). Needs WebUI credentials. */
|
|
118603
|
+
async acknowledgeServiceMessage(id) {
|
|
118604
|
+
await this.#requireManagement().acknowledgeServiceMessage(id);
|
|
118605
|
+
}
|
|
118606
|
+
/**
|
|
118607
|
+
* Version, product (CCU3 / RaspberryMatic / OpenCCU), serial and hostname.
|
|
118608
|
+
* Needs WebUI credentials. JSON-RPC `CCU.getVersion`, `CCU.getSerial`
|
|
118609
|
+
* (eq-3/occu `WebUI/www/api/methods/ccu/`) plus a ReGa read of `/VERSION`.
|
|
118610
|
+
*/
|
|
118611
|
+
async ccuInfo() {
|
|
118612
|
+
return this.#requireManagement().ccuInfo();
|
|
118613
|
+
}
|
|
118614
|
+
/**
|
|
118615
|
+
* The CCU's heating groups ("Gruppen"): HmIP heating groups and HomeMatic
|
|
118616
|
+
* heating groups with their member channels. Writes are separate and
|
|
118617
|
+
* undocumented: {@link Homematic.createGroup}, `updateGroup`, `deleteGroup`.
|
|
118618
|
+
* JSON-RPC `CCU.getHeatingGroupList` (eq-3/occu
|
|
118619
|
+
* `WebUI/www/api/methods/ccu/getheatinggrouplist.tcl`). Needs WebUI credentials.
|
|
118620
|
+
*/
|
|
118621
|
+
async groups() {
|
|
118622
|
+
return this.#requireManagement().groups();
|
|
118623
|
+
}
|
|
118624
|
+
/**
|
|
118625
|
+
* Create a heating group (`type` e.g. `hmip.heating.group`, `members` channel
|
|
118626
|
+
* addresses). Resolves with the group as read back from the CCU; a write the
|
|
118627
|
+
* read-back does not show rejects with `GroupWriteVerificationError`.
|
|
118628
|
+
*
|
|
118629
|
+
* UNDOCUMENTED and firmware-dependent: no CCU API exists for this, so the call
|
|
118630
|
+
* reproduces the WebUI's own HMServer page requests (eq-3/occu
|
|
118631
|
+
* `HMserver/opt/HMServer/pages/GroupEditPage.ftl`, `GroupListPage.ftl`).
|
|
118632
|
+
* Needs WebUI credentials.
|
|
118633
|
+
*/
|
|
118634
|
+
async createGroup(input) {
|
|
118635
|
+
return this.#requireManagement().createGroup(input);
|
|
118636
|
+
}
|
|
118637
|
+
/** Rename a group and/or replace its member list (same caveats as {@link Homematic.createGroup}). */
|
|
118638
|
+
async updateGroup(id, patch) {
|
|
118639
|
+
return this.#requireManagement().updateGroup(id, patch);
|
|
118640
|
+
}
|
|
118641
|
+
/** Delete a group, verified by the read-back (same caveats as {@link Homematic.createGroup}). */
|
|
118642
|
+
async deleteGroup(id) {
|
|
118643
|
+
await this.#requireManagement().deleteGroup(id);
|
|
118644
|
+
}
|
|
118645
|
+
/**
|
|
117847
118646
|
* The MASTER configuration parameters of a channel, with the metadata needed
|
|
117848
118647
|
* to render a form. Empty array when nothing is known for the channel.
|
|
117849
118648
|
*/
|
|
@@ -118231,6 +119030,12 @@ var Homematic = class {
|
|
|
118231
119030
|
if (byName !== void 0) return byName.id;
|
|
118232
119031
|
throw new ValidationError(`Unknown program "${idOrName}".`);
|
|
118233
119032
|
}
|
|
119033
|
+
/** Return the management service (built on first use), or throw before start(). */
|
|
119034
|
+
#requireManagement() {
|
|
119035
|
+
if (!this.#started) throw new ValidationError("Homematic is not started; call start() first.");
|
|
119036
|
+
this.#management ??= this.#central.getManagement();
|
|
119037
|
+
return this.#management;
|
|
119038
|
+
}
|
|
118234
119039
|
/** Return the bound hub fetcher, or throw if none (no credentials). */
|
|
118235
119040
|
#requireHub() {
|
|
118236
119041
|
if (this.#hub === void 0) throw new ValidationError("Hub is unavailable (no WebUI credentials configured).");
|