meteocat 4.0.3 → 4.0.5
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/workflows/publish-zip.yml +4 -4
- package/CHANGELOG.md +15 -0
- package/custom_components/meteocat/__init__.py +1 -1
- package/custom_components/meteocat/const.py +3 -0
- package/custom_components/meteocat/coordinator.py +160 -96
- package/custom_components/meteocat/manifest.json +1 -1
- package/custom_components/meteocat/sensor.py +202 -69
- package/custom_components/meteocat/strings.json +9 -0
- package/custom_components/meteocat/translations/ca.json +9 -0
- package/custom_components/meteocat/translations/en.json +9 -0
- package/custom_components/meteocat/translations/es.json +9 -0
- package/custom_components/meteocat/version.py +1 -1
- package/package.json +1 -1
- package/pyproject.toml +1 -1
|
@@ -32,10 +32,10 @@ jobs:
|
|
|
32
32
|
id: ha-version
|
|
33
33
|
run: |
|
|
34
34
|
# ──────────────────────────────────────────────────────────────
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
35
|
+
# Comportamiento normal: usar siempre la última versión
|
|
36
|
+
#latest=$(curl -s https://api.github.com/repos/home-assistant/core/releases/latest | jq -r .tag_name)
|
|
37
|
+
#latest_clean=${latest#v}
|
|
38
|
+
#echo "ha_version=$latest_clean" >> $GITHUB_OUTPUT
|
|
39
39
|
|
|
40
40
|
# Temporal: forzado a 2025.12.5 (enero 2026) por posible incompatibilidad con 2026.1.0
|
|
41
41
|
echo "ha_version=2025.12.5" >> $GITHUB_OUTPUT
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
## [4.0.5](https://github.com/figorr/meteocat/compare/v4.0.4...v4.0.5) (2026-01-31)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* fix state for UVI, Hourly and Daily File sensors ([af645a6](https://github.com/figorr/meteocat/commit/af645a6a99189b1d0dac3b9b1019a400ae4e92f5))
|
|
7
|
+
* include last update check to avoid continuous API calls when API returns outdated hourly and daily forecast data ([939a8ac](https://github.com/figorr/meteocat/commit/939a8ac66bfb3c3d750fda0cd719f310493f06c0))
|
|
8
|
+
|
|
9
|
+
## [4.0.4](https://github.com/figorr/meteocat/compare/v4.0.3...v4.0.4) (2026-01-25)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
### Bug Fixes
|
|
13
|
+
|
|
14
|
+
* include last update check to avoid continuous API calls when API returns outdated data ([32760e8](https://github.com/figorr/meteocat/commit/32760e854e3dd2a6af07c48373995df4f78106cb))
|
|
15
|
+
|
|
1
16
|
## [4.0.3](https://github.com/figorr/meteocat/compare/v4.0.2...v4.0.3) (2026-01-10)
|
|
2
17
|
|
|
3
18
|
|
|
@@ -61,10 +61,13 @@ DEFAULT_NAME = "METEOCAT"
|
|
|
61
61
|
DEFAULT_VALIDITY_DAYS = 1 # Número de días a partir de los cuales se considera que el archivo de información está obsoleto
|
|
62
62
|
DEFAULT_VALIDITY_HOURS = 6 # Hora a partir de la cual la API tiene la información actualizada de predicciones disponible para descarga
|
|
63
63
|
DEFAULT_VALIDITY_MINUTES = 0 # Minutos a partir de los cuales la API tiene la información actualizada de predicciones disponible para descarga
|
|
64
|
+
DEFAULT_HOURLY_FORECAST_MIN_HOURS_SINCE_LAST_UPDATE = 15 # Horas mínimas desde la última actualización de predicciones horararias para proceder a una nueva llamada a la API
|
|
65
|
+
DEFAULT_DAILY_FORECAST_MIN_HOURS_SINCE_LAST_UPDATE = 15 # Horas mínimas desde la última actualización de predicciones diarias para proceder a una nueva llamada a la API
|
|
64
66
|
DEFAULT_UVI_LOW_VALIDITY_HOURS = 5 # Hora a partir de la cual la API tiene la información actualizada de datos UVI disponible para descarga con límite bajo de cuota
|
|
65
67
|
DEFAULT_UVI_LOW_VALIDITY_MINUTES = 0 # Minutos a partir de los cuales la API tiene la información actualizada de datos UVI disponible para descarga con límite bajo de cuota
|
|
66
68
|
DEFAULT_UVI_HIGH_VALIDITY_HOURS = 9 # Hora a partir de la cual la API tiene la información actualizada de datos UVI disponible para descarga con límite alto de cuota
|
|
67
69
|
DEFAULT_UVI_HIGH_VALIDITY_MINUTES = 0 # Minutos a partir de los cuales la API tiene la información actualizada de datos UVI disponible para descarga con límite alto de cuota
|
|
70
|
+
DEFAULT_UVI_MIN_HOURS_SINCE_LAST_UPDATE = 15 # Horas mínimas desde la última actualización de datos UVI para proceder a una nueva llamada a la API
|
|
68
71
|
DEFAULT_ALERT_VALIDITY_TIME = 120 # Minutos a partir de los cuales las alertas están obsoletas y se se debe proceder a una nueva llamada a la API
|
|
69
72
|
DEFAULT_QUOTES_VALIDITY_TIME = 240 # Minutos a partir de los cuales los datos de cuotas están obsoletos y se se debe proceder a una nueva llamada a la API
|
|
70
73
|
DEFAULT_LIGHTNING_VALIDITY_TIME = 240 # Minutos a partir de los cuales los datos de rayos están obsoletos y se se debe proceder a una nueva llamada a la API
|
|
@@ -52,10 +52,13 @@ from .const import (
|
|
|
52
52
|
DEFAULT_VALIDITY_DAYS,
|
|
53
53
|
DEFAULT_VALIDITY_HOURS,
|
|
54
54
|
DEFAULT_VALIDITY_MINUTES,
|
|
55
|
+
DEFAULT_HOURLY_FORECAST_MIN_HOURS_SINCE_LAST_UPDATE,
|
|
56
|
+
DEFAULT_DAILY_FORECAST_MIN_HOURS_SINCE_LAST_UPDATE,
|
|
55
57
|
DEFAULT_UVI_LOW_VALIDITY_HOURS,
|
|
56
58
|
DEFAULT_UVI_LOW_VALIDITY_MINUTES,
|
|
57
59
|
DEFAULT_UVI_HIGH_VALIDITY_HOURS,
|
|
58
60
|
DEFAULT_UVI_HIGH_VALIDITY_MINUTES,
|
|
61
|
+
DEFAULT_UVI_MIN_HOURS_SINCE_LAST_UPDATE,
|
|
59
62
|
DEFAULT_ALERT_VALIDITY_TIME,
|
|
60
63
|
DEFAULT_QUOTES_VALIDITY_TIME,
|
|
61
64
|
ALERT_VALIDITY_MULTIPLIER_100,
|
|
@@ -406,26 +409,24 @@ class MeteocatUviCoordinator(DataUpdateCoordinator):
|
|
|
406
409
|
)
|
|
407
410
|
|
|
408
411
|
async def is_uvi_data_valid(self) -> Optional[dict]:
|
|
409
|
-
"""Valida datos UVI
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
412
|
+
"""Valida si los datos UVI en caché son aún válidos, considerando:
|
|
413
|
+
1. Antigüedad de la fecha del primer día (lógica existente según cuota)
|
|
414
|
+
2. Hora mínima del día (según cuota)
|
|
415
|
+
3. Han pasado más de DEFAULT_UVI_MIN_HOURS_SINCE_LAST_UPDATE horas desde la última actualización exitosa
|
|
413
416
|
"""
|
|
414
417
|
if not self.uvi_file.exists():
|
|
415
418
|
_LOGGER.debug("Archivo UVI no existe: %s", self.uvi_file)
|
|
416
419
|
return None
|
|
417
420
|
|
|
418
421
|
try:
|
|
419
|
-
|
|
420
|
-
content = await f.read()
|
|
421
|
-
data = json.loads(content)
|
|
422
|
+
data = await load_json_from_file(self.uvi_file)
|
|
422
423
|
|
|
423
424
|
# Validar estructura básica
|
|
424
425
|
if not isinstance(data, dict) or "uvi" not in data or not isinstance(data["uvi"], list) or not data["uvi"]:
|
|
425
426
|
_LOGGER.warning("Estructura UVI inválida en %s", self.uvi_file)
|
|
426
427
|
return None
|
|
427
428
|
|
|
428
|
-
#
|
|
429
|
+
# ── Condición 1: Antigüedad de la fecha del primer día ──
|
|
429
430
|
try:
|
|
430
431
|
first_date_str = data["uvi"][0].get("date")
|
|
431
432
|
first_date = datetime.strptime(first_date_str, "%Y-%m-%d").date()
|
|
@@ -433,39 +434,64 @@ class MeteocatUviCoordinator(DataUpdateCoordinator):
|
|
|
433
434
|
_LOGGER.warning("Fecha UVI inválida en %s: %s", self.uvi_file, exc)
|
|
434
435
|
return None
|
|
435
436
|
|
|
436
|
-
# Fecha y hora actual en zona local (Europe/Madrid)
|
|
437
437
|
now_local = datetime.now(TIMEZONE)
|
|
438
438
|
today = now_local.date()
|
|
439
439
|
current_time_local = now_local.time()
|
|
440
|
-
# Horas para actualización según límite de cuota
|
|
441
|
-
min_update_time_high = time(DEFAULT_UVI_HIGH_VALIDITY_HOURS, DEFAULT_UVI_HIGH_VALIDITY_MINUTES) # Hora para cuota alta
|
|
442
|
-
min_update_time_low = time(DEFAULT_UVI_LOW_VALIDITY_HOURS, DEFAULT_UVI_LOW_VALIDITY_MINUTES) # Hora para cuota baja
|
|
443
|
-
# Diferencia en días
|
|
444
440
|
days_diff = (today - first_date).days
|
|
445
441
|
|
|
446
|
-
#
|
|
442
|
+
# Determinar umbrales según cuota
|
|
447
443
|
if self.limit_prediccio >= PREDICCIO_HIGH_QUOTA_LIMIT:
|
|
448
|
-
|
|
444
|
+
min_days = DEFAULT_VALIDITY_DAYS # ej: 1 día
|
|
445
|
+
min_update_time = time(DEFAULT_UVI_HIGH_VALIDITY_HOURS, DEFAULT_UVI_HIGH_VALIDITY_MINUTES)
|
|
446
|
+
quota_level = "ALTA"
|
|
449
447
|
else:
|
|
450
|
-
|
|
448
|
+
min_days = DEFAULT_VALIDITY_DAYS + 1 # ej: 2 días
|
|
449
|
+
min_update_time = time(DEFAULT_UVI_LOW_VALIDITY_HOURS, DEFAULT_UVI_LOW_VALIDITY_MINUTES)
|
|
450
|
+
quota_level = "BAJA"
|
|
451
|
+
|
|
452
|
+
cond1 = days_diff >= min_days
|
|
453
|
+
cond2 = current_time_local >= min_update_time
|
|
454
|
+
|
|
455
|
+
# ── Condición 3: Más de X horas desde última actualización ──
|
|
456
|
+
cond3 = True # por defecto (si no hay dataUpdate → permite actualizar)
|
|
457
|
+
last_update_str = None
|
|
458
|
+
if "actualitzat" in data and "dataUpdate" in data["actualitzat"]:
|
|
459
|
+
try:
|
|
460
|
+
last_update = datetime.fromisoformat(data["actualitzat"]["dataUpdate"])
|
|
461
|
+
time_since = now_local - last_update
|
|
462
|
+
cond3 = time_since > timedelta(hours=DEFAULT_UVI_MIN_HOURS_SINCE_LAST_UPDATE)
|
|
463
|
+
last_update_str = last_update.strftime("%Y-%m-%d %H:%M:%S %z")
|
|
464
|
+
_LOGGER.debug(
|
|
465
|
+
"Tiempo desde última actualización: %s (%s %dh)",
|
|
466
|
+
time_since,
|
|
467
|
+
"supera" if cond3 else "NO supera",
|
|
468
|
+
DEFAULT_UVI_MIN_HOURS_SINCE_LAST_UPDATE
|
|
469
|
+
)
|
|
470
|
+
except ValueError:
|
|
471
|
+
_LOGGER.warning("Formato inválido en dataUpdate: %s", data["actualitzat"]["dataUpdate"])
|
|
472
|
+
cond3 = True # si corrupto → permite actualizar
|
|
473
|
+
|
|
474
|
+
should_update = cond1 and cond2 and cond3
|
|
451
475
|
|
|
452
476
|
_LOGGER.debug(
|
|
453
|
-
"[UVI %s] Validación
|
|
454
|
-
"
|
|
477
|
+
"[UVI %s] Validación → cond1(días >=%d)=%s | cond2(hora >=%s)=%s | "
|
|
478
|
+
"cond3(>%dh desde %s)=%s | cuota=%d (%s) | actualizar=%s",
|
|
455
479
|
self.town_id,
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
480
|
+
min_days,
|
|
481
|
+
cond1,
|
|
482
|
+
min_update_time.strftime("%H:%M"),
|
|
483
|
+
cond2,
|
|
484
|
+
DEFAULT_UVI_MIN_HOURS_SINCE_LAST_UPDATE,
|
|
485
|
+
last_update_str or "nunca",
|
|
486
|
+
cond3,
|
|
459
487
|
self.limit_prediccio,
|
|
460
|
-
|
|
461
|
-
current_time_local.strftime("%H:%M"),
|
|
462
|
-
min_update_time_high.strftime("%H:%M") if self.limit_prediccio >= 550 else min_update_time_low.strftime("%H:%M"),
|
|
488
|
+
quota_level,
|
|
463
489
|
should_update,
|
|
464
490
|
)
|
|
465
491
|
|
|
466
492
|
if should_update:
|
|
467
493
|
_LOGGER.info(
|
|
468
|
-
"Datos UVI obsoletos → llamando API (town=%s, cuota=%d)",
|
|
494
|
+
"Datos UVI obsoletos o antiguos → llamando API (town=%s, cuota=%d)",
|
|
469
495
|
self.town_id, self.limit_prediccio
|
|
470
496
|
)
|
|
471
497
|
return None
|
|
@@ -480,14 +506,15 @@ class MeteocatUviCoordinator(DataUpdateCoordinator):
|
|
|
480
506
|
_LOGGER.error("Error validando UVI: %s", e)
|
|
481
507
|
return None
|
|
482
508
|
|
|
483
|
-
async def _async_update_data(self) ->
|
|
484
|
-
"""Actualiza los datos de UVI desde la API
|
|
509
|
+
async def _async_update_data(self) -> Optional[dict]:
|
|
510
|
+
"""Actualiza los datos de UVI desde la API o caché."""
|
|
485
511
|
try:
|
|
486
512
|
valid_data = await self.is_uvi_data_valid()
|
|
487
513
|
if valid_data:
|
|
488
514
|
_LOGGER.debug("Los datos del índice UV están actualizados. No se realiza llamada a la API.")
|
|
489
|
-
return valid_data
|
|
515
|
+
return valid_data
|
|
490
516
|
|
|
517
|
+
# ── Llamada a la API ──
|
|
491
518
|
data = await asyncio.wait_for(
|
|
492
519
|
self.meteocat_uvi_data.get_uvi_index(self.town_id),
|
|
493
520
|
timeout=30,
|
|
@@ -500,8 +527,19 @@ class MeteocatUviCoordinator(DataUpdateCoordinator):
|
|
|
500
527
|
_LOGGER.error("Formato inválido: se esperaba un dict con 'uvi' -> %s", data)
|
|
501
528
|
raise ValueError("Formato de datos inválido")
|
|
502
529
|
|
|
503
|
-
|
|
504
|
-
|
|
530
|
+
# Añadir timestamp de actualización exitosa
|
|
531
|
+
now_iso = datetime.now(TIMEZONE).isoformat()
|
|
532
|
+
enhanced_data = {
|
|
533
|
+
"actualitzat": {
|
|
534
|
+
"dataUpdate": now_iso
|
|
535
|
+
},
|
|
536
|
+
**data # conserva ine, nom, comarca, capital, uvi, ...
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
await save_json_to_file(enhanced_data, self.uvi_file)
|
|
540
|
+
_LOGGER.debug("Datos UVI guardados con dataUpdate: %s", now_iso)
|
|
541
|
+
|
|
542
|
+
return enhanced_data # ← en lugar de return data["uvi"]
|
|
505
543
|
|
|
506
544
|
except asyncio.TimeoutError as err:
|
|
507
545
|
_LOGGER.warning("Tiempo de espera agotado al obtener datos UVI.")
|
|
@@ -517,17 +555,16 @@ class MeteocatUviCoordinator(DataUpdateCoordinator):
|
|
|
517
555
|
raise
|
|
518
556
|
except Exception as err:
|
|
519
557
|
_LOGGER.exception("Error inesperado al obtener datos del índice UV para %s: %s", self.town_id, err)
|
|
520
|
-
|
|
521
|
-
#
|
|
558
|
+
|
|
559
|
+
# ── FALLBACK SEGURO ──
|
|
522
560
|
cached_data = await load_json_from_file(self.uvi_file)
|
|
523
561
|
if cached_data and "uvi" in cached_data and cached_data["uvi"]:
|
|
524
562
|
raw_date = cached_data["uvi"][0].get("date", "unknown")
|
|
525
|
-
# Formatear fecha para log
|
|
526
563
|
try:
|
|
527
564
|
first_date = datetime.strptime(raw_date, "%Y-%m-%d").strftime("%d/%m/%Y")
|
|
528
565
|
except (ValueError, TypeError):
|
|
529
566
|
first_date = raw_date
|
|
530
|
-
|
|
567
|
+
|
|
531
568
|
_LOGGER.warning(
|
|
532
569
|
"API UVI falló → usando caché local:\n"
|
|
533
570
|
" • Archivo: %s\n"
|
|
@@ -535,13 +572,13 @@ class MeteocatUviCoordinator(DataUpdateCoordinator):
|
|
|
535
572
|
self.uvi_file.name,
|
|
536
573
|
first_date
|
|
537
574
|
)
|
|
538
|
-
|
|
539
|
-
self.async_set_updated_data(cached_data
|
|
540
|
-
return cached_data
|
|
541
|
-
|
|
575
|
+
|
|
576
|
+
self.async_set_updated_data(cached_data)
|
|
577
|
+
return cached_data
|
|
578
|
+
|
|
542
579
|
_LOGGER.error("No hay datos UVI ni en caché para %s", self.town_id)
|
|
543
|
-
self.async_set_updated_data(
|
|
544
|
-
return
|
|
580
|
+
self.async_set_updated_data(None)
|
|
581
|
+
return None
|
|
545
582
|
|
|
546
583
|
class MeteocatUviFileCoordinator(BaseFileCoordinator):
|
|
547
584
|
"""Coordinator to read and process UV data from a file."""
|
|
@@ -646,81 +683,100 @@ class MeteocatEntityCoordinator(DataUpdateCoordinator):
|
|
|
646
683
|
name=f"{DOMAIN} Entity Coordinator",
|
|
647
684
|
update_interval=DEFAULT_ENTITY_UPDATE_INTERVAL,
|
|
648
685
|
)
|
|
649
|
-
|
|
686
|
+
|
|
650
687
|
# --------------------------------------------------------------------- #
|
|
651
688
|
# VALIDACIÓN DINÁMICA DE DATOS DE PREDICCIÓN
|
|
652
689
|
# --------------------------------------------------------------------- #
|
|
653
|
-
async def validate_forecast_data(self, file_path: Path) -> dict:
|
|
654
|
-
"""Valida
|
|
655
|
-
|
|
656
|
-
- Si `limit_prediccio >= 550` → actualiza **el día siguiente** después de las DEFAULT_VALIDITY_HOURS:DEFAULT_VALIDITY_MINUTES.
|
|
657
|
-
- Si `limit_prediccio < 550` → actualiza **dos días después** después de las DEFAULT_VALIDITY_HOURS:DEFAULT_VALIDITY_MINUTES.
|
|
658
|
-
"""
|
|
690
|
+
async def validate_forecast_data(self, file_path: Path) -> Optional[dict]:
|
|
691
|
+
"""Valida si los datos de predicción son válidos considerando 3 condiciones."""
|
|
659
692
|
if not file_path.exists():
|
|
660
|
-
_LOGGER.warning("
|
|
693
|
+
_LOGGER.warning("Archivo no existe: %s", file_path)
|
|
661
694
|
return None
|
|
695
|
+
|
|
662
696
|
try:
|
|
663
|
-
|
|
664
|
-
content = await f.read()
|
|
665
|
-
data = json.loads(content)
|
|
697
|
+
data = await load_json_from_file(file_path)
|
|
666
698
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
today = datetime.now(timezone.utc).date()
|
|
699
|
+
if not isinstance(data, dict) or "dies" not in data or not data["dies"]:
|
|
700
|
+
_LOGGER.warning("Estructura inválida en %s", file_path)
|
|
701
|
+
return None
|
|
671
702
|
|
|
672
|
-
#
|
|
673
|
-
|
|
674
|
-
|
|
703
|
+
# ── Condición 1: Antigüedad del primer día de predicción ──
|
|
704
|
+
first_date_str = data["dies"][0]["data"].rstrip("Z")
|
|
705
|
+
try:
|
|
706
|
+
first_date = datetime.fromisoformat(first_date_str).date()
|
|
707
|
+
except Exception as exc:
|
|
708
|
+
_LOGGER.warning("Fecha inválida en %s: %s", file_path, exc)
|
|
709
|
+
return None
|
|
675
710
|
|
|
711
|
+
now_local = datetime.now(TIMEZONE)
|
|
712
|
+
today = now_local.date() # Si queremos respetar que los datos del json son UTC quizás mejor usar today = datetime.now(timezone.utc).date()
|
|
713
|
+
current_time_local = now_local.time()
|
|
676
714
|
days_diff = (today - first_date).days
|
|
677
715
|
|
|
678
|
-
#
|
|
679
|
-
# Lógica según cuota
|
|
680
|
-
# -----------------------------------------------------------------
|
|
716
|
+
# ── Condición 2: Lógica de umbrales según cuota para determinar días y horas válidos de actualización ──
|
|
681
717
|
if self.limit_prediccio >= PREDICCIO_HIGH_QUOTA_LIMIT:
|
|
682
|
-
|
|
683
|
-
|
|
718
|
+
min_days = DEFAULT_VALIDITY_DAYS
|
|
719
|
+
min_update_time = time(DEFAULT_VALIDITY_HOURS, DEFAULT_VALIDITY_MINUTES)
|
|
720
|
+
quota_level = "ALTA"
|
|
684
721
|
else:
|
|
685
|
-
|
|
686
|
-
|
|
722
|
+
min_days = DEFAULT_VALIDITY_DAYS + 1
|
|
723
|
+
min_update_time = time(DEFAULT_VALIDITY_HOURS, DEFAULT_VALIDITY_MINUTES)
|
|
724
|
+
quota_level = "BAJA"
|
|
725
|
+
|
|
726
|
+
cond1 = days_diff >= min_days
|
|
727
|
+
cond2 = current_time_local >= min_update_time
|
|
728
|
+
|
|
729
|
+
# ── Condición 3: Más de X horas desde última actualización ──
|
|
730
|
+
cond3 = True # por defecto permite actualizar si no hay timestamp
|
|
731
|
+
last_update_str = None
|
|
732
|
+
hours_threshold = (
|
|
733
|
+
DEFAULT_HOURLY_FORECAST_MIN_HOURS_SINCE_LAST_UPDATE
|
|
734
|
+
if "hourly" in file_path.name.lower()
|
|
735
|
+
else DEFAULT_DAILY_FORECAST_MIN_HOURS_SINCE_LAST_UPDATE
|
|
736
|
+
)
|
|
737
|
+
|
|
738
|
+
if "actualitzat" in data and "dataUpdate" in data["actualitzat"]:
|
|
739
|
+
try:
|
|
740
|
+
last_update = datetime.fromisoformat(data["actualitzat"]["dataUpdate"])
|
|
741
|
+
time_since = now_local - last_update
|
|
742
|
+
cond3 = time_since > timedelta(hours=hours_threshold)
|
|
743
|
+
last_update_str = last_update.strftime("%Y-%m-%d %H:%M:%S %z")
|
|
744
|
+
_LOGGER.debug(
|
|
745
|
+
"%s → tiempo desde última act.: %s (%s %dh)",
|
|
746
|
+
file_path.name, time_since,
|
|
747
|
+
"supera" if cond3 else "NO supera", hours_threshold
|
|
748
|
+
)
|
|
749
|
+
except ValueError:
|
|
750
|
+
_LOGGER.warning("dataUpdate inválido en %s: %s", file_path, data["actualitzat"]["dataUpdate"])
|
|
751
|
+
cond3 = True
|
|
752
|
+
|
|
753
|
+
should_update = cond1 and cond2 and cond3
|
|
687
754
|
|
|
688
|
-
# -----------------------------------------------------------------
|
|
689
|
-
# Logs detallados
|
|
690
|
-
# -----------------------------------------------------------------
|
|
691
755
|
_LOGGER.debug(
|
|
692
|
-
"[%s] Validación
|
|
693
|
-
"
|
|
694
|
-
file_path.name,
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
self.limit_prediccio,
|
|
699
|
-
"ALTA" if self.limit_prediccio >= 550 else "BAJA",
|
|
700
|
-
current_time_local.strftime("%H:%M"),
|
|
701
|
-
min_update_time.strftime("%H:%M"),
|
|
702
|
-
should_update,
|
|
756
|
+
"[%s] Validación → cond1(días >=%d)=%s | cond2(hora >=%s)=%s | "
|
|
757
|
+
"cond3(>%dh desde %s)=%s | cuota=%d (%s) | actualizar=%s",
|
|
758
|
+
file_path.name, min_days, cond1,
|
|
759
|
+
min_update_time.strftime("%H:%M"), cond2,
|
|
760
|
+
hours_threshold, last_update_str or "nunca", cond3,
|
|
761
|
+
self.limit_prediccio, quota_level, should_update
|
|
703
762
|
)
|
|
704
763
|
|
|
705
764
|
if should_update:
|
|
706
|
-
_LOGGER.
|
|
707
|
-
|
|
708
|
-
file_path.name, self.limit_prediccio
|
|
709
|
-
)
|
|
710
|
-
return None # → forzar actualización
|
|
765
|
+
_LOGGER.info("Datos obsoletos → llamando API para %s", file_path.name)
|
|
766
|
+
return None
|
|
711
767
|
|
|
712
|
-
_LOGGER.debug("Datos válidos
|
|
768
|
+
_LOGGER.debug("Datos válidos → usando caché %s", file_path.name)
|
|
713
769
|
return data
|
|
714
770
|
|
|
771
|
+
except json.JSONDecodeError:
|
|
772
|
+
_LOGGER.error("JSON corrupto en %s", file_path)
|
|
773
|
+
return None
|
|
715
774
|
except Exception as e:
|
|
716
|
-
_LOGGER.
|
|
775
|
+
_LOGGER.error("Error validando %s: %s", file_path, e)
|
|
717
776
|
return None
|
|
718
777
|
|
|
719
|
-
# --------------------------------------------------------------------- #
|
|
720
|
-
# OBTENCIÓN Y GUARDADO DE DATOS DESDE LA API
|
|
721
|
-
# --------------------------------------------------------------------- #
|
|
722
778
|
async def _fetch_and_save_data(self, api_method, file_path: Path) -> dict:
|
|
723
|
-
"""Obtiene datos de la API
|
|
779
|
+
"""Obtiene datos de la API, los procesa y guarda con timestamp."""
|
|
724
780
|
try:
|
|
725
781
|
data = await asyncio.wait_for(api_method(self.town_id), timeout=30)
|
|
726
782
|
|
|
@@ -734,16 +790,24 @@ class MeteocatEntityCoordinator(DataUpdateCoordinator):
|
|
|
734
790
|
):
|
|
735
791
|
details["valor"] = "0.0"
|
|
736
792
|
|
|
737
|
-
|
|
793
|
+
# Añadir timestamp de actualización exitosa
|
|
794
|
+
now_iso = datetime.now(TIMEZONE).isoformat()
|
|
795
|
+
enhanced_data = {
|
|
796
|
+
"actualitzat": {"dataUpdate": now_iso},
|
|
797
|
+
**data
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
await save_json_to_file(enhanced_data, file_path)
|
|
801
|
+
_LOGGER.debug("Guardado %s con dataUpdate: %s", file_path.name, now_iso)
|
|
738
802
|
|
|
739
|
-
# Actualizar cuotas
|
|
803
|
+
# Actualizar cuotas
|
|
740
804
|
if api_method.__name__ in ("get_prediccion_horaria", "get_prediccion_diaria"):
|
|
741
805
|
await _update_quotes(self.hass, "Prediccio")
|
|
742
806
|
|
|
743
|
-
return
|
|
807
|
+
return enhanced_data
|
|
744
808
|
|
|
745
809
|
except Exception as err:
|
|
746
|
-
_LOGGER.error(
|
|
810
|
+
_LOGGER.error("Error al obtener/guardar %s: %s", file_path, err)
|
|
747
811
|
raise
|
|
748
812
|
|
|
749
813
|
# --------------------------------------------------------------------- #
|
|
@@ -801,7 +865,7 @@ class MeteocatEntityCoordinator(DataUpdateCoordinator):
|
|
|
801
865
|
# === FALLBACK SEGURO ===
|
|
802
866
|
hourly_cache = await load_json_from_file(self.hourly_file) or {}
|
|
803
867
|
daily_cache = await load_json_from_file(self.daily_file) or {}
|
|
804
|
-
|
|
868
|
+
|
|
805
869
|
# --- Fecha horaria ---
|
|
806
870
|
h_raw = hourly_cache.get("dies", [{}])[0].get("data", "")
|
|
807
871
|
try:
|
|
@@ -825,7 +889,7 @@ class MeteocatEntityCoordinator(DataUpdateCoordinator):
|
|
|
825
889
|
self.hourly_file.name, h_display,
|
|
826
890
|
self.daily_file.name, d_display
|
|
827
891
|
)
|
|
828
|
-
|
|
892
|
+
|
|
829
893
|
self.async_set_updated_data({"hourly": hourly_cache, "daily": daily_cache})
|
|
830
894
|
return {"hourly": hourly_cache, "daily": daily_cache}
|
|
831
895
|
|
|
@@ -97,6 +97,10 @@ from .const import (
|
|
|
97
97
|
DEFAULT_VALIDITY_DAYS,
|
|
98
98
|
DEFAULT_VALIDITY_HOURS,
|
|
99
99
|
DEFAULT_VALIDITY_MINUTES,
|
|
100
|
+
DEFAULT_UVI_LOW_VALIDITY_HOURS,
|
|
101
|
+
DEFAULT_UVI_LOW_VALIDITY_MINUTES,
|
|
102
|
+
DEFAULT_UVI_HIGH_VALIDITY_HOURS,
|
|
103
|
+
DEFAULT_UVI_HIGH_VALIDITY_MINUTES,
|
|
100
104
|
DEFAULT_ALERT_VALIDITY_TIME,
|
|
101
105
|
DEFAULT_QUOTES_VALIDITY_TIME,
|
|
102
106
|
ALERT_VALIDITY_MULTIPLIER_100,
|
|
@@ -114,6 +118,10 @@ from .const import (
|
|
|
114
118
|
MOON_FILE_STATUS,
|
|
115
119
|
MOONRISE,
|
|
116
120
|
MOONSET,
|
|
121
|
+
PREDICCIO_HIGH_QUOTA_LIMIT,
|
|
122
|
+
DEFAULT_HOURLY_FORECAST_MIN_HOURS_SINCE_LAST_UPDATE,
|
|
123
|
+
DEFAULT_DAILY_FORECAST_MIN_HOURS_SINCE_LAST_UPDATE,
|
|
124
|
+
DEFAULT_UVI_MIN_HOURS_SINCE_LAST_UPDATE,
|
|
117
125
|
)
|
|
118
126
|
|
|
119
127
|
from .coordinator import (
|
|
@@ -1181,46 +1189,83 @@ class MeteocatHourlyForecastStatusSensor(CoordinatorEntity[MeteocatEntityCoordin
|
|
|
1181
1189
|
self._town_name = entry_data["town_name"]
|
|
1182
1190
|
self._town_id = entry_data["town_id"]
|
|
1183
1191
|
self._station_id = entry_data["station_id"]
|
|
1192
|
+
self._limit_prediccio = entry_data["limit_prediccio"]
|
|
1184
1193
|
self._attr_unique_id = f"sensor.{DOMAIN}_{self._town_id}_hourly_status"
|
|
1185
1194
|
self._attr_entity_category = getattr(description, "entity_category", None)
|
|
1186
1195
|
|
|
1196
|
+
def _get_forecast_data(self, forecast_type: str) -> Optional[dict]:
|
|
1197
|
+
"""Devuelve los datos del tipo de forecast (hourly o daily) o None."""
|
|
1198
|
+
if not self.coordinator.data:
|
|
1199
|
+
return None
|
|
1200
|
+
return self.coordinator.data.get(forecast_type)
|
|
1201
|
+
|
|
1187
1202
|
def _get_first_date(self):
|
|
1188
|
-
hourly_data = self.
|
|
1189
|
-
if hourly_data and "dies" in hourly_data:
|
|
1190
|
-
|
|
1203
|
+
hourly_data = self._get_forecast_data("hourly")
|
|
1204
|
+
if hourly_data and "dies" in hourly_data and hourly_data["dies"]:
|
|
1205
|
+
try:
|
|
1206
|
+
first_date_str = hourly_data["dies"][0]["data"].rstrip("Z")
|
|
1207
|
+
return datetime.fromisoformat(first_date_str).date()
|
|
1208
|
+
except (ValueError, TypeError, KeyError, IndexError):
|
|
1209
|
+
_LOGGER.warning("No se pudo parsear primera fecha del forecast horario")
|
|
1210
|
+
return None
|
|
1211
|
+
return None
|
|
1212
|
+
|
|
1213
|
+
def _get_last_api_update(self):
|
|
1214
|
+
hourly_data = self._get_forecast_data("hourly")
|
|
1215
|
+
if hourly_data and "actualitzat" in hourly_data and "dataUpdate" in hourly_data["actualitzat"]:
|
|
1216
|
+
try:
|
|
1217
|
+
return datetime.fromisoformat(hourly_data["actualitzat"]["dataUpdate"])
|
|
1218
|
+
except ValueError:
|
|
1219
|
+
_LOGGER.warning("Formato inválido en dataUpdate del forecast horario")
|
|
1220
|
+
return None
|
|
1191
1221
|
return None
|
|
1192
1222
|
|
|
1193
1223
|
@property
|
|
1194
|
-
def native_value(self):
|
|
1224
|
+
def native_value(self) -> str:
|
|
1195
1225
|
first_date = self._get_first_date()
|
|
1196
|
-
if first_date:
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1226
|
+
if not first_date:
|
|
1227
|
+
_LOGGER.debug("Hourly status: no hay datos disponibles aún")
|
|
1228
|
+
return "unknown"
|
|
1229
|
+
|
|
1230
|
+
now_local = datetime.now(TIMEZONE)
|
|
1231
|
+
today = now_local.date()
|
|
1232
|
+
current_time = now_local.time()
|
|
1233
|
+
days_difference = (today - first_date).days
|
|
1234
|
+
|
|
1235
|
+
# Replicar lógica del coordinador
|
|
1236
|
+
min_days = DEFAULT_VALIDITY_DAYS if self._limit_prediccio >= PREDICCIO_HIGH_QUOTA_LIMIT else DEFAULT_VALIDITY_DAYS + 1
|
|
1237
|
+
min_time = time(DEFAULT_VALIDITY_HOURS + 1, DEFAULT_VALIDITY_MINUTES) # Margen adicional de +1 hora sobre la hora mínima configurada.
|
|
1238
|
+
|
|
1239
|
+
cond1 = days_difference >= min_days
|
|
1240
|
+
cond2 = current_time >= min_time
|
|
1241
|
+
|
|
1242
|
+
_LOGGER.debug(
|
|
1243
|
+
"Hourly status → días: %d (≥%d)=%s | hora: %s (≥%s)=%s → %s",
|
|
1244
|
+
days_difference, min_days, cond1,
|
|
1245
|
+
current_time.strftime("%H:%M"), min_time.strftime("%H:%M"), cond2,
|
|
1246
|
+
"obsolete" if cond1 and cond2 else "updated"
|
|
1247
|
+
)
|
|
1248
|
+
|
|
1249
|
+
if cond1 and cond2:
|
|
1250
|
+
return "obsolete"
|
|
1251
|
+
return "updated"
|
|
1212
1252
|
|
|
1213
1253
|
@property
|
|
1214
|
-
def extra_state_attributes(self):
|
|
1215
|
-
attributes =
|
|
1254
|
+
def extra_state_attributes(self) -> dict:
|
|
1255
|
+
attributes: dict = {}
|
|
1256
|
+
|
|
1216
1257
|
first_date = self._get_first_date()
|
|
1217
1258
|
if first_date:
|
|
1218
1259
|
attributes["update_date"] = first_date.isoformat()
|
|
1260
|
+
|
|
1261
|
+
last_update = self._get_last_api_update()
|
|
1262
|
+
if last_update:
|
|
1263
|
+
attributes["data_updatetime"] = last_update.isoformat()
|
|
1264
|
+
|
|
1219
1265
|
return attributes
|
|
1220
|
-
|
|
1266
|
+
|
|
1221
1267
|
@property
|
|
1222
1268
|
def device_info(self) -> DeviceInfo:
|
|
1223
|
-
"""Return the device info."""
|
|
1224
1269
|
return DeviceInfo(
|
|
1225
1270
|
identifiers={(DOMAIN, self._town_id)},
|
|
1226
1271
|
name=f"Meteocat {self._station_id} {self._town_name}",
|
|
@@ -1237,46 +1282,83 @@ class MeteocatDailyForecastStatusSensor(CoordinatorEntity[MeteocatEntityCoordina
|
|
|
1237
1282
|
self._town_name = entry_data["town_name"]
|
|
1238
1283
|
self._town_id = entry_data["town_id"]
|
|
1239
1284
|
self._station_id = entry_data["station_id"]
|
|
1285
|
+
self._limit_prediccio = entry_data["limit_prediccio"]
|
|
1240
1286
|
self._attr_unique_id = f"sensor.{DOMAIN}_{self._town_id}_daily_status"
|
|
1241
1287
|
self._attr_entity_category = getattr(description, "entity_category", None)
|
|
1242
1288
|
|
|
1289
|
+
def _get_forecast_data(self, forecast_type: str) -> Optional[dict]:
|
|
1290
|
+
"""Devuelve los datos del tipo de forecast (hourly o daily) o None."""
|
|
1291
|
+
if not self.coordinator.data:
|
|
1292
|
+
return None
|
|
1293
|
+
return self.coordinator.data.get(forecast_type)
|
|
1294
|
+
|
|
1243
1295
|
def _get_first_date(self):
|
|
1244
|
-
daily_data = self.
|
|
1245
|
-
if daily_data and "dies" in daily_data:
|
|
1246
|
-
|
|
1296
|
+
daily_data = self._get_forecast_data("daily")
|
|
1297
|
+
if daily_data and "dies" in daily_data and daily_data["dies"]:
|
|
1298
|
+
try:
|
|
1299
|
+
first_date_str = daily_data["dies"][0]["data"].rstrip("Z")
|
|
1300
|
+
return datetime.fromisoformat(first_date_str).date()
|
|
1301
|
+
except (ValueError, TypeError, KeyError, IndexError):
|
|
1302
|
+
_LOGGER.warning("No se pudo parsear primera fecha del forecast diario")
|
|
1303
|
+
return None
|
|
1304
|
+
return None
|
|
1305
|
+
|
|
1306
|
+
def _get_last_api_update(self):
|
|
1307
|
+
daily_data = self._get_forecast_data("daily")
|
|
1308
|
+
if daily_data and "actualitzat" in daily_data and "dataUpdate" in daily_data["actualitzat"]:
|
|
1309
|
+
try:
|
|
1310
|
+
return datetime.fromisoformat(daily_data["actualitzat"]["dataUpdate"])
|
|
1311
|
+
except ValueError:
|
|
1312
|
+
_LOGGER.warning("Formato inválido en dataUpdate del forecast diario")
|
|
1313
|
+
return None
|
|
1247
1314
|
return None
|
|
1248
1315
|
|
|
1249
1316
|
@property
|
|
1250
|
-
def native_value(self):
|
|
1317
|
+
def native_value(self) -> str:
|
|
1251
1318
|
first_date = self._get_first_date()
|
|
1252
|
-
if first_date:
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1319
|
+
if not first_date:
|
|
1320
|
+
_LOGGER.debug("Daily status: no hay datos disponibles aún")
|
|
1321
|
+
return "unknown"
|
|
1322
|
+
|
|
1323
|
+
now_local = datetime.now(TIMEZONE)
|
|
1324
|
+
today = now_local.date()
|
|
1325
|
+
current_time = now_local.time()
|
|
1326
|
+
days_difference = (today - first_date).days
|
|
1327
|
+
|
|
1328
|
+
# Replicar lógica del coordinador
|
|
1329
|
+
min_days = DEFAULT_VALIDITY_DAYS if self._limit_prediccio >= PREDICCIO_HIGH_QUOTA_LIMIT else DEFAULT_VALIDITY_DAYS + 1
|
|
1330
|
+
min_time = time(DEFAULT_VALIDITY_HOURS + 1, DEFAULT_VALIDITY_MINUTES) # Margen adicional de +1 hora sobre la hora mínima configurada.
|
|
1331
|
+
|
|
1332
|
+
cond1 = days_difference >= min_days
|
|
1333
|
+
cond2 = current_time >= min_time
|
|
1334
|
+
|
|
1335
|
+
_LOGGER.debug(
|
|
1336
|
+
"Daily status → días: %d (≥%d)=%s | hora: %s (≥%s)=%s → %s",
|
|
1337
|
+
days_difference, min_days, cond1,
|
|
1338
|
+
current_time.strftime("%H:%M"), min_time.strftime("%H:%M"), cond2,
|
|
1339
|
+
"obsolete" if cond1 and cond2 else "updated"
|
|
1340
|
+
)
|
|
1341
|
+
|
|
1342
|
+
if cond1 and cond2:
|
|
1343
|
+
return "obsolete"
|
|
1344
|
+
return "updated"
|
|
1268
1345
|
|
|
1269
1346
|
@property
|
|
1270
|
-
def extra_state_attributes(self):
|
|
1271
|
-
attributes =
|
|
1347
|
+
def extra_state_attributes(self) -> dict:
|
|
1348
|
+
attributes: dict = {}
|
|
1349
|
+
|
|
1272
1350
|
first_date = self._get_first_date()
|
|
1273
1351
|
if first_date:
|
|
1274
1352
|
attributes["update_date"] = first_date.isoformat()
|
|
1353
|
+
|
|
1354
|
+
last_update = self._get_last_api_update()
|
|
1355
|
+
if last_update:
|
|
1356
|
+
attributes["data_updatetime"] = last_update.isoformat()
|
|
1357
|
+
|
|
1275
1358
|
return attributes
|
|
1276
|
-
|
|
1359
|
+
|
|
1277
1360
|
@property
|
|
1278
1361
|
def device_info(self) -> DeviceInfo:
|
|
1279
|
-
"""Return the device info."""
|
|
1280
1362
|
return DeviceInfo(
|
|
1281
1363
|
identifiers={(DOMAIN, self._town_id)},
|
|
1282
1364
|
name=f"Meteocat {self._station_id} {self._town_name}",
|
|
@@ -1293,40 +1375,91 @@ class MeteocatUviStatusSensor(CoordinatorEntity[MeteocatUviCoordinator], SensorE
|
|
|
1293
1375
|
self._town_name = entry_data["town_name"]
|
|
1294
1376
|
self._town_id = entry_data["town_id"]
|
|
1295
1377
|
self._station_id = entry_data["station_id"]
|
|
1378
|
+
self._limit_prediccio = entry_data["limit_prediccio"]
|
|
1296
1379
|
self._attr_unique_id = f"sensor.{DOMAIN}_{self._town_id}_uvi_status"
|
|
1297
1380
|
self._attr_entity_category = getattr(description, "entity_category", None)
|
|
1298
1381
|
|
|
1382
|
+
def _get_uvi_data_dict(self):
|
|
1383
|
+
"""Devuelve el diccionario completo de datos UVI (incluyendo 'actualitzat') o None."""
|
|
1384
|
+
if self.coordinator.data and isinstance(self.coordinator.data, dict):
|
|
1385
|
+
return self.coordinator.data
|
|
1386
|
+
return None
|
|
1387
|
+
|
|
1299
1388
|
def _get_first_date(self):
|
|
1300
|
-
|
|
1301
|
-
|
|
1389
|
+
data_dict = self._get_uvi_data_dict()
|
|
1390
|
+
if data_dict and "uvi" in data_dict and data_dict["uvi"]:
|
|
1391
|
+
try:
|
|
1392
|
+
return datetime.strptime(data_dict["uvi"][0].get("date"), "%Y-%m-%d").date()
|
|
1393
|
+
except (ValueError, TypeError, KeyError):
|
|
1394
|
+
return None
|
|
1395
|
+
return None
|
|
1396
|
+
|
|
1397
|
+
def _get_last_api_update(self):
|
|
1398
|
+
"""Devuelve el datetime de la última llamada exitosa a la API o None."""
|
|
1399
|
+
data_dict = self._get_uvi_data_dict()
|
|
1400
|
+
if data_dict and "actualitzat" in data_dict and "dataUpdate" in data_dict["actualitzat"]:
|
|
1401
|
+
try:
|
|
1402
|
+
return datetime.fromisoformat(data_dict["actualitzat"]["dataUpdate"])
|
|
1403
|
+
except ValueError:
|
|
1404
|
+
_LOGGER.warning("Formato inválido en dataUpdate del sensor UVI status")
|
|
1405
|
+
return None
|
|
1302
1406
|
return None
|
|
1303
1407
|
|
|
1304
1408
|
@property
|
|
1305
|
-
def native_value(self):
|
|
1409
|
+
def native_value(self) -> str:
|
|
1410
|
+
data_dict = self._get_uvi_data_dict()
|
|
1411
|
+
if not data_dict:
|
|
1412
|
+
_LOGGER.debug("UVI Status: no hay datos disponibles aún")
|
|
1413
|
+
return "unknown"
|
|
1414
|
+
|
|
1306
1415
|
first_date = self._get_first_date()
|
|
1307
|
-
if first_date:
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1416
|
+
if not first_date:
|
|
1417
|
+
_LOGGER.debug("UVI Status: no se pudo obtener first_date")
|
|
1418
|
+
return "unknown"
|
|
1419
|
+
|
|
1420
|
+
now_local = datetime.now(TIMEZONE)
|
|
1421
|
+
today = now_local.date()
|
|
1422
|
+
current_time = now_local.time()
|
|
1423
|
+
days_difference = (today - first_date).days
|
|
1424
|
+
|
|
1425
|
+
# ── Replicar lógica exacta del coordinador ──
|
|
1426
|
+
if self._limit_prediccio >= PREDICCIO_HIGH_QUOTA_LIMIT:
|
|
1427
|
+
min_days = DEFAULT_VALIDITY_DAYS
|
|
1428
|
+
min_time = time(DEFAULT_UVI_HIGH_VALIDITY_HOURS + 1, DEFAULT_UVI_HIGH_VALIDITY_MINUTES) # Margen adicional de +1 hora sobre la hora mínima configurada.
|
|
1429
|
+
quota_level = "ALTA"
|
|
1430
|
+
else:
|
|
1431
|
+
min_days = DEFAULT_VALIDITY_DAYS + 1
|
|
1432
|
+
min_time = time(DEFAULT_UVI_LOW_VALIDITY_HOURS + 1, DEFAULT_UVI_LOW_VALIDITY_MINUTES) # Margen adicional de +1 hora sobre la hora mínima configurada.
|
|
1433
|
+
quota_level = "BAJA"
|
|
1434
|
+
|
|
1435
|
+
cond1 = days_difference >= min_days
|
|
1436
|
+
cond2 = current_time >= min_time
|
|
1437
|
+
|
|
1438
|
+
_LOGGER.debug(
|
|
1439
|
+
"UVI Status → días: %d (≥%d)=%s | hora: %s (≥%s)=%s → %s",
|
|
1440
|
+
days_difference, min_days, cond1,
|
|
1441
|
+
current_time.strftime("%H:%M"), min_time.strftime("%H:%M"), cond2,
|
|
1442
|
+
self._limit_prediccio, quota_level,
|
|
1443
|
+
"obsolete" if cond1 and cond2 else "updated"
|
|
1444
|
+
)
|
|
1445
|
+
|
|
1446
|
+
if cond1 and cond2:
|
|
1447
|
+
return "obsolete"
|
|
1448
|
+
return "updated"
|
|
1323
1449
|
|
|
1324
1450
|
@property
|
|
1325
|
-
def extra_state_attributes(self):
|
|
1326
|
-
attributes =
|
|
1451
|
+
def extra_state_attributes(self) -> dict:
|
|
1452
|
+
attributes: dict = {}
|
|
1453
|
+
|
|
1454
|
+
# Primera fecha de los datos UVI
|
|
1327
1455
|
first_date = self._get_first_date()
|
|
1328
1456
|
if first_date:
|
|
1329
1457
|
attributes["update_date"] = first_date.isoformat()
|
|
1458
|
+
# Última actualizaciónde la API
|
|
1459
|
+
last_update = self._get_last_api_update()
|
|
1460
|
+
if last_update:
|
|
1461
|
+
attributes["data_updatetime"] = last_update.isoformat()
|
|
1462
|
+
|
|
1330
1463
|
return attributes
|
|
1331
1464
|
|
|
1332
1465
|
@property
|
|
@@ -212,6 +212,9 @@
|
|
|
212
212
|
"state_attributes": {
|
|
213
213
|
"update_date": {
|
|
214
214
|
"name": "Date"
|
|
215
|
+
},
|
|
216
|
+
"data_updatetime": {
|
|
217
|
+
"name": "Updated"
|
|
215
218
|
}
|
|
216
219
|
}
|
|
217
220
|
},
|
|
@@ -224,6 +227,9 @@
|
|
|
224
227
|
"state_attributes": {
|
|
225
228
|
"update_date": {
|
|
226
229
|
"name": "Date"
|
|
230
|
+
},
|
|
231
|
+
"data_updatetime": {
|
|
232
|
+
"name": "Updated"
|
|
227
233
|
}
|
|
228
234
|
}
|
|
229
235
|
},
|
|
@@ -236,6 +242,9 @@
|
|
|
236
242
|
"state_attributes": {
|
|
237
243
|
"update_date": {
|
|
238
244
|
"name": "Date"
|
|
245
|
+
},
|
|
246
|
+
"data_updatetime": {
|
|
247
|
+
"name": "Updated"
|
|
239
248
|
}
|
|
240
249
|
}
|
|
241
250
|
},
|
|
@@ -212,6 +212,9 @@
|
|
|
212
212
|
"state_attributes": {
|
|
213
213
|
"update_date": {
|
|
214
214
|
"name": "Data"
|
|
215
|
+
},
|
|
216
|
+
"data_updatetime": {
|
|
217
|
+
"name": "Actualitzat"
|
|
215
218
|
}
|
|
216
219
|
}
|
|
217
220
|
},
|
|
@@ -224,6 +227,9 @@
|
|
|
224
227
|
"state_attributes": {
|
|
225
228
|
"update_date": {
|
|
226
229
|
"name": "Data"
|
|
230
|
+
},
|
|
231
|
+
"data_updatetime": {
|
|
232
|
+
"name": "Actualitzat"
|
|
227
233
|
}
|
|
228
234
|
}
|
|
229
235
|
},
|
|
@@ -236,6 +242,9 @@
|
|
|
236
242
|
"state_attributes": {
|
|
237
243
|
"update_date": {
|
|
238
244
|
"name": "Data"
|
|
245
|
+
},
|
|
246
|
+
"data_updatetime": {
|
|
247
|
+
"name": "Actualitzat"
|
|
239
248
|
}
|
|
240
249
|
}
|
|
241
250
|
},
|
|
@@ -212,6 +212,9 @@
|
|
|
212
212
|
"state_attributes": {
|
|
213
213
|
"update_date": {
|
|
214
214
|
"name": "Date"
|
|
215
|
+
},
|
|
216
|
+
"data_updatetime": {
|
|
217
|
+
"name": "Updated"
|
|
215
218
|
}
|
|
216
219
|
}
|
|
217
220
|
},
|
|
@@ -224,6 +227,9 @@
|
|
|
224
227
|
"state_attributes": {
|
|
225
228
|
"update_date": {
|
|
226
229
|
"name": "Date"
|
|
230
|
+
},
|
|
231
|
+
"data_updatetime": {
|
|
232
|
+
"name": "Updated"
|
|
227
233
|
}
|
|
228
234
|
}
|
|
229
235
|
},
|
|
@@ -236,6 +242,9 @@
|
|
|
236
242
|
"state_attributes": {
|
|
237
243
|
"update_date": {
|
|
238
244
|
"name": "Date"
|
|
245
|
+
},
|
|
246
|
+
"data_updatetime": {
|
|
247
|
+
"name": "Updated"
|
|
239
248
|
}
|
|
240
249
|
}
|
|
241
250
|
},
|
|
@@ -212,6 +212,9 @@
|
|
|
212
212
|
"state_attributes": {
|
|
213
213
|
"update_date": {
|
|
214
214
|
"name": "Fecha"
|
|
215
|
+
},
|
|
216
|
+
"data_updatetime": {
|
|
217
|
+
"name": "Actualizado"
|
|
215
218
|
}
|
|
216
219
|
}
|
|
217
220
|
},
|
|
@@ -224,6 +227,9 @@
|
|
|
224
227
|
"state_attributes": {
|
|
225
228
|
"update_date": {
|
|
226
229
|
"name": "Fecha"
|
|
230
|
+
},
|
|
231
|
+
"data_updatetime": {
|
|
232
|
+
"name": "Actualizado"
|
|
227
233
|
}
|
|
228
234
|
}
|
|
229
235
|
},
|
|
@@ -236,6 +242,9 @@
|
|
|
236
242
|
"state_attributes": {
|
|
237
243
|
"update_date": {
|
|
238
244
|
"name": "Fecha"
|
|
245
|
+
},
|
|
246
|
+
"data_updatetime": {
|
|
247
|
+
"name": "Actualizado"
|
|
239
248
|
}
|
|
240
249
|
}
|
|
241
250
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "4.0.
|
|
1
|
+
__version__ = "4.0.5"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "meteocat",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.5",
|
|
4
4
|
"description": "[](https://opensource.org/licenses/Apache-2.0)\r [](https://pypi.org/project/meteocat)\r [](https://gitlab.com/figorr/meteocat/commits/master)",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"directories": {
|