1
0
mirror of https://github.com/esphome/esphome.git synced 2025-10-27 21:23:48 +00:00

Merge branch 'climate_overhead' into integration

This commit is contained in:
J. Nick Koston
2025-10-22 16:25:35 -10:00
29 changed files with 373 additions and 232 deletions

View File

@@ -989,7 +989,7 @@ message ListEntitiesClimateResponse {
bool supports_current_temperature = 5; // Deprecated: use feature_flags bool supports_current_temperature = 5; // Deprecated: use feature_flags
bool supports_two_point_target_temperature = 6; // Deprecated: use feature_flags bool supports_two_point_target_temperature = 6; // Deprecated: use feature_flags
repeated ClimateMode supported_modes = 7 [(container_pointer) = "std::set<climate::ClimateMode>"]; repeated ClimateMode supported_modes = 7 [(container_pointer_no_template) = "climate::ClimateModeMask"];
float visual_min_temperature = 8; float visual_min_temperature = 8;
float visual_max_temperature = 9; float visual_max_temperature = 9;
float visual_target_temperature_step = 10; float visual_target_temperature_step = 10;
@@ -998,11 +998,11 @@ message ListEntitiesClimateResponse {
// Deprecated in API version 1.5 // Deprecated in API version 1.5
bool legacy_supports_away = 11 [deprecated=true]; bool legacy_supports_away = 11 [deprecated=true];
bool supports_action = 12; // Deprecated: use feature_flags bool supports_action = 12; // Deprecated: use feature_flags
repeated ClimateFanMode supported_fan_modes = 13 [(container_pointer) = "std::set<climate::ClimateFanMode>"]; repeated ClimateFanMode supported_fan_modes = 13 [(container_pointer_no_template) = "climate::ClimateFanModeMask"];
repeated ClimateSwingMode supported_swing_modes = 14 [(container_pointer) = "std::set<climate::ClimateSwingMode>"]; repeated ClimateSwingMode supported_swing_modes = 14 [(container_pointer_no_template) = "climate::ClimateSwingModeMask"];
repeated string supported_custom_fan_modes = 15 [(container_pointer) = "std::set"]; repeated string supported_custom_fan_modes = 15 [(container_pointer) = "std::vector"];
repeated ClimatePreset supported_presets = 16 [(container_pointer) = "std::set<climate::ClimatePreset>"]; repeated ClimatePreset supported_presets = 16 [(container_pointer_no_template) = "climate::ClimatePresetMask"];
repeated string supported_custom_presets = 17 [(container_pointer) = "std::set"]; repeated string supported_custom_presets = 17 [(container_pointer) = "std::vector"];
bool disabled_by_default = 18; bool disabled_by_default = 18;
string icon = 19 [(field_ifdef) = "USE_ENTITY_ICON"]; string icon = 19 [(field_ifdef) = "USE_ENTITY_ICON"];
EntityCategory entity_category = 20; EntityCategory entity_category = 20;

View File

@@ -669,18 +669,18 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection
msg.supports_action = traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION); msg.supports_action = traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION);
// Current feature flags and other supported parameters // Current feature flags and other supported parameters
msg.feature_flags = traits.get_feature_flags(); msg.feature_flags = traits.get_feature_flags();
msg.supported_modes = &traits.get_supported_modes_for_api_(); msg.supported_modes = &traits.get_supported_modes();
msg.visual_min_temperature = traits.get_visual_min_temperature(); msg.visual_min_temperature = traits.get_visual_min_temperature();
msg.visual_max_temperature = traits.get_visual_max_temperature(); msg.visual_max_temperature = traits.get_visual_max_temperature();
msg.visual_target_temperature_step = traits.get_visual_target_temperature_step(); msg.visual_target_temperature_step = traits.get_visual_target_temperature_step();
msg.visual_current_temperature_step = traits.get_visual_current_temperature_step(); msg.visual_current_temperature_step = traits.get_visual_current_temperature_step();
msg.visual_min_humidity = traits.get_visual_min_humidity(); msg.visual_min_humidity = traits.get_visual_min_humidity();
msg.visual_max_humidity = traits.get_visual_max_humidity(); msg.visual_max_humidity = traits.get_visual_max_humidity();
msg.supported_fan_modes = &traits.get_supported_fan_modes_for_api_(); msg.supported_fan_modes = &traits.get_supported_fan_modes();
msg.supported_custom_fan_modes = &traits.get_supported_custom_fan_modes_for_api_(); msg.supported_custom_fan_modes = &traits.get_supported_custom_fan_modes();
msg.supported_presets = &traits.get_supported_presets_for_api_(); msg.supported_presets = &traits.get_supported_presets();
msg.supported_custom_presets = &traits.get_supported_custom_presets_for_api_(); msg.supported_custom_presets = &traits.get_supported_custom_presets();
msg.supported_swing_modes = &traits.get_supported_swing_modes_for_api_(); msg.supported_swing_modes = &traits.get_supported_swing_modes();
return fill_and_encode_entity_info(climate, msg, ListEntitiesClimateResponse::MESSAGE_TYPE, conn, remaining_size, return fill_and_encode_entity_info(climate, msg, ListEntitiesClimateResponse::MESSAGE_TYPE, conn, remaining_size,
is_single); is_single);
} }

View File

