iobroker.onlycat
Version:
Adapter for OnlyCat cat flaps with prey detection
1,234 lines (1,170 loc) • 113 kB
JavaScript
/*********************
* *
* 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'),