Skip to content

Commit

Permalink
Configurable Air Quality Sensor
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelahern committed Mar 10, 2024
1 parent a1150a9 commit 4114c92
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 63 deletions.
39 changes: 24 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
[![verified-by-homebridge](https://badgen.net/badge/homebridge/verified/purple)](https://github.com/homebridge/homebridge/wiki/Verified-Plugins)
[![npm](https://badgen.net/npm/v/homebridge-airthings)](https://www.npmjs.com/package/homebridge-airthings)
[![npm](https://badgen.net/npm/dt/homebridge-airthings)](https://www.npmjs.com/package/homebridge-airthings)
[![License](https://badgen.net/github/license/michaelahern/homebridge-airthings)](LICENSE)
[![Build](https://github.com/michaelahern/homebridge-airthings/actions/workflows/build.yml/badge.svg)](https://github.com/michaelahern/homebridge-airthings/actions/workflows/build.yml)
[![Donate](https://badgen.net/badge/Donate/PayPal/green)](https://paypal.me/michaeljahern)

Expand Down Expand Up @@ -42,6 +41,11 @@ Example accessory config in the Homebridge config.json:
"clientId": "00000000-0000-0000-0000-000000000000",
"clientSecret": "11111111-1111-1111-1111-111111111111",
"serialNumber": "2960123456",
"co2AirQualityDisabled": false,
"humidityAirQualityDisabled": false,
"pm25AirQualityDisabled": false,
"radonAirQualityDisabled": false,
"vocAirQualityDisabled": false,
"co2DetectedThreshold": 1000,
"radonLeakThreshold": 100,
"debug": false,
Expand All @@ -53,18 +57,23 @@ Example accessory config in the Homebridge config.json:

### Configuration Details

Field | Description
-------------------------|------------
**accessory** | (required) Must be "Airthings"
**name** | (required) Name for the device in HomeKit
**clientId** | (required) API Client ID generated in the [Airthings Dashboard](https://dashboard.airthings.com)
**clientSecret** | (required) API Client Secret generated in the [Airthings Dashboard](https://dashboard.airthings.com)
**serialNumber** | (required) Serial number of the device
**co2DetectedThreshold** | (optional) Configure a custom CO₂ detected threshold, default is 1000 ppm
**radonLeakThreshold** | (optional) Enable a Radon Leak Sensor with a threshold in Bq/m³, see additional notes below, disabled by default
**debug** | (optional) Enable debug logging, disabled by default
**refreshInterval** | (optional) Interval in seconds for refreshing sensor data, default is 150s<br/>_Note: The Airthings Consumer API has a [rate limit of 120 requests per hour](https://developer.airthings.com/docs/api-rate-limit#airthings-consumer)_
**tokenScope** | (optional, *experimental*) Configure a custom [Airthings API Token Scope](https://developer.airthings.com/api-docs#section/Authentication), default is read:device:current_values
Field | Description
-------------------------------|------------
**accessory** | (required) Must be "Airthings"
**name** | (required) Name for the device in HomeKit
**clientId** | (required) API Client ID generated in the [Airthings Dashboard](https://dashboard.airthings.com)
**clientSecret** | (required) API Client Secret generated in the [Airthings Dashboard](https://dashboard.airthings.com)
**serialNumber** | (required) Serial number of the device
**co2AirQualityDisabled** | (optional) Disable Carbon Dioxide (CO₂) in Air Quality sensor calculation, default is false
**humidityAirQualityDisabled** | (optional) Disable Humidity in Air Quality sensor calculation. default is false
**pm25AirQualityDisabled** | (optional) Disable Particulate Matter (PM2.5) in Air Quality sensor calculation, default is false
**radonAirQualityDisabled** | (optional) Disable Radon in Air Quality sensor calculation, default is false
**vocAirQualityDisabled** | (optional) Disable VOC in Air Quality sensor calculation, default is false
**co2DetectedThreshold** | (optional) Configure a custom Carbon Dioxide (CO₂) detected threshold, default is 1000 ppm
**radonLeakThreshold** | (optional) Enable a Radon Leak Sensor with a threshold in Bq/m³, see additional notes below, disabled by default
**debug** | (optional) Enable debug logging, disabled by default
**refreshInterval** | (optional) Interval in seconds for refreshing sensor data, default is 150s<br/>_Note: The Airthings Consumer API has a [rate limit of 120 requests per hour](https://developer.airthings.com/docs/api-rate-limit#airthings-consumer)_
**tokenScope** | (optional, *experimental*) Configure a custom [Airthings API Token Scope](https://developer.airthings.com/api-docs#section/Authentication), default is read:device:current_values

### How to request an Airthings API Client ID & Secret

Expand All @@ -81,9 +90,9 @@ Field | Description

### Air Quality

Air Quality Sensors are supported and implemented using standard Apple-defined services. Air Quality in this plugin is a composite of Radon, Particulate Matter (PM2.5), Volatile Organic Compound (VOC), Carbon Dioxide (CO₂), and Humidity sensors, depending on the sensors supported by your device. Air Quality values (Excellent, Fair, Poor) are based on [Airthings-defined thresholds](https://help.airthings.com/en/articles/5367327-view-understanding-the-sensor-thresholds) for each sensor.
Air Quality Sensors are supported and implemented using standard Apple-defined services. Air Quality in this plugin is a composite of Radon, Particulate Matter (PM2.5), Volatile Organic Compound (VOC), Carbon Dioxide (CO₂), and Humidity sensors, depending on the sensors supported by your device and your plugin configuration. Air Quality values (Good, Fair, Poor) are based on [Airthings-defined thresholds](https://help.airthings.com/en/articles/5367327-view-understanding-the-sensor-thresholds) for each sensor.

Sensor | 🟢 Excellent | 🟠 Fair | 🔴 Poor |
Sensor | 🟢 Good | 🟠 Fair | 🔴 Poor |
----------------------------------|---------------|------------------------------------|--------------------|
Radon | <100 Bq/m³ | ≥100 and <150 Bq/m³ | ≥150 Bq/m³ |
Particulate Matter (PM2.5) | <10 μg/m³ | ≥10 and <25 μg/m³ | ≥25 μg/m³ |
Expand Down
30 changes: 30 additions & 0 deletions config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,31 @@
"type": "string",
"required": true
},
"co2AirQualityDisabled": {
"title": "CO₂ Disabled in Air Quality Sensor",
"type": "boolean",
"required": false
},
"humidityAirQualityDisabled": {
"title": "Humidity Disabled in Air Quality Sensor",
"type": "boolean",
"required": false
},
"pm25AirQualityDisabled": {
"title": "PM2.5 Disabled in Air Quality Sensor",
"type": "boolean",
"required": false
},
"radonAirQualityDisabled": {
"title": "Radon Disabled in Air Quality Sensor",
"type": "boolean",
"required": false
},
"vocAirQualityDisabled": {
"title": "VOC Disabled in Air Quality Sensor",
"type": "boolean",
"required": false
},
"co2DetectedThreshold": {
"title": "CO₂ Detected Threshold (ppm)",
"type": "number",
Expand Down Expand Up @@ -81,6 +106,11 @@
"items": [
{
"items": [
"co2AirQualityDisabled",
"humidityAirQualityDisabled",
"pm25AirQualityDisabled",
"radonAirQualityDisabled",
"vocAirQualityDisabled",
"co2DetectedThreshold",
"radonLeakThreshold"
]
Expand Down
95 changes: 47 additions & 48 deletions src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,40 +108,43 @@ export class AirthingsPlugin implements AccessoryPlugin {
// HomeKit Air Quality Service
this.airQualityService = new api.hap.Service.AirQualitySensor("Air Quality");

if (this.airthingsDevice.sensors.mold) {
this.airQualityService.addCharacteristic(new api.hap.Characteristic("Mold", "68F9B9E6-88C7-4FB3-B8CE-60205F9F280E", {
format: Formats.UINT16,
perms: [Perms.NOTIFY, Perms.PAIRED_READ],
unit: "Risk",
minValue: 0,
maxValue: 10,
minStep: 1
}));
if (this.airthingsDevice.sensors.co2 && !this.airthingsConfig.co2AirQualityDisabled) {
this.airQualityService.getCharacteristic(api.hap.Characteristic.CarbonDioxideLevel).setProps({});
}

if (this.airthingsDevice.sensors.humidity && !this.airthingsConfig.humidityAirQualityDisabled) {
this.airQualityService.getCharacteristic(api.hap.Characteristic.CurrentRelativeHumidity).setProps({});
}

if (this.airthingsDevice.sensors.radonShortTermAvg) {
if (this.airthingsDevice.sensors.pm25 && !this.airthingsConfig.pm25AirQualityDisabled) {
this.airQualityService.getCharacteristic(api.hap.Characteristic.PM2_5Density).setProps({
unit: "µg/m³"
});
}

if (this.airthingsDevice.sensors.radonShortTermAvg && !this.airthingsConfig.radonAirQualityDisabled) {
this.airQualityService.addCharacteristic(new api.hap.Characteristic("Radon", "B42E01AA-ADE7-11E4-89D3-123B93F75CBA", {
format: Formats.UINT16,
perms: [Perms.NOTIFY, Perms.PAIRED_READ],
unit: "Bq/m³",
minValue: 0,
maxValue: 20000,
maxValue: 65535,
minStep: 1
}));
}

this.airQualityService.getCharacteristic(api.hap.Characteristic.VOCDensity).setProps({
unit: "µg/m³",
maxValue: 65535
});
if (this.airthingsDevice.sensors.voc && !this.airthingsConfig.vocAirQualityDisabled) {
this.airQualityService.getCharacteristic(api.hap.Characteristic.VOCDensity).setProps({
unit: "µg/m³",
maxValue: 65535
});

if (this.airthingsDevice.sensors.voc) {
this.airQualityService.addCharacteristic(new api.hap.Characteristic("VOC Density (ppb)", "E5B6DA60-E041-472A-BE2B-8318B8A724C5", {
format: Formats.UINT16,
perms: [Perms.NOTIFY, Perms.PAIRED_READ],
unit: "ppb",
minValue: 0,
maxValue: 10000,
maxValue: 65535,
minStep: 1
}));
}
Expand Down Expand Up @@ -238,19 +241,23 @@ export class AirthingsPlugin implements AccessoryPlugin {
this.getAirQuality(api, this.latestSamples)
);

if (this.latestSamples.data.mold) {
this.airQualityService.getCharacteristic("Mold")?.updateValue(this.latestSamples.data.mold);
if (this.latestSamples.data.co2 && !this.airthingsConfig.co2AirQualityDisabled) {
this.airQualityService.getCharacteristic(api.hap.Characteristic.CarbonDioxideLevel).updateValue(this.latestSamples.data.co2);
}

if (this.latestSamples.data.humidity && !this.airthingsConfig.humidityAirQualityDisabled) {
this.airQualityService.getCharacteristic(api.hap.Characteristic.CurrentRelativeHumidity).updateValue(this.latestSamples.data.humidity);
}

if (this.latestSamples.data.pm25) {
if (this.latestSamples.data.pm25 && !this.airthingsConfig.pm25AirQualityDisabled) {
this.airQualityService.getCharacteristic(api.hap.Characteristic.PM2_5Density).updateValue(this.latestSamples.data.pm25);
}

if (this.latestSamples.data.radonShortTermAvg) {
if (this.latestSamples.data.radonShortTermAvg && !this.airthingsConfig.radonAirQualityDisabled) {
this.airQualityService.getCharacteristic("Radon")?.updateValue(this.latestSamples.data.radonShortTermAvg);
}

if (this.latestSamples.data.voc) {
if (this.latestSamples.data.voc && !this.airthingsConfig.vocAirQualityDisabled) {
this.airQualityService.getCharacteristic(api.hap.Characteristic.VOCDensity)?.updateValue(
this.latestSamples.data.voc * 2.2727
);
Expand Down Expand Up @@ -329,81 +336,68 @@ export class AirthingsPlugin implements AccessoryPlugin {
getAirQuality(api: API, latestSamples: AirthingsApiDeviceSample) {
let aq = api.hap.Characteristic.AirQuality.UNKNOWN;

const humidity = latestSamples.data.humidity;
if (humidity) {
if (humidity < 25 || humidity >= 70) {
aq = Math.max(aq, api.hap.Characteristic.AirQuality.POOR);
}
else if (humidity < 30 || humidity >= 60) {
aq = Math.max(aq, api.hap.Characteristic.AirQuality.FAIR);
}
else {
aq = Math.max(aq, api.hap.Characteristic.AirQuality.EXCELLENT);
}
}

const co2 = latestSamples.data.co2;
if (co2) {
if (co2 && !this.airthingsConfig.co2AirQualityDisabled) {
if (co2 >= 1000) {
aq = Math.max(aq, api.hap.Characteristic.AirQuality.POOR);
}
else if (co2 >= 800) {
aq = Math.max(aq, api.hap.Characteristic.AirQuality.FAIR);
}
else {
aq = Math.max(aq, api.hap.Characteristic.AirQuality.EXCELLENT);
aq = Math.max(aq, api.hap.Characteristic.AirQuality.GOOD);
}
}

const mold = latestSamples.data.mold;
if (mold) {
if (mold >= 5) {
const humidity = latestSamples.data.humidity;
if (humidity && !this.airthingsConfig.humidityAirQualityDisabled) {
if (humidity < 25 || humidity >= 70) {
aq = Math.max(aq, api.hap.Characteristic.AirQuality.POOR);
}
else if (mold >= 3) {
else if (humidity < 30 || humidity >= 60) {
aq = Math.max(aq, api.hap.Characteristic.AirQuality.FAIR);
}
else {
aq = Math.max(aq, api.hap.Characteristic.AirQuality.EXCELLENT);
aq = Math.max(aq, api.hap.Characteristic.AirQuality.GOOD);
}
}

const pm25 = latestSamples.data.pm25;
if (pm25) {
if (pm25 && !this.airthingsConfig.pm25AirQualityDisabled) {
if (pm25 >= 25) {
aq = Math.max(aq, api.hap.Characteristic.AirQuality.POOR);
}
else if (pm25 >= 10) {
aq = Math.max(aq, api.hap.Characteristic.AirQuality.FAIR);
}
else {
aq = Math.max(aq, api.hap.Characteristic.AirQuality.EXCELLENT);
aq = Math.max(aq, api.hap.Characteristic.AirQuality.GOOD);
}
}

const radonShortTermAvg = latestSamples.data.radonShortTermAvg;
if (radonShortTermAvg) {
if (radonShortTermAvg && !this.airthingsConfig.radonAirQualityDisabled) {
if (radonShortTermAvg >= 150) {
aq = Math.max(aq, api.hap.Characteristic.AirQuality.POOR);
}
else if (radonShortTermAvg >= 100) {
aq = Math.max(aq, api.hap.Characteristic.AirQuality.FAIR);
}
else {
aq = Math.max(aq, api.hap.Characteristic.AirQuality.EXCELLENT);
aq = Math.max(aq, api.hap.Characteristic.AirQuality.GOOD);
}
}

const voc = latestSamples.data.voc;
if (voc) {
if (voc && !this.airthingsConfig.vocAirQualityDisabled) {
if (voc >= 2000) {
aq = Math.max(aq, api.hap.Characteristic.AirQuality.POOR);
}
else if (voc >= 250) {
aq = Math.max(aq, api.hap.Characteristic.AirQuality.FAIR);
}
else {
aq = Math.max(aq, api.hap.Characteristic.AirQuality.EXCELLENT);
aq = Math.max(aq, api.hap.Characteristic.AirQuality.GOOD);
}
}

Expand All @@ -415,6 +409,11 @@ interface AirthingsPluginConfig extends AccessoryConfig {
clientId?: string;
clientSecret?: string;
serialNumber?: string;
co2AirQualityDisabled?: boolean;
humidityAirQualityDisabled?: boolean;
pm25AirQualityDisabled?: boolean;
radonAirQualityDisabled?: boolean;
vocAirQualityDisabled?: boolean;
co2DetectedThreshold?: number;
radonLeakThreshold?: number;
debug?: boolean;
Expand Down

0 comments on commit 4114c92

Please sign in to comment.