diff --git a/CODEOWNERS b/CODEOWNERS index a6e08f225d..5013c0cd33 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -96,6 +96,7 @@ esphome/components/ch422g/* @clydebarrow @jesterret esphome/components/chsc6x/* @kkosik20 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/const/* @esphome/core 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..b7b0fe0063 --- /dev/null +++ b/esphome/components/cm1106/cm1106.cpp @@ -0,0 +1,112 @@ +#include "cm1106.h" +#include "esphome/core/log.h" + +#include + +namespace esphome { +namespace cm1106 { + +static const char *const 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}; + +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::setup() { + ESP_LOGCONFIG(TAG, "Setting up CM1106..."); + 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_LOGE(TAG, "Communication with CM1106 failed!"); + this->mark_failed(); + return; + } +} + +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!"); + 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(); + + uint16_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_CO2_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_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]); + this->status_set_warning(); + return; + } + + this->status_clear_warning(); + ESP_LOGD(TAG, "CM1106 Successfully calibrated sensor to %uppm", ppm); +} + +bool CM1106Component::cm1106_write_command_(const uint8_t *command, size_t command_len, uint8_t *response, + size_t response_len) { + // Empty RX Buffer + while (this->available()) + this->read(); + this->write_array(command, command_len - 1); + this->write_byte(cm1106_checksum(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); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with CM1106 failed!"); + } +} + +} // namespace cm1106 +} // namespace esphome diff --git a/esphome/components/cm1106/cm1106.h b/esphome/components/cm1106/cm1106.h new file mode 100644 index 0000000000..3b78e17cf4 --- /dev/null +++ b/esphome/components/cm1106/cm1106.h @@ -0,0 +1,40 @@ +#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 { + +class CM1106Component : public PollingComponent, public uart::UARTDevice { + public: + float get_setup_priority() const override { return esphome::setup_priority::DATA; } + + void setup() override; + void update() override; + void dump_config() override; + + void calibrate_zero(uint16_t ppm); + + void set_co2_sensor(sensor::Sensor *co2_sensor) { this->co2_sensor_ = co2_sensor; } + + 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); +}; + +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..1b8ac14fbe --- /dev/null +++ b/esphome/components/cm1106/sensor.py @@ -0,0 +1,72 @@ +"""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"] + +cm1106_ns = cg.esphome_ns.namespace("cm1106") +CM1106Component = cm1106_ns.class_( + "CM1106Component", cg.PollingComponent, uart.UARTDevice +) +CM1106CalibrateZeroAction = cm1106_ns.class_( + "CM1106CalibrateZeroAction", + automation.Action, +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(CM1106Component), + cv.Optional(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, + ), + }, + ) + .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 co2_config := config.get(CONF_CO2): + sens = await sensor.new_sensor(co2_config) + cg.add(var.set_co2_sensor(sens)) + + +CALIBRATION_ACTION_SCHEMA = maybe_simple_id( + { + cv.GenerateID(): 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: + """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