meteocat 2.2.7 → 3.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.
@@ -1,11 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
- import os
4
3
  import json
5
4
  import aiofiles
6
5
  import logging
7
6
  import asyncio
8
7
  import unicodedata
8
+ from pathlib import Path
9
9
  from datetime import datetime, timedelta, timezone, time
10
10
  from zoneinfo import ZoneInfo
11
11
  from typing import Dict, Any
@@ -30,6 +30,7 @@ from meteocatpy.exceptions import (
30
30
  UnknownAPIError,
31
31
  )
32
32
 
33
+ from .helpers import get_storage_dir
33
34
  from .condition import get_condition_from_statcel
34
35
  from .const import (
35
36
  DOMAIN,
@@ -70,28 +71,17 @@ DEFAULT_LIGHTNING_FILE_UPDATE_INTERVAL = timedelta(minutes=5)
70
71
  # Definir la zona horaria local
71
72
  TIMEZONE = ZoneInfo("Europe/Madrid")
72
73
 
73
- async def save_json_to_file(data: dict, output_file: str) -> None:
74
+ async def save_json_to_file(data: dict, output_file: Path) -> None:
74
75
  """Guarda datos JSON en un archivo de forma asíncrona."""
75
76
  try:
76
- # Crea el directorio si no existe
77
- os.makedirs(os.path.dirname(output_file), exist_ok=True)
78
-
79
- # Escribe los datos JSON de forma asíncrona
77
+ output_file.parent.mkdir(parents=True, exist_ok=True)
80
78
  async with aiofiles.open(output_file, mode="w", encoding="utf-8") as f:
81
79
  await f.write(json.dumps(data, indent=4, ensure_ascii=False))
82
80
  except Exception as e:
83
- raise RuntimeError(f"Error guardando JSON to {output_file}: {e}")
81
+ raise RuntimeError(f"Error guardando JSON en {output_file}: {e}")
84
82
 
85
- async def load_json_from_file(input_file: str) -> dict:
86
- """
87
- Carga un archivo JSON de forma asincrónica.
88
-
89
- Args:
90
- input_file (str): Ruta del archivo JSON.
91
-
92
- Returns:
93
- dict: Datos JSON cargados.
94
- """
83
+ async def load_json_from_file(input_file: Path) -> dict:
84
+ """Carga un archivo JSON de forma asincrónica."""
95
85
  try:
96
86
  async with aiofiles.open(input_file, "r", encoding="utf-8") as f:
97
87
  data = await f.read()
@@ -103,32 +93,31 @@ async def load_json_from_file(input_file: str) -> dict:
103
93
  _LOGGER.error("Error al decodificar JSON del archivo %s: %s", input_file, err)
104
94
  return {}
105
95
 
106
- def normalize_name(name):
96
+ def normalize_name(name: str) -> str:
107
97
  """Normaliza el nombre eliminando acentos y convirtiendo a minúsculas."""
108
98
  name = unicodedata.normalize("NFKD", name).encode("ASCII", "ignore").decode("utf-8")
109
99
  return name.lower()
110
100
 
111
- # Definir _quotes_lock para evitar que varios coordinadores inicien una carrera para modificar quotes.json al mismo tiempo
101
+ # Definir _quotes_lock para evitar que varios coordinadores modifiquen quotes.json al mismo tiempo
112
102
  _quotes_lock = asyncio.Lock()
113
103
 
114
104
  async def _update_quotes(hass: HomeAssistant, plan_name: str) -> None:
115
105
  """Actualiza las cuotas en quotes.json después de una consulta."""
116
106
  async with _quotes_lock:
117
- quotes_file = hass.config.path(
118
- "custom_components", "meteocat", "files", "quotes.json"
119
- )
107
+ # Ruta persistente en /config/meteocat_files/files
108
+ files_folder = get_storage_dir(hass, "files")
109
+ quotes_file = files_folder / "quotes.json"
110
+
120
111
  try:
121
112
  data = await load_json_from_file(quotes_file)
122
-
123
- # Validar estructura del archivo
113
+
124
114
  if not data or not isinstance(data, dict):
125
115
  _LOGGER.warning("quotes.json está vacío o tiene un formato inválido: %s", data)
126
116
  return
127
117
  if "plans" not in data or not isinstance(data["plans"], list):
128
118
  _LOGGER.warning("Estructura inesperada en quotes.json: %s", data)
129
119
  return
130
-
131
- # Buscar el plan y actualizar las cuotas
120
+
132
121
  for plan in data["plans"]:
133
122
  if plan.get("nom") == plan_name:
134
123
  plan["consultesRealitzades"] += 1
@@ -137,9 +126,8 @@ async def _update_quotes(hass: HomeAssistant, plan_name: str) -> None:
137
126
  "Cuota actualizada para el plan %s: Consultas realizadas %s, restantes %s",
138
127
  plan_name, plan["consultesRealitzades"], plan["consultesRestants"]
139
128
  )
140
- break # Salimos del bucle al encontrar el plan
129
+ break
141
130
 
142
- # Guardar cambios en el archivo
143
131
  await save_json_to_file(data, quotes_file)
144
132
 
145
133
  except FileNotFoundError:
@@ -163,24 +151,19 @@ class MeteocatSensorCoordinator(DataUpdateCoordinator):
163
151
  Args:
164
152
  hass (HomeAssistant): Instancia de Home Assistant.
165
153
  entry_data (dict): Datos de configuración obtenidos de core.config_entries.
