mirror of
https://github.com/home-assistant/core.git
synced 2025-07-27 15:17:35 +00:00
Add AEMET conditional station updates (#50227)
This commit is contained in:
parent
42df6750e2
commit
9247a157d8
@ -23,7 +23,6 @@ omit =
|
|||||||
homeassistant/components/adguard/sensor.py
|
homeassistant/components/adguard/sensor.py
|
||||||
homeassistant/components/adguard/switch.py
|
homeassistant/components/adguard/switch.py
|
||||||
homeassistant/components/ads/*
|
homeassistant/components/ads/*
|
||||||
homeassistant/components/aemet/abstract_aemet_sensor.py
|
|
||||||
homeassistant/components/aemet/weather_update_coordinator.py
|
homeassistant/components/aemet/weather_update_coordinator.py
|
||||||
homeassistant/components/aftership/sensor.py
|
homeassistant/components/aftership/sensor.py
|
||||||
homeassistant/components/agent_dvr/__init__.py
|
homeassistant/components/agent_dvr/__init__.py
|
||||||
|
@ -7,7 +7,13 @@ from homeassistant.config_entries import ConfigEntry
|
|||||||
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
|
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
from .const import DOMAIN, ENTRY_NAME, ENTRY_WEATHER_COORDINATOR, PLATFORMS
|
from .const import (
|
||||||
|
CONF_STATION_UPDATES,
|
||||||
|
DOMAIN,
|
||||||
|
ENTRY_NAME,
|
||||||
|
ENTRY_WEATHER_COORDINATOR,
|
||||||
|
PLATFORMS,
|
||||||
|
)
|
||||||
from .weather_update_coordinator import WeatherUpdateCoordinator
|
from .weather_update_coordinator import WeatherUpdateCoordinator
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -19,9 +25,12 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry):
|
|||||||
api_key = config_entry.data[CONF_API_KEY]
|
api_key = config_entry.data[CONF_API_KEY]
|
||||||
latitude = config_entry.data[CONF_LATITUDE]
|
latitude = config_entry.data[CONF_LATITUDE]
|
||||||
longitude = config_entry.data[CONF_LONGITUDE]
|
longitude = config_entry.data[CONF_LONGITUDE]
|
||||||
|
station_updates = config_entry.options.get(CONF_STATION_UPDATES, True)
|
||||||
|
|
||||||
aemet = AEMET(api_key)
|
aemet = AEMET(api_key)
|
||||||
weather_coordinator = WeatherUpdateCoordinator(hass, aemet, latitude, longitude)
|
weather_coordinator = WeatherUpdateCoordinator(
|
||||||
|
hass, aemet, latitude, longitude, station_updates
|
||||||
|
)
|
||||||
|
|
||||||
await weather_coordinator.async_config_entry_first_refresh()
|
await weather_coordinator.async_config_entry_first_refresh()
|
||||||
|
|
||||||
@ -33,9 +42,16 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry):
|
|||||||
|
|
||||||
hass.config_entries.async_setup_platforms(config_entry, PLATFORMS)
|
hass.config_entries.async_setup_platforms(config_entry, PLATFORMS)
|
||||||
|
|
||||||
|
config_entry.async_on_unload(config_entry.add_update_listener(async_update_options))
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_update_options(hass: HomeAssistant, config_entry: ConfigEntry) -> None:
|
||||||
|
"""Update options."""
|
||||||
|
await hass.config_entries.async_reload(config_entry.entry_id)
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry):
|
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry):
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
unload_ok = await hass.config_entries.async_unload_platforms(
|
unload_ok = await hass.config_entries.async_unload_platforms(
|
||||||
|
@ -1,58 +0,0 @@
|
|||||||
"""Abstraction form AEMET OpenData sensors."""
|
|
||||||
from homeassistant.components.sensor import SensorEntity
|
|
||||||
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, SensorEntity):
|
|
||||||
"""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 extra_state_attributes(self):
|
|
||||||
"""Return the state attributes."""
|
|
||||||
return {ATTR_ATTRIBUTION: ATTRIBUTION}
|
|
@ -4,9 +4,10 @@ import voluptuous as vol
|
|||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
|
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
|
||||||
|
from homeassistant.core import callback
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
from .const import DEFAULT_NAME, DOMAIN
|
from .const import CONF_STATION_UPDATES, DEFAULT_NAME, DOMAIN
|
||||||
|
|
||||||
|
|
||||||
class AemetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
class AemetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
@ -47,6 +48,35 @@ class AemetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
return self.async_show_form(step_id="user", data_schema=schema, errors=errors)
|
return self.async_show_form(step_id="user", data_schema=schema, errors=errors)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@callback
|
||||||
|
def async_get_options_flow(config_entry):
|
||||||
|
"""Get the options flow for this handler."""
|
||||||
|
return OptionsFlowHandler(config_entry)
|
||||||
|
|
||||||
|
|
||||||
|
class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||||
|
"""Handle a option flow for AEMET."""
|
||||||
|
|
||||||
|
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
|
||||||
|
"""Initialize options flow."""
|
||||||
|
self.config_entry = config_entry
|
||||||
|
|
||||||
|
async def async_step_init(self, user_input=None):
|
||||||
|
"""Handle options flow."""
|
||||||
|
if user_input is not None:
|
||||||
|
return self.async_create_entry(title="", data=user_input)
|
||||||
|
|
||||||
|
data_schema = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(
|
||||||
|
CONF_STATION_UPDATES,
|
||||||
|
default=self.config_entry.options.get(CONF_STATION_UPDATES),
|
||||||
|
): bool,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return self.async_show_form(step_id="init", data_schema=data_schema)
|
||||||
|
|
||||||
|
|
||||||
async def _is_aemet_api_online(hass, api_key):
|
async def _is_aemet_api_online(hass, api_key):
|
||||||
aemet = AEMET(api_key)
|
aemet = AEMET(api_key)
|
||||||
|
@ -34,12 +34,12 @@ from homeassistant.const import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
ATTRIBUTION = "Powered by AEMET OpenData"
|
ATTRIBUTION = "Powered by AEMET OpenData"
|
||||||
|
CONF_STATION_UPDATES = "station_updates"
|
||||||
PLATFORMS = ["sensor", "weather"]
|
PLATFORMS = ["sensor", "weather"]
|
||||||
DEFAULT_NAME = "AEMET"
|
DEFAULT_NAME = "AEMET"
|
||||||
DOMAIN = "aemet"
|
DOMAIN = "aemet"
|
||||||
ENTRY_NAME = "name"
|
ENTRY_NAME = "name"
|
||||||
ENTRY_WEATHER_COORDINATOR = "weather_coordinator"
|
ENTRY_WEATHER_COORDINATOR = "weather_coordinator"
|
||||||
UPDATE_LISTENER = "update_listener"
|
|
||||||
SENSOR_NAME = "sensor_name"
|
SENSOR_NAME = "sensor_name"
|
||||||
SENSOR_UNIT = "sensor_unit"
|
SENSOR_UNIT = "sensor_unit"
|
||||||
SENSOR_DEVICE_CLASS = "sensor_device_class"
|
SENSOR_DEVICE_CLASS = "sensor_device_class"
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
"""Support for the AEMET OpenData service."""
|
"""Support for the AEMET OpenData service."""
|
||||||
from .abstract_aemet_sensor import AbstractAemetSensor
|
from homeassistant.components.sensor import SensorEntity
|
||||||
|
from homeassistant.const import ATTR_ATTRIBUTION
|
||||||
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
|
ATTRIBUTION,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
ENTRY_NAME,
|
ENTRY_NAME,
|
||||||
ENTRY_WEATHER_COORDINATOR,
|
ENTRY_WEATHER_COORDINATOR,
|
||||||
@ -10,6 +14,9 @@ from .const import (
|
|||||||
FORECAST_MONITORED_CONDITIONS,
|
FORECAST_MONITORED_CONDITIONS,
|
||||||
FORECAST_SENSOR_TYPES,
|
FORECAST_SENSOR_TYPES,
|
||||||
MONITORED_CONDITIONS,
|
MONITORED_CONDITIONS,
|
||||||
|
SENSOR_DEVICE_CLASS,
|
||||||
|
SENSOR_NAME,
|
||||||
|
SENSOR_UNIT,
|
||||||
WEATHER_SENSOR_TYPES,
|
WEATHER_SENSOR_TYPES,
|
||||||
)
|
)
|
||||||
from .weather_update_coordinator import WeatherUpdateCoordinator
|
from .weather_update_coordinator import WeatherUpdateCoordinator
|
||||||
@ -56,6 +63,52 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
|||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
|
class AbstractAemetSensor(CoordinatorEntity, SensorEntity):
|
||||||
|
"""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 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 extra_state_attributes(self):
|
||||||
|
"""Return the state attributes."""
|
||||||
|
return {ATTR_ATTRIBUTION: ATTRIBUTION}
|
||||||
|
|
||||||
|
|
||||||
class AemetSensor(AbstractAemetSensor):
|
class AemetSensor(AbstractAemetSensor):
|
||||||
"""Implementation of an AEMET OpenData sensor."""
|
"""Implementation of an AEMET OpenData sensor."""
|
||||||
|
|
||||||
|
@ -18,5 +18,14 @@
|
|||||||
"title": "AEMET OpenData"
|
"title": "AEMET OpenData"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"data": {
|
||||||
|
"station_updates": "Gather data from AEMET weather stations"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,5 +18,14 @@
|
|||||||
"title": "AEMET OpenData"
|
"title": "AEMET OpenData"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"data": {
|
||||||
|
"station_updates": "Gather data from AEMET weather stations"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -118,7 +118,7 @@ class TownNotFound(UpdateFailed):
|
|||||||
class WeatherUpdateCoordinator(DataUpdateCoordinator):
|
class WeatherUpdateCoordinator(DataUpdateCoordinator):
|
||||||
"""Weather data update coordinator."""
|
"""Weather data update coordinator."""
|
||||||
|
|
||||||
def __init__(self, hass, aemet, latitude, longitude):
|
def __init__(self, hass, aemet, latitude, longitude, station_updates):
|
||||||
"""Initialize coordinator."""
|
"""Initialize coordinator."""
|
||||||
super().__init__(
|
super().__init__(
|
||||||
hass, _LOGGER, name=DOMAIN, update_interval=WEATHER_UPDATE_INTERVAL
|
hass, _LOGGER, name=DOMAIN, update_interval=WEATHER_UPDATE_INTERVAL
|
||||||
@ -129,6 +129,7 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
|
|||||||
self._town = None
|
self._town = None
|
||||||
self._latitude = latitude
|
self._latitude = latitude
|
||||||
self._longitude = longitude
|
self._longitude = longitude
|
||||||
|
self._station_updates = station_updates
|
||||||
self._data = {
|
self._data = {
|
||||||
"daily": None,
|
"daily": None,
|
||||||
"hourly": None,
|
"hourly": None,
|
||||||
@ -210,7 +211,7 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
|
|||||||
)
|
)
|
||||||
|
|
||||||
station = None
|
station = None
|
||||||
if self._get_weather_station():
|
if self._station_updates and self._get_weather_station():
|
||||||
station = self._aemet.get_conventional_observation_station_data(
|
station = self._aemet.get_conventional_observation_station_data(
|
||||||
self._station[AEMET_ATTR_IDEMA]
|
self._station[AEMET_ATTR_IDEMA]
|
||||||
)
|
)
|
||||||
|
@ -5,7 +5,7 @@ from unittest.mock import MagicMock, patch
|
|||||||
import requests_mock
|
import requests_mock
|
||||||
|
|
||||||
from homeassistant import data_entry_flow
|
from homeassistant import data_entry_flow
|
||||||
from homeassistant.components.aemet.const import DOMAIN
|
from homeassistant.components.aemet.const import CONF_STATION_UPDATES, DOMAIN
|
||||||
from homeassistant.config_entries import ENTRY_STATE_LOADED, SOURCE_USER
|
from homeassistant.config_entries import ENTRY_STATE_LOADED, SOURCE_USER
|
||||||
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
|
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
@ -58,8 +58,64 @@ async def test_form(hass):
|
|||||||
assert len(mock_setup_entry.mock_calls) == 1
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_form_options(hass):
|
||||||
|
"""Test the form options."""
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert entry.state == "loaded"
|
||||||
|
|
||||||
|
result = await hass.config_entries.options.async_init(entry.entry_id)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "init"
|
||||||
|
|
||||||
|
result = await hass.config_entries.options.async_configure(
|
||||||
|
result["flow_id"], user_input={CONF_STATION_UPDATES: False}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||||
|
assert entry.options == {
|
||||||
|
CONF_STATION_UPDATES: False,
|
||||||
|
}
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert entry.state == "loaded"
|
||||||
|
|
||||||
|
result = await hass.config_entries.options.async_init(entry.entry_id)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "init"
|
||||||
|
|
||||||
|
result = await hass.config_entries.options.async_configure(
|
||||||
|
result["flow_id"], user_input={CONF_STATION_UPDATES: True}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||||
|
assert entry.options == {
|
||||||
|
CONF_STATION_UPDATES: True,
|
||||||
|
}
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert entry.state == "loaded"
|
||||||
|
|
||||||
|
|
||||||
async def test_form_duplicated_id(hass):
|
async def test_form_duplicated_id(hass):
|
||||||
"""Test that the options form."""
|
"""Test setting up duplicated entry."""
|
||||||
|
|
||||||
now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00")
|
now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00")
|
||||||
with patch("homeassistant.util.dt.now", return_value=now), patch(
|
with patch("homeassistant.util.dt.now", return_value=now), patch(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user