UNPKG

homey-api

Version:
1,022 lines (862 loc) 29.8 kB
'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;