mirror of
https://github.com/esphome/esphome.git
synced 2026-10-14 18:22:48 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44fd62c89a | ||
|
|
1338902ac5 | ||
|
|
60a1d70805 | ||
|
|
4c15abad29 | ||
|
|
887d4c3a83 | ||
|
|
c1fa73470e | ||
|
|
e41d16ba7e | ||
|
|
5171113c87 | ||
|
|
5718e4b779 |
@@ -2,6 +2,7 @@
|
||||
#include "cc1101pa.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
|
||||
@@ -14,6 +15,9 @@ namespace esphome::cc1101 {
|
||||
ESPHOME_LOG_TAG(TAG, "cc1101");
|
||||
static constexpr size_t TX_FIFO_SIZE = 64;
|
||||
|
||||
// MDMCFG2 MOD_FORMAT values
|
||||
PROGMEM_STRING_TABLE(ModulationStrings, "2-FSK", "GFSK", "UNUSED", "ASK/OOK", "4-FSK", "UNUSED", "UNUSED", "MSK");
|
||||
|
||||
static void split_float(float value, int mbits, uint8_t &e, uint32_t &m) {
|
||||
int e_tmp;
|
||||
float m_tmp = std::frexp(value, &e_tmp);
|
||||
@@ -238,8 +242,6 @@ void CC1101Component::loop() {
|
||||
}
|
||||
|
||||
void CC1101Component::dump_config() {
|
||||
static const char *const MODULATION_NAMES[] = {"2-FSK", "GFSK", "UNUSED", "ASK/OOK",
|
||||
"4-FSK", "UNUSED", "UNUSED", "MSK"};
|
||||
int32_t freq = static_cast<int32_t>(this->state_.FREQ2 << 16 | this->state_.FREQ1 << 8 | this->state_.FREQ0) *
|
||||
XTAL_FREQUENCY / (1 << 16);
|
||||
float symbol_rate = (((256.0f + this->state_.DRATE_M) * (1 << this->state_.DRATE_E)) / (1 << 28)) * XTAL_FREQUENCY;
|
||||
@@ -253,8 +255,9 @@ void CC1101Component::dump_config() {
|
||||
" Symbol Rate: %.0f baud\n"
|
||||
" Filter Bandwidth: %.1f Hz\n"
|
||||
" Output Power: %.1f dBm",
|
||||
this->chip_id_, freq, this->state_.CHANNR, MODULATION_NAMES[this->state_.MOD_FORMAT & 0x07],
|
||||
symbol_rate, bw, this->output_power_effective_);
|
||||
this->chip_id_, freq, this->state_.CHANNR,
|
||||
LOG_STR_ARG(ModulationStrings::get_log_str(this->state_.MOD_FORMAT & 0x07, 0)), symbol_rate, bw,
|
||||
this->output_power_effective_);
|
||||
LOG_PIN(" CS Pin: ", this->cs_);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "display_color_utils.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::display {
|
||||
@@ -52,40 +53,65 @@ void Display::line_at_angle(int x, int y, int angle, int start_radius, int stop_
|
||||
this->line(x1, y1, x2, y2, color);
|
||||
}
|
||||
|
||||
void Display::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
|
||||
ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
|
||||
size_t line_stride = x_offset + w + x_pad; // length of each source line in pixels
|
||||
uint32_t color_value;
|
||||
// flatten keeps to_color() inlined in the pixel loop; in PSRAM builds GCC at
|
||||
// -Os stops inlining it once there are two call sites, which costs a call and
|
||||
// spills per pixel. Without PSRAM there is one call site and it changes
|
||||
// nothing, and draw_pixel_at() stays a virtual call either way.
|
||||
template<ColorBitness BITNESS>
|
||||
static void __attribute__((flatten))
|
||||
draw_packed_pixels(Display *display, int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
|
||||
bool big_endian, int x_offset, int y_offset, int x_pad) {
|
||||
const size_t line_stride = x_offset + w + x_pad; // length of each source line in pixels
|
||||
// BITNESS is a constant here, so to_color()'s three per pixel divisions
|
||||
// become a multiply and shift and its bitness switch folds.
|
||||
const auto draw_source_pixel = [&](int x, int y, size_t source_idx) ESPHOME_ALWAYS_INLINE {
|
||||
display->draw_pixel_at(
|
||||
x + x_start, y + y_start,
|
||||
ColorUtil::to_color(ColorUtil::read_packed<BITNESS>(ptr, source_idx, big_endian), order, BITNESS));
|
||||
};
|
||||
// The source and frame buffers are row-major, so walking rows keeps the read
|
||||
// and the write sequential. When the display swaps the axes before writing,
|
||||
// walking columns keeps the write sequential instead; with the frame buffer
|
||||
// in PSRAM a strided write costs more than a strided read.
|
||||
#ifdef USE_PSRAM
|
||||
if (display->pixel_axes_swapped()) {
|
||||
size_t column_idx = y_offset * line_stride + x_offset;
|
||||
for (int x = 0; x != w; x++, column_idx++) {
|
||||
size_t source_idx = column_idx;
|
||||
for (int y = 0; y != h; y++, source_idx += line_stride) {
|
||||
draw_source_pixel(x, y, source_idx);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
// Row walk: builds without PSRAM, and PSRAM builds whose axes are not swapped.
|
||||
for (int y = 0; y != h; y++) {
|
||||
size_t source_idx = (y_offset + y) * line_stride + x_offset;
|
||||
size_t source_idx_mod;
|
||||
for (int x = 0; x != w; x++, source_idx++) {
|
||||
switch (bitness) {
|
||||
default:
|
||||
color_value = ptr[source_idx];
|
||||
break;
|
||||
case COLOR_BITNESS_565:
|
||||
source_idx_mod = source_idx * 2;
|
||||
if (big_endian) {
|
||||
color_value = (ptr[source_idx_mod] << 8) + ptr[source_idx_mod + 1];
|
||||
} else {
|
||||
color_value = ptr[source_idx_mod] + (ptr[source_idx_mod + 1] << 8);
|
||||
}
|
||||
break;
|
||||
case COLOR_BITNESS_888:
|
||||
source_idx_mod = source_idx * 3;
|
||||
if (big_endian) {
|
||||
color_value = (ptr[source_idx_mod + 0] << 16) + (ptr[source_idx_mod + 1] << 8) + ptr[source_idx_mod + 2];
|
||||
} else {
|
||||
color_value = ptr[source_idx_mod + 0] + (ptr[source_idx_mod + 1] << 8) + (ptr[source_idx_mod + 2] << 16);
|
||||
}
|
||||
break;
|
||||
}
|
||||
this->draw_pixel_at(x + x_start, y + y_start, ColorUtil::to_color(color_value, order, bitness));
|
||||
draw_source_pixel(x, y, source_idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Display::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
|
||||
ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
|
||||
switch (bitness) {
|
||||
case COLOR_BITNESS_565:
|
||||
draw_packed_pixels<COLOR_BITNESS_565>(this, x_start, y_start, w, h, ptr, order, big_endian, x_offset, y_offset,
|
||||
x_pad);
|
||||
break;
|
||||
case COLOR_BITNESS_888:
|
||||
draw_packed_pixels<COLOR_BITNESS_888>(this, x_start, y_start, w, h, ptr, order, big_endian, x_offset, y_offset,
|
||||
x_pad);
|
||||
break;
|
||||
default:
|
||||
draw_packed_pixels<COLOR_BITNESS_332>(this, x_start, y_start, w, h, ptr, order, big_endian, x_offset, y_offset,
|
||||
x_pad);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void HOT Display::horizontal_line(int x, int y, int width, Color color) {
|
||||
// Future: Could be made more efficient by manipulating buffer directly in certain rotations.
|
||||
for (int i = x; i < x + width; i++)
|
||||
|
||||
@@ -139,6 +139,11 @@ enum DisplayRotation {
|
||||
DISPLAY_ROTATION_270_DEGREES = 270,
|
||||
};
|
||||
|
||||
/// Whether a rotation swaps the x and y axes.
|
||||
constexpr bool rotation_swaps_axes(DisplayRotation rotation) {
|
||||
return rotation == DISPLAY_ROTATION_90_DEGREES || rotation == DISPLAY_ROTATION_270_DEGREES;
|
||||
}
|
||||
|
||||
const int EDGES_TRIGON = 3;
|
||||
const int EDGES_TRIANGLE = 3;
|
||||
const int EDGES_TETRAGON = 4;
|
||||
@@ -717,6 +722,12 @@ class Display : public PollingComponent {
|
||||
|
||||
virtual DisplayRotation get_rotation() const { return this->rotation_; }
|
||||
|
||||
/// True when draw_pixel_at() swaps x and y before writing, so pixels that are
|
||||
/// neighbours along x land a row apart in the frame buffer. Not the same as
|
||||
/// get_width()/get_height() swapping: a display that rotates in hardware
|
||||
/// reports swapped dimensions but writes unswapped pixels.
|
||||
virtual bool pixel_axes_swapped() const { return rotation_swaps_axes(this->rotation_); }
|
||||
|
||||
/** Get the type of display that the buffer corresponds to. In case of dynamically configurable displays,
|
||||
* returns the type the display is currently configured to.
|
||||
*/
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include "esphome/core/color.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::display {
|
||||
enum ColorOrder : uint8_t { COLOR_ORDER_RGB = 0, COLOR_ORDER_BGR = 1, COLOR_ORDER_GRB = 2 };
|
||||
@@ -10,6 +11,27 @@ inline static uint8_t esp_scale(uint8_t i, uint8_t scale, uint8_t max_value = 25
|
||||
|
||||
class ColorUtil {
|
||||
public:
|
||||
/// Reads one packed pixel of the given bitness from a source buffer into the
|
||||
/// value to_color() takes. BITNESS is a template argument so the byte layout
|
||||
/// folds; pass it on to to_color() as a constant too, and its channel scaling
|
||||
/// divides by a constant, which the compiler turns into a multiply and shift.
|
||||
template<ColorBitness BITNESS>
|
||||
static ESPHOME_ALWAYS_INLINE uint32_t read_packed(const uint8_t *ptr, size_t index, bool big_endian) {
|
||||
if constexpr (BITNESS == COLOR_BITNESS_565) {
|
||||
const size_t idx = index * 2;
|
||||
if (big_endian)
|
||||
return (ptr[idx] << 8) + ptr[idx + 1];
|
||||
return ptr[idx] + (ptr[idx + 1] << 8);
|
||||
} else if constexpr (BITNESS == COLOR_BITNESS_888) {
|
||||
const size_t idx = index * 3;
|
||||
if (big_endian)
|
||||
return (ptr[idx + 0] << 16) + (ptr[idx + 1] << 8) + ptr[idx + 2];
|
||||
return ptr[idx + 0] + (ptr[idx + 1] << 8) + (ptr[idx + 2] << 16);
|
||||
} else {
|
||||
return ptr[index];
|
||||
}
|
||||
}
|
||||
|
||||
static Color to_color(uint32_t colorcode, ColorOrder color_order,
|
||||
ColorBitness color_bitness = ColorBitness::COLOR_BITNESS_888, bool right_bit_aligned = true) {
|
||||
uint8_t first_color, second_color, third_color;
|
||||
|
||||
@@ -106,6 +106,7 @@ class EPaperBase : public Display,
|
||||
|
||||
int get_width() override { return this->effective_transform_ & SWAP_XY ? this->height_ : this->width_; }
|
||||
int get_height() override { return this->effective_transform_ & SWAP_XY ? this->width_ : this->height_; }
|
||||
bool pixel_axes_swapped() const override { return this->effective_transform_ & SWAP_XY; }
|
||||
void draw_pixel_at(int x, int y, Color color) override;
|
||||
void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
|
||||
ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override;
|
||||
|
||||
@@ -47,6 +47,7 @@ CanMode = esp32_can_ns.enum("CanMode")
|
||||
CAN_MODES = {
|
||||
"NORMAL": CanMode.CAN_MODE_NORMAL,
|
||||
"LISTENONLY": CanMode.CAN_MODE_LISTEN_ONLY,
|
||||
"NO_ACK": CanMode.CAN_MODE_NO_ACK,
|
||||
}
|
||||
|
||||
# Currently the driver only supports a subset of the bit rates defined in canbus
|
||||
|
||||
@@ -79,10 +79,13 @@ bool ESP32Can::setup_internal() {
|
||||
}
|
||||
|
||||
// Select TWAI mode based on configuration
|
||||
twai_mode_t twai_mode = (this->mode_ == CAN_MODE_LISTEN_ONLY) ? TWAI_MODE_LISTEN_ONLY : TWAI_MODE_NORMAL;
|
||||
|
||||
twai_mode_t twai_mode = TWAI_MODE_NORMAL;
|
||||
if (this->mode_ == CAN_MODE_LISTEN_ONLY) {
|
||||
twai_mode = TWAI_MODE_LISTEN_ONLY;
|
||||
ESP_LOGI(TAG, "CAN bus configured in LISTEN_ONLY mode (passive, no ACKs)");
|
||||
} else if (this->mode_ == CAN_MODE_NO_ACK) {
|
||||
twai_mode = TWAI_MODE_NO_ACK;
|
||||
ESP_LOGI(TAG, "CAN bus configured in NO_ACK mode (transmit without requiring ACK)");
|
||||
}
|
||||
|
||||
twai_general_config_t g_config =
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace esphome::esp32_can {
|
||||
enum CanMode : uint8_t {
|
||||
CAN_MODE_NORMAL = 0,
|
||||
CAN_MODE_LISTEN_ONLY = 1,
|
||||
CAN_MODE_NO_ACK = 2,
|
||||
};
|
||||
|
||||
class ESP32Can final : public canbus::Canbus {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "esphome/components/climate/climate.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
#include "hon_climate.h"
|
||||
#include "hon_packet.h"
|
||||
|
||||
@@ -20,6 +21,27 @@ constexpr size_t ALARM_STATUS_REQUEST_INTERVAL_MS = 600000;
|
||||
const uint8_t ONE_BUF[] = {0x00, 0x01};
|
||||
const uint8_t ZERO_BUF[] = {0x00, 0x00};
|
||||
|
||||
// Alarm texts indexed by alarm code; the last entry is the fallback for unknown codes
|
||||
PROGMEM_STRING_TABLE(
|
||||
HonAlarmMessages, "Outdoor module failure", "Outdoor defrost sensor failure",
|
||||
"Outdoor compressor exhaust sensor failure", "Outdoor EEPROM abnormality", "Indoor coil sensor failure",
|
||||
"Indoor-outdoor communication failure", "Power supply overvoltage protection",
|
||||
"Communication failure between panel and indoor unit", "Outdoor compressor overheat protection",
|
||||
"Outdoor environmental sensor abnormality", "Full water protection", "Indoor EEPROM failure",
|
||||
"Outdoor out air sensor failure", "CBD and module communication failure", "Indoor DC fan failure",
|
||||
"Outdoor DC fan failure", "Door switch failure", "Dust filter needs cleaning reminder", "Water shortage protection",
|
||||
"Humidity sensor failure", "Indoor temperature sensor failure", "Manipulator limit failure",
|
||||
"Indoor PM2.5 sensor failure", "Outdoor PM2.5 sensor failure", "Indoor heating overload/high load alarm",
|
||||
"Outdoor AC current protection", "Outdoor compressor operation abnormality", "Outdoor DC current protection",
|
||||
"Outdoor no-load failure", "CT current abnormality", "Indoor cooling freeze protection",
|
||||
"High and low pressure protection", "Compressor out air temperature is too high",
|
||||
"Outdoor evaporator sensor failure", "Outdoor cooling overload", "Water pump drainage failure",
|
||||
"Three-phase power supply failure", "Four-way valve failure", "External alarm/scraper flow switch failure",
|
||||
"Temperature cutoff protection alarm", "Different mode operation failure", "Electronic expansion valve failure",
|
||||
"Dual heat source sensor Tw failure", "Communication failure with the wired controller",
|
||||
"Indoor unit address duplication failure", "50Hz zero crossing failure", "Outdoor unit failure",
|
||||
"Formaldehyde sensor failure", "VOC sensor failure", "CO2 sensor failure", "Firewall failure", "Unknown");
|
||||
|
||||
HonClimate::HonClimate()
|
||||
: cleaning_status_(CleaningState::NO_CLEANING), got_valid_outdoor_temp_(false), active_alarms_{0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00,
|
||||
@@ -687,9 +709,17 @@ void HonClimate::process_alarm_message_(const uint8_t *packet, uint8_t size, boo
|
||||
if ((packet[2 + i] & alarm_bit) != (this->active_alarms_[i] & alarm_bit)) {
|
||||
bool alarm_status = (packet[2 + i] & alarm_bit) != 0;
|
||||
int log_level = alarm_status ? ESPHOME_LOG_LEVEL_WARN : ESPHOME_LOG_LEVEL_INFO;
|
||||
const char *alarm_message = alarm_code < esphome::haier::hon_protocol::HON_ALARM_COUNT
|
||||
? esphome::haier::hon_protocol::HON_ALARM_MESSAGES[alarm_code].c_str()
|
||||
: "Unknown";
|
||||
#ifdef USE_ESP8266
|
||||
// Static so a delayed on_alarm automation can still read it, but only until the next
|
||||
// alarm change, including a later one in this packet; `code` stays correct
|
||||
static char alarm_message[HonAlarmMessages::MAX_LENGTH + 1];
|
||||
ESPHOME_strncpy_P(alarm_message,
|
||||
reinterpret_cast<const char *>(
|
||||
HonAlarmMessages::get_progmem_str(alarm_code, HonAlarmMessages::LAST_INDEX)),
|
||||
sizeof(alarm_message));
|
||||
#else
|
||||
const char *alarm_message = HonAlarmMessages::get_progmem_str(alarm_code, HonAlarmMessages::LAST_INDEX);
|
||||
#endif
|
||||
esp_log_printf_(log_level, TAG, __LINE__, "Alarm %s (%d): %s", alarm_status ? "activated" : "deactivated",
|
||||
alarm_code, alarm_message);
|
||||
if (alarm_status) {
|
||||
|
||||
@@ -197,60 +197,4 @@ enum class SubcommandsControl : uint16_t {
|
||||
// content: all values like in status packet)
|
||||
};
|
||||
|
||||
const std::string HON_ALARM_MESSAGES[] = {
|
||||
"Outdoor module failure",
|
||||
"Outdoor defrost sensor failure",
|
||||
"Outdoor compressor exhaust sensor failure",
|
||||
"Outdoor EEPROM abnormality",
|
||||
"Indoor coil sensor failure",
|
||||
"Indoor-outdoor communication failure",
|
||||
"Power supply overvoltage protection",
|
||||
"Communication failure between panel and indoor unit",
|
||||
"Outdoor compressor overheat protection",
|
||||
"Outdoor environmental sensor abnormality",
|
||||
"Full water protection",
|
||||
"Indoor EEPROM failure",
|
||||
"Outdoor out air sensor failure",
|
||||
"CBD and module communication failure",
|
||||
"Indoor DC fan failure",
|
||||
"Outdoor DC fan failure",
|
||||
"Door switch failure",
|
||||
"Dust filter needs cleaning reminder",
|
||||
"Water shortage protection",
|
||||
"Humidity sensor failure",
|
||||
"Indoor temperature sensor failure",
|
||||
"Manipulator limit failure",
|
||||
"Indoor PM2.5 sensor failure",
|
||||
"Outdoor PM2.5 sensor failure",
|
||||
"Indoor heating overload/high load alarm",
|
||||
"Outdoor AC current protection",
|
||||
"Outdoor compressor operation abnormality",
|
||||
"Outdoor DC current protection",
|
||||
"Outdoor no-load failure",
|
||||
"CT current abnormality",
|
||||
"Indoor cooling freeze protection",
|
||||
"High and low pressure protection",
|
||||
"Compressor out air temperature is too high",
|
||||
"Outdoor evaporator sensor failure",
|
||||
"Outdoor cooling overload",
|
||||
"Water pump drainage failure",
|
||||
"Three-phase power supply failure",
|
||||
"Four-way valve failure",
|
||||
"External alarm/scraper flow switch failure",
|
||||
"Temperature cutoff protection alarm",
|
||||
"Different mode operation failure",
|
||||
"Electronic expansion valve failure",
|
||||
"Dual heat source sensor Tw failure",
|
||||
"Communication failure with the wired controller",
|
||||
"Indoor unit address duplication failure",
|
||||
"50Hz zero crossing failure",
|
||||
"Outdoor unit failure",
|
||||
"Formaldehyde sensor failure",
|
||||
"VOC sensor failure",
|
||||
"CO2 sensor failure",
|
||||
"Firewall failure",
|
||||
};
|
||||
|
||||
constexpr size_t HON_ALARM_COUNT = sizeof(HON_ALARM_MESSAGES) / sizeof(HON_ALARM_MESSAGES[0]);
|
||||
|
||||
} // namespace esphome::haier::hon_protocol
|
||||
|
||||
@@ -126,8 +126,8 @@ CONFIG_SCHEMA = cv.All(
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await climate_ir.new_climate_ir(config)
|
||||
cg.add(var.set_protocol(config[CONF_PROTOCOL]))
|
||||
tag = heatpumpir_ns.ProtocolTag.template(config[CONF_PROTOCOL])
|
||||
var = await climate_ir.new_climate_ir(config, tag())
|
||||
cg.add(var.set_horizontal_default(config[CONF_HORIZONTAL_DEFAULT]))
|
||||
cg.add(var.set_vertical_default(config[CONF_VERTICAL_DEFAULT]))
|
||||
cg.add(var.set_max_temperature(config[CONF_MAX_TEMPERATURE]))
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
#if defined(USE_ARDUINO) || defined(USE_ESP32)
|
||||
|
||||
#include <cmath>
|
||||
#include <map>
|
||||
#include <IRSender.h>
|
||||
#include <HeatpumpIRFactory.h>
|
||||
#include "esphome/components/remote_base/remote_base.h"
|
||||
@@ -45,71 +44,76 @@ class IRSenderESPHome : public IRSender {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "heatpumpir.climate");
|
||||
|
||||
const std::map<Protocol, std::function<HeatpumpIR *()>> PROTOCOL_CONSTRUCTOR_MAP = {
|
||||
{PROTOCOL_AUX, []() { return new AUXHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_BALLU, []() { return new BalluHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_CARRIER_MCA, []() { return new CarrierMCAHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_CARRIER_NQV, []() { return new CarrierNQVHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_DAIKIN_ARC417, []() { return new DaikinHeatpumpARC417IR(); }}, // NOLINT
|
||||
{PROTOCOL_DAIKIN_ARC480, []() { return new DaikinHeatpumpARC480A14IR(); }}, // NOLINT
|
||||
{PROTOCOL_DAIKIN, []() { return new DaikinHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_ELECTROLUXYAL, []() { return new ElectroluxYALHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_FUEGO, []() { return new FuegoHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_FUJITSU_AWYZ, []() { return new FujitsuHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_GREE, []() { return new GreeGenericHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_GREEYAA, []() { return new GreeYAAHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_GREEYAN, []() { return new GreeYANHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_GREEYAC, []() { return new GreeYACHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_GREEYT, []() { return new GreeYTHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_GREEYAP, []() { return new GreeYAPHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_HISENSE_AUD, []() { return new HisenseHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_HITACHI, []() { return new HitachiHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_HYUNDAI, []() { return new HyundaiHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_IVT, []() { return new IVTHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_MIDEA, []() { return new MideaHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_MITSUBISHI_FA, []() { return new MitsubishiFAHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_MITSUBISHI_FD, []() { return new MitsubishiFDHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_MITSUBISHI_FE, []() { return new MitsubishiFEHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_MITSUBISHI_HEAVY_FDTC, []() { return new MitsubishiHeavyFDTCHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_MITSUBISHI_HEAVY_ZJ, []() { return new MitsubishiHeavyZJHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_MITSUBISHI_HEAVY_ZM, []() { return new MitsubishiHeavyZMHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_MITSUBISHI_HEAVY_ZMP, []() { return new MitsubishiHeavyZMPHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_MITSUBISHI_KJ, []() { return new MitsubishiKJHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_MITSUBISHI_MSC, []() { return new MitsubishiMSCHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_MITSUBISHI_MSY, []() { return new MitsubishiMSYHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_MITSUBISHI_SEZ, []() { return new MitsubishiSEZKDXXHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_PANASONIC_CKP, []() { return new PanasonicCKPHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_PANASONIC_DKE, []() { return new PanasonicDKEHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_PANASONIC_EKE, []() { return new PanasonicEKEHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_PANASONIC_JKE, []() { return new PanasonicJKEHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_PANASONIC_LKE, []() { return new PanasonicLKEHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_PANASONIC_NKE, []() { return new PanasonicNKEHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_SAMSUNG_AQV, []() { return new SamsungAQVHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_SAMSUNG_FJM, []() { return new SamsungFJMHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_SHARP, []() { return new SharpHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_TOSHIBA_DAISEIKAI, []() { return new ToshibaDaiseikaiHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_TOSHIBA, []() { return new ToshibaHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_ZHLT01, []() { return new ZHLT01HeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_NIBE, []() { return new NibeHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_QLIMA_1, []() { return new Qlima1HeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_QLIMA_2, []() { return new Qlima2HeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_SAMSUNG_AQV12MSAN, []() { return new SamsungAQV12MSANHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_ZHJG01, []() { return new ZHJG01HeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_AIRWAY, []() { return new AIRWAYHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_BGH_AUD, []() { return new BGHHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_PANASONIC_ALTDKE, []() { return new PanasonicAltDKEHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_PHILCO_PHS32, []() { return new PhilcoPHS32HeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_VAILLANTVAI8, []() { return new VaillantHeatpumpIR(); }}, // NOLINT
|
||||
{PROTOCOL_R51M, []() { return new R51MHeatpumpIR(); }}, // NOLINT
|
||||
};
|
||||
template<Protocol P> struct ProtocolClass;
|
||||
|
||||
template<Protocol P> HeatpumpIR *create_heatpump_ir() {
|
||||
return new typename ProtocolClass<P>::type(); // NOLINT
|
||||
}
|
||||
|
||||
// Codegen calls only the configured protocols, so the linker drops the other classes
|
||||
#define HEATPUMPIR_PROTOCOL(protocol, cls) \
|
||||
template<> struct ProtocolClass<protocol> { using type = cls; }; \
|
||||
template HeatpumpIR *create_heatpump_ir<protocol>();
|
||||
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_AUX, AUXHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_BALLU, BalluHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_CARRIER_MCA, CarrierMCAHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_CARRIER_NQV, CarrierNQVHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_DAIKIN_ARC417, DaikinHeatpumpARC417IR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_DAIKIN_ARC480, DaikinHeatpumpARC480A14IR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_DAIKIN, DaikinHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_ELECTROLUXYAL, ElectroluxYALHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_FUEGO, FuegoHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_FUJITSU_AWYZ, FujitsuHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_GREE, GreeGenericHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_GREEYAA, GreeYAAHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_GREEYAN, GreeYANHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_GREEYAC, GreeYACHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_GREEYT, GreeYTHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_GREEYAP, GreeYAPHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_HISENSE_AUD, HisenseHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_HITACHI, HitachiHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_HYUNDAI, HyundaiHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_IVT, IVTHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_MIDEA, MideaHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_FA, MitsubishiFAHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_FD, MitsubishiFDHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_FE, MitsubishiFEHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_HEAVY_FDTC, MitsubishiHeavyFDTCHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_HEAVY_ZJ, MitsubishiHeavyZJHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_HEAVY_ZM, MitsubishiHeavyZMHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_HEAVY_ZMP, MitsubishiHeavyZMPHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_KJ, MitsubishiKJHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_MSC, MitsubishiMSCHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_MSY, MitsubishiMSYHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_SEZ, MitsubishiSEZKDXXHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_PANASONIC_CKP, PanasonicCKPHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_PANASONIC_DKE, PanasonicDKEHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_PANASONIC_EKE, PanasonicEKEHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_PANASONIC_JKE, PanasonicJKEHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_PANASONIC_LKE, PanasonicLKEHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_PANASONIC_NKE, PanasonicNKEHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_SAMSUNG_AQV, SamsungAQVHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_SAMSUNG_FJM, SamsungFJMHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_SHARP, SharpHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_TOSHIBA_DAISEIKAI, ToshibaDaiseikaiHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_TOSHIBA, ToshibaHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_ZHLT01, ZHLT01HeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_NIBE, NibeHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_QLIMA_1, Qlima1HeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_QLIMA_2, Qlima2HeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_SAMSUNG_AQV12MSAN, SamsungAQV12MSANHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_ZHJG01, ZHJG01HeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_AIRWAY, AIRWAYHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_BGH_AUD, BGHHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_PANASONIC_ALTDKE, PanasonicAltDKEHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_PHILCO_PHS32, PhilcoPHS32HeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_VAILLANTVAI8, VaillantHeatpumpIR)
|
||||
HEATPUMPIR_PROTOCOL(PROTOCOL_R51M, R51MHeatpumpIR)
|
||||
|
||||
#undef HEATPUMPIR_PROTOCOL
|
||||
|
||||
void HeatpumpIRClimate::setup() {
|
||||
auto protocol_constructor = PROTOCOL_CONSTRUCTOR_MAP.find(protocol_);
|
||||
if (protocol_constructor == PROTOCOL_CONSTRUCTOR_MAP.end()) {
|
||||
ESP_LOGE(TAG, "Invalid protocol");
|
||||
return;
|
||||
}
|
||||
this->heatpump_ir_ = protocol_constructor->second();
|
||||
climate_ir::ClimateIR::setup();
|
||||
if (this->sensor_) {
|
||||
this->sensor_->add_on_state_callback([this](float state) {
|
||||
|
||||
@@ -89,25 +89,21 @@ enum VerticalDirection {
|
||||
VERTICAL_DIRECTION_DOWN = 5,
|
||||
};
|
||||
|
||||
/// Creates the HeatpumpIR library class for a protocol; instantiated in heatpumpir.cpp.
|
||||
template<Protocol P> HeatpumpIR *create_heatpump_ir();
|
||||
|
||||
/// Selects the protocol at compile time so the enum and the library object always match.
|
||||
template<Protocol P> struct ProtocolTag {};
|
||||
|
||||
// Temperature
|
||||
const float TEMP_MIN = 0; // Celsius
|
||||
const float TEMP_MAX = 100; // Celsius
|
||||
|
||||
class HeatpumpIRClimate final : public climate_ir::ClimateIR {
|
||||
public:
|
||||
HeatpumpIRClimate()
|
||||
: climate_ir::ClimateIR(TEMP_MIN, TEMP_MAX, 1.0f, true, true,
|
||||
{climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH,
|
||||
climate::CLIMATE_FAN_AUTO},
|
||||
{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_HORIZONTAL,
|
||||
climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_BOTH}) {}
|
||||
template<Protocol P>
|
||||
explicit HeatpumpIRClimate(ProtocolTag<P> /*tag*/) : HeatpumpIRClimate(P, create_heatpump_ir<P>()) {}
|
||||
void setup() override;
|
||||
void set_protocol(Protocol protocol) {
|
||||
this->protocol_ = protocol;
|
||||
if (this->has_fan_byte_presets_()) {
|
||||
this->presets_ = {climate::CLIMATE_PRESET_NONE, climate::CLIMATE_PRESET_ECO, climate::CLIMATE_PRESET_BOOST};
|
||||
}
|
||||
}
|
||||
void set_horizontal_default(HorizontalDirection horizontal_direction) {
|
||||
this->default_horizontal_direction_ = horizontal_direction;
|
||||
}
|
||||
@@ -119,6 +115,18 @@ class HeatpumpIRClimate final : public climate_ir::ClimateIR {
|
||||
void set_min_temperature(float temperature) { this->min_temperature_ = temperature; }
|
||||
|
||||
protected:
|
||||
HeatpumpIRClimate(Protocol protocol, HeatpumpIR *heatpump_ir)
|
||||
: climate_ir::ClimateIR(TEMP_MIN, TEMP_MAX, 1.0f, true, true,
|
||||
{climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH,
|
||||
climate::CLIMATE_FAN_AUTO},
|
||||
{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_HORIZONTAL,
|
||||
climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_BOTH}),
|
||||
heatpump_ir_(heatpump_ir),
|
||||
protocol_(protocol) {
|
||||
if (this->has_fan_byte_presets_()) {
|
||||
this->presets_ = {climate::CLIMATE_PRESET_NONE, climate::CLIMATE_PRESET_ECO, climate::CLIMATE_PRESET_BOOST};
|
||||
}
|
||||
}
|
||||
HeatpumpIR *heatpump_ir_;
|
||||
void control(const climate::ClimateCall &call) override;
|
||||
/// Transmit via IR the state of this climate controller.
|
||||
|
||||
@@ -204,6 +204,7 @@ class IT8951Display : public Display,
|
||||
ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override;
|
||||
int get_width() override { return (this->effective_transform_ & TRANSFORM_SWAP_XY) ? this->height_ : this->width_; }
|
||||
int get_height() override { return (this->effective_transform_ & TRANSFORM_SWAP_XY) ? this->width_ : this->height_; }
|
||||
bool pixel_axes_swapped() const override { return this->effective_transform_ & TRANSFORM_SWAP_XY; }
|
||||
|
||||
protected:
|
||||
int get_height_internal() override { return this->height_; }
|
||||
|
||||
@@ -130,19 +130,8 @@ class MipiSpi : public display::Display,
|
||||
}
|
||||
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
|
||||
|
||||
int get_width() override {
|
||||
if (this->rotation_ == display::DISPLAY_ROTATION_90_DEGREES ||
|
||||
this->rotation_ == display::DISPLAY_ROTATION_270_DEGREES)
|
||||
return HEIGHT;
|
||||
return WIDTH;
|
||||
}
|
||||
|
||||
int get_height() override {
|
||||
if (this->rotation_ == display::DISPLAY_ROTATION_90_DEGREES ||
|
||||
this->rotation_ == display::DISPLAY_ROTATION_270_DEGREES)
|
||||
return WIDTH;
|
||||
return HEIGHT;
|
||||
}
|
||||
int get_width() override { return display::rotation_swaps_axes(this->rotation_) ? HEIGHT : WIDTH; }
|
||||
int get_height() override { return display::rotation_swaps_axes(this->rotation_) ? WIDTH : HEIGHT; }
|
||||
|
||||
void set_init_sequence(const uint8_t *sequence, size_t len) {
|
||||
this->init_sequence_ = sequence;
|
||||
@@ -621,6 +610,9 @@ class MipiSpiBuffer
|
||||
#endif
|
||||
}
|
||||
|
||||
// With hardware rotation the buffer is written in drawn coordinates.
|
||||
bool pixel_axes_swapped() const override { return !HAS_HARDWARE_ROTATION && display::Display::pixel_axes_swapped(); }
|
||||
|
||||
// Draw a pixel at the given coordinates.
|
||||
void draw_pixel_at(int x, int y, Color color) override {
|
||||
if (this->is_point_clipped(x, y))
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
"""TMP102 threshold status entity."""
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import text_sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import ENTITY_CATEGORY_DIAGNOSTIC
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import CONF_TMP102_ID, TMP102Component, validate_unique_child
|
||||
|
||||
CONFIG_SCHEMA = text_sensor.text_sensor_schema(
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC
|
||||
).extend(
|
||||
{
|
||||
cv.GenerateID(CONF_TMP102_ID): cv.use_id(TMP102Component),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _final_validate(config: ConfigType) -> ConfigType:
|
||||
return validate_unique_child(config, "text_sensor")
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
cg.add_define("USE_TMP102_TEXT_SENSOR")
|
||||
parent = await cg.get_variable(config[CONF_TMP102_ID])
|
||||
var = await text_sensor.new_text_sensor(config)
|
||||
cg.add(parent.set_threshold_status_text_sensor(var))
|
||||
@@ -1,8 +1,14 @@
|
||||
#include "tmp102.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#ifdef USE_TMP102_TEXT_SENSOR
|
||||
#include "esphome/core/progmem.h"
|
||||
#endif
|
||||
|
||||
#include <cmath>
|
||||
#ifdef USE_TMP102_TEXT_SENSOR
|
||||
#include <cstdio>
|
||||
#endif
|
||||
|
||||
namespace esphome::tmp102 {
|
||||
|
||||
@@ -12,6 +18,30 @@ 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;
|
||||
|
||||
enum TMP102ThresholdStatus : uint8_t {
|
||||
TMP102_THRESHOLD_STATUS_NOT_CONFIGURED,
|
||||
TMP102_THRESHOLD_STATUS_SETUP_FAILED,
|
||||
TMP102_THRESHOLD_STATUS_OK,
|
||||
TMP102_THRESHOLD_STATUS_RESTORE_RESET,
|
||||
TMP102_THRESHOLD_STATUS_RESTORE_RESET_SAVE_FAILED,
|
||||
TMP102_THRESHOLD_STATUS_PREFERENCE_SAVE_FAILED,
|
||||
TMP102_THRESHOLD_STATUS_RECOVERY_FAILED,
|
||||
TMP102_THRESHOLD_STATUS_RECOVERED,
|
||||
TMP102_THRESHOLD_STATUS_WRITE_HIGH_FAILED,
|
||||
TMP102_THRESHOLD_STATUS_WRITE_LOW_FAILED,
|
||||
TMP102_THRESHOLD_STATUS_SENSOR_SETUP_FAILED,
|
||||
TMP102_THRESHOLD_STATUS_COUNT,
|
||||
};
|
||||
|
||||
#ifdef USE_TMP102_TEXT_SENSOR
|
||||
PROGMEM_STRING_TABLE(ThresholdStatusStrings, "Not configured", "Setup failed", "OK", "Restored thresholds reset",
|
||||
"Restore reset; save failed", "Preference save failed", "Configuration recovery failed",
|
||||
"Configuration restored", "Write failed: high threshold", "Write failed: low threshold",
|
||||
"Rejected: sensor setup failed");
|
||||
static_assert(ThresholdStatusStrings::COUNT == TMP102_THRESHOLD_STATUS_COUNT);
|
||||
#endif
|
||||
|
||||
#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;
|
||||
@@ -42,15 +72,30 @@ static uint16_t encode_temperature(float temperature, bool extended) {
|
||||
return static_cast<uint16_t>(static_cast<uint32_t>(static_cast<uint16_t>(steps)) << (extended ? 3 : 4));
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if defined(USE_TMP102_CONFIGURE) || defined(USE_TMP102_TEXT_SENSOR)
|
||||
void TMP102Component::setup() {
|
||||
if (!this->configure_)
|
||||
#ifdef USE_TMP102_CONFIGURE
|
||||
if (this->configure_) {
|
||||
this->setup_configured_();
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
this->publish_threshold_status_(TMP102_THRESHOLD_STATUS_NOT_CONFIGURED);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_TMP102_CONFIGURE
|
||||
void TMP102Component::setup_configured_() {
|
||||
bool corrected = false;
|
||||
#ifdef USE_TMP102_NUMBER
|
||||
float restored[2]{};
|
||||
this->prepare_number_limits_(restored);
|
||||
bool restore_corrected[2]{};
|
||||
corrected = this->prepare_number_limits_(restored, restore_corrected);
|
||||
#endif
|
||||
if (!this->write_configuration_()) {
|
||||
this->publish_threshold_status_(TMP102_THRESHOLD_STATUS_SETUP_FAILED);
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
@@ -61,14 +106,17 @@ void TMP102Component::setup() {
|
||||
if (control == nullptr)
|
||||
continue;
|
||||
const float value = this->get_limit_temperature_(static_cast<TMP102LimitType>(limit));
|
||||
if (value != restored[limit])
|
||||
if (restore_corrected[limit] || value != restored[limit])
|
||||
saved &= control->save_value_(value);
|
||||
control->publish_state(value);
|
||||
}
|
||||
if (!saved) {
|
||||
ESP_LOGW(TAG, "Failed to save threshold preferences");
|
||||
this->publish_threshold_status_(TMP102_THRESHOLD_STATUS_RESTORE_RESET_SAVE_FAILED);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
this->publish_threshold_status_(corrected ? TMP102_THRESHOLD_STATUS_RESTORE_RESET : TMP102_THRESHOLD_STATUS_OK);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -83,6 +131,9 @@ void TMP102Component::dump_config() {
|
||||
#ifdef USE_TMP102_BINARY_SENSOR
|
||||
LOG_BINARY_SENSOR(" ", "Alert", this->alert_binary_sensor_);
|
||||
#endif
|
||||
#ifdef USE_TMP102_TEXT_SENSOR
|
||||
LOG_TEXT_SENSOR(" ", "Threshold Status", this->threshold_status_text_sensor_);
|
||||
#endif
|
||||
#ifdef USE_TMP102_CONFIGURE
|
||||
if (!this->configure_)
|
||||
return;
|
||||
@@ -203,6 +254,9 @@ bool TMP102Component::check_configuration_() {
|
||||
this->set_warning_and_invalidate_alert_();
|
||||
if (!this->write_configuration_()) {
|
||||
ESP_LOGW(TAG, "Failed to restore configuration and thresholds");
|
||||
this->publish_threshold_status_(TMP102_THRESHOLD_STATUS_RECOVERY_FAILED);
|
||||
} else {
|
||||
this->publish_threshold_status_(TMP102_THRESHOLD_STATUS_RECOVERED);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -228,22 +282,25 @@ void TMP102Component::set_limit_control(TMP102LimitNumber *number) {
|
||||
this->limit_controls_[number->limit_type_] = number;
|
||||
}
|
||||
|
||||
void TMP102Component::prepare_number_limits_(float *restored) {
|
||||
bool TMP102Component::prepare_number_limits_(float *restored, bool *restore_corrected) {
|
||||
const bool extended = this->config_ & TMP102_EXTENDED_MODE_BIT;
|
||||
uint16_t raw[2] = {this->low_limit_, this->high_limit_};
|
||||
bool corrected = false;
|
||||
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));
|
||||
restored[limit] = control->load_value_(decode_temperature(raw[limit], extended), restore_corrected[limit]);
|
||||
corrected |= restore_corrected[limit];
|
||||
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;
|
||||
return true;
|
||||
}
|
||||
this->low_limit_ = raw[TMP102_LIMIT_LOW];
|
||||
this->high_limit_ = raw[TMP102_LIMIT_HIGH];
|
||||
return corrected;
|
||||
}
|
||||
|
||||
float TMP102Component::get_limit_temperature_(TMP102LimitType limit) const {
|
||||
@@ -261,10 +318,13 @@ bool TMP102Component::set_limit_temperature_(TMP102LimitType limit, float temper
|
||||
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));
|
||||
this->publish_threshold_rejection_(high, decode_temperature(raw, extended), decode_temperature(other, extended));
|
||||
return false;
|
||||
}
|
||||
if (!this->write_register_(static_cast<uint8_t>(TMP102_REGISTER_LOW_LIMIT + limit), raw)) {
|
||||
this->status_set_warning();
|
||||
this->publish_threshold_status_(high ? TMP102_THRESHOLD_STATUS_WRITE_HIGH_FAILED
|
||||
: TMP102_THRESHOLD_STATUS_WRITE_LOW_FAILED);
|
||||
return false;
|
||||
}
|
||||
(high ? this->high_limit_ : this->low_limit_) = raw;
|
||||
@@ -273,6 +333,32 @@ bool TMP102Component::set_limit_temperature_(TMP102LimitType limit, float temper
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef USE_TMP102_TEXT_SENSOR
|
||||
void TMP102Component::publish_threshold_status_(uint8_t status) {
|
||||
if (this->threshold_status_text_sensor_ == nullptr)
|
||||
return;
|
||||
char text[40];
|
||||
ESPHOME_strncpy_P(
|
||||
text,
|
||||
reinterpret_cast<ESPHOME_PGM_P>(ThresholdStatusStrings::get_progmem_str(status, TMP102_THRESHOLD_STATUS_OK)),
|
||||
sizeof(text));
|
||||
text[sizeof(text) - 1] = '\0';
|
||||
this->threshold_status_text_sensor_->publish_state(text);
|
||||
}
|
||||
|
||||
void TMP102Component::publish_threshold_rejection_(bool high, float value, float other) {
|
||||
if (this->threshold_status_text_sensor_ == nullptr)
|
||||
return;
|
||||
char text[96];
|
||||
if (high) {
|
||||
ESPHOME_snprintf_P(text, sizeof(text), ESPHOME_PSTR("Rejected: high %.4f°C crosses low %.4f°C"), value, other);
|
||||
} else {
|
||||
ESPHOME_snprintf_P(text, sizeof(text), ESPHOME_PSTR("Rejected: low %.4f°C crosses high %.4f°C"), value, other);
|
||||
}
|
||||
this->threshold_status_text_sensor_->publish_state(text);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_TMP102_BINARY_SENSOR
|
||||
void TMP102Component::invalidate_alert_() {
|
||||
if (this->alert_binary_sensor_ != nullptr)
|
||||
@@ -294,7 +380,7 @@ bool TMP102Component::read_alert_state_() {
|
||||
#endif
|
||||
|
||||
#ifdef USE_TMP102_NUMBER
|
||||
float TMP102LimitNumber::load_value_(float fallback) {
|
||||
float TMP102LimitNumber::load_value_(float fallback, bool &corrected) {
|
||||
float value = fallback;
|
||||
if (this->restore_value_) {
|
||||
this->pref_ = this->make_entity_preference<float>();
|
||||
@@ -303,6 +389,7 @@ float TMP102LimitNumber::load_value_(float fallback) {
|
||||
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;
|
||||
corrected = true;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
@@ -313,6 +400,7 @@ 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");
|
||||
this->parent_->publish_threshold_status_(TMP102_THRESHOLD_STATUS_SENSOR_SETUP_FAILED);
|
||||
return;
|
||||
}
|
||||
const bool accepted = this->parent_->set_limit_temperature_(this->limit_type_, value);
|
||||
@@ -320,6 +408,9 @@ void TMP102LimitNumber::control(float value) {
|
||||
this->publish_state(value);
|
||||
if (accepted && !this->save_value_(value)) {
|
||||
ESP_LOGW(TAG, "Failed to save threshold preference");
|
||||
this->parent_->publish_threshold_status_(TMP102_THRESHOLD_STATUS_PREFERENCE_SAVE_FAILED);
|
||||
} else if (accepted) {
|
||||
this->parent_->publish_threshold_status_(TMP102_THRESHOLD_STATUS_OK);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -12,11 +12,20 @@
|
||||
#include "esphome/core/preferences.h"
|
||||
#endif
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#ifdef USE_TMP102_TEXT_SENSOR
|
||||
#include "esphome/components/text_sensor/text_sensor.h"
|
||||
#endif
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace esphome::tmp102 {
|
||||
|
||||
#ifdef USE_HOST
|
||||
namespace testing {
|
||||
class TMP102Test;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_TMP102_NUMBER
|
||||
enum TMP102LimitType : uint8_t {
|
||||
TMP102_LIMIT_LOW = 0,
|
||||
@@ -31,8 +40,13 @@ class TMP102Component final : public PollingComponent, public i2c::I2CDevice, pu
|
||||
#ifdef USE_TMP102_BINARY_SENSOR
|
||||
SUB_BINARY_SENSOR(alert)
|
||||
#endif
|
||||
#ifdef USE_TMP102_CONFIGURE
|
||||
#ifdef USE_TMP102_TEXT_SENSOR
|
||||
SUB_TEXT_SENSOR(threshold_status)
|
||||
#endif
|
||||
#if defined(USE_TMP102_CONFIGURE) || defined(USE_TMP102_TEXT_SENSOR)
|
||||
void setup() override;
|
||||
#endif
|
||||
#ifdef USE_TMP102_CONFIGURE
|
||||
void set_configuration(uint16_t config, uint16_t high_limit, uint16_t low_limit) {
|
||||
this->config_ = config;
|
||||
this->high_limit_ = high_limit;
|
||||
@@ -47,6 +61,13 @@ class TMP102Component final : public PollingComponent, public i2c::I2CDevice, pu
|
||||
void update() override;
|
||||
|
||||
protected:
|
||||
#ifdef USE_TMP102_TEXT_SENSOR
|
||||
void publish_threshold_status_(uint8_t status);
|
||||
void publish_threshold_rejection_(bool high, float value, float other);
|
||||
#else
|
||||
void publish_threshold_status_(uint8_t) {}
|
||||
void publish_threshold_rejection_(bool, float, float) {}
|
||||
#endif
|
||||
void set_warning_and_invalidate_alert_() {
|
||||
this->status_set_warning();
|
||||
#ifdef USE_TMP102_BINARY_SENSOR
|
||||
@@ -58,6 +79,7 @@ class TMP102Component final : public PollingComponent, public i2c::I2CDevice, pu
|
||||
void invalidate_alert_();
|
||||
#endif
|
||||
#ifdef USE_TMP102_CONFIGURE
|
||||
void setup_configured_();
|
||||
bool write_register_(uint8_t reg, uint16_t value);
|
||||
bool write_configuration_();
|
||||
bool check_configuration_();
|
||||
@@ -67,11 +89,10 @@ class TMP102Component final : public PollingComponent, public i2c::I2CDevice, pu
|
||||
friend class TMP102LimitNumber;
|
||||
bool set_limit_temperature_(TMP102LimitType limit, float temperature);
|
||||
float get_limit_temperature_(TMP102LimitType limit) const;
|
||||
void prepare_number_limits_(float *restored);
|
||||
bool prepare_number_limits_(float *restored, bool *restore_corrected);
|
||||
|
||||
TMP102LimitNumber *limit_controls_[2]{};
|
||||
#endif
|
||||
|
||||
uint16_t config_{0};
|
||||
uint16_t high_limit_{0};
|
||||
uint16_t low_limit_{0};
|
||||
@@ -87,7 +108,10 @@ class TMP102LimitNumber final : public number::Number, public Parented<TMP102Com
|
||||
|
||||
protected:
|
||||
friend class TMP102Component;
|
||||
float load_value_(float fallback);
|
||||
#ifdef USE_HOST
|
||||
friend class testing::TMP102Test;
|
||||
#endif
|
||||
float load_value_(float fallback, bool &corrected);
|
||||
bool save_value_(float value);
|
||||
void control(float value) override;
|
||||
|
||||
|
||||
@@ -272,6 +272,7 @@
|
||||
#define USE_TMP102_BINARY_SENSOR
|
||||
#define USE_TMP102_CONFIGURE
|
||||
#define USE_TMP102_NUMBER
|
||||
#define USE_TMP102_TEXT_SENSOR
|
||||
#define USE_TOUCHSCREEN
|
||||
#define USE_UART_DEBUGGER
|
||||
#define USE_UART_WAKE_LOOP_ON_RX
|
||||
|
||||
+28
-7
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
@@ -68,6 +69,7 @@ template<size_t N> struct FixedString {
|
||||
};
|
||||
|
||||
/// Compile-time string table that packs strings into a single blob with offset lookup.
|
||||
/// Blobs over 255 bytes have no offsets and are scanned, so keep those for rarely read strings.
|
||||
/// Use PROGMEM_STRING_TABLE macro to instantiate with proper flash placement on ESP8266.
|
||||
///
|
||||
/// Example:
|
||||
@@ -78,6 +80,11 @@ template<size_t N> struct FixedString {
|
||||
template<FixedString... Strs> struct ProgmemStringTable {
|
||||
static constexpr size_t COUNT = sizeof...(Strs);
|
||||
static constexpr size_t BLOB_SIZE = (0 + ... + (Strs.size() + 1));
|
||||
/// Length of the longest string, for sizing a RAM copy on ESP8266
|
||||
static constexpr size_t MAX_LENGTH = std::max({Strs.size()...});
|
||||
/// One-byte offsets address up to 255 bytes; a larger blob has no offset table and is scanned instead,
|
||||
/// which costs less flash than wider offsets for the rarely read tables that get this big.
|
||||
static constexpr bool INDEXED = BLOB_SIZE <= UINT8_MAX;
|
||||
|
||||
/// Generate packed string blob at compile time
|
||||
static constexpr auto make_blob() {
|
||||
@@ -91,18 +98,28 @@ template<FixedString... Strs> struct ProgmemStringTable {
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Generate offset table at compile time (uint8_t limits blob to 255 bytes)
|
||||
/// Generate offset table at compile time
|
||||
static constexpr auto make_offsets() {
|
||||
static_assert(COUNT > 0, "PROGMEM_STRING_TABLE must contain at least one string");
|
||||
static_assert(COUNT <= 255, "PROGMEM_STRING_TABLE supports at most 255 strings with uint8_t indices");
|
||||
static_assert(BLOB_SIZE <= 255, "PROGMEM_STRING_TABLE blob exceeds 255 bytes; use fewer/shorter strings");
|
||||
std::array<uint8_t, COUNT> result{};
|
||||
size_t pos = 0, idx = 0;
|
||||
((result[idx++] = static_cast<uint8_t>(pos), pos += Strs.size() + 1), ...);
|
||||
return result;
|
||||
if constexpr (INDEXED) {
|
||||
std::array<uint8_t, COUNT> result{};
|
||||
size_t pos = 0, idx = 0;
|
||||
((result[idx++] = static_cast<uint8_t>(pos), pos += Strs.size() + 1), ...);
|
||||
return result;
|
||||
} else {
|
||||
return std::array<uint8_t, 0>{};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// Skip idx strings in a packed NUL-separated blob that may live in flash.
|
||||
inline const char *progmem_skip_strings(const char *blob, uint8_t idx) {
|
||||
while (idx-- > 0)
|
||||
blob += ESPHOME_strlen_P(blob) + 1;
|
||||
return blob;
|
||||
}
|
||||
|
||||
// Forward declaration for LogString (defined in log.h)
|
||||
struct LogString;
|
||||
|
||||
@@ -115,6 +132,7 @@ struct LogString;
|
||||
static constexpr size_t COUNT = Table::COUNT; \
|
||||
static constexpr uint8_t LAST_INDEX = COUNT - 1; \
|
||||
static constexpr size_t BLOB_SIZE = Table::BLOB_SIZE; \
|
||||
static constexpr size_t MAX_LENGTH = Table::MAX_LENGTH; \
|
||||
static constexpr auto BLOB ESPHOME_PROGMEM_STRING_TABLE_STORAGE = Table::make_blob(); \
|
||||
static constexpr auto OFFSETS ESPHOME_PROGMEM_STRING_TABLE_STORAGE = Table::make_offsets(); \
|
||||
static const char *get_(uint8_t idx, uint8_t fallback) { \
|
||||
@@ -122,7 +140,10 @@ struct LogString;
|
||||
idx = fallback; \
|
||||
/* std::launder is used here to prevent the inter-procedural analysis that */ \
|
||||
/* causes the false positive that the string is not null terminated */ \
|
||||
return std::launder(&BLOB[::esphome::progmem_read_byte(&OFFSETS[idx])]); \
|
||||
if constexpr (Table::INDEXED) \
|
||||
return std::launder(&BLOB[::esphome::progmem_read_byte(&OFFSETS[idx])]); \
|
||||
else \
|
||||
return std::launder(::esphome::progmem_skip_strings(BLOB.data(), idx)); \
|
||||
} \
|
||||
static ::ProgmemStr get_progmem_str(uint8_t idx, uint8_t fallback) { \
|
||||
return reinterpret_cast<::ProgmemStr>(get_(idx, fallback)); \
|
||||
|
||||
+10
-4
@@ -1120,8 +1120,12 @@ def _load_yaml_internal(fname: Path, *, track_document_range: bool = True) -> An
|
||||
res = parse_yaml(fname, f_handle, track_document_range=track_document_range)
|
||||
except (UnicodeDecodeError, OSError) as err:
|
||||
raise EsphomeError(f"Error reading file {fname}: {err}") from err
|
||||
# Top-level !include returns a deferred IncludeFile; resolve it so
|
||||
# callers always receive the final content.
|
||||
return res
|
||||
|
||||
|
||||
def _resolve_include(res: Any) -> Any:
|
||||
# A document that is just `!include` parses to a deferred IncludeFile; resolve it so
|
||||
# every parse_yaml caller, including the VS Code and ACE loaders, gets the final content.
|
||||
if isinstance(res, IncludeFile):
|
||||
res = res.load() if res.should_load() else {}
|
||||
return res
|
||||
@@ -1147,7 +1151,7 @@ def parse_yaml(
|
||||
# tracked mode; reject the combination instead of half-applying it.
|
||||
raise ValueError("track_document_range=False requires the default yaml_loader")
|
||||
try:
|
||||
return _load_yaml_internal_with_type(
|
||||
res = _load_yaml_internal_with_type(
|
||||
ESPHomeLoader,
|
||||
file_name,
|
||||
file_handle,
|
||||
@@ -1159,13 +1163,15 @@ def parse_yaml(
|
||||
# readable exceptions
|
||||
# Rewind the stream so we can try again
|
||||
file_handle.seek(0, 0)
|
||||
return _load_yaml_internal_with_type(
|
||||
res = _load_yaml_internal_with_type(
|
||||
ESPHomePurePythonLoader,
|
||||
file_name,
|
||||
file_handle,
|
||||
yaml_loader,
|
||||
track_document_range=track_document_range,
|
||||
)
|
||||
# Resolved after the fallback so a failed first parse never loads the include twice
|
||||
return _resolve_include(res)
|
||||
|
||||
|
||||
def _load_yaml_internal_with_type(
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
esphome:
|
||||
name: tmp102-test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
i2c:
|
||||
sda: GPIO21
|
||||
scl: GPIO22
|
||||
|
||||
sensor:
|
||||
- platform: tmp102
|
||||
id: parent_sensor
|
||||
name: Temperature
|
||||
|
||||
text_sensor:
|
||||
- platform: tmp102
|
||||
tmp102_id: parent_sensor
|
||||
name: First Status
|
||||
- platform: tmp102
|
||||
tmp102_id: parent_sensor
|
||||
name: Second Status
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
esphome:
|
||||
name: tmp102-test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
i2c:
|
||||
sda: GPIO21
|
||||
scl: GPIO22
|
||||
|
||||
sensor:
|
||||
- platform: tmp102
|
||||
id: parent_sensor
|
||||
name: Temperature
|
||||
|
||||
text_sensor:
|
||||
- platform: tmp102
|
||||
tmp102_id: parent_sensor
|
||||
name: Threshold Status
|
||||
@@ -58,6 +58,30 @@ def test_duplicate_alert_entities_are_rejected(
|
||||
)
|
||||
|
||||
|
||||
def test_status_entity_is_read_only(
|
||||
generate_main: Callable[[Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
source = generate_main(component_config_path("status.yaml"))
|
||||
assert "set_threshold_status_text_sensor" in source
|
||||
assert "set_configuration" not in source
|
||||
defines = " ".join(define.name for define in CORE.defines)
|
||||
assert "USE_TMP102_TEXT_SENSOR" in defines
|
||||
assert "USE_TMP102_CONFIGURE" not in defines
|
||||
|
||||
|
||||
def test_duplicate_status_entities_are_rejected(
|
||||
component_config_path: Callable[[str], Path],
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
) -> None:
|
||||
CORE.config_path = component_config_path("duplicate_status.yaml")
|
||||
assert read_config({}) is None
|
||||
assert (
|
||||
"Only one TMP102 text_sensor may reference each temperature sensor"
|
||||
in capsys.readouterr().out
|
||||
)
|
||||
|
||||
|
||||
def test_threshold_numbers_enable_configuration_once(
|
||||
generate_main: Callable[[Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
namespace esphome::core::testing {
|
||||
|
||||
PROGMEM_STRING_TABLE(SmallStrings, "OFF", "HEAT", "COOL", "UNKNOWN");
|
||||
|
||||
// Eight strings of 40 characters: a 328-byte blob, past what one-byte offsets can address, so it is scanned.
|
||||
PROGMEM_STRING_TABLE(LargeStrings, "0123456789012345678901234567890123456789",
|
||||
"1123456789012345678901234567890123456789", "2123456789012345678901234567890123456789",
|
||||
"3123456789012345678901234567890123456789", "4123456789012345678901234567890123456789",
|
||||
"5123456789012345678901234567890123456789", "6123456789012345678901234567890123456789",
|
||||
"7123456789012345678901234567890123456789");
|
||||
|
||||
// Five 50-character strings pack into exactly 255 bytes; one more byte switches to scanning.
|
||||
PROGMEM_STRING_TABLE(Exactly255, "01234567890123456789012345678901234567890123456789",
|
||||
"01234567890123456789012345678901234567890123456789",
|
||||
"01234567890123456789012345678901234567890123456789",
|
||||
"01234567890123456789012345678901234567890123456789",
|
||||
"01234567890123456789012345678901234567890123456789");
|
||||
PROGMEM_STRING_TABLE(Exactly256, "01234567890123456789012345678901234567890123456789",
|
||||
"01234567890123456789012345678901234567890123456789",
|
||||
"01234567890123456789012345678901234567890123456789",
|
||||
"01234567890123456789012345678901234567890123456789",
|
||||
"01234567890123456789012345678901234567890123456789x");
|
||||
|
||||
TEST(ProgmemStringTable, ScansInsteadOfIndexingAfter255Bytes) {
|
||||
static_assert(Exactly255::BLOB_SIZE == 255);
|
||||
static_assert(Exactly255::Table::INDEXED);
|
||||
static_assert(Exactly256::BLOB_SIZE == 256);
|
||||
static_assert(!Exactly256::Table::INDEXED);
|
||||
static_assert(Exactly256::OFFSETS.empty());
|
||||
EXPECT_EQ(Exactly255::OFFSETS[4], 204);
|
||||
EXPECT_STREQ(reinterpret_cast<const char *>(Exactly256::get_progmem_str(4, 0)),
|
||||
"01234567890123456789012345678901234567890123456789x");
|
||||
}
|
||||
|
||||
TEST(ProgmemStringTable, SmallTableKeepsOneByteOffsets) {
|
||||
static_assert(SmallStrings::BLOB_SIZE <= 255);
|
||||
static_assert(SmallStrings::Table::INDEXED);
|
||||
static_assert(SmallStrings::MAX_LENGTH == 7);
|
||||
EXPECT_STREQ(reinterpret_cast<const char *>(SmallStrings::get_log_str(0, SmallStrings::LAST_INDEX)), "OFF");
|
||||
EXPECT_STREQ(reinterpret_cast<const char *>(SmallStrings::get_log_str(2, SmallStrings::LAST_INDEX)), "COOL");
|
||||
EXPECT_STREQ(reinterpret_cast<const char *>(SmallStrings::get_log_str(200, SmallStrings::LAST_INDEX)), "UNKNOWN");
|
||||
}
|
||||
|
||||
TEST(ProgmemStringTable, LargeTableIsScanned) {
|
||||
static_assert(LargeStrings::BLOB_SIZE > 255);
|
||||
static_assert(!LargeStrings::Table::INDEXED);
|
||||
static_assert(LargeStrings::MAX_LENGTH == 40);
|
||||
EXPECT_STREQ(reinterpret_cast<const char *>(LargeStrings::get_progmem_str(0, LargeStrings::LAST_INDEX)),
|
||||
"0123456789012345678901234567890123456789");
|
||||
EXPECT_STREQ(reinterpret_cast<const char *>(LargeStrings::get_progmem_str(7, LargeStrings::LAST_INDEX)),
|
||||
"7123456789012345678901234567890123456789");
|
||||
EXPECT_STREQ(reinterpret_cast<const char *>(LargeStrings::get_progmem_str(8, LargeStrings::LAST_INDEX)),
|
||||
"7123456789012345678901234567890123456789");
|
||||
EXPECT_STREQ(reinterpret_cast<const char *>(LargeStrings::get_progmem_str(3, LargeStrings::LAST_INDEX)),
|
||||
"3123456789012345678901234567890123456789");
|
||||
}
|
||||
|
||||
} // namespace esphome::core::testing
|
||||
@@ -0,0 +1,20 @@
|
||||
from pathlib import Path
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.const import CONF_ESPHOME, CONF_INCLUDES
|
||||
from esphome.core import CORE
|
||||
from esphome.types import ConfigType
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
TESTS_DIR = Path(__file__).parent
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
# The column walk in Display::draw_pixels_at() is only built with PSRAM; define it
|
||||
# when display's own tests are built so both walk orders run, and leave other
|
||||
# components' test binaries on the plain build.
|
||||
async def to_code_testing(config: ConfigType) -> None:
|
||||
if any(Path(p) == TESTS_DIR for p in CORE.config[CONF_ESPHOME][CONF_INCLUDES]):
|
||||
cg.add_define("USE_PSRAM")
|
||||
|
||||
manifest.to_code = to_code_testing
|
||||
@@ -0,0 +1,109 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <initializer_list>
|
||||
#include <tuple>
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/components/display/display.h"
|
||||
|
||||
namespace esphome::display::testing {
|
||||
|
||||
// Records every pixel write; pixel_axes_swapped() picks the walk order.
|
||||
class RecordingDisplay : public Display {
|
||||
public:
|
||||
void set_axes_swapped(bool swapped) { this->axes_swapped_ = swapped; }
|
||||
bool pixel_axes_swapped() const override { return this->axes_swapped_; }
|
||||
void draw_pixel_at(int x, int y, Color color) override { this->pixels_.emplace_back(x, y, color.raw_32); }
|
||||
void update() override {}
|
||||
DisplayType get_display_type() override { return DisplayType::DISPLAY_TYPE_COLOR; }
|
||||
int get_width_internal() override { return 64; }
|
||||
int get_height_internal() override { return 64; }
|
||||
|
||||
std::vector<std::tuple<int, int, uint32_t>> sorted_pixels() const {
|
||||
auto sorted = this->pixels_;
|
||||
std::sort(sorted.begin(), sorted.end());
|
||||
return sorted;
|
||||
}
|
||||
size_t count() const { return this->pixels_.size(); }
|
||||
const std::vector<std::tuple<int, int, uint32_t>> &pixels() const { return this->pixels_; }
|
||||
|
||||
protected:
|
||||
bool axes_swapped_{false};
|
||||
std::vector<std::tuple<int, int, uint32_t>> pixels_;
|
||||
};
|
||||
|
||||
// A 7x5 region with a 2 pixel left offset, a 3 line top offset and 4 pixels of
|
||||
// right padding, so the source index arithmetic of both walks is exercised.
|
||||
static constexpr int W = 7, H = 5, X_OFFSET = 2, Y_OFFSET = 3, X_PAD = 4;
|
||||
static constexpr int LINE_STRIDE = X_OFFSET + W + X_PAD;
|
||||
static constexpr int LINES = Y_OFFSET + H;
|
||||
|
||||
static std::vector<uint8_t> make_source(int bytes_per_pixel) {
|
||||
std::vector<uint8_t> source(LINE_STRIDE * LINES * bytes_per_pixel);
|
||||
uint8_t value = 1;
|
||||
for (auto &byte : source)
|
||||
byte = value++;
|
||||
return source;
|
||||
}
|
||||
|
||||
static void expect_walks_match(ColorBitness bitness, int bytes_per_pixel, bool big_endian) {
|
||||
const auto source = make_source(bytes_per_pixel);
|
||||
RecordingDisplay rows;
|
||||
RecordingDisplay columns;
|
||||
columns.set_axes_swapped(true);
|
||||
rows.draw_pixels_at(10, 20, W, H, source.data(), COLOR_ORDER_RGB, bitness, big_endian, X_OFFSET, Y_OFFSET, X_PAD);
|
||||
columns.draw_pixels_at(10, 20, W, H, source.data(), COLOR_ORDER_RGB, bitness, big_endian, X_OFFSET, Y_OFFSET, X_PAD);
|
||||
EXPECT_EQ(rows.count(), static_cast<size_t>(W * H));
|
||||
EXPECT_EQ(columns.count(), static_cast<size_t>(W * H));
|
||||
EXPECT_EQ(rows.sorted_pixels(), columns.sorted_pixels());
|
||||
}
|
||||
|
||||
TEST(DrawPixelsAt, ColumnWalkMatchesRowWalk332) { expect_walks_match(COLOR_BITNESS_332, 1, true); }
|
||||
TEST(DrawPixelsAt, ColumnWalkMatchesRowWalk565BigEndian) { expect_walks_match(COLOR_BITNESS_565, 2, true); }
|
||||
TEST(DrawPixelsAt, ColumnWalkMatchesRowWalk565LittleEndian) { expect_walks_match(COLOR_BITNESS_565, 2, false); }
|
||||
TEST(DrawPixelsAt, ColumnWalkMatchesRowWalk888BigEndian) { expect_walks_match(COLOR_BITNESS_888, 3, true); }
|
||||
TEST(DrawPixelsAt, ColumnWalkMatchesRowWalk888LittleEndian) { expect_walks_match(COLOR_BITNESS_888, 3, false); }
|
||||
|
||||
// One known pixel per bitness and byte order, so a decode that swapped bytes or channels
|
||||
// could not pass the walk comparison above by being wrong the same way twice.
|
||||
static void expect_golden(ColorBitness bitness, std::initializer_list<uint8_t> bytes, bool big_endian, Color expected) {
|
||||
const std::vector<uint8_t> source(bytes);
|
||||
RecordingDisplay display;
|
||||
display.draw_pixels_at(3, 4, 1, 1, source.data(), COLOR_ORDER_RGB, bitness, big_endian, 0, 0, 0);
|
||||
ASSERT_EQ(display.count(), 1u);
|
||||
EXPECT_EQ(display.pixels()[0], std::make_tuple(3, 4, expected.raw_32));
|
||||
}
|
||||
|
||||
TEST(DrawPixelsAt, Golden565BigEndian) {
|
||||
expect_golden(COLOR_BITNESS_565, {0xE0, 0x07}, true, Color(230, 0, 57)); // 0xE007: r 28, g 0, b 7
|
||||
}
|
||||
TEST(DrawPixelsAt, Golden565LittleEndian) {
|
||||
expect_golden(COLOR_BITNESS_565, {0xE0, 0x07}, false, Color(0, 255, 0)); // 0x07E0: pure green
|
||||
}
|
||||
TEST(DrawPixelsAt, Golden888BigEndian) {
|
||||
expect_golden(COLOR_BITNESS_888, {0x10, 0x20, 0x30}, true, Color(0x10, 0x20, 0x30));
|
||||
}
|
||||
TEST(DrawPixelsAt, Golden888LittleEndian) {
|
||||
expect_golden(COLOR_BITNESS_888, {0x10, 0x20, 0x30}, false, Color(0x30, 0x20, 0x10));
|
||||
}
|
||||
TEST(DrawPixelsAt, Golden332) {
|
||||
expect_golden(COLOR_BITNESS_332, {0xE3}, true, Color(255, 0, 255)); // r 7 of 7, g 0, b 3 of 3
|
||||
}
|
||||
|
||||
// The column walk really is a different order, otherwise the tests above prove nothing.
|
||||
TEST(DrawPixelsAt, ColumnWalkChangesOrder) {
|
||||
const auto source = make_source(1);
|
||||
RecordingDisplay rows;
|
||||
RecordingDisplay columns;
|
||||
columns.set_axes_swapped(true);
|
||||
rows.draw_pixels_at(0, 0, W, H, source.data(), COLOR_ORDER_RGB, COLOR_BITNESS_332, true, X_OFFSET, Y_OFFSET, X_PAD);
|
||||
columns.draw_pixels_at(0, 0, W, H, source.data(), COLOR_ORDER_RGB, COLOR_BITNESS_332, true, X_OFFSET, Y_OFFSET,
|
||||
X_PAD);
|
||||
ASSERT_GT(rows.count(), 1u);
|
||||
ASSERT_GT(columns.count(), 1u);
|
||||
EXPECT_EQ(std::get<0>(rows.pixels()[1]), 1); // the row walk steps x first
|
||||
EXPECT_EQ(std::get<1>(columns.pixels()[1]), 1); // the column walk steps y first
|
||||
}
|
||||
|
||||
} // namespace esphome::display::testing
|
||||
@@ -18,7 +18,7 @@ canbus:
|
||||
tx_pin: ${tx_pin}
|
||||
can_id: 4
|
||||
bit_rate: 50kbps
|
||||
mode: NORMAL
|
||||
mode: NO_ACK
|
||||
on_frame:
|
||||
- can_id: 500
|
||||
then:
|
||||
|
||||
@@ -6,11 +6,12 @@ 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", "number"):
|
||||
for platform in ("sensor", "binary_sensor", "number", "text_sensor"):
|
||||
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")
|
||||
cg.add_define("USE_TMP102_TEXT_SENSOR")
|
||||
|
||||
manifest.to_code = to_code_testing
|
||||
manifest.dependencies = manifest.dependencies + [
|
||||
@@ -18,4 +19,5 @@ def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
"sensor",
|
||||
"binary_sensor",
|
||||
"number",
|
||||
"text_sensor",
|
||||
]
|
||||
|
||||
@@ -30,3 +30,8 @@ binary_sensor:
|
||||
- platform: tmp102
|
||||
tmp102_id: tmp102_extended_temperature
|
||||
name: TMP102 Alert
|
||||
|
||||
text_sensor:
|
||||
- platform: tmp102
|
||||
tmp102_id: tmp102_extended_temperature
|
||||
name: TMP102 Threshold Status
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
|
||||
|
||||
sensor:
|
||||
- platform: tmp102
|
||||
id: tmp102_text_sensor_parent
|
||||
name: TMP102 Temperature
|
||||
|
||||
text_sensor:
|
||||
- platform: tmp102
|
||||
tmp102_id: tmp102_text_sensor_parent
|
||||
name: TMP102 Threshold Status
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <functional>
|
||||
#include <utility>
|
||||
#include "esphome/components/host/preferences.h"
|
||||
#include "esphome/components/tmp102/tmp102.h"
|
||||
@@ -29,6 +30,7 @@ class RegisterBus final : public i2c::I2CBus {
|
||||
unsigned transactions{0};
|
||||
unsigned writes{0};
|
||||
uint8_t pointer{0};
|
||||
std::function<void()> on_write;
|
||||
|
||||
i2c::ErrorCode write_readv(uint8_t address, const uint8_t *data, size_t size, uint8_t *out,
|
||||
size_t out_size) override {
|
||||
@@ -42,6 +44,10 @@ class RegisterBus final : public i2c::I2CBus {
|
||||
return i2c::ERROR_NOT_ACKNOWLEDGED;
|
||||
++this->writes;
|
||||
this->registers[this->pointer] = (uint16_t(data[1]) << 8) | data[2];
|
||||
if (this->on_write) {
|
||||
auto callback = std::move(this->on_write);
|
||||
callback();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (out_size != 0) {
|
||||
@@ -59,6 +65,7 @@ class TMP102Test : public ::testing::Test {
|
||||
RegisterBus bus_;
|
||||
TMP102Component chip_;
|
||||
binary_sensor::BinarySensor alert_;
|
||||
text_sensor::TextSensor status_;
|
||||
TMP102LimitNumber high_{TMP102_LIMIT_HIGH};
|
||||
TMP102LimitNumber low_{TMP102_LIMIT_LOW};
|
||||
unsigned publications_{0};
|
||||
@@ -122,6 +129,8 @@ class TMP102Test : public ::testing::Test {
|
||||
static void save(TMP102LimitNumber &number, float value) {
|
||||
ASSERT_TRUE(number.make_entity_preference<float>().save(&value));
|
||||
}
|
||||
|
||||
static void fail_saves(TMP102LimitNumber &number) { number.pref_ = {}; }
|
||||
};
|
||||
|
||||
TEST_F(TMP102Test, LegacySetupAndContinuousReadDoNotWriteRegisters) {
|
||||
@@ -136,6 +145,24 @@ TEST_F(TMP102Test, LegacySetupAndContinuousReadDoNotWriteRegisters) {
|
||||
EXPECT_EQ(this->bus_.registers, original);
|
||||
}
|
||||
|
||||
TEST_F(TMP102Test, ReadOnlyStatusDoesNotConfigureOrWriteRegisters) {
|
||||
this->chip_.set_threshold_status_text_sensor(&this->status_);
|
||||
const auto original = this->bus_.registers;
|
||||
this->chip_.setup();
|
||||
EXPECT_EQ(this->bus_.transactions, 0);
|
||||
EXPECT_EQ(this->bus_.registers, original);
|
||||
ASSERT_TRUE(this->status_.has_state());
|
||||
EXPECT_EQ(this->status_.state, "Not configured");
|
||||
}
|
||||
|
||||
TEST_F(TMP102Test, ConfiguredSetupPublishesStatus) {
|
||||
this->chip_.set_threshold_status_text_sensor(&this->status_);
|
||||
this->configure();
|
||||
this->chip_.setup();
|
||||
ASSERT_TRUE(this->status_.has_state());
|
||||
EXPECT_EQ(this->status_.state, "OK");
|
||||
}
|
||||
|
||||
TEST_F(TMP102Test, SampleFormatOverridesConfiguredModeInBothDirections) {
|
||||
for (bool configured_extended : {false, true}) {
|
||||
this->configure(configured_extended ? 0x0010 : 0, configured_extended ? 0x2800 : 0x5000,
|
||||
@@ -408,6 +435,7 @@ TEST_F(TMP102Test, FailureOfAnySetupWriteMarksComponentFailed) {
|
||||
}
|
||||
|
||||
TEST_F(TMP102Test, InvalidRestoredPairIsRepairedAfterSuccessfulSetup) {
|
||||
this->chip_.set_threshold_status_text_sensor(&this->status_);
|
||||
this->numbers_();
|
||||
save(this->high_, 20);
|
||||
save(this->low_, 35);
|
||||
@@ -416,30 +444,53 @@ TEST_F(TMP102Test, InvalidRestoredPairIsRepairedAfterSuccessfulSetup) {
|
||||
EXPECT_FLOAT_EQ(this->low_.state, 25);
|
||||
EXPECT_FLOAT_EQ(saved(this->high_), 30);
|
||||
EXPECT_FLOAT_EQ(saved(this->low_), 25);
|
||||
EXPECT_EQ(this->status_.state, "Restored thresholds reset");
|
||||
}
|
||||
|
||||
TEST_F(TMP102Test, InvalidRestoredPairAndSaveFailurePublishCombinedStatus) {
|
||||
this->chip_.set_threshold_status_text_sensor(&this->status_);
|
||||
this->numbers_();
|
||||
save(this->high_, 20);
|
||||
save(this->low_, 35);
|
||||
this->bus_.on_write = [this]() {
|
||||
fail_saves(this->high_);
|
||||
fail_saves(this->low_);
|
||||
};
|
||||
this->chip_.setup();
|
||||
EXPECT_FLOAT_EQ(this->high_.state, 30);
|
||||
EXPECT_FLOAT_EQ(this->low_.state, 25);
|
||||
EXPECT_EQ(this->status_.state, "Restore reset; save failed");
|
||||
}
|
||||
|
||||
TEST_F(TMP102Test, InvalidRestoredValuesAreRepaired) {
|
||||
this->chip_.set_threshold_status_text_sensor(&this->status_);
|
||||
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);
|
||||
EXPECT_FLOAT_EQ(saved(this->high_), 30);
|
||||
EXPECT_FLOAT_EQ(saved(this->low_), 25);
|
||||
EXPECT_EQ(this->status_.state, "Restored thresholds reset");
|
||||
}
|
||||
|
||||
TEST_F(TMP102Test, FailedSetupDoesNotOverwritePreferences) {
|
||||
this->chip_.set_threshold_status_text_sensor(&this->status_);
|
||||
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_EQ(this->status_.state, "Setup 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->chip_.set_threshold_status_text_sensor(&this->status_);
|
||||
this->numbers_();
|
||||
save(this->high_, 90);
|
||||
this->bus_.fail_write = 3;
|
||||
@@ -450,6 +501,7 @@ TEST_F(TMP102Test, FailedSetupRejectsThresholdChanges) {
|
||||
EXPECT_FALSE(this->high_.has_state());
|
||||
EXPECT_EQ(this->bus_.writes, writes);
|
||||
EXPECT_FLOAT_EQ(saved(this->high_), 90);
|
||||
EXPECT_EQ(this->status_.state, "Rejected: sensor setup failed");
|
||||
}
|
||||
|
||||
TEST_F(TMP102Test, SetupOnlySavesChangedThresholds) {
|
||||
@@ -464,6 +516,7 @@ TEST_F(TMP102Test, SetupOnlySavesChangedThresholds) {
|
||||
}
|
||||
|
||||
TEST_F(TMP102Test, FailedNumberWriteKeepsAcceptedState) {
|
||||
this->chip_.set_threshold_status_text_sensor(&this->status_);
|
||||
this->numbers_();
|
||||
save(this->high_, 90);
|
||||
save(this->low_, 85);
|
||||
@@ -475,6 +528,47 @@ TEST_F(TMP102Test, FailedNumberWriteKeepsAcceptedState) {
|
||||
EXPECT_FLOAT_EQ(this->high_.state, 90);
|
||||
EXPECT_EQ(publications, 1);
|
||||
EXPECT_FLOAT_EQ(saved(this->high_), 90);
|
||||
EXPECT_EQ(this->status_.state, "Write failed: high threshold");
|
||||
|
||||
this->bus_.fail_write = 2;
|
||||
this->low_.make_call().set_value(80).perform();
|
||||
EXPECT_FLOAT_EQ(this->low_.state, 85);
|
||||
EXPECT_FLOAT_EQ(saved(this->low_), 85);
|
||||
EXPECT_EQ(this->status_.state, "Write failed: low threshold");
|
||||
}
|
||||
|
||||
TEST_F(TMP102Test, PreferenceSaveFailurePublishesStatus) {
|
||||
this->chip_.set_threshold_status_text_sensor(&this->status_);
|
||||
this->numbers_();
|
||||
this->chip_.setup();
|
||||
fail_saves(this->high_);
|
||||
this->high_.make_call().set_value(35).perform();
|
||||
EXPECT_FLOAT_EQ(this->high_.state, 35);
|
||||
EXPECT_EQ(this->status_.state, "Preference save failed");
|
||||
}
|
||||
|
||||
TEST_F(TMP102Test, RejectedNumberWritePublishesStatus) {
|
||||
this->chip_.set_threshold_status_text_sensor(&this->status_);
|
||||
this->numbers_();
|
||||
this->chip_.setup();
|
||||
this->high_.make_call().set_value(20).perform();
|
||||
EXPECT_EQ(this->status_.state, "Rejected: high 20.0000°C crosses low 25.0000°C");
|
||||
this->high_.make_call().set_value(35).perform();
|
||||
EXPECT_EQ(this->status_.state, "OK");
|
||||
}
|
||||
|
||||
TEST_F(TMP102Test, ConfigurationRecoveryPublishesStatus) {
|
||||
this->chip_.set_threshold_status_text_sensor(&this->status_);
|
||||
this->configure();
|
||||
this->chip_.setup();
|
||||
this->bus_.registers[1] = 0;
|
||||
this->chip_.update();
|
||||
EXPECT_EQ(this->status_.state, "Configuration restored");
|
||||
|
||||
this->bus_.registers[1] = 0;
|
||||
this->bus_.fail_write = 1;
|
||||
this->chip_.update();
|
||||
EXPECT_EQ(this->status_.state, "Configuration recovery failed");
|
||||
}
|
||||
|
||||
TEST_F(TMP102Test, RestoreDisabledNeverChangesSavedValue) {
|
||||
|
||||
@@ -954,6 +954,23 @@ def test_yaml_merge_chain_include_resolves() -> None:
|
||||
assert config["base"]["existing"] == "value"
|
||||
|
||||
|
||||
def test_parse_yaml_custom_loader_resolves_root_include() -> None:
|
||||
"""A document that is only `!include` is resolved for a custom loader too."""
|
||||
parent = Path("/fake/main.yaml")
|
||||
loader_calls: list[Path] = []
|
||||
|
||||
def custom_loader(fname: Path) -> dict[str, int]:
|
||||
loader_calls.append(fname)
|
||||
return {"x": 1}
|
||||
|
||||
config = yaml_util.parse_yaml(
|
||||
parent, io.StringIO("!include inner.yaml\n"), custom_loader
|
||||
)
|
||||
|
||||
assert config == {"x": 1}
|
||||
assert loader_calls == [Path("/fake/inner.yaml")]
|
||||
|
||||
|
||||
def test_yaml_merge_chain_include_depth_exceeded() -> None:
|
||||
"""Chain includes in merge keys exceeding depth limit raise a clear error."""
|
||||
parent = Path("/fake/main.yaml")
|
||||
|
||||
Reference in New Issue
Block a user