From 42007d03d49f94d26591ee3ae7dab549abd8baff Mon Sep 17 00:00:00 2001 From: Evgeny Date: Fri, 12 Jun 2020 04:14:54 +0200 Subject: [PATCH] AQI calculator for HM3301 (#1011) * HM3301 AQI calculator * remove logs * fixed after lint * fixed after lint * fixed after lint * check NP for AQI sensor * validation for AQI sensor --- .../hm3301/abstract_aqi_calculator.h | 14 +++++ esphome/components/hm3301/aqi_calculator.cpp | 46 ++++++++++++++++ .../hm3301/aqi_calculator_factory.h | 30 +++++++++++ esphome/components/hm3301/caqi_calculator.cpp | 52 +++++++++++++++++++ esphome/components/hm3301/hm3301.cpp | 36 ++++++++++--- esphome/components/hm3301/hm3301.h | 8 +++ esphome/components/hm3301/sensor.py | 31 ++++++++++- tests/test1.yaml | 3 ++ tests/test3.yaml | 3 ++ 9 files changed, 214 insertions(+), 9 deletions(-) create mode 100644 esphome/components/hm3301/abstract_aqi_calculator.h create mode 100644 esphome/components/hm3301/aqi_calculator.cpp create mode 100644 esphome/components/hm3301/aqi_calculator_factory.h create mode 100644 esphome/components/hm3301/caqi_calculator.cpp diff --git a/esphome/components/hm3301/abstract_aqi_calculator.h b/esphome/components/hm3301/abstract_aqi_calculator.h new file mode 100644 index 0000000000..f160a91148 --- /dev/null +++ b/esphome/components/hm3301/abstract_aqi_calculator.h @@ -0,0 +1,14 @@ +#pragma once + +#include "Arduino.h" + +namespace esphome { +namespace hm3301 { + +class AbstractAQICalculator { + public: + virtual uint8_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) = 0; +}; + +} // namespace hm3301 +} // namespace esphome diff --git a/esphome/components/hm3301/aqi_calculator.cpp b/esphome/components/hm3301/aqi_calculator.cpp new file mode 100644 index 0000000000..6b70c5d4fd --- /dev/null +++ b/esphome/components/hm3301/aqi_calculator.cpp @@ -0,0 +1,46 @@ +#include "abstract_aqi_calculator.h" + +namespace esphome { +namespace hm3301 { + +class AQICalculator : public AbstractAQICalculator { + public: + uint8_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) override { + int pm2_5_index = calculate_index_(pm2_5_value, pm2_5_calculation_grid_); + int pm10_0_index = calculate_index_(pm10_0_value, pm10_0_calculation_grid_); + + return (pm2_5_index < pm10_0_index) ? pm10_0_index : pm2_5_index; + } + + protected: + static const int AMOUNT_OF_LEVELS = 6; + + int index_grid_[AMOUNT_OF_LEVELS][2] = {{0, 51}, {51, 100}, {101, 150}, {151, 200}, {201, 300}, {301, 500}}; + + int pm2_5_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 12}, {13, 45}, {36, 55}, {56, 150}, {151, 250}, {251, 500}}; + + int pm10_0_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 54}, {55, 154}, {155, 254}, + {255, 354}, {355, 424}, {425, 604}}; + + int calculate_index_(uint16_t value, int array[AMOUNT_OF_LEVELS][2]) { + int grid_index = get_grid_index_(value, array); + int aqi_lo = index_grid_[grid_index][0]; + int aqi_hi = index_grid_[grid_index][1]; + int conc_lo = array[grid_index][0]; + int conc_hi = array[grid_index][1]; + + return ((aqi_hi - aqi_lo) / (conc_hi - conc_lo)) * (value - conc_lo) + aqi_lo; + } + + int get_grid_index_(uint16_t value, int array[AMOUNT_OF_LEVELS][2]) { + for (int i = 0; i < AMOUNT_OF_LEVELS - 1; i++) { + if (value >= array[i][0] && value <= array[i][1]) { + return i; + } + } + return -1; + } +}; + +} // namespace hm3301 +} // namespace esphome diff --git a/esphome/components/hm3301/aqi_calculator_factory.h b/esphome/components/hm3301/aqi_calculator_factory.h new file mode 100644 index 0000000000..483a158822 --- /dev/null +++ b/esphome/components/hm3301/aqi_calculator_factory.h @@ -0,0 +1,30 @@ +#pragma once + +#include "Arduino.h" +#include "caqi_calculator.cpp" +#include "aqi_calculator.cpp" + +namespace esphome { +namespace hm3301 { + +enum AQICalculatorType { CAQI_TYPE = 0, AQI_TYPE = 1 }; + +class AQICalculatorFactory { + public: + AbstractAQICalculator *get_calculator(AQICalculatorType type) { + if (type == 0) { + return caqi_calculator_; + } else if (type == 1) { + return aqi_calculator_; + } + + return nullptr; + } + + protected: + CAQICalculator *caqi_calculator_ = new CAQICalculator(); + AQICalculator *aqi_calculator_ = new AQICalculator(); +}; + +} // namespace hm3301 +} // namespace esphome diff --git a/esphome/components/hm3301/caqi_calculator.cpp b/esphome/components/hm3301/caqi_calculator.cpp new file mode 100644 index 0000000000..511179cb71 --- /dev/null +++ b/esphome/components/hm3301/caqi_calculator.cpp @@ -0,0 +1,52 @@ +#include "esphome/core/log.h" +#include "abstract_aqi_calculator.h" + +namespace esphome { +namespace hm3301 { + +class CAQICalculator : public AbstractAQICalculator { + public: + uint8_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) override { + int pm2_5_index = calculate_index_(pm2_5_value, pm2_5_calculation_grid_); + int pm10_0_index = calculate_index_(pm10_0_value, pm10_0_calculation_grid_); + + return (pm2_5_index < pm10_0_index) ? pm10_0_index : pm2_5_index; + } + + protected: + static const int AMOUNT_OF_LEVELS = 5; + + int index_grid_[AMOUNT_OF_LEVELS][2] = {{0, 25}, {26, 50}, {51, 75}, {76, 100}, {101, 400}}; + + int pm2_5_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 15}, {16, 30}, {31, 55}, {56, 110}, {111, 400}}; + + int pm10_0_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 25}, {26, 50}, {51, 90}, {91, 180}, {181, 400}}; + + int calculate_index_(uint16_t value, int array[AMOUNT_OF_LEVELS][2]) { + int grid_index = get_grid_index_(value, array); + if (grid_index == -1) { + return -1; + } + + int aqi_lo = index_grid_[grid_index][0]; + int aqi_hi = index_grid_[grid_index][1]; + int conc_lo = array[grid_index][0]; + int conc_hi = array[grid_index][1]; + + int aqi = ((aqi_hi - aqi_lo) / (conc_hi - conc_lo)) * (value - conc_lo) + aqi_lo; + + return aqi; + } + + int get_grid_index_(uint16_t value, int array[AMOUNT_OF_LEVELS][2]) { + for (int i = 0; i < AMOUNT_OF_LEVELS; i++) { + if (value >= array[i][0] && value <= array[i][1]) { + return i; + } + } + return -1; + } +}; + +} // namespace hm3301 +} // namespace esphome diff --git a/esphome/components/hm3301/hm3301.cpp b/esphome/components/hm3301/hm3301.cpp index 6456ee354a..cbce714012 100644 --- a/esphome/components/hm3301/hm3301.cpp +++ b/esphome/components/hm3301/hm3301.cpp @@ -1,5 +1,5 @@ -#include "hm3301.h" #include "esphome/core/log.h" +#include "hm3301.h" namespace esphome { namespace hm3301 { @@ -30,6 +30,7 @@ void HM3301Component::dump_config() { LOG_SENSOR(" ", "PM1.0", this->pm_1_0_sensor_); LOG_SENSOR(" ", "PM2.5", this->pm_2_5_sensor_); LOG_SENSOR(" ", "PM10.0", this->pm_10_0_sensor_); + LOG_SENSOR(" ", "AQI", this->aqi_sensor_); } float HM3301Component::get_setup_priority() const { return setup_priority::DATA; } @@ -47,17 +48,38 @@ void HM3301Component::update() { return; } + int16_t pm_1_0_value = -1; if (this->pm_1_0_sensor_ != nullptr) { - uint16_t value = get_sensor_value_(data_buffer_, PM_1_0_VALUE_INDEX); - this->pm_1_0_sensor_->publish_state(value); + pm_1_0_value = get_sensor_value_(data_buffer_, PM_1_0_VALUE_INDEX); } + + int16_t pm_2_5_value = -1; if (this->pm_2_5_sensor_ != nullptr) { - uint16_t value = get_sensor_value_(data_buffer_, PM_2_5_VALUE_INDEX); - this->pm_2_5_sensor_->publish_state(value); + pm_2_5_value = get_sensor_value_(data_buffer_, PM_2_5_VALUE_INDEX); } + + int16_t pm_10_0_value = -1; if (this->pm_10_0_sensor_ != nullptr) { - uint16_t value = get_sensor_value_(data_buffer_, PM_10_0_VALUE_INDEX); - this->pm_10_0_sensor_->publish_state(value); + pm_10_0_value = get_sensor_value_(data_buffer_, PM_10_0_VALUE_INDEX); + } + + int8_t aqi_value = -1; + if (this->aqi_sensor_ != nullptr && pm_2_5_value != -1 && pm_10_0_value != -1) { + AbstractAQICalculator *calculator = this->aqi_calculator_factory_.get_calculator(this->aqi_calc_type_); + aqi_value = calculator->get_aqi(pm_2_5_value, pm_10_0_value); + } + + if (pm_1_0_value != -1) { + this->pm_1_0_sensor_->publish_state(pm_1_0_value); + } + if (pm_2_5_value != -1) { + this->pm_2_5_sensor_->publish_state(pm_2_5_value); + } + if (pm_10_0_value != -1) { + this->pm_10_0_sensor_->publish_state(pm_10_0_value); + } + if (aqi_value != -1) { + this->aqi_sensor_->publish_state(aqi_value); } this->status_clear_warning(); diff --git a/esphome/components/hm3301/hm3301.h b/esphome/components/hm3301/hm3301.h index 0fbb32612e..5594f1719c 100644 --- a/esphome/components/hm3301/hm3301.h +++ b/esphome/components/hm3301/hm3301.h @@ -3,6 +3,7 @@ #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" +#include "aqi_calculator_factory.h" #include @@ -16,6 +17,9 @@ class HM3301Component : public PollingComponent, public i2c::I2CDevice { void set_pm_1_0_sensor(sensor::Sensor *pm_1_0_sensor) { pm_1_0_sensor_ = pm_1_0_sensor; } void set_pm_2_5_sensor(sensor::Sensor *pm_2_5_sensor) { pm_2_5_sensor_ = pm_2_5_sensor; } void set_pm_10_0_sensor(sensor::Sensor *pm_10_0_sensor) { pm_10_0_sensor_ = pm_10_0_sensor; } + void set_aqi_sensor(sensor::Sensor *aqi_sensor) { aqi_sensor_ = aqi_sensor; } + + void set_aqi_calculation_type(AQICalculatorType aqi_calc_type) { aqi_calc_type_ = aqi_calc_type; } void setup() override; void dump_config() override; @@ -32,6 +36,10 @@ class HM3301Component : public PollingComponent, public i2c::I2CDevice { sensor::Sensor *pm_1_0_sensor_{nullptr}; sensor::Sensor *pm_2_5_sensor_{nullptr}; sensor::Sensor *pm_10_0_sensor_{nullptr}; + sensor::Sensor *aqi_sensor_{nullptr}; + + AQICalculatorType aqi_calc_type_; + AQICalculatorFactory aqi_calculator_factory_ = AQICalculatorFactory(); bool read_sensor_value_(uint8_t *); bool validate_checksum_(const uint8_t *); diff --git a/esphome/components/hm3301/sensor.py b/esphome/components/hm3301/sensor.py index 718d0a20bb..ef7669bc03 100644 --- a/esphome/components/hm3301/sensor.py +++ b/esphome/components/hm3301/sensor.py @@ -8,6 +8,25 @@ DEPENDENCIES = ['i2c'] hm3301_ns = cg.esphome_ns.namespace('hm3301') HM3301Component = hm3301_ns.class_('HM3301Component', cg.PollingComponent, i2c.I2CDevice) +AQICalculatorType = hm3301_ns.enum('AQICalculatorType') + +CONF_AQI = 'aqi' +CONF_CALCULATION_TYPE = 'calculation_type' +UNIT_INDEX = 'index' + +AQI_CALCULATION_TYPE = { + 'CAQI': AQICalculatorType.CAQI_TYPE, + 'AQI': AQICalculatorType.AQI_TYPE +} + + +def validate(config): + if CONF_AQI in config and CONF_PM_2_5 not in config: + raise cv.Invalid("AQI sensor requires PM 2.5") + if CONF_AQI in config and CONF_PM_10_0 not in config: + raise cv.Invalid("AQI sensor requires PM 10 sensors") + return config + CONFIG_SCHEMA = cv.All(cv.Schema({ cv.GenerateID(): cv.declare_id(HM3301Component), @@ -18,8 +37,11 @@ CONFIG_SCHEMA = cv.All(cv.Schema({ sensor.sensor_schema(UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_CHEMICAL_WEAPON, 0), cv.Optional(CONF_PM_10_0): sensor.sensor_schema(UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_CHEMICAL_WEAPON, 0), - -}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x40))) + cv.Optional(CONF_AQI): + sensor.sensor_schema(UNIT_INDEX, ICON_CHEMICAL_WEAPON, 0).extend({ + cv.Required(CONF_CALCULATION_TYPE): cv.enum(AQI_CALCULATION_TYPE, upper=True), + }) +}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x40)), validate) def to_code(config): @@ -39,5 +61,10 @@ def to_code(config): sens = yield sensor.new_sensor(config[CONF_PM_10_0]) cg.add(var.set_pm_10_0_sensor(sens)) + if CONF_AQI in config: + sens = yield sensor.new_sensor(config[CONF_AQI]) + cg.add(var.set_aqi_sensor(sens)) + cg.add(var.set_aqi_calculation_type(config[CONF_AQI][CONF_CALCULATION_TYPE])) + # https://platformio.org/lib/show/6306/Grove%20-%20Laser%20PM2.5%20Sensor%20HM3301 cg.add_library('6306', '1.0.3') diff --git a/tests/test1.yaml b/tests/test1.yaml index d9563a470a..5f8bdd1111 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -738,6 +738,9 @@ sensor: name: "PM2.5" pm_10_0: name: "PM10.0" + aqi: + name: "AQI" + calculation_type: "CAQI" esp32_touch: setup_mode: False diff --git a/tests/test3.yaml b/tests/test3.yaml index 62658c3c4b..06bfce97a2 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -361,6 +361,9 @@ sensor: name: "PM2.5" pm_10_0: name: "PM10.0" + aqi: + name: "AQI" + calculation_type: "AQI" - platform: pmsx003 type: PMSX003 pm_1_0: