mirror of
https://github.com/home-assistant/core.git
synced 2025-12-20 02:48:57 +00:00
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
276 lines
8.9 KiB
Python
276 lines
8.9 KiB
Python
"""Test the Watts Vision integration initialization."""
|
|
|
|
from datetime import timedelta
|
|
from unittest.mock import AsyncMock
|
|
|
|
from aiohttp import ClientError
|
|
from freezegun.api import FrozenDateTimeFactory
|
|
import pytest
|
|
from visionpluspython.exceptions import (
|
|
WattsVisionAuthError,
|
|
WattsVisionConnectionError,
|
|
WattsVisionDeviceError,
|
|
WattsVisionError,
|
|
WattsVisionTimeoutError,
|
|
)
|
|
from visionpluspython.models import create_device_from_data
|
|
|
|
from homeassistant.components.watts.const import (
|
|
DISCOVERY_INTERVAL_MINUTES,
|
|
DOMAIN,
|
|
OAUTH2_TOKEN,
|
|
)
|
|
from homeassistant.config_entries import ConfigEntryState
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.helpers import device_registry as dr
|
|
|
|
from . import setup_integration
|
|
|
|
from tests.common import MockConfigEntry, async_fire_time_changed
|
|
from tests.test_util.aiohttp import AiohttpClientMocker
|
|
|
|
|
|
async def test_setup_entry_success(
|
|
hass: HomeAssistant,
|
|
mock_watts_client: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
) -> None:
|
|
"""Test successful setup and unload of entry."""
|
|
await setup_integration(hass, mock_config_entry)
|
|
|
|
assert mock_config_entry.state is ConfigEntryState.LOADED
|
|
|
|
mock_watts_client.discover_devices.assert_called_once()
|
|
|
|
unload_result = await hass.config_entries.async_unload(mock_config_entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
assert unload_result is True
|
|
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
|
|
|
|
|
|
@pytest.mark.usefixtures("setup_credentials")
|
|
async def test_setup_entry_auth_failed(
|
|
hass: HomeAssistant,
|
|
aioclient_mock: AiohttpClientMocker,
|
|
) -> None:
|
|
"""Test setup with authentication failure."""
|
|
config_entry = MockConfigEntry(
|
|
domain="watts",
|
|
unique_id="test-device-id",
|
|
data={
|
|
"device_id": "test-device-id",
|
|
"auth_implementation": "watts",
|
|
"token": {
|
|
"access_token": "test-access-token",
|
|
"refresh_token": "test-refresh-token",
|
|
"expires_at": 0, # Expired token to force refresh
|
|
},
|
|
},
|
|
)
|
|
config_entry.add_to_hass(hass)
|
|
|
|
aioclient_mock.post(OAUTH2_TOKEN, status=401)
|
|
|
|
result = await hass.config_entries.async_setup(config_entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
assert result is False
|
|
assert config_entry.state is ConfigEntryState.SETUP_ERROR
|
|
|
|
|
|
@pytest.mark.usefixtures("setup_credentials")
|
|
async def test_setup_entry_not_ready(
|
|
hass: HomeAssistant,
|
|
aioclient_mock: AiohttpClientMocker,
|
|
) -> None:
|
|
"""Test setup when network is temporarily unavailable."""
|
|
config_entry = MockConfigEntry(
|
|
domain="watts",
|
|
unique_id="test-device-id",
|
|
data={
|
|
"device_id": "test-device-id",
|
|
"auth_implementation": "watts",
|
|
"token": {
|
|
"access_token": "test-access-token",
|
|
"refresh_token": "test-refresh-token",
|
|
"expires_at": 0, # Expired token to force refresh
|
|
},
|
|
},
|
|
)
|
|
config_entry.add_to_hass(hass)
|
|
|
|
aioclient_mock.post(OAUTH2_TOKEN, exc=ClientError("Connection timeout"))
|
|
|
|
result = await hass.config_entries.async_setup(config_entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
assert result is False
|
|
assert config_entry.state is ConfigEntryState.SETUP_RETRY
|
|
|
|
|
|
async def test_setup_entry_hub_coordinator_update_failed(
|
|
hass: HomeAssistant,
|
|
mock_watts_client: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
) -> None:
|
|
"""Test setup when hub coordinator update fails."""
|
|
|
|
# Make discover_devices fail
|
|
mock_watts_client.discover_devices.side_effect = ConnectionError("API error")
|
|
|
|
mock_config_entry.add_to_hass(hass)
|
|
|
|
result = await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
assert result is False
|
|
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
|
|
|
|
|
@pytest.mark.usefixtures("setup_credentials")
|
|
async def test_setup_entry_server_error_5xx(
|
|
hass: HomeAssistant,
|
|
aioclient_mock: AiohttpClientMocker,
|
|
) -> None:
|
|
"""Test setup when server returns error."""
|
|
config_entry = MockConfigEntry(
|
|
domain="watts",
|
|
unique_id="test-device-id",
|
|
data={
|
|
"device_id": "test-device-id",
|
|
"auth_implementation": "watts",
|
|
"token": {
|
|
"access_token": "test-access-token",
|
|
"refresh_token": "test-refresh-token",
|
|
"expires_at": 0, # Expired token to force refresh
|
|
},
|
|
},
|
|
)
|
|
config_entry.add_to_hass(hass)
|
|
|
|
aioclient_mock.post(OAUTH2_TOKEN, status=500)
|
|
|
|
result = await hass.config_entries.async_setup(config_entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
assert result is False
|
|
assert config_entry.state is ConfigEntryState.SETUP_RETRY
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("exception", "expected_state"),
|
|
[
|
|
(WattsVisionAuthError("Auth failed"), ConfigEntryState.SETUP_ERROR),
|
|
(WattsVisionConnectionError("Connection lost"), ConfigEntryState.SETUP_RETRY),
|
|
(WattsVisionTimeoutError("Request timeout"), ConfigEntryState.SETUP_RETRY),
|
|
(WattsVisionDeviceError("Device error"), ConfigEntryState.SETUP_RETRY),
|
|
(WattsVisionError("API error"), ConfigEntryState.SETUP_RETRY),
|
|
(ValueError("Value error"), ConfigEntryState.SETUP_RETRY),
|
|
],
|
|
)
|
|
async def test_setup_entry_discover_devices_errors(
|
|
hass: HomeAssistant,
|
|
mock_watts_client: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
exception: Exception,
|
|
expected_state: ConfigEntryState,
|
|
) -> None:
|
|
"""Test setup errors during device discovery."""
|
|
mock_watts_client.discover_devices.side_effect = exception
|
|
|
|
mock_config_entry.add_to_hass(hass)
|
|
|
|
result = await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
assert result is False
|
|
assert mock_config_entry.state is expected_state
|
|
|
|
|
|
async def test_dynamic_device_creation(
|
|
hass: HomeAssistant,
|
|
mock_watts_client: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
device_registry: dr.DeviceRegistry,
|
|
freezer: FrozenDateTimeFactory,
|
|
) -> None:
|
|
"""Test new devices are created dynamically."""
|
|
await setup_integration(hass, mock_config_entry)
|
|
|
|
assert device_registry.async_get_device(identifiers={(DOMAIN, "thermostat_123")})
|
|
assert device_registry.async_get_device(identifiers={(DOMAIN, "thermostat_456")})
|
|
assert (
|
|
device_registry.async_get_device(identifiers={(DOMAIN, "thermostat_789")})
|
|
is None
|
|
)
|
|
|
|
new_device_data = {
|
|
"deviceId": "thermostat_789",
|
|
"deviceName": "Kitchen Thermostat",
|
|
"deviceType": "thermostat",
|
|
"interface": "homeassistant.components.THERMOSTAT",
|
|
"roomName": "Kitchen",
|
|
"isOnline": True,
|
|
"currentTemperature": 21.0,
|
|
"setpoint": 20.0,
|
|
"thermostatMode": "Comfort",
|
|
"minAllowedTemperature": 5.0,
|
|
"maxAllowedTemperature": 30.0,
|
|
"temperatureUnit": "C",
|
|
"availableThermostatModes": ["Program", "Eco", "Comfort", "Off"],
|
|
}
|
|
new_device = create_device_from_data(new_device_data)
|
|
|
|
current_devices = list(mock_watts_client.discover_devices.return_value)
|
|
mock_watts_client.discover_devices.return_value = [*current_devices, new_device]
|
|
|
|
freezer.tick(timedelta(minutes=DISCOVERY_INTERVAL_MINUTES))
|
|
async_fire_time_changed(hass)
|
|
await hass.async_block_till_done()
|
|
|
|
new_device_entry = device_registry.async_get_device(
|
|
identifiers={(DOMAIN, "thermostat_789")}
|
|
)
|
|
assert new_device_entry is not None
|
|
assert new_device_entry.name == "Kitchen Thermostat"
|
|
|
|
state = hass.states.get("climate.kitchen_thermostat")
|
|
assert state is not None
|
|
|
|
|
|
async def test_stale_device_removal(
|
|
hass: HomeAssistant,
|
|
mock_watts_client: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
device_registry: dr.DeviceRegistry,
|
|
freezer: FrozenDateTimeFactory,
|
|
) -> None:
|
|
"""Test stale devices are removed dynamically."""
|
|
await setup_integration(hass, mock_config_entry)
|
|
|
|
device_123 = device_registry.async_get_device(
|
|
identifiers={(DOMAIN, "thermostat_123")}
|
|
)
|
|
device_456 = device_registry.async_get_device(
|
|
identifiers={(DOMAIN, "thermostat_456")}
|
|
)
|
|
assert device_123 is not None
|
|
assert device_456 is not None
|
|
|
|
current_devices = list(mock_watts_client.discover_devices.return_value)
|
|
# remove thermostat_456
|
|
mock_watts_client.discover_devices.return_value = [
|
|
d for d in current_devices if d.device_id != "thermostat_456"
|
|
]
|
|
|
|
freezer.tick(timedelta(minutes=DISCOVERY_INTERVAL_MINUTES))
|
|
async_fire_time_changed(hass)
|
|
await hass.async_block_till_done()
|
|
|
|
# Verify thermostat_456 has been removed
|
|
device_456_after_removal = device_registry.async_get_device(
|
|
identifiers={(DOMAIN, "thermostat_456")}
|
|
)
|
|
assert device_456_after_removal is None
|