homebridge-airthings
Version:
A Homebridge plugin for Airthings Air Quality Monitors via the Airthings Consumer API.
336 lines • 18.3 kB
JavaScript
import { AirthingsClient, SensorUnits } from 'airthings-consumer-api';
import { getAirthingsDeviceInfoBySerialNumber } from './device.js';
export class AirthingsPlugin {
log;
airthingsClient;
airthingsConfig;
airthingsDevice;
informationService;
batteryService;
airQualityService;
temperatureService;
humidityService;
carbonDioxideService;
airPressureService;
radonService;
lastSensorResult = {
serialNumber: '',
sensors: []
};
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;
}
this.airthingsClient = new AirthingsClient({
clientId: config.clientId ?? '',
clientSecret: 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`);
// 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);
setInterval(async () => {
await this.refreshCharacteristics(api);
}, config.refreshInterval * 1000);
}
getServices() {
const services = [this.informationService, this.airQualityService];
if (this.airthingsDevice.sensors.battery && !this.airthingsConfig.batteryDisabled) {
services.push(this.batteryService);
}
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 getLatestSensorResult() {
if (this.airthingsConfig.serialNumber == undefined) {
return;
}
try {
const sensorResults = await this.airthingsClient.getSensors(SensorUnits.Metric, [this.airthingsConfig.serialNumber]);
if (sensorResults.results.length === 0) {
this.log.error('No sensor results found!');
return;
}
if (sensorResults.results[0]) {
this.lastSensorResult = sensorResults.results[0];
if (this.airthingsConfig.debug) {
this.log.info(JSON.stringify(this.lastSensorResult));
}
}
}
catch (err) {
if (err instanceof Error) {
this.log.error(err.message);
}
}
}
async refreshCharacteristics(api) {
await this.getLatestSensorResult();
const co2Sensor = this.lastSensorResult.sensors.find(x => x.sensorType === 'co2');
const humiditySensor = this.lastSensorResult.sensors.find(x => x.sensorType === 'humidity');
const pm25Sensor = this.lastSensorResult.sensors.find(x => x.sensorType === 'pm25');
const pressureSensor = this.lastSensorResult.sensors.find(x => x.sensorType === 'pressure');
const radonShortTermAvgSensor = this.lastSensorResult.sensors.find(x => x.sensorType === 'radonShortTermAvg');
const tempSensor = this.lastSensorResult.sensors.find(x => x.sensorType === 'temp');
const vocSensor = this.lastSensorResult.sensors.find(x => x.sensorType === 'voc');
const lastSensorResultRecordedAt = this.lastSensorResult.recorded ? Math.floor(new Date(this.lastSensorResult.recorded).getTime()) : undefined;
// HomeKit Battery Service
if (this.lastSensorResult.batteryPercentage) {
this.batteryService.getCharacteristic(api.hap.Characteristic.BatteryLevel).updateValue(this.lastSensorResult.batteryPercentage);
this.batteryService.getCharacteristic(api.hap.Characteristic.StatusLowBattery).updateValue(this.lastSensorResult.batteryPercentage > 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.lastSensorResult));
if (co2Sensor && !this.airthingsConfig.co2AirQualityDisabled) {
this.airQualityService.getCharacteristic(api.hap.Characteristic.CarbonDioxideLevel).updateValue(co2Sensor.value);
}
if (humiditySensor && !this.airthingsConfig.humidityAirQualityDisabled) {
this.airQualityService.getCharacteristic(api.hap.Characteristic.CurrentRelativeHumidity).updateValue(humiditySensor.value);
}
if (pm25Sensor && !this.airthingsConfig.pm25AirQualityDisabled) {
this.airQualityService.getCharacteristic(api.hap.Characteristic.PM2_5Density).updateValue(pm25Sensor.value);
}
if (radonShortTermAvgSensor && !this.airthingsConfig.radonAirQualityDisabled) {
this.airQualityService.getCharacteristic('Radon')?.updateValue(radonShortTermAvgSensor.value);
}
if (vocSensor && !this.airthingsConfig.vocAirQualityDisabled) {
this.airQualityService.getCharacteristic(api.hap.Characteristic.VOCDensity)?.updateValue(vocSensor.value * 2.2727);
this.airQualityService.getCharacteristic('VOC Density (ppb)')?.updateValue(vocSensor.value);
}
this.airQualityService.getCharacteristic(api.hap.Characteristic.StatusActive).updateValue(lastSensorResultRecordedAt != undefined && Date.now() - lastSensorResultRecordedAt < 2 * 60 * 60 * 1000);
// HomeKit Temperature Service
if (tempSensor) {
this.temperatureService.getCharacteristic(api.hap.Characteristic.CurrentTemperature).updateValue(tempSensor.value);
this.temperatureService.getCharacteristic(api.hap.Characteristic.StatusActive).updateValue(true);
}
else {
this.temperatureService.getCharacteristic(api.hap.Characteristic.StatusActive).updateValue(lastSensorResultRecordedAt != undefined && Date.now() - lastSensorResultRecordedAt < 2 * 60 * 60 * 1000);
}
// HomeKit Humidity Service
if (humiditySensor) {
this.humidityService.getCharacteristic(api.hap.Characteristic.CurrentRelativeHumidity).updateValue(humiditySensor.value);
this.humidityService.getCharacteristic(api.hap.Characteristic.StatusActive).updateValue(true);
}
else {
this.humidityService.getCharacteristic(api.hap.Characteristic.StatusActive).updateValue(lastSensorResultRecordedAt != undefined && Date.now() - lastSensorResultRecordedAt < 2 * 60 * 60 * 1000);
}
// HomeKit CO2 Service
if (co2Sensor && this.airthingsConfig.co2DetectedThreshold) {
this.carbonDioxideService.getCharacteristic(api.hap.Characteristic.CarbonDioxideDetected).updateValue(co2Sensor.value < 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(co2Sensor.value);
this.carbonDioxideService.getCharacteristic(api.hap.Characteristic.StatusActive).updateValue(true);
}
else {
this.carbonDioxideService.getCharacteristic(api.hap.Characteristic.StatusActive).updateValue(lastSensorResultRecordedAt != undefined && Date.now() - lastSensorResultRecordedAt < 2 * 60 * 60 * 1000);
}
// Eve Air Pressure Service
if (pressureSensor) {
this.airPressureService.getCharacteristic('Air Pressure')?.updateValue(pressureSensor.value);
this.airPressureService.getCharacteristic(api.hap.Characteristic.StatusActive).updateValue(true);
}
else {
this.airPressureService.getCharacteristic(api.hap.Characteristic.StatusActive).updateValue(lastSensorResultRecordedAt != undefined && Date.now() - lastSensorResultRecordedAt < 2 * 60 * 60 * 1000);
}
// HomeKit Radon (Leak) Service
if (radonShortTermAvgSensor && this.airthingsConfig.radonLeakThreshold) {
this.radonService.getCharacteristic(api.hap.Characteristic.LeakDetected).updateValue(radonShortTermAvgSensor.value < 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(lastSensorResultRecordedAt != undefined && Date.now() - lastSensorResultRecordedAt < 2 * 60 * 60 * 1000);
}
}
getAirQuality(api, lastResult) {
let aq = api.hap.Characteristic.AirQuality.UNKNOWN;
const co2Sensor = lastResult.sensors.find(x => x.sensorType === 'co2');
if (co2Sensor && !this.airthingsConfig.co2AirQualityDisabled) {
if (co2Sensor.value >= 1000) {
aq = Math.max(aq, api.hap.Characteristic.AirQuality.POOR);
}
else if (co2Sensor.value >= 800) {
aq = Math.max(aq, api.hap.Characteristic.AirQuality.FAIR);
}
else {
aq = Math.max(aq, api.hap.Characteristic.AirQuality.GOOD);
}
}
const humiditySensor = lastResult.sensors.find(x => x.sensorType === 'humidity');
if (humiditySensor && !this.airthingsConfig.humidityAirQualityDisabled) {
if (humiditySensor.value < 25 || humiditySensor.value >= 70) {
aq = Math.max(aq, api.hap.Characteristic.AirQuality.POOR);
}
else if (humiditySensor.value < 30 || humiditySensor.value >= 60) {
aq = Math.max(aq, api.hap.Characteristic.AirQuality.FAIR);
}
else {
aq = Math.max(aq, api.hap.Characteristic.AirQuality.GOOD);
}
}
const pm25Sensor = lastResult.sensors.find(x => x.sensorType === 'pm25');
if (pm25Sensor && !this.airthingsConfig.pm25AirQualityDisabled) {
if (pm25Sensor.value >= 25) {
aq = Math.max(aq, api.hap.Characteristic.AirQuality.POOR);
}
else if (pm25Sensor.value >= 10) {
aq = Math.max(aq, api.hap.Characteristic.AirQuality.FAIR);
}
else {
aq = Math.max(aq, api.hap.Characteristic.AirQuality.GOOD);
}
}
const radonShortTermAvgSensor = lastResult.sensors.find(x => x.sensorType === 'radonShortTermAvg');
if (radonShortTermAvgSensor && !this.airthingsConfig.radonAirQualityDisabled) {
if (radonShortTermAvgSensor.value >= 150) {
aq = Math.max(aq, api.hap.Characteristic.AirQuality.POOR);
}
else if (radonShortTermAvgSensor.value >= 100) {
aq = Math.max(aq, api.hap.Characteristic.AirQuality.FAIR);
}
else {
aq = Math.max(aq, api.hap.Characteristic.AirQuality.GOOD);
}
}
const vocSensor = lastResult.sensors.find(x => x.sensorType === 'voc');
if (vocSensor && !this.airthingsConfig.vocAirQualityDisabled) {
if (vocSensor.value >= 2000) {
aq = Math.max(aq, api.hap.Characteristic.AirQuality.POOR);
}
else if (vocSensor.value >= 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