UNPKG

iobroker.sureflap

Version:

Adpater for smart pet devices from Sure Petcare

1,292 lines (1,202 loc) 173 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 util = require('util'); const SurepetApi = require('./lib/surepet-api'); const ADAPTER_VERSION = '3.1.1'; // 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 { /** * Constructor * * @param {Partial<utils.AdapterOptions>} [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 = new Array(); 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; // promisify setObjectNotExists this.setObjectNotExistsPromise = util.promisify(this.setObjectNotExists); 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 */ 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 */ 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 * @param {ioBroker.State | null | undefined} 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 device = l[4]; const control = l[l.length - 1]; if (control === 'curfew_enabled') { this.changeCurfewEnabled(hierarchy, device, state.val === true); } else if (control === 'lockmode' && typeof (state.val) === 'number') { this.changeFlapLockMode(hierarchy, device, state.val); } else if (control === 'current_curfew' && typeof (state.val) === 'string') { this.changeCurrentCurfew(hierarchy, device, state.val); } else if (control === 'type' && typeof (state.val) === 'number') { const tagId = this.getPetTagId(l[l.length - 3]); this.changeFlapPetType(hierarchy, device, tagId, state.val); } } else if (this.isFeederControl(l)) { // change in control section of feeder const hierarchy = l.slice(2, l.length - 2).join('.'); const device = l[4]; const control = l[l.length - 1]; if (control === 'close_delay' && typeof (state.val) === 'number') { this.changeFeederCloseDelay(hierarchy, device, 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 { 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 pet = l[l.length - 2]; this.changePetLocation(hierarchy, pet, 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`); // @ts-ignore this.timerId = setTimeout(this.startLoadingData.bind(this), RETRY_FREQUENCY_LOGIN * 1000); } }); } /** * starts the update loop * * @return {Promise} */ startUpdateLoop() { return /** @type {Promise<void>} */(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`); // @ts-ignore this.timerId = setTimeout(this.startLoadingData.bind(this), RETRY_FREQUENCY_LOGIN * 1000); } }) .finally(() => { this.firstLoop = false; }); } /** * sets the update timer * * @return {Promise} */ setUpdateTimer() { return /** @type {Promise<void>} */(new Promise((resolve, reject) => { if (!this.adapterUnloaded) { // @ts-ignore this.timerId = setTimeout(this.updateLoop.bind(this), UPDATE_FREQUENCY_DATA * 1000); return resolve(); } else { return reject(new Error(`cannot set timer. Adapter already unloaded.`)); } })); } /*********************************************** * methods to communicate with surepetcare API * ***********************************************/ /** * authenticate and store auth token * * @return {Promise} */ doAuthenticate() { return /** @type {Promise<void>} */(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 * * @return {Promise} */ getHouseholds() { return /** @type {Promise<void>} */(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 * * @return {Promise} */ getDevices() { return /** @type {Promise<void>} */(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.`)); } else { 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 * * @return {Promise} */ getPets() { return /** @type {Promise<void>} */(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 * * @return {Promise} */ getEventHistory() { return /** @type {Promise<void>} */(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.`)); } else { 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 * * @return {Promise} */ getPetReports() { return /** @type {Promise<void>} */(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.`)); } else { 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 * * @return {Promise} */ updateDevices() { return /** @type {Promise<void>} */(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 * * @return {Promise} */ updatePets() { return /** @type {Promise<void>} */(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; } } } return resolve(); })); } /** * updates event history with received data * * @return {Promise} */ updateEventHistory() { return /** @type {Promise<void>} */(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 * @param {string} hubName * @param {number} value */ 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 * @param {string} feederName * @param {number} value */ 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 of a flap for a pet (outdoor pet = 2, indoor pet = 3) * * @param {string} hierarchy * @param {string} flapName * @param {number} petTag * @param {number} value */ changeFlapPetType(hierarchy, flapName, petTag, 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, petTag); return; } if (value < 2 || value > 3) { this.log.warn(`invalid value for pet type: '${value}'`); this.resetFlapPetTypeToAdapter(hierarchy, flapName, petTag); return; } const petName = this.getPetNameForTagId(petTag); this.log.debug(`changing pet type of pet '${petName}' for flap '${flapName}' to '${value}' ...`); this.api.setPetTypeForFlapAndPet(this.authToken, deviceId, petTag, 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, petTag); }); } /** * changes the lockmode of a flap (open = 0, locked in = 1, locked out = 2, locked both = 3) * * @param {string} hierarchy * @param {string} flapName * @param {number} value */ 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 * @param {string} petName * @param {boolean} value */ 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); }); } /** * switches the curfew for a flap on or off * * @param {string} hierarchy * @param {string} flapName * @param {boolean} value */ 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 => { this.log.error(`could not disable curfew because: ${err}`); this.resetFlapCurfewEnabledToAdapter(hierarchy, flapName); }); } } }); } /** * changes the current curfew for a flap * * @param {string} hierarchy * @param {string} flapName * @param {string} value */ changeCurrentCurfew(hierarchy, flapName, value) { 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; } let curfew = this.validateAndGetCurfewFromJsonString(value, deviceType); if (curfew === undefined) { this.log.error(`could not update curfew because of previous error`); this.resetControlCurrentCurfewToAdapter(hierarchy, flapName); } else { this.log.debug(`changing 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 updated`); }).catch(err => { this.log.error(`could not update curfew because: ${err}`); this.resetControlCurrentCurfewToAdapter(hierarchy, flapName); }); } } /**************************************** * methods to set values to the adapter * ****************************************/ /** * updates the adapter version state on the first update loop * * @return {Promise} */ updateAdapterVersion() { return /** @type {Promise<void>} */(new Promise((resolve, reject) => { if (!this.adapterUnloaded) { // update adapter version after fist loop, so we can react to old version if (this.firstLoop) { this.setAdapterVersionToAdapter(ADAPTER_VERSION); } return resolve(); } else { return reject(new Error(`cannot set adapter version. Adapter already unloaded.`)); } })); } /** * sets the current adapter version to the adapter * * @param {string} version */ setAdapterVersionToAdapter(version) { this.log.silly(`setting adapter version to adapter`); /* objects created via io-package.json, no need to create them here */ this.setState('info.version', version, true); } /** * sets connection status to the adapter * * @param {boolean} connected */ setConnectionStatusToAdapter(connected) { this.log.silly(`setting connection status to adapter`); /* objects created via io-package.json, no need to create them here */ this.setState('info.connection', connected, true); } /** * sets global online status to the adapter */ setGlobalOnlineStatusToAdapter() { this.log.silly(`setting global online status to adapter`); if (this.allDevicesOnline !== this.allDevicesOnlinePrev) { const objName = 'info.all_devices_online'; this.setState(objName, this.allDevicesOnline, true); } } /** * sets offline devices to the adapter */ setOfflineDevicesToAdapter() { this.log.silly(`setting offline devices to adapter`); if (JSON.stringify(this.offlineDevices) !== JSON.stringify(this.offlineDevicesPrev)) { const objName = 'info.offline_devices'; this.setState(objName, this.offlineDevices.join(','), true); } } /** * sets the last time data was received from surepet api */ setLastUpdateToAdapter() { this.log.silly(`setting last update to adapter`); /* object created via io-package.json, no need to create them here */ this.setState('info.last_update', this.getCurrentDateFormattedAsISO(), true); } /** * sets sureflap attributes to the adapter * * @param {string} prefix * @param {string} hierarchy * @param {number} hid a household id * @param {number} deviceIndex * @param {boolean} isCatFlap */ setSureflapConnectToAdapter(prefix, hierarchy, hid, deviceIndex, isCatFlap) { // lock mode if (this.objectContainsPath(this.devices[hid][deviceIndex], 'status.locking.mode')) { if (!this.devicesPrev[hid] || !this.objectContainsPath(this.devicesPrev[hid][deviceIndex], 'status.locking.mode') || (this.devices[hid][deviceIndex].status.locking.mode !== this.devicesPrev[hid][deviceIndex].status.locking.mode)) { const objName = prefix + hierarchy + '.' + this.devices[hid][deviceIndex].name + '.control' + '.lockmode'; try { this.setState(objName, this.devices[hid][deviceIndex].status.locking.mode, true); } catch (error) { this.log.error(`could not set lock mode to adapter (${error})`); } } this.warnings[FLAP_LOCK_MODE_DATA_MISSING][deviceIndex] = false; } else { if (!this.warnings[FLAP_LOCK_MODE_DATA_MISSING][deviceIndex]) { this.log.warn(`no lock mode data found for flap '${this.devices[hid][deviceIndex].name}'.`); this.warnings[FLAP_LOCK_MODE_DATA_MISSING][deviceIndex] = true; } } // curfew if (this.objectContainsPath(this.devices[hid][deviceIndex], 'control.curfew')) { if (!this.devicesPrev[hid] || !this.objectContainsPath(this.devicesPrev[hid][deviceIndex], 'control.curfew') || (JSON.stringify(this.devices[hid][deviceIndex].control.curfew) !== JSON.stringify(this.devicesPrev[hid][deviceIndex].control.curfew))) { if (this.devicesPrev[hid] && this.objectContainsPath(this.devicesPrev[hid][deviceIndex], 'control.curfew') && this.isCurfewEnabled(this.devicesPrev[hid][deviceIndex].control.curfew)) { const objNameLastEnabledCurfew = prefix + hierarchy + '.' + this.devices[hid][deviceIndex].name + '.last_enabled_curfew'; this.setCurfewToAdapter(objNameLastEnabledCurfew, this.devicesPrev[hid][deviceIndex].control.curfew); } const objNameCurrentCurfew = prefix + hierarchy + '.' + this.devices[hid][deviceIndex].name + '.control' + '.current_curfew'; this.setCurfewToAdapter(objNameCurrentCurfew, this.devices[hid][deviceIndex].control.curfew); const objNameCurfewEnabled = prefix + hierarchy + '.' + this.devices[hid][deviceIndex].name + '.control' + '.curfew_enabled'; try { this.setState(objNameCurfewEnabled, this.isCurfewEnabled(this.devices[hid][deviceIndex].control.curfew), true); } catch (error) { this.log.error(`could not set curfew to adapter (${error})`); } } // curfew active this.curfewActive = this.isCurfewActive(this.devices[hid][deviceIndex].control.curfew); if (this.curfewActivePrev === undefined || this.curfewActive !== this.curfewActivePrev) { this.setState(prefix + hierarchy + '.' + this.devices[hid][deviceIndex].name + '.curfew_active', this.curfewActive, true); this.log.info(`changing curfew_active from ${this.curfewActivePrev} to ${this.curfewActive}`); this.curfewActivePrev = this.curfewActive; } this.warnings[FLAP_CURFEW_DATA_MISSING][deviceIndex] = false; } else { if (!this.warnings[FLAP_CURFEW_DATA_MISSING][deviceIndex]) { this.log.warn(`no curfew data found for flap '${this.devices[hid][deviceIndex].name}'.`); this.warnings[FLAP_CURFEW_DATA_MISSING][deviceIndex] = true; } } // assigned pets type if (isCatFlap) { if (this.objectContainsPath(this.devices[hid][deviceIndex], 'tags') && Array.isArray(this.devices[hid][deviceIndex].tags)) { for (let t = 0; t < this.devices[hid][deviceIndex].tags.length; t++) { if (!this.devicesPrev[hid] || !this.devicesPrev[hid][deviceIndex].tags[t] || !this.devicesPrev[hid][deviceIndex].tags[t].profile || (this.devices[hid][deviceIndex].tags[t].profile !== this.devicesPrev[hid][deviceIndex].tags[t].profile)) { const name = this.getPetNameForTagId(this.devices[hid][deviceIndex].tags[t].id); if (name !== undefined) { const objName = prefix + hierarchy + '.' + this.devices[hid][deviceIndex].name + '.assigned_pets.' + name + '.control' + '.type'; try { this.setState(objName, this.devices[hid][deviceIndex].tags[t].profile, true); } catch (error) { this.log.error(`could not set pet type to adapter (${error})`); } } else { this.log.warn(`could not find pet with pet tag id (${this.devices[hid][deviceIndex].tags[t].id})`); this.log.debug(`cat flap '${this.devices[hid][deviceIndex].name}' has ${this.devices[hid][deviceIndex].tags.length} pets assigned and household has ${this.pets.length} pets assigned.`); } } } this.warnings[CAT_FLAP_PET_TYPE_DATA_MISSING][deviceIndex] = false; } else { if (!this.warnings[CAT_FLAP_PET_TYPE_DATA_MISSING][deviceIndex]) { this.log.warn(`no pet type data found for cat flap '${this.devices[hid][deviceIndex].name}'.`); this.warnings[CAT_FLAP_PET_TYPE_DATA_MISSING][deviceIndex] = true; } } } } /** * sets feeder attributes to the adapter * * @param {string} prefix * @param {string} hierarchy * @param {number} hid a household id * @param {number} deviceIndex */ setFeederConnectToAdapter(prefix, hierarchy, hid, deviceIndex) { const objName = prefix + hierarchy + '.' + this.devices[hid][deviceIndex].name; // close delay if (this.objectContainsPath(this.devices[hid][deviceIndex], 'control.lid.close_delay')) { if (!this.devicesPrev[hid] || !this.objectContainsPath(this.devicesPrev[hid][deviceIndex], 'control.lid.close_delay') || (this.devices[hid][deviceIndex].control.lid.close_delay !== this.devicesPrev[hid][deviceIndex].control.lid.close_delay)) { this.setState(objName + '.control' + '.close_delay', this.devices[hid][deviceIndex].control.lid.close_delay, true); this.warnings[FEEDER_CLOSE_DELAY_DATA_MISSING][deviceIndex] = false; } this.warnings[FEEDER_CLOSE_DELAY_DATA_MISSING][deviceIndex] = false; } else { if (!this.warnings[FEEDER_CLOSE_DELAY_DATA_MISSING][deviceIndex]) { this.log.warn(`no close delay setting found for '${this.devices[hid][deviceIndex].name}'.`); this.warnings[FEEDER_CLOSE_DELAY_DATA_MISSING][deviceIndex] = true; } } // feeder config if (this.objectContainsPath(this.devices[hid][deviceIndex], 'control.bowls.settings') && Array.isArray(this.devices[hid][deviceIndex].control.bowls.settings)) { if (!this.devicesPrev[hid] || !this.objectContainsPath(this.devicesPrev[hid][deviceIndex], 'control.bowls.settings') || (JSON.stringify(this.devices[hid][deviceIndex].control.bowls.settings) !== JSON.stringify(this.devicesPrev[hid][deviceIndex].control.bowls.settings))) { for (let b = 0; b < this.devices[hid][deviceIndex].control.bowls.settings.length; b++) { this.getObject(objName + '.bowls.' + b, (err, obj) => { if (!err && obj) { if (this.objectContainsPath(this.devices[hid][deviceIndex].control.bowls.settings[b], 'food_type')) { this.setState(objName + '.bowls.' + b + '.food_type', this.devices[hid][deviceIndex].control.bowls.settings[b].food_type, true); } if (this.objectContainsPath(this.devices[hid][deviceIndex].control.bowls.settings[b], 'target')) { this.setState(objName + '.bowls.' + b + '.target', this.devices[hid][deviceIndex].control.bowls.settings[b].target, true); } this.warnings[FEEDER_BOWL_CONFIG_ADAPTER_OBJECT_MISSING][deviceIndex] = false; } else { if (!this.warnings[FEEDER_BOWL_CONFIG_ADAPTER_OBJECT_MISSING][deviceIndex]) { this.log.warn(`got feeder config data for object '${objName + '.bowls.' + b}' but object does not exist. This can happen if number of bowls is changed and can be ignored.`); this.warnings[FEEDER_BOWL_CONFIG_ADAPTER_OBJECT_MISSING][deviceIndex] = true; } } }); } } this.warnings[FEEDER_BOWL_CONFIG_DATA_MISSING][deviceIndex] = false; } else { if (!this.warnings[FEEDER_BOWL_CONFIG_DATA_MISSING][deviceIndex]) { this.log.warn(`no feeder config data found for '${this.devices[hid][deviceIndex].name}'.`); this.warnings[FEEDER_BOWL_CONFIG_DATA_MISSING][deviceIndex] = true; } } // feeder remaining food data if (this.objectContainsPath(this.devices[hid][deviceIndex], 'status.bowl_status') && Array.isArray(this.devices[hid][deviceIndex].status.bowl_status)) { // get feeder remaining food data from new bowl_status if (!this.devicesPrev[hid] || !this.objectContainsPath(this.devicesPrev[hid][deviceIndex], 'status.bowl_status') || (JSON.stringify(this.devices[hid][deviceIndex].status.bowl_status) !== JSON.stringify(this.devicesPrev[hid][deviceIndex].status.bowl_status))) { this.log.silly(`Updating remaining food data from bowl_status.`); const bowlCount = this.objectContainsPath(this.devices[hid][deviceIndex], 'control.bowls.type') && this.devices[hid][deviceIndex].control.bowls.type === FEEDER_SINGLE_BOWL ? 1 : this.devices[hid][deviceIndex].status.bowl_status.length; for (let b = 0; b < bowlCount; b++) { this.getObject(objName + '.bowls.' + b, (err, obj) => { if (!err && obj) { if (this.objectContainsPath(this.devices[hid][deviceIndex].status.bowl_status[b], 'current_weight')) { this.setState(objName + '.bowls.' + b + '.weight', this.devices[hid][deviceIndex].status.bowl_status[b].current_weight, true); } if (this.objectContainsPath(this.devices[hid][deviceIndex].status.bowl_status[b], 'fill_percent')) { this.setState(objName + '.bowls.' + b + '.fill_percent', this.devices[hid][deviceIndex].status.bowl_status[b].fill_percent, true); } if (this.objectContainsPath(this.devices[hid][deviceIndex].status.bowl_status[b], 'last_filled_at')) { this.setState(objName + '.bowls.' + b + '.last_filled_at', this.devices[hid][deviceIndex].status.bowl_status[b].last_filled_at, true); } if (this.objectContainsPath(this.devices[hid][deviceIndex].status.bowl_status[b], 'last_zeroed_at')) { this.setState(objName + '.bowls.' + b + '.last_zeroed_at', this.devices[hid][deviceIndex].status.bowl_stat