meteocat 0.1.25 → 0.1.27

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 (35) hide show
  1. package/.github/workflows/release.yml +33 -33
  2. package/.pre-commit-config.yaml +37 -37
  3. package/.releaserc +23 -23
  4. package/.releaserc.toml +14 -14
  5. package/AUTHORS.md +12 -12
  6. package/CHANGELOG.md +430 -401
  7. package/README.md +40 -40
  8. package/custom_components/meteocat/__init__.py +136 -107
  9. package/custom_components/meteocat/condition.py +28 -28
  10. package/custom_components/meteocat/config_flow.py +199 -192
  11. package/custom_components/meteocat/const.py +55 -55
  12. package/custom_components/meteocat/coordinator.py +194 -195
  13. package/custom_components/meteocat/entity.py +98 -98
  14. package/custom_components/meteocat/helpers.py +42 -42
  15. package/custom_components/meteocat/manifest.json +12 -12
  16. package/custom_components/meteocat/options_flow.py +71 -71
  17. package/custom_components/meteocat/sensor.py +325 -303
  18. package/custom_components/meteocat/strings.json +25 -25
  19. package/custom_components/meteocat/translations/ca.json +25 -25
  20. package/custom_components/meteocat/translations/en.json +25 -25
  21. package/custom_components/meteocat/translations/es.json +25 -25
  22. package/custom_components/meteocat/version.py +2 -2
  23. package/filetree.py +48 -48
  24. package/filetree.txt +46 -46
  25. package/hacs.json +5 -5
  26. package/package.json +22 -22
  27. package/poetry.lock +3210 -3216
  28. package/pyproject.toml +64 -64
  29. package/releaserc.json +17 -17
  30. package/requirements.test.txt +3 -3
  31. package/setup.cfg +64 -64
  32. package/setup.py +10 -10
  33. package/tests/bandit.yaml +17 -17
  34. package/tests/conftest.py +19 -19
  35. package/tests/test_init.py +9 -9
