Skip to content
This repository was archived by the owner on Sep 27, 2023. It is now read-only.
19 changes: 19 additions & 0 deletions src/esphome/application.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1073,6 +1073,25 @@ text_sensor::TemplateTextSensor *Application::make_template_text_sensor(const st
}
#endif

#ifdef USE_CCS811_SENSOR
sensor::CCS811Component *Application::make_ccs811_sensor(const std::string &eco2_name, const std::string &tvoc_name, const std::string &switch_name, const std::string &status_name, const std::string &save_topic, uint32_t update_interval, uint8_t address, time::RealTimeClockComponent *time) {
CCS811Component::InitStruct init_struct = {
.eco2_name = eco2_name,
.tvoc_name = tvoc_name,
.switch_name = switch_name,
.status_name = status_name,
.save_topic = save_topic
};
auto* ccs811 = this->register_component(new sensor::CCS811Component(this->i2c_, init_struct, update_interval, address, time));
this->register_sensor(ccs811->get_eco2_sensor());
this->register_sensor(ccs811->get_tvoc_sensor());
this->register_text_sensor(ccs811->get_status_label());
this->register_switch(ccs811->get_baseline_switch());
return ccs811;
}

#endif

#ifdef USE_CSE7766
sensor::CSE7766Component *Application::make_cse7766(UARTComponent *parent, uint32_t update_interval) {
return this->register_component(new sensor::CSE7766Component(parent, update_interval));
Expand Down
8 changes: 8 additions & 0 deletions src/esphome/application.h
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
#include "esphome/sensor/bme680_component.h"
#include "esphome/sensor/bmp085_component.h"
#include "esphome/sensor/bmp280_component.h"
#include "esphome/sensor/ccs811_component.h"
#include "esphome/sensor/cse7766.h"
#include "esphome/sensor/custom_sensor.h"
#include "esphome/sensor/dallas_component.h"
Expand Down Expand Up @@ -797,6 +798,13 @@ class Application {
std::string entity_id);
#endif

#ifdef USE_CCS811_SENSOR
sensor::CCS811Component *make_ccs811_sensor(const std::string &eco2_name, const std::string &tvoc_name,
const std::string &switch_name, const std::string &status_name,
const std::string &save_topic, uint32_t update_interval,
uint8_t address, time::RealTimeClockComponent *time);
#endif

#ifdef USE_CSE7766
sensor::CSE7766Component *make_cse7766(UARTComponent *parent, uint32_t update_interval = 60000);
#endif
Expand Down
1 change: 1 addition & 0 deletions src/esphome/defines.h
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@
#define USE_VERSION_TEXT_SENSOR
#define USE_TEMPLATE_TEXT_SENSOR
#define USE_MQTT_SUBSCRIBE_SENSOR
#define USE_CCS811_SENSOR
#define USE_CSE7766
#define USE_PMSX003
#define USE_ENDSTOP_COVER
Expand Down
198 changes: 198 additions & 0 deletions src/esphome/sensor/ccs811_component.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
#include "esphome/defines.h"

#ifdef USE_CCS811_SENSOR

#include "esphome/sensor/ccs811_component.h"
#include "esphome/mqtt/mqtt_client_component.h"
#include "esphome/helpers.h"
#include "esphome/log.h"

constexpr auto BASELINE_TIMEOUT_NAME = "BASELINE_TIMEOUT";
constexpr auto LAST_ACTICITY_TIME_TIMEOUT_NAME = "LAST_ACTICITY_TIME_TIMEOUT_NAME";
constexpr auto MQTT_SUBSCRIBE_TIMEOUT_SEC = 15;
constexpr auto LAST_ACTIVITY_DIFFERENCE = 300;
constexpr auto WARM_UP_TIMEOUT_SEC = 1800; // 30 minutes
constexpr auto BASELINE_TOPIC_SUFFIX = "/baseline";
constexpr auto LAST_ACTIVITY_TIME_TOPIC_SUFFIX = "/time";

ESPHOME_NAMESPACE_BEGIN

namespace sensor {

const char* ccs811_status_to_string(CCS811Status status) {
switch (status) {
case CCS811Status::INITIALIZING: return "INITIALIZING";
case CCS811Status::SEARCHING_FOR_LAST_ACTIVITY_TIME: return "SEARCHING_FOR_LAST_ACTIVITY_TIME";
case CCS811Status::WARMING_UP: return "WARMING_UP";
case CCS811Status::SEARCHING_FOR_BASELINE: return "SEARCHING_FOR_BASELINE";
case CCS811Status::WAITING_FOR_BASELINE_SETTING: return "WAITING_FOR_BASELINE_SETTING";
case CCS811Status::MEASURING: return "MEASURING";
default: return "UNKNOWN";
}
}

const char* ccs811_status_to_human_readable(CCS811Status status) {
switch (status) {
case CCS811Status::INITIALIZING: return "Initializing ...";
case CCS811Status::SEARCHING_FOR_LAST_ACTIVITY_TIME: return "Searching for last activity time ...";
case CCS811Status::WARMING_UP: return "Warming up ...";
case CCS811Status::SEARCHING_FOR_BASELINE: return "Searching for baseline ...";
case CCS811Status::WAITING_FOR_BASELINE_SETTING: return "Waiting for baseline setting ...";
case CCS811Status::MEASURING: return "Measuring ...";
default: return "Unknown ...";
}
}

void CCS811Component::setup() {
this->set_status_(CCS811Status::INITIALIZING);
auto return_code = this->sensor_handle_.begin();
ESP_LOGI(CCS811Component::TAG, "Initializing of CCS811 finished with return code: %d!", return_code);
this->arrange_warm_up_();
}

void CCS811Component::update() {
if (this->status_ != CCS811Status::MEASURING) {
this->eco2_.publish_state(-1);
this->tvoc_.publish_state(-1);
} else {
if (this->sensor_handle_.dataAvailable()) {
this->sensor_handle_.readAlgorithmResults();
this->eco2_.publish_state(this->sensor_handle_.getCO2());
this->tvoc_.publish_state(this->sensor_handle_.getTVOC());
}
}

// Renew last activity time
auto now_struct = this->time_->now().to_c_tm();
const std::string time = to_string(mktime(&now_struct));
mqtt::global_mqtt_client->publish(this->last_activity_time_topic_, time, 0, true);
ESP_LOGD(CCS811Component::TAG, "%s: %u ppm", this->eco2_.get_name().c_str(), this->sensor_handle_.getCO2());
ESP_LOGD(CCS811Component::TAG, "%s: %u ppb", this->tvoc_.get_name().c_str(), this->sensor_handle_.getTVOC());
}

void CCS811Component::dump_config() {
ESP_LOGCONFIG(CCS811Component::TAG, "CCS811:");
LOG_I2C_DEVICE(this);
if (this->is_failed()) {
ESP_LOGE(TAG, "Communication with CCS811 failed!");
}
ESP_LOGCONFIG(CCS811Component::TAG, "%s: %u ppm", this->eco2_.get_name().c_str(), this->sensor_handle_.getCO2());
ESP_LOGCONFIG(CCS811Component::TAG, "%s: %u ppb", this->tvoc_.get_name().c_str(), this->sensor_handle_.getTVOC());
}

CCS811Component::BaselineSwitch::BaselineSwitch(const std::string& switch_name, CCS811Component* super):
switch_::Switch(switch_name), super(super) {}

void CCS811Component::BaselineSwitch::write_state(bool state) {
if (state) {
super->publish_baseline_();
}
publish_state(false);
}

CCS811Component::CCS811Component
(I2CComponent *parent, InitStruct init_struct, uint32_t update_interval, uint8_t address, time::RealTimeClockComponent *time):
PollingComponent(update_interval),
I2CDevice(parent, address),
sensor_handle_(address),
eco2_(init_struct.eco2_name, this),
tvoc_(init_struct.tvoc_name, this),
status_label_(init_struct.status_name),
baseline_switch_(init_struct.switch_name, this),
baseline_topic_(init_struct.save_topic + BASELINE_TOPIC_SUFFIX),
last_activity_time_topic_(init_struct.save_topic + LAST_ACTIVITY_TIME_TOPIC_SUFFIX),
time_(time)
{
this->status_label_.set_icon(ICON_INFO);
this->baseline_switch_.set_icon(ICON_CLOUD_UPLOAD);
}

void CCS811Component::set_status_(CCS811Status status) {
if (this->status_ != status) {
ESP_LOGCONFIG(CCS811Component::TAG, "Changing status from %s to %s!", ccs811_status_to_string(this->status_), ccs811_status_to_string(status));
this->status_ = status;
this->status_label_.publish_state(ccs811_status_to_human_readable(this->status_));
}
}

void CCS811Component::arrange_warm_up_() {
this->set_status_(CCS811Status::SEARCHING_FOR_LAST_ACTIVITY_TIME);

this->set_timeout(LAST_ACTICITY_TIME_TIMEOUT_NAME, MQTT_SUBSCRIBE_TIMEOUT_SEC * 1000, [this]() {
ESP_LOGCONFIG(CCS811Component::TAG, "Last activity time not found! Warming up for 30 minutes!");
this->set_status_(CCS811Status::WARMING_UP);

this->set_timeout(WARM_UP_TIMEOUT_SEC * 1000, [this](){
ESP_LOGCONFIG(CCS811Component::TAG, "Warming up finished!");
this->arrange_baseline_();
});
});

mqtt::global_mqtt_client->subscribe(this->last_activity_time_topic_, [this](const std::string &topic, std::string payload){
if (this->status_ == CCS811Status::SEARCHING_FOR_LAST_ACTIVITY_TIME) {
ESP_LOGCONFIG(CCS811Component::TAG, "Last activity time found!");
this->cancel_timeout(LAST_ACTICITY_TIME_TIMEOUT_NAME);

// Recv last activity time
auto now_struct = this->time_->now().to_c_tm();
time_t now = mktime(&now_struct);
long last_activity = atol(payload.c_str());

auto difference = now - last_activity;
if (difference <= LAST_ACTIVITY_DIFFERENCE) {
ESP_LOGCONFIG(CCS811Component::TAG, "No need of warming up!");
this->arrange_baseline_();
}
else {
ESP_LOGCONFIG(CCS811Component::TAG, "Sensor is being warmed up ...");
this->set_status_(CCS811Status::WARMING_UP);

this->set_timeout(WARM_UP_TIMEOUT_SEC * 1000, [this](){
ESP_LOGCONFIG(CCS811Component::TAG, "Warming up finished!");
this->arrange_baseline_();
});
}
}
});
}

void CCS811Component::arrange_baseline_() {
this->set_status_(CCS811Status::SEARCHING_FOR_BASELINE);

// Set baseline searching timeout
this->set_timeout(BASELINE_TIMEOUT_NAME, MQTT_SUBSCRIBE_TIMEOUT_SEC * 1000, [this]() {
ESP_LOGD(CCS811Component::TAG, "Baseline not found! Waiting for baseline setting!");
this->set_status_(CCS811Status::WAITING_FOR_BASELINE_SETTING);
});

mqtt::global_mqtt_client->subscribe(this->baseline_topic_, [this](const std::string &topic, std::string payload){
if (this->status_ == CCS811Status::SEARCHING_FOR_BASELINE) {
ESP_LOGD(CCS811Component::TAG, "Baseline found! Baseline is being set!");
this->cancel_timeout(BASELINE_TIMEOUT_NAME);

// Recv and set baseline
uint16_t baseline = atoi(payload.c_str());
this->sensor_handle_.setBaseline(baseline);
ESP_LOGD(CCS811Component::TAG, "Baseline %u set from topic %s!", baseline, topic.c_str());
this->set_status_(CCS811Status::MEASURING);
}
});
}

void CCS811Component::publish_baseline_() {
if (this->status_ == CCS811Status::MEASURING || this->status_ == CCS811Status::WAITING_FOR_BASELINE_SETTING) {
this->set_status_(CCS811Status::MEASURING);
// Publish baseline
uint16_t baseline = this->sensor_handle_.getBaseline();
std::string baseline_str = to_string(baseline);
mqtt::global_mqtt_client->publish(this->baseline_topic_, baseline_str, 0, true);
ESP_LOGCONFIG(CCS811Component::TAG, "Baseline %s published to topic %s!", baseline_str.c_str(), this->baseline_topic_.c_str());
} else {
ESP_LOGCONFIG(CCS811Component::TAG, "Publish baseline disabled!");
}
}
} // namespace sensor

ESPHOME_NAMESPACE_END

#endif //USE_CCS811_SENSOR
93 changes: 93 additions & 0 deletions src/esphome/sensor/ccs811_component.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
#ifndef ESPHOME_SENSOR_CCS811_COMPONENT_H
#define ESPHOME_SENSOR_CCS811_COMPONENT_H

#include "esphome/defines.h"

#ifdef USE_CCS811_SENSOR

#include "esphome/component.h"
#include "esphome/sensor/sensor.h"
#include "esphome/i2c_component.h"
#include "esphome/switch_/switch.h"
#include "esphome/text_sensor/text_sensor.h"
#include "esphome/time/rtc_component.h"
#include "SparkFunCCS811.h"

ESPHOME_NAMESPACE_BEGIN

namespace sensor {

using CCS811eCO2Sensor = sensor::EmptyPollingParentSensor<0, ICON_GAS_CYLINDER, UNIT_PPM>;
using CCS811TVOCSensor = sensor::EmptyPollingParentSensor<0, ICON_RADIATOR, UNIT_PPB>;

enum class CCS811Status {
INITIALIZING,
SEARCHING_FOR_LAST_ACTIVITY_TIME,
WARMING_UP,
SEARCHING_FOR_BASELINE,
WAITING_FOR_BASELINE_SETTING,
MEASURING
};

class CCS811Component : public PollingComponent, public I2CDevice {
constexpr static auto TAG = "sensor.ccs811";

public:
struct InitStruct {
const std::string &eco2_name;
const std::string &tvoc_name;
const std::string &switch_name;
const std::string &status_name;
const std::string &save_topic;
};

/// Construct the CCS811Component using the provided address and update interval.
CCS811Component(I2CComponent *parent, InitStruct init_struct, uint32_t update_interval, uint8_t address, time::RealTimeClockComponent *time);
/// 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::HARDWARE_LATE;
}

// ========== INTERNAL METHODS ==========
// (In most use cases you won't need these)
/// Get the internal temperature sensor used to expose the temperature as a sensor object.
CCS811eCO2Sensor *get_eco2_sensor() {return &eco2_;}
/// Get the internal pressure sensor used to expose the pressure as a sensor object.
CCS811TVOCSensor *get_tvoc_sensor() {return &tvoc_;}
text_sensor::TextSensor *get_status_label() {return &status_label_;}
switch_::Switch *get_baseline_switch() {return &baseline_switch_;}

protected:
volatile CCS811Status status_;
CCS811 sensor_handle_;
CCS811eCO2Sensor eco2_;
CCS811TVOCSensor tvoc_;
text_sensor::TextSensor status_label_;
struct BaselineSwitch : public switch_::Switch {
CCS811Component* super;
BaselineSwitch(const std::string&, CCS811Component*);
void write_state(bool state) override;
} baseline_switch_;
friend struct BaselineSwitch;
const std::string baseline_topic_;
const std::string last_activity_time_topic_;
time::RealTimeClockComponent *time_;
void set_status_(CCS811Status status_);
void arrange_warm_up_();
void arrange_baseline_();
void publish_baseline_();
};

} // namespace sensor

