From 6c4b5291e1df4f1d1d34ea80341460b7095f55b5 Mon Sep 17 00:00:00 2001 From: lymanepp <4195527+lymanepp@users.noreply.github.com> Date: Thu, 6 Jul 2023 17:05:46 -0400 Subject: [PATCH] Add humidity to NWS forecast (#95575) * Add humidity to NWS forecast to address https://github.com/home-assistant/core/issues/95572 * Use pynws 1.5.0 enhancements for probabilityOfPrecipitation, dewpoint, and relativeHumidity. * Update requirements to match pynws version * test for clear night * update docstring --------- Co-authored-by: MatthewFlamm <39341281+MatthewFlamm@users.noreply.github.com> --- homeassistant/components/nws/manifest.json | 2 +- homeassistant/components/nws/weather.py | 39 +++++++++++++--------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/nws/const.py | 21 ++++++++++-- tests/components/nws/test_weather.py | 19 +++++++++++ 6 files changed, 65 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/nws/manifest.json b/homeassistant/components/nws/manifest.json index ed7d825afff..7f5d01f9897 100644 --- a/homeassistant/components/nws/manifest.json +++ b/homeassistant/components/nws/manifest.json @@ -7,5 +7,5 @@ "iot_class": "cloud_polling", "loggers": ["metar", "pynws"], "quality_scale": "platinum", - "requirements": ["pynws==1.4.1"] + "requirements": ["pynws==1.5.0"] } diff --git a/homeassistant/components/nws/weather.py b/homeassistant/components/nws/weather.py index 9edf6e61751..e8a35ba66f1 100644 --- a/homeassistant/components/nws/weather.py +++ b/homeassistant/components/nws/weather.py @@ -8,6 +8,8 @@ from homeassistant.components.weather import ( ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_SUNNY, ATTR_FORECAST_CONDITION, + ATTR_FORECAST_HUMIDITY, + ATTR_FORECAST_NATIVE_DEW_POINT, ATTR_FORECAST_NATIVE_TEMP, ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_PRECIPITATION_PROBABILITY, @@ -52,16 +54,13 @@ from .const import ( PARALLEL_UPDATES = 0 -def convert_condition( - time: str, weather: tuple[tuple[str, int | None], ...] -) -> tuple[str, int | None]: +def convert_condition(time: str, weather: tuple[tuple[str, int | None], ...]) -> str: """Convert NWS codes to HA condition. Choose first condition in CONDITION_CLASSES that exists in weather code. If no match is found, return first condition from NWS """ conditions: list[str] = [w[0] for w in weather] - prec_probs = [w[1] or 0 for w in weather] # Choose condition with highest priority. cond = next( @@ -75,10 +74,10 @@ def convert_condition( if cond == "clear": if time == "day": - return ATTR_CONDITION_SUNNY, max(prec_probs) + return ATTR_CONDITION_SUNNY if time == "night": - return ATTR_CONDITION_CLEAR_NIGHT, max(prec_probs) - return cond, max(prec_probs) + return ATTR_CONDITION_CLEAR_NIGHT + return cond async def async_setup_entry( @@ -219,8 +218,7 @@ class NWSWeather(WeatherEntity): time = self.observation.get("iconTime") if weather: - cond, _ = convert_condition(time, weather) - return cond + return convert_condition(time, weather) return None @property @@ -256,16 +254,27 @@ class NWSWeather(WeatherEntity): else: data[ATTR_FORECAST_NATIVE_TEMP] = None + data[ATTR_FORECAST_PRECIPITATION_PROBABILITY] = forecast_entry.get( + "probabilityOfPrecipitation" + ) + + if (dewp := forecast_entry.get("dewpoint")) is not None: + data[ATTR_FORECAST_NATIVE_DEW_POINT] = TemperatureConverter.convert( + dewp, UnitOfTemperature.FAHRENHEIT, UnitOfTemperature.CELSIUS + ) + else: + data[ATTR_FORECAST_NATIVE_DEW_POINT] = None + + data[ATTR_FORECAST_HUMIDITY] = forecast_entry.get("relativeHumidity") + if self.mode == DAYNIGHT: data[ATTR_FORECAST_DAYTIME] = forecast_entry.get("isDaytime") + time = forecast_entry.get("iconTime") weather = forecast_entry.get("iconWeather") - if time and weather: - cond, precip = convert_condition(time, weather) - else: - cond, precip = None, None - data[ATTR_FORECAST_CONDITION] = cond - data[ATTR_FORECAST_PRECIPITATION_PROBABILITY] = precip + data[ATTR_FORECAST_CONDITION] = ( + convert_condition(time, weather) if time and weather else None + ) data[ATTR_FORECAST_WIND_BEARING] = forecast_entry.get("windBearing") wind_speed = forecast_entry.get("windSpeedAvg") diff --git a/requirements_all.txt b/requirements_all.txt index 3fe51080f28..e214740acec 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1861,7 +1861,7 @@ pynuki==1.6.2 pynut2==2.1.2 # homeassistant.components.nws -pynws==1.4.1 +pynws==1.5.0 # homeassistant.components.nx584 pynx584==0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c33cf3f4c99..583c7c96996 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1377,7 +1377,7 @@ pynuki==1.6.2 pynut2==2.1.2 # homeassistant.components.nws -pynws==1.4.1 +pynws==1.5.0 # homeassistant.components.nx584 pynx584==0.5 diff --git a/tests/components/nws/const.py b/tests/components/nws/const.py index 2048db2a2c3..106b80998ac 100644 --- a/tests/components/nws/const.py +++ b/tests/components/nws/const.py @@ -3,6 +3,8 @@ from homeassistant.components.nws.const import CONF_STATION from homeassistant.components.weather import ( ATTR_CONDITION_LIGHTNING_RAINY, ATTR_FORECAST_CONDITION, + ATTR_FORECAST_DEW_POINT, + ATTR_FORECAST_HUMIDITY, ATTR_FORECAST_PRECIPITATION_PROBABILITY, ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME, @@ -59,6 +61,9 @@ DEFAULT_OBSERVATION = { "windGust": 20, } +CLEAR_NIGHT_OBSERVATION = DEFAULT_OBSERVATION.copy() +CLEAR_NIGHT_OBSERVATION["iconTime"] = "night" + SENSOR_EXPECTED_OBSERVATION_METRIC = { "dewpoint": "5", "temperature": "10", @@ -183,6 +188,9 @@ DEFAULT_FORECAST = [ "timestamp": "2019-08-12T23:53:00+00:00", "iconTime": "night", "iconWeather": (("lightning-rainy", 40), ("lightning-rainy", 90)), + "probabilityOfPrecipitation": 89, + "dewpoint": 4, + "relativeHumidity": 75, }, ] @@ -192,7 +200,9 @@ EXPECTED_FORECAST_IMPERIAL = { ATTR_FORECAST_TEMP: 10, ATTR_FORECAST_WIND_SPEED: 10, ATTR_FORECAST_WIND_BEARING: 180, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 90, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: 89, + ATTR_FORECAST_DEW_POINT: 4, + ATTR_FORECAST_HUMIDITY: 75, } EXPECTED_FORECAST_METRIC = { @@ -211,7 +221,14 @@ EXPECTED_FORECAST_METRIC = { 2, ), ATTR_FORECAST_WIND_BEARING: 180, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 90, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: 89, + ATTR_FORECAST_DEW_POINT: round( + TemperatureConverter.convert( + 4, UnitOfTemperature.FAHRENHEIT, UnitOfTemperature.CELSIUS + ), + 1, + ), + ATTR_FORECAST_HUMIDITY: 75, } NONE_FORECAST = [{key: None for key in DEFAULT_FORECAST[0]}] diff --git a/tests/components/nws/test_weather.py b/tests/components/nws/test_weather.py index ce268796639..06d2c2006d8 100644 --- a/tests/components/nws/test_weather.py +++ b/tests/components/nws/test_weather.py @@ -7,6 +7,7 @@ import pytest from homeassistant.components import nws from homeassistant.components.weather import ( + ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_SUNNY, ATTR_FORECAST, DOMAIN as WEATHER_DOMAIN, @@ -19,6 +20,7 @@ import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM from .const import ( + CLEAR_NIGHT_OBSERVATION, EXPECTED_FORECAST_IMPERIAL, EXPECTED_FORECAST_METRIC, NONE_FORECAST, @@ -97,6 +99,23 @@ async def test_imperial_metric( assert forecast[0].get(key) == value +async def test_night_clear(hass: HomeAssistant, mock_simple_nws, no_sensor) -> None: + """Test with clear-night in observation.""" + instance = mock_simple_nws.return_value + instance.observation = CLEAR_NIGHT_OBSERVATION + + entry = MockConfigEntry( + domain=nws.DOMAIN, + data=NWS_CONFIG, + ) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("weather.abc_daynight") + assert state.state == ATTR_CONDITION_CLEAR_NIGHT + + async def test_none_values(hass: HomeAssistant, mock_simple_nws, no_sensor) -> None: """Test with none values in observation and forecast dicts.""" instance = mock_simple_nws.return_value