meteocat 0.1.23 → 0.1.26
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 +33 -0
- package/custom_components/meteocat/__init__.py +60 -31
- package/custom_components/meteocat/const.py +15 -14
- package/custom_components/meteocat/coordinator.py +25 -2
- package/custom_components/meteocat/manifest.json +2 -2
- package/custom_components/meteocat/sensor.py +159 -34
- package/custom_components/meteocat/version.py +1 -1
- package/package.json +1 -1
- package/pyproject.toml +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,36 @@
|
|
|
1
|
+
## [0.1.26](https://github.com/figorr/meteocat/compare/v0.1.25...v0.1.26) (2024-12-09)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* 0.1.26 ([9d6b083](https://github.com/figorr/meteocat/commit/9d6b083205d12b628d9a62b0c8c558c0ca9d39e2))
|
|
7
|
+
* add station timestamp constant ([6bd0569](https://github.com/figorr/meteocat/commit/6bd0569030361b89c532ef49faae4b938c1dcf8a))
|
|
8
|
+
* add station timestamp sensor ([ac8b98b](https://github.com/figorr/meteocat/commit/ac8b98bcb225d1348b450b619879b6f636a213da))
|
|
9
|
+
* fix async_remove_entry ([ba28daa](https://github.com/figorr/meteocat/commit/ba28daa87271b6286cf22cd2bcc39422f71b668a))
|
|
10
|
+
* fix entity_id names ([b670d8d](https://github.com/figorr/meteocat/commit/b670d8d53888dc93f371ba7c0e4ed2cdad7ac64b))
|
|
11
|
+
* fix loop when saving JSON ([151dbdd](https://github.com/figorr/meteocat/commit/151dbddd932313831028e1e3e17780dd33d44640))
|
|
12
|
+
* sensor names ([5244f7f](https://github.com/figorr/meteocat/commit/5244f7f8e9d332bfd6adf9e65c053e4b12d8a109))
|
|
13
|
+
|
|
14
|
+
## [0.1.25](https://github.com/figorr/meteocat/compare/v0.1.24...v0.1.25) (2024-12-08)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Bug Fixes
|
|
18
|
+
|
|
19
|
+
* 0.1.25 ([5ba0823](https://github.com/figorr/meteocat/commit/5ba0823db1da999d61c5a4d43733dff5db238ea4))
|
|
20
|
+
* add town and station sensors ([372383f](https://github.com/figorr/meteocat/commit/372383f1bd17ba8e385fef638cf3c1f58515dbaf))
|
|
21
|
+
|
|
22
|
+
## [0.1.24](https://github.com/figorr/meteocat/compare/v0.1.23...v0.1.24) (2024-12-08)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
### Bug Fixes
|
|
26
|
+
|
|
27
|
+
* 0.1.24 ([87b8b51](https://github.com/figorr/meteocat/commit/87b8b519f78ba97a3e9fe01ac1ee85c0efd0a879))
|
|
28
|
+
* add function to save station_data.json ([e78d872](https://github.com/figorr/meteocat/commit/e78d872938c002fe363c2634b1d8d171ea1e2d6e))
|
|
29
|
+
* add solar global irradiance ([dc757b0](https://github.com/figorr/meteocat/commit/dc757b0a8df6ee971c01e290568730cdf3eb54b0))
|
|
30
|
+
* add solar global irradiance sensor ([d0e7373](https://github.com/figorr/meteocat/commit/d0e737302f9d7889a698468ba6ffb881d8da17c2))
|
|
31
|
+
* bump meteocatpy to 0.0.8 ([c280c06](https://github.com/figorr/meteocat/commit/c280c06c7c3c0703e12e94b19108537bbd03baa0))
|
|
32
|
+
* fix constants ([5d4e0b7](https://github.com/figorr/meteocat/commit/5d4e0b77336f1e051a04a45ccaace40ada3ed33a))
|
|
33
|
+
|
|
1
34
|
## [0.1.23](https://github.com/figorr/meteocat/compare/v0.1.22...v0.1.23) (2024-12-07)
|
|
2
35
|
|
|
3
36
|
|
|
@@ -6,13 +6,15 @@ from homeassistant import core
|
|
|
6
6
|
from homeassistant.config_entries import ConfigEntry
|
|
7
7
|
from homeassistant.core import HomeAssistant
|
|
8
8
|
from homeassistant.exceptions import HomeAssistantError
|
|
9
|
+
from homeassistant.helpers.entity_platform import async_get_platforms
|
|
10
|
+
|
|
9
11
|
from .coordinator import MeteocatSensorCoordinator, MeteocatEntityCoordinator
|
|
10
12
|
from .const import DOMAIN, PLATFORMS
|
|
11
13
|
|
|
12
14
|
_LOGGER = logging.getLogger(__name__)
|
|
13
15
|
|
|
14
16
|
# Versión
|
|
15
|
-
__version__ = "0.1.
|
|
17
|
+
__version__ = "0.1.26"
|
|
16
18
|
|
|
17
19
|
|
|
18
20
|
async def async_setup(hass: core.HomeAssistant, config: dict) -> bool:
|
|
@@ -27,44 +29,34 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|
|
27
29
|
# Extraer los datos necesarios de la entrada de configuración
|
|
28
30
|
entry_data = entry.data
|
|
29
31
|
|
|
30
|
-
# Validar
|
|
32
|
+
# Validar campos requeridos
|
|
31
33
|
required_fields = [
|
|
32
34
|
"api_key", "town_name", "town_id", "variable_name",
|
|
33
35
|
"variable_id", "station_name", "station_id"
|
|
34
36
|
]
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
missing_fields = [field for field in required_fields if field not in entry_data]
|
|
38
|
+
if missing_fields:
|
|
39
|
+
_LOGGER.error(f"Faltan los siguientes campos en la configuración: {missing_fields}")
|
|
37
40
|
return False
|
|
38
41
|
|
|
39
|
-
_LOGGER.
|
|
40
|
-
f"
|
|
41
|
-
f"
|
|
42
|
-
f"
|
|
43
|
-
f"(ID: {entry_data['station_id']})."
|
|
42
|
+
_LOGGER.debug(
|
|
43
|
+
f"Datos de configuración: Municipio '{entry_data['town_name']}' (ID: {entry_data['town_id']}), "
|
|
44
|
+
f"Variable '{entry_data['variable_name']}' (ID: {entry_data['variable_id']}), "
|
|
45
|
+
f"Estación '{entry_data['station_name']}' (ID: {entry_data['station_id']})."
|
|
44
46
|
)
|
|
45
47
|
|
|
46
|
-
#
|
|
48
|
+
# Inicializar coordinadores
|
|
47
49
|
try:
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
entry_data=entry_data, # Pasa los datos como un diccionario
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
# Crear el coordinador para entidades de predicción
|
|
55
|
-
entity_coordinator = MeteocatEntityCoordinator(
|
|
56
|
-
hass=hass,
|
|
57
|
-
entry_data=entry_data, # Pasa los mismos datos
|
|
58
|
-
)
|
|
59
|
-
|
|
60
|
-
# Ejecutar la primera actualización
|
|
50
|
+
sensor_coordinator = MeteocatSensorCoordinator(hass=hass, entry_data=entry_data)
|
|
51
|
+
entity_coordinator = MeteocatEntityCoordinator(hass=hass, entry_data=entry_data)
|
|
52
|
+
|
|
61
53
|
await sensor_coordinator.async_config_entry_first_refresh()
|
|
62
54
|
await entity_coordinator.async_config_entry_first_refresh()
|
|
63
|
-
except
|
|
64
|
-
_LOGGER.
|
|
55
|
+
except Exception as err: # Capturar todos los errores
|
|
56
|
+
_LOGGER.exception(f"Error al inicializar los coordinadores: {err}")
|
|
65
57
|
return False
|
|
66
58
|
|
|
67
|
-
# Guardar
|
|
59
|
+
# Guardar coordinadores y datos en hass.data
|
|
68
60
|
hass.data.setdefault(DOMAIN, {})
|
|
69
61
|
hass.data[DOMAIN][entry.entry_id] = {
|
|
70
62
|
"sensor_coordinator": sensor_coordinator,
|
|
@@ -72,7 +64,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|
|
72
64
|
**entry_data,
|
|
73
65
|
}
|
|
74
66
|
|
|
75
|
-
# Configurar
|
|
67
|
+
# Configurar plataformas
|
|
68
|
+
_LOGGER.debug(f"Cargando plataformas: {PLATFORMS}")
|
|
76
69
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
|
77
70
|
|
|
78
71
|
return True
|
|
@@ -80,12 +73,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|
|
80
73
|
|
|
81
74
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|
82
75
|
"""Desactiva una entrada de configuración para Meteocat."""
|
|
76
|
+
platforms = async_get_platforms(hass, DOMAIN)
|
|
77
|
+
_LOGGER.info(f"Descargando plataformas: {[p.domain for p in platforms]}")
|
|
78
|
+
|
|
83
79
|
if entry.entry_id in hass.data.get(DOMAIN, {}):
|
|
84
80
|
hass.data[DOMAIN].pop(entry.entry_id, None)
|
|
85
|
-
if not hass.data[DOMAIN]:
|
|
81
|
+
if not hass.data[DOMAIN]:
|
|
86
82
|
hass.data.pop(DOMAIN)
|
|
87
83
|
|
|
88
|
-
# Desinstalar las plataformas
|
|
89
84
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
|
90
85
|
|
|
91
86
|
|
|
@@ -93,10 +88,14 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
|
|
93
88
|
"""Limpia cualquier dato adicional al desinstalar la integración."""
|
|
94
89
|
_LOGGER.info(f"Eliminando datos residuales de la integración: {entry.entry_id}")
|
|
95
90
|
|
|
96
|
-
# Definir
|
|
97
|
-
|
|
91
|
+
# Definir las rutas de los archivos y carpetas a eliminar
|
|
92
|
+
custom_components_path = hass.config.path("custom_components", DOMAIN)
|
|
93
|
+
assets_folder = os.path.join(custom_components_path, "assets")
|
|
94
|
+
files_folder = os.path.join(custom_components_path, "files")
|
|
98
95
|
symbols_file = os.path.join(assets_folder, "symbols.json")
|
|
96
|
+
station_data_file = os.path.join(files_folder, "station_data.json")
|
|
99
97
|
|
|
98
|
+
# Eliminar el archivo symbols.json si existe
|
|
100
99
|
try:
|
|
101
100
|
if os.path.exists(symbols_file):
|
|
102
101
|
os.remove(symbols_file)
|
|
@@ -105,3 +104,33 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
|
|
105
104
|
_LOGGER.info("El archivo symbols.json no se encontró.")
|
|
106
105
|
except OSError as e:
|
|
107
106
|
_LOGGER.error(f"Error al intentar eliminar el archivo symbols.json: {e}")
|
|
107
|
+
|
|
108
|
+
# Eliminar el archivo station_data.json si existe
|
|
109
|
+
try:
|
|
110
|
+
if os.path.exists(station_data_file):
|
|
111
|
+
os.remove(station_data_file)
|
|
112
|
+
_LOGGER.info("Archivo station_data.json eliminado correctamente.")
|
|
113
|
+
else:
|
|
114
|
+
_LOGGER.info("El archivo station_data.json no se encontró.")
|
|
115
|
+
except OSError as e:
|
|
116
|
+
_LOGGER.error(f"Error al intentar eliminar el archivo station_data.json: {e}")
|
|
117
|
+
|
|
118
|
+
# Eliminar la carpeta assets si está vacía
|
|
119
|
+
try:
|
|
120
|
+
if os.path.exists(assets_folder) and not os.listdir(assets_folder):
|
|
121
|
+
os.rmdir(assets_folder)
|
|
122
|
+
_LOGGER.info("Carpeta assets eliminada correctamente.")
|
|
123
|
+
elif os.path.exists(assets_folder):
|
|
124
|
+
_LOGGER.warning("La carpeta assets no está vacía y no se pudo eliminar.")
|
|
125
|
+
except OSError as e:
|
|
126
|
+
_LOGGER.error(f"Error al intentar eliminar la carpeta assets: {e}")
|
|
127
|
+
|
|
128
|
+
# Eliminar la carpeta files si está vacía
|
|
129
|
+
try:
|
|
130
|
+
if os.path.exists(files_folder) and not os.listdir(files_folder):
|
|
131
|
+
os.rmdir(files_folder)
|
|
132
|
+
_LOGGER.info("Carpeta files eliminada correctamente.")
|
|
133
|
+
elif os.path.exists(files_folder):
|
|
134
|
+
_LOGGER.warning("La carpeta files no está vacía y no se pudo eliminar.")
|
|
135
|
+
except OSError as e:
|
|
136
|
+
_LOGGER.error(f"Error al intentar eliminar la carpeta files: {e}")
|
|
@@ -16,10 +16,25 @@ TEMPERATURE = "temperature" # Temperatura
|
|
|
16
16
|
HUMIDITY = "humidity" # Humedad relativa
|
|
17
17
|
PRESSURE = "pressure" # Presión atmosférica
|
|
18
18
|
PRECIPITATION = "precipitation" # Precipitación
|
|
19
|
+
SOLAR_GLOBAL_IRRADIANCE = "solar_global_irradiance" # Irradiación solar global
|
|
19
20
|
UV_INDEX = "uv_index" # UV
|
|
20
21
|
MAX_TEMPERATURE = "max_temperature" # Temperatura máxima
|
|
21
22
|
MIN_TEMPERATURE = "min_temperature" # Temperatura mínima
|
|
22
23
|
WIND_GUST = "wind_gust" # Racha de viento
|
|
24
|
+
STATION_TIMESTAMP = "station_timestamp" # Código de tiempo de la estación
|
|
25
|
+
|
|
26
|
+
# Definición de códigos para variables
|
|
27
|
+
WIND_SPEED_CODE = 30
|
|
28
|
+
WIND_DIRECTION_CODE = 31
|
|
29
|
+
TEMPERATURE_CODE = 32
|
|
30
|
+
HUMIDITY_CODE = 33
|
|
31
|
+
PRESSURE_CODE = 34
|
|
32
|
+
PRECIPITATION_CODE = 35
|
|
33
|
+
SOLAR_GLOBAL_IRRADIANCE_CODE = 36
|
|
34
|
+
UV_INDEX_CODE = 39
|
|
35
|
+
MAX_TEMPERATURE_CODE = 40
|
|
36
|
+
MIN_TEMPERATURE_CODE = 42
|
|
37
|
+
WIND_GUST_CODE = 50
|
|
23
38
|
|
|
24
39
|
# Mapeo de códigos 'estatCel' a condiciones de Home Assistant
|
|
25
40
|
CONDITION_MAPPING = {
|
|
@@ -36,19 +51,5 @@ CONDITION_MAPPING = {
|
|
|
36
51
|
"snow-rainy": [27, 29, 30],
|
|
37
52
|
}
|
|
38
53
|
|
|
39
|
-
# Mapeo de códigos a claves de sensores
|
|
40
|
-
VARIABLE_CODE_MAPPING = {
|
|
41
|
-
30: WIND_SPEED,
|
|
42
|
-
31: WIND_DIRECTION,
|
|
43
|
-
32: TEMPERATURE,
|
|
44
|
-
33: HUMIDITY,
|
|
45
|
-
34: PRESSURE,
|
|
46
|
-
35: PRECIPITATION,
|
|
47
|
-
39: UV_INDEX,
|
|
48
|
-
40: MAX_TEMPERATURE,
|
|
49
|
-
42: MIN_TEMPERATURE,
|
|
50
|
-
50: WIND_GUST,
|
|
51
|
-
}
|
|
52
|
-
|
|
53
54
|
# Platforms
|
|
54
55
|
PLATFORMS = ["sensor"]
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import os
|
|
4
|
+
import json
|
|
5
|
+
import aiofiles
|
|
3
6
|
import logging
|
|
4
7
|
from datetime import timedelta
|
|
5
8
|
from typing import Dict
|
|
@@ -26,6 +29,17 @@ _LOGGER = logging.getLogger(__name__)
|
|
|
26
29
|
DEFAULT_SENSOR_UPDATE_INTERVAL = timedelta(minutes=90)
|
|
27
30
|
DEFAULT_ENTITY_UPDATE_INTERVAL = timedelta(hours=12)
|
|
28
31
|
|
|
32
|
+
async def save_json_to_file(data: dict, output_file: str) -> None:
|
|
33
|
+
"""Save the JSON data to a file asynchronously."""
|
|
34
|
+
try:
|
|
35
|
+
# Crea el directorio si no existe
|
|
36
|
+
os.makedirs(os.path.dirname(output_file), exist_ok=True)
|
|
37
|
+
|
|
38
|
+
# Escribe los datos JSON de forma asíncrona
|
|
39
|
+
async with aiofiles.open(output_file, mode="w", encoding="utf-8") as f:
|
|
40
|
+
await f.write(json.dumps(data, indent=4, ensure_ascii=False))
|
|
41
|
+
except Exception as e:
|
|
42
|
+
raise RuntimeError(f"Error saving JSON to {output_file}: {e}")
|
|
29
43
|
|
|
30
44
|
class MeteocatSensorCoordinator(DataUpdateCoordinator):
|
|
31
45
|
"""Coordinator para manejar la actualización de datos de los sensores."""
|
|
@@ -63,8 +77,18 @@ class MeteocatSensorCoordinator(DataUpdateCoordinator):
|
|
|
63
77
|
async def _async_update_data(self) -> Dict:
|
|
64
78
|
"""Actualiza los datos de los sensores desde la API de Meteocat."""
|
|
65
79
|
try:
|
|
66
|
-
|
|
80
|
+
# Obtener datos desde la API
|
|
81
|
+
data = await self.meteocat_station_data.get_station_data(self.station_id)
|
|
67
82
|
_LOGGER.debug("Datos de sensores actualizados exitosamente: %s", data)
|
|
83
|
+
|
|
84
|
+
# Determinar la ruta al archivo en la carpeta raíz del repositorio
|
|
85
|
+
output_file = os.path.join(
|
|
86
|
+
self.hass.config.path(), "custom_components", "meteocat", "files", "station_data.json"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Guardar los datos en un archivo JSON
|
|
90
|
+
await save_json_to_file(data, output_file)
|
|
91
|
+
|
|
68
92
|
return data
|
|
69
93
|
except ForbiddenError as err:
|
|
70
94
|
_LOGGER.error(
|
|
@@ -95,7 +119,6 @@ class MeteocatSensorCoordinator(DataUpdateCoordinator):
|
|
|
95
119
|
)
|
|
96
120
|
raise
|
|
97
121
|
|
|
98
|
-
|
|
99
122
|
class MeteocatEntityCoordinator(DataUpdateCoordinator):
|
|
100
123
|
"""Coordinator para manejar la actualización de datos de las entidades de predicción."""
|
|
101
124
|
|
|
@@ -7,6 +7,6 @@
|
|
|
7
7
|
"iot_class": "cloud_polling",
|
|
8
8
|
"documentation": "https://gitlab.com/figorr/meteocat",
|
|
9
9
|
"loggers": ["meteocatpy"],
|
|
10
|
-
"requirements": ["meteocatpy==0.0.
|
|
11
|
-
"version": "0.1.
|
|
10
|
+
"requirements": ["meteocatpy==0.0.8", "packaging>=20.3", "wrapt>=1.14.0"],
|
|
11
|
+
"version": "0.1.26"
|
|
12
12
|
}
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
|
-
from
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
import logging
|
|
6
|
+
from homeassistant.helpers.entity import (
|
|
7
|
+
DeviceInfo,
|
|
8
|
+
EntityCategory,
|
|
9
|
+
)
|
|
5
10
|
from homeassistant.components.sensor import (
|
|
6
11
|
SensorDeviceClass,
|
|
7
12
|
SensorEntity,
|
|
@@ -18,39 +23,57 @@ from homeassistant.const import (
|
|
|
18
23
|
UnitOfSpeed,
|
|
19
24
|
UnitOfTemperature,
|
|
20
25
|
UnitOfVolumetricFlux,
|
|
26
|
+
UnitOfIrradiance,
|
|
21
27
|
)
|
|
22
28
|
|
|
23
29
|
from .const import (
|
|
24
30
|
DOMAIN,
|
|
31
|
+
TOWN_NAME,
|
|
32
|
+
TOWN_ID,
|
|
33
|
+
STATION_NAME,
|
|
34
|
+
STATION_ID,
|
|
25
35
|
WIND_SPEED,
|
|
26
36
|
WIND_DIRECTION,
|
|
27
37
|
TEMPERATURE,
|
|
28
38
|
HUMIDITY,
|
|
29
39
|
PRESSURE,
|
|
30
40
|
PRECIPITATION,
|
|
41
|
+
SOLAR_GLOBAL_IRRADIANCE,
|
|
31
42
|
UV_INDEX,
|
|
32
43
|
MAX_TEMPERATURE,
|
|
33
44
|
MIN_TEMPERATURE,
|
|
34
45
|
WIND_GUST,
|
|
35
|
-
|
|
46
|
+
STATION_TIMESTAMP,
|
|
47
|
+
WIND_SPEED_CODE,
|
|
48
|
+
WIND_DIRECTION_CODE,
|
|
49
|
+
TEMPERATURE_CODE,
|
|
50
|
+
HUMIDITY_CODE,
|
|
51
|
+
PRESSURE_CODE,
|
|
52
|
+
PRECIPITATION_CODE,
|
|
53
|
+
SOLAR_GLOBAL_IRRADIANCE_CODE,
|
|
54
|
+
UV_INDEX_CODE,
|
|
55
|
+
MAX_TEMPERATURE_CODE,
|
|
56
|
+
MIN_TEMPERATURE_CODE,
|
|
57
|
+
WIND_GUST_CODE,
|
|
36
58
|
)
|
|
37
59
|
|
|
38
60
|
from .coordinator import MeteocatSensorCoordinator
|
|
39
61
|
|
|
62
|
+
_LOGGER = logging.getLogger(__name__)
|
|
40
63
|
|
|
41
64
|
@dataclass
|
|
42
65
|
class MeteocatSensorEntityDescription(SensorEntityDescription):
|
|
43
66
|
"""A class that describes Meteocat sensor entities."""
|
|
44
67
|
|
|
45
|
-
|
|
46
68
|
SENSOR_TYPES: tuple[MeteocatSensorEntityDescription, ...] = (
|
|
69
|
+
# Sensores dinámicos
|
|
47
70
|
MeteocatSensorEntityDescription(
|
|
48
71
|
key=WIND_SPEED,
|
|
49
72
|
name="Wind Speed",
|
|
50
73
|
icon="mdi:weather-windy",
|
|
51
74
|
device_class=SensorDeviceClass.WIND_SPEED,
|
|
52
75
|
state_class=SensorStateClass.MEASUREMENT,
|
|
53
|
-
native_unit_of_measurement=UnitOfSpeed.
|
|
76
|
+
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
|
|
54
77
|
),
|
|
55
78
|
MeteocatSensorEntityDescription(
|
|
56
79
|
key=WIND_DIRECTION,
|
|
@@ -90,6 +113,14 @@ SENSOR_TYPES: tuple[MeteocatSensorEntityDescription, ...] = (
|
|
|
90
113
|
state_class=SensorStateClass.MEASUREMENT,
|
|
91
114
|
native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
|
|
92
115
|
),
|
|
116
|
+
MeteocatSensorEntityDescription(
|
|
117
|
+
key=SOLAR_GLOBAL_IRRADIANCE,
|
|
118
|
+
name="Solar Global Irradiance",
|
|
119
|
+
icon="mdi:weather-sunny",
|
|
120
|
+
device_class=SensorDeviceClass.IRRADIANCE,
|
|
121
|
+
state_class=SensorStateClass.MEASUREMENT,
|
|
122
|
+
native_unit_of_measurement = UnitOfIrradiance.WATTS_PER_SQUARE_METER,
|
|
123
|
+
),
|
|
93
124
|
MeteocatSensorEntityDescription(
|
|
94
125
|
key=UV_INDEX,
|
|
95
126
|
name="UV Index",
|
|
@@ -117,8 +148,39 @@ SENSOR_TYPES: tuple[MeteocatSensorEntityDescription, ...] = (
|
|
|
117
148
|
icon="mdi:weather-windy",
|
|
118
149
|
device_class=SensorDeviceClass.WIND_SPEED,
|
|
119
150
|
state_class=SensorStateClass.MEASUREMENT,
|
|
120
|
-
native_unit_of_measurement=UnitOfSpeed.
|
|
151
|
+
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
|
|
152
|
+
),
|
|
153
|
+
# Sensores estáticos
|
|
154
|
+
MeteocatSensorEntityDescription(
|
|
155
|
+
key=TOWN_NAME,
|
|
156
|
+
name="Town Name",
|
|
157
|
+
icon="mdi:home-city",
|
|
158
|
+
entity_category=EntityCategory.DIAGNOSTIC,
|
|
121
159
|
),
|
|
160
|
+
MeteocatSensorEntityDescription(
|
|
161
|
+
key=TOWN_ID,
|
|
162
|
+
name="Town ID",
|
|
163
|
+
icon="mdi:identifier",
|
|
164
|
+
entity_category=EntityCategory.DIAGNOSTIC,
|
|
165
|
+
),
|
|
166
|
+
MeteocatSensorEntityDescription(
|
|
167
|
+
key=STATION_NAME,
|
|
168
|
+
name="Station Name",
|
|
169
|
+
icon="mdi:broadcast",
|
|
170
|
+
entity_category=EntityCategory.DIAGNOSTIC,
|
|
171
|
+
),
|
|
172
|
+
MeteocatSensorEntityDescription(
|
|
173
|
+
key=STATION_ID,
|
|
174
|
+
name="Station ID",
|
|
175
|
+
icon="mdi:identifier",
|
|
176
|
+
entity_category=EntityCategory.DIAGNOSTIC,
|
|
177
|
+
),
|
|
178
|
+
MeteocatSensorEntityDescription(
|
|
179
|
+
key=STATION_TIMESTAMP,
|
|
180
|
+
name="Station Timestamp",
|
|
181
|
+
icon="mdi:calendar-clock",
|
|
182
|
+
device_class=SensorDeviceClass.TIMESTAMP,
|
|
183
|
+
)
|
|
122
184
|
)
|
|
123
185
|
|
|
124
186
|
|
|
@@ -133,62 +195,125 @@ async def async_setup_entry(hass, entry, async_add_entities: AddEntitiesCallback
|
|
|
133
195
|
for description in SENSOR_TYPES
|
|
134
196
|
)
|
|
135
197
|
|
|
136
|
-
|
|
137
198
|
class MeteocatSensor(CoordinatorEntity[MeteocatSensorCoordinator], SensorEntity):
|
|
138
199
|
"""Representation of a Meteocat sensor."""
|
|
200
|
+
STATIC_KEYS = {TOWN_NAME, TOWN_ID, STATION_NAME, STATION_ID}
|
|
201
|
+
|
|
202
|
+
CODE_MAPPING = {
|
|
203
|
+
WIND_SPEED: WIND_SPEED_CODE,
|
|
204
|
+
WIND_DIRECTION: WIND_DIRECTION_CODE,
|
|
205
|
+
TEMPERATURE: TEMPERATURE_CODE,
|
|
206
|
+
HUMIDITY: HUMIDITY_CODE,
|
|
207
|
+
PRESSURE: PRESSURE_CODE,
|
|
208
|
+
PRECIPITATION: PRECIPITATION_CODE,
|
|
209
|
+
SOLAR_GLOBAL_IRRADIANCE: SOLAR_GLOBAL_IRRADIANCE_CODE,
|
|
210
|
+
UV_INDEX: UV_INDEX_CODE,
|
|
211
|
+
MAX_TEMPERATURE: MAX_TEMPERATURE_CODE,
|
|
212
|
+
MIN_TEMPERATURE: MIN_TEMPERATURE_CODE,
|
|
213
|
+
WIND_GUST: WIND_GUST_CODE,
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
_attr_has_entity_name = True # Activa el uso de nombres basados en el dispositivo
|
|
139
217
|
|
|
140
218
|
def __init__(self, coordinator, description, entry_data):
|
|
141
219
|
"""Initialize the sensor."""
|
|
142
220
|
super().__init__(coordinator)
|
|
143
221
|
self.entity_description = description
|
|
144
|
-
self.api_key = entry_data["api_key"]
|
|
145
|
-
self._town_name = entry_data["town_name"]
|
|
146
|
-
self._town_id = entry_data["town_id"]
|
|
147
|
-
self.
|
|
148
|
-
self.
|
|
149
|
-
self._station_name = entry_data["station_name"] # Usamos el nombre de la estación
|
|
150
|
-
self._station_id = entry_data["station_id"] # Usamos el ID de la estación
|
|
222
|
+
self.api_key = entry_data["api_key"]
|
|
223
|
+
self._town_name = entry_data["town_name"]
|
|
224
|
+
self._town_id = entry_data["town_id"]
|
|
225
|
+
self._station_name = entry_data["station_name"]
|
|
226
|
+
self._station_id = entry_data["station_id"]
|
|
151
227
|
|
|
152
228
|
# Unique ID for the entity
|
|
153
|
-
self._attr_unique_id = f"{self.
|
|
229
|
+
self._attr_unique_id = f"sensor.{DOMAIN}_{self._station_id}_{self.entity_description.key}"
|
|
230
|
+
|
|
231
|
+
# Asigna entity_category desde description (si está definido)
|
|
232
|
+
self._attr_entity_category = getattr(description, "entity_category", None)
|
|
233
|
+
|
|
234
|
+
# Log para depuración
|
|
235
|
+
_LOGGER.debug(
|
|
236
|
+
"Inicializando sensor: %s, Unique ID: %s",
|
|
237
|
+
self.entity_description.name,
|
|
238
|
+
self._attr_unique_id,
|
|
239
|
+
)
|
|
154
240
|
|
|
155
241
|
@property
|
|
156
242
|
def native_value(self):
|
|
157
243
|
"""Return the state of the sensor."""
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
244
|
+
# Información estática
|
|
245
|
+
if self.entity_description.key in self.STATIC_KEYS:
|
|
246
|
+
# Información estática del `entry_data`
|
|
247
|
+
if self.entity_description.key == TOWN_NAME:
|
|
248
|
+
return self._town_name
|
|
249
|
+
if self.entity_description.key == TOWN_ID:
|
|
250
|
+
return self._town_id
|
|
251
|
+
if self.entity_description.key == STATION_NAME:
|
|
252
|
+
return self._station_name
|
|
253
|
+
if self.entity_description.key == STATION_ID:
|
|
254
|
+
return self._station_id
|
|
255
|
+
# Información dinámica
|
|
256
|
+
sensor_code = self.CODE_MAPPING.get(self.entity_description.key)
|
|
257
|
+
|
|
163
258
|
if sensor_code is not None:
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
#
|
|
170
|
-
|
|
171
|
-
|
|
259
|
+
# Accedemos a las estaciones en el JSON recibido
|
|
260
|
+
stations = self.coordinator.data or []
|
|
261
|
+
for station in stations:
|
|
262
|
+
variables = station.get("variables", [])
|
|
263
|
+
|
|
264
|
+
# Filtramos por código
|
|
265
|
+
variable_data = next(
|
|
266
|
+
(var for var in variables if var.get("codi") == sensor_code),
|
|
267
|
+
None,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
if variable_data:
|
|
271
|
+
# Obtenemos la última lectura
|
|
272
|
+
lectures = variable_data.get("lectures", [])
|
|
273
|
+
if lectures:
|
|
274
|
+
latest_reading = lectures[-1]
|
|
275
|
+
value = latest_reading.get("valor")
|
|
172
276
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
277
|
+
# Convertimos grados a dirección cardinal para WIND_DIRECTION
|
|
278
|
+
if self.entity_description.key == WIND_DIRECTION and isinstance(value, (int, float)):
|
|
279
|
+
return self._convert_degrees_to_cardinal(value)
|
|
176
280
|
|
|
177
|
-
|
|
281
|
+
return value
|
|
282
|
+
# Lógica específica para el sensor de timestamp
|
|
283
|
+
if self.entity_description.key == "station_timestamp":
|
|
284
|
+
stations = self.coordinator.data or []
|
|
285
|
+
for station in stations:
|
|
286
|
+
variables = station.get("variables", [])
|
|
287
|
+
for variable in variables:
|
|
288
|
+
lectures = variable.get("lectures", [])
|
|
289
|
+
if lectures:
|
|
290
|
+
# Obtenemos el campo `data` de la última lectura
|
|
291
|
+
latest_reading = lectures[-1]
|
|
292
|
+
raw_timestamp = latest_reading.get("data")
|
|
293
|
+
|
|
294
|
+
if raw_timestamp:
|
|
295
|
+
# Convertir el timestamp a un objeto datetime
|
|
296
|
+
try:
|
|
297
|
+
return datetime.fromisoformat(raw_timestamp.replace("Z", "+00:00"))
|
|
298
|
+
except ValueError:
|
|
299
|
+
# Manejo de errores si el formato no es válido
|
|
300
|
+
return None
|
|
178
301
|
|
|
179
302
|
return None
|
|
180
303
|
|
|
181
304
|
@staticmethod
|
|
182
305
|
def _convert_degrees_to_cardinal(degree: float) -> str:
|
|
183
306
|
"""Convert degrees to cardinal direction."""
|
|
307
|
+
if not isinstance(degree, (int, float)):
|
|
308
|
+
return "Unknown" # Retorna "Unknown" si el valor no es un número válido
|
|
309
|
+
|
|
184
310
|
directions = [
|
|
185
311
|
"N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE",
|
|
186
|
-
"S", "
|
|
312
|
+
"S", "SSO", "SO", "OSO", "O", "ONO", "NO", "NNO", "N",
|
|
187
313
|
]
|
|
188
314
|
index = round(degree / 22.5) % 16
|
|
189
315
|
return directions[index]
|
|
190
316
|
|
|
191
|
-
|
|
192
317
|
@property
|
|
193
318
|
def device_info(self) -> DeviceInfo:
|
|
194
319
|
"""Return the device info."""
|
|
@@ -196,5 +321,5 @@ class MeteocatSensor(CoordinatorEntity[MeteocatSensorCoordinator], SensorEntity)
|
|
|
196
321
|
identifiers={(DOMAIN, self._town_id)},
|
|
197
322
|
name=self._town_name,
|
|
198
323
|
manufacturer="Meteocat",
|
|
199
|
-
model="Meteocat API"
|
|
324
|
+
model="Meteocat API",
|
|
200
325
|
)
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
# version.py
|
|
2
|
-
__version__ = "0.1.
|
|
2
|
+
__version__ = "0.1.26"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "meteocat",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.26",
|
|
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": {
|