Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

sen5x: add TPS & PM number concentration #6694

Open
wants to merge 8 commits into
base: dev
Choose a base branch
from
155 changes: 125 additions & 30 deletions esphome/components/sen5x/sen5x.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ 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_READ_PM_MEASUREMENT = 0x0413;
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;
Expand Down Expand Up @@ -126,14 +127,16 @@ void SEN5XComponent::setup() {
this->nox_sensor_ = nullptr; // mark as not used
}

if (!this->get_register(SEN5X_CMD_GET_FIRMWARE_VERSION, this->firmware_version_, 20)) {
uint16_t firmware_version;
if (!this->get_register(SEN5X_CMD_GET_FIRMWARE_VERSION, 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_);
this->firmware_version_major_ = firmware_version >> 8;
this->firmware_version_minor_ = firmware_version & 0xFF;
ESP_LOGD(TAG, "Firmware version %u.%u", this->firmware_version_major_, this->firmware_version_minor_);

if (this->voc_sensor_ && this->store_baseline_) {
uint32_t combined_serial =
Expand Down Expand Up @@ -215,9 +218,28 @@ void SEN5XComponent::setup() {
delay(20);
}

if ((this->pm_n_0_5_sensor_ || this->pm_n_1_0_sensor_ || this->pm_n_2_5_sensor_ || this->pm_n_4_0_sensor_ ||
this->pm_n_10_0_sensor_ || this->pm_tps_sensor_)) {
if (this->firmware_version_major_ > 0 || this->firmware_version_minor_ > 7) {
this->get_pm_number_concentration_and_tps_ = true;
} else {
ESP_LOGE(TAG, "For number concentration and TPS, firmware >0.7 is required. You are using <%u.%u>",
this->firmware_version_major_, this->firmware_version_minor_);
this->pm_n_0_5_sensor_ = nullptr;
this->pm_n_1_0_sensor_ = nullptr;
this->pm_n_2_5_sensor_ = nullptr;
this->pm_n_4_0_sensor_ = nullptr;
this->pm_n_10_0_sensor_ = nullptr;
this->pm_tps_sensor_ = nullptr;
this->get_pm_number_concentration_and_tps_ = false;
}
}

// 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 (this->pm_1_0_sensor_ || this->pm_2_5_sensor_ || this->pm_4_0_sensor_ || this->pm_10_0_sensor_ ||
this->pm_n_0_5_sensor_ || this->pm_n_1_0_sensor_ || this->pm_n_2_5_sensor_ || this->pm_n_4_0_sensor_ ||
this->pm_n_10_0_sensor_ || this->pm_tps_sensor_) {
// if any of the gas sensors are active we need a full measurement
cmd = SEN5X_CMD_START_MEASUREMENTS;
}
Expand Down Expand Up @@ -260,7 +282,7 @@ void SEN5XComponent::dump_config() {
}
}
ESP_LOGCONFIG(TAG, " Productname: %s", this->product_name_.c_str());
ESP_LOGCONFIG(TAG, " Firmware version: %d", this->firmware_version_);
ESP_LOGCONFIG(TAG, " Firmware version: %u.%u", this->firmware_version_major_, this->firmware_version_minor_);
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 cleaning interval %" PRId32 " seconds", auto_cleaning_interval_.value());
Expand All @@ -283,6 +305,12 @@ void SEN5XComponent::dump_config() {
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(" ", "PM_N 0.5", this->pm_n_0_5_sensor_);
LOG_SENSOR(" ", "PM_N 1.0", this->pm_n_1_0_sensor_);
LOG_SENSOR(" ", "PM_N 2.5", this->pm_n_2_5_sensor_);
LOG_SENSOR(" ", "PM_N 4.0", this->pm_n_4_0_sensor_);
LOG_SENSOR(" ", "PM_N 10.0", this->pm_n_10_0_sensor_);
LOG_SENSOR(" ", "PM_TPS", this->pm_tps_sensor_);
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
LOG_SENSOR(" ", "VOC", this->voc_sensor_); // SEN54 and SEN55 only
Expand All @@ -297,38 +325,104 @@ void SEN5XComponent::update() {
// 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%04" PRIX32 " ,state1: 0x%04" PRIX32,
this->voc_baselines_storage_.state0, voc_baselines_storage_.state1);
} else {
ESP_LOGW(TAG, "Could not store VOC baselines");
}
this->write_voc_baseline_();
}

this->update_measured_values_();

