meteocat 4.0.0 → 4.0.2

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 (49) 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 +971 -954
  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/coordinator.py +248 -68
  27. package/custom_components/meteocat/helpers.py +58 -58
  28. package/custom_components/meteocat/manifest.json +25 -25
  29. package/custom_components/meteocat/options_flow.py +287 -287
  30. package/custom_components/meteocat/sensor.py +4 -2
  31. package/custom_components/meteocat/strings.json +1060 -1058
  32. package/custom_components/meteocat/translations/ca.json +1060 -1058
  33. package/custom_components/meteocat/translations/en.json +1060 -1058
  34. package/custom_components/meteocat/translations/es.json +1060 -1058
  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 +80 -79
  39. package/hacs.json +8 -8
  40. package/info.md +11 -11
  41. package/package.json +22 -22
  42. package/poetry.lock +3222 -3222
  43. package/pyproject.toml +68 -68
  44. package/requirements.test.txt +3 -3
  45. package/setup.cfg +64 -64
  46. package/setup.py +10 -10
  47. package/tests/bandit.yaml +17 -17
  48. package/tests/conftest.py +19 -19
  49. 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)
@@ -1412,7 +1412,7 @@ class MeteocatAlertRegionSensor(CoordinatorEntity[MeteocatAlertsRegionCoordinato
1412
1412
  "Temps violent": "violent_weather",
1413
1413
  "Intensitat de pluja": "rain_intensity",
1414
1414
  "Acumulació de pluja": "rain_amount",
1415
- "Neu acumulada en 24 hores": "snow_amount_24",
1415
+ "Neu": "snow",
1416
1416
  "Vent": "wind",
1417
1417
  "Estat de la mar": "sea_state",
1418
1418
  "Fred": "cold",
@@ -1477,7 +1477,7 @@ class MeteocatAlertMeteorSensor(CoordinatorEntity[MeteocatAlertsRegionCoordinato
1477
1477
  ALERT_COLD: "Fred",
1478
1478
  ALERT_WARM: "Calor",
1479
1479
  ALERT_WARM_NIGHT: "Calor nocturna",
1480
- ALERT_SNOW: "Neu acumulada en 24 hores",
1480
+ ALERT_SNOW: "Neu",
1481
1481
  }
1482
1482
  STATE_MAPPING = {
1483
1483
  "Obert": "opened",
@@ -1515,6 +1515,8 @@ class MeteocatAlertMeteorSensor(CoordinatorEntity[MeteocatAlertsRegionCoordinato
1515
1515
  "gruix > 5 cm a cotes superiors a 600 metres fins a 800 metres": "thickness_5_at_600",
1516
1516
  "gruix > 2 cm a cotes superiors a 300 metres fins a 600 metres": "thickness_2_at_300",
1517
1517
  "gruix ≥ 0 cm a cotes inferiors a 300 metres": "thickness_0_at_300",
1518
+ "gruix >= 0 cm a cotes > 200 metres": "thickness_0_at_200",
1519
+ "gruix > 2 cm a cotes > 400 metres": "thickness_2_at_400",
1518
1520
  }
1519
1521
  _attr_has_entity_name = True
1520
1522