Convert nws integration to component configuration (#31398)

* convert nws to component configuration

* add more debug logging

* remove assumed state

* move initialization of data into init

* refactor update logic

* use forecast success for checking available entities

* Split unique_id into more usable pieces.

use a base_unique_id for each entry.  Add domain for signaling to entities.  Each entity in each platform can use base_unique_id to form individual unique_id's.

* Revert "move initialization of data into init"

This reverts commit 09eb0220469285b10f0500f5f6def67415931a81.

* add silver quality scale to manifest

* unsubscribe listener in will_remove_from_hass

* initialize _unsub_listener in __init__

* use async_on_remove

* remove scan interval from configuration

* Use better name

Co-Authored-By: J. Nick Koston <nick@koston.org>

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
MatthewFlamm 2020-04-08 11:22:25 -04:00 committed by GitHub
parent f5b7deda72
commit a3e84791c6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 496 additions and 273 deletions

View File

@ -1 +1,176 @@
"""NWS Integration.""" """The National Weather Service integration."""
import asyncio
import datetime
import logging
import aiohttp
from pynws import SimpleNWS
import voluptuous as vol
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE
from homeassistant.core import HomeAssistant
from homeassistant.helpers import discovery
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.event import async_track_time_interval
from .const import CONF_STATION, DOMAIN
_LOGGER = logging.getLogger(__name__)
_INDIVIDUAL_SCHEMA = vol.Schema(
{
vol.Required(CONF_API_KEY): cv.string,
vol.Inclusive(
CONF_LATITUDE, "coordinates", "Latitude and longitude must exist together"
): cv.latitude,
vol.Inclusive(
CONF_LONGITUDE, "coordinates", "Latitude and longitude must exist together"
): cv.longitude,
vol.Optional(CONF_STATION): cv.string,
}
)
CONFIG_SCHEMA = vol.Schema(
{DOMAIN: vol.All(cv.ensure_list, [_INDIVIDUAL_SCHEMA])}, extra=vol.ALLOW_EXTRA,
)
PLATFORMS = ["weather"]
DEFAULT_SCAN_INTERVAL = datetime.timedelta(minutes=10)
def base_unique_id(latitude, longitude):
"""Return unique id for entries in configuration."""
return f"{latitude}_{longitude}"
def signal_unique_id(latitude, longitude):
"""Return unique id for signaling to entries in configuration from component."""
return f"{DOMAIN}_{base_unique_id(latitude,longitude)}"
async def async_setup(hass: HomeAssistant, config: dict):
"""Set up the National Weather Service integration."""
if DOMAIN not in config:
return True
hass.data[DOMAIN] = hass.data.get(DOMAIN, {})
for entry in config[DOMAIN]:
latitude = entry.get(CONF_LATITUDE, hass.config.latitude)
longitude = entry.get(CONF_LONGITUDE, hass.config.longitude)
api_key = entry[CONF_API_KEY]
client_session = async_get_clientsession(hass)
if base_unique_id(latitude, longitude) in hass.data[DOMAIN]:
_LOGGER.error(
"Duplicate entry in config: latitude %s latitude: %s",
latitude,
longitude,
)
continue
nws_data = NwsData(hass, latitude, longitude, api_key, client_session)
hass.data[DOMAIN][base_unique_id(latitude, longitude)] = nws_data
async_track_time_interval(hass, nws_data.async_update, DEFAULT_SCAN_INTERVAL)
for component in PLATFORMS:
hass.async_create_task(
discovery.async_load_platform(hass, component, DOMAIN, {}, config)
)
return True
class NwsData:
"""Data class for National Weather Service integration."""
def __init__(self, hass, latitude, longitude, api_key, websession):
"""Initialize the data."""
self.hass = hass
self.latitude = latitude
self.longitude = longitude
ha_api_key = f"{api_key} homeassistant"
self.nws = SimpleNWS(latitude, longitude, ha_api_key, websession)
self.update_observation_success = True
self.update_forecast_success = True
self.update_forecast_hourly_success = True
async def async_set_station(self, station):
"""
Set to desired station.
If None, nearest station is used.
"""
await self.nws.set_station(station)
_LOGGER.debug("Nearby station list: %s", self.nws.stations)
@property
def station(self):
"""Return station name."""
return self.nws.station
@property
def observation(self):
"""Return observation."""
return self.nws.observation
@property
def forecast(self):
"""Return day+night forecast."""
return self.nws.forecast
@property
def forecast_hourly(self):
"""Return hourly forecast."""
return self.nws.forecast_hourly
@staticmethod
async def _async_update_item(update_call, update_type, station_name, success):
try:
_LOGGER.debug("Updating %s for station %s", update_type, station_name)
await update_call()
if success:
_LOGGER.warning(
"Success updating %s for station %s", update_type, station_name
)
success = True
except (aiohttp.ClientError, asyncio.TimeoutError) as err:
if success:
_LOGGER.warning(
"Error updating %s for station %s: %s",
update_type,
station_name,
err,
)
success = False
async def async_update(self, now=None):
"""Update all data."""
await self._async_update_item(
self.nws.update_observation,
"observation",
self.station,
self.update_observation_success,
)
await self._async_update_item(
self.nws.update_forecast,
"forecast",
self.station,
self.update_forecast_success,
)
await self._async_update_item(
self.nws.update_forecast_hourly,
"forecast_hourly",
self.station,
self.update_forecast_hourly_success,
)
async_dispatcher_send(
self.hass, signal_unique_id(self.latitude, self.longitude)
)

View File

@ -0,0 +1,54 @@
"""Constants for National Weather Service Integration."""
DOMAIN = "nws"
CONF_STATION = "station"
ATTRIBUTION = "Data from National Weather Service/NOAA"
ATTR_FORECAST_DETAILED_DESCRIPTION = "detailed_description"
ATTR_FORECAST_PRECIP_PROB = "precipitation_probability"
ATTR_FORECAST_DAYTIME = "daytime"
CONDITION_CLASSES = {
"exceptional": [
"Tornado",
"Hurricane conditions",
"Tropical storm conditions",
"Dust",
"Smoke",
"Haze",
"Hot",
"Cold",
],
"snowy": ["Snow", "Sleet", "Blizzard"],
"snowy-rainy": [
"Rain/snow",
"Rain/sleet",
"Freezing rain/snow",
"Freezing rain",
"Rain/freezing rain",
],
"hail": [],
"lightning-rainy": [
"Thunderstorm (high cloud cover)",
"Thunderstorm (medium cloud cover)",
"Thunderstorm (low cloud cover)",
],
"lightning": [],
"pouring": [],
"rainy": [
"Rain",
"Rain showers (high cloud cover)",
"Rain showers (low cloud cover)",
],
"windy-variant": ["Mostly cloudy and windy", "Overcast and windy"],
"windy": [
"Fair/clear and windy",
"A few clouds and windy",
"Partly cloudy and windy",
],
"fog": ["Fog/mist"],
"clear": ["Fair/clear"], # sunny and clear-night
"cloudy": ["Mostly cloudy", "Overcast"],
"partlycloudy": ["A few clouds", "Partly cloudy"],
}

View File

@ -3,5 +3,6 @@
"name": "National Weather Service (NWS)", "name": "National Weather Service (NWS)",
"documentation": "https://www.home-assistant.io/integrations/nws", "documentation": "https://www.home-assistant.io/integrations/nws",
"codeowners": ["@MatthewFlamm"], "codeowners": ["@MatthewFlamm"],
"requirements": ["pynws==0.10.4"] "requirements": ["pynws==0.10.4"],
"quality_scale": "silver"
} }

