diff --git a/homeassistant/components/blue_current/__init__.py b/homeassistant/components/blue_current/__init__.py index 604f251bfeb..16b81c3c1e7 100644 --- a/homeassistant/components/blue_current/__init__.py +++ b/homeassistant/components/blue_current/__init__.py @@ -1,6 +1,7 @@ """The Blue Current integration.""" from __future__ import annotations +import asyncio from contextlib import suppress from datetime import datetime from typing import Any @@ -14,8 +15,13 @@ from bluecurrent_api.exceptions import ( ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_NAME, CONF_API_TOKEN, Platform -from homeassistant.core import HomeAssistant +from homeassistant.const import ( + ATTR_NAME, + CONF_API_TOKEN, + EVENT_HOMEASSISTANT_STOP, + Platform, +) +from homeassistant.core import Event, HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_call_later @@ -47,7 +53,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b except BlueCurrentException as err: raise ConfigEntryNotReady from err - hass.async_create_task(connector.start_loop()) + hass.async_create_background_task(connector.start_loop(), "blue_current-websocket") await client.get_charge_points() await client.wait_for_response() @@ -56,6 +62,11 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b 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 @@ -78,9 +89,9 @@ class Connector: self, hass: HomeAssistant, config: ConfigEntry, client: Client ) -> None: """Initialize.""" - self.config: ConfigEntry = config - self.hass: HomeAssistant = hass - self.client: Client = client + self.config = config + self.hass = hass + self.client = client self.charge_points: dict[str, dict] = {} self.grid: dict[str, Any] = {} self.available = False @@ -93,22 +104,12 @@ class Connector: async def on_data(self, message: dict) -> None: """Handle received data.""" - async def handle_charge_points(data: list) -> None: - """Loop over the charge points and get their data.""" - for entry in data: - evse_id = entry[EVSE_ID] - model = entry[MODEL_TYPE] - name = entry[ATTR_NAME] - self.add_charge_point(evse_id, model, name) - await self.get_charge_point_data(evse_id) - await self.client.get_grid_status(data[0][EVSE_ID]) - object_name: str = message[OBJECT] # gets charge point ids if object_name == CHARGE_POINTS: charge_points_data: list = message[DATA] - await handle_charge_points(charge_points_data) + await self.handle_charge_point_data(charge_points_data) # gets charge point key / values elif object_name in VALUE_TYPES: @@ -122,8 +123,21 @@ class Connector: self.grid = data self.dispatch_grid_update_signal() - async def get_charge_point_data(self, evse_id: str) -> None: - """Get all the data of a charge point.""" + async def handle_charge_point_data(self, charge_points_data: list) -> None: + """Handle incoming chargepoint data.""" + await asyncio.gather( + *( + self.handle_charge_point( + entry[EVSE_ID], entry[MODEL_TYPE], entry[ATTR_NAME] + ) + for entry in charge_points_data + ) + ) + await self.client.get_grid_status(charge_points_data[0][EVSE_ID]) + + async def handle_charge_point(self, evse_id: str, model: str, name: str) -> None: + """Add the chargepoint and request their data.""" + self.add_charge_point(evse_id, model, name) await self.client.get_status(evse_id) def add_charge_point(self, evse_id: str, model: str, name: str) -> None: @@ -159,9 +173,8 @@ class Connector: """Keep trying to reconnect to the websocket.""" try: await self.connect(self.config.data[CONF_API_TOKEN]) - LOGGER.info("Reconnected to the Blue Current websocket") + LOGGER.debug("Reconnected to the Blue Current websocket") self.hass.async_create_task(self.start_loop()) - await self.client.get_charge_points() except RequestLimitReached: self.available = False async_call_later( diff --git a/homeassistant/components/blue_current/entity.py b/homeassistant/components/blue_current/entity.py index 300f2191cdc..c797fec08b0 100644 --- a/homeassistant/components/blue_current/entity.py +++ b/homeassistant/components/blue_current/entity.py @@ -1,4 +1,6 @@ """Entity representing a Blue Current charge point.""" +from abc import abstractmethod + from homeassistant.const import ATTR_NAME from homeassistant.core import callback from homeassistant.helpers.device_registry import DeviceInfo @@ -17,9 +19,9 @@ class BlueCurrentEntity(Entity): def __init__(self, connector: Connector, signal: str) -> None: """Initialize the entity.""" - self.connector: Connector = connector - self.signal: str = signal - self.has_value: bool = False + self.connector = connector + self.signal = signal + self.has_value = False async def async_added_to_hass(self) -> None: """Register callbacks.""" @@ -40,9 +42,9 @@ class BlueCurrentEntity(Entity): return self.connector.available and self.has_value @callback + @abstractmethod def update_from_latest_data(self) -> None: """Update the entity from the latest data.""" - raise NotImplementedError class ChargepointEntity(BlueCurrentEntity): @@ -50,6 +52,8 @@ class ChargepointEntity(BlueCurrentEntity): def __init__(self, connector: Connector, evse_id: str) -> None: """Initialize the entity.""" + super().__init__(connector, f"{DOMAIN}_value_update_{evse_id}") + chargepoint_name = connector.charge_points[evse_id][ATTR_NAME] self.evse_id = evse_id @@ -59,5 +63,3 @@ class ChargepointEntity(BlueCurrentEntity): manufacturer="Blue Current", model=connector.charge_points[evse_id][MODEL_TYPE], ) - - super().__init__(connector, f"{DOMAIN}_value_update_{self.evse_id}") diff --git a/homeassistant/components/blue_current/strings.json b/homeassistant/components/blue_current/strings.json index 293d0cd6ab7..3ba6349b714 100644 --- a/homeassistant/components/blue_current/strings.json +++ b/homeassistant/components/blue_current/strings.json @@ -13,7 +13,6 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "limit_reached": "Request limit reached", "invalid_token": "Invalid token", - "no_cards_found": "No charge cards found", "already_connected": "Already connected", "unknown": "[%key:common::config_flow::error::unknown%]" }, diff --git a/tests/components/blue_current/__init__.py b/tests/components/blue_current/__init__.py index 901c776a894..63d8d084fae 100644 --- a/tests/components/blue_current/__init__.py +++ b/tests/components/blue_current/__init__.py @@ -1,52 +1,108 @@ """Tests for the Blue Current integration.""" from __future__ import annotations -from unittest.mock import patch +from asyncio import Event, Future +from dataclasses import dataclass +from unittest.mock import MagicMock, patch from bluecurrent_api import Client -from homeassistant.components.blue_current import DOMAIN, Connector -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.dispatcher import async_dispatcher_send from tests.common import MockConfigEntry +DEFAULT_CHARGE_POINT = { + "evse_id": "101", + "model_type": "", + "name": "", +} + + +@dataclass +class FutureContainer: + """Dataclass that stores a future.""" + + future: Future + + +def create_client_mock( + hass: HomeAssistant, + future_container: FutureContainer, + started_loop: Event, + charge_point: dict, + status: dict | None, + grid: dict | None, +) -> MagicMock: + """Create a mock of the bluecurrent-api Client.""" + client_mock = MagicMock(spec=Client) + + async def start_loop(receiver): + """Set the receiver and await future.""" + client_mock.receiver = receiver + + started_loop.set() + started_loop.clear() + + if future_container.future.done(): + future_container.future = hass.loop.create_future() + await future_container.future + + 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], + } + ) + + async def get_status(evse_id: str) -> None: + """Send the status of a charge point to the callback.""" + await client_mock.receiver( + { + "object": "CH_STATUS", + "data": {"evse_id": evse_id} | status, + } + ) + + async def get_grid_status(evse_id: str) -> None: + """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.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 + + return client_mock + async def init_integration( - hass: HomeAssistant, platform, data: dict, grid=None -) -> MockConfigEntry: + hass: HomeAssistant, + config_entry: MockConfigEntry, + platform="", + charge_point: dict | None = None, + status: dict | None = None, + grid: dict | None = None, +) -> tuple[MagicMock, Event, FutureContainer]: """Set up the Blue Current integration in Home Assistant.""" - if grid is None: - grid = {} + if charge_point is None: + charge_point = DEFAULT_CHARGE_POINT - def init( - self: Connector, hass: HomeAssistant, config: ConfigEntry, client: Client - ) -> None: - """Mock grid and charge_points.""" + future_container = FutureContainer(hass.loop.create_future()) + started_loop = Event() - self.config = config - self.hass = hass - self.client = client - self.charge_points = data - self.grid = grid - self.available = True + client_mock = create_client_mock( + hass, future_container, started_loop, charge_point, status, grid + ) - with patch( - "homeassistant.components.blue_current.PLATFORMS", [platform] - ), patch.object(Connector, "__init__", init), patch( - "homeassistant.components.blue_current.Client", autospec=True + with patch("homeassistant.components.blue_current.PLATFORMS", [platform]), patch( + "homeassistant.components.blue_current.Client", return_value=client_mock ): - config_entry = MockConfigEntry( - domain=DOMAIN, - entry_id="uuid", - unique_id="uuid", - data={"api_token": "123", "card": {"123"}}, - ) config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - async_dispatcher_send(hass, "blue_current_value_update_101") - return config_entry + return client_mock, started_loop, future_container diff --git a/tests/components/blue_current/conftest.py b/tests/components/blue_current/conftest.py new file mode 100644 index 00000000000..da57fa72cd5 --- /dev/null +++ b/tests/components/blue_current/conftest.py @@ -0,0 +1,18 @@ +"""Define test fixtures for Blue Current.""" + +import pytest + +from homeassistant.components.blue_current.const import DOMAIN + +from tests.common import MockConfigEntry + + +@pytest.fixture(name="config_entry") +def config_entry_fixture() -> MockConfigEntry: + """Define a config entry fixture.""" + return MockConfigEntry( + domain=DOMAIN, + entry_id="uuid", + unique_id="1234", + data={"api_token": "123"}, + ) diff --git a/tests/components/blue_current/test_config_flow.py b/tests/components/blue_current/test_config_flow.py index 057701235ad..b34decd8264 100644 --- a/tests/components/blue_current/test_config_flow.py +++ b/tests/components/blue_current/test_config_flow.py @@ -23,6 +23,7 @@ async def test_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["errors"] == {} + assert result["type"] == FlowResultType.FORM async def test_user(hass: HomeAssistant) -> None: @@ -32,6 +33,7 @@ async def test_user(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["errors"] == {} + assert result["type"] == FlowResultType.FORM with patch( "homeassistant.components.blue_current.config_flow.Client.validate_api_token", @@ -53,6 +55,7 @@ async def test_user(hass: HomeAssistant) -> None: assert result2["title"] == "test@email.com" assert result2["data"] == {"api_token": "123"} + assert result2["type"] == FlowResultType.CREATE_ENTRY @pytest.mark.parametrize( @@ -77,6 +80,7 @@ async def test_flow_fails(hass: HomeAssistant, error: Exception, message: str) - data={"api_token": "123"}, ) assert result["errors"]["base"] == message + assert result["type"] == FlowResultType.FORM with patch( "homeassistant.components.blue_current.config_flow.Client.validate_api_token", @@ -98,6 +102,7 @@ async def test_flow_fails(hass: HomeAssistant, error: Exception, message: str) - assert result2["title"] == "test@email.com" assert result2["data"] == {"api_token": "123"} + assert result2["type"] == FlowResultType.CREATE_ENTRY @pytest.mark.parametrize( @@ -108,7 +113,11 @@ async def test_flow_fails(hass: HomeAssistant, error: Exception, message: str) - ], ) async def test_reauth( - hass: HomeAssistant, customer_id: str, reason: str, expected_api_token: str + hass: HomeAssistant, + config_entry: MockConfigEntry, + customer_id: str, + reason: str, + expected_api_token: str, ) -> None: """Test reauth flow.""" with patch( @@ -118,19 +127,13 @@ async def test_reauth( "homeassistant.components.blue_current.config_flow.Client.get_email", return_value="test@email.com", ): - entry = MockConfigEntry( - domain=DOMAIN, - entry_id="uuid", - unique_id="1234", - data={"api_token": "123"}, - ) - entry.add_to_hass(hass) + config_entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, context={ "source": config_entries.SOURCE_REAUTH, - "entry_id": entry.entry_id, - "unique_id": entry.unique_id, + "entry_id": config_entry.entry_id, + "unique_id": config_entry.unique_id, }, data={"api_token": "123"}, ) @@ -144,6 +147,6 @@ async def test_reauth( ) assert result["type"] == FlowResultType.ABORT assert result["reason"] == reason - assert entry.data == {"api_token": expected_api_token} + assert config_entry.data["api_token"] == expected_api_token await hass.async_block_till_done() diff --git a/tests/components/blue_current/test_init.py b/tests/components/blue_current/test_init.py index 14bd055cd45..ce6eb2d9716 100644 --- a/tests/components/blue_current/test_init.py +++ b/tests/components/blue_current/test_init.py @@ -1,9 +1,7 @@ """Test Blue Current Init Component.""" - from datetime import timedelta from unittest.mock import patch -from bluecurrent_api.client import Client from bluecurrent_api.exceptions import ( BlueCurrentException, InvalidApiToken, @@ -12,7 +10,7 @@ from bluecurrent_api.exceptions import ( ) import pytest -from homeassistant.components.blue_current import DOMAIN, Connector, async_setup_entry +from homeassistant.components.blue_current import async_setup_entry from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.exceptions import ( @@ -26,16 +24,19 @@ from . import init_integration from tests.common import MockConfigEntry -async def test_load_unload_entry(hass: HomeAssistant) -> None: +async def test_load_unload_entry( + hass: HomeAssistant, config_entry: MockConfigEntry +) -> None: """Test load and unload entry.""" - config_entry = await init_integration(hass, "sensor", {}) + with patch("homeassistant.components.blue_current.Client", autospec=True): + 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 isinstance(hass.data[DOMAIN][config_entry.entry_id], Connector) await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() assert config_entry.state == ConfigEntryState.NOT_LOADED - assert hass.data[DOMAIN] == {} @pytest.mark.parametrize( @@ -46,176 +47,61 @@ async def test_load_unload_entry(hass: HomeAssistant) -> None: ], ) async def test_config_exceptions( - hass: HomeAssistant, api_error: BlueCurrentException, config_error: IntegrationError + hass: HomeAssistant, + config_entry: MockConfigEntry, + api_error: BlueCurrentException, + config_error: IntegrationError, ) -> None: - """Tests 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( "homeassistant.components.blue_current.Client.connect", side_effect=api_error, ), pytest.raises(config_error): - config_entry = MockConfigEntry( - domain=DOMAIN, - entry_id="uuid", - unique_id="uuid", - data={"api_token": "123", "card": {"123"}}, - ) config_entry.add_to_hass(hass) - await async_setup_entry(hass, config_entry) -async def test_on_data(hass: HomeAssistant) -> None: - """Test on_data.""" +async def test_start_loop(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: + """Test start_loop.""" - await init_integration(hass, "sensor", {}) - - with patch( - "homeassistant.components.blue_current.async_dispatcher_send" - ) as test_async_dispatcher_send: - connector: Connector = hass.data[DOMAIN]["uuid"] - - # test CHARGE_POINTS - data = { - "object": "CHARGE_POINTS", - "data": [{"evse_id": "101", "model_type": "hidden", "name": ""}], - } - await connector.on_data(data) - assert connector.charge_points == {"101": {"model_type": "hidden", "name": ""}} - - # test CH_STATUS - data2 = { - "object": "CH_STATUS", - "data": { - "actual_v1": 12, - "actual_v2": 14, - "actual_v3": 15, - "actual_p1": 12, - "actual_p2": 14, - "actual_p3": 15, - "activity": "charging", - "start_datetime": "2021-11-18T14:12:23", - "stop_datetime": "2021-11-18T14:32:23", - "offline_since": "2021-11-18T14:32:23", - "total_cost": 10.52, - "vehicle_status": "standby", - "actual_kwh": 10, - "evse_id": "101", - }, - } - await connector.on_data(data2) - assert connector.charge_points == { - "101": { - "model_type": "hidden", - "name": "", - "actual_v1": 12, - "actual_v2": 14, - "actual_v3": 15, - "actual_p1": 12, - "actual_p2": 14, - "actual_p3": 15, - "activity": "charging", - "start_datetime": "2021-11-18T14:12:23", - "stop_datetime": "2021-11-18T14:32:23", - "offline_since": "2021-11-18T14:32:23", - "total_cost": 10.52, - "vehicle_status": "standby", - "actual_kwh": 10, - } - } - - test_async_dispatcher_send.assert_called_with( - hass, "blue_current_value_update_101" + with patch("homeassistant.components.blue_current.SMALL_DELAY", 0): + mock_client, started_loop, future_container = await init_integration( + hass, config_entry ) + future_container.future.set_exception(BlueCurrentException) - # test GRID_STATUS - data3 = { - "object": "GRID_STATUS", - "data": { - "grid_actual_p1": 12, - "grid_actual_p2": 14, - "grid_actual_p3": 15, - }, - } - await connector.on_data(data3) - assert connector.grid == { - "grid_actual_p1": 12, - "grid_actual_p2": 14, - "grid_actual_p3": 15, - } - test_async_dispatcher_send.assert_called_with(hass, "blue_current_grid_update") + await started_loop.wait() + assert mock_client.connect.call_count == 2 -async def test_start_loop(hass: HomeAssistant) -> None: - """Tests start_loop.""" +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.async_call_later" - ) as test_async_call_later: - config_entry = MockConfigEntry( - domain=DOMAIN, - entry_id="uuid", - unique_id="uuid", - data={"api_token": "123", "card": {"123"}}, + 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] - connector = Connector(hass, config_entry, Client) - - with patch( - "homeassistant.components.blue_current.Client.start_loop", - side_effect=WebsocketError("unknown command"), - ): - await connector.start_loop() - test_async_call_later.assert_called_with(hass, 1, connector.reconnect) - - with patch( - "homeassistant.components.blue_current.Client.start_loop", - side_effect=RequestLimitReached, - ): - await connector.start_loop() - test_async_call_later.assert_called_with(hass, 1, connector.reconnect) + await started_loop.wait() + assert mock_client.connect.call_count == 3 -async def test_reconnect(hass: HomeAssistant) -> None: - """Tests reconnect.""" +async def test_reconnect_request_limit_reached_error( + hass: HomeAssistant, config_entry: MockConfigEntry +) -> None: + """Test reconnect when connect throws a RequestLimitReached.""" - with patch( - "homeassistant.components.blue_current.async_call_later" - ) as test_async_call_later: - config_entry = MockConfigEntry( - domain=DOMAIN, - entry_id="uuid", - unique_id="uuid", - data={"api_token": "123", "card": {"123"}}, - ) + 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] + mock_client.get_next_reset_delta.return_value = timedelta(seconds=0) - connector = Connector(hass, config_entry, Client) - - with patch( - "homeassistant.components.blue_current.Client.connect", - side_effect=WebsocketError, - ): - await connector.reconnect() - - test_async_call_later.assert_called_with(hass, 20, connector.reconnect) - - with patch( - "homeassistant.components.blue_current.Client.connect", - side_effect=RequestLimitReached, - ), patch( - "homeassistant.components.blue_current.Client.get_next_reset_delta", - return_value=timedelta(hours=1), - ): - await connector.reconnect() - - test_async_call_later.assert_called_with( - hass, timedelta(hours=1), connector.reconnect - ) - - with patch("homeassistant.components.blue_current.Client.connect"), patch( - "homeassistant.components.blue_current.Connector.start_loop" - ) as test_start_loop, patch( - "homeassistant.components.blue_current.Client.get_charge_points" - ) as test_get_charge_points: - await connector.reconnect() - test_start_loop.assert_called_once() - test_get_charge_points.assert_called_once() + await started_loop.wait() + assert mock_client.get_next_reset_delta.call_count == 1 + assert mock_client.connect.call_count == 3 diff --git a/tests/components/blue_current/test_sensor.py b/tests/components/blue_current/test_sensor.py index a4bcbfcda00..68b42498c2f 100644 --- a/tests/components/blue_current/test_sensor.py +++ b/tests/components/blue_current/test_sensor.py @@ -1,18 +1,23 @@ """The tests for Blue current sensors.""" from datetime import datetime -from typing import Any -from homeassistant.components.blue_current import Connector +import pytest + from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.dispatcher import async_dispatcher_send from . import init_integration -TIMESTAMP_KEYS = ("start_datetime", "stop_datetime", "offline_since") - +from tests.common import MockConfigEntry charge_point = { + "evse_id": "101", + "model_type": "", + "name": "", +} + + +charge_point_status = { "actual_v1": 14, "actual_v2": 18, "actual_v3": 15, @@ -20,9 +25,6 @@ charge_point = { "actual_p2": 14, "actual_p3": 15, "activity": "available", - "start_datetime": datetime.strptime("20211118 14:12:23+08:00", "%Y%m%d %H:%M:%S%z"), - "stop_datetime": datetime.strptime("20211118 14:32:23+00:00", "%Y%m%d %H:%M:%S%z"), - "offline_since": datetime.strptime("20211118 14:32:23+00:00", "%Y%m%d %H:%M:%S%z"), "total_cost": 13.32, "avg_current": 16, "avg_voltage": 15.7, @@ -35,16 +37,12 @@ charge_point = { "current_left": 10, } -data: dict[str, Any] = { - "101": { - "model_type": "hidden", - "evse_id": "101", - "name": "", - **charge_point, - } +charge_point_status_timestamps = { + "start_datetime": datetime.strptime("20211118 14:12:23+08:00", "%Y%m%d %H:%M:%S%z"), + "stop_datetime": datetime.strptime("20211118 14:32:23+00:00", "%Y%m%d %H:%M:%S%z"), + "offline_since": datetime.strptime("20211118 14:32:23+00:00", "%Y%m%d %H:%M:%S%z"), } - charge_point_entity_ids = { "voltage_phase_1": "actual_v1", "voltage_phase_2": "actual_v2", @@ -53,9 +51,6 @@ charge_point_entity_ids = { "current_phase_2": "actual_p2", "current_phase_3": "actual_p3", "activity": "activity", - "started_on": "start_datetime", - "stopped_on": "stop_datetime", - "offline_since": "offline_since", "total_cost": "total_cost", "average_current": "avg_current", "average_voltage": "avg_voltage", @@ -68,6 +63,12 @@ charge_point_entity_ids = { "remaining_current": "current_left", } +charge_point_timestamp_entity_ids = { + "started_on": "start_datetime", + "stopped_on": "stop_datetime", + "offline_since": "offline_since", +} + grid = { "grid_actual_p1": 12, "grid_actual_p2": 14, @@ -85,9 +86,33 @@ grid_entity_ids = { } -async def test_sensors(hass: HomeAssistant) -> None: +async def test_sensors_created( + hass: HomeAssistant, config_entry: MockConfigEntry +) -> None: + """Test if all sensors are created.""" + await init_integration( + hass, + config_entry, + "sensor", + charge_point, + charge_point_status | charge_point_status_timestamps, + grid, + ) + + entity_registry = er.async_get(hass) + + sensors = er.async_entries_for_config_entry(entity_registry, "uuid") + assert len(charge_point_status) + len(charge_point_status_timestamps) + len( + grid + ) == len(sensors) + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_sensors(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: """Test the underlying sensors.""" - await init_integration(hass, "sensor", data, grid) + await init_integration( + hass, config_entry, "sensor", charge_point, charge_point_status, grid + ) entity_registry = er.async_get(hass) for entity_id, key in charge_point_entity_ids.items(): @@ -95,59 +120,83 @@ async def test_sensors(hass: HomeAssistant) -> None: assert entry assert entry.unique_id == f"{key}_101" - # skip sensors that are disabled by default. - if not entry.disabled: - state = hass.states.get(f"sensor.101_{entity_id}") - assert state is not None + state = hass.states.get(f"sensor.101_{entity_id}") + assert state is not None - value = charge_point[key] - - if key in TIMESTAMP_KEYS: - assert datetime.strptime(state.state, "%Y-%m-%dT%H:%M:%S%z") == value - else: - assert state.state == str(value) + value = charge_point_status[key] + assert state.state == str(value) for entity_id, key in grid_entity_ids.items(): entry = entity_registry.async_get(f"sensor.{entity_id}") assert entry assert entry.unique_id == key - # skip sensors that are disabled by default. - if not entry.disabled: - state = hass.states.get(f"sensor.{entity_id}") - assert state is not None - assert state.state == str(grid[key]) - - sensors = er.async_entries_for_config_entry(entity_registry, "uuid") - assert len(charge_point.keys()) + len(grid.keys()) == len(sensors) + state = hass.states.get(f"sensor.{entity_id}") + assert state is not None + assert state.state == str(grid[key]) -async def test_sensor_update(hass: HomeAssistant) -> None: +async def test_timestamp_sensors( + hass: HomeAssistant, config_entry: MockConfigEntry +) -> None: + """Test the underlying sensors.""" + await init_integration( + hass, config_entry, "sensor", status=charge_point_status_timestamps + ) + + entity_registry = er.async_get(hass) + for entity_id, key in charge_point_timestamp_entity_ids.items(): + entry = entity_registry.async_get(f"sensor.101_{entity_id}") + assert entry + assert entry.unique_id == f"{key}_101" + + state = hass.states.get(f"sensor.101_{entity_id}") + assert state is not None + + value = charge_point_status_timestamps[key] + assert datetime.strptime(state.state, "%Y-%m-%dT%H:%M:%S%z") == value + + +async def test_sensor_update( + hass: HomeAssistant, config_entry: MockConfigEntry +) -> None: """Test if the sensors get updated when there is new data.""" - await init_integration(hass, "sensor", data, grid) - key = "avg_voltage" - entity_id = "average_voltage" - timestamp_key = "start_datetime" - timestamp_entity_id = "started_on" - grid_key = "grid_avg_current" - grid_entity_id = "average_grid_current" + client, _, _ = await init_integration( + hass, + config_entry, + "sensor", + status=charge_point_status | charge_point_status_timestamps, + grid=grid, + ) - connector: Connector = hass.data["blue_current"]["uuid"] - - connector.charge_points = {"101": {key: 20, timestamp_key: None}} - connector.grid = {grid_key: 20} - async_dispatcher_send(hass, "blue_current_value_update_101") + await client.receiver( + { + "object": "CH_STATUS", + "data": { + "evse_id": "101", + "avg_voltage": 20, + "start_datetime": None, + "actual_kwh": None, + }, + } + ) await hass.async_block_till_done() - async_dispatcher_send(hass, "blue_current_grid_update") + + await client.receiver( + { + "object": "GRID_STATUS", + "data": {"grid_avg_current": 20}, + } + ) await hass.async_block_till_done() # test data updated - state = hass.states.get(f"sensor.101_{entity_id}") + state = hass.states.get("sensor.101_average_voltage") assert state is not None assert state.state == str(20) # grid - state = hass.states.get(f"sensor.{grid_entity_id}") + state = hass.states.get("sensor.average_grid_current") assert state assert state.state == str(20) @@ -157,25 +206,30 @@ async def test_sensor_update(hass: HomeAssistant) -> None: assert state.state == "unavailable" # test if timestamp keeps old value - state = hass.states.get(f"sensor.101_{timestamp_entity_id}") + state = hass.states.get("sensor.101_started_on") assert state assert ( datetime.strptime(state.state, "%Y-%m-%dT%H:%M:%S%z") - == charge_point[timestamp_key] + == charge_point_status_timestamps["start_datetime"] ) # test if older timestamp is ignored - connector.charge_points = { - "101": { - timestamp_key: datetime.strptime( - "20211118 14:11:23+08:00", "%Y%m%d %H:%M:%S%z" - ) + await client.receiver( + { + "object": "CH_STATUS", + "data": { + "evse_id": "101", + "start_datetime": datetime.strptime( + "20211118 14:11:23+08:00", "%Y%m%d %H:%M:%S%z" + ), + }, } - } - async_dispatcher_send(hass, "blue_current_value_update_101") - state = hass.states.get(f"sensor.101_{timestamp_entity_id}") + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.101_started_on") assert state assert ( datetime.strptime(state.state, "%Y-%m-%dT%H:%M:%S%z") - == charge_point[timestamp_key] + == charge_point_status_timestamps["start_datetime"] )