UNPKG

homebridge-foobot-multi

Version:
415 lines 20.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.FoobotPlatformAccessory = void 0; const foobot_1 = require("./foobot"); /** * Platform Accessory * An instance of this class is created for each accessory your platform registers * Each accessory may expose multiple services of different service types. */ class FoobotPlatformAccessory { constructor(platform, accessory) { // MAC is not currently used, leaving here for future // const mac = accessory.context.device.mac.match(/[A-F0-9]{2}/gi).join(':'); this.platform = platform; this.accessory = accessory; // initialize persistent data if (typeof accessory.context.data === 'undefined') { accessory.context.data = { airquality: this.platform.Characteristic.AirQuality.UNKNOWN, pm25density: 0, vocdensity: 0, currentTemperature: 0, currentRelativeHumidity: 0, carbonDioxideLevel: 0, carbonDioxidePeakLevel: 0, time: 0, }; } // set initial delay this.active = false; this.delay = 10 * 60 * 1000; // set accessory information this.accessoryInformation = this.accessory.getService(this.platform.Service.AccessoryInformation); this.accessoryInformation .setCharacteristic(this.platform.Characteristic.Manufacturer, 'Airboxlab SA') .setCharacteristic(this.platform.Characteristic.SerialNumber, accessory.context.device.uuid) .setCharacteristic(this.platform.Characteristic.Model, 'Foobot') .setCharacteristic(this.platform.Characteristic.AppMatchingIdentifier, 'id909284570'); // get the AirQualitySensor service if it exists, otherwise create a new AirQualitySensor service // you can create multiple services for each accessory this.airQualitySensor = this.accessory.getService(this.platform.Service.AirQualitySensor) || this.accessory.addService(this.platform.Service.AirQualitySensor); this.temperatureSensor = this.accessory.getService(this.platform.Service.TemperatureSensor) || this.accessory.addService(this.platform.Service.TemperatureSensor); this.humiditySensor = this.accessory.getService(this.platform.Service.HumiditySensor) || this.accessory.addService(this.platform.Service.HumiditySensor); this.carbonDioxideSensor = this.accessory.getService(this.platform.Service.CarbonDioxideSensor) || this.accessory.addService(this.platform.Service.CarbonDioxideSensor); // set the service name, this is what is displayed as the default name on the Home app // in this example we are using the name we stored in the `accessory.context` in the `discoverDevices` method. this.airQualitySensor.setCharacteristic(this.platform.Characteristic.Name, accessory.context.device.name + ' Air Quality'); this.temperatureSensor.setCharacteristic(this.platform.Characteristic.Name, accessory.context.device.name + ' Temperature'); this.humiditySensor.setCharacteristic(this.platform.Characteristic.Name, accessory.context.device.name + ' Humidity'); this.carbonDioxideSensor.setCharacteristic(this.platform.Characteristic.Name, accessory.context.device.name + ' Carbon Dioxide'); // create handlers for required characteristics this.airQualitySensor.getCharacteristic(this.platform.Characteristic.AirQuality) .onGet(this.handleAirQualityGet.bind(this)); this.temperatureSensor.getCharacteristic(this.platform.Characteristic.CurrentTemperature) .onGet(this.handleCurrentTemperatureGet.bind(this)); this.humiditySensor.getCharacteristic(this.platform.Characteristic.CurrentRelativeHumidity) .onGet(this.handleCurrentRelativeHumidityGet.bind(this)); this.carbonDioxideSensor.getCharacteristic(this.platform.Characteristic.CarbonDioxideDetected) .onGet(this.handleCarbonDioxideDetectedGet.bind(this)); // create handlers for optional characteristics this.airQualitySensor.getCharacteristic(this.platform.Characteristic.PM2_5Density) .onGet(this.handlePM2_5DensityGet.bind(this)); this.airQualitySensor.getCharacteristic(this.platform.Characteristic.VOCDensity) .onGet(this.handleVOCDensityGet.bind(this)); this.carbonDioxideSensor.getCharacteristic(this.platform.Characteristic.CarbonDioxideLevel) .onGet(this.handleCarbonDioxideLevelGet.bind(this)); this.carbonDioxideSensor.getCharacteristic(this.platform.Characteristic.CarbonDioxidePeakLevel) .onGet(this.handleCarbonDioxidePeakLevelGet.bind(this)); // remove CO2 service if (this.platform.config.co2off) { this.accessory.removeService(this.carbonDioxideSensor); } this.platform.log.debug('Finished initializing accessory:', accessory.context.device.name); /** * Updating characteristics values asynchronously. * Setup an interval to run based on Foobot API quotas * also, always update at startup * MINUTES_PER_REQUEST = 24 * 60 / REQUESTS_PER_DEVICE_PER_DAY * REQUESTS_PER_DEVICE_PER_DAY = 200 */ this.updatecharacteristics(); } /** * Handle requests to get the current value of the 'Air Quality' characteristic */ handleAirQualityGet() { // set this to a valid value for AirQuality const currentValue = this.accessory.context.data.airquality; return currentValue; } /** * Handle requests to get the current value of the 'PM2.5 Density' characteristic */ handlePM2_5DensityGet() { // set this to a valid value for PM2.5 Density const currentValue = this.accessory.context.data.pm25density; return currentValue; } /** * Handle requests to get the current value of the 'VOC Density' characteristic */ handleVOCDensityGet() { // set this to a valid value for VOC Density const currentValue = this.accessory.context.data.vocdensity; return currentValue; } /** * Handle requests to get the current value of the 'Current Temperature' characteristic */ handleCurrentTemperatureGet() { // set this to a valid value for CurrentTemperature const currentValue = this.accessory.context.data.currentTemperature; return currentValue; } /** * Handle requests to get the current value of the 'Current Relative Humidity' characteristic */ handleCurrentRelativeHumidityGet() { // set this to a valid value for CurrentRelativeHumidity const currentValue = this.accessory.context.data.currentRelativeHumidity; return currentValue; } /** * Handle requests to get the current value of the 'Carbon Dioxide Detected' characteristic */ handleCarbonDioxideDetectedGet() { // set this to a valid value for CarbonDioxideDetected const currentValue = this.detectCO2(this.accessory.context.data.carbonDioxideLevel); return currentValue; } /** * Handle requests to get the current value of the 'Carbon Dioxide Level' characteristic */ handleCarbonDioxideLevelGet() { // set this to a valid value for CarbonDioxideLevel const currentValue = this.accessory.context.data.carbonDioxideLevel; return currentValue; } /** * Handle requests to get the current value of the 'Carbon Dioxide Peak Level' characteristic */ handleCarbonDioxidePeakLevelGet() { // set this to a valid value for CarbonDioxidePeakLevel const currentValue = this.accessory.context.data.carbonDioxidePeakLevel; return currentValue; } async updatecharacteristics() { // fetch new data from the Foobot API this.platform.log.info('Updating datapoints for ' + this.accessory.context.device.name); let datapoints; try { const foobot = new foobot_1.Foobot(this.platform.config.apiKey); const uuid = this.accessory.context.device.uuid; let sensorList = 'pm,voc,tmp,hum,allpollu'; if (!this.platform.config.co2off) { sensorList += ',co2'; } datapoints = await foobot.getLastDataPoints(uuid, 0, 0, sensorList); this.setActive(true); } catch (err) { this.platform.log.error(err.message); this.setActive(false); // try again in a few minutes const delay = Math.round(10 * 60 * 1000 * Math.random()); clearTimeout(this.timeout); this.timeout = setTimeout(this.updatecharacteristics.bind(this), delay); this.platform.log.info(`Waiting ${delay}ms to try again`); return; } this.platform.log.debug(datapoints); // push the new values to HomeKit const pmIndex = datapoints.sensors.indexOf('pm'); if (pmIndex >= 0) { // constrain value to characteristic limits let pmValue = datapoints.datapoints[0][pmIndex]; pmValue = (pmValue < 0) ? 0 : pmValue; pmValue = (pmValue > 1000) ? 1000 : pmValue; // update value in the dynamic platform persistent storage this.accessory.context.data.pm25density = pmValue; this.airQualitySensor.updateCharacteristic(this.platform.Characteristic.PM2_5Density, this.accessory.context.data.pm25density); } const tmpIndex = datapoints.sensors.indexOf('tmp'); if (tmpIndex >= 0) { // constrain value to characteristic limits let tmpValue = datapoints.datapoints[0][tmpIndex]; tmpValue = (tmpValue < -270) ? -270 : tmpValue; tmpValue = (tmpValue > 100) ? 100 : tmpValue; // update value in the dynamic platform persistent storage this.accessory.context.data.currentTemperature = tmpValue; this.temperatureSensor.updateCharacteristic(this.platform.Characteristic.CurrentTemperature, this.accessory.context.data.currentTemperature); } const humIndex = datapoints.sensors.indexOf('hum'); if (humIndex >= 0) { // constrain value to characteristic limits let humValue = datapoints.datapoints[0][humIndex]; humValue = (humValue < 0) ? 0 : humValue; humValue = (humValue > 100) ? 100 : humValue; // update value in the dynamic platform persistent storage this.accessory.context.data.currentRelativeHumidity = humValue; this.humiditySensor.updateCharacteristic(this.platform.Characteristic.CurrentRelativeHumidity, this.accessory.context.data.currentRelativeHumidity); } const co2Index = datapoints.sensors.indexOf('co2'); if (co2Index >= 0) { // constrain value to characteristic limits let co2Value = datapoints.datapoints[0][co2Index]; co2Value = (co2Value < 0) ? 0 : co2Value; co2Value = (co2Value > 100000) ? 100000 : co2Value; // update value in the dynamic platform persistent storage this.accessory.context.data.carbonDioxideLevel = co2Value; this.accessory.context.data.carbonDioxidePeakLevel = this.peakCO2(co2Value); this.carbonDioxideSensor.updateCharacteristic(this.platform.Characteristic.CarbonDioxideDetected, this.detectCO2(this.accessory.context.data.carbonDioxideLevel)); this.carbonDioxideSensor.updateCharacteristic(this.platform.Characteristic.CarbonDioxideLevel, this.accessory.context.data.carbonDioxideLevel); this.carbonDioxideSensor.updateCharacteristic(this.platform.Characteristic.CarbonDioxidePeakLevel, this.accessory.context.data.carbonDioxidePeakLevel); } const vocIndex = datapoints.sensors.indexOf('voc'); if (vocIndex >= 0) { // update value in the dynamic platform persistent storage this.accessory.context.data.vocdensity = this.convertVOCppb2ugm3(datapoints.datapoints[0][vocIndex]); this.airQualitySensor.updateCharacteristic(this.platform.Characteristic.VOCDensity, this.accessory.context.data.vocdensity); } const allpolluIndex = datapoints.sensors.indexOf('allpollu'); if (allpolluIndex >= 0) { // update value in the dynamic platform persistent storage this.accessory.context.data.airquality = this.convertAllpollu2AirQuality(datapoints.datapoints[0][allpolluIndex]); this.airQualitySensor.updateCharacteristic(this.platform.Characteristic.AirQuality, this.accessory.context.data.airquality); } const timeIndex = datapoints.sensors.indexOf('time'); if (timeIndex >= 0) { const timeValue = datapoints.datapoints[0][timeIndex]; // update value in the dynamic platform persistent storage this.accessory.context.data.time = timeValue; } this.platform.log.debug('Finished updating datapoints for ' + this.accessory.context.device.name); // setup next call to the foobot API // alternate between 5 minute delays and 10 minute delays if (this.delay > 7.5 * 60 * 1000) { this.delay = 5 * 60 * 1000; } else { this.delay = 10 * 60 * 1000; } // Foobot updates data every five minutes // subtract the time delta from the delay // to try to get our calls aligned with theirs let delta = Date.now() - (this.accessory.context.data.time * 1000); this.platform.log.debug(`Delta between now and the time in the data: ${this.prettyPrintDuration(delta)}`); if (delta > 5 * 60 * 1000) { // unexpected, the delta should not exceed 5 minutes this.platform.log.warn(`Time delta of ${this.prettyPrintDuration(delta)} exceeds 5 minutes.`); delta = 0; } if (delta < (5 / 2 * 60 * 1000)) { // delta is small, so let us try to hit the next update this.delay = this.delay - delta; } else { // delta is larger, so add some time to wait for the update after next this.delay = this.delay + (5 * 60 * 1000) - delta; } // make sure multiple devices do not call the api at the same time // by adding an offset to the delay based on the index of each device this.delay = this.delay - (this.accessory.context.device.offset * 1000); this.platform.log.debug(`Waiting ${this.prettyPrintDuration(this.delay)} for next call...`); // clear any remaining timeouts and start a new one clearTimeout(this.timeout); this.timeout = setTimeout(this.updatecharacteristics.bind(this), this.delay); } prettyPrintDuration(milliseconds) { let result = ''; let remainder = milliseconds; if (remainder > 365.2525 * 24 * 60 * 60 * 1000) { const years = Math.floor(remainder / 1000 / 60 / 60 / 24 / 365.2525); remainder = remainder - (years * 365.2525 * 24 * 60 * 60 * 1000); result = `${years}y `; } if (remainder > 7 * 24 * 60 * 60 * 1000) { const weeks = Math.floor(remainder / 1000 / 60 / 60 / 24 / 7); remainder = remainder - (weeks * 7 * 24 * 60 * 60 * 1000); result = `${weeks}w `; } if (remainder > 24 * 60 * 60 * 1000) { const days = Math.floor(remainder / 1000 / 60 / 60 / 24); remainder = remainder - (days * 24 * 60 * 60 * 1000); result = `${days}d `; } if (remainder > 60 * 60 * 1000) { const hours = Math.floor(remainder / 1000 / 60 / 60); remainder = remainder - (hours * 60 * 60 * 1000); result = `${hours}h `; } if (remainder > 60 * 1000) { const minutes = Math.floor(remainder / 1000 / 60); remainder = remainder - (minutes * 60 * 1000); result = `${minutes}m `; } if (remainder > 1000) { const seconds = Math.floor(remainder / 1000); remainder = remainder - (seconds * 1000); result += `${seconds}s `; } result += `${remainder}ms`; return result; } /** * convert CO2 ppm values to normal/abnormal values */ detectCO2(value) { const CarbonDioxideDetected = this.platform.Characteristic.CarbonDioxideDetected; const limit = this.platform.config.co2limit; if (value > limit) { return CarbonDioxideDetected.CO2_LEVELS_ABNORMAL; } else { return CarbonDioxideDetected.CO2_LEVELS_NORMAL; } } /** * convert CO2 ppm values to normal/abnormal values */ peakCO2(value) { let peakLevel = this.accessory.context.data.carbonDioxidePeakLevel; if (value > peakLevel) { peakLevel = value; } return peakLevel; } /** * convert parts-per-billion to micrograms per cubic meter * for VOC density * Foobot is very sensitive to CO * therefore convert VOCs in ppb to ug/m3 based on mostly CO * https://uk-air.defra.gov.uk/assets/documents/reports/cat06/0502160851_Conversion_Factors_Between_ppb_and.pdf */ convertVOCppb2ugm3(ppb) { const CO_AT_20C_AND_1013MB = 1.1642; let ugm3 = ppb * CO_AT_20C_AND_1013MB; // Homekit Characteristic Limited to 1000 micrograms per cubic meter if (ugm3 > 1000) { ugm3 = 1000; } return ugm3; } /** * convert Foobot `allpullu` values to HomeKit AirQuality values * problem is Foobot shows six air quality levels * and HomeKit has five air quality levels * Fooboot | Homekit * All Blue | EXCELLENT * 2/3 Blue | GOOD * 1/3 Blue | FAIR * 1/3 Orange | INFERIOR (Homekit alerts) * 2/3 Orange | ??? * All Orange | POOR * values from gchokov * 0 - 25 EXCELLENT * 26 - 50 GOOD * 51 - 70 FAIR * 71 - 90 INFERIOR * 91 - 100 POOR * values from keithws (bellcurve) * 0 - 10 EXCELLENT (10%) * 10 - 30 GOOD (20%) * 30 - 70 FAIR (40%) * 70 - 90 INFERIOR (20%) * 90 - 100+ POOR (10%) * values from keithws (aligned inferior to 1/3 orange) * 0 - 17 EXCELLENT * 17 - 33 GOOD * 33 - 50 FAIR * 50 - 67 INFERIOR * 67 - 83 (missing) * 83 - 100+ POOR */ convertAllpollu2AirQuality(allpollu) { let result = this.platform.Characteristic.AirQuality.UNKNOWN; if (allpollu && !isNaN(allpollu)) { if (allpollu <= (50 / 3)) { result = this.platform.Characteristic.AirQuality.EXCELLENT; } else if (allpollu <= (50 / 3 * 2)) { result = this.platform.Characteristic.AirQuality.GOOD; } else if (allpollu <= 50) { result = this.platform.Characteristic.AirQuality.FAIR; } else if (allpollu <= (50 / 3 * 4)) { result = this.platform.Characteristic.AirQuality.INFERIOR; } else if (allpollu <= (50 / 3 * 5)) { // deliberate reuse of inferior result = this.platform.Characteristic.AirQuality.INFERIOR; } else { result = this.platform.Characteristic.AirQuality.POOR; } } return result; } setActive(isActive) { if (typeof isActive !== 'boolean') { isActive = isActive || true; } this.active = isActive; // set all services to active/inactive this.airQualitySensor.updateCharacteristic(this.platform.Characteristic.StatusActive, isActive); this.temperatureSensor.updateCharacteristic(this.platform.Characteristic.StatusActive, isActive); this.humiditySensor.updateCharacteristic(this.platform.Characteristic.StatusActive, isActive); if (!this.platform.config.co2off) { this.carbonDioxideSensor.updateCharacteristic(this.platform.Characteristic.StatusActive, isActive); } } } exports.FoobotPlatformAccessory = FoobotPlatformAccessory; //# sourceMappingURL=platformAccessory.js.map