Implement late feedback for Bluecurrent (#106918)

* Apply changes

* Fix MockClient

* Apply feedback

* Remove connector tests

* Change MockClient to inhert MagicMock

* Add reconnect tests and refactor mock client

* Refactor mock exception throwing

* Add future_fixture

* Move mocked client methods into create_client_mock

* Remove fixture and separate event from mock_client

* Add FutureContainer to store the loop_future
This commit is contained in:
Floris272 2024-02-11 20:57:38 +01:00 committed by GitHub
parent 654ab54aa0
commit 7dc9ad63bd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 325 additions and 294 deletions

View File

@ -1,6 +1,7 @@
"""The Blue Current integration.""" """The Blue Current integration."""
from __future__ import annotations from __future__ import annotations
import asyncio
from contextlib import suppress from contextlib import suppress
from datetime import datetime from datetime import datetime
from typing import Any from typing import Any
@ -14,8 +15,13 @@ from bluecurrent_api.exceptions import (
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_NAME, CONF_API_TOKEN, Platform from homeassistant.const import (
from homeassistant.core import HomeAssistant ATTR_NAME,
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 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: except BlueCurrentException as err:
raise ConfigEntryNotReady from 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.get_charge_points()
await client.wait_for_response() 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) 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
@ -78,9 +89,9 @@ class Connector:
self, hass: HomeAssistant, config: ConfigEntry, client: Client self, hass: HomeAssistant, config: ConfigEntry, client: Client
) -> None: ) -> None:
"""Initialize.""" """Initialize."""
self.config: ConfigEntry = config self.config = config
self.hass: HomeAssistant = hass self.hass = hass
self.client: 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 self.available = False
@ -93,22 +104,12 @@ class Connector:
async def on_data(self, message: dict) -> None: async def on_data(self, message: dict) -> None:
"""Handle received data.""" """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] object_name: str = message[OBJECT]
# gets charge point ids # gets charge point ids
if object_name == CHARGE_POINTS: if object_name == CHARGE_POINTS:
charge_points_data: list = message[DATA] 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 # gets charge point key / values
elif object_name in VALUE_TYPES: elif object_name in VALUE_TYPES:
@ -122,8 +123,21 @@ class Connector:
self.grid = data self.grid = data
self.dispatch_grid_update_signal() self.dispatch_grid_update_signal()
async def get_charge_point_data(self, evse_id: str) -> None: async def handle_charge_point_data(self, charge_points_data: list) -> None:
"""Get all the data of a charge point.""" """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) await self.client.get_status(evse_id)
def add_charge_point(self, evse_id: str, model: str, name: str) -> None: 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.""" """Keep trying to reconnect to the websocket."""
try: try:
await self.connect(self.config.data[CONF_API_TOKEN]) 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()) self.hass.async_create_task(self.start_loop())
await self.client.get_charge_points()
except RequestLimitReached: except RequestLimitReached:
self.available = False self.available = False
async_call_later( async_call_later(

View File

@ -1,4 +1,6 @@
"""Entity representing a Blue Current charge point.""" """Entity representing a Blue Current charge point."""
from abc import abstractmethod
from homeassistant.const import ATTR_NAME from homeassistant.const import ATTR_NAME
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
@ -17,9 +19,9 @@ class BlueCurrentEntity(Entity):
def __init__(self, connector: Connector, signal: str) -> None: def __init__(self, connector: Connector, signal: str) -> None:
"""Initialize the entity.""" """Initialize the entity."""
self.connector: Connector = connector self.connector = connector
self.signal: str = signal self.signal = signal
self.has_value: bool = False self.has_value = False
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Register callbacks.""" """Register callbacks."""
@ -40,9 +42,9 @@ class BlueCurrentEntity(Entity):
return self.connector.available and self.has_value return self.connector.available and self.has_value
@callback @callback
@abstractmethod
def update_from_latest_data(self) -> None: def update_from_latest_data(self) -> None:
"""Update the entity from the latest data.""" """Update the entity from the latest data."""
raise NotImplementedError
class ChargepointEntity(BlueCurrentEntity): class ChargepointEntity(BlueCurrentEntity):
@ -50,6 +52,8 @@ class ChargepointEntity(BlueCurrentEntity):
def __init__(self, connector: Connector, evse_id: str) -> None: def __init__(self, connector: Connector, evse_id: str) -> None:
"""Initialize the entity.""" """Initialize the entity."""
super().__init__(connector, f"{DOMAIN}_value_update_{evse_id}")
chargepoint_name = connector.charge_points[evse_id][ATTR_NAME] chargepoint_name = connector.charge_points[evse_id][ATTR_NAME]
self.evse_id = evse_id self.evse_id = evse_id
@ -59,5 +63,3 @@ class ChargepointEntity(BlueCurrentEntity):
manufacturer="Blue Current", manufacturer="Blue Current",
model=connector.charge_points[evse_id][MODEL_TYPE], model=connector.charge_points[evse_id][MODEL_TYPE],
) )
super().__init__(connector, f"{DOMAIN}_value_update_{self.evse_id}")

