From 8e93735861c785f5883a908022d4418cc413f1de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Gl=C3=B6ckl?= Date: Wed, 3 Mar 2021 01:54:52 +0100 Subject: [PATCH] Add support for the SM300D2 7-in-1 sensor module (#1524) * Added support for SM300D2 sensor module * Fixed lint errors due to added tvoc config * add device class Co-authored-by: Guillermo Ruffino --- esphome/components/ccs811/sensor.py | 3 +- esphome/components/sgp30/sensor.py | 3 +- esphome/components/sm300d2/__init__.py | 0 esphome/components/sm300d2/sensor.py | 61 ++++++++++++++++ esphome/components/sm300d2/sm300d2.cpp | 99 ++++++++++++++++++++++++++ esphome/components/sm300d2/sm300d2.h | 38 ++++++++++ esphome/const.py | 3 + tests/test1.yaml | 16 +++++ 8 files changed, 219 insertions(+), 4 deletions(-) create mode 100644 esphome/components/sm300d2/__init__.py create mode 100644 esphome/components/sm300d2/sensor.py create mode 100644 esphome/components/sm300d2/sm300d2.cpp create mode 100644 esphome/components/sm300d2/sm300d2.h diff --git a/esphome/components/ccs811/sensor.py b/esphome/components/ccs811/sensor.py index f205121e11..869a49dcdc 100644 --- a/esphome/components/ccs811/sensor.py +++ b/esphome/components/ccs811/sensor.py @@ -2,7 +2,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor from esphome.const import CONF_ID, DEVICE_CLASS_EMPTY, ICON_RADIATOR, UNIT_PARTS_PER_MILLION, \ - UNIT_PARTS_PER_BILLION, CONF_TEMPERATURE, CONF_HUMIDITY, ICON_MOLECULE_CO2 + UNIT_PARTS_PER_BILLION, CONF_TEMPERATURE, CONF_TVOC, CONF_HUMIDITY, ICON_MOLECULE_CO2 DEPENDENCIES = ['i2c'] @@ -10,7 +10,6 @@ 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({ diff --git a/esphome/components/sgp30/sensor.py b/esphome/components/sgp30/sensor.py index f28c0f0e71..a3ce1013e0 100644 --- a/esphome/components/sgp30/sensor.py +++ b/esphome/components/sgp30/sensor.py @@ -2,7 +2,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor from esphome.const import CONF_ID, DEVICE_CLASS_EMPTY, ICON_RADIATOR, UNIT_PARTS_PER_MILLION, \ - UNIT_PARTS_PER_BILLION, ICON_MOLECULE_CO2 + UNIT_PARTS_PER_BILLION, ICON_MOLECULE_CO2, CONF_TVOC DEPENDENCIES = ['i2c'] @@ -10,7 +10,6 @@ sgp30_ns = cg.esphome_ns.namespace('sgp30') SGP30Component = sgp30_ns.class_('SGP30Component', cg.PollingComponent, i2c.I2CDevice) CONF_ECO2 = 'eco2' -CONF_TVOC = 'tvoc' CONF_BASELINE = 'baseline' CONF_ECO2_BASELINE = 'eco2_baseline' CONF_TVOC_BASELINE = 'tvoc_baseline' diff --git a/esphome/components/sm300d2/__init__.py b/esphome/components/sm300d2/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/sm300d2/sensor.py b/esphome/components/sm300d2/sensor.py new file mode 100644 index 0000000000..2191143ec2 --- /dev/null +++ b/esphome/components/sm300d2/sensor.py @@ -0,0 +1,61 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, uart +from esphome.const import CONF_ID, CONF_CO2, CONF_FORMALDEHYDE, CONF_TVOC, CONF_PM_2_5, \ + CONF_PM_10_0, CONF_TEMPERATURE, CONF_HUMIDITY, DEVICE_CLASS_EMPTY, DEVICE_CLASS_TEMPERATURE, \ + DEVICE_CLASS_HUMIDITY, UNIT_PARTS_PER_MILLION, UNIT_MICROGRAMS_PER_CUBIC_METER, UNIT_CELSIUS, \ + UNIT_PERCENT, ICON_EMPTY, ICON_MOLECULE_CO2, ICON_FLASK, ICON_CHEMICAL_WEAPON, ICON_GRAIN + +DEPENDENCIES = ['uart'] + +sm300d2_ns = cg.esphome_ns.namespace('sm300d2') +SM300D2Sensor = sm300d2_ns.class_('SM300D2Sensor', cg.PollingComponent, uart.UARTDevice) + +CONFIG_SCHEMA = cv.All(cv.Schema({ + cv.GenerateID(): cv.declare_id(SM300D2Sensor), + + cv.Optional(CONF_CO2): + sensor.sensor_schema(UNIT_PARTS_PER_MILLION, ICON_MOLECULE_CO2, 0, DEVICE_CLASS_EMPTY), + cv.Optional(CONF_FORMALDEHYDE): + sensor.sensor_schema(UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_FLASK, 0, DEVICE_CLASS_EMPTY), + cv.Optional(CONF_TVOC): + sensor.sensor_schema(UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_CHEMICAL_WEAPON, 0, + DEVICE_CLASS_EMPTY), + cv.Optional(CONF_PM_2_5): + sensor.sensor_schema(UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_GRAIN, 0, DEVICE_CLASS_EMPTY), + cv.Optional(CONF_PM_10_0): + sensor.sensor_schema(UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_GRAIN, 0, DEVICE_CLASS_EMPTY), + cv.Optional(CONF_TEMPERATURE): + sensor.sensor_schema(UNIT_CELSIUS, ICON_EMPTY, 0, DEVICE_CLASS_TEMPERATURE), + cv.Optional(CONF_HUMIDITY): + sensor.sensor_schema(UNIT_PERCENT, ICON_EMPTY, 0, DEVICE_CLASS_HUMIDITY), + +}).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)) + if CONF_FORMALDEHYDE in config: + sens = yield sensor.new_sensor(config[CONF_FORMALDEHYDE]) + cg.add(var.set_formaldehyde_sensor(sens)) + if CONF_TVOC in config: + sens = yield sensor.new_sensor(config[CONF_TVOC]) + cg.add(var.set_tvoc_sensor(sens)) + if CONF_PM_2_5 in config: + sens = yield sensor.new_sensor(config[CONF_PM_2_5]) + cg.add(var.set_pm_2_5_sensor(sens)) + if CONF_PM_10_0 in config: + sens = yield sensor.new_sensor(config[CONF_PM_10_0]) + cg.add(var.set_pm_10_0_sensor(sens)) + if CONF_TEMPERATURE in config: + sens = yield sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature_sensor(sens)) + if CONF_HUMIDITY in config: + sens = yield sensor.new_sensor(config[CONF_HUMIDITY]) + cg.add(var.set_humidity_sensor(sens)) diff --git a/esphome/components/sm300d2/sm300d2.cpp b/esphome/components/sm300d2/sm300d2.cpp new file mode 100644 index 0000000000..25abc82f3c --- /dev/null +++ b/esphome/components/sm300d2/sm300d2.cpp @@ -0,0 +1,99 @@ +#include "sm300d2.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace sm300d2 { + +static const char *TAG = "sm300d2"; +static const uint8_t SM300D2_RESPONSE_LENGTH = 17; + +void SM300D2Sensor::update() { + uint8_t response[SM300D2_RESPONSE_LENGTH]; + + flush(); + bool read_success = read_array(response, SM300D2_RESPONSE_LENGTH); + flush(); + + if (!read_success) { + ESP_LOGW(TAG, "Reading data from SM300D2 failed!"); + status_set_warning(); + return; + } + + if (response[0] != 0x3C || response[1] != 0x02) { + ESP_LOGW(TAG, "Invalid preamble for SM300D2 response!"); + this->status_set_warning(); + return; + } + + uint16_t calculated_checksum = this->sm300d2_checksum_(response); + if (calculated_checksum != response[SM300D2_RESPONSE_LENGTH - 1]) { + ESP_LOGW(TAG, "SM300D2 Checksum doesn't match: 0x%02X!=0x%02X", response[SM300D2_RESPONSE_LENGTH - 1], + calculated_checksum); + this->status_set_warning(); + return; + } + + this->status_clear_warning(); + + ESP_LOGW(TAG, "Successfully read SM300D2 data"); + + const uint16_t co2 = (response[2] * 256) + response[3]; + const uint16_t formaldehyde = (response[4] * 256) + response[5]; + const uint16_t tvoc = (response[6] * 256) + response[7]; + const uint16_t pm_2_5 = (response[8] * 256) + response[9]; + const uint16_t pm_10_0 = (response[10] * 256) + response[11]; + const float temperature = response[12] + (response[13] * 0.1); + const float humidity = response[14] + (response[15] * 0.1); + + ESP_LOGD(TAG, "Received CO₂: %u ppm", co2); + if (this->co2_sensor_ != nullptr) + this->co2_sensor_->publish_state(co2); + + ESP_LOGD(TAG, "Received Formaldehyde: %u µg/m³", formaldehyde); + if (this->formaldehyde_sensor_ != nullptr) + this->formaldehyde_sensor_->publish_state(formaldehyde); + + ESP_LOGD(TAG, "Received TVOC: %u µg/m³", tvoc); + if (this->tvoc_sensor_ != nullptr) + this->tvoc_sensor_->publish_state(tvoc); + + ESP_LOGD(TAG, "Received PM2.5: %u µg/m³", pm_2_5); + if (this->pm_2_5_sensor_ != nullptr) + this->pm_2_5_sensor_->publish_state(pm_2_5); + + ESP_LOGD(TAG, "Received pm_10_0: %u µg/m³", pm_10_0); + if (this->pm_10_0_sensor_ != nullptr) + this->pm_10_0_sensor_->publish_state(pm_10_0); + + ESP_LOGD(TAG, "Received Temperature: %.2f °C", temperature); + if (this->temperature_sensor_ != nullptr) + this->temperature_sensor_->publish_state(temperature); + + ESP_LOGD(TAG, "Received Humidity: %.2f percent", humidity); + if (this->humidity_sensor_ != nullptr) + this->humidity_sensor_->publish_state(humidity); +} + +uint16_t SM300D2Sensor::sm300d2_checksum_(uint8_t *ptr) { + uint8_t sum = 0; + for (int i = 0; i < (SM300D2_RESPONSE_LENGTH - 1); i++) { + sum += *ptr++; + } + return sum; +} + +void SM300D2Sensor::dump_config() { + ESP_LOGCONFIG(TAG, "SM300D2:"); + LOG_SENSOR(" ", "CO2", this->co2_sensor_); + LOG_SENSOR(" ", "Formaldehyde", this->formaldehyde_sensor_); + LOG_SENSOR(" ", "TVOC", this->tvoc_sensor_); + LOG_SENSOR(" ", "PM2.5", this->pm_2_5_sensor_); + LOG_SENSOR(" ", "PM10", this->pm_10_0_sensor_); + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); + this->check_uart_settings(9600); +} + +} // namespace sm300d2 +} // namespace esphome diff --git a/esphome/components/sm300d2/sm300d2.h b/esphome/components/sm300d2/sm300d2.h new file mode 100644 index 0000000000..88c04e9813 --- /dev/null +++ b/esphome/components/sm300d2/sm300d2.h @@ -0,0 +1,38 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/uart/uart.h" + +namespace esphome { +namespace sm300d2 { + +class SM300D2Sensor : 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 set_formaldehyde_sensor(sensor::Sensor *formaldehyde_sensor) { formaldehyde_sensor_ = formaldehyde_sensor; } + void set_tvoc_sensor(sensor::Sensor *tvoc_sensor) { tvoc_sensor_ = tvoc_sensor; } + void set_pm_2_5_sensor(sensor::Sensor *pm_2_5_sensor) { pm_2_5_sensor_ = pm_2_5_sensor; } + void set_pm_10_0_sensor(sensor::Sensor *pm_10_0_sensor) { pm_10_0_sensor_ = pm_10_0_sensor; } + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } + void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; } + + void update() override; + void dump_config() override; + + protected: + uint16_t sm300d2_checksum_(uint8_t *ptr); + + sensor::Sensor *co2_sensor_{nullptr}; + sensor::Sensor *formaldehyde_sensor_{nullptr}; + sensor::Sensor *tvoc_sensor_{nullptr}; + sensor::Sensor *pm_2_5_sensor_{nullptr}; + sensor::Sensor *pm_10_0_sensor_{nullptr}; + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; +}; + +} // namespace sm300d2 +} // namespace esphome diff --git a/esphome/const.py b/esphome/const.py index f652cc1b6d..e431784985 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -538,6 +538,7 @@ CONF_TRIGGER_ID = 'trigger_id' CONF_TRIGGER_PIN = 'trigger_pin' CONF_TURN_OFF_ACTION = 'turn_off_action' CONF_TURN_ON_ACTION = 'turn_on_action' +CONF_TVOC = 'tvoc' CONF_TX_BUFFER_SIZE = 'tx_buffer_size' CONF_TX_PIN = 'tx_pin' CONF_TX_POWER = 'tx_power' @@ -595,10 +596,12 @@ ICON_COUNTER = 'mdi:counter' ICON_CURRENT_AC = 'mdi:current-ac' ICON_EMPTY = '' ICON_FLASH = 'mdi:flash' +ICON_FLASK = 'mdi:flask' ICON_FLASK_OUTLINE = 'mdi:flask-outline' ICON_FLOWER = 'mdi:flower' ICON_GAS_CYLINDER = 'mdi:gas-cylinder' ICON_GAUGE = 'mdi:gauge' +ICON_GRAIN = 'mdi:grain' ICON_LIGHTBULB = 'mdi:lightbulb' ICON_MAGNET = 'mdi:magnet' ICON_MOLECULE_CO2 = 'mdi:molecule-co2' diff --git a/tests/test1.yaml b/tests/test1.yaml index 1b1d3799a0..ff7971d901 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -642,6 +642,22 @@ sensor: co2: name: 'SenseAir CO2 Value' update_interval: 15s + - platform: sm300d2 + co2: + name: "SM300D2 CO2 Value" + formaldehyde: + name: "SM300D2 Formaldehyde Value" + tvoc: + name: "SM300D2 TVOC Value" + pm_2_5: + name: "SM300D2 PM2.5 Value" + pm_10_0: + name: "SM300D2 PM10 Value" + temperature: + name: "SM300D2 Temperature Value" + humidity: + name: "SM300D2 Humidity Value" + update_interval: 60s - platform: sht3xd temperature: name: 'Living Room Temperature 8'