Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion aioesphomeapi/api.proto
Original file line number Diff line number Diff line change
Expand Up @@ -2622,6 +2622,21 @@ message ZWaveProxyRequest {
bytes data = 2;
}

enum ZWaveProxyStatus {
ZWAVE_PROXY_STATUS_OK = 0; // Request completed successfully
ZWAVE_PROXY_STATUS_IN_USE = 1; // Denied: another client is already subscribed
}

// Acknowledges a ZWaveProxyRequest (subscribe/unsubscribe). Sent since API 1.16.
message ZWaveProxyRequestResponse {
option (id) = 151;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_ZWAVE_PROXY";

ZWaveProxyRequestType type = 1; // Which request type this responds to
ZWaveProxyStatus status = 2; // Result status
}

// ==================== INFRARED ====================
// Note: Feature and capability flag enums are defined in
// esphome/components/infrared/infrared.h
Expand Down Expand Up @@ -2771,6 +2786,10 @@ enum SerialProxyRequestType {
SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE = 0; // Subscribe to receive data from this serial proxy instance
SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE = 1; // Unsubscribe from this serial proxy instance
SERIAL_PROXY_REQUEST_TYPE_FLUSH = 2; // Flush the serial port (block until all TX data is sent)
// Values below are only valid in SerialProxyRequestResponse.type, identifying which
// operation is being acknowledged; they must not be sent in SerialProxyRequest.type.
SERIAL_PROXY_REQUEST_TYPE_CONFIGURE = 3; // Acknowledges a SerialProxyConfigureRequest
SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS = 4; // Acknowledges a SerialProxySetModemPinsRequest
}

