From 2401f81be3fab1e87b675b6ed8dc5e2e34a2b4cd Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 10 Sep 2025 19:48:01 +1200 Subject: [PATCH 01/11] Bump version to 2025.9.0b1 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index f312ca45e2..a7f591cbf5 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2025.9.0-dev +PROJECT_NUMBER = 2025.9.0b1 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index fae6020b88..a77f98c292 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2025.9.0-dev" +__version__ = "2025.9.0b1" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 55dd12c66bfbf592465cee10ca2105fcda336075 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Wed, 10 Sep 2025 05:58:07 -0500 Subject: [PATCH 02/11] [thermostat] Rename timer enums to mitigate naming conflict (#10666) --- .../thermostat/thermostat_climate.cpp | 144 ++++++++++-------- .../thermostat/thermostat_climate.h | 24 +-- 2 files changed, 90 insertions(+), 78 deletions(-) diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index f05730b369..e2db3ca5e1 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -11,11 +11,11 @@ static const char *const TAG = "thermostat.climate"; void ThermostatClimate::setup() { if (this->use_startup_delay_) { // start timers so that no actions are called for a moment - this->start_timer_(thermostat::TIMER_COOLING_OFF); - this->start_timer_(thermostat::TIMER_FANNING_OFF); - this->start_timer_(thermostat::TIMER_HEATING_OFF); + this->start_timer_(thermostat::THERMOSTAT_TIMER_COOLING_OFF); + this->start_timer_(thermostat::THERMOSTAT_TIMER_FANNING_OFF); + this->start_timer_(thermostat::THERMOSTAT_TIMER_HEATING_OFF); if (this->supports_fan_only_action_uses_fan_mode_timer_) - this->start_timer_(thermostat::TIMER_FAN_MODE); + this->start_timer_(thermostat::THERMOSTAT_TIMER_FAN_MODE); } // add a callback so that whenever the sensor state changes we can take action this->sensor_->add_on_state_callback([this](float state) { @@ -307,9 +307,10 @@ climate::ClimateAction ThermostatClimate::compute_action_(const bool ignore_time return climate::CLIMATE_ACTION_OFF; } // do not change the action if an "ON" timer is running - if ((!ignore_timers) && - (this->timer_active_(thermostat::TIMER_IDLE_ON) || this->timer_active_(thermostat::TIMER_COOLING_ON) || - this->timer_active_(thermostat::TIMER_FANNING_ON) || this->timer_active_(thermostat::TIMER_HEATING_ON))) { + if ((!ignore_timers) && (this->timer_active_(thermostat::THERMOSTAT_TIMER_IDLE_ON) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_COOLING_ON) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_FANNING_ON) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_HEATING_ON))) { return this->action; } @@ -444,18 +445,18 @@ void ThermostatClimate::switch_to_action_(climate::ClimateAction action, bool pu case climate::CLIMATE_ACTION_OFF: case climate::CLIMATE_ACTION_IDLE: if (this->idle_action_ready_()) { - this->start_timer_(thermostat::TIMER_IDLE_ON); + this->start_timer_(thermostat::THERMOSTAT_TIMER_IDLE_ON); if (this->action == climate::CLIMATE_ACTION_COOLING) - this->start_timer_(thermostat::TIMER_COOLING_OFF); + this->start_timer_(thermostat::THERMOSTAT_TIMER_COOLING_OFF); if (this->action == climate::CLIMATE_ACTION_FAN) { if (this->supports_fan_only_action_uses_fan_mode_timer_) { - this->start_timer_(thermostat::TIMER_FAN_MODE); + this->start_timer_(thermostat::THERMOSTAT_TIMER_FAN_MODE); } else { - this->start_timer_(thermostat::TIMER_FANNING_OFF); + this->start_timer_(thermostat::THERMOSTAT_TIMER_FANNING_OFF); } } if (this->action == climate::CLIMATE_ACTION_HEATING) - this->start_timer_(thermostat::TIMER_HEATING_OFF); + this->start_timer_(thermostat::THERMOSTAT_TIMER_HEATING_OFF); // trig = this->idle_action_trigger_; ESP_LOGVV(TAG, "Switching to IDLE/OFF action"); this->cooling_max_runtime_exceeded_ = false; @@ -465,10 +466,10 @@ void ThermostatClimate::switch_to_action_(climate::ClimateAction action, bool pu break; case climate::CLIMATE_ACTION_COOLING: if (this->cooling_action_ready_()) { - this->start_timer_(thermostat::TIMER_COOLING_ON); - this->start_timer_(thermostat::TIMER_COOLING_MAX_RUN_TIME); + this->start_timer_(thermostat::THERMOSTAT_TIMER_COOLING_ON); + this->start_timer_(thermostat::THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME); if (this->supports_fan_with_cooling_) { - this->start_timer_(thermostat::TIMER_FANNING_ON); + this->start_timer_(thermostat::THERMOSTAT_TIMER_FANNING_ON); trig_fan = this->fan_only_action_trigger_; } this->cooling_max_runtime_exceeded_ = false; @@ -479,10 +480,10 @@ void ThermostatClimate::switch_to_action_(climate::ClimateAction action, bool pu break; case climate::CLIMATE_ACTION_HEATING: if (this->heating_action_ready_()) { - this->start_timer_(thermostat::TIMER_HEATING_ON); - this->start_timer_(thermostat::TIMER_HEATING_MAX_RUN_TIME); + this->start_timer_(thermostat::THERMOSTAT_TIMER_HEATING_ON); + this->start_timer_(thermostat::THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME); if (this->supports_fan_with_heating_) { - this->start_timer_(thermostat::TIMER_FANNING_ON); + this->start_timer_(thermostat::THERMOSTAT_TIMER_FANNING_ON); trig_fan = this->fan_only_action_trigger_; } this->heating_max_runtime_exceeded_ = false; @@ -494,9 +495,9 @@ void ThermostatClimate::switch_to_action_(climate::ClimateAction action, bool pu case climate::CLIMATE_ACTION_FAN: if (this->fanning_action_ready_()) { if (this->supports_fan_only_action_uses_fan_mode_timer_) { - this->start_timer_(thermostat::TIMER_FAN_MODE); + this->start_timer_(thermostat::THERMOSTAT_TIMER_FAN_MODE); } else { - this->start_timer_(thermostat::TIMER_FANNING_ON); + this->start_timer_(thermostat::THERMOSTAT_TIMER_FANNING_ON); } trig = this->fan_only_action_trigger_; ESP_LOGVV(TAG, "Switching to FAN_ONLY action"); @@ -505,8 +506,8 @@ void ThermostatClimate::switch_to_action_(climate::ClimateAction action, bool pu break; case climate::CLIMATE_ACTION_DRYING: if (this->drying_action_ready_()) { - this->start_timer_(thermostat::TIMER_COOLING_ON); - this->start_timer_(thermostat::TIMER_FANNING_ON); + this->start_timer_(thermostat::THERMOSTAT_TIMER_COOLING_ON); + this->start_timer_(thermostat::THERMOSTAT_TIMER_FANNING_ON); trig = this->dry_action_trigger_; ESP_LOGVV(TAG, "Switching to DRYING action"); action_ready = true; @@ -549,14 +550,14 @@ void ThermostatClimate::switch_to_supplemental_action_(climate::ClimateAction ac switch (action) { case climate::CLIMATE_ACTION_OFF: case climate::CLIMATE_ACTION_IDLE: - this->cancel_timer_(thermostat::TIMER_COOLING_MAX_RUN_TIME); - this->cancel_timer_(thermostat::TIMER_HEATING_MAX_RUN_TIME); + this->cancel_timer_(thermostat::THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME); + this->cancel_timer_(thermostat::THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME); break; case climate::CLIMATE_ACTION_COOLING: - this->cancel_timer_(thermostat::TIMER_COOLING_MAX_RUN_TIME); + this->cancel_timer_(thermostat::THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME); break; case climate::CLIMATE_ACTION_HEATING: - this->cancel_timer_(thermostat::TIMER_HEATING_MAX_RUN_TIME); + this->cancel_timer_(thermostat::THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME); break; default: return; @@ -571,15 +572,15 @@ void ThermostatClimate::trigger_supplemental_action_() { switch (this->supplemental_action_) { case climate::CLIMATE_ACTION_COOLING: - if (!this->timer_active_(thermostat::TIMER_COOLING_MAX_RUN_TIME)) { - this->start_timer_(thermostat::TIMER_COOLING_MAX_RUN_TIME); + if (!this->timer_active_(thermostat::THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME)) { + this->start_timer_(thermostat::THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME); } trig = this->supplemental_cool_action_trigger_; ESP_LOGVV(TAG, "Calling supplemental COOLING action"); break; case climate::CLIMATE_ACTION_HEATING: - if (!this->timer_active_(thermostat::TIMER_HEATING_MAX_RUN_TIME)) { - this->start_timer_(thermostat::TIMER_HEATING_MAX_RUN_TIME); + if (!this->timer_active_(thermostat::THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME)) { + this->start_timer_(thermostat::THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME); } trig = this->supplemental_heat_action_trigger_; ESP_LOGVV(TAG, "Calling supplemental HEATING action"); @@ -657,7 +658,7 @@ void ThermostatClimate::switch_to_fan_mode_(climate::ClimateFanMode fan_mode, bo this->prev_fan_mode_trigger_->stop_action(); this->prev_fan_mode_trigger_ = nullptr; } - this->start_timer_(thermostat::TIMER_FAN_MODE); + this->start_timer_(thermostat::THERMOSTAT_TIMER_FAN_MODE); if (trig != nullptr) { trig->trigger(); } @@ -758,35 +759,44 @@ void ThermostatClimate::switch_to_swing_mode_(climate::ClimateSwingMode swing_mo bool ThermostatClimate::idle_action_ready_() { if (this->supports_fan_only_action_uses_fan_mode_timer_) { - return !(this->timer_active_(thermostat::TIMER_COOLING_ON) || this->timer_active_(thermostat::TIMER_FAN_MODE) || - this->timer_active_(thermostat::TIMER_HEATING_ON)); + return !(this->timer_active_(thermostat::THERMOSTAT_TIMER_COOLING_ON) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_FAN_MODE) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_HEATING_ON)); } - return !(this->timer_active_(thermostat::TIMER_COOLING_ON) || this->timer_active_(thermostat::TIMER_FANNING_ON) || - this->timer_active_(thermostat::TIMER_HEATING_ON)); + return !(this->timer_active_(thermostat::THERMOSTAT_TIMER_COOLING_ON) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_FANNING_ON) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_HEATING_ON)); } bool ThermostatClimate::cooling_action_ready_() { - return !(this->timer_active_(thermostat::TIMER_IDLE_ON) || this->timer_active_(thermostat::TIMER_FANNING_OFF) || - this->timer_active_(thermostat::TIMER_COOLING_OFF) || this->timer_active_(thermostat::TIMER_HEATING_ON)); + return !(this->timer_active_(thermostat::THERMOSTAT_TIMER_IDLE_ON) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_FANNING_OFF) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_COOLING_OFF) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_HEATING_ON)); } bool ThermostatClimate::drying_action_ready_() { - return !(this->timer_active_(thermostat::TIMER_IDLE_ON) || this->timer_active_(thermostat::TIMER_FANNING_OFF) || - this->timer_active_(thermostat::TIMER_COOLING_OFF) || this->timer_active_(thermostat::TIMER_HEATING_ON)); + return !(this->timer_active_(thermostat::THERMOSTAT_TIMER_IDLE_ON) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_FANNING_OFF) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_COOLING_OFF) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_HEATING_ON)); } -bool ThermostatClimate::fan_mode_ready_() { return !(this->timer_active_(thermostat::TIMER_FAN_MODE)); } +bool ThermostatClimate::fan_mode_ready_() { return !(this->timer_active_(thermostat::THERMOSTAT_TIMER_FAN_MODE)); } bool ThermostatClimate::fanning_action_ready_() { if (this->supports_fan_only_action_uses_fan_mode_timer_) { - return !(this->timer_active_(thermostat::TIMER_FAN_MODE)); + return !(this->timer_active_(thermostat::THERMOSTAT_TIMER_FAN_MODE)); } - return !(this->timer_active_(thermostat::TIMER_IDLE_ON) || this->timer_active_(thermostat::TIMER_FANNING_OFF)); + return !(this->timer_active_(thermostat::THERMOSTAT_TIMER_IDLE_ON) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_FANNING_OFF)); } bool ThermostatClimate::heating_action_ready_() { - return !(this->timer_active_(thermostat::TIMER_IDLE_ON) || this->timer_active_(thermostat::TIMER_COOLING_ON) || - this->timer_active_(thermostat::TIMER_FANNING_OFF) || this->timer_active_(thermostat::TIMER_HEATING_OFF)); + return !(this->timer_active_(thermostat::THERMOSTAT_TIMER_IDLE_ON) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_COOLING_ON) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_FANNING_OFF) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_HEATING_OFF)); } void ThermostatClimate::start_timer_(const ThermostatClimateTimerIndex timer_index) { @@ -1162,43 +1172,43 @@ void ThermostatClimate::set_heat_overrun(float overrun) { this->heating_overrun_ void ThermostatClimate::set_supplemental_cool_delta(float delta) { this->supplemental_cool_delta_ = delta; } void ThermostatClimate::set_supplemental_heat_delta(float delta) { this->supplemental_heat_delta_ = delta; } void ThermostatClimate::set_cooling_maximum_run_time_in_sec(uint32_t time) { - this->timer_[thermostat::TIMER_COOLING_MAX_RUN_TIME].time = + this->timer_[thermostat::THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME].time = 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); } void ThermostatClimate::set_cooling_minimum_off_time_in_sec(uint32_t time) { - this->timer_[thermostat::TIMER_COOLING_OFF].time = + this->timer_[thermostat::THERMOSTAT_TIMER_COOLING_OFF].time = 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); } void ThermostatClimate::set_cooling_minimum_run_time_in_sec(uint32_t time) { - this->timer_[thermostat::TIMER_COOLING_ON].time = + this->timer_[thermostat::THERMOSTAT_TIMER_COOLING_ON].time = 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); } void ThermostatClimate::set_fan_mode_minimum_switching_time_in_sec(uint32_t time) { - this->timer_[thermostat::TIMER_FAN_MODE].time = + this->timer_[thermostat::THERMOSTAT_TIMER_FAN_MODE].time = 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); } void ThermostatClimate::set_fanning_minimum_off_time_in_sec(uint32_t time) { - this->timer_[thermostat::TIMER_FANNING_OFF].time = + this->timer_[thermostat::THERMOSTAT_TIMER_FANNING_OFF].time = 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); } void ThermostatClimate::set_fanning_minimum_run_time_in_sec(uint32_t time) { - this->timer_[thermostat::TIMER_FANNING_ON].time = + this->timer_[thermostat::THERMOSTAT_TIMER_FANNING_ON].time = 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); } void ThermostatClimate::set_heating_maximum_run_time_in_sec(uint32_t time) { - this->timer_[thermostat::TIMER_HEATING_MAX_RUN_TIME].time = + this->timer_[thermostat::THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME].time = 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); } void ThermostatClimate::set_heating_minimum_off_time_in_sec(uint32_t time) { - this->timer_[thermostat::TIMER_HEATING_OFF].time = + this->timer_[thermostat::THERMOSTAT_TIMER_HEATING_OFF].time = 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); } void ThermostatClimate::set_heating_minimum_run_time_in_sec(uint32_t time) { - this->timer_[thermostat::TIMER_HEATING_ON].time = + this->timer_[thermostat::THERMOSTAT_TIMER_HEATING_ON].time = 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); } void ThermostatClimate::set_idle_minimum_time_in_sec(uint32_t time) { - this->timer_[thermostat::TIMER_IDLE_ON].time = + this->timer_[thermostat::THERMOSTAT_TIMER_IDLE_ON].time = 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); } void ThermostatClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } @@ -1327,13 +1337,14 @@ void ThermostatClimate::dump_config() { " Minimum Off Time: %" PRIu32 "s\n" " Minimum Run Time: %" PRIu32 "s", this->cooling_deadband_, this->cooling_overrun_, - this->timer_duration_(thermostat::TIMER_COOLING_OFF) / 1000, - this->timer_duration_(thermostat::TIMER_COOLING_ON) / 1000); - if ((this->supplemental_cool_delta_ > 0) || (this->timer_duration_(thermostat::TIMER_COOLING_MAX_RUN_TIME) > 0)) { + this->timer_duration_(thermostat::THERMOSTAT_TIMER_COOLING_OFF) / 1000, + this->timer_duration_(thermostat::THERMOSTAT_TIMER_COOLING_ON) / 1000); + if ((this->supplemental_cool_delta_ > 0) || + (this->timer_duration_(thermostat::THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME) > 0)) { ESP_LOGCONFIG(TAG, " Maximum Run Time: %" PRIu32 "s\n" " Supplemental Delta: %.1f°C", - this->timer_duration_(thermostat::TIMER_COOLING_MAX_RUN_TIME) / 1000, + this->timer_duration_(thermostat::THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME) / 1000, this->supplemental_cool_delta_); } } @@ -1345,13 +1356,14 @@ void ThermostatClimate::dump_config() { " Minimum Off Time: %" PRIu32 "s\n" " Minimum Run Time: %" PRIu32 "s", this->heating_deadband_, this->heating_overrun_, - this->timer_duration_(thermostat::TIMER_HEATING_OFF) / 1000, - this->timer_duration_(thermostat::TIMER_HEATING_ON) / 1000); - if ((this->supplemental_heat_delta_ > 0) || (this->timer_duration_(thermostat::TIMER_HEATING_MAX_RUN_TIME) > 0)) { + this->timer_duration_(thermostat::THERMOSTAT_TIMER_HEATING_OFF) / 1000, + this->timer_duration_(thermostat::THERMOSTAT_TIMER_HEATING_ON) / 1000); + if ((this->supplemental_heat_delta_ > 0) || + (this->timer_duration_(thermostat::THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME) > 0)) { ESP_LOGCONFIG(TAG, " Maximum Run Time: %" PRIu32 "s\n" " Supplemental Delta: %.1f°C", - this->timer_duration_(thermostat::TIMER_HEATING_MAX_RUN_TIME) / 1000, + this->timer_duration_(thermostat::THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME) / 1000, this->supplemental_heat_delta_); } } @@ -1360,15 +1372,15 @@ void ThermostatClimate::dump_config() { " Fan Parameters:\n" " Minimum Off Time: %" PRIu32 "s\n" " Minimum Run Time: %" PRIu32 "s", - this->timer_duration_(thermostat::TIMER_FANNING_OFF) / 1000, - this->timer_duration_(thermostat::TIMER_FANNING_ON) / 1000); + this->timer_duration_(thermostat::THERMOSTAT_TIMER_FANNING_OFF) / 1000, + this->timer_duration_(thermostat::THERMOSTAT_TIMER_FANNING_ON) / 1000); } if (this->supports_fan_mode_on_ || this->supports_fan_mode_off_ || this->supports_fan_mode_auto_ || this->supports_fan_mode_low_ || this->supports_fan_mode_medium_ || this->supports_fan_mode_high_ || this->supports_fan_mode_middle_ || this->supports_fan_mode_focus_ || this->supports_fan_mode_diffuse_ || this->supports_fan_mode_quiet_) { ESP_LOGCONFIG(TAG, " Minimum Fan Mode Switching Time: %" PRIu32 "s", - this->timer_duration_(thermostat::TIMER_FAN_MODE) / 1000); + this->timer_duration_(thermostat::THERMOSTAT_TIMER_FAN_MODE) / 1000); } ESP_LOGCONFIG(TAG, " Minimum Idle Time: %" PRIu32 "s\n" @@ -1381,7 +1393,7 @@ void ThermostatClimate::dump_config() { " FAN_ONLY: %s\n" " FAN_ONLY_ACTION_USES_FAN_MODE_TIMER: %s\n" " FAN_ONLY_COOLING: %s", - this->timer_[thermostat::TIMER_IDLE_ON].time / 1000, YESNO(this->supports_auto_), + this->timer_[thermostat::THERMOSTAT_TIMER_IDLE_ON].time / 1000, YESNO(this->supports_auto_), YESNO(this->supports_heat_cool_), YESNO(this->supports_heat_), YESNO(this->supports_cool_), YESNO(this->supports_dry_), YESNO(this->supports_fan_only_), YESNO(this->supports_fan_only_action_uses_fan_mode_timer_), YESNO(this->supports_fan_only_cooling_)); diff --git a/esphome/components/thermostat/thermostat_climate.h b/esphome/components/thermostat/thermostat_climate.h index e2c7b00266..526f07116e 100644 --- a/esphome/components/thermostat/thermostat_climate.h +++ b/esphome/components/thermostat/thermostat_climate.h @@ -14,17 +14,17 @@ namespace esphome { namespace thermostat { enum ThermostatClimateTimerIndex : uint8_t { - TIMER_COOLING_MAX_RUN_TIME = 0, - TIMER_COOLING_OFF = 1, - TIMER_COOLING_ON = 2, - TIMER_FAN_MODE = 3, - TIMER_FANNING_OFF = 4, - TIMER_FANNING_ON = 5, - TIMER_HEATING_MAX_RUN_TIME = 6, - TIMER_HEATING_OFF = 7, - TIMER_HEATING_ON = 8, - TIMER_IDLE_ON = 9, - TIMER_COUNT = 10, + THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME = 0, + THERMOSTAT_TIMER_COOLING_OFF = 1, + THERMOSTAT_TIMER_COOLING_ON = 2, + THERMOSTAT_TIMER_FAN_MODE = 3, + THERMOSTAT_TIMER_FANNING_OFF = 4, + THERMOSTAT_TIMER_FANNING_ON = 5, + THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME = 6, + THERMOSTAT_TIMER_HEATING_OFF = 7, + THERMOSTAT_TIMER_HEATING_ON = 8, + THERMOSTAT_TIMER_IDLE_ON = 9, + THERMOSTAT_TIMER_COUNT = 10, }; enum OnBootRestoreFrom : uint8_t { @@ -467,7 +467,7 @@ class ThermostatClimate : public climate::Climate, public Component { std::string default_custom_preset_{}; /// Climate action timers - std::array timer_{ + std::array timer_{ ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::cooling_max_run_time_timer_callback_, this)), ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::cooling_off_timer_callback_, this)), ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::cooling_on_timer_callback_, this)), From 56e85b3ef96277c0a97a29d8d87c9a25cade385b Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Wed, 10 Sep 2025 05:58:07 -0500 Subject: [PATCH 03/11] [thermostat] Rename timer enums to mitigate naming conflict (#10666) --- .../thermostat/thermostat_climate.cpp | 144 ++++++++++-------- .../thermostat/thermostat_climate.h | 24 +-- 2 files changed, 90 insertions(+), 78 deletions(-) diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index f05730b369..e2db3ca5e1 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -11,11 +11,11 @@ static const char *const TAG = "thermostat.climate"; void ThermostatClimate::setup() { if (this->use_startup_delay_) { // start timers so that no actions are called for a moment - this->start_timer_(thermostat::TIMER_COOLING_OFF); - this->start_timer_(thermostat::TIMER_FANNING_OFF); - this->start_timer_(thermostat::TIMER_HEATING_OFF); + this->start_timer_(thermostat::THERMOSTAT_TIMER_COOLING_OFF); + this->start_timer_(thermostat::THERMOSTAT_TIMER_FANNING_OFF); + this->start_timer_(thermostat::THERMOSTAT_TIMER_HEATING_OFF); if (this->supports_fan_only_action_uses_fan_mode_timer_) - this->start_timer_(thermostat::TIMER_FAN_MODE); + this->start_timer_(thermostat::THERMOSTAT_TIMER_FAN_MODE); } // add a callback so that whenever the sensor state changes we can take action this->sensor_->add_on_state_callback([this](float state) { @@ -307,9 +307,10 @@ climate::ClimateAction ThermostatClimate::compute_action_(const bool ignore_time return climate::CLIMATE_ACTION_OFF; } // do not change the action if an "ON" timer is running - if ((!ignore_timers) && - (this->timer_active_(thermostat::TIMER_IDLE_ON) || this->timer_active_(thermostat::TIMER_COOLING_ON) || - this->timer_active_(thermostat::TIMER_FANNING_ON) || this->timer_active_(thermostat::TIMER_HEATING_ON))) { + if ((!ignore_timers) && (this->timer_active_(thermostat::THERMOSTAT_TIMER_IDLE_ON) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_COOLING_ON) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_FANNING_ON) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_HEATING_ON))) { return this->action; } @@ -444,18 +445,18 @@ void ThermostatClimate::switch_to_action_(climate::ClimateAction action, bool pu case climate::CLIMATE_ACTION_OFF: case climate::CLIMATE_ACTION_IDLE: if (this->idle_action_ready_()) { - this->start_timer_(thermostat::TIMER_IDLE_ON); + this->start_timer_(thermostat::THERMOSTAT_TIMER_IDLE_ON); if (this->action == climate::CLIMATE_ACTION_COOLING) - this->start_timer_(thermostat::TIMER_COOLING_OFF); + this->start_timer_(thermostat::THERMOSTAT_TIMER_COOLING_OFF); if (this->action == climate::CLIMATE_ACTION_FAN) { if (this->supports_fan_only_action_uses_fan_mode_timer_) { - this->start_timer_(thermostat::TIMER_FAN_MODE); + this->start_timer_(thermostat::THERMOSTAT_TIMER_FAN_MODE); } else { - this->start_timer_(thermostat::TIMER_FANNING_OFF); + this->start_timer_(thermostat::THERMOSTAT_TIMER_FANNING_OFF); } } if (this->action == climate::CLIMATE_ACTION_HEATING) - this->start_timer_(thermostat::TIMER_HEATING_OFF); + this->start_timer_(thermostat::THERMOSTAT_TIMER_HEATING_OFF); // trig = this->idle_action_trigger_; ESP_LOGVV(TAG, "Switching to IDLE/OFF action"); this->cooling_max_runtime_exceeded_ = false; @@ -465,10 +466,10 @@ void ThermostatClimate::switch_to_action_(climate::ClimateAction action, bool pu break; case climate::CLIMATE_ACTION_COOLING: if (this->cooling_action_ready_()) { - this->start_timer_(thermostat::TIMER_COOLING_ON); - this->start_timer_(thermostat::TIMER_COOLING_MAX_RUN_TIME); + this->start_timer_(thermostat::THERMOSTAT_TIMER_COOLING_ON); + this->start_timer_(thermostat::THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME); if (this->supports_fan_with_cooling_) { - this->start_timer_(thermostat::TIMER_FANNING_ON); + this->start_timer_(thermostat::THERMOSTAT_TIMER_FANNING_ON); trig_fan = this->fan_only_action_trigger_; } this->cooling_max_runtime_exceeded_ = false; @@ -479,10 +480,10 @@ void ThermostatClimate::switch_to_action_(climate::ClimateAction action, bool pu break; case climate::CLIMATE_ACTION_HEATING: if (this->heating_action_ready_()) { - this->start_timer_(thermostat::TIMER_HEATING_ON); - this->start_timer_(thermostat::TIMER_HEATING_MAX_RUN_TIME); + this->start_timer_(thermostat::THERMOSTAT_TIMER_HEATING_ON); + this->start_timer_(thermostat::THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME); if (this->supports_fan_with_heating_) { - this->start_timer_(thermostat::TIMER_FANNING_ON); + this->start_timer_(thermostat::THERMOSTAT_TIMER_FANNING_ON); trig_fan = this->fan_only_action_trigger_; } this->heating_max_runtime_exceeded_ = false; @@ -494,9 +495,9 @@ void ThermostatClimate::switch_to_action_(climate::ClimateAction action, bool pu case climate::CLIMATE_ACTION_FAN: if (this->fanning_action_ready_()) { if (this->supports_fan_only_action_uses_fan_mode_timer_) { - this->start_timer_(thermostat::TIMER_FAN_MODE); + this->start_timer_(thermostat::THERMOSTAT_TIMER_FAN_MODE); } else { - this->start_timer_(thermostat::TIMER_FANNING_ON); + this->start_timer_(thermostat::THERMOSTAT_TIMER_FANNING_ON); } trig = this->fan_only_action_trigger_; ESP_LOGVV(TAG, "Switching to FAN_ONLY action"); @@ -505,8 +506,8 @@ void ThermostatClimate::switch_to_action_(climate::ClimateAction action, bool pu break; case climate::CLIMATE_ACTION_DRYING: if (this->drying_action_ready_()) { - this->start_timer_(thermostat::TIMER_COOLING_ON); - this->start_timer_(thermostat::TIMER_FANNING_ON); + this->start_timer_(thermostat::THERMOSTAT_TIMER_COOLING_ON); + this->start_timer_(thermostat::THERMOSTAT_TIMER_FANNING_ON); trig = this->dry_action_trigger_; ESP_LOGVV(TAG, "Switching to DRYING action"); action_ready = true; @@ -549,14 +550,14 @@ void ThermostatClimate::switch_to_supplemental_action_(climate::ClimateAction ac switch (action) { case climate::CLIMATE_ACTION_OFF: case climate::CLIMATE_ACTION_IDLE: - this->cancel_timer_(thermostat::TIMER_COOLING_MAX_RUN_TIME); - this->cancel_timer_(thermostat::TIMER_HEATING_MAX_RUN_TIME); + this->cancel_timer_(thermostat::THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME); + this->cancel_timer_(thermostat::THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME); break; case climate::CLIMATE_ACTION_COOLING: - this->cancel_timer_(thermostat::TIMER_COOLING_MAX_RUN_TIME); + this->cancel_timer_(thermostat::THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME); break; case climate::CLIMATE_ACTION_HEATING: - this->cancel_timer_(thermostat::TIMER_HEATING_MAX_RUN_TIME); + this->cancel_timer_(thermostat::THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME); break; default: return; @@ -571,15 +572,15 @@ void ThermostatClimate::trigger_supplemental_action_() { switch (this->supplemental_action_) { case climate::CLIMATE_ACTION_COOLING: - if (!this->timer_active_(thermostat::TIMER_COOLING_MAX_RUN_TIME)) { - this->start_timer_(thermostat::TIMER_COOLING_MAX_RUN_TIME); + if (!this->timer_active_(thermostat::THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME)) { + this->start_timer_(thermostat::THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME); } trig = this->supplemental_cool_action_trigger_; ESP_LOGVV(TAG, "Calling supplemental COOLING action"); break; case climate::CLIMATE_ACTION_HEATING: - if (!this->timer_active_(thermostat::TIMER_HEATING_MAX_RUN_TIME)) { - this->start_timer_(thermostat::TIMER_HEATING_MAX_RUN_TIME); + if (!this->timer_active_(thermostat::THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME)) { + this->start_timer_(thermostat::THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME); } trig = this->supplemental_heat_action_trigger_; ESP_LOGVV(TAG, "Calling supplemental HEATING action"); @@ -657,7 +658,7 @@ void ThermostatClimate::switch_to_fan_mode_(climate::ClimateFanMode fan_mode, bo this->prev_fan_mode_trigger_->stop_action(); this->prev_fan_mode_trigger_ = nullptr; } - this->start_timer_(thermostat::TIMER_FAN_MODE); + this->start_timer_(thermostat::THERMOSTAT_TIMER_FAN_MODE); if (trig != nullptr) { trig->trigger(); } @@ -758,35 +759,44 @@ void ThermostatClimate::switch_to_swing_mode_(climate::ClimateSwingMode swing_mo bool ThermostatClimate::idle_action_ready_() { if (this->supports_fan_only_action_uses_fan_mode_timer_) { - return !(this->timer_active_(thermostat::TIMER_COOLING_ON) || this->timer_active_(thermostat::TIMER_FAN_MODE) || - this->timer_active_(thermostat::TIMER_HEATING_ON)); + return !(this->timer_active_(thermostat::THERMOSTAT_TIMER_COOLING_ON) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_FAN_MODE) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_HEATING_ON)); } - return !(this->timer_active_(thermostat::TIMER_COOLING_ON) || this->timer_active_(thermostat::TIMER_FANNING_ON) || - this->timer_active_(thermostat::TIMER_HEATING_ON)); + return !(this->timer_active_(thermostat::THERMOSTAT_TIMER_COOLING_ON) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_FANNING_ON) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_HEATING_ON)); } bool ThermostatClimate::cooling_action_ready_() { - return !(this->timer_active_(thermostat::TIMER_IDLE_ON) || this->timer_active_(thermostat::TIMER_FANNING_OFF) || - this->timer_active_(thermostat::TIMER_COOLING_OFF) || this->timer_active_(thermostat::TIMER_HEATING_ON)); + return !(this->timer_active_(thermostat::THERMOSTAT_TIMER_IDLE_ON) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_FANNING_OFF) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_COOLING_OFF) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_HEATING_ON)); } bool ThermostatClimate::drying_action_ready_() { - return !(this->timer_active_(thermostat::TIMER_IDLE_ON) || this->timer_active_(thermostat::TIMER_FANNING_OFF) || - this->timer_active_(thermostat::TIMER_COOLING_OFF) || this->timer_active_(thermostat::TIMER_HEATING_ON)); + return !(this->timer_active_(thermostat::THERMOSTAT_TIMER_IDLE_ON) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_FANNING_OFF) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_COOLING_OFF) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_HEATING_ON)); } -bool ThermostatClimate::fan_mode_ready_() { return !(this->timer_active_(thermostat::TIMER_FAN_MODE)); } +bool ThermostatClimate::fan_mode_ready_() { return !(this->timer_active_(thermostat::THERMOSTAT_TIMER_FAN_MODE)); } bool ThermostatClimate::fanning_action_ready_() { if (this->supports_fan_only_action_uses_fan_mode_timer_) { - return !(this->timer_active_(thermostat::TIMER_FAN_MODE)); + return !(this->timer_active_(thermostat::THERMOSTAT_TIMER_FAN_MODE)); } - return !(this->timer_active_(thermostat::TIMER_IDLE_ON) || this->timer_active_(thermostat::TIMER_FANNING_OFF)); + return !(this->timer_active_(thermostat::THERMOSTAT_TIMER_IDLE_ON) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_FANNING_OFF)); } bool ThermostatClimate::heating_action_ready_() { - return !(this->timer_active_(thermostat::TIMER_IDLE_ON) || this->timer_active_(thermostat::TIMER_COOLING_ON) || - this->timer_active_(thermostat::TIMER_FANNING_OFF) || this->timer_active_(thermostat::TIMER_HEATING_OFF)); + return !(this->timer_active_(thermostat::THERMOSTAT_TIMER_IDLE_ON) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_COOLING_ON) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_FANNING_OFF) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_HEATING_OFF)); } void ThermostatClimate::start_timer_(const ThermostatClimateTimerIndex timer_index) { @@ -1162,43 +1172,43 @@ void ThermostatClimate::set_heat_overrun(float overrun) { this->heating_overrun_ void ThermostatClimate::set_supplemental_cool_delta(float delta) { this->supplemental_cool_delta_ = delta; } void ThermostatClimate::set_supplemental_heat_delta(float delta) { this->supplemental_heat_delta_ = delta; } void ThermostatClimate::set_cooling_maximum_run_time_in_sec(uint32_t time) { - this->timer_[thermostat::TIMER_COOLING_MAX_RUN_TIME].time = + this->timer_[thermostat::THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME].time = 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); } void ThermostatClimate::set_cooling_minimum_off_time_in_sec(uint32_t time) { - this->timer_[thermostat::TIMER_COOLING_OFF].time = + this->timer_[thermostat::THERMOSTAT_TIMER_COOLING_OFF].time = 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); } void ThermostatClimate::set_cooling_minimum_run_time_in_sec(uint32_t time) { - this->timer_[thermostat::TIMER_COOLING_ON].time = + this->timer_[thermostat::THERMOSTAT_TIMER_COOLING_ON].time = 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); } void ThermostatClimate::set_fan_mode_minimum_switching_time_in_sec(uint32_t time) { - this->timer_[thermostat::TIMER_FAN_MODE].time = + this->timer_[thermostat::THERMOSTAT_TIMER_FAN_MODE].time = 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); } void ThermostatClimate::set_fanning_minimum_off_time_in_sec(uint32_t time) { - this->timer_[thermostat::TIMER_FANNING_OFF].time = + this->timer_[thermostat::THERMOSTAT_TIMER_FANNING_OFF].time = 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); } void ThermostatClimate::set_fanning_minimum_run_time_in_sec(uint32_t time) { - this->timer_[thermostat::TIMER_FANNING_ON].time = + this->timer_[thermostat::THERMOSTAT_TIMER_FANNING_ON].time = 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); } void ThermostatClimate::set_heating_maximum_run_time_in_sec(uint32_t time) { - this->timer_[thermostat::TIMER_HEATING_MAX_RUN_TIME].time = + this->timer_[thermostat::THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME].time = 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); } void ThermostatClimate::set_heating_minimum_off_time_in_sec(uint32_t time) { - this->timer_[thermostat::TIMER_HEATING_OFF].time = + this->timer_[thermostat::THERMOSTAT_TIMER_HEATING_OFF].time = 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); } void ThermostatClimate::set_heating_minimum_run_time_in_sec(uint32_t time) { - this->timer_[thermostat::TIMER_HEATING_ON].time = + this->timer_[thermostat::THERMOSTAT_TIMER_HEATING_ON].time = 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); } void ThermostatClimate::set_idle_minimum_time_in_sec(uint32_t time) { - this->timer_[thermostat::TIMER_IDLE_ON].time = + this->timer_[thermostat::THERMOSTAT_TIMER_IDLE_ON].time = 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); } void ThermostatClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } @@ -1327,13 +1337,14 @@ void ThermostatClimate::dump_config() { " Minimum Off Time: %" PRIu32 "s\n" " Minimum Run Time: %" PRIu32 "s", this->cooling_deadband_, this->cooling_overrun_, - this->timer_duration_(thermostat::TIMER_COOLING_OFF) / 1000, - this->timer_duration_(thermostat::TIMER_COOLING_ON) / 1000); - if ((this->supplemental_cool_delta_ > 0) || (this->timer_duration_(thermostat::TIMER_COOLING_MAX_RUN_TIME) > 0)) { + this->timer_duration_(thermostat::THERMOSTAT_TIMER_COOLING_OFF) / 1000, + this->timer_duration_(thermostat::THERMOSTAT_TIMER_COOLING_ON) / 1000); + if ((this->supplemental_cool_delta_ > 0) || + (this->timer_duration_(thermostat::THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME) > 0)) { ESP_LOGCONFIG(TAG, " Maximum Run Time: %" PRIu32 "s\n" " Supplemental Delta: %.1f°C", - this->timer_duration_(thermostat::TIMER_COOLING_MAX_RUN_TIME) / 1000, + this->timer_duration_(thermostat::THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME) / 1000, this->supplemental_cool_delta_); } } @@ -1345,13 +1356,14 @@ void ThermostatClimate::dump_config() { " Minimum Off Time: %" PRIu32 "s\n" " Minimum Run Time: %" PRIu32 "s", this->heating_deadband_, this->heating_overrun_, - this->timer_duration_(thermostat::TIMER_HEATING_OFF) / 1000, - this->timer_duration_(thermostat::TIMER_HEATING_ON) / 1000); - if ((this->supplemental_heat_delta_ > 0) || (this->timer_duration_(thermostat::TIMER_HEATING_MAX_RUN_TIME) > 0)) { + this->timer_duration_(thermostat::THERMOSTAT_TIMER_HEATING_OFF) / 1000, + this->timer_duration_(thermostat::THERMOSTAT_TIMER_HEATING_ON) / 1000); + if ((this->supplemental_heat_delta_ > 0) || + (this->timer_duration_(thermostat::THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME) > 0)) { ESP_LOGCONFIG(TAG, " Maximum Run Time: %" PRIu32 "s\n" " Supplemental Delta: %.1f°C", - this->timer_duration_(thermostat::TIMER_HEATING_MAX_RUN_TIME) / 1000, + this->timer_duration_(thermostat::THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME) / 1000, this->supplemental_heat_delta_); } } @@ -1360,15 +1372,15 @@ void ThermostatClimate::dump_config() { " Fan Parameters:\n" " Minimum Off Time: %" PRIu32 "s\n" " Minimum Run Time: %" PRIu32 "s", - this->timer_duration_(thermostat::TIMER_FANNING_OFF) / 1000, - this->timer_duration_(thermostat::TIMER_FANNING_ON) / 1000); + this->timer_duration_(thermostat::THERMOSTAT_TIMER_FANNING_OFF) / 1000, + this->timer_duration_(thermostat::THERMOSTAT_TIMER_FANNING_ON) / 1000); } if (this->supports_fan_mode_on_ || this->supports_fan_mode_off_ || this->supports_fan_mode_auto_ || this->supports_fan_mode_low_ || this->supports_fan_mode_medium_ || this->supports_fan_mode_high_ || this->supports_fan_mode_middle_ || this->supports_fan_mode_focus_ || this->supports_fan_mode_diffuse_ || this->supports_fan_mode_quiet_) { ESP_LOGCONFIG(TAG, " Minimum Fan Mode Switching Time: %" PRIu32 "s", - this->timer_duration_(thermostat::TIMER_FAN_MODE) / 1000); + this->timer_duration_(thermostat::THERMOSTAT_TIMER_FAN_MODE) / 1000); } ESP_LOGCONFIG(TAG, " Minimum Idle Time: %" PRIu32 "s\n" @@ -1381,7 +1393,7 @@ void ThermostatClimate::dump_config() { " FAN_ONLY: %s\n" " FAN_ONLY_ACTION_USES_FAN_MODE_TIMER: %s\n" " FAN_ONLY_COOLING: %s", - this->timer_[thermostat::TIMER_IDLE_ON].time / 1000, YESNO(this->supports_auto_), + this->timer_[thermostat::THERMOSTAT_TIMER_IDLE_ON].time / 1000, YESNO(this->supports_auto_), YESNO(this->supports_heat_cool_), YESNO(this->supports_heat_), YESNO(this->supports_cool_), YESNO(this->supports_dry_), YESNO(this->supports_fan_only_), YESNO(this->supports_fan_only_action_uses_fan_mode_timer_), YESNO(this->supports_fan_only_cooling_)); diff --git a/esphome/components/thermostat/thermostat_climate.h b/esphome/components/thermostat/thermostat_climate.h index e2c7b00266..526f07116e 100644 --- a/esphome/components/thermostat/thermostat_climate.h +++ b/esphome/components/thermostat/thermostat_climate.h @@ -14,17 +14,17 @@ namespace esphome { namespace thermostat { enum ThermostatClimateTimerIndex : uint8_t { - TIMER_COOLING_MAX_RUN_TIME = 0, - TIMER_COOLING_OFF = 1, - TIMER_COOLING_ON = 2, - TIMER_FAN_MODE = 3, - TIMER_FANNING_OFF = 4, - TIMER_FANNING_ON = 5, - TIMER_HEATING_MAX_RUN_TIME = 6, - TIMER_HEATING_OFF = 7, - TIMER_HEATING_ON = 8, - TIMER_IDLE_ON = 9, - TIMER_COUNT = 10, + THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME = 0, + THERMOSTAT_TIMER_COOLING_OFF = 1, + THERMOSTAT_TIMER_COOLING_ON = 2, + THERMOSTAT_TIMER_FAN_MODE = 3, + THERMOSTAT_TIMER_FANNING_OFF = 4, + THERMOSTAT_TIMER_FANNING_ON = 5, + THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME = 6, + THERMOSTAT_TIMER_HEATING_OFF = 7, + THERMOSTAT_TIMER_HEATING_ON = 8, + THERMOSTAT_TIMER_IDLE_ON = 9, + THERMOSTAT_TIMER_COUNT = 10, }; enum OnBootRestoreFrom : uint8_t { @@ -467,7 +467,7 @@ class ThermostatClimate : public climate::Climate, public Component { std::string default_custom_preset_{}; /// Climate action timers - std::array timer_{ + std::array timer_{ ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::cooling_max_run_time_timer_callback_, this)), ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::cooling_off_timer_callback_, this)), ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::cooling_on_timer_callback_, this)), From 10aae33979641652afaa10f5fc975e9210965d7a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 10 Sep 2025 08:17:34 -0500 Subject: [PATCH 04/11] Improve coverage for various core modules (#10663) --- tests/unit_tests/conftest.py | 7 + .../test_config_validation_paths.py | 187 +++++++++++++++++ tests/unit_tests/test_external_files.py | 196 ++++++++++++++++++ tests/unit_tests/test_platformio_api.py | 129 ++++++++++++ tests/unit_tests/test_storage_json.py | 182 ++++++++++++++++ 5 files changed, 701 insertions(+) create mode 100644 tests/unit_tests/test_config_validation_paths.py create mode 100644 tests/unit_tests/test_external_files.py create mode 100644 tests/unit_tests/test_platformio_api.py create mode 100644 tests/unit_tests/test_storage_json.py diff --git a/tests/unit_tests/conftest.py b/tests/unit_tests/conftest.py index aac5a642f6..a1e438b577 100644 --- a/tests/unit_tests/conftest.py +++ b/tests/unit_tests/conftest.py @@ -36,3 +36,10 @@ def fixture_path() -> Path: Location of all fixture files. """ return here / "fixtures" + + +@pytest.fixture +def setup_core(tmp_path: Path) -> Path: + """Set up CORE with test paths.""" + CORE.config_path = str(tmp_path / "test.yaml") + return tmp_path diff --git a/tests/unit_tests/test_config_validation_paths.py b/tests/unit_tests/test_config_validation_paths.py new file mode 100644 index 0000000000..f8f038390e --- /dev/null +++ b/tests/unit_tests/test_config_validation_paths.py @@ -0,0 +1,187 @@ +"""Tests for config_validation.py path-related functions.""" + +from pathlib import Path + +import pytest +import voluptuous as vol + +from esphome import config_validation as cv + + +def test_directory_valid_path(setup_core: Path) -> None: + """Test directory validator with valid directory.""" + test_dir = setup_core / "test_directory" + test_dir.mkdir() + + result = cv.directory("test_directory") + + assert result == "test_directory" + + +def test_directory_absolute_path(setup_core: Path) -> None: + """Test directory validator with absolute path.""" + test_dir = setup_core / "test_directory" + test_dir.mkdir() + + result = cv.directory(str(test_dir)) + + assert result == str(test_dir) + + +def test_directory_nonexistent_path(setup_core: Path) -> None: + """Test directory validator raises error for non-existent directory.""" + with pytest.raises( + vol.Invalid, match="Could not find directory.*nonexistent_directory" + ): + cv.directory("nonexistent_directory") + + +def test_directory_file_instead_of_directory(setup_core: Path) -> None: + """Test directory validator raises error when path is a file.""" + test_file = setup_core / "test_file.txt" + test_file.write_text("content") + + with pytest.raises(vol.Invalid, match="is not a directory"): + cv.directory("test_file.txt") + + +def test_directory_with_parent_directory(setup_core: Path) -> None: + """Test directory validator with nested directory structure.""" + nested_dir = setup_core / "parent" / "child" / "grandchild" + nested_dir.mkdir(parents=True) + + result = cv.directory("parent/child/grandchild") + + assert result == "parent/child/grandchild" + + +def test_file_valid_path(setup_core: Path) -> None: + """Test file_ validator with valid file.""" + test_file = setup_core / "test_file.yaml" + test_file.write_text("test content") + + result = cv.file_("test_file.yaml") + + assert result == "test_file.yaml" + + +def test_file_absolute_path(setup_core: Path) -> None: + """Test file_ validator with absolute path.""" + test_file = setup_core / "test_file.yaml" + test_file.write_text("test content") + + result = cv.file_(str(test_file)) + + assert result == str(test_file) + + +def test_file_nonexistent_path(setup_core: Path) -> None: + """Test file_ validator raises error for non-existent file.""" + with pytest.raises(vol.Invalid, match="Could not find file.*nonexistent_file.yaml"): + cv.file_("nonexistent_file.yaml") + + +def test_file_directory_instead_of_file(setup_core: Path) -> None: + """Test file_ validator raises error when path is a directory.""" + test_dir = setup_core / "test_directory" + test_dir.mkdir() + + with pytest.raises(vol.Invalid, match="is not a file"): + cv.file_("test_directory") + + +def test_file_with_parent_directory(setup_core: Path) -> None: + """Test file_ validator with file in nested directory.""" + nested_dir = setup_core / "configs" / "sensors" + nested_dir.mkdir(parents=True) + test_file = nested_dir / "temperature.yaml" + test_file.write_text("sensor config") + + result = cv.file_("configs/sensors/temperature.yaml") + + assert result == "configs/sensors/temperature.yaml" + + +def test_directory_handles_trailing_slash(setup_core: Path) -> None: + """Test directory validator handles trailing slashes correctly.""" + test_dir = setup_core / "test_dir" + test_dir.mkdir() + + result = cv.directory("test_dir/") + assert result == "test_dir/" + + result = cv.directory("test_dir") + assert result == "test_dir" + + +def test_file_handles_various_extensions(setup_core: Path) -> None: + """Test file_ validator works with different file extensions.""" + yaml_file = setup_core / "config.yaml" + yaml_file.write_text("yaml content") + assert cv.file_("config.yaml") == "config.yaml" + + yml_file = setup_core / "config.yml" + yml_file.write_text("yml content") + assert cv.file_("config.yml") == "config.yml" + + txt_file = setup_core / "readme.txt" + txt_file.write_text("text content") + assert cv.file_("readme.txt") == "readme.txt" + + no_ext_file = setup_core / "LICENSE" + no_ext_file.write_text("license content") + assert cv.file_("LICENSE") == "LICENSE" + + +def test_directory_with_symlink(setup_core: Path) -> None: + """Test directory validator follows symlinks.""" + actual_dir = setup_core / "actual_directory" + actual_dir.mkdir() + + symlink_dir = setup_core / "symlink_directory" + symlink_dir.symlink_to(actual_dir) + + result = cv.directory("symlink_directory") + assert result == "symlink_directory" + + +def test_file_with_symlink(setup_core: Path) -> None: + """Test file_ validator follows symlinks.""" + actual_file = setup_core / "actual_file.txt" + actual_file.write_text("content") + + symlink_file = setup_core / "symlink_file.txt" + symlink_file.symlink_to(actual_file) + + result = cv.file_("symlink_file.txt") + assert result == "symlink_file.txt" + + +def test_directory_error_shows_full_path(setup_core: Path) -> None: + """Test directory validator error message includes full path.""" + with pytest.raises(vol.Invalid, match=".*missing_dir.*full path:.*"): + cv.directory("missing_dir") + + +def test_file_error_shows_full_path(setup_core: Path) -> None: + """Test file_ validator error message includes full path.""" + with pytest.raises(vol.Invalid, match=".*missing_file.yaml.*full path:.*"): + cv.file_("missing_file.yaml") + + +def test_directory_with_spaces_in_name(setup_core: Path) -> None: + """Test directory validator handles spaces in directory names.""" + dir_with_spaces = setup_core / "my test directory" + dir_with_spaces.mkdir() + + result = cv.directory("my test directory") + assert result == "my test directory" + + +def test_file_with_spaces_in_name(setup_core: Path) -> None: + """Test file_ validator handles spaces in file names.""" + file_with_spaces = setup_core / "my test file.yaml" + file_with_spaces.write_text("content") + + result = cv.file_("my test file.yaml") + assert result == "my test file.yaml" diff --git a/tests/unit_tests/test_external_files.py b/tests/unit_tests/test_external_files.py new file mode 100644 index 0000000000..3fa7de2f64 --- /dev/null +++ b/tests/unit_tests/test_external_files.py @@ -0,0 +1,196 @@ +"""Tests for external_files.py functions.""" + +from pathlib import Path +import time +from unittest.mock import MagicMock, patch + +import pytest +import requests + +from esphome import external_files +from esphome.config_validation import Invalid +from esphome.core import CORE, TimePeriod + + +def test_compute_local_file_dir(setup_core: Path) -> None: + """Test compute_local_file_dir creates and returns correct path.""" + domain = "font" + + result = external_files.compute_local_file_dir(domain) + + assert isinstance(result, Path) + assert result == Path(CORE.data_dir) / domain + assert result.exists() + assert result.is_dir() + + +def test_compute_local_file_dir_nested(setup_core: Path) -> None: + """Test compute_local_file_dir works with nested domains.""" + domain = "images/icons" + + result = external_files.compute_local_file_dir(domain) + + assert result == Path(CORE.data_dir) / "images" / "icons" + assert result.exists() + assert result.is_dir() + + +def test_is_file_recent_with_recent_file(setup_core: Path) -> None: + """Test is_file_recent returns True for recently created file.""" + test_file = setup_core / "recent.txt" + test_file.write_text("content") + + refresh = TimePeriod(seconds=3600) + + result = external_files.is_file_recent(str(test_file), refresh) + + assert result is True + + +def test_is_file_recent_with_old_file(setup_core: Path) -> None: + """Test is_file_recent returns False for old file.""" + test_file = setup_core / "old.txt" + test_file.write_text("content") + + old_time = time.time() - 7200 + + with patch("os.path.getctime", return_value=old_time): + refresh = TimePeriod(seconds=3600) + + result = external_files.is_file_recent(str(test_file), refresh) + + assert result is False + + +def test_is_file_recent_nonexistent_file(setup_core: Path) -> None: + """Test is_file_recent returns False for non-existent file.""" + test_file = setup_core / "nonexistent.txt" + refresh = TimePeriod(seconds=3600) + + result = external_files.is_file_recent(str(test_file), refresh) + + assert result is False + + +def test_is_file_recent_with_zero_refresh(setup_core: Path) -> None: + """Test is_file_recent with zero refresh period returns False.""" + test_file = setup_core / "test.txt" + test_file.write_text("content") + + # Mock getctime to return a time 10 seconds ago + with patch("os.path.getctime", return_value=time.time() - 10): + refresh = TimePeriod(seconds=0) + result = external_files.is_file_recent(str(test_file), refresh) + assert result is False + + +@patch("esphome.external_files.requests.head") +def test_has_remote_file_changed_not_modified( + mock_head: MagicMock, setup_core: Path +) -> None: + """Test has_remote_file_changed returns False when file not modified.""" + test_file = setup_core / "cached.txt" + test_file.write_text("cached content") + + mock_response = MagicMock() + mock_response.status_code = 304 + mock_head.return_value = mock_response + + url = "https://example.com/file.txt" + result = external_files.has_remote_file_changed(url, str(test_file)) + + assert result is False + mock_head.assert_called_once() + + call_args = mock_head.call_args + headers = call_args[1]["headers"] + assert external_files.IF_MODIFIED_SINCE in headers + assert external_files.CACHE_CONTROL in headers + + +@patch("esphome.external_files.requests.head") +def test_has_remote_file_changed_modified( + mock_head: MagicMock, setup_core: Path +) -> None: + """Test has_remote_file_changed returns True when file modified.""" + test_file = setup_core / "cached.txt" + test_file.write_text("cached content") + + mock_response = MagicMock() + mock_response.status_code = 200 + mock_head.return_value = mock_response + + url = "https://example.com/file.txt" + result = external_files.has_remote_file_changed(url, str(test_file)) + + assert result is True + + +def test_has_remote_file_changed_no_local_file(setup_core: Path) -> None: + """Test has_remote_file_changed returns True when local file doesn't exist.""" + test_file = setup_core / "nonexistent.txt" + + url = "https://example.com/file.txt" + result = external_files.has_remote_file_changed(url, str(test_file)) + + assert result is True + + +@patch("esphome.external_files.requests.head") +def test_has_remote_file_changed_network_error( + mock_head: MagicMock, setup_core: Path +) -> None: + """Test has_remote_file_changed handles network errors gracefully.""" + test_file = setup_core / "cached.txt" + test_file.write_text("cached content") + + mock_head.side_effect = requests.exceptions.RequestException("Network error") + + url = "https://example.com/file.txt" + + with pytest.raises(Invalid, match="Could not check if.*Network error"): + external_files.has_remote_file_changed(url, str(test_file)) + + +@patch("esphome.external_files.requests.head") +def test_has_remote_file_changed_timeout( + mock_head: MagicMock, setup_core: Path +) -> None: + """Test has_remote_file_changed respects timeout.""" + test_file = setup_core / "cached.txt" + test_file.write_text("cached content") + + mock_response = MagicMock() + mock_response.status_code = 304 + mock_head.return_value = mock_response + + url = "https://example.com/file.txt" + external_files.has_remote_file_changed(url, str(test_file)) + + call_args = mock_head.call_args + assert call_args[1]["timeout"] == external_files.NETWORK_TIMEOUT + + +def test_compute_local_file_dir_creates_parent_dirs(setup_core: Path) -> None: + """Test compute_local_file_dir creates parent directories.""" + domain = "level1/level2/level3/level4" + + result = external_files.compute_local_file_dir(domain) + + assert result.exists() + assert result.is_dir() + assert result.parent.name == "level3" + assert result.parent.parent.name == "level2" + assert result.parent.parent.parent.name == "level1" + + +def test_is_file_recent_handles_float_seconds(setup_core: Path) -> None: + """Test is_file_recent works with float seconds in TimePeriod.""" + test_file = setup_core / "test.txt" + test_file.write_text("content") + + refresh = TimePeriod(seconds=3600.5) + + result = external_files.is_file_recent(str(test_file), refresh) + + assert result is True diff --git a/tests/unit_tests/test_platformio_api.py b/tests/unit_tests/test_platformio_api.py new file mode 100644 index 0000000000..a1fa963e51 --- /dev/null +++ b/tests/unit_tests/test_platformio_api.py @@ -0,0 +1,129 @@ +"""Tests for platformio_api.py path functions.""" + +from pathlib import Path +from unittest.mock import patch + +from esphome import platformio_api +from esphome.core import CORE + + +def test_idedata_firmware_elf_path(setup_core: Path) -> None: + """Test IDEData.firmware_elf_path returns correct path.""" + CORE.build_path = str(setup_core / "build" / "test") + CORE.name = "test" + raw_data = {"prog_path": "/path/to/firmware.elf"} + idedata = platformio_api.IDEData(raw_data) + + assert idedata.firmware_elf_path == "/path/to/firmware.elf" + + +def test_idedata_firmware_bin_path(setup_core: Path) -> None: + """Test IDEData.firmware_bin_path returns Path with .bin extension.""" + CORE.build_path = str(setup_core / "build" / "test") + CORE.name = "test" + prog_path = str(Path("/path/to/firmware.elf")) + raw_data = {"prog_path": prog_path} + idedata = platformio_api.IDEData(raw_data) + + result = idedata.firmware_bin_path + assert isinstance(result, str) + expected = str(Path("/path/to/firmware.bin")) + assert result == expected + assert result.endswith(".bin") + + +def test_idedata_firmware_bin_path_preserves_directory(setup_core: Path) -> None: + """Test firmware_bin_path preserves the directory structure.""" + CORE.build_path = str(setup_core / "build" / "test") + CORE.name = "test" + prog_path = str(Path("/complex/path/to/build/firmware.elf")) + raw_data = {"prog_path": prog_path} + idedata = platformio_api.IDEData(raw_data) + + result = idedata.firmware_bin_path + expected = str(Path("/complex/path/to/build/firmware.bin")) + assert result == expected + + +def test_idedata_extra_flash_images(setup_core: Path) -> None: + """Test IDEData.extra_flash_images returns list of FlashImage objects.""" + CORE.build_path = str(setup_core / "build" / "test") + CORE.name = "test" + raw_data = { + "prog_path": "/path/to/firmware.elf", + "extra": { + "flash_images": [ + {"path": "/path/to/bootloader.bin", "offset": "0x1000"}, + {"path": "/path/to/partition.bin", "offset": "0x8000"}, + ] + }, + } + idedata = platformio_api.IDEData(raw_data) + + images = idedata.extra_flash_images + assert len(images) == 2 + assert all(isinstance(img, platformio_api.FlashImage) for img in images) + assert images[0].path == "/path/to/bootloader.bin" + assert images[0].offset == "0x1000" + assert images[1].path == "/path/to/partition.bin" + assert images[1].offset == "0x8000" + + +def test_idedata_extra_flash_images_empty(setup_core: Path) -> None: + """Test extra_flash_images returns empty list when no extra images.""" + CORE.build_path = str(setup_core / "build" / "test") + CORE.name = "test" + raw_data = {"prog_path": "/path/to/firmware.elf", "extra": {"flash_images": []}} + idedata = platformio_api.IDEData(raw_data) + + images = idedata.extra_flash_images + assert images == [] + + +def test_idedata_cc_path(setup_core: Path) -> None: + """Test IDEData.cc_path returns compiler path.""" + CORE.build_path = str(setup_core / "build" / "test") + CORE.name = "test" + raw_data = { + "prog_path": "/path/to/firmware.elf", + "cc_path": "/Users/test/.platformio/packages/toolchain-xtensa32/bin/xtensa-esp32-elf-gcc", + } + idedata = platformio_api.IDEData(raw_data) + + assert ( + idedata.cc_path + == "/Users/test/.platformio/packages/toolchain-xtensa32/bin/xtensa-esp32-elf-gcc" + ) + + +def test_flash_image_dataclass() -> None: + """Test FlashImage dataclass stores path and offset correctly.""" + image = platformio_api.FlashImage(path="/path/to/image.bin", offset="0x10000") + + assert image.path == "/path/to/image.bin" + assert image.offset == "0x10000" + + +def test_load_idedata_returns_dict(setup_core: Path) -> None: + """Test _load_idedata returns parsed idedata dict when successful.""" + CORE.build_path = str(setup_core / "build" / "test") + CORE.name = "test" + + # Create required files + platformio_ini = setup_core / "build" / "test" / "platformio.ini" + platformio_ini.parent.mkdir(parents=True, exist_ok=True) + platformio_ini.touch() + + idedata_path = setup_core / ".esphome" / "idedata" / "test.json" + idedata_path.parent.mkdir(parents=True, exist_ok=True) + idedata_path.write_text('{"prog_path": "/test/firmware.elf"}') + + with patch("esphome.platformio_api.run_platformio_cli_run") as mock_run: + mock_run.return_value = '{"prog_path": "/test/firmware.elf"}' + + config = {"name": "test"} + result = platformio_api._load_idedata(config) + + assert result is not None + assert isinstance(result, dict) + assert result["prog_path"] == "/test/firmware.elf" diff --git a/tests/unit_tests/test_storage_json.py b/tests/unit_tests/test_storage_json.py new file mode 100644 index 0000000000..52de327bbc --- /dev/null +++ b/tests/unit_tests/test_storage_json.py @@ -0,0 +1,182 @@ +"""Tests for storage_json.py path functions.""" + +from pathlib import Path +import sys +from unittest.mock import patch + +import pytest + +from esphome import storage_json +from esphome.core import CORE + + +def test_storage_path(setup_core: Path) -> None: + """Test storage_path returns correct path for current config.""" + CORE.config_path = str(setup_core / "my_device.yaml") + + result = storage_json.storage_path() + + data_dir = Path(CORE.data_dir) + expected = str(data_dir / "storage" / "my_device.yaml.json") + assert result == expected + + +def test_ext_storage_path(setup_core: Path) -> None: + """Test ext_storage_path returns correct path for given filename.""" + result = storage_json.ext_storage_path("other_device.yaml") + + data_dir = Path(CORE.data_dir) + expected = str(data_dir / "storage" / "other_device.yaml.json") + assert result == expected + + +def test_ext_storage_path_handles_various_extensions(setup_core: Path) -> None: + """Test ext_storage_path works with different file extensions.""" + result_yml = storage_json.ext_storage_path("device.yml") + assert result_yml.endswith("device.yml.json") + + result_no_ext = storage_json.ext_storage_path("device") + assert result_no_ext.endswith("device.json") + + result_path = storage_json.ext_storage_path("my/device.yaml") + assert result_path.endswith("device.yaml.json") + + +def test_esphome_storage_path(setup_core: Path) -> None: + """Test esphome_storage_path returns correct path.""" + result = storage_json.esphome_storage_path() + + data_dir = Path(CORE.data_dir) + expected = str(data_dir / "esphome.json") + assert result == expected + + +def test_ignored_devices_storage_path(setup_core: Path) -> None: + """Test ignored_devices_storage_path returns correct path.""" + result = storage_json.ignored_devices_storage_path() + + data_dir = Path(CORE.data_dir) + expected = str(data_dir / "ignored-devices.json") + assert result == expected + + +def test_trash_storage_path(setup_core: Path) -> None: + """Test trash_storage_path returns correct path.""" + CORE.config_path = str(setup_core / "configs" / "device.yaml") + + result = storage_json.trash_storage_path() + + expected = str(setup_core / "configs" / "trash") + assert result == expected + + +def test_archive_storage_path(setup_core: Path) -> None: + """Test archive_storage_path returns correct path.""" + CORE.config_path = str(setup_core / "configs" / "device.yaml") + + result = storage_json.archive_storage_path() + + expected = str(setup_core / "configs" / "archive") + assert result == expected + + +def test_storage_path_with_subdirectory(setup_core: Path) -> None: + """Test storage paths work correctly when config is in subdirectory.""" + subdir = setup_core / "configs" / "basement" + subdir.mkdir(parents=True, exist_ok=True) + CORE.config_path = str(subdir / "sensor.yaml") + + result = storage_json.storage_path() + + data_dir = Path(CORE.data_dir) + expected = str(data_dir / "storage" / "sensor.yaml.json") + assert result == expected + + +def test_storage_json_firmware_bin_path_property(setup_core: Path) -> None: + """Test StorageJSON firmware_bin_path property.""" + storage = storage_json.StorageJSON( + storage_version=1, + name="test_device", + friendly_name="Test Device", + comment=None, + esphome_version="2024.1.0", + src_version=None, + address="192.168.1.100", + web_port=80, + target_platform="ESP32", + build_path="build/test_device", + firmware_bin_path="/path/to/firmware.bin", + loaded_integrations={"wifi", "api"}, + loaded_platforms=set(), + no_mdns=False, + ) + + assert storage.firmware_bin_path == "/path/to/firmware.bin" + + +def test_storage_json_save_creates_directory(setup_core: Path, tmp_path: Path) -> None: + """Test StorageJSON.save creates storage directory if it doesn't exist.""" + storage_dir = tmp_path / "new_data" / "storage" + storage_file = storage_dir / "test.json" + + assert not storage_dir.exists() + + storage = storage_json.StorageJSON( + storage_version=1, + name="test", + friendly_name="Test", + comment=None, + esphome_version="2024.1.0", + src_version=None, + address="test.local", + web_port=None, + target_platform="ESP8266", + build_path=None, + firmware_bin_path=None, + loaded_integrations=set(), + loaded_platforms=set(), + no_mdns=False, + ) + + with patch("esphome.storage_json.write_file_if_changed") as mock_write: + storage.save(str(storage_file)) + mock_write.assert_called_once() + call_args = mock_write.call_args[0] + assert call_args[0] == str(storage_file) + + +def test_storage_json_from_wizard(setup_core: Path) -> None: + """Test StorageJSON.from_wizard creates correct storage object.""" + storage = storage_json.StorageJSON.from_wizard( + name="my_device", + friendly_name="My Device", + address="my_device.local", + platform="ESP32", + ) + + assert storage.name == "my_device" + assert storage.friendly_name == "My Device" + assert storage.address == "my_device.local" + assert storage.target_platform == "ESP32" + assert storage.build_path is None + assert storage.firmware_bin_path is None + + +@pytest.mark.skipif(sys.platform == "win32", reason="HA addons don't run on Windows") +@patch("esphome.core.is_ha_addon") +def test_storage_paths_with_ha_addon(mock_is_ha_addon: bool, tmp_path: Path) -> None: + """Test storage paths when running as Home Assistant addon.""" + mock_is_ha_addon.return_value = True + + CORE.config_path = str(tmp_path / "test.yaml") + + result = storage_json.storage_path() + # When is_ha_addon is True, CORE.data_dir returns "/data" + # This is the standard mount point for HA addon containers + expected = str(Path("/data") / "storage" / "test.yaml.json") + assert result == expected + + result = storage_json.esphome_storage_path() + expected = str(Path("/data") / "esphome.json") + assert result == expected From 32e4eb26ad54bace3a6f16db11e3a5881f0f9b38 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 10 Sep 2025 10:46:30 -0400 Subject: [PATCH 05/11] [remote] Remove duplicate implementations of remote code (#10548) --- .../components/remote_receiver/__init__.py | 4 +- ...ceiver_esp8266.cpp => remote_receiver.cpp} | 4 +- .../remote_receiver_libretiny.cpp | 125 ------------------ .../components/remote_transmitter/__init__.py | 4 +- .../remote_transmitter/remote_transmitter.cpp | 97 ++++++++++++++ .../remote_transmitter_esp8266.cpp | 107 --------------- .../remote_transmitter_libretiny.cpp | 110 --------------- 7 files changed, 103 insertions(+), 348 deletions(-) rename esphome/components/remote_receiver/{remote_receiver_esp8266.cpp => remote_receiver.cpp} (97%) delete mode 100644 esphome/components/remote_receiver/remote_receiver_libretiny.cpp delete mode 100644 esphome/components/remote_transmitter/remote_transmitter_esp8266.cpp delete mode 100644 esphome/components/remote_transmitter/remote_transmitter_libretiny.cpp diff --git a/esphome/components/remote_receiver/__init__.py b/esphome/components/remote_receiver/__init__.py index 9095016b55..956f240b14 100644 --- a/esphome/components/remote_receiver/__init__.py +++ b/esphome/components/remote_receiver/__init__.py @@ -196,8 +196,8 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( PlatformFramework.ESP32_ARDUINO, PlatformFramework.ESP32_IDF, }, - "remote_receiver_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO}, - "remote_receiver_libretiny.cpp": { + "remote_receiver.cpp": { + PlatformFramework.ESP8266_ARDUINO, PlatformFramework.BK72XX_ARDUINO, PlatformFramework.RTL87XX_ARDUINO, PlatformFramework.LN882X_ARDUINO, diff --git a/esphome/components/remote_receiver/remote_receiver_esp8266.cpp b/esphome/components/remote_receiver/remote_receiver.cpp similarity index 97% rename from esphome/components/remote_receiver/remote_receiver_esp8266.cpp rename to esphome/components/remote_receiver/remote_receiver.cpp index b8ac29a543..a8438e20d7 100644 --- a/esphome/components/remote_receiver/remote_receiver_esp8266.cpp +++ b/esphome/components/remote_receiver/remote_receiver.cpp @@ -3,12 +3,12 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#ifdef USE_ESP8266 +#if defined(USE_LIBRETINY) || defined(USE_ESP8266) namespace esphome { namespace remote_receiver { -static const char *const TAG = "remote_receiver.esp8266"; +static const char *const TAG = "remote_receiver"; void IRAM_ATTR HOT RemoteReceiverComponentStore::gpio_intr(RemoteReceiverComponentStore *arg) { const uint32_t now = micros(); diff --git a/esphome/components/remote_receiver/remote_receiver_libretiny.cpp b/esphome/components/remote_receiver/remote_receiver_libretiny.cpp deleted file mode 100644 index 8d801b37d2..0000000000 --- a/esphome/components/remote_receiver/remote_receiver_libretiny.cpp +++ /dev/null @@ -1,125 +0,0 @@ -#include "remote_receiver.h" -#include "esphome/core/hal.h" -#include "esphome/core/helpers.h" -#include "esphome/core/log.h" - -#ifdef USE_LIBRETINY - -namespace esphome { -namespace remote_receiver { - -static const char *const TAG = "remote_receiver.libretiny"; - -void IRAM_ATTR HOT RemoteReceiverComponentStore::gpio_intr(RemoteReceiverComponentStore *arg) { - const uint32_t now = micros(); - // If the lhs is 1 (rising edge) we should write to an uneven index and vice versa - const uint32_t next = (arg->buffer_write_at + 1) % arg->buffer_size; - const bool level = arg->pin.digital_read(); - if (level != next % 2) - return; - - // If next is buffer_read, we have hit an overflow - if (next == arg->buffer_read_at) - return; - - const uint32_t last_change = arg->buffer[arg->buffer_write_at]; - const uint32_t time_since_change = now - last_change; - if (time_since_change <= arg->filter_us) - return; - - arg->buffer[arg->buffer_write_at = next] = now; -} - -void RemoteReceiverComponent::setup() { - this->pin_->setup(); - auto &s = this->store_; - s.filter_us = this->filter_us_; - s.pin = this->pin_->to_isr(); - s.buffer_size = this->buffer_size_; - - this->high_freq_.start(); - if (s.buffer_size % 2 != 0) { - // Make sure divisible by two. This way, we know that every 0bxxx0 index is a space and every 0bxxx1 index is a mark - s.buffer_size++; - } - - s.buffer = new uint32_t[s.buffer_size]; - void *buf = (void *) s.buffer; - memset(buf, 0, s.buffer_size * sizeof(uint32_t)); - - // First index is a space. - if (this->pin_->digital_read()) { - s.buffer_write_at = s.buffer_read_at = 1; - } else { - s.buffer_write_at = s.buffer_read_at = 0; - } - this->pin_->attach_interrupt(RemoteReceiverComponentStore::gpio_intr, &this->store_, gpio::INTERRUPT_ANY_EDGE); -} -void RemoteReceiverComponent::dump_config() { - ESP_LOGCONFIG(TAG, "Remote Receiver:"); - LOG_PIN(" Pin: ", this->pin_); - if (this->pin_->digital_read()) { - ESP_LOGW(TAG, "Remote Receiver Signal starts with a HIGH value. Usually this means you have to " - "invert the signal using 'inverted: True' in the pin schema!"); - } - ESP_LOGCONFIG(TAG, - " Buffer Size: %u\n" - " Tolerance: %u%s\n" - " Filter out pulses shorter than: %u us\n" - " Signal is done after %u us of no changes", - this->buffer_size_, this->tolerance_, - (this->tolerance_mode_ == remote_base::TOLERANCE_MODE_TIME) ? " us" : "%", this->filter_us_, - this->idle_us_); -} - -void RemoteReceiverComponent::loop() { - auto &s = this->store_; - - // copy write at to local variables, as it's volatile - const uint32_t write_at = s.buffer_write_at; - const uint32_t dist = (s.buffer_size + write_at - s.buffer_read_at) % s.buffer_size; - // signals must at least one rising and one leading edge - if (dist <= 1) - return; - const uint32_t now = micros(); - if (now - s.buffer[write_at] < this->idle_us_) { - // The last change was fewer than the configured idle time ago. - return; - } - - ESP_LOGVV(TAG, "read_at=%u write_at=%u dist=%u now=%u end=%u", s.buffer_read_at, write_at, dist, now, - s.buffer[write_at]); - - // Skip first value, it's from the previous idle level - s.buffer_read_at = (s.buffer_read_at + 1) % s.buffer_size; - uint32_t prev = s.buffer_read_at; - s.buffer_read_at = (s.buffer_read_at + 1) % s.buffer_size; - const uint32_t reserve_size = 1 + (s.buffer_size + write_at - s.buffer_read_at) % s.buffer_size; - this->temp_.clear(); - this->temp_.reserve(reserve_size); - int32_t multiplier = s.buffer_read_at % 2 == 0 ? 1 : -1; - - for (uint32_t i = 0; prev != write_at; i++) { - int32_t delta = s.buffer[s.buffer_read_at] - s.buffer[prev]; - if (uint32_t(delta) >= this->idle_us_) { - // already found a space longer than idle. There must have been two pulses - break; - } - - ESP_LOGVV(TAG, " i=%u buffer[%u]=%u - buffer[%u]=%u -> %d", i, s.buffer_read_at, s.buffer[s.buffer_read_at], prev, - s.buffer[prev], multiplier * delta); - this->temp_.push_back(multiplier * delta); - prev = s.buffer_read_at; - s.buffer_read_at = (s.buffer_read_at + 1) % s.buffer_size; - multiplier *= -1; - } - s.buffer_read_at = (s.buffer_size + s.buffer_read_at - 1) % s.buffer_size; - this->temp_.push_back(this->idle_us_ * multiplier); - - this->call_listeners_dumpers_(); -} - -} // namespace remote_receiver -} // namespace esphome - -#endif diff --git a/esphome/components/remote_transmitter/__init__.py b/esphome/components/remote_transmitter/__init__.py index e79437013f..cb98c017f1 100644 --- a/esphome/components/remote_transmitter/__init__.py +++ b/esphome/components/remote_transmitter/__init__.py @@ -131,8 +131,8 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( PlatformFramework.ESP32_ARDUINO, PlatformFramework.ESP32_IDF, }, - "remote_transmitter_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO}, - "remote_transmitter_libretiny.cpp": { + "remote_transmitter.cpp": { + PlatformFramework.ESP8266_ARDUINO, PlatformFramework.BK72XX_ARDUINO, PlatformFramework.RTL87XX_ARDUINO, PlatformFramework.LN882X_ARDUINO, diff --git a/esphome/components/remote_transmitter/remote_transmitter.cpp b/esphome/components/remote_transmitter/remote_transmitter.cpp index 425418ff39..79d9cda93b 100644 --- a/esphome/components/remote_transmitter/remote_transmitter.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter.cpp @@ -2,10 +2,107 @@ #include "esphome/core/log.h" #include "esphome/core/application.h" +#if defined(USE_LIBRETINY) || defined(USE_ESP8266) + namespace esphome { namespace remote_transmitter { static const char *const TAG = "remote_transmitter"; +void RemoteTransmitterComponent::setup() { + this->pin_->setup(); + this->pin_->digital_write(false); +} + +void RemoteTransmitterComponent::dump_config() { + ESP_LOGCONFIG(TAG, + "Remote Transmitter:\n" + " Carrier Duty: %u%%", + this->carrier_duty_percent_); + LOG_PIN(" Pin: ", this->pin_); +} + +void RemoteTransmitterComponent::calculate_on_off_time_(uint32_t carrier_frequency, uint32_t *on_time_period, + uint32_t *off_time_period) { + if (carrier_frequency == 0) { + *on_time_period = 0; + *off_time_period = 0; + return; + } + uint32_t period = (1000000UL + carrier_frequency / 2) / carrier_frequency; // round(1000000/freq) + period = std::max(uint32_t(1), period); + *on_time_period = (period * this->carrier_duty_percent_) / 100; + *off_time_period = period - *on_time_period; +} + +void RemoteTransmitterComponent::await_target_time_() { + const uint32_t current_time = micros(); + if (this->target_time_ == 0) { + this->target_time_ = current_time; + } else if ((int32_t) (this->target_time_ - current_time) > 0) { + delayMicroseconds(this->target_time_ - current_time); + } +} + +void RemoteTransmitterComponent::mark_(uint32_t on_time, uint32_t off_time, uint32_t usec) { + this->await_target_time_(); + this->pin_->digital_write(true); + + const uint32_t target = this->target_time_ + usec; + if (this->carrier_duty_percent_ < 100 && (on_time > 0 || off_time > 0)) { + while (true) { // Modulate with carrier frequency + this->target_time_ += on_time; + if ((int32_t) (this->target_time_ - target) >= 0) + break; + this->await_target_time_(); + this->pin_->digital_write(false); + + this->target_time_ += off_time; + if ((int32_t) (this->target_time_ - target) >= 0) + break; + this->await_target_time_(); + this->pin_->digital_write(true); + } + } + this->target_time_ = target; +} + +void RemoteTransmitterComponent::space_(uint32_t usec) { + this->await_target_time_(); + this->pin_->digital_write(false); + this->target_time_ += usec; +} + +void RemoteTransmitterComponent::digital_write(bool value) { this->pin_->digital_write(value); } + +void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) { + ESP_LOGD(TAG, "Sending remote code"); + uint32_t on_time, off_time; + this->calculate_on_off_time_(this->temp_.get_carrier_frequency(), &on_time, &off_time); + this->target_time_ = 0; + this->transmit_trigger_->trigger(); + for (uint32_t i = 0; i < send_times; i++) { + InterruptLock lock; + for (int32_t item : this->temp_.get_data()) { + if (item > 0) { + const auto length = uint32_t(item); + this->mark_(on_time, off_time, length); + } else { + const auto length = uint32_t(-item); + this->space_(length); + } + App.feed_wdt(); + } + this->await_target_time_(); // wait for duration of last pulse + this->pin_->digital_write(false); + + if (i + 1 < send_times) + this->target_time_ += send_wait; + } + this->complete_trigger_->trigger(); +} + } // namespace remote_transmitter } // namespace esphome + +#endif diff --git a/esphome/components/remote_transmitter/remote_transmitter_esp8266.cpp b/esphome/components/remote_transmitter/remote_transmitter_esp8266.cpp deleted file mode 100644 index fdd4198773..0000000000 --- a/esphome/components/remote_transmitter/remote_transmitter_esp8266.cpp +++ /dev/null @@ -1,107 +0,0 @@ -#include "remote_transmitter.h" -#include "esphome/core/log.h" -#include "esphome/core/application.h" - -#ifdef USE_ESP8266 - -namespace esphome { -namespace remote_transmitter { - -static const char *const TAG = "remote_transmitter"; - -void RemoteTransmitterComponent::setup() { - this->pin_->setup(); - this->pin_->digital_write(false); -} - -void RemoteTransmitterComponent::dump_config() { - ESP_LOGCONFIG(TAG, - "Remote Transmitter:\n" - " Carrier Duty: %u%%", - this->carrier_duty_percent_); - LOG_PIN(" Pin: ", this->pin_); -} - -void RemoteTransmitterComponent::calculate_on_off_time_(uint32_t carrier_frequency, uint32_t *on_time_period, - uint32_t *off_time_period) { - if (carrier_frequency == 0) { - *on_time_period = 0; - *off_time_period = 0; - return; - } - uint32_t period = (1000000UL + carrier_frequency / 2) / carrier_frequency; // round(1000000/freq) - period = std::max(uint32_t(1), period); - *on_time_period = (period * this->carrier_duty_percent_) / 100; - *off_time_period = period - *on_time_period; -} - -void RemoteTransmitterComponent::await_target_time_() { - const uint32_t current_time = micros(); - if (this->target_time_ == 0) { - this->target_time_ = current_time; - } else if ((int32_t) (this->target_time_ - current_time) > 0) { - delayMicroseconds(this->target_time_ - current_time); - } -} - -void RemoteTransmitterComponent::mark_(uint32_t on_time, uint32_t off_time, uint32_t usec) { - this->await_target_time_(); - this->pin_->digital_write(true); - - const uint32_t target = this->target_time_ + usec; - if (this->carrier_duty_percent_ < 100 && (on_time > 0 || off_time > 0)) { - while (true) { // Modulate with carrier frequency - this->target_time_ += on_time; - if ((int32_t) (this->target_time_ - target) >= 0) - break; - this->await_target_time_(); - this->pin_->digital_write(false); - - this->target_time_ += off_time; - if ((int32_t) (this->target_time_ - target) >= 0) - break; - this->await_target_time_(); - this->pin_->digital_write(true); - } - } - this->target_time_ = target; -} - -void RemoteTransmitterComponent::space_(uint32_t usec) { - this->await_target_time_(); - this->pin_->digital_write(false); - this->target_time_ += usec; -} - -void RemoteTransmitterComponent::digital_write(bool value) { this->pin_->digital_write(value); } - -void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) { - ESP_LOGD(TAG, "Sending remote code"); - uint32_t on_time, off_time; - this->calculate_on_off_time_(this->temp_.get_carrier_frequency(), &on_time, &off_time); - this->target_time_ = 0; - this->transmit_trigger_->trigger(); - for (uint32_t i = 0; i < send_times; i++) { - for (int32_t item : this->temp_.get_data()) { - if (item > 0) { - const auto length = uint32_t(item); - this->mark_(on_time, off_time, length); - } else { - const auto length = uint32_t(-item); - this->space_(length); - } - App.feed_wdt(); - } - this->await_target_time_(); // wait for duration of last pulse - this->pin_->digital_write(false); - - if (i + 1 < send_times) - this->target_time_ += send_wait; - } - this->complete_trigger_->trigger(); -} - -} // namespace remote_transmitter -} // namespace esphome - -#endif diff --git a/esphome/components/remote_transmitter/remote_transmitter_libretiny.cpp b/esphome/components/remote_transmitter/remote_transmitter_libretiny.cpp deleted file mode 100644 index 9ba850090d..0000000000 --- a/esphome/components/remote_transmitter/remote_transmitter_libretiny.cpp +++ /dev/null @@ -1,110 +0,0 @@ -#include "remote_transmitter.h" -#include "esphome/core/log.h" -#include "esphome/core/application.h" - -#ifdef USE_LIBRETINY - -namespace esphome { -namespace remote_transmitter { - -static const char *const TAG = "remote_transmitter"; - -void RemoteTransmitterComponent::setup() { - this->pin_->setup(); - this->pin_->digital_write(false); -} - -void RemoteTransmitterComponent::dump_config() { - ESP_LOGCONFIG(TAG, - "Remote Transmitter:\n" - " Carrier Duty: %u%%", - this->carrier_duty_percent_); - LOG_PIN(" Pin: ", this->pin_); -} - -void RemoteTransmitterComponent::calculate_on_off_time_(uint32_t carrier_frequency, uint32_t *on_time_period, - uint32_t *off_time_period) { - if (carrier_frequency == 0) { - *on_time_period = 0; - *off_time_period = 0; - return; - } - uint32_t period = (1000000UL + carrier_frequency / 2) / carrier_frequency; // round(1000000/freq) - period = std::max(uint32_t(1), period); - *on_time_period = (period * this->carrier_duty_percent_) / 100; - *off_time_period = period - *on_time_period; -} - -void RemoteTransmitterComponent::await_target_time_() { - const uint32_t current_time = micros(); - if (this->target_time_ == 0) { - this->target_time_ = current_time; - } else { - while ((int32_t) (this->target_time_ - micros()) > 0) { - // busy loop that ensures micros is constantly called - } - } -} - -void RemoteTransmitterComponent::mark_(uint32_t on_time, uint32_t off_time, uint32_t usec) { - this->await_target_time_(); - this->pin_->digital_write(true); - - const uint32_t target = this->target_time_ + usec; - if (this->carrier_duty_percent_ < 100 && (on_time > 0 || off_time > 0)) { - while (true) { // Modulate with carrier frequency - this->target_time_ += on_time; - if ((int32_t) (this->target_time_ - target) >= 0) - break; - this->await_target_time_(); - this->pin_->digital_write(false); - - this->target_time_ += off_time; - if ((int32_t) (this->target_time_ - target) >= 0) - break; - this->await_target_time_(); - this->pin_->digital_write(true); - } - } - this->target_time_ = target; -} - -void RemoteTransmitterComponent::space_(uint32_t usec) { - this->await_target_time_(); - this->pin_->digital_write(false); - this->target_time_ += usec; -} - -void RemoteTransmitterComponent::digital_write(bool value) { this->pin_->digital_write(value); } - -void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) { - ESP_LOGD(TAG, "Sending remote code"); - uint32_t on_time, off_time; - this->calculate_on_off_time_(this->temp_.get_carrier_frequency(), &on_time, &off_time); - this->target_time_ = 0; - this->transmit_trigger_->trigger(); - for (uint32_t i = 0; i < send_times; i++) { - InterruptLock lock; - for (int32_t item : this->temp_.get_data()) { - if (item > 0) { - const auto length = uint32_t(item); - this->mark_(on_time, off_time, length); - } else { - const auto length = uint32_t(-item); - this->space_(length); - } - App.feed_wdt(); - } - this->await_target_time_(); // wait for duration of last pulse - this->pin_->digital_write(false); - - if (i + 1 < send_times) - this->target_time_ += send_wait; - } - this->complete_trigger_->trigger(); -} - -} // namespace remote_transmitter -} // namespace esphome - -#endif From af0da3f89721566d3b94cf2fe5ada1868e269051 Mon Sep 17 00:00:00 2001 From: esphomebot Date: Thu, 11 Sep 2025 03:41:18 +1200 Subject: [PATCH 06/11] Update webserver local assets to 20250910-110003 (#10668) --- .../components/captive_portal/captive_index.h | 174 +++++----- .../components/web_server/server_index_v2.h | 298 +++++++++--------- 2 files changed, 226 insertions(+), 246 deletions(-) diff --git a/esphome/components/captive_portal/captive_index.h b/esphome/components/captive_portal/captive_index.h index 8835762fb3..3122f27558 100644 --- a/esphome/components/captive_portal/captive_index.h +++ b/esphome/components/captive_portal/captive_index.h @@ -7,103 +7,83 @@ namespace esphome { namespace captive_portal { const uint8_t INDEX_GZ[] PROGMEM = { - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xdd, 0x58, 0x6d, 0x6f, 0xdb, 0x38, 0x12, 0xfe, 0xde, - 0x5f, 0x31, 0xa7, 0x36, 0x6b, 0x6b, 0x1b, 0x51, 0x22, 0xe5, 0xb7, 0xd8, 0x92, 0x16, 0x69, 0xae, 0x8b, 0x5d, 0xa0, - 0xdd, 0x2d, 0x90, 0x6c, 0xef, 0x43, 0x51, 0x20, 0xb4, 0x34, 0xb2, 0xd8, 0x48, 0xa4, 0x4e, 0xa4, 0x5f, 0x52, 0xc3, - 0xf7, 0xdb, 0x0f, 0x94, 0x6c, 0xc7, 0xe9, 0x35, 0x87, 0xeb, 0xe2, 0x0e, 0x87, 0xdd, 0x18, 0x21, 0x86, 0xe4, 0xcc, - 0x70, 0xe6, 0xf1, 0x0c, 0x67, 0xcc, 0xe8, 0x2f, 0x99, 0x4a, 0xcd, 0x7d, 0x8d, 0x50, 0x98, 0xaa, 0x4c, 0x22, 0x3b, - 0x42, 0xc9, 0xe5, 0x22, 0x46, 0x99, 0x44, 0x05, 0xf2, 0x2c, 0x89, 0x2a, 0x34, 0x1c, 0xd2, 0x82, 0x37, 0x1a, 0x4d, - 0xfc, 0xdb, 0xcd, 0x8f, 0xde, 0x04, 0xfc, 0x24, 0x2a, 0x85, 0xbc, 0x83, 0x06, 0xcb, 0x58, 0xa4, 0x4a, 0x42, 0xd1, - 0x60, 0x1e, 0x67, 0xdc, 0xf0, 0xa9, 0xa8, 0xf8, 0x02, 0x2d, 0x43, 0x2b, 0x26, 0x79, 0x85, 0xf1, 0x4a, 0xe0, 0xba, - 0x56, 0x8d, 0x81, 0x54, 0x49, 0x83, 0xd2, 0xc4, 0xce, 0x5a, 0x64, 0xa6, 0x88, 0x33, 0x5c, 0x89, 0x14, 0xbd, 0x76, - 0x72, 0x2e, 0xa4, 0x30, 0x82, 0x97, 0x9e, 0x4e, 0x79, 0x89, 0x31, 0x3d, 0x5f, 0x6a, 0x6c, 0xda, 0x09, 0x9f, 0x97, - 0x18, 0x4b, 0xe5, 0xf8, 0x49, 0xa4, 0xd3, 0x46, 0xd4, 0x06, 0xac, 0xbd, 0x71, 0xa5, 0xb2, 0x65, 0x89, 0x89, 0xef, - 0x73, 0xad, 0xd1, 0x68, 0x5f, 0xc8, 0x0c, 0x37, 0x64, 0x14, 0x86, 0x29, 0xe3, 0xe3, 0x9c, 0x7c, 0xd2, 0xcf, 0x32, - 0x95, 0x2e, 0x2b, 0x94, 0x86, 0x94, 0x2a, 0xe5, 0x46, 0x28, 0x49, 0x34, 0xf2, 0x26, 0x2d, 0xe2, 0x38, 0x76, 0x7e, - 0xd0, 0x7c, 0x85, 0xce, 0x77, 0xdf, 0xf5, 0x8f, 0x4c, 0x0b, 0x34, 0xaf, 0x4b, 0xb4, 0xa4, 0x7e, 0x75, 0x7f, 0xc3, - 0x17, 0xbf, 0xf0, 0x0a, 0xfb, 0x0e, 0xd7, 0x22, 0x43, 0xc7, 0xfd, 0x10, 0x7c, 0x24, 0xda, 0xdc, 0x97, 0x48, 0x32, - 0xa1, 0xeb, 0x92, 0xdf, 0xc7, 0xce, 0xbc, 0x54, 0xe9, 0x9d, 0xe3, 0xce, 0xf2, 0xa5, 0x4c, 0xad, 0x72, 0xd0, 0x7d, - 0x74, 0xb7, 0x25, 0x1a, 0x30, 0xf1, 0x5b, 0x6e, 0x0a, 0x52, 0xf1, 0x4d, 0xbf, 0x23, 0x84, 0xec, 0xb3, 0xef, 0xfb, - 0xf8, 0x92, 0x06, 0x81, 0x7b, 0xde, 0x0e, 0x81, 0xeb, 0xd3, 0x20, 0x98, 0x35, 0x68, 0x96, 0x8d, 0x04, 0xde, 0xbf, - 0x8d, 0x6a, 0x6e, 0x0a, 0xc8, 0x62, 0xa7, 0xa2, 0x8c, 0x04, 0xc1, 0x04, 0xe8, 0x05, 0x61, 0x43, 0x8f, 0x52, 0x12, - 0x7a, 0x74, 0x98, 0x8e, 0xbd, 0x21, 0xd0, 0x81, 0x37, 0x04, 0xc6, 0xc8, 0x10, 0x82, 0xcf, 0x0e, 0xe4, 0xa2, 0x2c, - 0x63, 0x47, 0x2a, 0x89, 0x0e, 0x68, 0xd3, 0xa8, 0x3b, 0x8c, 0x9d, 0x74, 0xd9, 0x34, 0x28, 0xcd, 0x95, 0x2a, 0x55, - 0xe3, 0xf8, 0xc9, 0x33, 0x78, 0xf4, 0xf7, 0xcd, 0x47, 0x98, 0x86, 0x4b, 0x9d, 0xab, 0xa6, 0x8a, 0x9d, 0xf6, 0x4b, - 0xe9, 0xbf, 0xd8, 0x9a, 0x1d, 0xd8, 0xc1, 0x3d, 0xd9, 0xf4, 0x54, 0x23, 0x16, 0x42, 0xc6, 0x0e, 0x65, 0x40, 0x27, - 0x8e, 0x9f, 0xdc, 0xba, 0xbb, 0x23, 0x26, 0xdc, 0x62, 0xb2, 0xf7, 0x52, 0xf5, 0x3f, 0xdc, 0x46, 0x7a, 0xb5, 0x80, - 0x4d, 0x55, 0x4a, 0x1d, 0x3b, 0x85, 0x31, 0xf5, 0xd4, 0xf7, 0xd7, 0xeb, 0x35, 0x59, 0x87, 0x44, 0x35, 0x0b, 0x9f, - 0x05, 0x41, 0xe0, 0xeb, 0xd5, 0xc2, 0x81, 0x2e, 0x3e, 0x1c, 0x36, 0x70, 0xa0, 0x40, 0xb1, 0x28, 0x4c, 0x4b, 0x27, - 0x2f, 0xb6, 0xb8, 0x8b, 0x2c, 0x47, 0x72, 0xfb, 0xf1, 0xe4, 0x14, 0x71, 0x72, 0x0a, 0xfe, 0x70, 0x82, 0x66, 0xef, - 0xad, 0x35, 0x6a, 0xcc, 0x19, 0x30, 0x08, 0xda, 0x0f, 0xf3, 0x2c, 0xbd, 0x9f, 0x79, 0x5f, 0xcc, 0xe0, 0x64, 0x06, - 0x0c, 0x9e, 0x01, 0xb0, 0x6a, 0xe4, 0x5d, 0x1c, 0xc5, 0xa9, 0xdd, 0x5e, 0xd1, 0xe0, 0x61, 0xc1, 0xca, 0xfc, 0x34, - 0x3a, 0x9d, 0x7b, 0xec, 0xbd, 0x65, 0xb0, 0xd8, 0x1f, 0x85, 0x3c, 0x56, 0xd0, 0xf7, 0x23, 0x3e, 0x84, 0xe1, 0x7e, - 0x65, 0xe8, 0x59, 0xfa, 0x38, 0xb3, 0x27, 0xc1, 0x70, 0xc5, 0x0a, 0x5a, 0x79, 0x23, 0x6f, 0xc8, 0x43, 0x08, 0xf7, - 0x26, 0x85, 0x10, 0xae, 0x58, 0x31, 0x7a, 0x3f, 0x3a, 0x5d, 0xf3, 0xc2, 0xcf, 0x3d, 0x0b, 0xf3, 0xd4, 0x71, 0x1e, - 0x30, 0x50, 0xa7, 0x18, 0x90, 0x4f, 0x4a, 0xc8, 0xbe, 0xe3, 0xb8, 0xbb, 0x1c, 0x4d, 0x5a, 0xf4, 0x1d, 0x3f, 0x55, - 0x32, 0x17, 0x0b, 0xf2, 0x49, 0x2b, 0xe9, 0xb8, 0xc4, 0x14, 0x28, 0xfb, 0x07, 0x51, 0x2b, 0x88, 0xed, 0x4e, 0xff, - 0xcb, 0x1d, 0xe3, 0x6e, 0x8f, 0xf9, 0x61, 0x84, 0x29, 0x31, 0x36, 0xc4, 0x66, 0xf4, 0xf9, 0x71, 0x75, 0xae, 0xb2, - 0xfb, 0x27, 0x52, 0xa7, 0xa0, 0x5d, 0xde, 0x08, 0x29, 0xb1, 0xb9, 0xc1, 0x8d, 0x89, 0x9d, 0xb7, 0x97, 0x57, 0x70, - 0x99, 0x65, 0x0d, 0x6a, 0x3d, 0x05, 0xe7, 0xa5, 0x21, 0x15, 0x4f, 0xff, 0x73, 0x5d, 0xf4, 0x91, 0xae, 0xbf, 0x89, - 0x1f, 0x05, 0xfc, 0x82, 0x66, 0xad, 0x9a, 0xbb, 0xbd, 0x36, 0x6b, 0xda, 0xcc, 0x66, 0x60, 0x13, 0x1b, 0xc2, 0x6b, - 0x4d, 0x74, 0x29, 0x52, 0xec, 0x53, 0x97, 0x54, 0xbc, 0x7e, 0xf0, 0x4a, 0x1e, 0x80, 0xba, 0x8d, 0x32, 0xb1, 0x82, - 0xb4, 0xe4, 0x5a, 0xc7, 0x8e, 0xec, 0x54, 0x39, 0xb0, 0x4f, 0x1b, 0x25, 0xd3, 0x52, 0xa4, 0x77, 0xb1, 0xf3, 0x95, - 0x1b, 0xe2, 0xd5, 0xfd, 0xcf, 0x59, 0xbf, 0xa7, 0xb5, 0xc8, 0x7a, 0x2e, 0x59, 0xf1, 0x72, 0x89, 0x10, 0x83, 0x29, - 0x84, 0x7e, 0x30, 0x70, 0xf6, 0xa4, 0x58, 0xad, 0xef, 0x7a, 0x2e, 0xc9, 0x55, 0xba, 0xd4, 0x7d, 0xd7, 0x39, 0x64, - 0x69, 0xc4, 0xbb, 0x3b, 0xd4, 0x79, 0xee, 0x7c, 0x61, 0x91, 0x57, 0x62, 0x6e, 0x9c, 0x87, 0x6c, 0x7e, 0xb1, 0xd5, - 0x7d, 0x49, 0x1a, 0xad, 0x85, 0xbb, 0x3b, 0x2e, 0x46, 0xba, 0xe6, 0xf2, 0x4b, 0x41, 0x6b, 0xa0, 0x4d, 0x1a, 0x49, - 0x2c, 0x65, 0x33, 0xa7, 0xe6, 0xf2, 0x78, 0xa0, 0xcf, 0x0f, 0xe4, 0x8b, 0xad, 0xe8, 0x4b, 0x7b, 0x4b, 0xde, 0x1d, - 0x35, 0x46, 0x7e, 0x26, 0x56, 0xc9, 0xed, 0xce, 0x7d, 0xf0, 0xe3, 0xef, 0x4b, 0x6c, 0xee, 0xaf, 0xb1, 0xc4, 0xd4, - 0xa8, 0xa6, 0xef, 0x3c, 0x97, 0x68, 0x1c, 0xb7, 0x73, 0xf8, 0xa7, 0x9b, 0xb7, 0x6f, 0x62, 0xd5, 0x6f, 0xdc, 0xf3, - 0xa7, 0xb8, 0x6d, 0xb5, 0xf8, 0xd0, 0x60, 0xf9, 0x8f, 0xb8, 0x67, 0xeb, 0x45, 0xef, 0xa3, 0xe3, 0x92, 0xd6, 0xdf, - 0xdb, 0x87, 0xa2, 0x61, 0x13, 0xfb, 0xe5, 0xa6, 0x2a, 0xcf, 0xad, 0x87, 0xde, 0x68, 0xe8, 0xee, 0x6e, 0x77, 0xee, - 0xce, 0x9d, 0x45, 0x7e, 0x77, 0xef, 0x27, 0x51, 0x7b, 0x05, 0x27, 0xdf, 0x6f, 0xe7, 0x6a, 0xe3, 0x69, 0xf1, 0x59, - 0xc8, 0xc5, 0x54, 0xc8, 0x02, 0x1b, 0x61, 0x76, 0x99, 0x58, 0x9d, 0x0b, 0x59, 0x2f, 0xcd, 0xb6, 0xe6, 0x59, 0x66, - 0x77, 0x86, 0xf5, 0x66, 0x96, 0x2b, 0x69, 0x2c, 0x27, 0x4e, 0x29, 0x56, 0xbb, 0x6e, 0xbf, 0xbd, 0x5b, 0xa6, 0x17, - 0xc3, 0xb3, 0x9d, 0x0d, 0xb8, 0xad, 0xc1, 0x8d, 0xf1, 0x78, 0x29, 0x16, 0x72, 0x9a, 0xa2, 0x34, 0xd8, 0x74, 0x42, - 0x39, 0xaf, 0x44, 0x79, 0x3f, 0xd5, 0x5c, 0x6a, 0x4f, 0x63, 0x23, 0xf2, 0xdd, 0x7c, 0x69, 0x8c, 0x92, 0xdb, 0xb9, - 0x6a, 0x32, 0x6c, 0xa6, 0xc1, 0xac, 0x23, 0xbc, 0x86, 0x67, 0x62, 0xa9, 0xa7, 0x24, 0x6c, 0xb0, 0x9a, 0xcd, 0x79, - 0x7a, 0xb7, 0x68, 0xd4, 0x52, 0x66, 0x5e, 0x6a, 0x6f, 0xe1, 0xe9, 0x73, 0x9a, 0xf3, 0x10, 0xd3, 0xd9, 0x7e, 0x96, - 0xe7, 0xf9, 0xac, 0x14, 0x12, 0xbd, 0xee, 0x56, 0x9b, 0x32, 0x32, 0xb0, 0x62, 0x27, 0x66, 0x12, 0x66, 0x17, 0x3a, - 0x1b, 0x69, 0x10, 0x9c, 0xcd, 0x0e, 0xee, 0x04, 0xb3, 0x74, 0xd9, 0x68, 0xd5, 0x4c, 0x6b, 0x25, 0xac, 0x99, 0xbb, - 0x8a, 0x0b, 0x79, 0x6a, 0xbd, 0x0d, 0x93, 0xd9, 0xbe, 0x3c, 0x4d, 0x85, 0x6c, 0x8f, 0x69, 0x8b, 0xd4, 0xac, 0x12, - 0xb2, 0x2b, 0xb2, 0x53, 0x36, 0x0a, 0xea, 0xcd, 0x8e, 0xec, 0x03, 0x64, 0x7b, 0xe0, 0xce, 0x4b, 0xdc, 0xcc, 0x3e, - 0x2d, 0xb5, 0x11, 0xf9, 0xbd, 0xb7, 0x2f, 0xd2, 0x53, 0x5d, 0xf3, 0x14, 0xbd, 0x39, 0x9a, 0x35, 0xa2, 0x9c, 0xb5, - 0x67, 0x78, 0xc2, 0x60, 0xa5, 0xf7, 0x38, 0x1d, 0xd5, 0xb4, 0x01, 0xfa, 0x58, 0xd7, 0xbf, 0xe3, 0xb6, 0xb1, 0xb8, - 0xad, 0x78, 0xb3, 0x10, 0xd2, 0x9b, 0x2b, 0x63, 0x54, 0x35, 0xf5, 0xc6, 0xf5, 0x66, 0xb6, 0x5f, 0xb2, 0xca, 0xa6, - 0xd4, 0x9a, 0xd9, 0xd6, 0xde, 0x03, 0xde, 0xb4, 0xde, 0x80, 0x56, 0xa5, 0xc8, 0xf6, 0x7c, 0x2d, 0x0b, 0x04, 0x47, - 0x78, 0xe8, 0xb0, 0xde, 0x80, 0x5d, 0x3b, 0x40, 0x3d, 0xc8, 0x27, 0x9c, 0x06, 0x5f, 0xf9, 0x46, 0xb2, 0x3c, 0x67, - 0xf3, 0xfc, 0x88, 0x94, 0x2d, 0xa1, 0x3b, 0xb1, 0x8f, 0x0a, 0x36, 0xa8, 0x37, 0xb3, 0xc3, 0x77, 0x33, 0xa8, 0x37, - 0x3b, 0xd1, 0xa6, 0xc5, 0xf6, 0x44, 0x4b, 0x1b, 0xaa, 0xd3, 0x65, 0x53, 0xf6, 0x9d, 0xaf, 0x84, 0xee, 0x59, 0x78, - 0xf5, 0x50, 0xe2, 0x7a, 0x4f, 0x97, 0xb8, 0x1e, 0xd8, 0xa6, 0xe8, 0x95, 0xda, 0xc4, 0xbd, 0xb6, 0xd8, 0x0c, 0x80, - 0x0d, 0x7a, 0x67, 0xe1, 0xeb, 0xb3, 0xf0, 0xea, 0xbf, 0x52, 0xbb, 0x7e, 0x77, 0xe1, 0xfa, 0x86, 0xaa, 0xf5, 0x8d, - 0x15, 0xab, 0xf3, 0xce, 0x3a, 0x7f, 0x16, 0xbe, 0x76, 0xdc, 0x9d, 0x20, 0x5a, 0x2c, 0xe8, 0xff, 0x02, 0xda, 0x7f, - 0xc5, 0x31, 0xbc, 0xa4, 0x13, 0x72, 0x01, 0xed, 0xd0, 0x41, 0x44, 0xc2, 0x09, 0x8c, 0xaf, 0x06, 0x64, 0x40, 0xc1, - 0xb6, 0x43, 0x23, 0x18, 0x93, 0xc9, 0x05, 0xd0, 0x11, 0x09, 0xc7, 0x40, 0x19, 0x30, 0x4a, 0x86, 0x6f, 0x58, 0x48, - 0x46, 0x43, 0x18, 0x5f, 0xb1, 0x80, 0x84, 0x0c, 0x3a, 0xde, 0x11, 0x61, 0x0c, 0x42, 0xcb, 0x12, 0x56, 0x01, 0xb0, - 0x34, 0x24, 0xc1, 0x18, 0x02, 0x18, 0x91, 0xe0, 0x82, 0x4c, 0x46, 0x30, 0x21, 0x63, 0x0a, 0x8c, 0x0c, 0x86, 0xa5, - 0x37, 0x24, 0x14, 0x46, 0x24, 0x1c, 0xf1, 0x09, 0x19, 0x84, 0xd0, 0x0e, 0x1d, 0x1c, 0x63, 0xc2, 0x98, 0x47, 0x02, - 0xfa, 0x26, 0x24, 0x6c, 0x0c, 0x63, 0x32, 0x18, 0x5c, 0xd2, 0x11, 0xb9, 0x18, 0x40, 0x37, 0x76, 0xf0, 0x52, 0x06, - 0xc3, 0xa7, 0x40, 0x63, 0x7f, 0x5e, 0xd0, 0x42, 0xc2, 0x28, 0x84, 0xe4, 0x62, 0xc2, 0x6d, 0x5f, 0xca, 0xa0, 0x1b, - 0x3b, 0xdc, 0x28, 0x85, 0xe0, 0x77, 0x63, 0x16, 0xfe, 0x79, 0x31, 0xa3, 0x16, 0x01, 0x46, 0x06, 0xe1, 0x25, 0x0d, - 0xc9, 0x08, 0xda, 0xa1, 0x3b, 0x9b, 0x32, 0x98, 0x5c, 0x5d, 0xc0, 0x04, 0x46, 0x64, 0x34, 0x81, 0x0b, 0x18, 0x5a, - 0x74, 0x2f, 0xc8, 0x64, 0xd0, 0x09, 0x79, 0x8c, 0x7c, 0x2b, 0x8c, 0x83, 0x3f, 0x30, 0x8c, 0x4f, 0xf9, 0xf4, 0x07, - 0x76, 0xe9, 0xff, 0x71, 0x05, 0x45, 0x7e, 0xd7, 0x86, 0x45, 0x7e, 0xf7, 0x3c, 0x60, 0xbb, 0xa8, 0x24, 0xb2, 0xdd, - 0x48, 0x12, 0x15, 0x14, 0x44, 0x16, 0x57, 0x3c, 0x4d, 0x4e, 0x5a, 0xfd, 0xc8, 0x2f, 0xe8, 0x61, 0xab, 0xa0, 0xc9, - 0xa3, 0xc6, 0xbd, 0xdb, 0x6b, 0x2b, 0x7d, 0x72, 0x53, 0x20, 0xbc, 0xbe, 0x7e, 0x07, 0x6b, 0x51, 0x96, 0x20, 0xd5, - 0x1a, 0x4c, 0x73, 0x0f, 0x46, 0xd9, 0x57, 0x03, 0x89, 0xa9, 0xb1, 0xa4, 0x29, 0x10, 0xf6, 0x7d, 0x04, 0x21, 0x24, - 0x9a, 0x37, 0xc9, 0xbb, 0x12, 0xb9, 0x46, 0x58, 0x88, 0x15, 0x82, 0x30, 0xa0, 0x55, 0x85, 0x60, 0x84, 0x1d, 0x8e, - 0x82, 0x2d, 0x5f, 0xe4, 0x77, 0x87, 0x74, 0x8d, 0xb2, 0xc8, 0x62, 0x89, 0x26, 0xd9, 0x77, 0xc4, 0x51, 0x11, 0x76, - 0x56, 0x5d, 0xa3, 0x31, 0x42, 0x2e, 0xac, 0x55, 0x61, 0x12, 0xd9, 0x5f, 0xb7, 0xc0, 0xdb, 0xdf, 0x0c, 0xb1, 0xbf, - 0x16, 0xb9, 0xb0, 0x6f, 0x06, 0x49, 0xd4, 0x76, 0x91, 0x56, 0x83, 0x6d, 0x64, 0xba, 0x07, 0x8e, 0x96, 0x2a, 0x51, - 0x2e, 0x4c, 0x11, 0x87, 0x0c, 0xea, 0x92, 0xa7, 0x58, 0xa8, 0x32, 0xc3, 0x26, 0xbe, 0xbe, 0xfe, 0xf9, 0xaf, 0xf6, - 0x35, 0xc4, 0x9a, 0x70, 0x94, 0xac, 0xf5, 0x5d, 0x27, 0x68, 0x89, 0xbd, 0xdc, 0x68, 0xd0, 0xbd, 0x6b, 0xd4, 0x5c, - 0xeb, 0xb5, 0x6a, 0xb2, 0x47, 0x5a, 0xde, 0x1d, 0x16, 0xf7, 0x9a, 0xda, 0xff, 0xb6, 0x1f, 0xed, 0x84, 0xf4, 0x72, - 0x5e, 0x09, 0x93, 0x5c, 0xf3, 0x15, 0x46, 0x7e, 0xb7, 0x91, 0x44, 0xbe, 0x75, 0xa0, 0xe3, 0x2d, 0xf6, 0x32, 0x05, - 0x4d, 0x7e, 0xbd, 0xb9, 0x84, 0xdf, 0xea, 0x8c, 0x1b, 0xec, 0xb0, 0x6f, 0xbd, 0xac, 0xd0, 0x14, 0x2a, 0x8b, 0xdf, - 0xfd, 0x7a, 0x7d, 0x73, 0xf4, 0x78, 0xd9, 0x32, 0x01, 0xca, 0xb4, 0x7b, 0x6f, 0x59, 0x96, 0x46, 0xd4, 0xbc, 0x31, - 0xad, 0x5a, 0xcf, 0x66, 0xc7, 0xc1, 0xa3, 0x76, 0x3f, 0x17, 0x25, 0x76, 0x4e, 0xed, 0x05, 0xfd, 0x04, 0xbe, 0x66, - 0xe3, 0xe1, 0xec, 0x2f, 0xac, 0xf4, 0xbb, 0x00, 0xf2, 0xbb, 0x68, 0xf2, 0xdb, 0xd7, 0xa8, 0x7f, 0x02, 0x14, 0xee, - 0xbc, 0x64, 0x9d, 0x12, 0x00, 0x00}; + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x95, 0x16, 0x6b, 0x8f, 0xdb, 0x36, 0xf2, 0x7b, 0x7e, + 0x05, 0x8f, 0x49, 0xbb, 0x52, 0xb3, 0x7a, 0x7a, 0xed, 0x6c, 0x24, 0x51, 0x45, 0x9a, 0xbb, 0xa2, 0x05, 0x9a, 0x36, + 0xc0, 0x6e, 0x73, 0x1f, 0x82, 0x00, 0x4b, 0x53, 0x23, 0x8b, 0x31, 0x45, 0xea, 0x48, 0xca, 0x8f, 0x18, 0xbe, 0xdf, + 0x7e, 0xa0, 0x24, 0x7b, 0x9d, 0x45, 0x73, 0xb8, 0xb3, 0x60, 0x61, 0x38, 0xef, 0x19, 0xcd, 0x83, 0xc5, 0xdf, 0x2a, + 0xc5, 0xec, 0xbe, 0x03, 0xd4, 0xd8, 0x56, 0x94, 0x85, 0x7b, 0x23, 0x41, 0xe5, 0x8a, 0x80, 0x2c, 0x8b, 0x06, 0x68, + 0x55, 0x16, 0x2d, 0x58, 0x8a, 0x58, 0x43, 0xb5, 0x01, 0x4b, 0xfe, 0xbc, 0xff, 0x39, 0xb8, 0x2d, 0x0b, 0xc1, 0xe5, + 0x1a, 0x69, 0x10, 0x84, 0x33, 0x25, 0x51, 0xa3, 0xa1, 0x26, 0x15, 0xb5, 0x34, 0xe3, 0x2d, 0x5d, 0xc1, 0x24, 0x22, + 0x69, 0x0b, 0x64, 0xc3, 0x61, 0xdb, 0x29, 0x6d, 0x11, 0x53, 0xd2, 0x82, 0xb4, 0x04, 0x6f, 0x79, 0x65, 0x1b, 0x52, + 0xc1, 0x86, 0x33, 0x08, 0x86, 0xc3, 0x35, 0x97, 0xdc, 0x72, 0x2a, 0x02, 0xc3, 0xa8, 0x00, 0x92, 0x5c, 0xf7, 0x06, + 0xf4, 0x70, 0xa0, 0x4b, 0x01, 0x44, 0x2a, 0x5c, 0x16, 0x86, 0x69, 0xde, 0x59, 0xe4, 0x5c, 0x25, 0xad, 0xaa, 0x7a, + 0x01, 0x65, 0x14, 0x51, 0x63, 0xc0, 0x9a, 0x88, 0xcb, 0x0a, 0x76, 0xe1, 0x32, 0x66, 0x2c, 0x86, 0xdb, 0xdb, 0xf0, + 0xb3, 0x79, 0x56, 0x29, 0xd6, 0xb7, 0x20, 0x6d, 0x28, 0x14, 0xa3, 0x96, 0x2b, 0x19, 0x1a, 0xa0, 0x9a, 0x35, 0x84, + 0x10, 0xfc, 0xa3, 0xa1, 0x1b, 0xc0, 0xdf, 0x7f, 0xef, 0x9d, 0x99, 0x56, 0x60, 0xff, 0x21, 0xc0, 0x81, 0xe6, 0xa7, + 0xfd, 0x3d, 0x5d, 0xfd, 0x4e, 0x5b, 0xf0, 0x30, 0x35, 0xbc, 0x02, 0xec, 0x7f, 0x8c, 0x3f, 0x85, 0xc6, 0xee, 0x05, + 0x84, 0x15, 0x37, 0x9d, 0xa0, 0x7b, 0x82, 0x97, 0x42, 0xb1, 0x35, 0xf6, 0xf3, 0xba, 0x97, 0xcc, 0x29, 0x47, 0xc6, + 0x03, 0xff, 0x20, 0xc0, 0x22, 0x4b, 0xde, 0x51, 0xdb, 0x84, 0x2d, 0xdd, 0x79, 0x23, 0xc0, 0xa5, 0x97, 0xfe, 0xe0, + 0xc1, 0xcb, 0x24, 0x8e, 0xfd, 0xeb, 0xe1, 0x15, 0xfb, 0x51, 0x12, 0xc7, 0xb9, 0x06, 0xdb, 0x6b, 0x89, 0xa8, 0xf7, + 0x50, 0x74, 0xd4, 0x36, 0xa8, 0x22, 0xf8, 0x5d, 0x92, 0xa2, 0xe4, 0x75, 0x98, 0xce, 0x7f, 0x0b, 0x5f, 0xa1, 0x9b, + 0x30, 0x9d, 0xb3, 0x57, 0xc1, 0x1c, 0x25, 0x37, 0xc1, 0x1c, 0xa5, 0x69, 0x38, 0x47, 0xf1, 0x17, 0x8c, 0x6a, 0x2e, + 0x04, 0xc1, 0x52, 0x49, 0xc0, 0xc8, 0x58, 0xad, 0xd6, 0x40, 0x30, 0xeb, 0xb5, 0x06, 0x69, 0xdf, 0x2a, 0xa1, 0x34, + 0x8e, 0xca, 0x67, 0xff, 0x97, 0x42, 0xab, 0xa9, 0x34, 0xb5, 0xd2, 0x2d, 0xc1, 0x43, 0xf6, 0xbd, 0x17, 0x07, 0x7b, + 0x44, 0xee, 0xe5, 0x5f, 0x10, 0x03, 0xa5, 0xf9, 0x8a, 0x4b, 0x82, 0x9d, 0xc6, 0x5b, 0x1c, 0x95, 0x0f, 0xfe, 0xf1, + 0x1c, 0x3d, 0x75, 0xd1, 0x4f, 0xf1, 0x28, 0xef, 0xe3, 0x43, 0x61, 0x36, 0x2b, 0xb4, 0x6b, 0x85, 0x34, 0x04, 0x37, + 0xd6, 0x76, 0x59, 0x14, 0x6d, 0xb7, 0xdb, 0x70, 0x3b, 0x0b, 0x95, 0x5e, 0x45, 0x69, 0x1c, 0xc7, 0x91, 0xd9, 0xac, + 0x30, 0x1a, 0x0b, 0x01, 0xa7, 0x37, 0x18, 0x35, 0xc0, 0x57, 0x8d, 0x1d, 0xe0, 0xf2, 0xc5, 0x01, 0x8e, 0x85, 0xe3, + 0x28, 0x1f, 0x3e, 0x5d, 0x58, 0xe1, 0x17, 0x56, 0xe0, 0x47, 0xea, 0xe1, 0x53, 0x98, 0x57, 0x43, 0x98, 0xaf, 0x68, + 0x8a, 0x52, 0x14, 0x0f, 0x4f, 0x1a, 0x38, 0x78, 0x3a, 0x05, 0x4f, 0x4e, 0xe8, 0xe2, 0xe4, 0xa0, 0x76, 0x11, 0xbc, + 0x3e, 0xcb, 0x26, 0x0e, 0xb3, 0x49, 0xe2, 0x47, 0x84, 0x13, 0xf8, 0x65, 0x71, 0x79, 0x0e, 0xd2, 0x0f, 0x97, 0x0c, + 0xce, 0x5a, 0x93, 0x7c, 0x58, 0xd0, 0x39, 0x9a, 0x4f, 0x98, 0x79, 0xe0, 0xe0, 0xf3, 0x09, 0xcd, 0x37, 0x69, 0x93, + 0xb4, 0xc1, 0x22, 0x98, 0xd3, 0x19, 0x9a, 0x4d, 0x8e, 0xcc, 0xd0, 0x6c, 0x93, 0x36, 0x8b, 0x0f, 0x8b, 0x4b, 0x5c, + 0x30, 0xfb, 0x72, 0x15, 0x95, 0xd8, 0xcf, 0x30, 0x7e, 0x8c, 0x5c, 0x5d, 0x46, 0x1e, 0x7e, 0x56, 0x5c, 0x7a, 0x18, + 0xfb, 0xc7, 0x1a, 0x2c, 0x6b, 0x3c, 0x1c, 0x31, 0x25, 0x6b, 0xbe, 0x0a, 0x3f, 0x1b, 0x25, 0xb1, 0x1f, 0xda, 0x06, + 0xa4, 0x77, 0x12, 0x75, 0x82, 0x30, 0x50, 0xbc, 0xa7, 0x14, 0xeb, 0x1f, 0xce, 0xf5, 0x6f, 0xb9, 0x15, 0x40, 0x6c, + 0xe8, 0x1a, 0xf6, 0xfa, 0x8c, 0x5d, 0xaa, 0x6a, 0xff, 0x8d, 0xd6, 0x68, 0x92, 0xb1, 0x2f, 0xb8, 0x94, 0xa0, 0xef, + 0x61, 0x67, 0x09, 0x7e, 0xf7, 0xe6, 0x2d, 0x7a, 0x53, 0x55, 0x1a, 0x8c, 0xc9, 0x10, 0x7e, 0x69, 0xc3, 0x96, 0xb2, + 0xff, 0x5d, 0x57, 0xf2, 0x95, 0xae, 0x7f, 0xf2, 0x9f, 0x39, 0xfa, 0x1d, 0xec, 0x56, 0xe9, 0xf5, 0xa4, 0xcd, 0xb9, + 0x96, 0xbb, 0x0e, 0xd3, 0xc4, 0x86, 0xb4, 0x33, 0xa1, 0x11, 0x9c, 0x81, 0x97, 0xf8, 0x61, 0x4b, 0xbb, 0xc7, 0xa8, + 0xe4, 0x29, 0x51, 0x0f, 0x45, 0xc5, 0x37, 0x88, 0x09, 0x6a, 0x0c, 0xc1, 0x72, 0x54, 0x85, 0xd1, 0x33, 0x34, 0xfc, + 0x94, 0x64, 0x82, 0xb3, 0x35, 0xc1, 0x7f, 0x31, 0x01, 0x7e, 0xda, 0xff, 0x5a, 0x79, 0x57, 0xc6, 0xf0, 0xea, 0xca, + 0x0f, 0x37, 0x54, 0xf4, 0x80, 0x08, 0xb2, 0x0d, 0x37, 0x8f, 0x0e, 0xe6, 0xdf, 0x14, 0xeb, 0xcc, 0xfa, 0xca, 0x0f, + 0x6b, 0xc5, 0x7a, 0xe3, 0xf9, 0xb8, 0x9c, 0xcc, 0x15, 0x74, 0x1c, 0x90, 0xf8, 0x39, 0x7e, 0xe2, 0x51, 0x20, 0xa0, + 0xb6, 0x67, 0x3e, 0x84, 0x5e, 0x1c, 0x8c, 0x27, 0x43, 0x6d, 0x0c, 0xf7, 0x8f, 0x67, 0x64, 0x61, 0x3a, 0x2a, 0x9f, + 0x0a, 0x3a, 0x07, 0x5d, 0xab, 0xc8, 0xd0, 0x41, 0xae, 0x5f, 0x3a, 0x2a, 0xcf, 0x06, 0x23, 0x7a, 0x02, 0x5f, 0x1c, + 0xb8, 0x27, 0xdd, 0x14, 0x5c, 0x9f, 0x35, 0x16, 0x51, 0xc5, 0x37, 0xe5, 0xc3, 0xd1, 0x7f, 0x8c, 0xe3, 0x5f, 0x3d, + 0xe8, 0xfd, 0x1d, 0x08, 0x60, 0x56, 0x69, 0x0f, 0x3f, 0x97, 0x60, 0xb1, 0x3f, 0x06, 0xfc, 0xcb, 0xfd, 0xbb, 0xdf, + 0x88, 0xf2, 0xb4, 0x7f, 0xfd, 0x2d, 0x6e, 0xb7, 0x0a, 0x3e, 0x6a, 0x10, 0xff, 0x26, 0x57, 0x6e, 0x19, 0x5c, 0x7d, + 0xc2, 0x7e, 0x38, 0xc4, 0xfb, 0xf0, 0xb8, 0x11, 0x5c, 0x3b, 0xbf, 0xdc, 0xb5, 0xe2, 0xda, 0x45, 0x18, 0x2c, 0xe6, + 0xfe, 0xf1, 0xe1, 0xe8, 0x1f, 0xfd, 0xbc, 0x88, 0xc6, 0xb9, 0x5e, 0x16, 0xc3, 0x88, 0x2d, 0x7f, 0x38, 0x2c, 0xd5, + 0x2e, 0x30, 0xfc, 0x0b, 0x97, 0xab, 0x8c, 0xcb, 0x06, 0x34, 0xb7, 0xc7, 0x8a, 0x6f, 0xae, 0xb9, 0xec, 0x7a, 0x7b, + 0xe8, 0x68, 0x55, 0x39, 0xca, 0xbc, 0xdb, 0xe5, 0xb5, 0x92, 0xd6, 0x71, 0x42, 0x96, 0x40, 0x7b, 0x1c, 0xe9, 0xc3, + 0x44, 0xc9, 0x5e, 0xcf, 0xbf, 0x3b, 0xba, 0x82, 0x3b, 0x58, 0xd8, 0xd9, 0x80, 0x0a, 0xbe, 0x92, 0x19, 0x03, 0x69, + 0x41, 0x8f, 0x42, 0x35, 0x6d, 0xb9, 0xd8, 0x67, 0x86, 0x4a, 0x13, 0x18, 0xd0, 0xbc, 0x3e, 0x2e, 0x7b, 0x6b, 0x95, + 0x3c, 0x2c, 0x95, 0xae, 0x40, 0x67, 0x71, 0x3e, 0x02, 0x81, 0xa6, 0x15, 0xef, 0x4d, 0x16, 0xce, 0x34, 0xb4, 0xf9, + 0x92, 0xb2, 0xf5, 0x4a, 0xab, 0x5e, 0x56, 0x01, 0x73, 0x93, 0x36, 0x7b, 0x9e, 0xd4, 0x74, 0x06, 0x2c, 0x9f, 0x4e, + 0x75, 0x5d, 0xe7, 0x82, 0x4b, 0x08, 0xc6, 0x59, 0x96, 0xa5, 0xe1, 0x8d, 0x13, 0xbb, 0x70, 0x33, 0x4c, 0x1d, 0x62, + 0xf4, 0x31, 0x89, 0xe3, 0xef, 0xf2, 0x53, 0x38, 0x71, 0xce, 0x7a, 0x6d, 0x94, 0xce, 0x3a, 0xc5, 0x9d, 0x9b, 0xc7, + 0x96, 0x72, 0x79, 0xe9, 0xbd, 0x2b, 0x93, 0x7c, 0x5a, 0x3f, 0x19, 0x97, 0x83, 0x99, 0x61, 0x09, 0xe5, 0x2d, 0x97, + 0xe3, 0x0e, 0xcd, 0xd2, 0x45, 0xdc, 0xed, 0x8e, 0xe1, 0x54, 0x20, 0x87, 0x13, 0x77, 0x2d, 0x60, 0x97, 0x7f, 0xee, + 0x8d, 0xe5, 0xf5, 0x3e, 0x98, 0x76, 0x70, 0x66, 0x3a, 0xca, 0x20, 0x58, 0x82, 0xdd, 0x02, 0xc8, 0x7c, 0xb0, 0x11, + 0x70, 0x0b, 0xad, 0x99, 0xf2, 0x74, 0x56, 0x33, 0x14, 0xe8, 0xd7, 0xba, 0xfe, 0x1b, 0xb7, 0xab, 0xc5, 0x43, 0x4b, + 0xf5, 0x8a, 0xcb, 0x60, 0xa9, 0xac, 0x55, 0x6d, 0x16, 0xbc, 0xea, 0x76, 0xf9, 0x84, 0x72, 0xca, 0xb2, 0xc4, 0xb9, + 0x39, 0xec, 0xd6, 0x53, 0xbe, 0x93, 0x6e, 0x87, 0x8c, 0x12, 0xbc, 0x9a, 0xf8, 0x06, 0x16, 0x14, 0x9f, 0xd3, 0x93, + 0xcc, 0xbb, 0x1d, 0x72, 0xb8, 0x53, 0xaa, 0x6f, 0xea, 0x5b, 0x9a, 0xc4, 0x7f, 0xf1, 0x45, 0xaa, 0xba, 0x4e, 0x97, + 0xf5, 0x39, 0x53, 0x6e, 0x4d, 0xba, 0xd6, 0x18, 0x4a, 0xab, 0x88, 0xc6, 0xdb, 0x8c, 0xab, 0x8c, 0xb2, 0x70, 0x19, + 0x2e, 0x8b, 0x26, 0x41, 0xbc, 0x22, 0x2d, 0x65, 0xe5, 0xc5, 0xf8, 0x2a, 0xa2, 0x26, 0x39, 0x91, 0x9a, 0xa4, 0xfc, + 0x6a, 0x18, 0x8d, 0xb4, 0xc1, 0xfb, 0xf2, 0xad, 0x92, 0x12, 0x98, 0xe5, 0x72, 0x85, 0xac, 0x42, 0x53, 0x0a, 0xc2, + 0x30, 0x2c, 0x96, 0xba, 0x7c, 0x2f, 0x80, 0x1a, 0x40, 0x5b, 0xca, 0x6d, 0x58, 0x44, 0x23, 0xff, 0xd8, 0xc7, 0xbc, + 0x22, 0x12, 0x6c, 0x39, 0x35, 0x6c, 0xd1, 0xcc, 0x46, 0x03, 0x77, 0x60, 0x9d, 0x26, 0x67, 0x60, 0x56, 0x16, 0x6e, + 0xe5, 0x22, 0x3a, 0x8c, 0x34, 0x12, 0x6d, 0x79, 0xcd, 0xdd, 0x95, 0xa5, 0x2c, 0x86, 0x22, 0x77, 0x1a, 0x5c, 0x9e, + 0xc7, 0xeb, 0xd5, 0x00, 0x09, 0x90, 0x2b, 0xdb, 0x90, 0x59, 0x8a, 0x3a, 0x41, 0x19, 0x34, 0x4a, 0x54, 0xa0, 0xc9, + 0xdd, 0xdd, 0xaf, 0x7f, 0x2f, 0x9d, 0x33, 0x8f, 0x72, 0x9d, 0x59, 0x8f, 0x62, 0x0e, 0x98, 0xa4, 0x16, 0x37, 0xe3, + 0xa5, 0xaa, 0xa3, 0xc6, 0x6c, 0x95, 0xae, 0xbe, 0xd2, 0xf1, 0x7e, 0x42, 0x8e, 0x7a, 0x86, 0xff, 0xd0, 0x2a, 0xe5, + 0x1d, 0xdd, 0x40, 0x11, 0x4d, 0x87, 0x22, 0x72, 0x0e, 0x8f, 0xf4, 0x66, 0xe2, 0x6b, 0x92, 0xf2, 0x8f, 0xfb, 0x37, + 0xe8, 0xcf, 0xae, 0xa2, 0x16, 0xc6, 0xb4, 0x0d, 0x51, 0xb5, 0x60, 0x1b, 0x55, 0x91, 0xf7, 0x7f, 0xdc, 0xdd, 0x9f, + 0x23, 0xec, 0x07, 0x26, 0x04, 0x92, 0x8d, 0xd7, 0xbb, 0x5e, 0x58, 0xde, 0x51, 0x6d, 0x07, 0xb5, 0x81, 0x9b, 0x22, + 0xa7, 0x18, 0x06, 0x7a, 0xcd, 0x05, 0x8c, 0x61, 0x8c, 0x82, 0x25, 0x3a, 0x79, 0x75, 0xb2, 0xf6, 0xc4, 0xaf, 0x68, + 0xfc, 0xda, 0xd1, 0xf8, 0xe9, 0xa3, 0xe1, 0xa6, 0xfb, 0x1f, 0x53, 0x58, 0x46, 0xb2, 0xf9, 0x0a, 0x00, 0x00}; } // namespace captive_portal } // namespace esphome diff --git a/esphome/components/web_server/server_index_v2.h b/esphome/components/web_server/server_index_v2.h index ec093d3186..e675d81552 100644 --- a/esphome/components/web_server/server_index_v2.h +++ b/esphome/components/web_server/server_index_v2.h @@ -494,155 +494,155 @@ const uint8_t INDEX_GZ[] PROGMEM = { 0x1c, 0x40, 0xc8, 0x12, 0x7c, 0xa6, 0xc1, 0x29, 0x21, 0xa4, 0xd5, 0x9f, 0x05, 0x5f, 0xe2, 0x9b, 0x98, 0xa6, 0xc1, 0xbc, 0xe8, 0x96, 0x04, 0xa0, 0x22, 0xa6, 0x6f, 0x45, 0x79, 0x6f, 0x9c, 0xa4, 0x8a, 0xea, 0xb5, 0x82, 0xb3, 0x59, 0x52, 0xcf, 0x96, 0x58, 0x9a, 0xe5, 0x93, 0x19, 0x25, 0xfc, 0xa6, 0x79, 0xeb, 0xf6, 0x36, 0xc7, 0xd7, 0x60, 0x76, - 0x65, 0x7c, 0x4d, 0x02, 0x5b, 0x3e, 0xbd, 0x0f, 0xc7, 0xe5, 0xef, 0x57, 0x34, 0xcf, 0xc3, 0xb1, 0xae, 0xb9, 0x3d, - 0x9e, 0x26, 0x41, 0xb4, 0x63, 0x69, 0x06, 0x08, 0x88, 0x89, 0x01, 0x46, 0xc0, 0xa7, 0xa1, 0x43, 0x64, 0x30, 0xf5, - 0x7a, 0x74, 0x4d, 0x0e, 0x5f, 0x2f, 0x12, 0xe1, 0xb8, 0x2a, 0x38, 0x99, 0x66, 0x54, 0x96, 0x2a, 0x34, 0x16, 0x27, - 0xfb, 0x50, 0xa0, 0x5e, 0x6f, 0x89, 0xa2, 0x19, 0x07, 0xca, 0xf6, 0x58, 0x9a, 0x63, 0xa2, 0x68, 0x76, 0xa2, 0x52, - 0x99, 0xa5, 0xb4, 0x1e, 0xbb, 0xf9, 0xbc, 0x3d, 0x84, 0x3f, 0x3a, 0x32, 0xf4, 0xf9, 0x68, 0x34, 0xba, 0x37, 0xaa, - 0xf6, 0x79, 0x34, 0xa2, 0x1d, 0x7a, 0xd4, 0x85, 0x24, 0x96, 0xa6, 0x8e, 0xc5, 0xb4, 0x0b, 0x89, 0xbb, 0xc5, 0xc3, - 0x2a, 0x43, 0xd8, 0x46, 0xc4, 0x8b, 0x87, 0x47, 0xd8, 0x8a, 0x69, 0x46, 0x17, 0x93, 0x30, 0x1b, 0xb3, 0x34, 0x68, - 0x15, 0xfe, 0x5c, 0x87, 0xa4, 0x3e, 0x3f, 0x3e, 0x3e, 0x2e, 0xfc, 0xc8, 0x3c, 0xb5, 0xa2, 0xa8, 0xf0, 0x87, 0x8b, - 0x72, 0x1a, 0xad, 0xd6, 0x68, 0x54, 0xf8, 0xcc, 0x14, 0x1c, 0x74, 0x86, 0xd1, 0x41, 0xa7, 0xf0, 0x6f, 0xac, 0x1a, - 0x85, 0x4f, 0xf5, 0x53, 0x46, 0xa3, 0x5a, 0x26, 0xcc, 0xe3, 0x56, 0xab, 0xf0, 0x15, 0xa1, 0x2d, 0xc0, 0x2c, 0x55, - 0x3f, 0x83, 0x70, 0x26, 0x38, 0x30, 0xf7, 0x6e, 0x22, 0xbc, 0xc1, 0xa5, 0xbe, 0x65, 0x44, 0x7d, 0x93, 0xa3, 0x40, - 0x17, 0xf8, 0x67, 0x3b, 0x78, 0x04, 0xc4, 0x2c, 0x83, 0x46, 0x89, 0x89, 0x2d, 0xd5, 0x5e, 0x03, 0x65, 0xc9, 0xd7, - 0x3f, 0x93, 0xa4, 0x8a, 0x29, 0x01, 0x27, 0x83, 0x9a, 0xea, 0x32, 0x3c, 0x4a, 0xb7, 0xc8, 0x0f, 0xf6, 0x69, 0xf9, - 0x71, 0xf7, 0x10, 0xf1, 0xc1, 0xfe, 0x70, 0xf1, 0x41, 0xa9, 0x25, 0x3e, 0x14, 0xf3, 0xb8, 0x13, 0xc4, 0x1d, 0xc6, - 0x74, 0xf8, 0xf1, 0x9a, 0xdf, 0x36, 0x61, 0x4b, 0x64, 0xae, 0x14, 0x2c, 0xbb, 0xbf, 0x35, 0x6b, 0xc6, 0x74, 0x66, - 0x7d, 0xd1, 0x43, 0xaa, 0x0f, 0x6f, 0x52, 0xe2, 0xbe, 0x31, 0xb6, 0xad, 0x2a, 0x19, 0x8d, 0x88, 0xfb, 0x66, 0x34, - 0x72, 0xcd, 0x59, 0xc9, 0x50, 0x50, 0x59, 0xeb, 0x75, 0xad, 0x44, 0xd6, 0xfa, 0xf2, 0x4b, 0xbb, 0xcc, 0x2e, 0xd0, - 0xa1, 0x27, 0x3b, 0xcc, 0xa4, 0xdf, 0x44, 0x2c, 0x87, 0xad, 0x06, 0x1f, 0x1a, 0xa9, 0xdf, 0xd5, 0x98, 0xd6, 0xae, - 0xd5, 0x2e, 0x01, 0xde, 0x70, 0x17, 0xf8, 0xea, 0x45, 0x01, 0x63, 0x6a, 0xf2, 0x16, 0x9f, 0xde, 0x7d, 0x15, 0x79, - 0x77, 0x02, 0x15, 0x2c, 0x7f, 0x93, 0xae, 0x1c, 0x02, 0x52, 0x30, 0x12, 0x62, 0x4f, 0xab, 0x10, 0x7c, 0x3c, 0x4e, - 0xe0, 0x5b, 0x2f, 0x8b, 0xda, 0xfd, 0xb1, 0xaa, 0x79, 0xbf, 0x36, 0xdf, 0xc0, 0x6e, 0xa8, 0x6f, 0x5b, 0x95, 0x9f, - 0x9e, 0x52, 0xc9, 0xe3, 0x73, 0xfd, 0x0d, 0x22, 0x69, 0x16, 0x2f, 0x34, 0x93, 0x5f, 0xa8, 0x94, 0x63, 0x01, 0xe9, - 0x36, 0xaa, 0xe3, 0xa8, 0x28, 0xf4, 0x61, 0x8d, 0x88, 0xe5, 0x53, 0xb8, 0xd7, 0x54, 0xb5, 0xa4, 0x9f, 0x62, 0xe1, - 0xf9, 0x8d, 0x15, 0xdf, 0xa9, 0x2d, 0x57, 0x61, 0x02, 0x3c, 0xca, 0x61, 0x7e, 0x27, 0x0a, 0x57, 0xfb, 0xdd, 0x0d, - 0x12, 0x5d, 0x47, 0xe1, 0x53, 0x45, 0x9e, 0xac, 0x19, 0x82, 0xf3, 0xbb, 0x5c, 0x10, 0xf3, 0xca, 0x14, 0x14, 0x76, - 0xfc, 0x52, 0xbe, 0x51, 0xd8, 0x92, 0xd1, 0x92, 0x7c, 0x1a, 0xa6, 0x8a, 0x8d, 0x12, 0x57, 0xf1, 0x83, 0xdd, 0x45, - 0xb5, 0xf2, 0x85, 0x6b, 0xc0, 0x56, 0xc4, 0xdb, 0x3b, 0xd9, 0x87, 0x06, 0x3d, 0xa7, 0x06, 0x7a, 0xba, 0x16, 0x64, - 0xf9, 0x44, 0xba, 0xc3, 0x95, 0x9f, 0xdf, 0x60, 0x3f, 0xbf, 0x71, 0xfe, 0xbc, 0x68, 0xde, 0xd0, 0xeb, 0x8f, 0x4c, - 0x34, 0x45, 0x38, 0x6d, 0x82, 0xe1, 0x23, 0x9d, 0xa3, 0x9a, 0x3d, 0xcb, 0x2c, 0x3f, 0x75, 0xd5, 0x41, 0x77, 0x96, - 0x43, 0x56, 0x84, 0x54, 0xdf, 0x83, 0x94, 0xa7, 0xb4, 0x5b, 0xcf, 0xe6, 0xb4, 0x83, 0xec, 0x06, 0x5b, 0x17, 0x0b, - 0x0e, 0x59, 0x14, 0xe2, 0x2e, 0x68, 0x69, 0xb6, 0xde, 0x32, 0x11, 0xf4, 0xd6, 0xc6, 0xfa, 0x81, 0x46, 0x6e, 0x43, - 0x4a, 0xaf, 0x6c, 0x3d, 0x93, 0x60, 0x5b, 0x26, 0xc0, 0xa7, 0x72, 0x1b, 0xc1, 0xa5, 0x6a, 0xfe, 0x5a, 0x49, 0xa1, - 0xab, 0xc5, 0x32, 0xb7, 0xf1, 0x21, 0x90, 0x05, 0xe1, 0x48, 0xd0, 0x0c, 0x3f, 0xa4, 0xe6, 0xb5, 0x3c, 0x86, 0xb4, - 0x00, 0x31, 0x13, 0xb4, 0x8f, 0xa7, 0xb7, 0x0f, 0xef, 0xfe, 0xfe, 0xe9, 0x17, 0x1a, 0x47, 0xe6, 0x5a, 0x1e, 0xd7, - 0xed, 0xc2, 0x46, 0x48, 0xc2, 0xbb, 0x80, 0xa5, 0x52, 0xe6, 0x5d, 0x83, 0x5f, 0xb4, 0x3b, 0xe5, 0x3a, 0x49, 0x37, - 0xa3, 0x89, 0xfc, 0x0a, 0x9f, 0x5e, 0x8a, 0x83, 0x47, 0xd3, 0x5b, 0xb3, 0x1a, 0xed, 0x95, 0xe4, 0xdb, 0x3f, 0x34, - 0xc7, 0x76, 0x7b, 0x52, 0x6f, 0x3d, 0x4f, 0xf4, 0x68, 0x7a, 0xdb, 0x55, 0x82, 0xb6, 0x99, 0x29, 0xa8, 0x5a, 0xd3, - 0x5b, 0x3b, 0xcb, 0xb8, 0xea, 0xc8, 0xf1, 0x0f, 0x72, 0x87, 0x86, 0x39, 0xed, 0xc2, 0xbd, 0xe3, 0x6c, 0x18, 0x26, - 0x5a, 0x98, 0x4f, 0x58, 0x14, 0x25, 0xb4, 0x6b, 0xe4, 0xb5, 0xd3, 0x7e, 0x04, 0x49, 0xba, 0xf6, 0x92, 0xd5, 0x57, - 0xc5, 0x42, 0x5e, 0x89, 0xa7, 0xf0, 0x3a, 0xe7, 0x09, 0x7c, 0xf4, 0x63, 0x23, 0x3a, 0x75, 0xf6, 0x6a, 0xab, 0x42, - 0x9e, 0xfc, 0x5d, 0x9f, 0xcb, 0x51, 0xeb, 0x4f, 0x5d, 0xb9, 0xe0, 0xad, 0xae, 0xe0, 0xd3, 0xa0, 0x79, 0x50, 0x9f, - 0x08, 0xbc, 0x2a, 0xa7, 0x80, 0x37, 0x4c, 0x0b, 0x83, 0xb4, 0x52, 0x7c, 0xda, 0xf1, 0xdb, 0xba, 0x4c, 0x76, 0x00, - 0x79, 0x61, 0x65, 0x51, 0x51, 0x9f, 0xcc, 0xbf, 0xcd, 0x6e, 0x79, 0xb2, 0x79, 0xb7, 0x3c, 0x31, 0xbb, 0xe5, 0x7e, - 0x8a, 0xfd, 0x7c, 0xd4, 0x86, 0x3f, 0xdd, 0x6a, 0x42, 0x41, 0xcb, 0x39, 0x98, 0xde, 0x3a, 0xa0, 0xa7, 0x35, 0x3b, - 0xd3, 0x5b, 0x95, 0x63, 0x0d, 0xb1, 0x9b, 0x16, 0x64, 0x1d, 0xe3, 0x96, 0x03, 0x85, 0xf0, 0xb7, 0x55, 0x7b, 0xd5, - 0x3e, 0x84, 0x77, 0xd0, 0xea, 0x68, 0xfd, 0x5d, 0xe7, 0xfe, 0x4d, 0x1b, 0xa4, 0x5c, 0x78, 0x81, 0xe1, 0xc6, 0xc8, - 0x17, 0xe1, 0xf5, 0x35, 0x8d, 0x82, 0x11, 0x1f, 0xce, 0xf2, 0x7f, 0xd2, 0xf0, 0x6b, 0x24, 0xde, 0xbb, 0xa5, 0x57, - 0xfa, 0x31, 0x4d, 0x55, 0xc6, 0xb7, 0xe9, 0x61, 0x51, 0xae, 0x53, 0x90, 0x0f, 0xc3, 0x84, 0x7a, 0x1d, 0xff, 0x70, - 0xc3, 0x26, 0xf8, 0x77, 0x59, 0x9b, 0x8d, 0x93, 0xf9, 0xbd, 0xc8, 0xb8, 0x17, 0x09, 0xbf, 0x0a, 0x07, 0xf6, 0x1a, - 0xb6, 0x8e, 0x37, 0x83, 0x3b, 0x30, 0x23, 0x5d, 0x18, 0xa1, 0xa0, 0xe5, 0x4e, 0x44, 0x47, 0xe1, 0x2c, 0x11, 0xf7, - 0xf7, 0xba, 0x8d, 0x32, 0xd6, 0x7a, 0xbd, 0x87, 0xa1, 0x57, 0x75, 0x1f, 0xc8, 0xa5, 0x3f, 0x7f, 0x72, 0x08, 0x7f, - 0x54, 0xfe, 0xd7, 0x5d, 0xa5, 0xab, 0x2b, 0xbb, 0x17, 0x74, 0xf5, 0xdd, 0x9a, 0x32, 0xae, 0x44, 0xb8, 0xd4, 0xc7, - 0x1f, 0x5a, 0x1b, 0xb4, 0xca, 0x07, 0x55, 0xd7, 0x5a, 0xd6, 0xaf, 0xaa, 0xfd, 0xeb, 0x3a, 0x7f, 0x60, 0xdd, 0xa1, - 0xd2, 0x5c, 0xeb, 0x75, 0xf5, 0x67, 0x08, 0xd7, 0x2a, 0x1b, 0x8c, 0xcb, 0xfa, 0xbb, 0xe4, 0xae, 0x34, 0x51, 0x54, - 0x34, 0x16, 0xac, 0x94, 0x5d, 0x65, 0xa5, 0xe4, 0x94, 0x5c, 0x9d, 0xf4, 0x6f, 0x27, 0x89, 0x33, 0x57, 0xc7, 0x25, - 0x89, 0xdb, 0xf6, 0x5b, 0xae, 0x23, 0xf3, 0x00, 0xe0, 0xd6, 0x76, 0x57, 0x7e, 0xde, 0xd6, 0xed, 0x83, 0xa6, 0x35, - 0x1f, 0x4b, 0xcd, 0xee, 0x65, 0x78, 0x47, 0xb3, 0xcb, 0x8e, 0xeb, 0x80, 0x9f, 0xa6, 0xa9, 0x52, 0x26, 0x64, 0x99, - 0xd3, 0x71, 0x9d, 0xdb, 0x49, 0x92, 0xe6, 0xc4, 0x8d, 0x85, 0x98, 0x06, 0xea, 0xfb, 0xb7, 0x37, 0x07, 0x3e, 0xcf, - 0xc6, 0xfb, 0x9d, 0x56, 0xab, 0x05, 0x17, 0xc0, 0xba, 0xce, 0x9c, 0xd1, 0x9b, 0xa7, 0xfc, 0x96, 0xb8, 0x2d, 0xa7, - 0xe5, 0xb4, 0x3b, 0xc7, 0x4e, 0xbb, 0x73, 0xe8, 0x3f, 0x3a, 0x76, 0x7b, 0x9f, 0x39, 0xce, 0x49, 0x44, 0x47, 0x39, - 0xfc, 0x70, 0x9c, 0x13, 0xa9, 0x78, 0xa9, 0xdf, 0x8e, 0xe3, 0x0f, 0x93, 0xbc, 0xd9, 0x76, 0x16, 0xfa, 0xd1, 0x71, - 0xe0, 0x50, 0x69, 0xe0, 0x7c, 0x3e, 0xea, 0x8c, 0x0e, 0x47, 0x4f, 0xba, 0xba, 0xb8, 0xf8, 0xac, 0x56, 0x1d, 0xab, - 0xff, 0x3b, 0x56, 0xb3, 0x5c, 0x64, 0xfc, 0x23, 0xd5, 0x39, 0x89, 0x0e, 0x88, 0x9e, 0x8d, 0x4d, 0x3b, 0xeb, 0x23, - 0xb5, 0x8f, 0xaf, 0x87, 0xa3, 0x4e, 0x55, 0x5d, 0xc2, 0xb8, 0x5f, 0x02, 0x79, 0xb2, 0x6f, 0x40, 0x3f, 0xb1, 0xd1, - 0xd4, 0x6e, 0x6e, 0x42, 0x54, 0xdb, 0xd5, 0x73, 0x1c, 0x9b, 0xf9, 0x9d, 0xc0, 0x19, 0x06, 0xa3, 0xab, 0x4a, 0x08, - 0x5c, 0x27, 0x22, 0xee, 0xab, 0x76, 0xe7, 0x18, 0xb7, 0xdb, 0x8f, 0xfc, 0x47, 0xc7, 0xc3, 0x16, 0x3e, 0xf4, 0x0f, - 0x9b, 0x07, 0xfe, 0x23, 0x7c, 0xdc, 0x3c, 0xc6, 0xc7, 0x2f, 0x8e, 0x87, 0xcd, 0x43, 0xff, 0x10, 0xb7, 0x9a, 0xc7, - 0x50, 0xd8, 0x3c, 0x6e, 0x1e, 0xcf, 0x9b, 0x87, 0xc7, 0xc3, 0x96, 0x2c, 0xed, 0xf8, 0x47, 0x47, 0xcd, 0x76, 0xcb, - 0x3f, 0x3a, 0xc2, 0x47, 0xfe, 0xa3, 0x47, 0xcd, 0xf6, 0x81, 0xff, 0xe8, 0xd1, 0xcb, 0xa3, 0x63, 0xff, 0x00, 0xde, - 0x1d, 0x1c, 0x0c, 0x0f, 0xfc, 0x76, 0xbb, 0x09, 0xff, 0xe0, 0x63, 0xbf, 0xa3, 0x7e, 0xb4, 0xdb, 0xfe, 0x41, 0x1b, - 0xb7, 0x92, 0xa3, 0x8e, 0xff, 0xe8, 0x09, 0x96, 0xff, 0xca, 0x6a, 0x58, 0xfe, 0x03, 0xdd, 0xe0, 0x27, 0x7e, 0xe7, - 0x91, 0xfa, 0x25, 0x3b, 0x9c, 0x1f, 0x1e, 0xff, 0xe0, 0xee, 0x6f, 0x9d, 0x43, 0x5b, 0xcd, 0xe1, 0xf8, 0xc8, 0x3f, - 0x38, 0xc0, 0x87, 0x6d, 0xff, 0xf8, 0x20, 0x6e, 0x1e, 0x76, 0xfc, 0x47, 0x8f, 0x87, 0xcd, 0xb6, 0xff, 0xf8, 0x31, - 0x6e, 0x35, 0x0f, 0xfc, 0x0e, 0x6e, 0xfb, 0x87, 0x07, 0xf2, 0xc7, 0x81, 0xdf, 0x99, 0x3f, 0x7e, 0xe2, 0x3f, 0x3a, - 0x8a, 0x1f, 0xf9, 0x87, 0xdf, 0x1e, 0x1e, 0xfb, 0x9d, 0x83, 0xf8, 0xe0, 0x91, 0xdf, 0x79, 0x3c, 0x7f, 0xe4, 0x1f, - 0xc6, 0xcd, 0xce, 0xa3, 0x7b, 0x5b, 0xb6, 0x3b, 0x3e, 0xe0, 0x48, 0xbe, 0x86, 0x17, 0x58, 0xbf, 0x80, 0xbf, 0xb1, - 0x6c, 0xfb, 0xef, 0xd8, 0x4d, 0xbe, 0xde, 0xf4, 0x89, 0x7f, 0xfc, 0x78, 0xa8, 0xaa, 0x43, 0x41, 0xd3, 0xd4, 0x80, - 0x26, 0xf3, 0xa6, 0x1a, 0x56, 0x76, 0xd7, 0x34, 0x1d, 0x99, 0xbf, 0x7a, 0xb0, 0x79, 0x13, 0x06, 0x56, 0xe3, 0xfe, - 0x87, 0xf6, 0x53, 0x2e, 0xf9, 0xc9, 0xfe, 0x58, 0x91, 0xfe, 0xb8, 0xf7, 0x99, 0xba, 0xdd, 0xf9, 0xb3, 0x2b, 0x9c, - 0x6e, 0x73, 0x7c, 0x64, 0x9f, 0x76, 0x7c, 0x70, 0xfa, 0x10, 0xcf, 0x47, 0xf6, 0x87, 0x7b, 0x3e, 0x52, 0xba, 0xe2, - 0x38, 0xbf, 0x16, 0x6b, 0x0e, 0x8e, 0x55, 0xab, 0xf8, 0xa9, 0xf0, 0x06, 0x39, 0x7c, 0x47, 0xac, 0xe8, 0x5e, 0x0b, - 0xc2, 0xa9, 0xed, 0x07, 0xe2, 0xc0, 0x62, 0xaf, 0x85, 0xe2, 0xb1, 0xc9, 0x36, 0x84, 0x84, 0x9f, 0x46, 0xc8, 0xb7, - 0x0f, 0xc1, 0x47, 0xf8, 0x87, 0xe3, 0x23, 0xb1, 0xf1, 0x51, 0xf3, 0xe5, 0x4b, 0x4f, 0x83, 0xf4, 0x14, 0x9c, 0xcb, - 0x67, 0x0f, 0x0e, 0x51, 0x35, 0xdc, 0x7d, 0x0a, 0x45, 0xb9, 0xab, 0x22, 0x5f, 0xef, 0x7e, 0x4d, 0xd8, 0x41, 0x9d, - 0x98, 0x24, 0xae, 0x76, 0xcb, 0x4c, 0xa5, 0xd4, 0xd1, 0x0f, 0xa5, 0x50, 0xea, 0xf8, 0x2d, 0xbf, 0x55, 0xba, 0x74, - 0xe0, 0x94, 0x2c, 0x59, 0x70, 0x11, 0xc2, 0x17, 0x6b, 0x13, 0x3e, 0x96, 0xdf, 0xb6, 0x85, 0xaf, 0x09, 0x40, 0xd2, - 0xcf, 0x50, 0x7d, 0xc8, 0x21, 0x70, 0x5d, 0x7d, 0xb7, 0x06, 0x9c, 0xc2, 0xfc, 0x06, 0x4e, 0xaa, 0x9a, 0xa8, 0xc4, - 0x04, 0xbc, 0x1d, 0xaf, 0x68, 0xc4, 0x42, 0xcf, 0xf5, 0xa6, 0x19, 0x1d, 0xd1, 0x2c, 0x6f, 0xd6, 0x8e, 0x6f, 0xca, - 0x93, 0x9b, 0xc8, 0x35, 0x9f, 0x46, 0xcd, 0xe0, 0x76, 0x6c, 0x32, 0xd0, 0xfe, 0x46, 0x57, 0x1b, 0x60, 0x6e, 0x81, - 0x4d, 0x49, 0x06, 0xb2, 0xb6, 0x52, 0xda, 0x5c, 0xa5, 0xb5, 0xb5, 0xfd, 0xce, 0x11, 0x72, 0x64, 0x31, 0xdc, 0x3b, - 0xfc, 0xbd, 0xd7, 0x3c, 0x68, 0xfd, 0x09, 0x59, 0xcd, 0xca, 0x8e, 0x2e, 0xb4, 0xbb, 0x2d, 0xad, 0xbe, 0x29, 0x5d, - 0x3f, 0x5b, 0xeb, 0x2a, 0x8a, 0xf8, 0x5c, 0xcd, 0xdd, 0x45, 0xdd, 0x54, 0x47, 0xb8, 0xd5, 0x0d, 0x11, 0x23, 0x36, - 0xf6, 0xec, 0x2f, 0x06, 0xab, 0x7b, 0x8d, 0xe5, 0x87, 0xc6, 0x51, 0x51, 0x55, 0x49, 0xd1, 0x42, 0xc6, 0x5b, 0x58, - 0xea, 0xa4, 0xcb, 0xa5, 0x97, 0x82, 0x8b, 0x9c, 0x58, 0x38, 0x85, 0x67, 0x54, 0x43, 0x72, 0x8a, 0x4b, 0x80, 0x24, - 0x82, 0x49, 0xaa, 0xfe, 0xaf, 0x8a, 0xcd, 0x0f, 0xed, 0xf8, 0xf2, 0x93, 0x30, 0x1d, 0x03, 0x15, 0x86, 0xe9, 0x78, - 0xcd, 0xad, 0xa6, 0x42, 0x46, 0x2b, 0xa5, 0x55, 0x57, 0x95, 0xfb, 0x2c, 0x7f, 0x7a, 0xf7, 0x5e, 0x5f, 0x80, 0xe6, - 0x82, 0x77, 0x5a, 0x46, 0x38, 0xaa, 0xcb, 0x9a, 0x1b, 0xe4, 0x8b, 0x93, 0x09, 0x15, 0xa1, 0xca, 0xd7, 0x04, 0x7d, - 0x02, 0x4e, 0xcd, 0x3a, 0xda, 0x1a, 0x25, 0xae, 0x94, 0xee, 0x24, 0xa2, 0x73, 0x36, 0xd4, 0xa2, 0x1e, 0x3b, 0xfa, - 0xe6, 0x80, 0xa6, 0x5c, 0x1a, 0xd2, 0xc6, 0xca, 0x1f, 0x33, 0x0c, 0x65, 0x46, 0x3e, 0x49, 0xb9, 0xdb, 0xfb, 0xa2, - 0xfc, 0xfa, 0xe9, 0xb6, 0x45, 0x48, 0x58, 0xfa, 0x71, 0x90, 0xd1, 0xe4, 0x9f, 0xc8, 0x17, 0x6c, 0xc8, 0xd3, 0x2f, - 0x2e, 0xe0, 0xab, 0xf4, 0x7e, 0x9c, 0xd1, 0x11, 0xf9, 0x02, 0x64, 0x7c, 0x20, 0xad, 0x0f, 0x60, 0x84, 0x8d, 0xdb, - 0x49, 0x82, 0xa5, 0xc6, 0xf4, 0x00, 0x85, 0x48, 0x81, 0xeb, 0x76, 0x8e, 0x5c, 0x47, 0xd9, 0xc4, 0xf2, 0x77, 0x4f, - 0x89, 0x53, 0xa9, 0x04, 0x38, 0xed, 0x8e, 0x7f, 0x14, 0x77, 0xfc, 0x27, 0xf3, 0xc7, 0xfe, 0x71, 0xdc, 0x7e, 0x3c, - 0x6f, 0xc2, 0xff, 0x1d, 0xff, 0x49, 0xd2, 0xec, 0xf8, 0x4f, 0xe0, 0xef, 0xb7, 0x87, 0xfe, 0x51, 0xdc, 0x6c, 0xfb, - 0xc7, 0xf3, 0x03, 0xff, 0xe0, 0x65, 0xbb, 0xe3, 0x1f, 0x38, 0x6d, 0x47, 0xb5, 0x03, 0x76, 0xad, 0xb8, 0xf3, 0x17, - 0x2b, 0x1b, 0x62, 0x43, 0x38, 0x4e, 0xe5, 0x9c, 0xba, 0xd8, 0x2b, 0xbf, 0xb1, 0xa8, 0xf7, 0xa7, 0x76, 0xd6, 0x3d, - 0x0b, 0x33, 0xf8, 0xd0, 0x4d, 0x7d, 0xef, 0xd6, 0xde, 0xe1, 0x1a, 0xbf, 0xd8, 0x30, 0x04, 0xec, 0x70, 0x17, 0xdb, - 0x47, 0xef, 0xe1, 0xdc, 0xba, 0xbc, 0x17, 0xdc, 0x5c, 0x8f, 0xb8, 0x9d, 0xb4, 0x55, 0x45, 0x73, 0x05, 0xa3, 0x64, - 0x16, 0x4c, 0x7e, 0x81, 0x41, 0x0e, 0xf2, 0x55, 0x54, 0xac, 0x8e, 0x0f, 0xa9, 0xaf, 0x19, 0xb7, 0x6e, 0x1f, 0xa0, - 0xd5, 0x81, 0x8d, 0x88, 0xc1, 0x7d, 0x11, 0x45, 0x61, 0x40, 0xaf, 0xb9, 0x69, 0x2b, 0x2c, 0x49, 0x7e, 0x41, 0xf3, - 0xbe, 0x0b, 0x45, 0x6e, 0xe0, 0x4a, 0x17, 0x9f, 0x5b, 0x7e, 0xec, 0xa7, 0x24, 0xec, 0xaa, 0x00, 0xcb, 0x43, 0x57, - 0xb0, 0x6b, 0x01, 0x3f, 0x2e, 0xda, 0xdb, 0xdb, 0xba, 0x5f, 0xa4, 0x02, 0x09, 0x73, 0xad, 0xbe, 0x11, 0x62, 0xb3, - 0x22, 0xd7, 0x46, 0x74, 0xd9, 0xaf, 0x44, 0x21, 0xd2, 0x78, 0xba, 0xa6, 0xa1, 0xf0, 0xc3, 0x54, 0x25, 0xd1, 0x58, - 0x0c, 0x0b, 0xb7, 0xe9, 0x01, 0x2a, 0xb8, 0x08, 0xad, 0xef, 0x00, 0xeb, 0x7d, 0xce, 0x45, 0x68, 0xce, 0xd2, 0x5a, - 0xd7, 0x06, 0x81, 0xa3, 0x37, 0xee, 0xf4, 0xde, 0xbc, 0x3f, 0x75, 0xd4, 0xf6, 0x3c, 0xd9, 0x8f, 0x3b, 0xbd, 0x13, - 0xe9, 0x33, 0x51, 0x27, 0xf1, 0x88, 0x3a, 0x89, 0xe7, 0xe8, 0x53, 0x99, 0x10, 0x49, 0x2b, 0xf6, 0xd5, 0xb4, 0xa5, - 0xcd, 0xa0, 0xbc, 0xbd, 0x93, 0x59, 0x22, 0x18, 0xdc, 0x71, 0xbd, 0x2f, 0x8f, 0xe1, 0xc1, 0x82, 0x95, 0x79, 0xd8, - 0x5a, 0x3b, 0xbc, 0x16, 0xa9, 0xf1, 0x0d, 0x8f, 0x58, 0x42, 0x4d, 0xe6, 0xb5, 0xee, 0xaa, 0x3c, 0x29, 0xb0, 0x5e, - 0x3b, 0x9f, 0x5d, 0x4f, 0x98, 0x70, 0xcd, 0x79, 0x86, 0x0f, 0xba, 0xc1, 0x89, 0x1c, 0xaa, 0x77, 0x55, 0x68, 0xe7, - 0xb5, 0xf9, 0x9a, 0x4f, 0x7d, 0x49, 0xf5, 0xec, 0xb5, 0x84, 0x80, 0x13, 0x72, 0xf1, 0x41, 0xaf, 0x74, 0x17, 0xdb, - 0xef, 0x8a, 0x93, 0xfd, 0xf8, 0xa0, 0x77, 0x15, 0x4c, 0x75, 0x7f, 0x2f, 0xf9, 0x78, 0x73, 0x5f, 0x09, 0x1f, 0xf7, - 0xe5, 0x51, 0x10, 0x75, 0x48, 0xd9, 0x28, 0xbf, 0x3c, 0x71, 0x7b, 0x27, 0x5a, 0x19, 0x70, 0x64, 0x60, 0xdd, 0x3d, - 0x6a, 0x99, 0xd3, 0x25, 0x09, 0x1f, 0xc3, 0x86, 0x54, 0x4d, 0xac, 0x41, 0x6a, 0x1e, 0xf7, 0xb8, 0xdd, 0x3b, 0x09, - 0x1d, 0xc9, 0x5b, 0x24, 0xf3, 0xc8, 0x83, 0x7d, 0x68, 0x1c, 0xf3, 0x09, 0xf5, 0x19, 0xdf, 0xbf, 0xa1, 0xd7, 0xcd, - 0x70, 0xca, 0x2a, 0xf7, 0x36, 0x28, 0x1d, 0xe5, 0x90, 0xdc, 0x78, 0xc4, 0xf5, 0xd9, 0xab, 0x4e, 0xe5, 0x6e, 0x3b, - 0x04, 0x9b, 0xc7, 0xb8, 0xe6, 0xa4, 0x4f, 0xce, 0x02, 0x8b, 0xf7, 0x4e, 0xf6, 0xc3, 0x15, 0x8c, 0x48, 0x7e, 0x5f, - 0x68, 0x47, 0x3b, 0x18, 0x36, 0x40, 0x6f, 0xae, 0xa3, 0xc4, 0x81, 0x71, 0xc8, 0x6b, 0x41, 0x5d, 0xb8, 0xbd, 0x7f, - 0xfd, 0x1f, 0xff, 0x4b, 0xfb, 0xd8, 0x4f, 0xf6, 0xe3, 0xb6, 0xe9, 0x6b, 0x65, 0x55, 0x8a, 0x13, 0x38, 0xee, 0x59, - 0x05, 0x85, 0xe9, 0x6d, 0x73, 0x9c, 0xb1, 0xa8, 0x19, 0x87, 0xc9, 0xc8, 0xed, 0x6d, 0xc7, 0xa6, 0x7d, 0x6c, 0x4b, - 0x43, 0x5d, 0x2f, 0x02, 0x7a, 0xfd, 0x4d, 0x07, 0x8f, 0xcc, 0xf9, 0x15, 0xb9, 0xb5, 0xed, 0x63, 0x48, 0xd5, 0xee, - 0xab, 0x1d, 0x45, 0x4a, 0xf5, 0x27, 0xc2, 0x34, 0x07, 0x4c, 0x6b, 0x27, 0x90, 0x0a, 0xd7, 0x29, 0x83, 0x5a, 0xff, - 0xf7, 0x7f, 0xfe, 0x97, 0xff, 0x66, 0x1e, 0x21, 0x56, 0xf5, 0xaf, 0xff, 0xfd, 0x3f, 0xff, 0x9f, 0xff, 0xfd, 0x5f, - 0xe1, 0xd4, 0x8a, 0x8e, 0x67, 0x49, 0xa6, 0xe2, 0x54, 0xc1, 0x2c, 0xc5, 0x5d, 0x1c, 0x48, 0xec, 0x9c, 0xb0, 0x5c, - 0xb0, 0x61, 0xfd, 0x4c, 0xd2, 0xb9, 0x1c, 0x50, 0xee, 0x4c, 0x0d, 0x9d, 0xdc, 0xe1, 0x45, 0x45, 0x50, 0x35, 0x94, - 0x4b, 0xc2, 0x2d, 0x4e, 0xf6, 0x01, 0xdf, 0x0f, 0x3b, 0xc6, 0xe9, 0x97, 0xcb, 0xb1, 0x30, 0x64, 0x02, 0x25, 0x45, - 0x55, 0xee, 0x40, 0x6c, 0x65, 0x01, 0x8f, 0x41, 0xc7, 0x2a, 0x96, 0xab, 0x57, 0x6b, 0xd3, 0xfd, 0x69, 0x96, 0x0b, - 0x36, 0x02, 0x94, 0x2b, 0x3f, 0xb1, 0x0c, 0x63, 0x37, 0x41, 0x57, 0x4c, 0xee, 0x0a, 0xd9, 0x8b, 0x22, 0xd0, 0xc3, - 0xe3, 0x3f, 0x15, 0x7f, 0x99, 0x80, 0x46, 0xe6, 0x78, 0x93, 0xf0, 0x56, 0x9b, 0xe7, 0x8f, 0x5a, 0xad, 0xe9, 0x2d, - 0x5a, 0x54, 0x23, 0xe0, 0x6d, 0x83, 0x49, 0x3a, 0xb6, 0x3b, 0x94, 0xf1, 0xef, 0xd2, 0x8d, 0xdd, 0x72, 0xc0, 0x17, - 0xee, 0xb4, 0x8a, 0xe2, 0xcf, 0x0b, 0xe9, 0x49, 0x65, 0xbf, 0x40, 0x9c, 0x5a, 0x3b, 0x9d, 0xaf, 0xb9, 0x3d, 0xb9, - 0x85, 0xd5, 0xaa, 0xa3, 0x5a, 0xc5, 0xed, 0xf5, 0xd3, 0x89, 0x76, 0x9c, 0xdd, 0x8e, 0x90, 0x1f, 0x42, 0xcc, 0x3b, - 0x6e, 0xe3, 0xb8, 0xb3, 0x28, 0xbb, 0x17, 0x82, 0x4f, 0xec, 0xc0, 0x3a, 0x0d, 0xe9, 0x90, 0x8e, 0x8c, 0xb3, 0x5e, - 0xbf, 0x57, 0x41, 0xf3, 0x22, 0x3e, 0xd8, 0x30, 0x96, 0x06, 0x49, 0x06, 0xd4, 0x9d, 0x56, 0xf1, 0x39, 0xec, 0xc0, - 0xc5, 0x28, 0xe1, 0xa1, 0x08, 0x24, 0xc1, 0x76, 0xed, 0xf0, 0x7c, 0x08, 0x3c, 0x89, 0x2f, 0x2c, 0x78, 0xba, 0xaa, - 0x2a, 0xb8, 0xcd, 0xeb, 0x67, 0x48, 0x0b, 0x5f, 0x36, 0xb7, 0xbb, 0x52, 0x5e, 0xb7, 0x6f, 0x75, 0xd4, 0xfb, 0x5d, - 0xcd, 0x5d, 0xa5, 0x05, 0x52, 0x07, 0x6d, 0x7e, 0xaf, 0xe4, 0xba, 0x7a, 0xfb, 0xb5, 0xf0, 0x5c, 0x09, 0xa6, 0xbb, - 0x5a, 0x4b, 0x16, 0x42, 0xad, 0x77, 0xe4, 0xdb, 0xd2, 0x64, 0x0a, 0xa7, 0x53, 0x59, 0x11, 0x75, 0x4f, 0xf6, 0x95, - 0xa6, 0x0b, 0xdc, 0x43, 0xa6, 0x74, 0xa8, 0x0c, 0x0a, 0x5d, 0x49, 0x6f, 0x05, 0xf5, 0x4b, 0xe7, 0x56, 0xc0, 0xa7, - 0xe3, 0x7a, 0xff, 0x0f, 0x82, 0x7a, 0x0b, 0xa7, 0xcf, 0x89, 0x00, 0x00}; + 0x65, 0x7c, 0xed, 0x25, 0x00, 0x5b, 0x3e, 0xbd, 0x0f, 0xc7, 0xe5, 0xef, 0x57, 0x34, 0xcf, 0xc3, 0xb1, 0xae, 0xb9, + 0x3d, 0x9e, 0x26, 0x41, 0xb4, 0x63, 0x69, 0x06, 0x08, 0x88, 0x89, 0x01, 0x46, 0xc0, 0xa7, 0xa1, 0x43, 0x64, 0x30, + 0xf5, 0x7a, 0x74, 0x4d, 0xe2, 0xaa, 0x5e, 0x24, 0xc2, 0x71, 0x55, 0x70, 0x32, 0xcd, 0xa8, 0x2c, 0x55, 0x68, 0x2c, + 0x4e, 0xf6, 0xa1, 0x40, 0xbd, 0xde, 0x12, 0x45, 0x33, 0x0e, 0x94, 0xed, 0xb1, 0x34, 0xc7, 0x44, 0xd1, 0xec, 0x44, + 0xa5, 0x32, 0x4b, 0x69, 0x3d, 0x76, 0xf3, 0x79, 0x7b, 0x08, 0x7f, 0x74, 0x64, 0xe8, 0xf3, 0xd1, 0x68, 0x74, 0x6f, + 0x54, 0xed, 0xf3, 0x68, 0x44, 0x3b, 0xf4, 0xa8, 0x0b, 0x49, 0x2c, 0x4d, 0x1d, 0x8b, 0x69, 0x17, 0x12, 0x77, 0x8b, + 0x87, 0x55, 0x86, 0xb0, 0x8d, 0x88, 0x17, 0x0f, 0x8f, 0xb0, 0x15, 0xd3, 0x8c, 0x2e, 0x26, 0x61, 0x36, 0x66, 0x69, + 0xd0, 0x2a, 0xfc, 0xb9, 0x0e, 0x49, 0x7d, 0x7e, 0x7c, 0x7c, 0x5c, 0xf8, 0x91, 0x79, 0x6a, 0x45, 0x51, 0xe1, 0x0f, + 0x17, 0xe5, 0x34, 0x5a, 0xad, 0xd1, 0xa8, 0xf0, 0x99, 0x29, 0x38, 0xe8, 0x0c, 0xa3, 0x83, 0x4e, 0xe1, 0xdf, 0x58, + 0x35, 0x0a, 0x9f, 0xea, 0xa7, 0x8c, 0x46, 0xb5, 0x4c, 0x98, 0xc7, 0xad, 0x56, 0xe1, 0x2b, 0x42, 0x5b, 0x80, 0x59, + 0xaa, 0x7e, 0x06, 0xe1, 0x4c, 0x70, 0x60, 0xee, 0xdd, 0x44, 0x78, 0x83, 0x4b, 0x7d, 0xcb, 0x88, 0xfa, 0x26, 0x47, + 0x81, 0x2e, 0xf0, 0xcf, 0x76, 0xf0, 0x08, 0x88, 0x59, 0x06, 0x8d, 0x12, 0x13, 0x5b, 0xaa, 0xbd, 0x06, 0xca, 0x92, + 0xaf, 0x7f, 0x26, 0x49, 0x15, 0x53, 0x02, 0x4e, 0x06, 0x35, 0xd5, 0x65, 0x78, 0x94, 0x6e, 0x91, 0x1f, 0xec, 0xd3, + 0xf2, 0xe3, 0xee, 0x21, 0xe2, 0x83, 0xfd, 0xe1, 0xe2, 0x83, 0x52, 0x4b, 0x7c, 0x28, 0xe6, 0x71, 0x27, 0x88, 0x3b, + 0x8c, 0xe9, 0xf0, 0xe3, 0x35, 0xbf, 0x6d, 0xc2, 0x96, 0xc8, 0x5c, 0x29, 0x58, 0x76, 0x7f, 0x6b, 0xd6, 0x8c, 0xe9, + 0xcc, 0xfa, 0xa2, 0x87, 0x54, 0x1f, 0xde, 0xa4, 0xc4, 0x7d, 0x63, 0x6c, 0x5b, 0x55, 0x32, 0x1a, 0x11, 0xf7, 0xcd, + 0x68, 0xe4, 0x9a, 0xb3, 0x92, 0xa1, 0xa0, 0xb2, 0xd6, 0xeb, 0x5a, 0x89, 0xac, 0xf5, 0xe5, 0x97, 0x76, 0x99, 0x5d, + 0xa0, 0x43, 0x4f, 0x76, 0x98, 0x49, 0xbf, 0x89, 0x58, 0x0e, 0x5b, 0x0d, 0x3e, 0x34, 0x52, 0xbf, 0xab, 0x31, 0xad, + 0x5d, 0xab, 0x5d, 0x02, 0xbc, 0xe1, 0x2e, 0xf0, 0xd5, 0x8b, 0x02, 0xc6, 0xd4, 0xe4, 0x2d, 0x3e, 0xbd, 0xfb, 0x2a, + 0xf2, 0xee, 0x04, 0x2a, 0x58, 0xfe, 0x26, 0x5d, 0x39, 0x04, 0xa4, 0x60, 0x24, 0xc4, 0x9e, 0x56, 0x21, 0xf8, 0x78, + 0x9c, 0xc0, 0xb7, 0x5e, 0x16, 0xb5, 0xfb, 0x63, 0x55, 0xf3, 0x7e, 0x6d, 0xbe, 0x81, 0xdd, 0x50, 0xdf, 0xb6, 0x2a, + 0x3f, 0x3d, 0xa5, 0x92, 0xc7, 0xe7, 0xfa, 0x1b, 0x44, 0xd2, 0x2c, 0x5e, 0x68, 0x26, 0xbf, 0x50, 0x29, 0xc7, 0x02, + 0xd2, 0x6d, 0x54, 0xc7, 0x51, 0x51, 0xe8, 0xc3, 0x1a, 0x11, 0xcb, 0xa7, 0x70, 0xaf, 0xa9, 0x6a, 0x49, 0x3f, 0xc5, + 0xc2, 0xf3, 0x1b, 0x2b, 0xbe, 0x53, 0x5b, 0xae, 0xc2, 0x04, 0x78, 0x94, 0xc3, 0xfc, 0x4e, 0x14, 0xae, 0xf6, 0xbb, + 0x1b, 0x24, 0xba, 0x8e, 0xc2, 0xa7, 0x8a, 0x3c, 0x59, 0x33, 0x04, 0xe7, 0x77, 0xb9, 0x20, 0xe6, 0x95, 0x29, 0x28, + 0xec, 0xf8, 0xa5, 0x7c, 0xa3, 0xb0, 0x25, 0xa3, 0x25, 0xf9, 0x34, 0x4c, 0x15, 0x1b, 0x25, 0xae, 0xe2, 0x07, 0xbb, + 0x8b, 0x6a, 0xe5, 0x0b, 0xd7, 0x80, 0xad, 0x88, 0xb7, 0x77, 0xb2, 0x0f, 0x0d, 0x7a, 0x4e, 0x0d, 0xf4, 0x74, 0x2d, + 0xc8, 0xf2, 0x89, 0x74, 0x87, 0x2b, 0x3f, 0xbf, 0xc1, 0x7e, 0x7e, 0xe3, 0xfc, 0x79, 0xd1, 0xbc, 0xa1, 0xd7, 0x1f, + 0x99, 0x68, 0x8a, 0x70, 0xda, 0x04, 0xc3, 0x47, 0x3a, 0x47, 0x35, 0x7b, 0x96, 0x59, 0x7e, 0xea, 0xaa, 0x83, 0xee, + 0x2c, 0x87, 0xac, 0x08, 0xa9, 0xbe, 0x07, 0x29, 0x4f, 0x69, 0xb7, 0x9e, 0xcd, 0x69, 0x07, 0xd9, 0x0d, 0xb6, 0x2e, + 0x16, 0x1c, 0xb2, 0x28, 0xc4, 0x5d, 0xd0, 0xd2, 0x6c, 0xbd, 0x65, 0x22, 0xe8, 0xad, 0x8d, 0xf5, 0x03, 0x8d, 0xdc, + 0x86, 0x94, 0x5e, 0xd9, 0x7a, 0x26, 0xc1, 0xb6, 0x4c, 0x80, 0x4f, 0xe5, 0x36, 0x82, 0x4b, 0xd5, 0xfc, 0xb5, 0x92, + 0x42, 0x57, 0x8b, 0x65, 0x6e, 0xe3, 0x43, 0x20, 0x0b, 0xc2, 0x91, 0xa0, 0x19, 0x7e, 0x48, 0xcd, 0x6b, 0x79, 0x0c, + 0x69, 0x01, 0x62, 0x26, 0x68, 0x1f, 0x4f, 0x6f, 0x1f, 0xde, 0xfd, 0xfd, 0xd3, 0x2f, 0x34, 0x8e, 0xcc, 0xb5, 0x3c, + 0xae, 0xdb, 0x85, 0x8d, 0x90, 0x84, 0x77, 0x01, 0x4b, 0xa5, 0xcc, 0xbb, 0x06, 0xbf, 0x68, 0x77, 0xca, 0x75, 0x92, + 0x6e, 0x46, 0x13, 0xf9, 0x15, 0x3e, 0xbd, 0x14, 0x07, 0x8f, 0xa6, 0xb7, 0x66, 0x35, 0xda, 0x2b, 0xc9, 0xb7, 0x7f, + 0x68, 0x8e, 0xed, 0xf6, 0xa4, 0xde, 0x7a, 0x9e, 0xe8, 0xd1, 0xf4, 0xb6, 0xab, 0x04, 0x6d, 0x33, 0x53, 0x50, 0xb5, + 0xa6, 0xb7, 0x76, 0x96, 0x71, 0xd5, 0x91, 0xe3, 0x1f, 0xe4, 0x0e, 0x0d, 0x73, 0xda, 0x85, 0x7b, 0xc7, 0xd9, 0x30, + 0x4c, 0xb4, 0x30, 0x9f, 0xb0, 0x28, 0x4a, 0x68, 0xd7, 0xc8, 0x6b, 0xa7, 0xfd, 0x08, 0x92, 0x74, 0xed, 0x25, 0xab, + 0xaf, 0x8a, 0x85, 0xbc, 0x12, 0x4f, 0xe1, 0x75, 0xce, 0x13, 0xf8, 0xe8, 0xc7, 0x46, 0x74, 0xea, 0xec, 0xd5, 0x56, + 0x85, 0x3c, 0xf9, 0xbb, 0x3e, 0x97, 0xa3, 0xd6, 0x9f, 0xba, 0x72, 0xc1, 0x5b, 0x5d, 0xc1, 0xa7, 0x41, 0xf3, 0xa0, + 0x3e, 0x11, 0x78, 0x55, 0x4e, 0x01, 0x6f, 0x98, 0x16, 0x06, 0x69, 0xa5, 0xf8, 0xb4, 0xe3, 0xb7, 0x75, 0x99, 0xec, + 0x00, 0xf2, 0xc2, 0xca, 0xa2, 0xa2, 0x3e, 0x99, 0x7f, 0x9b, 0xdd, 0xf2, 0x64, 0xf3, 0x6e, 0x79, 0x62, 0x76, 0xcb, + 0xfd, 0x14, 0xfb, 0xf9, 0xa8, 0x0d, 0x7f, 0xba, 0xd5, 0x84, 0x82, 0x96, 0x73, 0x30, 0xbd, 0x75, 0x40, 0x4f, 0x6b, + 0x76, 0xa6, 0xb7, 0x2a, 0xc7, 0x1a, 0x62, 0x37, 0x2d, 0xc8, 0x3a, 0xc6, 0x2d, 0x07, 0x0a, 0xe1, 0x6f, 0xab, 0xf6, + 0xaa, 0x7d, 0x08, 0xef, 0xa0, 0xd5, 0xd1, 0xfa, 0xbb, 0xce, 0xfd, 0x9b, 0x36, 0x48, 0xb9, 0xf0, 0x02, 0xc3, 0x8d, + 0x91, 0x2f, 0xc2, 0xeb, 0x6b, 0x1a, 0x05, 0x23, 0x3e, 0x9c, 0xe5, 0xff, 0xa4, 0xe1, 0xd7, 0x48, 0xbc, 0x77, 0x4b, + 0xaf, 0xf4, 0x63, 0x9a, 0xaa, 0x8c, 0x6f, 0xd3, 0xc3, 0xa2, 0x5c, 0xa7, 0x20, 0x1f, 0x86, 0x09, 0xf5, 0x3a, 0xfe, + 0xe1, 0x86, 0x4d, 0xf0, 0xef, 0xb2, 0x36, 0x1b, 0x27, 0xf3, 0x7b, 0x91, 0x71, 0x2f, 0x12, 0x7e, 0x15, 0x0e, 0xec, + 0x35, 0x6c, 0x1d, 0x6f, 0x06, 0x77, 0x60, 0x46, 0xba, 0x30, 0x42, 0x41, 0xcb, 0x9d, 0x88, 0x8e, 0xc2, 0x59, 0x22, + 0xee, 0xef, 0x75, 0x1b, 0x65, 0xac, 0xf5, 0x7a, 0x0f, 0x43, 0xaf, 0xea, 0x3e, 0x90, 0x4b, 0x7f, 0xfe, 0xe4, 0x10, + 0xfe, 0xa8, 0xfc, 0xaf, 0xbb, 0x4a, 0x57, 0x57, 0x76, 0x2f, 0xe8, 0xea, 0xbb, 0x35, 0x65, 0x5c, 0x89, 0x70, 0xa9, + 0x8f, 0x3f, 0xb4, 0x36, 0x68, 0x95, 0x0f, 0xaa, 0xae, 0xb5, 0xac, 0x5f, 0x55, 0xfb, 0xd7, 0x75, 0xfe, 0xc0, 0xba, + 0x43, 0xa5, 0xb9, 0xd6, 0xeb, 0xea, 0xcf, 0x10, 0xae, 0x55, 0x36, 0x18, 0x97, 0xf5, 0x77, 0xc9, 0x5d, 0x69, 0xa2, + 0xa8, 0x68, 0x2c, 0x58, 0x29, 0xbb, 0xca, 0x4a, 0xc9, 0x29, 0xb9, 0x3a, 0xe9, 0xdf, 0x4e, 0x12, 0x67, 0xae, 0x8e, + 0x4b, 0x12, 0xb7, 0xed, 0xb7, 0x5c, 0x47, 0xe6, 0x01, 0xc0, 0xad, 0xed, 0xae, 0xfc, 0xbc, 0xad, 0xdb, 0x07, 0x4d, + 0x6b, 0x3e, 0x96, 0x9a, 0xdd, 0xcb, 0xf0, 0x8e, 0x66, 0x97, 0x1d, 0xd7, 0x01, 0x3f, 0x4d, 0x53, 0xa5, 0x4c, 0xc8, + 0x32, 0xa7, 0xe3, 0x3a, 0xb7, 0x93, 0x24, 0xcd, 0x89, 0x1b, 0x0b, 0x31, 0x0d, 0xd4, 0xf7, 0x6f, 0x6f, 0x0e, 0x7c, + 0x9e, 0x8d, 0xf7, 0x3b, 0xad, 0x56, 0x0b, 0x2e, 0x80, 0x75, 0x9d, 0x39, 0xa3, 0x37, 0x4f, 0xf9, 0x2d, 0x71, 0x5b, + 0x4e, 0xcb, 0x69, 0x77, 0x8e, 0x9d, 0x76, 0xe7, 0xd0, 0x7f, 0x74, 0xec, 0xf6, 0x3e, 0x73, 0x9c, 0x93, 0x88, 0x8e, + 0x72, 0xf8, 0xe1, 0x38, 0x27, 0x52, 0xf1, 0x52, 0xbf, 0x1d, 0xc7, 0x1f, 0x26, 0x79, 0xb3, 0xed, 0x2c, 0xf4, 0xa3, + 0xe3, 0xc0, 0xa1, 0xd2, 0xc0, 0xf9, 0x7c, 0xd4, 0x19, 0x1d, 0x8e, 0x9e, 0x74, 0x75, 0x71, 0xf1, 0x59, 0xad, 0x3a, + 0x56, 0xff, 0x77, 0xac, 0x66, 0xb9, 0xc8, 0xf8, 0x47, 0xaa, 0x73, 0x12, 0x1d, 0x10, 0x3d, 0x1b, 0x9b, 0x76, 0xd6, + 0x47, 0x6a, 0x1f, 0x5f, 0x0f, 0x47, 0x9d, 0xaa, 0xba, 0x84, 0x71, 0xbf, 0x04, 0xf2, 0x64, 0xdf, 0x80, 0x7e, 0x62, + 0xa3, 0xa9, 0xdd, 0xdc, 0x84, 0xa8, 0xb6, 0xab, 0xe7, 0x38, 0x36, 0xf3, 0x3b, 0x81, 0x33, 0x0c, 0x46, 0x57, 0x95, + 0x10, 0xb8, 0x4e, 0x44, 0xdc, 0x57, 0xed, 0xce, 0x31, 0x6e, 0xb7, 0x1f, 0xf9, 0x8f, 0x8e, 0x87, 0x2d, 0x7c, 0xe8, + 0x1f, 0x36, 0x0f, 0xfc, 0x47, 0xf8, 0xb8, 0x79, 0x8c, 0x8f, 0x5f, 0x1c, 0x0f, 0x9b, 0x87, 0xfe, 0x21, 0x6e, 0x35, + 0x8f, 0xa1, 0xb0, 0x79, 0xdc, 0x3c, 0x9e, 0x37, 0x0f, 0x8f, 0x87, 0x2d, 0x59, 0xda, 0xf1, 0x8f, 0x8e, 0x9a, 0xed, + 0x96, 0x7f, 0x74, 0x84, 0x8f, 0xfc, 0x47, 0x8f, 0x9a, 0xed, 0x03, 0xff, 0xd1, 0xa3, 0x97, 0x47, 0xc7, 0xfe, 0x01, + 0xbc, 0x3b, 0x38, 0x18, 0x1e, 0xf8, 0xed, 0x76, 0x13, 0xfe, 0xc1, 0xc7, 0x7e, 0x47, 0xfd, 0x68, 0xb7, 0xfd, 0x83, + 0x36, 0x6e, 0x25, 0x47, 0x1d, 0xff, 0xd1, 0x13, 0x2c, 0xff, 0x95, 0xd5, 0xb0, 0xfc, 0x07, 0xba, 0xc1, 0x4f, 0xfc, + 0xce, 0x23, 0xf5, 0x4b, 0x76, 0x38, 0x3f, 0x3c, 0xfe, 0xc1, 0xdd, 0xdf, 0x3a, 0x87, 0xb6, 0x9a, 0xc3, 0xf1, 0x91, + 0x7f, 0x70, 0x80, 0x0f, 0xdb, 0xfe, 0xf1, 0x41, 0xdc, 0x3c, 0xec, 0xf8, 0x8f, 0x1e, 0x0f, 0x9b, 0x6d, 0xff, 0xf1, + 0x63, 0xdc, 0x6a, 0x1e, 0xf8, 0x1d, 0xdc, 0xf6, 0x0f, 0x0f, 0xe4, 0x8f, 0x03, 0xbf, 0x33, 0x7f, 0xfc, 0xc4, 0x7f, + 0x74, 0x14, 0x3f, 0xf2, 0x0f, 0xbf, 0x3d, 0x3c, 0xf6, 0x3b, 0x07, 0xf1, 0xc1, 0x23, 0xbf, 0xf3, 0x78, 0xfe, 0xc8, + 0x3f, 0x8c, 0x9b, 0x9d, 0x47, 0xf7, 0xb6, 0x6c, 0x77, 0x7c, 0xc0, 0x91, 0x7c, 0x0d, 0x2f, 0xb0, 0x7e, 0x01, 0x7f, + 0x63, 0xd9, 0xf6, 0xdf, 0xb1, 0x9b, 0x7c, 0xbd, 0xe9, 0x13, 0xff, 0xf8, 0xf1, 0x50, 0x55, 0x87, 0x82, 0xa6, 0xa9, + 0x01, 0x4d, 0xe6, 0x4d, 0x35, 0xac, 0xec, 0xae, 0x69, 0x3a, 0x32, 0x7f, 0xf5, 0x60, 0xf3, 0x26, 0x0c, 0xac, 0xc6, + 0xfd, 0x0f, 0xed, 0xa7, 0x5c, 0xf2, 0x93, 0xfd, 0xb1, 0x22, 0xfd, 0x71, 0xef, 0x33, 0x75, 0xbb, 0xf3, 0x67, 0x57, + 0x38, 0xdd, 0xe6, 0xf8, 0xc8, 0x3e, 0xed, 0xf8, 0xe0, 0xf4, 0x21, 0x9e, 0x8f, 0xec, 0x0f, 0xf7, 0x7c, 0xa4, 0x74, + 0xc5, 0x71, 0x7e, 0x2d, 0xd6, 0x1c, 0x1c, 0xab, 0x56, 0xf1, 0x53, 0xe1, 0x0d, 0x72, 0xf8, 0x8e, 0x58, 0xd1, 0xbd, + 0x16, 0x84, 0x53, 0xdb, 0x0f, 0xc4, 0x81, 0xc5, 0x5e, 0x0b, 0xc5, 0x63, 0x93, 0x6d, 0x08, 0x09, 0x3f, 0x8d, 0x90, + 0x6f, 0x1f, 0x82, 0x8f, 0xf0, 0x0f, 0xc7, 0x47, 0x62, 0xe3, 0xa3, 0xe6, 0xcb, 0x97, 0x9e, 0x06, 0xe9, 0x29, 0x38, + 0x97, 0xcf, 0x1e, 0x1c, 0xa2, 0x6a, 0xb8, 0xfb, 0x14, 0x8a, 0x72, 0x57, 0x45, 0xbe, 0xde, 0xfd, 0x9a, 0xb0, 0x83, + 0x3a, 0x31, 0x49, 0x5c, 0xed, 0x96, 0x99, 0x4a, 0xa9, 0xa3, 0x1f, 0x4a, 0xa1, 0xd4, 0xf1, 0x5b, 0x7e, 0xab, 0x74, + 0xe9, 0xc0, 0x29, 0x59, 0xb2, 0xe0, 0x22, 0x84, 0x2f, 0xd6, 0x26, 0x7c, 0x2c, 0xbf, 0x6d, 0x0b, 0x5f, 0x13, 0x80, + 0xa4, 0x9f, 0xa1, 0xfa, 0x90, 0x43, 0xe0, 0xba, 0xfa, 0x6e, 0x0d, 0x38, 0x85, 0xf9, 0x0d, 0x9c, 0x54, 0x35, 0x51, + 0x89, 0x09, 0x78, 0x3b, 0x5e, 0xd1, 0x88, 0x85, 0x9e, 0xeb, 0x4d, 0x33, 0x3a, 0xa2, 0x59, 0xde, 0xac, 0x1d, 0xdf, + 0x94, 0x27, 0x37, 0x91, 0x6b, 0x3e, 0x8d, 0x9a, 0xc1, 0xed, 0xd8, 0x64, 0xa0, 0xfd, 0x8d, 0xae, 0x36, 0xc0, 0xdc, + 0x02, 0x9b, 0x92, 0x0c, 0x64, 0x6d, 0xa5, 0xb4, 0xb9, 0x4a, 0x6b, 0x6b, 0xfb, 0x9d, 0x23, 0xe4, 0xc8, 0x62, 0xb8, + 0x77, 0xf8, 0x7b, 0xaf, 0x79, 0xd0, 0xfa, 0x13, 0xb2, 0x9a, 0x95, 0x1d, 0x5d, 0x68, 0x77, 0x5b, 0x5a, 0x7d, 0x53, + 0xba, 0x7e, 0xb6, 0xd6, 0x55, 0x14, 0xf1, 0xb9, 0x9a, 0xbb, 0x8b, 0xba, 0xa9, 0x8e, 0x70, 0xab, 0x1b, 0x22, 0x46, + 0x6c, 0xec, 0xd9, 0x5f, 0x0c, 0x56, 0xf7, 0x1a, 0xcb, 0x0f, 0x8d, 0xa3, 0xa2, 0xaa, 0x92, 0xa2, 0x85, 0x8c, 0xb7, + 0xb0, 0xd4, 0x49, 0x97, 0x4b, 0x2f, 0x05, 0x17, 0x39, 0xb1, 0x70, 0x0a, 0xcf, 0xa8, 0x86, 0xe4, 0x14, 0x97, 0x00, + 0x49, 0x04, 0x93, 0x54, 0xfd, 0x5f, 0x15, 0x9b, 0x1f, 0xda, 0xf1, 0xe5, 0x27, 0x61, 0x3a, 0x06, 0x2a, 0x0c, 0xd3, + 0xf1, 0x9a, 0x5b, 0x4d, 0x85, 0x8c, 0x56, 0x4a, 0xab, 0xae, 0x2a, 0xf7, 0x59, 0xfe, 0xf4, 0xee, 0xbd, 0xbe, 0x00, + 0xcd, 0x05, 0xef, 0xb4, 0x8c, 0x70, 0x54, 0x97, 0x35, 0x37, 0xc8, 0x17, 0x27, 0x13, 0x2a, 0x42, 0x95, 0xaf, 0x09, + 0xfa, 0x04, 0x9c, 0x9a, 0x75, 0xb4, 0x35, 0x4a, 0x5c, 0x29, 0xdd, 0x49, 0x44, 0xe7, 0x6c, 0xa8, 0x45, 0x3d, 0x76, + 0xf4, 0xcd, 0x01, 0x4d, 0xb9, 0x34, 0xa4, 0x8d, 0x95, 0x3f, 0x66, 0x18, 0xca, 0x8c, 0x7c, 0x92, 0x72, 0xb7, 0xf7, + 0x45, 0xf9, 0xf5, 0xd3, 0x6d, 0x8b, 0x90, 0xb0, 0xf4, 0xe3, 0x20, 0xa3, 0xc9, 0x3f, 0x91, 0x2f, 0xd8, 0x90, 0xa7, + 0x5f, 0x5c, 0xc0, 0x57, 0xe9, 0xfd, 0x38, 0xa3, 0x23, 0xf2, 0x05, 0xc8, 0xf8, 0x40, 0x5a, 0x1f, 0xc0, 0x08, 0x1b, + 0xb7, 0x93, 0x04, 0x4b, 0x8d, 0xe9, 0x01, 0x0a, 0x91, 0x02, 0xd7, 0xed, 0x1c, 0xb9, 0x8e, 0xb2, 0x89, 0xe5, 0xef, + 0x9e, 0x12, 0xa7, 0x52, 0x09, 0x70, 0xda, 0x1d, 0xff, 0x28, 0xee, 0xf8, 0x4f, 0xe6, 0x8f, 0xfd, 0xe3, 0xb8, 0xfd, + 0x78, 0xde, 0x84, 0xff, 0x3b, 0xfe, 0x93, 0xa4, 0xd9, 0xf1, 0x9f, 0xc0, 0xdf, 0x6f, 0x0f, 0xfd, 0xa3, 0xb8, 0xd9, + 0xf6, 0x8f, 0xe7, 0x07, 0xfe, 0xc1, 0xcb, 0x76, 0xc7, 0x3f, 0x70, 0xda, 0x8e, 0x6a, 0x07, 0xec, 0x5a, 0x71, 0xe7, + 0x2f, 0x56, 0x36, 0xc4, 0x86, 0x70, 0x9c, 0xca, 0x39, 0x75, 0xb1, 0x57, 0x7e, 0x63, 0x51, 0xef, 0x4f, 0xed, 0xac, + 0x7b, 0x16, 0x66, 0xf0, 0xa1, 0x9b, 0xfa, 0xde, 0xad, 0xbd, 0xc3, 0x35, 0x7e, 0xb1, 0x61, 0x08, 0xd8, 0xe1, 0x2e, + 0xb6, 0x8f, 0xde, 0xc3, 0xb9, 0x75, 0x79, 0x2f, 0xb8, 0xb9, 0x1e, 0x71, 0x3b, 0x69, 0xab, 0x8a, 0xe6, 0x0a, 0x46, + 0xc9, 0x2c, 0x98, 0xfc, 0x02, 0x83, 0x1c, 0xe4, 0xab, 0xa8, 0x58, 0x1d, 0x1f, 0x52, 0x5f, 0x33, 0x6e, 0xdd, 0x3e, + 0x40, 0xab, 0x03, 0x1b, 0x11, 0x83, 0xfb, 0x22, 0x8a, 0xc2, 0x80, 0x5e, 0x73, 0xd3, 0x56, 0x58, 0x92, 0xfc, 0x82, + 0xe6, 0x7d, 0x17, 0x8a, 0xdc, 0xc0, 0x95, 0x2e, 0x3e, 0xb7, 0xfc, 0xd8, 0x4f, 0x49, 0xd8, 0x55, 0x01, 0x96, 0x87, + 0xae, 0x60, 0xd7, 0x02, 0x7e, 0x5c, 0xb4, 0xb7, 0xb7, 0x75, 0xbf, 0x48, 0x05, 0x12, 0xe6, 0x5a, 0x7d, 0x23, 0xc4, + 0x66, 0x45, 0xae, 0x8d, 0xe8, 0xb2, 0x5f, 0x89, 0x42, 0xa4, 0xf1, 0x74, 0x4d, 0x43, 0xe1, 0x87, 0xa9, 0x4a, 0xa2, + 0xb1, 0x18, 0x16, 0x6e, 0xd3, 0x03, 0x54, 0x70, 0x11, 0x5a, 0xdf, 0x01, 0xd6, 0xfb, 0x9c, 0x8b, 0xd0, 0x9c, 0xa5, + 0xb5, 0xae, 0x0d, 0x02, 0x47, 0x6f, 0xdc, 0xe9, 0xbd, 0x79, 0x7f, 0xea, 0xa8, 0xed, 0x79, 0xb2, 0x1f, 0x77, 0x7a, + 0x27, 0xd2, 0x67, 0xa2, 0x4e, 0xe2, 0x11, 0x75, 0x12, 0xcf, 0xd1, 0xa7, 0x32, 0x21, 0x92, 0x56, 0xec, 0xab, 0x69, + 0x4b, 0x9b, 0x41, 0x79, 0x7b, 0x27, 0xb3, 0x44, 0x30, 0xb8, 0xe3, 0x7a, 0x5f, 0x1e, 0xc3, 0x83, 0x05, 0x2b, 0xf3, + 0xb0, 0xb5, 0x76, 0x78, 0x2d, 0x52, 0xe3, 0x1b, 0x1e, 0xb1, 0x84, 0x9a, 0xcc, 0x6b, 0xdd, 0x55, 0x79, 0x52, 0x60, + 0xbd, 0x76, 0x3e, 0xbb, 0x9e, 0x30, 0xe1, 0x9a, 0xf3, 0x0c, 0x1f, 0x74, 0x83, 0x13, 0x39, 0x54, 0xef, 0xaa, 0xd0, + 0xce, 0x6b, 0xf3, 0x35, 0x9f, 0xfa, 0x92, 0xea, 0xd9, 0x6b, 0x09, 0x01, 0x27, 0xe4, 0xe2, 0x83, 0x5e, 0xe9, 0x2e, + 0xb6, 0xdf, 0x15, 0x27, 0xfb, 0xf1, 0x41, 0xef, 0x2a, 0x98, 0xea, 0xfe, 0x5e, 0xf2, 0xf1, 0xe6, 0xbe, 0x12, 0x3e, + 0xee, 0xcb, 0xa3, 0x20, 0xea, 0x90, 0xb2, 0x51, 0x7e, 0x79, 0xe2, 0xf6, 0x4e, 0xb4, 0x32, 0xe0, 0xc8, 0xc0, 0xba, + 0x7b, 0xd4, 0x32, 0xa7, 0x4b, 0x12, 0x3e, 0x86, 0x0d, 0xa9, 0x9a, 0x58, 0x83, 0xd4, 0x3c, 0xee, 0x71, 0xbb, 0x77, + 0x12, 0x3a, 0x92, 0xb7, 0x48, 0xe6, 0x91, 0x07, 0xfb, 0xd0, 0x38, 0xe6, 0x13, 0xea, 0x33, 0xbe, 0x7f, 0x43, 0xaf, + 0x9b, 0xe1, 0x94, 0x55, 0xee, 0x6d, 0x50, 0x3a, 0xca, 0x21, 0xb9, 0xf1, 0x88, 0xeb, 0xb3, 0x57, 0x9d, 0xca, 0xdd, + 0x76, 0x08, 0x36, 0x8f, 0x71, 0xcd, 0x49, 0x9f, 0x9c, 0x05, 0x16, 0xef, 0x9d, 0xec, 0x87, 0x2b, 0x18, 0x91, 0xfc, + 0xbe, 0xd0, 0x8e, 0x76, 0x30, 0x6c, 0x80, 0xde, 0x5c, 0x47, 0x89, 0x03, 0xe3, 0x90, 0xd7, 0x82, 0xba, 0x70, 0x7b, + 0xff, 0xfa, 0x3f, 0xfe, 0x97, 0xf6, 0xb1, 0x9f, 0xec, 0xc7, 0x6d, 0xd3, 0xd7, 0xca, 0xaa, 0x14, 0x27, 0x70, 0xdc, + 0xb3, 0x0a, 0x0a, 0xd3, 0xdb, 0xe6, 0x38, 0x63, 0x51, 0x33, 0x0e, 0x93, 0x91, 0xdb, 0xdb, 0x8e, 0x4d, 0xfb, 0xd8, + 0x96, 0x86, 0xba, 0x5e, 0x04, 0xf4, 0xfa, 0x9b, 0x0e, 0x1e, 0x99, 0xf3, 0x2b, 0x72, 0x6b, 0xdb, 0xc7, 0x90, 0xaa, + 0xdd, 0x57, 0x3b, 0x8a, 0x94, 0xea, 0x4f, 0x84, 0x69, 0x0e, 0x98, 0xd6, 0x4e, 0x20, 0x15, 0xae, 0x53, 0x06, 0xb5, + 0xfe, 0xef, 0xff, 0xfc, 0x2f, 0xff, 0xcd, 0x3c, 0x42, 0xac, 0xea, 0x5f, 0xff, 0xfb, 0x7f, 0xfe, 0x3f, 0xff, 0xfb, + 0xbf, 0xc2, 0xa9, 0x15, 0x1d, 0xcf, 0x92, 0x4c, 0xc5, 0xa9, 0x82, 0x59, 0x8a, 0xbb, 0x38, 0x90, 0xd8, 0x39, 0x61, + 0xb9, 0x60, 0xc3, 0xfa, 0x99, 0xa4, 0x73, 0x39, 0xa0, 0xdc, 0x99, 0x1a, 0x3a, 0xb9, 0xc3, 0x8b, 0x8a, 0xa0, 0x6a, + 0x28, 0x97, 0x84, 0x5b, 0x9c, 0xec, 0x03, 0xbe, 0x1f, 0x76, 0x8c, 0xd3, 0x2f, 0x97, 0x63, 0x61, 0xc8, 0x04, 0x4a, + 0x8a, 0xaa, 0xdc, 0x81, 0xd8, 0xca, 0x02, 0x1e, 0x83, 0x8e, 0x55, 0x2c, 0x57, 0xaf, 0xd6, 0xa6, 0xfb, 0xd3, 0x2c, + 0x17, 0x6c, 0x04, 0x28, 0x57, 0x7e, 0x62, 0x19, 0xc6, 0x6e, 0x82, 0xae, 0x98, 0xdc, 0x15, 0xb2, 0x17, 0x45, 0xa0, + 0x87, 0xc7, 0x7f, 0x2a, 0xfe, 0x32, 0x01, 0x8d, 0xcc, 0xf1, 0x26, 0xe1, 0xad, 0x36, 0xcf, 0x1f, 0xb5, 0x5a, 0xd3, + 0x5b, 0xb4, 0xa8, 0x46, 0xc0, 0xdb, 0x06, 0x93, 0x74, 0x6c, 0x77, 0x28, 0xe3, 0xdf, 0xa5, 0x1b, 0xbb, 0xe5, 0x80, + 0x2f, 0xdc, 0x69, 0x15, 0xc5, 0x9f, 0x17, 0xd2, 0x93, 0xca, 0x7e, 0x81, 0x38, 0xb5, 0x76, 0x3a, 0x5f, 0x73, 0x7b, + 0x72, 0x0b, 0xab, 0x55, 0x47, 0xb5, 0x8a, 0xdb, 0xeb, 0xa7, 0x13, 0xed, 0x38, 0xbb, 0x1d, 0x21, 0x3f, 0x84, 0x98, + 0x77, 0xdc, 0xc6, 0x71, 0x67, 0x51, 0x76, 0x2f, 0x04, 0x9f, 0xd8, 0x81, 0x75, 0x1a, 0xd2, 0x21, 0x1d, 0x19, 0x67, + 0xbd, 0x7e, 0xaf, 0x82, 0xe6, 0x45, 0x7c, 0xb0, 0x61, 0x2c, 0x0d, 0x92, 0x0c, 0xa8, 0x3b, 0xad, 0xe2, 0x73, 0xd8, + 0x81, 0x8b, 0x51, 0xc2, 0x43, 0x11, 0x48, 0x82, 0xed, 0xda, 0xe1, 0xf9, 0x10, 0x78, 0x12, 0x5f, 0x58, 0xf0, 0x74, + 0x55, 0x55, 0x70, 0x9b, 0xd7, 0xcf, 0x90, 0x16, 0xbe, 0x6c, 0x6e, 0x77, 0xa5, 0xbc, 0x6e, 0xdf, 0xea, 0xa8, 0xf7, + 0xbb, 0x9a, 0xbb, 0x4a, 0x0b, 0xa4, 0x0e, 0xda, 0xfc, 0x5e, 0xc9, 0x75, 0xf5, 0xf6, 0x6b, 0xe1, 0xb9, 0x12, 0x4c, + 0x77, 0xb5, 0x96, 0x2c, 0x84, 0x5a, 0xef, 0xc8, 0xb7, 0xa5, 0xc9, 0x14, 0x4e, 0xa7, 0xb2, 0x22, 0xea, 0x9e, 0xec, + 0x2b, 0x4d, 0x17, 0xb8, 0x87, 0x4c, 0xe9, 0x50, 0x19, 0x14, 0xba, 0x92, 0xde, 0x0a, 0xea, 0x97, 0xce, 0xad, 0x80, + 0x4f, 0xc7, 0xf5, 0xfe, 0x1f, 0xe7, 0xe0, 0x1c, 0x12, 0xcf, 0x89, 0x00, 0x00}; } // namespace web_server } // namespace esphome From 41f0d1c622c17fbdb3fd32c0ac20af2211a1e339 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Sep 2025 21:09:45 +0000 Subject: [PATCH 07/11] Bump ruff from 0.12.12 to 0.13.0 (#10670) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston --- .pre-commit-config.yaml | 2 +- requirements_test.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2b161cf05c..cda0384d54 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.12.12 + rev: v0.13.0 hooks: # Run the linter. - id: ruff diff --git a/requirements_test.txt b/requirements_test.txt index bae9246768..01661f3b7c 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==3.3.8 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating -ruff==0.12.12 # also change in .pre-commit-config.yaml when updating +ruff==0.13.0 # also change in .pre-commit-config.yaml when updating pyupgrade==3.20.0 # also change in .pre-commit-config.yaml when updating pre-commit From e3f8a36eaa63ac0d1a8369f05def7bf6012e1162 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 10 Sep 2025 22:16:04 -0500 Subject: [PATCH 08/11] Add coverage for dashboard ahead of Path conversion (#10669) --- tests/dashboard/test_entries.py | 203 ++++++++++++++++++++ tests/dashboard/test_settings.py | 168 +++++++++++++++++ tests/dashboard/test_web_server_paths.py | 230 +++++++++++++++++++++++ 3 files changed, 601 insertions(+) create mode 100644 tests/dashboard/test_entries.py create mode 100644 tests/dashboard/test_settings.py create mode 100644 tests/dashboard/test_web_server_paths.py diff --git a/tests/dashboard/test_entries.py b/tests/dashboard/test_entries.py new file mode 100644 index 0000000000..a86c33a16f --- /dev/null +++ b/tests/dashboard/test_entries.py @@ -0,0 +1,203 @@ +"""Tests for dashboard entries Path-related functionality.""" + +from __future__ import annotations + +from pathlib import Path +import tempfile +from unittest.mock import MagicMock + +import pytest +import pytest_asyncio + +from esphome.core import CORE +from esphome.dashboard.entries import DashboardEntries, DashboardEntry + + +def create_cache_key() -> tuple[int, int, float, int]: + """Helper to create a valid DashboardCacheKeyType.""" + return (0, 0, 0.0, 0) + + +@pytest.fixture(autouse=True) +def setup_core(): + """Set up CORE for testing.""" + with tempfile.TemporaryDirectory() as tmpdir: + CORE.config_path = str(Path(tmpdir) / "test.yaml") + yield + CORE.reset() + + +@pytest.fixture +def mock_settings() -> MagicMock: + """Create mock dashboard settings.""" + settings = MagicMock() + settings.config_dir = "/test/config" + settings.absolute_config_dir = Path("/test/config") + return settings + + +@pytest_asyncio.fixture +async def dashboard_entries(mock_settings: MagicMock) -> DashboardEntries: + """Create a DashboardEntries instance for testing.""" + return DashboardEntries(mock_settings) + + +def test_dashboard_entry_path_initialization() -> None: + """Test DashboardEntry initializes with path correctly.""" + test_path = "/test/config/device.yaml" + cache_key = create_cache_key() + + entry = DashboardEntry(test_path, cache_key) + + assert entry.path == test_path + assert entry.cache_key == cache_key + + +def test_dashboard_entry_path_with_absolute_path() -> None: + """Test DashboardEntry handles absolute paths.""" + # Use a truly absolute path for the platform + test_path = Path.cwd() / "absolute" / "path" / "to" / "config.yaml" + cache_key = create_cache_key() + + entry = DashboardEntry(str(test_path), cache_key) + + assert entry.path == str(test_path) + assert Path(entry.path).is_absolute() + + +def test_dashboard_entry_path_with_relative_path() -> None: + """Test DashboardEntry handles relative paths.""" + test_path = "configs/device.yaml" + cache_key = create_cache_key() + + entry = DashboardEntry(test_path, cache_key) + + assert entry.path == test_path + assert not Path(entry.path).is_absolute() + + +@pytest.mark.asyncio +async def test_dashboard_entries_get_by_path( + dashboard_entries: DashboardEntries, +) -> None: + """Test getting entry by path.""" + test_path = "/test/config/device.yaml" + entry = DashboardEntry(test_path, create_cache_key()) + + dashboard_entries._entries[test_path] = entry + + result = dashboard_entries.get(test_path) + assert result == entry + + +@pytest.mark.asyncio +async def test_dashboard_entries_get_nonexistent_path( + dashboard_entries: DashboardEntries, +) -> None: + """Test getting non-existent entry returns None.""" + result = dashboard_entries.get("/nonexistent/path.yaml") + assert result is None + + +@pytest.mark.asyncio +async def test_dashboard_entries_path_normalization( + dashboard_entries: DashboardEntries, +) -> None: + """Test that paths are handled consistently.""" + path1 = "/test/config/device.yaml" + + entry = DashboardEntry(path1, create_cache_key()) + dashboard_entries._entries[path1] = entry + + result = dashboard_entries.get(path1) + assert result == entry + + +@pytest.mark.asyncio +async def test_dashboard_entries_path_with_spaces( + dashboard_entries: DashboardEntries, +) -> None: + """Test handling paths with spaces.""" + test_path = "/test/config/my device.yaml" + entry = DashboardEntry(test_path, create_cache_key()) + + dashboard_entries._entries[test_path] = entry + + result = dashboard_entries.get(test_path) + assert result == entry + assert result.path == test_path + + +@pytest.mark.asyncio +async def test_dashboard_entries_path_with_special_chars( + dashboard_entries: DashboardEntries, +) -> None: + """Test handling paths with special characters.""" + test_path = "/test/config/device-01_test.yaml" + entry = DashboardEntry(test_path, create_cache_key()) + + dashboard_entries._entries[test_path] = entry + + result = dashboard_entries.get(test_path) + assert result == entry + + +def test_dashboard_entries_windows_path() -> None: + """Test handling Windows-style paths.""" + test_path = r"C:\Users\test\esphome\device.yaml" + cache_key = create_cache_key() + + entry = DashboardEntry(test_path, cache_key) + + assert entry.path == test_path + + +@pytest.mark.asyncio +async def test_dashboard_entries_path_to_cache_key_mapping( + dashboard_entries: DashboardEntries, +) -> None: + """Test internal entries storage with paths and cache keys.""" + path1 = "/test/config/device1.yaml" + path2 = "/test/config/device2.yaml" + + entry1 = DashboardEntry(path1, create_cache_key()) + entry2 = DashboardEntry(path2, (1, 1, 1.0, 1)) + + dashboard_entries._entries[path1] = entry1 + dashboard_entries._entries[path2] = entry2 + + assert path1 in dashboard_entries._entries + assert path2 in dashboard_entries._entries + assert dashboard_entries._entries[path1].cache_key == create_cache_key() + assert dashboard_entries._entries[path2].cache_key == (1, 1, 1.0, 1) + + +def test_dashboard_entry_path_property() -> None: + """Test that path property returns expected value.""" + test_path = "/test/config/device.yaml" + entry = DashboardEntry(test_path, create_cache_key()) + + assert entry.path == test_path + assert isinstance(entry.path, str) + + +@pytest.mark.asyncio +async def test_dashboard_entries_all_returns_entries_with_paths( + dashboard_entries: DashboardEntries, +) -> None: + """Test that all() returns entries with their paths intact.""" + paths = [ + "/test/config/device1.yaml", + "/test/config/device2.yaml", + "/test/config/subfolder/device3.yaml", + ] + + for path in paths: + entry = DashboardEntry(path, create_cache_key()) + dashboard_entries._entries[path] = entry + + all_entries = dashboard_entries.async_all() + + assert len(all_entries) == len(paths) + retrieved_paths = [entry.path for entry in all_entries] + assert set(retrieved_paths) == set(paths) diff --git a/tests/dashboard/test_settings.py b/tests/dashboard/test_settings.py new file mode 100644 index 0000000000..90a79ac0f8 --- /dev/null +++ b/tests/dashboard/test_settings.py @@ -0,0 +1,168 @@ +"""Tests for dashboard settings Path-related functionality.""" + +from __future__ import annotations + +import os +from pathlib import Path +import tempfile + +import pytest + +from esphome.dashboard.settings import DashboardSettings + + +@pytest.fixture +def dashboard_settings(tmp_path: Path) -> DashboardSettings: + """Create DashboardSettings instance with temp directory.""" + settings = DashboardSettings() + # Resolve symlinks to ensure paths match + resolved_dir = tmp_path.resolve() + settings.config_dir = str(resolved_dir) + settings.absolute_config_dir = resolved_dir + return settings + + +def test_rel_path_simple(dashboard_settings: DashboardSettings) -> None: + """Test rel_path with simple relative path.""" + result = dashboard_settings.rel_path("config.yaml") + + expected = str(Path(dashboard_settings.config_dir) / "config.yaml") + assert result == expected + + +def test_rel_path_multiple_components(dashboard_settings: DashboardSettings) -> None: + """Test rel_path with multiple path components.""" + result = dashboard_settings.rel_path("subfolder", "device", "config.yaml") + + expected = str( + Path(dashboard_settings.config_dir) / "subfolder" / "device" / "config.yaml" + ) + assert result == expected + + +def test_rel_path_with_dots(dashboard_settings: DashboardSettings) -> None: + """Test rel_path prevents directory traversal.""" + # This should raise ValueError as it tries to go outside config_dir + with pytest.raises(ValueError): + dashboard_settings.rel_path("..", "outside.yaml") + + +def test_rel_path_absolute_path_within_config( + dashboard_settings: DashboardSettings, +) -> None: + """Test rel_path with absolute path that's within config dir.""" + internal_path = dashboard_settings.absolute_config_dir / "internal.yaml" + + internal_path.touch() + result = dashboard_settings.rel_path("internal.yaml") + expected = str(Path(dashboard_settings.config_dir) / "internal.yaml") + assert result == expected + + +def test_rel_path_absolute_path_outside_config( + dashboard_settings: DashboardSettings, +) -> None: + """Test rel_path with absolute path outside config dir raises error.""" + outside_path = "/tmp/outside/config.yaml" + + with pytest.raises(ValueError): + dashboard_settings.rel_path(outside_path) + + +def test_rel_path_empty_args(dashboard_settings: DashboardSettings) -> None: + """Test rel_path with no arguments returns config_dir.""" + result = dashboard_settings.rel_path() + assert result == dashboard_settings.config_dir + + +def test_rel_path_with_pathlib_path(dashboard_settings: DashboardSettings) -> None: + """Test rel_path works with Path objects as arguments.""" + path_obj = Path("subfolder") / "config.yaml" + result = dashboard_settings.rel_path(path_obj) + + expected = str(Path(dashboard_settings.config_dir) / "subfolder" / "config.yaml") + assert result == expected + + +def test_rel_path_normalizes_slashes(dashboard_settings: DashboardSettings) -> None: + """Test rel_path normalizes path separators.""" + # os.path.join normalizes slashes on Windows but preserves them on Unix + # Test that providing components separately gives same result + result1 = dashboard_settings.rel_path("folder", "subfolder", "file.yaml") + result2 = dashboard_settings.rel_path("folder", "subfolder", "file.yaml") + assert result1 == result2 + + # Also test that the result is as expected + expected = os.path.join( + dashboard_settings.config_dir, "folder", "subfolder", "file.yaml" + ) + assert result1 == expected + + +def test_rel_path_handles_spaces(dashboard_settings: DashboardSettings) -> None: + """Test rel_path handles paths with spaces.""" + result = dashboard_settings.rel_path("my folder", "my config.yaml") + + expected = str(Path(dashboard_settings.config_dir) / "my folder" / "my config.yaml") + assert result == expected + + +def test_rel_path_handles_special_chars(dashboard_settings: DashboardSettings) -> None: + """Test rel_path handles paths with special characters.""" + result = dashboard_settings.rel_path("device-01_test", "config.yaml") + + expected = str( + Path(dashboard_settings.config_dir) / "device-01_test" / "config.yaml" + ) + assert result == expected + + +def test_config_dir_as_path_property(dashboard_settings: DashboardSettings) -> None: + """Test that config_dir can be accessed and used with Path operations.""" + config_path = Path(dashboard_settings.config_dir) + + assert config_path.exists() + assert config_path.is_dir() + assert config_path.is_absolute() + + +def test_absolute_config_dir_property(dashboard_settings: DashboardSettings) -> None: + """Test absolute_config_dir is a Path object.""" + assert isinstance(dashboard_settings.absolute_config_dir, Path) + assert dashboard_settings.absolute_config_dir.exists() + assert dashboard_settings.absolute_config_dir.is_dir() + assert dashboard_settings.absolute_config_dir.is_absolute() + + +def test_rel_path_symlink_inside_config(dashboard_settings: DashboardSettings) -> None: + """Test rel_path with symlink that points inside config dir.""" + target = dashboard_settings.absolute_config_dir / "target.yaml" + target.touch() + symlink = dashboard_settings.absolute_config_dir / "link.yaml" + symlink.symlink_to(target) + result = dashboard_settings.rel_path("link.yaml") + expected = str(Path(dashboard_settings.config_dir) / "link.yaml") + assert result == expected + + +def test_rel_path_symlink_outside_config(dashboard_settings: DashboardSettings) -> None: + """Test rel_path with symlink that points outside config dir.""" + with tempfile.NamedTemporaryFile(suffix=".yaml") as tmp: + symlink = dashboard_settings.absolute_config_dir / "external_link.yaml" + symlink.symlink_to(tmp.name) + with pytest.raises(ValueError): + dashboard_settings.rel_path("external_link.yaml") + + +def test_rel_path_with_none_arg(dashboard_settings: DashboardSettings) -> None: + """Test rel_path handles None arguments gracefully.""" + result = dashboard_settings.rel_path("None") + expected = str(Path(dashboard_settings.config_dir) / "None") + assert result == expected + + +def test_rel_path_with_numeric_args(dashboard_settings: DashboardSettings) -> None: + """Test rel_path handles numeric arguments.""" + result = dashboard_settings.rel_path("123", "456.789") + expected = str(Path(dashboard_settings.config_dir) / "123" / "456.789") + assert result == expected diff --git a/tests/dashboard/test_web_server_paths.py b/tests/dashboard/test_web_server_paths.py new file mode 100644 index 0000000000..f66e6a7ec2 --- /dev/null +++ b/tests/dashboard/test_web_server_paths.py @@ -0,0 +1,230 @@ +"""Tests for dashboard web_server Path-related functionality.""" + +from __future__ import annotations + +import gzip +import os +from pathlib import Path +from unittest.mock import MagicMock, patch + +from esphome.dashboard import web_server + + +def test_get_base_frontend_path_production() -> None: + """Test get_base_frontend_path in production mode.""" + mock_module = MagicMock() + mock_module.where.return_value = "/usr/local/lib/esphome_dashboard" + + with ( + patch.dict(os.environ, {}, clear=True), + patch.dict("sys.modules", {"esphome_dashboard": mock_module}), + ): + result = web_server.get_base_frontend_path() + assert result == "/usr/local/lib/esphome_dashboard" + mock_module.where.assert_called_once() + + +def test_get_base_frontend_path_dev_mode() -> None: + """Test get_base_frontend_path in development mode.""" + test_path = "/home/user/esphome/dashboard" + + with patch.dict(os.environ, {"ESPHOME_DASHBOARD_DEV": test_path}): + result = web_server.get_base_frontend_path() + + # The function uses os.path.abspath which doesn't resolve symlinks + # We need to match that behavior + # The actual function adds "/" to the path, so we simulate that + test_path_with_slash = test_path if test_path.endswith("/") else test_path + "/" + expected = os.path.abspath( + os.path.join(os.getcwd(), test_path_with_slash, "esphome_dashboard") + ) + assert result == expected + + +def test_get_base_frontend_path_dev_mode_with_trailing_slash() -> None: + """Test get_base_frontend_path in dev mode with trailing slash.""" + test_path = "/home/user/esphome/dashboard/" + + with patch.dict(os.environ, {"ESPHOME_DASHBOARD_DEV": test_path}): + result = web_server.get_base_frontend_path() + + # The function uses os.path.abspath which doesn't resolve symlinks + expected = os.path.abspath(str(Path.cwd() / test_path / "esphome_dashboard")) + assert result == expected + + +def test_get_base_frontend_path_dev_mode_relative_path() -> None: + """Test get_base_frontend_path with relative dev path.""" + test_path = "./dashboard" + + with patch.dict(os.environ, {"ESPHOME_DASHBOARD_DEV": test_path}): + result = web_server.get_base_frontend_path() + + # The function uses os.path.abspath which doesn't resolve symlinks + # We need to match that behavior + # The actual function adds "/" to the path, so we simulate that + test_path_with_slash = test_path if test_path.endswith("/") else test_path + "/" + expected = os.path.abspath( + os.path.join(os.getcwd(), test_path_with_slash, "esphome_dashboard") + ) + assert result == expected + assert Path(result).is_absolute() + + +def test_get_static_path_single_component() -> None: + """Test get_static_path with single path component.""" + with patch("esphome.dashboard.web_server.get_base_frontend_path") as mock_base: + mock_base.return_value = "/base/frontend" + + result = web_server.get_static_path("file.js") + + assert result == os.path.join("/base/frontend", "static", "file.js") + + +def test_get_static_path_multiple_components() -> None: + """Test get_static_path with multiple path components.""" + with patch("esphome.dashboard.web_server.get_base_frontend_path") as mock_base: + mock_base.return_value = "/base/frontend" + + result = web_server.get_static_path("js", "esphome", "index.js") + + assert result == os.path.join( + "/base/frontend", "static", "js", "esphome", "index.js" + ) + + +def test_get_static_path_empty_args() -> None: + """Test get_static_path with no arguments.""" + with patch("esphome.dashboard.web_server.get_base_frontend_path") as mock_base: + mock_base.return_value = "/base/frontend" + + result = web_server.get_static_path() + + assert result == os.path.join("/base/frontend", "static") + + +def test_get_static_path_with_pathlib_path() -> None: + """Test get_static_path with Path objects.""" + with patch("esphome.dashboard.web_server.get_base_frontend_path") as mock_base: + mock_base.return_value = "/base/frontend" + + path_obj = Path("js") / "app.js" + result = web_server.get_static_path(str(path_obj)) + + assert result == os.path.join("/base/frontend", "static", "js", "app.js") + + +def test_get_static_file_url_production() -> None: + """Test get_static_file_url in production mode.""" + web_server.get_static_file_url.cache_clear() + mock_module = MagicMock() + mock_file = MagicMock() + mock_file.read.return_value = b"test content" + mock_file.__enter__ = MagicMock(return_value=mock_file) + mock_file.__exit__ = MagicMock(return_value=None) + + with ( + patch.dict(os.environ, {}, clear=True), + patch.dict("sys.modules", {"esphome_dashboard": mock_module}), + patch("esphome.dashboard.web_server.get_static_path") as mock_get_path, + patch("esphome.dashboard.web_server.open", create=True, return_value=mock_file), + ): + mock_get_path.return_value = "/fake/path/js/app.js" + result = web_server.get_static_file_url("js/app.js") + assert result.startswith("./static/js/app.js?hash=") + + +def test_get_static_file_url_dev_mode() -> None: + """Test get_static_file_url in development mode.""" + with patch.dict(os.environ, {"ESPHOME_DASHBOARD_DEV": "/dev/path"}): + web_server.get_static_file_url.cache_clear() + result = web_server.get_static_file_url("js/app.js") + + assert result == "./static/js/app.js" + + +def test_get_static_file_url_index_js_special_case() -> None: + """Test get_static_file_url replaces index.js with entrypoint.""" + web_server.get_static_file_url.cache_clear() + mock_module = MagicMock() + mock_module.entrypoint.return_value = "main.js" + + with ( + patch.dict(os.environ, {}, clear=True), + patch.dict("sys.modules", {"esphome_dashboard": mock_module}), + ): + result = web_server.get_static_file_url("js/esphome/index.js") + assert result == "./static/js/esphome/main.js" + + +def test_load_file_path(tmp_path: Path) -> None: + """Test loading a file.""" + test_file = tmp_path / "test.txt" + test_file.write_bytes(b"test content") + + with open(test_file, "rb") as f: + content = f.read() + assert content == b"test content" + + +def test_load_file_compressed_path(tmp_path: Path) -> None: + """Test loading a compressed file.""" + test_file = tmp_path / "test.txt.gz" + + with gzip.open(test_file, "wb") as gz: + gz.write(b"compressed content") + + with gzip.open(test_file, "rb") as gz: + content = gz.read() + assert content == b"compressed content" + + +def test_path_normalization_in_static_path() -> None: + """Test that paths are normalized correctly.""" + with patch("esphome.dashboard.web_server.get_base_frontend_path") as mock_base: + mock_base.return_value = "/base/frontend" + + # Test with separate components + result1 = web_server.get_static_path("js", "app.js") + result2 = web_server.get_static_path("js", "app.js") + + assert result1 == result2 + assert result1 == os.path.join("/base/frontend", "static", "js", "app.js") + + +def test_windows_path_handling() -> None: + """Test handling of Windows-style paths.""" + with patch("esphome.dashboard.web_server.get_base_frontend_path") as mock_base: + mock_base.return_value = r"C:\Program Files\esphome\frontend" + + result = web_server.get_static_path("js", "app.js") + + # os.path.join should handle this correctly on the platform + expected = os.path.join( + r"C:\Program Files\esphome\frontend", "static", "js", "app.js" + ) + assert result == expected + + +def test_path_with_special_characters() -> None: + """Test paths with special characters.""" + with patch("esphome.dashboard.web_server.get_base_frontend_path") as mock_base: + mock_base.return_value = "/base/frontend" + + result = web_server.get_static_path("js-modules", "app_v1.0.js") + + assert result == os.path.join( + "/base/frontend", "static", "js-modules", "app_v1.0.js" + ) + + +def test_path_with_spaces() -> None: + """Test paths with spaces.""" + with patch("esphome.dashboard.web_server.get_base_frontend_path") as mock_base: + mock_base.return_value = "/base/my frontend" + + result = web_server.get_static_path("my js", "my app.js") + + assert result == os.path.join( + "/base/my frontend", "static", "my js", "my app.js" + ) From fe1371f4dc9707e3cec70f2f72d9b4bfa795034a Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 11 Sep 2025 15:32:04 +1200 Subject: [PATCH 09/11] [adc] Fix `FILTER_SOURCE_FILES` location (#10673) --- esphome/components/adc/__init__.py | 27 +-------------------------- esphome/components/adc/sensor.py | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 26 deletions(-) diff --git a/esphome/components/adc/__init__.py b/esphome/components/adc/__init__.py index f260e13242..15dc447b6c 100644 --- a/esphome/components/adc/__init__.py +++ b/esphome/components/adc/__init__.py @@ -11,15 +11,8 @@ from esphome.components.esp32.const import ( VARIANT_ESP32S2, VARIANT_ESP32S3, ) -from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv -from esphome.const import ( - CONF_ANALOG, - CONF_INPUT, - CONF_NUMBER, - PLATFORM_ESP8266, - PlatformFramework, -) +from esphome.const import CONF_ANALOG, CONF_INPUT, CONF_NUMBER, PLATFORM_ESP8266 from esphome.core import CORE CODEOWNERS = ["@esphome/core"] @@ -273,21 +266,3 @@ def validate_adc_pin(value): )(value) raise NotImplementedError - - -FILTER_SOURCE_FILES = filter_source_files_from_platform( - { - "adc_sensor_esp32.cpp": { - PlatformFramework.ESP32_ARDUINO, - PlatformFramework.ESP32_IDF, - }, - "adc_sensor_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO}, - "adc_sensor_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO}, - "adc_sensor_libretiny.cpp": { - PlatformFramework.BK72XX_ARDUINO, - PlatformFramework.RTL87XX_ARDUINO, - PlatformFramework.LN882X_ARDUINO, - }, - "adc_sensor_zephyr.cpp": {PlatformFramework.NRF52_ZEPHYR}, - } -) diff --git a/esphome/components/adc/sensor.py b/esphome/components/adc/sensor.py index 49970c5e3d..607609bbc7 100644 --- a/esphome/components/adc/sensor.py +++ b/esphome/components/adc/sensor.py @@ -9,6 +9,7 @@ from esphome.components.zephyr import ( zephyr_add_prj_conf, zephyr_add_user, ) +from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( CONF_ATTENUATION, @@ -20,6 +21,7 @@ from esphome.const import ( PLATFORM_NRF52, STATE_CLASS_MEASUREMENT, UNIT_VOLT, + PlatformFramework, ) from esphome.core import CORE @@ -174,3 +176,21 @@ async def to_code(config): }}; """ ) + + +FILTER_SOURCE_FILES = filter_source_files_from_platform( + { + "adc_sensor_esp32.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, + }, + "adc_sensor_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO}, + "adc_sensor_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO}, + "adc_sensor_libretiny.cpp": { + PlatformFramework.BK72XX_ARDUINO, + PlatformFramework.RTL87XX_ARDUINO, + PlatformFramework.LN882X_ARDUINO, + }, + "adc_sensor_zephyr.cpp": {PlatformFramework.NRF52_ZEPHYR}, + } +) From c45efe8f409ecf32fc5fe0e7bfe2ca39eb2f5a99 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 11 Sep 2025 00:01:11 -0500 Subject: [PATCH 10/11] Add additional coverage for yaml_util (#10674) --- .../fixtures/yaml_util/named_dir/.hidden.yaml | 3 + .../fixtures/yaml_util/named_dir/not_yaml.txt | 1 + .../fixtures/yaml_util/named_dir/sensor1.yaml | 4 + .../fixtures/yaml_util/named_dir/sensor2.yaml | 4 + .../yaml_util/named_dir/subdir/sensor3.yaml | 4 + .../fixtures/yaml_util/secrets.yaml | 4 + .../fixtures/yaml_util/test_secret.yaml | 17 ++ tests/unit_tests/test_yaml_util.py | 220 +++++++++++++++++- 8 files changed, 255 insertions(+), 2 deletions(-) create mode 100644 tests/unit_tests/fixtures/yaml_util/named_dir/.hidden.yaml create mode 100644 tests/unit_tests/fixtures/yaml_util/named_dir/not_yaml.txt create mode 100644 tests/unit_tests/fixtures/yaml_util/named_dir/sensor1.yaml create mode 100644 tests/unit_tests/fixtures/yaml_util/named_dir/sensor2.yaml create mode 100644 tests/unit_tests/fixtures/yaml_util/named_dir/subdir/sensor3.yaml create mode 100644 tests/unit_tests/fixtures/yaml_util/secrets.yaml create mode 100644 tests/unit_tests/fixtures/yaml_util/test_secret.yaml diff --git a/tests/unit_tests/fixtures/yaml_util/named_dir/.hidden.yaml b/tests/unit_tests/fixtures/yaml_util/named_dir/.hidden.yaml new file mode 100644 index 0000000000..75eb989ea5 --- /dev/null +++ b/tests/unit_tests/fixtures/yaml_util/named_dir/.hidden.yaml @@ -0,0 +1,3 @@ +# This file should be ignored +platform: template +name: "Hidden Sensor" diff --git a/tests/unit_tests/fixtures/yaml_util/named_dir/not_yaml.txt b/tests/unit_tests/fixtures/yaml_util/named_dir/not_yaml.txt new file mode 100644 index 0000000000..98efb74b0f --- /dev/null +++ b/tests/unit_tests/fixtures/yaml_util/named_dir/not_yaml.txt @@ -0,0 +1 @@ +This is not a YAML file and should be ignored diff --git a/tests/unit_tests/fixtures/yaml_util/named_dir/sensor1.yaml b/tests/unit_tests/fixtures/yaml_util/named_dir/sensor1.yaml new file mode 100644 index 0000000000..a4b0a11916 --- /dev/null +++ b/tests/unit_tests/fixtures/yaml_util/named_dir/sensor1.yaml @@ -0,0 +1,4 @@ +platform: template +name: "Sensor 1" +lambda: |- + return 42.0; diff --git a/tests/unit_tests/fixtures/yaml_util/named_dir/sensor2.yaml b/tests/unit_tests/fixtures/yaml_util/named_dir/sensor2.yaml new file mode 100644 index 0000000000..72d4b714b6 --- /dev/null +++ b/tests/unit_tests/fixtures/yaml_util/named_dir/sensor2.yaml @@ -0,0 +1,4 @@ +platform: template +name: "Sensor 2" +lambda: |- + return 100.0; diff --git a/tests/unit_tests/fixtures/yaml_util/named_dir/subdir/sensor3.yaml b/tests/unit_tests/fixtures/yaml_util/named_dir/subdir/sensor3.yaml new file mode 100644 index 0000000000..bcb8dd320d --- /dev/null +++ b/tests/unit_tests/fixtures/yaml_util/named_dir/subdir/sensor3.yaml @@ -0,0 +1,4 @@ +platform: template +name: "Sensor 3 in subdir" +lambda: |- + return 200.0; diff --git a/tests/unit_tests/fixtures/yaml_util/secrets.yaml b/tests/unit_tests/fixtures/yaml_util/secrets.yaml new file mode 100644 index 0000000000..4eef570926 --- /dev/null +++ b/tests/unit_tests/fixtures/yaml_util/secrets.yaml @@ -0,0 +1,4 @@ +test_secret: "my_secret_value" +another_secret: "another_value" +wifi_password: "super_secret_wifi" +api_key: "0123456789abcdef" diff --git a/tests/unit_tests/fixtures/yaml_util/test_secret.yaml b/tests/unit_tests/fixtures/yaml_util/test_secret.yaml new file mode 100644 index 0000000000..c23afaee94 --- /dev/null +++ b/tests/unit_tests/fixtures/yaml_util/test_secret.yaml @@ -0,0 +1,17 @@ +esphome: + name: test_device + platform: ESP32 + board: esp32dev + +wifi: + ssid: "TestNetwork" + password: !secret wifi_password + +api: + encryption: + key: !secret api_key + +sensor: + - platform: template + name: "Test Sensor" + id: !secret test_secret diff --git a/tests/unit_tests/test_yaml_util.py b/tests/unit_tests/test_yaml_util.py index f31e9554dc..bc3c89a64d 100644 --- a/tests/unit_tests/test_yaml_util.py +++ b/tests/unit_tests/test_yaml_util.py @@ -1,9 +1,26 @@ -from esphome import yaml_util +from pathlib import Path +import shutil +from unittest.mock import patch + +import pytest + +from esphome import core, yaml_util from esphome.components import substitutions from esphome.core import EsphomeError +from esphome.util import OrderedDict -def test_include_with_vars(fixture_path): +@pytest.fixture(autouse=True) +def clear_secrets_cache() -> None: + """Clear the secrets cache before each test.""" + yaml_util._SECRET_VALUES.clear() + yaml_util._SECRET_CACHE.clear() + yield + yaml_util._SECRET_VALUES.clear() + yaml_util._SECRET_CACHE.clear() + + +def test_include_with_vars(fixture_path: Path) -> None: yaml_file = fixture_path / "yaml_util" / "includetest.yaml" actual = yaml_util.load_yaml(yaml_file) @@ -62,3 +79,202 @@ def test_parsing_with_custom_loader(fixture_path): assert loader_calls[0].endswith("includes/included.yaml") assert loader_calls[1].endswith("includes/list.yaml") assert loader_calls[2].endswith("includes/scalar.yaml") + + +def test_construct_secret_simple(fixture_path: Path) -> None: + """Test loading a YAML file with !secret tags.""" + yaml_file = fixture_path / "yaml_util" / "test_secret.yaml" + + actual = yaml_util.load_yaml(yaml_file) + + # Check that secrets were properly loaded + assert actual["wifi"]["password"] == "super_secret_wifi" + assert actual["api"]["encryption"]["key"] == "0123456789abcdef" + assert actual["sensor"][0]["id"] == "my_secret_value" + + +def test_construct_secret_missing(fixture_path: Path, tmp_path: Path) -> None: + """Test that missing secrets raise proper errors.""" + # Create a YAML file with a secret that doesn't exist + test_yaml = tmp_path / "test.yaml" + test_yaml.write_text(""" +esphome: + name: test + +wifi: + password: !secret nonexistent_secret +""") + + # Create an empty secrets file + secrets_yaml = tmp_path / "secrets.yaml" + secrets_yaml.write_text("some_other_secret: value") + + with pytest.raises(EsphomeError, match="Secret 'nonexistent_secret' not defined"): + yaml_util.load_yaml(str(test_yaml)) + + +def test_construct_secret_no_secrets_file(tmp_path: Path) -> None: + """Test that missing secrets.yaml file raises proper error.""" + # Create a YAML file with a secret but no secrets.yaml + test_yaml = tmp_path / "test.yaml" + test_yaml.write_text(""" +wifi: + password: !secret some_secret +""") + + # Mock CORE.config_path to avoid NoneType error + with ( + patch.object(core.CORE, "config_path", str(tmp_path / "main.yaml")), + pytest.raises(EsphomeError, match="secrets.yaml"), + ): + yaml_util.load_yaml(str(test_yaml)) + + +def test_construct_secret_fallback_to_main_config_dir( + fixture_path: Path, tmp_path: Path +) -> None: + """Test fallback to main config directory for secrets.""" + # Create a subdirectory with a YAML file that uses secrets + subdir = tmp_path / "subdir" + subdir.mkdir() + + test_yaml = subdir / "test.yaml" + test_yaml.write_text(""" +wifi: + password: !secret test_secret +""") + + # Create secrets.yaml in the main directory + main_secrets = tmp_path / "secrets.yaml" + main_secrets.write_text("test_secret: main_secret_value") + + # Mock CORE.config_path to point to main directory + with patch.object(core.CORE, "config_path", str(tmp_path / "main.yaml")): + actual = yaml_util.load_yaml(str(test_yaml)) + assert actual["wifi"]["password"] == "main_secret_value" + + +def test_construct_include_dir_named(fixture_path: Path, tmp_path: Path) -> None: + """Test !include_dir_named directive.""" + # Copy fixture directory to temporary location + src_dir = fixture_path / "yaml_util" + dst_dir = tmp_path / "yaml_util" + shutil.copytree(src_dir, dst_dir) + + # Create test YAML that uses include_dir_named + test_yaml = dst_dir / "test_include_named.yaml" + test_yaml.write_text(""" +sensor: !include_dir_named named_dir +""") + + actual = yaml_util.load_yaml(str(test_yaml)) + actual_sensor = actual["sensor"] + + # Check that files were loaded with their names as keys + assert isinstance(actual_sensor, OrderedDict) + assert "sensor1" in actual_sensor + assert "sensor2" in actual_sensor + assert "sensor3" in actual_sensor # Files from subdirs are included with basename + + # Check content of loaded files + assert actual_sensor["sensor1"]["platform"] == "template" + assert actual_sensor["sensor1"]["name"] == "Sensor 1" + assert actual_sensor["sensor2"]["platform"] == "template" + assert actual_sensor["sensor2"]["name"] == "Sensor 2" + + # Check that subdirectory files are included with their basename + assert actual_sensor["sensor3"]["platform"] == "template" + assert actual_sensor["sensor3"]["name"] == "Sensor 3 in subdir" + + # Check that hidden files and non-YAML files are not included + assert ".hidden" not in actual_sensor + assert "not_yaml" not in actual_sensor + + +def test_construct_include_dir_named_empty_dir(tmp_path: Path) -> None: + """Test !include_dir_named with empty directory.""" + # Create empty directory + empty_dir = tmp_path / "empty_dir" + empty_dir.mkdir() + + test_yaml = tmp_path / "test.yaml" + test_yaml.write_text(""" +sensor: !include_dir_named empty_dir +""") + + actual = yaml_util.load_yaml(str(test_yaml)) + + # Should return empty OrderedDict + assert isinstance(actual["sensor"], OrderedDict) + assert len(actual["sensor"]) == 0 + + +def test_construct_include_dir_named_with_dots(tmp_path: Path) -> None: + """Test that include_dir_named ignores files starting with dots.""" + # Create directory with various files + test_dir = tmp_path / "test_dir" + test_dir.mkdir() + + # Create visible file + visible_file = test_dir / "visible.yaml" + visible_file.write_text("key: visible_value") + + # Create hidden file + hidden_file = test_dir / ".hidden.yaml" + hidden_file.write_text("key: hidden_value") + + # Create hidden directory with files + hidden_dir = test_dir / ".hidden_dir" + hidden_dir.mkdir() + hidden_subfile = hidden_dir / "subfile.yaml" + hidden_subfile.write_text("key: hidden_subfile_value") + + test_yaml = tmp_path / "test.yaml" + test_yaml.write_text(""" +test: !include_dir_named test_dir +""") + + actual = yaml_util.load_yaml(str(test_yaml)) + + # Should only include visible file + assert "visible" in actual["test"] + assert actual["test"]["visible"]["key"] == "visible_value" + + # Should not include hidden files or directories + assert ".hidden" not in actual["test"] + assert ".hidden_dir" not in actual["test"] + + +def test_find_files_recursive(fixture_path: Path, tmp_path: Path) -> None: + """Test that _find_files works recursively through include_dir_named.""" + # Copy fixture directory to temporary location + src_dir = fixture_path / "yaml_util" + dst_dir = tmp_path / "yaml_util" + shutil.copytree(src_dir, dst_dir) + + # This indirectly tests _find_files by using include_dir_named + test_yaml = dst_dir / "test_include_recursive.yaml" + test_yaml.write_text(""" +all_sensors: !include_dir_named named_dir +""") + + actual = yaml_util.load_yaml(str(test_yaml)) + + # Should find sensor1.yaml, sensor2.yaml, and subdir/sensor3.yaml (all flattened) + assert len(actual["all_sensors"]) == 3 + assert "sensor1" in actual["all_sensors"] + assert "sensor2" in actual["all_sensors"] + assert "sensor3" in actual["all_sensors"] + + +def test_secret_values_tracking(fixture_path: Path) -> None: + """Test that secret values are properly tracked for dumping.""" + yaml_file = fixture_path / "yaml_util" / "test_secret.yaml" + + yaml_util.load_yaml(yaml_file) + + # Check that secret values are tracked + assert "super_secret_wifi" in yaml_util._SECRET_VALUES + assert yaml_util._SECRET_VALUES["super_secret_wifi"] == "wifi_password" + assert "0123456789abcdef" in yaml_util._SECRET_VALUES + assert yaml_util._SECRET_VALUES["0123456789abcdef"] == "api_key" From 625f108183fc58575315789033c5ed67fa2f0f8c Mon Sep 17 00:00:00 2001 From: rwrozelle Date: Thu, 11 Sep 2025 01:23:58 -0400 Subject: [PATCH 11/11] Openthread Fix Factory Reset (#9281) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- .../button/factory_reset_button.cpp | 19 +++++++- .../button/factory_reset_button.h | 7 ++- .../switch/factory_reset_switch.cpp | 19 +++++++- .../switch/factory_reset_switch.h | 6 ++- esphome/components/openthread/openthread.cpp | 48 ++++++++++++++++--- esphome/components/openthread/openthread.h | 11 +++++ 6 files changed, 99 insertions(+), 11 deletions(-) diff --git a/esphome/components/factory_reset/button/factory_reset_button.cpp b/esphome/components/factory_reset/button/factory_reset_button.cpp index 585975c043..d582317767 100644 --- a/esphome/components/factory_reset/button/factory_reset_button.cpp +++ b/esphome/components/factory_reset/button/factory_reset_button.cpp @@ -1,7 +1,13 @@ #include "factory_reset_button.h" + +#include "esphome/core/defines.h" + +#ifdef USE_OPENTHREAD +#include "esphome/components/openthread/openthread.h" +#endif +#include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" -#include "esphome/core/application.h" namespace esphome { namespace factory_reset { @@ -13,9 +19,20 @@ void FactoryResetButton::press_action() { ESP_LOGI(TAG, "Resetting"); // Let MQTT settle a bit delay(100); // NOLINT +#ifdef USE_OPENTHREAD + openthread::global_openthread_component->on_factory_reset(FactoryResetButton::factory_reset_callback); +#else + global_preferences->reset(); + App.safe_reboot(); +#endif +} + +#ifdef USE_OPENTHREAD +void FactoryResetButton::factory_reset_callback() { global_preferences->reset(); App.safe_reboot(); } +#endif } // namespace factory_reset } // namespace esphome diff --git a/esphome/components/factory_reset/button/factory_reset_button.h b/esphome/components/factory_reset/button/factory_reset_button.h index 9996a860d9..c68da2ca74 100644 --- a/esphome/components/factory_reset/button/factory_reset_button.h +++ b/esphome/components/factory_reset/button/factory_reset_button.h @@ -1,7 +1,9 @@ #pragma once -#include "esphome/core/component.h" +#include "esphome/core/defines.h" + #include "esphome/components/button/button.h" +#include "esphome/core/component.h" namespace esphome { namespace factory_reset { @@ -9,6 +11,9 @@ namespace factory_reset { class FactoryResetButton : public button::Button, public Component { public: void dump_config() override; +#ifdef USE_OPENTHREAD + static void factory_reset_callback(); +#endif protected: void press_action() override; diff --git a/esphome/components/factory_reset/switch/factory_reset_switch.cpp b/esphome/components/factory_reset/switch/factory_reset_switch.cpp index 1282c73f4e..75449aa526 100644 --- a/esphome/components/factory_reset/switch/factory_reset_switch.cpp +++ b/esphome/components/factory_reset/switch/factory_reset_switch.cpp @@ -1,7 +1,13 @@ #include "factory_reset_switch.h" + +#include "esphome/core/defines.h" + +#ifdef USE_OPENTHREAD +#include "esphome/components/openthread/openthread.h" +#endif +#include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" -#include "esphome/core/application.h" namespace esphome { namespace factory_reset { @@ -17,10 +23,21 @@ void FactoryResetSwitch::write_state(bool state) { ESP_LOGI(TAG, "Resetting"); // Let MQTT settle a bit delay(100); // NOLINT +#ifdef USE_OPENTHREAD + openthread::global_openthread_component->on_factory_reset(FactoryResetSwitch::factory_reset_callback); +#else global_preferences->reset(); App.safe_reboot(); +#endif } } +#ifdef USE_OPENTHREAD +void FactoryResetSwitch::factory_reset_callback() { + global_preferences->reset(); + App.safe_reboot(); +} +#endif + } // namespace factory_reset } // namespace esphome diff --git a/esphome/components/factory_reset/switch/factory_reset_switch.h b/esphome/components/factory_reset/switch/factory_reset_switch.h index 2c914ea76d..8ea0c79108 100644 --- a/esphome/components/factory_reset/switch/factory_reset_switch.h +++ b/esphome/components/factory_reset/switch/factory_reset_switch.h @@ -1,7 +1,8 @@ #pragma once -#include "esphome/core/component.h" #include "esphome/components/switch/switch.h" +#include "esphome/core/component.h" +#include "esphome/core/defines.h" namespace esphome { namespace factory_reset { @@ -9,6 +10,9 @@ namespace factory_reset { class FactoryResetSwitch : public switch_::Switch, public Component { public: void dump_config() override; +#ifdef USE_OPENTHREAD + static void factory_reset_callback(); +#endif protected: void write_state(bool state) override; diff --git a/esphome/components/openthread/openthread.cpp b/esphome/components/openthread/openthread.cpp index 322ff43238..5b5c113f83 100644 --- a/esphome/components/openthread/openthread.cpp +++ b/esphome/components/openthread/openthread.cpp @@ -11,8 +11,6 @@ #include #include #include -#include -#include #include #include @@ -77,8 +75,14 @@ std::optional OpenThreadComponent::get_omr_address_(InstanceLock & return {}; } -void srp_callback(otError err, const otSrpClientHostInfo *host_info, const otSrpClientService *services, - const otSrpClientService *removed_services, void *context) { +void OpenThreadComponent::defer_factory_reset_external_callback() { + ESP_LOGD(TAG, "Defer factory_reset_external_callback_"); + this->defer([this]() { this->factory_reset_external_callback_(); }); +} + +void OpenThreadSrpComponent::srp_callback(otError err, const otSrpClientHostInfo *host_info, + const otSrpClientService *services, + const otSrpClientService *removed_services, void *context) { if (err != 0) { ESP_LOGW(TAG, "SRP client reported an error: %s", otThreadErrorToString(err)); for (const otSrpClientHostInfo *host = host_info; host; host = nullptr) { @@ -90,16 +94,30 @@ void srp_callback(otError err, const otSrpClientHostInfo *host_info, const otSrp } } -void srp_start_callback(const otSockAddr *server_socket_address, void *context) { +void OpenThreadSrpComponent::srp_start_callback(const otSockAddr *server_socket_address, void *context) { ESP_LOGI(TAG, "SRP client has started"); } +void OpenThreadSrpComponent::srp_factory_reset_callback(otError err, const otSrpClientHostInfo *host_info, + const otSrpClientService *services, + const otSrpClientService *removed_services, void *context) { + OpenThreadComponent *obj = (OpenThreadComponent *) context; + if (err == OT_ERROR_NONE && removed_services != NULL && host_info != NULL && + host_info->mState == OT_SRP_CLIENT_ITEM_STATE_REMOVED) { + ESP_LOGD(TAG, "Successful Removal SRP Host and Services"); + } else if (err != OT_ERROR_NONE) { + // Handle other SRP client events or errors + ESP_LOGW(TAG, "SRP client event/error: %s", otThreadErrorToString(err)); + } + obj->defer_factory_reset_external_callback(); +} + void OpenThreadSrpComponent::setup() { otError error; InstanceLock lock = InstanceLock::acquire(); otInstance *instance = lock.get_instance(); - otSrpClientSetCallback(instance, srp_callback, nullptr); + otSrpClientSetCallback(instance, OpenThreadSrpComponent::srp_callback, nullptr); // set the host name uint16_t size; @@ -179,7 +197,8 @@ void OpenThreadSrpComponent::setup() { ESP_LOGD(TAG, "Added service: %s", full_service.c_str()); } - otSrpClientEnableAutoStartMode(instance, srp_start_callback, nullptr); + otSrpClientEnableAutoStartMode(instance, OpenThreadSrpComponent::srp_start_callback, nullptr); + ESP_LOGD(TAG, "Finished SRP setup"); } void *OpenThreadSrpComponent::pool_alloc_(size_t size) { @@ -217,6 +236,21 @@ bool OpenThreadComponent::teardown() { return this->teardown_complete_; } +void OpenThreadComponent::on_factory_reset(std::function callback) { + factory_reset_external_callback_ = callback; + ESP_LOGD(TAG, "Start Removal SRP Host and Services"); + otError error; + InstanceLock lock = InstanceLock::acquire(); + otInstance *instance = lock.get_instance(); + otSrpClientSetCallback(instance, OpenThreadSrpComponent::srp_factory_reset_callback, this); + error = otSrpClientRemoveHostAndServices(instance, true, true); + if (error != OT_ERROR_NONE) { + ESP_LOGW(TAG, "Failed to Remove SRP Host and Services"); + return; + } + ESP_LOGD(TAG, "Waiting on Confirmation Removal SRP Host and Services"); +} + } // namespace openthread } // namespace esphome diff --git a/esphome/components/openthread/openthread.h b/esphome/components/openthread/openthread.h index a0ea1b3f3a..a9aff78e56 100644 --- a/esphome/components/openthread/openthread.h +++ b/esphome/components/openthread/openthread.h @@ -6,6 +6,8 @@ #include "esphome/components/network/ip_address.h" #include "esphome/core/component.h" +#include +#include #include #include @@ -28,11 +30,14 @@ class OpenThreadComponent : public Component { network::IPAddresses get_ip_addresses(); std::optional get_omr_address(); void ot_main(); + void on_factory_reset(std::function callback); + void defer_factory_reset_external_callback(); protected: std::optional get_omr_address_(InstanceLock &lock); bool teardown_started_{false}; bool teardown_complete_{false}; + std::function factory_reset_external_callback_; }; extern OpenThreadComponent *global_openthread_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) @@ -43,6 +48,12 @@ class OpenThreadSrpComponent : public Component { // This has to run after the mdns component or else no services are available to advertise float get_setup_priority() const override { return this->mdns_->get_setup_priority() - 1.0; } void setup() override; + static void srp_callback(otError err, const otSrpClientHostInfo *host_info, const otSrpClientService *services, + const otSrpClientService *removed_services, void *context); + static void srp_start_callback(const otSockAddr *server_socket_address, void *context); + static void srp_factory_reset_callback(otError err, const otSrpClientHostInfo *host_info, + const otSrpClientService *services, const otSrpClientService *removed_services, + void *context); protected: esphome::mdns::MDNSComponent *mdns_{nullptr};