diff --git a/esphome/components/pm2005/__init__.py b/esphome/components/pm2005/__init__.py new file mode 100644 index 0000000000..3716dd7b5e --- /dev/null +++ b/esphome/components/pm2005/__init__.py @@ -0,0 +1 @@ +"""PM2005/2105 component for ESPHome.""" diff --git a/esphome/components/pm2005/pm2005.cpp b/esphome/components/pm2005/pm2005.cpp new file mode 100644 index 0000000000..62398cc95a --- /dev/null +++ b/esphome/components/pm2005/pm2005.cpp @@ -0,0 +1,94 @@ +#include "esphome/core/log.h" +#include "pm2005.h" + +namespace esphome { +namespace pm2005 { + +static const char *const TAG = "pm2005"; + +#ifdef TYPE_2005 +static const uint8_t SITUATION_VALUE_INDEX = 3; +static const uint8_t PM_1_0_VALUE_INDEX = 4; +static const uint8_t PM_2_5_VALUE_INDEX = 6; +static const uint8_t PM_10_0_VALUE_INDEX = 8; +static const uint8_t MEASURING_VALUE_INDEX = 10; +#else +static const uint8_t SITUATION_VALUE_INDEX = 2; +static const uint8_t PM_1_0_VALUE_INDEX = 3; +static const uint8_t PM_2_5_VALUE_INDEX = 5; +static const uint8_t PM_10_0_VALUE_INDEX = 7; +static const uint8_t MEASURING_VALUE_INDEX = 9; +#endif + +void PM2005Component::update() { + if (this->read(data_buffer_, 12) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Read result failed"); + this->status_set_warning(); + return; + } + + if (sensor_situation_ != data_buffer_[SITUATION_VALUE_INDEX]) { + sensor_situation_ = data_buffer_[SITUATION_VALUE_INDEX]; + if (sensor_situation_ == 1) + ESP_LOGD(TAG, "Sensor situation: Close."); + else if (sensor_situation_ == 2) { + ESP_LOGD(TAG, "Sensor situation: Malfunction."); + this->status_set_warning(); + } else if (sensor_situation_ == 3) + ESP_LOGD(TAG, "Sensor situation: Under detecting."); + else if (sensor_situation_ == 0x80) { + ESP_LOGD(TAG, "Sensor situation: Detecting completed."); + + if (this->pm_1_0_sensor_ != nullptr) { + int16_t pm1 = get_sensor_value_(data_buffer_, PM_1_0_VALUE_INDEX); + ESP_LOGD(TAG, "PM1.0: %d", pm1); + this->pm_1_0_sensor_->publish_state(pm1); + } + + if (this->pm_2_5_sensor_ != nullptr) { + int16_t pm25 = get_sensor_value_(data_buffer_, PM_2_5_VALUE_INDEX); + ESP_LOGD(TAG, "PM2.5: %d", pm25); + this->pm_2_5_sensor_->publish_state(pm25); + } + + if (this->pm_10_0_sensor_ != nullptr) { + int16_t pm10 = get_sensor_value_(data_buffer_, PM_10_0_VALUE_INDEX); + ESP_LOGD(TAG, "PM10: %d", pm10); + this->pm_10_0_sensor_->publish_state(pm10); + } + + uint16_t sensor_measuring_mode = get_sensor_value_(data_buffer_, MEASURING_VALUE_INDEX); + ; + if (sensor_measuring_mode == 2) + ESP_LOGD(TAG, "The measuring mode of sensor: Single measuring mode."); + else if (sensor_measuring_mode == 3) + ESP_LOGD(TAG, "The measuring mode of sensor: Continuous measuring mode."); + else if (sensor_measuring_mode == 5) + ESP_LOGD(TAG, "The measuring mode of sensor: Dynamic measuring mode."); + + this->status_clear_warning(); + } + } +} + +uint16_t PM2005Component::get_sensor_value_(const uint8_t *data, uint8_t i) { + return data_buffer_[i] * 0x100 + data_buffer_[i + 1]; +} + +void PM2005Component::dump_config() { + ESP_LOGCONFIG(TAG, "PM2005:"); + +#ifdef TYPE_2005 + ESP_LOGCONFIG(TAG, "Type: PM2005"); +#else + ESP_LOGCONFIG(TAG, "Type: PM2105"); +#endif + + LOG_I2C_DEVICE(this); + LOG_SENSOR(" ", "PM1.0", this->pm_1_0_sensor_); + LOG_SENSOR(" ", "PM2.5", this->pm_2_5_sensor_); + LOG_SENSOR(" ", "PM10 ", this->pm_10_0_sensor_); +} + +} // namespace pm2005 +} // namespace esphome diff --git a/esphome/components/pm2005/pm2005.h b/esphome/components/pm2005/pm2005.h new file mode 100644 index 0000000000..68dd46e9dc --- /dev/null +++ b/esphome/components/pm2005/pm2005.h @@ -0,0 +1,35 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace pm2005 { + +class PM2005Component : public PollingComponent, public i2c::I2CDevice { + public: + float get_setup_priority() const override { return esphome::setup_priority::DATA; } + + PM2005Component() = default; + + 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 dump_config() override; + void update() override; + + protected: + uint8_t sensor_situation_ = 0; + uint8_t data_buffer_[12]; + + sensor::Sensor *pm_1_0_sensor_{nullptr}; + sensor::Sensor *pm_2_5_sensor_{nullptr}; + sensor::Sensor *pm_10_0_sensor_{nullptr}; + + uint16_t get_sensor_value_(const uint8_t *data, uint8_t i); +}; + +} // namespace pm2005 +} // namespace esphome diff --git a/esphome/components/pm2005/sensor.py b/esphome/components/pm2005/sensor.py new file mode 100644 index 0000000000..2b8f24f37e --- /dev/null +++ b/esphome/components/pm2005/sensor.py @@ -0,0 +1,88 @@ +"""PM2005/2105 Sensor component for ESPHome.""" + +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_ID, + CONF_PM_1_0, + CONF_PM_2_5, + CONF_PM_10_0, + CONF_TYPE, + DEVICE_CLASS_PM1, + DEVICE_CLASS_PM10, + DEVICE_CLASS_PM25, + ICON_CHEMICAL_WEAPON, + STATE_CLASS_MEASUREMENT, + UNIT_MICROGRAMS_PER_CUBIC_METER, +) + +DEPENDENCIES = ["i2c"] +CODEOWNERS = ["@andrewjswan"] + +pm2005_ns = cg.esphome_ns.namespace("pm2005") +PM2005Component = pm2005_ns.class_("PM2005Component", cg.PollingComponent, i2c.I2CDevice) + +TYPE_2005 = "PM2005" +TYPE_2105 = "PM2105" +SENSOR_TYPE = [ + TYPE_2005, + TYPE_2105, +] + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(PM2005Component), + cv.Optional(CONF_TYPE, default=TYPE_2005): cv.one_of(*SENSOR_TYPE, upper=True), + cv.Optional(CONF_PM_1_0): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=0, + device_class=DEVICE_CLASS_PM1, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PM_2_5): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=0, + device_class=DEVICE_CLASS_PM25, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PM_10_0): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=0, + device_class=DEVICE_CLASS_PM10, + state_class=STATE_CLASS_MEASUREMENT, + ), + }, + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x28)), +) + + +async def to_code(config) -> None: + """Code generation entry point.""" + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + if config[CONF_TYPE] == TYPE_2005: + cg.add_define("TYPE_2005") + else: + cg.add_define("TYPE_2105") + + if CONF_PM_1_0 in config: + sens = await sensor.new_sensor(config[CONF_PM_1_0]) + cg.add(var.set_pm_1_0_sensor(sens)) + + if CONF_PM_2_5 in config: + sens = await sensor.new_sensor(config[CONF_PM_2_5]) + cg.add(var.set_pm_2_5_sensor(sens)) + + if CONF_PM_10_0 in config: + sens = await sensor.new_sensor(config[CONF_PM_10_0]) + cg.add(var.set_pm_10_0_sensor(sens))