iobroker.sureflap
Version:
Adpater for smart pet devices from Sure Petcare
1,152 lines (1,088 loc) • 273 kB
JavaScript
/***********************
* *
* 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 => {