Skip to content
Snippets Groups Projects
Unverified Commit eecf07d7 authored by Álvaro Fernández Rojas's avatar Álvaro Fernández Rojas Committed by GitHub
Browse files

Add AEMET OpenData integration (#45074)

parent 2f40f446
No related merge requests found
Showing
with 1771 additions and 0 deletions
......@@ -23,6 +23,8 @@ omit =
homeassistant/components/adguard/sensor.py
homeassistant/components/adguard/switch.py
homeassistant/components/ads/*
homeassistant/components/aemet/abstract_aemet_sensor.py
homeassistant/components/aemet/weather_update_coordinator.py
homeassistant/components/aftership/sensor.py
homeassistant/components/agent_dvr/__init__.py
homeassistant/components/agent_dvr/alarm_control_panel.py
......
......@@ -24,6 +24,7 @@ homeassistant/components/accuweather/* @bieniu
homeassistant/components/acmeda/* @atmurray
homeassistant/components/adguard/* @frenck
homeassistant/components/advantage_air/* @Bre77
homeassistant/components/aemet/* @noltari
homeassistant/components/agent_dvr/* @ispysoftware
homeassistant/components/airly/* @bieniu
homeassistant/components/airnow/* @asymworks
......
"""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
"""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}
"""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
)
"""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,
}
{
"domain": "aemet",
"name": "AEMET OpenData",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/aemet",
"requirements": ["AEMET-OpenData==0.1.8"],
"codeowners": ["@noltari"]
}
"""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
{
"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"
}
}
}
}
{
"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
"""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]
This diff is collapsed.
......@@ -11,6 +11,7 @@ FLOWS = [
"acmeda",
"adguard",
"advantage_air",
"aemet",
"agent_dvr",
"airly",
"airnow",
......
# Home Assistant Core, full dependency set
-r requirements.txt
# homeassistant.components.aemet
AEMET-OpenData==0.1.8
# homeassistant.components.dht
# Adafruit-DHT==1.4.0
......
......@@ -3,6 +3,9 @@
-r requirements_test.txt
# homeassistant.components.aemet
AEMET-OpenData==0.1.8
# homeassistant.components.homekit
HAP-python==3.3.0
......
"""Tests for the AEMET OpenData integration."""
"""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"}
"""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
"""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"
"""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
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment