UNPKG

homebridge-eightsleepthermostat

Version:

Homebridge thermostat accessory for the Eight Sleep Pod smart bed.

242 lines 9.4 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.EightSleepConnection = void 0; const promises_1 = require("fs/promises"); const path_1 = __importDefault(require("path")); const clientRequest_1 = require("./clientRequest"); // ** Uncomment next line to import/enable mocking data // import * as AxiosMock from './axiosMock'; const EIGHT_SLEEP_DIR = '8slp'; const SESSION_CACHE_FILE = '_login.txt'; const PRIMARY_USER_CACHE_FILE = '_users_me.txt'; class EightSleepConnection { constructor(platform, email, password) { this.platform = platform; this.cacheDir = path_1.default.resolve(this.platform.api.user.storagePath(), EIGHT_SLEEP_DIR); this.sessionCachePath = path_1.default.resolve(this.cacheDir, SESSION_CACHE_FILE); this.primaryUserCachePath = path_1.default.resolve(this.cacheDir, PRIMARY_USER_CACHE_FILE); this.log = this.platform.log; this.session = this.prepareSession(); this.primaryUserDevice = this.preparePrimaryUser(); /** * Session validation & reauthentication methods. Can be initiated externally * at any time to ensure session information (i.e. token) is up to date. */ this.validateActiveSession = async () => { this.log.debug('Validating session'); const activeSession = await this.session; if (!activeSession || !this.isValid(activeSession)) { this.log.debug('Reauthenticating expired session'); await this.reauthenticate(); } this.log.debug('Session validated'); }; // User credentials read from `config.json` on homebridge startup this.userCreds = { email: email, password: password, }; // ** Uncomment import statement/next line to enable mocking for debugging // AxiosMock.startIntercepting(clientAPI, this.log); this.preserveConnection(); } /** * This method will initiate a chain of events to either load the session * containing a userId & token info from cache, or send a new login request * to the 8slp API to fetch this information. * * The result of this method is stored by {@linkcode session} in the * form of `Promise<Session | null>` * * Associated methods: * {@linkcode loadCachedSession()} // loads cache & verifies token * {@linkcode login()} // if no cached session, will re-login * * @returns a Promise containing the loaded/fetched session * * @category Session */ async prepareSession() { try { let session = await this.loadCachedSession(); if (!session || !this.isValid(session)) { this.eraseCache(this.sessionCachePath); session = await this.login(); } this.updateClientSessionHeaders(session); return session; } catch (error) { this.log.error('Failed to prepare connection to Eight Sleep:', error); return null; } } // Catch error here so that a failure doesn't stop login execution from // proceeding when this method returns to `prepareSession()` async loadCachedSession() { try { const sessionData = await this.readCache(this.sessionCachePath); return sessionData; } catch (error) { this.log.debug('Error loading session from cache', error); return null; } } // Forward error up the chain as a failure here means we have already // exhausted any chance at a successful session load async login() { const response = await clientRequest_1.clientAPI.post('/login', this.userCreds); const session = response.data['session']; if (!this.isValid(session)) { throw new Error(`Unexpected issue with Eight Sleep API session - ${JSON.stringify(session)}`); } this.writeToCache(this.sessionCachePath, session); return session; } isValid(session) { const tokenExpDate = new Date(session.expirationDate).valueOf(); return this.verifyFields(session) && tokenExpDate > Date.now() + (1000 * 60 * 20); } verifyFields(session) { return (session.token && session.expirationDate && session.userId) ? true : false; } updateClientSessionHeaders(session) { clientRequest_1.clientAPI.defaults.headers.common['user-id'] = session.userId; clientRequest_1.clientAPI.defaults.headers.common['session-token'] = session.token; } async reauthenticate() { await this.eraseCache(this.sessionCachePath); this.session = this.prepareSession(); } // Check if reauth is needed every 10 minutes. Currently // being used only for debugging purposes... preserveConnection() { setInterval(this.validateActiveSession, 1000 * 60 * 10); } /** * This method will initiate a chain of events to either load the primary user data * containing the device `id` & `side` properties from cache, or will send a `GET` * request to `/users/me/` of 8slp API to fetch the user object * * The result of this method is stored by {@linkcode primaryUserDevice} in the * form of `Promise<PrimaryUser | null>` * * Associated methods: * {@linkcode loadCachedUser()} * {@linkcode fetchPrimaryUser()} * * @returns a Promise containing the loaded/fetched device * * @category CurrentDeviceType */ async preparePrimaryUser() { try { const cachedUser = await this.loadCachedUser(); let device = this.verifyDeviceFor(cachedUser); if (!device) { this.eraseCache(this.primaryUserCachePath); device = await this.fetchPrimaryUser(); } return device; } catch (error) { this.log.debug('Failed to get Eight Sleep device info:', error); return null; } } async loadCachedUser() { try { const cachedUser = await this.readCache(this.primaryUserCachePath); return cachedUser; } catch (error) { this.log.debug('Error loading user from cache:', error); return null; } } // GET: Full user profile data from client // - use on first time load when no user (i.e. device) previously cached async fetchPrimaryUser() { // Must wait for session -> if null, we're unable to fetch user profile // as we need the userId & token headers included in the request const session = await this.session; if (!session) { throw new Error('No session'); } const response = await clientRequest_1.clientAPI.get('/users/me'); const user = response.data['user']; const device = this.verifyDeviceFor(user); if (!device) { throw new Error('Unable to fetch user profile & current device from client'); } this.writeToCache(this.primaryUserCachePath, user); return device; } verifyDeviceFor(user) { if (!user || !user.currentDevice.id || !user.currentDevice.side) { return null; } return user.currentDevice; } /** * Caching functionality for both session data (token, userId, tokenExp) and the * primary user's full profile (we only care about 'currentDevice') * * Associated methods: * {@linkcode readCache()} * {@linkcode writeToCache()} * {@linkcode eraseCache()} * {@linkcode makeCacheDirectory()} * * @category Caching {@linkcode Session} & {@linkcode PrimaryUser} */ async readCache(filepath) { try { const cache = await (0, promises_1.readFile)(filepath, 'utf-8'); const parsedData = JSON.parse(cache); return parsedData; } catch (_a) { return Promise.reject('Cache is empty or data in cache could not be parsed.'); } } async writeToCache(filepath, data) { try { await this.makeCacheDirectory(); const jsonData = JSON.stringify(data); await (0, promises_1.writeFile)(filepath, jsonData); } catch (_a) { this.log.debug('Unable to write to cache'); } } async eraseCache(filepath) { try { await this.writeToCache(filepath, ''); } catch (_a) { this.log.debug('Unable to erase cache at:', filepath); } } async makeCacheDirectory() { try { await (0, promises_1.mkdir)(this.cacheDir); } catch (error) { /* - If dir already exists, caught error code will be 'EEXIST' * Only throw if it's NOT an 'EEXIST' error, since we can still proceed to write to cache directory if it is */ if (error.code !== 'EEXIST') { throw error; } } } } exports.EightSleepConnection = EightSleepConnection; //# sourceMappingURL=eightSleepConnection.js.map