add support for Sen5x sensor series (#3383)

Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
Martin 2022-05-10 10:15:02 +02:00 committed by GitHub
parent 86b52df839
commit 0e547390da
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 847 additions and 0 deletions

View File

@ -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

View File

View 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

View 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

View 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

View 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)

View File

@ -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: