meteocat 2.3.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +8 -2
  2. package/.github/ISSUE_TEMPLATE/config.yml +7 -0
  3. package/.github/ISSUE_TEMPLATE/improvement.md +39 -0
  4. package/.github/ISSUE_TEMPLATE/new_function.md +41 -0
  5. package/.github/labels.yml +63 -0
  6. package/.github/workflows/autocloser.yaml +11 -9
  7. package/.github/workflows/close-on-label.yml +48 -0
  8. package/.github/workflows/force-sync-labels.yml +18 -0
  9. package/.github/workflows/release.yml +1 -31
  10. package/.github/workflows/sync-gitlab.yml +17 -4
  11. package/.github/workflows/sync-labels.yml +21 -0
  12. package/.releaserc +21 -0
  13. package/AUTHORS.md +1 -0
  14. package/CHANGELOG.md +58 -1
  15. package/README.md +81 -6
  16. package/custom_components/meteocat/__init__.py +51 -41
  17. package/custom_components/meteocat/config_flow.py +52 -3
  18. package/custom_components/meteocat/const.py +3 -0
  19. package/custom_components/meteocat/coordinator.py +188 -2
  20. package/custom_components/meteocat/manifest.json +1 -1
  21. package/custom_components/meteocat/options_flow.py +61 -3
  22. package/custom_components/meteocat/sensor.py +305 -254
  23. package/custom_components/meteocat/strings.json +61 -15
  24. package/custom_components/meteocat/translations/ca.json +67 -22
  25. package/custom_components/meteocat/translations/en.json +61 -15
  26. package/custom_components/meteocat/translations/es.json +61 -15
  27. package/custom_components/meteocat/version.py +1 -1
  28. package/custom_components/meteocat/weather.py +1 -1
  29. package/hacs.json +1 -1
  30. package/images/daily_forecast_2_alerts.png +0 -0
  31. package/images/regenerate_assets.png +0 -0
  32. package/images/setup_options.png +0 -0
  33. package/images/system_options.png +0 -0
  34. package/package.json +1 -1
  35. package/pyproject.toml +1 -1
  36. package/scripts/update_version.sh +22 -0
  37. package/.github/workflows/close-duplicates.yml +0 -57
@@ -5,6 +5,7 @@ import voluptuous as vol
5
5
  from pathlib import Path
6
6
  import aiofiles
7
7
  import json
8
+ import importlib # Para importaciones lazy
8
9
 
9
10
  from homeassistant import core
10
11
  from homeassistant.config_entries import ConfigEntry
@@ -14,24 +15,6 @@ from homeassistant.helpers.entity_platform import async_get_platforms
14
15
  from homeassistant.helpers import config_validation as cv
15
16
 
16
17
  from .helpers import get_storage_dir
17
- from .coordinator import (
18
- MeteocatSensorCoordinator,
19
- MeteocatStaticSensorCoordinator,
20
- MeteocatEntityCoordinator,
21
- MeteocatUviCoordinator,
22
- MeteocatUviFileCoordinator,
23
- HourlyForecastCoordinator,
24
- DailyForecastCoordinator,
25
- MeteocatConditionCoordinator,
26
- MeteocatTempForecastCoordinator,
27
- MeteocatAlertsCoordinator,
28
- MeteocatAlertsRegionCoordinator,
29
- MeteocatQuotesCoordinator,
30
- MeteocatQuotesFileCoordinator,
31
- MeteocatLightningCoordinator,
32
- MeteocatLightningFileCoordinator,
33
- )
34
-
35
18
  from meteocatpy.town import MeteocatTown
36
19
  from meteocatpy.symbols import MeteocatSymbols
37
20
  from meteocatpy.variables import MeteocatVariables
@@ -41,7 +24,7 @@ from .const import DOMAIN, PLATFORMS
41
24
  _LOGGER = logging.getLogger(__name__)
42
25
 
43
26
  # Versión
44
- __version__ = ""
27
+ __version__ = "3.1.0"
45
28
 
46
29
  # Definir el esquema de configuración CONFIG_SCHEMA
