diff --git a/CODEOWNERS b/CODEOWNERS index 13fae0664b..7bb7f310a3 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -131,6 +131,7 @@ esphome/components/cst816/* @clydebarrow esphome/components/cst9220/* @clydebarrow esphome/components/ct_clamp/* @jesserockz esphome/components/current_based/* @djwmarcx +esphome/components/d01/* @ch604 esphome/components/dac7678/* @NickB1 esphome/components/daikin_arc/* @MagicBear esphome/components/daikin_brc/* @hagak diff --git a/esphome/components/d01/__init__.py b/esphome/components/d01/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/d01/d01.cpp b/esphome/components/d01/d01.cpp new file mode 100644 index 0000000000..f7a0c08ec4 --- /dev/null +++ b/esphome/components/d01/d01.cpp @@ -0,0 +1,45 @@ +#include "d01.h" +#include "esphome/core/log.h" + +// uart specification for d01 sensor from https://manuals.plus/ae/1005006417362019: +// +// A frame of serial output data includes 4 bytes, formatted as follows: +// __Characteristic byte: Fixed value 0xA5. +// __Data byte: DATAH is the high 7 bits of the concentration value, and DATAL is the low 7 bits of the concentration +// value. +// __Check byte: The low 7 bits of the sum of all bytes before the check byte. +// +// If the serial output is 4 bytes of data: 0*A5 0*01 0*2C 0*52, then DATAH = 0*01 = 1, DATAL = 0*2C = 44. +// Concentration value = 1*128 + 44 = 172 µg/m³. +// +// The PM2.5 dust concentration value obtained from the dust sensor needs to be calibrated with a K value coefficient +// based on the TSI instrument's photometric method. It is generally recommended to use 0.4. + +namespace esphome::d01 { + +static const char *const TAG = "d01"; + +static const uint8_t D01_FRAME_HEADER = 0xA5; + +void D01SensorComponent::dump_config() { LOG_SENSOR(" ", "D01 PM2.5", this); } + +void D01SensorComponent::loop() { + uint8_t buf[4]; + while (this->available() >= 4) { + if (this->peek() != D01_FRAME_HEADER) { + this->read(); + continue; + } + this->read_array(buf, 4); + uint8_t sum = (buf[0] + buf[1] + buf[2]) & 0x7F; + if (sum != buf[3]) { + ESP_LOGW(TAG, "checksum mismatch"); + continue; + } + uint16_t latest_concentration = (buf[1] & 0x7F) * 128 + (buf[2] & 0x7F); + ESP_LOGV(TAG, "Unadjusted PM2.5 Concentration: %d µg/m³", latest_concentration); + this->publish_state(latest_concentration); + } +} + +} // namespace esphome::d01 diff --git a/esphome/components/d01/d01.h b/esphome/components/d01/d01.h new file mode 100644 index 0000000000..73c7a8711d --- /dev/null +++ b/esphome/components/d01/d01.h @@ -0,0 +1,14 @@ +#pragma once +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/uart/uart.h" + +namespace esphome::d01 { + +class D01SensorComponent final : public sensor::Sensor, public Component, public uart::UARTDevice { + public: + void dump_config() override; + void loop() override; +}; + +} // namespace esphome::d01 diff --git a/esphome/components/d01/sensor.py b/esphome/components/d01/sensor.py new file mode 100644 index 0000000000..5bc5a4e424 --- /dev/null +++ b/esphome/components/d01/sensor.py @@ -0,0 +1,45 @@ +import esphome.codegen as cg +from esphome.components import sensor, uart +import esphome.config_validation as cv +from esphome.const import ( + DEVICE_CLASS_PM25, + ICON_BLUR, + STATE_CLASS_MEASUREMENT, + UNIT_MICROGRAMS_PER_CUBIC_METER, +) +from esphome.types import ConfigType + +CODEOWNERS = ["@ch604"] +DEPENDENCIES = ["uart"] + +d01_ns = cg.esphome_ns.namespace("d01") +D01SensorComponent = d01_ns.class_( + "D01SensorComponent", sensor.Sensor, uart.UARTDevice, cg.Component +) + + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + D01SensorComponent, + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_BLUR, + accuracy_decimals=0, + device_class=DEVICE_CLASS_PM25, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(uart.UART_DEVICE_SCHEMA) +) + +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "d01", + baud_rate=9600, + require_rx=True, + require_tx=False, +) + + +async def to_code(config: ConfigType) -> None: + var = await sensor.new_sensor(config) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) diff --git a/tests/components/d01/common.yaml b/tests/components/d01/common.yaml new file mode 100644 index 0000000000..b59ec06ff0 --- /dev/null +++ b/tests/components/d01/common.yaml @@ -0,0 +1,3 @@ +sensor: + - platform: d01 + name: D01 PM2.5 Concentration diff --git a/tests/components/d01/test.esp32-idf.yaml b/tests/components/d01/test.esp32-idf.yaml new file mode 100644 index 0000000000..b658bfbede --- /dev/null +++ b/tests/components/d01/test.esp32-idf.yaml @@ -0,0 +1,7 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml + d01: !include common.yaml diff --git a/tests/components/d01/test.esp8266-ard.yaml b/tests/components/d01/test.esp8266-ard.yaml new file mode 100644 index 0000000000..876615ae9f --- /dev/null +++ b/tests/components/d01/test.esp8266-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + tx_pin: GPIO0 + rx_pin: GPIO2 + +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml + d01: !include common.yaml diff --git a/tests/components/d01/test.rp2040-ard.yaml b/tests/components/d01/test.rp2040-ard.yaml new file mode 100644 index 0000000000..00ed175b42 --- /dev/null +++ b/tests/components/d01/test.rp2040-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +packages: + uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml + d01: !include common.yaml