Files
core/tests/components/nexblue/test_switch.py
T

391 lines
12 KiB
Python

"""Tests for NexBlue switches."""
from collections.abc import Generator
from dataclasses import replace
from datetime import timedelta
from unittest.mock import MagicMock, patch
from freezegun.api import FrozenDateTimeFactory
from nexblue_api import NexBlueError
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.nexblue.const import DOMAIN
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.const import ATTR_ASSUMED_STATE, STATE_UNAVAILABLE, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from .conftest import CHARGER, CHARGER_STATUS
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
@pytest.fixture(autouse=True)
def fixture_platforms() -> Generator[None]:
"""Limit this module's setup to the switch platform."""
with patch("homeassistant.components.nexblue.PLATFORMS", [Platform.SWITCH]):
yield
async def test_switch_entities_snapshot(
hass: HomeAssistant,
init_integration: MockConfigEntry,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
) -> None:
"""Test the complete NexBlue switch platform through a snapshot."""
await snapshot_platform(
hass,
entity_registry,
snapshot,
init_integration.entry_id,
)
async def test_charger_removed_from_list_becomes_unavailable(
hass: HomeAssistant,
init_integration: MockConfigEntry,
mock_client: MagicMock,
) -> None:
"""Test a charger missing from a refresh does not raise an exception."""
mock_client.async_list_chargers.return_value = []
await init_integration.runtime_data.async_refresh()
assert hass.states.get("switch.nb123456_charging").state == STATE_UNAVAILABLE
async def test_turn_on_starts_charging(
hass: HomeAssistant,
init_integration: MockConfigEntry,
mock_client: MagicMock,
entity_registry: er.EntityRegistry,
) -> None:
"""Test turning on sends the start command and updates the state."""
entity_id = entity_registry.async_get_entity_id(
SWITCH_DOMAIN, DOMAIN, "NB123456_charging"
)
assert entity_id
await hass.services.async_call(
SWITCH_DOMAIN,
"turn_on",
{"entity_id": entity_id},
blocking=True,
)
mock_client.async_start_charging.assert_awaited_once_with("NB123456")
assert hass.states.get(entity_id).state == "on"
async def test_turn_off_stops_charging(
hass: HomeAssistant,
init_integration: MockConfigEntry,
mock_client: MagicMock,
entity_registry: er.EntityRegistry,
) -> None:
"""Test turning off sends the stop command and updates the state."""
entity_id = entity_registry.async_get_entity_id(
SWITCH_DOMAIN, DOMAIN, "NB123456_charging"
)
assert entity_id
await hass.services.async_call(
SWITCH_DOMAIN,
"turn_off",
{"entity_id": entity_id},
blocking=True,
)
mock_client.async_stop_charging.assert_awaited_once_with("NB123456")
assert hass.states.get(entity_id).state == "off"
@pytest.mark.parametrize(
("service", "method"),
[
("turn_on", "async_start_charging"),
("turn_off", "async_stop_charging"),
],
)
async def test_repeated_assumed_command_is_ignored(
hass: HomeAssistant,
init_integration: MockConfigEntry,
mock_client: MagicMock,
entity_registry: er.EntityRegistry,
service: str,
method: str,
) -> None:
"""Test a repeated command is ignored while its state is assumed."""
entity_id = entity_registry.async_get_entity_id(
SWITCH_DOMAIN, DOMAIN, "NB123456_charging"
)
assert entity_id
await hass.services.async_call(
SWITCH_DOMAIN,
service,
{"entity_id": entity_id},
blocking=True,
)
await hass.services.async_call(
SWITCH_DOMAIN,
service,
{"entity_id": entity_id},
blocking=True,
)
getattr(mock_client, method).assert_awaited_once_with("NB123456")
async def test_command_error_is_reported(
hass: HomeAssistant,
init_integration: MockConfigEntry,
mock_client: MagicMock,
entity_registry: er.EntityRegistry,
) -> None:
"""Test a rejected command is reported as a Home Assistant error."""
mock_client.async_start_charging.side_effect = NexBlueError(
"The charger rejected the command"
)
entity_id = entity_registry.async_get_entity_id(
SWITCH_DOMAIN, DOMAIN, "NB123456_charging"
)
assert entity_id
with pytest.raises(HomeAssistantError, match="rejected the command"):
await hass.services.async_call(
SWITCH_DOMAIN,
"turn_on",
{"entity_id": entity_id},
blocking=True,
)
async def test_assumed_state_clears_after_confirmed_refresh(
hass: HomeAssistant,
init_integration: MockConfigEntry,
mock_client: MagicMock,
entity_registry: er.EntityRegistry,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test a confirmed refresh clears the assumed switch state early."""
entity_id = entity_registry.async_get_entity_id(
SWITCH_DOMAIN, DOMAIN, "NB123456_charging"
)
assert entity_id
mock_client.async_get_charger_status.return_value = replace(
CHARGER_STATUS, charging_state=0
)
await hass.services.async_call(
SWITCH_DOMAIN,
"turn_off",
{"entity_id": entity_id},
blocking=True,
)
assert hass.states.get(entity_id).attributes[ATTR_ASSUMED_STATE] is True
freezer.tick(timedelta(seconds=3))
async_fire_time_changed(hass)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state.state == "off"
assert not state.attributes.get(ATTR_ASSUMED_STATE)
async def test_assumed_state_clears_after_final_command_refresh(
hass: HomeAssistant,
init_integration: MockConfigEntry,
mock_client: MagicMock,
entity_registry: er.EntityRegistry,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test the final command refresh replaces an unconfirmed assumed state."""
entity_id = entity_registry.async_get_entity_id(
SWITCH_DOMAIN, DOMAIN, "NB123456_charging"
)
assert entity_id
mock_client.async_list_chargers.reset_mock()
mock_client.async_get_charger_status.reset_mock()
await hass.services.async_call(
SWITCH_DOMAIN,
"turn_off",
{"entity_id": entity_id},
blocking=True,
)
assert hass.states.get(entity_id).state == "off"
assert hass.states.get(entity_id).attributes[ATTR_ASSUMED_STATE] is True
assert mock_client.async_list_chargers.await_count == 0
assert mock_client.async_get_charger_status.await_count == 0
freezer.tick(timedelta(seconds=3))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert mock_client.async_list_chargers.await_count == 1
assert mock_client.async_get_charger_status.await_count == 1
assert hass.states.get(entity_id).attributes[ATTR_ASSUMED_STATE] is True
freezer.tick(timedelta(seconds=17))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert mock_client.async_list_chargers.await_count == 2
assert mock_client.async_get_charger_status.await_count == 2
assert not hass.states.get(entity_id).attributes.get(ATTR_ASSUMED_STATE)
assert hass.states.get(entity_id).state == "on"
async def test_new_command_replaces_pending_command_refreshes(
hass: HomeAssistant,
init_integration: MockConfigEntry,
mock_client: MagicMock,
entity_registry: er.EntityRegistry,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test a new command replaces the previous command's refreshes."""
entity_id = entity_registry.async_get_entity_id(
SWITCH_DOMAIN, DOMAIN, "NB123456_charging"
)
assert entity_id
mock_client.async_list_chargers.reset_mock()
await hass.services.async_call(
SWITCH_DOMAIN,
"turn_on",
{"entity_id": entity_id},
blocking=True,
)
await hass.services.async_call(
SWITCH_DOMAIN,
"turn_off",
{"entity_id": entity_id},
blocking=True,
)
freezer.tick(timedelta(seconds=3))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert mock_client.async_list_chargers.await_count == 1
async def test_command_keeps_other_charger_pending_refreshes(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_client: MagicMock,
entity_registry: er.EntityRegistry,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test a command does not cancel another charger's pending refreshes."""
second_charger = replace(CHARGER, serial_number="NB654321")
mock_client.async_list_chargers.return_value = [CHARGER, second_charger]
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
coordinator = mock_config_entry.runtime_data
first_entity_id = entity_registry.async_get_entity_id(
SWITCH_DOMAIN, DOMAIN, "NB123456_charging"
)
second_entity_id = entity_registry.async_get_entity_id(
SWITCH_DOMAIN, DOMAIN, "NB654321_charging"
)
assert first_entity_id
assert second_entity_id
with patch.object(coordinator, "async_request_refresh") as mock_refresh:
await hass.services.async_call(
SWITCH_DOMAIN,
"turn_on",
{"entity_id": first_entity_id},
blocking=True,
)
freezer.tick(timedelta(seconds=3))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert mock_refresh.await_count == 1
freezer.tick(timedelta(seconds=7))
await hass.services.async_call(
SWITCH_DOMAIN,
"turn_off",
{"entity_id": second_entity_id},
blocking=True,
)
freezer.tick(timedelta(seconds=3))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert mock_refresh.await_count == 2
freezer.tick(timedelta(seconds=7))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert mock_refresh.await_count == 3
async def test_pending_command_refreshes_cancelled_on_unload(
hass: HomeAssistant,
init_integration: MockConfigEntry,
mock_client: MagicMock,
entity_registry: er.EntityRegistry,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test pending command refreshes do not run after unloading the entry."""
entity_id = entity_registry.async_get_entity_id(
SWITCH_DOMAIN, DOMAIN, "NB123456_charging"
)
assert entity_id
mock_client.async_list_chargers.reset_mock()
await hass.services.async_call(
SWITCH_DOMAIN,
"turn_on",
{"entity_id": entity_id},
blocking=True,
)
assert mock_client.async_list_chargers.await_count == 0
assert await hass.config_entries.async_unload(init_integration.entry_id)
await hass.async_block_till_done()
freezer.tick(timedelta(seconds=15))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert mock_client.async_list_chargers.await_count == 0
@pytest.mark.parametrize(
("charging_state", "expected_state"),
[
(5, "on"),
(6, "off"),
(7, "on"),
],
)
async def test_charging_session_states(
hass: HomeAssistant,
init_integration: MockConfigEntry,
mock_client: MagicMock,
charging_state: int,
expected_state: str,
) -> None:
"""Test switch state reflects whether a charging session is active."""
mock_client.async_get_charger_status.return_value = replace(
CHARGER_STATUS, charging_state=charging_state
)
await init_integration.runtime_data.async_refresh()
assert hass.states.get("switch.nb123456_charging").state == expected_state