UNPKG

iobroker.onlycat

Version:

Adapter for OnlyCat cat flaps with prey detection

1,234 lines (1,170 loc) 113 kB
/********************* * * * OnlyCat Adapter * * * *********************/ 'use strict'; /* * Created with @iobroker/create-adapter v2.6.5 */ // 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, e.g.: const OnlyCatApi = require('./lib/onlycat-api'); // Constants // Adapter version const ADAPTER_VERSION = '0.5.2'; // Reconnect frequency const RETRY_FREQUENCY_CONNECT = 60; // Minimum Event Update frequency const MINIMUM_EVENT_UPDATE_FREQUENCY = 1; // Maximum Event Updates const MAX_EVENT_UPDATE = 10; // Event Trigger const EVENT_TRIGGER_SOURCE = { 0: 'MANUAL', 1: 'REMOTE', 2: 'INDOOR_MOTION', 3: 'OUTDOOR_MOTION' }; // Event Classification const EVENT_CLASSIFICATION = { 0: 'UNKNOWN', 1: 'CLEAR', 2: 'SUSPICIOUS', 3: 'CONTRABAND', 4: 'HUMAN_ACTIVITY', 10: 'REMOTE_UNLOCK', }; // Event Type generated from Trigger + Classification const EVENT_TYPE = { MANUAL: 0, REMOTE: 1, EXIT: 2, ENTRY: 3, CONTRABAND: 4 }; const EVENT_TYPE_NAME = { 0: 'manual', 1: 'remote', 2: 'exit', 3: 'entry', 4: 'contraband' }; const EVENT_TYPE_MAX = 5; class Template extends utils.Adapter { /** * @param {Partial<utils.AdapterOptions>} [options] adapter options */ constructor(options) { super({ ...options, name: 'onlycat', }); this.api = new OnlyCatApi(this); this.connectionStatusSubscription = undefined; this.userSubscription = undefined; // class variables // reconnect timer this.reconnectTimerId = undefined; // event update timer this.eventUpdateTimerId = undefined; // event update counter this.eventUpdateCounter = 0; // adapter unloaded indicator this.adapterUnloaded = false; // last error this.lastError = undefined; // is automatic reconnecting this.reconnecting = false; /* current and previous data from OnlyCat API */ // list of devices this.devices = undefined; // list of RFIDs this.rfids = []; // list of RFID profiles this.rfidProfiles = {}; // list of transit policy IDs this.transitPolicyIds = []; // list of transit policies this.transitPolicies = []; // list of events this.events = undefined; // list of previous events this.lastEvents = undefined; // current user this.currentUser = undefined; 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)); } /** * Is called when databases are connected and adapter received configuration. */ async onReady() { // Reset the connection indicator during startup this.setConnectionStatusToAdapter(false); // check adapter config for invalid values this.checkAdapterConfig(); // subscribe to control state changes this.subscribeStates('control.*'); this.subscribeStates('*.control.*'); // connect to OnlyCat API via socket.io and retrieve data this.log.debug(`Starting OnlyCat Adapter v${ADAPTER_VERSION}`); this.connectToApiAndStartRetrievingData(); } /** * Is called when adapter shuts down - callback has to be called under any circumstances! * * @param {() => void} callback method to be called on unload */ onUnload(callback) { try { this.adapterUnloaded = true; this.unsubscribeEvents(); this.clearReconnectTimer(); this.clearEventUpdateTimer(); this.clearSubscriptions(); this.api.closeConnection(); this.setConnectionStatusToAdapter(false); this.log.info(`everything cleaned up`); } catch (e) { this.log.warn(`adapter clean up failed: ${e}`); } finally { callback(); } } // 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 // */ // onObjectChange(id, obj) { // if (obj) { // // The object was changed // this.log.info(`object ${id} changed: ${JSON.stringify(obj)}`); // } else { // // The object was deleted // this.log.info(`object ${id} deleted`); // } // } /** * Is called if a subscribed state changes. * * @param {string} id the id of the changed state * @param {ioBroker.State | null | undefined} state the new state value */ onStateChange(id, state) { if (id && state && state.ack === false) { const pathElements = id.split('.'); const group = pathElements[pathElements.length - 2]; const control = pathElements[pathElements.length - 1]; if (group === 'control') { if (control === 'disconnect') { this.log.info(`Disconnect Button pressed: ${state.val}`); this.api.disconnectSocket(); } else if (control === 'reconnect') { this.log.info(`Reconnect Button pressed: ${state.val}`); this.api.disconnectEngine(); } else if (control === 'getEvents') { this.log.info(`GetEvents Button pressed: ${state.val}`); this.getAndUpdateEvents(); } else if (control === 'deviceTransitPolicyId' && pathElements.length > 2) { const deviceName = pathElements[pathElements.length - 3]; const deviceIndex = this.getDeviceIndexForDeviceDescription(deviceName); if (deviceIndex !== undefined) { if (typeof state.val === 'number') { this.setTransitPolicyForDevice(this.devices[deviceIndex].deviceId, state.val).catch( error => { this.log.error(error); this.resetTransitPolicyForDevice(deviceIndex); }, ); } } } } } } // If you need to accept messages in your adapter, uncomment the following block and the corresponding line in the constructor. // /** // * Some message was sent to this instance over message box. Used by email, pushover, text2speech, ... // * Using this method requires "common.messagebox" property to be set to true in io-package.json // * @param {ioBroker.Message} obj // */ // onMessage(obj) { // if (typeof obj === 'object' && obj.message) { // if (obj.command === 'send') { // // e.g. send email or pushover or whatever // this.log.info('send command'); // // Send response in callback if required // if (obj.callback) this.sendTo(obj.from, obj.command, 'Message received', obj.callback); // } // } // } /******************************************* * methods to communicate with OnlyCat API * *******************************************/ /** * Starts loading data from the OnlyCat API. */ connectToApiAndStartRetrievingData() { this.clearReconnectTimer(); this.clearEventUpdateTimer(); this.setConnectionStatusToAdapter(false); this.log.info(`Connecting...`); this.connectToApi() .then(() => this.getDevices()) .then(() => this.getDevicesDetails()) .then(() => this.getRfids()) .then(() => this.getRfidProfiles()) .then(() => this.getEvents()) .then(() => this.getTransitPolicyIds()) .then(() => this.getTransitPolicies()) .then(() => this.createAdapterObjectHierarchy()) .then(() => this.updateDevices()) .then(() => this.updateEvents()) .then(() => this.updateLatestEvents()) .then(() => this.updateTransitPolicies()) .then(() => this.updateAdapterVersion()) .then(() => this.subscribeEvents()) .catch(error => { if (error === undefined || error.message === undefined || error.message === this.lastError) { this.log.debug(error); } else { this.log.error(error); this.lastError = error.message; } this.setConnectionStatusToAdapter(false); this.log.info(`Disconnected.`); this.reconnectToApi(); }); } /** * Initializes the connection to OnlyCat API. * * @returns {Promise<void>} */ connectToApi() { return new Promise((resolve, reject) => { // set connection state to STARTING this.api.prepareConnection(); const connectingSubscription = this.api.connectionState$.subscribe(connectionState => { if (connectionState === this.api.ConnectionState.Disconnected) { connectingSubscription.unsubscribe(); this.log.debug(`New initial connection state: '${connectionState}'`); this.api.closeConnection(); reject(`Connection to OnlyCat API failed.`); } if (connectionState === this.api.ConnectionState.Connected) { connectingSubscription.unsubscribe(); this.setConnectionStatusToAdapter(true); this.log.info(`Connected.`); this.resetEventUpdateCounter(); this.clearConnectionStateSubscription(); this.clearUserSubscription(); this.connectionStatusSubscription = this.api.connectionState$.subscribe(connectionState => this.onConnectionStateChange(connectionState), ); this.userSubscription = this.api.user$.subscribe(user => this.onUserChange(user)); resolve(); } else { this.log.debug(`New initial connection state: '${connectionState}'`); } }); this.api.initConnection(); }); } /** * Reconnects to OnlyCat API after a disconnect. */ reconnectToApi() { if (!this.adapterUnloaded) { if (this.api.isReconnecting()) { this.log.info(`Automatic Reconnecting is active.`); //this.log.info(`Setting reconnecting to 'true'.`); this.reconnecting = true; } else { this.clearReconnectTimer(); this.resetEventUpdateCounter(); this.clearConnectionStateSubscription(); this.currentUser = undefined; this.api.closeConnection(); this.log.info(`Reconnecting in ${RETRY_FREQUENCY_CONNECT} seconds.`); this.reconnectTimerId = this.setTimeout( this.connectToApiAndStartRetrievingData.bind(this), RETRY_FREQUENCY_CONNECT * 1000, ); } } } /** * Subscribe to events. * * @returns {Promise<void>} */ subscribeEvents() { return new Promise(resolve => { this.log.debug(`Subscribing to events...`); this.api.subscribeToEvent('userEventUpdate', data => this.onEventUpdateReceived(data)); this.api.subscribeToEvent('deviceUpdate', data => this.onDeviceUpdateReceived(data)); this.log.debug(`Events subscribed.`); return resolve(); }); } /** * Unsubscribe from events. */ unsubscribeEvents() { this.log.debug(`Unsubscribing from events...`); this.api.unsubscribeFromEvent('userEventUpdate'); this.api.unsubscribeFromEvent('deviceUpdate'); this.log.debug(`Events unsubscribed.`); } /** * User change handler. * * @param {any} user the new user */ onUserChange(user) { if (user !== undefined) { this.log.debug(`User changed${user.id ? ` for user: ${user.id}` : ''}.`); if (this.currentUser !== undefined) { this.log.debug(`User changed, getting Events.`); this.resetEventUpdateCounter(); this.getAndUpdateEvents(); this.getAndUpdateDevicesAndTransitPolicies(); } this.currentUser = user; } } /** * Connection state change handler. * * @param {string} connectionState the new connection state */ onConnectionStateChange(connectionState) { this.log.debug(`New connection state: '${connectionState}'`); if (connectionState === this.api.ConnectionState.Connected) { this.clearReconnectTimer(); if (this.reconnecting) { this.reconnecting = false; } } if (connectionState === this.api.ConnectionState.Disconnected) { this.log.info(`Disconnected.`); this.clearEventUpdateTimer(); this.reconnectToApi(); } } /** * Handles received event updates. * * @param {any} data the received event data */ onEventUpdateReceived(data) { this.log.debug(`Received event update.`); this.log.silly(`Received event update: ${JSON.stringify(data)}`); this.resetEventUpdateCounter(); this.getAndUpdateEvents(); } /** * Handles received device updates. * * @param {any} data the received device data */ onDeviceUpdateReceived(data) { this.log.debug(`Received device update.`); this.log.silly(`Received device update: ${JSON.stringify(data)}`); let deviceIds = []; if (Array.isArray(data)) { for (let d = 0; d < data.length; d++) { if ('deviceId' in data[d]) { deviceIds.push(data[d].deviceId); } } } this.getAndUpdateDevicesAndTransitPoliciesForDeviceIds( deviceIds.length !== 0 ? deviceIds : this.getAllDeviceIds(), ); } /** * Handles event update timer. */ onEventUpdateTimer() { this.log.debug(`Event update timer triggered.`); this.getAndUpdateEvents(); } /** * Gets and updates events. */ getAndUpdateEvents() { this.getEvents() .then(() => this.updateEvents()) .then(() => this.updateLatestEvents()) .catch(error => { if (error === undefined || error.message === undefined || error.message === this.lastError) { this.log.debug(error); } else { this.log.error(error); this.lastError = error.message; } this.log.warn(`Event update failed.`); }); } /** * Gets and updates all devices and transit policies. */ getAndUpdateDevicesAndTransitPolicies() { this.getAndUpdateDevicesAndTransitPoliciesForDeviceIds(this.getAllDeviceIds()); } /** * Gets and updates the given devices and their transit policies. * * @param {Array} deviceIds an array of device IDs */ getAndUpdateDevicesAndTransitPoliciesForDeviceIds(deviceIds) { this.getDevices() .then(() => this.getDevicesDetailsForDeviceIds(deviceIds)) .then(() => this.getTransitPolicies()) .then(() => this.updateDevicesForDeviceIds(deviceIds)) .then(() => this.updateTransitPoliciesForDeviceIds(deviceIds)) .catch(error => { this.log.error(error); this.log.warn(`Device and transit policy update failed.`); }); } /** * Get devices from OnlyCat API. * * @returns {Promise<void>} */ getDevices() { return new Promise((resolve, reject) => { if (this.adapterUnloaded) { reject(`Can not get devices, adapter already unloaded.`); } else { this.log.debug(`Getting devices...`); this.api .request('getDevices', { subscribe: true }) .then(response => { this.devices = response.filter(device => { if (!device.deviceId) { this.log.error( `Received device without deviceId, ignoring device: ${JSON.stringify(device)}`, ); return false; } return true; }); for (const device of this.devices) { if ( !('description' in device) || device.description === null || device.description === '' || this.normalizeString(device.description) === '' ) { this.log.error( `Device with ID '${device.deviceId}' does not have a valid description (device name). Falling back to device ID.`, ); device.description = device.deviceId; } device.description_org = device.description; device.description = this.normalizeString(device.description); if (device.description_org !== device.description) { this.log.debug( `Normalizing device name from: '${device.description_org}' to '${device.description}'`, ); } } this.log.debug( this.devices.length === 1 ? `Got 1 device.` : `Got ${this.devices.length} devices.`, ); this.log.silly(`Getting devices response: '${JSON.stringify(response)}'.`); return resolve(); }) .catch(error => { reject(error); }); } }); } /** * Get devices details from OnlyCat API. * * @returns {Promise<void>} */ getDevicesDetails() { return this.getDevicesDetailsForDeviceIds(this.getAllDeviceIds()); } /** * Get devices details for the given device IDs from OnlyCat API. * * @param {Array} deviceIds an array of device IDs * @returns {Promise<void>} */ getDevicesDetailsForDeviceIds(deviceIds) { return new Promise((resolve, reject) => { if (this.adapterUnloaded) { reject(`Can not get devices details, adapter already unloaded.`); } else { const promiseArray = []; this.log.debug(`Getting devices details...`); for (let d = 0; d < this.devices.length; d++) { if (deviceIds.includes(this.devices[d].deviceId)) { promiseArray.push(this.getDeviceDetailsForDevice(d)); } } Promise.all(promiseArray) .then(() => { this.log.debug( this.devices.length === 1 ? `Got 1 device details.` : `Got ${deviceIds.length} device details.`, ); return resolve(); }) .catch(error => { this.log.warn(`Could not get device details (${error}).`); return reject(); }); } }); } /** * Get device details for a device from OnlyCat API. * * @param {number} deviceIndex a device index within this.devices * @returns {Promise<void>} */ getDeviceDetailsForDevice(deviceIndex) { return new Promise((resolve, reject) => { const deviceId = this.devices[deviceIndex].deviceId; if (this.adapterUnloaded) { reject(`Can not get device details for device ID '${deviceId}', adapter already unloaded.`); } else { this.log.debug(`Getting device details for device ID '${deviceId}'...`); this.api .request('getDevice', { deviceId: deviceId, subscribe: true }) .then(response => { if ('firmwareChannel' in response) { this.devices[deviceIndex].firmwareChannel = response.firmwareChannel; } if ('connectivity' in response) { this.devices[deviceIndex].connectivity = response.connectivity; } this.log.silly( `Getting device details for device ID '${deviceId}' response: '${JSON.stringify(response)}'.`, ); return resolve(); }) .catch(error => { reject(error); }); } }); } /** * Get RFIDs from OnlyCat API. * * @returns {Promise<void>} */ getRfids() { return new Promise((resolve, reject) => { if (this.adapterUnloaded) { reject(`Can not get RFIDs, adapter already unloaded.`); } else { const promiseArray = []; this.rfids = []; this.log.debug(`Getting RFIDs...`); for (let d = 0; d < this.devices.length; d++) { promiseArray.push(this.getRfidsForDevice(this.devices[d].deviceId)); } Promise.all(promiseArray) .then(() => { this.log.debug(this.rfids.length === 1 ? `Got 1 RFID.` : `Got ${this.rfids.length} RFIDs.`); this.log.silly(`Getting RFIDs response: '${JSON.stringify(this.rfids)}'.`); return resolve(); }) .catch(error => { this.log.warn(`Could not get RFIDs (${error}).`); return reject(); }); } }); } /** * Get RFIDs for device from OnlyCat API. * * @param {string} deviceId a device id * @returns {Promise<void>} */ getRfidsForDevice(deviceId) { return new Promise((resolve, reject) => { if (this.adapterUnloaded) { reject(`Can not get RFIDs for device with id '${deviceId}', adapter already unloaded.`); } else { this.log.debug(`Getting RFIDs for device with id '${deviceId}'...`); this.api .request('getLastSeenRfidCodesByDevice', { deviceId: deviceId }) .then(response => { for (let r = 0; r < response.length; r++) { if ('rfidCode' in response[r]) { this.rfids.push(response[r].rfidCode); } } this.log.debug( response.length === 1 ? `Got 1 RFID for '${deviceId}'.` : `Got ${response.length} RFIDs for '${deviceId}'.`, ); this.log.silly(`Getting RFIDs response: '${JSON.stringify(response)}'.`); return resolve(); }) .catch(error => { reject(error); }); } }); } /** * Get RFID Profiles from OnlyCat API. * * @returns {Promise<void>} */ getRfidProfiles() { return new Promise((resolve, reject) => { if (this.adapterUnloaded) { reject(`Can not get RFID profiles, adapter already unloaded.`); } else { const promiseArray = []; this.rfidProfiles = {}; this.log.debug(`Getting RFID profiles...`); for (let r = 0; r < this.rfids.length; r++) { promiseArray.push(this.getRfidProfileForRfid(this.rfids[r])); } Promise.all(promiseArray) .then(() => { let profileCount = 0; for (let r = 0; r < this.rfids.length; r++) { if (this.rfids[r] in this.rfidProfiles) { profileCount++; } } this.log.debug( profileCount === 1 ? `Got 1 RFID profile.` : `Got ${profileCount} RFID profiles.`, ); this.log.silly(`Getting RFID profiles response: '${JSON.stringify(this.rfidProfiles)}'.`); return resolve(); }) .catch(error => { this.log.warn(`Could not get RFIDs (${error}).`); return reject(); }); } }); } /** * Get RFID Profile for RFID from OnlyCat API. * * @param {string} rfid a rfid * @returns {Promise<void>} */ getRfidProfileForRfid(rfid) { return new Promise((resolve, reject) => { if (this.adapterUnloaded) { reject(`Can not get RFID profile for RFID '${rfid}', adapter already unloaded.`); } else { this.log.debug(`Getting RFID profile for RFID '${rfid}'...`); this.api .request('getRfidProfile', { rfidCode: rfid }) .then(response => { this.rfidProfiles[rfid] = response; if ('label' in this.rfidProfiles[rfid]) { this.rfidProfiles[rfid].label_org = this.rfidProfiles[rfid].label; this.rfidProfiles[rfid].label = this.normalizeString(this.rfidProfiles[rfid].label); if (this.rfidProfiles[rfid].label_org !== this.rfidProfiles[rfid].label) { this.log.debug( `Normalizing pet name from: '${this.rfidProfiles[rfid].label_org}' to '${this.rfidProfiles[rfid].label}'`, ); } } this.log.debug(`Got RFID profile for RFID '${rfid}'.`); this.log.silly(`Getting RFID profile response: '${JSON.stringify(response)}'.`); return resolve(); }) .catch(error => { reject(error); }); } }); } /** * Get Transit Policy IDs from OnlyCat API. * * @returns {Promise<void>} */ getTransitPolicyIds() { return new Promise((resolve, reject) => { if (this.adapterUnloaded) { reject(`Can not get transit policy IDs, adapter already unloaded.`); } else { const promiseArray = []; this.transitPolicyIds = []; this.log.debug(`Getting transit policy IDs...`); for (let d = 0; d < this.devices.length; d++) { promiseArray.push(this.getTransitPolicyIDsForDevice(this.devices[d].deviceId)); } Promise.all(promiseArray) .then(() => { this.log.debug( this.transitPolicyIds.length === 1 ? `Got 1 transit policy ID.` : `Got ${this.transitPolicyIds.length} transit policy IDs.`, ); return resolve(); }) .catch(error => { this.log.warn(`Could not get transit policy IDs (${error}).`); return reject(); }); } }); } /** * Get Transit Policy IDs from OnlyCat API. * * @param {string} deviceId a device id * @returns {Promise<void>} */ getTransitPolicyIDsForDevice(deviceId) { return new Promise((resolve, reject) => { if (this.adapterUnloaded) { reject(`Can not get transit policy IDs, adapter already unloaded.`); } else { this.log.debug(`Getting transit policy IDs for device '${deviceId}'...`); this.api .request('getDeviceTransitPolicies', { deviceId: deviceId }) .then(response => { for (let p = 0; p < response.length; p++) { if ('deviceTransitPolicyId' in response[p]) { this.transitPolicyIds.push(response[p].deviceTransitPolicyId); } } this.log.debug( response.length === 1 ? `Got 1 transit policy ID for '${deviceId}'.` : `Got ${response.length} transit policy IDs for '${deviceId}'.`, ); this.log.silly(`Getting transit policy IDs response: '${JSON.stringify(response)}'.`); return resolve(); }) .catch(error => { reject(error); }); } }); } /** * Get Transit Policies from OnlyCat API. * * @returns {Promise<void>} */ getTransitPolicies() { return new Promise((resolve, reject) => { if (this.adapterUnloaded) { reject(`Can not get transit policies, adapter already unloaded.`); } else { const promiseArray = []; this.transitPolicies = []; this.log.debug(`Getting transit policies...`); for (let i = 0; i < this.transitPolicyIds.length; i++) { promiseArray.push(this.getTransitPolicyForPolicyID(this.transitPolicyIds[i])); } Promise.all(promiseArray) .then(() => { this.log.debug( this.transitPolicyIds.length === 1 ? `Got 1 transit policy ID.` : `Got ${this.transitPolicyIds.length} transit policy IDs.`, ); return resolve(); }) .catch(error => { this.log.warn(`Could not get transit policy IDs (${error}).`); return reject(); }); } }); } /** * Get Transit Policy from OnlyCat API. * * @param {number} deviceTransitPolicyId a transit policy ID * @returns {Promise<void>} */ getTransitPolicyForPolicyID(deviceTransitPolicyId) { return new Promise((resolve, reject) => { if (this.adapterUnloaded) { reject(`Can not get transit policy, adapter already unloaded.`); } else { this.log.debug(`Getting transit policy for transit policy ID '${deviceTransitPolicyId}'...`); this.api .request('getDeviceTransitPolicy', { deviceTransitPolicyId: deviceTransitPolicyId }) .then(response => { this.transitPolicies[deviceTransitPolicyId] = response; if ('name' in this.transitPolicies[deviceTransitPolicyId]) { this.transitPolicies[deviceTransitPolicyId].name_org = this.transitPolicies[deviceTransitPolicyId].name; this.transitPolicies[deviceTransitPolicyId].name = this.normalizeString( this.transitPolicies[deviceTransitPolicyId].name, ); if ( this.transitPolicies[deviceTransitPolicyId].name_org !== this.transitPolicies[deviceTransitPolicyId].name ) { this.log.debug( `Normalizing transit policy name from: '${this.transitPolicies[deviceTransitPolicyId].name_org}' to '${this.transitPolicies[deviceTransitPolicyId].name}'`, ); } } this.log.silly(`Getting transit policy response: '${JSON.stringify(response)}'.`); return resolve(); }) .catch(error => { reject(error); }); } }); } /** * Get events from OnlyCat API. * * @returns {Promise<void>} */ getEvents() { return new Promise((resolve, reject) => { if (this.adapterUnloaded) { reject(`Can not get events, adapter already unloaded.`); } else { this.log.debug(`Getting events...`); this.api .request('getEvents', { subscribe: true }) .then(response => { this.lastEvents = this.events; this.events = response; this.log.debug( this.events.length <= 1 ? `Got ${this.events.length} event.` : `Got ${this.events.length} events.`, ); return resolve(); }) .catch(error => { reject(error); }); } }); } /** * Checks whether the last received event is final, i.e. has a frameCount * and schedules an event update if not. */ checkTriggerEventUpdate() { if (this.devices && this.events && this.events.length > 0) { this.log.debug(`Checking if last event is final...`); if (!this.isEventFinal(this.events[0])) { if (this.eventUpdateCounter < MAX_EVENT_UPDATE) { this.clearEventUpdateTimer(); this.eventUpdateCounter++; const updateTimeout = Math.max( MINIMUM_EVENT_UPDATE_FREQUENCY, this.fibonacci(this.eventUpdateCounter), ); this.log.debug( `Last event not yet final, trigger ${this.eventUpdateCounter}. update in ${updateTimeout} seconds.`, ); this.eventUpdateTimerId = this.setTimeout(this.onEventUpdateTimer.bind(this), updateTimeout * 1000); } else { this.log.debug( `Last event not yet final, but max event update counter reached: ${this.eventUpdateCounter}.`, ); } } else { this.log.debug(`Last event is final.`); this.resetEventUpdateCounter(); } } } /** * Set active Transit Policy for device to OnlyCat API. * * @param {string} deviceId a device ID * @param {number} deviceTransitPolicyId a transit policy ID * @returns {Promise<void>} */ setTransitPolicyForDevice(deviceId, deviceTransitPolicyId) { return new Promise((resolve, reject) => { if (this.adapterUnloaded) { reject(`Can not set active transit policy, adapter already unloaded.`); } else { this.log.debug(`Setting active transit policy '${deviceTransitPolicyId}' for device '${deviceId}'...`); this.api .request('activateDeviceTransitPolicy', { deviceId: deviceId, deviceTransitPolicyId: deviceTransitPolicyId, }) .then(response => { this.log.silly( `Setting active transit policy '${deviceTransitPolicyId}' for device '${deviceId}' response: '${JSON.stringify(response)}'.`, ); return resolve(); }) .catch(error => { reject(error); }); } }); } /** * Reset active Transit Policy for device to OnlyCat API. * * @param {number} deviceIndex a device index */ resetTransitPolicyForDevice(deviceIndex) { const objName = `${this.devices[deviceIndex].description}.control.deviceTransitPolicyId`; const value = this.devices[deviceIndex].deviceTransitPolicyId; this.log.debug( `resetting deviceTransitPolicyId for device '${this.devices[deviceIndex].description}' to: '${value}'`, ); this.setState(objName, value, true).catch(error => { this.log.error( `Could not reset deviceTransitPolicyId for device '${this.devices[deviceIndex].description}' because: '${error}'`, ); }); } /************************************************ * methods to initially create object hierarchy * ************************************************/ /** * Creates the adapters object hierarchy. * * @returns {Promise<void>} */ createAdapterObjectHierarchy() { return new Promise((resolve, reject) => { this.log.debug(`Creating object hierarchy...`); this.getAdapterVersionFromAdapter() .then(version => this.removeDeprecatedDataFromAdapter(version)) .then(() => this.removeDeletedOrRenamedDataFromAdapter()) .then(() => this.createDeviceHierarchyToAdapter()) .then(() => this.createEventHierarchyToAdapter()) .then(() => this.createPetHierarchyToAdapter()) .then(() => this.createTransitPolicyHierarchyToAdapter()) .then(() => { this.log.debug(`Object hierarchy created.`); return resolve(); }) .catch(() => { this.log.error(`Creating object hierarchy failed.`); return reject(); }); }); } /** * Creates device hierarchy data structures to the adapter. * * @returns {Promise<void>} */ createDeviceHierarchyToAdapter() { return new Promise((resolve, reject) => { const promiseArray = []; // create devices for (let d = 0; d < this.devices.length; d++) { promiseArray.push(this.createDeviceHierarchyForDeviceToAdapter(d)); } Promise.all(promiseArray) .then(() => { return resolve(); }) .catch(error => { this.log.warn(`Could not create adapter device hierarchy (${error}).`); return reject(); }); }); } /** * Creates device hierarchy data structures for a device to the adapter. * * @param {number} deviceIndex a device index within this.devices * @returns {Promise<void>} */ createDeviceHierarchyForDeviceToAdapter(deviceIndex) { return new Promise((resolve, reject) => { const promiseArray = []; // create device const objName = this.devices[deviceIndex].description; this.setObjectNotExists( objName, this.buildDeviceObject( `device '${this.devices[deviceIndex].description_org}' (${this.devices[deviceIndex].deviceId})`, ), () => { this.setObjectNotExists( `${objName}.connectivity`, this.buildChannelObject( `connectivity state of the device '${this.devices[deviceIndex].description_org}'`, ), () => { this.setObjectNotExists( `${objName}.control`, this.buildChannelObject( `controllable states for device '${this.devices[deviceIndex].description_org}'`, ), () => { promiseArray.push( this.setObjectNotExistsAsync( `${objName}.deviceId`, this.buildStateObject('id of the device', 'text', 'string'), ), this.setObjectNotExistsAsync( `${objName}.description`, this.buildStateObject('description of the device', 'text', 'string'), ), this.setObjectNotExistsAsync( `${objName}.firmwareChannel`, this.buildStateObject('firmwareChannel of the device', 'text', 'string'), ), this.setObjectNotExistsAsync( `${objName}.timeZone`, this.buildStateObject('timeZone of the device', 'text', 'string'), ), this.setObjectNotExistsAsync( `${objName}.cursorId`, this.buildStateObject('cursorId of the device', 'text', 'number'), ), this.setObjectNotExistsAsync( `${objName}.connectivity.connected`, this.buildStateObject('is the device connected'), ), this.setObjectNotExistsAsync( `${objName}.connectivity.disconnectReason`, this.buildStateObject('disconnect reason', 'text', 'string'), ), this.setObjectNotExistsAsync( `${objName}.connectivity.timestamp`, this.buildStateObject('timestamp', 'date', 'number'), ), this.setObjectNotExistsAsync( `${objName}.control.deviceTransitPolicyId`, this.buildStateObject( 'deviceTransitPolicyId of the device', 'text', 'number', false, ), ), ); Promise.all(promiseArray) .then(() => { return resolve(); }) .catch(error => { this.log.warn(`Could not create adapter device hierarchy (${error}).`); return reject(); }); }, ); }, ); }, ); }); } /** * Creates event hierarchy data structures in the adapter. * * @returns {Promise<void>} */ createEventHierarchyToAdapter() { return new Promise((resolve, reject) => { const promiseArray = []; for (let d = 0; d < this.devices.length; d++) { const objName = this.devices[d].description; promiseArray.push( this.createEventsAsJsonToAdapter(objName), this.createEventsAsStateObjectsToAdapter(objName), ); } Promise.all(promiseArray) .then(() => { return resolve(); }) .catch(error => { this.log.warn(`Could not create adapter events hierarchy (${error}).`); return reject(); }); }); } /** * Creates events as json. * * @param {string} objName the object name to create events for * @returns {Promise<void>} */ createEventsAsJsonToAdapter(objName) { return new Promise((resolve, reject) => { const promiseArray = []; this.setObjectNotExists(`${objName}.jsonEvents`, this.buildChannelObject('events in json format'), () => { for (let e = 0; e < 10; e++) { promiseArray.push( this.setObjectNotExistsAsync( `${objName}.jsonEvents.${this.padZero(e + 1)}`, this.buildStateObject(`event ${e + 1}`, 'json', 'string'), ), ); } Promise.all(promiseArray) .then(() => { return resolve(); }) .catch(error => { this.log.warn(`Could not create adapter events json hierarchy (${error}).`); return reject(); }); }); }); } /** * Creates events as state objects. * * @param {string} objName the object name to create events for * @returns {Promise<void>} */ createEventsAsStateObjectsToAdapter(objName) { return new Promise((resolve, reject) => { const promiseArray = []; this.setObjectNotExists(`${objName}.events`, this.buildChannelObject('events as state objects'), () => { for (let e = 0; e < 10; e++) { promiseArray.push( this.createEventStateObjectsToAdapter( `${objName}.events.${this.padZero(e + 1)}`, `event ${e + 1}`, ), ); } Promise.all(promiseArray) .then(() => { return resolve(); }) .catch(error => { this.log.warn(`Could not create adapter events objects hierarchy (${error}).`); return reject(); }); }); }); } /** * Creates an event as state objects. * * @param {string} objName the object name to create an event state for * @param {string} description a description for the event state * @returns {Promise<void>} */ createEventStateObjectsToAdapter(objName, description) { return new Promise((resolve, reject) => { const promiseArray = []; this.setObjectNotExists(objName, this.buildFolderObject(description), () => { // attributes from event promiseArray.push( this.setObjectNotExistsAsync( `${objName}.accessToken`, this.buildStateObject('Access token', 'text', 'string'),