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:
parent
9eb0415234
commit
e6148d223a
4 changed files with 139 additions and 25 deletions
|
@ -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,
|
||||||
|
|
|
@ -8,7 +8,8 @@
|
||||||
"data": {
|
"data": {
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"latitude": "Latitude",
|
"latitude": "Latitude",
|
||||||
"longitude": "Longitude"
|
"longitude": "Longitude",
|
||||||
|
"mode": "Mode"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Add table
Reference in a new issue