47
30
  CONFIG_SCHEMA = vol.Schema(
@@ -59,6 +42,8 @@ CONFIG_SCHEMA = vol.Schema(
59
42
  vol.Optional("province_id"): cv.string,
60
43
  vol.Optional("region_name"): cv.string,
61
44
  vol.Optional("region_id"): cv.string,
45
+ vol.Required("latitude"): cv.latitude,
46
+ vol.Required("longitude"): cv.longitude,
62
47
  }
63
48
  )
64
49
  },
@@ -119,6 +104,11 @@ async def async_setup(hass: core.HomeAssistant, config: dict) -> bool:
119
104
  """Configuración inicial del componente Meteocat."""
120
105
  return True
121
106
 
107
+ def _get_coordinator_module(cls_name: str):
108
+ """Importa dinámicamente un coordinador para evitar blocking imports."""
109
+ module = importlib.import_module(".coordinator", "custom_components.meteocat")
110
+ return getattr(module, cls_name)
111
+
122
112
  async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
123
113
  """Configura una entrada de configuración para Meteocat."""
124
114
  _LOGGER.info("Configurando la integración de Meteocat...")
@@ -129,14 +119,28 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
129
119
  # Validar campos requeridos
130
120
  required_fields = [
131
121
  "api_key", "town_name", "town_id", "variable_name",
132
- "variable_id", "station_name", "station_id", "province_name",
133
- "province_id", "region_name", "region_id"
122
+ "variable_id", "station_name", "station_id", "province_name",
123
+ "province_id", "region_name", "region_id", "latitude", "longitude"
134
124
  ]
135
125
  missing_fields = [field for field in required_fields if field not in entry_data]
136
126
  if missing_fields:
137
127
  _LOGGER.error(f"Faltan los siguientes campos en la configuración: {missing_fields}")
138
128
  return False
139
-
129
+
130
+ # Validar coordenadas válidas para Cataluña
131
+ latitude = entry_data.get("latitude")
132
+ longitude = entry_data.get("longitude")
133
+ if not (40.5 <= latitude <= 42.5 and 0.1 <= longitude <= 3.3): # Rango aproximado para Cataluña
134
+ _LOGGER.warning(
135
+ "Coordenadas inválidas (latitude: %s, longitude: %s). Usando coordenadas de Barcelona por defecto para MeteocatSunCoordinator.",
136
+ latitude, longitude
137
+ )
138
+ entry_data = {
139
+ **entry_data,
140
+ "latitude": 41.38879,
141
+ "longitude": 2.15899
142
+ }
143
+
140
144
  # Crear los assets básicos si faltan
141
145
  await ensure_assets_exist(
142
146
  hass,
@@ -150,33 +154,38 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
150
154
  f"Variable '{entry_data['variable_name']}' (ID: {entry_data['variable_id']}), "
151
155
  f"Estación '{entry_data['station_name']}' (ID: {entry_data['station_id']}), "
152
156
  f"Provincia '{entry_data['province_name']}' (ID: {entry_data['province_id']}), "
153
- f"Comarca '{entry_data['region_name']}' (ID: {entry_data['region_id']})."
157
+ f"Comarca '{entry_data['region_name']}' (ID: {entry_data['region_id']}), "
158
+ f"Coordenadas: ({entry_data['latitude']}, {entry_data['longitude']})."
154
159
  )
155
160
 
