Connecting to an ESPHome serial port¶
ESPHome can expose UARTs over its native API using the serial_proxy component. serialx can connect to ESPHome serial proxies with the esphome://host:port?port_name=Name URL scheme and expose a complete serial port implementation, with flow control, modem pin control, and buffer flushing.
Install the ESPHome extras first, since aioesphomeapi is not installed by default:
pip install 'serialx[esphome]'
Example script¶
Open a connection straight from the URL. serialx will create and tear down an ESPHome API client at the end of a session:
import asyncio
import serialx
async def main() -> None:
reader, writer = await serialx.open_serial_connection(
url="esphome://192.168.1.42:6053/?port_name=Zigbee",
key="base64-psk-here",
baudrate=115200,
)
try:
writer.write(b"ping\n")
await writer.drain()
data = await reader.readexactly(5)
print(data)
finally:
writer.close()
await writer.wait_closed()
if __name__ == "__main__":
asyncio.run(main())
Use password= instead of noise_psk= in the query string if the device uses legacy API password authentication. If the device does not use any authentication, you can omit this keyword argument.
A URL can carry the key, noise_psk or password in its query string. Pass it through serialx.redact_uri() before you log it or put it in an error message:
_LOGGER.info("Connected to %s", serialx.redact_uri(url))
Proxy modes¶
A serial proxy streams an opaque byte stream by default. A port configured with a protocol-aware tap can instead be switched into mode=protocol, letting the device handle that protocol’s timing-sensitive work on your behalf. It is still presented locally as a plain serial port:
reader, writer = await serialx.open_serial_connection(
url="esphome://192.168.1.42:6053/?port_name=Zigbee&mode=protocol",
baudrate=115200,
)
The mode is applied before the proxy starts streaming. Only raw (the default) and protocol are accepted; any other value is rejected. Which protocol protocol engages depends on the tap the device’s configuration gives that port.
Asking for protocol on a port with no tap, or of a device too old to have modes, raises serialx.UnsupportedSetting instead of opening the port. A client that asked for the mode is not going to do that protocol’s work itself, so plain bytes are not a fallback it can use.
Reusing an existing API client¶
If your application already holds an aioesphomeapi.APIClient, pass it to the transport directly to avoid opening a second connection:
import asyncio
import serialx
from aioesphomeapi import APIClient
from serialx.platforms.serial_esphome import ESPHomeSerialTransport
async def main() -> None:
api = APIClient(
address="192.168.1.42",
port=6053,
noise_psk="base64-psk-here",
password=None,
)
await api.connect(login=True)
reader, writer = await serialx.open_serial_connection(
url=None,
transport_cls=ESPHomeSerialTransport,
api=api,
port_name="Zigbee",
baudrate=115200,
)
try:
data = await reader.readexactly(5)
print(data)
finally:
writer.close()
await writer.wait_closed()
await api.disconnect()
if __name__ == "__main__":
asyncio.run(main())
When the API client is passed in externally, serialx does not disconnect it on close, it only unsubscribes from the specific serial port.