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 +17 -0
- package/custom_components/meteocat/__init__.py +1 -1
- package/custom_components/meteocat/const.py +1 -0
- package/custom_components/meteocat/coordinator.py +118 -94
- package/custom_components/meteocat/manifest.json +1 -1
- package/custom_components/meteocat/sensor.py +62 -2
- package/custom_components/meteocat/strings.json +3 -0
- package/custom_components/meteocat/translations/ca.json +3 -0
- package/custom_components/meteocat/translations/en.json +3 -0
- package/custom_components/meteocat/translations/es.json +3 -0
- package/custom_components/meteocat/version.py +1 -1
- package/package.json +1 -1
- package/pyproject.toml +1 -1
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.
|
|
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 =
|
|
315
|
-
today =
|
|
316
|
-
current_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
|
-
#
|
|
457
|
-
current_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
|
-
#
|
|
539
|
-
first_date =
|
|
540
|
-
today =
|
|
541
|
-
current_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 =
|
|
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 =
|
|
687
|
-
|
|
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,
|
|
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
|
-
|
|
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=
|
|
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":
|
|
727
|
-
"temperature": self._get_variable_value(dia, "temp",
|
|
728
|
-
"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),
|
|
729
711
|
"condition": condition,
|
|
730
|
-
"wind_speed": self._get_variable_value(dia, "velVent",
|
|
731
|
-
"wind_bearing": self._get_variable_value(dia, "dirVent",
|
|
732
|
-
"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),
|
|
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 =
|
|
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 =
|
|
745
|
-
|
|
746
|
-
|
|
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
|
-
|
|
765
|
-
|
|
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 =
|
|
812
|
+
today = datetime.now(TIMEZONE).date()
|
|
815
813
|
for dia in data["dies"]:
|
|
816
|
-
forecast_date =
|
|
817
|
-
|
|
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 =
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
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 =
|
|
853
|
+
today = datetime.now(TIMEZONE).date()
|
|
851
854
|
for dia in self.data["dies"]:
|
|
852
|
-
forecast_date =
|
|
853
|
-
|
|
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":
|
|
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 =
|
|
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 =
|
|
953
|
-
|
|
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 =
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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 =
|
|
1086
|
+
today = datetime.now(TIMEZONE).date()
|
|
1065
1087
|
for dia in data["dies"]:
|
|
1066
|
-
|
|
1067
|
-
|
|
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":
|
|
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
|
}
|
|
@@ -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
|
|
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
|
|
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
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
# version.py
|
|
2
|
-
__version__ = "0.1.
|
|
2
|
+
__version__ = "0.1.49"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "meteocat",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.49",
|
|
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": {
|