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
This commit is contained in:
Evgeny 2020-06-12 04:14:54 +02:00 committed by GitHub
parent 821c1a8bbd
commit 42007d03d4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 214 additions and 9 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -1,5 +1,5 @@
#include "hm3301.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "hm3301.h"
namespace esphome { namespace esphome {
namespace hm3301 { namespace hm3301 {
@ -30,6 +30,7 @@ void HM3301Component::dump_config() {
LOG_SENSOR(" ", "PM1.0", this->pm_1_0_sensor_); LOG_SENSOR(" ", "PM1.0", this->pm_1_0_sensor_);
LOG_SENSOR(" ", "PM2.5", this->pm_2_5_sensor_); LOG_SENSOR(" ", "PM2.5", this->pm_2_5_sensor_);
LOG_SENSOR(" ", "PM10.0", this->pm_10_0_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; } float HM3301Component::get_setup_priority() const { return setup_priority::DATA; }
@ -47,17 +48,38 @@ void HM3301Component::update() {
return; return;
} }
int16_t pm_1_0_value = -1;
if (this->pm_1_0_sensor_ != nullptr) { if (this->pm_1_0_sensor_ != nullptr) {
uint16_t value = get_sensor_value_(data_buffer_, PM_1_0_VALUE_INDEX); pm_1_0_value = get_sensor_value_(data_buffer_, PM_1_0_VALUE_INDEX);
this->pm_1_0_sensor_->publish_state(value);
} }
int16_t pm_2_5_value = -1;
if (this->pm_2_5_sensor_ != nullptr) { if (this->pm_2_5_sensor_ != nullptr) {
uint16_t value = get_sensor_value_(data_buffer_, PM_2_5_VALUE_INDEX); pm_2_5_value = get_sensor_value_(data_buffer_, PM_2_5_VALUE_INDEX);
this->pm_2_5_sensor_->publish_state(value);
} }
int16_t pm_10_0_value = -1;
if (this->pm_10_0_sensor_ != nullptr) { if (this->pm_10_0_sensor_ != nullptr) {
uint16_t value = get_sensor_value_(data_buffer_, PM_10_0_VALUE_INDEX); pm_10_0_value = get_sensor_value_(data_buffer_, PM_10_0_VALUE_INDEX);
this->pm_10_0_sensor_->publish_state(value); }
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(); this->status_clear_warning();

View file

@ -3,6 +3,7 @@
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h" #include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h" #include "esphome/components/i2c/i2c.h"
#include "aqi_calculator_factory.h"
#include <Seeed_HM330X.h> #include <Seeed_HM330X.h>
@ -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_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_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_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 setup() override;
void dump_config() 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_1_0_sensor_{nullptr};
sensor::Sensor *pm_2_5_sensor_{nullptr}; sensor::Sensor *pm_2_5_sensor_{nullptr};
sensor::Sensor *pm_10_0_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 read_sensor_value_(uint8_t *);
bool validate_checksum_(const uint8_t *); bool validate_checksum_(const uint8_t *);

View file

@ -8,6 +8,25 @@ DEPENDENCIES = ['i2c']
hm3301_ns = cg.esphome_ns.namespace('hm3301') hm3301_ns = cg.esphome_ns.namespace('hm3301')
HM3301Component = hm3301_ns.class_('HM3301Component', cg.PollingComponent, i2c.I2CDevice) 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({ CONFIG_SCHEMA = cv.All(cv.Schema({
cv.GenerateID(): cv.declare_id(HM3301Component), 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), sensor.sensor_schema(UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_CHEMICAL_WEAPON, 0),
cv.Optional(CONF_PM_10_0): cv.Optional(CONF_PM_10_0):
sensor.sensor_schema(UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_CHEMICAL_WEAPON, 0), sensor.sensor_schema(UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_CHEMICAL_WEAPON, 0),
cv.Optional(CONF_AQI):
}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x40))) 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): def to_code(config):
@ -39,5 +61,10 @@ def to_code(config):
sens = yield sensor.new_sensor(config[CONF_PM_10_0]) sens = yield sensor.new_sensor(config[CONF_PM_10_0])
cg.add(var.set_pm_10_0_sensor(sens)) 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 # https://platformio.org/lib/show/6306/Grove%20-%20Laser%20PM2.5%20Sensor%20HM3301
cg.add_library('6306', '1.0.3') cg.add_library('6306', '1.0.3')

View file

@ -738,6 +738,9 @@ sensor:
name: "PM2.5" name: "PM2.5"
pm_10_0: pm_10_0:
name: "PM10.0" name: "PM10.0"
aqi:
name: "AQI"
calculation_type: "CAQI"
esp32_touch: esp32_touch:
setup_mode: False setup_mode: False

View file

@ -361,6 +361,9 @@ sensor:
name: "PM2.5" name: "PM2.5"
pm_10_0: pm_10_0:
name: "PM10.0" name: "PM10.0"
aqi:
name: "AQI"
calculation_type: "AQI"
- platform: pmsx003 - platform: pmsx003
type: PMSX003 type: PMSX003
pm_1_0: pm_1_0: