meteocat 0.1.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/.github/workflows/release.yml +33 -0
  2. package/.pre-commit-config.yaml +37 -0
  3. package/.releaserc +11 -0
  4. package/.releaserc.toml +14 -0
  5. package/AUTHORS.md +12 -0
  6. package/CHANGELOG.md +361 -0
  7. package/LICENSE +194 -0
  8. package/README.md +41 -0
  9. package/custom_components/meteocat/__init__.py +139 -0
  10. package/custom_components/meteocat/condition.py +28 -0
  11. package/custom_components/meteocat/config_flow.py +192 -0
  12. package/custom_components/meteocat/const.py +46 -0
  13. package/custom_components/meteocat/coordinator.py +177 -0
  14. package/custom_components/meteocat/entity.py +91 -0
  15. package/custom_components/meteocat/helpers.py +42 -0
  16. package/custom_components/meteocat/manifest.json +12 -0
  17. package/custom_components/meteocat/options_flow.py +71 -0
  18. package/custom_components/meteocat/sensor.py +190 -0
  19. package/custom_components/meteocat/strings.json +26 -0
  20. package/custom_components/meteocat/translations/ca.json +26 -0
  21. package/custom_components/meteocat/translations/en.json +26 -0
  22. package/custom_components/meteocat/translations/es.json +26 -0
  23. package/custom_components/meteocat/version.py +2 -0
  24. package/filetree.py +48 -0
  25. package/filetree.txt +43 -0
  26. package/hacs.json +6 -0
  27. package/package.json +22 -0
  28. package/poetry.lock +3205 -0
  29. package/pyproject.toml +64 -0
  30. package/releaserc.json +18 -0
  31. package/requirements.test.txt +3 -0
  32. package/setup.cfg +64 -0
  33. package/setup.py +10 -0
  34. package/tests/__init__.py +0 -0
  35. package/tests/bandit.yaml +17 -0
  36. package/tests/conftest.py +19 -0
  37. package/tests/test_init.py +9 -0
