From ef687d3d7fd441275284260f3bcb5a65fbc2397b Mon Sep 17 00:00:00 2001 From: ktdad Date: Sat, 1 Jun 2019 07:20:59 -0400 Subject: [PATCH] check and test for missing observations --- homeassistant/components/nws/weather.py | 57 +++++-- homeassistant/components/weather/__init__.py | 12 +- tests/components/nws/test_nws.py | 167 +++++++++++++++++-- 3 files changed, 201 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/nws/weather.py b/homeassistant/components/nws/weather.py index 64745b49628..fcdbd53e04f 100644 --- a/homeassistant/components/nws/weather.py +++ b/homeassistant/components/nws/weather.py @@ -18,7 +18,7 @@ from homeassistant.const import ( CONF_API_KEY, CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE, LENGTH_KILOMETERS, LENGTH_METERS, LENGTH_MILES, PRESSURE_HPA, PRESSURE_PA, PRESSURE_INHG, TEMP_CELSIUS, TEMP_FAHRENHEIT) -from homeassistant.exceptions import PlatformNotReady +from homeassistant.exceptions import PlatformNotReady, ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers import config_validation as cv from homeassistant.util import Throttle @@ -139,7 +139,7 @@ async def async_setup_platform(hass, config, async_add_entities, if None in (latitude, longitude): _LOGGER.error("Latitude/longitude not set in Home Assistant config") - return + return ConfigEntryNotReady websession = async_get_clientsession(hass) # ID request as being from HA, pynws prepends the api_key in addition @@ -154,7 +154,7 @@ async def async_setup_platform(hass, config, async_add_entities, stations = await nws.stations() except ERRORS as status: _LOGGER.error("Error getting station list for %s: %s", - nws.latlon, status) + (latitude, longitude), status) raise PlatformNotReady _LOGGER.debug("Station list: %s", stations) nws.station = stations[0] @@ -197,9 +197,9 @@ class NWSWeather(WeatherEntity): self._nws.station, status) else: self._observation = obs[0] - if 'rawMessage' in self._observation.keys(): - self._metar_obs = self._metar( - self._observation['rawMessage']) + metar_msg = self._observation.get('rawMessage') + if metar_msg: + self._metar_obs = self._metar(metar_msg) else: self._metar_obs = None _LOGGER.debug("Observations: %s", self._observation) @@ -229,7 +229,9 @@ class NWSWeather(WeatherEntity): @property def temperature(self): """Return the current temperature.""" - temp_c = self._observation['temperature']['value'] + temp_c = None + if self._observation: + temp_c = self._observation.get('temperature', {}).get('value') if temp_c is None and self._metar_obs and self._metar_obs.temp: temp_c = self._metar_obs.temp.value(units='C') if temp_c is not None: @@ -239,7 +241,10 @@ class NWSWeather(WeatherEntity): @property def pressure(self): """Return the current pressure.""" - pressure_pa = self._observation['seaLevelPressure']['value'] + pressure_pa = None + if self._observation: + pressure_pa = self._observation.get('seaLevelPressure', + {}).get('value') if pressure_pa is None and self._metar_obs and self._metar_obs.press: pressure_hpa = self._metar_obs.press.value(units='HPA') @@ -247,9 +252,11 @@ class NWSWeather(WeatherEntity): return None pressure_pa = convert_pressure(pressure_hpa, PRESSURE_HPA, PRESSURE_PA) - + if pressure_pa is None: + return None if self._is_metric: - pressure = convert_pressure(pressure_pa, PRESSURE_PA, PRESSURE_HPA) + pressure = convert_pressure(pressure_pa, + PRESSURE_PA, PRESSURE_HPA) pressure = round(pressure) else: pressure = convert_pressure(pressure_pa, @@ -260,12 +267,18 @@ class NWSWeather(WeatherEntity): @property def humidity(self): """Return the name of the sensor.""" - return self._observation['relativeHumidity']['value'] + humidity = None + if self._observation: + humidity = self._observation.get('relativeHumidity', + {}).get('value') + return humidity @property def wind_speed(self): """Return the current windspeed.""" - wind_m_s = self._observation['windSpeed']['value'] + wind_m_s = None + if self._observation: + wind_m_s = self._observation.get('windSpeed', {}).get('value') if wind_m_s is None and self._metar_obs and self._metar_obs.wind_speed: wind_m_s = self._metar_obs.wind_speed.value(units='MPS') if wind_m_s is None: @@ -282,7 +295,10 @@ class NWSWeather(WeatherEntity): @property def wind_bearing(self): """Return the current wind bearing (degrees).""" - wind_bearing = self._observation['windDirection']['value'] + wind_bearing = None + if self._observation: + wind_bearing = self._observation.get('windDirection', + {}).get('value') if wind_bearing is None and (self._metar_obs and self._metar_obs.wind_dir): wind_bearing = self._metar_obs.wind_dir.value() @@ -296,14 +312,21 @@ class NWSWeather(WeatherEntity): @property def condition(self): """Return current condition.""" - time, weather = parse_icon(self._observation['icon']) - cond, _ = convert_condition(time, weather) - return cond + icon = None + if self._observation: + icon = self._observation.get('icon') + if icon: + time, weather = parse_icon(self._observation['icon']) + cond, _ = convert_condition(time, weather) + return cond + return @property def visibility(self): """Return visibility.""" - vis_m = self._observation['visibility']['value'] + vis_m = None + if self._observation: + vis_m = self._observation.get('visibility', {}).get('value') if vis_m is None and self._metar_obs and self._metar_obs.vis: vis_m = self._metar_obs.vis.value(units='M') if vis_m is None: diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index 34cd86347f2..91f64992c84 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -116,11 +116,13 @@ class WeatherEntity(Entity): @property def state_attributes(self): """Return the state attributes.""" - data = { - ATTR_WEATHER_TEMPERATURE: show_temp( - self.hass, self.temperature, self.temperature_unit, - self.precision), - } + data = {} + if self.temperature is not None: + data = { + ATTR_WEATHER_TEMPERATURE: show_temp( + self.hass, self.temperature, self.temperature_unit, + self.precision), + } humidity = self.humidity if humidity is not None: diff --git a/tests/components/nws/test_nws.py b/tests/components/nws/test_nws.py index 83caadc0c1e..b2d32f68001 100644 --- a/tests/components/nws/test_nws.py +++ b/tests/components/nws/test_nws.py @@ -2,6 +2,8 @@ import unittest from unittest.mock import patch +import aiohttp + from homeassistant.components import weather from homeassistant.components.nws.weather import ATTR_FORECAST_PRECIP_PROB from homeassistant.components.weather import ( @@ -55,6 +57,19 @@ OBS_METAR = [{ "relativeHumidity": {"value": None, "qualityControl": "qc:Z"}, }] +OBS_NONE = [{ + "rawMessage": None, + "textDescription": None, + "icon": None, + "temperature": {"value": None, "qualityControl": "qc:Z"}, + "windDirection": {"value": None, "qualityControl": "qc:Z"}, + "windSpeed": {"value": None, "qualityControl": "qc:Z"}, + "seaLevelPressure": {"value": None, "qualityControl": "qc:Z"}, + "visibility": {"value": None, "qualityControl": "qc:Z"}, + "relativeHumidity": {"value": None, "qualityControl": "qc:Z"}, +}] + + FORE = [{ 'endTime': '2018-12-21T18:00:00-05:00', 'windSpeed': '8 to 10 mph', @@ -356,7 +371,7 @@ class TestNwsMetric(unittest.TestCase): convert_distance(9, LENGTH_MILES, LENGTH_KILOMETERS)) -class MockNws_Metar(): +class MockNws_Metar(MockNws): """Mock Station from pynws.""" def __init__(self, websession, latlon, userid): @@ -367,18 +382,6 @@ class MockNws_Metar(): """Mock Observation.""" return OBS_METAR - async def forecast(self): - """Mock Forecast.""" - return FORE - - async def forecast_hourly(self): - """Mock Hourly Forecast.""" - return HOURLY_FORE - - async def stations(self): - """Mock stations.""" - return [STN] - class TestNWS_Metar(unittest.TestCase): """Test the NWS weather component with metar code.""" @@ -429,3 +432,141 @@ class TestNWS_Metar(unittest.TestCase): assert data.get(ATTR_WEATHER_WIND_BEARING) == truth.wind_dir.value() vis = convert_distance(truth.vis.value(), LENGTH_METERS, LENGTH_MILES) assert data.get(ATTR_WEATHER_VISIBILITY) == round(vis) + + +class MockNwsFailObs(MockNws): + """Mock Station from pynws.""" + + def __init__(self, websession, latlon, userid): + """Init mock nws.""" + pass + + async def observations(self, limit): + """Mock Observation.""" + raise aiohttp.ClientError + + +class MockNwsFailStn(MockNws): + """Mock Station from pynws.""" + + def __init__(self, websession, latlon, userid): + """Init mock nws.""" + pass + + async def stations(self): + """Mock Observation.""" + raise aiohttp.ClientError + + +class MockNwsFailFore(MockNws): + """Mock Station from pynws.""" + + def __init__(self, websession, latlon, userid): + """Init mock nws.""" + pass + + async def forecast(self): + """Mock Observation.""" + raise aiohttp.ClientError + + +class MockNws_NoObs(MockNws): + """Mock Station from pynws.""" + + def __init__(self, websession, latlon, userid): + """Init mock nws.""" + pass + + async def observations(self, limit): + """Mock Observation.""" + return OBS_NONE + + +class TestFailures(unittest.TestCase): + """Test the NWS weather component.""" + + def setUp(self): + """Set up things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.hass.config.units = IMPERIAL_SYSTEM + self.lat = self.hass.config.latitude = 40.00 + self.lon = self.hass.config.longitude = -8.00 + + def tearDown(self): + """Stop down everything that was started.""" + self.hass.stop() + + @MockDependency("metar") + @MockDependency("pynws") + @patch("pynws.Nws", new=MockNwsFailObs) + def test_obs_fail(self, mock_metar, mock_pynws): + """Test for successfully setting up the NWS platform with name.""" + assert setup_component(self.hass, weather.DOMAIN, { + 'weather': { + 'name': 'HomeWeather', + 'platform': 'nws', + 'api_key': 'test_email', + } + }) + + @MockDependency("metar") + @MockDependency("pynws") + @patch("pynws.Nws", new=MockNwsFailStn) + def test_fail_stn(self, mock_metar, mock_pynws): + """Test for successfully setting up the NWS platform with name.""" + assert setup_component(self.hass, weather.DOMAIN, { + 'weather': { + 'name': 'HomeWeather', + 'platform': 'nws', + 'api_key': 'test_email', + } + }) + state = self.hass.states.get('weather.homeweather') + assert state is None + + @MockDependency("metar") + @MockDependency("pynws") + @patch("pynws.Nws", new=MockNwsFailFore) + def test_fail_fore(self, mock_metar, mock_pynws): + """Test for successfully setting up the NWS platform with name.""" + assert setup_component(self.hass, weather.DOMAIN, { + 'weather': { + 'name': 'HomeWeather', + 'platform': 'nws', + 'api_key': 'test_email', + } + }) + + @MockDependency("metar") + @MockDependency("pynws") + @patch("pynws.Nws", new=MockNws_NoObs) + def test_no_obs(self, mock_metar, mock_pynws): + """Test for successfully setting up the NWS platform with name.""" + assert setup_component(self.hass, weather.DOMAIN, { + 'weather': { + 'name': 'HomeWeather', + 'platform': 'nws', + 'api_key': 'test_email', + } + }) + state = self.hass.states.get('weather.homeweather') + assert state.state == 'unknown' + + @MockDependency("metar") + @MockDependency("pynws") + @patch("pynws.Nws", new=MockNws) + def test_no_lat(self, mock_metar, mock_pynws): + """Test for successfully setting up the NWS platform with name.""" + hass = self.hass + hass.config.latitude = None + + assert setup_component(self.hass, weather.DOMAIN, { + 'weather': { + 'name': 'HomeWeather', + 'platform': 'nws', + 'api_key': 'test_email', + } + }) + + state = self.hass.states.get('weather.homeweather') + assert state is None