iobroker.zwave2
Version:
A better Z-Wave integration for ioBroker
1,391 lines • 64.6 kB
JavaScript
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var utils = __toESM(require("@iobroker/adapter-core"));
var import_core = require("@zwave-js/core");
var import_log_transport_json = require("@zwave-js/log-transport-json");
var import_shared = require("@zwave-js/shared");
var import_deferred_promise = require("alcalzone-shared/deferred-promise");
var import_objects = require("alcalzone-shared/objects");
var import_typeguards = require("alcalzone-shared/typeguards");
var import_fs_extra = __toESM(require("fs-extra"));
var import_path = __toESM(require("path"));
var import_zwave_js = require("zwave-js");
var import_Controller = require("zwave-js/Controller");
var import_Utils = require("zwave-js/Utils");
var import_global = require("./lib/global");
var import_objects2 = require("./lib/objects");
var import_serialPorts = require("./lib/serialPorts");
var import_shared2 = require("./lib/shared");
class ZWave2 extends utils.Adapter {
constructor(options = {}) {
super({
...options,
name: "zwave2",
objects: true
});
this.driverReady = false;
this.readyNodes = /* @__PURE__ */ new Set();
this.virtualNodesUpdated = false;
this.initialNodeInterviewStages = /* @__PURE__ */ new Map();
this.onNodeNotification = async (...params) => {
if (params[1] === import_core.CommandClasses.Notification) {
const [node, , args] = params;
this.log.debug(
`Node ${node.id}: received notification: ${args.label} - ${args.eventLabel}`
);
await (0, import_objects2.extendNotification_NotificationCC)(node, args);
}
};
this.pushPayloads = [];
this.pushCallbacks = /* @__PURE__ */ new Map();
this.pushToFrontendBusy = false;
this.on("ready", this.onReady.bind(this));
this.on("objectChange", this.onObjectChange.bind(this));
this.on("stateChange", this.onStateChange.bind(this));
this.on("message", this.onMessage.bind(this));
this.on("unload", this.onUnload.bind(this));
}
async onReady() {
import_global.Global.adapter = this;
const cacheDir = import_path.default.join(
utils.getAbsoluteInstanceDataDir(this),
"cache"
);
if (!!this.config.clearCache) {
await import_fs_extra.default.remove(cacheDir);
this.updateConfig({ clearCache: false });
return;
}
await this.subscribeStatesAsync("*");
this.setState("info.connection", false, true);
this.setState(`info.inclusion`, false, true);
this.setState(`info.exclusion`, false, true);
this.setState("info.healingNetwork", false, true);
if (!this.config.serialport) {
this.log.warn(
"No serial port configured. Please select one in the adapter settings!"
);
return;
}
const timeouts = this.config.driver_increaseTimeouts ? { ack: 2e3 } : void 0;
const attempts = this.config.driver_increaseSendAttempts ? { sendData: 5 } : void 0;
const securityKeys = {};
const S0_Legacy = this.config.networkKey || this.config.networkKey_S0;
if (typeof S0_Legacy === "string" && S0_Legacy.length === 32) {
securityKeys.S0_Legacy = Buffer.from(S0_Legacy, "hex");
}
for (const secClass of [
"S2_AccessControl",
"S2_Authenticated",
"S2_Unauthenticated"
]) {
const key = this.config[`networkKey_${secClass}`];
if (typeof key === "string" && key.length === 32) {
securityKeys[secClass] = Buffer.from(key, "hex");
}
}
try {
this.driver = new import_zwave_js.Driver(this.config.serialport, {
timeouts,
attempts,
logConfig: {
logToFile: !!this.config.writeLogFile
},
storage: {
cacheDir
},
securityKeys,
interview: {
queryAllUserCodes: true
},
enableSoftReset: !this.config.disableSoftReset,
inclusionUserCallbacks: {
validateDSKAndEnterPIN: (dsk) => {
this.validateDSKPromise = (0, import_deferred_promise.createDeferredPromise)();
this.pushToFrontend({
type: "inclusion",
status: {
type: "validateDSK",
dsk
}
});
return this.validateDSKPromise;
},
grantSecurityClasses: (grant) => {
this.grantSecurityClassesPromise = (0, import_deferred_promise.createDeferredPromise)();
this.pushToFrontend({
type: "inclusion",
status: {
type: "grantSecurityClasses",
request: grant
}
});
return this.grantSecurityClassesPromise;
},
abort: () => {
}
}
});
} catch (e) {
if ((0, import_core.isZWaveError)(e) && e.code === import_zwave_js.ZWaveErrorCodes.Driver_InvalidOptions) {
this.log.error(`The adapter options are invalid: ${e.message}`);
} else {
this.log.error(
`Failed create the Z-Wave driver: ${(0, import_shared2.getErrorMessage)(
e,
true
)}`
);
}
return;
}
this.driver.once("driver ready", async () => {
this.driverReady = true;
this.setState("info.connection", true, true);
this.log.info(
`The driver is ready. Found ${this.driver.controller.nodes.size} nodes.`
);
this.driver.controller.on("inclusion started", this.onInclusionStarted.bind(this)).on("exclusion started", this.onExclusionStarted.bind(this)).on("inclusion stopped", this.onInclusionStopped.bind(this)).on("exclusion stopped", this.onExclusionStopped.bind(this)).on("inclusion failed", this.onInclusionFailed.bind(this)).on("exclusion failed", this.onExclusionFailed.bind(this)).on("node added", this.onNodeAdded.bind(this)).on("node removed", this.onNodeRemoved.bind(this)).on(
"heal network progress",
this.onHealNetworkProgress.bind(this)
).on("heal network done", this.onHealNetworkDone.bind(this)).on(
"statistics updated",
this.onControllerStatisticsUpdated.bind(this)
);
await this.setStateAsync(
"info.configVersion",
this.driver.configVersion,
true
);
await this.setStateAsync("info.configUpdate", null, true);
void this.checkForConfigUpdates();
try {
const rfRegion = await this.driver.controller.getRFRegion();
await (0, import_objects2.setRFRegionState)(rfRegion);
} catch {
await (0, import_objects2.setRFRegionState)(void 0);
}
this.initialNodeInterviewStages = new Map(
[...this.driver.controller.nodes.values()].map((node) => [
node.id,
node.interviewStage
])
);
for (const [nodeId, node] of this.driver.controller.nodes) {
await (0, import_objects2.setNodeStatus)(
nodeId,
(0, import_objects2.nodeStatusToStatusState)(node.status)
);
await (0, import_objects2.setNodeReady)(nodeId, node.ready);
this.addNodeEventHandlers(node);
if (node.ready) {
void this.onNodeReady(node);
} else {
await this.extendNodeObjectsAndStates(node);
}
}
const nodeIdRegex = new RegExp(
`^${this.name}\\.${this.instance}\\.Node_(\\d+)`
);
const existingNodeIds = Object.keys(
await import_global.Global.$$(`${this.namespace}.*`, { type: "device" })
).map((id) => {
var _a;
return (_a = id.match(nodeIdRegex)) == null ? void 0 : _a[1];
}).filter((id) => !!id).map((id) => parseInt(id, 10)).filter((id, index, all) => all.indexOf(id) === index);
const unusedNodeIds = existingNodeIds.filter(
(id) => !this.driver.controller.nodes.has(id)
);
for (const nodeId of unusedNodeIds) {
this.log.warn(`Deleting orphaned node ${nodeId}`);
await (0, import_objects2.removeNode)(nodeId);
}
});
this.driver.on("error", this.onZWaveError.bind(this));
this.driver.once("all nodes ready", async () => {
this.log.info("All nodes are ready to use");
await this.updateVirtualNodes();
});
try {
this.driver.enableStatistics({
applicationName: "ioBroker.zwave2",
applicationVersion: require("iobroker.zwave2/package.json").version
});
} catch {
}
this.driver.enableErrorReporting();
try {
await this.driver.start();
} catch (e) {
this.log.error(
`The Z-Wave driver could not be started: ${(0, import_shared2.getErrorMessage)(e)}`
);
}
}
async onInclusionStarted(_secure, strategy) {
this.log.info(
`inclusion started (strategy: ${import_Controller.InclusionStrategy[strategy]})`
);
await this.setStateAsync("info.inclusion", true, true);
}
async onExclusionStarted() {
this.log.info("exclusion started");
await this.setStateAsync("info.exclusion", true, true);
}
async onInclusionStopped() {
this.log.info("inclusion stopped");
await this.setStateAsync("info.inclusion", false, true);
}
async onExclusionStopped() {
this.log.info("exclusion stopped");
await this.setStateAsync("info.exclusion", false, true);
}
async onInclusionFailed() {
this.log.info("inclusion failed");
await this.setStateAsync("info.inclusion", false, true);
}
async onExclusionFailed() {
this.log.info("exclusion failed");
await this.setStateAsync("info.exclusion", false, true);
}
async onNodeAdded(node, result) {
var _a;
this.log.info(`Node ${node.id}: added`);
this.virtualNodesUpdated = false;
this.addNodeEventHandlers(node);
this.pushToFrontend({
type: "inclusion",
status: {
type: "done",
nodeId: node.id,
lowSecurity: !!result.lowSecurity,
securityClass: import_core.SecurityClass[(_a = node.getHighestSecurityClass()) != null ? _a : import_core.SecurityClass.None]
}
});
}
async onNodeRemoved(node, replaced) {
if (replaced) {
this.log.info(`Node ${node.id}: replace started`);
this.readyNodes.delete(node.id);
} else {
this.log.info(`Node ${node.id}: removed`);
this.pushToFrontend({
type: "inclusion",
status: {
type: "exclusionDone",
nodeId: node.id
}
});
}
node.removeAllListeners();
await (0, import_objects2.removeNode)(node.id);
this.virtualNodesUpdated = false;
await this.updateVirtualNodes();
}
async onHealNetworkProgress(progress) {
const allDone = [...progress.values()].every((v) => v !== "pending");
if (allDone)
return;
this.pushToFrontend({
type: "healing",
status: {
type: "progress",
progress: (0, import_shared2.mapToRecord)(progress)
}
});
}
async onHealNetworkDone(result) {
this.pushToFrontend({
type: "healing",
status: {
type: "done",
progress: (0, import_shared2.mapToRecord)(result)
}
});
this.setState("info.healingNetwork", false, true);
}
async onControllerStatisticsUpdated(statistics) {
await (0, import_objects2.setControllerStatistics)(statistics);
}
addNodeEventHandlers(node) {
node.on("ready", this.onNodeReady.bind(this)).on("interview failed", this.onNodeInterviewFailed.bind(this)).on("interview completed", this.onNodeInterviewCompleted.bind(this)).on("wake up", this.onNodeWakeUp.bind(this)).on("sleep", this.onNodeSleep.bind(this)).on("alive", this.onNodeAlive.bind(this)).on("dead", this.onNodeDead.bind(this)).on("value added", this.onNodeValueAdded.bind(this)).on("value updated", this.onNodeValueUpdated.bind(this)).on("value removed", this.onNodeValueRemoved.bind(this)).on("value notification", this.onNodeValueNotification.bind(this)).on("metadata updated", this.onNodeMetadataUpdated.bind(this)).on(
"firmware update progress",
this.onNodeFirmwareUpdateProgress.bind(this)
).on(
"firmware update finished",
this.onNodeFirmwareUpdateFinished.bind(this)
).on("notification", this.onNodeNotification.bind(this)).on("statistics updated", this.onNodeStatisticsUpdated.bind(this));
}
async onNodeReady(node) {
if (this.readyNodes.has(node.id))
return;
this.readyNodes.add(node.id);
this.log.info(`Node ${node.id}: ready to use`);
await (0, import_objects2.setNodeStatus)(
node.id,
node.id === this.driver.controller.ownNodeId ? "alive" : (0, import_objects2.nodeStatusToStatusState)(node.status)
);
await (0, import_objects2.setNodeReady)(node.id, true);
const allValueIDs = node.getDefinedValueIDs();
await this.extendNodeObjectsAndStates(node, allValueIDs);
if (!node.isControllerNode) {
await this.cleanupNodeObjectsAndStates(node, allValueIDs);
}
await this.updateVirtualNodes();
}
async updateVirtualNodes() {
if (this.virtualNodesUpdated)
return;
this.virtualNodesUpdated = true;
this.log.info(`Updating broadcast/multicast node states`);
let node = this.driver.controller.getBroadcastNode();
const allValueIDs = node.getDefinedValueIDs();
await (0, import_objects2.ensureBroadcastNode)();
await this.extendVirtualNodeObjectsAndStates(
node,
import_objects2.DEVICE_ID_BROADCAST,
allValueIDs
);
await this.cleanupVirtualNodeObjects(import_objects2.DEVICE_ID_BROADCAST, allValueIDs);
const multicastNodes = await this.getMulticastNodeDefinitions();
for (const { objId, nodeIds } of multicastNodes) {
node = this.driver.controller.getMulticastGroup(
nodeIds.filter(
(n) => this.driver.controller.nodes.has(n)
)
);
const allValueIDs2 = node.getDefinedValueIDs();
const deviceId = objId.substr(this.namespace.length + 1);
await this.extendVirtualNodeObjectsAndStates(
node,
deviceId,
allValueIDs2
);
await this.cleanupVirtualNodeObjects(deviceId, allValueIDs2);
}
await this.cleanupOrphanedMulticastNodeTrees(
multicastNodes.map((n) => n.objId)
);
}
async getMulticastNodeDefinitions() {
const devices = (await this.getObjectViewAsync("system", "device", {
startkey: `${this.namespace}.Group_`,
endkey: `${this.namespace}.Group_\u9999`
})).rows.map((r) => r.value).filter((o) => !!o);
const ret = [];
for (const d of devices) {
if (!d.native.multicast)
continue;
if (!(0, import_typeguards.isArray)(d.native.nodeIds) || !d.native.nodeIds.length) {
continue;
}
if (!d.native.nodeIds.every(
(n) => typeof n === "number" && n > 0 && n <= import_core.MAX_NODES
)) {
this.log.warn(
`Multicast group object ${d._id} contains invalid node IDs, ignoring it!`
);
continue;
}
const missingNodes = d.native.nodeIds.filter(
(n) => !this.driver.controller.nodes.has(n)
);
if (missingNodes.length) {
this.log.warn(
`Multicast group ${d._id} references missing nodes ${missingNodes.join(", ")}!`
);
}
ret.push({ objId: d._id, nodeIds: d.native.nodeIds });
}
return ret;
}
async cleanupOrphanedMulticastNodeTrees(multicastGroupIds) {
const objectIds = [
...(await this.getObjectViewAsync("system", "channel", {
startkey: `${this.namespace}.Group_`,
endkey: `${this.namespace}.Group_\u9999`
})).rows.map((r) => r.value),
...(await this.getObjectViewAsync("system", "state", {
startkey: `${this.namespace}.Group_`,
endkey: `${this.namespace}.Group_\u9999`
})).rows.map((r) => r.value)
].map((o) => o == null ? void 0 : o._id).filter((id) => !!id);
const orphanedIds = objectIds.filter(
(oid) => !multicastGroupIds.some((gid) => oid.startsWith(gid + "."))
);
for (const id of orphanedIds) {
this.log.debug(`Deleting orphaned multicast object ${id}`);
try {
await this.delObjectAsync(id);
} catch (e) {
}
}
}
async extendNodeObjectsAndStates(node, allValueIDs) {
await (0, import_objects2.extendNode)(node);
if (node.isControllerNode)
return;
allValueIDs != null ? allValueIDs : allValueIDs = node.getDefinedValueIDs();
const uniqueCCs = allValueIDs.map((vid) => [vid.commandClass, vid.commandClassName]).filter(
([cc], index, arr) => arr.findIndex(([_cc]) => _cc === cc) === index
);
for (const [cc, ccName] of uniqueCCs) {
await (0, import_objects2.extendCC)(node, cc, ccName);
}
if (node.interviewStage < import_zwave_js.InterviewStage.Complete || node.interviewStage === import_zwave_js.InterviewStage.Complete && this.initialNodeInterviewStages.get(node.id) === import_zwave_js.InterviewStage.Complete) {
for (const valueId of allValueIDs) {
const value = node.getValue(valueId);
await (0, import_objects2.extendValue)(
node,
{
...valueId,
newValue: value
},
true
);
}
}
}
async extendVirtualNodeObjectsAndStates(node, deviceId, valueIDs) {
const uniqueCCs = valueIDs.map((vid) => [vid.commandClass, vid.commandClassName]).filter(
([cc], index, arr) => arr.findIndex(([_cc]) => _cc === cc) === index
);
for (const [cc, ccName] of uniqueCCs) {
await (0, import_objects2.extendVirtualNodeCC)(node, deviceId, cc, ccName);
}
for (const valueId of valueIDs) {
await (0, import_objects2.extendVirtualMetadata)(node, deviceId, valueId);
}
}
async cleanupNodeObjectsAndStates(node, allValueIDs) {
allValueIDs != null ? allValueIDs : allValueIDs = node.getDefinedValueIDs();
const uniqueCCs = allValueIDs.map((vid) => [vid.commandClass, vid.commandClassName]).filter(
([cc], index, arr) => arr.findIndex(([_cc]) => _cc === cc) === index
);
const nodeAbsoluteId = `${this.namespace}.${(0, import_shared2.computeDeviceId)(node.id)}`;
const desiredChannelIds = new Set(
uniqueCCs.map(
([, ccName]) => `${this.namespace}.${(0, import_objects2.computeChannelId)(node.id, ccName)}`
)
);
const existingChannelIds = Object.keys(
await import_global.Global.$$(`${nodeAbsoluteId}.*`, {
type: "channel"
})
);
const desiredStateIds = new Set(
allValueIDs.map(
(vid) => `${this.namespace}.${(0, import_objects2.computeStateId)(node.id, vid)}`
)
);
const existingStateIds = Object.keys(
await import_global.Global.$$(`${nodeAbsoluteId}.*`, {
type: "state"
})
);
const unusedChannels = existingChannelIds.filter((id) => !desiredChannelIds.has(id)).filter((id) => id.slice(nodeAbsoluteId.length + 1) !== "info");
for (const id of unusedChannels) {
this.log.warn(`Deleting orphaned channel ${id}`);
try {
await this.delObjectAsync(id);
} catch (e) {
}
}
const unusedStates = existingStateIds.filter((id) => !desiredStateIds.has(id)).filter((id) => id.slice(nodeAbsoluteId.length + 1).includes(".")).filter(
(id) => !id.slice(nodeAbsoluteId.length + 1).startsWith("info.")
).filter((id) => {
var _a, _b;
return !((_b = (_a = this.oObjects[id]) == null ? void 0 : _a.native) == null ? void 0 : _b.notificationEvent);
});
for (const id of unusedStates) {
this.log.warn(`Deleting orphaned state ${id}`);
try {
await this.delStateAsync(id);
} catch (e) {
}
try {
await this.delObjectAsync(id);
} catch (e) {
}
}
}
async cleanupVirtualNodeObjects(deviceId, valueIDs) {
const uniqueCCs = valueIDs.map((vid) => [vid.commandClass, vid.commandClassName]).filter(
([cc], index, arr) => arr.findIndex(([_cc]) => _cc === cc) === index
);
const nodeAbsoluteId = `${this.namespace}.${deviceId}`;
const desiredChannelIds = new Set(
uniqueCCs.map(
([, ccName]) => `${this.namespace}.${(0, import_objects2.computeVirtualChannelId)(
deviceId,
ccName
)}`
)
);
const existingChannelIds = Object.keys(
await import_global.Global.$$(`${nodeAbsoluteId}.*`, {
type: "channel"
})
);
const desiredStateIds = new Set(
valueIDs.map(
(vid) => `${this.namespace}.${(0, import_objects2.computeVirtualStateId)(deviceId, vid)}`
)
);
const existingStateIds = Object.keys(
await import_global.Global.$$(`${nodeAbsoluteId}.*`, {
type: "state"
})
);
const unusedChannels = existingChannelIds.filter(
(id) => !desiredChannelIds.has(id)
);
for (const id of unusedChannels) {
this.log.warn(`Deleting orphaned channel ${id}`);
try {
await this.delObjectAsync(id);
} catch (e) {
}
}
const unusedStates = existingStateIds.filter((id) => !desiredStateIds.has(id)).filter((id) => id.slice(nodeAbsoluteId.length + 1).includes(".")).filter((id) => {
var _a, _b;
return !((_b = (_a = this.oObjects[id]) == null ? void 0 : _a.native) == null ? void 0 : _b.notificationEvent);
});
for (const id of unusedStates) {
this.log.warn(`Deleting orphaned virtual state ${id}`);
try {
await this.delObjectAsync(id);
} catch (e) {
}
}
}
async ensureDeviceObject(node) {
const nodeAbsoluteId = `${this.namespace}.${(0, import_shared2.computeDeviceId)(node.id)}`;
if (!this.readyNodes.has(node.id) && !(nodeAbsoluteId in this.oObjects)) {
await (0, import_objects2.extendNode)(node);
}
}
async onNodeInterviewFailed(node, args) {
if (args.isFinal) {
this.log.error(
`Node ${node.id} interview failed: ${args.errorMessage}`
);
} else {
this.log.warn(
`Node ${node.id} interview failed: ${args.errorMessage}`
);
}
}
async onNodeInterviewCompleted(node) {
this.log.info(`Node ${node.id} interview completed`);
}
async onNodeWakeUp(node, oldStatus) {
await (0, import_objects2.setNodeStatus)(node.id, "awake");
this.log.info(
`Node ${node.id} is ${oldStatus === import_zwave_js.NodeStatus.Unknown ? "" : "now "}awake`
);
}
async onNodeSleep(node, oldStatus) {
await (0, import_objects2.setNodeStatus)(node.id, "asleep");
this.log.info(
`Node ${node.id} is ${oldStatus === import_zwave_js.NodeStatus.Unknown ? "" : "now "}asleep`
);
await this.ensureDeviceObject(node);
}
async onNodeAlive(node, oldStatus) {
await (0, import_objects2.setNodeStatus)(node.id, "alive");
if (oldStatus === import_zwave_js.NodeStatus.Dead) {
this.log.info(`Node ${node.id}: has returned from the dead`);
} else {
this.log.info(`Node ${node.id} is alive`);
}
}
async onNodeDead(node, oldStatus) {
await (0, import_objects2.setNodeStatus)(node.id, "dead");
this.log.info(
`Node ${node.id} is ${oldStatus === import_zwave_js.NodeStatus.Unknown ? "" : "now "}dead`
);
await this.ensureDeviceObject(node);
}
async onNodeValueAdded(node, args) {
let propertyName = (0, import_objects2.computeStateId)(node.id, args);
propertyName = propertyName.substr(propertyName.lastIndexOf(".") + 1);
this.log.debug(
`Node ${node.id}: value added: ${propertyName} => ${String(
args.newValue
)}`
);
await (0, import_objects2.extendValue)(node, args);
if (this.config.switchCompat)
await this.syncSwitchStates(node, args);
}
async onNodeValueUpdated(node, args) {
let propertyName = (0, import_objects2.computeStateId)(node.id, args);
propertyName = propertyName.substr(propertyName.lastIndexOf(".") + 1);
this.log.debug(
`Node ${node.id}: value updated: ${propertyName} => ${String(
args.newValue
)}`
);
await (0, import_objects2.extendValue)(node, args);
if (this.config.switchCompat)
await this.syncSwitchStates(node, args);
}
async onNodeValueNotification(node, args) {
let propertyName = (0, import_objects2.computeStateId)(node.id, args);
propertyName = propertyName.substr(propertyName.lastIndexOf(".") + 1);
this.log.debug(
`Node ${node.id}: value notification: ${propertyName} = ${String(
args.value
)}`
);
await (0, import_objects2.extendNotificationValue)(node, args);
}
async syncSwitchStates(node, args) {
if ((args.commandClass === import_core.CommandClasses["Binary Switch"] || args.commandClass === import_core.CommandClasses["Multilevel Switch"]) && args.property === "currentValue") {
await (0, import_objects2.extendValue)(node, {
...args,
property: "targetValue",
propertyName: "targetValue"
});
}
}
async onNodeValueRemoved(node, args) {
let propertyName = (0, import_objects2.computeStateId)(node.id, args);
propertyName = propertyName.substr(propertyName.lastIndexOf(".") + 1);
this.log.debug(`Node ${node.id}: value removed: ${propertyName}`);
await (0, import_objects2.removeValue)(node.id, args);
}
async onNodeMetadataUpdated(node, args) {
let propertyName = (0, import_objects2.computeStateId)(node.id, args);
propertyName = propertyName.substr(propertyName.lastIndexOf(".") + 1);
this.log.debug(`Node ${node.id}: metadata updated: ${propertyName}`);
await (0, import_objects2.extendMetadata)(node, args);
}
async onNodeFirmwareUpdateProgress(node, sentFragments, totalFragments) {
this.pushToFrontend({
type: "firmwareUpdate",
progress: {
type: "progress",
sentFragments,
totalFragments
}
});
}
async onNodeFirmwareUpdateFinished(node, status, waitTime) {
this.pushToFrontend({
type: "firmwareUpdate",
progress: {
type: "done",
status,
waitTime
}
});
}
async onNodeStatisticsUpdated(node, statistics) {
await (0, import_objects2.setNodeStatistics)(node.id, statistics);
}
async checkForConfigUpdates() {
var _a, _b;
if (!((_a = await this.getStateAsync("info.configUpdate")) == null ? void 0 : _a.val)) {
try {
await this.setStateChangedAsync(
"info.configUpdate",
(_b = await this.driver.checkForConfigUpdates()) != null ? _b : null,
true
);
} catch (e) {
await this.setStateChangedAsync(
"info.configUpdate",
null,
true
);
this.log.error(
`Failed to check for config updates: ${(0, import_shared2.getErrorMessage)(e)}`
);
}
}
const hour = new Date().getUTCHours();
let timeoutHours = 5 - hour;
if (timeoutHours <= 0)
timeoutHours += 24;
this.configUpdateTimeout = setTimeout(
() => this.checkForConfigUpdates(),
timeoutHours * 3600 * 1e3
);
}
async onUnload(callback) {
try {
this.log.info("Shutting down driver...");
const allNodeIds = [...this.driver.controller.nodes.keys()];
await this.driver.destroy();
this.log.info("Resetting node status...");
for (const nodeId of allNodeIds) {
await (0, import_objects2.setNodeStatus)(nodeId, "unknown");
await (0, import_objects2.setNodeReady)(nodeId, false);
await (0, import_objects2.setNodeStatistics)(nodeId, null);
}
if (this.configUpdateTimeout)
clearTimeout(this.configUpdateTimeout);
if (this.pushPayloadExpirationTimeout)
clearTimeout(this.pushPayloadExpirationTimeout);
await this.setStateAsync("info.configUpdating", false, true);
await (0, import_objects2.setControllerStatistics)(null);
this.log.info("Cleaned everything up!");
callback();
} catch (e) {
callback();
}
}
async onZWaveError(error) {
let level = "error";
if (error instanceof import_zwave_js.ZWaveError && error.code === import_zwave_js.ZWaveErrorCodes.Controller_NodeInsecureCommunication) {
level = "warn";
}
this.log[level](error.message);
if (error instanceof import_zwave_js.ZWaveError && error.code === import_zwave_js.ZWaveErrorCodes.Driver_Failed) {
this.log.error(`Restarting the adapter in a second...`);
setTimeout(() => {
this.terminate(utils.EXIT_CODES.START_IMMEDIATELY_AFTER_STOP);
}, 1e3);
}
}
async onObjectChange(id, _obj) {
const prefix = this.namespace + ".Group_";
if (id.startsWith(prefix) && id.indexOf(".", prefix.length) === -1) {
this.virtualNodesUpdated = false;
await this.updateVirtualNodes();
}
}
async onStateChange(id, state) {
if (state) {
if (!state.ack) {
if (!this.driverReady) {
this.log.warn(
`The driver is not yet ready, ignoring state change for "${id}"`
);
return;
}
if (id.endsWith("info.exclusion")) {
await this.setExclusionMode(state.val);
return;
} else if (id.startsWith(`${this.namespace}.info.`)) {
return;
}
const obj = this.oObjects[id];
if (!obj) {
this.log.error(
`Object definition for state ${id} is missing!`
);
return;
}
const { native } = obj;
const valueId = native.valueId;
if (!(valueId && typeof valueId.commandClass === "number" && (typeof valueId.property === "number" || typeof valueId.property === "string"))) {
this.log.error(
`Value ID missing or incomplete in object definition ${id}!`
);
return;
}
let node;
if (!!native.broadcast) {
node = this.driver.controller.getBroadcastNode();
} else if ((0, import_typeguards.isArray)(native.nodeIds)) {
node = this.driver.controller.getMulticastGroup(
native.nodeIds.filter(
(n) => this.driver.controller.nodes.has(n)
)
);
} else {
const nodeId = native.nodeId;
if (!nodeId) {
this.log.error(
`Node ID missing from object definition ${id}!`
);
return;
}
try {
node = this.driver.controller.nodes.getOrThrow(nodeId);
} catch {
this.log.error(`Node ${nodeId} does not exist!`);
return;
}
}
let newValue = state.val;
if (typeof newValue === "string" && (0, import_shared2.isBufferAsHex)(newValue)) {
newValue = (0, import_shared2.bufferFromHex)(newValue);
}
try {
await node.setValue(valueId, newValue);
await this.setStateAsync(id, { val: state.val, ack: true });
} catch (e) {
this.log.error((0, import_shared2.getErrorMessage)(e));
}
}
}
}
async setExclusionMode(active) {
try {
if (active) {
await this.driver.controller.beginExclusion({
strategy: import_Controller.ExclusionStrategy.DisableProvisioningEntry
});
} else {
await this.driver.controller.stopExclusion();
}
} catch (e) {
this.log.error((0, import_shared2.getErrorMessage)(e));
}
}
pushToFrontend(payload) {
this.pushPayloads.push(payload);
if (this.pushToFrontendBusy)
return;
this.pushToFrontendBusy = true;
if (this.pushCallbacks.size > 0) {
const payloads = this.pushPayloads.splice(
0,
this.pushPayloads.length
);
this.pushCallbacks.forEach((cb) => cb(payloads));
this.pushCallbacks.clear();
} else {
if (!this.pushPayloadExpirationTimeout) {
this.pushPayloadExpirationTimeout = setTimeout(() => {
this.pushPayloads.splice(0, this.pushPayloads.length);
}, 2500);
}
}
this.pushToFrontendBusy = false;
}
async onMessage(obj) {
var _a, _b, _c, _d;
const respond = (response) => {
if (obj.callback)
this.sendTo(obj.from, obj.command, response, obj.callback);
};
const responses = {
ACK: { error: null },
OK: { error: null, result: "ok" },
ERROR_UNKNOWN_COMMAND: { error: "Unknown command!" },
MISSING_PARAMETER: (paramName) => {
return { error: 'missing parameter "' + paramName + '"!' };
},
COMMAND_ACTIVE: { error: "command already active" },
RESULT: (result) => ({ error: null, result }),
ERROR: (error) => ({ error })
};
function requireParams(...params) {
if (!params.length)
return true;
for (const param of params) {
if (!(obj.message && obj.message.hasOwnProperty(param))) {
respond(responses.MISSING_PARAMETER(param));
return false;
}
}
return true;
}
if (obj) {
switch (obj.command) {
case "getNetworkMap": {
if (!this.driverReady) {
return respond(
responses.ERROR(
"The driver is not yet ready to show the network map!"
)
);
}
const tasks = [
...this.driver.controller.nodes.values()
].map(async (node) => ({
id: node.id,
name: `Node ${node.id}`,
neighbors: await this.driver.controller.getNodeNeighbors(
node.id
)
}));
const map = await Promise.all(tasks);
respond(responses.RESULT(map));
return;
}
case "getSerialPorts": {
const ports = await (0, import_serialPorts.enumerateSerialPorts)(this);
respond(responses.RESULT(ports));
return;
}
case "registerPushCallback": {
if (!requireParams("uuid"))
return;
const params = obj.message;
const clearPending = !!params.clearPending;
if (clearPending) {
this.pushPayloads.splice(0, this.pushPayloads.length);
}
if (this.pushPayloads.length) {
respond(responses.RESULT(this.pushPayloads));
this.pushPayloads.splice(0, this.pushPayloads.length);
if (this.pushPayloadExpirationTimeout)
clearTimeout(this.pushPayloadExpirationTimeout);
} else {
this.pushCallbacks.set(
params.uuid,
(result) => respond(responses.RESULT(result))
);
}
return;
}
case "supportsSmartStart": {
if (!this.driverReady) {
return respond(
responses.ERROR(
"The driver is not yet ready to answer that!"
)
);
}
const supportsSmartStart = !!this.driver.controller.supportsFeature(
import_Controller.ZWaveFeature.SmartStart
);
return respond(responses.RESULT(supportsSmartStart));
}
case "scanQRCode": {
if (!this.driverReady) {
return respond(
responses.ERROR(
"The driver is not yet ready to do that!"
)
);
}
if (!requireParams("code"))
return;
const params = obj.message;
const code = params.code;
const include = !!params.include;
try {
const provisioning = (0, import_core.parseQRCodeString)(code);
const node = this.driver.controller.getNodeByDSK(
provisioning.dsk
);
const supportsSmartStart = !!this.driver.controller.supportsFeature(
import_Controller.ZWaveFeature.SmartStart
);
if (include && node) {
return respond(
responses.RESULT({
type: "included",
nodeId: node.id
})
);
} else if (supportsSmartStart && this.driver.controller.getProvisioningEntry(
provisioning.dsk
)) {
return respond(
responses.RESULT({
type: "provisioned",
...provisioning
})
);
} else if (!supportsSmartStart || provisioning.version === import_core.QRCodeVersion.S2) {
if (!include) {
return respond(
responses.RESULT({ type: "S2" })
);
}
try {
const result = await this.driver.controller.beginInclusion(
{
strategy: import_Controller.InclusionStrategy.Security_S2,
provisioning
}
);
this.setState("info.inclusion", true, true);
if (result) {
return respond(
responses.RESULT({ type: "S2" })
);
} else {
respond(responses.COMMAND_ACTIVE);
}
} catch (e) {
respond(responses.ERROR((0, import_shared2.getErrorMessage)(e)));
this.setState("info.inclusion", false, true);
}
} else if (provisioning.version === import_core.QRCodeVersion.SmartStart) {
if (!include) {
return respond(
responses.RESULT({
type: "SmartStart",
...provisioning
})
);
}
this.driver.controller.provisionSmartStartNode(
provisioning
);
return respond(
responses.RESULT({
type: "SmartStart",
...provisioning
})
);
}
} catch {
}
return respond(responses.RESULT({ type: "none" }));
}
case "provisionSmartStartNode": {
if (!this.driverReady) {
return respond(
responses.ERROR(
"The driver is not yet ready to do that!"
)
);
}
if (!requireParams("dsk", "securityClasses"))
return;
const params = obj.message;
const status = params.status;
const dsk = params.dsk;
const securityClasses = params.securityClasses;
const additionalInfo = (_a = params.additionalInfo) != null ? _a : {};
if ("status" in additionalInfo)
delete additionalInfo.status;
if ("dsk" in additionalInfo)
delete additionalInfo.dsk;
if ("securityClasses" in additionalInfo)
delete additionalInfo.securityClasses;
try {
this.driver.controller.provisionSmartStartNode({
status,
dsk,
securityClasses,
...additionalInfo
});
respond(responses.OK);
} catch (e) {
respond(responses.ERROR((0, import_shared2.getErrorMessage)(e)));
}
return;
}
case "unprovisionSmartStartNode": {
if (!this.driverReady) {
return respond(
responses.ERROR(
"The driver is not yet ready to do that!"
)
);
}
if (!requireParams("dsk"))
return;
const params = obj.message;
const dsk = params.dsk;
try {
this.driver.controller.unprovisionSmartStartNode(dsk);
respond(responses.OK);
} catch (e) {
respond(responses.ERROR((0, import_shared2.getErrorMessage)(e)));
}
return;
}
case "getProvisioningEntries": {
if (!this.driverReady) {
return respond(
responses.ERROR(
"The driver is not yet ready to do that!"
)
);
}
const result = this.driver.controller.getProvisioningEntries();
for (const entry of result) {
if (typeof entry.manufacturerId === "number" && typeof entry.productType === "number" && typeof entry.productId === "number" && typeof entry.applicationVersion === "string") {
const device = await this.driver.configManager.lookupDevice(
entry.manufacturerId,
entry.productType,
entry.productId,
entry.applicationVersion
);
if (device) {
entry.manufacturer = device.manufacturer;
entry.label = device.label;
entry.description = device.description;
}
}
}
return respond(responses.RESULT(result));
}
case "beginInclusion": {
if (!this.driverReady) {
return respond(
responses.ERROR(
"The driver is not yet ready to include devices!"
)
);
}
if (!requireParams("strategy"))
return;
const params = obj.message;
const strategy = params.strategy;
const forceSecurity = !!params.forceSecurity;
this.validateDSKPromise = void 0;
this.grantSecurityClassesPromise = void 0;
try {
const result = await this.driver.controller.beginInclusion({
strategy,
forceSecurity
});
this.setState("info.inclusion", true, true);
if (result) {
respond(responses.OK);
} else {
respond(responses.COMMAND_ACTIVE);
}
} catch (e) {
respond(responses.ERROR((0, import_shared2.getErrorMessage)(e)));
this.setState("info.inclusion", false, true);
}
return;
}
case "validateDSK": {
if (!this.driverReady) {
return respond(
responses.ERROR(
"The driver is not yet ready to include devices!"
)
);
}
if (!requireParams("pin"))
return;
const params = obj.message;
const pin = params.pin;
if (!pin) {
(_b = this.validateDSKPromise) == null ? void 0 : _b.resolve(false);
} else {
(_c = this.validateDSKPromise) == null ? void 0 : _c.resolve(pin);
}
this.pushToFrontend({
type: "inclusion",
status: { type: "busy" }
});
respond(responses.ACK);
return;
}
case "grantSecurityClasses": {
if (!this.driverReady) {
return respond(
responses.ERROR(
"The driver is not yet ready to include devices!"
)
);
}
if (!requireParams("grant"))
return;
const params = obj.message;
const grant = params.grant;
(_d = this.grantSecurityClassesPromise) == null ? void 0 : _d.resolve(grant);
this.pushToFrontend({
type: "inclusion",
status: { type: "busy" }
});
respond(responses.ACK);
return;
}
case "stopInclusion": {
if (!this.driverReady) {
return respond(
responses.ERROR(
"The driver is not yet ready to include devices!"
)
);
}
const result = await this.driver.controller.stopInclusion();
this.setState("info.inclusion", false, true);
if (result) {
respond(responses.OK);
} else {
respond(responses.COMMAND_ACTIVE);
}
return;
}
case "beginHealingNetwork": {
if (!this.driverReady) {
return respond(
responses.ERROR(
"The driver is not yet ready to heal the network!"
)
);
}
const result = this.driver.controller.beginHealingNetwork();
if (result) {
respond(responses.OK);
this.setState("info.healingNetwork", true, true);
} else {
respond(responses.COMMAND_ACTIVE);
}
return;
}
case "stopHealingNetwork": {
if (!this.driverReady) {
return respond(
responses.ERROR(
"The driver is not yet ready to heal the network!"
)
);
}
this.driver.controller.stopHealingNetwork();
respond(responses.OK);
this.setState("info.healingNetwork", false, true);
return;
}
case "softReset": {
if (!this.driverReady) {
return respond(
responses.ERROR("The driver is not yet ready!")
);
}
try {
await this.driver.softReset();
respond(responses.OK);
} catch (e) {
respond(responses.ERROR((0, import_shared2.getErrorMessage)(e)));
}
return;
}
case "hardReset": {
if (!this.driverReady) {
return respond(
responses.ERROR("The driver is not yet ready!")
);
}
try {
await this.driver.hardReset();
respond(responses.OK);
this.restart();
} catch (e) {
respond(responses.ERROR((0, import_shared2.getErrorMessage)(e)));
}
return;
}
case "clearCache": {
this.updateConfig({ clearCache: true });
respond(responses.OK);
return;
}
case "removeFailedNode": {
if (!this.driverReady) {
return respond(
responses.ERROR(
"The driver is not yet ready to do that!"
)
);
}
if (!requireParams("nodeId"))
return;
const params = obj.message;
try {
await this.driver.controller.removeFailedNode(
params.nodeId
);
} catch (e) {
return respond(
responses.ERROR(
`Could not remove node ${params.nodeId}: ${(0, import_shared2.getErrorMessage)(e)}`
)
);
}
return respond(responses.OK);
}
case "replaceFailedNode": {
if (!this.driverReady) {
return respond(
responses.ERROR(
"The driver is not yet ready to replace devices!"
)
);
}
if (!requireParams("nodeId", "strategy"))
return;
const params = obj.message;
const strategy = params.strategy;
this.validateDSKPromise = void 0;
this.grantSecurityClassesPromise = void 0;
const userCallbacks = {
validateDSKAndEnterPIN: (dsk) => {
this.validateDSKPromise = (0, import_deferred_promise.createDeferredPromise)();
this.pushToFrontend({
type: "inclusion",
status: {
type: "validateDSK",
dsk
}
});
return this.validateDSKPromise;
},
grantSecurityClasses: (grant) => {
this.grantSecurityClassesPromise = (0, import_deferred_promise.createDeferredPromise)();
this.pushToFrontend({
type: "inclusion",
status: {
type: "grantSecurityClasses",
request: grant
}
});
return this.grantSecurityClassesPromise;
},
abort: () => {
}
};
try {
const result = await this.driver.controller.replaceFailedNode(
params.nodeId,
{
strategy,
userCallbacks
}
);
this.setState("info.inclusion", true, true);
if (result) {
respond(responses.OK);
} else {
respond(responses.COMMAND_ACTIVE);
}
} catch (e) {
respond(responses.ERROR((0, import_shared2.getErrorMessage)(e)));
this.setState("info.inclusion", false, true);
}
return;
}
case "setRFRegion": {
if (!this.driverReady) {
return respond(
responses.ERROR(
"The driver is not yet ready to do that!"
)
);
}
if (!requireParams("region"))
return;
const params = obj.message;
try {
await this.driver.controller.setRFRegion(params.region);
await (0, import_objects2.setRFRegionState)(params.region);
} catch (e) {
return respond(
responses.ERROR(
`Could not