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 +8 -0
- package/custom_components/meteocat/__init__.py +1 -1
- package/custom_components/meteocat/coordinator.py +56 -29
- package/custom_components/meteocat/manifest.json +1 -1
- package/custom_components/meteocat/version.py +1 -1
- package/custom_components/meteocat/weather.py +88 -45
- package/package.json +1 -1
- package/pyproject.toml +1 -1
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.
|
|
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
|
|
614
|
+
if not data or "dies" not in data:
|
|
615
615
|
return False
|
|
616
616
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
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
|
|
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
|
-
|
|
637
|
-
"""
|
|
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
|
|
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
|
-
|
|
649
|
-
|
|
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
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
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):
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
# version.py
|
|
2
|
-
__version__ = "0.1.
|
|
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(
|
|
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__(
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
return
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
126
|
-
temperature=forecast
|
|
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
|
|
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.
|
|
3
|
+
"version": "0.1.40",
|
|
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": {
|