mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-30 22:53:59 +00:00 
			
		
		
		
	add support for Sen5x sensor series (#3383)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
		| @@ -173,6 +173,7 @@ esphome/components/sdm_meter/* @jesserockz @polyfaces | |||||||
| esphome/components/sdp3x/* @Azimath | esphome/components/sdp3x/* @Azimath | ||||||
| esphome/components/selec_meter/* @sourabhjaiswal | esphome/components/selec_meter/* @sourabhjaiswal | ||||||
| esphome/components/select/* @esphome/core | esphome/components/select/* @esphome/core | ||||||
|  | esphome/components/sen5x/* @martgras | ||||||
| esphome/components/sensirion_common/* @martgras | esphome/components/sensirion_common/* @martgras | ||||||
| esphome/components/sensor/* @esphome/core | esphome/components/sensor/* @esphome/core | ||||||
| esphome/components/sgp40/* @SenexCrenshaw | esphome/components/sgp40/* @SenexCrenshaw | ||||||
|   | |||||||
							
								
								
									
										0
									
								
								esphome/components/sen5x/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								esphome/components/sen5x/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										21
									
								
								esphome/components/sen5x/automation.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								esphome/components/sen5x/automation.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include "esphome/core/component.h" | ||||||
|  | #include "esphome/core/automation.h" | ||||||
|  | #include "sen5x.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace sen5x { | ||||||
|  |  | ||||||
|  | template<typename... Ts> class StartFanAction : public Action<Ts...> { | ||||||
|  |  public: | ||||||
|  |   explicit StartFanAction(SEN5XComponent *sen5x) : sen5x_(sen5x) {} | ||||||
|  |  | ||||||
|  |   void play(Ts... x) override { this->sen5x_->start_fan_cleaning(); } | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   SEN5XComponent *sen5x_; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace sen5x | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										413
									
								
								esphome/components/sen5x/sen5x.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										413
									
								
								esphome/components/sen5x/sen5x.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,413 @@ | |||||||
|  | #include "sen5x.h" | ||||||
|  | #include "esphome/core/hal.h" | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace sen5x { | ||||||
|  |  | ||||||
|  | static const char *const TAG = "sen5x"; | ||||||
|  |  | ||||||
|  | static const uint16_t SEN5X_CMD_AUTO_CLEANING_INTERVAL = 0x8004; | ||||||
|  | static const uint16_t SEN5X_CMD_GET_DATA_READY_STATUS = 0x0202; | ||||||
|  | static const uint16_t SEN5X_CMD_GET_FIRMWARE_VERSION = 0xD100; | ||||||
|  | static const uint16_t SEN5X_CMD_GET_PRODUCT_NAME = 0xD014; | ||||||
|  | static const uint16_t SEN5X_CMD_GET_SERIAL_NUMBER = 0xD033; | ||||||
|  | static const uint16_t SEN5X_CMD_NOX_ALGORITHM_TUNING = 0x60E1; | ||||||
|  | static const uint16_t SEN5X_CMD_READ_MEASUREMENT = 0x03C4; | ||||||
|  | static const uint16_t SEN5X_CMD_RHT_ACCELERATION_MODE = 0x60F7; | ||||||
|  | static const uint16_t SEN5X_CMD_START_CLEANING_FAN = 0x5607; | ||||||
|  | static const uint16_t SEN5X_CMD_START_MEASUREMENTS = 0x0021; | ||||||
|  | static const uint16_t SEN5X_CMD_START_MEASUREMENTS_RHT_ONLY = 0x0037; | ||||||
|  | static const uint16_t SEN5X_CMD_STOP_MEASUREMENTS = 0x3f86; | ||||||
|  | static const uint16_t SEN5X_CMD_TEMPERATURE_COMPENSATION = 0x60B2; | ||||||
|  | static const uint16_t SEN5X_CMD_VOC_ALGORITHM_STATE = 0x6181; | ||||||
|  | static const uint16_t SEN5X_CMD_VOC_ALGORITHM_TUNING = 0x60D0; | ||||||
|  |  | ||||||
|  | void SEN5XComponent::setup() { | ||||||
|  |   ESP_LOGCONFIG(TAG, "Setting up sen5x..."); | ||||||
|  |  | ||||||
|  |   // the sensor needs 1000 ms to enter the idle state | ||||||
|  |   this->set_timeout(1000, [this]() { | ||||||
|  |     // Check if measurement is ready before reading the value | ||||||
|  |     if (!this->write_command(SEN5X_CMD_GET_DATA_READY_STATUS)) { | ||||||
|  |       ESP_LOGE(TAG, "Failed to write data ready status command"); | ||||||
|  |       this->mark_failed(); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     uint16_t raw_read_status; | ||||||
|  |     if (!this->read_data(raw_read_status)) { | ||||||
|  |       ESP_LOGE(TAG, "Failed to read data ready status"); | ||||||
|  |       this->mark_failed(); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     uint32_t stop_measurement_delay = 0; | ||||||
|  |     // In order to query the device periodic measurement must be ceased | ||||||
|  |     if (raw_read_status) { | ||||||
|  |       ESP_LOGD(TAG, "Sensor has data available, stopping periodic measurement"); | ||||||
|  |       if (!this->write_command(SEN5X_CMD_STOP_MEASUREMENTS)) { | ||||||
|  |         ESP_LOGE(TAG, "Failed to stop measurements"); | ||||||
|  |         this->mark_failed(); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |       // According to the SEN5x datasheet the sensor will only respond to other commands after waiting 200 ms after | ||||||
|  |       // issuing the stop_periodic_measurement command | ||||||
|  |       stop_measurement_delay = 200; | ||||||
|  |     } | ||||||
|  |     this->set_timeout(stop_measurement_delay, [this]() { | ||||||
|  |       uint16_t raw_serial_number[3]; | ||||||
|  |       if (!this->get_register(SEN5X_CMD_GET_SERIAL_NUMBER, raw_serial_number, 3, 20)) { | ||||||
|  |         ESP_LOGE(TAG, "Failed to read serial number"); | ||||||
|  |         this->error_code_ = SERIAL_NUMBER_IDENTIFICATION_FAILED; | ||||||
|  |         this->mark_failed(); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |       this->serial_number_[0] = static_cast<bool>(uint16_t(raw_serial_number[0]) & 0xFF); | ||||||
|  |       this->serial_number_[1] = static_cast<uint16_t>(raw_serial_number[0] & 0xFF); | ||||||
|  |       this->serial_number_[2] = static_cast<uint16_t>(raw_serial_number[1] >> 8); | ||||||
|  |       ESP_LOGD(TAG, "Serial number %02d.%02d.%02d", serial_number_[0], serial_number_[1], serial_number_[2]); | ||||||
|  |  | ||||||
|  |       uint16_t raw_product_name[16]; | ||||||
|  |       if (!this->get_register(SEN5X_CMD_GET_PRODUCT_NAME, raw_product_name, 16, 20)) { | ||||||
|  |         ESP_LOGE(TAG, "Failed to read product name"); | ||||||
|  |         this->error_code_ = PRODUCT_NAME_FAILED; | ||||||
|  |         this->mark_failed(); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |       // 2 ASCII bytes are encoded in an int | ||||||
|  |       const uint16_t *current_int = raw_product_name; | ||||||
|  |       char current_char; | ||||||
|  |       uint8_t max = 16; | ||||||
|  |       do { | ||||||
|  |         // first char | ||||||
|  |         current_char = *current_int >> 8; | ||||||
|  |         if (current_char) { | ||||||
|  |           product_name_.push_back(current_char); | ||||||
|  |           // second char | ||||||
|  |           current_char = *current_int & 0xFF; | ||||||
|  |           if (current_char) | ||||||
|  |             product_name_.push_back(current_char); | ||||||
|  |         } | ||||||
|  |         current_int++; | ||||||
|  |       } while (current_char && --max); | ||||||
|  |  | ||||||
|  |       Sen5xType sen5x_type = UNKNOWN; | ||||||
|  |       if (product_name_ == "SEN50") { | ||||||
|  |         sen5x_type = SEN50; | ||||||
|  |       } else { | ||||||
|  |         if (product_name_ == "SEN54") { | ||||||
|  |           sen5x_type = SEN54; | ||||||
|  |         } else { | ||||||
|  |           if (product_name_ == "SEN55") { | ||||||
|  |             sen5x_type = SEN55; | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |         ESP_LOGD(TAG, "Productname %s", product_name_.c_str()); | ||||||
|  |       } | ||||||
|  |       if (this->humidity_sensor_ && sen5x_type == SEN50) { | ||||||
|  |         ESP_LOGE(TAG, "For Relative humidity a SEN54 OR SEN55 is required. You are using a <%s> sensor", | ||||||
|  |                  this->product_name_.c_str()); | ||||||
|  |         this->humidity_sensor_ = nullptr;  // mark as not used | ||||||
|  |       } | ||||||
|  |       if (this->temperature_sensor_ && sen5x_type == SEN50) { | ||||||
|  |         ESP_LOGE(TAG, "For Temperature a SEN54 OR SEN55 is required. You are using a <%s> sensor", | ||||||
|  |                  this->product_name_.c_str()); | ||||||
|  |         this->temperature_sensor_ = nullptr;  // mark as not used | ||||||
|  |       } | ||||||
|  |       if (this->voc_sensor_ && sen5x_type == SEN50) { | ||||||
|  |         ESP_LOGE(TAG, "For VOC a SEN54 OR SEN55 is required. You are using a <%s> sensor", this->product_name_.c_str()); | ||||||
|  |         this->voc_sensor_ = nullptr;  // mark as not used | ||||||
|  |       } | ||||||
|  |       if (this->nox_sensor_ && sen5x_type != SEN55) { | ||||||
|  |         ESP_LOGE(TAG, "For NOx a SEN55 is required. You are using a <%s> sensor", this->product_name_.c_str()); | ||||||
|  |         this->nox_sensor_ = nullptr;  // mark as not used | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       if (!this->get_register(SEN5X_CMD_GET_FIRMWARE_VERSION, this->firmware_version_, 20)) { | ||||||
|  |         ESP_LOGE(TAG, "Failed to read firmware version"); | ||||||
|  |         this->error_code_ = FIRMWARE_FAILED; | ||||||
|  |         this->mark_failed(); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |       this->firmware_version_ >>= 8; | ||||||
|  |       ESP_LOGD(TAG, "Firmware version %d", this->firmware_version_); | ||||||
|  |  | ||||||
|  |       if (this->voc_sensor_ && this->store_baseline_) { | ||||||
|  |         // Hash with compilation time | ||||||
|  |         // This ensures the baseline storage is cleared after OTA | ||||||
|  |         uint32_t hash = fnv1_hash(App.get_compilation_time()); | ||||||
|  |         this->pref_ = global_preferences->make_preference<Sen5xBaselines>(hash, true); | ||||||
|  |  | ||||||
|  |         if (this->pref_.load(&this->voc_baselines_storage_)) { | ||||||
|  |           ESP_LOGI(TAG, "Loaded VOC baseline state0: 0x%04X, state1: 0x%04X", this->voc_baselines_storage_.state0, | ||||||
|  |                    voc_baselines_storage_.state1); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Initialize storage timestamp | ||||||
|  |         this->seconds_since_last_store_ = 0; | ||||||
|  |  | ||||||
|  |         if (this->voc_baselines_storage_.state0 > 0 && this->voc_baselines_storage_.state1 > 0) { | ||||||
|  |           ESP_LOGI(TAG, "Setting VOC baseline from save state0: 0x%04X, state1: 0x%04X", | ||||||
|  |                    this->voc_baselines_storage_.state0, voc_baselines_storage_.state1); | ||||||
|  |           uint16_t states[4]; | ||||||
|  |  | ||||||
|  |           states[0] = voc_baselines_storage_.state0 >> 16; | ||||||
|  |           states[1] = voc_baselines_storage_.state0 & 0xFFFF; | ||||||
|  |           states[2] = voc_baselines_storage_.state1 >> 16; | ||||||
|  |           states[3] = voc_baselines_storage_.state1 & 0xFFFF; | ||||||
|  |  | ||||||
|  |           if (!this->write_command(SEN5X_CMD_VOC_ALGORITHM_STATE, states, 4)) { | ||||||
|  |             ESP_LOGE(TAG, "Failed to set VOC baseline from saved state"); | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       bool result; | ||||||
|  |       if (this->auto_cleaning_interval_.has_value()) { | ||||||
|  |         // override default value | ||||||
|  |         result = write_command(SEN5X_CMD_AUTO_CLEANING_INTERVAL, this->auto_cleaning_interval_.value()); | ||||||
|  |       } else { | ||||||
|  |         result = write_command(SEN5X_CMD_AUTO_CLEANING_INTERVAL); | ||||||
|  |       } | ||||||
|  |       if (result) { | ||||||
|  |         delay(20); | ||||||
|  |         uint16_t secs[2]; | ||||||
|  |         if (this->read_data(secs, 2)) { | ||||||
|  |           auto_cleaning_interval_ = secs[0] << 16 | secs[1]; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       if (acceleration_mode_.has_value()) { | ||||||
|  |         result = this->write_command(SEN5X_CMD_RHT_ACCELERATION_MODE, acceleration_mode_.value()); | ||||||
|  |       } else { | ||||||
|  |         result = this->write_command(SEN5X_CMD_RHT_ACCELERATION_MODE); | ||||||
|  |       } | ||||||
|  |       if (!result) { | ||||||
|  |         ESP_LOGE(TAG, "Failed to set rh/t acceleration mode"); | ||||||
|  |         this->error_code_ = COMMUNICATION_FAILED; | ||||||
|  |         this->mark_failed(); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |       delay(20); | ||||||
|  |       if (!acceleration_mode_.has_value()) { | ||||||
|  |         uint16_t mode; | ||||||
|  |         if (this->read_data(mode)) { | ||||||
|  |           this->acceleration_mode_ = RhtAccelerationMode(mode); | ||||||
|  |         } else { | ||||||
|  |           ESP_LOGE(TAG, "Failed to read RHT Acceleration mode"); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       if (this->voc_tuning_params_.has_value()) | ||||||
|  |         this->write_tuning_parameters_(SEN5X_CMD_VOC_ALGORITHM_TUNING, this->voc_tuning_params_.value()); | ||||||
|  |       if (this->nox_tuning_params_.has_value()) | ||||||
|  |         this->write_tuning_parameters_(SEN5X_CMD_NOX_ALGORITHM_TUNING, this->nox_tuning_params_.value()); | ||||||
|  |  | ||||||
|  |       if (this->temperature_compensation_.has_value()) | ||||||
|  |         this->write_temperature_compensation_(this->temperature_compensation_.value()); | ||||||
|  |  | ||||||
|  |       // Finally start sensor measurements | ||||||
|  |       auto cmd = SEN5X_CMD_START_MEASUREMENTS_RHT_ONLY; | ||||||
|  |       if (this->pm_1_0_sensor_ || this->pm_2_5_sensor_ || this->pm_4_0_sensor_ || this->pm_10_0_sensor_) { | ||||||
|  |         // if any of the gas sensors are active we need a full measurement | ||||||
|  |         cmd = SEN5X_CMD_START_MEASUREMENTS; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       if (!this->write_command(cmd)) { | ||||||
|  |         ESP_LOGE(TAG, "Error starting continuous measurements."); | ||||||
|  |         this->error_code_ = MEASUREMENT_INIT_FAILED; | ||||||
|  |         this->mark_failed(); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |       initialized_ = true; | ||||||
|  |       ESP_LOGD(TAG, "Sensor initialized"); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void SEN5XComponent::dump_config() { | ||||||
|  |   ESP_LOGCONFIG(TAG, "sen5x:"); | ||||||
|  |   LOG_I2C_DEVICE(this); | ||||||
|  |   if (this->is_failed()) { | ||||||
|  |     switch (this->error_code_) { | ||||||
|  |       case COMMUNICATION_FAILED: | ||||||
|  |         ESP_LOGW(TAG, "Communication failed! Is the sensor connected?"); | ||||||
|  |         break; | ||||||
|  |       case MEASUREMENT_INIT_FAILED: | ||||||
|  |         ESP_LOGW(TAG, "Measurement Initialization failed!"); | ||||||
|  |         break; | ||||||
|  |       case SERIAL_NUMBER_IDENTIFICATION_FAILED: | ||||||
|  |         ESP_LOGW(TAG, "Unable to read sensor serial id"); | ||||||
|  |         break; | ||||||
|  |       case PRODUCT_NAME_FAILED: | ||||||
|  |         ESP_LOGW(TAG, "Unable to read product name"); | ||||||
|  |         break; | ||||||
|  |       case FIRMWARE_FAILED: | ||||||
|  |         ESP_LOGW(TAG, "Unable to read sensor firmware version"); | ||||||
|  |         break; | ||||||
|  |       default: | ||||||
|  |         ESP_LOGW(TAG, "Unknown setup error!"); | ||||||
|  |         break; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   ESP_LOGCONFIG(TAG, "  Productname: %s", this->product_name_.c_str()); | ||||||
|  |   ESP_LOGCONFIG(TAG, "  Firmware version: %d", this->firmware_version_); | ||||||
|  |   ESP_LOGCONFIG(TAG, "  Serial number %02d.%02d.%02d", serial_number_[0], serial_number_[1], serial_number_[2]); | ||||||
|  |   if (this->auto_cleaning_interval_.has_value()) { | ||||||
|  |     ESP_LOGCONFIG(TAG, "  Auto auto cleaning interval %d seconds", auto_cleaning_interval_.value()); | ||||||
|  |   } | ||||||
|  |   if (this->acceleration_mode_.has_value()) { | ||||||
|  |     switch (this->acceleration_mode_.value()) { | ||||||
|  |       case LOW_ACCELERATION: | ||||||
|  |         ESP_LOGCONFIG(TAG, "  Low RH/T acceleration mode"); | ||||||
|  |         break; | ||||||
|  |       case MEDIUM_ACCELERATION: | ||||||
|  |         ESP_LOGCONFIG(TAG, "  Medium RH/T accelertion mode"); | ||||||
|  |         break; | ||||||
|  |       case HIGH_ACCELERATION: | ||||||
|  |         ESP_LOGCONFIG(TAG, "  High RH/T accelertion mode"); | ||||||
|  |         break; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   LOG_UPDATE_INTERVAL(this); | ||||||
|  |   LOG_SENSOR("  ", "PM  1.0", this->pm_1_0_sensor_); | ||||||
|  |   LOG_SENSOR("  ", "PM  2.5", this->pm_2_5_sensor_); | ||||||
|  |   LOG_SENSOR("  ", "PM  4.0", this->pm_4_0_sensor_); | ||||||
|  |   LOG_SENSOR("  ", "PM 10.0", this->pm_10_0_sensor_); | ||||||
|  |   LOG_SENSOR("  ", "Temperature", this->temperature_sensor_); | ||||||
|  |   LOG_SENSOR("  ", "Humidity", this->humidity_sensor_); | ||||||
|  |   LOG_SENSOR("  ", "VOC", this->voc_sensor_);  // SEN54 and SEN55 only | ||||||
|  |   LOG_SENSOR("  ", "NOx", this->nox_sensor_);  // SEN55 only | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void SEN5XComponent::update() { | ||||||
|  |   if (!initialized_) { | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Store baselines after defined interval or if the difference between current and stored baseline becomes too | ||||||
|  |   // much | ||||||
|  |   if (this->store_baseline_ && this->seconds_since_last_store_ > SHORTEST_BASELINE_STORE_INTERVAL) { | ||||||
|  |     if (this->write_command(SEN5X_CMD_VOC_ALGORITHM_STATE)) { | ||||||
|  |       // run it a bit later to avoid adding a delay here | ||||||
|  |       this->set_timeout(550, [this]() { | ||||||
|  |         uint16_t states[4]; | ||||||
|  |         if (this->read_data(states, 4)) { | ||||||
|  |           uint32_t state0 = states[0] << 16 | states[1]; | ||||||
|  |           uint32_t state1 = states[2] << 16 | states[3]; | ||||||
|  |           if ((uint32_t) std::abs(static_cast<int32_t>(this->voc_baselines_storage_.state0 - state0)) > | ||||||
|  |                   MAXIMUM_STORAGE_DIFF || | ||||||
|  |               (uint32_t) std::abs(static_cast<int32_t>(this->voc_baselines_storage_.state1 - state1)) > | ||||||
|  |                   MAXIMUM_STORAGE_DIFF) { | ||||||
|  |             this->seconds_since_last_store_ = 0; | ||||||
|  |             this->voc_baselines_storage_.state0 = state0; | ||||||
|  |             this->voc_baselines_storage_.state1 = state1; | ||||||
|  |  | ||||||
|  |             if (this->pref_.save(&this->voc_baselines_storage_)) { | ||||||
|  |               ESP_LOGI(TAG, "Stored VOC baseline state0: 0x%04X ,state1: 0x%04X", this->voc_baselines_storage_.state0, | ||||||
|  |                        voc_baselines_storage_.state1); | ||||||
|  |             } else { | ||||||
|  |               ESP_LOGW(TAG, "Could not store VOC baselines"); | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (!this->write_command(SEN5X_CMD_READ_MEASUREMENT)) { | ||||||
|  |     this->status_set_warning(); | ||||||
|  |     ESP_LOGD(TAG, "write error read measurement (%d)", this->last_error_); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   this->set_timeout(20, [this]() { | ||||||
|  |     uint16_t measurements[8]; | ||||||
|  |  | ||||||
|  |     if (!this->read_data(measurements, 8)) { | ||||||
|  |       this->status_set_warning(); | ||||||
|  |       ESP_LOGD(TAG, "read data error (%d)", this->last_error_); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     float pm_1_0 = measurements[0] / 10.0; | ||||||
|  |     if (measurements[0] == 0xFFFF) | ||||||
|  |       pm_1_0 = NAN; | ||||||
|  |     float pm_2_5 = measurements[1] / 10.0; | ||||||
|  |     if (measurements[1] == 0xFFFF) | ||||||
|  |       pm_2_5 = NAN; | ||||||
|  |     float pm_4_0 = measurements[2] / 10.0; | ||||||
|  |     if (measurements[2] == 0xFFFF) | ||||||
|  |       pm_4_0 = NAN; | ||||||
|  |     float pm_10_0 = measurements[3] / 10.0; | ||||||
|  |     if (measurements[3] == 0xFFFF) | ||||||
|  |       pm_10_0 = NAN; | ||||||
|  |     float humidity = measurements[4] / 100.0; | ||||||
|  |     if (measurements[4] == 0xFFFF) | ||||||
|  |       humidity = NAN; | ||||||
|  |     float temperature = measurements[5] / 200.0; | ||||||
|  |     if (measurements[5] == 0xFFFF) | ||||||
|  |       temperature = NAN; | ||||||
|  |     float voc = measurements[6] / 10.0; | ||||||
|  |     if (measurements[6] == 0xFFFF) | ||||||
|  |       voc = NAN; | ||||||
|  |     float nox = measurements[7] / 10.0; | ||||||
|  |     if (measurements[7] == 0xFFFF) | ||||||
|  |       nox = NAN; | ||||||
|  |  | ||||||
|  |     if (this->pm_1_0_sensor_ != nullptr) | ||||||
|  |       this->pm_1_0_sensor_->publish_state(pm_1_0); | ||||||
|  |     if (this->pm_2_5_sensor_ != nullptr) | ||||||
|  |       this->pm_2_5_sensor_->publish_state(pm_2_5); | ||||||
|  |     if (this->pm_4_0_sensor_ != nullptr) | ||||||
|  |       this->pm_4_0_sensor_->publish_state(pm_4_0); | ||||||
|  |     if (this->pm_10_0_sensor_ != nullptr) | ||||||
|  |       this->pm_10_0_sensor_->publish_state(pm_10_0); | ||||||
|  |     if (this->temperature_sensor_ != nullptr) | ||||||
|  |       this->temperature_sensor_->publish_state(temperature); | ||||||
|  |     if (this->humidity_sensor_ != nullptr) | ||||||
|  |       this->humidity_sensor_->publish_state(humidity); | ||||||
|  |     if (this->voc_sensor_ != nullptr) | ||||||
|  |       this->voc_sensor_->publish_state(voc); | ||||||
|  |     if (this->nox_sensor_ != nullptr) | ||||||
|  |       this->nox_sensor_->publish_state(nox); | ||||||
|  |     this->status_clear_warning(); | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | bool SEN5XComponent::write_tuning_parameters_(uint16_t i2c_command, const GasTuning &tuning) { | ||||||
|  |   uint16_t params[6]; | ||||||
|  |   params[0] = tuning.index_offset; | ||||||
|  |   params[1] = tuning.learning_time_offset_hours; | ||||||
|  |   params[2] = tuning.learning_time_gain_hours; | ||||||
|  |   params[3] = tuning.gating_max_duration_minutes; | ||||||
|  |   params[4] = tuning.std_initial; | ||||||
|  |   params[5] = tuning.gain_factor; | ||||||
|  |   auto result = write_command(i2c_command, params, 6); | ||||||
|  |   if (!result) { | ||||||
|  |     ESP_LOGE(TAG, "set tuning parameters failed. i2c command=%0xX, err=%d", i2c_command, this->last_error_); | ||||||
|  |   } | ||||||
|  |   return result; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | bool SEN5XComponent::write_temperature_compensation_(const TemperatureCompensation &compensation) { | ||||||
|  |   uint16_t params[3]; | ||||||
|  |   params[0] = compensation.offset; | ||||||
|  |   params[1] = compensation.normalized_offset_slope; | ||||||
|  |   params[2] = compensation.time_constant; | ||||||
|  |   if (!write_command(SEN5X_CMD_TEMPERATURE_COMPENSATION, params, 3)) { | ||||||
|  |     ESP_LOGE(TAG, "set temperature_compensation failed. Err=%d", this->last_error_); | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  |   return true; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | bool SEN5XComponent::start_fan_cleaning() { | ||||||
|  |   if (!write_command(SEN5X_CMD_START_CLEANING_FAN)) { | ||||||
|  |     this->status_set_warning(); | ||||||
|  |     ESP_LOGE(TAG, "write error start fan (%d)", this->last_error_); | ||||||
|  |     return false; | ||||||
|  |   } else { | ||||||
|  |     ESP_LOGD(TAG, "Fan auto clean started"); | ||||||
|  |   } | ||||||
|  |   return true; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace sen5x | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										128
									
								
								esphome/components/sen5x/sen5x.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								esphome/components/sen5x/sen5x.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,128 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include "esphome/core/component.h" | ||||||
|  | #include "esphome/components/sensor/sensor.h" | ||||||
|  | #include "esphome/components/sensirion_common/i2c_sensirion.h" | ||||||
|  | #include "esphome/core/application.h" | ||||||
|  | #include "esphome/core/preferences.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace sen5x { | ||||||
|  |  | ||||||
|  | enum ERRORCODE { | ||||||
|  |   COMMUNICATION_FAILED, | ||||||
|  |   SERIAL_NUMBER_IDENTIFICATION_FAILED, | ||||||
|  |   MEASUREMENT_INIT_FAILED, | ||||||
|  |   PRODUCT_NAME_FAILED, | ||||||
|  |   FIRMWARE_FAILED, | ||||||
|  |   UNKNOWN | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // Shortest time interval of 3H for storing baseline values. | ||||||
|  | // Prevents wear of the flash because of too many write operations | ||||||
|  | const uint32_t SHORTEST_BASELINE_STORE_INTERVAL = 10800; | ||||||
|  | // Store anyway if the baseline difference exceeds the max storage diff value | ||||||
|  | const uint32_t MAXIMUM_STORAGE_DIFF = 50; | ||||||
|  |  | ||||||
|  | struct Sen5xBaselines { | ||||||
|  |   int32_t state0; | ||||||
|  |   int32_t state1; | ||||||
|  | } PACKED;  // NOLINT | ||||||
|  |  | ||||||
|  | enum RhtAccelerationMode : uint16_t { LOW_ACCELERATION = 0, MEDIUM_ACCELERATION = 1, HIGH_ACCELERATION = 2 }; | ||||||
|  |  | ||||||
|  | struct GasTuning { | ||||||
|  |   uint16_t index_offset; | ||||||
|  |   uint16_t learning_time_offset_hours; | ||||||
|  |   uint16_t learning_time_gain_hours; | ||||||
|  |   uint16_t gating_max_duration_minutes; | ||||||
|  |   uint16_t std_initial; | ||||||
|  |   uint16_t gain_factor; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | struct TemperatureCompensation { | ||||||
|  |   uint16_t offset; | ||||||
|  |   uint16_t normalized_offset_slope; | ||||||
|  |   uint16_t time_constant; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | class SEN5XComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice { | ||||||
|  |  public: | ||||||
|  |   float get_setup_priority() const override { return setup_priority::DATA; } | ||||||
|  |   void setup() override; | ||||||
|  |   void dump_config() override; | ||||||
|  |   void update() override; | ||||||
|  |  | ||||||
|  |   enum Sen5xType { SEN50, SEN54, SEN55, UNKNOWN }; | ||||||
|  |  | ||||||
|  |   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_4_0_sensor(sensor::Sensor *pm_4_0) { pm_4_0_sensor_ = pm_4_0; } | ||||||
|  |   void set_pm_10_0_sensor(sensor::Sensor *pm_10_0) { pm_10_0_sensor_ = pm_10_0; } | ||||||
|  |  | ||||||
|  |   void set_voc_sensor(sensor::Sensor *voc_sensor) { voc_sensor_ = voc_sensor; } | ||||||
|  |   void set_nox_sensor(sensor::Sensor *nox_sensor) { nox_sensor_ = nox_sensor; } | ||||||
|  |   void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; } | ||||||
|  |   void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } | ||||||
|  |   void set_store_baseline(bool store_baseline) { store_baseline_ = store_baseline; } | ||||||
|  |   void set_acceleration_mode(RhtAccelerationMode mode) { acceleration_mode_ = mode; } | ||||||
|  |   void set_auto_cleaning_interval(uint32_t auto_cleaning_interval) { auto_cleaning_interval_ = auto_cleaning_interval; } | ||||||
|  |   void set_voc_algorithm_tuning(uint16_t index_offset, uint16_t learning_time_offset_hours, | ||||||
|  |                                 uint16_t learning_time_gain_hours, uint16_t gating_max_duration_minutes, | ||||||
|  |                                 uint16_t std_initial, uint16_t gain_factor) { | ||||||
|  |     voc_tuning_params_.value().index_offset = index_offset; | ||||||
|  |     voc_tuning_params_.value().learning_time_offset_hours = learning_time_offset_hours; | ||||||
|  |     voc_tuning_params_.value().learning_time_gain_hours = learning_time_gain_hours; | ||||||
|  |     voc_tuning_params_.value().gating_max_duration_minutes = gating_max_duration_minutes; | ||||||
|  |     voc_tuning_params_.value().std_initial = std_initial; | ||||||
|  |     voc_tuning_params_.value().gain_factor = gain_factor; | ||||||
|  |   } | ||||||
|  |   void set_nox_algorithm_tuning(uint16_t index_offset, uint16_t learning_time_offset_hours, | ||||||
|  |                                 uint16_t learning_time_gain_hours, uint16_t gating_max_duration_minutes, | ||||||
|  |                                 uint16_t gain_factor) { | ||||||
|  |     nox_tuning_params_.value().index_offset = index_offset; | ||||||
|  |     nox_tuning_params_.value().learning_time_offset_hours = learning_time_offset_hours; | ||||||
|  |     nox_tuning_params_.value().learning_time_gain_hours = learning_time_gain_hours; | ||||||
|  |     nox_tuning_params_.value().gating_max_duration_minutes = gating_max_duration_minutes; | ||||||
|  |     nox_tuning_params_.value().std_initial = 50; | ||||||
|  |     nox_tuning_params_.value().gain_factor = gain_factor; | ||||||
|  |   } | ||||||
|  |   void set_temperature_compensation(float offset, float normalized_offset_slope, uint16_t time_constant) { | ||||||
|  |     temperature_compensation_.value().offset = offset * 200; | ||||||
|  |     temperature_compensation_.value().normalized_offset_slope = normalized_offset_slope * 100; | ||||||
|  |     temperature_compensation_.value().time_constant = time_constant; | ||||||
|  |   } | ||||||
|  |   bool start_fan_cleaning(); | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   bool write_tuning_parameters_(uint16_t i2c_command, const GasTuning &tuning); | ||||||
|  |   bool write_temperature_compensation_(const TemperatureCompensation &compensation); | ||||||
|  |   ERRORCODE error_code_; | ||||||
|  |   bool initialized_{false}; | ||||||
|  |   sensor::Sensor *pm_1_0_sensor_{nullptr}; | ||||||
|  |   sensor::Sensor *pm_2_5_sensor_{nullptr}; | ||||||
|  |   sensor::Sensor *pm_4_0_sensor_{nullptr}; | ||||||
|  |   sensor::Sensor *pm_10_0_sensor_{nullptr}; | ||||||
|  |   // SEN54 and SEN55 only | ||||||
|  |   sensor::Sensor *temperature_sensor_{nullptr}; | ||||||
|  |   sensor::Sensor *humidity_sensor_{nullptr}; | ||||||
|  |   sensor::Sensor *voc_sensor_{nullptr}; | ||||||
|  |   // SEN55 only | ||||||
|  |   sensor::Sensor *nox_sensor_{nullptr}; | ||||||
|  |  | ||||||
|  |   std::string product_name_; | ||||||
|  |   uint8_t serial_number_[4]; | ||||||
|  |   uint16_t firmware_version_; | ||||||
|  |   Sen5xBaselines voc_baselines_storage_; | ||||||
|  |   bool store_baseline_; | ||||||
|  |   uint32_t seconds_since_last_store_; | ||||||
|  |   ESPPreferenceObject pref_; | ||||||
|  |   optional<RhtAccelerationMode> acceleration_mode_; | ||||||
|  |   optional<uint32_t> auto_cleaning_interval_; | ||||||
|  |   optional<GasTuning> voc_tuning_params_; | ||||||
|  |   optional<GasTuning> nox_tuning_params_; | ||||||
|  |   optional<TemperatureCompensation> temperature_compensation_; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace sen5x | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										241
									
								
								esphome/components/sen5x/sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										241
									
								
								esphome/components/sen5x/sensor.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,241 @@ | |||||||
|  | import esphome.codegen as cg | ||||||
|  | import esphome.config_validation as cv | ||||||
|  | from esphome.components import i2c, sensor, sensirion_common | ||||||
|  | from esphome import automation | ||||||
|  | from esphome.automation import maybe_simple_id | ||||||
|  |  | ||||||
|  | from esphome.const import ( | ||||||
|  |     CONF_HUMIDITY, | ||||||
|  |     CONF_ID, | ||||||
|  |     CONF_OFFSET, | ||||||
|  |     CONF_PM_1_0, | ||||||
|  |     CONF_PM_10_0, | ||||||
|  |     CONF_PM_2_5, | ||||||
|  |     CONF_PM_4_0, | ||||||
|  |     CONF_STORE_BASELINE, | ||||||
|  |     CONF_TEMPERATURE, | ||||||
|  |     DEVICE_CLASS_HUMIDITY, | ||||||
|  |     DEVICE_CLASS_NITROUS_OXIDE, | ||||||
|  |     DEVICE_CLASS_PM1, | ||||||
|  |     DEVICE_CLASS_PM10, | ||||||
|  |     DEVICE_CLASS_PM25, | ||||||
|  |     DEVICE_CLASS_TEMPERATURE, | ||||||
|  |     DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, | ||||||
|  |     ICON_CHEMICAL_WEAPON, | ||||||
|  |     ICON_RADIATOR, | ||||||
|  |     ICON_THERMOMETER, | ||||||
|  |     ICON_WATER_PERCENT, | ||||||
|  |     STATE_CLASS_MEASUREMENT, | ||||||
|  |     UNIT_CELSIUS, | ||||||
|  |     UNIT_MICROGRAMS_PER_CUBIC_METER, | ||||||
|  |     UNIT_PERCENT, | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | CODEOWNERS = ["@martgras"] | ||||||
|  | DEPENDENCIES = ["i2c"] | ||||||
|  | AUTO_LOAD = ["sensirion_common"] | ||||||
|  |  | ||||||
|  | sen5x_ns = cg.esphome_ns.namespace("sen5x") | ||||||
|  | SEN5XComponent = sen5x_ns.class_( | ||||||
|  |     "SEN5XComponent", cg.PollingComponent, sensirion_common.SensirionI2CDevice | ||||||
|  | ) | ||||||
|  | RhtAccelerationMode = sen5x_ns.enum("RhtAccelerationMode") | ||||||
|  |  | ||||||
|  | CONF_ACCELERATION_MODE = "acceleration_mode" | ||||||
|  | CONF_ALGORITHM_TUNING = "algorithm_tuning" | ||||||
|  | CONF_AUTO_CLEANING_INTERVAL = "auto_cleaning_interval" | ||||||
|  | CONF_GAIN_FACTOR = "gain_factor" | ||||||
|  | CONF_GATING_MAX_DURATION_MINUTES = "gating_max_duration_minutes" | ||||||
|  | CONF_INDEX_OFFSET = "index_offset" | ||||||
|  | CONF_LEARNING_TIME_GAIN_HOURS = "learning_time_gain_hours" | ||||||
|  | CONF_LEARNING_TIME_OFFSET_HOURS = "learning_time_offset_hours" | ||||||
|  | CONF_NORMALIZED_OFFSET_SLOPE = "normalized_offset_slope" | ||||||
|  | CONF_NOX = "nox" | ||||||
|  | CONF_STD_INITIAL = "std_initial" | ||||||
|  | CONF_TEMPERATURE_COMPENSATION = "temperature_compensation" | ||||||
|  | CONF_TIME_CONSTANT = "time_constant" | ||||||
|  | CONF_VOC = "voc" | ||||||
|  | CONF_VOC_BASELINE = "voc_baseline" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # Actions | ||||||
|  | StartFanAction = sen5x_ns.class_("StartFanAction", automation.Action) | ||||||
|  |  | ||||||
|  | ACCELERATION_MODES = { | ||||||
|  |     "low": RhtAccelerationMode.LOW_ACCELERATION, | ||||||
|  |     "medium": RhtAccelerationMode.MEDIUM_ACCELERATION, | ||||||
|  |     "high": RhtAccelerationMode.HIGH_ACCELERATION, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | GAS_SENSOR = cv.Schema( | ||||||
|  |     { | ||||||
|  |         cv.Optional(CONF_ALGORITHM_TUNING): cv.Schema( | ||||||
|  |             { | ||||||
|  |                 cv.Optional(CONF_INDEX_OFFSET, default=100): cv.int_range(1, 250), | ||||||
|  |                 cv.Optional(CONF_LEARNING_TIME_OFFSET_HOURS, default=12): cv.int_range( | ||||||
|  |                     1, 1000 | ||||||
|  |                 ), | ||||||
|  |                 cv.Optional(CONF_LEARNING_TIME_GAIN_HOURS, default=12): cv.int_range( | ||||||
|  |                     1, 1000 | ||||||
|  |                 ), | ||||||
|  |                 cv.Optional( | ||||||
|  |                     CONF_GATING_MAX_DURATION_MINUTES, default=720 | ||||||
|  |                 ): cv.int_range(0, 3000), | ||||||
|  |                 cv.Optional(CONF_STD_INITIAL, default=50): cv.int_, | ||||||
|  |                 cv.Optional(CONF_GAIN_FACTOR, default=230): cv.int_range(1, 1000), | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | CONFIG_SCHEMA = ( | ||||||
|  |     cv.Schema( | ||||||
|  |         { | ||||||
|  |             cv.GenerateID(): cv.declare_id(SEN5XComponent), | ||||||
|  |             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_4_0): sensor.sensor_schema( | ||||||
|  |                 unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, | ||||||
|  |                 icon=ICON_CHEMICAL_WEAPON, | ||||||
|  |                 accuracy_decimals=2, | ||||||
|  |                 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_AUTO_CLEANING_INTERVAL): cv.time_period_in_seconds_, | ||||||
|  |             cv.Optional(CONF_VOC): sensor.sensor_schema( | ||||||
|  |                 icon=ICON_RADIATOR, | ||||||
|  |                 accuracy_decimals=0, | ||||||
|  |                 device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, | ||||||
|  |                 state_class=STATE_CLASS_MEASUREMENT, | ||||||
|  |             ).extend(GAS_SENSOR), | ||||||
|  |             cv.Optional(CONF_NOX): sensor.sensor_schema( | ||||||
|  |                 icon=ICON_RADIATOR, | ||||||
|  |                 accuracy_decimals=0, | ||||||
|  |                 device_class=DEVICE_CLASS_NITROUS_OXIDE, | ||||||
|  |                 state_class=STATE_CLASS_MEASUREMENT, | ||||||
|  |             ).extend(GAS_SENSOR), | ||||||
|  |             cv.Optional(CONF_STORE_BASELINE, default=True): cv.boolean, | ||||||
|  |             cv.Optional(CONF_VOC_BASELINE): cv.hex_uint16_t, | ||||||
|  |             cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( | ||||||
|  |                 unit_of_measurement=UNIT_CELSIUS, | ||||||
|  |                 icon=ICON_THERMOMETER, | ||||||
|  |                 accuracy_decimals=2, | ||||||
|  |                 device_class=DEVICE_CLASS_TEMPERATURE, | ||||||
|  |                 state_class=STATE_CLASS_MEASUREMENT, | ||||||
|  |             ), | ||||||
|  |             cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( | ||||||
|  |                 unit_of_measurement=UNIT_PERCENT, | ||||||
|  |                 icon=ICON_WATER_PERCENT, | ||||||
|  |                 accuracy_decimals=2, | ||||||
|  |                 device_class=DEVICE_CLASS_HUMIDITY, | ||||||
|  |                 state_class=STATE_CLASS_MEASUREMENT, | ||||||
|  |             ), | ||||||
|  |             cv.Optional(CONF_TEMPERATURE_COMPENSATION): cv.Schema( | ||||||
|  |                 { | ||||||
|  |                     cv.Optional(CONF_OFFSET, default=0): cv.float_, | ||||||
|  |                     cv.Optional(CONF_NORMALIZED_OFFSET_SLOPE, default=0): cv.percentage, | ||||||
|  |                     cv.Optional(CONF_TIME_CONSTANT, default=0): cv.int_, | ||||||
|  |                 } | ||||||
|  |             ), | ||||||
|  |             cv.Optional(CONF_ACCELERATION_MODE): cv.enum(ACCELERATION_MODES), | ||||||
|  |         } | ||||||
|  |     ) | ||||||
|  |     .extend(cv.polling_component_schema("60s")) | ||||||
|  |     .extend(i2c.i2c_device_schema(0x69)) | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | SENSOR_MAP = { | ||||||
|  |     CONF_PM_1_0: "set_pm_1_0_sensor", | ||||||
|  |     CONF_PM_2_5: "set_pm_2_5_sensor", | ||||||
|  |     CONF_PM_4_0: "set_pm_4_0_sensor", | ||||||
|  |     CONF_PM_10_0: "set_pm_10_0_sensor", | ||||||
|  |     CONF_VOC: "set_voc_sensor", | ||||||
|  |     CONF_NOX: "set_nox_sensor", | ||||||
|  |     CONF_TEMPERATURE: "set_temperature_sensor", | ||||||
|  |     CONF_HUMIDITY: "set_humidity_sensor", | ||||||
|  | } | ||||||
|  |  | ||||||
|  | SETTING_MAP = { | ||||||
|  |     CONF_AUTO_CLEANING_INTERVAL: "set_auto_cleaning_interval", | ||||||
|  |     CONF_ACCELERATION_MODE: "set_acceleration_mode", | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def to_code(config): | ||||||
|  |     var = cg.new_Pvariable(config[CONF_ID]) | ||||||
|  |     await cg.register_component(var, config) | ||||||
|  |     await i2c.register_i2c_device(var, config) | ||||||
|  |  | ||||||
|  |     for key, funcName in SETTING_MAP.items(): | ||||||
|  |         if key in config: | ||||||
|  |             cg.add(getattr(var, funcName)(config[key])) | ||||||
|  |  | ||||||
|  |     for key, funcName in SENSOR_MAP.items(): | ||||||
|  |         if key in config: | ||||||
|  |             sens = await sensor.new_sensor(config[key]) | ||||||
|  |             cg.add(getattr(var, funcName)(sens)) | ||||||
|  |  | ||||||
|  |     if CONF_VOC in config and CONF_ALGORITHM_TUNING in config[CONF_VOC]: | ||||||
|  |         cfg = config[CONF_VOC][CONF_ALGORITHM_TUNING] | ||||||
|  |         cg.add( | ||||||
|  |             var.set_voc_algorithm_tuning( | ||||||
|  |                 cfg[CONF_INDEX_OFFSET], | ||||||
|  |                 cfg[CONF_LEARNING_TIME_OFFSET_HOURS], | ||||||
|  |                 cfg[CONF_LEARNING_TIME_GAIN_HOURS], | ||||||
|  |                 cfg[CONF_GATING_MAX_DURATION_MINUTES], | ||||||
|  |                 cfg[CONF_STD_INITIAL], | ||||||
|  |                 cfg[CONF_GAIN_FACTOR], | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  |     if CONF_NOX in config and CONF_ALGORITHM_TUNING in config[CONF_NOX]: | ||||||
|  |         cfg = config[CONF_NOX][CONF_ALGORITHM_TUNING] | ||||||
|  |         cg.add( | ||||||
|  |             var.set_nox_algorithm_tuning( | ||||||
|  |                 cfg[CONF_INDEX_OFFSET], | ||||||
|  |                 cfg[CONF_LEARNING_TIME_OFFSET_HOURS], | ||||||
|  |                 cfg[CONF_LEARNING_TIME_GAIN_HOURS], | ||||||
|  |                 cfg[CONF_GATING_MAX_DURATION_MINUTES], | ||||||
|  |                 cfg[CONF_GAIN_FACTOR], | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  |     if CONF_TEMPERATURE_COMPENSATION in config: | ||||||
|  |         cg.add( | ||||||
|  |             var.set_temperature_compensation( | ||||||
|  |                 config[CONF_TEMPERATURE_COMPENSATION][CONF_OFFSET], | ||||||
|  |                 config[CONF_TEMPERATURE_COMPENSATION][CONF_NORMALIZED_OFFSET_SLOPE], | ||||||
|  |                 config[CONF_TEMPERATURE_COMPENSATION][CONF_TIME_CONSTANT], | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | SEN5X_ACTION_SCHEMA = maybe_simple_id( | ||||||
|  |     { | ||||||
|  |         cv.Required(CONF_ID): cv.use_id(SEN5XComponent), | ||||||
|  |     } | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @automation.register_action( | ||||||
|  |     "sen5x.start_fan_autoclean", StartFanAction, SEN5X_ACTION_SCHEMA | ||||||
|  | ) | ||||||
|  | async def sen54_fan_to_code(config, action_id, template_arg, args): | ||||||
|  |     paren = await cg.get_variable(config[CONF_ID]) | ||||||
|  |     return cg.new_Pvariable(action_id, template_arg, paren) | ||||||
| @@ -285,6 +285,49 @@ sensor: | |||||||
|     address: 0x77 |     address: 0x77 | ||||||
|     iir_filter: 2X |     iir_filter: 2X | ||||||
|  |  | ||||||
|  |   - platform: sen5x | ||||||
|  |     id: sen54 | ||||||
|  |     temperature: | ||||||
|  |       name: "Temperature" | ||||||
|  |       accuracy_decimals: 1 | ||||||
|  |     humidity: | ||||||
|  |       name: "Humidity" | ||||||
|  |       accuracy_decimals: 0 | ||||||
|  |     pm_1_0: | ||||||
|  |       name: " PM <1µm Weight concentration" | ||||||
|  |       id: pm_1_0 | ||||||
|  |       accuracy_decimals: 1 | ||||||
|  |     pm_2_5: | ||||||
|  |       name: " PM <2.5µm Weight concentration" | ||||||
|  |       id: pm_2_5 | ||||||
|  |       accuracy_decimals: 1 | ||||||
|  |     pm_4_0: | ||||||
|  |       name: " PM <4µm Weight concentration" | ||||||
|  |       id: pm_4_0 | ||||||
|  |       accuracy_decimals: 1 | ||||||
|  |     pm_10_0: | ||||||
|  |       name: " PM <10µm Weight concentration" | ||||||
|  |       id: pm_10_0 | ||||||
|  |       accuracy_decimals: 1 | ||||||
|  |     nox: | ||||||
|  |       name: "NOx" | ||||||
|  |     voc: | ||||||
|  |       name: "VOC" | ||||||
|  |       algorithm_tuning: | ||||||
|  |         index_offset: 100 | ||||||
|  |         learning_time_offset_hours: 12 | ||||||
|  |         learning_time_gain_hours: 12 | ||||||
|  |         gating_max_duration_minutes: 180 | ||||||
|  |         std_initial: 50 | ||||||
|  |         gain_factor: 230 | ||||||
|  |     temperature_compensation: | ||||||
|  |       offset: 0 | ||||||
|  |       normalized_offset_slope: 0 | ||||||
|  |       time_constant: 0 | ||||||
|  |     acceleration_mode: low | ||||||
|  |     store_baseline: true | ||||||
|  |     address: 0x69 | ||||||
|  |  | ||||||
| script: | script: | ||||||
|   - id: automation_test |   - id: automation_test | ||||||
|     then: |     then: | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user