meteocat 0.1.46 → 0.1.47

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.
@@ -24,8 +24,14 @@ jobs:
24
24
  # Paso 3: Instalar dependencias necesarias
25
25
  - name: Install dependencies
26
26
  run: npm ci
27
+
28
+ # Paso 4: Configurar el autor de Git
29
+ - name: Configure Git author
30
+ run: |
31
+ git config user.name "semantic-release-bot"
32
+ git config user.email "jdcuartero@yahoo.es"
27
33
 
28
- # Paso 4: Ejecutar semantic-release
34
+ # Paso 5: Ejecutar semantic-release
29
35
  - name: Run semantic-release
30
36
  env:
31
37
  GITHUB_TOKEN: ${{ secrets.WORKFLOW_TOKEN }}
package/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ ## [0.1.47](https://github.com/figorr/meteocat/compare/v0.1.46...v0.1.47) (2025-01-02)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * 0.1.47 ([03d1d08](https://github.com/figorr/meteocat/commit/03d1d08dd837dc47d8f2062ba561be706e1d8b05))
7
+ * convert to local time from UTC ([0dfe9f9](https://github.com/figorr/meteocat/commit/0dfe9f9ef7409bcd23d839963076aff14921f114))
8
+ * fix semantic-release-bot ([754f18b](https://github.com/figorr/meteocat/commit/754f18b2c0378c704eb255259192655b76100c43))
9
+ * fix timestamp sensor from UTC to local time Europe/Madrid ([e64539a](https://github.com/figorr/meteocat/commit/e64539a091adecfd4e23419c63bc54eda2293da3))
10
+
1
11
  ## [0.1.46](https://github.com/figorr/meteocat/compare/v0.1.45...v0.1.46) (2025-01-01)
2
12
 
3
13
 
@@ -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.46"
28
+ __version__ = "0.1.47"
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."""
@@ -6,6 +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
10
  from typing import Dict, Any
10
11
 
11
12
  from homeassistant.core import HomeAssistant
@@ -80,6 +81,30 @@ async def load_json_from_file(input_file: str) -> dict:
80
81
  _LOGGER.error("Error al decodificar JSON del archivo %s: %s", input_file, err)
81
82
  return {}
82
83
 
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
+
83
108
  class MeteocatSensorCoordinator(DataUpdateCoordinator):
84
109
  """Coordinator para manejar la actualización de datos de los sensores."""
85
110
 
@@ -286,9 +311,9 @@ class MeteocatUviCoordinator(DataUpdateCoordinator):
286
311
  data = json.loads(content)
287
312
 
288
313
  # Validar la fecha del primer elemento superior a 1 día
289
- first_date = datetime.strptime(data["uvi"][0].get("date"), "%Y-%m-%d").date()
290
- today = datetime.now(timezone.utc).date()
291
- current_time = datetime.now(timezone.utc).time()
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()
292
317
 
293
318
  # Log detallado
294
319
  _LOGGER.info(
@@ -428,8 +453,8 @@ class MeteocatUviFileCoordinator(DataUpdateCoordinator):
428
453
 
429
454
  def _get_uv_for_current_hour(self, raw_data):
430
455
  """Get UV data for the current hour."""
431
- # Fecha y hora actual
432
- current_datetime = datetime.now()
456
+ # # Obtiene la fecha y hora locales
457
+ current_datetime = get_local_datetime()
433
458
  current_date = current_datetime.strftime("%Y-%m-%d")
434
459
  current_hour = current_datetime.hour
435
460
 
@@ -510,10 +535,10 @@ class MeteocatEntityCoordinator(DataUpdateCoordinator):
510
535
  content = await f.read()
511
536
  data = json.loads(content)
512
537
 
513
- # Obtener la fecha del primer día
514
- first_date = datetime.fromisoformat(data["dies"][0]["data"].rstrip("Z")).date()
515
- today = datetime.now(timezone.utc).date()
516
- current_time = datetime.now(timezone.utc).time()
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
542
 
518
543
  # Log detallado
519
544
  _LOGGER.info(
@@ -552,6 +577,11 @@ class MeteocatEntityCoordinator(DataUpdateCoordinator):
552
577
  hourly_data = await self._fetch_and_save_data(
553
578
  self.meteocat_forecast.get_prediccion_horaria, self.hourly_file
554
579
  )
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()
555
585
 
556
586
  # Validar o actualizar datos diarios
557
587
  daily_data = await self.validate_forecast_data(self.daily_file)
@@ -560,6 +590,10 @@ class MeteocatEntityCoordinator(DataUpdateCoordinator):
560
590
  self.meteocat_forecast.get_prediccion_diaria, self.daily_file
561
591
  )
562
592
 
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
+
563
597
  return {"hourly": hourly_data, "daily": daily_data}
564
598
 
565
599
  except asyncio.TimeoutError as err:
@@ -646,10 +680,10 @@ class HourlyForecastCoordinator(DataUpdateCoordinator):
646
680
  if not data or "dies" not in data:
647
681
  return False
648
682
 
649
- now = datetime.now(timezone.utc)
683
+ now = get_local_datetime()
650
684
  for dia in data["dies"]:
651
685
  for forecast in dia.get("variables", {}).get("estatCel", {}).get("valors", []):
652
- forecast_time = datetime.fromisoformat(forecast["data"].rstrip("Z")).replace(tzinfo=timezone.utc)
686
+ forecast_time = convert_to_local_time(forecast["data"])
653
687
  if forecast_time >= now:
654
688
  return True
655
689
 
@@ -675,7 +709,7 @@ class HourlyForecastCoordinator(DataUpdateCoordinator):
675
709
  variables = dia.get("variables", {})
676
710
  condition_code = next(
677
711
  (item["valor"] for item in variables.get("estatCel", {}).get("valors", []) if
678
- datetime.fromisoformat(item["data"].rstrip("Z")).replace(tzinfo=timezone.utc) == forecast_time),
712
+ convert_to_local_time(item["data"]) == forecast_time),
679
713
  -1,
680
714
  )
681
715
 
@@ -704,10 +738,10 @@ class HourlyForecastCoordinator(DataUpdateCoordinator):
704
738
  return []
705
739
 
706
740
  forecasts = []
707
- now = datetime.now(timezone.utc)
741
+ now = get_local_datetime()
708
742
  for dia in self.data["dies"]:
709
743
  for forecast in dia.get("variables", {}).get("estatCel", {}).get("valors", []):
710
- forecast_time = datetime.fromisoformat(forecast["data"].rstrip("Z")).replace(tzinfo=timezone.utc)
744
+ forecast_time = convert_to_local_time(forecast["data"])
711
745
  if forecast_time >= now:
712
746
  forecasts.append(self.parse_hourly_forecast(dia, forecast_time))
713
747
  return forecasts
@@ -727,7 +761,7 @@ class HourlyForecastCoordinator(DataUpdateCoordinator):
727
761
 
728
762
  for valor in valores:
729
763
  try:
730
- data_hora = datetime.fromisoformat(valor["data"].rstrip("Z")).replace(tzinfo=timezone.utc)
764
+ data_hora = convert_to_local_time(valor["data"])
731
765
  if data_hora == target_time:
732
766
  return float(valor["valor"])
733
767
  except (KeyError, ValueError) as e:
@@ -777,9 +811,9 @@ class DailyForecastCoordinator(DataUpdateCoordinator):
777
811
  if not data or "dies" not in data or not data["dies"]:
778
812
  return False
779
813
 
780
- today = datetime.now(timezone.utc).date()
814
+ today = get_local_datetime().date() # Usar la hora local
781
815
  for dia in data["dies"]:
782
- forecast_date = datetime.fromisoformat(dia["data"].rstrip("Z")).date()
816
+ forecast_date = convert_to_local_time(dia["data"], "Europe/Madrid").date()
783
817
  if forecast_date >= today:
784
818
  return True
785
819
 
@@ -797,10 +831,10 @@ class DailyForecastCoordinator(DataUpdateCoordinator):
797
831
  data = json.loads(content)
798
832
 
799
833
  # Filtrar días pasados
800
- today = datetime.now(timezone.utc).date()
834
+ today = get_local_datetime().date() # Usar la hora local
801
835
  data["dies"] = [
802
836
  dia for dia in data["dies"]
803
- if datetime.fromisoformat(dia["data"].rstrip("Z")).date() >= today
837
+ if convert_to_local_time(dia["data"], "Europe/Madrid").date() >= today
804
838
  ]
805
839
  return data
806
840
  except Exception as e:
@@ -813,9 +847,9 @@ class DailyForecastCoordinator(DataUpdateCoordinator):
813
847
  if not self.data or "dies" not in self.data or not self.data["dies"]:
814
848
  return None
815
849
 
816
- today = datetime.now(timezone.utc).date()
850
+ today = get_local_datetime().date() # Usar la hora local
817
851
  for dia in self.data["dies"]:
818
- forecast_date = datetime.fromisoformat(dia["data"].rstrip("Z")).date()
852
+ forecast_date = convert_to_local_time(dia["data"], "Europe/Madrid").date()
819
853
  if forecast_date == today:
820
854
  return dia
821
855
  return None
@@ -827,7 +861,7 @@ class DailyForecastCoordinator(DataUpdateCoordinator):
827
861
  condition = get_condition_from_code(int(condition_code))
828
862
 
829
863
  forecast_data = {
830
- "date": datetime.fromisoformat(dia["data"].rstrip("Z")).date(),
864
+ "date": convert_to_local_time(dia["data"], "Europe/Madrid").date(),
831
865
  "temperature_max": float(variables.get("tmax", {}).get("valor", 0.0)),
832
866
  "temperature_min": float(variables.get("tmin", {}).get("valor", 0.0)),
833
867
  "precipitation": float(variables.get("precipitacio", {}).get("valor", 0.0)),
@@ -907,7 +941,7 @@ class MeteocatConditionCoordinator(DataUpdateCoordinator):
907
941
  def _get_condition_for_current_hour(self, raw_data):
908
942
  """Get condition data for the current hour."""
909
943
  # Fecha y hora actual
910
- current_datetime = datetime.now()
944
+ current_datetime = get_local_datetime() # Usar la zona horaria local
911
945
  current_date = current_datetime.strftime("%Y-%m-%d")
912
946
  current_hour = current_datetime.hour
913
947
 
@@ -915,7 +949,7 @@ class MeteocatConditionCoordinator(DataUpdateCoordinator):
915
949
  for day in raw_data.get("dies", []):
916
950
  if day["data"].startswith(current_date):
917
951
  for value in day["variables"]["estatCel"]["valors"]:
918
- data_hour = datetime.fromisoformat(value["data"])
952
+ data_hour = convert_to_local_time(value["data"])
919
953
  if data_hour.hour == current_hour:
920
954
  codi_estatcel = value["valor"]
921
955
  condition = get_condition_from_statcel(
@@ -986,9 +1020,9 @@ class MeteocatTempForecastCoordinator(DataUpdateCoordinator):
986
1020
  if not data or "dies" not in data or not data["dies"]:
987
1021
  return False
988
1022
 
989
- today = datetime.now(timezone.utc).date()
1023
+ today = get_local_datetime().date()
990
1024
  for dia in data["dies"]:
991
- forecast_date = datetime.fromisoformat(dia["data"].rstrip("Z")).date()
1025
+ forecast_date = convert_to_local_time(dia["data"]).date() # Convertir a hora local
992
1026
  if forecast_date >= today:
993
1027
  return True
994
1028
 
@@ -1006,10 +1040,10 @@ class MeteocatTempForecastCoordinator(DataUpdateCoordinator):
1006
1040
  data = json.loads(content)
1007
1041
 
1008
1042
  # Filtrar días pasados
1009
- today = datetime.now(timezone.utc).date()
1043
+ today = get_local_datetime().date() # Usar la hora local
1010
1044
  data["dies"] = [
1011
1045
  dia for dia in data["dies"]
1012
- if datetime.fromisoformat(dia["data"].rstrip("Z")).date() >= today
1046
+ if convert_to_local_time(dia["data"]).date() >= today # Convertir a hora local
1013
1047
  ]
1014
1048
 
1015
1049
  # Usar datos de temperatura del día actual si están disponibles
@@ -1027,9 +1061,9 @@ class MeteocatTempForecastCoordinator(DataUpdateCoordinator):
1027
1061
  if not data or "dies" not in data or not data["dies"]:
1028
1062
  return None
1029
1063
 
1030
- today = datetime.now(timezone.utc).date()
1064
+ today = get_local_datetime().date() # Usar la hora local
1031
1065
  for dia in data["dies"]:
1032
- forecast_date = datetime.fromisoformat(dia["data"].rstrip("Z")).date()
1066
+ forecast_date = convert_to_local_time(dia["data"]).date() # Convertir a hora local
1033
1067
  if forecast_date == today:
1034
1068
  return dia
1035
1069
  return None
@@ -1039,7 +1073,7 @@ class MeteocatTempForecastCoordinator(DataUpdateCoordinator):
1039
1073
  variables = dia.get("variables", {})
1040
1074
 
1041
1075
  temp_forecast_data = {
1042
- "date": datetime.fromisoformat(dia["data"].rstrip("Z")).date(),
1076
+ "date": convert_to_local_time(dia["data"]), # Fecha convertida a hora local
1043
1077
  "max_temp_forecast": float(variables.get("tmax", {}).get("valor", 0.0)),
1044
1078
  "min_temp_forecast": float(variables.get("tmin", {}).get("valor", 0.0)),
1045
1079
  }
@@ -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.46"
11
+ "version": "0.1.47"
12
12
  }
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass
4
4
  from datetime import datetime, timezone, time
5
+ from zoneinfo import ZoneInfo
5
6
  import logging
6
7
  from homeassistant.helpers.entity import (
7
8
  DeviceInfo,
@@ -327,6 +328,29 @@ async def async_setup_entry(hass, entry, async_add_entities: AddEntitiesCallback
327
328
  if description.key == UVI_FILE_STATUS
328
329
  )
329
330
 
331
+ # Cambiar UTC a la zona horaria local
332
+ def convert_to_local_time(utc_time: str, local_tz: str = "Europe/Madrid") -> datetime | None:
333
+ """
334
+ Convierte una fecha/hora UTC en formato ISO 8601 a la zona horaria local especificada.
335
+
336
+ Args:
337
+ utc_time (str): Fecha/hora en formato ISO 8601 (ejemplo: '2025-01-02T12:00:00Z').
338
+ local_tz (str): Zona horaria local en formato IANA (por defecto, 'Europe/Madrid').
339
+
340
+ Returns:
341
+ datetime | None: Objeto datetime convertido a la zona horaria local, o None si hay un error.
342
+ """
343
+ try:
344
+ # Convertir la cadena UTC a un objeto datetime
345
+ utc_dt = datetime.fromisoformat(utc_time.replace("Z", "+00:00"))
346
+
347
+ # Convertir a la zona horaria local usando ZoneInfo
348
+ local_dt = utc_dt.replace(tzinfo=ZoneInfo("UTC")).astimezone(ZoneInfo(local_tz))
349
+
350
+ return local_dt
351
+ except ValueError:
352
+ return None
353
+
330
354
  class MeteocatStaticSensor(CoordinatorEntity[MeteocatStaticSensorCoordinator], SensorEntity):
331
355
  """Representation of a static Meteocat sensor."""
332
356
  STATIC_KEYS = {TOWN_NAME, TOWN_ID, STATION_NAME, STATION_ID}
@@ -633,9 +657,13 @@ class MeteocatSensor(CoordinatorEntity[MeteocatSensorCoordinator], SensorEntity)
633
657
  if raw_timestamp:
634
658
  # Convertir el timestamp a un objeto datetime
635
659
  try:
636
- return datetime.fromisoformat(raw_timestamp.replace("Z", "+00:00"))
660
+ # Convertimos raw_timestamp a hora local
661
+ local_time = convert_to_local_time(raw_timestamp)
662
+ _LOGGER.debug("Hora UTC: %s convertida a hora local: %s", raw_timestamp, local_time)
663
+ return local_time
637
664
  except ValueError:
638
665
  # Manejo de errores si el formato no es válido
666
+ _LOGGER.error(f"Error al convertir el timestamp '{raw_timestamp}' a hora local.")
639
667
  return None
640
668
 
641
669
  # Nuevo sensor para la precipitación acumulada
@@ -1,2 +1,2 @@
1
1
  # version.py
2
- __version__ = "0.1.46"
2
+ __version__ = "0.1.47"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "meteocat",
3
- "version": "0.1.46",
3
+ "version": "0.1.47",
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.46"
3
+ version = "0.1.47"
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"