Skip to content

Commit

Permalink
Fix ESPHome deep sleep devices staying unavailable after unexpected d…
Browse files Browse the repository at this point in the history
…isconnect (home-assistant#96353)
  • Loading branch information
bdraco authored Jul 11, 2023
1 parent c252758 commit 5d5c583
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 2 deletions.
6 changes: 6 additions & 0 deletions homeassistant/components/esphome/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,12 @@ async def on_connect(self) -> None:
assert cli.api_version is not None
entry_data.api_version = cli.api_version
entry_data.available = True
# Reset expected disconnect flag on successful reconnect
# as it will be flipped to False on unexpected disconnect.
#
# We use this to determine if a deep sleep device should
# be marked as unavailable or not.
entry_data.expected_disconnect = True
if entry_data.device_info.name:
assert reconnect_logic is not None, "Reconnect logic must be set"
reconnect_logic.name = entry_data.device_info.name
Expand Down
18 changes: 17 additions & 1 deletion tests/components/esphome/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ def __init__(self, entry: MockConfigEntry) -> None:
self.entry = entry
self.state_callback: Callable[[EntityState], None]
self.on_disconnect: Callable[[bool], None]
self.on_connect: Callable[[bool], None]

def set_state_callback(self, state_callback: Callable[[EntityState], None]) -> None:
"""Set the state callback."""
Expand All @@ -171,6 +172,14 @@ async def mock_disconnect(self, expected_disconnect: bool) -> None:
"""Mock disconnecting."""
await self.on_disconnect(expected_disconnect)

def set_on_connect(self, on_connect: Callable[[], None]) -> None:
"""Set the connect callback."""
self.on_connect = on_connect

async def mock_connect(self) -> None:
"""Mock connecting."""
await self.on_connect()


async def _mock_generic_device_entry(
hass: HomeAssistant,
Expand Down Expand Up @@ -226,6 +235,7 @@ def __init__(self, *args, **kwargs):
"""Init the mock."""
super().__init__(*args, **kwargs)
mock_device.set_on_disconnect(kwargs["on_disconnect"])
mock_device.set_on_connect(kwargs["on_connect"])
self._try_connect = self.mock_try_connect

async def mock_try_connect(self):
Expand Down Expand Up @@ -313,9 +323,15 @@ async def _mock_device(
user_service: list[UserService],
states: list[EntityState],
entry: MockConfigEntry | None = None,
device_info: dict[str, Any] | None = None,
) -> MockESPHomeDevice:
return await _mock_generic_device_entry(
hass, mock_client, {}, (entity_info, user_service), states, entry
hass,
mock_client,
device_info or {},
(entity_info, user_service),
states,
entry,
)

return _mock_device
56 changes: 55 additions & 1 deletion tests/components/esphome/test_entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
UserService,
)

from homeassistant.const import ATTR_RESTORED, STATE_ON
from homeassistant.const import ATTR_RESTORED, STATE_ON, STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant

from .conftest import MockESPHomeDevice
Expand Down Expand Up @@ -130,3 +130,57 @@ async def test_entity_info_object_ids(
)
state = hass.states.get("binary_sensor.test_object_id_is_used")
assert state is not None


async def test_deep_sleep_device(
hass: HomeAssistant,
mock_client: APIClient,
hass_storage: dict[str, Any],
mock_esphome_device: Callable[
[APIClient, list[EntityInfo], list[UserService], list[EntityState]],
Awaitable[MockESPHomeDevice],
],
) -> None:
"""Test a deep sleep device."""
entity_info = [
BinarySensorInfo(
object_id="mybinary_sensor",
key=1,
name="my binary_sensor",
unique_id="my_binary_sensor",
),
]
states = [
BinarySensorState(key=1, state=True, missing_state=False),
BinarySensorState(key=2, state=True, missing_state=False),
]
user_service = []
mock_device = await mock_esphome_device(
mock_client=mock_client,
entity_info=entity_info,
user_service=user_service,
states=states,
device_info={"has_deep_sleep": True},
)
state = hass.states.get("binary_sensor.test_mybinary_sensor")
assert state is not None
assert state.state == STATE_ON

await mock_device.mock_disconnect(False)
await hass.async_block_till_done()
state = hass.states.get("binary_sensor.test_mybinary_sensor")
assert state is not None
assert state.state == STATE_UNAVAILABLE

await mock_device.mock_connect()
await hass.async_block_till_done()

state = hass.states.get("binary_sensor.test_mybinary_sensor")
assert state is not None
assert state.state == STATE_ON

await mock_device.mock_disconnect(True)
await hass.async_block_till_done()
state = hass.states.get("binary_sensor.test_mybinary_sensor")
assert state is not None
assert state.state == STATE_ON

0 comments on commit 5d5c583

Please sign in to comment.