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