Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
108 changes: 70 additions & 38 deletions homeassistant/components/elkm1/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,8 +279,10 @@ def _keypad_changed(keypad: Element, changeset: dict[str, Any]) -> None:
keypad.add_callback(_keypad_changed)

try:
if not await async_wait_for_elk_to_sync(elk, LOGIN_TIMEOUT, SYNC_TIMEOUT):
return False
await ElkSyncWaiter(elk, LOGIN_TIMEOUT, SYNC_TIMEOUT).async_wait()
except LoginFailed:
_LOGGER.error("ElkM1 login failed for %s", conf[CONF_HOST])
return False
Comment on lines +286 to +287
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in the future we could raise ConfigEntryAuthFailed but this integration doesn't support reauth right now

except TimeoutError as exc:
raise ConfigEntryNotReady(f"Timed out connecting to {conf[CONF_HOST]}") from exc

Expand Down Expand Up @@ -321,48 +323,78 @@ async def async_unload_entry(hass: HomeAssistant, entry: ElkM1ConfigEntry) -> bo
return unload_ok


async def async_wait_for_elk_to_sync(
elk: Elk,
login_timeout: int,
sync_timeout: int,
) -> bool:
"""Wait until the elk has finished sync. Can fail login or timeout."""
class LoginFailed(Exception):
"""Raised when login to ElkM1 fails."""

sync_event = asyncio.Event()
login_event = asyncio.Event()

success = True
class ElkSyncWaiter:
"""Wait for ElkM1 to sync."""

def login_status(succeeded: bool) -> None:
nonlocal success
def __init__(self, elk: Elk, login_timeout: int, sync_timeout: int) -> None:
"""Initialize the sync waiter."""
self._elk = elk
self._login_timeout = login_timeout
self._sync_timeout = sync_timeout
self._loop = asyncio.get_running_loop()
self._sync_future: asyncio.Future[None] = self._loop.create_future()
self._login_future: asyncio.Future[None] = self._loop.create_future()
self._login_succeeded = False

success = succeeded
def _set_future_if_not_done(self, future: asyncio.Future[None]) -> None:
"""Set the future result if not already done."""
if not future.done():
future.set_result(None)

@callback
def _login_status(self, succeeded: bool) -> None:
"""Handle login status callback."""
self._login_succeeded = succeeded
if succeeded:
_LOGGER.debug("ElkM1 login succeeded")
login_event.set()
self._set_future_if_not_done(self._login_future)
else:
elk.disconnect()
_LOGGER.error("ElkM1 login failed; invalid username or password")
login_event.set()
sync_event.set()

def sync_complete() -> None:
sync_event.set()

elk.add_handler("login", login_status)
elk.add_handler("sync_complete", sync_complete)
for name, event, timeout in (
("login", login_event, login_timeout),
("sync_complete", sync_event, sync_timeout),
):
_LOGGER.debug("Waiting for %s event for %s seconds", name, timeout)
self._async_set_exception_if_not_done(self._login_future, LoginFailed)

@callback
def _async_set_exception_if_not_done(
self, future: asyncio.Future[None], exception: type[Exception]
) -> None:
"""Handle timeout callback."""
if not future.done():
future.set_exception(exception)

@callback
def _sync_complete(self) -> None:
"""Handle sync complete callback."""
self._set_future_if_not_done(self._sync_future)

async def async_wait(self) -> bool:
"""Wait for login and sync to complete."""
self._elk.add_handler("login", self._login_status)
self._elk.add_handler("sync_complete", self._sync_complete)

try:
async with asyncio.timeout(timeout):
await event.wait()
except TimeoutError:
_LOGGER.debug("Timed out waiting for %s event", name)
elk.disconnect()
raise
_LOGGER.debug("Received %s event", name)

return success
for name, future, timeout in (
("login", self._login_future, self._login_timeout),
("sync_complete", self._sync_future, self._sync_timeout),
):
_LOGGER.debug("Waiting for %s event for %s seconds", name, timeout)
handle = self._loop.call_later(
timeout, self._async_set_exception_if_not_done, future, TimeoutError
)
step_succeeded = False
try:
await future
step_succeeded = True
finally:
handle.cancel()
if not step_succeeded:
self._elk.disconnect()

_LOGGER.debug("Received %s event", name)
finally:
self._elk.remove_handler("login", self._login_status)
self._elk.remove_handler("sync_complete", self._sync_complete)

return self._login_succeeded
7 changes: 4 additions & 3 deletions homeassistant/components/elkm1/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from homeassistant.util import slugify
from homeassistant.util.network import is_ip_address

from . import async_wait_for_elk_to_sync, hostname_from_url
from . import ElkSyncWaiter, LoginFailed, hostname_from_url
from .const import CONF_AUTO_CONFIGURE, DISCOVER_SCAN_TIMEOUT, DOMAIN, LOGIN_TIMEOUT
from .discovery import (
_short_mac,
Expand Down Expand Up @@ -89,8 +89,9 @@ async def validate_input(data: dict[str, str], mac: str | None) -> dict[str, str
elk.connect()

try:
if not await async_wait_for_elk_to_sync(elk, LOGIN_TIMEOUT, VALIDATE_TIMEOUT):
raise InvalidAuth
await ElkSyncWaiter(elk, LOGIN_TIMEOUT, VALIDATE_TIMEOUT).async_wait()
except LoginFailed as exc:
raise InvalidAuth from exc
finally:
elk.disconnect()

Expand Down
Loading