meteocat 0.1.39 → 0.1.40

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/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ## [0.1.40](https://github.com/figorr/meteocat/compare/v0.1.39...v0.1.40) (2024-12-26)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * 0.1.40 ([011f139](https://github.com/figorr/meteocat/commit/011f1391874a78d16c1ad1eebbd8f84e6b053b5b))
7
+ * fix hourly forecasts ([bc1aa17](https://github.com/figorr/meteocat/commit/bc1aa178222a7158590b32af442427def4187e38))
8
+
1
9
  ## [0.1.39](https://github.com/figorr/meteocat/compare/v0.1.38...v0.1.39) (2024-12-25)
2
10
 
3
11
 
@@ -24,7 +24,7 @@ from .const import DOMAIN, PLATFORMS
24
24
  _LOGGER = logging.getLogger(__name__)
25
25
 
26
26
  # Versión
27
- __version__ = "0.1.39"
27
+ __version__ = "0.1.40"
28
28
 
29
29
  def safe_remove(path: Path, is_folder: bool = False):
30
30
  """Elimina de forma segura un archivo o carpeta si existe."""
@@ -602,7 +602,7 @@ class HourlyForecastCoordinator(DataUpdateCoordinator):
602
602
  )
603
603
 
604
604
  async def _is_data_valid(self) -> bool:
605
- """Verifica si los datos en el archivo JSON son válidos y actuales."""
605
+ """Verifica si los datos horarios en el archivo JSON son válidos y actuales."""
606
606
  if not os.path.exists(self.file_path):
607
607
  return False
608
608
 
@@ -611,18 +611,23 @@ class HourlyForecastCoordinator(DataUpdateCoordinator):
611
611
  content = await f.read()
612
612
  data = json.loads(content)
613
613
 
614
- if not data or "dies" not in data or not data["dies"]:
614
+ if not data or "dies" not in data:
615
615
  return False
616
616
 
617
- # Validar que los datos sean para la fecha actual
618
- first_date = datetime.fromisoformat(data["dies"][0]["data"].rstrip("Z")).date()
619
- return first_date == datetime.now(timezone.utc).date()
617
+ now = datetime.now(timezone.utc)
618
+ for dia in data["dies"]:
619
+ for forecast in dia.get("variables", {}).get("estatCel", {}).get("valors", []):
620
+ forecast_time = datetime.fromisoformat(forecast["data"].rstrip("Z")).replace(tzinfo=timezone.utc)
621
+ if forecast_time >= now:
622
+ return True
623
+
624
+ return False
620
625
  except Exception as e:
621
626
  _LOGGER.warning("Error validando datos horarios en %s: %s", self.file_path, e)
622
627
  return False
623
628
 
624
629
  async def _async_update_data(self) -> dict:
625
- """Lee los datos de predicción horaria desde el archivo local."""
630
+ """Lee los datos horarios desde el archivo local."""
626
631
  if await self._is_data_valid():
627
632
  try:
628
633
  async with aiofiles.open(self.file_path, "r", encoding="utf-8") as f:
@@ -633,41 +638,63 @@ class HourlyForecastCoordinator(DataUpdateCoordinator):
633
638
 
634
639
  return {}
635
640
 
636
- async def async_forecast_hourly(self) -> list[Forecast] | None:
637
- """Devuelve una lista de objetos Forecast para las próximas horas."""
641
+ def parse_hourly_forecast(self, dia: dict, forecast_time: datetime) -> dict:
642
+ """Convierte una hora de predicción en un diccionario con los datos necesarios."""
643
+ variables = dia.get("variables", {})
644
+ condition_code = next(
645
+ (item["valor"] for item in variables.get("estatCel", {}).get("valors", []) if
646
+ datetime.fromisoformat(item["data"].rstrip("Z")).replace(tzinfo=timezone.utc) == forecast_time),
647
+ -1,
648
+ )
649
+ condition = get_condition_from_code(int(condition_code))
650
+
651
+ return {
652
+ "datetime": forecast_time.isoformat(),
653
+ "temperature": self._get_variable_value(dia, "temp", forecast_time),
654
+ "precipitation": self._get_variable_value(dia, "precipitacio", forecast_time),
655
+ "condition": condition,
656
+ "wind_speed": self._get_variable_value(dia, "velVent", forecast_time),
657
+ "wind_bearing": self._get_variable_value(dia, "dirVent", forecast_time),
658
+ "humidity": self._get_variable_value(dia, "humitat", forecast_time),
659
+ }
660
+
661
+ def get_all_hourly_forecasts(self) -> list[dict]:
662
+ """Obtiene una lista de predicciones horarias procesadas."""
638
663
  if not self.data or "dies" not in self.data:
639
- return None
664
+ return []
640
665
 
641
666
  forecasts = []
642
667
  now = datetime.now(timezone.utc)
643
-
644
668
  for dia in self.data["dies"]:
645
669
  for forecast in dia.get("variables", {}).get("estatCel", {}).get("valors", []):
646
670
  forecast_time = datetime.fromisoformat(forecast["data"].rstrip("Z")).replace(tzinfo=timezone.utc)
647
671
  if forecast_time >= now:
648
- # Usar la función para obtener la condición meteorológica
649
- condition = get_condition_from_code(int(forecast["valor"]))
650
-
651
- forecast_data = {
652
- "datetime": forecast_time,
653
- "temperature": self._get_variable_value(dia, "temp", forecast_time),
654
- "precipitation": self._get_variable_value(dia, "precipitacio", forecast_time),
655
- "condition": condition, # Se usa la condición traducida
656
- "wind_speed": self._get_variable_value(dia, "velVent", forecast_time),
657
- "wind_bearing": self._get_variable_value(dia, "dirVent", forecast_time),
658
- "humidity": self._get_variable_value(dia, "humitat", forecast_time),
659
- }
660
- forecasts.append(Forecast(**forecast_data))
661
-
662
- return forecasts if forecasts else None
672
+ forecasts.append(self.parse_hourly_forecast(dia, forecast_time))
673
+ return forecasts
663
674
 
664
675
  def _get_variable_value(self, dia, variable_name, target_time):
665
676
  """Devuelve el valor de una variable específica para una hora determinada."""
666
677
  variable = dia.get("variables", {}).get(variable_name, {})
667
- for valor in variable.get("valors", []):
668
- data_hora = datetime.fromisoformat(valor["data"].rstrip("Z"))
669
- if data_hora == target_time:
670
- return valor["valor"]
678
+ if not variable:
679
+ _LOGGER.warning("Variable '%s' no encontrada en los datos.", variable_name)
680
+ return None
681
+
682
+ # Obtener lista de valores, soportando tanto 'valors' como 'valor'
683
+ valores = variable.get("valors") or variable.get("valor")
684
+ if not valores:
685
+ _LOGGER.warning("No se encontraron valores para la variable '%s'.", variable_name)
686
+ return None
687
+
688
+ for valor in valores:
689
+ try:
690
+ data_hora = datetime.fromisoformat(valor["data"].rstrip("Z")).replace(tzinfo=timezone.utc)
691
+ if data_hora == target_time:
692
+ return float(valor["valor"])
693
+ except (KeyError, ValueError) as e:
694
+ _LOGGER.warning("Error procesando '%s' para %s: %s", variable_name, valor, e)
695
+ continue
696
+
697
+ _LOGGER.info("No se encontró un valor válido para '%s' en %s.", variable_name, target_time)
671
698
  return None
672
699
 
673
700
  class DailyForecastCoordinator(DataUpdateCoordinator):
@@ -8,5 +8,5 @@
8
8
  "documentation": "https://gitlab.com/figorr/meteocat",
9
9
  "loggers": ["meteocatpy"],
10
10
  "requirements": ["meteocatpy==0.0.15", "packaging>=20.3", "wrapt>=1.14.0"],
11
- "version": "0.1.39"
11
+ "version": "0.1.40"
12
12
  }