ESPHOME_NAMESPACE_END

#endif //USE_CCS811_SENSOR

#endif //ESPHOME_SENSOR_CCS811_COMPONENT_H
4 changes: 4 additions & 0 deletions src/esphome/sensor/sensor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ const char UNIT_OHM[] = "Ω";
const char ICON_GAS_CYLINDER[] = "mdi:gas-cylinder";
const char ICON_PERIODIC_TABLE_CO2[] = "mdi:periodic-table-co2";
const char UNIT_PPM[] = "ppm";
const char UNIT_PPB[] = "ppb";
const char UNIT_A[] = "A";
const char UNIT_W[] = "W";
const char ICON_MAGNET[] = "mdi:magnet";
Expand All @@ -160,6 +161,9 @@ const char UNIT_K[] = "K";
const char UNIT_MICROSIEMENS_PER_CENTIMETER[] = "µS/cm";
const char UNIT_MICROGRAMS_PER_CUBIC_METER[] = "µg/m³";
const char ICON_CHEMICAL_WEAPON[] = "mdi:chemical-weapon";
const char ICON_RADIATOR[] = "mdi:radiator";
const char ICON_INFO[] = "mdi:information";
const char ICON_CLOUD_UPLOAD[] = "mdi:cloud-upload";

SensorStateTrigger::SensorStateTrigger(Sensor *parent) {
parent->add_on_state_callback([this](float value) { this->trigger(value); });
Expand Down
4 changes: 4 additions & 0 deletions src/esphome/sensor/sensor.h
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,9 @@ extern const char ICON_LIGHTBULB[];
extern const char ICON_BATTERY[];
extern const char ICON_FLOWER[];
extern const char ICON_CHEMICAL_WEAPON[];
extern const char ICON_RADIATOR[];
extern const char ICON_INFO[];
extern const char ICON_CLOUD_UPLOAD[];

extern const char UNIT_C[];
extern const char UNIT_PERCENT[];
Expand All @@ -340,6 +343,7 @@ extern const char UNIT_M_PER_S_SQUARED[];
extern const char UNIT_LX[];
extern const char UNIT_OHM[];
extern const char UNIT_PPM[];
extern const char UNIT_PPB[];
extern const char UNIT_A[];
extern const char UNIT_W[];
extern const char UNIT_UT[];
Expand Down