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.
@@ -32,10 +32,10 @@ jobs:
32
32
  id: ha-version
33
33
  run: |
34
34
  # ──────────────────────────────────────────────────────────────
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
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
 
@@ -24,7 +24,7 @@ from .const import DOMAIN, PLATFORMS
24
24
  _LOGGER = logging.getLogger(__name__)
25
25
 
26
26
  # Versión
27
- __version__ = "4.0.3"
27
+ __version__ = "4.0.5"
28
28
 
29
29
  # Definir el esquema de configuración CONFIG_SCHEMA
30
30
  CONFIG_SCHEMA = vol.Schema(
@@ -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: misma lógica que predicción, basada en limit_prediccio.
410
-
411
- - Si `limit_prediccio >= 550` → actualiza **el día siguiente** después de las DEFAULT_VALIDITY_HOURS:DEFAULT_VALIDITY_MINUTES.
412
- - Si `limit_prediccio < 550` → actualiza **dos días después** después de las DEFAULT_VALIDITY_HOURS:DEFAULT_VALIDITY_MINUTES.
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
- async with aiofiles.open(self.uvi_file, "r", encoding="utf-8") as f:
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
- # Fecha del primer día
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
- # === LÓGICA DINÁMICA SEGÚN CUOTA ===
442
+ # Determinar umbrales según cuota
447
443
  if self.limit_prediccio >= PREDICCIO_HIGH_QUOTA_LIMIT:
448
- should_update = days_diff >= DEFAULT_VALIDITY_DAYS and current_time_local >= min_update_time_high
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
- should_update = days_diff > DEFAULT_VALIDITY_DAYS and current_time_local >= min_update_time_low
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: primer_día=%s, hoy=%s días=%d, "
454
- "cuota=%d (%s), hora=%s %s actualizar=%s",
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
- first_date,
457
- today,
458
- days_diff,
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
- "ALTA" if self.limit_prediccio >= 550 else "BAJA",
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) -> List[Dict]:
484
- """Actualiza los datos de UVI desde la API de Meteocat o caché."""
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["uvi"]
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
- await save_json_to_file(data, self.uvi_file)
504
- return data["uvi"]
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
- # === FALLBACK SEGURO ===
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["uvi"])
540
- return cached_data["uvi"]
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 y retorna datos de predicción si son válidos.
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("El archivo %s no existe. Se considerará inválido.", file_path)
693
+ _LOGGER.warning("Archivo no existe: %s", file_path)
661
694
  return None
695
+
662
696
  try:
663
- async with aiofiles.open(file_path, "r", encoding="utf-8") as f:
664
- content = await f.read()
665
- data = json.loads(content)
697
+ data = await load_json_from_file(file_path)
666
698
 
