mirror of
https://github.com/esphome/esphome.git
synced 2026-10-17 11:42:51 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
daf8b15073 | ||
|
|
c1fa73470e | ||
|
|
e41d16ba7e | ||
|
|
5171113c87 | ||
|
|
5718e4b779 |
@@ -1328,9 +1328,9 @@ message ListEntitiesClimateResponse {
|
||||
bool supports_action = 12; // Deprecated: use feature_flags
|
||||
repeated ClimateFanMode supported_fan_modes = 13 [(container_pointer_no_template) = "climate::ClimateFanModeMask"];
|
||||
repeated ClimateSwingMode supported_swing_modes = 14 [(container_pointer_no_template) = "climate::ClimateSwingModeMask"];
|
||||
repeated string supported_custom_fan_modes = 15 [(container_pointer_no_template) = "ConstVector<const char *, true>"];
|
||||
repeated string supported_custom_fan_modes = 15 [(container_pointer_no_template) = "std::vector<const char *>"];
|
||||
repeated ClimatePreset supported_presets = 16 [(container_pointer_no_template) = "climate::ClimatePresetMask"];
|
||||
repeated string supported_custom_presets = 17 [(container_pointer_no_template) = "ConstVector<const char *, true>"];
|
||||
repeated string supported_custom_presets = 17 [(container_pointer_no_template) = "std::vector<const char *>"];
|
||||
bool disabled_by_default = 18;
|
||||
string icon = 19 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
|
||||
EntityCategory entity_category = 20;
|
||||
|
||||
@@ -1776,9 +1776,9 @@ class ListEntitiesClimateResponse final : public InfoResponseProtoMessage {
|
||||
bool supports_action{false};
|
||||
const climate::ClimateFanModeMask *supported_fan_modes{};
|
||||
const climate::ClimateSwingModeMask *supported_swing_modes{};
|
||||
const ConstVector<const char *, true> *supported_custom_fan_modes{};
|
||||
const std::vector<const char *> *supported_custom_fan_modes{};
|
||||
const climate::ClimatePresetMask *supported_presets{};
|
||||
const ConstVector<const char *, true> *supported_custom_presets{};
|
||||
const std::vector<const char *> *supported_custom_presets{};
|
||||
float visual_current_temperature_step{0.0f};
|
||||
bool supports_current_humidity{false};
|
||||
bool supports_target_humidity{false};
|
||||
|
||||
@@ -61,7 +61,7 @@ void BedJetClimate::dump_config() {
|
||||
|
||||
void BedJetClimate::setup() {
|
||||
// Set custom modes once during setup — stored on Climate base class, wired via get_traits()
|
||||
this->set_supported_custom_fan_modes_static(BEDJET_FAN_STEP_NAMES, BEDJET_FAN_SPEED_COUNT);
|
||||
this->set_supported_custom_fan_modes(BEDJET_FAN_STEP_NAMES);
|
||||
this->set_supported_custom_presets({
|
||||
this->heating_mode_ == HEAT_MODE_EXTENDED ? "LTD HT" : "EXT HT",
|
||||
"M1",
|
||||
|
||||
@@ -442,24 +442,6 @@ async def setup_climate_core_(var: MockObj, config: ConfigType) -> None:
|
||||
await web_server.add_entity_config(var, web_server_config)
|
||||
|
||||
|
||||
def set_custom_fan_modes(var: MockObj, modes: list[str]) -> None:
|
||||
"""Point the climate at a shared flash table of custom fan modes."""
|
||||
if modes:
|
||||
table = cg.shared_progmem_array(
|
||||
"climate_custom_fan_modes", cg.const_char_ptr, modes
|
||||
)
|
||||
cg.add(var.set_supported_custom_fan_modes_static(table, len(modes)))
|
||||
|
||||
|
||||
def set_custom_presets(var: MockObj, presets: list[str]) -> None:
|
||||
"""Point the climate at a shared flash table of custom presets."""
|
||||
if presets:
|
||||
table = cg.shared_progmem_array(
|
||||
"climate_custom_presets", cg.const_char_ptr, presets
|
||||
)
|
||||
cg.add(var.set_supported_custom_presets_static(table, len(presets)))
|
||||
|
||||
|
||||
async def register_climate(var: MockObj, config: ConfigType) -> None:
|
||||
if not CORE.has_id(config[CONF_ID]):
|
||||
var = cg.Pvariable(config[CONF_ID], var)
|
||||
|
||||
@@ -479,8 +479,10 @@ void Climate::publish_state() {
|
||||
ClimateTraits Climate::get_traits() {
|
||||
auto traits = this->traits();
|
||||
// Wire custom mode pointers from Climate-owned storage
|
||||
traits.set_supported_custom_fan_modes_(&this->supported_custom_fan_modes_);
|
||||
traits.set_supported_custom_presets_(&this->supported_custom_presets_);
|
||||
if (this->supported_custom_fan_modes_)
|
||||
traits.set_supported_custom_fan_modes_(this->supported_custom_fan_modes_);
|
||||
if (this->supported_custom_presets_)
|
||||
traits.set_supported_custom_presets_(this->supported_custom_presets_);
|
||||
#ifdef USE_CLIMATE_VISUAL_OVERRIDES
|
||||
if (!std::isnan(this->visual_min_temperature_override_)) {
|
||||
traits.set_visual_min_temperature(this->visual_min_temperature_override_);
|
||||
@@ -626,7 +628,7 @@ template<typename T> bool set_primary_mode(optional<T> &primary, const char *&cu
|
||||
* After: fan_mode=nullopt, custom_fan_mode_="Turbo" (pointer from traits)
|
||||
*
|
||||
* Lifetime Safety:
|
||||
* - found_ptr must come from Climate::find_custom_*_()
|
||||
* - found_ptr must come from traits.find_custom_*_mode_()
|
||||
* - Only pointers found in traits are stored, ensuring they remain valid
|
||||
* - Prevents dangling pointers from temporary strings
|
||||
*
|
||||
@@ -682,7 +684,11 @@ const char *Climate::find_custom_fan_mode_(const char *custom_fan_mode) {
|
||||
}
|
||||
|
||||
const char *Climate::find_custom_fan_mode_(const char *custom_fan_mode, size_t len) {
|
||||
return vector_find(this->supported_custom_fan_modes_, custom_fan_mode, len);
|
||||
if (this->supported_custom_fan_modes_) {
|
||||
return vector_find(*this->supported_custom_fan_modes_, custom_fan_mode, len);
|
||||
}
|
||||
// Fallback for deprecated path: external components may set modes on ClimateTraits directly
|
||||
return this->get_traits().find_custom_fan_mode_(custom_fan_mode, len);
|
||||
}
|
||||
|
||||
const char *Climate::find_custom_preset_(const char *custom_preset) {
|
||||
@@ -690,7 +696,11 @@ const char *Climate::find_custom_preset_(const char *custom_preset) {
|
||||
}
|
||||
|
||||
const char *Climate::find_custom_preset_(const char *custom_preset, size_t len) {
|
||||
return vector_find(this->supported_custom_presets_, custom_preset, len);
|
||||
if (this->supported_custom_presets_) {
|
||||
return vector_find(*this->supported_custom_presets_, custom_preset, len);
|
||||
}
|
||||
// Fallback for deprecated path: external components may set modes on ClimateTraits directly
|
||||
return this->get_traits().find_custom_preset_(custom_preset, len);
|
||||
}
|
||||
|
||||
void Climate::dump_traits_(const char *tag) {
|
||||
|
||||
@@ -246,34 +246,26 @@ class Climate : public EntityBase {
|
||||
}
|
||||
#endif
|
||||
|
||||
/// Set the supported custom fan modes; the list is copied (stored on Climate, referenced by ClimateTraits).
|
||||
/// Set the supported custom fan modes (stored on Climate, referenced by ClimateTraits).
|
||||
void set_supported_custom_fan_modes(std::initializer_list<const char *> modes) {
|
||||
this->supported_custom_fan_modes_.assign_copy(modes.begin(), modes.size());
|
||||
this->ensure_custom_fan_modes_().assign(modes.begin(), modes.end());
|
||||
}
|
||||
void set_supported_custom_fan_modes(const std::vector<const char *> &modes) {
|
||||
this->supported_custom_fan_modes_.assign_copy(modes.data(), modes.size());
|
||||
this->ensure_custom_fan_modes_() = modes;
|
||||
}
|
||||
template<size_t N> void set_supported_custom_fan_modes(const char *const (&modes)[N]) {
|
||||
this->supported_custom_fan_modes_.assign_copy(modes, N);
|
||||
}
|
||||
/// Points at a static table that outlives the climate (codegen); call before any runtime copy.
|
||||
void set_supported_custom_fan_modes_static(const char *const *modes, size_t count) {
|
||||
this->supported_custom_fan_modes_.assign_static(modes, count);
|
||||
this->ensure_custom_fan_modes_().assign(modes, modes + N);
|
||||
}
|
||||
|
||||
/// Set the supported custom presets; the list is copied (stored on Climate, referenced by ClimateTraits).
|
||||
/// Set the supported custom presets (stored on Climate, referenced by ClimateTraits).
|
||||
void set_supported_custom_presets(std::initializer_list<const char *> presets) {
|
||||
this->supported_custom_presets_.assign_copy(presets.begin(), presets.size());
|
||||
this->ensure_custom_presets_().assign(presets.begin(), presets.end());
|
||||
}
|
||||
void set_supported_custom_presets(const std::vector<const char *> &presets) {
|
||||
this->supported_custom_presets_.assign_copy(presets.data(), presets.size());
|
||||
this->ensure_custom_presets_() = presets;
|
||||
}
|
||||
template<size_t N> void set_supported_custom_presets(const char *const (&presets)[N]) {
|
||||
this->supported_custom_presets_.assign_copy(presets, N);
|
||||
}
|
||||
/// Points at a static table that outlives the climate (codegen); call before any runtime copy.
|
||||
void set_supported_custom_presets_static(const char *const *presets, size_t count) {
|
||||
this->supported_custom_presets_.assign_static(presets, count);
|
||||
this->ensure_custom_presets_().assign(presets, presets + N);
|
||||
}
|
||||
|
||||
/// Check if a custom fan mode is currently active.
|
||||
@@ -396,8 +388,22 @@ class Climate : public EntityBase {
|
||||
#endif
|
||||
|
||||
private:
|
||||
ClimateCustomModes supported_custom_fan_modes_;
|
||||
ClimateCustomModes supported_custom_presets_;
|
||||
/// Lazy-allocate custom mode vectors (never freed — entity lives forever).
|
||||
std::vector<const char *> &ensure_custom_fan_modes_() {
|
||||
if (!this->supported_custom_fan_modes_) {
|
||||
this->supported_custom_fan_modes_ = new std::vector<const char *>(); // NOLINT
|
||||
}
|
||||
return *this->supported_custom_fan_modes_;
|
||||
}
|
||||
std::vector<const char *> &ensure_custom_presets_() {
|
||||
if (!this->supported_custom_presets_) {
|
||||
this->supported_custom_presets_ = new std::vector<const char *>(); // NOLINT
|
||||
}
|
||||
return *this->supported_custom_presets_;
|
||||
}
|
||||
|
||||
std::vector<const char *> *supported_custom_fan_modes_{nullptr};
|
||||
std::vector<const char *> *supported_custom_presets_{nullptr};
|
||||
|
||||
/** The active custom fan mode (private - enforces use of safe setters).
|
||||
*
|
||||
|
||||
@@ -2,16 +2,31 @@
|
||||
|
||||
namespace esphome::climate {
|
||||
|
||||
ClimateTraits::ClimateTraits() = default;
|
||||
// Compat: shared empty vector for getters when no custom modes are set.
|
||||
// Remove in 2026.11.0 when deprecated ClimateTraits setters are removed
|
||||
// and getters can return const vector * instead of const vector &.
|
||||
static const std::vector<const char *> EMPTY_CUSTOM_MODES; // NOLINT
|
||||
|
||||
static const ClimateCustomModes EMPTY_CUSTOM_MODES; // NOLINT
|
||||
|
||||
const ClimateCustomModes &ClimateTraits::get_supported_custom_fan_modes() const {
|
||||
return this->supported_custom_fan_modes_ != nullptr ? *this->supported_custom_fan_modes_ : EMPTY_CUSTOM_MODES;
|
||||
const std::vector<const char *> &ClimateTraits::get_supported_custom_fan_modes() const {
|
||||
if (this->supported_custom_fan_modes_) {
|
||||
return *this->supported_custom_fan_modes_;
|
||||
}
|
||||
// Compat: fall back to owned vector from deprecated setters. Remove in 2026.11.0.
|
||||
if (!this->compat_custom_fan_modes_.empty()) {
|
||||
return this->compat_custom_fan_modes_;
|
||||
}
|
||||
return EMPTY_CUSTOM_MODES;
|
||||
}
|
||||
|
||||
const ClimateCustomModes &ClimateTraits::get_supported_custom_presets() const {
|
||||
return this->supported_custom_presets_ != nullptr ? *this->supported_custom_presets_ : EMPTY_CUSTOM_MODES;
|
||||
const std::vector<const char *> &ClimateTraits::get_supported_custom_presets() const {
|
||||
if (this->supported_custom_presets_) {
|
||||
return *this->supported_custom_presets_;
|
||||
}
|
||||
// Compat: fall back to owned vector from deprecated setters. Remove in 2026.11.0.
|
||||
if (!this->compat_custom_presets_.empty()) {
|
||||
return this->compat_custom_presets_;
|
||||
}
|
||||
return EMPTY_CUSTOM_MODES;
|
||||
}
|
||||
|
||||
int8_t ClimateTraits::get_target_temperature_accuracy_decimals() const {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstring>
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
#include "climate_mode.h"
|
||||
#include "esphome/core/finite_set_mask.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
@@ -18,12 +18,9 @@ using ClimateSwingModeMask =
|
||||
FiniteSetMask<ClimateSwingMode, DefaultBitPolicy<ClimateSwingMode, CLIMATE_SWING_HORIZONTAL + 1>>;
|
||||
using ClimatePresetMask = FiniteSetMask<ClimatePreset, DefaultBitPolicy<ClimatePreset, CLIMATE_PRESET_ACTIVITY + 1>>;
|
||||
|
||||
/// Custom fan modes and custom presets: a flash table from codegen, or an owned copy set at runtime.
|
||||
using ClimateCustomModes = ConstVector<const char *, true>;
|
||||
|
||||
// Lightweight linear search for small lists (1-20 items) of const char* pointers
|
||||
// Lightweight linear search for small vectors (1-20 items) of const char* pointers
|
||||
// Avoids std::find template overhead
|
||||
template<typename List> inline bool vector_contains(const List &vec, const char *value, size_t len) {
|
||||
inline bool vector_contains(const std::vector<const char *> &vec, const char *value, size_t len) {
|
||||
for (const char *item : vec) {
|
||||
if (strncmp(item, value, len) == 0 && item[len] == '\0')
|
||||
return true;
|
||||
@@ -31,12 +28,12 @@ template<typename List> inline bool vector_contains(const List &vec, const char
|
||||
return false;
|
||||
}
|
||||
|
||||
template<typename List> inline bool vector_contains(const List &vec, const char *value) {
|
||||
inline bool vector_contains(const std::vector<const char *> &vec, const char *value) {
|
||||
return vector_contains(vec, value, strlen(value));
|
||||
}
|
||||
|
||||
// Find and return matching pointer from a list, or nullptr if not found
|
||||
template<typename List> inline const char *vector_find(const List &vec, const char *value, size_t len) {
|
||||
// Find and return matching pointer from vector, or nullptr if not found
|
||||
inline const char *vector_find(const std::vector<const char *> &vec, const char *value, size_t len) {
|
||||
for (const char *item : vec) {
|
||||
if (strncmp(item, value, len) == 0 && item[len] == '\0')
|
||||
return item;
|
||||
@@ -74,13 +71,9 @@ template<typename List> inline const char *vector_find(const List &vec, const ch
|
||||
class Climate; // Forward declaration
|
||||
|
||||
class ClimateTraits {
|
||||
friend class Climate; // Climate wires its custom mode lists into traits
|
||||
friend class Climate; // Allow Climate to access protected find methods
|
||||
|
||||
public:
|
||||
// Out of line on purpose: an inlined default init makes GCC copy the defaults from a .rodata
|
||||
// template per traits() override, and .rodata is RAM on ESP8266.
|
||||
ClimateTraits();
|
||||
|
||||
/// Get/set feature flags (see ClimateFeatures enum in climate_mode.h)
|
||||
uint32_t get_feature_flags() const { return this->feature_flags_; }
|
||||
void add_feature_flags(uint32_t feature_flags) { this->feature_flags_ |= feature_flags; }
|
||||
@@ -100,14 +93,42 @@ class ClimateTraits {
|
||||
if (!this->supported_fan_modes_.empty()) {
|
||||
return true;
|
||||
}
|
||||
return this->supported_custom_fan_modes_ != nullptr && !this->supported_custom_fan_modes_->empty();
|
||||
// Same precedence as get_supported_custom_fan_modes() getter
|
||||
if (this->supported_custom_fan_modes_) {
|
||||
return !this->supported_custom_fan_modes_->empty();
|
||||
}
|
||||
return !this->compat_custom_fan_modes_.empty(); // Compat: remove in 2026.11.0
|
||||
}
|
||||
const ClimateFanModeMask &get_supported_fan_modes() const { return this->supported_fan_modes_; }
|
||||
|
||||
/// Empty when the climate has no custom fan modes. Set them on the Climate entity.
|
||||
const ClimateCustomModes &get_supported_custom_fan_modes() const;
|
||||
// Remove before 2026.11.0
|
||||
ESPDEPRECATED("Call set_supported_custom_fan_modes() on the Climate entity instead. Removed in 2026.11.0", "2026.5.0")
|
||||
void set_supported_custom_fan_modes(std::initializer_list<const char *> modes) {
|
||||
// Compat: store in owned vector. Copies copy the vector (deprecated path still copies this vector).
|
||||
this->compat_custom_fan_modes_ = modes;
|
||||
}
|
||||
// Remove before 2026.11.0
|
||||
ESPDEPRECATED("Call set_supported_custom_fan_modes() on the Climate entity instead. Removed in 2026.11.0", "2026.5.0")
|
||||
void set_supported_custom_fan_modes(const std::vector<const char *> &modes) {
|
||||
this->compat_custom_fan_modes_ = modes;
|
||||
}
|
||||
// Remove before 2026.11.0
|
||||
template<size_t N>
|
||||
ESPDEPRECATED("Call set_supported_custom_fan_modes() on the Climate entity instead. Removed in 2026.11.0", "2026.5.0")
|
||||
void set_supported_custom_fan_modes(const char *const (&modes)[N]) {
|
||||
this->compat_custom_fan_modes_.assign(modes, modes + N);
|
||||
}
|
||||
|
||||
// Deleted overloads to catch incorrect std::string usage at compile time with clear error messages
|
||||
void set_supported_custom_fan_modes(const std::vector<std::string> &modes) = delete;
|
||||
void set_supported_custom_fan_modes(std::initializer_list<std::string> modes) = delete;
|
||||
|
||||
// Compat: returns const ref with empty fallback. In 2026.11.0 change to return const vector *.
|
||||
const std::vector<const char *> &get_supported_custom_fan_modes() const;
|
||||
bool supports_custom_fan_mode(const char *custom_fan_mode) const {
|
||||
return vector_contains(this->get_supported_custom_fan_modes(), custom_fan_mode);
|
||||
return (this->supported_custom_fan_modes_ &&
|
||||
vector_contains(*this->supported_custom_fan_modes_, custom_fan_mode)) ||
|
||||
vector_contains(this->compat_custom_fan_modes_, custom_fan_mode); // Compat: remove in 2026.11.0
|
||||
}
|
||||
bool supports_custom_fan_mode(const std::string &custom_fan_mode) const {
|
||||
return this->supports_custom_fan_mode(custom_fan_mode.c_str());
|
||||
@@ -119,10 +140,32 @@ class ClimateTraits {
|
||||
bool get_supports_presets() const { return !this->supported_presets_.empty(); }
|
||||
const ClimatePresetMask &get_supported_presets() const { return this->supported_presets_; }
|
||||
|
||||
/// Empty when the climate has no custom presets. Set them on the Climate entity.
|
||||
const ClimateCustomModes &get_supported_custom_presets() const;
|
||||
// Remove before 2026.11.0
|
||||
ESPDEPRECATED("Call set_supported_custom_presets() on the Climate entity instead. Removed in 2026.11.0", "2026.5.0")
|
||||
void set_supported_custom_presets(std::initializer_list<const char *> presets) {
|
||||
this->compat_custom_presets_ = presets;
|
||||
}
|
||||
// Remove before 2026.11.0
|
||||
ESPDEPRECATED("Call set_supported_custom_presets() on the Climate entity instead. Removed in 2026.11.0", "2026.5.0")
|
||||
void set_supported_custom_presets(const std::vector<const char *> &presets) {
|
||||
this->compat_custom_presets_ = presets;
|
||||
}
|
||||
// Remove before 2026.11.0
|
||||
template<size_t N>
|
||||
ESPDEPRECATED("Call set_supported_custom_presets() on the Climate entity instead. Removed in 2026.11.0", "2026.5.0")
|
||||
void set_supported_custom_presets(const char *const (&presets)[N]) {
|
||||
this->compat_custom_presets_.assign(presets, presets + N);
|
||||
}
|
||||
|
||||
// Deleted overloads to catch incorrect std::string usage at compile time with clear error messages
|
||||
void set_supported_custom_presets(const std::vector<std::string> &presets) = delete;
|
||||
void set_supported_custom_presets(std::initializer_list<std::string> presets) = delete;
|
||||
|
||||
// Compat: returns const ref with empty fallback. In 2026.11.0 change to return const vector *.
|
||||
const std::vector<const char *> &get_supported_custom_presets() const;
|
||||
bool supports_custom_preset(const char *custom_preset) const {
|
||||
return vector_contains(this->get_supported_custom_presets(), custom_preset);
|
||||
return (this->supported_custom_presets_ && vector_contains(*this->supported_custom_presets_, custom_preset)) ||
|
||||
vector_contains(this->compat_custom_presets_, custom_preset); // Compat: remove in 2026.11.0
|
||||
}
|
||||
bool supports_custom_preset(const std::string &custom_preset) const {
|
||||
return this->supports_custom_preset(custom_preset.c_str());
|
||||
@@ -189,8 +232,38 @@ class ClimateTraits {
|
||||
}
|
||||
|
||||
/// Set custom mode pointers (only Climate::get_traits() should call these).
|
||||
void set_supported_custom_fan_modes_(const ClimateCustomModes *modes) { this->supported_custom_fan_modes_ = modes; }
|
||||
void set_supported_custom_presets_(const ClimateCustomModes *presets) { this->supported_custom_presets_ = presets; }
|
||||
void set_supported_custom_fan_modes_(const std::vector<const char *> *modes) {
|
||||
this->supported_custom_fan_modes_ = modes;
|
||||
}
|
||||
void set_supported_custom_presets_(const std::vector<const char *> *presets) {
|
||||
this->supported_custom_presets_ = presets;
|
||||
}
|
||||
|
||||
/// Find and return the matching custom fan mode pointer from supported modes, or nullptr if not found
|
||||
/// This is protected as it's an implementation detail - use Climate::find_custom_fan_mode_() instead
|
||||
const char *find_custom_fan_mode_(const char *custom_fan_mode) const {
|
||||
return this->find_custom_fan_mode_(custom_fan_mode, strlen(custom_fan_mode));
|
||||
}
|
||||
const char *find_custom_fan_mode_(const char *custom_fan_mode, size_t len) const {
|
||||
if (this->supported_custom_fan_modes_) {
|
||||
return vector_find(*this->supported_custom_fan_modes_, custom_fan_mode, len);
|
||||
}
|
||||
// Compat: check owned vector from deprecated setters. Remove in 2026.11.0.
|
||||
return vector_find(this->compat_custom_fan_modes_, custom_fan_mode, len);
|
||||
}
|
||||
|
||||
/// Find and return the matching custom preset pointer from supported presets, or nullptr if not found
|
||||
/// This is protected as it's an implementation detail - use Climate::find_custom_preset_() instead
|
||||
const char *find_custom_preset_(const char *custom_preset) const {
|
||||
return this->find_custom_preset_(custom_preset, strlen(custom_preset));
|
||||
}
|
||||
const char *find_custom_preset_(const char *custom_preset, size_t len) const {
|
||||
if (this->supported_custom_presets_) {
|
||||
return vector_find(*this->supported_custom_presets_, custom_preset, len);
|
||||
}
|
||||
// Compat: check owned vector from deprecated setters. Remove in 2026.11.0.
|
||||
return vector_find(this->compat_custom_presets_, custom_preset, len);
|
||||
}
|
||||
|
||||
uint32_t feature_flags_{0};
|
||||
float visual_min_temperature_{10};
|
||||
@@ -206,10 +279,17 @@ class ClimateTraits {
|
||||
climate::ClimatePresetMask supported_presets_;
|
||||
TemperatureUnit temperature_unit_{TemperatureUnit::CELSIUS};
|
||||
|
||||
/// Owned by the Climate entity; get_traits() wires these pointers.
|
||||
const ClimateCustomModes *supported_custom_fan_modes_{nullptr};
|
||||
const ClimateCustomModes *supported_custom_presets_{nullptr};
|
||||
/** Custom mode storage - pointers to vectors owned by the Climate base class.
|
||||
*
|
||||
* ClimateTraits does not own this data; Climate stores the vectors and
|
||||
* get_traits() wires these pointers automatically.
|
||||
*/
|
||||
const std::vector<const char *> *supported_custom_fan_modes_{nullptr};
|
||||
const std::vector<const char *> *supported_custom_presets_{nullptr};
|
||||
// Compat: owned storage for deprecated setters. Copies copy the vector (copies include this vector).
|
||||
// Remove in 2026.11.0.
|
||||
std::vector<const char *> compat_custom_fan_modes_;
|
||||
std::vector<const char *> compat_custom_presets_;
|
||||
};
|
||||
static_assert(std::is_trivially_copyable_v<ClimateTraits>, "ClimateTraits is copied on every traits() call");
|
||||
|
||||
} // namespace esphome::climate
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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_; }
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
namespace esphome::midea::ac {
|
||||
|
||||
const char Constants::TAG[] PROGMEM = "midea";
|
||||
const char *const Constants::FREEZE_PROTECTION = "Freeze Protection";
|
||||
const char *const Constants::SILENT = "Silent";
|
||||
const char *const Constants::TURBO = "Turbo";
|
||||
|
||||
ClimateMode Converters::to_climate_mode(MideaMode mode) {
|
||||
switch (mode) {
|
||||
|
||||
@@ -18,9 +18,9 @@ using MideaPreset = dudanov::midea::ac::Preset;
|
||||
class Constants {
|
||||
public:
|
||||
static const char TAG[]; // PROGMEM, like ESPHOME_LOG_TAG
|
||||
static constexpr const char *FREEZE_PROTECTION = "Freeze Protection";
|
||||
static constexpr const char *SILENT = "Silent";
|
||||
static constexpr const char *TURBO = "Turbo";
|
||||
static const char *const FREEZE_PROTECTION;
|
||||
static const char *const SILENT;
|
||||
static const char *const TURBO;
|
||||
};
|
||||
|
||||
class Converters {
|
||||
|
||||
@@ -52,6 +52,8 @@ class AirConditioner final : public ApplianceBase<dudanov::midea::ac::AirConditi
|
||||
void set_supported_modes(ClimateModeMask modes) { this->supported_modes_ = modes; }
|
||||
void set_supported_swing_modes(ClimateSwingModeMask modes) { this->supported_swing_modes_ = modes; }
|
||||
void set_supported_presets(ClimatePresetMask presets) { this->supported_presets_ = presets; }
|
||||
void set_custom_presets(std::initializer_list<const char *> presets) { this->set_supported_custom_presets(presets); }
|
||||
void set_custom_fan_modes(std::initializer_list<const char *> modes) { this->set_supported_custom_fan_modes(modes); }
|
||||
|
||||
protected:
|
||||
void control(const ClimateCall &call) override;
|
||||
|
||||
@@ -234,8 +234,10 @@ async def to_code(config):
|
||||
cg.add(var.set_supported_swing_modes(config[CONF_SUPPORTED_SWING_MODES]))
|
||||
if CONF_SUPPORTED_PRESETS in config:
|
||||
cg.add(var.set_supported_presets(config[CONF_SUPPORTED_PRESETS]))
|
||||
climate.set_custom_presets(var, config.get(CONF_CUSTOM_PRESETS, []))
|
||||
climate.set_custom_fan_modes(var, config.get(CONF_CUSTOM_FAN_MODES, []))
|
||||
if CONF_CUSTOM_PRESETS in config:
|
||||
cg.add(var.set_custom_presets(config[CONF_CUSTOM_PRESETS]))
|
||||
if CONF_CUSTOM_FAN_MODES in config:
|
||||
cg.add(var.set_custom_fan_modes(config[CONF_CUSTOM_FAN_MODES]))
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_OUTDOOR_TEMPERATURE, var.set_outdoor_temperature_sensor)
|
||||
await sensors(CONF_POWER_USAGE, var.set_power_sensor)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -324,7 +324,12 @@ async def to_code(config: ConfigType) -> None:
|
||||
for mode in config.get(CONF_SUPPORTED_FAN_MODES, []):
|
||||
cg.add(var.add_supported_fan_mode(mode))
|
||||
|
||||
climate.set_custom_fan_modes(var, config.get(CONF_CUSTOM_FAN_MODES, []))
|
||||
if CONF_CUSTOM_FAN_MODES in config:
|
||||
cg.add(
|
||||
var.set_supported_custom_fan_modes(
|
||||
cg.ArrayInitializer(*config[CONF_CUSTOM_FAN_MODES])
|
||||
)
|
||||
)
|
||||
|
||||
for mode in config.get(CONF_SUPPORTED_SWING_MODES, []):
|
||||
cg.add(var.add_supported_swing_mode(mode))
|
||||
@@ -332,7 +337,12 @@ async def to_code(config: ConfigType) -> None:
|
||||
for preset in config.get(CONF_SUPPORTED_PRESETS, []):
|
||||
cg.add(var.add_supported_preset(preset))
|
||||
|
||||
climate.set_custom_presets(var, config.get(CONF_CUSTOM_PRESETS, []))
|
||||
if CONF_CUSTOM_PRESETS in config:
|
||||
cg.add(
|
||||
var.set_supported_custom_presets(
|
||||
cg.ArrayInitializer(*config[CONF_CUSTOM_PRESETS])
|
||||
)
|
||||
)
|
||||
|
||||
for key, trigger_getter, arg_type in SET_ACTIONS:
|
||||
if (conf := config.get(key)) is not None:
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#include "weikai.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
namespace esphome::weikai {
|
||||
|
||||
@@ -53,19 +54,18 @@ void print_buffer(const uint8_t *data, size_t length) {
|
||||
}
|
||||
}
|
||||
|
||||
static const char *const REG_TO_STR_P0[16] = {"GENA", "GRST", "GMUT", "SPAGE", "SCR", "LCR", "FCR", "SIER",
|
||||
"SIFR", "TFCNT", "RFCNT", "FSR", "LSR", "FDAT", "FWCR", "RS485"};
|
||||
static const char *const REG_TO_STR_P1[16] = {"GENA", "GRST", "GMUT", "SPAGE", "BAUD1", "BAUD0", "PRES", "RFTL",
|
||||
"TFTL", "FWTH", "FWTL", "XON1", "XOFF1", "SADR", "SAEN", "RTSDLY"};
|
||||
PROGMEM_STRING_TABLE(RegNamesPage0, "GENA", "GRST", "GMUT", "SPAGE", "SCR", "LCR", "FCR", "SIER", "SIFR", "TFCNT",
|
||||
"RFCNT", "FSR", "LSR", "FDAT", "FWCR", "RS485");
|
||||
PROGMEM_STRING_TABLE(RegNamesPage1, "GENA", "GRST", "GMUT", "SPAGE", "BAUD1", "BAUD0", "PRES", "RFTL", "TFTL", "FWTH",
|
||||
"FWTL", "XON1", "XOFF1", "SADR", "SAEN", "RTSDLY");
|
||||
|
||||
// method to print a register value as text: used in the log messages ...
|
||||
const char *reg_to_str(int reg, bool page1) {
|
||||
const LogString *reg_to_str(int reg, bool page1) {
|
||||
if (reg == WKREG_GPDAT) {
|
||||
return "GPDAT";
|
||||
return LOG_STR("GPDAT");
|
||||
} else if (reg == WKREG_GPDIR) {
|
||||
return "GPDIR";
|
||||
return LOG_STR("GPDIR");
|
||||
} else {
|
||||
return page1 ? REG_TO_STR_P1[reg & 0x0F] : REG_TO_STR_P0[reg & 0x0F];
|
||||
return page1 ? RegNamesPage1::get_log_str(reg & 0x0F, 0) : RegNamesPage0::get_log_str(reg & 0x0F, 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,9 @@ constexpr size_t FIFO_SIZE = 256;
|
||||
/// @brief size of the ring buffer set to size of the FIFO
|
||||
constexpr size_t RING_BUFFER_SIZE = FIFO_SIZE;
|
||||
|
||||
/// @brief Name of a register for the log messages
|
||||
const LogString *reg_to_str(int reg, bool page1);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
/// @brief This is an helper class that provides a simple ring buffers that works as a FIFO
|
||||
/// @details This ring buffer is used to buffer the bytes received in the FIFO of the Weika device. The best way to read
|
||||
|
||||
@@ -25,21 +25,7 @@ void print_buffer(const uint8_t *data, size_t length) {
|
||||
}
|
||||
}
|
||||
|
||||
static const char *const REG_TO_STR_P0[16] = {"GENA", "GRST", "GMUT", "SPAGE", "SCR", "LCR", "FCR", "SIER",
|
||||
"SIFR", "TFCNT", "RFCNT", "FSR", "LSR", "FDAT", "FWCR", "RS485"};
|
||||
static const char *const REG_TO_STR_P1[16] = {"GENA", "GRST", "GMUT", "SPAGE", "BAUD1", "BAUD0", "PRES", "RFTL",
|
||||
"TFTL", "FWTH", "FWTL", "XON1", "XOFF1", "SADR", "SAEN", "RTSDLY"};
|
||||
using namespace weikai;
|
||||
// method to print a register value as text: used in the log messages ...
|
||||
const char *reg_to_str(int reg, bool page1) {
|
||||
if (reg == WKREG_GPDAT) {
|
||||
return "GPDAT";
|
||||
} else if (reg == WKREG_GPDIR) {
|
||||
return "GPDIR";
|
||||
} else {
|
||||
return page1 ? REG_TO_STR_P1[reg & 0x0F] : REG_TO_STR_P0[reg & 0x0F];
|
||||
}
|
||||
}
|
||||
enum RegType { REG = 0, FIFO = 1 }; ///< Register or FIFO
|
||||
|
||||
/// @brief Computes the I²C bus's address used to access the component
|
||||
@@ -72,11 +58,11 @@ uint8_t WeikaiRegisterI2C::read_reg() const {
|
||||
if (error == i2c::NO_ERROR) {
|
||||
this->comp_->status_clear_warning();
|
||||
ESP_LOGVV(TAG, "WeikaiRegisterI2C::read_reg() @%02X reg=%s ch=%u I2C_code:%d, buf=%02X", address,
|
||||
reg_to_str(this->register_, comp_i2c->page1()), this->channel_, (int) error, value);
|
||||
LOG_STR_ARG(reg_to_str(this->register_, comp_i2c->page1())), this->channel_, (int) error, value);
|
||||
} else { // error
|
||||
this->comp_->status_set_warning();
|
||||
ESP_LOGE(TAG, "WeikaiRegisterI2C::read_reg() @%02X reg=%s ch=%u I2C_code:%d, buf=%02X", address,
|
||||
reg_to_str(this->register_, comp_i2c->page1()), this->channel_, (int) error, value);
|
||||
LOG_STR_ARG(reg_to_str(this->register_, comp_i2c->page1())), this->channel_, (int) error, value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
@@ -108,11 +94,11 @@ void WeikaiRegisterI2C::write_reg(uint8_t value) {
|
||||
if (error == i2c::NO_ERROR) {
|
||||
this->comp_->status_clear_warning();
|
||||
ESP_LOGVV(TAG, "WK2168Reg::write_reg() @%02X reg=%s ch=%d I2C_code:%d buf=%02X", address,
|
||||
reg_to_str(this->register_, comp_i2c->page1()), this->channel_, (int) error, value);
|
||||
LOG_STR_ARG(reg_to_str(this->register_, comp_i2c->page1())), this->channel_, (int) error, value);
|
||||
} else { // error
|
||||
this->comp_->status_set_warning();
|
||||
ESP_LOGE(TAG, "WK2168Reg::write_reg() @%02X reg=%s ch=%d I2C_code:%d buf=%d", address,
|
||||
reg_to_str(this->register_, comp_i2c->page1()), this->channel_, (int) error, value);
|
||||
LOG_STR_ARG(reg_to_str(this->register_, comp_i2c->page1())), this->channel_, (int) error, value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -52,22 +52,6 @@ void print_buffer(const uint8_t *data, size_t length) {
|
||||
}
|
||||
}
|
||||
|
||||
static const char *const REG_TO_STR_P0[16] = {"GENA", "GRST", "GMUT", "SPAGE", "SCR", "LCR", "FCR", "SIER",
|
||||
"SIFR", "TFCNT", "RFCNT", "FSR", "LSR", "FDAT", "FWCR", "RS485"};
|
||||
static const char *const REG_TO_STR_P1[16] = {"GENA", "GRST", "GMUT", "SPAGE", "BAUD1", "BAUD0", "PRES", "RFTL",
|
||||
"TFTL", "FWTH", "FWTL", "XON1", "XOFF1", "SADR", "SAEN", "RTSDLY"};
|
||||
|
||||
// method to print a register value as text: used in the log messages ...
|
||||
const char *reg_to_str(int reg, bool page1) {
|
||||
if (reg == WKREG_GPDAT) {
|
||||
return "GPDAT";
|
||||
} else if (reg == WKREG_GPDIR) {
|
||||
return "GPDIR";
|
||||
} else {
|
||||
return page1 ? REG_TO_STR_P1[reg & 0x0F] : REG_TO_STR_P0[reg & 0x0F];
|
||||
}
|
||||
}
|
||||
|
||||
enum RegType { REG = 0, FIFO = 1 }; ///< Register or FIFO
|
||||
enum CmdType { WRITE_CMD = 0, READ_CMD = 1 }; ///< Read or Write transfer
|
||||
|
||||
@@ -101,7 +85,7 @@ uint8_t WeikaiRegisterSPI::read_reg() const {
|
||||
spi_comp->disable();
|
||||
char bin_buf[9];
|
||||
ESP_LOGVV(TAG, "WeikaiRegisterSPI::read_reg() cmd=%s(%02X) reg=%s ch=%d buf=%02X", format_bin_to(bin_buf, cmd), cmd,
|
||||
reg_to_str(this->register_, this->comp_->page1()), this->channel_, val);
|
||||
LOG_STR_ARG(reg_to_str(this->register_, this->comp_->page1())), this->channel_, val);
|
||||
return val;
|
||||
}
|
||||
|
||||
@@ -128,7 +112,7 @@ void WeikaiRegisterSPI::write_reg(uint8_t value) {
|
||||
spi_comp->disable();
|
||||
char bin_buf[9];
|
||||
ESP_LOGVV(TAG, "WeikaiRegisterSPI::write_reg() cmd=%s(%02X) reg=%s ch=%d buf=%02X", format_bin_to(bin_buf, buf[0]),
|
||||
buf[0], reg_to_str(this->register_, this->comp_->page1()), this->channel_, buf[1]);
|
||||
buf[0], LOG_STR_ARG(reg_to_str(this->register_, this->comp_->page1())), this->channel_, buf[1]);
|
||||
}
|
||||
|
||||
void WeikaiRegisterSPI::write_fifo(uint8_t *data, size_t length) {
|
||||
|
||||
@@ -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)); \
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp8266:
|
||||
board: esp01_1m
|
||||
|
||||
sensor:
|
||||
- platform: template
|
||||
id: room_temp
|
||||
lambda: return 21.0;
|
||||
|
||||
climate:
|
||||
- platform: template
|
||||
id: climate_a
|
||||
name: Climate A
|
||||
optimistic: true
|
||||
sensor: room_temp
|
||||
supported_modes: ["OFF", HEAT]
|
||||
custom_fan_modes: [turbo, silent]
|
||||
custom_presets: [eco]
|
||||
- platform: template
|
||||
id: climate_b
|
||||
name: Climate B
|
||||
optimistic: true
|
||||
sensor: room_temp
|
||||
supported_modes: ["OFF", HEAT]
|
||||
custom_fan_modes: [turbo, silent]
|
||||
- platform: template
|
||||
id: climate_c
|
||||
name: Climate C
|
||||
optimistic: true
|
||||
sensor: room_temp
|
||||
supported_modes: ["OFF", HEAT]
|
||||
@@ -1,27 +0,0 @@
|
||||
"""Custom fan modes and presets from YAML live in shared flash tables."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
|
||||
def test_custom_modes_use_shared_tables(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
main_cpp = generate_main(component_config_path("custom_modes.yaml"))
|
||||
|
||||
fan_tables = re.findall(
|
||||
r"static constexpr const char \* (climate_custom_fan_modes\w*)\[\] PROGMEM",
|
||||
main_cpp,
|
||||
)
|
||||
assert len(fan_tables) == 1 # Climate A and B share one table
|
||||
assert (
|
||||
main_cpp.count(f"set_supported_custom_fan_modes_static({fan_tables[0]}, 2);")
|
||||
== 2
|
||||
)
|
||||
assert (
|
||||
"climate_a->set_supported_custom_presets_static(climate_custom_presets, 1);"
|
||||
in main_cpp
|
||||
)
|
||||
assert "climate_c->set_supported_custom" not in main_cpp
|
||||
@@ -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],
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/components/climate/climate.h"
|
||||
|
||||
namespace esphome::climate::testing {
|
||||
@@ -73,35 +70,4 @@ TEST(ClimateRestoreStateTest, KeepsRestoringTheOtherFieldsWhenTheModeIsDropped)
|
||||
EXPECT_EQ(climate.fan_mode, CLIMATE_FAN_HIGH);
|
||||
}
|
||||
|
||||
static constexpr const char *const FAN_TABLE[] = {"Turbo", "Silent"};
|
||||
|
||||
TEST(ClimateCustomModesTest, StaticTableIsViewedNotCopied) {
|
||||
TestClimate climate;
|
||||
climate.set_supported_custom_fan_modes_static(FAN_TABLE, 2);
|
||||
auto traits = climate.get_traits();
|
||||
ASSERT_EQ(traits.get_supported_custom_fan_modes().size(), 2u);
|
||||
EXPECT_EQ(traits.get_supported_custom_fan_modes().data(), FAN_TABLE);
|
||||
EXPECT_TRUE(traits.get_supports_fan_modes());
|
||||
EXPECT_TRUE(traits.supports_custom_fan_mode("Silent"));
|
||||
EXPECT_FALSE(traits.supports_custom_fan_mode("Quiet"));
|
||||
}
|
||||
|
||||
TEST(ClimateCustomModesTest, RuntimeListsAreCopiedAndReplaced) {
|
||||
TestClimate climate;
|
||||
std::vector<const char *> presets{"Eco", "Sleep"};
|
||||
climate.set_supported_custom_presets(presets);
|
||||
presets[0] = "Changed"; // the climate keeps its own copy
|
||||
EXPECT_STREQ(climate.get_traits().get_supported_custom_presets()[0], "Eco");
|
||||
climate.set_supported_custom_presets({"Night"}); // frees the previous copy
|
||||
ASSERT_EQ(climate.get_traits().get_supported_custom_presets().size(), 1u);
|
||||
EXPECT_STREQ(climate.get_traits().get_supported_custom_presets()[0], "Night");
|
||||
}
|
||||
|
||||
TEST(ClimateCustomModesTest, TraitsWithoutAClimateHaveEmptyLists) {
|
||||
ClimateTraits traits;
|
||||
EXPECT_TRUE(traits.get_supported_custom_fan_modes().empty());
|
||||
EXPECT_TRUE(traits.get_supported_custom_presets().empty());
|
||||
EXPECT_FALSE(traits.supports_custom_preset("Eco"));
|
||||
}
|
||||
|
||||
} // namespace esphome::climate::testing
|
||||
|
||||
@@ -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
|
||||
@@ -288,10 +288,6 @@ button:
|
||||
ESP_LOGD("test", "%s", preset);
|
||||
for (const char *mode : traits.get_supported_custom_fan_modes())
|
||||
ESP_LOGD("test", "%s", mode);
|
||||
if (fan_modes.size() > 1) {
|
||||
const auto *it = std::find(fan_modes.begin(), fan_modes.end(), fan_modes[1]);
|
||||
ESP_LOGD("test", "index %d", (int) (it - fan_modes.begin()));
|
||||
}
|
||||
std::vector<const char *> presets{"eco_plus", "max"};
|
||||
id(template_climate_custom_modes).set_supported_custom_presets(presets);
|
||||
id(template_climate_custom_modes).set_supported_custom_presets({"eco_plus"});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
"""Legacy climate component — tests deprecated ClimateTraits setters backward compat.
|
||||
|
||||
Remove this entire directory in 2026.11.0 when the deprecated setters are removed.
|
||||
"""
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
"""Legacy climate platform that uses deprecated ClimateTraits setters."""
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import climate
|
||||
import esphome.config_validation as cv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
legacy_climate_ns = cg.esphome_ns.namespace("legacy_climate_test")
|
||||
LegacyClimate = legacy_climate_ns.class_("LegacyClimate", climate.Climate, cg.Component)
|
||||
|
||||
CONFIG_SCHEMA = climate.climate_schema(LegacyClimate).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await climate.new_climate(config)
|
||||
await cg.register_component(var, config)
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/climate/climate.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
namespace esphome::legacy_climate_test {
|
||||
|
||||
/// Test climate that uses the DEPRECATED ClimateTraits setters for custom modes.
|
||||
/// This validates backward compatibility for external components that haven't migrated.
|
||||
class LegacyClimate : public climate::Climate, public Component {
|
||||
public:
|
||||
void setup() override {
|
||||
this->mode = climate::CLIMATE_MODE_OFF;
|
||||
this->target_temperature = 22.0f;
|
||||
this->current_temperature = 20.0f;
|
||||
this->publish_state();
|
||||
}
|
||||
|
||||
protected:
|
||||
climate::ClimateTraits traits() override {
|
||||
auto traits = climate::ClimateTraits();
|
||||
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
|
||||
traits.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_HEAT, climate::CLIMATE_MODE_COOL});
|
||||
traits.set_visual_min_temperature(16.0f);
|
||||
traits.set_visual_max_temperature(30.0f);
|
||||
traits.set_visual_temperature_step(0.5f);
|
||||
|
||||
// DEPRECATED API: setting custom modes directly on ClimateTraits.
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
traits.set_supported_custom_fan_modes({"Turbo", "Silent", "Auto"});
|
||||
traits.set_supported_custom_presets({"Eco Mode", "Night Mode"});
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
return traits;
|
||||
}
|
||||
|
||||
void control(const climate::ClimateCall &call) override {
|
||||
if (call.get_mode().has_value()) {
|
||||
this->mode = *call.get_mode();
|
||||
}
|
||||
if (call.get_target_temperature().has_value()) {
|
||||
this->target_temperature = *call.get_target_temperature();
|
||||
}
|
||||
if (call.has_custom_fan_mode()) {
|
||||
this->set_custom_fan_mode_(call.get_custom_fan_mode());
|
||||
}
|
||||
if (call.has_custom_preset()) {
|
||||
this->set_custom_preset_(call.get_custom_preset());
|
||||
}
|
||||
this->publish_state();
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace esphome::legacy_climate_test
|
||||
@@ -0,0 +1,18 @@
|
||||
esphome:
|
||||
name: legacy-climate-compat
|
||||
|
||||
host:
|
||||
api:
|
||||
logger:
|
||||
level: DEBUG
|
||||
|
||||
external_components:
|
||||
- source:
|
||||
type: local
|
||||
path: EXTERNAL_COMPONENT_PATH
|
||||
components: [legacy_climate_component]
|
||||
|
||||
climate:
|
||||
- platform: legacy_climate_component
|
||||
name: "Legacy Climate"
|
||||
id: legacy_climate
|
||||
@@ -66,6 +66,7 @@
|
||||
"tests/integration/test_ir_rf_transmit_complete.py": 10.73,
|
||||
"tests/integration/test_large_message_batching.py": 13.94,
|
||||
"tests/integration/test_legacy_area.py": 13.34,
|
||||
"tests/integration/test_legacy_climate_compat.py": 13.19,
|
||||
"tests/integration/test_legacy_fan_compat.py": 9.31,
|
||||
"tests/integration/test_light_automations.py": 14.39,
|
||||
"tests/integration/test_light_binary_effect_off_phase.py": 14.75,
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
"""Integration test for backward compatibility of deprecated ClimateTraits setters.
|
||||
|
||||
Verifies that external components using the old traits.set_supported_custom_fan_modes()
|
||||
and traits.set_supported_custom_presets() API still work correctly during the
|
||||
deprecation period.
|
||||
|
||||
Remove this entire test file and the legacy_climate_component external component
|
||||
in 2026.11.0 when the deprecated ClimateTraits setters are removed.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
import aioesphomeapi
|
||||
from aioesphomeapi import ClimateInfo
|
||||
import pytest
|
||||
|
||||
from .state_utils import InitialStateHelper
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_legacy_climate_compat(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test that deprecated ClimateTraits custom mode setters still work end-to-end."""
|
||||
external_components_path = str(
|
||||
Path(__file__).parent / "fixtures" / "external_components"
|
||||
)
|
||||
yaml_config = yaml_config.replace(
|
||||
"EXTERNAL_COMPONENT_PATH", external_components_path
|
||||
)
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
async with run_compiled(yaml_config), api_client_connected() as client:
|
||||
entities, _ = await client.list_entities_services()
|
||||
initial_state_helper = InitialStateHelper(entities)
|
||||
|
||||
climate_infos = [e for e in entities if isinstance(e, ClimateInfo)]
|
||||
assert len(climate_infos) == 1, (
|
||||
f"Expected 1 climate entity, got {len(climate_infos)}"
|
||||
)
|
||||
|
||||
test_climate = climate_infos[0]
|
||||
|
||||
# Verify custom fan modes set via deprecated ClimateTraits setter are exposed
|
||||
assert set(test_climate.supported_custom_fan_modes) == {
|
||||
"Turbo",
|
||||
"Silent",
|
||||
"Auto",
|
||||
}, (
|
||||
f"Expected custom fan modes {{Turbo, Silent, Auto}}, "
|
||||
f"got {test_climate.supported_custom_fan_modes}"
|
||||
)
|
||||
|
||||
# Verify custom presets set via deprecated ClimateTraits setter are exposed
|
||||
assert set(test_climate.supported_custom_presets) == {
|
||||
"Eco Mode",
|
||||
"Night Mode",
|
||||
}, (
|
||||
f"Expected custom presets {{Eco Mode, Night Mode}}, "
|
||||
f"got {test_climate.supported_custom_presets}"
|
||||
)
|
||||
|
||||
# Set up state tracking with InitialStateHelper
|
||||
turbo_future: asyncio.Future[aioesphomeapi.ClimateState] = loop.create_future()
|
||||
|
||||
def on_state(state: aioesphomeapi.EntityState) -> None:
|
||||
if (
|
||||
isinstance(state, aioesphomeapi.ClimateState)
|
||||
and state.custom_fan_mode == "Turbo"
|
||||
and not turbo_future.done()
|
||||
):
|
||||
turbo_future.set_result(state)
|
||||
|
||||
client.subscribe_states(initial_state_helper.on_state_wrapper(on_state))
|
||||
|
||||
try:
|
||||
await initial_state_helper.wait_for_initial_states()
|
||||
except TimeoutError:
|
||||
pytest.fail("Timeout waiting for initial states")
|
||||
|
||||
# Verify we can set a custom fan mode via API (tests find_custom_fan_mode_ compat path)
|
||||
client.climate_command(test_climate.key, custom_fan_mode="Turbo")
|
||||
|
||||
try:
|
||||
turbo_state = await asyncio.wait_for(turbo_future, timeout=5.0)
|
||||
except TimeoutError:
|
||||
pytest.fail("Custom fan mode 'Turbo' not received within 5 seconds")
|
||||
|
||||
assert turbo_state.custom_fan_mode == "Turbo"
|
||||
Reference in New Issue
Block a user