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)",
"documentation": "https://www.home-assistant.io/integrations/nws",
"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."""
from collections import OrderedDict
from datetime import timedelta
from json import JSONDecodeError
import asyncio
import logging
import aiohttp
from pynws import SimpleNWS
import voluptuous as vol
from homeassistant.components.weather import (
ATTR_FORECAST_CONDITION,
@ -14,15 +10,11 @@ from homeassistant.components.weather import (
ATTR_FORECAST_TIME,
ATTR_FORECAST_WIND_BEARING,
ATTR_FORECAST_WIND_SPEED,
PLATFORM_SCHEMA,
WeatherEntity,
)
from homeassistant.const import (
CONF_API_KEY,
CONF_LATITUDE,
CONF_LONGITUDE,
CONF_MODE,
CONF_NAME,
LENGTH_KILOMETERS,
LENGTH_METERS,
LENGTH_MILES,
@ -32,110 +24,26 @@ from homeassistant.const import (
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
)
from homeassistant.core import callback
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.util import Throttle
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.util.distance import convert as convert_distance
from homeassistant.util.pressure import convert as convert_pressure
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__)
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):
"""
@ -165,85 +73,73 @@ def convert_condition(time, weather):
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."""
latitude = config.get(CONF_LATITUDE, hass.config.latitude)
longitude = config.get(CONF_LONGITUDE, hass.config.longitude)
station = config.get(CONF_STATION)
api_key = config[CONF_API_KEY]
mode = config[CONF_MODE]
websession = async_get_clientsession(hass)
# 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)
nws_data = hass.data[DOMAIN][base_unique_id(latitude, longitude)]
_LOGGER.debug("Setting up station: %s", station)
try:
await nws.set_station(station)
except ERRORS as status:
_LOGGER.error(
"Error getting station list for %s: %s", (latitude, longitude), status
)
await nws_data.async_set_station(station)
except (aiohttp.ClientError, asyncio.TimeoutError) as err:
_LOGGER.error("Error automatically setting station: %s", str(err))
raise PlatformNotReady
_LOGGER.debug("Station list: %s", nws.stations)
_LOGGER.debug(
"Initialized for coordinates %s, %s -> station %s",
latitude,
longitude,
nws.station,
)
await nws_data.async_update()
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):
"""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."""
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.mode = mode
self.observation = None
self._forecast = None
@Throttle(MIN_TIME_BETWEEN_UPDATES)
async def async_update(self):
"""Update Condition."""
_LOGGER.debug("Updating station observations %s", self.nws.station)
try:
await self.nws.update_observation()
except ERRORS as status:
_LOGGER.error(
"Error updating observation from station %s: %s",
self.nws.station,
status,
async def async_added_to_hass(self) -> None:
"""Set up a listener and load data."""
self.async_on_remove(
async_dispatcher_connect(
self.hass,
signal_unique_id(self.latitude, self.longitude),
self._update_callback,
)
else:
self.observation = self.nws.observation
_LOGGER.debug("Observation: %s", self.observation)
_LOGGER.debug("Updating forecast")
try:
if self.mode == "daynight":
await self.nws.update_forecast()
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
)
self._update_callback()
@callback
def _update_callback(self) -> None:
"""Load data from integration."""
self.observation = self.nws.observation
if self.mode == "daynight":
self._forecast = self.nws.forecast
else:
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
def attribution(self):
@ -253,7 +149,7 @@ class NWSWeather(WeatherEntity):
@property
def name(self):
"""Return the name of the station."""
return self.station_name
return f"{self.station} {self.mode.title()}"
@property
def temperature(self):
@ -354,7 +250,7 @@ class NWSWeather(WeatherEntity):
forecast = []
for forecast_entry in self._forecast:
data = {
ATTR_FORECAST_DETAIL_DESCRIPTION: forecast_entry.get(
ATTR_FORECAST_DETAILED_DESCRIPTION: forecast_entry.get(
"detailedForecast"
),
ATTR_FORECAST_TEMP: forecast_entry.get("temperature"),
@ -385,3 +281,20 @@ class NWSWeather(WeatherEntity):
data[ATTR_FORECAST_WIND_SPEED] = None
forecast.append(data)
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."""
from homeassistant.components.nws.const import DOMAIN
from homeassistant.components.nws.weather import ATTR_FORECAST_PRECIP_PROB
from homeassistant.components.weather import (
ATTR_FORECAST_CONDITION,
@ -14,6 +15,7 @@ from homeassistant.components.weather import (
ATTR_WEATHER_WIND_SPEED,
)
from homeassistant.const import (
CONF_API_KEY,
LENGTH_KILOMETERS,
LENGTH_METERS,
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.temperature import convert as convert_temperature
MINIMAL_CONFIG = {DOMAIN: [{CONF_API_KEY: "test"}]}
DEFAULT_STATIONS = ["ABC", "XYZ"]
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."""
from unittest.mock import patch
from datetime import timedelta
import aiohttp
import pytest
from homeassistant.components import nws
from homeassistant.components.weather import ATTR_FORECAST
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 .const import (
DEFAULT_FORECAST,
DEFAULT_OBSERVATION,
from tests.common import async_fire_time_changed
from tests.components.nws.const import (
EXPECTED_FORECAST_IMPERIAL,
EXPECTED_FORECAST_METRIC,
EXPECTED_OBSERVATION_IMPERIAL,
EXPECTED_OBSERVATION_METRIC,
MINIMAL_CONFIG,
NONE_FORECAST,
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 = {
"weather": {
"platform": "nws",
@ -48,21 +39,29 @@ HOURLY_CONFIG = {
(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."""
hass.config.units = units
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.return_value = mock_coro()
instance.observation = DEFAULT_OBSERVATION
instance.forecast = DEFAULT_FORECAST
assert await async_setup_component(hass, nws.DOMAIN, MINIMAL_CONFIG)
await hass.async_block_till_done()
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.state == "sunny"
@ -75,50 +74,17 @@ async def test_imperial_metric(hass, units, result_observation, result_forecast)
assert forecast[0].get(key) == value
async def test_hourly(hass):
"""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):
async def test_none_values(hass, mock_simple_nws):
"""Test with none values in observation and forecast dicts."""
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.return_value = mock_coro()
instance.observation = NONE_OBSERVATION
instance.forecast = NONE_FORECAST
await async_setup_component(hass, "weather", MINIMAL_CONFIG)
instance = mock_simple_nws.return_value
instance.observation = NONE_OBSERVATION
instance.forecast = NONE_FORECAST
state = hass.states.get("weather.abc")
assert state
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.state == "unknown"
data = state.attributes
for key in EXPECTED_OBSERVATION_IMPERIAL:
assert data.get(key) is None
@ -128,19 +94,16 @@ async def test_none_values(hass):
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."""
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.return_value = mock_coro()
instance.observation = None
instance.forecast = None
await async_setup_component(hass, "weather", MINIMAL_CONFIG)
instance = mock_simple_nws.return_value
instance.observation = None
instance.forecast = None
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.state == "unknown"
@ -152,46 +115,68 @@ async def test_none(hass):
assert forecast is None
async def test_error_station(hass):
async def test_error_station(hass, mock_simple_nws):
"""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")
assert state is None
instance = mock_simple_nws.return_value
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."""
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.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)
instance = mock_simple_nws.return_value
instance.update_observation.side_effect = aiohttp.ClientError
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."""
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.side_effect = aiohttp.ClientError
instance.observation = None
instance.forecast = None
await async_setup_component(hass, "weather", MINIMAL_CONFIG)
assert "Error updating forecast from station ABC" in caplog.text
instance = mock_simple_nws.return_value
instance.update_forecast.side_effect = aiohttp.ClientError
assert await async_setup_component(hass, nws.DOMAIN, MINIMAL_CONFIG)
await hass.async_block_till_done()
instance.update_forecast.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" 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