meteocat 3.1.0 → 4.0.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 (57) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +45 -45
  2. package/.github/ISSUE_TEMPLATE/config.yml +8 -8
  3. package/.github/ISSUE_TEMPLATE/improvement.md +39 -39
  4. package/.github/ISSUE_TEMPLATE/new_function.md +41 -41
  5. package/.github/labels.yml +63 -63
  6. package/.github/workflows/autocloser.yaml +27 -27
  7. package/.github/workflows/close-on-label.yml +48 -48
  8. package/.github/workflows/force-sync-labels.yml +18 -18
  9. package/.github/workflows/hassfest.yaml +13 -13
  10. package/.github/workflows/publish-zip.yml +67 -67
  11. package/.github/workflows/release.yml +41 -41
  12. package/.github/workflows/stale.yml +63 -63
  13. package/.github/workflows/sync-gitlab.yml +107 -107
  14. package/.github/workflows/sync-labels.yml +21 -21
  15. package/.github/workflows/validate.yaml +16 -16
  16. package/.pre-commit-config.yaml +37 -37
  17. package/.releaserc +37 -37
  18. package/AUTHORS.md +13 -13
  19. package/CHANGELOG.md +954 -898
  20. package/README.md +207 -204
  21. package/conftest.py +11 -11
  22. package/custom_components/meteocat/__init__.py +298 -293
  23. package/custom_components/meteocat/condition.py +63 -59
  24. package/custom_components/meteocat/config_flow.py +613 -435
  25. package/custom_components/meteocat/const.py +132 -120
  26. package/custom_components/meteocat/coordinator.py +1040 -205
  27. package/custom_components/meteocat/helpers.py +58 -63
  28. package/custom_components/meteocat/manifest.json +25 -24
  29. package/custom_components/meteocat/options_flow.py +287 -277
  30. package/custom_components/meteocat/sensor.py +366 -4
  31. package/custom_components/meteocat/strings.json +1058 -867
  32. package/custom_components/meteocat/translations/ca.json +1058 -867
  33. package/custom_components/meteocat/translations/en.json +1058 -867
  34. package/custom_components/meteocat/translations/es.json +1058 -867
  35. package/custom_components/meteocat/version.py +1 -1
  36. package/custom_components/meteocat/weather.py +218 -218
  37. package/filetree.py +48 -48
  38. package/filetree.txt +79 -70
  39. package/hacs.json +8 -8
  40. package/images/daily_forecast_2_alerts.png +0 -0
  41. package/images/daily_forecast_no_alerts.png +0 -0
  42. package/images/diagnostic_sensors.png +0 -0
  43. package/images/dynamic_sensors.png +0 -0
  44. package/images/options.png +0 -0
  45. package/images/regenerate_assets.png +0 -0
  46. package/images/setup_options.png +0 -0
  47. package/images/system_options.png +0 -0
  48. package/info.md +11 -11
  49. package/package.json +22 -22
  50. package/poetry.lock +3222 -3222
  51. package/pyproject.toml +68 -68
  52. package/requirements.test.txt +3 -3
  53. package/setup.cfg +64 -64
  54. package/setup.py +10 -10
  55. package/tests/bandit.yaml +17 -17
  56. package/tests/conftest.py +19 -19
  57. package/tests/test_init.py +9 -9
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  from dataclasses import dataclass
4
4
  from datetime import datetime, timezone, time, timedelta
5
5
  from zoneinfo import ZoneInfo
6
+ from typing import Dict, Any, Optional
6
7
  import os
7
8
  import json
8
9
  import aiofiles
@@ -105,9 +106,14 @@ from .const import (
105
106
  DEFAULT_LIGHTNING_VALIDITY_TIME,
106
107
  DEFAULT_LIGHTNING_VALIDITY_HOURS,
107
108
  DEFAULT_LIGHTNING_VALIDITY_MINUTES,
109
+ SUN,
108
110
  SUNRISE,
109
111
  SUNSET,
110
112
  SUN_FILE_STATUS,
113
+ MOON_PHASE,
114
+ MOON_FILE_STATUS,
115
+ MOONRISE,
116
+ MOONSET,
111
117
  )
112
118
 
