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 +22 -0
- package/custom_components/meteocat/__init__.py +6 -1
- package/custom_components/meteocat/condition.py +21 -0
- package/custom_components/meteocat/const.py +2 -0
- package/custom_components/meteocat/coordinator.py +181 -35
- package/custom_components/meteocat/helpers.py +30 -20
- package/custom_components/meteocat/manifest.json +1 -1
- package/custom_components/meteocat/sensor.py +78 -4
- package/custom_components/meteocat/strings.json +6 -0
- package/custom_components/meteocat/translations/ca.json +8 -1
- package/custom_components/meteocat/translations/en.json +7 -1
- package/custom_components/meteocat/translations/es.json +6 -0
- package/custom_components/meteocat/version.py +1 -1
- package/custom_components/meteocat/weather.py +88 -45
- package/package.json +1 -1
- package/pyproject.toml +1 -1
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.
|
|
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(
|
|
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(
|
|
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
|
-
|
|
295
|
-
first_date = uvi_data[0].get("date")
|
|
296
|
-
if
|
|
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
|
-
|
|
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
|
|
618
|
+
if not data or "dies" not in data:
|
|
615
619
|
return False
|
|
616
620
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
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
|
|
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
|
-
|
|
637
|
-
"""
|
|
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
|
|
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
|
-
|
|
649
|
-
|
|
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
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
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
|
|
5
|
-
from homeassistant.
|
|
6
|
-
from homeassistant.
|
|
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
|
|
12
|
-
"""
|
|
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
|
|
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
|
|
21
|
-
sunrise =
|
|
22
|
-
sunset =
|
|
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
|
|
31
|
+
return sunrise, sunset
|
|
27
32
|
|
|
28
|
-
|
|
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
|
|
32
|
-
"""
|
|
33
|
-
#
|
|
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
|
-
|
|
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
|
|
@@ -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 ==
|
|
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 ==
|
|
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
|
+
)
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
# version.py
|
|
2
|
-
__version__ = "0.1.
|
|
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(
|
|
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__(
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
return
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
126
|
-
temperature=forecast
|
|
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
|
|
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.
|
|
3
|
+
"version": "0.1.41",
|
|
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": {
|