Compare commits

...
33 changed files with 819 additions and 207 deletions
+7 -4
View File
@@ -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_);
}
+53 -27
View File
@@ -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++)
+11
View File
@@ -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;
+1
View File
@@ -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
+5 -2
View File
@@ -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 =
+1
View File
@@ -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 {
+33 -3
View File
@@ -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) {
-56
View File
@@ -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
+2 -2
View File
@@ -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]))
+68 -64
View File
@@ -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) {
+20 -12
View File
@@ -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.
+1
View File
@@ -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_; }
+5 -13
View File
@@ -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))
+31
View File
@@ -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))
+98 -7
View File
@@ -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
+28 -4
View File
@@ -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;
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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
+20
View File
@@ -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
+1 -1
View File
@@ -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:
+3 -1
View File
@@ -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",
]
+5
View File
@@ -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
+94
View File
@@ -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) {
+17
View File
@@ -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")