Allow hourly forecast in IPMA (#30979)

* update ipma component for pyipma 2.0
* fix wind speed; refactor forecast
* update requirements*.txt
* fix tests; update CODEOWNERS; update pyipma to 2.0.1
* minor changes as suggested in PR
* make lint happy
* fix mocking coroutines
* restore old unique id
* fix station lat/lon; update pyipma version
* add hourly forecast option to IPMA
* add forecast tests
* use for instead of lambda
This commit is contained in:
Abílio Costa 2020-02-14 17:04:41 +00:00 committed by GitHub
parent 9eb0415234
commit e6148d223a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 139 additions and 25 deletions

View file

@ -2,10 +2,11 @@
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE, CONF_NAME
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from .const import DOMAIN, HOME_LOCATION_NAME from .const import DOMAIN, HOME_LOCATION_NAME
from .weather import FORECAST_MODE
@config_entries.HANDLERS.register(DOMAIN) @config_entries.HANDLERS.register(DOMAIN)
@ -49,6 +50,7 @@ class IpmaFlowHandler(config_entries.ConfigFlow):
vol.Required(CONF_NAME, default=name): str, vol.Required(CONF_NAME, default=name): str,
vol.Required(CONF_LATITUDE, default=latitude): cv.latitude, vol.Required(CONF_LATITUDE, default=latitude): cv.latitude,
vol.Required(CONF_LONGITUDE, default=longitude): cv.longitude, vol.Required(CONF_LONGITUDE, default=longitude): cv.longitude,
vol.Required(CONF_MODE, default="daily"): vol.In(FORECAST_MODE),
} }
), ),
errors=self._errors, errors=self._errors,

View file

@ -8,7 +8,8 @@
"data": { "data": {
"name": "Name", "name": "Name",
"latitude": "Latitude", "latitude": "Latitude",
"longitude": "Longitude" "longitude": "Longitude",
"mode": "Mode"
} }
} }
}, },

View file

@ -13,13 +13,22 @@ from homeassistant.components.weather import (
ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP,
ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TEMP_LOW,
ATTR_FORECAST_TIME, ATTR_FORECAST_TIME,
ATTR_FORECAST_WIND_BEARING,
ATTR_FORECAST_WIND_SPEED,
PLATFORM_SCHEMA, PLATFORM_SCHEMA,
WeatherEntity, WeatherEntity,
) )
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_CELSIUS from homeassistant.const import (
CONF_LATITUDE,
CONF_LONGITUDE,
CONF_MODE,
CONF_NAME,
TEMP_CELSIUS,
)
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.util import Throttle from homeassistant.util import Throttle
from homeassistant.util.dt import now, parse_datetime
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -44,11 +53,14 @@ CONDITION_CLASSES = {
"exceptional": [], "exceptional": [],
} }
FORECAST_MODE = ["hourly", "daily"]
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{ {
vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_LATITUDE): cv.latitude, vol.Optional(CONF_LATITUDE): cv.latitude,
vol.Optional(CONF_LONGITUDE): cv.longitude, vol.Optional(CONF_LONGITUDE): cv.longitude,
vol.Optional(CONF_MODE, default="daily"): vol.In(FORECAST_MODE),
} }
) )
@ -96,10 +108,12 @@ async def async_get_location(hass, api, latitude, longitude):
location = await Location.get(api, float(latitude), float(longitude)) location = await Location.get(api, float(latitude), float(longitude))
_LOGGER.debug( _LOGGER.debug(
"Initializing for coordinates %s, %s -> station %s", "Initializing for coordinates %s, %s -> station %s (%d, %d)",
latitude, latitude,
longitude, longitude,
location.station, location.station,
location.id_station,
location.global_id_local,
) )
return location return location
@ -112,6 +126,7 @@ class IPMAWeather(WeatherEntity):
"""Initialise the platform with a data instance and station name.""" """Initialise the platform with a data instance and station name."""
self._api = api self._api = api
self._location_name = config.get(CONF_NAME, location.name) self._location_name = config.get(CONF_NAME, location.name)
self._mode = config.get(CONF_MODE)
self._location = location self._location = location
self._observation = None self._observation = None
self._forecast = None self._forecast = None
@ -129,7 +144,7 @@ class IPMAWeather(WeatherEntity):
_LOGGER.warning("Could not update weather observation") _LOGGER.warning("Could not update weather observation")
if new_forecast: if new_forecast:
self._forecast = [f for f in new_forecast if f.forecasted_hours == 24] self._forecast = new_forecast
else: else:
_LOGGER.warning("Could not update weather forecast") _LOGGER.warning("Could not update weather forecast")
@ -220,6 +235,39 @@ class IPMAWeather(WeatherEntity):
if not self._forecast: if not self._forecast:
return [] return []
if self._mode == "hourly":
forecast_filtered = [
x
for x in self._forecast
if x.forecasted_hours == 1
and parse_datetime(x.forecast_date)
> (now().utcnow() - timedelta(hours=1))
]
fcdata_out = [
{
ATTR_FORECAST_TIME: data_in.forecast_date,
ATTR_FORECAST_CONDITION: next(
(
k
for k, v in CONDITION_CLASSES.items()
if int(data_in.weather_type) in v
),
None,
),
ATTR_FORECAST_TEMP: float(data_in.feels_like_temperature),
ATTR_FORECAST_PRECIPITATION: (
data_in.precipitation_probability
if float(data_in.precipitation_probability) >= 0
else None
),
ATTR_FORECAST_WIND_SPEED: data_in.wind_strength,
ATTR_FORECAST_WIND_BEARING: data_in.wind_direction,
}
for data_in in forecast_filtered
]
else:
forecast_filtered = [f for f in self._forecast if f.forecasted_hours == 24]
fcdata_out = [ fcdata_out = [
{ {
ATTR_FORECAST_TIME: data_in.forecast_date, ATTR_FORECAST_TIME: data_in.forecast_date,
@ -234,8 +282,10 @@ class IPMAWeather(WeatherEntity):
ATTR_FORECAST_TEMP_LOW: data_in.min_temperature, ATTR_FORECAST_TEMP_LOW: data_in.min_temperature,
ATTR_FORECAST_TEMP: data_in.max_temperature, ATTR_FORECAST_TEMP: data_in.max_temperature,
ATTR_FORECAST_PRECIPITATION: data_in.precipitation_probability, ATTR_FORECAST_PRECIPITATION: data_in.precipitation_probability,
ATTR_FORECAST_WIND_SPEED: data_in.wind_strength,
ATTR_FORECAST_WIND_BEARING: data_in.wind_direction,
} }
for data_in in self._forecast for data_in in forecast_filtered
] ]
return fcdata_out return fcdata_out

