meteocat 3.2.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.
- package/.github/ISSUE_TEMPLATE/bug_report.md +45 -45
- package/.github/ISSUE_TEMPLATE/config.yml +8 -8
- package/.github/ISSUE_TEMPLATE/improvement.md +39 -39
- package/.github/ISSUE_TEMPLATE/new_function.md +41 -41
- package/.github/labels.yml +63 -63
- package/.github/workflows/autocloser.yaml +27 -27
- package/.github/workflows/close-on-label.yml +48 -48
- package/.github/workflows/force-sync-labels.yml +18 -18
- package/.github/workflows/hassfest.yaml +13 -13
- package/.github/workflows/publish-zip.yml +67 -67
- package/.github/workflows/release.yml +41 -41
- package/.github/workflows/stale.yml +63 -63
- package/.github/workflows/sync-gitlab.yml +107 -107
- package/.github/workflows/sync-labels.yml +21 -21
- package/.github/workflows/validate.yaml +16 -16
- package/.pre-commit-config.yaml +37 -37
- package/.releaserc +37 -37
- package/AUTHORS.md +13 -13
- package/CHANGELOG.md +954 -932
- package/README.md +207 -207
- package/conftest.py +11 -11
- package/custom_components/meteocat/__init__.py +298 -298
- package/custom_components/meteocat/condition.py +63 -63
- package/custom_components/meteocat/config_flow.py +613 -613
- package/custom_components/meteocat/const.py +132 -132
- package/custom_components/meteocat/helpers.py +58 -58
- package/custom_components/meteocat/manifest.json +25 -25
- package/custom_components/meteocat/options_flow.py +287 -287
- package/custom_components/meteocat/strings.json +1058 -1058
- package/custom_components/meteocat/translations/ca.json +1058 -1058
- package/custom_components/meteocat/translations/en.json +1058 -1058
- package/custom_components/meteocat/translations/es.json +1058 -1058
- package/custom_components/meteocat/version.py +1 -1
- package/custom_components/meteocat/weather.py +218 -218
- package/filetree.py +48 -48
- package/filetree.txt +79 -79
- package/hacs.json +8 -8
- package/info.md +11 -11
- package/package.json +22 -22
- package/poetry.lock +3222 -3222
- package/pyproject.toml +68 -68
- package/requirements.test.txt +3 -3
- package/setup.cfg +64 -64
- package/setup.py +10 -10
- package/tests/bandit.yaml +17 -17
- package/tests/conftest.py +19 -19
- package/tests/test_init.py +9 -9
|
@@ -1,614 +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 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."""
|
|
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."""
|
|
614
614
|
return MeteocatOptionsFlowHandler(config_entry)
|