airthings-consumer-api
Version:
An unofficial Node.js client library for Airthings Air Quality Monitors via the Airthings Consumer API.
213 lines • 9.49 kB
JavaScript
import { SensorUnits } from './schemas.js';
/**
* The Airthings for Consumer API provides secure and authorized access for Airthings
* consumers to retrieve the latest data from their Airthings air quality monitors. Leveraging
* HTTPS and OAuth for enhanced security, this API empowers users to seamlessly access
* real-time information from their Airthings devices, gaining valuable insights into the air
* quality within their environments.
*/
export class AirthingsClient {
#accessToken;
#opts;
#rateLimitMetrics;
/**
* @param opts - The options for the Airthings client, primarily client credentials
* @remarks
* Create an Airthings Client ID & Secret at https://consumer-api-doc.airthings.com/dashboard
* @example
* ```javascript
* const client = new AirthingsClient({
* clientId: 'clientId',
* clientSecret: 'clientSecret'
* });
* ```
*/
constructor(opts) {
this.#accessToken = null;
this.#opts = opts;
this.#rateLimitMetrics = { limit: -1, remaining: -1, reset: -1 };
}
/**
* List all accounts the current user is member of
* @returns
* Lists all accounts the current user is member of. The data returned by this endpoint
* changes when a user is added or removed from business accounts. It is safe to assume
* that the accountId remains constant for Consumer users. The accountId returned by this
* endpoint is used to fetch the devices and sensors from the other endpoints.
* @see [Airthings Consumer API: Accounts](https://consumer-api-doc.airthings.com/api-docs#tag/Accounts)
* @throws {@link AirthingsError} If the request fails
*/
async getAccounts() {
const url = 'https://consumer-api.airthings.com/v1/accounts';
const response = await this.#handleFetch(url);
return await response.json();
}
/**
* Get all devices connected to a user
* @returns
* List all devices (and their sensor abilities) connected to a user’s account. The data
* returned by this endpoint changes when a device is registered, unregistered or renamed.
* @see [Airthings Consumer API: Devices](https://consumer-api-doc.airthings.com/api-docs#tag/Device)
* @throws {@link AirthingsError} If the request fails
* @example
* ```javascript
* const devicesResponse = await client.getDevices();
* devicesResponse.devices.forEach((device) => {
* console.log(device);
* });
* ```
*/
async getDevices() {
await this.#ensureAccountIdConfig();
const url = `https://consumer-api.airthings.com/v1/accounts/${this.#opts.accountId}/devices`;
const response = await this.#handleFetch(url);
return await response.json();
}
/**
* Get sensors for a set of devices
* @param unit - The units type sensor values will be returned in, metric or imperial
* @param sn - An optional list of serial numbers to filter the results
* @returns
* Get sensors for a set of devices. The response will contain the latest sensor values for
* the devices. The sensor values are updated depending on the device types sampling
* rate. It is recommended to poll the API at a regular interval to get the latest
* sensor values. The response will be paginated with a maximum of 50 records per page.
* @see [Airthings Consumer API: Sensors](https://consumer-api-doc.airthings.com/api-docs#tag/Sensor)
* @throws {@link AirthingsError} If the request fails
* @example
* ```javascript
* const sensorsResponse = await client.getSensors(SensorUnits.Imperial);
* sensorsResponse.results.forEach((sensor) => {
* console.log(sensor);
* });
* ```
*/
async getSensors(unit, sn) {
await this.#ensureAccountIdConfig();
let url = `https://consumer-api.airthings.com/v1/accounts/${this.#opts.accountId}/sensors?unit=${SensorUnits[unit].toLowerCase()}`;
if (sn && sn.length > 0) {
url += `&sn=${sn.join(',')}`;
}
const response = await this.#handleFetch(url);
this.#rateLimitMetrics.limit = parseInt(response.headers.get('X-RateLimit-Limit') || '-1');
this.#rateLimitMetrics.remaining = parseInt(response.headers.get('X-RateLimit-Remaining') || '-1');
this.#rateLimitMetrics.reset = parseInt(response.headers.get('X-RateLimit-Reset') || '-1');
return await response.json();
}
/**
* Get the remote control state of a Renew device
* @param sn - The serial number of the device
* @returns
* The last reported operational mode of a Renew (AP\_1) air purifier.
* The state reflects what the device last reported, not necessarily the
* command last sent. If the device has never synced a mode, a 404 error
* is returned.
* @see [Airthings Consumer API: Remote Control](https://consumer-api-doc.airthings.com/api-docs#tag/Remote-Control)
* @throws {@link AirthingsError} If the request fails
* @example
* ```javascript
* const state = await client.getRemoteControl('4100007329');
* console.log(state.mode);
* ```
*/
async getRemoteControl(sn) {
await this.#ensureAccountIdConfig();
const url = `https://consumer-api.airthings.com/v1/accounts/${this.#opts.accountId}/devices/${sn}/remote-control`;
const response = await this.#handleFetch(url);
return await response.json();
}
/**
* Set the remote control mode of a Renew device
* @param sn - The serial number of the device
* @param state - The desired operational mode and optional fan speed
* @returns
* Set the operational mode of a Renew (AP\_1) air purifier. Available modes
* are OFF, AUTO, SLEEP, BOOST, and MANUAL. Fan speed (1-5) is required for
* MANUAL mode. The command is forwarded to the device asynchronously. Use
* {@link getRemoteControl} to confirm the device has applied the new mode.
* @see [Airthings Consumer API: Remote Control](https://consumer-api-doc.airthings.com/api-docs#tag/Remote-Control)
* @throws {@link AirthingsError} If the request fails
* @example
* ```javascript
* import { RemoteControlMode } from 'airthings-consumer-api';
*
* await client.setRemoteControl('4100007329', {
* mode: RemoteControlMode.Manual,
* fanSpeed: 3
* });
* ```
*/
async setRemoteControl(sn, state) {
await this.#ensureAccountIdConfig();
const url = `https://consumer-api.airthings.com/v1/accounts/${this.#opts.accountId}/devices/${sn}/remote-control`;
await this.#handleFetch(url, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(state)
});
}
/**
* Get rate limit metrics from the last getSensors request
* @returns
* Current rate limit metrics reflecting the `X-RateLimit-Limit`,
* `X-RateLimit-Remaining`, and `X-RateLimit-Reset` response headers from
* the most recent {@link getSensors} call. Before any {@link getSensors}
* call has been made, all rate limit metrics are initialised to `-1`.
* @see [Airthings Consumer API: Rate Limits](https://consumer-api-doc.airthings.com/docs/api/rate-limit)
*/
getSensorsRateLimitMetrics() {
return this.#rateLimitMetrics;
}
async #ensureAccountIdConfig() {
if (!this.#opts.accountId) {
const accountsResponse = await this.getAccounts();
if (accountsResponse.accounts[0]) {
this.#opts.accountId = accountsResponse.accounts[0].id;
}
else {
throw new AirthingsError('No Account ID');
}
}
}
async #handleFetch(url, init) {
await this.#refreshAccessToken();
if (!this.#accessToken) {
throw new AirthingsError('No Access Token');
}
const headers = new Headers(init?.headers);
headers.set('Authorization', `${this.#accessToken.type} ${this.#accessToken.token}`);
const response = await fetch(url, { ...init, headers });
await this.#handleFetchResponseError(response);
return response;
}
async #handleFetchResponseError(response) {
if (!response.ok) {
throw new AirthingsError(`Request Error [${response.status}: ${await response.text()}]`);
}
}
async #refreshAccessToken() {
if (this.#accessToken && this.#accessToken.expires - (5 * 60 * 1000) > Date.now()) {
return;
}
const response = await fetch(`https://accounts-api.airthings.com/v1/token`, {
method: 'POST',
headers: {
'Authorization': `Basic ${Buffer.from(`${this.#opts.clientId}:${this.#opts.clientSecret}`).toString('base64')}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
grant_type: 'client_credentials'
})
});
await this.#handleFetchResponseError(response);
const tokenData = await response.json();
this.#accessToken = {
token: tokenData.access_token,
type: tokenData.token_type,
expires: tokenData.expires_in * 1000 + Date.now()
};
}
}
export class AirthingsError extends Error {
}
//# sourceMappingURL=client.js.map