diff --git a/CODEOWNERS b/CODEOWNERS index 16b9008379..e2b29547cb 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -168,7 +168,7 @@ esphome/components/rf_bridge/* @jesserockz esphome/components/rgbct/* @jesserockz esphome/components/rtttl/* @glmnet esphome/components/safe_mode/* @jsuanet @paulmonigatti -esphome/components/scd4x/* @sjtrny +esphome/components/scd4x/* @martgras @sjtrny esphome/components/script/* @esphome/core esphome/components/sdm_meter/* @jesserockz @polyfaces esphome/components/sdp3x/* @Azimath diff --git a/esphome/components/scd4x/automation.h b/esphome/components/scd4x/automation.h new file mode 100644 index 0000000000..21ecb2ea4c --- /dev/null +++ b/esphome/components/scd4x/automation.h @@ -0,0 +1,28 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "scd4x.h" + +namespace esphome { +namespace scd4x { + +template class PerformForcedCalibrationAction : public Action, public Parented { + public: + void play(Ts... x) override { + if (this->value_.has_value()) { + this->parent_->perform_forced_calibration(value_.value()); + } + } + + protected: + TEMPLATABLE_VALUE(uint16_t, value) +}; + +template class FactoryResetAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->factory_reset(); } +}; + +} // namespace scd4x +} // namespace esphome diff --git a/esphome/components/scd4x/scd4x.cpp b/esphome/components/scd4x/scd4x.cpp index 559c95df32..cbda996a4c 100644 --- a/esphome/components/scd4x/scd4x.cpp +++ b/esphome/components/scd4x/scd4x.cpp @@ -13,39 +13,32 @@ static const uint16_t SCD4X_CMD_ALTITUDE_COMPENSATION = 0x2427; static const uint16_t SCD4X_CMD_AMBIENT_PRESSURE_COMPENSATION = 0xe000; static const uint16_t SCD4X_CMD_AUTOMATIC_SELF_CALIBRATION = 0x2416; static const uint16_t SCD4X_CMD_START_CONTINUOUS_MEASUREMENTS = 0x21b1; +static const uint16_t SCD4X_CMD_START_LOW_POWER_CONTINUOUS_MEASUREMENTS = 0x21ac; +static const uint16_t SCD4X_CMD_START_LOW_POWER_SINGLE_SHOT = 0x219d; // SCD41 only +static const uint16_t SCD4X_CMD_START_LOW_POWER_SINGLE_SHOT_RHT_ONLY = 0x2196; static const uint16_t SCD4X_CMD_GET_DATA_READY_STATUS = 0xe4b8; static const uint16_t SCD4X_CMD_READ_MEASUREMENT = 0xec05; static const uint16_t SCD4X_CMD_PERFORM_FORCED_CALIBRATION = 0x362f; static const uint16_t SCD4X_CMD_STOP_MEASUREMENTS = 0x3f86; - +static const uint16_t SCD4X_CMD_FACTORY_RESET = 0x3632; +static const uint16_t SCD4X_CMD_GET_FEATURESET = 0x202f; static const float SCD4X_TEMPERATURE_OFFSET_MULTIPLIER = (1 << 16) / 175.0f; +static const uint16_t SCD41_ID = 0x1408; +static const uint16_t SCD40_ID = 0x440; void SCD4XComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up scd4x..."); - // the sensor needs 1000 ms to enter the idle state this->set_timeout(1000, [this]() { - uint16_t raw_read_status; - if (!this->get_register(SCD4X_CMD_GET_DATA_READY_STATUS, raw_read_status)) { - ESP_LOGE(TAG, "Failed to read data ready status"); + this->status_clear_error(); + if (!this->write_command(SCD4X_CMD_STOP_MEASUREMENTS)) { + ESP_LOGE(TAG, "Failed to stop measurements"); 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(SCD4X_CMD_STOP_MEASUREMENTS)) { - ESP_LOGE(TAG, "Failed to stop measurements"); - this->mark_failed(); - return; - } - // According to the SCD4x datasheet the sensor will only respond to other commands after waiting 500 ms after - // issuing the stop_periodic_measurement command - stop_measurement_delay = 500; - } - this->set_timeout(stop_measurement_delay, [this]() { + // According to the SCD4x datasheet the sensor will only respond to other commands after waiting 500 ms after + // issuing the stop_periodic_measurement command + this->set_timeout(500, [this]() { uint16_t raw_serial_number[3]; if (!this->get_register(SCD4X_CMD_GET_SERIAL_NUMBER, raw_serial_number, 3, 1)) { ESP_LOGE(TAG, "Failed to read serial number"); @@ -89,15 +82,9 @@ void SCD4XComponent::setup() { return; } - // Finally start sensor measurements - if (!this->write_command(SCD4X_CMD_START_CONTINUOUS_MEASUREMENTS)) { - ESP_LOGE(TAG, "Error starting continuous measurements."); - this->error_code_ = MEASUREMENT_INIT_FAILED; - this->mark_failed(); - return; - } - initialized_ = true; + // Finally start sensor measurements + this->start_measurement_(); ESP_LOGD(TAG, "Sensor initialized"); }); }); @@ -123,12 +110,31 @@ void SCD4XComponent::dump_config() { } } ESP_LOGCONFIG(TAG, " Automatic self calibration: %s", ONOFF(this->enable_asc_)); - if (this->ambient_pressure_compensation_) { - ESP_LOGCONFIG(TAG, " Altitude compensation disabled"); - ESP_LOGCONFIG(TAG, " Ambient pressure compensation: %dmBar", this->ambient_pressure_); + if (this->ambient_pressure_source_ != nullptr) { + ESP_LOGCONFIG(TAG, " Dynamic ambient pressure compensation using sensor '%s'", + this->ambient_pressure_source_->get_name().c_str()); } else { - ESP_LOGCONFIG(TAG, " Ambient pressure compensation disabled"); - ESP_LOGCONFIG(TAG, " Altitude compensation: %dm", this->altitude_compensation_); + if (this->ambient_pressure_compensation_) { + ESP_LOGCONFIG(TAG, " Altitude compensation disabled"); + ESP_LOGCONFIG(TAG, " Ambient pressure compensation: %dmBar", this->ambient_pressure_); + } else { + ESP_LOGCONFIG(TAG, " Ambient pressure compensation disabled"); + ESP_LOGCONFIG(TAG, " Altitude compensation: %dm", this->altitude_compensation_); + } + } + switch (this->measurement_mode_) { + case PERIODIC: + ESP_LOGCONFIG(TAG, " Measurement mode: periodic (5s)"); + break; + case LOW_POWER_PERIODIC: + ESP_LOGCONFIG(TAG, " Measurement mode: low power periodic (30s)"); + break; + case SINGLE_SHOT: + ESP_LOGCONFIG(TAG, " Measurement mode: single shot"); + break; + case SINGLE_SHOT_RHT_ONLY: + ESP_LOGCONFIG(TAG, " Measurement mode: single shot rht only"); + break; } ESP_LOGCONFIG(TAG, " Temperature offset: %.2f °C", this->temperature_offset_); LOG_UPDATE_INTERVAL(this); @@ -149,47 +155,105 @@ void SCD4XComponent::update() { } } - // Check if data is ready - if (!this->write_command(SCD4X_CMD_GET_DATA_READY_STATUS)) { - this->status_set_warning(); - return; + uint32_t wait_time = 0; + if (this->measurement_mode_ == SINGLE_SHOT || this->measurement_mode_ == SINGLE_SHOT_RHT_ONLY) { + start_measurement_(); + wait_time = + this->measurement_mode_ == SINGLE_SHOT ? 5000 : 50; // Single shot measurement takes 5 secs rht mode 50 ms } + this->set_timeout(wait_time, [this]() { + // Check if data is ready + if (!this->write_command(SCD4X_CMD_GET_DATA_READY_STATUS)) { + this->status_set_warning(); + return; + } - uint16_t raw_read_status; - if (!this->read_data(raw_read_status) || raw_read_status == 0x00) { - this->status_set_warning(); - ESP_LOGW(TAG, "Data not ready yet!"); - return; - } + uint16_t raw_read_status; - if (!this->write_command(SCD4X_CMD_READ_MEASUREMENT)) { - ESP_LOGW(TAG, "Error reading measurement!"); - this->status_set_warning(); - return; - } + if (!this->read_data(raw_read_status) || raw_read_status == 0x00) { + this->status_set_warning(); + ESP_LOGW(TAG, "Data not ready yet!"); + return; + } - // Read off sensor data - uint16_t raw_data[3]; - if (!this->read_data(raw_data, 3)) { - this->status_set_warning(); - return; - } + if (!this->write_command(SCD4X_CMD_READ_MEASUREMENT)) { + ESP_LOGW(TAG, "Error reading measurement!"); + this->status_set_warning(); + return; // NO RETRY + } + // Read off sensor data + uint16_t raw_data[3]; + if (!this->read_data(raw_data, 3)) { + this->status_set_warning(); + return; + } + if (this->co2_sensor_ != nullptr) + this->co2_sensor_->publish_state(raw_data[0]); - if (this->co2_sensor_ != nullptr) - this->co2_sensor_->publish_state(raw_data[0]); - - if (this->temperature_sensor_ != nullptr) { - const float temperature = -45.0f + (175.0f * (raw_data[1])) / (1 << 16); - this->temperature_sensor_->publish_state(temperature); - } - - if (this->humidity_sensor_ != nullptr) { - const float humidity = (100.0f * raw_data[2]) / (1 << 16); - this->humidity_sensor_->publish_state(humidity); - } - - this->status_clear_warning(); + if (this->temperature_sensor_ != nullptr) { + const float temperature = -45.0f + (175.0f * (raw_data[1])) / (1 << 16); + this->temperature_sensor_->publish_state(temperature); + } + if (this->humidity_sensor_ != nullptr) { + const float humidity = (100.0f * raw_data[2]) / (1 << 16); + this->humidity_sensor_->publish_state(humidity); + } + this->status_clear_warning(); + }); // set_timeout } + +bool SCD4XComponent::perform_forced_calibration(uint16_t current_co2_concentration) { + /* + Operate the SCD4x in the operation mode later used in normal sensor operation (periodic measurement, low power + periodic measurement or single shot) for > 3 minutes in an environment with homogenous and constant CO2 + concentration before performing a forced recalibration. + */ + if (!this->write_command(SCD4X_CMD_STOP_MEASUREMENTS)) { + ESP_LOGE(TAG, "Failed to stop measurements"); + this->status_set_warning(); + } + this->set_timeout(500, [this, current_co2_concentration]() { + if (this->write_command(SCD4X_CMD_PERFORM_FORCED_CALIBRATION, current_co2_concentration)) { + ESP_LOGD(TAG, "setting forced calibration Co2 level %d ppm", current_co2_concentration); + // frc takes 400 ms + // because this method will be used very rarly + // the simple aproach with delay is ok + delay(400); // NOLINT' + if (!this->start_measurement_()) { + return false; + } else { + ESP_LOGD(TAG, "forced calibration complete"); + } + return true; + } else { + ESP_LOGE(TAG, "force calibration failed"); + this->error_code_ = FRC_FAILED; + this->status_set_warning(); + return false; + } + }); + return true; +} + +bool SCD4XComponent::factory_reset() { + if (!this->write_command(SCD4X_CMD_STOP_MEASUREMENTS)) { + ESP_LOGE(TAG, "Failed to stop measurements"); + this->status_set_warning(); + return false; + } + + this->set_timeout(500, [this]() { + if (!this->write_command(SCD4X_CMD_FACTORY_RESET)) { + ESP_LOGE(TAG, "Failed to send factory reset command"); + this->status_set_warning(); + return false; + } + ESP_LOGD(TAG, "Factory reset complete"); + return true; + }); + return true; +} + // Note pressure in bar here. Convert to hPa void SCD4XComponent::set_ambient_pressure_compensation(float pressure_in_bar) { ambient_pressure_compensation_ = true; @@ -213,5 +277,38 @@ bool SCD4XComponent::update_ambient_pressure_compensation_(uint16_t pressure_in_ } } +bool SCD4XComponent::start_measurement_() { + uint16_t measurement_command = SCD4X_CMD_START_CONTINUOUS_MEASUREMENTS; + switch (this->measurement_mode_) { + case PERIODIC: + measurement_command = SCD4X_CMD_START_CONTINUOUS_MEASUREMENTS; + break; + case LOW_POWER_PERIODIC: + measurement_command = SCD4X_CMD_START_LOW_POWER_CONTINUOUS_MEASUREMENTS; + break; + case SINGLE_SHOT: + measurement_command = SCD4X_CMD_START_LOW_POWER_SINGLE_SHOT; + break; + case SINGLE_SHOT_RHT_ONLY: + measurement_command = SCD4X_CMD_START_LOW_POWER_SINGLE_SHOT_RHT_ONLY; + break; + } + + static uint8_t remaining_retries = 3; + while (remaining_retries) { + if (!this->write_command(measurement_command)) { + ESP_LOGE(TAG, "Error starting measurements."); + this->error_code_ = MEASUREMENT_INIT_FAILED; + this->status_set_warning(); + if (--remaining_retries == 0) + return false; + delay(50); // NOLINT wait 50 ms and try again + } + this->status_clear_warning(); + return true; + } + return false; +} + } // namespace scd4x } // namespace esphome diff --git a/esphome/components/scd4x/scd4x.h b/esphome/components/scd4x/scd4x.h index 3e534bcf98..23c3766e60 100644 --- a/esphome/components/scd4x/scd4x.h +++ b/esphome/components/scd4x/scd4x.h @@ -1,5 +1,6 @@ #pragma once - +#include +#include "esphome/core/application.h" #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/sensirion_common/i2c_sensirion.h" @@ -7,7 +8,14 @@ namespace esphome { namespace scd4x { -enum ERRORCODE { COMMUNICATION_FAILED, SERIAL_NUMBER_IDENTIFICATION_FAILED, MEASUREMENT_INIT_FAILED, UNKNOWN }; +enum ERRORCODE { + COMMUNICATION_FAILED, + SERIAL_NUMBER_IDENTIFICATION_FAILED, + MEASUREMENT_INIT_FAILED, + FRC_FAILED, + UNKNOWN +}; +enum MeasurementMode { PERIODIC, LOW_POWER_PERIODIC, SINGLE_SHOT, SINGLE_SHOT_RHT_ONLY }; class SCD4XComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice { public: @@ -25,10 +33,13 @@ class SCD4XComponent : public PollingComponent, public sensirion_common::Sensiri void set_co2_sensor(sensor::Sensor *co2) { co2_sensor_ = co2; } void set_temperature_sensor(sensor::Sensor *temperature) { temperature_sensor_ = temperature; }; void set_humidity_sensor(sensor::Sensor *humidity) { humidity_sensor_ = humidity; } + void set_measurement_mode(MeasurementMode mode) { measurement_mode_ = mode; } + bool perform_forced_calibration(uint16_t current_co2_concentration); + bool factory_reset(); protected: bool update_ambient_pressure_compensation_(uint16_t pressure_in_hpa); - + bool start_measurement_(); ERRORCODE error_code_; bool initialized_{false}; @@ -38,7 +49,7 @@ class SCD4XComponent : public PollingComponent, public sensirion_common::Sensiri bool ambient_pressure_compensation_; uint16_t ambient_pressure_; bool enable_asc_; - + MeasurementMode measurement_mode_{PERIODIC}; sensor::Sensor *co2_sensor_{nullptr}; sensor::Sensor *temperature_sensor_{nullptr}; sensor::Sensor *humidity_sensor_{nullptr}; diff --git a/esphome/components/scd4x/sensor.py b/esphome/components/scd4x/sensor.py index 6ab0e1ba99..4c94d4257f 100644 --- a/esphome/components/scd4x/sensor.py +++ b/esphome/components/scd4x/sensor.py @@ -2,11 +2,15 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor from esphome.components import sensirion_common +from esphome import automation +from esphome.automation import maybe_simple_id + from esphome.const import ( CONF_ID, CONF_CO2, CONF_HUMIDITY, CONF_TEMPERATURE, + CONF_VALUE, DEVICE_CLASS_CARBON_DIOXIDE, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, @@ -19,7 +23,7 @@ from esphome.const import ( UNIT_PERCENT, ) -CODEOWNERS = ["@sjtrny"] +CODEOWNERS = ["@sjtrny", "@martgras"] DEPENDENCIES = ["i2c"] AUTO_LOAD = ["sensirion_common"] @@ -27,12 +31,29 @@ scd4x_ns = cg.esphome_ns.namespace("scd4x") SCD4XComponent = scd4x_ns.class_( "SCD4XComponent", cg.PollingComponent, sensirion_common.SensirionI2CDevice ) +MeasurementMode = scd4x_ns.enum("MEASUREMENT_MODE") +MEASUREMENT_MODE_OPTIONS = { + "periodic": MeasurementMode.PERIODIC, + "low_power_periodic": MeasurementMode.LOW_POWER_PERIODIC, + "single_shot": MeasurementMode.SINGLE_SHOT, + "single_shot_rht_only": MeasurementMode.SINGLE_SHOT_RHT_ONLY, +} + + +# Actions +PerformForcedCalibrationAction = scd4x_ns.class_( + "PerformForcedCalibrationAction", automation.Action +) +FactoryResetAction = scd4x_ns.class_("FactoryResetAction", automation.Action) + -CONF_AUTOMATIC_SELF_CALIBRATION = "automatic_self_calibration" CONF_ALTITUDE_COMPENSATION = "altitude_compensation" CONF_AMBIENT_PRESSURE_COMPENSATION = "ambient_pressure_compensation" -CONF_TEMPERATURE_OFFSET = "temperature_offset" CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE = "ambient_pressure_compensation_source" +CONF_AUTOMATIC_SELF_CALIBRATION = "automatic_self_calibration" +CONF_MEASUREMENT_MODE = "measurement_mode" +CONF_TEMPERATURE_OFFSET = "temperature_offset" + CONFIG_SCHEMA = ( cv.Schema( @@ -69,6 +90,9 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE): cv.use_id( sensor.Sensor ), + cv.Optional(CONF_MEASUREMENT_MODE, default="periodic"): cv.enum( + MEASUREMENT_MODE_OPTIONS, lower=True + ), } ) .extend(cv.polling_component_schema("60s")) @@ -106,3 +130,42 @@ async def to_code(config): if CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE in config: sens = await cg.get_variable(config[CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE]) cg.add(var.set_ambient_pressure_source(sens)) + + cg.add(var.set_measurement_mode(config[CONF_MEASUREMENT_MODE])) + + +SCD4X_ACTION_SCHEMA = maybe_simple_id( + { + cv.GenerateID(): cv.use_id(SCD4XComponent), + cv.Required(CONF_VALUE): cv.templatable(cv.positive_int), + } +) + + +@automation.register_action( + "scd4x.perform_forced_calibration", + PerformForcedCalibrationAction, + SCD4X_ACTION_SCHEMA, +) +async def scd4x_frc_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + template_ = await cg.templatable(config[CONF_VALUE], args, cg.uint16) + cg.add(var.set_value(template_)) + return var + + +SCD4X_RESET_ACTION_SCHEMA = maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(SCD4XComponent), + } +) + + +@automation.register_action( + "scd4x.factory_reset", FactoryResetAction, SCD4X_RESET_ACTION_SCHEMA +) +async def scd4x_reset_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var diff --git a/tests/test1.yaml b/tests/test1.yaml index deaf1c237e..e15b2cf6b7 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -890,6 +890,7 @@ sensor: temperature_offset: 4.2C i2c_id: i2c_bus - platform: scd4x + id: scd40 co2: name: "SCD4X CO2" temperature: @@ -2822,3 +2823,14 @@ lock: - platform: copy source_id: test_lock2 name: Generic Output Lock Copy + +button: + - platform: template + name: "Start calibration" + on_press: + - scd4x.perform_forced_calibration: + value: 419 + id: scd40 + - scd4x.factory_reset: + id: scd40 +