From 0ed0bdc65515dc685f70474fb781de3cc39acda8 Mon Sep 17 00:00:00 2001 From: Greg Cormier Date: Thu, 10 Aug 2023 01:04:22 -0400 Subject: [PATCH] New PM sensor Panasonic SN-GCJA5 (#4988) --- CODEOWNERS | 1 + esphome/components/gcja5/__init__.py | 1 + esphome/components/gcja5/gcja5.cpp | 119 +++++++++++++++++++++++++++ esphome/components/gcja5/gcja5.h | 52 ++++++++++++ esphome/components/gcja5/sensor.py | 118 ++++++++++++++++++++++++++ tests/test1.yaml | 22 +++++ 6 files changed, 313 insertions(+) create mode 100644 esphome/components/gcja5/__init__.py create mode 100644 esphome/components/gcja5/gcja5.cpp create mode 100644 esphome/components/gcja5/gcja5.h create mode 100644 esphome/components/gcja5/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index cda8a2f0f5..408caee4f2 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -102,6 +102,7 @@ esphome/components/fastled_base/* @OttoWinter esphome/components/feedback/* @ianchi esphome/components/fingerprint_grow/* @OnFreund @loongyh esphome/components/fs3000/* @kahrendt +esphome/components/gcja5/* @gcormier esphome/components/globals/* @esphome/core esphome/components/gp8403/* @jesserockz esphome/components/gpio/* @esphome/core diff --git a/esphome/components/gcja5/__init__.py b/esphome/components/gcja5/__init__.py new file mode 100644 index 0000000000..122ffaf6ed --- /dev/null +++ b/esphome/components/gcja5/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@gcormier"] diff --git a/esphome/components/gcja5/gcja5.cpp b/esphome/components/gcja5/gcja5.cpp new file mode 100644 index 0000000000..7f980ca0ad --- /dev/null +++ b/esphome/components/gcja5/gcja5.cpp @@ -0,0 +1,119 @@ +/* From snooping with a logic analyzer, the I2C on this sensor is broken. I was only able + * to receive 1's as a response from the sensor. I was able to get the UART working. + * + * The datasheet says the values should be divided by 1000, but this must only be for the I2C + * implementation. Comparing UART values with another sensor, there is no need to divide by 1000. + */ +#include "gcja5.h" +#include "esphome/core/log.h" +#include + +namespace esphome { +namespace gcja5 { + +static const char *const TAG = "gcja5"; + +void GCJA5Component::setup() { ESP_LOGCONFIG(TAG, "Setting up gcja5..."); } + +void GCJA5Component::loop() { + const uint32_t now = millis(); + if (now - this->last_transmission_ >= 500) { + // last transmission too long ago. Reset RX index. + this->rx_message_.clear(); + } + + if (this->available() == 0) { + return; + } + + // There must now be data waiting + this->last_transmission_ = now; + uint8_t val; + while (this->available() != 0) { + this->read_byte(&val); + this->rx_message_.push_back(val); + + // check if rx_message_ has 32 bytes of data + if (this->rx_message_.size() == 32) { + this->parse_data_(); + + if (this->have_good_data_) { + if (this->pm_1_0_sensor_ != nullptr) + this->pm_1_0_sensor_->publish_state(get_32_bit_uint_(1)); + if (this->pm_2_5_sensor_ != nullptr) + this->pm_2_5_sensor_->publish_state(get_32_bit_uint_(5)); + if (this->pm_10_0_sensor_ != nullptr) + this->pm_10_0_sensor_->publish_state(get_32_bit_uint_(9)); + if (this->pmc_0_3_sensor_ != nullptr) + this->pmc_0_3_sensor_->publish_state(get_16_bit_uint_(13)); + if (this->pmc_0_5_sensor_ != nullptr) + this->pmc_0_5_sensor_->publish_state(get_16_bit_uint_(15)); + if (this->pmc_1_0_sensor_ != nullptr) + this->pmc_1_0_sensor_->publish_state(get_16_bit_uint_(17)); + if (this->pmc_2_5_sensor_ != nullptr) + this->pmc_2_5_sensor_->publish_state(get_16_bit_uint_(21)); + if (this->pmc_5_0_sensor_ != nullptr) + this->pmc_5_0_sensor_->publish_state(get_16_bit_uint_(23)); + if (this->pmc_10_0_sensor_ != nullptr) + this->pmc_10_0_sensor_->publish_state(get_16_bit_uint_(25)); + } else { + this->status_set_warning(); + ESP_LOGV(TAG, "Have 32 bytes but not good data. Skipping."); + } + + this->rx_message_.clear(); + } + } +} + +bool GCJA5Component::calculate_checksum_() { + uint8_t crc = 0; + + for (uint8_t i = 1; i < 30; i++) + crc = crc ^ this->rx_message_[i]; + + ESP_LOGVV(TAG, "Checksum packet was (0x%02X), calculated checksum was (0x%02X)", this->rx_message_[30], crc); + + return (crc == this->rx_message_[30]); +} + +uint32_t GCJA5Component::get_32_bit_uint_(uint8_t start_index) { + return (((uint32_t) this->rx_message_[start_index + 3]) << 24) | + (((uint32_t) this->rx_message_[start_index + 2]) << 16) | + (((uint32_t) this->rx_message_[start_index + 1]) << 8) | ((uint32_t) this->rx_message_[start_index]); +} + +uint16_t GCJA5Component::get_16_bit_uint_(uint8_t start_index) { + return (((uint32_t) this->rx_message_[start_index + 1]) << 8) | ((uint32_t) this->rx_message_[start_index]); +} + +void GCJA5Component::parse_data_() { + ESP_LOGVV(TAG, "GCJA5 Data: "); + for (uint8_t i = 0; i < 32; i++) { + ESP_LOGVV(TAG, " %u: 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", i + 1, BYTE_TO_BINARY(this->rx_message_[i]), + this->rx_message_[i]); + } + + if (this->rx_message_[0] != 0x02 || this->rx_message_[31] != 0x03 || !this->calculate_checksum_()) { + ESP_LOGVV(TAG, "Discarding bad packet - failed checks."); + return; + } else + ESP_LOGVV(TAG, "Good packet found."); + + this->have_good_data_ = true; + uint8_t status = this->rx_message_[29]; + if (!this->first_status_log_) { + this->first_status_log_ = true; + + ESP_LOGI(TAG, "GCJA5 Status"); + ESP_LOGI(TAG, "Overall Status : %i", (status >> 6) & 0x03); + ESP_LOGI(TAG, "PD Status : %i", (status >> 4) & 0x03); + ESP_LOGI(TAG, "LD Status : %i", (status >> 2) & 0x03); + ESP_LOGI(TAG, "Fan Status : %i", (status >> 0) & 0x03); + } +} + +void GCJA5Component::dump_config() { ; } + +} // namespace gcja5 +} // namespace esphome diff --git a/esphome/components/gcja5/gcja5.h b/esphome/components/gcja5/gcja5.h new file mode 100644 index 0000000000..7593c90323 --- /dev/null +++ b/esphome/components/gcja5/gcja5.h @@ -0,0 +1,52 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/uart/uart.h" + +namespace esphome { +namespace gcja5 { + +class GCJA5Component : public Component, public uart::UARTDevice { + public: + void setup() override; + void dump_config() override; + void loop() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + 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: + void parse_data_(); + bool calculate_checksum_(); + + uint32_t get_32_bit_uint_(uint8_t start_index); + uint16_t get_16_bit_uint_(uint8_t start_index); + uint32_t last_transmission_{0}; + std::vector rx_message_; + + bool have_good_data_{false}; + bool first_status_log_{false}; + 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 gcja5 +} // namespace esphome diff --git a/esphome/components/gcja5/sensor.py b/esphome/components/gcja5/sensor.py new file mode 100644 index 0000000000..5bcdc572ff --- /dev/null +++ b/esphome/components/gcja5/sensor.py @@ -0,0 +1,118 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import uart, 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_PM1, + DEVICE_CLASS_PM10, + DEVICE_CLASS_PM25, + STATE_CLASS_MEASUREMENT, +) + +CODEOWNERS = ["@gcormier"] +DEPENDENCIES = ["uart"] + +gcja5_ns = cg.esphome_ns.namespace("gcja5") + +GCJA5Component = gcja5_ns.class_("GCJA5Component", cg.PollingComponent, uart.UARTDevice) + +CONF_PMC_0_3 = "pmc_0_3" +CONF_PMC_5_0 = "pmc_5_0" + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(GCJA5Component), + cv.Optional(CONF_PM_1_0): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=2, + 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=2, + 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=2, + device_class=DEVICE_CLASS_PM10, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PMC_0_3): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_COUNTER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PMC_0_5): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_COUNTER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PMC_1_0): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_COUNTER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PMC_2_5): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_COUNTER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PMC_5_0): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_COUNTER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PMC_10_0): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_COUNTER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + } +).extend(uart.UART_DEVICE_SCHEMA) +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "gcja5", baud_rate=9600, require_rx=True, parity="EVEN" +) +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", +} + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) + + for key, funcName in TYPES.items(): + if key in config: + sens = await sensor.new_sensor(config[key]) + cg.add(getattr(var, funcName)(sens)) diff --git a/tests/test1.yaml b/tests/test1.yaml index 254abe2b01..5c9b83a915 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -228,6 +228,10 @@ uart: baud_rate: 256000 parity: NONE stop_bits: 1 + - id: gcja5_uart + rx_pin: GPIO10 + parity: EVEN + baud_rate: 9600 ota: safe_mode: true @@ -341,6 +345,24 @@ mcp23s17: deviceaddress: 1 sensor: + - platform: gcja5 + pm_1_0: + name: "Particulate Matter <1.0µm Concentration" + pm_2_5: + name: "Particulate Matter <2.5µm Concentration" + pm_10_0: + name: "Particulate Matter <10.0µm Concentration" + pmc_0_5: + name: "PMC 0.5" + pmc_1_0: + name: "PMC 1.0" + pmc_2_5: + name: "PMC 2.5" + pmc_5_0: + name: "PMC 5.0" + pmc_10_0: + name: "PMC 10.0" + uart_id: gcja5_uart - platform: internal_temperature name: Internal Temperature - platform: ble_client