mirror of
https://github.com/esphome/esphome.git
synced 2026-02-08 00:31:58 +00:00
[hc8] Add support for HC8 CO2 sensor (#11872)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
This commit is contained in:
@@ -202,6 +202,7 @@ esphome/components/havells_solar/* @sourabhjaiswal
|
|||||||
esphome/components/hbridge/fan/* @WeekendWarrior
|
esphome/components/hbridge/fan/* @WeekendWarrior
|
||||||
esphome/components/hbridge/light/* @DotNetDann
|
esphome/components/hbridge/light/* @DotNetDann
|
||||||
esphome/components/hbridge/switch/* @dwmw2
|
esphome/components/hbridge/switch/* @dwmw2
|
||||||
|
esphome/components/hc8/* @omartijn
|
||||||
esphome/components/hdc2010/* @optimusprimespace @ssieb
|
esphome/components/hdc2010/* @optimusprimespace @ssieb
|
||||||
esphome/components/he60r/* @clydebarrow
|
esphome/components/he60r/* @clydebarrow
|
||||||
esphome/components/heatpumpir/* @rob-deutsch
|
esphome/components/heatpumpir/* @rob-deutsch
|
||||||
|
|||||||
1
esphome/components/hc8/__init__.py
Normal file
1
esphome/components/hc8/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
CODEOWNERS = ["@omartijn"]
|
||||||
99
esphome/components/hc8/hc8.cpp
Normal file
99
esphome/components/hc8/hc8.cpp
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
#include "hc8.h"
|
||||||
|
#include "esphome/core/application.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
|
||||||
|
namespace esphome::hc8 {
|
||||||
|
|
||||||
|
static const char *const TAG = "hc8";
|
||||||
|
static const std::array<uint8_t, 5> HC8_COMMAND_GET_PPM{0x64, 0x69, 0x03, 0x5E, 0x4E};
|
||||||
|
static const std::array<uint8_t, 3> HC8_COMMAND_CALIBRATE_PREAMBLE{0x11, 0x03, 0x03};
|
||||||
|
|
||||||
|
void HC8Component::setup() {
|
||||||
|
// send an initial query to the device, this will
|
||||||
|
// get it out of "active output mode", where it
|
||||||
|
// generates data every second
|
||||||
|
this->write_array(HC8_COMMAND_GET_PPM);
|
||||||
|
this->flush();
|
||||||
|
|
||||||
|
// ensure the buffer is empty
|
||||||
|
while (this->available())
|
||||||
|
this->read();
|
||||||
|
}
|
||||||
|
|
||||||
|
void HC8Component::update() {
|
||||||
|
uint32_t now_ms = App.get_loop_component_start_time();
|
||||||
|
uint32_t warmup_ms = this->warmup_seconds_ * 1000;
|
||||||
|
if (now_ms < warmup_ms) {
|
||||||
|
ESP_LOGW(TAG, "HC8 warming up, %" PRIu32 " s left", (warmup_ms - now_ms) / 1000);
|
||||||
|
this->status_set_warning();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (this->available())
|
||||||
|
this->read();
|
||||||
|
|
||||||
|
this->write_array(HC8_COMMAND_GET_PPM);
|
||||||
|
this->flush();
|
||||||
|
|
||||||
|
// the sensor is a bit slow in responding, so trying to
|
||||||
|
// read immediately after sending a query will timeout
|
||||||
|
this->set_timeout(50, [this]() {
|
||||||
|
std::array<uint8_t, 14> response;
|
||||||
|
if (!this->read_array(response.data(), response.size())) {
|
||||||
|
ESP_LOGW(TAG, "Reading data from HC8 failed!");
|
||||||
|
this->status_set_warning();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response[0] != 0x64 || response[1] != 0x69) {
|
||||||
|
ESP_LOGW(TAG, "Invalid preamble from HC8!");
|
||||||
|
this->status_set_warning();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (crc16(response.data(), 12) != encode_uint16(response[13], response[12])) {
|
||||||
|
ESP_LOGW(TAG, "HC8 Checksum mismatch");
|
||||||
|
this->status_set_warning();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->status_clear_warning();
|
||||||
|
|
||||||
|
const uint16_t ppm = encode_uint16(response[5], response[4]);
|
||||||
|
ESP_LOGD(TAG, "HC8 Received CO₂=%uppm", ppm);
|
||||||
|
if (this->co2_sensor_ != nullptr)
|
||||||
|
this->co2_sensor_->publish_state(ppm);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void HC8Component::calibrate(uint16_t baseline) {
|
||||||
|
ESP_LOGD(TAG, "HC8 Calibrating baseline to %uppm", baseline);
|
||||||
|
|
||||||
|
std::array<uint8_t, 6> command{};
|
||||||
|
std::copy(begin(HC8_COMMAND_CALIBRATE_PREAMBLE), end(HC8_COMMAND_CALIBRATE_PREAMBLE), begin(command));
|
||||||
|
command[3] = baseline >> 8;
|
||||||
|
command[4] = baseline;
|
||||||
|
command[5] = 0;
|
||||||
|
|
||||||
|
// the last byte is a checksum over the data
|
||||||
|
for (uint8_t i = 0; i < 5; ++i)
|
||||||
|
command[5] -= command[i];
|
||||||
|
|
||||||
|
this->write_array(command);
|
||||||
|
this->flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
float HC8Component::get_setup_priority() const { return setup_priority::DATA; }
|
||||||
|
|
||||||
|
void HC8Component::dump_config() {
|
||||||
|
ESP_LOGCONFIG(TAG, "HC8:");
|
||||||
|
LOG_SENSOR(" ", "CO2", this->co2_sensor_);
|
||||||
|
this->check_uart_settings(9600);
|
||||||
|
|
||||||
|
ESP_LOGCONFIG(TAG, " Warmup time: %" PRIu32 " s", this->warmup_seconds_);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace esphome::hc8
|
||||||
37
esphome/components/hc8/hc8.h
Normal file
37
esphome/components/hc8/hc8.h
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/core/component.h"
|
||||||
|
#include "esphome/core/automation.h"
|
||||||
|
#include "esphome/components/sensor/sensor.h"
|
||||||
|
#include "esphome/components/uart/uart.h"
|
||||||
|
|
||||||
|
#include <cinttypes>
|
||||||
|
|
||||||
|
namespace esphome::hc8 {
|
||||||
|
|
||||||
|
class HC8Component : public PollingComponent, public uart::UARTDevice {
|
||||||
|
public:
|
||||||
|
float get_setup_priority() const override;
|
||||||
|
|
||||||
|
void setup() override;
|
||||||
|
void update() override;
|
||||||
|
void dump_config() override;
|
||||||
|
|
||||||
|
void calibrate(uint16_t baseline);
|
||||||
|
|
||||||
|
void set_co2_sensor(sensor::Sensor *co2_sensor) { co2_sensor_ = co2_sensor; }
|
||||||
|
void set_warmup_seconds(uint32_t seconds) { warmup_seconds_ = seconds; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
sensor::Sensor *co2_sensor_{nullptr};
|
||||||
|
uint32_t warmup_seconds_{0};
|
||||||
|
};
|
||||||
|
|
||||||
|
template<typename... Ts> class HC8CalibrateAction : public Action<Ts...>, public Parented<HC8Component> {
|
||||||
|
public:
|
||||||
|
TEMPLATABLE_VALUE(uint16_t, baseline)
|
||||||
|
|
||||||
|
void play(const Ts &...x) override { this->parent_->calibrate(this->baseline_.value(x...)); }
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace esphome::hc8
|
||||||
79
esphome/components/hc8/sensor.py
Normal file
79
esphome/components/hc8/sensor.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
from esphome import automation
|
||||||
|
import esphome.codegen as cg
|
||||||
|
from esphome.components import sensor, uart
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome.const import (
|
||||||
|
CONF_BASELINE,
|
||||||
|
CONF_CO2,
|
||||||
|
CONF_ID,
|
||||||
|
DEVICE_CLASS_CARBON_DIOXIDE,
|
||||||
|
ICON_MOLECULE_CO2,
|
||||||
|
STATE_CLASS_MEASUREMENT,
|
||||||
|
UNIT_PARTS_PER_MILLION,
|
||||||
|
)
|
||||||
|
|
||||||
|
DEPENDENCIES = ["uart"]
|
||||||
|
|
||||||
|
CONF_WARMUP_TIME = "warmup_time"
|
||||||
|
|
||||||
|
hc8_ns = cg.esphome_ns.namespace("hc8")
|
||||||
|
HC8Component = hc8_ns.class_("HC8Component", cg.PollingComponent, uart.UARTDevice)
|
||||||
|
HC8CalibrateAction = hc8_ns.class_("HC8CalibrateAction", automation.Action)
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = (
|
||||||
|
cv.Schema(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.declare_id(HC8Component),
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
cv.Optional(
|
||||||
|
CONF_WARMUP_TIME, default="75s"
|
||||||
|
): cv.positive_time_period_seconds,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.extend(cv.polling_component_schema("60s"))
|
||||||
|
.extend(uart.UART_DEVICE_SCHEMA)
|
||||||
|
)
|
||||||
|
|
||||||
|
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||||
|
"hc8",
|
||||||
|
baud_rate=9600,
|
||||||
|
require_rx=True,
|
||||||
|
require_tx=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def to_code(config):
|
||||||
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
|
await cg.register_component(var, config)
|
||||||
|
await uart.register_uart_device(var, config)
|
||||||
|
|
||||||
|
if co2 := config.get(CONF_CO2):
|
||||||
|
sens = await sensor.new_sensor(co2)
|
||||||
|
cg.add(var.set_co2_sensor(sens))
|
||||||
|
|
||||||
|
cg.add(var.set_warmup_seconds(config[CONF_WARMUP_TIME]))
|
||||||
|
|
||||||
|
|
||||||
|
CALIBRATION_ACTION_SCHEMA = cv.Schema(
|
||||||
|
{
|
||||||
|
cv.Required(CONF_ID): cv.use_id(HC8Component),
|
||||||
|
cv.Required(CONF_BASELINE): cv.templatable(cv.uint16_t),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@automation.register_action(
|
||||||
|
"hc8.calibrate", HC8CalibrateAction, CALIBRATION_ACTION_SCHEMA
|
||||||
|
)
|
||||||
|
async def hc8_calibration_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_BASELINE], args, cg.uint16)
|
||||||
|
cg.add(var.set_baseline(template_))
|
||||||
|
return var
|
||||||
13
tests/components/hc8/common.yaml
Normal file
13
tests/components/hc8/common.yaml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
esphome:
|
||||||
|
on_boot:
|
||||||
|
then:
|
||||||
|
- hc8.calibrate:
|
||||||
|
id: hc8_sensor
|
||||||
|
baseline: 420
|
||||||
|
|
||||||
|
sensor:
|
||||||
|
- platform: hc8
|
||||||
|
id: hc8_sensor
|
||||||
|
co2:
|
||||||
|
name: HC8 CO2 Value
|
||||||
|
update_interval: 15s
|
||||||
4
tests/components/hc8/test.esp32-idf.yaml
Normal file
4
tests/components/hc8/test.esp32-idf.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
packages:
|
||||||
|
uart: !include ../../test_build_components/common/uart/esp32-idf.yaml
|
||||||
|
|
||||||
|
<<: !include common.yaml
|
||||||
4
tests/components/hc8/test.esp8266-ard.yaml
Normal file
4
tests/components/hc8/test.esp8266-ard.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
packages:
|
||||||
|
uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml
|
||||||
|
|
||||||
|
<<: !include common.yaml
|
||||||
4
tests/components/hc8/test.rp2040-ard.yaml
Normal file
4
tests/components/hc8/test.rp2040-ard.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
packages:
|
||||||
|
uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml
|
||||||
|
|
||||||
|
<<: !include common.yaml
|
||||||
Reference in New Issue
Block a user