166
- update_interval (timedelta): Intervalo de actualización.
167
154
  """
168
- self.api_key = entry_data["api_key"] # Usamos la API key de la configuración
169
- self.town_name = entry_data["town_name"] # Usamos el nombre del municipio
170
- self.town_id = entry_data["town_id"] # Usamos el ID del municipio
171
- self.station_name = entry_data["station_name"] # Usamos el nombre de la estación
172
- self.station_id = entry_data["station_id"] # Usamos el ID de la estación
173
- self.variable_name = entry_data["variable_name"] # Usamos el nombre de la variable
174
- self.variable_id = entry_data["variable_id"] # Usamos el ID de la variable
155
+ self.api_key = entry_data["api_key"]
156
+ self.town_name = entry_data["town_name"]
157
+ self.town_id = entry_data["town_id"]
158
+ self.station_name = entry_data["station_name"]
159
+ self.station_id = entry_data["station_id"]
160
+ self.variable_name = entry_data["variable_name"]
161
+ self.variable_id = entry_data["variable_id"]
175
162
  self.meteocat_station_data = MeteocatStationData(self.api_key)
176
163
 
177
- self.station_file = os.path.join(
178
- hass.config.path(),
179
- "custom_components",
180
- "meteocat",
181
- "files",
182
- f"station_{self.station_id.lower()}_data.json"
183
- )
164
+ # Ruta persistente en /config/meteocat_files/files
165
+ files_folder = get_storage_dir(hass, "files")
166
+ self.station_file = files_folder / f"station_{self.station_id.lower()}_data.json"
184
167
 
185
168
  super().__init__(
186
169
  hass,
@@ -192,17 +175,15 @@ class MeteocatSensorCoordinator(DataUpdateCoordinator):
192
175
  async def _async_update_data(self) -> Dict:
193
176
  """Actualiza los datos de los sensores desde la API de Meteocat."""
194
177
  try:
195
- # Obtener datos desde la API con manejo de tiempo límite
196
178
  data = await asyncio.wait_for(
197
179
  self.meteocat_station_data.get_station_data(self.station_id),
198
- timeout=30 # Tiempo límite de 30 segundos
180
+ timeout=30
199
181
  )
200
182
  _LOGGER.debug("Datos de sensores actualizados exitosamente: %s", data)
201
183
 
202
- # Actualizar las cuotas usando la función externa
203
- await _update_quotes(self.hass, "XEMA") # Asegúrate de usar el nombre correcto del plan aquí
184
+ # Actualizar las cuotas
185
+ await _update_quotes(self.hass, "XEMA")
204
186
 
205
- # Validar que los datos sean una lista de diccionarios
206
187
  if not isinstance(data, list) or not all(isinstance(item, dict) for item in data):
207
188
  _LOGGER.error(
208
189
  "Formato inválido: Se esperaba una lista de dicts, pero se obtuvo %s. Datos: %s",
@@ -211,10 +192,11 @@ class MeteocatSensorCoordinator(DataUpdateCoordinator):
211
192
  )
212
193
  raise ValueError("Formato de datos inválido")
213
194
 
214
- # Guardar los datos en un archivo JSON
195
+ # Guardar datos en JSON persistente
215
196
  await save_json_to_file(data, self.station_file)
216
197
 
217
198
  return data
199
+
218
200
  except asyncio.TimeoutError as err:
219
201
  _LOGGER.warning("Tiempo de espera agotado al obtener datos de la API de Meteocat.")
220
202
  raise ConfigEntryNotReady from err
@@ -241,28 +223,27 @@ class MeteocatSensorCoordinator(DataUpdateCoordinator):
241
223
  raise
242
224
  except Exception as err:
243
225
  if isinstance(err, ConfigEntryNotReady):
244
- # El dispositivo no pudo inicializarse por primera vez
245
226
  _LOGGER.exception(
246
- "No se pudo inicializar el dispositivo (Station ID: %s) debido a un error: %s",
227
+ "No se pudo inicializar el dispositivo (Station ID: %s): %s",
247
228
  self.station_id,
248
229
  err,
249
230
  )
250
- raise # Re-raise the exception to indicate a fundamental failure in initialization
231
+ raise
251
232
  else:
252
- # Manejar error durante la actualización de datos
253
233
  _LOGGER.exception(
254
- "Error inesperado al obtener datos de los sensores para (Station ID: %s): %s",
234
+ "Error inesperado al obtener datos de los sensores (Station ID: %s): %s",
255
235
  self.station_id,
256
236
  err,
257
237
  )
258
- # Intentar cargar datos en caché si hay un error
238
+
239
+ # Cargar datos en caché si la API falla
259
240
  cached_data = await load_json_from_file(self.station_file)
260
241
  if cached_data:
261
242
  _LOGGER.warning("Usando datos en caché para la estación %s.", self.station_id)
262
243
  return cached_data
263
- # No se puede actualizar el estado, retornar None
244
+
264
245
  _LOGGER.error("No se pudo obtener datos actualizados ni cargar datos en caché.")
265
- return None # o cualquier otro valor que indique un estado de error
246
+ return None
266
247
 
267
248
  class MeteocatStaticSensorCoordinator(DataUpdateCoordinator):
268
249
  """Coordinator to manage and update static sensor data."""
@@ -272,20 +253,12 @@ class MeteocatStaticSensorCoordinator(DataUpdateCoordinator):
272
253
  hass: HomeAssistant,
273
254
  entry_data: dict,
274
255
  ):
275
- """
276
- Initialize the MeteocatStaticSensorCoordinator.
277
-
278
- Args:
279
- hass (HomeAssistant): Home Assistant instance.
280
- entry_data (dict): Configuration data from core.config_entries.
281
- update_interval (timedelta): Update interval for the coordinator.
282
- """
283
- self.town_name = entry_data["town_name"] # Nombre del municipio
284
- self.town_id = entry_data["town_id"] # ID del municipio
285
- self.station_name = entry_data["station_name"] # Nombre de la estación
286
- self.station_id = entry_data["station_id"] # ID de la estación
287
- self.region_name = entry_data["region_name"] # Nombre de la región
288
- self.region_id = entry_data["region_id"] # ID de la región
256
+ self.town_name = entry_data["town_name"]
257
+ self.town_id = entry_data["town_id"]
258
+ self.station_name = entry_data["station_name"]
259
+ self.station_id = entry_data["station_id"]
260
+ self.region_name = entry_data["region_name"]
261
+ self.region_id = entry_data["region_id"]
289
262
 
290
263
  super().__init__(
291
264
  hass,
@@ -295,13 +268,9 @@ class MeteocatStaticSensorCoordinator(DataUpdateCoordinator):
295
268
  )
296
269
 
297
270
  async def _async_update_data(self):
298
- """
299
- Fetch and return static sensor data.
300
-
301
- Since static sensors use entry_data, this method simply logs the process.
302
- """
271
+ """Retorna los datos estáticos (no necesita archivos)."""
303
272
  _LOGGER.debug(
304
- "Updating static sensor data for town: %s (ID: %s), station: %s (ID: %s), region: %s (ID: %s)",
273
+ "Updating static sensor data: town %s (ID %s), station %s (ID %s), region %s (ID %s)",
305
274
  self.town_name,
306
275
  self.town_id,
307
276
  self.station_name,
@@ -326,24 +295,13 @@ class MeteocatUviCoordinator(DataUpdateCoordinator):
326
295
  hass: HomeAssistant,
327
296
  entry_data: dict,
328
297
  ):
329
- """
330
- Inicializa el coordinador del Índice UV de Meteocat.
331
-
332
- Args:
333
- hass (HomeAssistant): Instancia de Home Assistant.
334
- entry_data (dict): Datos de configuración obtenidos de core.config_entries.
335
- update_interval (timedelta): Intervalo de actualización.
336
- """
337
- self.api_key = entry_data["api_key"] # Usamos la API key de la configuración
338
- self.town_id = entry_data["town_id"] # Usamos el ID del municipio
298
+ self.api_key = entry_data["api_key"]
299
+ self.town_id = entry_data["town_id"]
339
300
  self.meteocat_uvi_data = MeteocatUviData(self.api_key)
340
- self.uvi_file = os.path.join(
341
- hass.config.path(),
342
- "custom_components",
343
- "meteocat",
344
- "files",
345
- f"uvi_{self.town_id.lower()}_data.json"
346
- )
301
+
302
+ # Ruta persistente en /config/meteocat_files/files
303
+ files_folder = get_storage_dir(hass, "files")
304
+ self.uvi_file = files_folder / f"uvi_{self.town_id.lower()}_data.json"
347
305
 
348
306
  super().__init__(
349
307
  hass,
@@ -352,10 +310,10 @@ class MeteocatUviCoordinator(DataUpdateCoordinator):
352
310
  update_interval=DEFAULT_UVI_UPDATE_INTERVAL,
353
311
  )
354
312
 
355
- async def is_uvi_data_valid(self) -> dict:
313
+ async def is_uvi_data_valid(self) -> dict | None:
356
314
  """Comprueba si el archivo JSON contiene datos válidos para el día actual y devuelve los datos si son válidos."""
357
315
  try:
358
- if not os.path.exists(self.uvi_file):
316
+ if not self.uvi_file.exists():
359
317
  _LOGGER.info("El archivo %s no existe. Se considerará inválido.", self.uvi_file)
360
318
  return None
361
319
 
@@ -363,29 +321,39 @@ class MeteocatUviCoordinator(DataUpdateCoordinator):
363
321
  content = await file.read()
364
322
  data = json.loads(content)
365
323
 
366
- # Validar la fecha del primer elemento superior a 1 día
367
- first_date = datetime.strptime(data["uvi"][0].get("date"), "%Y-%m-%d").date()
324
+ # Validaciones de estructura
325
+ if not isinstance(data, dict) or "uvi" not in data or not isinstance(data["uvi"], list) or not data["uvi"]:
326
+ _LOGGER.warning("Estructura inválida o sin datos en %s: %s", self.uvi_file, data)
327
+ return None
328
+
329
+ # Obtener la fecha del primer elemento con protección
330
+ try:
331
+ first_date = datetime.strptime(data["uvi"][0].get("date"), "%Y-%m-%d").date()
332
+ except Exception as exc:
333
+ _LOGGER.warning("Fecha inválida en %s: %s", self.uvi_file, exc)
334
+ return None
335
+
368
336
  today = datetime.now(timezone.utc).date()
369
337
  current_time = datetime.now(timezone.utc).time()
370
338
 
371
- # Log detallado
372
- _LOGGER.info(
373
- "Validando datos UVI en %s: Fecha de hoy: %s, Fecha del primer elemento: %s",
339
+ _LOGGER.debug(
340
+ "Validando datos UVI en %s: Fecha de hoy: %s, Fecha del primer elemento: %s, Hora actual: %s",
374
341
  self.uvi_file,
375
342
  today,
376
343
  first_date,
377
344
  current_time,
378
345
  )
379
346
 
380
- # Verificar si la antigüedad es mayor a un día
381
347
  if (today - first_date).days > DEFAULT_VALIDITY_DAYS and current_time >= time(DEFAULT_VALIDITY_HOURS, DEFAULT_VALIDITY_MINUTES):
382
- _LOGGER.info(
383
- "Los datos en %s son antiguos. Se procederá a llamar a la API.",
384
- self.uvi_file,
385
- )
348
+ _LOGGER.info("Los datos en %s son antiguos. Se procederá a llamar a la API.", self.uvi_file)
386
349
  return None
350
+
387
351
  _LOGGER.info("Los datos en %s son válidos. Se usarán sin llamar a la API.", self.uvi_file)
388
352
  return data
353
+
354
+ except json.JSONDecodeError:
355
+ _LOGGER.error("El archivo %s contiene JSON inválido o está corrupto.", self.uvi_file)
356
+ return None
389
357
  except Exception as e:
390
358
  _LOGGER.error("Error al validar el archivo JSON del índice UV: %s", e)
391
359
  return None
@@ -393,70 +361,50 @@ class MeteocatUviCoordinator(DataUpdateCoordinator):
393
361
  async def _async_update_data(self) -> Dict:
394
362
  """Actualiza los datos de UVI desde la API de Meteocat."""
395
363
  try:
396
- # Validar el archivo JSON existente
397
364
  valid_data = await self.is_uvi_data_valid()
398
365
  if valid_data:
399
- _LOGGER.info("Los datos del índice UV están actualizados. No se realiza llamada a la API.")
400
- return valid_data['uvi']
366
+ _LOGGER.debug("Los datos del índice UV están actualizados. No se realiza llamada a la API.")
367
+ return valid_data["uvi"]
401
368
 
402
- # Obtener datos desde la API con manejo de tiempo límite
403
369
  data = await asyncio.wait_for(
404
370
  self.meteocat_uvi_data.get_uvi_index(self.town_id),
405
- timeout=30 # Tiempo límite de 30 segundos
371
+ timeout=30,
406
372
  )
407
- _LOGGER.debug("Datos actualizados exitosamente: %s", data)
373
+ _LOGGER.debug("Datos UVI obtenidos desde API: %s", data)
408
374
 
409
- # Actualizar las cuotas usando la función externa
410
- await _update_quotes(self.hass, "Prediccio") # Asegúrate de usar el nombre correcto del plan aquí
375
+ await _update_quotes(self.hass, "Prediccio")
411
376
 
412
- # Validar que los datos sean un dict con una clave 'uvi'
413
- if not isinstance(data, dict) or 'uvi' not in data:
414
- _LOGGER.error("Formato inválido: Se esperaba un dict con la clave 'uvi'. Datos: %s", data)
377
+ if not isinstance(data, dict) or "uvi" not in data or not isinstance(data["uvi"], list):
378
+ _LOGGER.error("Formato inválido: se esperaba un dict con 'uvi' -> %s", data)
415
379
  raise ValueError("Formato de datos inválido")
416
380
 
417
- # Guardar los datos en un archivo JSON
418
381
  await save_json_to_file(data, self.uvi_file)
382
+ return data["uvi"]
419
383
 
420
- return data['uvi']
421
384
  except asyncio.TimeoutError as err:
422
- _LOGGER.warning("Tiempo de espera agotado al obtener datos de la API de Meteocat.")
385
+ _LOGGER.warning("Tiempo de espera agotado al obtener datos UVI.")
423
386
  raise ConfigEntryNotReady from err
424
387
  except ForbiddenError as err:
425
- _LOGGER.error(
426
- "Acceso denegado al obtener datos del índice UV para (Town ID: %s): %s",
427
- self.town_id,
428
- err,
429
- )
388
+ _LOGGER.error("Acceso denegado al obtener datos UVI para town %s: %s", self.town_id, err)
430
389
  raise ConfigEntryNotReady from err
431
390
  except TooManyRequestsError as err:
432
- _LOGGER.warning(
433
- "Límite de solicitudes alcanzado al obtener datos del índice UV para (Town ID: %s): %s",
434
- self.town_id,
435
- err,
436
- )
391
+ _LOGGER.warning("Límite de solicitudes alcanzado al obtener datos UVI para town %s: %s", self.town_id, err)
437
392
  raise ConfigEntryNotReady from err
438
393
  except (BadRequestError, InternalServerError, UnknownAPIError) as err:
439
- _LOGGER.error(
440
- "Error al obtener datos del índice UV para (Town ID: %s): %s",
441
- self.town_id,
442
- err,
443
- )
394
+ _LOGGER.error("Error API al obtener datos UVI para town %s: %s", self.town_id, err)
444
395
  raise
445
396
  except Exception as err:
446
- _LOGGER.exception(
447
- "Error inesperado al obtener datos del índice UV para (Town ID: %s): %s",
448
- self.town_id,
449
- err,
450
- )
451
- # Intentar cargar datos en caché si hay un error
397
+ _LOGGER.exception("Error inesperado al obtener datos del índice UV para %s: %s", self.town_id, err)
398
+
399
+ # Fallback a caché en disco
452
400
  cached_data = await load_json_from_file(self.uvi_file)
453
401
  if cached_data:
454
402
  _LOGGER.warning("Usando datos en caché para la ciudad %s.", self.town_id)
455
- return cached_data.get('uvi', [])
456
- # No se puede actualizar el estado, retornar None
457
- _LOGGER.error("No se pudo obtener datos actualizados ni cargar datos en caché.")
403
+ return cached_data.get("uvi", [])
404
+ _LOGGER.error("No se pudo obtener datos UVI ni cargar caché.")
458
405
  return None
459
406
 
407
+
460
408
  class MeteocatUviFileCoordinator(DataUpdateCoordinator):
461
409
  """Coordinator to read and process UV data from a file."""
462
410
 
@@ -465,15 +413,7 @@ class MeteocatUviFileCoordinator(DataUpdateCoordinator):
465
413
  hass: HomeAssistant,
466
414
  entry_data: dict,
467
415
  ):
468
- """
469
- Inicializa el coordinador del sensor del Índice UV de Meteocat.
470
-
471
- Args:
472
- hass (HomeAssistant): Instancia de Home Assistant.
473
- entry_data (dict): Datos de configuración obtenidos de core.config_entries.
474
- update_interval (timedelta): Intervalo de actualización.
475
- """
476
- self.town_id = entry_data["town_id"] # Usamos el ID del municipio
416
+ self.town_id = entry_data["town_id"]
477
417
 
478
418
  super().__init__(
479
419
  hass,
@@ -481,28 +421,22 @@ class MeteocatUviFileCoordinator(DataUpdateCoordinator):
481
421
  name=f"{DOMAIN} Uvi File Coordinator",
482
422
  update_interval=DEFAULT_UVI_SENSOR_UPDATE_INTERVAL,
483
423
  )
484
- self._file_path = os.path.join(
485
- hass.config.path("custom_components/meteocat/files"),
486
- f"uvi_{self.town_id.lower()}_data.json",
487
- )
424
+
425
+ # Ruta persistente en /config/meteocat_files/files
426
+ files_folder = get_storage_dir(hass, "files")
427
+ self._file_path = files_folder / f"uvi_{self.town_id.lower()}_data.json"
488
428
 
489
429
  async def _async_update_data(self):
490
430
  """Read and process UV data for the current hour from the file asynchronously."""
491
431
  try:
492
432
  async with aiofiles.open(self._file_path, "r", encoding="utf-8") as file:
493
- raw_data = await file.read()
494
- raw_data = json.loads(raw_data) # Parse JSON data
433
+ raw = await file.read()
434
+ raw_data = json.loads(raw)
495
435
  except FileNotFoundError:
496
- _LOGGER.error(
497
- "No se ha encontrado el archivo JSON con datos del índice UV en %s.",
498
- self._file_path,
499
- )
436
+ _LOGGER.error("No se ha encontrado el archivo JSON con datos del índice UV en %s.", self._file_path)
500
437
  return {}
501
438
  except json.JSONDecodeError:
502
- _LOGGER.error(
503
- "Error al decodificar el archivo JSON del índice UV en %s.",
504
- self._file_path,
505
- )
439
+ _LOGGER.error("Error al decodificar el archivo JSON del índice UV en %s.", self._file_path)
506
440
  return {}
507
441
 
508
442
  return self._get_uv_for_current_hour(raw_data)
@@ -516,10 +450,10 @@ class MeteocatUviFileCoordinator(DataUpdateCoordinator):
516
450
 
517
451
  # Busca los datos para la fecha actual
518
452
  for day_data in raw_data.get("uvi", []):
519
- if day_data["date"] == current_date:
453
+ if day_data.get("date") == current_date:
520
454
  # Encuentra los datos de la hora actual
521
455
  for hour_data in day_data.get("hours", []):
522
- if hour_data["hour"] == current_hour:
456
+ if hour_data.get("hour") == current_hour:
523
457
  return {
524
458
  "hour": hour_data.get("hour", 0),
525
459
  "uvi": hour_data.get("uvi", 0),
@@ -548,7 +482,6 @@ class MeteocatEntityCoordinator(DataUpdateCoordinator):
548
482
  Args:
549
483
  hass (HomeAssistant): Instancia de Home Assistant.
550
484
  entry_data (dict): Datos de configuración obtenidos de core.config_entries.
551
- update_interval (timedelta): Intervalo de actualización.
552
485
  """
