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/selec_meter/* @sourabhjaiswal | ||||
| esphome/components/select/* @esphome/core | ||||
| esphome/components/sen5x/* @martgras | ||||
| esphome/components/sensirion_common/* @martgras | ||||
| esphome/components/sensor/* @esphome/core | ||||
| 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 | ||||
|     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: | ||||
|   - id: automation_test | ||||
|     then: | ||||
|   | ||||
		Reference in New Issue
	
	Block a user