156
- # Inicializar coordinadores
157
- coordinators = [
158
- ("sensor_coordinator", MeteocatSensorCoordinator),
159
- ("static_sensor_coordinator", MeteocatStaticSensorCoordinator),
160
- ("entity_coordinator", MeteocatEntityCoordinator),
161
- ("uvi_coordinator", MeteocatUviCoordinator),
162
- ("uvi_file_coordinator", MeteocatUviFileCoordinator),
163
- ("hourly_forecast_coordinator", HourlyForecastCoordinator),
164
- ("daily_forecast_coordinator", DailyForecastCoordinator),
165
- ("condition_coordinator", MeteocatConditionCoordinator),
166
- ("temp_forecast_coordinator", MeteocatTempForecastCoordinator),
167
- ("alerts_coordinator", MeteocatAlertsCoordinator),
168
- ("alerts_region_coordinator", MeteocatAlertsRegionCoordinator),
169
- ("quotes_coordinator", MeteocatQuotesCoordinator),
170
- ("quotes_file_coordinator", MeteocatQuotesFileCoordinator),
171
- ("lightning_coordinator", MeteocatLightningCoordinator),
172
- ("lightning_file_coordinator", MeteocatLightningFileCoordinator),
161
+ # Lista de coordinadores con sus clases
162
+ coordinator_configs = [
163
+ ("sensor_coordinator", "MeteocatSensorCoordinator"),
164
+ ("static_sensor_coordinator", "MeteocatStaticSensorCoordinator"),
165
+ ("entity_coordinator", "MeteocatEntityCoordinator"),
166
+ ("uvi_coordinator", "MeteocatUviCoordinator"),
167
+ ("uvi_file_coordinator", "MeteocatUviFileCoordinator"),
168
+ ("hourly_forecast_coordinator", "HourlyForecastCoordinator"),
169
+ ("daily_forecast_coordinator", "DailyForecastCoordinator"),
170
+ ("condition_coordinator", "MeteocatConditionCoordinator"),
171
+ ("temp_forecast_coordinator", "MeteocatTempForecastCoordinator"),
172
+ ("alerts_coordinator", "MeteocatAlertsCoordinator"),
173
+ ("alerts_region_coordinator", "MeteocatAlertsRegionCoordinator"),
174
+ ("quotes_coordinator", "MeteocatQuotesCoordinator"),
175
+ ("quotes_file_coordinator", "MeteocatQuotesFileCoordinator"),
176
+ ("lightning_coordinator", "MeteocatLightningCoordinator"),
177
+ ("lightning_file_coordinator", "MeteocatLightningFileCoordinator"),
178
+ ("sun_coordinator", "MeteocatSunCoordinator"),
179
+ ("sun_file_coordinator", "MeteocatSunFileCoordinator"),
173
180
  ]
174
181
 
175
182
  hass.data.setdefault(DOMAIN, {})
176
183
  hass.data[DOMAIN][entry.entry_id] = {}
177
184
 
178
185
  try:
179
- for key, cls in coordinators:
186
+ for key, cls_name in coordinator_configs:
187
+ # Importación lazy: importa la clase solo cuando sea necesario
188
+ cls = await hass.async_add_executor_job(_get_coordinator_module, cls_name)
180
189
  coordinator = cls(hass=hass, entry_data=entry_data)
181
190
  await coordinator.async_config_entry_first_refresh()
182
191
  hass.data[DOMAIN][entry.entry_id][key] = coordinator
@@ -251,6 +260,7 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
251
260
  files_folder / f"uvi_{town_id.lower()}_data.json",
252
261
  files_folder / f"forecast_{town_id.lower()}_hourly_data.json",
253
262
  files_folder / f"forecast_{town_id.lower()}_daily_data.json",
263
+ files_folder / f"sun_{town_id.lower()}_data.json",
254
264
  ])
255
265
 
256
266
  # 3. Archivos de comarca (region_id)
@@ -12,6 +12,8 @@ import voluptuous as vol
12
12
  import aiofiles
13
13
  import unicodedata
14
14
 
15
+ from astral import LocationInfo
16
+ from astral.sun import sun
15
17
  from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
16
18
  from homeassistant.core import callback
17
19
  from homeassistant.exceptions import HomeAssistantError
@@ -39,8 +41,8 @@ from .const import (
39
41
  LIMIT_XEMA,
40
42
  LIMIT_PREDICCIO,
41
43
  LIMIT_XDDE,
42
- LIMIT_BASIC,
43
44
  LIMIT_QUOTA,
45
+ LIMIT_BASIC,
44
46
  )
45
47
 
46
48
  from .options_flow import MeteocatOptionsFlowHandler
