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 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.40"
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(hours=48)
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(hours=48)
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=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, output_file)
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
- # Intentar cargar datos en caché si hay un error
185
- cached_data = load_json_from_file(output_file)
186
- if cached_data:
187
- _LOGGER.info("Usando datos en caché para la estación %s.", self.station_id)
188
- return cached_data
189
- # No se puede actualizar el estado, retornar None o un estado fallido
190
- return None # o cualquier otro valor que indique un estado de error
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=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 los sensores."""
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 sensor del Índice UV de Meteocat.
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.output_file = os.path.join(
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=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.output_file):
277
+ if not os.path.exists(self.uvi_file):
280
278
  return None
281
279
 
282
- async with aiofiles.open(self.output_file, "r", encoding="utf-8") as file:
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
- # Validar la fecha del primer elemento
295
- first_date = uvi_data[0].get("date")
296
- if first_date != datetime.now(timezone.utc).strftime("%Y-%m-%d"):
297
- return None
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 los sensores desde la API de Meteocat."""
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 de sensores actualizados exitosamente: %s", data)
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.output_file)
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
- # Intentar cargar datos en caché si hay un error
360
- cached_data = load_json_from_file(self.output_file)
361
- if cached_data:
362
- _LOGGER.info("Usando datos en caché para la ciudad %s.", self.town_id)
363
- return cached_data.get('uvi', [])
364
- return None
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=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=update_interval,
502
+ update_interval=DEFAULT_ENTITY_UPDATE_INTERVAL,
475
503
  )
476
504
 
477
- async def _is_data_valid(self, file_path: str) -> bool:
478
- """Verifica si los datos en el archivo JSON son válidos y actuales."""
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
- return False
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
- if not data or "dies" not in data or not data["dies"]:
488
- return False
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
- return first_date == datetime.now(timezone.utc).date()
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 False
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) -> Dict:
504
- """Actualiza los datos de predicción desde la API de Meteocat."""
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
- hourly_data = (
522
- load_json_from_file(hourly_file)
523
- if await self._is_data_valid(hourly_file)
524
- else await self._fetch_and_save_data(
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
- daily_data = (
529
- load_json_from_file(daily_file)
530
- if await self._is_data_valid(daily_file)
531
- else await self._fetch_and_save_data(
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
- return {
568
- "hourly": load_json_from_file(hourly_file) or {},
569
- "daily": load_json_from_file(daily_file) or {},
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=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
- condition = get_condition_from_code(int(condition_code))
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=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=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, timedelta
5
- from homeassistant.core import HomeAssistant
6
- from homeassistant.util.dt import as_local, as_utc
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: HomeAssistant, current_time: datetime | None = None):
12
- """Get the sunrise and sunset times."""
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 con offset)
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 tiempos de amanecer y atardecer desde el helper
21
- sunrise = get_astral_event_next(hass, "sunrise", utc_point_in_time=current_time)
22
- sunset = get_astral_event_next(hass, "sunset", utc_point_in_time=current_time)
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 as_local(sunrise), as_local(sunset)
31
+ return sunrise, sunset
27
32
 
28
- # Lanza un error si no se pudieron determinar los eventos
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: datetime, hass: HomeAssistant) -> bool:
32
- """Determine if it is currently night based on sunrise and sunset times."""
33
- # Convierte current_time a UTC si no tiene información de zona horaria
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
- # Compara las horas
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.15", "packaging>=20.3", "wrapt>=1.14.0"],
11
- "version": "0.1.40"
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 == "station_timestamp":
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 == "precipitation_accumulated":
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
+ )
@@ -127,6 +127,12 @@
127
127
  "name": "Hour"
128
128
  }
129
129
  }
130
+ },
131
+ "max_temperature_forecast": {
132
+ "name": "Max Temperature Today"
133
+ },
134
+ "min_temperature_forecast": {
135
+ "name": "Min Temperature Today"
130
136
  }
131
137
  }
132
138
  }
@@ -127,7 +127,14 @@
127
127
  "name": "Hora"
128
128
  }
129
129
  }
130
+ },
131
+ "max_temperature_forecast": {
132
+ "name": "Temperatura Max Avui"
133
+ },
134
+ "min_temperature_forecast": {
135
+ "name": "Temperatura Min Avui"
130
136
  }
131
137
  }
132
138
  }
133
- }
139
+ }
140
+
@@ -127,8 +127,14 @@
127
127
  "name": "Hour"
128
128
  }
129
129
  }
130
+ },
131
+ "max_temperature_forecast": {
132
+ "name": "Max Temperature Today"
133
+ },
134
+ "min_temperature_forecast": {
135
+ "name": "Min Temperature Today"
130
136
  }
131
137
  }
132
138
  }
133
139
  }
134
-
140
+
@@ -127,6 +127,12 @@
127
127
  "name": "Hora"
128
128
  }
129
129
  }
130
+ },
131
+ "max_temperature_forecast": {
132
+ "name": "Temperatura Max Hoy"
133
+ },
134
+ "min_temperature_forecast": {
135
+ "name": "Temperatura Min Hoy"
130
136
  }
131
137
  }
132
138
  }
@@ -1,2 +1,2 @@
1
1
  # version.py
2
- __version__ = "0.1.40"
2
+ __version__ = "0.1.42"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "meteocat",
3
- "version": "0.1.40",
3
+ "version": "0.1.42",
4
4
  "description": "[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)\r [![Python version compatibility](https://img.shields.io/pypi/pyversions/meteocat)](https://pypi.org/project/meteocat)\r [![pipeline status](https://gitlab.com/figorr/meteocat/badges/master/pipeline.svg)](https://gitlab.com/figorr/meteocat/commits/master)",
5
5
  "main": "index.js",
6
6
  "directories": {
package/pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "meteocat"
3
- version = "0.1.40"
3
+ version = "0.1.42"
4
4
  description = "Script para obtener datos meteorológicos de la API de Meteocat"
5
5
  authors = ["figorr <jdcuartero@yahoo.es>"]
6
6
  license = "Apache-2.0"