mirror of
				https://github.com/esphome/esphome.git
				synced 2025-11-04 00:51:49 +00:00 
			
		
		
		
	Compare commits
	
		
			24 Commits
		
	
	
		
			2025.9.0b1
			...
			jesserockz
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					b6e0502203 | ||
| 
						 | 
					6d427bec49 | ||
| 
						 | 
					d3592c451b | ||
| 
						 | 
					24eb33a1c0 | ||
| 
						 | 
					cf1fef8cfb | ||
| 
						 | 
					28bba0666c | ||
| 
						 | 
					4390fd80a3 | ||
| 
						 | 
					4813c5134e | ||
| 
						 | 
					bbef0e173e | ||
| 
						 | 
					3240e19a7c | ||
| 
						 | 
					ac0cd946f0 | ||
| 
						 | 
					61bac6c6e6 | ||
| 
						 | 
					5fd64c5c89 | ||
| 
						 | 
					625f108183 | ||
| 
						 | 
					c45efe8f40 | ||
| 
						 | 
					fe1371f4dc | ||
| 
						 | 
					e3f8a36eaa | ||
| 
						 | 
					41f0d1c622 | ||
| 
						 | 
					6469bb168d | ||
| 
						 | 
					af0da3f897 | ||
| 
						 | 
					32e4eb26ad | ||
| 
						 | 
					10aae33979 | ||
| 
						 | 
					55dd12c66b | ||
| 
						 | 
					9dd17b464d | 
@@ -11,7 +11,7 @@ ci:
 | 
			
		||||
repos:
 | 
			
		||||
  - repo: https://github.com/astral-sh/ruff-pre-commit
 | 
			
		||||
    # Ruff version.
 | 
			
		||||
    rev: v0.12.12
 | 
			
		||||
    rev: v0.13.0
 | 
			
		||||
    hooks:
 | 
			
		||||
      # Run the linter.
 | 
			
		||||
      - id: ruff
 | 
			
		||||
 
 | 
			
		||||
