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>
This commit is contained in:
parent
e94726ec84
commit
6c4b5291e1
6 changed files with 65 additions and 20 deletions
|
@ -7,5 +7,5 @@
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["metar", "pynws"],
|
"loggers": ["metar", "pynws"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"requirements": ["pynws==1.4.1"]
|
"requirements": ["pynws==1.5.0"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,8 @@ from homeassistant.components.weather import (
|
||||||
ATTR_CONDITION_CLEAR_NIGHT,
|
ATTR_CONDITION_CLEAR_NIGHT,
|
||||||
ATTR_CONDITION_SUNNY,
|
ATTR_CONDITION_SUNNY,
|
||||||
ATTR_FORECAST_CONDITION,
|
ATTR_FORECAST_CONDITION,
|
||||||
|
ATTR_FORECAST_HUMIDITY,
|
||||||
|
ATTR_FORECAST_NATIVE_DEW_POINT,
|
||||||
ATTR_FORECAST_NATIVE_TEMP,
|
ATTR_FORECAST_NATIVE_TEMP,
|
||||||
ATTR_FORECAST_NATIVE_WIND_SPEED,
|
ATTR_FORECAST_NATIVE_WIND_SPEED,
|
||||||
ATTR_FORECAST_PRECIPITATION_PROBABILITY,
|
ATTR_FORECAST_PRECIPITATION_PROBABILITY,
|
||||||
|
@ -52,16 +54,13 @@ from .const import (
|
||||||
PARALLEL_UPDATES = 0
|
PARALLEL_UPDATES = 0
|
||||||
|
|
||||||
|
|
||||||
def convert_condition(
|
def convert_condition(time: str, weather: tuple[tuple[str, int | None], ...]) -> str:
|
||||||
time: str, weather: tuple[tuple[str, int | None], ...]
|
|
||||||
) -> tuple[str, int | None]:
|
|
||||||
"""Convert NWS codes to HA condition.
|
"""Convert NWS codes to HA condition.
|
||||||
|
|
||||||
Choose first condition in CONDITION_CLASSES that exists in weather code.
|
Choose first condition in CONDITION_CLASSES that exists in weather code.
|
||||||
If no match is found, return first condition from NWS
|
If no match is found, return first condition from NWS
|
||||||
"""
|
"""
|
||||||
conditions: list[str] = [w[0] for w in weather]
|
conditions: list[str] = [w[0] for w in weather]
|
||||||
prec_probs = [w[1] or 0 for w in weather]
|
|
||||||
|
|
||||||
# Choose condition with highest priority.
|
# Choose condition with highest priority.
|
||||||
cond = next(
|
cond = next(
|
||||||
|
@ -75,10 +74,10 @@ def convert_condition(
|
||||||
|
|
||||||
if cond == "clear":
|
if cond == "clear":
|
||||||
if time == "day":
|
if time == "day":
|
||||||
return ATTR_CONDITION_SUNNY, max(prec_probs)
|
return ATTR_CONDITION_SUNNY
|
||||||
if time == "night":
|
if time == "night":
|
||||||
return ATTR_CONDITION_CLEAR_NIGHT, max(prec_probs)
|
return ATTR_CONDITION_CLEAR_NIGHT
|
||||||
return cond, max(prec_probs)
|
return cond
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
|
@ -219,8 +218,7 @@ class NWSWeather(WeatherEntity):
|
||||||
time = self.observation.get("iconTime")
|
time = self.observation.get("iconTime")
|
||||||
|
|
||||||
if weather:
|
if weather:
|
||||||
cond, _ = convert_condition(time, weather)
|
return convert_condition(time, weather)
|
||||||
return cond
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -256,16 +254,27 @@ class NWSWeather(WeatherEntity):
|
||||||
else:
|
else:
|
||||||
data[ATTR_FORECAST_NATIVE_TEMP] = None
|
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:
|
if self.mode == DAYNIGHT:
|
||||||
data[ATTR_FORECAST_DAYTIME] = forecast_entry.get("isDaytime")
|
data[ATTR_FORECAST_DAYTIME] = forecast_entry.get("isDaytime")
|
||||||
|
|
||||||
time = forecast_entry.get("iconTime")
|
time = forecast_entry.get("iconTime")
|
||||||
weather = forecast_entry.get("iconWeather")
|
weather = forecast_entry.get("iconWeather")
|
||||||
if time and weather:
|
data[ATTR_FORECAST_CONDITION] = (
|
||||||
cond, precip = convert_condition(time, weather)
|
convert_condition(time, weather) if time and weather else None
|
||||||
else:
|
)
|
||||||
cond, precip = None, None
|
|
||||||
data[ATTR_FORECAST_CONDITION] = cond
|
|
||||||
data[ATTR_FORECAST_PRECIPITATION_PROBABILITY] = precip
|
|
||||||
|
|
||||||
data[ATTR_FORECAST_WIND_BEARING] = forecast_entry.get("windBearing")
|
data[ATTR_FORECAST_WIND_BEARING] = forecast_entry.get("windBearing")
|
||||||
wind_speed = forecast_entry.get("windSpeedAvg")
|
wind_speed = forecast_entry.get("windSpeedAvg")
|
||||||
|
|
|
@ -1861,7 +1861,7 @@ pynuki==1.6.2
|
||||||
pynut2==2.1.2
|
pynut2==2.1.2
|
||||||
|
|
||||||
# homeassistant.components.nws
|
# homeassistant.components.nws
|
||||||
pynws==1.4.1
|
pynws==1.5.0
|
||||||
|
|
||||||
# homeassistant.components.nx584
|
# homeassistant.components.nx584
|
||||||
pynx584==0.5
|
pynx584==0.5
|
||||||
|
|
|
@ -1377,7 +1377,7 @@ pynuki==1.6.2
|
||||||
pynut2==2.1.2
|
pynut2==2.1.2
|
||||||
|
|
||||||
# homeassistant.components.nws
|
# homeassistant.components.nws
|
||||||
pynws==1.4.1
|
pynws==1.5.0
|
||||||
|
|
||||||
# homeassistant.components.nx584
|
# homeassistant.components.nx584
|
||||||
pynx584==0.5
|
pynx584==0.5
|
||||||
|
|
|
@ -3,6 +3,8 @@ from homeassistant.components.nws.const import CONF_STATION
|
||||||
from homeassistant.components.weather import (
|
from homeassistant.components.weather import (
|
||||||
ATTR_CONDITION_LIGHTNING_RAINY,
|
ATTR_CONDITION_LIGHTNING_RAINY,
|
||||||
ATTR_FORECAST_CONDITION,
|
ATTR_FORECAST_CONDITION,
|
||||||
|
ATTR_FORECAST_DEW_POINT,
|
||||||
|
ATTR_FORECAST_HUMIDITY,
|
||||||
ATTR_FORECAST_PRECIPITATION_PROBABILITY,
|
ATTR_FORECAST_PRECIPITATION_PROBABILITY,
|
||||||
ATTR_FORECAST_TEMP,
|
ATTR_FORECAST_TEMP,
|
||||||
ATTR_FORECAST_TIME,
|
ATTR_FORECAST_TIME,
|
||||||
|
@ -59,6 +61,9 @@ DEFAULT_OBSERVATION = {
|
||||||
"windGust": 20,
|
"windGust": 20,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CLEAR_NIGHT_OBSERVATION = DEFAULT_OBSERVATION.copy()
|
||||||
|
CLEAR_NIGHT_OBSERVATION["iconTime"] = "night"
|
||||||
|
|
||||||
SENSOR_EXPECTED_OBSERVATION_METRIC = {
|
SENSOR_EXPECTED_OBSERVATION_METRIC = {
|
||||||
"dewpoint": "5",
|
"dewpoint": "5",
|
||||||
"temperature": "10",
|
"temperature": "10",
|
||||||
|
@ -183,6 +188,9 @@ DEFAULT_FORECAST = [
|
||||||
"timestamp": "2019-08-12T23:53:00+00:00",
|
"timestamp": "2019-08-12T23:53:00+00:00",
|
||||||
"iconTime": "night",
|
"iconTime": "night",
|
||||||
"iconWeather": (("lightning-rainy", 40), ("lightning-rainy", 90)),
|
"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_TEMP: 10,
|
||||||
ATTR_FORECAST_WIND_SPEED: 10,
|
ATTR_FORECAST_WIND_SPEED: 10,
|
||||||
ATTR_FORECAST_WIND_BEARING: 180,
|
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 = {
|
EXPECTED_FORECAST_METRIC = {
|
||||||
|
@ -211,7 +221,14 @@ EXPECTED_FORECAST_METRIC = {
|
||||||
2,
|
2,
|
||||||
),
|
),
|
||||||
ATTR_FORECAST_WIND_BEARING: 180,
|
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]}]
|
NONE_FORECAST = [{key: None for key in DEFAULT_FORECAST[0]}]
|
||||||
|
|
|
@ -7,6 +7,7 @@ import pytest
|
||||||
|
|
||||||
from homeassistant.components import nws
|
from homeassistant.components import nws
|
||||||
from homeassistant.components.weather import (
|
from homeassistant.components.weather import (
|
||||||
|
ATTR_CONDITION_CLEAR_NIGHT,
|
||||||
ATTR_CONDITION_SUNNY,
|
ATTR_CONDITION_SUNNY,
|
||||||
ATTR_FORECAST,
|
ATTR_FORECAST,
|
||||||
DOMAIN as WEATHER_DOMAIN,
|
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 homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
|
CLEAR_NIGHT_OBSERVATION,
|
||||||
EXPECTED_FORECAST_IMPERIAL,
|
EXPECTED_FORECAST_IMPERIAL,
|
||||||
EXPECTED_FORECAST_METRIC,
|
EXPECTED_FORECAST_METRIC,
|
||||||
NONE_FORECAST,
|
NONE_FORECAST,
|
||||||
|
@ -97,6 +99,23 @@ async def test_imperial_metric(
|
||||||
assert forecast[0].get(key) == value
|
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:
|
async def test_none_values(hass: HomeAssistant, mock_simple_nws, no_sensor) -> None:
|
||||||
"""Test with none values in observation and forecast dicts."""
|
"""Test with none values in observation and forecast dicts."""
|
||||||
instance = mock_simple_nws.return_value
|
instance = mock_simple_nws.return_value
|
||||||
|
|
Loading…
Add table
Reference in a new issue