"""Support for IPMA weather service.""" from __future__ import annotations from datetime import timedelta import logging import async_timeout from pyipma.api import IPMA_API from pyipma.location import Location import voluptuous as vol from homeassistant.components.weather import ( ATTR_CONDITION_CLOUDY, ATTR_CONDITION_EXCEPTIONAL, ATTR_CONDITION_FOG, ATTR_CONDITION_HAIL, ATTR_CONDITION_LIGHTNING, ATTR_CONDITION_LIGHTNING_RAINY, ATTR_CONDITION_PARTLYCLOUDY, ATTR_CONDITION_POURING, ATTR_CONDITION_RAINY, ATTR_CONDITION_SNOWY, ATTR_CONDITION_SNOWY_RAINY, ATTR_CONDITION_SUNNY, ATTR_CONDITION_WINDY, ATTR_CONDITION_WINDY_VARIANT, ATTR_FORECAST_CONDITION, ATTR_FORECAST_PRECIPITATION_PROBABILITY, ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, ATTR_FORECAST_WIND_SPEED, PLATFORM_SCHEMA, WeatherEntity, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE, CONF_NAME, TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv, entity_registry from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import Throttle from homeassistant.util.dt import now, parse_datetime _LOGGER = logging.getLogger(__name__) ATTRIBUTION = "Instituto Português do Mar e Atmosfera" MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=30) CONDITION_CLASSES = { ATTR_CONDITION_CLOUDY: [4, 5, 24, 25, 27], ATTR_CONDITION_FOG: [16, 17, 26], ATTR_CONDITION_HAIL: [21, 22], ATTR_CONDITION_LIGHTNING: [19], ATTR_CONDITION_LIGHTNING_RAINY: [20, 23], ATTR_CONDITION_PARTLYCLOUDY: [2, 3], ATTR_CONDITION_POURING: [8, 11], ATTR_CONDITION_RAINY: [6, 7, 9, 10, 12, 13, 14, 15], ATTR_CONDITION_SNOWY: [18], ATTR_CONDITION_SNOWY_RAINY: [], ATTR_CONDITION_SUNNY: [1], ATTR_CONDITION_WINDY: [], ATTR_CONDITION_WINDY_VARIANT: [], ATTR_CONDITION_EXCEPTIONAL: [], } FORECAST_MODE = ["hourly", "daily"] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_LATITUDE): cv.latitude, vol.Optional(CONF_LONGITUDE): cv.longitude, vol.Optional(CONF_MODE, default="daily"): vol.In(FORECAST_MODE), } ) async def async_setup_platform( hass: HomeAssistant, config: ConfigType, async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the ipma platform. Deprecated. """ _LOGGER.warning("Loading IPMA via platform config is deprecated") latitude = config.get(CONF_LATITUDE, hass.config.latitude) longitude = config.get(CONF_LONGITUDE, hass.config.longitude) if None in (latitude, longitude): _LOGGER.error("Latitude or longitude not set in Home Assistant config") return api = await async_get_api(hass) location = await async_get_location(hass, api, latitude, longitude) async_add_entities([IPMAWeather(location, api, config)], True) async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Add a weather entity from a config_entry.""" latitude = config_entry.data[CONF_LATITUDE] longitude = config_entry.data[CONF_LONGITUDE] mode = config_entry.data[CONF_MODE] api = await async_get_api(hass) location = await async_get_location(hass, api, latitude, longitude) # Migrate old unique_id @callback def _async_migrator(entity_entry: entity_registry.RegistryEntry): # Reject if new unique_id if entity_entry.unique_id.count(",") == 2: return None new_unique_id = ( f"{location.station_latitude}, {location.station_longitude}, {mode}" ) _LOGGER.info( "Migrating unique_id from [%s] to [%s]", entity_entry.unique_id, new_unique_id, ) return {"new_unique_id": new_unique_id} await entity_registry.async_migrate_entries( hass, config_entry.entry_id, _async_migrator ) async_add_entities([IPMAWeather(location, api, config_entry.data)], True) 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.""" async with async_timeout.timeout(30): location = await Location.get(api, float(latitude), float(longitude)) _LOGGER.debug( "Initializing for coordinates %s, %s -> station %s (%d, %d)", latitude, longitude, location.station, location.id_station, location.global_id_local, ) return location class IPMAWeather(WeatherEntity): """Representation of a weather condition.""" def __init__(self, location: Location, api: IPMA_API, config): """Initialise the platform with a data instance and station name.""" self._api = api self._location_name = config.get(CONF_NAME, location.name) self._mode = config.get(CONF_MODE) self._location = location self._observation = None self._forecast = None @Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_update(self): """Update Condition and Forecast.""" async with async_timeout.timeout(10): 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 = new_forecast else: _LOGGER.warning("Could not update weather forecast") _LOGGER.debug( "Updated location %s, observation %s", self._location.name, self._observation, ) @property def unique_id(self) -> str: """Return a unique id.""" return f"{self._location.station_latitude}, {self._location.station_longitude}, {self._mode}" @property def attribution(self): """Return the attribution.""" return ATTRIBUTION @property def name(self): """Return the name of the station.""" return self._location_name @property def condition(self): """Return the current condition.""" if not self._forecast: return return next( ( k for k, v in CONDITION_CLASSES.items() if self._forecast[0].weather_type in v ), None, ) @property def temperature(self): """Return the current temperature.""" if not self._observation: return None return self._observation.temperature @property def pressure(self): """Return the current pressure.""" if not self._observation: return None return self._observation.pressure @property def humidity(self): """Return the name of the sensor.""" if not self._observation: return None return self._observation.humidity @property def wind_speed(self): """Return the current windspeed.""" if not self._observation: return None return self._observation.wind_intensity_km @property def wind_bearing(self): """Return the current wind bearing (degrees).""" if not self._observation: return None return self._observation.wind_direction @property def temperature_unit(self): """Return the unit of measurement.""" return TEMP_CELSIUS @property def forecast(self): """Return the forecast array.""" if not self._forecast: 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_PROBABILITY: ( int(float(data_in.precipitation_probability)) if int(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 = [ { 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_LOW: data_in.min_temperature, ATTR_FORECAST_TEMP: data_in.max_temperature, ATTR_FORECAST_PRECIPITATION_PROBABILITY: data_in.precipitation_probability, ATTR_FORECAST_WIND_SPEED: data_in.wind_strength, ATTR_FORECAST_WIND_BEARING: data_in.wind_direction, } for data_in in forecast_filtered ] return fcdata_out