@@ -172,7 +174,7 @@ class MeteocatConfigFlow(ConfigFlow, domain=DOMAIN):
172
174
  "Archivo regional %s creado con plantilla inicial", alerts_region_file
173
175
  )
174
176
 
175
- # Archivo lightning regional
177
+ # Archivo lightning regional
176
178
  lightning_file = alerts_dir / f"lightning_{self.region_id}.json"
177
179
  if not lightning_file.exists():
178
180
  async with aiofiles.open(lightning_file, "w", encoding="utf-8") as file:
@@ -183,6 +185,51 @@ class MeteocatConfigFlow(ConfigFlow, domain=DOMAIN):
183
185
  "Archivo lightning %s creado con plantilla inicial", lightning_file
184
186
  )
185
187
 
188
+ async def create_sun_file(self):
189
+ """Crea el archivo sun_{town_id}_data.json con datos iniciales de sunrise y sunset."""
190
+ if not self.selected_municipi or not self.latitude or not self.longitude:
191
+ _LOGGER.warning("No se puede crear sun_{town_id}_data.json: faltan municipio o coordenadas")
192
+ return
193
+
194
+ town_id = self.selected_municipi["codi"]
195
+ files_dir = get_storage_dir(self.hass, "files")
196
+ sun_file = files_dir / f"sun_{town_id}_data.json"
197
+
198
+ if not sun_file.exists():
199
+ try:
200
+ # Crear objeto LocationInfo con las coordenadas y zona horaria
201
+ location = LocationInfo(
202
+ name=self.selected_municipi["nom"],
203
+ region="Catalonia",
204
+ timezone="Europe/Madrid",
205
+ latitude=self.latitude,
206
+ longitude=self.longitude
207
+ )
208
+
209
+ # Calcular sunrise y sunset para el día actual
210
+ current_time = datetime.now(timezone.utc).astimezone(TIMEZONE)
211
+ sun_data = sun(location.observer, date=current_time.date(), tzinfo=TIMEZONE)
212
+
213
+ # Formatear los datos para el archivo
214
+ sun_data_formatted = {
215
+ "actualitzat": {"dataUpdate": current_time.isoformat()},
216
+ "dades": [
217
+ {
218
+ "sunrise": sun_data["sunrise"].isoformat(),
219
+ "sunset": sun_data["sunset"].isoformat(),
220
+ "date": current_time.date().isoformat()
221
+ }
222
+ ]
223
+ }
224
+
225
+ # Guardar el archivo
226
+ async with aiofiles.open(sun_file, "w", encoding="utf-8") as file:
227
+ await file.write(json.dumps(sun_data_formatted, ensure_ascii=False, indent=4))
228
+ _LOGGER.info("Archivo sun_%s_data.json creado con datos iniciales", town_id)
229
+
230
+ except Exception as ex:
231
+ _LOGGER.error("Error al crear sun_%s_data.json: %s", town_id, ex)
232
+
186
233
  async def async_step_user(
187
234
  self, user_input: dict[str, Any] | None = None
188
235
  ) -> ConfigFlowResult:
@@ -319,7 +366,9 @@ class MeteocatConfigFlow(ConfigFlow, domain=DOMAIN):
319
366
  self.province_name = station_metadata.get("provincia", {}).get("nom", "")
320
367
  self.station_status = station_metadata.get("estats", [{}])[0].get("codi", "")
321
368
 
369
+ # Crear archivos de alertas y sun
322
370
  await self.create_alerts_file()
371
+ await self.create_sun_file()
323
372
  return await self.async_step_set_api_limits()
324
373
  except Exception as ex:
325
374
  _LOGGER.error("Error al obtener los metadatos de la estación: %s", ex)
@@ -384,4 +433,4 @@ class MeteocatConfigFlow(ConfigFlow, domain=DOMAIN):
384
433
  @callback
385
434
  def async_get_options_flow(config_entry: ConfigEntry) -> MeteocatOptionsFlowHandler:
386
435
  """Devuelve el flujo de opciones para esta configuración."""
