diff --git a/.coveragerc b/.coveragerc
index 3bf3fa10947f785f6c1b9fb390370fb0cfdf94c7..8c7d4b3393de0618ea22bcb75dd567a012da0f78 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -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
diff --git a/CODEOWNERS b/CODEOWNERS
index 3d7ff5f79a502f7e11ff565ac266de5e4d7cdbaf..6e3bc1feb87df7f08193d63ab390af9d66598d05 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -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
diff --git a/homeassistant/components/aemet/__init__.py b/homeassistant/components/aemet/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..58b1a3b10f0399df6ca22d28190665c5b8fe5c1a
--- /dev/null
+++ b/homeassistant/components/aemet/__init__.py
@@ -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
diff --git a/homeassistant/components/aemet/abstract_aemet_sensor.py b/homeassistant/components/aemet/abstract_aemet_sensor.py
new file mode 100644
index 0000000000000000000000000000000000000000..6b7c3c69fee30abc797747da08f1c68506dc1908
--- /dev/null
+++ b/homeassistant/components/aemet/abstract_aemet_sensor.py
@@ -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}
diff --git a/homeassistant/components/aemet/config_flow.py b/homeassistant/components/aemet/config_flow.py
new file mode 100644
index 0000000000000000000000000000000000000000..27f389660a8f9cd919f36b89bce2c98c6c4e3c6d
--- /dev/null
+++ b/homeassistant/components/aemet/config_flow.py
@@ -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
+    )
diff --git a/homeassistant/components/aemet/const.py b/homeassistant/components/aemet/const.py
new file mode 100644
index 0000000000000000000000000000000000000000..13b9d944bf07dca90d34a12687b009d280551bd2
--- /dev/null
+++ b/homeassistant/components/aemet/const.py
@@ -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,
+}
diff --git a/homeassistant/components/aemet/manifest.json b/homeassistant/components/aemet/manifest.json
new file mode 100644
index 0000000000000000000000000000000000000000..eb5dc295f297ed957dddce4797e3a2d10b1f7e33
--- /dev/null
+++ b/homeassistant/components/aemet/manifest.json
@@ -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"]
+}
diff --git a/homeassistant/components/aemet/sensor.py b/homeassistant/components/aemet/sensor.py
new file mode 100644
index 0000000000000000000000000000000000000000..b57de1ce89029ed7b4eba91cd42214a22e578303
--- /dev/null
+++ b/homeassistant/components/aemet/sensor.py
@@ -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
diff --git a/homeassistant/components/aemet/strings.json b/homeassistant/components/aemet/strings.json
new file mode 100644
index 0000000000000000000000000000000000000000..a25a503bade5147a462539c2b26e27fcec294f62
--- /dev/null
+++ b/homeassistant/components/aemet/strings.json
@@ -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"
+            }
+        }
+    }
+}
diff --git a/homeassistant/components/aemet/translations/en.json b/homeassistant/components/aemet/translations/en.json
new file mode 100644
index 0000000000000000000000000000000000000000..60e7f5f2ec22cb81505dab4200dbe8a36eac179d
--- /dev/null
+++ b/homeassistant/components/aemet/translations/en.json
@@ -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"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/aemet/weather.py b/homeassistant/components/aemet/weather.py
new file mode 100644
index 0000000000000000000000000000000000000000..e54a297cc091e3a6cb263deccc943fd681c69f1e
--- /dev/null
+++ b/homeassistant/components/aemet/weather.py
@@ -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]
diff --git a/homeassistant/components/aemet/weather_update_coordinator.py b/homeassistant/components/aemet/weather_update_coordinator.py
new file mode 100644
index 0000000000000000000000000000000000000000..6a06b1dd39147a5072796417dadd54694ac0f2d8
--- /dev/null
+++ b/homeassistant/components/aemet/weather_update_coordinator.py
@@ -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)
diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py
index f5f550b3073e38a1f4bc4531c95a6711deba2054..41d85c4b20b93bc3b5365421f286f8474ddbe96c 100644
--- a/homeassistant/generated/config_flows.py
+++ b/homeassistant/generated/config_flows.py
@@ -11,6 +11,7 @@ FLOWS = [
     "acmeda",
     "adguard",
     "advantage_air",
+    "aemet",
     "agent_dvr",
     "airly",
     "airnow",
diff --git a/requirements_all.txt b/requirements_all.txt
index 9ee4663bd17b78689819f38bdd6182c72b0b5dd5..d8ec67e1d30a04c1734d9f64448da1a0f7098ac3 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -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
 
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 737b3e4a989d91b6a3aab9aa32f4c218df871ebc..3a0ce6dd428b16339828bcbbc432f1677356fe75 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -3,6 +3,9 @@
 
 -r requirements_test.txt
 
+# homeassistant.components.aemet
+AEMET-OpenData==0.1.8
+
 # homeassistant.components.homekit
 HAP-python==3.3.0
 
diff --git a/tests/components/aemet/__init__.py b/tests/components/aemet/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..a92ff2764b1b5cd2481f94d7d1783c08eb578ee4
--- /dev/null
+++ b/tests/components/aemet/__init__.py
@@ -0,0 +1 @@
+"""Tests for the AEMET OpenData integration."""
diff --git a/tests/components/aemet/test_config_flow.py b/tests/components/aemet/test_config_flow.py
new file mode 100644
index 0000000000000000000000000000000000000000..3c93a9d63213b6c44880598f1b28426304bba32e
--- /dev/null
+++ b/tests/components/aemet/test_config_flow.py
@@ -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"}
diff --git a/tests/components/aemet/test_init.py b/tests/components/aemet/test_init.py
new file mode 100644
index 0000000000000000000000000000000000000000..f1c6c48f3f3323e229a198f85cc5c3acd30641da
--- /dev/null
+++ b/tests/components/aemet/test_init.py
@@ -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
diff --git a/tests/components/aemet/test_sensor.py b/tests/components/aemet/test_sensor.py
new file mode 100644
index 0000000000000000000000000000000000000000..05f2d8d0b500d0114d22d78f5bbe1fb8df84edb9
--- /dev/null
+++ b/tests/components/aemet/test_sensor.py
@@ -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"
diff --git a/tests/components/aemet/test_weather.py b/tests/components/aemet/test_weather.py
new file mode 100644
index 0000000000000000000000000000000000000000..eef6107d54322cb67c68983cdc25571e151ae5a8
--- /dev/null
+++ b/tests/components/aemet/test_weather.py
@@ -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
diff --git a/tests/components/aemet/util.py b/tests/components/aemet/util.py
new file mode 100644
index 0000000000000000000000000000000000000000..991e7459bf6d95fa9068ed38a5684b0652d5226b
--- /dev/null
+++ b/tests/components/aemet/util.py
@@ -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()
diff --git a/tests/fixtures/aemet/station-3195-data.json b/tests/fixtures/aemet/station-3195-data.json
new file mode 100644
index 0000000000000000000000000000000000000000..1784a5fb3a42e8eff5666c0613add68dcd66a212
--- /dev/null
+++ b/tests/fixtures/aemet/station-3195-data.json
@@ -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
+} ]
diff --git a/tests/fixtures/aemet/station-3195.json b/tests/fixtures/aemet/station-3195.json
new file mode 100644
index 0000000000000000000000000000000000000000..f97df3bea63447a14a921f89a7068a17598ddeaa
--- /dev/null
+++ b/tests/fixtures/aemet/station-3195.json
@@ -0,0 +1,6 @@
+{
+  "descripcion" : "exito",
+  "estado" : 200,
+  "datos" : "https://opendata.aemet.es/opendata/sh/208c3ca3",
+  "metadatos" : "https://opendata.aemet.es/opendata/sh/55c2971b"
+}
diff --git a/tests/fixtures/aemet/station-list-data.json b/tests/fixtures/aemet/station-list-data.json
new file mode 100644
index 0000000000000000000000000000000000000000..8b35bff6e4aefb4bc989c5b4a4122f7032814005
--- /dev/null
+++ b/tests/fixtures/aemet/station-list-data.json
@@ -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�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
+} ]
diff --git a/tests/fixtures/aemet/station-list.json b/tests/fixtures/aemet/station-list.json
new file mode 100644
index 0000000000000000000000000000000000000000..6e0dbc97d6de980116391f6d781fdfba57f6a267
--- /dev/null
+++ b/tests/fixtures/aemet/station-list.json
@@ -0,0 +1,6 @@
+{
+  "descripcion" : "exito",
+  "estado" : 200,
+  "datos" : "https://opendata.aemet.es/opendata/sh/2c55192f",
+  "metadatos" : "https://opendata.aemet.es/opendata/sh/55c2971b"
+}
diff --git a/tests/fixtures/aemet/town-28065-forecast-daily-data.json b/tests/fixtures/aemet/town-28065-forecast-daily-data.json
new file mode 100644
index 0000000000000000000000000000000000000000..77877c72f3a2e5329afe130e58d6547f837c134d
--- /dev/null
+++ b/tests/fixtures/aemet/town-28065-forecast-daily-data.json
@@ -0,0 +1,625 @@
+[ {
+  "origen" : {
+    "productor" : "Agencia Estatal de Meteorolog�a - AEMET. Gobierno de Espa�a",
+    "web" : "http://www.aemet.es",
+    "enlace" : "http://www.aemet.es/es/eltiempo/prediccion/municipios/getafe-id28065",
+    "language" : "es",
+    "copyright" : "� AEMET. Autorizado el uso de la informaci�n y su reproducci�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
+} ]
diff --git a/tests/fixtures/aemet/town-28065-forecast-daily.json b/tests/fixtures/aemet/town-28065-forecast-daily.json
new file mode 100644
index 0000000000000000000000000000000000000000..35935658c506db88889843aed75db6ac56563069
--- /dev/null
+++ b/tests/fixtures/aemet/town-28065-forecast-daily.json
@@ -0,0 +1,6 @@
+{
+  "descripcion" : "exito",
+  "estado" : 200,
+  "datos" : "https://opendata.aemet.es/opendata/sh/64e29abb",
+  "metadatos" : "https://opendata.aemet.es/opendata/sh/dfd88b22"
+}
diff --git a/tests/fixtures/aemet/town-28065-forecast-hourly-data.json b/tests/fixtures/aemet/town-28065-forecast-hourly-data.json
new file mode 100644
index 0000000000000000000000000000000000000000..2bd3a22235a1f27e3f9755b4e700ee240d270aea
--- /dev/null
+++ b/tests/fixtures/aemet/town-28065-forecast-hourly-data.json
@@ -0,0 +1,1416 @@
+[ {
+  "origen" : {
+    "productor" : "Agencia Estatal de Meteorolog�a - AEMET. Gobierno de Espa�a",
+    "web" : "http://www.aemet.es",
+    "enlace" : "http://www.aemet.es/es/eltiempo/prediccion/municipios/horas/getafe-id28065",
+    "language" : "es",
+    "copyright" : "� AEMET. Autorizado el uso de la informaci�n y su reproducci�n citando a AEMET como autora de la misma.",
+    "notaLegal" : "http://www.aemet.es/es/nota_legal"
+  },
+  "elaborado" : "2021-01-09T11:47:45",
+  "nombre" : "Getafe",
+  "provincia" : "Madrid",
+  "prediccion" : {
+    "dia" : [ {
+      "estadoCielo" : [ {
+        "value" : "36n",
+        "periodo" : "07",
+        "descripcion" : "Cubierto con nieve"
+      }, {
+        "value" : "36n",
+        "periodo" : "08",
+        "descripcion" : "Cubierto con nieve"
+      }, {
+        "value" : "36",
+        "periodo" : "09",
+        "descripcion" : "Cubierto con nieve"
+      }, {
+        "value" : "36",
+        "periodo" : "10",
+        "descripcion" : "Cubierto con nieve"
+      }, {
+        "value" : "36",
+        "periodo" : "11",
+        "descripcion" : "Cubierto con nieve"
+      }, {
+        "value" : "36",
+        "periodo" : "12",
+        "descripcion" : "Cubierto con nieve"
+      }, {
+        "value" : "36",
+        "periodo" : "13",
+        "descripcion" : "Cubierto con nieve"
+      }, {
+        "value" : "46",
+        "periodo" : "14",
+        "descripcion" : "Cubierto con lluvia escasa"
+      }, {
+        "value" : "46",
+        "periodo" : "15",
+        "descripcion" : "Cubierto con lluvia escasa"
+      }, {
+        "value" : "36",
+        "periodo" : "16",
+        "descripcion" : "Cubierto con nieve"
+      }, {
+        "value" : "36",
+        "periodo" : "17",
+        "descripcion" : "Cubierto con nieve"
+      }, {
+        "value" : "74n",
+        "periodo" : "18",
+        "descripcion" : "Cubierto con nieve escasa"
+      }, {
+        "value" : "46n",
+        "periodo" : "19",
+        "descripcion" : "Cubierto con lluvia escasa"
+      }, {
+        "value" : "46n",
+        "periodo" : "20",
+        "descripcion" : "Cubierto con lluvia escasa"
+      }, {
+        "value" : "16n",
+        "periodo" : "21",
+        "descripcion" : "Cubierto"
+      }, {
+        "value" : "16n",
+        "periodo" : "22",
+        "descripcion" : "Cubierto"
+      }, {
+        "value" : "12n",
+        "periodo" : "23",
+        "descripcion" : "Poco nuboso"
+      } ],
+      "precipitacion" : [ {
+        "value" : "1.4",
+        "periodo" : "07"
+      }, {
+        "value" : "2.1",
+        "periodo" : "08"
+      }, {
+        "value" : "1.9",
+        "periodo" : "09"
+      }, {
+        "value" : "2",
+        "periodo" : "10"
+      }, {
+        "value" : "1.9",
+        "periodo" : "11"
+      }, {
+        "value" : "1.8",
+        "periodo" : "12"
+      }, {
+        "value" : "1.5",
+        "periodo" : "13"
+      }, {
+        "value" : "0.5",
+        "periodo" : "14"
+      }, {
+        "value" : "0.6",
+        "periodo" : "15"
+      }, {
+        "value" : "0.8",
+        "periodo" : "16"
+      }, {
+        "value" : "0.6",
+        "periodo" : "17"
+      }, {
+        "value" : "0.2",
+        "periodo" : "18"
+      }, {
+        "value" : "0.2",
+        "periodo" : "19"
+      }, {
+        "value" : "0.1",
+        "periodo" : "20"
+      }, {
+        "value" : "0",
+        "periodo" : "21"
+      }, {
+        "value" : "0",
+        "periodo" : "22"
+      }, {
+        "value" : "0",
+        "periodo" : "23"
+      } ],
+      "probPrecipitacion" : [ {
+        "value" : "",
+        "periodo" : "0107"
+      }, {
+        "value" : "100",
+        "periodo" : "0713"
+      }, {
+        "value" : "100",
+        "periodo" : "1319"
+      }, {
+        "value" : "100",
+        "periodo" : "1901"
+      } ],
+      "probTormenta" : [ {
+        "value" : "",
+        "periodo" : "0107"
+      }, {
+        "value" : "0",
+        "periodo" : "0713"
+      }, {
+        "value" : "0",
+        "periodo" : "1319"
+      }, {
+        "value" : "0",
+        "periodo" : "1901"
+      } ],
+      "nieve" : [ {
+        "value" : "1.4",
+        "periodo" : "07"
+      }, {
+        "value" : "2.1",
+        "periodo" : "08"
+      }, {
+        "value" : "1.9",
+        "periodo" : "09"
+      }, {
+        "value" : "2",
+        "periodo" : "10"
+      }, {
+        "value" : "1.9",
+        "periodo" : "11"
+      }, {
+        "value" : "1.8",
+        "periodo" : "12"
+      }, {
+        "value" : "1.2",
+        "periodo" : "13"
+      }, {
+        "value" : "0.1",
+        "periodo" : "14"
+      }, {
+        "value" : "0.2",
+        "periodo" : "15"
+      }, {
+        "value" : "0.6",
+        "periodo" : "16"
+      }, {
+        "value" : "0.6",
+        "periodo" : "17"
+      }, {
+        "value" : "0.2",
+        "periodo" : "18"
+      }, {
+        "value" : "0.1",
+        "periodo" : "19"
+      }, {
+        "value" : "0",
+        "periodo" : "20"
+      }, {
+        "value" : "0",
+        "periodo" : "21"
+      }, {
+        "value" : "0",
+        "periodo" : "22"
+      }, {
+        "value" : "0",
+        "periodo" : "23"
+      } ],
+      "probNieve" : [ {
+        "value" : "",
+        "periodo" : "0107"
+      }, {
+        "value" : "100",
+        "periodo" : "0713"
+      }, {
+        "value" : "100",
+        "periodo" : "1319"
+      }, {
+        "value" : "80",
+        "periodo" : "1901"
+      } ],
+      "temperatura" : [ {
+        "value" : "-1",
+        "periodo" : "07"
+      }, {
+        "value" : "-1",
+        "periodo" : "08"
+      }, {
+        "value" : "-1",
+        "periodo" : "09"
+      }, {
+        "value" : "-1",
+        "periodo" : "10"
+      }, {
+        "value" : "-1",
+        "periodo" : "11"
+      }, {
+        "value" : "-0",
+        "periodo" : "12"
+      }, {
+        "value" : "-0",
+        "periodo" : "13"
+      }, {
+        "value" : "0",
+        "periodo" : "14"
+      }, {
+        "value" : "1",
+        "periodo" : "15"
+      }, {
+        "value" : "1",
+        "periodo" : "16"
+      }, {
+        "value" : "1",
+        "periodo" : "17"
+      }, {
+        "value" : "1",
+        "periodo" : "18"
+      }, {
+        "value" : "1",
+        "periodo" : "19"
+      }, {
+        "value" : "1",
+        "periodo" : "20"
+      }, {
+        "value" : "1",
+        "periodo" : "21"
+      }, {
+        "value" : "1",
+        "periodo" : "22"
+      }, {
+        "value" : "1",
+        "periodo" : "23"
+      } ],
+      "sensTermica" : [ {
+        "value" : "-8",
+        "periodo" : "07"
+      }, {
+        "value" : "-7",
+        "periodo" : "08"
+      }, {
+        "value" : "-7",
+        "periodo" : "09"
+      }, {
+        "value" : "-6",
+        "periodo" : "10"
+      }, {
+        "value" : "-6",
+        "periodo" : "11"
+      }, {
+        "value" : "-4",
+        "periodo" : "12"
+      }, {
+        "value" : "-4",
+        "periodo" : "13"
+      }, {
+        "value" : "-4",
+        "periodo" : "14"
+      }, {
+        "value" : "-2",
+        "periodo" : "15"
+      }, {
+        "value" : "-2",
+        "periodo" : "16"
+      }, {
+        "value" : "-2",
+        "periodo" : "17"
+      }, {
+        "value" : "1",
+        "periodo" : "18"
+      }, {
+        "value" : "-2",
+        "periodo" : "19"
+      }, {
+        "value" : "1",
+        "periodo" : "20"
+      }, {
+        "value" : "1",
+        "periodo" : "21"
+      }, {
+        "value" : "1",
+        "periodo" : "22"
+      }, {
+        "value" : "-2",
+        "periodo" : "23"
+      } ],
+      "humedadRelativa" : [ {
+        "value" : "96",
+        "periodo" : "07"
+      }, {
+        "value" : "96",
+        "periodo" : "08"
+      }, {
+        "value" : "99",
+        "periodo" : "09"
+      }, {
+        "value" : "100",
+        "periodo" : "10"
+      }, {
+        "value" : "100",
+        "periodo" : "11"
+      }, {
+        "value" : "100",
+        "periodo" : "12"
+      }, {
+        "value" : "100",
+        "periodo" : "13"
+      }, {
+        "value" : "100",
+        "periodo" : "14"
+      }, {
+        "value" : "100",
+        "periodo" : "15"
+      }, {
+        "value" : "97",
+        "periodo" : "16"
+      }, {
+        "value" : "94",
+        "periodo" : "17"
+      }, {
+        "value" : "93",
+        "periodo" : "18"
+      }, {
+        "value" : "93",
+        "periodo" : "19"
+      }, {
+        "value" : "92",
+        "periodo" : "20"
+      }, {
+        "value" : "89",
+        "periodo" : "21"
+      }, {
+        "value" : "89",
+        "periodo" : "22"
+      }, {
+        "value" : "85",
+        "periodo" : "23"
+      } ],
+      "vientoAndRachaMax" : [ {
+        "direccion" : [ "NE" ],
+        "velocidad" : [ "28" ],
+        "periodo" : "07"
+      }, {
+        "value" : "41",
+        "periodo" : "07"
+      }, {
+        "direccion" : [ "NE" ],
+        "velocidad" : [ "27" ],
+        "periodo" : "08"
+      }, {
+        "value" : "41",
+        "periodo" : "08"
+      }, {
+        "direccion" : [ "NE" ],
+        "velocidad" : [ "25" ],
+        "periodo" : "09"
+      }, {
+        "value" : "39",
+        "periodo" : "09"
+      }, {
+        "direccion" : [ "NE" ],
+        "velocidad" : [ "20" ],
+        "periodo" : "10"
+      }, {
+        "value" : "36",
+        "periodo" : "10"
+      }, {
+        "direccion" : [ "NE" ],
+        "velocidad" : [ "17" ],
+        "periodo" : "11"
+      }, {
+        "value" : "29",
+        "periodo" : "11"
+      }, {
+        "direccion" : [ "E" ],
+        "velocidad" : [ "15" ],
+        "periodo" : "12"
+      }, {
+        "value" : "24",
+        "periodo" : "12"
+      }, {
+        "direccion" : [ "SE" ],
+        "velocidad" : [ "15" ],
+        "periodo" : "13"
+      }, {
+        "value" : "22",
+        "periodo" : "13"
+      }, {
+        "direccion" : [ "SE" ],
+        "velocidad" : [ "14" ],
+        "periodo" : "14"
+      }, {
+        "value" : "24",
+        "periodo" : "14"
+      }, {
+        "direccion" : [ "SE" ],
+        "velocidad" : [ "10" ],
+        "periodo" : "15"
+      }, {
+        "value" : "20",
+        "periodo" : "15"
+      }, {
+        "direccion" : [ "SE" ],
+        "velocidad" : [ "8" ],
+        "periodo" : "16"
+      }, {
+        "value" : "14",
+        "periodo" : "16"
+      }, {
+        "direccion" : [ "SE" ],
+        "velocidad" : [ "9" ],
+        "periodo" : "17"
+      }, {
+        "value" : "13",
+        "periodo" : "17"
+      }, {
+        "direccion" : [ "E" ],
+        "velocidad" : [ "7" ],
+        "periodo" : "18"
+      }, {
+        "value" : "13",
+        "periodo" : "18"
+      }, {
+        "direccion" : [ "SE" ],
+        "velocidad" : [ "8" ],
+        "periodo" : "19"
+      }, {
+        "value" : "12",
+        "periodo" : "19"
+      }, {
+        "direccion" : [ "SE" ],
+        "velocidad" : [ "6" ],
+        "periodo" : "20"
+      }, {
+        "value" : "12",
+        "periodo" : "20"
+      }, {
+        "direccion" : [ "E" ],
+        "velocidad" : [ "6" ],
+        "periodo" : "21"
+      }, {
+        "value" : "8",
+        "periodo" : "21"
+      }, {
+        "direccion" : [ "NE" ],
+        "velocidad" : [ "6" ],
+        "periodo" : "22"
+      }, {
+        "value" : "9",
+        "periodo" : "22"
+      }, {
+        "direccion" : [ "E" ],
+        "velocidad" : [ "8" ],
+        "periodo" : "23"
+      }, {
+        "value" : "11",
+        "periodo" : "23"
+      } ],
+      "fecha" : "2021-01-09T00:00:00",
+      "orto" : "08:37",
+      "ocaso" : "18:07"
+    }, {
+      "estadoCielo" : [ {
+        "value" : "12n",
+        "periodo" : "00",
+        "descripcion" : "Poco nuboso"
+      }, {
+        "value" : "81n",
+        "periodo" : "01",
+        "descripcion" : "Niebla"
+      }, {
+        "value" : "81n",
+        "periodo" : "02",
+        "descripcion" : "Niebla"
+      }, {
+        "value" : "81n",
+        "periodo" : "03",
+        "descripcion" : "Niebla"
+      }, {
+        "value" : "17n",
+        "periodo" : "04",
+        "descripcion" : "Nubes altas"
+      }, {
+        "value" : "16n",
+        "periodo" : "05",
+        "descripcion" : "Cubierto"
+      }, {
+        "value" : "16n",
+        "periodo" : "06",
+        "descripcion" : "Cubierto"
+      }, {
+        "value" : "16n",
+        "periodo" : "07",
+        "descripcion" : "Cubierto"
+      }, {
+        "value" : "16n",
+        "periodo" : "08",
+        "descripcion" : "Cubierto"
+      }, {
+        "value" : "14",
+        "periodo" : "09",
+        "descripcion" : "Nuboso"
+      }, {
+        "value" : "12",
+        "periodo" : "10",
+        "descripcion" : "Poco nuboso"
+      }, {
+        "value" : "12",
+        "periodo" : "11",
+        "descripcion" : "Poco nuboso"
+      }, {
+        "value" : "17",
+        "periodo" : "12",
+        "descripcion" : "Nubes altas"
+      }, {
+        "value" : "17",
+        "periodo" : "13",
+        "descripcion" : "Nubes altas"
+      }, {
+        "value" : "17",
+        "periodo" : "14",
+        "descripcion" : "Nubes altas"
+      }, {
+        "value" : "17",
+        "periodo" : "15",
+        "descripcion" : "Nubes altas"
+      }, {
+        "value" : "17",
+        "periodo" : "16",
+        "descripcion" : "Nubes altas"
+      }, {
+        "value" : "17",
+        "periodo" : "17",
+        "descripcion" : "Nubes altas"
+      }, {
+        "value" : "12n",
+        "periodo" : "18",
+        "descripcion" : "Poco nuboso"
+      }, {
+        "value" : "12n",
+        "periodo" : "19",
+        "descripcion" : "Poco nuboso"
+      }, {
+        "value" : "14n",
+        "periodo" : "20",
+        "descripcion" : "Nuboso"
+      }, {
+        "value" : "16n",
+        "periodo" : "21",
+        "descripcion" : "Cubierto"
+      }, {
+        "value" : "16n",
+        "periodo" : "22",
+        "descripcion" : "Cubierto"
+      }, {
+        "value" : "15n",
+        "periodo" : "23",
+        "descripcion" : "Muy nuboso"
+      } ],
+      "precipitacion" : [ {
+        "value" : "0",
+        "periodo" : "00"
+      }, {
+        "value" : "0",
+        "periodo" : "01"
+      }, {
+        "value" : "0",
+        "periodo" : "02"
+      }, {
+        "value" : "0",
+        "periodo" : "03"
+      }, {
+        "value" : "0",
+        "periodo" : "04"
+      }, {
+        "value" : "0",
+        "periodo" : "05"
+      }, {
+        "value" : "0",
+        "periodo" : "06"
+      }, {
+        "value" : "0",
+        "periodo" : "07"
+      }, {
+        "value" : "0",
+        "periodo" : "08"
+      }, {
+        "value" : "Ip",
+        "periodo" : "09"
+      }, {
+        "value" : "0",
+        "periodo" : "10"
+      }, {
+        "value" : "0",
+        "periodo" : "11"
+      }, {
+        "value" : "0",
+        "periodo" : "12"
+      }, {
+        "value" : "0",
+        "periodo" : "13"
+      }, {
+        "value" : "0",
+        "periodo" : "14"
+      }, {
+        "value" : "0",
+        "periodo" : "15"
+      }, {
+        "value" : "0",
+        "periodo" : "16"
+      }, {
+        "value" : "0",
+        "periodo" : "17"
+      }, {
+        "value" : "0",
+        "periodo" : "18"
+      }, {
+        "value" : "0",
+        "periodo" : "19"
+      }, {
+        "value" : "0",
+        "periodo" : "20"
+      }, {
+        "value" : "0",
+        "periodo" : "21"
+      }, {
+        "value" : "0",
+        "periodo" : "22"
+      }, {
+        "value" : "0",
+        "periodo" : "23"
+      } ],
+      "probPrecipitacion" : [ {
+        "value" : "10",
+        "periodo" : "0107"
+      }, {
+        "value" : "15",
+        "periodo" : "0713"
+      }, {
+        "value" : "5",
+        "periodo" : "1319"
+      }, {
+        "value" : "0",
+        "periodo" : "1901"
+      } ],
+      "probTormenta" : [ {
+        "value" : "0",
+        "periodo" : "0107"
+      }, {
+        "value" : "0",
+        "periodo" : "0713"
+      }, {
+        "value" : "0",
+        "periodo" : "1319"
+      }, {
+        "value" : "0",
+        "periodo" : "1901"
+      } ],
+      "nieve" : [ {
+        "value" : "0",
+        "periodo" : "00"
+      }, {
+        "value" : "0",
+        "periodo" : "01"
+      }, {
+        "value" : "0",
+        "periodo" : "02"
+      }, {
+        "value" : "0",
+        "periodo" : "03"
+      }, {
+        "value" : "0",
+        "periodo" : "04"
+      }, {
+        "value" : "0",
+        "periodo" : "05"
+      }, {
+        "value" : "0",
+        "periodo" : "06"
+      }, {
+        "value" : "0",
+        "periodo" : "07"
+      }, {
+        "value" : "0",
+        "periodo" : "08"
+      }, {
+        "value" : "Ip",
+        "periodo" : "09"
+      }, {
+        "value" : "0",
+        "periodo" : "10"
+      }, {
+        "value" : "0",
+        "periodo" : "11"
+      }, {
+        "value" : "0",
+        "periodo" : "12"
+      }, {
+        "value" : "0",
+        "periodo" : "13"
+      }, {
+        "value" : "0",
+        "periodo" : "14"
+      }, {
+        "value" : "0",
+        "periodo" : "15"
+      }, {
+        "value" : "0",
+        "periodo" : "16"
+      }, {
+        "value" : "0",
+        "periodo" : "17"
+      }, {
+        "value" : "0",
+        "periodo" : "18"
+      }, {
+        "value" : "0",
+        "periodo" : "19"
+      }, {
+        "value" : "0",
+        "periodo" : "20"
+      }, {
+        "value" : "0",
+        "periodo" : "21"
+      }, {
+        "value" : "0",
+        "periodo" : "22"
+      }, {
+        "value" : "0",
+        "periodo" : "23"
+      } ],
+      "probNieve" : [ {
+        "value" : "10",
+        "periodo" : "0107"
+      }, {
+        "value" : "10",
+        "periodo" : "0713"
+      }, {
+        "value" : "0",
+        "periodo" : "1319"
+      }, {
+        "value" : "0",
+        "periodo" : "1901"
+      } ],
+      "temperatura" : [ {
+        "value" : "1",
+        "periodo" : "00"
+      }, {
+        "value" : "0",
+        "periodo" : "01"
+      }, {
+        "value" : "-0",
+        "periodo" : "02"
+      }, {
+        "value" : "-0",
+        "periodo" : "03"
+      }, {
+        "value" : "-1",
+        "periodo" : "04"
+      }, {
+        "value" : "-1",
+        "periodo" : "05"
+      }, {
+        "value" : "-1",
+        "periodo" : "06"
+      }, {
+        "value" : "-2",
+        "periodo" : "07"
+      }, {
+        "value" : "-1",
+        "periodo" : "08"
+      }, {
+        "value" : "-1",
+        "periodo" : "09"
+      }, {
+        "value" : "0",
+        "periodo" : "10"
+      }, {
+        "value" : "2",
+        "periodo" : "11"
+      }, {
+        "value" : "3",
+        "periodo" : "12"
+      }, {
+        "value" : "3",
+        "periodo" : "13"
+      }, {
+        "value" : "3",
+        "periodo" : "14"
+      }, {
+        "value" : "4",
+        "periodo" : "15"
+      }, {
+        "value" : "3",
+        "periodo" : "16"
+      }, {
+        "value" : "2",
+        "periodo" : "17"
+      }, {
+        "value" : "1",
+        "periodo" : "18"
+      }, {
+        "value" : "1",
+        "periodo" : "19"
+      }, {
+        "value" : "1",
+        "periodo" : "20"
+      }, {
+        "value" : "1",
+        "periodo" : "21"
+      }, {
+        "value" : "0",
+        "periodo" : "22"
+      }, {
+        "value" : "-0",
+        "periodo" : "23"
+      } ],
+      "sensTermica" : [ {
+        "value" : "1",
+        "periodo" : "00"
+      }, {
+        "value" : "0",
+        "periodo" : "01"
+      }, {
+        "value" : "-0",
+        "periodo" : "02"
+      }, {
+        "value" : "-0",
+        "periodo" : "03"
+      }, {
+        "value" : "-4",
+        "periodo" : "04"
+      }, {
+        "value" : "-1",
+        "periodo" : "05"
+      }, {
+        "value" : "-4",
+        "periodo" : "06"
+      }, {
+        "value" : "-6",
+        "periodo" : "07"
+      }, {
+        "value" : "-6",
+        "periodo" : "08"
+      }, {
+        "value" : "-7",
+        "periodo" : "09"
+      }, {
+        "value" : "-5",
+        "periodo" : "10"
+      }, {
+        "value" : "-3",
+        "periodo" : "11"
+      }, {
+        "value" : "-2",
+        "periodo" : "12"
+      }, {
+        "value" : "-1",
+        "periodo" : "13"
+      }, {
+        "value" : "-1",
+        "periodo" : "14"
+      }, {
+        "value" : "0",
+        "periodo" : "15"
+      }, {
+        "value" : "-1",
+        "periodo" : "16"
+      }, {
+        "value" : "-2",
+        "periodo" : "17"
+      }, {
+        "value" : "-4",
+        "periodo" : "18"
+      }, {
+        "value" : "-4",
+        "periodo" : "19"
+      }, {
+        "value" : "-3",
+        "periodo" : "20"
+      }, {
+        "value" : "-4",
+        "periodo" : "21"
+      }, {
+        "value" : "-5",
+        "periodo" : "22"
+      }, {
+        "value" : "-5",
+        "periodo" : "23"
+      } ],
+      "humedadRelativa" : [ {
+        "value" : "74",
+        "periodo" : "00"
+      }, {
+        "value" : "71",
+        "periodo" : "01"
+      }, {
+        "value" : "80",
+        "periodo" : "02"
+      }, {
+        "value" : "84",
+        "periodo" : "03"
+      }, {
+        "value" : "81",
+        "periodo" : "04"
+      }, {
+        "value" : "78",
+        "periodo" : "05"
+      }, {
+        "value" : "90",
+        "periodo" : "06"
+      }, {
+        "value" : "100",
+        "periodo" : "07"
+      }, {
+        "value" : "100",
+        "periodo" : "08"
+      }, {
+        "value" : "93",
+        "periodo" : "09"
+      }, {
+        "value" : "84",
+        "periodo" : "10"
+      }, {
+        "value" : "78",
+        "periodo" : "11"
+      }, {
+        "value" : "73",
+        "periodo" : "12"
+      }, {
+        "value" : "74",
+        "periodo" : "13"
+      }, {
+        "value" : "74",
+        "periodo" : "14"
+      }, {
+        "value" : "73",
+        "periodo" : "15"
+      }, {
+        "value" : "78",
+        "periodo" : "16"
+      }, {
+        "value" : "79",
+        "periodo" : "17"
+      }, {
+        "value" : "79",
+        "periodo" : "18"
+      }, {
+        "value" : "77",
+        "periodo" : "19"
+      }, {
+        "value" : "75",
+        "periodo" : "20"
+      }, {
+        "value" : "77",
+        "periodo" : "21"
+      }, {
+        "value" : "80",
+        "periodo" : "22"
+      }, {
+        "value" : "80",
+        "periodo" : "23"
+      } ],
+      "vientoAndRachaMax" : [ {
+        "direccion" : [ "NE" ],
+        "velocidad" : [ "6" ],
+        "periodo" : "00"
+      }, {
+        "value" : "12",
+        "periodo" : "00"
+      }, {
+        "direccion" : [ "NE" ],
+        "velocidad" : [ "5" ],
+        "periodo" : "01"
+      }, {
+        "value" : "10",
+        "periodo" : "01"
+      }, {
+        "direccion" : [ "N" ],
+        "velocidad" : [ "6" ],
+        "periodo" : "02"
+      }, {
+        "value" : "11",
+        "periodo" : "02"
+      }, {
+        "direccion" : [ "N" ],
+        "velocidad" : [ "6" ],
+        "periodo" : "03"
+      }, {
+        "value" : "9",
+        "periodo" : "03"
+      }, {
+        "direccion" : [ "NE" ],
+        "velocidad" : [ "8" ],
+        "periodo" : "04"
+      }, {
+        "value" : "12",
+        "periodo" : "04"
+      }, {
+        "direccion" : [ "N" ],
+        "velocidad" : [ "5" ],
+        "periodo" : "05"
+      }, {
+        "value" : "11",
+        "periodo" : "05"
+      }, {
+        "direccion" : [ "N" ],
+        "velocidad" : [ "9" ],
+        "periodo" : "06"
+      }, {
+        "value" : "13",
+        "periodo" : "06"
+      }, {
+        "direccion" : [ "NE" ],
+        "velocidad" : [ "13" ],
+        "periodo" : "07"
+      }, {
+        "value" : "18",
+        "periodo" : "07"
+      }, {
+        "direccion" : [ "NE" ],
+        "velocidad" : [ "17" ],
+        "periodo" : "08"
+      }, {
+        "value" : "25",
+        "periodo" : "08"
+      }, {
+        "direccion" : [ "NE" ],
+        "velocidad" : [ "21" ],
+        "periodo" : "09"
+      }, {
+        "value" : "31",
+        "periodo" : "09"
+      }, {
+        "direccion" : [ "NE" ],
+        "velocidad" : [ "21" ],
+        "periodo" : "10"
+      }, {
+        "value" : "32",
+        "periodo" : "10"
+      }, {
+        "direccion" : [ "NE" ],
+        "velocidad" : [ "21" ],
+        "periodo" : "11"
+      }, {
+        "value" : "30",
+        "periodo" : "11"
+      }, {
+        "direccion" : [ "NE" ],
+        "velocidad" : [ "22" ],
+        "periodo" : "12"
+      }, {
+        "value" : "32",
+        "periodo" : "12"
+      }, {
+        "direccion" : [ "NE" ],
+        "velocidad" : [ "20" ],
+        "periodo" : "13"
+      }, {
+        "value" : "32",
+        "periodo" : "13"
+      }, {
+        "direccion" : [ "NE" ],
+        "velocidad" : [ "19" ],
+        "periodo" : "14"
+      }, {
+        "value" : "30",
+        "periodo" : "14"
+      }, {
+        "direccion" : [ "NE" ],
+        "velocidad" : [ "17" ],
+        "periodo" : "15"
+      }, {
+        "value" : "28",
+        "periodo" : "15"
+      }, {
+        "direccion" : [ "NE" ],
+        "velocidad" : [ "16" ],
+        "periodo" : "16"
+      }, {
+        "value" : "25",
+        "periodo" : "16"
+      }, {
+        "direccion" : [ "NE" ],
+        "velocidad" : [ "16" ],
+        "periodo" : "17"
+      }, {
+        "value" : "24",
+        "periodo" : "17"
+      }, {
+        "direccion" : [ "NE" ],
+        "velocidad" : [ "17" ],
+        "periodo" : "18"
+      }, {
+        "value" : "24",
+        "periodo" : "18"
+      }, {
+        "direccion" : [ "NE" ],
+        "velocidad" : [ "17" ],
+        "periodo" : "19"
+      }, {
+        "value" : "25",
+        "periodo" : "19"
+      }, {
+        "direccion" : [ "NE" ],
+        "velocidad" : [ "16" ],
+        "periodo" : "20"
+      }, {
+        "value" : "25",
+        "periodo" : "20"
+      }, {
+        "direccion" : [ "NE" ],
+        "velocidad" : [ "17" ],
+        "periodo" : "21"
+      }, {
+        "value" : "24",
+        "periodo" : "21"
+      }, {
+        "direccion" : [ "NE" ],
+        "velocidad" : [ "19" ],
+        "periodo" : "22"
+      }, {
+        "value" : "27",
+        "periodo" : "22"
+      }, {
+        "direccion" : [ "NE" ],
+        "velocidad" : [ "21" ],
+        "periodo" : "23"
+      }, {
+        "value" : "30",
+        "periodo" : "23"
+      } ],
+      "fecha" : "2021-01-10T00:00:00",
+      "orto" : "08:36",
+      "ocaso" : "18:08"
+    }, {
+      "estadoCielo" : [ {
+        "value" : "14n",
+        "periodo" : "00",
+        "descripcion" : "Nuboso"
+      }, {
+        "value" : "12n",
+        "periodo" : "01",
+        "descripcion" : "Poco nuboso"
+      }, {
+        "value" : "11n",
+        "periodo" : "02",
+        "descripcion" : "Despejado"
+      }, {
+        "value" : "11n",
+        "periodo" : "03",
+        "descripcion" : "Despejado"
+      }, {
+        "value" : "11n",
+        "periodo" : "04",
+        "descripcion" : "Despejado"
+      }, {
+        "value" : "11n",
+        "periodo" : "05",
+        "descripcion" : "Despejado"
+      }, {
+        "value" : "11n",
+        "periodo" : "06",
+        "descripcion" : "Despejado"
+      } ],
+      "precipitacion" : [ {
+        "value" : "0",
+        "periodo" : "00"
+      }, {
+        "value" : "0",
+        "periodo" : "01"
+      }, {
+        "value" : "0",
+        "periodo" : "02"
+      }, {
+        "value" : "0",
+        "periodo" : "03"
+      }, {
+        "value" : "0",
+        "periodo" : "04"
+      }, {
+        "value" : "0",
+        "periodo" : "05"
+      }, {
+        "value" : "0",
+        "periodo" : "06"
+      } ],
+      "probPrecipitacion" : [ {
+        "value" : "0",
+        "periodo" : "0107"
+      }, {
+        "value" : "",
+        "periodo" : "0713"
+      }, {
+        "value" : "",
+        "periodo" : "1319"
+      }, {
+        "value" : "",
+        "periodo" : "1901"
+      } ],
+      "probTormenta" : [ {
+        "value" : "0",
+        "periodo" : "0107"
+      }, {
+        "value" : "",
+        "periodo" : "0713"
+      }, {
+        "value" : "",
+        "periodo" : "1319"
+      }, {
+        "value" : "",
+        "periodo" : "1901"
+      } ],
+      "nieve" : [ {
+        "value" : "0",
+        "periodo" : "00"
+      }, {
+        "value" : "0",
+        "periodo" : "01"
+      }, {
+        "value" : "0",
+        "periodo" : "02"
+      }, {
+        "value" : "0",
+        "periodo" : "03"
+      }, {
+        "value" : "0",
+        "periodo" : "04"
+      }, {
+        "value" : "0",
+        "periodo" : "05"
+      }, {
+        "value" : "0",
+        "periodo" : "06"
+      } ],
+      "probNieve" : [ {
+        "value" : "0",
+        "periodo" : "0107"
+      }, {
+        "value" : "",
+        "periodo" : "0713"
+      }, {
+        "value" : "",
+        "periodo" : "1319"
+      }, {
+        "value" : "",
+        "periodo" : "1901"
+      } ],
+      "temperatura" : [ {
+        "value" : "-1",
+        "periodo" : "00"
+      }, {
+        "value" : "-1",
+        "periodo" : "01"
+      }, {
+        "value" : "-2",
+        "periodo" : "02"
+      }, {
+        "value" : "-2",
+        "periodo" : "03"
+      }, {
+        "value" : "-3",
+        "periodo" : "04"
+      }, {
+        "value" : "-4",
+        "periodo" : "05"
+      }, {
+        "value" : "-4",
+        "periodo" : "06"
+      } ],
+      "sensTermica" : [ {
+        "value" : "-6",
+        "periodo" : "00"
+      }, {
+        "value" : "-6",
+        "periodo" : "01"
+      }, {
+        "value" : "-6",
+        "periodo" : "02"
+      }, {
+        "value" : "-6",
+        "periodo" : "03"
+      }, {
+        "value" : "-7",
+        "periodo" : "04"
+      }, {
+        "value" : "-8",
+        "periodo" : "05"
+      }, {
+        "value" : "-8",
+        "periodo" : "06"
+      } ],
+      "humedadRelativa" : [ {
+        "value" : "81",
+        "periodo" : "00"
+      }, {
+        "value" : "79",
+        "periodo" : "01"
+      }, {
+        "value" : "77",
+        "periodo" : "02"
+      }, {
+        "value" : "76",
+        "periodo" : "03"
+      }, {
+        "value" : "76",
+        "periodo" : "04"
+      }, {
+        "value" : "76",
+        "periodo" : "05"
+      }, {
+        "value" : "78",
+        "periodo" : "06"
+      } ],
+      "vientoAndRachaMax" : [ {
+        "direccion" : [ "NE" ],
+        "velocidad" : [ "19" ],
+        "periodo" : "00"
+      }, {
+        "value" : "30",
+        "periodo" : "00"
+      }, {
+        "direccion" : [ "NE" ],
+        "velocidad" : [ "16" ],
+        "periodo" : "01"
+      }, {
+        "value" : "27",
+        "periodo" : "01"
+      }, {
+        "direccion" : [ "NE" ],
+        "velocidad" : [ "12" ],
+        "periodo" : "02"
+      }, {
+        "value" : "22",
+        "periodo" : "02"
+      }, {
+        "direccion" : [ "NE" ],
+        "velocidad" : [ "10" ],
+        "periodo" : "03"
+      }, {
+        "value" : "17",
+        "periodo" : "03"
+      }, {
+        "direccion" : [ "NE" ],
+        "velocidad" : [ "11" ],
+        "periodo" : "04"
+      }, {
+        "value" : "15",
+        "periodo" : "04"
+      }, {
+        "direccion" : [ "NE" ],
+        "velocidad" : [ "10" ],
+        "periodo" : "05"
+      }, {
+        "value" : "15",
+        "periodo" : "05"
+      }, {
+        "direccion" : [ "N" ],
+        "velocidad" : [ "10" ],
+        "periodo" : "06"
+      }, {
+        "value" : "15",
+        "periodo" : "06"
+      } ],
+      "fecha" : "2021-01-11T00:00:00",
+      "orto" : "08:36",
+      "ocaso" : "18:09"
+    } ]
+  },
+  "id" : "28065",
+  "version" : "1.0"
+} ]
diff --git a/tests/fixtures/aemet/town-28065-forecast-hourly.json b/tests/fixtures/aemet/town-28065-forecast-hourly.json
new file mode 100644
index 0000000000000000000000000000000000000000..2fbcaaeb33e48a36e75fb3212b59feeaeeff0d37
--- /dev/null
+++ b/tests/fixtures/aemet/town-28065-forecast-hourly.json
@@ -0,0 +1,6 @@
+{
+  "descripcion" : "exito",
+  "estado" : 200,
+  "datos" : "https://opendata.aemet.es/opendata/sh/18ca1886",
+  "metadatos" : "https://opendata.aemet.es/opendata/sh/93a7c63d"
+}
diff --git a/tests/fixtures/aemet/town-id28065.json b/tests/fixtures/aemet/town-id28065.json
new file mode 100644
index 0000000000000000000000000000000000000000..342b163062c5394e936928988f7b619b5c66f01d
--- /dev/null
+++ b/tests/fixtures/aemet/town-id28065.json
@@ -0,0 +1,15 @@
+[ {
+  "latitud" : "40�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\""
+} ]
diff --git a/tests/fixtures/aemet/town-list.json b/tests/fixtures/aemet/town-list.json
new file mode 100644
index 0000000000000000000000000000000000000000..d5ed23ef9350c5a4340d7e27d8a84e6293e38a5d
--- /dev/null
+++ b/tests/fixtures/aemet/town-list.json
@@ -0,0 +1,43 @@
+[ {
+  "latitud" : "40�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�19'54.277752\"",
+  "id_old" : "28370",
+  "url" : "leganes-id28074",
+  "latitud_dec" : "40.33174382",
+  "altitud" : "667",
+  "capital" : "Legan�s",
+  "num_hab" : "186696",
+  "zona_comarcal" : "722802",
+  "destacada" : "1",
+  "nombre" : "Legan�s",
+  "longitud_dec" : "-3.76655557",
+  "id" : "id28074",
+  "longitud" : "-3�45'59.600052\""
+}, {
+  "latitud" : "40�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\""
+} ]