Bump bluecurrent-api to 1.2.2 (#110483)

This commit is contained in:
Floris272 2024-03-20 11:28:27 +01:00 committed by GitHub
parent 6552e12161
commit 249f708071
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 86 additions and 91 deletions

View file

@ -4,7 +4,6 @@ from __future__ import annotations
import asyncio import asyncio
from contextlib import suppress from contextlib import suppress
from datetime import datetime
from typing import Any from typing import Any
from bluecurrent_api import Client from bluecurrent_api import Client
@ -16,24 +15,17 @@ from bluecurrent_api.exceptions import (
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import ATTR_NAME, CONF_API_TOKEN, Platform
ATTR_NAME, from homeassistant.core import HomeAssistant
CONF_API_TOKEN,
EVENT_HOMEASSISTANT_STOP,
Platform,
)
from homeassistant.core import Event, HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.dispatcher import async_dispatcher_send 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 from .const import DOMAIN, EVSE_ID, LOGGER, MODEL_TYPE
PLATFORMS = [Platform.SENSOR] PLATFORMS = [Platform.SENSOR]
CHARGE_POINTS = "CHARGE_POINTS" CHARGE_POINTS = "CHARGE_POINTS"
DATA = "data" DATA = "data"
SMALL_DELAY = 1 DELAY = 5
LARGE_DELAY = 20
GRID = "GRID" GRID = "GRID"
OBJECT = "object" OBJECT = "object"
@ -48,26 +40,19 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
connector = Connector(hass, config_entry, client) connector = Connector(hass, config_entry, client)
try: try:
await connector.connect(api_token) await client.validate_api_token(api_token)
except InvalidApiToken as err: except InvalidApiToken as err:
raise ConfigEntryAuthFailed("Invalid API token.") from err raise ConfigEntryAuthFailed("Invalid API token.") from err
except BlueCurrentException as err: except BlueCurrentException as err:
raise ConfigEntryNotReady from 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.wait_for_charge_points()
await client.get_charge_points()
await client.wait_for_response()
hass.data[DOMAIN][config_entry.entry_id] = connector hass.data[DOMAIN][config_entry.entry_id] = connector
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) 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 return True
@ -95,12 +80,6 @@ class Connector:
self.client = client self.client = client
self.charge_points: dict[str, dict] = {} self.charge_points: dict[str, dict] = {}
self.grid: dict[str, Any] = {} 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: async def on_data(self, message: dict) -> None:
"""Handle received data.""" """Handle received data."""
@ -158,34 +137,39 @@ class Connector:
"""Dispatch a grid signal.""" """Dispatch a grid signal."""
async_dispatcher_send(self.hass, f"{DOMAIN}_grid_update") 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.""" """Start the receive loop."""
try: try:
await self.client.start_loop(self.on_data) while True:
except BlueCurrentException as err: try:
LOGGER.warning( await self.client.connect(self.on_data)
"Disconnected from the Blue Current websocket. Retrying to connect in background. %s", except RequestLimitReached:
err, 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: def _on_disconnect(self) -> None:
"""Keep trying to reconnect to the websocket.""" """Dispatch signals to update entity states."""
try: for evse_id in self.charge_points:
await self.connect(self.config.data[CONF_API_TOKEN]) self.dispatch_value_update_signal(evse_id)
LOGGER.debug("Reconnected to the Blue Current websocket") self.dispatch_grid_update_signal()
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)
async def disconnect(self) -> None: async def _disconnect(self) -> None:
"""Disconnect from the websocket.""" """Disconnect from the websocket."""
with suppress(WebsocketError): with suppress(WebsocketError):
await self.client.disconnect() await self.client.disconnect()
self._on_disconnect()
@property
def connected(self) -> bool:
"""Returns the connection status."""
return self.client.is_connected()

View file

@ -40,7 +40,7 @@ class BlueCurrentEntity(Entity):
@property @property
def available(self) -> bool: def available(self) -> bool:
"""Return entity availability.""" """Return entity availability."""
return self.connector.available and self.has_value return self.connector.connected and self.has_value
@callback @callback
@abstractmethod @abstractmethod

View file

@ -5,5 +5,6 @@
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/blue_current", "documentation": "https://www.home-assistant.io/integrations/blue_current",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"requirements": ["bluecurrent-api==1.0.6"] "loggers": ["bluecurrent_api"],
"requirements": ["bluecurrent-api==1.2.2"]
} }

View file

@ -568,7 +568,7 @@ blinkpy==0.22.6
blockchain==1.4.4 blockchain==1.4.4
# homeassistant.components.blue_current # homeassistant.components.blue_current
bluecurrent-api==1.0.6 bluecurrent-api==1.2.2
# homeassistant.components.bluemaestro # homeassistant.components.bluemaestro
bluemaestro-ble==0.2.3 bluemaestro-ble==0.2.3

View file

@ -487,7 +487,7 @@ blebox-uniapi==2.2.2
blinkpy==0.22.6 blinkpy==0.22.6
# homeassistant.components.blue_current # homeassistant.components.blue_current
bluecurrent-api==1.0.6 bluecurrent-api==1.2.2
# homeassistant.components.bluemaestro # homeassistant.components.bluemaestro
bluemaestro-ble==0.2.3 bluemaestro-ble==0.2.3

View file

@ -31,15 +31,21 @@ def create_client_mock(
future_container: FutureContainer, future_container: FutureContainer,
started_loop: Event, started_loop: Event,
charge_point: dict, charge_point: dict,
status: dict | None, status: dict,
grid: dict | None, grid: dict,
) -> MagicMock: ) -> MagicMock:
"""Create a mock of the bluecurrent-api Client.""" """Create a mock of the bluecurrent-api Client."""
client_mock = MagicMock(spec=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.""" """Set the receiver and await future."""
client_mock.receiver = receiver client_mock.receiver = receiver
await client_mock.get_charge_points()
started_loop.set() started_loop.set()
started_loop.clear() started_loop.clear()
@ -50,13 +56,13 @@ def create_client_mock(
async def get_charge_points() -> None: async def get_charge_points() -> None:
"""Send a list of charge points to the callback.""" """Send a list of charge points to the callback."""
await started_loop.wait()
await client_mock.receiver( await client_mock.receiver(
{ {
"object": "CHARGE_POINTS", "object": "CHARGE_POINTS",
"data": [charge_point], "data": [charge_point],
} }
) )
received_charge_points.set()
async def get_status(evse_id: str) -> None: async def get_status(evse_id: str) -> None:
"""Send the status of a charge point to the callback.""" """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.""" """Send the grid status to the callback."""
await client_mock.receiver({"object": "GRID_STATUS", "data": grid}) 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_charge_points.side_effect = get_charge_points
client_mock.get_status.side_effect = get_status client_mock.get_status.side_effect = get_status
client_mock.get_grid_status.side_effect = get_grid_status client_mock.get_grid_status.side_effect = get_grid_status
@ -92,6 +99,12 @@ async def init_integration(
if charge_point is None: if charge_point is None:
charge_point = DEFAULT_CHARGE_POINT charge_point = DEFAULT_CHARGE_POINT
if status is None:
status = {}
if grid is None:
grid = {}
future_container = FutureContainer(hass.loop.create_future()) future_container = FutureContainer(hass.loop.create_future())
started_loop = Event() started_loop = Event()

View file

@ -127,6 +127,11 @@ async def test_reauth(
), patch( ), patch(
"homeassistant.components.blue_current.config_flow.Client.get_email", "homeassistant.components.blue_current.config_flow.Client.get_email",
return_value="test@email.com", 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) config_entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(

View file

@ -29,15 +29,22 @@ async def test_load_unload_entry(
hass: HomeAssistant, config_entry: MockConfigEntry hass: HomeAssistant, config_entry: MockConfigEntry
) -> None: ) -> None:
"""Test load and unload entry.""" """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) config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id) await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() 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.config_entries.async_unload(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
assert config_entry.state == ConfigEntryState.NOT_LOADED assert config_entry.state == ConfigEntryState.NOT_LOADED
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -55,43 +62,29 @@ async def test_config_exceptions(
) -> None: ) -> None:
"""Test if the correct config error is raised when connecting to the api fails.""" """Test if the correct config error is raised when connecting to the api fails."""
with patch( with patch(
"homeassistant.components.blue_current.Client.connect", "homeassistant.components.blue_current.Client.validate_api_token",
side_effect=api_error, side_effect=api_error,
), pytest.raises(config_error): ), pytest.raises(config_error):
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
await async_setup_entry(hass, config_entry) await async_setup_entry(hass, config_entry)
async def test_start_loop(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: async def test_connect_websocket_error(
"""Test start_loop.""" 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( mock_client, started_loop, future_container = await init_integration(
hass, config_entry hass, config_entry
) )
future_container.future.set_exception(BlueCurrentException) future_container.future.set_exception(WebsocketError)
await started_loop.wait() await started_loop.wait()
assert mock_client.connect.call_count == 2 assert mock_client.connect.call_count == 2
async def test_reconnect_websocket_error( async def test_connect_request_limit_reached_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(
hass: HomeAssistant, config_entry: MockConfigEntry hass: HomeAssistant, config_entry: MockConfigEntry
) -> None: ) -> None:
"""Test reconnect when connect throws a RequestLimitReached.""" """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( mock_client, started_loop, future_container = await init_integration(
hass, config_entry hass, config_entry
) )
future_container.future.set_exception(BlueCurrentException) future_container.future.set_exception(RequestLimitReached)
mock_client.connect.side_effect = [RequestLimitReached, None]
mock_client.get_next_reset_delta.return_value = timedelta(seconds=0) mock_client.get_next_reset_delta.return_value = timedelta(seconds=0)
await started_loop.wait() await started_loop.wait()
assert mock_client.get_next_reset_delta.call_count == 1 assert mock_client.get_next_reset_delta.call_count == 1
assert mock_client.connect.call_count == 3 assert mock_client.connect.call_count == 2