meteocat 2.2.4 → 2.2.6

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.
@@ -0,0 +1,51 @@
1
+ name: Mark stale issues and pull requests
2
+
3
+ on:
4
+ schedule:
5
+ - cron: "30 1 * * *" # Ejecutar diariamente a la 01:30 UTC
6
+ workflow_dispatch: # Permite ejecución manual desde la interfaz
7
+
8
+ jobs:
9
+ stale:
10
+ runs-on: ubuntu-latest
11
+ permissions:
12
+ issues: write
13
+ pull-requests: write
14
+
15
+ steps:
16
+ # 1. Issues/PRs generales sin actividad (excepto etiquetados)
17
+ - uses: actions/stale@v8
18
+ with:
19
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
20
+ days-before-stale: 60
21
+ days-before-close: 7
22
+ exempt-issue-labels: "help wanted,bug,feature"
23
+ exempt-pr-labels: "help wanted"
24
+ stale-issue-message: "This issue has had no activity for 60 days and will be closed in a week if there is no further activity."
25
+ stale-pr-message: "This pull request has had no activity for 60 days and will be closed in a week if there is no further activity."
26
+ stale-issue-label: "stale"
27
+ stale-pr-label: "stale"
28
+
29
+ # 2. Issues tipo "feature" sin actividad (solo etiquetar)
30
+ - uses: actions/stale@v8
31
+ with:
32
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
33
+ only-labels: "feature"
34
+ days-before-stale: 21
35
+ days-before-close: -1
36
+ stale-issue-message: >-
37
+ This feature request has had no activity for 3 weeks.
38
+ It has been marked as *help wanted* and will remain open.
39
+ stale-issue-label: "help wanted"
40
+
41
+ # 3. Issues tipo "bug" sin actividad (solo etiquetar)
42
+ - uses: actions/stale@v8
43
+ with:
44
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
45
+ only-labels: "bug"
46
+ days-before-stale: 21
47
+ days-before-close: -1
48
+ stale-issue-message: >-
49
+ This bug report has had no activity for 3 weeks.
50
+ It has been marked as *help wanted* and will remain open.
51
+ stale-issue-label: "help wanted"
package/CHANGELOG.md CHANGED
@@ -1,3 +1,23 @@
1
+ ## [2.2.6](https://github.com/figorr/meteocat/compare/v2.2.5...v2.2.6) (2025-08-27)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * 2.2.6 ([bb92000](https://github.com/figorr/meteocat/commit/bb9200099ed951168bc891c870c6fd61b758c73d))
7
+ * Fix alerts region data update at first setup of the next entrances ([40018dc](https://github.com/figorr/meteocat/commit/40018dc0bb703a8dc606aaedcfa27b5c3e6bbf7a))
8
+ * Fix delete an entry but keeping common files for the rest of entries ([211e545](https://github.com/figorr/meteocat/commit/211e54510c458378ca46c465303995d1a9df0dbb))
9
+ * Fix region alerts json file for multiple entrances ([2b7072c](https://github.com/figorr/meteocat/commit/2b7072cb6fd7eafa1ab95dcedb2c2dea8f35005d))
10
+
11
+ ## [2.2.5](https://github.com/figorr/meteocat/compare/v2.2.4...v2.2.5) (2025-02-16)
12
+
13
+
14
+ ### Bug Fixes
15
+
16
+ * 2.2.5 ([cd8fb52](https://github.com/figorr/meteocat/commit/cd8fb52c607e3a74308246136b6ac2d47b51e125))
17
+ * fix lightning status sensor native value ([dc3badc](https://github.com/figorr/meteocat/commit/dc3badc7b66d9afbcc14e23da3f0909f845cef2f))
18
+ * fix validity at lightning coordinator and cached data ([d959bc5](https://github.com/figorr/meteocat/commit/d959bc56dfb19a475b2e34a2649bbabd803ab41e))
19
+ * new lighting validity hour ([eab0215](https://github.com/figorr/meteocat/commit/eab0215cc3d68de9f900a4ecf1844ef3c6c22610))
20
+
1
21
  ## [2.2.4](https://github.com/figorr/meteocat/compare/v2.2.3...v2.2.4) (2025-02-09)
2
22
 
3
23
 
@@ -33,7 +33,7 @@ from .const import DOMAIN, PLATFORMS
33
33
  _LOGGER = logging.getLogger(__name__)
34
34
 
35
35
  # Versión
36
- __version__ = "2.2.4"
36
+ __version__ = "2.2.6"
37
37
 
38
38
  # Definir el esquema de configuración CONFIG_SCHEMA
39
39
  CONFIG_SCHEMA = vol.Schema(
@@ -192,68 +192,48 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
192
192
  """Limpia cualquier dato adicional al desinstalar la integración."""
193
193
  _LOGGER.info(f"Eliminando datos residuales de la integración: {entry.entry_id}")
194
194
 
195
- # Definir las rutas base a eliminar
195
+ # Definir las rutas base
196
196
  custom_components_path = Path(hass.config.path("custom_components")) / DOMAIN
197
197
  assets_folder = custom_components_path / "assets"
198
198
  files_folder = custom_components_path / "files"
199
199
 
200
- # Definir archivos relacionados a eliminar
200
+ # Archivos comunes
201
201
  symbols_file = assets_folder / "symbols.json"
202
202
  variables_file = assets_folder / "variables.json"
203
+ alerts_file = files_folder / "alerts.json"
204
+ quotes_file = files_folder / "quotes.json"
203
205
 
204
- # Obtener el `station_id` para identificar el archivo a eliminar
206
+ # Archivos específicos de cada entry
205
207
  station_id = entry.data.get("station_id")
206
- if not station_id:
207
- _LOGGER.warning("No se encontró 'station_id' en la configuración. No se puede eliminar el archivo de datos de la estación.")
208
- return
209
-
210
- # Archivo JSON de la estación
211
- station_data_file = files_folder / f"station_{station_id.lower()}_data.json"
212
-
213
- # Obtener el `town_id` para identificar el archivo a eliminar
214
208
  town_id = entry.data.get("town_id")
215
- if not town_id:
216
- _LOGGER.warning("No se encontró 'town_id' en la configuración. No se puede eliminar el archivo de datos de la estación.")
217
- return
218
-
219
- # Archivo JSON UVI del municipio
220
- town_data_file = files_folder / f"uvi_{town_id.lower()}_data.json"
221
-
222
- # Arhivos JSON de las predicciones del municipio a eliminar
223
- forecast_hourly_data_file = files_folder / f"forecast_{town_id.lower()}_hourly_data.json"
224
- forecast_daily_data_file = files_folder / f"forecast_{town_id.lower()}_daily_data.json"
225
-
226
- # Obtener el `region_id` para identificar el archivo a eliminar
227
209
  region_id = entry.data.get("region_id")
228
- if not region_id:
229
- _LOGGER.warning("No se encontró 'region_id' en la configuración. No se puede eliminar el archivo de alertas de la comarca.")
230
- return
231
-
232
- # Archivos JSON de alertas
233
- alerts_file = files_folder / "alerts.json"
234
- alerts_region_file = files_folder / f"alerts_{region_id}.json"
235
-
236
- # Archivo JSON de cuotas
237
- quotes_file = files_folder / f"quotes.json"
238
210
 
239
- # Archivo JSON de rayos
240
- lightning_file = files_folder / f"lightning_{region_id}.json"
241
-
242
- # Validar la ruta base
243
211
  if not custom_components_path.exists():
244
212
  _LOGGER.warning(f"La ruta {custom_components_path} no existe. No se realizará la limpieza.")
245
213
  return
246
214
 
247
- # Eliminar archivos y carpetas
215
+ # Eliminar archivos específicos de la entrada
216
+ if station_id:
217
+ safe_remove(files_folder / f"station_{station_id.lower()}_data.json")
218
+ if town_id:
219
+ safe_remove(files_folder / f"uvi_{town_id.lower()}_data.json")
220
+ safe_remove(files_folder / f"forecast_{town_id.lower()}_hourly_data.json")
221
+ safe_remove(files_folder / f"forecast_{town_id.lower()}_daily_data.json")
222
+ if region_id:
223
+ safe_remove(files_folder / f"alerts_{region_id}.json")
224
+ safe_remove(files_folder / f"lightning_{region_id}.json")
225
+
226
+ # Siempre eliminables
248
227
  safe_remove(symbols_file)
249
228
  safe_remove(variables_file)
250
- safe_remove(station_data_file)
251
- safe_remove(town_data_file)
252
- safe_remove(forecast_hourly_data_file)
253
- safe_remove(forecast_daily_data_file)
254
- safe_remove(alerts_file)
255
- safe_remove(quotes_file)
256
- safe_remove(alerts_region_file)
257
- safe_remove(lightning_file)
258
- safe_remove(assets_folder, is_folder=True)
259
- safe_remove(files_folder, is_folder=True)
229
+
230
+ # 🔑 Solo eliminar los archivos comunes si ya no quedan otras entradas
231
+ remaining_entries = [
232
+ e for e in hass.config_entries.async_entries(DOMAIN)
233
+ if e.entry_id != entry.entry_id
234
+ ]
235
+ if not remaining_entries: # significa que estamos borrando la última
236
+ safe_remove(alerts_file)
237
+ safe_remove(quotes_file)
238
+ safe_remove(assets_folder, is_folder=True)
239
+ safe_remove(files_folder, is_folder=True)
@@ -10,14 +10,13 @@ from datetime import datetime, timezone
10
10
  from zoneinfo import ZoneInfo
11
11
 
12
12
  import voluptuous as vol
13
- from aiohttp import ClientError
14
13
  import aiofiles
15
14
  import unicodedata
16
15
 
17
16
  from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
18
17
  from homeassistant.core import callback
19
18
  from homeassistant.exceptions import HomeAssistantError
20
- from homeassistant.helpers import aiohttp_client, config_validation as cv
19
+ from homeassistant.helpers import config_validation as cv
21
20
 
22
21
  from .const import (
23
22
  DOMAIN,
@@ -38,12 +37,12 @@ from .const import (
38
37
  PROVINCE_NAME,
39
38
  STATION_STATUS,
40
39
  LIMIT_XEMA,
41
- LIMIT_PREDICCIO,
40
+ LIMIT_PREDICCIO,
42
41
  LIMIT_XDDE,
43
42
  LIMIT_BASIC,
44
43
  LIMIT_QUOTA
45
44
  )
46
-
45
+
47
46
  from .options_flow import MeteocatOptionsFlowHandler
48
47
  from meteocatpy.town import MeteocatTown
49
48
  from meteocatpy.symbols import MeteocatSymbols
@@ -51,18 +50,22 @@ from meteocatpy.variables import MeteocatVariables
51
50
  from meteocatpy.townstations import MeteocatTownStations
52
51
  from meteocatpy.infostation import MeteocatInfoStation
53
52
  from meteocatpy.quotes import MeteocatQuotes
54
-
55
53
  from meteocatpy.exceptions import BadRequestError, ForbiddenError, TooManyRequestsError, InternalServerError, UnknownAPIError
56
54
 
57
55
  _LOGGER = logging.getLogger(__name__)
58
-
59
56
  TIMEZONE = ZoneInfo("Europe/Madrid")
60
57
 
61
- def normalize_name(name):
58
+ INITIAL_TEMPLATE = {
59
+ "actualitzat": {"dataUpdate": "1970-01-01T00:00:00+00:00"},
60
+ "dades": []
61
+ }
62
+
63
+ def normalize_name(name: str) -> str:
62
64
  """Normaliza el nombre eliminando acentos y convirtiendo a minúsculas."""
63
65
  name = unicodedata.normalize("NFKD", name).encode("ASCII", "ignore").decode("utf-8")
64
66
  return name.lower()
65
67
 
68
+
66
69
  class MeteocatConfigFlow(ConfigFlow, domain=DOMAIN):
67
70
  """Flujo de configuración para Meteocat."""
68
71
 
@@ -76,27 +79,25 @@ class MeteocatConfigFlow(ConfigFlow, domain=DOMAIN):
76
79
  self.station_id: str | None = None
77
80
  self.station_name: str | None = None
78
81
  self.region_id: str | None = None
79
- self._cache = {}
80
-
81
- async def fetch_and_save_quotes(self, api_key):
82
+ self.region_name: str | None = None
83
+ self.province_id: str | None = None
84
+ self.province_name: str | None = None
85
+ self.station_type: str | None = None
86
+ self.latitude: float | None = None
87
+ self.longitude: float | None = None
88
+ self.altitude: float | None = None
89
+ self.station_status: str | None = None
90
+
91
+ async def fetch_and_save_quotes(self, api_key: str):
82
92
  """Obtiene las cuotas de la API de Meteocat y las guarda en quotes.json."""
83
93
  meteocat_quotes = MeteocatQuotes(api_key)
84
- quotes_dir = os.path.join(
85
- self.hass.config.path(),
86
- "custom_components",
87
- "meteocat",
88
- "files"
89
- )
94
+ quotes_dir = os.path.join(self.hass.config.path(), "custom_components", "meteocat", "files")
90
95
  os.makedirs(quotes_dir, exist_ok=True)
91
96
  quotes_file = os.path.join(quotes_dir, "quotes.json")
92
97
 
93
98
  try:
94
- data = await asyncio.wait_for(
95
- meteocat_quotes.get_quotes(),
96
- timeout=30
97
- )
98
-
99
- # Modificar los nombres de los planes con normalización
99
+ data = await asyncio.wait_for(meteocat_quotes.get_quotes(), timeout=30)
100
+
100
101
  plan_mapping = {
101
102
  "xdde_": "XDDE",
102
103
  "prediccio_": "Prediccio",
@@ -109,7 +110,6 @@ class MeteocatConfigFlow(ConfigFlow, domain=DOMAIN):
109
110
  for plan in data["plans"]:
110
111
  normalized_nom = normalize_name(plan["nom"])
111
112
  new_name = next((v for k, v in plan_mapping.items() if normalized_nom.startswith(k)), plan["nom"])
112
-
113
113
  modified_plans.append({
114
114
  "nom": new_name,
115
115
  "periode": plan["periode"],
@@ -118,28 +118,23 @@ class MeteocatConfigFlow(ConfigFlow, domain=DOMAIN):
118
118
  "consultesRealitzades": plan["consultesRealitzades"]
119
119
  })
120
120
 
121
- # Añadir la clave 'actualitzat' con la fecha y hora actual de la zona horaria local
122
121
  current_time = datetime.now(timezone.utc).astimezone(TIMEZONE).isoformat()
123
122
  data_with_timestamp = {
124
- "actualitzat": {
125
- "dataUpdate": current_time
126
- },
123
+ "actualitzat": {"dataUpdate": current_time},
127
124
  "client": data["client"],
128
125
  "plans": modified_plans
129
126
  }
130
127
 
131
- # Guardar los datos en el archivo JSON
132
128
  async with aiofiles.open(quotes_file, "w", encoding="utf-8") as file:
133
129
  await file.write(json.dumps(data_with_timestamp, ensure_ascii=False, indent=4))
134
-
135
130
  _LOGGER.info("Cuotas guardadas exitosamente en %s", quotes_file)
136
131
 
137
132
  except Exception as ex:
138
133
  _LOGGER.error("Error al obtener o guardar las cuotas: %s", ex)
139
134
  raise HomeAssistantError("No se pudieron obtener las cuotas de la API")
140
-
135
+
141
136
  async def create_alerts_file(self):
142
- """Crea el archivo alerts.json si no existe."""
137
+ """Crea los archivos de alertas global y regional si no existen."""
143
138
  alerts_dir = os.path.join(
144
139
  self.hass.config.path(),
145
140
  "custom_components",
@@ -147,67 +142,44 @@ class MeteocatConfigFlow(ConfigFlow, domain=DOMAIN):
147
142
  "files"
148
143
  )
149
144
  os.makedirs(alerts_dir, exist_ok=True)
150
- alerts_file = os.path.join(alerts_dir, "alerts.json")
151
145
 
146
+ # Archivo global de alertas
147
+ alerts_file = os.path.join(alerts_dir, "alerts.json")
152
148
  if not os.path.exists(alerts_file):
153
- initial_data = {
154
- "actualitzat": {
155
- "dataUpdate": "1970-01-01T00:00:00+00:00"
156
- },
157
- "dades": []
158
- }
159
149
  async with aiofiles.open(alerts_file, "w", encoding="utf-8") as file:
160
- await file.write(json.dumps(initial_data, ensure_ascii=False, indent=4))
161
-
162
- _LOGGER.info("Archivo alerts.json creado en %s", alerts_file)
163
-
164
- async def create_lightning_file(self):
165
- """Crea el archivo lightning_{self.region_id}.json si no existe."""
166
- lightning_dir = os.path.join(
167
- self.hass.config.path(),
168
- "custom_components",
169
- "meteocat",
170
- "files"
171
- )
172
- os.makedirs(lightning_dir, exist_ok=True)
173
- lightning_file = os.path.join(lightning_dir, f"lightning_{self.region_id}.json")
174
-
175
- if not os.path.exists(lightning_file):
176
- initial_data = {
177
- "actualitzat": {
178
- "dataUpdate": "1970-01-01T00:00:00+00:00"
179
- },
180
- "dades": []
181
- }
182
- async with aiofiles.open(lightning_file, "w", encoding="utf-8") as file:
183
- await file.write(json.dumps(initial_data, ensure_ascii=False, indent=4))
184
-
185
- _LOGGER.info("Archivo %s creado", lightning_file)
186
-
187
- async def async_step_user(
188
- self, user_input: dict[str, Any] | None = None
189
- ) -> ConfigFlowResult:
190
- """Primer paso: Solicitar la API Key."""
150
+ await file.write(json.dumps(INITIAL_TEMPLATE, ensure_ascii=False, indent=4))
151
+ _LOGGER.info("Archivo global %s creado con plantilla inicial", alerts_file)
152
+
153
+ # Solo si existe region_id
154
+ if self.region_id:
155
+ # Archivo regional de alertas
156
+ alerts_region_file = os.path.join(alerts_dir, f"alerts_{self.region_id}.json")
157
+ if not os.path.exists(alerts_region_file):
158
+ async with aiofiles.open(alerts_region_file, "w", encoding="utf-8") as file:
159
+ await file.write(json.dumps(INITIAL_TEMPLATE, ensure_ascii=False, indent=4))
160
+ _LOGGER.info("Archivo regional %s creado con plantilla inicial", alerts_region_file)
161
+
162
+ # Archivo lightning regional
163
+ lightning_file = os.path.join(alerts_dir, f"lightning_{self.region_id}.json")
164
+ if not os.path.exists(lightning_file):
165
+ async with aiofiles.open(lightning_file, "w", encoding="utf-8") as file:
166
+ await file.write(json.dumps(INITIAL_TEMPLATE, ensure_ascii=False, indent=4))
167
+ _LOGGER.info("Archivo lightning %s creado con plantilla inicial", lightning_file)
168
+
169
+ async def async_step_user(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
170
+ """Primer paso: solicitar API Key."""
191
171
  errors = {}
192
-
193
172
  if user_input is not None:
194
173
  self.api_key = user_input[CONF_API_KEY]
195
-
196
174
  town_client = MeteocatTown(self.api_key)
197
-
198
175
  try:
199
176
  self.municipis = await town_client.get_municipis()
200
- # Aquí obtenemos y guardamos las cuotas
201
177
  await self.fetch_and_save_quotes(self.api_key)
202
- # Aquí creamos el archivo alerts.json si no existe
178
+ # Crea solo el archivo global de alertas (regional se hará después)
203
179
  await self.create_alerts_file()
204
- except (BadRequestError, ForbiddenError, TooManyRequestsError, InternalServerError, UnknownAPIError) as ex:
180
+ except Exception as ex:
205
181
  _LOGGER.error("Error al conectar con la API de Meteocat: %s", ex)
206
182
  errors["base"] = "cannot_connect"
207
- except Exception as ex:
208
- _LOGGER.error("Error inesperado al validar la API Key: %s", ex)
209
- errors["base"] = "unknown"
210
-
211
183
  if not errors:
212
184
  return await self.async_step_select_municipi()
213
185
 
@@ -215,92 +187,44 @@ class MeteocatConfigFlow(ConfigFlow, domain=DOMAIN):
215
187
  return self.async_show_form(step_id="user", data_schema=schema, errors=errors)
216
188
 
217
189
  async def async_step_select_municipi(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
218
- """Segundo paso: Seleccionar el municipio."""
190
+ """Segundo paso: seleccionar el municipio."""
219
191
  errors = {}
220
-
221
192
  if user_input is not None:
222
193
  selected_codi = user_input["municipi"]
223
- self.selected_municipi = next(
224
- (m for m in self.municipis if m["codi"] == selected_codi), None
225
- )
226
-
194
+ self.selected_municipi = next((m for m in self.municipis if m["codi"] == selected_codi), None)
227
195
  if self.selected_municipi:
228
196
  await self.fetch_symbols_and_variables()
229
197
 
230
- if not errors and self.selected_municipi:
198
+ if self.selected_municipi:
231
199
  return await self.async_step_select_station()
232
200
 
233
- schema = vol.Schema(
234
- {vol.Required("municipi"): vol.In({m["codi"]: m["nom"] for m in self.municipis})}
235
- )
236
-
201
+ schema = vol.Schema({vol.Required("municipi"): vol.In({m["codi"]: m["nom"] for m in self.municipis})})
237
202
  return self.async_show_form(step_id="select_municipi", data_schema=schema, errors=errors)
238
203
 
239
204
  async def fetch_symbols_and_variables(self):
240
205
  """Descarga y guarda los símbolos y variables después de seleccionar el municipio."""
241
-
242
- errors = {}
243
-
244
- # Crear directorio de activos (assets) si no existe
245
- assets_dir = os.path.join(
246
- self.hass.config.path(),
247
- "custom_components",
248
- "meteocat",
249
- "assets"
250
- )
206
+ assets_dir = os.path.join(self.hass.config.path(), "custom_components", "meteocat", "assets")
251
207
  os.makedirs(assets_dir, exist_ok=True)
252
-
253
- # Rutas para los archivos de símbolos y variables
254
208
  symbols_file = os.path.join(assets_dir, "symbols.json")
255
209
  variables_file = os.path.join(assets_dir, "variables.json")
256
-
257
210
  try:
258
- # Descargar y guardar los símbolos
259
- symbols_client = MeteocatSymbols(self.api_key)
260
- symbols_data = await symbols_client.fetch_symbols()
261
-
211
+ symbols_data = await MeteocatSymbols(self.api_key).fetch_symbols()
262
212
  async with aiofiles.open(symbols_file, "w", encoding="utf-8") as file:
263
213
  await file.write(json.dumps({"symbols": symbols_data}, ensure_ascii=False, indent=4))
264
-
265
- _LOGGER.info(f"Símbolos guardados en {symbols_file}")
266
-
267
- # Descargar y guardar las variables
268
- variables_client = MeteocatVariables(self.api_key)
269
- variables_data = await variables_client.get_variables()
270
-
214
+ variables_data = await MeteocatVariables(self.api_key).get_variables()
271
215
  async with aiofiles.open(variables_file, "w", encoding="utf-8") as file:
272
216
  await file.write(json.dumps({"variables": variables_data}, ensure_ascii=False, indent=4))
273
-
274
- _LOGGER.info(f"Variables guardadas en {variables_file}")
275
-
276
- # Buscar la variable de temperatura
277
- self.variable_id = next(
278
- (v["codi"] for v in variables_data if v["nom"].lower() == "temperatura"), None
279
- )
280
- if not self.variable_id:
281
- _LOGGER.error("No se encontró la variable 'Temperatura'")
282
- errors["base"] = "variable_not_found"
283
- except (BadRequestError, ForbiddenError, TooManyRequestsError, InternalServerError, UnknownAPIError) as ex:
284
- _LOGGER.error("Error al conectar con la API de Meteocat: %s", ex)
285
- errors["base"] = "cannot_connect"
217
+ self.variable_id = next((v["codi"] for v in variables_data if v["nom"].lower() == "temperatura"), None)
286
218
  except Exception as ex:
287
- _LOGGER.error("Error inesperado al descargar los datos: %s", ex)
288
- errors["base"] = "unknown"
219
+ _LOGGER.error("Error al descargar símbolos o variables: %s", ex)
220
+ raise HomeAssistantError("No se pudieron obtener símbolos o variables")
289
221
 
290
- if errors:
291
- raise HomeAssistantError(errors)
292
-
293
- async def async_step_select_station(
294
- self, user_input: dict[str, Any] | None = None
295
- ) -> ConfigFlowResult:
296
- """Tercer paso: Seleccionar la estación para la variable seleccionada."""
222
+ async def async_step_select_station(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
223
+ """Tercer paso: seleccionar estación."""
297
224
  errors = {}
298
-
299
225
  townstations_client = MeteocatTownStations(self.api_key)
300
226
  try:
301
- stations_data = await townstations_client.get_town_stations(
302
- self.selected_municipi["codi"], self.variable_id
303
- )
227
+ stations_data = await townstations_client.get_town_stations(self.selected_municipi["codi"], self.variable_id)
304
228
  except Exception as ex:
305
229
  _LOGGER.error("Error al obtener las estaciones: %s", ex)
306
230
  errors["base"] = "stations_fetch_failed"
@@ -309,19 +233,15 @@ class MeteocatConfigFlow(ConfigFlow, domain=DOMAIN):
309
233
  if user_input is not None:
310
234
  selected_station_codi = user_input["station"]
311
235
  selected_station = next(
312
- (station for station in stations_data[0]["variables"][0]["estacions"] if station["codi"] == selected_station_codi),
313
- None
236
+ (station for station in stations_data[0]["variables"][0]["estacions"] if station["codi"] == selected_station_codi), None
314
237
  )
315
-
316
238
  if selected_station:
317
239
  self.station_id = selected_station["codi"]
318
240
  self.station_name = selected_station["nom"]
319
241
 
320
242
  # Obtener metadatos de la estación
321
- infostation_client = MeteocatInfoStation(self.api_key)
322
243
  try:
323
- station_metadata = await infostation_client.get_infostation(self.station_id)
324
- # Extraer los valores necesarios de los metadatos
244
+ station_metadata = await MeteocatInfoStation(self.api_key).get_infostation(self.station_id)
325
245
  self.station_type = station_metadata.get("tipus", "")
326
246
  self.latitude = station_metadata.get("coordenades", {}).get("latitud", 0.0)
327
247
  self.longitude = station_metadata.get("coordenades", {}).get("longitud", 0.0)
@@ -332,9 +252,9 @@ class MeteocatConfigFlow(ConfigFlow, domain=DOMAIN):
332
252
  self.province_name = station_metadata.get("provincia", {}).get("nom", "")
333
253
  self.station_status = station_metadata.get("estats", [{}])[0].get("codi", "")
334
254
 
335
- # Crear el archivo lightning después de obtener region_id
336
- await self.create_lightning_file()
337
-
255
+ # Crear archivos regionales de alertas y lightning
256
+ await self.create_alerts_file()
257
+
338
258
  return await self.async_step_set_api_limits()
339
259
  except Exception as ex:
340
260
  _LOGGER.error("Error al obtener los metadatos de la estación: %s", ex)
@@ -342,22 +262,12 @@ class MeteocatConfigFlow(ConfigFlow, domain=DOMAIN):
342
262
  else:
343
263
  errors["base"] = "station_not_found"
344
264
 
345
- schema = vol.Schema(
346
- {
347
- vol.Required("station"): vol.In(
348
- {station["codi"]: station["nom"] for station in stations_data[0]["variables"][0]["estacions"]}
349
- )
350
- }
351
- )
265
+ schema = vol.Schema({vol.Required("station"): vol.In({station["codi"]: station["nom"] for station in stations_data[0]["variables"][0]["estacions"]})})
266
+ return self.async_show_form(step_id="select_station", data_schema=schema, errors=errors)
352
267
 
353
- return self.async_show_form(
354
- step_id="select_station", data_schema=schema, errors=errors
355
- )
356
-
357
268
  async def async_step_set_api_limits(self, user_input=None):
358
- """Cuarto paso: Introducir los límites de XEMA y PREDICCIO del plan de la API."""
269
+ """Cuarto paso: límites de la API."""
359
270
  errors = {}
360
-
361
271
  if user_input is not None:
362
272
  self.limit_xema = user_input.get(LIMIT_XEMA, 750)
363
273
  self.limit_prediccio = user_input.get(LIMIT_PREDICCIO, 100)
@@ -388,22 +298,17 @@ class MeteocatConfigFlow(ConfigFlow, domain=DOMAIN):
388
298
  LIMIT_XDDE: self.limit_xdde,
389
299
  LIMIT_QUOTA: self.limit_quota,
390
300
  LIMIT_BASIC: self.limit_basic,
391
- },
301
+ }
392
302
  )
393
303
 
394
- schema = vol.Schema(
395
- {
396
- vol.Required(LIMIT_XEMA, default=750): cv.positive_int,
397
- vol.Required(LIMIT_PREDICCIO, default=100): cv.positive_int,
398
- vol.Required(LIMIT_XDDE, default=250): cv.positive_int,
399
- vol.Required(LIMIT_QUOTA, default=300): cv.positive_int,
400
- vol.Required(LIMIT_BASIC, default=2000): cv.positive_int,
401
- }
402
- )
403
-
404
- return self.async_show_form(
405
- step_id="set_api_limits", data_schema=schema, errors=errors
406
- )
304
+ schema = vol.Schema({
305
+ vol.Required(LIMIT_XEMA, default=750): cv.positive_int,
306
+ vol.Required(LIMIT_PREDICCIO, default=100): cv.positive_int,
307
+ vol.Required(LIMIT_XDDE, default=250): cv.positive_int,
308
+ vol.Required(LIMIT_QUOTA, default=300): cv.positive_int,
309
+ vol.Required(LIMIT_BASIC, default=2000): cv.positive_int,
310
+ })
311
+ return self.async_show_form(step_id="set_api_limits", data_schema=schema, errors=errors)
407
312
 
408
313
  @staticmethod
409
314
  @callback
@@ -56,7 +56,7 @@ DEFAULT_VALIDITY_MINUTES = 0 # Minutos a partir de los cuales la API tiene la i
56
56
  DEFAULT_ALERT_VALIDITY_TIME = 120 # Minutos a partir de los cuales las alertas están obsoletas y se se debe proceder a una nueva llamada a la API
57
57
  DEFAULT_QUOTES_VALIDITY_TIME = 240 # Minutos a partir de los cuales los datos de cuotas están obsoletos y se se debe proceder a una nueva llamada a la API
58
58
  DEFAULT_LIGHTNING_VALIDITY_TIME = 240 # Minutos a partir de los cuales los datos de rayos están obsoletos y se se debe proceder a una nueva llamada a la API
59
- DEFAULT_LIGHTNING_VALIDITY_HOURS = 2 # Hora a partir de la cual la API tiene la información actualizada de rayos disponible para descarga
59
+ DEFAULT_LIGHTNING_VALIDITY_HOURS = 1 # Hora a partir de la cual la API tiene la información actualizada de rayos disponible para descarga
60
60
  DEFAULT_LIGHTNING_VALIDITY_MINUTES = 0 # Minutos a partir de los cuales la API tiene la información actualizada de rayos disponible para descarga
61
61
 
62
62
  # Multiplicadores para la duración de validez basada en limit_prediccio
@@ -1268,6 +1268,18 @@ class MeteocatAlertsCoordinator(DataUpdateCoordinator):
1268
1268
  if now - last_update > validity_duration:
1269
1269
  return await self._fetch_and_save_new_data()
1270
1270
  else:
1271
+ # Comprobar si el archivo regional sigue con INITIAL_TEMPLATE o sin datos válidos
1272
+ region_data = await load_json_from_file(self.alerts_region_file)
1273
+ if (
1274
+ not region_data
1275
+ or region_data.get("actualitzat", {}).get("dataUpdate") in [None, "1970-01-01T00:00:00+00:00"]
1276
+ ):
1277
+ _LOGGER.info(
1278
+ "El archivo regional %s sigue con plantilla inicial. Regenerando a partir de alerts.json",
1279
+ self.alerts_region_file,
1280
+ )
1281
+ await self._filter_alerts_by_region()
1282
+
1271
1283
  # Devolver los datos del archivo existente
1272
1284
  _LOGGER.debug("Usando datos existentes de alertas: %s", existing_data)
1273
1285
  return {
@@ -1334,7 +1346,7 @@ class MeteocatAlertsCoordinator(DataUpdateCoordinator):
1334
1346
  "Usando datos en caché para las alertas. Última actualización: %s",
1335
1347
  cached_data["actualitzat"]["dataUpdate"],
1336
1348
  )
1337
- return cached_data
1349
+ return {"actualizado": cached_data['actualitzat']['dataUpdate']}
1338
1350
 
1339
1351
  # Si no se puede actualizar ni cargar datos en caché, retornar None
1340
1352
  _LOGGER.error("No se pudo obtener datos actualizados ni cargar datos en caché de alertas.")
@@ -1783,7 +1795,7 @@ class MeteocatQuotesCoordinator(DataUpdateCoordinator):
1783
1795
  cached_data = await load_json_from_file(self.quotes_file)
1784
1796
  if cached_data:
1785
1797
  _LOGGER.warning("Usando datos en caché para las cuotas de la API de Meteocat.")
1786
- return cached_data
1798
+ return {"actualizado": cached_data['actualitzat']['dataUpdate']}
1787
1799
 
1788
1800
  _LOGGER.error("No se pudo obtener datos actualizados ni cargar datos en caché.")
1789
1801
  return None
@@ -1910,15 +1922,19 @@ class MeteocatLightningCoordinator(DataUpdateCoordinator):
1910
1922
  existing_data = await load_json_from_file(self.lightning_file) or {}
1911
1923
 
1912
1924
  # Definir la duración de validez de los datos
1913
- current_time = datetime.now(timezone.utc).time()
1914
- validity_start_time = time(DEFAULT_LIGHTNING_VALIDITY_HOURS, DEFAULT_LIGHTNING_VALIDITY_MINUTES)
1925
+ now = datetime.now(timezone.utc).astimezone(TIMEZONE)
1926
+ current_time = now.time() # Extraer solo la parte de la hora
1927
+ offset = now.utcoffset().total_seconds() / 3600 # Obtener el offset en horas
1928
+
1929
+ # Determinar la hora de validez considerando el offset horario, el horario de verano (+02:00) o invierno (+01:00)
1930
+ validity_start_time = time(int(DEFAULT_LIGHTNING_VALIDITY_HOURS + offset), DEFAULT_LIGHTNING_VALIDITY_MINUTES)
1931
+
1915
1932
  validity_duration = timedelta(minutes=DEFAULT_LIGHTNING_VALIDITY_TIME)
1916
1933
 
1917
1934
  if not existing_data:
1918
1935
  return await self._fetch_and_save_new_data()
1919
1936
  else:
1920
1937
  last_update = datetime.fromisoformat(existing_data['actualitzat']['dataUpdate'])
1921
- now = datetime.now(timezone.utc).astimezone(TIMEZONE)
1922
1938
 
1923
1939
  if now - last_update >= validity_duration and current_time >= validity_start_time:
1924
1940
  return await self._fetch_and_save_new_data()
@@ -1967,7 +1983,7 @@ class MeteocatLightningCoordinator(DataUpdateCoordinator):
1967
1983
  cached_data = await load_json_from_file(self.lightning_file)
1968
1984
  if cached_data:
1969
1985
  _LOGGER.warning("Usando datos en caché para los datos de rayos de la API de Meteocat.")
1970
- return cached_data
1986
+ return {"actualizado": cached_data['actualitzat']['dataUpdate']}
1971
1987
 
1972
1988
  _LOGGER.error("No se pudo obtener datos actualizados ni cargar datos en caché.")
1973
1989
  return None
@@ -2012,20 +2028,20 @@ class MeteocatLightningFileCoordinator(DataUpdateCoordinator):
2012
2028
  if not existing_data:
2013
2029
  _LOGGER.warning("No se encontraron datos en %s.", self.lightning_file)
2014
2030
  return {
2015
- "actualizado": datetime.now(ZoneInfo("Europe/Madrid")).isoformat(),
2031
+ "actualizado": datetime.now(TIMEZONE).isoformat(),
2016
2032
  "region": self._reset_data(),
2017
2033
  "town": self._reset_data()
2018
2034
  }
2019
2035
 
2020
2036
  # Convertir la cadena de fecha a un objeto datetime y ajustar a la zona horaria local
2021
2037
  update_date = datetime.fromisoformat(existing_data.get("actualitzat", {}).get("dataUpdate", ""))
2022
- update_date = update_date.astimezone(ZoneInfo("Europe/Madrid"))
2023
- now = datetime.now(ZoneInfo("Europe/Madrid"))
2038
+ update_date = update_date.astimezone(TIMEZONE)
2039
+ now = datetime.now(TIMEZONE)
2024
2040
 
2025
2041
  if update_date.date() != now.date(): # Si la fecha no es la de hoy
2026
2042
  _LOGGER.info("Los datos de rayos son de un día diferente. Reiniciando valores a cero.")
2027
2043
  region_data = town_data = self._reset_data()
2028
- update_date = datetime.now(ZoneInfo("Europe/Madrid")).isoformat() # Usar la fecha actual
2044
+ update_date = datetime.now(TIMEZONE).isoformat() # Usar la fecha actual
2029
2045
  else:
2030
2046
  region_data = self._process_region_data(existing_data.get("dades", []))
2031
2047
  town_data = self._process_town_data(existing_data.get("dades", []))
@@ -9,5 +9,5 @@
9
9
  "issue_tracker": "https://github.com/figorr/meteocat/issues",
10
10
  "loggers": ["meteocatpy"],
11
11
  "requirements": ["meteocatpy==1.0.1", "packaging>=20.3", "wrapt>=1.14.0"],
12
- "version": "2.2.4"
12
+ "version": "2.2.6"
13
13
  }
@@ -103,6 +103,8 @@ from .const import (
103
103
  ALERT_VALIDITY_MULTIPLIER_500,
104
104
  ALERT_VALIDITY_MULTIPLIER_DEFAULT,
105
105
  DEFAULT_LIGHTNING_VALIDITY_TIME,
106
+ DEFAULT_LIGHTNING_VALIDITY_HOURS,
107
+ DEFAULT_LIGHTNING_VALIDITY_MINUTES,
106
108
  )
107
109
 
108
110
  from .coordinator import (
@@ -1687,29 +1689,40 @@ class MeteocatLightningStatusSensor(CoordinatorEntity[MeteocatLightningCoordinat
1687
1689
  self._attr_entity_category = getattr(description, "entity_category", None)
1688
1690
 
1689
1691
  def _get_data_update(self):
1690
- """Obtiene la fecha de actualización directamente desde el coordinador."""
1692
+ """Obtiene la fecha de actualización directamente desde el coordinador y la convierte a UTC."""
1691
1693
  data_update = self.coordinator.data.get("actualizado")
1692
1694
  if data_update:
1693
1695
  try:
1694
- return datetime.fromisoformat(data_update.rstrip("Z"))
1696
+ local_time = datetime.fromisoformat(data_update) # Ya tiene offset (+01:00 o +02:00)
1697
+ return local_time.astimezone(ZoneInfo("UTC")) # Convertir a UTC
1695
1698
  except ValueError:
1696
1699
  _LOGGER.error("Formato de fecha de actualización inválido: %s", data_update)
1697
1700
  return None
1701
+
1702
+ def _determine_status(self, now, data_update, current_time, validity_start_time, validity_duration):
1703
+ """Determina el estado basado en la fecha de actualización."""
1704
+ if now - data_update > timedelta(days=1):
1705
+ return "obsolete"
1706
+ elif now - data_update < validity_duration or current_time < validity_start_time:
1707
+ return "updated"
1708
+ return "obsolete"
1698
1709
 
1699
1710
  @property
1700
1711
  def native_value(self):
1701
- """Devuelve el estado actual de las alertas basado en la fecha de actualización."""
1712
+ """Devuelve el estado del archivo de rayos basado en la fecha de actualización."""
1702
1713
  data_update = self._get_data_update()
1703
1714
  if not data_update:
1704
1715
  return "unknown"
1705
1716
 
1706
- current_time = datetime.now(ZoneInfo("UTC"))
1717
+ now = datetime.now(timezone.utc).astimezone(TIMEZONE)
1718
+ current_time = now.time() # Extraer solo la parte de la hora
1719
+ offset = now.utcoffset().total_seconds() / 3600 # Obtener el offset en horas
1707
1720
 
1708
- # Comprobar si el archivo de alertas está obsoleto
1709
- if current_time - data_update >= timedelta(minutes=DEFAULT_LIGHTNING_VALIDITY_TIME):
1710
- return "obsolete"
1721
+ # Determinar la hora de validez considerando el offset horario, el horario de verano (+02:00) o invierno (+01:00)
1722
+ validity_start_time = time(int(DEFAULT_LIGHTNING_VALIDITY_HOURS + offset), DEFAULT_LIGHTNING_VALIDITY_MINUTES)
1723
+ validity_duration = timedelta(minutes=DEFAULT_LIGHTNING_VALIDITY_TIME)
1711
1724
 
1712
- return "updated"
1725
+ return self._determine_status(now, data_update, current_time, validity_start_time, validity_duration)
1713
1726
 
1714
1727
  @property
1715
1728
  def extra_state_attributes(self):
@@ -1,2 +1,2 @@
1
1
  # version.py
2
- __version__ = "2.2.4"
2
+ __version__ = "2.2.6"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "meteocat",
3
- "version": "2.2.4",
3
+ "version": "2.2.6",
4
4
  "description": "[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)\r [![Python version compatibility](https://img.shields.io/pypi/pyversions/meteocat)](https://pypi.org/project/meteocat)\r [![pipeline status](https://gitlab.com/figorr/meteocat/badges/master/pipeline.svg)](https://gitlab.com/figorr/meteocat/commits/master)",
5
5
  "main": "index.js",
6
6
  "directories": {
package/pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "meteocat"
3
- version = "2.2.4"
3
+ version = "2.2.6"
4
4
  description = "Script para obtener datos meteorológicos de la API de Meteocat"
5
5
  authors = ["figorr <jdcuartero@yahoo.es>"]
6
6
  license = "Apache-2.0"