mirror of
https://github.com/esphome/esphome.git
synced 2026-02-28 09:54:19 -07:00
[sen6x] Add SEN6x sensor support (#12553)
Co-authored-by: Martin Ebner <martinebner@me.com> Co-authored-by: Tobias Stanzel <tobi.stanzel@gmail.com> Co-authored-by: Big Mike <mike@bigmike.land> Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
This commit is contained in:
@@ -429,6 +429,7 @@ esphome/components/select/* @esphome/core
|
||||
esphome/components/sen0321/* @notjj
|
||||
esphome/components/sen21231/* @shreyaskarnik
|
||||
esphome/components/sen5x/* @martgras
|
||||
esphome/components/sen6x/* @martgras @mebner86 @mikelawrence @tuct
|
||||
esphome/components/sensirion_common/* @martgras
|
||||
esphome/components/sensor/* @esphome/core
|
||||
esphome/components/sfa30/* @ghsensdev
|
||||
|
||||
0
esphome/components/sen6x/__init__.py
Normal file
0
esphome/components/sen6x/__init__.py
Normal file
376
esphome/components/sen6x/sen6x.cpp
Normal file
376
esphome/components/sen6x/sen6x.cpp
Normal file
@@ -0,0 +1,376 @@
|
||||
#include "sen6x.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <cmath>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
namespace esphome::sen6x {
|
||||
|
||||
static const char *const TAG = "sen6x";
|
||||
|
||||
static constexpr uint16_t SEN6X_CMD_GET_DATA_READY_STATUS = 0x0202;
|
||||
static constexpr uint16_t SEN6X_CMD_GET_FIRMWARE_VERSION = 0xD100;
|
||||
static constexpr uint16_t SEN6X_CMD_GET_PRODUCT_NAME = 0xD014;
|
||||
static constexpr uint16_t SEN6X_CMD_GET_SERIAL_NUMBER = 0xD033;
|
||||
|
||||
static constexpr uint16_t SEN6X_CMD_READ_MEASUREMENT = 0x0300; // SEN66 only!
|
||||
static constexpr uint16_t SEN6X_CMD_READ_MEASUREMENT_SEN62 = 0x04A3;
|
||||
static constexpr uint16_t SEN6X_CMD_READ_MEASUREMENT_SEN63C = 0x0471;
|
||||
static constexpr uint16_t SEN6X_CMD_READ_MEASUREMENT_SEN65 = 0x0446;
|
||||
static constexpr uint16_t SEN6X_CMD_READ_MEASUREMENT_SEN68 = 0x0467;
|
||||
static constexpr uint16_t SEN6X_CMD_READ_MEASUREMENT_SEN69C = 0x04B5;
|
||||
|
||||
static constexpr uint16_t SEN6X_CMD_START_MEASUREMENTS = 0x0021;
|
||||
static constexpr uint16_t SEN6X_CMD_RESET = 0xD304;
|
||||
|
||||
static inline void set_read_command_and_words(SEN6XComponent::Sen6xType type, uint16_t &read_cmd, uint8_t &read_words) {
|
||||
read_cmd = SEN6X_CMD_READ_MEASUREMENT;
|
||||
read_words = 9;
|
||||
switch (type) {
|
||||
case SEN6XComponent::SEN62:
|
||||
read_cmd = SEN6X_CMD_READ_MEASUREMENT_SEN62;
|
||||
read_words = 6;
|
||||
break;
|
||||
case SEN6XComponent::SEN63C:
|
||||
read_cmd = SEN6X_CMD_READ_MEASUREMENT_SEN63C;
|
||||
read_words = 7;
|
||||
break;
|
||||
case SEN6XComponent::SEN65:
|
||||
read_cmd = SEN6X_CMD_READ_MEASUREMENT_SEN65;
|
||||
read_words = 8;
|
||||
break;
|
||||
case SEN6XComponent::SEN66:
|
||||
read_cmd = SEN6X_CMD_READ_MEASUREMENT;
|
||||
read_words = 9;
|
||||
break;
|
||||
case SEN6XComponent::SEN68:
|
||||
read_cmd = SEN6X_CMD_READ_MEASUREMENT_SEN68;
|
||||
read_words = 9;
|
||||
break;
|
||||
case SEN6XComponent::SEN69C:
|
||||
read_cmd = SEN6X_CMD_READ_MEASUREMENT_SEN69C;
|
||||
read_words = 10;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void SEN6XComponent::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up sen6x...");
|
||||
|
||||
// the sensor needs 100 ms to enter the idle state
|
||||
this->set_timeout(100, [this]() {
|
||||
// Reset the sensor to ensure a clean state regardless of prior commands or power issues
|
||||
if (!this->write_command(SEN6X_CMD_RESET)) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
|
||||
// After reset the sensor needs 100 ms to become ready
|
||||
this->set_timeout(100, [this]() {
|
||||
// Step 1: Read serial number (~25ms with I2C delay)
|
||||
uint16_t raw_serial_number[16];
|
||||
if (!this->get_register(SEN6X_CMD_GET_SERIAL_NUMBER, raw_serial_number, 16, 20)) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
this->serial_number_ = SEN6XComponent::sensirion_convert_to_string_in_place(raw_serial_number, 16);
|
||||
ESP_LOGI(TAG, "Serial number: %s", this->serial_number_.c_str());
|
||||
|
||||
// Step 2: Read product name in next loop iteration
|
||||
this->set_timeout(0, [this]() {
|
||||
uint16_t raw_product_name[16];
|
||||
if (!this->get_register(SEN6X_CMD_GET_PRODUCT_NAME, raw_product_name, 16, 20)) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
|
||||
this->product_name_ = SEN6XComponent::sensirion_convert_to_string_in_place(raw_product_name, 16);
|
||||
|
||||
Sen6xType inferred_type = this->infer_type_from_product_name_(this->product_name_);
|
||||
if (this->sen6x_type_ == UNKNOWN) {
|
||||
this->sen6x_type_ = inferred_type;
|
||||
if (inferred_type == UNKNOWN) {
|
||||
ESP_LOGE(TAG, "Unknown product '%s'", this->product_name_.c_str());
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "Type inferred from product: %s", this->product_name_.c_str());
|
||||
} else if (this->sen6x_type_ != inferred_type && inferred_type != UNKNOWN) {
|
||||
ESP_LOGW(TAG, "Configured type (used) mismatches product '%s'", this->product_name_.c_str());
|
||||
}
|
||||
ESP_LOGI(TAG, "Product: %s", this->product_name_.c_str());
|
||||
|
||||
// Validate configured sensors against detected type and disable unsupported ones
|
||||
const bool has_voc_nox = (this->sen6x_type_ == SEN65 || this->sen6x_type_ == SEN66 ||
|
||||
this->sen6x_type_ == SEN68 || this->sen6x_type_ == SEN69C);
|
||||
const bool has_co2 = (this->sen6x_type_ == SEN63C || this->sen6x_type_ == SEN66 || this->sen6x_type_ == SEN69C);
|
||||
const bool has_hcho = (this->sen6x_type_ == SEN68 || this->sen6x_type_ == SEN69C);
|
||||
if (this->voc_sensor_ && !has_voc_nox) {
|
||||
ESP_LOGE(TAG, "VOC requires SEN65, SEN66, SEN68, or SEN69C");
|
||||
this->voc_sensor_ = nullptr;
|
||||
}
|
||||
if (this->nox_sensor_ && !has_voc_nox) {
|
||||
ESP_LOGE(TAG, "NOx requires SEN65, SEN66, SEN68, or SEN69C");
|
||||
this->nox_sensor_ = nullptr;
|
||||
}
|
||||
if (this->co2_sensor_ && !has_co2) {
|
||||
ESP_LOGE(TAG, "CO2 requires SEN63C, SEN66, or SEN69C");
|
||||
this->co2_sensor_ = nullptr;
|
||||
}
|
||||
if (this->hcho_sensor_ && !has_hcho) {
|
||||
ESP_LOGE(TAG, "Formaldehyde requires SEN68 or SEN69C");
|
||||
this->hcho_sensor_ = nullptr;
|
||||
}
|
||||
|
||||
// Step 3: Read firmware version and start measurements in next loop iteration
|
||||
this->set_timeout(0, [this]() {
|
||||
uint16_t raw_firmware_version = 0;
|
||||
if (!this->get_register(SEN6X_CMD_GET_FIRMWARE_VERSION, raw_firmware_version, 20)) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
this->firmware_version_major_ = (raw_firmware_version >> 8) & 0xFF;
|
||||
this->firmware_version_minor_ = raw_firmware_version & 0xFF;
|
||||
ESP_LOGI(TAG, "Firmware: %u.%u", this->firmware_version_major_, this->firmware_version_minor_);
|
||||
|
||||
if (!this->write_command(SEN6X_CMD_START_MEASUREMENTS)) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
|
||||
this->set_timeout(60000, [this]() { this->startup_complete_ = true; });
|
||||
this->initialized_ = true;
|
||||
ESP_LOGD(TAG, "Initialized");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void SEN6XComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"sen6x:\n"
|
||||
" Product: %s\n"
|
||||
" Serial: %s\n"
|
||||
" Firmware: %u.%u\n"
|
||||
" Address: 0x%02X",
|
||||
this->product_name_.c_str(), this->serial_number_.c_str(), this->firmware_version_major_,
|
||||
this->firmware_version_minor_, this->address_);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
LOG_SENSOR(" ", "PM 1.0", this->pm_1_0_sensor_);
|
||||
LOG_SENSOR(" ", "PM 2.5", this->pm_2_5_sensor_);
|
||||
LOG_SENSOR(" ", "PM 4.0", this->pm_4_0_sensor_);
|
||||
LOG_SENSOR(" ", "PM 10.0", this->pm_10_0_sensor_);
|
||||
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
|
||||
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
|
||||
LOG_SENSOR(" ", "VOC", this->voc_sensor_);
|
||||
LOG_SENSOR(" ", "NOx", this->nox_sensor_);
|
||||
LOG_SENSOR(" ", "HCHO", this->hcho_sensor_);
|
||||
LOG_SENSOR(" ", "CO2", this->co2_sensor_);
|
||||
}
|
||||
|
||||
void SEN6XComponent::update() {
|
||||
if (!this->initialized_) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint16_t read_cmd;
|
||||
uint8_t read_words;
|
||||
set_read_command_and_words(this->sen6x_type_, read_cmd, read_words);
|
||||
|
||||
const uint8_t poll_retries = 24;
|
||||
auto poll_ready = std::make_shared<std::function<void(uint8_t)>>();
|
||||
*poll_ready = [this, poll_ready, read_cmd, read_words](uint8_t retries_left) {
|
||||
const uint8_t attempt = static_cast<uint8_t>(poll_retries - retries_left + 1);
|
||||
ESP_LOGV(TAG, "Data ready polling attempt %u", attempt);
|
||||
|
||||
if (!this->write_command(SEN6X_CMD_GET_DATA_READY_STATUS)) {
|
||||
this->status_set_warning();
|
||||
ESP_LOGD(TAG, "write data ready status error (%d)", this->last_error_);
|
||||
return;
|
||||
}
|
||||
|
||||
this->set_timeout(20, [this, poll_ready, retries_left, read_cmd, read_words]() {
|
||||
uint16_t raw_read_status;
|
||||
if (!this->read_data(&raw_read_status, 1)) {
|
||||
this->status_set_warning();
|
||||
ESP_LOGD(TAG, "read data ready status error (%d)", this->last_error_);
|
||||
return;
|
||||
}
|
||||
|
||||
if ((raw_read_status & 0x0001) == 0) {
|
||||
if (retries_left == 0) {
|
||||
this->status_set_warning();
|
||||
ESP_LOGD(TAG, "Data not ready");
|
||||
return;
|
||||
}
|
||||
this->set_timeout(50, [poll_ready, retries_left]() { (*poll_ready)(retries_left - 1); });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this->write_command(read_cmd)) {
|
||||
this->status_set_warning();
|
||||
ESP_LOGD(TAG, "Read measurement failed (%d)", this->last_error_);
|
||||
return;
|
||||
}
|
||||
|
||||
this->set_timeout(20, [this, read_words]() {
|
||||
uint16_t measurements[10];
|
||||
|
||||
if (!this->read_data(measurements, read_words)) {
|
||||
this->status_set_warning();
|
||||
ESP_LOGD(TAG, "Read data failed (%d)", this->last_error_);
|
||||
return;
|
||||
}
|
||||
int8_t voc_index = -1;
|
||||
int8_t nox_index = -1;
|
||||
int8_t hcho_index = -1;
|
||||
int8_t co2_index = -1;
|
||||
bool co2_uint16 = false;
|
||||
switch (this->sen6x_type_) {
|
||||
case SEN62:
|
||||
break;
|
||||
case SEN63C:
|
||||
co2_index = 6;
|
||||
break;
|
||||
case SEN65:
|
||||
voc_index = 6;
|
||||
nox_index = 7;
|
||||
break;
|
||||
case SEN66:
|
||||
voc_index = 6;
|
||||
nox_index = 7;
|
||||
co2_index = 8;
|
||||
co2_uint16 = true;
|
||||
break;
|
||||
case SEN68:
|
||||
voc_index = 6;
|
||||
nox_index = 7;
|
||||
hcho_index = 8;
|
||||
break;
|
||||
case SEN69C:
|
||||
voc_index = 6;
|
||||
nox_index = 7;
|
||||
hcho_index = 8;
|
||||
co2_index = 9;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
float pm_1_0 = measurements[0] / 10.0f;
|
||||
if (measurements[0] == 0xFFFF)
|
||||
pm_1_0 = NAN;
|
||||
float pm_2_5 = measurements[1] / 10.0f;
|
||||
if (measurements[1] == 0xFFFF)
|
||||
pm_2_5 = NAN;
|
||||
float pm_4_0 = measurements[2] / 10.0f;
|
||||
if (measurements[2] == 0xFFFF)
|
||||
pm_4_0 = NAN;
|
||||
float pm_10_0 = measurements[3] / 10.0f;
|
||||
if (measurements[3] == 0xFFFF)
|
||||
pm_10_0 = NAN;
|
||||
float humidity = static_cast<int16_t>(measurements[4]) / 100.0f;
|
||||
if (measurements[4] == 0x7FFF)
|
||||
humidity = NAN;
|
||||
float temperature = static_cast<int16_t>(measurements[5]) / 200.0f;
|
||||
if (measurements[5] == 0x7FFF)
|
||||
temperature = NAN;
|
||||
|
||||
float voc = NAN;
|
||||
float nox = NAN;
|
||||
float hcho = NAN;
|
||||
float co2 = NAN;
|
||||
|
||||
if (voc_index >= 0) {
|
||||
voc = static_cast<int16_t>(measurements[voc_index]) / 10.0f;
|
||||
if (measurements[voc_index] == 0x7FFF)
|
||||
voc = NAN;
|
||||
}
|
||||
if (nox_index >= 0) {
|
||||
nox = static_cast<int16_t>(measurements[nox_index]) / 10.0f;
|
||||
if (measurements[nox_index] == 0x7FFF)
|
||||
nox = NAN;
|
||||
}
|
||||
|
||||
if (hcho_index >= 0) {
|
||||
const uint16_t hcho_raw = measurements[hcho_index];
|
||||
hcho = hcho_raw / 10.0f;
|
||||
if (hcho_raw == 0xFFFF)
|
||||
hcho = NAN;
|
||||
}
|
||||
|
||||
if (co2_index >= 0) {
|
||||
if (co2_uint16) {
|
||||
const uint16_t co2_raw = measurements[co2_index];
|
||||
co2 = static_cast<float>(co2_raw);
|
||||
if (co2_raw == 0xFFFF)
|
||||
co2 = NAN;
|
||||
} else {
|
||||
const int16_t co2_raw = static_cast<int16_t>(measurements[co2_index]);
|
||||
co2 = static_cast<float>(co2_raw);
|
||||
if (co2_raw == 0x7FFF)
|
||||
co2 = NAN;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this->startup_complete_) {
|
||||
ESP_LOGD(TAG, "Startup delay, ignoring values");
|
||||
this->status_clear_warning();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->pm_1_0_sensor_ != nullptr)
|
||||
this->pm_1_0_sensor_->publish_state(pm_1_0);
|
||||
if (this->pm_2_5_sensor_ != nullptr)
|
||||
this->pm_2_5_sensor_->publish_state(pm_2_5);
|
||||
if (this->pm_4_0_sensor_ != nullptr)
|
||||
this->pm_4_0_sensor_->publish_state(pm_4_0);
|
||||
if (this->pm_10_0_sensor_ != nullptr)
|
||||
this->pm_10_0_sensor_->publish_state(pm_10_0);
|
||||
if (this->temperature_sensor_ != nullptr)
|
||||
this->temperature_sensor_->publish_state(temperature);
|
||||
if (this->humidity_sensor_ != nullptr)
|
||||
this->humidity_sensor_->publish_state(humidity);
|
||||
if (this->voc_sensor_ != nullptr)
|
||||
this->voc_sensor_->publish_state(voc);
|
||||
if (this->nox_sensor_ != nullptr)
|
||||
this->nox_sensor_->publish_state(nox);
|
||||
if (this->hcho_sensor_ != nullptr)
|
||||
this->hcho_sensor_->publish_state(hcho);
|
||||
if (this->co2_sensor_ != nullptr)
|
||||
this->co2_sensor_->publish_state(co2);
|
||||
|
||||
this->status_clear_warning();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
(*poll_ready)(poll_retries);
|
||||
}
|
||||
|
||||
SEN6XComponent::Sen6xType SEN6XComponent::infer_type_from_product_name_(const std::string &product_name) {
|
||||
if (product_name == "SEN62")
|
||||
return SEN62;
|
||||
if (product_name == "SEN63C")
|
||||
return SEN63C;
|
||||
if (product_name == "SEN65")
|
||||
return SEN65;
|
||||
if (product_name == "SEN66")
|
||||
return SEN66;
|
||||
if (product_name == "SEN68")
|
||||
return SEN68;
|
||||
if (product_name == "SEN69C")
|
||||
return SEN69C;
|
||||
return UNKNOWN;
|
||||
}
|
||||
|
||||
} // namespace esphome::sen6x
|
||||
43
esphome/components/sen6x/sen6x.h
Normal file
43
esphome/components/sen6x/sen6x.h
Normal file
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/sensirion_common/i2c_sensirion.h"
|
||||
|
||||
namespace esphome::sen6x {
|
||||
|
||||
class SEN6XComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice {
|
||||
SUB_SENSOR(pm_1_0)
|
||||
SUB_SENSOR(pm_2_5)
|
||||
SUB_SENSOR(pm_4_0)
|
||||
SUB_SENSOR(pm_10_0)
|
||||
SUB_SENSOR(temperature)
|
||||
SUB_SENSOR(humidity)
|
||||
SUB_SENSOR(voc)
|
||||
SUB_SENSOR(nox)
|
||||
SUB_SENSOR(co2)
|
||||
SUB_SENSOR(hcho)
|
||||
|
||||
public:
|
||||
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
void update() override;
|
||||
|
||||
enum Sen6xType { SEN62, SEN63C, SEN65, SEN66, SEN68, SEN69C, UNKNOWN };
|
||||
|
||||
void set_type(const std::string &type) { sen6x_type_ = infer_type_from_product_name_(type); }
|
||||
|
||||
protected:
|
||||
Sen6xType infer_type_from_product_name_(const std::string &product_name);
|
||||
|
||||
bool initialized_{false};
|
||||
std::string product_name_;
|
||||
Sen6xType sen6x_type_{UNKNOWN};
|
||||
std::string serial_number_;
|
||||
uint8_t firmware_version_major_{0};
|
||||
uint8_t firmware_version_minor_{0};
|
||||
bool startup_complete_{false};
|
||||
};
|
||||
|
||||
} // namespace esphome::sen6x
|
||||
149
esphome/components/sen6x/sensor.py
Normal file
149
esphome/components/sen6x/sensor.py
Normal file
@@ -0,0 +1,149 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c, sensirion_common, sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_CO2,
|
||||
CONF_FORMALDEHYDE,
|
||||
CONF_HUMIDITY,
|
||||
CONF_ID,
|
||||
CONF_NOX,
|
||||
CONF_PM_1_0,
|
||||
CONF_PM_2_5,
|
||||
CONF_PM_4_0,
|
||||
CONF_PM_10_0,
|
||||
CONF_TEMPERATURE,
|
||||
CONF_TYPE,
|
||||
CONF_VOC,
|
||||
DEVICE_CLASS_AQI,
|
||||
DEVICE_CLASS_CARBON_DIOXIDE,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_PM1,
|
||||
DEVICE_CLASS_PM10,
|
||||
DEVICE_CLASS_PM25,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
ICON_CHEMICAL_WEAPON,
|
||||
ICON_MOLECULE_CO2,
|
||||
ICON_RADIATOR,
|
||||
ICON_THERMOMETER,
|
||||
ICON_WATER_PERCENT,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_MICROGRAMS_PER_CUBIC_METER,
|
||||
UNIT_PARTS_PER_MILLION,
|
||||
UNIT_PERCENT,
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@martgras", "@mebner86", "@mikelawrence", "@tuct"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
AUTO_LOAD = ["sensirion_common"]
|
||||
|
||||
sen6x_ns = cg.esphome_ns.namespace("sen6x")
|
||||
SEN6XComponent = sen6x_ns.class_(
|
||||
"SEN6XComponent", cg.PollingComponent, sensirion_common.SensirionI2CDevice
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(SEN6XComponent),
|
||||
cv.Optional(CONF_TYPE): cv.one_of(
|
||||
"SEN62", "SEN63C", "SEN65", "SEN66", "SEN68", "SEN69C", upper=True
|
||||
),
|
||||
cv.Optional(CONF_PM_1_0): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
|
||||
icon=ICON_CHEMICAL_WEAPON,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_PM1,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_PM_2_5): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
|
||||
icon=ICON_CHEMICAL_WEAPON,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_PM25,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_PM_4_0): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
|
||||
icon=ICON_CHEMICAL_WEAPON,
|
||||
accuracy_decimals=2,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_PM_10_0): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
|
||||
icon=ICON_CHEMICAL_WEAPON,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_PM10,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
icon=ICON_THERMOMETER,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
icon=ICON_WATER_PERCENT,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_VOC): sensor.sensor_schema(
|
||||
icon=ICON_RADIATOR,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_AQI,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_NOX): sensor.sensor_schema(
|
||||
icon=ICON_RADIATOR,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_AQI,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_CO2): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PARTS_PER_MILLION,
|
||||
icon=ICON_MOLECULE_CO2,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_CARBON_DIOXIDE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_FORMALDEHYDE): sensor.sensor_schema(
|
||||
unit_of_measurement="ppb",
|
||||
icon=ICON_RADIATOR,
|
||||
accuracy_decimals=0,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
.extend(i2c.i2c_device_schema(0x6B))
|
||||
)
|
||||
|
||||
SENSOR_MAP = {
|
||||
CONF_PM_1_0: "set_pm_1_0_sensor",
|
||||
CONF_PM_2_5: "set_pm_2_5_sensor",
|
||||
CONF_PM_4_0: "set_pm_4_0_sensor",
|
||||
CONF_PM_10_0: "set_pm_10_0_sensor",
|
||||
CONF_TEMPERATURE: "set_temperature_sensor",
|
||||
CONF_HUMIDITY: "set_humidity_sensor",
|
||||
CONF_VOC: "set_voc_sensor",
|
||||
CONF_NOX: "set_nox_sensor",
|
||||
CONF_CO2: "set_co2_sensor",
|
||||
CONF_FORMALDEHYDE: "set_hcho_sensor",
|
||||
}
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
if CONF_TYPE in config:
|
||||
cg.add(var.set_type(config[CONF_TYPE]))
|
||||
|
||||
for key, func_name in SENSOR_MAP.items():
|
||||
if cfg := config.get(key):
|
||||
sens = await sensor.new_sensor(cfg)
|
||||
cg.add(getattr(var, func_name)(sens))
|
||||
37
tests/components/sen6x/common.yaml
Normal file
37
tests/components/sen6x/common.yaml
Normal file
@@ -0,0 +1,37 @@
|
||||
sensor:
|
||||
# Test with explicit type parameter
|
||||
- platform: sen6x
|
||||
id: sen6x_sensor
|
||||
type: SEN69C
|
||||
i2c_id: i2c_bus
|
||||
temperature:
|
||||
name: Temperature
|
||||
accuracy_decimals: 1
|
||||
humidity:
|
||||
name: Humidity
|
||||
accuracy_decimals: 0
|
||||
pm_1_0:
|
||||
name: PM <1µm Weight concentration
|
||||
id: pm_1_0
|
||||
accuracy_decimals: 1
|
||||
pm_2_5:
|
||||
name: PM <2.5µm Weight concentration
|
||||
id: pm_2_5
|
||||
accuracy_decimals: 1
|
||||
pm_4_0:
|
||||
name: PM <4µm Weight concentration
|
||||
id: pm_4_0
|
||||
accuracy_decimals: 1
|
||||
pm_10_0:
|
||||
name: PM <10µm Weight concentration
|
||||
id: pm_10_0
|
||||
accuracy_decimals: 1
|
||||
nox:
|
||||
name: NOx
|
||||
voc:
|
||||
name: VOC
|
||||
co2:
|
||||
name: Carbon Dioxide
|
||||
formaldehyde:
|
||||
name: Formaldehyde
|
||||
address: 0x6B
|
||||
4
tests/components/sen6x/test.esp32-idf.yaml
Normal file
4
tests/components/sen6x/test.esp32-idf.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
4
tests/components/sen6x/test.esp8266-ard.yaml
Normal file
4
tests/components/sen6x/test.esp8266-ard.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
4
tests/components/sen6x/test.rp2040-ard.yaml
Normal file
4
tests/components/sen6x/test.rp2040-ard.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
Reference in New Issue
Block a user