1
0
mirror of https://github.com/esphome/esphome.git synced 2025-03-15 15:18:16 +00:00

Add CUBIC CM1106 Single Beam NDIR CO2 Sensor Module

This commit is contained in:
Andrew J.Swan 2025-02-21 11:35:08 +02:00
parent 9f603a474f
commit 3c7b9ea569
12 changed files with 261 additions and 0 deletions

View File

@ -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

View File

@ -0,0 +1 @@
"""CM1106 component for ESPHome."""

View File

@ -0,0 +1,94 @@
#include "cm1106.h"
#include "esphome/core/log.h"
#include <cinttypes>
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

View File

@ -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<typename... Ts> class CM1106CalibrateZeroAction : public Action<Ts...> {
public:
CM1106CalibrateZeroAction(CM1106Component *cm1106) : cm1106_(cm1106) {}
void play(Ts... x) override { this->cm1106_->calibrate_zero(400); }
protected:
CM1106Component *cm1106_;
};
} // namespace cm1106
} // namespace esphome

View File

@ -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)

View File

@ -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

View File

@ -0,0 +1,5 @@
substitutions:
tx_pin: GPIO17
rx_pin: GPIO16
<<: !include common.yaml

View File

@ -0,0 +1,5 @@
substitutions:
tx_pin: GPIO4
rx_pin: GPIO5
<<: !include common.yaml

View File

@ -0,0 +1,5 @@
substitutions:
tx_pin: GPIO4
rx_pin: GPIO5
<<: !include common.yaml

View File

@ -0,0 +1,5 @@
substitutions:
tx_pin: GPIO17
rx_pin: GPIO16
<<: !include common.yaml

View File

@ -0,0 +1,5 @@
substitutions:
tx_pin: GPIO4
rx_pin: GPIO5
<<: !include common.yaml

View File

@ -0,0 +1,5 @@
substitutions:
tx_pin: GPIO4
rx_pin: GPIO5
<<: !include common.yaml