package/README.md ADDED
@@ -0,0 +1,41 @@
1
+ # Meteocat for Home Assistant
2
+
3
+ [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
4
+ [![Python version compatibility](https://img.shields.io/pypi/pyversions/meteocat)](https://pypi.org/project/meteocat)
5
+ [![pipeline status](https://gitlab.com/figorr/meteocat/badges/master/pipeline.svg)](https://gitlab.com/figorr/meteocat/commits/master)
6
+
7
+
8
+ This is a project to obtain meteorological data from the Meteocat API.
9
+
10
+ **NOTE:** Meteocat API requires to use an API_KEY, you should ask to (https://apidocs.meteocat.gencat.cat/documentacio/acces-ciutada-i-administracio/)
11
+
12
+ # Credits
13
+
14
+ This is a personal project.
15
+
16
+ Authors:
17
+ - Figorr
18
+
19
+ # Contributing
20
+
21
+ 1. [Check for open features/bugs](https://gitlab.com/figorr/meteocat/issues)
22
+ or [initiate a discussion on one](https://gitlab.com/figorr/meteocat/issues/new).
23
+ 2. [Fork the repository](https://gitlab.com/figorr/meteocat/forks/new).
24
+ 3. Install the dev environment: `make init`.
25
+ 4. Enter the virtual environment: `pipenv shell`
26
+ 5. Code your new feature or bug fix.
27
+ 6. Write a test that covers your new functionality.
28
+ 7. Update `README.md` with any new documentation.
29
+ 8. Run tests and ensure 100% code coverage for your contribution: `make coverage`
30
+ 9. Ensure you have no linting errors: `make lint`
31
+ 10. Ensure you have typed your code correctly: `make typing`
32
+ 11. Add yourself to `AUTHORS.md`.
33
+ 12. Submit a pull request!
34
+
35
+ # License
36
+
37
+ [Apache-2.0](LICENSE). By providing a contribution, you agree the contribution is licensed under Apache-2.0.
38
+
39
+ # API Reference
40
+
41
+ [See the docs 馃摎](https://apidocs.meteocat.gencat.cat/section/informacio-general/).
@@ -0,0 +1,139 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import logging
5
+ from homeassistant import core
6
+ from homeassistant.config_entries import ConfigEntry
7
+ from homeassistant.core import HomeAssistant
8
+ from homeassistant.exceptions import HomeAssistantError
9
+ from .coordinator import MeteocatSensorCoordinator, MeteocatEntityCoordinator
10
+ from .const import (
11
+ DOMAIN,
12
+ CONF_API_KEY,
13
+ TOWN_NAME,
14
+ TOWN_ID,
15
+ VARIABLE_NAME,
16
+ VARIABLE_ID,
17
+ STATION_NAME,
18
+ STATION_ID,
19
+ )
20
+
21
+ _LOGGER = logging.getLogger(__name__)
22
+
23
+ # Versi贸n
24
+ __version__ = "0.1.18"
25
+
26
+ # Plataformas soportadas por la integraci贸n
27
+ PLATFORMS = ["sensor", "entity"]
28
+
29
+
30
+ async def async_setup(hass: core.HomeAssistant, config: dict) -> bool:
31
+ """Configuraci贸n inicial del componente Meteocat."""
32
+ return True
33
+
34
+
35
+ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
36
+ """Configura una entrada de configuraci贸n para Meteocat."""
37
+ _LOGGER.info("Configurando la integraci贸n de Meteocat...")
38
+
39
+ # Extraer los datos necesarios de la entrada de configuraci贸n
40
+ entry_data = entry.data
41
+ required_fields = [
42
+ CONF_API_KEY, TOWN_NAME, TOWN_ID, VARIABLE_ID, STATION_NAME, STATION_ID
43
+ ]
44
+
45
+ # Validar que todos los campos requeridos est茅n presentes
46
+ if not all(field in entry_data for field in required_fields):
47
+ _LOGGER.error("Faltan datos en la configuraci贸n. Por favor, reconfigura la integraci贸n.")
48
+ return False
49
+
50
+ api_key = entry_data[CONF_API_KEY]
51
+ town_name = entry_data[TOWN_NAME]
52
+ town_id = entry_data[TOWN_ID]
53
+ variable_name = entry_data[VARIABLE_NAME]
54
+ variable_id = entry_data[VARIABLE_ID]
55
+ station_name = entry_data[STATION_NAME]
56
+ station_id = entry_data[STATION_ID]
57
+
58
+ _LOGGER.info(
59
+ f"Integraci贸n configurada para el municipio '{town_name}' (ID: {town_id}), "
60
+ f"variable '{variable_name}' (ID: {variable_id}), estaci贸n {station_name} (ID: {station_id})."
61
+ )
62
+
63
+ # Inicializa y refresca los coordinadores
64
+ try:
65
+ # Pasar los datos adicionales al constructor del coordinador
66
+ sensor_coordinator = MeteocatSensorCoordinator(
67
+ hass,
68
+ entry,
69
+ town_name=town_name,
70
+ town_id=town_id,
71
+ station_name=station_name,
72
+ station_id=station_id,
73
+ variable_name=variable_name,
74
+ variable_id=variable_id,
75
+ )
76
+ # Pasar los mismos datos al constructor de MeteocatEntityCoordinator
77
+ entity_coordinator = MeteocatEntityCoordinator(
78
+ hass,
79
+ entry,
80
+ town_name=town_name,
81
+ town_id=town_id,
82
+ station_name=station_name,
83
+ station_id=station_id,
84
+ variable_name=variable_name,
85
+ variable_id=variable_id,
86
+ )
87
+
88
+ await sensor_coordinator.async_config_entry_first_refresh()
89
+ await entity_coordinator.async_config_entry_first_refresh()
90
+ except HomeAssistantError as err:
91
+ _LOGGER.error(f"Error al inicializar los coordinadores: {err}")
92
+ return False
93
+
94
+ # Guardar los datos y los coordinadores en hass.data
95
+ hass.data.setdefault(DOMAIN, {})
96
+ hass.data[DOMAIN][entry.entry_id] = {
97
+ "sensor_coordinator": sensor_coordinator,
98
+ "entity_coordinator": entity_coordinator,
99
+ "api_key": api_key,
100
+ "town_id": town_id,
101
+ "town_name": town_name,
102
+ "station_id": station_id,
103
+ "station_name": station_name,
104
+ "variable_id": variable_id,
105
+ "variable_name": variable_name,
106
+ }
107
+
108
+ # Configurar las plataformas
109
+ await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
110
+
111
+ return True
112
+
113
+ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
114
+ """Desactiva una entrada de configuraci贸n para Meteocat."""
115
+ if entry.entry_id in hass.data.get(DOMAIN, {}):
116
+ hass.data[DOMAIN].pop(entry.entry_id, None)
117
+ if not hass.data[DOMAIN]: # Si no quedan entradas, elimina el dominio
118
+ hass.data.pop(DOMAIN)
119
+
120
+ # Desinstalar las plataformas
121
+ return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
122
+
123
+
124
+ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
125
+ """Limpia cualquier dato adicional al desinstalar la integraci贸n."""
126
+ _LOGGER.info(f"Eliminando datos residuales de la integraci贸n: {entry.entry_id}")
127
+
128
+ # Definir la ruta del archivo de s铆mbolos
129
+ assets_folder = hass.config.path("custom_components", DOMAIN, "assets")
130
+ symbols_file = os.path.join(assets_folder, "symbols.json")
131
+
132
+ try:
133
+ if os.path.exists(symbols_file):
134
+ os.remove(symbols_file)
135
+ _LOGGER.info("Archivo symbols.json eliminado correctamente.")
136
+ else:
137
+ _LOGGER.info("El archivo symbols.json no se encontr贸.")
138
+ except OSError as e:
139
+ _LOGGER.error(f"Error al intentar eliminar el archivo symbols.json: {e}")
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+ from .const import CONDITION_MAPPING
5
+ from .helpers import is_night # Importar la funci贸n is_night de helpers.py
6
+
7
+ def get_condition_from_statcel(codi_estatcel, current_time: datetime, hass) -> dict:
8
+ """
9
+ Convierte el c贸digo 'estatCel' en condici贸n de Home Assistant.
10
+
11
+ :param codi_estatcel: C贸digo del estado del cielo (celestial state code).
12
+ :param current_time: Fecha y hora actual (datetime).
13
+ :param hass: Instancia de Home Assistant.
14
+ :return: Diccionario con la condici贸n y el icono.
15
+ """
16
+ # Determinar si es de noche usando la l贸gica centralizada en helpers.py
17
+ is_night_flag = is_night(current_time, hass)
18
+
19
+ # Identificar la condici贸n basada en el c贸digo
20
+ for condition, codes in CONDITION_MAPPING.items():
21
+ if codi_estatcel in codes:
22
+ # Ajustar para condiciones nocturnas si aplica
23
+ if condition == "sunny" and is_night_flag:
24
+ return {"condition": "clear-night", "icon": None}
25
+ return {"condition": condition, "icon": None}
26
+
27
+ # Si no coincide ning煤n c贸digo, devolver condici贸n desconocida
28
+ return {"condition": "unknown", "icon": None}
@@ -0,0 +1,192 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import logging
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ import voluptuous as vol
9
+ from aiohttp import ClientError
10
+ import aiofiles
11
+
12
+ from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
13
+ from homeassistant.core import callback
14
+ from homeassistant.exceptions import HomeAssistantError
15
+ from homeassistant.helpers import aiohttp_client, config_validation as cv
16
+
17
+ from .const import (
18
+ DOMAIN,
19
+ CONF_API_KEY,
20
+ TOWN_NAME,
21
+ TOWN_ID,
22
+ VARIABLE_NAME,
23
+ VARIABLE_ID,
24
+ STATION_NAME,
25
+ STATION_ID
26
+ )
27
+
28
+ from .options_flow import MeteocatOptionsFlowHandler
29
+ from meteocatpy.town import MeteocatTown
30
+ from meteocatpy.symbols import MeteocatSymbols
31
+ from meteocatpy.variables import MeteocatVariables
32
+ from meteocatpy.townstations import MeteocatTownStations
33
+ from meteocatpy.exceptions import BadRequestError, ForbiddenError, TooManyRequestsError, InternalServerError, UnknownAPIError
34
+
35
+ _LOGGER = logging.getLogger(__name__)
36
+
37
+ class MeteocatConfigFlow(ConfigFlow, domain=DOMAIN):
38
+ """Flujo de configuraci贸n para Meteocat."""
39
+
40
+ VERSION = 1
41
+
42
+ def __init__(self):
43
+ self.api_key: str | None = None
44
+ self.municipis: list[dict[str, Any]] = []
45
+ self.selected_municipi: dict[str, Any] | None = None
46
+ self.variable_id: str | None = None
47
+ self.station_id: str | None = None
48
+ self.station_name: str | None = None
49
+
50
+ async def async_step_user(
51
+ self, user_input: dict[str, Any] | None = None
52
+ ) -> ConfigFlowResult:
53
+ """Primer paso: Solicitar la API Key."""
54
+ errors = {}
55
+
56
+ if user_input is not None:
57
+ self.api_key = user_input[CONF_API_KEY]
58
+
59
+ town_client = MeteocatTown(self.api_key)
60
+
61
+ try:
62
+ self.municipis = await town_client.get_municipis()
63
+ except (BadRequestError, ForbiddenError, TooManyRequestsError, InternalServerError, UnknownAPIError) as ex:
64
+ _LOGGER.error("Error al conectar con la API de Meteocat: %s", ex)
65
+ errors["base"] = "cannot_connect"
66
+ except Exception as ex:
67
+ _LOGGER.error("Error inesperado al validar la API Key: %s", ex)
68
+ errors["base"] = "unknown"
69
+
70
+ if not errors:
71
+ return await self.async_step_select_municipi()
72
+
73
+ schema = vol.Schema({vol.Required(CONF_API_KEY): str})
74
+ return self.async_show_form(step_id="user", data_schema=schema, errors=errors)
75
+
76
+ async def async_step_select_municipi(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
77
+ """Segundo paso: Seleccionar el municipio."""
78
+ errors = {}
79
+
80
+ if user_input is not None:
81
+ selected_codi = user_input["municipi"]
82
+ self.selected_municipi = next(
83
+ (m for m in self.municipis if m["codi"] == selected_codi), None
84
+ )
85
+
86
+ if self.selected_municipi:
87
+ await self.fetch_symbols_and_variables()
88
+
89
+ if not errors and self.selected_municipi:
90
+ return await self.async_step_select_station()
91
+
92
+ schema = vol.Schema(
93
+ {vol.Required("municipi"): vol.In({m["codi"]: m["nom"] for m in self.municipis})}
94
+ )
95
+
96
+ return self.async_show_form(step_id="select_municipi", data_schema=schema, errors=errors)
97
+
98
+ async def fetch_symbols_and_variables(self):
99
+ """Descarga los s铆mbolos y las variables despu茅s de seleccionar el municipio."""
100
+
101
+ errors = {}
102
+
103
+ # Descargar y guardar los s铆mbolos
104
+ assets_dir = Path(__file__).parent / "assets"
105
+ assets_dir.mkdir(parents=True, exist_ok=True)
106
+ symbols_file = assets_dir / "symbols.json"
107
+ symbols_client = MeteocatSymbols(self.api_key)
108
+
109
+ try:
110
+ symbols_data = await symbols_client.fetch_symbols()
111
+ async with aiofiles.open(symbols_file, "w", encoding="utf-8") as file:
112
+ await file.write(json.dumps({"symbols": symbols_data}, ensure_ascii=False, indent=4))
113
+ except (BadRequestError, ForbiddenError, TooManyRequestsError, InternalServerError, UnknownAPIError) as ex:
114
+ _LOGGER.error("Error al descargar o guardar los s铆mbolos: %s", ex)
115
+ errors["base"] = "symbols_download_failed"
116
+
117
+ if not errors:
118
+ variables_client = MeteocatVariables(self.api_key)
119
+ try:
120
+ variables_data = await variables_client.get_variables()
121
+ self.variable_id = next(
122
+ (v["codi"] for v in variables_data if v["nom"].lower() == "temperatura"), None
123
+ )
124
+ if not self.variable_id:
125
+ _LOGGER.error("No se encontr贸 la variable 'Temperatura'")
126
+ errors["base"] = "variable_not_found"
127
+ except (BadRequestError, ForbiddenError, TooManyRequestsError, InternalServerError, UnknownAPIError) as ex:
128
+ _LOGGER.error("Error al obtener las variables: %s", ex)
129
+ errors["base"] = "variables_fetch_failed"
130
+
131
+ if errors:
132
+ raise HomeAssistantError(errors)
133
+
134
+ async def async_step_select_station(
135
+ self, user_input: dict[str, Any] | None = None
136
+ ) -> ConfigFlowResult:
137
+ """Tercer paso: Seleccionar la estaci贸n para la variable seleccionada."""
138
+ errors = {}
139
+
140
+ townstations_client = MeteocatTownStations(self.api_key)
141
+ try:
142
+ stations_data = await townstations_client.get_town_stations(
143
+ self.selected_municipi["codi"], self.variable_id
144
+ )
145
+ except Exception as ex:
146
+ _LOGGER.error("Error al obtener las estaciones: %s", ex)
147
+ errors["base"] = "stations_fetch_failed"
148
+ stations_data = []
149
+
150
+ if user_input is not None:
151
+ selected_station_codi = user_input["station"]
152
+ selected_station = next(
153
+ (station for station in stations_data[0]["variables"][0]["estacions"] if station["codi"] == selected_station_codi),
154
+ None
155
+ )
156
+
157
+ if selected_station:
158
+ self.station_id = selected_station["codi"]
159
+ self.station_name = selected_station["nom"]
160
+
161
+ return self.async_create_entry(
162
+ title=self.selected_municipi["nom"],
163
+ data={
164
+ CONF_API_KEY: self.api_key,
165
+ TOWN_NAME: self.selected_municipi["nom"],
166
+ TOWN_ID: self.selected_municipi["codi"],
167
+ VARIABLE_NAME: "Temperatura",
168
+ VARIABLE_ID: str(self.variable_id),
169
+ STATION_NAME: self.station_name,
170
+ STATION_ID: self.station_id
171
+ },
172
+ )
173
+ else:
174
+ errors["base"] = "station_not_found"
175
+
176
+ schema = vol.Schema(
177
+ {
178
+ vol.Required("station"): vol.In(
179
+ {station["codi"]: station["nom"] for station in stations_data[0]["variables"][0]["estacions"]}
180
+ )
181
+ }
182
+ )
183
+
184
+ return self.async_show_form(
185
+ step_id="select_station", data_schema=schema, errors=errors
186
+ )
187
+
188
+ @staticmethod
189
+ @callback
190
+ def async_get_options_flow(config_entry: ConfigEntry) -> MeteocatOptionsFlowHandler:
191
+ """Devuelve el flujo de opciones para esta configuraci贸n."""
192
+ return MeteocatOptionsFlowHandler(config_entry)
@@ -0,0 +1,46 @@
1
+ # Constantes generales
2
+ DOMAIN = "meteocat"
3
+ BASE_URL = "https://api.meteo.cat"
4
+ CONF_API_KEY = "api_key"
5
+ TOWN_NAME = "town_name"
6
+ TOWN_ID = "town_id"
7
+ VARIABLE_NAME = "variable_name"
8
+ VARIABLE_ID = "variable_id"
9
+ STATION_NAME = "station_name"
10
+ STATION_ID = "station_id"
11
+
12
+ # C贸digos de sensores de la API
13
+ WIND_SPEED = "30" # Velocidad del viento
14
+ WIND_DIRECTION = "31" # Direcci贸n del viento
15
+ TEMPERATURE = "32" # Temperatura
16
+ HUMIDITY = "33" # Humedad relativa
17
+ PRESSURE = "34" # Presi贸n atmosf茅rica
18
+ PRECIPITATION = "35" # Precipitaci贸n
19
+ UV_INDEX = "39" # UV
20
+ MAX_TEMPERATURE = "40" # Temperatura m谩xima
21
+ MIN_TEMPERATURE = "42" # Temperatura m铆nima
22
+ WIND_GUST = "50" # Racha de viento
23
+
24
+ # Unidades de medida de los sensores
25
+ WIND_SPEED_UNIT = "m/s"
26
+ WIND_DIRECTION_UNIT = "掳"
27
+ TEMPERATURE_UNIT = "掳C"
28
+ HUMIDITY_UNIT = "%"
29
+ PRESSURE_UNIT = "hPa"
30
+ PRECIPITATION_UNIT = "mm"
31
+ UV_INDEX_UNIT = "UV"
32
+
33
+ # Mapeo de c贸digos 'estatCel' a condiciones de Home Assistant
34
+ CONDITION_MAPPING = {
35
+ "sunny": [1],
36
+ "clear-night": [1],
37
+ "partlycloudy": [2, 3],
38
+ "cloudy": [4, 20, 21, 22],
39
+ "rainy": [5, 6, 23],
40
+ "pouring": [7, 8, 25],
41
+ "lightning-rainy": [8, 24],
42
+ "hail": [9],
43
+ "snowy": [10, 26, 27, 28],
44
+ "fog": [11, 12],
45
+ "snow-rainy": [27, 29, 30],
46
+ }
@@ -0,0 +1,177 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from datetime import timedelta
5
+ from typing import Dict
6
+
7
+ from homeassistant.core import HomeAssistant
8
+ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
9
+ from homeassistant.exceptions import ConfigEntryNotReady
10
+
11
+ from meteocatpy.data import MeteocatStationData
12
+ from meteocatpy.forecast import MeteocatForecast
13
+ from meteocatpy.exceptions import (
14
+ BadRequestError,
15
+ ForbiddenError,
16
+ TooManyRequestsError,
17
+ InternalServerError,
18
+ UnknownAPIError,
19
+ )
20
+
21
+ from .const import DOMAIN
22
+
23
+ _LOGGER = logging.getLogger(__name__)
24
+
25
+ # Valores predeterminados para los intervalos de actualizaci贸n
26
+ DEFAULT_SENSOR_UPDATE_INTERVAL = timedelta(minutes=90)
27
+ DEFAULT_ENTITY_UPDATE_INTERVAL = timedelta(hours=12)
28
+
29
+
30
+ class MeteocatSensorCoordinator(DataUpdateCoordinator):
31
+ """Coordinator para manejar la actualizaci贸n de datos de los sensores."""
32
+
33
+ def __init__(
34
+ self,
35
+ hass: HomeAssistant,
36
+ api_key: str,
37
+ town_name: str,
38
+ town_id: str,
39
+ station_name: str,
40
+ station_id: str,
41
+ variable_name: str,
42
+ variable_id: str,
43
+ update_interval: timedelta = DEFAULT_SENSOR_UPDATE_INTERVAL,
44
+ ):
45
+ """Inicializa el coordinador de sensores de Meteocat."""
46
+ self.api_key = api_key
47
+ self.town_name = town_name
48
+ self.town_id = town_id
49
+ self.station_name = station_name
50
+ self.station_id = station_id
51
+ self.variable_name = variable_name
52
+ self.variable_id = variable_id
53
+ self.meteocat_station_data = MeteocatStationData(api_key)
54
+ super().__init__(
55
+ hass,
56
+ _LOGGER,
57
+ name=f"{DOMAIN} Sensor Coordinator",
58
+ update_interval=update_interval,
59
+ )
60
+
61
+ async def _async_update_data(self) -> Dict:
62
+ """
63
+ Actualiza los datos de los sensores desde la API de Meteocat.
64
+
65
+ Returns:
66
+ dict: Datos actualizados de los sensores.
67
+ """
68
+ try:
69
+ data = await self.meteocat_station_data.get_station_data_with_variables(self.station_id)
70
+ _LOGGER.debug("Datos de sensores actualizados exitosamente: %s", data)
71
+ return data
72
+ except ForbiddenError as err:
73
+ _LOGGER.error(
74
+ "Acceso denegado al obtener datos de sensores (Station ID: %s): %s",
75
+ self.station_id,
76
+ err,
77
+ )
78
+ raise ConfigEntryNotReady from err
79
+ except TooManyRequestsError as err:
80
+ _LOGGER.warning(
81
+ "L铆mite de solicitudes alcanzado al obtener datos de sensores (Station ID: %s): %s",
82
+ self.station_id,
83
+ err,
84
+ )
85
+ raise ConfigEntryNotReady from err
86
+ except (BadRequestError, InternalServerError, UnknownAPIError) as err:
87
+ _LOGGER.error(
88
+ "Error al obtener datos de sensores (Station ID: %s): %s",
89
+ self.station_id,
90
+ err,
91
+ )
92
+ raise
93
+ except Exception as err:
94
+ _LOGGER.exception(
95
+ "Error inesperado al obtener datos de sensores (Station ID: %s): %s",
96
+ self.station_id,
97
+ err,
98
+ )
99
+ raise
100
+
101
+
102
+ class MeteocatEntityCoordinator(DataUpdateCoordinator):
103
+ """Coordinator para manejar la actualizaci贸n de datos de las entidades de predicci贸n."""
104
+
105
+ def __init__(
106
+ self,
107
+ hass: HomeAssistant,
108
+ api_key: str,
109
+ town_name: str,
110
+ town_id: str,
111
+ station_name: str,
112
+ station_id: str,
113
+ variable_name: str,
114
+ variable_id: str,
115
+ update_interval: timedelta = DEFAULT_ENTITY_UPDATE_INTERVAL,
116
+ ):
117
+ """Inicializa el coordinador de datos para entidades de predicci贸n."""
118
+ self.api_key = api_key
119
+ self.town_name = town_name
120
+ self.town_id = town_id
121
+ self.station_name = station_name
122
+ self.station_id = station_id
123
+ self.variable_name = variable_name
124
+ self.variable_id = variable_id
125
+ self.meteocat_forecast = MeteocatForecast(api_key)
126
+ super().__init__(
127
+ hass,
128
+ _LOGGER,
129
+ name=f"{DOMAIN} Entity Coordinator",
130
+ update_interval=update_interval,
131
+ )
132
+
133
+ async def _async_update_data(self) -> Dict:
134
+ """
135
+ Actualiza los datos de las entidades de predicci贸n desde la API de Meteocat.
136
+
137
+ Returns:
138
+ dict: Datos actualizados de predicci贸n horaria y diaria.
139
+ """
140
+ try:
141
+ hourly_forecast = await self.meteocat_forecast.get_prediccion_horaria(self.town_id)
142
+ daily_forecast = await self.meteocat_forecast.get_prediccion_diaria(self.town_id)
143
+ _LOGGER.debug(
144
+ "Datos de predicci贸n actualizados exitosamente (Town ID: %s)", self.town_id
145
+ )
146
+ return {
147
+ "hourly_forecast": hourly_forecast,
148
+ "daily_forecast": daily_forecast,
149
+ }
150
+ except ForbiddenError as err:
151
+ _LOGGER.error(
152
+ "Acceso denegado al obtener datos de predicci贸n (Town ID: %s): %s",
153
+ self.town_id,
154
+ err,
155
+ )
156
+ raise ConfigEntryNotReady from err
157
+ except TooManyRequestsError as err:
158
+ _LOGGER.warning(
159
+ "L铆mite de solicitudes alcanzado al obtener datos de predicci贸n (Town ID: %s): %s",
160
+ self.town_id,
161
+ err,
162
+ )
163
+ raise ConfigEntryNotReady from err
164
+ except (BadRequestError, InternalServerError, UnknownAPIError) as err:
165
+ _LOGGER.error(
166
+ "Error al obtener datos de predicci贸n (Town ID: %s): %s",
167
+ self.town_id,
168
+ err,
169
+ )
170
+ raise
171
+ except Exception as err:
172
+ _LOGGER.exception(
173
+ "Error inesperado al obtener datos de predicci贸n (Town ID: %s): %s",
174
+ self.town_id,
175
+ err,
176
+ )
177
+ raise