Update pyipma to 2.0 (#30746)

* 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
This commit is contained in:
Abílio Costa 2020-01-21 16:04:22 +00:00 committed by springstan
parent c2df4f56a3
commit 2aff913d9b
6 changed files with 138 additions and 110 deletions

View File

@ -168,7 +168,7 @@ homeassistant/components/intent/* @home-assistant/core
homeassistant/components/intesishome/* @jnimmo homeassistant/components/intesishome/* @jnimmo
homeassistant/components/ios/* @robbiet480 homeassistant/components/ios/* @robbiet480
homeassistant/components/iperf3/* @rohankapoorcom homeassistant/components/iperf3/* @rohankapoorcom
homeassistant/components/ipma/* @dgomes homeassistant/components/ipma/* @dgomes @abmantis
homeassistant/components/iqvia/* @bachya homeassistant/components/iqvia/* @bachya
homeassistant/components/irish_rail_transport/* @ttroy50 homeassistant/components/irish_rail_transport/* @ttroy50
homeassistant/components/izone/* @Swamp-Ig homeassistant/components/izone/* @Swamp-Ig

View File

@ -3,7 +3,7 @@
"name": "Instituto Português do Mar e Atmosfera (IPMA)", "name": "Instituto Português do Mar e Atmosfera (IPMA)",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/ipma", "documentation": "https://www.home-assistant.io/integrations/ipma",
"requirements": ["pyipma==1.2.1"], "requirements": ["pyipma==2.0.2"],
"dependencies": [], "dependencies": [],
"codeowners": ["@dgomes"] "codeowners": ["@dgomes", "@abmantis"]
} }

View File

@ -3,7 +3,8 @@ from datetime import timedelta
import logging import logging
import async_timeout import async_timeout
from pyipma import Station from pyipma.api import IPMA_API
from pyipma.location import Location
import voluptuous as vol import voluptuous as vol
from homeassistant.components.weather import ( from homeassistant.components.weather import (
@ -24,8 +25,6 @@ _LOGGER = logging.getLogger(__name__)
ATTRIBUTION = "Instituto Português do Mar e Atmosfera" ATTRIBUTION = "Instituto Português do Mar e Atmosfera"
ATTR_WEATHER_DESCRIPTION = "description"
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=30) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=30)
CONDITION_CLASSES = { CONDITION_CLASSES = {
@ -68,9 +67,10 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
_LOGGER.error("Latitude or longitude not set in Home Assistant config") _LOGGER.error("Latitude or longitude not set in Home Assistant config")
return return
station = await async_get_station(hass, latitude, longitude) api = await async_get_api(hass)
location = await async_get_location(hass, api, latitude, longitude)
async_add_entities([IPMAWeather(station, config)], True) async_add_entities([IPMAWeather(location, api, config)], True)
async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_entry(hass, config_entry, async_add_entities):
@ -78,61 +78,71 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
latitude = config_entry.data[CONF_LATITUDE] latitude = config_entry.data[CONF_LATITUDE]
longitude = config_entry.data[CONF_LONGITUDE] longitude = config_entry.data[CONF_LONGITUDE]
station = await async_get_station(hass, latitude, longitude) api = await async_get_api(hass)
location = await async_get_location(hass, api, latitude, longitude)
async_add_entities([IPMAWeather(station, config_entry.data)], True) async_add_entities([IPMAWeather(location, api, config_entry.data)], True)
async def async_get_station(hass, latitude, longitude): async def async_get_api(hass):
"""Retrieve weather station, station name to be used as the entity name.""" """Get the pyipma api object."""
websession = async_get_clientsession(hass) websession = async_get_clientsession(hass)
return IPMA_API(websession)
async def async_get_location(hass, api, latitude, longitude):
"""Retrieve pyipma location, location name to be used as the entity name."""
with async_timeout.timeout(10): with async_timeout.timeout(10):
station = await Station.get(websession, 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",
latitude, latitude,
longitude, longitude,
station.local, location.station,
) )
return station return location
class IPMAWeather(WeatherEntity): class IPMAWeather(WeatherEntity):
"""Representation of a weather condition.""" """Representation of a weather condition."""
def __init__(self, station, config): def __init__(self, location: Location, api: IPMA_API, config):
"""Initialise the platform with a data instance and station name.""" """Initialise the platform with a data instance and station name."""
self._station_name = config.get(CONF_NAME, station.local) self._api = api
self._station = station self._location_name = config.get(CONF_NAME, location.name)
self._condition = None self._location = location
self._observation = None
self._forecast = None self._forecast = None
self._description = None
@Throttle(MIN_TIME_BETWEEN_UPDATES) @Throttle(MIN_TIME_BETWEEN_UPDATES)
async def async_update(self): async def async_update(self):
"""Update Condition and Forecast.""" """Update Condition and Forecast."""
with async_timeout.timeout(10): with async_timeout.timeout(10):
_new_condition = await self._station.observation() new_observation = await self._location.observation(self._api)
if _new_condition is None: new_forecast = await self._location.forecast(self._api)
_LOGGER.warning("Could not update weather conditions")
return if new_observation:
self._condition = _new_condition self._observation = new_observation
else:
_LOGGER.warning("Could not update weather observation")
if new_forecast:
self._forecast = [f for f in new_forecast if f.forecasted_hours == 24]
else:
_LOGGER.warning("Could not update weather forecast")
_LOGGER.debug( _LOGGER.debug(
"Updating station %s, condition %s", "Updated location %s, observation %s",
self._station.local, self._location.name,
self._condition, self._observation,
) )
self._forecast = await self._station.forecast()
self._description = self._forecast[0].description
@property @property
def unique_id(self) -> str: def unique_id(self) -> str:
"""Return a unique id.""" """Return a unique id."""
return f"{self._station.latitude}, {self._station.longitude}" return f"{self._location.station_latitude}, {self._location.station_longitude}"
@property @property
def attribution(self): def attribution(self):
@ -142,7 +152,7 @@ class IPMAWeather(WeatherEntity):
@property @property
def name(self): def name(self):
"""Return the name of the station.""" """Return the name of the station."""
return self._station_name return self._location_name
@property @property
def condition(self): def condition(self):
@ -154,7 +164,7 @@ class IPMAWeather(WeatherEntity):
( (
k k
for k, v in CONDITION_CLASSES.items() for k, v in CONDITION_CLASSES.items()
if self._forecast[0].idWeatherType in v if self._forecast[0].weather_type in v
), ),
None, None,
) )
@ -162,42 +172,42 @@ class IPMAWeather(WeatherEntity):
@property @property
def temperature(self): def temperature(self):
"""Return the current temperature.""" """Return the current temperature."""
if not self._condition: if not self._observation:
return None return None
return self._condition.temperature return self._observation.temperature
@property @property
def pressure(self): def pressure(self):
"""Return the current pressure.""" """Return the current pressure."""
if not self._condition: if not self._observation:
return None return None
return self._condition.pressure return self._observation.pressure
@property @property
def humidity(self): def humidity(self):
"""Return the name of the sensor.""" """Return the name of the sensor."""
if not self._condition: if not self._observation:
return None return None
return self._condition.humidity return self._observation.humidity
@property @property
def wind_speed(self): def wind_speed(self):
"""Return the current windspeed.""" """Return the current windspeed."""
if not self._condition: if not self._observation:
return None return None
return self._condition.windspeed return self._observation.wind_intensity_km
@property @property
def wind_bearing(self): def wind_bearing(self):
"""Return the current wind bearing (degrees).""" """Return the current wind bearing (degrees)."""
if not self._condition: if not self._observation:
return None return None
return self._condition.winddirection return self._observation.wind_direction
@property @property
def temperature_unit(self): def temperature_unit(self):
@ -207,33 +217,25 @@ class IPMAWeather(WeatherEntity):
@property @property
def forecast(self): def forecast(self):
"""Return the forecast array.""" """Return the forecast array."""
if self._forecast: if not self._forecast:
fcdata_out = [] return []
for data_in in self._forecast:
data_out = {} fcdata_out = [
data_out[ATTR_FORECAST_TIME] = data_in.forecastDate {
data_out[ATTR_FORECAST_CONDITION] = next( ATTR_FORECAST_TIME: data_in.forecast_date,
ATTR_FORECAST_CONDITION: next(
( (
k k
for k, v in CONDITION_CLASSES.items() for k, v in CONDITION_CLASSES.items()
if int(data_in.idWeatherType) in v if int(data_in.weather_type) in v
), ),
None, None,
) ),
data_out[ATTR_FORECAST_TEMP_LOW] = data_in.tMin ATTR_FORECAST_TEMP_LOW: data_in.min_temperature,
data_out[ATTR_FORECAST_TEMP] = data_in.tMax ATTR_FORECAST_TEMP: data_in.max_temperature,
data_out[ATTR_FORECAST_PRECIPITATION] = data_in.precipitaProb ATTR_FORECAST_PRECIPITATION: data_in.precipitation_probability,
}
for data_in in self._forecast
]
fcdata_out.append(data_out) return fcdata_out
return fcdata_out
@property
def device_state_attributes(self):
"""Return the state attributes."""
data = dict()
if self._description:
data[ATTR_WEATHER_DESCRIPTION] = self._description
return data

View File

@ -1297,7 +1297,7 @@ pyicloud==0.9.1
pyintesishome==1.6 pyintesishome==1.6
# homeassistant.components.ipma # homeassistant.components.ipma
pyipma==1.2.1 pyipma==2.0.2
# homeassistant.components.iqvia # homeassistant.components.iqvia
pyiqvia==0.2.1 pyiqvia==0.2.1

View File

@ -451,7 +451,7 @@ pyhomematic==0.1.63
pyicloud==0.9.1 pyicloud==0.9.1
# homeassistant.components.ipma # homeassistant.components.ipma
pyipma==1.2.1 pyipma==2.0.2
# homeassistant.components.iqvia # homeassistant.components.iqvia
pyiqvia==0.2.1 pyiqvia==0.2.1

View File

@ -18,73 +18,99 @@ from tests.common import MockConfigEntry, mock_coro
TEST_CONFIG = {"name": "HomeTown", "latitude": "40.00", "longitude": "-8.00"} TEST_CONFIG = {"name": "HomeTown", "latitude": "40.00", "longitude": "-8.00"}
class MockStation: class MockLocation:
"""Mock Station from pyipma.""" """Mock Location from pyipma."""
async def observation(self): async def observation(self, api):
"""Mock Observation.""" """Mock Observation."""
Observation = namedtuple( Observation = namedtuple(
"Observation", "Observation",
[ [
"temperature", "accumulated_precipitation",
"humidity", "humidity",
"windspeed",
"winddirection",
"precipitation",
"pressure", "pressure",
"description", "radiation",
"temperature",
"wind_direction",
"wind_intensity_km",
], ],
) )
return Observation(18, 71.0, 3.94, "NW", 0, 1000.0, "---") return Observation(0.0, 71.0, 1000.0, 0.0, 18.0, "NW", 3.94)
async def forecast(self): async def forecast(self, api):
"""Mock Forecast.""" """Mock Forecast."""
Forecast = namedtuple( Forecast = namedtuple(
"Forecast", "Forecast",
[ [
"precipitaProb", "feels_like_temperature",
"tMin", "forecast_date",
"tMax", "forecasted_hours",
"predWindDir", "humidity",
"idWeatherType", "max_temperature",
"classWindSpeed", "min_temperature",
"longitude", "precipitation_probability",
"forecastDate", "temperature",
"classPrecInt", "update_date",
"latitude", "weather_type",
"description", "wind_direction",
"wind_strength",
], ],
) )
return [ return [
Forecast( Forecast(
73.0, None,
13.7, "2020-01-15T00:00:00",
18.7, 24,
"NW", None,
6, 16.2,
2, 10.6,
-8.64, "100.0",
"2018-05-31", 13.4,
2, "2020-01-15T07:51:00",
40.61, 9,
"Aguaceiros, com vento Moderado de Noroeste", "S",
) None,
),
Forecast(
"7.7",
"2020-01-15T02:00:00",
1,
"86.9",
None,
None,
"-99.0",
10.6,
"2020-01-15T07:51:00",
10,
"S",
"32.7",
),
] ]
@property @property
def local(self): def name(self):
"""Mock location.""" """Mock location."""
return "HomeTown" return "HomeTown"
@property @property
def latitude(self): def station_latitude(self):
"""Mock latitude.""" """Mock latitude."""
return 0 return 0
@property @property
def longitude(self): def global_id_local(self):
"""Mock global identifier of the location."""
return 1130600
@property
def id_station(self):
"""Mock identifier of the station."""
return 1200545
@property
def station_longitude(self):
"""Mock longitude.""" """Mock longitude."""
return 0 return 0
@ -92,8 +118,8 @@ class MockStation:
async def test_setup_configuration(hass): async def test_setup_configuration(hass):
"""Test for successfully setting up the IPMA platform.""" """Test for successfully setting up the IPMA platform."""
with patch( with patch(
"homeassistant.components.ipma.weather.async_get_station", "homeassistant.components.ipma.weather.async_get_location",
return_value=mock_coro(MockStation()), 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"}}
@ -115,8 +141,8 @@ async def test_setup_configuration(hass):
async def test_setup_config_flow(hass): async def test_setup_config_flow(hass):
"""Test for successfully setting up the IPMA platform.""" """Test for successfully setting up the IPMA platform."""
with patch( with patch(
"homeassistant.components.ipma.weather.async_get_station", "homeassistant.components.ipma.weather.async_get_location",
return_value=mock_coro(MockStation()), return_value=mock_coro(MockLocation()),
): ):
entry = MockConfigEntry(domain="ipma", data=TEST_CONFIG) entry = MockConfigEntry(domain="ipma", data=TEST_CONFIG)
await hass.config_entries.async_forward_entry_setup(entry, WEATHER_DOMAIN) await hass.config_entries.async_forward_entry_setup(entry, WEATHER_DOMAIN)