enum SerialProxyStatus {
Expand All @@ -2779,6 +2798,8 @@ enum SerialProxyStatus {
SERIAL_PROXY_STATUS_ERROR = 2; // Driver or hardware error
SERIAL_PROXY_STATUS_TIMEOUT = 3; // Timed out before TX completed
SERIAL_PROXY_STATUS_NOT_SUPPORTED = 4; // Request type not supported by this instance
SERIAL_PROXY_STATUS_PORT_IN_USE = 5; // Denied: another client holds the port
SERIAL_PROXY_STATUS_INVALID_ARGUMENT = 6; // Invalid instance index or parameter value
}

// Generic request message for simple serial proxy operations
Expand All @@ -2791,7 +2812,9 @@ message SerialProxyRequest {
SerialProxyRequestType type = 2; // Request type
}

// Response to a SerialProxyRequest (e.g. flush completion or failure)
// Acknowledges a serial proxy operation (subscribe, unsubscribe, flush,
// configure, or modem pin change). Sent since API 1.16; the type field
// identifies which operation is being acknowledged.
message SerialProxyRequestResponse {
option (id) = 147;
option (source) = SOURCE_SERVER;
Expand Down
248 changes: 127 additions & 121 deletions aioesphomeapi/api_pb2.py

Large diffs are not rendered by default.

193 changes: 171 additions & 22 deletions aioesphomeapi/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
VoiceAssistantTimerEventResponse,
WaterHeaterCommandRequest,
ZWaveProxyRequest,
ZWaveProxyRequestResponse,
)
from .client_base import (
APIClientBase,
Expand Down Expand Up @@ -190,6 +191,8 @@
WaterHeaterStateFlag,
ZWaveProxyCapabilities as ZWaveProxyCapabilitiesModel,
ZWaveProxyRequest as ZWaveProxyRequestModel,
ZWaveProxyRequestResponse as ZWaveProxyRequestResponseModel,
ZWaveProxyRequestType,
message_types_to_names,
)
from .model_conversions import (
Expand Down Expand Up @@ -259,6 +262,34 @@ def _validate_connection_params(
# API version 1.14+ may omit object_id to reduce protocol overhead
MIN_VERSION_OBJECT_ID_OPTIONAL = APIVersion(1, 14)

# API version 1.16+ acknowledges proxy subscribe and port configuration requests
MIN_VERSION_PROXY_ACK = APIVersion(1, 16)


def _make_serial_proxy_configure_request(
instance: int,
baudrate: int,
flow_control: bool,
parity: SerialProxyParity,
stop_bits: int,
data_size: int,
) -> SerialProxyConfigureRequest:
"""Validate parameters and build a SerialProxyConfigureRequest."""
if not 1 <= stop_bits <= 2:
msg = f"stop_bits must be 1 or 2, got {stop_bits}"
raise ValueError(msg)
if not 5 <= data_size <= 8:
msg = f"data_size must be 5-8, got {data_size}"
raise ValueError(msg)
return SerialProxyConfigureRequest(
instance=instance,
baudrate=baudrate,
flow_control=flow_control,
parity=parity,
stop_bits=stop_bits,
data_size=data_size,
)


def _fill_object_ids_if_needed(
api_version: APIVersion,
Expand Down Expand Up @@ -638,6 +669,10 @@ def radio_frequency_transmit_raw_timings(
req.timings.extend(timings)
self._get_connection().send_message(req)

def _supports_proxy_ack(self) -> bool:
api_version = self.api_version
return api_version is None or api_version >= MIN_VERSION_PROXY_ACK

def serial_proxy_configure(
self,
instance: int,
Expand All @@ -649,23 +684,38 @@ def serial_proxy_configure(
data_size: int = 8,
) -> None:
"""Configure UART parameters for a serial proxy instance."""
if not 1 <= stop_bits <= 2:
msg = f"stop_bits must be 1 or 2, got {stop_bits}"
raise ValueError(msg)
if not 5 <= data_size <= 8:
msg = f"data_size must be 5-8, got {data_size}"
raise ValueError(msg)
self._get_connection().send_message(
SerialProxyConfigureRequest(
instance=instance,
baudrate=baudrate,
flow_control=flow_control,
parity=parity,
stop_bits=stop_bits,
data_size=data_size,
_make_serial_proxy_configure_request(
instance, baudrate, flow_control, parity, stop_bits, data_size
)
)

async def serial_proxy_configure_await_response(
self,
instance: int,
baudrate: int,
*,
flow_control: bool = False,
parity: SerialProxyParity = SerialProxyParity.NONE,
stop_bits: int = 1,
data_size: int = 8,
timeout: float = 10.0,
) -> SerialProxyRequestResponseModel | None:
"""Configure UART parameters and await the device acknowledgement.

Returns None when the device is too old to acknowledge (API < 1.16);
the request is still sent.
"""
req = _make_serial_proxy_configure_request(
instance, baudrate, flow_control, parity, stop_bits, data_size
)
if not self._supports_proxy_ack():
self._get_connection().send_message(req)
return None
return await self._await_serial_proxy_response(
req, instance, SerialProxyRequestType.CONFIGURE, timeout
)

def serial_proxy_write(
self,
instance: int,
Expand Down Expand Up @@ -706,6 +756,26 @@ def serial_proxy_set_modem_pins(
)
)

async def serial_proxy_set_modem_pins_await_response(
self,
instance: int,
*,
line_states: int = 0,
timeout: float = 10.0,
) -> SerialProxyRequestResponseModel | None:
"""Set modem control pin states and await the device acknowledgement.

Returns None when the device is too old to acknowledge (API < 1.16);
the request is still sent.
"""
req = SerialProxySetModemPinsRequest(instance=instance, line_states=line_states)
if not self._supports_proxy_ack():
self._get_connection().send_message(req)
return None
return await self._await_serial_proxy_response(
req, instance, SerialProxyRequestType.SET_MODEM_PINS, timeout
)

async def serial_proxy_get_modem_pins(
self,
instance: int,
Expand Down Expand Up @@ -758,17 +828,17 @@ def serial_proxy_unsubscribe(
)
)

async def _send_serial_proxy_request_await_response(
async def _await_serial_proxy_response(
self,
req: message.Message,
instance: int,
request_type: SerialProxyRequestType,
timeout: float = 10.0,
response_type: SerialProxyRequestType,
timeout: float,
) -> SerialProxyRequestResponseModel:
"""Send a serial proxy request and await its matching response."""
req = SerialProxyRequest(instance=instance, type=request_type)
"""Send a serial proxy message and await its matching acknowledgement."""

def is_matching_response(msg: SerialProxyRequestResponse) -> bool:
return bool(msg.instance == instance and msg.type == request_type)
return bool(msg.instance == instance and msg.type == response_type)

[resp] = await self._get_connection().send_messages_await_response_complex(
(req,),
Expand All @@ -779,11 +849,30 @@ def is_matching_response(msg: SerialProxyRequestResponse) -> bool:
)
return SerialProxyRequestResponseModel.from_pb(resp)

async def _send_serial_proxy_request_await_response(
self,
instance: int,
request_type: SerialProxyRequestType,
timeout: float = 10.0,
) -> SerialProxyRequestResponseModel | None:
"""Send a serial proxy request and await its matching response.

Returns None when the device is too old to acknowledge (API < 1.16);
the request is still sent.
"""
req = SerialProxyRequest(instance=instance, type=request_type)
if not self._supports_proxy_ack():
self._get_connection().send_message(req)
return None
return await self._await_serial_proxy_response(
req, instance, request_type, timeout
)

async def serial_proxy_subscribe_await_response(
self,
instance: int,
timeout: float = 10.0,
) -> SerialProxyRequestResponseModel:
) -> SerialProxyRequestResponseModel | None:
"""Subscribe and await confirmation from the serial proxy instance."""
return await self._send_serial_proxy_request_await_response(
instance, SerialProxyRequestType.SUBSCRIBE, timeout
Expand All @@ -793,19 +882,79 @@ async def serial_proxy_unsubscribe_await_response(
self,
instance: int,
timeout: float = 10.0,
) -> SerialProxyRequestResponseModel:
) -> SerialProxyRequestResponseModel | None:
"""Unsubscribe and await confirmation from the serial proxy instance."""
return await self._send_serial_proxy_request_await_response(
instance, SerialProxyRequestType.UNSUBSCRIBE, timeout
)

def zwave_proxy_subscribe(self) -> None:
"""Subscribe to receive frames from the Z-Wave proxy."""
self._get_connection().send_message(
ZWaveProxyRequest(type=ZWaveProxyRequestType.SUBSCRIBE)
)

def zwave_proxy_unsubscribe(self) -> None:
"""Unsubscribe from the Z-Wave proxy."""
self._get_connection().send_message(
ZWaveProxyRequest(type=ZWaveProxyRequestType.UNSUBSCRIBE)
)

async def _send_zwave_proxy_request_await_response(
self,
request_type: ZWaveProxyRequestType,
timeout: float = 10.0,
) -> ZWaveProxyRequestResponseModel | None:
"""Send a Z-Wave proxy request and await its matching response.

Returns None when the device is too old to acknowledge (API < 1.16);
the request is still sent.
"""
req = ZWaveProxyRequest(type=request_type)
if not self._supports_proxy_ack():
self._get_connection().send_message(req)
return None

def is_matching_response(msg: ZWaveProxyRequestResponse) -> bool:
return bool(msg.type == request_type)

[resp] = await self._get_connection().send_messages_await_response_complex(
(req,),
is_matching_response,
is_matching_response,
(ZWaveProxyRequestResponse,),
timeout,
)
return ZWaveProxyRequestResponseModel.from_pb(resp)

async def zwave_proxy_subscribe_await_response(
self,
timeout: float = 10.0,
) -> ZWaveProxyRequestResponseModel | None:
"""Subscribe and await confirmation from the Z-Wave proxy."""
return await self._send_zwave_proxy_request_await_response(
ZWaveProxyRequestType.SUBSCRIBE, timeout
)

async def zwave_proxy_unsubscribe_await_response(
self,
timeout: float = 10.0,
) -> ZWaveProxyRequestResponseModel | None:
"""Unsubscribe and await confirmation from the Z-Wave proxy."""
return await self._send_zwave_proxy_request_await_response(
ZWaveProxyRequestType.UNSUBSCRIBE, timeout
)

async def serial_proxy_flush(
self,
instance: int,
timeout: float = 10.0,
) -> SerialProxyRequestResponseModel:
"""Flush the serial port and await confirmation."""
return await self._send_serial_proxy_request_await_response(
# Flush has been acknowledged since the feature was introduced,
# so it is not gated on MIN_VERSION_PROXY_ACK
return await self._await_serial_proxy_response(
SerialProxyRequest(instance=instance, type=SerialProxyRequestType.FLUSH),
instance,
SerialProxyRequestType.FLUSH,
timeout,
Expand Down
2 changes: 2 additions & 0 deletions aioesphomeapi/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@
WaterHeaterStateResponse,
ZWaveProxyFrame,
ZWaveProxyRequest,
ZWaveProxyRequestResponse,
)

TWO_CHAR = re.compile(r".{2}")
Expand Down Expand Up @@ -557,6 +558,7 @@ def __init__(self, address: int, error: int) -> None:
148: ListEntitiesRadioFrequencyResponse,
149: DeviceCapabilitiesRequest,
150: DeviceCapabilitiesResponse,
151: ZWaveProxyRequestResponse,
}

MESSAGE_NUMBER_TO_PROTO = tuple(MESSAGE_TYPE_TO_PROTO.values())
Loading
Loading