View File

@ -13,7 +13,6 @@
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"limit_reached": "Request limit reached", "limit_reached": "Request limit reached",
"invalid_token": "Invalid token", "invalid_token": "Invalid token",
"no_cards_found": "No charge cards found",
"already_connected": "Already connected", "already_connected": "Already connected",
"unknown": "[%key:common::config_flow::error::unknown%]" "unknown": "[%key:common::config_flow::error::unknown%]"
}, },

View File

@ -1,52 +1,108 @@
"""Tests for the Blue Current integration.""" """Tests for the Blue Current integration."""
from __future__ import annotations 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 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.core import HomeAssistant
from homeassistant.helpers.dispatcher import async_dispatcher_send
from tests.common import MockConfigEntry 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( async def init_integration(
hass: HomeAssistant, platform, data: dict, grid=None hass: HomeAssistant,
) -> MockConfigEntry: 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.""" """Set up the Blue Current integration in Home Assistant."""
if grid is None: if charge_point is None:
grid = {} charge_point = DEFAULT_CHARGE_POINT
def init( future_container = FutureContainer(hass.loop.create_future())
self: Connector, hass: HomeAssistant, config: ConfigEntry, client: Client started_loop = Event()
) -> None:
"""Mock grid and charge_points."""
self.config = config client_mock = create_client_mock(
self.hass = hass hass, future_container, started_loop, charge_point, status, grid
self.client = client
self.charge_points = data
self.grid = grid
self.available = True
with patch(
"homeassistant.components.blue_current.PLATFORMS", [platform]
), patch.object(Connector, "__init__", init), patch(
"homeassistant.components.blue_current.Client", autospec=True
):
config_entry = MockConfigEntry(
domain=DOMAIN,
entry_id="uuid",
unique_id="uuid",
data={"api_token": "123", "card": {"123"}},
) )
with patch("homeassistant.components.blue_current.PLATFORMS", [platform]), patch(
"homeassistant.components.blue_current.Client", return_value=client_mock
):
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()
async_dispatcher_send(hass, "blue_current_value_update_101") return client_mock, started_loop, future_container
return config_entry

View File

@ -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"},
)

View File

@ -23,6 +23,7 @@ async def test_form(hass: HomeAssistant) -> None:
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
) )
assert result["errors"] == {} assert result["errors"] == {}
assert result["type"] == FlowResultType.FORM
async def test_user(hass: HomeAssistant) -> None: 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} DOMAIN, context={"source": config_entries.SOURCE_USER}
) )
assert result["errors"] == {} assert result["errors"] == {}
assert result["type"] == FlowResultType.FORM
with patch( with patch(
"homeassistant.components.blue_current.config_flow.Client.validate_api_token", "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["title"] == "test@email.com"
assert result2["data"] == {"api_token": "123"} assert result2["data"] == {"api_token": "123"}
assert result2["type"] == FlowResultType.CREATE_ENTRY
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -77,6 +80,7 @@ async def test_flow_fails(hass: HomeAssistant, error: Exception, message: str) -
data={"api_token": "123"}, data={"api_token": "123"},
) )
assert result["errors"]["base"] == message assert result["errors"]["base"] == message
assert result["type"] == FlowResultType.FORM
with patch( with patch(
"homeassistant.components.blue_current.config_flow.Client.validate_api_token", "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["title"] == "test@email.com"
assert result2["data"] == {"api_token": "123"} assert result2["data"] == {"api_token": "123"}
assert result2["type"] == FlowResultType.CREATE_ENTRY
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -108,7 +113,11 @@ async def test_flow_fails(hass: HomeAssistant, error: Exception, message: str) -
], ],
) )
async def test_reauth( 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: ) -> None:
"""Test reauth flow.""" """Test reauth flow."""
with patch( with patch(
@ -118,19 +127,13 @@ async def test_reauth(
"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",
): ):
entry = MockConfigEntry( config_entry.add_to_hass(hass)
domain=DOMAIN,
entry_id="uuid",
unique_id="1234",
data={"api_token": "123"},
)
entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
context={ context={
"source": config_entries.SOURCE_REAUTH, "source": config_entries.SOURCE_REAUTH,
"entry_id": entry.entry_id, "entry_id": config_entry.entry_id,
"unique_id": entry.unique_id, "unique_id": config_entry.unique_id,
}, },
data={"api_token": "123"}, data={"api_token": "123"},
) )
@ -144,6 +147,6 @@ async def test_reauth(
) )
assert result["type"] == FlowResultType.ABORT assert result["type"] == FlowResultType.ABORT
assert result["reason"] == reason 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() await hass.async_block_till_done()

View File

@ -1,9 +1,7 @@
"""Test Blue Current Init Component.""" """Test Blue Current Init Component."""
from datetime import timedelta from datetime import timedelta
from unittest.mock import patch from unittest.mock import patch
from bluecurrent_api.client import Client
from bluecurrent_api.exceptions import ( from bluecurrent_api.exceptions import (
BlueCurrentException, BlueCurrentException,
InvalidApiToken, InvalidApiToken,
@ -12,7 +10,7 @@ from bluecurrent_api.exceptions import (
) )
import pytest 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.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ( from homeassistant.exceptions import (
@ -26,16 +24,19 @@ from . import init_integration
from tests.common import MockConfigEntry 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.""" """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 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.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
assert hass.data[DOMAIN] == {}
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -46,176 +47,61 @@ async def test_load_unload_entry(hass: HomeAssistant) -> None:
], ],
) )
async def test_config_exceptions( 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: ) -> 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( with patch(
"homeassistant.components.blue_current.Client.connect", "homeassistant.components.blue_current.Client.connect",
side_effect=api_error, side_effect=api_error,
), pytest.raises(config_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) config_entry.add_to_hass(hass)
await async_setup_entry(hass, config_entry) await async_setup_entry(hass, config_entry)
async def test_on_data(hass: HomeAssistant) -> None: async def test_start_loop(hass: HomeAssistant, config_entry: MockConfigEntry) -> None:
"""Test on_data.""" """Test start_loop."""
await init_integration(hass, "sensor", {}) with patch("homeassistant.components.blue_current.SMALL_DELAY", 0):
mock_client, started_loop, future_container = await init_integration(
with patch( hass, config_entry
"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"
) )
future_container.future.set_exception(BlueCurrentException)
# test GRID_STATUS await started_loop.wait()
data3 = { assert mock_client.connect.call_count == 2
"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")
async def test_start_loop(hass: HomeAssistant) -> None: async def test_reconnect_websocket_error(
"""Tests start_loop.""" hass: HomeAssistant, config_entry: MockConfigEntry
) -> None:
"""Test reconnect when connect throws a WebsocketError."""
with patch( with patch("homeassistant.components.blue_current.LARGE_DELAY", 0):
"homeassistant.components.blue_current.async_call_later" mock_client, started_loop, future_container = await init_integration(
) as test_async_call_later: hass, config_entry
config_entry = MockConfigEntry(
domain=DOMAIN,
entry_id="uuid",
unique_id="uuid",
data={"api_token": "123", "card": {"123"}},
) )
future_container.future.set_exception(BlueCurrentException)
mock_client.connect.side_effect = [WebsocketError, None]
connector = Connector(hass, config_entry, Client) await started_loop.wait()
assert mock_client.connect.call_count == 3
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)
async def test_reconnect(hass: HomeAssistant) -> None: async def test_reconnect_request_limit_reached_error(
"""Tests reconnect.""" hass: HomeAssistant, config_entry: MockConfigEntry
) -> None:
"""Test reconnect when connect throws a RequestLimitReached."""
with patch( mock_client, started_loop, future_container = await init_integration(
"homeassistant.components.blue_current.async_call_later" hass, config_entry
) as test_async_call_later:
config_entry = MockConfigEntry(
domain=DOMAIN,
entry_id="uuid",
unique_id="uuid",
data={"api_token": "123", "card": {"123"}},
) )
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) await started_loop.wait()
assert mock_client.get_next_reset_delta.call_count == 1
with patch( assert mock_client.connect.call_count == 3
"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()

View File

@ -1,18 +1,23 @@
"""The tests for Blue current sensors.""" """The tests for Blue current sensors."""
from datetime import datetime from datetime import datetime
from typing import Any
from homeassistant.components.blue_current import Connector import pytest
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.dispatcher import async_dispatcher_send
from . import init_integration from . import init_integration
TIMESTAMP_KEYS = ("start_datetime", "stop_datetime", "offline_since") from tests.common import MockConfigEntry
charge_point = { charge_point = {
"evse_id": "101",
"model_type": "",
"name": "",
}
charge_point_status = {
"actual_v1": 14, "actual_v1": 14,
"actual_v2": 18, "actual_v2": 18,
"actual_v3": 15, "actual_v3": 15,
@ -20,9 +25,6 @@ charge_point = {
"actual_p2": 14, "actual_p2": 14,
"actual_p3": 15, "actual_p3": 15,
"activity": "available", "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, "total_cost": 13.32,
"avg_current": 16, "avg_current": 16,
"avg_voltage": 15.7, "avg_voltage": 15.7,
@ -35,15 +37,11 @@ charge_point = {
"current_left": 10, "current_left": 10,
} }
data: dict[str, Any] = { charge_point_status_timestamps = {
"101": { "start_datetime": datetime.strptime("20211118 14:12:23+08:00", "%Y%m%d %H:%M:%S%z"),
"model_type": "hidden", "stop_datetime": datetime.strptime("20211118 14:32:23+00:00", "%Y%m%d %H:%M:%S%z"),
"evse_id": "101", "offline_since": datetime.strptime("20211118 14:32:23+00:00", "%Y%m%d %H:%M:%S%z"),
"name": "",
**charge_point,
} }
}
charge_point_entity_ids = { charge_point_entity_ids = {
"voltage_phase_1": "actual_v1", "voltage_phase_1": "actual_v1",
@ -53,9 +51,6 @@ charge_point_entity_ids = {
"current_phase_2": "actual_p2", "current_phase_2": "actual_p2",
"current_phase_3": "actual_p3", "current_phase_3": "actual_p3",
"activity": "activity", "activity": "activity",
"started_on": "start_datetime",
"stopped_on": "stop_datetime",
"offline_since": "offline_since",
"total_cost": "total_cost", "total_cost": "total_cost",
"average_current": "avg_current", "average_current": "avg_current",
"average_voltage": "avg_voltage", "average_voltage": "avg_voltage",
@ -68,6 +63,12 @@ charge_point_entity_ids = {
"remaining_current": "current_left", "remaining_current": "current_left",
} }
charge_point_timestamp_entity_ids = {
"started_on": "start_datetime",
"stopped_on": "stop_datetime",
"offline_since": "offline_since",
}
grid = { grid = {
"grid_actual_p1": 12, "grid_actual_p1": 12,
"grid_actual_p2": 14, "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.""" """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) entity_registry = er.async_get(hass)
for entity_id, key in charge_point_entity_ids.items(): for entity_id, key in charge_point_entity_ids.items():
@ -95,16 +120,10 @@ async def test_sensors(hass: HomeAssistant) -> None:
assert entry assert entry
assert entry.unique_id == f"{key}_101" 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}") state = hass.states.get(f"sensor.101_{entity_id}")
assert state is not None assert state is not None
value = charge_point[key] value = charge_point_status[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) assert state.state == str(value)
for entity_id, key in grid_entity_ids.items(): for entity_id, key in grid_entity_ids.items():
@ -112,42 +131,72 @@ async def test_sensors(hass: HomeAssistant) -> None:
assert entry assert entry
assert entry.unique_id == key assert entry.unique_id == key
# skip sensors that are disabled by default.
if not entry.disabled:
state = hass.states.get(f"sensor.{entity_id}") state = hass.states.get(f"sensor.{entity_id}")
assert state is not None assert state is not None
assert state.state == str(grid[key]) 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) 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) -> None: async def test_sensor_update(
hass: HomeAssistant, config_entry: MockConfigEntry
) -> None:
"""Test if the sensors get updated when there is new data.""" """Test if the sensors get updated when there is new data."""
await init_integration(hass, "sensor", data, grid) client, _, _ = await init_integration(
key = "avg_voltage" hass,
entity_id = "average_voltage" config_entry,
timestamp_key = "start_datetime" "sensor",
timestamp_entity_id = "started_on" status=charge_point_status | charge_point_status_timestamps,
grid_key = "grid_avg_current" grid=grid,
grid_entity_id = "average_grid_current" )
connector: Connector = hass.data["blue_current"]["uuid"] await client.receiver(
{
connector.charge_points = {"101": {key: 20, timestamp_key: None}} "object": "CH_STATUS",
connector.grid = {grid_key: 20} "data": {
async_dispatcher_send(hass, "blue_current_value_update_101") "evse_id": "101",
"avg_voltage": 20,
"start_datetime": None,
"actual_kwh": None,
},
}
)
await hass.async_block_till_done() 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() await hass.async_block_till_done()
# test data updated # 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 is not None
assert state.state == str(20) assert state.state == str(20)
# grid # grid
state = hass.states.get(f"sensor.{grid_entity_id}") state = hass.states.get("sensor.average_grid_current")
assert state assert state
assert state.state == str(20) assert state.state == str(20)
@ -157,25 +206,30 @@ async def test_sensor_update(hass: HomeAssistant) -> None:
assert state.state == "unavailable" assert state.state == "unavailable"
# test if timestamp keeps old value # 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 state
assert ( assert (
datetime.strptime(state.state, "%Y-%m-%dT%H:%M:%S%z") 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 # test if older timestamp is ignored
connector.charge_points = { await client.receiver(
"101": { {
timestamp_key: datetime.strptime( "object": "CH_STATUS",
"data": {
"evse_id": "101",
"start_datetime": datetime.strptime(
"20211118 14:11:23+08:00", "%Y%m%d %H:%M:%S%z" "20211118 14:11:23+08:00", "%Y%m%d %H:%M:%S%z"
),
},
}
) )
} await hass.async_block_till_done()
}
async_dispatcher_send(hass, "blue_current_value_update_101") state = hass.states.get("sensor.101_started_on")
state = hass.states.get(f"sensor.101_{timestamp_entity_id}")
assert state assert state
assert ( assert (
datetime.strptime(state.state, "%Y-%m-%dT%H:%M:%S%z") datetime.strptime(state.state, "%Y-%m-%dT%H:%M:%S%z")
== charge_point[timestamp_key] == charge_point_status_timestamps["start_datetime"]
) )