homebridge-pura
Version:
Homebridge plugin for Pura smart fragrance diffusers
200 lines • 6.8 kB
JavaScript
/**
* Pura API Client
* Based on pypura library: https://github.com/natekspencer/pypura
*/
import { CognitoUserPool, CognitoUser, AuthenticationDetails, } from 'amazon-cognito-identity-js';
import fetch from 'node-fetch';
// Constants from pypura
const USER_POOL_ID = 'us-east-1_LaB718hYv'; // Base64 decoded from pypura
const CLIENT_ID = '4ie4kbat0jb5iljfbaalsiqf9j'; // Base64 decoded from pypura
const BASE_URL = 'https://trypura.io/mobile/api/';
export class PuraApi {
userPool;
cognitoUser = null;
session = null;
log;
constructor(log) {
this.log = log;
this.userPool = new CognitoUserPool({
UserPoolId: USER_POOL_ID,
ClientId: CLIENT_ID,
});
}
/**
* Authenticate with Pura API
*/
async authenticate(username, password) {
return new Promise((resolve, reject) => {
const authenticationDetails = new AuthenticationDetails({
Username: username,
Password: password,
});
this.cognitoUser = new CognitoUser({
Username: username,
Pool: this.userPool,
});
this.cognitoUser.authenticateUser(authenticationDetails, {
onSuccess: (session) => {
this.session = session;
this.log.debug('Pura authentication successful');
resolve({
accessToken: session.getAccessToken().getJwtToken(),
idToken: session.getIdToken().getJwtToken(),
refreshToken: session.getRefreshToken().getToken(),
});
},
onFailure: (err) => {
this.log.error('Pura authentication failed:', err.message);
reject(new Error(`Pura authentication failed: ${err.message}`));
},
});
});
}
/**
* Refresh authentication tokens
*/
async refreshToken() {
if (!this.cognitoUser || !this.session) {
throw new Error('Not authenticated');
}
return new Promise((resolve, reject) => {
const refreshToken = this.session.getRefreshToken();
this.cognitoUser.refreshSession(refreshToken, (err, session) => {
if (err) {
this.log.error('Token refresh failed:', err.message);
reject(new Error(`Token refresh failed: ${err.message}`));
return;
}
this.session = session;
this.log.debug('Pura token refresh successful');
resolve({
accessToken: session.getAccessToken().getJwtToken(),
idToken: session.getIdToken().getJwtToken(),
refreshToken: session.getRefreshToken().getToken(),
});
});
});
}
/**
* Get authorization header for API requests
*/
getAuthHeader() {
if (!this.session) {
throw new Error('Not authenticated');
}
return `Bearer ${this.session.getIdToken().getJwtToken()}`;
}
/**
* Make authenticated API request
*/
async makeRequest(method, endpoint, data) {
const url = new URL(endpoint, BASE_URL).toString();
const options = {
method: method.toUpperCase(),
headers: {
'Content-Type': 'application/json',
Authorization: this.getAuthHeader(),
},
// timeout: 10000, // Not supported in fetch
};
if (data && method.toLowerCase() !== 'get') {
options.body = JSON.stringify(data);
}
this.log.debug(`Making ${method} request to ${url}`);
try {
const response = await fetch(url, options);
if (!response.ok) {
const errorText = await response.text();
this.log.error(`API request failed: ${response.status} - ${errorText}`);
throw new Error(`API request failed: ${response.status} - ${errorText}`);
}
const result = await response.json();
this.log.debug('API response:', result);
return result;
}
catch (error) {
this.log.error('API request error:', error);
throw error;
}
}
/**
* Get all devices
*/
async getDevices() {
try {
const response = await this.makeRequest('GET', 'v2/users/devices');
return response.devices || [];
}
catch (error) {
this.log.error('Failed to get devices:', error);
throw error;
}
}
/**
* Set device intensity
*/
async setIntensity(deviceId, bay, intensity) {
try {
const response = await this.makeRequest('POST', `devices/${deviceId}/intensity`, {
bay,
controller: 'mobile',
intensity,
});
return response.success === true;
}
catch (error) {
this.log.error(`Failed to set intensity for device ${deviceId}:`, error);
return false;
}
}
/**
* Set always on mode
*/
async setAlwaysOn(deviceId, bay) {
try {
const response = await this.makeRequest('POST', `devices/${deviceId}/always-on`, {
bay,
});
return response.success === true;
}
catch (error) {
this.log.error(`Failed to set always on for device ${deviceId}:`, error);
return false;
}
}
/**
* Stop all diffusion
*/
async stopAll(deviceId) {
try {
const response = await this.makeRequest('POST', `devices/${deviceId}/stop-all`);
return response.success === true;
}
catch (error) {
this.log.error(`Failed to stop all for device ${deviceId}:`, error);
return false;
}
}
/**
* Set timer
*/
async setTimer(deviceId, bay, intensity, durationMinutes) {
try {
const start = Math.floor(Date.now() / 1000);
const end = start + (durationMinutes * 60);
const response = await this.makeRequest('POST', `devices/${deviceId}/timer`, {
bay,
intensity,
start,
end,
validateOverride: true,
});
return response.success === true;
}
catch (error) {
this.log.error(`Failed to set timer for device ${deviceId}:`, error);
return false;
}
}
}
//# sourceMappingURL=puraApi.js.map