[cm1106] Add ABC function (#8894)

Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
CzBiX
2026-10-10 09:12:40 -10:00
committed by GitHub
co-authored by pre-commit-ci-lite[bot] J. Nick Koston Copilot Autofix powered by AI
parent 17cbb75161
commit 865863abf5
4 changed files with 120 additions and 23 deletions
+60 -21
View File
@@ -6,10 +6,16 @@
namespace esphome::cm1106 {
ESPHOME_LOG_TAG(TAG, "cm1106");
static const uint8_t C_M1106_CMD_GET_CO2[4] = {0x11, 0x01, 0x01, 0xED};
static const uint8_t C_M1106_CMD_SET_CO2_CALIB[6] = {0x11, 0x03, 0x03, 0x00, 0x00, 0x00};
static const uint8_t C_M1106_CMD_SET_CO2_CALIB_RESPONSE[4] = {0x16, 0x01, 0x03, 0xE6};
static constexpr uint8_t C_M1106_CMD_GET_CO2[4] = {0x11, 0x01, 0x01, 0xED};
static constexpr uint8_t C_M1106_CMD_SET_CO2_CALIB[6] = {0x11, 0x03, 0x03, 0x00, 0x00, 0x00};
static constexpr uint8_t C_M1106_CMD_SET_CO2_CALIB_RESPONSE[4] = {0x16, 0x01, 0x03, 0xE6};
// The factory default ABC calibration cycle differs between CM1106 models, so
// every ABC write applies the configured cycle and baseline explicitly.
static constexpr uint8_t C_M1106_CMD_SET_ABC_STATUS[10] = {0x11, 0x07, 0x10, 0x64, 0x00, 0x0F, 0x01, 0x90, 0x64, 0x00};
static constexpr uint8_t C_M1106_CMD_SET_ABC_STATUS_RESPONSE[4] = {0x16, 0x01, 0x10, 0xD9};
static constexpr uint8_t CM1106_ABC_FLAG_ENABLE = 0x0;
static constexpr uint8_t CM1106_ABC_FLAG_DISABLE = 0x2;
uint8_t cm1106_checksum(const uint8_t *response, size_t len) {
uint8_t crc = 0;
for (size_t i = 0; i < len - 1; i++) {
@@ -25,34 +31,46 @@ void CM1106Component::setup() {
this->mark_failed();
return;
}
if (this->abc_boot_logic_ != CM1106_ABC_NONE) {
this->abc_set_(this->abc_boot_logic_ == CM1106_ABC_ENABLED);
}
}
void CM1106Component::update() {
uint8_t response[8] = {0};
if (!this->cm1106_write_command_(C_M1106_CMD_GET_CO2, sizeof(C_M1106_CMD_GET_CO2), response, sizeof(response))) {
ESP_LOGW(TAG, "Reading data from CM1106 failed!");
ESP_LOGW(TAG, "Reading data failed!");
this->status_set_warning();
return;
}
if (response[0] != 0x16 || response[1] != 0x05 || response[2] != 0x01) {
ESP_LOGW(TAG, "Got wrong UART response from CM1106: %02X %02X %02X %02X", response[0], response[1], response[2],
response[3]);
ESP_LOGW(TAG, "Unexpected response: %02X %02X %02X %02X", response[0], response[1], response[2], response[3]);
this->status_set_warning();
return;
}
uint8_t checksum = cm1106_checksum(response, sizeof(response));
if (response[7] != checksum) {
ESP_LOGW(TAG, "CM1106 Checksum doesn't match: 0x%02X!=0x%02X", response[7], checksum);
ESP_LOGW(TAG, "Checksum doesn't match: 0x%02X!=0x%02X", response[7], checksum);
this->status_set_warning();
return;
}
const uint16_t ppm = response[3] << 8 | response[4];
const uint8_t status = response[5];
if (status & 0x1) {
ESP_LOGW(TAG, "Preheating; CO₂=%uppm", ppm);
this->status_set_warning();
return;
}
if (status) {
ESP_LOGW(TAG, "Status: %02X", status);
}
this->status_clear_warning();
uint16_t ppm = response[3] << 8 | response[4];
ESP_LOGD(TAG, "CM1106 Received CO₂=%uppm DF3=%02X DF4=%02X", ppm, response[5], response[6]);
ESP_LOGD(TAG, "CO₂=%uppm DF4=%02X", ppm, response[6]);
if (this->co2_sensor_ != nullptr)
this->co2_sensor_->publish_state(ppm);
}
@@ -62,24 +80,38 @@ void CM1106Component::calibrate_zero(uint16_t ppm) {
memcpy(cmd, C_M1106_CMD_SET_CO2_CALIB, sizeof(cmd));
cmd[3] = ppm >> 8;
cmd[4] = ppm & 0xFF;
if (this->send_command_expect_ack_(cmd, sizeof(cmd), C_M1106_CMD_SET_CO2_CALIB_RESPONSE)) {
ESP_LOGD(TAG, "Successfully calibrated sensor to %uppm", ppm);
}
}
void CM1106Component::abc_set_(bool enabled) {
ESP_LOGD(TAG, "%sabling automatic baseline calibration", enabled ? LOG_STR_LITERAL("En") : LOG_STR_LITERAL("Dis"));
uint8_t cmd[10];
memcpy(cmd, C_M1106_CMD_SET_ABC_STATUS, sizeof(cmd));
cmd[4] = enabled ? CM1106_ABC_FLAG_ENABLE : CM1106_ABC_FLAG_DISABLE;
cmd[5] = this->abc_cycle_;
cmd[6] = this->abc_baseline_ >> 8;
cmd[7] = this->abc_baseline_ & 0xFF;
if (this->send_command_expect_ack_(cmd, sizeof(cmd), C_M1106_CMD_SET_ABC_STATUS_RESPONSE)) {
ESP_LOGD(TAG, "Successfully set ABC status");
}
}
bool CM1106Component::send_command_expect_ack_(const uint8_t *command, size_t command_len, const uint8_t *ack) {
uint8_t response[4] = {0};
if (!this->cm1106_write_command_(cmd, sizeof(cmd), response, sizeof(response))) {
ESP_LOGW(TAG, "Reading data from CM1106 failed!");
if (!this->cm1106_write_command_(command, command_len, response, sizeof(response))) {
ESP_LOGW(TAG, "Command %02X failed", command[2]);
this->status_set_warning();
return;
return false;
}
// check if correct response received
if (memcmp(response, C_M1106_CMD_SET_CO2_CALIB_RESPONSE, sizeof(response)) != 0) {
ESP_LOGW(TAG, "Got wrong UART response from CM1106: %02X %02X %02X %02X", response[0], response[1], response[2],
response[3]);
if (memcmp(response, ack, sizeof(response)) != 0) {
ESP_LOGW(TAG, "Unexpected response: %02X %02X %02X %02X", response[0], response[1], response[2], response[3]);
this->status_set_warning();
return;
return false;
}
this->status_clear_warning();
ESP_LOGD(TAG, "CM1106 Successfully calibrated sensor to %uppm", ppm);
return true;
}
bool CM1106Component::cm1106_write_command_(const uint8_t *command, size_t command_len, uint8_t *response,
@@ -100,6 +132,13 @@ bool CM1106Component::cm1106_write_command_(const uint8_t *command, size_t comma
void CM1106Component::dump_config() {
ESP_LOGCONFIG(TAG, "CM1106:");
LOG_SENSOR(" ", "CO2", this->co2_sensor_);
if (this->abc_boot_logic_ != CM1106_ABC_NONE) {
ESP_LOGCONFIG(TAG,
" Automatic baseline calibration on boot: %s\n"
" ABC calibration cycle: %u days\n"
" ABC baseline: %uppm",
ONOFF(this->abc_boot_logic_ == CM1106_ABC_ENABLED), this->abc_cycle_, this->abc_baseline_);
}
if (this->is_failed()) {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
}
+23 -2
View File
@@ -6,6 +6,14 @@
namespace esphome::cm1106 {
// ABC logic requested at boot; CM1106_ABC_NONE means nothing is sent and the
// sensor keeps its stored setting.
enum CM1106ABCLogic : uint8_t {
CM1106_ABC_NONE = 0,
CM1106_ABC_ENABLED,
CM1106_ABC_DISABLED,
};
class CM1106Component final : public PollingComponent, public uart::UARTDevice {
public:
void setup() override;
@@ -13,13 +21,26 @@ class CM1106Component final : public PollingComponent, public uart::UARTDevice {
void dump_config() override;
void calibrate_zero(uint16_t ppm);
void abc_enable() { this->abc_set_(true); }
void abc_disable() { this->abc_set_(false); }
void set_co2_sensor(sensor::Sensor *co2_sensor) { this->co2_sensor_ = co2_sensor; }
void set_abc(bool enabled, uint8_t cycle_days, uint16_t baseline_ppm) {
this->abc_boot_logic_ = enabled ? CM1106_ABC_ENABLED : CM1106_ABC_DISABLED;
this->abc_cycle_ = cycle_days;
this->abc_baseline_ = baseline_ppm;
}
protected:
sensor::Sensor *co2_sensor_{nullptr};
bool cm1106_write_command_(const uint8_t *command, size_t command_len, uint8_t *response, size_t response_len);
/// Sends a command and checks the sensor's four byte acknowledgement, updating the warning status.
bool send_command_expect_ack_(const uint8_t *command, size_t command_len, const uint8_t *ack);
void abc_set_(bool enabled);
sensor::Sensor *co2_sensor_{nullptr};
CM1106ABCLogic abc_boot_logic_{CM1106_ABC_NONE};
uint8_t abc_cycle_{15};
uint16_t abc_baseline_{400};
};
} // namespace esphome::cm1106
+29
View File
@@ -4,9 +4,12 @@ from esphome import automation
from esphome.automation import maybe_simple_id
import esphome.codegen as cg
from esphome.components import sensor, uart
from esphome.components.const import CONF_AUTOMATIC_BASELINE_CALIBRATION
import esphome.config_validation as cv
from esphome.const import (
CONF_BASELINE,
CONF_CO2,
CONF_CYCLE,
CONF_ID,
DEVICE_CLASS_CARBON_DIOXIDE,
ICON_MOLECULE_CO2,
@@ -18,11 +21,22 @@ from esphome.types import ConfigType
DEPENDENCIES = ["uart"]
CODEOWNERS = ["@andrewjswan"]
# true enables ABC with these defaults, false disables it, a mapping enables it with custom values
_ABC_SETTINGS_SCHEMA = cv.Schema(
{
cv.Optional(CONF_CYCLE, default=15): cv.int_range(min=1, max=90),
cv.Optional(CONF_BASELINE, default=400): cv.int_range(min=400, max=10000),
}
)
cm1106_ns = cg.esphome_ns.namespace("cm1106")
CM1106Component = cm1106_ns.class_(
"CM1106Component", cg.PollingComponent, uart.UARTDevice
)
CONFIG_SCHEMA = (
cv.Schema(
{
@@ -34,6 +48,9 @@ CONFIG_SCHEMA = (
device_class=DEVICE_CLASS_CARBON_DIOXIDE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_AUTOMATIC_BASELINE_CALIBRATION): cv.Any(
cv.boolean, _ABC_SETTINGS_SCHEMA
),
},
)
.extend(cv.polling_component_schema("60s"))
@@ -57,6 +74,11 @@ async def to_code(config: ConfigType) -> None:
sensors = sensor.sub_sensors(config)
await sensors(CONF_CO2, var.set_co2_sensor)
if (abc := config.get(CONF_AUTOMATIC_BASELINE_CALIBRATION)) is not None:
enabled = abc if isinstance(abc, bool) else True
settings = _ABC_SETTINGS_SCHEMA({}) if isinstance(abc, bool) else abc
cg.add(var.set_abc(enabled, settings[CONF_CYCLE], settings[CONF_BASELINE]))
CALIBRATION_ACTION_SCHEMA = maybe_simple_id(
{
@@ -70,3 +92,10 @@ automation.register_apply_action(
CALIBRATION_ACTION_SCHEMA,
automation.ApplyCall("calibrate_zero(400)"),
)
for _name, _call in (
("cm1106.abc_enable", "abc_enable()"),
("cm1106.abc_disable", "abc_disable()"),
):
automation.register_apply_action(
_name, CALIBRATION_ACTION_SCHEMA, automation.ApplyCall(_call)
)
+8
View File
@@ -1,5 +1,13 @@
sensor:
- platform: cm1106
id: cm1106_sensor
co2:
name: CM1106 CO2 Value
on_value:
then:
- cm1106.abc_enable: cm1106_sensor
- cm1106.abc_disable: cm1106_sensor
update_interval: 15s
automatic_baseline_calibration:
cycle: 7
baseline: 420