diff --git a/homeassistant/components/blue_current/__init__.py b/homeassistant/components/blue_current/__init__.py index 39c77c1bdfe..55362a7392d 100644 --- a/homeassistant/components/blue_current/__init__.py +++ b/homeassistant/components/blue_current/__init__.py @@ -4,7 +4,6 @@ from __future__ import annotations import asyncio from contextlib import suppress -from datetime import datetime from typing import Any from bluecurrent_api import Client @@ -16,24 +15,17 @@ from bluecurrent_api.exceptions import ( ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_NAME, - CONF_API_TOKEN, - EVENT_HOMEASSISTANT_STOP, - Platform, -) -from homeassistant.core import Event, HomeAssistant +from homeassistant.const import ATTR_NAME, CONF_API_TOKEN, Platform +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.event import async_call_later from .const import DOMAIN, EVSE_ID, LOGGER, MODEL_TYPE PLATFORMS = [Platform.SENSOR] CHARGE_POINTS = "CHARGE_POINTS" DATA = "data" -SMALL_DELAY = 1 -LARGE_DELAY = 20 +DELAY = 5 GRID = "GRID" OBJECT = "object" @@ -48,26 +40,19 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b connector = Connector(hass, config_entry, client) try: - await connector.connect(api_token) + await client.validate_api_token(api_token) except InvalidApiToken as err: raise ConfigEntryAuthFailed("Invalid API token.") from err except BlueCurrentException as err: raise ConfigEntryNotReady from err + config_entry.async_create_background_task( + hass, connector.run_task(), "blue_current-websocket" + ) - hass.async_create_background_task(connector.start_loop(), "blue_current-websocket") - await client.get_charge_points() - - await client.wait_for_response() + await client.wait_for_charge_points() hass.data[DOMAIN][config_entry.entry_id] = connector await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) - config_entry.async_on_unload(connector.disconnect) - - async def _async_disconnect_websocket(_: Event) -> None: - await connector.disconnect() - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_disconnect_websocket) - return True @@ -95,12 +80,6 @@ class Connector: self.client = client self.charge_points: dict[str, dict] = {} self.grid: dict[str, Any] = {} - self.available = False - - async def connect(self, token: str) -> None: - """Register on_data and connect to the websocket.""" - await self.client.connect(token) - self.available = True async def on_data(self, message: dict) -> None: """Handle received data.""" @@ -158,34 +137,39 @@ class Connector: """Dispatch a grid signal.""" async_dispatcher_send(self.hass, f"{DOMAIN}_grid_update") - async def start_loop(self) -> None: + async def run_task(self) -> None: """Start the receive loop.""" try: - await self.client.start_loop(self.on_data) - except BlueCurrentException as err: - LOGGER.warning( - "Disconnected from the Blue Current websocket. Retrying to connect in background. %s", - err, - ) + while True: + try: + await self.client.connect(self.on_data) + except RequestLimitReached: + LOGGER.warning( + "Request limit reached. reconnecting at 00:00 (Europe/Amsterdam)" + ) + delay = self.client.get_next_reset_delta().seconds + except WebsocketError: + LOGGER.debug("Disconnected, retrying in background") + delay = DELAY - async_call_later(self.hass, SMALL_DELAY, self.reconnect) + self._on_disconnect() + await asyncio.sleep(delay) + finally: + await self._disconnect() - async def reconnect(self, _event_time: datetime | None = None) -> None: - """Keep trying to reconnect to the websocket.""" - try: - await self.connect(self.config.data[CONF_API_TOKEN]) - LOGGER.debug("Reconnected to the Blue Current websocket") - self.hass.async_create_task(self.start_loop()) - except RequestLimitReached: - self.available = False - async_call_later( - self.hass, self.client.get_next_reset_delta(), self.reconnect - ) - except WebsocketError: - self.available = False - async_call_later(self.hass, LARGE_DELAY, self.reconnect) + def _on_disconnect(self) -> None: + """Dispatch signals to update entity states.""" + for evse_id in self.charge_points: + self.dispatch_value_update_signal(evse_id) + self.dispatch_grid_update_signal() - async def disconnect(self) -> None: + async def _disconnect(self) -> None: """Disconnect from the websocket.""" with suppress(WebsocketError): await self.client.disconnect() + self._on_disconnect() + + @property + def connected(self) -> bool: + """Returns the connection status.""" + return self.client.is_connected() diff --git a/homeassistant/components/blue_current/entity.py b/homeassistant/components/blue_current/entity.py index 547b2410000..ecbbd8f0851 100644 --- a/homeassistant/components/blue_current/entity.py +++ b/homeassistant/components/blue_current/entity.py @@ -40,7 +40,7 @@ class BlueCurrentEntity(Entity): @property def available(self) -> bool: """Return entity availability.""" - return self.connector.available and self.has_value + return self.connector.connected and self.has_value @callback @abstractmethod diff --git a/homeassistant/components/blue_current/manifest.json b/homeassistant/components/blue_current/manifest.json index cadaac30d68..fddd48554e2 100644 --- a/homeassistant/components/blue_current/manifest.json +++ b/homeassistant/components/blue_current/manifest.json @@ -5,5 +5,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/blue_current", "iot_class": "cloud_push", - "requirements": ["bluecurrent-api==1.0.6"] + "loggers": ["bluecurrent_api"], + "requirements": ["bluecurrent-api==1.2.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index a25f2eaec8c..a56caf9d960 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -568,7 +568,7 @@ blinkpy==0.22.6 blockchain==1.4.4 # homeassistant.components.blue_current -bluecurrent-api==1.0.6 +bluecurrent-api==1.2.2 # homeassistant.components.bluemaestro bluemaestro-ble==0.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 571bd2e0ef7..738f43a1eca 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -487,7 +487,7 @@ blebox-uniapi==2.2.2 blinkpy==0.22.6 # homeassistant.components.blue_current -bluecurrent-api==1.0.6 +bluecurrent-api==1.2.2 # homeassistant.components.bluemaestro bluemaestro-ble==0.2.3 diff --git a/tests/components/blue_current/__init__.py b/tests/components/blue_current/__init__.py index e020855b59c..b5c15064449 100644 --- a/tests/components/blue_current/__init__.py +++ b/tests/components/blue_current/__init__.py @@ -31,15 +31,21 @@ def create_client_mock( future_container: FutureContainer, started_loop: Event, charge_point: dict, - status: dict | None, - grid: dict | None, + status: dict, + grid: dict, ) -> MagicMock: """Create a mock of the bluecurrent-api Client.""" client_mock = MagicMock(spec=Client) + received_charge_points = Event() - async def start_loop(receiver): + async def wait_for_charge_points(): + """Wait until chargepoints are received.""" + await received_charge_points.wait() + + async def connect(receiver): """Set the receiver and await future.""" client_mock.receiver = receiver + await client_mock.get_charge_points() started_loop.set() started_loop.clear() @@ -50,13 +56,13 @@ def create_client_mock( async def get_charge_points() -> None: """Send a list of charge points to the callback.""" - await started_loop.wait() await client_mock.receiver( { "object": "CHARGE_POINTS", "data": [charge_point], } ) + received_charge_points.set() async def get_status(evse_id: str) -> None: """Send the status of a charge point to the callback.""" @@ -71,7 +77,8 @@ def create_client_mock( """Send the grid status to the callback.""" await client_mock.receiver({"object": "GRID_STATUS", "data": grid}) - client_mock.start_loop.side_effect = start_loop + client_mock.connect.side_effect = connect + client_mock.wait_for_charge_points.side_effect = wait_for_charge_points client_mock.get_charge_points.side_effect = get_charge_points client_mock.get_status.side_effect = get_status client_mock.get_grid_status.side_effect = get_grid_status @@ -92,6 +99,12 @@ async def init_integration( if charge_point is None: charge_point = DEFAULT_CHARGE_POINT + if status is None: + status = {} + + if grid is None: + grid = {} + future_container = FutureContainer(hass.loop.create_future()) started_loop = Event() diff --git a/tests/components/blue_current/test_config_flow.py b/tests/components/blue_current/test_config_flow.py index f56e722d785..2e278af4982 100644 --- a/tests/components/blue_current/test_config_flow.py +++ b/tests/components/blue_current/test_config_flow.py @@ -127,6 +127,11 @@ async def test_reauth( ), patch( "homeassistant.components.blue_current.config_flow.Client.get_email", return_value="test@email.com", + ), patch( + "homeassistant.components.blue_current.config_flow.Client.wait_for_charge_points", + ), patch( + "homeassistant.components.blue_current.Client.connect", + lambda self, on_data: hass.loop.create_future(), ): config_entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( diff --git a/tests/components/blue_current/test_init.py b/tests/components/blue_current/test_init.py index 37c14922674..4f570156c82 100644 --- a/tests/components/blue_current/test_init.py +++ b/tests/components/blue_current/test_init.py @@ -29,15 +29,22 @@ async def test_load_unload_entry( hass: HomeAssistant, config_entry: MockConfigEntry ) -> None: """Test load and unload entry.""" - with patch("homeassistant.components.blue_current.Client", autospec=True): + with patch( + "homeassistant.components.blue_current.Client.validate_api_token" + ), patch( + "homeassistant.components.blue_current.Client.wait_for_charge_points" + ), patch("homeassistant.components.blue_current.Client.disconnect"), patch( + "homeassistant.components.blue_current.Client.connect", + lambda self, on_data: hass.loop.create_future(), + ): config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state == ConfigEntryState.LOADED - await hass.config_entries.async_unload(config_entry.entry_id) - await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.NOT_LOADED + await hass.config_entries.async_unload(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state == ConfigEntryState.NOT_LOADED @pytest.mark.parametrize( @@ -55,43 +62,29 @@ async def test_config_exceptions( ) -> None: """Test if the correct config error is raised when connecting to the api fails.""" with patch( - "homeassistant.components.blue_current.Client.connect", + "homeassistant.components.blue_current.Client.validate_api_token", side_effect=api_error, ), pytest.raises(config_error): config_entry.add_to_hass(hass) await async_setup_entry(hass, config_entry) -async def test_start_loop(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: - """Test start_loop.""" +async def test_connect_websocket_error( + hass: HomeAssistant, config_entry: MockConfigEntry +) -> None: + """Test reconnect when connect throws a WebsocketError.""" - with patch("homeassistant.components.blue_current.SMALL_DELAY", 0): + with patch("homeassistant.components.blue_current.DELAY", 0): mock_client, started_loop, future_container = await init_integration( hass, config_entry ) - future_container.future.set_exception(BlueCurrentException) + future_container.future.set_exception(WebsocketError) await started_loop.wait() assert mock_client.connect.call_count == 2 -async def test_reconnect_websocket_error( - hass: HomeAssistant, config_entry: MockConfigEntry -) -> None: - """Test reconnect when connect throws a WebsocketError.""" - - with patch("homeassistant.components.blue_current.LARGE_DELAY", 0): - mock_client, started_loop, future_container = await init_integration( - hass, config_entry - ) - future_container.future.set_exception(BlueCurrentException) - mock_client.connect.side_effect = [WebsocketError, None] - - await started_loop.wait() - assert mock_client.connect.call_count == 3 - - -async def test_reconnect_request_limit_reached_error( +async def test_connect_request_limit_reached_error( hass: HomeAssistant, config_entry: MockConfigEntry ) -> None: """Test reconnect when connect throws a RequestLimitReached.""" @@ -99,10 +92,9 @@ async def test_reconnect_request_limit_reached_error( mock_client, started_loop, future_container = await init_integration( hass, config_entry ) - future_container.future.set_exception(BlueCurrentException) - mock_client.connect.side_effect = [RequestLimitReached, None] + future_container.future.set_exception(RequestLimitReached) mock_client.get_next_reset_delta.return_value = timedelta(seconds=0) await started_loop.wait() assert mock_client.get_next_reset_delta.call_count == 1 - assert mock_client.connect.call_count == 3 + assert mock_client.connect.call_count == 2