Skip to content
This repository has been archived by the owner on Feb 11, 2023. It is now read-only.

Commit

Permalink
feat: replace co2 sensor with air quality sensor
Browse files Browse the repository at this point in the history
  • Loading branch information
dhayab committed Oct 12, 2019
1 parent c0a88ce commit 443351d
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 35 deletions.
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# homebridge-withings-air-quality

This plugin retrieves carbon dioxide and temperature levels from a Withings Smart Body Analyzer smart scale and exposes them as Homekit sensors with [Homebridge](https://github.com/nfarina/homebridge).
This plugin retrieves carbon dioxide and temperature levels from a Withings Smart Body Analyzer smart scale and exposes them as HomeKit sensors with [Homebridge](https://github.com/nfarina/homebridge).

## Installation

Expand Down Expand Up @@ -29,10 +29,15 @@ Here is a sample config (you can view [config.sample.json](./config.sample.json)
"email": "my.withings@email.com",
"password": "myWithingsPassword",
"mac": "0ab2c3d4e5f6", // Do not add separators
"coThreshold": 1000 // Optional
"levels": [
350, // Excellent
1000, // Good
2500, // Fair
5000 // Inferior
]
}
]
}
```

The `coThreshold` parameter is used to trigger an "abnormal level" alert on the carbon dioxide sensor in Homekit. The value is measured in ppm (parts per million). For more information, [check out this page](https://support.withings.com/hc/en-us/articles/201489797-Smart-Body-Analyzer-WS-50-Frequently-asked-questions-about-air-quality-measurements).
The `levels` parameter is used to control the information displayed on the air quality sensor. The values are measured in ppm (parts per million). Carbon dioxide level higher than the value for "Inferior" will trigger a warning in HomeKit. For more information, [check out this page](https://support.withings.com/hc/en-us/articles/201489797-Smart-Body-Analyzer-WS-50-Frequently-asked-questions-about-air-quality-measurements).
2 changes: 1 addition & 1 deletion config.sample.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"email": "my.withings@email.com",
"password": "myWithingsPassword",
"mac": "0ab2c3d4e5f6",
"coThreshold": 1000
"levels": [350, 1000, 2500, 5000]
}
]
}
43 changes: 36 additions & 7 deletions config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,42 @@
"maxLength": 12,
"required": true
},
"coThreshold": {
"title": "Carbon dioxide alert threshold",
"description": "Will trigger the abnormal level alert if your carbon dioxide level is higher than this value.",
"type": "number",
"default": 1000,
"minimum": 300,
"maximum": 100000
"levels": {
"title": "Air quality levels",
"type": "array",
"minItems": 4,
"uniqueItems": true,
"items": [
{
"title": "Excellent",
"type": "number",
"default": 350,
"minimum": 0,
"maximum": 1000000
},
{
"title": "Good",
"type": "number",
"default": 1000,
"minimum": 0,
"maximum": 1000000
},
{
"title": "Fair",
"type": "number",
"default": 2500,
"minimum": 0,
"maximum": 1000000
},
{
"title": "Inferior",
"description": "Carbon dioxide levels higher than this value will trigger a warning in HomeKit",
"type": "number",
"default": 5000,
"minimum": 0,
"maximum": 1000000
}
]
}
}
}
Expand Down
70 changes: 46 additions & 24 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import pkg from '../package.json';
import { WithingsApi } from './lib/api';
import { DataType } from './lib/api.types';

type AirQualityLevel = { level: number, threshold: number, label: string };
type Config = { name: string, email: string, password: string, mac: string, levels?: number[] };
type Homebridge = {
hap: typeof Hap;
registerAccessory(pluginName: string, accessoryName: string, constructor: any, configurationRequestHandler?: any): void;
Expand All @@ -13,7 +15,13 @@ let Service: typeof Hap.Service;
let Characteristic: typeof Hap.Characteristic;

const DEFAULT_BATTERY_LOW_LEVEL = 10;
const DEFAULT_CO_THRESHOLD = 1000;
const DEFAULT_AIR_QUALITY_LEVELS: AirQualityLevel[] = [
{ level: Hap.Characteristic.AirQuality.EXCELLENT, threshold: 350, label: 'Excellent' },
{ level: Hap.Characteristic.AirQuality.GOOD, threshold: 1000, label: 'Good' },
{ level: Hap.Characteristic.AirQuality.FAIR, threshold: 2500, label: 'Fair' },
{ level: Hap.Characteristic.AirQuality.INFERIOR, threshold: 5000, label: 'Inferior' },
{ level: Hap.Characteristic.AirQuality.POOR, threshold: Number.MAX_SAFE_INTEGER, label: 'Poor' },
];

export default function (homebridge: Homebridge) {
Service = homebridge.hap.Service;
Expand All @@ -25,20 +33,28 @@ export default function (homebridge: Homebridge) {
class WithingsScale {
private readonly api: WithingsApi;
private readonly name = this.config.name;
private readonly coThreshold = this.config.coThreshold && parseInt(this.config.coThreshold, 10) || DEFAULT_CO_THRESHOLD;
private readonly levels: AirQualityLevel[];

readonly informationService = new Service.AccessoryInformation('', '');
readonly batteryService = new Service.BatteryService('', '');
readonly carbonDioxideService = new Service.CarbonDioxideSensor('', '');
readonly airQualityService = new Service.AirQualitySensor('', '');
readonly temperatureService = new Service.TemperatureSensor('', '');

constructor(
private readonly log: any,
private readonly config: Record<string, string>,
private readonly config: Config,
) {
this.api = new WithingsApi(config.email, config.password, config.mac);
this.api.on('error', ({ message, error }) => this.log.warn(message, '/', error.message || error));

if (!config.levels || config.levels.length !== DEFAULT_AIR_QUALITY_LEVELS.length - 1) {
this.levels = DEFAULT_AIR_QUALITY_LEVELS;
} else {
this.levels = DEFAULT_AIR_QUALITY_LEVELS.map((level, index) => {
return { ...level, threshold: config.levels[index] || level.threshold };
});
}

this.init();
}

Expand All @@ -49,24 +65,21 @@ class WithingsScale {
this.initServiceEvents(
'battery',
this.batteryService.getCharacteristic(Characteristic.BatteryLevel),
() => this.api.getBatteryLevel(),
(value) => this.setBatteryLevel(value),
() => this.updateBatteryLevel(),
);

this.carbonDioxideService.setCharacteristic(Characteristic.Name, 'CO² Level');
this.airQualityService.setCharacteristic(Characteristic.Name, 'Air Quality');
this.initServiceEvents(
'carbondioxide',
this.carbonDioxideService.getCharacteristic(Characteristic.CarbonDioxideLevel),
() => this.api.getCarbonDioxide(),
(value) => this.setCarbonDioxideLevel(value),
this.airQualityService.getCharacteristic(Characteristic.AirQuality),
() => this.updateAirQuality(),
);

this.temperatureService.setCharacteristic(Characteristic.Name, 'Temperature');
this.initServiceEvents(
'temperature',
this.temperatureService.getCharacteristic(Characteristic.CurrentTemperature),
() => this.api.getTemperature(),
(value) => this.setTemperature(value),
() => this.updateTemperature(),
);
}

Expand All @@ -83,7 +96,7 @@ class WithingsScale {
this.informationService,
this.batteryService,
this.temperatureService,
this.carbonDioxideService,
this.airQualityService,
];
}

Expand All @@ -95,37 +108,46 @@ class WithingsScale {
private async initServiceEvents(
type: DataType,
characteristic: Hap.Characteristic,
queryFn: () => number,
updateFn: (value: number) => void,
updateFn: () => Hap.CharacteristicValue,
) {
this.api.on(type, (value) => updateFn(value));
this.api.on(type, () => updateFn());
characteristic.on('get' as Hap.CharacteristicEventTypes, async (cb: Hap.NodeCallback<Hap.CharacteristicValue>) => {
const value = queryFn();
updateFn(value);
const value = updateFn();
cb(null, value);
});
}

private setBatteryLevel(value: number) {
private updateBatteryLevel() {
const value = this.api.getBatteryLevel();
this.log.debug(`[Battery Level] ${value}%`);
this.batteryService
.setCharacteristic(Characteristic.BatteryLevel, value)
.setCharacteristic(Characteristic.StatusLowBattery, value < DEFAULT_BATTERY_LOW_LEVEL ? 1 : 0)
;

return value;
}

private setCarbonDioxideLevel(value: number) {
this.log.debug(`[Carbon Dioxide Level] ${value}ppm`);
this.carbonDioxideService
.setCharacteristic(Characteristic.CarbonDioxideDetected, value >= this.coThreshold ? 1 : 0)
private updateAirQuality() {
const value = this.api.getCarbonDioxide();
const { label, level } = this.levels.find(({ threshold }) => value <= threshold);

this.log.debug(`[Air Quality] ${label} (${value}ppm)`);
this.airQualityService
.setCharacteristic(Characteristic.AirQuality, level)
.setCharacteristic(Characteristic.CarbonDioxideLevel, value)
;

return level;
}

private setTemperature(value: number) {
private updateTemperature() {
const value = this.api.getTemperature();
this.log.debug(`[Temperature] ${value}°C`);
this.temperatureService
.setCharacteristic(Characteristic.CurrentTemperature, value)
;

return value;
}
}

0 comments on commit 443351d

Please sign in to comment.