mirror of
https://github.com/esphome/esphome.git
synced 2024-11-14 02:58:11 +01:00
CCS811 support (#536)
* CCS811 * Move define, add test * Remove sun artifact * Lint * Lint
This commit is contained in:
parent
855e9367d4
commit
0281914507
9 changed files with 255 additions and 0 deletions
0
esphome/components/ccs811/__init__.py
Normal file
0
esphome/components/ccs811/__init__.py
Normal file
123
esphome/components/ccs811/ccs811.cpp
Normal file
123
esphome/components/ccs811/ccs811.cpp
Normal file
|
@ -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
|
47
esphome/components/ccs811/ccs811.h
Normal file
47
esphome/components/ccs811/ccs811.h
Normal file
|
@ -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<uint8_t> 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<uint16_t> baseline_{};
|
||||
};
|
||||
|
||||
} // namespace ccs811
|
||||
} // namespace esphome
|
35
esphome/components/ccs811/sensor.py
Normal file
35
esphome/components/ccs811/sensor.py
Normal file
|
@ -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]))
|
|
@ -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<size_t N> optional<std::array<uint8_t, N>> read_bytes(uint8_t a_register) { // NOLINT
|
||||
std::array<uint8_t, N> 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<uint8_t> 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<uint8_t> &data) { // NOLINT
|
||||
return this->write_bytes(a_register, data.data(), data.size());
|
||||
}
|
||||
|
||||
template<size_t N> bool write_bytes(uint8_t a_register, const std::array<uint8_t, N> &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.
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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<uint8_t, 2> decode_uint16(uint16_t value) {
|
||||
uint8_t msb = (value >> 8) & 0xFF;
|
||||
uint8_t lsb = (value >> 0) & 0xFF;
|
||||
return {msb, lsb};
|
||||
}
|
||||
|
||||
} // namespace esphome
|
||||
|
|
|
@ -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<uint8_t, 2> decode_uint16(uint16_t value);
|
||||
|
||||
/** Cross-platform method to disable interrupts.
|
||||
*
|
||||
* Useful when you need to do some timing-dependent communication.
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in a new issue