UNPKG

zwave-js-ui

Version:

Z-Wave Control Panel and MQTT Gateway

1,324 lines 181 kB
"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