mirror of
https://github.com/esphome/esphome.git
synced 2024-11-25 00:18:11 +01:00
extend scd4x (#3409)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
parent
4e1f6518e8
commit
782186e13d
6 changed files with 287 additions and 76 deletions
|
@ -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
|
||||
|
|
28
esphome/components/scd4x/automation.h
Normal file
28
esphome/components/scd4x/automation.h
Normal file
|
@ -0,0 +1,28 @@
|
|||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "scd4x.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace scd4x {
|
||||
|
||||
template<typename... Ts> class PerformForcedCalibrationAction : public Action<Ts...>, public Parented<SCD4XComponent> {
|
||||
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<typename... Ts> class FactoryResetAction : public Action<Ts...>, public Parented<SCD4XComponent> {
|
||||
public:
|
||||
void play(Ts... x) override { this->parent_->factory_reset(); }
|
||||
};
|
||||
|
||||
} // namespace scd4x
|
||||
} // namespace esphome
|
|
@ -13,29 +13,24 @@ 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->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");
|
||||
this->status_clear_error();
|
||||
if (!this->write_command(SCD4X_CMD_STOP_MEASUREMENTS)) {
|
||||
ESP_LOGE(TAG, "Failed to stop measurements");
|
||||
this->mark_failed();
|
||||
|
@ -43,9 +38,7 @@ void SCD4XComponent::setup() {
|
|||
}
|
||||
// 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]() {
|
||||
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,6 +110,10 @@ void SCD4XComponent::dump_config() {
|
|||
}
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " Automatic self calibration: %s", ONOFF(this->enable_asc_));
|
||||
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 {
|
||||
if (this->ambient_pressure_compensation_) {
|
||||
ESP_LOGCONFIG(TAG, " Altitude compensation disabled");
|
||||
ESP_LOGCONFIG(TAG, " Ambient pressure compensation: %dmBar", this->ambient_pressure_);
|
||||
|
@ -130,6 +121,21 @@ void SCD4XComponent::dump_config() {
|
|||
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);
|
||||
LOG_SENSOR(" ", "CO2", this->co2_sensor_);
|
||||
|
@ -149,6 +155,13 @@ void SCD4XComponent::update() {
|
|||
}
|
||||
}
|
||||
|
||||
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();
|
||||
|
@ -156,6 +169,7 @@ void SCD4XComponent::update() {
|
|||
}
|
||||
|
||||
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!");
|
||||
|
@ -165,16 +179,14 @@ void SCD4XComponent::update() {
|
|||
if (!this->write_command(SCD4X_CMD_READ_MEASUREMENT)) {
|
||||
ESP_LOGW(TAG, "Error reading measurement!");
|
||||
this->status_set_warning();
|
||||
return;
|
||||
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]);
|
||||
|
||||
|
@ -182,14 +194,66 @@ void SCD4XComponent::update() {
|
|||
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
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#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};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in a new issue