meteocat 3.2.0 → 4.0.0

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 (47) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +45 -45
  2. package/.github/ISSUE_TEMPLATE/config.yml +8 -8
  3. package/.github/ISSUE_TEMPLATE/improvement.md +39 -39
  4. package/.github/ISSUE_TEMPLATE/new_function.md +41 -41
  5. package/.github/labels.yml +63 -63
  6. package/.github/workflows/autocloser.yaml +27 -27
  7. package/.github/workflows/close-on-label.yml +48 -48
  8. package/.github/workflows/force-sync-labels.yml +18 -18
  9. package/.github/workflows/hassfest.yaml +13 -13
  10. package/.github/workflows/publish-zip.yml +67 -67
  11. package/.github/workflows/release.yml +41 -41
  12. package/.github/workflows/stale.yml +63 -63
  13. package/.github/workflows/sync-gitlab.yml +107 -107
  14. package/.github/workflows/sync-labels.yml +21 -21
  15. package/.github/workflows/validate.yaml +16 -16
  16. package/.pre-commit-config.yaml +37 -37
  17. package/.releaserc +37 -37
  18. package/AUTHORS.md +13 -13
  19. package/CHANGELOG.md +954 -932
  20. package/README.md +207 -207
  21. package/conftest.py +11 -11
  22. package/custom_components/meteocat/__init__.py +298 -298
  23. package/custom_components/meteocat/condition.py +63 -63
  24. package/custom_components/meteocat/config_flow.py +613 -613
  25. package/custom_components/meteocat/const.py +132 -132
  26. package/custom_components/meteocat/helpers.py +58 -58
  27. package/custom_components/meteocat/manifest.json +25 -25
  28. package/custom_components/meteocat/options_flow.py +287 -287
  29. package/custom_components/meteocat/strings.json +1058 -1058
  30. package/custom_components/meteocat/translations/ca.json +1058 -1058
  31. package/custom_components/meteocat/translations/en.json +1058 -1058
  32. package/custom_components/meteocat/translations/es.json +1058 -1058
  33. package/custom_components/meteocat/version.py +1 -1
  34. package/custom_components/meteocat/weather.py +218 -218
  35. package/filetree.py +48 -48
  36. package/filetree.txt +79 -79
  37. package/hacs.json +8 -8
  38. package/info.md +11 -11
  39. package/package.json +22 -22
  40. package/poetry.lock +3222 -3222
  41. package/pyproject.toml +68 -68
  42. package/requirements.test.txt +3 -3
  43. package/setup.cfg +64 -64
  44. package/setup.py +10 -10
  45. package/tests/bandit.yaml +17 -17
  46. package/tests/conftest.py +19 -19
  47. package/tests/test_init.py +9 -9
