mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 07:03:55 +00:00 
			
		
		
		
	New PM sensor Panasonic SN-GCJA5 (#4988)
This commit is contained in:
		| @@ -102,6 +102,7 @@ esphome/components/fastled_base/* @OttoWinter | |||||||
| esphome/components/feedback/* @ianchi | esphome/components/feedback/* @ianchi | ||||||
| esphome/components/fingerprint_grow/* @OnFreund @loongyh | esphome/components/fingerprint_grow/* @OnFreund @loongyh | ||||||
| esphome/components/fs3000/* @kahrendt | esphome/components/fs3000/* @kahrendt | ||||||
|  | esphome/components/gcja5/* @gcormier | ||||||
| esphome/components/globals/* @esphome/core | esphome/components/globals/* @esphome/core | ||||||
| esphome/components/gp8403/* @jesserockz | esphome/components/gp8403/* @jesserockz | ||||||
| esphome/components/gpio/* @esphome/core | esphome/components/gpio/* @esphome/core | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								esphome/components/gcja5/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								esphome/components/gcja5/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | CODEOWNERS = ["@gcormier"] | ||||||
							
								
								
									
										119
									
								
								esphome/components/gcja5/gcja5.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								esphome/components/gcja5/gcja5.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -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 <cstring> | ||||||
|  |  | ||||||
|  | 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 | ||||||
							
								
								
									
										52
									
								
								esphome/components/gcja5/gcja5.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								esphome/components/gcja5/gcja5.h
									
									
									
									
									
										Normal file
									
								
							| @@ -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<uint8_t> 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 | ||||||
							
								
								
									
										118
									
								
								esphome/components/gcja5/sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								esphome/components/gcja5/sensor.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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)) | ||||||
| @@ -228,6 +228,10 @@ uart: | |||||||
|     baud_rate: 256000 |     baud_rate: 256000 | ||||||
|     parity: NONE |     parity: NONE | ||||||
|     stop_bits: 1 |     stop_bits: 1 | ||||||
|  |   - id: gcja5_uart | ||||||
|  |     rx_pin: GPIO10 | ||||||
|  |     parity: EVEN | ||||||
|  |     baud_rate: 9600 | ||||||
|  |  | ||||||
| ota: | ota: | ||||||
|   safe_mode: true |   safe_mode: true | ||||||
| @@ -341,6 +345,24 @@ mcp23s17: | |||||||
|     deviceaddress: 1 |     deviceaddress: 1 | ||||||
|  |  | ||||||
| sensor: | 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 |   - platform: internal_temperature | ||||||
|     name: Internal Temperature |     name: Internal Temperature | ||||||
|   - platform: ble_client |   - platform: ble_client | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user