diff --git a/hass_nabucasa/acme.py b/hass_nabucasa/acme.py index 302a5e2b5..da7ebaaae 100644 --- a/hass_nabucasa/acme.py +++ b/hass_nabucasa/acme.py @@ -18,6 +18,7 @@ import josepy as jose from . import cloud_api +from .utils import UTC FILE_ACCOUNT_KEY = "acme_account.pem" FILE_PRIVATE_KEY = "remote_private.pem" @@ -104,7 +105,7 @@ def expire_date(self) -> Optional[datetime]: """Return datetime of expire date for certificate.""" if not self._x509: return None - return self._x509.not_valid_after + return self._x509.not_valid_after.replace(tzinfo=UTC) @property def common_name(self) -> Optional[str]: @@ -344,14 +345,15 @@ async def issue_certificate(self) -> None: challenge = await self.cloud.run_executor(self._start_challenge, csr) # Update DNS - async with async_timeout.timeout(10): - resp = await cloud_api.async_remote_challenge_txt( - self.cloud, challenge.validation - ) - - if resp.status != 200: + try: + async with async_timeout.timeout(15): + resp = await cloud_api.async_remote_challenge_txt( + self.cloud, challenge.validation + ) + assert resp.status == 200 + except (asyncio.TimeoutError, AssertionError): _LOGGER.error("Can't set challenge token to NabuCasa DNS!") - raise AcmeNabuCasaError() + raise AcmeNabuCasaError() from None # Finish validation try: diff --git a/hass_nabucasa/client.py b/hass_nabucasa/client.py index d75711a79..90b5bc5cd 100644 --- a/hass_nabucasa/client.py +++ b/hass_nabucasa/client.py @@ -43,12 +43,6 @@ async def cleanups(self) -> None: """Called on logout.""" raise NotImplementedError() - async def async_user_message( - self, identifier: str, title: str, message: str - ) -> None: - """Create a message for user to UI.""" - raise NotImplementedError() - async def async_alexa_message(self, payload: Dict[Any, Any]) -> Dict[Any, Any]: """process cloud alexa message to client.""" raise NotImplementedError() @@ -64,3 +58,11 @@ async def async_webhook_message(self, payload: Dict[Any, Any]) -> Dict[Any, Any] async def async_cloudhooks_update(self, data: Dict[str, Dict[str, str]]) -> None: """Update local list of cloudhooks.""" raise NotImplementedError() + + def dispatcher_message(self, identifier: str, data: Any = None) -> None: + """Send data to dispatcher.""" + raise NotImplementedError() + + def user_message(self, identifier: str, title: str, message: str) -> None: + """Create a message for user to UI.""" + raise NotImplementedError() diff --git a/hass_nabucasa/const.py b/hass_nabucasa/const.py index c04b6923d..d8190bea9 100644 --- a/hass_nabucasa/const.py +++ b/hass_nabucasa/const.py @@ -10,6 +10,9 @@ STATE_CONNECTED = "connected" STATE_DISCONNECTED = "disconnected" +DISPATCH_REMOTE_CONNECT = "remote_connect" +DISPATCH_REMOTE_DISCONNECT = "remote_disconnect" + SERVERS = { "production": { "cognito_client_id": "60i2uvhvbiref2mftj7rgcrt9u", diff --git a/hass_nabucasa/iot.py b/hass_nabucasa/iot.py index dfd78a08f..70977f491 100644 --- a/hass_nabucasa/iot.py +++ b/hass_nabucasa/iot.py @@ -143,7 +143,7 @@ async def _handle_connection(self): except Unauthenticated as err: _LOGGER.error("Unable to refresh token: %s", err) - await self.cloud.client.async_user_message( + self.cloud.client.user_message( "cloud_subscription_expired", "Home Assistant Cloud", MESSAGE_AUTH_FAIL ) @@ -155,7 +155,7 @@ async def _handle_connection(self): return if self.cloud.subscription_expired: - await self.cloud.client.async_user_message( + self.cloud.client.user_message( "cloud_subscription_expired", "Home Assistant Cloud", MESSAGE_EXPIRATION ) self.close_requested = True diff --git a/hass_nabucasa/remote.py b/hass_nabucasa/remote.py index d37a77f5a..53779c118 100644 --- a/hass_nabucasa/remote.py +++ b/hass_nabucasa/remote.py @@ -12,9 +12,8 @@ from snitun.utils.aes import generate_aes_keyset from snitun.utils.aiohttp_client import SniTunClientAioHttp -from . import cloud_api, utils +from . import cloud_api, utils, const from .acme import AcmeClientError, AcmeHandler -from .const import MESSAGE_REMOTE_SETUP, MESSAGE_REMOTE_READY _LOGGER = logging.getLogger(__name__) @@ -117,10 +116,11 @@ async def load_backend(self) -> None: self._acme_task = self.cloud.run_task(self._certificate_handler()) # Load instance data from backend - async with async_timeout.timeout(10): - resp = await cloud_api.async_remote_register(self.cloud) - - if resp.status != 200: + try: + async with async_timeout.timeout(15): + resp = await cloud_api.async_remote_register(self.cloud) + assert resp.status == 200 + except (asyncio.TimeoutError, AssertionError): _LOGGER.error("Can't update remote details from Home Assistant cloud") return data = await resp.json() @@ -148,17 +148,17 @@ async def load_backend(self) -> None: try: await self._acme.issue_certificate() except AcmeClientError: - await self.cloud.client.async_user_message( + self.cloud.client.user_message( "cloud_remote_acme", "Home Assistant Cloud", - MESSAGE_REMOTE_SETUP + const.MESSAGE_REMOTE_SETUP, ) return else: - await self.cloud.client.async_user_message( + self.cloud.client.user_message( "cloud_remote_acme", "Home Assistant Cloud", - MESSAGE_REMOTE_READY, + const.MESSAGE_REMOTE_READY, ) # Setup snitun / aiohttp wrapper @@ -219,13 +219,14 @@ async def _refresh_snitun_token(self) -> None: # Generate session token aes_key, aes_iv = generate_aes_keyset() - async with async_timeout.timeout(10): - resp = await cloud_api.async_remote_token(self.cloud, aes_key, aes_iv) + try: + async with async_timeout.timeout(15): + resp = await cloud_api.async_remote_token(self.cloud, aes_key, aes_iv) + assert resp.status == 200 + except (asyncio.TimeoutError, AssertionError): + raise RemoteBackendError() from None - if resp.status != 200: - raise RemoteBackendError() data = await resp.json() - self._token = SniTunToken( data["token"].encode(), aes_key, @@ -248,6 +249,8 @@ async def connect(self) -> None: await self._snitun.connect( self._token.fernet, self._token.aes_key, self._token.aes_iv ) + + self.cloud.client.dispatcher_message(const.DISPATCH_REMOTE_CONNECT) except SniTunConnectionError: _LOGGER.error("Connection problem to snitun server") except RemoteBackendError: @@ -271,6 +274,7 @@ async def disconnect(self) -> None: if not self._snitun.is_connected: return await self._snitun.disconnect() + self.cloud.client.dispatcher_message(const.DISPATCH_REMOTE_DISCONNECT) async def _reconnect_snitun(self) -> None: """Reconnect after disconnect.""" @@ -279,6 +283,7 @@ async def _reconnect_snitun(self) -> None: if self._snitun.is_connected: await self._snitun.wait() + self.cloud.client.dispatcher_message(const.DISPATCH_REMOTE_DISCONNECT) await asyncio.sleep(random.randint(1, 15)) await self.connect() except asyncio.CancelledError: diff --git a/hass_nabucasa/utils.py b/hass_nabucasa/utils.py index 0cd312650..085c084eb 100644 --- a/hass_nabucasa/utils.py +++ b/hass_nabucasa/utils.py @@ -58,11 +58,11 @@ def server_context_modern() -> ssl.SSLContext: def next_midnight() -> int: - """Return the seconds till next midnight.""" - midnight = dt.datetime.utcnow().replace( + """Return the seconds till next local midnight.""" + midnight = dt.datetime.now().replace( hour=0, minute=0, second=0, microsecond=0 ) + dt.timedelta(days=1) - return (midnight - dt.datetime.utcnow()).total_seconds() + return (midnight - dt.datetime.now()).total_seconds() class Registry(dict): diff --git a/setup.py b/setup.py index c6167f99e..6e581d877 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup -VERSION = "0.5" +VERSION = "0.6" setup( name="hass-nabucasa", diff --git a/tests/common.py b/tests/common.py index bc1315486..02aad87c7 100644 --- a/tests/common.py +++ b/tests/common.py @@ -3,7 +3,7 @@ from datetime import datetime from pathlib import Path import tempfile -from typing import Optional +from typing import Optional, Any from hass_nabucasa.client import CloudClient @@ -36,6 +36,7 @@ def __init__(self, loop, websession): self.prop_remote_autostart = True self.mock_user = [] + self.mock_dispatcher = [] self.mock_alexa = [] self.mock_google = [] self.mock_webhooks = [] @@ -75,12 +76,14 @@ def remote_autostart(self) -> bool: async def cleanups(self): """Need nothing to do.""" - async def async_user_message( - self, identifier: str, title: str, message: str - ) -> None: + def user_message(self, identifier: str, title: str, message: str) -> None: """Create a message for user to UI.""" self.mock_user.append((identifier, title, message)) + def dispatcher_message(self, identifier: str, data: Any = None) -> None: + """Send data to dispatcher.""" + self.mock_dispatcher.append((identifier, data)) + async def async_alexa_message(self, payload): """process cloud alexa message to client.""" self.mock_alexa.append(payload) diff --git a/tests/test_remote.py b/tests/test_remote.py index acf8c88a9..4608299e1 100644 --- a/tests/test_remote.py +++ b/tests/test_remote.py @@ -7,6 +7,7 @@ from hass_nabucasa.remote import RemoteUI from hass_nabucasa.utils import utcnow +from hass_nabucasa.const import DISPATCH_REMOTE_CONNECT, DISPATCH_REMOTE_DISCONNECT from .common import mock_coro, MockAcme, MockSnitun @@ -278,6 +279,7 @@ async def test_call_disconnect( await remote.disconnect() assert snitun_mock.call_disconnect assert not remote.is_connected + assert cloud_mock.client.mock_dispatcher[-1][0] == DISPATCH_REMOTE_DISCONNECT async def test_load_backend_no_autostart( @@ -319,6 +321,7 @@ async def test_load_backend_no_autostart( assert snitun_mock.call_connect assert snitun_mock.connect_args[0] == b"test-token" + assert cloud_mock.client.mock_dispatcher[-1][0] == DISPATCH_REMOTE_CONNECT async def test_get_certificate_details(