UNPKG

iobroker.device-watcher

Version:
1,451 lines (1,317 loc) 84.9 kB
'use strict'; const utils = require('@iobroker/adapter-core'); const adapterName = require('./package.json').name.split('.').pop(); const schedule = require('node-schedule'); const arrApart = require('./lib/arrApart.js'); // list of supported adapters const translations = require('./lib/translations.js'); const tools = require('./lib/tools.js'); const crud = require('./lib/crud.js'); // indicator if the adapter is running (for intervall/shedule) let isUnloaded = false; const adapterUpdateListDP = 'admin.*.info.updatesJson'; class DeviceWatcher extends utils.Adapter { constructor(options) { super({ ...options, name: adapterName, useFormatDate: true, }); // instances and adapters // raw arrays this.listInstanceRaw = new Map(); this.adapterUpdatesJsonRaw = []; this.listErrorInstanceRaw = []; // user arrays this.listAllInstances = []; this.listAllActiveInstances = []; this.listDeactivatedInstances = []; this.listAdapterUpdates = []; this.listErrorInstance = []; //counts this.countAllInstances = 0; this.countAllActiveInstances = 0; this.countDeactivatedInstances = 0; this.countAdapterUpdates = 0; this.countErrorInstance = 0; // devices // raw arrays this.listAllDevicesRaw = new Map(); this.batteryLowPoweredRaw = []; this.offlineDevicesRaw = []; this.upgradableDevicesRaw = []; // arrays this.listAllDevicesUserRaw = []; this.listAllDevices = []; this.offlineDevices = []; this.linkQualityDevices = []; this.batteryPowered = []; this.batteryLowPowered = []; this.selAdapter = []; this.adapterSelected = []; this.upgradableList = []; // counts this.offlineDevicesCount = 0; this.deviceCounter = 0; this.linkQualityCount = 0; this.batteryPoweredCount = 0; this.lowBatteryPoweredCount = 0; this.upgradableDevicesCount = 0; // Blacklist // Instances this.blacklistInstancesLists = []; this.blacklistInstancesNotify = []; // Devices this.blacklistLists = []; this.blacklistAdapterLists = []; this.blacklistNotify = []; // Timelist instances this.userTimeInstancesList = new Map(); // Interval timer this.refreshDataTimeout = null; // Check if main function is running this.mainRunning = false; this.on('ready', this.onReady.bind(this)); this.on('stateChange', this.onStateChange.bind(this)); this.on('objectChange', this.onObjectChange.bind(this)); this.on('message', this.onMessage.bind(this)); this.on('unload', this.onUnload.bind(this)); } /** * onReady */ async onReady() { this.log.debug(`Adapter ${adapterName} was started`); // set user language if (this.config.userSelectedLanguage === '') { if (this.language !== undefined && this.language !== null) { this.config.userSelectedLanguage = this.language; } else { this.config.userSelectedLanguage = 'de'; } } this.log.debug(`Set language to ${this.config.userSelectedLanguage}`); this.configCreateInstanceList = this.config.checkAdapterInstances; this.configListOnlyBattery = this.config.listOnlyBattery; this.configCreateOwnFolder = this.config.createOwnFolder; this.configCreateHtmlList = this.config.createHtmlList; this.configSetAdapter = { alexa2: this.config.alexa2Devices, apcups: this.config.apcupsDevices, ble: this.config.bleDevices, deconz: this.config.deconzDevices, ecovacsdeebot: this.config.ecovacsdeebotDevices, enocean: this.config.enoceanDevices, esphome: this.config.esphomeDevices, eusec: this.config.eusecDevices, fhemTFAsensors: this.config.fhemTFAsensorsDevices, fritzdect: this.config.fritzdectDevices, fullybrowser: this.config.fullybrowserDevices, fullybrowserV3: this.config.fullybrowserV3Devices, fullyMQTT: this.config.fullyMQTTDevices, ham: this.config.hamDevices, harmony: this.config.harmonyDevices, hmiP: this.config.hmiPDevices, hmrpc: this.config.hmrpcDevices, homeconnect: this.config.homeconnectDevices, homekitController: this.config.homekitControllerDevices, hs100: this.config.hs100Devices, hue: this.config.hueDevices, hueExt: this.config.hueExtDevices, innogy: this.config.innogyDevices, jeelink: this.config.jeelinkDevices, loqedSmartLock: this.config.loqedSmartLockDevices, lupusec: this.config.lupusecDevices, maxcube: this.config.maxcubeDevices, meross: this.config.merossDevices, mihome: this.config.mihomeDevices, mihomeGW: this.config.mihomeDevices, mihomeVacuum: this.config.mihomeVacuumDevices, mqttClientZigbee2Mqtt: this.config.mqttClientZigbee2MqttDevices, mqttNuki: this.config.mqttNukiDevices, musiccast: this.config.musiccastDevices, netatmo: this.config.netatmoDevices, nukiExt: this.config.nukiExtDevices, nut: this.config.nutDevices, ping: this.config.pingDevices, proxmox: this.config.proxmoxDevices, ring: this.config.ringDevices, roomba: this.config.roombaDevices, shelly: this.config.shellyDevices, smartgarden: this.config.smartgardenDevices, sonoff: this.config.sonoffDevices, sonos: this.config.sonosDevices, sureflap: this.config.sureflapDevices, switchbotBle: this.config.switchbotBleDevices, tado: this.config.tadoDevices, tapo: this.config.tapoDevices, tradfri: this.config.tradfriDevices, tuya: this.config.tuyaDevices, unifi: this.config.unifiDevices, viessmann: this.config.viessmannDevices, wifilight: this.config.wifilightDevices, wled: this.config.wledDevices, xsense: this.config.xsenseDevices, yeelight: this.config.yeelightDevices, zigbee: this.config.zigbeeDevices, zigbee2MQTT: this.config.zigbee2mqttDevices, zwave2: this.config.zwaveDevices, }; this.configMaxMinutes = { alexa2: this.config.alexa2MaxMinutes, apcups: this.config.apcupsMaxMinutes, ble: this.config.bleMaxMinutes, deconz: this.config.deconzMaxMinutes, ecovacsdeebot: this.config.ecovacsdeebotMaxMinutes, enocean: this.config.enoceanMaxMinutes, esphome: this.config.esphomeMaxMinutes, eusec: this.config.eusecMaxMinutes, fhemTFAsensors: this.config.fhemTFAsensorsMaxMinutes, fritzdect: this.config.fritzdectMaxMinutes, fullybrowser: this.config.fullybrowserMaxMinutes, fullybrowserV3: this.config.fullybrowserV3MaxMinutes, fullyMQTT: this.config.fullyMQTTMaxMinutes, ham: this.config.hamMaxMinutes, harmony: this.config.harmonyMaxMinutes, hmiP: this.config.hmiPMaxMinutes, hmrpc: this.config.hmrpcMaxMinutes, homeconnect: this.config.homeconnectMaxMinutes, homekitController: this.config.homekitControllerMaxMinutes, hs100: this.config.hs100MaxMinutes, hue: this.config.hueMaxMinutes, hueExt: this.config.hueextMaxMinutes, innogy: this.config.innogyMaxMinutes, jeelink: this.config.jeelinkMaxMinutes, loqedSmartLock: this.config.loqedSmartLockMaxMinutes, lupusec: this.config.lupusecMaxMinutes, maxcube: this.config.maxcubeMaxMinutes, meross: this.config.merossMaxMinutes, mihome: this.config.mihomeMaxMinutes, mihomeGW: this.config.mihomeMaxMinutes, mihomeVacuum: this.config.mihomeVacuumMaxMinutes, mqttClientZigbee2Mqtt: this.config.mqttClientZigbee2MqttMaxMinutes, mqttNuki: this.config.mqttNukiMaxMinutes, musiccast: this.config.musiccastMaxMinutes, netatmo: this.config.netatmoMaxMinutes, nukiExt: this.config.nukiextendMaxMinutes, nut: this.config.nutMaxMinutes, ping: this.config.pingMaxMinutes, proxmox: this.config.proxmoxMaxMinutes, ring: this.config.ringMaxMinutes, roomba: this.config.roombaMaxMinutes, shelly: this.config.shellyMaxMinutes, smartgarden: this.config.smartgardenMaxMinutes, sonoff: this.config.sonoffMaxMinutes, sonos: this.config.sonosMaxMinutes, sureflap: this.config.sureflapMaxMinutes, switchbotBle: this.config.switchbotMaxMinutes, tado: this.config.tadoMaxMinutes, tapo: this.config.tapoMaxMinutes, tradfri: this.config.tradfriMaxMinutes, tuya: this.config.tuyaMaxMinutes, unifi: this.config.unifiMaxMinutes, viessmann: this.config.viessmannMaxMinutes, wifilight: this.config.wifilightMaxMinutes, wled: this.config.wledMaxMinutes, xsense: this.config.xsenseMaxMinutes, yeelight: this.config.yeelightMaxMinutes, zigbee: this.config.zigbeeMaxMinutes, zigbee2MQTT: this.config.zigbee2mqttMaxMinutes, zwave2: this.config.zwaveMaxMinutes, }; try { // create list with selected adapters for monitor devices for (const [id] of Object.entries(arrApart)) { if (this.configSetAdapter[id]) { this.selAdapter.push(arrApart[id]); this.adapterSelected.push(tools.capitalize(id)); } } // Check if an adapter to monitor devices is selected. if (this.adapterSelected.length >= 1) { // show list in debug log this.log.debug(JSON.stringify(this.selAdapter)); this.log.info(`Number of selected adapters to monitor devices: ${this.adapterSelected.length}. Loading data from: ${this.adapterSelected.join(', ')} ...`); } else { this.log.info(`No adapters selected to monitor devices.`); } // create Blacklist await crud.createBlacklist(this); // create user defined list with time of error for instances await crud.createTimeListInstances(this); //create datapoints for each adapter if selected for (const [id] of Object.entries(arrApart)) { try { if (!this.configCreateOwnFolder) { await crud.deleteDPsForEachAdapter(this, id); await crud.deleteHtmlListDatapoints(this, id); } else { if (this.configSetAdapter && this.configSetAdapter[id]) { await crud.createDPsForEachAdapter(this, id); // create HTML list datapoints if (!this.configCreateHtmlList) { await crud.deleteHtmlListDatapoints(this, id); } else { await crud.createHtmlListDatapoints(this, id); } this.log.debug(`Created datapoints for ${tools.capitalize(id)}`); } } } catch (error) { this.log.error(`[onReady - create and fill datapoints for each adapter] - ${error}`); } } // create HTML list datapoints if (!this.configCreateHtmlList) { await crud.deleteHtmlListDatapoints(this); await crud.deleteHtmlListDatapointsInstances(this); } else { await crud.createHtmlListDatapoints(this); if (this.config.checkAdapterInstances) await crud.createHtmlListDatapointsInstances(this); } if (!this.config.checkAdapterInstances) await crud.deleteHtmlListDatapointsInstances(this); // instances and adapters if (this.configCreateInstanceList) { // instances await crud.createDPsForInstances(this); await this.getAllInstanceData(); // adapter updates await crud.createAdapterUpdateData(this, adapterUpdateListDP); } else { await crud.deleteDPsForInstances(this); } await this.main(); // update last contact data in interval await this.refreshData(); // send overview for low battery devices if (this.config.checkSendBatteryMsgDaily) await this.sendScheduleNotifications('lowBatteryDevices'); // send overview of offline devices if (this.config.checkSendOfflineMsgDaily) await this.sendScheduleNotifications('offlineDevices'); // send overview of upgradeable devices if (this.config.checkSendUpgradeMsgDaily) await this.sendScheduleNotifications('updateDevices'); // send overview of updatable adapters if (this.config.checkSendAdapterUpdateMsgDaily) await this.sendScheduleNotifications('updateAdapter'); // send overview of deactivated instances if (this.config.checkSendInstanceDeactivatedDaily) await this.sendScheduleNotifications('deactivatedInstance'); // send overview of instances with error if (this.config.checkSendInstanceFailedDaily) await this.sendScheduleNotifications('errorInstance'); } catch (error) { this.log.error(`[onReady] - ${error}`); this.terminate ? this.terminate(15) : process.exit(15); } } // <-- onReady end /** * main function */ async main() { this.log.debug(`Function started main`); this.mainRunning = true; // cancel run if no adapter is selected if (this.adapterSelected.length === 0) return; // fill counts and lists of all selected adapter try { for (let i = 0; i < this.selAdapter.length; i++) { await crud.createData(this, i); await crud.createLists(this); } await crud.writeDatapoints(this); // fill the datapoints this.log.debug(`Created and filled data for all adapters`); } catch (error) { this.log.error(`[main - create data of all adapter] - ${error}`); } // fill datapoints for each adapter if selected if (this.configCreateOwnFolder) { try { for (const [id] of Object.entries(arrApart)) { if (this.configSetAdapter && this.configSetAdapter[id]) { for (const deviceData of this.listAllDevicesRaw.values()) { // list device only if selected adapter matched with device if (!deviceData.adapterID.includes(id)) continue; await crud.createLists(this, id); } await crud.writeDatapoints(this, id); // fill the datapoints this.log.debug(`Created and filled data for ${tools.capitalize(id)}`); } } } catch (error) { this.log.error(`[main - create and fill datapoints for each adapter] - ${error}`); } } this.mainRunning = false; this.log.debug(`Function finished: ${this.main.name}`); } //<--End of main function // If you need to react to object changes, uncomment the following block and the corresponding line in the constructor. // You also need to subscribe to the objects with `this.subscribeObjects`, similar to `this.subscribeStates`. // /** * Is called if a subscribed object changes * @param {string} id * @param {ioBroker.Object | null | undefined} obj */ async onObjectChange(id, obj) { if (obj) { try { // The object was changed //this.log.debug(`object ${id} changed: ${JSON.stringify(obj)}`); if (this.config.checkAdapterInstances && id.startsWith('system.adapter.')) { //read new instance data and add it to the lists await this.getInstanceData(id); } else { if (Array.from(this.listAllDevicesRaw.values()).some((obj) => obj.mainSelector === id)) { if (!this.mainRunning) { await this.main(); } else { return; } } else { return; } } } catch (error) { this.log.error(`Issue at object change: ${error}`); } } else { try { // The object was deleted this.log.debug(`object ${id} deleted`); // delete instance data in map if (this.listInstanceRaw.has(id)) { this.listInstanceRaw.delete(id); } // delete device data in map if (this.listAllDevicesRaw.has(id)) { this.listAllDevicesRaw.delete(id); } //unsubscribe of Objects and states this.unsubscribeForeignObjects(id); this.unsubscribeForeignStates(id); } catch (error) { this.log.error(`Issue at object deletion: ${error}`); } } } /** * Is called if a subscribed state changes * @param {string} id * @param {ioBroker.State | null | undefined} state */ async onStateChange(id, state) { if (state) { // this.log.debug(`State changed: ${id} changed ${state.val}`); try { /*============================================= = Instances / Adapter = =============================================*/ if (this.config.checkAdapterInstances) { // Adapter Update data if (id.endsWith('updatesJson')) { await this.renewAdapterUpdateData(id); } // Instanz data if (Array.from(this.listInstanceRaw.values()).some((obj) => Object.values(obj).includes(id))) { await this.renewInstanceData(id, state); } } /*============================================= = Devices = =============================================*/ if (Array.from(this.listAllDevicesRaw.values()).some((obj) => Object.values(obj).includes(id))) { await this.renewDeviceData(id, state); } } catch (error) { this.log.error(`Issue at state change: ${error}`); } } else { // The state was deleted this.log.debug(`state ${id} deleted`); } } /** * @param {ioBroker.Message} obj */ onMessage(obj) { const devices = []; const instances = []; const instancesTime = []; let countDevices = 0; let countInstances = 0; switch (obj.command) { case 'devicesList': if (obj.message) { try { for (const deviceData of this.listAllDevicesRaw.values()) { const label = `${deviceData.Adapter}: ${deviceData.Device}`; const valueObjectDevices = { deviceName: deviceData.Device, adapter: deviceData.Adapter, path: deviceData.Path, }; devices[countDevices] = { label: label, value: JSON.stringify(valueObjectDevices) }; countDevices++; } const sortDevices = devices.slice(0); sortDevices.sort(function (a, b) { const x = a.label; const y = b.label; return x < y ? -1 : x > y ? 1 : 0; }); this.sendTo(obj.from, obj.command, sortDevices, obj.callback); } catch (error) { this.log.error(`[onMessage - deviceList for blacklisttable] - ${error}`); } } break; case 'instancesList': if (obj.message) { try { for (const [instance, instanceData] of this.listInstanceRaw) { const label = `${instanceData.Adapter}: ${instance}`; const valueObjectInstances = { adapter: instanceData.Adapter, instanceID: instance, }; instances[countInstances] = { label: label, value: JSON.stringify(valueObjectInstances) }; countInstances++; } const sortInstances = instances.slice(0); sortInstances.sort(function (a, b) { const x = a.label; const y = b.label; return x < y ? -1 : x > y ? 1 : 0; }); this.sendTo(obj.from, obj.command, sortInstances, obj.callback); } catch (error) { this.log.error(`[onMessage - instanceList] - ${error}`); } } break; case 'instancesListTime': if (obj.message) { try { for (const [instance, instanceData] of this.listInstanceRaw) { const label = `${instanceData.Adapter}: ${instance}`; const valueObjectInstances = { adapter: instanceData.Adapter, instanceName: instance, }; instancesTime[countInstances] = { label: label, value: JSON.stringify(valueObjectInstances) }; countInstances++; } const sortInstances = instancesTime.slice(0); sortInstances.sort(function (a, b) { const x = a.label; const y = b.label; return x < y ? -1 : x > y ? 1 : 0; }); this.sendTo(obj.from, obj.command, sortInstances, obj.callback); } catch (error) { this.log.error(`[onMessage - instanceList] - ${error}`); } } break; } } /** * refresh data with interval * is neccessary to refresh lastContact data, especially of devices without state changes */ async refreshData() { if (isUnloaded) return; // cancel run if unloaded was called. const nextTimeout = this.config.updateinterval * 1000; // devices data await tools.checkLastContact(this); await crud.createLists(this); await crud.writeDatapoints(this); // devices data in own adapter folder if (this.configCreateOwnFolder) { for (const [id] of Object.entries(arrApart)) { if (this.configSetAdapter && this.configSetAdapter[id]) { await crud.createLists(this, id); await crud.writeDatapoints(this, id); this.log.debug(`Created and filled data for ${tools.capitalize(id)}`); } } } // instance and adapter data if (this.configCreateInstanceList) { await this.createInstanceList(); await this.writeInstanceDPs(); } // Clear existing timeout if (this.refreshDataTimeout) { this.clearTimeout(this.refreshDataTimeout); this.refreshDataTimeout = null; } this.refreshDataTimeout = this.setTimeout(async () => { this.log.debug('Updating Data'); await this.refreshData(); }, nextTimeout); } // <-- refreshData end /*============================================= = functions to get data = =============================================*/ /** * @param {object} id - deviceID * @param {object} i - each Device */ async getDeviceName(id, i) { try { //id = id.replace(/[\]\\[.*,;'"`<>\\\s?]/g, '-'); const currDeviceString = id.slice(0, id.lastIndexOf('.') + 1 - 1); const shortCurrDeviceString = currDeviceString.slice(0, currDeviceString.lastIndexOf('.') + 1 - 1); const shortshortCurrDeviceString = shortCurrDeviceString.slice(0, shortCurrDeviceString.lastIndexOf('.') + 1 - 1); // Get device name const deviceObject = await this.getForeignObjectAsync(currDeviceString); const shortDeviceObject = await this.getForeignObjectAsync(shortCurrDeviceString); const shortshortDeviceObject = await this.getForeignObjectAsync(shortshortCurrDeviceString); let deviceName; let folderName; let deviceID; switch (this.selAdapter[i].adapterID) { case 'fullybrowser': deviceName = (await tools.getInitValue(this,currDeviceString + this.selAdapter[i].id)) + ' ' + (await tools.getInitValue(this,currDeviceString + this.selAdapter[i].id2)); break; // Get ID with short currDeviceString from objectjson case 'hueExt': case 'hmrpc': case 'nukiExt': case 'wled': case 'mqttNuki': case 'loqedSmartLock': case 'viessmann': case 'homekitController': case 'ring': if (shortDeviceObject && typeof shortDeviceObject === 'object' && shortDeviceObject.common) { deviceName = shortDeviceObject.common.name; } break; // Get ID with short short currDeviceString from objectjson (HMiP Devices) case 'hmiP': if (shortshortDeviceObject && typeof shortshortDeviceObject === 'object' && shortshortDeviceObject.common) { deviceName = shortshortDeviceObject.common.name; } break; // Get ID with short currDeviceString from datapoint case 'mihomeVacuum': case 'roomba': folderName = shortCurrDeviceString.slice(shortCurrDeviceString.lastIndexOf('.') + 1); deviceID = await tools.getInitValue(this,shortCurrDeviceString + this.selAdapter[i].id); deviceName = `I${folderName} ${deviceID}`; break; //Get ID of foldername case 'tado': case 'wifilight': case 'fullybrowserV3': case 'sonoff': deviceName = currDeviceString.slice(currDeviceString.lastIndexOf('.') + 1); break; // Format Device name case 'sureflap': if (deviceObject && typeof deviceObject === 'object' && deviceObject.common) { deviceName = deviceObject.common.name // @ts-ignore FIXME: fix syntax error .replace(/'/g, '') .replace(/\(\d+\)/g, '') .trim() .replace('Hub', 'Hub -') .replace('Device', 'Device -'); } break; //Get ID of foldername case 'yeelight': deviceName = shortCurrDeviceString.slice(shortCurrDeviceString.lastIndexOf('.') + 1); break; // Get ID with main selektor from objectjson default: if (this.selAdapter[i].id !== 'none' || this.selAdapter[i].id !== undefined) deviceName = await tools.getInitValue(this,currDeviceString + this.selAdapter[i].id); if (deviceName === null || deviceName === undefined) { if (deviceObject && typeof deviceObject === 'object' && deviceObject.common) { deviceName = deviceObject.common.name; } } break; } return deviceName; } catch (error) { this.log.error(`[getDeviceName] - ${error}`); } } /** * calculate Signalstrength * @param {object} deviceQualityState - State value * @param {object} adapterID - adapter name */ async calculateSignalStrength(deviceQualityState, adapterID) { let linkQuality; let linkQualityRaw; let mqttNukiValue; if (deviceQualityState != null) { const { val } = deviceQualityState; if (typeof val === 'number') { if (this.config.trueState) { linkQuality = val; } else { switch (adapterID) { case 'roomba': case 'sonoff': case 'smartgarden': linkQuality = `${val}%`; linkQualityRaw = val; break; case 'lupusec': case 'fullybrowserV3': linkQuality = val; break; default: if (val <= -255) { linkQuality = ' - '; } else if (val < 0) { linkQualityRaw = Math.min(Math.max(2 * (val + 100), 0), 100); linkQuality = `${linkQualityRaw}%`; } else if (val >= 0) { linkQualityRaw = parseFloat(((100 / 255) * val).toFixed(0)); linkQuality = `${linkQualityRaw}%`; } break; } } } else if (typeof val === 'string') { switch (adapterID) { case 'netatmo': linkQuality = val; break; case 'nukiExt': linkQuality = ' - '; break; case 'mqttNuki': linkQuality = val; mqttNukiValue = parseInt(linkQuality); if (this.config.trueState) { linkQuality = val; } else if (mqttNukiValue < 0) { linkQualityRaw = Math.min(Math.max(2 * (mqttNukiValue + 100), 0), 100); linkQuality = `${linkQualityRaw}%`; } break; } } } else { linkQuality = ' - '; } return [linkQuality, linkQualityRaw]; } /** * get battery data * @param {object} deviceBatteryState - State value * @param {object} deviceLowBatState - State value * @param {object} faultReportingState - State value * @param {object} adapterID - adapter name */ async getBatteryData(deviceBatteryState, deviceLowBatState, faultReportingState, adapterID) { let batteryHealth = '-'; let isBatteryDevice = false; let batteryHealthRaw; let batteryHealthUnitRaw; switch (adapterID) { case 'hmrpc': if (deviceBatteryState === undefined) { if (faultReportingState !== undefined && faultReportingState !== 6) { batteryHealth = 'ok'; isBatteryDevice = true; } else if (deviceLowBatState !== undefined && deviceLowBatState !== 1) { batteryHealth = 'ok'; isBatteryDevice = true; } else if (deviceLowBatState !== undefined) { batteryHealth = 'low'; isBatteryDevice = true; } } else if (deviceBatteryState !== 0 && deviceBatteryState < 6) { batteryHealth = `${deviceBatteryState}V`; batteryHealthRaw = deviceBatteryState; batteryHealthUnitRaw = 'V'; isBatteryDevice = true; } break; case 'xsense': if (deviceBatteryState === undefined) { // do nothin brdge has no battery isBatteryDevice = false; } else if (deviceBatteryState >= 0) { batteryHealthRaw = deviceBatteryState; batteryHealthUnitRaw = ''; isBatteryDevice = true; switch (batteryHealthRaw) { case 1: batteryHealth = 'low'; break; case 2: batteryHealth = 'medium'; break; case 3: batteryHealth = 'ok'; break; default: batteryHealth = 'error'; } } break; default: if (deviceBatteryState === undefined) { if (deviceLowBatState !== undefined) { if (deviceLowBatState !== true && deviceLowBatState !== 'NORMAL' && deviceLowBatState !== 1) { batteryHealth = 'ok'; isBatteryDevice = true; } else if (deviceLowBatState !== true) { batteryHealth = 'low'; isBatteryDevice = true; } } } else { if (typeof deviceBatteryState === 'string') { if (deviceBatteryState === 'high' || deviceBatteryState === 'medium') { batteryHealth = 'ok'; isBatteryDevice = true; } else if (deviceBatteryState === 'low') { batteryHealth = 'low'; isBatteryDevice = true; } } else { batteryHealth = `${deviceBatteryState}%`; batteryHealthRaw = deviceBatteryState; batteryHealthUnitRaw = '%'; isBatteryDevice = true; } } break; } return [batteryHealth, isBatteryDevice, batteryHealthRaw, batteryHealthUnitRaw]; } /** * set low bat indicator * @param {object} deviceBatteryState * @param {object} deviceLowBatState * @param {object} faultReportState * @param {object} adapterID */ async setLowbatIndicator(deviceBatteryState, deviceLowBatState, faultReportState, adapterID) { let lowBatIndicator = false; if (deviceLowBatState !== undefined || faultReportState !== undefined) { switch (adapterID) { case 'hmrpc': if (deviceLowBatState === 1 || deviceLowBatState === true || faultReportState === 6) { lowBatIndicator = true; } break; default: if (typeof deviceLowBatState === 'number' && deviceLowBatState === 0) { lowBatIndicator = true; } else if (typeof deviceLowBatState === 'string' && deviceLowBatState !== 'NORMAL') { lowBatIndicator = true; } else if (typeof deviceLowBatState === 'boolean' && deviceLowBatState) { lowBatIndicator = true; } } } else if (typeof deviceBatteryState === 'number' && deviceBatteryState < this.config.minWarnBatterie) { lowBatIndicator = true; } else if (typeof deviceBatteryState === 'string' && deviceBatteryState === 'low') { lowBatIndicator = true; } return lowBatIndicator; } /** * get Last Contact * @param {object} selector - Selector */ async getLastContact(selector) { const lastContact = tools.getTimestamp(selector); let lastContactString; lastContactString = `${this.formatDate(new Date(selector), 'hh:mm')}`; if (Math.round(lastContact) > 100) { lastContactString = `${Math.round(lastContact / 60)} ${translations.hours[this.config.userSelectedLanguage]}`; } if (Math.round(lastContact / 60) > 48) { lastContactString = `${Math.round(lastContact / 60 / 24)} ${translations.days[this.config.userSelectedLanguage]}`; } return lastContactString; } /** * get online state and time * @param {object} timeSelector - device Timeselector * @param {string} adapterID - ID of Adapter * @param {string} unreachDP - Datapoint of Unreach * @param {object} linkQuality - Linkquality Value * @param {object} deviceUnreachState - State of deviceUnreach datapoint * @param {string} deviceStateSelectorHMRPC - Selector of device state (like .state) * @param {string} rssiPeerSelectorHMRPC - HM RSSI Peer Datapoint */ async getOnlineState(timeSelector, adapterID, unreachDP, linkQuality, deviceUnreachState, deviceStateSelectorHMRPC, rssiPeerSelectorHMRPC) { let lastContactString; let deviceState = 'Online'; try { const deviceTimeSelector = await this.getForeignStateAsync(timeSelector); const deviceUnreachSelector = await this.getForeignStateAsync(unreachDP); const deviceStateSelector = await this.getForeignStateAsync(deviceStateSelectorHMRPC); // for hmrpc devices const rssiPeerSelector = await this.getForeignStateAsync(rssiPeerSelectorHMRPC); const lastDeviceUnreachStateChange = deviceUnreachSelector != undefined ? tools.getTimestamp(deviceUnreachSelector.lc) : tools.getTimestamp(timeSelector.ts); // ignore disabled device from zigbee2MQTT if (adapterID === 'zigbee2MQTT') { const is_device_disabled = await tools.isDisabledDevice(this, unreachDP.substring(0, unreachDP.lastIndexOf('.'))); if (is_device_disabled) { return [null, 'disabled', '0%']; } } // If there is no contact since user sets minutes add device in offline list // calculate to days after 48 hours switch (unreachDP) { case 'none': if (deviceTimeSelector) lastContactString = await this.getLastContact(deviceTimeSelector.ts); break; default: //State changed if (adapterID === 'hmrpc') { if (linkQuality !== ' - ' && deviceTimeSelector) { if (deviceUnreachState === 1) { lastContactString = await this.getLastContact(deviceTimeSelector.lc); } else { lastContactString = await this.getLastContact(deviceTimeSelector.ts); } } else { if (deviceStateSelector) { // because old hm devices don't send rssi states lastContactString = await this.getLastContact(deviceStateSelector.ts); } else if (rssiPeerSelector) { // because old hm sensors don't send rssi/state values lastContactString = await this.getLastContact(rssiPeerSelector.ts); } } } else { if ((!deviceUnreachState || deviceUnreachState === 0) && deviceTimeSelector) { lastContactString = await this.getLastContact(deviceTimeSelector.lc); } else { if (deviceTimeSelector) lastContactString = await this.getLastContact(deviceTimeSelector.ts); } break; } } /*============================================= = Set Online Status = =============================================*/ let lastContact; if (deviceTimeSelector) lastContact = tools.getTimestamp(deviceTimeSelector.ts); if (this.configMaxMinutes !== undefined) { switch (adapterID) { case 'hmrpc': if (this.configMaxMinutes[adapterID] <= 0) { if (deviceUnreachState === 1) { deviceState = 'Offline'; //set online state to offline if (linkQuality !== ' - ') linkQuality = '0%'; // set linkQuality to nothing } } else if (lastDeviceUnreachStateChange > this.configMaxMinutes[adapterID] && deviceUnreachState === 1) { deviceState = 'Offline'; //set online state to offline if (linkQuality !== ' - ') linkQuality = '0%'; // set linkQuality to nothing } break; case 'proxmox': if (this.configMaxMinutes[adapterID] <= 0) { if (deviceUnreachState !== 'running' && deviceUnreachState !== 'online') { deviceState = 'Offline'; //set online state to offline if (linkQuality !== ' - ') linkQuality = '0%'; // set linkQuality to nothing } } else if (lastDeviceUnreachStateChange > this.configMaxMinutes[adapterID] && deviceUnreachState !== 'running' && deviceUnreachState !== 'online') { deviceState = 'Offline'; //set online state to offline if (linkQuality !== ' - ') linkQuality = '0%'; // set linkQuality to nothing } break; case 'hmiP': case 'maxcube': if (this.configMaxMinutes[adapterID] <= 0) { if (deviceUnreachState) { deviceState = 'Offline'; //set online state to offline if (linkQuality !== ' - ') linkQuality = '0%'; // set linkQuality to nothing } } else if (lastDeviceUnreachStateChange > this.configMaxMinutes[adapterID] && deviceUnreachState) { deviceState = 'Offline'; //set online state to offline if (linkQuality !== ' - ') linkQuality = '0%'; // set linkQuality to nothing } break; case 'apcups': case 'hue': case 'hueExt': case 'ping': case 'deconz': case 'shelly': case 'sonoff': case 'tradfri': case 'unifi': case 'zigbee': case 'zigbee2MQTT': if (this.configMaxMinutes[adapterID] <= 0) { if (!deviceUnreachState) { deviceState = 'Offline'; //set online state to offline if (linkQuality !== ' - ') linkQuality = '0%'; // set linkQuality to nothing } } else if (!deviceUnreachState && lastDeviceUnreachStateChange > this.configMaxMinutes[adapterID]) { deviceState = 'Offline'; //set online state to offline if (linkQuality !== ' - ') linkQuality = '0%'; // set linkQuality to nothing } break; case 'mqttClientZigbee2Mqtt': if (this.configMaxMinutes[adapterID] <= 0) { if (deviceUnreachState !== 'online') { deviceState = 'Offline'; //set online state to offline if (linkQuality !== ' - ') linkQuality = '0%'; // set linkQuality to nothing } } else if (deviceUnreachState !== 'online' && lastDeviceUnreachStateChange > this.configMaxMinutes[adapterID]) { deviceState = 'Offline'; //set online state to offline if (linkQuality !== ' - ') linkQuality = '0%'; // set linkQuality to nothing } break; case 'mihome': if (deviceUnreachState !== undefined) { if (this.configMaxMinutes[adapterID] <= 0) { if (!deviceUnreachState) { deviceState = 'Offline'; //set online state to offline if (linkQuality !== ' - ') linkQuality = '0%'; // set linkQuality to nothing } } else if (lastContact && lastContact > this.configMaxMinutes[adapterID]) { deviceState = 'Offline'; //set online state to offline if (linkQuality !== ' - ') linkQuality = '0%'; // set linkQuality to nothing } } else { if (this.config.mihomeMaxMinutes <= 0) { if (this.configMaxMinutes[adapterID] <= 0) { deviceState = 'Offline'; //set online state to offline if (linkQuality !== ' - ') linkQuality = '0%'; // set linkQuality to nothing } } else if (lastContact && lastContact > this.configMaxMinutes[adapterID]) { deviceState = 'Offline'; //set online state to offline if (linkQuality !== ' - ') linkQuality = '0%'; // set linkQuality to nothing } } break; case 'smartgarden': if (this.configMaxMinutes[adapterID] <= 0) { if (deviceUnreachState === 'OFFLINE') { deviceState = 'Offline'; //set online state to offline if (linkQuality !== ' - ') linkQuality = '0%'; // set linkQuality to nothing } } else if (deviceUnreachState === 'OFFLINE' && lastDeviceUnreachStateChange > this.configMaxMinutes[adapterID]) { deviceState = 'Offline'; //set online state to offline if (linkQuality !== ' - ') linkQuality = '0%'; // set linkQuality to nothing } break; default: if (this.configMaxMinutes[adapterID] <= 0) { if (!deviceUnreachState) { deviceState = 'Offline'; //set online state to offline if (linkQuality !== ' - ') linkQuality = '0%'; // set linkQuality to nothing } } else if (lastContact && lastContact > this.configMaxMinutes[adapterID]) { deviceState = 'Offline'; //set online state to offline if (linkQuality !== ' - ') linkQuality = '0%'; // set linkQuality to nothing } break; } } return [lastContactString, deviceState, linkQuality]; } catch (error) { this.log.error(`[getLastContact] - ${error}`); } } /** * @param {any} adapterID * @param {string | number | boolean | null} deviceUpdateSelector */ async checkDeviceUpdate(adapterID, deviceUpdateSelector) { let isUpgradable; switch (adapterID) { case 'hmiP': if (deviceUpdateSelector === 'UPDATE_AVAILABLE') { isUpgradable = true; } else { isUpgradable = false; } break; case 'ring': if (deviceUpdateSelector !== 'Up to Date') { isUpgradable = true; } else { isUpgradable = false; } break; default: if (deviceUpdateSelector !== null && typeof deviceUpdateSelector === 'boolean') { if (deviceUpdateSelector) { isUpgradable = true; } else if (!deviceUpdateSelector) { isUpgradable = false; } } } return isUpgradable; } /** * fill the lists for user * @param {object} device */ async theLists(device) { // Raw List with all devices for user if (device.Status !== 'disabled') { this.listAllDevicesUserRaw.push({ Device: device.Device, Adapter: device.Adapter, Instance: device.instance, 'Instance connected': device.instancedeviceConnected, isBatteryDevice: device.isBatteryDevice, Battery: device.Battery, BatteryRaw: device.BatteryRaw, BatteryUnitRaw: device.BatteryUnitRaw, isLowBat: device.LowBat, 'Signal strength': device.SignalStrength, 'Signal strength Raw': device.SignalStrengthRaw, 'Last contact': device.LastContact, 'Update Available': device.Upgradable, Status: device.Status, }); // List with all devices this.listAllDevices.push({ [translations.Device[this.config.userSelectedLanguage]]: device.Device, [translations.Adapter[this.config.userSelectedLanguage]]: device.Adapter, [translations.Battery[this.config.userSelectedLanguage]]: device.Battery, [translations.Signal_strength[this.config.userSelectedLanguage]]: device.SignalStrength, [translations.Last_Contact[this.config.userSelectedLanguage]]: device.LastContact, [translations.Status[this.config.userSelectedLanguage]]: device.Status, }); // LinkQuality lists if (device.SignalStrength != ' - ') { this.linkQualityDevices.push({ [translations.Device[this.config.userSelectedLanguage]]: device.Device, [translations.Adapter[this.config.userSelectedLanguage]]: device.Adapter, [translations.Signal_strength[this.config.userSelectedLanguage]]: device.SignalStrength, }); } // Battery lists if (device.isBatteryDevice) { this.batteryPowered.push({ [translations.Device[this.config.userSelectedLanguage]]: device.Device, [translations.Adapter[this.config.userSelectedLanguage]]: device.Adapter, [translations.Battery[this.config.userSelectedLanguage]]: device.Battery, [translations.Status[this.config.userSelectedLanguage]]: device.Status, }); } // Low Bat lists if (device.LowBat && device.Status !== 'Offline') { this.batteryLowPowered.push({ [translations.Device[this.config.userSelectedLanguage]]: device.Device, [translations.Adapter[this.config.userSelectedLanguage]]: device.Adapter, [translations.Battery[this.config.userSelectedLanguage]]: device.Battery, }); } // Offline List if (device.Status === 'Offline') { this.offlineDevices.push({ [translations.Device[this.config.userSelectedLanguage]]: device.Device, [translations.Adapter[this.config.userSelectedLanguage]]: device.Adapter, [translations.Last_Contact[this.config.userSelectedLanguage]]: device.LastContact, }); } // Device update List if (device.Upgradable === true || device.Upgradable === 1) { this.upgradableList.push({ [translations.Device[this.config.userSelectedLanguage]]: device.Device, [translations.Adapter[this.config.userSelectedLanguage]]: device.Adapter, }); } } } /** * @param {string | string[]} id * @param {ioBroker.State} state */ async renewDeviceData(id, state) { let batteryData; let signalData; let oldLowBatState; let contactData; let oldStatus; let isLowBatValue; const deviceID = id.slice(0, id.lastIndexOf('.') + 1 - 1); const deviceData = this.listAllDevicesRaw.get(deviceID); if (deviceData) { // On statechange update available datapoint switch (id) { // device connection case deviceData.instanceDeviceConnectionDP: if (state.val !== deviceData.instancedeviceConnected) { deviceData.instancedeviceConnected = state.val; } break; // device updates case deviceData.UpdateDP: if (state.val !== deviceData.Upgradable) { deviceData.Upgradable = await this.checkDeviceUpdate(deviceData.adapterID, state.val); if (deviceData.Upgradable === true) { if (this.config.checkSendDeviceUpgrade && !this.blacklistNotify.includes(deviceData.Path)) { await this.sendStateNotifications('Devices', 'updateDevice', deviceID); } } } break; // device signal case deviceData.SignalStrengthDP: signalData = await this.calculateSignalStrength(state, deviceData.adapterID); deviceData.SignalStrength = signalData[0]; break; // device battery case deviceData.batteryDP: if (deviceData.isBatteryDevice) { oldLowBatState = deviceData.LowBat; if (state.val === 0 && deviceData.BatteryRaw >= 5) return; batteryData = await this.getBatteryData(state.val, oldLowBatState, deviceData.faultReport, deviceData.adapterID); deviceData.Battery = batteryData[0]; deviceData.BatteryRaw = batteryData[2]; deviceData.BatteryUnitRaw = batteryData[3]; if (deviceData.LowBatDP !== 'none') { isLowBatValue = await tools.getInitValue(this,deviceData.LowBatDP); } else { isLowBatValue = undefined; } deviceData.LowBat = await this.setLowbatIndicator(state.val, isLowBatValue, deviceData.faultReport, deviceData.adapterID); if (deviceData.LowBat && oldLowBatState !== deviceData.LowBat) { if (this.config.checkSendBatteryMsg && !this.blacklistNotify.includes(deviceData.Path)) { await this.sendStateNotifications('Devices', 'lowBatDevice', deviceID); } } } break; // device low bat case deviceData.LowBatDP: if (deviceData.isBatteryDevice) { oldLowBatState = deviceData.LowBat; batteryData = await this.getBatteryData(deviceData.BatteryRaw, state.val, deviceData.faultReport, deviceData.adapterID); deviceData.Battery = batteryData[0]; deviceData.BatteryRaw = batteryData[2]; deviceData.BatteryUnitRaw = batteryData[3]; deviceData.LowBat = await this.setLowbatIndicator(deviceData.BatteryRaw, state.val, deviceData.faultReport, deviceData.adapterID); if (deviceData.LowBat && oldLowBatState !== deviceData.LowBat) { if (this.config.checkSendBatteryMsg && !this.blacklistNotify.includes(deviceData.Path)) { await this.sendStateNotifications('Devices', 'lowBatDevice', deviceID); } } } break; //device error / fault reports case deviceData.faultReportDP: if (deviceData.isBatteryDevice) { oldLowBatState = deviceData.LowBat; batteryData = await this.getBatteryData(deviceData.BatteryRaw, oldLowBatState, state.val, deviceData.adapterID); deviceData.Battery = batteryData[0]; deviceData.BatteryRaw = batteryData[2]; deviceData.BatteryUnitRaw = batteryData[3]; deviceData.LowBat = await this.setLowbatIndicator(deviceData.BatteryRaw, undefined, state.val, deviceData.adapterID); if (deviceData.LowBat && oldLowBatState !== deviceData.LowBat) { if (this.config.checkSendBatteryMsg && !this.blacklistNotify.includes(deviceData.Path)) { await this.sendStateNotifications('Devices', 'lowBatDevice', deviceID); } } } break; // device unreach case deviceData.UnreachDP: if (deviceData.instancedeviceConnected !== undefined) { if (deviceData.UnreachState !== state.val) { oldStatus = deviceData.Status; deviceData.UnreachState = state.val; contactData = await this.getOnlineState( deviceData.timeSelector, deviceData.adapterID, deviceData.UnreachDP, deviceData.SignalStrength, deviceData.UnreachState, deviceData.deviceStateSelectorHMRPC, deviceData.rssiPeerSelectorHMRPC, ); if (contactData !== undefined && contactData !== null) { deviceData.LastContact = contactData[0]; deviceData.Status = contactData[1]; deviceData.SignalStrength = contactData[2]; } if (this.config.checkSendOfflineMsg && oldStatus !== deviceData.Status && !this.blacklistNotify.includes(deviceData.Path)) { // check if the generally deviceData connected state is for a while true if (await tools.getTimestampConnectionDP(this, deviceData.instanceDeviceConnectionDP, 50000)) { await this.sendStateNotifications('Devices', 'onlineStateDevice', deviceID); } } } } break; } } } /** * get all Instances at start */ async getAllInstanceData() { try { const allInstances = `system.adapter.*`; await this.getInstanceData(allInstances); } catch (error) { this.log.error(`[getInstance] - ${error}`); } } /** * get instance data *@param {string} instanceObject */ async getInstanceData(instanceObject) { try { const instanceAliveDP = await this.getForeignStatesAsync(`${instanceObject}.alive`); this.adapterUpdatesJsonRaw = await this.getAdapterUpdateData(adapterUpdateListDP); for (const [id] of Object.entries(instanceAliveDP)) { if (!(typeof id === 'string' && id.startsWith(`system.adapter.`))) continue; // get instance name const instanceID = await this.getInstanceName(id); // get instance connected to host data const instanceConnectedHostDP = `system.adapter.${instanceID}.connected`; const instanceConnectedHostVal = await tools.getInitValue(this,instanceConnectedHostDP); // get instance connected to device data const instanceConnectedDeviceDP = `${instanceID}.info.connection`; let instanceConnectedDeviceVal; if (instanceConnectedDeviceDP !== undefined && typeof instanceConnectedDeviceDP === 'boolean') { instanceConnectedDeviceVal = await tools.getInitValue(this,instanceConnectedDeviceDP); } else { instanceConnectedDeviceVal = 'N/A'; } // get adapter version const instanceObjectPath = `system.adapter.${instanceID}`; let adapterName; let adapterVersion; let adapterAvailableUpdate = ''; let instanceMode; let scheduleTime = 'N/A'; const instanceObjectData = await this.getForeignObjectAsync(instanceObjectPath); if (instanceObjectData) { // @ts-ignore adapterName = tools.capitalize(instanceObjectData.common.name); adapterVersion = instanceObjectData.common.version; instanceMode = instanceObjectData.common.mode; if (instanceMode === 'schedule') { scheduleTime = instanceObjectData.common.schedule; } } const updateEntry = this.adapterUpdatesJsonRaw.find( entry => entry.adapter.toLowerCase() === adapterName.toLowerCase() ); if (updateEntry) { adapterAvailableUpdate = updateEntry.newVersion; } else { adapterAvailableUpdate = ' - '; } let isAlive; let isHealthy; let instanceStatus; if (instanceMode === 'schedule') { const instanceStatusRaw = await this.checkScheduleisHealty(instanceID, scheduleTime); isAlive = instanceStatusRaw[0]; isHealthy