667
- # Fecha del primer día de predicción (solo fecha)
668
- first_date_str = data["dies"][0]["data"].rstrip("Z")
669
- first_date = datetime.fromisoformat(first_date_str).date()
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
- # Hora actual en zona local (Europe/Madrid)
673
- current_time_local = datetime.now(TIMEZONE).time()
674
- min_update_time = time(DEFAULT_VALIDITY_HOURS, DEFAULT_VALIDITY_MINUTES)
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
- # Cuota alta → actualiza cuando los datos son de ayer (o antes) + hora OK
683
- should_update = days_diff >= DEFAULT_VALIDITY_DAYS and current_time_local >= min_update_time
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
- # Cuota baja → actualiza solo cuando los datos son de anteayer + hora OK
686
- should_update = days_diff > DEFAULT_VALIDITY_DAYS and current_time_local >= min_update_time
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: primer_día=%s, hoy=%s días=%d, "
693
- "cuota=%d (%s), hora_local=%s %s actualizar=%s",
694
- file_path.name,
695
- first_date,
696
- today,
697
- days_diff,
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.debug(
707
- "Datos obsoletos o actualizables → llamando API (%s, cuota=%d)",
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 en %s → usando caché", file_path.name)
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.warning("Error validando %s: %s", file_path, e)
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 y los guarda en un archivo JSON."""
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
- await save_json_to_file(data, file_path)
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 (dependiendo del tipo de predicción horaria/diaria)
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 data
807
+ return enhanced_data
744
808
 
745
809
  except Exception as err:
746
- _LOGGER.error(f"Error al obtener datos de la API para {file_path}: {err}")
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
 
@@ -21,5 +21,5 @@
21
21
  "packaging>=20.3",
22
22
  "wrapt>=1.14.0"
23
23
  ],
24
- "version": "4.0.3"
24
+ "version": "4.0.5"
25
25
  }
@@ -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.coordinator.data.get("hourly")
1189
- if hourly_data and "dies" in hourly_data:
1190
- return datetime.fromisoformat(hourly_data["dies"][0]["data"].rstrip("Z")).date()
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
- today = datetime.now(timezone.utc).date()
1198
- current_time = datetime.now(timezone.utc).time()
1199
- days_difference = (today - first_date).days
1200
- _LOGGER.debug(
1201
- f"Diferencia de días para predicciones horarias: {days_difference}."
1202
- f"Hora actual de validación: {current_time}."
1203
- f"Para la validación: "
1204
- f"número de días= {DEFAULT_VALIDITY_DAYS}, "
1205
- f"hora de contacto a la API >= {DEFAULT_VALIDITY_HOURS}, "
1206
- f"minutos de contacto a la API >= {DEFAULT_VALIDITY_MINUTES}."
1207
- )
1208
- if days_difference > DEFAULT_VALIDITY_DAYS and current_time >= time(DEFAULT_VALIDITY_HOURS, DEFAULT_VALIDITY_MINUTES):
1209
- return "obsolete"
1210
- return "updated"
1211
- return "unknown"
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 = super().extra_state_attributes or {}
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.coordinator.data.get("daily")
1245
- if daily_data and "dies" in daily_data:
1246
- return datetime.fromisoformat(daily_data["dies"][0]["data"].rstrip("Z")).date()
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
- today = datetime.now(timezone.utc).date()
1254
- current_time = datetime.now(timezone.utc).time()
1255
- days_difference = (today - first_date).days
1256
- _LOGGER.debug(
1257
- f"Diferencia de días para predicciones diarias: {days_difference}."
1258
- f"Hora actual de validación: {current_time}."
1259
- f"Para la validación: "
1260
- f"número de días= {DEFAULT_VALIDITY_DAYS}, "
1261
- f"hora de contacto a la API >= {DEFAULT_VALIDITY_HOURS}, "
1262
- f"minutos de contacto a la API >= {DEFAULT_VALIDITY_MINUTES}."
1263
- )
1264
- if days_difference > DEFAULT_VALIDITY_DAYS and current_time >= time(DEFAULT_VALIDITY_HOURS, DEFAULT_VALIDITY_MINUTES):
1265
- return "obsolete"
1266
- return "updated"
1267
- return "unknown"
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 = super().extra_state_attributes or {}
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
- if self.coordinator.data:
1301
- return datetime.strptime(self.coordinator.data[0].get("date"), "%Y-%m-%d").date()
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
- today = datetime.now(timezone.utc).date()
1309
- current_time = datetime.now(timezone.utc).time()
1310
- days_difference = (today - first_date).days
1311
- _LOGGER.debug(
1312
- f"Diferencia de días para datos UVI: {days_difference}."
1313
- f"Hora actual de validación: {current_time}."
1314
- f"Para la validación: "
1315
- f"número de días= {DEFAULT_VALIDITY_DAYS}, "
1316
- f"hora de contacto a la API >= {DEFAULT_VALIDITY_HOURS}, "
1317
- f"minutos de contacto a la API >= {DEFAULT_VALIDITY_MINUTES}."
1318
- )
1319
- if days_difference > DEFAULT_VALIDITY_DAYS and current_time >= time(DEFAULT_VALIDITY_HOURS, DEFAULT_VALIDITY_MINUTES):
1320
- return "obsolete"
1321
- return "updated"
1322
- return "unknown"
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 = super().extra_state_attributes or {}
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.3"
1
+ __version__ = "4.0.5"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "meteocat",
3
- "version": "4.0.3",
3
+ "version": "4.0.5",
4
4
  "description": "[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)\r [![Python version compatibility](https://img.shields.io/pypi/pyversions/meteocat)](https://pypi.org/project/meteocat)\r [![pipeline status](https://gitlab.com/figorr/meteocat/badges/master/pipeline.svg)](https://gitlab.com/figorr/meteocat/commits/master)",
5
5
  "main": "index.js",
6
6
  "directories": {
package/pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "meteocat"
3
- version = "4.0.3"
3
+ version = "4.0.5"
4
4
  description = "Script para obtener datos meteorológicos de la API de Meteocat"
5
5
  authors = ["figorr <jdcuartero@yahoo.es>"]
6
6
  license = "Apache-2.0"