mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 07:03:55 +00:00 
			
		
		
		
	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:
		
							
								
								
									
										14
									
								
								esphome/components/hm3301/abstract_aqi_calculator.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								esphome/components/hm3301/abstract_aqi_calculator.h
									
									
									
									
									
										Normal 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 | ||||||
							
								
								
									
										46
									
								
								esphome/components/hm3301/aqi_calculator.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								esphome/components/hm3301/aqi_calculator.cpp
									
									
									
									
									
										Normal 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 | ||||||
							
								
								
									
										30
									
								
								esphome/components/hm3301/aqi_calculator_factory.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								esphome/components/hm3301/aqi_calculator_factory.h
									
									
									
									
									
										Normal 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 | ||||||
							
								
								
									
										52
									
								
								esphome/components/hm3301/caqi_calculator.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								esphome/components/hm3301/caqi_calculator.cpp
									
									
									
									
									
										Normal 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 | ||||||
| @@ -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(); | ||||||
|   | |||||||
| @@ -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 *); | ||||||
|   | |||||||
| @@ -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') | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -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: | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user