homebridge-airthings
Version:
A Homebridge plugin for Airthings air quality monitors via the Airthings Consumer API.
319 lines • 17.3 kB
JavaScript
import { AirthingsApi } from './api.js';
import { getAirthingsDeviceInfoBySerialNumber } from './device.js';
export class AirthingsPlugin {
log;
timer;
airthingsApi;
airthingsConfig;
airthingsDevice;
informationService;
batteryService;
airQualityService;
temperatureService;
humidityService;
carbonDioxideService;
airPressureService;
radonService;
latestSamples = {
data: {}
};
constructor(log, config, api) {
this.log = log;
if (!config.clientId) {
this.log.error('Missing required config value: clientId');
}
if (!config.clientSecret) {
this.log.error('Missing required config value: clientSecret');
}
if (!config.serialNumber) {
this.log.error('Missing required config value: serialNumber');
config.serialNumber = '0000000000';
}
if (!config.co2DetectedThreshold) {
config.co2DetectedThreshold = 1000;
}
if (!Number.isSafeInteger(config.co2DetectedThreshold)) {
this.log.warn('Invalid config value: co2DetectedThreshold (not a valid integer)');
config.co2DetectedThreshold = 1000;
}
if (config.radonLeakThreshold && !Number.isSafeInteger(config.radonLeakThreshold)) {
this.log.warn('Invalid config value: radonLeakThreshold (not a valid integer)');
config.radonLeakThreshold = undefined;
}
if (!config.debug) {
config.debug = false;
}
if (!config.refreshInterval) {
config.refreshInterval = 150;
}
if (!Number.isSafeInteger(config.refreshInterval)) {
this.log.warn('Invalid config value: refreshInterval (not a valid integer)');
config.refreshInterval = 150;
}
if (config.refreshInterval < 60) {
this.log.warn('Invalid config value: refreshInterval (<60s may cause rate limiting)');
config.refreshInterval = 60;
}
if (!config.tokenScope) {
config.tokenScope = 'read:device:current_values';
}
this.airthingsApi = new AirthingsApi(config.tokenScope, config.clientId, config.clientSecret);
this.airthingsConfig = config;
this.airthingsDevice = getAirthingsDeviceInfoBySerialNumber(config.serialNumber);
this.log.info(`Device Model: ${this.airthingsDevice.model}`);
this.log.info(`Serial Number: ${this.airthingsConfig.serialNumber}`);
this.log.info('Sensor Settings:');
this.log.info(` * CO₂ Detected Threshold: ${this.airthingsConfig.co2DetectedThreshold} ppm`);
this.log.info(` * Radon Leak Sensor: ${this.airthingsDevice.sensors.radonShortTermAvg ? (this.airthingsConfig.radonLeakThreshold ? 'Enabled' : 'Disabled') : 'Not Supported'}`);
if (this.airthingsDevice.sensors.radonShortTermAvg && this.airthingsConfig.radonLeakThreshold) {
this.log.info(` * Radon Leak Threshold: ${this.airthingsConfig.radonLeakThreshold} Bq/m³`);
}
this.log.info('Advanced Settings:');
this.log.info(` * Debug Logging: ${this.airthingsConfig.debug}`);
this.log.info(` * Refresh Interval: ${this.airthingsConfig.refreshInterval}s`);
this.log.info(` * Token Scope: ${this.airthingsConfig.tokenScope}`);
// HomeKit Accessory Information Service
this.informationService = new api.hap.Service.AccessoryInformation()
.setCharacteristic(api.hap.Characteristic.Manufacturer, 'Airthings')
.setCharacteristic(api.hap.Characteristic.Model, this.airthingsDevice.model)
.setCharacteristic(api.hap.Characteristic.Name, config.name)
.setCharacteristic(api.hap.Characteristic.SerialNumber, config.serialNumber)
.setCharacteristic(api.hap.Characteristic.FirmwareRevision, 'Unknown');
// HomeKit Battery Service
this.batteryService = new api.hap.Service.Battery('Battery');
// HomeKit Air Quality Service
this.airQualityService = new api.hap.Service.AirQualitySensor('Air Quality');
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.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: "uint16" /* Formats.UINT16 */,
perms: ["ev" /* Perms.NOTIFY */, "pr" /* Perms.PAIRED_READ */],
unit: 'Bq/m³',
minValue: 0,
maxValue: 65535,
minStep: 1
}));
}
if (this.airthingsDevice.sensors.voc && !this.airthingsConfig.vocAirQualityDisabled) {
this.airQualityService.getCharacteristic(api.hap.Characteristic.VOCDensity).setProps({
unit: 'µg/m³',
maxValue: 65535
});
this.airQualityService.addCharacteristic(new api.hap.Characteristic('VOC Density (ppb)', 'E5B6DA60-E041-472A-BE2B-8318B8A724C5', {
format: "uint16" /* Formats.UINT16 */,
perms: ["ev" /* Perms.NOTIFY */, "pr" /* Perms.PAIRED_READ */],
unit: 'ppb',
minValue: 0,
maxValue: 65535,
minStep: 1
}));
}
// HomeKit Temperature Service
this.temperatureService = new api.hap.Service.TemperatureSensor('Temp');
// HomeKit Humidity Service
this.humidityService = new api.hap.Service.HumiditySensor('Humidity');
// HomeKit CO2 Service
this.carbonDioxideService = new api.hap.Service.CarbonDioxideSensor('CO2');
// Eve Air Pressure Service
this.airPressureService = new api.hap.Service('Air Pressure', 'e863f00a-079e-48ff-8f27-9c2605a29f52');
this.airPressureService.addCharacteristic(new api.hap.Characteristic('Air Pressure', 'e863f10f-079e-48ff-8f27-9c2605a29f52', {
format: "uint16" /* Formats.UINT16 */,
perms: ["ev" /* Perms.NOTIFY */, "pr" /* Perms.PAIRED_READ */],
unit: 'mBar',
minValue: 0,
maxValue: 1200,
minStep: 1
}));
this.airPressureService.addCharacteristic(api.hap.Characteristic.StatusActive);
// HomeKit Radon (Leak) Service
this.radonService = new api.hap.Service.LeakSensor('Radon');
this.refreshCharacteristics(api);
this.timer = setInterval(async () => {
await this.refreshCharacteristics(api);
}, config.refreshInterval * 1000);
}
getServices() {
const services = [this.informationService, this.batteryService, this.airQualityService];
if (this.airthingsDevice.sensors.temp) {
services.push(this.temperatureService);
}
if (this.airthingsDevice.sensors.humidity) {
services.push(this.humidityService);
}
if (this.airthingsDevice.sensors.co2) {
services.push(this.carbonDioxideService);
}
if (this.airthingsDevice.sensors.pressure) {
services.push(this.airPressureService);
}
if (this.airthingsDevice.sensors.radonShortTermAvg && this.airthingsConfig.radonLeakThreshold != undefined) {
services.push(this.radonService);
}
return services;
}
async getLatestSamples() {
if (this.airthingsConfig.serialNumber == undefined) {
return;
}
try {
this.latestSamples = await this.airthingsApi.getLatestSamples(this.airthingsConfig.serialNumber);
if (this.airthingsConfig.debug) {
this.log.info(JSON.stringify(this.latestSamples.data));
}
}
catch (err) {
if (err instanceof Error) {
this.log.error(err.message);
}
}
}
async refreshCharacteristics(api) {
await this.getLatestSamples();
// HomeKit Battery Service
if (this.latestSamples.data.battery != undefined) {
this.batteryService.getCharacteristic(api.hap.Characteristic.BatteryLevel).updateValue(this.latestSamples.data.battery);
this.batteryService.getCharacteristic(api.hap.Characteristic.StatusLowBattery).updateValue(this.latestSamples.data.battery > 10
? api.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL
: api.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW);
}
// HomeKit Air Quality Service
this.airQualityService.getCharacteristic(api.hap.Characteristic.AirQuality).updateValue(this.getAirQuality(api, this.latestSamples));
if (this.latestSamples.data.co2 != undefined && !this.airthingsConfig.co2AirQualityDisabled) {
this.airQualityService.getCharacteristic(api.hap.Characteristic.CarbonDioxideLevel).updateValue(this.latestSamples.data.co2);
}
if (this.latestSamples.data.humidity != undefined && !this.airthingsConfig.humidityAirQualityDisabled) {
this.airQualityService.getCharacteristic(api.hap.Characteristic.CurrentRelativeHumidity).updateValue(this.latestSamples.data.humidity);
}
if (this.latestSamples.data.pm25 != undefined && !this.airthingsConfig.pm25AirQualityDisabled) {
this.airQualityService.getCharacteristic(api.hap.Characteristic.PM2_5Density).updateValue(this.latestSamples.data.pm25);
}
if (this.latestSamples.data.radonShortTermAvg != undefined && !this.airthingsConfig.radonAirQualityDisabled) {
this.airQualityService.getCharacteristic('Radon')?.updateValue(this.latestSamples.data.radonShortTermAvg);
}
if (this.latestSamples.data.voc != undefined && !this.airthingsConfig.vocAirQualityDisabled) {
this.airQualityService.getCharacteristic(api.hap.Characteristic.VOCDensity)?.updateValue(this.latestSamples.data.voc * 2.2727);
this.airQualityService.getCharacteristic('VOC Density (ppb)')?.updateValue(this.latestSamples.data.voc);
}
this.airQualityService.getCharacteristic(api.hap.Characteristic.StatusActive).updateValue(this.latestSamples.data.time != undefined && Date.now() / 1000 - this.latestSamples.data.time < 2 * 60 * 60);
// HomeKit Temperature Service
if (this.latestSamples.data.temp != undefined) {
this.temperatureService.getCharacteristic(api.hap.Characteristic.CurrentTemperature).updateValue(this.latestSamples.data.temp);
this.temperatureService.getCharacteristic(api.hap.Characteristic.StatusActive).updateValue(true);
}
else {
this.temperatureService.getCharacteristic(api.hap.Characteristic.StatusActive).updateValue(this.latestSamples.data.time != undefined && Date.now() / 1000 - this.latestSamples.data.time < 2 * 60 * 60);
}
// HomeKit Humidity Service
if (this.latestSamples.data.humidity != undefined) {
this.humidityService.getCharacteristic(api.hap.Characteristic.CurrentRelativeHumidity).updateValue(this.latestSamples.data.humidity);
this.humidityService.getCharacteristic(api.hap.Characteristic.StatusActive).updateValue(true);
}
else {
this.humidityService.getCharacteristic(api.hap.Characteristic.StatusActive).updateValue(this.latestSamples.data.time != undefined && Date.now() / 1000 - this.latestSamples.data.time < 2 * 60 * 60);
}
// HomeKit CO2 Service
if (this.latestSamples.data.co2 != undefined && this.airthingsConfig.co2DetectedThreshold) {
this.carbonDioxideService.getCharacteristic(api.hap.Characteristic.CarbonDioxideDetected).updateValue(this.latestSamples.data.co2 < this.airthingsConfig.co2DetectedThreshold
? api.hap.Characteristic.CarbonDioxideDetected.CO2_LEVELS_NORMAL
: api.hap.Characteristic.CarbonDioxideDetected.CO2_LEVELS_ABNORMAL);
this.carbonDioxideService.getCharacteristic(api.hap.Characteristic.CarbonDioxideLevel).updateValue(this.latestSamples.data.co2);
this.carbonDioxideService.getCharacteristic(api.hap.Characteristic.StatusActive).updateValue(true);
}
else {
this.carbonDioxideService.getCharacteristic(api.hap.Characteristic.StatusActive).updateValue(this.latestSamples.data.time != undefined && Date.now() / 1000 - this.latestSamples.data.time < 2 * 60 * 60);
}
// Eve Air Pressure Service
if (this.latestSamples.data.pressure != undefined) {
this.airPressureService.getCharacteristic('Air Pressure')?.updateValue(this.latestSamples.data.pressure);
this.airPressureService.getCharacteristic(api.hap.Characteristic.StatusActive).updateValue(true);
}
else {
this.airPressureService.getCharacteristic(api.hap.Characteristic.StatusActive).updateValue(this.latestSamples.data.time != undefined && Date.now() / 1000 - this.latestSamples.data.time < 2 * 60 * 60);
}
// HomeKit Radon (Leak) Service
if (this.latestSamples.data.radonShortTermAvg != undefined && this.airthingsConfig.radonLeakThreshold) {
this.radonService.getCharacteristic(api.hap.Characteristic.LeakDetected).updateValue(this.latestSamples.data.radonShortTermAvg < this.airthingsConfig.radonLeakThreshold
? api.hap.Characteristic.LeakDetected.LEAK_NOT_DETECTED
: api.hap.Characteristic.LeakDetected.LEAK_DETECTED);
this.radonService.getCharacteristic(api.hap.Characteristic.StatusActive).updateValue(true);
}
else {
this.radonService.getCharacteristic(api.hap.Characteristic.StatusActive).updateValue(this.latestSamples.data.time != undefined && Date.now() / 1000 - this.latestSamples.data.time < 2 * 60 * 60);
}
}
getAirQuality(api, latestSamples) {
let aq = api.hap.Characteristic.AirQuality.UNKNOWN;
const co2 = latestSamples.data.co2;
if (co2 != undefined && !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.GOOD);
}
}
const humidity = latestSamples.data.humidity;
if (humidity != undefined && !this.airthingsConfig.humidityAirQualityDisabled) {
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.GOOD);
}
}
const pm25 = latestSamples.data.pm25;
if (pm25 != undefined && !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.GOOD);
}
}
const radonShortTermAvg = latestSamples.data.radonShortTermAvg;
if (radonShortTermAvg != undefined && !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.GOOD);
}
}
const voc = latestSamples.data.voc;
if (voc != undefined && !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.GOOD);
}
}
return aq;
}
}
//# sourceMappingURL=plugin.js.map