@@ -1,298 +1,298 @@
1
- from __future__ import annotations
2
-
3
- import logging
4
- import voluptuous as vol
5
- from pathlib import Path
6
- import aiofiles
7
- import json
8
- import importlib # Para importaciones lazy
9
-
10
- from homeassistant import core
11
- from homeassistant.config_entries import ConfigEntry
12
- from homeassistant.core import HomeAssistant
13
- from homeassistant.exceptions import HomeAssistantError
14
- from homeassistant.helpers.entity_platform import async_get_platforms
15
- from homeassistant.helpers import config_validation as cv
16
-
17
- from .helpers import get_storage_dir
18
- from meteocatpy.town import MeteocatTown
19
- from meteocatpy.symbols import MeteocatSymbols
20
- from meteocatpy.variables import MeteocatVariables
21
- from meteocatpy.townstations import MeteocatTownStations
22
- from .const import DOMAIN, PLATFORMS
23
-
24
- _LOGGER = logging.getLogger(__name__)
25
-
26
- # Versión
27
- __version__ = "3.2.0"
28
-
29
- # Definir el esquema de configuración CONFIG_SCHEMA
30
- CONFIG_SCHEMA = vol.Schema(
31
- {
32
- DOMAIN: vol.Schema(
33
- {
34
- vol.Required("api_key"): cv.string,
35
- vol.Required("town_name"): cv.string,
36
- vol.Required("town_id"): cv.string,
37
- vol.Optional("variable_name", default="temperature"): cv.string,
38
- vol.Required("variable_id"): cv.string,
39
- vol.Optional("station_name"): cv.string,
40
- vol.Optional("station_id"): cv.string,
41
- vol.Optional("province_name"): cv.string,
42
- vol.Optional("province_id"): cv.string,
43
- vol.Optional("region_name"): cv.string,
44
- vol.Optional("region_id"): cv.string,
45
- vol.Required("latitude"): cv.latitude,
46
- vol.Required("longitude"): cv.longitude,
47
- vol.Required("altitude"): vol.Coerce(float),
48
- }
49
- )
50
- },
51
- extra=vol.ALLOW_EXTRA,
52
- )
53
-
54
- def safe_remove(path: Path, is_folder: bool = False) -> None:
55
- """Elimina un archivo o carpeta vacía de forma segura."""
56
- try:
57
- if is_folder:
58
- if path.exists() and path.is_dir():
59
- path.rmdir() # Solo elimina si está vacía
60
- _LOGGER.info("Carpeta eliminada: %s", path)
61
- else:
62
- if path.exists():
63
- path.unlink()
64
- _LOGGER.info("Archivo eliminado: %s", path)
65
- except Exception as e:
66
- _LOGGER.error("Error eliminando %s: %s", path, e)
67
-
68
- async def ensure_assets_exist(hass, api_key, town_id=None, variable_id=None):
69
- """Comprueba y crea los assets básicos si faltan."""
70
- assets_dir = get_storage_dir(hass, "assets")
71
- assets_dir.mkdir(parents=True, exist_ok=True)
72
-
73
- # Lista de assets: (nombre_archivo, fetch_func, clave_json, args)
74
- assets = [
75
- ("towns.json", MeteocatTown(api_key).get_municipis, "towns", []),
76
- ("stations.json", MeteocatTownStations(api_key).stations_service.get_stations, "stations", []),
77
- ("variables.json", MeteocatVariables(api_key).get_variables, "variables", []),
78
- ("symbols.json", MeteocatSymbols(api_key).fetch_symbols, "symbols", []),
79
- ]
80
-
81
- # Si tenemos town_id y variable_id, agregamos stations_<town_id>.json
82
- if town_id and variable_id:
83
- assets.append(
84
- (f"stations_{town_id}.json", MeteocatTownStations(api_key).get_town_stations, "town_stations", [town_id, variable_id])
85
- )
86
-
87
- for filename, fetch_func, key, args in assets:
88
- file_path = assets_dir / filename
89
- if not file_path.exists():
90
- _LOGGER.debug("Intentando descargar datos para %s desde la API con args: %s", key, args)
91
- try:
92
- data = await fetch_func(*args)
93
- except Exception as ex:
94
- _LOGGER.warning(
95
- "No se pudieron obtener los datos para %s. Intenta regenerarlo más adelante desde las opciones de la integración. Detalle: %s",
96
- key,
97
- ex,
98
- )
99
- data = []
100
- async with aiofiles.open(file_path, "w", encoding="utf-8") as file:
101
- await file.write(json.dumps({key: data}, ensure_ascii=False, indent=4))
102
- _LOGGER.info("Archivo creado: %s", file_path)
103
-
104
- async def async_setup(hass: core.HomeAssistant, config: dict) -> bool:
105
- """Configuración inicial del componente Meteocat."""
106
- return True
107
-
108
- def _get_coordinator_module(cls_name: str):
109
- """Importa dinámicamente un coordinador para evitar blocking imports."""
110
- module = importlib.import_module(".coordinator", "custom_components.meteocat")
111
- return getattr(module, cls_name)
112
-
113
- async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
114
- """Configura una entrada de configuración para Meteocat."""
115
- _LOGGER.info("Configurando la integración de Meteocat...")
116
-
117
- # Extraer los datos necesarios de la entrada de configuración
118
- entry_data = entry.data
119
-
120
- # Validar campos requeridos
121
- required_fields = [
122
- "api_key", "town_name", "town_id", "variable_name",
123
- "variable_id", "station_name", "station_id", "province_name",
124
- "province_id", "region_name", "region_id", "latitude", "longitude", "altitude"
125
- ]
126
- missing_fields = [field for field in required_fields if field not in entry_data]
127
- if missing_fields:
128
- _LOGGER.error(f"Faltan los siguientes campos en la configuración: {missing_fields}")
129
- return False
130
-
131
- # Validar coordenadas válidas para Cataluña
132
- latitude = entry_data.get("latitude")
133
- longitude = entry_data.get("longitude")
134
- if not (40.5 <= latitude <= 42.5 and 0.1 <= longitude <= 3.3): # Rango aproximado para Cataluña
135
- _LOGGER.warning(
136
- "Coordenadas inválidas (latitude: %s, longitude: %s). Usando coordenadas de Barcelona por defecto para MeteocatSunCoordinator.",
137
- latitude, longitude
138
- )
139
- entry_data = {
140
- **entry_data,
141
- "latitude": 41.38879,
142
- "longitude": 2.15899
143
- }
144
-
145
- # Crear los assets básicos si faltan
146
- await ensure_assets_exist(
147
- hass,
148
- api_key=entry_data["api_key"],
149
- town_id=entry_data.get("town_id"),
150
- variable_id=entry_data.get("variable_id"),
151
- )
152
-
153
- _LOGGER.debug(
154
- f"Datos de configuración: Municipio '{entry_data['town_name']}' (ID: {entry_data['town_id']}), "
155
- f"Variable '{entry_data['variable_name']}' (ID: {entry_data['variable_id']}), "
156
- f"Estación '{entry_data['station_name']}' (ID: {entry_data['station_id']}), "
157
- f"Provincia '{entry_data['province_name']}' (ID: {entry_data['province_id']}), "
158
- f"Comarca '{entry_data['region_name']}' (ID: {entry_data['region_id']}), "
159
- f"Coordenadas: ({entry_data['latitude']}, {entry_data['longitude']})."
160
- f"Altitud: ({entry_data['altitude']})."
161
- )
162
-
163
- # Lista de coordinadores con sus clases
164
- coordinator_configs = [
165
- ("sensor_coordinator", "MeteocatSensorCoordinator"),
166
- ("static_sensor_coordinator", "MeteocatStaticSensorCoordinator"),
167
- ("entity_coordinator", "MeteocatEntityCoordinator"),
168
- ("uvi_coordinator", "MeteocatUviCoordinator"),
169
- ("uvi_file_coordinator", "MeteocatUviFileCoordinator"),
170
- ("hourly_forecast_coordinator", "HourlyForecastCoordinator"),
171
- ("daily_forecast_coordinator", "DailyForecastCoordinator"),
172
- ("condition_coordinator", "MeteocatConditionCoordinator"),
173
- ("temp_forecast_coordinator", "MeteocatTempForecastCoordinator"),
174
- ("alerts_coordinator", "MeteocatAlertsCoordinator"),
175
- ("alerts_region_coordinator", "MeteocatAlertsRegionCoordinator"),
176
- ("quotes_coordinator", "MeteocatQuotesCoordinator"),
177
- ("quotes_file_coordinator", "MeteocatQuotesFileCoordinator"),
178
- ("lightning_coordinator", "MeteocatLightningCoordinator"),
179
- ("lightning_file_coordinator", "MeteocatLightningFileCoordinator"),
180
- ("sun_coordinator", "MeteocatSunCoordinator"),
181
- ("sun_file_coordinator", "MeteocatSunFileCoordinator"),
182
- ("moon_coordinator", "MeteocatMoonCoordinator"),
183
- ("moon_file_coordinator", "MeteocatMoonFileCoordinator"),
184
- ]
185
-
186
- hass.data.setdefault(DOMAIN, {})
187
- hass.data[DOMAIN][entry.entry_id] = {}
188
-
189
- try:
190
- for key, cls_name in coordinator_configs:
191
- # Importación lazy: importa la clase solo cuando sea necesario
192
- cls = await hass.async_add_executor_job(_get_coordinator_module, cls_name)
193
- coordinator = cls(hass=hass, entry_data=entry_data)
194
- await coordinator.async_config_entry_first_refresh()
195
- hass.data[DOMAIN][entry.entry_id][key] = coordinator
196
-
197
- except Exception as err:
198
- _LOGGER.exception("Error al inicializar los coordinadores: %s", err)
199
- return False
200
-
201
- hass.data[DOMAIN][entry.entry_id].update(entry_data)
202
-
203
- _LOGGER.debug(f"Cargando plataformas: {PLATFORMS}")
204
- await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
205
-
206
- return True
207
-
208
- async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
209
- """Desactiva una entrada de configuración para Meteocat."""
210
- platforms = async_get_platforms(hass, DOMAIN)
211
- _LOGGER.info(f"Descargando plataformas: {[p.domain for p in platforms]}")
212
-
213
- if entry.entry_id in hass.data.get(DOMAIN, {}):
214
- hass.data[DOMAIN].pop(entry.entry_id, None)
215
- if not hass.data[DOMAIN]:
216
- hass.data.pop(DOMAIN)
217
-
218
- return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
219
-
220
- async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
221
- """Limpia cualquier dato adicional al desinstalar la integración."""
222
- _LOGGER.info(f"Eliminando datos residuales de la integración: {entry.entry_id}")
223
-
224
- # Rutas persistentes en /config/meteocat_files
225
- base_folder = get_storage_dir(hass)
226
- assets_folder = get_storage_dir(hass, "assets")
227
- files_folder = get_storage_dir(hass, "files")
228
-
229
- # Archivos comunes (solo se eliminan si no queda ninguna entrada)
230
- common_files = [
231
- assets_folder / "towns.json",
232
- assets_folder / "symbols.json",
233
- assets_folder / "variables.json",
234
- assets_folder / "stations.json",
235
- files_folder / "alerts.json",
236
- files_folder / "quotes.json",
237
- ]
238
-
239
- # Identificadores de la entrada eliminada
240
- station_id = entry.data.get("station_id")
241
- town_id = entry.data.get("town_id")
242
- region_id = entry.data.get("region_id")
243
-
244
- specific_files = []
245
-
246
- # 1. Archivos de estación
247
- if station_id:
248
- other_entries_with_station = [
249
- e for e in hass.config_entries.async_entries(DOMAIN)
250
- if e.entry_id != entry.entry_id and e.data.get("station_id") == station_id
251
- ]
252
- if not other_entries_with_station:
253
- specific_files.append(files_folder / f"station_{station_id.lower()}_data.json")
254
-
255
- # 2. Archivos de municipio
256
- if town_id:
257
- other_entries_with_town = [
258
- e for e in hass.config_entries.async_entries(DOMAIN)
259
- if e.entry_id != entry.entry_id and e.data.get("town_id") == town_id
260
- ]
261
- if not other_entries_with_town:
262
- specific_files.extend([
263
- assets_folder / f"stations_{town_id.lower()}.json",
264
- files_folder / f"uvi_{town_id.lower()}_data.json",
265
- files_folder / f"forecast_{town_id.lower()}_hourly_data.json",
266
- files_folder / f"forecast_{town_id.lower()}_daily_data.json",
267
- files_folder / f"sun_{town_id.lower()}_data.json",
268
- files_folder / f"moon_{town_id.lower()}_data.json",
269
- ])
270
-
271
- # 3. Archivos de comarca (region_id)
272
- if region_id:
273
- other_entries_with_region = [
274
- e for e in hass.config_entries.async_entries(DOMAIN)
275
- if e.entry_id != entry.entry_id and e.data.get("region_id") == region_id
276
- ]
277
- if not other_entries_with_region:
278
- specific_files.extend([
279
- files_folder / f"alerts_{region_id}.json",
280
- files_folder / f"lightning_{region_id}.json",
281
- ])
282
-
283
- # Eliminar archivos específicos (solo si ya no los necesita nadie más)
284
- for f in specific_files:
285
- safe_remove(f)
286
-
287
- # Comprobar si quedan entradas activas de la integración
288
- remaining_entries = [
289
- e for e in hass.config_entries.async_entries(DOMAIN)
290
- if e.entry_id != entry.entry_id
291
- ]
292
- if not remaining_entries:
293
- for f in common_files:
294
- safe_remove(f)
295
-
296
- # Intentar eliminar carpetas vacías
297
- for folder in [assets_folder, files_folder, base_folder]:
298
- safe_remove(folder, is_folder=True)
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import voluptuous as vol
5
+ from pathlib import Path
6
+ import aiofiles
7
+ import json
8
+ import importlib # Para importaciones lazy
9
+
10
+ from homeassistant import core
11
+ from homeassistant.config_entries import ConfigEntry
12
+ from homeassistant.core import HomeAssistant
13
+ from homeassistant.exceptions import HomeAssistantError
14
+ from homeassistant.helpers.entity_platform import async_get_platforms
15
+ from homeassistant.helpers import config_validation as cv
16
+
17
+ from .helpers import get_storage_dir
18
+ from meteocatpy.town import MeteocatTown
19
+ from meteocatpy.symbols import MeteocatSymbols
20
+ from meteocatpy.variables import MeteocatVariables
21
+ from meteocatpy.townstations import MeteocatTownStations
22
+ from .const import DOMAIN, PLATFORMS
23
+
24
+ _LOGGER = logging.getLogger(__name__)
25
+
26
+ # Versión
27
+ __version__ = "4.0.0"
28
+
29
+ # Definir el esquema de configuración CONFIG_SCHEMA
30
+ CONFIG_SCHEMA = vol.Schema(
31
+ {
32
+ DOMAIN: vol.Schema(
33
+ {
34
+ vol.Required("api_key"): cv.string,
35
+ vol.Required("town_name"): cv.string,
36
+ vol.Required("town_id"): cv.string,
37
+ vol.Optional("variable_name", default="temperature"): cv.string,
38
+ vol.Required("variable_id"): cv.string,
39
+ vol.Optional("station_name"): cv.string,
40
+ vol.Optional("station_id"): cv.string,
41
+ vol.Optional("province_name"): cv.string,
42
+ vol.Optional("province_id"): cv.string,
43
+ vol.Optional("region_name"): cv.string,
44
+ vol.Optional("region_id"): cv.string,
45
+ vol.Required("latitude"): cv.latitude,
46
+ vol.Required("longitude"): cv.longitude,
47
+ vol.Required("altitude"): vol.Coerce(float),
48
+ }
49
+ )
50
+ },
51
+ extra=vol.ALLOW_EXTRA,
52
+ )
53
+
54
+ def safe_remove(path: Path, is_folder: bool = False) -> None:
55
+ """Elimina un archivo o carpeta vacía de forma segura."""
56
+ try:
57
+ if is_folder:
58
+ if path.exists() and path.is_dir():
59
+ path.rmdir() # Solo elimina si está vacía
60
+ _LOGGER.info("Carpeta eliminada: %s", path)
61
+ else:
62
+ if path.exists():
63
+ path.unlink()
64
+ _LOGGER.info("Archivo eliminado: %s", path)
65
+ except Exception as e:
66
+ _LOGGER.error("Error eliminando %s: %s", path, e)
67
+
68
+ async def ensure_assets_exist(hass, api_key, town_id=None, variable_id=None):
69
+ """Comprueba y crea los assets básicos si faltan."""
70
+ assets_dir = get_storage_dir(hass, "assets")
71
+ assets_dir.mkdir(parents=True, exist_ok=True)
72
+
73
+ # Lista de assets: (nombre_archivo, fetch_func, clave_json, args)
74
+ assets = [
75
+ ("towns.json", MeteocatTown(api_key).get_municipis, "towns", []),
76
+ ("stations.json", MeteocatTownStations(api_key).stations_service.get_stations, "stations", []),
77
+ ("variables.json", MeteocatVariables(api_key).get_variables, "variables", []),
78
+ ("symbols.json", MeteocatSymbols(api_key).fetch_symbols, "symbols", []),
79
+ ]
80
+
81
+ # Si tenemos town_id y variable_id, agregamos stations_<town_id>.json
82
+ if town_id and variable_id:
83
+ assets.append(
84
+ (f"stations_{town_id}.json", MeteocatTownStations(api_key).get_town_stations, "town_stations", [town_id, variable_id])
85
+ )
86
+
87
+ for filename, fetch_func, key, args in assets:
88
+ file_path = assets_dir / filename
89
+ if not file_path.exists():
90
+ _LOGGER.debug("Intentando descargar datos para %s desde la API con args: %s", key, args)
91
+ try:
92
+ data = await fetch_func(*args)
93
+ except Exception as ex:
94
+ _LOGGER.warning(
95
+ "No se pudieron obtener los datos para %s. Intenta regenerarlo más adelante desde las opciones de la integración. Detalle: %s",
96
+ key,
97
+ ex,
98
+ )
99
+ data = []
100
+ async with aiofiles.open(file_path, "w", encoding="utf-8") as file:
101
+ await file.write(json.dumps({key: data}, ensure_ascii=False, indent=4))
102
+ _LOGGER.info("Archivo creado: %s", file_path)
103
+
104
+ async def async_setup(hass: core.HomeAssistant, config: dict) -> bool:
105
+ """Configuración inicial del componente Meteocat."""
106
+ return True
107
+
108
+ def _get_coordinator_module(cls_name: str):
109
+ """Importa dinámicamente un coordinador para evitar blocking imports."""
110
+ module = importlib.import_module(".coordinator", "custom_components.meteocat")
111
+ return getattr(module, cls_name)
112
+
113
+ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
114
+ """Configura una entrada de configuración para Meteocat."""
115
+ _LOGGER.info("Configurando la integración de Meteocat...")
116
+
117
+ # Extraer los datos necesarios de la entrada de configuración
118
+ entry_data = entry.data
119
+
120
+ # Validar campos requeridos
121
+ required_fields = [
122
+ "api_key", "town_name", "town_id", "variable_name",
123
+ "variable_id", "station_name", "station_id", "province_name",
124
+ "province_id", "region_name", "region_id", "latitude", "longitude", "altitude"
125
+ ]
126
+ missing_fields = [field for field in required_fields if field not in entry_data]
127
+ if missing_fields:
128
+ _LOGGER.error(f"Faltan los siguientes campos en la configuración: {missing_fields}")
129
+ return False
130
+
131
+ # Validar coordenadas válidas para Cataluña
132
+ latitude = entry_data.get("latitude")
133
+ longitude = entry_data.get("longitude")
134
+ if not (40.5 <= latitude <= 42.5 and 0.1 <= longitude <= 3.3): # Rango aproximado para Cataluña
135
+ _LOGGER.warning(
136
+ "Coordenadas inválidas (latitude: %s, longitude: %s). Usando coordenadas de Barcelona por defecto para MeteocatSunCoordinator.",
137
+ latitude, longitude
138
+ )
139
+ entry_data = {
140
+ **entry_data,
141
+ "latitude": 41.38879,
142
+ "longitude": 2.15899
143
+ }
144
+
145
+ # Crear los assets básicos si faltan
146
+ await ensure_assets_exist(
147
+ hass,
148
+ api_key=entry_data["api_key"],
149
+ town_id=entry_data.get("town_id"),
150
+ variable_id=entry_data.get("variable_id"),
151
+ )
152
+
153
+ _LOGGER.debug(
154
+ f"Datos de configuración: Municipio '{entry_data['town_name']}' (ID: {entry_data['town_id']}), "
155
+ f"Variable '{entry_data['variable_name']}' (ID: {entry_data['variable_id']}), "
156
+ f"Estación '{entry_data['station_name']}' (ID: {entry_data['station_id']}), "
157
+ f"Provincia '{entry_data['province_name']}' (ID: {entry_data['province_id']}), "
158
+ f"Comarca '{entry_data['region_name']}' (ID: {entry_data['region_id']}), "
159
+ f"Coordenadas: ({entry_data['latitude']}, {entry_data['longitude']})."
160
+ f"Altitud: ({entry_data['altitude']})."
161
+ )
162
+
163
+ # Lista de coordinadores con sus clases
164
+ coordinator_configs = [
165
+ ("sensor_coordinator", "MeteocatSensorCoordinator"),
166
+ ("static_sensor_coordinator", "MeteocatStaticSensorCoordinator"),
167
+ ("entity_coordinator", "MeteocatEntityCoordinator"),
168
+ ("uvi_coordinator", "MeteocatUviCoordinator"),
169
+ ("uvi_file_coordinator", "MeteocatUviFileCoordinator"),
170
+ ("hourly_forecast_coordinator", "HourlyForecastCoordinator"),
171
+ ("daily_forecast_coordinator", "DailyForecastCoordinator"),
172
+ ("condition_coordinator", "MeteocatConditionCoordinator"),
173
+ ("temp_forecast_coordinator", "MeteocatTempForecastCoordinator"),
174
+ ("alerts_coordinator", "MeteocatAlertsCoordinator"),
175
+ ("alerts_region_coordinator", "MeteocatAlertsRegionCoordinator"),
176
+ ("quotes_coordinator", "MeteocatQuotesCoordinator"),
177
+ ("quotes_file_coordinator", "MeteocatQuotesFileCoordinator"),
178
+ ("lightning_coordinator", "MeteocatLightningCoordinator"),
179
+ ("lightning_file_coordinator", "MeteocatLightningFileCoordinator"),
180
+ ("sun_coordinator", "MeteocatSunCoordinator"),
181
+ ("sun_file_coordinator", "MeteocatSunFileCoordinator"),
182
+ ("moon_coordinator", "MeteocatMoonCoordinator"),
183
+ ("moon_file_coordinator", "MeteocatMoonFileCoordinator"),
184
+ ]
185
+
186
+ hass.data.setdefault(DOMAIN, {})
187
+ hass.data[DOMAIN][entry.entry_id] = {}
188
+
189
+ try:
190
+ for key, cls_name in coordinator_configs:
191
+ # Importación lazy: importa la clase solo cuando sea necesario
192
+ cls = await hass.async_add_executor_job(_get_coordinator_module, cls_name)
193
+ coordinator = cls(hass=hass, entry_data=entry_data)
194
+ await coordinator.async_config_entry_first_refresh()
195
+ hass.data[DOMAIN][entry.entry_id][key] = coordinator
196
+
197
+ except Exception as err:
198
+ _LOGGER.exception("Error al inicializar los coordinadores: %s", err)
199
+ return False
200
+
201
+ hass.data[DOMAIN][entry.entry_id].update(entry_data)
202
+
203
+ _LOGGER.debug(f"Cargando plataformas: {PLATFORMS}")
204
+ await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
205
+
206
+ return True
207
+
208
+ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
209
+ """Desactiva una entrada de configuración para Meteocat."""
210
+ platforms = async_get_platforms(hass, DOMAIN)
211
+ _LOGGER.info(f"Descargando plataformas: {[p.domain for p in platforms]}")
212
+
213
+ if entry.entry_id in hass.data.get(DOMAIN, {}):
214
+ hass.data[DOMAIN].pop(entry.entry_id, None)
215
+ if not hass.data[DOMAIN]:
216
+ hass.data.pop(DOMAIN)
217
+
218
+ return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
219
+
220
+ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
221
+ """Limpia cualquier dato adicional al desinstalar la integración."""
222
+ _LOGGER.info(f"Eliminando datos residuales de la integración: {entry.entry_id}")
223
+
224
+ # Rutas persistentes en /config/meteocat_files
225
+ base_folder = get_storage_dir(hass)
226
+ assets_folder = get_storage_dir(hass, "assets")
227
+ files_folder = get_storage_dir(hass, "files")
228
+
229
+ # Archivos comunes (solo se eliminan si no queda ninguna entrada)
230
+ common_files = [
231
+ assets_folder / "towns.json",
232
+ assets_folder / "symbols.json",
233
+ assets_folder / "variables.json",
234
+ assets_folder / "stations.json",
235
+ files_folder / "alerts.json",
236
+ files_folder / "quotes.json",
237
+ ]
238
+
239
+ # Identificadores de la entrada eliminada
240
+ station_id = entry.data.get("station_id")
241
+ town_id = entry.data.get("town_id")
242
+ region_id = entry.data.get("region_id")
243
+
244
+ specific_files = []
245
+
246
+ # 1. Archivos de estación
247
+ if station_id:
248
+ other_entries_with_station = [
249
+ e for e in hass.config_entries.async_entries(DOMAIN)
250
+ if e.entry_id != entry.entry_id and e.data.get("station_id") == station_id
251
+ ]
252
+ if not other_entries_with_station:
253
+ specific_files.append(files_folder / f"station_{station_id.lower()}_data.json")
254
+
255
+ # 2. Archivos de municipio
256
+ if town_id:
257
+ other_entries_with_town = [
258
+ e for e in hass.config_entries.async_entries(DOMAIN)
259
+ if e.entry_id != entry.entry_id and e.data.get("town_id") == town_id
260
+ ]
261
+ if not other_entries_with_town:
262
+ specific_files.extend([
263
+ assets_folder / f"stations_{town_id.lower()}.json",
264
+ files_folder / f"uvi_{town_id.lower()}_data.json",
265
+ files_folder / f"forecast_{town_id.lower()}_hourly_data.json",
266
+ files_folder / f"forecast_{town_id.lower()}_daily_data.json",
267
+ files_folder / f"sun_{town_id.lower()}_data.json",
268
+ files_folder / f"moon_{town_id.lower()}_data.json",
269
+ ])
270
+
271
+ # 3. Archivos de comarca (region_id)
272
+ if region_id:
273
+ other_entries_with_region = [
274
+ e for e in hass.config_entries.async_entries(DOMAIN)
275
+ if e.entry_id != entry.entry_id and e.data.get("region_id") == region_id
276
+ ]
277
+ if not other_entries_with_region:
278
+ specific_files.extend([
279
+ files_folder / f"alerts_{region_id}.json",
280
+ files_folder / f"lightning_{region_id}.json",
281
+ ])
282
+
283
+ # Eliminar archivos específicos (solo si ya no los necesita nadie más)
284
+ for f in specific_files:
285
+ safe_remove(f)
286
+
287
+ # Comprobar si quedan entradas activas de la integración
288
+ remaining_entries = [
289
+ e for e in hass.config_entries.async_entries(DOMAIN)
290
+ if e.entry_id != entry.entry_id
291
+ ]
292
+ if not remaining_entries:
293
+ for f in common_files:
294
+ safe_remove(f)
295
+
296
+ # Intentar eliminar carpetas vacías
297
+ for folder in [assets_folder, files_folder, base_folder]:
298
+ safe_remove(folder, is_folder=True)