[core] Clarify resolve error when a device has no network log/OTA transport (#17107)

This commit is contained in:
J. Nick Koston
2026-06-21 15:00:55 -05:00
committed by GitHub
parent c6ead57a9e
commit 921758f87d
2 changed files with 84 additions and 11 deletions

View File

@@ -268,6 +268,36 @@ def _ota_hostnames_for_default(purpose: Purpose) -> list[str]:
return _resolve_with_cache(CORE.address, purpose) return _resolve_with_cache(CORE.address, purpose)
def _unresolved_default_error(purpose: Purpose, defaults: list[str]) -> str:
"""Build the error when a default device target produced no usable host.
When the OTA default was requested and the address resolves but the config
lacks the transport the purpose needs (``api:`` for logs, an ``ota:``
platform for uploads), name that gap instead of the misleading
"could not be resolved" / set-use_address hint.
"""
if "OTA" in defaults and has_resolvable_address():
if purpose == Purpose.LOGGING and not has_api():
return (
"Cannot view logs over the network: no 'api:' component is "
"configured. Network log streaming requires the native API; add "
"an 'api:' component, enable MQTT logging, or view logs over USB."
)
if purpose == Purpose.UPLOADING and not has_ota():
return (
"Cannot upload over the network: no 'ota:' platform is "
"configured. Add an 'ota:' platform, or upload over USB."
)
if CORE.dashboard:
hint = "If you know the IP, set 'use_address' in your network config."
else:
hint = "If you know the IP, try --device <IP>"
return (
f"All specified devices {defaults} could not be resolved. "
f"Is the device connected to the network? {hint}"
)
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,
@@ -317,14 +347,7 @@ def choose_upload_log_host(
else: else:
resolved.append(device) resolved.append(device)
if not resolved: if not resolved:
if CORE.dashboard: raise EsphomeError(_unresolved_default_error(purpose, defaults))
hint = "If you know the IP, set 'use_address' in your network config."
else:
hint = "If you know the IP, try --device <IP>"
raise EsphomeError(
f"All specified devices {defaults} could not be resolved. "
f"Is the device connected to the network? {hint}"
)
return resolved return resolved
# No devices specified, show interactive chooser # No devices specified, show interactive chooser

View File

@@ -24,6 +24,7 @@ from esphome.__main__ import (
_make_crystal_freq_callback, _make_crystal_freq_callback,
_redact_with_legacy_fallback, _redact_with_legacy_fallback,
_resolve_network_devices, _resolve_network_devices,
_unresolved_default_error,
_validate_bootloader_binary, _validate_bootloader_binary,
_validate_partition_table_binary, _validate_partition_table_binary,
choose_upload_log_host, choose_upload_log_host,
@@ -713,9 +714,7 @@ def test_choose_upload_log_host_with_ota_device_with_api_config() -> None:
"""Test OTA device when API is configured (no upload without OTA in config).""" """Test OTA device when API is configured (no upload without OTA in config)."""
setup_core(config={CONF_API: {}}, address="192.168.1.100") setup_core(config={CONF_API: {}}, address="192.168.1.100")
with pytest.raises( with pytest.raises(EsphomeError, match="no 'ota:' platform is configured"):
EsphomeError, match="All specified devices .* could not be resolved"
):
choose_upload_log_host( choose_upload_log_host(
default="OTA", default="OTA",
check_default=None, check_default=None,
@@ -735,6 +734,57 @@ def test_choose_upload_log_host_with_ota_device_with_api_config_logging() -> Non
assert result == ["192.168.1.100"] assert result == ["192.168.1.100"]
def test_choose_upload_log_host_logging_without_api_reports_missing_api() -> None:
"""A resolvable device with only ota: fails logs with a missing-api message."""
setup_core(
config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}]}, address="192.168.1.100"
)
with pytest.raises(EsphomeError, match="no 'api:' component is configured"):
choose_upload_log_host(
default="OTA",
check_default=None,
purpose=Purpose.LOGGING,
)
def test_choose_upload_log_host_logging_no_transport_reports_missing_api() -> None:
"""A resolvable device with neither api: nor MQTT logging fails clearly."""
setup_core(address="192.168.1.100")
with pytest.raises(EsphomeError, match="no 'api:' component is configured"):
choose_upload_log_host(
default="OTA",
check_default=None,
purpose=Purpose.LOGGING,
)
def test_unresolved_default_error_unresolvable_keeps_dashboard_hint() -> None:
"""A .local host with mDNS disabled and no cache keeps the dashboard hint."""
setup_core(
config={CONF_API: {}, CONF_MDNS: {CONF_DISABLED: True}},
address="esp32-a1s.local",
)
CORE.dashboard = True
msg = _unresolved_default_error(Purpose.LOGGING, ["OTA"])
assert "could not be resolved" in msg
assert "set 'use_address'" in msg
def test_unresolved_default_error_upload_with_ota_is_generic() -> None:
"""With ota: present the upload error stays generic, not transport-specific."""
setup_core(
config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}]}, address="192.168.1.100"
)
CORE.dashboard = False
msg = _unresolved_default_error(Purpose.UPLOADING, ["OTA"])
assert "could not be resolved" in msg
assert "try --device <IP>" in msg
@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."""