Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 62 additions & 7 deletions src/client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { SensorUnits } from './schemas.js';
import type { Accounts, Devices, SensorResults, SensorResultsRateLimitMetrics } from './schemas.js';
import type { Accounts, Devices, RemoteControlState, SensorResults, SensorResultsRateLimitMetrics } from './schemas.js';

/**
* The Airthings for Consumer API provides secure and authorized access for Airthings
Expand Down Expand Up @@ -106,6 +106,62 @@ export class AirthingsClient {
return await response.json() as SensorResults;
}

/**
* 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);
* ```
*/
public async getRemoteControl(sn: string): Promise<RemoteControlState> {
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() as RemoteControlState;
}

/**
* 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
* });
* ```
*/
public async setRemoteControl(sn: string, state: RemoteControlState): Promise<void> {
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)
});
Comment thread
michaelahern marked this conversation as resolved.
}

/**
* Get rate limit metrics from the last getSensors request
* @returns
Expand All @@ -131,18 +187,17 @@ export class AirthingsClient {
}
}

async #handleFetch(url: string): Promise<Response> {
async #handleFetch(url: string, init?: RequestInit): Promise<Response> {
await this.#refreshAccessToken();

if (!this.#accessToken) {
throw new AirthingsError('No Access Token');
}

const response = await fetch(url, {
headers: {
Authorization: `${this.#accessToken.type} ${this.#accessToken.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);

Expand Down
12 changes: 11 additions & 1 deletion src/example.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AirthingsClient, SensorUnits } from './module.js';
import { AirthingsClient, RemoteControlMode, SensorUnits } from './module.js';

/**
* Entry point that initializes an Airthings client, validates required
Expand Down Expand Up @@ -32,6 +32,16 @@ async function main() {
});

console.log(client.getSensorsRateLimitMetrics());

const renewDevice = devicesResponse.devices.find(d => d.type === 'AP_1');
if (renewDevice) {
const state = await client.getRemoteControl(renewDevice.serialNumber);
console.log(state);

await client.setRemoteControl(renewDevice.serialNumber, {
mode: RemoteControlMode.Auto
});
}
Comment thread
michaelahern marked this conversation as resolved.
}

main().catch(err => console.error(err));
24 changes: 24 additions & 0 deletions src/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,27 @@ export enum SensorUnits {
Metric,
Imperial
}

export enum RemoteControlMode {
Off = 'OFF',
Auto = 'AUTO',
Sleep = 'SLEEP',
Boost = 'BOOST',
Manual = 'MANUAL'
}

/**
* @example
* ```json
* { mode: 'AUTO' }
* ```
* @example
* ```json
* { mode: 'MANUAL', fanSpeed: 3 }
* ```
Comment thread
michaelahern marked this conversation as resolved.
*/
export interface RemoteControlState {
mode: RemoteControlMode;
/** Fan speed level (1-5). Required when mode is {@link RemoteControlMode.Manual}. */
fanSpeed?: number;
}
Comment thread
michaelahern marked this conversation as resolved.
Loading