From 34dce0acbf06f0d74a62d06f444684c9984ec64d Mon Sep 17 00:00:00 2001 From: Jevgeni Kiski Date: Tue, 24 Oct 2023 02:35:51 +0300 Subject: [PATCH] AMS iAQ Core CO2 sensor component (#5192) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/iaqcore/__init__.py | 0 esphome/components/iaqcore/iaqcore.cpp | 99 ++++++++++++++++++++++++++ esphome/components/iaqcore/iaqcore.h | 29 ++++++++ esphome/components/iaqcore/sensor.py | 57 +++++++++++++++ tests/test1.yaml | 6 ++ 6 files changed, 192 insertions(+) create mode 100644 esphome/components/iaqcore/__init__.py create mode 100644 esphome/components/iaqcore/iaqcore.cpp create mode 100644 esphome/components/iaqcore/iaqcore.h create mode 100644 esphome/components/iaqcore/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index a8305a6e90..d86109581c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -132,6 +132,7 @@ esphome/components/i2s_audio/* @jesserockz esphome/components/i2s_audio/media_player/* @jesserockz esphome/components/i2s_audio/microphone/* @jesserockz esphome/components/i2s_audio/speaker/* @jesserockz +esphome/components/iaqcore/* @yozik04 esphome/components/ili9xxx/* @clydebarrow @nielsnl68 esphome/components/improv_base/* @esphome/core esphome/components/improv_serial/* @esphome/core diff --git a/esphome/components/iaqcore/__init__.py b/esphome/components/iaqcore/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/iaqcore/iaqcore.cpp b/esphome/components/iaqcore/iaqcore.cpp new file mode 100644 index 0000000000..810e8da0b2 --- /dev/null +++ b/esphome/components/iaqcore/iaqcore.cpp @@ -0,0 +1,99 @@ +#include "iaqcore.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace iaqcore { + +static const char *const TAG = "iaqcore"; + +enum IAQCoreErrorCode : uint8_t { ERROR_OK = 0, ERROR_RUNIN = 0x10, ERROR_BUSY = 0x01, ERROR_ERROR = 0x80 }; + +struct SensorData { + uint16_t co2; + IAQCoreErrorCode status; + int32_t resistance; + uint16_t tvoc; + + SensorData(const uint8_t *buffer) { + this->co2 = encode_uint16(buffer[0], buffer[1]); + this->status = static_cast(buffer[2]); + this->resistance = encode_uint32(buffer[3], buffer[4], buffer[5], buffer[6]); + this->tvoc = encode_uint16(buffer[7], buffer[8]); + } +}; + +void IAQCore::setup() { + if (this->write(nullptr, 0) != i2c::ERROR_OK) { + ESP_LOGD(TAG, "Communication failed!"); + this->mark_failed(); + return; + } +} + +void IAQCore::update() { + uint8_t buffer[sizeof(SensorData)]; + + if (this->read_register(0xB5, buffer, sizeof(buffer), false) != i2c::ERROR_OK) { + ESP_LOGD(TAG, "Read failed"); + this->status_set_warning(); + this->publish_nans_(); + return; + } + + SensorData data(buffer); + + switch (data.status) { + case ERROR_OK: + ESP_LOGD(TAG, "OK"); + break; + case ERROR_RUNIN: + ESP_LOGI(TAG, "Warming up"); + break; + case ERROR_BUSY: + ESP_LOGI(TAG, "Busy"); + break; + case ERROR_ERROR: + ESP_LOGE(TAG, "Error"); + break; + } + + if (data.status != ERROR_OK) { + this->status_set_warning(); + this->publish_nans_(); + return; + } + + if (this->co2_ != nullptr) { + this->co2_->publish_state(data.co2); + } + if (this->tvoc_ != nullptr) { + this->tvoc_->publish_state(data.tvoc); + } + + this->status_clear_warning(); +} + +void IAQCore::publish_nans_() { + if (this->co2_ != nullptr) { + this->co2_->publish_state(NAN); + } + if (this->tvoc_ != nullptr) { + this->tvoc_->publish_state(NAN); + } +} + +void IAQCore::dump_config() { + ESP_LOGCONFIG(TAG, "AMS iAQ Core:"); + LOG_I2C_DEVICE(this); + LOG_UPDATE_INTERVAL(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with AMS iAQ Core failed!"); + } + LOG_SENSOR(" ", "CO2", this->co2_); + LOG_SENSOR(" ", "TVOC", this->tvoc_); +} + +} // namespace iaqcore +} // namespace esphome diff --git a/esphome/components/iaqcore/iaqcore.h b/esphome/components/iaqcore/iaqcore.h new file mode 100644 index 0000000000..f343c2a705 --- /dev/null +++ b/esphome/components/iaqcore/iaqcore.h @@ -0,0 +1,29 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace iaqcore { + +class IAQCore : public PollingComponent, public i2c::I2CDevice { + public: + void set_co2(sensor::Sensor *co2) { co2_ = co2; } + void set_tvoc(sensor::Sensor *tvoc) { tvoc_ = tvoc; } + + void setup() override; + void update() override; + void dump_config() override; + + float get_setup_priority() const override { return setup_priority::DATA; } + + protected: + sensor::Sensor *co2_{nullptr}; + sensor::Sensor *tvoc_{nullptr}; + + void publish_nans_(); +}; + +} // namespace iaqcore +} // namespace esphome diff --git a/esphome/components/iaqcore/sensor.py b/esphome/components/iaqcore/sensor.py new file mode 100644 index 0000000000..51c5b283b7 --- /dev/null +++ b/esphome/components/iaqcore/sensor.py @@ -0,0 +1,57 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_CO2, + CONF_ID, + CONF_TVOC, + DEVICE_CLASS_CARBON_DIOXIDE, + DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + STATE_CLASS_MEASUREMENT, + UNIT_PARTS_PER_MILLION, + UNIT_PARTS_PER_BILLION, +) + +DEPENDENCIES = ["i2c"] +CODEOWNERS = ["@yozik04"] + + +iaqcore_ns = cg.esphome_ns.namespace("iaqcore") +iAQCore = iaqcore_ns.class_("IAQCore", cg.PollingComponent, i2c.I2CDevice) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(iAQCore), + cv.Optional(CONF_CO2): sensor.sensor_schema( + unit_of_measurement=UNIT_PARTS_PER_MILLION, + accuracy_decimals=0, + device_class=DEVICE_CLASS_CARBON_DIOXIDE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TVOC): sensor.sensor_schema( + unit_of_measurement=UNIT_PARTS_PER_BILLION, + accuracy_decimals=0, + device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x5A)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + if co2_config := config.get(CONF_CO2): + sens = await sensor.new_sensor(co2_config) + cg.add(var.set_co2(sens)) + + if tvoc_config := config.get(CONF_TVOC): + sens = await sensor.new_sensor(tvoc_config) + cg.add(var.set_tvoc(sens)) + + await i2c.register_i2c_device(var, config) diff --git a/tests/test1.yaml b/tests/test1.yaml index e1db8dda8d..0a9867c0f9 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1442,6 +1442,12 @@ sensor: id: temp_etuve humidity: name: "Humidity hyt271" + - platform: iaqcore + i2c_id: i2c_bus + co2: + name: "iAQ Core CO2 Sensor" + tvoc: + name: "iAQ Core TVOC Sensor" - platform: tmp1075 name: "Temperature TMP1075" update_interval: 10s