homebridge-syntex-tuya
Version:
A webhook plugin for Tuya devices
329 lines (279 loc) • 8.36 kB
JavaScript
const querystring = require('querystring');
// TODO: Translate All Errors
class Session
{
constructor(accessToken, refreshToken, expiresIn, areaCode, areaBaseUrl)
{
this.accessToken;
this.refreshToken;
this.expiresOn;
this.areaCode = areaCode;
this.areaBaseUrl = areaBaseUrl;
this.resetToken(accessToken, refreshToken, expiresIn);
}
resetToken(accessToken, refreshToken, expiresIn)
{
this.accessToken = accessToken;
this.refreshToken = refreshToken;
this.expiresOn = this.getCurrentEpoch() + expiresIn - 100; // subtract 100 ticks to expire token before it actually does
}
hasToken()
{
return this.accessToken && true;
}
isTokenExpired()
{
return this.expiresOn < this.getCurrentEpoch();
}
hasValidToken()
{
return this.hasToken() && !this.isTokenExpired();
}
getCurrentEpoch()
{
return Math.round((new Date()).getTime() / 1000);
}
}
module.exports = class TuyaWebApi
{
constructor(platform)
{
this.logger = platform.logger;
this.username = platform.username;
this.password = platform.password;
this.countryCode = platform.countryCode;
this.tuyaPlatform = platform.tuyaPlatform;
this.RequestManager = platform.RequestManager;
this.session = new Session();
this.authBaseUrl = 'https://px1.tuyaeu.com';
}
discoverDevices()
{
if(!this.session.hasValidToken())
{
throw new Error('No valid token');
}
var body = {
header : {
name : 'Discovery',
namespace : 'discovery',
payloadVersion : 1
},
payload : {
accessToken : this.session.accessToken
}
};
return new Promise((resolve, reject) => {
this.sendRequest(this.session.areaBaseUrl + '/homeassistant/skill', { body }, (data) => {
if(data != null && data.header != null && data.header.code == 'SUCCESS' && data.payload != null && data.payload.devices != null)
{
resolve(data.payload.devices);
}
else
{
if(data != null)
{
if(data.header != null && data.header.code === 'FrequentlyInvoke')
{
this.logger.log('error', 'bridge', 'Bridge', '%device_discovery% %frequently_invoke%! %query_once%: ' + JSON.stringify(data.header.msg).split('once in ')[1].split(' seconds')[0] + 's');
}
else
{
this.logger.log('error', 'bridge', 'Bridge', '%device_discovery% %invalid_response%! ( ' + JSON.stringify(data) + ' )');
}
}
reject();
}
});
});
}
getDeviceState(service)
{
if(!this.session.hasValidToken())
{
throw new Error('No valid token');
}
var body = {
header : {
name : 'QueryDevice',
namespace : 'query',
payloadVersion : 1
},
payload : {
accessToken : this.session.accessToken,
devId : service.sid,
value : 1
}
};
return new Promise((resolve, reject) => {
this.sendRequest(this.session.areaBaseUrl + '/homeassistant/skill', { body }, (data) => {
if(data != null && data.header != null && data.header.code == 'SUCCESS' && data.payload != null && data.payload.data != null)
{
resolve(data.payload.data);
}
else
{
if(data != null)
{
if(data.header != null && data.header.code === 'FrequentlyInvoke')
{
this.logger.log('error', service.id, service.letters, '[' + service.name + '] %frequently_invoke%! %query_once%: ' + JSON.stringify(data.header.msg).split('once in ')[1].split(' seconds')[0] + 's ( ' + service.sid + ' )');
}
else
{
this.logger.log('error', service.id, service.letters, '[' + service.name + '] %invalid_response%! ( ' + JSON.stringify(data) + ' )');
}
}
reject();
}
});
});
}
setDeviceState(service, method, payload = {})
{
if(!this.session.hasValidToken())
{
throw new Error('No valid token');
}
/* Methods
* turnOnOff -> 0 = off, 1 = on
* brightnessSet --> 0..100
*/
var body = {
header : {
name : method,
namespace : 'control',
payloadVersion : 1
},
payload : payload
}
body.payload.accessToken = this.session.accessToken;
body.payload.devId = service.sid;
return new Promise((resolve, reject) => {
this.sendRequest(this.session.areaBaseUrl + '/homeassistant/skill', { body }, (data) => {
if(data != null && data.header != null && data.header.code == 'SUCCESS')
{
service.setConnectionState(true, () => resolve(), true);
}
else
{
if(data != null)
{
if(data.header != null && data.header.code === 'TargetOffline')
{
service.setConnectionState(false, () => {}, true);
this.logger.log('error', service.id, service.letters, '[' + service.name + '] %accessory_offline%! ( ' + service.sid + ' )');
}
else if(data.header != null && data.header.code === 'FrequentlyInvoke')
{
this.logger.log('error', service.id, service.letters, '[' + service.name + '] %frequently_invoke%! %query_once%: ' + JSON.stringify(data.header.msg).split('once in ')[1].split(' seconds')[0] + 's ( ' + service.sid + ' )');
}
else
{
this.logger.log('error', service.id, service.letters, '[' + service.name + '] %invalid_response%! ( ' + JSON.stringify(data) + ' )');
}
}
reject();
}
});
});
}
getOrRefreshToken()
{
if(!this.session.hasToken())
{
if(!this.username)
{
throw new Error('No username configured');
}
if(!this.password)
{
throw new Error('No password configured');
}
if(this.username && this.password && this.countryCode)
{
const form = {
userName : this.username,
password : this.password,
countryCode : this.countryCode,
bizType : this.tuyaPlatform,
from : 'tuya',
};
const formData = querystring.stringify(form);
const contentLength = formData.length;
return new Promise((resolve, reject) => {
var options = {
headers : {
'Content-Length' : contentLength,
'Content-Type' : 'application/x-www-form-urlencoded'
},
data : formData,
method : 'POST'
};
this.RequestManager.fetch(this.authBaseUrl + '/homeassistant/auth.do', options).then((response) => {
if(response.data instanceof Object)
{
if(response.data.responseStatus === 'error')
{
reject(new Error('Authentication fault: ' + response.data.errorMsg));
}
else
{
// NOTE: Received token
this.session.resetToken(response.data.access_token, response.data.refresh_token, response.data.expires_in);
// NOTE: Change url based on areacode in accesstoken first two chars
this.session.areaCode = 'EU';
if(response.data.access_token)
{
this.session.areaCode = response.data.access_token.substr(0, 2);
}
switch(this.session.areaCode)
{
case 'AY':
this.session.areaBaseUrl = 'https://px1.tuyacn.com';
break;
case 'EU':
this.session.areaBaseUrl = 'https://px1.tuyaeu.com';
break;
case 'US':
default:
this.session.areaBaseUrl = 'https://px1.tuyaus.com';
}
resolve(this.session);
}
}
else
{
reject(new Error('Authentication fault, could not retreive token.' + JSON.stringify(response.error)));
}
});
});
}
}
else if(this.session.isTokenExpired())
{
return new Promise((resolve, reject) => {
this.sendRequest(this.session.areaBaseUrl + '/homeassistant/access.do?grant_type=refresh_token&refresh_token=' + this.session.refreshToken, '', 'GET', (data) => {
if(data != null)
{
this.session.resetToken(data.access_token, data.refresh_token, data.expires_in);
resolve(this.session);
}
else
{
reject();
}
});
});
}
}
sendRequest(url, options, callback)
{
this.RequestManager.fetch(url, { data : options.body, verbose : true }).then((response) => {
if(callback != null)
{
callback(response.data);
}
});
}
}