meteocat 0.1.39 → 0.1.41

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,25 @@
1
+ ## [0.1.41](https://github.com/figorr/meteocat/compare/v0.1.40...v0.1.41) (2024-12-27)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * 0.1.41 ([ba2c800](https://github.com/figorr/meteocat/commit/ba2c80048cc2da6efe5f950a5d8fb958a53a7ef6))
7
+ * add new Max and Min Today Temperature sensors ([6cc726d](https://github.com/figorr/meteocat/commit/6cc726d127432ddc13a54638157f1f9566967ed4))
8
+ * add Today temp max and min translations ([8e1d31e](https://github.com/figorr/meteocat/commit/8e1d31e78c1d5df61c37635a19d95b6339f0b0a5))
9
+ * add Today temp max and min translations ([041c3ab](https://github.com/figorr/meteocat/commit/041c3ab877b269f3b3d3359792ecf13b14969466))
10
+ * add Today temp max and min translations ([be25fc4](https://github.com/figorr/meteocat/commit/be25fc43bef58e1cecc2afaff8e0b8d3288399fb))
11
+ * add Today temp max and min translations ([e236182](https://github.com/figorr/meteocat/commit/e236182dfe29b15c3eb06d6a8fd3e02712837858))
12
+ * fix condition when is night ([fb64e0b](https://github.com/figorr/meteocat/commit/fb64e0b754eb5a39afe9135a0be842d5e8bdeae0))
13
+ * fix sunset and sunrise events for night flag ([4770e56](https://github.com/figorr/meteocat/commit/4770e5633707933c0bdd2aae0f74ada363da86bb))
14
+
15
+ ## [0.1.40](https://github.com/figorr/meteocat/compare/v0.1.39...v0.1.40) (2024-12-26)
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * 0.1.40 ([011f139](https://github.com/figorr/meteocat/commit/011f1391874a78d16c1ad1eebbd8f84e6b053b5b))
21
+ * fix hourly forecasts ([bc1aa17](https://github.com/figorr/meteocat/commit/bc1aa178222a7158590b32af442427def4187e38))
22
+
1
23
  ## [0.1.39](https://github.com/figorr/meteocat/compare/v0.1.38...v0.1.39) (2024-12-25)
2
24
 
3
25
 
@@ -17,6 +17,7 @@ from .coordinator import (
17
17
  HourlyForecastCoordinator,
18
18
  DailyForecastCoordinator,
19
19
  MeteocatConditionCoordinator,
20
+ MeteocatTempForecastCoordinator,
20
21
  )
21
22
 
22
23
  from .const import DOMAIN, PLATFORMS
@@ -24,7 +25,7 @@ from .const import DOMAIN, PLATFORMS
24
25
  _LOGGER = logging.getLogger(__name__)
25
26
 
26
27
  # Versión
27
- __version__ = "0.1.39"
28
+ __version__ = "0.1.41"
28
29
 
29
30
  def safe_remove(path: Path, is_folder: bool = False):
30
31
  """Elimina de forma segura un archivo o carpeta si existe."""
@@ -91,6 +92,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
91
92
  condition_coordinator = MeteocatConditionCoordinator(hass=hass, entry_data=entry_data)
92
93
  await condition_coordinator.async_config_entry_first_refresh()
93
94
 
95
+ temp_forecast_coordinator = MeteocatTempForecastCoordinator(hass=hass, entry_data=entry_data)
96
+ await temp_forecast_coordinator.async_config_entry_first_refresh()
97
+
94
98
  except Exception as err: # Capturar todos los errores
95
99
  _LOGGER.exception(f"Error al inicializar los coordinadores: {err}")
96
100
  return False
@@ -106,6 +110,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
106
110
  "hourly_forecast_coordinator": hourly_forecast_coordinator,
107
111
  "daily_forecast_coordinator": daily_forecast_coordinator,
108
112
  "condition_coordinator": condition_coordinator,
113
+ "temp_forecast_coordinator": temp_forecast_coordinator,
109
114
  **entry_data,
110
115
  }
111
116
 
@@ -3,7 +3,9 @@ from __future__ import annotations
3
3
  from datetime import datetime
4
4
  from .const import CONDITION_MAPPING
5
5
  from .helpers import is_night
6
+ import logging
6
7
 
8
+ _LOGGER = logging.getLogger(__name__)
7
9
 
8
10
  def get_condition_from_statcel(
9
11
  codi_estatcel, current_time: datetime, hass, is_hourly: bool = True
@@ -17,6 +19,13 @@ def get_condition_from_statcel(
17
19
  :param is_hourly: Indica si los datos son de predicción horaria (True) o diaria (False).
18
20
  :return: Diccionario con la condición y el icono.
19
21
  """
22
+
23
+ _LOGGER.debug(
24
+ "Entrando en get_condition_from_statcel con codi_estatcel: %s, is_hourly: %s",
25
+ codi_estatcel,
26
+ is_hourly,
27
+ )
28
+
20
29
  # Asegurarse de que codi_estatcel sea una lista válida
21
30
  if codi_estatcel is None:
22
31
  codi_estatcel = []
@@ -31,7 +40,19 @@ def get_condition_from_statcel(
31
40
  if any(code in codes for code in codi_estatcel):
32
41
  # Ajustar para condiciones nocturnas si aplica
33
42
  if condition == "sunny" and is_night_flag:
43
+ _LOGGER.debug(
44
+ "Códigos EstatCel: %s, Es Noche: %s, Condición Devuelta: clear-night",
45
+ codi_estatcel,
46
+ is_night_flag,
47
+ )
34
48
  return {"condition": "clear-night", "icon": None}
49
+
50
+ _LOGGER.debug(
51
+ "Códigos EstatCel: %s, Es Noche: %s, Condición Devuelta: %s",
52
+ codi_estatcel,
53
+ is_night_flag,
54
+ condition,
55
+ )
35
56
  return {"condition": condition, "icon": None}
36
57
 
37
58
  # Si no coincide ningún código, devolver condición desconocida
@@ -32,6 +32,8 @@ FEELS_LIKE = "feels_like" # Sensación térmica
32
32
  WIND_GUST = "wind_gust" # Racha de viento
33
33
  STATION_TIMESTAMP = "station_timestamp" # Código de tiempo de la estación
34
34
  CONDITION = "condition" # Estado del cielo
35
+ MAX_TEMPERATURE_FORECAST = "max_temperature_forecast" # Temperatura máxima prevista
36
+ MIN_TEMPERATURE_FORECAST = "min_temperature_forecast" # Temperatura mínima prevista
35
37
 
36
38
  # Definición de códigos para variables
37
39
  WIND_SPEED_CODE = 30
@@ -36,12 +36,13 @@ _LOGGER = logging.getLogger(__name__)
36
36
  # Valores predeterminados para los intervalos de actualización
37
37
  DEFAULT_SENSOR_UPDATE_INTERVAL = timedelta(minutes=90)
38
38
  DEFAULT_STATIC_SENSOR_UPDATE_INTERVAL = timedelta(hours=24)
39
- DEFAULT_ENTITY_UPDATE_INTERVAL = timedelta(hours=48)
39
+ DEFAULT_ENTITY_UPDATE_INTERVAL = timedelta(minutes=60)
40
40
  DEFAULT_HOURLY_FORECAST_UPDATE_INTERVAL = timedelta(minutes=5)
41
41
  DEFAULT_DAILY_FORECAST_UPDATE_INTERVAL = timedelta(minutes=15)
42
- DEFAULT_UVI_UPDATE_INTERVAL = timedelta(hours=48)
42
+ DEFAULT_UVI_UPDATE_INTERVAL = timedelta(minutes=60)
43
43
  DEFAULT_UVI_SENSOR_UPDATE_INTERVAL = timedelta(minutes=5)
44
44
  DEFAULT_CONDITION_SENSOR_UPDATE_INTERVAL = timedelta(minutes=5)
45
+ DEFAULT_TEMP_FORECAST_UPDATE_INTERVAL = timedelta(minutes=5)
45
46
 
46
47
  async def save_json_to_file(data: dict, output_file: str) -> None:
47
48
  """Guarda datos JSON en un archivo de forma asíncrona."""
@@ -291,9 +292,9 @@ class MeteocatUviCoordinator(DataUpdateCoordinator):
291
292
  if not isinstance(uvi_data, list) or not uvi_data:
292
293
  return None
293
294
 
294
- # Validar la fecha del primer elemento
295
- first_date = uvi_data[0].get("date")
296
- if first_date != datetime.now(timezone.utc).strftime("%Y-%m-%d"):
295
+ # Validar la fecha del primer elemento superior a 1 día
296
+ first_date = datetime.strptime(uvi_data[0].get("date"), "%Y-%m-%d").date()
297
+ if (datetime.now(timezone.utc).date() - first_date).days > 1:
297
298
  return None
298
299
 
299
300
  return data
@@ -489,7 +490,10 @@ class MeteocatEntityCoordinator(DataUpdateCoordinator):
489
490
 
490
491
  # Obtener la fecha del primer día
491
492
  first_date = datetime.fromisoformat(data["dies"][0]["data"].rstrip("Z")).date()
492
- return first_date == datetime.now(timezone.utc).date()
493
+ today = datetime.now(timezone.utc).date()
494
+
495
+ # Verificar si la antigüedad es mayor a un día
496
+ return (today - first_date).days <= 1
493
497
  except Exception as e:
494
498
  _LOGGER.warning("Error validando datos en %s: %s", file_path, e)
495
499
  return False
@@ -602,7 +606,7 @@ class HourlyForecastCoordinator(DataUpdateCoordinator):
602
606
  )
603
607
 
604
608
  async def _is_data_valid(self) -> bool:
605
- """Verifica si los datos en el archivo JSON son válidos y actuales."""
609
+ """Verifica si los datos horarios en el archivo JSON son válidos y actuales."""
606
610
  if not os.path.exists(self.file_path):
607
611
  return False
608
612
 
@@ -611,18 +615,23 @@ class HourlyForecastCoordinator(DataUpdateCoordinator):
611
615
  content = await f.read()
612
616
  data = json.loads(content)
613
617
 
614
- if not data or "dies" not in data or not data["dies"]:
618
+ if not data or "dies" not in data:
615
619
  return False
616
620
 
617
- # Validar que los datos sean para la fecha actual
618
- first_date = datetime.fromisoformat(data["dies"][0]["data"].rstrip("Z")).date()
619
- return first_date == datetime.now(timezone.utc).date()
621
+ now = datetime.now(timezone.utc)
622
+ for dia in data["dies"]:
623
+ for forecast in dia.get("variables", {}).get("estatCel", {}).get("valors", []):
624
+ forecast_time = datetime.fromisoformat(forecast["data"].rstrip("Z")).replace(tzinfo=timezone.utc)
625
+ if forecast_time >= now:
626
+ return True
627
+
628
+ return False
620
629
  except Exception as e:
621
630
  _LOGGER.warning("Error validando datos horarios en %s: %s", self.file_path, e)
622
631
  return False
623
632
 
624
633
  async def _async_update_data(self) -> dict:
625
- """Lee los datos de predicción horaria desde el archivo local."""
634
+ """Lee los datos horarios desde el archivo local."""
626
635
  if await self._is_data_valid():
627
636
  try:
628
637
  async with aiofiles.open(self.file_path, "r", encoding="utf-8") as f:
@@ -633,41 +642,71 @@ class HourlyForecastCoordinator(DataUpdateCoordinator):
633
642
 
634
643
  return {}
635
644
 
636
- async def async_forecast_hourly(self) -> list[Forecast] | None:
637
- """Devuelve una lista de objetos Forecast para las próximas horas."""
645
+ def parse_hourly_forecast(self, dia: dict, forecast_time: datetime) -> dict:
646
+ """Convierte una hora de predicción en un diccionario con los datos necesarios."""
647
+ variables = dia.get("variables", {})
648
+ condition_code = next(
649
+ (item["valor"] for item in variables.get("estatCel", {}).get("valors", []) if
650
+ datetime.fromisoformat(item["data"].rstrip("Z")).replace(tzinfo=timezone.utc) == forecast_time),
651
+ -1,
652
+ )
653
+
654
+ # Determinar la condición usando `get_condition_from_statcel`
655
+ condition_data = get_condition_from_statcel(
656
+ codi_estatcel=condition_code,
657
+ current_time=forecast_time,
658
+ hass=self.hass,
659
+ is_hourly=True
660
+ )
661
+ condition = condition_data["condition"]
662
+
663
+ return {
664
+ "datetime": forecast_time.isoformat(),
665
+ "temperature": self._get_variable_value(dia, "temp", forecast_time),
666
+ "precipitation": self._get_variable_value(dia, "precipitacio", forecast_time),
667
+ "condition": condition,
668
+ "wind_speed": self._get_variable_value(dia, "velVent", forecast_time),
669
+ "wind_bearing": self._get_variable_value(dia, "dirVent", forecast_time),
670
+ "humidity": self._get_variable_value(dia, "humitat", forecast_time),
671
+ }
672
+
673
+ def get_all_hourly_forecasts(self) -> list[dict]:
674
+ """Obtiene una lista de predicciones horarias procesadas."""
638
675
  if not self.data or "dies" not in self.data:
639
- return None
676
+ return []
640
677
 
641
678
  forecasts = []
642
679
  now = datetime.now(timezone.utc)
643
-
644
680
  for dia in self.data["dies"]:
645
681
  for forecast in dia.get("variables", {}).get("estatCel", {}).get("valors", []):
646
682
  forecast_time = datetime.fromisoformat(forecast["data"].rstrip("Z")).replace(tzinfo=timezone.utc)
647
683
  if forecast_time >= now:
648
- # Usar la función para obtener la condición meteorológica
649
- condition = get_condition_from_code(int(forecast["valor"]))
650
-
651
- forecast_data = {
652
- "datetime": forecast_time,
653
- "temperature": self._get_variable_value(dia, "temp", forecast_time),
654
- "precipitation": self._get_variable_value(dia, "precipitacio", forecast_time),
655
- "condition": condition, # Se usa la condición traducida
656
- "wind_speed": self._get_variable_value(dia, "velVent", forecast_time),
657
- "wind_bearing": self._get_variable_value(dia, "dirVent", forecast_time),
658
- "humidity": self._get_variable_value(dia, "humitat", forecast_time),
659
- }
660
- forecasts.append(Forecast(**forecast_data))
661
-
662
- return forecasts if forecasts else None
684
+ forecasts.append(self.parse_hourly_forecast(dia, forecast_time))
685
+ return forecasts
663
686
 
664
687
  def _get_variable_value(self, dia, variable_name, target_time):
665
688
  """Devuelve el valor de una variable específica para una hora determinada."""
666
689
  variable = dia.get("variables", {}).get(variable_name, {})
667
- for valor in variable.get("valors", []):
668
- data_hora = datetime.fromisoformat(valor["data"].rstrip("Z"))
669
- if data_hora == target_time:
670
- return valor["valor"]
690
+ if not variable:
691
+ _LOGGER.warning("Variable '%s' no encontrada en los datos.", variable_name)
692
+ return None
693
+
694
+ # Obtener lista de valores, soportando tanto 'valors' como 'valor'
695
+ valores = variable.get("valors") or variable.get("valor")
696
+ if not valores:
697
+ _LOGGER.warning("No se encontraron valores para la variable '%s'.", variable_name)
698
+ return None
699
+
700
+ for valor in valores:
701
+ try:
702
+ data_hora = datetime.fromisoformat(valor["data"].rstrip("Z")).replace(tzinfo=timezone.utc)
703
+ if data_hora == target_time:
704
+ return float(valor["valor"])
705
+ except (KeyError, ValueError) as e:
706
+ _LOGGER.warning("Error procesando '%s' para %s: %s", variable_name, valor, e)
707
+ continue
708
+
709
+ _LOGGER.info("No se encontró un valor válido para '%s' en %s.", variable_name, target_time)
671
710
  return None
672
711
 
673
712
  class DailyForecastCoordinator(DataUpdateCoordinator):
@@ -815,6 +854,8 @@ class MeteocatConditionCoordinator(DataUpdateCoordinator):
815
854
 
816
855
  async def _async_update_data(self):
817
856
  """Read and process condition data for the current hour from the file asynchronously."""
857
+ _LOGGER.debug("Iniciando actualización de datos desde el archivo: %s", self._file_path)
858
+
818
859
  try:
819
860
  async with aiofiles.open(self._file_path, "r", encoding="utf-8") as file:
820
861
  raw_data = await file.read()
@@ -862,6 +903,12 @@ class MeteocatConditionCoordinator(DataUpdateCoordinator):
862
903
  "hour": current_hour,
863
904
  "date": current_date,
864
905
  })
906
+ _LOGGER.debug(
907
+ "Hora actual: %s, Código estatCel: %s, Condición procesada: %s",
908
+ current_datetime,
909
+ codi_estatcel,
910
+ condition,
911
+ )
865
912
  return condition
866
913
  break # Sale del bucle una vez encontrada la fecha actual
867
914
 
@@ -873,3 +920,102 @@ class MeteocatConditionCoordinator(DataUpdateCoordinator):
873
920
  )
874
921
  return {"condition": "unknown", "hour": current_hour, "icon": None, "date": current_date}
875
922
 
923
+ class MeteocatTempForecastCoordinator(DataUpdateCoordinator):
924
+ """Coordinator para manejar las predicciones diarias desde archivos locales."""
925
+
926
+ def __init__(
927
+ self,
928
+ hass: HomeAssistant,
929
+ entry_data: dict,
930
+ update_interval: timedelta = DEFAULT_TEMP_FORECAST_UPDATE_INTERVAL,
931
+ ):
932
+ """Inicializa el coordinador para predicciones diarias."""
933
+ self.town_name = entry_data["town_name"]
934
+ self.town_id = entry_data["town_id"]
935
+ self.station_name = entry_data["station_name"]
936
+ self.station_id = entry_data["station_id"]
937
+ self.file_path = os.path.join(
938
+ hass.config.path(),
939
+ "custom_components",
940
+ "meteocat",
941
+ "files",
942
+ f"forecast_{self.town_id.lower()}_daily_data.json",
943
+ )
944
+ super().__init__(
945
+ hass,
946
+ _LOGGER,
947
+ name=f"{DOMAIN} Daily Forecast Coordinator",
948
+ update_interval=update_interval,
949
+ )
950
+
951
+ async def _is_data_valid(self) -> bool:
952
+ """Verifica si hay datos válidos y actuales en el archivo JSON."""
953
+ if not os.path.exists(self.file_path):
954
+ return False
955
+
956
+ try:
957
+ async with aiofiles.open(self.file_path, "r", encoding="utf-8") as f:
958
+ content = await f.read()
959
+ data = json.loads(content)
960
+
961
+ if not data or "dies" not in data or not data["dies"]:
962
+ return False
963
+
964
+ today = datetime.now(timezone.utc).date()
965
+ for dia in data["dies"]:
966
+ forecast_date = datetime.fromisoformat(dia["data"].rstrip("Z")).date()
967
+ if forecast_date >= today:
968
+ return True
969
+
970
+ return False
971
+ except Exception as e:
972
+ _LOGGER.warning("Error validando datos diarios en %s: %s", self.file_path, e)
973
+ return False
974
+
975
+ async def _async_update_data(self) -> dict:
976
+ """Lee y filtra los datos de predicción diaria desde el archivo local."""
977
+ if await self._is_data_valid():
978
+ try:
979
+ async with aiofiles.open(self.file_path, "r", encoding="utf-8") as f:
980
+ content = await f.read()
981
+ data = json.loads(content)
982
+
983
+ # Filtrar días pasados
984
+ today = datetime.now(timezone.utc).date()
985
+ data["dies"] = [
986
+ dia for dia in data["dies"]
987
+ if datetime.fromisoformat(dia["data"].rstrip("Z")).date() >= today
988
+ ]
989
+
990
+ # Usar datos del día actual si están disponibles
991
+ today_temp_forecast = self.get_temp_forecast_for_today(data)
992
+ if today_temp_forecast:
993
+ parsed_data = self.parse_forecast(today_temp_forecast)
994
+ return parsed_data
995
+ except Exception as e:
996
+ _LOGGER.warning("Error leyendo archivo de predicción diaria: %s", e)
997
+
998
+ return {}
999
+
1000
+ def get_temp_forecast_for_today(self, data: dict) -> dict | None:
1001
+ """Obtiene los datos diarios para el día actual."""
1002
+ if not data or "dies" not in data or not data["dies"]:
1003
+ return None
1004
+
1005
+ today = datetime.now(timezone.utc).date()
1006
+ for dia in data["dies"]:
1007
+ forecast_date = datetime.fromisoformat(dia["data"].rstrip("Z")).date()
1008
+ if forecast_date == today:
1009
+ return dia
1010
+ return None
1011
+
1012
+ def parse_forecast(self, dia: dict) -> dict:
1013
+ """Convierte un día de predicción en un diccionario con los datos necesarios."""
1014
+ variables = dia.get("variables", {})
1015
+
1016
+ temp_forecast_data = {
1017
+ "date": datetime.fromisoformat(dia["data"].rstrip("Z")).date(),
1018
+ "max_temp_forecast": float(variables.get("tmax", {}).get("valor", 0.0)),
1019
+ "min_temp_forecast": float(variables.get("tmin", {}).get("valor", 0.0)),
1020
+ }
1021
+ return temp_forecast_data
@@ -1,41 +1,51 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import logging
4
- from datetime import datetime, timedelta
5
- from homeassistant.core import HomeAssistant
6
- from homeassistant.util.dt import as_local, as_utc
7
- from homeassistant.helpers.sun import get_astral_event_next
4
+ from datetime import datetime
5
+ from homeassistant.util.dt import as_local, as_utc, start_of_local_day
6
+ from homeassistant.helpers.sun import get_astral_event_date
8
7
 
9
8
  _LOGGER = logging.getLogger(__name__)
10
9
 
11
- def get_sun_times(hass: HomeAssistant, current_time: datetime | None = None):
12
- """Get the sunrise and sunset times."""
13
- # Usa la hora actual si no se proporciona una hora específica
10
+ def get_sun_times(hass, current_time=None):
11
+ """Obtén las horas de amanecer y atardecer para el día actual."""
14
12
  if current_time is None:
15
13
  current_time = datetime.now()
16
14
 
17
- # Asegúrate de que current_time es aware (UTC con offset)
15
+ # Asegúrate de que current_time es aware (UTC)
18
16
  current_time = as_utc(current_time)
17
+ today = start_of_local_day(as_local(current_time))
19
18
 
20
- # Obtén los tiempos de amanecer y atardecer desde el helper
21
- sunrise = get_astral_event_next(hass, "sunrise", utc_point_in_time=current_time)
22
- sunset = get_astral_event_next(hass, "sunset", utc_point_in_time=current_time)
19
+ # Obtén los eventos de amanecer y atardecer del día actual
20
+ sunrise = get_astral_event_date(hass, "sunrise", today)
21
+ sunset = get_astral_event_date(hass, "sunset", today)
22
+
23
+ _LOGGER.debug(
24
+ "Sunrise: %s, Sunset: %s, Current Time: %s",
25
+ sunrise,
26
+ sunset,
27
+ as_local(current_time),
28
+ )
23
29
 
24
- # Asegúrate de que no sean None y conviértelos a la zona horaria local
25
30
  if sunrise and sunset:
26
- return as_local(sunrise), as_local(sunset)
31
+ return sunrise, sunset
27
32
 
28
- # Lanza un error si no se pudieron determinar los eventos
29
- raise ValueError("Sunrise or sunset data is unavailable.")
33
+ raise ValueError("No se pudieron determinar los datos de amanecer y atardecer.")
30
34
 
31
- def is_night(current_time: datetime, hass: HomeAssistant) -> bool:
32
- """Determine if it is currently night based on sunrise and sunset times."""
33
- # Convierte current_time a UTC si no tiene información de zona horaria
35
+ def is_night(current_time, hass):
36
+ """Determina si actualmente es de noche."""
37
+ # Asegúrate de que current_time es aware (UTC)
34
38
  if current_time.tzinfo is None:
35
39
  current_time = as_utc(current_time)
36
40
 
37
- # Obtén los tiempos de amanecer y atardecer
38
41
  sunrise, sunset = get_sun_times(hass, current_time)
39
42
 
40
- # Compara las horas
43
+ _LOGGER.debug(
44
+ "Hora actual: %s, Amanecer: %s, Atardecer: %s",
45
+ as_local(current_time),
46
+ as_local(sunrise),
47
+ as_local(sunset),
48
+ )
49
+
50
+ # Es de noche si es antes del amanecer o después del atardecer
41
51
  return current_time < sunrise or current_time > sunset
@@ -8,5 +8,5 @@
8
8
  "documentation": "https://gitlab.com/figorr/meteocat",
9
9
  "loggers": ["meteocatpy"],
10
10
  "requirements": ["meteocatpy==0.0.15", "packaging>=20.3", "wrapt>=1.14.0"],
11
- "version": "0.1.39"
11
+ "version": "0.1.41"
12
12
  }
@@ -46,6 +46,8 @@ from .const import (
46
46
  WIND_GUST,
47
47
  STATION_TIMESTAMP,
48
48
  CONDITION,
49
+ MAX_TEMPERATURE_FORECAST,
50
+ MIN_TEMPERATURE_FORECAST,
49
51
  WIND_SPEED_CODE,
50
52
  WIND_DIRECTION_CODE,
51
53
  TEMPERATURE_CODE,
@@ -65,6 +67,7 @@ from .coordinator import (
65
67
  MeteocatStaticSensorCoordinator,
66
68
  MeteocatUviFileCoordinator,
67
69
  MeteocatConditionCoordinator,
70
+ MeteocatTempForecastCoordinator,
68
71
  )
69
72
 
70
73
  _LOGGER = logging.getLogger(__name__)
@@ -209,7 +212,23 @@ SENSOR_TYPES: tuple[MeteocatSensorEntityDescription, ...] = (
209
212
  key=CONDITION,
210
213
  translation_key="condition",
211
214
  icon="mdi:weather-partly-cloudy",
212
- )
215
+ ),
216
+ MeteocatSensorEntityDescription(
217
+ key=MAX_TEMPERATURE_FORECAST,
218
+ translation_key="max_temperature_forecast",
219
+ icon="mdi:thermometer-plus",
220
+ device_class=SensorDeviceClass.TEMPERATURE,
221
+ state_class=SensorStateClass.MEASUREMENT,
222
+ native_unit_of_measurement=UnitOfTemperature.CELSIUS,
223
+ ),
224
+ MeteocatSensorEntityDescription(
225
+ key=MIN_TEMPERATURE_FORECAST,
226
+ translation_key="min_temperature_forecast",
227
+ icon="mdi:thermometer-minus",
228
+ device_class=SensorDeviceClass.TEMPERATURE,
229
+ state_class=SensorStateClass.MEASUREMENT,
230
+ native_unit_of_measurement=UnitOfTemperature.CELSIUS,
231
+ ),
213
232
  )
214
233
 
215
234
  @callback
@@ -222,12 +241,13 @@ async def async_setup_entry(hass, entry, async_add_entities: AddEntitiesCallback
222
241
  uvi_file_coordinator = entry_data.get("uvi_file_coordinator")
223
242
  static_sensor_coordinator = entry_data.get("static_sensor_coordinator")
224
243
  condition_coordinator = entry_data.get("condition_coordinator")
244
+ temp_forecast_coordinator = entry_data.get("temp_forecast_coordinator")
225
245
 
226
246
  # Sensores generales
227
247
  async_add_entities(
228
248
  MeteocatSensor(coordinator, description, entry_data)
229
249
  for description in SENSOR_TYPES
230
- if description.key not in {TOWN_NAME, TOWN_ID, STATION_NAME, STATION_ID, UV_INDEX, CONDITION} # Excluir estáticos y UVI
250
+ if description.key not in {TOWN_NAME, TOWN_ID, STATION_NAME, STATION_ID, UV_INDEX, CONDITION, MAX_TEMPERATURE_FORECAST, MIN_TEMPERATURE_FORECAST} # Excluir estáticos y UVI
231
251
  )
232
252
 
233
253
  # Sensores estáticos
@@ -251,6 +271,13 @@ async def async_setup_entry(hass, entry, async_add_entities: AddEntitiesCallback
251
271
  if description.key == CONDITION # Incluir CONDITION en el coordinador CONDITION COORDINATOR
252
272
  )
253
273
 
274
+ # Sensores temperatura previsión
275
+ async_add_entities(
276
+ MeteocatTempForecast(temp_forecast_coordinator, description, entry_data)
277
+ for description in SENSOR_TYPES
278
+ if description.key in {MAX_TEMPERATURE_FORECAST, MIN_TEMPERATURE_FORECAST}
279
+ )
280
+
254
281
  class MeteocatStaticSensor(CoordinatorEntity[MeteocatStaticSensorCoordinator], SensorEntity):
255
282
  """Representation of a static Meteocat sensor."""
256
283
  STATIC_KEYS = {TOWN_NAME, TOWN_ID, STATION_NAME, STATION_ID}
@@ -543,7 +570,7 @@ class MeteocatSensor(CoordinatorEntity[MeteocatSensorCoordinator], SensorEntity)
543
570
  return value
544
571
 
545
572
  # Lógica específica para el sensor de timestamp
546
- if self.entity_description.key == "station_timestamp":
573
+ if self.entity_description.key == STATION_TIMESTAMP:
547
574
  stations = self.coordinator.data or []
548
575
  for station in stations:
549
576
  variables = station.get("variables", [])
@@ -563,7 +590,7 @@ class MeteocatSensor(CoordinatorEntity[MeteocatSensorCoordinator], SensorEntity)
563
590
  return None
564
591
 
565
592
  # Nuevo sensor para la precipitación acumulada
566
- if self.entity_description.key == "precipitation_accumulated":
593
+ if self.entity_description.key == PRECIPITATION_ACCUMULATED:
567
594
  stations = self.coordinator.data or []
568
595
  total_precipitation = 0.0 # Usa float para permitir acumulación de decimales
569
596
 
@@ -644,3 +671,50 @@ class MeteocatSensor(CoordinatorEntity[MeteocatSensorCoordinator], SensorEntity)
644
671
  manufacturer="Meteocat",
645
672
  model="Meteocat API",
646
673
  )
674
+
675
+ class MeteocatTempForecast(CoordinatorEntity[MeteocatTempForecastCoordinator], SensorEntity):
676
+ """Representation of a Meteocat UV Index sensor."""
677
+
678
+ _attr_has_entity_name = True # Activa el uso de nombres basados en el dispositivo
679
+
680
+ def __init__(self, temp_forecast_coordinator, description, entry_data):
681
+ """Initialize the UV Index sensor."""
682
+ super().__init__(temp_forecast_coordinator)
683
+ self.entity_description = description
684
+ self._town_name = entry_data["town_name"]
685
+ self._town_id = entry_data["town_id"]
686
+ self._station_id = entry_data["station_id"]
687
+
688
+ # Unique ID for the entity
689
+ self._attr_unique_id = f"sensor.{DOMAIN}_{self._town_id}_{self.entity_description.key}"
690
+
691
+ # Asigna entity_category desde description (si está definido)
692
+ self._attr_entity_category = getattr(description, "entity_category", None)
693
+
694
+ # Log para depuración
695
+ _LOGGER.debug(
696
+ "Inicializando sensor: %s, Unique ID: %s",
697
+ self.entity_description.name,
698
+ self._attr_unique_id,
699
+ )
700
+
701
+ @property
702
+ def native_value(self):
703
+ """Return the Max and Min Temp Forecast value."""
704
+ temp_forecast_data = self.coordinator.data or {}
705
+
706
+ if self.entity_description.key == MAX_TEMPERATURE_FORECAST:
707
+ return temp_forecast_data.get("max_temp_forecast", None)
708
+ if self.entity_description.key == MIN_TEMPERATURE_FORECAST:
709
+ return temp_forecast_data.get("min_temp_forecast", None)
710
+ return None
711
+
712
+ @property
713
+ def device_info(self) -> DeviceInfo:
714
+ """Return the device info."""
715
+ return DeviceInfo(
716
+ identifiers={(DOMAIN, self._town_id)},
717
+ name="Meteocat " + self._station_id + " " + self._town_name,
718
+ manufacturer="Meteocat",
719
+ model="Meteocat API",
720
+ )
@@ -127,6 +127,12 @@
127
127
  "name": "Hour"
128
128
  }
129
129
  }
130
+ },
131
+ "max_temperature_forecast": {
132
+ "name": "Max Temperature Today"
133
+ },
134
+ "min_temperature_forecast": {
135
+ "name": "Min Temperature Today"
130
136
  }
131
137
  }
132
138
  }
@@ -127,7 +127,14 @@
127
127
  "name": "Hora"
128
128
  }
129
129
  }
130
+ },
131
+ "max_temperature_forecast": {
132
+ "name": "Temperatura Max Avui"
133
+ },
134
+ "min_temperature_forecast": {
135
+ "name": "Temperatura Min Avui"
130
136
  }
131
137
  }
132
138
  }
133
- }
139
+ }
140
+
@@ -127,8 +127,14 @@
127
127
  "name": "Hour"
128
128
  }
129
129
  }
130
+ },
131
+ "max_temperature_forecast": {
132
+ "name": "Max Temperature Today"
133
+ },
134
+ "min_temperature_forecast": {
135
+ "name": "Min Temperature Today"
130
136
  }
131
137
  }
132
138
  }
133
139
  }
134
-
140
+
@@ -127,6 +127,12 @@
127
127
  "name": "Hora"
128
128
  }
129
129
  }
130
+ },
131
+ "max_temperature_forecast": {
132
+ "name": "Temperatura Max Hoy"
133
+ },
134
+ "min_temperature_forecast": {
135
+ "name": "Temperatura Min Hoy"
130
136
  }
131
137
  }
132
138
  }
@@ -1,2 +1,2 @@
1
1
  # version.py
2
- __version__ = "0.1.39"
2
+ __version__ = "0.1.41"
@@ -11,13 +11,11 @@ from homeassistant.components.weather import (
11
11
  )
12
12
  from homeassistant.core import callback
13
13
  from homeassistant.helpers.entity_platform import AddEntitiesCallback
14
- from homeassistant.components.weather import Forecast
15
14
  from homeassistant.helpers.device_registry import DeviceInfo
16
15
  from homeassistant.const import (
17
16
  DEGREE,
18
17
  PERCENTAGE,
19
18
  UnitOfPrecipitationDepth,
20
- UnitOfVolumetricFlux,
21
19
  UnitOfPressure,
22
20
  UnitOfSpeed,
23
21
  UnitOfTemperature,
@@ -26,10 +24,20 @@ from homeassistant.const import (
26
24
  from .const import (
27
25
  DOMAIN,
28
26
  ATTRIBUTION,
27
+ WIND_SPEED_CODE,
28
+ WIND_DIRECTION_CODE,
29
+ TEMPERATURE_CODE,
30
+ HUMIDITY_CODE,
31
+ PRESSURE_CODE,
32
+ PRECIPITATION_CODE,
33
+ WIND_GUST_CODE,
29
34
  )
30
35
  from .coordinator import (
31
36
  HourlyForecastCoordinator,
32
37
  DailyForecastCoordinator,
38
+ MeteocatSensorCoordinator,
39
+ MeteocatUviFileCoordinator,
40
+ MeteocatConditionCoordinator,
33
41
  )
34
42
 
35
43
  _LOGGER = logging.getLogger(__name__)
@@ -41,16 +49,28 @@ async def async_setup_entry(hass, entry, async_add_entities: AddEntitiesCallback
41
49
 
42
50
  hourly_forecast_coordinator = entry_data.get("hourly_forecast_coordinator")
43
51
  daily_forecast_coordinator = entry_data.get("daily_forecast_coordinator")
52
+ sensor_coordinator = entry_data.get("sensor_coordinator")
53
+ uvi_file_coordinator = entry_data.get("uvi_file_coordinator")
54
+ condition_coordinator = entry_data.get("condition_coordinator")
44
55
 
45
56
  async_add_entities([
46
- MeteocatWeatherEntity(hourly_forecast_coordinator, daily_forecast_coordinator, entry_data)
57
+ MeteocatWeatherEntity(
58
+ hourly_forecast_coordinator,
59
+ daily_forecast_coordinator,
60
+ sensor_coordinator,
61
+ uvi_file_coordinator,
62
+ condition_coordinator,
63
+ entry_data
64
+ )
47
65
  ])
48
66
 
49
67
  class MeteocatWeatherEntity(CoordinatorEntity, WeatherEntity):
50
68
  """Representation of a Meteocat Weather Entity."""
69
+
51
70
  _attr_attribution = ATTRIBUTION
52
71
  _attr_has_entity_name = True
53
72
  _attr_native_precipitation_unit = UnitOfPrecipitationDepth.MILLIMETERS
73
+ _attr_native_precipitation_probability_unit = PERCENTAGE
54
74
  _attr_native_pressure_unit = UnitOfPressure.HPA
55
75
  _attr_native_temperature_unit = UnitOfTemperature.CELSIUS
56
76
  _attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR
@@ -63,12 +83,18 @@ class MeteocatWeatherEntity(CoordinatorEntity, WeatherEntity):
63
83
  self,
64
84
  hourly_forecast_coordinator: HourlyForecastCoordinator,
65
85
  daily_forecast_coordinator: DailyForecastCoordinator,
86
+ sensor_coordinator: MeteocatSensorCoordinator,
87
+ uvi_file_coordinator: MeteocatUviFileCoordinator,
88
+ condition_coordinator: MeteocatConditionCoordinator,
66
89
  entry_data: dict,
67
90
  ) -> None:
68
91
  """Initialize the weather entity."""
69
- super().__init__(hourly_forecast_coordinator)
92
+ super().__init__(daily_forecast_coordinator)
70
93
  self._hourly_forecast_coordinator = hourly_forecast_coordinator
71
94
  self._daily_forecast_coordinator = daily_forecast_coordinator
95
+ self._sensor_coordinator = sensor_coordinator
96
+ self._uvi_file_coordinator = uvi_file_coordinator
97
+ self._condition_coordinator = condition_coordinator
72
98
  self._town_name = entry_data["town_name"]
73
99
  self._town_id = entry_data["town_id"]
74
100
  self._station_id = entry_data["station_id"]
@@ -86,73 +112,90 @@ class MeteocatWeatherEntity(CoordinatorEntity, WeatherEntity):
86
112
  @property
87
113
  def condition(self) -> Optional[str]:
88
114
  """Return the current weather condition."""
89
- forecast_today = self._daily_forecast_coordinator.get_forecast_for_today()
90
- if forecast_today:
91
- return forecast_today.get("condition")
115
+ condition_data = self._condition_coordinator.data or {}
116
+ return condition_data.get("condition")
117
+
118
+ def _get_latest_sensor_value(self, code: str) -> Optional[float]:
119
+ """Helper method to retrieve the latest sensor value."""
120
+ sensor_code = code
121
+ if not sensor_code:
122
+ return None
123
+
124
+ stations = self._sensor_coordinator.data or []
125
+ for station in stations:
126
+ variables = station.get("variables", [])
127
+ variable_data = next(
128
+ (var for var in variables if var.get("codi") == sensor_code),
129
+ None,
130
+ )
131
+ if variable_data:
132
+ lectures = variable_data.get("lectures", [])
133
+ if lectures:
134
+ return lectures[-1].get("valor")
92
135
  return None
93
136
 
94
137
  @property
95
- def temperature(self) -> Optional[float]:
96
- """Return the current temperature."""
97
- forecast_today = self._daily_forecast_coordinator.get_forecast_for_today()
98
- if forecast_today:
99
- return (forecast_today.get("temperature_max") + forecast_today.get("temperature_min")) / 2
100
- return None
138
+ def native_temperature(self) -> Optional[float]:
139
+ return self._get_latest_sensor_value(TEMPERATURE_CODE)
140
+
141
+ @property
142
+ def humidity(self) -> Optional[float]:
143
+ return self._get_latest_sensor_value(HUMIDITY_CODE)
144
+
145
+ @property
146
+ def native_pressure(self) -> Optional[float]:
147
+ return self._get_latest_sensor_value(PRESSURE_CODE)
101
148
 
102
149
  @property
103
- def forecast(self) -> list[Forecast]:
150
+ def native_wind_speed(self) -> Optional[float]:
151
+ return self._get_latest_sensor_value(WIND_SPEED_CODE)
152
+
153
+ @property
154
+ def native_wind_gust_speed(self) -> Optional[float]:
155
+ return self._get_latest_sensor_value(WIND_GUST_CODE)
156
+
157
+ @property
158
+ def uv_index(self) -> Optional[float]:
159
+ """Return the UV index."""
160
+ uvi_data = self._uvi_file_coordinator.data or {}
161
+ return uvi_data.get("uvi")
162
+
163
+ async def async_forecast_daily(self) -> list[Forecast] | None:
104
164
  """Return the daily forecast."""
165
+ await self._daily_forecast_coordinator.async_request_refresh()
105
166
  daily_forecasts = self._daily_forecast_coordinator.get_all_daily_forecasts()
167
+ if not daily_forecasts:
168
+ return None
169
+
106
170
  return [
107
171
  Forecast(
108
172
  datetime=forecast["date"],
109
173
  temperature=forecast["temperature_max"],
110
174
  templow=forecast["temperature_min"],
111
- precipitation=forecast["precipitation"],
175
+ precipitation_probability=forecast["precipitation"],
112
176
  condition=forecast["condition"],
113
177
  )
114
178
  for forecast in daily_forecasts
115
179
  ]
116
-
180
+
117
181
  async def async_forecast_hourly(self) -> list[Forecast] | None:
118
182
  """Return the hourly forecast."""
119
- hourly_forecasts = await self._hourly_forecast_coordinator.async_forecast_hourly()
183
+ await self._hourly_forecast_coordinator.async_request_refresh()
184
+ hourly_forecasts = self._hourly_forecast_coordinator.get_all_hourly_forecasts()
120
185
  if not hourly_forecasts:
121
186
  return None
122
187
 
123
188
  return [
124
189
  Forecast(
125
- datetime=forecast.datetime,
126
- temperature=forecast.temperature,
127
- precipitation=forecast.precipitation,
128
- condition=forecast.condition,
129
- wind_speed=forecast.wind_speed,
130
- wind_bearing=forecast.wind_bearing,
131
- humidity=forecast.humidity,
132
- )
133
- for forecast in hourly_forecasts
134
- ]
135
-
136
- async def async_forecast_daily(self) -> list[Forecast] | None:
137
- """Return the daily forecast."""
138
- # Asegúrate de que los datos estén actualizados antes de procesarlos
139
- await self._daily_forecast_coordinator.async_request_refresh()
140
-
141
- # Obtén las predicciones diarias procesadas desde el coordinador
142
- daily_forecasts = self._daily_forecast_coordinator.get_all_daily_forecasts()
143
- if not daily_forecasts:
144
- return None
145
-
146
- # Convierte las predicciones a la estructura Forecast de Home Assistant
147
- return [
148
- Forecast(
149
- datetime=forecast["date"],
150
- temperature=forecast["temperature_max"],
151
- templow=forecast["temperature_min"],
190
+ datetime=forecast["datetime"],
191
+ temperature=forecast["temperature"],
152
192
  precipitation=forecast["precipitation"],
153
193
  condition=forecast["condition"],
194
+ wind_speed=forecast["wind_speed"],
195
+ wind_bearing=forecast["wind_bearing"],
196
+ humidity=forecast["humidity"],
154
197
  )
155
- for forecast in daily_forecasts
198
+ for forecast in hourly_forecasts
156
199
  ]
157
200
 
158
201
  async def async_update(self) -> None:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "meteocat",
3
- "version": "0.1.39",
3
+ "version": "0.1.41",
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.39"
3
+ version = "0.1.41"
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"