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:
parent
654ab54aa0
commit
7dc9ad63bd
8 changed files with 325 additions and 294 deletions
|
@ -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(
|
||||
|
|
|
@ -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}")
|
||||
|
|
|
@ -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%]"
|
||||
},
|
||||
|
|
|
@ -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
|
||||
|
|
18
tests/components/blue_current/conftest.py
Normal file
18
tests/components/blue_current/conftest.py
Normal 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"},
|
||||
)
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"]
|
||||
)
|
||||
|
|
Loading…
Add table
Reference in a new issue