UNPKG

iobroker.sureflap

Version:

Adpater for smart pet devices from Sure Petcare

1,152 lines (1,088 loc) 273 kB
/*********************** * * * Sure Flap Adapter * * * ***********************/ 'use strict'; /* * Created with @iobroker/create-adapter v1.31.0 */ // The adapter-core module gives you access to the core ioBroker functions // you need to create an adapter const utils = require('@iobroker/adapter-core'); // Load your modules here, e.g.: // const fs = require("fs"); const SurepetApi = require('./lib/surepet-api'); const ADAPTER_VERSION = '3.4.0'; // Constants - data update frequency const RETRY_FREQUENCY_LOGIN = 60; const UPDATE_FREQUENCY_DATA = 10; const UPDATE_FREQUENCY_HISTORY = 60; const UPDATE_FREQUENCY_REPORT = 60; // Constants - device types const DEVICE_TYPE_HUB = 1; const DEVICE_TYPE_PET_FLAP = 3; const DEVICE_TYPE_FEEDER = 4; const DEVICE_TYPE_CAT_FLAP = 6; const DEVICE_TYPE_WATER_DISPENSER = 8; // Constants - feeder parameter const FEEDER_SINGLE_BOWL = 1; const FEEDER_FOOD_WET = 1; const FEEDER_FOOD_DRY = 2; // Constants - repeatable errors const HUB_LED_MODE_MISSING = 101; const DEVICE_BATTERY_DATA_MISSING = 201; const DEVICE_BATTERY_PERCENTAGE_DATA_MISSING = 202; const DEVICE_SERIAL_NUMBER_MISSING = 203; const DEVICE_SIGNAL_STRENGTH_MISSING = 204; const DEVICE_VERSION_NUMBER_MISSING = 205; const DEVICE_ONLINE_STATUS_MISSING = 206; const FLAP_LOCK_MODE_DATA_MISSING = 301; const FLAP_CURFEW_DATA_MISSING = 302; const FEEDER_CLOSE_DELAY_DATA_MISSING = 401; const FEEDER_BOWL_CONFIG_DATA_MISSING = 402; const FEEDER_BOWL_CONFIG_ADAPTER_OBJECT_MISSING = 403; const FEEDER_BOWL_STATUS_ADAPTER_OBJECT_MISSING = 404; const FEEDER_BOWL_REMAINING_FOOD_DATA_MISSING = 405; const FEEDER_BOWL_REMAINING_FOOD_ADAPTER_OBJECT_MISSING = 406; const CAT_FLAP_PET_TYPE_DATA_MISSING = 601; const DISPENSER_WATER_STATUS_ADAPTER_OBJECT_MISSING = 801; const DISPENSER_WATER_REMAINING_DATA_MISSING = 802; const DISPENSER_WATER_REMAINING_ADAPTER_OBJECT_MISSING = 803; const PET_POSITION_DATA_MISSING = 901; const PET_FEEDING_DATA_MISSING = 902; const PET_DRINKING_DATA_MISSING = 903; const PET_FLAP_STATUS_DATA_MISSING = 904; const PET_OUTSIDE_DATA_MISSING = 905; const PET_HOUSEHOLD_MISSING = 906; const PET_NAME_MISSING = 907; class Sureflap extends utils.Adapter { /** * @param {Partial<utils.AdapterOptions>} [options] adapter options */ constructor(options) { super({ ...options, name: 'sureflap', }); // class variables // init api this.api = new SurepetApi(this); /* update loop status */ // number of login attempts this.numberOfLogins = 0; // is first update loop this.firstLoop = true; // timer id this.timerId = 0; // adapter unloaded this.adapterUnloaded = false; // last history update timestamp this.lastHistoryUpdate = 0; // update history this loop this.updateHistory = false; // last aggregated report update timestamp this.lastReportUpdate = 0; // update aggregated report this loop this.updateReport = false; /* connected device types */ // flap connected to hub this.hasFlap = false; // feeder connected to hub this.hasFeeder = false; // water dispenser connected to hub this.hasDispenser = false; /* current and previous data from surepet API */ // auth token this.authToken = undefined; // list of households this.households = []; // list of pets this.pets = undefined; // previous list of pets this.petsPrev = undefined; // list of devices per household this.devices = {}; // previous list of devices per household this.devicesPrev = {}; // history this.history = {}; // previous history this.historyPrev = {}; // pet reports this.report = []; // previous pet reports this.reportPrev = []; // are all devices online this.allDevicesOnline = undefined; // were all devices online prev this.allDevicesOnlinePrev = undefined; // list of offline devices this.offlineDevices = []; // list of previously offline devices this.offlineDevicesPrev = []; // is curfew active this.curfewActive = undefined; // was curfew previously active this.curfewActivePrev = undefined; /* remember repeatable warnings to not spam iobroker log */ // noinspection JSPrimitiveTypeWrapperUsage this.warnings = []; this.warnings[HUB_LED_MODE_MISSING] = []; this.warnings[DEVICE_BATTERY_DATA_MISSING] = []; this.warnings[DEVICE_BATTERY_PERCENTAGE_DATA_MISSING] = []; this.warnings[DEVICE_SERIAL_NUMBER_MISSING] = []; this.warnings[DEVICE_SIGNAL_STRENGTH_MISSING] = []; this.warnings[DEVICE_VERSION_NUMBER_MISSING] = []; this.warnings[DEVICE_ONLINE_STATUS_MISSING] = []; this.warnings[FLAP_LOCK_MODE_DATA_MISSING] = []; this.warnings[FLAP_CURFEW_DATA_MISSING] = []; this.warnings[FEEDER_CLOSE_DELAY_DATA_MISSING] = []; this.warnings[FEEDER_BOWL_CONFIG_DATA_MISSING] = []; this.warnings[FEEDER_BOWL_CONFIG_ADAPTER_OBJECT_MISSING] = []; this.warnings[FEEDER_BOWL_STATUS_ADAPTER_OBJECT_MISSING] = []; this.warnings[FEEDER_BOWL_REMAINING_FOOD_DATA_MISSING] = []; this.warnings[FEEDER_BOWL_REMAINING_FOOD_ADAPTER_OBJECT_MISSING] = []; this.warnings[DISPENSER_WATER_STATUS_ADAPTER_OBJECT_MISSING] = []; this.warnings[DISPENSER_WATER_REMAINING_DATA_MISSING] = []; this.warnings[DISPENSER_WATER_REMAINING_ADAPTER_OBJECT_MISSING] = []; this.warnings[CAT_FLAP_PET_TYPE_DATA_MISSING] = []; this.warnings[PET_POSITION_DATA_MISSING] = []; this.warnings[PET_FEEDING_DATA_MISSING] = []; this.warnings[PET_DRINKING_DATA_MISSING] = []; this.warnings[PET_FLAP_STATUS_DATA_MISSING] = []; this.warnings[PET_OUTSIDE_DATA_MISSING] = []; this.warnings[PET_HOUSEHOLD_MISSING] = []; this.warnings[PET_NAME_MISSING] = []; this.lastError = undefined; this.lastLoginError = 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() { // Initialize your adapter here // Reset the connection indicator during startup this.setConnectionStatusToAdapter(false); // check adapter config for invalid values this.checkAdapterConfig(); // In order to get state updates, you need to subscribe to them. The following line adds a subscription for our variable we have created above. // You can also add a subscription for multiple states. The following line watches all states starting with "lights." // this.subscribeStates("lights.*"); // Or, if you really must, you can also watch all states. Don't do this if you don't need to. Otherwise this will cause a lot of unnecessary load on the system: // this.subscribeStates("*"); this.subscribeStates('*.control.*'); this.subscribeStates('*.pets.*.inside'); // start loading the data from the surepetcare API this.startLoadingData(); } /** * Is called when adapter shuts down - callback has to be called under any circumstances! * * @param {() => void} callback a callback method */ onUnload(callback) { try { this.adapterUnloaded = true; clearTimeout(this.timerId); this.setConnectionStatusToAdapter(false); this.log.info(`everything cleaned up`); } catch (e) { this.log.warn(`adapter clean up failed: ${e}`); } finally { callback(); } } /** * Processes incoming messages * * @param {ioBroker.Message} obj a message object */ onMessage(obj) { this.log.debug(`[onMessage] Message received`); if (obj) { if (obj.command === 'testLogin') { this.log.debug(`[onMessage] received command ${obj.command} from ${obj.from}`); try { const host = obj.message?.host; const username = obj.message?.username; const password = obj.message?.password; if (host && username && password) { this.api .doLoginAndGetAuthTokenForHostAndUsernameAndPassword(host, username, password) .then(async token => { if (token) { this.log.debug(`[onMessage] ${obj.command} result: Login successful`); obj.callback && this.sendTo( obj.from, obj.command, { native: { _login: true, _error: null, }, }, obj.callback, ); } else { this.log.debug(`[onMessage] ${obj.command} result: Login failed`); obj.callback && this.sendTo( obj.from, obj.command, { native: { _error: `Error: Login failed` } }, obj.callback, ); } }) .catch(err => { this.log.error(`[onMessage] ${obj.command} err: ${err}`); obj.callback && this.sendTo( obj.from, obj.command, { native: { _error: `Error: ${err}` } }, obj.callback, ); }); } else { this.log.error(`[onMessage] ${obj.command} err: Host or Username or Password not set`); if (!host) { obj.callback && this.sendTo( obj.from, obj.command, { native: { _error: `Error: host not defined/found` } }, obj.callback, ); } else if (!username) { obj.callback && this.sendTo( obj.from, obj.command, { native: { _error: `Error: username not defined/found` } }, obj.callback, ); } else { obj.callback && this.sendTo( obj.from, obj.command, { native: { _error: `Error: password not defined/found` } }, obj.callback, ); } } } catch (err) { this.log.error(`[onMessage] ${obj.command} err: ${err}`); obj.callback && this.sendTo(obj.from, obj.command, { native: { _error: `Error: ${err}` } }, obj.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 state ID * @param {ioBroker.State | null | undefined} state the state */ onStateChange(id, state) { // desired value is set if (id && state && state.ack === false) { const l = id.split('.'); if (this.isDeviceControl(l)) { if (this.isFlapControl(l)) { // change in control section of sureflap const hierarchy = l.slice(2, l.length - 2).join('.'); const deviceName = l[4]; const control = l[l.length - 1]; if (control === 'curfew_enabled') { this.changeCurfewEnabled(hierarchy, deviceName, state.val === true); } else if (control === 'lockmode' && typeof state.val === 'number') { this.changeFlapLockMode(hierarchy, deviceName, state.val); } else if (control === 'current_curfew' && typeof state.val === 'string') { this.changeCurrentCurfew(hierarchy, deviceName, state.val); } else if (control === 'type' && typeof state.val === 'number') { const petName = l[l.length - 2]; const petTagId = this.getPetTagId(petName); this.changeFlapPetType(hierarchy, deviceName, petName, petTagId, state.val); } } else if (this.isFeederControl(l)) { // change in control section of feeder const hierarchy = l.slice(2, l.length - 2).join('.'); const deviceName = l[4]; const control = l[l.length - 1]; if (control === 'close_delay' && typeof state.val === 'number') { this.changeFeederCloseDelay(hierarchy, deviceName, state.val); } } else if (this.isHubControl(l)) { // change hub led mode const hierarchy = l.slice(2, l.length - 3).join('.'); const hub = l[l.length - 3]; this.changeHubLedMode(hierarchy, hub, Number(state.val)); } else if (this.isPetAssigment(l)) { // change pet assigment to device const hierarchy = l.slice(2, l.length - 3).join('.'); const deviceName = l[4]; const petName = l[l.length - 2]; this.changePetAssigment(hierarchy, deviceName, petName, state.val === true); } else { this.log.warn(`not allowed to change object ${id}`); } } else if (this.isPetLocation(l)) { // change of pet location const hierarchy = l.slice(2, l.length - 3).join('.'); const petName = l[l.length - 2]; this.changePetLocation(hierarchy, petName, state.val === true); } else { this.log.warn(`not allowed to change object ${id}`); } } } /************************************************* * methods to start and keep update loop running * *************************************************/ /** * starts loading data from the surepet API */ startLoadingData() { this.log.debug(`starting SureFlap Adapter v${ADAPTER_VERSION}`); clearTimeout(this.timerId); this.doAuthenticate() .then(() => this.getHouseholds()) .then(() => this.startUpdateLoop()) .catch(error => { if (error === undefined || error.message === undefined || error.message === this.lastLoginError) { this.log.debug(error); } else { this.log.error(error); this.lastLoginError = error.message; } this.log.info(`disconnected`); if (!this.adapterUnloaded) { this.log.info(`Restarting in ${RETRY_FREQUENCY_LOGIN} seconds`); this.timerId = setTimeout(this.startLoadingData.bind(this), RETRY_FREQUENCY_LOGIN * 1000); } }); } /** * starts the update loop * * @returns {Promise} a promise */ startUpdateLoop() { return new Promise(resolve => { this.lastLoginError = undefined; this.log.info(`starting update loop...`); this.firstLoop = true; this.updateLoop(); this.log.info(`update loop started`); return resolve(); }); } /** * the update loop, refreshing the data every UPDATE_FREQUENCY_DATA seconds */ updateLoop() { clearTimeout(this.timerId); this.getDevices() .then(() => this.getPets()) .then(() => this.getEventHistory()) .then(() => this.getPetReports()) .then(() => this.createAdapterObjectHierarchy()) .then(() => this.updateDevices()) .then(() => this.updatePets()) .then(() => this.updateEventHistory()) .then(() => this.updateAdapterVersion()) .then(() => this.setUpdateTimer()) .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.info(`update loop stopped`); this.log.info(`disconnected`); if (!this.adapterUnloaded) { this.log.info(`Restarting in ${RETRY_FREQUENCY_LOGIN} seconds`); this.timerId = setTimeout(this.startLoadingData.bind(this), RETRY_FREQUENCY_LOGIN * 1000); } }) .finally(() => { this.firstLoop = false; }); } /** * sets the update timer * * @returns {Promise} a promise */ setUpdateTimer() { return new Promise((resolve, reject) => { if (!this.adapterUnloaded) { this.timerId = setTimeout(this.updateLoop.bind(this), UPDATE_FREQUENCY_DATA * 1000); return resolve(); } return reject(new Error(`cannot set timer. Adapter already unloaded.`)); }); } /*********************************************** * methods to communicate with surepetcare API * ***********************************************/ /** * authenticate and store auth token * * @returns {Promise} a promise */ doAuthenticate() { return new Promise((resolve, reject) => { this.setConnectionStatusToAdapter(false); this.numberOfLogins++; this.log.info(`connecting...`); this.log.debug(`login count: ${this.numberOfLogins}`); this.api .doLoginAndGetAuthToken() .then(token => { this.authToken = token; this.setConnectionStatusToAdapter(true); this.log.info(`connected`); this.numberOfLogins = 0; return resolve(); }) .catch(error => { return reject(error); }); }); } /** * get households * * @returns {Promise} a promise */ getHouseholds() { return new Promise((resolve, reject) => { this.api .getHouseholds(this.authToken) .then(households => { this.households = households; this.normalizeHouseholdNames(); this.log.info(households.length === 1 ? `Got 1 household` : `Got ${households.length} households`); return resolve(); }) .catch(error => { return reject(error); }); }); } /** * gets the data for devices * * @returns {Promise} a promise */ getDevices() { return new Promise((resolve, reject) => { const promiseArray = []; for (let h = 0; h < this.households.length; h++) { const hid = this.households[h].id; promiseArray.push(this.api.getDevicesForHousehold(this.authToken, hid)); } Promise.all(promiseArray) .then(values => { let deviceCount = 0; for (let h = 0; h < this.households.length; h++) { const hid = this.households[h].id; if (values[h] === undefined) { return reject(new Error(`getting devices failed.`)); } if (this.devices[hid] !== undefined) { this.devicesPrev[hid] = JSON.parse(JSON.stringify(this.devices[hid])); } this.devices[hid] = values[h]; deviceCount += this.devices[hid].length; } this.normalizeDeviceNames(); this.normalizeCurfew(); this.normalizeLockMode(); this.smoothBatteryOutliers(); this.getOfflineDevices(); this.calculateBatteryPercentageForDevices(); this.getConnectedDeviceTypes(); this.setLastUpdateToAdapter(); if (this.firstLoop) { this.log.info(deviceCount === 1 ? `Got 1 device` : `Got ${deviceCount} devices`); } return resolve(); }) .catch(error => { return reject(error); }); }); } /** * gets the data for pets * * @returns {Promise} a promise */ getPets() { return new Promise((resolve, reject) => { this.api .getPets(this.authToken) .then(pets => { if (this.pets !== undefined) { this.petsPrev = JSON.parse(JSON.stringify(this.pets)); } this.pets = pets; this.normalizePetNames(); if (this.firstLoop) { this.log.info(pets.length === 1 ? `Got 1 pet` : `Got ${pets.length} pets`); } return resolve(); }) .catch(error => { return reject(error); }); }); } /** * gets the event history data * * @returns {Promise} a promise */ getEventHistory() { return new Promise((resolve, reject) => { this.updateHistory = false; if (this.lastHistoryUpdate + UPDATE_FREQUENCY_HISTORY * 1000 < Date.now()) { const promiseArray = []; for (let h = 0; h < this.households.length; h++) { promiseArray.push(this.api.getHistoryForHousehold(this.authToken, this.households[h].id)); } Promise.all(promiseArray) .then(values => { for (let h = 0; h < this.households.length; h++) { const hid = this.households[h].id; if (values[h] === undefined) { return reject(new Error(`getting history failed.`)); } if (this.history[hid] !== undefined) { this.historyPrev[hid] = JSON.parse(JSON.stringify(this.history[hid])); } this.history[hid] = values[h]; } this.lastHistoryUpdate = Date.now(); this.updateHistory = true; return resolve(); }) .catch(err => { return reject(err); }); } else { return resolve(); } }); } /** * gets the aggregated reports for all pets * * @returns {Promise} a promise */ getPetReports() { return new Promise((resolve, reject) => { this.updateReport = false; if ( (!this.updateHistory || this.firstLoop) && (this.hasFeeder || this.hasDispenser || this.hasFlap) && this.lastReportUpdate + UPDATE_FREQUENCY_REPORT * 1000 < Date.now() ) { const promiseArray = []; for (let p = 0; p < this.pets.length; p++) { promiseArray.push( this.api.getReportForPet(this.authToken, this.pets[p].household_id, this.pets[p].id), ); } Promise.all(promiseArray) .then(values => { for (let p = 0; p < this.pets.length; p++) { if (values[p] === undefined) { return reject(new Error(`getting report data for pet '${this.pets[p].name}' failed.`)); } if (this.report[p] !== undefined) { this.reportPrev[p] = JSON.parse(JSON.stringify(this.report[p])); } this.report[p] = values[p]; } this.lastReportUpdate = Date.now(); this.updateReport = true; return resolve(); }) .catch(err => { return reject(err); }); } else { return resolve(); } }); } /******************************************************************* * methods to get information from the response of the surepet API * *******************************************************************/ /** * update devices with the received data * * @returns {Promise} a promise */ updateDevices() { return new Promise(resolve => { this.setGlobalOnlineStatusToAdapter(); this.setOfflineDevicesToAdapter(); for (let h = 0; h < this.households.length; h++) { const hid = this.households[h].id; const prefix = this.households[h].name; for (let d = 0; d < this.devices[hid].length; d++) { if (this.hasParentDevice(this.devices[hid][d])) { const hierarchy = `.${this.getParentDeviceName(this.devices[hid][d])}`; if ([DEVICE_TYPE_PET_FLAP, DEVICE_TYPE_CAT_FLAP].includes(this.devices[hid][d].product_id)) { // Sureflap Connect this.setSureflapConnectToAdapter( prefix, hierarchy, hid, d, this.devices[hid][d].product_id === DEVICE_TYPE_CAT_FLAP, ); } else if (this.devices[hid][d].product_id === DEVICE_TYPE_FEEDER) { // Feeder Connect this.setFeederConnectToAdapter(prefix, hierarchy, hid, d); } else if (this.devices[hid][d].product_id === DEVICE_TYPE_WATER_DISPENSER) { // water dispenser this.setWaterDispenserConnectToAdapter(prefix, hierarchy, hid, d); } this.setBatteryStatusToAdapter(prefix, hierarchy, hid, d); this.setSerialNumberToAdapter(prefix, hierarchy, hid, d); this.setSignalStrengthToAdapter(prefix, hierarchy, hid, d); } else { this.setHubStatusToAdapter(prefix, hid, d); } this.setVersionsToAdapter(prefix, hid, d); this.setOnlineStatusToAdapter(prefix, hid, d); } } return resolve(); }); } /** * update pets with received data * * @returns {Promise} a promise */ updatePets() { return new Promise(resolve => { const numPets = this.pets.length; for (let p = 0; p < numPets; p++) { if (this.pets[p].name !== undefined) { const petName = this.pets[p].name; const householdName = this.getHouseholdNameForId(this.pets[p].household_id); if (householdName !== undefined) { const prefix = `${householdName}.pets`; if (this.hasFlap) { this.setPetNameAndPositionToAdapter(prefix, petName, p); // add time spent outside and number of entries if (this.updateReport) { this.setPetOutsideToAdapter(`${prefix}.${petName}.movement`, p); } // add last used flap and direction if (this.updateHistory) { this.setPetLastMovementToAdapter(prefix, p, petName, this.pets[p].household_id); } } else { this.setPetNameToAdapter(prefix, petName, p); } if (this.hasFeeder && this.updateReport) { this.setPetFeedingToAdapter(`${prefix}.${petName}.food`, p); } if (this.hasDispenser && this.updateReport) { this.setPetDrinkingToAdapter(`${prefix}.${petName}.water`, p); } } else { if (!this.warnings[PET_HOUSEHOLD_MISSING][p]) { this.log.warn(`could not get household for pet (${petName})`); this.warnings[PET_HOUSEHOLD_MISSING][p] = true; } } } else { if (!this.warnings[PET_NAME_MISSING][p]) { this.log.warn(`no name found for pet with id '${this.pets[p].id}'.`); this.warnings[PET_NAME_MISSING][p] = true; } } } if (this.config.unknown_movement_enable && this.updateHistory) { this.setUnknownPetLastMovementToAdapter(); } return resolve(); }); } /** * updates event history with received data * * @returns {Promise} a promise */ updateEventHistory() { return new Promise(resolve => { if (this.updateHistory) { if (this.config.history_enable) { for (let h = 0; h < this.households.length; h++) { const hid = this.households[h].id; const prefix = this.households[h].name; if ( this.historyPrev[hid] === undefined || JSON.stringify(this.history[hid]) !== JSON.stringify(this.historyPrev[hid]) ) { this.log.debug(`updating event history for household '${prefix}'`); /* structure of history changes, so we need to delete and recreate history event structure on change */ this.deleteEventHistoryForHousehold(h, hid, false) .then(() => { if (Array.isArray(this.history[hid])) { const historyEntries = Math.min( this.history[hid].length, this.config.history_entries, ); this.log.debug(`updating event history with ${historyEntries} events`); for (let i = 0; i < historyEntries; i++) { this.setHistoryEventToAdapter(prefix, hid, i); } } }) .catch(err => { this.log.error(`updating event history failed (${err})`); }); } } } if (this.config.history_json_enable) { for (let h = 0; h < this.households.length; h++) { const hid = this.households[h].id; const prefix = this.households[h].name; if ( this.historyPrev[hid] === undefined || JSON.stringify(this.history[hid]) !== JSON.stringify(this.historyPrev[hid]) ) { this.log.debug(`updating json event history for household '${prefix}'`); /* structure of history changes, so we need to delete and recreate history event structure on change */ if (Array.isArray(this.history[hid])) { const historyEntries = Math.min( this.history[hid].length, this.config.history_json_entries, ); this.log.debug(`updating json event history with ${historyEntries} events`); for (let i = 0; i < historyEntries; i++) { this.setState( `${prefix}.history.json.${i}`, JSON.stringify(this.history[hid][i]), true, ); } } } } } } return resolve(); }); } /******************************************** * methods to set values to the surepet API * ********************************************/ /** * changes the LED mode of the hub (off = 0, high = 1, dimmed = 4) * * @param {string} hierarchy a object hierarchy string * @param {string} hubName a hub device name * @param {number} value the LED mode to set */ changeHubLedMode(hierarchy, hubName, value) { const deviceId = this.getDeviceId(hubName, [DEVICE_TYPE_HUB]); if (deviceId === -1) { this.log.warn(`could not find device Id for hub: '${hubName}'`); this.resetHubLedModeToAdapter(hierarchy, hubName); return; } if (value !== 0 && value !== 1 && value !== 4) { this.log.warn(`invalid value for led mode: '${value}'`); this.resetHubLedModeToAdapter(hierarchy, hubName); return; } this.log.debug(`changing hub led mode for hub '${hubName}' to '${value}' ...`); this.api .setLedModeForHub(this.authToken, deviceId, value) .then(() => { this.log.info(`hub led mode for hub '${hubName}' changed to '${value}'`); }) .catch(err => { this.log.error(`changing hub led mode for hub '${hubName}' to '${value}' failed: ${err}`); this.resetHubLedModeToAdapter(hierarchy, hubName); }); } /** * changes the close delay of a feeder (fast = 0, normal = 4, slow = 20) * * @param {string} hierarchy a object hierarchy string * @param {string} feederName a feeder device name * @param {number} value the delay to set */ changeFeederCloseDelay(hierarchy, feederName, value) { const deviceId = this.getDeviceId(feederName, [DEVICE_TYPE_FEEDER]); if (deviceId === -1) { this.log.warn(`could not find device Id for feeder: '${feederName}'`); this.resetFeederCloseDelayToAdapter(hierarchy, feederName); return; } if (value !== 0 && value !== 4 && value !== 20) { this.log.warn(`invalid value for close delay: '${value}'`); this.resetFeederCloseDelayToAdapter(hierarchy, feederName); return; } this.log.debug(`changing close delay for feeder '${feederName}' to '${value}' ...`); this.api .setCloseDelayForFeeder(this.authToken, deviceId, value) .then(() => { this.log.info(`close delay for feeder '${feederName}' changed to '${value}'`); }) .catch(err => { this.log.error(`changing close delay for feeder '${feederName}' to '${value}' failed: ${err}`); this.resetFeederCloseDelayToAdapter(hierarchy, feederName); }); } /** * changes the pet type for an assigned pet of a flap (outdoor pet = 2, indoor pet = 3) * * @param {string} hierarchy a object hierarchy string * @param {string} flapName a flap device name * @param {string} petName a pet name * @param {number} petTagId a pet tag ID * @param {number} value the pet type to set */ changeFlapPetType(hierarchy, flapName, petName, petTagId, value) { const deviceId = this.getDeviceId(flapName, [DEVICE_TYPE_CAT_FLAP, DEVICE_TYPE_PET_FLAP]); if (deviceId === -1) { this.log.warn(`could not find device Id for flap: '${flapName}'`); this.resetFlapPetTypeToAdapter(hierarchy, flapName, petName, petTagId); return; } if (value < 2 || value > 3) { this.log.warn(`invalid value for pet type: '${value}'`); this.resetFlapPetTypeToAdapter(hierarchy, flapName, petName, petTagId); return; } this.log.debug(`changing pet type of pet '${petName}' for flap '${flapName}' to '${value}' ...`); this.api .setPetTypeForFlapAndPet(this.authToken, deviceId, petTagId, value) .then(() => { this.log.info(`pet type of pet '${petName}' for flap '${flapName}' changed to '${value}'`); }) .catch(err => { this.log.error( `changing pet type of pet '${petName}' for flap '${flapName}' to '${value}' failed: ${err}`, ); this.resetFlapPetTypeToAdapter(hierarchy, flapName, petName, petTagId); }); } /** * changes the lockmode of a flap (open = 0, locked in = 1, locked out = 2, locked both = 3) * * @param {string} hierarchy a object hierarchy string * @param {string} flapName a flap device name * @param {number} value the lock mode to set */ changeFlapLockMode(hierarchy, flapName, value) { const deviceId = this.getDeviceId(flapName, [DEVICE_TYPE_CAT_FLAP, DEVICE_TYPE_PET_FLAP]); if (deviceId === -1) { this.log.warn(`could not find device Id for flap: '${flapName}'`); this.resetFlapLockModeToAdapter(hierarchy, flapName); return; } if (value < 0 || value > 3) { this.log.warn(`invalid value for lock mode: '${value}'`); this.resetFlapLockModeToAdapter(hierarchy, flapName); return; } this.log.debug(`changing lock mode for flap '${flapName}' to '${value}' ...`); this.api .setLockModeForFlap(this.authToken, deviceId, value) .then(() => { this.log.info(`lock mode for flap '${flapName}' changed to '${value}'`); }) .catch(err => { this.log.error(`changing lock mode for flap '${flapName}' to '${value}' failed: ${err}`); this.resetFlapLockModeToAdapter(hierarchy, flapName); }); } /** * changes the location of a pet (inside = true, outside = false) * * @param {string} hierarchy a object hierarchy string * @param {string} petName a pet name * @param {boolean} value the location to set */ changePetLocation(hierarchy, petName, value) { const petId = this.getPetId(petName); if (petId === -1) { this.log.warn(`could not find pet Id for pet: '${petName}'`); this.resetPetLocationToAdapter(hierarchy, petName); return; } this.log.debug(`changing location of pet '${petName}' to '${value ? 'inside' : 'outside'}' ...`); this.api .setLocationForPet(this.authToken, petId, value ? 1 : 2) .then(() => { this.log.info(`location for pet '${petName}' changed to '${value ? 'inside' : 'outside'}'`); }) .catch(error => { this.log.error( `changing location for pet '${petName}' to '${value ? 'inside' : 'outside'}' failed: ${error}`, ); this.resetPetLocationToAdapter(hierarchy, petName); }); } /** * changes the assigment of a pet for a device (assigned = true, unassigned = false) * * @param {string} hierarchy a object hierarchy string * @param {string} deviceName a device name * @param {string} petName a pet name * @param {boolean} value assign or remove the pet */ changePetAssigment(hierarchy, deviceName, petName, value) { const petTagId = this.getPetTagId(petName); const deviceId = this.getDeviceId(deviceName, []); if (petTagId === -1) { this.log.warn(`could not find pet tag Id for pet: '${petName}'`); this.resetPetAssigmentToAdapter(hierarchy, deviceName, petName); return; } else if (deviceId === -1) { this.log.warn(`could not find device Id for pet: '${deviceName}'`); this.resetPetAssigmentToAdapter(hierarchy, deviceName, petName); return; } this.log.debug( `changing assigment of pet '${petName}' for '${deviceName}' to '${value ? 'assigned' : 'unassigned'}' ...`, ); this.api .setPetAssignmentForDevice(this.authToken, deviceId, petTagId, value) .then(() => { this.log.info( `assigment of pet '${petName}' for '${deviceName}' changed to '${value ? 'assigned' : 'unassigned'}'`, ); }) .catch(error => { this.log.error( `changing assigment of pet '${petName}' for '${deviceName}' to '${value ? 'assigned' : 'unassigned'}' failed: ${error}`, ); this.resetPetAssigmentToAdapter(hierarchy, deviceName, petName); }); } /** * switches the curfew for a flap on or off * * @param {string} hierarchy a object hierarchy string * @param {string} flapName a flap device name * @param {boolean} value enables or disabled the curfew */ changeCurfewEnabled(hierarchy, flapName, value) { let currentState = false; const objNameCurrentCurfew = `${hierarchy}.control` + `.current_curfew`; const deviceType = this.getDeviceTypeByDeviceName(flapName, [DEVICE_TYPE_CAT_FLAP, DEVICE_TYPE_PET_FLAP]); const deviceId = this.getDeviceId(flapName, [DEVICE_TYPE_CAT_FLAP, DEVICE_TYPE_PET_FLAP]); if (deviceId === -1) { this.log.warn(`could not find device Id for flap: '${flapName}'`); this.resetFlapCurfewEnabledToAdapter(hierarchy, flapName); return; } this.getCurfewFromAdapter(objNameCurrentCurfew) .then(curfew => { currentState = this.isCurfewEnabled(curfew); }) .finally(() => { this.log.debug(`control curfew old state: ${currentState} new state: ${value}`); if (currentState !== value) { if (value === true) { // enable curfew const objNameLastCurfew = `${hierarchy}.last_enabled_curfew`; this.getCurfewFromAdapter(objNameLastCurfew) .then(curfew => { if (curfew.length > 0) { this.log.debug(`setting curfew to: '${JSON.stringify(curfew)}' ...`); curfew = this.convertCurfewLocalTimesToUtcTimes(curfew); if (DEVICE_TYPE_PET_FLAP === deviceType) { // pet flap takes single object instead of array curfew = curfew[0]; curfew.enabled = true; } this.api .setCurfewForFlap(this.authToken, deviceId, curfew) .then(() => { this.log.info(`curfew successfully enabled`); }) .catch(err => { this.log.error(`could not enable curfew because: ${err}`); this.resetFlapCurfewEnabledToAdapter(hierarchy, flapName); }); } else { this.log.error( `could not enable curfew because: last_enabled_curfew does not contain a curfew`, ); this.resetFlapCurfewEnabledToAdapter(hierarchy, flapName); } }) .catch(err => { this.log.error(`could not enable curfew because: ${err}`); this.resetFlapCurfewEnabledToAdapter(hierarchy, flapName); }); } else { // disable curfew const objName = `${hierarchy}.control` + `.current_curfew`; this.getCurfewFromAdapter(objName) .then(curfew => { for (let h = 0; h < curfew.length; h++) { curfew[h].enabled = false; } this.log.debug(`setting curfew to: ${JSON.stringify(curfew)}`); curfew = this.convertCurfewLocalTimesToUtcTimes(curfew); if (DEVICE_TYPE_PET_FLAP === deviceType) { // pet flap takes single object instead of array curfew = curfew[0]; } this.api .setCurfewForFlap(this.authToken, deviceId, curfew) .then(() => { this.log.info(`curfew successfully disabled`); }) .catch(err => { this.log.error(`could not disable curfew because: ${err}`); this.resetFlapCurfewEnabledToAdapter(hierarchy, flapName); }); }) .catch(err => {