meteocat 2.2.5 → 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.
- package/.github/workflows/stale.yml +51 -0
- package/CHANGELOG.md +10 -0
- package/custom_components/meteocat/__init__.py +29 -49
- package/custom_components/meteocat/config_flow.py +81 -176
- package/custom_components/meteocat/coordinator.py +12 -0
- package/custom_components/meteocat/manifest.json +1 -1
- package/custom_components/meteocat/version.py +1 -1
- package/package.json +1 -1
- package/pyproject.toml +1 -1
|
@@ -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,13 @@
|
|
|
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
|
+
|
|
1
11
|
## [2.2.5](https://github.com/figorr/meteocat/compare/v2.2.4...v2.2.5) (2025-02-16)
|
|
2
12
|
|
|
3
13
|
|
|
@@ -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.
|
|
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
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
|
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
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
"
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
#
|
|
178
|
+
# Crea solo el archivo global de alertas (regional se hará después)
|
|
203
179
|
await self.create_alerts_file()
|
|
204
|
-
except
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
|
288
|
-
|
|
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
|
-
|
|
291
|
-
|
|
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
|
|
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
|
|
336
|
-
await self.
|
|
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:
|
|
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
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
|
@@ -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 {
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
# version.py
|
|
2
|
-
__version__ = "2.2.
|
|
2
|
+
__version__ = "2.2.6"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "meteocat",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.6",
|
|
4
4
|
"description": "[](https://opensource.org/licenses/Apache-2.0)\r [](https://pypi.org/project/meteocat)\r [](https://gitlab.com/figorr/meteocat/commits/master)",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"directories": {
|