UNPKG

iobroker.unifi

Version:
1,275 lines (1,062 loc) 56.3 kB
'use strict'; /* * Created with @iobroker/create-adapter v1.17.0 */ // The adapter-core module gives you access to the core ioBroker functions // you need to create an adapter const utils = require('@iobroker/adapter-core'); // Load your modules here const UnifiClass = require('node-unifi'); const jsonLogic = require('./admin/lib/json_logic.js'); class Unifi extends utils.Adapter { /** * @param {Partial<utils.AdapterOptions>} [options={}] */ constructor(options) { super({ ...options, name: 'unifi', }); this.on('ready', this.onReady.bind(this)); this.on('stateChange', this.onStateChange.bind(this)); this.on('unload', this.onUnload.bind(this)); this.controllers = {}; this.objectsFilter = {}; this.settings = {}; this.update = {}; this.clients = {}; this.vouchers = {}; this.dpi = {}; this.statesFilter = {}; this.queryTimeout = null; this.ownObjects = {}; this.stopped = false; } /** * Is called when adapter received configuration. */ async onReady() { try { // subscribe to all state changes this.subscribeStates('*.wlans.*.enabled'); this.subscribeStates('*.vouchers.create_vouchers'); this.subscribeStates('trigger_update'); this.subscribeStates('*.port_table.port_*.port_poe_enabled'); this.subscribeStates('*.port_table.port_*.port_poe_cycle'); this.subscribeStates('*.clients.*.reconnect'); this.subscribeStates('*.clients.*.blocked'); this.subscribeStates('*.devices.*.led_override'); this.subscribeStates('*.devices.*.restart'); this.log.info('UniFi adapter is ready'); // Load configuration this.settings.updateInterval = (parseInt(this.config.updateInterval, 10) * 1000) || (60 * 1000); this.settings.controllerIp = this.config.controllerIp; this.settings.controllerPort = this.config.controllerPort; this.settings.controllerUsername = this.config.controllerUsername; this.settings.controllerPassword = this.config.controllerPassword; this.settings.ignoreSSLErrors = this.config.ignoreSSLErrors !== undefined ? this.config.ignoreSSLErrors : true; this.update.blacklist = this.config.blacklistClients; this.update.clients = this.config.updateClients; this.update.devices = this.config.updateDevices; this.update.health = this.config.updateHealth; this.update.networks = this.config.updateNetworks; this.update.sysinfo = this.config.updateSysinfo; this.update.vouchers = this.config.updateVouchers; this.update.vouchersNoUsed = this.config.updateVouchersNoUsed; this.update.wlans = this.config.updateWlans; this.update.alarms = this.config.updateAlarms; this.update.alarmsNoArchived = this.config.updateAlarmsNoArchived; this.update.dpi = this.config.updateDpi; this.update.gatewayTraffic = this.config.updateGatewayTraffic; this.update.gatewayTrafficMaxDays = this.config.gatewayTrafficMaxDays; // @ts-ignore this.objectsFilter = this.config.blacklist || this.config.objectsFilter; // blacklist was renamed to objectsFilter in v0.5.3 // @ts-ignore this.statesFilter = this.config.whitelist || this.config.statesFilter; // blacklist was renamed to statesFilter in v0.5.3 // @ts-ignore this.clients.isOnlineOffset = (parseInt(this.config.clientsIsOnlineOffset, 10) * 1000) || (60 * 1000); this.vouchers.number = this.config.createVouchersNumber; this.vouchers.duration = this.config.createVouchersDuration; this.vouchers.quota = this.config.createVouchersQuota; this.vouchers.uploadLimit = !this.config.createVouchersUploadLimit ? null : this.config.createVouchersUploadLimit; this.vouchers.downloadLimit = !this.config.createVouchersDownloadLimit ? null : this.config.createVouchersDownloadLimit; this.vouchers.byteQuota = !this.config.createVouchersByteQuota ? null : this.config.createVouchersByteQuota; this.vouchers.note = this.config.createVouchersNote; if (this.settings.controllerIp !== '' && this.settings.controllerUsername !== '' && this.settings.controllerPassword !== '') { // Send some log messages this.log.debug(`controller = ${this.settings.controllerIp}:${this.settings.controllerPort}`); this.log.debug(`updateInterval = ${this.settings.updateInterval / 1000}`); // Start main function this.updateUnifiData(); } else { this.log.error('Adapter deactivated due to missing configuration.'); await this.setStateAsync('info.connection', { ack: true, val: false }); this.setForeignState(`system.adapter.${this.namespace}.alive`, false); } } catch (err) { this.handleError(err, undefined, 'onReady'); } } /** * Is called if a subscribed state changes * @param {string} id * @param {ioBroker.State | null | undefined} state */ async onStateChange(id, state) { if (state && !state.ack) { // The state was changed const idParts = id.split('.'); const site = idParts[2]; const mac = idParts[4]; try { if (idParts[3] === 'wlans' && idParts[5] === 'enabled') { await this.updateWlanStatus(site, id, state); } else if (idParts[3] === 'vouchers' && idParts[4] === 'create_vouchers') { await this.createUnifiVouchers(site); } else if (idParts[2] === 'trigger_update') { await this.updateUnifiData(true); } else if (idParts[7] === 'port_poe_enabled') { const portNumber = idParts[6].split('_').pop(); this.switchPoeOfPort(site, mac, portNumber, state.val); } else if (idParts[7] === 'port_poe_cycle') { const portNumber = idParts[6].split('_').pop(); const mac = idParts[4]; this.log.info(`onStateChange: port power cycle (port: ${portNumber}, device: ${mac})`); await this.controllers[site].powerCycleSwitchPort(mac, portNumber); } else if (idParts[5] === 'reconnect') { await this.reconnectClient(id, idParts, site); } else if (idParts[5] === 'blocked') { await this.blockClient(id, site, idParts, state.val); } else if (idParts[5] === 'led_override') { const deviceId = await this.getStateAsync(id.substring(0, id.lastIndexOf('.')) + '.device_id'); this.log.info(`onStateChange: override led to '${state.val}' (device: ${deviceId.val})`); await this.controllers[site].setLEDOverride(deviceId.val, state.val); } else if (idParts[5] === 'restart') { const mac = idParts[4]; this.log.info(`onStateChange: restart device '${mac}'`); await this.controllers[site].restartDevice(mac, 'soft'); } } catch (err) { this.handleError(err, site, 'onStateChange'); } } } /** * Is called when adapter shuts down - callback has to be called under any circumstances! * @param {() => void} callback */ onUnload(callback) { try { this.stopped = true; if (this.queryTimeout) { clearTimeout(this.queryTimeout); } this.log.info('cleaned everything up...'); callback(); } catch (e) { callback(); } } /** * Function to handle error messages * @param {Object} err * @param {String} site * @param {String | undefined} methodName */ async handleError(err, site, methodName = undefined) { if (err.message === 'api.err.Invalid') { this.log.error(`Error site ${site}: Incorrect username or password.`); } else if (err.message === 'api.err.LoginRequired') { this.log.error(`Error site ${site}: Login required. Check username and password.`); } else if (err.message === 'api.err.Ubic2faTokenRequired') { this.log.error(`Error site ${site}: 2-Factor-Authentication required by UniFi controller. 2FA is not supported by this adapter.`); } else if (err.message === 'api.err.ServerBusy') { this.log.error(`Error site ${site}: Server is busy. There seems to be a problem with the UniFi controller.`); } else if (err.message === 'api.err.NoPermission' || (err.response && err.response.data && err.response.data.meta && err.response.data.meta.msg && err.response.data.meta.msg === 'api.err.NoPermission')) { this.log.error(`Error site ${site}: Permission denied. Check access rights.`); } else if (err.message.includes('connect EHOSTUNREACH') || err.message.includes('connect ENETUNREACH')) { this.log.error(`Error site ${site}: Host or network cannot be reached.`); } else if (err.message.includes('connect ECONNREFUSED')) { this.log.error(`Error site ${site}: Connection refused. Incorrect IP or port.`); } else if (err.message.includes('connect ETIMEDOUT')) { this.log.error(`Error site ${site}: Connection timedout.`); } else if (err.message.includes('read ECONNRESET')) { this.log.error(`Error site ${site}: Connection was closed by the UniFi controller.`); } else if (err.message.includes('getaddrinfo EAI_AGAIN')) { this.log.error(`Error site ${site}: This error is not related to the adapter. There seems to be a DNS issue. Please google for "getaddrinfo EAI_AGAIN" to fix the issue.`); } else if (err.message.includes('getaddrinfo ENOTFOUND')) { this.log.error(`Error site ${site}: Host not found. Incorrect IP or port.`); } else if (err.message.includes('socket hang up')) { this.log.error(`Error site ${site}: Socket hang up: ${err.message}`); } else if (err.message.includes('socket disconnected')) { this.log.error(`Error site ${site}: Socket disconnected: ${err.message}`); } else if (err.message.includes('SSL routines') || err.message.includes('ssl3_') || err.message.includes('certificate has expired')) { this.log.error(`Error site ${site}: SSL/Certificate issue: ${err.message}`); } else if (err.message === 'api.err.InvalidArgs' || err.message === 'api.err.IncorrectNumberRange') { this.log.error(`Parameters for this call are invalid (${err.message})! Please check the parameters`); } else if (err.message === 'aborted') { this.log.error(`Request aborted.`); } else if (err.message.includes('Returned data is not in valid format')) { this.log.error(err.message); } else { if (err.response && err.response.data) { this.log.error(`Error site ${site} (data): ${JSON.stringify(err.response.data)}`); } if (methodName) { this.log.error(`[${methodName} site ${site}] error: ${err.message}, stack: ${err.stack}`); } else { this.log.error(`Error site ${site}: ${err.message}, stack: ${err.stack}`); } if (this.supportsFeature && this.supportsFeature('PLUGINS')) { const sentryInstance = this.getPluginInstance('sentry'); if (sentryInstance) { sentryInstance.getSentryObject().captureException(err); } } } } /** * Function that takes care of the API calls and processes * the responses afterwards */ async updateUnifiData(preventReschedule = false) { try { this.log.debug('Update started'); const defaultController = new UnifiClass.Controller({ host: this.settings.controllerIp, port: this.settings.controllerPort, username: this.settings.controllerUsername, password: this.settings.controllerPassword, sslverify: !this.settings.ignoreSSLErrors, timeout: 10000 }); try { await defaultController.login(); } catch (err) { this.handleError(err, undefined, 'updateUnifiData-login'); // In case of connection timeout, try again later if (err.code === 'ECONNABORTED') { this.queryTimeout = setTimeout(() => { this.updateUnifiData(); }, this.settings.updateInterval); } return; } this.log.debug('Login successful'); try { const sites = await this.fetchSites(defaultController); for (const site of sites) { if (this.stopped) { return; } try { if (!this.controllers[site]) { if (site === 'default') { this.controllers[site] = defaultController; /* try { defaultController.onAny((event, data) => { this.log.debug(`EVENT [${site}] ${event} : ${JSON.stringify(data)}`); }); await defaultController.listen(); } catch (err) { this.handleError(err, site, 'subscribe Events'); }*/ } else { this.controllers[site] = new UnifiClass.Controller({ host: this.settings.controllerIp, port: this.settings.controllerPort, username: this.settings.controllerUsername, password: this.settings.controllerPassword, site, sslverify: !this.settings.ignoreSSLErrors }); await this.controllers[site].login(); } } this.log.debug(`Update site: ${site}`); if (this.update.sysinfo === true) { await this.fetchSiteSysinfo(site); } if (this.update.clients === true) { await this.fetchClients(site); } if (this.update.devices === true) { await this.fetchDevices(site); } if (this.update.wlans === true) { await this.fetchWlans(site); } if (this.update.networks === true) { await this.fetchNetworks(site); } if (this.update.health === true) { await this.fetchHealth(site); } if (this.update.vouchers === true) { await this.fetchVouchers(site); } if (this.update.dpi === true) { await this.fetchDpi(site); } if (this.update.gatewayTraffic === true) { await this.fetchGatewayTraffic(site); } if (this.update.alarms === true) { await this.fetchAlarms(site); } // finalize, logout and finish //await this.controllers[site].logout(); } catch (err) { this.handleError(err, site, 'updateUnifiData'); } } // Update is_online of offline clients await this.setClientOnlineStatus(); } catch (err) { this.handleError(err, undefined, 'updateUnifiData-fetchSites'); return; } await this.setStateAsync('info.connection', { ack: true, val: true }); this.log.debug('Update done'); } catch (err) { await this.setStateAsync('info.connection', { ack: true, val: false }); this.handleError(err, undefined, 'updateUnifiData'); } if (preventReschedule === false) { // schedule a new execution of updateUnifiData in X seconds this.queryTimeout = setTimeout(() => { this.updateUnifiData(); }, this.settings.updateInterval); } } /** * Function to fetch site{Object} siteController * * @param {UnifiClass} siteController */ async fetchSites(siteController) { const data = await siteController.getSites(); if (data === undefined) { throw new Error(`fetchSites: Returned data is not in valid format: ${JSON.stringify(data)}`); } const sites = data.map((s) => { return s.name; }); this.log.debug(`fetchSites: ${sites}`); await this.processSites(sites, data); return sites; } /** * Function that receives the sites as a JSON data array * @param {String[]} sites * @param {Object[]} data */ async processSites(sites, data) { const objects = require('./admin/lib/objects_sites.json'); for (const site of sites) { const x = sites.indexOf(site); const siteData = data[x]; this.log.silly(`processSites: site: ${site}, data: ${JSON.stringify(data[x])}`); await this.applyJsonLogic('', siteData, objects, ['site']); } } /** * Function to fetch site sysinfo * @param {String} site */ async fetchSiteSysinfo(site) { const data = await this.controllers[site].getSiteSysinfo(); if (data === undefined) { throw new Error(`fetchSiteSysinfo ${site}: Returned data is not in valid format: ${JSON.stringify(data)}`); } this.log.debug(`fetchSiteSysinfo ${site}: ${data.length}`); this.log.silly(`fetchSiteSysinfo ${site}: ${JSON.stringify(data)}`); await this.processSiteSysinfo(site, data); return data; } /** * Function that receives the site sysinfo as a JSON data array * @param {String} site * @param {Object} data */ async processSiteSysinfo(site, data) { const objects = require('./admin/lib/objects_sysinfo.json'); await this.applyJsonLogic(site, data, objects, this.statesFilter.sysinfo); } /** * Function to fetch clients * @param {String} site */ async fetchClients(site) { const data = await this.controllers[site].getClientDevices(); if (!Array.isArray(data)) { throw new Error(`fetchClients ${site}: Returned data is not in valid format: ${JSON.stringify(data)}`); } this.log.debug(`fetchClients ${site}: ${data.length}`); this.log.silly(`fetchClients ${site}: ${JSON.stringify(data)}`); await this.processClients(site, data); await this.processBlockedClients(site); return data; } /** * Function that receives the clients as a JSON data array * @param {String} site * @param {Object} data */ async processClients(site, data) { const objects = require('./admin/lib/objects_clients.json'); if(this.update.blacklist === true){ if (data) { // Process objectsFilter const siteData = data.filter((item) => { if (this.objectsFilter.clients.includes(item.mac) == true || this.objectsFilter.clients.includes(item.ip) == true || this.objectsFilter.clients.includes(item.name) == true || this.objectsFilter.clients.includes(item.hostname) == true) { return item; } }); if (siteData.length > 0) { await this.applyJsonLogic(site, siteData, objects, this.statesFilter.clients); } } } if(this.update.blacklist === false){ if (data) { // Process objectsFilter const siteData = data.filter((item) => { if (this.objectsFilter.clients.includes(item.mac) !== true && this.objectsFilter.clients.includes(item.ip) !== true && this.objectsFilter.clients.includes(item.name) !== true && this.objectsFilter.clients.includes(item.hostname) !== true) { return item; } }); this.log.silly(`processClients: filtered data: ${JSON.stringify(siteData)}`); if (siteData.length > 0) { await this.applyJsonLogic(site, siteData, objects, this.statesFilter.clients); } } } } /** * Function to identify blocked clients and set the correct state * @param {Object} site */ async processBlockedClients(site) { if (this.statesFilter.clients.includes('clients.client.blocked')) { const blockedClients = await this.controllers[site].getBlockedUsers(); const allClients = await this.getStatesAsync(`*.clients.*.blocked`); // this.log.warn(JSON.stringify(blockedClients)); for (const id in allClients) { if (blockedClients && blockedClients.length > 0) { const clientMac = id.match(/([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})/)[0]; const index = blockedClients.findIndex(x => x.mac === clientMac); if (index === -1) { await this.setStateAsync(id, false, true); } else { await this.setStateAsync(id, true, true); this.log.debug(`client '${clientMac}' is blocked`); } } else { await this.setStateAsync(id, false, true); } } } } /** * Update is_online of offline clients */ async setClientOnlineStatus() { const wlanStates = await this.getStatesAsync('*.clients.*.last_seen_by_uap'); const wiredStates = await this.getStatesAsync('*.clients.*.last_seen_by_usw'); // Workaround for UniFi bug "wireless clients shown as wired clients" // https://community.ui.com/questions/Wireless-clients-shown-as-wired-clients/49d49818-4dab-473a-ba7f-d51bc4c067d1 for (const [key, value] of Object.entries(wlanStates)) { const wiredStateId = key.replace('last_seen_by_uap', 'last_seen_by_usw'); if (Object.prototype.hasOwnProperty.call(wiredStates, wiredStateId)) { delete wiredStates[wiredStateId]; } } const states = { ...wlanStates, ...wiredStates }; const now = Math.floor(Date.now() / 1000) * 1000; for (const [key, value] of Object.entries(states)) { if (value !== null && typeof value.val === 'string') { const lastSeen = Date.parse(value.val.replace(' ', 'T')); const isOnline = (lastSeen - (now - this.settings.updateInterval - this.clients.isOnlineOffset) >= 0); const stateId = key.replace(/last_seen_by_(usw|uap)/gi, 'is_online'); const oldState = await this.getStateAsync(stateId); if (oldState === null) { // This is the case if the client is new to the adapter with older JS-Controller versions // Check if object is available and set the value const oldObject = await this.getForeignObjectAsync(stateId); if (oldObject !== null) { await this.setForeignStateAsync(stateId, { ack: true, val: isOnline }); } } else if (oldState.val != isOnline) { await this.setForeignStateAsync(stateId, { ack: true, val: isOnline }); } } } } /** * Function to fetch devices * @param {String} site */ async fetchDevices(site) { const data = await this.controllers[site].getAccessDevices(); if (!Array.isArray(data)) { throw new Error(`fetchDevices ${site}: Returned data is not in valid format: ${JSON.stringify(data)}`); } this.log.debug(`fetchDevices ${site}: ${data.length}`); this.log.silly(`fetchDevices ${site}: ${JSON.stringify(data)}`); await this.processDevices(site, data); return data; } /** * Function that receives the devices as a JSON data array * @param {String} site * @param {Object} data */ async processDevices(site, data) { const objects = require('./admin/lib/objects_devices.json'); if (data) { // Process objectsFilter const siteData = data.filter((item) => { if (this.objectsFilter.devices.includes(item.mac) !== true && this.objectsFilter.devices.includes(item.ip) !== true && this.objectsFilter.devices.includes(item.name) !== true) { return item; } }); this.log.silly(`processDevices: filtered data: ${JSON.stringify(siteData)}`); if (siteData.length > 0) { await this.applyJsonLogic(site, siteData, objects, this.statesFilter.devices); } } } /** * Function to fetch WLANs * @param {String} site */ async fetchWlans(site) { const data = await this.controllers[site].getWLanSettings(); if (!Array.isArray(data)) { throw new Error(`fetchWlans ${site}: Returned data is not in valid format: ${JSON.stringify(data)}`); } this.log.debug(`fetchWlans ${site}: ${data.length}`); this.log.silly(`fetchWlans ${site}: ${JSON.stringify(data)}`); await this.processWlans(site, data); return data; } /** * Function that receives the WLANs as a JSON data array * @param {String} site * @param {Object} data */ async processWlans(site, data) { const objects = require('./admin/lib/objects_wlans.json'); if (data) { // Process objectsFilter const siteData = data.filter((item) => { if (this.objectsFilter.wlans.includes(item.name) !== true) { return item; } }); this.log.silly(`processWlans: filtered data: ${JSON.stringify(siteData)}`); if (siteData.length > 0) { await this.applyJsonLogic(site, siteData, objects, this.statesFilter.wlans); } } } /** * Function to fetch networks * @param {String} site */ async fetchNetworks(site) { const data = await this.controllers[site].getNetworkConf(); if (!Array.isArray(data)) { throw new Error(`fetchNetworks ${site}: Returned data is not in valid format: ${JSON.stringify(data)}`); } this.log.debug(`fetchNetworks ${site}: ${data.length}`); this.log.silly(`fetchNetworks ${site}: ${JSON.stringify(data)}`); await this.processNetworks(site, data); return data; } /** * Function that receives the networks as a JSON data array * @param {String} site * @param {Object} data */ async processNetworks(site, data) { const objects = require('./admin/lib/objects_networks.json'); if (data) { // Process objectsFilter const siteData = data.filter((item) => { if (this.objectsFilter.networks.includes(item.name) !== true) { return item; } }); this.log.silly(`processNetworks: filtered data: ${JSON.stringify(siteData)}`); if (siteData.length > 0) { await this.applyJsonLogic(site, siteData, objects, this.statesFilter.networks); } } } /** * Function to fetch health * @param {String} site */ async fetchHealth(site) { const data = await this.controllers[site].getHealth(); if (!Array.isArray(data)) { throw new Error(`fetchHealth ${site}: Returned data is not in valid format: ${JSON.stringify(data)}`); } this.log.debug(`fetchHealth ${site}: ${data.length}`); this.log.silly(`fetchHealth ${site}: ${JSON.stringify(data)}`); await this.processHealth(site, data); return data; } /** * Function that receives the health as a JSON data array * @param {String} site * @param {Object} data */ async processHealth(site, data) { const objects = require('./admin/lib/objects_health.json'); if (data) { // Process objectsFilter const siteData = data.filter((item) => { if (this.objectsFilter.health.includes(item.subsystem) !== true) { return item; } }); this.log.silly(`processHealth: filtered data: ${JSON.stringify(siteData)}`); if (siteData.length > 0) { await this.applyJsonLogic(site, siteData, objects, this.statesFilter.health); } } } /** * Function to fetch vouchers * @param {String} site */ async fetchVouchers(site) { const data = await this.controllers[site].getVouchers(); if (!Array.isArray(data)) { throw new Error(`fetchVouchers ${site}: Returned data is not in valid format: ${JSON.stringify(data)}`); } this.log.debug(`fetchVouchers ${site}: ${data.length}`); this.log.silly(`fetchVouchers ${site}: ${JSON.stringify(data)}`); await this.processVouchers(site, data); return data; } /** * Function that receives the vouchers as a JSON data array * @param {String} site * @param {Object} data */ async processVouchers(site, data) { const objects = require('./admin/lib/objects_vouchers.json'); if (data) { let siteData = data; if (this.update.vouchersNoUsed) { // Remove used vouchers siteData = siteData.filter((item) => { if (item.used === 0) { return item; } }); this.log.silly(`processVouchers: filtered data: ${JSON.stringify(siteData)}`); const existingVouchers = await this.getForeignObjectsAsync(`${this.namespace}.${site}.vouchers.voucher_*`, 'channel'); for (const voucher in existingVouchers) { const voucherId = voucher.replace(`${this.namespace}.${site}.vouchers.voucher_`, ''); if (!siteData.find(item => item.code === voucherId)) { const voucherChannelId = `${this.namespace}.${site}.vouchers.voucher_${voucherId}`; this.log.debug(`deleting data points of voucher with id '${voucherId}'`); // voucher id not exist in api request result -> get dps and delete them const dpsOfVoucherId = await this.getForeignObjectsAsync(`${voucherChannelId}.*`); for (const id in dpsOfVoucherId) { // delete datapoint await this.delObjectAsync(id); if (this.ownObjects[id.replace(`${this.namespace}.`, '')]) { // remove from own objects if exist await delete this.ownObjects[id.replace(`${this.namespace}.`, '')]; } } // delete voucher channel await this.delObjectAsync(`${voucherChannelId}`); if (this.ownObjects[voucherChannelId.replace(`${this.namespace}.`, '')]) { // remove from own objects if exist await delete this.ownObjects[voucherChannelId.replace(`${this.namespace}.`, '')]; } } } } await this.applyJsonLogic(site, siteData, objects, this.statesFilter.vouchers); } } /** * Function to fetch dpi * @param {String} site */ async fetchDpi(site) { const data = await this.controllers[site].getDPIStats(site); if (!Array.isArray(data)) { throw new Error(`fetchDpi ${site}: Returned data is not in valid format. This option is only available for gateways!: ${JSON.stringify(data)}`); } if (data[0] && data[0].by_cat && data[0].by_app) { this.log.debug(`fetchDpi ${site}: categories: ${data[0].by_cat.length}, apps: ${data[0].by_app.length}`); } this.log.silly(`fetchDpi ${site}: ${JSON.stringify(data)}`); await this.processDpi(site, data); return data; } /** * Function that receives the dpi as a JSON data array * @param {String} site * @param {Object} data */ async processDpi(site, data) { const objects = require('./admin/lib/objects_dpi.json'); if (data) { // Process objectsFilter const siteData = data.filter((item) => { // if (this.objectsFilter.dpi.includes(item.subsystem) !== true) { // return item; // } return item; }); this.log.silly(`processDpi: filtered data: ${JSON.stringify(siteData)}`); if (siteData.length > 0) { await this.applyJsonLogic(site, siteData, objects, this.statesFilter.dpi); } } } /** * Function to fetch daily gateway traffic * @param {String} site */ async fetchGatewayTraffic(site) { let start = undefined; let end = undefined; if (this.update.gatewayTrafficMaxDays > 0) { const now = new Date(); end = now.getTime(); now.setDate(now.getDate() - this.update.gatewayTrafficMaxDays); start = now.getTime(); this.log.silly(`fetchGatewayTraffic: start: ${new Date(start).toLocaleDateString()}, end: ${new Date(end).toLocaleDateString()}`); } const data = await this.controllers[site].getDailyGatewayStats(start, end, ['lan-rx_bytes', 'lan-tx_bytes']); if (!Array.isArray(data)) { throw new Error(`fetchGatewayTraffic ${site}: Returned data is not in valid format. This option is only available for gateways!: ${JSON.stringify(data)}`); } this.log.debug(`fetchGatewayTraffic ${site}: ${data.length}`); this.log.silly(`fetchGatewayTraffic ${site}: ${JSON.stringify(data)}`); await this.processGatewayTraffic(site, data); return data; } /** * Function that receives the daily gateway traffic as a JSON data array * @param {String} site * @param {Object} data */ async processGatewayTraffic(site, data) { const objects = require('./admin/lib/objects_gateway_traffic.json'); if (data) { // Process objectsFilter const siteData = data.filter((item) => { // if (this.objectsFilter.dpi.includes(item.subsystem) !== true) { // return item; // } return item; }); this.log.silly(`processGatewayTraffic: filtered data: ${JSON.stringify(siteData)}`); if (siteData.length > 0) { await this.applyJsonLogic(site, siteData, objects, this.statesFilter.gateway_traffic); } } } /** * Function to fetch alarms * @param {String} site */ async fetchAlarms(site) { const data = await this.controllers[site].getAlarms(); if (!Array.isArray(data)) { throw new Error(`fetchAlarms ${site}: Returned data is not in valid format: ${JSON.stringify(data)}`); } this.log.debug(`fetchAlarms ${site}: ${data.length}`); this.log.silly(`fetchAlarms ${site}: ${JSON.stringify(data)}`); await this.processAlarms(site, data); return data; } /** * Function that receives the alarms as a JSON data array * @param {String} site * @param {Object} data */ async processAlarms(site, data) { const objects = require('./admin/lib/objects_alarms.json'); if (data) { // Process objectsFilter const siteData = data.filter((item) => { // if (this.objectsFilter.dpi.includes(item.subsystem) !== true) { // return item; // } return item; }); this.log.silly(`processAlarms: filtered data: ${JSON.stringify(siteData)}`); if (this.update.alarmsNoArchived) { const existingAlarms = await this.getForeignObjectsAsync(`${this.namespace}.${site}.alarms.alarm_*`, 'channel'); const alarmDatapoints = await this.getUnifiObjectsLibIds('alarms'); for (const alarm in existingAlarms) { const alarmId = alarm.replace(`${this.namespace}.${site}.alarms.alarm_`, ''); if (!siteData.find(item => item._id === alarmId)) { this.log.debug(`deleting data points of alarm with id '${alarmId}'`); for (const dp of alarmDatapoints) { const dpId = `${site}.${dp.replace('.alarm', `.alarm_${alarmId}`)}`; if (await this.getObjectAsync(dpId)) { await this.delObjectAsync(dpId); } if (this.ownObjects[dpId]) { // remove from own objects if exist await delete this.ownObjects[dpId]; } } } } } if (siteData.length > 0) { await this.applyJsonLogic(site, siteData, objects, this.statesFilter.alarms); } } } /** * Disable or enable a WLAN * @param {*} site * @param {*} objId * @param {*} state */ async updateWlanStatus(site, objId, state) { try { //await this.controllers[site].login(this.settings.controllerUsername, this.settings.controllerPassword); //this.log.debug('Login successful'); await this.setWlanStatus(site, objId, state); // finalize, logout and finish //await this.controllers[site].logout(); this.log.info(`WLAN status set to ${state.val}`); return true; } catch (err) { this.handleError(err, site, 'updateWlanStatus'); } } /** * Function to fetch vouchers * @param {String} site * @param {Object} objId * @param {Object} state */ async setWlanStatus(site, objId, state) { const obj = await this.getForeignObjectAsync(objId); if (!obj || !obj.native) { throw new Error(`setWlanStatus: Object ${objId} invalid, please restart adapter!`); } const wlanId = obj.native.wlan_id; const disable = !state.val; const data = await this.controllers[site].disableWLan(wlanId, disable); if (!Array.isArray(data)) { throw new Error(`setWlanStatus: Returned data is not in valid format: ${JSON.stringify(data)}`); } this.log.debug(`setWlanStatus: ${data.length}`); await this.processWlans(site, data); return data; } /** * Create vouchers * @param {String} site */ async createUnifiVouchers(site) { try { //await this.controllers[site].login(this.settings.controllerUsername, this.settings.controllerPassword); //this.log.debug('Login successful'); await this.createVouchers(site); await this.fetchVouchers(site); // finalize, logout and finish //await this.controllers[site].logout(); this.log.info('Vouchers created'); return true; } catch (err) { this.handleError(err, site, 'createUnifiVouchers'); return false; } } /** * Function to create vouchers * @param {String} site */ async createVouchers(site) { const minutes = this.vouchers.duration || 60; const count = this.vouchers.number || 1; const quota = this.vouchers.quota || 1; const note = this.vouchers.note || ''; const up = this.vouchers.uploadLimit || 0; const down = this.vouchers.downloadLimit || 0; const mbytes = this.vouchers.byteQuota || 0; const data = await this.controllers[site].createVouchers(site, minutes, count, quota, note, up, down, mbytes); if (!Array.isArray(data)) { throw new Error(`createVouchers: Returned data is not in valid format: ${JSON.stringify(data)}`); } this.log.debug(`createVouchers: ${data.length}`); await this.processWlans(site, data); return data; } /** * Function to switch poe power for port of device * @param {String} site * @param {String} deviceMac * @param {String} port * @param {Boolean} val */ async switchPoeOfPort(site, deviceMac, port, val) { try { this.log.info(`switchPoeOfPort: switching poe power of port ${port} for device ${deviceMac} to ${val}`); // we have to get whole data of 'port_overrides' to change poe power of single port. // we must sent the 'port_overrides' for all ports, otherwise the other port will set to default settings const result = await this.fetchDevices(site); const dataDevice = result.filter(x => x.mac === deviceMac); if (dataDevice && dataDevice.length) { const deviceId = dataDevice[0].device_id; // eslint-disable-next-line prefer-const let port_overrides = dataDevice[0].port_overrides; if (port_overrides && port_overrides.length > 0) { const indexOfPort = port_overrides.findIndex(x => x.port_idx === parseInt(port)); if (indexOfPort !== -1) { // port_overrides has settings for this port port_overrides[indexOfPort].poe_mode = val ? 'auto' : 'off'; } else { // port_overrides has no settings for this port this.log.debug(`switchPoeOfPort: port ${port} not exists in port_overrides object -> create item`); port_overrides[indexOfPort].poe_mode = val ? 'auto' : 'off'; } await this.controllers[site].setDeviceSettingsBase(deviceId, { port_overrides: port_overrides }); await this.fetchDevices(site); } else { this.log.debug(`switchPoeOfPort: no port_overrides object exists!`); } } } catch (err) { this.handleError(err, undefined, 'switchPoeOfPort'); } } /** * Function to reconnect a client * @param {String} id * @param {Array<String>} idParts * @param {String} site */ async reconnectClient(id, idParts, site) { try { const mac = idParts[4]; const name = await this.getStateAsync(id.replace(idParts[5], 'name')); if (name && name.val) { this.log.info(`reconnectClient: reconnecting client '${name.val}' (mac: ${mac})'`); } else { this.log.info(`reconnectClient: reconnecting client '${mac}'`); } await this.controllers[site].reconnectClient(mac); } catch (err) { this.handleError(err, undefined, 'reconnectClient'); } } /** * Funtion to block / unblock client * @param {String} id * @param {String} site * @param {Array<String>} idParts * @param {Boolean} block */ async blockClient(id, site, idParts, block) { const mac = idParts[4]; const name = await this.getStateAsync(id.replace(idParts[5], 'name')); if (name && name.val) { this.log.info(`${block ? 'block' : 'unblock'} client '${name.val}' (mac: ${mac})'`); } else { this.log.info(`${block ? 'block' : 'unblock'} client '${mac}'`); } if (block) { await this.controllers[site].blockClient(mac); } else { await this.controllers[site].unblockClient(mac); } } /** * Function to apply JSON logic to API responses * @param {*} objectTree * @param {*} data * @param {*} objects * @param {*} statesFilter */ async applyJsonLogic(objectTree, data, objects, statesFilter) { try { for (const key in objects) { if (this.stopped) { return; } if (statesFilter === undefined || statesFilter.length === 0 || statesFilter.includes(key)) { const obj = { '_id': null, 'type': null, 'common': {}, 'native': {} }; // Process object id if (Object.prototype.hasOwnProperty.call(objects[key], '_id')) { obj._id = objects[key]._id; } else { obj._id = await this.applyRule(objects[key].logic._id, data); } if (obj._id !== null && obj._id.slice(-1) !== -1) { if (objectTree !== '') { obj._id = `${objectTree}.${obj._id}`; } // Process type if (Object.prototype.hasOwnProperty.call(objects[key], 'type')) { obj.type = objects[key].type; } else { obj.type = await this.applyRule(objects[key].logic.type, data); } // Process common if (Object.prototype.hasOwnProperty.call(objects[key], 'common')) { obj.common = JSON.parse(JSON.stringify(objects[key].common)); } if (Object.prototype.hasOwnProperty.call(objects[key].logic, 'common')) { const common = objects[key].logic.common; for (const commonKey in common) { obj.common[commonKey] = await this.applyRule(common[commonKey], data); } } // Process native if (Object.prototype.hasOwnProperty.call(objects[key], 'native')) { obj.native = JSON.parse(JSON.stringify(objects[key].native)); } if (Object.prototype.hasOwnProperty.call(objects[key].logic, 'native')) { const native = objects[key].logic.native; for (const nativeKey in native) { obj.native[nativeKey] = await this.applyRule(native[nativeKey], data); } } // Cleanup _id const FORBIDDEN_CHARS = /[\]\[*,;'"`<>\\?\s]/g; let tempId = obj._id.replace(FORBIDDEN_CHARS, '_'); tempId = tempId.toLowerCase(); obj._id = tempId; //this.log.debug(JSON.stringify(obj)); // Update object if changed if (!Object.prototype.hasOwnProperty.call(this.ownObjects, obj._id)) { await this.extendObjectAsync(obj._id, { type: obj.type,