diff --git a/esphome/components/senseair/__init__.py b/esphome/components/senseair/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/senseair/senseair.cpp b/esphome/components/senseair/senseair.cpp new file mode 100644 index 0000000000..96f456282f --- /dev/null +++ b/esphome/components/senseair/senseair.cpp @@ -0,0 +1,79 @@ +#include "senseair.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace senseair { + +static const char *TAG = "senseair"; +static const uint8_t SENSEAIR_REQUEST_LENGTH = 8; +static const uint8_t SENSEAIR_RESPONSE_LENGTH = 13; +static const uint8_t SENSEAIR_COMMAND_GET_PPM[] = {0xFE, 0x04, 0x00, 0x00, 0x00, 0x04, 0xE5, 0xC6}; + +void SenseAirComponent::update() { + uint8_t response[SENSEAIR_RESPONSE_LENGTH]; + if (!this->senseair_write_command_(SENSEAIR_COMMAND_GET_PPM, response)) { + ESP_LOGW(TAG, "Reading data from SenseAir failed!"); + this->status_set_warning(); + return; + } + + if (response[0] != 0xFE || response[1] != 0x04) { + ESP_LOGW(TAG, "Invalid preamble from SenseAir!"); + this->status_set_warning(); + return; + } + + uint16_t calc_checksum = this->senseair_checksum_(response, 11); + uint16_t resp_checksum = (uint16_t(response[12]) << 8) | response[11]; + if (resp_checksum != calc_checksum) { + ESP_LOGW(TAG, "SenseAir checksum doesn't match: 0x%02X!=0x%02X", resp_checksum, calc_checksum); + this->status_set_warning(); + return; + } + + this->status_clear_warning(); + const uint8_t length = response[2]; + const uint16_t status = (uint16_t(response[3]) << 8) | response[4]; + const uint16_t ppm = (uint16_t(response[length + 1]) << 8) | response[length + 2]; + + ESP_LOGD(TAG, "SenseAir Received CO₂=%uppm Status=0x%02X", ppm, status); + if (this->co2_sensor_ != nullptr) + this->co2_sensor_->publish_state(ppm); +} + +uint16_t SenseAirComponent::senseair_checksum_(uint8_t *ptr, uint8_t length) { + uint16_t crc = 0xFFFF; + uint8_t i; + while (length--) { + crc ^= *ptr++; + for (i = 0; i < 8; i++) { + if ((crc & 0x01) != 0) { + crc >>= 1; + crc ^= 0xA001; + } else { + crc >>= 1; + } + } + } + return crc; +} + +bool SenseAirComponent::senseair_write_command_(const uint8_t *command, uint8_t *response) { + this->flush(); + this->write_array(command, SENSEAIR_REQUEST_LENGTH); + + if (response == nullptr) + return true; + + bool ret = this->read_array(response, SENSEAIR_RESPONSE_LENGTH); + this->flush(); + return ret; +} + +void SenseAirComponent::dump_config() { + ESP_LOGCONFIG(TAG, "SenseAir:"); + LOG_SENSOR(" ", "CO2", this->co2_sensor_); +} + +} // namespace senseair +} // namespace esphome diff --git a/esphome/components/senseair/senseair.h b/esphome/components/senseair/senseair.h new file mode 100644 index 0000000000..23bcf40b5a --- /dev/null +++ b/esphome/components/senseair/senseair.h @@ -0,0 +1,26 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/uart/uart.h" + +namespace esphome { +namespace senseair { + +class SenseAirComponent : public PollingComponent, public uart::UARTDevice { + public: + float get_setup_priority() const override { return setup_priority::DATA; } + void set_co2_sensor(sensor::Sensor *co2_sensor) { co2_sensor_ = co2_sensor; } + + void update() override; + void dump_config() override; + + protected: + uint16_t senseair_checksum_(uint8_t *ptr, uint8_t length); + bool senseair_write_command_(const uint8_t *command, uint8_t *response); + + sensor::Sensor *co2_sensor_{nullptr}; +}; + +} // namespace senseair +} // namespace esphome diff --git a/esphome/components/senseair/sensor.py b/esphome/components/senseair/sensor.py new file mode 100644 index 0000000000..393bfd5182 --- /dev/null +++ b/esphome/components/senseair/sensor.py @@ -0,0 +1,24 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, uart +from esphome.const import CONF_CO2, CONF_ID, ICON_PERIODIC_TABLE_CO2, UNIT_PARTS_PER_MILLION + +DEPENDENCIES = ['uart'] + +senseair_ns = cg.esphome_ns.namespace('senseair') +SenseAirComponent = senseair_ns.class_('SenseAirComponent', cg.PollingComponent, uart.UARTDevice) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(SenseAirComponent), + cv.Required(CONF_CO2): sensor.sensor_schema(UNIT_PARTS_PER_MILLION, ICON_PERIODIC_TABLE_CO2, 0), +}).extend(cv.polling_component_schema('60s')).extend(uart.UART_DEVICE_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield uart.register_uart_device(var, config) + + if CONF_CO2 in config: + sens = yield sensor.new_sensor(config[CONF_CO2]) + cg.add(var.set_co2_sensor(sens)) diff --git a/tests/test1.yaml b/tests/test1.yaml index 09482f846e..b2293969cd 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -511,6 +511,10 @@ sensor: - platform: pulse_width name: Pulse Width pin: GPIO12 + - platform: senseair + co2: + name: "SenseAir CO2 Value" + update_interval: 15s - platform: sht3xd temperature: name: "Living Room Temperature 8"