@@ -1,2 +1,2 @@
1
1
  # version.py
2
- __version__ = "0.1.39"
2
+ __version__ = "0.1.40"
@@ -11,13 +11,11 @@ from homeassistant.components.weather import (
11
11
  )
12
12
  from homeassistant.core import callback
13
13
  from homeassistant.helpers.entity_platform import AddEntitiesCallback
14
- from homeassistant.components.weather import Forecast
15
14
  from homeassistant.helpers.device_registry import DeviceInfo
16
15
  from homeassistant.const import (
17
16
  DEGREE,
18
17
  PERCENTAGE,
19
18
  UnitOfPrecipitationDepth,
20
- UnitOfVolumetricFlux,
21
19
  UnitOfPressure,
22
20
  UnitOfSpeed,
23
21
  UnitOfTemperature,
@@ -26,10 +24,20 @@ from homeassistant.const import (
26
24
  from .const import (
27
25
  DOMAIN,
28
26
  ATTRIBUTION,
27
+ WIND_SPEED_CODE,
28
+ WIND_DIRECTION_CODE,
29
+ TEMPERATURE_CODE,
30
+ HUMIDITY_CODE,
31
+ PRESSURE_CODE,
32
+ PRECIPITATION_CODE,
33
+ WIND_GUST_CODE,
29
34
  )
30
35
  from .coordinator import (
31
36
  HourlyForecastCoordinator,
32
37
  DailyForecastCoordinator,
38
+ MeteocatSensorCoordinator,
39
+ MeteocatUviFileCoordinator,
40
+ MeteocatConditionCoordinator,
33
41
  )
34
42
 
35
43
  _LOGGER = logging.getLogger(__name__)
@@ -41,16 +49,28 @@ async def async_setup_entry(hass, entry, async_add_entities: AddEntitiesCallback
41
49
 
42
50
  hourly_forecast_coordinator = entry_data.get("hourly_forecast_coordinator")
43
51
  daily_forecast_coordinator = entry_data.get("daily_forecast_coordinator")
52
+ sensor_coordinator = entry_data.get("sensor_coordinator")
53
+ uvi_file_coordinator = entry_data.get("uvi_file_coordinator")
54
+ condition_coordinator = entry_data.get("condition_coordinator")
44
55
 
45
56
  async_add_entities([
46
- MeteocatWeatherEntity(hourly_forecast_coordinator, daily_forecast_coordinator, entry_data)
57
+ MeteocatWeatherEntity(
58
+ hourly_forecast_coordinator,
59
+ daily_forecast_coordinator,
60
+ sensor_coordinator,
61
+ uvi_file_coordinator,
62
+ condition_coordinator,
63
+ entry_data
64
+ )
47
65
  ])
48
66
 
49
67
  class MeteocatWeatherEntity(CoordinatorEntity, WeatherEntity):
50
68
  """Representation of a Meteocat Weather Entity."""
69
+
51
70
  _attr_attribution = ATTRIBUTION
52
71
  _attr_has_entity_name = True
53
72
  _attr_native_precipitation_unit = UnitOfPrecipitationDepth.MILLIMETERS
73
+ _attr_native_precipitation_probability_unit = PERCENTAGE
54
74
  _attr_native_pressure_unit = UnitOfPressure.HPA
55
75
  _attr_native_temperature_unit = UnitOfTemperature.CELSIUS
56
76
  _attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR
@@ -63,12 +83,18 @@ class MeteocatWeatherEntity(CoordinatorEntity, WeatherEntity):
63
83
  self,
64
84
  hourly_forecast_coordinator: HourlyForecastCoordinator,
65
85
  daily_forecast_coordinator: DailyForecastCoordinator,
86
+ sensor_coordinator: MeteocatSensorCoordinator,
87
+ uvi_file_coordinator: MeteocatUviFileCoordinator,
88
+ condition_coordinator: MeteocatConditionCoordinator,
66
89
  entry_data: dict,
67
90
  ) -> None:
68
91
  """Initialize the weather entity."""
69
- super().__init__(hourly_forecast_coordinator)
92
+ super().__init__(daily_forecast_coordinator)
70
93
  self._hourly_forecast_coordinator = hourly_forecast_coordinator
71
94
  self._daily_forecast_coordinator = daily_forecast_coordinator
95
+ self._sensor_coordinator = sensor_coordinator
96
+ self._uvi_file_coordinator = uvi_file_coordinator
97
+ self._condition_coordinator = condition_coordinator
72
98
  self._town_name = entry_data["town_name"]
73
99
  self._town_id = entry_data["town_id"]
74
100
  self._station_id = entry_data["station_id"]
@@ -86,73 +112,90 @@ class MeteocatWeatherEntity(CoordinatorEntity, WeatherEntity):
86
112
  @property
87
113
  def condition(self) -> Optional[str]:
88
114
  """Return the current weather condition."""
89
- forecast_today = self._daily_forecast_coordinator.get_forecast_for_today()
90
- if forecast_today:
91
- return forecast_today.get("condition")
115
+ condition_data = self._condition_coordinator.data or {}
116
+ return condition_data.get("condition")
117
+
118
+ def _get_latest_sensor_value(self, code: str) -> Optional[float]:
119
+ """Helper method to retrieve the latest sensor value."""
120
+ sensor_code = code
121
+ if not sensor_code:
122
+ return None
123
+
124
+ stations = self._sensor_coordinator.data or []
125
+ for station in stations:
126
+ variables = station.get("variables", [])
127
+ variable_data = next(
128
+ (var for var in variables if var.get("codi") == sensor_code),
129
+ None,
130
+ )
131
+ if variable_data:
132
+ lectures = variable_data.get("lectures", [])
133
+ if lectures:
134
+ return lectures[-1].get("valor")
92
135
  return None
93
136
 
94
137
  @property
95
- def temperature(self) -> Optional[float]:
96
- """Return the current temperature."""
97
- forecast_today = self._daily_forecast_coordinator.get_forecast_for_today()
98
- if forecast_today:
99
- return (forecast_today.get("temperature_max") + forecast_today.get("temperature_min")) / 2
100
- return None
138
+ def native_temperature(self) -> Optional[float]:
139
+ return self._get_latest_sensor_value(TEMPERATURE_CODE)
140
+
141
+ @property
142
+ def humidity(self) -> Optional[float]:
143
+ return self._get_latest_sensor_value(HUMIDITY_CODE)
144
+
145
+ @property
146
+ def native_pressure(self) -> Optional[float]:
147
+ return self._get_latest_sensor_value(PRESSURE_CODE)
101
148
 
102
149
  @property
103
- def forecast(self) -> list[Forecast]:
150
+ def native_wind_speed(self) -> Optional[float]:
151
+ return self._get_latest_sensor_value(WIND_SPEED_CODE)
152
+
153
+ @property
154
+ def native_wind_gust_speed(self) -> Optional[float]:
155
+ return self._get_latest_sensor_value(WIND_GUST_CODE)
156
+
157
+ @property
158
+ def uv_index(self) -> Optional[float]:
159
+ """Return the UV index."""
160
+ uvi_data = self._uvi_file_coordinator.data or {}
161
+ return uvi_data.get("uvi")
162
+
163
+ async def async_forecast_daily(self) -> list[Forecast] | None:
104
164
  """Return the daily forecast."""
165
+ await self._daily_forecast_coordinator.async_request_refresh()
105
166
  daily_forecasts = self._daily_forecast_coordinator.get_all_daily_forecasts()
167
+ if not daily_forecasts:
168
+ return None
169
+
106
170
  return [
107
171
  Forecast(
108
172
  datetime=forecast["date"],
109
173
  temperature=forecast["temperature_max"],
110
174
  templow=forecast["temperature_min"],
111
- precipitation=forecast["precipitation"],
175
+ precipitation_probability=forecast["precipitation"],
112
176
  condition=forecast["condition"],
113
177
  )
114
178
  for forecast in daily_forecasts
115
179
  ]
116
-
180
+
117
181
  async def async_forecast_hourly(self) -> list[Forecast] | None:
118
182
  """Return the hourly forecast."""
119
- hourly_forecasts = await self._hourly_forecast_coordinator.async_forecast_hourly()
183
+ await self._hourly_forecast_coordinator.async_request_refresh()
184
+ hourly_forecasts = self._hourly_forecast_coordinator.get_all_hourly_forecasts()
120
185
  if not hourly_forecasts:
121
186
  return None
122
187
 
123
188
  return [
124
189
  Forecast(
125
- datetime=forecast.datetime,
126
- temperature=forecast.temperature,
127
- precipitation=forecast.precipitation,
128
- condition=forecast.condition,
129
- wind_speed=forecast.wind_speed,
130
- wind_bearing=forecast.wind_bearing,
131
- humidity=forecast.humidity,
132
- )
133
- for forecast in hourly_forecasts
134
- ]
135
-
136
- async def async_forecast_daily(self) -> list[Forecast] | None:
137
- """Return the daily forecast."""
138
- # Asegúrate de que los datos estén actualizados antes de procesarlos
139
- await self._daily_forecast_coordinator.async_request_refresh()
140
-
141
- # Obtén las predicciones diarias procesadas desde el coordinador
142
- daily_forecasts = self._daily_forecast_coordinator.get_all_daily_forecasts()
143
- if not daily_forecasts:
144
- return None
145
-
146
- # Convierte las predicciones a la estructura Forecast de Home Assistant
147
- return [
148
- Forecast(
149
- datetime=forecast["date"],
150
- temperature=forecast["temperature_max"],
151
- templow=forecast["temperature_min"],
190
+ datetime=forecast["datetime"],
191
+ temperature=forecast["temperature"],
152
192
  precipitation=forecast["precipitation"],
153
193
  condition=forecast["condition"],
194
+ wind_speed=forecast["wind_speed"],
195
+ wind_bearing=forecast["wind_bearing"],
196
+ humidity=forecast["humidity"],
154
197
  )
155
- for forecast in daily_forecasts
198
+ for forecast in hourly_forecasts
156
199
  ]
157
200
 
158
201
  async def async_update(self) -> None:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "meteocat",
3
- "version": "0.1.39",
3
+ "version": "0.1.40",
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 = "0.1.39"
3
+ version = "0.1.40"
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"