meteocat 0.1.40 → 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 +14 -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 +126 -7
- 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/package.json +1 -1
- package/pyproject.toml +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
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
|
+
|
|
1
15
|
## [0.1.40](https://github.com/figorr/meteocat/compare/v0.1.39...v0.1.40) (2024-12-26)
|
|
2
16
|
|
|
3
17
|
|
|
@@ -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
|
|
@@ -646,7 +650,15 @@ class HourlyForecastCoordinator(DataUpdateCoordinator):
|
|
|
646
650
|
datetime.fromisoformat(item["data"].rstrip("Z")).replace(tzinfo=timezone.utc) == forecast_time),
|
|
647
651
|
-1,
|
|
648
652
|
)
|
|
649
|
-
|
|
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"]
|
|
650
662
|
|
|
651
663
|
return {
|
|
652
664
|
"datetime": forecast_time.isoformat(),
|
|
@@ -842,6 +854,8 @@ class MeteocatConditionCoordinator(DataUpdateCoordinator):
|
|
|
842
854
|
|
|
843
855
|
async def _async_update_data(self):
|
|
844
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
|
+
|
|
845
859
|
try:
|
|
846
860
|
async with aiofiles.open(self._file_path, "r", encoding="utf-8") as file:
|
|
847
861
|
raw_data = await file.read()
|
|
@@ -889,6 +903,12 @@ class MeteocatConditionCoordinator(DataUpdateCoordinator):
|
|
|
889
903
|
"hour": current_hour,
|
|
890
904
|
"date": current_date,
|
|
891
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
|
+
)
|
|
892
912
|
return condition
|
|
893
913
|
break # Sale del bucle una vez encontrada la fecha actual
|
|
894
914
|
|
|
@@ -900,3 +920,102 @@ class MeteocatConditionCoordinator(DataUpdateCoordinator):
|
|
|
900
920
|
)
|
|
901
921
|
return {"condition": "unknown", "hour": current_hour, "icon": None, "date": current_date}
|
|
902
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"
|
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": {
|