mirror of
				https://github.com/esphome/esphome.git
				synced 2025-11-04 00:51:49 +00:00 
			
		
		
		
	Compare commits
	
		
			58 Commits
		
	
	
		
			2025.9.0b1
			...
			2025.9.3
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					93e18e850e | ||
| 
						 | 
					59c0ffb98b | ||
| 
						 | 
					29658b79bc | ||
| 
						 | 
					158a59aa83 | ||
| 
						 | 
					c95180504a | ||
| 
						 | 
					a96c013eb1 | ||
| 
						 | 
					58166b3e71 | ||
| 
						 | 
					345fc0b6ca | ||
| 
						 | 
					127058e700 | ||
| 
						 | 
					57f7a709cf | ||
| 
						 | 
					f2a9e9265e | ||
| 
						 | 
					1ecd26adb5 | ||
| 
						 | 
					6d9fc672d5 | ||
| 
						 | 
					b9361b0868 | ||
| 
						 | 
					e47f4ef602 | ||
| 
						 | 
					961be7fd12 | ||
| 
						 | 
					a5a21f47d1 | ||
| 
						 | 
					a06cd84974 | ||
| 
						 | 
					e3703b43c1 | ||
| 
						 | 
					f6dc25c0ce | ||
| 
						 | 
					d2df232706 | ||
| 
						 | 
					404e679e66 | ||
| 
						 | 
					8d401ad05a | ||
| 
						 | 
					e542816f7d | ||
| 
						 | 
					12cadf0a04 | ||
| 
						 | 
					adc3d3127d | ||
| 
						 | 
					61ab682099 | ||
| 
						 | 
					c05b7cca5e | ||
| 
						 | 
					6ac395da6d | ||
| 
						 | 
					54616ae1b4 | ||
| 
						 | 
					e33dcda907 | ||
| 
						 | 
					04c1b90e57 | ||
| 
						 | 
					ddb8fedef7 | ||
| 
						 | 
					04f4f79cb4 | ||
| 
						 | 
					8890071360 | ||
| 
						 | 
					4b3a997a8e | ||
| 
						 | 
					2a4ab6a811 | ||
| 
						 | 
					971de64494 | ||
| 
						 | 
					926fdcbecd | ||
| 
						 | 
					6b147312cd | ||
| 
						 | 
					2d9152d9b9 | ||
| 
						 | 
					24f9550ce5 | ||
| 
						 | 
					3427aaab8c | ||
| 
						 | 
					4e17d14acc | ||
| 
						 | 
					1750f02ef3 | ||
| 
						 | 
					ae158179bd | ||
| 
						 | 
					c601494779 | ||
| 
						 | 
					646f4e66be | ||
| 
						 | 
					5b5e5c213c | ||
| 
						 | 
					46235684b1 | ||
| 
						 | 
					5b702a1efa | ||
| 
						 | 
					56e9fd2e38 | ||
| 
						 | 
					65f15a706f | ||
| 
						 | 
					eee64cc3a6 | ||
| 
						 | 
					f43fb3c3a3 | ||
| 
						 | 
					79b0025fe6 | ||
| 
						 | 
					c6a039a72f | ||
| 
						 | 
					6f1fa094c2 | 
							
								
								
									
										2
									
								
								Doxyfile
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								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.0b1
 | 
			
		||||
PROJECT_NUMBER         = 2025.9.3
 | 
			
		||||
 | 
			
		||||
# 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
 | 
			
		||||
 
 | 
			
		||||
@@ -15,9 +15,11 @@ import argcomplete
 | 
			
		||||
 | 
			
		||||
from esphome import const, writer, yaml_util
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components.mqtt import CONF_DISCOVER_IP
 | 
			
		||||
from esphome.config import iter_component_configs, read_config, strip_default_ids
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    ALLOWED_NAME_CHARS,
 | 
			
		||||
    CONF_API,
 | 
			
		||||
    CONF_BAUD_RATE,
 | 
			
		||||
    CONF_BROKER,
 | 
			
		||||
    CONF_DEASSERT_RTS_DTR,
 | 
			
		||||