if (this->get_pm_number_concentration_and_tps_) {
// delay the extra reading to allow update_measured_values to complete.
this->set_timeout(30, [this]() { this->update_measured_pm_(); });
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no guarantee that this will be enough time. Ideally the component would be changes to have a small state machine and run through the stage one at a time in loop once triggered by the update function.

Copy link
Contributor Author

@CodeInPolish CodeInPolish May 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's very true. Wouldn't a loop in the update function block the main event loop though?

Another approach I was testing was to perform only one reading (either pm with rh/t/nox/vox or full pm) per update.
If get_pm_number_concentration_and_tps_ was true, we would need to halve the user specified update_interval in the setup with set_update_interval and while it was reporting the correct update interval when dumping the config, the sensor was still exporting entities with the original period (i.e. halving it didn't affect the actual period between update calls).

I brainstormed a little and came up with another solution:

  • use an FSM with 3 states: idle, update_measured_values_progress, update_measured_values_done
  • set the idle, update_measured_values_progress and update_measured_values_done accordingly
  • create a small helper function to determine the current state of the FSM. This would be responsible for caling itself with a this->set_timeout(10) if the state isn't update_measured_values_done
  • Finally, trigger update_measured_pm_() function (from the helper function) when the FSM is in update_measured_values_done state and reset the FSM to idle in update_measured_pm_()

This would ensure update_measured_values_() has had enough time to complete without blocking the main event loop with a loop. What are your thoughts on this?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, in the loop state machine you can check the current time against the start time of the last state change and when enough time has passed, you read the data and publish. No delays and timeouts are needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jesserockz thank you. I misunderstood what you said earlier (and also didn't know loop was still triggered for PollingComponent)

I went ahead and implemented the brainstormed solution, because it's less code and less complicated than a full blown FSM for all the tasks to perform. If that's not an acceptable solution, let me know.


void SEN5XComponent::write_voc_baseline_() {
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%04" PRIX32 " ,state1: 0x%04" PRIX32,
this->voc_baselines_storage_.state0, voc_baselines_storage_.state1);
} else {
ESP_LOGW(TAG, "Could not store VOC baselines");
}
}
});
}
}
});
}
}

void SEN5XComponent::update_measured_pm_() {
if (!this->write_command(SEN5X_CMD_READ_PM_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[10];

if (!this->read_data(measurements, 10)) {
this->status_set_warning();
ESP_LOGD(TAG, "read data error (%d)", this->last_error_);
return;
}

float pm_n_0_5 = measurements[4] / 10.0;
if (measurements[4] == 0xFFFF)
pm_n_0_5 = NAN;
CodeInPolish marked this conversation as resolved.
Show resolved Hide resolved
float pm_n_1_0 = measurements[5] / 10.0;
if (measurements[5] == 0xFFFF)
pm_n_1_0 = NAN;
float pm_n_2_5 = measurements[6] / 10.0;
if (measurements[6] == 0xFFFF)
pm_n_2_5 = NAN;
float pm_n_4_0 = measurements[7] / 10.0;
if (measurements[7] == 0xFFFF)
pm_n_4_0 = NAN;
float pm_n_10_0 = measurements[8] / 10.0;
if (measurements[8] == 0xFFFF)
pm_n_10_0 = NAN;
float pm_tps = measurements[9] / 1000.0;
if (measurements[9] == 0xFFFF)
pm_tps = NAN;

if (this->pm_n_0_5_sensor_ != nullptr)
this->pm_n_0_5_sensor_->publish_state(pm_n_0_5);
if (this->pm_n_1_0_sensor_ != nullptr)
this->pm_n_1_0_sensor_->publish_state(pm_n_1_0);
if (this->pm_n_2_5_sensor_ != nullptr)
this->pm_n_2_5_sensor_->publish_state(pm_n_2_5);
if (this->pm_n_4_0_sensor_ != nullptr)
this->pm_n_4_0_sensor_->publish_state(pm_n_4_0);
if (this->pm_n_10_0_sensor_ != nullptr)
this->pm_n_10_0_sensor_->publish_state(pm_n_10_0);
if (this->pm_tps_sensor_ != nullptr)
this->pm_tps_sensor_->publish_state(pm_tps);

this->status_clear_warning();
});
}

void SEN5XComponent::update_measured_values_() {
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];

Expand All @@ -337,6 +431,7 @@ void SEN5XComponent::update() {
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;
Expand All @@ -350,10 +445,10 @@ void SEN5XComponent::update() {
if (measurements[3] == 0xFFFF)
pm_10_0 = NAN;
float humidity = measurements[4] / 100.0;
if (measurements[4] == 0xFFFF)
if (measurements[4] >= 0x7FFF)
humidity = NAN;
float temperature = (int16_t) measurements[5] / 200.0;
if (measurements[5] == 0xFFFF)
if (measurements[5] >= 0x7FFF)
temperature = NAN;
float voc = measurements[6] / 10.0;
if (measurements[6] == 0xFFFF)
Expand Down
22 changes: 21 additions & 1 deletion esphome/components/sen5x/sen5x.h
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,13 @@ class SEN5XComponent : public PollingComponent, public sensirion_common::Sensiri
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_pm_n_0_5_sensor(sensor::Sensor *pm_n_0_5) { pm_n_0_5_sensor_ = pm_n_0_5; }
void set_pm_n_1_0_sensor(sensor::Sensor *pm_n_1_0) { pm_n_1_0_sensor_ = pm_n_1_0; }
void set_pm_n_2_5_sensor(sensor::Sensor *pm_n_2_5) { pm_n_2_5_sensor_ = pm_n_2_5; }
void set_pm_n_4_0_sensor(sensor::Sensor *pm_n_4_0) { pm_n_4_0_sensor_ = pm_n_4_0; }
void set_pm_n_10_0_sensor(sensor::Sensor *pm_n_10_0) { pm_n_10_0_sensor_ = pm_n_10_0; }
void set_pm_tps_sensor(sensor::Sensor *pm_tps) { pm_tps_sensor_ = pm_tps; }

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; }
Expand Down Expand Up @@ -103,12 +110,22 @@ class SEN5XComponent : public PollingComponent, public sensirion_common::Sensiri
protected:
bool write_tuning_parameters_(uint16_t i2c_command, const GasTuning &tuning);
bool write_temperature_compensation_(const TemperatureCompensation &compensation);
void write_voc_baseline_();
void update_measured_values_();
void update_measured_pm_();
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};
// Firmware >0.7 only
sensor::Sensor *pm_n_0_5_sensor_{nullptr};
sensor::Sensor *pm_n_1_0_sensor_{nullptr};
sensor::Sensor *pm_n_2_5_sensor_{nullptr};
sensor::Sensor *pm_n_4_0_sensor_{nullptr};
sensor::Sensor *pm_n_10_0_sensor_{nullptr};
sensor::Sensor *pm_tps_sensor_{nullptr};
// SEN54 and SEN55 only
sensor::Sensor *temperature_sensor_{nullptr};
sensor::Sensor *humidity_sensor_{nullptr};
Expand All @@ -118,7 +135,8 @@ class SEN5XComponent : public PollingComponent, public sensirion_common::Sensiri

