diff --git a/CODEOWNERS b/CODEOWNERS index 18359beb5d0..9636f324769 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -168,7 +168,7 @@ homeassistant/components/intent/* @home-assistant/core homeassistant/components/intesishome/* @jnimmo homeassistant/components/ios/* @robbiet480 homeassistant/components/iperf3/* @rohankapoorcom -homeassistant/components/ipma/* @dgomes +homeassistant/components/ipma/* @dgomes @abmantis homeassistant/components/iqvia/* @bachya homeassistant/components/irish_rail_transport/* @ttroy50 homeassistant/components/izone/* @Swamp-Ig diff --git a/homeassistant/components/ipma/manifest.json b/homeassistant/components/ipma/manifest.json index d01bf3e8da4..cd66ce7461b 100644 --- a/homeassistant/components/ipma/manifest.json +++ b/homeassistant/components/ipma/manifest.json @@ -3,7 +3,7 @@ "name": "Instituto Português do Mar e Atmosfera (IPMA)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ipma", - "requirements": ["pyipma==1.2.1"], + "requirements": ["pyipma==2.0.2"], "dependencies": [], - "codeowners": ["@dgomes"] + "codeowners": ["@dgomes", "@abmantis"] } diff --git a/homeassistant/components/ipma/weather.py b/homeassistant/components/ipma/weather.py index c088d76d165..7b07406d007 100644 --- a/homeassistant/components/ipma/weather.py +++ b/homeassistant/components/ipma/weather.py @@ -3,7 +3,8 @@ from datetime import timedelta import logging import async_timeout -from pyipma import Station +from pyipma.api import IPMA_API +from pyipma.location import Location import voluptuous as vol from homeassistant.components.weather import ( @@ -24,8 +25,6 @@ _LOGGER = logging.getLogger(__name__) ATTRIBUTION = "Instituto Português do Mar e Atmosfera" -ATTR_WEATHER_DESCRIPTION = "description" - MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=30) 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") 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): @@ -78,61 +78,71 @@ async def async_setup_entry(hass, config_entry, async_add_entities): latitude = config_entry.data[CONF_LATITUDE] 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): - """Retrieve weather station, station name to be used as the entity name.""" - +async def async_get_api(hass): + """Get the pyipma api object.""" 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): - station = await Station.get(websession, float(latitude), float(longitude)) + location = await Location.get(api, float(latitude), float(longitude)) _LOGGER.debug( "Initializing for coordinates %s, %s -> station %s", latitude, longitude, - station.local, + location.station, ) - return station + return location class IPMAWeather(WeatherEntity): """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.""" - self._station_name = config.get(CONF_NAME, station.local) - self._station = station - self._condition = None + self._api = api + self._location_name = config.get(CONF_NAME, location.name) + self._location = location + self._observation = None self._forecast = None - self._description = None @Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_update(self): """Update Condition and Forecast.""" with async_timeout.timeout(10): - _new_condition = await self._station.observation() - if _new_condition is None: - _LOGGER.warning("Could not update weather conditions") - return - self._condition = _new_condition + new_observation = await self._location.observation(self._api) + new_forecast = await self._location.forecast(self._api) + + if new_observation: + 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( - "Updating station %s, condition %s", - self._station.local, - self._condition, + "Updated location %s, observation %s", + self._location.name, + self._observation, ) - self._forecast = await self._station.forecast() - self._description = self._forecast[0].description @property def unique_id(self) -> str: """Return a unique id.""" - return f"{self._station.latitude}, {self._station.longitude}" + return f"{self._location.station_latitude}, {self._location.station_longitude}" @property def attribution(self): @@ -142,7 +152,7 @@ class IPMAWeather(WeatherEntity): @property def name(self): """Return the name of the station.""" - return self._station_name + return self._location_name @property def condition(self): @@ -154,7 +164,7 @@ class IPMAWeather(WeatherEntity): ( k for k, v in CONDITION_CLASSES.items() - if self._forecast[0].idWeatherType in v + if self._forecast[0].weather_type in v ), None, ) @@ -162,42 +172,42 @@ class IPMAWeather(WeatherEntity): @property def temperature(self): """Return the current temperature.""" - if not self._condition: + if not self._observation: return None - return self._condition.temperature + return self._observation.temperature @property def pressure(self): """Return the current pressure.""" - if not self._condition: + if not self._observation: return None - return self._condition.pressure + return self._observation.pressure @property def humidity(self): """Return the name of the sensor.""" - if not self._condition: + if not self._observation: return None - return self._condition.humidity + return self._observation.humidity @property def wind_speed(self): """Return the current windspeed.""" - if not self._condition: + if not self._observation: return None - return self._condition.windspeed + return self._observation.wind_intensity_km @property def wind_bearing(self): """Return the current wind bearing (degrees).""" - if not self._condition: + if not self._observation: return None - return self._condition.winddirection + return self._observation.wind_direction @property def temperature_unit(self): @@ -207,33 +217,25 @@ class IPMAWeather(WeatherEntity): @property def forecast(self): """Return the forecast array.""" - if self._forecast: - fcdata_out = [] - for data_in in self._forecast: - data_out = {} - data_out[ATTR_FORECAST_TIME] = data_in.forecastDate - data_out[ATTR_FORECAST_CONDITION] = next( + if not self._forecast: + return [] + + 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.idWeatherType) in v + if int(data_in.weather_type) in v ), None, - ) - data_out[ATTR_FORECAST_TEMP_LOW] = data_in.tMin - data_out[ATTR_FORECAST_TEMP] = data_in.tMax - data_out[ATTR_FORECAST_PRECIPITATION] = data_in.precipitaProb + ), + ATTR_FORECAST_TEMP_LOW: data_in.min_temperature, + ATTR_FORECAST_TEMP: data_in.max_temperature, + ATTR_FORECAST_PRECIPITATION: data_in.precipitation_probability, + } + for data_in in self._forecast + ] - fcdata_out.append(data_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 + return fcdata_out diff --git a/requirements_all.txt b/requirements_all.txt index 60a34a3c22f..f0e1848c76e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1297,7 +1297,7 @@ pyicloud==0.9.1 pyintesishome==1.6 # homeassistant.components.ipma -pyipma==1.2.1 +pyipma==2.0.2 # homeassistant.components.iqvia pyiqvia==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bfabcf0d7a1..594e8622e70 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -451,7 +451,7 @@ pyhomematic==0.1.63 pyicloud==0.9.1 # homeassistant.components.ipma -pyipma==1.2.1 +pyipma==2.0.2 # homeassistant.components.iqvia pyiqvia==0.2.1 diff --git a/tests/components/ipma/test_weather.py b/tests/components/ipma/test_weather.py index de13d3c94b2..ead4654cba2 100644 --- a/tests/components/ipma/test_weather.py +++ b/tests/components/ipma/test_weather.py @@ -18,73 +18,99 @@ from tests.common import MockConfigEntry, mock_coro TEST_CONFIG = {"name": "HomeTown", "latitude": "40.00", "longitude": "-8.00"} -class MockStation: - """Mock Station from pyipma.""" +class MockLocation: + """Mock Location from pyipma.""" - async def observation(self): + async def observation(self, api): """Mock Observation.""" Observation = namedtuple( "Observation", [ - "temperature", + "accumulated_precipitation", "humidity", - "windspeed", - "winddirection", - "precipitation", "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.""" Forecast = namedtuple( "Forecast", [ - "precipitaProb", - "tMin", - "tMax", - "predWindDir", - "idWeatherType", - "classWindSpeed", - "longitude", - "forecastDate", - "classPrecInt", - "latitude", - "description", + "feels_like_temperature", + "forecast_date", + "forecasted_hours", + "humidity", + "max_temperature", + "min_temperature", + "precipitation_probability", + "temperature", + "update_date", + "weather_type", + "wind_direction", + "wind_strength", ], ) return [ Forecast( - 73.0, - 13.7, - 18.7, - "NW", - 6, - 2, - -8.64, - "2018-05-31", - 2, - 40.61, - "Aguaceiros, com vento Moderado de Noroeste", - ) + None, + "2020-01-15T00:00:00", + 24, + None, + 16.2, + 10.6, + "100.0", + 13.4, + "2020-01-15T07:51:00", + 9, + "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 - def local(self): + def name(self): """Mock location.""" return "HomeTown" @property - def latitude(self): + def station_latitude(self): """Mock latitude.""" return 0 @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.""" return 0 @@ -92,8 +118,8 @@ class MockStation: async def test_setup_configuration(hass): """Test for successfully setting up the IPMA platform.""" with patch( - "homeassistant.components.ipma.weather.async_get_station", - return_value=mock_coro(MockStation()), + "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"}} @@ -115,8 +141,8 @@ async def test_setup_configuration(hass): async def test_setup_config_flow(hass): """Test for successfully setting up the IPMA platform.""" with patch( - "homeassistant.components.ipma.weather.async_get_station", - return_value=mock_coro(MockStation()), + "homeassistant.components.ipma.weather.async_get_location", + return_value=mock_coro(MockLocation()), ): entry = MockConfigEntry(domain="ipma", data=TEST_CONFIG) await hass.config_entries.async_forward_entry_setup(entry, WEATHER_DOMAIN)