@@ -43,6 +45,7 @@ from esphome.const import (
 | 
			
		||||
    SECRETS_FILES,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import CORE, EsphomeError, coroutine
 | 
			
		||||
from esphome.enum import StrEnum
 | 
			
		||||
from esphome.helpers import get_bool_env, indent, is_ip_address
 | 
			
		||||
from esphome.log import AnsiFore, color, setup_log
 | 
			
		||||
from esphome.types import ConfigType
 | 
			
		||||
@@ -106,13 +109,15 @@ def choose_prompt(options, purpose: str = None):
 | 
			
		||||
    return options[opt - 1][1]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Purpose(StrEnum):
 | 
			
		||||
    UPLOADING = "uploading"
 | 
			
		||||
    LOGGING = "logging"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def choose_upload_log_host(
 | 
			
		||||
    default: list[str] | str | None,
 | 
			
		||||
    check_default: str | None,
 | 
			
		||||
    show_ota: bool,
 | 
			
		||||
    show_mqtt: bool,
 | 
			
		||||
    show_api: bool,
 | 
			
		||||
    purpose: str | None = None,
 | 
			
		||||
    purpose: Purpose,
 | 
			
		||||
) -> list[str]:
 | 
			
		||||
    # Convert to list for uniform handling
 | 
			
		||||
    defaults = [default] if isinstance(default, str) else default or []
 | 
			
		||||
@@ -132,13 +137,30 @@ def choose_upload_log_host(
 | 
			
		||||
                ]
 | 
			
		||||
                resolved.append(choose_prompt(options, purpose=purpose))
 | 
			
		||||
            elif device == "OTA":
 | 
			
		||||
                if CORE.address and (
 | 
			
		||||
                    (show_ota and "ota" in CORE.config)
 | 
			
		||||
                    or (show_api and "api" in CORE.config)
 | 
			
		||||
                # ensure IP adresses are used first
 | 
			
		||||
                if is_ip_address(CORE.address) and (
 | 
			
		||||
                    (purpose == Purpose.LOGGING and has_api())
 | 
			
		||||
                    or (purpose == Purpose.UPLOADING and has_ota())
 | 
			
		||||
                ):
 | 
			
		||||
                    resolved.append(CORE.address)
 | 
			
		||||
                elif show_mqtt and has_mqtt_logging():
 | 
			
		||||
                    resolved.append("MQTT")
 | 
			
		||||
 | 
			
		||||
                if purpose == Purpose.LOGGING:
 | 
			
		||||
                    if has_api() and has_mqtt_ip_lookup():
 | 
			
		||||
                        resolved.append("MQTTIP")
 | 
			
		||||
 | 
			
		||||
                    if has_mqtt_logging():
 | 
			
		||||
                        resolved.append("MQTT")
 | 
			
		||||
 | 
			
		||||
                    if has_api() and has_non_ip_address():
 | 
			
		||||
                        resolved.append(CORE.address)
 | 
			
		||||
 | 
			
		||||
                elif purpose == Purpose.UPLOADING:
 | 
			
		||||
                    if has_ota() and has_mqtt_ip_lookup():
 | 
			
		||||
                        resolved.append("MQTTIP")
 | 
			
		||||
 | 
			
		||||
                    if has_ota() and has_non_ip_address():
 | 
			
		||||
                        resolved.append(CORE.address)
 | 
			
		||||
 | 
			
		||||
            else:
 | 
			
		||||
                resolved.append(device)
 | 
			
		||||
        if not resolved:
 | 
			
		||||
@@ -149,39 +171,111 @@ def choose_upload_log_host(
 | 
			
		||||
    options = [
 | 
			
		||||
        (f"{port.path} ({port.description})", port.path) for port in get_serial_ports()
 | 
			
		||||
    ]
 | 
			
		||||
    if (show_ota and "ota" in CORE.config) or (show_api and "api" in CORE.config):
 | 
			
		||||
        options.append((f"Over The Air ({CORE.address})", CORE.address))
 | 
			
		||||
    if show_mqtt and has_mqtt_logging():
 | 
			
		||||
        mqtt_config = CORE.config[CONF_MQTT]
 | 
			
		||||
        options.append((f"MQTT ({mqtt_config[CONF_BROKER]})", "MQTT"))
 | 
			
		||||
 | 
			
		||||
    if purpose == Purpose.LOGGING:
 | 
			
		||||
        if has_mqtt_logging():
 | 
			
		||||
            mqtt_config = CORE.config[CONF_MQTT]
 | 
			
		||||
            options.append((f"MQTT ({mqtt_config[CONF_BROKER]})", "MQTT"))
 | 
			
		||||
 | 
			
		||||
        if has_api():
 | 
			
		||||
            if has_resolvable_address():
 | 
			
		||||
                options.append((f"Over The Air ({CORE.address})", CORE.address))
 | 
			
		||||
            if has_mqtt_ip_lookup():
 | 
			
		||||
                options.append(("Over The Air (MQTT IP lookup)", "MQTTIP"))
 | 
			
		||||
 | 
			
		||||
    elif purpose == Purpose.UPLOADING and has_ota():
 | 
			
		||||
        if has_resolvable_address():
 | 
			
		||||
            options.append((f"Over The Air ({CORE.address})", CORE.address))
 | 
			
		||||
        if has_mqtt_ip_lookup():
 | 
			
		||||
            options.append(("Over The Air (MQTT IP lookup)", "MQTTIP"))
 | 
			
		||||
 | 
			
		||||
    if check_default is not None and check_default in [opt[1] for opt in options]:
 | 
			
		||||
        return [check_default]
 | 
			
		||||
    return [choose_prompt(options, purpose=purpose)]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def mqtt_logging_enabled(mqtt_config):
 | 
			
		||||
def has_mqtt_logging() -> bool:
 | 
			
		||||
    """Check if MQTT logging is available."""
 | 
			
		||||
    if CONF_MQTT not in CORE.config:
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
    mqtt_config = CORE.config[CONF_MQTT]
 | 
			
		||||
 | 
			
		||||
    # enabled by default
 | 
			
		||||
    if CONF_LOG_TOPIC not in mqtt_config:
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
    log_topic = mqtt_config[CONF_LOG_TOPIC]
 | 
			
		||||
    if log_topic is None:
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
    if CONF_TOPIC not in log_topic:
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
    return log_topic.get(CONF_LEVEL, None) != "NONE"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def has_mqtt_logging() -> bool:
 | 
			
		||||
    """Check if MQTT logging is available."""
 | 
			
		||||
    return (mqtt_config := CORE.config.get(CONF_MQTT)) and mqtt_logging_enabled(
 | 
			
		||||
        mqtt_config
 | 
			
		||||
    )
 | 
			
		||||
def has_mqtt() -> bool:
 | 
			
		||||
    """Check if MQTT is available."""
 | 
			
		||||
    return CONF_MQTT in CORE.config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def has_api() -> bool:
 | 
			
		||||
    """Check if API is available."""
 | 
			
		||||
    return CONF_API in CORE.config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def has_ota() -> bool:
 | 
			
		||||
    """Check if OTA is available."""
 | 
			
		||||
    return CONF_OTA in CORE.config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def has_mqtt_ip_lookup() -> bool:
 | 
			
		||||
    """Check if MQTT is available and IP lookup is supported."""
 | 
			
		||||
    if CONF_MQTT not in CORE.config:
 | 
			
		||||
        return False
 | 
			
		||||
    # Default Enabled
 | 
			
		||||
    if CONF_DISCOVER_IP not in CORE.config[CONF_MQTT]:
 | 
			
		||||
        return True
 | 
			
		||||
    return CORE.config[CONF_MQTT][CONF_DISCOVER_IP]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def has_mdns() -> bool:
 | 
			
		||||
    """Check if MDNS is available."""
 | 
			
		||||
    return CONF_MDNS not in CORE.config or not CORE.config[CONF_MDNS][CONF_DISABLED]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def has_non_ip_address() -> bool:
 | 
			
		||||
    """Check if CORE.address is set and is not an IP address."""
 | 
			
		||||
    return CORE.address is not None and not is_ip_address(CORE.address)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def has_ip_address() -> bool:
 | 
			
		||||
    """Check if CORE.address is a valid IP address."""
 | 
			
		||||
    return CORE.address is not None and is_ip_address(CORE.address)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def has_resolvable_address() -> bool:
 | 
			
		||||
    """Check if CORE.address is resolvable (via mDNS or is an IP address)."""
 | 
			
		||||
    return has_mdns() or has_ip_address()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def mqtt_get_ip(config: ConfigType, username: str, password: str, client_id: str):
 | 
			
		||||
    from esphome import mqtt
 | 
			
		||||
 | 
			
		||||
    return mqtt.get_esphome_device_ip(config, username, password, client_id)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
_PORT_TO_PORT_TYPE = {
 | 
			
		||||
    "MQTT": "MQTT",
 | 
			
		||||
    "MQTTIP": "MQTTIP",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_port_type(port: str) -> str:
 | 
			
		||||
    if port.startswith("/") or port.startswith("COM"):
 | 
			
		||||
        return "SERIAL"
 | 
			
		||||
    if port == "MQTT":
 | 
			
		||||
        return "MQTT"
 | 
			
		||||
    return "NETWORK"
 | 
			
		||||
    return _PORT_TO_PORT_TYPE.get(port, "NETWORK")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def run_miniterm(config: ConfigType, port: str, args) -> int:
 | 
			
		||||
@@ -226,7 +320,9 @@ def run_miniterm(config: ConfigType, port: str, args) -> int:
 | 
			
		||||
                        .replace(b"\n", b"")
 | 
			
		||||
                        .decode("utf8", "backslashreplace")
 | 
			
		||||
                    )
 | 
			
		||||
                    time_str = datetime.now().time().strftime("[%H:%M:%S]")
 | 
			
		||||
                    time_ = datetime.now()
 | 
			
		||||
                    nanoseconds = time_.microsecond // 1000
 | 
			
		||||
                    time_str = f"[{time_.hour:02}:{time_.minute:02}:{time_.second:02}.{nanoseconds:03}]"
 | 
			
		||||
                    safe_print(parser.parse_line(line, time_str))
 | 
			
		||||
 | 
			
		||||
                    backtrace_state = platformio_api.process_stacktrace(
 | 
			
		||||
@@ -437,23 +533,9 @@ def upload_program(
 | 
			
		||||
    password = ota_conf.get(CONF_PASSWORD, "")
 | 
			
		||||
    binary = args.file if getattr(args, "file", None) is not None else CORE.firmware_bin
 | 
			
		||||
 | 
			
		||||
    # Check if we should use MQTT for address resolution
 | 
			
		||||
    # This happens when no device was specified, or the current host is "MQTT"/"OTA"
 | 
			
		||||
    if (
 | 
			
		||||
        CONF_MQTT in config  # pylint: disable=too-many-boolean-expressions
 | 
			
		||||
        and (not devices or host in ("MQTT", "OTA"))
 | 
			
		||||
        and (
 | 
			
		||||
            ((config[CONF_MDNS][CONF_DISABLED]) and not is_ip_address(CORE.address))
 | 
			
		||||
            or get_port_type(host) == "MQTT"
 | 
			
		||||
        )
 | 
			
		||||
    ):
 | 
			
		||||
        from esphome import mqtt
 | 
			
		||||
 | 
			
		||||
        devices = [
 | 
			
		||||
            mqtt.get_esphome_device_ip(
 | 
			
		||||
                config, args.username, args.password, args.client_id
 | 
			
		||||
            )
 | 
			
		||||
        ]
 | 
			
		||||
    # MQTT address resolution
 | 
			
		||||
    if get_port_type(host) in ("MQTT", "MQTTIP"):
 | 
			
		||||
        devices = mqtt_get_ip(config, args.username, args.password, args.client_id)
 | 
			
		||||
 | 
			
		||||
    return espota2.run_ota(devices, remote_port, password, binary)
 | 
			
		||||
 | 
			
		||||
@@ -474,20 +556,28 @@ def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int
 | 
			
		||||
    if get_port_type(port) == "SERIAL":
 | 
			
		||||
        check_permissions(port)
 | 
			
		||||
        return run_miniterm(config, port, args)
 | 
			
		||||
    if get_port_type(port) == "NETWORK" and "api" in config:
 | 
			
		||||
        addresses_to_use = devices
 | 
			
		||||
        if config[CONF_MDNS][CONF_DISABLED] and CONF_MQTT in config:
 | 
			
		||||
            from esphome import mqtt
 | 
			
		||||
 | 
			
		||||
            mqtt_address = mqtt.get_esphome_device_ip(
 | 
			
		||||
    port_type = get_port_type(port)
 | 
			
		||||
 | 
			
		||||
    # Check if we should use API for logging
 | 
			
		||||
    if has_api():
 | 
			
		||||
        addresses_to_use: list[str] | None = None
 | 
			
		||||
 | 
			
		||||
        if port_type == "NETWORK" and (has_mdns() or is_ip_address(port)):
 | 
			
		||||
            addresses_to_use = devices
 | 
			
		||||
        elif port_type in ("NETWORK", "MQTT", "MQTTIP") and has_mqtt_ip_lookup():
 | 
			
		||||
            # Only use MQTT IP lookup if the first condition didn't match
 | 
			
		||||
            # (for MQTT/MQTTIP types, or for NETWORK when mdns/ip check fails)
 | 
			
		||||
            addresses_to_use = mqtt_get_ip(
 | 
			
		||||
                config, args.username, args.password, args.client_id
 | 
			
		||||
            )[0]
 | 
			
		||||
            addresses_to_use = [mqtt_address]
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        from esphome.components.api.client import run_logs
 | 
			
		||||
        if addresses_to_use is not None:
 | 
			
		||||
            from esphome.components.api.client import run_logs
 | 
			
		||||
 | 
			
		||||
        return run_logs(config, addresses_to_use)
 | 
			
		||||
    if get_port_type(port) in ("NETWORK", "MQTT") and "mqtt" in config:
 | 
			
		||||
            return run_logs(config, addresses_to_use)
 | 
			
		||||
 | 
			
		||||
    if port_type in ("NETWORK", "MQTT") and has_mqtt_logging():
 | 
			
		||||
        from esphome import mqtt
 | 
			
		||||
 | 
			
		||||
        return mqtt.show_logs(
 | 
			
		||||
@@ -553,10 +643,7 @@ def command_upload(args: ArgsProtocol, config: ConfigType) -> int | None:
 | 
			
		||||
    devices = choose_upload_log_host(
 | 
			
		||||
        default=args.device,
 | 
			
		||||
        check_default=None,
 | 
			
		||||
        show_ota=True,
 | 
			
		||||
        show_mqtt=False,
 | 
			
		||||
        show_api=False,
 | 
			
		||||
        purpose="uploading",
 | 
			
		||||
        purpose=Purpose.UPLOADING,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    exit_code, _ = upload_program(config, args, devices)
 | 
			
		||||
@@ -581,10 +668,7 @@ def command_logs(args: ArgsProtocol, config: ConfigType) -> int | None:
 | 
			
		||||
    devices = choose_upload_log_host(
 | 
			
		||||
        default=args.device,
 | 
			
		||||
        check_default=None,
 | 
			
		||||
        show_ota=False,
 | 
			
		||||
        show_mqtt=True,
 | 
			
		||||
        show_api=True,
 | 
			
		||||
        purpose="logging",
 | 
			
		||||
        purpose=Purpose.LOGGING,
 | 
			
		||||
    )
 | 
			
		||||
    return show_logs(config, args, devices)
 | 
			
		||||
 | 
			
		||||
@@ -610,10 +694,7 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None:
 | 
			
		||||
    devices = choose_upload_log_host(
 | 
			
		||||
        default=args.device,
 | 
			
		||||
        check_default=None,
 | 
			
		||||
        show_ota=True,
 | 
			
		||||
        show_mqtt=False,
 | 
			
		||||
        show_api=True,
 | 
			
		||||
        purpose="uploading",
 | 
			
		||||
        purpose=Purpose.UPLOADING,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    exit_code, successful_device = upload_program(config, args, devices)
 | 
			
		||||
@@ -630,10 +711,7 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None:
 | 
			
		||||
    devices = choose_upload_log_host(
 | 
			
		||||
        default=successful_device,
 | 
			
		||||
        check_default=successful_device,
 | 
			
		||||
        show_ota=False,
 | 
			
		||||
        show_mqtt=True,
 | 
			
		||||
        show_api=True,
 | 
			
		||||
        purpose="logging",
 | 
			
		||||
        purpose=Purpose.LOGGING,
 | 
			
		||||
    )
 | 
			
		||||
    return show_logs(config, args, devices)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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},
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
@@ -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},
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
@@ -113,7 +113,7 @@ void ADE7880::update() {
 | 
			
		||||
  if (this->channel_a_ != nullptr) {
 | 
			
		||||
    auto *chan = this->channel_a_;
 | 
			
		||||
    this->update_sensor_from_s24zp_register16_(chan->current, AIRMS, [](float val) { return val / 100000.0f; });
 | 
			
		||||
    this->update_sensor_from_s24zp_register16_(chan->voltage, BVRMS, [](float val) { return val / 10000.0f; });
 | 
			
		||||
    this->update_sensor_from_s24zp_register16_(chan->voltage, AVRMS, [](float val) { return val / 10000.0f; });
 | 
			
		||||
    this->update_sensor_from_s24zp_register16_(chan->active_power, AWATT, [](float val) { return val / 100.0f; });
 | 
			
		||||
    this->update_sensor_from_s24zp_register16_(chan->apparent_power, AVA, [](float val) { return val / 100.0f; });
 | 
			
		||||
    this->update_sensor_from_s16_register16_(chan->power_factor, APF,
 | 
			
		||||
 
 | 
			
		||||
@@ -193,6 +193,7 @@ async def to_code(config):
 | 
			
		||||
        if key := encryption_config.get(CONF_KEY):
 | 
			
		||||
            decoded = base64.b64decode(key)
 | 
			
		||||
            cg.add(var.set_noise_psk(list(decoded)))
 | 
			
		||||
            cg.add_define("USE_API_NOISE_PSK_FROM_YAML")
 | 
			
		||||
        else:
 | 
			
		||||
            # No key provided, but encryption desired
 | 
			
		||||
            # This will allow a plaintext client to provide a noise key,
 | 
			
		||||
 
 | 
			
		||||
@@ -27,9 +27,6 @@ service APIConnection {
 | 
			
		||||
  rpc subscribe_logs (SubscribeLogsRequest) returns (void) {}
 | 
			
		||||
  rpc subscribe_homeassistant_services (SubscribeHomeassistantServicesRequest) returns (void) {}
 | 
			
		||||
  rpc subscribe_home_assistant_states (SubscribeHomeAssistantStatesRequest) returns (void) {}
 | 
			
		||||
  rpc get_time (GetTimeRequest) returns (GetTimeResponse) {
 | 
			
		||||
    option (needs_authentication) = false;
 | 
			
		||||
  }
 | 
			
		||||
  rpc execute_service (ExecuteServiceRequest) returns (void) {}
 | 
			
		||||
  rpc noise_encryption_set_key (NoiseEncryptionSetKeyRequest) returns (NoiseEncryptionSetKeyResponse) {}
 | 
			
		||||
 | 
			
		||||
@@ -809,12 +806,12 @@ message HomeAssistantStateResponse {
 | 
			
		||||
// ==================== IMPORT TIME ====================
 | 
			
		||||
message GetTimeRequest {
 | 
			
		||||
  option (id) = 36;
 | 
			
		||||
  option (source) = SOURCE_BOTH;
 | 
			
		||||
  option (source) = SOURCE_SERVER;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message GetTimeResponse {
 | 
			
		||||
  option (id) = 37;
 | 
			
		||||
  option (source) = SOURCE_BOTH;
 | 
			
		||||
  option (source) = SOURCE_CLIENT;
 | 
			
		||||
  option (no_delay) = true;
 | 
			
		||||
 | 
			
		||||
  fixed32 epoch_seconds = 1;
 | 
			
		||||
 
 | 
			
		||||
@@ -42,6 +42,8 @@ static constexpr uint8_t MAX_PING_RETRIES = 60;
 | 
			
		||||
static constexpr uint16_t PING_RETRY_INTERVAL = 1000;
 | 
			
		||||
static constexpr uint32_t KEEPALIVE_DISCONNECT_TIMEOUT = (KEEPALIVE_TIMEOUT_MS * 5) / 2;
 | 
			
		||||
 | 
			
		||||
static constexpr auto ESPHOME_VERSION_REF = StringRef::from_lit(ESPHOME_VERSION);
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "api.connection";
 | 
			
		||||
#ifdef USE_CAMERA
 | 
			
		||||
static const int CAMERA_STOP_STREAM = 5000;
 | 
			
		||||
@@ -1081,12 +1083,6 @@ void APIConnection::on_get_time_response(const GetTimeResponse &value) {
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
bool APIConnection::send_get_time_response(const GetTimeRequest &msg) {
 | 
			
		||||
  GetTimeResponse resp;
 | 
			
		||||
  resp.epoch_seconds = ::time(nullptr);
 | 
			
		||||
  return this->send_message(resp, GetTimeResponse::MESSAGE_TYPE);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#ifdef USE_BLUETOOTH_PROXY
 | 
			
		||||
void APIConnection::subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) {
 | 
			
		||||
  bluetooth_proxy::global_bluetooth_proxy->subscribe_api_connection(this, msg.flags);
 | 
			
		||||
@@ -1376,9 +1372,8 @@ bool APIConnection::send_hello_response(const HelloRequest &msg) {
 | 
			
		||||
  HelloResponse resp;
 | 
			
		||||
  resp.api_version_major = 1;
 | 
			
		||||
  resp.api_version_minor = 12;
 | 
			
		||||
  // Temporary string for concatenation - will be valid during send_message call
 | 
			
		||||
  std::string server_info = App.get_name() + " (esphome v" ESPHOME_VERSION ")";
 | 
			
		||||
  resp.set_server_info(StringRef(server_info));
 | 
			
		||||
  // Send only the version string - the client only logs this for debugging and doesn't use it otherwise
 | 
			
		||||
  resp.set_server_info(ESPHOME_VERSION_REF);
 | 
			
		||||
  resp.set_name(StringRef(App.get_name()));
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_PASSWORD
 | 
			
		||||
@@ -1425,8 +1420,6 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
 | 
			
		||||
  std::string mac_address = get_mac_address_pretty();
 | 
			
		||||
  resp.set_mac_address(StringRef(mac_address));
 | 
			
		||||
 | 
			
		||||
  // Compile-time StringRef constants
 | 
			
		||||
  static constexpr auto ESPHOME_VERSION_REF = StringRef::from_lit(ESPHOME_VERSION);
 | 
			
		||||
  resp.set_esphome_version(ESPHOME_VERSION_REF);
 | 
			
		||||
 | 
			
		||||
  resp.set_compilation_time(App.get_compilation_time_ref());
 | 
			
		||||
 
 | 
			
		||||
@@ -219,7 +219,6 @@ class APIConnection final : public APIServerConnection {
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_STATES
 | 
			
		||||
  void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) override;
 | 
			
		||||
#endif
 | 
			
		||||
  bool send_get_time_response(const GetTimeRequest &msg) override;
 | 
			
		||||
#ifdef USE_API_SERVICES
 | 
			
		||||
  void execute_service(const ExecuteServiceRequest &msg) override;
 | 
			
		||||
#endif
 | 
			
		||||
 
 | 
			
		||||
@@ -921,14 +921,6 @@ bool GetTimeResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
 | 
			
		||||
  }
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
void GetTimeResponse::encode(ProtoWriteBuffer buffer) const {
 | 
			
		||||
  buffer.encode_fixed32(1, this->epoch_seconds);
 | 
			
		||||
  buffer.encode_string(2, this->timezone_ref_);
 | 
			
		||||
}
 | 
			
		||||
void GetTimeResponse::calculate_size(ProtoSize &size) const {
 | 
			
		||||
  size.add_fixed32(1, this->epoch_seconds);
 | 
			
		||||
  size.add_length(1, this->timezone_ref_.size());
 | 
			
		||||
}
 | 
			
		||||
#ifdef USE_API_SERVICES
 | 
			
		||||
void ListEntitiesServicesArgument::encode(ProtoWriteBuffer buffer) const {
 | 
			
		||||
  buffer.encode_string(1, this->name_ref_);
 | 
			
		||||
 
 | 
			
		||||
@@ -1180,10 +1180,6 @@ class GetTimeResponse final : public ProtoDecodableMessage {
 | 
			
		||||
#endif
 | 
			
		||||
  uint32_t epoch_seconds{0};
 | 
			
		||||
  std::string timezone{};
 | 
			
		||||
  StringRef timezone_ref_{};
 | 
			
		||||
  void set_timezone(const StringRef &ref) { this->timezone_ref_ = ref; }
 | 
			
		||||
  void encode(ProtoWriteBuffer buffer) const override;
 | 
			
		||||
  void calculate_size(ProtoSize &size) const override;
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  void dump_to(std::string &out) const override;
 | 
			
		||||
#endif
 | 
			
		||||
 
 | 
			
		||||
@@ -1113,13 +1113,7 @@ void GetTimeRequest::dump_to(std::string &out) const { out.append("GetTimeReques
 | 
			
		||||
void GetTimeResponse::dump_to(std::string &out) const {
 | 
			
		||||
  MessageDumpHelper helper(out, "GetTimeResponse");
 | 
			
		||||
  dump_field(out, "epoch_seconds", this->epoch_seconds);
 | 
			
		||||
  out.append("  timezone: ");
 | 
			
		||||
  if (!this->timezone_ref_.empty()) {
 | 
			
		||||
    out.append("'").append(this->timezone_ref_.c_str()).append("'");
 | 
			
		||||
  } else {
 | 
			
		||||
    out.append("'").append(this->timezone).append("'");
 | 
			
		||||
  }
 | 
			
		||||
  out.append("\n");
 | 
			
		||||
  dump_field(out, "timezone", this->timezone);
 | 
			
		||||
}
 | 
			
		||||
#ifdef USE_API_SERVICES
 | 
			
		||||
void ListEntitiesServicesArgument::dump_to(std::string &out) const {
 | 
			
		||||
 
 | 
			
		||||
@@ -160,15 +160,6 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
#endif
 | 
			
		||||
    case GetTimeRequest::MESSAGE_TYPE: {
 | 
			
		||||
      GetTimeRequest msg;
 | 
			
		||||
      // Empty message: no decode needed
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
      ESP_LOGVV(TAG, "on_get_time_request: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
      this->on_get_time_request(msg);
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case GetTimeResponse::MESSAGE_TYPE: {
 | 
			
		||||
      GetTimeResponse msg;
 | 
			
		||||
      msg.decode(msg_data, msg_size);
 | 
			
		||||
@@ -656,11 +647,6 @@ void APIServerConnection::on_subscribe_home_assistant_states_request(const Subsc
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
void APIServerConnection::on_get_time_request(const GetTimeRequest &msg) {
 | 
			
		||||
  if (this->check_connection_setup_() && !this->send_get_time_response(msg)) {
 | 
			
		||||
    this->on_fatal_error();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#ifdef USE_API_SERVICES
 | 
			
		||||
void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest &msg) {
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
 
 | 
			
		||||
@@ -71,7 +71,7 @@ class APIServerConnectionBase : public ProtoService {
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_STATES
 | 
			
		||||
  virtual void on_home_assistant_state_response(const HomeAssistantStateResponse &value){};
 | 
			
		||||
#endif
 | 
			
		||||
  virtual void on_get_time_request(const GetTimeRequest &value){};
 | 
			
		||||
 | 
			
		||||
  virtual void on_get_time_response(const GetTimeResponse &value){};
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_SERVICES
 | 
			
		||||
@@ -226,7 +226,6 @@ class APIServerConnection : public APIServerConnectionBase {
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_STATES
 | 
			
		||||
  virtual void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) = 0;
 | 
			
		||||
#endif
 | 
			
		||||
  virtual bool send_get_time_response(const GetTimeRequest &msg) = 0;
 | 
			
		||||
#ifdef USE_API_SERVICES
 | 
			
		||||
  virtual void execute_service(const ExecuteServiceRequest &msg) = 0;
 | 
			
		||||
#endif
 | 
			
		||||
@@ -348,7 +347,6 @@ class APIServerConnection : public APIServerConnectionBase {
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_STATES
 | 
			
		||||
  void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) override;
 | 
			
		||||
#endif
 | 
			
		||||
  void on_get_time_request(const GetTimeRequest &msg) override;
 | 
			
		||||
#ifdef USE_API_SERVICES
 | 
			
		||||
  void on_execute_service_request(const ExecuteServiceRequest &msg) override;
 | 
			
		||||
#endif
 | 
			
		||||
 
 | 
			
		||||
@@ -37,12 +37,14 @@ void APIServer::setup() {
 | 
			
		||||
 | 
			
		||||
  this->noise_pref_ = global_preferences->make_preference<SavedNoisePsk>(hash, true);
 | 
			
		||||
 | 
			
		||||
#ifndef USE_API_NOISE_PSK_FROM_YAML
 | 
			
		||||
  // Only load saved PSK if not set from YAML
 | 
			
		||||
  SavedNoisePsk noise_pref_saved{};
 | 
			
		||||
  if (this->noise_pref_.load(&noise_pref_saved)) {
 | 
			
		||||
    ESP_LOGD(TAG, "Loaded saved Noise PSK");
 | 
			
		||||
 | 
			
		||||
    this->set_noise_psk(noise_pref_saved.psk);
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  // Schedule reboot if no clients connect within timeout
 | 
			
		||||
@@ -409,6 +411,12 @@ void APIServer::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeo
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_NOISE
 | 
			
		||||
bool APIServer::save_noise_psk(psk_t psk, bool make_active) {
 | 
			
		||||
#ifdef USE_API_NOISE_PSK_FROM_YAML
 | 
			
		||||
  // When PSK is set from YAML, this function should never be called
 | 
			
		||||
  // but if it is, reject the change
 | 
			
		||||
  ESP_LOGW(TAG, "Key set in YAML");
 | 
			
		||||
  return false;
 | 
			
		||||
#else
 | 
			
		||||
  auto &old_psk = this->noise_ctx_->get_psk();
 | 
			
		||||
  if (std::equal(old_psk.begin(), old_psk.end(), psk.begin())) {
 | 
			
		||||
    ESP_LOGW(TAG, "New PSK matches old");
 | 
			
		||||
@@ -437,6 +445,7 @@ bool APIServer::save_noise_psk(psk_t psk, bool make_active) {
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
  return true;
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -62,9 +62,11 @@ async def async_run_logs(config: dict[str, Any], addresses: list[str]) -> None:
 | 
			
		||||
        time_ = datetime.now()
 | 
			
		||||
        message: bytes = msg.message
 | 
			
		||||
        text = message.decode("utf8", "backslashreplace")
 | 
			
		||||
        for parsed_msg in parse_log_message(
 | 
			
		||||
            text, f"[{time_.hour:02}:{time_.minute:02}:{time_.second:02}]"
 | 
			
		||||
        ):
 | 
			
		||||
        nanoseconds = time_.microsecond // 1000
 | 
			
		||||
        timestamp = (
 | 
			
		||||
            f"[{time_.hour:02}:{time_.minute:02}:{time_.second:02}.{nanoseconds:03}]"
 | 
			
		||||
        )
 | 
			
		||||
        for parsed_msg in parse_log_message(text, timestamp):
 | 
			
		||||
            print(parsed_msg.replace("\033", "\\033") if dashboard else parsed_msg)
 | 
			
		||||
 | 
			
		||||
    stop = await async_run(cli, on_log, name=name)
 | 
			
		||||
 
 | 
			
		||||
@@ -130,7 +130,9 @@ class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, publ
 | 
			
		||||
 | 
			
		||||
  std::string get_bluetooth_mac_address_pretty() {
 | 
			
		||||
    const uint8_t *mac = esp_bt_dev_get_address();
 | 
			
		||||
    return str_snprintf("%02X:%02X:%02X:%02X:%02X:%02X", 17, mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
 | 
			
		||||
    char buf[18];
 | 
			
		||||
    format_mac_addr_upper(mac, buf);
 | 
			
		||||
    return std::string(buf);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,8 @@ from esphome.const import (
 | 
			
		||||
    PLATFORM_LN882X,
 | 
			
		||||
    PLATFORM_RTL87XX,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
 | 
			
		||||
from esphome.core import CORE, coroutine_with_priority
 | 
			
		||||
from esphome.coroutine import CoroPriority
 | 
			
		||||
 | 
			
		||||
AUTO_LOAD = ["web_server_base", "ota.web_server"]
 | 
			
		||||
DEPENDENCIES = ["wifi"]
 | 
			
		||||
@@ -40,7 +41,7 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@coroutine_with_priority(CoroPriority.COMMUNICATION)
 | 
			
		||||
@coroutine_with_priority(CoroPriority.CAPTIVE_PORTAL)
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    paren = await cg.get_variable(config[CONF_WEB_SERVER_BASE_ID])
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,7 @@
 | 
			
		||||
#include <cstdio>
 | 
			
		||||
#include <cinttypes>
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome::esp32_ble {
 | 
			
		||||
 | 
			
		||||
@@ -169,22 +170,42 @@ bool ESPBTUUID::operator==(const ESPBTUUID &uuid) const {
 | 
			
		||||
}
 | 
			
		||||
esp_bt_uuid_t ESPBTUUID::get_uuid() const { return this->uuid_; }
 | 
			
		||||
std::string ESPBTUUID::to_string() const {
 | 
			
		||||
  char buf[40];  // Enough for 128-bit UUID with dashes
 | 
			
		||||
  char *pos = buf;
 | 
			
		||||
 | 
			
		||||
  switch (this->uuid_.len) {
 | 
			
		||||
    case ESP_UUID_LEN_16:
 | 
			
		||||
      return str_snprintf("0x%02X%02X", 6, this->uuid_.uuid.uuid16 >> 8, this->uuid_.uuid.uuid16 & 0xff);
 | 
			
		||||
      *pos++ = '0';
 | 
			
		||||
      *pos++ = 'x';
 | 
			
		||||
      *pos++ = format_hex_pretty_char(this->uuid_.uuid.uuid16 >> 12);
 | 
			
		||||
      *pos++ = format_hex_pretty_char((this->uuid_.uuid.uuid16 >> 8) & 0x0F);
 | 
			
		||||
      *pos++ = format_hex_pretty_char((this->uuid_.uuid.uuid16 >> 4) & 0x0F);
 | 
			
		||||
      *pos++ = format_hex_pretty_char(this->uuid_.uuid.uuid16 & 0x0F);
 | 
			
		||||
      *pos = '\0';
 | 
			
		||||
      return std::string(buf);
 | 
			
		||||
 | 
			
		||||
    case ESP_UUID_LEN_32:
 | 
			
		||||
      return str_snprintf("0x%02" PRIX32 "%02" PRIX32 "%02" PRIX32 "%02" PRIX32, 10, (this->uuid_.uuid.uuid32 >> 24),
 | 
			
		||||
                          (this->uuid_.uuid.uuid32 >> 16 & 0xff), (this->uuid_.uuid.uuid32 >> 8 & 0xff),
 | 
			
		||||
                          this->uuid_.uuid.uuid32 & 0xff);
 | 
			
		||||
      *pos++ = '0';
 | 
			
		||||
      *pos++ = 'x';
 | 
			
		||||
      for (int shift = 28; shift >= 0; shift -= 4) {
 | 
			
		||||
        *pos++ = format_hex_pretty_char((this->uuid_.uuid.uuid32 >> shift) & 0x0F);
 | 
			
		||||
      }
 | 
			
		||||
      *pos = '\0';
 | 
			
		||||
      return std::string(buf);
 | 
			
		||||
 | 
			
		||||
    default:
 | 
			
		||||
    case ESP_UUID_LEN_128:
 | 
			
		||||
      std::string buf;
 | 
			
		||||
      // Format: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
 | 
			
		||||
      for (int8_t i = 15; i >= 0; i--) {
 | 
			
		||||
        buf += str_snprintf("%02X", 2, this->uuid_.uuid.uuid128[i]);
 | 
			
		||||
        if (i == 6 || i == 8 || i == 10 || i == 12)
 | 
			
		||||
          buf += "-";
 | 
			
		||||
        uint8_t byte = this->uuid_.uuid.uuid128[i];
 | 
			
		||||
        *pos++ = format_hex_pretty_char(byte >> 4);
 | 
			
		||||
        *pos++ = format_hex_pretty_char(byte & 0x0F);
 | 
			
		||||
        if (i == 12 || i == 10 || i == 8 || i == 6) {
 | 
			
		||||
          *pos++ = '-';
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      return buf;
 | 
			
		||||
      *pos = '\0';
 | 
			
		||||
      return std::string(buf);
 | 
			
		||||
  }
 | 
			
		||||
  return "";
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -31,12 +31,13 @@ void ESP32BLEBeacon::dump_config() {
 | 
			
		||||
  char uuid[37];
 | 
			
		||||
  char *bpos = uuid;
 | 
			
		||||
  for (int8_t ii = 0; ii < 16; ++ii) {
 | 
			
		||||
    bpos += sprintf(bpos, "%02X", this->uuid_[ii]);
 | 
			
		||||
    *bpos++ = format_hex_pretty_char(this->uuid_[ii] >> 4);
 | 
			
		||||
    *bpos++ = format_hex_pretty_char(this->uuid_[ii] & 0x0F);
 | 
			
		||||
    if (ii == 3 || ii == 5 || ii == 7 || ii == 9) {
 | 
			
		||||
      bpos += sprintf(bpos, "-");
 | 
			
		||||
      *bpos++ = '-';
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  uuid[36] = '\0';
 | 
			
		||||
  *bpos = '\0';
 | 
			
		||||
  ESP_LOGCONFIG(TAG,
 | 
			
		||||
                "  UUID: %s, Major: %u, Minor: %u, Min Interval: %ums, Max Interval: %ums, Measured Power: %d"
 | 
			
		||||
                ", TX Power: %ddBm",
 | 
			
		||||
 
 | 
			
		||||
@@ -60,11 +60,14 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
 | 
			
		||||
    if (address == 0) {
 | 
			
		||||
      this->address_str_ = "";
 | 
			
		||||
    } else {
 | 
			
		||||
      this->address_str_ =
 | 
			
		||||
          str_snprintf("%02X:%02X:%02X:%02X:%02X:%02X", 17, (uint8_t) (this->address_ >> 40) & 0xff,
 | 
			
		||||
                       (uint8_t) (this->address_ >> 32) & 0xff, (uint8_t) (this->address_ >> 24) & 0xff,
 | 
			
		||||
                       (uint8_t) (this->address_ >> 16) & 0xff, (uint8_t) (this->address_ >> 8) & 0xff,
 | 
			
		||||
                       (uint8_t) (this->address_ >> 0) & 0xff);
 | 
			
		||||
      char buf[18];
 | 
			
		||||
      uint8_t mac[6] = {
 | 
			
		||||
          (uint8_t) ((this->address_ >> 40) & 0xff), (uint8_t) ((this->address_ >> 32) & 0xff),
 | 
			
		||||
          (uint8_t) ((this->address_ >> 24) & 0xff), (uint8_t) ((this->address_ >> 16) & 0xff),
 | 
			
		||||
          (uint8_t) ((this->address_ >> 8) & 0xff),  (uint8_t) ((this->address_ >> 0) & 0xff),
 | 
			
		||||
      };
 | 
			
		||||
      format_mac_addr_upper(mac, buf);
 | 
			
		||||
      this->address_str_ = buf;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  const std::string &address_str() const { return this->address_str_; }
 | 
			
		||||
 
 | 
			
		||||
@@ -607,9 +607,8 @@ void ESPBTDevice::parse_adv_(const uint8_t *payload, uint8_t len) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
std::string ESPBTDevice::address_str() const {
 | 
			
		||||
  char mac[24];
 | 
			
		||||
  snprintf(mac, sizeof(mac), "%02X:%02X:%02X:%02X:%02X:%02X", this->address_[0], this->address_[1], this->address_[2],
 | 
			
		||||
           this->address_[3], this->address_[4], this->address_[5]);
 | 
			
		||||
  char mac[18];
 | 
			
		||||
  format_mac_addr_upper(this->address_, mac);
 | 
			
		||||
  return mac;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -15,6 +15,8 @@ using namespace bytebuffer;
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "esp32_improv.component";
 | 
			
		||||
static const char *const ESPHOME_MY_LINK = "https://my.home-assistant.io/redirect/config_flow_start?domain=esphome";
 | 
			
		||||
static constexpr uint16_t STOP_ADVERTISING_DELAY =
 | 
			
		||||
    10000;  // Delay (ms) before stopping service to allow BLE clients to read the final state
 | 
			
		||||
 | 
			
		||||
ESP32ImprovComponent::ESP32ImprovComponent() { global_improv_component = this; }
 | 
			
		||||
 | 
			
		||||
@@ -31,6 +33,9 @@ void ESP32ImprovComponent::setup() {
 | 
			
		||||
#endif
 | 
			
		||||
  global_ble_server->on(BLEServerEvt::EmptyEvt::ON_DISCONNECT,
 | 
			
		||||
                        [this](uint16_t conn_id) { this->set_error_(improv::ERROR_NONE); });
 | 
			
		||||
 | 
			
		||||
  // Start with loop disabled - will be enabled by start() when needed
 | 
			
		||||
  this->disable_loop();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ESP32ImprovComponent::setup_characteristics() {
 | 
			
		||||
@@ -190,6 +195,25 @@ void ESP32ImprovComponent::set_status_indicator_state_(bool state) {
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG
 | 
			
		||||
const char *ESP32ImprovComponent::state_to_string_(improv::State state) {
 | 
			
		||||
  switch (state) {
 | 
			
		||||
    case improv::STATE_STOPPED:
 | 
			
		||||
      return "STOPPED";
 | 
			
		||||
    case improv::STATE_AWAITING_AUTHORIZATION:
 | 
			
		||||
      return "AWAITING_AUTHORIZATION";
 | 
			
		||||
    case improv::STATE_AUTHORIZED:
 | 
			
		||||
      return "AUTHORIZED";
 | 
			
		||||
    case improv::STATE_PROVISIONING:
 | 
			
		||||
      return "PROVISIONING";
 | 
			
		||||
    case improv::STATE_PROVISIONED:
 | 
			
		||||
      return "PROVISIONED";
 | 
			
		||||
    default:
 | 
			
		||||
      return "UNKNOWN";
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
bool ESP32ImprovComponent::check_identify_() {
 | 
			
		||||
  uint32_t now = millis();
 | 
			
		||||
 | 
			
		||||
@@ -203,31 +227,42 @@ bool ESP32ImprovComponent::check_identify_() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ESP32ImprovComponent::set_state_(improv::State state) {
 | 
			
		||||
  ESP_LOGV(TAG, "Setting state: %d", state);
 | 
			
		||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG
 | 
			
		||||
  if (this->state_ != state) {
 | 
			
		||||
    ESP_LOGD(TAG, "State transition: %s (0x%02X) -> %s (0x%02X)", this->state_to_string_(this->state_), this->state_,
 | 
			
		||||
             this->state_to_string_(state), state);
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
  this->state_ = state;
 | 
			
		||||
  if (this->status_->get_value().empty() || this->status_->get_value()[0] != state) {
 | 
			
		||||
  if (this->status_ != nullptr && (this->status_->get_value().empty() || this->status_->get_value()[0] != state)) {
 | 
			
		||||
    this->status_->set_value(ByteBuffer::wrap(static_cast<uint8_t>(state)));
 | 
			
		||||
    if (state != improv::STATE_STOPPED)
 | 
			
		||||
      this->status_->notify();
 | 
			
		||||
  }
 | 
			
		||||
  std::vector<uint8_t> service_data(8, 0);
 | 
			
		||||
  service_data[0] = 0x77;  // PR
 | 
			
		||||
  service_data[1] = 0x46;  // IM
 | 
			
		||||
  service_data[2] = static_cast<uint8_t>(state);
 | 
			
		||||
  // Only advertise valid Improv states (0x01-0x04).
 | 
			
		||||
  // STATE_STOPPED (0x00) is internal only and not part of the Improv spec.
 | 
			
		||||
  // Advertising 0x00 causes undefined behavior in some clients and makes them
 | 
			
		||||
  // repeatedly connect trying to determine the actual state.
 | 
			
		||||
  if (state != improv::STATE_STOPPED) {
 | 
			
		||||
    std::vector<uint8_t> service_data(8, 0);
 | 
			
		||||
    service_data[0] = 0x77;  // PR
 | 
			
		||||
    service_data[1] = 0x46;  // IM
 | 
			
		||||
    service_data[2] = static_cast<uint8_t>(state);
 | 
			
		||||
 | 
			
		||||
  uint8_t capabilities = 0x00;
 | 
			
		||||
    uint8_t capabilities = 0x00;
 | 
			
		||||
#ifdef USE_OUTPUT
 | 
			
		||||
  if (this->status_indicator_ != nullptr)
 | 
			
		||||
    capabilities |= improv::CAPABILITY_IDENTIFY;
 | 
			
		||||
    if (this->status_indicator_ != nullptr)
 | 
			
		||||
      capabilities |= improv::CAPABILITY_IDENTIFY;
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  service_data[3] = capabilities;
 | 
			
		||||
  service_data[4] = 0x00;  // Reserved
 | 
			
		||||
  service_data[5] = 0x00;  // Reserved
 | 
			
		||||
  service_data[6] = 0x00;  // Reserved
 | 
			
		||||
  service_data[7] = 0x00;  // Reserved
 | 
			
		||||
    service_data[3] = capabilities;
 | 
			
		||||
    service_data[4] = 0x00;  // Reserved
 | 
			
		||||
    service_data[5] = 0x00;  // Reserved
 | 
			
		||||
    service_data[6] = 0x00;  // Reserved
 | 
			
		||||
    service_data[7] = 0x00;  // Reserved
 | 
			
		||||
 | 
			
		||||
  esp32_ble::global_ble->advertising_set_service_data(service_data);
 | 
			
		||||
    esp32_ble::global_ble->advertising_set_service_data(service_data);
 | 
			
		||||
  }
 | 
			
		||||
#ifdef USE_ESP32_IMPROV_STATE_CALLBACK
 | 
			
		||||
  this->state_callback_.call(this->state_, this->error_state_);
 | 
			
		||||
#endif
 | 
			
		||||
@@ -237,7 +272,12 @@ void ESP32ImprovComponent::set_error_(improv::Error error) {
 | 
			
		||||
  if (error != improv::ERROR_NONE) {
 | 
			
		||||
    ESP_LOGE(TAG, "Error: %d", error);
 | 
			
		||||
  }
 | 
			
		||||
  if (this->error_->get_value().empty() || this->error_->get_value()[0] != error) {
 | 
			
		||||
  // The error_ characteristic is initialized in setup_characteristics() which is called
 | 
			
		||||
  // from the loop, while the BLE disconnect callback is registered in setup().
 | 
			
		||||
  // error_ can be nullptr if:
 | 
			
		||||
  // 1. A client connects/disconnects before setup_characteristics() is called
 | 
			
		||||
  // 2. The device is already provisioned so the service never starts (should_start_ is false)
 | 
			
		||||
  if (this->error_ != nullptr && (this->error_->get_value().empty() || this->error_->get_value()[0] != error)) {
 | 
			
		||||
    this->error_->set_value(ByteBuffer::wrap(static_cast<uint8_t>(error)));
 | 
			
		||||
    if (this->state_ != improv::STATE_STOPPED)
 | 
			
		||||
      this->error_->notify();
 | 
			
		||||
@@ -261,7 +301,10 @@ void ESP32ImprovComponent::start() {
 | 
			
		||||
 | 
			
		||||
void ESP32ImprovComponent::stop() {
 | 
			
		||||
  this->should_start_ = false;
 | 
			
		||||
  this->set_timeout("end-service", 1000, [this] {
 | 
			
		||||
  // Wait before stopping the service to ensure all BLE clients see the state change.
 | 
			
		||||
  // This prevents clients from repeatedly reconnecting and wasting resources by allowing
 | 
			
		||||
  // them to observe that the device is provisioned before the service disappears.
 | 
			
		||||
  this->set_timeout("end-service", STOP_ADVERTISING_DELAY, [this] {
 | 
			
		||||
    if (this->state_ == improv::STATE_STOPPED || this->service_ == nullptr)
 | 
			
		||||
      return;
 | 
			
		||||
    this->service_->stop();
 | 
			
		||||
 
 | 
			
		||||
@@ -79,12 +79,12 @@ class ESP32ImprovComponent : public Component {
 | 
			
		||||
  std::vector<uint8_t> incoming_data_;
 | 
			
		||||
  wifi::WiFiAP connecting_sta_;
 | 
			
		||||
 | 
			
		||||
  BLEService *service_ = nullptr;
 | 
			
		||||
  BLECharacteristic *status_;
 | 
			
		||||
  BLECharacteristic *error_;
 | 
			
		||||
  BLECharacteristic *rpc_;
 | 
			
		||||
  BLECharacteristic *rpc_response_;
 | 
			
		||||
  BLECharacteristic *capabilities_;
 | 
			
		||||
  BLEService *service_{nullptr};
 | 
			
		||||
  BLECharacteristic *status_{nullptr};
 | 
			
		||||
  BLECharacteristic *error_{nullptr};
 | 
			
		||||
  BLECharacteristic *rpc_{nullptr};
 | 
			
		||||
  BLECharacteristic *rpc_response_{nullptr};
 | 
			
		||||
  BLECharacteristic *capabilities_{nullptr};
 | 
			
		||||
 | 
			
		||||
#ifdef USE_BINARY_SENSOR
 | 
			
		||||
  binary_sensor::BinarySensor *authorizer_{nullptr};
 | 
			
		||||
@@ -108,6 +108,9 @@ class ESP32ImprovComponent : public Component {
 | 
			
		||||
  void process_incoming_data_();
 | 
			
		||||
  void on_wifi_connect_timeout_();
 | 
			
		||||
  bool check_identify_();
 | 
			
		||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG
 | 
			
		||||
  const char *state_to_string_(improv::State state);
 | 
			
		||||
#endif
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,8 @@ from esphome.const import (
 | 
			
		||||
    CONF_SAFE_MODE,
 | 
			
		||||
    CONF_VERSION,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import CoroPriority, coroutine_with_priority
 | 
			
		||||
from esphome.core import coroutine_with_priority
 | 
			
		||||
from esphome.coroutine import CoroPriority
 | 
			
		||||
import esphome.final_validate as fv
 | 
			
		||||
 | 
			
		||||
_LOGGER = logging.getLogger(__name__)
 | 
			
		||||
@@ -121,7 +122,7 @@ CONFIG_SCHEMA = (
 | 
			
		||||
FINAL_VALIDATE_SCHEMA = ota_esphome_final_validate
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@coroutine_with_priority(CoroPriority.COMMUNICATION)
 | 
			
		||||
@coroutine_with_priority(CoroPriority.OTA_UPDATES)
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    cg.add(var.set_port(config[CONF_PORT]))
 | 
			
		||||
 
 | 
			
		||||
@@ -77,6 +77,13 @@ ETHERNET_TYPES = {
 | 
			
		||||
    "DM9051": EthernetType.ETHERNET_TYPE_DM9051,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# PHY types that need compile-time defines for conditional compilation
 | 
			
		||||
_PHY_TYPE_TO_DEFINE = {
 | 
			
		||||
    "KSZ8081": "USE_ETHERNET_KSZ8081",
 | 
			
		||||
    "KSZ8081RNA": "USE_ETHERNET_KSZ8081",
 | 
			
		||||
    # Add other PHY types here only if they need conditional compilation
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
SPI_ETHERNET_TYPES = ["W5500", "DM9051"]
 | 
			
		||||
SPI_ETHERNET_DEFAULT_POLLING_INTERVAL = TimePeriodMilliseconds(milliseconds=10)
 | 
			
		||||
 | 
			
		||||
@@ -345,6 +352,10 @@ async def to_code(config):
 | 
			
		||||
    if CONF_MANUAL_IP in config:
 | 
			
		||||
        cg.add(var.set_manual_ip(manual_ip(config[CONF_MANUAL_IP])))
 | 
			
		||||
 | 
			
		||||
    # Add compile-time define for PHY types with specific code
 | 
			
		||||
    if phy_define := _PHY_TYPE_TO_DEFINE.get(config[CONF_TYPE]):
 | 
			
		||||
        cg.add_define(phy_define)
 | 
			
		||||
 | 
			
		||||
    cg.add_define("USE_ETHERNET")
 | 
			
		||||
 | 
			
		||||
    # Disable WiFi when using Ethernet to save memory
 | 
			
		||||
 
 | 
			
		||||
@@ -229,10 +229,12 @@ void EthernetComponent::setup() {
 | 
			
		||||
  ESPHL_ERROR_CHECK(err, "ETH driver install error");
 | 
			
		||||
 | 
			
		||||
#ifndef USE_ETHERNET_SPI
 | 
			
		||||
#ifdef USE_ETHERNET_KSZ8081
 | 
			
		||||
  if (this->type_ == ETHERNET_TYPE_KSZ8081RNA && this->clk_mode_ == EMAC_CLK_OUT) {
 | 
			
		||||
    // KSZ8081RNA default is incorrect. It expects a 25MHz clock instead of the 50MHz we provide.
 | 
			
		||||
    this->ksz8081_set_clock_reference_(mac);
 | 
			
		||||
  }
 | 
			
		||||
#endif  // USE_ETHERNET_KSZ8081
 | 
			
		||||
 | 
			
		||||
  for (const auto &phy_register : this->phy_registers_) {
 | 
			
		||||
    this->write_phy_register_(mac, phy_register);
 | 
			
		||||
@@ -300,6 +302,7 @@ void EthernetComponent::loop() {
 | 
			
		||||
        this->state_ = EthernetComponentState::CONNECTING;
 | 
			
		||||
        this->start_connect_();
 | 
			
		||||
      } else {
 | 
			
		||||
        this->finish_connect_();
 | 
			
		||||
        // When connected and stable, disable the loop to save CPU cycles
 | 
			
		||||
        this->disable_loop();
 | 
			
		||||
      }
 | 
			
		||||
@@ -486,10 +489,35 @@ void EthernetComponent::got_ip6_event_handler(void *arg, esp_event_base_t event_
 | 
			
		||||
}
 | 
			
		||||
#endif /* USE_NETWORK_IPV6 */
 | 
			
		||||
 | 
			
		||||
void EthernetComponent::finish_connect_() {
 | 
			
		||||
#if USE_NETWORK_IPV6
 | 
			
		||||
  // Retry IPv6 link-local setup if it failed during initial connect
 | 
			
		||||
  // This handles the case where min_ipv6_addr_count is NOT set (or is 0),
 | 
			
		||||
  // allowing us to reach CONNECTED state with just IPv4.
 | 
			
		||||
  // If IPv6 setup failed in start_connect_() because the interface wasn't ready:
 | 
			
		||||
  // - Bootup timing issues (#10281)
 | 
			
		||||
  // - Cable unplugged/network interruption (#10705)
 | 
			
		||||
  // We can now retry since we're in CONNECTED state and the interface is definitely up.
 | 
			
		||||
  if (!this->ipv6_setup_done_) {
 | 
			
		||||
    esp_err_t err = esp_netif_create_ip6_linklocal(this->eth_netif_);
 | 
			
		||||
    if (err == ESP_OK) {
 | 
			
		||||
      ESP_LOGD(TAG, "IPv6 link-local address created (retry succeeded)");
 | 
			
		||||
    }
 | 
			
		||||
    // Always set the flag to prevent continuous retries
 | 
			
		||||
    // If IPv6 setup fails here with the interface up and stable, it's
 | 
			
		||||
    // likely a persistent issue (IPv6 disabled at router, hardware
 | 
			
		||||
    // limitation, etc.) that won't be resolved by further retries.
 | 
			
		||||
    // The device continues to work with IPv4.
 | 
			
		||||
    this->ipv6_setup_done_ = true;
 | 
			
		||||
  }
 | 
			
		||||
#endif /* USE_NETWORK_IPV6 */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void EthernetComponent::start_connect_() {
 | 
			
		||||
  global_eth_component->got_ipv4_address_ = false;
 | 
			
		||||
#if USE_NETWORK_IPV6
 | 
			
		||||
  global_eth_component->ipv6_count_ = 0;
 | 
			
		||||
  this->ipv6_setup_done_ = false;
 | 
			
		||||
#endif /* USE_NETWORK_IPV6 */
 | 
			
		||||
  this->connect_begin_ = millis();
 | 
			
		||||
  this->status_set_warning(LOG_STR("waiting for IP configuration"));
 | 
			
		||||
@@ -545,9 +573,27 @@ void EthernetComponent::start_connect_() {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
#if USE_NETWORK_IPV6
 | 
			
		||||
  // Attempt to create IPv6 link-local address
 | 
			
		||||
  // We MUST attempt this here, not just in finish_connect_(), because with
 | 
			
		||||
  // min_ipv6_addr_count set, the component won't reach CONNECTED state without IPv6.
 | 
			
		||||
  // However, this may fail with ESP_FAIL if the interface is not up yet:
 | 
			
		||||
  // - At bootup when link isn't ready (#10281)
 | 
			
		||||
  // - After disconnection/cable unplugged (#10705)
 | 
			
		||||
  // We'll retry in finish_connect_() if it fails here.
 | 
			
		||||
  err = esp_netif_create_ip6_linklocal(this->eth_netif_);
 | 
			
		||||
  if (err != ESP_OK) {
 | 
			
		||||
    ESPHL_ERROR_CHECK(err, "Enable IPv6 link local failed");
 | 
			
		||||
    if (err == ESP_ERR_ESP_NETIF_INVALID_PARAMS) {
 | 
			
		||||
      // This is a programming error, not a transient failure
 | 
			
		||||
      ESPHL_ERROR_CHECK(err, "esp_netif_create_ip6_linklocal invalid parameters");
 | 
			
		||||
    } else {
 | 
			
		||||
      // ESP_FAIL means the interface isn't up yet
 | 
			
		||||
      // This is expected and non-fatal, happens in multiple scenarios:
 | 
			
		||||
      // - During reconnection after network interruptions (#10705)
 | 
			
		||||
      // - At bootup when the link isn't ready yet (#10281)
 | 
			
		||||
      // We'll retry once we reach CONNECTED state and the interface is up
 | 
			
		||||
      ESP_LOGW(TAG, "esp_netif_create_ip6_linklocal failed: %s", esp_err_to_name(err));
 | 
			
		||||
      // Don't mark component as failed - this is a transient error
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
#endif /* USE_NETWORK_IPV6 */
 | 
			
		||||
 | 
			
		||||
@@ -638,7 +684,9 @@ void EthernetComponent::get_eth_mac_address_raw(uint8_t *mac) {
 | 
			
		||||
std::string EthernetComponent::get_eth_mac_address_pretty() {
 | 
			
		||||
  uint8_t mac[6];
 | 
			
		||||
  get_eth_mac_address_raw(mac);
 | 
			
		||||
  return str_snprintf("%02X:%02X:%02X:%02X:%02X:%02X", 17, mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
 | 
			
		||||
  char buf[18];
 | 
			
		||||
  format_mac_addr_upper(mac, buf);
 | 
			
		||||
  return std::string(buf);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
eth_duplex_t EthernetComponent::get_duplex_mode() {
 | 
			
		||||
@@ -675,6 +723,7 @@ bool EthernetComponent::powerdown() {
 | 
			
		||||
 | 
			
		||||
#ifndef USE_ETHERNET_SPI
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ETHERNET_KSZ8081
 | 
			
		||||
constexpr uint8_t KSZ80XX_PC2R_REG_ADDR = 0x1F;
 | 
			
		||||
 | 
			
		||||
void EthernetComponent::ksz8081_set_clock_reference_(esp_eth_mac_t *mac) {
 | 
			
		||||
@@ -703,6 +752,7 @@ void EthernetComponent::ksz8081_set_clock_reference_(esp_eth_mac_t *mac) {
 | 
			
		||||
    ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s", format_hex_pretty((u_int8_t *) &phy_control_2, 2).c_str());
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif  // USE_ETHERNET_KSZ8081
 | 
			
		||||
 | 
			
		||||
void EthernetComponent::write_phy_register_(esp_eth_mac_t *mac, PHYRegister register_data) {
 | 
			
		||||
  esp_err_t err;
 | 
			
		||||
 
 | 
			
		||||
@@ -102,9 +102,12 @@ class EthernetComponent : public Component {
 | 
			
		||||
#endif /* LWIP_IPV6 */
 | 
			
		||||
 | 
			
		||||
  void start_connect_();
 | 
			
		||||
  void finish_connect_();
 | 
			
		||||
  void dump_connect_params_();
 | 
			
		||||
#ifdef USE_ETHERNET_KSZ8081
 | 
			
		||||
  /// @brief Set `RMII Reference Clock Select` bit for KSZ8081.
 | 
			
		||||
  void ksz8081_set_clock_reference_(esp_eth_mac_t *mac);
 | 
			
		||||
#endif
 | 
			
		||||
  /// @brief Set arbitratry PHY registers from config.
 | 
			
		||||
  void write_phy_register_(esp_eth_mac_t *mac, PHYRegister register_data);
 | 
			
		||||
 | 
			
		||||
@@ -144,6 +147,7 @@ class EthernetComponent : public Component {
 | 
			
		||||
  bool got_ipv4_address_{false};
 | 
			
		||||
#if LWIP_IPV6
 | 
			
		||||
  uint8_t ipv6_count_{0};
 | 
			
		||||
  bool ipv6_setup_done_{false};
 | 
			
		||||
#endif /* LWIP_IPV6 */
 | 
			
		||||
 | 
			
		||||
  // Pointers at the end (naturally aligned)
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@ namespace gpio {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "gpio.binary_sensor";
 | 
			
		||||
 | 
			
		||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG
 | 
			
		||||
static const LogString *interrupt_type_to_string(gpio::InterruptType type) {
 | 
			
		||||
  switch (type) {
 | 
			
		||||
    case gpio::INTERRUPT_RISING_EDGE:
 | 
			
		||||
@@ -22,6 +23,7 @@ static const LogString *interrupt_type_to_string(gpio::InterruptType type) {
 | 
			
		||||
static const LogString *gpio_mode_to_string(bool use_interrupt) {
 | 
			
		||||
  return use_interrupt ? LOG_STR("interrupt") : LOG_STR("polling");
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
void IRAM_ATTR GPIOBinarySensorStore::gpio_intr(GPIOBinarySensorStore *arg) {
 | 
			
		||||
  bool new_state = arg->isr_pin_.digital_read();
 | 
			
		||||
 
 | 
			
		||||
@@ -128,4 +128,4 @@ async def to_code(config):
 | 
			
		||||
 | 
			
		||||
    cg.add_library("tonia/HeatpumpIR", "1.0.37")
 | 
			
		||||
    if CORE.is_libretiny or CORE.is_esp32:
 | 
			
		||||
        CORE.add_platformio_option("lib_ignore", "IRremoteESP8266")
 | 
			
		||||
        CORE.add_platformio_option("lib_ignore", ["IRremoteESP8266"])
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,8 @@ import esphome.codegen as cg
 | 
			
		||||
from esphome.components.ota import BASE_OTA_SCHEMA, OTAComponent, ota_to_code
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import CONF_ID, CONF_PASSWORD, CONF_URL, CONF_USERNAME
 | 
			
		||||
from esphome.core import CoroPriority, coroutine_with_priority
 | 
			
		||||
from esphome.core import coroutine_with_priority
 | 
			
		||||
from esphome.coroutine import CoroPriority
 | 
			
		||||
 | 
			
		||||
from .. import CONF_HTTP_REQUEST_ID, HttpRequestComponent, http_request_ns
 | 
			
		||||
 | 
			
		||||
@@ -40,7 +41,7 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@coroutine_with_priority(CoroPriority.COMMUNICATION)
 | 
			
		||||
@coroutine_with_priority(CoroPriority.OTA_UPDATES)
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await ota_to_code(var, config)
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,7 @@ from esphome.const import (
 | 
			
		||||
    DEVICE_CLASS_TEMPERATURE,
 | 
			
		||||
    DEVICE_CLASS_VOLTAGE,
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    STATE_CLASS_TOTAL_INCREASING,
 | 
			
		||||
    UNIT_AMPERE,
 | 
			
		||||
    UNIT_CELSIUS,
 | 
			
		||||
    UNIT_VOLT,
 | 
			
		||||
@@ -162,7 +163,7 @@ INA2XX_SCHEMA = cv.Schema(
 | 
			
		||||
                unit_of_measurement=UNIT_WATT_HOURS,
 | 
			
		||||
                accuracy_decimals=8,
 | 
			
		||||
                device_class=DEVICE_CLASS_ENERGY,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
                state_class=STATE_CLASS_TOTAL_INCREASING,
 | 
			
		||||
            ),
 | 
			
		||||
            key=CONF_NAME,
 | 
			
		||||
        ),
 | 
			
		||||
@@ -170,7 +171,8 @@ INA2XX_SCHEMA = cv.Schema(
 | 
			
		||||
            sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_JOULE,
 | 
			
		||||
                accuracy_decimals=8,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
                device_class=DEVICE_CLASS_ENERGY,
 | 
			
		||||
                state_class=STATE_CLASS_TOTAL_INCREASING,
 | 
			
		||||
            ),
 | 
			
		||||
            key=CONF_NAME,
 | 
			
		||||
        ),
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,3 @@
 | 
			
		||||
#include <cstdio>
 | 
			
		||||
#include <cstring>
 | 
			
		||||
#include "md5.h"
 | 
			
		||||
#ifdef USE_MD5
 | 
			
		||||
@@ -44,7 +43,9 @@ void MD5Digest::get_bytes(uint8_t *output) { memcpy(output, this->digest_, 16);
 | 
			
		||||
 | 
			
		||||
void MD5Digest::get_hex(char *output) {
 | 
			
		||||
  for (size_t i = 0; i < 16; i++) {
 | 
			
		||||
    sprintf(output + i * 2, "%02x", this->digest_[i]);
 | 
			
		||||
    uint8_t byte = this->digest_[i];
 | 
			
		||||
    output[i * 2] = format_hex_char(byte >> 4);
 | 
			
		||||
    output[i * 2 + 1] = format_hex_char(byte & 0x0F);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,8 @@ from esphome.const import (
 | 
			
		||||
    CONF_SERVICES,
 | 
			
		||||
    PlatformFramework,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
 | 
			
		||||
from esphome.core import CORE, coroutine_with_priority
 | 
			
		||||
from esphome.coroutine import CoroPriority
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@esphome/core"]
 | 
			
		||||
DEPENDENCIES = ["network"]
 | 
			
		||||
@@ -72,7 +73,7 @@ def mdns_service(
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@coroutine_with_priority(CoroPriority.COMMUNICATION)
 | 
			
		||||
@coroutine_with_priority(CoroPriority.NETWORK_SERVICES)
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    if config[CONF_DISABLED] is True:
 | 
			
		||||
        return
 | 
			
		||||
 
 | 
			
		||||
@@ -401,6 +401,12 @@ class DriverChip:
 | 
			
		||||
        sequence.append((MADCTL, madctl))
 | 
			
		||||
        return madctl
 | 
			
		||||
 | 
			
		||||
    def skip_command(self, command: str):
 | 
			
		||||
        """
 | 
			
		||||
        Allow suppressing a standard command in the init sequence.
 | 
			
		||||
        """
 | 
			
		||||
        return self.get_default(f"no_{command.lower()}", False)
 | 
			
		||||
 | 
			
		||||
    def get_sequence(self, config) -> tuple[tuple[int, ...], int]:
 | 
			
		||||
        """
 | 
			
		||||
        Create the init sequence for the display.
 | 
			
		||||
@@ -432,7 +438,9 @@ class DriverChip:
 | 
			
		||||
            sequence.append((INVOFF,))
 | 
			
		||||
        if brightness := config.get(CONF_BRIGHTNESS, self.get_default(CONF_BRIGHTNESS)):
 | 
			
		||||
            sequence.append((BRIGHTNESS, brightness))
 | 
			
		||||
        sequence.append((SLPOUT,))
 | 
			
		||||
        # Add a SLPOUT command if required.
 | 
			
		||||
        if not self.skip_command("SLPOUT"):
 | 
			
		||||
            sequence.append((SLPOUT,))
 | 
			
		||||
        sequence.append((DISPON,))
 | 
			
		||||
 | 
			
		||||
        # Flatten the sequence into a list of bytes, with the length of each command
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,7 @@ wave_4_3 = DriverChip(
 | 
			
		||||
    "ESP32-S3-TOUCH-LCD-4.3",
 | 
			
		||||
    swap_xy=UNDEFINED,
 | 
			
		||||
    initsequence=(),
 | 
			
		||||
    color_order="RGB",
 | 
			
		||||
    width=800,
 | 
			
		||||
    height=480,
 | 
			
		||||
    pclk_frequency="16MHz",
 | 
			
		||||
 
 | 
			
		||||
@@ -27,7 +27,8 @@ DriverChip(
 | 
			
		||||
    bus_mode=TYPE_QUAD,
 | 
			
		||||
    brightness=0xD0,
 | 
			
		||||
    color_order=MODE_RGB,
 | 
			
		||||
    initsequence=(SLPOUT,),  # Requires early SLPOUT
 | 
			
		||||
    no_slpout=True,  # SLPOUT is in the init sequence, early
 | 
			
		||||
    initsequence=(SLPOUT,),
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
DriverChip(
 | 
			
		||||
@@ -95,6 +96,7 @@ CO5300 = DriverChip(
 | 
			
		||||
    brightness=0xD0,
 | 
			
		||||
    color_order=MODE_RGB,
 | 
			
		||||
    bus_mode=TYPE_QUAD,
 | 
			
		||||
    no_slpout=True,
 | 
			
		||||
    initsequence=(
 | 
			
		||||
        (SLPOUT,),  # Requires early SLPOUT
 | 
			
		||||
        (PAGESEL, 0x00),
 | 
			
		||||
 
 | 
			
		||||
@@ -491,7 +491,7 @@ bool MQTTClientComponent::publish(const std::string &topic, const std::string &p
 | 
			
		||||
 | 
			
		||||
bool MQTTClientComponent::publish(const std::string &topic, const char *payload, size_t payload_length, uint8_t qos,
 | 
			
		||||
                                  bool retain) {
 | 
			
		||||
  return publish({.topic = topic, .payload = payload, .qos = qos, .retain = retain});
 | 
			
		||||
  return publish({.topic = topic, .payload = std::string(payload, payload_length), .qos = qos, .retain = retain});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool MQTTClientComponent::publish(const MQTTMessage &message) {
 | 
			
		||||
 
 | 
			
		||||
@@ -11,8 +11,6 @@
 | 
			
		||||
#include <openthread/instance.h>
 | 
			
		||||
#include <openthread/logging.h>
 | 
			
		||||
#include <openthread/netdata.h>
 | 
			
		||||
#include <openthread/srp_client.h>
 | 
			
		||||
#include <openthread/srp_client_buffers.h>
 | 
			
		||||
#include <openthread/tasklet.h>
 | 
			
		||||
 | 
			
		||||
#include <cstring>
 | 
			
		||||
@@ -77,8 +75,14 @@ std::optional<otIp6Address> 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<void()> 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
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,8 @@
 | 
			
		||||
#include "esphome/components/network/ip_address.h"
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
 | 
			
		||||
#include <openthread/srp_client.h>
 | 
			
		||||
#include <openthread/srp_client_buffers.h>
 | 
			
		||||
#include <openthread/thread.h>
 | 
			
		||||
 | 
			
		||||
#include <optional>
 | 
			
		||||
@@ -28,11 +30,14 @@ class OpenThreadComponent : public Component {
 | 
			
		||||
  network::IPAddresses get_ip_addresses();
 | 
			
		||||
  std::optional<otIp6Address> get_omr_address();
 | 
			
		||||
  void ot_main();
 | 
			
		||||
  void on_factory_reset(std::function<void()> callback);
 | 
			
		||||
  void defer_factory_reset_external_callback();
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  std::optional<otIp6Address> get_omr_address_(InstanceLock &lock);
 | 
			
		||||
  bool teardown_started_{false};
 | 
			
		||||
  bool teardown_complete_{false};
 | 
			
		||||
  std::function<void()> 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};
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,8 @@ from esphome.const import (
 | 
			
		||||
    CONF_TRIGGER_ID,
 | 
			
		||||
    PlatformFramework,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
 | 
			
		||||
from esphome.core import CORE, coroutine_with_priority
 | 
			
		||||
from esphome.coroutine import CoroPriority
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@esphome/core"]
 | 
			
		||||
AUTO_LOAD = ["md5", "safe_mode"]
 | 
			
		||||
@@ -82,7 +83,7 @@ BASE_OTA_SCHEMA = cv.Schema(
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@coroutine_with_priority(CoroPriority.COMMUNICATION)
 | 
			
		||||
@coroutine_with_priority(CoroPriority.OTA_UPDATES)
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    cg.add_define("USE_OTA")
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -121,15 +121,11 @@ def transport_schema(cls):
 | 
			
		||||
    return TRANSPORT_SCHEMA.extend({cv.GenerateID(): cv.declare_id(cls)})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Build a list of sensors for this platform
 | 
			
		||||
CORE.data[DOMAIN] = {CONF_SENSORS: []}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_sensors(transport_id):
 | 
			
		||||
    """Return the list of sensors for this platform."""
 | 
			
		||||
    return (
 | 
			
		||||
        sensor
 | 
			
		||||
        for sensor in CORE.data[DOMAIN][CONF_SENSORS]
 | 
			
		||||
        for sensor in CORE.data.setdefault(DOMAIN, {}).setdefault(CONF_SENSORS, [])
 | 
			
		||||
        if sensor[CONF_TRANSPORT_ID] == transport_id
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
@@ -137,7 +133,8 @@ def get_sensors(transport_id):
 | 
			
		||||
def validate_packet_transport_sensor(config):
 | 
			
		||||
    if CONF_NAME in config and CONF_INTERNAL not in config:
 | 
			
		||||
        raise cv.Invalid("Must provide internal: config when using name:")
 | 
			
		||||
    CORE.data[DOMAIN][CONF_SENSORS].append(config)
 | 
			
		||||
    conf_sensors = CORE.data.setdefault(DOMAIN, {}).setdefault(CONF_SENSORS, [])
 | 
			
		||||
    conf_sensors.append(config)
 | 
			
		||||
    return config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -28,12 +28,12 @@ bool Select::has_option(const std::string &option) const { return this->index_of
 | 
			
		||||
bool Select::has_index(size_t index) const { return index < this->size(); }
 | 
			
		||||
 | 
			
		||||
size_t Select::size() const {
 | 
			
		||||
  auto options = traits.get_options();
 | 
			
		||||
  const auto &options = traits.get_options();
 | 
			
		||||
  return options.size();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
optional<size_t> Select::index_of(const std::string &option) const {
 | 
			
		||||
  auto options = traits.get_options();
 | 
			
		||||
  const auto &options = traits.get_options();
 | 
			
		||||
  auto it = std::find(options.begin(), options.end(), option);
 | 
			
		||||
  if (it == options.end()) {
 | 
			
		||||
    return {};
 | 
			
		||||
@@ -51,7 +51,7 @@ optional<size_t> Select::active_index() const {
 | 
			
		||||
 | 
			
		||||
optional<std::string> Select::at(size_t index) const {
 | 
			
		||||
  if (this->has_index(index)) {
 | 
			
		||||
    auto options = traits.get_options();
 | 
			
		||||
    const auto &options = traits.get_options();
 | 
			
		||||
    return options.at(index);
 | 
			
		||||
  } else {
 | 
			
		||||
    return {};
 | 
			
		||||
 
 | 
			
		||||
@@ -45,7 +45,7 @@ void SelectCall::perform() {
 | 
			
		||||
  auto *parent = this->parent_;
 | 
			
		||||
  const auto *name = parent->get_name().c_str();
 | 
			
		||||
  const auto &traits = parent->traits;
 | 
			
		||||
  auto options = traits.get_options();
 | 
			
		||||
  const auto &options = traits.get_options();
 | 
			
		||||
 | 
			
		||||
  if (this->operation_ == SELECT_OP_NONE) {
 | 
			
		||||
    ESP_LOGW(TAG, "'%s' - SelectCall performed without selecting an operation", name);
 | 
			
		||||
 
 | 
			
		||||
@@ -288,11 +288,15 @@ void Sim800LComponent::parse_cmd_(std::string message) {
 | 
			
		||||
          if (item == 3) {  // stat
 | 
			
		||||
            uint8_t current_call_state = parse_number<uint8_t>(message.substr(start, end - start)).value_or(6);
 | 
			
		||||
            if (current_call_state != this->call_state_) {
 | 
			
		||||
              ESP_LOGD(TAG, "Call state is now: %d", current_call_state);
 | 
			
		||||
              if (current_call_state == 0)
 | 
			
		||||
                this->call_connected_callback_.call();
 | 
			
		||||
              if (current_call_state == 4) {
 | 
			
		||||
                ESP_LOGV(TAG, "Premature call state '4'. Ignoring, waiting for RING");
 | 
			
		||||
              } else {
 | 
			
		||||
                this->call_state_ = current_call_state;
 | 
			
		||||
                ESP_LOGD(TAG, "Call state is now: %d", current_call_state);
 | 
			
		||||
                if (current_call_state == 0)
 | 
			
		||||
                  this->call_connected_callback_.call();
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
            this->call_state_ = current_call_state;
 | 
			
		||||
            break;
 | 
			
		||||
          }
 | 
			
		||||
          // item 4 = ""
 | 
			
		||||
 
 | 
			
		||||
@@ -217,7 +217,7 @@ void SX126x::configure() {
 | 
			
		||||
    this->write_opcode_(RADIO_SET_MODULATIONPARAMS, buf, 4);
 | 
			
		||||
 | 
			
		||||
    // set packet params and sync word
 | 
			
		||||
    this->set_packet_params_(this->payload_length_);
 | 
			
		||||
    this->set_packet_params_(this->get_max_packet_size());
 | 
			
		||||
    if (this->sync_value_.size() == 2) {
 | 
			
		||||
      this->write_register_(REG_LORA_SYNCWORD, this->sync_value_.data(), this->sync_value_.size());
 | 
			
		||||
    }
 | 
			
		||||
@@ -236,7 +236,7 @@ void SX126x::configure() {
 | 
			
		||||
    this->write_opcode_(RADIO_SET_MODULATIONPARAMS, buf, 8);
 | 
			
		||||
 | 
			
		||||
    // set packet params and sync word
 | 
			
		||||
    this->set_packet_params_(this->payload_length_);
 | 
			
		||||
    this->set_packet_params_(this->get_max_packet_size());
 | 
			
		||||
    if (!this->sync_value_.empty()) {
 | 
			
		||||
      this->write_register_(REG_GFSK_SYNCWORD, this->sync_value_.data(), this->sync_value_.size());
 | 
			
		||||
    }
 | 
			
		||||
@@ -274,7 +274,7 @@ void SX126x::set_packet_params_(uint8_t payload_length) {
 | 
			
		||||
    buf[2] = (this->preamble_detect_ > 0) ? ((this->preamble_detect_ - 1) | 0x04) : 0x00;
 | 
			
		||||
    buf[3] = this->sync_value_.size() * 8;
 | 
			
		||||
    buf[4] = 0x00;
 | 
			
		||||
    buf[5] = 0x00;
 | 
			
		||||
    buf[5] = (this->payload_length_ > 0) ? 0x00 : 0x01;
 | 
			
		||||
    buf[6] = payload_length;
 | 
			
		||||
    buf[7] = this->crc_enable_ ? 0x06 : 0x01;
 | 
			
		||||
    buf[8] = 0x00;
 | 
			
		||||
@@ -314,6 +314,9 @@ SX126xError SX126x::transmit_packet(const std::vector<uint8_t> &packet) {
 | 
			
		||||
  buf[0] = 0xFF;
 | 
			
		||||
  buf[1] = 0xFF;
 | 
			
		||||
  this->write_opcode_(RADIO_CLR_IRQSTATUS, buf, 2);
 | 
			
		||||
  if (this->payload_length_ == 0) {
 | 
			
		||||
    this->set_packet_params_(this->get_max_packet_size());
 | 
			
		||||
  }
 | 
			
		||||
  if (this->rx_start_) {
 | 
			
		||||
    this->set_mode_rx();
 | 
			
		||||
  } else {
 | 
			
		||||
 
 | 
			
		||||
@@ -72,6 +72,7 @@ void USBUartTypeCH34X::enable_channels() {
 | 
			
		||||
    if (channel->index_ >= 2)
 | 
			
		||||
      cmd += 0xE;
 | 
			
		||||
    this->control_transfer(USB_VENDOR_DEV | usb_host::USB_DIR_OUT, cmd, value, (factor << 8) | divisor, callback);
 | 
			
		||||
    this->control_transfer(USB_VENDOR_DEV | usb_host::USB_DIR_OUT, cmd + 3, 0x80, 0, callback);
 | 
			
		||||
  }
 | 
			
		||||
  USBUartTypeCdcAcm::enable_channels();
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -242,7 +242,6 @@ void VoiceAssistant::loop() {
 | 
			
		||||
      msg.flags = flags;
 | 
			
		||||
      msg.audio_settings = audio_settings;
 | 
			
		||||
      msg.set_wake_word_phrase(StringRef(this->wake_word_));
 | 
			
		||||
      this->wake_word_ = "";
 | 
			
		||||
 | 
			
		||||
      // Reset media player state tracking
 | 
			
		||||
#ifdef USE_MEDIA_PLAYER
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,8 @@ from esphome.components.esp32 import add_idf_component
 | 
			
		||||
from esphome.components.ota import BASE_OTA_SCHEMA, OTAComponent, ota_to_code
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import CONF_ID
 | 
			
		||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
 | 
			
		||||
from esphome.core import CORE, coroutine_with_priority
 | 
			
		||||
from esphome.coroutine import CoroPriority
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@esphome/core"]
 | 
			
		||||
DEPENDENCIES = ["network", "web_server_base"]
 | 
			
		||||
@@ -22,7 +23,7 @@ CONFIG_SCHEMA = (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@coroutine_with_priority(CoroPriority.COMMUNICATION)
 | 
			
		||||
@coroutine_with_priority(CoroPriority.WEB_SERVER_OTA)
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await ota_to_code(var, config)
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,8 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import CONF_ID
 | 
			
		||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
 | 
			
		||||
from esphome.core import CORE, coroutine_with_priority
 | 
			
		||||
from esphome.coroutine import CoroPriority
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@esphome/core"]
 | 
			
		||||
DEPENDENCIES = ["network"]
 | 
			
		||||
@@ -26,7 +27,7 @@ CONFIG_SCHEMA = cv.Schema(
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@coroutine_with_priority(CoroPriority.COMMUNICATION)
 | 
			
		||||
@coroutine_with_priority(CoroPriority.WEB_SERVER_BASE)
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
@@ -39,5 +40,7 @@ async def to_code(config):
 | 
			
		||||
            cg.add_library("Update", None)
 | 
			
		||||
        if CORE.is_esp8266:
 | 
			
		||||
            cg.add_library("ESP8266WiFi", None)
 | 
			
		||||
        if CORE.is_libretiny:
 | 
			
		||||
            CORE.add_platformio_option("lib_ignore", ["ESPAsyncTCP", "RPAsyncTCP"])
 | 
			
		||||
        # https://github.com/ESP32Async/ESPAsyncWebServer/blob/main/library.json
 | 
			
		||||
        cg.add_library("ESP32Async/ESPAsyncWebServer", "3.7.10")
 | 
			
		||||
 
 | 
			
		||||
@@ -593,7 +593,7 @@ void WiFiComponent::check_scanning_finished() {
 | 
			
		||||
  for (auto &res : this->scan_result_) {
 | 
			
		||||
    char bssid_s[18];
 | 
			
		||||
    auto bssid = res.get_bssid();
 | 
			
		||||
    sprintf(bssid_s, "%02X:%02X:%02X:%02X:%02X:%02X", bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]);
 | 
			
		||||
    format_mac_addr_upper(bssid.data(), bssid_s);
 | 
			
		||||
 | 
			
		||||
    if (res.get_matches()) {
 | 
			
		||||
      ESP_LOGI(TAG, "- '%s' %s" LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(),
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
#include "esphome/components/text_sensor/text_sensor.h"
 | 
			
		||||
#include "esphome/components/wifi/wifi_component.h"
 | 
			
		||||
#ifdef USE_WIFI
 | 
			
		||||
@@ -106,8 +107,8 @@ class BSSIDWiFiInfo : public PollingComponent, public text_sensor::TextSensor {
 | 
			
		||||
    wifi::bssid_t bssid = wifi::global_wifi_component->wifi_bssid();
 | 
			
		||||
    if (memcmp(bssid.data(), last_bssid_.data(), 6) != 0) {
 | 
			
		||||
      std::copy(bssid.begin(), bssid.end(), last_bssid_.begin());
 | 
			
		||||
      char buf[30];
 | 
			
		||||
      sprintf(buf, "%02X:%02X:%02X:%02X:%02X:%02X", bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]);
 | 
			
		||||
      char buf[18];
 | 
			
		||||
      format_mac_addr_upper(bssid.data(), buf);
 | 
			
		||||
      this->publish_state(buf);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ from enum import Enum
 | 
			
		||||
 | 
			
		||||
from esphome.enum import StrEnum
 | 
			
		||||
 | 
			
		||||
__version__ = "2025.9.0b1"
 | 
			
		||||
__version__ = "2025.9.3"
 | 
			
		||||
 | 
			
		||||
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
 | 
			
		||||
VALID_SUBSTITUTIONS_CHARACTERS = (
 | 
			
		||||
@@ -114,6 +114,7 @@ CONF_AND = "and"
 | 
			
		||||
CONF_ANGLE = "angle"
 | 
			
		||||
CONF_ANY = "any"
 | 
			
		||||
CONF_AP = "ap"
 | 
			
		||||
CONF_API = "api"
 | 
			
		||||
CONF_APPARENT_POWER = "apparent_power"
 | 
			
		||||
CONF_ARDUINO_VERSION = "arduino_version"
 | 
			
		||||
CONF_AREA = "area"
 | 
			
		||||
 
 | 
			
		||||
@@ -396,7 +396,7 @@ async def add_includes(includes):
 | 
			
		||||
async def _add_platformio_options(pio_options):
 | 
			
		||||
    # Add includes at the very end, so that they override everything
 | 
			
		||||
    for key, val in pio_options.items():
 | 
			
		||||
        if key == "build_flags" and not isinstance(val, list):
 | 
			
		||||
        if key in ["build_flags", "lib_ignore"] and not isinstance(val, list):
 | 
			
		||||
            val = [val]
 | 
			
		||||
        cg.add_platformio_option(key, val)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -175,6 +175,7 @@
 | 
			
		||||
#ifdef USE_ARDUINO
 | 
			
		||||
#define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 2, 1)
 | 
			
		||||
#define USE_ETHERNET
 | 
			
		||||
#define USE_ETHERNET_KSZ8081
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP_IDF
 | 
			
		||||
 
 | 
			
		||||
@@ -255,23 +255,22 @@ size_t parse_hex(const char *str, size_t length, uint8_t *data, size_t count) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
std::string format_mac_address_pretty(const uint8_t *mac) {
 | 
			
		||||
  return str_snprintf("%02X:%02X:%02X:%02X:%02X:%02X", 17, mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
 | 
			
		||||
  char buf[18];
 | 
			
		||||
  format_mac_addr_upper(mac, buf);
 | 
			
		||||
  return std::string(buf);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static char format_hex_char(uint8_t v) { return v >= 10 ? 'a' + (v - 10) : '0' + v; }
 | 
			
		||||
std::string format_hex(const uint8_t *data, size_t length) {
 | 
			
		||||
  std::string ret;
 | 
			
		||||
  ret.resize(length * 2);
 | 
			
		||||
  for (size_t i = 0; i < length; i++) {
 | 
			
		||||
    ret[2 * i] = format_hex_char((data[i] & 0xF0) >> 4);
 | 
			
		||||
    ret[2 * i] = format_hex_char(data[i] >> 4);
 | 
			
		||||
    ret[2 * i + 1] = format_hex_char(data[i] & 0x0F);
 | 
			
		||||
  }
 | 
			
		||||
  return ret;
 | 
			
		||||
}
 | 
			
		||||
std::string format_hex(const std::vector<uint8_t> &data) { return format_hex(data.data(), data.size()); }
 | 
			
		||||
 | 
			
		||||
static char format_hex_pretty_char(uint8_t v) { return v >= 10 ? 'A' + (v - 10) : '0' + v; }
 | 
			
		||||
 | 
			
		||||
// Shared implementation for uint8_t and string hex formatting
 | 
			
		||||
static std::string format_hex_pretty_uint8(const uint8_t *data, size_t length, char separator, bool show_length) {
 | 
			
		||||
  if (data == nullptr || length == 0)
 | 
			
		||||
@@ -280,7 +279,7 @@ static std::string format_hex_pretty_uint8(const uint8_t *data, size_t length, c
 | 
			
		||||
  uint8_t multiple = separator ? 3 : 2;  // 3 if separator is not \0, 2 otherwise
 | 
			
		||||
  ret.resize(multiple * length - (separator ? 1 : 0));
 | 
			
		||||
  for (size_t i = 0; i < length; i++) {
 | 
			
		||||
    ret[multiple * i] = format_hex_pretty_char((data[i] & 0xF0) >> 4);
 | 
			
		||||
    ret[multiple * i] = format_hex_pretty_char(data[i] >> 4);
 | 
			
		||||
    ret[multiple * i + 1] = format_hex_pretty_char(data[i] & 0x0F);
 | 
			
		||||
    if (separator && i != length - 1)
 | 
			
		||||
      ret[multiple * i + 2] = separator;
 | 
			
		||||
@@ -591,7 +590,9 @@ bool HighFrequencyLoopRequester::is_high_frequency() { return num_requests > 0;
 | 
			
		||||
std::string get_mac_address() {
 | 
			
		||||
  uint8_t mac[6];
 | 
			
		||||
  get_mac_address_raw(mac);
 | 
			
		||||
  return str_snprintf("%02x%02x%02x%02x%02x%02x", 12, mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
 | 
			
		||||
  char buf[13];
 | 
			
		||||
  format_mac_addr_lower_no_sep(mac, buf);
 | 
			
		||||
  return std::string(buf);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
std::string get_mac_address_pretty() {
 | 
			
		||||
 
 | 
			
		||||
@@ -380,6 +380,35 @@ template<typename T, enable_if_t<std::is_unsigned<T>::value, int> = 0> optional<
 | 
			
		||||
  return parse_hex<T>(str.c_str(), str.length());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Convert a nibble (0-15) to lowercase hex char
 | 
			
		||||
inline char format_hex_char(uint8_t v) { return v >= 10 ? 'a' + (v - 10) : '0' + v; }
 | 
			
		||||
 | 
			
		||||
/// Convert a nibble (0-15) to uppercase hex char (used for pretty printing)
 | 
			
		||||
/// This always uses uppercase (A-F) for pretty/human-readable output
 | 
			
		||||
inline char format_hex_pretty_char(uint8_t v) { return v >= 10 ? 'A' + (v - 10) : '0' + v; }
 | 
			
		||||
 | 
			
		||||
/// Format MAC address as XX:XX:XX:XX:XX:XX (uppercase)
 | 
			
		||||
inline void format_mac_addr_upper(const uint8_t *mac, char *output) {
 | 
			
		||||
  for (size_t i = 0; i < 6; i++) {
 | 
			
		||||
    uint8_t byte = mac[i];
 | 
			
		||||
    output[i * 3] = format_hex_pretty_char(byte >> 4);
 | 
			
		||||
    output[i * 3 + 1] = format_hex_pretty_char(byte & 0x0F);
 | 
			
		||||
    if (i < 5)
 | 
			
		||||
      output[i * 3 + 2] = ':';
 | 
			
		||||
  }
 | 
			
		||||
  output[17] = '\0';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Format MAC address as xxxxxxxxxxxxxx (lowercase, no separators)
 | 
			
		||||
inline void format_mac_addr_lower_no_sep(const uint8_t *mac, char *output) {
 | 
			
		||||
  for (size_t i = 0; i < 6; i++) {
 | 
			
		||||
    uint8_t byte = mac[i];
 | 
			
		||||
    output[i * 2] = format_hex_char(byte >> 4);
 | 
			
		||||
    output[i * 2 + 1] = format_hex_char(byte & 0x0F);
 | 
			
		||||
  }
 | 
			
		||||
  output[12] = '\0';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Format the six-byte array \p mac into a MAC address.
 | 
			
		||||
std::string format_mac_address_pretty(const uint8_t mac[6]);
 | 
			
		||||
/// Format the byte array \p data of length \p len in lowercased hex.
 | 
			
		||||
 
 | 
			
		||||
@@ -345,7 +345,7 @@ void HOT Scheduler::call(uint32_t now) {
 | 
			
		||||
    // Execute callback without holding lock to prevent deadlocks
 | 
			
		||||
    // if the callback tries to call defer() again
 | 
			
		||||
    if (!this->should_skip_item_(item.get())) {
 | 
			
		||||
      this->execute_item_(item.get(), now);
 | 
			
		||||
      now = this->execute_item_(item.get(), now);
 | 
			
		||||
    }
 | 
			
		||||
    // Recycle the defer item after execution
 | 
			
		||||
    this->recycle_item_(std::move(item));
 | 
			
		||||
@@ -483,7 +483,7 @@ void HOT Scheduler::call(uint32_t now) {
 | 
			
		||||
    // Warning: During callback(), a lot of stuff can happen, including:
 | 
			
		||||
    //  - timeouts/intervals get added, potentially invalidating vector pointers
 | 
			
		||||
    //  - timeouts/intervals get cancelled
 | 
			
		||||
    this->execute_item_(item.get(), now);
 | 
			
		||||
    now = this->execute_item_(item.get(), now);
 | 
			
		||||
 | 
			
		||||
    LockGuard guard{this->lock_};
 | 
			
		||||
 | 
			
		||||
@@ -568,11 +568,11 @@ void HOT Scheduler::pop_raw_() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Helper to execute a scheduler item
 | 
			
		||||
void HOT Scheduler::execute_item_(SchedulerItem *item, uint32_t now) {
 | 
			
		||||
uint32_t HOT Scheduler::execute_item_(SchedulerItem *item, uint32_t now) {
 | 
			
		||||
  App.set_current_component(item->component);
 | 
			
		||||
  WarnIfComponentBlockingGuard guard{item->component, now};
 | 
			
		||||
  item->callback();
 | 
			
		||||
  guard.finish();
 | 
			
		||||
  return guard.finish();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Common implementation for cancel operations
 | 
			
		||||
 
 | 
			
		||||
@@ -254,7 +254,7 @@ class Scheduler {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Helper to execute a scheduler item
 | 
			
		||||
  void execute_item_(SchedulerItem *item, uint32_t now);
 | 
			
		||||
  uint32_t execute_item_(SchedulerItem *item, uint32_t now);
 | 
			
		||||
 | 
			
		||||
  // Helper to check if item should be skipped
 | 
			
		||||
  bool should_skip_item_(SchedulerItem *item) const {
 | 
			
		||||
 
 | 
			
		||||
@@ -90,11 +90,30 @@ class CoroPriority(enum.IntEnum):
 | 
			
		||||
    # Examples: status_led (80)
 | 
			
		||||
    STATUS = 80
 | 
			
		||||
 | 
			
		||||
    # Web server infrastructure
 | 
			
		||||
    # Examples: web_server_base (65)
 | 
			
		||||
    WEB_SERVER_BASE = 65
 | 
			
		||||
 | 
			
		||||
    # Network portal services
 | 
			
		||||
    # Examples: captive_portal (64)
 | 
			
		||||
    CAPTIVE_PORTAL = 64
 | 
			
		||||
 | 
			
		||||
    # Communication protocols and services
 | 
			
		||||
    # Examples: web_server_base (65), captive_portal (64), wifi (60), ethernet (60),
 | 
			
		||||
    # mdns (55), ota_updates (54), web_server_ota (52)
 | 
			
		||||
    # Examples: wifi (60), ethernet (60)
 | 
			
		||||
    COMMUNICATION = 60
 | 
			
		||||
 | 
			
		||||
    # Network discovery and management services
 | 
			
		||||
    # Examples: mdns (55)
 | 
			
		||||
    NETWORK_SERVICES = 55
 | 
			
		||||
 | 
			
		||||
    # OTA update services
 | 
			
		||||
    # Examples: ota_updates (54)
 | 
			
		||||
    OTA_UPDATES = 54
 | 
			
		||||
 | 
			
		||||
    # Web-based OTA services
 | 
			
		||||
    # Examples: web_server_ota (52)
 | 
			
		||||
    WEB_SERVER_OTA = 52
 | 
			
		||||
 | 
			
		||||
    # Application-level services
 | 
			
		||||
    # Examples: safe_mode (50)
 | 
			
		||||
    APPLICATION = 50
 | 
			
		||||
 
 | 
			
		||||
@@ -1038,12 +1038,9 @@ class ArchiveRequestHandler(BaseHandler):
 | 
			
		||||
        shutil.move(config_file, os.path.join(archive_path, configuration))
 | 
			
		||||
 | 
			
		||||
        storage_json = StorageJSON.load(storage_path)
 | 
			
		||||
        if storage_json is not None:
 | 
			
		||||
        if storage_json is not None and storage_json.build_path:
 | 
			
		||||
            # Delete build folder (if exists)
 | 
			
		||||
            name = storage_json.name
 | 
			
		||||
            build_folder = os.path.join(settings.config_dir, name)
 | 
			
		||||
            if build_folder is not None:
 | 
			
		||||
                shutil.rmtree(build_folder, os.path.join(archive_path, name))
 | 
			
		||||
            shutil.rmtree(storage_json.build_path, ignore_errors=True)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UnArchiveRequestHandler(BaseHandler):
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
import os
 | 
			
		||||
import random
 | 
			
		||||
import string
 | 
			
		||||
from typing import Literal, NotRequired, TypedDict, Unpack
 | 
			
		||||
import unicodedata
 | 
			
		||||
 | 
			
		||||
import voluptuous as vol
 | 
			
		||||
@@ -103,11 +104,25 @@ HARDWARE_BASE_CONFIGS = {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def sanitize_double_quotes(value):
 | 
			
		||||
def sanitize_double_quotes(value: str) -> str:
 | 
			
		||||
    return value.replace("\\", "\\\\").replace('"', '\\"')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def wizard_file(**kwargs):
 | 
			
		||||
class WizardFileKwargs(TypedDict):
 | 
			
		||||
    """Keyword arguments for wizard_file function."""
 | 
			
		||||
 | 
			
		||||
    name: str
 | 
			
		||||
    platform: Literal["ESP8266", "ESP32", "RP2040", "BK72XX", "LN882X", "RTL87XX"]
 | 
			
		||||
    board: str
 | 
			
		||||
    ssid: NotRequired[str]
 | 
			
		||||
    psk: NotRequired[str]
 | 
			
		||||
    password: NotRequired[str]
 | 
			
		||||
    ota_password: NotRequired[str]
 | 
			
		||||
    api_encryption_key: NotRequired[str]
 | 
			
		||||
    friendly_name: NotRequired[str]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def wizard_file(**kwargs: Unpack[WizardFileKwargs]) -> str:
 | 
			
		||||
    letters = string.ascii_letters + string.digits
 | 
			
		||||
    ap_name_base = kwargs["name"].replace("_", " ").title()
 | 
			
		||||
    ap_name = f"{ap_name_base} Fallback Hotspot"
 | 
			
		||||
@@ -180,7 +195,25 @@ captive_portal:
 | 
			
		||||
    return config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def wizard_write(path, **kwargs):
 | 
			
		||||
class WizardWriteKwargs(TypedDict):
 | 
			
		||||
    """Keyword arguments for wizard_write function."""
 | 
			
		||||
 | 
			
		||||
    name: str
 | 
			
		||||
    type: Literal["basic", "empty", "upload"]
 | 
			
		||||
    # Required for "basic" type
 | 
			
		||||
    board: NotRequired[str]
 | 
			
		||||
    platform: NotRequired[str]
 | 
			
		||||
    ssid: NotRequired[str]
 | 
			
		||||
    psk: NotRequired[str]
 | 
			
		||||
    password: NotRequired[str]
 | 
			
		||||
    ota_password: NotRequired[str]
 | 
			
		||||
    api_encryption_key: NotRequired[str]
 | 
			
		||||
    friendly_name: NotRequired[str]
 | 
			
		||||
    # Required for "upload" type
 | 
			
		||||
    file_text: NotRequired[str]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def wizard_write(path: str, **kwargs: Unpack[WizardWriteKwargs]) -> bool:
 | 
			
		||||
    from esphome.components.bk72xx import boards as bk72xx_boards
 | 
			
		||||
    from esphome.components.esp32 import boards as esp32_boards
 | 
			
		||||
    from esphome.components.esp8266 import boards as esp8266_boards
 | 
			
		||||
@@ -237,14 +270,14 @@ def wizard_write(path, **kwargs):
 | 
			
		||||
 | 
			
		||||
if get_bool_env(ENV_QUICKWIZARD):
 | 
			
		||||
 | 
			
		||||
    def sleep(time):
 | 
			
		||||
    def sleep(time: float) -> None:
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
else:
 | 
			
		||||
    from time import sleep
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def safe_print_step(step, big):
 | 
			
		||||
def safe_print_step(step: int, big: str) -> None:
 | 
			
		||||
    safe_print()
 | 
			
		||||
    safe_print()
 | 
			
		||||
    safe_print(f"============= STEP {step} =============")
 | 
			
		||||
@@ -253,14 +286,14 @@ def safe_print_step(step, big):
 | 
			
		||||
    sleep(0.25)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def default_input(text, default):
 | 
			
		||||
def default_input(text: str, default: str) -> str:
 | 
			
		||||
    safe_print()
 | 
			
		||||
    safe_print(f"Press ENTER for default ({default})")
 | 
			
		||||
    return safe_input(text.format(default)) or default
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# From https://stackoverflow.com/a/518232/8924614
 | 
			
		||||
def strip_accents(value):
 | 
			
		||||
def strip_accents(value: str) -> str:
 | 
			
		||||
    return "".join(
 | 
			
		||||
        c
 | 
			
		||||
        for c in unicodedata.normalize("NFD", str(value))
 | 
			
		||||
@@ -268,7 +301,7 @@ def strip_accents(value):
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def wizard(path):
 | 
			
		||||
def wizard(path: str) -> int:
 | 
			
		||||
    from esphome.components.bk72xx import boards as bk72xx_boards
 | 
			
		||||
    from esphome.components.esp32 import boards as esp32_boards
 | 
			
		||||
    from esphome.components.esp8266 import boards as esp8266_boards
 | 
			
		||||
@@ -509,6 +542,7 @@ def wizard(path):
 | 
			
		||||
        ssid=ssid,
 | 
			
		||||
        psk=psk,
 | 
			
		||||
        password=password,
 | 
			
		||||
        type="basic",
 | 
			
		||||
    ):
 | 
			
		||||
        return 1
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -315,6 +315,19 @@ def clean_build():
 | 
			
		||||
        _LOGGER.info("Deleting %s", dependencies_lock)
 | 
			
		||||
        os.remove(dependencies_lock)
 | 
			
		||||
 | 
			
		||||
    # Clean PlatformIO cache to resolve CMake compiler detection issues
 | 
			
		||||
    # This helps when toolchain paths change or get corrupted
 | 
			
		||||
    try:
 | 
			
		||||
        from platformio.project.helpers import get_project_cache_dir
 | 
			
		||||
    except ImportError:
 | 
			
		||||
        # PlatformIO is not available, skip cache cleaning
 | 
			
		||||
        pass
 | 
			
		||||
    else:
 | 
			
		||||
        cache_dir = get_project_cache_dir()
 | 
			
		||||
        if cache_dir and cache_dir.strip() and os.path.isdir(cache_dir):
 | 
			
		||||
            _LOGGER.info("Deleting PlatformIO cache %s", cache_dir)
 | 
			
		||||
            shutil.rmtree(cache_dir)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
GITIGNORE_CONTENT = """# Gitignore settings for ESPHome
 | 
			
		||||
# This is an example and may include too much for your use-case.
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,7 @@ platformio==6.1.18  # When updating platformio, also update /docker/Dockerfile
 | 
			
		||||
esptool==5.0.2
 | 
			
		||||
click==8.1.7
 | 
			
		||||
esphome-dashboard==20250904.0
 | 
			
		||||
aioesphomeapi==40.1.0
 | 
			
		||||
aioesphomeapi==40.2.1
 | 
			
		||||
zeroconf==0.147.2
 | 
			
		||||
puremagic==1.30
 | 
			
		||||
ruamel.yaml==0.18.15 # dashboard_import
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										42
									
								
								tests/components/mdns/test-comprehensive.esp8266-ard.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								tests/components/mdns/test-comprehensive.esp8266-ard.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,42 @@
 | 
			
		||||
# Comprehensive ESP8266 test for mdns with multiple network components
 | 
			
		||||
# Tests the complete priority chain:
 | 
			
		||||
# wifi (60) -> mdns (55) -> ota (54) -> web_server_ota (52)
 | 
			
		||||
 | 
			
		||||
esphome:
 | 
			
		||||
  name: mdns-comprehensive-test
 | 
			
		||||
 | 
			
		||||
esp8266:
 | 
			
		||||
  board: esp01_1m
 | 
			
		||||
 | 
			
		||||
logger:
 | 
			
		||||
  level: DEBUG
 | 
			
		||||
 | 
			
		||||
wifi:
 | 
			
		||||
  ssid: MySSID
 | 
			
		||||
  password: password1
 | 
			
		||||
 | 
			
		||||
# web_server_base should run at priority 65 (before wifi)
 | 
			
		||||
web_server:
 | 
			
		||||
  port: 80
 | 
			
		||||
 | 
			
		||||
# mdns should run at priority 55 (after wifi at 60)
 | 
			
		||||
mdns:
 | 
			
		||||
  services:
 | 
			
		||||
    - service: _http
 | 
			
		||||
      protocol: _tcp
 | 
			
		||||
      port: 80
 | 
			
		||||
 | 
			
		||||
# OTA should run at priority 54 (after mdns)
 | 
			
		||||
ota:
 | 
			
		||||
  - platform: esphome
 | 
			
		||||
    password: "otapassword"
 | 
			
		||||
 | 
			
		||||
# Test status LED at priority 80
 | 
			
		||||
status_led:
 | 
			
		||||
  pin:
 | 
			
		||||
    number: GPIO2
 | 
			
		||||
    inverted: true
 | 
			
		||||
 | 
			
		||||
# Include API at priority 40
 | 
			
		||||
api:
 | 
			
		||||
  password: "apipassword"
 | 
			
		||||
@@ -1,13 +1,16 @@
 | 
			
		||||
from __future__ import annotations
 | 
			
		||||
 | 
			
		||||
import asyncio
 | 
			
		||||
from collections.abc import Generator
 | 
			
		||||
import gzip
 | 
			
		||||
import json
 | 
			
		||||
import os
 | 
			
		||||
from unittest.mock import Mock
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
from unittest.mock import MagicMock, Mock, patch
 | 
			
		||||
 | 
			
		||||
import pytest
 | 
			
		||||
import pytest_asyncio
 | 
			
		||||
from tornado.httpclient import AsyncHTTPClient, HTTPResponse
 | 
			
		||||
from tornado.httpclient import AsyncHTTPClient, HTTPClientError, HTTPResponse
 | 
			
		||||
from tornado.httpserver import HTTPServer
 | 
			
		||||
from tornado.ioloop import IOLoop
 | 
			
		||||
from tornado.testing import bind_unused_port
 | 
			
		||||
@@ -34,6 +37,66 @@ class DashboardTestHelper:
 | 
			
		||||
        return await future
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture
 | 
			
		||||
def mock_async_run_system_command() -> Generator[MagicMock]:
 | 
			
		||||
    """Fixture to mock async_run_system_command."""
 | 
			
		||||
    with patch("esphome.dashboard.web_server.async_run_system_command") as mock:
 | 
			
		||||
        yield mock
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture
 | 
			
		||||
def mock_trash_storage_path(tmp_path: Path) -> Generator[MagicMock]:
 | 
			
		||||
    """Fixture to mock trash_storage_path."""
 | 
			
		||||
    trash_dir = tmp_path / "trash"
 | 
			
		||||
    with patch(
 | 
			
		||||
        "esphome.dashboard.web_server.trash_storage_path", return_value=str(trash_dir)
 | 
			
		||||
    ) as mock:
 | 
			
		||||
        yield mock
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture
 | 
			
		||||
def mock_archive_storage_path(tmp_path: Path) -> Generator[MagicMock]:
 | 
			
		||||
    """Fixture to mock archive_storage_path."""
 | 
			
		||||
    archive_dir = tmp_path / "archive"
 | 
			
		||||
    with patch(
 | 
			
		||||
        "esphome.dashboard.web_server.archive_storage_path",
 | 
			
		||||
        return_value=str(archive_dir),
 | 
			
		||||
    ) as mock:
 | 
			
		||||
        yield mock
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture
 | 
			
		||||
def mock_dashboard_settings() -> Generator[MagicMock]:
 | 
			
		||||
    """Fixture to mock dashboard settings."""
 | 
			
		||||
    with patch("esphome.dashboard.web_server.settings") as mock_settings:
 | 
			
		||||
        # Set default auth settings to avoid authentication issues
 | 
			
		||||
        mock_settings.using_auth = False
 | 
			
		||||
        mock_settings.on_ha_addon = False
 | 
			
		||||
        yield mock_settings
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture
 | 
			
		||||
def mock_ext_storage_path(tmp_path: Path) -> Generator[MagicMock]:
 | 
			
		||||
    """Fixture to mock ext_storage_path."""
 | 
			
		||||
    with patch("esphome.dashboard.web_server.ext_storage_path") as mock:
 | 
			
		||||
        mock.return_value = str(tmp_path / "storage.json")
 | 
			
		||||
        yield mock
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture
 | 
			
		||||
def mock_storage_json() -> Generator[MagicMock]:
 | 
			
		||||
    """Fixture to mock StorageJSON."""
 | 
			
		||||
    with patch("esphome.dashboard.web_server.StorageJSON") as mock:
 | 
			
		||||
        yield mock
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture
 | 
			
		||||
def mock_idedata() -> Generator[MagicMock]:
 | 
			
		||||
    """Fixture to mock platformio_api.IDEData."""
 | 
			
		||||
    with patch("esphome.dashboard.web_server.platformio_api.IDEData") as mock:
 | 
			
		||||
        yield mock
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest_asyncio.fixture()
 | 
			
		||||
async def dashboard() -> DashboardTestHelper:
 | 
			
		||||
    sock, port = bind_unused_port()
 | 
			
		||||
@@ -80,3 +143,590 @@ async def test_devices_page(dashboard: DashboardTestHelper) -> None:
 | 
			
		||||
    first_device = configured_devices[0]
 | 
			
		||||
    assert first_device["name"] == "pico"
 | 
			
		||||
    assert first_device["configuration"] == "pico.yaml"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.asyncio
 | 
			
		||||
async def test_wizard_handler_invalid_input(dashboard: DashboardTestHelper) -> None:
 | 
			
		||||
    """Test the WizardRequestHandler.post method with invalid inputs."""
 | 
			
		||||
    # Test with missing name (should fail with 422)
 | 
			
		||||
    body_no_name = json.dumps(
 | 
			
		||||
        {
 | 
			
		||||
            "name": "",  # Empty name
 | 
			
		||||
            "platform": "ESP32",
 | 
			
		||||
            "board": "esp32dev",
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    with pytest.raises(HTTPClientError) as exc_info:
 | 
			
		||||
        await dashboard.fetch(
 | 
			
		||||
            "/wizard",
 | 
			
		||||
            method="POST",
 | 
			
		||||
            body=body_no_name,
 | 
			
		||||
            headers={"Content-Type": "application/json"},
 | 
			
		||||
        )
 | 
			
		||||
    assert exc_info.value.code == 422
 | 
			
		||||
 | 
			
		||||
    # Test with invalid wizard type (should fail with 422)
 | 
			
		||||
    body_invalid_type = json.dumps(
 | 
			
		||||
        {
 | 
			
		||||
            "name": "test_device",
 | 
			
		||||
            "type": "invalid_type",
 | 
			
		||||
            "platform": "ESP32",
 | 
			
		||||
            "board": "esp32dev",
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    with pytest.raises(HTTPClientError) as exc_info:
 | 
			
		||||
        await dashboard.fetch(
 | 
			
		||||
            "/wizard",
 | 
			
		||||
            method="POST",
 | 
			
		||||
            body=body_invalid_type,
 | 
			
		||||
            headers={"Content-Type": "application/json"},
 | 
			
		||||
        )
 | 
			
		||||
    assert exc_info.value.code == 422
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.asyncio
 | 
			
		||||
async def test_wizard_handler_conflict(dashboard: DashboardTestHelper) -> None:
 | 
			
		||||
    """Test the WizardRequestHandler.post when config already exists."""
 | 
			
		||||
    # Try to create a wizard for existing pico.yaml (should conflict)
 | 
			
		||||
    body = json.dumps(
 | 
			
		||||
        {
 | 
			
		||||
            "name": "pico",  # This already exists in fixtures
 | 
			
		||||
            "platform": "ESP32",
 | 
			
		||||
            "board": "esp32dev",
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    with pytest.raises(HTTPClientError) as exc_info:
 | 
			
		||||
        await dashboard.fetch(
 | 
			
		||||
            "/wizard",
 | 
			
		||||
            method="POST",
 | 
			
		||||
            body=body,
 | 
			
		||||
            headers={"Content-Type": "application/json"},
 | 
			
		||||
        )
 | 
			
		||||
    assert exc_info.value.code == 409
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.asyncio
 | 
			
		||||
async def test_download_binary_handler_not_found(
 | 
			
		||||
    dashboard: DashboardTestHelper,
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test the DownloadBinaryRequestHandler.get with non-existent config."""
 | 
			
		||||
    with pytest.raises(HTTPClientError) as exc_info:
 | 
			
		||||
        await dashboard.fetch(
 | 
			
		||||
            "/download.bin?configuration=nonexistent.yaml",
 | 
			
		||||
            method="GET",
 | 
			
		||||
        )
 | 
			
		||||
    assert exc_info.value.code == 404
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.asyncio
 | 
			
		||||
@pytest.mark.usefixtures("mock_ext_storage_path")
 | 
			
		||||
async def test_download_binary_handler_no_file_param(
 | 
			
		||||
    dashboard: DashboardTestHelper,
 | 
			
		||||
    tmp_path: Path,
 | 
			
		||||
    mock_storage_json: MagicMock,
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test the DownloadBinaryRequestHandler.get without file parameter."""
 | 
			
		||||
    # Mock storage to exist, but still should fail without file param
 | 
			
		||||
    mock_storage = Mock()
 | 
			
		||||
    mock_storage.name = "test_device"
 | 
			
		||||
    mock_storage.firmware_bin_path = str(tmp_path / "firmware.bin")
 | 
			
		||||
    mock_storage_json.load.return_value = mock_storage
 | 
			
		||||
 | 
			
		||||
    with pytest.raises(HTTPClientError) as exc_info:
 | 
			
		||||
        await dashboard.fetch(
 | 
			
		||||
            "/download.bin?configuration=pico.yaml",
 | 
			
		||||
            method="GET",
 | 
			
		||||
        )
 | 
			
		||||
    assert exc_info.value.code == 400
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.asyncio
 | 
			
		||||
@pytest.mark.usefixtures("mock_ext_storage_path")
 | 
			
		||||
async def test_download_binary_handler_with_file(
 | 
			
		||||
    dashboard: DashboardTestHelper,
 | 
			
		||||
    tmp_path: Path,
 | 
			
		||||
    mock_storage_json: MagicMock,
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test the DownloadBinaryRequestHandler.get with existing binary file."""
 | 
			
		||||
    # Create a fake binary file
 | 
			
		||||
    build_dir = tmp_path / ".esphome" / "build" / "test"
 | 
			
		||||
    build_dir.mkdir(parents=True)
 | 
			
		||||
    firmware_file = build_dir / "firmware.bin"
 | 
			
		||||
    firmware_file.write_bytes(b"fake firmware content")
 | 
			
		||||
 | 
			
		||||
    # Mock storage JSON
 | 
			
		||||
    mock_storage = Mock()
 | 
			
		||||
    mock_storage.name = "test_device"
 | 
			
		||||
    mock_storage.firmware_bin_path = str(firmware_file)
 | 
			
		||||
    mock_storage_json.load.return_value = mock_storage
 | 
			
		||||
 | 
			
		||||
    response = await dashboard.fetch(
 | 
			
		||||
        "/download.bin?configuration=test.yaml&file=firmware.bin",
 | 
			
		||||
        method="GET",
 | 
			
		||||
    )
 | 
			
		||||
    assert response.code == 200
 | 
			
		||||
    assert response.body == b"fake firmware content"
 | 
			
		||||
    assert response.headers["Content-Type"] == "application/octet-stream"
 | 
			
		||||
    assert "attachment" in response.headers["Content-Disposition"]
 | 
			
		||||
    assert "test_device-firmware.bin" in response.headers["Content-Disposition"]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.asyncio
 | 
			
		||||
@pytest.mark.usefixtures("mock_ext_storage_path")
 | 
			
		||||
async def test_download_binary_handler_compressed(
 | 
			
		||||
    dashboard: DashboardTestHelper,
 | 
			
		||||
    tmp_path: Path,
 | 
			
		||||
    mock_storage_json: MagicMock,
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test the DownloadBinaryRequestHandler.get with compression."""
 | 
			
		||||
    # Create a fake binary file
 | 
			
		||||
    build_dir = tmp_path / ".esphome" / "build" / "test"
 | 
			
		||||
    build_dir.mkdir(parents=True)
 | 
			
		||||
    firmware_file = build_dir / "firmware.bin"
 | 
			
		||||
    original_content = b"fake firmware content for compression test"
 | 
			
		||||
    firmware_file.write_bytes(original_content)
 | 
			
		||||
 | 
			
		||||
    # Mock storage JSON
 | 
			
		||||
    mock_storage = Mock()
 | 
			
		||||
    mock_storage.name = "test_device"
 | 
			
		||||
    mock_storage.firmware_bin_path = str(firmware_file)
 | 
			
		||||
    mock_storage_json.load.return_value = mock_storage
 | 
			
		||||
 | 
			
		||||
    response = await dashboard.fetch(
 | 
			
		||||
        "/download.bin?configuration=test.yaml&file=firmware.bin&compressed=1",
 | 
			
		||||
        method="GET",
 | 
			
		||||
    )
 | 
			
		||||
    assert response.code == 200
 | 
			
		||||
    # Decompress and verify content
 | 
			
		||||
    decompressed = gzip.decompress(response.body)
 | 
			
		||||
    assert decompressed == original_content
 | 
			
		||||
    assert response.headers["Content-Type"] == "application/octet-stream"
 | 
			
		||||
    assert "firmware.bin.gz" in response.headers["Content-Disposition"]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.asyncio
 | 
			
		||||
@pytest.mark.usefixtures("mock_ext_storage_path")
 | 
			
		||||
async def test_download_binary_handler_custom_download_name(
 | 
			
		||||
    dashboard: DashboardTestHelper,
 | 
			
		||||
    tmp_path: Path,
 | 
			
		||||
    mock_storage_json: MagicMock,
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test the DownloadBinaryRequestHandler.get with custom download name."""
 | 
			
		||||
    # Create a fake binary file
 | 
			
		||||
    build_dir = tmp_path / ".esphome" / "build" / "test"
 | 
			
		||||
    build_dir.mkdir(parents=True)
 | 
			
		||||
    firmware_file = build_dir / "firmware.bin"
 | 
			
		||||
    firmware_file.write_bytes(b"content")
 | 
			
		||||
 | 
			
		||||
    # Mock storage JSON
 | 
			
		||||
    mock_storage = Mock()
 | 
			
		||||
    mock_storage.name = "test_device"
 | 
			
		||||
    mock_storage.firmware_bin_path = str(firmware_file)
 | 
			
		||||
    mock_storage_json.load.return_value = mock_storage
 | 
			
		||||
 | 
			
		||||
    response = await dashboard.fetch(
 | 
			
		||||
        "/download.bin?configuration=test.yaml&file=firmware.bin&download=custom_name.bin",
 | 
			
		||||
        method="GET",
 | 
			
		||||
    )
 | 
			
		||||
    assert response.code == 200
 | 
			
		||||
    assert "custom_name.bin" in response.headers["Content-Disposition"]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.asyncio
 | 
			
		||||
@pytest.mark.usefixtures("mock_ext_storage_path")
 | 
			
		||||
async def test_download_binary_handler_idedata_fallback(
 | 
			
		||||
    dashboard: DashboardTestHelper,
 | 
			
		||||
    tmp_path: Path,
 | 
			
		||||
    mock_async_run_system_command: MagicMock,
 | 
			
		||||
    mock_storage_json: MagicMock,
 | 
			
		||||
    mock_idedata: MagicMock,
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test the DownloadBinaryRequestHandler.get falling back to idedata for extra images."""
 | 
			
		||||
    # Create build directory but no bootloader file initially
 | 
			
		||||
    build_dir = tmp_path / ".esphome" / "build" / "test"
 | 
			
		||||
    build_dir.mkdir(parents=True)
 | 
			
		||||
    firmware_file = build_dir / "firmware.bin"
 | 
			
		||||
    firmware_file.write_bytes(b"firmware")
 | 
			
		||||
 | 
			
		||||
    # Create bootloader file that idedata will find
 | 
			
		||||
    bootloader_file = tmp_path / "bootloader.bin"
 | 
			
		||||
    bootloader_file.write_bytes(b"bootloader content")
 | 
			
		||||
 | 
			
		||||
    # Mock storage JSON
 | 
			
		||||
    mock_storage = Mock()
 | 
			
		||||
    mock_storage.name = "test_device"
 | 
			
		||||
    mock_storage.firmware_bin_path = str(firmware_file)
 | 
			
		||||
    mock_storage_json.load.return_value = mock_storage
 | 
			
		||||
 | 
			
		||||
    # Mock idedata response
 | 
			
		||||
    mock_image = Mock()
 | 
			
		||||
    mock_image.path = str(bootloader_file)
 | 
			
		||||
    mock_idedata_instance = Mock()
 | 
			
		||||
    mock_idedata_instance.extra_flash_images = [mock_image]
 | 
			
		||||
    mock_idedata.return_value = mock_idedata_instance
 | 
			
		||||
 | 
			
		||||
    # Mock async_run_system_command to return idedata JSON
 | 
			
		||||
    mock_async_run_system_command.return_value = (0, '{"extra_flash_images": []}', "")
 | 
			
		||||
 | 
			
		||||
    response = await dashboard.fetch(
 | 
			
		||||
        "/download.bin?configuration=test.yaml&file=bootloader.bin",
 | 
			
		||||
        method="GET",
 | 
			
		||||
    )
 | 
			
		||||
    assert response.code == 200
 | 
			
		||||
    assert response.body == b"bootloader content"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.asyncio
 | 
			
		||||
async def test_edit_request_handler_post_invalid_file(
 | 
			
		||||
    dashboard: DashboardTestHelper,
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test the EditRequestHandler.post with non-yaml file."""
 | 
			
		||||
    with pytest.raises(HTTPClientError) as exc_info:
 | 
			
		||||
        await dashboard.fetch(
 | 
			
		||||
            "/edit?configuration=test.txt",
 | 
			
		||||
            method="POST",
 | 
			
		||||
            body=b"content",
 | 
			
		||||
        )
 | 
			
		||||
    assert exc_info.value.code == 404
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.asyncio
 | 
			
		||||
async def test_edit_request_handler_post_existing(
 | 
			
		||||
    dashboard: DashboardTestHelper,
 | 
			
		||||
    tmp_path: Path,
 | 
			
		||||
    mock_dashboard_settings: MagicMock,
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test the EditRequestHandler.post with existing yaml file."""
 | 
			
		||||
    # Create a temporary yaml file to edit (don't modify fixtures)
 | 
			
		||||
    test_file = tmp_path / "test_edit.yaml"
 | 
			
		||||
    test_file.write_text("esphome:\n  name: original\n")
 | 
			
		||||
 | 
			
		||||
    # Configure the mock settings
 | 
			
		||||
    mock_dashboard_settings.rel_path.return_value = str(test_file)
 | 
			
		||||
    mock_dashboard_settings.absolute_config_dir = test_file.parent
 | 
			
		||||
 | 
			
		||||
    new_content = "esphome:\n  name: modified\n"
 | 
			
		||||
    response = await dashboard.fetch(
 | 
			
		||||
        "/edit?configuration=test_edit.yaml",
 | 
			
		||||
        method="POST",
 | 
			
		||||
        body=new_content.encode(),
 | 
			
		||||
    )
 | 
			
		||||
    assert response.code == 200
 | 
			
		||||
 | 
			
		||||
    # Verify the file was actually modified
 | 
			
		||||
    assert test_file.read_text() == new_content
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.asyncio
 | 
			
		||||
async def test_unarchive_request_handler(
 | 
			
		||||
    dashboard: DashboardTestHelper,
 | 
			
		||||
    mock_archive_storage_path: MagicMock,
 | 
			
		||||
    mock_dashboard_settings: MagicMock,
 | 
			
		||||
    tmp_path: Path,
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test the UnArchiveRequestHandler.post method."""
 | 
			
		||||
    # Set up an archived file
 | 
			
		||||
    archive_dir = Path(mock_archive_storage_path.return_value)
 | 
			
		||||
    archive_dir.mkdir(parents=True, exist_ok=True)
 | 
			
		||||
    archived_file = archive_dir / "archived.yaml"
 | 
			
		||||
    archived_file.write_text("test content")
 | 
			
		||||
 | 
			
		||||
    # Set up the destination path where the file should be moved
 | 
			
		||||
    config_dir = tmp_path / "config"
 | 
			
		||||
    config_dir.mkdir(parents=True, exist_ok=True)
 | 
			
		||||
    destination_file = config_dir / "archived.yaml"
 | 
			
		||||
    mock_dashboard_settings.rel_path.return_value = str(destination_file)
 | 
			
		||||
 | 
			
		||||
    response = await dashboard.fetch(
 | 
			
		||||
        "/unarchive?configuration=archived.yaml",
 | 
			
		||||
        method="POST",
 | 
			
		||||
        body=b"",
 | 
			
		||||
    )
 | 
			
		||||
    assert response.code == 200
 | 
			
		||||
 | 
			
		||||
    # Verify the file was actually moved from archive to config
 | 
			
		||||
    assert not archived_file.exists()  # File should be gone from archive
 | 
			
		||||
    assert destination_file.exists()  # File should now be in config
 | 
			
		||||
    assert destination_file.read_text() == "test content"  # Content preserved
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.asyncio
 | 
			
		||||
async def test_secret_keys_handler_no_file(dashboard: DashboardTestHelper) -> None:
 | 
			
		||||
    """Test the SecretKeysRequestHandler.get when no secrets file exists."""
 | 
			
		||||
    # By default, there's no secrets file in the test fixtures
 | 
			
		||||
    with pytest.raises(HTTPClientError) as exc_info:
 | 
			
		||||
        await dashboard.fetch("/secret_keys", method="GET")
 | 
			
		||||
    assert exc_info.value.code == 404
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.asyncio
 | 
			
		||||
async def test_secret_keys_handler_with_file(
 | 
			
		||||
    dashboard: DashboardTestHelper,
 | 
			
		||||
    tmp_path: Path,
 | 
			
		||||
    mock_dashboard_settings: MagicMock,
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test the SecretKeysRequestHandler.get when secrets file exists."""
 | 
			
		||||
    # Create a secrets file in temp directory
 | 
			
		||||
    secrets_file = tmp_path / "secrets.yaml"
 | 
			
		||||
    secrets_file.write_text(
 | 
			
		||||
        "wifi_ssid: TestNetwork\nwifi_password: TestPass123\napi_key: test_key\n"
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    # Configure mock to return our temp secrets file
 | 
			
		||||
    # Since the file actually exists, os.path.isfile will return True naturally
 | 
			
		||||
    mock_dashboard_settings.rel_path.return_value = str(secrets_file)
 | 
			
		||||
 | 
			
		||||
    response = await dashboard.fetch("/secret_keys", method="GET")
 | 
			
		||||
    assert response.code == 200
 | 
			
		||||
    data = json.loads(response.body.decode())
 | 
			
		||||
    assert "wifi_ssid" in data
 | 
			
		||||
    assert "wifi_password" in data
 | 
			
		||||
    assert "api_key" in data
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.asyncio
 | 
			
		||||
async def test_json_config_handler(
 | 
			
		||||
    dashboard: DashboardTestHelper,
 | 
			
		||||
    mock_async_run_system_command: MagicMock,
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test the JsonConfigRequestHandler.get method."""
 | 
			
		||||
    # This will actually run the esphome config command on pico.yaml
 | 
			
		||||
    mock_output = json.dumps(
 | 
			
		||||
        {
 | 
			
		||||
            "esphome": {"name": "pico"},
 | 
			
		||||
            "esp32": {"board": "esp32dev"},
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    mock_async_run_system_command.return_value = (0, mock_output, "")
 | 
			
		||||
 | 
			
		||||
    response = await dashboard.fetch(
 | 
			
		||||
        "/json-config?configuration=pico.yaml", method="GET"
 | 
			
		||||
    )
 | 
			
		||||
    assert response.code == 200
 | 
			
		||||
    data = json.loads(response.body.decode())
 | 
			
		||||
    assert data["esphome"]["name"] == "pico"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.asyncio
 | 
			
		||||
async def test_json_config_handler_invalid_config(
 | 
			
		||||
    dashboard: DashboardTestHelper,
 | 
			
		||||
    mock_async_run_system_command: MagicMock,
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test the JsonConfigRequestHandler.get with invalid config."""
 | 
			
		||||
    # Simulate esphome config command failure
 | 
			
		||||
    mock_async_run_system_command.return_value = (1, "", "Error: Invalid configuration")
 | 
			
		||||
 | 
			
		||||
    with pytest.raises(HTTPClientError) as exc_info:
 | 
			
		||||
        await dashboard.fetch("/json-config?configuration=pico.yaml", method="GET")
 | 
			
		||||
    assert exc_info.value.code == 422
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.asyncio
 | 
			
		||||
async def test_json_config_handler_not_found(dashboard: DashboardTestHelper) -> None:
 | 
			
		||||
    """Test the JsonConfigRequestHandler.get with non-existent file."""
 | 
			
		||||
    with pytest.raises(HTTPClientError) as exc_info:
 | 
			
		||||
        await dashboard.fetch(
 | 
			
		||||
            "/json-config?configuration=nonexistent.yaml", method="GET"
 | 
			
		||||
        )
 | 
			
		||||
    assert exc_info.value.code == 404
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_start_web_server_with_address_port(
 | 
			
		||||
    tmp_path: Path,
 | 
			
		||||
    mock_trash_storage_path: MagicMock,
 | 
			
		||||
    mock_archive_storage_path: MagicMock,
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test the start_web_server function with address and port."""
 | 
			
		||||
    app = Mock()
 | 
			
		||||
    trash_dir = Path(mock_trash_storage_path.return_value)
 | 
			
		||||
    archive_dir = Path(mock_archive_storage_path.return_value)
 | 
			
		||||
 | 
			
		||||
    # Create trash dir to test migration
 | 
			
		||||
    trash_dir.mkdir()
 | 
			
		||||
    (trash_dir / "old.yaml").write_text("old")
 | 
			
		||||
 | 
			
		||||
    web_server.start_web_server(app, None, "127.0.0.1", 6052, str(tmp_path / "config"))
 | 
			
		||||
 | 
			
		||||
    # The function calls app.listen directly for non-socket mode
 | 
			
		||||
    app.listen.assert_called_once_with(6052, "127.0.0.1")
 | 
			
		||||
 | 
			
		||||
    # Verify trash was moved to archive
 | 
			
		||||
    assert not trash_dir.exists()
 | 
			
		||||
    assert archive_dir.exists()
 | 
			
		||||
    assert (archive_dir / "old.yaml").exists()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.asyncio
 | 
			
		||||
async def test_edit_request_handler_get(dashboard: DashboardTestHelper) -> None:
 | 
			
		||||
    """Test EditRequestHandler.get method."""
 | 
			
		||||
    # Test getting a valid yaml file
 | 
			
		||||
    response = await dashboard.fetch("/edit?configuration=pico.yaml")
 | 
			
		||||
    assert response.code == 200
 | 
			
		||||
    assert response.headers["content-type"] == "application/yaml"
 | 
			
		||||
    content = response.body.decode()
 | 
			
		||||
    assert "esphome:" in content  # Verify it's a valid ESPHome config
 | 
			
		||||
 | 
			
		||||
    # Test getting a non-existent file
 | 
			
		||||
    with pytest.raises(HTTPClientError) as exc_info:
 | 
			
		||||
        await dashboard.fetch("/edit?configuration=nonexistent.yaml")
 | 
			
		||||
    assert exc_info.value.code == 404
 | 
			
		||||
 | 
			
		||||
    # Test getting a non-yaml file
 | 
			
		||||
    with pytest.raises(HTTPClientError) as exc_info:
 | 
			
		||||
        await dashboard.fetch("/edit?configuration=test.txt")
 | 
			
		||||
    assert exc_info.value.code == 404
 | 
			
		||||
 | 
			
		||||
    # Test path traversal attempt
 | 
			
		||||
    with pytest.raises(HTTPClientError) as exc_info:
 | 
			
		||||
        await dashboard.fetch("/edit?configuration=../../../etc/passwd")
 | 
			
		||||
    assert exc_info.value.code == 404
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.asyncio
 | 
			
		||||
async def test_archive_request_handler_post(
 | 
			
		||||
    dashboard: DashboardTestHelper,
 | 
			
		||||
    mock_archive_storage_path: MagicMock,
 | 
			
		||||
    mock_ext_storage_path: MagicMock,
 | 
			
		||||
    tmp_path: Path,
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test ArchiveRequestHandler.post method without storage_json."""
 | 
			
		||||
 | 
			
		||||
    # Set up temp directories
 | 
			
		||||
    config_dir = Path(get_fixture_path("conf"))
 | 
			
		||||
    archive_dir = tmp_path / "archive"
 | 
			
		||||
 | 
			
		||||
    # Create a test configuration file
 | 
			
		||||
    test_config = config_dir / "test_archive.yaml"
 | 
			
		||||
    test_config.write_text("esphome:\n  name: test_archive\n")
 | 
			
		||||
 | 
			
		||||
    # Archive the configuration
 | 
			
		||||
    response = await dashboard.fetch(
 | 
			
		||||
        "/archive",
 | 
			
		||||
        method="POST",
 | 
			
		||||
        body="configuration=test_archive.yaml",
 | 
			
		||||
        headers={"Content-Type": "application/x-www-form-urlencoded"},
 | 
			
		||||
    )
 | 
			
		||||
    assert response.code == 200
 | 
			
		||||
 | 
			
		||||
    # Verify file was moved to archive
 | 
			
		||||
    assert not test_config.exists()
 | 
			
		||||
    assert (archive_dir / "test_archive.yaml").exists()
 | 
			
		||||
    assert (
 | 
			
		||||
        archive_dir / "test_archive.yaml"
 | 
			
		||||
    ).read_text() == "esphome:\n  name: test_archive\n"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.asyncio
 | 
			
		||||
async def test_archive_handler_with_build_folder(
 | 
			
		||||
    dashboard: DashboardTestHelper,
 | 
			
		||||
    mock_archive_storage_path: MagicMock,
 | 
			
		||||
    mock_ext_storage_path: MagicMock,
 | 
			
		||||
    mock_dashboard_settings: MagicMock,
 | 
			
		||||
    mock_storage_json: MagicMock,
 | 
			
		||||
    tmp_path: Path,
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test ArchiveRequestHandler.post with storage_json and build folder."""
 | 
			
		||||
    config_dir = tmp_path / "config"
 | 
			
		||||
    config_dir.mkdir()
 | 
			
		||||
    archive_dir = tmp_path / "archive"
 | 
			
		||||
    archive_dir.mkdir()
 | 
			
		||||
    build_dir = tmp_path / "build"
 | 
			
		||||
    build_dir.mkdir()
 | 
			
		||||
 | 
			
		||||
    configuration = "test_device.yaml"
 | 
			
		||||
    test_config = config_dir / configuration
 | 
			
		||||
    test_config.write_text("esphome:\n  name: test_device\n")
 | 
			
		||||
 | 
			
		||||
    build_folder = build_dir / "test_device"
 | 
			
		||||
    build_folder.mkdir()
 | 
			
		||||
    (build_folder / "firmware.bin").write_text("binary content")
 | 
			
		||||
    (build_folder / ".pioenvs").mkdir()
 | 
			
		||||
 | 
			
		||||
    mock_dashboard_settings.config_dir = str(config_dir)
 | 
			
		||||
    mock_dashboard_settings.rel_path.return_value = str(test_config)
 | 
			
		||||
    mock_archive_storage_path.return_value = str(archive_dir)
 | 
			
		||||
 | 
			
		||||
    mock_storage = MagicMock()
 | 
			
		||||
    mock_storage.name = "test_device"
 | 
			
		||||
    mock_storage.build_path = str(build_folder)
 | 
			
		||||
    mock_storage_json.load.return_value = mock_storage
 | 
			
		||||
 | 
			
		||||
    response = await dashboard.fetch(
 | 
			
		||||
        "/archive",
 | 
			
		||||
        method="POST",
 | 
			
		||||
        body=f"configuration={configuration}",
 | 
			
		||||
        headers={"Content-Type": "application/x-www-form-urlencoded"},
 | 
			
		||||
    )
 | 
			
		||||
    assert response.code == 200
 | 
			
		||||
 | 
			
		||||
    assert not test_config.exists()
 | 
			
		||||
    assert (archive_dir / configuration).exists()
 | 
			
		||||
 | 
			
		||||
    assert not build_folder.exists()
 | 
			
		||||
    assert not (archive_dir / "test_device").exists()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.asyncio
 | 
			
		||||
async def test_archive_handler_no_build_folder(
 | 
			
		||||
    dashboard: DashboardTestHelper,
 | 
			
		||||
    mock_archive_storage_path: MagicMock,
 | 
			
		||||
    mock_ext_storage_path: MagicMock,
 | 
			
		||||
    mock_dashboard_settings: MagicMock,
 | 
			
		||||
    mock_storage_json: MagicMock,
 | 
			
		||||
    tmp_path: Path,
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test ArchiveRequestHandler.post with storage_json but no build folder."""
 | 
			
		||||
    config_dir = tmp_path / "config"
 | 
			
		||||
    config_dir.mkdir()
 | 
			
		||||
    archive_dir = tmp_path / "archive"
 | 
			
		||||
    archive_dir.mkdir()
 | 
			
		||||
 | 
			
		||||
    configuration = "test_device.yaml"
 | 
			
		||||
    test_config = config_dir / configuration
 | 
			
		||||
    test_config.write_text("esphome:\n  name: test_device\n")
 | 
			
		||||
 | 
			
		||||
    mock_dashboard_settings.config_dir = str(config_dir)
 | 
			
		||||
    mock_dashboard_settings.rel_path.return_value = str(test_config)
 | 
			
		||||
    mock_archive_storage_path.return_value = str(archive_dir)
 | 
			
		||||
 | 
			
		||||
    mock_storage = MagicMock()
 | 
			
		||||
    mock_storage.name = "test_device"
 | 
			
		||||
    mock_storage.build_path = None
 | 
			
		||||
    mock_storage_json.load.return_value = mock_storage
 | 
			
		||||
 | 
			
		||||
    response = await dashboard.fetch(
 | 
			
		||||
        "/archive",
 | 
			
		||||
        method="POST",
 | 
			
		||||
        body=f"configuration={configuration}",
 | 
			
		||||
        headers={"Content-Type": "application/x-www-form-urlencoded"},
 | 
			
		||||
    )
 | 
			
		||||
    assert response.code == 200
 | 
			
		||||
 | 
			
		||||
    assert not test_config.exists()
 | 
			
		||||
    assert (archive_dir / configuration).exists()
 | 
			
		||||
    assert not (archive_dir / "test_device").exists()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.skipif(os.name == "nt", reason="Unix sockets are not supported on Windows")
 | 
			
		||||
@pytest.mark.usefixtures("mock_trash_storage_path", "mock_archive_storage_path")
 | 
			
		||||
def test_start_web_server_with_unix_socket(tmp_path: Path) -> None:
 | 
			
		||||
    """Test the start_web_server function with unix socket."""
 | 
			
		||||
    app = Mock()
 | 
			
		||||
    socket_path = tmp_path / "test.sock"
 | 
			
		||||
 | 
			
		||||
    # Don't create trash_dir - it doesn't exist, so no migration needed
 | 
			
		||||
    with (
 | 
			
		||||
        patch("tornado.httpserver.HTTPServer") as mock_server_class,
 | 
			
		||||
        patch("tornado.netutil.bind_unix_socket") as mock_bind,
 | 
			
		||||
    ):
 | 
			
		||||
        server = Mock()
 | 
			
		||||
        mock_server_class.return_value = server
 | 
			
		||||
        mock_bind.return_value = Mock()
 | 
			
		||||
 | 
			
		||||
        web_server.start_web_server(
 | 
			
		||||
            app, str(socket_path), None, None, str(tmp_path / "config")
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        mock_server_class.assert_called_once_with(app)
 | 
			
		||||
        mock_bind.assert_called_once_with(str(socket_path), mode=0o666)
 | 
			
		||||
        server.add_socket.assert_called_once()
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,10 @@
 | 
			
		||||
esphome:
 | 
			
		||||
  name: noise-key-test
 | 
			
		||||
 | 
			
		||||
host:
 | 
			
		||||
 | 
			
		||||
api:
 | 
			
		||||
  encryption:
 | 
			
		||||
    key: "zX9/JHxMKwpP0jUGsF0iESCm1wRvNgR6NkKVOhn7kSs="
 | 
			
		||||
 | 
			
		||||
logger:
 | 
			
		||||
							
								
								
									
										51
									
								
								tests/integration/test_noise_encryption_key_protection.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								tests/integration/test_noise_encryption_key_protection.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,51 @@
 | 
			
		||||
"""Integration test for noise encryption key protection from YAML."""
 | 
			
		||||
 | 
			
		||||
from __future__ import annotations
 | 
			
		||||
 | 
			
		||||
import base64
 | 
			
		||||
 | 
			
		||||
from aioesphomeapi import InvalidEncryptionKeyAPIError
 | 
			
		||||
import pytest
 | 
			
		||||
 | 
			
		||||
from .types import APIClientConnectedFactory, RunCompiledFunction
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.asyncio
 | 
			
		||||
async def test_noise_encryption_key_protection(
 | 
			
		||||
    yaml_config: str,
 | 
			
		||||
    run_compiled: RunCompiledFunction,
 | 
			
		||||
    api_client_connected: APIClientConnectedFactory,
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test that noise encryption key set in YAML cannot be changed via API."""
 | 
			
		||||
    # The key that's set in the YAML fixture
 | 
			
		||||
    noise_psk = "zX9/JHxMKwpP0jUGsF0iESCm1wRvNgR6NkKVOhn7kSs="
 | 
			
		||||
 | 
			
		||||
    # Keep ESPHome process running throughout all tests
 | 
			
		||||
    async with run_compiled(yaml_config):
 | 
			
		||||
        # First connection - test key change attempt
 | 
			
		||||
        async with api_client_connected(noise_psk=noise_psk) as client:
 | 
			
		||||
            # Verify connection is established
 | 
			
		||||
            device_info = await client.device_info()
 | 
			
		||||
            assert device_info is not None
 | 
			
		||||
 | 
			
		||||
            # Try to set a new encryption key via API
 | 
			
		||||
            new_key = base64.b64encode(
 | 
			
		||||
                b"x" * 32
 | 
			
		||||
            )  # Valid 32-byte key in base64 as bytes
 | 
			
		||||
 | 
			
		||||
            # This should fail since key was set in YAML
 | 
			
		||||
            success = await client.noise_encryption_set_key(new_key)
 | 
			
		||||
            assert success is False
 | 
			
		||||
 | 
			
		||||
        # Reconnect with the original key to verify it still works
 | 
			
		||||
        async with api_client_connected(noise_psk=noise_psk) as client:
 | 
			
		||||
            # Verify connection is still successful with original key
 | 
			
		||||
            device_info = await client.device_info()
 | 
			
		||||
            assert device_info is not None
 | 
			
		||||
            assert device_info.name == "noise-key-test"
 | 
			
		||||
 | 
			
		||||
        # Verify that connecting with a wrong key fails
 | 
			
		||||
        wrong_key = base64.b64encode(b"y" * 32).decode()  # Different key
 | 
			
		||||
        with pytest.raises(InvalidEncryptionKeyAPIError):
 | 
			
		||||
            async with api_client_connected(noise_psk=wrong_key) as client:
 | 
			
		||||
                await client.device_info()
 | 
			
		||||
@@ -9,8 +9,10 @@ not be part of a unit test suite.
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
from collections.abc import Generator
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
import sys
 | 
			
		||||
from unittest.mock import Mock, patch
 | 
			
		||||
 | 
			
		||||
import pytest
 | 
			
		||||
 | 
			
		||||
@@ -36,3 +38,52 @@ 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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture
 | 
			
		||||
def mock_write_file_if_changed() -> Generator[Mock, None, None]:
 | 
			
		||||
    """Mock write_file_if_changed for storage_json."""
 | 
			
		||||
    with patch("esphome.storage_json.write_file_if_changed") as mock:
 | 
			
		||||
        yield mock
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture
 | 
			
		||||
def mock_copy_file_if_changed() -> Generator[Mock, None, None]:
 | 
			
		||||
    """Mock copy_file_if_changed for core.config."""
 | 
			
		||||
    with patch("esphome.core.config.copy_file_if_changed") as mock:
 | 
			
		||||
        yield mock
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture
 | 
			
		||||
def mock_run_platformio_cli() -> Generator[Mock, None, None]:
 | 
			
		||||
    """Mock run_platformio_cli for platformio_api."""
 | 
			
		||||
    with patch("esphome.platformio_api.run_platformio_cli") as mock:
 | 
			
		||||
        yield mock
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture
 | 
			
		||||
def mock_run_platformio_cli_run() -> Generator[Mock, None, None]:
 | 
			
		||||
    """Mock run_platformio_cli_run for platformio_api."""
 | 
			
		||||
    with patch("esphome.platformio_api.run_platformio_cli_run") as mock:
 | 
			
		||||
        yield mock
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture
 | 
			
		||||
def mock_decode_pc() -> Generator[Mock, None, None]:
 | 
			
		||||
    """Mock _decode_pc for platformio_api."""
 | 
			
		||||
    with patch("esphome.platformio_api._decode_pc") as mock:
 | 
			
		||||
        yield mock
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture
 | 
			
		||||
def mock_run_external_command() -> Generator[Mock, None, None]:
 | 
			
		||||
    """Mock run_external_command for platformio_api."""
 | 
			
		||||
    with patch("esphome.platformio_api.run_external_command") as mock:
 | 
			
		||||
        yield mock
 | 
			
		||||
 
 | 
			
		||||
@@ -1,15 +1,34 @@
 | 
			
		||||
"""Unit tests for core config functionality including areas and devices."""
 | 
			
		||||
 | 
			
		||||
from collections.abc import Callable
 | 
			
		||||
import os
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
import types
 | 
			
		||||
from typing import Any
 | 
			
		||||
from unittest.mock import MagicMock, Mock, patch
 | 
			
		||||
 | 
			
		||||
import pytest
 | 
			
		||||
 | 
			
		||||
from esphome import config_validation as cv, core
 | 
			
		||||
from esphome.const import CONF_AREA, CONF_AREAS, CONF_DEVICES
 | 
			
		||||
from esphome.core import config
 | 
			
		||||
from esphome.core.config import Area, validate_area_config
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_AREA,
 | 
			
		||||
    CONF_AREAS,
 | 
			
		||||
    CONF_BUILD_PATH,
 | 
			
		||||
    CONF_DEVICES,
 | 
			
		||||
    CONF_ESPHOME,
 | 
			
		||||
    CONF_NAME,
 | 
			
		||||
    CONF_NAME_ADD_MAC_SUFFIX,
 | 
			
		||||
    KEY_CORE,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import CORE, config
 | 
			
		||||
from esphome.core.config import (
 | 
			
		||||
    Area,
 | 
			
		||||
    preload_core_config,
 | 
			
		||||
    valid_include,
 | 
			
		||||
    valid_project_name,
 | 
			
		||||
    validate_area_config,
 | 
			
		||||
    validate_hostname,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
from .common import load_config_from_fixture
 | 
			
		||||
 | 
			
		||||
@@ -245,3 +264,316 @@ def test_add_platform_defines_priority() -> None:
 | 
			
		||||
        f"_add_platform_defines priority ({config._add_platform_defines.priority}) must be lower than "
 | 
			
		||||
        f"globals priority ({globals_to_code.priority}) to fix issue #10431 (sensor count bug with lambdas)"
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_valid_include_with_angle_brackets() -> None:
 | 
			
		||||
    """Test valid_include accepts angle bracket includes."""
 | 
			
		||||
    assert valid_include("<ArduinoJson.h>") == "<ArduinoJson.h>"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_valid_include_with_valid_file(tmp_path: Path) -> None:
 | 
			
		||||
    """Test valid_include accepts valid include files."""
 | 
			
		||||
    CORE.config_path = str(tmp_path / "test.yaml")
 | 
			
		||||
    include_file = tmp_path / "include.h"
 | 
			
		||||
    include_file.touch()
 | 
			
		||||
 | 
			
		||||
    assert valid_include(str(include_file)) == str(include_file)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_valid_include_with_valid_directory(tmp_path: Path) -> None:
 | 
			
		||||
    """Test valid_include accepts valid directories."""
 | 
			
		||||
    CORE.config_path = str(tmp_path / "test.yaml")
 | 
			
		||||
    include_dir = tmp_path / "includes"
 | 
			
		||||
    include_dir.mkdir()
 | 
			
		||||
 | 
			
		||||
    assert valid_include(str(include_dir)) == str(include_dir)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_valid_include_invalid_extension(tmp_path: Path) -> None:
 | 
			
		||||
    """Test valid_include rejects files with invalid extensions."""
 | 
			
		||||
    CORE.config_path = str(tmp_path / "test.yaml")
 | 
			
		||||
    invalid_file = tmp_path / "file.txt"
 | 
			
		||||
    invalid_file.touch()
 | 
			
		||||
 | 
			
		||||
    with pytest.raises(cv.Invalid, match="Include has invalid file extension"):
 | 
			
		||||
        valid_include(str(invalid_file))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_valid_project_name_valid() -> None:
 | 
			
		||||
    """Test valid_project_name accepts valid project names."""
 | 
			
		||||
    assert valid_project_name("esphome.my_project") == "esphome.my_project"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_valid_project_name_no_namespace() -> None:
 | 
			
		||||
    """Test valid_project_name rejects names without namespace."""
 | 
			
		||||
    with pytest.raises(cv.Invalid, match="project name needs to have a namespace"):
 | 
			
		||||
        valid_project_name("my_project")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_valid_project_name_multiple_dots() -> None:
 | 
			
		||||
    """Test valid_project_name rejects names with multiple dots."""
 | 
			
		||||
    with pytest.raises(cv.Invalid, match="project name needs to have a namespace"):
 | 
			
		||||
        valid_project_name("esphome.my.project")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_validate_hostname_valid() -> None:
 | 
			
		||||
    """Test validate_hostname accepts valid hostnames."""
 | 
			
		||||
    config = {CONF_NAME: "my-device", CONF_NAME_ADD_MAC_SUFFIX: False}
 | 
			
		||||
    assert validate_hostname(config) == config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_validate_hostname_too_long() -> None:
 | 
			
		||||
    """Test validate_hostname rejects hostnames that are too long."""
 | 
			
		||||
    config = {
 | 
			
		||||
        CONF_NAME: "a" * 32,  # 32 chars, max is 31
 | 
			
		||||
        CONF_NAME_ADD_MAC_SUFFIX: False,
 | 
			
		||||
    }
 | 
			
		||||
    with pytest.raises(cv.Invalid, match="Hostnames can only be 31 characters long"):
 | 
			
		||||
        validate_hostname(config)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_validate_hostname_too_long_with_mac_suffix() -> None:
 | 
			
		||||
    """Test validate_hostname accounts for MAC suffix length."""
 | 
			
		||||
    config = {
 | 
			
		||||
        CONF_NAME: "a" * 25,  # 25 chars, max is 24 with MAC suffix
 | 
			
		||||
        CONF_NAME_ADD_MAC_SUFFIX: True,
 | 
			
		||||
    }
 | 
			
		||||
    with pytest.raises(cv.Invalid, match="Hostnames can only be 24 characters long"):
 | 
			
		||||
        validate_hostname(config)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_validate_hostname_with_underscore(caplog) -> None:
 | 
			
		||||
    """Test validate_hostname warns about underscores."""
 | 
			
		||||
    config = {CONF_NAME: "my_device", CONF_NAME_ADD_MAC_SUFFIX: False}
 | 
			
		||||
    assert validate_hostname(config) == config
 | 
			
		||||
    assert (
 | 
			
		||||
        "Using the '_' (underscore) character in the hostname is discouraged"
 | 
			
		||||
        in caplog.text
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_preload_core_config_basic(setup_core: Path) -> None:
 | 
			
		||||
    """Test preload_core_config sets basic CORE attributes."""
 | 
			
		||||
    config = {
 | 
			
		||||
        CONF_ESPHOME: {
 | 
			
		||||
            CONF_NAME: "test_device",
 | 
			
		||||
        },
 | 
			
		||||
        "esp32": {},
 | 
			
		||||
    }
 | 
			
		||||
    result = {}
 | 
			
		||||
 | 
			
		||||
    platform = preload_core_config(config, result)
 | 
			
		||||
 | 
			
		||||
    assert CORE.name == "test_device"
 | 
			
		||||
    assert platform == "esp32"
 | 
			
		||||
    assert KEY_CORE in CORE.data
 | 
			
		||||
    assert CONF_BUILD_PATH in config[CONF_ESPHOME]
 | 
			
		||||
    # Verify default build path is "build/<device_name>"
 | 
			
		||||
    build_path = config[CONF_ESPHOME][CONF_BUILD_PATH]
 | 
			
		||||
    assert build_path.endswith(os.path.join("build", "test_device"))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_preload_core_config_with_build_path(setup_core: Path) -> None:
 | 
			
		||||
    """Test preload_core_config uses provided build path."""
 | 
			
		||||
    config = {
 | 
			
		||||
        CONF_ESPHOME: {
 | 
			
		||||
            CONF_NAME: "test_device",
 | 
			
		||||
            CONF_BUILD_PATH: "/custom/build/path",
 | 
			
		||||
        },
 | 
			
		||||
        "esp8266": {},
 | 
			
		||||
    }
 | 
			
		||||
    result = {}
 | 
			
		||||
 | 
			
		||||
    platform = preload_core_config(config, result)
 | 
			
		||||
 | 
			
		||||
    assert config[CONF_ESPHOME][CONF_BUILD_PATH] == "/custom/build/path"
 | 
			
		||||
    assert platform == "esp8266"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_preload_core_config_env_build_path(setup_core: Path) -> None:
 | 
			
		||||
    """Test preload_core_config uses ESPHOME_BUILD_PATH env var."""
 | 
			
		||||
    config = {
 | 
			
		||||
        CONF_ESPHOME: {
 | 
			
		||||
            CONF_NAME: "test_device",
 | 
			
		||||
        },
 | 
			
		||||
        "rp2040": {},
 | 
			
		||||
    }
 | 
			
		||||
    result = {}
 | 
			
		||||
 | 
			
		||||
    with patch.dict(os.environ, {"ESPHOME_BUILD_PATH": "/env/build"}):
 | 
			
		||||
        platform = preload_core_config(config, result)
 | 
			
		||||
 | 
			
		||||
    assert CONF_BUILD_PATH in config[CONF_ESPHOME]
 | 
			
		||||
    assert "test_device" in config[CONF_ESPHOME][CONF_BUILD_PATH]
 | 
			
		||||
    # Verify it uses the env var path with device name appended
 | 
			
		||||
    build_path = config[CONF_ESPHOME][CONF_BUILD_PATH]
 | 
			
		||||
    expected_path = os.path.join("/env/build", "test_device")
 | 
			
		||||
    assert build_path == expected_path or build_path == expected_path.replace(
 | 
			
		||||
        "/", os.sep
 | 
			
		||||
    )
 | 
			
		||||
    assert platform == "rp2040"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_preload_core_config_no_platform(setup_core: Path) -> None:
 | 
			
		||||
    """Test preload_core_config raises when no platform is specified."""
 | 
			
		||||
    config = {
 | 
			
		||||
        CONF_ESPHOME: {
 | 
			
		||||
            CONF_NAME: "test_device",
 | 
			
		||||
        },
 | 
			
		||||
    }
 | 
			
		||||
    result = {}
 | 
			
		||||
 | 
			
		||||
    # Mock _is_target_platform to avoid expensive component loading
 | 
			
		||||
    with patch("esphome.core.config._is_target_platform") as mock_is_platform:
 | 
			
		||||
        # Return True for known platforms
 | 
			
		||||
        mock_is_platform.side_effect = lambda name: name in [
 | 
			
		||||
            "esp32",
 | 
			
		||||
            "esp8266",
 | 
			
		||||
            "rp2040",
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        with pytest.raises(cv.Invalid, match="Platform missing"):
 | 
			
		||||
            preload_core_config(config, result)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_preload_core_config_multiple_platforms(setup_core: Path) -> None:
 | 
			
		||||
    """Test preload_core_config raises when multiple platforms are specified."""
 | 
			
		||||
    config = {
 | 
			
		||||
        CONF_ESPHOME: {
 | 
			
		||||
            CONF_NAME: "test_device",
 | 
			
		||||
        },
 | 
			
		||||
        "esp32": {},
 | 
			
		||||
        "esp8266": {},
 | 
			
		||||
    }
 | 
			
		||||
    result = {}
 | 
			
		||||
 | 
			
		||||
    # Mock _is_target_platform to avoid expensive component loading
 | 
			
		||||
    with patch("esphome.core.config._is_target_platform") as mock_is_platform:
 | 
			
		||||
        # Return True for known platforms
 | 
			
		||||
        mock_is_platform.side_effect = lambda name: name in [
 | 
			
		||||
            "esp32",
 | 
			
		||||
            "esp8266",
 | 
			
		||||
            "rp2040",
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        with pytest.raises(cv.Invalid, match="Found multiple target platform blocks"):
 | 
			
		||||
            preload_core_config(config, result)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_include_file_header(tmp_path: Path, mock_copy_file_if_changed: Mock) -> None:
 | 
			
		||||
    """Test include_file adds include statement for header files."""
 | 
			
		||||
    src_file = tmp_path / "source.h"
 | 
			
		||||
    src_file.write_text("// Header content")
 | 
			
		||||
 | 
			
		||||
    CORE.build_path = str(tmp_path / "build")
 | 
			
		||||
 | 
			
		||||
    with patch("esphome.core.config.cg") as mock_cg:
 | 
			
		||||
        # Mock RawStatement to capture the text
 | 
			
		||||
        mock_raw_statement = MagicMock()
 | 
			
		||||
        mock_raw_statement.text = ""
 | 
			
		||||
 | 
			
		||||
        def raw_statement_side_effect(text):
 | 
			
		||||
            mock_raw_statement.text = text
 | 
			
		||||
            return mock_raw_statement
 | 
			
		||||
 | 
			
		||||
        mock_cg.RawStatement.side_effect = raw_statement_side_effect
 | 
			
		||||
 | 
			
		||||
        config.include_file(str(src_file), "test.h")
 | 
			
		||||
 | 
			
		||||
        mock_copy_file_if_changed.assert_called_once()
 | 
			
		||||
        mock_cg.add_global.assert_called_once()
 | 
			
		||||
        # Check that include statement was added
 | 
			
		||||
        assert '#include "test.h"' in mock_raw_statement.text
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_include_file_cpp(tmp_path: Path, mock_copy_file_if_changed: Mock) -> None:
 | 
			
		||||
    """Test include_file does not add include for cpp files."""
 | 
			
		||||
    src_file = tmp_path / "source.cpp"
 | 
			
		||||
    src_file.write_text("// CPP content")
 | 
			
		||||
 | 
			
		||||
    CORE.build_path = str(tmp_path / "build")
 | 
			
		||||
 | 
			
		||||
    with patch("esphome.core.config.cg") as mock_cg:
 | 
			
		||||
        config.include_file(str(src_file), "test.cpp")
 | 
			
		||||
 | 
			
		||||
        mock_copy_file_if_changed.assert_called_once()
 | 
			
		||||
        # Should not add include statement for .cpp files
 | 
			
		||||
        mock_cg.add_global.assert_not_called()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_get_usable_cpu_count() -> None:
 | 
			
		||||
    """Test get_usable_cpu_count returns CPU count."""
 | 
			
		||||
    count = config.get_usable_cpu_count()
 | 
			
		||||
    assert isinstance(count, int)
 | 
			
		||||
    assert count > 0
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_get_usable_cpu_count_with_process_cpu_count() -> None:
 | 
			
		||||
    """Test get_usable_cpu_count uses process_cpu_count when available."""
 | 
			
		||||
    # Test with process_cpu_count (Python 3.13+)
 | 
			
		||||
    # Create a mock os module with process_cpu_count
 | 
			
		||||
 | 
			
		||||
    mock_os = types.SimpleNamespace(process_cpu_count=lambda: 8, cpu_count=lambda: 4)
 | 
			
		||||
 | 
			
		||||
    with patch("esphome.core.config.os", mock_os):
 | 
			
		||||
        # When process_cpu_count exists, it should be used
 | 
			
		||||
        count = config.get_usable_cpu_count()
 | 
			
		||||
        assert count == 8
 | 
			
		||||
 | 
			
		||||
    # Test fallback to cpu_count when process_cpu_count not available
 | 
			
		||||
    mock_os_no_process = types.SimpleNamespace(cpu_count=lambda: 4)
 | 
			
		||||
 | 
			
		||||
    with patch("esphome.core.config.os", mock_os_no_process):
 | 
			
		||||
        count = config.get_usable_cpu_count()
 | 
			
		||||
        assert count == 4
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_list_target_platforms(tmp_path: Path) -> None:
 | 
			
		||||
    """Test _list_target_platforms returns available platforms."""
 | 
			
		||||
    # Create mock components directory structure
 | 
			
		||||
    components_dir = tmp_path / "components"
 | 
			
		||||
    components_dir.mkdir()
 | 
			
		||||
 | 
			
		||||
    # Create platform and non-platform directories with __init__.py
 | 
			
		||||
    platforms = ["esp32", "esp8266", "rp2040", "libretiny", "host"]
 | 
			
		||||
    non_platforms = ["sensor"]
 | 
			
		||||
 | 
			
		||||
    for component in platforms + non_platforms:
 | 
			
		||||
        component_dir = components_dir / component
 | 
			
		||||
        component_dir.mkdir()
 | 
			
		||||
        (component_dir / "__init__.py").touch()
 | 
			
		||||
 | 
			
		||||
    # Create a file (not a directory)
 | 
			
		||||
    (components_dir / "README.md").touch()
 | 
			
		||||
 | 
			
		||||
    # Create a directory without __init__.py
 | 
			
		||||
    (components_dir / "no_init").mkdir()
 | 
			
		||||
 | 
			
		||||
    # Mock Path(__file__).parents[1] to return our tmp_path
 | 
			
		||||
    with patch("esphome.core.config.Path") as mock_path:
 | 
			
		||||
        mock_file_path = MagicMock()
 | 
			
		||||
        mock_file_path.parents = [MagicMock(), tmp_path]
 | 
			
		||||
        mock_path.return_value = mock_file_path
 | 
			
		||||
 | 
			
		||||
        platforms = config._list_target_platforms()
 | 
			
		||||
 | 
			
		||||
    assert isinstance(platforms, list)
 | 
			
		||||
    # Should include platform components
 | 
			
		||||
    assert "esp32" in platforms
 | 
			
		||||
    assert "esp8266" in platforms
 | 
			
		||||
    assert "rp2040" in platforms
 | 
			
		||||
    assert "libretiny" in platforms
 | 
			
		||||
    assert "host" in platforms
 | 
			
		||||
    # Should not include non-platform components
 | 
			
		||||
    assert "sensor" not in platforms
 | 
			
		||||
    assert "README.md" not in platforms
 | 
			
		||||
    assert "no_init" not in platforms
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_is_target_platform() -> None:
 | 
			
		||||
    """Test _is_target_platform identifies valid platforms."""
 | 
			
		||||
    assert config._is_target_platform("esp32") is True
 | 
			
		||||
    assert config._is_target_platform("esp8266") is True
 | 
			
		||||
    assert config._is_target_platform("rp2040") is True
 | 
			
		||||
    assert config._is_target_platform("invalid_platform") is False
 | 
			
		||||
    assert config._is_target_platform("api") is False  # Component but not platform
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										187
									
								
								tests/unit_tests/test_config_validation_paths.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										187
									
								
								tests/unit_tests/test_config_validation_paths.py
									
									
									
									
									
										Normal file
									
								
							@@ -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"
 | 
			
		||||
@@ -13,7 +13,12 @@ def test_coro_priority_enum_values() -> None:
 | 
			
		||||
    assert CoroPriority.CORE == 100
 | 
			
		||||
    assert CoroPriority.DIAGNOSTICS == 90
 | 
			
		||||
    assert CoroPriority.STATUS == 80
 | 
			
		||||
    assert CoroPriority.WEB_SERVER_BASE == 65
 | 
			
		||||
    assert CoroPriority.CAPTIVE_PORTAL == 64
 | 
			
		||||
    assert CoroPriority.COMMUNICATION == 60
 | 
			
		||||
    assert CoroPriority.NETWORK_SERVICES == 55
 | 
			
		||||
    assert CoroPriority.OTA_UPDATES == 54
 | 
			
		||||
    assert CoroPriority.WEB_SERVER_OTA == 52
 | 
			
		||||
    assert CoroPriority.APPLICATION == 50
 | 
			
		||||
    assert CoroPriority.WEB == 40
 | 
			
		||||
    assert CoroPriority.AUTOMATION == 30
 | 
			
		||||
@@ -70,7 +75,12 @@ def test_float_and_enum_are_interchangeable() -> None:
 | 
			
		||||
        (CoroPriority.CORE, 100.0),
 | 
			
		||||
        (CoroPriority.DIAGNOSTICS, 90.0),
 | 
			
		||||
        (CoroPriority.STATUS, 80.0),
 | 
			
		||||
        (CoroPriority.WEB_SERVER_BASE, 65.0),
 | 
			
		||||
        (CoroPriority.CAPTIVE_PORTAL, 64.0),
 | 
			
		||||
        (CoroPriority.COMMUNICATION, 60.0),
 | 
			
		||||
        (CoroPriority.NETWORK_SERVICES, 55.0),
 | 
			
		||||
        (CoroPriority.OTA_UPDATES, 54.0),
 | 
			
		||||
        (CoroPriority.WEB_SERVER_OTA, 52.0),
 | 
			
		||||
        (CoroPriority.APPLICATION, 50.0),
 | 
			
		||||
        (CoroPriority.WEB, 40.0),
 | 
			
		||||
        (CoroPriority.AUTOMATION, 30.0),
 | 
			
		||||
@@ -164,8 +174,13 @@ def test_enum_priority_comparison() -> None:
 | 
			
		||||
    assert CoroPriority.NETWORK_TRANSPORT > CoroPriority.CORE
 | 
			
		||||
    assert CoroPriority.CORE > CoroPriority.DIAGNOSTICS
 | 
			
		||||
    assert CoroPriority.DIAGNOSTICS > CoroPriority.STATUS
 | 
			
		||||
    assert CoroPriority.STATUS > CoroPriority.COMMUNICATION
 | 
			
		||||
    assert CoroPriority.COMMUNICATION > CoroPriority.APPLICATION
 | 
			
		||||
    assert CoroPriority.STATUS > CoroPriority.WEB_SERVER_BASE
 | 
			
		||||
    assert CoroPriority.WEB_SERVER_BASE > CoroPriority.CAPTIVE_PORTAL
 | 
			
		||||
    assert CoroPriority.CAPTIVE_PORTAL > CoroPriority.COMMUNICATION
 | 
			
		||||
    assert CoroPriority.COMMUNICATION > CoroPriority.NETWORK_SERVICES
 | 
			
		||||
    assert CoroPriority.NETWORK_SERVICES > CoroPriority.OTA_UPDATES
 | 
			
		||||
    assert CoroPriority.OTA_UPDATES > CoroPriority.WEB_SERVER_OTA
 | 
			
		||||
    assert CoroPriority.WEB_SERVER_OTA > CoroPriority.APPLICATION
 | 
			
		||||
    assert CoroPriority.APPLICATION > CoroPriority.WEB
 | 
			
		||||
    assert CoroPriority.WEB > CoroPriority.AUTOMATION
 | 
			
		||||
    assert CoroPriority.AUTOMATION > CoroPriority.BUS
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										196
									
								
								tests/unit_tests/test_external_files.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										196
									
								
								tests/unit_tests/test_external_files.py
									
									
									
									
									
										Normal file
									
								
							@@ -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
 | 
			
		||||
							
								
								
									
										1545
									
								
								tests/unit_tests/test_main.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1545
									
								
								tests/unit_tests/test_main.py
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										636
									
								
								tests/unit_tests/test_platformio_api.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										636
									
								
								tests/unit_tests/test_platformio_api.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,636 @@
 | 
			
		||||
"""Tests for platformio_api.py path functions."""
 | 
			
		||||
 | 
			
		||||
import json
 | 
			
		||||
import os
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
import shutil
 | 
			
		||||
from types import SimpleNamespace
 | 
			
		||||
from unittest.mock import MagicMock, Mock, patch
 | 
			
		||||
 | 
			
		||||
import pytest
 | 
			
		||||
 | 
			
		||||
from esphome import platformio_api
 | 
			
		||||
from esphome.core import CORE, EsphomeError
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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, mock_run_platformio_cli_run
 | 
			
		||||
) -> 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"}')
 | 
			
		||||
 | 
			
		||||
    mock_run_platformio_cli_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"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_load_idedata_uses_cache_when_valid(
 | 
			
		||||
    setup_core: Path, mock_run_platformio_cli_run: Mock
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test _load_idedata uses cached data when unchanged."""
 | 
			
		||||
    CORE.build_path = str(setup_core / "build" / "test")
 | 
			
		||||
    CORE.name = "test"
 | 
			
		||||
 | 
			
		||||
    # Create platformio.ini
 | 
			
		||||
    platformio_ini = setup_core / "build" / "test" / "platformio.ini"
 | 
			
		||||
    platformio_ini.parent.mkdir(parents=True, exist_ok=True)
 | 
			
		||||
    platformio_ini.write_text("content")
 | 
			
		||||
 | 
			
		||||
    # Create idedata cache file that's newer
 | 
			
		||||
    idedata_path = setup_core / ".esphome" / "idedata" / "test.json"
 | 
			
		||||
    idedata_path.parent.mkdir(parents=True, exist_ok=True)
 | 
			
		||||
    idedata_path.write_text('{"prog_path": "/cached/firmware.elf"}')
 | 
			
		||||
 | 
			
		||||
    # Make idedata newer than platformio.ini
 | 
			
		||||
    platformio_ini_mtime = platformio_ini.stat().st_mtime
 | 
			
		||||
    os.utime(idedata_path, (platformio_ini_mtime + 1, platformio_ini_mtime + 1))
 | 
			
		||||
 | 
			
		||||
    config = {"name": "test"}
 | 
			
		||||
    result = platformio_api._load_idedata(config)
 | 
			
		||||
 | 
			
		||||
    # Should not call _run_idedata since cache is valid
 | 
			
		||||
    mock_run_platformio_cli_run.assert_not_called()
 | 
			
		||||
 | 
			
		||||
    assert result["prog_path"] == "/cached/firmware.elf"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_load_idedata_regenerates_when_platformio_ini_newer(
 | 
			
		||||
    setup_core: Path, mock_run_platformio_cli_run: Mock
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test _load_idedata regenerates when platformio.ini is newer."""
 | 
			
		||||
    CORE.build_path = str(setup_core / "build" / "test")
 | 
			
		||||
    CORE.name = "test"
 | 
			
		||||
 | 
			
		||||
    # Create idedata cache file first
 | 
			
		||||
    idedata_path = setup_core / ".esphome" / "idedata" / "test.json"
 | 
			
		||||
    idedata_path.parent.mkdir(parents=True, exist_ok=True)
 | 
			
		||||
    idedata_path.write_text('{"prog_path": "/old/firmware.elf"}')
 | 
			
		||||
 | 
			
		||||
    # Create platformio.ini that's newer
 | 
			
		||||
    idedata_mtime = idedata_path.stat().st_mtime
 | 
			
		||||
    platformio_ini = setup_core / "build" / "test" / "platformio.ini"
 | 
			
		||||
    platformio_ini.parent.mkdir(parents=True, exist_ok=True)
 | 
			
		||||
    platformio_ini.write_text("content")
 | 
			
		||||
    # Make platformio.ini newer than idedata
 | 
			
		||||
    os.utime(platformio_ini, (idedata_mtime + 1, idedata_mtime + 1))
 | 
			
		||||
 | 
			
		||||
    # Mock platformio to return new data
 | 
			
		||||
    new_data = {"prog_path": "/new/firmware.elf"}
 | 
			
		||||
    mock_run_platformio_cli_run.return_value = json.dumps(new_data)
 | 
			
		||||
 | 
			
		||||
    config = {"name": "test"}
 | 
			
		||||
    result = platformio_api._load_idedata(config)
 | 
			
		||||
 | 
			
		||||
    # Should call _run_idedata since platformio.ini is newer
 | 
			
		||||
    mock_run_platformio_cli_run.assert_called_once()
 | 
			
		||||
 | 
			
		||||
    assert result["prog_path"] == "/new/firmware.elf"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_load_idedata_regenerates_on_corrupted_cache(
 | 
			
		||||
    setup_core: Path, mock_run_platformio_cli_run: Mock
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test _load_idedata regenerates when cache file is corrupted."""
 | 
			
		||||
    CORE.build_path = str(setup_core / "build" / "test")
 | 
			
		||||
    CORE.name = "test"
 | 
			
		||||
 | 
			
		||||
    # Create platformio.ini
 | 
			
		||||
    platformio_ini = setup_core / "build" / "test" / "platformio.ini"
 | 
			
		||||
    platformio_ini.parent.mkdir(parents=True, exist_ok=True)
 | 
			
		||||
    platformio_ini.write_text("content")
 | 
			
		||||
 | 
			
		||||
    # Create corrupted idedata cache file
 | 
			
		||||
    idedata_path = setup_core / ".esphome" / "idedata" / "test.json"
 | 
			
		||||
    idedata_path.parent.mkdir(parents=True, exist_ok=True)
 | 
			
		||||
    idedata_path.write_text('{"prog_path": invalid json')
 | 
			
		||||
 | 
			
		||||
    # Make idedata newer so it would be used if valid
 | 
			
		||||
    platformio_ini_mtime = platformio_ini.stat().st_mtime
 | 
			
		||||
    os.utime(idedata_path, (platformio_ini_mtime + 1, platformio_ini_mtime + 1))
 | 
			
		||||
 | 
			
		||||
    # Mock platformio to return new data
 | 
			
		||||
    new_data = {"prog_path": "/new/firmware.elf"}
 | 
			
		||||
    mock_run_platformio_cli_run.return_value = json.dumps(new_data)
 | 
			
		||||
 | 
			
		||||
    config = {"name": "test"}
 | 
			
		||||
    result = platformio_api._load_idedata(config)
 | 
			
		||||
 | 
			
		||||
    # Should call _run_idedata since cache is corrupted
 | 
			
		||||
    mock_run_platformio_cli_run.assert_called_once()
 | 
			
		||||
 | 
			
		||||
    assert result["prog_path"] == "/new/firmware.elf"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_run_idedata_parses_json_from_output(
 | 
			
		||||
    setup_core: Path, mock_run_platformio_cli_run: Mock
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test _run_idedata extracts JSON from platformio output."""
 | 
			
		||||
    config = {"name": "test"}
 | 
			
		||||
 | 
			
		||||
    expected_data = {
 | 
			
		||||
        "prog_path": "/path/to/firmware.elf",
 | 
			
		||||
        "cc_path": "/path/to/gcc",
 | 
			
		||||
        "extra": {"flash_images": []},
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    # Simulate platformio output with JSON embedded
 | 
			
		||||
    mock_run_platformio_cli_run.return_value = (
 | 
			
		||||
        f"Some preamble\n{json.dumps(expected_data)}\nSome postamble"
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    result = platformio_api._run_idedata(config)
 | 
			
		||||
 | 
			
		||||
    assert result == expected_data
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_run_idedata_raises_on_no_json(
 | 
			
		||||
    setup_core: Path, mock_run_platformio_cli_run: Mock
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test _run_idedata raises EsphomeError when no JSON found."""
 | 
			
		||||
    config = {"name": "test"}
 | 
			
		||||
 | 
			
		||||
    mock_run_platformio_cli_run.return_value = "No JSON in this output"
 | 
			
		||||
 | 
			
		||||
    with pytest.raises(EsphomeError):
 | 
			
		||||
        platformio_api._run_idedata(config)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_run_idedata_raises_on_invalid_json(
 | 
			
		||||
    setup_core: Path, mock_run_platformio_cli_run: Mock
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test _run_idedata raises on malformed JSON."""
 | 
			
		||||
    config = {"name": "test"}
 | 
			
		||||
    mock_run_platformio_cli_run.return_value = '{"invalid": json"}'
 | 
			
		||||
 | 
			
		||||
    # The ValueError from json.loads is re-raised
 | 
			
		||||
    with pytest.raises(ValueError):
 | 
			
		||||
        platformio_api._run_idedata(config)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_run_platformio_cli_sets_environment_variables(
 | 
			
		||||
    setup_core: Path, mock_run_external_command: Mock
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test run_platformio_cli sets correct environment variables."""
 | 
			
		||||
    CORE.build_path = str(setup_core / "build" / "test")
 | 
			
		||||
 | 
			
		||||
    with patch.dict(os.environ, {}, clear=False):
 | 
			
		||||
        mock_run_external_command.return_value = 0
 | 
			
		||||
        platformio_api.run_platformio_cli("test", "arg")
 | 
			
		||||
 | 
			
		||||
        # Check environment variables were set
 | 
			
		||||
        assert os.environ["PLATFORMIO_FORCE_COLOR"] == "true"
 | 
			
		||||
        assert (
 | 
			
		||||
            setup_core / "build" / "test"
 | 
			
		||||
            in Path(os.environ["PLATFORMIO_BUILD_DIR"]).parents
 | 
			
		||||
            or Path(os.environ["PLATFORMIO_BUILD_DIR"]) == setup_core / "build" / "test"
 | 
			
		||||
        )
 | 
			
		||||
        assert "PLATFORMIO_LIBDEPS_DIR" in os.environ
 | 
			
		||||
        assert "PYTHONWARNINGS" in os.environ
 | 
			
		||||
 | 
			
		||||
        # Check command was called correctly
 | 
			
		||||
        mock_run_external_command.assert_called_once()
 | 
			
		||||
        args = mock_run_external_command.call_args[0]
 | 
			
		||||
        assert "platformio" in args
 | 
			
		||||
        assert "test" in args
 | 
			
		||||
        assert "arg" in args
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_run_platformio_cli_run_builds_command(
 | 
			
		||||
    setup_core: Path, mock_run_platformio_cli: Mock
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test run_platformio_cli_run builds correct command."""
 | 
			
		||||
    CORE.build_path = str(setup_core / "build" / "test")
 | 
			
		||||
    mock_run_platformio_cli.return_value = 0
 | 
			
		||||
 | 
			
		||||
    config = {"name": "test"}
 | 
			
		||||
    platformio_api.run_platformio_cli_run(config, True, "extra", "args")
 | 
			
		||||
 | 
			
		||||
    mock_run_platformio_cli.assert_called_once_with(
 | 
			
		||||
        "run", "-d", CORE.build_path, "-v", "extra", "args"
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_run_compile(setup_core: Path, mock_run_platformio_cli_run: Mock) -> None:
 | 
			
		||||
    """Test run_compile with process limit."""
 | 
			
		||||
    from esphome.const import CONF_COMPILE_PROCESS_LIMIT, CONF_ESPHOME
 | 
			
		||||
 | 
			
		||||
    CORE.build_path = str(setup_core / "build" / "test")
 | 
			
		||||
    config = {CONF_ESPHOME: {CONF_COMPILE_PROCESS_LIMIT: 4}}
 | 
			
		||||
    mock_run_platformio_cli_run.return_value = 0
 | 
			
		||||
 | 
			
		||||
    platformio_api.run_compile(config, verbose=True)
 | 
			
		||||
 | 
			
		||||
    mock_run_platformio_cli_run.assert_called_once_with(config, True, "-j4")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_get_idedata_caches_result(
 | 
			
		||||
    setup_core: Path, mock_run_platformio_cli_run: Mock
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test get_idedata caches result in CORE.data."""
 | 
			
		||||
    from esphome.const import KEY_CORE
 | 
			
		||||
 | 
			
		||||
    CORE.build_path = str(setup_core / "build" / "test")
 | 
			
		||||
    CORE.name = "test"
 | 
			
		||||
    CORE.data[KEY_CORE] = {}
 | 
			
		||||
 | 
			
		||||
    # Create platformio.ini to avoid regeneration
 | 
			
		||||
    platformio_ini = setup_core / "build" / "test" / "platformio.ini"
 | 
			
		||||
    platformio_ini.parent.mkdir(parents=True, exist_ok=True)
 | 
			
		||||
    platformio_ini.write_text("content")
 | 
			
		||||
 | 
			
		||||
    # Mock platformio to return data
 | 
			
		||||
    idedata = {"prog_path": "/test/firmware.elf"}
 | 
			
		||||
    mock_run_platformio_cli_run.return_value = json.dumps(idedata)
 | 
			
		||||
 | 
			
		||||
    config = {"name": "test"}
 | 
			
		||||
 | 
			
		||||
    # First call should load and cache
 | 
			
		||||
    result1 = platformio_api.get_idedata(config)
 | 
			
		||||
    mock_run_platformio_cli_run.assert_called_once()
 | 
			
		||||
 | 
			
		||||
    # Second call should use cache from CORE.data
 | 
			
		||||
    result2 = platformio_api.get_idedata(config)
 | 
			
		||||
    mock_run_platformio_cli_run.assert_called_once()  # Still only called once
 | 
			
		||||
 | 
			
		||||
    assert result1 is result2
 | 
			
		||||
    assert isinstance(result1, platformio_api.IDEData)
 | 
			
		||||
    assert result1.firmware_elf_path == "/test/firmware.elf"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_idedata_addr2line_path_windows(setup_core: Path) -> None:
 | 
			
		||||
    """Test IDEData.addr2line_path on Windows."""
 | 
			
		||||
    raw_data = {"prog_path": "/path/to/firmware.elf", "cc_path": "C:\\tools\\gcc.exe"}
 | 
			
		||||
    idedata = platformio_api.IDEData(raw_data)
 | 
			
		||||
 | 
			
		||||
    result = idedata.addr2line_path
 | 
			
		||||
    assert result == "C:\\tools\\addr2line.exe"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_idedata_addr2line_path_unix(setup_core: Path) -> None:
 | 
			
		||||
    """Test IDEData.addr2line_path on Unix."""
 | 
			
		||||
    raw_data = {"prog_path": "/path/to/firmware.elf", "cc_path": "/usr/bin/gcc"}
 | 
			
		||||
    idedata = platformio_api.IDEData(raw_data)
 | 
			
		||||
 | 
			
		||||
    result = idedata.addr2line_path
 | 
			
		||||
    assert result == "/usr/bin/addr2line"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_patch_structhash(setup_core: Path) -> None:
 | 
			
		||||
    """Test patch_structhash monkey patches platformio functions."""
 | 
			
		||||
    # Create simple namespace objects to act as modules
 | 
			
		||||
    mock_cli = SimpleNamespace()
 | 
			
		||||
    mock_helpers = SimpleNamespace()
 | 
			
		||||
    mock_run = SimpleNamespace(cli=mock_cli, helpers=mock_helpers)
 | 
			
		||||
 | 
			
		||||
    # Mock platformio modules
 | 
			
		||||
    with patch.dict(
 | 
			
		||||
        "sys.modules",
 | 
			
		||||
        {
 | 
			
		||||
            "platformio.run.cli": mock_cli,
 | 
			
		||||
            "platformio.run.helpers": mock_helpers,
 | 
			
		||||
            "platformio.run": mock_run,
 | 
			
		||||
            "platformio.project.helpers": MagicMock(),
 | 
			
		||||
            "platformio.fs": MagicMock(),
 | 
			
		||||
            "platformio": MagicMock(),
 | 
			
		||||
        },
 | 
			
		||||
    ):
 | 
			
		||||
        # Call patch_structhash
 | 
			
		||||
        platformio_api.patch_structhash()
 | 
			
		||||
 | 
			
		||||
        # Verify both modules had clean_build_dir patched
 | 
			
		||||
        # Check that clean_build_dir was set on both modules
 | 
			
		||||
        assert hasattr(mock_cli, "clean_build_dir")
 | 
			
		||||
        assert hasattr(mock_helpers, "clean_build_dir")
 | 
			
		||||
 | 
			
		||||
        # Verify they got the same function assigned
 | 
			
		||||
        assert mock_cli.clean_build_dir is mock_helpers.clean_build_dir
 | 
			
		||||
 | 
			
		||||
        # Verify it's a real function (not a Mock)
 | 
			
		||||
        assert callable(mock_cli.clean_build_dir)
 | 
			
		||||
        assert mock_cli.clean_build_dir.__name__ == "patched_clean_build_dir"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_patched_clean_build_dir_removes_outdated(setup_core: Path) -> None:
 | 
			
		||||
    """Test patched_clean_build_dir removes build dir when platformio.ini is newer."""
 | 
			
		||||
    build_dir = setup_core / "build"
 | 
			
		||||
    build_dir.mkdir()
 | 
			
		||||
    platformio_ini = setup_core / "platformio.ini"
 | 
			
		||||
    platformio_ini.write_text("config")
 | 
			
		||||
 | 
			
		||||
    # Make platformio.ini newer than build_dir
 | 
			
		||||
    build_mtime = build_dir.stat().st_mtime
 | 
			
		||||
    os.utime(platformio_ini, (build_mtime + 1, build_mtime + 1))
 | 
			
		||||
 | 
			
		||||
    # Track if directory was removed
 | 
			
		||||
    removed_paths: list[str] = []
 | 
			
		||||
 | 
			
		||||
    def track_rmtree(path: str) -> None:
 | 
			
		||||
        removed_paths.append(path)
 | 
			
		||||
        shutil.rmtree(path)
 | 
			
		||||
 | 
			
		||||
    # Create mock modules that patch_structhash expects
 | 
			
		||||
    mock_cli = SimpleNamespace()
 | 
			
		||||
    mock_helpers = SimpleNamespace()
 | 
			
		||||
    mock_project_helpers = MagicMock()
 | 
			
		||||
    mock_project_helpers.get_project_dir.return_value = str(setup_core)
 | 
			
		||||
    mock_fs = SimpleNamespace(rmtree=track_rmtree)
 | 
			
		||||
 | 
			
		||||
    with patch.dict(
 | 
			
		||||
        "sys.modules",
 | 
			
		||||
        {
 | 
			
		||||
            "platformio": SimpleNamespace(fs=mock_fs),
 | 
			
		||||
            "platformio.fs": mock_fs,
 | 
			
		||||
            "platformio.project.helpers": mock_project_helpers,
 | 
			
		||||
            "platformio.run": SimpleNamespace(cli=mock_cli, helpers=mock_helpers),
 | 
			
		||||
            "platformio.run.cli": mock_cli,
 | 
			
		||||
            "platformio.run.helpers": mock_helpers,
 | 
			
		||||
        },
 | 
			
		||||
    ):
 | 
			
		||||
        # Call patch_structhash to install the patched function
 | 
			
		||||
        platformio_api.patch_structhash()
 | 
			
		||||
 | 
			
		||||
        # Call the patched function
 | 
			
		||||
        mock_helpers.clean_build_dir(str(build_dir), [])
 | 
			
		||||
 | 
			
		||||
        # Verify directory was removed and recreated
 | 
			
		||||
        assert len(removed_paths) == 1
 | 
			
		||||
        assert removed_paths[0] == str(build_dir)
 | 
			
		||||
        assert build_dir.exists()  # makedirs recreated it
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_patched_clean_build_dir_keeps_updated(setup_core: Path) -> None:
 | 
			
		||||
    """Test patched_clean_build_dir keeps build dir when it's up to date."""
 | 
			
		||||
    build_dir = setup_core / "build"
 | 
			
		||||
    build_dir.mkdir()
 | 
			
		||||
    test_file = build_dir / "test.txt"
 | 
			
		||||
    test_file.write_text("test content")
 | 
			
		||||
 | 
			
		||||
    platformio_ini = setup_core / "platformio.ini"
 | 
			
		||||
    platformio_ini.write_text("config")
 | 
			
		||||
 | 
			
		||||
    # Make build_dir newer than platformio.ini
 | 
			
		||||
    ini_mtime = platformio_ini.stat().st_mtime
 | 
			
		||||
    os.utime(build_dir, (ini_mtime + 1, ini_mtime + 1))
 | 
			
		||||
 | 
			
		||||
    # Track if rmtree is called
 | 
			
		||||
    removed_paths: list[str] = []
 | 
			
		||||
 | 
			
		||||
    def track_rmtree(path: str) -> None:
 | 
			
		||||
        removed_paths.append(path)
 | 
			
		||||
 | 
			
		||||
    # Create mock modules
 | 
			
		||||
    mock_cli = SimpleNamespace()
 | 
			
		||||
    mock_helpers = SimpleNamespace()
 | 
			
		||||
    mock_project_helpers = MagicMock()
 | 
			
		||||
    mock_project_helpers.get_project_dir.return_value = str(setup_core)
 | 
			
		||||
    mock_fs = SimpleNamespace(rmtree=track_rmtree)
 | 
			
		||||
 | 
			
		||||
    with patch.dict(
 | 
			
		||||
        "sys.modules",
 | 
			
		||||
        {
 | 
			
		||||
            "platformio": SimpleNamespace(fs=mock_fs),
 | 
			
		||||
            "platformio.fs": mock_fs,
 | 
			
		||||
            "platformio.project.helpers": mock_project_helpers,
 | 
			
		||||
            "platformio.run": SimpleNamespace(cli=mock_cli, helpers=mock_helpers),
 | 
			
		||||
            "platformio.run.cli": mock_cli,
 | 
			
		||||
            "platformio.run.helpers": mock_helpers,
 | 
			
		||||
        },
 | 
			
		||||
    ):
 | 
			
		||||
        # Call patch_structhash to install the patched function
 | 
			
		||||
        platformio_api.patch_structhash()
 | 
			
		||||
 | 
			
		||||
        # Call the patched function
 | 
			
		||||
        mock_helpers.clean_build_dir(str(build_dir), [])
 | 
			
		||||
 | 
			
		||||
        # Verify rmtree was NOT called
 | 
			
		||||
        assert len(removed_paths) == 0
 | 
			
		||||
 | 
			
		||||
        # Verify directory and file still exist
 | 
			
		||||
        assert build_dir.exists()
 | 
			
		||||
        assert test_file.exists()
 | 
			
		||||
        assert test_file.read_text() == "test content"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_patched_clean_build_dir_creates_missing(setup_core: Path) -> None:
 | 
			
		||||
    """Test patched_clean_build_dir creates build dir when it doesn't exist."""
 | 
			
		||||
    build_dir = setup_core / "build"
 | 
			
		||||
    platformio_ini = setup_core / "platformio.ini"
 | 
			
		||||
    platformio_ini.write_text("config")
 | 
			
		||||
 | 
			
		||||
    # Ensure build_dir doesn't exist
 | 
			
		||||
    assert not build_dir.exists()
 | 
			
		||||
 | 
			
		||||
    # Track if rmtree is called
 | 
			
		||||
    removed_paths: list[str] = []
 | 
			
		||||
 | 
			
		||||
    def track_rmtree(path: str) -> None:
 | 
			
		||||
        removed_paths.append(path)
 | 
			
		||||
 | 
			
		||||
    # Create mock modules
 | 
			
		||||
    mock_cli = SimpleNamespace()
 | 
			
		||||
    mock_helpers = SimpleNamespace()
 | 
			
		||||
    mock_project_helpers = MagicMock()
 | 
			
		||||
    mock_project_helpers.get_project_dir.return_value = str(setup_core)
 | 
			
		||||
    mock_fs = SimpleNamespace(rmtree=track_rmtree)
 | 
			
		||||
 | 
			
		||||
    with patch.dict(
 | 
			
		||||
        "sys.modules",
 | 
			
		||||
        {
 | 
			
		||||
            "platformio": SimpleNamespace(fs=mock_fs),
 | 
			
		||||
            "platformio.fs": mock_fs,
 | 
			
		||||
            "platformio.project.helpers": mock_project_helpers,
 | 
			
		||||
            "platformio.run": SimpleNamespace(cli=mock_cli, helpers=mock_helpers),
 | 
			
		||||
            "platformio.run.cli": mock_cli,
 | 
			
		||||
            "platformio.run.helpers": mock_helpers,
 | 
			
		||||
        },
 | 
			
		||||
    ):
 | 
			
		||||
        # Call patch_structhash to install the patched function
 | 
			
		||||
        platformio_api.patch_structhash()
 | 
			
		||||
 | 
			
		||||
        # Call the patched function
 | 
			
		||||
        mock_helpers.clean_build_dir(str(build_dir), [])
 | 
			
		||||
 | 
			
		||||
        # Verify rmtree was NOT called
 | 
			
		||||
        assert len(removed_paths) == 0
 | 
			
		||||
 | 
			
		||||
        # Verify directory was created
 | 
			
		||||
        assert build_dir.exists()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_process_stacktrace_esp8266_exception(setup_core: Path, caplog) -> None:
 | 
			
		||||
    """Test process_stacktrace handles ESP8266 exceptions."""
 | 
			
		||||
    config = {"name": "test"}
 | 
			
		||||
 | 
			
		||||
    # Test exception type parsing
 | 
			
		||||
    line = "Exception (28):"
 | 
			
		||||
    backtrace_state = False
 | 
			
		||||
 | 
			
		||||
    result = platformio_api.process_stacktrace(config, line, backtrace_state)
 | 
			
		||||
 | 
			
		||||
    assert "Access to invalid address: LOAD (wild pointer?)" in caplog.text
 | 
			
		||||
    assert result is False
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_process_stacktrace_esp8266_backtrace(
 | 
			
		||||
    setup_core: Path, mock_decode_pc: Mock
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test process_stacktrace handles ESP8266 multi-line backtrace."""
 | 
			
		||||
    config = {"name": "test"}
 | 
			
		||||
 | 
			
		||||
    # Start of backtrace
 | 
			
		||||
    line1 = ">>>stack>>>"
 | 
			
		||||
    state = platformio_api.process_stacktrace(config, line1, False)
 | 
			
		||||
    assert state is True
 | 
			
		||||
 | 
			
		||||
    # Backtrace content with addresses
 | 
			
		||||
    line2 = "40201234 40205678"
 | 
			
		||||
    state = platformio_api.process_stacktrace(config, line2, state)
 | 
			
		||||
    assert state is True
 | 
			
		||||
    assert mock_decode_pc.call_count == 2
 | 
			
		||||
 | 
			
		||||
    # End of backtrace
 | 
			
		||||
    line3 = "<<<stack<<<"
 | 
			
		||||
    state = platformio_api.process_stacktrace(config, line3, state)
 | 
			
		||||
    assert state is False
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_process_stacktrace_esp32_backtrace(
 | 
			
		||||
    setup_core: Path, mock_decode_pc: Mock
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test process_stacktrace handles ESP32 single-line backtrace."""
 | 
			
		||||
    config = {"name": "test"}
 | 
			
		||||
 | 
			
		||||
    line = "Backtrace: 0x40081234:0x3ffb1234 0x40085678:0x3ffb5678"
 | 
			
		||||
    state = platformio_api.process_stacktrace(config, line, False)
 | 
			
		||||
 | 
			
		||||
    # Should decode both addresses
 | 
			
		||||
    assert mock_decode_pc.call_count == 2
 | 
			
		||||
    mock_decode_pc.assert_any_call(config, "40081234")
 | 
			
		||||
    mock_decode_pc.assert_any_call(config, "40085678")
 | 
			
		||||
    assert state is False
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_process_stacktrace_bad_alloc(
 | 
			
		||||
    setup_core: Path, mock_decode_pc: Mock, caplog
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test process_stacktrace handles bad alloc messages."""
 | 
			
		||||
    config = {"name": "test"}
 | 
			
		||||
 | 
			
		||||
    line = "last failed alloc call: 40201234(512)"
 | 
			
		||||
    state = platformio_api.process_stacktrace(config, line, False)
 | 
			
		||||
 | 
			
		||||
    assert "Memory allocation of 512 bytes failed at 40201234" in caplog.text
 | 
			
		||||
    mock_decode_pc.assert_called_once_with(config, "40201234")
 | 
			
		||||
    assert state is False
 | 
			
		||||
							
								
								
									
										660
									
								
								tests/unit_tests/test_storage_json.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										660
									
								
								tests/unit_tests/test_storage_json.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,660 @@
 | 
			
		||||
"""Tests for storage_json.py path functions."""
 | 
			
		||||
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
import json
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
import sys
 | 
			
		||||
from unittest.mock import MagicMock, Mock, patch
 | 
			
		||||
 | 
			
		||||
import pytest
 | 
			
		||||
 | 
			
		||||
from esphome import storage_json
 | 
			
		||||
from esphome.const import CONF_DISABLED, CONF_MDNS
 | 
			
		||||
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, mock_write_file_if_changed: Mock
 | 
			
		||||
) -> 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,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    storage.save(str(storage_file))
 | 
			
		||||
    mock_write_file_if_changed.assert_called_once()
 | 
			
		||||
    call_args = mock_write_file_if_changed.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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_storage_json_as_dict() -> None:
 | 
			
		||||
    """Test StorageJSON.as_dict returns correct dictionary."""
 | 
			
		||||
    storage = storage_json.StorageJSON(
 | 
			
		||||
        storage_version=1,
 | 
			
		||||
        name="test_device",
 | 
			
		||||
        friendly_name="Test Device",
 | 
			
		||||
        comment="Test comment",
 | 
			
		||||
        esphome_version="2024.1.0",
 | 
			
		||||
        src_version=1,
 | 
			
		||||
        address="192.168.1.100",
 | 
			
		||||
        web_port=80,
 | 
			
		||||
        target_platform="ESP32",
 | 
			
		||||
        build_path="/path/to/build",
 | 
			
		||||
        firmware_bin_path="/path/to/firmware.bin",
 | 
			
		||||
        loaded_integrations={"wifi", "api", "ota"},
 | 
			
		||||
        loaded_platforms={"sensor", "binary_sensor"},
 | 
			
		||||
        no_mdns=True,
 | 
			
		||||
        framework="arduino",
 | 
			
		||||
        core_platform="esp32",
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    result = storage.as_dict()
 | 
			
		||||
 | 
			
		||||
    assert result["storage_version"] == 1
 | 
			
		||||
    assert result["name"] == "test_device"
 | 
			
		||||
    assert result["friendly_name"] == "Test Device"
 | 
			
		||||
    assert result["comment"] == "Test comment"
 | 
			
		||||
    assert result["esphome_version"] == "2024.1.0"
 | 
			
		||||
    assert result["src_version"] == 1
 | 
			
		||||
    assert result["address"] == "192.168.1.100"
 | 
			
		||||
    assert result["web_port"] == 80
 | 
			
		||||
    assert result["esp_platform"] == "ESP32"
 | 
			
		||||
    assert result["build_path"] == "/path/to/build"
 | 
			
		||||
    assert result["firmware_bin_path"] == "/path/to/firmware.bin"
 | 
			
		||||
    assert "api" in result["loaded_integrations"]
 | 
			
		||||
    assert "wifi" in result["loaded_integrations"]
 | 
			
		||||
    assert "ota" in result["loaded_integrations"]
 | 
			
		||||
    assert result["loaded_integrations"] == sorted(
 | 
			
		||||
        ["wifi", "api", "ota"]
 | 
			
		||||
    )  # Should be sorted
 | 
			
		||||
    assert "sensor" in result["loaded_platforms"]
 | 
			
		||||
    assert result["loaded_platforms"] == sorted(
 | 
			
		||||
        ["sensor", "binary_sensor"]
 | 
			
		||||
    )  # Should be sorted
 | 
			
		||||
    assert result["no_mdns"] is True
 | 
			
		||||
    assert result["framework"] == "arduino"
 | 
			
		||||
    assert result["core_platform"] == "esp32"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_storage_json_to_json() -> None:
 | 
			
		||||
    """Test StorageJSON.to_json returns valid JSON string."""
 | 
			
		||||
    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,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    json_str = storage.to_json()
 | 
			
		||||
 | 
			
		||||
    # Should be valid JSON
 | 
			
		||||
    parsed = json.loads(json_str)
 | 
			
		||||
    assert parsed["name"] == "test"
 | 
			
		||||
    assert parsed["storage_version"] == 1
 | 
			
		||||
 | 
			
		||||
    # Should end with newline
 | 
			
		||||
    assert json_str.endswith("\n")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_storage_json_save(tmp_path: Path) -> None:
 | 
			
		||||
    """Test StorageJSON.save writes file correctly."""
 | 
			
		||||
    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="ESP32",
 | 
			
		||||
        build_path=None,
 | 
			
		||||
        firmware_bin_path=None,
 | 
			
		||||
        loaded_integrations=set(),
 | 
			
		||||
        loaded_platforms=set(),
 | 
			
		||||
        no_mdns=False,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    save_path = tmp_path / "test.json"
 | 
			
		||||
 | 
			
		||||
    with patch("esphome.storage_json.write_file_if_changed") as mock_write:
 | 
			
		||||
        storage.save(str(save_path))
 | 
			
		||||
        mock_write.assert_called_once_with(str(save_path), storage.to_json())
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_storage_json_from_esphome_core(setup_core: Path) -> None:
 | 
			
		||||
    """Test StorageJSON.from_esphome_core creates correct storage object."""
 | 
			
		||||
    # Mock CORE object
 | 
			
		||||
    mock_core = MagicMock()
 | 
			
		||||
    mock_core.name = "my_device"
 | 
			
		||||
    mock_core.friendly_name = "My Device"
 | 
			
		||||
    mock_core.comment = "A test device"
 | 
			
		||||
    mock_core.address = "192.168.1.50"
 | 
			
		||||
    mock_core.web_port = 8080
 | 
			
		||||
    mock_core.target_platform = "esp32"
 | 
			
		||||
    mock_core.is_esp32 = True
 | 
			
		||||
    mock_core.build_path = "/build/my_device"
 | 
			
		||||
    mock_core.firmware_bin = "/build/my_device/firmware.bin"
 | 
			
		||||
    mock_core.loaded_integrations = {"wifi", "api"}
 | 
			
		||||
    mock_core.loaded_platforms = {"sensor"}
 | 
			
		||||
    mock_core.config = {CONF_MDNS: {CONF_DISABLED: True}}
 | 
			
		||||
    mock_core.target_framework = "esp-idf"
 | 
			
		||||
 | 
			
		||||
    with patch("esphome.components.esp32.get_esp32_variant") as mock_variant:
 | 
			
		||||
        mock_variant.return_value = "ESP32-C3"
 | 
			
		||||
 | 
			
		||||
        result = storage_json.StorageJSON.from_esphome_core(mock_core, old=None)
 | 
			
		||||
 | 
			
		||||
    assert result.name == "my_device"
 | 
			
		||||
    assert result.friendly_name == "My Device"
 | 
			
		||||
    assert result.comment == "A test device"
 | 
			
		||||
    assert result.address == "192.168.1.50"
 | 
			
		||||
    assert result.web_port == 8080
 | 
			
		||||
    assert result.target_platform == "ESP32-C3"
 | 
			
		||||
    assert result.build_path == "/build/my_device"
 | 
			
		||||
    assert result.firmware_bin_path == "/build/my_device/firmware.bin"
 | 
			
		||||
    assert result.loaded_integrations == {"wifi", "api"}
 | 
			
		||||
    assert result.loaded_platforms == {"sensor"}
 | 
			
		||||
    assert result.no_mdns is True
 | 
			
		||||
    assert result.framework == "esp-idf"
 | 
			
		||||
    assert result.core_platform == "esp32"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_storage_json_from_esphome_core_mdns_enabled(setup_core: Path) -> None:
 | 
			
		||||
    """Test from_esphome_core with mDNS enabled."""
 | 
			
		||||
    mock_core = MagicMock()
 | 
			
		||||
    mock_core.name = "test"
 | 
			
		||||
    mock_core.friendly_name = "Test"
 | 
			
		||||
    mock_core.comment = None
 | 
			
		||||
    mock_core.address = "test.local"
 | 
			
		||||
    mock_core.web_port = None
 | 
			
		||||
    mock_core.target_platform = "esp8266"
 | 
			
		||||
    mock_core.is_esp32 = False
 | 
			
		||||
    mock_core.build_path = "/build"
 | 
			
		||||
    mock_core.firmware_bin = "/build/firmware.bin"
 | 
			
		||||
    mock_core.loaded_integrations = set()
 | 
			
		||||
    mock_core.loaded_platforms = set()
 | 
			
		||||
    mock_core.config = {}  # No MDNS config means enabled
 | 
			
		||||
    mock_core.target_framework = "arduino"
 | 
			
		||||
 | 
			
		||||
    result = storage_json.StorageJSON.from_esphome_core(mock_core, old=None)
 | 
			
		||||
 | 
			
		||||
    assert result.no_mdns is False
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_storage_json_load_valid_file(tmp_path: Path) -> None:
 | 
			
		||||
    """Test StorageJSON.load with valid JSON file."""
 | 
			
		||||
    storage_data = {
 | 
			
		||||
        "storage_version": 1,
 | 
			
		||||
        "name": "loaded_device",
 | 
			
		||||
        "friendly_name": "Loaded Device",
 | 
			
		||||
        "comment": "Loaded from file",
 | 
			
		||||
        "esphome_version": "2024.1.0",
 | 
			
		||||
        "src_version": 2,
 | 
			
		||||
        "address": "10.0.0.1",
 | 
			
		||||
        "web_port": 8080,
 | 
			
		||||
        "esp_platform": "ESP32",
 | 
			
		||||
        "build_path": "/loaded/build",
 | 
			
		||||
        "firmware_bin_path": "/loaded/firmware.bin",
 | 
			
		||||
        "loaded_integrations": ["wifi", "api"],
 | 
			
		||||
        "loaded_platforms": ["sensor"],
 | 
			
		||||
        "no_mdns": True,
 | 
			
		||||
        "framework": "arduino",
 | 
			
		||||
        "core_platform": "esp32",
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    file_path = tmp_path / "storage.json"
 | 
			
		||||
    file_path.write_text(json.dumps(storage_data))
 | 
			
		||||
 | 
			
		||||
    result = storage_json.StorageJSON.load(str(file_path))
 | 
			
		||||
 | 
			
		||||
    assert result is not None
 | 
			
		||||
    assert result.name == "loaded_device"
 | 
			
		||||
    assert result.friendly_name == "Loaded Device"
 | 
			
		||||
    assert result.comment == "Loaded from file"
 | 
			
		||||
    assert result.esphome_version == "2024.1.0"
 | 
			
		||||
    assert result.src_version == 2
 | 
			
		||||
    assert result.address == "10.0.0.1"
 | 
			
		||||
    assert result.web_port == 8080
 | 
			
		||||
    assert result.target_platform == "ESP32"
 | 
			
		||||
    assert result.build_path == "/loaded/build"
 | 
			
		||||
    assert result.firmware_bin_path == "/loaded/firmware.bin"
 | 
			
		||||
    assert result.loaded_integrations == {"wifi", "api"}
 | 
			
		||||
    assert result.loaded_platforms == {"sensor"}
 | 
			
		||||
    assert result.no_mdns is True
 | 
			
		||||
    assert result.framework == "arduino"
 | 
			
		||||
    assert result.core_platform == "esp32"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_storage_json_load_invalid_file(tmp_path: Path) -> None:
 | 
			
		||||
    """Test StorageJSON.load with invalid JSON file."""
 | 
			
		||||
    file_path = tmp_path / "invalid.json"
 | 
			
		||||
    file_path.write_text("not valid json{")
 | 
			
		||||
 | 
			
		||||
    result = storage_json.StorageJSON.load(str(file_path))
 | 
			
		||||
 | 
			
		||||
    assert result is None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_storage_json_load_nonexistent_file() -> None:
 | 
			
		||||
    """Test StorageJSON.load with non-existent file."""
 | 
			
		||||
    result = storage_json.StorageJSON.load("/nonexistent/file.json")
 | 
			
		||||
 | 
			
		||||
    assert result is None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_storage_json_equality() -> None:
 | 
			
		||||
    """Test StorageJSON equality comparison."""
 | 
			
		||||
    storage1 = storage_json.StorageJSON(
 | 
			
		||||
        storage_version=1,
 | 
			
		||||
        name="test",
 | 
			
		||||
        friendly_name="Test",
 | 
			
		||||
        comment=None,
 | 
			
		||||
        esphome_version="2024.1.0",
 | 
			
		||||
        src_version=1,
 | 
			
		||||
        address="test.local",
 | 
			
		||||
        web_port=80,
 | 
			
		||||
        target_platform="ESP32",
 | 
			
		||||
        build_path="/build",
 | 
			
		||||
        firmware_bin_path="/firmware.bin",
 | 
			
		||||
        loaded_integrations={"wifi"},
 | 
			
		||||
        loaded_platforms=set(),
 | 
			
		||||
        no_mdns=False,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    storage2 = storage_json.StorageJSON(
 | 
			
		||||
        storage_version=1,
 | 
			
		||||
        name="test",
 | 
			
		||||
        friendly_name="Test",
 | 
			
		||||
        comment=None,
 | 
			
		||||
        esphome_version="2024.1.0",
 | 
			
		||||
        src_version=1,
 | 
			
		||||
        address="test.local",
 | 
			
		||||
        web_port=80,
 | 
			
		||||
        target_platform="ESP32",
 | 
			
		||||
        build_path="/build",
 | 
			
		||||
        firmware_bin_path="/firmware.bin",
 | 
			
		||||
        loaded_integrations={"wifi"},
 | 
			
		||||
        loaded_platforms=set(),
 | 
			
		||||
        no_mdns=False,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    storage3 = storage_json.StorageJSON(
 | 
			
		||||
        storage_version=1,
 | 
			
		||||
        name="different",  # Different name
 | 
			
		||||
        friendly_name="Test",
 | 
			
		||||
        comment=None,
 | 
			
		||||
        esphome_version="2024.1.0",
 | 
			
		||||
        src_version=1,
 | 
			
		||||
        address="test.local",
 | 
			
		||||
        web_port=80,
 | 
			
		||||
        target_platform="ESP32",
 | 
			
		||||
        build_path="/build",
 | 
			
		||||
        firmware_bin_path="/firmware.bin",
 | 
			
		||||
        loaded_integrations={"wifi"},
 | 
			
		||||
        loaded_platforms=set(),
 | 
			
		||||
        no_mdns=False,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    assert storage1 == storage2
 | 
			
		||||
    assert storage1 != storage3
 | 
			
		||||
    assert storage1 != "not a storage object"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_esphome_storage_json_as_dict() -> None:
 | 
			
		||||
    """Test EsphomeStorageJSON.as_dict returns correct dictionary."""
 | 
			
		||||
    storage = storage_json.EsphomeStorageJSON(
 | 
			
		||||
        storage_version=1,
 | 
			
		||||
        cookie_secret="secret123",
 | 
			
		||||
        last_update_check="2024-01-15T10:30:00",
 | 
			
		||||
        remote_version="2024.1.1",
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    result = storage.as_dict()
 | 
			
		||||
 | 
			
		||||
    assert result["storage_version"] == 1
 | 
			
		||||
    assert result["cookie_secret"] == "secret123"
 | 
			
		||||
    assert result["last_update_check"] == "2024-01-15T10:30:00"
 | 
			
		||||
    assert result["remote_version"] == "2024.1.1"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_esphome_storage_json_last_update_check_property() -> None:
 | 
			
		||||
    """Test EsphomeStorageJSON.last_update_check property."""
 | 
			
		||||
    storage = storage_json.EsphomeStorageJSON(
 | 
			
		||||
        storage_version=1,
 | 
			
		||||
        cookie_secret="secret",
 | 
			
		||||
        last_update_check="2024-01-15T10:30:00",
 | 
			
		||||
        remote_version=None,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    # Test getter
 | 
			
		||||
    result = storage.last_update_check
 | 
			
		||||
    assert isinstance(result, datetime)
 | 
			
		||||
    assert result.year == 2024
 | 
			
		||||
    assert result.month == 1
 | 
			
		||||
    assert result.day == 15
 | 
			
		||||
    assert result.hour == 10
 | 
			
		||||
    assert result.minute == 30
 | 
			
		||||
 | 
			
		||||
    # Test setter
 | 
			
		||||
    new_date = datetime(2024, 2, 20, 15, 45, 30)
 | 
			
		||||
    storage.last_update_check = new_date
 | 
			
		||||
    assert storage.last_update_check_str == "2024-02-20T15:45:30"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_esphome_storage_json_last_update_check_invalid() -> None:
 | 
			
		||||
    """Test EsphomeStorageJSON.last_update_check with invalid date."""
 | 
			
		||||
    storage = storage_json.EsphomeStorageJSON(
 | 
			
		||||
        storage_version=1,
 | 
			
		||||
        cookie_secret="secret",
 | 
			
		||||
        last_update_check="invalid date",
 | 
			
		||||
        remote_version=None,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    result = storage.last_update_check
 | 
			
		||||
    assert result is None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_esphome_storage_json_to_json() -> None:
 | 
			
		||||
    """Test EsphomeStorageJSON.to_json returns valid JSON string."""
 | 
			
		||||
    storage = storage_json.EsphomeStorageJSON(
 | 
			
		||||
        storage_version=1,
 | 
			
		||||
        cookie_secret="mysecret",
 | 
			
		||||
        last_update_check="2024-01-15T10:30:00",
 | 
			
		||||
        remote_version="2024.1.1",
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    json_str = storage.to_json()
 | 
			
		||||
 | 
			
		||||
    # Should be valid JSON
 | 
			
		||||
    parsed = json.loads(json_str)
 | 
			
		||||
    assert parsed["cookie_secret"] == "mysecret"
 | 
			
		||||
    assert parsed["storage_version"] == 1
 | 
			
		||||
 | 
			
		||||
    # Should end with newline
 | 
			
		||||
    assert json_str.endswith("\n")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_esphome_storage_json_save(tmp_path: Path) -> None:
 | 
			
		||||
    """Test EsphomeStorageJSON.save writes file correctly."""
 | 
			
		||||
    storage = storage_json.EsphomeStorageJSON(
 | 
			
		||||
        storage_version=1,
 | 
			
		||||
        cookie_secret="secret",
 | 
			
		||||
        last_update_check=None,
 | 
			
		||||
        remote_version=None,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    save_path = tmp_path / "esphome.json"
 | 
			
		||||
 | 
			
		||||
    with patch("esphome.storage_json.write_file_if_changed") as mock_write:
 | 
			
		||||
        storage.save(str(save_path))
 | 
			
		||||
        mock_write.assert_called_once_with(str(save_path), storage.to_json())
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_esphome_storage_json_load_valid_file(tmp_path: Path) -> None:
 | 
			
		||||
    """Test EsphomeStorageJSON.load with valid JSON file."""
 | 
			
		||||
    storage_data = {
 | 
			
		||||
        "storage_version": 1,
 | 
			
		||||
        "cookie_secret": "loaded_secret",
 | 
			
		||||
        "last_update_check": "2024-01-20T14:30:00",
 | 
			
		||||
        "remote_version": "2024.1.2",
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    file_path = tmp_path / "esphome.json"
 | 
			
		||||
    file_path.write_text(json.dumps(storage_data))
 | 
			
		||||
 | 
			
		||||
    result = storage_json.EsphomeStorageJSON.load(str(file_path))
 | 
			
		||||
 | 
			
		||||
    assert result is not None
 | 
			
		||||
    assert result.storage_version == 1
 | 
			
		||||
    assert result.cookie_secret == "loaded_secret"
 | 
			
		||||
    assert result.last_update_check_str == "2024-01-20T14:30:00"
 | 
			
		||||
    assert result.remote_version == "2024.1.2"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_esphome_storage_json_load_invalid_file(tmp_path: Path) -> None:
 | 
			
		||||
    """Test EsphomeStorageJSON.load with invalid JSON file."""
 | 
			
		||||
    file_path = tmp_path / "invalid.json"
 | 
			
		||||
    file_path.write_text("not valid json{")
 | 
			
		||||
 | 
			
		||||
    result = storage_json.EsphomeStorageJSON.load(str(file_path))
 | 
			
		||||
 | 
			
		||||
    assert result is None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_esphome_storage_json_load_nonexistent_file() -> None:
 | 
			
		||||
    """Test EsphomeStorageJSON.load with non-existent file."""
 | 
			
		||||
    result = storage_json.EsphomeStorageJSON.load("/nonexistent/file.json")
 | 
			
		||||
 | 
			
		||||
    assert result is None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_esphome_storage_json_get_default() -> None:
 | 
			
		||||
    """Test EsphomeStorageJSON.get_default creates default storage."""
 | 
			
		||||
    with patch("esphome.storage_json.os.urandom") as mock_urandom:
 | 
			
		||||
        # Mock urandom to return predictable bytes
 | 
			
		||||
        mock_urandom.return_value = b"test" * 16  # 64 bytes
 | 
			
		||||
 | 
			
		||||
        result = storage_json.EsphomeStorageJSON.get_default()
 | 
			
		||||
 | 
			
		||||
    assert result.storage_version == 1
 | 
			
		||||
    assert len(result.cookie_secret) == 128  # 64 bytes hex = 128 chars
 | 
			
		||||
    assert result.last_update_check is None
 | 
			
		||||
    assert result.remote_version is None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_esphome_storage_json_equality() -> None:
 | 
			
		||||
    """Test EsphomeStorageJSON equality comparison."""
 | 
			
		||||
    storage1 = storage_json.EsphomeStorageJSON(
 | 
			
		||||
        storage_version=1,
 | 
			
		||||
        cookie_secret="secret",
 | 
			
		||||
        last_update_check="2024-01-15T10:30:00",
 | 
			
		||||
        remote_version="2024.1.1",
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    storage2 = storage_json.EsphomeStorageJSON(
 | 
			
		||||
        storage_version=1,
 | 
			
		||||
        cookie_secret="secret",
 | 
			
		||||
        last_update_check="2024-01-15T10:30:00",
 | 
			
		||||
        remote_version="2024.1.1",
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    storage3 = storage_json.EsphomeStorageJSON(
 | 
			
		||||
        storage_version=1,
 | 
			
		||||
        cookie_secret="different",  # Different secret
 | 
			
		||||
        last_update_check="2024-01-15T10:30:00",
 | 
			
		||||
        remote_version="2024.1.1",
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    assert storage1 == storage2
 | 
			
		||||
    assert storage1 != storage3
 | 
			
		||||
    assert storage1 != "not a storage object"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_storage_json_load_legacy_esphomeyaml_version(tmp_path: Path) -> None:
 | 
			
		||||
    """Test loading storage with legacy esphomeyaml_version field."""
 | 
			
		||||
    storage_data = {
 | 
			
		||||
        "storage_version": 1,
 | 
			
		||||
        "name": "legacy_device",
 | 
			
		||||
        "friendly_name": "Legacy Device",
 | 
			
		||||
        "esphomeyaml_version": "1.14.0",  # Legacy field name
 | 
			
		||||
        "address": "legacy.local",
 | 
			
		||||
        "esp_platform": "ESP8266",
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    file_path = tmp_path / "legacy.json"
 | 
			
		||||
    file_path.write_text(json.dumps(storage_data))
 | 
			
		||||
 | 
			
		||||
    result = storage_json.StorageJSON.load(str(file_path))
 | 
			
		||||
 | 
			
		||||
    assert result is not None
 | 
			
		||||
    assert result.esphome_version == "1.14.0"  # Should map to esphome_version
 | 
			
		||||
@@ -141,3 +141,170 @@ def test_list_yaml_files_mixed_extensions(tmp_path: Path) -> None:
 | 
			
		||||
        str(yaml_file),
 | 
			
		||||
        str(yml_file),
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_list_yaml_files_does_not_recurse_into_subdirectories(tmp_path: Path) -> None:
 | 
			
		||||
    """Test that list_yaml_files only finds files in specified directory, not subdirectories."""
 | 
			
		||||
    # Create directory structure with YAML files at different depths
 | 
			
		||||
    root = tmp_path / "configs"
 | 
			
		||||
    root.mkdir()
 | 
			
		||||
 | 
			
		||||
    # Create YAML files in the root directory
 | 
			
		||||
    (root / "config1.yaml").write_text("test: 1")
 | 
			
		||||
    (root / "config2.yml").write_text("test: 2")
 | 
			
		||||
    (root / "device.yaml").write_text("test: device")
 | 
			
		||||
 | 
			
		||||
    # Create subdirectory with YAML files (should NOT be found)
 | 
			
		||||
    subdir = root / "subdir"
 | 
			
		||||
    subdir.mkdir()
 | 
			
		||||
    (subdir / "nested1.yaml").write_text("test: nested1")
 | 
			
		||||
    (subdir / "nested2.yml").write_text("test: nested2")
 | 
			
		||||
 | 
			
		||||
    # Create deeper subdirectory (should NOT be found)
 | 
			
		||||
    deep_subdir = subdir / "deeper"
 | 
			
		||||
    deep_subdir.mkdir()
 | 
			
		||||
    (deep_subdir / "very_nested.yaml").write_text("test: very_nested")
 | 
			
		||||
 | 
			
		||||
    # Test listing files from the root directory
 | 
			
		||||
    result = util.list_yaml_files([str(root)])
 | 
			
		||||
 | 
			
		||||
    # Should only find the 3 files in root, not the 3 in subdirectories
 | 
			
		||||
    assert len(result) == 3
 | 
			
		||||
 | 
			
		||||
    # Check that only root-level files are found
 | 
			
		||||
    assert str(root / "config1.yaml") in result
 | 
			
		||||
    assert str(root / "config2.yml") in result
 | 
			
		||||
    assert str(root / "device.yaml") in result
 | 
			
		||||
 | 
			
		||||
    # Ensure nested files are NOT found
 | 
			
		||||
    for r in result:
 | 
			
		||||
        assert "subdir" not in r
 | 
			
		||||
        assert "deeper" not in r
 | 
			
		||||
        assert "nested1.yaml" not in r
 | 
			
		||||
        assert "nested2.yml" not in r
 | 
			
		||||
        assert "very_nested.yaml" not in r
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_list_yaml_files_excludes_secrets(tmp_path: Path) -> None:
 | 
			
		||||
    """Test that secrets.yaml and secrets.yml are excluded."""
 | 
			
		||||
    root = tmp_path / "configs"
 | 
			
		||||
    root.mkdir()
 | 
			
		||||
 | 
			
		||||
    # Create various YAML files including secrets
 | 
			
		||||
    (root / "config.yaml").write_text("test: config")
 | 
			
		||||
    (root / "secrets.yaml").write_text("wifi_password: secret123")
 | 
			
		||||
    (root / "secrets.yml").write_text("api_key: secret456")
 | 
			
		||||
    (root / "device.yaml").write_text("test: device")
 | 
			
		||||
 | 
			
		||||
    result = util.list_yaml_files([str(root)])
 | 
			
		||||
 | 
			
		||||
    # Should find 2 files (config.yaml and device.yaml), not secrets
 | 
			
		||||
    assert len(result) == 2
 | 
			
		||||
    assert str(root / "config.yaml") in result
 | 
			
		||||
    assert str(root / "device.yaml") in result
 | 
			
		||||
    assert str(root / "secrets.yaml") not in result
 | 
			
		||||
    assert str(root / "secrets.yml") not in result
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_list_yaml_files_excludes_hidden_files(tmp_path: Path) -> None:
 | 
			
		||||
    """Test that hidden files (starting with .) are excluded."""
 | 
			
		||||
    root = tmp_path / "configs"
 | 
			
		||||
    root.mkdir()
 | 
			
		||||
 | 
			
		||||
    # Create regular and hidden YAML files
 | 
			
		||||
    (root / "config.yaml").write_text("test: config")
 | 
			
		||||
    (root / ".hidden.yaml").write_text("test: hidden")
 | 
			
		||||
    (root / ".backup.yml").write_text("test: backup")
 | 
			
		||||
    (root / "device.yaml").write_text("test: device")
 | 
			
		||||
 | 
			
		||||
    result = util.list_yaml_files([str(root)])
 | 
			
		||||
 | 
			
		||||
    # Should find only non-hidden files
 | 
			
		||||
    assert len(result) == 2
 | 
			
		||||
    assert str(root / "config.yaml") in result
 | 
			
		||||
    assert str(root / "device.yaml") in result
 | 
			
		||||
    assert str(root / ".hidden.yaml") not in result
 | 
			
		||||
    assert str(root / ".backup.yml") not in result
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_filter_yaml_files_basic() -> None:
 | 
			
		||||
    """Test filter_yaml_files function."""
 | 
			
		||||
    files = [
 | 
			
		||||
        "/path/to/config.yaml",
 | 
			
		||||
        "/path/to/device.yml",
 | 
			
		||||
        "/path/to/readme.txt",
 | 
			
		||||
        "/path/to/script.py",
 | 
			
		||||
        "/path/to/data.json",
 | 
			
		||||
        "/path/to/another.yaml",
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    result = util.filter_yaml_files(files)
 | 
			
		||||
 | 
			
		||||
    assert len(result) == 3
 | 
			
		||||
    assert "/path/to/config.yaml" in result
 | 
			
		||||
    assert "/path/to/device.yml" in result
 | 
			
		||||
    assert "/path/to/another.yaml" in result
 | 
			
		||||
    assert "/path/to/readme.txt" not in result
 | 
			
		||||
    assert "/path/to/script.py" not in result
 | 
			
		||||
    assert "/path/to/data.json" not in result
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_filter_yaml_files_excludes_secrets() -> None:
 | 
			
		||||
    """Test that filter_yaml_files excludes secrets files."""
 | 
			
		||||
    files = [
 | 
			
		||||
        "/path/to/config.yaml",
 | 
			
		||||
        "/path/to/secrets.yaml",
 | 
			
		||||
        "/path/to/secrets.yml",
 | 
			
		||||
        "/path/to/device.yaml",
 | 
			
		||||
        "/some/dir/secrets.yaml",
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    result = util.filter_yaml_files(files)
 | 
			
		||||
 | 
			
		||||
    assert len(result) == 2
 | 
			
		||||
    assert "/path/to/config.yaml" in result
 | 
			
		||||
    assert "/path/to/device.yaml" in result
 | 
			
		||||
    assert "/path/to/secrets.yaml" not in result
 | 
			
		||||
    assert "/path/to/secrets.yml" not in result
 | 
			
		||||
    assert "/some/dir/secrets.yaml" not in result
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_filter_yaml_files_excludes_hidden() -> None:
 | 
			
		||||
    """Test that filter_yaml_files excludes hidden files."""
 | 
			
		||||
    files = [
 | 
			
		||||
        "/path/to/config.yaml",
 | 
			
		||||
        "/path/to/.hidden.yaml",
 | 
			
		||||
        "/path/to/.backup.yml",
 | 
			
		||||
        "/path/to/device.yaml",
 | 
			
		||||
        "/some/dir/.config.yaml",
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    result = util.filter_yaml_files(files)
 | 
			
		||||
 | 
			
		||||
    assert len(result) == 2
 | 
			
		||||
    assert "/path/to/config.yaml" in result
 | 
			
		||||
    assert "/path/to/device.yaml" in result
 | 
			
		||||
    assert "/path/to/.hidden.yaml" not in result
 | 
			
		||||
    assert "/path/to/.backup.yml" not in result
 | 
			
		||||
    assert "/some/dir/.config.yaml" not in result
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_filter_yaml_files_case_sensitive() -> None:
 | 
			
		||||
    """Test that filter_yaml_files is case-sensitive for extensions."""
 | 
			
		||||
    files = [
 | 
			
		||||
        "/path/to/config.yaml",
 | 
			
		||||
        "/path/to/config.YAML",
 | 
			
		||||
        "/path/to/config.YML",
 | 
			
		||||
        "/path/to/config.Yaml",
 | 
			
		||||
        "/path/to/config.yml",
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    result = util.filter_yaml_files(files)
 | 
			
		||||
 | 
			
		||||
    # Should only match lowercase .yaml and .yml
 | 
			
		||||
    assert len(result) == 2
 | 
			
		||||
    assert "/path/to/config.yaml" in result
 | 
			
		||||
    assert "/path/to/config.yml" in result
 | 
			
		||||
    assert "/path/to/config.YAML" not in result
 | 
			
		||||
    assert "/path/to/config.YML" not in result
 | 
			
		||||
    assert "/path/to/config.Yaml" not in result
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,12 @@
 | 
			
		||||
"""Tests for the wizard.py file."""
 | 
			
		||||
 | 
			
		||||
import os
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
from typing import Any
 | 
			
		||||
from unittest.mock import MagicMock
 | 
			
		||||
 | 
			
		||||
import pytest
 | 
			
		||||
from pytest import MonkeyPatch
 | 
			
		||||
 | 
			
		||||
from esphome.components.bk72xx.boards import BK72XX_BOARD_PINS
 | 
			
		||||
from esphome.components.esp32.boards import ESP32_BOARD_PINS
 | 
			
		||||
@@ -15,7 +18,7 @@ import esphome.wizard as wz
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture
 | 
			
		||||
def default_config():
 | 
			
		||||
def default_config() -> dict[str, Any]:
 | 
			
		||||
    return {
 | 
			
		||||
        "type": "basic",
 | 
			
		||||
        "name": "test-name",
 | 
			
		||||
@@ -28,7 +31,7 @@ def default_config():
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture
 | 
			
		||||
def wizard_answers():
 | 
			
		||||
def wizard_answers() -> list[str]:
 | 
			
		||||
    return [
 | 
			
		||||
        "test-node",  # Name of the node
 | 
			
		||||
        "ESP8266",  # platform
 | 
			
		||||
@@ -53,7 +56,9 @@ def test_sanitize_quotes_replaces_with_escaped_char():
 | 
			
		||||
    assert output_str == '\\"key\\": \\"value\\"'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_config_file_fallback_ap_includes_descriptive_name(default_config):
 | 
			
		||||
def test_config_file_fallback_ap_includes_descriptive_name(
 | 
			
		||||
    default_config: dict[str, Any],
 | 
			
		||||
):
 | 
			
		||||
    """
 | 
			
		||||
    The fallback AP should include the node and a descriptive name
 | 
			
		||||
    """
 | 
			
		||||
@@ -67,7 +72,9 @@ def test_config_file_fallback_ap_includes_descriptive_name(default_config):
 | 
			
		||||
    assert 'ssid: "Test Node Fallback Hotspot"' in config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_config_file_fallback_ap_name_less_than_32_chars(default_config):
 | 
			
		||||
def test_config_file_fallback_ap_name_less_than_32_chars(
 | 
			
		||||
    default_config: dict[str, Any],
 | 
			
		||||
):
 | 
			
		||||
    """
 | 
			
		||||
    The fallback AP name must be less than 32 chars.
 | 
			
		||||
    Since it is composed of the node name and "Fallback Hotspot" this can be too long and needs truncating
 | 
			
		||||
@@ -82,7 +89,7 @@ def test_config_file_fallback_ap_name_less_than_32_chars(default_config):
 | 
			
		||||
    assert 'ssid: "A Very Long Name For This Node"' in config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_config_file_should_include_ota(default_config):
 | 
			
		||||
def test_config_file_should_include_ota(default_config: dict[str, Any]):
 | 
			
		||||
    """
 | 
			
		||||
    The Over-The-Air update should be enabled by default
 | 
			
		||||
    """
 | 
			
		||||
@@ -95,7 +102,9 @@ def test_config_file_should_include_ota(default_config):
 | 
			
		||||
    assert "ota:" in config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_config_file_should_include_ota_when_password_set(default_config):
 | 
			
		||||
def test_config_file_should_include_ota_when_password_set(
 | 
			
		||||
    default_config: dict[str, Any],
 | 
			
		||||
):
 | 
			
		||||
    """
 | 
			
		||||
    The Over-The-Air update should be enabled when a password is set
 | 
			
		||||
    """
 | 
			
		||||
@@ -109,7 +118,9 @@ def test_config_file_should_include_ota_when_password_set(default_config):
 | 
			
		||||
    assert "ota:" in config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_wizard_write_sets_platform(default_config, tmp_path, monkeypatch):
 | 
			
		||||
def test_wizard_write_sets_platform(
 | 
			
		||||
    default_config: dict[str, Any], tmp_path: Path, monkeypatch: MonkeyPatch
 | 
			
		||||
):
 | 
			
		||||
    """
 | 
			
		||||
    If the platform is not explicitly set, use "ESP8266" if the board is one of the ESP8266 boards
 | 
			
		||||
    """
 | 
			
		||||
@@ -126,7 +137,7 @@ def test_wizard_write_sets_platform(default_config, tmp_path, monkeypatch):
 | 
			
		||||
    assert "esp8266:" in generated_config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_wizard_empty_config(tmp_path, monkeypatch):
 | 
			
		||||
def test_wizard_empty_config(tmp_path: Path, monkeypatch: MonkeyPatch):
 | 
			
		||||
    """
 | 
			
		||||
    The wizard should be able to create an empty configuration
 | 
			
		||||
    """
 | 
			
		||||
@@ -146,7 +157,7 @@ def test_wizard_empty_config(tmp_path, monkeypatch):
 | 
			
		||||
    assert generated_config == ""
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_wizard_upload_config(tmp_path, monkeypatch):
 | 
			
		||||
def test_wizard_upload_config(tmp_path: Path, monkeypatch: MonkeyPatch):
 | 
			
		||||
    """
 | 
			
		||||
    The wizard should be able to import an base64 encoded configuration
 | 
			
		||||
    """
 | 
			
		||||
@@ -168,7 +179,7 @@ def test_wizard_upload_config(tmp_path, monkeypatch):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_wizard_write_defaults_platform_from_board_esp8266(
 | 
			
		||||
    default_config, tmp_path, monkeypatch
 | 
			
		||||
    default_config: dict[str, Any], tmp_path: Path, monkeypatch: MonkeyPatch
 | 
			
		||||
):
 | 
			
		||||
    """
 | 
			
		||||
    If the platform is not explicitly set, use "ESP8266" if the board is one of the ESP8266 boards
 | 
			
		||||
@@ -189,7 +200,7 @@ def test_wizard_write_defaults_platform_from_board_esp8266(
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_wizard_write_defaults_platform_from_board_esp32(
 | 
			
		||||
    default_config, tmp_path, monkeypatch
 | 
			
		||||
    default_config: dict[str, Any], tmp_path: Path, monkeypatch: MonkeyPatch
 | 
			
		||||
):
 | 
			
		||||
    """
 | 
			
		||||
    If the platform is not explicitly set, use "ESP32" if the board is one of the ESP32 boards
 | 
			
		||||
@@ -210,7 +221,7 @@ def test_wizard_write_defaults_platform_from_board_esp32(
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_wizard_write_defaults_platform_from_board_bk72xx(
 | 
			
		||||
    default_config, tmp_path, monkeypatch
 | 
			
		||||
    default_config: dict[str, Any], tmp_path: Path, monkeypatch: MonkeyPatch
 | 
			
		||||
):
 | 
			
		||||
    """
 | 
			
		||||
    If the platform is not explicitly set, use "BK72XX" if the board is one of BK72XX boards
 | 
			
		||||
@@ -231,7 +242,7 @@ def test_wizard_write_defaults_platform_from_board_bk72xx(
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_wizard_write_defaults_platform_from_board_ln882x(
 | 
			
		||||
    default_config, tmp_path, monkeypatch
 | 
			
		||||
    default_config: dict[str, Any], tmp_path: Path, monkeypatch: MonkeyPatch
 | 
			
		||||
):
 | 
			
		||||
    """
 | 
			
		||||
    If the platform is not explicitly set, use "LN882X" if the board is one of LN882X boards
 | 
			
		||||
@@ -252,7 +263,7 @@ def test_wizard_write_defaults_platform_from_board_ln882x(
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_wizard_write_defaults_platform_from_board_rtl87xx(
 | 
			
		||||
    default_config, tmp_path, monkeypatch
 | 
			
		||||
    default_config: dict[str, Any], tmp_path: Path, monkeypatch: MonkeyPatch
 | 
			
		||||
):
 | 
			
		||||
    """
 | 
			
		||||
    If the platform is not explicitly set, use "RTL87XX" if the board is one of RTL87XX boards
 | 
			
		||||
@@ -272,7 +283,7 @@ def test_wizard_write_defaults_platform_from_board_rtl87xx(
 | 
			
		||||
    assert "rtl87xx:" in generated_config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_safe_print_step_prints_step_number_and_description(monkeypatch):
 | 
			
		||||
def test_safe_print_step_prints_step_number_and_description(monkeypatch: MonkeyPatch):
 | 
			
		||||
    """
 | 
			
		||||
    The safe_print_step function prints the step number and the passed description
 | 
			
		||||
    """
 | 
			
		||||
@@ -296,7 +307,7 @@ def test_safe_print_step_prints_step_number_and_description(monkeypatch):
 | 
			
		||||
    assert any(f"STEP {step_num}" in arg for arg in all_args)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_default_input_uses_default_if_no_input_supplied(monkeypatch):
 | 
			
		||||
def test_default_input_uses_default_if_no_input_supplied(monkeypatch: MonkeyPatch):
 | 
			
		||||
    """
 | 
			
		||||
    The default_input() function should return the supplied default value if the user doesn't enter anything
 | 
			
		||||
    """
 | 
			
		||||
@@ -312,7 +323,7 @@ def test_default_input_uses_default_if_no_input_supplied(monkeypatch):
 | 
			
		||||
    assert retval == default_string
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_default_input_uses_user_supplied_value(monkeypatch):
 | 
			
		||||
def test_default_input_uses_user_supplied_value(monkeypatch: MonkeyPatch):
 | 
			
		||||
    """
 | 
			
		||||
    The default_input() function should return the value that the user enters
 | 
			
		||||
    """
 | 
			
		||||
@@ -376,7 +387,9 @@ def test_wizard_rejects_existing_files(tmpdir):
 | 
			
		||||
    assert retval == 2
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_wizard_accepts_default_answers_esp8266(tmpdir, monkeypatch, wizard_answers):
 | 
			
		||||
def test_wizard_accepts_default_answers_esp8266(
 | 
			
		||||
    tmpdir, monkeypatch: MonkeyPatch, wizard_answers: list[str]
 | 
			
		||||
):
 | 
			
		||||
    """
 | 
			
		||||
    The wizard should accept the given default answers for esp8266
 | 
			
		||||
    """
 | 
			
		||||
@@ -396,7 +409,9 @@ def test_wizard_accepts_default_answers_esp8266(tmpdir, monkeypatch, wizard_answ
 | 
			
		||||
    assert retval == 0
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_wizard_accepts_default_answers_esp32(tmpdir, monkeypatch, wizard_answers):
 | 
			
		||||
def test_wizard_accepts_default_answers_esp32(
 | 
			
		||||
    tmpdir, monkeypatch: MonkeyPatch, wizard_answers: list[str]
 | 
			
		||||
):
 | 
			
		||||
    """
 | 
			
		||||
    The wizard should accept the given default answers for esp32
 | 
			
		||||
    """
 | 
			
		||||
@@ -418,7 +433,9 @@ def test_wizard_accepts_default_answers_esp32(tmpdir, monkeypatch, wizard_answer
 | 
			
		||||
    assert retval == 0
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_wizard_offers_better_node_name(tmpdir, monkeypatch, wizard_answers):
 | 
			
		||||
def test_wizard_offers_better_node_name(
 | 
			
		||||
    tmpdir, monkeypatch: MonkeyPatch, wizard_answers: list[str]
 | 
			
		||||
):
 | 
			
		||||
    """
 | 
			
		||||
    When the node name does not conform, a better alternative is offered
 | 
			
		||||
    * Removes special chars
 | 
			
		||||
@@ -449,7 +466,9 @@ def test_wizard_offers_better_node_name(tmpdir, monkeypatch, wizard_answers):
 | 
			
		||||
    assert wz.default_input.call_args.args[1] == expected_name
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_wizard_requires_correct_platform(tmpdir, monkeypatch, wizard_answers):
 | 
			
		||||
def test_wizard_requires_correct_platform(
 | 
			
		||||
    tmpdir, monkeypatch: MonkeyPatch, wizard_answers: list[str]
 | 
			
		||||
):
 | 
			
		||||
    """
 | 
			
		||||
    When the platform is not either esp32 or esp8266, the wizard should reject it
 | 
			
		||||
    """
 | 
			
		||||
@@ -471,7 +490,9 @@ def test_wizard_requires_correct_platform(tmpdir, monkeypatch, wizard_answers):
 | 
			
		||||
    assert retval == 0
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_wizard_requires_correct_board(tmpdir, monkeypatch, wizard_answers):
 | 
			
		||||
def test_wizard_requires_correct_board(
 | 
			
		||||
    tmpdir, monkeypatch: MonkeyPatch, wizard_answers: list[str]
 | 
			
		||||
):
 | 
			
		||||
    """
 | 
			
		||||
    When the board is not a valid esp8266 board, the wizard should reject it
 | 
			
		||||
    """
 | 
			
		||||
@@ -493,7 +514,9 @@ def test_wizard_requires_correct_board(tmpdir, monkeypatch, wizard_answers):
 | 
			
		||||
    assert retval == 0
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_wizard_requires_valid_ssid(tmpdir, monkeypatch, wizard_answers):
 | 
			
		||||
def test_wizard_requires_valid_ssid(
 | 
			
		||||
    tmpdir, monkeypatch: MonkeyPatch, wizard_answers: list[str]
 | 
			
		||||
):
 | 
			
		||||
    """
 | 
			
		||||
    When the board is not a valid esp8266 board, the wizard should reject it
 | 
			
		||||
    """
 | 
			
		||||
@@ -515,7 +538,9 @@ def test_wizard_requires_valid_ssid(tmpdir, monkeypatch, wizard_answers):
 | 
			
		||||
    assert retval == 0
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_wizard_write_protects_existing_config(tmpdir, default_config, monkeypatch):
 | 
			
		||||
def test_wizard_write_protects_existing_config(
 | 
			
		||||
    tmpdir, default_config: dict[str, Any], monkeypatch: MonkeyPatch
 | 
			
		||||
):
 | 
			
		||||
    """
 | 
			
		||||
    The wizard_write function should not overwrite existing config files and return False
 | 
			
		||||
    """
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,34 @@
 | 
			
		||||
"""Test writer module functionality."""
 | 
			
		||||
 | 
			
		||||
from collections.abc import Callable
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
from typing import Any
 | 
			
		||||
from unittest.mock import MagicMock, patch
 | 
			
		||||
 | 
			
		||||
import pytest
 | 
			
		||||
 | 
			
		||||
from esphome.core import EsphomeError
 | 
			
		||||
from esphome.storage_json import StorageJSON
 | 
			
		||||
from esphome.writer import storage_should_clean, update_storage_json
 | 
			
		||||
from esphome.writer import (
 | 
			
		||||
    CPP_AUTO_GENERATE_BEGIN,
 | 
			
		||||
    CPP_AUTO_GENERATE_END,
 | 
			
		||||
    CPP_INCLUDE_BEGIN,
 | 
			
		||||
    CPP_INCLUDE_END,
 | 
			
		||||
    GITIGNORE_CONTENT,
 | 
			
		||||
    clean_build,
 | 
			
		||||
    clean_cmake_cache,
 | 
			
		||||
    storage_should_clean,
 | 
			
		||||
    update_storage_json,
 | 
			
		||||
    write_cpp,
 | 
			
		||||
    write_gitignore,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture
 | 
			
		||||
def mock_copy_src_tree():
 | 
			
		||||
    """Mock copy_src_tree to avoid side effects during tests."""
 | 
			
		||||
    with patch("esphome.writer.copy_src_tree"):
 | 
			
		||||
        yield
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture
 | 
			
		||||
@@ -218,3 +239,493 @@ def test_update_storage_json_logging_components_removed(
 | 
			
		||||
 | 
			
		||||
    # Verify save was called
 | 
			
		||||
    new_storage.save.assert_called_once_with("/test/path")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@patch("esphome.writer.CORE")
 | 
			
		||||
def test_clean_cmake_cache(
 | 
			
		||||
    mock_core: MagicMock,
 | 
			
		||||
    tmp_path: Path,
 | 
			
		||||
    caplog: pytest.LogCaptureFixture,
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test clean_cmake_cache removes CMakeCache.txt file."""
 | 
			
		||||
    # Create directory structure
 | 
			
		||||
    pioenvs_dir = tmp_path / ".pioenvs"
 | 
			
		||||
    pioenvs_dir.mkdir()
 | 
			
		||||
    device_dir = pioenvs_dir / "test_device"
 | 
			
		||||
    device_dir.mkdir()
 | 
			
		||||
    cmake_cache_file = device_dir / "CMakeCache.txt"
 | 
			
		||||
    cmake_cache_file.write_text("# CMake cache file")
 | 
			
		||||
 | 
			
		||||
    # Setup mocks
 | 
			
		||||
    mock_core.relative_pioenvs_path.side_effect = [
 | 
			
		||||
        str(pioenvs_dir),  # First call for directory check
 | 
			
		||||
        str(cmake_cache_file),  # Second call for file path
 | 
			
		||||
    ]
 | 
			
		||||
    mock_core.name = "test_device"
 | 
			
		||||
 | 
			
		||||
    # Verify file exists before
 | 
			
		||||
    assert cmake_cache_file.exists()
 | 
			
		||||
 | 
			
		||||
    # Call the function
 | 
			
		||||
    with caplog.at_level("INFO"):
 | 
			
		||||
        clean_cmake_cache()
 | 
			
		||||
 | 
			
		||||
    # Verify file was removed
 | 
			
		||||
    assert not cmake_cache_file.exists()
 | 
			
		||||
 | 
			
		||||
    # Verify logging
 | 
			
		||||
    assert "Deleting" in caplog.text
 | 
			
		||||
    assert "CMakeCache.txt" in caplog.text
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@patch("esphome.writer.CORE")
 | 
			
		||||
def test_clean_cmake_cache_no_pioenvs_dir(
 | 
			
		||||
    mock_core: MagicMock,
 | 
			
		||||
    tmp_path: Path,
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test clean_cmake_cache when pioenvs directory doesn't exist."""
 | 
			
		||||
    # Setup non-existent directory path
 | 
			
		||||
    pioenvs_dir = tmp_path / ".pioenvs"
 | 
			
		||||
 | 
			
		||||
    # Setup mocks
 | 
			
		||||
    mock_core.relative_pioenvs_path.return_value = str(pioenvs_dir)
 | 
			
		||||
 | 
			
		||||
    # Verify directory doesn't exist
 | 
			
		||||
    assert not pioenvs_dir.exists()
 | 
			
		||||
 | 
			
		||||
    # Call the function - should not crash
 | 
			
		||||
    clean_cmake_cache()
 | 
			
		||||
 | 
			
		||||
    # Verify directory still doesn't exist
 | 
			
		||||
    assert not pioenvs_dir.exists()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@patch("esphome.writer.CORE")
 | 
			
		||||
def test_clean_cmake_cache_no_cmake_file(
 | 
			
		||||
    mock_core: MagicMock,
 | 
			
		||||
    tmp_path: Path,
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test clean_cmake_cache when CMakeCache.txt doesn't exist."""
 | 
			
		||||
    # Create directory structure without CMakeCache.txt
 | 
			
		||||
    pioenvs_dir = tmp_path / ".pioenvs"
 | 
			
		||||
    pioenvs_dir.mkdir()
 | 
			
		||||
    device_dir = pioenvs_dir / "test_device"
 | 
			
		||||
    device_dir.mkdir()
 | 
			
		||||
    cmake_cache_file = device_dir / "CMakeCache.txt"
 | 
			
		||||
 | 
			
		||||
    # Setup mocks
 | 
			
		||||
    mock_core.relative_pioenvs_path.side_effect = [
 | 
			
		||||
        str(pioenvs_dir),  # First call for directory check
 | 
			
		||||
        str(cmake_cache_file),  # Second call for file path
 | 
			
		||||
    ]
 | 
			
		||||
    mock_core.name = "test_device"
 | 
			
		||||
 | 
			
		||||
    # Verify file doesn't exist
 | 
			
		||||
    assert not cmake_cache_file.exists()
 | 
			
		||||
 | 
			
		||||
    # Call the function - should not crash
 | 
			
		||||
    clean_cmake_cache()
 | 
			
		||||
 | 
			
		||||
    # Verify file still doesn't exist
 | 
			
		||||
    assert not cmake_cache_file.exists()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@patch("esphome.writer.CORE")
 | 
			
		||||
def test_clean_build(
 | 
			
		||||
    mock_core: MagicMock,
 | 
			
		||||
    tmp_path: Path,
 | 
			
		||||
    caplog: pytest.LogCaptureFixture,
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test clean_build removes all build artifacts."""
 | 
			
		||||
    # Create directory structure and files
 | 
			
		||||
    pioenvs_dir = tmp_path / ".pioenvs"
 | 
			
		||||
    pioenvs_dir.mkdir()
 | 
			
		||||
    (pioenvs_dir / "test_file.o").write_text("object file")
 | 
			
		||||
 | 
			
		||||
    piolibdeps_dir = tmp_path / ".piolibdeps"
 | 
			
		||||
    piolibdeps_dir.mkdir()
 | 
			
		||||
    (piolibdeps_dir / "library").mkdir()
 | 
			
		||||
 | 
			
		||||
    dependencies_lock = tmp_path / "dependencies.lock"
 | 
			
		||||
    dependencies_lock.write_text("lock file")
 | 
			
		||||
 | 
			
		||||
    # Create PlatformIO cache directory
 | 
			
		||||
    platformio_cache_dir = tmp_path / ".platformio" / ".cache"
 | 
			
		||||
    platformio_cache_dir.mkdir(parents=True)
 | 
			
		||||
    (platformio_cache_dir / "downloads").mkdir()
 | 
			
		||||
    (platformio_cache_dir / "http").mkdir()
 | 
			
		||||
    (platformio_cache_dir / "tmp").mkdir()
 | 
			
		||||
    (platformio_cache_dir / "downloads" / "package.tar.gz").write_text("package")
 | 
			
		||||
 | 
			
		||||
    # Setup mocks
 | 
			
		||||
    mock_core.relative_pioenvs_path.return_value = str(pioenvs_dir)
 | 
			
		||||
    mock_core.relative_piolibdeps_path.return_value = str(piolibdeps_dir)
 | 
			
		||||
    mock_core.relative_build_path.return_value = str(dependencies_lock)
 | 
			
		||||
 | 
			
		||||
    # Verify all exist before
 | 
			
		||||
    assert pioenvs_dir.exists()
 | 
			
		||||
    assert piolibdeps_dir.exists()
 | 
			
		||||
    assert dependencies_lock.exists()
 | 
			
		||||
    assert platformio_cache_dir.exists()
 | 
			
		||||
 | 
			
		||||
    # Mock PlatformIO's get_project_cache_dir
 | 
			
		||||
    with patch(
 | 
			
		||||
        "platformio.project.helpers.get_project_cache_dir"
 | 
			
		||||
    ) as mock_get_cache_dir:
 | 
			
		||||
        mock_get_cache_dir.return_value = str(platformio_cache_dir)
 | 
			
		||||
 | 
			
		||||
        # Call the function
 | 
			
		||||
        with caplog.at_level("INFO"):
 | 
			
		||||
            clean_build()
 | 
			
		||||
 | 
			
		||||
    # Verify all were removed
 | 
			
		||||
    assert not pioenvs_dir.exists()
 | 
			
		||||
    assert not piolibdeps_dir.exists()
 | 
			
		||||
    assert not dependencies_lock.exists()
 | 
			
		||||
    assert not platformio_cache_dir.exists()
 | 
			
		||||
 | 
			
		||||
    # Verify logging
 | 
			
		||||
    assert "Deleting" in caplog.text
 | 
			
		||||
    assert ".pioenvs" in caplog.text
 | 
			
		||||
    assert ".piolibdeps" in caplog.text
 | 
			
		||||
    assert "dependencies.lock" in caplog.text
 | 
			
		||||
    assert "PlatformIO cache" in caplog.text
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@patch("esphome.writer.CORE")
 | 
			
		||||
def test_clean_build_partial_exists(
 | 
			
		||||
    mock_core: MagicMock,
 | 
			
		||||
    tmp_path: Path,
 | 
			
		||||
    caplog: pytest.LogCaptureFixture,
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test clean_build when only some paths exist."""
 | 
			
		||||
    # Create only pioenvs directory
 | 
			
		||||
    pioenvs_dir = tmp_path / ".pioenvs"
 | 
			
		||||
    pioenvs_dir.mkdir()
 | 
			
		||||
    (pioenvs_dir / "test_file.o").write_text("object file")
 | 
			
		||||
 | 
			
		||||
    piolibdeps_dir = tmp_path / ".piolibdeps"
 | 
			
		||||
    dependencies_lock = tmp_path / "dependencies.lock"
 | 
			
		||||
 | 
			
		||||
    # Setup mocks
 | 
			
		||||
    mock_core.relative_pioenvs_path.return_value = str(pioenvs_dir)
 | 
			
		||||
    mock_core.relative_piolibdeps_path.return_value = str(piolibdeps_dir)
 | 
			
		||||
    mock_core.relative_build_path.return_value = str(dependencies_lock)
 | 
			
		||||
 | 
			
		||||
    # Verify only pioenvs exists
 | 
			
		||||
    assert pioenvs_dir.exists()
 | 
			
		||||
    assert not piolibdeps_dir.exists()
 | 
			
		||||
    assert not dependencies_lock.exists()
 | 
			
		||||
 | 
			
		||||
    # Call the function
 | 
			
		||||
    with caplog.at_level("INFO"):
 | 
			
		||||
        clean_build()
 | 
			
		||||
 | 
			
		||||
    # Verify only existing path was removed
 | 
			
		||||
    assert not pioenvs_dir.exists()
 | 
			
		||||
    assert not piolibdeps_dir.exists()
 | 
			
		||||
    assert not dependencies_lock.exists()
 | 
			
		||||
 | 
			
		||||
    # Verify logging - only pioenvs should be logged
 | 
			
		||||
    assert "Deleting" in caplog.text
 | 
			
		||||
    assert ".pioenvs" in caplog.text
 | 
			
		||||
    assert ".piolibdeps" not in caplog.text
 | 
			
		||||
    assert "dependencies.lock" not in caplog.text
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@patch("esphome.writer.CORE")
 | 
			
		||||
def test_clean_build_nothing_exists(
 | 
			
		||||
    mock_core: MagicMock,
 | 
			
		||||
    tmp_path: Path,
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test clean_build when no build artifacts exist."""
 | 
			
		||||
    # Setup paths that don't exist
 | 
			
		||||
    pioenvs_dir = tmp_path / ".pioenvs"
 | 
			
		||||
    piolibdeps_dir = tmp_path / ".piolibdeps"
 | 
			
		||||
    dependencies_lock = tmp_path / "dependencies.lock"
 | 
			
		||||
 | 
			
		||||
    # Setup mocks
 | 
			
		||||
    mock_core.relative_pioenvs_path.return_value = str(pioenvs_dir)
 | 
			
		||||
    mock_core.relative_piolibdeps_path.return_value = str(piolibdeps_dir)
 | 
			
		||||
    mock_core.relative_build_path.return_value = str(dependencies_lock)
 | 
			
		||||
 | 
			
		||||
    # Verify nothing exists
 | 
			
		||||
    assert not pioenvs_dir.exists()
 | 
			
		||||
    assert not piolibdeps_dir.exists()
 | 
			
		||||
    assert not dependencies_lock.exists()
 | 
			
		||||
 | 
			
		||||
    # Call the function - should not crash
 | 
			
		||||
    clean_build()
 | 
			
		||||
 | 
			
		||||
    # Verify nothing was created
 | 
			
		||||
    assert not pioenvs_dir.exists()
 | 
			
		||||
    assert not piolibdeps_dir.exists()
 | 
			
		||||
    assert not dependencies_lock.exists()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@patch("esphome.writer.CORE")
 | 
			
		||||
def test_clean_build_platformio_not_available(
 | 
			
		||||
    mock_core: MagicMock,
 | 
			
		||||
    tmp_path: Path,
 | 
			
		||||
    caplog: pytest.LogCaptureFixture,
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test clean_build when PlatformIO is not available."""
 | 
			
		||||
    # Create directory structure and files
 | 
			
		||||
    pioenvs_dir = tmp_path / ".pioenvs"
 | 
			
		||||
    pioenvs_dir.mkdir()
 | 
			
		||||
 | 
			
		||||
    piolibdeps_dir = tmp_path / ".piolibdeps"
 | 
			
		||||
    piolibdeps_dir.mkdir()
 | 
			
		||||
 | 
			
		||||
    dependencies_lock = tmp_path / "dependencies.lock"
 | 
			
		||||
    dependencies_lock.write_text("lock file")
 | 
			
		||||
 | 
			
		||||
    # Setup mocks
 | 
			
		||||
    mock_core.relative_pioenvs_path.return_value = str(pioenvs_dir)
 | 
			
		||||
    mock_core.relative_piolibdeps_path.return_value = str(piolibdeps_dir)
 | 
			
		||||
    mock_core.relative_build_path.return_value = str(dependencies_lock)
 | 
			
		||||
 | 
			
		||||
    # Verify all exist before
 | 
			
		||||
    assert pioenvs_dir.exists()
 | 
			
		||||
    assert piolibdeps_dir.exists()
 | 
			
		||||
    assert dependencies_lock.exists()
 | 
			
		||||
 | 
			
		||||
    # Mock import error for platformio
 | 
			
		||||
    with (
 | 
			
		||||
        patch.dict("sys.modules", {"platformio.project.helpers": None}),
 | 
			
		||||
        caplog.at_level("INFO"),
 | 
			
		||||
    ):
 | 
			
		||||
        # Call the function
 | 
			
		||||
        clean_build()
 | 
			
		||||
 | 
			
		||||
    # Verify standard paths were removed but no cache cleaning attempted
 | 
			
		||||
    assert not pioenvs_dir.exists()
 | 
			
		||||
    assert not piolibdeps_dir.exists()
 | 
			
		||||
    assert not dependencies_lock.exists()
 | 
			
		||||
 | 
			
		||||
    # Verify no cache logging
 | 
			
		||||
    assert "PlatformIO cache" not in caplog.text
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@patch("esphome.writer.CORE")
 | 
			
		||||
def test_clean_build_empty_cache_dir(
 | 
			
		||||
    mock_core: MagicMock,
 | 
			
		||||
    tmp_path: Path,
 | 
			
		||||
    caplog: pytest.LogCaptureFixture,
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test clean_build when get_project_cache_dir returns empty/whitespace."""
 | 
			
		||||
    # Create directory structure and files
 | 
			
		||||
    pioenvs_dir = tmp_path / ".pioenvs"
 | 
			
		||||
    pioenvs_dir.mkdir()
 | 
			
		||||
 | 
			
		||||
    # Setup mocks
 | 
			
		||||
    mock_core.relative_pioenvs_path.return_value = str(pioenvs_dir)
 | 
			
		||||
    mock_core.relative_piolibdeps_path.return_value = str(tmp_path / ".piolibdeps")
 | 
			
		||||
    mock_core.relative_build_path.return_value = str(tmp_path / "dependencies.lock")
 | 
			
		||||
 | 
			
		||||
    # Verify pioenvs exists before
 | 
			
		||||
    assert pioenvs_dir.exists()
 | 
			
		||||
 | 
			
		||||
    # Mock PlatformIO's get_project_cache_dir to return whitespace
 | 
			
		||||
    with patch(
 | 
			
		||||
        "platformio.project.helpers.get_project_cache_dir"
 | 
			
		||||
    ) as mock_get_cache_dir:
 | 
			
		||||
        mock_get_cache_dir.return_value = "   "  # Whitespace only
 | 
			
		||||
 | 
			
		||||
        # Call the function
 | 
			
		||||
        with caplog.at_level("INFO"):
 | 
			
		||||
            clean_build()
 | 
			
		||||
 | 
			
		||||
    # Verify pioenvs was removed
 | 
			
		||||
    assert not pioenvs_dir.exists()
 | 
			
		||||
 | 
			
		||||
    # Verify no cache cleaning was attempted due to empty string
 | 
			
		||||
    assert "PlatformIO cache" not in caplog.text
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@patch("esphome.writer.CORE")
 | 
			
		||||
def test_write_gitignore_creates_new_file(
 | 
			
		||||
    mock_core: MagicMock,
 | 
			
		||||
    tmp_path: Path,
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test write_gitignore creates a new .gitignore file when it doesn't exist."""
 | 
			
		||||
    gitignore_path = tmp_path / ".gitignore"
 | 
			
		||||
 | 
			
		||||
    # Setup mocks
 | 
			
		||||
    mock_core.relative_config_path.return_value = str(gitignore_path)
 | 
			
		||||
 | 
			
		||||
    # Verify file doesn't exist
 | 
			
		||||
    assert not gitignore_path.exists()
 | 
			
		||||
 | 
			
		||||
    # Call the function
 | 
			
		||||
    write_gitignore()
 | 
			
		||||
 | 
			
		||||
    # Verify file was created with correct content
 | 
			
		||||
    assert gitignore_path.exists()
 | 
			
		||||
    assert gitignore_path.read_text() == GITIGNORE_CONTENT
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@patch("esphome.writer.CORE")
 | 
			
		||||
def test_write_gitignore_skips_existing_file(
 | 
			
		||||
    mock_core: MagicMock,
 | 
			
		||||
    tmp_path: Path,
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test write_gitignore doesn't overwrite existing .gitignore file."""
 | 
			
		||||
    gitignore_path = tmp_path / ".gitignore"
 | 
			
		||||
    existing_content = "# Custom gitignore\n/custom_dir/\n"
 | 
			
		||||
    gitignore_path.write_text(existing_content)
 | 
			
		||||
 | 
			
		||||
    # Setup mocks
 | 
			
		||||
    mock_core.relative_config_path.return_value = str(gitignore_path)
 | 
			
		||||
 | 
			
		||||
    # Verify file exists with custom content
 | 
			
		||||
    assert gitignore_path.exists()
 | 
			
		||||
    assert gitignore_path.read_text() == existing_content
 | 
			
		||||
 | 
			
		||||
    # Call the function
 | 
			
		||||
    write_gitignore()
 | 
			
		||||
 | 
			
		||||
    # Verify file was not modified
 | 
			
		||||
    assert gitignore_path.exists()
 | 
			
		||||
    assert gitignore_path.read_text() == existing_content
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@patch("esphome.writer.write_file_if_changed")  # Mock to capture output
 | 
			
		||||
@patch("esphome.writer.copy_src_tree")  # Keep this mock as it's complex
 | 
			
		||||
@patch("esphome.writer.CORE")
 | 
			
		||||
def test_write_cpp_with_existing_file(
 | 
			
		||||
    mock_core: MagicMock,
 | 
			
		||||
    mock_copy_src_tree: MagicMock,
 | 
			
		||||
    mock_write_file: MagicMock,
 | 
			
		||||
    tmp_path: Path,
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test write_cpp when main.cpp already exists."""
 | 
			
		||||
    # Create a real file with markers
 | 
			
		||||
    main_cpp = tmp_path / "main.cpp"
 | 
			
		||||
    existing_content = f"""#include "esphome.h"
 | 
			
		||||
{CPP_INCLUDE_BEGIN}
 | 
			
		||||
// Old includes
 | 
			
		||||
{CPP_INCLUDE_END}
 | 
			
		||||
void setup() {{
 | 
			
		||||
{CPP_AUTO_GENERATE_BEGIN}
 | 
			
		||||
// Old code
 | 
			
		||||
{CPP_AUTO_GENERATE_END}
 | 
			
		||||
}}
 | 
			
		||||
void loop() {{}}"""
 | 
			
		||||
    main_cpp.write_text(existing_content)
 | 
			
		||||
 | 
			
		||||
    # Setup mocks
 | 
			
		||||
    mock_core.relative_src_path.return_value = str(main_cpp)
 | 
			
		||||
    mock_core.cpp_global_section = "// Global section"
 | 
			
		||||
 | 
			
		||||
    # Call the function
 | 
			
		||||
    test_code = "  // New generated code"
 | 
			
		||||
    write_cpp(test_code)
 | 
			
		||||
 | 
			
		||||
    # Verify copy_src_tree was called
 | 
			
		||||
    mock_copy_src_tree.assert_called_once()
 | 
			
		||||
 | 
			
		||||
    # Get the content that would be written
 | 
			
		||||
    mock_write_file.assert_called_once()
 | 
			
		||||
    written_path, written_content = mock_write_file.call_args[0]
 | 
			
		||||
 | 
			
		||||
    # Check that markers are preserved and content is updated
 | 
			
		||||
    assert CPP_INCLUDE_BEGIN in written_content
 | 
			
		||||
    assert CPP_INCLUDE_END in written_content
 | 
			
		||||
    assert CPP_AUTO_GENERATE_BEGIN in written_content
 | 
			
		||||
    assert CPP_AUTO_GENERATE_END in written_content
 | 
			
		||||
    assert test_code in written_content
 | 
			
		||||
    assert "// Global section" in written_content
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@patch("esphome.writer.write_file_if_changed")  # Mock to capture output
 | 
			
		||||
@patch("esphome.writer.copy_src_tree")  # Keep this mock as it's complex
 | 
			
		||||
@patch("esphome.writer.CORE")
 | 
			
		||||
def test_write_cpp_creates_new_file(
 | 
			
		||||
    mock_core: MagicMock,
 | 
			
		||||
    mock_copy_src_tree: MagicMock,
 | 
			
		||||
    mock_write_file: MagicMock,
 | 
			
		||||
    tmp_path: Path,
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test write_cpp when main.cpp doesn't exist."""
 | 
			
		||||
    # Setup path for new file
 | 
			
		||||
    main_cpp = tmp_path / "main.cpp"
 | 
			
		||||
 | 
			
		||||
    # Setup mocks
 | 
			
		||||
    mock_core.relative_src_path.return_value = str(main_cpp)
 | 
			
		||||
    mock_core.cpp_global_section = "// Global section"
 | 
			
		||||
 | 
			
		||||
    # Verify file doesn't exist
 | 
			
		||||
    assert not main_cpp.exists()
 | 
			
		||||
 | 
			
		||||
    # Call the function
 | 
			
		||||
    test_code = "  // Generated code"
 | 
			
		||||
    write_cpp(test_code)
 | 
			
		||||
 | 
			
		||||
    # Verify copy_src_tree was called
 | 
			
		||||
    mock_copy_src_tree.assert_called_once()
 | 
			
		||||
 | 
			
		||||
    # Get the content that would be written
 | 
			
		||||
    mock_write_file.assert_called_once()
 | 
			
		||||
    written_path, written_content = mock_write_file.call_args[0]
 | 
			
		||||
    assert written_path == str(main_cpp)
 | 
			
		||||
 | 
			
		||||
    # Check that all necessary parts are in the new file
 | 
			
		||||
    assert '#include "esphome.h"' in written_content
 | 
			
		||||
    assert CPP_INCLUDE_BEGIN in written_content
 | 
			
		||||
    assert CPP_INCLUDE_END in written_content
 | 
			
		||||
    assert CPP_AUTO_GENERATE_BEGIN in written_content
 | 
			
		||||
    assert CPP_AUTO_GENERATE_END in written_content
 | 
			
		||||
    assert test_code in written_content
 | 
			
		||||
    assert "void setup()" in written_content
 | 
			
		||||
    assert "void loop()" in written_content
 | 
			
		||||
    assert "App.setup();" in written_content
 | 
			
		||||
    assert "App.loop();" in written_content
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.usefixtures("mock_copy_src_tree")
 | 
			
		||||
@patch("esphome.writer.CORE")
 | 
			
		||||
def test_write_cpp_with_missing_end_marker(
 | 
			
		||||
    mock_core: MagicMock,
 | 
			
		||||
    tmp_path: Path,
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test write_cpp raises error when end marker is missing."""
 | 
			
		||||
    # Create a file with begin marker but no end marker
 | 
			
		||||
    main_cpp = tmp_path / "main.cpp"
 | 
			
		||||
    existing_content = f"""#include "esphome.h"
 | 
			
		||||
{CPP_AUTO_GENERATE_BEGIN}
 | 
			
		||||
// Code without end marker"""
 | 
			
		||||
    main_cpp.write_text(existing_content)
 | 
			
		||||
 | 
			
		||||
    # Setup mocks
 | 
			
		||||
    mock_core.relative_src_path.return_value = str(main_cpp)
 | 
			
		||||
 | 
			
		||||
    # Call should raise an error
 | 
			
		||||
    with pytest.raises(EsphomeError, match="Could not find auto generated code end"):
 | 
			
		||||
        write_cpp("// New code")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.usefixtures("mock_copy_src_tree")
 | 
			
		||||
@patch("esphome.writer.CORE")
 | 
			
		||||
def test_write_cpp_with_duplicate_markers(
 | 
			
		||||
    mock_core: MagicMock,
 | 
			
		||||
    tmp_path: Path,
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test write_cpp raises error when duplicate markers exist."""
 | 
			
		||||
    # Create a file with duplicate begin markers
 | 
			
		||||
    main_cpp = tmp_path / "main.cpp"
 | 
			
		||||
    existing_content = f"""#include "esphome.h"
 | 
			
		||||
{CPP_AUTO_GENERATE_BEGIN}
 | 
			
		||||
// First section
 | 
			
		||||
{CPP_AUTO_GENERATE_END}
 | 
			
		||||
{CPP_AUTO_GENERATE_BEGIN}
 | 
			
		||||
// Duplicate section
 | 
			
		||||
{CPP_AUTO_GENERATE_END}"""
 | 
			
		||||
    main_cpp.write_text(existing_content)
 | 
			
		||||
 | 
			
		||||
    # Setup mocks
 | 
			
		||||
    mock_core.relative_src_path.return_value = str(main_cpp)
 | 
			
		||||
 | 
			
		||||
    # Call should raise an error
 | 
			
		||||
    with pytest.raises(EsphomeError, match="Found multiple auto generate code begins"):
 | 
			
		||||
        write_cpp("// New code")
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user