387
- return MeteocatOptionsFlowHandler(config_entry)
436
+ return MeteocatOptionsFlowHandler(config_entry)
@@ -42,6 +42,9 @@ QUOTA_BASIC = "quota_basic"
42
42
  QUOTA_XEMA = "quota_xema"
43
43
  QUOTA_QUERIES = "quota_queries"
44
44
  LIGHTNING_FILE_STATUS = "lightning_file_status"
45
+ SUNRISE = "sunrise"
46
+ SUNSET = "sunset"
47
+ SUN_FILE_STATUS = "sun_file_status"
45
48
 
46
49
  from homeassistant.const import Platform
47
50
 
@@ -6,11 +6,13 @@ import logging
6
6
  import asyncio
7
7
  import unicodedata
8
8
  from pathlib import Path
9
- from datetime import datetime, timedelta, timezone, time
9
+ from astral.sun import sun
10
+ from astral import LocationInfo
11
+ from datetime import date, datetime, timedelta, timezone, time
10
12
  from zoneinfo import ZoneInfo
11
13
  from typing import Dict, Any
12
14
 
13
- from homeassistant.core import HomeAssistant
15
+ from homeassistant.core import HomeAssistant, EVENT_HOMEASSISTANT_START
14
16
  from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
15
17
  from homeassistant.exceptions import ConfigEntryNotReady
16
18
  from homeassistant.components.weather import Forecast
@@ -67,6 +69,8 @@ DEFAULT_QUOTES_UPDATE_INTERVAL = timedelta(minutes=10)
67
69
  DEFAULT_QUOTES_FILE_UPDATE_INTERVAL = timedelta(minutes=5)
68
70
  DEFAULT_LIGHTNING_UPDATE_INTERVAL = timedelta(minutes=10)
69
71
  DEFAULT_LIGHTNING_FILE_UPDATE_INTERVAL = timedelta(minutes=5)
72
+ DEFAULT_SUN_UPDATE_INTERVAL = timedelta(minutes=1)
73
+ DEFAULT_SUN_FILE_UPDATE_INTERVAL = timedelta(seconds=30)
70
74
 
71
75
  # Definir la zona horaria local
72
76
  TIMEZONE = ZoneInfo("Europe/Madrid")
@@ -1924,3 +1928,185 @@ class MeteocatLightningFileCoordinator(DataUpdateCoordinator):
1924
1928
  "cg+": 0,
1925
1929
  "total": 0
1926
1930
  }
