meteocat 0.1.40 → 0.1.42
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 +23 -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 +247 -101
- package/custom_components/meteocat/helpers.py +30 -20
- package/custom_components/meteocat/manifest.json +2 -2
- 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,26 @@
|
|
|
1
|
+
## [0.1.42](https://github.com/figorr/meteocat/compare/v0.1.41...v0.1.42) (2024-12-28)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* 0.1.42 ([f180cf1](https://github.com/figorr/meteocat/commit/f180cf1400e614684cfee81849369bb74796ee5e))
|
|
7
|
+
* bump meteocatpy to 0.0.16 ([0e14f79](https://github.com/figorr/meteocat/commit/0e14f79445ee4c059d47a315bcbdc20858a0c666))
|
|
8
|
+
* set logger to warning when using cache data ([b840d72](https://github.com/figorr/meteocat/commit/b840d7202c439f83b08597b9365c007e92aca1c5))
|
|
9
|
+
|
|
10
|
+
## [0.1.41](https://github.com/figorr/meteocat/compare/v0.1.40...v0.1.41) (2024-12-27)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* 0.1.41 ([ba2c800](https://github.com/figorr/meteocat/commit/ba2c80048cc2da6efe5f950a5d8fb958a53a7ef6))
|
|
16
|
+
* add new Max and Min Today Temperature sensors ([6cc726d](https://github.com/figorr/meteocat/commit/6cc726d127432ddc13a54638157f1f9566967ed4))
|
|
17
|
+
* add Today temp max and min translations ([8e1d31e](https://github.com/figorr/meteocat/commit/8e1d31e78c1d5df61c37635a19d95b6339f0b0a5))
|
|
18
|
+
* add Today temp max and min translations ([041c3ab](https://github.com/figorr/meteocat/commit/041c3ab877b269f3b3d3359792ecf13b14969466))
|
|
19
|
+
* add Today temp max and min translations ([be25fc4](https://github.com/figorr/meteocat/commit/be25fc43bef58e1cecc2afaff8e0b8d3288399fb))
|
|
20
|
+
* add Today temp max and min translations ([e236182](https://github.com/figorr/meteocat/commit/e236182dfe29b15c3eb06d6a8fd3e02712837858))
|
|
21
|
+
* fix condition when is night ([fb64e0b](https://github.com/figorr/meteocat/commit/fb64e0b754eb5a39afe9135a0be842d5e8bdeae0))
|
|
22
|
+
* fix sunset and sunrise events for night flag ([4770e56](https://github.com/figorr/meteocat/commit/4770e5633707933c0bdd2aae0f74ada363da86bb))
|
|
23
|
+
|
|
1
24
|
## [0.1.40](https://github.com/figorr/meteocat/compare/v0.1.39...v0.1.40) (2024-12-26)
|
|
2
25
|
|
|
3
26
|
|
|
@@ -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.42"
|
|
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."""
|
|
@@ -83,7 +84,6 @@ class MeteocatSensorCoordinator(DataUpdateCoordinator):
|
|
|
83
84
|
self,
|
|
84
85
|
hass: HomeAssistant,
|
|
85
86
|
entry_data: dict,
|
|
86
|
-
update_interval: timedelta = DEFAULT_SENSOR_UPDATE_INTERVAL,
|
|
87
87
|
):
|
|
88
88
|
"""
|
|
89
89
|
Inicializa el coordinador de sensores de Meteocat.
|
|
@@ -102,11 +102,19 @@ class MeteocatSensorCoordinator(DataUpdateCoordinator):
|
|
|
102
102
|
self.variable_id = entry_data["variable_id"] # Usamos el ID de la variable
|
|
103
103
|
self.meteocat_station_data = MeteocatStationData(self.api_key)
|
|
104
104
|
|
|
105
|
+
self.station_file = os.path.join(
|
|
106
|
+
hass.config.path(),
|
|
107
|
+
"custom_components",
|
|
108
|
+
"meteocat",
|
|
109
|
+
"files",
|
|
110
|
+
f"station_{self.station_id.lower()}_data.json"
|
|
111
|
+
)
|
|
112
|
+
|
|
105
113
|
super().__init__(
|
|
106
114
|
hass,
|
|
107
115
|
_LOGGER,
|
|
108
116
|
name=f"{DOMAIN} Sensor Coordinator",
|
|
109
|
-
update_interval=
|
|
117
|
+
update_interval=DEFAULT_SENSOR_UPDATE_INTERVAL,
|
|
110
118
|
)
|
|
111
119
|
|
|
112
120
|
async def _async_update_data(self) -> Dict:
|
|
@@ -128,17 +136,8 @@ class MeteocatSensorCoordinator(DataUpdateCoordinator):
|
|
|
128
136
|
)
|
|
129
137
|
raise ValueError("Formato de datos inválido")
|
|
130
138
|
|
|
131
|
-
# Determinar la ruta al archivo en la carpeta raíz del repositorio
|
|
132
|
-
output_file = os.path.join(
|
|
133
|
-
self.hass.config.path(),
|
|
134
|
-
"custom_components",
|
|
135
|
-
"meteocat",
|
|
136
|
-
"files",
|
|
137
|
-
f"station_{self.station_id.lower()}_data.json"
|
|
138
|
-
)
|
|
139
|
-
|
|
140
139
|
# Guardar los datos en un archivo JSON
|
|
141
|
-
await save_json_to_file(data,
|
|
140
|
+
await save_json_to_file(data, self.station_file)
|
|
142
141
|
|
|
143
142
|
return data
|
|
144
143
|
except asyncio.TimeoutError as err:
|
|
@@ -181,13 +180,14 @@ class MeteocatSensorCoordinator(DataUpdateCoordinator):
|
|
|
181
180
|
self.station_id,
|
|
182
181
|
err,
|
|
183
182
|
)
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
183
|
+
# Intentar cargar datos en caché si hay un error
|
|
184
|
+
cached_data = load_json_from_file(self.station_file)
|
|
185
|
+
if cached_data:
|
|
186
|
+
_LOGGER.warning("Usando datos en caché para la estación %s.", self.station_id)
|
|
187
|
+
return cached_data
|
|
188
|
+
# No se puede actualizar el estado, retornar None
|
|
189
|
+
_LOGGER.error("No se pudo obtener datos actualizados ni cargar datos en caché.")
|
|
190
|
+
return None # o cualquier otro valor que indique un estado de error
|
|
191
191
|
|
|
192
192
|
class MeteocatStaticSensorCoordinator(DataUpdateCoordinator):
|
|
193
193
|
"""Coordinator to manage and update static sensor data."""
|
|
@@ -196,7 +196,6 @@ class MeteocatStaticSensorCoordinator(DataUpdateCoordinator):
|
|
|
196
196
|
self,
|
|
197
197
|
hass: HomeAssistant,
|
|
198
198
|
entry_data: dict,
|
|
199
|
-
update_interval: timedelta = DEFAULT_STATIC_SENSOR_UPDATE_INTERVAL,
|
|
200
199
|
):
|
|
201
200
|
"""
|
|
202
201
|
Initialize the MeteocatStaticSensorCoordinator.
|
|
@@ -215,7 +214,7 @@ class MeteocatStaticSensorCoordinator(DataUpdateCoordinator):
|
|
|
215
214
|
hass,
|
|
216
215
|
_LOGGER,
|
|
217
216
|
name=f"{DOMAIN} Static Sensor Coordinator",
|
|
218
|
-
update_interval=
|
|
217
|
+
update_interval=DEFAULT_STATIC_SENSOR_UPDATE_INTERVAL,
|
|
219
218
|
)
|
|
220
219
|
|
|
221
220
|
async def _async_update_data(self):
|
|
@@ -239,16 +238,15 @@ class MeteocatStaticSensorCoordinator(DataUpdateCoordinator):
|
|
|
239
238
|
}
|
|
240
239
|
|
|
241
240
|
class MeteocatUviCoordinator(DataUpdateCoordinator):
|
|
242
|
-
"""Coordinator para manejar la actualización de datos de
|
|
241
|
+
"""Coordinator para manejar la actualización de datos de UVI desde la API de Meteocat."""
|
|
243
242
|
|
|
244
243
|
def __init__(
|
|
245
244
|
self,
|
|
246
245
|
hass: HomeAssistant,
|
|
247
246
|
entry_data: dict,
|
|
248
|
-
update_interval: timedelta = DEFAULT_UVI_UPDATE_INTERVAL,
|
|
249
247
|
):
|
|
250
248
|
"""
|
|
251
|
-
Inicializa el coordinador del
|
|
249
|
+
Inicializa el coordinador del Índice UV de Meteocat.
|
|
252
250
|
|
|
253
251
|
Args:
|
|
254
252
|
hass (HomeAssistant): Instancia de Home Assistant.
|
|
@@ -258,7 +256,7 @@ class MeteocatUviCoordinator(DataUpdateCoordinator):
|
|
|
258
256
|
self.api_key = entry_data["api_key"] # Usamos la API key de la configuración
|
|
259
257
|
self.town_id = entry_data["town_id"] # Usamos el ID del municipio
|
|
260
258
|
self.meteocat_uvi_data = MeteocatUviData(self.api_key)
|
|
261
|
-
self.
|
|
259
|
+
self.uvi_file = os.path.join(
|
|
262
260
|
hass.config.path(),
|
|
263
261
|
"custom_components",
|
|
264
262
|
"meteocat",
|
|
@@ -270,16 +268,16 @@ class MeteocatUviCoordinator(DataUpdateCoordinator):
|
|
|
270
268
|
hass,
|
|
271
269
|
_LOGGER,
|
|
272
270
|
name=f"{DOMAIN} Uvi Coordinator",
|
|
273
|
-
update_interval=
|
|
271
|
+
update_interval=DEFAULT_UVI_UPDATE_INTERVAL,
|
|
274
272
|
)
|
|
275
273
|
|
|
276
274
|
async def is_uvi_data_valid(self) -> dict:
|
|
277
275
|
"""Comprueba si el archivo JSON contiene datos válidos para el día actual y devuelve los datos si son válidos."""
|
|
278
276
|
try:
|
|
279
|
-
if not os.path.exists(self.
|
|
277
|
+
if not os.path.exists(self.uvi_file):
|
|
280
278
|
return None
|
|
281
279
|
|
|
282
|
-
async with aiofiles.open(self.
|
|
280
|
+
async with aiofiles.open(self.uvi_file, "r", encoding="utf-8") as file:
|
|
283
281
|
content = await file.read()
|
|
284
282
|
data = json.loads(content)
|
|
285
283
|
|
|
@@ -291,18 +289,33 @@ class MeteocatUviCoordinator(DataUpdateCoordinator):
|
|
|
291
289
|
if not isinstance(uvi_data, list) or not uvi_data:
|
|
292
290
|
return None
|
|
293
291
|
|
|
294
|
-
|
|
295
|
-
first_date = uvi_data[0].get("date")
|
|
296
|
-
|
|
297
|
-
|
|
292
|
+
# Validar la fecha del primer elemento superior a 1 día
|
|
293
|
+
first_date = datetime.strptime(uvi_data[0].get("date"), "%Y-%m-%d").date()
|
|
294
|
+
today = datetime.now(timezone.utc).date()
|
|
295
|
+
|
|
296
|
+
# Log detallado
|
|
297
|
+
_LOGGER.info(
|
|
298
|
+
"Validando datos UVI en %s: Fecha de hoy: %s, Fecha del primer elemento: %s",
|
|
299
|
+
self.uvi_file,
|
|
300
|
+
today,
|
|
301
|
+
first_date,
|
|
302
|
+
)
|
|
298
303
|
|
|
304
|
+
# Verificar si la antigüedad es mayor a un día
|
|
305
|
+
if (today - first_date).days > 1:
|
|
306
|
+
_LOGGER.info(
|
|
307
|
+
"Los datos en %s son antiguos. Se procederá a llamar a la API.",
|
|
308
|
+
self.uvi_file,
|
|
309
|
+
)
|
|
310
|
+
return None
|
|
311
|
+
_LOGGER.info("Los datos en %s son válidos. Se usarán sin llamar a la API.", self.uvi_file)
|
|
299
312
|
return data
|
|
300
313
|
except Exception as e:
|
|
301
314
|
_LOGGER.error("Error al validar el archivo JSON del índice UV: %s", e)
|
|
302
315
|
return None
|
|
303
316
|
|
|
304
317
|
async def _async_update_data(self) -> Dict:
|
|
305
|
-
"""Actualiza los datos de
|
|
318
|
+
"""Actualiza los datos de UVI desde la API de Meteocat."""
|
|
306
319
|
try:
|
|
307
320
|
# Validar el archivo JSON existente
|
|
308
321
|
valid_data = await self.is_uvi_data_valid()
|
|
@@ -315,7 +328,7 @@ class MeteocatUviCoordinator(DataUpdateCoordinator):
|
|
|
315
328
|
self.meteocat_uvi_data.get_uvi_index(self.town_id),
|
|
316
329
|
timeout=30 # Tiempo límite de 30 segundos
|
|
317
330
|
)
|
|
318
|
-
_LOGGER.debug("Datos
|
|
331
|
+
_LOGGER.debug("Datos actualizados exitosamente: %s", data)
|
|
319
332
|
|
|
320
333
|
# Validar que los datos sean un dict con una clave 'uvi'
|
|
321
334
|
if not isinstance(data, dict) or 'uvi' not in data:
|
|
@@ -323,7 +336,7 @@ class MeteocatUviCoordinator(DataUpdateCoordinator):
|
|
|
323
336
|
raise ValueError("Formato de datos inválido")
|
|
324
337
|
|
|
325
338
|
# Guardar los datos en un archivo JSON
|
|
326
|
-
await save_json_to_file(data, self.
|
|
339
|
+
await save_json_to_file(data, self.uvi_file)
|
|
327
340
|
|
|
328
341
|
return data['uvi']
|
|
329
342
|
except asyncio.TimeoutError as err:
|
|
@@ -356,12 +369,14 @@ class MeteocatUviCoordinator(DataUpdateCoordinator):
|
|
|
356
369
|
self.town_id,
|
|
357
370
|
err,
|
|
358
371
|
)
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
372
|
+
# Intentar cargar datos en caché si hay un error
|
|
373
|
+
cached_data = load_json_from_file(self.uvi_file)
|
|
374
|
+
if cached_data:
|
|
375
|
+
_LOGGER.warning("Usando datos en caché para la ciudad %s.", self.town_id)
|
|
376
|
+
return cached_data.get('uvi', [])
|
|
377
|
+
# No se puede actualizar el estado, retornar None
|
|
378
|
+
_LOGGER.error("No se pudo obtener datos actualizados ni cargar datos en caché.")
|
|
379
|
+
return None
|
|
365
380
|
|
|
366
381
|
class MeteocatUviFileCoordinator(DataUpdateCoordinator):
|
|
367
382
|
"""Coordinator to read and process UV data from a file."""
|
|
@@ -370,7 +385,6 @@ class MeteocatUviFileCoordinator(DataUpdateCoordinator):
|
|
|
370
385
|
self,
|
|
371
386
|
hass: HomeAssistant,
|
|
372
387
|
entry_data: dict,
|
|
373
|
-
update_interval: timedelta = DEFAULT_UVI_SENSOR_UPDATE_INTERVAL,
|
|
374
388
|
):
|
|
375
389
|
"""
|
|
376
390
|
Inicializa el coordinador del sensor del Índice UV de Meteocat.
|
|
@@ -386,7 +400,7 @@ class MeteocatUviFileCoordinator(DataUpdateCoordinator):
|
|
|
386
400
|
hass,
|
|
387
401
|
_LOGGER,
|
|
388
402
|
name=f"{DOMAIN} Uvi File Coordinator",
|
|
389
|
-
update_interval=
|
|
403
|
+
update_interval=DEFAULT_UVI_SENSOR_UPDATE_INTERVAL,
|
|
390
404
|
)
|
|
391
405
|
self._file_path = os.path.join(
|
|
392
406
|
hass.config.path("custom_components/meteocat/files"),
|
|
@@ -448,7 +462,6 @@ class MeteocatEntityCoordinator(DataUpdateCoordinator):
|
|
|
448
462
|
self,
|
|
449
463
|
hass: HomeAssistant,
|
|
450
464
|
entry_data: dict,
|
|
451
|
-
update_interval: timedelta = DEFAULT_ENTITY_UPDATE_INTERVAL,
|
|
452
465
|
):
|
|
453
466
|
"""
|
|
454
467
|
Inicializa el coordinador de datos para entidades de predicción.
|
|
@@ -467,32 +480,64 @@ class MeteocatEntityCoordinator(DataUpdateCoordinator):
|
|
|
467
480
|
self.variable_id = entry_data["variable_id"]
|
|
468
481
|
self.meteocat_forecast = MeteocatForecast(self.api_key)
|
|
469
482
|
|
|
483
|
+
self.hourly_file = os.path.join(
|
|
484
|
+
hass.config.path(),
|
|
485
|
+
"custom_components",
|
|
486
|
+
"meteocat",
|
|
487
|
+
"files",
|
|
488
|
+
f"forecast_{self.town_id}_hourly_data.json",
|
|
489
|
+
)
|
|
490
|
+
self.daily_file = os.path.join(
|
|
491
|
+
hass.config.path(),
|
|
492
|
+
"custom_components",
|
|
493
|
+
"meteocat",
|
|
494
|
+
"files",
|
|
495
|
+
f"forecast_{self.town_id}_daily_data.json",
|
|
496
|
+
)
|
|
497
|
+
|
|
470
498
|
super().__init__(
|
|
471
499
|
hass,
|
|
472
500
|
_LOGGER,
|
|
473
501
|
name=f"{DOMAIN} Entity Coordinator",
|
|
474
|
-
update_interval=
|
|
502
|
+
update_interval=DEFAULT_ENTITY_UPDATE_INTERVAL,
|
|
475
503
|
)
|
|
476
504
|
|
|
477
|
-
async def
|
|
478
|
-
"""
|
|
505
|
+
async def validate_forecast_data(self, file_path: str) -> dict:
|
|
506
|
+
"""Valida y retorna datos de predicción si son válidos."""
|
|
479
507
|
if not os.path.exists(file_path):
|
|
480
|
-
|
|
481
|
-
|
|
508
|
+
_LOGGER.info("El archivo %s no existe. Se considerará inválido.", file_path)
|
|
509
|
+
return None
|
|
482
510
|
try:
|
|
483
511
|
async with aiofiles.open(file_path, "r", encoding="utf-8") as f:
|
|
484
512
|
content = await f.read()
|
|
485
513
|
data = json.loads(content)
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
return
|
|
489
|
-
|
|
514
|
+
if "dies" not in data or not data["dies"]:
|
|
515
|
+
_LOGGER.warning("El archivo %s no contiene datos válidos.", file_path)
|
|
516
|
+
return None
|
|
490
517
|
# Obtener la fecha del primer día
|
|
491
518
|
first_date = datetime.fromisoformat(data["dies"][0]["data"].rstrip("Z")).date()
|
|
492
|
-
|
|
519
|
+
today = datetime.now(timezone.utc).date()
|
|
520
|
+
|
|
521
|
+
# Log detallado
|
|
522
|
+
_LOGGER.info(
|
|
523
|
+
"Validando datos en %s: Fecha de hoy: %s, Fecha del primer elemento: %s",
|
|
524
|
+
file_path,
|
|
525
|
+
today,
|
|
526
|
+
first_date,
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
# Verificar si la antigüedad es mayor a un día
|
|
530
|
+
if (today - first_date).days > 1:
|
|
531
|
+
_LOGGER.info(
|
|
532
|
+
"Los datos en %s son antiguos. Se procederá a llamar a la API.",
|
|
533
|
+
file_path,
|
|
534
|
+
)
|
|
535
|
+
return None
|
|
536
|
+
_LOGGER.info("Los datos en %s son válidos. Se usarán sin llamar a la API.", file_path)
|
|
537
|
+
return data
|
|
493
538
|
except Exception as e:
|
|
494
539
|
_LOGGER.warning("Error validando datos en %s: %s", file_path, e)
|
|
495
|
-
return
|
|
540
|
+
return None
|
|
496
541
|
|
|
497
542
|
async def _fetch_and_save_data(self, api_method, file_path: str) -> dict:
|
|
498
543
|
"""Obtiene datos de la API y los guarda en un archivo JSON."""
|
|
@@ -500,44 +545,25 @@ class MeteocatEntityCoordinator(DataUpdateCoordinator):
|
|
|
500
545
|
await save_json_to_file(data, file_path)
|
|
501
546
|
return data
|
|
502
547
|
|
|
503
|
-
async def _async_update_data(self) ->
|
|
504
|
-
"""Actualiza los datos de predicción
|
|
505
|
-
hourly_file = os.path.join(
|
|
506
|
-
self.hass.config.path(),
|
|
507
|
-
"custom_components",
|
|
508
|
-
"meteocat",
|
|
509
|
-
"files",
|
|
510
|
-
f"forecast_{self.town_id.lower()}_hourly_data.json",
|
|
511
|
-
)
|
|
512
|
-
daily_file = os.path.join(
|
|
513
|
-
self.hass.config.path(),
|
|
514
|
-
"custom_components",
|
|
515
|
-
"meteocat",
|
|
516
|
-
"files",
|
|
517
|
-
f"forecast_{self.town_id.lower()}_daily_data.json",
|
|
518
|
-
)
|
|
519
|
-
|
|
548
|
+
async def _async_update_data(self) -> dict:
|
|
549
|
+
"""Actualiza los datos de predicción horaria y diaria."""
|
|
520
550
|
try:
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
self.meteocat_forecast.get_prediccion_horaria, hourly_file
|
|
551
|
+
# Validar o actualizar datos horarios
|
|
552
|
+
hourly_data = await self.validate_forecast_data(self.hourly_file)
|
|
553
|
+
if not hourly_data:
|
|
554
|
+
hourly_data = await self._fetch_and_save_data(
|
|
555
|
+
self.meteocat_forecast.get_prediccion_horaria, self.hourly_file
|
|
526
556
|
)
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
self.meteocat_forecast.get_prediccion_diaria, daily_file
|
|
557
|
+
|
|
558
|
+
# Validar o actualizar datos diarios
|
|
559
|
+
daily_data = await self.validate_forecast_data(self.daily_file)
|
|
560
|
+
if not daily_data:
|
|
561
|
+
daily_data = await self._fetch_and_save_data(
|
|
562
|
+
self.meteocat_forecast.get_prediccion_diaria, self.daily_file
|
|
533
563
|
)
|
|
534
|
-
)
|
|
535
564
|
|
|
536
|
-
_LOGGER.debug(
|
|
537
|
-
"Datos de predicción horaria y diaria actualizados correctamente para %s.",
|
|
538
|
-
self.town_id,
|
|
539
|
-
)
|
|
540
565
|
return {"hourly": hourly_data, "daily": daily_data}
|
|
566
|
+
|
|
541
567
|
except asyncio.TimeoutError as err:
|
|
542
568
|
_LOGGER.warning("Tiempo de espera agotado al obtener datos de predicción.")
|
|
543
569
|
raise ConfigEntryNotReady from err
|
|
@@ -564,10 +590,19 @@ class MeteocatEntityCoordinator(DataUpdateCoordinator):
|
|
|
564
590
|
raise
|
|
565
591
|
except Exception as err:
|
|
566
592
|
_LOGGER.exception("Error inesperado al obtener datos de predicción: %s", err)
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
593
|
+
|
|
594
|
+
# Si ocurre un error, intentar cargar datos desde los archivos locales
|
|
595
|
+
hourly_cache = load_json_from_file(self.hourly_file) or {}
|
|
596
|
+
daily_cache = load_json_from_file(self.daily_file) or {}
|
|
597
|
+
|
|
598
|
+
_LOGGER.warning(
|
|
599
|
+
"Cargando datos desde caché para %s. Datos horarios: %s, Datos diarios: %s",
|
|
600
|
+
self.town_id,
|
|
601
|
+
"Encontrados" if hourly_cache else "No encontrados",
|
|
602
|
+
"Encontrados" if daily_cache else "No encontrados",
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
return {"hourly": hourly_cache, "daily": daily_cache}
|
|
571
606
|
|
|
572
607
|
def get_condition_from_code(code: int) -> str:
|
|
573
608
|
"""Devuelve la condición meteorológica basada en el código."""
|
|
@@ -580,7 +615,6 @@ class HourlyForecastCoordinator(DataUpdateCoordinator):
|
|
|
580
615
|
self,
|
|
581
616
|
hass: HomeAssistant,
|
|
582
617
|
entry_data: dict,
|
|
583
|
-
update_interval: timedelta = DEFAULT_HOURLY_FORECAST_UPDATE_INTERVAL,
|
|
584
618
|
):
|
|
585
619
|
"""Inicializa el coordinador para predicciones horarias."""
|
|
586
620
|
self.town_name = entry_data["town_name"]
|
|
@@ -598,7 +632,7 @@ class HourlyForecastCoordinator(DataUpdateCoordinator):
|
|
|
598
632
|
hass,
|
|
599
633
|
_LOGGER,
|
|
600
634
|
name=f"{DOMAIN} Hourly Forecast Coordinator",
|
|
601
|
-
update_interval=
|
|
635
|
+
update_interval=DEFAULT_HOURLY_FORECAST_UPDATE_INTERVAL,
|
|
602
636
|
)
|
|
603
637
|
|
|
604
638
|
async def _is_data_valid(self) -> bool:
|
|
@@ -646,7 +680,15 @@ class HourlyForecastCoordinator(DataUpdateCoordinator):
|
|
|
646
680
|
datetime.fromisoformat(item["data"].rstrip("Z")).replace(tzinfo=timezone.utc) == forecast_time),
|
|
647
681
|
-1,
|
|
648
682
|
)
|
|
649
|
-
|
|
683
|
+
|
|
684
|
+
# Determinar la condición usando `get_condition_from_statcel`
|
|
685
|
+
condition_data = get_condition_from_statcel(
|
|
686
|
+
codi_estatcel=condition_code,
|
|
687
|
+
current_time=forecast_time,
|
|
688
|
+
hass=self.hass,
|
|
689
|
+
is_hourly=True
|
|
690
|
+
)
|
|
691
|
+
condition = condition_data["condition"]
|
|
650
692
|
|
|
651
693
|
return {
|
|
652
694
|
"datetime": forecast_time.isoformat(),
|
|
@@ -704,7 +746,6 @@ class DailyForecastCoordinator(DataUpdateCoordinator):
|
|
|
704
746
|
self,
|
|
705
747
|
hass: HomeAssistant,
|
|
706
748
|
entry_data: dict,
|
|
707
|
-
update_interval: timedelta = DEFAULT_DAILY_FORECAST_UPDATE_INTERVAL,
|
|
708
749
|
):
|
|
709
750
|
"""Inicializa el coordinador para predicciones diarias."""
|
|
710
751
|
self.town_name = entry_data["town_name"]
|
|
@@ -722,7 +763,7 @@ class DailyForecastCoordinator(DataUpdateCoordinator):
|
|
|
722
763
|
hass,
|
|
723
764
|
_LOGGER,
|
|
724
765
|
name=f"{DOMAIN} Daily Forecast Coordinator",
|
|
725
|
-
update_interval=
|
|
766
|
+
update_interval=DEFAULT_DAILY_FORECAST_UPDATE_INTERVAL,
|
|
726
767
|
)
|
|
727
768
|
|
|
728
769
|
async def _is_data_valid(self) -> bool:
|
|
@@ -815,7 +856,6 @@ class MeteocatConditionCoordinator(DataUpdateCoordinator):
|
|
|
815
856
|
self,
|
|
816
857
|
hass: HomeAssistant,
|
|
817
858
|
entry_data: dict,
|
|
818
|
-
update_interval: timedelta = DEFAULT_CONDITION_SENSOR_UPDATE_INTERVAL,
|
|
819
859
|
):
|
|
820
860
|
"""
|
|
821
861
|
Initialize the Meteocat Condition Coordinator.
|
|
@@ -832,7 +872,7 @@ class MeteocatConditionCoordinator(DataUpdateCoordinator):
|
|
|
832
872
|
hass,
|
|
833
873
|
_LOGGER,
|
|
834
874
|
name=f"{DOMAIN} Condition Coordinator",
|
|
835
|
-
update_interval=
|
|
875
|
+
update_interval=DEFAULT_CONDITION_SENSOR_UPDATE_INTERVAL,
|
|
836
876
|
)
|
|
837
877
|
|
|
838
878
|
self._file_path = os.path.join(
|
|
@@ -842,6 +882,8 @@ class MeteocatConditionCoordinator(DataUpdateCoordinator):
|
|
|
842
882
|
|
|
843
883
|
async def _async_update_data(self):
|
|
844
884
|
"""Read and process condition data for the current hour from the file asynchronously."""
|
|
885
|
+
_LOGGER.debug("Iniciando actualización de datos desde el archivo: %s", self._file_path)
|
|
886
|
+
|
|
845
887
|
try:
|
|
846
888
|
async with aiofiles.open(self._file_path, "r", encoding="utf-8") as file:
|
|
847
889
|
raw_data = await file.read()
|
|
@@ -889,6 +931,12 @@ class MeteocatConditionCoordinator(DataUpdateCoordinator):
|
|
|
889
931
|
"hour": current_hour,
|
|
890
932
|
"date": current_date,
|
|
891
933
|
})
|
|
934
|
+
_LOGGER.debug(
|
|
935
|
+
"Hora actual: %s, Código estatCel: %s, Condición procesada: %s",
|
|
936
|
+
current_datetime,
|
|
937
|
+
codi_estatcel,
|
|
938
|
+
condition,
|
|
939
|
+
)
|
|
892
940
|
return condition
|
|
893
941
|
break # Sale del bucle una vez encontrada la fecha actual
|
|
894
942
|
|
|
@@ -900,3 +948,101 @@ class MeteocatConditionCoordinator(DataUpdateCoordinator):
|
|
|
900
948
|
)
|
|
901
949
|
return {"condition": "unknown", "hour": current_hour, "icon": None, "date": current_date}
|
|
902
950
|
|
|
951
|
+
class MeteocatTempForecastCoordinator(DataUpdateCoordinator):
|
|
952
|
+
"""Coordinator para manejar las predicciones diarias desde archivos locales."""
|
|
953
|
+
|
|
954
|
+
def __init__(
|
|
955
|
+
self,
|
|
956
|
+
hass: HomeAssistant,
|
|
957
|
+
entry_data: dict,
|
|
958
|
+
):
|
|
959
|
+
"""Inicializa el coordinador para predicciones diarias."""
|
|
960
|
+
self.town_name = entry_data["town_name"]
|
|
961
|
+
self.town_id = entry_data["town_id"]
|
|
962
|
+
self.station_name = entry_data["station_name"]
|
|
963
|
+
self.station_id = entry_data["station_id"]
|
|
964
|
+
self.file_path = os.path.join(
|
|
965
|
+
hass.config.path(),
|
|
966
|
+
"custom_components",
|
|
967
|
+
"meteocat",
|
|
968
|
+
"files",
|
|
969
|
+
f"forecast_{self.town_id.lower()}_daily_data.json",
|
|
970
|
+
)
|
|
971
|
+
super().__init__(
|
|
972
|
+
hass,
|
|
973
|
+
_LOGGER,
|
|
974
|
+
name=f"{DOMAIN} Daily Forecast Coordinator",
|
|
975
|
+
update_interval=DEFAULT_TEMP_FORECAST_UPDATE_INTERVAL,
|
|
976
|
+
)
|
|
977
|
+
|
|
978
|
+
async def _is_data_valid(self) -> bool:
|
|
979
|
+
"""Verifica si hay datos válidos y actuales en el archivo JSON."""
|
|
980
|
+
if not os.path.exists(self.file_path):
|
|
981
|
+
return False
|
|
982
|
+
|
|
983
|
+
try:
|
|
984
|
+
async with aiofiles.open(self.file_path, "r", encoding="utf-8") as f:
|
|
985
|
+
content = await f.read()
|
|
986
|
+
data = json.loads(content)
|
|
987
|
+
|
|
988
|
+
if not data or "dies" not in data or not data["dies"]:
|
|
989
|
+
return False
|
|
990
|
+
|
|
991
|
+
today = datetime.now(timezone.utc).date()
|
|
992
|
+
for dia in data["dies"]:
|
|
993
|
+
forecast_date = datetime.fromisoformat(dia["data"].rstrip("Z")).date()
|
|
994
|
+
if forecast_date >= today:
|
|
995
|
+
return True
|
|
996
|
+
|
|
997
|
+
return False
|
|
998
|
+
except Exception as e:
|
|
999
|
+
_LOGGER.warning("Error validando datos diarios en %s: %s", self.file_path, e)
|
|
1000
|
+
return False
|
|
1001
|
+
|
|
1002
|
+
async def _async_update_data(self) -> dict:
|
|
1003
|
+
"""Lee y filtra los datos de predicción diaria desde el archivo local."""
|
|
1004
|
+
if await self._is_data_valid():
|
|
1005
|
+
try:
|
|
1006
|
+
async with aiofiles.open(self.file_path, "r", encoding="utf-8") as f:
|
|
1007
|
+
content = await f.read()
|
|
1008
|
+
data = json.loads(content)
|
|
1009
|
+
|
|
1010
|
+
# Filtrar días pasados
|
|
1011
|
+
today = datetime.now(timezone.utc).date()
|
|
1012
|
+
data["dies"] = [
|
|
1013
|
+
dia for dia in data["dies"]
|
|
1014
|
+
if datetime.fromisoformat(dia["data"].rstrip("Z")).date() >= today
|
|
1015
|
+
]
|
|
1016
|
+
|
|
1017
|
+
# Usar datos del día actual si están disponibles
|
|
1018
|
+
today_temp_forecast = self.get_temp_forecast_for_today(data)
|
|
1019
|
+
if today_temp_forecast:
|
|
1020
|
+
parsed_data = self.parse_temp_forecast(today_temp_forecast)
|
|
1021
|
+
return parsed_data
|
|
1022
|
+
except Exception as e:
|
|
1023
|
+
_LOGGER.warning("Error leyendo archivo de predicción diaria: %s", e)
|
|
1024
|
+
|
|
1025
|
+
return {}
|
|
1026
|
+
|
|
1027
|
+
def get_temp_forecast_for_today(self, data: dict) -> dict | None:
|
|
1028
|
+
"""Obtiene los datos diarios para el día actual."""
|
|
1029
|
+
if not data or "dies" not in data or not data["dies"]:
|
|
1030
|
+
return None
|
|
1031
|
+
|
|
1032
|
+
today = datetime.now(timezone.utc).date()
|
|
1033
|
+
for dia in data["dies"]:
|
|
1034
|
+
forecast_date = datetime.fromisoformat(dia["data"].rstrip("Z")).date()
|
|
1035
|
+
if forecast_date == today:
|
|
1036
|
+
return dia
|
|
1037
|
+
return None
|
|
1038
|
+
|
|
1039
|
+
def parse_temp_forecast(self, dia: dict) -> dict:
|
|
1040
|
+
"""Convierte un día de predicción en un diccionario con los datos necesarios."""
|
|
1041
|
+
variables = dia.get("variables", {})
|
|
1042
|
+
|
|
1043
|
+
temp_forecast_data = {
|
|
1044
|
+
"date": datetime.fromisoformat(dia["data"].rstrip("Z")).date(),
|
|
1045
|
+
"max_temp_forecast": float(variables.get("tmax", {}).get("valor", 0.0)),
|
|
1046
|
+
"min_temp_forecast": float(variables.get("tmin", {}).get("valor", 0.0)),
|
|
1047
|
+
}
|
|
1048
|
+
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
|
|
@@ -7,6 +7,6 @@
|
|
|
7
7
|
"iot_class": "cloud_polling",
|
|
8
8
|
"documentation": "https://gitlab.com/figorr/meteocat",
|
|
9
9
|
"loggers": ["meteocatpy"],
|
|
10
|
-
"requirements": ["meteocatpy==0.0.
|
|
11
|
-
"version": "0.1.
|
|
10
|
+
"requirements": ["meteocatpy==0.0.16", "packaging>=20.3", "wrapt>=1.14.0"],
|
|
11
|
+
"version": "0.1.42"
|
|
12
12
|
}
|
|
@@ -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.42"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "meteocat",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.42",
|
|
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": {
|