UNPKG

homebridge-hatch-baby-rest

Version:
106 lines (105 loc) 3.79 kB
import { delay, logError } from "../shared/util.js"; const apiBaseUrl = 'https://prod-sleep.hatchbaby.com/', defaultRequestOptions = { method: 'GET', }, defaultHeaders = { USER_AGENT: 'hatch_rest_api', 'content-type': 'application/json', }; export function apiPath(path) { return apiBaseUrl + path; } export async function requestWithRetry(options, retryCount = 1) { try { const optionsWithDefaults = { ...defaultRequestOptions, ...options, headers: { ...defaultHeaders, ...options.headers, }, }; if (options.json) { optionsWithDefaults.body = JSON.stringify(options.json); } const response = await fetch(new Request(options.url, optionsWithDefaults)); if (!response.ok) { const errorWithResponse = new Error(`Failed to fetch ${options.url}. Response: ${response.status} ${response.statusText}. ${await response.text()}`); errorWithResponse.response = response; throw errorWithResponse; } const responseJson = await response.json(); return responseJson; } catch (e) { if (!e.response) { // Exponential backoff doubled each retry // Cap at 60 seconds to avoid extremely long waits const backoffTime = Math.min(1000 * Math.pow(2, retryCount), 60000); logError(`Failed to reach Hatch Baby server at ${options.url}. ${e.message}. Trying again in ${backoffTime / 1000} seconds... (Attempt ${retryCount + 1})`); await delay(backoffTime); return requestWithRetry(options, retryCount + 1); } throw e; } } export class RestClient { authOptions; loginPromise; constructor(authOptions) { this.authOptions = authOptions; this.loginPromise = this.logIn(); } async logIn() { try { const resp = await requestWithRetry({ url: apiPath('public/v1/login'), json: { email: this.authOptions.email, password: this.authOptions.password, }, method: 'POST', }); if ('status' in resp && resp.status === 'failure') { throw new Error(resp.message); } return resp; } catch (requestError) { const errorMessage = 'Failed to fetch oauth token from Hatch Baby. Verify that your email and password are correct.'; logError(requestError.response || requestError); logError(errorMessage); throw new Error(errorMessage); } } refreshAuth() { this.loginPromise = this.logIn(); } async request(options) { try { const loginResponse = await this.loginPromise, headers = { ...options.headers, 'X-HatchBaby-Auth': loginResponse.token, }, response = await requestWithRetry({ ...options, headers, }); return response.payload; } catch (e) { const response = e.response || {}, { url } = options; if (response.status === 401) { this.refreshAuth(); return this.request(options); } if (response.status === 404 && url.startsWith(apiBaseUrl)) { logError('404 from endpoint ' + url); throw new Error('Not found with response: ' + JSON.stringify(response.data)); } logError(`Request to ${url} failed`); throw e; } } getAccount() { return this.loginPromise.then((l) => l.payload); } }