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.
Files changed (47) 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 -932
  20. package/README.md +207 -207
  21. package/conftest.py +11 -11
  22. package/custom_components/meteocat/__init__.py +298 -298
  23. package/custom_components/meteocat/condition.py +63 -63
  24. package/custom_components/meteocat/config_flow.py +613 -613
  25. package/custom_components/meteocat/const.py +132 -132
  26. package/custom_components/meteocat/helpers.py +58 -58
  27. package/custom_components/meteocat/manifest.json +25 -25
  28. package/custom_components/meteocat/options_flow.py +287 -287
  29. package/custom_components/meteocat/strings.json +1058 -1058
  30. package/custom_components/meteocat/translations/ca.json +1058 -1058
  31. package/custom_components/meteocat/translations/en.json +1058 -1058
  32. package/custom_components/meteocat/translations/es.json +1058 -1058
  33. package/custom_components/meteocat/version.py +1 -1
  34. package/custom_components/meteocat/weather.py +218 -218
  35. package/filetree.py +48 -48
  36. package/filetree.txt +79 -79
  37. package/hacs.json +8 -8
  38. package/info.md +11 -11
  39. package/package.json +22 -22
  40. package/poetry.lock +3222 -3222
  41. package/pyproject.toml +68 -68
  42. package/requirements.test.txt +3 -3
  43. package/setup.cfg +64 -64
  44. package/setup.py +10 -10
  45. package/tests/bandit.yaml +17 -17
  46. package/tests/conftest.py +19 -19
  47. package/tests/test_init.py +9 -9