553
486
  self.api_key = entry_data["api_key"]
554
487
  self.town_name = entry_data["town_name"]
@@ -559,20 +492,10 @@ class MeteocatEntityCoordinator(DataUpdateCoordinator):
559
492
  self.variable_id = entry_data["variable_id"]
560
493
  self.meteocat_forecast = MeteocatForecast(self.api_key)
561
494
 
562
- self.hourly_file = os.path.join(
563
- hass.config.path(),
564
- "custom_components",
565
- "meteocat",
566
- "files",
567
- f"forecast_{self.town_id}_hourly_data.json",
568
- )
569
- self.daily_file = os.path.join(
570
- hass.config.path(),
571
- "custom_components",
572
- "meteocat",
573
- "files",
574
- f"forecast_{self.town_id}_daily_data.json",
575
- )
495
+ # Ruta persistente en /config/meteocat_files/files
496
+ files_folder = get_storage_dir(hass, "files")
497
+ self.hourly_file = files_folder / f"forecast_{self.town_id}_hourly_data.json"
498
+ self.daily_file = files_folder / f"forecast_{self.town_id}_daily_data.json"
576
499
 
577
500
  super().__init__(
578
501
  hass,
@@ -581,9 +504,9 @@ class MeteocatEntityCoordinator(DataUpdateCoordinator):
581
504
  update_interval=DEFAULT_ENTITY_UPDATE_INTERVAL,
582
505
  )