@@ -1377,16 +1377,16 @@ class ListEntitiesClimateResponse final : public InfoResponseProtoMessage {
#endif #endif
bool supports_current_temperature{false}; bool supports_current_temperature{false};
bool supports_two_point_target_temperature{false}; bool supports_two_point_target_temperature{false};
const std::set<climate::ClimateMode> *supported_modes{}; const climate::ClimateModeMask *supported_modes{};
float visual_min_temperature{0.0f}; float visual_min_temperature{0.0f};
float visual_max_temperature{0.0f}; float visual_max_temperature{0.0f};
float visual_target_temperature_step{0.0f}; float visual_target_temperature_step{0.0f};
bool supports_action{false}; bool supports_action{false};
const std::set<climate::ClimateFanMode> *supported_fan_modes{}; const climate::ClimateFanModeMask *supported_fan_modes{};
const std::set<climate::ClimateSwingMode> *supported_swing_modes{}; const climate::ClimateSwingModeMask *supported_swing_modes{};
const std::set<std::string> *supported_custom_fan_modes{}; const std::vector<std::string> *supported_custom_fan_modes{};
const std::set<climate::ClimatePreset> *supported_presets{}; const climate::ClimatePresetMask *supported_presets{};
const std::set<std::string> *supported_custom_presets{}; const std::vector<std::string> *supported_custom_presets{};
float visual_current_temperature_step{0.0f}; float visual_current_temperature_step{0.0f};
bool supports_current_humidity{false}; bool supports_current_humidity{false};
bool supports_target_humidity{false}; bool supports_target_humidity{false};

View File

@@ -99,9 +99,8 @@ enum BedjetCommand : uint8_t {
static const uint8_t BEDJET_FAN_SPEED_COUNT = 20; static const uint8_t BEDJET_FAN_SPEED_COUNT = 20;
static const char *const BEDJET_FAN_STEP_NAMES[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_; static constexpr const char *const BEDJET_FAN_STEP_NAMES[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_;
static const std::string BEDJET_FAN_STEP_NAME_STRINGS[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_; static const std::string BEDJET_FAN_STEP_NAME_STRINGS[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_;
static const std::set<std::string> BEDJET_FAN_STEP_NAMES_SET BEDJET_FAN_STEP_NAMES_;
} // namespace bedjet } // namespace bedjet
} // namespace esphome } // namespace esphome

View File

@@ -43,7 +43,7 @@ class BedJetClimate : public climate::Climate, public BedJetClient, public Polli
}); });
// It would be better if we had a slider for the fan modes. // It would be better if we had a slider for the fan modes.
traits.set_supported_custom_fan_modes(BEDJET_FAN_STEP_NAMES_SET); traits.set_supported_custom_fan_modes(BEDJET_FAN_STEP_NAMES);
traits.set_supported_presets({ traits.set_supported_presets({
// If we support NONE, then have to decide what happens if the user switches to it (turn off?) // If we support NONE, then have to decide what happens if the user switches to it (turn off?)
// climate::CLIMATE_PRESET_NONE, // climate::CLIMATE_PRESET_NONE,

View File

@@ -385,7 +385,7 @@ void Climate::save_state_() {
if (!traits.get_supported_custom_fan_modes().empty() && custom_fan_mode.has_value()) { if (!traits.get_supported_custom_fan_modes().empty() && custom_fan_mode.has_value()) {
state.uses_custom_fan_mode = true; state.uses_custom_fan_mode = true;
const auto &supported = traits.get_supported_custom_fan_modes(); const auto &supported = traits.get_supported_custom_fan_modes();
// std::set has consistent order (lexicographic for strings) // std::vector maintains insertion order
size_t i = 0; size_t i = 0;
for (const auto &mode : supported) { for (const auto &mode : supported) {
if (mode == custom_fan_mode) { if (mode == custom_fan_mode) {
@@ -402,7 +402,7 @@ void Climate::save_state_() {
if (!traits.get_supported_custom_presets().empty() && custom_preset.has_value()) { if (!traits.get_supported_custom_presets().empty() && custom_preset.has_value()) {
state.uses_custom_preset = true; state.uses_custom_preset = true;
const auto &supported = traits.get_supported_custom_presets(); const auto &supported = traits.get_supported_custom_presets();
// std::set has consistent order (lexicographic for strings) // std::vector maintains insertion order
size_t i = 0; size_t i = 0;
for (const auto &preset : supported) { for (const auto &preset : supported) {
if (preset == custom_preset) { if (preset == custom_preset) {

View File

@@ -7,6 +7,7 @@ namespace esphome {
namespace climate { namespace climate {
/// Enum for all modes a climate device can be in. /// Enum for all modes a climate device can be in.
/// NOTE: If adding values, update ClimateModeMask in climate_traits.h to use the new last value
enum ClimateMode : uint8_t { enum ClimateMode : uint8_t {
/// The climate device is off /// The climate device is off
CLIMATE_MODE_OFF = 0, CLIMATE_MODE_OFF = 0,
@@ -24,7 +25,7 @@ enum ClimateMode : uint8_t {
* For example, the target temperature can be adjusted based on a schedule, or learned behavior. * For example, the target temperature can be adjusted based on a schedule, or learned behavior.
* The target temperature can't be adjusted when in this mode. * The target temperature can't be adjusted when in this mode.
*/ */
CLIMATE_MODE_AUTO = 6 CLIMATE_MODE_AUTO = 6 // Update ClimateModeMask in climate_traits.h if adding values after this
}; };
/// Enum for the current action of the climate device. Values match those of ClimateMode. /// Enum for the current action of the climate device. Values match those of ClimateMode.
@@ -43,6 +44,7 @@ enum ClimateAction : uint8_t {
CLIMATE_ACTION_FAN = 6, CLIMATE_ACTION_FAN = 6,
}; };
/// NOTE: If adding values, update ClimateFanModeMask in climate_traits.h to use the new last value
enum ClimateFanMode : uint8_t { enum ClimateFanMode : uint8_t {
/// The fan mode is set to On /// The fan mode is set to On
CLIMATE_FAN_ON = 0, CLIMATE_FAN_ON = 0,
@@ -63,10 +65,11 @@ enum ClimateFanMode : uint8_t {
/// The fan mode is set to Diffuse /// The fan mode is set to Diffuse
CLIMATE_FAN_DIFFUSE = 8, CLIMATE_FAN_DIFFUSE = 8,
/// The fan mode is set to Quiet /// The fan mode is set to Quiet
CLIMATE_FAN_QUIET = 9, CLIMATE_FAN_QUIET = 9, // Update ClimateFanModeMask in climate_traits.h if adding values after this
}; };
/// Enum for all modes a climate swing can be in /// Enum for all modes a climate swing can be in
/// NOTE: If adding values, update ClimateSwingModeMask in climate_traits.h to use the new last value
enum ClimateSwingMode : uint8_t { enum ClimateSwingMode : uint8_t {
/// The swing mode is set to Off /// The swing mode is set to Off
CLIMATE_SWING_OFF = 0, CLIMATE_SWING_OFF = 0,
@@ -75,10 +78,11 @@ enum ClimateSwingMode : uint8_t {
/// The fan mode is set to Vertical /// The fan mode is set to Vertical
CLIMATE_SWING_VERTICAL = 2, CLIMATE_SWING_VERTICAL = 2,
/// The fan mode is set to Horizontal /// The fan mode is set to Horizontal
CLIMATE_SWING_HORIZONTAL = 3, CLIMATE_SWING_HORIZONTAL = 3, // Update ClimateSwingModeMask in climate_traits.h if adding values after this
}; };
/// Enum for all preset modes /// Enum for all preset modes
/// NOTE: If adding values, update ClimatePresetMask in climate_traits.h to use the new last value
enum ClimatePreset : uint8_t { enum ClimatePreset : uint8_t {
/// No preset is active /// No preset is active
CLIMATE_PRESET_NONE = 0, CLIMATE_PRESET_NONE = 0,
@@ -95,7 +99,7 @@ enum ClimatePreset : uint8_t {
/// Device is prepared for sleep /// Device is prepared for sleep
CLIMATE_PRESET_SLEEP = 6, CLIMATE_PRESET_SLEEP = 6,
/// Device is reacting to activity (e.g., movement sensors) /// Device is reacting to activity (e.g., movement sensors)
CLIMATE_PRESET_ACTIVITY = 7, CLIMATE_PRESET_ACTIVITY = 7, // Update ClimatePresetMask in climate_traits.h if adding values after this
}; };
enum ClimateFeature : uint32_t { enum ClimateFeature : uint32_t {

View File

@@ -1,19 +1,33 @@
#pragma once #pragma once
#include <set> #include <vector>
#include "climate_mode.h" #include "climate_mode.h"
#include "esphome/core/finite_set_mask.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
namespace esphome { namespace esphome {
#ifdef USE_API
namespace api {
class APIConnection;
} // namespace api
#endif
namespace climate { namespace climate {
// Type aliases for climate enum bitmasks
// These replace std::set<EnumType> to eliminate red-black tree overhead
// For contiguous enums starting at 0, DefaultBitPolicy provides 1:1 mapping (enum value = bit position)
// Bitmask size is automatically calculated from the last enum value
using ClimateModeMask = FiniteSetMask<ClimateMode, DefaultBitPolicy<ClimateMode, CLIMATE_MODE_AUTO + 1>>;
using ClimateFanModeMask = FiniteSetMask<ClimateFanMode, DefaultBitPolicy<ClimateFanMode, CLIMATE_FAN_QUIET + 1>>;
using ClimateSwingModeMask =
FiniteSetMask<ClimateSwingMode, DefaultBitPolicy<ClimateSwingMode, CLIMATE_SWING_HORIZONTAL + 1>>;
using ClimatePresetMask = FiniteSetMask<ClimatePreset, DefaultBitPolicy<ClimatePreset, CLIMATE_PRESET_ACTIVITY + 1>>;
// Lightweight linear search for small vectors (1-20 items)
// Avoids std::find template overhead
template<typename T> inline bool vector_contains(const std::vector<T> &vec, const T &value) {
for (const auto &item : vec) {
if (item == value)
return true;
}
return false;
}
/** This class contains all static data for climate devices. /** This class contains all static data for climate devices.
* *
* All climate devices must support these features: * All climate devices must support these features:
@@ -107,48 +121,60 @@ class ClimateTraits {
} }
} }
void set_supported_modes(std::set<ClimateMode> modes) { this->supported_modes_ = std::move(modes); } void set_supported_modes(ClimateModeMask modes) { this->supported_modes_ = modes; }
void add_supported_mode(ClimateMode mode) { this->supported_modes_.insert(mode); } void add_supported_mode(ClimateMode mode) { this->supported_modes_.insert(mode); }
bool supports_mode(ClimateMode mode) const { return this->supported_modes_.count(mode); } bool supports_mode(ClimateMode mode) const { return this->supported_modes_.count(mode); }
const std::set<ClimateMode> &get_supported_modes() const { return this->supported_modes_; } const ClimateModeMask &get_supported_modes() const { return this->supported_modes_; }
void set_supported_fan_modes(std::set<ClimateFanMode> modes) { this->supported_fan_modes_ = std::move(modes); } void set_supported_fan_modes(ClimateFanModeMask modes) { this->supported_fan_modes_ = modes; }
void add_supported_fan_mode(ClimateFanMode mode) { this->supported_fan_modes_.insert(mode); } void add_supported_fan_mode(ClimateFanMode mode) { this->supported_fan_modes_.insert(mode); }
void add_supported_custom_fan_mode(const std::string &mode) { this->supported_custom_fan_modes_.insert(mode); } void add_supported_custom_fan_mode(const std::string &mode) { this->supported_custom_fan_modes_.push_back(mode); }
bool supports_fan_mode(ClimateFanMode fan_mode) const { return this->supported_fan_modes_.count(fan_mode); } bool supports_fan_mode(ClimateFanMode fan_mode) const { return this->supported_fan_modes_.count(fan_mode); }
bool get_supports_fan_modes() const { bool get_supports_fan_modes() const {
return !this->supported_fan_modes_.empty() || !this->supported_custom_fan_modes_.empty(); return !this->supported_fan_modes_.empty() || !this->supported_custom_fan_modes_.empty();
} }
const std::set<ClimateFanMode> &get_supported_fan_modes() const { return this->supported_fan_modes_; } const ClimateFanModeMask &get_supported_fan_modes() const { return this->supported_fan_modes_; }
void set_supported_custom_fan_modes(std::set<std::string> supported_custom_fan_modes) { void set_supported_custom_fan_modes(std::vector<std::string> supported_custom_fan_modes) {
this->supported_custom_fan_modes_ = std::move(supported_custom_fan_modes); this->supported_custom_fan_modes_ = std::move(supported_custom_fan_modes);
} }
const std::set<std::string> &get_supported_custom_fan_modes() const { return this->supported_custom_fan_modes_; } void set_supported_custom_fan_modes(std::initializer_list<std::string> modes) {
this->supported_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(modes, modes + N);
}
const std::vector<std::string> &get_supported_custom_fan_modes() const { return this->supported_custom_fan_modes_; }
bool supports_custom_fan_mode(const std::string &custom_fan_mode) const { bool supports_custom_fan_mode(const std::string &custom_fan_mode) const {
return this->supported_custom_fan_modes_.count(custom_fan_mode); return vector_contains(this->supported_custom_fan_modes_, custom_fan_mode);
} }
void set_supported_presets(std::set<ClimatePreset> presets) { this->supported_presets_ = std::move(presets); } void set_supported_presets(ClimatePresetMask presets) { this->supported_presets_ = presets; }
void add_supported_preset(ClimatePreset preset) { this->supported_presets_.insert(preset); } void add_supported_preset(ClimatePreset preset) { this->supported_presets_.insert(preset); }
void add_supported_custom_preset(const std::string &preset) { this->supported_custom_presets_.insert(preset); } void add_supported_custom_preset(const std::string &preset) { this->supported_custom_presets_.push_back(preset); }
bool supports_preset(ClimatePreset preset) const { return this->supported_presets_.count(preset); } bool supports_preset(ClimatePreset preset) const { return this->supported_presets_.count(preset); }
bool get_supports_presets() const { return !this->supported_presets_.empty(); } bool get_supports_presets() const { return !this->supported_presets_.empty(); }
const std::set<climate::ClimatePreset> &get_supported_presets() const { return this->supported_presets_; } const ClimatePresetMask &get_supported_presets() const { return this->supported_presets_; }
void set_supported_custom_presets(std::set<std::string> supported_custom_presets) { void set_supported_custom_presets(std::vector<std::string> supported_custom_presets) {
this->supported_custom_presets_ = std::move(supported_custom_presets); this->supported_custom_presets_ = std::move(supported_custom_presets);
} }
const std::set<std::string> &get_supported_custom_presets() const { return this->supported_custom_presets_; } void set_supported_custom_presets(std::initializer_list<std::string> presets) {
this->supported_custom_presets_ = presets;
}
template<size_t N> void set_supported_custom_presets(const char *const (&presets)[N]) {
this->supported_custom_presets_.assign(presets, presets + N);
}
const std::vector<std::string> &get_supported_custom_presets() const { return this->supported_custom_presets_; }
bool supports_custom_preset(const std::string &custom_preset) const { bool supports_custom_preset(const std::string &custom_preset) const {
return this->supported_custom_presets_.count(custom_preset); return vector_contains(this->supported_custom_presets_, custom_preset);
} }
void set_supported_swing_modes(std::set<ClimateSwingMode> modes) { this->supported_swing_modes_ = std::move(modes); } void set_supported_swing_modes(ClimateSwingModeMask modes) { this->supported_swing_modes_ = modes; }
void add_supported_swing_mode(ClimateSwingMode mode) { this->supported_swing_modes_.insert(mode); } void add_supported_swing_mode(ClimateSwingMode mode) { this->supported_swing_modes_.insert(mode); }
bool supports_swing_mode(ClimateSwingMode swing_mode) const { return this->supported_swing_modes_.count(swing_mode); } bool supports_swing_mode(ClimateSwingMode swing_mode) const { return this->supported_swing_modes_.count(swing_mode); }
bool get_supports_swing_modes() const { return !this->supported_swing_modes_.empty(); } bool get_supports_swing_modes() const { return !this->supported_swing_modes_.empty(); }
const std::set<ClimateSwingMode> &get_supported_swing_modes() const { return this->supported_swing_modes_; } const ClimateSwingModeMask &get_supported_swing_modes() const { return this->supported_swing_modes_; }
float get_visual_min_temperature() const { return this->visual_min_temperature_; } float get_visual_min_temperature() const { return this->visual_min_temperature_; }
void set_visual_min_temperature(float visual_min_temperature) { void set_visual_min_temperature(float visual_min_temperature) {
@@ -179,23 +205,6 @@ class ClimateTraits {
void set_visual_max_humidity(float visual_max_humidity) { this->visual_max_humidity_ = visual_max_humidity; } void set_visual_max_humidity(float visual_max_humidity) { this->visual_max_humidity_ = visual_max_humidity; }
protected: protected:
#ifdef USE_API
// The API connection is a friend class to access internal methods
friend class api::APIConnection;
// These methods return references to internal data structures.
// They are used by the API to avoid copying data when encoding messages.
// Warning: Do not use these methods outside of the API connection code.
// They return references to internal data that can be invalidated.
const std::set<ClimateMode> &get_supported_modes_for_api_() const { return this->supported_modes_; }
const std::set<ClimateFanMode> &get_supported_fan_modes_for_api_() const { return this->supported_fan_modes_; }
const std::set<std::string> &get_supported_custom_fan_modes_for_api_() const {
return this->supported_custom_fan_modes_;
}
const std::set<climate::ClimatePreset> &get_supported_presets_for_api_() const { return this->supported_presets_; }
const std::set<std::string> &get_supported_custom_presets_for_api_() const { return this->supported_custom_presets_; }
const std::set<ClimateSwingMode> &get_supported_swing_modes_for_api_() const { return this->supported_swing_modes_; }
#endif
void set_mode_support_(climate::ClimateMode mode, bool supported) { void set_mode_support_(climate::ClimateMode mode, bool supported) {
if (supported) { if (supported) {
this->supported_modes_.insert(mode); this->supported_modes_.insert(mode);
@@ -226,12 +235,12 @@ class ClimateTraits {
float visual_min_humidity_{30}; float visual_min_humidity_{30};
float visual_max_humidity_{99}; float visual_max_humidity_{99};
std::set<climate::ClimateMode> supported_modes_ = {climate::CLIMATE_MODE_OFF}; climate::ClimateModeMask supported_modes_{climate::CLIMATE_MODE_OFF};
std::set<climate::ClimateFanMode> supported_fan_modes_; climate::ClimateFanModeMask supported_fan_modes_;
std::set<climate::ClimateSwingMode> supported_swing_modes_; climate::ClimateSwingModeMask supported_swing_modes_;
std::set<climate::ClimatePreset> supported_presets_; climate::ClimatePresetMask supported_presets_;
std::set<std::string> supported_custom_fan_modes_; std::vector<std::string> supported_custom_fan_modes_;
std::set<std::string> supported_custom_presets_; std::vector<std::string> supported_custom_presets_;
}; };
} // namespace climate } // namespace climate

View File

@@ -24,16 +24,18 @@ class ClimateIR : public Component,
public remote_base::RemoteTransmittable { public remote_base::RemoteTransmittable {
public: public:
ClimateIR(float minimum_temperature, float maximum_temperature, float temperature_step = 1.0f, ClimateIR(float minimum_temperature, float maximum_temperature, float temperature_step = 1.0f,
bool supports_dry = false, bool supports_fan_only = false, std::set<climate::ClimateFanMode> fan_modes = {}, bool supports_dry = false, bool supports_fan_only = false,
std::set<climate::ClimateSwingMode> swing_modes = {}, std::set<climate::ClimatePreset> presets = {}) { climate::ClimateFanModeMask fan_modes = climate::ClimateFanModeMask(),
climate::ClimateSwingModeMask swing_modes = climate::ClimateSwingModeMask(),
climate::ClimatePresetMask presets = climate::ClimatePresetMask()) {
this->minimum_temperature_ = minimum_temperature; this->minimum_temperature_ = minimum_temperature;
this->maximum_temperature_ = maximum_temperature; this->maximum_temperature_ = maximum_temperature;
this->temperature_step_ = temperature_step; this->temperature_step_ = temperature_step;
this->supports_dry_ = supports_dry; this->supports_dry_ = supports_dry;
this->supports_fan_only_ = supports_fan_only; this->supports_fan_only_ = supports_fan_only;
this->fan_modes_ = std::move(fan_modes); this->fan_modes_ = fan_modes;
this->swing_modes_ = std::move(swing_modes); this->swing_modes_ = swing_modes;
this->presets_ = std::move(presets); this->presets_ = presets;
} }
void setup() override; void setup() override;
@@ -60,9 +62,9 @@ class ClimateIR : public Component,
bool supports_heat_{true}; bool supports_heat_{true};
bool supports_dry_{false}; bool supports_dry_{false};
bool supports_fan_only_{false}; bool supports_fan_only_{false};
std::set<climate::ClimateFanMode> fan_modes_ = {}; climate::ClimateFanModeMask fan_modes_{};
std::set<climate::ClimateSwingMode> swing_modes_ = {}; climate::ClimateSwingModeMask swing_modes_{};
std::set<climate::ClimatePreset> presets_ = {}; climate::ClimatePresetMask presets_{};
sensor::Sensor *sensor_{nullptr}; sensor::Sensor *sensor_{nullptr};
}; };

View File

@@ -171,7 +171,7 @@ void HaierClimateBase::toggle_power() {
PendingAction({ActionRequest::TOGGLE_POWER, esphome::optional<haier_protocol::HaierMessage>()}); PendingAction({ActionRequest::TOGGLE_POWER, esphome::optional<haier_protocol::HaierMessage>()});
} }
void HaierClimateBase::set_supported_swing_modes(const std::set<climate::ClimateSwingMode> &modes) { void HaierClimateBase::set_supported_swing_modes(climate::ClimateSwingModeMask modes) {
this->traits_.set_supported_swing_modes(modes); this->traits_.set_supported_swing_modes(modes);
if (!modes.empty()) if (!modes.empty())
this->traits_.add_supported_swing_mode(climate::CLIMATE_SWING_OFF); this->traits_.add_supported_swing_mode(climate::CLIMATE_SWING_OFF);
@@ -179,13 +179,13 @@ void HaierClimateBase::set_supported_swing_modes(const std::set<climate::Climate
void HaierClimateBase::set_answer_timeout(uint32_t timeout) { this->haier_protocol_.set_answer_timeout(timeout); } void HaierClimateBase::set_answer_timeout(uint32_t timeout) { this->haier_protocol_.set_answer_timeout(timeout); }
void HaierClimateBase::set_supported_modes(const std::set<climate::ClimateMode> &modes) { void HaierClimateBase::set_supported_modes(climate::ClimateModeMask modes) {
this->traits_.set_supported_modes(modes); this->traits_.set_supported_modes(modes);
this->traits_.add_supported_mode(climate::CLIMATE_MODE_OFF); // Always available this->traits_.add_supported_mode(climate::CLIMATE_MODE_OFF); // Always available
this->traits_.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL); // Always available this->traits_.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL); // Always available
} }
void HaierClimateBase::set_supported_presets(const std::set<climate::ClimatePreset> &presets) { void HaierClimateBase::set_supported_presets(climate::ClimatePresetMask presets) {
this->traits_.set_supported_presets(presets); this->traits_.set_supported_presets(presets);
if (!presets.empty()) if (!presets.empty())
this->traits_.add_supported_preset(climate::CLIMATE_PRESET_NONE); this->traits_.add_supported_preset(climate::CLIMATE_PRESET_NONE);

View File

@@ -1,7 +1,6 @@
#pragma once #pragma once
#include <chrono> #include <chrono>
#include <set>
#include "esphome/components/climate/climate.h" #include "esphome/components/climate/climate.h"
#include "esphome/components/uart/uart.h" #include "esphome/components/uart/uart.h"
#include "esphome/core/automation.h" #include "esphome/core/automation.h"
@@ -60,9 +59,9 @@ class HaierClimateBase : public esphome::Component,
void send_power_off_command(); void send_power_off_command();
void toggle_power(); void toggle_power();
void reset_protocol() { this->reset_protocol_request_ = true; }; void reset_protocol() { this->reset_protocol_request_ = true; };
void set_supported_modes(const std::set<esphome::climate::ClimateMode> &modes); void set_supported_modes(esphome::climate::ClimateModeMask modes);
void set_supported_swing_modes(const std::set<esphome::climate::ClimateSwingMode> &modes); void set_supported_swing_modes(esphome::climate::ClimateSwingModeMask modes);
void set_supported_presets(const std::set<esphome::climate::ClimatePreset> &presets); void set_supported_presets(esphome::climate::ClimatePresetMask presets);
bool valid_connection() const { return this->protocol_phase_ >= ProtocolPhases::IDLE; }; bool valid_connection() const { return this->protocol_phase_ >= ProtocolPhases::IDLE; };
size_t available() noexcept override { return esphome::uart::UARTDevice::available(); }; size_t available() noexcept override { return esphome::uart::UARTDevice::available(); };
size_t read_array(uint8_t *data, size_t len) noexcept override { size_t read_array(uint8_t *data, size_t len) noexcept override {

View File

@@ -1033,9 +1033,9 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *
{ {
// Swing mode // Swing mode
ClimateSwingMode old_swing_mode = this->swing_mode; ClimateSwingMode old_swing_mode = this->swing_mode;
const std::set<ClimateSwingMode> &swing_modes = traits_.get_supported_swing_modes(); const auto &swing_modes = traits_.get_supported_swing_modes();
bool vertical_swing_supported = swing_modes.find(CLIMATE_SWING_VERTICAL) != swing_modes.end(); bool vertical_swing_supported = swing_modes.count(CLIMATE_SWING_VERTICAL);
bool horizontal_swing_supported = swing_modes.find(CLIMATE_SWING_HORIZONTAL) != swing_modes.end(); bool horizontal_swing_supported = swing_modes.count(CLIMATE_SWING_HORIZONTAL);
if (horizontal_swing_supported && if (horizontal_swing_supported &&
(packet.control.horizontal_swing_mode == (uint8_t) hon_protocol::HorizontalSwingMode::AUTO)) { (packet.control.horizontal_swing_mode == (uint8_t) hon_protocol::HorizontalSwingMode::AUTO)) {
if (vertical_swing_supported && if (vertical_swing_supported &&
@@ -1218,13 +1218,13 @@ void HonClimate::fill_control_messages_queue_() {
(uint8_t) hon_protocol::DataParameters::QUIET_MODE, (uint8_t) hon_protocol::DataParameters::QUIET_MODE,
quiet_mode_buf, 2); quiet_mode_buf, 2);
} }
if ((fast_mode_buf[1] != 0xFF) && ((presets.find(climate::ClimatePreset::CLIMATE_PRESET_BOOST) != presets.end()))) { if ((fast_mode_buf[1] != 0xFF) && presets.count(climate::ClimatePreset::CLIMATE_PRESET_BOOST)) {
this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL, this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL,
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
(uint8_t) hon_protocol::DataParameters::FAST_MODE, (uint8_t) hon_protocol::DataParameters::FAST_MODE,
fast_mode_buf, 2); fast_mode_buf, 2);
} }
if ((away_mode_buf[1] != 0xFF) && ((presets.find(climate::ClimatePreset::CLIMATE_PRESET_AWAY) != presets.end()))) { if ((away_mode_buf[1] != 0xFF) && presets.count(climate::ClimatePreset::CLIMATE_PRESET_AWAY)) {
this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL, this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL,
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
(uint8_t) hon_protocol::DataParameters::TEN_DEGREE, (uint8_t) hon_protocol::DataParameters::TEN_DEGREE,

View File

@@ -97,11 +97,10 @@ const float TEMP_MAX = 100; // Celsius
class HeatpumpIRClimate : public climate_ir::ClimateIR { class HeatpumpIRClimate : public climate_ir::ClimateIR {
public: public:
HeatpumpIRClimate() HeatpumpIRClimate()
: climate_ir::ClimateIR( : climate_ir::ClimateIR(TEMP_MIN, TEMP_MAX, 1.0f, true, true,
TEMP_MIN, TEMP_MAX, 1.0f, true, true, {climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH,
std::set<climate::ClimateFanMode>{climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_AUTO},
climate::CLIMATE_FAN_HIGH, climate::CLIMATE_FAN_AUTO}, {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_HORIZONTAL,
std::set<climate::ClimateSwingMode>{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_HORIZONTAL,
climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_BOTH}) {} climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_BOTH}) {}
void setup() override; void setup() override;
void set_protocol(Protocol protocol) { this->protocol_ = protocol; } void set_protocol(Protocol protocol) { this->protocol_ = protocol; }

View File

@@ -28,6 +28,38 @@ void ImprovSerialComponent::setup() {
} }
} }
void ImprovSerialComponent::loop() {
if (this->last_read_byte_ && (millis() - this->last_read_byte_ > IMPROV_SERIAL_TIMEOUT)) {
this->last_read_byte_ = 0;
this->rx_buffer_.clear();
ESP_LOGV(TAG, "Timeout");
}
auto byte = this->read_byte_();
while (byte.has_value()) {
if (this->parse_improv_serial_byte_(byte.value())) {
this->last_read_byte_ = millis();
} else {
this->last_read_byte_ = 0;
this->rx_buffer_.clear();
}
byte = this->read_byte_();
}
if (this->state_ == improv::STATE_PROVISIONING) {
if (wifi::global_wifi_component->is_connected()) {
wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(),
this->connecting_sta_.get_password());
this->connecting_sta_ = {};
this->cancel_timeout("wifi-connect-timeout");
this->set_state_(improv::STATE_PROVISIONED);
std::vector<uint8_t> url = this->build_rpc_settings_response_(improv::WIFI_SETTINGS);
this->send_response_(url);
}
}
}
void ImprovSerialComponent::dump_config() { ESP_LOGCONFIG(TAG, "Improv Serial:"); } void ImprovSerialComponent::dump_config() { ESP_LOGCONFIG(TAG, "Improv Serial:"); }
optional<uint8_t> ImprovSerialComponent::read_byte_() { optional<uint8_t> ImprovSerialComponent::read_byte_() {
@@ -78,8 +110,28 @@ optional<uint8_t> ImprovSerialComponent::read_byte_() {
return byte; return byte;
} }
void ImprovSerialComponent::write_data_(std::vector<uint8_t> &data) { void ImprovSerialComponent::write_data_(const uint8_t *data, const size_t size) {
data.push_back('\n'); // First, set length field
this->tx_header_[TX_LENGTH_IDX] = this->tx_header_[TX_TYPE_IDX] == TYPE_RPC_RESPONSE ? size : 1;
const bool there_is_data = data != nullptr && size > 0;
// If there_is_data, checksum must not include our optional data byte
const uint8_t header_checksum_len = there_is_data ? TX_BUFFER_SIZE - 3 : TX_BUFFER_SIZE - 2;
// Only transmit the full buffer length if there is no data (only state/error byte is provided in this case)
const uint8_t header_tx_len = there_is_data ? TX_BUFFER_SIZE - 3 : TX_BUFFER_SIZE;
// Calculate checksum for message
uint8_t checksum = 0;
for (uint8_t i = 0; i < header_checksum_len; i++) {
checksum += this->tx_header_[i];
}
if (there_is_data) {
// Include data in checksum
for (size_t i = 0; i < size; i++) {
checksum += data[i];
}
}
this->tx_header_[TX_CHECKSUM_IDX] = checksum;
#ifdef USE_ESP32 #ifdef USE_ESP32
switch (logger::global_logger->get_uart()) { switch (logger::global_logger->get_uart()) {
case logger::UART_SELECTION_UART0: case logger::UART_SELECTION_UART0:
@@ -87,63 +139,45 @@ void ImprovSerialComponent::write_data_(std::vector<uint8_t> &data) {
#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32C6) && \ #if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32C6) && \
!defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32S3) !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32S3)
case logger::UART_SELECTION_UART2: case logger::UART_SELECTION_UART2:
#endif // !USE_ESP32_VARIANT_ESP32C3 && !USE_ESP32_VARIANT_ESP32S2 && !USE_ESP32_VARIANT_ESP32S3 #endif
uart_write_bytes(this->uart_num_, data.data(), data.size()); uart_write_bytes(this->uart_num_, this->tx_header_, header_tx_len);
if (there_is_data) {
uart_write_bytes(this->uart_num_, data, size);
uart_write_bytes(this->uart_num_, &this->tx_header_[TX_CHECKSUM_IDX], 2); // Footer: checksum and newline
}
break; break;
#if defined(USE_LOGGER_USB_CDC) && defined(CONFIG_ESP_CONSOLE_USB_CDC) #if defined(USE_LOGGER_USB_CDC) && defined(CONFIG_ESP_CONSOLE_USB_CDC)
case logger::UART_SELECTION_USB_CDC: { case logger::UART_SELECTION_USB_CDC:
const char *msg = (char *) data.data(); esp_usb_console_write_buf((const char *) this->tx_header_, header_tx_len);
esp_usb_console_write_buf(msg, data.size()); if (there_is_data) {
break; esp_usb_console_write_buf((const char *) data, size);
esp_usb_console_write_buf((const char *) &this->tx_header_[TX_CHECKSUM_IDX],
2); // Footer: checksum and newline
} }
#endif // USE_LOGGER_USB_CDC break;
#endif
#ifdef USE_LOGGER_USB_SERIAL_JTAG #ifdef USE_LOGGER_USB_SERIAL_JTAG
case logger::UART_SELECTION_USB_SERIAL_JTAG: case logger::UART_SELECTION_USB_SERIAL_JTAG:
usb_serial_jtag_write_bytes((char *) data.data(), data.size(), 20 / portTICK_PERIOD_MS); usb_serial_jtag_write_bytes((const char *) this->tx_header_, header_tx_len, 20 / portTICK_PERIOD_MS);
delay(10); if (there_is_data) {
usb_serial_jtag_ll_txfifo_flush(); // fixes for issue in IDF 4.4.7 usb_serial_jtag_write_bytes((const char *) data, size, 20 / portTICK_PERIOD_MS);
usb_serial_jtag_write_bytes((const char *) &this->tx_header_[TX_CHECKSUM_IDX], 2,
20 / portTICK_PERIOD_MS); // Footer: checksum and newline
}
break; break;
#endif // USE_LOGGER_USB_SERIAL_JTAG #endif
default: default:
break; break;
} }
#elif defined(USE_ARDUINO) #elif defined(USE_ARDUINO)
this->hw_serial_->write(data.data(), data.size()); this->hw_serial_->write(this->tx_header_, header_tx_len);
if (there_is_data) {
this->hw_serial_->write(data, size);
this->hw_serial_->write(&this->tx_header_[TX_CHECKSUM_IDX], 2); // Footer: checksum and newline
}
#endif #endif
} }
void ImprovSerialComponent::loop() {
if (this->last_read_byte_ && (millis() - this->last_read_byte_ > IMPROV_SERIAL_TIMEOUT)) {
this->last_read_byte_ = 0;
this->rx_buffer_.clear();
ESP_LOGV(TAG, "Improv Serial timeout");
}
auto byte = this->read_byte_();
while (byte.has_value()) {
if (this->parse_improv_serial_byte_(byte.value())) {
this->last_read_byte_ = millis();
} else {
this->last_read_byte_ = 0;
this->rx_buffer_.clear();
}
byte = this->read_byte_();
}
if (this->state_ == improv::STATE_PROVISIONING) {
if (wifi::global_wifi_component->is_connected()) {
wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(),
this->connecting_sta_.get_password());
this->connecting_sta_ = {};
this->cancel_timeout("wifi-connect-timeout");
this->set_state_(improv::STATE_PROVISIONED);
std::vector<uint8_t> url = this->build_rpc_settings_response_(improv::WIFI_SETTINGS);
this->send_response_(url);
}
}
}
std::vector<uint8_t> ImprovSerialComponent::build_rpc_settings_response_(improv::Command command) { std::vector<uint8_t> ImprovSerialComponent::build_rpc_settings_response_(improv::Command command) {
std::vector<std::string> urls; std::vector<std::string> urls;
#ifdef USE_IMPROV_SERIAL_NEXT_URL #ifdef USE_IMPROV_SERIAL_NEXT_URL
@@ -177,13 +211,13 @@ std::vector<uint8_t> ImprovSerialComponent::build_version_info_() {
bool ImprovSerialComponent::parse_improv_serial_byte_(uint8_t byte) { bool ImprovSerialComponent::parse_improv_serial_byte_(uint8_t byte) {
size_t at = this->rx_buffer_.size(); size_t at = this->rx_buffer_.size();
this->rx_buffer_.push_back(byte); this->rx_buffer_.push_back(byte);
ESP_LOGV(TAG, "Improv Serial byte: 0x%02X", byte); ESP_LOGV(TAG, "Byte: 0x%02X", byte);
const uint8_t *raw = &this->rx_buffer_[0]; const uint8_t *raw = &this->rx_buffer_[0];
return improv::parse_improv_serial_byte( return improv::parse_improv_serial_byte(
at, byte, raw, [this](improv::ImprovCommand command) -> bool { return this->parse_improv_payload_(command); }, at, byte, raw, [this](improv::ImprovCommand command) -> bool { return this->parse_improv_payload_(command); },
[this](improv::Error error) -> void { [this](improv::Error error) -> void {
ESP_LOGW(TAG, "Error decoding Improv payload"); ESP_LOGW(TAG, "Error decoding payload");
this->set_error_(error); this->set_error_(error);
}); });
} }
@@ -199,7 +233,7 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
wifi::global_wifi_component->set_sta(sta); wifi::global_wifi_component->set_sta(sta);
wifi::global_wifi_component->start_connecting(sta, false); wifi::global_wifi_component->start_connecting(sta, false);
this->set_state_(improv::STATE_PROVISIONING); this->set_state_(improv::STATE_PROVISIONING);
ESP_LOGD(TAG, "Received Improv wifi settings ssid=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(), ESP_LOGD(TAG, "Received settings: SSID=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(),
command.password.c_str()); command.password.c_str());
auto f = std::bind(&ImprovSerialComponent::on_wifi_connect_timeout_, this); auto f = std::bind(&ImprovSerialComponent::on_wifi_connect_timeout_, this);
@@ -240,7 +274,7 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
return true; return true;
} }
default: { default: {
ESP_LOGW(TAG, "Unknown Improv payload"); ESP_LOGW(TAG, "Unknown payload");
this->set_error_(improv::ERROR_UNKNOWN_RPC); this->set_error_(improv::ERROR_UNKNOWN_RPC);
return false; return false;
} }
@@ -249,57 +283,26 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
void ImprovSerialComponent::set_state_(improv::State state) { void ImprovSerialComponent::set_state_(improv::State state) {
this->state_ = state; this->state_ = state;
this->tx_header_[TX_TYPE_IDX] = TYPE_CURRENT_STATE;
std::vector<uint8_t> data = {'I', 'M', 'P', 'R', 'O', 'V'}; this->tx_header_[TX_DATA_IDX] = state;
data.resize(11); this->write_data_();
data[6] = IMPROV_SERIAL_VERSION;
data[7] = TYPE_CURRENT_STATE;
data[8] = 1;
data[9] = state;
uint8_t checksum = 0x00;
for (uint8_t d : data)
checksum += d;
data[10] = checksum;
this->write_data_(data);
} }
void ImprovSerialComponent::set_error_(improv::Error error) { void ImprovSerialComponent::set_error_(improv::Error error) {
std::vector<uint8_t> data = {'I', 'M', 'P', 'R', 'O', 'V'}; this->tx_header_[TX_TYPE_IDX] = TYPE_ERROR_STATE;
data.resize(11); this->tx_header_[TX_DATA_IDX] = error;
data[6] = IMPROV_SERIAL_VERSION; this->write_data_();
data[7] = TYPE_ERROR_STATE;
data[8] = 1;
data[9] = error;
uint8_t checksum = 0x00;
for (uint8_t d : data)
checksum += d;
data[10] = checksum;
this->write_data_(data);
} }
void ImprovSerialComponent::send_response_(std::vector<uint8_t> &response) { void ImprovSerialComponent::send_response_(std::vector<uint8_t> &response) {
std::vector<uint8_t> data = {'I', 'M', 'P', 'R', 'O', 'V'}; this->tx_header_[TX_TYPE_IDX] = TYPE_RPC_RESPONSE;
data.resize(9); this->write_data_(response.data(), response.size());
data[6] = IMPROV_SERIAL_VERSION;
data[7] = TYPE_RPC_RESPONSE;
data[8] = response.size();
data.insert(data.end(), response.begin(), response.end());
uint8_t checksum = 0x00;
for (uint8_t d : data)
checksum += d;
data.push_back(checksum);
this->write_data_(data);
} }
void ImprovSerialComponent::on_wifi_connect_timeout_() { void ImprovSerialComponent::on_wifi_connect_timeout_() {
this->set_error_(improv::ERROR_UNABLE_TO_CONNECT); this->set_error_(improv::ERROR_UNABLE_TO_CONNECT);
this->set_state_(improv::STATE_AUTHORIZED); this->set_state_(improv::STATE_AUTHORIZED);
ESP_LOGW(TAG, "Timed out trying to connect to given WiFi network"); ESP_LOGW(TAG, "Timed out while connecting to Wi-Fi network");
wifi::global_wifi_component->clear_sta(); wifi::global_wifi_component->clear_sta();
} }

View File

@@ -26,6 +26,16 @@
namespace esphome { namespace esphome {
namespace improv_serial { namespace improv_serial {
// TX buffer layout constants
static constexpr uint8_t TX_HEADER_SIZE = 6; // Bytes 0-5 = "IMPROV"
static constexpr uint8_t TX_VERSION_IDX = 6;
static constexpr uint8_t TX_TYPE_IDX = 7;
static constexpr uint8_t TX_LENGTH_IDX = 8;
static constexpr uint8_t TX_DATA_IDX = 9; // For state/error messages only
static constexpr uint8_t TX_CHECKSUM_IDX = 10;
static constexpr uint8_t TX_NEWLINE_IDX = 11;
static constexpr uint8_t TX_BUFFER_SIZE = 12;
enum ImprovSerialType : uint8_t { enum ImprovSerialType : uint8_t {
TYPE_CURRENT_STATE = 0x01, TYPE_CURRENT_STATE = 0x01,
TYPE_ERROR_STATE = 0x02, TYPE_ERROR_STATE = 0x02,
@@ -57,7 +67,22 @@ class ImprovSerialComponent : public Component, public improv_base::ImprovBase {
std::vector<uint8_t> build_version_info_(); std::vector<uint8_t> build_version_info_();
optional<uint8_t> read_byte_(); optional<uint8_t> read_byte_();
void write_data_(std::vector<uint8_t> &data); void write_data_(const uint8_t *data = nullptr, size_t size = 0);
uint8_t tx_header_[TX_BUFFER_SIZE] = {
'I', // 0: Header
'M', // 1: Header
'P', // 2: Header
'R', // 3: Header
'O', // 4: Header
'V', // 5: Header
IMPROV_SERIAL_VERSION, // 6: Version
0, // 7: ImprovSerialType
0, // 8: Length
0, // 9...X: Data (here, one byte reserved for state/error)
0, // X + 10: Checksum
'\n',
};
#ifdef USE_ESP32 #ifdef USE_ESP32
uart_port_t uart_num_; uart_port_t uart_num_;

View File

@@ -22,6 +22,10 @@ class LightEffect {
/// Apply this effect. Use the provided state for starting transitions, ... /// Apply this effect. Use the provided state for starting transitions, ...
virtual void apply() = 0; virtual void apply() = 0;
/**
* Returns the name of this effect.
* The returned pointer is valid for the lifetime of the program and must not be freed.
*/
const char *get_name() const { return this->name_; } const char *get_name() const { return this->name_; }
/// Internal method called by the LightState when this light effect is registered in it. /// Internal method called by the LightState when this light effect is registered in it.

View File

@@ -19,6 +19,9 @@ using climate::ClimateTraits;
using climate::ClimateMode; using climate::ClimateMode;
using climate::ClimateSwingMode; using climate::ClimateSwingMode;
using climate::ClimateFanMode; using climate::ClimateFanMode;
using climate::ClimateModeMask;
using climate::ClimateSwingModeMask;
using climate::ClimatePresetMask;
class AirConditioner : public ApplianceBase<dudanov::midea::ac::AirConditioner>, public climate::Climate { class AirConditioner : public ApplianceBase<dudanov::midea::ac::AirConditioner>, public climate::Climate {
public: public:
@@ -40,20 +43,20 @@ class AirConditioner : public ApplianceBase<dudanov::midea::ac::AirConditioner>,
void do_power_on() { this->base_.setPowerState(true); } void do_power_on() { this->base_.setPowerState(true); }
void do_power_off() { this->base_.setPowerState(false); } void do_power_off() { this->base_.setPowerState(false); }
void do_power_toggle() { this->base_.setPowerState(this->mode == ClimateMode::CLIMATE_MODE_OFF); } void do_power_toggle() { this->base_.setPowerState(this->mode == ClimateMode::CLIMATE_MODE_OFF); }
void set_supported_modes(const std::set<ClimateMode> &modes) { this->supported_modes_ = modes; } void set_supported_modes(ClimateModeMask modes) { this->supported_modes_ = modes; }
void set_supported_swing_modes(const std::set<ClimateSwingMode> &modes) { this->supported_swing_modes_ = modes; } void set_supported_swing_modes(ClimateSwingModeMask modes) { this->supported_swing_modes_ = modes; }
void set_supported_presets(const std::set<ClimatePreset> &presets) { this->supported_presets_ = presets; } void set_supported_presets(ClimatePresetMask presets) { this->supported_presets_ = presets; }
void set_custom_presets(const std::set<std::string> &presets) { this->supported_custom_presets_ = presets; } void set_custom_presets(const std::vector<std::string> &presets) { this->supported_custom_presets_ = presets; }
void set_custom_fan_modes(const std::set<std::string> &modes) { this->supported_custom_fan_modes_ = modes; } void set_custom_fan_modes(const std::vector<std::string> &modes) { this->supported_custom_fan_modes_ = modes; }
protected: protected:
void control(const ClimateCall &call) override; void control(const ClimateCall &call) override;
ClimateTraits traits() override; ClimateTraits traits() override;
std::set<ClimateMode> supported_modes_{}; ClimateModeMask supported_modes_{};
std::set<ClimateSwingMode> supported_swing_modes_{}; ClimateSwingModeMask supported_swing_modes_{};
std::set<ClimatePreset> supported_presets_{}; ClimatePresetMask supported_presets_{};
std::set<std::string> supported_custom_presets_{}; std::vector<std::string> supported_custom_presets_{};
std::set<std::string> supported_custom_fan_modes_{}; std::vector<std::string> supported_custom_fan_modes_{};
Sensor *outdoor_sensor_{nullptr}; Sensor *outdoor_sensor_{nullptr};
Sensor *humidity_sensor_{nullptr}; Sensor *humidity_sensor_{nullptr};
Sensor *power_sensor_{nullptr}; Sensor *power_sensor_{nullptr};

View File

@@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
import asyncio
import logging import logging
from pathlib import Path from pathlib import Path
@@ -277,3 +278,19 @@ def upload_program(config: ConfigType, args, host: str) -> bool:
raise EsphomeError(f"Upload failed with result: {result}") raise EsphomeError(f"Upload failed with result: {result}")
return handled return handled
def show_logs(config: ConfigType, args, devices: list[str]) -> bool:
address = devices[0]
from .ble_logger import is_mac_address, logger_connect, logger_scan
if devices[0] == "BLE":
ble_device = asyncio.run(logger_scan(CORE.config["esphome"]["name"]))
if ble_device:
address = ble_device.address
else:
return True
if is_mac_address(address):
asyncio.run(logger_connect(address))
return True
return False

View File

@@ -0,0 +1,60 @@
import asyncio
import logging
import re
from typing import Final
from bleak import BleakClient, BleakScanner, BLEDevice
from bleak.exc import (
BleakCharacteristicNotFoundError,
BleakDBusError,
BleakDeviceNotFoundError,
)
_LOGGER = logging.getLogger(__name__)
NUS_SERVICE_UUID = "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"
NUS_TX_CHAR_UUID = "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"
MAC_ADDRESS_PATTERN: Final = re.compile(
r"([0-9A-F]{2}[:]){5}[0-9A-F]{2}$", flags=re.IGNORECASE
)
def is_mac_address(value: str) -> bool:
return MAC_ADDRESS_PATTERN.match(value)
async def logger_scan(name: str) -> BLEDevice | None:
_LOGGER.info("Scanning bluetooth for %s...", name)
device = await BleakScanner.find_device_by_name(name)
if not device:
_LOGGER.error("%s Bluetooth LE device was not found!", name)
return device
async def logger_connect(host: str) -> int | None:
disconnected_event = asyncio.Event()
def handle_disconnect(client):
disconnected_event.set()
def handle_rx(_, data: bytearray):
print(data.decode("utf-8"), end="")
_LOGGER.info("Connecting %s...", host)
try:
async with BleakClient(host, disconnected_callback=handle_disconnect) as client:
_LOGGER.info("Connected %s...", host)
try:
await client.start_notify(NUS_TX_CHAR_UUID, handle_rx)
except BleakDBusError as e:
_LOGGER.error("Bluetooth LE logger: %s", e)
disconnected_event.set()
await disconnected_event.wait()
except BleakDeviceNotFoundError:
_LOGGER.error("Device %s not found", host)
return 1
except BleakCharacteristicNotFoundError:
_LOGGER.error("Device %s has no NUS characteristic", host)
return 1

View File

@@ -40,6 +40,10 @@ enum OnBootRestoreFrom : uint8_t {
}; };
struct ThermostatClimateTimer { struct ThermostatClimateTimer {
ThermostatClimateTimer() = default;
ThermostatClimateTimer(bool active, uint32_t time, uint32_t started, std::function<void()> func)
: active(active), time(time), started(started), func(std::move(func)) {}
bool active; bool active;
uint32_t time; uint32_t time;
uint32_t started; uint32_t started;

View File

@@ -405,7 +405,7 @@ void ToshibaClimate::setup() {
this->swing_modes_ = this->toshiba_swing_modes_(); this->swing_modes_ = this->toshiba_swing_modes_();
// Ensure swing mode is always initialized to a valid value // Ensure swing mode is always initialized to a valid value
if (this->swing_modes_.empty() || this->swing_modes_.find(this->swing_mode) == this->swing_modes_.end()) { if (this->swing_modes_.empty() || !this->swing_modes_.count(this->swing_mode)) {
// No swing support for this model or current swing mode not supported, reset to OFF // No swing support for this model or current swing mode not supported, reset to OFF
this->swing_mode = climate::CLIMATE_SWING_OFF; this->swing_mode = climate::CLIMATE_SWING_OFF;
} }

View File

@@ -71,10 +71,10 @@ class ToshibaClimate : public climate_ir::ClimateIR {
return TOSHIBA_RAS_2819T_TEMP_C_MAX; return TOSHIBA_RAS_2819T_TEMP_C_MAX;
return TOSHIBA_GENERIC_TEMP_C_MAX; // Default to GENERIC for unknown models return TOSHIBA_GENERIC_TEMP_C_MAX; // Default to GENERIC for unknown models
} }
std::set<climate::ClimateSwingMode> toshiba_swing_modes_() { climate::ClimateSwingModeMask toshiba_swing_modes_() {
return (this->model_ == MODEL_GENERIC) return (this->model_ == MODEL_GENERIC)
? std::set<climate::ClimateSwingMode>{} ? climate::ClimateSwingModeMask()
: std::set<climate::ClimateSwingMode>{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL}; : climate::ClimateSwingModeMask{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL};
} }
void encode_(remote_base::RemoteTransmitData *data, const uint8_t *message, uint8_t nbytes, uint8_t repeat); void encode_(remote_base::RemoteTransmitData *data, const uint8_t *message, uint8_t nbytes, uint8_t repeat);
bool decode_(remote_base::RemoteReceiveData *data, uint8_t *message, uint8_t nbytes); bool decode_(remote_base::RemoteReceiveData *data, uint8_t *message, uint8_t nbytes);

View File

@@ -306,18 +306,12 @@ climate::ClimateTraits TuyaClimate::traits() {
traits.add_supported_preset(climate::CLIMATE_PRESET_NONE); traits.add_supported_preset(climate::CLIMATE_PRESET_NONE);
} }
if (this->swing_vertical_id_.has_value() && this->swing_horizontal_id_.has_value()) { if (this->swing_vertical_id_.has_value() && this->swing_horizontal_id_.has_value()) {
std::set<climate::ClimateSwingMode> supported_swing_modes = { traits.set_supported_swing_modes({climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_BOTH,
climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_BOTH, climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_HORIZONTAL});
climate::CLIMATE_SWING_HORIZONTAL};
traits.set_supported_swing_modes(std::move(supported_swing_modes));
} else if (this->swing_vertical_id_.has_value()) { } else if (this->swing_vertical_id_.has_value()) {
std::set<climate::ClimateSwingMode> supported_swing_modes = {climate::CLIMATE_SWING_OFF, traits.set_supported_swing_modes({climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL});
climate::CLIMATE_SWING_VERTICAL};
traits.set_supported_swing_modes(std::move(supported_swing_modes));
} else if (this->swing_horizontal_id_.has_value()) { } else if (this->swing_horizontal_id_.has_value()) {
std::set<climate::ClimateSwingMode> supported_swing_modes = {climate::CLIMATE_SWING_OFF, traits.set_supported_swing_modes({climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_HORIZONTAL});
climate::CLIMATE_SWING_HORIZONTAL};
traits.set_supported_swing_modes(std::move(supported_swing_modes));
} }
if (fan_speed_id_) { if (fan_speed_id_) {

View File

@@ -234,6 +234,9 @@ def copy_files():
"url": "https://esphome.io/", "url": "https://esphome.io/",
"vendor": "esphome", "vendor": "esphome",
"build": { "build": {
"bsp": {
"name": "adafruit"
},
"softdevice": { "softdevice": {
"sd_fwid": "0x00B6" "sd_fwid": "0x00B6"
} }

View File

@@ -22,6 +22,7 @@ pillow==11.3.0
cairosvg==2.8.2 cairosvg==2.8.2
freetype-py==2.5.1 freetype-py==2.5.1
jinja2==3.1.6 jinja2==3.1.6
bleak==1.0.1
# esp-idf >= 5.0 requires this # esp-idf >= 5.0 requires this
pyparsing >= 3.0 pyparsing >= 3.0

View File

@@ -0,0 +1,8 @@
wifi:
ssid: MySSID
password: password1
logger:
hardware_uart: UART0
improv_serial:

View File

@@ -0,0 +1 @@
<<: !include common-uart0.yaml

View File

@@ -44,6 +44,7 @@ class InitialStateHelper:
helper = InitialStateHelper(entities) helper = InitialStateHelper(entities)
client.subscribe_states(helper.on_state_wrapper(user_callback)) client.subscribe_states(helper.on_state_wrapper(user_callback))
await helper.wait_for_initial_states() await helper.wait_for_initial_states()
# Access initial states via helper.initial_states[key]
""" """
def __init__(self, entities: list[EntityInfo]) -> None: def __init__(self, entities: list[EntityInfo]) -> None:
@@ -63,6 +64,8 @@ class InitialStateHelper:
self._entities_by_id = { self._entities_by_id = {
(entity.device_id, entity.key): entity for entity in entities (entity.device_id, entity.key): entity for entity in entities
} }
# Store initial states by key for test access
self.initial_states: dict[int, EntityState] = {}
# Log all entities # Log all entities
_LOGGER.debug( _LOGGER.debug(
@@ -127,6 +130,9 @@ class InitialStateHelper:
# If this entity is waiting for initial state # If this entity is waiting for initial state
if entity_id in self._wait_initial_states: if entity_id in self._wait_initial_states:
# Store the initial state for test access
self.initial_states[state.key] = state
# Remove from waiting set # Remove from waiting set
self._wait_initial_states.discard(entity_id) self._wait_initial_states.discard(entity_id)

View File

@@ -2,12 +2,11 @@
from __future__ import annotations from __future__ import annotations
import asyncio
import aioesphomeapi import aioesphomeapi
from aioesphomeapi import ClimateAction, ClimateMode, ClimatePreset, EntityState from aioesphomeapi import ClimateAction, ClimateInfo, ClimateMode, ClimatePreset
import pytest import pytest
from .state_utils import InitialStateHelper
from .types import APIClientConnectedFactory, RunCompiledFunction from .types import APIClientConnectedFactory, RunCompiledFunction
@@ -18,26 +17,27 @@ async def test_host_mode_climate_basic_state(
api_client_connected: APIClientConnectedFactory, api_client_connected: APIClientConnectedFactory,
) -> None: ) -> None:
"""Test basic climate state reporting.""" """Test basic climate state reporting."""
loop = asyncio.get_running_loop()
async with run_compiled(yaml_config), api_client_connected() as client: async with run_compiled(yaml_config), api_client_connected() as client:
states: dict[int, EntityState] = {} # Get entities and set up state synchronization
climate_future: asyncio.Future[EntityState] = loop.create_future() entities, services = 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, "Expected at least 1 climate entity"
def on_state(state: EntityState) -> None: # Subscribe with the wrapper (no-op callback since we just want initial states)
states[state.key] = state client.subscribe_states(initial_state_helper.on_state_wrapper(lambda _: None))
if (
isinstance(state, aioesphomeapi.ClimateState)
and not climate_future.done()
):
climate_future.set_result(state)
client.subscribe_states(on_state)
# Wait for all initial states to be broadcast
try: try:
climate_state = await asyncio.wait_for(climate_future, timeout=5.0) await initial_state_helper.wait_for_initial_states()
except TimeoutError: except TimeoutError:
pytest.fail("Climate state not received within 5 seconds") pytest.fail("Timeout waiting for initial states")
# Get the climate entity and its initial state
test_climate = climate_infos[0]
climate_state = initial_state_helper.initial_states.get(test_climate.key)
assert climate_state is not None, "Climate initial state not found"
assert isinstance(climate_state, aioesphomeapi.ClimateState) assert isinstance(climate_state, aioesphomeapi.ClimateState)
assert climate_state.mode == ClimateMode.OFF assert climate_state.mode == ClimateMode.OFF
assert climate_state.action == ClimateAction.OFF assert climate_state.action == ClimateAction.OFF