meteocat 0.1.47 → 0.1.49

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/CHANGELOG.md CHANGED
@@ -1,3 +1,20 @@
1
+ ## [0.1.49](https://github.com/figorr/meteocat/compare/v0.1.48...v0.1.49) (2025-01-03)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * 0.1.49 ([81dea76](https://github.com/figorr/meteocat/commit/81dea7618fc1b3fd6aa4edff923720e424ca7f8d))
7
+ * add daily precipitation probability sensor ([4d59c3f](https://github.com/figorr/meteocat/commit/4d59c3f9480c09da678fd6ce7efa775619a17d86))
8
+ * add daily precipitation probability sensor ([c786f26](https://github.com/figorr/meteocat/commit/c786f26d36b8f17bb9512d405a5a2ba921a8b881))
9
+
10
+ ## [0.1.48](https://github.com/figorr/meteocat/compare/v0.1.47...v0.1.48) (2025-01-03)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * 0.1.48 ([b8b8830](https://github.com/figorr/meteocat/commit/b8b8830a23b0d63744f30c3dff33fc9f172a3c01))
16
+ * fix UTC to local time conversion ([14cf7d2](https://github.com/figorr/meteocat/commit/14cf7d2ca696ede2bedcd6041195df44fdd6d245))
17
+
1
18
  ## [0.1.47](https://github.com/figorr/meteocat/compare/v0.1.46...v0.1.47) (2025-01-02)
2
19
 
3
20
 
@@ -25,7 +25,7 @@ from .const import DOMAIN, PLATFORMS
25
25
  _LOGGER = logging.getLogger(__name__)
26
26
 
27
27
  # Versión
28
- __version__ = "0.1.47"
28
+ __version__ = "0.1.49"
29
29
 
30
30
  def safe_remove(path: Path, is_folder: bool = False):
31
31
  """Elimina de forma segura un archivo o carpeta si existe."""
@@ -40,6 +40,7 @@ HUMIDITY = "humidity" # Humedad relativa
40
40
  PRESSURE = "pressure" # Presión atmosférica
41
41
  PRECIPITATION = "precipitation" # Precipitación
42
42
  PRECIPITATION_ACCUMULATED = "precipitation_accumulated" #Precipitación acumulada
43
+ PRECIPITATION_PROBABILITY = "precipitation_probability" #Precipitación probabilidad
43
44
  SOLAR_GLOBAL_IRRADIANCE = "solar_global_irradiance" # Irradiación solar global
44
45
  UV_INDEX = "uv_index" # UV
45
46
  MAX_TEMPERATURE = "max_temperature" # Temperatura máxima
@@ -6,7 +6,7 @@ import aiofiles
6
6
  import logging
7
7
  import asyncio
8
8
  from datetime import datetime, timedelta, timezone, time
9
- from zoneinfo import ZoneInfo
9
+ from zoneinfo import ZoneInfo
10
10
  from typing import Dict, Any
11
11
 
12
12
  from homeassistant.core import HomeAssistant
@@ -48,6 +48,9 @@ DEFAULT_UVI_SENSOR_UPDATE_INTERVAL = timedelta(minutes=5)
48
48
  DEFAULT_CONDITION_SENSOR_UPDATE_INTERVAL = timedelta(minutes=5)
49
49
  DEFAULT_TEMP_FORECAST_UPDATE_INTERVAL = timedelta(minutes=5)
50
50
 
51
+ # Definir la zona horaria local
52
+ TIMEZONE = ZoneInfo("Europe/Madrid")
53
+
51
54
  async def save_json_to_file(data: dict, output_file: str) -> None:
52
55
  """Guarda datos JSON en un archivo de forma asíncrona."""
53
56
  try:
@@ -81,30 +84,6 @@ async def load_json_from_file(input_file: str) -> dict:
81
84
  _LOGGER.error("Error al decodificar JSON del archivo %s: %s", input_file, err)
82
85
  return {}
83
86
 
84
- # Cambiar UTC a la zona horaria local
85
- def convert_to_local_time(utc_time: str, local_tz: str = "Europe/Madrid") -> datetime | None:
86
- """
87
- Convierte una fecha/hora UTC en formato ISO 8601 a la zona horaria local especificada.
88
-
89
- Args:
90
- utc_time (str): Fecha/hora en formato ISO 8601 (ejemplo: '2025-01-02T12:00:00Z').
91
- local_tz (str): Zona horaria local en formato IANA (por defecto, 'Europe/Madrid').
92
-
93
- Returns:
94
- datetime | None: Objeto datetime convertido a la zona horaria local, o None si hay un error.
95
- """
96
- try:
97
- utc_dt = datetime.fromisoformat(utc_time.replace("Z", "+00:00"))
98
- local_dt = utc_dt.replace(tzinfo=ZoneInfo("UTC")).astimezone(ZoneInfo(local_tz))
99
- return local_dt
100
- except ValueError:
101
- return None
102
-
103
- # Obtener la hora local en lugar de la hora UTC
104
- def get_local_datetime(tz: str = "Europe/Madrid"):
105
- """Obtiene la fecha y hora local según la zona horaria proporcionada."""
106
- return datetime.now(ZoneInfo(tz))
107
-
108
87
  class MeteocatSensorCoordinator(DataUpdateCoordinator):
109
88
  """Coordinator para manejar la actualización de datos de los sensores."""
110
89
 
@@ -311,9 +290,9 @@ class MeteocatUviCoordinator(DataUpdateCoordinator):
311
290
  data = json.loads(content)
312
291
 
313
292
  # Validar la fecha del primer elemento superior a 1 día
314
- first_date = convert_to_local_time(data["uvi"][0].get("date"), "%Y-%m-%d").date()
315
- today = get_local_datetime().date() # Usar la hora local
316
- current_time = get_local_datetime().time()
293
+ first_date = datetime.strptime(data["uvi"][0].get("date"), "%Y-%m-%d").date()
294
+ today = datetime.now(timezone.utc).date()
295
+ current_time = datetime.now(timezone.utc).time()
317
296
 
318
297
  # Log detallado
319
298
  _LOGGER.info(
@@ -453,8 +432,8 @@ class MeteocatUviFileCoordinator(DataUpdateCoordinator):
453
432
 
454
433
  def _get_uv_for_current_hour(self, raw_data):
455
434
  """Get UV data for the current hour."""
456
- # # Obtiene la fecha y hora locales
457
- current_datetime = get_local_datetime()
435
+ # Fecha y hora actual
436
+ current_datetime = datetime.now()
458
437
  current_date = current_datetime.strftime("%Y-%m-%d")
459
438
  current_hour = current_datetime.hour
460
439
 
@@ -535,10 +514,10 @@ class MeteocatEntityCoordinator(DataUpdateCoordinator):
535
514
  content = await f.read()
536
515
  data = json.loads(content)
537
516
 
538
- # Convertir la fecha del primer día a la zona horaria local
539
- first_date = convert_to_local_time(data["dies"][0]["data"]).date()
540
- today = get_local_datetime().date() # Usar la hora local
541
- current_time = get_local_datetime().time()
517
+ # Obtener la fecha del primer día
518
+ first_date = datetime.fromisoformat(data["dies"][0]["data"].rstrip("Z")).date()
519
+ today = datetime.now(timezone.utc).date()
520
+ current_time = datetime.now(timezone.utc).time()
542
521
 
543
522
  # Log detallado
544
523
  _LOGGER.info(
@@ -577,11 +556,6 @@ class MeteocatEntityCoordinator(DataUpdateCoordinator):
577
556
  hourly_data = await self._fetch_and_save_data(
578
557
  self.meteocat_forecast.get_prediccion_horaria, self.hourly_file
579
558
  )
580
-
581
- # Convertir las fechas horarias a hora local
582
- for day in hourly_data.get("dies", []):
583
- for hour_data in day["variables"].get("temp", {}).get("valors", []):
584
- hour_data["data"] = convert_to_local_time(hour_data["data"]).isoformat()
585
559
 
586
560
  # Validar o actualizar datos diarios
587
561
  daily_data = await self.validate_forecast_data(self.daily_file)
@@ -590,10 +564,6 @@ class MeteocatEntityCoordinator(DataUpdateCoordinator):
590
564
  self.meteocat_forecast.get_prediccion_diaria, self.daily_file
591
565
  )
592
566
 
593
- # Convertir las fechas diarias a hora local
594
- for day in daily_data.get("dies", []):
595
- day["data"] = convert_to_local_time(day["data"]).isoformat()
596
-
597
567
  return {"hourly": hourly_data, "daily": daily_data}
598
568
 
599
569
  except asyncio.TimeoutError as err:
@@ -667,6 +637,12 @@ class HourlyForecastCoordinator(DataUpdateCoordinator):
667
637
  update_interval=DEFAULT_HOURLY_FORECAST_UPDATE_INTERVAL,
668
638
  )
669
639
 
640
+ def _convert_to_local_time(self, forecast_time: datetime) -> datetime:
641
+ """Convierte una hora UTC a la hora local en la zona horaria de Madrid, considerando el horario de verano."""
642
+ # Convertir la hora UTC a la hora local usando la zona horaria de Madrid
643
+ local_time = forecast_time.astimezone(TIMEZONE)
644
+ return local_time
645
+
670
646
  async def _is_data_valid(self) -> bool:
671
647
  """Verifica si los datos horarios en el archivo JSON son válidos y actuales."""
672
648
  if not os.path.exists(self.file_path):
@@ -680,11 +656,13 @@ class HourlyForecastCoordinator(DataUpdateCoordinator):
680
656
  if not data or "dies" not in data:
681
657
  return False
682
658
 
683
- now = get_local_datetime()
659
+ now = datetime.now(TIMEZONE)
684
660
  for dia in data["dies"]:
685
661
  for forecast in dia.get("variables", {}).get("estatCel", {}).get("valors", []):
686
- forecast_time = convert_to_local_time(forecast["data"])
687
- if forecast_time >= now:
662
+ forecast_time = datetime.fromisoformat(forecast["data"].rstrip("Z")).replace(tzinfo=timezone.utc)
663
+ # Convertir la hora de la predicción a la hora local
664
+ forecast_time_local = self._convert_to_local_time(forecast_time)
665
+ if forecast_time_local >= now:
688
666
  return True
689
667
 
690
668
  return False
@@ -704,32 +682,36 @@ class HourlyForecastCoordinator(DataUpdateCoordinator):
704
682
 
705
683
  return {}
706
684
 
707
- def parse_hourly_forecast(self, dia: dict, forecast_time: datetime) -> dict:
685
+ def parse_hourly_forecast(self, dia: dict, forecast_time_local: datetime) -> dict:
708
686
  """Convierte una hora de predicción en un diccionario con los datos necesarios."""
709
687
  variables = dia.get("variables", {})
688
+
689
+ # Buscar el código de condición correspondiente al tiempo objetivo (en hora local)
710
690
  condition_code = next(
711
691
  (item["valor"] for item in variables.get("estatCel", {}).get("valors", []) if
712
- convert_to_local_time(item["data"]) == forecast_time),
692
+ self._convert_to_local_time(
693
+ datetime.fromisoformat(item["data"].rstrip("Z")).replace(tzinfo=timezone.utc)
694
+ ) == forecast_time_local),
713
695
  -1,
714
696
  )
715
-
697
+
716
698
  # Determinar la condición usando `get_condition_from_statcel`
717
699
  condition_data = get_condition_from_statcel(
718
700
  codi_estatcel=condition_code,
719
- current_time=forecast_time,
701
+ current_time=forecast_time_local,
720
702
  hass=self.hass,
721
703
  is_hourly=True
722
704
  )
723
705
  condition = condition_data["condition"]
724
706
 
725
707
  return {
726
- "datetime": forecast_time.isoformat(),
727
- "temperature": self._get_variable_value(dia, "temp", forecast_time),
728
- "precipitation": self._get_variable_value(dia, "precipitacio", forecast_time),
708
+ "datetime": forecast_time_local.isoformat(),
709
+ "temperature": self._get_variable_value(dia, "temp", forecast_time_local),
710
+ "precipitation": self._get_variable_value(dia, "precipitacio", forecast_time_local),
729
711
  "condition": condition,
730
- "wind_speed": self._get_variable_value(dia, "velVent", forecast_time),
731
- "wind_bearing": self._get_variable_value(dia, "dirVent", forecast_time),
732
- "humidity": self._get_variable_value(dia, "humitat", forecast_time),
712
+ "wind_speed": self._get_variable_value(dia, "velVent", forecast_time_local),
713
+ "wind_bearing": self._get_variable_value(dia, "dirVent", forecast_time_local),
714
+ "humidity": self._get_variable_value(dia, "humitat", forecast_time_local),
733
715
  }
734
716
 
735
717
  def get_all_hourly_forecasts(self) -> list[dict]:
@@ -738,12 +720,14 @@ class HourlyForecastCoordinator(DataUpdateCoordinator):
738
720
  return []
739
721
 
740
722
  forecasts = []
741
- now = get_local_datetime()
723
+ now = datetime.now(TIMEZONE)
742
724
  for dia in self.data["dies"]:
743
725
  for forecast in dia.get("variables", {}).get("estatCel", {}).get("valors", []):
744
- forecast_time = convert_to_local_time(forecast["data"])
745
- if forecast_time >= now:
746
- forecasts.append(self.parse_hourly_forecast(dia, forecast_time))
726
+ forecast_time = datetime.fromisoformat(forecast["data"].rstrip("Z")).replace(tzinfo=timezone.utc)
727
+ # Convertir la hora de la predicción a la hora local
728
+ forecast_time_local = self._convert_to_local_time(forecast_time)
729
+ if forecast_time_local >= now:
730
+ forecasts.append(self.parse_hourly_forecast(dia, forecast_time_local))
747
731
  return forecasts
748
732
 
749
733
  def _get_variable_value(self, dia, variable_name, target_time):
@@ -761,8 +745,12 @@ class HourlyForecastCoordinator(DataUpdateCoordinator):
761
745
 
762
746
  for valor in valores:
763
747
  try:
764
- data_hora = convert_to_local_time(valor["data"])
765
- if data_hora == target_time:
748
+ # Convertir tiempo del JSON a hora local
749
+ data_hora = datetime.fromisoformat(valor["data"].rstrip("Z")).replace(tzinfo=timezone.utc)
750
+ data_hora_local = self._convert_to_local_time(data_hora)
751
+
752
+ # Comparar con tiempo objetivo en hora local
753
+ if data_hora_local == target_time:
766
754
  return float(valor["valor"])
767
755
  except (KeyError, ValueError) as e:
768
756
  _LOGGER.warning("Error procesando '%s' para %s: %s", variable_name, valor, e)
@@ -798,6 +786,16 @@ class DailyForecastCoordinator(DataUpdateCoordinator):
798
786
  update_interval=DEFAULT_DAILY_FORECAST_UPDATE_INTERVAL,
799
787
  )
800
788
 
789
+ def _convert_to_local_date(self, forecast_time: datetime) -> datetime.date:
790
+ """Convierte una hora UTC a la fecha local en la zona horaria de Madrid, considerando el horario de verano."""
791
+ # Asegura que forecast_time es datetime y no date
792
+ if not isinstance(forecast_time, datetime):
793
+ forecast_time = datetime.combine(forecast_time, time(0, tzinfo=timezone.utc))
794
+
795
+ # Convertir la hora UTC a la hora local y extraer solo la fecha
796
+ local_datetime = forecast_time.astimezone(TIMEZONE)
797
+ return local_datetime.date()
798
+
801
799
  async def _is_data_valid(self) -> bool:
802
800
  """Verifica si hay datos válidos y actuales en el archivo JSON."""
803
801
  if not os.path.exists(self.file_path):
@@ -811,10 +809,11 @@ class DailyForecastCoordinator(DataUpdateCoordinator):
811
809
  if not data or "dies" not in data or not data["dies"]:
812
810
  return False
813
811
 
814
- today = get_local_datetime().date() # Usar la hora local
812
+ today = datetime.now(TIMEZONE).date()
815
813
  for dia in data["dies"]:
816
- forecast_date = convert_to_local_time(dia["data"], "Europe/Madrid").date()
817
- if forecast_date >= today:
814
+ forecast_date = datetime.fromisoformat(dia["data"].rstrip("Z")).date()
815
+ forecast_date_local = self._convert_to_local_date(forecast_date)
816
+ if forecast_date_local >= today:
818
817
  return True
819
818
 
820
819
  return False
@@ -831,11 +830,15 @@ class DailyForecastCoordinator(DataUpdateCoordinator):
831
830
  data = json.loads(content)
832
831
 
833
832
  # Filtrar días pasados
834
- today = get_local_datetime().date() # Usar la hora local
835
- data["dies"] = [
836
- dia for dia in data["dies"]
837
- if convert_to_local_time(dia["data"], "Europe/Madrid").date() >= today
838
- ]
833
+ today = datetime.now(TIMEZONE).date()
834
+ filtered_days = []
835
+ for dia in data["dies"]:
836
+ forecast_date = datetime.fromisoformat(dia["data"].rstrip("Z"))
837
+ forecast_date_local = self._convert_to_local_date(forecast_date)
838
+ if forecast_date_local >= today:
839
+ filtered_days.append(dia)
840
+
841
+ data["dies"] = filtered_days
839
842
  return data
840
843
  except Exception as e:
841
844
  _LOGGER.warning("Error leyendo archivo de predicción diaria: %s", e)
@@ -847,10 +850,11 @@ class DailyForecastCoordinator(DataUpdateCoordinator):
847
850
  if not self.data or "dies" not in self.data or not self.data["dies"]:
848
851
  return None
849
852
 
850
- today = get_local_datetime().date() # Usar la hora local
853
+ today = datetime.now(TIMEZONE).date()
851
854
  for dia in self.data["dies"]:
852
- forecast_date = convert_to_local_time(dia["data"], "Europe/Madrid").date()
853
- if forecast_date == today:
855
+ forecast_date = datetime.fromisoformat(dia["data"].rstrip("Z")).date()
856
+ forecast_date_local = self._convert_to_local_date(forecast_date)
857
+ if forecast_date_local == today:
854
858
  return dia
855
859
  return None
856
860
 
@@ -860,8 +864,12 @@ class DailyForecastCoordinator(DataUpdateCoordinator):
860
864
  condition_code = variables.get("estatCel", {}).get("valor", -1)
861
865
  condition = get_condition_from_code(int(condition_code))
862
866
 
867
+ # Usar la fecha original del pronóstico
868
+ forecast_date = datetime.fromisoformat(dia["data"].rstrip("Z"))
869
+ forecast_date_local = self._convert_to_local_date(forecast_date)
870
+
863
871
  forecast_data = {
864
- "date": convert_to_local_time(dia["data"], "Europe/Madrid").date(),
872
+ "date": forecast_date_local.isoformat(),
865
873
  "temperature_max": float(variables.get("tmax", {}).get("valor", 0.0)),
866
874
  "temperature_min": float(variables.get("tmin", {}).get("valor", 0.0)),
867
875
  "precipitation": float(variables.get("precipitacio", {}).get("valor", 0.0)),
@@ -937,11 +945,15 @@ class MeteocatConditionCoordinator(DataUpdateCoordinator):
937
945
  return self.DEFAULT_CONDITION
938
946
 
939
947
  return self._get_condition_for_current_hour(raw_data) or self.DEFAULT_CONDITION
948
+
949
+ def _convert_to_local_time(self, forecast_time: datetime) -> datetime:
950
+ """Convierte una hora UTC a la hora local en la zona horaria de Madrid, considerando el horario de verano."""
951
+ return forecast_time.astimezone(TIMEZONE)
940
952
 
941
953
  def _get_condition_for_current_hour(self, raw_data):
942
954
  """Get condition data for the current hour."""
943
955
  # Fecha y hora actual
944
- current_datetime = get_local_datetime() # Usar la zona horaria local
956
+ current_datetime = datetime.now(TIMEZONE)
945
957
  current_date = current_datetime.strftime("%Y-%m-%d")
946
958
  current_hour = current_datetime.hour
947
959
 
@@ -949,8 +961,9 @@ class MeteocatConditionCoordinator(DataUpdateCoordinator):
949
961
  for day in raw_data.get("dies", []):
950
962
  if day["data"].startswith(current_date):
951
963
  for value in day["variables"]["estatCel"]["valors"]:
952
- data_hour = convert_to_local_time(value["data"])
953
- if data_hour.hour == current_hour:
964
+ data_hour = datetime.fromisoformat(value["data"]).replace(tzinfo=ZoneInfo("UTC"))
965
+ local_hour = self._convert_to_local_time(data_hour)
966
+ if local_hour.hour == current_hour:
954
967
  codi_estatcel = value["valor"]
955
968
  condition = get_condition_from_statcel(
956
969
  codi_estatcel,
@@ -1007,6 +1020,11 @@ class MeteocatTempForecastCoordinator(DataUpdateCoordinator):
1007
1020
  update_interval=DEFAULT_TEMP_FORECAST_UPDATE_INTERVAL,
1008
1021
  )
1009
1022
 
1023
+ def _convert_to_local_time(self, forecast_time: datetime) -> datetime:
1024
+ """Convierte una hora UTC a la hora local en la zona horaria de Madrid, considerando el horario de verano."""
1025
+ local_time = forecast_time.astimezone(TIMEZONE)
1026
+ return local_time
1027
+
1010
1028
  async def _is_data_valid(self) -> bool:
1011
1029
  """Verifica si hay datos válidos y actuales en el archivo JSON."""
1012
1030
  if not os.path.exists(self.file_path):
@@ -1020,16 +1038,18 @@ class MeteocatTempForecastCoordinator(DataUpdateCoordinator):
1020
1038
  if not data or "dies" not in data or not data["dies"]:
1021
1039
  return False
1022
1040
 
1023
- today = get_local_datetime().date()
1024
- for dia in data["dies"]:
1025
- forecast_date = convert_to_local_time(dia["data"]).date() # Convertir a hora local
1026
- if forecast_date >= today:
1027
- return True
1028
-
1029
- return False
1041
+ today = datetime.now(TIMEZONE).date()
1042
+ if any(
1043
+ self._convert_to_local_time(
1044
+ datetime.fromisoformat(dia["data"].rstrip("Z")).replace(tzinfo=timezone.utc)
1045
+ ).date() >= today
1046
+ for dia in data["dies"]
1047
+ ):
1048
+ return True
1030
1049
  except Exception as e:
1031
1050
  _LOGGER.warning("Error validando datos diarios en %s: %s", self.file_path, e)
1032
- return False
1051
+
1052
+ return False
1033
1053
 
1034
1054
  async def _async_update_data(self) -> dict:
1035
1055
  """Lee y filtra los datos de predicción diaria desde el archivo local."""
@@ -1039,20 +1059,22 @@ class MeteocatTempForecastCoordinator(DataUpdateCoordinator):
1039
1059
  content = await f.read()
1040
1060
  data = json.loads(content)
1041
1061
 
1042
- # Filtrar días pasados
1043
- today = get_local_datetime().date() # Usar la hora local
1062
+ today = datetime.now(TIMEZONE).date()
1044
1063
  data["dies"] = [
1045
1064
  dia for dia in data["dies"]
1046
- if convert_to_local_time(dia["data"]).date() >= today # Convertir a hora local
1065
+ if self._convert_to_local_time(
1066
+ datetime.fromisoformat(dia["data"].rstrip("Z")).replace(tzinfo=timezone.utc)
1067
+ ).date() >= today
1047
1068
  ]
1048
1069
 
1049
- # Usar datos de temperatura del día actual si están disponibles
1050
1070
  today_temp_forecast = self.get_temp_forecast_for_today(data)
1051
1071
  if today_temp_forecast:
1052
1072
  parsed_data = self.parse_temp_forecast(today_temp_forecast)
1053
1073
  return parsed_data
1054
1074
  except Exception as e:
1055
- _LOGGER.warning("Error leyendo temperaturas del archivo de predicción diaria: %s", e)
1075
+ _LOGGER.warning(
1076
+ "Error leyendo temperaturas del archivo de predicción diaria '%s': %s", self.file_path, e
1077
+ )
1056
1078
 
1057
1079
  return {}
1058
1080
 
@@ -1061,19 +1083,21 @@ class MeteocatTempForecastCoordinator(DataUpdateCoordinator):
1061
1083
  if not data or "dies" not in data or not data["dies"]:
1062
1084
  return None
1063
1085
 
1064
- today = get_local_datetime().date() # Usar la hora local
1086
+ today = datetime.now(TIMEZONE).date()
1065
1087
  for dia in data["dies"]:
1066
- forecast_date = convert_to_local_time(dia["data"]).date() # Convertir a hora local
1067
- if forecast_date == today:
1088
+ forecast_date_utc = datetime.fromisoformat(dia["data"].rstrip("Z")).replace(tzinfo=timezone.utc)
1089
+ forecast_date_local = self._convert_to_local_time(forecast_date_utc)
1090
+ if forecast_date_local.date() == today:
1068
1091
  return dia
1069
1092
  return None
1070
1093
 
1071
1094
  def parse_temp_forecast(self, dia: dict) -> dict:
1072
1095
  """Convierte la temperatura de un día de predicción en un diccionario con los datos necesarios."""
1073
1096
  variables = dia.get("variables", {})
1097
+ forecast_date_utc = datetime.fromisoformat(dia["data"].rstrip("Z")).replace(tzinfo=timezone.utc)
1074
1098
 
1075
1099
  temp_forecast_data = {
1076
- "date": convert_to_local_time(dia["data"]), # Fecha convertida a hora local
1100
+ "date": self._convert_to_local_time(forecast_date_utc).date(),
1077
1101
  "max_temp_forecast": float(variables.get("tmax", {}).get("valor", 0.0)),
1078
1102
  "min_temp_forecast": float(variables.get("tmin", {}).get("valor", 0.0)),
1079
1103
  }
@@ -8,5 +8,5 @@
8
8
  "documentation": "https://gitlab.com/figorr/meteocat",
9
9
  "loggers": ["meteocatpy"],
10
10
  "requirements": ["meteocatpy==0.0.17", "packaging>=20.3", "wrapt>=1.14.0"],
11
- "version": "0.1.47"
11
+ "version": "0.1.49"
12
12
  }
@@ -40,6 +40,7 @@ from .const import (
40
40
  PRESSURE,
41
41
  PRECIPITATION,
42
42
  PRECIPITATION_ACCUMULATED,
43
+ PRECIPITATION_PROBABILITY,
43
44
  SOLAR_GLOBAL_IRRADIANCE,
44
45
  UV_INDEX,
45
46
  MAX_TEMPERATURE,
@@ -76,9 +77,13 @@ from .coordinator import (
76
77
  MeteocatConditionCoordinator,
77
78
  MeteocatTempForecastCoordinator,
78
79
  MeteocatEntityCoordinator,
80
+ DailyForecastCoordinator,
79
81
  MeteocatUviCoordinator,
80
82
  )
81
83
 
84
+ # Definir la zona horaria local
85
+ TIMEZONE = ZoneInfo("Europe/Madrid")
86
+
82
87
  _LOGGER = logging.getLogger(__name__)
83
88
 
84
89
  @dataclass
@@ -141,6 +146,13 @@ SENSOR_TYPES: tuple[MeteocatSensorEntityDescription, ...] = (
141
146
  state_class=SensorStateClass.MEASUREMENT,
142
147
  native_unit_of_measurement="mm",
143
148
  ),
149
+ MeteocatSensorEntityDescription(
150
+ key=PRECIPITATION_PROBABILITY,
151
+ translation_key="precipitation_probability",
152
+ icon="mdi:weather-rainy",
153
+ device_class=None,
154
+ native_unit_of_measurement=PERCENTAGE,
155
+ ),
144
156
  MeteocatSensorEntityDescription(
145
157
  key=SOLAR_GLOBAL_IRRADIANCE,
146
158
  translation_key="solar_global_irradiance",
@@ -268,6 +280,7 @@ async def async_setup_entry(hass, entry, async_add_entities: AddEntitiesCallback
268
280
  uvi_file_coordinator = entry_data.get("uvi_file_coordinator")
269
281
  static_sensor_coordinator = entry_data.get("static_sensor_coordinator")
270
282
  condition_coordinator = entry_data.get("condition_coordinator")
283
+ daily_forecast_coordinator = entry_data.get("daily_forecast_coordinator")
271
284
  temp_forecast_coordinator = entry_data.get("temp_forecast_coordinator")
272
285
  entity_coordinator = entry_data.get("entity_coordinator")
273
286
  uvi_coordinator = entry_data.get("uvi_coordinator")
@@ -307,6 +320,13 @@ async def async_setup_entry(hass, entry, async_add_entities: AddEntitiesCallback
307
320
  if description.key in {MAX_TEMPERATURE_FORECAST, MIN_TEMPERATURE_FORECAST}
308
321
  )
309
322
 
323
+ # Sensor precipitación probabilidad
324
+ async_add_entities(
325
+ MeteocatPrecipitationProbabilitySensor(daily_forecast_coordinator, description, entry_data)
326
+ for description in SENSOR_TYPES
327
+ if description.key == PRECIPITATION_PROBABILITY
328
+ )
329
+
310
330
  # Sensores de estado de los archivos de previsión horaria
311
331
  async_add_entities(
312
332
  MeteocatHourlyForecastStatusSensor(entity_coordinator, description, entry_data)
@@ -750,12 +770,12 @@ class MeteocatSensor(CoordinatorEntity[MeteocatSensorCoordinator], SensorEntity)
750
770
  )
751
771
 
752
772
  class MeteocatTempForecast(CoordinatorEntity[MeteocatTempForecastCoordinator], SensorEntity):
753
- """Representation of a Meteocat UV Index sensor."""
773
+ """Representation of a Meteocat Min and Max Temperature sensors."""
754
774
 
755
775
  _attr_has_entity_name = True # Activa el uso de nombres basados en el dispositivo
756
776
 
757
777
  def __init__(self, temp_forecast_coordinator, description, entry_data):
758
- """Initialize the UV Index sensor."""
778
+ """Initialize the Mina and Max Temperature sensors."""
759
779
  super().__init__(temp_forecast_coordinator)
760
780
  self.entity_description = description
761
781
  self._town_name = entry_data["town_name"]
@@ -796,6 +816,46 @@ class MeteocatTempForecast(CoordinatorEntity[MeteocatTempForecastCoordinator], S
796
816
  model="Meteocat API",
797
817
  )
798
818
 
819
+ class MeteocatPrecipitationProbabilitySensor(CoordinatorEntity[DailyForecastCoordinator], SensorEntity):
820
+ """Representation of a Meteocat precipitation probability sensor."""
821
+
822
+ _attr_has_entity_name = True # Enable device-based naming
823
+
824
+ def __init__(self, daily_forecast_coordinator, description, entry_data):
825
+ super().__init__(daily_forecast_coordinator)
826
+ self.entity_description = description
827
+ self._town_name = entry_data["town_name"]
828
+ self._town_id = entry_data["town_id"]
829
+ self._station_id = entry_data["station_id"]
830
+
831
+ self._attr_unique_id = f"sensor.{DOMAIN}_{self._town_id}_{self.entity_description.key}"
832
+ self._attr_entity_category = getattr(description, "entity_category", None)
833
+
834
+ _LOGGER.debug(
835
+ "Initializing sensor: %s, Unique ID: %s",
836
+ self.entity_description.name,
837
+ self._attr_unique_id,
838
+ )
839
+
840
+ @property
841
+ def native_value(self):
842
+ """Retorna la probabilidad de precipitación del día actual."""
843
+ forecast = self.coordinator.get_forecast_for_today()
844
+ if forecast:
845
+ precipitation = forecast.get("variables", {}).get("precipitacio", {}).get("valor", None)
846
+ if precipitation is not None and float(precipitation) >= 0:
847
+ return float(precipitation)
848
+ return None
849
+
850
+ @property
851
+ def device_info(self) -> DeviceInfo:
852
+ return DeviceInfo(
853
+ identifiers={(DOMAIN, self._town_id)},
854
+ name="Meteocat " + self._station_id + " " + self._town_name,
855
+ manufacturer="Meteocat",
856
+ model="Meteocat API",
857
+ )
858
+
799
859
  class MeteocatHourlyForecastStatusSensor(CoordinatorEntity[MeteocatEntityCoordinator], SensorEntity):
800
860
 
801
861
  _attr_has_entity_name = True # Activa el uso de nombres basados en el dispositivo
@@ -70,6 +70,9 @@
70
70
  "precipitation_accumulated": {
71
71
  "name": "Precipitation Accumulated"
72
72
  },
73
+ "precipitation_probability": {
74
+ "name": "Precipitation Probability"
75
+ },
73
76
  "solar_global_irradiance": {
74
77
  "name": "Solar Global Irradiance"
75
78
  },
@@ -70,6 +70,9 @@
70
70
  "precipitation_accumulated": {
71
71
  "name": "Precipitació Acumulada"
72
72
  },
73
+ "precipitation_probability": {
74
+ "name": "Precipitació Probabilitat"
75
+ },
73
76
  "solar_global_irradiance": {
74
77
  "name": "Irradiació Solar Global"
75
78
  },
@@ -70,6 +70,9 @@
70
70
  "precipitation_accumulated": {
71
71
  "name": "Precipitation Accumulated"
72
72
  },
73
+ "precipitation_probability": {
74
+ "name": "Precipitation Probability"
75
+ },
73
76
  "solar_global_irradiance": {
74
77
  "name": "Solar Global Irradiance"
75
78
  },
@@ -70,6 +70,9 @@
70
70
  "precipitation_accumulated": {
71
71
  "name": "Precipitación Acumulada"
72
72
  },
73
+ "precipitation_probability": {
74
+ "name": "Precipitación Probabilidad"
75
+ },
73
76
  "solar_global_irradiance": {
74
77
  "name": "Irradiación Solar Global"
75
78
  },
@@ -1,2 +1,2 @@
1
1
  # version.py
2
- __version__ = "0.1.47"
2
+ __version__ = "0.1.49"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "meteocat",
3
- "version": "0.1.47",
3
+ "version": "0.1.49",
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 = "0.1.47"
3
+ version = "0.1.49"
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"