diff --git a/CODEOWNERS b/CODEOWNERS index f6f7ac6f9c..433820d624 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -324,6 +324,7 @@ esphome/components/pcf8563/* @KoenBreeman esphome/components/pid/* @OttoWinter esphome/components/pipsolar/* @andreashergert1984 esphome/components/pm1006/* @habbie +esphome/components/pm2005/* @andrewjswan esphome/components/pmsa003i/* @sjtrny esphome/components/pmwcs3/* @SeByDocKy esphome/components/pn532/* @OttoWinter @jesserockz 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..38847210fd --- /dev/null +++ b/esphome/components/pm2005/pm2005.cpp @@ -0,0 +1,123 @@ +#include "esphome/core/log.h" +#include "pm2005.h" + +namespace esphome { +namespace pm2005 { + +static const char *const TAG = "pm2005"; + +// Converts a sensor situation to a human readable string +static const LogString *pm2005_get_situation_string(int status) { + switch (status) { + case 1: + return LOG_STR("Close"); + case 2: + return LOG_STR("Malfunction"); + case 3: + return LOG_STR("Under detecting"); + case 0x80: + return LOG_STR("Detecting completed"); + default: + return LOG_STR("Invalid"); + } +} + +// Converts a sensor measuring mode to a human readable string +static const LogString *pm2005_get_measuring_mode_string(int status) { + switch (status) { + case 2: + return LOG_STR("Single"); + case 3: + return LOG_STR("Continuous"); + case 5: + return LOG_STR("Dynamic"); + default: + return LOG_STR("Timing"); + } +} + +static inline uint16_t get_sensor_value(const uint8_t *data, uint8_t i) { return data[i] * 0x100 + data[i + 1]; } + +void PM2005Component::setup() { + if (this->sensor_type_ == PM2005) { + ESP_LOGCONFIG(TAG, "Setting up PM2005..."); + + this->situation_value_index_ = 3; + this->pm_1_0_value_index_ = 4; + this->pm_2_5_value_index_ = 6; + this->pm_10_0_value_index_ = 8; + this->measuring_value_index_ = 10; + } else { + ESP_LOGCONFIG(TAG, "Setting up PM2105..."); + + this->situation_value_index_ = 2; + this->pm_1_0_value_index_ = 3; + this->pm_2_5_value_index_ = 5; + this->pm_10_0_value_index_ = 7; + this->measuring_value_index_ = 9; + } + + if (this->read(this->data_buffer_, 12) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Communication failed!"); + this->mark_failed(); + return; + } +} + +void PM2005Component::update() { + if (this->read(this->data_buffer_, 12) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Read result failed."); + this->status_set_warning(); + return; + } + + if (this->sensor_situation_ == this->data_buffer_[this->situation_value_index_]) { + return; + } + + this->sensor_situation_ = this->data_buffer_[this->situation_value_index_]; + ESP_LOGD(TAG, "Sensor situation: %s.", LOG_STR_ARG(pm2005_get_situation_string(this->sensor_situation_))); + if (this->sensor_situation_ == 2) { + this->status_set_warning(); + return; + } + if (this->sensor_situation_ != 0x80) { + return; + } + + uint16_t pm1 = get_sensor_value(this->data_buffer_, this->pm_1_0_value_index_); + uint16_t pm25 = get_sensor_value(this->data_buffer_, this->pm_2_5_value_index_); + uint16_t pm10 = get_sensor_value(this->data_buffer_, this->pm_10_0_value_index_); + uint16_t sensor_measuring_mode = get_sensor_value(this->data_buffer_, this->measuring_value_index_); + ESP_LOGD(TAG, "PM1.0: %d, PM2.5: %d, PM10: %d, Measuring mode: %s.", pm1, pm25, pm10, + LOG_STR_ARG(pm2005_get_measuring_mode_string(sensor_measuring_mode))); + + if (this->pm_1_0_sensor_ != nullptr) { + this->pm_1_0_sensor_->publish_state(pm1); + } + if (this->pm_2_5_sensor_ != nullptr) { + this->pm_2_5_sensor_->publish_state(pm25); + } + if (this->pm_10_0_sensor_ != nullptr) { + this->pm_10_0_sensor_->publish_state(pm10); + } + + this->status_clear_warning(); +} + +void PM2005Component::dump_config() { + ESP_LOGCONFIG(TAG, "PM2005:"); + ESP_LOGCONFIG(TAG, " Type: PM2%u05", this->sensor_type_ == PM2105); + + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with PM2%u05 failed!", this->sensor_type_ == PM2105); + } + + 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..219fbae5cb --- /dev/null +++ b/esphome/components/pm2005/pm2005.h @@ -0,0 +1,46 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace pm2005 { + +enum SensorType { + PM2005, + PM2105, +}; + +class PM2005Component : public PollingComponent, public i2c::I2CDevice { + public: + float get_setup_priority() const override { return esphome::setup_priority::DATA; } + + void set_sensor_type(SensorType sensor_type) { this->sensor_type_ = sensor_type; } + + void set_pm_1_0_sensor(sensor::Sensor *pm_1_0_sensor) { this->pm_1_0_sensor_ = pm_1_0_sensor; } + void set_pm_2_5_sensor(sensor::Sensor *pm_2_5_sensor) { this->pm_2_5_sensor_ = pm_2_5_sensor; } + void set_pm_10_0_sensor(sensor::Sensor *pm_10_0_sensor) { this->pm_10_0_sensor_ = pm_10_0_sensor; } + + void setup() override; + void dump_config() override; + void update() override; + + protected: + uint8_t sensor_situation_{0}; + uint8_t data_buffer_[12]; + SensorType sensor_type_{PM2005}; + + sensor::Sensor *pm_1_0_sensor_{nullptr}; + sensor::Sensor *pm_2_5_sensor_{nullptr}; + sensor::Sensor *pm_10_0_sensor_{nullptr}; + + uint8_t situation_value_index_{3}; + uint8_t pm_1_0_value_index_{4}; + uint8_t pm_2_5_value_index_{6}; + uint8_t pm_10_0_value_index_{8}; + uint8_t measuring_value_index_{10}; +}; + +} // namespace pm2005 +} // namespace esphome diff --git a/esphome/components/pm2005/sensor.py b/esphome/components/pm2005/sensor.py new file mode 100644 index 0000000000..66f630f8ff --- /dev/null +++ b/esphome/components/pm2005/sensor.py @@ -0,0 +1,86 @@ +"""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 +) + +SensorType = pm2005_ns.enum("SensorType") +SENSOR_TYPE = { + "PM2005": SensorType.PM2005, + "PM2105": SensorType.PM2105, +} + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(PM2005Component), + cv.Optional(CONF_TYPE, default="PM2005"): cv.enum(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) + + cg.add(var.set_sensor_type(config[CONF_TYPE])) + + if pm_1_0_config := config.get(CONF_PM_1_0): + sens = await sensor.new_sensor(pm_1_0_config) + cg.add(var.set_pm_1_0_sensor(sens)) + + if pm_2_5_config := config.get(CONF_PM_2_5): + sens = await sensor.new_sensor(pm_2_5_config) + cg.add(var.set_pm_2_5_sensor(sens)) + + if pm_10_0_config := config.get(CONF_PM_10_0): + sens = await sensor.new_sensor(pm_10_0_config) + cg.add(var.set_pm_10_0_sensor(sens)) diff --git a/tests/components/pm2005/common.yaml b/tests/components/pm2005/common.yaml new file mode 100644 index 0000000000..b8f6683b22 --- /dev/null +++ b/tests/components/pm2005/common.yaml @@ -0,0 +1,13 @@ +i2c: + - id: i2c_pm2005 + scl: ${scl_pin} + sda: ${sda_pin} + +sensor: + - platform: pm2005 + pm_1_0: + name: PM1.0 + pm_2_5: + name: PM2.5 + pm_10_0: + name: PM10.0 diff --git a/tests/components/pm2005/test.esp32-ard.yaml b/tests/components/pm2005/test.esp32-ard.yaml new file mode 100644 index 0000000000..63c3bd6afd --- /dev/null +++ b/tests/components/pm2005/test.esp32-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO16 + sda_pin: GPIO17 + +<<: !include common.yaml diff --git a/tests/components/pm2005/test.esp32-c3-ard.yaml b/tests/components/pm2005/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/pm2005/test.esp32-c3-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/pm2005/test.esp32-c3-idf.yaml b/tests/components/pm2005/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/pm2005/test.esp32-c3-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/pm2005/test.esp32-idf.yaml b/tests/components/pm2005/test.esp32-idf.yaml new file mode 100644 index 0000000000..63c3bd6afd --- /dev/null +++ b/tests/components/pm2005/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO16 + sda_pin: GPIO17 + +<<: !include common.yaml diff --git a/tests/components/pm2005/test.esp8266-ard.yaml b/tests/components/pm2005/test.esp8266-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/pm2005/test.esp8266-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/pm2005/test.rp2040-ard.yaml b/tests/components/pm2005/test.rp2040-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/pm2005/test.rp2040-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml