mirror of
https://github.com/esphome/esphome.git
synced 2026-10-14 02:02:51 +00:00
[tmp102] Add comparator alert binary sensor (#20540)
This commit is contained in:
@@ -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))
|
||||
@@ -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
|
||||
|
||||
@@ -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_();
|
||||
|
||||
@@ -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"),
|
||||
[
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user