113
119
  from .coordinator import (
@@ -127,6 +133,8 @@ from .coordinator import (
127
133
  MeteocatLightningFileCoordinator,
128
134
  MeteocatSunCoordinator,
129
135
  MeteocatSunFileCoordinator,
136
+ MeteocatMoonCoordinator,
137
+ MeteocatMoonFileCoordinator,
130
138
  )
131
139
 
132
140
  # Definir la zona horaria local
@@ -437,7 +445,13 @@ SENSOR_TYPES: tuple[MeteocatSensorEntityDescription, ...] = (
437
445
  icon="mdi:counter",
438
446
  entity_category=EntityCategory.DIAGNOSTIC,
439
447
  ),
440
- # Nuevos sensores de sol
448
+ MeteocatSensorEntityDescription(
449
+ key=SUN,
450
+ translation_key="sun",
451
+ icon="mdi:weather-sunny",
452
+ device_class=SensorDeviceClass.ENUM,
453
+ options=["above_horizon", "below_horizon"],
454
+ ),
441
455
  MeteocatSensorEntityDescription(
442
456
  key=SUNRISE,
443
457
  translation_key="sunrise",
@@ -456,6 +470,41 @@ SENSOR_TYPES: tuple[MeteocatSensorEntityDescription, ...] = (
456
470
  icon="mdi:update",
457
471
  entity_category=EntityCategory.DIAGNOSTIC,
458
472
  ),
473
+ MeteocatSensorEntityDescription(
474
+ key=MOON_PHASE,
475
+ translation_key="moon_phase",
476
+ device_class=SensorDeviceClass.ENUM,
477
+ options=[
478
+ "new_moon",
479
+ "waxing_crescent",
480
+ "first_quarter",
481
+ "waxing_gibbous",
482
+ "full_moon",
483
+ "waning_gibbous",
484
+ "last_quarter",
485
+ "waning_crescent",
486
+ "unknown",
487
+ ],
488
+ state_class=None,
489
+ ),
490
+ MeteocatSensorEntityDescription(
491
+ key=MOON_FILE_STATUS,
492
+ translation_key="moon_file_status",
493
+ icon="mdi:update",
494
+ entity_category=EntityCategory.DIAGNOSTIC,
495
+ ),
496
+ MeteocatSensorEntityDescription(
497
+ key=MOONRISE,
498
+ translation_key="moonrise",
499
+ icon="mdi:weather-moonset-up",
500
+ device_class=SensorDeviceClass.TIMESTAMP,
501
+ ),
502
+ MeteocatSensorEntityDescription(
503
+ key=MOONSET,
504
+ translation_key="moonset",
505
+ icon="mdi:weather-moonset-down",
506
+ device_class=SensorDeviceClass.TIMESTAMP,
507
+ ),
459
508
  )
460
509
 
461
510
  @callback
@@ -480,6 +529,8 @@ async def async_setup_entry(hass, entry, async_add_entities: AddEntitiesCallback
480
529
  lightning_file_coordinator = entry_data.get("lightning_file_coordinator")
481
530
  sun_coordinator = entry_data.get("sun_coordinator")
482
531
  sun_file_coordinator = entry_data.get("sun_file_coordinator")
532
+ moon_coordinator = entry_data.get("moon_coordinator")
533
+ moon_file_coordinator = entry_data.get("moon_file_coordinator")
483
534
 
484
535
  # Sensores generales
485
536
  async_add_entities(
@@ -624,6 +675,13 @@ async def async_setup_entry(hass, entry, async_add_entities: AddEntitiesCallback
624
675
  if description.key == SUN_FILE_STATUS
625
676
  )
626
677
 
678
+ # Sensor de posición del sol
679
+ async_add_entities(
680
+ MeteocatSunPositionSensor(sun_file_coordinator, description, entry_data)
681
+ for description in SENSOR_TYPES
682
+ if description.key == SUN
683
+ )
684
+
627
685
  # Sensores de sol
628
686
  async_add_entities(
629
687
  MeteocatSunSensor(sun_file_coordinator, description, entry_data)
@@ -631,6 +689,26 @@ async def async_setup_entry(hass, entry, async_add_entities: AddEntitiesCallback
631
689
  if description.key in {SUNRISE, SUNSET}
632
690
  )
633
691
 
692
+ # Sensor de fase lunar
693
+ async_add_entities(
694
+ MeteocatMoonSensor(moon_file_coordinator, description, entry_data)
695
+ for description in SENSOR_TYPES
696
+ if description.key == MOON_PHASE
697
+ )
698
+
699
+ # Sensor de estado de archivo lunar
700
+ async_add_entities(
701
+ MeteocatMoonStatusSensor(moon_coordinator, description, entry_data)
702
+ for description in SENSOR_TYPES
703
+ if description.key == MOON_FILE_STATUS
704
+ )
705
+
706
+ # Sensores de salida y puesta de la luna
707
+ async_add_entities(
708
+ MeteocatMoonTimeSensor(moon_file_coordinator, description, entry_data)
709
+ for description in SENSOR_TYPES
710
+ if description.key in {MOONRISE, MOONSET}
711
+ )
634
712
 
635
713
  # Cambiar UTC a la zona horaria local
636
714
  def convert_to_local_time(utc_time: str, local_tz: str = "Europe/Madrid") -> datetime | None:
@@ -1376,7 +1454,7 @@ class MeteocatAlertRegionSensor(CoordinatorEntity[MeteocatAlertsRegionCoordinato
1376
1454
  _LOGGER.warning("Meteor desconocido sin mapeo: '%s'. Añadirlo a 'METEOR_MAPPING' del coordinador 'MeteocatAlertRegionSensor' si es necesario.", meteor)
1377
1455
  mapped_name = "unknown"
1378
1456
  attributes[f"alert_{i+1}"] = mapped_name
1379
- _LOGGER.info("Atributos traducidos del sensor: %s", attributes)
1457
+ _LOGGER.debug("Atributos traducidos del sensor: %s", attributes)
1380
1458
  return attributes
1381
1459
 
1382
1460
  @property
@@ -1770,6 +1848,83 @@ class MeteocatLightningSensor(CoordinatorEntity[MeteocatLightningFileCoordinator
1770
1848
  model="Meteocat API",
1771
1849
  )
1772
1850
 
1851
+ class MeteocatSunPositionSensor(CoordinatorEntity[MeteocatSunFileCoordinator], SensorEntity):
1852
+ """Representation of Meteocat Sun position sensor."""
1853
+ _attr_has_entity_name = True
1854
+ entity_description: MeteocatSensorEntityDescription
1855
+
1856
+ def __init__(self, coordinator, description, entry_data):
1857
+ """Initialize the Sun position sensor."""
1858
+ super().__init__(coordinator)
1859
+ self.entity_description = description
1860
+ self._town_name = entry_data.get(TOWN_NAME)
1861
+ self._town_id = entry_data.get(TOWN_ID)
1862
+ self._station_id = entry_data.get(STATION_ID)
1863
+ self._attr_unique_id = f"{DOMAIN}_{self._town_id}_{description.key}"
1864
+
1865
+ _LOGGER.debug(
1866
+ "Inicializando sensor de posición solar: %s (ID: %s)",
1867
+ description.translation_key, self._attr_unique_id
1868
+ )
1869
+
1870
+ @property
1871
+ def native_value(self) -> str | None:
1872
+ """Return 'above_horizon' or 'below_horizon'."""
1873
+ return self.coordinator.data.get("sun_horizon_position")
1874
+
1875
+ @property
1876
+ def extra_state_attributes(self) -> dict[str, Any]:
1877
+ """Return all sun-related attributes as raw values (datetime strings or numbers)."""
1878
+ data = self.coordinator.data
1879
+ attrs = {}
1880
+
1881
+ # === POSICIÓN ACTUAL ===
1882
+ attrs["elevation"] = data.get("sun_elevation")
1883
+ attrs["azimuth"] = data.get("sun_azimuth")
1884
+ attrs["rising"] = data.get("sun_rising")
1885
+
1886
+ # === DURACIÓN DE LUZ DIURNA EN H:M:S (justo antes de daylight_duration) ===
1887
+ daylight_duration = data.get("daylight_duration")
1888
+ if isinstance(daylight_duration, (int, float)) and daylight_duration >= 0:
1889
+ total_seconds = int(round(daylight_duration * 3600)) # Horas → segundos con redondeo
1890
+ hours = total_seconds // 3600
1891
+ minutes = (total_seconds % 3600) // 60
1892
+ seconds = total_seconds % 60
1893
+ attrs["daylight_duration_hms"] = f"{hours:02d}:{minutes:02d}:{seconds:02d}"
1894
+
1895
+ # === EVENTOS DEL DÍA (orden garantizado por inserción) ===
1896
+ event_keys = [
1897
+ "daylight_duration",
1898
+ "dawn_astronomical",
1899
+ "dawn_nautical",
1900
+ "dawn_civil",
1901
+ "sunrise",
1902
+ "noon",
1903
+ "sunset",
1904
+ "dusk_civil",
1905
+ "dusk_nautical",
1906
+ "dusk_astronomical",
1907
+ "midnight",
1908
+ ]
1909
+
1910
+ for key in event_keys:
1911
+ if key in data:
1912
+ attrs[key] = data[key]
1913
+
1914
+ attrs["last_updated"] = data.get("sun_position_updated")
1915
+
1916
+ return attrs
1917
+
1918
+ @property
1919
+ def device_info(self) -> DeviceInfo:
1920
+ """Return the device info."""
1921
+ return DeviceInfo(
1922
+ identifiers={(DOMAIN, self._town_id)},
1923
+ name=f"Meteocat {self._station_id} {self._town_name}",
1924
+ manufacturer="Meteocat",
1925
+ model="Meteocat API",
1926
+ )
1927
+
1773
1928
  class MeteocatSunSensor(CoordinatorEntity[MeteocatSunFileCoordinator], SensorEntity):
1774
1929
  """Representation of Meteocat Sun sensors (sunrise/sunset)."""
1775
1930
  _attr_has_entity_name = True
@@ -1785,7 +1940,7 @@ class MeteocatSunSensor(CoordinatorEntity[MeteocatSunFileCoordinator], SensorEnt
1785
1940
  self._attr_entity_category = getattr(description, "entity_category", None)
1786
1941
 
1787
1942
  _LOGGER.debug(
1788
- "Inicializando sensor: %s, Unique ID: %s",
1943
+ "Inicializando sensor solar: %s, Unique ID: %s",
1789
1944
  self.entity_description.name,
1790
1945
  self._attr_unique_id,
1791
1946
  )
@@ -1813,10 +1968,20 @@ class MeteocatSunSensor(CoordinatorEntity[MeteocatSunFileCoordinator], SensorEnt
1813
1968
  try:
1814
1969
  dt = datetime.fromisoformat(time_str)
1815
1970
  attributes["friendly_time"] = dt.strftime("%H:%M")
1971
+ attributes["friendly_date"] = dt.strftime("%Y-%m-%d")
1972
+
1973
+ # Día base en inglés (minúsculas)
1974
+ day_key = dt.strftime("%A").lower()
1975
+ attributes["friendly_day"] = day_key
1976
+
1816
1977
  except ValueError:
1817
1978
  attributes["friendly_time"] = None
1979
+ attributes["friendly_date"] = None
1980
+ attributes["friendly_day"] = None
1818
1981
  else:
1819
1982
  attributes["friendly_time"] = None
1983
+ attributes["friendly_date"] = None
1984
+ attributes["friendly_day"] = None
1820
1985
  return attributes
1821
1986
 
1822
1987
  @property
@@ -1851,7 +2016,7 @@ class MeteocatSunStatusSensor(CoordinatorEntity[MeteocatSunCoordinator], SensorE
1851
2016
 
1852
2017
  def _get_data_update(self):
1853
2018
  """Obtain the update date from the coordinator and convert to UTC."""
1854
- data_update = self.coordinator.data.get("actualizado")
2019
+ data_update = self.coordinator.data.get("actualitzat", {}).get("dataUpdate")
1855
2020
  if data_update:
1856
2021
  try:
1857
2022
  local_time = datetime.fromisoformat(data_update)
@@ -1889,3 +2054,200 @@ class MeteocatSunStatusSensor(CoordinatorEntity[MeteocatSunCoordinator], SensorE
1889
2054
  manufacturer="Meteocat",
1890
2055
  model="Meteocat API",
1891
2056
  )
2057
+
2058
+ class MeteocatMoonSensor(CoordinatorEntity[MeteocatMoonFileCoordinator], SensorEntity):
2059
+ """Representation of Meteocat Moon sensor (moon phase)."""
2060
+ _attr_has_entity_name = True
2061
+
2062
+ def __init__(self, moon_file_coordinator, description, entry_data):
2063
+ """Initialize the Moon sensor."""
2064
+ super().__init__(moon_file_coordinator)
2065
+ self.entity_description = description
2066
+ self._town_name = entry_data["town_name"]
2067
+ self._town_id = entry_data["town_id"]
2068
+ self._station_id = entry_data["station_id"]
2069
+ self._attr_unique_id = f"sensor.{DOMAIN}_{self._town_id}_{self.entity_description.key}"
2070
+ self._attr_entity_category = getattr(description, "entity_category", None)
2071
+
2072
+ _LOGGER.debug(
2073
+ "Inicializando sensor: %s, Unique ID: %s",
2074
+ self.entity_description.name,
2075
+ self._attr_unique_id,
2076
+ )
2077
+
2078
+ @property
2079
+ def native_value(self):
2080
+ """Return the moon phase name as the state."""
2081
+ return self.coordinator.data.get("moon_phase_name")
2082
+
2083
+ @property
2084
+ def extra_state_attributes(self):
2085
+ """Return additional attributes for the sensor."""
2086
+ attributes = super().extra_state_attributes or {}
2087
+ attributes["moon_day"] = self.coordinator.data.get("moon_day")
2088
+ attributes["moon_phase_value"] = self.coordinator.data.get("moon_phase")
2089
+ attributes["illuminated_percentage"] = self.coordinator.data.get("illuminated_percentage")
2090
+ attributes["moon_distance"] = self.coordinator.data.get("moon_distance")
2091
+ attributes["moon_angular_diameter"] = self.coordinator.data.get("moon_angular_diameter")
2092
+ attributes["lunation"] = self.coordinator.data.get("lunation")
2093
+ attributes["lunation_duration"] = self.coordinator.data.get("lunation_duration")
2094
+ attributes["last_updated"] = self.coordinator.data.get("last_lunar_update_date")
2095
+ return attributes
2096
+
2097
+ @property
2098
+ def icon(self):
2099
+ """Return the icon based on the moon phase."""
2100
+ phase = self.coordinator.data.get("moon_phase_name")
2101
+ icon_map = {
2102
+ "new_moon": "mdi:moon-new",
2103
+ "waxing_crescent": "mdi:moon-waxing-crescent",
2104
+ "first_quarter": "mdi:moon-first-quarter",
2105
+ "waxing_gibbous": "mdi:moon-waxing-gibbous",
2106
+ "full_moon": "mdi:moon-full",
2107
+ "waning_gibbous": "mdi:moon-waning-gibbous",
2108
+ "last_quarter": "mdi:moon-last-quarter",
2109
+ "waning_crescent": "mdi:moon-waning-crescent",
2110
+ "unknown": "mdi:moon",
2111
+ }
2112
+ return icon_map.get(phase, "mdi:moon")
2113
+
2114
+ @property
2115
+ def device_info(self) -> DeviceInfo:
2116
+ """Return the device info."""
2117
+ return DeviceInfo(
2118
+ identifiers={(DOMAIN, self._town_id)},
2119
+ name=f"Meteocat {self._station_id} {self._town_name}",
2120
+ manufacturer="Meteocat",
2121
+ model="Meteocat API",
2122
+ )
2123
+
2124
+ class MeteocatMoonStatusSensor(CoordinatorEntity[MeteocatMoonCoordinator], SensorEntity):
2125
+ """Representation of Meteocat Moon file status sensor."""
2126
+ _attr_has_entity_name = True
2127
+
2128
+ def __init__(self, moon_coordinator, description, entry_data):
2129
+ """Initialize the Moon status sensor."""
2130
+ super().__init__(moon_coordinator)
2131
+ self.entity_description = description
2132
+ self._town_name = entry_data["town_name"]
2133
+ self._town_id = entry_data["town_id"]
2134
+ self._station_id = entry_data["station_id"]
2135
+ self._attr_unique_id = f"sensor.{DOMAIN}_{self._town_id}_moon_status"
2136
+ self._attr_entity_category = getattr(description, "entity_category", None)
2137
+
2138
+ _LOGGER.debug(
2139
+ "Inicializando sensor: %s, Unique ID: %s",
2140
+ self.entity_description.name,
2141
+ self._attr_unique_id,
2142
+ )
2143
+
2144
+ def _get_data_update(self):
2145
+ """Obtain the update date from the coordinator and convert to UTC."""
2146
+ data_update = self.coordinator.data.get("actualizado")
2147
+ if data_update:
2148
+ try:
2149
+ local_time = datetime.fromisoformat(data_update)
2150
+ return local_time.astimezone(ZoneInfo("UTC"))
2151
+ except ValueError:
2152
+ _LOGGER.error("Formato de fecha de actualización inválido: %s", data_update)
2153
+ return None
2154
+
2155
+ @property
2156
+ def native_value(self):
2157
+ """Return the status of the moon file based on the update date."""
2158
+ data_update = self._get_data_update()
2159
+ if not data_update:
2160
+ return "unknown"
2161
+ now = datetime.now(timezone.utc).astimezone(TIMEZONE)
2162
+ if (now - data_update) > timedelta(days=1):
2163
+ return "obsolete"
2164
+ return "updated"
2165
+
2166
+ @property
2167
+ def extra_state_attributes(self):
2168
+ """Return additional attributes for the sensor."""
2169
+ attributes = super().extra_state_attributes or {}
2170
+ data_update = self._get_data_update()
2171
+ if data_update:
2172
+ attributes["update_date"] = data_update.isoformat()
2173
+ return attributes
2174
+
2175
+ @property
2176
+ def device_info(self) -> DeviceInfo:
2177
+ """Return the device info."""
2178
+ return DeviceInfo(
2179
+ identifiers={(DOMAIN, self._town_id)},
2180
+ name=f"Meteocat {self._station_id} {self._town_name}",
2181
+ manufacturer="Meteocat",
2182
+ model="Meteocat API",
2183
+ )
2184
+
2185
+ class MeteocatMoonTimeSensor(CoordinatorEntity[MeteocatMoonFileCoordinator], SensorEntity):
2186
+ """Representation of Meteocat Moon time sensors (moonrise/moonset)."""
2187
+ _attr_has_entity_name = True
2188
+
2189
+ def __init__(self, moon_file_coordinator, description, entry_data):
2190
+ super().__init__(moon_file_coordinator)
2191
+ self.entity_description = description
2192
+ self._town_name = entry_data["town_name"]
2193
+ self._town_id = entry_data["town_id"]
2194
+ self._station_id = entry_data["station_id"]
2195
+ self._attr_unique_id = f"sensor.{DOMAIN}_{self._town_id}_{self.entity_description.key}"
2196
+ self._attr_entity_category = getattr(description, "entity_category", None)
2197
+
2198
+ _LOGGER.debug(
2199
+ "Inicializando sensor lunar: %s, Unique ID: %s",
2200
+ self.entity_description.name,
2201
+ self._attr_unique_id,
2202
+ )
2203
+
2204
+ @property
2205
+ def native_value(self):
2206
+ """Return the moonrise or moonset as a datetime."""
2207
+ time_str = self.coordinator.data.get(self.entity_description.key)
2208
+ if time_str:
2209
+ try:
2210
+ return datetime.fromisoformat(time_str)
2211
+ except ValueError:
2212
+ _LOGGER.error("Formato de fecha inválido para %s: %s", self.entity_description.key, time_str)
2213
+ return None
2214
+ return None
2215
+
2216
+ @property
2217
+ def extra_state_attributes(self):
2218
+ """Return additional attributes for the sensor."""
2219
+ attributes = super().extra_state_attributes or {}
2220
+ time_str = self.coordinator.data.get(self.entity_description.key)
2221
+ key = self.entity_description.key # "moonrise" o "moonset"
2222
+
2223
+ if time_str:
2224
+ try:
2225
+ dt = datetime.fromisoformat(time_str)
2226
+ # Atributos adicionales amigables
2227
+ attributes["friendly_time"] = dt.strftime("%H:%M")
2228
+ attributes["friendly_date"] = dt.strftime("%Y-%m-%d")
2229
+
2230
+ # Día base en inglés (minúsculas)
2231
+ day_key = dt.strftime("%A").lower()
2232
+ attributes["friendly_day"] = day_key
2233
+
2234
+ # No establecemos "status" aquí para evitar que HA lo muestre como "desconocido"
2235
+
2236
+ except ValueError:
2237
+ attributes["friendly_time"] = None
2238
+ attributes["friendly_date"] = None
2239
+ attributes["friendly_day"] = None
2240
+ else:
2241
+ attributes["friendly_time"] = None
2242
+ attributes["friendly_date"] = None
2243
+ attributes["friendly_day"] = None
2244
+ return attributes
2245
+
2246
+ @property
2247
+ def device_info(self) -> DeviceInfo:
2248
+ return DeviceInfo(
2249
+ identifiers={(DOMAIN, self._town_id)},
2250
+ name=f"Meteocat {self._station_id} {self._town_name}",
2251
+ manufacturer="Meteocat",
2252
+ model="Meteocat API",
2253
+ )