From 76f174e4fbb38301ec88cc0fc6b5beff45ae064b Mon Sep 17 00:00:00 2001 From: David Karlsson <2795016+devdavidkarlsson@users.noreply.github.com> Date: Mon, 27 Apr 2026 13:41:02 +0200 Subject: [PATCH] Add remote control support for Airthings Renew (AP_1) air purifiers Adds getRemoteControl() and setRemoteControl() methods to AirthingsClient for the new /v1/accounts/{accountId}/devices/{sn}/remote-control endpoints. Supports OFF, AUTO, SLEEP, BOOST, and MANUAL modes with configurable fan speed. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/client.ts | 69 +++++++++- src/example.ts | 12 +- src/schemas.ts | 24 ++++ vendor-docs/openapi.yaml | 271 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 368 insertions(+), 8 deletions(-) diff --git a/src/client.ts b/src/client.ts index acd2dc2..a3be897 100644 --- a/src/client.ts +++ b/src/client.ts @@ -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 @@ -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 { + 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 { + 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 @@ -131,18 +187,17 @@ export class AirthingsClient { } } - async #handleFetch(url: string): Promise { + async #handleFetch(url: string, init?: RequestInit): Promise { 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); diff --git a/src/example.ts b/src/example.ts index 70fe5f7..7691740 100644 --- a/src/example.ts +++ b/src/example.ts @@ -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 @@ -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 + }); + } } main().catch(err => console.error(err)); diff --git a/src/schemas.ts b/src/schemas.ts index f023f74..4c6d0cc 100644 --- a/src/schemas.ts +++ b/src/schemas.ts @@ -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 } + * ``` + */ +export interface RemoteControlState { + mode: RemoteControlMode; + /** Fan speed level (1-5). Required when mode is {@link RemoteControlMode.Manual}. */ + fanSpeed?: number; +} diff --git a/vendor-docs/openapi.yaml b/vendor-docs/openapi.yaml index 7d937d4..d59a212 100644 --- a/vendor-docs/openapi.yaml +++ b/vendor-docs/openapi.yaml @@ -124,6 +124,155 @@ paths: application/json: schema: $ref: '#/components/schemas/Error' + /v1/accounts/{accountId}/devices/{sn}/remote-control: + put: + summary: Set remote control mode for a Renew device + description: | + Set the operational mode of a Renew (AP_1) air purifier. + + **Available modes:** + | Mode | Description | + |------|-------------| + | `OFF` | Turn off the air purifier | + | `AUTO` | Automatically adjusts fan speed based on air quality measured by the built-in laser PM sensor | + | `SLEEP` | Runs at low speed for 8 hours at a whisper-quiet 23 dB; dims the control panel | + | `BOOST` | Runs at maximum fan speed for 60 minutes for rapid air purification | + | `MANUAL` | Fixed fan speed set by `fanSpeed` (1 = lowest / quietest, 5 = highest) | + + The command is forwarded to the device asynchronously. Use the GET endpoint to confirm the device has applied the new mode. + operationId: setRemoteControl + parameters: + - $ref: '#/components/parameters/accountId' + - $ref: '#/components/parameters/deviceSerialNumber' + tags: + - Remote Control + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RemoteControlRequest' + examples: + off: + summary: Turn off + value: + mode: OFF + auto: + summary: Automatic mode + value: + mode: AUTO + sleep: + summary: Sleep mode + value: + mode: SLEEP + boost: + summary: Boost mode + value: + mode: BOOST + manual: + summary: Manual fan speed (level 3 of 5) + value: + mode: MANUAL + fanSpeed: 3 + responses: + 200: + description: Mode successfully queued for the device + 400: + description: Bad request — unsupported device type, unknown mode, or missing `fanSpeed` for MANUAL mode + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + message: "fanSpeed is required for MANUAL mode" + 403: + description: Forbidden (user does not own the device) + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + 429: + description: Rate limit exceeded + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + default: + description: Error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + get: + summary: Get remote control state of a Renew device + description: | + Get 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` is returned. + operationId: getRemoteControl + parameters: + - $ref: '#/components/parameters/accountId' + - $ref: '#/components/parameters/deviceSerialNumber' + tags: + - Remote Control + security: + - bearerAuth: [] + responses: + 200: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/RemoteControlResponse' + examples: + auto: + summary: Device is in AUTO mode + value: + mode: AUTO + manual: + summary: Device is in MANUAL mode at fan speed 3 + value: + mode: MANUAL + fanSpeed: 3 + off: + summary: Device is off + value: + mode: OFF + 400: + description: Bad request — unsupported device type + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + 403: + description: Forbidden (user does not own the device) + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + 404: + description: The device has not yet reported a remote control state + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + message: "Device 4100007329 has no remote control state. The device may not have synced yet." + 429: + description: Rate limit exceeded + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + default: + description: Error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' /v1/health: get: summary: Get the health of the API @@ -213,6 +362,121 @@ components: type: array items: type: string + RemoteControlRequest: + oneOf: + - $ref: '#/components/schemas/RemoteControlOffRequest' + - $ref: '#/components/schemas/RemoteControlAutoRequest' + - $ref: '#/components/schemas/RemoteControlSleepRequest' + - $ref: '#/components/schemas/RemoteControlBoostRequest' + - $ref: '#/components/schemas/RemoteControlManualRequest' + discriminator: + propertyName: mode + mapping: + OFF: '#/components/schemas/RemoteControlOffRequest' + AUTO: '#/components/schemas/RemoteControlAutoRequest' + SLEEP: '#/components/schemas/RemoteControlSleepRequest' + BOOST: '#/components/schemas/RemoteControlBoostRequest' + MANUAL: '#/components/schemas/RemoteControlManualRequest' + RemoteControlResponse: + oneOf: + - $ref: '#/components/schemas/RemoteControlOffResponse' + - $ref: '#/components/schemas/RemoteControlAutoResponse' + - $ref: '#/components/schemas/RemoteControlSleepResponse' + - $ref: '#/components/schemas/RemoteControlBoostResponse' + - $ref: '#/components/schemas/RemoteControlManualResponse' + discriminator: + propertyName: mode + mapping: + OFF: '#/components/schemas/RemoteControlOffResponse' + AUTO: '#/components/schemas/RemoteControlAutoResponse' + SLEEP: '#/components/schemas/RemoteControlSleepResponse' + BOOST: '#/components/schemas/RemoteControlBoostResponse' + MANUAL: '#/components/schemas/RemoteControlManualResponse' + RemoteControlOffRequest: + type: object + description: Turn off the air purifier + required: [mode] + properties: + mode: + type: string + enum: [OFF] + RemoteControlAutoRequest: + type: object + description: Automatically adjusts fan speed based on air quality measured by the built-in laser PM sensor + required: [mode] + properties: + mode: + type: string + enum: [AUTO] + RemoteControlSleepRequest: + type: object + description: Runs at low speed for 8 hours at a whisper-quiet 23 dB; dims the control panel for nighttime use + required: [mode] + properties: + mode: + type: string + enum: [SLEEP] + RemoteControlBoostRequest: + type: object + description: Runs at maximum fan speed for 60 minutes for rapid air purification + required: [mode] + properties: + mode: + type: string + enum: [BOOST] + RemoteControlManualRequest: + type: object + description: Fixed fan speed controlled manually + required: [mode, fanSpeed] + properties: + mode: + type: string + enum: [MANUAL] + fanSpeed: + type: integer + minimum: 1 + maximum: 5 + description: Fan speed level — 1 (lowest / quietest) to 5 (highest) + RemoteControlOffResponse: + type: object + required: [mode] + properties: + mode: + type: string + enum: [OFF] + RemoteControlAutoResponse: + type: object + required: [mode] + properties: + mode: + type: string + enum: [AUTO] + RemoteControlSleepResponse: + type: object + required: [mode] + properties: + mode: + type: string + enum: [SLEEP] + RemoteControlBoostResponse: + type: object + required: [mode] + properties: + mode: + type: string + enum: [BOOST] + RemoteControlManualResponse: + type: object + required: [mode, fanSpeed] + properties: + mode: + type: string + enum: [MANUAL] + fanSpeed: + type: integer + minimum: 1 + maximum: 5 + description: Fan speed level — 1 (lowest / quietest) to 5 (highest) parameters: accountId: name: accountId @@ -221,6 +485,13 @@ components: description: The account ID associated with the user schema: type: string + deviceSerialNumber: + name: sn + in: path + required: true + description: The serial number of the device + schema: + type: string deviceSerialNumbers: name: sn in: query