mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 15:12:06 +00:00 
			
		
		
		
	Merge branch 'client_info_flash' into integration
This commit is contained in:
		| @@ -205,7 +205,8 @@ void APIConnection::loop() { | ||||
|     // Disconnect if not responded within 2.5*keepalive | ||||
|     if (now - this->last_traffic_ > KEEPALIVE_DISCONNECT_TIMEOUT) { | ||||
|       on_fatal_error(); | ||||
|       ESP_LOGW(TAG, "%s (%s) is unresponsive; disconnecting", this->get_name(), this->get_peername()); | ||||
|       ESP_LOGW(TAG, "%s (%s) is unresponsive; disconnecting", this->client_info_.name.c_str(), | ||||
|                this->client_info_.peername.c_str()); | ||||
|     } | ||||
|   } else if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS && !this->flags_.remove) { | ||||
|     // Only send ping if we're not disconnecting | ||||
| @@ -255,7 +256,7 @@ bool APIConnection::send_disconnect_response(const DisconnectRequest &msg) { | ||||
|   // remote initiated disconnect_client | ||||
|   // don't close yet, we still need to send the disconnect response | ||||
|   // close will happen on next loop | ||||
|   ESP_LOGD(TAG, "%s (%s) disconnected", this->get_name(), this->get_peername()); | ||||
|   ESP_LOGD(TAG, "%s (%s) disconnected", this->client_info_.name.c_str(), this->client_info_.peername.c_str()); | ||||
|   this->flags_.next_close = true; | ||||
|   DisconnectResponse resp; | ||||
|   return this->send_message(resp, DisconnectResponse::MESSAGE_TYPE); | ||||
| @@ -1385,7 +1386,7 @@ void APIConnection::complete_authentication_() { | ||||
|   } | ||||
|  | ||||
|   this->flags_.connection_state = static_cast<uint8_t>(ConnectionState::AUTHENTICATED); | ||||
|   ESP_LOGD(TAG, "%s (%s) connected", this->get_name(), this->get_peername()); | ||||
|   ESP_LOGD(TAG, "%s (%s) connected", this->client_info_.name.c_str(), this->client_info_.peername.c_str()); | ||||
| #ifdef USE_API_CLIENT_CONNECTED_TRIGGER | ||||
|   this->parent_->get_client_connected_trigger()->trigger(this->client_info_.name, this->client_info_.peername); | ||||
| #endif | ||||
| @@ -1609,12 +1610,12 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) { | ||||
| #ifdef USE_API_PASSWORD | ||||
| void APIConnection::on_unauthenticated_access() { | ||||
|   this->on_fatal_error(); | ||||
|   ESP_LOGD(TAG, "%s (%s) no authentication", this->get_name(), this->get_peername()); | ||||
|   ESP_LOGD(TAG, "%s (%s) no authentication", this->client_info_.name.c_str(), this->client_info_.peername.c_str()); | ||||
| } | ||||
| #endif | ||||
| void APIConnection::on_no_setup_connection() { | ||||
|   this->on_fatal_error(); | ||||
|   ESP_LOGD(TAG, "%s (%s) no connection setup", this->get_name(), this->get_peername()); | ||||
|   ESP_LOGD(TAG, "%s (%s) no connection setup", this->client_info_.name.c_str(), this->client_info_.peername.c_str()); | ||||
| } | ||||
| void APIConnection::on_fatal_error() { | ||||
|   this->helper_->close(); | ||||
| @@ -1866,8 +1867,8 @@ void APIConnection::process_state_subscriptions_() { | ||||
| #endif  // USE_API_HOMEASSISTANT_STATES | ||||
|  | ||||
| void APIConnection::log_warning_(const LogString *message, APIError err) { | ||||
|   ESP_LOGW(TAG, "%s (%s): %s %s errno=%d", this->get_name(), this->get_peername(), LOG_STR_ARG(message), | ||||
|            LOG_STR_ARG(api_error_to_logstr(err)), errno); | ||||
|   ESP_LOGW(TAG, "%s (%s): %s %s errno=%d", this->client_info_.name.c_str(), this->client_info_.peername.c_str(), | ||||
|            LOG_STR_ARG(message), LOG_STR_ARG(api_error_to_logstr(err)), errno); | ||||
| } | ||||
|  | ||||
| void APIConnection::log_socket_operation_failed_(APIError err) { | ||||
|   | ||||
| @@ -270,8 +270,8 @@ class APIConnection final : public APIServerConnection { | ||||
|   bool try_to_clear_buffer(bool log_out_of_space); | ||||
|   bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override; | ||||
|  | ||||
|   const char *get_name() const { return this->client_info_.name.c_str(); } | ||||
|   const char *get_peername() const { return this->client_info_.peername.c_str(); } | ||||
|   const std::string &get_name() const { return this->client_info_.name; } | ||||
|   const std::string &get_peername() const { return this->client_info_.peername; } | ||||
|  | ||||
|  protected: | ||||
|   // Helper function to handle authentication completion | ||||
|   | ||||
| @@ -177,7 +177,8 @@ void APIServer::loop() { | ||||
|     // Network is down - disconnect all clients | ||||
|     for (auto &client : this->clients_) { | ||||
|       client->on_fatal_error(); | ||||
|       ESP_LOGW(TAG, "%s (%s): Network down; disconnect", client->get_name(), client->get_peername()); | ||||
|       ESP_LOGW(TAG, "%s (%s): Network down; disconnect", client->client_info_.name.c_str(), | ||||
|                client->client_info_.peername.c_str()); | ||||
|     } | ||||
|     // Continue to process and clean up the clients below | ||||
|   } | ||||
|   | ||||
| @@ -7,24 +7,20 @@ namespace hdc1080 { | ||||
|  | ||||
| static const char *const TAG = "hdc1080"; | ||||
|  | ||||
| static const uint8_t HDC1080_ADDRESS = 0x40;  // 0b1000000 from datasheet | ||||
| static const uint8_t HDC1080_CMD_CONFIGURATION = 0x02; | ||||
| static const uint8_t HDC1080_CMD_TEMPERATURE = 0x00; | ||||
| static const uint8_t HDC1080_CMD_HUMIDITY = 0x01; | ||||
|  | ||||
| void HDC1080Component::setup() { | ||||
|   const uint8_t data[2] = { | ||||
|       0b00000000,  // resolution 14bit for both humidity and temperature | ||||
|       0b00000000   // reserved | ||||
|   }; | ||||
|   const uint8_t config[2] = {0x00, 0x00};  // resolution 14bit for both humidity and temperature | ||||
|  | ||||
|   if (!this->write_bytes(HDC1080_CMD_CONFIGURATION, data, 2)) { | ||||
|     // as instruction is same as powerup defaults (for now), interpret as warning if this fails | ||||
|     ESP_LOGW(TAG, "HDC1080 initial config instruction error"); | ||||
|     this->status_set_warning(); | ||||
|   // if configuration fails - there is a problem | ||||
|   if (this->write_register(HDC1080_CMD_CONFIGURATION, config, 2) != i2c::ERROR_OK) { | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
| } | ||||
|  | ||||
| void HDC1080Component::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, "HDC1080:"); | ||||
|   LOG_I2C_DEVICE(this); | ||||
| @@ -35,39 +31,51 @@ void HDC1080Component::dump_config() { | ||||
|   LOG_SENSOR("  ", "Temperature", this->temperature_); | ||||
|   LOG_SENSOR("  ", "Humidity", this->humidity_); | ||||
| } | ||||
|  | ||||
| void HDC1080Component::update() { | ||||
|   uint16_t raw_temp; | ||||
|   // regardless of what sensor/s are defined in yaml configuration | ||||
|   // the hdc1080 setup configuration used, requires both temperature and humidity to be read | ||||
|  | ||||
|   this->status_clear_warning(); | ||||
|  | ||||
|   if (this->write(&HDC1080_CMD_TEMPERATURE, 1) != i2c::ERROR_OK) { | ||||
|     this->status_set_warning(); | ||||
|     return; | ||||
|   } | ||||
|   delay(20); | ||||
|   if (this->read(reinterpret_cast<uint8_t *>(&raw_temp), 2) != i2c::ERROR_OK) { | ||||
|  | ||||
|   this->set_timeout(20, [this]() { | ||||
|     uint16_t raw_temperature; | ||||
|     if (this->read(reinterpret_cast<uint8_t *>(&raw_temperature), 2) != i2c::ERROR_OK) { | ||||
|       this->status_set_warning(); | ||||
|       return; | ||||
|     } | ||||
|   raw_temp = i2c::i2ctohs(raw_temp); | ||||
|   float temp = raw_temp * 0.0025177f - 40.0f;  // raw * 2^-16 * 165 - 40 | ||||
|   this->temperature_->publish_state(temp); | ||||
|  | ||||
|   uint16_t raw_humidity; | ||||
|     if (this->temperature_ != nullptr) { | ||||
|       raw_temperature = i2c::i2ctohs(raw_temperature); | ||||
|       float temperature = raw_temperature * 0.0025177f - 40.0f;  // raw * 2^-16 * 165 - 40 | ||||
|       this->temperature_->publish_state(temperature); | ||||
|     } | ||||
|  | ||||
|     if (this->write(&HDC1080_CMD_HUMIDITY, 1) != i2c::ERROR_OK) { | ||||
|       this->status_set_warning(); | ||||
|       return; | ||||
|     } | ||||
|   delay(20); | ||||
|  | ||||
|     this->set_timeout(20, [this]() { | ||||
|       uint16_t raw_humidity; | ||||
|       if (this->read(reinterpret_cast<uint8_t *>(&raw_humidity), 2) != i2c::ERROR_OK) { | ||||
|         this->status_set_warning(); | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       if (this->humidity_ != nullptr) { | ||||
|         raw_humidity = i2c::i2ctohs(raw_humidity); | ||||
|         float humidity = raw_humidity * 0.001525879f;  // raw * 2^-16 * 100 | ||||
|         this->humidity_->publish_state(humidity); | ||||
|  | ||||
|   ESP_LOGD(TAG, "Got temperature=%.1f°C humidity=%.1f%%", temp, humidity); | ||||
|   this->status_clear_warning(); | ||||
|       } | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
| float HDC1080Component::get_setup_priority() const { return setup_priority::DATA; } | ||||
|  | ||||
| }  // namespace hdc1080 | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -12,13 +12,11 @@ class HDC1080Component : public PollingComponent, public i2c::I2CDevice { | ||||
|   void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } | ||||
|   void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } | ||||
|  | ||||
|   /// Setup the sensor and check for connection. | ||||
|   void setup() override; | ||||
|   void dump_config() override; | ||||
|   /// Retrieve the latest sensor values. This operation takes approximately 16ms. | ||||
|   void update() override; | ||||
|  | ||||
|   float get_setup_priority() const override; | ||||
|   float get_setup_priority() const override { return setup_priority::DATA; } | ||||
|  | ||||
|  protected: | ||||
|   sensor::Sensor *temperature_{nullptr}; | ||||
|   | ||||
| @@ -62,6 +62,11 @@ SPIRAM_SPEEDS = { | ||||
| } | ||||
|  | ||||
|  | ||||
| def supported() -> bool: | ||||
|     variant = get_esp32_variant() | ||||
|     return variant in SPIRAM_MODES | ||||
|  | ||||
|  | ||||
| def validate_psram_mode(config): | ||||
|     esp32_config = fv.full_config.get()[PLATFORM_ESP32] | ||||
|     if config[CONF_SPEED] == "120MHZ": | ||||
| @@ -95,7 +100,7 @@ def get_config_schema(config): | ||||
|     variant = get_esp32_variant() | ||||
|     speeds = [f"{s}MHZ" for s in SPIRAM_SPEEDS.get(variant, [])] | ||||
|     if not speeds: | ||||
|         return cv.Invalid("PSRAM is not supported on this chip") | ||||
|         raise cv.Invalid("PSRAM is not supported on this chip") | ||||
|     modes = SPIRAM_MODES[variant] | ||||
|     return cv.Schema( | ||||
|         { | ||||
|   | ||||
| @@ -40,7 +40,13 @@ void RemoteTransmitterComponent::await_target_time_() { | ||||
|   if (this->target_time_ == 0) { | ||||
|     this->target_time_ = current_time; | ||||
|   } else if ((int32_t) (this->target_time_ - current_time) > 0) { | ||||
| #if defined(USE_LIBRETINY) | ||||
|     // busy loop for libretiny is required (see the comment inside micros() in wiring.c) | ||||
|     while ((int32_t) (this->target_time_ - micros()) > 0) | ||||
|       ; | ||||
| #else | ||||
|     delayMicroseconds(this->target_time_ - current_time); | ||||
| #endif | ||||
|   } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,9 +1,10 @@ | ||||
| from ast import literal_eval | ||||
| import logging | ||||
| import math | ||||
| import re | ||||
|  | ||||
| import jinja2 as jinja | ||||
| from jinja2.nativetypes import NativeEnvironment | ||||
| from jinja2.sandbox import SandboxedEnvironment | ||||
|  | ||||
| TemplateError = jinja.TemplateError | ||||
| TemplateSyntaxError = jinja.TemplateSyntaxError | ||||
| @@ -70,7 +71,7 @@ class Jinja: | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, context_vars): | ||||
|         self.env = NativeEnvironment( | ||||
|         self.env = SandboxedEnvironment( | ||||
|             trim_blocks=True, | ||||
|             lstrip_blocks=True, | ||||
|             block_start_string="<%", | ||||
| @@ -90,6 +91,15 @@ class Jinja: | ||||
|             **SAFE_GLOBAL_FUNCTIONS, | ||||
|         } | ||||
|  | ||||
|     def safe_eval(self, expr): | ||||
|         try: | ||||
|             result = literal_eval(expr) | ||||
|             if not isinstance(result, str): | ||||
|                 return result | ||||
|         except (ValueError, SyntaxError, MemoryError, TypeError): | ||||
|             pass | ||||
|         return expr | ||||
|  | ||||
|     def expand(self, content_str): | ||||
|         """ | ||||
|         Renders a string that may contain Jinja expressions or statements | ||||
| @@ -106,7 +116,7 @@ class Jinja: | ||||
|             override_vars = content_str.upvalues | ||||
|         try: | ||||
|             template = self.env.from_string(content_str) | ||||
|             result = template.render(override_vars) | ||||
|             result = self.safe_eval(template.render(override_vars)) | ||||
|             if isinstance(result, Undefined): | ||||
|                 # This happens when the expression is simply an undefined variable. Jinja does not | ||||
|                 # raise an exception, instead we get "Undefined". | ||||
|   | ||||
| @@ -429,8 +429,9 @@ void VoiceAssistant::client_subscription(api::APIConnection *client, bool subscr | ||||
|  | ||||
|   if (this->api_client_ != nullptr) { | ||||
|     ESP_LOGE(TAG, "Multiple API Clients attempting to connect to Voice Assistant"); | ||||
|     ESP_LOGE(TAG, "Current client: %s (%s)", this->api_client_->get_name(), this->api_client_->get_peername()); | ||||
|     ESP_LOGE(TAG, "New client: %s (%s)", client->get_name(), client->get_peername()); | ||||
|     ESP_LOGE(TAG, "Current client: %s (%s)", this->api_client_->get_name().c_str(), | ||||
|              this->api_client_->get_peername().c_str()); | ||||
|     ESP_LOGE(TAG, "New client: %s (%s)", client->get_name().c_str(), client->get_peername().c_str()); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -829,15 +829,28 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa | ||||
|     } | ||||
|  | ||||
|     auto call = obj->make_call(); | ||||
|     if (match.method_equals("open")) { | ||||
|       call.set_command_open(); | ||||
|     } else if (match.method_equals("close")) { | ||||
|       call.set_command_close(); | ||||
|     } else if (match.method_equals("stop")) { | ||||
|       call.set_command_stop(); | ||||
|     } else if (match.method_equals("toggle")) { | ||||
|       call.set_command_toggle(); | ||||
|     } else if (!match.method_equals("set")) { | ||||
|  | ||||
|     // Lookup table for cover methods | ||||
|     static const struct { | ||||
|       const char *name; | ||||
|       cover::CoverCall &(cover::CoverCall::*action)(); | ||||
|     } METHODS[] = { | ||||
|         {"open", &cover::CoverCall::set_command_open}, | ||||
|         {"close", &cover::CoverCall::set_command_close}, | ||||
|         {"stop", &cover::CoverCall::set_command_stop}, | ||||
|         {"toggle", &cover::CoverCall::set_command_toggle}, | ||||
|     }; | ||||
|  | ||||
|     bool found = false; | ||||
|     for (const auto &method : METHODS) { | ||||
|       if (match.method_equals(method.name)) { | ||||
|         (call.*method.action)(); | ||||
|         found = true; | ||||
|         break; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     if (!found && !match.method_equals("set")) { | ||||
|       request->send(404); | ||||
|       return; | ||||
|     } | ||||
| @@ -1483,15 +1496,28 @@ void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMa | ||||
|     } | ||||
|  | ||||
|     auto call = obj->make_call(); | ||||
|     if (match.method_equals("open")) { | ||||
|       call.set_command_open(); | ||||
|     } else if (match.method_equals("close")) { | ||||
|       call.set_command_close(); | ||||
|     } else if (match.method_equals("stop")) { | ||||
|       call.set_command_stop(); | ||||
|     } else if (match.method_equals("toggle")) { | ||||
|       call.set_command_toggle(); | ||||
|     } else if (!match.method_equals("set")) { | ||||
|  | ||||
|     // Lookup table for valve methods | ||||
|     static const struct { | ||||
|       const char *name; | ||||
|       valve::ValveCall &(valve::ValveCall::*action)(); | ||||
|     } METHODS[] = { | ||||
|         {"open", &valve::ValveCall::set_command_open}, | ||||
|         {"close", &valve::ValveCall::set_command_close}, | ||||
|         {"stop", &valve::ValveCall::set_command_stop}, | ||||
|         {"toggle", &valve::ValveCall::set_command_toggle}, | ||||
|     }; | ||||
|  | ||||
|     bool found = false; | ||||
|     for (const auto &method : METHODS) { | ||||
|       if (match.method_equals(method.name)) { | ||||
|         (call.*method.action)(); | ||||
|         found = true; | ||||
|         break; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     if (!found && !match.method_equals("set")) { | ||||
|       request->send(404); | ||||
|       return; | ||||
|     } | ||||
| @@ -1555,17 +1581,28 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques | ||||
|     auto call = obj->make_call(); | ||||
|     parse_string_param_(request, "code", call, &decltype(call)::set_code); | ||||
|  | ||||
|     if (match.method_equals("disarm")) { | ||||
|       call.disarm(); | ||||
|     } else if (match.method_equals("arm_away")) { | ||||
|       call.arm_away(); | ||||
|     } else if (match.method_equals("arm_home")) { | ||||
|       call.arm_home(); | ||||
|     } else if (match.method_equals("arm_night")) { | ||||
|       call.arm_night(); | ||||
|     } else if (match.method_equals("arm_vacation")) { | ||||
|       call.arm_vacation(); | ||||
|     } else { | ||||
|     // Lookup table for alarm control panel methods | ||||
|     static const struct { | ||||
|       const char *name; | ||||
|       alarm_control_panel::AlarmControlPanelCall &(alarm_control_panel::AlarmControlPanelCall::*action)(); | ||||
|     } METHODS[] = { | ||||
|         {"disarm", &alarm_control_panel::AlarmControlPanelCall::disarm}, | ||||
|         {"arm_away", &alarm_control_panel::AlarmControlPanelCall::arm_away}, | ||||
|         {"arm_home", &alarm_control_panel::AlarmControlPanelCall::arm_home}, | ||||
|         {"arm_night", &alarm_control_panel::AlarmControlPanelCall::arm_night}, | ||||
|         {"arm_vacation", &alarm_control_panel::AlarmControlPanelCall::arm_vacation}, | ||||
|     }; | ||||
|  | ||||
|     bool found = false; | ||||
|     for (const auto &method : METHODS) { | ||||
|       if (match.method_equals(method.name)) { | ||||
|         (call.*method.action)(); | ||||
|         found = true; | ||||
|         break; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     if (!found) { | ||||
|       request->send(404); | ||||
|       return; | ||||
|     } | ||||
| @@ -1731,24 +1768,24 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) const { | ||||
|   const auto &url = request->url(); | ||||
|   const auto method = request->method(); | ||||
|  | ||||
|   // Simple URL checks | ||||
|   if (url == "/") | ||||
|     return true; | ||||
|  | ||||
|   // Static URL checks | ||||
|   static const char *const STATIC_URLS[] = { | ||||
|       "/", | ||||
| #ifdef USE_ARDUINO | ||||
|   if (url == "/events") | ||||
|     return true; | ||||
|       "/events", | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_WEBSERVER_CSS_INCLUDE | ||||
|   if (url == "/0.css") | ||||
|     return true; | ||||
|       "/0.css", | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_WEBSERVER_JS_INCLUDE | ||||
|   if (url == "/0.js") | ||||
|     return true; | ||||
|       "/0.js", | ||||
| #endif | ||||
|   }; | ||||
|  | ||||
|   for (const auto &static_url : STATIC_URLS) { | ||||
|     if (url == static_url) | ||||
|       return true; | ||||
|   } | ||||
|  | ||||
| #ifdef USE_WEBSERVER_PRIVATE_NETWORK_ACCESS | ||||
|   if (method == HTTP_OPTIONS && request->hasHeader(HEADER_CORS_REQ_PNA)) | ||||
| @@ -1768,92 +1805,87 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) const { | ||||
|   if (!is_get_or_post) | ||||
|     return false; | ||||
|  | ||||
|   // GET-only components | ||||
|   if (is_get) { | ||||
|   // Use lookup tables for domain checks | ||||
|   static const char *const GET_ONLY_DOMAINS[] = { | ||||
| #ifdef USE_SENSOR | ||||
|     if (match.domain_equals("sensor")) | ||||
|       return true; | ||||
|       "sensor", | ||||
| #endif | ||||
| #ifdef USE_BINARY_SENSOR | ||||
|     if (match.domain_equals("binary_sensor")) | ||||
|       return true; | ||||
|       "binary_sensor", | ||||
| #endif | ||||
| #ifdef USE_TEXT_SENSOR | ||||
|     if (match.domain_equals("text_sensor")) | ||||
|       return true; | ||||
|       "text_sensor", | ||||
| #endif | ||||
| #ifdef USE_EVENT | ||||
|     if (match.domain_equals("event")) | ||||
|       return true; | ||||
|       "event", | ||||
| #endif | ||||
|   } | ||||
|   }; | ||||
|  | ||||
|   // GET+POST components | ||||
|   if (is_get_or_post) { | ||||
|   static const char *const GET_POST_DOMAINS[] = { | ||||
| #ifdef USE_SWITCH | ||||
|     if (match.domain_equals("switch")) | ||||
|       return true; | ||||
|       "switch", | ||||
| #endif | ||||
| #ifdef USE_BUTTON | ||||
|     if (match.domain_equals("button")) | ||||
|       return true; | ||||
|       "button", | ||||
| #endif | ||||
| #ifdef USE_FAN | ||||
|     if (match.domain_equals("fan")) | ||||
|       return true; | ||||
|       "fan", | ||||
| #endif | ||||
| #ifdef USE_LIGHT | ||||
|     if (match.domain_equals("light")) | ||||
|       return true; | ||||
|       "light", | ||||
| #endif | ||||
| #ifdef USE_COVER | ||||
|     if (match.domain_equals("cover")) | ||||
|       return true; | ||||
|       "cover", | ||||
| #endif | ||||
| #ifdef USE_NUMBER | ||||
|     if (match.domain_equals("number")) | ||||
|       return true; | ||||
|       "number", | ||||
| #endif | ||||
| #ifdef USE_DATETIME_DATE | ||||
|     if (match.domain_equals("date")) | ||||
|       return true; | ||||
|       "date", | ||||
| #endif | ||||
| #ifdef USE_DATETIME_TIME | ||||
|     if (match.domain_equals("time")) | ||||
|       return true; | ||||
|       "time", | ||||
| #endif | ||||
| #ifdef USE_DATETIME_DATETIME | ||||
|     if (match.domain_equals("datetime")) | ||||
|       return true; | ||||
|       "datetime", | ||||
| #endif | ||||
| #ifdef USE_TEXT | ||||
|     if (match.domain_equals("text")) | ||||
|       return true; | ||||
|       "text", | ||||
| #endif | ||||
| #ifdef USE_SELECT | ||||
|     if (match.domain_equals("select")) | ||||
|       return true; | ||||
|       "select", | ||||
| #endif | ||||
| #ifdef USE_CLIMATE | ||||
|     if (match.domain_equals("climate")) | ||||
|       return true; | ||||
|       "climate", | ||||
| #endif | ||||
| #ifdef USE_LOCK | ||||
|     if (match.domain_equals("lock")) | ||||
|       return true; | ||||
|       "lock", | ||||
| #endif | ||||
| #ifdef USE_VALVE | ||||
|     if (match.domain_equals("valve")) | ||||
|       return true; | ||||
|       "valve", | ||||
| #endif | ||||
| #ifdef USE_ALARM_CONTROL_PANEL | ||||
|     if (match.domain_equals("alarm_control_panel")) | ||||
|       return true; | ||||
|       "alarm_control_panel", | ||||
| #endif | ||||
| #ifdef USE_UPDATE | ||||
|     if (match.domain_equals("update")) | ||||
|       return true; | ||||
|       "update", | ||||
| #endif | ||||
|   }; | ||||
|  | ||||
|   // Check GET-only domains | ||||
|   if (is_get) { | ||||
|     for (const auto &domain : GET_ONLY_DOMAINS) { | ||||
|       if (match.domain_equals(domain)) | ||||
|         return true; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // Check GET+POST domains | ||||
|   if (is_get_or_post) { | ||||
|     for (const auto &domain : GET_POST_DOMAINS) { | ||||
|       if (match.domain_equals(domain)) | ||||
|         return true; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   return false; | ||||
|   | ||||
| @@ -192,7 +192,7 @@ def install_custom_components_meta_finder(): | ||||
|     install_meta_finder(custom_components_dir) | ||||
|  | ||||
|  | ||||
| def _lookup_module(domain, exception): | ||||
| def _lookup_module(domain: str, exception: bool) -> ComponentManifest | None: | ||||
|     if domain in _COMPONENT_CACHE: | ||||
|         return _COMPONENT_CACHE[domain] | ||||
|  | ||||
| @@ -219,16 +219,16 @@ def _lookup_module(domain, exception): | ||||
|     return manif | ||||
|  | ||||
|  | ||||
| def get_component(domain, exception=False): | ||||
| def get_component(domain: str, exception: bool = False) -> ComponentManifest | None: | ||||
|     assert "." not in domain | ||||
|     return _lookup_module(domain, exception) | ||||
|  | ||||
|  | ||||
| def get_platform(domain, platform): | ||||
| def get_platform(domain: str, platform: str) -> ComponentManifest | None: | ||||
|     full = f"{platform}.{domain}" | ||||
|     return _lookup_module(full, False) | ||||
|  | ||||
|  | ||||
| _COMPONENT_CACHE = {} | ||||
| _COMPONENT_CACHE: dict[str, ComponentManifest] = {} | ||||
| CORE_COMPONENTS_PATH = (Path(__file__).parent / "components").resolve() | ||||
| _COMPONENT_CACHE["esphome"] = ComponentManifest(esphome.core.config) | ||||
|   | ||||
							
								
								
									
										194
									
								
								tests/component_tests/psram/test_psram.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										194
									
								
								tests/component_tests/psram/test_psram.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,194 @@ | ||||
| """Tests for PSRAM component.""" | ||||
|  | ||||
| from typing import Any | ||||
|  | ||||
| import pytest | ||||
|  | ||||
| from esphome.components.esp32.const import ( | ||||
|     KEY_VARIANT, | ||||
|     VARIANT_ESP32, | ||||
|     VARIANT_ESP32C2, | ||||
|     VARIANT_ESP32C3, | ||||
|     VARIANT_ESP32C5, | ||||
|     VARIANT_ESP32C6, | ||||
|     VARIANT_ESP32H2, | ||||
|     VARIANT_ESP32P4, | ||||
|     VARIANT_ESP32S2, | ||||
|     VARIANT_ESP32S3, | ||||
| ) | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import CONF_ESPHOME, PlatformFramework | ||||
| from tests.component_tests.types import SetCoreConfigCallable | ||||
|  | ||||
| UNSUPPORTED_PSRAM_VARIANTS = [ | ||||
|     VARIANT_ESP32C2, | ||||
|     VARIANT_ESP32C3, | ||||
|     VARIANT_ESP32C5, | ||||
|     VARIANT_ESP32C6, | ||||
|     VARIANT_ESP32H2, | ||||
| ] | ||||
|  | ||||
| SUPPORTED_PSRAM_VARIANTS = [ | ||||
|     VARIANT_ESP32, | ||||
|     VARIANT_ESP32S2, | ||||
|     VARIANT_ESP32S3, | ||||
|     VARIANT_ESP32P4, | ||||
| ] | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize( | ||||
|     ("config", "error_match"), | ||||
|     [ | ||||
|         pytest.param( | ||||
|             {}, | ||||
|             r"PSRAM is not supported on this chip", | ||||
|             id="psram_not_supported", | ||||
|         ), | ||||
|     ], | ||||
| ) | ||||
| @pytest.mark.parametrize("variant", UNSUPPORTED_PSRAM_VARIANTS) | ||||
| def test_psram_configuration_errors_unsupported_variants( | ||||
|     config: Any, | ||||
|     error_match: str, | ||||
|     variant: str, | ||||
|     set_core_config: SetCoreConfigCallable, | ||||
| ) -> None: | ||||
|     set_core_config( | ||||
|         PlatformFramework.ESP32_IDF, | ||||
|         platform_data={KEY_VARIANT: variant}, | ||||
|         full_config={CONF_ESPHOME: {}}, | ||||
|     ) | ||||
|     """Test detection of invalid PSRAM configuration on unsupported variants.""" | ||||
|     from esphome.components.psram import CONFIG_SCHEMA | ||||
|  | ||||
|     with pytest.raises(cv.Invalid, match=error_match): | ||||
|         CONFIG_SCHEMA(config) | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize("variant", SUPPORTED_PSRAM_VARIANTS) | ||||
| def test_psram_configuration_valid_supported_variants( | ||||
|     variant: str, | ||||
|     set_core_config: SetCoreConfigCallable, | ||||
| ) -> None: | ||||
|     set_core_config( | ||||
|         PlatformFramework.ESP32_IDF, | ||||
|         platform_data={KEY_VARIANT: variant}, | ||||
|         full_config={ | ||||
|             CONF_ESPHOME: {}, | ||||
|             "esp32": { | ||||
|                 "variant": variant, | ||||
|                 "cpu_frequency": "160MHz", | ||||
|                 "framework": {"type": "esp-idf"}, | ||||
|             }, | ||||
|         }, | ||||
|     ) | ||||
|     """Test that PSRAM configuration is valid on supported variants.""" | ||||
|     from esphome.components.psram import CONFIG_SCHEMA, FINAL_VALIDATE_SCHEMA | ||||
|  | ||||
|     # This should not raise an exception | ||||
|     config = CONFIG_SCHEMA({}) | ||||
|     FINAL_VALIDATE_SCHEMA(config) | ||||
|  | ||||
|  | ||||
| def _setup_psram_final_validation_test( | ||||
|     esp32_config: dict, | ||||
|     set_core_config: SetCoreConfigCallable, | ||||
|     set_component_config: Any, | ||||
| ) -> str: | ||||
|     """Helper function to set up ESP32 configuration for PSRAM final validation tests.""" | ||||
|     # Use ESP32S3 for schema validation to allow all options, then override for final validation | ||||
|     schema_variant = "ESP32S3" | ||||
|     final_variant = esp32_config.get("variant", "ESP32S3") | ||||
|     full_esp32_config = { | ||||
|         "variant": final_variant, | ||||
|         "cpu_frequency": esp32_config.get("cpu_frequency", "240MHz"), | ||||
|         "framework": {"type": "esp-idf"}, | ||||
|     } | ||||
|  | ||||
|     set_core_config( | ||||
|         PlatformFramework.ESP32_IDF, | ||||
|         platform_data={KEY_VARIANT: schema_variant}, | ||||
|         full_config={ | ||||
|             CONF_ESPHOME: {}, | ||||
|             "esp32": full_esp32_config, | ||||
|         }, | ||||
|     ) | ||||
|     set_component_config("esp32", full_esp32_config) | ||||
|  | ||||
|     return final_variant | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize( | ||||
|     ("config", "esp32_config", "expect_error", "error_match"), | ||||
|     [ | ||||
|         pytest.param( | ||||
|             {"speed": "120MHz"}, | ||||
|             {"cpu_frequency": "160MHz"}, | ||||
|             True, | ||||
|             r"PSRAM 120MHz requires 240MHz CPU frequency", | ||||
|             id="120mhz_requires_240mhz_cpu", | ||||
|         ), | ||||
|         pytest.param( | ||||
|             {"mode": "octal"}, | ||||
|             {"variant": "ESP32"}, | ||||
|             True, | ||||
|             r"Octal PSRAM is only supported on ESP32-S3", | ||||
|             id="octal_mode_only_esp32s3", | ||||
|         ), | ||||
|         pytest.param( | ||||
|             {"mode": "quad", "enable_ecc": True}, | ||||
|             {}, | ||||
|             True, | ||||
|             r"ECC is only available in octal mode", | ||||
|             id="ecc_only_in_octal_mode", | ||||
|         ), | ||||
|         pytest.param( | ||||
|             {"speed": "120MHZ"}, | ||||
|             {"cpu_frequency": "240MHZ"}, | ||||
|             False, | ||||
|             None, | ||||
|             id="120mhz_with_240mhz_cpu", | ||||
|         ), | ||||
|         pytest.param( | ||||
|             {"mode": "octal"}, | ||||
|             {"variant": "ESP32S3"}, | ||||
|             False, | ||||
|             None, | ||||
|             id="octal_mode_on_esp32s3", | ||||
|         ), | ||||
|         pytest.param( | ||||
|             {"mode": "octal", "enable_ecc": True}, | ||||
|             {"variant": "ESP32S3"}, | ||||
|             False, | ||||
|             None, | ||||
|             id="ecc_in_octal_mode", | ||||
|         ), | ||||
|     ], | ||||
| ) | ||||
| def test_psram_final_validation( | ||||
|     config: Any, | ||||
|     esp32_config: dict, | ||||
|     expect_error: bool, | ||||
|     error_match: str | None, | ||||
|     set_core_config: SetCoreConfigCallable, | ||||
|     set_component_config: Any, | ||||
| ) -> None: | ||||
|     """Test PSRAM final validation for both error and valid cases.""" | ||||
|     from esphome.components.psram import CONFIG_SCHEMA, FINAL_VALIDATE_SCHEMA | ||||
|     from esphome.core import CORE | ||||
|  | ||||
|     final_variant = _setup_psram_final_validation_test( | ||||
|         esp32_config, set_core_config, set_component_config | ||||
|     ) | ||||
|  | ||||
|     validated_config = CONFIG_SCHEMA(config) | ||||
|  | ||||
|     # Update CORE variant for final validation | ||||
|     CORE.data["esp32"][KEY_VARIANT] = final_variant | ||||
|  | ||||
|     if expect_error: | ||||
|         with pytest.raises(cv.Invalid, match=error_match): | ||||
|             FINAL_VALIDATE_SCHEMA(validated_config) | ||||
|     else: | ||||
|         # This should not raise an exception | ||||
|         FINAL_VALIDATE_SCHEMA(validated_config) | ||||
| @@ -5,6 +5,9 @@ substitutions: | ||||
|   var21: '79' | ||||
|   value: 33 | ||||
|   values: 44 | ||||
|   position: | ||||
|     x: 79 | ||||
|     y: 82 | ||||
|  | ||||
| esphome: | ||||
|   name: test | ||||
| @@ -26,3 +29,7 @@ test_list: | ||||
|   - Literal $values ${are not substituted} | ||||
|   - ["list $value", "${is not}", "${substituted}"] | ||||
|   - {"$dictionary": "$value", "${is not}": "${substituted}"} | ||||
|   - |- | ||||
|     {{{ "x", "79"}, { "y", "82"}}} | ||||
|   - '{{{"AA"}}}' | ||||
|   - '"HELLO"' | ||||
|   | ||||
| @@ -8,6 +8,9 @@ substitutions: | ||||
|   var21: "79" | ||||
|   value: 33 | ||||
|   values: 44 | ||||
|   position: | ||||
|     x: 79 | ||||
|     y: 82 | ||||
|  | ||||
| test_list: | ||||
|   - "$var1" | ||||
| @@ -27,3 +30,7 @@ test_list: | ||||
|   - !literal Literal $values ${are not substituted} | ||||
|   - !literal ["list $value", "${is not}", "${substituted}"] | ||||
|   - !literal {"$dictionary": "$value", "${is not}": "${substituted}"} | ||||
|   - |- # Test parsing things that look like a python set of sets when rendered: | ||||
|     {{{ "x", "${ position.x }"}, { "y", "${ position.y }"}}} | ||||
|   - ${ '{{{"AA"}}}' } | ||||
|   - ${ '"HELLO"' } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user