mirror of
https://github.com/home-assistant/core.git
synced 2026-09-18 06:28:02 +01:00
391 lines
12 KiB
Python
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
|