diff --git a/esphome/components/ccs811/__init__.py b/esphome/components/ccs811/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/ccs811/ccs811.cpp b/esphome/components/ccs811/ccs811.cpp new file mode 100644 index 0000000000..8672c68ec8 --- /dev/null +++ b/esphome/components/ccs811/ccs811.cpp @@ -0,0 +1,123 @@ +#include "ccs811.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace ccs811 { + +static const char *TAG = "ccs811"; + +// based on +// - https://cdn.sparkfun.com/datasheets/BreakoutBoards/CCS811_Programming_Guide.pdf + +#define CHECK_TRUE(f, error_code) \ + if (!(f)) { \ + this->mark_failed(); \ + this->error_code_ = (error_code); \ + return; \ + } + +#define CHECKED_IO(f) CHECK_TRUE(f, COMMUNICAITON_FAILED) + +void CCS811Component::setup() { + // page 9 programming guide - hwid is always 0x81 + uint8_t hw_id; + CHECKED_IO(this->read_byte(0x20, &hw_id)) + CHECK_TRUE(hw_id == 0x81, INVALID_ID) + + // software reset, page 3 - allowed to fail + this->write_bytes(0xFF, {0x11, 0xE5, 0x72, 0x8A}); + delay(5); + + // page 10, APP_START + CHECK_TRUE(!this->status_has_error_(), SENSOR_REPORTED_ERROR) + CHECK_TRUE(this->status_app_is_valid_(), APP_INVALID) + CHECK_TRUE(this->write_bytes(0xF4, {}), APP_START_FAILED) + // App setup, wait for it to load + delay(1); + + // set MEAS_MODE (page 5) + uint8_t meas_mode = 0; + uint32_t interval = this->get_update_interval(); + if (interval <= 1000) + meas_mode = 1 << 4; + else if (interval <= 10000) + meas_mode = 2 << 4; + else + meas_mode = 3 << 4; + + CHECKED_IO(this->write_byte(0x01, meas_mode)) + + if (this->baseline_.has_value()) { + // baseline available, write to sensor + this->write_bytes(0x11, decode_uint16(*this->baseline_)); + } +} +void CCS811Component::update() { + if (!this->status_has_data_()) + this->status_set_warning(); + + // page 12 - alg result data + auto alg_data = this->read_bytes<4>(0x02); + if (!alg_data.has_value()) { + ESP_LOGW(TAG, "Reading CCS811 data failed!"); + this->status_set_warning(); + return; + } + auto res = *alg_data; + uint16_t co2 = encode_uint16(res[0], res[1]); + uint16_t tvoc = encode_uint16(res[2], res[3]); + + // also print baseline + auto baseline_data = this->read_bytes<2>(0x11); + uint16_t baseline = 0; + if (baseline_data.has_value()) { + baseline = encode_uint16((*baseline_data)[0], (*baseline_data)[1]); + } + + ESP_LOGD(TAG, "Got co2=%u ppm, tvoc=%u ppb, baseline=0x%04X", co2, tvoc, baseline); + + if (this->co2_ != nullptr) + this->co2_->publish_state(co2); + if (this->tvoc_ != nullptr) + this->tvoc_->publish_state(tvoc); + + this->status_clear_warning(); +} +void CCS811Component::dump_config() { + ESP_LOGCONFIG(TAG, "CCS811"); + LOG_I2C_DEVICE(this) + LOG_UPDATE_INTERVAL(this) + LOG_SENSOR(" ", "CO2 Sesnor", this->co2_) + LOG_SENSOR(" ", "TVOC Sensor", this->tvoc_) + if (this->baseline_) { + ESP_LOGCONFIG(TAG, " Baseline: %04X", *this->baseline_); + } else { + ESP_LOGCONFIG(TAG, " Baseline: NOT SET"); + } + if (this->is_failed()) { + switch (this->error_code_) { + case COMMUNICAITON_FAILED: + ESP_LOGW(TAG, "Communication failed! Is the sensor connected?"); + break; + case INVALID_ID: + ESP_LOGW(TAG, "Sensor reported an invalid ID. Is this a CCS811?"); + break; + case SENSOR_REPORTED_ERROR: + ESP_LOGW(TAG, "Sensor reported internal error"); + break; + case APP_INVALID: + ESP_LOGW(TAG, "Sensor reported invalid APP installed."); + break; + case APP_START_FAILED: + ESP_LOGW(TAG, "Sensor reported APP start failed."); + break; + case UNKNOWN: + default: + ESP_LOGW(TAG, "Unknown setup error!"); + break; + } + } +} + +} // namespace ccs811 +} // namespace esphome diff --git a/esphome/components/ccs811/ccs811.h b/esphome/components/ccs811/ccs811.h new file mode 100644 index 0000000000..7781cfd9a5 --- /dev/null +++ b/esphome/components/ccs811/ccs811.h @@ -0,0 +1,47 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/preferences.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace ccs811 { + +class CCS811Component : public PollingComponent, public i2c::I2CDevice { + public: + void set_co2(sensor::Sensor *co2) { co2_ = co2; } + void set_tvoc(sensor::Sensor *tvoc) { tvoc_ = tvoc; } + void set_baseline(uint16_t baseline) { baseline_ = baseline; } + + /// Setup the sensor and test for a connection. + void setup() override; + /// Schedule temperature+pressure readings. + void update() override; + + void dump_config() override; + + float get_setup_priority() const override { return setup_priority::DATA; } + + protected: + optional read_status_() { return this->read_byte(0x00); } + bool status_has_error_() { return this->read_status_().value_or(1) & 1; } + bool status_app_is_valid_() { return this->read_status_().value_or(0) & (1 << 4); } + bool status_has_data_() { return this->read_status_().value_or(0) & (1 << 3); } + + enum ErrorCode { + UNKNOWN, + COMMUNICAITON_FAILED, + INVALID_ID, + SENSOR_REPORTED_ERROR, + APP_INVALID, + APP_START_FAILED, + } error_code_{UNKNOWN}; + + sensor::Sensor *co2_{nullptr}; + sensor::Sensor *tvoc_{nullptr}; + optional baseline_{}; +}; + +} // namespace ccs811 +} // namespace esphome diff --git a/esphome/components/ccs811/sensor.py b/esphome/components/ccs811/sensor.py new file mode 100644 index 0000000000..40bdd45303 --- /dev/null +++ b/esphome/components/ccs811/sensor.py @@ -0,0 +1,35 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import CONF_ID, ICON_GAS_CYLINDER, ICON_RADIATOR, UNIT_PARTS_PER_MILLION, \ + UNIT_PARTS_PER_BILLION + +DEPENDENCIES = ['i2c'] + +ccs811_ns = cg.esphome_ns.namespace('ccs811') +CCS811Component = ccs811_ns.class_('CCS811Component', cg.PollingComponent, i2c.I2CDevice) + +CONF_ECO2 = 'eco2' +CONF_TVOC = 'tvoc' +CONF_BASELINE = 'baseline' + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(CCS811Component), + cv.Required(CONF_ECO2): sensor.sensor_schema(UNIT_PARTS_PER_MILLION, ICON_GAS_CYLINDER, 0), + cv.Required(CONF_TVOC): sensor.sensor_schema(UNIT_PARTS_PER_BILLION, ICON_RADIATOR, 0), + cv.Optional(CONF_BASELINE): cv.hex_uint16_t, +}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x5A)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield i2c.register_i2c_device(var, config) + + sens = yield sensor.new_sensor(config[CONF_ECO2]) + cg.add(var.set_co2(sens)) + sens = yield sensor.new_sensor(config[CONF_TVOC]) + cg.add(var.set_tvoc(sens)) + + if CONF_BASELINE in config: + cg.add(var.set_baseline(config[CONF_BASELINE])) diff --git a/esphome/components/i2c/i2c.h b/esphome/components/i2c/i2c.h index 0184c8020a..e41bd6c5e8 100644 --- a/esphome/components/i2c/i2c.h +++ b/esphome/components/i2c/i2c.h @@ -163,6 +163,14 @@ class I2CDevice { */ bool read_bytes(uint8_t a_register, uint8_t *data, uint8_t len, uint32_t conversion = 0); // NOLINT + template optional> read_bytes(uint8_t a_register) { // NOLINT + std::array res; + if (!this->read_bytes(a_register, res.data(), N)) { + return {}; + } + return res; + } + /** Read len amount of 16-bit words (MSB first) from a register into data. * * @param a_register The register number to write to the bus before reading. @@ -176,6 +184,13 @@ class I2CDevice { /// Read a single byte from a register into the data variable. Return true if successful. bool read_byte(uint8_t a_register, uint8_t *data, uint32_t conversion = 0); // NOLINT + optional read_byte(uint8_t a_register) { // NOLINT + uint8_t data; + if (!this->read_byte(a_register, &data)) + return {}; + return data; + } + /// Read a single 16-bit words (MSB first) from a register into the data variable. Return true if successful. bool read_byte_16(uint8_t a_register, uint16_t *data, uint32_t conversion = 0); // NOLINT @@ -188,6 +203,20 @@ class I2CDevice { */ bool write_bytes(uint8_t a_register, const uint8_t *data, uint8_t len); // NOLINT + /** Write a vector of data to a register. + * + * @param a_register The register to write to. + * @param data The data to write. + * @return If the operation was successful. + */ + bool write_bytes(uint8_t a_register, const std::vector &data) { // NOLINT + return this->write_bytes(a_register, data.data(), data.size()); + } + + template bool write_bytes(uint8_t a_register, const std::array &data) { // NOLINT + return this->write_bytes(a_register, data.data(), data.size()); + } + /** Write len amount of 16-bit words (MSB first) to the specified register. * * @param a_register The register to write the values to. diff --git a/esphome/const.py b/esphome/const.py index b820c0d4c1..6459695710 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -463,6 +463,7 @@ ICON_PERCENT = 'mdi:percent' ICON_PERIODIC_TABLE_CO2 = 'mdi:periodic-table-co2' ICON_POWER = 'mdi:power' ICON_PULSE = 'mdi:pulse' +ICON_RADIATOR = 'mdi:radiator' ICON_RESTART = 'mdi:restart' ICON_ROTATE_RIGHT = 'mdi:rotate-right' ICON_SCALE = 'mdi:scale' @@ -492,6 +493,7 @@ UNIT_MICROSIEMENS_PER_CENTIMETER = u'µS/cm' UNIT_MICROTESLA = u'µT' UNIT_OHM = u'Ω' UNIT_PARTS_PER_MILLION = 'ppm' +UNIT_PARTS_PER_BILLION = 'ppb' UNIT_PERCENT = '%' UNIT_PULSES_PER_MINUTE = 'pulses/min' UNIT_SECOND = 's' diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 093f0f7d30..769a7825da 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -307,4 +307,11 @@ bool str_endswith(const std::string &full, const std::string &ending) { return full.rfind(ending) == (full.size() - ending.size()); } +uint16_t encode_uint16(uint8_t msb, uint8_t lsb) { return (uint16_t(msb) << 8) | uint16_t(lsb); } +std::array decode_uint16(uint16_t value) { + uint8_t msb = (value >> 8) & 0xFF; + uint8_t lsb = (value >> 0) & 0xFF; + return {msb, lsb}; +} + } // namespace esphome diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 95fdf972e4..d21cb85b7d 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -127,6 +127,11 @@ uint8_t reverse_bits_8(uint8_t x); uint16_t reverse_bits_16(uint16_t x); uint32_t reverse_bits_32(uint32_t x); +/// Encode a 16-bit unsigned integer given a most and least-significant byte. +uint16_t encode_uint16(uint8_t msb, uint8_t lsb); +/// Decode a 16-bit unsigned integer into an array of two values: most significant byte, least significant byte. +std::array decode_uint16(uint16_t value); + /** Cross-platform method to disable interrupts. * * Useful when you need to do some timing-dependent communication. diff --git a/tests/test1.yaml b/tests/test1.yaml index 4efcb6ac5a..47b1c5ab67 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -511,6 +511,13 @@ sensor: name: "SDS011 PM10.0" update_interval: 5min rx_only: false + - platform: ccs811 + eco2: + name: CCS811 eCO2 + tvoc: + name: CCS811 TVOC + update_interval: 30s + baseline: 0x4242 esp32_touch: