meteocat 3.1.0 → 4.0.0

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.
Files changed (57) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +45 -45
  2. package/.github/ISSUE_TEMPLATE/config.yml +8 -8
  3. package/.github/ISSUE_TEMPLATE/improvement.md +39 -39
  4. package/.github/ISSUE_TEMPLATE/new_function.md +41 -41
  5. package/.github/labels.yml +63 -63
  6. package/.github/workflows/autocloser.yaml +27 -27
  7. package/.github/workflows/close-on-label.yml +48 -48
  8. package/.github/workflows/force-sync-labels.yml +18 -18
  9. package/.github/workflows/hassfest.yaml +13 -13
  10. package/.github/workflows/publish-zip.yml +67 -67
  11. package/.github/workflows/release.yml +41 -41
  12. package/.github/workflows/stale.yml +63 -63
  13. package/.github/workflows/sync-gitlab.yml +107 -107
  14. package/.github/workflows/sync-labels.yml +21 -21
  15. package/.github/workflows/validate.yaml +16 -16
  16. package/.pre-commit-config.yaml +37 -37
  17. package/.releaserc +37 -37
  18. package/AUTHORS.md +13 -13
  19. package/CHANGELOG.md +954 -898
  20. package/README.md +207 -204
  21. package/conftest.py +11 -11
  22. package/custom_components/meteocat/__init__.py +298 -293
  23. package/custom_components/meteocat/condition.py +63 -59
  24. package/custom_components/meteocat/config_flow.py +613 -435
  25. package/custom_components/meteocat/const.py +132 -120
  26. package/custom_components/meteocat/coordinator.py +1040 -205
  27. package/custom_components/meteocat/helpers.py +58 -63
  28. package/custom_components/meteocat/manifest.json +25 -24
  29. package/custom_components/meteocat/options_flow.py +287 -277
  30. package/custom_components/meteocat/sensor.py +366 -4
  31. package/custom_components/meteocat/strings.json +1058 -867
  32. package/custom_components/meteocat/translations/ca.json +1058 -867
  33. package/custom_components/meteocat/translations/en.json +1058 -867
  34. package/custom_components/meteocat/translations/es.json +1058 -867
  35. package/custom_components/meteocat/version.py +1 -1
  36. package/custom_components/meteocat/weather.py +218 -218
  37. package/filetree.py +48 -48
  38. package/filetree.txt +79 -70
  39. package/hacs.json +8 -8
  40. package/images/daily_forecast_2_alerts.png +0 -0
  41. package/images/daily_forecast_no_alerts.png +0 -0
  42. package/images/diagnostic_sensors.png +0 -0
  43. package/images/dynamic_sensors.png +0 -0
  44. package/images/options.png +0 -0
  45. package/images/regenerate_assets.png +0 -0
  46. package/images/setup_options.png +0 -0
  47. package/images/system_options.png +0 -0
  48. package/info.md +11 -11
  49. package/package.json +22 -22
  50. package/poetry.lock +3222 -3222
  51. package/pyproject.toml +68 -68
  52. package/requirements.test.txt +3 -3
  53. package/setup.cfg +64 -64
  54. package/setup.py +10 -10
  55. package/tests/bandit.yaml +17 -17
  56. package/tests/conftest.py +19 -19
  57. package/tests/test_init.py +9 -9
