mirror of
https://github.com/esphome/esphome.git
synced 2026-10-14 10:12:52 +00:00
[tmp102] Add hardware configuration options (#20063)
Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
co-authored by
J. Nick Koston
J. Nick Koston
parent
c8a300164b
commit
6322cef8e2
@@ -1,11 +1,85 @@
|
||||
"""
|
||||
The TMP102 is a two-wire, serial output temperature
|
||||
sensor available in a tiny SOT563 package. Requiring
|
||||
no external components, the TMP102 is capable of
|
||||
reading temperatures to a resolution of 0.0625°C.
|
||||
"""Shared TMP102 types and configuration validation."""
|
||||
|
||||
https://www.sparkfun.com/datasheets/Sensors/Temperature/tmp102.pdf
|
||||
|
||||
"""
|
||||
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.types import ConfigType
|
||||
|
||||
DOMAIN = "tmp102"
|
||||
|
||||
tmp102_ns = cg.esphome_ns.namespace("tmp102")
|
||||
TMP102Component = tmp102_ns.class_(
|
||||
"TMP102Component", cg.PollingComponent, i2c.I2CDevice, sensor.Sensor
|
||||
)
|
||||
|
||||
CONF_EXTENDED_MODE = "extended_mode"
|
||||
CONF_ONE_SHOT_MODE = "one_shot_mode"
|
||||
CONF_TEMPERATURE_HIGH = "temperature_high"
|
||||
CONF_TEMPERATURE_LOW = "temperature_low"
|
||||
CONF_ALERT_POLARITY = "alert_polarity"
|
||||
CONF_THERMOSTAT_MODE = "thermostat_mode"
|
||||
CONF_FAULT_QUEUE = "fault_queue"
|
||||
|
||||
CONVERSION_RATES = {"0.25Hz": 0, "1Hz": 1, "4Hz": 2, "8Hz": 3}
|
||||
THERMOSTAT_MODES = {"comparator": 0, "interrupt": 1}
|
||||
ALERT_POLARITIES = {"active_low": 0, "active_high": 1}
|
||||
FAULT_QUEUE_BITS = {1: 0, 2: 1, 4: 2, 6: 3}
|
||||
|
||||
TMP102_CONVERSION_FACTOR = 0.0625
|
||||
TMP102_DEFAULT_THIGH = 80.0
|
||||
TMP102_DEFAULT_TLOW = 75.0
|
||||
|
||||
|
||||
def encode_temperature(temperature: float, extended: bool) -> int:
|
||||
"""Encode a temperature as a TMP102 limit-register word."""
|
||||
steps = round(temperature / TMP102_CONVERSION_FACTOR)
|
||||
return (steps << (3 if extended else 4)) & 0xFFFF
|
||||
|
||||
|
||||
def build_configuration(config: ConfigType) -> int:
|
||||
"""Build the writable TMP102 configuration-register bits."""
|
||||
value = CONVERSION_RATES[config.get(CONF_CONVERSION_RATE, "4Hz")] << 6
|
||||
value |= FAULT_QUEUE_BITS[config.get(CONF_FAULT_QUEUE, 1)] << 11
|
||||
value |= ALERT_POLARITIES[config.get(CONF_ALERT_POLARITY, "active_low")] << 10
|
||||
value |= THERMOSTAT_MODES[config.get(CONF_THERMOSTAT_MODE, "comparator")] << 9
|
||||
if config.get(CONF_ONE_SHOT_MODE, False):
|
||||
value |= 1 << 8
|
||||
if config.get(CONF_EXTENDED_MODE, False):
|
||||
value |= 1 << 4
|
||||
return value
|
||||
|
||||
|
||||
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
|
||||
high = config.get(CONF_TEMPERATURE_HIGH)
|
||||
low = config.get(CONF_TEMPERATURE_LOW)
|
||||
for key, value in ((CONF_TEMPERATURE_HIGH, high), (CONF_TEMPERATURE_LOW, low)):
|
||||
if value is not None and not t_min <= value <= t_max:
|
||||
raise cv.Invalid(
|
||||
f"{key} ({value}°C) out of range for "
|
||||
f"{'extended' if extended else 'normal'} mode "
|
||||
f"({t_min}°C to {t_max}°C)",
|
||||
[key],
|
||||
)
|
||||
|
||||
if high is not None and low is not None and low > high:
|
||||
raise cv.Invalid(
|
||||
f"low limit ({low}°C) must be <= high limit ({high}°C)",
|
||||
[CONF_TEMPERATURE_LOW],
|
||||
)
|
||||
if high is not None and low is None and high < TMP102_DEFAULT_TLOW:
|
||||
raise cv.Invalid(
|
||||
f"high limit ({high}°C) is below the chip power-up TLOW default "
|
||||
f"({TMP102_DEFAULT_TLOW}°C). Set temperature_low explicitly.",
|
||||
[CONF_TEMPERATURE_HIGH],
|
||||
)
|
||||
if low is not None and high is None and low > TMP102_DEFAULT_THIGH:
|
||||
raise cv.Invalid(
|
||||
f"low limit ({low}°C) is above the chip power-up THIGH default "
|
||||
f"({TMP102_DEFAULT_THIGH}°C). Set temperature_high explicitly.",
|
||||
[CONF_TEMPERATURE_LOW],
|
||||
)
|
||||
return config
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
"""
|
||||
The TMP102 is a two-wire, serial output temperature
|
||||
sensor available in a tiny SOT563 package. Requiring
|
||||
no external components, the TMP102 is capable of
|
||||
reading temperatures to a resolution of 0.0625°C.
|
||||
|
||||
https://www.sparkfun.com/datasheets/Sensors/Temperature/tmp102.pdf
|
||||
|
||||
"""
|
||||
"""TMP102 temperature sensor and optional hardware configuration."""
|
||||
|
||||
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 (
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
@@ -18,15 +11,42 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import (
|
||||
ALERT_POLARITIES,
|
||||
CONF_ALERT_POLARITY,
|
||||
CONF_EXTENDED_MODE,
|
||||
CONF_FAULT_QUEUE,
|
||||
CONF_ONE_SHOT_MODE,
|
||||
CONF_TEMPERATURE_HIGH,
|
||||
CONF_TEMPERATURE_LOW,
|
||||
CONF_THERMOSTAT_MODE,
|
||||
CONVERSION_RATES,
|
||||
FAULT_QUEUE_BITS,
|
||||
THERMOSTAT_MODES,
|
||||
TMP102_DEFAULT_THIGH,
|
||||
TMP102_DEFAULT_TLOW,
|
||||
TMP102Component,
|
||||
build_configuration,
|
||||
encode_temperature,
|
||||
validate_tmp102_thresholds,
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@timsavage"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
|
||||
tmp102_ns = cg.esphome_ns.namespace("tmp102")
|
||||
TMP102Component = tmp102_ns.class_(
|
||||
"TMP102Component", cg.PollingComponent, i2c.I2CDevice, sensor.Sensor
|
||||
)
|
||||
# No defaults here: absence of all advanced options preserves upstream's read-only behavior and firmware size.
|
||||
_ADVANCED_SCHEMA = {
|
||||
cv.Optional(CONF_EXTENDED_MODE): cv.boolean,
|
||||
cv.Optional(CONF_CONVERSION_RATE): cv.enum(CONVERSION_RATES),
|
||||
cv.Optional(CONF_ONE_SHOT_MODE): cv.boolean,
|
||||
cv.Optional(CONF_ALERT_POLARITY): cv.enum(ALERT_POLARITIES),
|
||||
cv.Optional(CONF_THERMOSTAT_MODE): cv.enum(THERMOSTAT_MODES),
|
||||
cv.Optional(CONF_FAULT_QUEUE): cv.enum(FAULT_QUEUE_BITS, int=True),
|
||||
cv.Optional(CONF_TEMPERATURE_HIGH): cv.temperature,
|
||||
cv.Optional(CONF_TEMPERATURE_LOW): cv.temperature,
|
||||
}
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
sensor.sensor_schema(
|
||||
TMP102Component,
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
@@ -36,6 +56,8 @@ CONFIG_SCHEMA = (
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
.extend(i2c.i2c_device_schema(0x48))
|
||||
.extend(_ADVANCED_SCHEMA),
|
||||
validate_tmp102_thresholds,
|
||||
)
|
||||
|
||||
|
||||
@@ -43,3 +65,18 @@ async def to_code(config: ConfigType) -> None:
|
||||
var = await sensor.new_sensor(config)
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
if 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),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -8,13 +8,33 @@ ESPHOME_LOG_TAG(TAG, "tmp102");
|
||||
|
||||
static constexpr uint32_t READ_TEMP_TIMEOUT_ID = 0;
|
||||
|
||||
static const uint8_t TMP102_ADDRESS = 0x48;
|
||||
static const uint8_t TMP102_REGISTER_TEMPERATURE = 0x00;
|
||||
static const uint8_t TMP102_REGISTER_CONFIGURATION = 0x01;
|
||||
static const uint8_t TMP102_REGISTER_LOW_LIMIT = 0x02;
|
||||
static const uint8_t TMP102_REGISTER_HIGH_LIMIT = 0x03;
|
||||
static constexpr uint8_t TMP102_REGISTER_TEMPERATURE = 0x00;
|
||||
static constexpr float TMP102_CONVERSION_FACTOR = 0.0625f;
|
||||
#ifdef USE_TMP102_CONFIGURE
|
||||
static constexpr uint8_t TMP102_REGISTER_CONFIGURATION = 0x01;
|
||||
static constexpr uint8_t TMP102_REGISTER_LOW_LIMIT = 0x02;
|
||||
static constexpr uint8_t TMP102_REGISTER_HIGH_LIMIT = 0x03;
|
||||
|
||||
static const float TMP102_CONVERSION_FACTOR = 0.0625;
|
||||
static constexpr uint16_t TMP102_CONFIGURATION_MASK = 0x1FD0;
|
||||
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;
|
||||
|
||||
static float decode_temperature(uint16_t raw, bool extended) {
|
||||
return (static_cast<int16_t>(raw) >> (extended ? 3 : 4)) * TMP102_CONVERSION_FACTOR;
|
||||
}
|
||||
|
||||
void TMP102Component::setup() {
|
||||
if (!this->configure_)
|
||||
return;
|
||||
if (!this->write_configuration_())
|
||||
this->mark_failed();
|
||||
}
|
||||
#endif
|
||||
|
||||
void TMP102Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "TMP102:");
|
||||
@@ -24,9 +44,44 @@ void TMP102Component::dump_config() {
|
||||
}
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
LOG_SENSOR(" ", "Temperature", this);
|
||||
#ifdef USE_TMP102_CONFIGURE
|
||||
if (!this->configure_)
|
||||
return;
|
||||
const uint8_t rate = (this->config_ >> 6) & 0x03;
|
||||
const LogString *rate_str = rate == 0 ? LOG_STR("0.25Hz")
|
||||
: rate == 1 ? LOG_STR("1Hz")
|
||||
: rate == 2 ? LOG_STR("4Hz")
|
||||
: LOG_STR("8Hz");
|
||||
const uint8_t fault_bits = (this->config_ >> 11) & 0x03;
|
||||
const bool extended = this->config_ & TMP102_EXTENDED_MODE_BIT;
|
||||
ESP_LOGCONFIG(
|
||||
TAG,
|
||||
" Extended Mode: %s\n"
|
||||
" One-Shot Mode: %s\n"
|
||||
" Conversion Rate: %s\n"
|
||||
" Thermostat Mode: %s\n"
|
||||
" Alert Polarity: %s\n"
|
||||
" Fault Queue: %u\n"
|
||||
" Temperature High: %.4f°C\n"
|
||||
" Temperature Low: %.4f°C",
|
||||
YESNO(extended), YESNO(this->config_ & TMP102_SHUTDOWN_BIT), LOG_STR_ARG(rate_str),
|
||||
this->config_ & TMP102_THERMOSTAT_MODE_BIT ? LOG_STR_LITERAL("Interrupt") : LOG_STR_LITERAL("Comparator"),
|
||||
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));
|
||||
if (this->config_ & TMP102_THERMOSTAT_MODE_BIT) {
|
||||
ESP_LOGW(TAG, "Register reads clear the interrupt-mode ALERT pin");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void TMP102Component::update() {
|
||||
#ifdef USE_TMP102_CONFIGURE
|
||||
if (this->configure_) {
|
||||
this->update_configured_();
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
if (this->write(&TMP102_REGISTER_TEMPERATURE, 1) != i2c::ERROR_OK) {
|
||||
this->status_set_warning();
|
||||
return;
|
||||
@@ -47,4 +102,75 @@ void TMP102Component::update() {
|
||||
});
|
||||
}
|
||||
|
||||
#ifdef USE_TMP102_CONFIGURE
|
||||
void TMP102Component::update_configured_() {
|
||||
if (!this->check_configuration_())
|
||||
return;
|
||||
if (this->config_ & TMP102_SHUTDOWN_BIT) {
|
||||
if (!this->write_register_(TMP102_REGISTER_CONFIGURATION, this->config_ | TMP102_ONE_SHOT_BIT)) {
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
this->set_timeout(READ_TEMP_TIMEOUT_ID, TMP102_ONESHOT_DELAY_MS, [this]() { this->read_temperature_(); });
|
||||
} else {
|
||||
this->read_temperature_();
|
||||
}
|
||||
}
|
||||
|
||||
bool TMP102Component::write_register_(uint8_t reg, uint16_t value) {
|
||||
uint8_t frame[3] = {reg, static_cast<uint8_t>(value >> 8), static_cast<uint8_t>(value)};
|
||||
if (this->write(frame, 3) == i2c::ERROR_OK)
|
||||
return true;
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TMP102Component::write_configuration_() {
|
||||
return this->write_register_(TMP102_REGISTER_CONFIGURATION, this->config_) &&
|
||||
this->write_register_(TMP102_REGISTER_HIGH_LIMIT, this->high_limit_) &&
|
||||
this->write_register_(TMP102_REGISTER_LOW_LIMIT, this->low_limit_);
|
||||
}
|
||||
|
||||
bool TMP102Component::check_configuration_() {
|
||||
uint16_t config;
|
||||
if (!this->read_byte_16(TMP102_REGISTER_CONFIGURATION, &config)) {
|
||||
this->status_set_warning();
|
||||
return false;
|
||||
}
|
||||
bool matches = (config & TMP102_CONFIGURATION_MASK) == (this->config_ & TMP102_CONFIGURATION_MASK);
|
||||
// A chip reset is invisible in the configuration register when it matches the reset default; check the limits.
|
||||
if (matches &&
|
||||
(this->config_ & TMP102_CONFIGURATION_MASK) == (TMP102_RESET_CONFIGURATION & TMP102_CONFIGURATION_MASK)) {
|
||||
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();
|
||||
return false;
|
||||
}
|
||||
matches = high == this->high_limit_ && low == this->low_limit_;
|
||||
}
|
||||
if (matches)
|
||||
return true;
|
||||
|
||||
ESP_LOGW(TAG, "Configuration changed; restoring configured settings and thresholds");
|
||||
this->status_set_warning();
|
||||
if (!this->write_configuration_()) {
|
||||
ESP_LOGW(TAG, "Failed to restore configuration and thresholds");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void TMP102Component::read_temperature_() {
|
||||
uint16_t raw;
|
||||
if (!this->read_byte_16(TMP102_REGISTER_TEMPERATURE, &raw)) {
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
const float temperature = decode_temperature(raw, raw & 0x0001);
|
||||
ESP_LOGD(TAG, "Got Temperature=%.2f°C", temperature);
|
||||
this->publish_state(temperature);
|
||||
this->status_clear_warning();
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace esphome::tmp102
|
||||
|
||||
@@ -1,15 +1,41 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace esphome::tmp102 {
|
||||
|
||||
class TMP102Component final : public PollingComponent, public i2c::I2CDevice, public sensor::Sensor {
|
||||
public:
|
||||
#ifdef USE_TMP102_CONFIGURE
|
||||
void setup() override;
|
||||
void set_configuration(uint16_t config, uint16_t high_limit, uint16_t low_limit) {
|
||||
this->config_ = config;
|
||||
this->high_limit_ = high_limit;
|
||||
this->low_limit_ = low_limit;
|
||||
this->configure_ = true;
|
||||
}
|
||||
#endif
|
||||
void dump_config() override;
|
||||
void update() override;
|
||||
|
||||
protected:
|
||||
#ifdef USE_TMP102_CONFIGURE
|
||||
bool write_register_(uint8_t reg, uint16_t value);
|
||||
bool write_configuration_();
|
||||
bool check_configuration_();
|
||||
void read_temperature_();
|
||||
void update_configured_();
|
||||
|
||||
uint16_t config_{0};
|
||||
uint16_t high_limit_{0};
|
||||
uint16_t low_limit_{0};
|
||||
bool configure_{false};
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace esphome::tmp102
|
||||
|
||||
@@ -243,6 +243,7 @@
|
||||
#define USE_TEXT_SENSOR_FILTER
|
||||
#define USE_TIME
|
||||
#define USE_TIME_TRIGGERS
|
||||
#define USE_TMP102_CONFIGURE
|
||||
#define USE_TOUCHSCREEN
|
||||
#define USE_UART_DEBUGGER
|
||||
#define USE_UART_WAKE_LOOP_ON_RX
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
"""TMP102 regression tests."""
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
esphome:
|
||||
name: tmp102-test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
i2c:
|
||||
sda: GPIO21
|
||||
scl: GPIO22
|
||||
|
||||
sensor:
|
||||
- platform: tmp102
|
||||
id: chip
|
||||
name: Temperature
|
||||
extended_mode: false
|
||||
conversion_rate: 1Hz
|
||||
one_shot_mode: false
|
||||
alert_polarity: active_high
|
||||
thermostat_mode: interrupt
|
||||
fault_queue: 2
|
||||
temperature_high: 30
|
||||
temperature_low: 25
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
esphome:
|
||||
name: tmp102-test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
i2c:
|
||||
sda: GPIO21
|
||||
scl: GPIO22
|
||||
|
||||
sensor:
|
||||
- platform: tmp102
|
||||
id: chip
|
||||
name: Temperature
|
||||
@@ -0,0 +1,95 @@
|
||||
"""Exercise TMP102 sensor validation and code generation."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components import tmp102 as component
|
||||
from esphome.components.tmp102.sensor import CONFIG_SCHEMA
|
||||
import esphome.config_validation as cv
|
||||
from esphome.core import CORE
|
||||
|
||||
|
||||
def test_basic_unchanged(
|
||||
generate_main: Callable[[Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
source = generate_main(component_config_path("basic.yaml"))
|
||||
assert "set_configuration" not in source
|
||||
defines = " ".join(define.name for define in CORE.defines)
|
||||
assert "USE_TMP102_" not in defines
|
||||
|
||||
|
||||
def test_advanced_options_enable_configuration_once(
|
||||
generate_main: Callable[[Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
source = generate_main(component_config_path("advanced.yaml"))
|
||||
assert source.count("set_configuration(") == 1
|
||||
assert "set_configuration(3648, 7680, 6400)" in source
|
||||
defines = " ".join(define.name for define in CORE.defines)
|
||||
assert "USE_TMP102_CONFIGURE" in defines
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("extended", "high", "low"),
|
||||
[
|
||||
(False, 127.9375, -55),
|
||||
(True, 150, -55),
|
||||
(False, 30, 30),
|
||||
],
|
||||
)
|
||||
def test_valid_limits(extended: bool, high: float, low: float) -> None:
|
||||
validated = CONFIG_SCHEMA(
|
||||
{
|
||||
"name": "Temperature",
|
||||
"extended_mode": extended,
|
||||
"temperature_high": high,
|
||||
"temperature_low": low,
|
||||
}
|
||||
)
|
||||
assert validated["temperature_high"] == high
|
||||
assert validated["temperature_low"] == low
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"config",
|
||||
[
|
||||
{"temperature_high": float("nan")},
|
||||
{"temperature_low": float("inf")},
|
||||
{"temperature_low": -55.0625},
|
||||
{"temperature_high": 128},
|
||||
{"extended_mode": True, "temperature_high": 150.0625},
|
||||
{"temperature_high": 30, "temperature_low": 31},
|
||||
{"temperature_high": 74},
|
||||
{"temperature_low": 81},
|
||||
{"fault_queue": 3},
|
||||
],
|
||||
)
|
||||
def test_invalid_limits(config: dict) -> None:
|
||||
with pytest.raises(cv.Invalid):
|
||||
CONFIG_SCHEMA({"name": "Temperature", **config})
|
||||
|
||||
|
||||
def test_precomputed_register_values() -> None:
|
||||
config = CONFIG_SCHEMA(
|
||||
{
|
||||
"name": "Temperature",
|
||||
"extended_mode": True,
|
||||
"conversion_rate": "0.25Hz",
|
||||
"one_shot_mode": True,
|
||||
"alert_polarity": "active_high",
|
||||
"thermostat_mode": "interrupt",
|
||||
"fault_queue": 6,
|
||||
"temperature_high": 12.22,
|
||||
"temperature_low": -12.22,
|
||||
}
|
||||
)
|
||||
assert component.build_configuration(config) == 0x1F10
|
||||
assert component.encode_temperature(config["temperature_high"], True) == 0x0620
|
||||
assert component.encode_temperature(config["temperature_low"], True) == 0xF9E0
|
||||
|
||||
|
||||
def test_defaults_match_chip_reset() -> None:
|
||||
assert component.build_configuration({}) == 0x0080
|
||||
@@ -0,0 +1,13 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.core import CORE
|
||||
from esphome.types import ConfigType
|
||||
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")
|
||||
cg.add_define("USE_TMP102_CONFIGURE")
|
||||
|
||||
manifest.to_code = to_code_testing
|
||||
manifest.dependencies = manifest.dependencies + ["i2c", "sensor"]
|
||||
@@ -2,3 +2,16 @@ sensor:
|
||||
- platform: tmp102
|
||||
i2c_id: i2c_bus
|
||||
name: TMP102 Temperature
|
||||
- platform: tmp102
|
||||
id: tmp102_extended_temperature
|
||||
name: TMP102 Extended Temperature
|
||||
i2c_id: i2c_bus
|
||||
address: 0x49
|
||||
extended_mode: true
|
||||
one_shot_mode: true
|
||||
conversion_rate: 8Hz
|
||||
alert_polarity: active_high
|
||||
thermostat_mode: interrupt
|
||||
fault_queue: 6
|
||||
temperature_high: 140°C
|
||||
temperature_low: 120°C
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
tmp102: !include common.yaml
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
tmp102: !include common.yaml
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
tmp102: !include common.yaml
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
#ifdef USE_HOST
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <array>
|
||||
#include "esphome/components/tmp102/tmp102.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
namespace esphome::tmp102::testing {
|
||||
|
||||
class RegisterBus final : public i2c::I2CBus {
|
||||
public:
|
||||
std::array<uint16_t, 4> registers{0x1900, 0x60A0, 0x4B00, 0x5000};
|
||||
int fail_read{-1};
|
||||
int fail_write{-1};
|
||||
int fail_select{-1};
|
||||
unsigned transactions{0};
|
||||
unsigned writes{0};
|
||||
uint8_t pointer{0};
|
||||
|
||||
i2c::ErrorCode write_readv(uint8_t address, const uint8_t *data, size_t size, uint8_t *out,
|
||||
size_t out_size) override {
|
||||
++this->transactions;
|
||||
if (size != 0) {
|
||||
if (data[0] >= this->registers.size() || data[0] == this->fail_select)
|
||||
return i2c::ERROR_NOT_ACKNOWLEDGED;
|
||||
this->pointer = data[0];
|
||||
if (size == 3) {
|
||||
if (this->pointer == this->fail_write)
|
||||
return i2c::ERROR_NOT_ACKNOWLEDGED;
|
||||
++this->writes;
|
||||
this->registers[this->pointer] = (uint16_t(data[1]) << 8) | data[2];
|
||||
}
|
||||
}
|
||||
if (out_size != 0) {
|
||||
if (out_size != 2 || this->pointer == this->fail_read)
|
||||
return i2c::ERROR_NOT_ACKNOWLEDGED;
|
||||
out[0] = this->registers[this->pointer] >> 8;
|
||||
out[1] = this->registers[this->pointer];
|
||||
}
|
||||
return i2c::ERROR_OK;
|
||||
}
|
||||
};
|
||||
|
||||
class TMP102Test : public ::testing::Test {
|
||||
protected:
|
||||
RegisterBus bus_;
|
||||
TMP102Component chip_;
|
||||
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);
|
||||
this->chip_.set_i2c_bus(&this->bus_);
|
||||
this->chip_.set_i2c_address(0x48);
|
||||
this->chip_.add_on_state_callback([this](float) { ++this->publications_; });
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
App.scheduler.cancel_timeout(&this->chip_, uint32_t{0});
|
||||
App.scheduler.call(millis());
|
||||
App.~Application();
|
||||
}
|
||||
|
||||
static void run_timers(uint32_t duration) {
|
||||
App.scheduler.call(millis());
|
||||
delay(duration);
|
||||
App.scheduler.call(millis());
|
||||
}
|
||||
|
||||
void configure(uint16_t config = 0x0080, uint16_t high = 0x5000, uint16_t low = 0x4B00) {
|
||||
this->chip_.set_configuration(config, high, low);
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(TMP102Test, LegacySetupAndContinuousReadDoNotWriteRegisters) {
|
||||
const auto original = this->bus_.registers;
|
||||
this->chip_.setup();
|
||||
EXPECT_EQ(this->bus_.transactions, 0);
|
||||
this->chip_.update();
|
||||
run_timers(55);
|
||||
EXPECT_FLOAT_EQ(this->chip_.state, 25);
|
||||
EXPECT_EQ(this->publications_, 1);
|
||||
EXPECT_EQ(this->bus_.writes, 0);
|
||||
EXPECT_EQ(this->bus_.registers, original);
|
||||
}
|
||||
|
||||
TEST_F(TMP102Test, SampleFormatOverridesConfiguredModeInBothDirections) {
|
||||
for (bool configured_extended : {false, true}) {
|
||||
this->configure(configured_extended ? 0x0010 : 0, configured_extended ? 0x2800 : 0x5000,
|
||||
configured_extended ? 0x2580 : 0x4B00);
|
||||
this->chip_.setup();
|
||||
const auto writes = this->bus_.writes;
|
||||
for (auto raw : {0x1900, 0x0C81, 0xFF00, 0xFF81}) {
|
||||
this->bus_.registers[0] = raw;
|
||||
this->chip_.update();
|
||||
EXPECT_FLOAT_EQ(this->chip_.state, raw & 0x8000 ? -1 : 25);
|
||||
EXPECT_FALSE(this->chip_.status_has_warning());
|
||||
EXPECT_EQ(this->bus_.writes, writes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(TMP102Test, WritableConfigurationFields) {
|
||||
this->configure(0x1FD0, 0x2800, 0x2580);
|
||||
this->chip_.setup();
|
||||
EXPECT_EQ(this->bus_.registers[1], 0x1FD0);
|
||||
EXPECT_EQ(this->bus_.registers[3], 0x2800);
|
||||
EXPECT_EQ(this->bus_.registers[2], 0x2580);
|
||||
}
|
||||
|
||||
TEST_F(TMP102Test, RestoresRegistersAfterChipOnlyResetBeforePublishing) {
|
||||
this->configure(0x0090, 0x2D00, 0x2A80);
|
||||
this->chip_.setup();
|
||||
const auto configured = this->bus_.registers;
|
||||
this->chip_.update();
|
||||
this->bus_.registers = {0x1900, 0x60A0, 0x4B00, 0x5000};
|
||||
this->chip_.update();
|
||||
EXPECT_EQ(this->publications_, 1);
|
||||
EXPECT_TRUE(this->chip_.status_has_warning());
|
||||
for (size_t reg = 1; reg < 4; ++reg)
|
||||
EXPECT_EQ(this->bus_.registers[reg], configured[reg]);
|
||||
this->chip_.update();
|
||||
EXPECT_EQ(this->publications_, 2);
|
||||
EXPECT_FLOAT_EQ(this->chip_.state, 25);
|
||||
EXPECT_FALSE(this->chip_.status_has_warning());
|
||||
}
|
||||
|
||||
TEST_F(TMP102Test, RecoveryRetriesFailedWritesAndDetectsThresholdOnlyChange) {
|
||||
this->configure();
|
||||
this->chip_.setup();
|
||||
this->bus_.registers[3] = 0;
|
||||
this->bus_.fail_write = 3;
|
||||
this->chip_.update();
|
||||
EXPECT_EQ(this->publications_, 0);
|
||||
this->bus_.fail_write = -1;
|
||||
this->chip_.update();
|
||||
EXPECT_EQ(this->bus_.registers[3], 0x5000);
|
||||
this->chip_.update();
|
||||
EXPECT_EQ(this->publications_, 1);
|
||||
}
|
||||
|
||||
TEST_F(TMP102Test, ConfigurationReadFailuresDoNotPublishOrOverwriteRegisters) {
|
||||
this->configure();
|
||||
this->chip_.setup();
|
||||
for (int reg : {1, 2, 3}) {
|
||||
const auto publications = this->publications_;
|
||||
const auto writes = this->bus_.writes;
|
||||
this->bus_.fail_read = reg;
|
||||
this->chip_.update();
|
||||
EXPECT_TRUE(this->chip_.status_has_warning());
|
||||
EXPECT_EQ(this->publications_, publications);
|
||||
EXPECT_EQ(this->bus_.writes, writes);
|
||||
this->bus_.fail_read = -1;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(TMP102Test, ReadOnlyConfigurationBitsDoNotTriggerRecovery) {
|
||||
this->configure();
|
||||
this->chip_.setup();
|
||||
const auto writes = this->bus_.writes;
|
||||
this->bus_.registers[1] ^= 0xE020;
|
||||
this->chip_.update();
|
||||
EXPECT_EQ(this->publications_, 1);
|
||||
EXPECT_EQ(this->bus_.writes, writes);
|
||||
EXPECT_FALSE(this->chip_.status_has_warning());
|
||||
}
|
||||
|
||||
TEST_F(TMP102Test, OneShotTriggerFailureSetsWarning) {
|
||||
this->configure(0x0180);
|
||||
this->chip_.setup();
|
||||
this->bus_.fail_write = 1;
|
||||
this->chip_.update();
|
||||
run_timers(45);
|
||||
EXPECT_EQ(this->publications_, 0);
|
||||
EXPECT_TRUE(this->chip_.status_has_warning());
|
||||
}
|
||||
|
||||
TEST_F(TMP102Test, OneShotRestartsPendingReadAndReselectsTemperature) {
|
||||
this->configure(0x0180);
|
||||
this->chip_.setup();
|
||||
this->chip_.update();
|
||||
this->chip_.update();
|
||||
EXPECT_EQ(this->publications_, 0);
|
||||
this->bus_.pointer = 3;
|
||||
run_timers(45);
|
||||
EXPECT_EQ(this->publications_, 1);
|
||||
EXPECT_FLOAT_EQ(this->chip_.state, 25);
|
||||
}
|
||||
|
||||
TEST_F(TMP102Test, LimitsAreOnlyCheckedWhenConfigurationMatchesResetDefault) {
|
||||
this->configure();
|
||||
this->chip_.setup();
|
||||
this->bus_.registers[2] = 0;
|
||||
this->chip_.update();
|
||||
EXPECT_EQ(this->publications_, 0);
|
||||
EXPECT_TRUE(this->chip_.status_has_warning());
|
||||
EXPECT_EQ(this->bus_.registers[2], 0x4B00);
|
||||
|
||||
this->configure(0x00C0);
|
||||
this->chip_.setup();
|
||||
this->bus_.registers[2] = 0;
|
||||
const auto transactions = this->bus_.transactions;
|
||||
const auto writes = this->bus_.writes;
|
||||
this->chip_.update();
|
||||
EXPECT_EQ(this->bus_.transactions - transactions, 2);
|
||||
EXPECT_EQ(this->bus_.writes, writes);
|
||||
EXPECT_EQ(this->publications_, 1);
|
||||
}
|
||||
|
||||
TEST_F(TMP102Test, FailureOfAnySetupWriteMarksComponentFailed) {
|
||||
for (int reg : {1, 2, 3}) {
|
||||
TMP102Component failed;
|
||||
failed.set_i2c_bus(&this->bus_);
|
||||
failed.set_i2c_address(0x48);
|
||||
failed.set_configuration(0x0080, 0x5000, 0x4B00);
|
||||
this->bus_.fail_write = reg;
|
||||
failed.setup();
|
||||
EXPECT_TRUE(failed.is_failed());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::tmp102::testing
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user