[tmp102] Add number entities for runtime thermostat thresholds (#20596)

Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
techdregs
2026-10-12 16:25:49 -10:00
committed by GitHub
co-authored by pre-commit-ci-lite[bot] J. Nick Koston
parent 74e96b7da6
commit f829447b2e
14 changed files with 653 additions and 14 deletions
+38 -3
View File
@@ -1,10 +1,14 @@
"""Shared TMP102 types and configuration validation."""
from dataclasses import dataclass, field
import math
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
from esphome.core import CORE
import esphome.final_validate as fv
from esphome.types import ConfigType
@@ -16,6 +20,7 @@ tmp102_ns = cg.esphome_ns.namespace("tmp102")
TMP102Component = tmp102_ns.class_(
"TMP102Component", cg.PollingComponent, i2c.I2CDevice, sensor.Sensor
)
TMP102LimitType = tmp102_ns.enum("TMP102LimitType")
CONF_EXTENDED_MODE = "extended_mode"
CONF_ONE_SHOT_MODE = "one_shot_mode"
@@ -34,10 +39,40 @@ TMP102_CONVERSION_FACTOR = 0.0625
TMP102_DEFAULT_THIGH = 80.0
TMP102_DEFAULT_TLOW = 75.0
TMP102_MIN_TEMPERATURE = -55.0
TMP102_MAX_TEMPERATURE = 127.9375
TMP102_MAX_TEMPERATURE_EXTENDED = 150.0
def max_temperature(extended: bool) -> float:
return TMP102_MAX_TEMPERATURE_EXTENDED if extended else TMP102_MAX_TEMPERATURE
@dataclass
class TMP102Data:
# Extended mode of each sensor that has threshold numbers, keyed by sensor ID.
number_parents: dict[str, bool] = field(default_factory=dict)
def _get_data() -> TMP102Data:
if DOMAIN not in CORE.data:
CORE.data[DOMAIN] = TMP102Data()
return CORE.data[DOMAIN]
def add_number_parent(parent_id: object, extended: bool) -> None:
_get_data().number_parents[str(parent_id)] = extended
def number_parent_extended(parent_id: object) -> bool | None:
"""Return the extended mode of a sensor with threshold numbers, or None without numbers."""
return _get_data().number_parents.get(str(parent_id))
def encode_temperature(temperature: float, extended: bool) -> int:
"""Encode a temperature as a TMP102 limit-register word."""
steps = round(temperature / TMP102_CONVERSION_FACTOR)
scaled = temperature / TMP102_CONVERSION_FACTOR
steps = int(scaled + math.copysign(0.5, scaled))
return (steps << (3 if extended else 4)) & 0xFFFF
@@ -56,8 +91,8 @@ def build_configuration(config: ConfigType) -> int:
def validate_tmp102_thresholds(config: ConfigType) -> ConfigType:
extended = config.get(CONF_EXTENDED_MODE, False)
t_min = -55.0
t_max = 150.0 if extended else 127.9375
t_min = TMP102_MIN_TEMPERATURE
t_max = max_temperature(extended)
high = config.get(CONF_TEMPERATURE_HIGH)
low = config.get(CONF_TEMPERATURE_LOW)
for key, value in ((CONF_TEMPERATURE_HIGH, high), (CONF_TEMPERATURE_LOW, low)):
+87
View File
@@ -0,0 +1,87 @@
"""Adjustable TMP102 thermostat thresholds."""
import esphome.codegen as cg
from esphome.components import number
import esphome.config_validation as cv
from esphome.const import (
CONF_RESTORE_VALUE,
DEVICE_CLASS_TEMPERATURE,
ENTITY_CATEGORY_CONFIG,
UNIT_CELSIUS,
)
import esphome.final_validate as fv
from esphome.types import ConfigType
from . import (
CONF_EXTENDED_MODE,
CONF_TEMPERATURE_HIGH,
CONF_TEMPERATURE_LOW,
CONF_TMP102_ID,
TMP102_CONVERSION_FACTOR,
TMP102_MIN_TEMPERATURE,
TMP102Component,
TMP102LimitType,
add_number_parent,
max_temperature,
number_parent_extended,
tmp102_ns,
validate_unique_child,
)
TMP102LimitNumber = tmp102_ns.class_(
"TMP102LimitNumber", number.Number, cg.Parented.template(TMP102Component)
)
_LIMIT_SCHEMA = number.number_schema(
TMP102LimitNumber,
unit_of_measurement=UNIT_CELSIUS,
device_class=DEVICE_CLASS_TEMPERATURE,
entity_category=ENTITY_CATEGORY_CONFIG,
).extend({cv.Optional(CONF_RESTORE_VALUE, default=True): cv.boolean})
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(CONF_TMP102_ID): cv.use_id(TMP102Component),
cv.Optional(CONF_TEMPERATURE_HIGH): _LIMIT_SCHEMA,
cv.Optional(CONF_TEMPERATURE_LOW): _LIMIT_SCHEMA,
}
),
cv.has_at_least_one_key(CONF_TEMPERATURE_HIGH, CONF_TEMPERATURE_LOW),
)
def _final_validate(config: ConfigType) -> ConfigType:
validate_unique_child(config, "number")
full = fv.full_config.get()
parent = full.get_config_for_path(full.get_path_for_id(config[CONF_TMP102_ID])[:-1])
add_number_parent(config[CONF_TMP102_ID], parent.get(CONF_EXTENDED_MODE, False))
return config
FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config: ConfigType) -> None:
cg.add_define("USE_TMP102_NUMBER")
parent = await cg.get_variable(config[CONF_TMP102_ID])
limits = {
"min_value": TMP102_MIN_TEMPERATURE,
"max_value": max_temperature(number_parent_extended(config[CONF_TMP102_ID])),
"step": TMP102_CONVERSION_FACTOR,
}
numbers = number.sub_numbers(config, parent=parent)
if var := await numbers(
CONF_TEMPERATURE_HIGH,
parent.set_limit_control,
TMP102LimitType.TMP102_LIMIT_HIGH,
**limits,
):
cg.add(var.set_restore_value(config[CONF_TEMPERATURE_HIGH][CONF_RESTORE_VALUE]))
if var := await numbers(
CONF_TEMPERATURE_LOW,
parent.set_limit_control,
TMP102LimitType.TMP102_LIMIT_LOW,
**limits,
):
cg.add(var.set_restore_value(config[CONF_TEMPERATURE_LOW][CONF_RESTORE_VALUE]))
+10 -5
View File
@@ -5,6 +5,7 @@ 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_ID,
DEVICE_CLASS_TEMPERATURE,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
@@ -28,6 +29,7 @@ from . import (
TMP102Component,
build_configuration,
encode_temperature,
number_parent_extended,
validate_tmp102_thresholds,
)
@@ -66,17 +68,20 @@ async def to_code(config: ConfigType) -> None:
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
if not any(key in config for key in _ADVANCED_SCHEMA):
has_numbers = number_parent_extended(config[CONF_ID]) is not None
if not has_numbers and not any(key in config for key in _ADVANCED_SCHEMA):
return
extended = config.get(CONF_EXTENDED_MODE, False)
high = config.get(CONF_TEMPERATURE_HIGH, TMP102_DEFAULT_THIGH)
low = config.get(CONF_TEMPERATURE_LOW, TMP102_DEFAULT_TLOW)
cg.add_define("USE_TMP102_CONFIGURE")
cg.add(
var.set_configuration(
build_configuration(config),
encode_temperature(high, extended),
encode_temperature(low, extended),
encode_temperature(
config.get(CONF_TEMPERATURE_HIGH, TMP102_DEFAULT_THIGH), extended
),
encode_temperature(
config.get(CONF_TEMPERATURE_LOW, TMP102_DEFAULT_TLOW), extended
),
)
)
+118 -1
View File
@@ -2,6 +2,8 @@
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include <cmath>
namespace esphome::tmp102 {
ESPHOME_LOG_TAG(TAG, "tmp102");
@@ -18,6 +20,9 @@ static constexpr uint16_t TMP102_ALERT_POLARITY_BIT = 0x0400;
#ifdef USE_TMP102_CONFIGURE
static constexpr uint8_t TMP102_REGISTER_LOW_LIMIT = 0x02;
static constexpr uint8_t TMP102_REGISTER_HIGH_LIMIT = 0x03;
#ifdef USE_TMP102_NUMBER
static_assert(TMP102_REGISTER_LOW_LIMIT + TMP102_LIMIT_HIGH == TMP102_REGISTER_HIGH_LIMIT);
#endif
static constexpr uint16_t TMP102_CONFIGURATION_MASK = 0x1FD0;
static constexpr uint16_t TMP102_RESET_CONFIGURATION = 0x60A0;
@@ -31,11 +36,39 @@ static float decode_temperature(uint16_t raw, bool extended) {
return (static_cast<int16_t>(raw) >> (extended ? 3 : 4)) * TMP102_CONVERSION_FACTOR;
}
#ifdef USE_TMP102_NUMBER
static uint16_t encode_temperature(float temperature, bool extended) {
const int16_t steps = static_cast<int16_t>(std::lround(temperature / TMP102_CONVERSION_FACTOR));
return static_cast<uint16_t>(static_cast<uint32_t>(static_cast<uint16_t>(steps)) << (extended ? 3 : 4));
}
#endif
void TMP102Component::setup() {
if (!this->configure_)
return;
if (!this->write_configuration_())
#ifdef USE_TMP102_NUMBER
float restored[2]{};
this->prepare_number_limits_(restored);
#endif
if (!this->write_configuration_()) {
this->mark_failed();
return;
}
#ifdef USE_TMP102_NUMBER
bool saved = true;
for (uint8_t limit = 0; limit < 2; ++limit) {
TMP102LimitNumber *control = this->limit_controls_[limit];
if (control == nullptr)
continue;
const float value = this->get_limit_temperature_(static_cast<TMP102LimitType>(limit));
if (value != restored[limit])
saved &= control->save_value_(value);
control->publish_state(value);
}
if (!saved) {
ESP_LOGW(TAG, "Failed to save threshold preferences");
}
#endif
}
#endif
@@ -75,6 +108,10 @@ void TMP102Component::dump_config() {
this->config_ & TMP102_ALERT_POLARITY_BIT ? LOG_STR_LITERAL("Active High") : LOG_STR_LITERAL("Active Low"),
fault_bits == 3 ? 6 : 1U << fault_bits, decode_temperature(this->high_limit_, extended),
decode_temperature(this->low_limit_, extended));
#ifdef USE_TMP102_NUMBER
LOG_NUMBER(" ", "High Limit", this->limit_controls_[TMP102_LIMIT_HIGH]);
LOG_NUMBER(" ", "Low Limit", this->limit_controls_[TMP102_LIMIT_LOW]);
#endif
if (this->config_ & TMP102_THERMOSTAT_MODE_BIT) {
ESP_LOGW(TAG, "Register reads clear the interrupt-mode ALERT pin");
}
@@ -185,6 +222,55 @@ void TMP102Component::read_temperature_() {
#endif
this->status_clear_warning();
}
#ifdef USE_TMP102_NUMBER
void TMP102Component::set_limit_control(TMP102LimitNumber *number) {
this->limit_controls_[number->limit_type_] = number;
}
void TMP102Component::prepare_number_limits_(float *restored) {
const bool extended = this->config_ & TMP102_EXTENDED_MODE_BIT;
uint16_t raw[2] = {this->low_limit_, this->high_limit_};
for (uint8_t limit = 0; limit < 2; ++limit) {
TMP102LimitNumber *control = this->limit_controls_[limit];
if (control == nullptr)
continue;
restored[limit] = control->load_value_(decode_temperature(raw[limit], extended));
raw[limit] = encode_temperature(restored[limit], extended);
}
if (static_cast<int16_t>(raw[TMP102_LIMIT_LOW]) > static_cast<int16_t>(raw[TMP102_LIMIT_HIGH])) {
ESP_LOGW(TAG, "Restored thresholds are inverted; using configured values");
return;
}
this->low_limit_ = raw[TMP102_LIMIT_LOW];
this->high_limit_ = raw[TMP102_LIMIT_HIGH];
}
float TMP102Component::get_limit_temperature_(TMP102LimitType limit) const {
const uint16_t raw = limit == TMP102_LIMIT_HIGH ? this->high_limit_ : this->low_limit_;
return decode_temperature(raw, this->config_ & TMP102_EXTENDED_MODE_BIT);
}
bool TMP102Component::set_limit_temperature_(TMP102LimitType limit, float temperature) {
const bool extended = this->config_ & TMP102_EXTENDED_MODE_BIT;
const uint16_t raw = encode_temperature(temperature, extended);
const bool high = limit == TMP102_LIMIT_HIGH;
const uint16_t other = high ? this->low_limit_ : this->high_limit_;
if (high ? static_cast<int16_t>(raw) < static_cast<int16_t>(other)
: static_cast<int16_t>(raw) > static_cast<int16_t>(other)) {
ESP_LOGW(TAG, "Rejected %s threshold %.4f°C: crosses %s threshold %.4f°C",
high ? LOG_STR_LITERAL("high") : LOG_STR_LITERAL("low"), decode_temperature(raw, extended),
high ? LOG_STR_LITERAL("low") : LOG_STR_LITERAL("high"), decode_temperature(other, extended));
return false;
}
if (!this->write_register_(static_cast<uint8_t>(TMP102_REGISTER_LOW_LIMIT + limit), raw)) {
this->status_set_warning();
return false;
}
(high ? this->high_limit_ : this->low_limit_) = raw;
return true;
}
#endif
#endif
#ifdef USE_TMP102_BINARY_SENSOR
@@ -207,4 +293,35 @@ bool TMP102Component::read_alert_state_() {
}
#endif
#ifdef USE_TMP102_NUMBER
float TMP102LimitNumber::load_value_(float fallback) {
float value = fallback;
if (this->restore_value_) {
this->pref_ = this->make_entity_preference<float>();
this->pref_.load(&value);
}
if (!std::isfinite(value) || value < this->traits.get_min_value() || value > this->traits.get_max_value()) {
ESP_LOGW(TAG, "Invalid restored threshold; using configured value");
value = fallback;
}
return value;
}
bool TMP102LimitNumber::save_value_(float value) { return !this->restore_value_ || this->pref_.save(&value); }
void TMP102LimitNumber::control(float value) {
// A chip that failed setup holds nothing the entity could show
if (this->parent_->is_failed()) {
ESP_LOGW(TAG, "Rejected threshold change: sensor setup failed");
return;
}
const bool accepted = this->parent_->set_limit_temperature_(this->limit_type_, value);
value = this->parent_->get_limit_temperature_(this->limit_type_);
this->publish_state(value);
if (accepted && !this->save_value_(value)) {
ESP_LOGW(TAG, "Failed to save threshold preference");
}
}
#endif
} // namespace esphome::tmp102
+43
View File
@@ -6,12 +6,26 @@
#include "esphome/components/binary_sensor/binary_sensor.h"
#endif
#include "esphome/components/i2c/i2c.h"
#ifdef USE_TMP102_NUMBER
#include "esphome/components/number/number.h"
#include "esphome/core/helpers.h"
#include "esphome/core/preferences.h"
#endif
#include "esphome/components/sensor/sensor.h"
#include <cstdint>
namespace esphome::tmp102 {
#ifdef USE_TMP102_NUMBER
enum TMP102LimitType : uint8_t {
TMP102_LIMIT_LOW = 0,
TMP102_LIMIT_HIGH = 1,
};
class TMP102LimitNumber;
#endif
class TMP102Component final : public PollingComponent, public i2c::I2CDevice, public sensor::Sensor {
public:
#ifdef USE_TMP102_BINARY_SENSOR
@@ -25,6 +39,9 @@ class TMP102Component final : public PollingComponent, public i2c::I2CDevice, pu
this->low_limit_ = low_limit;
this->configure_ = true;
}
#ifdef USE_TMP102_NUMBER
void set_limit_control(TMP102LimitNumber *number);
#endif
#endif
void dump_config() override;
void update() override;
@@ -46,6 +63,14 @@ class TMP102Component final : public PollingComponent, public i2c::I2CDevice, pu
bool check_configuration_();
void read_temperature_();
void update_configured_();
#ifdef USE_TMP102_NUMBER
friend class TMP102LimitNumber;
bool set_limit_temperature_(TMP102LimitType limit, float temperature);
float get_limit_temperature_(TMP102LimitType limit) const;
void prepare_number_limits_(float *restored);
TMP102LimitNumber *limit_controls_[2]{};
#endif
uint16_t config_{0};
uint16_t high_limit_{0};
@@ -54,4 +79,22 @@ class TMP102Component final : public PollingComponent, public i2c::I2CDevice, pu
#endif
};
#ifdef USE_TMP102_NUMBER
class TMP102LimitNumber final : public number::Number, public Parented<TMP102Component> {
public:
explicit TMP102LimitNumber(TMP102LimitType limit_type) : limit_type_(limit_type) {}
void set_restore_value(bool restore_value) { this->restore_value_ = restore_value; }
protected:
friend class TMP102Component;
float load_value_(float fallback);
bool save_value_(float value);
void control(float value) override;
ESPPreferenceObject pref_;
TMP102LimitType limit_type_;
bool restore_value_{true};
};
#endif
} // namespace esphome::tmp102
+1
View File
@@ -266,6 +266,7 @@
#define USE_TIME_TRIGGERS
#define USE_TMP102_BINARY_SENSOR
#define USE_TMP102_CONFIGURE
#define USE_TMP102_NUMBER
#define USE_TOUCHSCREEN
#define USE_UART_DEBUGGER
#define USE_UART_WAKE_LOOP_ON_RX
@@ -0,0 +1,25 @@
---
esphome:
name: tmp102-test
esp32:
board: esp32dev
i2c:
sda: GPIO21
scl: GPIO22
sensor:
- platform: tmp102
id: chip
name: Temperature
number:
- platform: tmp102
tmp102_id: chip
temperature_high:
name: High One
- platform: tmp102
tmp102_id: chip
temperature_low:
name: Low Two
@@ -0,0 +1,24 @@
---
esphome:
name: tmp102-test
esp32:
board: esp32dev
i2c:
sda: GPIO21
scl: GPIO22
sensor:
- platform: tmp102
id: chip
name: Temperature
number:
- platform: tmp102
tmp102_id: chip
temperature_high:
name: High
temperature_low:
name: Low
restore_value: false
@@ -0,0 +1,25 @@
---
esphome:
name: tmp102-test
esp32:
board: esp32dev
i2c:
sda: GPIO21
scl: GPIO22
sensor:
- platform: tmp102
id: chip
name: Temperature
extended_mode: true
temperature_high: 140
temperature_low: 120
number:
- platform: tmp102
temperature_high:
name: High
temperature_low:
name: Low
@@ -0,0 +1,22 @@
---
esphome:
name: tmp102-test
esp32:
board: esp32dev
i2c:
sda: GPIO21
scl: GPIO22
sensor:
- platform: tmp102
id: chip
name: Temperature
number:
- platform: tmp102
tmp102_id: chip
temperature_high:
name: High
initial_value: 30
@@ -58,6 +58,54 @@ def test_duplicate_alert_entities_are_rejected(
)
def test_threshold_numbers_enable_configuration_once(
generate_main: Callable[[Path], str],
component_config_path: Callable[[str], Path],
) -> None:
source = generate_main(component_config_path("numbers.yaml"))
assert source.count("set_configuration(") == 1
assert "set_configuration(128, 20480, 19200)" in source
assert source.count("set_limit_control(") == 2
assert "tmp102::TMP102_LIMIT_HIGH" in source
assert "tmp102::TMP102_LIMIT_LOW" in source
assert "set_restore_value(false)" in source
assert "set_max_value(127.9375f)" in source
defines = " ".join(define.name for define in CORE.defines)
assert "USE_TMP102_NUMBER" in defines
assert "USE_TMP102_CONFIGURE" in defines
def test_threshold_numbers_use_sensor_thresholds(
generate_main: Callable[[Path], str],
component_config_path: Callable[[str], Path],
) -> None:
source = generate_main(component_config_path("numbers_inherit.yaml"))
assert source.count("set_configuration(") == 1
assert "set_configuration(144, 17920, 15360)" in source
assert "set_max_value(150.0f)" in source
def test_threshold_number_initial_value_is_rejected(
component_config_path: Callable[[str], Path],
capsys: pytest.CaptureFixture[str],
) -> None:
CORE.config_path = component_config_path("numbers_initial_value.yaml")
assert read_config({}) is None
assert "[initial_value] is an invalid option" in capsys.readouterr().out
def test_duplicate_numbers_are_rejected(
component_config_path: Callable[[str], Path],
capsys: pytest.CaptureFixture[str],
) -> None:
CORE.config_path = component_config_path("duplicate_numbers.yaml")
assert read_config({}) is None
assert (
"Only one TMP102 number may reference each temperature sensor"
in capsys.readouterr().out
)
@pytest.mark.parametrize(
("extended", "high", "low"),
[
@@ -117,5 +165,12 @@ def test_precomputed_register_values() -> None:
assert component.encode_temperature(config["temperature_low"], True) == 0xF9E0
def test_threshold_encoding_rounds_half_steps_away_from_zero() -> None:
assert component.encode_temperature(30.03125, False) == 481 << 4
assert component.encode_temperature(-30.03125, False) == (-481 << 4) & 0xFFFF
assert component.encode_temperature(30.03125, True) == 481 << 3
assert component.encode_temperature(-30.03125, True) == (-481 << 3) & 0xFFFF
def test_defaults_match_chip_reset() -> None:
assert component.build_configuration({}) == 0x0080
+8 -2
View File
@@ -6,10 +6,16 @@ from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
async def to_code_testing(config: ConfigType) -> None:
for platform in ("sensor", "binary_sensor"):
for platform in ("sensor", "binary_sensor", "number"):
CORE.testing_ensure_platform_registered(platform)
cg.add_define("USE_TMP102_CONFIGURE")
cg.add_define("USE_TMP102_BINARY_SENSOR")
cg.add_define("USE_TMP102_NUMBER")
manifest.to_code = to_code_testing
manifest.dependencies = manifest.dependencies + ["i2c", "sensor", "binary_sensor"]
manifest.dependencies = manifest.dependencies + [
"i2c",
"sensor",
"binary_sensor",
"number",
]
+10
View File
@@ -16,6 +16,16 @@ sensor:
temperature_high: 140°C
temperature_low: 120°C
number:
- platform: tmp102
tmp102_id: tmp102_extended_temperature
temperature_high:
name: TMP102 High Threshold
restore_value: true
temperature_low:
name: TMP102 Low Threshold
restore_value: false
binary_sensor:
- platform: tmp102
tmp102_id: tmp102_extended_temperature
+187 -3
View File
@@ -3,12 +3,23 @@
#include <gtest/gtest.h>
#include <array>
#include <cmath>
#include <utility>
#include "esphome/components/host/preferences.h"
#include "esphome/components/tmp102/tmp102.h"
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
namespace esphome::tmp102::testing {
class EntityIdentity : public EntityBase {
public:
static void configure(EntityBase &entity, const char *name, uint32_t hash) {
auto configure = &EntityIdentity::configure_entity_;
(entity.*configure)(name, hash, 0);
}
};
class RegisterBus final : public i2c::I2CBus {
public:
std::array<uint16_t, 4> registers{0x1900, 0x60A0, 0x4B00, 0x5000};
@@ -48,21 +59,32 @@ class TMP102Test : public ::testing::Test {
RegisterBus bus_;
TMP102Component chip_;
binary_sensor::BinarySensor alert_;
TMP102LimitNumber high_{TMP102_LIMIT_HIGH};
TMP102LimitNumber low_{TMP102_LIMIT_LOW};
unsigned publications_{0};
void SetUp() override {
new (&App) Application;
char name[] = "tmp102-regression-tests";
char friendly_name[] = "TMP102 tests";
App.pre_setup(name, 23, friendly_name, 12);
App.pre_setup("tmp102-regression-tests", 23, "TMP102 tests", 12);
host::setup_preferences();
host::host_preferences->reset();
this->chip_.set_i2c_bus(&this->bus_);
this->chip_.set_i2c_address(0x48);
this->chip_.add_on_state_callback([this](float) { ++this->publications_; });
EntityIdentity::configure(this->high_, "High", 0x102001);
EntityIdentity::configure(this->low_, "Low", 0x102002);
for (auto *number : {&this->high_, &this->low_}) {
number->set_parent(&this->chip_);
number->traits.set_min_value(-55);
number->traits.set_max_value(127.9375f);
number->traits.set_step(0.0625f);
}
}
void TearDown() override {
App.scheduler.cancel_timeout(&this->chip_, uint32_t{0});
App.scheduler.call(millis());
host::host_preferences->reset();
App.~Application();
}
@@ -75,6 +97,31 @@ class TMP102Test : public ::testing::Test {
void configure(uint16_t config = 0x0080, uint16_t high = 0x5000, uint16_t low = 0x4B00) {
this->chip_.set_configuration(config, high, low);
}
static uint16_t encode(float temperature, bool extended = false) {
const int16_t steps = static_cast<int16_t>(std::lround(temperature / 0.0625f));
return static_cast<uint16_t>(static_cast<uint32_t>(static_cast<uint16_t>(steps)) << (extended ? 3 : 4));
}
void numbers_(float initial_high = 30, float initial_low = 25, bool extended = false) {
this->configure(extended ? 0x0090 : 0x0080, encode(initial_high, extended), encode(initial_low, extended));
if (extended) {
this->high_.traits.set_max_value(150);
this->low_.traits.set_max_value(150);
}
this->chip_.set_limit_control(&this->high_);
this->chip_.set_limit_control(&this->low_);
}
static float saved(TMP102LimitNumber &number) {
float value = NAN;
EXPECT_TRUE(number.make_entity_preference<float>().load(&value));
return value;
}
static void save(TMP102LimitNumber &number, float value) {
ASSERT_TRUE(number.make_entity_preference<float>().save(&value));
}
};
TEST_F(TMP102Test, LegacySetupAndContinuousReadDoNotWriteRegisters) {
@@ -190,6 +237,62 @@ TEST_F(TMP102Test, OneShotPublishesAlertAfterDelayedRead) {
EXPECT_FALSE(this->chip_.status_has_warning());
}
TEST_F(TMP102Test, QuantizedRuntimeValuesMatchRegistersAndPreferences) {
this->numbers_((10.0f - 32) * 5 / 9, (9.0f - 32) * 5 / 9);
this->chip_.setup();
EXPECT_FLOAT_EQ(this->high_.state, -12.25f);
EXPECT_FLOAT_EQ(this->low_.state, -12.75f);
EXPECT_EQ(this->bus_.registers[3], 0xF3C0);
for (auto [requested, expected] :
{std::pair{12.22f, 12.25f}, {-12.22f, -12.25f}, {12.03125f, 12.0625f}, {-12.03125f, -12.0625f}}) {
this->high_.make_call().set_value(requested).perform();
EXPECT_FLOAT_EQ(this->high_.state, expected);
EXPECT_FLOAT_EQ(saved(this->high_), expected);
EXPECT_FLOAT_EQ(static_cast<int16_t>(this->bus_.registers[3]) / 256.0f, expected);
}
}
TEST_F(TMP102Test, ExtendedModeRuntimeValueMatchesRegister) {
this->numbers_(140, 120, true);
this->chip_.setup();
this->high_.make_call().set_value(145.0625f).perform();
EXPECT_FLOAT_EQ(this->high_.state, 145.0625f);
EXPECT_EQ(this->bus_.registers[3], encode(145.0625f, true));
EXPECT_FLOAT_EQ(saved(this->high_), 145.0625f);
}
TEST_F(TMP102Test, HighOnlyInvalidRestoreFallsBackAgainstStaticLow) {
this->configure(0x0080, encode(30), encode(25));
this->chip_.set_limit_control(&this->high_);
save(this->high_, 20);
this->chip_.setup();
EXPECT_FLOAT_EQ(this->high_.state, 30);
EXPECT_EQ(this->bus_.registers[3], encode(30));
EXPECT_EQ(this->bus_.registers[2], encode(25));
EXPECT_FLOAT_EQ(saved(this->high_), 30);
}
TEST_F(TMP102Test, OrderingUsesAcceptedQuantizedThreshold) {
this->numbers_(30.02f, 30.01f);
this->chip_.setup();
EXPECT_FLOAT_EQ(this->high_.state, 30);
EXPECT_FLOAT_EQ(this->low_.state, 30);
this->high_.make_call().set_value(29.96f).perform();
EXPECT_FLOAT_EQ(this->high_.state, 30);
this->high_.make_call().set_value(29.99f).perform();
EXPECT_FLOAT_EQ(this->high_.state, 30);
}
TEST_F(TMP102Test, FailedThresholdWriteWarningClearsAfterSuccessfulRead) {
this->numbers_();
this->chip_.setup();
this->bus_.fail_write = 3;
this->high_.make_call().set_value(90).perform();
EXPECT_TRUE(this->chip_.status_has_warning());
this->chip_.update();
EXPECT_FALSE(this->chip_.status_has_warning());
}
TEST_F(TMP102Test, RestoresRegistersAfterChipOnlyResetBeforePublishing) {
this->configure(0x0090, 0x2D00, 0x2A80);
this->chip_.setup();
@@ -304,6 +407,87 @@ TEST_F(TMP102Test, FailureOfAnySetupWriteMarksComponentFailed) {
}
}
TEST_F(TMP102Test, InvalidRestoredPairIsRepairedAfterSuccessfulSetup) {
this->numbers_();
save(this->high_, 20);
save(this->low_, 35);
this->chip_.setup();
EXPECT_FLOAT_EQ(this->high_.state, 30);
EXPECT_FLOAT_EQ(this->low_.state, 25);
EXPECT_FLOAT_EQ(saved(this->high_), 30);
EXPECT_FLOAT_EQ(saved(this->low_), 25);
}
TEST_F(TMP102Test, InvalidRestoredValuesAreRepaired) {
this->numbers_();
save(this->high_, NAN);
save(this->low_, INFINITY);
this->chip_.setup();
EXPECT_FLOAT_EQ(this->high_.state, 30);
EXPECT_FLOAT_EQ(this->low_.state, 25);
}
TEST_F(TMP102Test, FailedSetupDoesNotOverwritePreferences) {
this->numbers_();
save(this->high_, 90);
save(this->low_, 85);
this->bus_.fail_write = 3;
this->chip_.setup();
EXPECT_TRUE(this->chip_.is_failed());
EXPECT_FALSE(this->high_.has_state());
EXPECT_FLOAT_EQ(saved(this->high_), 90);
EXPECT_FLOAT_EQ(saved(this->low_), 85);
}
TEST_F(TMP102Test, FailedSetupRejectsThresholdChanges) {
this->numbers_();
save(this->high_, 90);
this->bus_.fail_write = 3;
this->chip_.setup();
ASSERT_TRUE(this->chip_.is_failed());
const unsigned writes = this->bus_.writes;
this->high_.make_call().set_value(60).perform();
EXPECT_FALSE(this->high_.has_state());
EXPECT_EQ(this->bus_.writes, writes);
EXPECT_FLOAT_EQ(saved(this->high_), 90);
}
TEST_F(TMP102Test, SetupOnlySavesChangedThresholds) {
this->numbers_();
save(this->high_, 35);
this->chip_.setup();
EXPECT_FLOAT_EQ(this->high_.state, 35);
EXPECT_FLOAT_EQ(this->low_.state, 25);
EXPECT_EQ(this->bus_.registers[3], encode(35));
float value;
EXPECT_FALSE(this->low_.make_entity_preference<float>().load(&value));
}
TEST_F(TMP102Test, FailedNumberWriteKeepsAcceptedState) {
this->numbers_();
save(this->high_, 90);
save(this->low_, 85);
this->chip_.setup();
unsigned publications = 0;
this->high_.add_on_state_callback([&publications](float) { ++publications; });
this->bus_.fail_write = 3;
this->high_.make_call().set_value(100).perform();
EXPECT_FLOAT_EQ(this->high_.state, 90);
EXPECT_EQ(publications, 1);
EXPECT_FLOAT_EQ(saved(this->high_), 90);
}
TEST_F(TMP102Test, RestoreDisabledNeverChangesSavedValue) {
this->numbers_();
save(this->high_, 90);
this->high_.set_restore_value(false);
this->chip_.setup();
EXPECT_FLOAT_EQ(this->high_.state, 30);
this->high_.make_call().set_value(40).perform();
EXPECT_FLOAT_EQ(this->high_.state, 40);
EXPECT_FLOAT_EQ(saved(this->high_), 90);
}
} // namespace esphome::tmp102::testing
#endif