diff --git a/CODEOWNERS b/CODEOWNERS index f45c9c1db4..1edea18157 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -86,6 +86,7 @@ esphome/components/number/* @esphome/core esphome/components/ota/* @esphome/core esphome/components/output/* @esphome/core esphome/components/pid/* @OttoWinter +esphome/components/pmsa003i/* @sjtrny esphome/components/pn532/* @OttoWinter @jesserockz esphome/components/pn532_i2c/* @OttoWinter @jesserockz esphome/components/pn532_spi/* @OttoWinter @jesserockz diff --git a/esphome/components/pmsa003i/__init__.py b/esphome/components/pmsa003i/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/pmsa003i/pmsa003i.cpp b/esphome/components/pmsa003i/pmsa003i.cpp new file mode 100644 index 0000000000..1396c9f3d4 --- /dev/null +++ b/esphome/components/pmsa003i/pmsa003i.cpp @@ -0,0 +1,100 @@ +#include "pmsa003i.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pmsa003i { + +static const char *const TAG = "pmsa003i"; + +void PMSA003IComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up pmsa003i..."); + + PM25AQIData data; + bool successful_read = this->read_data_(&data); + + if (!successful_read) { + this->mark_failed(); + return; + } +} + +void PMSA003IComponent::dump_config() { LOG_I2C_DEVICE(this); } + +void PMSA003IComponent::update() { + PM25AQIData data; + + bool successful_read = this->read_data_(&data); + + // Update sensors + if (successful_read) { + this->status_clear_warning(); + ESP_LOGV(TAG, "Read success. Updating sensors."); + + if (this->standard_units_) { + if (this->pm_1_0_sensor_ != nullptr) + this->pm_1_0_sensor_->publish_state(data.pm10_standard); + if (this->pm_2_5_sensor_ != nullptr) + this->pm_2_5_sensor_->publish_state(data.pm25_standard); + if (this->pm_10_0_sensor_ != nullptr) + this->pm_10_0_sensor_->publish_state(data.pm100_standard); + } else { + if (this->pm_1_0_sensor_ != nullptr) + this->pm_1_0_sensor_->publish_state(data.pm10_env); + if (this->pm_2_5_sensor_ != nullptr) + this->pm_2_5_sensor_->publish_state(data.pm25_env); + if (this->pm_10_0_sensor_ != nullptr) + this->pm_10_0_sensor_->publish_state(data.pm100_env); + } + + if (this->pmc_0_3_sensor_ != nullptr) + this->pmc_0_3_sensor_->publish_state(data.particles_03um); + if (this->pmc_0_5_sensor_ != nullptr) + this->pmc_0_5_sensor_->publish_state(data.particles_05um); + if (this->pmc_1_0_sensor_ != nullptr) + this->pmc_1_0_sensor_->publish_state(data.particles_10um); + if (this->pmc_2_5_sensor_ != nullptr) + this->pmc_2_5_sensor_->publish_state(data.particles_25um); + if (this->pmc_5_0_sensor_ != nullptr) + this->pmc_5_0_sensor_->publish_state(data.particles_50um); + if (this->pmc_10_0_sensor_ != nullptr) + this->pmc_10_0_sensor_->publish_state(data.particles_100um); + } else { + this->status_set_warning(); + ESP_LOGV(TAG, "Read failure. Skipping update."); + } +} + +bool PMSA003IComponent::read_data_(PM25AQIData *data) { + const uint8_t num_bytes = 32; + uint8_t buffer[num_bytes]; + + this->read_bytes_raw(buffer, num_bytes); + + // https://github.com/adafruit/Adafruit_PM25AQI + + // Check that start byte is correct! + if (buffer[0] != 0x42) { + return false; + } + + // get checksum ready + int16_t sum = 0; + for (uint8_t i = 0; i < 30; i++) { + sum += buffer[i]; + } + + // The data comes in endian'd, this solves it so it works on all platforms + uint16_t buffer_u16[15]; + for (uint8_t i = 0; i < 15; i++) { + buffer_u16[i] = buffer[2 + i * 2 + 1]; + buffer_u16[i] += (buffer[2 + i * 2] << 8); + } + + // put it into a nice struct :) + memcpy((void *) data, (void *) buffer_u16, 30); + + return (sum == data->checksum); +} + +} // namespace pmsa003i +} // namespace esphome diff --git a/esphome/components/pmsa003i/pmsa003i.h b/esphome/components/pmsa003i/pmsa003i.h new file mode 100644 index 0000000000..10176218ed --- /dev/null +++ b/esphome/components/pmsa003i/pmsa003i.h @@ -0,0 +1,68 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace pmsa003i { + +/**! Structure holding Plantower's standard packet **/ +// From https://github.com/adafruit/Adafruit_PM25AQI +struct PM25AQIData { + uint16_t framelen; ///< How long this data chunk is + uint16_t pm10_standard, ///< Standard PM1.0 + pm25_standard, ///< Standard PM2.5 + pm100_standard; ///< Standard PM10.0 + uint16_t pm10_env, ///< Environmental PM1.0 + pm25_env, ///< Environmental PM2.5 + pm100_env; ///< Environmental PM10.0 + uint16_t particles_03um, ///< 0.3um Particle Count + particles_05um, ///< 0.5um Particle Count + particles_10um, ///< 1.0um Particle Count + particles_25um, ///< 2.5um Particle Count + particles_50um, ///< 5.0um Particle Count + particles_100um; ///< 10.0um Particle Count + uint16_t unused; ///< Unused + uint16_t checksum; ///< Packet checksum +}; + +class PMSA003IComponent : public PollingComponent, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + void update() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + void set_standard_units(bool standard_units) { standard_units_ = standard_units; } + + void set_pm_1_0_sensor(sensor::Sensor *pm_1_0) { pm_1_0_sensor_ = pm_1_0; } + void set_pm_2_5_sensor(sensor::Sensor *pm_2_5) { pm_2_5_sensor_ = pm_2_5; } + void set_pm_10_0_sensor(sensor::Sensor *pm_10_0) { pm_10_0_sensor_ = pm_10_0; } + + void set_pmc_0_3_sensor(sensor::Sensor *pmc_0_3) { pmc_0_3_sensor_ = pmc_0_3; } + void set_pmc_0_5_sensor(sensor::Sensor *pmc_0_5) { pmc_0_5_sensor_ = pmc_0_5; } + void set_pmc_1_0_sensor(sensor::Sensor *pmc_1_0) { pmc_1_0_sensor_ = pmc_1_0; } + void set_pmc_2_5_sensor(sensor::Sensor *pmc_2_5) { pmc_2_5_sensor_ = pmc_2_5; } + void set_pmc_5_0_sensor(sensor::Sensor *pmc_5_0) { pmc_5_0_sensor_ = pmc_5_0; } + void set_pmc_10_0_sensor(sensor::Sensor *pmc_10_0) { pmc_10_0_sensor_ = pmc_10_0; } + + protected: + bool read_data_(PM25AQIData *data); + + bool standard_units_; + + sensor::Sensor *pm_1_0_sensor_{nullptr}; + sensor::Sensor *pm_2_5_sensor_{nullptr}; + sensor::Sensor *pm_10_0_sensor_{nullptr}; + + sensor::Sensor *pmc_0_3_sensor_{nullptr}; + sensor::Sensor *pmc_0_5_sensor_{nullptr}; + sensor::Sensor *pmc_1_0_sensor_{nullptr}; + sensor::Sensor *pmc_2_5_sensor_{nullptr}; + sensor::Sensor *pmc_5_0_sensor_{nullptr}; + sensor::Sensor *pmc_10_0_sensor_{nullptr}; +}; + +} // namespace pmsa003i +} // namespace esphome diff --git a/esphome/components/pmsa003i/sensor.py b/esphome/components/pmsa003i/sensor.py new file mode 100644 index 0000000000..ac26270cfc --- /dev/null +++ b/esphome/components/pmsa003i/sensor.py @@ -0,0 +1,104 @@ +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_PMC_0_5, + CONF_PMC_1_0, + CONF_PMC_2_5, + CONF_PMC_10_0, + UNIT_MICROGRAMS_PER_CUBIC_METER, + ICON_CHEMICAL_WEAPON, + ICON_COUNTER, + DEVICE_CLASS_EMPTY, +) + +CODEOWNERS = ["@sjtrny"] +DEPENDENCIES = ["i2c"] + +pmsa003i_ns = cg.esphome_ns.namespace("pmsa003i") + +PMSA003IComponent = pmsa003i_ns.class_( + "PMSA003IComponent", cg.PollingComponent, i2c.I2CDevice +) + +CONF_STANDARD_UNITS = "standard_units" +UNIT_COUNTS_PER_100ML = "#/0.1L" +CONF_PMC_0_3 = "pmc_0_3" +CONF_PMC_5_0 = "pmc_5_0" + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(PMSA003IComponent), + cv.Optional(CONF_STANDARD_UNITS, default=True): cv.boolean, + cv.Optional(CONF_PM_1_0): sensor.sensor_schema( + UNIT_MICROGRAMS_PER_CUBIC_METER, + ICON_CHEMICAL_WEAPON, + 2, + DEVICE_CLASS_EMPTY, + ), + cv.Optional(CONF_PM_2_5): sensor.sensor_schema( + UNIT_MICROGRAMS_PER_CUBIC_METER, + ICON_CHEMICAL_WEAPON, + 2, + DEVICE_CLASS_EMPTY, + ), + cv.Optional(CONF_PM_10_0): sensor.sensor_schema( + UNIT_MICROGRAMS_PER_CUBIC_METER, + ICON_CHEMICAL_WEAPON, + 2, + DEVICE_CLASS_EMPTY, + ), + cv.Optional(CONF_PMC_0_3): sensor.sensor_schema( + UNIT_COUNTS_PER_100ML, ICON_COUNTER, 0, DEVICE_CLASS_EMPTY + ), + cv.Optional(CONF_PMC_0_5): sensor.sensor_schema( + UNIT_COUNTS_PER_100ML, ICON_COUNTER, 0, DEVICE_CLASS_EMPTY + ), + cv.Optional(CONF_PMC_1_0): sensor.sensor_schema( + UNIT_COUNTS_PER_100ML, ICON_COUNTER, 0, DEVICE_CLASS_EMPTY + ), + cv.Optional(CONF_PMC_2_5): sensor.sensor_schema( + UNIT_COUNTS_PER_100ML, ICON_COUNTER, 0, DEVICE_CLASS_EMPTY + ), + cv.Optional(CONF_PMC_5_0): sensor.sensor_schema( + UNIT_COUNTS_PER_100ML, ICON_COUNTER, 0, DEVICE_CLASS_EMPTY + ), + cv.Optional(CONF_PMC_10_0): sensor.sensor_schema( + UNIT_COUNTS_PER_100ML, ICON_COUNTER, 0, DEVICE_CLASS_EMPTY + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x12)) +) + +TYPES = { + CONF_PM_1_0: "set_pm_1_0_sensor", + CONF_PM_2_5: "set_pm_2_5_sensor", + CONF_PM_10_0: "set_pm_10_0_sensor", + CONF_PMC_0_3: "set_pmc_0_3_sensor", + CONF_PMC_0_5: "set_pmc_0_5_sensor", + CONF_PMC_1_0: "set_pmc_1_0_sensor", + CONF_PMC_2_5: "set_pmc_2_5_sensor", + CONF_PMC_5_0: "set_pmc_5_0_sensor", + CONF_PMC_10_0: "set_pmc_10_0_sensor", +} + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield i2c.register_i2c_device(var, config) + + cg.add(var.set_standard_units(config[CONF_STANDARD_UNITS])) + + for key, funcName in TYPES.items(): + + if key in config: + sens = yield sensor.new_sensor(config[key]) + cg.add(getattr(var, funcName)(sens)) diff --git a/tests/test1.yaml b/tests/test1.yaml index 0e81b19998..2bc767c885 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -675,6 +675,27 @@ sensor: name: 'Outside Pressure' address: 0x77 update_interval: 15s + - platform: pmsa003i + pm_1_0: + name: "PMSA003i PM1.0" + pm_2_5: + name: "PMSA003i PM2.5" + pm_10_0: + name: "PMSA003i PM10.0" + pmc_0_3: + name: "PMSA003i PMC <0.3µm" + pmc_0_5: + name: "PMSA003i PMC <0.5µm" + pmc_1_0: + name: "PMSA003i PMC <1µm" + pmc_2_5: + name: "PMSA003i PMC <2.5µm" + pmc_5_0: + name: "PMSA003i PMC <5µm" + pmc_10_0: + name: "PMSA003i PMC <10µm" + address: 0x12 + standard_units: True - platform: pulse_counter name: 'Pulse Counter' pin: GPIO12