From 3c7b9ea5694b3f22e394b73ba52cd6a9b2d84a33 Mon Sep 17 00:00:00 2001 From: "Andrew J.Swan" Date: Fri, 21 Feb 2025 11:35:08 +0200 Subject: [PATCH] Add CUBIC CM1106 Single Beam NDIR CO2 Sensor Module --- CODEOWNERS | 1 + esphome/components/cm1106/__init__.py | 1 + esphome/components/cm1106/cm1106.cpp | 94 +++++++++++++++++++ esphome/components/cm1106/cm1106.h | 46 +++++++++ esphome/components/cm1106/sensor.py | 78 +++++++++++++++ tests/components/cm1106/common.yaml | 11 +++ tests/components/cm1106/test.esp32-ard.yaml | 5 + .../components/cm1106/test.esp32-c3-ard.yaml | 5 + .../components/cm1106/test.esp32-c3-idf.yaml | 5 + tests/components/cm1106/test.esp32-idf.yaml | 5 + tests/components/cm1106/test.esp8266-ard.yaml | 5 + tests/components/cm1106/test.rp2040-ard.yaml | 5 + 12 files changed, 261 insertions(+) create mode 100644 esphome/components/cm1106/__init__.py create mode 100644 esphome/components/cm1106/cm1106.cpp create mode 100644 esphome/components/cm1106/cm1106.h create mode 100644 esphome/components/cm1106/sensor.py create mode 100644 tests/components/cm1106/common.yaml create mode 100644 tests/components/cm1106/test.esp32-ard.yaml create mode 100644 tests/components/cm1106/test.esp32-c3-ard.yaml create mode 100644 tests/components/cm1106/test.esp32-c3-idf.yaml create mode 100644 tests/components/cm1106/test.esp32-idf.yaml create mode 100644 tests/components/cm1106/test.esp8266-ard.yaml create mode 100644 tests/components/cm1106/test.rp2040-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index c725c2e736..00eff24c7a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -95,6 +95,7 @@ esphome/components/cd74hc4067/* @asoehlke esphome/components/ch422g/* @clydebarrow @jesterret esphome/components/climate/* @esphome/core esphome/components/climate_ir/* @glmnet +esphome/components/cm1106/* @andrewjswan esphome/components/color_temperature/* @jesserockz esphome/components/combination/* @Cat-Ion @kahrendt esphome/components/coolix/* @glmnet diff --git a/esphome/components/cm1106/__init__.py b/esphome/components/cm1106/__init__.py new file mode 100644 index 0000000000..fa3c3f1925 --- /dev/null +++ b/esphome/components/cm1106/__init__.py @@ -0,0 +1 @@ +"""CM1106 component for ESPHome.""" diff --git a/esphome/components/cm1106/cm1106.cpp b/esphome/components/cm1106/cm1106.cpp new file mode 100644 index 0000000000..59eb3658c7 --- /dev/null +++ b/esphome/components/cm1106/cm1106.cpp @@ -0,0 +1,94 @@ +#include "cm1106.h" +#include "esphome/core/log.h" + +#include + +namespace esphome { +namespace cm1106 { + +uint8_t cm1106_checksum(const uint8_t *response, size_t len) { + uint8_t crc = 0; + for (int i = 0; i < len - 1; i++) { + crc -= response[i]; + } + return crc; +} + +void CM1106Component::update() { + uint8_t response[8] = {0}; + if (!this->cm1106_write_command_(c_m1106_cmd_get_c_o2_, sizeof(c_m1106_cmd_get_c_o2_), response, sizeof(response))) { + ESP_LOGW(TAG, "Reading data from CM1106 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]); + 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); + this->status_set_warning(); + return; + } + + this->status_clear_warning(); + + int16_t ppm = response[3] << 8 | response[4]; + ESP_LOGD(TAG, "CM1106 Received CO₂=%uppm DF3=%02X DF4=%02X", ppm, response[5], response[6]); + if (this->co2_sensor_ != nullptr) + this->co2_sensor_->publish_state(ppm); +} + +void CM1106Component::calibrate_zero(uint16_t ppm) { + uint8_t cmd[6]; + memcpy(cmd, c_m1106_cmd_set_c_o2_calib_, sizeof(cmd)); + cmd[3] = ppm >> 8; + cmd[4] = ppm & 0xFF; + uint8_t response[4] = {0}; + + if (!this->cm1106_write_command_(cmd, sizeof(cmd), response, sizeof(response))) { + ESP_LOGW(TAG, "Reading data from CM1106 failed!"); + this->status_set_warning(); + return; + } + + // check if correct response received + if (memcmp(response, c_m1106_cmd_set_c_o2_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]); + this->status_set_warning(); + return; + } + + this->status_clear_warning(); + ESP_LOGD(TAG, "CM1106 Successfully calibrated sensor to %uppm", ppm); +} + +bool CM1106Component::cm1106_write_command_(uint8_t *command, size_t command_len, uint8_t *response, + size_t response_len) { + // Empty RX Buffer + while (this->available()) + this->read(); + command[command_len - 1] = cm1106_checksum(command, command_len); + this->write_array(command, command_len); + this->flush(); + + if (response == nullptr) + return true; + + return this->read_array(response, response_len); +} + +void CM1106Component::dump_config() { + ESP_LOGCONFIG(TAG, "CM1106:"); + LOG_SENSOR(" ", "CO2", this->co2_sensor_); + this->check_uart_settings(9600); +} + +} // namespace cm1106 +} // namespace esphome diff --git a/esphome/components/cm1106/cm1106.h b/esphome/components/cm1106/cm1106.h new file mode 100644 index 0000000000..1252627d60 --- /dev/null +++ b/esphome/components/cm1106/cm1106.h @@ -0,0 +1,46 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/uart/uart.h" + +namespace esphome { +namespace cm1106 { + +static const char *const TAG = "cm1106"; + +class CM1106Component : public PollingComponent, public uart::UARTDevice { + public: + float get_setup_priority() const override { return esphome::setup_priority::DATA; } + + void update() override; + void dump_config() override; + + void calibrate_zero(uint16_t ppm); + + void set_co2_sensor(sensor::Sensor *co2_sensor) { co2_sensor_ = co2_sensor; } + + private: + uint8_t c_m1106_cmd_get_c_o2_[4] = {0x11, 0x01, 0x01, 0xED}; + uint8_t c_m1106_cmd_set_c_o2_calib_[6] = {0x11, 0x03, 0x03, 0x00, 0x00, 0x00}; + uint8_t c_m1106_cmd_set_c_o2_calib_response_[4] = {0x16, 0x01, 0x03, 0xE6}; + + protected: + bool cm1106_write_command_(uint8_t *command, size_t command_len, uint8_t *response, size_t response_len); + + sensor::Sensor *co2_sensor_{nullptr}; +}; + +template class CM1106CalibrateZeroAction : public Action { + public: + CM1106CalibrateZeroAction(CM1106Component *cm1106) : cm1106_(cm1106) {} + + void play(Ts... x) override { this->cm1106_->calibrate_zero(400); } + + protected: + CM1106Component *cm1106_; +}; + +} // namespace cm1106 +} // namespace esphome diff --git a/esphome/components/cm1106/sensor.py b/esphome/components/cm1106/sensor.py new file mode 100644 index 0000000000..bd0325cda2 --- /dev/null +++ b/esphome/components/cm1106/sensor.py @@ -0,0 +1,78 @@ +"""CM1106 Sensor component for ESPHome.""" + +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.automation import maybe_simple_id +from esphome.components import sensor, uart +from esphome.const import ( + CONF_CO2, + CONF_ID, + DEVICE_CLASS_CARBON_DIOXIDE, + ICON_MOLECULE_CO2, + STATE_CLASS_MEASUREMENT, + UNIT_PARTS_PER_MILLION, +) + +DEPENDENCIES = ["uart"] +CODEOWNERS = ["@andrewjswan"] + +CONF_AUTOMATIC_BASELINE_CALIBRATION = "automatic_baseline_calibration" + +cm1106_ns = cg.esphome_ns.namespace("cm1106") +CM1106Component = cm1106_ns.class_( + "CM1106Component", cg.PollingComponent, uart.UARTDevice +) +CM1106CalibrateZeroAction = cm1106_ns.class_( + "CM1106CalibrateZeroAction", + automation.Action, +) +CM1106ABCEnableAction = cm1106_ns.class_("CM1106ABCEnableAction", automation.Action) +CM1106ABCDisableAction = cm1106_ns.class_("CM1106ABCDisableAction", automation.Action) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(CM1106Component), + cv.Required(CONF_CO2): sensor.sensor_schema( + unit_of_measurement=UNIT_PARTS_PER_MILLION, + icon=ICON_MOLECULE_CO2, + accuracy_decimals=0, + device_class=DEVICE_CLASS_CARBON_DIOXIDE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_AUTOMATIC_BASELINE_CALIBRATION): cv.boolean, + }, + ) + .extend(cv.polling_component_schema("60s")) + .extend(uart.UART_DEVICE_SCHEMA) +) + + +async def to_code(config) -> None: + """Code generation entry point.""" + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) + + if CONF_CO2 in config: + sens = await sensor.new_sensor(config[CONF_CO2]) + cg.add(var.set_co2_sensor(sens)) + + +CALIBRATION_ACTION_SCHEMA = maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(CM1106Component), + }, +) + + +@automation.register_action( + "cm1106.calibrate_zero", + CM1106CalibrateZeroAction, + CALIBRATION_ACTION_SCHEMA, +) +async def cm1106_calibration_to_code(config, action_id, template_arg, args) -> None: # noqa: ARG001 + """Service code generation entry point.""" + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) diff --git a/tests/components/cm1106/common.yaml b/tests/components/cm1106/common.yaml new file mode 100644 index 0000000000..a01e78024e --- /dev/null +++ b/tests/components/cm1106/common.yaml @@ -0,0 +1,11 @@ +uart: + - id: uart_cm1106 + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 9600 + +sensor: + - platform: cm1106 + co2: + name: CM1106 CO2 Value + update_interval: 15s diff --git a/tests/components/cm1106/test.esp32-ard.yaml b/tests/components/cm1106/test.esp32-ard.yaml new file mode 100644 index 0000000000..f486544afa --- /dev/null +++ b/tests/components/cm1106/test.esp32-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO17 + rx_pin: GPIO16 + +<<: !include common.yaml diff --git a/tests/components/cm1106/test.esp32-c3-ard.yaml b/tests/components/cm1106/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..b516342f3b --- /dev/null +++ b/tests/components/cm1106/test.esp32-c3-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/cm1106/test.esp32-c3-idf.yaml b/tests/components/cm1106/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..b516342f3b --- /dev/null +++ b/tests/components/cm1106/test.esp32-c3-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/cm1106/test.esp32-idf.yaml b/tests/components/cm1106/test.esp32-idf.yaml new file mode 100644 index 0000000000..f486544afa --- /dev/null +++ b/tests/components/cm1106/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO17 + rx_pin: GPIO16 + +<<: !include common.yaml diff --git a/tests/components/cm1106/test.esp8266-ard.yaml b/tests/components/cm1106/test.esp8266-ard.yaml new file mode 100644 index 0000000000..b516342f3b --- /dev/null +++ b/tests/components/cm1106/test.esp8266-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/cm1106/test.rp2040-ard.yaml b/tests/components/cm1106/test.rp2040-ard.yaml new file mode 100644 index 0000000000..b516342f3b --- /dev/null +++ b/tests/components/cm1106/test.rp2040-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +<<: !include common.yaml