meteocat 3.0.0 → 3.2.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 (40) 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/sync-gitlab.yml +15 -4
  10. package/.github/workflows/sync-labels.yml +21 -0
  11. package/CHANGELOG.md +80 -11
  12. package/README.md +16 -4
  13. package/custom_components/meteocat/__init__.py +57 -42
  14. package/custom_components/meteocat/condition.py +6 -2
  15. package/custom_components/meteocat/config_flow.py +231 -4
  16. package/custom_components/meteocat/const.py +17 -2
  17. package/custom_components/meteocat/coordinator.py +1122 -101
  18. package/custom_components/meteocat/helpers.py +31 -36
  19. package/custom_components/meteocat/manifest.json +3 -2
  20. package/custom_components/meteocat/options_flow.py +71 -3
  21. package/custom_components/meteocat/sensor.py +660 -247
  22. package/custom_components/meteocat/strings.json +252 -15
  23. package/custom_components/meteocat/translations/ca.json +249 -13
  24. package/custom_components/meteocat/translations/en.json +252 -15
  25. package/custom_components/meteocat/translations/es.json +252 -15
  26. package/custom_components/meteocat/version.py +1 -1
  27. package/filetree.txt +12 -3
  28. package/hacs.json +1 -1
  29. package/images/daily_forecast_2_alerts.png +0 -0
  30. package/images/daily_forecast_no_alerts.png +0 -0
  31. package/images/diagnostic_sensors.png +0 -0
  32. package/images/dynamic_sensors.png +0 -0
  33. package/images/options.png +0 -0
  34. package/images/regenerate_assets.png +0 -0
  35. package/images/setup_options.png +0 -0
  36. package/images/system_options.png +0 -0
  37. package/package.json +1 -1
  38. package/pyproject.toml +1 -1
  39. package/scripts/update_version.sh +6 -0
  40. package/.github/workflows/close-duplicates.yml +0 -57
@@ -4,8 +4,8 @@ import logging
4
4
  from datetime import datetime
5
5
  from pathlib import Path
6
6
  from homeassistant.core import HomeAssistant
7
- from homeassistant.util.dt import as_local, as_utc, start_of_local_day
8
- from homeassistant.helpers.sun import get_astral_event_date
7
+ from homeassistant.util import dt as dt_util
8
+ from solarmoonpy.location import Location
9
9
 
10
10
  _LOGGER = logging.getLogger(__name__)
11
11
 
@@ -19,45 +19,40 @@ def get_storage_dir(hass: HomeAssistant, subdir: str | None = None) -> Path:
19
19
  return base_dir
20
20
 
21
21
  # Cálculo de amanecer y atardecer para definir cuando es de noche
22
- def get_sun_times(hass, current_time=None):
23
- """Obtén las horas de amanecer y atardecer para el día actual."""
24
- if current_time is None:
25
- current_time = datetime.now()
22
+ def get_sun_times(location: Location, current_time: datetime | None = None) -> tuple[datetime, datetime]:
23
+ """Obtiene las horas de amanecer y atardecer para una ubicación usando solarmoonpy."""
24
+ now = dt_util.as_local(current_time or dt_util.now())
25
+ today = now.date()
26
+ sunrise = location.sunrise(date=today, local=True)
27
+ sunset = location.sunset(date=today, local=True)
26
28
 
27
- # Asegúrate de que current_time es aware (UTC)
28
- current_time = as_utc(current_time)
29
- today = start_of_local_day(as_local(current_time))
30
-
31
- # Obtén los eventos de amanecer y atardecer del día actual
32
- sunrise = get_astral_event_date(hass, "sunrise", today)
33
- sunset = get_astral_event_date(hass, "sunset", today)
29
+ if not sunrise or not sunset:
30
+ raise ValueError("No se pudieron calcular amanecer o atardecer.")
34
31
 
35
32
  _LOGGER.debug(
36
- "Sunrise: %s, Sunset: %s, Current Time: %s",
37
- sunrise,
38
- sunset,
39
- as_local(current_time),
33
+ "[solarmoonpy] Amanecer: %s, Atardecer: %s, Hora actual: %s",
34
+ sunrise, sunset, now
40
35
  )
36
+ return sunrise, sunset
41
37
 
42
- if sunrise and sunset:
43
- return sunrise, sunset
44
-
45
- raise ValueError("No se pudieron determinar los datos de amanecer y atardecer.")
46
-
47
- def is_night(current_time, hass):
48
- """Determina si actualmente es de noche."""
49
- # Asegúrate de que current_time es aware (UTC)
38
+ def is_night(current_time, location: Location) -> bool:
39
+ """Determina si actualmente es de noche usando una instancia de Location."""
40
+ # Asegurarse de que current_time sea aware y en zona local
50
41
  if current_time.tzinfo is None:
51
- current_time = as_utc(current_time)
52
-
53
- sunrise, sunset = get_sun_times(hass, current_time)
54
-
42
+ _LOGGER.warning("current_time sin zona horaria, asumiendo UTC")
43
+ current_time = dt_util.as_local(dt_util.utc_to_local(current_time))
44
+ else:
45
+ current_time = dt_util.as_local(current_time)
46
+
47
+ try:
48
+ sunrise, sunset = get_sun_times(location, current_time)
49
+ except Exception as e:
50
+ _LOGGER.warning("Fallo al calcular amanecer/atardecer con solarmoonpy: %s", e)
51
+ return False # fallback seguro
52
+
53
+ is_night_now = current_time < sunrise or current_time > sunset
55
54
  _LOGGER.debug(
56
- "Hora actual: %s, Amanecer: %s, Atardecer: %s",
57
- as_local(current_time),
58
- as_local(sunrise),
59
- as_local(sunset),
55
+ "[solarmoonpy] Hora actual: %s | Amanecer: %s | Atardecer: %s → Noche: %s",
56
+ current_time, sunrise, sunset, is_night_now
60
57
  )