@@ -139,6 +139,7 @@ esphome/components/ens160_base/* @latonita @vincentscode
 | 
			
		||||
esphome/components/ens160_i2c/* @latonita
 | 
			
		||||
esphome/components/ens160_spi/* @latonita
 | 
			
		||||
esphome/components/ens210/* @itn3rd77
 | 
			
		||||
esphome/components/epdiy/* @jesserockz
 | 
			
		||||
esphome/components/es7210/* @kahrendt
 | 
			
		||||
esphome/components/es7243e/* @kbx81
 | 
			
		||||
esphome/components/es8156/* @kbx81
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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.10.0-dev
 | 
			
		||||
 | 
			
		||||
# 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"
 | 
			
		||||
 | 
			
		||||
    return log_topic[CONF_LEVEL] != "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},
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										0
									
								
								esphome/components/epdiy/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								esphome/components/epdiy/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										107
									
								
								esphome/components/epdiy/display.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								esphome/components/epdiy/display.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,107 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import display, esp32
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_FULL_UPDATE_EVERY,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_LAMBDA,
 | 
			
		||||
    CONF_MODEL,
 | 
			
		||||
    CONF_PAGES,
 | 
			
		||||
)
 | 
			
		||||
from esphome.cpp_generator import MockObj
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@jesserockz"]
 | 
			
		||||
DEPENDENCIES = ["esp32", "psram"]
 | 
			
		||||
 | 
			
		||||
CONF_POWER_OFF_DELAY_ENABLED = "power_off_delay_enabled"
 | 
			
		||||
 | 
			
		||||
epdiy_ns = cg.esphome_ns.namespace("epdiy")
 | 
			
		||||
EPDiyDisplay = epdiy_ns.class_("EPDiyDisplay", display.Display)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class EpdBoardDefinition(MockObj):
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return f"&{self.base}"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class EpdDisplay_t(MockObj):
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return f"&{self.base}"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
EpdInitOptions = cg.global_ns.enum("EpdInitOptions")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Model:
 | 
			
		||||
    def __init__(
 | 
			
		||||
        self,
 | 
			
		||||
        *,
 | 
			
		||||
        board_definition: MockObj,
 | 
			
		||||
        display_t: MockObj,
 | 
			
		||||
        init_options: MockObj,
 | 
			
		||||
        width: int,
 | 
			
		||||
        height: int,
 | 
			
		||||
        vcom_mv: int = 0,
 | 
			
		||||
    ):
 | 
			
		||||
        self.board_definition = board_definition
 | 
			
		||||
        self.display_t = display_t
 | 
			
		||||
        self.init_options = init_options
 | 
			
		||||
        self.width = width
 | 
			
		||||
        self.height = height
 | 
			
		||||
        self.vcom_mv = vcom_mv
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
MODELS: dict[str, Model] = {
 | 
			
		||||
    "lilygo_t5_4.7": Model(
 | 
			
		||||
        board_definition=EpdBoardDefinition("epd_board_lilygo_t5_47"),
 | 
			
		||||
        display_t=EpdDisplay_t("ED047TC2"),
 | 
			
		||||
        init_options=(EpdInitOptions.EPD_LUT_64K, EpdInitOptions.EPD_FEED_QUEUE_8),
 | 
			
		||||
        width=960,
 | 
			
		||||
        height=540,
 | 
			
		||||
    ),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    display.FULL_DISPLAY_SCHEMA.extend(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(EPDiyDisplay),
 | 
			
		||||
            cv.Required(CONF_MODEL): cv.one_of(*MODELS.keys()),
 | 
			
		||||
            cv.Optional(CONF_FULL_UPDATE_EVERY, default=10): cv.uint32_t,
 | 
			
		||||
            cv.Optional(CONF_POWER_OFF_DELAY_ENABLED, default=False): cv.boolean,
 | 
			
		||||
        }
 | 
			
		||||
    ).extend(cv.polling_component_schema("60s")),
 | 
			
		||||
    cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA),
 | 
			
		||||
    cv.only_with_esp_idf,  # When trying to add library via platformio it breaks, using as an idf component works fine
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
 | 
			
		||||
    await display.register_display(var, config)
 | 
			
		||||
 | 
			
		||||
    model = MODELS[config[CONF_MODEL]]
 | 
			
		||||
    cg.add(
 | 
			
		||||
        var.set_model_details(
 | 
			
		||||
            model.board_definition,
 | 
			
		||||
            model.display_t,
 | 
			
		||||
            cg.RawExpression(
 | 
			
		||||
                f"static_cast<EpdInitOptions>({'|'.join(str(o) for o in model.init_options)})"
 | 
			
		||||
            ),
 | 
			
		||||
            model.vcom_mv,
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    if CONF_LAMBDA in config:
 | 
			
		||||
        lambda_ = await cg.process_lambda(
 | 
			
		||||
            config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void
 | 
			
		||||
        )
 | 
			
		||||
        cg.add(var.set_writer(lambda_))
 | 
			
		||||
 | 
			
		||||
    cg.add(var.set_power_off_delay_enabled(config[CONF_POWER_OFF_DELAY_ENABLED]))
 | 
			
		||||
 | 
			
		||||
    esp32.add_idf_component(
 | 
			
		||||
        name="vroland/epdiy",
 | 
			
		||||
        repo="https://github.com/vroland/epdiy",
 | 
			
		||||
        ref="c61e9e923ce2418150d54f88cea5d196cdc40c54",
 | 
			
		||||
    )
 | 
			
		||||
							
								
								
									
										76
									
								
								esphome/components/epdiy/epdiy_display.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								esphome/components/epdiy/epdiy_display.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,76 @@
 | 
			
		||||
#include "epdiy_display.h"
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/application.h"
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP_IDF
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome::epdiy {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "epdiy";
 | 
			
		||||
 | 
			
		||||
static constexpr uint8_t TEMPERATURE = 23;  // default temperature for e-paper displays
 | 
			
		||||
 | 
			
		||||
float EPDiyDisplay::get_setup_priority() const { return esphome::setup_priority::LATE; }
 | 
			
		||||
 | 
			
		||||
void EPDiyDisplay::setup() {
 | 
			
		||||
  epd_init(this->board_definition_, this->display_t_, this->init_options_);
 | 
			
		||||
  if (this->vcom_mv_ != 0) {
 | 
			
		||||
    epd_set_vcom(this->vcom_mv_);
 | 
			
		||||
  }
 | 
			
		||||
  this->state_ = epd_hl_init(nullptr);
 | 
			
		||||
  this->framebuffer_ = epd_hl_get_framebuffer(&this->state_);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void EPDiyDisplay::update() {
 | 
			
		||||
  this->do_update_();
 | 
			
		||||
  this->defer([this]() { this->flush_screen_changes_(); });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void EPDiyDisplay::fill(Color color) {
 | 
			
		||||
  if (color == display::COLOR_OFF) {
 | 
			
		||||
    memset(this->framebuffer_, 0xFF, this->get_buffer_length());
 | 
			
		||||
 | 
			
		||||
    epd_poweron();
 | 
			
		||||
    epd_hl_update_screen(&this->state_, MODE_GC16, TEMPERATURE);
 | 
			
		||||
    epd_clear();
 | 
			
		||||
 | 
			
		||||
    epd_poweroff();
 | 
			
		||||
    App.feed_wdt();
 | 
			
		||||
  } else {
 | 
			
		||||
    Display::fill(color);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void EPDiyDisplay::flush_screen_changes_() {
 | 
			
		||||
  epd_poweron();
 | 
			
		||||
 | 
			
		||||
  epd_hl_update_screen(&this->state_, MODE_GC16, TEMPERATURE);
 | 
			
		||||
  memset(this->state_.back_fb, 0xFF, this->get_buffer_length());
 | 
			
		||||
 | 
			
		||||
  uint16_t delay = 0;
 | 
			
		||||
  if (this->power_off_delay_enabled_) {
 | 
			
		||||
    delay = 700;
 | 
			
		||||
  }
 | 
			
		||||
  this->set_timeout("poweroff", delay, []() { epd_poweroff(); });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void EPDiyDisplay::on_shutdown() {
 | 
			
		||||
  epd_poweroff();
 | 
			
		||||
  epd_deinit();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HOT EPDiyDisplay::draw_pixel_at(int x, int y, Color color) {
 | 
			
		||||
  if (color.red == 255 && color.green == 255 && color.blue == 255) {
 | 
			
		||||
    epd_draw_pixel(x, y, 0, this->framebuffer_);
 | 
			
		||||
  } else {
 | 
			
		||||
    int col = (0.2126 * color.red) + (0.7152 * color.green) + (0.0722 * color.blue);
 | 
			
		||||
    int cl = 255 - col;
 | 
			
		||||
    epd_draw_pixel(x, y, cl, this->framebuffer_);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome::epdiy
 | 
			
		||||
 | 
			
		||||
#endif  // USE_ESP_IDF
 | 
			
		||||
							
								
								
									
										63
									
								
								esphome/components/epdiy/epdiy_display.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								esphome/components/epdiy/epdiy_display.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,63 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP_IDF
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
 | 
			
		||||
#include "esphome/components/display/display_buffer.h"
 | 
			
		||||
#include "esphome/components/display/display_color_utils.h"
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
#include "esphome/core/version.h"
 | 
			
		||||
 | 
			
		||||
#include "epd_display.h"
 | 
			
		||||
#include "epd_highlevel.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome::epdiy {
 | 
			
		||||
 | 
			
		||||
class EPDiyDisplay : public display::Display {
 | 
			
		||||
 public:
 | 
			
		||||
  float get_setup_priority() const override;
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  void update() override;
 | 
			
		||||
  void on_shutdown() override;
 | 
			
		||||
 | 
			
		||||
  display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_GRAYSCALE; }
 | 
			
		||||
 | 
			
		||||
  int get_width_internal() override { return this->display_t_->width; };
 | 
			
		||||
  int get_height_internal() override { return this->display_t_->height; };
 | 
			
		||||
 | 
			
		||||
  size_t get_buffer_length() const { return this->display_t_->width / 2 * this->display_t_->height; }
 | 
			
		||||
 | 
			
		||||
  void set_power_off_delay_enabled(bool power_off_delay_enabled) {
 | 
			
		||||
    this->power_off_delay_enabled_ = power_off_delay_enabled;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void set_model_details(const EpdBoardDefinition *board_definition, const EpdDisplay_t *display_t,
 | 
			
		||||
                         enum EpdInitOptions init_options, uint16_t vcom) {
 | 
			
		||||
    this->board_definition_ = board_definition;
 | 
			
		||||
    this->display_t_ = display_t;
 | 
			
		||||
    this->init_options_ = init_options;
 | 
			
		||||
    this->vcom_mv_ = vcom;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void fill(Color color) override;
 | 
			
		||||
 | 
			
		||||
  void draw_pixel_at(int x, int y, Color color) override;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void flush_screen_changes_();
 | 
			
		||||
  EpdiyHighlevelState state_;
 | 
			
		||||
 | 
			
		||||
  uint8_t *framebuffer_;
 | 
			
		||||
 | 
			
		||||
  const EpdBoardDefinition *board_definition_;
 | 
			
		||||
  const EpdDisplay_t *display_t_;
 | 
			
		||||
  enum EpdInitOptions init_options_;
 | 
			
		||||
  uint16_t vcom_mv_;
 | 
			
		||||
 | 
			
		||||
  bool power_off_delay_enabled_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome::epdiy
 | 
			
		||||
 | 
			
		||||
#endif  // USE_ESP_IDF
 | 
			
		||||
@@ -353,6 +353,7 @@ SUPPORTED_PLATFORMIO_ESP_IDF_5X = [
 | 
			
		||||
# pioarduino versions that don't require a release number
 | 
			
		||||
# List based on https://github.com/pioarduino/esp-idf/releases
 | 
			
		||||
SUPPORTED_PIOARDUINO_ESP_IDF_5X = [
 | 
			
		||||
    cv.Version(5, 5, 1),
 | 
			
		||||
    cv.Version(5, 5, 0),
 | 
			
		||||
    cv.Version(5, 4, 2),
 | 
			
		||||
    cv.Version(5, 4, 1),
 | 
			
		||||
 
 | 
			
		||||
@@ -43,13 +43,6 @@ void BLEClientBase::setup() {
 | 
			
		||||
void BLEClientBase::set_state(espbt::ClientState st) {
 | 
			
		||||
  ESP_LOGV(TAG, "[%d] [%s] Set state %d", this->connection_index_, this->address_str_.c_str(), (int) st);
 | 
			
		||||
  ESPBTClient::set_state(st);
 | 
			
		||||
 | 
			
		||||
  if (st == espbt::ClientState::READY_TO_CONNECT) {
 | 
			
		||||
    // Enable loop for state processing
 | 
			
		||||
    this->enable_loop();
 | 
			
		||||
    // Connect immediately instead of waiting for next loop
 | 
			
		||||
    this->connect();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BLEClientBase::loop() {
 | 
			
		||||
@@ -65,8 +58,8 @@ void BLEClientBase::loop() {
 | 
			
		||||
    }
 | 
			
		||||
    this->set_state(espbt::ClientState::IDLE);
 | 
			
		||||
  }
 | 
			
		||||
  // If its idle, we can disable the loop as set_state
 | 
			
		||||
  // will enable it again when we need to connect.
 | 
			
		||||
  // If idle, we can disable the loop as connect()
 | 
			
		||||
  // will enable it again when a connection is needed.
 | 
			
		||||
  else if (this->state_ == espbt::ClientState::IDLE) {
 | 
			
		||||
    this->disable_loop();
 | 
			
		||||
  }
 | 
			
		||||
@@ -108,9 +101,20 @@ bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) {
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
void BLEClientBase::connect() {
 | 
			
		||||
  // Prevent duplicate connection attempts
 | 
			
		||||
  if (this->state_ == espbt::ClientState::CONNECTING || this->state_ == espbt::ClientState::CONNECTED ||
 | 
			
		||||
      this->state_ == espbt::ClientState::ESTABLISHED) {
 | 
			
		||||
    ESP_LOGW(TAG, "[%d] [%s] Connection already in progress, state=%s", this->connection_index_,
 | 
			
		||||
             this->address_str_.c_str(), espbt::client_state_to_string(this->state_));
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  ESP_LOGI(TAG, "[%d] [%s] 0x%02x Connecting", this->connection_index_, this->address_str_.c_str(),
 | 
			
		||||
           this->remote_addr_type_);
 | 
			
		||||
  this->paired_ = false;
 | 
			
		||||
  // Enable loop for state processing
 | 
			
		||||
  this->enable_loop();
 | 
			
		||||
  // Immediately transition to CONNECTING to prevent duplicate connection attempts
 | 
			
		||||
  this->set_state(espbt::ClientState::CONNECTING);
 | 
			
		||||
 | 
			
		||||
  // Determine connection parameters based on connection type
 | 
			
		||||
  if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) {
 | 
			
		||||
@@ -168,7 +172,7 @@ void BLEClientBase::unconditional_disconnect() {
 | 
			
		||||
    this->log_gattc_warning_("esp_ble_gattc_close", err);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (this->state_ == espbt::ClientState::READY_TO_CONNECT || this->state_ == espbt::ClientState::DISCOVERED) {
 | 
			
		||||
  if (this->state_ == espbt::ClientState::DISCOVERED) {
 | 
			
		||||
    this->set_address(0);
 | 
			
		||||
    this->set_state(espbt::ClientState::IDLE);
 | 
			
		||||
  } else {
 | 
			
		||||
@@ -212,8 +216,6 @@ void BLEClientBase::handle_connection_result_(esp_err_t ret) {
 | 
			
		||||
  if (ret) {
 | 
			
		||||
    this->log_gattc_warning_("esp_ble_gattc_open", ret);
 | 
			
		||||
    this->set_state(espbt::ClientState::IDLE);
 | 
			
		||||
  } else {
 | 
			
		||||
    this->set_state(espbt::ClientState::CONNECTING);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -51,8 +51,6 @@ const char *client_state_to_string(ClientState state) {
 | 
			
		||||
      return "IDLE";
 | 
			
		||||
    case ClientState::DISCOVERED:
 | 
			
		||||
      return "DISCOVERED";
 | 
			
		||||
    case ClientState::READY_TO_CONNECT:
 | 
			
		||||
      return "READY_TO_CONNECT";
 | 
			
		||||
    case ClientState::CONNECTING:
 | 
			
		||||
      return "CONNECTING";
 | 
			
		||||
    case ClientState::CONNECTED:
 | 
			
		||||
@@ -795,7 +793,7 @@ void ESP32BLETracker::try_promote_discovered_clients_() {
 | 
			
		||||
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
 | 
			
		||||
    this->update_coex_preference_(true);
 | 
			
		||||
#endif
 | 
			
		||||
    client->set_state(ClientState::READY_TO_CONNECT);
 | 
			
		||||
    client->connect();
 | 
			
		||||
    break;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -159,8 +159,6 @@ enum class ClientState : uint8_t {
 | 
			
		||||
  IDLE,
 | 
			
		||||
  // Device advertisement found.
 | 
			
		||||
  DISCOVERED,
 | 
			
		||||
  // Device is discovered and the scanner is stopped
 | 
			
		||||
  READY_TO_CONNECT,
 | 
			
		||||
  // Connection in progress.
 | 
			
		||||
  CONNECTING,
 | 
			
		||||
  // Initial connection established.
 | 
			
		||||
@@ -313,7 +311,6 @@ class ESP32BLETracker : public Component,
 | 
			
		||||
          counts.discovered++;
 | 
			
		||||
          break;
 | 
			
		||||
        case ClientState::CONNECTING:
 | 
			
		||||
        case ClientState::READY_TO_CONNECT:
 | 
			
		||||
          counts.connecting++;
 | 
			
		||||
          break;
 | 
			
		||||
        default:
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
 
 | 
			
		||||
@@ -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};
 | 
			
		||||
 
 | 
			
		||||
@@ -270,6 +270,7 @@ void PacketTransport::add_binary_data_(uint8_t key, const char *id, bool data) {
 | 
			
		||||
  auto len = 1 + 1 + 1 + strlen(id);
 | 
			
		||||
  if (len + this->header_.size() + this->data_.size() > this->get_max_packet_size()) {
 | 
			
		||||
    this->flush_();
 | 
			
		||||
    this->init_data_();
 | 
			
		||||
  }
 | 
			
		||||
  add(this->data_, key);
 | 
			
		||||
  add(this->data_, (uint8_t) data);
 | 
			
		||||
@@ -284,6 +285,7 @@ void PacketTransport::add_data_(uint8_t key, const char *id, uint32_t data) {
 | 
			
		||||
  auto len = 4 + 1 + 1 + strlen(id);
 | 
			
		||||
  if (len + this->header_.size() + this->data_.size() > this->get_max_packet_size()) {
 | 
			
		||||
    this->flush_();
 | 
			
		||||
    this->init_data_();
 | 
			
		||||
  }
 | 
			
		||||
  add(this->data_, key);
 | 
			
		||||
  add(this->data_, data);
 | 
			
		||||
 
 | 
			
		||||
@@ -196,8 +196,8 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform(
 | 
			
		||||
            PlatformFramework.ESP32_ARDUINO,
 | 
			
		||||
            PlatformFramework.ESP32_IDF,
 | 
			
		||||
        },
 | 
			
		||||
        "remote_receiver_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO},
 | 
			
		||||
        "remote_receiver_libretiny.cpp": {
 | 
			
		||||
        "remote_receiver.cpp": {
 | 
			
		||||
            PlatformFramework.ESP8266_ARDUINO,
 | 
			
		||||
            PlatformFramework.BK72XX_ARDUINO,
 | 
			
		||||
            PlatformFramework.RTL87XX_ARDUINO,
 | 
			
		||||
            PlatformFramework.LN882X_ARDUINO,
 | 
			
		||||
 
 | 
			
		||||
@@ -3,12 +3,12 @@
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP8266
 | 
			
		||||
#if defined(USE_LIBRETINY) || defined(USE_ESP8266)
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace remote_receiver {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "remote_receiver.esp8266";
 | 
			
		||||
static const char *const TAG = "remote_receiver";
 | 
			
		||||
 | 
			
		||||
void IRAM_ATTR HOT RemoteReceiverComponentStore::gpio_intr(RemoteReceiverComponentStore *arg) {
 | 
			
		||||
  const uint32_t now = micros();
 | 
			
		||||
@@ -1,125 +0,0 @@
 | 
			
		||||
#include "remote_receiver.h"
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
#ifdef USE_LIBRETINY
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace remote_receiver {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "remote_receiver.libretiny";
 | 
			
		||||
 | 
			
		||||
void IRAM_ATTR HOT RemoteReceiverComponentStore::gpio_intr(RemoteReceiverComponentStore *arg) {
 | 
			
		||||
  const uint32_t now = micros();
 | 
			
		||||
  // If the lhs is 1 (rising edge) we should write to an uneven index and vice versa
 | 
			
		||||
  const uint32_t next = (arg->buffer_write_at + 1) % arg->buffer_size;
 | 
			
		||||
  const bool level = arg->pin.digital_read();
 | 
			
		||||
  if (level != next % 2)
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
  // If next is buffer_read, we have hit an overflow
 | 
			
		||||
  if (next == arg->buffer_read_at)
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
  const uint32_t last_change = arg->buffer[arg->buffer_write_at];
 | 
			
		||||
  const uint32_t time_since_change = now - last_change;
 | 
			
		||||
  if (time_since_change <= arg->filter_us)
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
  arg->buffer[arg->buffer_write_at = next] = now;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void RemoteReceiverComponent::setup() {
 | 
			
		||||
  this->pin_->setup();
 | 
			
		||||
  auto &s = this->store_;
 | 
			
		||||
  s.filter_us = this->filter_us_;
 | 
			
		||||
  s.pin = this->pin_->to_isr();
 | 
			
		||||
  s.buffer_size = this->buffer_size_;
 | 
			
		||||
 | 
			
		||||
  this->high_freq_.start();
 | 
			
		||||
  if (s.buffer_size % 2 != 0) {
 | 
			
		||||
    // Make sure divisible by two. This way, we know that every 0bxxx0 index is a space and every 0bxxx1 index is a mark
 | 
			
		||||
    s.buffer_size++;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  s.buffer = new uint32_t[s.buffer_size];
 | 
			
		||||
  void *buf = (void *) s.buffer;
 | 
			
		||||
  memset(buf, 0, s.buffer_size * sizeof(uint32_t));
 | 
			
		||||
 | 
			
		||||
  // First index is a space.
 | 
			
		||||
  if (this->pin_->digital_read()) {
 | 
			
		||||
    s.buffer_write_at = s.buffer_read_at = 1;
 | 
			
		||||
  } else {
 | 
			
		||||
    s.buffer_write_at = s.buffer_read_at = 0;
 | 
			
		||||
  }
 | 
			
		||||
  this->pin_->attach_interrupt(RemoteReceiverComponentStore::gpio_intr, &this->store_, gpio::INTERRUPT_ANY_EDGE);
 | 
			
		||||
}
 | 
			
		||||
void RemoteReceiverComponent::dump_config() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Remote Receiver:");
 | 
			
		||||
  LOG_PIN("  Pin: ", this->pin_);
 | 
			
		||||
  if (this->pin_->digital_read()) {
 | 
			
		||||
    ESP_LOGW(TAG, "Remote Receiver Signal starts with a HIGH value. Usually this means you have to "
 | 
			
		||||
                  "invert the signal using 'inverted: True' in the pin schema!");
 | 
			
		||||
  }
 | 
			
		||||
  ESP_LOGCONFIG(TAG,
 | 
			
		||||
                "  Buffer Size: %u\n"
 | 
			
		||||
                "  Tolerance: %u%s\n"
 | 
			
		||||
                "  Filter out pulses shorter than: %u us\n"
 | 
			
		||||
                "  Signal is done after %u us of no changes",
 | 
			
		||||
                this->buffer_size_, this->tolerance_,
 | 
			
		||||
                (this->tolerance_mode_ == remote_base::TOLERANCE_MODE_TIME) ? " us" : "%", this->filter_us_,
 | 
			
		||||
                this->idle_us_);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void RemoteReceiverComponent::loop() {
 | 
			
		||||
  auto &s = this->store_;
 | 
			
		||||
 | 
			
		||||
  // copy write at to local variables, as it's volatile
 | 
			
		||||
  const uint32_t write_at = s.buffer_write_at;
 | 
			
		||||
  const uint32_t dist = (s.buffer_size + write_at - s.buffer_read_at) % s.buffer_size;
 | 
			
		||||
  // signals must at least one rising and one leading edge
 | 
			
		||||
  if (dist <= 1)
 | 
			
		||||
    return;
 | 
			
		||||
  const uint32_t now = micros();
 | 
			
		||||
  if (now - s.buffer[write_at] < this->idle_us_) {
 | 
			
		||||
    // The last change was fewer than the configured idle time ago.
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ESP_LOGVV(TAG, "read_at=%u write_at=%u dist=%u now=%u end=%u", s.buffer_read_at, write_at, dist, now,
 | 
			
		||||
            s.buffer[write_at]);
 | 
			
		||||
 | 
			
		||||
  // Skip first value, it's from the previous idle level
 | 
			
		||||
  s.buffer_read_at = (s.buffer_read_at + 1) % s.buffer_size;
 | 
			
		||||
  uint32_t prev = s.buffer_read_at;
 | 
			
		||||
  s.buffer_read_at = (s.buffer_read_at + 1) % s.buffer_size;
 | 
			
		||||
  const uint32_t reserve_size = 1 + (s.buffer_size + write_at - s.buffer_read_at) % s.buffer_size;
 | 
			
		||||
  this->temp_.clear();
 | 
			
		||||
  this->temp_.reserve(reserve_size);
 | 
			
		||||
  int32_t multiplier = s.buffer_read_at % 2 == 0 ? 1 : -1;
 | 
			
		||||
 | 
			
		||||
  for (uint32_t i = 0; prev != write_at; i++) {
 | 
			
		||||
    int32_t delta = s.buffer[s.buffer_read_at] - s.buffer[prev];
 | 
			
		||||
    if (uint32_t(delta) >= this->idle_us_) {
 | 
			
		||||
      // already found a space longer than idle. There must have been two pulses
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ESP_LOGVV(TAG, "  i=%u buffer[%u]=%u - buffer[%u]=%u -> %d", i, s.buffer_read_at, s.buffer[s.buffer_read_at], prev,
 | 
			
		||||
              s.buffer[prev], multiplier * delta);
 | 
			
		||||
    this->temp_.push_back(multiplier * delta);
 | 
			
		||||
    prev = s.buffer_read_at;
 | 
			
		||||
    s.buffer_read_at = (s.buffer_read_at + 1) % s.buffer_size;
 | 
			
		||||
    multiplier *= -1;
 | 
			
		||||
  }
 | 
			
		||||
  s.buffer_read_at = (s.buffer_size + s.buffer_read_at - 1) % s.buffer_size;
 | 
			
		||||
  this->temp_.push_back(this->idle_us_ * multiplier);
 | 
			
		||||
 | 
			
		||||
  this->call_listeners_dumpers_();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace remote_receiver
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
@@ -131,8 +131,8 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform(
 | 
			
		||||
            PlatformFramework.ESP32_ARDUINO,
 | 
			
		||||
            PlatformFramework.ESP32_IDF,
 | 
			
		||||
        },
 | 
			
		||||
        "remote_transmitter_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO},
 | 
			
		||||
        "remote_transmitter_libretiny.cpp": {
 | 
			
		||||
        "remote_transmitter.cpp": {
 | 
			
		||||
            PlatformFramework.ESP8266_ARDUINO,
 | 
			
		||||
            PlatformFramework.BK72XX_ARDUINO,
 | 
			
		||||
            PlatformFramework.RTL87XX_ARDUINO,
 | 
			
		||||
            PlatformFramework.LN882X_ARDUINO,
 | 
			
		||||
 
 | 
			
		||||
@@ -2,10 +2,107 @@
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/core/application.h"
 | 
			
		||||
 | 
			
		||||
#if defined(USE_LIBRETINY) || defined(USE_ESP8266)
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace remote_transmitter {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "remote_transmitter";
 | 
			
		||||
 | 
			
		||||
void RemoteTransmitterComponent::setup() {
 | 
			
		||||
  this->pin_->setup();
 | 
			
		||||
  this->pin_->digital_write(false);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void RemoteTransmitterComponent::dump_config() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG,
 | 
			
		||||
                "Remote Transmitter:\n"
 | 
			
		||||
                "  Carrier Duty: %u%%",
 | 
			
		||||
                this->carrier_duty_percent_);
 | 
			
		||||
  LOG_PIN("  Pin: ", this->pin_);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void RemoteTransmitterComponent::calculate_on_off_time_(uint32_t carrier_frequency, uint32_t *on_time_period,
 | 
			
		||||
                                                        uint32_t *off_time_period) {
 | 
			
		||||
  if (carrier_frequency == 0) {
 | 
			
		||||
    *on_time_period = 0;
 | 
			
		||||
    *off_time_period = 0;
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  uint32_t period = (1000000UL + carrier_frequency / 2) / carrier_frequency;  // round(1000000/freq)
 | 
			
		||||
  period = std::max(uint32_t(1), period);
 | 
			
		||||
  *on_time_period = (period * this->carrier_duty_percent_) / 100;
 | 
			
		||||
  *off_time_period = period - *on_time_period;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void RemoteTransmitterComponent::await_target_time_() {
 | 
			
		||||
  const uint32_t current_time = micros();
 | 
			
		||||
  if (this->target_time_ == 0) {
 | 
			
		||||
    this->target_time_ = current_time;
 | 
			
		||||
  } else if ((int32_t) (this->target_time_ - current_time) > 0) {
 | 
			
		||||
    delayMicroseconds(this->target_time_ - current_time);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void RemoteTransmitterComponent::mark_(uint32_t on_time, uint32_t off_time, uint32_t usec) {
 | 
			
		||||
  this->await_target_time_();
 | 
			
		||||
  this->pin_->digital_write(true);
 | 
			
		||||
 | 
			
		||||
  const uint32_t target = this->target_time_ + usec;
 | 
			
		||||
  if (this->carrier_duty_percent_ < 100 && (on_time > 0 || off_time > 0)) {
 | 
			
		||||
    while (true) {  // Modulate with carrier frequency
 | 
			
		||||
      this->target_time_ += on_time;
 | 
			
		||||
      if ((int32_t) (this->target_time_ - target) >= 0)
 | 
			
		||||
        break;
 | 
			
		||||
      this->await_target_time_();
 | 
			
		||||
      this->pin_->digital_write(false);
 | 
			
		||||
 | 
			
		||||
      this->target_time_ += off_time;
 | 
			
		||||
      if ((int32_t) (this->target_time_ - target) >= 0)
 | 
			
		||||
        break;
 | 
			
		||||
      this->await_target_time_();
 | 
			
		||||
      this->pin_->digital_write(true);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  this->target_time_ = target;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void RemoteTransmitterComponent::space_(uint32_t usec) {
 | 
			
		||||
  this->await_target_time_();
 | 
			
		||||
  this->pin_->digital_write(false);
 | 
			
		||||
  this->target_time_ += usec;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void RemoteTransmitterComponent::digital_write(bool value) { this->pin_->digital_write(value); }
 | 
			
		||||
 | 
			
		||||
void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) {
 | 
			
		||||
  ESP_LOGD(TAG, "Sending remote code");
 | 
			
		||||
  uint32_t on_time, off_time;
 | 
			
		||||
  this->calculate_on_off_time_(this->temp_.get_carrier_frequency(), &on_time, &off_time);
 | 
			
		||||
  this->target_time_ = 0;
 | 
			
		||||
  this->transmit_trigger_->trigger();
 | 
			
		||||
  for (uint32_t i = 0; i < send_times; i++) {
 | 
			
		||||
    InterruptLock lock;
 | 
			
		||||
    for (int32_t item : this->temp_.get_data()) {
 | 
			
		||||
      if (item > 0) {
 | 
			
		||||
        const auto length = uint32_t(item);
 | 
			
		||||
        this->mark_(on_time, off_time, length);
 | 
			
		||||
      } else {
 | 
			
		||||
        const auto length = uint32_t(-item);
 | 
			
		||||
        this->space_(length);
 | 
			
		||||
      }
 | 
			
		||||
      App.feed_wdt();
 | 
			
		||||
    }
 | 
			
		||||
    this->await_target_time_();  // wait for duration of last pulse
 | 
			
		||||
    this->pin_->digital_write(false);
 | 
			
		||||
 | 
			
		||||
    if (i + 1 < send_times)
 | 
			
		||||
      this->target_time_ += send_wait;
 | 
			
		||||
  }
 | 
			
		||||
  this->complete_trigger_->trigger();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace remote_transmitter
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
 
 | 
			
		||||
@@ -1,107 +0,0 @@
 | 
			
		||||
#include "remote_transmitter.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/core/application.h"
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP8266
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace remote_transmitter {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "remote_transmitter";
 | 
			
		||||
 | 
			
		||||
void RemoteTransmitterComponent::setup() {
 | 
			
		||||
  this->pin_->setup();
 | 
			
		||||
  this->pin_->digital_write(false);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void RemoteTransmitterComponent::dump_config() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG,
 | 
			
		||||
                "Remote Transmitter:\n"
 | 
			
		||||
                "  Carrier Duty: %u%%",
 | 
			
		||||
                this->carrier_duty_percent_);
 | 
			
		||||
  LOG_PIN("  Pin: ", this->pin_);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void RemoteTransmitterComponent::calculate_on_off_time_(uint32_t carrier_frequency, uint32_t *on_time_period,
 | 
			
		||||
                                                        uint32_t *off_time_period) {
 | 
			
		||||
  if (carrier_frequency == 0) {
 | 
			
		||||
    *on_time_period = 0;
 | 
			
		||||
    *off_time_period = 0;
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  uint32_t period = (1000000UL + carrier_frequency / 2) / carrier_frequency;  // round(1000000/freq)
 | 
			
		||||
  period = std::max(uint32_t(1), period);
 | 
			
		||||
  *on_time_period = (period * this->carrier_duty_percent_) / 100;
 | 
			
		||||
  *off_time_period = period - *on_time_period;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void RemoteTransmitterComponent::await_target_time_() {
 | 
			
		||||
  const uint32_t current_time = micros();
 | 
			
		||||
  if (this->target_time_ == 0) {
 | 
			
		||||
    this->target_time_ = current_time;
 | 
			
		||||
  } else if ((int32_t) (this->target_time_ - current_time) > 0) {
 | 
			
		||||
    delayMicroseconds(this->target_time_ - current_time);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void RemoteTransmitterComponent::mark_(uint32_t on_time, uint32_t off_time, uint32_t usec) {
 | 
			
		||||
  this->await_target_time_();
 | 
			
		||||
  this->pin_->digital_write(true);
 | 
			
		||||
 | 
			
		||||
  const uint32_t target = this->target_time_ + usec;
 | 
			
		||||
  if (this->carrier_duty_percent_ < 100 && (on_time > 0 || off_time > 0)) {
 | 
			
		||||
    while (true) {  // Modulate with carrier frequency
 | 
			
		||||
      this->target_time_ += on_time;
 | 
			
		||||
      if ((int32_t) (this->target_time_ - target) >= 0)
 | 
			
		||||
        break;
 | 
			
		||||
      this->await_target_time_();
 | 
			
		||||
      this->pin_->digital_write(false);
 | 
			
		||||
 | 
			
		||||
      this->target_time_ += off_time;
 | 
			
		||||
      if ((int32_t) (this->target_time_ - target) >= 0)
 | 
			
		||||
        break;
 | 
			
		||||
      this->await_target_time_();
 | 
			
		||||
      this->pin_->digital_write(true);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  this->target_time_ = target;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void RemoteTransmitterComponent::space_(uint32_t usec) {
 | 
			
		||||
  this->await_target_time_();
 | 
			
		||||
  this->pin_->digital_write(false);
 | 
			
		||||
  this->target_time_ += usec;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void RemoteTransmitterComponent::digital_write(bool value) { this->pin_->digital_write(value); }
 | 
			
		||||
 | 
			
		||||
void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) {
 | 
			
		||||
  ESP_LOGD(TAG, "Sending remote code");
 | 
			
		||||
  uint32_t on_time, off_time;
 | 
			
		||||
  this->calculate_on_off_time_(this->temp_.get_carrier_frequency(), &on_time, &off_time);
 | 
			
		||||
  this->target_time_ = 0;
 | 
			
		||||
  this->transmit_trigger_->trigger();
 | 
			
		||||
  for (uint32_t i = 0; i < send_times; i++) {
 | 
			
		||||
    for (int32_t item : this->temp_.get_data()) {
 | 
			
		||||
      if (item > 0) {
 | 
			
		||||
        const auto length = uint32_t(item);
 | 
			
		||||
        this->mark_(on_time, off_time, length);
 | 
			
		||||
      } else {
 | 
			
		||||
        const auto length = uint32_t(-item);
 | 
			
		||||
        this->space_(length);
 | 
			
		||||
      }
 | 
			
		||||
      App.feed_wdt();
 | 
			
		||||
    }
 | 
			
		||||
    this->await_target_time_();  // wait for duration of last pulse
 | 
			
		||||
    this->pin_->digital_write(false);
 | 
			
		||||
 | 
			
		||||
    if (i + 1 < send_times)
 | 
			
		||||
      this->target_time_ += send_wait;
 | 
			
		||||
  }
 | 
			
		||||
  this->complete_trigger_->trigger();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace remote_transmitter
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
@@ -1,110 +0,0 @@
 | 
			
		||||
#include "remote_transmitter.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/core/application.h"
 | 
			
		||||
 | 
			
		||||
#ifdef USE_LIBRETINY
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace remote_transmitter {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "remote_transmitter";
 | 
			
		||||
 | 
			
		||||
void RemoteTransmitterComponent::setup() {
 | 
			
		||||
  this->pin_->setup();
 | 
			
		||||
  this->pin_->digital_write(false);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void RemoteTransmitterComponent::dump_config() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG,
 | 
			
		||||
                "Remote Transmitter:\n"
 | 
			
		||||
                "  Carrier Duty: %u%%",
 | 
			
		||||
                this->carrier_duty_percent_);
 | 
			
		||||
  LOG_PIN("  Pin: ", this->pin_);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void RemoteTransmitterComponent::calculate_on_off_time_(uint32_t carrier_frequency, uint32_t *on_time_period,
 | 
			
		||||
                                                        uint32_t *off_time_period) {
 | 
			
		||||
  if (carrier_frequency == 0) {
 | 
			
		||||
    *on_time_period = 0;
 | 
			
		||||
    *off_time_period = 0;
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  uint32_t period = (1000000UL + carrier_frequency / 2) / carrier_frequency;  // round(1000000/freq)
 | 
			
		||||
  period = std::max(uint32_t(1), period);
 | 
			
		||||
  *on_time_period = (period * this->carrier_duty_percent_) / 100;
 | 
			
		||||
  *off_time_period = period - *on_time_period;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void RemoteTransmitterComponent::await_target_time_() {
 | 
			
		||||
  const uint32_t current_time = micros();
 | 
			
		||||
  if (this->target_time_ == 0) {
 | 
			
		||||
    this->target_time_ = current_time;
 | 
			
		||||
  } else {
 | 
			
		||||
    while ((int32_t) (this->target_time_ - micros()) > 0) {
 | 
			
		||||
      // busy loop that ensures micros is constantly called
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void RemoteTransmitterComponent::mark_(uint32_t on_time, uint32_t off_time, uint32_t usec) {
 | 
			
		||||
  this->await_target_time_();
 | 
			
		||||
  this->pin_->digital_write(true);
 | 
			
		||||
 | 
			
		||||
  const uint32_t target = this->target_time_ + usec;
 | 
			
		||||
  if (this->carrier_duty_percent_ < 100 && (on_time > 0 || off_time > 0)) {
 | 
			
		||||
    while (true) {  // Modulate with carrier frequency
 | 
			
		||||
      this->target_time_ += on_time;
 | 
			
		||||
      if ((int32_t) (this->target_time_ - target) >= 0)
 | 
			
		||||
        break;
 | 
			
		||||
      this->await_target_time_();
 | 
			
		||||
      this->pin_->digital_write(false);
 | 
			
		||||
 | 
			
		||||
      this->target_time_ += off_time;
 | 
			
		||||
      if ((int32_t) (this->target_time_ - target) >= 0)
 | 
			
		||||
        break;
 | 
			
		||||
      this->await_target_time_();
 | 
			
		||||
      this->pin_->digital_write(true);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  this->target_time_ = target;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void RemoteTransmitterComponent::space_(uint32_t usec) {
 | 
			
		||||
  this->await_target_time_();
 | 
			
		||||
  this->pin_->digital_write(false);
 | 
			
		||||
  this->target_time_ += usec;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void RemoteTransmitterComponent::digital_write(bool value) { this->pin_->digital_write(value); }
 | 
			
		||||
 | 
			
		||||
void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) {
 | 
			
		||||
  ESP_LOGD(TAG, "Sending remote code");
 | 
			
		||||
  uint32_t on_time, off_time;
 | 
			
		||||
  this->calculate_on_off_time_(this->temp_.get_carrier_frequency(), &on_time, &off_time);
 | 
			
		||||
  this->target_time_ = 0;
 | 
			
		||||
  this->transmit_trigger_->trigger();
 | 
			
		||||
  for (uint32_t i = 0; i < send_times; i++) {
 | 
			
		||||
    InterruptLock lock;
 | 
			
		||||
    for (int32_t item : this->temp_.get_data()) {
 | 
			
		||||
      if (item > 0) {
 | 
			
		||||
        const auto length = uint32_t(item);
 | 
			
		||||
        this->mark_(on_time, off_time, length);
 | 
			
		||||
      } else {
 | 
			
		||||
        const auto length = uint32_t(-item);
 | 
			
		||||
        this->space_(length);
 | 
			
		||||
      }
 | 
			
		||||
      App.feed_wdt();
 | 
			
		||||
    }
 | 
			
		||||
    this->await_target_time_();  // wait for duration of last pulse
 | 
			
		||||
    this->pin_->digital_write(false);
 | 
			
		||||
 | 
			
		||||
    if (i + 1 < send_times)
 | 
			
		||||
      this->target_time_ += send_wait;
 | 
			
		||||
  }
 | 
			
		||||
  this->complete_trigger_->trigger();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace remote_transmitter
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ from enum import Enum
 | 
			
		||||
 | 
			
		||||
from esphome.enum import StrEnum
 | 
			
		||||
 | 
			
		||||
__version__ = "2025.9.0b1"
 | 
			
		||||
__version__ = "2025.10.0-dev"
 | 
			
		||||
 | 
			
		||||
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"
 | 
			
		||||
 
 | 
			
		||||
@@ -19,3 +19,6 @@ dependencies:
 | 
			
		||||
      - if: "target in [esp32h2, esp32p4]"
 | 
			
		||||
  zorxx/multipart-parser:
 | 
			
		||||
    version: 1.0.1
 | 
			
		||||
  vroland/epdiy:
 | 
			
		||||
    git: https://github.com/vroland/epdiy.git
 | 
			
		||||
    version: c61e9e923ce2418150d54f88cea5d196cdc40c54
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
pylint==3.3.8
 | 
			
		||||
flake8==7.3.0  # also change in .pre-commit-config.yaml when updating
 | 
			
		||||
ruff==0.12.12  # also change in .pre-commit-config.yaml when updating
 | 
			
		||||
ruff==0.13.0  # also change in .pre-commit-config.yaml when updating
 | 
			
		||||
pyupgrade==3.20.0  # also change in .pre-commit-config.yaml when updating
 | 
			
		||||
pre-commit
 | 
			
		||||
 | 
			
		||||
@@ -8,7 +8,7 @@ pre-commit
 | 
			
		||||
pytest==8.4.2
 | 
			
		||||
pytest-cov==7.0.0
 | 
			
		||||
pytest-mock==3.15.0
 | 
			
		||||
pytest-asyncio==1.1.0
 | 
			
		||||
pytest-asyncio==1.2.0
 | 
			
		||||
pytest-xdist==3.8.0
 | 
			
		||||
asyncmock==0.4.2
 | 
			
		||||
hypothesis==6.92.1
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										203
									
								
								tests/dashboard/test_entries.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										203
									
								
								tests/dashboard/test_entries.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,203 @@
 | 
			
		||||
"""Tests for dashboard entries Path-related functionality."""
 | 
			
		||||
 | 
			
		||||
from __future__ import annotations
 | 
			
		||||
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
import tempfile
 | 
			
		||||
from unittest.mock import MagicMock
 | 
			
		||||
 | 
			
		||||
import pytest
 | 
			
		||||
import pytest_asyncio
 | 
			
		||||
 | 
			
		||||
from esphome.core import CORE
 | 
			
		||||
from esphome.dashboard.entries import DashboardEntries, DashboardEntry
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def create_cache_key() -> tuple[int, int, float, int]:
 | 
			
		||||
    """Helper to create a valid DashboardCacheKeyType."""
 | 
			
		||||
    return (0, 0, 0.0, 0)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture(autouse=True)
 | 
			
		||||
def setup_core():
 | 
			
		||||
    """Set up CORE for testing."""
 | 
			
		||||
    with tempfile.TemporaryDirectory() as tmpdir:
 | 
			
		||||
        CORE.config_path = str(Path(tmpdir) / "test.yaml")
 | 
			
		||||
        yield
 | 
			
		||||
        CORE.reset()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture
 | 
			
		||||
def mock_settings() -> MagicMock:
 | 
			
		||||
    """Create mock dashboard settings."""
 | 
			
		||||
    settings = MagicMock()
 | 
			
		||||
    settings.config_dir = "/test/config"
 | 
			
		||||
    settings.absolute_config_dir = Path("/test/config")
 | 
			
		||||
    return settings
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest_asyncio.fixture
 | 
			
		||||
async def dashboard_entries(mock_settings: MagicMock) -> DashboardEntries:
 | 
			
		||||
    """Create a DashboardEntries instance for testing."""
 | 
			
		||||
    return DashboardEntries(mock_settings)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_dashboard_entry_path_initialization() -> None:
 | 
			
		||||
    """Test DashboardEntry initializes with path correctly."""
 | 
			
		||||
    test_path = "/test/config/device.yaml"
 | 
			
		||||
    cache_key = create_cache_key()
 | 
			
		||||
 | 
			
		||||
    entry = DashboardEntry(test_path, cache_key)
 | 
			
		||||
 | 
			
		||||
    assert entry.path == test_path
 | 
			
		||||
    assert entry.cache_key == cache_key
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_dashboard_entry_path_with_absolute_path() -> None:
 | 
			
		||||
    """Test DashboardEntry handles absolute paths."""
 | 
			
		||||
    # Use a truly absolute path for the platform
 | 
			
		||||
    test_path = Path.cwd() / "absolute" / "path" / "to" / "config.yaml"
 | 
			
		||||
    cache_key = create_cache_key()
 | 
			
		||||
 | 
			
		||||
    entry = DashboardEntry(str(test_path), cache_key)
 | 
			
		||||
 | 
			
		||||
    assert entry.path == str(test_path)
 | 
			
		||||
    assert Path(entry.path).is_absolute()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_dashboard_entry_path_with_relative_path() -> None:
 | 
			
		||||
    """Test DashboardEntry handles relative paths."""
 | 
			
		||||
    test_path = "configs/device.yaml"
 | 
			
		||||
    cache_key = create_cache_key()
 | 
			
		||||
 | 
			
		||||
    entry = DashboardEntry(test_path, cache_key)
 | 
			
		||||
 | 
			
		||||
    assert entry.path == test_path
 | 
			
		||||
    assert not Path(entry.path).is_absolute()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.asyncio
 | 
			
		||||
async def test_dashboard_entries_get_by_path(
 | 
			
		||||
    dashboard_entries: DashboardEntries,
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test getting entry by path."""
 | 
			
		||||
    test_path = "/test/config/device.yaml"
 | 
			
		||||
    entry = DashboardEntry(test_path, create_cache_key())
 | 
			
		||||
 | 
			
		||||
    dashboard_entries._entries[test_path] = entry
 | 
			
		||||
 | 
			
		||||
    result = dashboard_entries.get(test_path)
 | 
			
		||||
    assert result == entry
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.asyncio
 | 
			
		||||
async def test_dashboard_entries_get_nonexistent_path(
 | 
			
		||||
    dashboard_entries: DashboardEntries,
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test getting non-existent entry returns None."""
 | 
			
		||||
    result = dashboard_entries.get("/nonexistent/path.yaml")
 | 
			
		||||
    assert result is None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.asyncio
 | 
			
		||||
async def test_dashboard_entries_path_normalization(
 | 
			
		||||
    dashboard_entries: DashboardEntries,
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test that paths are handled consistently."""
 | 
			
		||||
    path1 = "/test/config/device.yaml"
 | 
			
		||||
 | 
			
		||||
    entry = DashboardEntry(path1, create_cache_key())
 | 
			
		||||
    dashboard_entries._entries[path1] = entry
 | 
			
		||||
 | 
			
		||||
    result = dashboard_entries.get(path1)
 | 
			
		||||
    assert result == entry
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.asyncio
 | 
			
		||||
async def test_dashboard_entries_path_with_spaces(
 | 
			
		||||
    dashboard_entries: DashboardEntries,
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test handling paths with spaces."""
 | 
			
		||||
    test_path = "/test/config/my device.yaml"
 | 
			
		||||
    entry = DashboardEntry(test_path, create_cache_key())
 | 
			
		||||
 | 
			
		||||
    dashboard_entries._entries[test_path] = entry
 | 
			
		||||
 | 
			
		||||
    result = dashboard_entries.get(test_path)
 | 
			
		||||
    assert result == entry
 | 
			
		||||
    assert result.path == test_path
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.asyncio
 | 
			
		||||
async def test_dashboard_entries_path_with_special_chars(
 | 
			
		||||
    dashboard_entries: DashboardEntries,
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test handling paths with special characters."""
 | 
			
		||||
    test_path = "/test/config/device-01_test.yaml"
 | 
			
		||||
    entry = DashboardEntry(test_path, create_cache_key())
 | 
			
		||||
 | 
			
		||||
    dashboard_entries._entries[test_path] = entry
 | 
			
		||||
 | 
			
		||||
    result = dashboard_entries.get(test_path)
 | 
			
		||||
    assert result == entry
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_dashboard_entries_windows_path() -> None:
 | 
			
		||||
    """Test handling Windows-style paths."""
 | 
			
		||||
    test_path = r"C:\Users\test\esphome\device.yaml"
 | 
			
		||||
    cache_key = create_cache_key()
 | 
			
		||||
 | 
			
		||||
    entry = DashboardEntry(test_path, cache_key)
 | 
			
		||||
 | 
			
		||||
    assert entry.path == test_path
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.asyncio
 | 
			
		||||
async def test_dashboard_entries_path_to_cache_key_mapping(
 | 
			
		||||
    dashboard_entries: DashboardEntries,
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test internal entries storage with paths and cache keys."""
 | 
			
		||||
    path1 = "/test/config/device1.yaml"
 | 
			
		||||
    path2 = "/test/config/device2.yaml"
 | 
			
		||||
 | 
			
		||||
    entry1 = DashboardEntry(path1, create_cache_key())
 | 
			
		||||
    entry2 = DashboardEntry(path2, (1, 1, 1.0, 1))
 | 
			
		||||
 | 
			
		||||
    dashboard_entries._entries[path1] = entry1
 | 
			
		||||
    dashboard_entries._entries[path2] = entry2
 | 
			
		||||
 | 
			
		||||
    assert path1 in dashboard_entries._entries
 | 
			
		||||
    assert path2 in dashboard_entries._entries
 | 
			
		||||
    assert dashboard_entries._entries[path1].cache_key == create_cache_key()
 | 
			
		||||
    assert dashboard_entries._entries[path2].cache_key == (1, 1, 1.0, 1)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_dashboard_entry_path_property() -> None:
 | 
			
		||||
    """Test that path property returns expected value."""
 | 
			
		||||
    test_path = "/test/config/device.yaml"
 | 
			
		||||
    entry = DashboardEntry(test_path, create_cache_key())
 | 
			
		||||
 | 
			
		||||
    assert entry.path == test_path
 | 
			
		||||
    assert isinstance(entry.path, str)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.asyncio
 | 
			
		||||
async def test_dashboard_entries_all_returns_entries_with_paths(
 | 
			
		||||
    dashboard_entries: DashboardEntries,
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test that all() returns entries with their paths intact."""
 | 
			
		||||
    paths = [
 | 
			
		||||
        "/test/config/device1.yaml",
 | 
			
		||||
        "/test/config/device2.yaml",
 | 
			
		||||
        "/test/config/subfolder/device3.yaml",
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    for path in paths:
 | 
			
		||||
        entry = DashboardEntry(path, create_cache_key())
 | 
			
		||||
        dashboard_entries._entries[path] = entry
 | 
			
		||||
 | 
			
		||||
    all_entries = dashboard_entries.async_all()
 | 
			
		||||
 | 
			
		||||
    assert len(all_entries) == len(paths)
 | 
			
		||||
    retrieved_paths = [entry.path for entry in all_entries]
 | 
			
		||||
    assert set(retrieved_paths) == set(paths)
 | 
			
		||||
							
								
								
									
										168
									
								
								tests/dashboard/test_settings.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										168
									
								
								tests/dashboard/test_settings.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,168 @@
 | 
			
		||||
"""Tests for dashboard settings Path-related functionality."""
 | 
			
		||||
 | 
			
		||||
from __future__ import annotations
 | 
			
		||||
 | 
			
		||||
import os
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
import tempfile
 | 
			
		||||
 | 
			
		||||
import pytest
 | 
			
		||||
 | 
			
		||||
from esphome.dashboard.settings import DashboardSettings
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture
 | 
			
		||||
def dashboard_settings(tmp_path: Path) -> DashboardSettings:
 | 
			
		||||
    """Create DashboardSettings instance with temp directory."""
 | 
			
		||||
    settings = DashboardSettings()
 | 
			
		||||
    # Resolve symlinks to ensure paths match
 | 
			
		||||
    resolved_dir = tmp_path.resolve()
 | 
			
		||||
    settings.config_dir = str(resolved_dir)
 | 
			
		||||
    settings.absolute_config_dir = resolved_dir
 | 
			
		||||
    return settings
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_rel_path_simple(dashboard_settings: DashboardSettings) -> None:
 | 
			
		||||
    """Test rel_path with simple relative path."""
 | 
			
		||||
    result = dashboard_settings.rel_path("config.yaml")
 | 
			
		||||
 | 
			
		||||
    expected = str(Path(dashboard_settings.config_dir) / "config.yaml")
 | 
			
		||||
    assert result == expected
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_rel_path_multiple_components(dashboard_settings: DashboardSettings) -> None:
 | 
			
		||||
    """Test rel_path with multiple path components."""
 | 
			
		||||
    result = dashboard_settings.rel_path("subfolder", "device", "config.yaml")
 | 
			
		||||
 | 
			
		||||
    expected = str(
 | 
			
		||||
        Path(dashboard_settings.config_dir) / "subfolder" / "device" / "config.yaml"
 | 
			
		||||
    )
 | 
			
		||||
    assert result == expected
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_rel_path_with_dots(dashboard_settings: DashboardSettings) -> None:
 | 
			
		||||
    """Test rel_path prevents directory traversal."""
 | 
			
		||||
    # This should raise ValueError as it tries to go outside config_dir
 | 
			
		||||
    with pytest.raises(ValueError):
 | 
			
		||||
        dashboard_settings.rel_path("..", "outside.yaml")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_rel_path_absolute_path_within_config(
 | 
			
		||||
    dashboard_settings: DashboardSettings,
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test rel_path with absolute path that's within config dir."""
 | 
			
		||||
    internal_path = dashboard_settings.absolute_config_dir / "internal.yaml"
 | 
			
		||||
 | 
			
		||||
    internal_path.touch()
 | 
			
		||||
    result = dashboard_settings.rel_path("internal.yaml")
 | 
			
		||||
    expected = str(Path(dashboard_settings.config_dir) / "internal.yaml")
 | 
			
		||||
    assert result == expected
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_rel_path_absolute_path_outside_config(
 | 
			
		||||
    dashboard_settings: DashboardSettings,
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test rel_path with absolute path outside config dir raises error."""
 | 
			
		||||
    outside_path = "/tmp/outside/config.yaml"
 | 
			
		||||
 | 
			
		||||
    with pytest.raises(ValueError):
 | 
			
		||||
        dashboard_settings.rel_path(outside_path)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_rel_path_empty_args(dashboard_settings: DashboardSettings) -> None:
 | 
			
		||||
    """Test rel_path with no arguments returns config_dir."""
 | 
			
		||||
    result = dashboard_settings.rel_path()
 | 
			
		||||
    assert result == dashboard_settings.config_dir
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_rel_path_with_pathlib_path(dashboard_settings: DashboardSettings) -> None:
 | 
			
		||||
    """Test rel_path works with Path objects as arguments."""
 | 
			
		||||
    path_obj = Path("subfolder") / "config.yaml"
 | 
			
		||||
    result = dashboard_settings.rel_path(path_obj)
 | 
			
		||||
 | 
			
		||||
    expected = str(Path(dashboard_settings.config_dir) / "subfolder" / "config.yaml")
 | 
			
		||||
    assert result == expected
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_rel_path_normalizes_slashes(dashboard_settings: DashboardSettings) -> None:
 | 
			
		||||
    """Test rel_path normalizes path separators."""
 | 
			
		||||
    # os.path.join normalizes slashes on Windows but preserves them on Unix
 | 
			
		||||
    # Test that providing components separately gives same result
 | 
			
		||||
    result1 = dashboard_settings.rel_path("folder", "subfolder", "file.yaml")
 | 
			
		||||
    result2 = dashboard_settings.rel_path("folder", "subfolder", "file.yaml")
 | 
			
		||||
    assert result1 == result2
 | 
			
		||||
 | 
			
		||||
    # Also test that the result is as expected
 | 
			
		||||
    expected = os.path.join(
 | 
			
		||||
        dashboard_settings.config_dir, "folder", "subfolder", "file.yaml"
 | 
			
		||||
    )
 | 
			
		||||
    assert result1 == expected
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_rel_path_handles_spaces(dashboard_settings: DashboardSettings) -> None:
 | 
			
		||||
    """Test rel_path handles paths with spaces."""
 | 
			
		||||
    result = dashboard_settings.rel_path("my folder", "my config.yaml")
 | 
			
		||||
 | 
			
		||||
    expected = str(Path(dashboard_settings.config_dir) / "my folder" / "my config.yaml")
 | 
			
		||||
    assert result == expected
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_rel_path_handles_special_chars(dashboard_settings: DashboardSettings) -> None:
 | 
			
		||||
    """Test rel_path handles paths with special characters."""
 | 
			
		||||
    result = dashboard_settings.rel_path("device-01_test", "config.yaml")
 | 
			
		||||
 | 
			
		||||
    expected = str(
 | 
			
		||||
        Path(dashboard_settings.config_dir) / "device-01_test" / "config.yaml"
 | 
			
		||||
    )
 | 
			
		||||
    assert result == expected
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_config_dir_as_path_property(dashboard_settings: DashboardSettings) -> None:
 | 
			
		||||
    """Test that config_dir can be accessed and used with Path operations."""
 | 
			
		||||
    config_path = Path(dashboard_settings.config_dir)
 | 
			
		||||
 | 
			
		||||
    assert config_path.exists()
 | 
			
		||||
    assert config_path.is_dir()
 | 
			
		||||
    assert config_path.is_absolute()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_absolute_config_dir_property(dashboard_settings: DashboardSettings) -> None:
 | 
			
		||||
    """Test absolute_config_dir is a Path object."""
 | 
			
		||||
    assert isinstance(dashboard_settings.absolute_config_dir, Path)
 | 
			
		||||
    assert dashboard_settings.absolute_config_dir.exists()
 | 
			
		||||
    assert dashboard_settings.absolute_config_dir.is_dir()
 | 
			
		||||
    assert dashboard_settings.absolute_config_dir.is_absolute()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_rel_path_symlink_inside_config(dashboard_settings: DashboardSettings) -> None:
 | 
			
		||||
    """Test rel_path with symlink that points inside config dir."""
 | 
			
		||||
    target = dashboard_settings.absolute_config_dir / "target.yaml"
 | 
			
		||||
    target.touch()
 | 
			
		||||
    symlink = dashboard_settings.absolute_config_dir / "link.yaml"
 | 
			
		||||
    symlink.symlink_to(target)
 | 
			
		||||
    result = dashboard_settings.rel_path("link.yaml")
 | 
			
		||||
    expected = str(Path(dashboard_settings.config_dir) / "link.yaml")
 | 
			
		||||
    assert result == expected
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_rel_path_symlink_outside_config(dashboard_settings: DashboardSettings) -> None:
 | 
			
		||||
    """Test rel_path with symlink that points outside config dir."""
 | 
			
		||||
    with tempfile.NamedTemporaryFile(suffix=".yaml") as tmp:
 | 
			
		||||
        symlink = dashboard_settings.absolute_config_dir / "external_link.yaml"
 | 
			
		||||
        symlink.symlink_to(tmp.name)
 | 
			
		||||
        with pytest.raises(ValueError):
 | 
			
		||||
            dashboard_settings.rel_path("external_link.yaml")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_rel_path_with_none_arg(dashboard_settings: DashboardSettings) -> None:
 | 
			
		||||
    """Test rel_path handles None arguments gracefully."""
 | 
			
		||||
    result = dashboard_settings.rel_path("None")
 | 
			
		||||
    expected = str(Path(dashboard_settings.config_dir) / "None")
 | 
			
		||||
    assert result == expected
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_rel_path_with_numeric_args(dashboard_settings: DashboardSettings) -> None:
 | 
			
		||||
    """Test rel_path handles numeric arguments."""
 | 
			
		||||
    result = dashboard_settings.rel_path("123", "456.789")
 | 
			
		||||
    expected = str(Path(dashboard_settings.config_dir) / "123" / "456.789")
 | 
			
		||||
    assert result == expected
 | 
			
		||||
@@ -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,499 @@ 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."""
 | 
			
		||||
 | 
			
		||||
    # 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.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()
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										230
									
								
								tests/dashboard/test_web_server_paths.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										230
									
								
								tests/dashboard/test_web_server_paths.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,230 @@
 | 
			
		||||
"""Tests for dashboard web_server Path-related functionality."""
 | 
			
		||||
 | 
			
		||||
from __future__ import annotations
 | 
			
		||||
 | 
			
		||||
import gzip
 | 
			
		||||
import os
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
from unittest.mock import MagicMock, patch
 | 
			
		||||
 | 
			
		||||
from esphome.dashboard import web_server
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_get_base_frontend_path_production() -> None:
 | 
			
		||||
    """Test get_base_frontend_path in production mode."""
 | 
			
		||||
    mock_module = MagicMock()
 | 
			
		||||
    mock_module.where.return_value = "/usr/local/lib/esphome_dashboard"
 | 
			
		||||
 | 
			
		||||
    with (
 | 
			
		||||
        patch.dict(os.environ, {}, clear=True),
 | 
			
		||||
        patch.dict("sys.modules", {"esphome_dashboard": mock_module}),
 | 
			
		||||
    ):
 | 
			
		||||
        result = web_server.get_base_frontend_path()
 | 
			
		||||
        assert result == "/usr/local/lib/esphome_dashboard"
 | 
			
		||||
        mock_module.where.assert_called_once()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_get_base_frontend_path_dev_mode() -> None:
 | 
			
		||||
    """Test get_base_frontend_path in development mode."""
 | 
			
		||||
    test_path = "/home/user/esphome/dashboard"
 | 
			
		||||
 | 
			
		||||
    with patch.dict(os.environ, {"ESPHOME_DASHBOARD_DEV": test_path}):
 | 
			
		||||
        result = web_server.get_base_frontend_path()
 | 
			
		||||
 | 
			
		||||
        # The function uses os.path.abspath which doesn't resolve symlinks
 | 
			
		||||
        # We need to match that behavior
 | 
			
		||||
        # The actual function adds "/" to the path, so we simulate that
 | 
			
		||||
        test_path_with_slash = test_path if test_path.endswith("/") else test_path + "/"
 | 
			
		||||
        expected = os.path.abspath(
 | 
			
		||||
            os.path.join(os.getcwd(), test_path_with_slash, "esphome_dashboard")
 | 
			
		||||
        )
 | 
			
		||||
        assert result == expected
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_get_base_frontend_path_dev_mode_with_trailing_slash() -> None:
 | 
			
		||||
    """Test get_base_frontend_path in dev mode with trailing slash."""
 | 
			
		||||
    test_path = "/home/user/esphome/dashboard/"
 | 
			
		||||
 | 
			
		||||
    with patch.dict(os.environ, {"ESPHOME_DASHBOARD_DEV": test_path}):
 | 
			
		||||
        result = web_server.get_base_frontend_path()
 | 
			
		||||
 | 
			
		||||
        # The function uses os.path.abspath which doesn't resolve symlinks
 | 
			
		||||
        expected = os.path.abspath(str(Path.cwd() / test_path / "esphome_dashboard"))
 | 
			
		||||
        assert result == expected
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_get_base_frontend_path_dev_mode_relative_path() -> None:
 | 
			
		||||
    """Test get_base_frontend_path with relative dev path."""
 | 
			
		||||
    test_path = "./dashboard"
 | 
			
		||||
 | 
			
		||||
    with patch.dict(os.environ, {"ESPHOME_DASHBOARD_DEV": test_path}):
 | 
			
		||||
        result = web_server.get_base_frontend_path()
 | 
			
		||||
 | 
			
		||||
        # The function uses os.path.abspath which doesn't resolve symlinks
 | 
			
		||||
        # We need to match that behavior
 | 
			
		||||
        # The actual function adds "/" to the path, so we simulate that
 | 
			
		||||
        test_path_with_slash = test_path if test_path.endswith("/") else test_path + "/"
 | 
			
		||||
        expected = os.path.abspath(
 | 
			
		||||
            os.path.join(os.getcwd(), test_path_with_slash, "esphome_dashboard")
 | 
			
		||||
        )
 | 
			
		||||
        assert result == expected
 | 
			
		||||
        assert Path(result).is_absolute()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_get_static_path_single_component() -> None:
 | 
			
		||||
    """Test get_static_path with single path component."""
 | 
			
		||||
    with patch("esphome.dashboard.web_server.get_base_frontend_path") as mock_base:
 | 
			
		||||
        mock_base.return_value = "/base/frontend"
 | 
			
		||||
 | 
			
		||||
        result = web_server.get_static_path("file.js")
 | 
			
		||||
 | 
			
		||||
        assert result == os.path.join("/base/frontend", "static", "file.js")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_get_static_path_multiple_components() -> None:
 | 
			
		||||
    """Test get_static_path with multiple path components."""
 | 
			
		||||
    with patch("esphome.dashboard.web_server.get_base_frontend_path") as mock_base:
 | 
			
		||||
        mock_base.return_value = "/base/frontend"
 | 
			
		||||
 | 
			
		||||
        result = web_server.get_static_path("js", "esphome", "index.js")
 | 
			
		||||
 | 
			
		||||
        assert result == os.path.join(
 | 
			
		||||
            "/base/frontend", "static", "js", "esphome", "index.js"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_get_static_path_empty_args() -> None:
 | 
			
		||||
    """Test get_static_path with no arguments."""
 | 
			
		||||
    with patch("esphome.dashboard.web_server.get_base_frontend_path") as mock_base:
 | 
			
		||||
        mock_base.return_value = "/base/frontend"
 | 
			
		||||
 | 
			
		||||
        result = web_server.get_static_path()
 | 
			
		||||
 | 
			
		||||
        assert result == os.path.join("/base/frontend", "static")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_get_static_path_with_pathlib_path() -> None:
 | 
			
		||||
    """Test get_static_path with Path objects."""
 | 
			
		||||
    with patch("esphome.dashboard.web_server.get_base_frontend_path") as mock_base:
 | 
			
		||||
        mock_base.return_value = "/base/frontend"
 | 
			
		||||
 | 
			
		||||
        path_obj = Path("js") / "app.js"
 | 
			
		||||
        result = web_server.get_static_path(str(path_obj))
 | 
			
		||||
 | 
			
		||||
        assert result == os.path.join("/base/frontend", "static", "js", "app.js")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_get_static_file_url_production() -> None:
 | 
			
		||||
    """Test get_static_file_url in production mode."""
 | 
			
		||||
    web_server.get_static_file_url.cache_clear()
 | 
			
		||||
    mock_module = MagicMock()
 | 
			
		||||
    mock_file = MagicMock()
 | 
			
		||||
    mock_file.read.return_value = b"test content"
 | 
			
		||||
    mock_file.__enter__ = MagicMock(return_value=mock_file)
 | 
			
		||||
    mock_file.__exit__ = MagicMock(return_value=None)
 | 
			
		||||
 | 
			
		||||
    with (
 | 
			
		||||
        patch.dict(os.environ, {}, clear=True),
 | 
			
		||||
        patch.dict("sys.modules", {"esphome_dashboard": mock_module}),
 | 
			
		||||
        patch("esphome.dashboard.web_server.get_static_path") as mock_get_path,
 | 
			
		||||
        patch("esphome.dashboard.web_server.open", create=True, return_value=mock_file),
 | 
			
		||||
    ):
 | 
			
		||||
        mock_get_path.return_value = "/fake/path/js/app.js"
 | 
			
		||||
        result = web_server.get_static_file_url("js/app.js")
 | 
			
		||||
        assert result.startswith("./static/js/app.js?hash=")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_get_static_file_url_dev_mode() -> None:
 | 
			
		||||
    """Test get_static_file_url in development mode."""
 | 
			
		||||
    with patch.dict(os.environ, {"ESPHOME_DASHBOARD_DEV": "/dev/path"}):
 | 
			
		||||
        web_server.get_static_file_url.cache_clear()
 | 
			
		||||
        result = web_server.get_static_file_url("js/app.js")
 | 
			
		||||
 | 
			
		||||
        assert result == "./static/js/app.js"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_get_static_file_url_index_js_special_case() -> None:
 | 
			
		||||
    """Test get_static_file_url replaces index.js with entrypoint."""
 | 
			
		||||
    web_server.get_static_file_url.cache_clear()
 | 
			
		||||
    mock_module = MagicMock()
 | 
			
		||||
    mock_module.entrypoint.return_value = "main.js"
 | 
			
		||||
 | 
			
		||||
    with (
 | 
			
		||||
        patch.dict(os.environ, {}, clear=True),
 | 
			
		||||
        patch.dict("sys.modules", {"esphome_dashboard": mock_module}),
 | 
			
		||||
    ):
 | 
			
		||||
        result = web_server.get_static_file_url("js/esphome/index.js")
 | 
			
		||||
        assert result == "./static/js/esphome/main.js"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_load_file_path(tmp_path: Path) -> None:
 | 
			
		||||
    """Test loading a file."""
 | 
			
		||||
    test_file = tmp_path / "test.txt"
 | 
			
		||||
    test_file.write_bytes(b"test content")
 | 
			
		||||
 | 
			
		||||
    with open(test_file, "rb") as f:
 | 
			
		||||
        content = f.read()
 | 
			
		||||
    assert content == b"test content"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_load_file_compressed_path(tmp_path: Path) -> None:
 | 
			
		||||
    """Test loading a compressed file."""
 | 
			
		||||
    test_file = tmp_path / "test.txt.gz"
 | 
			
		||||
 | 
			
		||||
    with gzip.open(test_file, "wb") as gz:
 | 
			
		||||
        gz.write(b"compressed content")
 | 
			
		||||
 | 
			
		||||
    with gzip.open(test_file, "rb") as gz:
 | 
			
		||||
        content = gz.read()
 | 
			
		||||
    assert content == b"compressed content"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_path_normalization_in_static_path() -> None:
 | 
			
		||||
    """Test that paths are normalized correctly."""
 | 
			
		||||
    with patch("esphome.dashboard.web_server.get_base_frontend_path") as mock_base:
 | 
			
		||||
        mock_base.return_value = "/base/frontend"
 | 
			
		||||
 | 
			
		||||
        # Test with separate components
 | 
			
		||||
        result1 = web_server.get_static_path("js", "app.js")
 | 
			
		||||
        result2 = web_server.get_static_path("js", "app.js")
 | 
			
		||||
 | 
			
		||||
        assert result1 == result2
 | 
			
		||||
        assert result1 == os.path.join("/base/frontend", "static", "js", "app.js")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_windows_path_handling() -> None:
 | 
			
		||||
    """Test handling of Windows-style paths."""
 | 
			
		||||
    with patch("esphome.dashboard.web_server.get_base_frontend_path") as mock_base:
 | 
			
		||||
        mock_base.return_value = r"C:\Program Files\esphome\frontend"
 | 
			
		||||
 | 
			
		||||
        result = web_server.get_static_path("js", "app.js")
 | 
			
		||||
 | 
			
		||||
        # os.path.join should handle this correctly on the platform
 | 
			
		||||
        expected = os.path.join(
 | 
			
		||||
            r"C:\Program Files\esphome\frontend", "static", "js", "app.js"
 | 
			
		||||
        )
 | 
			
		||||
        assert result == expected
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_path_with_special_characters() -> None:
 | 
			
		||||
    """Test paths with special characters."""
 | 
			
		||||
    with patch("esphome.dashboard.web_server.get_base_frontend_path") as mock_base:
 | 
			
		||||
        mock_base.return_value = "/base/frontend"
 | 
			
		||||
 | 
			
		||||
        result = web_server.get_static_path("js-modules", "app_v1.0.js")
 | 
			
		||||
 | 
			
		||||
        assert result == os.path.join(
 | 
			
		||||
            "/base/frontend", "static", "js-modules", "app_v1.0.js"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_path_with_spaces() -> None:
 | 
			
		||||
    """Test paths with spaces."""
 | 
			
		||||
    with patch("esphome.dashboard.web_server.get_base_frontend_path") as mock_base:
 | 
			
		||||
        mock_base.return_value = "/base/my frontend"
 | 
			
		||||
 | 
			
		||||
        result = web_server.get_static_path("my js", "my app.js")
 | 
			
		||||
 | 
			
		||||
        assert result == os.path.join(
 | 
			
		||||
            "/base/my frontend", "static", "my js", "my app.js"
 | 
			
		||||
        )
 | 
			
		||||
@@ -36,3 +36,10 @@ def fixture_path() -> Path:
 | 
			
		||||
    Location of all fixture files.
 | 
			
		||||
    """
 | 
			
		||||
    return here / "fixtures"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture
 | 
			
		||||
def setup_core(tmp_path: Path) -> Path:
 | 
			
		||||
    """Set up CORE with test paths."""
 | 
			
		||||
    CORE.config_path = str(tmp_path / "test.yaml")
 | 
			
		||||
    return tmp_path
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,3 @@
 | 
			
		||||
# This file should be ignored
 | 
			
		||||
platform: template
 | 
			
		||||
name: "Hidden Sensor"
 | 
			
		||||
@@ -0,0 +1 @@
 | 
			
		||||
This is not a YAML file and should be ignored
 | 
			
		||||
@@ -0,0 +1,4 @@
 | 
			
		||||
platform: template
 | 
			
		||||
name: "Sensor 1"
 | 
			
		||||
lambda: |-
 | 
			
		||||
  return 42.0;
 | 
			
		||||
@@ -0,0 +1,4 @@
 | 
			
		||||
platform: template
 | 
			
		||||
name: "Sensor 2"
 | 
			
		||||
lambda: |-
 | 
			
		||||
  return 100.0;
 | 
			
		||||
@@ -0,0 +1,4 @@
 | 
			
		||||
platform: template
 | 
			
		||||
name: "Sensor 3 in subdir"
 | 
			
		||||
lambda: |-
 | 
			
		||||
  return 200.0;
 | 
			
		||||
							
								
								
									
										4
									
								
								tests/unit_tests/fixtures/yaml_util/secrets.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								tests/unit_tests/fixtures/yaml_util/secrets.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
			
		||||
test_secret: "my_secret_value"
 | 
			
		||||
another_secret: "another_value"
 | 
			
		||||
wifi_password: "super_secret_wifi"
 | 
			
		||||
api_key: "0123456789abcdef"
 | 
			
		||||
							
								
								
									
										17
									
								
								tests/unit_tests/fixtures/yaml_util/test_secret.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								tests/unit_tests/fixtures/yaml_util/test_secret.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
			
		||||
esphome:
 | 
			
		||||
  name: test_device
 | 
			
		||||
  platform: ESP32
 | 
			
		||||
  board: esp32dev
 | 
			
		||||
 | 
			
		||||
wifi:
 | 
			
		||||
  ssid: "TestNetwork"
 | 
			
		||||
  password: !secret wifi_password
 | 
			
		||||
 | 
			
		||||
api:
 | 
			
		||||
  encryption:
 | 
			
		||||
    key: !secret api_key
 | 
			
		||||
 | 
			
		||||
sensor:
 | 
			
		||||
  - platform: template
 | 
			
		||||
    name: "Test Sensor"
 | 
			
		||||
    id: !secret test_secret
 | 
			
		||||
							
								
								
									
										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"
 | 
			
		||||
							
								
								
									
										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
 | 
			
		||||
							
								
								
									
										1533
									
								
								tests/unit_tests/test_main.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1533
									
								
								tests/unit_tests/test_main.py
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										129
									
								
								tests/unit_tests/test_platformio_api.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								tests/unit_tests/test_platformio_api.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,129 @@
 | 
			
		||||
"""Tests for platformio_api.py path functions."""
 | 
			
		||||
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
from unittest.mock import patch
 | 
			
		||||
 | 
			
		||||
from esphome import platformio_api
 | 
			
		||||
from esphome.core import CORE
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_idedata_firmware_elf_path(setup_core: Path) -> None:
 | 
			
		||||
    """Test IDEData.firmware_elf_path returns correct path."""
 | 
			
		||||
    CORE.build_path = str(setup_core / "build" / "test")
 | 
			
		||||
    CORE.name = "test"
 | 
			
		||||
    raw_data = {"prog_path": "/path/to/firmware.elf"}
 | 
			
		||||
    idedata = platformio_api.IDEData(raw_data)
 | 
			
		||||
 | 
			
		||||
    assert idedata.firmware_elf_path == "/path/to/firmware.elf"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_idedata_firmware_bin_path(setup_core: Path) -> None:
 | 
			
		||||
    """Test IDEData.firmware_bin_path returns Path with .bin extension."""
 | 
			
		||||
    CORE.build_path = str(setup_core / "build" / "test")
 | 
			
		||||
    CORE.name = "test"
 | 
			
		||||
    prog_path = str(Path("/path/to/firmware.elf"))
 | 
			
		||||
    raw_data = {"prog_path": prog_path}
 | 
			
		||||
    idedata = platformio_api.IDEData(raw_data)
 | 
			
		||||
 | 
			
		||||
    result = idedata.firmware_bin_path
 | 
			
		||||
    assert isinstance(result, str)
 | 
			
		||||
    expected = str(Path("/path/to/firmware.bin"))
 | 
			
		||||
    assert result == expected
 | 
			
		||||
    assert result.endswith(".bin")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_idedata_firmware_bin_path_preserves_directory(setup_core: Path) -> None:
 | 
			
		||||
    """Test firmware_bin_path preserves the directory structure."""
 | 
			
		||||
    CORE.build_path = str(setup_core / "build" / "test")
 | 
			
		||||
    CORE.name = "test"
 | 
			
		||||
    prog_path = str(Path("/complex/path/to/build/firmware.elf"))
 | 
			
		||||
    raw_data = {"prog_path": prog_path}
 | 
			
		||||
    idedata = platformio_api.IDEData(raw_data)
 | 
			
		||||
 | 
			
		||||
    result = idedata.firmware_bin_path
 | 
			
		||||
    expected = str(Path("/complex/path/to/build/firmware.bin"))
 | 
			
		||||
    assert result == expected
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_idedata_extra_flash_images(setup_core: Path) -> None:
 | 
			
		||||
    """Test IDEData.extra_flash_images returns list of FlashImage objects."""
 | 
			
		||||
    CORE.build_path = str(setup_core / "build" / "test")
 | 
			
		||||
    CORE.name = "test"
 | 
			
		||||
    raw_data = {
 | 
			
		||||
        "prog_path": "/path/to/firmware.elf",
 | 
			
		||||
        "extra": {
 | 
			
		||||
            "flash_images": [
 | 
			
		||||
                {"path": "/path/to/bootloader.bin", "offset": "0x1000"},
 | 
			
		||||
                {"path": "/path/to/partition.bin", "offset": "0x8000"},
 | 
			
		||||
            ]
 | 
			
		||||
        },
 | 
			
		||||
    }
 | 
			
		||||
    idedata = platformio_api.IDEData(raw_data)
 | 
			
		||||
 | 
			
		||||
    images = idedata.extra_flash_images
 | 
			
		||||
    assert len(images) == 2
 | 
			
		||||
    assert all(isinstance(img, platformio_api.FlashImage) for img in images)
 | 
			
		||||
    assert images[0].path == "/path/to/bootloader.bin"
 | 
			
		||||
    assert images[0].offset == "0x1000"
 | 
			
		||||
    assert images[1].path == "/path/to/partition.bin"
 | 
			
		||||
    assert images[1].offset == "0x8000"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_idedata_extra_flash_images_empty(setup_core: Path) -> None:
 | 
			
		||||
    """Test extra_flash_images returns empty list when no extra images."""
 | 
			
		||||
    CORE.build_path = str(setup_core / "build" / "test")
 | 
			
		||||
    CORE.name = "test"
 | 
			
		||||
    raw_data = {"prog_path": "/path/to/firmware.elf", "extra": {"flash_images": []}}
 | 
			
		||||
    idedata = platformio_api.IDEData(raw_data)
 | 
			
		||||
 | 
			
		||||
    images = idedata.extra_flash_images
 | 
			
		||||
    assert images == []
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_idedata_cc_path(setup_core: Path) -> None:
 | 
			
		||||
    """Test IDEData.cc_path returns compiler path."""
 | 
			
		||||
    CORE.build_path = str(setup_core / "build" / "test")
 | 
			
		||||
    CORE.name = "test"
 | 
			
		||||
    raw_data = {
 | 
			
		||||
        "prog_path": "/path/to/firmware.elf",
 | 
			
		||||
        "cc_path": "/Users/test/.platformio/packages/toolchain-xtensa32/bin/xtensa-esp32-elf-gcc",
 | 
			
		||||
    }
 | 
			
		||||
    idedata = platformio_api.IDEData(raw_data)
 | 
			
		||||
 | 
			
		||||
    assert (
 | 
			
		||||
        idedata.cc_path
 | 
			
		||||
        == "/Users/test/.platformio/packages/toolchain-xtensa32/bin/xtensa-esp32-elf-gcc"
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_flash_image_dataclass() -> None:
 | 
			
		||||
    """Test FlashImage dataclass stores path and offset correctly."""
 | 
			
		||||
    image = platformio_api.FlashImage(path="/path/to/image.bin", offset="0x10000")
 | 
			
		||||
 | 
			
		||||
    assert image.path == "/path/to/image.bin"
 | 
			
		||||
    assert image.offset == "0x10000"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_load_idedata_returns_dict(setup_core: Path) -> None:
 | 
			
		||||
    """Test _load_idedata returns parsed idedata dict when successful."""
 | 
			
		||||
    CORE.build_path = str(setup_core / "build" / "test")
 | 
			
		||||
    CORE.name = "test"
 | 
			
		||||
 | 
			
		||||
    # Create required files
 | 
			
		||||
    platformio_ini = setup_core / "build" / "test" / "platformio.ini"
 | 
			
		||||
    platformio_ini.parent.mkdir(parents=True, exist_ok=True)
 | 
			
		||||
    platformio_ini.touch()
 | 
			
		||||
 | 
			
		||||
    idedata_path = setup_core / ".esphome" / "idedata" / "test.json"
 | 
			
		||||
    idedata_path.parent.mkdir(parents=True, exist_ok=True)
 | 
			
		||||
    idedata_path.write_text('{"prog_path": "/test/firmware.elf"}')
 | 
			
		||||
 | 
			
		||||
    with patch("esphome.platformio_api.run_platformio_cli_run") as mock_run:
 | 
			
		||||
        mock_run.return_value = '{"prog_path": "/test/firmware.elf"}'
 | 
			
		||||
 | 
			
		||||
        config = {"name": "test"}
 | 
			
		||||
        result = platformio_api._load_idedata(config)
 | 
			
		||||
 | 
			
		||||
    assert result is not None
 | 
			
		||||
    assert isinstance(result, dict)
 | 
			
		||||
    assert result["prog_path"] == "/test/firmware.elf"
 | 
			
		||||
							
								
								
									
										182
									
								
								tests/unit_tests/test_storage_json.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										182
									
								
								tests/unit_tests/test_storage_json.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,182 @@
 | 
			
		||||
"""Tests for storage_json.py path functions."""
 | 
			
		||||
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
import sys
 | 
			
		||||
from unittest.mock import patch
 | 
			
		||||
 | 
			
		||||
import pytest
 | 
			
		||||
 | 
			
		||||
from esphome import storage_json
 | 
			
		||||
from esphome.core import CORE
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_storage_path(setup_core: Path) -> None:
 | 
			
		||||
    """Test storage_path returns correct path for current config."""
 | 
			
		||||
    CORE.config_path = str(setup_core / "my_device.yaml")
 | 
			
		||||
 | 
			
		||||
    result = storage_json.storage_path()
 | 
			
		||||
 | 
			
		||||
    data_dir = Path(CORE.data_dir)
 | 
			
		||||
    expected = str(data_dir / "storage" / "my_device.yaml.json")
 | 
			
		||||
    assert result == expected
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_ext_storage_path(setup_core: Path) -> None:
 | 
			
		||||
    """Test ext_storage_path returns correct path for given filename."""
 | 
			
		||||
    result = storage_json.ext_storage_path("other_device.yaml")
 | 
			
		||||
 | 
			
		||||
    data_dir = Path(CORE.data_dir)
 | 
			
		||||
    expected = str(data_dir / "storage" / "other_device.yaml.json")
 | 
			
		||||
    assert result == expected
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_ext_storage_path_handles_various_extensions(setup_core: Path) -> None:
 | 
			
		||||
    """Test ext_storage_path works with different file extensions."""
 | 
			
		||||
    result_yml = storage_json.ext_storage_path("device.yml")
 | 
			
		||||
    assert result_yml.endswith("device.yml.json")
 | 
			
		||||
 | 
			
		||||
    result_no_ext = storage_json.ext_storage_path("device")
 | 
			
		||||
    assert result_no_ext.endswith("device.json")
 | 
			
		||||
 | 
			
		||||
    result_path = storage_json.ext_storage_path("my/device.yaml")
 | 
			
		||||
    assert result_path.endswith("device.yaml.json")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_esphome_storage_path(setup_core: Path) -> None:
 | 
			
		||||
    """Test esphome_storage_path returns correct path."""
 | 
			
		||||
    result = storage_json.esphome_storage_path()
 | 
			
		||||
 | 
			
		||||
    data_dir = Path(CORE.data_dir)
 | 
			
		||||
    expected = str(data_dir / "esphome.json")
 | 
			
		||||
    assert result == expected
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_ignored_devices_storage_path(setup_core: Path) -> None:
 | 
			
		||||
    """Test ignored_devices_storage_path returns correct path."""
 | 
			
		||||
    result = storage_json.ignored_devices_storage_path()
 | 
			
		||||
 | 
			
		||||
    data_dir = Path(CORE.data_dir)
 | 
			
		||||
    expected = str(data_dir / "ignored-devices.json")
 | 
			
		||||
    assert result == expected
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_trash_storage_path(setup_core: Path) -> None:
 | 
			
		||||
    """Test trash_storage_path returns correct path."""
 | 
			
		||||
    CORE.config_path = str(setup_core / "configs" / "device.yaml")
 | 
			
		||||
 | 
			
		||||
    result = storage_json.trash_storage_path()
 | 
			
		||||
 | 
			
		||||
    expected = str(setup_core / "configs" / "trash")
 | 
			
		||||
    assert result == expected
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_archive_storage_path(setup_core: Path) -> None:
 | 
			
		||||
    """Test archive_storage_path returns correct path."""
 | 
			
		||||
    CORE.config_path = str(setup_core / "configs" / "device.yaml")
 | 
			
		||||
 | 
			
		||||
    result = storage_json.archive_storage_path()
 | 
			
		||||
 | 
			
		||||
    expected = str(setup_core / "configs" / "archive")
 | 
			
		||||
    assert result == expected
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_storage_path_with_subdirectory(setup_core: Path) -> None:
 | 
			
		||||
    """Test storage paths work correctly when config is in subdirectory."""
 | 
			
		||||
    subdir = setup_core / "configs" / "basement"
 | 
			
		||||
    subdir.mkdir(parents=True, exist_ok=True)
 | 
			
		||||
    CORE.config_path = str(subdir / "sensor.yaml")
 | 
			
		||||
 | 
			
		||||
    result = storage_json.storage_path()
 | 
			
		||||
 | 
			
		||||
    data_dir = Path(CORE.data_dir)
 | 
			
		||||
    expected = str(data_dir / "storage" / "sensor.yaml.json")
 | 
			
		||||
    assert result == expected
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_storage_json_firmware_bin_path_property(setup_core: Path) -> None:
 | 
			
		||||
    """Test StorageJSON firmware_bin_path property."""
 | 
			
		||||
    storage = storage_json.StorageJSON(
 | 
			
		||||
        storage_version=1,
 | 
			
		||||
        name="test_device",
 | 
			
		||||
        friendly_name="Test Device",
 | 
			
		||||
        comment=None,
 | 
			
		||||
        esphome_version="2024.1.0",
 | 
			
		||||
        src_version=None,
 | 
			
		||||
        address="192.168.1.100",
 | 
			
		||||
        web_port=80,
 | 
			
		||||
        target_platform="ESP32",
 | 
			
		||||
        build_path="build/test_device",
 | 
			
		||||
        firmware_bin_path="/path/to/firmware.bin",
 | 
			
		||||
        loaded_integrations={"wifi", "api"},
 | 
			
		||||
        loaded_platforms=set(),
 | 
			
		||||
        no_mdns=False,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    assert storage.firmware_bin_path == "/path/to/firmware.bin"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_storage_json_save_creates_directory(setup_core: Path, tmp_path: Path) -> None:
 | 
			
		||||
    """Test StorageJSON.save creates storage directory if it doesn't exist."""
 | 
			
		||||
    storage_dir = tmp_path / "new_data" / "storage"
 | 
			
		||||
    storage_file = storage_dir / "test.json"
 | 
			
		||||
 | 
			
		||||
    assert not storage_dir.exists()
 | 
			
		||||
 | 
			
		||||
    storage = storage_json.StorageJSON(
 | 
			
		||||
        storage_version=1,
 | 
			
		||||
        name="test",
 | 
			
		||||
        friendly_name="Test",
 | 
			
		||||
        comment=None,
 | 
			
		||||
        esphome_version="2024.1.0",
 | 
			
		||||
        src_version=None,
 | 
			
		||||
        address="test.local",
 | 
			
		||||
        web_port=None,
 | 
			
		||||
        target_platform="ESP8266",
 | 
			
		||||
        build_path=None,
 | 
			
		||||
        firmware_bin_path=None,
 | 
			
		||||
        loaded_integrations=set(),
 | 
			
		||||
        loaded_platforms=set(),
 | 
			
		||||
        no_mdns=False,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    with patch("esphome.storage_json.write_file_if_changed") as mock_write:
 | 
			
		||||
        storage.save(str(storage_file))
 | 
			
		||||
        mock_write.assert_called_once()
 | 
			
		||||
        call_args = mock_write.call_args[0]
 | 
			
		||||
        assert call_args[0] == str(storage_file)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_storage_json_from_wizard(setup_core: Path) -> None:
 | 
			
		||||
    """Test StorageJSON.from_wizard creates correct storage object."""
 | 
			
		||||
    storage = storage_json.StorageJSON.from_wizard(
 | 
			
		||||
        name="my_device",
 | 
			
		||||
        friendly_name="My Device",
 | 
			
		||||
        address="my_device.local",
 | 
			
		||||
        platform="ESP32",
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    assert storage.name == "my_device"
 | 
			
		||||
    assert storage.friendly_name == "My Device"
 | 
			
		||||
    assert storage.address == "my_device.local"
 | 
			
		||||
    assert storage.target_platform == "ESP32"
 | 
			
		||||
    assert storage.build_path is None
 | 
			
		||||
    assert storage.firmware_bin_path is None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.skipif(sys.platform == "win32", reason="HA addons don't run on Windows")
 | 
			
		||||
@patch("esphome.core.is_ha_addon")
 | 
			
		||||
def test_storage_paths_with_ha_addon(mock_is_ha_addon: bool, tmp_path: Path) -> None:
 | 
			
		||||
    """Test storage paths when running as Home Assistant addon."""
 | 
			
		||||
    mock_is_ha_addon.return_value = True
 | 
			
		||||
 | 
			
		||||
    CORE.config_path = str(tmp_path / "test.yaml")
 | 
			
		||||
 | 
			
		||||
    result = storage_json.storage_path()
 | 
			
		||||
    # When is_ha_addon is True, CORE.data_dir returns "/data"
 | 
			
		||||
    # This is the standard mount point for HA addon containers
 | 
			
		||||
    expected = str(Path("/data") / "storage" / "test.yaml.json")
 | 
			
		||||
    assert result == expected
 | 
			
		||||
 | 
			
		||||
    result = storage_json.esphome_storage_path()
 | 
			
		||||
    expected = str(Path("/data") / "esphome.json")
 | 
			
		||||
    assert result == expected
 | 
			
		||||
@@ -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,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,396 @@ 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")
 | 
			
		||||
 | 
			
		||||
    # 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()
 | 
			
		||||
 | 
			
		||||
    # 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()
 | 
			
		||||
 | 
			
		||||
    # Verify logging
 | 
			
		||||
    assert "Deleting" in caplog.text
 | 
			
		||||
    assert ".pioenvs" in caplog.text
 | 
			
		||||
    assert ".piolibdeps" in caplog.text
 | 
			
		||||
    assert "dependencies.lock" 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_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")
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,26 @@
 | 
			
		||||
from esphome import yaml_util
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
import shutil
 | 
			
		||||
from unittest.mock import patch
 | 
			
		||||
 | 
			
		||||
import pytest
 | 
			
		||||
 | 
			
		||||
from esphome import core, yaml_util
 | 
			
		||||
from esphome.components import substitutions
 | 
			
		||||
from esphome.core import EsphomeError
 | 
			
		||||
from esphome.util import OrderedDict
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_include_with_vars(fixture_path):
 | 
			
		||||
@pytest.fixture(autouse=True)
 | 
			
		||||
def clear_secrets_cache() -> None:
 | 
			
		||||
    """Clear the secrets cache before each test."""
 | 
			
		||||
    yaml_util._SECRET_VALUES.clear()
 | 
			
		||||
    yaml_util._SECRET_CACHE.clear()
 | 
			
		||||
    yield
 | 
			
		||||
    yaml_util._SECRET_VALUES.clear()
 | 
			
		||||
    yaml_util._SECRET_CACHE.clear()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_include_with_vars(fixture_path: Path) -> None:
 | 
			
		||||
    yaml_file = fixture_path / "yaml_util" / "includetest.yaml"
 | 
			
		||||
 | 
			
		||||
    actual = yaml_util.load_yaml(yaml_file)
 | 
			
		||||
@@ -62,3 +79,202 @@ def test_parsing_with_custom_loader(fixture_path):
 | 
			
		||||
    assert loader_calls[0].endswith("includes/included.yaml")
 | 
			
		||||
    assert loader_calls[1].endswith("includes/list.yaml")
 | 
			
		||||
    assert loader_calls[2].endswith("includes/scalar.yaml")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_construct_secret_simple(fixture_path: Path) -> None:
 | 
			
		||||
    """Test loading a YAML file with !secret tags."""
 | 
			
		||||
    yaml_file = fixture_path / "yaml_util" / "test_secret.yaml"
 | 
			
		||||
 | 
			
		||||
    actual = yaml_util.load_yaml(yaml_file)
 | 
			
		||||
 | 
			
		||||
    # Check that secrets were properly loaded
 | 
			
		||||
    assert actual["wifi"]["password"] == "super_secret_wifi"
 | 
			
		||||
    assert actual["api"]["encryption"]["key"] == "0123456789abcdef"
 | 
			
		||||
    assert actual["sensor"][0]["id"] == "my_secret_value"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_construct_secret_missing(fixture_path: Path, tmp_path: Path) -> None:
 | 
			
		||||
    """Test that missing secrets raise proper errors."""
 | 
			
		||||
    # Create a YAML file with a secret that doesn't exist
 | 
			
		||||
    test_yaml = tmp_path / "test.yaml"
 | 
			
		||||
    test_yaml.write_text("""
 | 
			
		||||
esphome:
 | 
			
		||||
  name: test
 | 
			
		||||
 | 
			
		||||
wifi:
 | 
			
		||||
  password: !secret nonexistent_secret
 | 
			
		||||
""")
 | 
			
		||||
 | 
			
		||||
    # Create an empty secrets file
 | 
			
		||||
    secrets_yaml = tmp_path / "secrets.yaml"
 | 
			
		||||
    secrets_yaml.write_text("some_other_secret: value")
 | 
			
		||||
 | 
			
		||||
    with pytest.raises(EsphomeError, match="Secret 'nonexistent_secret' not defined"):
 | 
			
		||||
        yaml_util.load_yaml(str(test_yaml))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_construct_secret_no_secrets_file(tmp_path: Path) -> None:
 | 
			
		||||
    """Test that missing secrets.yaml file raises proper error."""
 | 
			
		||||
    # Create a YAML file with a secret but no secrets.yaml
 | 
			
		||||
    test_yaml = tmp_path / "test.yaml"
 | 
			
		||||
    test_yaml.write_text("""
 | 
			
		||||
wifi:
 | 
			
		||||
  password: !secret some_secret
 | 
			
		||||
""")
 | 
			
		||||
 | 
			
		||||
    # Mock CORE.config_path to avoid NoneType error
 | 
			
		||||
    with (
 | 
			
		||||
        patch.object(core.CORE, "config_path", str(tmp_path / "main.yaml")),
 | 
			
		||||
        pytest.raises(EsphomeError, match="secrets.yaml"),
 | 
			
		||||
    ):
 | 
			
		||||
        yaml_util.load_yaml(str(test_yaml))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_construct_secret_fallback_to_main_config_dir(
 | 
			
		||||
    fixture_path: Path, tmp_path: Path
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test fallback to main config directory for secrets."""
 | 
			
		||||
    # Create a subdirectory with a YAML file that uses secrets
 | 
			
		||||
    subdir = tmp_path / "subdir"
 | 
			
		||||
    subdir.mkdir()
 | 
			
		||||
 | 
			
		||||
    test_yaml = subdir / "test.yaml"
 | 
			
		||||
    test_yaml.write_text("""
 | 
			
		||||
wifi:
 | 
			
		||||
  password: !secret test_secret
 | 
			
		||||
""")
 | 
			
		||||
 | 
			
		||||
    # Create secrets.yaml in the main directory
 | 
			
		||||
    main_secrets = tmp_path / "secrets.yaml"
 | 
			
		||||
    main_secrets.write_text("test_secret: main_secret_value")
 | 
			
		||||
 | 
			
		||||
    # Mock CORE.config_path to point to main directory
 | 
			
		||||
    with patch.object(core.CORE, "config_path", str(tmp_path / "main.yaml")):
 | 
			
		||||
        actual = yaml_util.load_yaml(str(test_yaml))
 | 
			
		||||
        assert actual["wifi"]["password"] == "main_secret_value"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_construct_include_dir_named(fixture_path: Path, tmp_path: Path) -> None:
 | 
			
		||||
    """Test !include_dir_named directive."""
 | 
			
		||||
    # Copy fixture directory to temporary location
 | 
			
		||||
    src_dir = fixture_path / "yaml_util"
 | 
			
		||||
    dst_dir = tmp_path / "yaml_util"
 | 
			
		||||
    shutil.copytree(src_dir, dst_dir)
 | 
			
		||||
 | 
			
		||||
    # Create test YAML that uses include_dir_named
 | 
			
		||||
    test_yaml = dst_dir / "test_include_named.yaml"
 | 
			
		||||
    test_yaml.write_text("""
 | 
			
		||||
sensor: !include_dir_named named_dir
 | 
			
		||||
""")
 | 
			
		||||
 | 
			
		||||
    actual = yaml_util.load_yaml(str(test_yaml))
 | 
			
		||||
    actual_sensor = actual["sensor"]
 | 
			
		||||
 | 
			
		||||
    # Check that files were loaded with their names as keys
 | 
			
		||||
    assert isinstance(actual_sensor, OrderedDict)
 | 
			
		||||
    assert "sensor1" in actual_sensor
 | 
			
		||||
    assert "sensor2" in actual_sensor
 | 
			
		||||
    assert "sensor3" in actual_sensor  # Files from subdirs are included with basename
 | 
			
		||||
 | 
			
		||||
    # Check content of loaded files
 | 
			
		||||
    assert actual_sensor["sensor1"]["platform"] == "template"
 | 
			
		||||
    assert actual_sensor["sensor1"]["name"] == "Sensor 1"
 | 
			
		||||
    assert actual_sensor["sensor2"]["platform"] == "template"
 | 
			
		||||
    assert actual_sensor["sensor2"]["name"] == "Sensor 2"
 | 
			
		||||
 | 
			
		||||
    # Check that subdirectory files are included with their basename
 | 
			
		||||
    assert actual_sensor["sensor3"]["platform"] == "template"
 | 
			
		||||
    assert actual_sensor["sensor3"]["name"] == "Sensor 3 in subdir"
 | 
			
		||||
 | 
			
		||||
    # Check that hidden files and non-YAML files are not included
 | 
			
		||||
    assert ".hidden" not in actual_sensor
 | 
			
		||||
    assert "not_yaml" not in actual_sensor
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_construct_include_dir_named_empty_dir(tmp_path: Path) -> None:
 | 
			
		||||
    """Test !include_dir_named with empty directory."""
 | 
			
		||||
    # Create empty directory
 | 
			
		||||
    empty_dir = tmp_path / "empty_dir"
 | 
			
		||||
    empty_dir.mkdir()
 | 
			
		||||
 | 
			
		||||
    test_yaml = tmp_path / "test.yaml"
 | 
			
		||||
    test_yaml.write_text("""
 | 
			
		||||
sensor: !include_dir_named empty_dir
 | 
			
		||||
""")
 | 
			
		||||
 | 
			
		||||
    actual = yaml_util.load_yaml(str(test_yaml))
 | 
			
		||||
 | 
			
		||||
    # Should return empty OrderedDict
 | 
			
		||||
    assert isinstance(actual["sensor"], OrderedDict)
 | 
			
		||||
    assert len(actual["sensor"]) == 0
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_construct_include_dir_named_with_dots(tmp_path: Path) -> None:
 | 
			
		||||
    """Test that include_dir_named ignores files starting with dots."""
 | 
			
		||||
    # Create directory with various files
 | 
			
		||||
    test_dir = tmp_path / "test_dir"
 | 
			
		||||
    test_dir.mkdir()
 | 
			
		||||
 | 
			
		||||
    # Create visible file
 | 
			
		||||
    visible_file = test_dir / "visible.yaml"
 | 
			
		||||
    visible_file.write_text("key: visible_value")
 | 
			
		||||
 | 
			
		||||
    # Create hidden file
 | 
			
		||||
    hidden_file = test_dir / ".hidden.yaml"
 | 
			
		||||
    hidden_file.write_text("key: hidden_value")
 | 
			
		||||
 | 
			
		||||
    # Create hidden directory with files
 | 
			
		||||
    hidden_dir = test_dir / ".hidden_dir"
 | 
			
		||||
    hidden_dir.mkdir()
 | 
			
		||||
    hidden_subfile = hidden_dir / "subfile.yaml"
 | 
			
		||||
    hidden_subfile.write_text("key: hidden_subfile_value")
 | 
			
		||||
 | 
			
		||||
    test_yaml = tmp_path / "test.yaml"
 | 
			
		||||
    test_yaml.write_text("""
 | 
			
		||||
test: !include_dir_named test_dir
 | 
			
		||||
""")
 | 
			
		||||
 | 
			
		||||
    actual = yaml_util.load_yaml(str(test_yaml))
 | 
			
		||||
 | 
			
		||||
    # Should only include visible file
 | 
			
		||||
    assert "visible" in actual["test"]
 | 
			
		||||
    assert actual["test"]["visible"]["key"] == "visible_value"
 | 
			
		||||
 | 
			
		||||
    # Should not include hidden files or directories
 | 
			
		||||
    assert ".hidden" not in actual["test"]
 | 
			
		||||
    assert ".hidden_dir" not in actual["test"]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_find_files_recursive(fixture_path: Path, tmp_path: Path) -> None:
 | 
			
		||||
    """Test that _find_files works recursively through include_dir_named."""
 | 
			
		||||
    # Copy fixture directory to temporary location
 | 
			
		||||
    src_dir = fixture_path / "yaml_util"
 | 
			
		||||
    dst_dir = tmp_path / "yaml_util"
 | 
			
		||||
    shutil.copytree(src_dir, dst_dir)
 | 
			
		||||
 | 
			
		||||
    # This indirectly tests _find_files by using include_dir_named
 | 
			
		||||
    test_yaml = dst_dir / "test_include_recursive.yaml"
 | 
			
		||||
    test_yaml.write_text("""
 | 
			
		||||
all_sensors: !include_dir_named named_dir
 | 
			
		||||
""")
 | 
			
		||||
 | 
			
		||||
    actual = yaml_util.load_yaml(str(test_yaml))
 | 
			
		||||
 | 
			
		||||
    # Should find sensor1.yaml, sensor2.yaml, and subdir/sensor3.yaml (all flattened)
 | 
			
		||||
    assert len(actual["all_sensors"]) == 3
 | 
			
		||||
    assert "sensor1" in actual["all_sensors"]
 | 
			
		||||
    assert "sensor2" in actual["all_sensors"]
 | 
			
		||||
    assert "sensor3" in actual["all_sensors"]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_secret_values_tracking(fixture_path: Path) -> None:
 | 
			
		||||
    """Test that secret values are properly tracked for dumping."""
 | 
			
		||||
    yaml_file = fixture_path / "yaml_util" / "test_secret.yaml"
 | 
			
		||||
 | 
			
		||||
    yaml_util.load_yaml(yaml_file)
 | 
			
		||||
 | 
			
		||||
    # Check that secret values are tracked
 | 
			
		||||
    assert "super_secret_wifi" in yaml_util._SECRET_VALUES
 | 
			
		||||
    assert yaml_util._SECRET_VALUES["super_secret_wifi"] == "wifi_password"
 | 
			
		||||
    assert "0123456789abcdef" in yaml_util._SECRET_VALUES
 | 
			
		||||
    assert yaml_util._SECRET_VALUES["0123456789abcdef"] == "api_key"
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user