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."""
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(

View file

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

View file

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

View file

@ -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

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}
)
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()

View file

@ -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

View file

@ -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"]
)