61
-
62
- # Es de noche si es antes del amanecer o después del atardecer
63
- return current_time < sunrise or current_time > sunset
58
+ return is_night_now
@@ -16,9 +16,10 @@
16
16
  "meteocatpy"
17
17
  ],
18
18
  "requirements": [
19
- "meteocatpy==1.0.1",
19
+ "meteocatpy==1.0.2",
20
+ "solarmoonpy==1.0.8",
20
21
  "packaging>=20.3",
21
22
  "wrapt>=1.14.0"
22
23
  ],
23
- "version": "3.0.0"
24
+ "version": "3.2.0"
24
25
  }
@@ -13,8 +13,12 @@ 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,
19
+ ALTITUDE,
17
20
  )
21
+
18
22
  from meteocatpy.town import MeteocatTown
19
23
  from meteocatpy.exceptions import (
20
24
  BadRequestError,
@@ -35,6 +39,12 @@ class MeteocatOptionsFlowHandler(OptionsFlow):
35
39
  self.api_key: str | None = None
36
40
  self.limit_xema: int | None = None
37
41
  self.limit_prediccio: int | None = None
42
+ self.limit_xdde: int | None = None
43
+ self.limit_quota: int | None = None
44
+ self.limit_basic: int | None = None
45
+ self.latitude: float | None = None
46
+ self.longitude: float | None = None
47
+ self.altitude: float | None = None
38
48
 
39
49
  async def async_step_init(self, user_input: dict | None = None):
40
50
  """Paso inicial del flujo de opciones."""
@@ -45,6 +55,8 @@ class MeteocatOptionsFlowHandler(OptionsFlow):
45
55
  return await self.async_step_update_limits_only()
46
56
  elif user_input["option"] == "regenerate_assets":
47
57
  return await self.async_step_confirm_regenerate_assets()
58
+ elif user_input["option"] == "update_coordinates":
59
+ return await self.async_step_update_coordinates()
48
60
 
49
61
  return self.async_show_form(
50
62
  step_id="init",
@@ -54,7 +66,8 @@ class MeteocatOptionsFlowHandler(OptionsFlow):
54
66
  options=[
55
67
  "update_api_and_limits",
56
68
  "update_limits_only",
57
- "regenerate_assets"
69
+ "regenerate_assets",
70
+ "update_coordinates"
58
71
  ],
59
72
  translation_key="option"
60
73
  )
@@ -177,7 +190,62 @@ class MeteocatOptionsFlowHandler(OptionsFlow):
177
190
  return self.async_show_form(
178
191
  step_id="update_limits_only", data_schema=schema, errors=errors
179
192
  )
180
-
193
+
194
+ async def async_step_update_coordinates(self, user_input: dict | None = None):
195
+ """Permite al usuario actualizar las coordenadas (latitude, longitude)."""
196
+ errors = {}
197
+
198
+ if user_input is not None:
199
+ self.latitude = user_input.get(LATITUDE)
200
+ self.longitude = user_input.get(LONGITUDE)
201
+ self.altitude = user_input.get(ALTITUDE)
202
+
203
+ # Validar que las coordenadas estén dentro del rango de Cataluña
204
+ if not (40.5 <= self.latitude <= 42.5 and 0.1 <= self.longitude <= 3.3):
205
+ _LOGGER.error(
206
+ "Coordenadas fuera del rango de Cataluña (latitude: %s, longitude: %s).",
207
+ self.latitude, self.longitude
208
+ )
209
+ errors["base"] = "invalid_coordinates"
210
+ # Validar que la altitud sea positiva
211
+ elif self.altitude < 0:
212
+ _LOGGER.error("Altitud inválida: %s. Debe ser >= 0.", self.altitude)
213
+ errors["base"] = "invalid_altitude"
214
+ else:
215
+ # Actualizar la configuración con las nuevas coordenadas
216
+ self.hass.config_entries.async_update_entry(
217
+ self._config_entry,
218
+ data={
219
+ **self._config_entry.data,
220
+ LATITUDE: self.latitude,
221
+ LONGITUDE: self.longitude,
222
+ ALTITUDE: self.altitude
223
+ },
224
+ )
225
+ # Recargar la integración para aplicar los cambios
226
+ await self.hass.config_entries.async_reload(self._config_entry.entry_id)
227
+ _LOGGER.info(
228
+ "Coordenadas actualizadas a latitude: %s, longitude: %s, altitude=%s.",
229
+ self.latitude, self.longitude, self.altitude
230
+ )
231
+ return self.async_create_entry(title="", data={})
232
+
233
+ schema = vol.Schema({
234
+ vol.Required(LATITUDE, default=self._config_entry.data.get(LATITUDE)): cv.latitude,
235
+ vol.Required(LONGITUDE, default=self._config_entry.data.get(LONGITUDE)): cv.longitude,
236
+ vol.Required(ALTITUDE, default=self._config_entry.data.get(ALTITUDE)): vol.Coerce(float),
237
+ })
238
+ return self.async_show_form(
239
+ step_id="update_coordinates",
240
+ data_schema=schema,
241
+ errors=errors,
242
+ description_placeholders={
243
+ "current_latitude": self._config_entry.data.get(LATITUDE),
244
+ "current_longitude": self._config_entry.data.get(LONGITUDE),
245
+ "current_altitude": self._config_entry.data.get(ALTITUDE, 0.0)
246
+ }
247
+ )
248
+
181
249
  async def async_step_confirm_regenerate_assets(self, user_input: dict | None = None):
182
250
  """Confirma si el usuario realmente quiere regenerar los assets."""
183
251
  if user_input is not None: