mirror of
https://github.com/esphome/esphome.git
synced 2026-06-24 11:25:35 +00:00
[hdc302x] Add new component (#10160)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
This commit is contained in:
@@ -213,6 +213,7 @@ esphome/components/hbridge/light/* @DotNetDann
|
||||
esphome/components/hbridge/switch/* @dwmw2
|
||||
esphome/components/hc8/* @omartijn
|
||||
esphome/components/hdc2010/* @optimusprimespace @ssieb
|
||||
esphome/components/hdc302x/* @joshuasing
|
||||
esphome/components/he60r/* @clydebarrow
|
||||
esphome/components/heatpumpir/* @rob-deutsch
|
||||
esphome/components/hitachi_ac424/* @sourabhjaiswal
|
||||
|
||||
1
esphome/components/hdc302x/__init__.py
Normal file
1
esphome/components/hdc302x/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
CODEOWNERS = ["@joshuasing"]
|
||||
171
esphome/components/hdc302x/hdc302x.cpp
Normal file
171
esphome/components/hdc302x/hdc302x.cpp
Normal file
@@ -0,0 +1,171 @@
|
||||
#include "hdc302x.h"
|
||||
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::hdc302x {
|
||||
|
||||
static const char *const TAG = "hdc302x.sensor";
|
||||
|
||||
// Commands (per datasheet Table 7-4)
|
||||
static const uint8_t HDC302X_CMD_SOFT_RESET[2] = {0x30, 0xa2};
|
||||
static const uint8_t HDC302X_CMD_CLEAR_STATUS_REGISTER[2] = {0x30, 0x41};
|
||||
|
||||
static const uint8_t HDC302X_CMD_TRIGGER_MSB = 0x24;
|
||||
|
||||
static const uint8_t HDC302X_CMD_HEATER_ENABLE[2] = {0x30, 0x6d};
|
||||
static const uint8_t HDC302X_CMD_HEATER_DISABLE[2] = {0x30, 0x66};
|
||||
static const uint8_t HDC302X_CMD_HEATER_CONFIGURE[2] = {0x30, 0x6e};
|
||||
|
||||
void HDC302XComponent::setup() {
|
||||
// Soft reset the device
|
||||
if (this->write(HDC302X_CMD_SOFT_RESET, 2) != i2c::ERROR_OK) {
|
||||
this->mark_failed(LOG_STR("Soft reset failed"));
|
||||
return;
|
||||
}
|
||||
// Delay SensorRR (reset ready), per datasheet, 6.5.
|
||||
delay(3);
|
||||
|
||||
// Clear status register
|
||||
if (this->write(HDC302X_CMD_CLEAR_STATUS_REGISTER, 2) != i2c::ERROR_OK) {
|
||||
this->mark_failed(LOG_STR("Clear status failed"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void HDC302XComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"HDC302x:\n"
|
||||
" Heater: %s",
|
||||
this->heater_active_ ? "active" : "inactive");
|
||||
LOG_I2C_DEVICE(this);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
LOG_SENSOR(" ", "Temperature", this->temp_sensor_);
|
||||
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
|
||||
}
|
||||
|
||||
void HDC302XComponent::update() {
|
||||
uint8_t cmd[] = {
|
||||
HDC302X_CMD_TRIGGER_MSB,
|
||||
this->power_mode_,
|
||||
};
|
||||
if (this->write(cmd, 2) != i2c::ERROR_OK) {
|
||||
this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
|
||||
// Read data after ADC conversion has completed
|
||||
this->set_timeout(this->conversion_delay_ms_(), [this]() { this->read_data_(); });
|
||||
}
|
||||
|
||||
void HDC302XComponent::start_heater(uint16_t power, uint32_t duration_ms) {
|
||||
if (!this->disable_heater_()) {
|
||||
ESP_LOGD(TAG, "Heater disable before start failed");
|
||||
}
|
||||
if (!this->configure_heater_(power) || !this->enable_heater_()) {
|
||||
ESP_LOGW(TAG, "Heater start failed");
|
||||
return;
|
||||
}
|
||||
this->heater_active_ = true;
|
||||
this->cancel_timeout("heater_off");
|
||||
if (duration_ms > 0) {
|
||||
this->set_timeout("heater_off", duration_ms, [this]() { this->stop_heater(); });
|
||||
}
|
||||
}
|
||||
|
||||
void HDC302XComponent::stop_heater() {
|
||||
this->cancel_timeout("heater_off");
|
||||
if (!this->disable_heater_()) {
|
||||
ESP_LOGW(TAG, "Heater stop failed");
|
||||
}
|
||||
this->heater_active_ = false;
|
||||
}
|
||||
|
||||
bool HDC302XComponent::enable_heater_() {
|
||||
if (this->write(HDC302X_CMD_HEATER_ENABLE, 2) != i2c::ERROR_OK) {
|
||||
ESP_LOGE(TAG, "Enable heater failed");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HDC302XComponent::configure_heater_(uint16_t power_level) {
|
||||
if (power_level > 0x3fff) {
|
||||
ESP_LOGW(TAG, "Heater power 0x%04x exceeds max 0x3fff", power_level);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Heater current level config.
|
||||
uint8_t config[] = {
|
||||
static_cast<uint8_t>((power_level >> 8) & 0xff), // MSB
|
||||
static_cast<uint8_t>(power_level & 0xff) // LSB
|
||||
};
|
||||
|
||||
// Configure level of heater current (per datasheet 7.5.7.8).
|
||||
uint8_t cmd[] = {
|
||||
HDC302X_CMD_HEATER_CONFIGURE[0], HDC302X_CMD_HEATER_CONFIGURE[1], config[0], config[1],
|
||||
crc8(config, 2, 0xff, 0x31, true),
|
||||
};
|
||||
if (this->write(cmd, sizeof(cmd)) != i2c::ERROR_OK) {
|
||||
ESP_LOGE(TAG, "Configure heater failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HDC302XComponent::disable_heater_() {
|
||||
if (this->write(HDC302X_CMD_HEATER_DISABLE, 2) != i2c::ERROR_OK) {
|
||||
ESP_LOGE(TAG, "Disable heater failed");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void HDC302XComponent::read_data_() {
|
||||
uint8_t buf[6];
|
||||
if (this->read(buf, 6) != i2c::ERROR_OK) {
|
||||
this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check checksums
|
||||
if (crc8(buf, 2, 0xff, 0x31, true) != buf[2] || crc8(buf + 3, 2, 0xff, 0x31, true) != buf[5]) {
|
||||
this->status_set_warning(LOG_STR("Read data: invalid CRC"));
|
||||
return;
|
||||
}
|
||||
|
||||
this->status_clear_warning();
|
||||
|
||||
if (this->temp_sensor_ != nullptr) {
|
||||
uint16_t raw_t = encode_uint16(buf[0], buf[1]);
|
||||
// Calculate temperature in Celsius per datasheet section 7.3.3.
|
||||
float temp = -45 + 175 * (float(raw_t) / 65535.0f);
|
||||
this->temp_sensor_->publish_state(temp);
|
||||
}
|
||||
|
||||
if (this->humidity_sensor_ != nullptr) {
|
||||
uint16_t raw_rh = encode_uint16(buf[3], buf[4]);
|
||||
// Calculate RH% per datasheet section 7.3.3.
|
||||
float humidity = 100 * (float(raw_rh) / 65535.0f);
|
||||
this->humidity_sensor_->publish_state(humidity);
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t HDC302XComponent::conversion_delay_ms_() {
|
||||
// ADC conversion delay per datasheet, Table 7-5. - Trigger on Demand
|
||||
switch (this->power_mode_) {
|
||||
case HDC302XPowerMode::BALANCED:
|
||||
return 8;
|
||||
case HDC302XPowerMode::LOW_POWER:
|
||||
return 5;
|
||||
case HDC302XPowerMode::ULTRA_LOW_POWER:
|
||||
return 4;
|
||||
case HDC302XPowerMode::HIGH_ACCURACY:
|
||||
default:
|
||||
return 13;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::hdc302x
|
||||
68
esphome/components/hdc302x/hdc302x.h
Normal file
68
esphome/components/hdc302x/hdc302x.h
Normal file
@@ -0,0 +1,68 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
|
||||
namespace esphome::hdc302x {
|
||||
|
||||
enum HDC302XPowerMode : uint8_t {
|
||||
HIGH_ACCURACY = 0x00,
|
||||
BALANCED = 0x0b,
|
||||
LOW_POWER = 0x16,
|
||||
ULTRA_LOW_POWER = 0xff,
|
||||
};
|
||||
|
||||
/**
|
||||
HDC302x Temperature and humidity sensor.
|
||||
|
||||
Datasheet:
|
||||
https://www.ti.com/lit/ds/symlink/hdc3020.pdf
|
||||
*/
|
||||
class HDC302XComponent : public PollingComponent, public i2c::I2CDevice {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
void update() override;
|
||||
|
||||
void start_heater(uint16_t power, uint32_t duration_ms);
|
||||
void stop_heater();
|
||||
|
||||
void set_temp_sensor(sensor::Sensor *temp_sensor) { this->temp_sensor_ = temp_sensor; }
|
||||
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; }
|
||||
|
||||
void set_power_mode(HDC302XPowerMode power_mode) { this->power_mode_ = power_mode; }
|
||||
|
||||
protected:
|
||||
sensor::Sensor *temp_sensor_{nullptr};
|
||||
sensor::Sensor *humidity_sensor_{nullptr};
|
||||
|
||||
HDC302XPowerMode power_mode_{HDC302XPowerMode::HIGH_ACCURACY};
|
||||
bool heater_active_{false};
|
||||
|
||||
bool enable_heater_();
|
||||
bool configure_heater_(uint16_t power_level);
|
||||
bool disable_heater_();
|
||||
void read_data_();
|
||||
uint32_t conversion_delay_ms_();
|
||||
};
|
||||
|
||||
template<typename... Ts> class HeaterOnAction : public Action<Ts...>, public Parented<HDC302XComponent> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(uint16_t, power)
|
||||
TEMPLATABLE_VALUE(uint32_t, duration)
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
auto power_val = this->power_.value(x...);
|
||||
auto duration_val = this->duration_.value(x...);
|
||||
this->parent_->start_heater(power_val, duration_val);
|
||||
}
|
||||
};
|
||||
|
||||
template<typename... Ts> class HeaterOffAction : public Action<Ts...>, public Parented<HDC302XComponent> {
|
||||
public:
|
||||
void play(const Ts &...x) override { this->parent_->stop_heater(); }
|
||||
};
|
||||
|
||||
} // namespace esphome::hdc302x
|
||||
135
esphome/components/hdc302x/sensor.py
Normal file
135
esphome/components/hdc302x/sensor.py
Normal file
@@ -0,0 +1,135 @@
|
||||
from esphome import automation
|
||||
from esphome.automation import maybe_simple_id
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c, sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_DURATION,
|
||||
CONF_HUMIDITY,
|
||||
CONF_ID,
|
||||
CONF_POWER,
|
||||
CONF_POWER_MODE,
|
||||
CONF_TEMPERATURE,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_PERCENT,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["i2c"]
|
||||
|
||||
hdc302x_ns = cg.esphome_ns.namespace("hdc302x")
|
||||
HDC302XComponent = hdc302x_ns.class_(
|
||||
"HDC302XComponent", cg.PollingComponent, i2c.I2CDevice
|
||||
)
|
||||
|
||||
HDC302XPowerMode = hdc302x_ns.enum("HDC302XPowerMode")
|
||||
POWER_MODE_OPTIONS = {
|
||||
"HIGH_ACCURACY": HDC302XPowerMode.HIGH_ACCURACY,
|
||||
"BALANCED": HDC302XPowerMode.BALANCED,
|
||||
"LOW_POWER": HDC302XPowerMode.LOW_POWER,
|
||||
"ULTRA_LOW_POWER": HDC302XPowerMode.ULTRA_LOW_POWER,
|
||||
}
|
||||
|
||||
# Actions
|
||||
HeaterOnAction = hdc302x_ns.class_("HeaterOnAction", automation.Action)
|
||||
HeaterOffAction = hdc302x_ns.class_("HeaterOffAction", automation.Action)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(HDC302XComponent),
|
||||
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
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,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_POWER_MODE, default="HIGH_ACCURACY"): cv.enum(
|
||||
POWER_MODE_OPTIONS, upper=True
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
.extend(i2c.i2c_device_schema(0x44)) # Default address per datasheet, Table 7-2.
|
||||
)
|
||||
|
||||
|
||||
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 temp_config := config.get(CONF_TEMPERATURE):
|
||||
sens = await sensor.new_sensor(temp_config)
|
||||
cg.add(var.set_temp_sensor(sens))
|
||||
|
||||
if humidity_config := config.get(CONF_HUMIDITY):
|
||||
sens = await sensor.new_sensor(humidity_config)
|
||||
cg.add(var.set_humidity_sensor(sens))
|
||||
|
||||
cg.add(var.set_power_mode(config[CONF_POWER_MODE]))
|
||||
|
||||
|
||||
# HDC302x heater power configs, per datasheet Table 7-15.
|
||||
HDC302X_HEATER_POWER_MAP = {
|
||||
"QUARTER": 0x009F,
|
||||
"HALF": 0x03FF,
|
||||
"FULL": 0x3FFF,
|
||||
}
|
||||
|
||||
|
||||
def heater_power_value(value):
|
||||
"""Accept enum names or raw uint16 values"""
|
||||
if isinstance(value, cv.Lambda):
|
||||
return value
|
||||
if isinstance(value, str):
|
||||
upper = value.upper()
|
||||
if upper in HDC302X_HEATER_POWER_MAP:
|
||||
return HDC302X_HEATER_POWER_MAP[upper]
|
||||
raise cv.Invalid(
|
||||
f"Unknown heater power preset: {value}. Use QUARTER, HALF, FULL, or a raw value 0-16383"
|
||||
)
|
||||
return cv.int_range(min=0, max=0x3FFF)(value)
|
||||
|
||||
|
||||
HDC302X_ACTION_SCHEMA = maybe_simple_id({cv.GenerateID(): cv.use_id(HDC302XComponent)})
|
||||
|
||||
HDC302X_HEATER_ON_ACTION_SCHEMA = maybe_simple_id(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(HDC302XComponent),
|
||||
cv.Optional(CONF_POWER, default="QUARTER"): cv.templatable(heater_power_value),
|
||||
cv.Optional(CONF_DURATION, default="5s"): cv.templatable(
|
||||
cv.positive_time_period_milliseconds
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"hdc302x.heater_on", HeaterOnAction, HDC302X_HEATER_ON_ACTION_SCHEMA
|
||||
)
|
||||
async def hdc302x_heater_on_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
template_ = await cg.templatable(config[CONF_POWER], args, cg.uint16)
|
||||
cg.add(var.set_power(template_))
|
||||
template_ = await cg.templatable(config[CONF_DURATION], args, cg.uint32)
|
||||
cg.add(var.set_duration(template_))
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"hdc302x.heater_off", HeaterOffAction, HDC302X_ACTION_SCHEMA
|
||||
)
|
||||
async def hdc302x_heater_off_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
return var
|
||||
19
tests/components/hdc302x/common.yaml
Normal file
19
tests/components/hdc302x/common.yaml
Normal file
@@ -0,0 +1,19 @@
|
||||
esphome:
|
||||
on_boot:
|
||||
then:
|
||||
- hdc302x.heater_on:
|
||||
id: hdc302x_sensor
|
||||
power: QUARTER
|
||||
duration: 5s
|
||||
- hdc302x.heater_off:
|
||||
id: hdc302x_sensor
|
||||
|
||||
sensor:
|
||||
- platform: hdc302x
|
||||
id: hdc302x_sensor
|
||||
i2c_id: i2c_bus
|
||||
temperature:
|
||||
name: Temperature
|
||||
humidity:
|
||||
name: Humidity
|
||||
update_interval: 15s
|
||||
4
tests/components/hdc302x/test.esp32-idf.yaml
Normal file
4
tests/components/hdc302x/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/hdc302x/test.esp8266-ard.yaml
Normal file
4
tests/components/hdc302x/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/hdc302x/test.rp2040-ard.yaml
Normal file
4
tests/components/hdc302x/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