mirror of
https://github.com/home-assistant/core.git
synced 2025-07-16 09:47:13 +00:00
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:
parent
f5b7deda72
commit
a3e84791c6
@ -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)
|
||||
)
|
||||
|
54
homeassistant/components/nws/const.py
Normal file
54
homeassistant/components/nws/const.py
Normal 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"],
|
||||
}
|
@ -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"
|
||||
}
|
||||
|
@ -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
|
||||
)
|
||||
|
24
tests/components/nws/conftest.py
Normal file
24
tests/components/nws/conftest.py
Normal 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
|
@ -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 = {
|
||||
|
67
tests/components/nws/test_init.py
Normal file
67
tests/components/nws/test_init.py
Normal 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
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user