mirror of
https://github.com/esphome/esphome.git
synced 2026-06-01 01:19:45 +08:00
[core] Add stable esphome.upload_targets module for port classification (#16346)
This commit is contained in:
+1
-29
@@ -64,6 +64,7 @@ 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
|
||||||
|
from esphome.upload_targets import PortType, get_port_type
|
||||||
from esphome.util import (
|
from esphome.util import (
|
||||||
PICOTOOL_PACKAGE,
|
PICOTOOL_PACKAGE,
|
||||||
FlashImage,
|
FlashImage,
|
||||||
@@ -194,14 +195,6 @@ class Purpose(StrEnum):
|
|||||||
LOGGING = "logging"
|
LOGGING = "logging"
|
||||||
|
|
||||||
|
|
||||||
class PortType(StrEnum):
|
|
||||||
SERIAL = "SERIAL"
|
|
||||||
NETWORK = "NETWORK"
|
|
||||||
MQTT = "MQTT"
|
|
||||||
MQTTIP = "MQTTIP"
|
|
||||||
BOOTSEL = "BOOTSEL"
|
|
||||||
|
|
||||||
|
|
||||||
# Magic MQTT port types that require special handling
|
# Magic MQTT port types that require special handling
|
||||||
_MQTT_PORT_TYPES = frozenset({PortType.MQTT, PortType.MQTTIP})
|
_MQTT_PORT_TYPES = frozenset({PortType.MQTT, PortType.MQTTIP})
|
||||||
|
|
||||||
@@ -597,27 +590,6 @@ def _resolve_network_devices(
|
|||||||
return network_devices
|
return network_devices
|
||||||
|
|
||||||
|
|
||||||
def get_port_type(port: str) -> PortType:
|
|
||||||
"""Determine the type of port/device identifier.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
PortType.SERIAL for serial ports (/dev/ttyUSB0, COM1, etc.)
|
|
||||||
PortType.BOOTSEL for RP2040 BOOTSEL upload via picotool
|
|
||||||
PortType.MQTT for MQTT logging
|
|
||||||
PortType.MQTTIP for MQTT IP lookup
|
|
||||||
PortType.NETWORK for IP addresses, hostnames, or mDNS names
|
|
||||||
"""
|
|
||||||
if port == "BOOTSEL":
|
|
||||||
return PortType.BOOTSEL
|
|
||||||
if port.startswith("/") or port.startswith("COM"):
|
|
||||||
return PortType.SERIAL
|
|
||||||
if port == "MQTT":
|
|
||||||
return PortType.MQTT
|
|
||||||
if port == "MQTTIP":
|
|
||||||
return PortType.MQTTIP
|
|
||||||
return PortType.NETWORK
|
|
||||||
|
|
||||||
|
|
||||||
def run_miniterm(config: ConfigType, port: str, args) -> int:
|
def run_miniterm(config: ConfigType, port: str, args) -> int:
|
||||||
from aioesphomeapi import LogParser
|
from aioesphomeapi import LogParser
|
||||||
import serial
|
import serial
|
||||||
|
|||||||
@@ -405,11 +405,12 @@ def _upload_using_platformio(
|
|||||||
|
|
||||||
|
|
||||||
def upload_program(config: ConfigType, args, host: str) -> bool:
|
def upload_program(config: ConfigType, args, host: str) -> bool:
|
||||||
from esphome.__main__ import check_permissions, get_port_type
|
from esphome.__main__ import check_permissions
|
||||||
|
from esphome.upload_targets import PortType, get_port_type
|
||||||
|
|
||||||
mcumgr_device: str | None = None
|
mcumgr_device: str | None = None
|
||||||
|
|
||||||
if get_port_type(host) == "SERIAL":
|
if get_port_type(host) == PortType.SERIAL:
|
||||||
check_permissions(host)
|
check_permissions(host)
|
||||||
if zephyr_data()[KEY_BOOTLOADER] == BOOTLOADER_MCUBOOT:
|
if zephyr_data()[KEY_BOOTLOADER] == BOOTLOADER_MCUBOOT:
|
||||||
mcumgr_device = host
|
mcumgr_device = host
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
"""Stable classification of ``--device`` / port strings.
|
||||||
|
|
||||||
|
External tooling (the device-builder dashboard at
|
||||||
|
esphome/device-builder, and other consumers) needs to decide whether
|
||||||
|
a user-supplied port string names a local serial device, an OTA
|
||||||
|
network target, an MQTT magic string, or an RP2040 BOOTSEL upload.
|
||||||
|
|
||||||
|
This module is the single stable home for that classification. The
|
||||||
|
upstream CLI (``esphome.__main__``) re-exports ``PortType`` and
|
||||||
|
``get_port_type`` from here for its own use; external callers should
|
||||||
|
import directly from ``esphome.upload_targets`` so the surface stays
|
||||||
|
stable across releases (``esphome/__main__`` is a CLI entrypoint and
|
||||||
|
not a stable import path).
|
||||||
|
|
||||||
|
Please keep ``PortType`` member names / values and the
|
||||||
|
``get_port_type`` signature stable — see the docstrings on each for
|
||||||
|
the contract.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from esphome.enum import StrEnum
|
||||||
|
|
||||||
|
|
||||||
|
class PortType(StrEnum):
|
||||||
|
"""Port classification returned by :func:`get_port_type`.
|
||||||
|
|
||||||
|
Used by device-builder (esphome/device-builder) and other
|
||||||
|
external tooling to route a user-supplied ``--device`` value to
|
||||||
|
the right upload / log path. Member names and string values are
|
||||||
|
part of the stable surface — adding new members is fine, but
|
||||||
|
existing names / values must not be renamed or changed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
SERIAL = "SERIAL"
|
||||||
|
NETWORK = "NETWORK"
|
||||||
|
MQTT = "MQTT"
|
||||||
|
MQTTIP = "MQTTIP"
|
||||||
|
BOOTSEL = "BOOTSEL"
|
||||||
|
|
||||||
|
|
||||||
|
def get_port_type(port: str) -> PortType:
|
||||||
|
"""Determine the type of port/device identifier.
|
||||||
|
|
||||||
|
Used by device-builder (esphome/device-builder)'s dashboard to
|
||||||
|
decide whether a user-supplied ``--device`` value names a local
|
||||||
|
serial port (must build / flash locally), an OTA network target
|
||||||
|
(eligible for remote builds), an MQTT magic string, or an RP2040
|
||||||
|
BOOTSEL upload. Please keep the signature stable.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PortType.SERIAL for serial ports (/dev/ttyUSB0, COM1, etc.)
|
||||||
|
PortType.BOOTSEL for RP2040 BOOTSEL upload via picotool
|
||||||
|
PortType.MQTT for MQTT logging
|
||||||
|
PortType.MQTTIP for MQTT IP lookup
|
||||||
|
PortType.NETWORK for IP addresses, hostnames, or mDNS names
|
||||||
|
"""
|
||||||
|
if port == "BOOTSEL":
|
||||||
|
return PortType.BOOTSEL
|
||||||
|
if port.startswith("/") or port.startswith("COM"):
|
||||||
|
return PortType.SERIAL
|
||||||
|
if port == "MQTT":
|
||||||
|
return PortType.MQTT
|
||||||
|
if port == "MQTTIP":
|
||||||
|
return PortType.MQTTIP
|
||||||
|
return PortType.NETWORK
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
"""Tests for the stable upload-targets classification helpers."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from esphome.upload_targets import PortType, get_port_type
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"port",
|
||||||
|
[
|
||||||
|
"/dev/ttyUSB0",
|
||||||
|
"/dev/ttyACM0",
|
||||||
|
"/dev/cu.usbserial-1410",
|
||||||
|
"/dev/tty.usbmodem1101",
|
||||||
|
"COM1",
|
||||||
|
"COM23",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_get_port_type_serial(port: str) -> None:
|
||||||
|
"""Local serial devices classify as SERIAL."""
|
||||||
|
assert get_port_type(port) is PortType.SERIAL
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_port_type_bootsel() -> None:
|
||||||
|
"""``BOOTSEL`` magic string classifies as BOOTSEL."""
|
||||||
|
assert get_port_type("BOOTSEL") is PortType.BOOTSEL
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_port_type_mqtt() -> None:
|
||||||
|
"""``MQTT`` magic string classifies as MQTT."""
|
||||||
|
assert get_port_type("MQTT") is PortType.MQTT
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_port_type_mqttip() -> None:
|
||||||
|
"""``MQTTIP`` magic string classifies as MQTTIP."""
|
||||||
|
assert get_port_type("MQTTIP") is PortType.MQTTIP
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"port",
|
||||||
|
[
|
||||||
|
"192.168.1.10",
|
||||||
|
"fe80::1",
|
||||||
|
"device.local",
|
||||||
|
"my-esp.example.com",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_get_port_type_network(port: str) -> None:
|
||||||
|
"""IP addresses, mDNS, and hostnames classify as NETWORK."""
|
||||||
|
assert get_port_type(port) is PortType.NETWORK
|
||||||
|
|
||||||
|
|
||||||
|
def test_port_type_values_are_stable() -> None:
|
||||||
|
"""Member values are part of the stable surface.
|
||||||
|
|
||||||
|
External tooling (device-builder, etc.) may compare against the
|
||||||
|
string values directly. Renaming or changing these breaks
|
||||||
|
downstream consumers — guard against accidental edits.
|
||||||
|
"""
|
||||||
|
assert PortType.SERIAL.value == "SERIAL"
|
||||||
|
assert PortType.NETWORK.value == "NETWORK"
|
||||||
|
assert PortType.MQTT.value == "MQTT"
|
||||||
|
assert PortType.MQTTIP.value == "MQTTIP"
|
||||||
|
assert PortType.BOOTSEL.value == "BOOTSEL"
|
||||||
|
|
||||||
|
|
||||||
|
def test_main_re_exports_for_backwards_compat() -> None:
|
||||||
|
"""``esphome.__main__`` re-exports the stable surface.
|
||||||
|
|
||||||
|
The CLI entry point pre-dated the stable module and existing
|
||||||
|
internal callers (and any third-party code that snuck in via
|
||||||
|
``__main__``) still import from there. The re-export must
|
||||||
|
resolve to the same objects.
|
||||||
|
"""
|
||||||
|
from esphome.__main__ import (
|
||||||
|
PortType as MainPortType,
|
||||||
|
get_port_type as main_get_port_type,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert MainPortType is PortType
|
||||||
|
assert main_get_port_type is get_port_type
|
||||||
Reference in New Issue
Block a user