From 8aef1514d2dbd95ec4583247eab7e10e9f13806e Mon Sep 17 00:00:00 2001 From: Christian Witting Date: Fri, 15 Dec 2023 19:29:21 +0100 Subject: [PATCH] Add support for DFRobot HCHO Formaldehyde Sensor (SEN0231) --- CODEOWNERS | 1 + esphome/components/sen0231/__init__.py | 1 + esphome/components/sen0231/sen0231.cpp | 66 ++++++++++++++++++++++++++ esphome/components/sen0231/sen0231.h | 24 ++++++++++ esphome/components/sen0231/sensor.py | 34 +++++++++++++ tests/test1.yaml | 10 ++++ 6 files changed, 136 insertions(+) create mode 100644 esphome/components/sen0231/__init__.py create mode 100644 esphome/components/sen0231/sen0231.cpp create mode 100644 esphome/components/sen0231/sen0231.h create mode 100644 esphome/components/sen0231/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index db4431777..6507a9d1a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -280,6 +280,7 @@ esphome/components/sdm_meter/* @jesserockz @polyfaces esphome/components/sdp3x/* @Azimath esphome/components/selec_meter/* @sourabhjaiswal esphome/components/select/* @esphome/core +esphome/components/sen0231/* @cwitting esphome/components/sen0321/* @notjj esphome/components/sen21231/* @shreyaskarnik esphome/components/sen5x/* @martgras diff --git a/esphome/components/sen0231/__init__.py b/esphome/components/sen0231/__init__.py new file mode 100644 index 000000000..7060c50c2 --- /dev/null +++ b/esphome/components/sen0231/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@cwitting"] diff --git a/esphome/components/sen0231/sen0231.cpp b/esphome/components/sen0231/sen0231.cpp new file mode 100644 index 000000000..e341b8e66 --- /dev/null +++ b/esphome/components/sen0231/sen0231.cpp @@ -0,0 +1,66 @@ +#include "sen0231.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace sen0231_sensor { + +static const char *const TAG = "sen0231_sensor.sensor"; + +static constexpr uint8_t START_BYTE = 0; +static constexpr uint8_t NAME_BYTE = 1; +static constexpr uint8_t UNIT_BYTE = 2; +static constexpr uint8_t NUM_DECIMAL_BYTE = 3; +static constexpr uint8_t CONCENTRATION_HIGH_BYTE = 4; +static constexpr uint8_t CONCENTRATION_LOW_BYTE = 5; +static constexpr uint8_t FULL_RANGE_HIGH_BYTE = 6; +static constexpr uint8_t FULL_RANGE_LOW_BYTE = 7; +static constexpr uint8_t CHECKSUM_BYTE = 8; +static constexpr uint8_t NUM_BYTES = 9; + +void Sen0231Sensor::setup() { ESP_LOGCONFIG(TAG, "Setting up sen0231..."); } + +void Sen0231Sensor::update() { this->read_data_(); } + +void Sen0231Sensor::dump_config() { + ESP_LOGCONFIG(TAG, "DF Robot Formaldehyde Sensor sen0231:"); + LOG_UPDATE_INTERVAL(this); +} + +uint8_t check_sum(const uint8_t array[], uint8_t length) { + uint8_t sum = 0; + for (int i = 1; i < length - 1; i++) { + sum += array[i]; + } + sum = (~sum) + 1; + return sum; +} + +void Sen0231Sensor::read_data_() { + uint8_t buffer[NUM_BYTES] = {0}; + + // Read the full buffer, but we are only interested in the last frame + while (available() > 0) { + for (uint8_t i = 0; i < NUM_BYTES - 1; i++) { + // Shift buffer + buffer[i] = buffer[i + 1]; + } + + // Fill into last element + buffer[NUM_BYTES - 1] = read(); + } + + uint8_t check_val = check_sum(buffer, NUM_BYTES); + if ((buffer[START_BYTE] != 0xFF) || (buffer[NAME_BYTE] != 0x17) || (buffer[UNIT_BYTE] != 0x04) || + (buffer[CHECKSUM_BYTE] != check_val)) { + ESP_LOGE(TAG, "Error in received data %d, %d, %d, %d", buffer[START_BYTE], buffer[NAME_BYTE], buffer[UNIT_BYTE], + buffer[CHECKSUM_BYTE]); + return; + } + + float val = ((uint16_t) (buffer[CONCENTRATION_HIGH_BYTE] << 8) + buffer[CONCENTRATION_LOW_BYTE]) / 1000.0; + this->publish_state(val); +} + +} // namespace sen0231_sensor +} // namespace esphome diff --git a/esphome/components/sen0231/sen0231.h b/esphome/components/sen0231/sen0231.h new file mode 100644 index 000000000..76b3d94c6 --- /dev/null +++ b/esphome/components/sen0231/sen0231.h @@ -0,0 +1,24 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/uart/uart.h" + +// ref: +// https://wiki.dfrobot.com/Gravity__HCHO_Sensor_SKU__SEN0231 + +namespace esphome { +namespace sen0231_sensor { + +class Sen0231Sensor : public sensor::Sensor, public PollingComponent, public uart::UARTDevice { + public: + void update() override; + void dump_config() override; + void setup() override; + + protected: + void read_data_(); +}; + +} // namespace sen0231_sensor +} // namespace esphome diff --git a/esphome/components/sen0231/sensor.py b/esphome/components/sen0231/sensor.py new file mode 100644 index 000000000..8c98621e9 --- /dev/null +++ b/esphome/components/sen0231/sensor.py @@ -0,0 +1,34 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import uart, sensor +from esphome.const import ( + ICON_CHEMICAL_WEAPON, + UNIT_PARTS_PER_MILLION, + STATE_CLASS_MEASUREMENT, +) + +CODEOWNERS = ["@cwitting"] +DEPENDENCIES = ["uart"] + +sen0231_sensor_ns = cg.esphome_ns.namespace("sen0231_sensor") +Sen0231Sensor = sen0231_sensor_ns.class_( + "Sen0231Sensor", cg.PollingComponent, uart.UARTDevice +) + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + Sen0231Sensor, + unit_of_measurement=UNIT_PARTS_PER_MILLION, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=3, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend(cv.polling_component_schema("60s")) + .extend(uart.UART_DEVICE_SCHEMA) +) + + +async def to_code(config): + var = await sensor.new_sensor(config) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) diff --git a/tests/test1.yaml b/tests/test1.yaml index 3558fa328..0c0b020a5 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -263,6 +263,11 @@ uart: rx_pin: GPIO10 parity: EVEN baud_rate: 9600 + - id: sen0231_uart + rx_pin: + allow_other_uses: true + number: 27 + baud_rate: 9600 ota: safe_mode: true @@ -1326,6 +1331,11 @@ sensor: id: sen0321_ozone update_interval: 10s i2c_id: i2c_bus + - platform: sen0231 + name: Workshop Formaldehyde Sensor + id: sen0231_formaldehyde + update_interval: 10s + uart_id: sen0231_uart - platform: sgp30 eco2: name: Workshop eCO2