@@ -1,303 +1,325 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import dataclass
4
- from datetime import datetime
5
- from homeassistant.helpers.entity import DeviceInfo
6
- from homeassistant.components.sensor import (
7
- SensorDeviceClass,
8
- SensorEntity,
9
- SensorEntityDescription,
10
- SensorStateClass,
11
- )
12
- from homeassistant.core import callback
13
- from homeassistant.helpers.entity_platform import AddEntitiesCallback
14
- from homeassistant.helpers.update_coordinator import CoordinatorEntity
15
- from homeassistant.const import (
16
- DEGREE,
17
- PERCENTAGE,
18
- UnitOfPressure,
19
- UnitOfSpeed,
20
- UnitOfTemperature,
21
- UnitOfVolumetricFlux,
22
- UnitOfIrradiance,
23
- )
24
-
25
- from .const import (
26
- DOMAIN,
27
- TOWN_NAME,
28
- TOWN_ID,
29
- STATION_NAME,
30
- STATION_ID,
31
- WIND_SPEED,
32
- WIND_DIRECTION,
33
- TEMPERATURE,
34
- HUMIDITY,
35
- PRESSURE,
36
- PRECIPITATION,
37
- SOLAR_GLOBAL_IRRADIANCE,
38
- UV_INDEX,
39
- MAX_TEMPERATURE,
40
- MIN_TEMPERATURE,
41
- WIND_GUST,
42
- STATION_TIMESTAMP,
43
- WIND_SPEED_CODE,
44
- WIND_DIRECTION_CODE,
45
- TEMPERATURE_CODE,
46
- HUMIDITY_CODE,
47
- PRESSURE_CODE,
48
- PRECIPITATION_CODE,
49
- SOLAR_GLOBAL_IRRADIANCE_CODE,
50
- UV_INDEX_CODE,
51
- MAX_TEMPERATURE_CODE,
52
- MIN_TEMPERATURE_CODE,
53
- WIND_GUST_CODE,
54
- )
55
-
56
- from .coordinator import MeteocatSensorCoordinator
57
-
58
- @dataclass
59
- class MeteocatSensorEntityDescription(SensorEntityDescription):
60
- """A class that describes Meteocat sensor entities."""
61
-
62
- SENSOR_TYPES: tuple[MeteocatSensorEntityDescription, ...] = (
63
- # Sensores dinámicos
64
- MeteocatSensorEntityDescription(
65
- key=WIND_SPEED,
66
- name="Wind Speed",
67
- icon="mdi:weather-windy",
68
- device_class=SensorDeviceClass.WIND_SPEED,
69
- state_class=SensorStateClass.MEASUREMENT,
70
- native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
71
- ),
72
- MeteocatSensorEntityDescription(
73
- key=WIND_DIRECTION,
74
- name="Wind Direction",
75
- icon="mdi:compass",
76
- device_class=None,
77
- ),
78
- MeteocatSensorEntityDescription(
79
- key=TEMPERATURE,
80
- name="Temperature",
81
- icon="mdi:thermometer",
82
- device_class=SensorDeviceClass.TEMPERATURE,
83
- state_class=SensorStateClass.MEASUREMENT,
84
- native_unit_of_measurement=UnitOfTemperature.CELSIUS,
85
- ),
86
- MeteocatSensorEntityDescription(
87
- key=HUMIDITY,
88
- name="Humidity",
89
- icon="mdi:water-percent",
90
- device_class=SensorDeviceClass.HUMIDITY,
91
- state_class=SensorStateClass.MEASUREMENT,
92
- native_unit_of_measurement=PERCENTAGE,
93
- ),
94
- MeteocatSensorEntityDescription(
95
- key=PRESSURE,
96
- name="Pressure",
97
- icon="mdi:gauge",
98
- device_class=SensorDeviceClass.ATMOSPHERIC_PRESSURE,
99
- state_class=SensorStateClass.MEASUREMENT,
100
- native_unit_of_measurement=UnitOfPressure.HPA,
101
- ),
102
- MeteocatSensorEntityDescription(
103
- key=PRECIPITATION,
104
- name="Precipitation",
105
- icon="mdi:weather-rainy",
106
- device_class=None,
107
- state_class=SensorStateClass.MEASUREMENT,
108
- native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
109
- ),
110
- MeteocatSensorEntityDescription(
111
- key=SOLAR_GLOBAL_IRRADIANCE,
112
- name="Solar Global Irradiance",
113
- icon="mdi:weather-sunny",
114
- device_class=SensorDeviceClass.IRRADIANCE,
115
- state_class=SensorStateClass.MEASUREMENT,
116
- native_unit_of_measurement = UnitOfIrradiance.WATTS_PER_SQUARE_METER,
117
- ),
118
- MeteocatSensorEntityDescription(
119
- key=UV_INDEX,
120
- name="UV Index",
121
- icon="mdi:weather-sunny",
122
- ),
123
- MeteocatSensorEntityDescription(
124
- key=MAX_TEMPERATURE,
125
- name="Max Temperature",
126
- icon="mdi:thermometer-plus",
127
- device_class=SensorDeviceClass.TEMPERATURE,
128
- state_class=SensorStateClass.MEASUREMENT,
129
- native_unit_of_measurement=UnitOfTemperature.CELSIUS,
130
- ),
131
- MeteocatSensorEntityDescription(
132
- key=MIN_TEMPERATURE,
133
- name="Min Temperature",
134
- icon="mdi:thermometer-minus",
135
- device_class=SensorDeviceClass.TEMPERATURE,
136
- state_class=SensorStateClass.MEASUREMENT,
137
- native_unit_of_measurement=UnitOfTemperature.CELSIUS,
138
- ),
139
- MeteocatSensorEntityDescription(
140
- key=WIND_GUST,
141
- name="Wind Gust",
142
- icon="mdi:weather-windy",
143
- device_class=SensorDeviceClass.WIND_SPEED,
144
- state_class=SensorStateClass.MEASUREMENT,
145
- native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
146
- ),
147
- # Sensores estáticos
148
- MeteocatSensorEntityDescription(
149
- key=TOWN_NAME,
150
- name="Town Name",
151
- icon="mdi:home-city",
152
- ),
153
- MeteocatSensorEntityDescription(
154
- key=TOWN_ID,
155
- name="Town ID",
156
- icon="mdi:identifier",
157
- ),
158
- MeteocatSensorEntityDescription(
159
- key=STATION_NAME,
160
- name="Station Name",
161
- icon="mdi:broadcast",
162
- ),
163
- MeteocatSensorEntityDescription(
164
- key=STATION_ID,
165
- name="Station ID",
166
- icon="mdi:identifier",
167
- ),
168
- MeteocatSensorEntityDescription(
169
- key=STATION_TIMESTAMP,
170
- name="Station Timestamp",
171
- icon="mdi:calendar-clock",
172
- device_class=SensorDeviceClass.TIMESTAMP,
173
- )
174
- )
175
-
176
-
177
- @callback
178
- async def async_setup_entry(hass, entry, async_add_entities: AddEntitiesCallback) -> None:
179
- """Set up Meteocat sensors from a config entry."""
180
- entry_data = hass.data[DOMAIN][entry.entry_id]
181
- coordinator = entry_data["sensor_coordinator"]
182
-
183
- async_add_entities(
184
- MeteocatSensor(coordinator, description, entry_data)
185
- for description in SENSOR_TYPES
186
- )
187
-
188
- class MeteocatSensor(CoordinatorEntity[MeteocatSensorCoordinator], SensorEntity):
189
- """Representation of a Meteocat sensor."""
190
- STATIC_KEYS = {TOWN_NAME, TOWN_ID, STATION_NAME, STATION_ID}
191
-
192
- CODE_MAPPING = {
193
- WIND_SPEED: WIND_SPEED_CODE,
194
- WIND_DIRECTION: WIND_DIRECTION_CODE,
195
- TEMPERATURE: TEMPERATURE_CODE,
196
- HUMIDITY: HUMIDITY_CODE,
197
- PRESSURE: PRESSURE_CODE,
198
- PRECIPITATION: PRECIPITATION_CODE,
199
- SOLAR_GLOBAL_IRRADIANCE: SOLAR_GLOBAL_IRRADIANCE_CODE,
200
- UV_INDEX: UV_INDEX_CODE,
201
- MAX_TEMPERATURE: MAX_TEMPERATURE_CODE,
202
- MIN_TEMPERATURE: MIN_TEMPERATURE_CODE,
203
- WIND_GUST: WIND_GUST_CODE,
204
- }
205
-
206
- def __init__(self, coordinator, description, entry_data):
207
- """Initialize the sensor."""
208
- super().__init__(coordinator)
209
- self.entity_description = description
210
- self.api_key = entry_data["api_key"]
211
- self._town_name = entry_data["town_name"]
212
- self._town_id = entry_data["town_id"]
213
- self._station_name = entry_data["station_name"]
214
- self._station_id = entry_data["station_id"]
215
-
216
- # Unique ID for the entity
217
- self._attr_unique_id = f"{self._town_id}_{self.entity_description.key}"
218
-
219
- @property
220
- def native_value(self):
221
- """Return the state of the sensor."""
222
- # Información estática
223
- if self.entity_description.key in self.STATIC_KEYS:
224
- # Información estática del `entry_data`
225
- if self.entity_description.key == TOWN_NAME:
226
- return self._town_name
227
- if self.entity_description.key == TOWN_ID:
228
- return self._town_id
229
- if self.entity_description.key == STATION_NAME:
230
- return self._station_name
231
- if self.entity_description.key == STATION_ID:
232
- return self._station_id
233
- # Información dinámica
234
- sensor_code = self.CODE_MAPPING.get(self.entity_description.key)
235
-
236
- if sensor_code is not None:
237
- # Accedemos a las estaciones en el JSON recibido
238
- stations = self.coordinator.data or []
239
- for station in stations:
240
- variables = station.get("variables", [])
241
-
242
- # Filtramos por código
243
- variable_data = next(
244
- (var for var in variables if var.get("codi") == sensor_code),
245
- None,
246
- )
247
-
248
- if variable_data:
249
- # Obtenemos la última lectura
250
- lectures = variable_data.get("lectures", [])
251
- if lectures:
252
- latest_reading = lectures[-1]
253
- value = latest_reading.get("valor")
254
-
255
- # Convertimos grados a dirección cardinal para WIND_DIRECTION
256
- if self.entity_description.key == WIND_DIRECTION and isinstance(value, (int, float)):
257
- return self._convert_degrees_to_cardinal(value)
258
-
259
- return value
260
- # Lógica específica para el sensor de timestamp
261
- if self.entity_description.key == "station_timestamp":
262
- stations = self.coordinator.data or []
263
- for station in stations:
264
- variables = station.get("variables", [])
265
- for variable in variables:
266
- lectures = variable.get("lectures", [])
267
- if lectures:
268
- # Obtenemos el campo `data` de la última lectura
269
- latest_reading = lectures[-1]
270
- raw_timestamp = latest_reading.get("data")
271
-
272
- if raw_timestamp:
273
- # Convertir el timestamp a un objeto datetime
274
- try:
275
- return datetime.fromisoformat(raw_timestamp.replace("Z", "+00:00"))
276
- except ValueError:
277
- # Manejo de errores si el formato no es válido
278
- return None
279
-
280
- return None
281
-
282
- @staticmethod
283
- def _convert_degrees_to_cardinal(degree: float) -> str:
284
- """Convert degrees to cardinal direction."""
285
- if not isinstance(degree, (int, float)):
286
- return "Unknown" # Retorna "Unknown" si el valor no es un número válido
287
-
288
- directions = [
289
- "N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE",
290
- "S", "SSO", "SO", "OSO", "O", "ONO", "NO", "NNO", "N",
291
- ]
292
- index = round(degree / 22.5) % 16
293
- return directions[index]
294
-
295
- @property
296
- def device_info(self) -> DeviceInfo:
297
- """Return the device info."""
298
- return DeviceInfo(
299
- identifiers={(DOMAIN, self._town_id)},
300
- name=self._town_name,
301
- manufacturer="Meteocat",
302
- model="Meteocat API",
303
- )
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from datetime import datetime
5
+ import logging
6
+ from homeassistant.helpers.entity import (
7
+ DeviceInfo,
8
+ EntityCategory,
9
+ )
10
+ from homeassistant.components.sensor import (
11
+ SensorDeviceClass,
12
+ SensorEntity,
13
+ SensorEntityDescription,
14
+ SensorStateClass,
15
+ )
16
+ from homeassistant.core import callback
17
+ from homeassistant.helpers.entity_platform import AddEntitiesCallback
18
+ from homeassistant.helpers.update_coordinator import CoordinatorEntity
19
+ from homeassistant.const import (
20
+ DEGREE,
21
+ PERCENTAGE,
22
+ UnitOfPressure,
23
+ UnitOfSpeed,
24
+ UnitOfTemperature,
25
+ UnitOfVolumetricFlux,
26
+ UnitOfIrradiance,
27
+ )
28
+
29
+ from .const import (
30
+ DOMAIN,
31
+ TOWN_NAME,
32
+ TOWN_ID,
33
+ STATION_NAME,
34
+ STATION_ID,
35
+ WIND_SPEED,
36
+ WIND_DIRECTION,
37
+ TEMPERATURE,
38
+ HUMIDITY,
39
+ PRESSURE,
40
+ PRECIPITATION,
41
+ SOLAR_GLOBAL_IRRADIANCE,
42
+ UV_INDEX,
43
+ MAX_TEMPERATURE,
44
+ MIN_TEMPERATURE,
45
+ WIND_GUST,
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,
58
+ )
59
+
60
+ from .coordinator import MeteocatSensorCoordinator
61
+
62
+ _LOGGER = logging.getLogger(__name__)
63
+
64
+ @dataclass
65
+ class MeteocatSensorEntityDescription(SensorEntityDescription):
66
+ """A class that describes Meteocat sensor entities."""
67
+
68
+ SENSOR_TYPES: tuple[MeteocatSensorEntityDescription, ...] = (
69
+ # Sensores dinámicos
70
+ MeteocatSensorEntityDescription(
71
+ key=WIND_SPEED,
72
+ name="Wind Speed",
73
+ icon="mdi:weather-windy",
74
+ device_class=SensorDeviceClass.WIND_SPEED,
75
+ state_class=SensorStateClass.MEASUREMENT,
76
+ native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
77
+ ),
78
+ MeteocatSensorEntityDescription(
79
+ key=WIND_DIRECTION,
80
+ name="Wind Direction",
81
+ icon="mdi:compass",
82
+ device_class=None,
83
+ ),
84
+ MeteocatSensorEntityDescription(
85
+ key=TEMPERATURE,
86
+ name="Temperature",
87
+ icon="mdi:thermometer",
88
+ device_class=SensorDeviceClass.TEMPERATURE,
89
+ state_class=SensorStateClass.MEASUREMENT,
90
+ native_unit_of_measurement=UnitOfTemperature.CELSIUS,
91
+ ),
92
+ MeteocatSensorEntityDescription(
93
+ key=HUMIDITY,
94
+ name="Humidity",
95
+ icon="mdi:water-percent",
96
+ device_class=SensorDeviceClass.HUMIDITY,
97
+ state_class=SensorStateClass.MEASUREMENT,
98
+ native_unit_of_measurement=PERCENTAGE,
99
+ ),
100
+ MeteocatSensorEntityDescription(
101
+ key=PRESSURE,
102
+ name="Pressure",
103
+ icon="mdi:gauge",
104
+ device_class=SensorDeviceClass.ATMOSPHERIC_PRESSURE,
105
+ state_class=SensorStateClass.MEASUREMENT,
106
+ native_unit_of_measurement=UnitOfPressure.HPA,
107
+ ),
108
+ MeteocatSensorEntityDescription(
109
+ key=PRECIPITATION,
110
+ name="Precipitation",
111
+ icon="mdi:weather-rainy",
112
+ device_class=None,
113
+ state_class=SensorStateClass.MEASUREMENT,
114
+ native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
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
+ ),
124
+ MeteocatSensorEntityDescription(
125
+ key=UV_INDEX,
126
+ name="UV Index",
127
+ icon="mdi:weather-sunny",
128
+ ),
129
+ MeteocatSensorEntityDescription(
130
+ key=MAX_TEMPERATURE,
131
+ name="Max Temperature",
132
+ icon="mdi:thermometer-plus",
133
+ device_class=SensorDeviceClass.TEMPERATURE,
134
+ state_class=SensorStateClass.MEASUREMENT,
135
+ native_unit_of_measurement=UnitOfTemperature.CELSIUS,
136
+ ),
137
+ MeteocatSensorEntityDescription(
138
+ key=MIN_TEMPERATURE,
139
+ name="Min Temperature",
140
+ icon="mdi:thermometer-minus",
141
+ device_class=SensorDeviceClass.TEMPERATURE,
142
+ state_class=SensorStateClass.MEASUREMENT,
143
+ native_unit_of_measurement=UnitOfTemperature.CELSIUS,
144
+ ),
145
+ MeteocatSensorEntityDescription(
146
+ key=WIND_GUST,
147
+ name="Wind Gust",
148
+ icon="mdi:weather-windy",
149
+ device_class=SensorDeviceClass.WIND_SPEED,
150
+ state_class=SensorStateClass.MEASUREMENT,
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,
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
+ )
184
+ )
185
+
186
+
187
+ @callback
188
+ async def async_setup_entry(hass, entry, async_add_entities: AddEntitiesCallback) -> None:
189
+ """Set up Meteocat sensors from a config entry."""
190
+ entry_data = hass.data[DOMAIN][entry.entry_id]
191
+ coordinator = entry_data["sensor_coordinator"]
192
+
193
+ async_add_entities(
194
+ MeteocatSensor(coordinator, description, entry_data)
195
+ for description in SENSOR_TYPES
196
+ )
197
+
198
+ class MeteocatSensor(CoordinatorEntity[MeteocatSensorCoordinator], SensorEntity):
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
217
+
218
+ def __init__(self, coordinator, description, entry_data):
219
+ """Initialize the sensor."""
220
+ super().__init__(coordinator)
221
+ self.entity_description = description
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"]
227
+
228
+ # Unique ID for the entity
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
+ )
240
+
241
+ @property
242
+ def native_value(self):
243
+ """Return the state of the sensor."""
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
+
258
+ if sensor_code is not None:
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")
276
+
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)
280
+
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
301
+
302
+ return None
303
+
304
+ @staticmethod
305
+ def _convert_degrees_to_cardinal(degree: float) -> str:
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
+
310
+ directions = [
311
+ "N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE",
312
+ "S", "SSO", "SO", "OSO", "O", "ONO", "NO", "NNO", "N",
313
+ ]
314
+ index = round(degree / 22.5) % 16
315
+ return directions[index]
316
+
317
+ @property
318
+ def device_info(self) -> DeviceInfo:
319
+ """Return the device info."""
320
+ return DeviceInfo(
321
+ identifiers={(DOMAIN, self._town_id)},
322
+ name=self._town_name,
323
+ manufacturer="Meteocat",
324
+ model="Meteocat API",
325
+ )