@@ -1,288 +1,288 @@
1
- from __future__ import annotations
2
-
3
- import logging
4
- from homeassistant.config_entries import ConfigEntry, OptionsFlow
5
- from homeassistant.exceptions import HomeAssistantError
6
- from homeassistant.helpers import config_validation as cv
7
- from homeassistant.helpers.selector import SelectSelector, SelectSelectorConfig
8
- import voluptuous as vol
9
-
10
- from .const import (
11
- CONF_API_KEY,
12
- LIMIT_XEMA,
13
- LIMIT_PREDICCIO,
14
- LIMIT_XDDE,
15
- LIMIT_QUOTA,
16
- LIMIT_BASIC,
17
- LATITUDE,
18
- LONGITUDE,
19
- ALTITUDE,
20
- )
21
-
22
- from meteocatpy.town import MeteocatTown
23
- from meteocatpy.exceptions import (
24
- BadRequestError,
25
- ForbiddenError,
26
- TooManyRequestsError,
27
- InternalServerError,
28
- UnknownAPIError,
29
- )
30
-
31
- _LOGGER = logging.getLogger(__name__)
32
-
33
- class MeteocatOptionsFlowHandler(OptionsFlow):
34
- """Manejo del flujo de opciones para Meteocat."""
35
-
36
- def __init__(self, config_entry: ConfigEntry):
37
- """Inicializa el flujo de opciones."""
38
- self._config_entry = config_entry
39
- self.api_key: str | None = None
40
- self.limit_xema: int | None = None
41
- self.limit_prediccio: int | None = None
42
- self.limit_xdde: int | None = None
43
- self.limit_quota: int | None = None
44
- self.limit_basic: int | None = None
45
- self.latitude: float | None = None
46
- self.longitude: float | None = None
47
- self.altitude: float | None = None
48
-
49
- async def async_step_init(self, user_input: dict | None = None):
50
- """Paso inicial del flujo de opciones."""
51
- if user_input is not None:
52
- if user_input["option"] == "update_api_and_limits":
53
- return await self.async_step_update_api_and_limits()
54
- elif user_input["option"] == "update_limits_only":
55
- return await self.async_step_update_limits_only()
56
- elif user_input["option"] == "regenerate_assets":
57
- return await self.async_step_confirm_regenerate_assets()
58
- elif user_input["option"] == "update_coordinates":
59
- return await self.async_step_update_coordinates()
60
-
61
- return self.async_show_form(
62
- step_id="init",
63
- data_schema=vol.Schema({
64
- vol.Required("option"): SelectSelector(
65
- SelectSelectorConfig(
66
- options=[
67
- "update_api_and_limits",
68
- "update_limits_only",
69
- "regenerate_assets",
70
- "update_coordinates"
71
- ],
72
- translation_key="option"
73
- )
74
- )
75
- })
76
- )
77
-
78
- async def async_step_update_api_and_limits(self, user_input: dict | None = None):
79
- """Permite al usuario actualizar la API Key y los límites."""
80
- errors = {}
81
-
82
- if user_input is not None:
83
- self.api_key = user_input.get(CONF_API_KEY)
84
- self.limit_xema = user_input.get(LIMIT_XEMA)
85
- self.limit_prediccio = user_input.get(LIMIT_PREDICCIO)
86
- self.limit_xdde = user_input.get(LIMIT_XDDE)
87
- self.limit_quota = user_input.get(LIMIT_QUOTA)
88
- self.limit_basic = user_input.get(LIMIT_BASIC)
89
-
90
- # Validar la nueva API Key utilizando MeteocatTown
91
- if self.api_key:
92
- town_client = MeteocatTown(self.api_key)
93
- try:
94
- await town_client.get_municipis() # Verificar que la API Key sea válida
95
- except (
96
- BadRequestError,
97
- ForbiddenError,
98
- TooManyRequestsError,
99
- InternalServerError,
100
- UnknownAPIError,
101
- ) as ex:
102
- _LOGGER.error("Error al validar la nueva API Key: %s", ex)
103
- errors["base"] = "cannot_connect"
104
- except Exception as ex:
105
- _LOGGER.error("Error inesperado al validar la nueva API Key: %s", ex)
106
- errors["base"] = "unknown"
107
-
108
- # Validar que los límites sean números positivos
109
- limits_to_validate = [self.limit_xema, self.limit_prediccio, self.limit_xdde, self.limit_quota, self.limit_basic]
110
- if not all(cv.positive_int(limit) for limit in limits_to_validate if limit is not None):
111
- errors["base"] = "invalid_limit"
112
-
113
- if not errors:
114
- # Actualizar la configuración de la entrada con la nueva API Key y límites
115
- data_update = {}
116
- if self.api_key:
117
- data_update[CONF_API_KEY] = self.api_key
118
- if self.limit_xema:
119
- data_update[LIMIT_XEMA] = self.limit_xema
120
- if self.limit_prediccio:
121
- data_update[LIMIT_PREDICCIO] = self.limit_prediccio
122
- if self.limit_xdde:
123
- data_update[LIMIT_XDDE] = self.limit_xdde
124
- if self.limit_quota:
125
- data_update[LIMIT_QUOTA] = self.limit_quota
126
- if self.limit_basic:
127
- data_update[LIMIT_BASIC] = self.limit_basic
128
-
129
- self.hass.config_entries.async_update_entry(
130
- self._config_entry,
131
- data={**self._config_entry.data, **data_update},
132
- )
133
- # Recargar la integración para aplicar los cambios dinámicamente
134
- await self.hass.config_entries.async_reload(self._config_entry.entry_id)
135
-
136
- return self.async_create_entry(title="", data={})
137
-
138
- schema = vol.Schema({
139
- vol.Required(CONF_API_KEY): str,
140
- vol.Required(LIMIT_XEMA, default=self._config_entry.data.get(LIMIT_XEMA)): cv.positive_int,
141
- vol.Required(LIMIT_PREDICCIO, default=self._config_entry.data.get(LIMIT_PREDICCIO)): cv.positive_int,
142
- vol.Required(LIMIT_XDDE, default=self._config_entry.data.get(LIMIT_XDDE)): cv.positive_int,
143
- vol.Required(LIMIT_QUOTA, default=self._config_entry.data.get(LIMIT_QUOTA)): cv.positive_int,
144
- vol.Required(LIMIT_BASIC, default=self._config_entry.data.get(LIMIT_BASIC)): cv.positive_int,
145
- })
146
- return self.async_show_form(
147
- step_id="update_api_and_limits", data_schema=schema, errors=errors
148
- )
149
-
150
- async def async_step_update_limits_only(self, user_input: dict | None = None):
151
- """Permite al usuario actualizar solo los límites de la API."""
152
- errors = {}
153
-
154
- if user_input is not None:
155
- self.limit_xema = user_input.get(LIMIT_XEMA)
156
- self.limit_prediccio = user_input.get(LIMIT_PREDICCIO)
157
- self.limit_xdde = user_input.get(LIMIT_XDDE)
158
- self.limit_quota = user_input.get(LIMIT_QUOTA)
159
- self.limit_basic = user_input.get(LIMIT_BASIC)
160
-
161
- # Validar que los límites sean números positivos
162
- limits_to_validate = [self.limit_xema, self.limit_prediccio, self.limit_xdde, self.limit_quota, self.limit_basic]
163
- if not all(cv.positive_int(limit) for limit in limits_to_validate if limit is not None):
164
- errors["base"] = "invalid_limit"
165
-
166
- if not errors:
167
- self.hass.config_entries.async_update_entry(
168
- self._config_entry,
169
- data={
170
- **self._config_entry.data,
171
- LIMIT_XEMA: self.limit_xema,
172
- LIMIT_PREDICCIO: self.limit_prediccio,
173
- LIMIT_XDDE: self.limit_xdde,
174
- LIMIT_QUOTA: self.limit_quota,
175
- LIMIT_BASIC: self.limit_basic
176
- },
177
- )
178
- # Recargar la integración para aplicar los cambios dinámicamente
179
- await self.hass.config_entries.async_reload(self._config_entry.entry_id)
180
-
181
- return self.async_create_entry(title="", data={})
182
-
183
- schema = vol.Schema({
184
- vol.Required(LIMIT_XEMA, default=self._config_entry.data.get(LIMIT_XEMA)): cv.positive_int,
185
- vol.Required(LIMIT_PREDICCIO, default=self._config_entry.data.get(LIMIT_PREDICCIO)): cv.positive_int,
186
- vol.Required(LIMIT_XDDE, default=self._config_entry.data.get(LIMIT_XDDE)): cv.positive_int,
187
- vol.Required(LIMIT_QUOTA, default=self._config_entry.data.get(LIMIT_QUOTA)): cv.positive_int,
188
- vol.Required(LIMIT_BASIC, default=self._config_entry.data.get(LIMIT_BASIC)): cv.positive_int,
189
- })
190
- return self.async_show_form(
191
- step_id="update_limits_only", data_schema=schema, errors=errors
192
- )
193
-
194
- async def async_step_update_coordinates(self, user_input: dict | None = None):
195
- """Permite al usuario actualizar las coordenadas (latitude, longitude)."""
196
- errors = {}
197
-
198
- if user_input is not None:
199
- self.latitude = user_input.get(LATITUDE)
200
- self.longitude = user_input.get(LONGITUDE)
201
- self.altitude = user_input.get(ALTITUDE)
202
-
203
- # Validar que las coordenadas estén dentro del rango de Cataluña
204
- if not (40.5 <= self.latitude <= 42.5 and 0.1 <= self.longitude <= 3.3):
205
- _LOGGER.error(
206
- "Coordenadas fuera del rango de Cataluña (latitude: %s, longitude: %s).",
207
- self.latitude, self.longitude
208
- )
209
- errors["base"] = "invalid_coordinates"
210
- # Validar que la altitud sea positiva
211
- elif self.altitude < 0:
212
- _LOGGER.error("Altitud inválida: %s. Debe ser >= 0.", self.altitude)
213
- errors["base"] = "invalid_altitude"
214
- else:
215
- # Actualizar la configuración con las nuevas coordenadas
216
- self.hass.config_entries.async_update_entry(
217
- self._config_entry,
218
- data={
219
- **self._config_entry.data,
220
- LATITUDE: self.latitude,
221
- LONGITUDE: self.longitude,
222
- ALTITUDE: self.altitude
223
- },
224
- )
225
- # Recargar la integración para aplicar los cambios
226
- await self.hass.config_entries.async_reload(self._config_entry.entry_id)
227
- _LOGGER.info(
228
- "Coordenadas actualizadas a latitude: %s, longitude: %s, altitude=%s.",
229
- self.latitude, self.longitude, self.altitude
230
- )
231
- return self.async_create_entry(title="", data={})
232
-
233
- schema = vol.Schema({
234
- vol.Required(LATITUDE, default=self._config_entry.data.get(LATITUDE)): cv.latitude,
235
- vol.Required(LONGITUDE, default=self._config_entry.data.get(LONGITUDE)): cv.longitude,
236
- vol.Required(ALTITUDE, default=self._config_entry.data.get(ALTITUDE)): vol.Coerce(float),
237
- })
238
- return self.async_show_form(
239
- step_id="update_coordinates",
240
- data_schema=schema,
241
- errors=errors,
242
- description_placeholders={
243
- "current_latitude": self._config_entry.data.get(LATITUDE),
244
- "current_longitude": self._config_entry.data.get(LONGITUDE),
245
- "current_altitude": self._config_entry.data.get(ALTITUDE, 0.0)
246
- }
247
- )
248
-
249
- async def async_step_confirm_regenerate_assets(self, user_input: dict | None = None):
250
- """Confirma si el usuario realmente quiere regenerar los assets."""
251
- if user_input is not None:
252
- if user_input.get("confirm") is True:
253
- return await self.async_step_regenerate_assets()
254
- else:
255
- # Volver al menú inicial si el usuario cancela
256
- return await self.async_step_init()
257
-
258
- schema = vol.Schema({
259
- vol.Required("confirm", default=False): bool
260
- })
261
- return self.async_show_form(
262
- step_id="confirm_regenerate_assets",
263
- data_schema=schema,
264
- description_placeholders={
265
- "warning": "Esto regenerará los archivos faltantes de towns.json, stations.json, variables.json, symbols.json y stations_<town_id>.json. ¿Desea continuar?"
266
- }
267
- )
268
-
269
- async def async_step_regenerate_assets(self, user_input: dict | None = None):
270
- """Regenera los archivos de assets."""
271
- from . import ensure_assets_exist # importamos la función desde __init__.py
272
-
273
- errors = {}
274
- try:
275
- # Llamar a la función que garantiza que los assets existan
276
- await ensure_assets_exist(self.hass, self._config_entry.data)
277
-
278
- _LOGGER.info("Archivos de assets regenerados correctamente.")
279
- # Forzar recarga de la integración
280
- await self.hass.config_entries.async_reload(self._config_entry.entry_id)
281
-
282
- return self.async_create_entry(title="", data={})
283
-
284
- except Exception as ex:
285
- _LOGGER.error("Error al regenerar assets: %s", ex)
286
- errors["base"] = "regenerate_failed"
287
-
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from homeassistant.config_entries import ConfigEntry, OptionsFlow
5
+ from homeassistant.exceptions import HomeAssistantError
6
+ from homeassistant.helpers import config_validation as cv
7
+ from homeassistant.helpers.selector import SelectSelector, SelectSelectorConfig
8
+ import voluptuous as vol
9
+
10
+ from .const import (
11
+ CONF_API_KEY,
12
+ LIMIT_XEMA,
13
+ LIMIT_PREDICCIO,
14
+ LIMIT_XDDE,
15
+ LIMIT_QUOTA,
16
+ LIMIT_BASIC,
17
+ LATITUDE,
18
+ LONGITUDE,
19
+ ALTITUDE,
20
+ )
21
+
22
+ from meteocatpy.town import MeteocatTown
23
+ from meteocatpy.exceptions import (
24
+ BadRequestError,
25
+ ForbiddenError,
26
+ TooManyRequestsError,
27
+ InternalServerError,
28
+ UnknownAPIError,
29
+ )
30
+
31
+ _LOGGER = logging.getLogger(__name__)
32
+
33
+ class MeteocatOptionsFlowHandler(OptionsFlow):
34
+ """Manejo del flujo de opciones para Meteocat."""
35
+
36
+ def __init__(self, config_entry: ConfigEntry):
37
+ """Inicializa el flujo de opciones."""
38
+ self._config_entry = config_entry
39
+ self.api_key: str | None = None
40
+ self.limit_xema: int | None = None
41
+ self.limit_prediccio: int | None = None
42
+ self.limit_xdde: int | None = None
43
+ self.limit_quota: int | None = None
44
+ self.limit_basic: int | None = None
45
+ self.latitude: float | None = None
46
+ self.longitude: float | None = None
47
+ self.altitude: float | None = None
48
+
49
+ async def async_step_init(self, user_input: dict | None = None):
50
+ """Paso inicial del flujo de opciones."""
51
+ if user_input is not None:
52
+ if user_input["option"] == "update_api_and_limits":
53
+ return await self.async_step_update_api_and_limits()
54
+ elif user_input["option"] == "update_limits_only":
55
+ return await self.async_step_update_limits_only()
56
+ elif user_input["option"] == "regenerate_assets":
57
+ return await self.async_step_confirm_regenerate_assets()
58
+ elif user_input["option"] == "update_coordinates":
59
+ return await self.async_step_update_coordinates()
60
+
61
+ return self.async_show_form(
62
+ step_id="init",
63
+ data_schema=vol.Schema({
64
+ vol.Required("option"): SelectSelector(
65
+ SelectSelectorConfig(
66
+ options=[
67
+ "update_api_and_limits",
68
+ "update_limits_only",
69
+ "regenerate_assets",
70
+ "update_coordinates"
71
+ ],
72
+ translation_key="option"
73
+ )
74
+ )
75
+ })
76
+ )
77
+
78
+ async def async_step_update_api_and_limits(self, user_input: dict | None = None):
79
+ """Permite al usuario actualizar la API Key y los límites."""
80
+ errors = {}
81
+
82
+ if user_input is not None:
83
+ self.api_key = user_input.get(CONF_API_KEY)
84
+ self.limit_xema = user_input.get(LIMIT_XEMA)
85
+ self.limit_prediccio = user_input.get(LIMIT_PREDICCIO)
86
+ self.limit_xdde = user_input.get(LIMIT_XDDE)
87
+ self.limit_quota = user_input.get(LIMIT_QUOTA)
88
+ self.limit_basic = user_input.get(LIMIT_BASIC)
89
+
90
+ # Validar la nueva API Key utilizando MeteocatTown
91
+ if self.api_key:
92
+ town_client = MeteocatTown(self.api_key)
93
+ try:
94
+ await town_client.get_municipis() # Verificar que la API Key sea válida
95
+ except (
96
+ BadRequestError,
97
+ ForbiddenError,
98
+ TooManyRequestsError,
99
+ InternalServerError,
100
+ UnknownAPIError,
101
+ ) as ex:
102
+ _LOGGER.error("Error al validar la nueva API Key: %s", ex)
103
+ errors["base"] = "cannot_connect"
104
+ except Exception as ex:
105
+ _LOGGER.error("Error inesperado al validar la nueva API Key: %s", ex)
106
+ errors["base"] = "unknown"
107
+
108
+ # Validar que los límites sean números positivos
109
+ limits_to_validate = [self.limit_xema, self.limit_prediccio, self.limit_xdde, self.limit_quota, self.limit_basic]
110
+ if not all(cv.positive_int(limit) for limit in limits_to_validate if limit is not None):
111
+ errors["base"] = "invalid_limit"
112
+
113
+ if not errors:
114
+ # Actualizar la configuración de la entrada con la nueva API Key y límites
115
+ data_update = {}
116
+ if self.api_key:
117
+ data_update[CONF_API_KEY] = self.api_key
118
+ if self.limit_xema:
119
+ data_update[LIMIT_XEMA] = self.limit_xema
120
+ if self.limit_prediccio:
121
+ data_update[LIMIT_PREDICCIO] = self.limit_prediccio
122
+ if self.limit_xdde:
123
+ data_update[LIMIT_XDDE] = self.limit_xdde
124
+ if self.limit_quota:
125
+ data_update[LIMIT_QUOTA] = self.limit_quota
126
+ if self.limit_basic:
127
+ data_update[LIMIT_BASIC] = self.limit_basic
128
+
129
+ self.hass.config_entries.async_update_entry(
130
+ self._config_entry,
131
+ data={**self._config_entry.data, **data_update},
132
+ )
133
+ # Recargar la integración para aplicar los cambios dinámicamente
134
+ await self.hass.config_entries.async_reload(self._config_entry.entry_id)
135
+
136
+ return self.async_create_entry(title="", data={})
137
+
138
+ schema = vol.Schema({
139
+ vol.Required(CONF_API_KEY): str,
140
+ vol.Required(LIMIT_XEMA, default=self._config_entry.data.get(LIMIT_XEMA)): cv.positive_int,
141
+ vol.Required(LIMIT_PREDICCIO, default=self._config_entry.data.get(LIMIT_PREDICCIO)): cv.positive_int,
142
+ vol.Required(LIMIT_XDDE, default=self._config_entry.data.get(LIMIT_XDDE)): cv.positive_int,
143
+ vol.Required(LIMIT_QUOTA, default=self._config_entry.data.get(LIMIT_QUOTA)): cv.positive_int,
144
+ vol.Required(LIMIT_BASIC, default=self._config_entry.data.get(LIMIT_BASIC)): cv.positive_int,
145
+ })
146
+ return self.async_show_form(
147
+ step_id="update_api_and_limits", data_schema=schema, errors=errors
148
+ )
149
+
150
+ async def async_step_update_limits_only(self, user_input: dict | None = None):
151
+ """Permite al usuario actualizar solo los límites de la API."""
152
+ errors = {}
153
+
154
+ if user_input is not None:
155
+ self.limit_xema = user_input.get(LIMIT_XEMA)
156
+ self.limit_prediccio = user_input.get(LIMIT_PREDICCIO)
157
+ self.limit_xdde = user_input.get(LIMIT_XDDE)
158
+ self.limit_quota = user_input.get(LIMIT_QUOTA)
159
+ self.limit_basic = user_input.get(LIMIT_BASIC)
160
+
161
+ # Validar que los límites sean números positivos
162
+ limits_to_validate = [self.limit_xema, self.limit_prediccio, self.limit_xdde, self.limit_quota, self.limit_basic]
163
+ if not all(cv.positive_int(limit) for limit in limits_to_validate if limit is not None):
164
+ errors["base"] = "invalid_limit"
165
+
166
+ if not errors:
167
+ self.hass.config_entries.async_update_entry(
168
+ self._config_entry,
169
+ data={
170
+ **self._config_entry.data,
171
+ LIMIT_XEMA: self.limit_xema,
172
+ LIMIT_PREDICCIO: self.limit_prediccio,
173
+ LIMIT_XDDE: self.limit_xdde,
174
+ LIMIT_QUOTA: self.limit_quota,
175
+ LIMIT_BASIC: self.limit_basic
176
+ },
177
+ )
178
+ # Recargar la integración para aplicar los cambios dinámicamente
179
+ await self.hass.config_entries.async_reload(self._config_entry.entry_id)
180
+
181
+ return self.async_create_entry(title="", data={})
182
+
183
+ schema = vol.Schema({
184
+ vol.Required(LIMIT_XEMA, default=self._config_entry.data.get(LIMIT_XEMA)): cv.positive_int,
185
+ vol.Required(LIMIT_PREDICCIO, default=self._config_entry.data.get(LIMIT_PREDICCIO)): cv.positive_int,
186
+ vol.Required(LIMIT_XDDE, default=self._config_entry.data.get(LIMIT_XDDE)): cv.positive_int,
187
+ vol.Required(LIMIT_QUOTA, default=self._config_entry.data.get(LIMIT_QUOTA)): cv.positive_int,
188
+ vol.Required(LIMIT_BASIC, default=self._config_entry.data.get(LIMIT_BASIC)): cv.positive_int,
189
+ })
190
+ return self.async_show_form(
191
+ step_id="update_limits_only", data_schema=schema, errors=errors
192
+ )
193
+
194
+ async def async_step_update_coordinates(self, user_input: dict | None = None):
195
+ """Permite al usuario actualizar las coordenadas (latitude, longitude)."""
196
+ errors = {}
197
+
198
+ if user_input is not None:
199
+ self.latitude = user_input.get(LATITUDE)
200
+ self.longitude = user_input.get(LONGITUDE)
201
+ self.altitude = user_input.get(ALTITUDE)
202
+
203
+ # Validar que las coordenadas estén dentro del rango de Cataluña
204
+ if not (40.5 <= self.latitude <= 42.5 and 0.1 <= self.longitude <= 3.3):
205
+ _LOGGER.error(
206
+ "Coordenadas fuera del rango de Cataluña (latitude: %s, longitude: %s).",
207
+ self.latitude, self.longitude
208
+ )
209
+ errors["base"] = "invalid_coordinates"
210
+ # Validar que la altitud sea positiva
211
+ elif self.altitude < 0:
212
+ _LOGGER.error("Altitud inválida: %s. Debe ser >= 0.", self.altitude)
213
+ errors["base"] = "invalid_altitude"
214
+ else:
215
+ # Actualizar la configuración con las nuevas coordenadas
216
+ self.hass.config_entries.async_update_entry(
217
+ self._config_entry,
218
+ data={
219
+ **self._config_entry.data,
220
+ LATITUDE: self.latitude,
221
+ LONGITUDE: self.longitude,
222
+ ALTITUDE: self.altitude
223
+ },
224
+ )
225
+ # Recargar la integración para aplicar los cambios
226
+ await self.hass.config_entries.async_reload(self._config_entry.entry_id)
227
+ _LOGGER.info(
228
+ "Coordenadas actualizadas a latitude: %s, longitude: %s, altitude=%s.",
229
+ self.latitude, self.longitude, self.altitude
230
+ )
231
+ return self.async_create_entry(title="", data={})
232
+
233
+ schema = vol.Schema({
234
+ vol.Required(LATITUDE, default=self._config_entry.data.get(LATITUDE)): cv.latitude,
235
+ vol.Required(LONGITUDE, default=self._config_entry.data.get(LONGITUDE)): cv.longitude,
236
+ vol.Required(ALTITUDE, default=self._config_entry.data.get(ALTITUDE)): vol.Coerce(float),
237
+ })
238
+ return self.async_show_form(
239
+ step_id="update_coordinates",
240
+ data_schema=schema,
241
+ errors=errors,
242
+ description_placeholders={
243
+ "current_latitude": self._config_entry.data.get(LATITUDE),
244
+ "current_longitude": self._config_entry.data.get(LONGITUDE),
245
+ "current_altitude": self._config_entry.data.get(ALTITUDE, 0.0)
246
+ }
247
+ )
248
+
249
+ async def async_step_confirm_regenerate_assets(self, user_input: dict | None = None):
250
+ """Confirma si el usuario realmente quiere regenerar los assets."""
251
+ if user_input is not None:
252
+ if user_input.get("confirm") is True:
253
+ return await self.async_step_regenerate_assets()
254
+ else:
255
+ # Volver al menú inicial si el usuario cancela
256
+ return await self.async_step_init()
257
+
258
+ schema = vol.Schema({
259
+ vol.Required("confirm", default=False): bool
260
+ })
261
+ return self.async_show_form(
262
+ step_id="confirm_regenerate_assets",
263
+ data_schema=schema,
264
+ description_placeholders={
265
+ "warning": "Esto regenerará los archivos faltantes de towns.json, stations.json, variables.json, symbols.json y stations_<town_id>.json. ¿Desea continuar?"
266
+ }
267
+ )
268
+
269
+ async def async_step_regenerate_assets(self, user_input: dict | None = None):
270
+ """Regenera los archivos de assets."""
271
+ from . import ensure_assets_exist # importamos la función desde __init__.py
272
+
273
+ errors = {}
274
+ try:
275
+ # Llamar a la función que garantiza que los assets existan
276
+ await ensure_assets_exist(self.hass, self._config_entry.data)
277
+
278
+ _LOGGER.info("Archivos de assets regenerados correctamente.")
279
+ # Forzar recarga de la integración
280
+ await self.hass.config_entries.async_reload(self._config_entry.entry_id)
281
+
282
+ return self.async_create_entry(title="", data={})
283
+
284
+ except Exception as ex:
285
+ _LOGGER.error("Error al regenerar assets: %s", ex)
286
+ errors["base"] = "regenerate_failed"
287
+
288
288
  return self.async_show_form(step_id="regenerate_assets", errors=errors)