homey-api
Version:
1,022 lines (862 loc) • 29.8 kB
JavaScript
'use strict';
const SocketIOClient = require('socket.io-client');
const APIErrorHomeyOffline = require('../APIErrorHomeyOffline');
const Util = require('../Util');
const HomeyAPI = require('./HomeyAPI');
const HomeyAPIError = require('./HomeyAPIError');
const ManagerApps = require('./HomeyAPIV3/ManagerApps');
const ManagerDrivers = require('./HomeyAPIV3/ManagerDrivers');
const ManagerDevices = require('./HomeyAPIV3/ManagerDevices');
const ManagerFlow = require('./HomeyAPIV3/ManagerFlow');
const ManagerFlowToken = require('./HomeyAPIV3/ManagerFlowToken');
const ManagerInsights = require('./HomeyAPIV3/ManagerInsights');
const ManagerUsers = require('./HomeyAPIV3/ManagerUsers');
// eslint-disable-next-line no-unused-vars
const Manager = require('./HomeyAPIV3/Manager');
const tierCache = {};
const versionCache = {};
/**
* An authenticated Homey API. Do not construct this class manually.
* @class
* @hideconstructor
* @extends HomeyAPI
*/
class HomeyAPIV3 extends HomeyAPI {
static MANAGERS = {
ManagerApps,
ManagerDrivers,
ManagerDevices,
ManagerFlow,
ManagerFlowToken,
ManagerInsights,
ManagerUsers,
};
constructor({
properties,
strategy = [
HomeyAPI.DISCOVERY_STRATEGIES.MDNS,
HomeyAPI.DISCOVERY_STRATEGIES.CLOUD,
HomeyAPI.DISCOVERY_STRATEGIES.LOCAL,
HomeyAPI.DISCOVERY_STRATEGIES.LOCAL_SECURE,
HomeyAPI.DISCOVERY_STRATEGIES.REMOTE_FORWARDED,
],
baseUrl = null,
token = null,
session = null,
reconnect = true,
...props
}) {
super({ properties, ...props });
this.__refreshMap = {};
Object.defineProperty(this, '__baseUrl', {
value: null,
enumerable: false,
writable: true,
});
Object.defineProperty(this, '__strategyId', {
value: null,
enumerable: false,
writable: true,
});
Object.defineProperty(this, '__reconnect', {
value: reconnect,
enumerable: false,
writable: true,
});
Object.defineProperty(this, '__destroyed', {
value: false,
enumerable: false,
writable: true,
});
Object.defineProperty(this, '__token', {
value: token,
enumerable: false,
writable: true,
});
Object.defineProperty(this, '__session', {
value: session,
enumerable: false,
writable: true,
});
Object.defineProperty(this, '__strategies', {
value: Array.isArray(strategy) ? strategy : [strategy],
enumerable: false,
writable: false,
});
Object.defineProperty(this, '__managers', {
value: {},
enumerable: false,
writable: false,
});
Object.defineProperty(this, '__baseUrlPromise', {
value: typeof baseUrl === 'string' ? Promise.resolve(baseUrl) : null,
enumerable: false,
writable: true,
});
Object.defineProperty(this, '__loginPromise', {
value: null,
enumerable: false,
writable: true,
});
this.generateManagersFromSpecification();
}
/*
* Get the Homey's base URL promise
*/
get baseUrl() {
return (async () => {
if (!this.__baseUrlPromise) {
this.__baseUrlPromise = this.discoverBaseUrl().then(({ baseUrl }) => {
return baseUrl;
});
this.__baseUrlPromise.catch(() => {});
}
return this.__baseUrlPromise;
})();
}
get tier() {
return tierCache[this.id];
}
get version() {
return versionCache[this.id];
}
get strategyId() {
return this.__strategyId;
}
/*
* Generate Managers from JSON specification
* A manager instance is created when it's first accessed
*/
getSpecification() {
// eslint-disable-next-line global-require
return require('../../assets/specifications/HomeyAPIV2.json');
}
generateManagersFromSpecification() {
const { managers } = this.getSpecification();
Object.entries(managers).forEach(([managerName, manager]) => {
this.generateManagerFromSpecification(managerName, manager);
});
}
generateManagerFromSpecification(managerName, manager) {
Object.defineProperty(this, manager.idCamelCase, {
get: () => {
if (!this.__managers[managerName]) {
const ManagerClass = this.constructor.MANAGERS[managerName]
? this.constructor.MANAGERS[managerName]
: (() => {
return class extends Manager {};
})();
ManagerClass.ID = manager.id;
this.__managers[managerName] = new ManagerClass({
homey: this,
items: manager.items || {},
operations: manager.operations || {},
});
}
return this.__managers[managerName];
},
enumerable: false,
});
}
/*
* Discover the URL to talk to Homey
* We prefer localSecure, because it's fastest and most secure
* If that doesn't work, we prefer local OR mdns, whichever is fastest
* Finally, we fallback to cloud
*/
async discoverBaseUrl() {
const urls = {};
if (this.__strategies.includes(HomeyAPI.DISCOVERY_STRATEGIES.MDNS)) {
if (Util.isHTTPUnsecureSupported()) {
urls[HomeyAPI.DISCOVERY_STRATEGIES.MDNS] = `http://homey-${this.id}.local`;
}
}
if (this.__strategies.includes(HomeyAPI.DISCOVERY_STRATEGIES.LOCAL)) {
if (Util.isHTTPUnsecureSupported() && this.__properties.localUrl) {
urls[HomeyAPI.DISCOVERY_STRATEGIES.LOCAL] = `${this.__properties.localUrl}`;
}
}
if (this.__strategies.includes(HomeyAPI.DISCOVERY_STRATEGIES.LOCAL_SECURE)) {
if (this.__properties.localUrlSecure) {
urls[HomeyAPI.DISCOVERY_STRATEGIES.LOCAL_SECURE] = `${this.__properties.localUrlSecure}`;
}
}
if (this.__strategies.includes(HomeyAPI.DISCOVERY_STRATEGIES.CLOUD)) {
if (this.__properties.remoteUrl) {
urls[HomeyAPI.DISCOVERY_STRATEGIES.CLOUD] = `${this.__properties.remoteUrl}`;
}
}
if (this.__strategies.includes(HomeyAPI.DISCOVERY_STRATEGIES.REMOTE_FORWARDED)) {
if (this.__properties.remoteUrlForwarded) {
urls[HomeyAPI.DISCOVERY_STRATEGIES.REMOTE_FORWARDED] = `${this.__properties.remoteUrlForwarded}`;
}
}
if (!Object.keys(urls).length) {
throw new Error('No Discovery Strategies Available');
}
// Don't discover, just set the only strategy
if (Object.keys(urls).length === 1) {
this.__baseUrl = Object.values(urls)[0];
this.__strategyId = Object.keys(urls)[0];
return {
baseUrl: this.__baseUrl,
strategyId: this.__strategyId,
};
}
this.__debug(`Discovery Strategies: ${Object.keys(urls).join(',')}`);
// Create the returned Promise
let resolve;
let reject;
const promise = new Promise((resolve_, reject_) => {
resolve = resolve_;
reject = reject_;
});
promise
.then(({ baseUrl, strategyId }) => {
this.__baseUrl = baseUrl;
this.__strategyId = strategyId;
})
.catch(() => {});
// Ping method
const ping = async (strategyId, timeout) => {
let pingTimeout;
const baseUrl = urls[strategyId];
return Promise.race([
Util.fetch(`${baseUrl}/api/manager/system/ping?id=${this.id}`, {
headers: {
'X-Homey-ID': this.id,
},
}).then(async res => {
const text = await res.text();
if (!res.ok) throw new Error(text || res.statusText);
if (text === 'false') throw new Error('Invalid Homey ID');
const homeyId = res.headers.get('X-Homey-ID');
if (homeyId) {
if (homeyId !== this.id) throw new Error('Invalid Homey ID'); // TODO: Add to Homey Connect
}
// Set the version that Homey told us.
// It's the absolute truth, because the Cloud API may be behind.
const homeyVersion = res.headers.get('X-Homey-Version');
if (homeyVersion !== this.version) {
this.version = homeyVersion;
}
return {
baseUrl,
strategyId,
};
}),
new Promise((_, reject) => {
pingTimeout = setTimeout(() => reject(new Error('PingTimeout')), timeout);
}),
]).finally(() => clearTimeout(pingTimeout));
};
const pings = {};
// Ping localSecure (https://xxx-xxx-xxx-xx.homey.homeylocal.com)
if (urls[HomeyAPI.DISCOVERY_STRATEGIES.LOCAL_SECURE]) {
pings[HomeyAPI.DISCOVERY_STRATEGIES.LOCAL_SECURE] = ping(HomeyAPI.DISCOVERY_STRATEGIES.LOCAL_SECURE, 5000);
pings[HomeyAPI.DISCOVERY_STRATEGIES.LOCAL_SECURE].catch(err => {
this.__debug(`Ping ${HomeyAPI.DISCOVERY_STRATEGIES.LOCAL_SECURE} Error:`, err && err.message);
this.__debug(urls[HomeyAPI.DISCOVERY_STRATEGIES.LOCAL_SECURE]);
});
}
// Ping local (http://xxx-xxx-xxx-xxx)
if (urls[HomeyAPI.DISCOVERY_STRATEGIES.LOCAL]) {
pings[HomeyAPI.DISCOVERY_STRATEGIES.LOCAL] = ping(HomeyAPI.DISCOVERY_STRATEGIES.LOCAL, 1000);
pings[HomeyAPI.DISCOVERY_STRATEGIES.LOCAL].catch(err =>
this.__debug(`Ping ${HomeyAPI.DISCOVERY_STRATEGIES.LOCAL} Error:`, err && err.message)
);
}
// Ping mdns (http://homey-<homeyId>.local)
if (urls[HomeyAPI.DISCOVERY_STRATEGIES.MDNS]) {
pings[HomeyAPI.DISCOVERY_STRATEGIES.MDNS] = ping(HomeyAPI.DISCOVERY_STRATEGIES.MDNS, 3000);
pings[HomeyAPI.DISCOVERY_STRATEGIES.MDNS].catch(err =>
this.__debug(`Ping ${HomeyAPI.DISCOVERY_STRATEGIES.MDNS} Error:`, err && err.message)
);
}
// Ping cloud (https://<homeyId>.connect.athom.com)
if (urls[HomeyAPI.DISCOVERY_STRATEGIES.CLOUD]) {
pings[HomeyAPI.DISCOVERY_STRATEGIES.CLOUD] = ping(HomeyAPI.DISCOVERY_STRATEGIES.CLOUD, 5000);
pings[HomeyAPI.DISCOVERY_STRATEGIES.CLOUD].catch(err =>
this.__debug(`Ping ${HomeyAPI.DISCOVERY_STRATEGIES.CLOUD} Error:`, err && err.message)
);
}
// Ping Direct (https://xxx-xxx-xxx-xx.homey.homeylocal.com:12345)
if (urls[HomeyAPI.DISCOVERY_STRATEGIES.REMOTE_FORWARDED]) {
pings[HomeyAPI.DISCOVERY_STRATEGIES.REMOTE_FORWARDED] = ping(
HomeyAPI.DISCOVERY_STRATEGIES.REMOTE_FORWARDED,
2000
);
pings[HomeyAPI.DISCOVERY_STRATEGIES.REMOTE_FORWARDED].catch(err =>
this.__debug(`Ping ${HomeyAPI.DISCOVERY_STRATEGIES.REMOTE_FORWARDED} Error:`, err && err.message)
);
}
// Select the best route
if (pings[HomeyAPI.DISCOVERY_STRATEGIES.LOCAL_SECURE]) {
pings[HomeyAPI.DISCOVERY_STRATEGIES.LOCAL_SECURE]
.then(result => resolve(result))
.catch(() => {
const promises = [];
if (pings[HomeyAPI.DISCOVERY_STRATEGIES.LOCAL]) {
promises.push(pings[HomeyAPI.DISCOVERY_STRATEGIES.LOCAL]);
}
if (pings[HomeyAPI.DISCOVERY_STRATEGIES.REMOTE_FORWARDED]) {
promises.push(pings[HomeyAPI.DISCOVERY_STRATEGIES.REMOTE_FORWARDED]);
}
if (pings[HomeyAPI.DISCOVERY_STRATEGIES.MDNS]) {
promises.push(pings[HomeyAPI.DISCOVERY_STRATEGIES.MDNS]);
}
if (pings[HomeyAPI.DISCOVERY_STRATEGIES.CLOUD]) {
promises.push(pings[HomeyAPI.DISCOVERY_STRATEGIES.CLOUD]);
}
if (!promises.length) {
throw new APIErrorHomeyOffline();
}
return Util.promiseAny(promises);
})
.then(result => resolve(result))
.catch(() => reject(new APIErrorHomeyOffline()));
} else if (pings[HomeyAPI.DISCOVERY_STRATEGIES.LOCAL]) {
pings[HomeyAPI.DISCOVERY_STRATEGIES.LOCAL]
.then(result => resolve(result))
.catch(() => {
if (pings[HomeyAPI.DISCOVERY_STRATEGIES.CLOUD]) {
pings[HomeyAPI.DISCOVERY_STRATEGIES.CLOUD]
.then(result => resolve(result))
.catch(err => reject(new APIErrorHomeyOffline(err)));
}
});
} else if (pings[HomeyAPI.DISCOVERY_STRATEGIES.MDNS]) {
pings[HomeyAPI.DISCOVERY_STRATEGIES.MDNS]
.then(result => resolve(result))
.catch(() => {
if (pings[HomeyAPI.DISCOVERY_STRATEGIES.CLOUD]) {
pings[HomeyAPI.DISCOVERY_STRATEGIES.CLOUD]
.then(result => resolve(result))
.catch(err => reject(new APIErrorHomeyOffline(err)));
}
});
} else if (pings[HomeyAPI.DISCOVERY_STRATEGIES.REMOTE_FORWARDED]) {
pings[HomeyAPI.DISCOVERY_STRATEGIES.REMOTE_FORWARDED]
.then(result => resolve(result))
.catch(() => {
if (pings[HomeyAPI.DISCOVERY_STRATEGIES.CLOUD]) {
pings[HomeyAPI.DISCOVERY_STRATEGIES.CLOUD]
.then(result => resolve(result))
.catch(err => reject(new APIErrorHomeyOffline(err)));
}
});
} else if (pings[HomeyAPI.DISCOVERY_STRATEGIES.CLOUD]) {
pings[HomeyAPI.DISCOVERY_STRATEGIES.CLOUD]
.then(result => resolve(result))
.catch(err => reject(new APIErrorHomeyOffline(err)));
} else {
reject(new APIErrorHomeyOffline());
}
return promise;
}
async call({
$timeout = 10000,
method,
headers,
path,
body,
json = true,
isRetryAfterRefresh = false,
shouldRetry = true,
}) {
const token = this.__token;
const baseUrl = await this.baseUrl;
method = String(method).toUpperCase();
headers = {
...headers,
'X-Homey-ID': this.id,
};
if (token && path !== '/api/manager/users/login') {
headers['Authorization'] = `Bearer ${token}`;
}
const originalBody = body;
if (['PUT', 'POST'].includes(method)) {
if (body && json === true) {
headers['Content-Type'] = 'application/json';
body = JSON.stringify(body);
}
} else {
body = undefined;
}
this.__debug(method, `${baseUrl}${path}`);
const res = await Util.timeout(
Util.fetch(`${baseUrl}${path}`, {
method,
headers,
body,
}),
$timeout
);
const resStatusCode = res.status;
if (resStatusCode === 204) return undefined;
const resStatusText = res.status;
const resHeadersContentType = res.headers.get('Content-Type');
const version = res.headers.get('x-homey-version');
if (version) {
versionCache[this.id] = version;
}
const tier = res.headers.get('x-homey-tier');
if (tier) {
tierCache[this.id] = tier;
}
const resBodyText = await res.text();
let resBodyJson;
if (resHeadersContentType && resHeadersContentType.startsWith('application/json')) {
try {
resBodyJson = JSON.parse(resBodyText);
// eslint-disable-next-line no-empty
} catch (err) {}
}
if (!res.ok) {
// If Session Expired, clear the stored token
if (resStatusCode === 401 && shouldRetry === true && token != null) {
this.__debug('Session expired');
await this.refreshForToken(token, isRetryAfterRefresh);
if (!isRetryAfterRefresh) {
return this.call({
method,
headers,
path,
body: originalBody,
isRetryAfterRefresh: true,
});
}
}
if (resBodyJson) {
throw new HomeyAPIError(
{
error: resBodyJson.error,
error_description: resBodyJson.error_description,
stack: resBodyJson.stack,
},
resStatusCode
);
}
if (resBodyText) {
throw new HomeyAPIError(
{
error: resBodyText,
},
resStatusCode
);
}
throw new HomeyAPIError(
{
error: resStatusText,
},
resStatusCode
);
}
if (typeof resBodyJson !== 'undefined') {
return resBodyJson;
}
return resBodyText;
}
async login() {
if (!this.__loginPromise) {
this.__loginPromise = Promise.resolve().then(async () => {
// Check store for a valid Homey.Session
const store = await this.__getStore();
if (store && store.token && store.session) {
this.__debug('Got token from store');
this.__token = store.token;
this.__session = store.session;
return;
}
// Create a Session by generating a JWT token on AthomCloudAPI,
// and then sending the JWT token to Homey.
if (this.__api) {
this.__debug('Retrieving token...');
const jwtToken = await this.__api.createDelegationToken({ audience: 'homey' });
const token = await this.users.login({
$socket: false,
token: jwtToken,
shouldRetry: false,
});
this.__token = token;
const session = await this.sessions.getSessionMe({
$socket: false,
shouldRetry: false,
});
this.__session = session;
await this.__setStore({ token, session });
this.__debug('Got token');
return;
}
throw new Error('Cannot Sign In: Missing AthomCloudAPI');
});
this.__loginPromise
.then(() => {
this.__loginPromise = null;
})
.catch(err => {
this.__debug('Error Logging In:', err);
this.__loginPromise = null;
this.__token = null;
this.__session = null;
});
}
return this.__loginPromise;
}
async logout() {
this.__token = null;
this.__session = null;
await this.__setStore({
token: null,
session: null,
});
}
async refreshForToken(token, isRetryAfterRefresh = false) {
if (this.__token === token && this.__token !== null && this.__refreshMap[token] == null) {
this.__refreshMap[token] = Promise.resolve().then(async () => {
await this.__setStore({
token: null,
session: null,
});
if (!isRetryAfterRefresh) {
this.__debug('Refreshing token...');
// If the login fails, the tokens are cleared on the instance. We don't call logout and clear
// the token on the instance because that could cause a handshake client to fire with a null
// token. Handshake client also needs to attempt to refresh the token, and if the refresh was
// already started, it should reuse this promise. That also goes the other way around.
await this.login();
}
});
this.__refreshMap[token]
.then(() => {})
.catch(err => {
this.__debug('Error Refreshing Token:', err);
})
.finally(() => {
// Delete after 30 seconds some requests might still be pending an they should be able
// to receive a rejected promise for this token.
this.__refreshMap[token+'timeout'] = setTimeout(() => {
delete this.__refreshMap[token];
delete this.__refreshMap[token+'timeout'];
}, 30 * 1000);
});
}
await this.__refreshMap[token];
}
/**
* If Homey is connected to Socket.io.
* @returns {Boolean}
*/
isConnected() {
return this.__homeySocket && this.__homeySocket.connected;
}
async subscribe(
uri,
{
onConnect = () => {},
onReconnect = () => {},
onReconnectError = () => {},
onDisconnect = () => {},
onEvent = () => {},
}
) {
this.__debug('subscribe', uri);
await this.connect();
await Util.timeout(
new Promise((resolve, reject) => {
if (this.isConnected() !== true) {
reject(new Error('Not connected after connect.'));
return;
}
this.__homeySocket.once('disconnect', reason => {
reject(new Error(reason));
});
this.__debug('subscribing', uri);
this.__homeySocket.emit('subscribe', uri, err => {
if (err) {
this.__debug('Failed to subscribe', uri, err);
return reject(err);
}
this.__debug('subscribed', uri);
return resolve();
});
}),
10000,
`Failed to subscribe to ${uri} (Timeout after 10000ms).`
);
// On Connect
const __onEvent = (event, data) => {
onEvent(event, data);
};
this.__homeySocket.on(uri, __onEvent);
onConnect();
// On Disconnect
const __onDisconnect = reason => {
onDisconnect(reason);
};
this.__socket.on('disconnect', __onDisconnect);
// On Reconnect
const __onReconnect = () => {
Promise.resolve()
.then(async () => {
await this.connect();
await Util.timeout(
new Promise((resolve, reject) => {
if (this.isConnected() !== true) {
reject(new Error('Not connected after connect. (Reconnect)'));
return;
}
this.__homeySocket.once('disconnect', reason => {
reject(new Error(reason));
});
this.__debug('subscribing', uri);
this.__homeySocket.emit('subscribe', uri, err => {
if (err) {
this.__debug('Failed to subscribe', uri, err);
return reject(err);
}
this.__debug('subscribed', uri);
return resolve();
});
}),
10000,
`Failed to subscribe to ${uri} (Timeout after 10000ms).`
);
this.__homeySocket.on(uri, __onEvent);
onReconnect();
})
.catch(err => onReconnectError(err));
};
this.__socket.on('reconnect', __onReconnect);
return {
unsubscribe: () => {
if (this.__homeySocket) {
this.__homeySocket.emit('unsubscribe', uri);
this.__homeySocket.removeListener(uri, __onEvent);
}
if (this.__socket) {
this.__socket.removeListener('disconnect', __onDisconnect);
this.__socket.removeListener('reconnect', __onReconnect);
}
},
};
}
async connect() {
if (!this.__connectPromise) {
this.__connectPromise = Promise.resolve().then(async () => {
// Ensure Base URL
const baseUrl = await this.baseUrl;
// Ensure Token
if (!this.__token) await this.login();
return new Promise((resolve, reject) => {
this.__debug(`SocketIOClient ${baseUrl}`);
this.__socket = SocketIOClient(baseUrl, {
autoConnect: false,
transports: ['websocket'],
transportOptions: {
pingTimeout: 8000,
pingInterval: 5000,
},
reconnection: this.__reconnect,
});
this.__socket.on('disconnect', reason => {
this.__debug('SocketIOClient.onDisconnect', reason);
this.emit('disconnect', reason);
});
this.__socket.on('error', err => {
this.__debug('SocketIOClient.onError', err.message);
this.emit('error', err);
});
this.__socket.on('reconnect', () => {
this.__debug('SocketIOClient.onReconnect');
this.emit('reconnect');
});
this.__socket.on('reconnect_attempt', () => {
this.__debug(`SocketIOClient.onReconnectAttempt`);
this.emit('reconnect_attempt');
});
this.__socket.on('reconnecting', attempt => {
this.__debug(`SocketIOClient.onReconnecting (Attempt #${attempt})`);
this.emit('reconnecting');
});
this.__socket.on('reconnect_error', err => {
this.__debug('SocketIOClient.onReconnectError', err.message, err);
this.emit('reconnect_error');
});
this.__socket.on('connect_error', err => {
this.__debug('SocketIOClient.onConnectError', err.message);
this.emit('connect_error');
reject(err);
});
this.__socket.on('connect', () => {
this.__debug('SocketIOClient.onConnect');
this.emit('connect');
this.__handshakeClient()
.then(() => {
this.__debug('SocketIOClient.onConnect.onHandshakeClientSuccess');
resolve();
})
.catch(err => {
this.__debug('SocketIOClient.onConnect.onHandshakeClientError', err.message);
reject(err);
});
});
this.__socket.open();
});
});
this.__connectPromise.catch(err => {
this.__debug('SocketIOClient Error', err.message);
delete this.__connectPromise;
});
}
return this.__connectPromise;
}
async disconnect() {
// Should we wait for connect here?
// Also disconnect __homeySocket?
if (this.__socket) {
await new Promise(resolve => {
if (this.__socket.connected) {
this.__socket.once('disconnect', () => resolve());
this.__socket.disconnect();
} else {
resolve();
}
this.__socket.removeAllListeners();
this.__socket = null;
});
}
// TODO todo what?
}
destroy() {
this.__destroyed = true;
if (this.__homeySocket) {
this.__homeySocket.removeAllListeners();
this.__homeySocket.close();
this.__homeySocket = null;
}
if (this.__socket) {
this.__socket.removeAllListeners();
this.__socket.close();
this.__socket = null;
}
this.removeAllListeners();
}
isDestroyed() {
return this.__destroyed;
}
async __handshakeClient() {
this.__debug('__handshakeClient');
const onResult = ({ namespace }) => {
this.__debug('SocketIOClient.onHandshakeClientSuccess', `Namespace: ${namespace}`);
return new Promise((resolve, reject) => {
this.__homeySocket = this.__socket.io.socket(namespace);
this.__homeySocket.once('connect', () => {
this.__debug(`SocketIOClient.Namespace[${namespace}].onConnect`);
resolve();
});
this.__homeySocket.once('connect_error', err => {
this.__debug(`SocketIOClient.Namespace[${namespace}].onConnectError`, err.message);
if (err) {
if (err instanceof Error) {
return reject(err);
}
// .statusCode for homey-core .code for homey-client.
if (typeof err === 'object') {
return reject(new HomeyAPIError({ error_description: err.message }, err.statusCode || err.code));
}
return reject(new Error(String(err)));
}
reject(new Error(`Unknown error connecting to namespace ${namespace}.`));
});
this.__homeySocket.on('disconnect', reason => {
this.__debug(`SocketIOClient.Namespace[${namespace}].onDisconnect`, reason);
});
this.__homeySocket.on('reconnecting', attempt => {
this.__debug(`SocketIOClient.Namespace[${namespace}].onReconnecting (Attempt #${attempt})`);
});
this.__homeySocket.on('reconnect', () => {
this.__debug(`SocketIOClient.Namespace[${namespace}].onReconnect`);
});
this.__homeySocket.on('reconnect_error', err => {
this.__debug(`SocketIOClient.Namespace[${namespace}].onReconnectError`, err.message);
});
this.__homeySocket.open();
});
};
const handshakeClient = async token => {
return new Promise((resolve, reject) => {
this.__socket.emit(
'handshakeClient',
{
token,
homeyId: this.id,
},
(err, result) => {
if (err != null) {
if (typeof err === 'object') {
err = new HomeyAPIError(
{
stack: err.stack,
error: err.error,
error_description: err.error_description,
},
err.statusCode || err.code || 500
);
} else if (typeof err === 'string') {
err = new HomeyAPIError(
{
error: err,
},
500
);
}
return reject(err);
}
return resolve(result);
}
);
});
};
const token = this.__token;
try {
const result = await Util.timeout(
handshakeClient(token),
5000,
`Failed to handshake client (Timeout after 5000ms).`
);
return onResult(result);
} catch (err) {
if (err.statusCode === 401 || err.code === 401) {
this.__debug('Token expired, refreshing...');
await this.refreshForToken(token, false);
const result = await Util.timeout(
handshakeClient(this.__token),
5000,
`Failed to handshake client (Timeout after 5000ms).`
);
return onResult(result);
}
throw err;
}
}
hasScope(scope) {
if (this.__session == null) {
this.__debug('Tried to call hasScope without a session present.');
return true;
}
return this.__session.intersectedScopes.some(availableScope => {
return this.constructor.instanceOfScope(scope, availableScope);
});
}
static instanceOfScope(scopeOne, scopeTwo) {
const partsOne = scopeOne.split(':');
const partsTwo = scopeTwo.split(':');
let suffixIsEqual = true;
if (partsOne[1]) {
suffixIsEqual = partsOne[1] === partsTwo[1];
}
return scopeOne.indexOf(partsTwo[0]) === 0 && suffixIsEqual;
}
}
module.exports = HomeyAPIV3;