meteocat 2.2.6 → 2.3.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.
- package/.github/ISSUE_TEMPLATE/bug_report.md +39 -0
- package/.github/ISSUE_TEMPLATE/config.yml +1 -0
- package/.github/workflows/autocloser.yaml +25 -0
- package/.github/workflows/close-duplicates.yml +57 -0
- package/.github/workflows/publish-zip.yml +67 -0
- package/.github/workflows/release.yml +38 -6
- package/.github/workflows/stale.yml +12 -0
- package/.github/workflows/sync-gitlab.yml +94 -0
- package/.releaserc +1 -8
- package/CHANGELOG.md +29 -0
- package/README.md +29 -4
- package/custom_components/meteocat/__init__.py +154 -110
- package/custom_components/meteocat/config_flow.py +125 -55
- package/custom_components/meteocat/coordinator.py +200 -368
- package/custom_components/meteocat/helpers.py +12 -0
- package/custom_components/meteocat/manifest.json +22 -11
- package/custom_components/meteocat/options_flow.py +46 -2
- package/custom_components/meteocat/sensor.py +47 -8
- package/custom_components/meteocat/strings.json +10 -2
- package/custom_components/meteocat/translations/ca.json +10 -2
- package/custom_components/meteocat/translations/en.json +10 -2
- package/custom_components/meteocat/translations/es.json +10 -2
- package/custom_components/meteocat/version.py +1 -2
- package/filetree.txt +9 -0
- package/hacs.json +5 -2
- package/images/options.png +0 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/.releaserc.toml +0 -14
- package/releaserc.json +0 -18
|
@@ -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:
|
|
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
|
-
|
|
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
|
|
81
|
+
raise RuntimeError(f"Error guardando JSON en {output_file}: {e}")
|
|
84
82
|
|
|
85
|
-
async def load_json_from_file(input_file:
|
|
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
|
|
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
|
-
|
|
118
|
-
|
|
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
|
|
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"]
|
|
169
|
-
self.town_name = entry_data["town_name"]
|
|
170
|
-
self.town_id = entry_data["town_id"]
|
|
171
|
-
self.station_name = entry_data["station_name"]
|
|
172
|
-
self.station_id = entry_data["station_id"]
|
|
173
|
-
self.variable_name = entry_data["variable_name"]
|
|
174
|
-
self.variable_id = entry_data["variable_id"]
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
|
180
|
+
timeout=30
|
|
199
181
|
)
|
|
200
182
|
_LOGGER.debug("Datos de sensores actualizados exitosamente: %s", data)
|
|
201
183
|
|
|
202
|
-
# Actualizar las cuotas
|
|
203
|
-
await _update_quotes(self.hass, "XEMA")
|
|
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
|
|
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)
|
|
227
|
+
"No se pudo inicializar el dispositivo (Station ID: %s): %s",
|
|
247
228
|
self.station_id,
|
|
248
229
|
err,
|
|
249
230
|
)
|
|
250
|
-
raise
|
|
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
|
|
234
|
+
"Error inesperado al obtener datos de los sensores (Station ID: %s): %s",
|
|
255
235
|
self.station_id,
|
|
256
236
|
err,
|
|
257
237
|
)
|
|
258
|
-
|
|
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
|
-
|
|
244
|
+
|
|
264
245
|
_LOGGER.error("No se pudo obtener datos actualizados ni cargar datos en caché.")
|
|
265
|
-
return None
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
|
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
|
-
|
|
367
|
-
|
|
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
|
-
|
|
372
|
-
|
|
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.
|
|
400
|
-
return valid_data[
|
|
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
|
|
371
|
+
timeout=30,
|
|
406
372
|
)
|
|
407
|
-
_LOGGER.debug("Datos
|
|
373
|
+
_LOGGER.debug("Datos UVI obtenidos desde API: %s", data)
|
|
408
374
|
|
|
409
|
-
|
|
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
|
-
|
|
413
|
-
|
|
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
|
|
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
|
-
|
|
448
|
-
|
|
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(
|
|
456
|
-
|
|
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
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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
|
-
|
|
494
|
-
raw_data = json.loads(
|
|
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
|
|
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
|
|
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
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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:
|
|
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
|
|
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(
|
|
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:
|
|
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(
|
|
628
|
-
for var, details in day.get(
|
|
629
|
-
if
|
|
630
|
-
|
|
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__
|
|
564
|
+
if api_method.__name__ in ("get_prediccion_horaria", "get_prediccion_diaria"):
|
|
636
565
|
await _update_quotes(self.hass, "Prediccio")
|
|
637
|
-
|
|
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
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
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
|
|
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
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
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
|
|
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
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
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
|
-
|
|
1023
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
data
|
|
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
|
-
|
|
1134
|
-
|
|
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
|
-
|
|
1138
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
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
|
-
#
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
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
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
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
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
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
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
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
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
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.
|
|
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
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
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
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
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,
|