Skip to content

Commit

Permalink
added last pet movement
Browse files Browse the repository at this point in the history
  • Loading branch information
Sickbart committed Jan 24, 2024
1 parent 1612c61 commit ba09934
Show file tree
Hide file tree
Showing 6 changed files with 208 additions and 20 deletions.
4 changes: 2 additions & 2 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
MIT License

Copyright (c) 2023 Sickboy78 <[email protected]>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
Expand All @@ -19,3 +17,5 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Copyright (c) 2024 Sickboy78 <[email protected]>
15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,12 @@ adapter<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; │ ├ times_eaten<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; │ └ dry..wet<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; └ weight<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ├ movement<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; │ ├ last_direction<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; │ ├ last_flap<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; │ ├ last_time<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; │ ├ time_spent_outside_<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; │ └ times_outside<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; └ water<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ├ last_time_drunk<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ├ time_spent<br>
Expand All @@ -162,6 +168,11 @@ The pictures of the SureFlap® devices are provided free to use from [Sure Petca

## Changelog

### 2.0.1 (2024-01-24)
* (Sickboy78) added last movement for pets
* (Sickboy78) added time spent outside today for pets
* (Sickboy78) dependency updates

### 1.2.3 (2023-12-29)
* (Sickboy78) added api host to config and set default to new api
* (Sickboy78) improved removing of obsolete objects
Expand Down Expand Up @@ -260,8 +271,6 @@ The pictures of the SureFlap® devices are provided free to use from [Sure Petca

MIT License

Copyright (c) 2023 Sickboy78 <[email protected]>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
Expand All @@ -279,3 +288,5 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Copyright (c) 2024 Sickboy78 <[email protected]>
15 changes: 14 additions & 1 deletion io-package.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
{
"common": {
"name": "sureflap",
"version": "1.2.3",
"version": "2.0.1",
"news": {
"2.0.1": {
"en": "added last movement for pets",
"de": "letzte Bewegung für Haustiere hinzugefügt",
"ru": "Слава Украины",
"pt": "adicionado o último movimento para animais de estimação",
"nl": "laatste beweging voor huisdieren toegevoegd",
"fr": "ajout d'un dernier mouvement pour les animaux domestiques",
"it": "aggiunto l'ultimo movimento per gli animali domestici",
"es": "lisatud viimane liikumine lemmikloomade jaoks",
"pl": "Dodano ostatni ruch dla zwierząt domowych",
"uk": "додано останній рух для домашніх тварин",
"zh-cn": "为宠物添加最后一个动作"
},
"1.2.3": {
"en": "new configurable API host",
"de": "neuer konfigurierbarer API Hostname",
Expand Down
188 changes: 176 additions & 12 deletions main.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ class Sureflap extends utils.Adapter {
this.feederFoodBowlObjectMissing = [];
this.waterDispenserWaterObjectMissing = [];
this.petDrinkingDataMissing = [];
this.petFlapStatusDataMissing = [];
this.petOutsideDataMissing = [];
this.lastError = null;
this.lastLoginError = null;

Expand Down Expand Up @@ -246,7 +248,7 @@ class Sureflap extends utils.Adapter {
* starts loading data from the surepet API
*/
startLoadingData() {
this.log.debug(`starting SureFlap Adapter v1.2.3`);
this.log.debug(`starting SureFlap Adapter v2.0.1`);
clearTimeout(this.timerId);
this.doAuthenticate()
.then(() => this.startUpdateLoop())
Expand Down Expand Up @@ -616,6 +618,7 @@ class Sureflap extends utils.Adapter {
for (let p = 0; p < numPets; p++) {
const pet_name = this.sureFlapState.pets[p].name;
const household_name = this.getHouseholdNameForId(this.sureFlapState.pets[p].household_id);
const household_index = this.getHouseholdIndexForId(this.sureFlapState.pets[p].household_id);
const prefix = household_name + '.pets';
if(this.hasFlap) {
if('position' in this.sureFlapState.pets[p]) {
Expand All @@ -626,11 +629,18 @@ class Sureflap extends utils.Adapter {
} else {
this.setPetStatusToAdapter(prefix, pet_name, p);
if(!this.petPositionObjectMissing[p]) {
this.log.warn(`no position object found for pet '${this.sureFlapState.pets[p].name}'`);
this.log.debug(`no position object found for pet '${this.sureFlapState.pets[p].name}'`);
this.petPositionObjectMissing[p] = true;
}
}
// add time spent outside and number of entries
if(this.updateReport) {
this.setPetOutsideToAdapter(prefix + '.' + pet_name + '.movement', p);
}
// add last used flap and direction
if(this.updateHistory) {
this.setPetLastMovementToAdapter(prefix, p, pet_name, household_index);
}
} else {
this.setPetStatusToAdapter(prefix, pet_name, p);
}
Expand Down Expand Up @@ -1168,7 +1178,7 @@ class Sureflap extends utils.Adapter {
});
}
} else {
this.log.warn(`no remaining food data for feeder '${this.sureFlapState.devices[deviceIndex].name}' found`);
this.log.debug(`no remaining food data for feeder '${this.sureFlapState.devices[deviceIndex].name}' found`);
}
}
}
Expand Down Expand Up @@ -1229,7 +1239,7 @@ class Sureflap extends utils.Adapter {
}
});
} else {
this.log.warn(`no remaining water data for water dispenser '${this.sureFlapState.devices[deviceIndex].name}' found`);
this.log.debug(`no remaining water data for water dispenser '${this.sureFlapState.devices[deviceIndex].name}' found`);
}
}
}
Expand Down Expand Up @@ -1430,7 +1440,7 @@ class Sureflap extends utils.Adapter {
* @param {number} p
*/
setPetFeedingToAdapter(prefix, p) {
if(!this.sureFlapReport[p].feeding != undefined && this.sureFlapReport[p].feeding.datapoints != undefined && this.sureFlapReport[p].feeding.datapoints.length >0) {
if(!this.sureFlapReport[p].feeding != undefined && this.sureFlapReport[p].feeding.datapoints != undefined && Array.isArray(this.sureFlapReport[p].feeding.datapoints) && this.sureFlapReport[p].feeding.datapoints.length > 0) {
if(!this.sureFlapReportPrev[p] || !this.sureFlapReportPrev[p].feeding || JSON.stringify(this.sureFlapReport[p].feeding) !== JSON.stringify(this.sureFlapReportPrev[p].feeding)) {
const consumption_data = this.calculateFoodConsumption(p);
this.log.debug(`updating food consumed for pet '${this.sureFlapState.pets[p].name}' with '${JSON.stringify(consumption_data)}'`);
Expand All @@ -1455,7 +1465,7 @@ class Sureflap extends utils.Adapter {
* @param {number} p
*/
setPetDrinkingToAdapter(prefix, p) {
if(!this.sureFlapReport[p].drinking != undefined && this.sureFlapReport[p].drinking.datapoints != undefined && this.sureFlapReport[p].drinking.datapoints.length >0) {
if(!this.sureFlapReport[p].drinking != undefined && this.sureFlapReport[p].drinking.datapoints != undefined && Array.isArray(this.sureFlapReport[p].drinking.datapoints) && this.sureFlapReport[p].drinking.datapoints.length > 0) {
if(!this.sureFlapReportPrev[p] || !this.sureFlapReportPrev[p].drinking || JSON.stringify(this.sureFlapReport[p].drinking) !== JSON.stringify(this.sureFlapReportPrev[p].drinking)) {
const consumption_data = this.calculateWaterConsumption(p);
this.log.debug(`updating water consumed for pet '${this.sureFlapState.pets[p].name}' with '${JSON.stringify(consumption_data)}'`);
Expand All @@ -1473,6 +1483,54 @@ class Sureflap extends utils.Adapter {
}
}

/**
* sets pet outside to the adapter
* @param {string} prefix
* @param {number} p
*/
setPetOutsideToAdapter(prefix, p) {
if(!this.sureFlapReport[p].movement != undefined && this.sureFlapReport[p].movement.datapoints != undefined && Array.isArray(this.sureFlapReport[p].movement.datapoints) && this.sureFlapReport[p].movement.datapoints.length > 0) {
if(!this.sureFlapReportPrev[p] || !this.sureFlapReportPrev[p].movement || JSON.stringify(this.sureFlapReport[p].movement) !== JSON.stringify(this.sureFlapReportPrev[p].movement)) {
const outside_data = this.calculateTimeOutside(p);
this.log.debug(`updating time outside for pet '${this.sureFlapState.pets[p].name}' with '${JSON.stringify(outside_data)}'`);
this.setState(prefix + '.times_outside', outside_data.count, true);
this.setState(prefix + '.time_spent_outside', outside_data.time_spent_outside, true);
}
this.petOutsideDataMissing[p] = false;
} else {
if(!this.petOutsideDataMissing[p]) {
this.log.warn(`aggregated report for pet '${this.sureFlapState.pets[p].name}' does not contain movement data`);
this.petOutsideDataMissing[p] = true;
}
}
}

/**
* sets pet last movement to the adapter
* @param {string} prefix
* @param {number} pet_index
* @param {string} pet_name
* @param {number} h
*/
setPetLastMovementToAdapter(prefix, pet_index, pet_name, h) {
if(this.sureFlapHistoryPrev[h] == undefined || JSON.stringify(this.sureFlapHistory[h]) !== JSON.stringify(this.sureFlapHistoryPrev[h])) {
const movement = this.calculateLastMovement(pet_name, h);
if(movement != undefined && 'last_direction' in movement && 'last_flap' in movement && 'last_time' in movement) {
const hierarchy = '.' + pet_name + '.movement';
this.log.debug(`updating last movement for pet '${pet_name}' with '${JSON.stringify(movement)}'`);
this.setState(prefix + hierarchy + '.last_time', movement.last_time, true);
this.setState(prefix + hierarchy + '.last_direction', movement.last_direction, true);
this.setState(prefix + hierarchy + '.last_flap', movement.last_flap, true);
this.petFlapStatusDataMissing[pet_index] = false;
} else {
if(!this.petFlapStatusDataMissing[pet_index]) {
this.log.warn(`history does not contain flap movement for pet '${pet_name}'`);
this.petFlapStatusDataMissing[pet_index] = true;
}
}
}
}

/**
* sets history event to the adapter
* @param {string} prefix
Expand Down Expand Up @@ -2366,6 +2424,13 @@ class Sureflap extends utils.Adapter {
if(this.hasFlap) {
promiseArray.push(this.setObjectNotExistsPromise(obj_name + '.inside', this.buildStateObject('is ' + name + ' inside', 'indicator', 'boolean', false)));
promiseArray.push(this.setObjectNotExistsPromise(obj_name + '.since', this.buildStateObject('last location change', 'date', 'string')));
this.setObjectNotExists(obj_name + '.movement', this.buildFolderObject('movement'), () => {
promiseArray.push(this.setObjectNotExistsPromise(obj_name + '.movement' + '.last_time', this.buildStateObject('date and time of last movement', 'date', 'string')));
promiseArray.push(this.setObjectNotExistsPromise(obj_name + '.movement' + '.last_direction', this.buildStateObject('direction of last movement', 'value', 'number')));
promiseArray.push(this.setObjectNotExistsPromise(obj_name + '.movement' + '.last_flap', this.buildStateObject('name of last used flap', 'value', 'string')));
promiseArray.push(this.setObjectNotExistsPromise(obj_name + '.movement' + '.times_outside', this.buildStateObject('number of times outside today', 'value', 'number')));
promiseArray.push(this.setObjectNotExistsPromise(obj_name + '.movement' + '.time_spent_outside', this.buildStateObject('time spent in seconds outside today', 'value', 'number')));
});
}
if(this.hasFeeder) {
this.setObjectNotExists(obj_name + '.food', this.buildFolderObject('food'), () => {
Expand Down Expand Up @@ -2484,6 +2549,81 @@ class Sureflap extends utils.Adapter {
}
}

/**
* calculates last movement for pet
* @param {string} pet_name
* @param {number} household
* @returns {object} last used flap data object
*/
calculateLastMovement(pet_name, household) {
const data = {};
if(Array.isArray(this.sureFlapHistory[household])) {
for(let i = 0; i < this.sureFlapHistory[household].length; i++) {
const datapoint = this.sureFlapHistory[household][i];
if('type' in datapoint && datapoint.type === 0) {
if('pets' in datapoint && Array.isArray(datapoint.pets) && datapoint.pets.length > 0) {
for(let p = 0; p < datapoint.pets.length; p++) {
if('name' in datapoint.pets[p] && pet_name === datapoint.pets[p].name) {
if('movements' in datapoint && Array.isArray(datapoint.movements) && datapoint.movements.length > 0) {
for(let m = 0; m < datapoint.movements.length; m++) {
if('direction' in datapoint.movements[m] && datapoint.movements[m].direction !== 0) {
if('created_at' in datapoint && 'devices' in datapoint && Array.isArray(datapoint.devices) && datapoint.devices.length > 0) {
for(let d = 0; d < datapoint.devices.length; d++) {
if('product_id' in datapoint.devices[d] && (datapoint.devices[d].product_id === DEVICE_TYPE_CAT_FLAP || datapoint.devices[d].product_id === DEVICE_TYPE_PET_FLAP)) {
if('name' in datapoint.devices[d]) {
if(!('last_time' in data) || new Date(datapoint.created_at) > new Date(data.last_time)) {
data.last_direction = datapoint.movements[m].direction;
data.last_flap = datapoint.devices[d].name;
data.last_time = datapoint.created_at;
}
}
}
}
}
}
}
}
}
}
}
}
}
}
return data;
}

/**
* calculates time outside data for pet
* @param {number} pet
* @returns {object} time outside data object
*/
calculateTimeOutside(pet) {
const data = {};
data.count = 0;
data.time_spent_outside = 0;
for (let i = 0; i < this.sureFlapReport[pet].movement.datapoints.length; i++) {
const datapoint = this.sureFlapReport[pet].movement.datapoints[i];
if ('from' in datapoint && 'to' in datapoint && !('active' in datapoint)) {
if (this.isToday(new Date(datapoint.to))) {
data.count++;
if('duration' in datapoint && this.isToday(new Date(datapoint.from))) {
data.time_spent_outside += datapoint.duration;
} else {
if(this.isToday(new Date(datapoint.from))) {
data.time_spent_outside += Math.floor((new Date(datapoint.to).getTime() - new Date(datapoint.from).getTime()) / 1000);
} else {
const todayMidnight = new Date();
todayMidnight.setHours(0,0,0,0);
data.time_spent_outside += Math.floor((new Date(datapoint.to).getTime() - todayMidnight.getTime()) / 1000);
}
}
this.log.silly(`datapoint '${i}' is time spent outside today`);
}
}
}
return data;
}

/**
* calculates food consumption data for pet
* @param {number} pet
Expand All @@ -2500,18 +2640,23 @@ class Sureflap extends utils.Adapter {
for (let i = 0; i < this.sureFlapReport[pet].feeding.datapoints.length; i++) {
const datapoint = this.sureFlapReport[pet].feeding.datapoints[i];
if (datapoint.context === 1) {
data.last_time = datapoint.to;
if(new Date(datapoint.to) > new Date(data.last_time)) {
data.last_time = datapoint.to;
}
if (this.isToday(new Date(datapoint.to))) {
data.count++;
data.time_spent += new Date(datapoint.to).getTime() - new Date(datapoint.from).getTime();
if('duration' in datapoint) {
data.time_spent += datapoint.duration;
} else {
data.time_spent += Math.floor((new Date(datapoint.to).getTime() - new Date(datapoint.from).getTime()) / 1000);
}
this.log.silly(`datapoint '${i}' is food eaten today`);
for (let b = 0; b < datapoint.weights.length; b++) {
data.weight[datapoint.weights[b].food_type_id] -= datapoint.weights[b].change;
}
}
}
}
data.time_spent = Math.floor(data.time_spent / 1000);
return data;
}

Expand All @@ -2529,16 +2674,21 @@ class Sureflap extends utils.Adapter {
for (let i = 0; i < this.sureFlapReport[pet].drinking.datapoints.length; i++) {
const datapoint = this.sureFlapReport[pet].drinking.datapoints[i];
if (datapoint.context === 1) {
data.last_time = datapoint.to;
if(new Date(datapoint.to) > new Date(data.last_time)) {
data.last_time = datapoint.to;
}
if (this.isToday(new Date(datapoint.to))) {
data.count++;
data.time_spent += new Date(datapoint.to).getTime() - new Date(datapoint.from).getTime();
if('duration' in datapoint) {
data.time_spent += datapoint.duration;
} else {
data.time_spent += Math.floor((new Date(datapoint.to).getTime() - new Date(datapoint.from).getTime()) / 1000);
}
this.log.silly(`datapoint '${i}' is water drunk today`);
data.weight -= datapoint.weights[0].change;
}
}
}
data.time_spent = Math.floor(data.time_spent / 1000);
return data;
}

Expand Down Expand Up @@ -2781,6 +2931,20 @@ class Sureflap extends utils.Adapter {
return '';
}

/**
* returns the household index of given household id
* @param {string} id a household id
* @return {number} household index
*/
getHouseholdIndexForId(id) {
for (let i=0; i < this.sureFlapState.households.length; i++) {
if (this.sureFlapState.households[i].id === id) {
return i;
}
}
return -1;
}

/**
* normalizes lockmode by changing lockmode 4 to 0
* Catflap has 4 lockmodes, Petflap has 5 lockmodes (extra mode for curfew)
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit ba09934

Please sign in to comment.