From 4030a405884dba5411755518811ff7d1ed479ddb Mon Sep 17 00:00:00 2001 From: Daniel Raper Date: Fri, 6 Dec 2024 18:03:33 +0000 Subject: [PATCH] Add some testing --- custom_components/ohme/binary_sensor.py | 2 +- custom_components/ohme/button.py | 2 +- custom_components/ohme/number.py | 2 +- custom_components/ohme/sensor.py | 2 +- custom_components/ohme/switch.py | 2 +- custom_components/ohme/time.py | 2 +- pyproject.toml | 3 + tests/test_binary_sensor.py | 70 +++++++++++++++++ tests/test_button.py | 63 ++++++++++++++++ tests/test_number.py | 79 ++++++++++++++++++++ tests/test_sensor.py | 31 ++++++++ tests/test_switch.py | 99 +++++++++++++++++++++++++ tests/test_time.py | 66 +++++++++++++++++ 13 files changed, 417 insertions(+), 6 deletions(-) create mode 100644 tests/test_binary_sensor.py create mode 100644 tests/test_button.py create mode 100644 tests/test_number.py create mode 100644 tests/test_sensor.py create mode 100644 tests/test_switch.py create mode 100644 tests/test_time.py diff --git a/custom_components/ohme/binary_sensor.py b/custom_components/ohme/binary_sensor.py index 14ad93d..161d2a9 100644 --- a/custom_components/ohme/binary_sensor.py +++ b/custom_components/ohme/binary_sensor.py @@ -32,7 +32,7 @@ async def async_setup_entry( CurrentSlotBinarySensor(coordinator, hass, client), ChargerOnlineBinarySensor(coordinator_advanced, hass, client)] - async_add_entities(sensors, update_before_add=True) + await async_add_entities(sensors, update_before_add=True) class ConnectedBinarySensor( diff --git a/custom_components/ohme/button.py b/custom_components/ohme/button.py index b815be4..b419308 100644 --- a/custom_components/ohme/button.py +++ b/custom_components/ohme/button.py @@ -30,7 +30,7 @@ async def async_setup_entry( OhmeApproveChargeButton(coordinator, hass, client) ) - async_add_entities(buttons, update_before_add=True) + await async_add_entities(buttons, update_before_add=True) class OhmeApproveChargeButton(OhmeEntity, ButtonEntity): diff --git a/custom_components/ohme/number.py b/custom_components/ohme/number.py index 333c816..4f756b8 100644 --- a/custom_components/ohme/number.py +++ b/custom_components/ohme/number.py @@ -31,7 +31,7 @@ async def async_setup_entry( PriceCapNumber(coordinators[COORDINATOR_ACCOUNTINFO], hass, client) ) - async_add_entities(numbers, update_before_add=True) + await async_add_entities(numbers, update_before_add=True) class TargetPercentNumber(OhmeEntity, NumberEntity): diff --git a/custom_components/ohme/sensor.py b/custom_components/ohme/sensor.py index 7c504e6..ff7f686 100644 --- a/custom_components/ohme/sensor.py +++ b/custom_components/ohme/sensor.py @@ -43,7 +43,7 @@ async def async_setup_entry( SlotListSensor(coordinator, hass, client), BatterySOCSensor(coordinator, hass, client)] - async_add_entities(sensors, update_before_add=True) + await async_add_entities(sensors, update_before_add=True) class PowerDrawSensor(OhmeEntity, SensorEntity): diff --git a/custom_components/ohme/switch.py b/custom_components/ohme/switch.py index bb8057f..b780e75 100644 --- a/custom_components/ohme/switch.py +++ b/custom_components/ohme/switch.py @@ -60,7 +60,7 @@ async def async_setup_entry( "sleep_when_inactive", "power-sleep", "stealthEnabled") ) - async_add_entities(switches, update_before_add=True) + await async_add_entities(switches, update_before_add=True) class OhmePauseChargeSwitch(OhmeEntity, SwitchEntity): diff --git a/custom_components/ohme/time.py b/custom_components/ohme/time.py index 7d4f306..0cfed48 100644 --- a/custom_components/ohme/time.py +++ b/custom_components/ohme/time.py @@ -26,7 +26,7 @@ async def async_setup_entry( numbers = [TargetTime(coordinators[COORDINATOR_CHARGESESSIONS], coordinators[COORDINATOR_SCHEDULES], hass, client)] - async_add_entities(numbers, update_before_add=True) + await async_add_entities(numbers, update_before_add=True) class TargetTime(OhmeEntity, TimeEntity): diff --git a/pyproject.toml b/pyproject.toml index 6eb3df5..ee1254a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,2 +1,5 @@ [tool.pytest.ini_options] asyncio_mode = "auto" +filterwarnings = [ + "ignore::RuntimeWarning" +] diff --git a/tests/test_binary_sensor.py b/tests/test_binary_sensor.py new file mode 100644 index 0000000..4468a28 --- /dev/null +++ b/tests/test_binary_sensor.py @@ -0,0 +1,70 @@ +import pytest +from unittest.mock import AsyncMock, MagicMock +from homeassistant.core import HomeAssistant +from homeassistant.util.dt import (utcnow) +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from custom_components.ohme.const import DOMAIN, DATA_CLIENT, DATA_SLOTS, DATA_COORDINATORS, COORDINATOR_CHARGESESSIONS, COORDINATOR_ADVANCED + +from custom_components.ohme.binary_sensor import ( + ConnectedBinarySensor, + ChargingBinarySensor, + PendingApprovalBinarySensor, + CurrentSlotBinarySensor, + ChargerOnlineBinarySensor, +) + +@pytest.fixture +def mock_hass(): + hass = MagicMock(spec=HomeAssistant) + hass.data = { + DOMAIN: { + "test_account": { + DATA_CLIENT: MagicMock(), + DATA_COORDINATORS: { + COORDINATOR_CHARGESESSIONS: MagicMock(spec=DataUpdateCoordinator), + COORDINATOR_ADVANCED: MagicMock(spec=DataUpdateCoordinator), + } + } + } + } + return hass + +@pytest.fixture +def mock_coordinator(): + return MagicMock(spec=DataUpdateCoordinator) + +@pytest.fixture +def mock_client(): + mock = MagicMock() + mock.email = "test_account" + return mock + +def test_connected_binary_sensor(mock_hass, mock_coordinator, mock_client): + sensor = ConnectedBinarySensor(mock_coordinator, mock_hass, mock_client) + mock_coordinator.data = {"mode": "CONNECTED"} + assert sensor.is_on is True + + mock_coordinator.data = {"mode": "DISCONNECTED"} + assert sensor.is_on is False + +def test_charging_binary_sensor(mock_hass, mock_coordinator, mock_client): + sensor = ChargingBinarySensor(mock_coordinator, mock_hass, mock_client) + mock_coordinator.data = {"power": {"watt": 100}, "batterySoc": {"wh": 50}, "mode": "CONNECTED", "allSessionSlots": []} + sensor._last_reading = {"power": {"watt": 100}, "batterySoc": {"wh": 40}} + assert sensor._calculate_state() is True + +def test_pending_approval_binary_sensor(mock_hass, mock_coordinator, mock_client): + sensor = PendingApprovalBinarySensor(mock_coordinator, mock_hass, mock_client) + mock_coordinator.data = {"mode": "PENDING_APPROVAL"} + assert sensor.is_on is True + + mock_coordinator.data = {"mode": "CONNECTED"} + assert sensor.is_on is False + +def test_charger_online_binary_sensor(mock_hass, mock_coordinator, mock_client): + sensor = ChargerOnlineBinarySensor(mock_coordinator, mock_hass, mock_client) + mock_coordinator.data = {"online": True} + assert sensor.is_on is True + + mock_coordinator.data = {"online": False} + assert sensor.is_on is False diff --git a/tests/test_button.py b/tests/test_button.py new file mode 100644 index 0000000..64dd7bb --- /dev/null +++ b/tests/test_button.py @@ -0,0 +1,63 @@ +import pytest +from unittest.mock import AsyncMock, MagicMock, patch +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.setup import async_setup_component +from custom_components.ohme.button import async_setup_entry, OhmeApproveChargeButton +from custom_components.ohme.const import DOMAIN, DATA_CLIENT, DATA_COORDINATORS, COORDINATOR_CHARGESESSIONS + +@pytest.fixture +def mock_hass(): + hass = MagicMock() + hass.data = { + DOMAIN: { + 'test_account': { + + } + } + } + return hass + +@pytest.fixture +def mock_config_entry(): + return AsyncMock(data={'email': 'test@example.com'}) + +@pytest.fixture +def mock_client(): + client = AsyncMock() + client.is_capable.return_value = True + client.async_approve_charge = AsyncMock() + return client + +@pytest.fixture +def mock_coordinator(): + coordinator = AsyncMock() + coordinator.async_refresh = AsyncMock() + return coordinator + +@pytest.fixture +def setup_hass(mock_hass, mock_config_entry, mock_client, mock_coordinator): + mock_hass.data = { + DOMAIN: { + 'test@example.com': { + DATA_CLIENT: mock_client, + DATA_COORDINATORS: { + COORDINATOR_CHARGESESSIONS: mock_coordinator + } + } + } + } + return mock_hass + +@pytest.mark.asyncio +async def test_async_setup_entry(setup_hass, mock_config_entry): + async_add_entities = AsyncMock() + await async_setup_entry(setup_hass, mock_config_entry, async_add_entities) + assert async_add_entities.call_count == 1 + +@pytest.mark.asyncio +async def test_ohme_approve_charge_button(setup_hass, mock_client, mock_coordinator): + button = OhmeApproveChargeButton(mock_coordinator, setup_hass, mock_client) + await button.async_press() + mock_client.async_approve_charge.assert_called_once() + mock_coordinator.async_refresh.assert_called_once() diff --git a/tests/test_number.py b/tests/test_number.py new file mode 100644 index 0000000..68ef0fc --- /dev/null +++ b/tests/test_number.py @@ -0,0 +1,79 @@ +import pytest +from unittest.mock import AsyncMock, MagicMock, patch +from homeassistant.core import HomeAssistant +from custom_components.ohme.const import DOMAIN, DATA_CLIENT, DATA_COORDINATORS, COORDINATOR_ACCOUNTINFO, COORDINATOR_CHARGESESSIONS, COORDINATOR_SCHEDULES + +from custom_components.ohme.number import ( + async_setup_entry, + TargetPercentNumber, + PreconditioningNumber, + PriceCapNumber, +) + +@pytest.fixture +def mock_hass(): + hass = MagicMock(data = { + DOMAIN: { + "test@example.com": { + DATA_COORDINATORS: [ + AsyncMock(), + AsyncMock(), + AsyncMock(), + AsyncMock() + ], + DATA_CLIENT: AsyncMock() + } + } + }) + return hass + +@pytest.fixture +def mock_config_entry(): + return AsyncMock(data={"email": "test@example.com"}) + +@pytest.fixture +def mock_async_add_entities(): + return AsyncMock() + +@pytest.mark.asyncio +async def test_async_setup_entry(mock_hass, mock_config_entry, mock_async_add_entities): + await async_setup_entry(mock_hass, mock_config_entry, mock_async_add_entities) + assert mock_async_add_entities.call_count == 1 + +@pytest.mark.asyncio +async def test_target_percent_number(mock_hass): + coordinator = mock_hass.data[DOMAIN]["test@example.com"][DATA_COORDINATORS][COORDINATOR_CHARGESESSIONS] + coordinator_schedules = mock_hass.data[DOMAIN]["test@example.com"][DATA_COORDINATORS][COORDINATOR_SCHEDULES] + client = mock_hass.data[DOMAIN]["test@example.com"][DATA_CLIENT] + + number = TargetPercentNumber(coordinator, coordinator_schedules, mock_hass, client) + + with patch('custom_components.ohme.number.session_in_progress', return_value=True): + await number.async_added_to_hass() + await number.async_set_native_value(50) + + assert number._state is None or number._state == 50 + +@pytest.mark.asyncio +async def test_preconditioning_number(mock_hass): + coordinator = mock_hass.data[DOMAIN]["test@example.com"][DATA_COORDINATORS][COORDINATOR_CHARGESESSIONS] + coordinator_schedules = mock_hass.data[DOMAIN]["test@example.com"][DATA_COORDINATORS][COORDINATOR_SCHEDULES] + client = mock_hass.data[DOMAIN]["test@example.com"][DATA_CLIENT] + + number = PreconditioningNumber(coordinator, coordinator_schedules, mock_hass, client) + + with patch('custom_components.ohme.number.session_in_progress', return_value=True): + await number.async_added_to_hass() + await number.async_set_native_value(30) + + assert number._state is None or number._state == 30 + +@pytest.mark.asyncio +async def test_price_cap_number(mock_hass): + coordinator = mock_hass.data[DOMAIN]["test@example.com"][DATA_COORDINATORS][COORDINATOR_ACCOUNTINFO] + client = mock_hass.data[DOMAIN]["test@example.com"][DATA_CLIENT] + + number = PriceCapNumber(coordinator, mock_hass, client) + await number.async_set_native_value(10.0) + + assert number._state is None or number._state == 10.0 diff --git a/tests/test_sensor.py b/tests/test_sensor.py new file mode 100644 index 0000000..1727a13 --- /dev/null +++ b/tests/test_sensor.py @@ -0,0 +1,31 @@ +import pytest +from unittest.mock import MagicMock +from custom_components.ohme.sensor import VoltageSensor + +@pytest.fixture +def mock_coordinator(): + """Fixture for creating a mock coordinator.""" + coordinator = MagicMock() + return coordinator + +@pytest.fixture +def voltage_sensor(mock_coordinator): + """Fixture for creating a VoltageSensor instance.""" + hass = MagicMock() + client = MagicMock() + return VoltageSensor(mock_coordinator, hass, client) + +def test_voltage_sensor_native_value_with_data(voltage_sensor, mock_coordinator): + """Test native_value when coordinator has data.""" + mock_coordinator.data = {'power': {'volt': 230}} + assert voltage_sensor.native_value == 230 + +def test_voltage_sensor_native_value_no_data(voltage_sensor, mock_coordinator): + """Test native_value when coordinator has no data.""" + mock_coordinator.data = None + assert voltage_sensor.native_value is None + +def test_voltage_sensor_native_value_no_power_data(voltage_sensor, mock_coordinator): + """Test native_value when coordinator has no power data.""" + mock_coordinator.data = {'power': None} + assert voltage_sensor.native_value is None diff --git a/tests/test_switch.py b/tests/test_switch.py new file mode 100644 index 0000000..800a30e --- /dev/null +++ b/tests/test_switch.py @@ -0,0 +1,99 @@ +import pytest +from unittest.mock import AsyncMock, patch +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from custom_components.ohme.const import DOMAIN, DATA_CLIENT, DATA_COORDINATORS, COORDINATOR_CHARGESESSIONS, COORDINATOR_ACCOUNTINFO + +from custom_components.ohme.switch import ( + async_setup_entry, + OhmePauseChargeSwitch, + OhmeMaxChargeSwitch, + OhmeConfigurationSwitch, + OhmeSolarBoostSwitch, + OhmePriceCapSwitch, +) + +@pytest.fixture +def mock_hass(): + return AsyncMock(spec=HomeAssistant) + +@pytest.fixture +def mock_client(): + client = AsyncMock() + client.cap_available.return_value = True + client.solar_capable.return_value = True + client.is_capable.side_effect = lambda x: x in ["buttonsLockable", "pluginsRequireApprovalMode", "stealth"] + return client + +@pytest.fixture +def mock_coordinator(): + return AsyncMock(spec=DataUpdateCoordinator) + +@pytest.fixture +def mock_config_entry(): + return AsyncMock(data={'email': 'test@example.com'}) + +@pytest.fixture +def setup_hass_data(mock_hass, mock_client, mock_coordinator): + mock_hass.data = { + DOMAIN: { + 'test@example.com': { + DATA_CLIENT: mock_client, + DATA_COORDINATORS: { + COORDINATOR_CHARGESESSIONS: mock_coordinator, + COORDINATOR_ACCOUNTINFO: mock_coordinator, + } + } + } + } + +@pytest.mark.asyncio +async def test_async_setup_entry(mock_hass, mock_config_entry, setup_hass_data): + async_add_entities = AsyncMock() + await async_setup_entry(mock_hass, mock_config_entry, async_add_entities) + assert async_add_entities.call_count == 1 + assert len(async_add_entities.call_args[0][0]) == 7 + +@pytest.mark.asyncio +async def test_ohme_pause_charge_switch(mock_hass, mock_client, mock_coordinator): + switch = OhmePauseChargeSwitch(mock_coordinator, mock_hass, mock_client) + await switch.async_turn_on() + mock_client.async_pause_charge.assert_called_once() + await switch.async_turn_off() + mock_client.async_resume_charge.assert_called_once() + +@pytest.mark.asyncio +async def test_ohme_max_charge_switch(mock_hass, mock_client, mock_coordinator): + switch = OhmeMaxChargeSwitch(mock_coordinator, mock_hass, mock_client) + await switch.async_turn_on() + mock_client.async_max_charge.assert_called_once_with(True) + mock_client.async_max_charge.reset_mock() + await switch.async_turn_off() + mock_client.async_max_charge.assert_called_once_with(False) + +@pytest.mark.asyncio +async def test_ohme_configuration_switch(mock_hass, mock_client, mock_coordinator): + switch = OhmeConfigurationSwitch(mock_coordinator, mock_hass, mock_client, "lock_buttons", "lock", "buttonsLocked") + await switch.async_turn_on() + mock_client.async_set_configuration_value.assert_called_once_with({"buttonsLocked": True}) + mock_client.async_set_configuration_value.reset_mock() + await switch.async_turn_off() + mock_client.async_set_configuration_value.assert_called_once_with({"buttonsLocked": False}) + +@pytest.mark.asyncio +async def test_ohme_solar_boost_switch(mock_hass, mock_client, mock_coordinator): + switch = OhmeSolarBoostSwitch(mock_coordinator, mock_hass, mock_client) + await switch.async_turn_on() + mock_client.async_set_configuration_value.assert_called_once_with({"solarMode": "ZERO_EXPORT"}) + mock_client.async_set_configuration_value.reset_mock() + await switch.async_turn_off() + mock_client.async_set_configuration_value.assert_called_once_with({"solarMode": "IGNORE"}) + +@pytest.mark.asyncio +async def test_ohme_price_cap_switch(mock_hass, mock_client, mock_coordinator): + switch = OhmePriceCapSwitch(mock_coordinator, mock_hass, mock_client) + await switch.async_turn_on() + mock_client.async_change_price_cap.assert_called_once_with(enabled=True) + mock_client.async_change_price_cap.reset_mock() + await switch.async_turn_off() + mock_client.async_change_price_cap.assert_called_once_with(enabled=False) diff --git a/tests/test_time.py b/tests/test_time.py new file mode 100644 index 0000000..906ad8b --- /dev/null +++ b/tests/test_time.py @@ -0,0 +1,66 @@ +import pytest +from datetime import time as dt_time +from unittest.mock import AsyncMock, MagicMock, patch +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import Entity +from custom_components.ohme.time import async_setup_entry, TargetTime +from custom_components.ohme.const import DOMAIN, DATA_CLIENT, DATA_COORDINATORS, COORDINATOR_CHARGESESSIONS, COORDINATOR_SCHEDULES + +@pytest.fixture +def mock_hass(): + hass = MagicMock() + hass.data = { + DOMAIN: { + 'test@example.com': { + DATA_COORDINATORS: [ + MagicMock(async_refresh=AsyncMock()), + MagicMock(async_refresh=AsyncMock()), + MagicMock(async_refresh=AsyncMock()), + MagicMock(async_refresh=AsyncMock()) + ], + DATA_CLIENT: MagicMock(async_apply_session_rule=AsyncMock(), async_update_schedule=AsyncMock()) + + } + } + } + return hass + +@pytest.fixture +def mock_config_entry(): + return AsyncMock(data={'email': 'test@example.com'}) + +@pytest.fixture +def mock_async_add_entities(): + return AsyncMock() + +@pytest.mark.asyncio +async def test_async_setup_entry(mock_hass, mock_config_entry, mock_async_add_entities): + await async_setup_entry(mock_hass, mock_config_entry, mock_async_add_entities) + assert mock_async_add_entities.called + +@pytest.fixture +def target_time_entity(mock_hass): + coordinator = mock_hass.data[DOMAIN]['test@example.com'][DATA_COORDINATORS][COORDINATOR_CHARGESESSIONS] + coordinator_schedules = mock_hass.data[DOMAIN]['test@example.com'][DATA_COORDINATORS][COORDINATOR_SCHEDULES] + client = mock_hass.data[DOMAIN]['test@example.com'][DATA_CLIENT] + return TargetTime(coordinator, coordinator_schedules, mock_hass, client) + +@pytest.mark.asyncio +async def test_async_added_to_hass(target_time_entity): + with patch.object(target_time_entity.coordinator_schedules, 'async_add_listener', return_value=AsyncMock()) as mock_add_listener: + await target_time_entity.async_added_to_hass() + assert mock_add_listener.called + +@pytest.mark.asyncio +async def test_async_set_value(target_time_entity): + with patch('custom_components.ohme.time.session_in_progress', return_value=True): + await target_time_entity.async_set_value(dt_time(12, 30)) + assert target_time_entity._client.async_apply_session_rule.called + + with patch('custom_components.ohme.time.session_in_progress', return_value=False): + await target_time_entity.async_set_value(dt_time(12, 30)) + assert target_time_entity._client.async_update_schedule.called + +def test_native_value(target_time_entity): + target_time_entity._state = dt_time(12, 30) + assert target_time_entity.native_value == dt_time(12, 30)