meteocat 2.0.3 → 2.2.2

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,11 +1,29 @@
1
+ ## [2.2.2](https://github.com/figorr/meteocat/compare/v2.2.1...v2.2.2) (2025-02-04)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * 2.2.2 ([3667e5d](https://github.com/figorr/meteocat/commit/3667e5d65069ee0079ebbaebeab6c929be8b9630))
7
+ * fix version 2.2.1 ([359cc7f](https://github.com/figorr/meteocat/commit/359cc7f7a9f1f025890bedc5177785016e9968d1))
8
+
9
+ # [2.1.0](https://github.com/figorr/meteocat/compare/v2.0.3...v2.1.0) (2025-01-27)
10
+
11
+
12
+ ### Bug Fixes
13
+
14
+ * 2.1.0 ([759c293](https://github.com/figorr/meteocat/commit/759c2932fea8e4f174a753eca5b4c09f9d2549b6))
15
+
16
+
17
+ ### Features
18
+
19
+ * fix version ([596ee59](https://github.com/figorr/meteocat/commit/596ee59a835ee913238f66d40869ce27a53da4e9))
20
+
1
21
  ## [2.0.3](https://github.com/figorr/meteocat/compare/v2.0.2...v2.0.3) (2025-01-27)
2
22
 
23
+ * 2.0.3
3
24
 
4
25
  ### Bug Fixes
5
26
 
6
- * 2.0.2 ([efe8e9a](https://github.com/figorr/meteocat/commit/efe8e9aad133c7dff8fc7eabce7e5ab598b2e037))
7
- * 2.0.3 ([12a5470](https://github.com/figorr/meteocat/commit/12a5470994dbe95176a75bc05a530b33868b8141))
8
- * fix version 2.0.2 ([264c946](https://github.com/figorr/meteocat/commit/264c94659445d40601415eed22b7075f74d36823))
9
27
 
10
28
  ## [2.0.2](https://github.com/figorr/meteocat/compare/v2.0.1...v2.0.2) (2025-01-27)
11
29
 
@@ -22,6 +22,8 @@ from .coordinator import (
22
22
  MeteocatTempForecastCoordinator,
23
23
  MeteocatAlertsCoordinator,
24
24
  MeteocatAlertsRegionCoordinator,
25
+ MeteocatQuotesCoordinator,
26
+ MeteocatQuotesFileCoordinator,
25
27
  )
26
28
 
27
29
  from .const import DOMAIN, PLATFORMS
@@ -29,7 +31,7 @@ from .const import DOMAIN, PLATFORMS
29
31
  _LOGGER = logging.getLogger(__name__)
30
32
 
31
33
  # Versión
32
- __version__ = "2.0.3"
34
+ __version__ = "2.2.2"
33
35
 
34
36
  # Definir el esquema de configuración CONFIG_SCHEMA
35
37
  CONFIG_SCHEMA = vol.Schema(
@@ -129,6 +131,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
129
131
  alerts_region_coordinator = MeteocatAlertsRegionCoordinator(hass=hass, entry_data=entry_data)
130
132
  await alerts_region_coordinator.async_config_entry_first_refresh()
131
133
 
134
+ quotes_coordinator = MeteocatQuotesCoordinator(hass=hass, entry_data=entry_data)
135
+ await quotes_coordinator.async_config_entry_first_refresh()
136
+
137
+ quotes_file_coordinator = MeteocatQuotesFileCoordinator(hass=hass, entry_data=entry_data)
138
+ await quotes_file_coordinator.async_config_entry_first_refresh()
139
+
132
140
  except Exception as err: # Capturar todos los errores
133
141
  _LOGGER.exception(f"Error al inicializar los coordinadores: {err}")
134
142
  return False
@@ -147,6 +155,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
147
155
  "temp_forecast_coordinator": temp_forecast_coordinator,
148
156
  "alerts_coordinator": alerts_coordinator,
149
157
  "alerts_region_coordinator": alerts_region_coordinator,
158
+ "quotes_coordinator": quotes_coordinator,
159
+ "quotes_file_coordinator": quotes_file_coordinator,
150
160
  **entry_data,
151
161
  }
152
162
 
@@ -213,6 +223,9 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
213
223
  alerts_file = files_folder / "alerts.json"
214
224
  alerts_region_file = files_folder / f"alerts_{region_id}.json"
215
225
 
226
+ # Archivos JSON de cuotas
227
+ quotes_file = files_folder / f"quotes.json"
228
+
216
229
  # Validar la ruta base
217
230
  if not custom_components_path.exists():
218
231
  _LOGGER.warning(f"La ruta {custom_components_path} no existe. No se realizará la limpieza.")
@@ -226,6 +239,7 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
226
239
  safe_remove(forecast_hourly_data_file)
227
240
  safe_remove(forecast_daily_data_file)
228
241
  safe_remove(alerts_file)
242
+ safe_remove(quotes_file)
229
243
  safe_remove(alerts_region_file)
230
244
  safe_remove(assets_folder, is_folder=True)
231
245
  safe_remove(files_folder, is_folder=True)
@@ -1,14 +1,18 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import os
4
+ import asyncio
4
5
  import json
5
6
  import logging
6
7
  from pathlib import Path
7
8
  from typing import Any
9
+ from datetime import datetime, timezone
10
+ from zoneinfo import ZoneInfo
8
11
 
9
12
  import voluptuous as vol
10
13
  from aiohttp import ClientError
11
14
  import aiofiles
15
+ import unicodedata
12
16
 
13
17
  from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
14
18
  from homeassistant.core import callback
@@ -34,7 +38,10 @@ from .const import (
34
38
  PROVINCE_NAME,
35
39
  STATION_STATUS,
36
40
  LIMIT_XEMA,
37
- LIMIT_PREDICCIO
41
+ LIMIT_PREDICCIO,
42
+ LIMIT_XDDE,
43
+ LIMIT_BASIC,
44
+ LIMIT_QUOTA
38
45
  )
39
46
 
40
47
  from .options_flow import MeteocatOptionsFlowHandler
@@ -43,11 +50,19 @@ from meteocatpy.symbols import MeteocatSymbols
43
50
  from meteocatpy.variables import MeteocatVariables
44
51
  from meteocatpy.townstations import MeteocatTownStations
45
52
  from meteocatpy.infostation import MeteocatInfoStation
53
+ from meteocatpy.quotes import MeteocatQuotes
46
54
 
47
55
  from meteocatpy.exceptions import BadRequestError, ForbiddenError, TooManyRequestsError, InternalServerError, UnknownAPIError
48
56
 
49
57
  _LOGGER = logging.getLogger(__name__)
50
58
 
59
+ TIMEZONE = ZoneInfo("Europe/Madrid")
60
+
61
+ def normalize_name(name):
62
+ """Normaliza el nombre eliminando acentos y convirtiendo a minúsculas."""
63
+ name = unicodedata.normalize("NFKD", name).encode("ASCII", "ignore").decode("utf-8")
64
+ return name.lower()
65
+
51
66
  class MeteocatConfigFlow(ConfigFlow, domain=DOMAIN):
52
67
  """Flujo de configuración para Meteocat."""
53
68
 
@@ -62,6 +77,89 @@ class MeteocatConfigFlow(ConfigFlow, domain=DOMAIN):
62
77
  self.station_name: str | None = None
63
78
  self._cache = {}
64
79
 
80
+ async def fetch_and_save_quotes(self, api_key):
81
+ """Obtiene las cuotas de la API de Meteocat y las guarda en quotes.json."""
82
+ meteocat_quotes = MeteocatQuotes(api_key)
83
+ quotes_dir = os.path.join(
84
+ self.hass.config.path(),
85
+ "custom_components",
86
+ "meteocat",
87
+ "files"
88
+ )
89
+ os.makedirs(quotes_dir, exist_ok=True)
90
+ quotes_file = os.path.join(quotes_dir, "quotes.json")
91
+
92
+ try:
93
+ data = await asyncio.wait_for(
94
+ meteocat_quotes.get_quotes(),
95
+ timeout=30
96
+ )
97
+
98
+ # Modificar los nombres de los planes con normalización
99
+ plan_mapping = {
100
+ "xdde_": "XDDE",
101
+ "prediccio_": "Prediccio",
102
+ "referencia basic": "Basic",
103
+ "xema_": "XEMA",
104
+ "quota": "Quota"
105
+ }
106
+
107
+ modified_plans = []
108
+ for plan in data["plans"]:
109
+ normalized_nom = normalize_name(plan["nom"])
110
+ new_name = next((v for k, v in plan_mapping.items() if normalized_nom.startswith(k)), plan["nom"])
111
+
112
+ modified_plans.append({
113
+ "nom": new_name,
114
+ "periode": plan["periode"],
115
+ "maxConsultes": plan["maxConsultes"],
116
+ "consultesRestants": plan["consultesRestants"],
117
+ "consultesRealitzades": plan["consultesRealitzades"]
118
+ })
119
+
120
+ # Añadir la clave 'actualitzat' con la fecha y hora actual de la zona horaria local
121
+ current_time = datetime.now(timezone.utc).astimezone(TIMEZONE).isoformat()
122
+ data_with_timestamp = {
123
+ "actualitzat": {
124
+ "dataUpdate": current_time
125
+ },
126
+ "client": data["client"],
127
+ "plans": modified_plans
128
+ }
129
+
130
+ # Guardar los datos en el archivo JSON
131
+ async with aiofiles.open(quotes_file, "w", encoding="utf-8") as file:
132
+ await file.write(json.dumps(data_with_timestamp, ensure_ascii=False, indent=4))
133
+
134
+ _LOGGER.info("Cuotas guardadas exitosamente en %s", quotes_file)
135
+
136
+ except Exception as ex:
137
+ _LOGGER.error("Error al obtener o guardar las cuotas: %s", ex)
138
+ raise HomeAssistantError("No se pudieron obtener las cuotas de la API")
139
+
140
+ async def create_alerts_file(self):
141
+ """Crea el archivo alerts.json si no existe."""
142
+ alerts_dir = os.path.join(
143
+ self.hass.config.path(),
144
+ "custom_components",
145
+ "meteocat",
146
+ "files"
147
+ )
148
+ os.makedirs(alerts_dir, exist_ok=True)
149
+ alerts_file = os.path.join(alerts_dir, "alerts.json")
150
+
151
+ if not os.path.exists(alerts_file):
152
+ initial_data = {
153
+ "actualitzat": {
154
+ "dataUpdate": "1970-01-01T00:00:00+00:00"
155
+ },
156
+ "dades": []
157
+ }
158
+ async with aiofiles.open(alerts_file, "w", encoding="utf-8") as file:
159
+ await file.write(json.dumps(initial_data, ensure_ascii=False, indent=4))
160
+
161
+ _LOGGER.info("Archivo alerts.json creado en %s", alerts_file)
162
+
65
163
  async def async_step_user(
66
164
  self, user_input: dict[str, Any] | None = None
67
165
  ) -> ConfigFlowResult:
@@ -75,6 +173,10 @@ class MeteocatConfigFlow(ConfigFlow, domain=DOMAIN):
75
173
 
76
174
  try:
77
175
  self.municipis = await town_client.get_municipis()
176
+ # Aquí obtenemos y guardamos las cuotas
177
+ await self.fetch_and_save_quotes(self.api_key)
178
+ # Aquí creamos el archivo alerts.json si no existe
179
+ await self.create_alerts_file()
78
180
  except (BadRequestError, ForbiddenError, TooManyRequestsError, InternalServerError, UnknownAPIError) as ex:
79
181
  _LOGGER.error("Error al conectar con la API de Meteocat: %s", ex)
80
182
  errors["base"] = "cannot_connect"
@@ -231,6 +333,9 @@ class MeteocatConfigFlow(ConfigFlow, domain=DOMAIN):
231
333
  if user_input is not None:
232
334
  self.limit_xema = user_input.get(LIMIT_XEMA, 750)
233
335
  self.limit_prediccio = user_input.get(LIMIT_PREDICCIO, 100)
336
+ self.limit_xdde = user_input.get(LIMIT_XDDE, 250)
337
+ self.limit_quota = user_input.get(LIMIT_QUOTA, 300)
338
+ self.limit_basic = user_input.get(LIMIT_BASIC, 300)
234
339
  return self.async_create_entry(
235
340
  title=self.selected_municipi["nom"],
236
341
  data={
@@ -252,6 +357,9 @@ class MeteocatConfigFlow(ConfigFlow, domain=DOMAIN):
252
357
  STATION_STATUS: str(self.station_status),
253
358
  LIMIT_XEMA: self.limit_xema,
254
359
  LIMIT_PREDICCIO: self.limit_prediccio,
360
+ LIMIT_XDDE: self.limit_xdde,
361
+ LIMIT_QUOTA: self.limit_quota,
362
+ LIMIT_BASIC: self.limit_basic,
255
363
  },
256
364
  )
257
365
 
@@ -259,6 +367,9 @@ class MeteocatConfigFlow(ConfigFlow, domain=DOMAIN):
259
367
  {
260
368
  vol.Required(LIMIT_XEMA, default=750): cv.positive_int,
261
369
  vol.Required(LIMIT_PREDICCIO, default=100): cv.positive_int,
370
+ vol.Required(LIMIT_XDDE, default=250): cv.positive_int,
371
+ vol.Required(LIMIT_QUOTA, default=300): cv.positive_int,
372
+ vol.Required(LIMIT_BASIC, default=2000): cv.positive_int,
262
373
  }
263
374
  )
264
375
 
@@ -18,6 +18,9 @@ PROVINCE_ID = "province_id"
18
18
  PROVINCE_NAME = "province_name"
19
19
  LIMIT_XEMA = "limit_xema"
20
20
  LIMIT_PREDICCIO = "limit_prediccio"
21
+ LIMIT_XDDE = "limit_xdde"
22
+ LIMIT_BASIC = "limit_basic"
23
+ LIMIT_QUOTA = "limit_quota"
21
24
  STATION_STATUS = "station_status"
22
25
  HOURLY_FORECAST_FILE_STATUS = "hourly_forecast_file_status"
23
26
  DAILY_FORECAST_FILE_STATUS = "daily_forecast_file_status"
@@ -32,6 +35,12 @@ ALERT_COLD = "alert_cold"
32
35
  ALERT_WARM = "alert_warm"
33
36
  ALERT_WARM_NIGHT = "alert_warm_night"
34
37
  ALERT_SNOW = "alert_snow"
38
+ QUOTA_FILE_STATUS = "quota_file_status"
39
+ QUOTA_XDDE = "quota_xdde"
40
+ QUOTA_PREDICCIO = "quota_prediccio"
41
+ QUOTA_BASIC = "quota_basic"
42
+ QUOTA_XEMA = "quota_xema"
43
+ QUOTA_QUERIES = "quota_queries"
35
44
 
36
45
  from homeassistant.const import Platform
37
46
 
@@ -44,6 +53,7 @@ DEFAULT_VALIDITY_DAYS = 1 # Número de días a partir de los cuales se consider
44
53
  DEFAULT_VALIDITY_HOURS = 5 # Hora a partir de la cual la API tiene la información actualizada de predicciones disponible para descarga
45
54
  DEFAULT_VALIDITY_MINUTES = 0 # Minutos a partir de los cuales la API tiene la información actualizada de predicciones disponible para descarga
46
55
  DEFAULT_ALERT_VALIDITY_TIME = 120 # Minutos a partir de los cuales las alertas están obsoletas y se se debe proceder a una nueva llamada a la API
56
+ DEFAULT_QUOTES_VALIDITY_TIME = 240 # Minutos a partir de los cuales los datos de cuotas están obsoletos y se se debe proceder a una nueva llamada a la API
47
57
 
48
58
  # Multiplicadores para la duración de validez basada en limit_prediccio
49
59
  ALERT_VALIDITY_MULTIPLIER_100 = 12 # para limit_prediccio <= 100