Skip to content
Closed

mmm #1502

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
e22ca6a
Update URLs (#1472)
smarthome-10 Jan 3, 2026
995e615
Bump version to 43.9.2
github-actions[bot] Jan 3, 2026
9cd5a57
Sync api.proto from esphome (#1473)
bdraco Jan 3, 2026
0089186
Bump version to 43.10.0
github-actions[bot] Jan 3, 2026
6116bd1
Adding API Password deprecation information (#1382)
generalmat82 Jan 3, 2026
82f4921
Update readme to show how to regenerate protobuf (#1474)
bdraco Jan 3, 2026
cfc1882
Add missing water_heater entity command mappings (#1476)
bdraco Jan 4, 2026
98919c5
Bump version to 43.10.1
github-actions[bot] Jan 4, 2026
be736f4
Bump pypa/cibuildwheel from 3.3.0 to 3.3.1 (#1478)
dependabot[bot] Jan 9, 2026
3b19c87
Bump version to 43.10.2
github-actions[bot] Jan 9, 2026
e0427cd
Add packed buffer support to protobuf (#1480)
kbx81 Jan 10, 2026
d27ba5a
Bump ruff from 0.14.10 to 0.14.11 (#1479)
dependabot[bot] Jan 10, 2026
eb5a558
Bump ruff to 0.14.11 in pre-commit (#1481)
bdraco Jan 11, 2026
7f66c28
Bump version to 43.11.0
github-actions[bot] Jan 11, 2026
c10d9b0
[infrared] New component/entity type (#1477)
kbx81 Jan 11, 2026
a07ad4e
Bump version to 43.12.0
github-actions[bot] Jan 11, 2026
e1dd002
Sync api.proto from ESPHome (#1482)
bdraco Jan 12, 2026
85ee1e2
Bump version to 43.13.0
github-actions[bot] Jan 12, 2026
b062b4b
Add bluetooth_gatt_stop_notify and auto-cleanup notify callbacks on d…
bdraco Jan 25, 2026
e00b237
Bump ruff from 0.14.11 to 0.14.14 (#1485)
dependabot[bot] Jan 25, 2026
5717d4a
[pre-commit.ci] pre-commit autoupdate (#1484)
pre-commit-ci[bot] Jan 25, 2026
284b33b
Bump version to 43.14.0
github-actions[bot] Jan 25, 2026
68b7fef
Bump docker/login-action from 3.6.0 to 3.7.0 (#1489)
dependabot[bot] Jan 30, 2026
a176960
[pre-commit.ci] pre-commit autoupdate (#1488)
pre-commit-ci[bot] Jan 30, 2026
b4cb5ea
Bump version to 43.15.0
github-actions[bot] Jan 30, 2026
ae636b3
[pre-commit.ci] pre-commit autoupdate (#1491)
pre-commit-ci[bot] Feb 2, 2026
4097724
Bump ruff from 0.14.14 to 0.15.0 (#1492)
dependabot[bot] Feb 9, 2026
a821e72
Bump pytest-codspeed from 4.2.0 to 4.3.0 (#1494)
dependabot[bot] Feb 10, 2026
7167351
Add WaterHeaterFeature class for ESPHome (#1493)
tronikos Feb 10, 2026
575651e
Bump version to 44.0.0
github-actions[bot] Feb 10, 2026
05fc8df
Bump docker/build-push-action from 6.18.0 to 6.19.2 (#1496)
dependabot[bot] Feb 12, 2026
19f3a47
Bump version to 44.1.0
github-actions[bot] Feb 12, 2026
538013a
Bump ruff from 0.15.0 to 0.15.1 (#1497)
dependabot[bot] Feb 13, 2026
efc667b
Merge branch 'sync_with_esphome'
imonshome77 Feb 20, 2026
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
6 changes: 3 additions & 3 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@ jobs:
uses: actions/checkout@v6
-
name: Log in to docker hub
uses: docker/login-action@v3.6.0
uses: docker/login-action@v3.7.0
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASSWORD }}
-
name: Log in to the GitHub container registry
uses: docker/login-action@v3.6.0
uses: docker/login-action@v3.7.0
with:
registry: ghcr.io
username: ${{ github.actor }}
Expand All @@ -47,7 +47,7 @@ jobs:
uses: docker/setup-buildx-action@v3.12.0
-
name: Build and Push
uses: docker/build-push-action@v6.18.0
uses: docker/build-push-action@v6.19.2
with:
context: .
tags: |
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ jobs:
echo "CIBW_BUILD=${{ matrix.pyver }}*" >> $GITHUB_ENV
fi
- name: Build wheels
uses: pypa/cibuildwheel@v3.3.0
uses: pypa/cibuildwheel@v3.3.1
env:
CIBW_SKIP: cp36-* cp37-* cp38-* cp39-* cp310-* pp* ${{ matrix.musl == 'musllinux' && '*manylinux*' || '*musllinux*' }}
CIBW_BEFORE_ALL_LINUX: apt-get install -y gcc || yum install -y gcc || apk add gcc
Expand Down
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ repos:
- id: pyupgrade
args: [--py311-plus]
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.10
rev: v0.15.0
hooks:
- id: ruff
args: [--fix]
Expand All @@ -29,7 +29,7 @@ repos:
- --keep-updates
files: ^(aioesphomeapi)/.+\.py$
- repo: https://github.com/MarcoGorelli/cython-lint
rev: v0.18.1
rev: v0.19.0
hooks:
- id: cython-lint
- id: double-quote-cython-strings
Expand Down
54 changes: 40 additions & 14 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ aioesphomeapi
=============

.. image:: https://github.com/esphome/aioesphomeapi/workflows/CI/badge.svg
:target: https://github.com/esphome/aioesphomeapi?query=workflow%3ACI+branch%3Amain
:target: https://github.com/esphome/aioesphomeapi/actions/workflows/ci.yml?query=branch%3Amain

.. image:: https://img.shields.io/pypi/v/aioesphomeapi.svg
:target: https://pypi.python.org/pypi/aioesphomeapi
:target: https://pypi.org/project/aioesphomeapi/

.. image:: https://codecov.io/gh/esphome/aioesphomeapi/branch/main/graph/badge.svg
:target: https://app.codecov.io/gh/esphome/aioesphomeapi/tree/main
Expand All @@ -18,7 +18,7 @@ aioesphomeapi
Installation
------------

The module is available from the `Python Package Index <https://pypi.python.org/pypi>`_.
The module is available from the `Python Package Index <https://pypi.org/>`_.

.. code:: bash

Expand All @@ -33,15 +33,32 @@ Building the extension can be forcefully disabled by setting the environment var
Usage
-----

It's required that you enable the `Native API <https://esphome.io/components/api.html>`_ component for the device.
It's required that you enable the `Native API <https://esphome.io/components/api/>`_ component for the device.

.. code:: yaml

# Example configuration entry
api:
password: 'MyPassword'

Check the output to get the local address of the device or use the ``name:``under ``esphome:`` from the device configuration.
For secure communication, use encryption (recommended):

.. code:: yaml

api:
encryption:
key: !secret api_encryption_key

Generate an encryption key with ``openssl rand -base64 32`` or visit https://esphome.io/components/api/

**Note:** Password authentication was removed in ESPHome 2026.1.0. Encryption is optional but recommended for security.

To connect to older devices still using password authentication:

.. code:: python

api = aioesphomeapi.APIClient("device.local", 6053, password="MyPassword")

Check the output to get the local address of the device or use the ``name:`` under ``esphome:`` from the device configuration.

.. code:: bash

Expand All @@ -60,7 +77,11 @@ The sample code below will connect to the device and retrieve details.
"""Connect to an ESPHome device and get details."""

# Establish connection
api = aioesphomeapi.APIClient("api_test.local", 6053, "MyPassword")
api = aioesphomeapi.APIClient(
"api_test.local",
6053,
noise_psk="YOUR_ENCRYPTION_KEY", # Remove if not using encryption
)
await api.connect(login=True)

# Get API version of the device's firmware
Expand All @@ -86,16 +107,19 @@ Subscribe to state changes of an ESPHome device.

async def main():
"""Connect to an ESPHome device and wait for state changes."""
cli = aioesphomeapi.APIClient("api_test.local", 6053, "MyPassword")

await cli.connect(login=True)
api = aioesphomeapi.APIClient(
"api_test.local",
6053,
noise_psk="YOUR_ENCRYPTION_KEY", # Remove if not using encryption
)
await api.connect(login=True)

def change_callback(state):
"""Print the state changes of the device.."""
"""Print the state changes of the device."""
print(state)

# Subscribe to the state changes
cli.subscribe_states(change_callback)
api.subscribe_states(change_callback)

loop = asyncio.get_event_loop()
try:
Expand Down Expand Up @@ -129,8 +153,10 @@ For development is recommended to use a Python virtual environment (``venv``).

# Run linters & test
$ script/lint
# Update protobuf _pb2.py definitions (requires a protobuf compiler installation)
$ script/gen-protoc
# Update protobuf _pb2.py definitions (requires docker or podman)
$ docker run --rm -v $PWD:/aioesphomeapi ghcr.io/esphome/aioesphomeapi-proto-builder:latest
# Or with podman:
$ podman run --rm -v $PWD:/aioesphomeapi --userns=keep-id ghcr.io/esphome/aioesphomeapi-proto-builder:latest

A cli tool is also available for watching logs:

Expand Down
12 changes: 11 additions & 1 deletion aioesphomeapi/api_options.proto
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ extend google.protobuf.MessageOptions {
extend google.protobuf.FieldOptions {
optional string field_ifdef = 1042;
optional uint32 fixed_array_size = 50007;
optional bool no_zero_copy = 50008 [default=false];
optional bool fixed_array_skip_zero = 50009 [default=false];
optional string fixed_array_size_define = 50010;
optional string fixed_array_with_length_define = 50011;
Expand Down Expand Up @@ -80,4 +79,15 @@ extend google.protobuf.FieldOptions {
// Example: [(container_pointer_no_template) = "light::ColorModeMask"]
// generates: const light::ColorModeMask *supported_color_modes{};
optional string container_pointer_no_template = 50014;

// packed_buffer: Expose raw packed buffer instead of decoding into container
// When set on a packed repeated field, the generated code stores a pointer
// to the raw protobuf buffer instead of decoding values. This enables
// zero-copy passthrough when the consumer can decode on-demand.
// The field must be a packed repeated field (packed=true).
// Generates three fields:
// - const uint8_t *<field>_data_{nullptr};
// - uint16_t <field>_length_{0};
// - uint16_t <field>_count_{0};
optional bool packed_buffer = 50015 [default=false];
}
2 changes: 1 addition & 1 deletion aioesphomeapi/api_options_pb2.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

828 changes: 411 additions & 417 deletions aioesphomeapi/api_pb2.py

Large diffs are not rendered by default.

94 changes: 84 additions & 10 deletions aioesphomeapi/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@
HomeassistantActionRequest,
HomeassistantActionResponse,
HomeAssistantStateResponse,
InfraredRFReceiveEvent,
InfraredRFTransmitRawTimingsRequest,
LightCommandRequest,
ListEntitiesDoneResponse,
ListEntitiesRequest,
Expand Down Expand Up @@ -101,6 +103,7 @@
on_bluetooth_message_types,
on_bluetooth_scanner_state_response,
on_home_assistant_action_request,
on_infrared_rf_receive_event,
on_state_msg,
on_subscribe_home_assistant_state_response,
on_zwave_proxy_request_message,
Expand Down Expand Up @@ -140,6 +143,7 @@
FanDirection,
FanSpeed,
HomeassistantServiceCall,
InfraredRFReceiveEvent as InfraredRFReceiveEventModel,
LegacyCoverCommand,
LockCommand,
LogLevel,
Expand Down Expand Up @@ -463,6 +467,36 @@ def subscribe_zwave_proxy_request(
(ZWaveProxyRequest,),
)

def subscribe_infrared_rf_receive(
self,
on_infrared_rf_receive: Callable[[InfraredRFReceiveEventModel], None],
) -> Callable[[], None]:
"""Subscribe to Infrared/RF Receive Event messages."""
return self._get_connection().add_message_callback(
partial(
on_infrared_rf_receive_event,
on_infrared_rf_receive,
),
(InfraredRFReceiveEvent,),
)

def infrared_rf_transmit_raw_timings(
self,
key: int,
carrier_frequency: int,
timings: list[int],
repeat_count: int = 1,
device_id: int = 0,
) -> None:
"""Send an infrared/RF raw timings transmit request."""
req = InfraredRFTransmitRawTimingsRequest()
req.device_id = device_id
req.key = key
req.carrier_frequency = carrier_frequency
req.repeat_count = repeat_count
req.timings.extend(timings)
self._get_connection().send_message(req)

async def _send_bluetooth_message_await_response(
self,
address: int,
Expand Down Expand Up @@ -591,6 +625,14 @@ async def bluetooth_device_connect( # pylint: disable=too-many-locals, too-many
if self._debug_enabled:
_LOGGER.debug("%s: Using connection version %s", address, request_type)

def on_bluetooth_connection_state_with_notify_cleanup(
connected: bool, mtu: int, error: int
) -> None:
"""Wrap connection state callback to clean up notify callbacks on disconnect."""
if not connected:
self.bluetooth_gatt_stop_notify_for_address(address)
on_bluetooth_connection_state(connected, mtu, error)

unsub = self._get_connection().send_message_callback_response(
BluetoothDeviceRequest(
address=address,
Expand All @@ -602,7 +644,7 @@ async def bluetooth_device_connect( # pylint: disable=too-many-locals, too-many
on_bluetooth_device_connection_response,
connect_future,
address,
on_bluetooth_connection_state,
on_bluetooth_connection_state_with_notify_cleanup,
),
(BluetoothDeviceConnectionResponse,),
)
Expand Down Expand Up @@ -953,17 +995,44 @@ async def bluetooth_gatt_start_notify(
remove_callback()
raise

key = (address, handle)
self._notify_callbacks[key] = remove_callback

async def stop_notify() -> None:
if self._connection is None:
return
self.bluetooth_gatt_stop_notify(address, handle)

def wrapped_remove_callback() -> None:
self._notify_callbacks.pop(key, None)
remove_callback()

return stop_notify, wrapped_remove_callback

def bluetooth_gatt_stop_notify(self, address: int, handle: int) -> None:
"""Stop a notify session for a GATT characteristic.

This is a synchronous method that can be safely called from
exception handlers without awaiting.
"""
key = (address, handle)
if remove_callback := self._notify_callbacks.pop(key, None):
remove_callback()

if self._connection is not None:
self._connection.send_message(
BluetoothGATTNotifyRequest(address=address, handle=handle, enable=False)
)

return stop_notify, remove_callback
def bluetooth_gatt_stop_notify_for_address(self, address: int) -> None:
"""Stop all notify sessions for a Bluetooth device.

This is a synchronous method that removes all notify callbacks
for a given address. It does not send disable messages since
this is typically called when the device has disconnected.
"""
keys_to_remove = [key for key in self._notify_callbacks if key[0] == address]
for key in keys_to_remove:
if remove_callback := self._notify_callbacks.pop(key, None):
remove_callback()

def subscribe_home_assistant_states(
self,
Expand Down Expand Up @@ -1336,14 +1405,19 @@ def water_heater_command(
req.has_fields |= WaterHeaterCommandField.TARGET_TEMPERATURE
req.target_temperature = target_temperature

if away is not None or on is not None:
req.has_fields |= WaterHeaterCommandField.STATE
state = WaterHeaterStateFlag(0)
# Use the new granular AWAY_STATE/ON_STATE fields instead of the
# deprecated combined STATE field. Water heater support only shipped
# in HA 2026.2.0/ESPHome 2026.1.0 so early adopters can update
# their devices; no need for legacy STATE backward compat.
if away is not None:
req.has_fields |= WaterHeaterCommandField.AWAY_STATE
if away:
state |= WaterHeaterStateFlag.AWAY
req.state |= WaterHeaterStateFlag.AWAY

if on is not None:
req.has_fields |= WaterHeaterCommandField.ON_STATE
if on:
state |= WaterHeaterStateFlag.ON
req.state = state
req.state |= WaterHeaterStateFlag.ON

if target_temperature_low is not None:
req.has_fields |= WaterHeaterCommandField.TARGET_TEMPERATURE_LOW
Expand Down
1 change: 1 addition & 0 deletions aioesphomeapi/client_base.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ cdef class APIClientBase:
cdef public APIConnection _connection
cdef public bint _debug_enabled
cdef public object _loop
cdef public dict _notify_callbacks
cdef public ConnectionParams _params
cdef public str cached_name
cdef public str log_name
Expand Down
Loading