1931
+
1932
+ class MeteocatSunCoordinator(DataUpdateCoordinator):
1933
+ """Coordinator para manejar la actualización de los datos de sol calculados con Astral."""
1934
+
1935
+ def __init__(
1936
+ self,
1937
+ hass: HomeAssistant,
1938
+ entry_data: dict,
1939
+ ):
1940
+ """
1941
+ Inicializa el coordinador de sol de Meteocat.
1942
+
1943
+ Args:
1944
+ hass (HomeAssistant): Instancia de Home Assistant.
1945
+ entry_data (dict): Datos de configuración obtenidos de core.config_entries.
1946
+ """
1947
+ self.latitude = entry_data.get("latitude")
1948
+ self.longitude = entry_data.get("longitude")
1949
+ self.timezone_str = hass.config.time_zone or "Europe/Madrid"
1950
+ self.town_id = entry_data.get("town_id")
1951
+
1952
+ self.location = LocationInfo(
1953
+ name=entry_data.get("town_name", "Municipio"),
1954
+ region="Spain",
1955
+ timezone=self.timezone_str,
1956
+ latitude=self.latitude,
1957
+ longitude=self.longitude,
1958
+ )
1959
+
1960
+ # Ruta persistente en /config/meteocat_files/files
1961
+ files_folder = get_storage_dir(hass, "files")
1962
+ self.sun_file = files_folder / f"sun_{self.town_id.lower()}_data.json"
1963
+
1964
+ super().__init__(
1965
+ hass,
1966
+ _LOGGER,
1967
+ name=f"{DOMAIN} Sun Coordinator",
1968
+ update_interval=DEFAULT_SUN_UPDATE_INTERVAL, # Ej. timedelta(minutes=1)
1969
+ )
1970
+
1971
+ async def _async_update_data(self) -> Dict:
1972
+ """Actualiza los datos de sol calculados o usa datos en caché según si los eventos han pasado."""
1973
+ existing_data = await load_json_from_file(self.sun_file) or {}
1974
+
1975
+ now = datetime.now(tz=ZoneInfo(self.timezone_str))
1976
+
1977
+ if not existing_data or "dades" not in existing_data or not existing_data["dades"]:
1978
+ return await self._calculate_and_save_new_data()
1979
+
1980
+ last_update_str = existing_data.get('actualitzat', {}).get('dataUpdate')
1981
+ if not last_update_str:
1982
+ return await self._calculate_and_save_new_data()
1983
+
1984
+ last_update = datetime.fromisoformat(last_update_str)
1985
+
1986
+ dades = existing_data["dades"][0]
1987
+ saved_sunrise = datetime.fromisoformat(dades["sunrise"])
1988
+ saved_sunset = datetime.fromisoformat(dades["sunset"])
1989
+
1990
+ # Verificar si los datos necesitan actualización
1991
+ if now > saved_sunrise or now > saved_sunset:
1992
+ return await self._calculate_and_save_new_data()
1993
+ else:
1994
+ _LOGGER.debug("Usando datos existentes de sol: %s", existing_data)
1995
+ return {"actualizado": existing_data['actualitzat']['dataUpdate']}
1996
+
1997
+ async def _calculate_and_save_new_data(self):
1998
+ """Calcula nuevos datos de sol y los guarda en el archivo JSON."""
1999
+ try:
2000
+ now = datetime.now(tz=ZoneInfo(self.timezone_str))
2001
+ today = now.date()
2002
+
2003
+ sun_data_today = sun(self.location.observer, date=today, tzinfo=ZoneInfo(self.timezone_str))
2004
+ sunrise = sun_data_today["sunrise"]
2005
+ sunset = sun_data_today["sunset"]
2006
+
2007
+ if now > sunset:
2008
+ next_day = today + timedelta(days=1)
2009
+ sun_data_next = sun(self.location.observer, date=next_day, tzinfo=ZoneInfo(self.timezone_str))
2010
+ sunrise = sun_data_next["sunrise"]
2011
+ sunset = sun_data_next["sunset"]
2012
+ elif now > sunrise:
2013
+ next_day = today + timedelta(days=1)
2014
+ sun_data_next = sun(self.location.observer, date=next_day, tzinfo=ZoneInfo(self.timezone_str))
2015
+ sunrise = sun_data_next["sunrise"]
2016
+ # sunset permanece como el de hoy
2017
+
2018
+ # Estructurar los datos en el formato correcto
2019
+ current_time = now.isoformat()
2020
+ data_with_timestamp = {
2021
+ "actualitzat": {
2022
+ "dataUpdate": current_time
2023
+ },
2024
+ "dades": [
2025
+ {
2026
+ "sunrise": sunrise.isoformat(),
2027
+ "sunset": sunset.isoformat()
2028
+ }
2029
+ ]
2030
+ }
2031
+
2032
+ # Guardar los datos en un archivo JSON
2033
+ await save_json_to_file(data_with_timestamp, self.sun_file)
2034
+
2035
+ _LOGGER.debug("Datos de sol actualizados exitosamente: %s", data_with_timestamp)
2036
+
2037
+ return {"actualizado": data_with_timestamp['actualitzat']['dataUpdate']}
2038
+
2039
+ except Exception as err:
2040
+ _LOGGER.exception("Error inesperado al calcular los datos de sol: %s", err)
2041
+
2042
+ # Intentar cargar datos en caché si falla el cálculo (aunque es improbable)
2043
+ cached_data = await load_json_from_file(self.sun_file)
2044
+ if cached_data:
2045
+ _LOGGER.warning("Usando datos en caché para los datos de sol.")
2046
+ return {"actualizado": cached_data['actualitzat']['dataUpdate']}
2047
+
2048
+ _LOGGER.error("No se pudo calcular datos actualizados ni cargar datos en caché.")
2049
+ return None
2050
+
2051
+ class MeteocatSunFileCoordinator(DataUpdateCoordinator):
2052
+ """Coordinator para manejar la actualización de los datos de sol desde sun_{town_id}.json."""
2053
+
2054
+ def __init__(
2055
+ self,
2056
+ hass: HomeAssistant,
2057
+ entry_data: dict,
2058
+ ):
2059
+ """
2060
+ Inicializa el coordinador de sol desde archivo.
2061
+
2062
+ Args:
2063
+ hass (HomeAssistant): Instancia de Home Assistant.
2064
+ entry_data (dict): Datos de configuración de la entrada.
2065
+ """
2066
+ self.town_id = entry_data["town_id"]
2067
+ self.timezone_str = hass.config.time_zone or "Europe/Madrid"
2068
+
2069
+ # Ruta persistente en /config/meteocat_files/files
2070
+ files_folder = get_storage_dir(hass, "files")
2071
+ self.sun_file = files_folder / f"sun_{self.town_id.lower()}_data.json"
2072
+
2073
+ super().__init__(
2074
+ hass,
2075
+ _LOGGER,
2076
+ name="Meteocat Sun File Coordinator",
2077
+ update_interval=DEFAULT_SUN_FILE_UPDATE_INTERVAL, # Ej. timedelta(seconds=30)
2078
+ )
2079
+
2080
+ async def _async_update_data(self) -> Dict[str, Any]:
2081
+ """Carga los datos de sol desde el archivo JSON y procesa la información."""
2082
+ existing_data = await load_json_from_file(self.sun_file)
2083
+
2084
+ if not existing_data or "dades" not in existing_data or not existing_data["dades"]:
2085
+ _LOGGER.warning("No se encontraron datos en %s.", self.sun_file)
2086
+ return self._reset_data()
2087
+
2088
+ update_date_str = existing_data.get("actualitzat", {}).get("dataUpdate", "")
2089
+ update_date = datetime.fromisoformat(update_date_str) if update_date_str else None
2090
+ now = datetime.now(ZoneInfo(self.timezone_str))
2091
+
2092
+ dades = existing_data["dades"][0]
2093
+ saved_sunrise = datetime.fromisoformat(dades["sunrise"])
2094
+ saved_sunset = datetime.fromisoformat(dades["sunset"])
2095
+
2096
+ if saved_sunrise < now and saved_sunset < now:
2097
+ _LOGGER.info("Los datos de sol están caducados. Reiniciando valores.")
2098
+ return self._reset_data()
2099
+ else:
2100
+ return {
2101
+ "actualizado": update_date.isoformat() if update_date else now.isoformat(),
2102
+ "sunrise": dades.get("sunrise"),
2103
+ "sunset": dades.get("sunset")
2104
+ }
2105
+
2106
+ def _reset_data(self):
2107
+ """Resetea los datos a valores nulos."""
2108
+ return {
2109
+ "actualizado": datetime.now(ZoneInfo(self.timezone_str)).isoformat(),
2110
+ "sunrise": None,
2111
+ "sunset": None
2112
+ }
@@ -20,5 +20,5 @@
20
20
  "packaging>=20.3",
