iobroker.sureflap
Version:
Adpater for smart pet devices from Sure Petcare
559 lines (515 loc) • 19.6 kB
JavaScript
'use strict';
const https = require('https');
// Constants - timeout for https requests
const REQUEST_TIMEOUT = 120000;
/**
* SurepetCareApi
*
* Connects to SurepetCareApi via Rest and pulls updates.
*/
class SurepetApi {
/**
* @param {import('@iobroker/adapter-core').AdapterInstance} adapter ioBroker adapter instance
*/
constructor(adapter) {
this.adapter = adapter;
}
/**
* does a login via the surepet API
*
* @returns {Promise} Promise of an auth token
*/
doLoginAndGetAuthToken() {
return this.doLoginAndGetAuthTokenForHostAndUsernameAndPassword(
this.adapter.config.api_host,
this.adapter.config.username,
this.adapter.config.password,
);
}
/**
* does a login via the surepet API
*
* @param {string} host a hostname
* @param {string} username a username
* @param {string} password a password
* @returns {Promise} Promise of an auth token
*/
doLoginAndGetAuthTokenForHostAndUsernameAndPassword(host, username, password) {
return new Promise((resolve, reject) => {
if (host === undefined || host === null || host === '') {
return reject(new Error('No host provided.'));
}
if (username === undefined || username === null || username === '') {
return reject(new Error('No username provided.'));
}
if (password === undefined || password === null || password === '') {
return reject(new Error('No password provided.'));
}
const postData = JSON.stringify(this.buildLoginJsonData(username, password));
const options = this.buildOptionsForHostAndPathAndMethod(host, '/api/auth/login', 'POST', '');
this.adapter.log.debug(`login with json: ${this.replacePassword(postData)}`);
this.httpsRequest(options, postData)
.then(result => {
if (result === undefined || result.data === undefined || !('token' in result.data)) {
return reject(new Error(`login failed. possible wrong login or pwd?`));
}
return resolve(result.data['token']);
})
.catch(error => {
return reject(error);
});
});
}
/********************************************
* methods to get data from surepetcare API *
********************************************/
/**
* get households
*
* @param {string} authToken an authentication token for SurepetCareApi
* @returns {Promise} Promise of an array of households
*/
getHouseholds(authToken) {
return new Promise((resolve, reject) => {
const options = this.buildOptions('/api/household', 'GET', authToken);
this.httpsRequest(options, '')
.then(result => {
if (
result === undefined ||
result.data === undefined ||
!Array.isArray(result.data) ||
result.data.size === 0
) {
return reject(new Error(`getting households failed.`));
}
return resolve(result.data);
})
.catch(error => {
return reject(error);
});
});
}
/**
* get pets
*
* @param {string} authToken an authentication token for SurepetCareApi
* @returns {Promise} Promise of an array of pets
*/
getPets(authToken) {
return new Promise((resolve, reject) => {
const options = this.buildOptions('/api/pet', 'GET', authToken);
this.httpsRequest(options, '')
.then(result => {
if (result === undefined || result.data === undefined) {
return reject(new Error(`getting pets failed.`));
}
return resolve(result.data);
})
.catch(error => {
return reject(error);
});
});
}
/**
* get devices for the given household
*
* @param {string} authToken an authentication token for SurepetCareApi
* @param {number} householdId a household ID
* @returns {Promise} Promise of an array of devices
*/
getDevicesForHousehold(authToken, householdId) {
return new Promise((resolve, reject) => {
const options = this.buildOptions(`/api/device?householdid=${householdId}`, 'GET', authToken);
this.httpsRequest(options, '')
.then(result => {
if (result === undefined || result.data === undefined || !Array.isArray(result.data)) {
return reject(new Error(`getting devices failed.`));
}
return resolve(result.data);
})
.catch(error => {
return reject(error);
});
});
}
/**
* get history for the given household
*
* @param {string} authToken an authentication token for SurepetCareApi
* @param {number} householdId a household ID
* @returns {Promise} Promise of an array of history events
*/
getHistoryForHousehold(authToken, householdId) {
return new Promise((resolve, reject) => {
const options = this.buildOptions(`/api/timeline/household/${householdId}?page_size=25`, 'GET', authToken);
this.httpsRequest(options, '')
.then(result => {
if (result === undefined || result.data === undefined) {
return reject(new Error(`getting history failed.`));
}
return resolve(result.data);
})
.catch(error => {
return reject(error);
});
});
}
/**
* get report data for the given pet
*
* @param {string} authToken an authentication token for SurepetCareApi
* @param {number} householdId a household ID
* @param {number} petId a pet ID
* @returns {Promise} Promise of an array of pet reports
*/
getReportForPet(authToken, householdId, petId) {
return new Promise((resolve, reject) => {
const now = new Date();
const from = this.getReportFromDate(now);
const to = this.getReportToDate(now);
const options = this.buildOptions(
`/api/v2/report/household/${householdId}/pet/${petId}/aggregate?from=${from}&to=${to}`,
'GET',
authToken,
);
this.httpsRequest(options, '')
.then(result => {
if (result === undefined || result.data === undefined) {
return reject(new Error(`getting pet reports failed.`));
}
return resolve(result.data);
})
.catch(error => {
return reject(error);
});
});
}
/******************************************
* methods to set data to surepetcare API *
******************************************/
/**
* set hub led mode
*
* @param {string} authToken an authentication token for SurepetCareApi
* @param {number} deviceId a device ID of a hub
* @param {number} ledMode the LED mode to set
* @returns {Promise} a promise
*/
setLedModeForHub(authToken, deviceId, ledMode) {
return new Promise((resolve, reject) => {
const postData = JSON.stringify({ led_mode: ledMode });
const options = this.buildOptions(`/api/device/${deviceId}/control/async`, 'PUT', authToken);
this.httpsRequest(options, postData)
.then(() => {
return resolve();
})
.catch(error => {
return reject(error);
});
});
}
/**
* set feeder close delay
*
* @param {string} authToken an authentication token for SurepetCareApi
* @param {number} deviceId a device ID of a feeder
* @param {number} closeDelay the close delay to set
* @returns {Promise} a promise
*/
setCloseDelayForFeeder(authToken, deviceId, closeDelay) {
return new Promise((resolve, reject) => {
const postData = JSON.stringify({ lid: { close_delay: closeDelay } });
const options = this.buildOptions(`/api/device/${deviceId}/control/async`, 'PUT', authToken);
this.httpsRequest(options, postData)
.then(() => {
return resolve();
})
.catch(error => {
return reject(error);
});
});
}
/**
* set flap pet type
*
* @param {string} authToken an authentication token for SurepetCareApi
* @param {number} deviceId a device ID
* @param {number} petTag a tag ID of a pet
* @param {number} petType the pet type to set
* @returns {Promise} a promise
*/
setPetTypeForFlapAndPet(authToken, deviceId, petTag, petType) {
return new Promise((resolve, reject) => {
const postData = JSON.stringify([{ tag_id: petTag, request_action: 0, profile: petType }]);
const options = this.buildOptions(`/api/v2/device/${deviceId}/tag/async`, 'PUT', authToken);
this.httpsRequest(options, postData)
.then(() => {
return resolve();
})
.catch(error => {
return reject(error);
});
});
}
/**
* assign or unassign a pet to/from a device
*
* @param {string} authToken an authentication token for SurepetCareApi
* @param {number} deviceId a device ID
* @param {number} petTag a tag ID of a pet
* @param {boolean} assign whether to assign or remove the pet from the device
* @returns {Promise} a promise
*/
setPetAssignmentForDevice(authToken, deviceId, petTag, assign) {
return new Promise((resolve, reject) => {
const postData = JSON.stringify([{ tag_id: petTag, request_action: assign ? 1 : 2 }]);
const options = this.buildOptions(`/api/v2/device/${deviceId}/tag/async`, 'PUT', authToken);
this.httpsRequest(options, postData)
.then(() => {
return resolve();
})
.catch(error => {
return reject(error);
});
});
}
/**
* set flap lockmode
*
* @param {string} authToken an authentication token for SurepetCareApi
* @param {number} deviceId a device ID
* @param {number} lockMode the lock mode to set
* @returns {Promise} a promise
*/
setLockModeForFlap(authToken, deviceId, lockMode) {
return new Promise((resolve, reject) => {
const postData = JSON.stringify({ locking: lockMode });
const options = this.buildOptions(`/api/device/${deviceId}/control`, 'PUT', authToken);
this.httpsRequest(options, postData)
.then(() => {
return resolve();
})
.catch(error => {
return reject(error);
});
});
}
/**
* set pet location
*
* @param {string} authToken an authentication token for SurepetCareApi
* @param {number} petId a pet ID
* @param {number} location the location to set
* @returns {Promise} a promise
*/
setLocationForPet(authToken, petId, location) {
return new Promise((resolve, reject) => {
const postData = JSON.stringify({
where: location,
since: this.getDateFormattedAsISOWithTimezone(new Date()),
});
const options = this.buildOptions(`/api/pet/${petId}/position`, 'POST', authToken);
this.httpsRequest(options, postData)
.then(() => {
return resolve();
})
.catch(error => {
return reject(error);
});
});
}
/**
* set flap curfew
*
* @param {string} authToken an authentication token for SurepetCareApi
* @param {number} deviceId a device ID
* @param {object} curfew the curfew to set
* @returns {Promise} a promise
*/
setCurfewForFlap(authToken, deviceId, curfew) {
return new Promise((resolve, reject) => {
const postData = JSON.stringify({ curfew });
const options = this.buildOptions(`/api/device/${deviceId}/control/async`, 'PUT', authToken);
this.httpsRequest(options, postData)
.then(() => {
return resolve();
})
.catch(error => {
return reject(error);
});
});
}
/**************************
* general https methods *
**************************/
/**
* builds a json options object for a https request
*
* @param {string} path a url path
* @param {string} method a http method
* @param {string} authToken an authentication token for SurepetCareApi
* @returns {object} a json object
*/
buildOptions(path, method, authToken) {
return this.buildOptionsForHostAndPathAndMethod(this.adapter.config.api_host, path, method, authToken);
}
/**
* builds a json options object for a https request
*
* @param {string} host a hostname
* @param {string} path a url path
* @param {string} method a http method
* @param {string} authToken an authentication token for SurepetCareApi
* @returns {object} a json object
*/
buildOptionsForHostAndPathAndMethod(host, path, method, authToken) {
const options = {
hostname: host,
port: 443,
path: path,
method: method,
timeout: REQUEST_TIMEOUT,
headers: {
Accept: 'application/json, text/plain, */*',
'Cache-Control': 'no-cache',
'Content-Type': 'application/json;charset=utf-8',
Host: host,
Origin: 'https://production.surehub.io',
Pragma: 'no-cache',
Referer: 'https://production.surehub.io/',
'spc-client-type': 'react',
'User-Agent': 'ioBroker/7.0',
},
};
if (authToken !== undefined && authToken !== '') {
options.headers['Authorization'] = `Bearer ${authToken}`;
}
return options;
}
/**
* builds a json login data object
*
* @param {string} username a username
* @param {string} password a password
* @returns {object} a login json object
*/
buildLoginJsonData(username, password) {
return {
email_address: username,
password: password,
device_id: '1050547954',
};
}
/**
* does a https request
*
* @param {object} options http options
* @param {object} postData post body data
* @returns {Promise} Promise of an response JSon object
*/
httpsRequest(options, postData) {
return new Promise((resolve, reject) => {
const path = options.path.split('?')[0];
this.adapter.log.silly(`doing https request to '${path}'`);
const req = https.request(options, res => {
if (res.statusCode && (res.statusCode < 200 || res.statusCode >= 300)) {
this.adapter.log.debug(`Request (${path}) returned status code ${res.statusCode}.`);
return reject(new Error(`Request returned status code ${res.statusCode}.`));
}
const data = [];
res.on('data', chunk => {
data.push(chunk);
});
res.on('end', () => {
try {
const obj = JSON.parse(data.join(''));
return resolve(obj);
} catch (err) {
if (err instanceof Error) {
this.adapter.log.debug(`JSon parse error in data: '${data}'`);
}
this.adapter.log.debug(`Response (${path}) error.`);
return reject(new Error(`Response error: '${err}'.`));
}
});
res.on('error', err => {
this.adapter.log.debug(`Response (${path}) error.`);
return reject(new Error(`Response error: '${err}'.`));
});
});
req.on('error', err => {
this.adapter.log.debug(`Request (${path}) error.`);
return reject(new Error(`Request error: '${err}'.`));
});
req.on('timeout', () => {
req.destroy();
this.adapter.log.debug(`Request (${path}) timeout.`);
return reject(new Error(`Request timeout.`));
});
req.write(postData);
req.end();
});
}
/***************************
* general helper methods *
***************************/
/**
* replaces password from json data with ******
*
* @param {string} jsonString a json string
* @returns {string} a json string without password
*/
replacePassword(jsonString) {
const json = JSON.parse(jsonString);
json.password = '******';
return JSON.stringify(json);
}
/**
* returns a date 7 days back with time set to 00:00:00
*
* @param {Date} date a date
* @returns {string} a date as ISO string
*/
getReportFromDate(date) {
const from = new Date(date.toDateString());
from.setDate(from.getDate() - 7);
return from.toISOString();
}
/**
* returns a date with time set to 23:59:59
*
* @param {Date} date a date
* @returns {string} a date as ISO string
*/
getReportToDate(date) {
const to = new Date(date.toDateString());
to.setHours(23, 59, 59, 999);
return to.toISOString();
}
/**
* returns given date in ISO format with timezone
*
* @param {Date} date a date
* @returns {string} a data string in ISO format with timezone
*/
getDateFormattedAsISOWithTimezone(date) {
const tzo = -date.getTimezoneOffset();
const dif = tzo >= 0 ? '+' : '-';
return `${date.getFullYear()}-${this.padZero(date.getMonth() + 1)}-${this.padZero(
date.getDate(),
)}T${this.padZero(date.getHours())}:${this.padZero(date.getMinutes())}:${this.padZero(date.getSeconds())}${
dif
}${this.padZero(tzo / 60)}:${this.padZero(tzo % 60)}`;
}
/**
* adds a leading zero if number is < 10
*
* @param {number} num a number
* @returns {string} a string
*/
padZero(num) {
const norm = Math.floor(Math.abs(num));
return (norm < 10 ? '0' : '') + norm;
}
}
module.exports = SurepetApi;