View File

@ -1,12 +1,8 @@
"""Support for NWS weather service.""" """Support for NWS weather service."""
from collections import OrderedDict import asyncio
from datetime import timedelta
from json import JSONDecodeError
import logging import logging
import aiohttp import aiohttp
from pynws import SimpleNWS
import voluptuous as vol
from homeassistant.components.weather import ( from homeassistant.components.weather import (
ATTR_FORECAST_CONDITION, ATTR_FORECAST_CONDITION,
@ -14,15 +10,11 @@ from homeassistant.components.weather import (
ATTR_FORECAST_TIME, ATTR_FORECAST_TIME,
ATTR_FORECAST_WIND_BEARING, ATTR_FORECAST_WIND_BEARING,
ATTR_FORECAST_WIND_SPEED, ATTR_FORECAST_WIND_SPEED,
PLATFORM_SCHEMA,
WeatherEntity, WeatherEntity,
) )
from homeassistant.const import ( from homeassistant.const import (
CONF_API_KEY,
CONF_LATITUDE, CONF_LATITUDE,
CONF_LONGITUDE, CONF_LONGITUDE,
CONF_MODE,
CONF_NAME,
LENGTH_KILOMETERS, LENGTH_KILOMETERS,
LENGTH_METERS, LENGTH_METERS,
LENGTH_MILES, LENGTH_MILES,
@ -32,110 +24,26 @@ from homeassistant.const import (
TEMP_CELSIUS, TEMP_CELSIUS,
TEMP_FAHRENHEIT, TEMP_FAHRENHEIT,
) )
from homeassistant.core import callback
from homeassistant.exceptions import PlatformNotReady from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.util import Throttle
from homeassistant.util.distance import convert as convert_distance from homeassistant.util.distance import convert as convert_distance
from homeassistant.util.pressure import convert as convert_pressure from homeassistant.util.pressure import convert as convert_pressure
from homeassistant.util.temperature import convert as convert_temperature from homeassistant.util.temperature import convert as convert_temperature
from . import base_unique_id, signal_unique_id
from .const import (
ATTR_FORECAST_DAYTIME,
ATTR_FORECAST_DETAILED_DESCRIPTION,
ATTR_FORECAST_PRECIP_PROB,
ATTRIBUTION,
CONDITION_CLASSES,
CONF_STATION,
DOMAIN,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ATTRIBUTION = "Data from National Weather Service/NOAA"
SCAN_INTERVAL = timedelta(minutes=15)
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5)
CONF_STATION = "station"
ATTR_FORECAST_DETAIL_DESCRIPTION = "detailed_description"
ATTR_FORECAST_PRECIP_PROB = "precipitation_probability"
ATTR_FORECAST_DAYTIME = "daytime"
# Ordered so that a single condition can be chosen from multiple weather codes.
# Catalog of NWS icon weather codes listed at:
# https://api.weather.gov/icons
CONDITION_CLASSES = OrderedDict(
[
(
"exceptional",
[
"Tornado",
"Hurricane conditions",
"Tropical storm conditions",
"Dust",
"Smoke",
"Haze",
"Hot",
"Cold",
],
),
("snowy", ["Snow", "Sleet", "Blizzard"]),
(
"snowy-rainy",
[
"Rain/snow",
"Rain/sleet",
"Freezing rain/snow",
"Freezing rain",
"Rain/freezing rain",
],
),
("hail", []),
(
"lightning-rainy",
[
"Thunderstorm (high cloud cover)",
"Thunderstorm (medium cloud cover)",
"Thunderstorm (low cloud cover)",
],
),
("lightning", []),
("pouring", []),
(
"rainy",
[
"Rain",
"Rain showers (high cloud cover)",
"Rain showers (low cloud cover)",
],
),
("windy-variant", ["Mostly cloudy and windy", "Overcast and windy"]),
(
"windy",
[
"Fair/clear and windy",
"A few clouds and windy",
"Partly cloudy and windy",
],
),
("fog", ["Fog/mist"]),
("clear", ["Fair/clear"]), # sunny and clear-night
("cloudy", ["Mostly cloudy", "Overcast"]),
("partlycloudy", ["A few clouds", "Partly cloudy"]),
]
)
ERRORS = (aiohttp.ClientError, JSONDecodeError)
FORECAST_MODE = ["daynight", "hourly"]
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Optional(CONF_NAME): cv.string,
vol.Inclusive(
CONF_LATITUDE, "coordinates", "Latitude and longitude must exist together"
): cv.latitude,
vol.Inclusive(
CONF_LONGITUDE, "coordinates", "Latitude and longitude must exist together"
): cv.longitude,
vol.Optional(CONF_MODE, default="daynight"): vol.In(FORECAST_MODE),
vol.Optional(CONF_STATION): cv.string,
vol.Required(CONF_API_KEY): cv.string,
}
)
def convert_condition(time, weather): def convert_condition(time, weather):
""" """
@ -165,85 +73,73 @@ def convert_condition(time, weather):
return cond, max(prec_probs) return cond, max(prec_probs)
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): async def async_setup_platform(hass, config, async_add_entities, discovery_info):
"""Set up the NWS weather platform.""" """Set up the NWS weather platform."""
latitude = config.get(CONF_LATITUDE, hass.config.latitude) latitude = config.get(CONF_LATITUDE, hass.config.latitude)
longitude = config.get(CONF_LONGITUDE, hass.config.longitude) longitude = config.get(CONF_LONGITUDE, hass.config.longitude)
station = config.get(CONF_STATION) station = config.get(CONF_STATION)
api_key = config[CONF_API_KEY]
mode = config[CONF_MODE]
websession = async_get_clientsession(hass) nws_data = hass.data[DOMAIN][base_unique_id(latitude, longitude)]
# ID request as being from HA, pynws prepends the api_key in addition
api_key_ha = f"{api_key} homeassistant"
nws = SimpleNWS(latitude, longitude, api_key_ha, websession)
_LOGGER.debug("Setting up station: %s", station)
try: try:
await nws.set_station(station) await nws_data.async_set_station(station)
except ERRORS as status: except (aiohttp.ClientError, asyncio.TimeoutError) as err:
_LOGGER.error( _LOGGER.error("Error automatically setting station: %s", str(err))
"Error getting station list for %s: %s", (latitude, longitude), status
)
raise PlatformNotReady raise PlatformNotReady
_LOGGER.debug("Station list: %s", nws.stations) await nws_data.async_update()
_LOGGER.debug(
"Initialized for coordinates %s, %s -> station %s",
latitude,
longitude,
nws.station,
)
async_add_entities([NWSWeather(nws, mode, hass.config.units, config)], True) async_add_entities(
[
NWSWeather(nws_data, "daynight", hass.config.units),
NWSWeather(nws_data, "hourly", hass.config.units),
],
False,
)
class NWSWeather(WeatherEntity): class NWSWeather(WeatherEntity):
"""Representation of a weather condition.""" """Representation of a weather condition."""
def __init__(self, nws, mode, units, config): def __init__(self, nws, mode, units):
"""Initialise the platform with a data instance and station name.""" """Initialise the platform with a data instance and station name."""
self.nws = nws self.nws = nws
self.station_name = config.get(CONF_NAME, self.nws.station) self.station = nws.station
self.latitude = nws.latitude
self.longitude = nws.longitude
self.is_metric = units.is_metric self.is_metric = units.is_metric
self.mode = mode self.mode = mode
self.observation = None self.observation = None
self._forecast = None self._forecast = None
@Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_added_to_hass(self) -> None:
async def async_update(self): """Set up a listener and load data."""
"""Update Condition.""" self.async_on_remove(
_LOGGER.debug("Updating station observations %s", self.nws.station) async_dispatcher_connect(
try: self.hass,
await self.nws.update_observation() signal_unique_id(self.latitude, self.longitude),
except ERRORS as status: self._update_callback,
_LOGGER.error(
"Error updating observation from station %s: %s",
self.nws.station,
status,
) )
else: )
self.observation = self.nws.observation self._update_callback()
_LOGGER.debug("Observation: %s", self.observation)
_LOGGER.debug("Updating forecast") @callback
try: def _update_callback(self) -> None:
if self.mode == "daynight": """Load data from integration."""
await self.nws.update_forecast() self.observation = self.nws.observation
else:
await self.nws.update_forecast_hourly()
except ERRORS as status:
_LOGGER.error(
"Error updating forecast from station %s: %s", self.nws.station, status
)
return
if self.mode == "daynight": if self.mode == "daynight":
self._forecast = self.nws.forecast self._forecast = self.nws.forecast
else: else:
self._forecast = self.nws.forecast_hourly self._forecast = self.nws.forecast_hourly
_LOGGER.debug("Forecast: %s", self._forecast)
_LOGGER.debug("Finished updating") self.async_schedule_update_ha_state()
@property
def should_poll(self) -> bool:
"""Entities do not individually poll."""
return False
@property @property
def attribution(self): def attribution(self):
@ -253,7 +149,7 @@ class NWSWeather(WeatherEntity):
@property @property
def name(self): def name(self):
"""Return the name of the station.""" """Return the name of the station."""
return self.station_name return f"{self.station} {self.mode.title()}"
@property @property
def temperature(self): def temperature(self):
@ -354,7 +250,7 @@ class NWSWeather(WeatherEntity):
forecast = [] forecast = []
for forecast_entry in self._forecast: for forecast_entry in self._forecast:
data = { data = {
ATTR_FORECAST_DETAIL_DESCRIPTION: forecast_entry.get( ATTR_FORECAST_DETAILED_DESCRIPTION: forecast_entry.get(
"detailedForecast" "detailedForecast"
), ),
ATTR_FORECAST_TEMP: forecast_entry.get("temperature"), ATTR_FORECAST_TEMP: forecast_entry.get("temperature"),
@ -385,3 +281,20 @@ class NWSWeather(WeatherEntity):
data[ATTR_FORECAST_WIND_SPEED] = None data[ATTR_FORECAST_WIND_SPEED] = None
forecast.append(data) forecast.append(data)
return forecast return forecast
@property
def unique_id(self):
"""Return a unique_id for this entity."""
return f"{base_unique_id(self.latitude, self.longitude)}_{self.mode}"
@property
def available(self):
"""Return if state is available."""
if self.mode == "daynight":
return (
self.nws.update_observation_success and self.nws.update_forecast_success
)
return (
self.nws.update_observation_success
and self.nws.update_forecast_hourly_success
)

View File

@ -0,0 +1,24 @@
"""Fixtures for National Weather Service tests."""
from unittest.mock import patch
import pytest
from tests.common import mock_coro
from tests.components.nws.const import DEFAULT_FORECAST, DEFAULT_OBSERVATION
@pytest.fixture()
def mock_simple_nws():
"""Mock pynws SimpleNWS with default values."""
with patch("homeassistant.components.nws.SimpleNWS") as mock_nws:
instance = mock_nws.return_value
instance.set_station.return_value = mock_coro()
instance.update_observation.return_value = mock_coro()
instance.update_forecast.return_value = mock_coro()
instance.update_forecast_hourly.return_value = mock_coro()
instance.station = "ABC"
instance.stations = ["ABC"]
instance.observation = DEFAULT_OBSERVATION
instance.forecast = DEFAULT_FORECAST
instance.forecast_hourly = DEFAULT_FORECAST
yield mock_nws

View File

@ -1,4 +1,5 @@
"""Helpers for interacting with pynws.""" """Helpers for interacting with pynws."""
from homeassistant.components.nws.const import DOMAIN
from homeassistant.components.nws.weather import ATTR_FORECAST_PRECIP_PROB from homeassistant.components.nws.weather import ATTR_FORECAST_PRECIP_PROB
from homeassistant.components.weather import ( from homeassistant.components.weather import (
ATTR_FORECAST_CONDITION, ATTR_FORECAST_CONDITION,
@ -14,6 +15,7 @@ from homeassistant.components.weather import (
ATTR_WEATHER_WIND_SPEED, ATTR_WEATHER_WIND_SPEED,
) )
from homeassistant.const import ( from homeassistant.const import (
CONF_API_KEY,
LENGTH_KILOMETERS, LENGTH_KILOMETERS,
LENGTH_METERS, LENGTH_METERS,
LENGTH_MILES, LENGTH_MILES,
@ -27,6 +29,8 @@ from homeassistant.util.distance import convert as convert_distance
from homeassistant.util.pressure import convert as convert_pressure from homeassistant.util.pressure import convert as convert_pressure
from homeassistant.util.temperature import convert as convert_temperature from homeassistant.util.temperature import convert as convert_temperature
MINIMAL_CONFIG = {DOMAIN: [{CONF_API_KEY: "test"}]}
DEFAULT_STATIONS = ["ABC", "XYZ"] DEFAULT_STATIONS = ["ABC", "XYZ"]
DEFAULT_OBSERVATION = { DEFAULT_OBSERVATION = {

View File

@ -0,0 +1,67 @@
"""Tests for init module."""
from homeassistant.components import nws
from homeassistant.components.nws.const import CONF_STATION, DOMAIN
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE
from homeassistant.setup import async_setup_component
from tests.common import assert_setup_component
from tests.components.nws.const import MINIMAL_CONFIG
LATLON_CONFIG = {
DOMAIN: [{CONF_API_KEY: "test", CONF_LATITUDE: 45.0, CONF_LONGITUDE: -75.0}]
}
FULL_CONFIG = {
DOMAIN: [
{
CONF_API_KEY: "test",
CONF_LATITUDE: 45.0,
CONF_LONGITUDE: -75.0,
CONF_STATION: "XYZ",
}
]
}
DUPLICATE_CONFIG = {
DOMAIN: [
{CONF_API_KEY: "test", CONF_LATITUDE: 45.0, CONF_LONGITUDE: -75.0},
{CONF_API_KEY: "test", CONF_LATITUDE: 45.0, CONF_LONGITUDE: -75.0},
]
}
async def test_no_config(hass, mock_simple_nws):
"""Test that nws does not setup with no config."""
with assert_setup_component(0):
assert await async_setup_component(hass, DOMAIN, {}) is True
assert DOMAIN not in hass.data
async def test_successful_minimal_config(hass, mock_simple_nws):
"""Test that nws setup with minimal config."""
hass.config.latitude = 40.0
hass.config.longitude = -75.0
with assert_setup_component(1):
assert await async_setup_component(hass, DOMAIN, MINIMAL_CONFIG) is True
assert DOMAIN in hass.data
assert nws.base_unique_id(40.0, -75.0) in hass.data[DOMAIN]
async def test_successful_latlon_config(hass, mock_simple_nws):
"""Test that nws setup with latlon config."""
with assert_setup_component(1):
assert await async_setup_component(hass, DOMAIN, LATLON_CONFIG) is True
assert DOMAIN in hass.data
assert nws.base_unique_id(45.0, -75.0) in hass.data[DOMAIN]
async def test_successful_full_config(hass, mock_simple_nws):
"""Test that nws setup with full config."""
with assert_setup_component(1):
assert await async_setup_component(hass, DOMAIN, FULL_CONFIG) is True
assert DOMAIN in hass.data
assert nws.base_unique_id(45.0, -75.0) in hass.data[DOMAIN]
async def test_unsuccessful_duplicate_config(hass, mock_simple_nws):
"""Test that nws setup with duplicate config."""
assert await async_setup_component(hass, DOMAIN, DUPLICATE_CONFIG) is True
assert len(hass.data[DOMAIN]) == 1

View File

@ -1,35 +1,26 @@
"""Tests for the NWS weather component.""" """Tests for the NWS weather component."""
from unittest.mock import patch from datetime import timedelta
import aiohttp import aiohttp
import pytest import pytest
from homeassistant.components import nws
from homeassistant.components.weather import ATTR_FORECAST from homeassistant.components.weather import ATTR_FORECAST
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM
from .const import ( from tests.common import async_fire_time_changed
DEFAULT_FORECAST, from tests.components.nws.const import (
DEFAULT_OBSERVATION,
EXPECTED_FORECAST_IMPERIAL, EXPECTED_FORECAST_IMPERIAL,
EXPECTED_FORECAST_METRIC, EXPECTED_FORECAST_METRIC,
EXPECTED_OBSERVATION_IMPERIAL, EXPECTED_OBSERVATION_IMPERIAL,
EXPECTED_OBSERVATION_METRIC, EXPECTED_OBSERVATION_METRIC,
MINIMAL_CONFIG,
NONE_FORECAST, NONE_FORECAST,
NONE_OBSERVATION, NONE_OBSERVATION,
) )
from tests.common import mock_coro
MINIMAL_CONFIG = {
"weather": {
"platform": "nws",
"api_key": "x@example.com",
"latitude": 40.0,
"longitude": -85.0,
}
}
HOURLY_CONFIG = { HOURLY_CONFIG = {
"weather": { "weather": {
"platform": "nws", "platform": "nws",
@ -48,21 +39,29 @@ HOURLY_CONFIG = {
(METRIC_SYSTEM, EXPECTED_OBSERVATION_METRIC, EXPECTED_FORECAST_METRIC), (METRIC_SYSTEM, EXPECTED_OBSERVATION_METRIC, EXPECTED_FORECAST_METRIC),
], ],
) )
async def test_imperial_metric(hass, units, result_observation, result_forecast): async def test_imperial_metric(
hass, units, result_observation, result_forecast, mock_simple_nws
):
"""Test with imperial and metric units.""" """Test with imperial and metric units."""
hass.config.units = units hass.config.units = units
with patch("homeassistant.components.nws.weather.SimpleNWS") as mock_nws: assert await async_setup_component(hass, nws.DOMAIN, MINIMAL_CONFIG)
instance = mock_nws.return_value await hass.async_block_till_done()
instance.station = "ABC"
instance.set_station.return_value = mock_coro()
instance.update_observation.return_value = mock_coro()
instance.update_forecast.return_value = mock_coro()
instance.observation = DEFAULT_OBSERVATION
instance.forecast = DEFAULT_FORECAST
await async_setup_component(hass, "weather", MINIMAL_CONFIG) state = hass.states.get("weather.abc_hourly")
assert state
assert state.state == "sunny"
data = state.attributes
for key, value in result_observation.items():
assert data.get(key) == value
forecast = data.get(ATTR_FORECAST)
for key, value in result_forecast.items():
assert forecast[0].get(key) == value
state = hass.states.get("weather.abc_daynight")
state = hass.states.get("weather.abc")
assert state assert state
assert state.state == "sunny" assert state.state == "sunny"
@ -75,50 +74,17 @@ async def test_imperial_metric(hass, units, result_observation, result_forecast)
assert forecast[0].get(key) == value assert forecast[0].get(key) == value
async def test_hourly(hass): async def test_none_values(hass, mock_simple_nws):
"""Test with hourly option."""
hass.config.units = IMPERIAL_SYSTEM
with patch("homeassistant.components.nws.weather.SimpleNWS") as mock_nws:
instance = mock_nws.return_value
instance.station = "ABC"
instance.set_station.return_value = mock_coro()
instance.update_observation.return_value = mock_coro()
instance.update_forecast_hourly.return_value = mock_coro()
instance.observation = DEFAULT_OBSERVATION
instance.forecast_hourly = DEFAULT_FORECAST
await async_setup_component(hass, "weather", HOURLY_CONFIG)
state = hass.states.get("weather.abc")
assert state
assert state.state == "sunny"
data = state.attributes
for key, value in EXPECTED_OBSERVATION_IMPERIAL.items():
assert data.get(key) == value
forecast = data.get(ATTR_FORECAST)
for key, value in EXPECTED_FORECAST_IMPERIAL.items():
assert forecast[0].get(key) == value
async def test_none_values(hass):
"""Test with none values in observation and forecast dicts.""" """Test with none values in observation and forecast dicts."""
with patch("homeassistant.components.nws.weather.SimpleNWS") as mock_nws: instance = mock_simple_nws.return_value
instance = mock_nws.return_value instance.observation = NONE_OBSERVATION
instance.station = "ABC" instance.forecast = NONE_FORECAST
instance.set_station.return_value = mock_coro()
instance.update_observation.return_value = mock_coro()
instance.update_forecast.return_value = mock_coro()
instance.observation = NONE_OBSERVATION
instance.forecast = NONE_FORECAST
await async_setup_component(hass, "weather", MINIMAL_CONFIG)
state = hass.states.get("weather.abc") assert await async_setup_component(hass, nws.DOMAIN, MINIMAL_CONFIG)
assert state await hass.async_block_till_done()
state = hass.states.get("weather.abc_daynight")
assert state.state == "unknown" assert state.state == "unknown"
data = state.attributes data = state.attributes
for key in EXPECTED_OBSERVATION_IMPERIAL: for key in EXPECTED_OBSERVATION_IMPERIAL:
assert data.get(key) is None assert data.get(key) is None
@ -128,19 +94,16 @@ async def test_none_values(hass):
assert forecast[0].get(key) is None assert forecast[0].get(key) is None
async def test_none(hass): async def test_none(hass, mock_simple_nws):
"""Test with None as observation and forecast.""" """Test with None as observation and forecast."""
with patch("homeassistant.components.nws.weather.SimpleNWS") as mock_nws: instance = mock_simple_nws.return_value
instance = mock_nws.return_value instance.observation = None
instance.station = "ABC" instance.forecast = None
instance.set_station.return_value = mock_coro()
instance.update_observation.return_value = mock_coro()
instance.update_forecast.return_value = mock_coro()
instance.observation = None
instance.forecast = None
await async_setup_component(hass, "weather", MINIMAL_CONFIG)
state = hass.states.get("weather.abc") assert await async_setup_component(hass, nws.DOMAIN, MINIMAL_CONFIG)
await hass.async_block_till_done()
state = hass.states.get("weather.abc_daynight")
assert state assert state
assert state.state == "unknown" assert state.state == "unknown"
@ -152,46 +115,68 @@ async def test_none(hass):
assert forecast is None assert forecast is None
async def test_error_station(hass): async def test_error_station(hass, mock_simple_nws):
"""Test error in setting station.""" """Test error in setting station."""
with patch("homeassistant.components.nws.weather.SimpleNWS") as mock_nws:
instance = mock_nws.return_value
instance.station = "ABC"
instance.set_station.side_effect = aiohttp.ClientError
instance.update_observation.return_value = mock_coro()
instance.update_forecast.return_value = mock_coro()
instance.observation = None
instance.forecast = None
await async_setup_component(hass, "weather", MINIMAL_CONFIG)
state = hass.states.get("weather.abc") instance = mock_simple_nws.return_value
assert state is None instance.set_station.side_effect = aiohttp.ClientError
assert await async_setup_component(hass, nws.DOMAIN, MINIMAL_CONFIG) is True
await hass.async_block_till_done()
assert hass.states.get("weather.abc_hourly") is None
assert hass.states.get("weather.abc_daynight") is None
async def test_error_observation(hass, caplog): async def test_error_observation(hass, mock_simple_nws, caplog):
"""Test error during update observation.""" """Test error during update observation."""
with patch("homeassistant.components.nws.weather.SimpleNWS") as mock_nws: instance = mock_simple_nws.return_value
instance = mock_nws.return_value instance.update_observation.side_effect = aiohttp.ClientError
instance.station = "ABC"
instance.set_station.return_value = mock_coro()
instance.update_observation.side_effect = aiohttp.ClientError
instance.update_forecast.return_value = mock_coro()
instance.observation = None
instance.forecast = None
await async_setup_component(hass, "weather", MINIMAL_CONFIG)
assert "Error updating observation from station ABC" in caplog.text assert await async_setup_component(hass, nws.DOMAIN, MINIMAL_CONFIG)
await hass.async_block_till_done()
instance.update_observation.side_effect = None
future_time = dt_util.utcnow() + timedelta(minutes=15)
async_fire_time_changed(hass, future_time)
await hass.async_block_till_done()
assert "Error updating observation" in caplog.text
assert "Success updating observation" in caplog.text
async def test_error_forecast(hass, caplog): async def test_error_forecast(hass, caplog, mock_simple_nws):
"""Test error during update forecast.""" """Test error during update forecast."""
with patch("homeassistant.components.nws.weather.SimpleNWS") as mock_nws: instance = mock_simple_nws.return_value
instance = mock_nws.return_value instance.update_forecast.side_effect = aiohttp.ClientError
instance.station = "ABC"
instance.set_station.return_value = mock_coro() assert await async_setup_component(hass, nws.DOMAIN, MINIMAL_CONFIG)
instance.update_observation.return_value = mock_coro() await hass.async_block_till_done()
instance.update_forecast.side_effect = aiohttp.ClientError
instance.observation = None instance.update_forecast.side_effect = None
instance.forecast = None
await async_setup_component(hass, "weather", MINIMAL_CONFIG) future_time = dt_util.utcnow() + timedelta(minutes=15)
assert "Error updating forecast from station ABC" in caplog.text async_fire_time_changed(hass, future_time)
await hass.async_block_till_done()
assert "Error updating forecast" in caplog.text
assert "Success updating forecast" in caplog.text
async def test_error_forecast_hourly(hass, caplog, mock_simple_nws):
"""Test error during update forecast hourly."""
instance = mock_simple_nws.return_value
instance.update_forecast_hourly.side_effect = aiohttp.ClientError
assert await async_setup_component(hass, nws.DOMAIN, MINIMAL_CONFIG)
await hass.async_block_till_done()
instance.update_forecast_hourly.side_effect = None
future_time = dt_util.utcnow() + timedelta(minutes=15)
async_fire_time_changed(hass, future_time)
await hass.async_block_till_done()
assert "Error updating forecast_hourly" in caplog.text
assert "Success updating forecast_hourly" in caplog.text