[tmp102] Add comparator alert binary sensor (#20540)

This commit is contained in:
techdregs
2026-10-11 14:33:06 -10:00
committed by GitHub
parent ce19978cfd
commit 826a7a252a
11 changed files with 265 additions and 11 deletions
+19
View File
@@ -4,8 +4,12 @@ import esphome.codegen as cg
from esphome.components import i2c, sensor
from esphome.components.const import CONF_CONVERSION_RATE
import esphome.config_validation as cv
from esphome.const import CONF_PLATFORM
import esphome.final_validate as fv
from esphome.types import ConfigType
CONF_TMP102_ID = "tmp102_id"
DOMAIN = "tmp102"
tmp102_ns = cg.esphome_ns.namespace("tmp102")
@@ -83,3 +87,18 @@ def validate_tmp102_thresholds(config: ConfigType) -> ConfigType:
[CONF_TEMPERATURE_LOW],
)
return config
def validate_unique_child(config: ConfigType, platform: str) -> ConfigType:
entries = fv.full_config.get().get(platform, [])
matches = [
entry
for entry in entries
if entry.get(CONF_PLATFORM) == DOMAIN
and entry.get(CONF_TMP102_ID) == config[CONF_TMP102_ID]
]
if len(matches) > 1:
raise cv.Invalid(
f"Only one TMP102 {platform} may reference each temperature sensor"
)
return config
@@ -0,0 +1,28 @@
"""TMP102 comparator alert entity."""
import esphome.codegen as cg
from esphome.components import binary_sensor
import esphome.config_validation as cv
from esphome.types import ConfigType
from . import CONF_TMP102_ID, TMP102Component, validate_unique_child
CONFIG_SCHEMA = binary_sensor.binary_sensor_schema().extend(
{
cv.GenerateID(CONF_TMP102_ID): cv.use_id(TMP102Component),
}
)
def _final_validate(config: ConfigType) -> ConfigType:
return validate_unique_child(config, "binary_sensor")
FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config: ConfigType) -> None:
cg.add_define("USE_TMP102_BINARY_SENSOR")
parent = await cg.get_variable(config[CONF_TMP102_ID])
var = await binary_sensor.new_binary_sensor(config)
cg.add(parent.set_alert_binary_sensor(var))
+43 -9
View File
@@ -10,8 +10,12 @@ static constexpr uint32_t READ_TEMP_TIMEOUT_ID = 0;
static constexpr uint8_t TMP102_REGISTER_TEMPERATURE = 0x00;
static constexpr float TMP102_CONVERSION_FACTOR = 0.0625f;
#ifdef USE_TMP102_CONFIGURE
#if defined(USE_TMP102_CONFIGURE) || defined(USE_TMP102_BINARY_SENSOR)
static constexpr uint8_t TMP102_REGISTER_CONFIGURATION = 0x01;
static constexpr uint16_t TMP102_ALERT_BIT = 0x0020;
static constexpr uint16_t TMP102_ALERT_POLARITY_BIT = 0x0400;
#endif
#ifdef USE_TMP102_CONFIGURE
static constexpr uint8_t TMP102_REGISTER_LOW_LIMIT = 0x02;
static constexpr uint8_t TMP102_REGISTER_HIGH_LIMIT = 0x03;
@@ -20,7 +24,6 @@ static constexpr uint16_t TMP102_RESET_CONFIGURATION = 0x60A0;
static constexpr uint16_t TMP102_ONE_SHOT_BIT = 0x8000;
static constexpr uint16_t TMP102_SHUTDOWN_BIT = 0x0100;
static constexpr uint16_t TMP102_THERMOSTAT_MODE_BIT = 0x0200;
static constexpr uint16_t TMP102_ALERT_POLARITY_BIT = 0x0400;
static constexpr uint16_t TMP102_EXTENDED_MODE_BIT = 0x0010;
static constexpr uint32_t TMP102_ONESHOT_DELAY_MS = 40;
@@ -44,6 +47,9 @@ void TMP102Component::dump_config() {
}
LOG_UPDATE_INTERVAL(this);
LOG_SENSOR(" ", "Temperature", this);
#ifdef USE_TMP102_BINARY_SENSOR
LOG_BINARY_SENSOR(" ", "Alert", this->alert_binary_sensor_);
#endif
#ifdef USE_TMP102_CONFIGURE
if (!this->configure_)
return;
@@ -83,13 +89,13 @@ void TMP102Component::update() {
}
#endif
if (this->write(&TMP102_REGISTER_TEMPERATURE, 1) != i2c::ERROR_OK) {
this->status_set_warning();
this->set_warning_and_invalidate_alert_();
return;
}
this->set_timeout(READ_TEMP_TIMEOUT_ID, 50, [this]() {
int16_t raw_temperature;
if (this->read(reinterpret_cast<uint8_t *>(&raw_temperature), 2) != i2c::ERROR_OK) {
this->status_set_warning();
this->set_warning_and_invalidate_alert_();
return;
}
raw_temperature = i2c::i2ctohs(raw_temperature);
@@ -98,6 +104,10 @@ void TMP102Component::update() {
ESP_LOGD(TAG, "Got Temperature=%.1f°C", temperature);
this->publish_state(temperature);
#ifdef USE_TMP102_BINARY_SENSOR
if (this->alert_binary_sensor_ != nullptr && !this->read_alert_state_())
return;
#endif
this->status_clear_warning();
});
}
@@ -108,7 +118,7 @@ void TMP102Component::update_configured_() {
return;
if (this->config_ & TMP102_SHUTDOWN_BIT) {
if (!this->write_register_(TMP102_REGISTER_CONFIGURATION, this->config_ | TMP102_ONE_SHOT_BIT)) {
this->status_set_warning();
this->set_warning_and_invalidate_alert_();
return;
}
this->set_timeout(READ_TEMP_TIMEOUT_ID, TMP102_ONESHOT_DELAY_MS, [this]() { this->read_temperature_(); });
@@ -134,7 +144,7 @@ bool TMP102Component::write_configuration_() {
bool TMP102Component::check_configuration_() {
uint16_t config;
if (!this->read_byte_16(TMP102_REGISTER_CONFIGURATION, &config)) {
this->status_set_warning();
this->set_warning_and_invalidate_alert_();
return false;
}
bool matches = (config & TMP102_CONFIGURATION_MASK) == (this->config_ & TMP102_CONFIGURATION_MASK);
@@ -144,7 +154,7 @@ bool TMP102Component::check_configuration_() {
uint16_t high, low;
if (!this->read_byte_16(TMP102_REGISTER_HIGH_LIMIT, &high) ||
!this->read_byte_16(TMP102_REGISTER_LOW_LIMIT, &low)) {
this->status_set_warning();
this->set_warning_and_invalidate_alert_();
return false;
}
matches = high == this->high_limit_ && low == this->low_limit_;
@@ -153,7 +163,7 @@ bool TMP102Component::check_configuration_() {
return true;
ESP_LOGW(TAG, "Configuration changed; restoring configured settings and thresholds");
this->status_set_warning();
this->set_warning_and_invalidate_alert_();
if (!this->write_configuration_()) {
ESP_LOGW(TAG, "Failed to restore configuration and thresholds");
}
@@ -163,14 +173,38 @@ bool TMP102Component::check_configuration_() {
void TMP102Component::read_temperature_() {
uint16_t raw;
if (!this->read_byte_16(TMP102_REGISTER_TEMPERATURE, &raw)) {
this->status_set_warning();
this->set_warning_and_invalidate_alert_();
return;
}
const float temperature = decode_temperature(raw, raw & 0x0001);
ESP_LOGD(TAG, "Got Temperature=%.2f°C", temperature);
this->publish_state(temperature);
#ifdef USE_TMP102_BINARY_SENSOR
if (this->alert_binary_sensor_ != nullptr && !this->read_alert_state_())
return;
#endif
this->status_clear_warning();
}
#endif
#ifdef USE_TMP102_BINARY_SENSOR
void TMP102Component::invalidate_alert_() {
if (this->alert_binary_sensor_ != nullptr)
this->alert_binary_sensor_->invalidate_state();
}
bool TMP102Component::read_alert_state_() {
uint16_t config;
if (!this->read_byte_16(TMP102_REGISTER_CONFIGURATION, &config)) {
this->set_warning_and_invalidate_alert_();
ESP_LOGW(TAG, "Failed to read alert state");
return false;
}
// AL equals POL while the comparator status is active.
const bool alert = bool(config & TMP102_ALERT_BIT) == bool(config & TMP102_ALERT_POLARITY_BIT);
this->alert_binary_sensor_->publish_state(alert);
return true;
}
#endif
} // namespace esphome::tmp102
+16
View File
@@ -2,6 +2,9 @@
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#ifdef USE_TMP102_BINARY_SENSOR
#include "esphome/components/binary_sensor/binary_sensor.h"
#endif
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/sensor/sensor.h"
@@ -11,6 +14,9 @@ namespace esphome::tmp102 {
class TMP102Component final : public PollingComponent, public i2c::I2CDevice, public sensor::Sensor {
public:
#ifdef USE_TMP102_BINARY_SENSOR
SUB_BINARY_SENSOR(alert)
#endif
#ifdef USE_TMP102_CONFIGURE
void setup() override;
void set_configuration(uint16_t config, uint16_t high_limit, uint16_t low_limit) {
@@ -24,6 +30,16 @@ class TMP102Component final : public PollingComponent, public i2c::I2CDevice, pu
void update() override;
protected:
void set_warning_and_invalidate_alert_() {
this->status_set_warning();
#ifdef USE_TMP102_BINARY_SENSOR
this->invalidate_alert_();
#endif
}
#ifdef USE_TMP102_BINARY_SENSOR
bool read_alert_state_();
void invalidate_alert_();
#endif
#ifdef USE_TMP102_CONFIGURE
bool write_register_(uint8_t reg, uint16_t value);
bool write_configuration_();
+1
View File
@@ -244,6 +244,7 @@
#define USE_TEXT_SENSOR_FILTER
#define USE_TIME
#define USE_TIME_TRIGGERS
#define USE_TMP102_BINARY_SENSOR
#define USE_TMP102_CONFIGURE
#define USE_TOUCHSCREEN
#define USE_UART_DEBUGGER
@@ -0,0 +1,19 @@
---
esphome:
name: tmp102-test
esp32:
board: esp32dev
i2c:
sda: GPIO21
scl: GPIO22
sensor:
- platform: tmp102
id: chip
name: Temperature
binary_sensor:
- platform: tmp102
name: Alert
@@ -0,0 +1,23 @@
---
esphome:
name: tmp102-test
esp32:
board: esp32dev
i2c:
sda: GPIO21
scl: GPIO22
sensor:
- platform: tmp102
id: chip
name: Temperature
binary_sensor:
- platform: tmp102
tmp102_id: chip
name: Alert One
- platform: tmp102
tmp102_id: chip
name: Alert Two
@@ -7,6 +7,7 @@ import pytest
from esphome.components import tmp102 as component
from esphome.components.tmp102.sensor import CONFIG_SCHEMA
from esphome.config import read_config
import esphome.config_validation as cv
from esphome.core import CORE
@@ -32,6 +33,31 @@ def test_advanced_options_enable_configuration_once(
assert "USE_TMP102_CONFIGURE" in defines
def test_alert_entity_is_read_only(
generate_main: Callable[[Path], str],
component_config_path: Callable[[str], Path],
) -> None:
source = generate_main(component_config_path("alert.yaml"))
assert "set_alert_binary_sensor" in source
assert "set_configuration" not in source
defines = " ".join(define.name for define in CORE.defines)
assert "USE_TMP102_BINARY_SENSOR" in defines
assert "USE_TMP102_CONFIGURE" not in defines
def test_duplicate_alert_entities_are_rejected(
component_config_path: Callable[[str], Path],
capsys: pytest.CaptureFixture[str],
) -> None:
CORE.config_path = component_config_path("duplicate_alert.yaml")
assert read_config({}) is None
captured = capsys.readouterr()
assert (
"Only one TMP102 binary_sensor may reference each temperature sensor"
in captured.out
)
@pytest.mark.parametrize(
("extended", "high", "low"),
[
+4 -2
View File
@@ -6,8 +6,10 @@ from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
async def to_code_testing(config: ConfigType) -> None:
CORE.testing_ensure_platform_registered("sensor")
for platform in ("sensor", "binary_sensor"):
CORE.testing_ensure_platform_registered(platform)
cg.add_define("USE_TMP102_CONFIGURE")
cg.add_define("USE_TMP102_BINARY_SENSOR")
manifest.to_code = to_code_testing
manifest.dependencies = manifest.dependencies + ["i2c", "sensor"]
manifest.dependencies = manifest.dependencies + ["i2c", "sensor", "binary_sensor"]
+5
View File
@@ -15,3 +15,8 @@ sensor:
fault_queue: 6
temperature_high: 140°C
temperature_low: 120°C
binary_sensor:
- platform: tmp102
tmp102_id: tmp102_extended_temperature
name: TMP102 Alert
+81
View File
@@ -47,6 +47,7 @@ class TMP102Test : public ::testing::Test {
protected:
RegisterBus bus_;
TMP102Component chip_;
binary_sensor::BinarySensor alert_;
unsigned publications_{0};
void SetUp() override {
@@ -112,6 +113,83 @@ TEST_F(TMP102Test, WritableConfigurationFields) {
EXPECT_EQ(this->bus_.registers[2], 0x2580);
}
TEST_F(TMP102Test, AlertEntityPreservesHardwareAndUsesObservedPolarity) {
this->chip_.set_alert_binary_sensor(&this->alert_);
this->chip_.setup();
EXPECT_EQ(this->bus_.transactions, 0);
for (uint16_t config : {0x60A0, 0x6080, 0x64A0, 0x6480}) {
this->bus_.registers[1] = config;
this->chip_.update();
run_timers(55);
ASSERT_TRUE(this->alert_.has_state());
EXPECT_EQ(this->alert_.state, bool(config & 0x20) == bool(config & 0x400));
EXPECT_EQ(this->bus_.registers[1], config);
}
EXPECT_EQ(this->bus_.writes, 0);
}
TEST_F(TMP102Test, FailedTemperatureOrAlertReadInvalidatesAlert) {
this->chip_.set_alert_binary_sensor(&this->alert_);
this->chip_.setup();
this->chip_.update();
run_timers(55);
ASSERT_TRUE(this->alert_.has_state());
for (int reg : {0, 1}) {
this->bus_.fail_read = reg;
this->chip_.update();
run_timers(55);
EXPECT_TRUE(this->chip_.status_has_warning());
EXPECT_FALSE(this->alert_.has_state());
this->bus_.fail_read = -1;
this->chip_.update();
run_timers(55);
EXPECT_FALSE(this->chip_.status_has_warning());
EXPECT_TRUE(this->alert_.has_state());
}
}
TEST_F(TMP102Test, ConfiguredAlertUsesObservedPolarity) {
this->chip_.set_alert_binary_sensor(&this->alert_);
for (uint16_t config : {0x0080, 0x00A0, 0x0480, 0x04A0}) {
this->configure(config & ~0x0020);
this->chip_.setup();
this->bus_.registers[1] = config;
this->chip_.update();
ASSERT_TRUE(this->alert_.has_state());
EXPECT_EQ(this->alert_.state, bool(config & 0x0020) == bool(config & 0x0400));
EXPECT_FALSE(this->chip_.status_has_warning());
}
}
TEST_F(TMP102Test, ConfiguredAlertInvalidatesDuringRecoveryAndRepublishes) {
this->chip_.set_alert_binary_sensor(&this->alert_);
this->configure(0x0090, 0x2D00, 0x2A80);
this->chip_.setup();
this->chip_.update();
ASSERT_TRUE(this->alert_.has_state());
this->bus_.registers = {0x1900, 0x60A0, 0x4B00, 0x5000};
this->chip_.update();
EXPECT_FALSE(this->alert_.has_state());
EXPECT_TRUE(this->chip_.status_has_warning());
this->chip_.update();
EXPECT_TRUE(this->alert_.has_state());
EXPECT_FALSE(this->chip_.status_has_warning());
}
TEST_F(TMP102Test, OneShotPublishesAlertAfterDelayedRead) {
this->chip_.set_alert_binary_sensor(&this->alert_);
this->configure(0x0180);
this->chip_.setup();
this->chip_.update();
EXPECT_FALSE(this->alert_.has_state());
run_timers(45);
EXPECT_TRUE(this->alert_.has_state());
EXPECT_TRUE(this->alert_.state);
EXPECT_FALSE(this->chip_.status_has_warning());
}
TEST_F(TMP102Test, RestoresRegistersAfterChipOnlyResetBeforePublishing) {
this->configure(0x0090, 0x2D00, 0x2A80);
this->chip_.setup();
@@ -170,13 +248,16 @@ TEST_F(TMP102Test, ReadOnlyConfigurationBitsDoNotTriggerRecovery) {
}
TEST_F(TMP102Test, OneShotTriggerFailureSetsWarning) {
this->chip_.set_alert_binary_sensor(&this->alert_);
this->configure(0x0180);
this->chip_.setup();
this->alert_.publish_state(false);
this->bus_.fail_write = 1;
this->chip_.update();
run_timers(45);
EXPECT_EQ(this->publications_, 0);
EXPECT_TRUE(this->chip_.status_has_warning());
EXPECT_FALSE(this->alert_.has_state());
}
TEST_F(TMP102Test, OneShotRestartsPendingReadAndReselectsTemperature) {