zwave-js-ui
Version:
Z-Wave Control Panel and MQTT Gateway
1,324 lines • 181 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.EventSource = exports.ZwaveClientStatus = exports.ZUIScheduleEntryLockMode = exports.DriverNotReadyError = exports.allowedApis = exports.configManager = exports.deviceConfigPriorityDir = void 0;
// eslint-disable-next-line one-var
const core_1 = require("@zwave-js/core");
const node_1 = require("@zwave-js/core/bindings/log/node");
const log_transport_json_1 = require("@zwave-js/log-transport-json");
const utils_1 = require("./utils");
const zwave_js_1 = require("zwave-js");
const Utils_1 = require("zwave-js/Utils");
const app_1 = require("../config/app");
const store_1 = __importDefault(require("../config/store"));
const jsonStore_1 = __importDefault(require("./jsonStore"));
const LogManager = __importStar(require("./logger"));
const utils = __importStar(require("./utils"));
const server_1 = require("@zwave-js/server");
const fs_extra_1 = require("fs-extra");
const EventEmitter_1 = require("./EventEmitter");
const config_1 = require("@zwave-js/config");
const promises_1 = require("fs/promises");
const BackupManager_1 = __importStar(require("./BackupManager"));
const SocketEvents_1 = require("./SocketEvents");
const types_1 = require("util/types");
const PkgFsBindings_1 = require("./PkgFsBindings");
const path_1 = require("path");
exports.deviceConfigPriorityDir = (0, path_1.join)(app_1.storeDir, 'config');
exports.configManager = new config_1.ConfigManager({
deviceConfigPriorityDir: exports.deviceConfigPriorityDir,
});
const logger = LogManager.module('Z-Wave');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const loglevels = require('triple-beam').configs.npm.levels;
const NEIGHBORS_LOCK_REFRESH = 60 * 1000;
function validateMethods(methods) {
return methods;
}
// ZwaveClient Apis that can be called with MQTT apis
exports.allowedApis = validateMethods([
'setNodeName',
'setNodeLocation',
'setNodeDefaultSetValueOptions',
'_createScene',
'_removeScene',
'_setScenes',
'_getScenes',
'_sceneGetValues',
'_addSceneValue',
'_removeSceneValue',
'_activateScene',
'refreshNeighbors',
'getNodeNeighbors',
'discoverNodeNeighbors',
'getAssociations',
'checkAssociation',
'addAssociations',
'removeAssociations',
'removeAllAssociations',
'removeNodeFromAllAssociations',
'getNodes',
'getInfo',
'refreshValues',
'refreshCCValues',
'pollValue',
'setPowerlevel',
'setRFRegion',
'setMaxLRPowerLevel',
'updateControllerNodeProps',
'startInclusion',
'startExclusion',
'stopInclusion',
'stopExclusion',
'replaceFailedNode',
'hardReset',
'softReset',
'rebuildNodeRoutes',
'getPriorityRoute',
'setPriorityRoute',
'assignReturnRoutes',
'getPriorityReturnRoute',
'getPrioritySUCReturnRoute',
'getCustomReturnRoute',
'getCustomSUCReturnRoute',
'assignPriorityReturnRoute',
'assignPrioritySUCReturnRoute',
'assignCustomReturnRoutes',
'assignCustomSUCReturnRoutes',
'deleteReturnRoutes',
'deleteSUCReturnRoutes',
'removePriorityRoute',
'beginRebuildingRoutes',
'stopRebuildingRoutes',
'isFailedNode',
'removeFailedNode',
'refreshInfo',
'updateFirmware',
'firmwareUpdateOTW',
'abortFirmwareUpdate',
'dumpNode',
'getAvailableFirmwareUpdates',
'firmwareUpdateOTA',
'sendCommand',
'writeValue',
'writeBroadcast',
'writeMulticast',
'driverFunction',
'checkForConfigUpdates',
'installConfigUpdate',
'shutdownZwaveAPI',
'pingNode',
'restart',
'grantSecurityClasses',
'validateDSK',
'abortInclusion',
'backupNVMRaw',
'restoreNVM',
'getProvisioningEntries',
'getProvisioningEntry',
'unprovisionSmartStartNode',
'provisionSmartStartNode',
'parseQRCodeString',
'checkLifelineHealth',
'abortHealthCheck',
'checkRouteHealth',
'checkLinkReliability',
'abortLinkReliabilityCheck',
'syncNodeDateAndTime',
'manuallyIdleNotificationValue',
'getSchedules',
'cancelGetSchedule',
'setSchedule',
'setEnabledSchedule',
]);
// Define CommandClasses and properties that should be observed
const observedCCProps = {
[core_1.CommandClasses.Battery]: {
level(node, value) {
const levels = node.batteryLevels || {};
levels[value.endpoint] = value.value;
node.batteryLevels = levels;
node.minBatteryLevel = Math.min(...Object.values(levels));
this.emitNodeUpdate(node, {
batteryLevels: levels,
minBatteryLevel: node.minBatteryLevel,
});
},
},
[core_1.CommandClasses['User Code']]: {
userIdStatus(node, value) {
const userId = value.propertyKey;
const status = value.value;
if (!node.userCodes) {
return;
}
if (status === undefined ||
status === zwave_js_1.UserIDStatus.Available ||
status === zwave_js_1.UserIDStatus.StatusNotAvailable) {
node.userCodes.available = node.userCodes.available.filter((id) => id !== userId);
}
else {
node.userCodes.available.push(userId);
}
if (status === zwave_js_1.UserIDStatus.Enabled) {
node.userCodes.enabled.push(userId);
}
else {
node.userCodes.enabled = node.userCodes.enabled.filter((id) => id !== userId);
}
this.emitNodeUpdate(node, {
userCodes: node.userCodes,
});
},
},
[core_1.CommandClasses['Node Naming and Location']]: {
name(node, value) {
this.setNodeName(node.id, value.value).catch((error) => {
logger.error(`Error while setting node name: ${error.message}`);
});
},
location(node, value) {
this.setNodeLocation(node.id, value.value).catch((error) => {
logger.error(`Error while setting node location: ${error.message}`);
});
},
},
};
const ZWAVEJS_LOG_FILE = utils.joinPath(app_1.logsDir, 'zwavejs_%DATE%.log');
class DriverNotReadyError extends Error {
constructor() {
super('Driver is not ready');
// We need to set the prototype explicitly
Object.setPrototypeOf(this, DriverNotReadyError.prototype);
Object.getPrototypeOf(this).name = 'DriverNotReadyError';
}
}
exports.DriverNotReadyError = DriverNotReadyError;
var ZUIScheduleEntryLockMode;
(function (ZUIScheduleEntryLockMode) {
ZUIScheduleEntryLockMode["DAILY"] = "daily";
ZUIScheduleEntryLockMode["WEEKLY"] = "weekly";
ZUIScheduleEntryLockMode["YEARLY"] = "yearly";
})(ZUIScheduleEntryLockMode || (exports.ZUIScheduleEntryLockMode = ZUIScheduleEntryLockMode = {}));
var ZwaveClientStatus;
(function (ZwaveClientStatus) {
ZwaveClientStatus["CONNECTED"] = "connected";
ZwaveClientStatus["BOOTLOADER_READY"] = "bootloader ready";
ZwaveClientStatus["DRIVER_READY"] = "driver ready";
ZwaveClientStatus["SCAN_DONE"] = "scan done";
ZwaveClientStatus["DRIVER_FAILED"] = "driver failed";
ZwaveClientStatus["CLOSED"] = "closed";
})(ZwaveClientStatus || (exports.ZwaveClientStatus = ZwaveClientStatus = {}));
var EventSource;
(function (EventSource) {
EventSource["DRIVER"] = "driver";
EventSource["CONTROLLER"] = "controller";
EventSource["NODE"] = "node";
})(EventSource || (exports.EventSource = EventSource = {}));
class ZwaveClient extends EventEmitter_1.TypedEventEmitter {
cfg;
socket;
closed;
destroyed = false;
_driverReady;
scenes;
_nodes;
storeNodes;
_devices;
driverInfo;
status;
// used to store node info before inclusion like name and location
tmpNode;
// tells if a node replacement is in progress
isReplacing = false;
hasUserCallbacks = false;
_error;
_scanComplete;
_cntStatus;
lastUpdate;
_driver;
server;
statelessTimeouts;
commandsTimeout;
healTimeout;
updatesCheckTimeout;
pollIntervals;
_lockNeighborsRefresh;
_lockGetSchedule;
_cancelGetSchedule;
nvmEvent;
backoffRetry = 0;
restartTimeout;
driverFunctionCache = [];
// Foreach valueId, we store a callback function to be called when the value changes
valuesObservers = {};
_grantResolve;
_dskResolve;
throttledFunctions = new Map();
inclusionUserCallbacks = {
grantSecurityClasses: this._onGrantSecurityClasses.bind(this),
validateDSKAndEnterPIN: this._onValidateDSK.bind(this),
abort: this._onAbortInclusion.bind(this),
};
_inclusionState = undefined;
get driverReady() {
return this.driver && this._driverReady && !this.closed;
}
set driverReady(ready) {
if (this._driverReady !== ready) {
this._driverReady = ready;
this.emit('driverStatus', ready);
}
}
get cntStatus() {
return this._cntStatus;
}
get scanComplete() {
return this._scanComplete;
}
get error() {
return this._error;
}
get driver() {
return this._driver;
}
get nodes() {
return this._nodes;
}
get devices() {
return this._devices;
}
get maxNodeEventsQueueSize() {
return this.cfg.maxNodeEventsQueueSize || 100;
}
get cacheSnippets() {
return this.driverFunctionCache;
}
constructor(config, socket) {
super();
this.cfg = config;
this.socket = socket;
this.init();
}
get homeHex() {
return this.driverInfo.name;
}
/**
* Init internal vars
*/
init() {
this.statelessTimeouts = {};
this.pollIntervals = {};
this._lockNeighborsRefresh = false;
this.closed = false;
this.driverReady = false;
this.scenes = jsonStore_1.default.get(store_1.default.scenes);
this._nodes = new Map();
this._devices = {};
this.driverInfo = {};
this.healTimeout = null;
this.status = ZwaveClientStatus.CLOSED;
}
/**
* Restart client connection
*
*/
async restart() {
await this.close(true);
this.init();
await this.connect();
}
backoffRestart() {
// fix edge case where client is half closed and restart is called
if (this.checkIfDestroyed()) {
return;
}
const timeout = Math.min(2 ** this.backoffRetry * 1000, 15000);
this.backoffRetry++;
logger.info(`Restarting client in ${timeout / 1000} seconds, retry ${this.backoffRetry}`);
this.restartTimeout = setTimeout(() => {
this.restart().catch((error) => {
logger.error(`Error while restarting driver: ${error.message}`);
});
}, timeout);
}
/**
* Checks if this client is destroyed and if so closes it
* @returns True if client is destroyed
*/
checkIfDestroyed() {
if (this.destroyed) {
logger.debug(`Client listening on '${this.cfg.port}' is destroyed, closing`);
this.close(true).catch((error) => {
logger.error(`Error while closing driver: ${error.message}`);
});
return true;
}
return false;
}
/**
* Used to schedule next network rebuildNodeRoutes at hours: cfg.healHours
*/
// scheduleHeal() {
// if (!this.cfg.healNetwork) {
// return
// }
// const now = new Date()
// let start: Date
// const hour = this.cfg.healHour
// if (now.getHours() < hour) {
// start = new Date(
// now.getFullYear(),
// now.getMonth(),
// now.getDate(),
// hour,
// 0,
// 0,
// 0
// )
// } else {
// start = new Date(
// now.getFullYear(),
// now.getMonth(),
// now.getDate() + 1,
// hour,
// 0,
// 0,
// 0
// )
// }
// const wait = start.getTime() - now.getTime()
// if (wait < 0) {
// this.scheduleHeal()
// } else {
// this.healTimeout = setTimeout(() => {
// this.rebuildNodeRoutes()
// }, wait)
// }
// }
/**
* Call `fn` function at most once every `wait` milliseconds
* */
throttle(key, fn, wait) {
const entry = this.throttledFunctions.get(key);
const now = Date.now();
// first time it's called or wait is already passed since last call
if (!entry || entry.lastUpdate + wait < now) {
this.throttledFunctions.set(key, {
lastUpdate: now,
fn,
timeout: null,
});
fn();
}
else {
// if it's called again and no timeout is set, set a timeout to call function
if (!entry.timeout) {
entry.timeout = setTimeout(() => {
const oldEntry = this.throttledFunctions.get(key);
if (oldEntry?.fn) {
oldEntry.lastUpdate = Date.now();
fn();
}
}, entry.lastUpdate + wait - now);
}
// discard the old function and store the new one
entry.fn = fn;
}
}
clearThrottle(key) {
const entry = this.throttledFunctions.get(key);
if (entry) {
if (entry.timeout) {
clearTimeout(entry.timeout);
}
this.throttledFunctions.delete(key);
}
}
/**
* Returns the driver ZWaveNode object
*/
getNode(nodeId) {
return this._driver.controller.nodes.get(nodeId);
}
setUserCallbacks() {
this.hasUserCallbacks = true;
if (!this._driver || !this.cfg.serverEnabled) {
return;
}
logger.info('Setting user callbacks');
this.driver.updateOptions({
inclusionUserCallbacks: {
...this.inclusionUserCallbacks,
},
});
}
removeUserCallbacks() {
this.hasUserCallbacks = false;
if (!this._driver || !this.cfg.serverEnabled) {
return;
}
logger.info('Removing user callbacks');
this.driver.updateOptions({
inclusionUserCallbacks: undefined,
});
// when no user is connected, give back the control to HA server
if (this.server?.['sockets'] !== undefined) {
this.server.setInclusionUserCallbacks();
}
}
/**
* Returns the driver ZWaveNode ValueId object or null
*/
getZwaveValue(idString) {
if (!idString || typeof idString !== 'string') {
return null;
}
const parts = idString.split('-');
if (parts.length < 3) {
return null;
}
return {
commandClass: parseInt(parts[0]),
endpoint: parseInt(parts[1]),
property: parts[2],
propertyKey: parts[3],
};
}
subscribeObservers(node, valueId) {
const valueObserver = observedCCProps[valueId.commandClass]?.[valueId.property];
if (valueObserver) {
this.valuesObservers[valueId.id] = valueObserver;
valueObserver.call(this, node, valueId);
}
}
/**
* Calls driver healNetwork function and schedule next rebuildNodeRoutes
*
*/
// rebuildNodeRoutes() {
// if (this.healTimeout) {
// clearTimeout(this.healTimeout)
// this.healTimeout = null
// }
// try {
// this.beginRebuildingRoutes()
// logger.info('Network auto rebuildNodeRoutes started')
// } catch (error) {
// logger.error(
// `Error while doing scheduled network rebuildNodeRoutes ${error.message}`,
// error
// )
// }
// // schedule next
// this.scheduleHeal()
// }
/**
* Used to Update an hass device of a specific node
*
*/
updateDevice(hassDevice, nodeId, deleteDevice = false) {
const node = this._nodes.get(nodeId);
// check for existing node and node hassdevice with given id
if (node && hassDevice.id && node.hassDevices?.[hassDevice.id]) {
if (deleteDevice) {
delete node.hassDevices[hassDevice.id];
}
else {
const id = hassDevice.id;
delete hassDevice.id;
node.hassDevices[id] = hassDevice;
}
this.emitNodeUpdate(node, {
hassDevices: node.hassDevices,
});
}
}
/**
* Used to Add a new hass device to a specific node
*/
addDevice(hassDevice, nodeId) {
const node = this._nodes.get(nodeId);
// check for existing node and node hassdevice with given id
if (node && hassDevice.id) {
delete hassDevice.id;
const id = hassDevice.type + '_' + hassDevice.object_id;
hassDevice.persistent = false;
node.hassDevices[id] = hassDevice;
this.emitNodeUpdate(node, {
hassDevices: node.hassDevices,
});
}
}
/**
* Used to update hass devices list of a specific node and store them in `nodes.json`
*
*/
async storeDevices(devices, nodeId, remove) {
const node = this._nodes.get(nodeId);
if (node) {
for (const id in devices) {
devices[id].persistent = !remove;
}
if (remove) {
delete this.storeNodes[nodeId].hassDevices;
}
else {
this.storeNodes[nodeId].hassDevices = devices;
}
node.hassDevices = utils.copy(devices);
await this.updateStoreNodes();
this.emitNodeUpdate(node, {
hassDevices: node.hassDevices,
});
}
}
/**
* Method used to close client connection, use this before destroy
*/
async close(keepListeners = false) {
this.status = ZwaveClientStatus.CLOSED;
this.closed = true;
this.driverReady = false;
if (this.commandsTimeout) {
clearTimeout(this.commandsTimeout);
this.commandsTimeout = null;
}
if (this.restartTimeout) {
clearTimeout(this.restartTimeout);
this.restartTimeout = null;
}
if (this.healTimeout) {
clearTimeout(this.healTimeout);
this.healTimeout = null;
}
if (this.updatesCheckTimeout) {
clearTimeout(this.updatesCheckTimeout);
this.updatesCheckTimeout = null;
}
if (this.statelessTimeouts) {
for (const k in this.statelessTimeouts) {
clearTimeout(this.statelessTimeouts[k]);
delete this.statelessTimeouts[k];
}
}
if (this.pollIntervals) {
for (const k in this.pollIntervals) {
clearTimeout(this.pollIntervals[k]);
delete this.pollIntervals[k];
}
}
for (const [key, entry] of this.throttledFunctions) {
clearTimeout(entry.timeout);
this.throttledFunctions.delete(key);
}
if (this.server) {
await this.server.destroy();
this.server = null;
}
if (this._driver) {
await this._driver.destroy();
this._driver = null;
}
if (!keepListeners) {
this.destroyed = true;
this.removeAllListeners();
}
logger.info('Client closed');
}
getStatus() {
const status = {
driverReady: this.driverReady,
status: this.driverReady && !this.closed,
config: this.cfg,
};
return status;
}
/** Used to get the general state of the client. Sent to socket on connection */
getState() {
return {
nodes: this.getNodes(),
info: this.getInfo(),
error: this.error,
cntStatus: this.cntStatus,
inclusionState: this._inclusionState,
};
}
/**
* If the node supports Schedule Lock CC parses all available schedules and cache them
*/
async getSchedules(nodeId, opts = {
fromCache: true,
}) {
const zwaveNode = this.getNode(nodeId);
if (!zwaveNode?.commandClasses['Schedule Entry Lock'].isSupported()) {
throw new Error('Schedule Entry Lock CC not supported on node ' + nodeId);
}
if (this._lockGetSchedule) {
throw new Error('Another request is in progress, cancel it or wait...');
}
const promise = async () => {
this._cancelGetSchedule = false;
this._lockGetSchedule = true;
const { mode, fromCache } = opts;
// TODO: should we check also other endpoints?
const endpointIndex = 0;
const endpoint = zwaveNode.getEndpoint(endpointIndex);
const userCodes = zwave_js_1.UserCodeCC.getSupportedUsersCached(this.driver, endpoint);
const numSlots = {
numWeekDaySlots: zwave_js_1.ScheduleEntryLockCC.getNumWeekDaySlotsCached(this.driver, endpoint),
numYearDaySlots: zwave_js_1.ScheduleEntryLockCC.getNumYearDaySlotsCached(this.driver, endpoint),
numDailyRepeatingSlots: zwave_js_1.ScheduleEntryLockCC.getNumDailyRepeatingSlotsCached(this.driver, endpoint),
};
const node = this._nodes.get(nodeId);
const weeklySchedules = node.schedule?.weekly?.slots ?? [];
const yearlySchedules = node.schedule?.yearly?.slots ?? [];
const dailySchedules = node.schedule?.daily?.slots ?? [];
node.schedule = {
daily: {
numSlots: numSlots.numDailyRepeatingSlots,
slots: dailySchedules,
},
weekly: {
numSlots: numSlots.numWeekDaySlots,
slots: weeklySchedules,
},
yearly: {
numSlots: numSlots.numYearDaySlots,
slots: yearlySchedules,
},
};
node.userCodes = {
total: userCodes,
available: [],
enabled: [],
};
const pushSchedule = (arr, slot, schedule, enabled) => {
const index = arr.findIndex((s) => s.userId === slot.userId && s.slotId === slot.slotId);
if (schedule) {
const newSlot = {
...slot,
...schedule,
enabled,
};
if (index === -1) {
arr.push(newSlot);
}
else {
arr[index] = newSlot;
}
}
else if (index !== -1) {
arr.splice(index, 1);
}
};
for (let i = 1; i <= userCodes; i++) {
const status = zwave_js_1.UserCodeCC.getUserIdStatusCached(this.driver, endpoint, i);
if (status === undefined ||
status === zwave_js_1.UserIDStatus.Available ||
status === zwave_js_1.UserIDStatus.StatusNotAvailable) {
// skip query on not enabled userIds or empty codes
continue;
}
node.userCodes.available.push(i);
const enabledUserId = zwave_js_1.ScheduleEntryLockCC.getUserCodeScheduleEnabledCached(this.driver, endpoint, i);
if (enabledUserId) {
node.userCodes.enabled.push(i);
}
const enabledType = zwave_js_1.ScheduleEntryLockCC.getUserCodeScheduleKindCached(this.driver, endpoint, i);
const getCached = (kind, slotId) => zwave_js_1.ScheduleEntryLockCC.getScheduleCached(this.driver, endpoint, kind, i, slotId);
if (!mode || mode === ZUIScheduleEntryLockMode.WEEKLY) {
const enabled = enabledType === zwave_js_1.ScheduleEntryLockScheduleKind.WeekDay;
for (let s = 1; s <= numSlots.numWeekDaySlots; s++) {
if (this._cancelGetSchedule)
return;
const slot = {
userId: i,
slotId: s,
};
const schedule = fromCache
? (getCached(zwave_js_1.ScheduleEntryLockScheduleKind.WeekDay, s))
: await zwaveNode.commandClasses['Schedule Entry Lock'].getWeekDaySchedule(slot);
pushSchedule(weeklySchedules, slot, schedule, enabled);
}
}
if (!mode || mode === ZUIScheduleEntryLockMode.YEARLY) {
const enabled = enabledType === zwave_js_1.ScheduleEntryLockScheduleKind.YearDay;
for (let s = 1; s <= numSlots.numYearDaySlots; s++) {
if (this._cancelGetSchedule)
return;
const slot = {
userId: i,
slotId: s,
};
const schedule = fromCache
? (getCached(zwave_js_1.ScheduleEntryLockScheduleKind.YearDay, s))
: await zwaveNode.commandClasses['Schedule Entry Lock'].getYearDaySchedule(slot);
pushSchedule(yearlySchedules, slot, schedule, enabled);
}
}
if (!mode || mode === ZUIScheduleEntryLockMode.DAILY) {
const enabled = enabledType ===
zwave_js_1.ScheduleEntryLockScheduleKind.DailyRepeating;
for (let s = 1; s <= numSlots.numDailyRepeatingSlots; s++) {
if (this._cancelGetSchedule)
return;
const slot = {
userId: i,
slotId: s,
};
const schedule = fromCache
? (getCached(zwave_js_1.ScheduleEntryLockScheduleKind.DailyRepeating, s))
: await zwaveNode.commandClasses['Schedule Entry Lock'].getDailyRepeatingSchedule(slot);
pushSchedule(dailySchedules, slot, schedule, enabled);
}
}
}
this.emitNodeUpdate(node, {
schedule: node.schedule,
userCodes: node.userCodes,
});
return node.schedule;
};
return promise().finally(() => {
this._lockGetSchedule = false;
this._cancelGetSchedule = false;
});
}
cancelGetSchedule() {
this._cancelGetSchedule = true;
}
async setSchedule(nodeId, type, schedule) {
const zwaveNode = this.getNode(nodeId);
if (!zwaveNode?.commandClasses['Schedule Entry Lock'].isSupported()) {
throw new Error('Schedule Entry Lock CC not supported on node ' + nodeId);
}
const slot = {
userId: schedule.userId,
slotId: schedule.slotId,
};
delete schedule.userId;
delete schedule.slotId;
delete schedule['enabled'];
const isDelete = Object.keys(schedule).length === 0;
if (isDelete) {
schedule = undefined;
}
let result;
if (type === 'daily') {
result = await zwaveNode.commandClasses['Schedule Entry Lock'].setDailyRepeatingSchedule(slot, schedule);
}
else if (type === 'weekly') {
result = await zwaveNode.commandClasses['Schedule Entry Lock'].setWeekDaySchedule(slot, schedule);
}
else if (type === 'yearly') {
result = await zwaveNode.commandClasses['Schedule Entry Lock'].setYearDaySchedule(slot, schedule);
}
else {
throw new Error('Invalid schedule type');
}
// means that is not using supervision, read slot and check if it matches
if (!result) {
const methods = {
daily: 'getDailyRepeatingSchedule',
weekly: 'getWeekDaySchedule',
yearly: 'getYearDaySchedule',
};
const res = await zwaveNode.commandClasses['Schedule Entry Lock'][methods[type]](slot);
if ((isDelete && !res) ||
(!isDelete && res && utils.deepEqual(res, schedule))) {
result = {
status: core_1.SupervisionStatus.Success,
};
}
else {
result = {
status: core_1.SupervisionStatus.Fail,
};
}
}
if (result.status === core_1.SupervisionStatus.Success) {
const node = this._nodes.get(nodeId);
// update enabled state
for (const mode in node.schedule) {
node.schedule[mode].slots = node.schedule[mode].slots.map((s) => ({
...s,
enabled: mode === type,
}));
}
const slots = node.schedule?.[type]?.slots;
if (slots) {
const slotIndex = slots.findIndex((s) => s.userId === slot.userId && s.slotId === slot.slotId);
const newSlot = isDelete
? null
: {
...slot,
...schedule,
enabled: true,
};
if (isDelete) {
if (slotIndex !== -1) {
slots.splice(slotIndex, 1);
}
}
else if (slotIndex !== -1) {
slots[slotIndex] = newSlot;
}
else {
slots.push(newSlot);
}
const isEnabledUsercode = node.userCodes?.enabled?.includes(slot.userId);
if (!isDelete && !isEnabledUsercode) {
node.userCodes.enabled.push(slot.userId);
}
else if (isDelete && isEnabledUsercode) {
const index = node.userCodes.enabled.indexOf(slot.userId);
if (index >= 0) {
node.userCodes.enabled.splice(index, 1);
}
}
this.emitNodeUpdate(node, {
schedule: node.schedule,
userCodes: node.userCodes,
});
}
}
return result;
}
async setEnabledSchedule(nodeId, enabled, userId) {
const zwaveNode = this.getNode(nodeId);
if (!zwaveNode) {
throw new Error('Node not found');
}
const result = await zwaveNode.commandClasses['Schedule Entry Lock'].setEnabled(enabled, userId);
// if result is not defined here we don't have a way
// to know if the command was successful or not as there is no
// 'get' command for this, so we just assume it was successful
if ((0, core_1.isUnsupervisedOrSucceeded)(result)) {
const node = this._nodes.get(nodeId);
if (node) {
if (userId) {
if (enabled) {
node.userCodes?.enabled.push(userId);
}
else {
const index = node.userCodes?.enabled.indexOf(userId);
if (index >= 0) {
node.userCodes.enabled.splice(index, 1);
}
}
}
else {
node.userCodes.enabled = enabled
? node.userCodes.available.slice()
: [];
}
this.emitNodeUpdate(node, {
userCodes: node.userCodes,
});
}
}
return result;
}
/**
* Populate node `groups`
*/
getGroups(nodeId, ignoreUpdate = false) {
const zwaveNode = this.getNode(nodeId);
const node = this._nodes.get(nodeId);
if (node && zwaveNode) {
let endpointGroups = new Map();
try {
endpointGroups =
this._driver.controller.getAllAssociationGroups(nodeId);
}
catch (error) {
this.logNode(zwaveNode, 'warn', `Error while fetching groups associations: ${error.message}`);
}
node.groups = [];
for (const [endpoint, groups] of endpointGroups) {
for (const [groupIndex, group] of groups) {
// https://zwave-js.github.io/node-zwave-js/#/api/controller?id=associationgroup-interface
node.groups.push({
title: group.label,
endpoint: endpoint,
value: groupIndex,
maxNodes: group.maxNodes,
isLifeline: group.isLifeline,
multiChannel: group.multiChannel,
});
}
}
}
if (node && !ignoreUpdate) {
this.emitNodeUpdate(node, { groups: node.groups });
}
}
/**
* Get an array of current [associations](https://zwave-js.github.io/node-zwave-js/#/api/controller?id=association-interface) of a specific group
*/
async getAssociations(nodeId, refresh = false) {
const zwaveNode = this.getNode(nodeId);
const toReturn = [];
if (zwaveNode) {
try {
if (refresh) {
await zwaveNode.refreshCCValues(core_1.CommandClasses.Association);
await zwaveNode.refreshCCValues(core_1.CommandClasses['Multi Channel Association']);
}
// https://zwave-js.github.io/node-zwave-js/#/api/controller?id=association-interface
// the result is a map where the key is the group number and the value is the array of associations {nodeId, endpoint?}
const result = this._driver.controller.getAllAssociations(nodeId);
for (const [source, group] of result.entries()) {
for (const [groupId, associations] of group) {
for (const a of associations) {
toReturn.push({
endpoint: source.endpoint,
groupId: groupId,
nodeId: a.nodeId,
targetEndpoint: a.endpoint,
});
}
}
}
}
catch (error) {
this.logNode(zwaveNode, 'warn', `Error while fetching groups associations: ${error.message}`);
// node doesn't support groups associations
}
}
else {
this.logNode(zwaveNode, 'warn', `Error while fetching groups associations, node not found`);
}
return toReturn;
}
/**
* Check if a given association is allowed
*/
checkAssociation(source, groupId, association) {
return this.driver.controller.checkAssociation(source, groupId, association);
}
/**
* Add a node to the array of specified [associations](https://zwave-js.github.io/node-zwave-js/#/api/controller?id=association-interface)
*/
async addAssociations(source, groupId, associations) {
const zwaveNode = this.getNode(source.nodeId);
const sourceMsg = `Node ${source.nodeId +
(source.endpoint ? ' Endpoint ' + source.endpoint : '')}`;
if (!zwaveNode) {
throw new Error(`Node ${source.nodeId} not found`);
}
const result = [];
for (const a of associations) {
const checkResult = this._driver.controller.checkAssociation(source, groupId, a);
result.push(checkResult);
if (checkResult === zwave_js_1.AssociationCheckResult.OK) {
this.logNode(zwaveNode, 'info', `Adding Node ${a.nodeId} to Group ${groupId} of ${sourceMsg}`);
await this._driver.controller.addAssociations(source, groupId, [
a,
]);
}
else {
this.logNode(zwaveNode, 'warn', `Unable to add Node ${a.nodeId} to Group ${groupId} of ${sourceMsg}: ${(0, Utils_1.getEnumMemberName)(zwave_js_1.AssociationCheckResult, checkResult)}`);
}
}
return result;
}
/**
* Remove a node from an association group
*
*/
async removeAssociations(source, groupId, associations) {
const zwaveNode = this.getNode(source.nodeId);
const sourceMsg = `Node ${source.nodeId +
(source.endpoint ? ' Endpoint ' + source.endpoint : '')}`;
if (zwaveNode) {
try {
this.logNode(zwaveNode, 'info', `Removing associations from ${sourceMsg} Group ${groupId}: %o`, associations);
await this._driver.controller.removeAssociations(source, groupId, associations);
}
catch (error) {
this.logNode(zwaveNode, 'warn', `Error while removing associations from ${sourceMsg}: ${error.message}`);
}
}
else {
this.logNode(zwaveNode, 'warn', `Error while removing associations from ${sourceMsg}, node not found`);
}
}
/**
* Remove all associations
*/
async removeAllAssociations(nodeId) {
const zwaveNode = this.getNode(nodeId);
if (zwaveNode) {
try {
const allAssociations = this._driver.controller.getAllAssociations(nodeId);
for (const [source, groupAssociations,] of allAssociations.entries()) {
for (const [groupId, associations] of groupAssociations) {
if (associations.length > 0) {
await this._driver.controller.removeAssociations(source, groupId, associations);
this.logNode(zwaveNode, 'info', `Removed ${associations.length} associations from Node ${source.nodeId +
(source.endpoint
? ' Endpoint ' + source.endpoint
: '')} group ${groupId}`);
}
}
}
}
catch (error) {
this.logNode(zwaveNode, 'warn', `Error while removing all associations from ${nodeId}: ${error.message}`);
}
}
else {
this.logNode(zwaveNode, 'warn', `Node not found when calling 'removeAllAssociations'`);
}
}
/**
* Setting the date and time on a node could be hard, this helper method will set it using the date provided (default to now).
*
* The following CCs will be used (when supported or necessary) in this process:
* - Time Parameters CC
* - Clock CC
* - Time CC
* - Schedule Entry Lock CC (for setting the timezone)
*/
syncNodeDateAndTime(nodeId, date = new Date()) {
const zwaveNode = this.getNode(nodeId);
if (zwaveNode) {
this.logNode(zwaveNode, 'info', `Syncing Node ${nodeId} date and time`);
return zwaveNode.setDateAndTime(date);
}
else {
this.logNode(zwaveNode, 'warn', `Node not found when calling 'syncNodeDateAndTime'`);
}
}
manuallyIdleNotificationValue(valueId) {
const zwaveNode = this.getNode(valueId.nodeId);
if (zwaveNode) {
zwaveNode.manuallyIdleNotificationValue(valueId);
}
else {
this.logNode(zwaveNode, 'warn', `Node not found when calling 'manuallyIdleNotificationValue'`);
}
}
/**
* Remove node from all associations
*/
async removeNodeFromAllAssociations(nodeId) {
const zwaveNode = this.getNode(nodeId);
if (zwaveNode) {
try {
this.logNode(zwaveNode, 'info', `Removing Node ${nodeId} from all associations`);
await this._driver.controller.removeNodeFromAllAssociations(nodeId);
}
catch (error) {
this.logNode(zwaveNode, 'warn', `Error while removing Node ${nodeId} from all associations: ${error.message}`);
}
}
else {
this.logNode(zwaveNode, 'warn', `Node not found when calling 'removeNodeFromAllAssociations'`);
}
}
/**
* Refresh all nodes neighbors
*/
async refreshNeighbors() {
if (this._lockNeighborsRefresh === true) {
throw Error('you can refresh neighbors only once every 60 seconds');
}
this._lockNeighborsRefresh = true;
// set the timeout here so if something fails later we don't keep the lock
setTimeout(() => (this._lockNeighborsRefresh = false), NEIGHBORS_LOCK_REFRESH);
const toReturn = {};
// when accessing the controller memory, the Z-Wave radio must be turned off with to avoid resource conflicts and inconsistent data
await this._driver.controller.toggleRF(false);
for (const [nodeId, node] of this._nodes) {
await this.getNodeNeighbors(nodeId, true, false);
toReturn[nodeId] = node.neighbors;
}
// turn rf back to on
await this._driver.controller.toggleRF(true);
return toReturn;
}
/**
* Get neighbors of a specific node
*/
async getNodeNeighbors(nodeId, preventThrow = false, emitNodeUpdate = true) {
try {
if (!this.driverReady) {
throw new DriverNotReadyError();
}
const zwaveNode = this.getNode(nodeId);
if (zwaveNode.protocol === core_1.Protocols.ZWaveLongRange) {
return [];
}
const neighbors = await this._driver.controller.getNodeNeighbors(nodeId);
this.logNode(nodeId, 'debug', `Neighbors: ${neighbors.join(', ')}`);
const node = this.nodes.get(nodeId);
if (node) {
node.neighbors = [...neighbors];
if (emitNodeUpdate) {
this.emitNodeUpdate(node, {
neighbors: node.neighbors,
});
}
}
return neighbors;
}
catch (error) {
this.logNode(nodeId, 'warn', `Error while getting neighbors from ${nodeId}: ${error.message}`);
if (!preventThrow) {
throw error;
}
return Promise.resolve([]);
}
}
/**
* Instructs a node to (re-)discover its neighbors.
*/
async discoverNodeNeighbors(nodeId) {
if (!this.driverReady) {
throw new DriverNotReadyError();
}
const result = await this._driver.controller.discoverNodeNeighbors(nodeId);
if (result) {
// update neighbors
this.getNodeNeighbors(nodeId, true).catch(() => {
// noop
});
}
return result;
}
/**
* Execute a driver function.
* More info [here](/usage/driver_function?id=driver-function)
*/
driverFunction(code) {
if (!this.driverReady) {
throw new DriverNotReadyError();
}
if (!this.driverFunctionCache.find((c) => c.content === code)) {
const name = `CACHED_${this.driverFunctionCache.length}`;
this.driverFunctionCache.push({ name, content: code });
}
const AsyncFunction = Object.getPrototypeOf(
// eslint-disable-next-line @typescript-eslint/no-empty-function
async function () { }).constructor;
const fn = new AsyncFunction('driver', code);
return fn.call({ zwaveClient: this, require, logger }, this._driver);
}
/**
* Method used to start Z-Wave connection using configuration `port`
*/
async connect() {
if (this.cfg.enabled === false) {
logger.info('Z-Wave driver DISABLED');
return;
}
if (this.driverReady) {
logger.info(`Driver already connected to ${this.cfg.port}`);
return;
}
// this could happen when the driver fails the connect and a reconnect timeout triggers
if (this.closed || this.checkIfDestroyed()) {
return;
}
if (!this.cfg?.port) {
logger.warn('Z-Wave driver not inited, no port configured');
return;
}
let shouldUpdateSettings = false;
// extend options with hidden `options`
const zwaveOptions = {
bootloaderMode: this.cfg.allowBootloaderOnly ? 'allow' : 'recover',
storage: {
cacheDir: app_1.storeDir,
deviceConfigPriorityDir: this.cfg.deviceConfigPriorityDir || exports.deviceConfigPriorityDir,
},
logConfig: {
// https://zwave-js.github.io/node-zwave-js/#/api/driver?id=logconfig
enabled: this.cfg.logEnabled,
level: this.cfg.logLevel
? loglevels[this.cfg.logLevel]
: 'info',
logToFile: this.cfg.logToFile,
filename: ZWAVEJS_LOG_FILE,
forceConsole: (0, utils_1.isDocker)() ? !this.cfg.logToFile : false,
maxFiles: this.cfg.maxFiles || 7,
nodeFilter: this.cfg.nodeFilter && this.cfg.nodeFilter.length > 0
? this.cfg.nodeFilter.map((n) => parseInt(n))
: undefined,
},
emitValueUpdateAfterSetValue: true,
apiKeys: {
firmwareUpdateService: '421e29797c3c2926f84efc737352d6190354b3b526a6dce6633674dd33a8a4f964c794f5',
},
timeouts: {
report: this.cfg.higherReportsTimeout ? 10000 : undefined,
sendToSleep: this.cfg.sendToSleepTimeout,
response: this.cfg.responseTimeout,
},
features: {
unresponsiveControllerRecovery: this.cfg
.disableControllerRecovery
? false
: true,
watchdog: this.cfg.disableWatchdog ? false : true,
},
userAgent: {
[utils.pkgJson.name]: utils.pkgJson.version,
},
disableOptimisticValueUpdate: this.cfg.disableOptimisticValueUpdate,
};
// when no env is specified copy config db to store dir
// fixes issues with pkg (and no more need to set this env on docker)
if (!process.env.ZWAVEJS_EXTERNAL_CONFIG) {
zwaveOptions.storage.deviceConfigExternalDir = app_1.configDbDir;
}
if (this.cfg.rf) {
const { region, txPower, maxLongRangePowerlevel } = this.cfg.rf;
let { autoPowerlevels } = this.cfg.rf;
zwaveOptions.rf = {};
if (typeof region === 'number') {
zwaveOptions.rf.region = region;
}
if (autoPowerlevels === undefined &&
typeof maxLongRangePowerlevel !== 'number' &&
typeof txPower?.powerlevel !== 'number') {
// if autoPowerlevels is undefined and maxLongRangePowerlevel is not a number (likely '' or undefined), assume autoPowerlevels is true
autoPowerlevels = true;
this.cfg.rf.autoPowerlevels = true;
shouldUpdateSettings = true;
}
if (autoPowerlevels) {
zwaveOptions.rf.maxLongRangePowerlevel = 'auto';
zwaveOptions.rf.txPower ??= {};
zwaveOptions.rf.txPower.powerlevel = 'auto';
}
if (!autoPowerlevels &&
(max