meteocat 0.1.46 → 0.1.48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/release.yml +7 -1
- package/CHANGELOG.md +18 -0
- package/custom_components/meteocat/__init__.py +1 -1
- package/custom_components/meteocat/coordinator.py +104 -46
- package/custom_components/meteocat/manifest.json +1 -1
- package/custom_components/meteocat/sensor.py +29 -1
- package/custom_components/meteocat/version.py +1 -1
- package/package.json +1 -1
- package/pyproject.toml +1 -1
|
@@ -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
|
|
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,21 @@
|
|
|
1
|
+
## [0.1.48](https://github.com/figorr/meteocat/compare/v0.1.47...v0.1.48) (2025-01-03)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* 0.1.48 ([b8b8830](https://github.com/figorr/meteocat/commit/b8b8830a23b0d63744f30c3dff33fc9f172a3c01))
|
|
7
|
+
* fix UTC to local time conversion ([14cf7d2](https://github.com/figorr/meteocat/commit/14cf7d2ca696ede2bedcd6041195df44fdd6d245))
|
|
8
|
+
|
|
9
|
+
## [0.1.47](https://github.com/figorr/meteocat/compare/v0.1.46...v0.1.47) (2025-01-02)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
### Bug Fixes
|
|
13
|
+
|
|
14
|
+
* 0.1.47 ([03d1d08](https://github.com/figorr/meteocat/commit/03d1d08dd837dc47d8f2062ba561be706e1d8b05))
|
|
15
|
+
* convert to local time from UTC ([0dfe9f9](https://github.com/figorr/meteocat/commit/0dfe9f9ef7409bcd23d839963076aff14921f114))
|
|
16
|
+
* fix semantic-release-bot ([754f18b](https://github.com/figorr/meteocat/commit/754f18b2c0378c704eb255259192655b76100c43))
|
|
17
|
+
* fix timestamp sensor from UTC to local time Europe/Madrid ([e64539a](https://github.com/figorr/meteocat/commit/e64539a091adecfd4e23419c63bc54eda2293da3))
|
|
18
|
+
|
|
1
19
|
## [0.1.46](https://github.com/figorr/meteocat/compare/v0.1.45...v0.1.46) (2025-01-01)
|
|
2
20
|
|
|
3
21
|
|
|
@@ -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.
|
|
28
|
+
__version__ = "0.1.48"
|
|
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
|
|
@@ -47,6 +48,9 @@ DEFAULT_UVI_SENSOR_UPDATE_INTERVAL = timedelta(minutes=5)
|
|
|
47
48
|
DEFAULT_CONDITION_SENSOR_UPDATE_INTERVAL = timedelta(minutes=5)
|
|
48
49
|
DEFAULT_TEMP_FORECAST_UPDATE_INTERVAL = timedelta(minutes=5)
|
|
49
50
|
|
|
51
|
+
# Definir la zona horaria local
|
|
52
|
+
TIMEZONE = ZoneInfo("Europe/Madrid")
|
|
53
|
+
|
|
50
54
|
async def save_json_to_file(data: dict, output_file: str) -> None:
|
|
51
55
|
"""Guarda datos JSON en un archivo de forma asíncrona."""
|
|
52
56
|
try:
|
|
@@ -633,6 +637,12 @@ class HourlyForecastCoordinator(DataUpdateCoordinator):
|
|
|
633
637
|
update_interval=DEFAULT_HOURLY_FORECAST_UPDATE_INTERVAL,
|
|
634
638
|
)
|
|
635
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
|
+
|
|
636
646
|
async def _is_data_valid(self) -> bool:
|
|
637
647
|
"""Verifica si los datos horarios en el archivo JSON son válidos y actuales."""
|
|
638
648
|
if not os.path.exists(self.file_path):
|
|
@@ -646,11 +656,13 @@ class HourlyForecastCoordinator(DataUpdateCoordinator):
|
|
|
646
656
|
if not data or "dies" not in data:
|
|
647
657
|
return False
|
|
648
658
|
|
|
649
|
-
now = datetime.now(
|
|
659
|
+
now = datetime.now(TIMEZONE)
|
|
650
660
|
for dia in data["dies"]:
|
|
651
661
|
for forecast in dia.get("variables", {}).get("estatCel", {}).get("valors", []):
|
|
652
662
|
forecast_time = datetime.fromisoformat(forecast["data"].rstrip("Z")).replace(tzinfo=timezone.utc)
|
|
653
|
-
|
|
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:
|
|
654
666
|
return True
|
|
655
667
|
|
|
656
668
|
return False
|
|
@@ -670,32 +682,36 @@ class HourlyForecastCoordinator(DataUpdateCoordinator):
|
|
|
670
682
|
|
|
671
683
|
return {}
|
|
672
684
|
|
|
673
|
-
def parse_hourly_forecast(self, dia: dict,
|
|
685
|
+
def parse_hourly_forecast(self, dia: dict, forecast_time_local: datetime) -> dict:
|
|
674
686
|
"""Convierte una hora de predicción en un diccionario con los datos necesarios."""
|
|
675
687
|
variables = dia.get("variables", {})
|
|
688
|
+
|
|
689
|
+
# Buscar el código de condición correspondiente al tiempo objetivo (en hora local)
|
|
676
690
|
condition_code = next(
|
|
677
691
|
(item["valor"] for item in variables.get("estatCel", {}).get("valors", []) if
|
|
678
|
-
|
|
692
|
+
self._convert_to_local_time(
|
|
693
|
+
datetime.fromisoformat(item["data"].rstrip("Z")).replace(tzinfo=timezone.utc)
|
|
694
|
+
) == forecast_time_local),
|
|
679
695
|
-1,
|
|
680
696
|
)
|
|
681
|
-
|
|
697
|
+
|
|
682
698
|
# Determinar la condición usando `get_condition_from_statcel`
|
|
683
699
|
condition_data = get_condition_from_statcel(
|
|
684
700
|
codi_estatcel=condition_code,
|
|
685
|
-
current_time=
|
|
701
|
+
current_time=forecast_time_local,
|
|
686
702
|
hass=self.hass,
|
|
687
703
|
is_hourly=True
|
|
688
704
|
)
|
|
689
705
|
condition = condition_data["condition"]
|
|
690
706
|
|
|
691
707
|
return {
|
|
692
|
-
"datetime":
|
|
693
|
-
"temperature": self._get_variable_value(dia, "temp",
|
|
694
|
-
"precipitation": self._get_variable_value(dia, "precipitacio",
|
|
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),
|
|
695
711
|
"condition": condition,
|
|
696
|
-
"wind_speed": self._get_variable_value(dia, "velVent",
|
|
697
|
-
"wind_bearing": self._get_variable_value(dia, "dirVent",
|
|
698
|
-
"humidity": self._get_variable_value(dia, "humitat",
|
|
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),
|
|
699
715
|
}
|
|
700
716
|
|
|
701
717
|
def get_all_hourly_forecasts(self) -> list[dict]:
|
|
@@ -704,12 +720,14 @@ class HourlyForecastCoordinator(DataUpdateCoordinator):
|
|
|
704
720
|
return []
|
|
705
721
|
|
|
706
722
|
forecasts = []
|
|
707
|
-
now = datetime.now(
|
|
723
|
+
now = datetime.now(TIMEZONE)
|
|
708
724
|
for dia in self.data["dies"]:
|
|
709
725
|
for forecast in dia.get("variables", {}).get("estatCel", {}).get("valors", []):
|
|
710
726
|
forecast_time = datetime.fromisoformat(forecast["data"].rstrip("Z")).replace(tzinfo=timezone.utc)
|
|
711
|
-
|
|
712
|
-
|
|
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))
|
|
713
731
|
return forecasts
|
|
714
732
|
|
|
715
733
|
def _get_variable_value(self, dia, variable_name, target_time):
|
|
@@ -727,8 +745,12 @@ class HourlyForecastCoordinator(DataUpdateCoordinator):
|
|
|
727
745
|
|
|
728
746
|
for valor in valores:
|
|
729
747
|
try:
|
|
748
|
+
# Convertir tiempo del JSON a hora local
|
|
730
749
|
data_hora = datetime.fromisoformat(valor["data"].rstrip("Z")).replace(tzinfo=timezone.utc)
|
|
731
|
-
|
|
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:
|
|
732
754
|
return float(valor["valor"])
|
|
733
755
|
except (KeyError, ValueError) as e:
|
|
734
756
|
_LOGGER.warning("Error procesando '%s' para %s: %s", variable_name, valor, e)
|
|
@@ -764,6 +786,16 @@ class DailyForecastCoordinator(DataUpdateCoordinator):
|
|
|
764
786
|
update_interval=DEFAULT_DAILY_FORECAST_UPDATE_INTERVAL,
|
|
765
787
|
)
|
|
766
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
|
+
|
|
767
799
|
async def _is_data_valid(self) -> bool:
|
|
768
800
|
"""Verifica si hay datos válidos y actuales en el archivo JSON."""
|
|
769
801
|
if not os.path.exists(self.file_path):
|
|
@@ -777,10 +809,11 @@ class DailyForecastCoordinator(DataUpdateCoordinator):
|
|
|
777
809
|
if not data or "dies" not in data or not data["dies"]:
|
|
778
810
|
return False
|
|
779
811
|
|
|
780
|
-
today = datetime.now(
|
|
812
|
+
today = datetime.now(TIMEZONE).date()
|
|
781
813
|
for dia in data["dies"]:
|
|
782
814
|
forecast_date = datetime.fromisoformat(dia["data"].rstrip("Z")).date()
|
|
783
|
-
|
|
815
|
+
forecast_date_local = self._convert_to_local_date(forecast_date)
|
|
816
|
+
if forecast_date_local >= today:
|
|
784
817
|
return True
|
|
785
818
|
|
|
786
819
|
return False
|
|
@@ -797,11 +830,15 @@ class DailyForecastCoordinator(DataUpdateCoordinator):
|
|
|
797
830
|
data = json.loads(content)
|
|
798
831
|
|
|
799
832
|
# Filtrar días pasados
|
|
800
|
-
today = datetime.now(
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
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
|
|
805
842
|
return data
|
|
806
843
|
except Exception as e:
|
|
807
844
|
_LOGGER.warning("Error leyendo archivo de predicción diaria: %s", e)
|
|
@@ -813,10 +850,11 @@ class DailyForecastCoordinator(DataUpdateCoordinator):
|
|
|
813
850
|
if not self.data or "dies" not in self.data or not self.data["dies"]:
|
|
814
851
|
return None
|
|
815
852
|
|
|
816
|
-
today = datetime.now(
|
|
853
|
+
today = datetime.now(TIMEZONE).date()
|
|
817
854
|
for dia in self.data["dies"]:
|
|
818
855
|
forecast_date = datetime.fromisoformat(dia["data"].rstrip("Z")).date()
|
|
819
|
-
|
|
856
|
+
forecast_date_local = self._convert_to_local_date(forecast_date)
|
|
857
|
+
if forecast_date_local == today:
|
|
820
858
|
return dia
|
|
821
859
|
return None
|
|
822
860
|
|
|
@@ -826,8 +864,12 @@ class DailyForecastCoordinator(DataUpdateCoordinator):
|
|
|
826
864
|
condition_code = variables.get("estatCel", {}).get("valor", -1)
|
|
827
865
|
condition = get_condition_from_code(int(condition_code))
|
|
828
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
|
+
|
|
829
871
|
forecast_data = {
|
|
830
|
-
"date":
|
|
872
|
+
"date": forecast_date_local.isoformat(),
|
|
831
873
|
"temperature_max": float(variables.get("tmax", {}).get("valor", 0.0)),
|
|
832
874
|
"temperature_min": float(variables.get("tmin", {}).get("valor", 0.0)),
|
|
833
875
|
"precipitation": float(variables.get("precipitacio", {}).get("valor", 0.0)),
|
|
@@ -903,11 +945,15 @@ class MeteocatConditionCoordinator(DataUpdateCoordinator):
|
|
|
903
945
|
return self.DEFAULT_CONDITION
|
|
904
946
|
|
|
905
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)
|
|
906
952
|
|
|
907
953
|
def _get_condition_for_current_hour(self, raw_data):
|
|
908
954
|
"""Get condition data for the current hour."""
|
|
909
955
|
# Fecha y hora actual
|
|
910
|
-
current_datetime = datetime.now()
|
|
956
|
+
current_datetime = datetime.now(TIMEZONE)
|
|
911
957
|
current_date = current_datetime.strftime("%Y-%m-%d")
|
|
912
958
|
current_hour = current_datetime.hour
|
|
913
959
|
|
|
@@ -915,8 +961,9 @@ class MeteocatConditionCoordinator(DataUpdateCoordinator):
|
|
|
915
961
|
for day in raw_data.get("dies", []):
|
|
916
962
|
if day["data"].startswith(current_date):
|
|
917
963
|
for value in day["variables"]["estatCel"]["valors"]:
|
|
918
|
-
data_hour = datetime.fromisoformat(value["data"])
|
|
919
|
-
|
|
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:
|
|
920
967
|
codi_estatcel = value["valor"]
|
|
921
968
|
condition = get_condition_from_statcel(
|
|
922
969
|
codi_estatcel,
|
|
@@ -973,6 +1020,11 @@ class MeteocatTempForecastCoordinator(DataUpdateCoordinator):
|
|
|
973
1020
|
update_interval=DEFAULT_TEMP_FORECAST_UPDATE_INTERVAL,
|
|
974
1021
|
)
|
|
975
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
|
+
|
|
976
1028
|
async def _is_data_valid(self) -> bool:
|
|
977
1029
|
"""Verifica si hay datos válidos y actuales en el archivo JSON."""
|
|
978
1030
|
if not os.path.exists(self.file_path):
|
|
@@ -986,16 +1038,18 @@ class MeteocatTempForecastCoordinator(DataUpdateCoordinator):
|
|
|
986
1038
|
if not data or "dies" not in data or not data["dies"]:
|
|
987
1039
|
return False
|
|
988
1040
|
|
|
989
|
-
today = datetime.now(
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
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
|
|
996
1049
|
except Exception as e:
|
|
997
1050
|
_LOGGER.warning("Error validando datos diarios en %s: %s", self.file_path, e)
|
|
998
|
-
|
|
1051
|
+
|
|
1052
|
+
return False
|
|
999
1053
|
|
|
1000
1054
|
async def _async_update_data(self) -> dict:
|
|
1001
1055
|
"""Lee y filtra los datos de predicción diaria desde el archivo local."""
|
|
@@ -1005,20 +1059,22 @@ class MeteocatTempForecastCoordinator(DataUpdateCoordinator):
|
|
|
1005
1059
|
content = await f.read()
|
|
1006
1060
|
data = json.loads(content)
|
|
1007
1061
|
|
|
1008
|
-
|
|
1009
|
-
today = datetime.now(timezone.utc).date()
|
|
1062
|
+
today = datetime.now(TIMEZONE).date()
|
|
1010
1063
|
data["dies"] = [
|
|
1011
1064
|
dia for dia in data["dies"]
|
|
1012
|
-
if
|
|
1065
|
+
if self._convert_to_local_time(
|
|
1066
|
+
datetime.fromisoformat(dia["data"].rstrip("Z")).replace(tzinfo=timezone.utc)
|
|
1067
|
+
).date() >= today
|
|
1013
1068
|
]
|
|
1014
1069
|
|
|
1015
|
-
# Usar datos de temperatura del día actual si están disponibles
|
|
1016
1070
|
today_temp_forecast = self.get_temp_forecast_for_today(data)
|
|
1017
1071
|
if today_temp_forecast:
|
|
1018
1072
|
parsed_data = self.parse_temp_forecast(today_temp_forecast)
|
|
1019
1073
|
return parsed_data
|
|
1020
1074
|
except Exception as e:
|
|
1021
|
-
_LOGGER.warning(
|
|
1075
|
+
_LOGGER.warning(
|
|
1076
|
+
"Error leyendo temperaturas del archivo de predicción diaria '%s': %s", self.file_path, e
|
|
1077
|
+
)
|
|
1022
1078
|
|
|
1023
1079
|
return {}
|
|
1024
1080
|
|
|
@@ -1027,19 +1083,21 @@ class MeteocatTempForecastCoordinator(DataUpdateCoordinator):
|
|
|
1027
1083
|
if not data or "dies" not in data or not data["dies"]:
|
|
1028
1084
|
return None
|
|
1029
1085
|
|
|
1030
|
-
today = datetime.now(
|
|
1086
|
+
today = datetime.now(TIMEZONE).date()
|
|
1031
1087
|
for dia in data["dies"]:
|
|
1032
|
-
|
|
1033
|
-
|
|
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:
|
|
1034
1091
|
return dia
|
|
1035
1092
|
return None
|
|
1036
1093
|
|
|
1037
1094
|
def parse_temp_forecast(self, dia: dict) -> dict:
|
|
1038
1095
|
"""Convierte la temperatura de un día de predicción en un diccionario con los datos necesarios."""
|
|
1039
1096
|
variables = dia.get("variables", {})
|
|
1097
|
+
forecast_date_utc = datetime.fromisoformat(dia["data"].rstrip("Z")).replace(tzinfo=timezone.utc)
|
|
1040
1098
|
|
|
1041
1099
|
temp_forecast_data = {
|
|
1042
|
-
"date":
|
|
1100
|
+
"date": self._convert_to_local_time(forecast_date_utc).date(),
|
|
1043
1101
|
"max_temp_forecast": float(variables.get("tmax", {}).get("valor", 0.0)),
|
|
1044
1102
|
"min_temp_forecast": float(variables.get("tmin", {}).get("valor", 0.0)),
|
|
1045
1103
|
}
|
|
@@ -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
|
-
|
|
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.
|
|
2
|
+
__version__ = "0.1.48"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "meteocat",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.48",
|
|
4
4
|
"description": "[](https://opensource.org/licenses/Apache-2.0)\r [](https://pypi.org/project/meteocat)\r [](https://gitlab.com/figorr/meteocat/commits/master)",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"directories": {
|