21
21
  "wrapt>=1.14.0"
22
22
  ],
23
- "version": ""
23
+ "version": "3.1.0"
24
24
  }
@@ -13,8 +13,11 @@ from .const import (
13
13
  LIMIT_PREDICCIO,
14
14
  LIMIT_XDDE,
15
15
  LIMIT_QUOTA,
16
- LIMIT_BASIC
16
+ LIMIT_BASIC,
17
+ LATITUDE,
18
+ LONGITUDE,
17
19
  )
20
+
18
21
  from meteocatpy.town import MeteocatTown
19
22
  from meteocatpy.exceptions import (
20
23
  BadRequestError,
@@ -35,6 +38,11 @@ class MeteocatOptionsFlowHandler(OptionsFlow):
35
38
  self.api_key: str | None = None
36
39
  self.limit_xema: int | None = None
37
40
  self.limit_prediccio: int | None = None
41
+ self.limit_xdde: int | None = None
42
+ self.limit_quota: int | None = None
43
+ self.limit_basic: int | None = None
44
+ self.latitude: float | None = None
45
+ self.longitude: float | None = None
38
46
 
39
47
  async def async_step_init(self, user_input: dict | None = None):
40
48
  """Paso inicial del flujo de opciones."""
@@ -45,6 +53,8 @@ class MeteocatOptionsFlowHandler(OptionsFlow):
45
53
  return await self.async_step_update_limits_only()
46
54
  elif user_input["option"] == "regenerate_assets":
47
55
  return await self.async_step_confirm_regenerate_assets()
56
+ elif user_input["option"] == "update_coordinates":
57
+ return await self.async_step_update_coordinates()
48
58
 
49
59
  return self.async_show_form(
50
60
  step_id="init",
@@ -54,7 +64,8 @@ class MeteocatOptionsFlowHandler(OptionsFlow):
54
64
  options=[
55
65
  "update_api_and_limits",
56
66
  "update_limits_only",
57
- "regenerate_assets"
67
+ "regenerate_assets",
68
+ "update_coordinates" # Nueva opción
58
69
  ],
59
70
  translation_key="option"
60
71
  )
@@ -177,7 +188,54 @@ class MeteocatOptionsFlowHandler(OptionsFlow):
177
188
  return self.async_show_form(
178
189
  step_id="update_limits_only", data_schema=schema, errors=errors
179
190
  )
180
-
191
+
192
+ async def async_step_update_coordinates(self, user_input: dict | None = None):
193
+ """Permite al usuario actualizar las coordenadas (latitude, longitude)."""
194
+ errors = {}
195
+
196
+ if user_input is not None:
197
+ self.latitude = user_input.get(LATITUDE)
198
+ self.longitude = user_input.get(LONGITUDE)
199
+
200
+ # Validar que las coordenadas estén dentro del rango de Cataluña
201
+ if not (40.5 <= self.latitude <= 42.5 and 0.1 <= self.longitude <= 3.3):
202
+ _LOGGER.error(
203
+ "Coordenadas fuera del rango de Cataluña (latitude: %s, longitude: %s).",
204
+ self.latitude, self.longitude
205
+ )
206
+ errors["base"] = "invalid_coordinates"
207
+ else:
208
+ # Actualizar la configuración con las nuevas coordenadas
209
+ self.hass.config_entries.async_update_entry(
210
+ self._config_entry,
211
+ data={
212
+ **self._config_entry.data,
213
+ LATITUDE: self.latitude,
214
+ LONGITUDE: self.longitude
215
+ },
216
+ )
217
+ # Recargar la integración para aplicar los cambios
218
+ await self.hass.config_entries.async_reload(self._config_entry.entry_id)
219
+ _LOGGER.info(
220
+ "Coordenadas actualizadas a latitude: %s, longitude: %s",
221
+ self.latitude, self.longitude
222
+ )
223
+ return self.async_create_entry(title="", data={})
224
+
225
+ schema = vol.Schema({
226
+ vol.Required(LATITUDE, default=self._config_entry.data.get(LATITUDE)): cv.latitude,
227
+ vol.Required(LONGITUDE, default=self._config_entry.data.get(LONGITUDE)): cv.longitude,
228
+ })
229
+ return self.async_show_form(
230
+ step_id="update_coordinates",
231
+ data_schema=schema,
232
+ errors=errors,
233
+ description_placeholders={
234
+ "current_latitude": self._config_entry.data.get(LATITUDE),
235
+ "current_longitude": self._config_entry.data.get(LONGITUDE)
236
+ }
237
+ )
238
+
181
239
  async def async_step_confirm_regenerate_assets(self, user_input: dict | None = None):
182
240
  """Confirma si el usuario realmente quiere regenerar los assets."""
183
241
  if user_input is not None: