mirror of
				https://github.com/esphome/esphome.git
				synced 2025-11-04 09:01:49 +00:00 
			
		
		
		
	[core] fix upload to device via MQTT IP lookup (e.g. when mDNS is disable) (#10632)
Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: J. Nick Koston <nick@home-assistant.io> Co-authored-by: J. Nick Koston <nick+github@koston.org>
This commit is contained in:
		@@ -15,9 +15,11 @@ import argcomplete
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from esphome import const, writer, yaml_util
 | 
					from esphome import const, writer, yaml_util
 | 
				
			||||||
import esphome.codegen as cg
 | 
					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.config import iter_component_configs, read_config, strip_default_ids
 | 
				
			||||||
from esphome.const import (
 | 
					from esphome.const import (
 | 
				
			||||||
    ALLOWED_NAME_CHARS,
 | 
					    ALLOWED_NAME_CHARS,
 | 
				
			||||||
 | 
					    CONF_API,
 | 
				
			||||||
    CONF_BAUD_RATE,
 | 
					    CONF_BAUD_RATE,
 | 
				
			||||||
    CONF_BROKER,
 | 
					    CONF_BROKER,
 | 
				
			||||||
    CONF_DEASSERT_RTS_DTR,
 | 
					    CONF_DEASSERT_RTS_DTR,
 | 
				
			||||||
@@ -43,6 +45,7 @@ from esphome.const import (
 | 
				
			|||||||
    SECRETS_FILES,
 | 
					    SECRETS_FILES,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
from esphome.core import CORE, EsphomeError, coroutine
 | 
					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.helpers import get_bool_env, indent, is_ip_address
 | 
				
			||||||
from esphome.log import AnsiFore, color, setup_log
 | 
					from esphome.log import AnsiFore, color, setup_log
 | 
				
			||||||
from esphome.types import ConfigType
 | 
					from esphome.types import ConfigType
 | 
				
			||||||
@@ -106,13 +109,15 @@ def choose_prompt(options, purpose: str = None):
 | 
				
			|||||||
    return options[opt - 1][1]
 | 
					    return options[opt - 1][1]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Purpose(StrEnum):
 | 
				
			||||||
 | 
					    UPLOADING = "uploading"
 | 
				
			||||||
 | 
					    LOGGING = "logging"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def choose_upload_log_host(
 | 
					def choose_upload_log_host(
 | 
				
			||||||
    default: list[str] | str | None,
 | 
					    default: list[str] | str | None,
 | 
				
			||||||
    check_default: str | None,
 | 
					    check_default: str | None,
 | 
				
			||||||
    show_ota: bool,
 | 
					    purpose: Purpose,
 | 
				
			||||||
    show_mqtt: bool,
 | 
					 | 
				
			||||||
    show_api: bool,
 | 
					 | 
				
			||||||
    purpose: str | None = None,
 | 
					 | 
				
			||||||
) -> list[str]:
 | 
					) -> list[str]:
 | 
				
			||||||
    # Convert to list for uniform handling
 | 
					    # Convert to list for uniform handling
 | 
				
			||||||
    defaults = [default] if isinstance(default, str) else default or []
 | 
					    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))
 | 
					                resolved.append(choose_prompt(options, purpose=purpose))
 | 
				
			||||||
            elif device == "OTA":
 | 
					            elif device == "OTA":
 | 
				
			||||||
                if CORE.address and (
 | 
					                # ensure IP adresses are used first
 | 
				
			||||||
                    (show_ota and "ota" in CORE.config)
 | 
					                if is_ip_address(CORE.address) and (
 | 
				
			||||||
                    or (show_api and "api" in CORE.config)
 | 
					                    (purpose == Purpose.LOGGING and has_api())
 | 
				
			||||||
 | 
					                    or (purpose == Purpose.UPLOADING and has_ota())
 | 
				
			||||||
                ):
 | 
					                ):
 | 
				
			||||||
                    resolved.append(CORE.address)
 | 
					                    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:
 | 
					            else:
 | 
				
			||||||
                resolved.append(device)
 | 
					                resolved.append(device)
 | 
				
			||||||
        if not resolved:
 | 
					        if not resolved:
 | 
				
			||||||
@@ -149,39 +171,111 @@ def choose_upload_log_host(
 | 
				
			|||||||
    options = [
 | 
					    options = [
 | 
				
			||||||
        (f"{port.path} ({port.description})", port.path) for port in get_serial_ports()
 | 
					        (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 purpose == Purpose.LOGGING:
 | 
				
			||||||
    if show_mqtt and has_mqtt_logging():
 | 
					        if has_mqtt_logging():
 | 
				
			||||||
        mqtt_config = CORE.config[CONF_MQTT]
 | 
					            mqtt_config = CORE.config[CONF_MQTT]
 | 
				
			||||||
        options.append((f"MQTT ({mqtt_config[CONF_BROKER]})", "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]:
 | 
					    if check_default is not None and check_default in [opt[1] for opt in options]:
 | 
				
			||||||
        return [check_default]
 | 
					        return [check_default]
 | 
				
			||||||
    return [choose_prompt(options, purpose=purpose)]
 | 
					    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]
 | 
					    log_topic = mqtt_config[CONF_LOG_TOPIC]
 | 
				
			||||||
    if log_topic is None:
 | 
					    if log_topic is None:
 | 
				
			||||||
        return False
 | 
					        return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if CONF_TOPIC not in log_topic:
 | 
					    if CONF_TOPIC not in log_topic:
 | 
				
			||||||
        return False
 | 
					        return False
 | 
				
			||||||
    return log_topic.get(CONF_LEVEL, None) != "NONE"
 | 
					
 | 
				
			||||||
 | 
					    return log_topic[CONF_LEVEL] != "NONE"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def has_mqtt_logging() -> bool:
 | 
					def has_mqtt() -> bool:
 | 
				
			||||||
    """Check if MQTT logging is available."""
 | 
					    """Check if MQTT is available."""
 | 
				
			||||||
    return (mqtt_config := CORE.config.get(CONF_MQTT)) and mqtt_logging_enabled(
 | 
					    return CONF_MQTT in CORE.config
 | 
				
			||||||
        mqtt_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:
 | 
					def get_port_type(port: str) -> str:
 | 
				
			||||||
    if port.startswith("/") or port.startswith("COM"):
 | 
					    if port.startswith("/") or port.startswith("COM"):
 | 
				
			||||||
        return "SERIAL"
 | 
					        return "SERIAL"
 | 
				
			||||||
    if port == "MQTT":
 | 
					    return _PORT_TO_PORT_TYPE.get(port, "NETWORK")
 | 
				
			||||||
        return "MQTT"
 | 
					 | 
				
			||||||
    return "NETWORK"
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def run_miniterm(config: ConfigType, port: str, args) -> int:
 | 
					def run_miniterm(config: ConfigType, port: str, args) -> int:
 | 
				
			||||||
@@ -439,23 +533,9 @@ def upload_program(
 | 
				
			|||||||
    password = ota_conf.get(CONF_PASSWORD, "")
 | 
					    password = ota_conf.get(CONF_PASSWORD, "")
 | 
				
			||||||
    binary = args.file if getattr(args, "file", None) is not None else CORE.firmware_bin
 | 
					    binary = args.file if getattr(args, "file", None) is not None else CORE.firmware_bin
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Check if we should use MQTT for address resolution
 | 
					    # MQTT address resolution
 | 
				
			||||||
    # This happens when no device was specified, or the current host is "MQTT"/"OTA"
 | 
					    if get_port_type(host) in ("MQTT", "MQTTIP"):
 | 
				
			||||||
    if (
 | 
					        devices = mqtt_get_ip(config, args.username, args.password, args.client_id)
 | 
				
			||||||
        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
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
        ]
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return espota2.run_ota(devices, remote_port, password, binary)
 | 
					    return espota2.run_ota(devices, remote_port, password, binary)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -476,20 +556,28 @@ def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int
 | 
				
			|||||||
    if get_port_type(port) == "SERIAL":
 | 
					    if get_port_type(port) == "SERIAL":
 | 
				
			||||||
        check_permissions(port)
 | 
					        check_permissions(port)
 | 
				
			||||||
        return run_miniterm(config, port, args)
 | 
					        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
 | 
					                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)
 | 
					            return run_logs(config, addresses_to_use)
 | 
				
			||||||
    if get_port_type(port) in ("NETWORK", "MQTT") and "mqtt" in config:
 | 
					
 | 
				
			||||||
 | 
					    if port_type in ("NETWORK", "MQTT") and has_mqtt_logging():
 | 
				
			||||||
        from esphome import mqtt
 | 
					        from esphome import mqtt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return mqtt.show_logs(
 | 
					        return mqtt.show_logs(
 | 
				
			||||||
@@ -555,10 +643,7 @@ def command_upload(args: ArgsProtocol, config: ConfigType) -> int | None:
 | 
				
			|||||||
    devices = choose_upload_log_host(
 | 
					    devices = choose_upload_log_host(
 | 
				
			||||||
        default=args.device,
 | 
					        default=args.device,
 | 
				
			||||||
        check_default=None,
 | 
					        check_default=None,
 | 
				
			||||||
        show_ota=True,
 | 
					        purpose=Purpose.UPLOADING,
 | 
				
			||||||
        show_mqtt=False,
 | 
					 | 
				
			||||||
        show_api=False,
 | 
					 | 
				
			||||||
        purpose="uploading",
 | 
					 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    exit_code, _ = upload_program(config, args, devices)
 | 
					    exit_code, _ = upload_program(config, args, devices)
 | 
				
			||||||
@@ -583,10 +668,7 @@ def command_logs(args: ArgsProtocol, config: ConfigType) -> int | None:
 | 
				
			|||||||
    devices = choose_upload_log_host(
 | 
					    devices = choose_upload_log_host(
 | 
				
			||||||
        default=args.device,
 | 
					        default=args.device,
 | 
				
			||||||
        check_default=None,
 | 
					        check_default=None,
 | 
				
			||||||
        show_ota=False,
 | 
					        purpose=Purpose.LOGGING,
 | 
				
			||||||
        show_mqtt=True,
 | 
					 | 
				
			||||||
        show_api=True,
 | 
					 | 
				
			||||||
        purpose="logging",
 | 
					 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    return show_logs(config, args, devices)
 | 
					    return show_logs(config, args, devices)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -612,10 +694,7 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None:
 | 
				
			|||||||
    devices = choose_upload_log_host(
 | 
					    devices = choose_upload_log_host(
 | 
				
			||||||
        default=args.device,
 | 
					        default=args.device,
 | 
				
			||||||
        check_default=None,
 | 
					        check_default=None,
 | 
				
			||||||
        show_ota=True,
 | 
					        purpose=Purpose.UPLOADING,
 | 
				
			||||||
        show_mqtt=False,
 | 
					 | 
				
			||||||
        show_api=True,
 | 
					 | 
				
			||||||
        purpose="uploading",
 | 
					 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    exit_code, successful_device = upload_program(config, args, devices)
 | 
					    exit_code, successful_device = upload_program(config, args, devices)
 | 
				
			||||||
@@ -632,10 +711,7 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None:
 | 
				
			|||||||
    devices = choose_upload_log_host(
 | 
					    devices = choose_upload_log_host(
 | 
				
			||||||
        default=successful_device,
 | 
					        default=successful_device,
 | 
				
			||||||
        check_default=successful_device,
 | 
					        check_default=successful_device,
 | 
				
			||||||
        show_ota=False,
 | 
					        purpose=Purpose.LOGGING,
 | 
				
			||||||
        show_mqtt=True,
 | 
					 | 
				
			||||||
        show_api=True,
 | 
					 | 
				
			||||||
        purpose="logging",
 | 
					 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    return show_logs(config, args, devices)
 | 
					    return show_logs(config, args, devices)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -114,6 +114,7 @@ CONF_AND = "and"
 | 
				
			|||||||
CONF_ANGLE = "angle"
 | 
					CONF_ANGLE = "angle"
 | 
				
			||||||
CONF_ANY = "any"
 | 
					CONF_ANY = "any"
 | 
				
			||||||
CONF_AP = "ap"
 | 
					CONF_AP = "ap"
 | 
				
			||||||
 | 
					CONF_API = "api"
 | 
				
			||||||
CONF_APPARENT_POWER = "apparent_power"
 | 
					CONF_APPARENT_POWER = "apparent_power"
 | 
				
			||||||
CONF_ARDUINO_VERSION = "arduino_version"
 | 
					CONF_ARDUINO_VERSION = "arduino_version"
 | 
				
			||||||
CONF_AREA = "area"
 | 
					CONF_AREA = "area"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,16 +12,28 @@ import pytest
 | 
				
			|||||||
from pytest import CaptureFixture
 | 
					from pytest import CaptureFixture
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from esphome.__main__ import (
 | 
					from esphome.__main__ import (
 | 
				
			||||||
 | 
					    Purpose,
 | 
				
			||||||
    choose_upload_log_host,
 | 
					    choose_upload_log_host,
 | 
				
			||||||
    command_rename,
 | 
					    command_rename,
 | 
				
			||||||
    command_wizard,
 | 
					    command_wizard,
 | 
				
			||||||
 | 
					    get_port_type,
 | 
				
			||||||
 | 
					    has_ip_address,
 | 
				
			||||||
 | 
					    has_mqtt,
 | 
				
			||||||
 | 
					    has_mqtt_ip_lookup,
 | 
				
			||||||
 | 
					    has_mqtt_logging,
 | 
				
			||||||
 | 
					    has_non_ip_address,
 | 
				
			||||||
 | 
					    has_resolvable_address,
 | 
				
			||||||
 | 
					    mqtt_get_ip,
 | 
				
			||||||
    show_logs,
 | 
					    show_logs,
 | 
				
			||||||
    upload_program,
 | 
					    upload_program,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
from esphome.const import (
 | 
					from esphome.const import (
 | 
				
			||||||
 | 
					    CONF_API,
 | 
				
			||||||
    CONF_BROKER,
 | 
					    CONF_BROKER,
 | 
				
			||||||
    CONF_DISABLED,
 | 
					    CONF_DISABLED,
 | 
				
			||||||
    CONF_ESPHOME,
 | 
					    CONF_ESPHOME,
 | 
				
			||||||
 | 
					    CONF_LEVEL,
 | 
				
			||||||
 | 
					    CONF_LOG_TOPIC,
 | 
				
			||||||
    CONF_MDNS,
 | 
					    CONF_MDNS,
 | 
				
			||||||
    CONF_MQTT,
 | 
					    CONF_MQTT,
 | 
				
			||||||
    CONF_NAME,
 | 
					    CONF_NAME,
 | 
				
			||||||
@@ -30,6 +42,7 @@ from esphome.const import (
 | 
				
			|||||||
    CONF_PLATFORM,
 | 
					    CONF_PLATFORM,
 | 
				
			||||||
    CONF_PORT,
 | 
					    CONF_PORT,
 | 
				
			||||||
    CONF_SUBSTITUTIONS,
 | 
					    CONF_SUBSTITUTIONS,
 | 
				
			||||||
 | 
					    CONF_TOPIC,
 | 
				
			||||||
    CONF_USE_ADDRESS,
 | 
					    CONF_USE_ADDRESS,
 | 
				
			||||||
    CONF_WIFI,
 | 
					    CONF_WIFI,
 | 
				
			||||||
    KEY_CORE,
 | 
					    KEY_CORE,
 | 
				
			||||||
@@ -147,6 +160,13 @@ def mock_is_ip_address() -> Generator[Mock]:
 | 
				
			|||||||
        yield mock
 | 
					        yield mock
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@pytest.fixture
 | 
				
			||||||
 | 
					def mock_mqtt_get_ip() -> Generator[Mock]:
 | 
				
			||||||
 | 
					    """Mock mqtt_get_ip for testing."""
 | 
				
			||||||
 | 
					    with patch("esphome.__main__.mqtt_get_ip") as mock:
 | 
				
			||||||
 | 
					        yield mock
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@pytest.fixture
 | 
					@pytest.fixture
 | 
				
			||||||
def mock_serial_ports() -> Generator[Mock]:
 | 
					def mock_serial_ports() -> Generator[Mock]:
 | 
				
			||||||
    """Mock get_serial_ports to return test ports."""
 | 
					    """Mock get_serial_ports to return test ports."""
 | 
				
			||||||
@@ -189,62 +209,56 @@ def mock_run_external_process() -> Generator[Mock]:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
def test_choose_upload_log_host_with_string_default() -> None:
 | 
					def test_choose_upload_log_host_with_string_default() -> None:
 | 
				
			||||||
    """Test with a single string default device."""
 | 
					    """Test with a single string default device."""
 | 
				
			||||||
 | 
					    setup_core()
 | 
				
			||||||
    result = choose_upload_log_host(
 | 
					    result = choose_upload_log_host(
 | 
				
			||||||
        default="192.168.1.100",
 | 
					        default="192.168.1.100",
 | 
				
			||||||
        check_default=None,
 | 
					        check_default=None,
 | 
				
			||||||
        show_ota=False,
 | 
					        purpose=Purpose.UPLOADING,
 | 
				
			||||||
        show_mqtt=False,
 | 
					 | 
				
			||||||
        show_api=False,
 | 
					 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    assert result == ["192.168.1.100"]
 | 
					    assert result == ["192.168.1.100"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_choose_upload_log_host_with_list_default() -> None:
 | 
					def test_choose_upload_log_host_with_list_default() -> None:
 | 
				
			||||||
    """Test with a list of default devices."""
 | 
					    """Test with a list of default devices."""
 | 
				
			||||||
 | 
					    setup_core()
 | 
				
			||||||
    result = choose_upload_log_host(
 | 
					    result = choose_upload_log_host(
 | 
				
			||||||
        default=["192.168.1.100", "192.168.1.101"],
 | 
					        default=["192.168.1.100", "192.168.1.101"],
 | 
				
			||||||
        check_default=None,
 | 
					        check_default=None,
 | 
				
			||||||
        show_ota=False,
 | 
					        purpose=Purpose.UPLOADING,
 | 
				
			||||||
        show_mqtt=False,
 | 
					 | 
				
			||||||
        show_api=False,
 | 
					 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    assert result == ["192.168.1.100", "192.168.1.101"]
 | 
					    assert result == ["192.168.1.100", "192.168.1.101"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_choose_upload_log_host_with_multiple_ip_addresses() -> None:
 | 
					def test_choose_upload_log_host_with_multiple_ip_addresses() -> None:
 | 
				
			||||||
    """Test with multiple IP addresses as defaults."""
 | 
					    """Test with multiple IP addresses as defaults."""
 | 
				
			||||||
 | 
					    setup_core()
 | 
				
			||||||
    result = choose_upload_log_host(
 | 
					    result = choose_upload_log_host(
 | 
				
			||||||
        default=["1.2.3.4", "4.5.5.6"],
 | 
					        default=["1.2.3.4", "4.5.5.6"],
 | 
				
			||||||
        check_default=None,
 | 
					        check_default=None,
 | 
				
			||||||
        show_ota=False,
 | 
					        purpose=Purpose.LOGGING,
 | 
				
			||||||
        show_mqtt=False,
 | 
					 | 
				
			||||||
        show_api=False,
 | 
					 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    assert result == ["1.2.3.4", "4.5.5.6"]
 | 
					    assert result == ["1.2.3.4", "4.5.5.6"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_choose_upload_log_host_with_mixed_hostnames_and_ips() -> None:
 | 
					def test_choose_upload_log_host_with_mixed_hostnames_and_ips() -> None:
 | 
				
			||||||
    """Test with a mix of hostnames and IP addresses."""
 | 
					    """Test with a mix of hostnames and IP addresses."""
 | 
				
			||||||
 | 
					    setup_core()
 | 
				
			||||||
    result = choose_upload_log_host(
 | 
					    result = choose_upload_log_host(
 | 
				
			||||||
        default=["host.one", "host.one.local", "1.2.3.4"],
 | 
					        default=["host.one", "host.one.local", "1.2.3.4"],
 | 
				
			||||||
        check_default=None,
 | 
					        check_default=None,
 | 
				
			||||||
        show_ota=False,
 | 
					        purpose=Purpose.UPLOADING,
 | 
				
			||||||
        show_mqtt=False,
 | 
					 | 
				
			||||||
        show_api=False,
 | 
					 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    assert result == ["host.one", "host.one.local", "1.2.3.4"]
 | 
					    assert result == ["host.one", "host.one.local", "1.2.3.4"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_choose_upload_log_host_with_ota_list() -> None:
 | 
					def test_choose_upload_log_host_with_ota_list() -> None:
 | 
				
			||||||
    """Test with OTA as the only item in the list."""
 | 
					    """Test with OTA as the only item in the list."""
 | 
				
			||||||
    setup_core(config={"ota": {}}, address="192.168.1.100")
 | 
					    setup_core(config={CONF_OTA: {}}, address="192.168.1.100")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    result = choose_upload_log_host(
 | 
					    result = choose_upload_log_host(
 | 
				
			||||||
        default=["OTA"],
 | 
					        default=["OTA"],
 | 
				
			||||||
        check_default=None,
 | 
					        check_default=None,
 | 
				
			||||||
        show_ota=True,
 | 
					        purpose=Purpose.UPLOADING,
 | 
				
			||||||
        show_mqtt=False,
 | 
					 | 
				
			||||||
        show_api=False,
 | 
					 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    assert result == ["192.168.1.100"]
 | 
					    assert result == ["192.168.1.100"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -252,16 +266,27 @@ def test_choose_upload_log_host_with_ota_list() -> None:
 | 
				
			|||||||
@pytest.mark.usefixtures("mock_has_mqtt_logging")
 | 
					@pytest.mark.usefixtures("mock_has_mqtt_logging")
 | 
				
			||||||
def test_choose_upload_log_host_with_ota_list_mqtt_fallback() -> None:
 | 
					def test_choose_upload_log_host_with_ota_list_mqtt_fallback() -> None:
 | 
				
			||||||
    """Test with OTA list falling back to MQTT when no address."""
 | 
					    """Test with OTA list falling back to MQTT when no address."""
 | 
				
			||||||
    setup_core()
 | 
					    setup_core(config={CONF_OTA: {}, "mqtt": {}})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    result = choose_upload_log_host(
 | 
					    result = choose_upload_log_host(
 | 
				
			||||||
        default=["OTA"],
 | 
					        default=["OTA"],
 | 
				
			||||||
        check_default=None,
 | 
					        check_default=None,
 | 
				
			||||||
        show_ota=False,
 | 
					        purpose=Purpose.UPLOADING,
 | 
				
			||||||
        show_mqtt=True,
 | 
					 | 
				
			||||||
        show_api=False,
 | 
					 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    assert result == ["MQTT"]
 | 
					    assert result == ["MQTTIP"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@pytest.mark.usefixtures("mock_has_mqtt_logging")
 | 
				
			||||||
 | 
					def test_choose_upload_log_host_with_ota_list_mqtt_fallback_logging() -> None:
 | 
				
			||||||
 | 
					    """Test with OTA list with API and MQTT when no address."""
 | 
				
			||||||
 | 
					    setup_core(config={CONF_API: {}, "mqtt": {}})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    result = choose_upload_log_host(
 | 
				
			||||||
 | 
					        default=["OTA"],
 | 
				
			||||||
 | 
					        check_default=None,
 | 
				
			||||||
 | 
					        purpose=Purpose.LOGGING,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    assert result == ["MQTTIP", "MQTT"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@pytest.mark.usefixtures("mock_no_serial_ports")
 | 
					@pytest.mark.usefixtures("mock_no_serial_ports")
 | 
				
			||||||
@@ -269,12 +294,11 @@ def test_choose_upload_log_host_with_serial_device_no_ports(
 | 
				
			|||||||
    caplog: pytest.LogCaptureFixture,
 | 
					    caplog: pytest.LogCaptureFixture,
 | 
				
			||||||
) -> None:
 | 
					) -> None:
 | 
				
			||||||
    """Test SERIAL device when no serial ports are found."""
 | 
					    """Test SERIAL device when no serial ports are found."""
 | 
				
			||||||
 | 
					    setup_core()
 | 
				
			||||||
    result = choose_upload_log_host(
 | 
					    result = choose_upload_log_host(
 | 
				
			||||||
        default="SERIAL",
 | 
					        default="SERIAL",
 | 
				
			||||||
        check_default=None,
 | 
					        check_default=None,
 | 
				
			||||||
        show_ota=False,
 | 
					        purpose=Purpose.UPLOADING,
 | 
				
			||||||
        show_mqtt=False,
 | 
					 | 
				
			||||||
        show_api=False,
 | 
					 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    assert result == []
 | 
					    assert result == []
 | 
				
			||||||
    assert "No serial ports found, skipping SERIAL device" in caplog.text
 | 
					    assert "No serial ports found, skipping SERIAL device" in caplog.text
 | 
				
			||||||
@@ -285,13 +309,11 @@ def test_choose_upload_log_host_with_serial_device_with_ports(
 | 
				
			|||||||
    mock_choose_prompt: Mock,
 | 
					    mock_choose_prompt: Mock,
 | 
				
			||||||
) -> None:
 | 
					) -> None:
 | 
				
			||||||
    """Test SERIAL device when serial ports are available."""
 | 
					    """Test SERIAL device when serial ports are available."""
 | 
				
			||||||
 | 
					    setup_core()
 | 
				
			||||||
    result = choose_upload_log_host(
 | 
					    result = choose_upload_log_host(
 | 
				
			||||||
        default="SERIAL",
 | 
					        default="SERIAL",
 | 
				
			||||||
        check_default=None,
 | 
					        check_default=None,
 | 
				
			||||||
        show_ota=False,
 | 
					        purpose=Purpose.UPLOADING,
 | 
				
			||||||
        show_mqtt=False,
 | 
					 | 
				
			||||||
        show_api=False,
 | 
					 | 
				
			||||||
        purpose="testing",
 | 
					 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    assert result == ["/dev/ttyUSB0"]
 | 
					    assert result == ["/dev/ttyUSB0"]
 | 
				
			||||||
    mock_choose_prompt.assert_called_once_with(
 | 
					    mock_choose_prompt.assert_called_once_with(
 | 
				
			||||||
@@ -299,34 +321,42 @@ def test_choose_upload_log_host_with_serial_device_with_ports(
 | 
				
			|||||||
            ("/dev/ttyUSB0 (USB Serial)", "/dev/ttyUSB0"),
 | 
					            ("/dev/ttyUSB0 (USB Serial)", "/dev/ttyUSB0"),
 | 
				
			||||||
            ("/dev/ttyUSB1 (Another USB Serial)", "/dev/ttyUSB1"),
 | 
					            ("/dev/ttyUSB1 (Another USB Serial)", "/dev/ttyUSB1"),
 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
        purpose="testing",
 | 
					        purpose=Purpose.UPLOADING,
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_choose_upload_log_host_with_ota_device_with_ota_config() -> None:
 | 
					def test_choose_upload_log_host_with_ota_device_with_ota_config() -> None:
 | 
				
			||||||
    """Test OTA device when OTA is configured."""
 | 
					    """Test OTA device when OTA is configured."""
 | 
				
			||||||
    setup_core(config={"ota": {}}, address="192.168.1.100")
 | 
					    setup_core(config={CONF_OTA: {}}, address="192.168.1.100")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    result = choose_upload_log_host(
 | 
					    result = choose_upload_log_host(
 | 
				
			||||||
        default="OTA",
 | 
					        default="OTA",
 | 
				
			||||||
        check_default=None,
 | 
					        check_default=None,
 | 
				
			||||||
        show_ota=True,
 | 
					        purpose=Purpose.UPLOADING,
 | 
				
			||||||
        show_mqtt=False,
 | 
					 | 
				
			||||||
        show_api=False,
 | 
					 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    assert result == ["192.168.1.100"]
 | 
					    assert result == ["192.168.1.100"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_choose_upload_log_host_with_ota_device_with_api_config() -> None:
 | 
					def test_choose_upload_log_host_with_ota_device_with_api_config() -> None:
 | 
				
			||||||
    """Test OTA device when API is configured."""
 | 
					    """Test OTA device when API is configured (no upload without OTA in config)."""
 | 
				
			||||||
    setup_core(config={"api": {}}, address="192.168.1.100")
 | 
					    setup_core(config={CONF_API: {}}, address="192.168.1.100")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    result = choose_upload_log_host(
 | 
					    result = choose_upload_log_host(
 | 
				
			||||||
        default="OTA",
 | 
					        default="OTA",
 | 
				
			||||||
        check_default=None,
 | 
					        check_default=None,
 | 
				
			||||||
        show_ota=False,
 | 
					        purpose=Purpose.UPLOADING,
 | 
				
			||||||
        show_mqtt=False,
 | 
					    )
 | 
				
			||||||
        show_api=True,
 | 
					    assert result == []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_choose_upload_log_host_with_ota_device_with_api_config_logging() -> None:
 | 
				
			||||||
 | 
					    """Test OTA device when API is configured."""
 | 
				
			||||||
 | 
					    setup_core(config={CONF_API: {}}, address="192.168.1.100")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    result = choose_upload_log_host(
 | 
				
			||||||
 | 
					        default="OTA",
 | 
				
			||||||
 | 
					        check_default=None,
 | 
				
			||||||
 | 
					        purpose=Purpose.LOGGING,
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    assert result == ["192.168.1.100"]
 | 
					    assert result == ["192.168.1.100"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -334,14 +364,12 @@ def test_choose_upload_log_host_with_ota_device_with_api_config() -> None:
 | 
				
			|||||||
@pytest.mark.usefixtures("mock_has_mqtt_logging")
 | 
					@pytest.mark.usefixtures("mock_has_mqtt_logging")
 | 
				
			||||||
def test_choose_upload_log_host_with_ota_device_fallback_to_mqtt() -> None:
 | 
					def test_choose_upload_log_host_with_ota_device_fallback_to_mqtt() -> None:
 | 
				
			||||||
    """Test OTA device fallback to MQTT when no OTA/API config."""
 | 
					    """Test OTA device fallback to MQTT when no OTA/API config."""
 | 
				
			||||||
    setup_core()
 | 
					    setup_core(config={"mqtt": {}})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    result = choose_upload_log_host(
 | 
					    result = choose_upload_log_host(
 | 
				
			||||||
        default="OTA",
 | 
					        default="OTA",
 | 
				
			||||||
        check_default=None,
 | 
					        check_default=None,
 | 
				
			||||||
        show_ota=False,
 | 
					        purpose=Purpose.LOGGING,
 | 
				
			||||||
        show_mqtt=True,
 | 
					 | 
				
			||||||
        show_api=False,
 | 
					 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    assert result == ["MQTT"]
 | 
					    assert result == ["MQTT"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -354,9 +382,7 @@ def test_choose_upload_log_host_with_ota_device_no_fallback() -> None:
 | 
				
			|||||||
    result = choose_upload_log_host(
 | 
					    result = choose_upload_log_host(
 | 
				
			||||||
        default="OTA",
 | 
					        default="OTA",
 | 
				
			||||||
        check_default=None,
 | 
					        check_default=None,
 | 
				
			||||||
        show_ota=True,
 | 
					        purpose=Purpose.UPLOADING,
 | 
				
			||||||
        show_mqtt=True,
 | 
					 | 
				
			||||||
        show_api=False,
 | 
					 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    assert result == []
 | 
					    assert result == []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -364,7 +390,7 @@ def test_choose_upload_log_host_with_ota_device_no_fallback() -> None:
 | 
				
			|||||||
@pytest.mark.usefixtures("mock_choose_prompt")
 | 
					@pytest.mark.usefixtures("mock_choose_prompt")
 | 
				
			||||||
def test_choose_upload_log_host_multiple_devices() -> None:
 | 
					def test_choose_upload_log_host_multiple_devices() -> None:
 | 
				
			||||||
    """Test with multiple devices including special identifiers."""
 | 
					    """Test with multiple devices including special identifiers."""
 | 
				
			||||||
    setup_core(config={"ota": {}}, address="192.168.1.100")
 | 
					    setup_core(config={CONF_OTA: {}}, address="192.168.1.100")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    mock_ports = [MockSerialPort("/dev/ttyUSB0", "USB Serial")]
 | 
					    mock_ports = [MockSerialPort("/dev/ttyUSB0", "USB Serial")]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -372,9 +398,7 @@ def test_choose_upload_log_host_multiple_devices() -> None:
 | 
				
			|||||||
        result = choose_upload_log_host(
 | 
					        result = choose_upload_log_host(
 | 
				
			||||||
            default=["192.168.1.50", "OTA", "SERIAL"],
 | 
					            default=["192.168.1.50", "OTA", "SERIAL"],
 | 
				
			||||||
            check_default=None,
 | 
					            check_default=None,
 | 
				
			||||||
            show_ota=True,
 | 
					            purpose=Purpose.UPLOADING,
 | 
				
			||||||
            show_mqtt=False,
 | 
					 | 
				
			||||||
            show_api=False,
 | 
					 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        assert result == ["192.168.1.50", "192.168.1.100", "/dev/ttyUSB0"]
 | 
					        assert result == ["192.168.1.50", "192.168.1.100", "/dev/ttyUSB0"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -393,22 +417,19 @@ def test_choose_upload_log_host_no_defaults_with_serial_ports(
 | 
				
			|||||||
        result = choose_upload_log_host(
 | 
					        result = choose_upload_log_host(
 | 
				
			||||||
            default=None,
 | 
					            default=None,
 | 
				
			||||||
            check_default=None,
 | 
					            check_default=None,
 | 
				
			||||||
            show_ota=False,
 | 
					            purpose=Purpose.UPLOADING,
 | 
				
			||||||
            show_mqtt=False,
 | 
					 | 
				
			||||||
            show_api=False,
 | 
					 | 
				
			||||||
            purpose="uploading",
 | 
					 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        assert result == ["/dev/ttyUSB0"]
 | 
					        assert result == ["/dev/ttyUSB0"]
 | 
				
			||||||
        mock_choose_prompt.assert_called_once_with(
 | 
					        mock_choose_prompt.assert_called_once_with(
 | 
				
			||||||
            [("/dev/ttyUSB0 (USB Serial)", "/dev/ttyUSB0")],
 | 
					            [("/dev/ttyUSB0 (USB Serial)", "/dev/ttyUSB0")],
 | 
				
			||||||
            purpose="uploading",
 | 
					            purpose=Purpose.UPLOADING,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@pytest.mark.usefixtures("mock_no_serial_ports")
 | 
					@pytest.mark.usefixtures("mock_no_serial_ports")
 | 
				
			||||||
def test_choose_upload_log_host_no_defaults_with_ota() -> None:
 | 
					def test_choose_upload_log_host_no_defaults_with_ota() -> None:
 | 
				
			||||||
    """Test interactive mode with OTA option."""
 | 
					    """Test interactive mode with OTA option."""
 | 
				
			||||||
    setup_core(config={"ota": {}}, address="192.168.1.100")
 | 
					    setup_core(config={CONF_OTA: {}}, address="192.168.1.100")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    with patch(
 | 
					    with patch(
 | 
				
			||||||
        "esphome.__main__.choose_prompt", return_value="192.168.1.100"
 | 
					        "esphome.__main__.choose_prompt", return_value="192.168.1.100"
 | 
				
			||||||
@@ -416,21 +437,19 @@ def test_choose_upload_log_host_no_defaults_with_ota() -> None:
 | 
				
			|||||||
        result = choose_upload_log_host(
 | 
					        result = choose_upload_log_host(
 | 
				
			||||||
            default=None,
 | 
					            default=None,
 | 
				
			||||||
            check_default=None,
 | 
					            check_default=None,
 | 
				
			||||||
            show_ota=True,
 | 
					            purpose=Purpose.UPLOADING,
 | 
				
			||||||
            show_mqtt=False,
 | 
					 | 
				
			||||||
            show_api=False,
 | 
					 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        assert result == ["192.168.1.100"]
 | 
					        assert result == ["192.168.1.100"]
 | 
				
			||||||
        mock_prompt.assert_called_once_with(
 | 
					        mock_prompt.assert_called_once_with(
 | 
				
			||||||
            [("Over The Air (192.168.1.100)", "192.168.1.100")],
 | 
					            [("Over The Air (192.168.1.100)", "192.168.1.100")],
 | 
				
			||||||
            purpose=None,
 | 
					            purpose=Purpose.UPLOADING,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@pytest.mark.usefixtures("mock_no_serial_ports")
 | 
					@pytest.mark.usefixtures("mock_no_serial_ports")
 | 
				
			||||||
def test_choose_upload_log_host_no_defaults_with_api() -> None:
 | 
					def test_choose_upload_log_host_no_defaults_with_api() -> None:
 | 
				
			||||||
    """Test interactive mode with API option."""
 | 
					    """Test interactive mode with API option."""
 | 
				
			||||||
    setup_core(config={"api": {}}, address="192.168.1.100")
 | 
					    setup_core(config={CONF_API: {}}, address="192.168.1.100")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    with patch(
 | 
					    with patch(
 | 
				
			||||||
        "esphome.__main__.choose_prompt", return_value="192.168.1.100"
 | 
					        "esphome.__main__.choose_prompt", return_value="192.168.1.100"
 | 
				
			||||||
@@ -438,14 +457,12 @@ def test_choose_upload_log_host_no_defaults_with_api() -> None:
 | 
				
			|||||||
        result = choose_upload_log_host(
 | 
					        result = choose_upload_log_host(
 | 
				
			||||||
            default=None,
 | 
					            default=None,
 | 
				
			||||||
            check_default=None,
 | 
					            check_default=None,
 | 
				
			||||||
            show_ota=False,
 | 
					            purpose=Purpose.LOGGING,
 | 
				
			||||||
            show_mqtt=False,
 | 
					 | 
				
			||||||
            show_api=True,
 | 
					 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        assert result == ["192.168.1.100"]
 | 
					        assert result == ["192.168.1.100"]
 | 
				
			||||||
        mock_prompt.assert_called_once_with(
 | 
					        mock_prompt.assert_called_once_with(
 | 
				
			||||||
            [("Over The Air (192.168.1.100)", "192.168.1.100")],
 | 
					            [("Over The Air (192.168.1.100)", "192.168.1.100")],
 | 
				
			||||||
            purpose=None,
 | 
					            purpose=Purpose.LOGGING,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -458,14 +475,12 @@ def test_choose_upload_log_host_no_defaults_with_mqtt() -> None:
 | 
				
			|||||||
        result = choose_upload_log_host(
 | 
					        result = choose_upload_log_host(
 | 
				
			||||||
            default=None,
 | 
					            default=None,
 | 
				
			||||||
            check_default=None,
 | 
					            check_default=None,
 | 
				
			||||||
            show_ota=False,
 | 
					            purpose=Purpose.LOGGING,
 | 
				
			||||||
            show_mqtt=True,
 | 
					 | 
				
			||||||
            show_api=False,
 | 
					 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        assert result == ["MQTT"]
 | 
					        assert result == ["MQTT"]
 | 
				
			||||||
        mock_prompt.assert_called_once_with(
 | 
					        mock_prompt.assert_called_once_with(
 | 
				
			||||||
            [("MQTT (mqtt.local)", "MQTT")],
 | 
					            [("MQTT (mqtt.local)", "MQTT")],
 | 
				
			||||||
            purpose=None,
 | 
					            purpose=Purpose.LOGGING,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -475,7 +490,7 @@ def test_choose_upload_log_host_no_defaults_with_all_options(
 | 
				
			|||||||
) -> None:
 | 
					) -> None:
 | 
				
			||||||
    """Test interactive mode with all options available."""
 | 
					    """Test interactive mode with all options available."""
 | 
				
			||||||
    setup_core(
 | 
					    setup_core(
 | 
				
			||||||
        config={"ota": {}, "api": {}, CONF_MQTT: {CONF_BROKER: "mqtt.local"}},
 | 
					        config={CONF_OTA: {}, CONF_API: {}, CONF_MQTT: {CONF_BROKER: "mqtt.local"}},
 | 
				
			||||||
        address="192.168.1.100",
 | 
					        address="192.168.1.100",
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -485,32 +500,59 @@ def test_choose_upload_log_host_no_defaults_with_all_options(
 | 
				
			|||||||
        result = choose_upload_log_host(
 | 
					        result = choose_upload_log_host(
 | 
				
			||||||
            default=None,
 | 
					            default=None,
 | 
				
			||||||
            check_default=None,
 | 
					            check_default=None,
 | 
				
			||||||
            show_ota=True,
 | 
					            purpose=Purpose.UPLOADING,
 | 
				
			||||||
            show_mqtt=True,
 | 
					 | 
				
			||||||
            show_api=True,
 | 
					 | 
				
			||||||
            purpose="testing",
 | 
					 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        assert result == ["/dev/ttyUSB0"]
 | 
					        assert result == ["/dev/ttyUSB0"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        expected_options = [
 | 
					        expected_options = [
 | 
				
			||||||
            ("/dev/ttyUSB0 (USB Serial)", "/dev/ttyUSB0"),
 | 
					            ("/dev/ttyUSB0 (USB Serial)", "/dev/ttyUSB0"),
 | 
				
			||||||
            ("Over The Air (192.168.1.100)", "192.168.1.100"),
 | 
					            ("Over The Air (192.168.1.100)", "192.168.1.100"),
 | 
				
			||||||
            ("MQTT (mqtt.local)", "MQTT"),
 | 
					            ("Over The Air (MQTT IP lookup)", "MQTTIP"),
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
        mock_choose_prompt.assert_called_once_with(expected_options, purpose="testing")
 | 
					        mock_choose_prompt.assert_called_once_with(
 | 
				
			||||||
 | 
					            expected_options, purpose=Purpose.UPLOADING
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_choose_upload_log_host_no_defaults_with_all_options_logging(
 | 
				
			||||||
 | 
					    mock_choose_prompt: Mock,
 | 
				
			||||||
 | 
					) -> None:
 | 
				
			||||||
 | 
					    """Test interactive mode with all options available."""
 | 
				
			||||||
 | 
					    setup_core(
 | 
				
			||||||
 | 
					        config={CONF_OTA: {}, CONF_API: {}, CONF_MQTT: {CONF_BROKER: "mqtt.local"}},
 | 
				
			||||||
 | 
					        address="192.168.1.100",
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    mock_ports = [MockSerialPort("/dev/ttyUSB0", "USB Serial")]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    with patch("esphome.__main__.get_serial_ports", return_value=mock_ports):
 | 
				
			||||||
 | 
					        result = choose_upload_log_host(
 | 
				
			||||||
 | 
					            default=None,
 | 
				
			||||||
 | 
					            check_default=None,
 | 
				
			||||||
 | 
					            purpose=Purpose.LOGGING,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        assert result == ["/dev/ttyUSB0"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expected_options = [
 | 
				
			||||||
 | 
					            ("/dev/ttyUSB0 (USB Serial)", "/dev/ttyUSB0"),
 | 
				
			||||||
 | 
					            ("MQTT (mqtt.local)", "MQTT"),
 | 
				
			||||||
 | 
					            ("Over The Air (192.168.1.100)", "192.168.1.100"),
 | 
				
			||||||
 | 
					            ("Over The Air (MQTT IP lookup)", "MQTTIP"),
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					        mock_choose_prompt.assert_called_once_with(
 | 
				
			||||||
 | 
					            expected_options, purpose=Purpose.LOGGING
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@pytest.mark.usefixtures("mock_no_serial_ports")
 | 
					@pytest.mark.usefixtures("mock_no_serial_ports")
 | 
				
			||||||
def test_choose_upload_log_host_check_default_matches() -> None:
 | 
					def test_choose_upload_log_host_check_default_matches() -> None:
 | 
				
			||||||
    """Test when check_default matches an available option."""
 | 
					    """Test when check_default matches an available option."""
 | 
				
			||||||
    setup_core(config={"ota": {}}, address="192.168.1.100")
 | 
					    setup_core(config={CONF_OTA: {}}, address="192.168.1.100")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    result = choose_upload_log_host(
 | 
					    result = choose_upload_log_host(
 | 
				
			||||||
        default=None,
 | 
					        default=None,
 | 
				
			||||||
        check_default="192.168.1.100",
 | 
					        check_default="192.168.1.100",
 | 
				
			||||||
        show_ota=True,
 | 
					        purpose=Purpose.UPLOADING,
 | 
				
			||||||
        show_mqtt=False,
 | 
					 | 
				
			||||||
        show_api=False,
 | 
					 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    assert result == ["192.168.1.100"]
 | 
					    assert result == ["192.168.1.100"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -526,9 +568,7 @@ def test_choose_upload_log_host_check_default_no_match() -> None:
 | 
				
			|||||||
        result = choose_upload_log_host(
 | 
					        result = choose_upload_log_host(
 | 
				
			||||||
            default=None,
 | 
					            default=None,
 | 
				
			||||||
            check_default="192.168.1.100",
 | 
					            check_default="192.168.1.100",
 | 
				
			||||||
            show_ota=False,
 | 
					            purpose=Purpose.UPLOADING,
 | 
				
			||||||
            show_mqtt=False,
 | 
					 | 
				
			||||||
            show_api=False,
 | 
					 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        assert result == ["fallback"]
 | 
					        assert result == ["fallback"]
 | 
				
			||||||
        mock_prompt.assert_called_once()
 | 
					        mock_prompt.assert_called_once()
 | 
				
			||||||
@@ -537,13 +577,12 @@ def test_choose_upload_log_host_check_default_no_match() -> None:
 | 
				
			|||||||
@pytest.mark.usefixtures("mock_no_serial_ports")
 | 
					@pytest.mark.usefixtures("mock_no_serial_ports")
 | 
				
			||||||
def test_choose_upload_log_host_empty_defaults_list() -> None:
 | 
					def test_choose_upload_log_host_empty_defaults_list() -> None:
 | 
				
			||||||
    """Test with an empty list as default."""
 | 
					    """Test with an empty list as default."""
 | 
				
			||||||
 | 
					    setup_core()
 | 
				
			||||||
    with patch("esphome.__main__.choose_prompt", return_value="chosen") as mock_prompt:
 | 
					    with patch("esphome.__main__.choose_prompt", return_value="chosen") as mock_prompt:
 | 
				
			||||||
        result = choose_upload_log_host(
 | 
					        result = choose_upload_log_host(
 | 
				
			||||||
            default=[],
 | 
					            default=[],
 | 
				
			||||||
            check_default=None,
 | 
					            check_default=None,
 | 
				
			||||||
            show_ota=False,
 | 
					            purpose=Purpose.UPLOADING,
 | 
				
			||||||
            show_mqtt=False,
 | 
					 | 
				
			||||||
            show_api=False,
 | 
					 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        assert result == ["chosen"]
 | 
					        assert result == ["chosen"]
 | 
				
			||||||
        mock_prompt.assert_called_once()
 | 
					        mock_prompt.assert_called_once()
 | 
				
			||||||
@@ -559,9 +598,7 @@ def test_choose_upload_log_host_all_devices_unresolved(
 | 
				
			|||||||
    result = choose_upload_log_host(
 | 
					    result = choose_upload_log_host(
 | 
				
			||||||
        default=["SERIAL", "OTA"],
 | 
					        default=["SERIAL", "OTA"],
 | 
				
			||||||
        check_default=None,
 | 
					        check_default=None,
 | 
				
			||||||
        show_ota=False,
 | 
					        purpose=Purpose.UPLOADING,
 | 
				
			||||||
        show_mqtt=False,
 | 
					 | 
				
			||||||
        show_api=False,
 | 
					 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    assert result == []
 | 
					    assert result == []
 | 
				
			||||||
    assert (
 | 
					    assert (
 | 
				
			||||||
@@ -577,38 +614,132 @@ def test_choose_upload_log_host_mixed_resolved_unresolved() -> None:
 | 
				
			|||||||
    result = choose_upload_log_host(
 | 
					    result = choose_upload_log_host(
 | 
				
			||||||
        default=["192.168.1.50", "SERIAL", "OTA"],
 | 
					        default=["192.168.1.50", "SERIAL", "OTA"],
 | 
				
			||||||
        check_default=None,
 | 
					        check_default=None,
 | 
				
			||||||
        show_ota=False,
 | 
					        purpose=Purpose.UPLOADING,
 | 
				
			||||||
        show_mqtt=False,
 | 
					 | 
				
			||||||
        show_api=False,
 | 
					 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    assert result == ["192.168.1.50"]
 | 
					    assert result == ["192.168.1.50"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_choose_upload_log_host_ota_both_conditions() -> None:
 | 
					def test_choose_upload_log_host_ota_both_conditions() -> None:
 | 
				
			||||||
    """Test OTA device when both OTA and API are configured and enabled."""
 | 
					    """Test OTA device when both OTA and API are configured and enabled."""
 | 
				
			||||||
    setup_core(config={"ota": {}, "api": {}}, address="192.168.1.100")
 | 
					    setup_core(config={CONF_OTA: {}, CONF_API: {}}, address="192.168.1.100")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    result = choose_upload_log_host(
 | 
					    result = choose_upload_log_host(
 | 
				
			||||||
        default="OTA",
 | 
					        default="OTA",
 | 
				
			||||||
        check_default=None,
 | 
					        check_default=None,
 | 
				
			||||||
        show_ota=True,
 | 
					        purpose=Purpose.UPLOADING,
 | 
				
			||||||
        show_mqtt=False,
 | 
					 | 
				
			||||||
        show_api=True,
 | 
					 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    assert result == ["192.168.1.100"]
 | 
					    assert result == ["192.168.1.100"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@pytest.mark.usefixtures("mock_serial_ports")
 | 
				
			||||||
 | 
					def test_choose_upload_log_host_ota_ip_all_options() -> None:
 | 
				
			||||||
 | 
					    """Test OTA device when both static IP, OTA, API and MQTT are configured and enabled but MDNS not."""
 | 
				
			||||||
 | 
					    setup_core(
 | 
				
			||||||
 | 
					        config={
 | 
				
			||||||
 | 
					            CONF_OTA: {},
 | 
				
			||||||
 | 
					            CONF_API: {},
 | 
				
			||||||
 | 
					            CONF_MQTT: {
 | 
				
			||||||
 | 
					                CONF_BROKER: "mqtt.local",
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            CONF_MDNS: {
 | 
				
			||||||
 | 
					                CONF_DISABLED: True,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        address="192.168.1.100",
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    result = choose_upload_log_host(
 | 
				
			||||||
 | 
					        default="OTA",
 | 
				
			||||||
 | 
					        check_default=None,
 | 
				
			||||||
 | 
					        purpose=Purpose.UPLOADING,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    assert result == ["192.168.1.100", "MQTTIP"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@pytest.mark.usefixtures("mock_serial_ports")
 | 
				
			||||||
 | 
					def test_choose_upload_log_host_ota_local_all_options() -> None:
 | 
				
			||||||
 | 
					    """Test OTA device when both static IP, OTA, API and MQTT are configured and enabled but MDNS not."""
 | 
				
			||||||
 | 
					    setup_core(
 | 
				
			||||||
 | 
					        config={
 | 
				
			||||||
 | 
					            CONF_OTA: {},
 | 
				
			||||||
 | 
					            CONF_API: {},
 | 
				
			||||||
 | 
					            CONF_MQTT: {
 | 
				
			||||||
 | 
					                CONF_BROKER: "mqtt.local",
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            CONF_MDNS: {
 | 
				
			||||||
 | 
					                CONF_DISABLED: True,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        address="test.local",
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    result = choose_upload_log_host(
 | 
				
			||||||
 | 
					        default="OTA",
 | 
				
			||||||
 | 
					        check_default=None,
 | 
				
			||||||
 | 
					        purpose=Purpose.UPLOADING,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    assert result == ["MQTTIP", "test.local"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@pytest.mark.usefixtures("mock_serial_ports")
 | 
				
			||||||
 | 
					def test_choose_upload_log_host_ota_ip_all_options_logging() -> None:
 | 
				
			||||||
 | 
					    """Test OTA device when both static IP, OTA, API and MQTT are configured and enabled but MDNS not."""
 | 
				
			||||||
 | 
					    setup_core(
 | 
				
			||||||
 | 
					        config={
 | 
				
			||||||
 | 
					            CONF_OTA: {},
 | 
				
			||||||
 | 
					            CONF_API: {},
 | 
				
			||||||
 | 
					            CONF_MQTT: {
 | 
				
			||||||
 | 
					                CONF_BROKER: "mqtt.local",
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            CONF_MDNS: {
 | 
				
			||||||
 | 
					                CONF_DISABLED: True,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        address="192.168.1.100",
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    result = choose_upload_log_host(
 | 
				
			||||||
 | 
					        default="OTA",
 | 
				
			||||||
 | 
					        check_default=None,
 | 
				
			||||||
 | 
					        purpose=Purpose.LOGGING,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    assert result == ["192.168.1.100", "MQTTIP", "MQTT"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@pytest.mark.usefixtures("mock_serial_ports")
 | 
				
			||||||
 | 
					def test_choose_upload_log_host_ota_local_all_options_logging() -> None:
 | 
				
			||||||
 | 
					    """Test OTA device when both static IP, OTA, API and MQTT are configured and enabled but MDNS not."""
 | 
				
			||||||
 | 
					    setup_core(
 | 
				
			||||||
 | 
					        config={
 | 
				
			||||||
 | 
					            CONF_OTA: {},
 | 
				
			||||||
 | 
					            CONF_API: {},
 | 
				
			||||||
 | 
					            CONF_MQTT: {
 | 
				
			||||||
 | 
					                CONF_BROKER: "mqtt.local",
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            CONF_MDNS: {
 | 
				
			||||||
 | 
					                CONF_DISABLED: True,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        address="test.local",
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    result = choose_upload_log_host(
 | 
				
			||||||
 | 
					        default="OTA",
 | 
				
			||||||
 | 
					        check_default=None,
 | 
				
			||||||
 | 
					        purpose=Purpose.LOGGING,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    assert result == ["MQTTIP", "MQTT", "test.local"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@pytest.mark.usefixtures("mock_no_mqtt_logging")
 | 
					@pytest.mark.usefixtures("mock_no_mqtt_logging")
 | 
				
			||||||
def test_choose_upload_log_host_no_address_with_ota_config() -> None:
 | 
					def test_choose_upload_log_host_no_address_with_ota_config() -> None:
 | 
				
			||||||
    """Test OTA device when OTA is configured but no address is set."""
 | 
					    """Test OTA device when OTA is configured but no address is set."""
 | 
				
			||||||
    setup_core(config={"ota": {}})
 | 
					    setup_core(config={CONF_OTA: {}})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    result = choose_upload_log_host(
 | 
					    result = choose_upload_log_host(
 | 
				
			||||||
        default="OTA",
 | 
					        default="OTA",
 | 
				
			||||||
        check_default=None,
 | 
					        check_default=None,
 | 
				
			||||||
        show_ota=True,
 | 
					        purpose=Purpose.UPLOADING,
 | 
				
			||||||
        show_mqtt=False,
 | 
					 | 
				
			||||||
        show_api=False,
 | 
					 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    assert result == []
 | 
					    assert result == []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -806,18 +937,15 @@ def test_upload_program_ota_no_config(
 | 
				
			|||||||
        upload_program(config, args, devices)
 | 
					        upload_program(config, args, devices)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@patch("esphome.mqtt.get_esphome_device_ip")
 | 
					 | 
				
			||||||
def test_upload_program_ota_with_mqtt_resolution(
 | 
					def test_upload_program_ota_with_mqtt_resolution(
 | 
				
			||||||
    mock_mqtt_get_ip: Mock,
 | 
					    mock_mqtt_get_ip: Mock,
 | 
				
			||||||
    mock_is_ip_address: Mock,
 | 
					    mock_is_ip_address: Mock,
 | 
				
			||||||
    mock_run_ota: Mock,
 | 
					    mock_run_ota: Mock,
 | 
				
			||||||
    mock_get_port_type: Mock,
 | 
					 | 
				
			||||||
    tmp_path: Path,
 | 
					    tmp_path: Path,
 | 
				
			||||||
) -> None:
 | 
					) -> None:
 | 
				
			||||||
    """Test upload_program with OTA using MQTT for address resolution."""
 | 
					    """Test upload_program with OTA using MQTT for address resolution."""
 | 
				
			||||||
    setup_core(address="device.local", platform=PLATFORM_ESP32, tmp_path=tmp_path)
 | 
					    setup_core(address="device.local", platform=PLATFORM_ESP32, tmp_path=tmp_path)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    mock_get_port_type.side_effect = ["MQTT", "NETWORK"]
 | 
					 | 
				
			||||||
    mock_is_ip_address.return_value = False
 | 
					    mock_is_ip_address.return_value = False
 | 
				
			||||||
    mock_mqtt_get_ip.return_value = ["192.168.1.100"]
 | 
					    mock_mqtt_get_ip.return_value = ["192.168.1.100"]
 | 
				
			||||||
    mock_run_ota.return_value = (0, "192.168.1.100")
 | 
					    mock_run_ota.return_value = (0, "192.168.1.100")
 | 
				
			||||||
@@ -847,9 +975,7 @@ def test_upload_program_ota_with_mqtt_resolution(
 | 
				
			|||||||
    expected_firmware = str(
 | 
					    expected_firmware = str(
 | 
				
			||||||
        tmp_path / ".esphome" / "build" / "test" / ".pioenvs" / "test" / "firmware.bin"
 | 
					        tmp_path / ".esphome" / "build" / "test" / ".pioenvs" / "test" / "firmware.bin"
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    mock_run_ota.assert_called_once_with(
 | 
					    mock_run_ota.assert_called_once_with(["192.168.1.100"], 3232, "", expected_firmware)
 | 
				
			||||||
        [["192.168.1.100"]], 3232, "", expected_firmware
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@patch("esphome.__main__.importlib.import_module")
 | 
					@patch("esphome.__main__.importlib.import_module")
 | 
				
			||||||
@@ -910,18 +1036,16 @@ def test_show_logs_no_logger() -> None:
 | 
				
			|||||||
@patch("esphome.components.api.client.run_logs")
 | 
					@patch("esphome.components.api.client.run_logs")
 | 
				
			||||||
def test_show_logs_api(
 | 
					def test_show_logs_api(
 | 
				
			||||||
    mock_run_logs: Mock,
 | 
					    mock_run_logs: Mock,
 | 
				
			||||||
    mock_get_port_type: Mock,
 | 
					 | 
				
			||||||
) -> None:
 | 
					) -> None:
 | 
				
			||||||
    """Test show_logs with API."""
 | 
					    """Test show_logs with API."""
 | 
				
			||||||
    setup_core(
 | 
					    setup_core(
 | 
				
			||||||
        config={
 | 
					        config={
 | 
				
			||||||
            "logger": {},
 | 
					            "logger": {},
 | 
				
			||||||
            "api": {},
 | 
					            CONF_API: {},
 | 
				
			||||||
            CONF_MDNS: {CONF_DISABLED: False},
 | 
					            CONF_MDNS: {CONF_DISABLED: False},
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        platform=PLATFORM_ESP32,
 | 
					        platform=PLATFORM_ESP32,
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    mock_get_port_type.return_value = "NETWORK"
 | 
					 | 
				
			||||||
    mock_run_logs.return_value = 0
 | 
					    mock_run_logs.return_value = 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    args = MockArgs()
 | 
					    args = MockArgs()
 | 
				
			||||||
@@ -935,24 +1059,21 @@ def test_show_logs_api(
 | 
				
			|||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@patch("esphome.mqtt.get_esphome_device_ip")
 | 
					 | 
				
			||||||
@patch("esphome.components.api.client.run_logs")
 | 
					@patch("esphome.components.api.client.run_logs")
 | 
				
			||||||
def test_show_logs_api_with_mqtt_fallback(
 | 
					def test_show_logs_api_with_mqtt_fallback(
 | 
				
			||||||
    mock_run_logs: Mock,
 | 
					    mock_run_logs: Mock,
 | 
				
			||||||
    mock_mqtt_get_ip: Mock,
 | 
					    mock_mqtt_get_ip: Mock,
 | 
				
			||||||
    mock_get_port_type: Mock,
 | 
					 | 
				
			||||||
) -> None:
 | 
					) -> None:
 | 
				
			||||||
    """Test show_logs with API using MQTT for address resolution."""
 | 
					    """Test show_logs with API using MQTT for address resolution."""
 | 
				
			||||||
    setup_core(
 | 
					    setup_core(
 | 
				
			||||||
        config={
 | 
					        config={
 | 
				
			||||||
            "logger": {},
 | 
					            "logger": {},
 | 
				
			||||||
            "api": {},
 | 
					            CONF_API: {},
 | 
				
			||||||
            CONF_MDNS: {CONF_DISABLED: True},
 | 
					            CONF_MDNS: {CONF_DISABLED: True},
 | 
				
			||||||
            CONF_MQTT: {CONF_BROKER: "mqtt.local"},
 | 
					            CONF_MQTT: {CONF_BROKER: "mqtt.local"},
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        platform=PLATFORM_ESP32,
 | 
					        platform=PLATFORM_ESP32,
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    mock_get_port_type.return_value = "NETWORK"
 | 
					 | 
				
			||||||
    mock_run_logs.return_value = 0
 | 
					    mock_run_logs.return_value = 0
 | 
				
			||||||
    mock_mqtt_get_ip.return_value = ["192.168.1.200"]
 | 
					    mock_mqtt_get_ip.return_value = ["192.168.1.200"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -969,7 +1090,6 @@ def test_show_logs_api_with_mqtt_fallback(
 | 
				
			|||||||
@patch("esphome.mqtt.show_logs")
 | 
					@patch("esphome.mqtt.show_logs")
 | 
				
			||||||
def test_show_logs_mqtt(
 | 
					def test_show_logs_mqtt(
 | 
				
			||||||
    mock_mqtt_show_logs: Mock,
 | 
					    mock_mqtt_show_logs: Mock,
 | 
				
			||||||
    mock_get_port_type: Mock,
 | 
					 | 
				
			||||||
) -> None:
 | 
					) -> None:
 | 
				
			||||||
    """Test show_logs with MQTT."""
 | 
					    """Test show_logs with MQTT."""
 | 
				
			||||||
    setup_core(
 | 
					    setup_core(
 | 
				
			||||||
@@ -979,7 +1099,6 @@ def test_show_logs_mqtt(
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
        platform=PLATFORM_ESP32,
 | 
					        platform=PLATFORM_ESP32,
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    mock_get_port_type.return_value = "MQTT"
 | 
					 | 
				
			||||||
    mock_mqtt_show_logs.return_value = 0
 | 
					    mock_mqtt_show_logs.return_value = 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    args = MockArgs(
 | 
					    args = MockArgs(
 | 
				
			||||||
@@ -1001,7 +1120,6 @@ def test_show_logs_mqtt(
 | 
				
			|||||||
@patch("esphome.mqtt.show_logs")
 | 
					@patch("esphome.mqtt.show_logs")
 | 
				
			||||||
def test_show_logs_network_with_mqtt_only(
 | 
					def test_show_logs_network_with_mqtt_only(
 | 
				
			||||||
    mock_mqtt_show_logs: Mock,
 | 
					    mock_mqtt_show_logs: Mock,
 | 
				
			||||||
    mock_get_port_type: Mock,
 | 
					 | 
				
			||||||
) -> None:
 | 
					) -> None:
 | 
				
			||||||
    """Test show_logs with network port but only MQTT configured."""
 | 
					    """Test show_logs with network port but only MQTT configured."""
 | 
				
			||||||
    setup_core(
 | 
					    setup_core(
 | 
				
			||||||
@@ -1012,7 +1130,6 @@ def test_show_logs_network_with_mqtt_only(
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
        platform=PLATFORM_ESP32,
 | 
					        platform=PLATFORM_ESP32,
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    mock_get_port_type.return_value = "NETWORK"
 | 
					 | 
				
			||||||
    mock_mqtt_show_logs.return_value = 0
 | 
					    mock_mqtt_show_logs.return_value = 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    args = MockArgs(
 | 
					    args = MockArgs(
 | 
				
			||||||
@@ -1031,9 +1148,7 @@ def test_show_logs_network_with_mqtt_only(
 | 
				
			|||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_show_logs_no_method_configured(
 | 
					def test_show_logs_no_method_configured() -> None:
 | 
				
			||||||
    mock_get_port_type: Mock,
 | 
					 | 
				
			||||||
) -> None:
 | 
					 | 
				
			||||||
    """Test show_logs when no remote logging method is configured."""
 | 
					    """Test show_logs when no remote logging method is configured."""
 | 
				
			||||||
    setup_core(
 | 
					    setup_core(
 | 
				
			||||||
        config={
 | 
					        config={
 | 
				
			||||||
@@ -1042,7 +1157,6 @@ def test_show_logs_no_method_configured(
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
        platform=PLATFORM_ESP32,
 | 
					        platform=PLATFORM_ESP32,
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    mock_get_port_type.return_value = "NETWORK"
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    args = MockArgs()
 | 
					    args = MockArgs()
 | 
				
			||||||
    devices = ["192.168.1.100"]
 | 
					    devices = ["192.168.1.100"]
 | 
				
			||||||
@@ -1075,6 +1189,175 @@ def test_show_logs_platform_specific_handler(
 | 
				
			|||||||
    mock_module.show_logs.assert_called_once_with(config, args, devices)
 | 
					    mock_module.show_logs.assert_called_once_with(config, args, devices)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_has_mqtt_logging_no_log_topic() -> None:
 | 
				
			||||||
 | 
					    """Test has_mqtt_logging returns True when CONF_LOG_TOPIC is not in mqtt_config."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Setup MQTT config without CONF_LOG_TOPIC (defaults to enabled - this is the missing test case)
 | 
				
			||||||
 | 
					    setup_core(config={CONF_MQTT: {CONF_BROKER: "mqtt.local"}})
 | 
				
			||||||
 | 
					    assert has_mqtt_logging() is True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Setup MQTT config with CONF_LOG_TOPIC set to None (explicitly disabled)
 | 
				
			||||||
 | 
					    setup_core(config={CONF_MQTT: {CONF_BROKER: "mqtt.local", CONF_LOG_TOPIC: None}})
 | 
				
			||||||
 | 
					    assert has_mqtt_logging() is False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Setup MQTT config with CONF_LOG_TOPIC set with topic and level (explicitly enabled)
 | 
				
			||||||
 | 
					    setup_core(
 | 
				
			||||||
 | 
					        config={
 | 
				
			||||||
 | 
					            CONF_MQTT: {
 | 
				
			||||||
 | 
					                CONF_BROKER: "mqtt.local",
 | 
				
			||||||
 | 
					                CONF_LOG_TOPIC: {CONF_TOPIC: "esphome/logs", CONF_LEVEL: "DEBUG"},
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    assert has_mqtt_logging() is True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Setup MQTT config with CONF_LOG_TOPIC set but level is NONE (disabled)
 | 
				
			||||||
 | 
					    setup_core(
 | 
				
			||||||
 | 
					        config={
 | 
				
			||||||
 | 
					            CONF_MQTT: {
 | 
				
			||||||
 | 
					                CONF_BROKER: "mqtt.local",
 | 
				
			||||||
 | 
					                CONF_LOG_TOPIC: {CONF_TOPIC: "esphome/logs", CONF_LEVEL: "NONE"},
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    assert has_mqtt_logging() is False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Setup without MQTT config at all
 | 
				
			||||||
 | 
					    setup_core(config={})
 | 
				
			||||||
 | 
					    assert has_mqtt_logging() is False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_has_mqtt() -> None:
 | 
				
			||||||
 | 
					    """Test has_mqtt function."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Test with MQTT configured
 | 
				
			||||||
 | 
					    setup_core(config={CONF_MQTT: {CONF_BROKER: "mqtt.local"}})
 | 
				
			||||||
 | 
					    assert has_mqtt() is True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Test without MQTT configured
 | 
				
			||||||
 | 
					    setup_core(config={})
 | 
				
			||||||
 | 
					    assert has_mqtt() is False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Test with other components but no MQTT
 | 
				
			||||||
 | 
					    setup_core(config={CONF_API: {}, CONF_OTA: {}})
 | 
				
			||||||
 | 
					    assert has_mqtt() is False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_get_port_type() -> None:
 | 
				
			||||||
 | 
					    """Test get_port_type function."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    assert get_port_type("/dev/ttyUSB0") == "SERIAL"
 | 
				
			||||||
 | 
					    assert get_port_type("/dev/ttyACM0") == "SERIAL"
 | 
				
			||||||
 | 
					    assert get_port_type("COM1") == "SERIAL"
 | 
				
			||||||
 | 
					    assert get_port_type("COM10") == "SERIAL"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    assert get_port_type("MQTT") == "MQTT"
 | 
				
			||||||
 | 
					    assert get_port_type("MQTTIP") == "MQTTIP"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    assert get_port_type("192.168.1.100") == "NETWORK"
 | 
				
			||||||
 | 
					    assert get_port_type("esphome-device.local") == "NETWORK"
 | 
				
			||||||
 | 
					    assert get_port_type("10.0.0.1") == "NETWORK"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_has_mqtt_ip_lookup() -> None:
 | 
				
			||||||
 | 
					    """Test has_mqtt_ip_lookup function."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    CONF_DISCOVER_IP = "discover_ip"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    setup_core(config={})
 | 
				
			||||||
 | 
					    assert has_mqtt_ip_lookup() is False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    setup_core(config={CONF_MQTT: {CONF_BROKER: "mqtt.local"}})
 | 
				
			||||||
 | 
					    assert has_mqtt_ip_lookup() is True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    setup_core(config={CONF_MQTT: {CONF_BROKER: "mqtt.local", CONF_DISCOVER_IP: True}})
 | 
				
			||||||
 | 
					    assert has_mqtt_ip_lookup() is True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    setup_core(config={CONF_MQTT: {CONF_BROKER: "mqtt.local", CONF_DISCOVER_IP: False}})
 | 
				
			||||||
 | 
					    assert has_mqtt_ip_lookup() is False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_has_non_ip_address() -> None:
 | 
				
			||||||
 | 
					    """Test has_non_ip_address function."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    setup_core(address=None)
 | 
				
			||||||
 | 
					    assert has_non_ip_address() is False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    setup_core(address="192.168.1.100")
 | 
				
			||||||
 | 
					    assert has_non_ip_address() is False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    setup_core(address="10.0.0.1")
 | 
				
			||||||
 | 
					    assert has_non_ip_address() is False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    setup_core(address="esphome-device.local")
 | 
				
			||||||
 | 
					    assert has_non_ip_address() is True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    setup_core(address="my-device")
 | 
				
			||||||
 | 
					    assert has_non_ip_address() is True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_has_ip_address() -> None:
 | 
				
			||||||
 | 
					    """Test has_ip_address function."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    setup_core(address=None)
 | 
				
			||||||
 | 
					    assert has_ip_address() is False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    setup_core(address="192.168.1.100")
 | 
				
			||||||
 | 
					    assert has_ip_address() is True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    setup_core(address="10.0.0.1")
 | 
				
			||||||
 | 
					    assert has_ip_address() is True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    setup_core(address="esphome-device.local")
 | 
				
			||||||
 | 
					    assert has_ip_address() is False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    setup_core(address="my-device")
 | 
				
			||||||
 | 
					    assert has_ip_address() is False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_mqtt_get_ip() -> None:
 | 
				
			||||||
 | 
					    """Test mqtt_get_ip function."""
 | 
				
			||||||
 | 
					    config = {CONF_MQTT: {CONF_BROKER: "mqtt.local"}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    with patch("esphome.mqtt.get_esphome_device_ip") as mock_get_ip:
 | 
				
			||||||
 | 
					        mock_get_ip.return_value = ["192.168.1.100", "192.168.1.101"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        result = mqtt_get_ip(config, "user", "pass", "client-id")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        assert result == ["192.168.1.100", "192.168.1.101"]
 | 
				
			||||||
 | 
					        mock_get_ip.assert_called_once_with(config, "user", "pass", "client-id")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_has_resolvable_address() -> None:
 | 
				
			||||||
 | 
					    """Test has_resolvable_address function."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Test with mDNS enabled and hostname address
 | 
				
			||||||
 | 
					    setup_core(config={}, address="esphome-device.local")
 | 
				
			||||||
 | 
					    assert has_resolvable_address() is True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Test with mDNS disabled and hostname address
 | 
				
			||||||
 | 
					    setup_core(
 | 
				
			||||||
 | 
					        config={CONF_MDNS: {CONF_DISABLED: True}}, address="esphome-device.local"
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    assert has_resolvable_address() is False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Test with IP address (mDNS doesn't matter)
 | 
				
			||||||
 | 
					    setup_core(config={}, address="192.168.1.100")
 | 
				
			||||||
 | 
					    assert has_resolvable_address() is True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Test with IP address and mDNS disabled
 | 
				
			||||||
 | 
					    setup_core(config={CONF_MDNS: {CONF_DISABLED: True}}, address="192.168.1.100")
 | 
				
			||||||
 | 
					    assert has_resolvable_address() is True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Test with no address but mDNS enabled (can still resolve mDNS names)
 | 
				
			||||||
 | 
					    setup_core(config={}, address=None)
 | 
				
			||||||
 | 
					    assert has_resolvable_address() is True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Test with no address and mDNS disabled
 | 
				
			||||||
 | 
					    setup_core(config={CONF_MDNS: {CONF_DISABLED: True}}, address=None)
 | 
				
			||||||
 | 
					    assert has_resolvable_address() is False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_command_wizard(tmp_path: Path) -> None:
 | 
					def test_command_wizard(tmp_path: Path) -> None:
 | 
				
			||||||
    """Test command_wizard function."""
 | 
					    """Test command_wizard function."""
 | 
				
			||||||
    config_file = tmp_path / "test.yaml"
 | 
					    config_file = tmp_path / "test.yaml"
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user