mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 01:08:12 +00:00
Add AEMET OpenData integration (#45074)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
2f40f44670
commit
eecf07d7df
@ -23,6 +23,8 @@ omit =
|
||||
homeassistant/components/adguard/sensor.py
|
||||
homeassistant/components/adguard/switch.py
|
||||
homeassistant/components/ads/*
|
||||
homeassistant/components/aemet/abstract_aemet_sensor.py
|
||||
homeassistant/components/aemet/weather_update_coordinator.py
|
||||
homeassistant/components/aftership/sensor.py
|
||||
homeassistant/components/agent_dvr/__init__.py
|
||||
homeassistant/components/agent_dvr/alarm_control_panel.py
|
||||
|
@ -24,6 +24,7 @@ homeassistant/components/accuweather/* @bieniu
|
||||
homeassistant/components/acmeda/* @atmurray
|
||||
homeassistant/components/adguard/* @frenck
|
||||
homeassistant/components/advantage_air/* @Bre77
|
||||
homeassistant/components/aemet/* @noltari
|
||||
homeassistant/components/agent_dvr/* @ispysoftware
|
||||
homeassistant/components/airly/* @bieniu
|
||||
homeassistant/components/airnow/* @asymworks
|
||||
|
61
homeassistant/components/aemet/__init__.py
Normal file
61
homeassistant/components/aemet/__init__.py
Normal file
@ -0,0 +1,61 @@
|
||||
"""The AEMET OpenData component."""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from aemet_opendata.interface import AEMET
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import COMPONENTS, DOMAIN, ENTRY_NAME, ENTRY_WEATHER_COORDINATOR
|
||||
from .weather_update_coordinator import WeatherUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: dict) -> bool:
|
||||
"""Set up the AEMET OpenData component."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry):
|
||||
"""Set up AEMET OpenData as config entry."""
|
||||
name = config_entry.data[CONF_NAME]
|
||||
api_key = config_entry.data[CONF_API_KEY]
|
||||
latitude = config_entry.data[CONF_LATITUDE]
|
||||
longitude = config_entry.data[CONF_LONGITUDE]
|
||||
|
||||
aemet = AEMET(api_key)
|
||||
weather_coordinator = WeatherUpdateCoordinator(hass, aemet, latitude, longitude)
|
||||
|
||||
await weather_coordinator.async_refresh()
|
||||
|
||||
hass.data[DOMAIN][config_entry.entry_id] = {
|
||||
ENTRY_NAME: name,
|
||||
ENTRY_WEATHER_COORDINATOR: weather_coordinator,
|
||||
}
|
||||
|
||||
for component in COMPONENTS:
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_forward_entry_setup(config_entry, component)
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry):
|
||||
"""Unload a config entry."""
|
||||
unload_ok = all(
|
||||
await asyncio.gather(
|
||||
*[
|
||||
hass.config_entries.async_forward_entry_unload(config_entry, component)
|
||||
for component in COMPONENTS
|
||||
]
|
||||
)
|
||||
)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(config_entry.entry_id)
|
||||
|
||||
return unload_ok
|
57
homeassistant/components/aemet/abstract_aemet_sensor.py
Normal file
57
homeassistant/components/aemet/abstract_aemet_sensor.py
Normal file
@ -0,0 +1,57 @@
|
||||
"""Abstraction form AEMET OpenData sensors."""
|
||||
from homeassistant.const import ATTR_ATTRIBUTION
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import ATTRIBUTION, SENSOR_DEVICE_CLASS, SENSOR_NAME, SENSOR_UNIT
|
||||
from .weather_update_coordinator import WeatherUpdateCoordinator
|
||||
|
||||
|
||||
class AbstractAemetSensor(CoordinatorEntity):
|
||||
"""Abstract class for an AEMET OpenData sensor."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
unique_id,
|
||||
sensor_type,
|
||||
sensor_configuration,
|
||||
coordinator: WeatherUpdateCoordinator,
|
||||
):
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator)
|
||||
self._name = name
|
||||
self._unique_id = unique_id
|
||||
self._sensor_type = sensor_type
|
||||
self._sensor_name = sensor_configuration[SENSOR_NAME]
|
||||
self._unit_of_measurement = sensor_configuration.get(SENSOR_UNIT)
|
||||
self._device_class = sensor_configuration.get(SENSOR_DEVICE_CLASS)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return f"{self._name} {self._sensor_name}"
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique_id for this entity."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def attribution(self):
|
||||
"""Return the attribution."""
|
||||
return ATTRIBUTION
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the device_class."""
|
||||
return self._device_class
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement of this entity, if any."""
|
||||
return self._unit_of_measurement
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {ATTR_ATTRIBUTION: ATTRIBUTION}
|
58
homeassistant/components/aemet/config_flow.py
Normal file
58
homeassistant/components/aemet/config_flow.py
Normal file
@ -0,0 +1,58 @@
|
||||
"""Config flow for AEMET OpenData."""
|
||||
from aemet_opendata import AEMET
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from .const import DEFAULT_NAME
|
||||
from .const import DOMAIN # pylint:disable=unused-import
|
||||
|
||||
|
||||
class AemetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Config flow for AEMET OpenData."""
|
||||
|
||||
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle a flow initialized by the user."""
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
latitude = user_input[CONF_LATITUDE]
|
||||
longitude = user_input[CONF_LONGITUDE]
|
||||
|
||||
await self.async_set_unique_id(f"{latitude}-{longitude}")
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
api_online = await _is_aemet_api_online(self.hass, user_input[CONF_API_KEY])
|
||||
if not api_online:
|
||||
errors["base"] = "invalid_api_key"
|
||||
|
||||
if not errors:
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_NAME], data=user_input
|
||||
)
|
||||
|
||||
schema = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_API_KEY): str,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): str,
|
||||
vol.Optional(
|
||||
CONF_LATITUDE, default=self.hass.config.latitude
|
||||
): cv.latitude,
|
||||
vol.Optional(
|
||||
CONF_LONGITUDE, default=self.hass.config.longitude
|
||||
): cv.longitude,
|
||||
}
|
||||
)
|
||||
|
||||
return self.async_show_form(step_id="user", data_schema=schema, errors=errors)
|
||||
|
||||
|
||||
async def _is_aemet_api_online(hass, api_key):
|
||||
aemet = AEMET(api_key)
|
||||
return await hass.async_add_executor_job(
|
||||
aemet.get_conventional_observation_stations, False
|
||||
)
|
326
homeassistant/components/aemet/const.py
Normal file
326
homeassistant/components/aemet/const.py
Normal file
@ -0,0 +1,326 @@
|
||||
"""Constant values for the AEMET OpenData component."""
|
||||
|
||||
from homeassistant.components.weather import (
|
||||
ATTR_CONDITION_CLEAR_NIGHT,
|
||||
ATTR_CONDITION_CLOUDY,
|
||||
ATTR_CONDITION_FOG,
|
||||
ATTR_CONDITION_LIGHTNING,
|
||||
ATTR_CONDITION_LIGHTNING_RAINY,
|
||||
ATTR_CONDITION_PARTLYCLOUDY,
|
||||
ATTR_CONDITION_POURING,
|
||||
ATTR_CONDITION_RAINY,
|
||||
ATTR_CONDITION_SNOWY,
|
||||
ATTR_CONDITION_SUNNY,
|
||||
ATTR_FORECAST_CONDITION,
|
||||
ATTR_FORECAST_PRECIPITATION,
|
||||
ATTR_FORECAST_PRECIPITATION_PROBABILITY,
|
||||
ATTR_FORECAST_TEMP,
|
||||
ATTR_FORECAST_TEMP_LOW,
|
||||
ATTR_FORECAST_TIME,
|
||||
ATTR_FORECAST_WIND_BEARING,
|
||||
ATTR_FORECAST_WIND_SPEED,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
DEGREE,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_PRESSURE,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
DEVICE_CLASS_TIMESTAMP,
|
||||
PERCENTAGE,
|
||||
PRECIPITATION_MILLIMETERS_PER_HOUR,
|
||||
PRESSURE_HPA,
|
||||
SPEED_KILOMETERS_PER_HOUR,
|
||||
TEMP_CELSIUS,
|
||||
)
|
||||
|
||||
ATTRIBUTION = "Powered by AEMET OpenData"
|
||||
COMPONENTS = ["sensor", "weather"]
|
||||
DEFAULT_NAME = "AEMET"
|
||||
DOMAIN = "aemet"
|
||||
ENTRY_NAME = "name"
|
||||
ENTRY_WEATHER_COORDINATOR = "weather_coordinator"
|
||||
UPDATE_LISTENER = "update_listener"
|
||||
SENSOR_NAME = "sensor_name"
|
||||
SENSOR_UNIT = "sensor_unit"
|
||||
SENSOR_DEVICE_CLASS = "sensor_device_class"
|
||||
|
||||
ATTR_API_CONDITION = "condition"
|
||||
ATTR_API_FORECAST_DAILY = "forecast-daily"
|
||||
ATTR_API_FORECAST_HOURLY = "forecast-hourly"
|
||||
ATTR_API_HUMIDITY = "humidity"
|
||||
ATTR_API_PRESSURE = "pressure"
|
||||
ATTR_API_RAIN = "rain"
|
||||
ATTR_API_RAIN_PROB = "rain-probability"
|
||||
ATTR_API_SNOW = "snow"
|
||||
ATTR_API_SNOW_PROB = "snow-probability"
|
||||
ATTR_API_STATION_ID = "station-id"
|
||||
ATTR_API_STATION_NAME = "station-name"
|
||||
ATTR_API_STATION_TIMESTAMP = "station-timestamp"
|
||||
ATTR_API_STORM_PROB = "storm-probability"
|
||||
ATTR_API_TEMPERATURE = "temperature"
|
||||
ATTR_API_TEMPERATURE_FEELING = "temperature-feeling"
|
||||
ATTR_API_TOWN_ID = "town-id"
|
||||
ATTR_API_TOWN_NAME = "town-name"
|
||||
ATTR_API_TOWN_TIMESTAMP = "town-timestamp"
|
||||
ATTR_API_WIND_BEARING = "wind-bearing"
|
||||
ATTR_API_WIND_MAX_SPEED = "wind-max-speed"
|
||||
ATTR_API_WIND_SPEED = "wind-speed"
|
||||
|
||||
CONDITIONS_MAP = {
|
||||
ATTR_CONDITION_CLEAR_NIGHT: {
|
||||
"11n", # Despejado (de noche)
|
||||
},
|
||||
ATTR_CONDITION_CLOUDY: {
|
||||
"14", # Nuboso
|
||||
"14n", # Nuboso (de noche)
|
||||
"15", # Muy nuboso
|
||||
"15n", # Muy nuboso (de noche)
|
||||
"16", # Cubierto
|
||||
"16n", # Cubierto (de noche)
|
||||
"17", # Nubes altas
|
||||
"17n", # Nubes altas (de noche)
|
||||
},
|
||||
ATTR_CONDITION_FOG: {
|
||||
"81", # Niebla
|
||||
"81n", # Niebla (de noche)
|
||||
"82", # Bruma - Neblina
|
||||
"82n", # Bruma - Neblina (de noche)
|
||||
},
|
||||
ATTR_CONDITION_LIGHTNING: {
|
||||
"51", # Intervalos nubosos con tormenta
|
||||
"51n", # Intervalos nubosos con tormenta (de noche)
|
||||
"52", # Nuboso con tormenta
|
||||
"52n", # Nuboso con tormenta (de noche)
|
||||
"53", # Muy nuboso con tormenta
|
||||
"53n", # Muy nuboso con tormenta (de noche)
|
||||
"54", # Cubierto con tormenta
|
||||
"54n", # Cubierto con tormenta (de noche)
|
||||
},
|
||||
ATTR_CONDITION_LIGHTNING_RAINY: {
|
||||
"61", # Intervalos nubosos con tormenta y lluvia escasa
|
||||
"61n", # Intervalos nubosos con tormenta y lluvia escasa (de noche)
|
||||
"62", # Nuboso con tormenta y lluvia escasa
|
||||
"62n", # Nuboso con tormenta y lluvia escasa (de noche)
|
||||
"63", # Muy nuboso con tormenta y lluvia escasa
|
||||
"63n", # Muy nuboso con tormenta y lluvia escasa (de noche)
|
||||
"64", # Cubierto con tormenta y lluvia escasa
|
||||
"64n", # Cubierto con tormenta y lluvia escasa (de noche)
|
||||
},
|
||||
ATTR_CONDITION_PARTLYCLOUDY: {
|
||||
"12", # Poco nuboso
|
||||
"12n", # Poco nuboso (de noche)
|
||||
"13", # Intervalos nubosos
|
||||
"13n", # Intervalos nubosos (de noche)
|
||||
},
|
||||
ATTR_CONDITION_POURING: {
|
||||
"27", # Chubascos
|
||||
"27n", # Chubascos (de noche)
|
||||
},
|
||||
ATTR_CONDITION_RAINY: {
|
||||
"23", # Intervalos nubosos con lluvia
|
||||
"23n", # Intervalos nubosos con lluvia (de noche)
|
||||
"24", # Nuboso con lluvia
|
||||
"24n", # Nuboso con lluvia (de noche)
|
||||
"25", # Muy nuboso con lluvia
|
||||
"25n", # Muy nuboso con lluvia (de noche)
|
||||
"26", # Cubierto con lluvia
|
||||
"26n", # Cubierto con lluvia (de noche)
|
||||
"43", # Intervalos nubosos con lluvia escasa
|
||||
"43n", # Intervalos nubosos con lluvia escasa (de noche)
|
||||
"44", # Nuboso con lluvia escasa
|
||||
"44n", # Nuboso con lluvia escasa (de noche)
|
||||
"45", # Muy nuboso con lluvia escasa
|
||||
"45n", # Muy nuboso con lluvia escasa (de noche)
|
||||
"46", # Cubierto con lluvia escasa
|
||||
"46n", # Cubierto con lluvia escasa (de noche)
|
||||
},
|
||||
ATTR_CONDITION_SNOWY: {
|
||||
"33", # Intervalos nubosos con nieve
|
||||
"33n", # Intervalos nubosos con nieve (de noche)
|
||||
"34", # Nuboso con nieve
|
||||
"34n", # Nuboso con nieve (de noche)
|
||||
"35", # Muy nuboso con nieve
|
||||
"35n", # Muy nuboso con nieve (de noche)
|
||||
"36", # Cubierto con nieve
|
||||
"36n", # Cubierto con nieve (de noche)
|
||||
"71", # Intervalos nubosos con nieve escasa
|
||||
"71n", # Intervalos nubosos con nieve escasa (de noche)
|
||||
"72", # Nuboso con nieve escasa
|
||||
"72n", # Nuboso con nieve escasa (de noche)
|
||||
"73", # Muy nuboso con nieve escasa
|
||||
"73n", # Muy nuboso con nieve escasa (de noche)
|
||||
"74", # Cubierto con nieve escasa
|
||||
"74n", # Cubierto con nieve escasa (de noche)
|
||||
},
|
||||
ATTR_CONDITION_SUNNY: {
|
||||
"11", # Despejado
|
||||
},
|
||||
}
|
||||
|
||||
FORECAST_MONITORED_CONDITIONS = [
|
||||
ATTR_FORECAST_CONDITION,
|
||||
ATTR_FORECAST_PRECIPITATION,
|
||||
ATTR_FORECAST_PRECIPITATION_PROBABILITY,
|
||||
ATTR_FORECAST_TEMP,
|
||||
ATTR_FORECAST_TEMP_LOW,
|
||||
ATTR_FORECAST_TIME,
|
||||
ATTR_FORECAST_WIND_BEARING,
|
||||
ATTR_FORECAST_WIND_SPEED,
|
||||
]
|
||||
MONITORED_CONDITIONS = [
|
||||
ATTR_API_CONDITION,
|
||||
ATTR_API_HUMIDITY,
|
||||
ATTR_API_PRESSURE,
|
||||
ATTR_API_RAIN,
|
||||
ATTR_API_RAIN_PROB,
|
||||
ATTR_API_SNOW,
|
||||
ATTR_API_SNOW_PROB,
|
||||
ATTR_API_STATION_ID,
|
||||
ATTR_API_STATION_NAME,
|
||||
ATTR_API_STATION_TIMESTAMP,
|
||||
ATTR_API_STORM_PROB,
|
||||
ATTR_API_TEMPERATURE,
|
||||
ATTR_API_TEMPERATURE_FEELING,
|
||||
ATTR_API_TOWN_ID,
|
||||
ATTR_API_TOWN_NAME,
|
||||
ATTR_API_TOWN_TIMESTAMP,
|
||||
ATTR_API_WIND_BEARING,
|
||||
ATTR_API_WIND_MAX_SPEED,
|
||||
ATTR_API_WIND_SPEED,
|
||||
]
|
||||
|
||||
FORECAST_MODE_DAILY = "daily"
|
||||
FORECAST_MODE_HOURLY = "hourly"
|
||||
FORECAST_MODES = [
|
||||
FORECAST_MODE_DAILY,
|
||||
FORECAST_MODE_HOURLY,
|
||||
]
|
||||
FORECAST_MODE_ATTR_API = {
|
||||
FORECAST_MODE_DAILY: ATTR_API_FORECAST_DAILY,
|
||||
FORECAST_MODE_HOURLY: ATTR_API_FORECAST_HOURLY,
|
||||
}
|
||||
|
||||
FORECAST_SENSOR_TYPES = {
|
||||
ATTR_FORECAST_CONDITION: {
|
||||
SENSOR_NAME: "Condition",
|
||||
},
|
||||
ATTR_FORECAST_PRECIPITATION: {
|
||||
SENSOR_NAME: "Precipitation",
|
||||
SENSOR_UNIT: PRECIPITATION_MILLIMETERS_PER_HOUR,
|
||||
},
|
||||
ATTR_FORECAST_PRECIPITATION_PROBABILITY: {
|
||||
SENSOR_NAME: "Precipitation probability",
|
||||
SENSOR_UNIT: PERCENTAGE,
|
||||
},
|
||||
ATTR_FORECAST_TEMP: {
|
||||
SENSOR_NAME: "Temperature",
|
||||
SENSOR_UNIT: TEMP_CELSIUS,
|
||||
SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
|
||||
},
|
||||
ATTR_FORECAST_TEMP_LOW: {
|
||||
SENSOR_NAME: "Temperature Low",
|
||||
SENSOR_UNIT: TEMP_CELSIUS,
|
||||
SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
|
||||
},
|
||||
ATTR_FORECAST_TIME: {
|
||||
SENSOR_NAME: "Time",
|
||||
SENSOR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP,
|
||||
},
|
||||
ATTR_FORECAST_WIND_BEARING: {
|
||||
SENSOR_NAME: "Wind bearing",
|
||||
SENSOR_UNIT: DEGREE,
|
||||
},
|
||||
ATTR_FORECAST_WIND_SPEED: {
|
||||
SENSOR_NAME: "Wind speed",
|
||||
SENSOR_UNIT: SPEED_KILOMETERS_PER_HOUR,
|
||||
},
|
||||
}
|
||||
WEATHER_SENSOR_TYPES = {
|
||||
ATTR_API_CONDITION: {
|
||||
SENSOR_NAME: "Condition",
|
||||
},
|
||||
ATTR_API_HUMIDITY: {
|
||||
SENSOR_NAME: "Humidity",
|
||||
SENSOR_UNIT: PERCENTAGE,
|
||||
SENSOR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY,
|
||||
},
|
||||
ATTR_API_PRESSURE: {
|
||||
SENSOR_NAME: "Pressure",
|
||||
SENSOR_UNIT: PRESSURE_HPA,
|
||||
SENSOR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE,
|
||||
},
|
||||
ATTR_API_RAIN: {
|
||||
SENSOR_NAME: "Rain",
|
||||
SENSOR_UNIT: PRECIPITATION_MILLIMETERS_PER_HOUR,
|
||||
},
|
||||
ATTR_API_RAIN_PROB: {
|
||||
SENSOR_NAME: "Rain probability",
|
||||
SENSOR_UNIT: PERCENTAGE,
|
||||
},
|
||||
ATTR_API_SNOW: {
|
||||
SENSOR_NAME: "Snow",
|
||||
SENSOR_UNIT: PRECIPITATION_MILLIMETERS_PER_HOUR,
|
||||
},
|
||||
ATTR_API_SNOW_PROB: {
|
||||
SENSOR_NAME: "Snow probability",
|
||||
SENSOR_UNIT: PERCENTAGE,
|
||||
},
|
||||
ATTR_API_STATION_ID: {
|
||||
SENSOR_NAME: "Station ID",
|
||||
},
|
||||
ATTR_API_STATION_NAME: {
|
||||
SENSOR_NAME: "Station name",
|
||||
},
|
||||
ATTR_API_STATION_TIMESTAMP: {
|
||||
SENSOR_NAME: "Station timestamp",
|
||||
SENSOR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP,
|
||||
},
|
||||
ATTR_API_STORM_PROB: {
|
||||
SENSOR_NAME: "Storm probability",
|
||||
SENSOR_UNIT: PERCENTAGE,
|
||||
},
|
||||
ATTR_API_TEMPERATURE: {
|
||||
SENSOR_NAME: "Temperature",
|
||||
SENSOR_UNIT: TEMP_CELSIUS,
|
||||
SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
|
||||
},
|
||||
ATTR_API_TEMPERATURE_FEELING: {
|
||||
SENSOR_NAME: "Temperature feeling",
|
||||
SENSOR_UNIT: TEMP_CELSIUS,
|
||||
SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
|
||||
},
|
||||
ATTR_API_TOWN_ID: {
|
||||
SENSOR_NAME: "Town ID",
|
||||
},
|
||||
ATTR_API_TOWN_NAME: {
|
||||
SENSOR_NAME: "Town name",
|
||||
},
|
||||
ATTR_API_TOWN_TIMESTAMP: {
|
||||
SENSOR_NAME: "Town timestamp",
|
||||
SENSOR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP,
|
||||
},
|
||||
ATTR_API_WIND_BEARING: {
|
||||
SENSOR_NAME: "Wind bearing",
|
||||
SENSOR_UNIT: DEGREE,
|
||||
},
|
||||
ATTR_API_WIND_MAX_SPEED: {
|
||||
SENSOR_NAME: "Wind max speed",
|
||||
SENSOR_UNIT: SPEED_KILOMETERS_PER_HOUR,
|
||||
},
|
||||
ATTR_API_WIND_SPEED: {
|
||||
SENSOR_NAME: "Wind speed",
|
||||
SENSOR_UNIT: SPEED_KILOMETERS_PER_HOUR,
|
||||
},
|
||||
}
|
||||
|
||||
WIND_BEARING_MAP = {
|
||||
"C": None,
|
||||
"N": 0.0,
|
||||
"NE": 45.0,
|
||||
"E": 90.0,
|
||||
"SE": 135.0,
|
||||
"S": 180.0,
|
||||
"SO": 225.0,
|
||||
"O": 270.0,
|
||||
"NO": 315.0,
|
||||
}
|
8
homeassistant/components/aemet/manifest.json
Normal file
8
homeassistant/components/aemet/manifest.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"domain": "aemet",
|
||||
"name": "AEMET OpenData",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/aemet",
|
||||
"requirements": ["AEMET-OpenData==0.1.8"],
|
||||
"codeowners": ["@noltari"]
|
||||
}
|
114
homeassistant/components/aemet/sensor.py
Normal file
114
homeassistant/components/aemet/sensor.py
Normal file
@ -0,0 +1,114 @@
|
||||
"""Support for the AEMET OpenData service."""
|
||||
from .abstract_aemet_sensor import AbstractAemetSensor
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
ENTRY_NAME,
|
||||
ENTRY_WEATHER_COORDINATOR,
|
||||
FORECAST_MODE_ATTR_API,
|
||||
FORECAST_MODE_DAILY,
|
||||
FORECAST_MODES,
|
||||
FORECAST_MONITORED_CONDITIONS,
|
||||
FORECAST_SENSOR_TYPES,
|
||||
MONITORED_CONDITIONS,
|
||||
WEATHER_SENSOR_TYPES,
|
||||
)
|
||||
from .weather_update_coordinator import WeatherUpdateCoordinator
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up AEMET OpenData sensor entities based on a config entry."""
|
||||
domain_data = hass.data[DOMAIN][config_entry.entry_id]
|
||||
name = domain_data[ENTRY_NAME]
|
||||
weather_coordinator = domain_data[ENTRY_WEATHER_COORDINATOR]
|
||||
|
||||
weather_sensor_types = WEATHER_SENSOR_TYPES
|
||||
forecast_sensor_types = FORECAST_SENSOR_TYPES
|
||||
|
||||
entities = []
|
||||
for sensor_type in MONITORED_CONDITIONS:
|
||||
unique_id = f"{config_entry.unique_id}-{sensor_type}"
|
||||
entities.append(
|
||||
AemetSensor(
|
||||
name,
|
||||
unique_id,
|
||||
sensor_type,
|
||||
weather_sensor_types[sensor_type],
|
||||
weather_coordinator,
|
||||
)
|
||||
)
|
||||
|
||||
for mode in FORECAST_MODES:
|
||||
name = f"{domain_data[ENTRY_NAME]} {mode}"
|
||||
|
||||
for sensor_type in FORECAST_MONITORED_CONDITIONS:
|
||||
unique_id = f"{config_entry.unique_id}-forecast-{mode}-{sensor_type}"
|
||||
entities.append(
|
||||
AemetForecastSensor(
|
||||
f"{name} Forecast",
|
||||
unique_id,
|
||||
sensor_type,
|
||||
forecast_sensor_types[sensor_type],
|
||||
weather_coordinator,
|
||||
mode,
|
||||
)
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class AemetSensor(AbstractAemetSensor):
|
||||
"""Implementation of an AEMET OpenData sensor."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
unique_id,
|
||||
sensor_type,
|
||||
sensor_configuration,
|
||||
weather_coordinator: WeatherUpdateCoordinator,
|
||||
):
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(
|
||||
name, unique_id, sensor_type, sensor_configuration, weather_coordinator
|
||||
)
|
||||
self._weather_coordinator = weather_coordinator
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
return self._weather_coordinator.data.get(self._sensor_type)
|
||||
|
||||
|
||||
class AemetForecastSensor(AbstractAemetSensor):
|
||||
"""Implementation of an AEMET OpenData forecast sensor."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
unique_id,
|
||||
sensor_type,
|
||||
sensor_configuration,
|
||||
weather_coordinator: WeatherUpdateCoordinator,
|
||||
forecast_mode,
|
||||
):
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(
|
||||
name, unique_id, sensor_type, sensor_configuration, weather_coordinator
|
||||
)
|
||||
self._weather_coordinator = weather_coordinator
|
||||
self._forecast_mode = forecast_mode
|
||||
|
||||
@property
|
||||
def entity_registry_enabled_default(self) -> bool:
|
||||
"""Return if the entity should be enabled when first added to the entity registry."""
|
||||
return self._forecast_mode == FORECAST_MODE_DAILY
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
forecasts = self._weather_coordinator.data.get(
|
||||
FORECAST_MODE_ATTR_API[self._forecast_mode]
|
||||
)
|
||||
if forecasts:
|
||||
return forecasts[0].get(self._sensor_type)
|
||||
return None
|
22
homeassistant/components/aemet/strings.json
Normal file
22
homeassistant/components/aemet/strings.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_location%]"
|
||||
},
|
||||
"error": {
|
||||
"invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]",
|
||||
"latitude": "[%key:common::config_flow::data::latitude%]",
|
||||
"longitude": "[%key:common::config_flow::data::longitude%]",
|
||||
"name": "Name of the integration"
|
||||
},
|
||||
"description": "Set up AEMET OpenData integration. To generate API key go to https://opendata.aemet.es/centrodedescargas/altaUsuario",
|
||||
"title": "AEMET OpenData"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
22
homeassistant/components/aemet/translations/en.json
Normal file
22
homeassistant/components/aemet/translations/en.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Location is already configured"
|
||||
},
|
||||
"error": {
|
||||
"invalid_api_key": "Invalid API key"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "API Key",
|
||||
"latitude": "Latitude",
|
||||
"longitude": "Longitude",
|
||||
"name": "Name of the integration"
|
||||
},
|
||||
"description": "Set up AEMET OpenData integration. To generate API key go to https://opendata.aemet.es/centrodedescargas/altaUsuario",
|
||||
"title": "AEMET OpenData"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
113
homeassistant/components/aemet/weather.py
Normal file
113
homeassistant/components/aemet/weather.py
Normal file
@ -0,0 +1,113 @@
|
||||
"""Support for the AEMET OpenData service."""
|
||||
from homeassistant.components.weather import WeatherEntity
|
||||
from homeassistant.const import TEMP_CELSIUS
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import (
|
||||
ATTR_API_CONDITION,
|
||||
ATTR_API_HUMIDITY,
|
||||
ATTR_API_PRESSURE,
|
||||
ATTR_API_TEMPERATURE,
|
||||
ATTR_API_WIND_BEARING,
|
||||
ATTR_API_WIND_SPEED,
|
||||
ATTRIBUTION,
|
||||
DOMAIN,
|
||||
ENTRY_NAME,
|
||||
ENTRY_WEATHER_COORDINATOR,
|
||||
FORECAST_MODE_ATTR_API,
|
||||
FORECAST_MODE_DAILY,
|
||||
FORECAST_MODES,
|
||||
)
|
||||
from .weather_update_coordinator import WeatherUpdateCoordinator
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up AEMET OpenData weather entity based on a config entry."""
|
||||
domain_data = hass.data[DOMAIN][config_entry.entry_id]
|
||||
weather_coordinator = domain_data[ENTRY_WEATHER_COORDINATOR]
|
||||
|
||||
entities = []
|
||||
for mode in FORECAST_MODES:
|
||||
name = f"{domain_data[ENTRY_NAME]} {mode}"
|
||||
unique_id = f"{config_entry.unique_id} {mode}"
|
||||
entities.append(AemetWeather(name, unique_id, weather_coordinator, mode))
|
||||
|
||||
if entities:
|
||||
async_add_entities(entities, False)
|
||||
|
||||
|
||||
class AemetWeather(CoordinatorEntity, WeatherEntity):
|
||||
"""Implementation of an AEMET OpenData sensor."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
unique_id,
|
||||
coordinator: WeatherUpdateCoordinator,
|
||||
forecast_mode,
|
||||
):
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator)
|
||||
self._name = name
|
||||
self._unique_id = unique_id
|
||||
self._forecast_mode = forecast_mode
|
||||
|
||||
@property
|
||||
def attribution(self):
|
||||
"""Return the attribution."""
|
||||
return ATTRIBUTION
|
||||
|
||||
@property
|
||||
def condition(self):
|
||||
"""Return the current condition."""
|
||||
return self.coordinator.data[ATTR_API_CONDITION]
|
||||
|
||||
@property
|
||||
def entity_registry_enabled_default(self) -> bool:
|
||||
"""Return if the entity should be enabled when first added to the entity registry."""
|
||||
return self._forecast_mode == FORECAST_MODE_DAILY
|
||||
|
||||
@property
|
||||
def forecast(self):
|
||||
"""Return the forecast array."""
|
||||
return self.coordinator.data[FORECAST_MODE_ATTR_API[self._forecast_mode]]
|
||||
|
||||
@property
|
||||
def humidity(self):
|
||||
"""Return the humidity."""
|
||||
return self.coordinator.data[ATTR_API_HUMIDITY]
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def pressure(self):
|
||||
"""Return the pressure."""
|
||||
return self.coordinator.data[ATTR_API_PRESSURE]
|
||||
|
||||
@property
|
||||
def temperature(self):
|
||||
"""Return the temperature."""
|
||||
return self.coordinator.data[ATTR_API_TEMPERATURE]
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
return TEMP_CELSIUS
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique_id for this entity."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def wind_bearing(self):
|
||||
"""Return the temperature."""
|
||||
return self.coordinator.data[ATTR_API_WIND_BEARING]
|
||||
|
||||
@property
|
||||
def wind_speed(self):
|
||||
"""Return the temperature."""
|
||||
return self.coordinator.data[ATTR_API_WIND_SPEED]
|
637
homeassistant/components/aemet/weather_update_coordinator.py
Normal file
637
homeassistant/components/aemet/weather_update_coordinator.py
Normal file
@ -0,0 +1,637 @@
|
||||
"""Weather data coordinator for the AEMET OpenData service."""
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from aemet_opendata.const import (
|
||||
AEMET_ATTR_DATE,
|
||||
AEMET_ATTR_DAY,
|
||||
AEMET_ATTR_DIRECTION,
|
||||
AEMET_ATTR_ELABORATED,
|
||||
AEMET_ATTR_FORECAST,
|
||||
AEMET_ATTR_HUMIDITY,
|
||||
AEMET_ATTR_ID,
|
||||
AEMET_ATTR_IDEMA,
|
||||
AEMET_ATTR_MAX,
|
||||
AEMET_ATTR_MIN,
|
||||
AEMET_ATTR_NAME,
|
||||
AEMET_ATTR_PRECIPITATION,
|
||||
AEMET_ATTR_PRECIPITATION_PROBABILITY,
|
||||
AEMET_ATTR_SKY_STATE,
|
||||
AEMET_ATTR_SNOW,
|
||||
AEMET_ATTR_SNOW_PROBABILITY,
|
||||
AEMET_ATTR_SPEED,
|
||||
AEMET_ATTR_STATION_DATE,
|
||||
AEMET_ATTR_STATION_HUMIDITY,
|
||||
AEMET_ATTR_STATION_LOCATION,
|
||||
AEMET_ATTR_STATION_PRESSURE_SEA,
|
||||
AEMET_ATTR_STATION_TEMPERATURE,
|
||||
AEMET_ATTR_STORM_PROBABILITY,
|
||||
AEMET_ATTR_TEMPERATURE,
|
||||
AEMET_ATTR_TEMPERATURE_FEELING,
|
||||
AEMET_ATTR_WIND,
|
||||
AEMET_ATTR_WIND_GUST,
|
||||
ATTR_DATA,
|
||||
)
|
||||
from aemet_opendata.helpers import (
|
||||
get_forecast_day_value,
|
||||
get_forecast_hour_value,
|
||||
get_forecast_interval_value,
|
||||
)
|
||||
import async_timeout
|
||||
|
||||
from homeassistant.components.weather import (
|
||||
ATTR_FORECAST_CONDITION,
|
||||
ATTR_FORECAST_PRECIPITATION,
|
||||
ATTR_FORECAST_PRECIPITATION_PROBABILITY,
|
||||
ATTR_FORECAST_TEMP,
|
||||
ATTR_FORECAST_TEMP_LOW,
|
||||
ATTR_FORECAST_TIME,
|
||||
ATTR_FORECAST_WIND_BEARING,
|
||||
ATTR_FORECAST_WIND_SPEED,
|
||||
)
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import (
|
||||
ATTR_API_CONDITION,
|
||||
ATTR_API_FORECAST_DAILY,
|
||||
ATTR_API_FORECAST_HOURLY,
|
||||
ATTR_API_HUMIDITY,
|
||||
ATTR_API_PRESSURE,
|
||||
ATTR_API_RAIN,
|
||||
ATTR_API_RAIN_PROB,
|
||||
ATTR_API_SNOW,
|
||||
ATTR_API_SNOW_PROB,
|
||||
ATTR_API_STATION_ID,
|
||||
ATTR_API_STATION_NAME,
|
||||
ATTR_API_STATION_TIMESTAMP,
|
||||
ATTR_API_STORM_PROB,
|
||||
ATTR_API_TEMPERATURE,
|
||||
ATTR_API_TEMPERATURE_FEELING,
|
||||
ATTR_API_TOWN_ID,
|
||||
ATTR_API_TOWN_NAME,
|
||||
ATTR_API_TOWN_TIMESTAMP,
|
||||
ATTR_API_WIND_BEARING,
|
||||
ATTR_API_WIND_MAX_SPEED,
|
||||
ATTR_API_WIND_SPEED,
|
||||
CONDITIONS_MAP,
|
||||
DOMAIN,
|
||||
WIND_BEARING_MAP,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
WEATHER_UPDATE_INTERVAL = timedelta(minutes=10)
|
||||
|
||||
|
||||
def format_condition(condition: str) -> str:
|
||||
"""Return condition from dict CONDITIONS_MAP."""
|
||||
for key, value in CONDITIONS_MAP.items():
|
||||
if condition in value:
|
||||
return key
|
||||
_LOGGER.error('condition "%s" not found in CONDITIONS_MAP', condition)
|
||||
return condition
|
||||
|
||||
|
||||
def format_float(value) -> float:
|
||||
"""Try converting string to float."""
|
||||
try:
|
||||
return float(value)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def format_int(value) -> int:
|
||||
"""Try converting string to int."""
|
||||
try:
|
||||
return int(value)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
class TownNotFound(UpdateFailed):
|
||||
"""Raised when town is not found."""
|
||||
|
||||
|
||||
class WeatherUpdateCoordinator(DataUpdateCoordinator):
|
||||
"""Weather data update coordinator."""
|
||||
|
||||
def __init__(self, hass, aemet, latitude, longitude):
|
||||
"""Initialize coordinator."""
|
||||
super().__init__(
|
||||
hass, _LOGGER, name=DOMAIN, update_interval=WEATHER_UPDATE_INTERVAL
|
||||
)
|
||||
|
||||
self._aemet = aemet
|
||||
self._station = None
|
||||
self._town = None
|
||||
self._latitude = latitude
|
||||
self._longitude = longitude
|
||||
self._data = {
|
||||
"daily": None,
|
||||
"hourly": None,
|
||||
"station": None,
|
||||
}
|
||||
|
||||
async def _async_update_data(self):
|
||||
data = {}
|
||||
with async_timeout.timeout(120):
|
||||
weather_response = await self._get_aemet_weather()
|
||||
data = self._convert_weather_response(weather_response)
|
||||
return data
|
||||
|
||||
async def _get_aemet_weather(self):
|
||||
"""Poll weather data from AEMET OpenData."""
|
||||
weather = await self.hass.async_add_executor_job(self._get_weather_and_forecast)
|
||||
return weather
|
||||
|
||||
def _get_weather_station(self):
|
||||
if not self._station:
|
||||
self._station = (
|
||||
self._aemet.get_conventional_observation_station_by_coordinates(
|
||||
self._latitude, self._longitude
|
||||
)
|
||||
)
|
||||
if self._station:
|
||||
_LOGGER.debug(
|
||||
"station found for coordinates [%s, %s]: %s",
|
||||
self._latitude,
|
||||
self._longitude,
|
||||
self._station,
|
||||
)
|
||||
if not self._station:
|
||||
_LOGGER.debug(
|
||||
"station not found for coordinates [%s, %s]",
|
||||
self._latitude,
|
||||
self._longitude,
|
||||
)
|
||||
return self._station
|
||||
|
||||
def _get_weather_town(self):
|
||||
if not self._town:
|
||||
self._town = self._aemet.get_town_by_coordinates(
|
||||
self._latitude, self._longitude
|
||||
)
|
||||
if self._town:
|
||||
_LOGGER.debug(
|
||||
"town found for coordinates [%s, %s]: %s",
|
||||
self._latitude,
|
||||
self._longitude,
|
||||
self._town,
|
||||
)
|
||||
if not self._town:
|
||||
_LOGGER.error(
|
||||
"town not found for coordinates [%s, %s]",
|
||||
self._latitude,
|
||||
self._longitude,
|
||||
)
|
||||
raise TownNotFound
|
||||
return self._town
|
||||
|
||||
def _get_weather_and_forecast(self):
|
||||
"""Get weather and forecast data from AEMET OpenData."""
|
||||
|
||||
self._get_weather_town()
|
||||
|
||||
daily = self._aemet.get_specific_forecast_town_daily(self._town[AEMET_ATTR_ID])
|
||||
if not daily:
|
||||
_LOGGER.error(
|
||||
'error fetching daily data for town "%s"', self._town[AEMET_ATTR_ID]
|
||||
)
|
||||
|
||||
hourly = self._aemet.get_specific_forecast_town_hourly(
|
||||
self._town[AEMET_ATTR_ID]
|
||||
)
|
||||
if not hourly:
|
||||
_LOGGER.error(
|
||||
'error fetching hourly data for town "%s"', self._town[AEMET_ATTR_ID]
|
||||
)
|
||||
|
||||
station = None
|
||||
if self._get_weather_station():
|
||||
station = self._aemet.get_conventional_observation_station_data(
|
||||
self._station[AEMET_ATTR_IDEMA]
|
||||
)
|
||||
if not station:
|
||||
_LOGGER.error(
|
||||
'error fetching data for station "%s"',
|
||||
self._station[AEMET_ATTR_IDEMA],
|
||||
)
|
||||
|
||||
if daily:
|
||||
self._data["daily"] = daily
|
||||
if hourly:
|
||||
self._data["hourly"] = hourly
|
||||
if station:
|
||||
self._data["station"] = station
|
||||
|
||||
return AemetWeather(
|
||||
self._data["daily"],
|
||||
self._data["hourly"],
|
||||
self._data["station"],
|
||||
)
|
||||
|
||||
def _convert_weather_response(self, weather_response):
|
||||
"""Format the weather response correctly."""
|
||||
if not weather_response or not weather_response.hourly:
|
||||
return None
|
||||
|
||||
elaborated = dt_util.parse_datetime(
|
||||
weather_response.hourly[ATTR_DATA][0][AEMET_ATTR_ELABORATED]
|
||||
)
|
||||
now = dt_util.now()
|
||||
hour = now.hour
|
||||
|
||||
# Get current day
|
||||
day = None
|
||||
for cur_day in weather_response.hourly[ATTR_DATA][0][AEMET_ATTR_FORECAST][
|
||||
AEMET_ATTR_DAY
|
||||
]:
|
||||
cur_day_date = dt_util.parse_datetime(cur_day[AEMET_ATTR_DATE])
|
||||
if now.date() == cur_day_date.date():
|
||||
day = cur_day
|
||||
break
|
||||
|
||||
# Get station data
|
||||
station_data = None
|
||||
if weather_response.station:
|
||||
station_data = weather_response.station[ATTR_DATA][-1]
|
||||
|
||||
condition = None
|
||||
humidity = None
|
||||
pressure = None
|
||||
rain = None
|
||||
rain_prob = None
|
||||
snow = None
|
||||
snow_prob = None
|
||||
station_id = None
|
||||
station_name = None
|
||||
station_timestamp = None
|
||||
storm_prob = None
|
||||
temperature = None
|
||||
temperature_feeling = None
|
||||
town_id = None
|
||||
town_name = None
|
||||
town_timestamp = dt_util.as_utc(elaborated)
|
||||
wind_bearing = None
|
||||
wind_max_speed = None
|
||||
wind_speed = None
|
||||
|
||||
# Get weather values
|
||||
if day:
|
||||
condition = self._get_condition(day, hour)
|
||||
humidity = self._get_humidity(day, hour)
|
||||
rain = self._get_rain(day, hour)
|
||||
rain_prob = self._get_rain_prob(day, hour)
|
||||
snow = self._get_snow(day, hour)
|
||||
snow_prob = self._get_snow_prob(day, hour)
|
||||
station_id = self._get_station_id()
|
||||
station_name = self._get_station_name()
|
||||
storm_prob = self._get_storm_prob(day, hour)
|
||||
temperature = self._get_temperature(day, hour)
|
||||
temperature_feeling = self._get_temperature_feeling(day, hour)
|
||||
town_id = self._get_town_id()
|
||||
town_name = self._get_town_name()
|
||||
wind_bearing = self._get_wind_bearing(day, hour)
|
||||
wind_max_speed = self._get_wind_max_speed(day, hour)
|
||||
wind_speed = self._get_wind_speed(day, hour)
|
||||
|
||||
# Overwrite weather values with closest station data (if present)
|
||||
if station_data:
|
||||
if AEMET_ATTR_STATION_DATE in station_data:
|
||||
station_dt = dt_util.parse_datetime(
|
||||
station_data[AEMET_ATTR_STATION_DATE] + "Z"
|
||||
)
|
||||
station_timestamp = dt_util.as_utc(station_dt).isoformat()
|
||||
if AEMET_ATTR_STATION_HUMIDITY in station_data:
|
||||
humidity = format_float(station_data[AEMET_ATTR_STATION_HUMIDITY])
|
||||
if AEMET_ATTR_STATION_PRESSURE_SEA in station_data:
|
||||
pressure = format_float(station_data[AEMET_ATTR_STATION_PRESSURE_SEA])
|
||||
if AEMET_ATTR_STATION_TEMPERATURE in station_data:
|
||||
temperature = format_float(station_data[AEMET_ATTR_STATION_TEMPERATURE])
|
||||
|
||||
# Get forecast from weather data
|
||||
forecast_daily = self._get_daily_forecast_from_weather_response(
|
||||
weather_response, now
|
||||
)
|
||||
forecast_hourly = self._get_hourly_forecast_from_weather_response(
|
||||
weather_response, now
|
||||
)
|
||||
|
||||
return {
|
||||
ATTR_API_CONDITION: condition,
|
||||
ATTR_API_FORECAST_DAILY: forecast_daily,
|
||||
ATTR_API_FORECAST_HOURLY: forecast_hourly,
|
||||
ATTR_API_HUMIDITY: humidity,
|
||||
ATTR_API_TEMPERATURE: temperature,
|
||||
ATTR_API_TEMPERATURE_FEELING: temperature_feeling,
|
||||
ATTR_API_PRESSURE: pressure,
|
||||
ATTR_API_RAIN: rain,
|
||||
ATTR_API_RAIN_PROB: rain_prob,
|
||||
ATTR_API_SNOW: snow,
|
||||
ATTR_API_SNOW_PROB: snow_prob,
|
||||
ATTR_API_STATION_ID: station_id,
|
||||
ATTR_API_STATION_NAME: station_name,
|
||||
ATTR_API_STATION_TIMESTAMP: station_timestamp,
|
||||
ATTR_API_STORM_PROB: storm_prob,
|
||||
ATTR_API_TOWN_ID: town_id,
|
||||
ATTR_API_TOWN_NAME: town_name,
|
||||
ATTR_API_TOWN_TIMESTAMP: town_timestamp,
|
||||
ATTR_API_WIND_BEARING: wind_bearing,
|
||||
ATTR_API_WIND_MAX_SPEED: wind_max_speed,
|
||||
ATTR_API_WIND_SPEED: wind_speed,
|
||||
}
|
||||
|
||||
def _get_daily_forecast_from_weather_response(self, weather_response, now):
|
||||
if weather_response.daily:
|
||||
parse = False
|
||||
forecast = []
|
||||
for day in weather_response.daily[ATTR_DATA][0][AEMET_ATTR_FORECAST][
|
||||
AEMET_ATTR_DAY
|
||||
]:
|
||||
day_date = dt_util.parse_datetime(day[AEMET_ATTR_DATE])
|
||||
if now.date() == day_date.date():
|
||||
parse = True
|
||||
if parse:
|
||||
cur_forecast = self._convert_forecast_day(day_date, day)
|
||||
if cur_forecast:
|
||||
forecast.append(cur_forecast)
|
||||
return forecast
|
||||
return None
|
||||
|
||||
def _get_hourly_forecast_from_weather_response(self, weather_response, now):
|
||||
if weather_response.hourly:
|
||||
parse = False
|
||||
hour = now.hour
|
||||
forecast = []
|
||||
for day in weather_response.hourly[ATTR_DATA][0][AEMET_ATTR_FORECAST][
|
||||
AEMET_ATTR_DAY
|
||||
]:
|
||||
day_date = dt_util.parse_datetime(day[AEMET_ATTR_DATE])
|
||||
hour_start = 0
|
||||
if now.date() == day_date.date():
|
||||
parse = True
|
||||
hour_start = now.hour
|
||||
if parse:
|
||||
for hour in range(hour_start, 24):
|
||||
cur_forecast = self._convert_forecast_hour(day_date, day, hour)
|
||||
if cur_forecast:
|
||||
forecast.append(cur_forecast)
|
||||
return forecast
|
||||
return None
|
||||
|
||||
def _convert_forecast_day(self, date, day):
|
||||
condition = self._get_condition_day(day)
|
||||
if not condition:
|
||||
return None
|
||||
|
||||
return {
|
||||
ATTR_FORECAST_CONDITION: condition,
|
||||
ATTR_FORECAST_PRECIPITATION_PROBABILITY: self._get_precipitation_prob_day(
|
||||
day
|
||||
),
|
||||
ATTR_FORECAST_TEMP: self._get_temperature_day(day),
|
||||
ATTR_FORECAST_TEMP_LOW: self._get_temperature_low_day(day),
|
||||
ATTR_FORECAST_TIME: dt_util.as_utc(date),
|
||||
ATTR_FORECAST_WIND_SPEED: self._get_wind_speed_day(day),
|
||||
ATTR_FORECAST_WIND_BEARING: self._get_wind_bearing_day(day),
|
||||
}
|
||||
|
||||
def _convert_forecast_hour(self, date, day, hour):
|
||||
condition = self._get_condition(day, hour)
|
||||
if not condition:
|
||||
return None
|
||||
|
||||
forecast_dt = date.replace(hour=hour, minute=0, second=0)
|
||||
|
||||
return {
|
||||
ATTR_FORECAST_CONDITION: condition,
|
||||
ATTR_FORECAST_PRECIPITATION: self._calc_precipitation(day, hour),
|
||||
ATTR_FORECAST_PRECIPITATION_PROBABILITY: self._calc_precipitation_prob(
|
||||
day, hour
|
||||
),
|
||||
ATTR_FORECAST_TEMP: self._get_temperature(day, hour),
|
||||
ATTR_FORECAST_TIME: dt_util.as_utc(forecast_dt),
|
||||
ATTR_FORECAST_WIND_SPEED: self._get_wind_speed(day, hour),
|
||||
ATTR_FORECAST_WIND_BEARING: self._get_wind_bearing(day, hour),
|
||||
}
|
||||
|
||||
def _calc_precipitation(self, day, hour):
|
||||
"""Calculate the precipitation."""
|
||||
rain_value = self._get_rain(day, hour)
|
||||
if not rain_value:
|
||||
rain_value = 0
|
||||
|
||||
snow_value = self._get_snow(day, hour)
|
||||
if not snow_value:
|
||||
snow_value = 0
|
||||
|
||||
if round(rain_value + snow_value, 1) == 0:
|
||||
return None
|
||||
return round(rain_value + snow_value, 1)
|
||||
|
||||
def _calc_precipitation_prob(self, day, hour):
|
||||
"""Calculate the precipitation probability (hour)."""
|
||||
rain_value = self._get_rain_prob(day, hour)
|
||||
if not rain_value:
|
||||
rain_value = 0
|
||||
|
||||
snow_value = self._get_snow_prob(day, hour)
|
||||
if not snow_value:
|
||||
snow_value = 0
|
||||
|
||||
if rain_value == 0 and snow_value == 0:
|
||||
return None
|
||||
return max(rain_value, snow_value)
|
||||
|
||||
@staticmethod
|
||||
def _get_condition(day_data, hour):
|
||||
"""Get weather condition (hour) from weather data."""
|
||||
val = get_forecast_hour_value(day_data[AEMET_ATTR_SKY_STATE], hour)
|
||||
if val:
|
||||
return format_condition(val)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_condition_day(day_data):
|
||||
"""Get weather condition (day) from weather data."""
|
||||
val = get_forecast_day_value(day_data[AEMET_ATTR_SKY_STATE])
|
||||
if val:
|
||||
return format_condition(val)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_humidity(day_data, hour):
|
||||
"""Get humidity from weather data."""
|
||||
val = get_forecast_hour_value(day_data[AEMET_ATTR_HUMIDITY], hour)
|
||||
if val:
|
||||
return format_int(val)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_precipitation_prob_day(day_data):
|
||||
"""Get humidity from weather data."""
|
||||
val = get_forecast_day_value(day_data[AEMET_ATTR_PRECIPITATION_PROBABILITY])
|
||||
if val:
|
||||
return format_int(val)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_rain(day_data, hour):
|
||||
"""Get rain from weather data."""
|
||||
val = get_forecast_hour_value(day_data[AEMET_ATTR_PRECIPITATION], hour)
|
||||
if val:
|
||||
return format_float(val)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_rain_prob(day_data, hour):
|
||||
"""Get rain probability from weather data."""
|
||||
val = get_forecast_interval_value(
|
||||
day_data[AEMET_ATTR_PRECIPITATION_PROBABILITY], hour
|
||||
)
|
||||
if val:
|
||||
return format_int(val)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_snow(day_data, hour):
|
||||
"""Get snow from weather data."""
|
||||
val = get_forecast_hour_value(day_data[AEMET_ATTR_SNOW], hour)
|
||||
if val:
|
||||
return format_float(val)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_snow_prob(day_data, hour):
|
||||
"""Get snow probability from weather data."""
|
||||
val = get_forecast_interval_value(day_data[AEMET_ATTR_SNOW_PROBABILITY], hour)
|
||||
if val:
|
||||
return format_int(val)
|
||||
return None
|
||||
|
||||
def _get_station_id(self):
|
||||
"""Get station ID from weather data."""
|
||||
if self._station:
|
||||
return self._station[AEMET_ATTR_IDEMA]
|
||||
return None
|
||||
|
||||
def _get_station_name(self):
|
||||
"""Get station name from weather data."""
|
||||
if self._station:
|
||||
return self._station[AEMET_ATTR_STATION_LOCATION]
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_storm_prob(day_data, hour):
|
||||
"""Get storm probability from weather data."""
|
||||
val = get_forecast_interval_value(day_data[AEMET_ATTR_STORM_PROBABILITY], hour)
|
||||
if val:
|
||||
return format_int(val)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_temperature(day_data, hour):
|
||||
"""Get temperature (hour) from weather data."""
|
||||
val = get_forecast_hour_value(day_data[AEMET_ATTR_TEMPERATURE], hour)
|
||||
if val:
|
||||
return format_int(val)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_temperature_day(day_data):
|
||||
"""Get temperature (day) from weather data."""
|
||||
val = get_forecast_day_value(
|
||||
day_data[AEMET_ATTR_TEMPERATURE], key=AEMET_ATTR_MAX
|
||||
)
|
||||
if val:
|
||||
return format_int(val)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_temperature_low_day(day_data):
|
||||
"""Get temperature (day) from weather data."""
|
||||
val = get_forecast_day_value(
|
||||
day_data[AEMET_ATTR_TEMPERATURE], key=AEMET_ATTR_MIN
|
||||
)
|
||||
if val:
|
||||
return format_int(val)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_temperature_feeling(day_data, hour):
|
||||
"""Get temperature from weather data."""
|
||||
val = get_forecast_hour_value(day_data[AEMET_ATTR_TEMPERATURE_FEELING], hour)
|
||||
if val:
|
||||
return format_int(val)
|
||||
return None
|
||||
|
||||
def _get_town_id(self):
|
||||
"""Get town ID from weather data."""
|
||||
if self._town:
|
||||
return self._town[AEMET_ATTR_ID]
|
||||
return None
|
||||
|
||||
def _get_town_name(self):
|
||||
"""Get town name from weather data."""
|
||||
if self._town:
|
||||
return self._town[AEMET_ATTR_NAME]
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_wind_bearing(day_data, hour):
|
||||
"""Get wind bearing (hour) from weather data."""
|
||||
val = get_forecast_hour_value(
|
||||
day_data[AEMET_ATTR_WIND_GUST], hour, key=AEMET_ATTR_DIRECTION
|
||||
)[0]
|
||||
if val in WIND_BEARING_MAP:
|
||||
return WIND_BEARING_MAP[val]
|
||||
_LOGGER.error("%s not found in Wind Bearing map", val)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_wind_bearing_day(day_data):
|
||||
"""Get wind bearing (day) from weather data."""
|
||||
val = get_forecast_day_value(
|
||||
day_data[AEMET_ATTR_WIND], key=AEMET_ATTR_DIRECTION
|
||||
)
|
||||
if val in WIND_BEARING_MAP:
|
||||
return WIND_BEARING_MAP[val]
|
||||
_LOGGER.error("%s not found in Wind Bearing map", val)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_wind_max_speed(day_data, hour):
|
||||
"""Get wind max speed from weather data."""
|
||||
val = get_forecast_hour_value(day_data[AEMET_ATTR_WIND_GUST], hour)
|
||||
if val:
|
||||
return format_int(val)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_wind_speed(day_data, hour):
|
||||
"""Get wind speed (hour) from weather data."""
|
||||
val = get_forecast_hour_value(
|
||||
day_data[AEMET_ATTR_WIND_GUST], hour, key=AEMET_ATTR_SPEED
|
||||
)[0]
|
||||
if val:
|
||||
return format_int(val)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_wind_speed_day(day_data):
|
||||
"""Get wind speed (day) from weather data."""
|
||||
val = get_forecast_day_value(day_data[AEMET_ATTR_WIND], key=AEMET_ATTR_SPEED)
|
||||
if val:
|
||||
return format_int(val)
|
||||
return None
|
||||
|
||||
|
||||
@dataclass
|
||||
class AemetWeather:
|
||||
"""Class to harmonize weather data model."""
|
||||
|
||||
daily: dict = field(default_factory=dict)
|
||||
hourly: dict = field(default_factory=dict)
|
||||
station: dict = field(default_factory=dict)
|
@ -11,6 +11,7 @@ FLOWS = [
|
||||
"acmeda",
|
||||
"adguard",
|
||||
"advantage_air",
|
||||
"aemet",
|
||||
"agent_dvr",
|
||||
"airly",
|
||||
"airnow",
|
||||
|
@ -1,6 +1,9 @@
|
||||
# Home Assistant Core, full dependency set
|
||||
-r requirements.txt
|
||||
|
||||
# homeassistant.components.aemet
|
||||
AEMET-OpenData==0.1.8
|
||||
|
||||
# homeassistant.components.dht
|
||||
# Adafruit-DHT==1.4.0
|
||||
|
||||
|
@ -3,6 +3,9 @@
|
||||
|
||||
-r requirements_test.txt
|
||||
|
||||
# homeassistant.components.aemet
|
||||
AEMET-OpenData==0.1.8
|
||||
|
||||
# homeassistant.components.homekit
|
||||
HAP-python==3.3.0
|
||||
|
||||
|
1
tests/components/aemet/__init__.py
Normal file
1
tests/components/aemet/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Tests for the AEMET OpenData integration."""
|
100
tests/components/aemet/test_config_flow.py
Normal file
100
tests/components/aemet/test_config_flow.py
Normal file
@ -0,0 +1,100 @@
|
||||
"""Define tests for the AEMET OpenData config flow."""
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import requests_mock
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.components.aemet.const import DOMAIN
|
||||
from homeassistant.config_entries import ENTRY_STATE_LOADED, SOURCE_USER
|
||||
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from .util import aemet_requests_mock
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
CONFIG = {
|
||||
CONF_NAME: "aemet",
|
||||
CONF_API_KEY: "foo",
|
||||
CONF_LATITUDE: 40.30403754,
|
||||
CONF_LONGITUDE: -3.72935236,
|
||||
}
|
||||
|
||||
|
||||
async def test_form(hass):
|
||||
"""Test that the form is served with valid input."""
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.aemet.async_setup", return_value=True
|
||||
) as mock_setup, patch(
|
||||
"homeassistant.components.aemet.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry, requests_mock.mock() as _m:
|
||||
aemet_requests_mock(_m)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == SOURCE_USER
|
||||
assert result["errors"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], CONFIG
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
conf_entries = hass.config_entries.async_entries(DOMAIN)
|
||||
entry = conf_entries[0]
|
||||
assert entry.state == ENTRY_STATE_LOADED
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert result["title"] == CONFIG[CONF_NAME]
|
||||
assert result["data"][CONF_LATITUDE] == CONFIG[CONF_LATITUDE]
|
||||
assert result["data"][CONF_LONGITUDE] == CONFIG[CONF_LONGITUDE]
|
||||
assert result["data"][CONF_API_KEY] == CONFIG[CONF_API_KEY]
|
||||
|
||||
assert len(mock_setup.mock_calls) == 1
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_form_duplicated_id(hass):
|
||||
"""Test that the options form."""
|
||||
|
||||
now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00")
|
||||
with patch("homeassistant.util.dt.now", return_value=now), patch(
|
||||
"homeassistant.util.dt.utcnow", return_value=now
|
||||
), requests_mock.mock() as _m:
|
||||
aemet_requests_mock(_m)
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN, unique_id="40.30403754--3.72935236", data=CONFIG
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}, data=CONFIG
|
||||
)
|
||||
|
||||
assert result["type"] == "abort"
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_form_api_offline(hass):
|
||||
"""Test setting up with api call error."""
|
||||
mocked_aemet = MagicMock()
|
||||
|
||||
mocked_aemet.get_conventional_observation_stations.return_value = None
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.aemet.config_flow.AEMET",
|
||||
return_value=mocked_aemet,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}, data=CONFIG
|
||||
)
|
||||
|
||||
assert result["errors"] == {"base": "invalid_api_key"}
|
44
tests/components/aemet/test_init.py
Normal file
44
tests/components/aemet/test_init.py
Normal file
@ -0,0 +1,44 @@
|
||||
"""Define tests for the AEMET OpenData init."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import requests_mock
|
||||
|
||||
from homeassistant.components.aemet.const import DOMAIN
|
||||
from homeassistant.config_entries import ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED
|
||||
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from .util import aemet_requests_mock
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
CONFIG = {
|
||||
CONF_NAME: "aemet",
|
||||
CONF_API_KEY: "foo",
|
||||
CONF_LATITUDE: 40.30403754,
|
||||
CONF_LONGITUDE: -3.72935236,
|
||||
}
|
||||
|
||||
|
||||
async def test_unload_entry(hass):
|
||||
"""Test that the options form."""
|
||||
|
||||
now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00")
|
||||
with patch("homeassistant.util.dt.now", return_value=now), patch(
|
||||
"homeassistant.util.dt.utcnow", return_value=now
|
||||
), requests_mock.mock() as _m:
|
||||
aemet_requests_mock(_m)
|
||||
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN, unique_id="aemet_unique_id", data=CONFIG
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert config_entry.state == ENTRY_STATE_LOADED
|
||||
|
||||
await hass.config_entries.async_unload(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert config_entry.state == ENTRY_STATE_NOT_LOADED
|
137
tests/components/aemet/test_sensor.py
Normal file
137
tests/components/aemet/test_sensor.py
Normal file
@ -0,0 +1,137 @@
|
||||
"""The sensor tests for the AEMET OpenData platform."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant.components.weather import (
|
||||
ATTR_CONDITION_PARTLYCLOUDY,
|
||||
ATTR_CONDITION_SNOWY,
|
||||
)
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from .util import async_init_integration
|
||||
|
||||
|
||||
async def test_aemet_forecast_create_sensors(hass):
|
||||
"""Test creation of forecast sensors."""
|
||||
|
||||
now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00")
|
||||
with patch("homeassistant.util.dt.now", return_value=now), patch(
|
||||
"homeassistant.util.dt.utcnow", return_value=now
|
||||
):
|
||||
await async_init_integration(hass)
|
||||
|
||||
state = hass.states.get("sensor.aemet_daily_forecast_condition")
|
||||
assert state.state == ATTR_CONDITION_PARTLYCLOUDY
|
||||
|
||||
state = hass.states.get("sensor.aemet_daily_forecast_precipitation")
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
state = hass.states.get("sensor.aemet_daily_forecast_precipitation_probability")
|
||||
assert state.state == "30"
|
||||
|
||||
state = hass.states.get("sensor.aemet_daily_forecast_temperature")
|
||||
assert state.state == "4"
|
||||
|
||||
state = hass.states.get("sensor.aemet_daily_forecast_temperature_low")
|
||||
assert state.state == "-4"
|
||||
|
||||
state = hass.states.get("sensor.aemet_daily_forecast_time")
|
||||
assert state.state == "2021-01-10 00:00:00+00:00"
|
||||
|
||||
state = hass.states.get("sensor.aemet_daily_forecast_wind_bearing")
|
||||
assert state.state == "45.0"
|
||||
|
||||
state = hass.states.get("sensor.aemet_daily_forecast_wind_speed")
|
||||
assert state.state == "20"
|
||||
|
||||
state = hass.states.get("sensor.aemet_hourly_forecast_condition")
|
||||
assert state is None
|
||||
|
||||
state = hass.states.get("sensor.aemet_hourly_forecast_precipitation")
|
||||
assert state is None
|
||||
|
||||
state = hass.states.get("sensor.aemet_hourly_forecast_precipitation_probability")
|
||||
assert state is None
|
||||
|
||||
state = hass.states.get("sensor.aemet_hourly_forecast_temperature")
|
||||
assert state is None
|
||||
|
||||
state = hass.states.get("sensor.aemet_hourly_forecast_temperature_low")
|
||||
assert state is None
|
||||
|
||||
state = hass.states.get("sensor.aemet_hourly_forecast_time")
|
||||
assert state is None
|
||||
|
||||
state = hass.states.get("sensor.aemet_hourly_forecast_wind_bearing")
|
||||
assert state is None
|
||||
|
||||
state = hass.states.get("sensor.aemet_hourly_forecast_wind_speed")
|
||||
assert state is None
|
||||
|
||||
|
||||
async def test_aemet_weather_create_sensors(hass):
|
||||
"""Test creation of weather sensors."""
|
||||
|
||||
now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00")
|
||||
with patch("homeassistant.util.dt.now", return_value=now), patch(
|
||||
"homeassistant.util.dt.utcnow", return_value=now
|
||||
):
|
||||
await async_init_integration(hass)
|
||||
|
||||
state = hass.states.get("sensor.aemet_condition")
|
||||
assert state.state == ATTR_CONDITION_SNOWY
|
||||
|
||||
state = hass.states.get("sensor.aemet_humidity")
|
||||
assert state.state == "99.0"
|
||||
|
||||
state = hass.states.get("sensor.aemet_pressure")
|
||||
assert state.state == "1004.4"
|
||||
|
||||
state = hass.states.get("sensor.aemet_rain")
|
||||
assert state.state == "1.8"
|
||||
|
||||
state = hass.states.get("sensor.aemet_rain_probability")
|
||||
assert state.state == "100"
|
||||
|
||||
state = hass.states.get("sensor.aemet_snow")
|
||||
assert state.state == "1.8"
|
||||
|
||||
state = hass.states.get("sensor.aemet_snow_probability")
|
||||
assert state.state == "100"
|
||||
|
||||
state = hass.states.get("sensor.aemet_station_id")
|
||||
assert state.state == "3195"
|
||||
|
||||
state = hass.states.get("sensor.aemet_station_name")
|
||||
assert state.state == "MADRID RETIRO"
|
||||
|
||||
state = hass.states.get("sensor.aemet_station_timestamp")
|
||||
assert state.state == "2021-01-09T12:00:00+00:00"
|
||||
|
||||
state = hass.states.get("sensor.aemet_storm_probability")
|
||||
assert state.state == "0"
|
||||
|
||||
state = hass.states.get("sensor.aemet_temperature")
|
||||
assert state.state == "-0.7"
|
||||
|
||||
state = hass.states.get("sensor.aemet_temperature_feeling")
|
||||
assert state.state == "-4"
|
||||
|
||||
state = hass.states.get("sensor.aemet_town_id")
|
||||
assert state.state == "id28065"
|
||||
|
||||
state = hass.states.get("sensor.aemet_town_name")
|
||||
assert state.state == "Getafe"
|
||||
|
||||
state = hass.states.get("sensor.aemet_town_timestamp")
|
||||
assert state.state == "2021-01-09 11:47:45+00:00"
|
||||
|
||||
state = hass.states.get("sensor.aemet_wind_bearing")
|
||||
assert state.state == "90.0"
|
||||
|
||||
state = hass.states.get("sensor.aemet_wind_max_speed")
|
||||
assert state.state == "24"
|
||||
|
||||
state = hass.states.get("sensor.aemet_wind_speed")
|
||||
assert state.state == "15"
|
61
tests/components/aemet/test_weather.py
Normal file
61
tests/components/aemet/test_weather.py
Normal file
@ -0,0 +1,61 @@
|
||||
"""The sensor tests for the AEMET OpenData platform."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant.components.aemet.const import ATTRIBUTION
|
||||
from homeassistant.components.weather import (
|
||||
ATTR_CONDITION_PARTLYCLOUDY,
|
||||
ATTR_CONDITION_SNOWY,
|
||||
ATTR_FORECAST,
|
||||
ATTR_FORECAST_CONDITION,
|
||||
ATTR_FORECAST_PRECIPITATION,
|
||||
ATTR_FORECAST_PRECIPITATION_PROBABILITY,
|
||||
ATTR_FORECAST_TEMP,
|
||||
ATTR_FORECAST_TEMP_LOW,
|
||||
ATTR_FORECAST_TIME,
|
||||
ATTR_FORECAST_WIND_BEARING,
|
||||
ATTR_FORECAST_WIND_SPEED,
|
||||
ATTR_WEATHER_HUMIDITY,
|
||||
ATTR_WEATHER_PRESSURE,
|
||||
ATTR_WEATHER_TEMPERATURE,
|
||||
ATTR_WEATHER_WIND_BEARING,
|
||||
ATTR_WEATHER_WIND_SPEED,
|
||||
)
|
||||
from homeassistant.const import ATTR_ATTRIBUTION
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from .util import async_init_integration
|
||||
|
||||
|
||||
async def test_aemet_weather(hass):
|
||||
"""Test states of the weather."""
|
||||
|
||||
now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00")
|
||||
with patch("homeassistant.util.dt.now", return_value=now), patch(
|
||||
"homeassistant.util.dt.utcnow", return_value=now
|
||||
):
|
||||
await async_init_integration(hass)
|
||||
|
||||
state = hass.states.get("weather.aemet_daily")
|
||||
assert state
|
||||
assert state.state == ATTR_CONDITION_SNOWY
|
||||
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
|
||||
assert state.attributes.get(ATTR_WEATHER_HUMIDITY) == 99.0
|
||||
assert state.attributes.get(ATTR_WEATHER_PRESSURE) == 1004.4
|
||||
assert state.attributes.get(ATTR_WEATHER_TEMPERATURE) == -0.7
|
||||
assert state.attributes.get(ATTR_WEATHER_WIND_BEARING) == 90.0
|
||||
assert state.attributes.get(ATTR_WEATHER_WIND_SPEED) == 15
|
||||
forecast = state.attributes.get(ATTR_FORECAST)[0]
|
||||
assert forecast.get(ATTR_FORECAST_CONDITION) == ATTR_CONDITION_PARTLYCLOUDY
|
||||
assert forecast.get(ATTR_FORECAST_PRECIPITATION) is None
|
||||
assert forecast.get(ATTR_FORECAST_PRECIPITATION_PROBABILITY) == 30
|
||||
assert forecast.get(ATTR_FORECAST_TEMP) == 4
|
||||
assert forecast.get(ATTR_FORECAST_TEMP_LOW) == -4
|
||||
assert forecast.get(ATTR_FORECAST_TIME) == dt_util.parse_datetime(
|
||||
"2021-01-10 00:00:00+00:00"
|
||||
)
|
||||
assert forecast.get(ATTR_FORECAST_WIND_BEARING) == 45.0
|
||||
assert forecast.get(ATTR_FORECAST_WIND_SPEED) == 20
|
||||
|
||||
state = hass.states.get("weather.aemet_hourly")
|
||||
assert state is None
|
93
tests/components/aemet/util.py
Normal file
93
tests/components/aemet/util.py
Normal file
@ -0,0 +1,93 @@
|
||||
"""Tests for the AEMET OpenData integration."""
|
||||
|
||||
import requests_mock
|
||||
|
||||
from homeassistant.components.aemet import DOMAIN
|
||||
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry, load_fixture
|
||||
|
||||
|
||||
def aemet_requests_mock(mock):
|
||||
"""Mock requests performed to AEMET OpenData API."""
|
||||
|
||||
station_3195_fixture = "aemet/station-3195.json"
|
||||
station_3195_data_fixture = "aemet/station-3195-data.json"
|
||||
station_list_fixture = "aemet/station-list.json"
|
||||
station_list_data_fixture = "aemet/station-list-data.json"
|
||||
|
||||
town_28065_forecast_daily_fixture = "aemet/town-28065-forecast-daily.json"
|
||||
town_28065_forecast_daily_data_fixture = "aemet/town-28065-forecast-daily-data.json"
|
||||
town_28065_forecast_hourly_fixture = "aemet/town-28065-forecast-hourly.json"
|
||||
town_28065_forecast_hourly_data_fixture = (
|
||||
"aemet/town-28065-forecast-hourly-data.json"
|
||||
)
|
||||
town_id28065_fixture = "aemet/town-id28065.json"
|
||||
town_list_fixture = "aemet/town-list.json"
|
||||
|
||||
mock.get(
|
||||
"https://opendata.aemet.es/opendata/api/observacion/convencional/datos/estacion/3195",
|
||||
text=load_fixture(station_3195_fixture),
|
||||
)
|
||||
mock.get(
|
||||
"https://opendata.aemet.es/opendata/sh/208c3ca3",
|
||||
text=load_fixture(station_3195_data_fixture),
|
||||
)
|
||||
mock.get(
|
||||
"https://opendata.aemet.es/opendata/api/observacion/convencional/todas",
|
||||
text=load_fixture(station_list_fixture),
|
||||
)
|
||||
mock.get(
|
||||
"https://opendata.aemet.es/opendata/sh/2c55192f",
|
||||
text=load_fixture(station_list_data_fixture),
|
||||
)
|
||||
mock.get(
|
||||
"https://opendata.aemet.es/opendata/api/prediccion/especifica/municipio/diaria/28065",
|
||||
text=load_fixture(town_28065_forecast_daily_fixture),
|
||||
)
|
||||
mock.get(
|
||||
"https://opendata.aemet.es/opendata/sh/64e29abb",
|
||||
text=load_fixture(town_28065_forecast_daily_data_fixture),
|
||||
)
|
||||
mock.get(
|
||||
"https://opendata.aemet.es/opendata/api/prediccion/especifica/municipio/horaria/28065",
|
||||
text=load_fixture(town_28065_forecast_hourly_fixture),
|
||||
)
|
||||
mock.get(
|
||||
"https://opendata.aemet.es/opendata/sh/18ca1886",
|
||||
text=load_fixture(town_28065_forecast_hourly_data_fixture),
|
||||
)
|
||||
mock.get(
|
||||
"https://opendata.aemet.es/opendata/api/maestro/municipio/id28065",
|
||||
text=load_fixture(town_id28065_fixture),
|
||||
)
|
||||
mock.get(
|
||||
"https://opendata.aemet.es/opendata/api/maestro/municipios",
|
||||
text=load_fixture(town_list_fixture),
|
||||
)
|
||||
|
||||
|
||||
async def async_init_integration(
|
||||
hass: HomeAssistant,
|
||||
skip_setup: bool = False,
|
||||
):
|
||||
"""Set up the AEMET OpenData integration in Home Assistant."""
|
||||
|
||||
with requests_mock.mock() as _m:
|
||||
aemet_requests_mock(_m)
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
CONF_API_KEY: "mock",
|
||||
CONF_LATITUDE: "40.30403754",
|
||||
CONF_LONGITUDE: "-3.72935236",
|
||||
CONF_NAME: "AEMET",
|
||||
},
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
if not skip_setup:
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
369
tests/fixtures/aemet/station-3195-data.json
vendored
Normal file
369
tests/fixtures/aemet/station-3195-data.json
vendored
Normal file
@ -0,0 +1,369 @@
|
||||
[ {
|
||||
"idema" : "3195",
|
||||
"lon" : -3.678095,
|
||||
"fint" : "2021-01-08T14:00:00",
|
||||
"prec" : 1.2,
|
||||
"alt" : 667.0,
|
||||
"lat" : 40.411804,
|
||||
"ubi" : "MADRID RETIRO",
|
||||
"pres" : 929.9,
|
||||
"hr" : 97.0,
|
||||
"pres_nmar" : 1009.9,
|
||||
"tamin" : -0.1,
|
||||
"ta" : 0.1,
|
||||
"tamax" : 0.2,
|
||||
"tpr" : -0.3,
|
||||
"rviento" : 132.0
|
||||
}, {
|
||||
"idema" : "3195",
|
||||
"lon" : -3.678095,
|
||||
"fint" : "2021-01-08T15:00:00",
|
||||
"prec" : 1.5,
|
||||
"alt" : 667.0,
|
||||
"lat" : 40.411804,
|
||||
"ubi" : "MADRID RETIRO",
|
||||
"pres" : 929.0,
|
||||
"hr" : 98.0,
|
||||
"pres_nmar" : 1008.9,
|
||||
"tamin" : 0.1,
|
||||
"ta" : 0.2,
|
||||
"tamax" : 0.3,
|
||||
"tpr" : 0.0,
|
||||
"rviento" : 154.0
|
||||
}, {
|
||||
"idema" : "3195",
|
||||
"lon" : -3.678095,
|
||||
"fint" : "2021-01-08T16:00:00",
|
||||
"prec" : 0.7,
|
||||
"alt" : 667.0,
|
||||
"lat" : 40.411804,
|
||||
"ubi" : "MADRID RETIRO",
|
||||
"pres" : 928.8,
|
||||
"hr" : 98.0,
|
||||
"pres_nmar" : 1008.6,
|
||||
"tamin" : 0.2,
|
||||
"ta" : 0.3,
|
||||
"tamax" : 0.3,
|
||||
"tpr" : 0.0,
|
||||
"rviento" : 177.0
|
||||
}, {
|
||||
"idema" : "3195",
|
||||
"lon" : -3.678095,
|
||||
"fint" : "2021-01-08T17:00:00",
|
||||
"prec" : 1.7,
|
||||
"alt" : 667.0,
|
||||
"lat" : 40.411804,
|
||||
"ubi" : "MADRID RETIRO",
|
||||
"pres" : 928.6,
|
||||
"hr" : 99.0,
|
||||
"pres_nmar" : 1008.5,
|
||||
"tamin" : 0.1,
|
||||
"ta" : 0.1,
|
||||
"tamax" : 0.3,
|
||||
"tpr" : 0.0,
|
||||
"rviento" : 174.0
|
||||
}, {
|
||||
"idema" : "3195",
|
||||
"lon" : -3.678095,
|
||||
"fint" : "2021-01-08T18:00:00",
|
||||
"prec" : 1.9,
|
||||
"alt" : 667.0,
|
||||
"lat" : 40.411804,
|
||||
"ubi" : "MADRID RETIRO",
|
||||
"pres" : 928.2,
|
||||
"hr" : 99.0,
|
||||
"pres_nmar" : 1008.1,
|
||||
"tamin" : -0.1,
|
||||
"ta" : -0.1,
|
||||
"tamax" : 0.1,
|
||||
"tpr" : -0.3,
|
||||
"rviento" : 163.0
|
||||
}, {
|
||||
"idema" : "3195",
|
||||
"lon" : -3.678095,
|
||||
"fint" : "2021-01-08T19:00:00",
|
||||
"prec" : 3.0,
|
||||
"alt" : 667.0,
|
||||
"lat" : 40.411804,
|
||||
"ubi" : "MADRID RETIRO",
|
||||
"pres" : 928.4,
|
||||
"hr" : 99.0,
|
||||
"pres_nmar" : 1008.4,
|
||||
"tamin" : -0.3,
|
||||
"ta" : -0.3,
|
||||
"tamax" : 0.0,
|
||||
"tpr" : -0.5,
|
||||
"rviento" : 79.0
|
||||
}, {
|
||||
"idema" : "3195",
|
||||
"lon" : -3.678095,
|
||||
"fint" : "2021-01-08T20:00:00",
|
||||
"prec" : 3.5,
|
||||
"alt" : 667.0,
|
||||
"lat" : 40.411804,
|
||||
"ubi" : "MADRID RETIRO",
|
||||
"pres" : 928.4,
|
||||
"hr" : 99.0,
|
||||
"pres_nmar" : 1008.5,
|
||||
"tamin" : -0.6,
|
||||
"ta" : -0.6,
|
||||
"tamax" : -0.3,
|
||||
"tpr" : -0.7,
|
||||
"rviento" : 0.0
|
||||
}, {
|
||||
"idema" : "3195",
|
||||
"lon" : -3.678095,
|
||||
"fint" : "2021-01-08T21:00:00",
|
||||
"prec" : 2.6,
|
||||
"alt" : 667.0,
|
||||
"lat" : 40.411804,
|
||||
"ubi" : "MADRID RETIRO",
|
||||
"pres" : 928.1,
|
||||
"hr" : 99.0,
|
||||
"pres_nmar" : 1008.2,
|
||||
"tamin" : -0.7,
|
||||
"ta" : -0.7,
|
||||
"tamax" : -0.5,
|
||||
"tpr" : -0.7,
|
||||
"rviento" : 0.0
|
||||
}, {
|
||||
"idema" : "3195",
|
||||
"lon" : -3.678095,
|
||||
"fint" : "2021-01-08T22:00:00",
|
||||
"prec" : 3.0,
|
||||
"alt" : 667.0,
|
||||
"lat" : 40.411804,
|
||||
"ubi" : "MADRID RETIRO",
|
||||
"pres" : 927.6,
|
||||
"hr" : 99.0,
|
||||
"pres_nmar" : 1007.7,
|
||||
"tamin" : -0.8,
|
||||
"ta" : -0.8,
|
||||
"tamax" : -0.7,
|
||||
"tpr" : -1.0,
|
||||
"rviento" : 0.0
|
||||
}, {
|
||||
"idema" : "3195",
|
||||
"lon" : -3.678095,
|
||||
"fint" : "2021-01-08T23:00:00",
|
||||
"prec" : 2.9,
|
||||
"alt" : 667.0,
|
||||
"lat" : 40.411804,
|
||||
"ubi" : "MADRID RETIRO",
|
||||
"pres" : 926.9,
|
||||
"hr" : 99.0,
|
||||
"pres_nmar" : 1007.0,
|
||||
"tamin" : -0.9,
|
||||
"ta" : -0.9,
|
||||
"tamax" : -0.7,
|
||||
"tpr" : -1.0,
|
||||
"rviento" : 0.0
|
||||
}, {
|
||||
"idema" : "3195",
|
||||
"lon" : -3.678095,
|
||||
"fint" : "2021-01-09T00:00:00",
|
||||
"prec" : 1.4,
|
||||
"alt" : 667.0,
|
||||
"lat" : 40.411804,
|
||||
"ubi" : "MADRID RETIRO",
|
||||
"pres" : 926.5,
|
||||
"hr" : 99.0,
|
||||
"pres_nmar" : 1006.6,
|
||||
"tamin" : -1.0,
|
||||
"ta" : -1.0,
|
||||
"tamax" : -0.8,
|
||||
"tpr" : -1.2,
|
||||
"rviento" : 0.0
|
||||
}, {
|
||||
"idema" : "3195",
|
||||
"lon" : -3.678095,
|
||||
"fint" : "2021-01-09T01:00:00",
|
||||
"prec" : 2.0,
|
||||
"alt" : 667.0,
|
||||
"lat" : 40.411804,
|
||||
"ubi" : "MADRID RETIRO",
|
||||
"pres" : 925.9,
|
||||
"hr" : 99.0,
|
||||
"pres_nmar" : 1006.0,
|
||||
"tamin" : -1.3,
|
||||
"ta" : -1.3,
|
||||
"tamax" : -1.0,
|
||||
"tpr" : -1.4,
|
||||
"rviento" : 0.0
|
||||
}, {
|
||||
"idema" : "3195",
|
||||
"lon" : -3.678095,
|
||||
"fint" : "2021-01-09T02:00:00",
|
||||
"prec" : 1.5,
|
||||
"alt" : 667.0,
|
||||
"lat" : 40.411804,
|
||||
"ubi" : "MADRID RETIRO",
|
||||
"pres" : 925.7,
|
||||
"hr" : 99.0,
|
||||
"pres_nmar" : 1005.8,
|
||||
"tamin" : -1.5,
|
||||
"ta" : -1.4,
|
||||
"tamax" : -1.3,
|
||||
"tpr" : -1.4,
|
||||
"rviento" : 0.0
|
||||
}, {
|
||||
"idema" : "3195",
|
||||
"lon" : -3.678095,
|
||||
"fint" : "2021-01-09T03:00:00",
|
||||
"prec" : 1.2,
|
||||
"alt" : 667.0,
|
||||
"lat" : 40.411804,
|
||||
"ubi" : "MADRID RETIRO",
|
||||
"pres" : 925.6,
|
||||
"hr" : 99.0,
|
||||
"pres_nmar" : 1005.7,
|
||||
"tamin" : -1.5,
|
||||
"ta" : -1.4,
|
||||
"tamax" : -1.4,
|
||||
"tpr" : -1.4,
|
||||
"rviento" : 0.0
|
||||
}, {
|
||||
"idema" : "3195",
|
||||
"lon" : -3.678095,
|
||||
"fint" : "2021-01-09T04:00:00",
|
||||
"prec" : 1.1,
|
||||
"alt" : 667.0,
|
||||
"lat" : 40.411804,
|
||||
"ubi" : "MADRID RETIRO",
|
||||
"pres" : 924.9,
|
||||
"hr" : 99.0,
|
||||
"pres_nmar" : 1005.0,
|
||||
"tamin" : -1.5,
|
||||
"ta" : -1.5,
|
||||
"tamax" : -1.4,
|
||||
"tpr" : -1.7,
|
||||
"rviento" : 0.0
|
||||
}, {
|
||||
"idema" : "3195",
|
||||
"lon" : -3.678095,
|
||||
"fint" : "2021-01-09T05:00:00",
|
||||
"prec" : 0.7,
|
||||
"alt" : 667.0,
|
||||
"lat" : 40.411804,
|
||||
"ubi" : "MADRID RETIRO",
|
||||
"pres" : 924.6,
|
||||
"hr" : 99.0,
|
||||
"pres_nmar" : 1004.7,
|
||||
"tamin" : -1.5,
|
||||
"ta" : -1.5,
|
||||
"tamax" : -1.4,
|
||||
"tpr" : -1.7,
|
||||
"rviento" : 0.0
|
||||
}, {
|
||||
"idema" : "3195",
|
||||
"lon" : -3.678095,
|
||||
"fint" : "2021-01-09T06:00:00",
|
||||
"prec" : 0.2,
|
||||
"alt" : 667.0,
|
||||
"lat" : 40.411804,
|
||||
"ubi" : "MADRID RETIRO",
|
||||
"pres" : 924.4,
|
||||
"hr" : 99.0,
|
||||
"pres_nmar" : 1004.5,
|
||||
"tamin" : -1.6,
|
||||
"ta" : -1.6,
|
||||
"tamax" : -1.5,
|
||||
"tpr" : -1.7,
|
||||
"rviento" : 0.0
|
||||
}, {
|
||||
"idema" : "3195",
|
||||
"lon" : -3.678095,
|
||||
"fint" : "2021-01-09T07:00:00",
|
||||
"prec" : 0.0,
|
||||
"alt" : 667.0,
|
||||
"lat" : 40.411804,
|
||||
"ubi" : "MADRID RETIRO",
|
||||
"pres" : 924.4,
|
||||
"hr" : 99.0,
|
||||
"pres_nmar" : 1004.5,
|
||||
"tamin" : -1.6,
|
||||
"ta" : -1.6,
|
||||
"tamax" : -1.6,
|
||||
"tpr" : -1.7,
|
||||
"rviento" : 0.0
|
||||
}, {
|
||||
"idema" : "3195",
|
||||
"lon" : -3.678095,
|
||||
"fint" : "2021-01-09T08:00:00",
|
||||
"prec" : 0.1,
|
||||
"alt" : 667.0,
|
||||
"lat" : 40.411804,
|
||||
"ubi" : "MADRID RETIRO",
|
||||
"pres" : 924.8,
|
||||
"hr" : 99.0,
|
||||
"pres_nmar" : 1004.9,
|
||||
"tamin" : -1.6,
|
||||
"ta" : -1.6,
|
||||
"tamax" : -1.5,
|
||||
"tpr" : -1.7,
|
||||
"rviento" : 0.0
|
||||
}, {
|
||||
"idema" : "3195",
|
||||
"lon" : -3.678095,
|
||||
"fint" : "2021-01-09T09:00:00",
|
||||
"prec" : 0.0,
|
||||
"alt" : 667.0,
|
||||
"lat" : 40.411804,
|
||||
"ubi" : "MADRID RETIRO",
|
||||
"pres" : 925.0,
|
||||
"hr" : 99.0,
|
||||
"pres_nmar" : 1005.0,
|
||||
"tamin" : -1.6,
|
||||
"ta" : -1.3,
|
||||
"tamax" : -1.3,
|
||||
"tpr" : -1.4,
|
||||
"rviento" : 0.0
|
||||
}, {
|
||||
"idema" : "3195",
|
||||
"lon" : -3.678095,
|
||||
"fint" : "2021-01-09T10:00:00",
|
||||
"prec" : 0.0,
|
||||
"alt" : 667.0,
|
||||
"lat" : 40.411804,
|
||||
"ubi" : "MADRID RETIRO",
|
||||
"pres" : 925.3,
|
||||
"hr" : 99.0,
|
||||
"pres_nmar" : 1005.3,
|
||||
"tamin" : -1.3,
|
||||
"ta" : -1.2,
|
||||
"tamax" : -1.1,
|
||||
"tpr" : -1.4,
|
||||
"rviento" : 0.0
|
||||
}, {
|
||||
"idema" : "3195",
|
||||
"lon" : -3.678095,
|
||||
"fint" : "2021-01-09T11:00:00",
|
||||
"prec" : 4.4,
|
||||
"alt" : 667.0,
|
||||
"lat" : 40.411804,
|
||||
"ubi" : "MADRID RETIRO",
|
||||
"pres" : 925.4,
|
||||
"hr" : 99.0,
|
||||
"pres_nmar" : 1005.4,
|
||||
"tamin" : -1.2,
|
||||
"ta" : -1.0,
|
||||
"tamax" : -1.0,
|
||||
"tpr" : -1.2,
|
||||
"rviento" : 0.0
|
||||
}, {
|
||||
"idema" : "3195",
|
||||
"lon" : -3.678095,
|
||||
"fint" : "2021-01-09T12:00:00",
|
||||
"prec" : 7.0,
|
||||
"alt" : 667.0,
|
||||
"lat" : 40.411804,
|
||||
"ubi" : "MADRID RETIRO",
|
||||
"pres" : 924.6,
|
||||
"hr" : 99.0,
|
||||
"pres_nmar" : 1004.4,
|
||||
"tamin" : -1.0,
|
||||
"ta" : -0.7,
|
||||
"tamax" : -0.6,
|
||||
"tpr" : -0.7,
|
||||
"rviento" : 0.0
|
||||
} ]
|
6
tests/fixtures/aemet/station-3195.json
vendored
Normal file
6
tests/fixtures/aemet/station-3195.json
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"descripcion" : "exito",
|
||||
"estado" : 200,
|
||||
"datos" : "https://opendata.aemet.es/opendata/sh/208c3ca3",
|
||||
"metadatos" : "https://opendata.aemet.es/opendata/sh/55c2971b"
|
||||
}
|
42
tests/fixtures/aemet/station-list-data.json
vendored
Normal file
42
tests/fixtures/aemet/station-list-data.json
vendored
Normal file
@ -0,0 +1,42 @@
|
||||
[ {
|
||||
"idema" : "3194U",
|
||||
"lon" : -3.724167,
|
||||
"fint" : "2021-01-08T14:00:00",
|
||||
"prec" : 1.3,
|
||||
"alt" : 664.0,
|
||||
"lat" : 40.45167,
|
||||
"ubi" : "MADRID C. UNIVERSITARIA",
|
||||
"hr" : 98.0,
|
||||
"tamin" : 0.6,
|
||||
"ta" : 0.9,
|
||||
"tamax" : 1.0,
|
||||
"tpr" : 0.6
|
||||
}, {
|
||||
"idema" : "3194Y",
|
||||
"lon" : -3.813369,
|
||||
"fint" : "2021-01-08T14:00:00",
|
||||
"prec" : 0.2,
|
||||
"alt" : 665.0,
|
||||
"lat" : 40.448437,
|
||||
"ubi" : "POZUELO DE ALARCON (AUTOM<4F>TICA)",
|
||||
"hr" : 93.0,
|
||||
"tamin" : 0.5,
|
||||
"ta" : 0.6,
|
||||
"tamax" : 0.6
|
||||
}, {
|
||||
"idema" : "3195",
|
||||
"lon" : -3.678095,
|
||||
"fint" : "2021-01-08T14:00:00",
|
||||
"prec" : 1.2,
|
||||
"alt" : 667.0,
|
||||
"lat" : 40.411804,
|
||||
"ubi" : "MADRID RETIRO",
|
||||
"pres" : 929.9,
|
||||
"hr" : 97.0,
|
||||
"pres_nmar" : 1009.9,
|
||||
"tamin" : -0.1,
|
||||
"ta" : 0.1,
|
||||
"tamax" : 0.2,
|
||||
"tpr" : -0.3,
|
||||
"rviento" : 132.0
|
||||
} ]
|
6
tests/fixtures/aemet/station-list.json
vendored
Normal file
6
tests/fixtures/aemet/station-list.json
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"descripcion" : "exito",
|
||||
"estado" : 200,
|
||||
"datos" : "https://opendata.aemet.es/opendata/sh/2c55192f",
|
||||
"metadatos" : "https://opendata.aemet.es/opendata/sh/55c2971b"
|
||||
}
|
625
tests/fixtures/aemet/town-28065-forecast-daily-data.json
vendored
Normal file
625
tests/fixtures/aemet/town-28065-forecast-daily-data.json
vendored
Normal file
@ -0,0 +1,625 @@
|
||||
[ {
|
||||
"origen" : {
|
||||
"productor" : "Agencia Estatal de Meteorolog<6F>a - AEMET. Gobierno de Espa<70>a",
|
||||
"web" : "http://www.aemet.es",
|
||||
"enlace" : "http://www.aemet.es/es/eltiempo/prediccion/municipios/getafe-id28065",
|
||||
"language" : "es",
|
||||
"copyright" : "<22> AEMET. Autorizado el uso de la informaci<63>n y su reproducci<63>n citando a AEMET como autora de la misma.",
|
||||
"notaLegal" : "http://www.aemet.es/es/nota_legal"
|
||||
},
|
||||
"elaborado" : "2021-01-09T11:54:00",
|
||||
"nombre" : "Getafe",
|
||||
"provincia" : "Madrid",
|
||||
"prediccion" : {
|
||||
"dia" : [ {
|
||||
"probPrecipitacion" : [ {
|
||||
"value" : 0,
|
||||
"periodo" : "00-24"
|
||||
}, {
|
||||
"value" : 0,
|
||||
"periodo" : "00-12"
|
||||
}, {
|
||||
"value" : 100,
|
||||
"periodo" : "12-24"
|
||||
}, {
|
||||
"value" : 0,
|
||||
"periodo" : "00-06"
|
||||
}, {
|
||||
"value" : 100,
|
||||
"periodo" : "06-12"
|
||||
}, {
|
||||
"value" : 100,
|
||||
"periodo" : "12-18"
|
||||
}, {
|
||||
"value" : 100,
|
||||
"periodo" : "18-24"
|
||||
} ],
|
||||
"cotaNieveProv" : [ {
|
||||
"value" : "",
|
||||
"periodo" : "00-24"
|
||||
}, {
|
||||
"value" : "",
|
||||
"periodo" : "00-12"
|
||||
}, {
|
||||
"value" : "500",
|
||||
"periodo" : "12-24"
|
||||
}, {
|
||||
"value" : "",
|
||||
"periodo" : "00-06"
|
||||
}, {
|
||||
"value" : "400",
|
||||
"periodo" : "06-12"
|
||||
}, {
|
||||
"value" : "500",
|
||||
"periodo" : "12-18"
|
||||
}, {
|
||||
"value" : "600",
|
||||
"periodo" : "18-24"
|
||||
} ],
|
||||
"estadoCielo" : [ {
|
||||
"value" : "",
|
||||
"periodo" : "00-24",
|
||||
"descripcion" : ""
|
||||
}, {
|
||||
"value" : "",
|
||||
"periodo" : "00-12",
|
||||
"descripcion" : ""
|
||||
}, {
|
||||
"value" : "36",
|
||||
"periodo" : "12-24",
|
||||
"descripcion" : "Cubierto con nieve"
|
||||
}, {
|
||||
"value" : "",
|
||||
"periodo" : "00-06",
|
||||
"descripcion" : ""
|
||||
}, {
|
||||
"value" : "36",
|
||||
"periodo" : "06-12",
|
||||
"descripcion" : "Cubierto con nieve"
|
||||
}, {
|
||||
"value" : "36",
|
||||
"periodo" : "12-18",
|
||||
"descripcion" : "Cubierto con nieve"
|
||||
}, {
|
||||
"value" : "34n",
|
||||
"periodo" : "18-24",
|
||||
"descripcion" : "Nuboso con nieve"
|
||||
} ],
|
||||
"viento" : [ {
|
||||
"direccion" : "",
|
||||
"velocidad" : 0,
|
||||
"periodo" : "00-24"
|
||||
}, {
|
||||
"direccion" : "",
|
||||
"velocidad" : 0,
|
||||
"periodo" : "00-12"
|
||||
}, {
|
||||
"direccion" : "E",
|
||||
"velocidad" : 15,
|
||||
"periodo" : "12-24"
|
||||
}, {
|
||||
"direccion" : "NE",
|
||||
"velocidad" : 30,
|
||||
"periodo" : "00-06"
|
||||
}, {
|
||||
"direccion" : "E",
|
||||
"velocidad" : 15,
|
||||
"periodo" : "06-12"
|
||||
}, {
|
||||
"direccion" : "E",
|
||||
"velocidad" : 5,
|
||||
"periodo" : "12-18"
|
||||
}, {
|
||||
"direccion" : "NE",
|
||||
"velocidad" : 5,
|
||||
"periodo" : "18-24"
|
||||
} ],
|
||||
"rachaMax" : [ {
|
||||
"value" : "",
|
||||
"periodo" : "00-24"
|
||||
}, {
|
||||
"value" : "",
|
||||
"periodo" : "00-12"
|
||||
}, {
|
||||
"value" : "",
|
||||
"periodo" : "12-24"
|
||||
}, {
|
||||
"value" : "40",
|
||||
"periodo" : "00-06"
|
||||
}, {
|
||||
"value" : "",
|
||||
"periodo" : "06-12"
|
||||
}, {
|
||||
"value" : "",
|
||||
"periodo" : "12-18"
|
||||
}, {
|
||||
"value" : "",
|
||||
"periodo" : "18-24"
|
||||
} ],
|
||||
"temperatura" : {
|
||||
"maxima" : 2,
|
||||
"minima" : -1,
|
||||
"dato" : [ {
|
||||
"value" : -1,
|
||||
"hora" : 6
|
||||
}, {
|
||||
"value" : 0,
|
||||
"hora" : 12
|
||||
}, {
|
||||
"value" : 1,
|
||||
"hora" : 18
|
||||
}, {
|
||||
"value" : 1,
|
||||
"hora" : 24
|
||||
} ]
|
||||
},
|
||||
"sensTermica" : {
|
||||
"maxima" : 1,
|
||||
"minima" : -9,
|
||||
"dato" : [ {
|
||||
"value" : -1,
|
||||
"hora" : 6
|
||||
}, {
|
||||
"value" : -4,
|
||||
"hora" : 12
|
||||
}, {
|
||||
"value" : 1,
|
||||
"hora" : 18
|
||||
}, {
|
||||
"value" : 1,
|
||||
"hora" : 24
|
||||
} ]
|
||||
},
|
||||
"humedadRelativa" : {
|
||||
"maxima" : 100,
|
||||
"minima" : 75,
|
||||
"dato" : [ {
|
||||
"value" : 100,
|
||||
"hora" : 6
|
||||
}, {
|
||||
"value" : 100,
|
||||
"hora" : 12
|
||||
}, {
|
||||
"value" : 95,
|
||||
"hora" : 18
|
||||
}, {
|
||||
"value" : 75,
|
||||
"hora" : 24
|
||||
} ]
|
||||
},
|
||||
"uvMax" : 1,
|
||||
"fecha" : "2021-01-09T00:00:00"
|
||||
}, {
|
||||
"probPrecipitacion" : [ {
|
||||
"value" : 30,
|
||||
"periodo" : "00-24"
|
||||
}, {
|
||||
"value" : 25,
|
||||
"periodo" : "00-12"
|
||||
}, {
|
||||
"value" : 5,
|
||||
"periodo" : "12-24"
|
||||
}, {
|
||||
"value" : 5,
|
||||
"periodo" : "00-06"
|
||||
}, {
|
||||
"value" : 15,
|
||||
"periodo" : "06-12"
|
||||
}, {
|
||||
"value" : 5,
|
||||
"periodo" : "12-18"
|
||||
}, {
|
||||
"value" : 0,
|
||||
"periodo" : "18-24"
|
||||
} ],
|
||||
"cotaNieveProv" : [ {
|
||||
"value" : "600",
|
||||
"periodo" : "00-24"
|
||||
}, {
|
||||
"value" : "600",
|
||||
"periodo" : "00-12"
|
||||
}, {
|
||||
"value" : "",
|
||||
"periodo" : "12-24"
|
||||
}, {
|
||||
"value" : "",
|
||||
"periodo" : "00-06"
|
||||
}, {
|
||||
"value" : "600",
|
||||
"periodo" : "06-12"
|
||||
}, {
|
||||
"value" : "",
|
||||
"periodo" : "12-18"
|
||||
}, {
|
||||
"value" : "",
|
||||
"periodo" : "18-24"
|
||||
} ],
|
||||
"estadoCielo" : [ {
|
||||
"value" : "13",
|
||||
"periodo" : "00-24",
|
||||
"descripcion" : "Intervalos nubosos"
|
||||
}, {
|
||||
"value" : "15",
|
||||
"periodo" : "00-12",
|
||||
"descripcion" : "Muy nuboso"
|
||||
}, {
|
||||
"value" : "12",
|
||||
"periodo" : "12-24",
|
||||
"descripcion" : "Poco nuboso"
|
||||
}, {
|
||||
"value" : "14n",
|
||||
"periodo" : "00-06",
|
||||
"descripcion" : "Nuboso"
|
||||
}, {
|
||||
"value" : "15",
|
||||
"periodo" : "06-12",
|
||||
"descripcion" : "Muy nuboso"
|
||||
}, {
|
||||
"value" : "12",
|
||||
"periodo" : "12-18",
|
||||
"descripcion" : "Poco nuboso"
|
||||
}, {
|
||||
"value" : "12n",
|
||||
"periodo" : "18-24",
|
||||
"descripcion" : "Poco nuboso"
|
||||
} ],
|
||||
"viento" : [ {
|
||||
"direccion" : "NE",
|
||||
"velocidad" : 20,
|
||||
"periodo" : "00-24"
|
||||
}, {
|
||||
"direccion" : "NE",
|
||||
"velocidad" : 20,
|
||||
"periodo" : "00-12"
|
||||
}, {
|
||||
"direccion" : "NE",
|
||||
"velocidad" : 20,
|
||||
"periodo" : "12-24"
|
||||
}, {
|
||||
"direccion" : "N",
|
||||
"velocidad" : 10,
|
||||
"periodo" : "00-06"
|
||||
}, {
|
||||
"direccion" : "NE",
|
||||
"velocidad" : 20,
|
||||
"periodo" : "06-12"
|
||||
}, {
|
||||
"direccion" : "NE",
|
||||
"velocidad" : 15,
|
||||
"periodo" : "12-18"
|
||||
}, {
|
||||
"direccion" : "NE",
|
||||
"velocidad" : 20,
|
||||
"periodo" : "18-24"
|
||||
} ],
|
||||
"rachaMax" : [ {
|
||||
"value" : "30",
|
||||
"periodo" : "00-24"
|
||||
}, {
|
||||
"value" : "30",
|
||||
"periodo" : "00-12"
|
||||
}, {
|
||||
"value" : "30",
|
||||
"periodo" : "12-24"
|
||||
}, {
|
||||
"value" : "",
|
||||
"periodo" : "00-06"
|
||||
}, {
|
||||
"value" : "30",
|
||||
"periodo" : "06-12"
|
||||
}, {
|
||||
"value" : "",
|
||||
"periodo" : "12-18"
|
||||
}, {
|
||||
"value" : "",
|
||||
"periodo" : "18-24"
|
||||
} ],
|
||||
"temperatura" : {
|
||||
"maxima" : 4,
|
||||
"minima" : -4,
|
||||
"dato" : [ {
|
||||
"value" : -1,
|
||||
"hora" : 6
|
||||
}, {
|
||||
"value" : 3,
|
||||
"hora" : 12
|
||||
}, {
|
||||
"value" : 1,
|
||||
"hora" : 18
|
||||
}, {
|
||||
"value" : -1,
|
||||
"hora" : 24
|
||||
} ]
|
||||
},
|
||||
"sensTermica" : {
|
||||
"maxima" : 1,
|
||||
"minima" : -7,
|
||||
"dato" : [ {
|
||||
"value" : -4,
|
||||
"hora" : 6
|
||||
}, {
|
||||
"value" : -2,
|
||||
"hora" : 12
|
||||
}, {
|
||||
"value" : -4,
|
||||
"hora" : 18
|
||||
}, {
|
||||
"value" : -6,
|
||||
"hora" : 24
|
||||
} ]
|
||||
},
|
||||
"humedadRelativa" : {
|
||||
"maxima" : 100,
|
||||
"minima" : 70,
|
||||
"dato" : [ {
|
||||
"value" : 90,
|
||||
"hora" : 6
|
||||
}, {
|
||||
"value" : 75,
|
||||
"hora" : 12
|
||||
}, {
|
||||
"value" : 80,
|
||||
"hora" : 18
|
||||
}, {
|
||||
"value" : 80,
|
||||
"hora" : 24
|
||||
} ]
|
||||
},
|
||||
"uvMax" : 1,
|
||||
"fecha" : "2021-01-10T00:00:00"
|
||||
}, {
|
||||
"probPrecipitacion" : [ {
|
||||
"value" : 0,
|
||||
"periodo" : "00-24"
|
||||
}, {
|
||||
"value" : 0,
|
||||
"periodo" : "00-12"
|
||||
}, {
|
||||
"value" : 0,
|
||||
"periodo" : "12-24"
|
||||
} ],
|
||||
"cotaNieveProv" : [ {
|
||||
"value" : "",
|
||||
"periodo" : "00-24"
|
||||
}, {
|
||||
"value" : "",
|
||||
"periodo" : "00-12"
|
||||
}, {
|
||||
"value" : "",
|
||||
"periodo" : "12-24"
|
||||
} ],
|
||||
"estadoCielo" : [ {
|
||||
"value" : "12",
|
||||
"periodo" : "00-24",
|
||||
"descripcion" : "Poco nuboso"
|
||||
}, {
|
||||
"value" : "12",
|
||||
"periodo" : "00-12",
|
||||
"descripcion" : "Poco nuboso"
|
||||
}, {
|
||||
"value" : "12",
|
||||
"periodo" : "12-24",
|
||||
"descripcion" : "Poco nuboso"
|
||||
} ],
|
||||
"viento" : [ {
|
||||
"direccion" : "N",
|
||||
"velocidad" : 5,
|
||||
"periodo" : "00-24"
|
||||
}, {
|
||||
"direccion" : "NE",
|
||||
"velocidad" : 20,
|
||||
"periodo" : "00-12"
|
||||
}, {
|
||||
"direccion" : "NO",
|
||||
"velocidad" : 10,
|
||||
"periodo" : "12-24"
|
||||
} ],
|
||||
"rachaMax" : [ {
|
||||
"value" : "",
|
||||
"periodo" : "00-24"
|
||||
}, {
|
||||
"value" : "",
|
||||
"periodo" : "00-12"
|
||||
}, {
|
||||
"value" : "",
|
||||
"periodo" : "12-24"
|
||||
} ],
|
||||
"temperatura" : {
|
||||
"maxima" : 3,
|
||||
"minima" : -7,
|
||||
"dato" : [ ]
|
||||
},
|
||||
"sensTermica" : {
|
||||
"maxima" : 3,
|
||||
"minima" : -8,
|
||||
"dato" : [ ]
|
||||
},
|
||||
"humedadRelativa" : {
|
||||
"maxima" : 85,
|
||||
"minima" : 60,
|
||||
"dato" : [ ]
|
||||
},
|
||||
"uvMax" : 1,
|
||||
"fecha" : "2021-01-11T00:00:00"
|
||||
}, {
|
||||
"probPrecipitacion" : [ {
|
||||
"value" : 0,
|
||||
"periodo" : "00-24"
|
||||
}, {
|
||||
"value" : 0,
|
||||
"periodo" : "00-12"
|
||||
}, {
|
||||
"value" : 0,
|
||||
"periodo" : "12-24"
|
||||
} ],
|
||||
"cotaNieveProv" : [ {
|
||||
"value" : "",
|
||||
"periodo" : "00-24"
|
||||
}, {
|
||||
"value" : "",
|
||||
"periodo" : "00-12"
|
||||
}, {
|
||||
"value" : "",
|
||||
"periodo" : "12-24"
|
||||
} ],
|
||||
"estadoCielo" : [ {
|
||||
"value" : "12",
|
||||
"periodo" : "00-24",
|
||||
"descripcion" : "Poco nuboso"
|
||||
}, {
|
||||
"value" : "12",
|
||||
"periodo" : "00-12",
|
||||
"descripcion" : "Poco nuboso"
|
||||
}, {
|
||||
"value" : "12",
|
||||
"periodo" : "12-24",
|
||||
"descripcion" : "Poco nuboso"
|
||||
} ],
|
||||
"viento" : [ {
|
||||
"direccion" : "C",
|
||||
"velocidad" : 0,
|
||||
"periodo" : "00-24"
|
||||
}, {
|
||||
"direccion" : "E",
|
||||
"velocidad" : 5,
|
||||
"periodo" : "00-12"
|
||||
}, {
|
||||
"direccion" : "C",
|
||||
"velocidad" : 0,
|
||||
"periodo" : "12-24"
|
||||
} ],
|
||||
"rachaMax" : [ {
|
||||
"value" : "",
|
||||
"periodo" : "00-24"
|
||||
}, {
|
||||
"value" : "",
|
||||
"periodo" : "00-12"
|
||||
}, {
|
||||
"value" : "",
|
||||
"periodo" : "12-24"
|
||||
} ],
|
||||
"temperatura" : {
|
||||
"maxima" : -1,
|
||||
"minima" : -13,
|
||||
"dato" : [ ]
|
||||
},
|
||||
"sensTermica" : {
|
||||
"maxima" : -1,
|
||||
"minima" : -13,
|
||||
"dato" : [ ]
|
||||
},
|
||||
"humedadRelativa" : {
|
||||
"maxima" : 100,
|
||||
"minima" : 65,
|
||||
"dato" : [ ]
|
||||
},
|
||||
"uvMax" : 2,
|
||||
"fecha" : "2021-01-12T00:00:00"
|
||||
}, {
|
||||
"probPrecipitacion" : [ {
|
||||
"value" : 0
|
||||
} ],
|
||||
"cotaNieveProv" : [ {
|
||||
"value" : ""
|
||||
} ],
|
||||
"estadoCielo" : [ {
|
||||
"value" : "11",
|
||||
"descripcion" : "Despejado"
|
||||
} ],
|
||||
"viento" : [ {
|
||||
"direccion" : "C",
|
||||
"velocidad" : 0
|
||||
} ],
|
||||
"rachaMax" : [ {
|
||||
"value" : ""
|
||||
} ],
|
||||
"temperatura" : {
|
||||
"maxima" : 6,
|
||||
"minima" : -11,
|
||||
"dato" : [ ]
|
||||
},
|
||||
"sensTermica" : {
|
||||
"maxima" : 6,
|
||||
"minima" : -11,
|
||||
"dato" : [ ]
|
||||
},
|
||||
"humedadRelativa" : {
|
||||
"maxima" : 100,
|
||||
"minima" : 65,
|
||||
"dato" : [ ]
|
||||
},
|
||||
"uvMax" : 2,
|
||||
"fecha" : "2021-01-13T00:00:00"
|
||||
}, {
|
||||
"probPrecipitacion" : [ {
|
||||
"value" : 0
|
||||
} ],
|
||||
"cotaNieveProv" : [ {
|
||||
"value" : ""
|
||||
} ],
|
||||
"estadoCielo" : [ {
|
||||
"value" : "12",
|
||||
"descripcion" : "Poco nuboso"
|
||||
} ],
|
||||
"viento" : [ {
|
||||
"direccion" : "C",
|
||||
"velocidad" : 0
|
||||
} ],
|
||||
"rachaMax" : [ {
|
||||
"value" : ""
|
||||
} ],
|
||||
"temperatura" : {
|
||||
"maxima" : 6,
|
||||
"minima" : -7,
|
||||
"dato" : [ ]
|
||||
},
|
||||
"sensTermica" : {
|
||||
"maxima" : 6,
|
||||
"minima" : -7,
|
||||
"dato" : [ ]
|
||||
},
|
||||
"humedadRelativa" : {
|
||||
"maxima" : 100,
|
||||
"minima" : 80,
|
||||
"dato" : [ ]
|
||||
},
|
||||
"fecha" : "2021-01-14T00:00:00"
|
||||
}, {
|
||||
"probPrecipitacion" : [ {
|
||||
"value" : 0
|
||||
} ],
|
||||
"cotaNieveProv" : [ {
|
||||
"value" : ""
|
||||
} ],
|
||||
"estadoCielo" : [ {
|
||||
"value" : "14",
|
||||
"descripcion" : "Nuboso"
|
||||
} ],
|
||||
"viento" : [ {
|
||||
"direccion" : "C",
|
||||
"velocidad" : 0
|
||||
} ],
|
||||
"rachaMax" : [ {
|
||||
"value" : ""
|
||||
} ],
|
||||
"temperatura" : {
|
||||
"maxima" : 5,
|
||||
"minima" : -4,
|
||||
"dato" : [ ]
|
||||
},
|
||||
"sensTermica" : {
|
||||
"maxima" : 5,
|
||||
"minima" : -4,
|
||||
"dato" : [ ]
|
||||
},
|
||||
"humedadRelativa" : {
|
||||
"maxima" : 100,
|
||||
"minima" : 55,
|
||||
"dato" : [ ]
|
||||
},
|
||||
"fecha" : "2021-01-15T00:00:00"
|
||||
} ]
|
||||
},
|
||||
"id" : 28065,
|
||||
"version" : 1.0
|
||||
} ]
|
6
tests/fixtures/aemet/town-28065-forecast-daily.json
vendored
Normal file
6
tests/fixtures/aemet/town-28065-forecast-daily.json
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"descripcion" : "exito",
|
||||
"estado" : 200,
|
||||
"datos" : "https://opendata.aemet.es/opendata/sh/64e29abb",
|
||||
"metadatos" : "https://opendata.aemet.es/opendata/sh/dfd88b22"
|
||||
}
|
1416
tests/fixtures/aemet/town-28065-forecast-hourly-data.json
vendored
Normal file
1416
tests/fixtures/aemet/town-28065-forecast-hourly-data.json
vendored
Normal file
File diff suppressed because it is too large
Load Diff
6
tests/fixtures/aemet/town-28065-forecast-hourly.json
vendored
Normal file
6
tests/fixtures/aemet/town-28065-forecast-hourly.json
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"descripcion" : "exito",
|
||||
"estado" : 200,
|
||||
"datos" : "https://opendata.aemet.es/opendata/sh/18ca1886",
|
||||
"metadatos" : "https://opendata.aemet.es/opendata/sh/93a7c63d"
|
||||
}
|
15
tests/fixtures/aemet/town-id28065.json
vendored
Normal file
15
tests/fixtures/aemet/town-id28065.json
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
[ {
|
||||
"latitud" : "40<34>18'14.535144\"",
|
||||
"id_old" : "28325",
|
||||
"url" : "getafe-id28065",
|
||||
"latitud_dec" : "40.30403754",
|
||||
"altitud" : "622",
|
||||
"capital" : "Getafe",
|
||||
"num_hab" : "173057",
|
||||
"zona_comarcal" : "722802",
|
||||
"destacada" : "1",
|
||||
"nombre" : "Getafe",
|
||||
"longitud_dec" : "-3.72935236",
|
||||
"id" : "id28065",
|
||||
"longitud" : "-3<>43'45.668496\""
|
||||
} ]
|
43
tests/fixtures/aemet/town-list.json
vendored
Normal file
43
tests/fixtures/aemet/town-list.json
vendored
Normal file
@ -0,0 +1,43 @@
|
||||
[ {
|
||||
"latitud" : "40<34>18'14.535144\"",
|
||||
"id_old" : "28325",
|
||||
"url" : "getafe-id28065",
|
||||
"latitud_dec" : "40.30403754",
|
||||
"altitud" : "622",
|
||||
"capital" : "Getafe",
|
||||
"num_hab" : "173057",
|
||||
"zona_comarcal" : "722802",
|
||||
"destacada" : "1",
|
||||
"nombre" : "Getafe",
|
||||
"longitud_dec" : "-3.72935236",
|
||||
"id" : "id28065",
|
||||
"longitud" : "-3<>43'45.668496\""
|
||||
}, {
|
||||
"latitud" : "40<34>19'54.277752\"",
|
||||
"id_old" : "28370",
|
||||
"url" : "leganes-id28074",
|
||||
"latitud_dec" : "40.33174382",
|
||||
"altitud" : "667",
|
||||
"capital" : "Legan<61>s",
|
||||
"num_hab" : "186696",
|
||||
"zona_comarcal" : "722802",
|
||||
"destacada" : "1",
|
||||
"nombre" : "Legan<61>s",
|
||||
"longitud_dec" : "-3.76655557",
|
||||
"id" : "id28074",
|
||||
"longitud" : "-3<>45'59.600052\""
|
||||
}, {
|
||||
"latitud" : "40<34>24'30.282876\"",
|
||||
"id_old" : "28001",
|
||||
"url" : "madrid-id28079",
|
||||
"latitud_dec" : "40.40841191",
|
||||
"altitud" : "657",
|
||||
"capital" : "Madrid",
|
||||
"num_hab" : "3165235",
|
||||
"zona_comarcal" : "722802",
|
||||
"destacada" : "1",
|
||||
"nombre" : "Madrid",
|
||||
"longitud_dec" : "-3.68760088",
|
||||
"id" : "id28079",
|
||||
"longitud" : "-3<>41'15.363168\""
|
||||
} ]
|
Loading…
x
Reference in New Issue
Block a user