583
506
 
584
- async def validate_forecast_data(self, file_path: str) -> dict:
507
+ async def validate_forecast_data(self, file_path: Path) -> dict:
585
508
  """Valida y retorna datos de predicción si son válidos."""
586
- if not os.path.exists(file_path):
509
+ if not file_path.exists():
587
510
  _LOGGER.info("El archivo %s no existe. Se considerará inválido.", file_path)
588
511
  return None
589
512
  try:
@@ -606,7 +529,9 @@ class MeteocatEntityCoordinator(DataUpdateCoordinator):
606
529
  )
607
530
 
608
531
  # Verificar si la antigüedad es mayor a un día
609
- if (today - first_date).days > DEFAULT_VALIDITY_DAYS and current_time >= time(DEFAULT_VALIDITY_HOURS, DEFAULT_VALIDITY_MINUTES):
532
+ if (today - first_date).days > DEFAULT_VALIDITY_DAYS and current_time >= time(
533
+ DEFAULT_VALIDITY_HOURS, DEFAULT_VALIDITY_MINUTES
534
+ ):
610
535
  _LOGGER.info(
611
536
  "Los datos en %s son antiguos. Se procederá a llamar a la API.",
612
537
  file_path,
@@ -618,25 +543,27 @@ class MeteocatEntityCoordinator(DataUpdateCoordinator):
618
543
  _LOGGER.warning("Error validando datos en %s: %s", file_path, e)
619
544
  return None
620
545
 
621
- async def _fetch_and_save_data(self, api_method, file_path: str) -> dict:
546
+ async def _fetch_and_save_data(self, api_method, file_path: Path) -> dict:
622
547
  """Obtiene datos de la API y los guarda en un archivo JSON."""
623
548
  try:
624
549
  data = await asyncio.wait_for(api_method(self.town_id), timeout=30)
625
550
 
626
551
  # Procesar los datos antes de guardarlos
627
- for day in data.get('dies', []):
628
- for var, details in day.get('variables', {}).items():
629
- if var == 'precipitacio' and isinstance(details.get('valor'), str) and details['valor'].startswith('-'):
630
- details['valor'] = '0.0'
552
+ for day in data.get("dies", []):
553
+ for var, details in day.get("variables", {}).items():
554
+ if (
555
+ var == "precipitacio"
556
+ and isinstance(details.get("valor"), str)
557
+ and details["valor"].startswith("-")
558
+ ):
559
+ details["valor"] = "0.0"
631
560
 
632
561
  await save_json_to_file(data, file_path)
633
-
562
+
634
563
  # Actualizar cuotas dependiendo del tipo de predicción
635
- if api_method.__name__ == 'get_prediccion_horaria':
564
+ if api_method.__name__ in ("get_prediccion_horaria", "get_prediccion_diaria"):
636
565
  await _update_quotes(self.hass, "Prediccio")
637
- elif api_method.__name__ == 'get_prediccion_diaria':
638
- await _update_quotes(self.hass, "Prediccio")
639
-
566
+
640
567
  return data
641
568
  except Exception as err:
642
569
  _LOGGER.error(f"Error al obtener datos de la API para {file_path}: {err}")
@@ -718,13 +645,11 @@ class HourlyForecastCoordinator(DataUpdateCoordinator):
718
645
  self.town_id = entry_data["town_id"]
719
646
  self.station_name = entry_data["station_name"]
720
647
  self.station_id = entry_data["station_id"]
721
- self.file_path = os.path.join(
722
- hass.config.path(),
723
- "custom_components",
724
- "meteocat",
725
- "files",
726
- f"forecast_{self.town_id.lower()}_hourly_data.json",
727
- )
648
+
649
+ # Ruta persistente en /config/meteocat_files/files
650
+ files_folder = get_storage_dir(hass, "files")
651
+ self.file_path = files_folder / f"forecast_{self.town_id.lower()}_hourly_data.json"
652
+
728
653
  super().__init__(
729
654
  hass,
730
655
  _LOGGER,
@@ -740,7 +665,7 @@ class HourlyForecastCoordinator(DataUpdateCoordinator):
740
665
 
741
666
  async def _is_data_valid(self) -> bool:
742
667
  """Verifica si los datos horarios en el archivo JSON son válidos y actuales."""
743
- if not os.path.exists(self.file_path):
668
+ if not self.file_path.exists():
744
669
  return False
745
670
 
746
671
  try:
@@ -773,7 +698,7 @@ class HourlyForecastCoordinator(DataUpdateCoordinator):
773
698
  content = await f.read()
774
699
  return json.loads(content)
775
700
  except Exception as e:
776
- _LOGGER.warning("Error leyendo archivo de predicción horaria: %s", e)
701
+ _LOGGER.warning("Error leyendo archivo de predicción horaria en %s: %s", self.file_path, e)
777
702
 
778
703
  return {}
779
704
 
@@ -867,13 +792,11 @@ class DailyForecastCoordinator(DataUpdateCoordinator):
867
792
  self.town_id = entry_data["town_id"]
868
793
  self.station_name = entry_data["station_name"]
869
794
  self.station_id = entry_data["station_id"]
870
- self.file_path = os.path.join(
871
- hass.config.path(),
872
- "custom_components",
873
- "meteocat",
874
- "files",
875
- f"forecast_{self.town_id.lower()}_daily_data.json",
876
- )
795
+
796
+ # Ruta persistente en /config/meteocat_files/files
797
+ files_folder = get_storage_dir(hass, "files")
798
+ self.file_path = files_folder / f"forecast_{self.town_id.lower()}_daily_data.json"
799
+
877
800
  super().__init__(
878
801
  hass,
879
802
  _LOGGER,
@@ -893,7 +816,7 @@ class DailyForecastCoordinator(DataUpdateCoordinator):
893
816
 
894
817
  async def _is_data_valid(self) -> bool:
895
818
  """Verifica si hay datos válidos y actuales en el archivo JSON."""
896
- if not os.path.exists(self.file_path):
819
+ if not self.file_path.exists():
897
820
  return False
898
821
 
899
822
  try:
@@ -936,7 +859,7 @@ class DailyForecastCoordinator(DataUpdateCoordinator):
936
859
  data["dies"] = filtered_days
937
860
  return data
938
861
  except Exception as e:
939
- _LOGGER.warning("Error leyendo archivo de predicción diaria: %s", e)
862
+ _LOGGER.warning("Error leyendo archivo de predicción diaria en %s: %s", self.file_path, e)
940
863
 
941
864
  return {}
942
865
 
@@ -998,7 +921,6 @@ class MeteocatConditionCoordinator(DataUpdateCoordinator):
998
921
  Args:
999
922
  hass (HomeAssistant): Instance of Home Assistant.
1000
923
  entry_data (dict): Configuration data from core.config_entries.
1001
- update_interval (timedelta): Update interval for the sensor.
1002
924
  """
1003
925
  self.town_id = entry_data["town_id"] # Municipality ID
1004
926
  self.hass = hass
@@ -1010,33 +932,16 @@ class MeteocatConditionCoordinator(DataUpdateCoordinator):
1010
932
  update_interval=DEFAULT_CONDITION_SENSOR_UPDATE_INTERVAL,
1011
933
  )
1012
934
 
1013
- self._file_path = os.path.join(
1014
- hass.config.path("custom_components/meteocat/files"),
1015
- f"forecast_{self.town_id.lower()}_hourly_data.json",
1016
- )
935
+ # Ruta persistente en /config/meteocat_files/files
936
+ files_folder = get_storage_dir(hass, "files")
937
+ self._file_path = files_folder / f"forecast_{self.town_id.lower()}_hourly_data.json"
1017
938
 
1018
939
  async def _async_update_data(self):
1019
940
  """Read and process condition data for the current hour from the file asynchronously."""
1020
941
  _LOGGER.debug("Iniciando actualización de datos desde el archivo: %s", self._file_path)
1021
942
 
1022
- try:
1023
- async with aiofiles.open(self._file_path, "r", encoding="utf-8") as file:
1024
- raw_data = await file.read()
1025
- raw_data = json.loads(raw_data) # Parse JSON data
1026
- except FileNotFoundError:
1027
- _LOGGER.error(
1028
- "No se ha encontrado el archivo JSON con datos del estado del cielo en %s.",
1029
- self._file_path,
1030
- )
1031
- return self.DEFAULT_CONDITION
1032
- except json.JSONDecodeError:
1033
- _LOGGER.error(
1034
- "Error al decodificar el archivo JSON del estado del cielo en %s.",
1035
- self._file_path,
1036
- )
1037
- return self.DEFAULT_CONDITION
1038
- except Exception as e:
1039
- _LOGGER.error("Error inesperado al leer los datos del archivo %s: %s", self._file_path, e)
943
+ raw_data = await load_json_from_file(self._file_path)
944
+ if not raw_data:
1040
945
  return self.DEFAULT_CONDITION
1041
946
 
1042
947
  return self._get_condition_for_current_hour(raw_data) or self.DEFAULT_CONDITION
@@ -1047,12 +952,10 @@ class MeteocatConditionCoordinator(DataUpdateCoordinator):
1047
952
 
1048
953
  def _get_condition_for_current_hour(self, raw_data):
1049
954
  """Get condition data for the current hour."""
1050
- # Fecha y hora actual
1051
955
  current_datetime = datetime.now(TIMEZONE)
1052
956
  current_date = current_datetime.strftime("%Y-%m-%d")
1053
957
  current_hour = current_datetime.hour
1054
958
 
1055
- # Busca los datos para la fecha actual
1056
959
  for day in raw_data.get("dies", []):
1057
960
  if day["data"].startswith(current_date):
1058
961
  for value in day["variables"]["estatCel"]["valors"]:
@@ -1066,7 +969,6 @@ class MeteocatConditionCoordinator(DataUpdateCoordinator):
1066
969
  self.hass,
1067
970
  is_hourly=True,
1068
971
  )
1069
- # Añadir hora y fecha a los datos de la condición
1070
972
  condition.update({
1071
973
  "hour": current_hour,
1072
974
  "date": current_date,
@@ -1078,9 +980,8 @@ class MeteocatConditionCoordinator(DataUpdateCoordinator):
1078
980
  condition,
1079
981
  )
1080
982
  return condition
1081
- break # Sale del bucle una vez encontrada la fecha actual
983
+ break
1082
984
 
1083
- # Si no se encuentran datos, devuelve un diccionario vacío con valores predeterminados
1084
985
  _LOGGER.warning(
1085
986
  "No se encontraron datos del Estado del Cielo para hoy (%s) y la hora actual (%s).",
1086
987
  current_date,
@@ -1088,6 +989,7 @@ class MeteocatConditionCoordinator(DataUpdateCoordinator):
1088
989
  )
1089
990
  return {"condition": "unknown", "hour": current_hour, "icon": None, "date": current_date}
1090
991
 
992
+
1091
993
  class MeteocatTempForecastCoordinator(DataUpdateCoordinator):
1092
994
  """Coordinator para manejar la temperatura máxima y mínima de las predicciones diarias desde archivos locales."""
1093
995
 
@@ -1101,13 +1003,7 @@ class MeteocatTempForecastCoordinator(DataUpdateCoordinator):
1101
1003
  self.town_id = entry_data["town_id"]
1102
1004
  self.station_name = entry_data["station_name"]
1103
1005
  self.station_id = entry_data["station_id"]
1104
- self.file_path = os.path.join(
1105
- hass.config.path(),
1106
- "custom_components",
1107
- "meteocat",
1108
- "files",
1109
- f"forecast_{self.town_id.lower()}_daily_data.json",
1110
- )
1006
+
1111
1007
  super().__init__(
1112
1008
  hass,
1113
1009
  _LOGGER,
@@ -1115,61 +1011,46 @@ class MeteocatTempForecastCoordinator(DataUpdateCoordinator):
1115
1011
  update_interval=DEFAULT_TEMP_FORECAST_UPDATE_INTERVAL,
1116
1012
  )
1117
1013
 
1014
+ # Ruta persistente en /config/meteocat_files/files
1015
+ files_folder = get_storage_dir(hass, "files")
1016
+ self.file_path = files_folder / f"forecast_{self.town_id.lower()}_daily_data.json"
1017
+
1118
1018
  def _convert_to_local_time(self, forecast_time: datetime) -> datetime:
1119
1019
  """Convierte una hora UTC a la hora local en la zona horaria de Madrid, considerando el horario de verano."""
1120
- local_time = forecast_time.astimezone(TIMEZONE)
1121
- return local_time
1020
+ return forecast_time.astimezone(TIMEZONE)
1122
1021
 
1123
1022
  async def _is_data_valid(self) -> bool:
1124
1023
  """Verifica si hay datos válidos y actuales en el archivo JSON."""
1125
- if not os.path.exists(self.file_path):
1024
+ data = await load_json_from_file(self.file_path)
1025
+ if not data or "dies" not in data or not data["dies"]:
1126
1026
  return False
1127
1027
 
1128
- try:
1129
- async with aiofiles.open(self.file_path, "r", encoding="utf-8") as f:
1130
- content = await f.read()
1131
- data = json.loads(content)
1028
+ today = datetime.now(TIMEZONE).date()
1029
+ return any(
1030
+ self._convert_to_local_time(
1031
+ datetime.fromisoformat(dia["data"].rstrip("Z")).replace(tzinfo=timezone.utc)
1032
+ ).date() >= today
1033
+ for dia in data["dies"]
1034
+ )
1132
1035
 
1133
- if not data or "dies" not in data or not data["dies"]:
1134
- return False
1036
+ async def _async_update_data(self) -> dict:
1037
+ """Lee y filtra los datos de predicción diaria desde el archivo local."""
1038
+ if await self._is_data_valid():
1039
+ data = await load_json_from_file(self.file_path)
1040
+ if not data:
1041
+ return {}
1135
1042
 
1136
1043
  today = datetime.now(TIMEZONE).date()
1137
- if any(
1138
- self._convert_to_local_time(
1044
+ data["dies"] = [
1045
+ dia for dia in data["dies"]
1046
+ if self._convert_to_local_time(
1139
1047
  datetime.fromisoformat(dia["data"].rstrip("Z")).replace(tzinfo=timezone.utc)
1140
1048
  ).date() >= today
1141
- for dia in data["dies"]
1142
- ):
1143
- return True
1144
- except Exception as e:
1145
- _LOGGER.warning("Error validando datos diarios en %s: %s", self.file_path, e)
1146
-
1147
- return False
1049
+ ]
1148
1050
 
1149
- async def _async_update_data(self) -> dict:
1150
- """Lee y filtra los datos de predicción diaria desde el archivo local."""
1151
- if await self._is_data_valid():
1152
- try:
1153
- async with aiofiles.open(self.file_path, "r", encoding="utf-8") as f:
1154
- content = await f.read()
1155
- data = json.loads(content)
1156
-
1157
- today = datetime.now(TIMEZONE).date()
1158
- data["dies"] = [
1159
- dia for dia in data["dies"]
1160
- if self._convert_to_local_time(
1161
- datetime.fromisoformat(dia["data"].rstrip("Z")).replace(tzinfo=timezone.utc)
1162
- ).date() >= today
1163
- ]
1164
-
1165
- today_temp_forecast = self.get_temp_forecast_for_today(data)
1166
- if today_temp_forecast:
1167
- parsed_data = self.parse_temp_forecast(today_temp_forecast)
1168
- return parsed_data
1169
- except Exception as e:
1170
- _LOGGER.warning(
1171
- "Error leyendo temperaturas del archivo de predicción diaria '%s': %s", self.file_path, e
1172
- )
1051
+ today_temp_forecast = self.get_temp_forecast_for_today(data)
1052
+ if today_temp_forecast:
1053
+ return self.parse_temp_forecast(today_temp_forecast)
1173
1054
 
1174
1055
  return {}
1175
1056
 
@@ -1214,23 +1095,10 @@ class MeteocatAlertsCoordinator(DataUpdateCoordinator):
1214
1095
  self.limit_prediccio = entry_data["limit_prediccio"] # Límite de llamada a la API para PREDICCIONES
1215
1096
  self.alerts_data = MeteocatAlerts(self.api_key)
1216
1097
 
1217
- # Define la ruta del archivo JSON principal donde se guardarán las alertas
1218
- self.alerts_file = os.path.join(
1219
- hass.config.path(),
1220
- "custom_components",
1221
- "meteocat",
1222
- "files",
1223
- "alerts.json"
1224
- )
1225
-
1226
- # Define la ruta del archivo JSON filtrado por región
1227
- self.alerts_region_file = os.path.join(
1228
- hass.config.path(),
1229
- "custom_components",
1230
- "meteocat",
1231
- "files",
1232
- f"alerts_{self.region_id}.json"
1233
- )
1098
+ # Ruta persistente en /config/meteocat_files/files
1099
+ files_folder = get_storage_dir(hass, "files")
1100
+ self.alerts_file = files_folder / "alerts.json"
1101
+ self.alerts_region_file = files_folder / f"alerts_{self.region_id}.json"
1234
1102
 
1235
1103
  super().__init__(
1236
1104
  hass,
@@ -1461,19 +1329,17 @@ class MeteocatAlertsRegionCoordinator(DataUpdateCoordinator):
1461
1329
  self.station_id = entry_data["station_id"]
1462
1330
  self.region_name = entry_data["region_name"]
1463
1331
  self.region_id = entry_data["region_id"]
1332
+
1464
1333
  super().__init__(
1465
1334
  hass,
1466
1335
  _LOGGER,
1467
1336
  name=f"{DOMAIN} Alerts Region Coordinator",
1468
1337
  update_interval=DEFAULT_ALERTS_REGION_UPDATE_INTERVAL,
1469
1338
  )
1470
- self._file_path = os.path.join(
1471
- hass.config.path(),
1472
- "custom_components",
1473
- "meteocat",
1474
- "files",
1475
- f"alerts_{self.region_id}.json",
1476
- )
1339
+
1340
+ # Ruta persistente en /config/meteocat_files/files
1341
+ files_folder = get_storage_dir(hass, "files")
1342
+ self._file_path = files_folder / f"alerts_{self.region_id}.json"
1477
1343
 
1478
1344
  def _convert_to_local_time(self, time_str: str) -> datetime:
1479
1345
  """Convierte una cadena de tiempo UTC a la zona horaria de Madrid."""
@@ -1544,16 +1410,11 @@ class MeteocatAlertsRegionCoordinator(DataUpdateCoordinator):
1544
1410
 
1545
1411
  async def _async_update_data(self) -> Dict[str, Any]:
1546
1412
  """Carga y procesa los datos de alertas desde el archivo JSON."""
1547
- try:
1548
- async with aiofiles.open(self._file_path, "r", encoding="utf-8") as file:
1549
- raw_data = await file.read()
1550
- data = json.loads(raw_data)
1551
- _LOGGER.info("Datos cargados desde %s: %s", self._file_path, data) # Log de la carga de datos
1552
- except FileNotFoundError:
1553
- _LOGGER.error("No se encontró el archivo JSON de alertas en %s.", self._file_path)
1554
- return {}
1555
- except json.JSONDecodeError:
1556
- _LOGGER.error("Error al decodificar el archivo JSON de alertas en %s.", self._file_path)
1413
+ data = await load_json_from_file(self._file_path)
1414
+ _LOGGER.info("Datos cargados desde %s: %s", self._file_path, data) # Log de la carga de datos
1415
+
1416
+ if not data:
1417
+ _LOGGER.error("No se pudo cargar el archivo JSON de alertas en %s.", self._file_path)
1557
1418
  return {}
1558
1419
 
1559
1420
  return self._process_alerts_data(data)
@@ -1678,13 +1539,9 @@ class MeteocatQuotesCoordinator(DataUpdateCoordinator):
1678
1539
  self.api_key = entry_data["api_key"] # Usamos la API key de la configuración
1679
1540
  self.meteocat_quotes = MeteocatQuotes(self.api_key)
1680
1541
 
1681
- self.quotes_file = os.path.join(
1682
- hass.config.path(),
1683
- "custom_components",
1684
- "meteocat",
1685
- "files",
1686
- "quotes.json"
1687
- )
1542
+ # Ruta persistente en /config/meteocat_files/files
1543
+ files_folder = get_storage_dir(hass, "files")
1544
+ self.quotes_file = files_folder / "quotes.json"
1688
1545
 
1689
1546
  super().__init__(
1690
1547
  hass,
@@ -1824,17 +1681,13 @@ class MeteocatQuotesFileCoordinator(DataUpdateCoordinator):
1824
1681
  name="Meteocat Quotes File Coordinator",
1825
1682
  update_interval=DEFAULT_QUOTES_FILE_UPDATE_INTERVAL,
1826
1683
  )
1827
- self.quotes_file = os.path.join(
1828
- hass.config.path(),
1829
- "custom_components",
1830
- "meteocat",
1831
- "files",
1832
- "quotes.json"
1833
- )
1684
+ # Ruta persistente en /config/meteocat_files/files
1685
+ files_folder = get_storage_dir(hass, "files")
1686
+ self.quotes_file = files_folder / "quotes.json"
1834
1687
 
1835
1688
  async def _async_update_data(self) -> Dict[str, Any]:
1836
1689
  """Carga los datos de quotes.json y devuelve el estado de las cuotas."""
1837
- existing_data = await self._load_json_file()
1690
+ existing_data = await load_json_from_file(self.quotes_file)
1838
1691
 
1839
1692
  if not existing_data:
1840
1693
  _LOGGER.warning("No se encontraron datos en quotes.json.")
@@ -1855,19 +1708,6 @@ class MeteocatQuotesFileCoordinator(DataUpdateCoordinator):
1855
1708
  ]
1856
1709
  }
1857
1710
 
1858
- async def _load_json_file(self) -> dict:
1859
- """Carga el archivo JSON de forma asincrónica."""
1860
- try:
1861
- async with aiofiles.open(self.quotes_file, "r", encoding="utf-8") as f:
1862
- data = await f.read()
1863
- return json.loads(data)
1864
- except FileNotFoundError:
1865
- _LOGGER.warning("El archivo %s no existe.", self.quotes_file)
1866
- return {}
1867
- except json.JSONDecodeError as err:
1868
- _LOGGER.error("Error al decodificar JSON del archivo %s: %s", self.quotes_file, err)
1869
- return {}
1870
-
1871
1711
  async def get_plan_info(self, plan_name: str) -> dict:
1872
1712
  """Obtiene la información de un plan específico."""
1873
1713
  data = await self._async_update_data()
@@ -1902,13 +1742,9 @@ class MeteocatLightningCoordinator(DataUpdateCoordinator):
1902
1742
  self.region_id = entry_data["region_id"] # Región de la configuración
1903
1743
  self.meteocat_lightning = MeteocatLightning(self.api_key)
1904
1744
 
1905
- self.lightning_file = os.path.join(
1906
- hass.config.path(),
1907
- "custom_components",
1908
- "meteocat",
1909
- "files",
1910
- f"lightning_{self.region_id}.json",
1911
- )
1745
+ # Ruta persistente en /config/meteocat_files/files
1746
+ files_folder = get_storage_dir(hass, "files")
1747
+ self.lightning_file = files_folder / f"lightning_{self.region_id}.json"
1912
1748
 
1913
1749
  super().__init__(
1914
1750
  hass,
@@ -2006,13 +1842,9 @@ class MeteocatLightningFileCoordinator(DataUpdateCoordinator):
2006
1842
  self.region_id = entry_data["region_id"]
2007
1843
  self.town_id = entry_data["town_id"]
2008
1844
 
2009
- self.lightning_file = os.path.join(
2010
- hass.config.path(),
2011
- "custom_components",
2012
- "meteocat",
2013
- "files",
2014
- f"lightning_{self.region_id}.json",
2015
- )
1845
+ # Ruta persistente en /config/meteocat_files/files
1846
+ files_folder = get_storage_dir(hass, "files")
1847
+ self.lightning_file = files_folder / f"lightning_{self.region_id}.json"
2016
1848
 
2017
1849
  super().__init__(
2018
1850
  hass,