iobroker.sureflap
Version:
Adpater for smart pet devices from Sure Petcare
1,292 lines (1,202 loc) • 173 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 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