@@ -1,436 +1,614 @@
1
- from __future__ import annotations
2
-
3
- import asyncio
4
- import json
5
- import logging
6
- from pathlib import Path
7
- from typing import Any
8
- from datetime import datetime, timezone
9
- from zoneinfo import ZoneInfo
10
-
11
- import voluptuous as vol
12
- import aiofiles
13
- import unicodedata
14
-
15
- from astral import LocationInfo
16
- from astral.sun import sun
17
- from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
18
- from homeassistant.core import callback
19
- from homeassistant.exceptions import HomeAssistantError
20
- from homeassistant.helpers import config_validation as cv
21
-
22
- from .helpers import get_storage_dir
23
- from .const import (
24
- DOMAIN,
25
- CONF_API_KEY,
26
- TOWN_NAME,
27
- TOWN_ID,
28
- VARIABLE_NAME,
29
- VARIABLE_ID,
30
- STATION_NAME,
31
- STATION_ID,
32
- STATION_TYPE,
33
- LATITUDE,
34
- LONGITUDE,
35
- ALTITUDE,
36
- REGION_ID,
37
- REGION_NAME,
38
- PROVINCE_ID,
39
- PROVINCE_NAME,
40
- STATION_STATUS,
41
- LIMIT_XEMA,
42
- LIMIT_PREDICCIO,
43
- LIMIT_XDDE,
44
- LIMIT_QUOTA,
45
- LIMIT_BASIC,
46
- )
47
-
48
- from .options_flow import MeteocatOptionsFlowHandler
49
- from meteocatpy.town import MeteocatTown
50
- from meteocatpy.symbols import MeteocatSymbols
51
- from meteocatpy.variables import MeteocatVariables
52
- from meteocatpy.townstations import MeteocatTownStations
53
- from meteocatpy.infostation import MeteocatInfoStation
54
- from meteocatpy.quotes import MeteocatQuotes
55
- from meteocatpy.exceptions import BadRequestError, ForbiddenError, TooManyRequestsError, InternalServerError, UnknownAPIError
56
-
57
- _LOGGER = logging.getLogger(__name__)
58
-
59
- # Definir la zona horaria local
60
- TIMEZONE = ZoneInfo("Europe/Madrid")
61
-
62
- INITIAL_TEMPLATE = {
63
- "actualitzat": {"dataUpdate": "1970-01-01T00:00:00+00:00"},
64
- "dades": []
65
- }
66
-
67
- def normalize_name(name: str) -> str:
68
- """Normaliza el nombre eliminando acentos y convirtiendo a minúsculas."""
69
- name = unicodedata.normalize("NFKD", name).encode("ASCII", "ignore").decode("utf-8")
70
- return name.lower()
71
-
72
- class MeteocatConfigFlow(ConfigFlow, domain=DOMAIN):
73
- """Flujo de configuración para Meteocat."""
74
-
75
- VERSION = 1
76
-
77
- def __init__(self):
78
- self.api_key: str | None = None
79
- self.municipis: list[dict[str, Any]] = []
80
- self.selected_municipi: dict[str, Any] | None = None
81
- self.variable_id: str | None = None
82
- self.station_id: str | None = None
83
- self.station_name: str | None = None
84
- self.region_id: str | None = None
85
- self.region_name: str | None = None
86
- self.province_id: str | None = None
87
- self.province_name: str | None = None
88
- self.station_type: str | None = None
89
- self.latitude: float | None = None
90
- self.longitude: float | None = None
91
- self.altitude: float | None = None
92
- self.station_status: str | None = None
93
-
94
- async def fetch_and_save_quotes(self, api_key: str):
95
- """Obtiene las cuotas de la API de Meteocat y las guarda en quotes.json."""
96
- meteocat_quotes = MeteocatQuotes(api_key)
97
- quotes_dir = get_storage_dir(self.hass, "files")
98
- quotes_file = quotes_dir / "quotes.json"
99
-
100
- try:
101
- data = await asyncio.wait_for(meteocat_quotes.get_quotes(), timeout=30)
102
-
103
- plan_mapping = {
104
- "xdde_": "XDDE",
105
- "prediccio_": "Prediccio",
106
- "referencia basic": "Basic",
107
- "xema_": "XEMA",
108
- "quota": "Quota",
109
- }
110
-
111
- modified_plans = []
112
- for plan in data["plans"]:
113
- normalized_nom = normalize_name(plan["nom"])
114
- new_name = next(
115
- (v for k, v in plan_mapping.items() if normalized_nom.startswith(k)), None
116
- )
117
- if new_name is None:
118
- _LOGGER.warning(
119
- "Nombre de plan desconocido en la API: %s (se usará el original)",
120
- plan["nom"],
121
- )
122
- new_name = plan["nom"]
123
-
124
- modified_plans.append(
125
- {
126
- "nom": new_name,
127
- "periode": plan["periode"],
128
- "maxConsultes": plan["maxConsultes"],
129
- "consultesRestants": plan["consultesRestants"],
130
- "consultesRealitzades": plan["consultesRealitzades"],
131
- }
132
- )
133
-
134
- current_time = datetime.now(timezone.utc).astimezone(TIMEZONE).isoformat()
135
- data_with_timestamp = {
136
- "actualitzat": {"dataUpdate": current_time},
137
- "client": data["client"],
138
- "plans": modified_plans,
139
- }
140
-
141
- async with aiofiles.open(quotes_file, "w", encoding="utf-8") as file:
142
- await file.write(
143
- json.dumps(data_with_timestamp, ensure_ascii=False, indent=4)
144
- )
145
- _LOGGER.info("Cuotas guardadas exitosamente en %s", quotes_file)
146
-
147
- except Exception as ex:
148
- _LOGGER.error("Error al obtener o guardar las cuotas: %s", ex)
149
- raise HomeAssistantError("No se pudieron obtener las cuotas de la API")
150
-
151
- async def create_alerts_file(self):
152
- """Crea los archivos de alertas global y regional si no existen."""
153
- alerts_dir = get_storage_dir(self.hass, "files")
154
-
155
- # Archivo global de alertas
156
- alerts_file = alerts_dir / "alerts.json"
157
- if not alerts_file.exists():
158
- async with aiofiles.open(alerts_file, "w", encoding="utf-8") as file:
159
- await file.write(
160
- json.dumps(INITIAL_TEMPLATE, ensure_ascii=False, indent=4)
161
- )
162
- _LOGGER.info("Archivo global %s creado con plantilla inicial", alerts_file)
163
-
164
- # Solo si existe region_id
165
- if self.region_id:
166
- # Archivo regional de alertas
167
- alerts_region_file = alerts_dir / f"alerts_{self.region_id}.json"
168
- if not alerts_region_file.exists():
169
- async with aiofiles.open(alerts_region_file, "w", encoding="utf-8") as file:
170
- await file.write(
171
- json.dumps(INITIAL_TEMPLATE, ensure_ascii=False, indent=4)
172
- )
173
- _LOGGER.info(
174
- "Archivo regional %s creado con plantilla inicial", alerts_region_file
175
- )
176
-
177
- # Archivo lightning regional
178
- lightning_file = alerts_dir / f"lightning_{self.region_id}.json"
179
- if not lightning_file.exists():
180
- async with aiofiles.open(lightning_file, "w", encoding="utf-8") as file:
181
- await file.write(
182
- json.dumps(INITIAL_TEMPLATE, ensure_ascii=False, indent=4)
183
- )
184
- _LOGGER.info(
185
- "Archivo lightning %s creado con plantilla inicial", lightning_file
186
- )
187
-
188
- async def create_sun_file(self):
189
- """Crea el archivo sun_{town_id}_data.json con datos iniciales de sunrise y sunset."""
190
- if not self.selected_municipi or not self.latitude or not self.longitude:
191
- _LOGGER.warning("No se puede crear sun_{town_id}_data.json: faltan municipio o coordenadas")
192
- return
193
-
194
- town_id = self.selected_municipi["codi"]
195
- files_dir = get_storage_dir(self.hass, "files")
196
- sun_file = files_dir / f"sun_{town_id}_data.json"
197
-
198
- if not sun_file.exists():
199
- try:
200
- # Crear objeto LocationInfo con las coordenadas y zona horaria
201
- location = LocationInfo(
202
- name=self.selected_municipi["nom"],
203
- region="Catalonia",
204
- timezone="Europe/Madrid",
205
- latitude=self.latitude,
206
- longitude=self.longitude
207
- )
208
-
209
- # Calcular sunrise y sunset para el día actual
210
- current_time = datetime.now(timezone.utc).astimezone(TIMEZONE)
211
- sun_data = sun(location.observer, date=current_time.date(), tzinfo=TIMEZONE)
212
-
213
- # Formatear los datos para el archivo
214
- sun_data_formatted = {
215
- "actualitzat": {"dataUpdate": current_time.isoformat()},
216
- "dades": [
217
- {
218
- "sunrise": sun_data["sunrise"].isoformat(),
219
- "sunset": sun_data["sunset"].isoformat(),
220
- "date": current_time.date().isoformat()
221
- }
222
- ]
223
- }
224
-
225
- # Guardar el archivo
226
- async with aiofiles.open(sun_file, "w", encoding="utf-8") as file:
227
- await file.write(json.dumps(sun_data_formatted, ensure_ascii=False, indent=4))
228
- _LOGGER.info("Archivo sun_%s_data.json creado con datos iniciales", town_id)
229
-
230
- except Exception as ex:
231
- _LOGGER.error("Error al crear sun_%s_data.json: %s", town_id, ex)
232
-
233
- async def async_step_user(
234
- self, user_input: dict[str, Any] | None = None
235
- ) -> ConfigFlowResult:
236
- """Primer paso: solicitar API Key."""
237
- errors = {}
238
- if user_input is not None:
239
- self.api_key = user_input[CONF_API_KEY]
240
- town_client = MeteocatTown(self.api_key)
241
- try:
242
- self.municipis = await town_client.get_municipis()
243
-
244
- # Guardar lista de municipios en towns.json
245
- assets_dir = get_storage_dir(self.hass, "assets")
246
- towns_file = assets_dir / "towns.json"
247
- async with aiofiles.open(towns_file, "w", encoding="utf-8") as file:
248
- await file.write(json.dumps({"towns": self.municipis}, ensure_ascii=False, indent=4))
249
- _LOGGER.info("Towns guardados en %s", towns_file)
250
-
251
- # Crea el archivo de cuotas
252
- await self.fetch_and_save_quotes(self.api_key)
253
- # Crea solo el archivo global de alertas (regional se hará después)
254
- await self.create_alerts_file()
255
- except Exception as ex:
256
- _LOGGER.error("Error al conectar con la API de Meteocat: %s", ex)
257
- errors["base"] = "cannot_connect"
258
- if not errors:
259
- return await self.async_step_select_municipi()
260
-
261
- schema = vol.Schema({vol.Required(CONF_API_KEY): str})
262
- return self.async_show_form(step_id="user", data_schema=schema, errors=errors)
263
-
264
- async def async_step_select_municipi(
265
- self, user_input: dict[str, Any] | None = None
266
- ) -> ConfigFlowResult:
267
- """Segundo paso: seleccionar el municipio."""
268
- errors = {}
269
- if user_input is not None:
270
- selected_codi = user_input["municipi"]
271
- self.selected_municipi = next(
272
- (m for m in self.municipis if m["codi"] == selected_codi), None
273
- )
274
- if self.selected_municipi:
275
- await self.fetch_symbols_and_variables()
276
-
277
- if self.selected_municipi:
278
- return await self.async_step_select_station()
279
-
280
- schema = vol.Schema(
281
- {vol.Required("municipi"): vol.In({m["codi"]: m["nom"] for m in self.municipis})}
282
- )
283
- return self.async_show_form(step_id="select_municipi", data_schema=schema, errors=errors)
284
-
285
- async def fetch_symbols_and_variables(self):
286
- """Descarga y guarda los símbolos y variables después de seleccionar el municipio."""
287
- assets_dir = get_storage_dir(self.hass, "assets")
288
- symbols_file = assets_dir / "symbols.json"
289
- variables_file = assets_dir / "variables.json"
290
- try:
291
- symbols_data = await MeteocatSymbols(self.api_key).fetch_symbols()
292
- async with aiofiles.open(symbols_file, "w", encoding="utf-8") as file:
293
- await file.write(json.dumps({"symbols": symbols_data}, ensure_ascii=False, indent=4))
294
-
295
- variables_data = await MeteocatVariables(self.api_key).get_variables()
296
- async with aiofiles.open(variables_file, "w", encoding="utf-8") as file:
297
- await file.write(json.dumps({"variables": variables_data}, ensure_ascii=False, indent=4))
298
-
299
- self.variable_id = next(
300
- (v["codi"] for v in variables_data if v["nom"].lower() == "temperatura"),
301
- None,
302
- )
303
- except json.JSONDecodeError as ex:
304
- _LOGGER.error("Archivo existente corrupto al cargar símbolos/variables: %s", ex)
305
- raise HomeAssistantError("Archivo corrupto de símbolos o variables")
306
- except Exception as ex:
307
- _LOGGER.error("Error al descargar símbolos o variables: %s", ex)
308
- raise HomeAssistantError("No se pudieron obtener símbolos o variables")
309
-
310
- async def async_step_select_station(
311
- self, user_input: dict[str, Any] | None = None
312
- ) -> ConfigFlowResult:
313
- """Tercer paso: seleccionar estación."""
314
- errors = {}
315
- townstations_client = MeteocatTownStations(self.api_key)
316
-
317
- try:
318
- # Obtener la lista completa de estaciones de la API
319
- all_stations = await townstations_client.stations_service.get_stations()
320
- assets_dir = get_storage_dir(self.hass, "assets")
321
- stations_file = assets_dir / "stations.json"
322
- async with aiofiles.open(stations_file, "w", encoding="utf-8") as file:
323
- await file.write(json.dumps({"stations": all_stations}, ensure_ascii=False, indent=4))
324
- _LOGGER.info("Lista completa de estaciones guardadas en %s", stations_file)
325
-
326
- # Obtener estaciones filtradas por municipio y variable
327
- stations_data = await townstations_client.get_town_stations(
328
- self.selected_municipi["codi"], self.variable_id
329
- )
330
-
331
- town_stations_file = assets_dir / f"stations_{self.selected_municipi['codi']}.json"
332
- async with aiofiles.open(town_stations_file, "w", encoding="utf-8") as file:
333
- await file.write(json.dumps({"town_stations": stations_data}, ensure_ascii=False, indent=4))
334
- _LOGGER.info("Lista de estaciones del municipio guardadas en %s", town_stations_file)
335
-
336
- except Exception as ex:
337
- _LOGGER.error("Error al obtener las estaciones: %s", ex)
338
- errors["base"] = "stations_fetch_failed"
339
- stations_data = []
340
-
341
- if not stations_data or "variables" not in stations_data[0]:
342
- errors["base"] = "no_stations"
343
- return self.async_show_form(step_id="select_station", errors=errors)
344
-
345
- if user_input is not None:
346
- selected_station_codi = user_input["station"]
347
- selected_station = next(
348
- (station for station in stations_data[0]["variables"][0]["estacions"]
349
- if station["codi"] == selected_station_codi),
350
- None,
351
- )
352
- if selected_station:
353
- self.station_id = selected_station["codi"]
354
- self.station_name = selected_station["nom"]
355
-
356
- # Obtener metadatos de la estación
357
- try:
358
- station_metadata = await MeteocatInfoStation(self.api_key).get_infostation(self.station_id)
359
- self.station_type = station_metadata.get("tipus", "")
360
- self.latitude = station_metadata.get("coordenades", {}).get("latitud", 0.0)
361
- self.longitude = station_metadata.get("coordenades", {}).get("longitud", 0.0)
362
- self.altitude = station_metadata.get("altitud", 0)
363
- self.region_id = station_metadata.get("comarca", {}).get("codi", "")
364
- self.region_name = station_metadata.get("comarca", {}).get("nom", "")
365
- self.province_id = station_metadata.get("provincia", {}).get("codi", "")
366
- self.province_name = station_metadata.get("provincia", {}).get("nom", "")
367
- self.station_status = station_metadata.get("estats", [{}])[0].get("codi", "")
368
-
369
- # Crear archivos de alertas y sun
370
- await self.create_alerts_file()
371
- await self.create_sun_file()
372
- return await self.async_step_set_api_limits()
373
- except Exception as ex:
374
- _LOGGER.error("Error al obtener los metadatos de la estación: %s", ex)
375
- errors["base"] = "metadata_fetch_failed"
376
- else:
377
- errors["base"] = "station_not_found"
378
-
379
- schema = vol.Schema(
380
- {vol.Required("station"): vol.In(
381
- {station["codi"]: station["nom"] for station in stations_data[0]["variables"][0]["estacions"]}
382
- )}
383
- )
384
- return self.async_show_form(step_id="select_station", data_schema=schema, errors=errors)
385
-
386
- async def async_step_set_api_limits(self, user_input=None):
387
- """Cuarto paso: límites de la API."""
388
- errors = {}
389
- if user_input is not None:
390
- self.limit_xema = user_input.get(LIMIT_XEMA, 750)
391
- self.limit_prediccio = user_input.get(LIMIT_PREDICCIO, 100)
392
- self.limit_xdde = user_input.get(LIMIT_XDDE, 250)
393
- self.limit_quota = user_input.get(LIMIT_QUOTA, 300)
394
- self.limit_basic = user_input.get(LIMIT_BASIC, 2000)
395
-
396
- return self.async_create_entry(
397
- title=self.selected_municipi["nom"],
398
- data={
399
- CONF_API_KEY: self.api_key,
400
- TOWN_NAME: self.selected_municipi["nom"],
401
- TOWN_ID: self.selected_municipi["codi"],
402
- VARIABLE_NAME: "Temperatura",
403
- VARIABLE_ID: str(self.variable_id),
404
- STATION_NAME: self.station_name,
405
- STATION_ID: self.station_id,
406
- STATION_TYPE: self.station_type,
407
- LATITUDE: self.latitude,
408
- LONGITUDE: self.longitude,
409
- ALTITUDE: self.altitude,
410
- REGION_ID: str(self.region_id),
411
- REGION_NAME: self.region_name,
412
- PROVINCE_ID: str(self.province_id),
413
- PROVINCE_NAME: self.province_name,
414
- STATION_STATUS: str(self.station_status),
415
- LIMIT_XEMA: self.limit_xema,
416
- LIMIT_PREDICCIO: self.limit_prediccio,
417
- LIMIT_XDDE: self.limit_xdde,
418
- LIMIT_QUOTA: self.limit_quota,
419
- LIMIT_BASIC: self.limit_basic,
420
- },
421
- )
422
-
423
- schema = vol.Schema({
424
- vol.Required(LIMIT_XEMA, default=750): cv.positive_int,
425
- vol.Required(LIMIT_PREDICCIO, default=100): cv.positive_int,
426
- vol.Required(LIMIT_XDDE, default=250): cv.positive_int,
427
- vol.Required(LIMIT_QUOTA, default=300): cv.positive_int,
428
- vol.Required(LIMIT_BASIC, default=2000): cv.positive_int,
429
- })
430
- return self.async_show_form(step_id="set_api_limits", data_schema=schema, errors=errors)
431
-
432
- @staticmethod
433
- @callback
434
- def async_get_options_flow(config_entry: ConfigEntry) -> MeteocatOptionsFlowHandler:
435
- """Devuelve el flujo de opciones para esta configuración."""
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import json
5
+ import logging
6
+ from pathlib import Path
7
+ from typing import Any
8
+ from datetime import date, datetime, timezone, timedelta
9
+ from zoneinfo import ZoneInfo
10
+
11
+ import voluptuous as vol
12
+ import aiofiles
13
+ import unicodedata
14
+
15
+ from solarmoonpy.location import Location, LocationInfo
16
+ from solarmoonpy.moon import (
17
+ moon_phase,
18
+ moon_day,
19
+ moon_rise_set,
20
+ illuminated_percentage,
21
+ moon_distance,
22
+ moon_angular_diameter,
23
+ lunation_number,
24
+ get_moon_phase_name,
25
+ get_lunation_duration
26
+ )
27
+ from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
28
+ from homeassistant.core import callback
29
+ from homeassistant.exceptions import HomeAssistantError
30
+ from homeassistant.helpers import config_validation as cv
31
+
32
+ from .helpers import get_storage_dir
33
+ from .const import (
34
+ DOMAIN,
35
+ CONF_API_KEY,
36
+ TOWN_NAME,
37
+ TOWN_ID,
38
+ VARIABLE_NAME,
39
+ VARIABLE_ID,
40
+ STATION_NAME,
41
+ STATION_ID,
42
+ STATION_TYPE,
43
+ LATITUDE,
44
+ LONGITUDE,
45
+ ALTITUDE,
46
+ REGION_ID,
47
+ REGION_NAME,
48
+ PROVINCE_ID,
49
+ PROVINCE_NAME,
50
+ STATION_STATUS,
51
+ LIMIT_XEMA,
52
+ LIMIT_PREDICCIO,
53
+ LIMIT_XDDE,
54
+ LIMIT_QUOTA,
55
+ LIMIT_BASIC,
56
+ )
57
+
58
+ from .options_flow import MeteocatOptionsFlowHandler
59
+ from meteocatpy.town import MeteocatTown
60
+ from meteocatpy.symbols import MeteocatSymbols
61
+ from meteocatpy.variables import MeteocatVariables
62
+ from meteocatpy.townstations import MeteocatTownStations
63
+ from meteocatpy.infostation import MeteocatInfoStation
64
+ from meteocatpy.quotes import MeteocatQuotes
65
+ from meteocatpy.exceptions import BadRequestError, ForbiddenError, TooManyRequestsError, InternalServerError, UnknownAPIError
66
+
67
+ _LOGGER = logging.getLogger(__name__)
68
+
69
+ # Definir la zona horaria local
70
+ TIMEZONE = ZoneInfo("Europe/Madrid")
71
+
72
+ INITIAL_TEMPLATE = {
73
+ "actualitzat": {"dataUpdate": "1970-01-01T00:00:00+00:00"},
74
+ "dades": []
75
+ }
76
+
77
+ def normalize_name(name: str) -> str:
78
+ """Normaliza el nombre eliminando acentos y convirtiendo a minúsculas."""
79
+ name = unicodedata.normalize("NFKD", name).encode("ASCII", "ignore").decode("utf-8")
80
+ return name.lower()
81
+
82
+ class MeteocatConfigFlow(ConfigFlow, domain=DOMAIN):
83
+ """Flujo de configuración para Meteocat."""
84
+
85
+ VERSION = 1
86
+
87
+ def __init__(self):
88
+ self.api_key: str | None = None
89
+ self.municipis: list[dict[str, Any]] = []
90
+ self.selected_municipi: dict[str, Any] | None = None
91
+ self.variable_id: str | None = None
92
+ self.station_id: str | None = None
93
+ self.station_name: str | None = None
94
+ self.region_id: str | None = None
95
+ self.region_name: str | None = None
96
+ self.province_id: str | None = None
97
+ self.province_name: str | None = None
98
+ self.station_type: str | None = None
99
+ self.latitude: float | None = None
100
+ self.longitude: float | None = None
101
+ self.altitude: float | None = None
102
+ self.station_status: str | None = None
103
+ self.location: Location | None = None
104
+ self.timezone_str: str | None = None
105
+
106
+ async def fetch_and_save_quotes(self, api_key: str):
107
+ """Obtiene las cuotas de la API de Meteocat y las guarda en quotes.json."""
108
+ meteocat_quotes = MeteocatQuotes(api_key)
109
+ quotes_dir = get_storage_dir(self.hass, "files")
110
+ quotes_file = quotes_dir / "quotes.json"
111
+
112
+ try:
113
+ data = await asyncio.wait_for(meteocat_quotes.get_quotes(), timeout=30)
114
+
115
+ plan_mapping = {
116
+ "xdde_": "XDDE",
117
+ "prediccio_": "Prediccio",
118
+ "referencia basic": "Basic",
119
+ "xema_": "XEMA",
120
+ "quota": "Quota",
121
+ }
122
+
123
+ modified_plans = []
124
+ for plan in data["plans"]:
125
+ normalized_nom = normalize_name(plan["nom"])
126
+ new_name = next(
127
+ (v for k, v in plan_mapping.items() if normalized_nom.startswith(k)), None
128
+ )
129
+ if new_name is None:
130
+ _LOGGER.warning(
131
+ "Nombre de plan desconocido en la API: %s (se usará el original)",
132
+ plan["nom"],
133
+ )
134
+ new_name = plan["nom"]
135
+
136
+ modified_plans.append(
137
+ {
138
+ "nom": new_name,
139
+ "periode": plan["periode"],
140
+ "maxConsultes": plan["maxConsultes"],
141
+ "consultesRestants": plan["consultesRestants"],
142
+ "consultesRealitzades": plan["consultesRealitzades"],
143
+ }
144
+ )
145
+
146
+ current_time = datetime.now(timezone.utc).astimezone(TIMEZONE).isoformat()
147
+ data_with_timestamp = {
148
+ "actualitzat": {"dataUpdate": current_time},
149
+ "client": data["client"],
150
+ "plans": modified_plans,
151
+ }
152
+
153
+ async with aiofiles.open(quotes_file, "w", encoding="utf-8") as file:
154
+ await file.write(
155
+ json.dumps(data_with_timestamp, ensure_ascii=False, indent=4)
156
+ )
157
+ _LOGGER.info("Cuotas guardadas exitosamente en %s", quotes_file)
158
+
159
+ except Exception as ex:
160
+ _LOGGER.error("Error al obtener o guardar las cuotas: %s", ex)
161
+ raise HomeAssistantError("No se pudieron obtener las cuotas de la API")
162
+
163
+ async def create_alerts_file(self):
164
+ """Crea los archivos de alertas global y regional si no existen."""
165
+ alerts_dir = get_storage_dir(self.hass, "files")
166
+
167
+ # Archivo global de alertas
168
+ alerts_file = alerts_dir / "alerts.json"
169
+ if not alerts_file.exists():
170
+ async with aiofiles.open(alerts_file, "w", encoding="utf-8") as file:
171
+ await file.write(
172
+ json.dumps(INITIAL_TEMPLATE, ensure_ascii=False, indent=4)
173
+ )
174
+ _LOGGER.info("Archivo global %s creado con plantilla inicial", alerts_file)
175
+
176
+ # Solo si existe region_id
177
+ if self.region_id:
178
+ # Archivo regional de alertas
179
+ alerts_region_file = alerts_dir / f"alerts_{self.region_id}.json"
180
+ if not alerts_region_file.exists():
181
+ async with aiofiles.open(alerts_region_file, "w", encoding="utf-8") as file:
182
+ await file.write(
183
+ json.dumps(INITIAL_TEMPLATE, ensure_ascii=False, indent=4)
184
+ )
185
+ _LOGGER.info(
186
+ "Archivo regional %s creado con plantilla inicial", alerts_region_file
187
+ )
188
+
189
+ # Archivo lightning regional
190
+ lightning_file = alerts_dir / f"lightning_{self.region_id}.json"
191
+ if not lightning_file.exists():
192
+ async with aiofiles.open(lightning_file, "w", encoding="utf-8") as file:
193
+ await file.write(
194
+ json.dumps(INITIAL_TEMPLATE, ensure_ascii=False, indent=4)
195
+ )
196
+ _LOGGER.info(
197
+ "Archivo lightning %s creado con plantilla inicial", lightning_file
198
+ )
199
+
200
+ async def create_sun_file(self):
201
+ """Crea el archivo sun_{town_id}_data.json con eventos solares + posición inicial del sol."""
202
+ if not self.selected_municipi or self.latitude is None or self.longitude is None:
203
+ _LOGGER.warning("No se puede crear sun_{town_id}_data.json: faltan municipio o coordenadas")
204
+ return
205
+
206
+ town_id = self.selected_municipi["codi"]
207
+ files_dir = get_storage_dir(self.hass, "files")
208
+ sun_file = files_dir / f"sun_{town_id.lower()}_data.json"
209
+
210
+ if sun_file.exists():
211
+ _LOGGER.debug("El archivo %s ya existe, no se crea de nuevo.", sun_file)
212
+ return
213
+
214
+ try:
215
+ # ZONA HORARIA DEL HASS
216
+ self.timezone_str = self.hass.config.time_zone or "Europe/Madrid"
217
+ tz = ZoneInfo(self.timezone_str)
218
+
219
+ # CREAR UBICACIÓN
220
+ self.location = Location(LocationInfo(
221
+ name=self.selected_municipi.get("nom", "Municipio"),
222
+ region="Spain",
223
+ timezone=self.timezone_str,
224
+ latitude=self.latitude,
225
+ longitude=self.longitude,
226
+ elevation=self.altitude or 0.0,
227
+ ))
228
+
229
+ now = datetime.now(tz)
230
+ today = now.date()
231
+ tomorrow = today + timedelta(days=1)
232
+
233
+ # EVENTOS HOY Y MAÑANA
234
+ events_today = self.location.sun_events(date=today, local=True)
235
+ events_tomorrow = self.location.sun_events(date=tomorrow, local=True)
236
+
237
+ # LÓGICA DE EVENTOS (igual que en el coordinador)
238
+ expected = {}
239
+ events_list = [
240
+ "dawn_astronomical", "dawn_nautical", "dawn_civil",
241
+ "sunrise", "noon", "sunset",
242
+ "dusk_civil", "dusk_nautical", "dusk_astronomical",
243
+ "midnight",
244
+ ]
245
+
246
+ for event in events_list:
247
+ event_time = events_today.get(event)
248
+ if event_time and now >= event_time:
249
+ expected[event] = events_tomorrow.get(event)
250
+ else:
251
+ expected[event] = event_time
252
+
253
+ # daylight_duration según sunrise
254
+ sunrise = expected["sunrise"]
255
+ expected["daylight_duration"] = (
256
+ events_tomorrow["daylight_duration"]
257
+ if sunrise == events_tomorrow["sunrise"]
258
+ else events_today["daylight_duration"]
259
+ )
260
+
261
+ # POSICIÓN ACTUAL DEL SOL
262
+ sun_pos = self.location.sun_position(dt=now, local=True)
263
+
264
+ # CONSTRUIR DADES
265
+ dades_dict = {
266
+ "dawn_civil": expected["dawn_civil"].isoformat() if expected["dawn_civil"] else None,
267
+ "dawn_nautical": expected["dawn_nautical"].isoformat() if expected["dawn_nautical"] else None,
268
+ "dawn_astronomical": expected["dawn_astronomical"].isoformat() if expected["dawn_astronomical"] else None,
269
+ "sunrise": expected["sunrise"].isoformat() if expected["sunrise"] else None,
270
+ "noon": expected["noon"].isoformat() if expected["noon"] else None,
271
+ "sunset": expected["sunset"].isoformat() if expected["sunset"] else None,
272
+ "dusk_civil": expected["dusk_civil"].isoformat() if expected["dusk_civil"] else None,
273
+ "dusk_nautical": expected["dusk_nautical"].isoformat() if expected["dusk_nautical"] else None,
274
+ "dusk_astronomical": expected["dusk_astronomical"].isoformat() if expected["dusk_astronomical"] else None,
275
+ "midnight": expected["midnight"].isoformat() if expected["midnight"] else None,
276
+ "daylight_duration": expected["daylight_duration"],
277
+
278
+ # CAMPOS DE POSICIÓN SOLAR
279
+ "sun_elevation": round(sun_pos["elevation"], 2),
280
+ "sun_azimuth": round(sun_pos["azimuth"], 2),
281
+ "sun_horizon_position": sun_pos["horizon_position"],
282
+ "sun_rising": sun_pos["rising"],
283
+ "sun_position_updated": now.isoformat(),
284
+ }
285
+
286
+ # JSON FINAL
287
+ data_with_timestamp = {
288
+ "actualitzat": {"dataUpdate": now.isoformat()},
289
+ "dades": [dades_dict],
290
+ }
291
+
292
+ # GUARDAR
293
+ sun_file.parent.mkdir(parents=True, exist_ok=True)
294
+ async with aiofiles.open(sun_file, "w", encoding="utf-8") as file:
295
+ await file.write(json.dumps(data_with_timestamp, ensure_ascii=False, indent=4))
296
+
297
+ _LOGGER.info(
298
+ "Archivo sun_%s_data.json creado con eventos + posición solar inicial (elev=%.2f°, az=%.2f°)",
299
+ town_id, sun_pos["elevation"], sun_pos["azimuth"]
300
+ )
301
+
302
+ except Exception as ex:
303
+ _LOGGER.error("Error al crear sun_%s_data.json: %s", town_id, ex)
304
+
305
+ async def create_moon_file(self):
306
+ """Crea el archivo moon_{town_id}_data.json con datos iniciales de la fase lunar, moonrise y moonset."""
307
+ if not self.selected_municipi or not self.latitude or not self.longitude:
308
+ _LOGGER.warning("No se puede crear moon_{town_id}_data.json: faltan municipio o coordenadas")
309
+ return
310
+
311
+ town_id = self.selected_municipi["codi"]
312
+ files_dir = get_storage_dir(self.hass, "files")
313
+ moon_file = files_dir / f"moon_{town_id}_data.json"
314
+
315
+ if not moon_file.exists():
316
+ try:
317
+ # Fecha actual en UTC
318
+ current_time = datetime.now(timezone.utc).astimezone(TIMEZONE)
319
+ today = current_time.date()
320
+
321
+ # Inicializar parámetros con valores por defecto
322
+ phase = None
323
+ moon_day_today = None
324
+ lunation = None
325
+ illuminated = None
326
+ distance = None
327
+ angular_diameter = None
328
+ moon_phase_name = None
329
+ lunation_duration = None
330
+
331
+ # Calcular parámetros con manejo de errores individual
332
+ try:
333
+ phase = round(moon_phase(today), 2)
334
+ except Exception as ex:
335
+ _LOGGER.error("Error al calcular moon_phase: %s", ex)
336
+
337
+ try:
338
+ moon_day_today = moon_day(today)
339
+ except Exception as ex:
340
+ _LOGGER.error("Error al calcular moon_day: %s", ex)
341
+
342
+ try:
343
+ lunation = lunation_number(today)
344
+ except Exception as ex:
345
+ _LOGGER.error("Error al calcular lunation_number: %s", ex)
346
+
347
+ try:
348
+ illuminated = round(illuminated_percentage(today), 2)
349
+ except Exception as ex:
350
+ _LOGGER.error("Error al calcular illuminated_percentage: %s", ex)
351
+
352
+ try:
353
+ distance = round(moon_distance(today), 0)
354
+ except Exception as ex:
355
+ _LOGGER.error("Error al calcular moon_distance: %s", ex)
356
+
357
+ try:
358
+ angular_diameter = round(moon_angular_diameter(today), 2)
359
+ except Exception as ex:
360
+ _LOGGER.error("Error al calcular moon_angular_diameter: %s", ex)
361
+
362
+ try:
363
+ moon_phase_name = get_moon_phase_name(today)
364
+ except Exception as ex:
365
+ _LOGGER.error("Error al calcular moon_phase_name: %s", ex)
366
+
367
+ try:
368
+ lunation_duration = get_lunation_duration(today)
369
+ except Exception as ex:
370
+ _LOGGER.error("Error al calcular lunation_duration: %s", ex)
371
+
372
+ # Moonrise y moonset aproximados (UTC)
373
+ try:
374
+ rise_utc, set_utc = moon_rise_set(self.latitude, self.longitude, today)
375
+ rise_local = rise_utc.astimezone(TIMEZONE).isoformat() if rise_utc else None
376
+ set_local = set_utc.astimezone(TIMEZONE).isoformat() if set_utc else None
377
+ except Exception as ex:
378
+ _LOGGER.error("Error al calcular moon_rise_set: %s", ex)
379
+ rise_local = None
380
+ set_local = None
381
+
382
+ # Formatear datos para guardar
383
+ moon_data_formatted = {
384
+ "actualitzat": {"dataUpdate": current_time.isoformat()},
385
+ "last_lunar_update_date": today.isoformat(),
386
+ "dades": [
387
+ {
388
+ "moon_day": moon_day_today,
389
+ "moon_phase": phase,
390
+ "moon_phase_name": moon_phase_name,
391
+ "illuminated_percentage": illuminated,
392
+ "moon_distance": distance,
393
+ "moon_angular_diameter": angular_diameter,
394
+ "lunation": lunation,
395
+ "lunation_duration": lunation_duration,
396
+ "moonrise": rise_local,
397
+ "moonset": set_local
398
+ }
399
+ ]
400
+ }
401
+
402
+ # Guardar el archivo
403
+ async with aiofiles.open(moon_file, "w", encoding="utf-8") as file:
404
+ await file.write(json.dumps(moon_data_formatted, ensure_ascii=False, indent=4))
405
+ _LOGGER.info("Archivo moon_%s_data.json creado con datos iniciales", town_id)
406
+
407
+ except Exception as ex:
408
+ _LOGGER.error("Error general al crear moon_%s_data.json: %s", town_id, ex)
409
+
410
+ async def async_step_user(
411
+ self, user_input: dict[str, Any] | None = None
412
+ ) -> ConfigFlowResult:
413
+ """Primer paso: solicitar API Key."""
414
+ errors = {}
415
+ if user_input is not None:
416
+ self.api_key = user_input[CONF_API_KEY]
417
+ town_client = MeteocatTown(self.api_key)
418
+ try:
419
+ self.municipis = await town_client.get_municipis()
420
+
421
+ # Guardar lista de municipios en towns.json
422
+ assets_dir = get_storage_dir(self.hass, "assets")
423
+ towns_file = assets_dir / "towns.json"
424
+ async with aiofiles.open(towns_file, "w", encoding="utf-8") as file:
425
+ await file.write(json.dumps({"towns": self.municipis}, ensure_ascii=False, indent=4))
426
+ _LOGGER.info("Towns guardados en %s", towns_file)
427
+
428
+ # Crea el archivo de cuotas
429
+ await self.fetch_and_save_quotes(self.api_key)
430
+ # Crea solo el archivo global de alertas (regional se hará después)
431
+ await self.create_alerts_file()
432
+ except Exception as ex:
433
+ _LOGGER.error("Error al conectar con la API de Meteocat: %s", ex)
434
+ errors["base"] = "cannot_connect"
435
+ if not errors:
436
+ return await self.async_step_select_municipi()
437
+
438
+ schema = vol.Schema({vol.Required(CONF_API_KEY): str})
439
+ return self.async_show_form(step_id="user", data_schema=schema, errors=errors)
440
+
441
+ async def async_step_select_municipi(
442
+ self, user_input: dict[str, Any] | None = None
443
+ ) -> ConfigFlowResult:
444
+ """Segundo paso: seleccionar el municipio."""
445
+ errors = {}
446
+ if user_input is not None:
447
+ selected_codi = user_input["municipi"]
448
+ self.selected_municipi = next(
449
+ (m for m in self.municipis if m["codi"] == selected_codi), None
450
+ )
451
+ if self.selected_municipi:
452
+ await self.fetch_symbols_and_variables()
453
+
454
+ if self.selected_municipi:
455
+ return await self.async_step_select_station()
456
+
457
+ schema = vol.Schema(
458
+ {vol.Required("municipi"): vol.In({m["codi"]: m["nom"] for m in self.municipis})}
459
+ )
460
+ return self.async_show_form(step_id="select_municipi", data_schema=schema, errors=errors)
461
+
462
+ async def fetch_symbols_and_variables(self):
463
+ """Descarga y guarda los símbolos y variables después de seleccionar el municipio."""
464
+ assets_dir = get_storage_dir(self.hass, "assets")
465
+ symbols_file = assets_dir / "symbols.json"
466
+ variables_file = assets_dir / "variables.json"
467
+ try:
468
+ symbols_data = await MeteocatSymbols(self.api_key).fetch_symbols()
469
+ async with aiofiles.open(symbols_file, "w", encoding="utf-8") as file:
470
+ await file.write(json.dumps({"symbols": symbols_data}, ensure_ascii=False, indent=4))
471
+
472
+ variables_data = await MeteocatVariables(self.api_key).get_variables()
473
+ async with aiofiles.open(variables_file, "w", encoding="utf-8") as file:
474
+ await file.write(json.dumps({"variables": variables_data}, ensure_ascii=False, indent=4))
475
+
476
+ self.variable_id = next(
477
+ (v["codi"] for v in variables_data if v["nom"].lower() == "temperatura"),
478
+ None,
479
+ )
480
+ except json.JSONDecodeError as ex:
481
+ _LOGGER.error("Archivo existente corrupto al cargar símbolos/variables: %s", ex)
482
+ raise HomeAssistantError("Archivo corrupto de símbolos o variables")
483
+ except Exception as ex:
484
+ _LOGGER.error("Error al descargar símbolos o variables: %s", ex)
485
+ raise HomeAssistantError("No se pudieron obtener símbolos o variables")
486
+
487
+ async def async_step_select_station(
488
+ self, user_input: dict[str, Any] | None = None
489
+ ) -> ConfigFlowResult:
490
+ """Tercer paso: seleccionar estación."""
491
+ errors = {}
492
+ townstations_client = MeteocatTownStations(self.api_key)
493
+
494
+ try:
495
+ # Obtener la lista completa de estaciones de la API
496
+ all_stations = await townstations_client.stations_service.get_stations()
497
+ assets_dir = get_storage_dir(self.hass, "assets")
498
+ stations_file = assets_dir / "stations.json"
499
+ async with aiofiles.open(stations_file, "w", encoding="utf-8") as file:
500
+ await file.write(json.dumps({"stations": all_stations}, ensure_ascii=False, indent=4))
501
+ _LOGGER.info("Lista completa de estaciones guardadas en %s", stations_file)
502
+
503
+ # Obtener estaciones filtradas por municipio y variable
504
+ stations_data = await townstations_client.get_town_stations(
505
+ self.selected_municipi["codi"], self.variable_id
506
+ )
507
+
508
+ town_stations_file = assets_dir / f"stations_{self.selected_municipi['codi']}.json"
509
+ async with aiofiles.open(town_stations_file, "w", encoding="utf-8") as file:
510
+ await file.write(json.dumps({"town_stations": stations_data}, ensure_ascii=False, indent=4))
511
+ _LOGGER.info("Lista de estaciones del municipio guardadas en %s", town_stations_file)
512
+
513
+ except Exception as ex:
514
+ _LOGGER.error("Error al obtener las estaciones: %s", ex)
515
+ errors["base"] = "stations_fetch_failed"
516
+ stations_data = []
517
+
518
+ if not stations_data or "variables" not in stations_data[0]:
519
+ errors["base"] = "no_stations"
520
+ return self.async_show_form(step_id="select_station", errors=errors)
521
+
522
+ if user_input is not None:
523
+ selected_station_codi = user_input["station"]
524
+ selected_station = next(
525
+ (station for station in stations_data[0]["variables"][0]["estacions"]
526
+ if station["codi"] == selected_station_codi),
527
+ None,
528
+ )
529
+ if selected_station:
530
+ self.station_id = selected_station["codi"]
531
+ self.station_name = selected_station["nom"]
532
+
533
+ # Obtener metadatos de la estación
534
+ try:
535
+ station_metadata = await MeteocatInfoStation(self.api_key).get_infostation(self.station_id)
536
+ self.station_type = station_metadata.get("tipus", "")
537
+ self.latitude = station_metadata.get("coordenades", {}).get("latitud", 0.0)
538
+ self.longitude = station_metadata.get("coordenades", {}).get("longitud", 0.0)
539
+ self.altitude = station_metadata.get("altitud", 0)
540
+ self.region_id = station_metadata.get("comarca", {}).get("codi", "")
541
+ self.region_name = station_metadata.get("comarca", {}).get("nom", "")
542
+ self.province_id = station_metadata.get("provincia", {}).get("codi", "")
543
+ self.province_name = station_metadata.get("provincia", {}).get("nom", "")
544
+ self.station_status = station_metadata.get("estats", [{}])[0].get("codi", "")
545
+
546
+ # Crear archivos de alertas, sol y luna
547
+ await self.create_alerts_file()
548
+ await self.create_sun_file()
549
+ await self.create_moon_file()
550
+ return await self.async_step_set_api_limits()
551
+ except Exception as ex:
552
+ _LOGGER.error("Error al obtener los metadatos de la estación: %s", ex)
553
+ errors["base"] = "metadata_fetch_failed"
554
+ else:
555
+ errors["base"] = "station_not_found"
556
+
557
+ schema = vol.Schema(
558
+ {vol.Required("station"): vol.In(
559
+ {station["codi"]: station["nom"] for station in stations_data[0]["variables"][0]["estacions"]}
560
+ )}
561
+ )
562
+ return self.async_show_form(step_id="select_station", data_schema=schema, errors=errors)
563
+
564
+ async def async_step_set_api_limits(self, user_input=None):
565
+ """Cuarto paso: límites de la API."""
566
+ errors = {}
567
+ if user_input is not None:
568
+ self.limit_xema = user_input.get(LIMIT_XEMA, 750)
569
+ self.limit_prediccio = user_input.get(LIMIT_PREDICCIO, 100)
570
+ self.limit_xdde = user_input.get(LIMIT_XDDE, 250)
571
+ self.limit_quota = user_input.get(LIMIT_QUOTA, 300)
572
+ self.limit_basic = user_input.get(LIMIT_BASIC, 2000)
573
+
574
+ return self.async_create_entry(
575
+ title=self.selected_municipi["nom"],
576
+ data={
577
+ CONF_API_KEY: self.api_key,
578
+ TOWN_NAME: self.selected_municipi["nom"],
579
+ TOWN_ID: self.selected_municipi["codi"],
580
+ VARIABLE_NAME: "Temperatura",
581
+ VARIABLE_ID: str(self.variable_id),
582
+ STATION_NAME: self.station_name,
583
+ STATION_ID: self.station_id,
584
+ STATION_TYPE: self.station_type,
585
+ LATITUDE: self.latitude,
586
+ LONGITUDE: self.longitude,
587
+ ALTITUDE: self.altitude,
588
+ REGION_ID: str(self.region_id),
589
+ REGION_NAME: self.region_name,
590
+ PROVINCE_ID: str(self.province_id),
591
+ PROVINCE_NAME: self.province_name,
592
+ STATION_STATUS: str(self.station_status),
593
+ LIMIT_XEMA: self.limit_xema,
594
+ LIMIT_PREDICCIO: self.limit_prediccio,
595
+ LIMIT_XDDE: self.limit_xdde,
596
+ LIMIT_QUOTA: self.limit_quota,
597
+ LIMIT_BASIC: self.limit_basic,
598
+ },
599
+ )
600
+
601
+ schema = vol.Schema({
602
+ vol.Required(LIMIT_XEMA, default=750): cv.positive_int,
603
+ vol.Required(LIMIT_PREDICCIO, default=100): cv.positive_int,
604
+ vol.Required(LIMIT_XDDE, default=250): cv.positive_int,
605
+ vol.Required(LIMIT_QUOTA, default=300): cv.positive_int,
606
+ vol.Required(LIMIT_BASIC, default=2000): cv.positive_int,
607
+ })
608
+ return self.async_show_form(step_id="set_api_limits", data_schema=schema, errors=errors)
609
+
610
+ @staticmethod
611
+ @callback
612
+ def async_get_options_flow(config_entry: ConfigEntry) -> MeteocatOptionsFlowHandler:
613
+ """Devuelve el flujo de opciones para esta configuración."""
436
614
  return MeteocatOptionsFlowHandler(config_entry)