Fix ESPHome button not getting device updates (#95311)

This commit is contained in:
J. Nick Koston 2023-06-26 22:34:37 -05:00 committed by GitHub
parent d6cd5648b9
commit 0af71851a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 90 additions and 12 deletions

View File

@ -308,7 +308,6 @@ omit =
homeassistant/components/escea/discovery.py homeassistant/components/escea/discovery.py
homeassistant/components/esphome/__init__.py homeassistant/components/esphome/__init__.py
homeassistant/components/esphome/bluetooth/* homeassistant/components/esphome/bluetooth/*
homeassistant/components/esphome/button.py
homeassistant/components/esphome/camera.py homeassistant/components/esphome/camera.py
homeassistant/components/esphome/cover.py homeassistant/components/esphome/cover.py
homeassistant/components/esphome/domain_data.py homeassistant/components/esphome/domain_data.py

View File

@ -42,10 +42,18 @@ class EsphomeButton(EsphomeEntity[ButtonInfo, EntityState], ButtonEntity):
@callback @callback
def _on_device_update(self) -> None: def _on_device_update(self) -> None:
"""Update the entity state when device info has changed.""" """Call when device updates or entry data changes.
# This override the EsphomeEntity method as the button entity
# never gets a state update. The default behavior is only to write entity state when the
self._on_state_update() device is unavailable when the device state changes.
This method overrides the default behavior since buttons do
not have a state, so we will never get a state update for a
button. As such, we need to write the state on every device
update to ensure the button goes available and unavailable
as the device becomes available or unavailable.
"""
self._on_entry_data_changed()
self.async_write_ha_state()
async def async_press(self) -> None: async def async_press(self) -> None:
"""Press the button.""" """Press the button."""

View File

@ -153,6 +153,7 @@ class MockESPHomeDevice:
"""Init the mock.""" """Init the mock."""
self.entry = entry self.entry = entry
self.state_callback: Callable[[EntityState], None] self.state_callback: Callable[[EntityState], None]
self.on_disconnect: Callable[[bool], None]
def set_state_callback(self, state_callback: Callable[[EntityState], None]) -> None: def set_state_callback(self, state_callback: Callable[[EntityState], None]) -> None:
"""Set the state callback.""" """Set the state callback."""
@ -162,6 +163,14 @@ class MockESPHomeDevice:
"""Mock setting state.""" """Mock setting state."""
self.state_callback(state) self.state_callback(state)
def set_on_disconnect(self, on_disconnect: Callable[[bool], None]) -> None:
"""Set the disconnect callback."""
self.on_disconnect = on_disconnect
async def mock_disconnect(self, expected_disconnect: bool) -> None:
"""Mock disconnecting."""
await self.on_disconnect(expected_disconnect)
async def _mock_generic_device_entry( async def _mock_generic_device_entry(
hass: HomeAssistant, hass: HomeAssistant,
@ -209,15 +218,23 @@ async def _mock_generic_device_entry(
mock_client.subscribe_states = _subscribe_states mock_client.subscribe_states = _subscribe_states
try_connect_done = Event() try_connect_done = Event()
real_try_connect = ReconnectLogic._try_connect
async def mock_try_connect(self): class MockReconnectLogic(ReconnectLogic):
"""Set an event when ReconnectLogic._try_connect has been awaited.""" """Mock ReconnectLogic."""
result = await real_try_connect(self)
try_connect_done.set()
return result
with patch.object(ReconnectLogic, "_try_connect", mock_try_connect): def __init__(self, *args, **kwargs):
"""Init the mock."""
super().__init__(*args, **kwargs)
mock_device.set_on_disconnect(kwargs["on_disconnect"])
self._try_connect = self.mock_try_connect
async def mock_try_connect(self):
"""Set an event when ReconnectLogic._try_connect has been awaited."""
result = await super()._try_connect()
try_connect_done.set()
return result
with patch("homeassistant.components.esphome.ReconnectLogic", MockReconnectLogic):
assert await hass.config_entries.async_setup(entry.entry_id) assert await hass.config_entries.async_setup(entry.entry_id)
await try_connect_done.wait() await try_connect_done.wait()

View File

@ -0,0 +1,54 @@
"""Test ESPHome buttones."""
from unittest.mock import call
from aioesphomeapi import APIClient, ButtonInfo
from homeassistant.components.button import (
DOMAIN as BUTTON_DOMAIN,
SERVICE_PRESS,
)
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, STATE_UNKNOWN
from homeassistant.core import HomeAssistant
async def test_button_generic_entity(
hass: HomeAssistant, mock_client: APIClient, mock_esphome_device
) -> None:
"""Test a generic button entity."""
entity_info = [
ButtonInfo(
object_id="mybutton",
key=1,
name="my button",
unique_id="my_button",
)
]
states = []
user_service = []
mock_device = await mock_esphome_device(
mock_client=mock_client,
entity_info=entity_info,
user_service=user_service,
states=states,
)
state = hass.states.get("button.test_my_button")
assert state is not None
assert state.state == STATE_UNKNOWN
await hass.services.async_call(
BUTTON_DOMAIN,
SERVICE_PRESS,
{ATTR_ENTITY_ID: "button.test_my_button"},
blocking=True,
)
mock_client.button_command.assert_has_calls([call(1)])
state = hass.states.get("button.test_my_button")
assert state is not None
assert state.state != STATE_UNKNOWN
await mock_device.mock_disconnect(False)
state = hass.states.get("button.test_my_button")
assert state is not None
assert state.state == STATE_UNAVAILABLE