mirror of
https://github.com/esphome/esphome.git
synced 2026-10-14 02:02:51 +00:00
[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:
co-authored by
pre-commit-ci-lite[bot]
J. Nick Koston
parent
74e96b7da6
commit
f829447b2e
@@ -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)):
|
||||
|
||||
@@ -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]))
|
||||
@@ -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
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user