homebridge-hatch-baby-rest
Version:
Homebridge plugin for Hatch Rest/Restore WiFi sound machines
106 lines (105 loc) • 3.79 kB
JavaScript
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);
}
}