View file

@ -4,6 +4,14 @@ from unittest.mock import patch
from homeassistant.components import weather from homeassistant.components import weather
from homeassistant.components.weather import ( from homeassistant.components.weather import (
ATTR_FORECAST,
ATTR_FORECAST_CONDITION,
ATTR_FORECAST_PRECIPITATION,
ATTR_FORECAST_TEMP,
ATTR_FORECAST_TEMP_LOW,
ATTR_FORECAST_TIME,
ATTR_FORECAST_WIND_BEARING,
ATTR_FORECAST_WIND_SPEED,
ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_HUMIDITY,
ATTR_WEATHER_PRESSURE, ATTR_WEATHER_PRESSURE,
ATTR_WEATHER_TEMPERATURE, ATTR_WEATHER_TEMPERATURE,
@ -12,6 +20,7 @@ from homeassistant.components.weather import (
DOMAIN as WEATHER_DOMAIN, DOMAIN as WEATHER_DOMAIN,
) )
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from homeassistant.util.dt import now
from tests.common import MockConfigEntry, mock_coro from tests.common import MockConfigEntry, mock_coro
@ -71,16 +80,16 @@ class MockLocation:
"2020-01-15T07:51:00", "2020-01-15T07:51:00",
9, 9,
"S", "S",
None, "10",
), ),
Forecast( Forecast(
"7.7", "7.7",
"2020-01-15T02:00:00", now().utcnow().strftime("%Y-%m-%dT%H:%M:%S"),
1, 1,
"86.9", "86.9",
None, None,
None, None,
"-99.0", "80.0",
10.6, 10.6,
"2020-01-15T07:51:00", "2020-01-15T07:51:00",
10, 10,
@ -122,7 +131,9 @@ async def test_setup_configuration(hass):
return_value=mock_coro(MockLocation()), return_value=mock_coro(MockLocation()),
): ):
assert await async_setup_component( assert await async_setup_component(
hass, weather.DOMAIN, {"weather": {"name": "HomeTown", "platform": "ipma"}} hass,
weather.DOMAIN,
{"weather": {"name": "HomeTown", "platform": "ipma", "mode": "hourly"}},
) )
await hass.async_block_till_done() await hass.async_block_till_done()
@ -158,3 +169,53 @@ async def test_setup_config_flow(hass):
assert data.get(ATTR_WEATHER_WIND_SPEED) == 3.94 assert data.get(ATTR_WEATHER_WIND_SPEED) == 3.94
assert data.get(ATTR_WEATHER_WIND_BEARING) == "NW" assert data.get(ATTR_WEATHER_WIND_BEARING) == "NW"
assert state.attributes.get("friendly_name") == "HomeTown" assert state.attributes.get("friendly_name") == "HomeTown"
async def test_daily_forecast(hass):
"""Test for successfully getting daily forecast."""
with patch(
"homeassistant.components.ipma.weather.async_get_location",
return_value=mock_coro(MockLocation()),
):
assert await async_setup_component(
hass,
weather.DOMAIN,
{"weather": {"name": "HomeTown", "platform": "ipma", "mode": "daily"}},
)
await hass.async_block_till_done()
state = hass.states.get("weather.hometown")
assert state.state == "rainy"
forecast = state.attributes.get(ATTR_FORECAST)[0]
assert forecast.get(ATTR_FORECAST_TIME) == "2020-01-15T00:00:00"
assert forecast.get(ATTR_FORECAST_CONDITION) == "rainy"
assert forecast.get(ATTR_FORECAST_TEMP) == 16.2
assert forecast.get(ATTR_FORECAST_TEMP_LOW) == 10.6
assert forecast.get(ATTR_FORECAST_PRECIPITATION) == "100.0"
assert forecast.get(ATTR_FORECAST_WIND_SPEED) == "10"
assert forecast.get(ATTR_FORECAST_WIND_BEARING) == "S"
async def test_hourly_forecast(hass):
"""Test for successfully getting daily forecast."""
with patch(
"homeassistant.components.ipma.weather.async_get_location",
return_value=mock_coro(MockLocation()),
):
assert await async_setup_component(
hass,
weather.DOMAIN,
{"weather": {"name": "HomeTown", "platform": "ipma", "mode": "hourly"}},
)
await hass.async_block_till_done()
state = hass.states.get("weather.hometown")
assert state.state == "rainy"
forecast = state.attributes.get(ATTR_FORECAST)[0]
assert forecast.get(ATTR_FORECAST_CONDITION) == "rainy"
assert forecast.get(ATTR_FORECAST_TEMP) == 7.7
assert forecast.get(ATTR_FORECAST_PRECIPITATION) == "80.0"
assert forecast.get(ATTR_FORECAST_WIND_SPEED) == "32.7"
assert forecast.get(ATTR_FORECAST_WIND_BEARING) == "S"