mirror of
https://github.com/home-assistant/core.git
synced 2026-07-01 11:46:40 +01:00
9decee350f
Co-authored-by: Ariel Ebersberger <31776703+justanotherariel@users.noreply.github.com>
406 lines
14 KiB
Python
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
|