344 lines
10 KiB
Python
344 lines
10 KiB
Python
"""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
|