1
0
mirror of https://github.com/home-assistant/core.git synced 2026-07-01 11:46:40 +01:00
Files
core/tests/components/nws/test_init.py
T
2026-06-18 17:21:06 +02:00

406 lines
14 KiB
Python

"""Tests for init module."""
from pynws import NwsNoDataError
import pytest
from homeassistant.components.nws.const import CONF_LOCATION_ENTITY, DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, CONF_API_KEY
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from .const import NWS_CONFIG
from tests.common import MockConfigEntry
@pytest.fixture
def location_entity_config(entity_registry: er.EntityRegistry) -> dict:
"""Create a person entity in the registry and return a config dict using its registry ID."""
entry = entity_registry.async_get_or_create("person", "person", "test_user")
entity_registry.async_get_or_create("person", "person", "other_user")
return {
"entry": entry,
"config": {
CONF_API_KEY: "test",
CONF_LOCATION_ENTITY: entry.id,
},
}
async def test_unload_entry(hass: HomeAssistant, mock_simple_nws) -> None:
"""Test that nws setup with config yaml."""
entry = MockConfigEntry(
domain=DOMAIN,
data=NWS_CONFIG,
)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
assert entry.state is ConfigEntryState.LOADED
assert await hass.config_entries.async_unload(entry.entry_id)
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.NOT_LOADED
async def test_setup_with_location_entity(
hass: HomeAssistant, mock_simple_nws, location_entity_config: dict
) -> None:
"""Test setup resolves coordinates from location entity."""
entity = location_entity_config["entry"]
hass.states.async_set(
entity.entity_id,
"home",
{ATTR_LATITUDE: 40.0, ATTR_LONGITUDE: -80.0},
)
config_entry = MockConfigEntry(domain=DOMAIN, data=location_entity_config["config"])
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 is ConfigEntryState.LOADED
call_args = mock_simple_nws.call_args
assert call_args[0][0] == 40.0
assert call_args[0][1] == -80.0
assert config_entry.runtime_data.latitude == 40.0
assert config_entry.runtime_data.longitude == -80.0
async def test_setup_with_location_entity_auto_station(
hass: HomeAssistant, mock_simple_nws, location_entity_config: dict
) -> None:
"""Test entity-based setup always calls set_station(None) for auto-discovery."""
entity = location_entity_config["entry"]
hass.states.async_set(
entity.entity_id,
"home",
{ATTR_LATITUDE: 40.0, ATTR_LONGITUDE: -80.0},
)
config_entry = MockConfigEntry(domain=DOMAIN, data=location_entity_config["config"])
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 is ConfigEntryState.LOADED
mock_instance = mock_simple_nws.return_value
mock_instance.set_station.assert_called_once_with(None)
async def test_setup_with_location_entity_unavailable(
hass: HomeAssistant, mock_simple_nws, location_entity_config: dict
) -> None:
"""Test setup raises ConfigEntryNotReady when entity is unavailable."""
config_entry = MockConfigEntry(domain=DOMAIN, data=location_entity_config["config"])
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 is ConfigEntryState.SETUP_RETRY
assert config_entry.error_reason_translation_key == "entity_unavailable"
async def test_setup_with_location_entity_not_in_registry(
hass: HomeAssistant, mock_simple_nws
) -> None:
"""Test setup raises ConfigEntryError when entity registry ID no longer exists."""
config_entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_API_KEY: "test",
CONF_LOCATION_ENTITY: "nonexistent_registry_id",
},
)
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 is ConfigEntryState.SETUP_ERROR
assert config_entry.error_reason_translation_key == "entity_not_found"
async def test_setup_with_location_entity_disabled(
hass: HomeAssistant, mock_simple_nws, entity_registry: er.EntityRegistry
) -> None:
"""Test setup raises ConfigEntryError when entity is disabled."""
entry = entity_registry.async_get_or_create("person", "person", "disabled_user")
entity_registry.async_get_or_create("person", "person", "other_user")
entity_registry.async_update_entity(
entry.entity_id, disabled_by=er.RegistryEntryDisabler.USER
)
config_entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_API_KEY: "test",
CONF_LOCATION_ENTITY: entry.id,
},
)
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 is ConfigEntryState.SETUP_ERROR
assert config_entry.error_reason_translation_key == "entity_disabled"
async def test_location_change_updates_coordinates(
hass: HomeAssistant, mock_simple_nws, location_entity_config: dict
) -> None:
"""Test that a significant location change updates the API without reload."""
entity = location_entity_config["entry"]
hass.states.async_set(
entity.entity_id,
"home",
{ATTR_LATITUDE: 40.0, ATTR_LONGITUDE: -80.0},
)
config_entry = MockConfigEntry(domain=DOMAIN, data=location_entity_config["config"])
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 is ConfigEntryState.LOADED
assert mock_simple_nws.call_count == 1
hass.states.async_set(
entity.entity_id,
"away",
{ATTR_LATITUDE: 41.0, ATTR_LONGITUDE: -81.0},
)
await hass.async_block_till_done()
assert mock_simple_nws.call_count == 2
second_call = mock_simple_nws.call_args_list[1]
assert second_call[0][0] == 41.0
assert second_call[0][1] == -81.0
assert config_entry.runtime_data.latitude == 41.0
assert config_entry.runtime_data.longitude == -81.0
coordinator = config_entry.runtime_data.coordinator_observation
new_station = mock_simple_nws.return_value.station
assert coordinator.name == f"NWS observation station {new_station}"
async def test_location_change_resets_api_success_time(
hass: HomeAssistant, mock_simple_nws, location_entity_config: dict
) -> None:
"""Test that a location change resets last_api_success_time so failures are not tolerated."""
entity = location_entity_config["entry"]
hass.states.async_set(
entity.entity_id,
"home",
{ATTR_LATITUDE: 40.0, ATTR_LONGITUDE: -80.0},
)
config_entry = MockConfigEntry(domain=DOMAIN, data=location_entity_config["config"])
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
coordinator = config_entry.runtime_data.coordinator_observation
assert coordinator.last_api_success_time is not None
mock_instance = mock_simple_nws.return_value
mock_instance.update_observation.side_effect = NwsNoDataError("no data")
hass.states.async_set(
entity.entity_id,
"away",
{ATTR_LATITUDE: 41.0, ATTR_LONGITUDE: -81.0},
)
await hass.async_block_till_done()
assert coordinator.last_update_success is False
@pytest.mark.parametrize(
("new_lat", "new_lon"),
[
pytest.param(40.0, -80.0, id="same_coordinates"),
pytest.param(40.0001, -80.0001, id="within_threshold"),
],
)
async def test_no_update_without_significant_move(
hass: HomeAssistant,
mock_simple_nws,
location_entity_config: dict,
new_lat: float,
new_lon: float,
) -> None:
"""Test that unchanged or within-threshold coordinate changes do not create a new API."""
entity = location_entity_config["entry"]
hass.states.async_set(
entity.entity_id,
"home",
{ATTR_LATITUDE: 40.0, ATTR_LONGITUDE: -80.0},
)
config_entry = MockConfigEntry(domain=DOMAIN, data=location_entity_config["config"])
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
hass.states.async_set(
entity.entity_id,
"away",
{ATTR_LATITUDE: new_lat, ATTR_LONGITUDE: new_lon},
)
await config_entry.runtime_data.coordinator_observation.async_refresh()
await hass.async_block_till_done()
assert mock_simple_nws.call_count == 1
assert config_entry.runtime_data.latitude == 40.0
assert config_entry.runtime_data.longitude == -80.0
async def test_listener_filters_jitter(
hass: HomeAssistant, mock_simple_nws, location_entity_config: dict
) -> None:
"""Test that the state change listener filters out sub-threshold GPS jitter."""
entity = location_entity_config["entry"]
hass.states.async_set(
entity.entity_id,
"home",
{ATTR_LATITUDE: 40.0, ATTR_LONGITUDE: -80.0},
)
config_entry = MockConfigEntry(domain=DOMAIN, data=location_entity_config["config"])
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
mock_instance = mock_simple_nws.return_value
initial_obs_calls = mock_instance.update_observation.call_count
hass.states.async_set(
entity.entity_id,
"home",
{ATTR_LATITUDE: 40.0001, ATTR_LONGITUDE: -80.0001},
)
await hass.async_block_till_done()
assert mock_instance.update_observation.call_count == initial_obs_calls
assert mock_simple_nws.call_count == 1
async def test_location_entity_becomes_unavailable_no_update(
hass: HomeAssistant, mock_simple_nws, location_entity_config: dict
) -> None:
"""Test that entity becoming unavailable does not update coordinates."""
entity = location_entity_config["entry"]
hass.states.async_set(
entity.entity_id,
"home",
{ATTR_LATITUDE: 40.0, ATTR_LONGITUDE: -80.0},
)
config_entry = MockConfigEntry(domain=DOMAIN, data=location_entity_config["config"])
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
hass.states.async_set(entity.entity_id, "unknown", {})
await hass.async_block_till_done()
assert mock_simple_nws.call_count == 1
assert config_entry.runtime_data.latitude == 40.0
assert config_entry.runtime_data.longitude == -80.0
async def _setup_tracked_entity(
hass: HomeAssistant, entity_registry: er.EntityRegistry
) -> tuple[er.RegistryEntry, MockConfigEntry]:
"""Set up a config entry tracking a person entity and return both."""
entry = entity_registry.async_get_or_create("person", "person", "test_user")
entity_registry.async_get_or_create("person", "person", "other_user")
hass.states.async_set(
entry.entity_id,
"home",
{ATTR_LATITUDE: 40.0, ATTR_LONGITUDE: -80.0},
)
config_entry = MockConfigEntry(
domain=DOMAIN,
data={CONF_API_KEY: "test", CONF_LOCATION_ENTITY: entry.id},
)
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 is ConfigEntryState.LOADED
return entry, config_entry
async def test_entity_registry_removal_triggers_reload(
hass: HomeAssistant,
mock_simple_nws,
entity_registry: er.EntityRegistry,
) -> None:
"""Test that removing the tracked entity triggers a config entry reload."""
entry, config_entry = await _setup_tracked_entity(hass, entity_registry)
entity_registry.async_remove(entry.entity_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.SETUP_ERROR
async def test_entity_registry_rename_triggers_reload(
hass: HomeAssistant,
mock_simple_nws,
entity_registry: er.EntityRegistry,
) -> None:
"""Test that renaming the tracked entity triggers a config entry reload."""
entry, config_entry = await _setup_tracked_entity(hass, entity_registry)
entity_registry.async_update_entity(
entry.entity_id, new_entity_id="person.renamed_person"
)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.SETUP_RETRY
async def test_unload_with_location_entity(
hass: HomeAssistant, mock_simple_nws, location_entity_config: dict
) -> None:
"""Test unload works cleanly with a location entity configured."""
entity = location_entity_config["entry"]
hass.states.async_set(
entity.entity_id,
"home",
{ATTR_LATITUDE: 40.0, ATTR_LONGITUDE: -80.0},
)
config_entry = MockConfigEntry(domain=DOMAIN, data=location_entity_config["config"])
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 is ConfigEntryState.LOADED
assert await hass.config_entries.async_unload(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.NOT_LOADED