core/tests/components/nws/test_weather.py

637 lines
18 KiB
Python
Raw Normal View History

"""Tests for the NWS weather component."""
from datetime import timedelta
import aiohttp
2023-08-22 08:01:17 +00:00
from freezegun.api import FrozenDateTimeFactory
from pynws import NwsNoDataError
import pytest
2023-08-22 08:01:17 +00:00
from syrupy.assertion import SnapshotAssertion
from homeassistant.components import nws
from homeassistant.components.nws.const import (
DEFAULT_SCAN_INTERVAL,
OBSERVATION_VALID_TIME,
)
from homeassistant.components.weather import (
ATTR_CONDITION_CLEAR_NIGHT,
ATTR_CONDITION_SUNNY,
DOMAIN as WEATHER_DOMAIN,
SERVICE_GET_FORECASTS,
)
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component
from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM
from .const import (
CLEAR_NIGHT_OBSERVATION,
EXPECTED_FORECAST_IMPERIAL,
EXPECTED_FORECAST_METRIC,
NONE_FORECAST,
NONE_OBSERVATION,
NWS_CONFIG,
WEATHER_EXPECTED_OBSERVATION_IMPERIAL,
WEATHER_EXPECTED_OBSERVATION_METRIC,
)
from tests.common import MockConfigEntry, async_fire_time_changed
2023-08-22 08:01:17 +00:00
from tests.typing import WebSocketGenerator
@pytest.mark.parametrize(
("units", "result_observation", "result_forecast"),
[
(
US_CUSTOMARY_SYSTEM,
WEATHER_EXPECTED_OBSERVATION_IMPERIAL,
EXPECTED_FORECAST_IMPERIAL,
),
(METRIC_SYSTEM, WEATHER_EXPECTED_OBSERVATION_METRIC, EXPECTED_FORECAST_METRIC),
],
)
async def test_imperial_metric(
hass: HomeAssistant,
units,
result_observation,
result_forecast,
mock_simple_nws,
no_sensor,
) -> None:
"""Test with imperial and metric units."""
hass.config.units = units
2020-08-27 11:56:20 +00:00
entry = MockConfigEntry(
domain=nws.DOMAIN,
data=NWS_CONFIG,
)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("weather.abc")
assert state
assert state.state == ATTR_CONDITION_SUNNY
data = state.attributes
for key, value in result_observation.items():
assert data.get(key) == value
async def test_night_clear(hass: HomeAssistant, mock_simple_nws, no_sensor) -> None:
"""Test with clear-night in observation."""
instance = mock_simple_nws.return_value
instance.observation = CLEAR_NIGHT_OBSERVATION
entry = MockConfigEntry(
domain=nws.DOMAIN,
data=NWS_CONFIG,
)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("weather.abc")
assert state.state == ATTR_CONDITION_CLEAR_NIGHT
async def test_none_values(hass: HomeAssistant, mock_simple_nws, no_sensor) -> None:
"""Test with none values in observation and forecast dicts."""
instance = mock_simple_nws.return_value
instance.observation = NONE_OBSERVATION
instance.forecast = NONE_FORECAST
2020-08-27 11:56:20 +00:00
entry = MockConfigEntry(
domain=nws.DOMAIN,
data=NWS_CONFIG,
)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("weather.abc")
assert state.state == STATE_UNKNOWN
data = state.attributes
for key in WEATHER_EXPECTED_OBSERVATION_IMPERIAL:
assert data.get(key) is None
async def test_data_caching_error_observation(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
mock_simple_nws,
no_sensor,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test caching of data with errors."""
instance = mock_simple_nws.return_value
entry = MockConfigEntry(
domain=nws.DOMAIN,
data=NWS_CONFIG,
)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("weather.abc")
assert state.state == "sunny"
# data is still valid even when update fails
instance.update_observation.side_effect = NwsNoDataError("Test")
freezer.tick(DEFAULT_SCAN_INTERVAL + timedelta(seconds=100))
async_fire_time_changed(hass)
await hass.async_block_till_done()
state = hass.states.get("weather.abc")
assert state.state == "sunny"
assert (
"NWS observation update failed, but data still valid. Last success: "
in caplog.text
)
# data is no longer valid after OBSERVATION_VALID_TIME
freezer.tick(OBSERVATION_VALID_TIME + timedelta(seconds=1))
async_fire_time_changed(hass)
await hass.async_block_till_done()
state = hass.states.get("weather.abc")
assert state.state == STATE_UNAVAILABLE
assert "Error fetching NWS observation station ABC data: Test" in caplog.text
async def test_no_data_error_observation(
hass: HomeAssistant, mock_simple_nws, no_sensor, caplog: pytest.LogCaptureFixture
) -> None:
"""Test catching NwsNoDataDrror."""
instance = mock_simple_nws.return_value
instance.update_observation.side_effect = NwsNoDataError("Test")
entry = MockConfigEntry(
domain=nws.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 "Error fetching NWS observation station ABC data: Test" in caplog.text
async def test_no_data_error_forecast(
hass: HomeAssistant, mock_simple_nws, no_sensor, caplog: pytest.LogCaptureFixture
) -> None:
"""Test catching NwsNoDataDrror."""
instance = mock_simple_nws.return_value
instance.update_forecast.side_effect = NwsNoDataError("Test")
entry = MockConfigEntry(
domain=nws.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 (
"Error fetching NWS forecast station ABC data: No data returned" in caplog.text
)
async def test_no_data_error_forecast_hourly(
hass: HomeAssistant, mock_simple_nws, no_sensor, caplog: pytest.LogCaptureFixture
) -> None:
"""Test catching NwsNoDataDrror."""
instance = mock_simple_nws.return_value
instance.update_forecast_hourly.side_effect = NwsNoDataError("Test")
entry = MockConfigEntry(
domain=nws.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 (
"Error fetching NWS forecast hourly station ABC data: No data returned"
in caplog.text
)
async def test_none(hass: HomeAssistant, mock_simple_nws, no_sensor) -> None:
"""Test with None as observation and forecast."""
instance = mock_simple_nws.return_value
instance.observation = None
instance.forecast = None
2020-08-27 11:56:20 +00:00
entry = MockConfigEntry(
domain=nws.DOMAIN,
data=NWS_CONFIG,
)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("weather.abc")
assert state
assert state.state == STATE_UNKNOWN
data = state.attributes
for key in WEATHER_EXPECTED_OBSERVATION_IMPERIAL:
assert data.get(key) is None
async def test_error_station(hass: HomeAssistant, mock_simple_nws, no_sensor) -> None:
"""Test error in setting station."""
instance = mock_simple_nws.return_value
instance.set_station.side_effect = aiohttp.ClientError
2020-08-27 11:56:20 +00:00
entry = MockConfigEntry(
domain=nws.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 hass.states.get("weather.abc") is None
async def test_entity_refresh(hass: HomeAssistant, mock_simple_nws, no_sensor) -> None:
"""Test manual refresh."""
instance = mock_simple_nws.return_value
await async_setup_component(hass, "homeassistant", {})
2020-08-27 11:56:20 +00:00
entry = MockConfigEntry(
domain=nws.DOMAIN,
data=NWS_CONFIG,
)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
instance.update_observation.assert_called_once()
instance.update_forecast.assert_called_once()
instance.update_forecast_hourly.assert_called_once()
await hass.services.async_call(
"homeassistant",
"update_entity",
{"entity_id": "weather.abc"},
blocking=True,
)
await hass.async_block_till_done()
assert instance.update_observation.call_count == 2
assert instance.update_forecast.call_count == 2
assert instance.update_forecast_hourly.call_count == 2
async def test_error_observation(
hass: HomeAssistant, mock_simple_nws, no_sensor
) -> None:
"""Test error during update observation."""
instance = mock_simple_nws.return_value
# first update fails
instance.update_observation.side_effect = aiohttp.ClientError
entry = MockConfigEntry(
domain=nws.DOMAIN,
data=NWS_CONFIG,
)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
instance.update_observation.assert_called_once()
state = hass.states.get("weather.abc")
assert state
assert state.state == STATE_UNAVAILABLE
async def test_new_config_entry(
hass: HomeAssistant, entity_registry: er.EntityRegistry, no_sensor
) -> None:
2023-08-22 08:01:17 +00:00
"""Test the expected entities are created."""
entry = MockConfigEntry(
domain=nws.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.states.async_entity_ids("weather")) == 1
entry = hass.config_entries.async_entries()[0]
assert len(er.async_entries_for_config_entry(entity_registry, entry.entry_id)) == 1
2023-08-22 08:01:17 +00:00
@pytest.mark.parametrize(
("service"),
[
SERVICE_GET_FORECASTS,
],
)
2023-08-22 08:01:17 +00:00
async def test_forecast_service(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
snapshot: SnapshotAssertion,
mock_simple_nws,
no_sensor,
service: str,
) -> None:
2023-08-22 08:01:17 +00:00
"""Test multiple forecast."""
instance = mock_simple_nws.return_value
entry = MockConfigEntry(
domain=nws.DOMAIN,
data=NWS_CONFIG,
)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
2023-08-22 08:01:17 +00:00
instance.update_observation.assert_called_once()
instance.update_forecast.assert_called_once()
instance.update_forecast_hourly.assert_called_once()
for forecast_type in ("twice_daily", "hourly"):
response = await hass.services.async_call(
WEATHER_DOMAIN,
service,
2023-08-22 08:01:17 +00:00
{
"entity_id": "weather.abc",
2023-08-22 08:01:17 +00:00
"type": forecast_type,
},
blocking=True,
return_response=True,
)
assert response == snapshot
# Calling the services should use cached data
instance.update_observation.assert_called_once()
instance.update_forecast.assert_called_once()
instance.update_forecast_hourly.assert_called_once()
# Trigger data refetch
freezer.tick(nws.DEFAULT_SCAN_INTERVAL + timedelta(seconds=1))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert instance.update_observation.call_count == 2
assert instance.update_forecast.call_count == 2
assert instance.update_forecast_hourly.call_count == 2
2023-08-22 08:01:17 +00:00
for forecast_type in ("twice_daily", "hourly"):
response = await hass.services.async_call(
WEATHER_DOMAIN,
service,
2023-08-22 08:01:17 +00:00
{
"entity_id": "weather.abc",
2023-08-22 08:01:17 +00:00
"type": forecast_type,
},
blocking=True,
return_response=True,
)
assert response == snapshot
# Calling the services should update the hourly forecast
assert instance.update_observation.call_count == 2
assert instance.update_forecast.call_count == 2
assert instance.update_forecast_hourly.call_count == 2
# third update fails, but data is cached
instance.update_forecast_hourly.side_effect = aiohttp.ClientError
freezer.tick(nws.DEFAULT_SCAN_INTERVAL + timedelta(seconds=1))
async_fire_time_changed(hass)
await hass.async_block_till_done()
response = await hass.services.async_call(
WEATHER_DOMAIN,
service,
2023-08-22 08:01:17 +00:00
{
"entity_id": "weather.abc",
2023-08-22 08:01:17 +00:00
"type": "hourly",
},
blocking=True,
return_response=True,
)
assert response == snapshot
# after additional 35 minutes data caching expires, data is no longer shown
freezer.tick(timedelta(minutes=35))
async_fire_time_changed(hass)
await hass.async_block_till_done()
response = await hass.services.async_call(
WEATHER_DOMAIN,
service,
2023-08-22 08:01:17 +00:00
{
"entity_id": "weather.abc",
2023-08-22 08:01:17 +00:00
"type": "hourly",
},
blocking=True,
return_response=True,
)
assert response == snapshot
2023-08-22 08:01:17 +00:00
@pytest.mark.parametrize(
("forecast_type", "entity_id"),
[("hourly", "weather.abc")],
2023-08-22 08:01:17 +00:00
)
async def test_forecast_subscription(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
entity_registry: er.EntityRegistry,
2023-08-22 08:01:17 +00:00
freezer: FrozenDateTimeFactory,
snapshot: SnapshotAssertion,
mock_simple_nws,
no_sensor,
forecast_type: str,
entity_id: str,
) -> None:
"""Test multiple forecast."""
client = await hass_ws_client(hass)
2023-08-22 08:01:17 +00:00
# Pre-create the hourly entity
entity_registry.async_get_or_create(
WEATHER_DOMAIN,
nws.DOMAIN,
"35_-75_hourly",
2023-08-22 08:01:17 +00:00
suggested_object_id="abc_hourly",
)
2023-08-22 08:01:17 +00:00
entry = MockConfigEntry(
domain=nws.DOMAIN,
data=NWS_CONFIG,
)
2023-08-22 08:01:17 +00:00
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
await client.send_json_auto_id(
{
"type": "weather/subscribe_forecast",
"forecast_type": forecast_type,
"entity_id": entity_id,
}
)
msg = await client.receive_json()
assert msg["success"]
assert msg["result"] is None
subscription_id = msg["id"]
msg = await client.receive_json()
assert msg["id"] == subscription_id
assert msg["type"] == "event"
forecast1 = msg["event"]["forecast"]
assert forecast1 != []
assert forecast1 == snapshot
freezer.tick(nws.DEFAULT_SCAN_INTERVAL + timedelta(seconds=1))
await hass.async_block_till_done()
msg = await client.receive_json()
assert msg["id"] == subscription_id
assert msg["type"] == "event"
forecast2 = msg["event"]["forecast"]
assert forecast2 != []
assert forecast2 == snapshot
@pytest.mark.parametrize(
("forecast_type", "entity_id"),
[("hourly", "weather.abc")],
)
async def test_forecast_subscription_with_failing_coordinator(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
entity_registry: er.EntityRegistry,
freezer: FrozenDateTimeFactory,
snapshot: SnapshotAssertion,
mock_simple_nws_times_out,
no_sensor,
forecast_type: str,
entity_id: str,
) -> None:
"""Test a forecast subscription when the coordinator is failing to update."""
client = await hass_ws_client(hass)
# Pre-create the hourly entity
entity_registry.async_get_or_create(
WEATHER_DOMAIN,
nws.DOMAIN,
"35_-75_hourly",
suggested_object_id="abc_hourly",
)
entry = MockConfigEntry(
domain=nws.DOMAIN,
data=NWS_CONFIG,
)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
await client.send_json_auto_id(
{
"type": "weather/subscribe_forecast",
"forecast_type": forecast_type,
"entity_id": entity_id,
}
)
msg = await client.receive_json()
assert not msg["success"]
@pytest.mark.parametrize(
("forecast_type"),
[
"hourly",
"twice_daily",
],
)
async def test_detailed_forecast_service(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
snapshot: SnapshotAssertion,
mock_simple_nws,
no_sensor,
forecast_type: str,
) -> None:
"""Test detailed forecast."""
entry = MockConfigEntry(
domain=nws.DOMAIN,
data=NWS_CONFIG,
)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
response = await hass.services.async_call(
nws.DOMAIN,
"get_forecasts_extra",
{
"entity_id": "weather.abc",
"type": forecast_type,
},
blocking=True,
return_response=True,
)
assert response == snapshot
@pytest.mark.parametrize(
("forecast_type"),
[
"hourly",
"twice_daily",
],
)
async def test_detailed_forecast_service_no_data(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
snapshot: SnapshotAssertion,
mock_simple_nws,
no_sensor,
forecast_type: str,
) -> None:
"""Test detailed forecast."""
instance = mock_simple_nws.return_value
instance.forecast = None
instance.forecast_hourly = None
entry = MockConfigEntry(
domain=nws.DOMAIN,
data=NWS_CONFIG,
)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
response = await hass.services.async_call(
nws.DOMAIN,
"get_forecasts_extra",
{
"entity_id": "weather.abc",
"type": forecast_type,
},
blocking=True,
return_response=True,
)
assert response == snapshot