std::string product_name_;
uint8_t serial_number_[4];
uint16_t firmware_version_;
uint8_t firmware_version_major_;
uint8_t firmware_version_minor_;
Sen5xBaselines voc_baselines_storage_;
bool store_baseline_;
uint32_t seconds_since_last_store_;
Expand All @@ -128,6 +146,8 @@ class SEN5XComponent : public PollingComponent, public sensirion_common::Sensiri
optional<GasTuning> voc_tuning_params_;
optional<GasTuning> nox_tuning_params_;
optional<TemperatureCompensation> temperature_compensation_;
// Driver state variables
bool get_pm_number_concentration_and_tps_;
};

} // namespace sen5x
Expand Down
49 changes: 49 additions & 0 deletions esphome/components/sen5x/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
UNIT_MICROGRAMS_PER_CUBIC_METER,
UNIT_MICROMETER,
UNIT_PERCENT,
)

Expand All @@ -40,6 +41,12 @@
)
RhtAccelerationMode = sen5x_ns.enum("RhtAccelerationMode")

CONF_PM_N_0_5 = "pm_n_0_5"
CONF_PM_N_1_0 = "pm_n_1_0"
CONF_PM_N_2_5 = "pm_n_2_5"
CONF_PM_N_4_0 = "pm_n_4_0"
CONF_PM_N_10_0 = "pm_n_10_0"
CONF_PM_TPS = "pm_tps"
CONF_ACCELERATION_MODE = "acceleration_mode"
CONF_ALGORITHM_TUNING = "algorithm_tuning"
CONF_AUTO_CLEANING_INTERVAL = "auto_cleaning_interval"
Expand Down Expand Up @@ -127,6 +134,42 @@ def float_previously_pct(value):
device_class=DEVICE_CLASS_PM10,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_PM_N_0_5): sensor.sensor_schema(
unit_of_measurement="#/cm³",
icon=ICON_CHEMICAL_WEAPON,
accuracy_decimals=2,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_PM_N_1_0): sensor.sensor_schema(
unit_of_measurement="#/cm³",
icon=ICON_CHEMICAL_WEAPON,
accuracy_decimals=2,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_PM_N_2_5): sensor.sensor_schema(
unit_of_measurement="#/cm³",
icon=ICON_CHEMICAL_WEAPON,
accuracy_decimals=2,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_PM_N_4_0): sensor.sensor_schema(
unit_of_measurement="#/cm³",
icon=ICON_CHEMICAL_WEAPON,
accuracy_decimals=2,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_PM_N_10_0): sensor.sensor_schema(
unit_of_measurement="#/cm³",
icon=ICON_CHEMICAL_WEAPON,
accuracy_decimals=2,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_PM_TPS): sensor.sensor_schema(
unit_of_measurement=UNIT_MICROMETER,
icon=ICON_CHEMICAL_WEAPON,
accuracy_decimals=2,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_AUTO_CLEANING_INTERVAL): cv.update_interval,
cv.Optional(CONF_VOC): sensor.sensor_schema(
icon=ICON_RADIATOR,
Expand Down Expand Up @@ -177,6 +220,12 @@ def float_previously_pct(value):
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_PM_N_0_5: "set_pm_n_0_5_sensor",
CONF_PM_N_1_0: "set_pm_n_1_0_sensor",
CONF_PM_N_2_5: "set_pm_n_2_5_sensor",
CONF_PM_N_4_0: "set_pm_n_4_0_sensor",
CONF_PM_N_10_0: "set_pm_n_10_0_sensor",
CONF_PM_TPS: "set_pm_tps_sensor",
CONF_VOC: "set_voc_sensor",
CONF_NOX: "set_nox_sensor",
CONF_TEMPERATURE: "set_temperature_sensor",
Expand Down
Loading
Loading