meteocat 2.1.0 → 2.2.3

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,21 @@
1
+ ## [2.2.3](https://github.com/figorr/meteocat/compare/v2.2.2...v2.2.3) (2025-02-08)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * 2.2.3 ([ce224a0](https://github.com/figorr/meteocat/commit/ce224a097e7c879c46985ff3a16143de1b822006))
7
+ * bump meteocatpy to 1.0.1 ([7119065](https://github.com/figorr/meteocat/commit/711906534fca5c61a9a3ab968a3572e70f05929e))
8
+ * new lightning sensors ([8528f57](https://github.com/figorr/meteocat/commit/8528f57d688f7fc21f66715ffeac086895afd1aa))
9
+ * update README ([10c86e5](https://github.com/figorr/meteocat/commit/10c86e5e373c661cf23524421c756374711d89fe))
10
+
11
+ ## [2.2.2](https://github.com/figorr/meteocat/compare/v2.2.1...v2.2.2) (2025-02-04)
12
+
13
+
14
+ ### Bug Fixes
15
+
16
+ * 2.2.2 ([3667e5d](https://github.com/figorr/meteocat/commit/3667e5d65069ee0079ebbaebeab6c929be8b9630))
17
+ * fix version 2.2.1 ([359cc7f](https://github.com/figorr/meteocat/commit/359cc7f7a9f1f025890bedc5177785016e9968d1))
18
+
1
19
  # [2.1.0](https://github.com/figorr/meteocat/compare/v2.0.3...v2.1.0) (2025-01-27)
2
20
 
3
21
 
package/README.md CHANGED
@@ -56,7 +56,7 @@ Once you pick the town you will be prompted to pick a station from the list. The
56
56
 
57
57
  ![Meteocat custom component station picker dialog](images/pick_station.png)
58
58
 
59
- Then you will be asked to set the XEMA and PREDICTIONS limits from the API.
59
+ Then you will be asked to set the API limits from your plan.
60
60
 
61
61
  ![Meteocat custom component api limits setting dialog](images/api_limits.png)
62
62
 
@@ -22,6 +22,10 @@ from .coordinator import (
22
22
  MeteocatTempForecastCoordinator,
23
23
  MeteocatAlertsCoordinator,
24
24
  MeteocatAlertsRegionCoordinator,
25
+ MeteocatQuotesCoordinator,
26
+ MeteocatQuotesFileCoordinator,
27
+ MeteocatLightningCoordinator,
28
+ MeteocatLightningFileCoordinator,
25
29
  )
26
30
 
27
31
  from .const import DOMAIN, PLATFORMS
@@ -29,7 +33,7 @@ from .const import DOMAIN, PLATFORMS
29
33
  _LOGGER = logging.getLogger(__name__)
30
34
 
31
35
  # Versión
32
- __version__ = "2.1.0"
36
+ __version__ = "2.2.3"
33
37
 
34
38
  # Definir el esquema de configuración CONFIG_SCHEMA
35
39
  CONFIG_SCHEMA = vol.Schema(
@@ -129,6 +133,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
129
133
  alerts_region_coordinator = MeteocatAlertsRegionCoordinator(hass=hass, entry_data=entry_data)
130
134
  await alerts_region_coordinator.async_config_entry_first_refresh()
131
135
 
136
+ quotes_coordinator = MeteocatQuotesCoordinator(hass=hass, entry_data=entry_data)
137
+ await quotes_coordinator.async_config_entry_first_refresh()
138
+
139
+ quotes_file_coordinator = MeteocatQuotesFileCoordinator(hass=hass, entry_data=entry_data)
140
+ await quotes_file_coordinator.async_config_entry_first_refresh()
141
+
142
+ lightning_coordinator = MeteocatLightningCoordinator(hass=hass, entry_data=entry_data)
143
+ await lightning_coordinator.async_config_entry_first_refresh()
144
+
145
+ lightning_file_coordinator = MeteocatLightningFileCoordinator(hass=hass, entry_data=entry_data)
146
+ await lightning_file_coordinator.async_config_entry_first_refresh()
147
+
132
148
  except Exception as err: # Capturar todos los errores
133
149
  _LOGGER.exception(f"Error al inicializar los coordinadores: {err}")
134
150
  return False
@@ -147,6 +163,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
147
163
  "temp_forecast_coordinator": temp_forecast_coordinator,
148
164
  "alerts_coordinator": alerts_coordinator,
149
165
  "alerts_region_coordinator": alerts_region_coordinator,
166
+ "quotes_coordinator": quotes_coordinator,
167
+ "quotes_file_coordinator": quotes_file_coordinator,
168
+ "lightning_coordinator": lightning_coordinator,
169
+ "lightning_file_coordinator": lightning_file_coordinator,
150
170
  **entry_data,
151
171
  }
152
172
 
@@ -213,6 +233,12 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
213
233
  alerts_file = files_folder / "alerts.json"
214
234
  alerts_region_file = files_folder / f"alerts_{region_id}.json"
215
235
 
236
+ # Archivo JSON de cuotas
237
+ quotes_file = files_folder / f"quotes.json"
238
+
239
+ # Archivo JSON de rayos
240
+ lightning_file = files_folder / f"lightning_{region_id}.json"
241
+
216
242
  # Validar la ruta base
217
243
  if not custom_components_path.exists():
218
244
  _LOGGER.warning(f"La ruta {custom_components_path} no existe. No se realizará la limpieza.")
@@ -226,6 +252,8 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
226
252
  safe_remove(forecast_hourly_data_file)
227
253
  safe_remove(forecast_daily_data_file)
228
254
  safe_remove(alerts_file)
255
+ safe_remove(quotes_file)
229
256
  safe_remove(alerts_region_file)
257
+ safe_remove(lightning_file)
230
258
  safe_remove(assets_folder, is_folder=True)
231
259
  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
 
@@ -60,8 +75,115 @@ class MeteocatConfigFlow(ConfigFlow, domain=DOMAIN):
60
75
  self.variable_id: str | None = None
61
76
  self.station_id: str | None = None
62
77
  self.station_name: str | None = None
78
+ self.region_id: str | None = None
63
79
  self._cache = {}
64
80
 
81
+ async def fetch_and_save_quotes(self, api_key):
82
+ """Obtiene las cuotas de la API de Meteocat y las guarda en quotes.json."""
83
+ meteocat_quotes = MeteocatQuotes(api_key)
84
+ quotes_dir = os.path.join(
85
+ self.hass.config.path(),
86
+ "custom_components",
87
+ "meteocat",
88
+ "files"
89
+ )
90
+ os.makedirs(quotes_dir, exist_ok=True)
91
+ quotes_file = os.path.join(quotes_dir, "quotes.json")
92
+
93
+ try:
94
+ data = await asyncio.wait_for(
95
+ meteocat_quotes.get_quotes(),
96
+ timeout=30
97
+ )
98
+
99
+ # Modificar los nombres de los planes con normalización
100
+ plan_mapping = {
101
+ "xdde_": "XDDE",
102
+ "prediccio_": "Prediccio",
103
+ "referencia basic": "Basic",
104
+ "xema_": "XEMA",
105
+ "quota": "Quota"
106
+ }
107
+
108
+ modified_plans = []
109
+ for plan in data["plans"]:
110
+ normalized_nom = normalize_name(plan["nom"])
111
+ new_name = next((v for k, v in plan_mapping.items() if normalized_nom.startswith(k)), plan["nom"])
112
+
113
+ modified_plans.append({
114
+ "nom": new_name,
115
+ "periode": plan["periode"],
116
+ "maxConsultes": plan["maxConsultes"],
117
+ "consultesRestants": plan["consultesRestants"],
118
+ "consultesRealitzades": plan["consultesRealitzades"]
119
+ })
120
+
121
+ # Añadir la clave 'actualitzat' con la fecha y hora actual de la zona horaria local
122
+ current_time = datetime.now(timezone.utc).astimezone(TIMEZONE).isoformat()
123
+ data_with_timestamp = {
124
+ "actualitzat": {
125
+ "dataUpdate": current_time
126
+ },
127
+ "client": data["client"],
128
+ "plans": modified_plans
129
+ }
130
+
131
+ # Guardar los datos en el archivo JSON
132
+ async with aiofiles.open(quotes_file, "w", encoding="utf-8") as file:
133
+ await file.write(json.dumps(data_with_timestamp, ensure_ascii=False, indent=4))
134
+
135
+ _LOGGER.info("Cuotas guardadas exitosamente en %s", quotes_file)
136
+
137
+ except Exception as ex:
138
+ _LOGGER.error("Error al obtener o guardar las cuotas: %s", ex)
139
+ raise HomeAssistantError("No se pudieron obtener las cuotas de la API")
140
+
141
+ async def create_alerts_file(self):
142
+ """Crea el archivo alerts.json si no existe."""
143
+ alerts_dir = os.path.join(
144
+ self.hass.config.path(),
145
+ "custom_components",
146
+ "meteocat",
147
+ "files"
148
+ )
149
+ os.makedirs(alerts_dir, exist_ok=True)
150
+ alerts_file = os.path.join(alerts_dir, "alerts.json")
151
+
152
+ if not os.path.exists(alerts_file):
153
+ initial_data = {
154
+ "actualitzat": {
155
+ "dataUpdate": "1970-01-01T00:00:00+00:00"
156
+ },
157
+ "dades": []
158
+ }
159
+ async with aiofiles.open(alerts_file, "w", encoding="utf-8") as file:
160
+ await file.write(json.dumps(initial_data, ensure_ascii=False, indent=4))
161
+
162
+ _LOGGER.info("Archivo alerts.json creado en %s", alerts_file)
163
+
164
+ async def create_lightning_file(self):
165
+ """Crea el archivo lightning_{self.region_id}.json si no existe."""
166
+ lightning_dir = os.path.join(
167
+ self.hass.config.path(),
168
+ "custom_components",
169
+ "meteocat",
170
+ "files"
171
+ )
172
+ os.makedirs(lightning_dir, exist_ok=True)
173
+ lightning_file = os.path.join(lightning_dir, f"lightning_{self.region_id}.json")
174
+
175
+ if not os.path.exists(lightning_file):
176
+ initial_data = {
177
+ "actualitzat": {
178
+ "dataUpdate": "1970-01-01T00:00:00+00:00"
179
+ },
180
+ "dades": []
181
+ }
182
+ async with aiofiles.open(lightning_file, "w", encoding="utf-8") as file:
183
+ await file.write(json.dumps(initial_data, ensure_ascii=False, indent=4))
184
+
185
+ _LOGGER.info("Archivo %s creado", lightning_file)
186
+
65
187
  async def async_step_user(
66
188
  self, user_input: dict[str, Any] | None = None
67
189
  ) -> ConfigFlowResult:
@@ -75,6 +197,10 @@ class MeteocatConfigFlow(ConfigFlow, domain=DOMAIN):
75
197
 
76
198
  try:
77
199
  self.municipis = await town_client.get_municipis()
200
+ # Aquí obtenemos y guardamos las cuotas
201
+ await self.fetch_and_save_quotes(self.api_key)
202
+ # Aquí creamos el archivo alerts.json si no existe
203
+ await self.create_alerts_file()
78
204
  except (BadRequestError, ForbiddenError, TooManyRequestsError, InternalServerError, UnknownAPIError) as ex:
79
205
  _LOGGER.error("Error al conectar con la API de Meteocat: %s", ex)
80
206
  errors["base"] = "cannot_connect"
@@ -205,6 +331,10 @@ class MeteocatConfigFlow(ConfigFlow, domain=DOMAIN):
205
331
  self.province_id = station_metadata.get("provincia", {}).get("codi", "")
206
332
  self.province_name = station_metadata.get("provincia", {}).get("nom", "")
207
333
  self.station_status = station_metadata.get("estats", [{}])[0].get("codi", "")
334
+
335
+ # Crear el archivo lightning después de obtener region_id
336
+ await self.create_lightning_file()
337
+
208
338
  return await self.async_step_set_api_limits()
209
339
  except Exception as ex:
210
340
  _LOGGER.error("Error al obtener los metadatos de la estación: %s", ex)
@@ -231,6 +361,9 @@ class MeteocatConfigFlow(ConfigFlow, domain=DOMAIN):
231
361
  if user_input is not None:
232
362
  self.limit_xema = user_input.get(LIMIT_XEMA, 750)
233
363
  self.limit_prediccio = user_input.get(LIMIT_PREDICCIO, 100)
364
+ self.limit_xdde = user_input.get(LIMIT_XDDE, 250)
365
+ self.limit_quota = user_input.get(LIMIT_QUOTA, 300)
366
+ self.limit_basic = user_input.get(LIMIT_BASIC, 300)
234
367
  return self.async_create_entry(
235
368
  title=self.selected_municipi["nom"],
236
369
  data={
@@ -252,6 +385,9 @@ class MeteocatConfigFlow(ConfigFlow, domain=DOMAIN):
252
385
  STATION_STATUS: str(self.station_status),
253
386
  LIMIT_XEMA: self.limit_xema,
254
387
  LIMIT_PREDICCIO: self.limit_prediccio,
388
+ LIMIT_XDDE: self.limit_xdde,
389
+ LIMIT_QUOTA: self.limit_quota,
390
+ LIMIT_BASIC: self.limit_basic,
255
391
  },
256
392
  )
257
393
 
@@ -259,6 +395,9 @@ class MeteocatConfigFlow(ConfigFlow, domain=DOMAIN):
259
395
  {
260
396
  vol.Required(LIMIT_XEMA, default=750): cv.positive_int,
261
397
  vol.Required(LIMIT_PREDICCIO, default=100): cv.positive_int,
398
+ vol.Required(LIMIT_XDDE, default=250): cv.positive_int,
399
+ vol.Required(LIMIT_QUOTA, default=300): cv.positive_int,
400
+ vol.Required(LIMIT_BASIC, default=2000): cv.positive_int,
262
401
  }
263
402
  )
264
403
 
@@ -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,13 @@ 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"
44
+ LIGHTNING_FILE_STATUS = "lightning_file_status"
35
45
 
36
46
  from homeassistant.const import Platform
37
47
 
@@ -44,6 +54,8 @@ DEFAULT_VALIDITY_DAYS = 1 # Número de días a partir de los cuales se consider
44
54
  DEFAULT_VALIDITY_HOURS = 5 # Hora a partir de la cual la API tiene la información actualizada de predicciones disponible para descarga
45
55
  DEFAULT_VALIDITY_MINUTES = 0 # Minutos a partir de los cuales la API tiene la información actualizada de predicciones disponible para descarga
46
56
  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
57
+ 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
58
+ DEFAULT_LIGHTNING_VALIDITY_TIME = 240 # Minutos a partir de los cuales los datos de rayos están obsoletos y se se debe proceder a una nueva llamada a la API
47
59
 
48
60
  # Multiplicadores para la duración de validez basada en limit_prediccio
49
61
  ALERT_VALIDITY_MULTIPLIER_100 = 12 # para limit_prediccio <= 100
@@ -71,6 +83,8 @@ STATION_TIMESTAMP = "station_timestamp" # Código de tiempo de la estación
71
83
  CONDITION = "condition" # Estado del cielo
72
84
  MAX_TEMPERATURE_FORECAST = "max_temperature_forecast" # Temperatura máxima prevista
73
85
  MIN_TEMPERATURE_FORECAST = "min_temperature_forecast" # Temperatura mínima prevista
86
+ LIGHTNING_REGION = "lightning_region" # Rayos de la comarca
87
+ LIGHTNING_TOWN = "lightning_town" # Rayos de la población
74
88
 
75
89
  # Definición de códigos para variables
76
90
  WIND_SPEED_CODE = 30