meteocat 3.0.0 → 3.2.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 (40) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +8 -2
  2. package/.github/ISSUE_TEMPLATE/config.yml +7 -0
  3. package/.github/ISSUE_TEMPLATE/improvement.md +39 -0
  4. package/.github/ISSUE_TEMPLATE/new_function.md +41 -0
  5. package/.github/labels.yml +63 -0
  6. package/.github/workflows/autocloser.yaml +11 -9
  7. package/.github/workflows/close-on-label.yml +48 -0
  8. package/.github/workflows/force-sync-labels.yml +18 -0
  9. package/.github/workflows/sync-gitlab.yml +15 -4
  10. package/.github/workflows/sync-labels.yml +21 -0
  11. package/CHANGELOG.md +80 -11
  12. package/README.md +16 -4
  13. package/custom_components/meteocat/__init__.py +57 -42
  14. package/custom_components/meteocat/condition.py +6 -2
  15. package/custom_components/meteocat/config_flow.py +231 -4
  16. package/custom_components/meteocat/const.py +17 -2
  17. package/custom_components/meteocat/coordinator.py +1122 -101
  18. package/custom_components/meteocat/helpers.py +31 -36
  19. package/custom_components/meteocat/manifest.json +3 -2
  20. package/custom_components/meteocat/options_flow.py +71 -3
  21. package/custom_components/meteocat/sensor.py +660 -247
  22. package/custom_components/meteocat/strings.json +252 -15
  23. package/custom_components/meteocat/translations/ca.json +249 -13
  24. package/custom_components/meteocat/translations/en.json +252 -15
  25. package/custom_components/meteocat/translations/es.json +252 -15
  26. package/custom_components/meteocat/version.py +1 -1
  27. package/filetree.txt +12 -3
  28. package/hacs.json +1 -1
  29. package/images/daily_forecast_2_alerts.png +0 -0
  30. package/images/daily_forecast_no_alerts.png +0 -0
  31. package/images/diagnostic_sensors.png +0 -0
  32. package/images/dynamic_sensors.png +0 -0
  33. package/images/options.png +0 -0
  34. package/images/regenerate_assets.png +0 -0
  35. package/images/setup_options.png +0 -0
  36. package/images/system_options.png +0 -0
  37. package/package.json +1 -1
  38. package/pyproject.toml +1 -1
  39. package/scripts/update_version.sh +6 -0
  40. package/.github/workflows/close-duplicates.yml +0 -57
@@ -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
@@ -71,13 +72,13 @@ from .const import (
71
72
  ALERTS,
72
73
  ALERT_FILE_STATUS,
73
74
  ALERT_WIND,
74
- ALERT_RAIN_INTENSITY,
75
- ALERT_RAIN,
76
- ALERT_SEA,
77
- ALERT_COLD,
78
- ALERT_WARM,
79
- ALERT_WARM_NIGHT,
80
- ALERT_SNOW,
75
+ ALERT_RAIN_INTENSITY,
76
+ ALERT_RAIN,
77
+ ALERT_SEA,
78
+ ALERT_COLD,
79
+ ALERT_WARM,
80
+ ALERT_WARM_NIGHT,
81
+ ALERT_SNOW,
81
82
  LIGHTNING_FILE_STATUS,
82
83
  LIGHTNING_REGION,
83
84
  LIGHTNING_TOWN,
@@ -105,6 +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,
110
+ SUNRISE,
111
+ SUNSET,
112
+ SUN_FILE_STATUS,
113
+ MOON_PHASE,
114
+ MOON_FILE_STATUS,
115
+ MOONRISE,
116
+ MOONSET,
108
117
  )
109
118
 
110
119
  from .coordinator import (
@@ -122,6 +131,10 @@ from .coordinator import (
122
131
  MeteocatQuotesFileCoordinator,
123
132
  MeteocatLightningCoordinator,
124
133
  MeteocatLightningFileCoordinator,
134
+ MeteocatSunCoordinator,
135
+ MeteocatSunFileCoordinator,
136
+ MeteocatMoonCoordinator,
137
+ MeteocatMoonFileCoordinator,
125
138
  )
126
139
 
127
140
  # Definir la zona horaria local
@@ -208,7 +221,7 @@ SENSOR_TYPES: tuple[MeteocatSensorEntityDescription, ...] = (
208
221
  icon="mdi:weather-sunny",
209
222
  device_class=SensorDeviceClass.IRRADIANCE,
210
223
  state_class=SensorStateClass.MEASUREMENT,
211
- native_unit_of_measurement = UnitOfIrradiance.WATTS_PER_SQUARE_METER,
224
+ native_unit_of_measurement=UnitOfIrradiance.WATTS_PER_SQUARE_METER,
212
225
  ),
213
226
  MeteocatSensorEntityDescription(
214
227
  key=UV_INDEX,
@@ -305,7 +318,7 @@ SENSOR_TYPES: tuple[MeteocatSensorEntityDescription, ...] = (
305
318
  translation_key="condition",
306
319
  icon="mdi:weather-partly-cloudy",
307
320
  ),
308
- MeteocatSensorEntityDescription(
321
+ MeteocatSensorEntityDescription(
309
322
  key=MAX_TEMPERATURE_FORECAST,
310
323
  translation_key="max_temperature_forecast",
311
324
  icon="mdi:thermometer-plus",
@@ -367,37 +380,37 @@ SENSOR_TYPES: tuple[MeteocatSensorEntityDescription, ...] = (
367
380
  translation_key="alert_wind",
368
381
  icon="mdi:alert-outline",
369
382
  ),
370
- MeteocatSensorEntityDescription(
383
+ MeteocatSensorEntityDescription(
371
384
  key=ALERT_RAIN_INTENSITY,
372
385
  translation_key="alert_rain_intensity",
373
386
  icon="mdi:alert-outline",
374
387
  ),
375
- MeteocatSensorEntityDescription(
388
+ MeteocatSensorEntityDescription(
376
389
  key=ALERT_RAIN,
377
390
  translation_key="alert_rain",
378
391
  icon="mdi:alert-outline",
379
392
  ),
380
- MeteocatSensorEntityDescription(
393
+ MeteocatSensorEntityDescription(
381
394
  key=ALERT_SEA,
382
395
  translation_key="alert_sea",
383
396
  icon="mdi:alert-outline",
384
397
  ),
385
- MeteocatSensorEntityDescription(
398
+ MeteocatSensorEntityDescription(
386
399
  key=ALERT_COLD,
387
400
  translation_key="alert_cold",
388
401
  icon="mdi:alert-outline",
389
402
  ),
390
- MeteocatSensorEntityDescription(
403
+ MeteocatSensorEntityDescription(
391
404
  key=ALERT_WARM,
392
405
  translation_key="alert_warm",
393
406
  icon="mdi:alert-outline",
394
407
  ),
395
- MeteocatSensorEntityDescription(
408
+ MeteocatSensorEntityDescription(
396
409
  key=ALERT_WARM_NIGHT,
397
410
  translation_key="alert_warm_night",
398
411
  icon="mdi:alert-outline",
399
412
  ),
400
- MeteocatSensorEntityDescription(
413
+ MeteocatSensorEntityDescription(
401
414
  key=ALERT_SNOW,
402
415
  translation_key="alert_snow",
403
416
  icon="mdi:alert-outline",
@@ -431,7 +444,67 @@ SENSOR_TYPES: tuple[MeteocatSensorEntityDescription, ...] = (
431
444
  translation_key="quota_queries",
432
445
  icon="mdi:counter",
433
446
  entity_category=EntityCategory.DIAGNOSTIC,
434
- )
447
+ ),
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
+ ),
455
+ MeteocatSensorEntityDescription(
456
+ key=SUNRISE,
457
+ translation_key="sunrise",
458
+ icon="mdi:weather-sunset-up",
459
+ device_class=SensorDeviceClass.TIMESTAMP,
460
+ ),
461
+ MeteocatSensorEntityDescription(
462
+ key=SUNSET,
463
+ translation_key="sunset",
464
+ icon="mdi:weather-sunset-down",
465
+ device_class=SensorDeviceClass.TIMESTAMP,
466
+ ),
467
+ MeteocatSensorEntityDescription(
468
+ key=SUN_FILE_STATUS,
469
+ translation_key="sun_file_status",
470
+ icon="mdi:update",
471
+ entity_category=EntityCategory.DIAGNOSTIC,
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
+ ),
435
508
  )
436
509
 
437
510
  @callback
@@ -454,12 +527,31 @@ async def async_setup_entry(hass, entry, async_add_entities: AddEntitiesCallback
454
527
  quotes_file_coordinator = entry_data.get("quotes_file_coordinator")
455
528
  lightning_coordinator = entry_data.get("lightning_coordinator")
456
529
  lightning_file_coordinator = entry_data.get("lightning_file_coordinator")
530
+ sun_coordinator = entry_data.get("sun_coordinator")
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")
457
534
 
458
535
  # Sensores generales
459
536
  async_add_entities(
460
537
  MeteocatSensor(sensor_coordinator, description, entry_data)
461
538
  for description in SENSOR_TYPES
462
- if description.key in {WIND_SPEED, WIND_DIRECTION, WIND_DIRECTION_CARDINAL, TEMPERATURE, HUMIDITY, PRESSURE, PRECIPITATION, PRECIPITATION_ACCUMULATED, SOLAR_GLOBAL_IRRADIANCE, MAX_TEMPERATURE, MIN_TEMPERATURE, FEELS_LIKE, WIND_GUST, STATION_TIMESTAMP} # Incluir sensores generales en el coordinador SENSOR COORDINATOR
539
+ if description.key in {
540
+ WIND_SPEED,
541
+ WIND_DIRECTION,
542
+ WIND_DIRECTION_CARDINAL,
543
+ TEMPERATURE,
544
+ HUMIDITY,
545
+ PRESSURE,
546
+ PRECIPITATION,
547
+ PRECIPITATION_ACCUMULATED,
548
+ SOLAR_GLOBAL_IRRADIANCE,
549
+ MAX_TEMPERATURE,
550
+ MIN_TEMPERATURE,
551
+ FEELS_LIKE,
552
+ WIND_GUST,
553
+ STATION_TIMESTAMP,
554
+ }
463
555
  )
464
556
 
465
557
  # Sensores estáticos
@@ -473,14 +565,14 @@ async def async_setup_entry(hass, entry, async_add_entities: AddEntitiesCallback
473
565
  async_add_entities(
474
566
  MeteocatUviSensor(uvi_file_coordinator, description, entry_data)
475
567
  for description in SENSOR_TYPES
476
- if description.key == UV_INDEX # Incluir UVI en el coordinador UVI FILE COORDINATOR
568
+ if description.key == UV_INDEX
477
569
  )
478
570
 
479
571
  # Sensor CONDITION para estado del cielo
480
572
  async_add_entities(
481
573
  MeteocatConditionSensor(condition_coordinator, description, entry_data)
482
574
  for description in SENSOR_TYPES
483
- if description.key == CONDITION # Incluir CONDITION en el coordinador CONDITION COORDINATOR
575
+ if description.key == CONDITION
484
576
  )
485
577
 
486
578
  # Sensores temperatura previsión
@@ -536,7 +628,16 @@ async def async_setup_entry(hass, entry, async_add_entities: AddEntitiesCallback
536
628
  async_add_entities(
537
629
  MeteocatAlertMeteorSensor(alerts_region_coordinator, description, entry_data)
538
630
  for description in SENSOR_TYPES
539
- if description.key in {ALERT_WIND, ALERT_RAIN_INTENSITY, ALERT_RAIN, ALERT_SEA, ALERT_COLD, ALERT_WARM, ALERT_WARM_NIGHT, ALERT_SNOW}
631
+ if description.key in {
632
+ ALERT_WIND,
633
+ ALERT_RAIN_INTENSITY,
634
+ ALERT_RAIN,
635
+ ALERT_SEA,
636
+ ALERT_COLD,
637
+ ALERT_WARM,
638
+ ALERT_WARM_NIGHT,
639
+ ALERT_SNOW,
640
+ }
540
641
  )
541
642
 
542
643
  # Sensores de estado de cuotas
@@ -567,6 +668,48 @@ async def async_setup_entry(hass, entry, async_add_entities: AddEntitiesCallback
567
668
  if description.key in {LIGHTNING_REGION, LIGHTNING_TOWN}
568
669
  )
569
670
 
671
+ # Sensor de estado de archivo de sol
672
+ async_add_entities(
673
+ MeteocatSunStatusSensor(sun_coordinator, description, entry_data)
674
+ for description in SENSOR_TYPES
675
+ if description.key == SUN_FILE_STATUS
676
+ )
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
+
685
+ # Sensores de sol
686
+ async_add_entities(
687
+ MeteocatSunSensor(sun_file_coordinator, description, entry_data)
688
+ for description in SENSOR_TYPES
689
+ if description.key in {SUNRISE, SUNSET}
690
+ )
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
+ )
712
+
570
713
  # Cambiar UTC a la zona horaria local
571
714
  def convert_to_local_time(utc_time: str, local_tz: str = "Europe/Madrid") -> datetime | None:
572
715
  """
@@ -594,7 +737,7 @@ class MeteocatStaticSensor(CoordinatorEntity[MeteocatStaticSensorCoordinator], S
594
737
  """Representation of a static Meteocat sensor."""
595
738
  STATIC_KEYS = {TOWN_NAME, TOWN_ID, STATION_NAME, STATION_ID, REGION_NAME, REGION_ID}
596
739
 
597
- _attr_has_entity_name = True # Activa el uso de nombres basados en el dispositivo
740
+ _attr_has_entity_name = True
598
741
 
599
742
  def __init__(self, static_sensor_coordinator, description, entry_data):
600
743
  """Initialize the static sensor."""
@@ -607,13 +750,9 @@ class MeteocatStaticSensor(CoordinatorEntity[MeteocatStaticSensorCoordinator], S
607
750
  self._region_name = entry_data["region_name"]
608
751
  self._region_id = entry_data["region_id"]
609
752
 
610
- # Unique ID for the entity
611
753
  self._attr_unique_id = f"sensor.{DOMAIN}_{self._town_id}_{self.entity_description.key}"
612
-
613
- # Assign entity_category if defined in the description
614
754
  self._attr_entity_category = getattr(description, "entity_category", None)
615
755
 
616
- # Log para depuración
617
756
  _LOGGER.debug(
618
757
  "Inicializando sensor: %s, Unique ID: %s",
619
758
  self.entity_description.name,
@@ -623,9 +762,7 @@ class MeteocatStaticSensor(CoordinatorEntity[MeteocatStaticSensorCoordinator], S
623
762
  @property
624
763
  def native_value(self):
625
764
  """Return the state of the sensor."""
626
- # Información estática
627
765
  if self.entity_description.key in self.STATIC_KEYS:
628
- # Información estática del `entry_data`
629
766
  if self.entity_description.key == TOWN_NAME:
630
767
  return self._town_name
631
768
  if self.entity_description.key == TOWN_ID:
@@ -644,15 +781,14 @@ class MeteocatStaticSensor(CoordinatorEntity[MeteocatStaticSensorCoordinator], S
644
781
  """Return the device info."""
645
782
  return DeviceInfo(
646
783
  identifiers={(DOMAIN, self._town_id)},
647
- name="Meteocat " + self._station_id + " " + self._town_name,
784
+ name=f"Meteocat {self._station_id} {self._town_name}",
648
785
  manufacturer="Meteocat",
649
786
  model="Meteocat API",
650
787
  )
651
788
 
652
789
  class MeteocatUviSensor(CoordinatorEntity[MeteocatUviFileCoordinator], SensorEntity):
653
790
  """Representation of a Meteocat UV Index sensor."""
654
-
655
- _attr_has_entity_name = True # Activa el uso de nombres basados en el dispositivo
791
+ _attr_has_entity_name = True
656
792
 
657
793
  def __init__(self, uvi_file_coordinator, description, entry_data):
658
794
  """Initialize the UV Index sensor."""
@@ -661,14 +797,9 @@ class MeteocatUviSensor(CoordinatorEntity[MeteocatUviFileCoordinator], SensorEnt
661
797
  self._town_name = entry_data["town_name"]
662
798
  self._town_id = entry_data["town_id"]
663
799
  self._station_id = entry_data["station_id"]
664
-
665
- # Unique ID for the entity
666
800
  self._attr_unique_id = f"sensor.{DOMAIN}_{self._town_id}_{self.entity_description.key}"
667
-
668
- # Asigna entity_category desde description (si está definido)
669
801
  self._attr_entity_category = getattr(description, "entity_category", None)
670
802
 
671
- # Log para depuración
672
803
  _LOGGER.debug(
673
804
  "Inicializando sensor: %s, Unique ID: %s",
674
805
  self.entity_description.name,
@@ -688,7 +819,6 @@ class MeteocatUviSensor(CoordinatorEntity[MeteocatUviFileCoordinator], SensorEnt
688
819
  attributes = super().extra_state_attributes or {}
689
820
  if self.entity_description.key == UV_INDEX:
690
821
  uvi_data = self.coordinator.data or {}
691
- # Add the "hour" attribute if it exists
692
822
  attributes["hour"] = uvi_data.get("hour")
693
823
  return attributes
694
824
 
@@ -697,31 +827,25 @@ class MeteocatUviSensor(CoordinatorEntity[MeteocatUviFileCoordinator], SensorEnt
697
827
  """Return the device info."""
698
828
  return DeviceInfo(
699
829
  identifiers={(DOMAIN, self._town_id)},
700
- name="Meteocat " + self._station_id + " " + self._town_name,
830
+ name=f"Meteocat {self._station_id} {self._town_name}",
701
831
  manufacturer="Meteocat",
702
832
  model="Meteocat API",
703
833
  )
704
834
 
705
835
  class MeteocatConditionSensor(CoordinatorEntity[MeteocatConditionCoordinator], SensorEntity):
706
- """Representation of a Meteocat UV Index sensor."""
707
-
708
- _attr_has_entity_name = True # Activa el uso de nombres basados en el dispositivo
836
+ """Representation of a Meteocat Condition sensor."""
837
+ _attr_has_entity_name = True
709
838
 
710
839
  def __init__(self, condition_coordinator, description, entry_data):
711
- """Initialize the UV Index sensor."""
840
+ """Initialize the Condition sensor."""
712
841
  super().__init__(condition_coordinator)
713
842
  self.entity_description = description
714
843
  self._town_name = entry_data["town_name"]
715
844
  self._town_id = entry_data["town_id"]
716
845
  self._station_id = entry_data["station_id"]
717
-
718
- # Unique ID for the entity
719
846
  self._attr_unique_id = f"sensor.{DOMAIN}_{self._town_id}_{self.entity_description.key}"
720
-
721
- # Asigna entity_category desde description (si está definido)
722
847
  self._attr_entity_category = getattr(description, "entity_category", None)
723
848
 
724
- # Log para depuración
725
849
  _LOGGER.debug(
726
850
  "Inicializando sensor: %s, Unique ID: %s",
727
851
  self.entity_description.name,
@@ -730,7 +854,7 @@ class MeteocatConditionSensor(CoordinatorEntity[MeteocatConditionCoordinator], S
730
854
 
731
855
  @property
732
856
  def native_value(self):
733
- """Return the current UV index value."""
857
+ """Return the current condition value."""
734
858
  if self.entity_description.key == CONDITION:
735
859
  condition_data = self.coordinator.data or {}
736
860
  return condition_data.get("condition", None)
@@ -741,7 +865,6 @@ class MeteocatConditionSensor(CoordinatorEntity[MeteocatConditionCoordinator], S
741
865
  attributes = super().extra_state_attributes or {}
742
866
  if self.entity_description.key == CONDITION:
743
867
  condition_data = self.coordinator.data or {}
744
- # Add the "hour" attribute if it exists
745
868
  attributes["hour"] = condition_data.get("hour", None)
746
869
  return attributes
747
870
 
@@ -750,14 +873,59 @@ class MeteocatConditionSensor(CoordinatorEntity[MeteocatConditionCoordinator], S
750
873
  """Return the device info."""
751
874
  return DeviceInfo(
752
875
  identifiers={(DOMAIN, self._town_id)},
753
- name="Meteocat " + self._station_id + " " + self._town_name,
876
+ name=f"Meteocat {self._station_id} {self._town_name}",
877
+ manufacturer="Meteocat",
878
+ model="Meteocat API",
879
+ )
880
+
881
+ class MeteocatSunSensor(CoordinatorEntity[MeteocatSunCoordinator], SensorEntity):
882
+ """Representation of a Meteocat Sun sensor (sunrise/sunset)."""
883
+ _attr_has_entity_name = True
884
+
885
+ def __init__(self, sun_coordinator, description, entry_data):
886
+ """Initialize the Sun sensor."""
887
+ super().__init__(sun_coordinator)
888
+ self.entity_description = description
889
+ self._town_name = entry_data["town_name"]
890
+ self._town_id = entry_data["town_id"]
891
+ self._station_id = entry_data["station_id"]
892
+ self._attr_unique_id = f"sensor.{DOMAIN}_{self._town_id}_{self.entity_description.key}"
893
+ self._attr_entity_category = getattr(description, "entity_category", None)
894
+
895
+ _LOGGER.debug(
896
+ "Inicializando sensor: %s, Unique ID: %s",
897
+ self.entity_description.name,
898
+ self._attr_unique_id,
899
+ )
900
+
901
+ @property
902
+ def native_value(self):
903
+ """Return the sunrise or sunset time as a datetime."""
904
+ if self.entity_description.key in {SUNRISE, SUNSET}:
905
+ return self.coordinator.data.get(self.entity_description.key)
906
+ return None
907
+
908
+ @property
909
+ def extra_state_attributes(self):
910
+ """Return additional attributes for the sensor."""
911
+ attributes = super().extra_state_attributes or {}
912
+ if self.entity_description.key in {SUNRISE, SUNSET}:
913
+ dt = self.coordinator.data.get(self.entity_description.key)
914
+ attributes["friendly_time"] = dt.strftime("%H:%M") if dt else None
915
+ return attributes
916
+
917
+ @property
918
+ def device_info(self) -> DeviceInfo:
919
+ """Return the device info."""
920
+ return DeviceInfo(
921
+ identifiers={(DOMAIN, self._town_id)},
922
+ name=f"Meteocat {self._station_id} {self._town_name}",
754
923
  manufacturer="Meteocat",
755
924
  model="Meteocat API",
756
925
  )
757
926
 
758
927
  class MeteocatSensor(CoordinatorEntity[MeteocatSensorCoordinator], SensorEntity):
759
928
  """Representation of a Meteocat sensor."""
760
-
761
929
  CODE_MAPPING = {
762
930
  WIND_SPEED: WIND_SPEED_CODE,
763
931
  WIND_DIRECTION: WIND_DIRECTION_CODE,
@@ -766,13 +934,11 @@ class MeteocatSensor(CoordinatorEntity[MeteocatSensorCoordinator], SensorEntity)
766
934
  PRESSURE: PRESSURE_CODE,
767
935
  PRECIPITATION: PRECIPITATION_CODE,
768
936
  SOLAR_GLOBAL_IRRADIANCE: SOLAR_GLOBAL_IRRADIANCE_CODE,
769
- # UV_INDEX: UV_INDEX_CODE,
770
937
  MAX_TEMPERATURE: MAX_TEMPERATURE_CODE,
771
938
  MIN_TEMPERATURE: MIN_TEMPERATURE_CODE,
772
939
  WIND_GUST: WIND_GUST_CODE,
773
940
  }
774
-
775
- _attr_has_entity_name = True # Activa el uso de nombres basados en el dispositivo
941
+ _attr_has_entity_name = True
776
942
 
777
943
  def __init__(self, sensor_coordinator, description, entry_data):
778
944
  """Initialize the sensor."""
@@ -783,14 +949,8 @@ class MeteocatSensor(CoordinatorEntity[MeteocatSensorCoordinator], SensorEntity)
783
949
  self._town_id = entry_data["town_id"]
784
950
  self._station_name = entry_data["station_name"]
785
951
  self._station_id = entry_data["station_id"]
786
-
787
- # Unique ID for the entity
788
952
  self._attr_unique_id = f"sensor.{DOMAIN}_{self._town_id}_{self.entity_description.key}"
789
-
790
- # Asigna entity_category desde description (si está definido)
791
953
  self._attr_entity_category = getattr(description, "entity_category", None)
792
-
793
- # Log para depuración
794
954
  _LOGGER.debug(
795
955
  "Inicializando sensor: %s, Unique ID: %s",
796
956
  self.entity_description.name,
@@ -800,16 +960,11 @@ class MeteocatSensor(CoordinatorEntity[MeteocatSensorCoordinator], SensorEntity)
800
960
  @property
801
961
  def native_value(self):
802
962
  """Return the state of the sensor."""
803
- # Información dinámica
804
963
  if self.entity_description.key == FEELS_LIKE:
805
964
  stations = self.coordinator.data or []
806
-
807
- # Variables necesarias
808
965
  temperature = None
809
966
  humidity = None
810
967
  wind_speed = None
811
-
812
- # Obtener valores de las variables
813
968
  for station in stations:
814
969
  variables = station.get("variables", [])
815
970
  for var in variables:
@@ -818,25 +973,19 @@ class MeteocatSensor(CoordinatorEntity[MeteocatSensorCoordinator], SensorEntity)
818
973
  if not lectures:
819
974
  continue
820
975
  latest_reading = lectures[-1].get("valor")
821
-
822
976
  if code == TEMPERATURE_CODE:
823
977
  temperature = float(latest_reading)
824
978
  elif code == HUMIDITY_CODE:
825
979
  humidity = float(latest_reading)
826
980
  elif code == WIND_SPEED_CODE:
827
981
  wind_speed = float(latest_reading)
828
-
829
- # Verificar que todas las variables necesarias están presentes
830
982
  if temperature is not None and humidity is not None and wind_speed is not None:
831
- # Cálculo del windchill
832
983
  windchill = (
833
984
  13.1267 +
834
985
  0.6215 * temperature -
835
986
  11.37 * (wind_speed ** 0.16) +
836
987
  0.3965 * temperature * (wind_speed ** 0.16)
837
988
  )
838
-
839
- # Cálculo del heat_index
840
989
  heat_index = (
841
990
  -8.78469476 +
842
991
  1.61139411 * temperature +
@@ -848,8 +997,6 @@ class MeteocatSensor(CoordinatorEntity[MeteocatSensorCoordinator], SensorEntity)
848
997
  0.00072546 * temperature * (humidity ** 2) -
849
998
  0.000003582 * (temperature ** 2) * (humidity ** 2)
850
999
  )
851
-
852
- # Lógica de selección
853
1000
  if -50 <= temperature <= 10 and wind_speed > 4.8:
854
1001
  _LOGGER.debug(f"Sensación térmica por frío, calculada según la fórmula de Wind Chill: {windchill} ºC")
855
1002
  return round(windchill, 1)
@@ -861,50 +1008,36 @@ class MeteocatSensor(CoordinatorEntity[MeteocatSensorCoordinator], SensorEntity)
861
1008
  return round(temperature, 1)
862
1009
 
863
1010
  sensor_code = self.CODE_MAPPING.get(self.entity_description.key)
864
-
865
1011
  if sensor_code is not None:
866
- # Accedemos a las estaciones en el JSON recibido
867
1012
  stations = self.coordinator.data or []
868
1013
  for station in stations:
869
1014
  variables = station.get("variables", [])
870
-
871
- # Filtramos por código
872
1015
  variable_data = next(
873
1016
  (var for var in variables if var.get("codi") == sensor_code),
874
1017
  None,
875
1018
  )
876
-
877
1019
  if variable_data:
878
- # Obtenemos la última lectura
879
1020
  lectures = variable_data.get("lectures", [])
880
1021
  if lectures:
881
1022
  latest_reading = lectures[-1]
882
1023
  value = latest_reading.get("valor")
883
-
884
1024
  return value
885
-
886
- # Para el sensor WIND_DIRECTION_CARDINAL, convertir grados a dirección cardinal
1025
+
887
1026
  if self.entity_description.key == WIND_DIRECTION_CARDINAL:
888
1027
  stations = self.coordinator.data or []
889
1028
  for station in stations:
890
1029
  variables = station.get("variables", [])
891
-
892
- # Filtramos por código
893
1030
  variable_data = next(
894
1031
  (var for var in variables if var.get("codi") == WIND_DIRECTION_CODE),
895
1032
  None,
896
1033
  )
897
-
898
1034
  if variable_data:
899
- # Obtenemos la última lectura
900
1035
  lectures = variable_data.get("lectures", [])
901
1036
  if lectures:
902
1037
  latest_reading = lectures[-1]
903
1038
  value = latest_reading.get("valor")
904
-
905
1039
  return self._convert_degrees_to_cardinal(value)
906
-
907
- # Lógica específica para el sensor de timestamp
1040
+
908
1041
  if self.entity_description.key == STATION_TIMESTAMP:
909
1042
  stations = self.coordinator.data or []
910
1043
  for station in stations:
@@ -912,42 +1045,30 @@ class MeteocatSensor(CoordinatorEntity[MeteocatSensorCoordinator], SensorEntity)
912
1045
  for variable in variables:
913
1046
  lectures = variable.get("lectures", [])
914
1047
  if lectures:
915
- # Obtenemos el campo `data` de la última lectura
916
1048
  latest_reading = lectures[-1]
917
1049
  raw_timestamp = latest_reading.get("data")
918
-
919
1050
  if raw_timestamp:
920
- # Convertir el timestamp a un objeto datetime
921
1051
  try:
922
- # Convertimos raw_timestamp a hora local
923
1052
  local_time = convert_to_local_time(raw_timestamp)
924
1053
  _LOGGER.debug("Hora UTC: %s convertida a hora local: %s", raw_timestamp, local_time)
925
1054
  return local_time
926
1055
  except ValueError:
927
- # Manejo de errores si el formato no es válido
928
1056
  _LOGGER.error(f"Error al convertir el timestamp '{raw_timestamp}' a hora local.")
929
1057
  return None
930
1058
 
931
- # Nuevo sensor para la precipitación acumulada
932
1059
  if self.entity_description.key == PRECIPITATION_ACCUMULATED:
933
1060
  stations = self.coordinator.data or []
934
- total_precipitation = 0.0 # Usa float para permitir acumulación de decimales
935
-
1061
+ total_precipitation = 0.0
936
1062
  for station in stations:
937
1063
  variables = station.get("variables", [])
938
-
939
- # Filtramos por código de precipitación
940
1064
  variable_data = next(
941
1065
  (var for var in variables if var.get("codi") == PRECIPITATION_CODE),
942
1066
  None,
943
1067
  )
944
-
945
1068
  if variable_data:
946
- # Sumamos las lecturas de precipitación
947
1069
  lectures = variable_data.get("lectures", [])
948
1070
  for lecture in lectures:
949
- total_precipitation += float(lecture.get("valor", 0.0)) # Convertimos a float
950
-
1071
+ total_precipitation += float(lecture.get("valor", 0.0))
951
1072
  _LOGGER.debug(f"Total precipitación acumulada: {total_precipitation} mm")
952
1073
  return total_precipitation
953
1074
 
@@ -957,11 +1078,10 @@ class MeteocatSensor(CoordinatorEntity[MeteocatSensorCoordinator], SensorEntity)
957
1078
  def _convert_degrees_to_cardinal(degree: float) -> str:
958
1079
  """Convert degrees to cardinal direction."""
959
1080
  if not isinstance(degree, (int, float)):
960
- return "Unknown" # Retorna "Unknown" si el valor no es un número válido
961
-
1081
+ return "Unknown"
962
1082
  directions = [
963
- "north", "north_northeast", "northeast", "east_northeast", "east", "east_southeast", "southeast", "south_southeast",
964
- "south", "south_southwest", "southwest", "west_southwest", "west", "west_northwest", "northwest", "north_northwest"
1083
+ "north", "north_northeast", "northeast", "east_northeast", "east", "east_southeast", "southeast", "south_southeast",
1084
+ "south", "south_southwest", "southwest", "west_southwest", "west", "west_northwest", "northwest", "north_northwest"
965
1085
  ]
966
1086
  index = round(degree / 22.5) % 16
967
1087
  return directions[index]
@@ -971,31 +1091,24 @@ class MeteocatSensor(CoordinatorEntity[MeteocatSensorCoordinator], SensorEntity)
971
1091
  """Return the device info."""
972
1092
  return DeviceInfo(
973
1093
  identifiers={(DOMAIN, self._town_id)},
974
- name="Meteocat " + self._station_id + " " + self._town_name,
1094
+ name=f"Meteocat {self._station_id} {self._town_name}",
975
1095
  manufacturer="Meteocat",
976
1096
  model="Meteocat API",
977
1097
  )
978
1098
 
979
1099
  class MeteocatTempForecast(CoordinatorEntity[MeteocatTempForecastCoordinator], SensorEntity):
980
1100
  """Representation of a Meteocat Min and Max Temperature sensors."""
981
-
982
- _attr_has_entity_name = True # Activa el uso de nombres basados en el dispositivo
1101
+ _attr_has_entity_name = True
983
1102
 
984
1103
  def __init__(self, temp_forecast_coordinator, description, entry_data):
985
- """Initialize the Mina and Max Temperature sensors."""
1104
+ """Initialize the Min and Max Temperature sensors."""
986
1105
  super().__init__(temp_forecast_coordinator)
987
1106
  self.entity_description = description
988
1107
  self._town_name = entry_data["town_name"]
989
1108
  self._town_id = entry_data["town_id"]
990
1109
  self._station_id = entry_data["station_id"]
991
-
992
- # Unique ID for the entity
993
1110
  self._attr_unique_id = f"sensor.{DOMAIN}_{self._town_id}_{self.entity_description.key}"
994
-
995
- # Asigna entity_category desde description (si está definido)
996
1111
  self._attr_entity_category = getattr(description, "entity_category", None)
997
-
998
- # Log para depuración
999
1112
  _LOGGER.debug(
1000
1113
  "Inicializando sensor: %s, Unique ID: %s",
1001
1114
  self.entity_description.name,
@@ -1006,7 +1119,6 @@ class MeteocatTempForecast(CoordinatorEntity[MeteocatTempForecastCoordinator], S
1006
1119
  def native_value(self):
1007
1120
  """Return the Max and Min Temp Forecast value."""
1008
1121
  temp_forecast_data = self.coordinator.data or {}
1009
-
1010
1122
  if self.entity_description.key == MAX_TEMPERATURE_FORECAST:
1011
1123
  return temp_forecast_data.get("max_temp_forecast", None)
1012
1124
  if self.entity_description.key == MIN_TEMPERATURE_FORECAST:
@@ -1018,15 +1130,14 @@ class MeteocatTempForecast(CoordinatorEntity[MeteocatTempForecastCoordinator], S
1018
1130
  """Return the device info."""
1019
1131
  return DeviceInfo(
1020
1132
  identifiers={(DOMAIN, self._town_id)},
1021
- name="Meteocat " + self._station_id + " " + self._town_name,
1133
+ name=f"Meteocat {self._station_id} {self._town_name}",
1022
1134
  manufacturer="Meteocat",
1023
1135
  model="Meteocat API",
1024
1136
  )
1025
1137
 
1026
1138
  class MeteocatPrecipitationProbabilitySensor(CoordinatorEntity[DailyForecastCoordinator], SensorEntity):
1027
1139
  """Representation of a Meteocat precipitation probability sensor."""
1028
-
1029
- _attr_has_entity_name = True # Enable device-based naming
1140
+ _attr_has_entity_name = True
1030
1141
 
1031
1142
  def __init__(self, daily_forecast_coordinator, description, entry_data):
1032
1143
  super().__init__(daily_forecast_coordinator)
@@ -1034,10 +1145,8 @@ class MeteocatPrecipitationProbabilitySensor(CoordinatorEntity[DailyForecastCoor
1034
1145
  self._town_name = entry_data["town_name"]
1035
1146
  self._town_id = entry_data["town_id"]
1036
1147
  self._station_id = entry_data["station_id"]
1037
-
1038
1148
  self._attr_unique_id = f"sensor.{DOMAIN}_{self._town_id}_{self.entity_description.key}"
1039
1149
  self._attr_entity_category = getattr(description, "entity_category", None)
1040
-
1041
1150
  _LOGGER.debug(
1042
1151
  "Initializing sensor: %s, Unique ID: %s",
1043
1152
  self.entity_description.name,
@@ -1058,14 +1167,13 @@ class MeteocatPrecipitationProbabilitySensor(CoordinatorEntity[DailyForecastCoor
1058
1167
  def device_info(self) -> DeviceInfo:
1059
1168
  return DeviceInfo(
1060
1169
  identifiers={(DOMAIN, self._town_id)},
1061
- name="Meteocat " + self._station_id + " " + self._town_name,
1170
+ name=f"Meteocat {self._station_id} {self._town_name}",
1062
1171
  manufacturer="Meteocat",
1063
1172
  model="Meteocat API",
1064
1173
  )
1065
1174
 
1066
1175
  class MeteocatHourlyForecastStatusSensor(CoordinatorEntity[MeteocatEntityCoordinator], SensorEntity):
1067
-
1068
- _attr_has_entity_name = True # Activa el uso de nombres basados en el dispositivo
1176
+ _attr_has_entity_name = True
1069
1177
 
1070
1178
  def __init__(self, entity_coordinator, description, entry_data):
1071
1179
  super().__init__(entity_coordinator)
@@ -1073,11 +1181,7 @@ class MeteocatHourlyForecastStatusSensor(CoordinatorEntity[MeteocatEntityCoordin
1073
1181
  self._town_name = entry_data["town_name"]
1074
1182
  self._town_id = entry_data["town_id"]
1075
1183
  self._station_id = entry_data["station_id"]
1076
-
1077
- # Unique ID for the entity
1078
1184
  self._attr_unique_id = f"sensor.{DOMAIN}_{self._town_id}_hourly_status"
1079
-
1080
- # Assign entity_category if defined in the description
1081
1185
  self._attr_entity_category = getattr(description, "entity_category", None)
1082
1186
 
1083
1187
  def _get_first_date(self):
@@ -1101,8 +1205,6 @@ class MeteocatHourlyForecastStatusSensor(CoordinatorEntity[MeteocatEntityCoordin
1101
1205
  f"hora de contacto a la API >= {DEFAULT_VALIDITY_HOURS}, "
1102
1206
  f"minutos de contacto a la API >= {DEFAULT_VALIDITY_MINUTES}."
1103
1207
  )
1104
-
1105
- # Validar fecha y hora según la lógica del coordinador
1106
1208
  if days_difference > DEFAULT_VALIDITY_DAYS and current_time >= time(DEFAULT_VALIDITY_HOURS, DEFAULT_VALIDITY_MINUTES):
1107
1209
  return "obsolete"
1108
1210
  return "updated"
@@ -1121,14 +1223,13 @@ class MeteocatHourlyForecastStatusSensor(CoordinatorEntity[MeteocatEntityCoordin
1121
1223
  """Return the device info."""
1122
1224
  return DeviceInfo(
1123
1225
  identifiers={(DOMAIN, self._town_id)},
1124
- name="Meteocat " + self._station_id + " " + self._town_name,
1226
+ name=f"Meteocat {self._station_id} {self._town_name}",
1125
1227
  manufacturer="Meteocat",
1126
1228
  model="Meteocat API",
1127
1229
  )
1128
1230
 
1129
1231
  class MeteocatDailyForecastStatusSensor(CoordinatorEntity[MeteocatEntityCoordinator], SensorEntity):
1130
-
1131
- _attr_has_entity_name = True # Activa el uso de nombres basados en el dispositivo
1232
+ _attr_has_entity_name = True
1132
1233
 
1133
1234
  def __init__(self, entity_coordinator, description, entry_data):
1134
1235
  super().__init__(entity_coordinator)
@@ -1136,11 +1237,7 @@ class MeteocatDailyForecastStatusSensor(CoordinatorEntity[MeteocatEntityCoordina
1136
1237
  self._town_name = entry_data["town_name"]
1137
1238
  self._town_id = entry_data["town_id"]
1138
1239
  self._station_id = entry_data["station_id"]
1139
-
1140
- # Unique ID for the entity
1141
1240
  self._attr_unique_id = f"sensor.{DOMAIN}_{self._town_id}_daily_status"
1142
-
1143
- # Assign entity_category if defined in the description
1144
1241
  self._attr_entity_category = getattr(description, "entity_category", None)
1145
1242
 
1146
1243
  def _get_first_date(self):
@@ -1164,8 +1261,6 @@ class MeteocatDailyForecastStatusSensor(CoordinatorEntity[MeteocatEntityCoordina
1164
1261
  f"hora de contacto a la API >= {DEFAULT_VALIDITY_HOURS}, "
1165
1262
  f"minutos de contacto a la API >= {DEFAULT_VALIDITY_MINUTES}."
1166
1263
  )
1167
-
1168
- # Validar fecha y hora según la lógica del coordinador
1169
1264
  if days_difference > DEFAULT_VALIDITY_DAYS and current_time >= time(DEFAULT_VALIDITY_HOURS, DEFAULT_VALIDITY_MINUTES):
1170
1265
  return "obsolete"
1171
1266
  return "updated"
@@ -1184,14 +1279,13 @@ class MeteocatDailyForecastStatusSensor(CoordinatorEntity[MeteocatEntityCoordina
1184
1279
  """Return the device info."""
1185
1280
  return DeviceInfo(
1186
1281
  identifiers={(DOMAIN, self._town_id)},
1187
- name="Meteocat " + self._station_id + " " + self._town_name,
1282
+ name=f"Meteocat {self._station_id} {self._town_name}",
1188
1283
  manufacturer="Meteocat",
1189
1284
  model="Meteocat API",
1190
1285
  )
1191
1286
 
1192
1287
  class MeteocatUviStatusSensor(CoordinatorEntity[MeteocatUviCoordinator], SensorEntity):
1193
-
1194
- _attr_has_entity_name = True # Activa el uso de nombres basados en el dispositivo
1288
+ _attr_has_entity_name = True
1195
1289
 
1196
1290
  def __init__(self, uvi_coordinator, description, entry_data):
1197
1291
  super().__init__(uvi_coordinator)
@@ -1199,11 +1293,7 @@ class MeteocatUviStatusSensor(CoordinatorEntity[MeteocatUviCoordinator], SensorE
1199
1293
  self._town_name = entry_data["town_name"]
1200
1294
  self._town_id = entry_data["town_id"]
1201
1295
  self._station_id = entry_data["station_id"]
1202
-
1203
- # Unique ID for the entity
1204
1296
  self._attr_unique_id = f"sensor.{DOMAIN}_{self._town_id}_uvi_status"
1205
-
1206
- # Assign entity_category if defined in the description
1207
1297
  self._attr_entity_category = getattr(description, "entity_category", None)
1208
1298
 
1209
1299
  def _get_first_date(self):
@@ -1226,8 +1316,6 @@ class MeteocatUviStatusSensor(CoordinatorEntity[MeteocatUviCoordinator], SensorE
1226
1316
  f"hora de contacto a la API >= {DEFAULT_VALIDITY_HOURS}, "
1227
1317
  f"minutos de contacto a la API >= {DEFAULT_VALIDITY_MINUTES}."
1228
1318
  )
1229
-
1230
- # Validar fecha y hora según la lógica del coordinador
1231
1319
  if days_difference > DEFAULT_VALIDITY_DAYS and current_time >= time(DEFAULT_VALIDITY_HOURS, DEFAULT_VALIDITY_MINUTES):
1232
1320
  return "obsolete"
1233
1321
  return "updated"
@@ -1246,13 +1334,13 @@ class MeteocatUviStatusSensor(CoordinatorEntity[MeteocatUviCoordinator], SensorE
1246
1334
  """Return the device info."""
1247
1335
  return DeviceInfo(
1248
1336
  identifiers={(DOMAIN, self._town_id)},
1249
- name="Meteocat " + self._station_id + " " + self._town_name,
1337
+ name=f"Meteocat {self._station_id} {self._town_name}",
1250
1338
  manufacturer="Meteocat",
1251
1339
  model="Meteocat API",
1252
1340
  )
1253
1341
 
1254
1342
  class MeteocatAlertStatusSensor(CoordinatorEntity[MeteocatAlertsCoordinator], SensorEntity):
1255
- _attr_has_entity_name = True # Activa el uso de nombres basados en el dispositivo
1343
+ _attr_has_entity_name = True
1256
1344
 
1257
1345
  def __init__(self, alerts_coordinator, description, entry_data):
1258
1346
  super().__init__(alerts_coordinator)
@@ -1262,11 +1350,7 @@ class MeteocatAlertStatusSensor(CoordinatorEntity[MeteocatAlertsCoordinator], Se
1262
1350
  self._station_id = entry_data["station_id"]
1263
1351
  self._region_id = entry_data["region_id"]
1264
1352
  self._limit_prediccio = entry_data["limit_prediccio"]
1265
-
1266
- # Unique ID for the entity
1267
1353
  self._attr_unique_id = f"sensor.{DOMAIN}_{self._town_id}_alert_status"
1268
-
1269
- # Assign entity_category if defined in the description
1270
1354
  self._attr_entity_category = getattr(description, "entity_category", None)
1271
1355
 
1272
1356
  def _get_data_update(self):
@@ -1289,7 +1373,6 @@ class MeteocatAlertStatusSensor(CoordinatorEntity[MeteocatAlertsCoordinator], Se
1289
1373
  multiplier = ALERT_VALIDITY_MULTIPLIER_500
1290
1374
  else:
1291
1375
  multiplier = ALERT_VALIDITY_MULTIPLIER_DEFAULT
1292
-
1293
1376
  return timedelta(minutes=DEFAULT_ALERT_VALIDITY_TIME * multiplier)
1294
1377
 
1295
1378
  @property
@@ -1298,11 +1381,8 @@ class MeteocatAlertStatusSensor(CoordinatorEntity[MeteocatAlertsCoordinator], Se
1298
1381
  data_update = self._get_data_update()
1299
1382
  if not data_update:
1300
1383
  return "unknown"
1301
-
1302
1384
  current_time = datetime.now(ZoneInfo("UTC"))
1303
1385
  validity_duration = self._get_validity_duration()
1304
-
1305
- # Comprobar si el archivo de alertas está obsoleto
1306
1386
  if current_time - data_update >= validity_duration:
1307
1387
  return "obsolete"
1308
1388
  return "updated"
@@ -1328,7 +1408,6 @@ class MeteocatAlertStatusSensor(CoordinatorEntity[MeteocatAlertsCoordinator], Se
1328
1408
 
1329
1409
  class MeteocatAlertRegionSensor(CoordinatorEntity[MeteocatAlertsRegionCoordinator], SensorEntity):
1330
1410
  """Sensor dinámico que muestra el estado de las alertas por región."""
1331
-
1332
1411
  METEOR_MAPPING = {
1333
1412
  "Temps violent": "violent_weather",
1334
1413
  "Intensitat de pluja": "rain_intensity",
@@ -1340,8 +1419,7 @@ class MeteocatAlertRegionSensor(CoordinatorEntity[MeteocatAlertsRegionCoordinato
1340
1419
  "Calor": "heat",
1341
1420
  "Calor nocturna": "night_heat",
1342
1421
  }
1343
-
1344
- _attr_has_entity_name = True # Activa el uso de nombres basados en el dispositivo
1422
+ _attr_has_entity_name = True
1345
1423
 
1346
1424
  def __init__(self, alerts_region_coordinator, description, entry_data):
1347
1425
  super().__init__(alerts_region_coordinator)
@@ -1350,11 +1428,7 @@ class MeteocatAlertRegionSensor(CoordinatorEntity[MeteocatAlertsRegionCoordinato
1350
1428
  self._town_id = entry_data["town_id"]
1351
1429
  self._station_id = entry_data["station_id"]
1352
1430
  self._region_id = entry_data["region_id"]
1353
-
1354
- # Unique ID for the entity
1355
1431
  self._attr_unique_id = f"sensor.{DOMAIN}_{self._town_id}_alerts"
1356
-
1357
- # Assign entity_category if defined in the description
1358
1432
  self._attr_entity_category = getattr(description, "entity_category", None)
1359
1433
 
1360
1434
  def _map_meteor_case_insensitive(self, meteor: str) -> str:
@@ -1373,8 +1447,6 @@ class MeteocatAlertRegionSensor(CoordinatorEntity[MeteocatAlertsRegionCoordinato
1373
1447
  def extra_state_attributes(self):
1374
1448
  """Devuelve los atributos extra del sensor con los nombres traducidos."""
1375
1449
  meteor_details = self.coordinator.data.get("detalles", {}).get("meteor", {})
1376
-
1377
- # Convertimos las claves al formato deseado usando el mapping
1378
1450
  attributes = {}
1379
1451
  for i, meteor in enumerate(meteor_details.keys()):
1380
1452
  mapped_name = self._map_meteor_case_insensitive(meteor)
@@ -1382,8 +1454,7 @@ class MeteocatAlertRegionSensor(CoordinatorEntity[MeteocatAlertsRegionCoordinato
1382
1454
  _LOGGER.warning("Meteor desconocido sin mapeo: '%s'. Añadirlo a 'METEOR_MAPPING' del coordinador 'MeteocatAlertRegionSensor' si es necesario.", meteor)
1383
1455
  mapped_name = "unknown"
1384
1456
  attributes[f"alert_{i+1}"] = mapped_name
1385
-
1386
- _LOGGER.info("Atributos traducidos del sensor: %s", attributes)
1457
+ _LOGGER.debug("Atributos traducidos del sensor: %s", attributes)
1387
1458
  return attributes
1388
1459
 
1389
1460
  @property
@@ -1391,7 +1462,7 @@ class MeteocatAlertRegionSensor(CoordinatorEntity[MeteocatAlertsRegionCoordinato
1391
1462
  """Devuelve la información del dispositivo."""
1392
1463
  return DeviceInfo(
1393
1464
  identifiers={(DOMAIN, self._town_id)},
1394
- name="Meteocat " + self._station_id + " " + self._town_name,
1465
+ name=f"Meteocat {self._station_id} {self._town_name}",
1395
1466
  manufacturer="Meteocat",
1396
1467
  model="Meteocat API",
1397
1468
  )
@@ -1408,12 +1479,10 @@ class MeteocatAlertMeteorSensor(CoordinatorEntity[MeteocatAlertsRegionCoordinato
1408
1479
  ALERT_WARM_NIGHT: "Calor nocturna",
1409
1480
  ALERT_SNOW: "Neu acumulada en 24 hores",
1410
1481
  }
1411
-
1412
1482
  STATE_MAPPING = {
1413
1483
  "Obert": "opened",
1414
1484
  "Tancat": "closed",
1415
1485
  }
1416
-
1417
1486
  UMBRAL_MAPPING = {
1418
1487
  "Ratxes de vent > 25 m/s": "wind_gusts_25",
1419
1488
  "Esclafits": "microburst",
@@ -1447,8 +1516,7 @@ class MeteocatAlertMeteorSensor(CoordinatorEntity[MeteocatAlertsRegionCoordinato
1447
1516
  "gruix > 2 cm a cotes superiors a 300 metres fins a 600 metres": "thickness_2_at_300",
1448
1517
  "gruix ≥ 0 cm a cotes inferiors a 300 metres": "thickness_0_at_300",
1449
1518
  }
1450
-
1451
- _attr_has_entity_name = True # Activa el uso de nombres basados en el dispositivo
1519
+ _attr_has_entity_name = True
1452
1520
 
1453
1521
  def __init__(self, alerts_region_coordinator, description, entry_data):
1454
1522
  super().__init__(alerts_region_coordinator)
@@ -1457,14 +1525,8 @@ class MeteocatAlertMeteorSensor(CoordinatorEntity[MeteocatAlertsRegionCoordinato
1457
1525
  self._town_id = entry_data["town_id"]
1458
1526
  self._station_id = entry_data["station_id"]
1459
1527
  self._region_id = entry_data["region_id"]
1460
-
1461
- # Unique ID for the entity
1462
1528
  self._attr_unique_id = f"sensor.{DOMAIN}_{self._town_id}_{self.entity_description.key}"
1463
-
1464
- # Assign entity_category if defined in the description
1465
1529
  self._attr_entity_category = getattr(description, "entity_category", None)
1466
-
1467
- # Log para depuración
1468
1530
  _LOGGER.debug(
1469
1531
  "Inicializando sensor: %s, Unique ID: %s",
1470
1532
  self.entity_description.name,
@@ -1494,10 +1556,7 @@ class MeteocatAlertMeteorSensor(CoordinatorEntity[MeteocatAlertsRegionCoordinato
1494
1556
  meteor_type = self.METEOR_MAPPING.get(self.entity_description.key)
1495
1557
  if not meteor_type:
1496
1558
  return "Desconocido"
1497
-
1498
1559
  meteor_data = self._get_meteor_data_case_insensitive(meteor_type)
1499
-
1500
- # Convertir estado para translation_key
1501
1560
  estado_original = meteor_data.get("estado", "Tancat")
1502
1561
  return self.STATE_MAPPING.get(estado_original, "unknown")
1503
1562
 
@@ -1512,22 +1571,17 @@ class MeteocatAlertMeteorSensor(CoordinatorEntity[MeteocatAlertsRegionCoordinato
1512
1571
  self.coordinator.data.get("detalles", {}).get("meteor", {}).keys(),
1513
1572
  )
1514
1573
  return "unknown"
1515
-
1516
1574
  meteor_data = self._get_meteor_data_case_insensitive(meteor_type)
1517
1575
  if not meteor_data:
1518
1576
  return {}
1519
-
1520
- # Convertir umbral para translation_key
1521
1577
  umbral_original = meteor_data.get("umbral")
1522
1578
  umbral_convertido = self._get_umbral_case_insensitive(umbral_original)
1523
-
1524
1579
  if umbral_convertido == "unknown" and umbral_original is not None:
1525
1580
  _LOGGER.warning(
1526
1581
  "Umbral desconocido para sensor %s: '%s'. Añadirlo a 'UMBRAL_MAPPING' del coordinador 'MeteocatAlertMeteorSensor' si es necesario.",
1527
1582
  self.entity_description.key,
1528
1583
  umbral_original
1529
1584
  )
1530
-
1531
1585
  return {
1532
1586
  "inicio": meteor_data.get("inicio"),
1533
1587
  "fin": meteor_data.get("fin"),
@@ -1544,13 +1598,13 @@ class MeteocatAlertMeteorSensor(CoordinatorEntity[MeteocatAlertsRegionCoordinato
1544
1598
  """Devuelve la información del dispositivo."""
1545
1599
  return DeviceInfo(
1546
1600
  identifiers={(DOMAIN, self._town_id)},
1547
- name="Meteocat " + self._station_id + " " + self._town_name,
1601
+ name=f"Meteocat {self._station_id} {self._town_name}",
1548
1602
  manufacturer="Meteocat",
1549
1603
  model="Meteocat API",
1550
1604
  )
1551
1605
 
1552
1606
  class MeteocatQuotaStatusSensor(CoordinatorEntity[MeteocatQuotesCoordinator], SensorEntity):
1553
- _attr_has_entity_name = True # Activa el uso de nombres basados en el dispositivo
1607
+ _attr_has_entity_name = True
1554
1608
 
1555
1609
  def __init__(self, quotes_coordinator, description, entry_data):
1556
1610
  super().__init__(quotes_coordinator)
@@ -1558,11 +1612,7 @@ class MeteocatQuotaStatusSensor(CoordinatorEntity[MeteocatQuotesCoordinator], Se
1558
1612
  self._town_name = entry_data["town_name"]
1559
1613
  self._town_id = entry_data["town_id"]
1560
1614
  self._station_id = entry_data["station_id"]
1561
-
1562
- # Unique ID for the entity
1563
1615
  self._attr_unique_id = f"sensor.{DOMAIN}_{self._town_id}_quota_status"
1564
-
1565
- # Assign entity_category if defined in the description
1566
1616
  self._attr_entity_category = getattr(description, "entity_category", None)
1567
1617
 
1568
1618
  def _get_data_update(self):
@@ -1581,13 +1631,9 @@ class MeteocatQuotaStatusSensor(CoordinatorEntity[MeteocatQuotesCoordinator], Se
1581
1631
  data_update = self._get_data_update()
1582
1632
  if not data_update:
1583
1633
  return "unknown"
1584
-
1585
1634
  current_time = datetime.now(ZoneInfo("UTC"))
1586
-
1587
- # Comprobar si el archivo de alertas está obsoleto
1588
1635
  if current_time - data_update >= timedelta(minutes=DEFAULT_QUOTES_VALIDITY_TIME):
1589
1636
  return "obsolete"
1590
-
1591
1637
  return "updated"
1592
1638
 
1593
1639
  @property
@@ -1611,8 +1657,6 @@ class MeteocatQuotaStatusSensor(CoordinatorEntity[MeteocatQuotesCoordinator], Se
1611
1657
 
1612
1658
  class MeteocatQuotaSensor(CoordinatorEntity[MeteocatQuotesFileCoordinator], SensorEntity):
1613
1659
  """Representation of Meteocat Quota sensors."""
1614
-
1615
- # Mapeo de claves en sensor.py a nombres en quotes.json
1616
1660
  QUOTA_MAPPING = {
1617
1661
  "quota_xdde": "XDDE",
1618
1662
  "quota_prediccio": "Prediccio",
@@ -1620,15 +1664,12 @@ class MeteocatQuotaSensor(CoordinatorEntity[MeteocatQuotesFileCoordinator], Sens
1620
1664
  "quota_xema": "XEMA",
1621
1665
  "quota_queries": "Quota",
1622
1666
  }
1623
-
1624
- # Mapeo de periodos para facilitar la traducción del estado
1625
1667
  PERIOD_STATE_MAPPING = {
1626
1668
  "Setmanal": "weekly",
1627
1669
  "Mensual": "monthly",
1628
1670
  "Anual": "annual",
1629
1671
  }
1630
-
1631
- _attr_has_entity_name = True # Activa el uso de nombres basados en el dispositivo
1672
+ _attr_has_entity_name = True
1632
1673
 
1633
1674
  def __init__(self, quotes_file_coordinator, description, entry_data):
1634
1675
  super().__init__(quotes_file_coordinator)
@@ -1636,28 +1677,20 @@ class MeteocatQuotaSensor(CoordinatorEntity[MeteocatQuotesFileCoordinator], Sens
1636
1677
  self._town_name = entry_data["town_name"]
1637
1678
  self._town_id = entry_data["town_id"]
1638
1679
  self._station_id = entry_data["station_id"]
1639
-
1640
- # Unique ID for the entity
1641
1680
  self._attr_unique_id = f"sensor.{DOMAIN}_{self._town_id}_{self.entity_description.key}"
1642
-
1643
- # Assign entity_category if defined in the description
1644
1681
  self._attr_entity_category = getattr(description, "entity_category", None)
1645
1682
 
1646
1683
  def _get_plan_data(self):
1647
1684
  """Encuentra los datos del plan correspondiente al sensor actual."""
1648
1685
  if not self.coordinator.data:
1649
1686
  return None
1650
-
1651
1687
  plan_name = self.QUOTA_MAPPING.get(self.entity_description.key)
1652
-
1653
1688
  if not plan_name:
1654
1689
  _LOGGER.error(f"No se encontró un mapeo para la clave: {self.entity_description.key}")
1655
1690
  return None
1656
-
1657
1691
  for plan in self.coordinator.data.get("plans", []):
1658
1692
  if plan.get("nom") == plan_name:
1659
- return plan # Retorna el plan encontrado
1660
-
1693
+ return plan
1661
1694
  _LOGGER.warning(f"No se encontró el plan '{plan_name}' en los datos del coordinador.")
1662
1695
  return None
1663
1696
 
@@ -1665,18 +1698,14 @@ class MeteocatQuotaSensor(CoordinatorEntity[MeteocatQuotesFileCoordinator], Sens
1665
1698
  def native_value(self):
1666
1699
  """Devuelve el estado de la cuota: 'ok' si no se ha excedido, 'exceeded' si se ha superado."""
1667
1700
  plan = self._get_plan_data()
1668
-
1669
1701
  if not plan:
1670
1702
  return None
1671
-
1672
1703
  max_consultes = plan.get("maxConsultes")
1673
1704
  consultes_realitzades = plan.get("consultesRealitzades")
1674
-
1675
1705
  if max_consultes is None or consultes_realitzades is None or \
1676
1706
  not isinstance(max_consultes, (int, float)) or not isinstance(consultes_realitzades, (int, float)):
1677
1707
  _LOGGER.warning(f"Datos inválidos para el plan '{plan.get('nom', 'unknown')}': {plan}")
1678
1708
  return None
1679
-
1680
1709
  return "ok" if consultes_realitzades <= max_consultes else "exceeded"
1681
1710
 
1682
1711
  @property
@@ -1684,21 +1713,16 @@ class MeteocatQuotaSensor(CoordinatorEntity[MeteocatQuotesFileCoordinator], Sens
1684
1713
  """Devuelve atributos adicionales del estado del sensor."""
1685
1714
  attributes = super().extra_state_attributes or {}
1686
1715
  plan = self._get_plan_data()
1687
-
1688
1716
  if not plan:
1689
1717
  return {}
1690
-
1691
- # Aplicar el mapeo de periodos
1692
1718
  period = plan.get("periode", "desconocido")
1693
- translated_period = self.PERIOD_STATE_MAPPING.get(period, period) # Si no está en el mapping, dejar el original
1694
-
1719
+ translated_period = self.PERIOD_STATE_MAPPING.get(period, period)
1695
1720
  attributes.update({
1696
1721
  "period": translated_period,
1697
1722
  "max_queries": plan.get("maxConsultes"),
1698
1723
  "made_queries": plan.get("consultesRealitzades"),
1699
1724
  "remain_queries": plan.get("consultesRestants"),
1700
1725
  })
1701
-
1702
1726
  return attributes
1703
1727
 
1704
1728
  @property
@@ -1712,7 +1736,7 @@ class MeteocatQuotaSensor(CoordinatorEntity[MeteocatQuotesFileCoordinator], Sens
1712
1736
  )
1713
1737
 
1714
1738
  class MeteocatLightningStatusSensor(CoordinatorEntity[MeteocatLightningCoordinator], SensorEntity):
1715
- _attr_has_entity_name = True # Activa el uso de nombres basados en el dispositivo
1739
+ _attr_has_entity_name = True
1716
1740
 
1717
1741
  def __init__(self, lightning_coordinator, description, entry_data):
1718
1742
  super().__init__(lightning_coordinator)
@@ -1720,11 +1744,7 @@ class MeteocatLightningStatusSensor(CoordinatorEntity[MeteocatLightningCoordinat
1720
1744
  self._town_name = entry_data["town_name"]
1721
1745
  self._town_id = entry_data["town_id"]
1722
1746
  self._station_id = entry_data["station_id"]
1723
-
1724
- # Unique ID for the entity
1725
1747
  self._attr_unique_id = f"sensor.{DOMAIN}_{self._town_id}_lightning_status"
1726
-
1727
- # Assign entity_category if defined in the description
1728
1748
  self._attr_entity_category = getattr(description, "entity_category", None)
1729
1749
 
1730
1750
  def _get_data_update(self):
@@ -1732,8 +1752,8 @@ class MeteocatLightningStatusSensor(CoordinatorEntity[MeteocatLightningCoordinat
1732
1752
  data_update = self.coordinator.data.get("actualizado")
1733
1753
  if data_update:
1734
1754
  try:
1735
- local_time = datetime.fromisoformat(data_update) # Ya tiene offset (+01:00 o +02:00)
1736
- return local_time.astimezone(ZoneInfo("UTC")) # Convertir a UTC
1755
+ local_time = datetime.fromisoformat(data_update)
1756
+ return local_time.astimezone(ZoneInfo("UTC"))
1737
1757
  except ValueError:
1738
1758
  _LOGGER.error("Formato de fecha de actualización inválido: %s", data_update)
1739
1759
  return None
@@ -1752,15 +1772,11 @@ class MeteocatLightningStatusSensor(CoordinatorEntity[MeteocatLightningCoordinat
1752
1772
  data_update = self._get_data_update()
1753
1773
  if not data_update:
1754
1774
  return "unknown"
1755
-
1756
1775
  now = datetime.now(timezone.utc).astimezone(TIMEZONE)
1757
- current_time = now.time() # Extraer solo la parte de la hora
1758
- offset = now.utcoffset().total_seconds() / 3600 # Obtener el offset en horas
1759
-
1760
- # Determinar la hora de validez considerando el offset horario, el horario de verano (+02:00) o invierno (+01:00)
1776
+ current_time = now.time()
1777
+ offset = now.utcoffset().total_seconds() / 3600
1761
1778
  validity_start_time = time(int(DEFAULT_LIGHTNING_VALIDITY_HOURS + offset), DEFAULT_LIGHTNING_VALIDITY_MINUTES)
1762
1779
  validity_duration = timedelta(minutes=DEFAULT_LIGHTNING_VALIDITY_TIME)
1763
-
1764
1780
  return self._determine_status(now, data_update, current_time, validity_start_time, validity_duration)
1765
1781
 
1766
1782
  @property
@@ -1784,8 +1800,7 @@ class MeteocatLightningStatusSensor(CoordinatorEntity[MeteocatLightningCoordinat
1784
1800
 
1785
1801
  class MeteocatLightningSensor(CoordinatorEntity[MeteocatLightningFileCoordinator], SensorEntity):
1786
1802
  """Representation of Meteocat Lightning sensors."""
1787
-
1788
- _attr_has_entity_name = True # Activa el uso de nombres basados en el dispositivo
1803
+ _attr_has_entity_name = True
1789
1804
 
1790
1805
  def __init__(self, lightning_file_coordinator, description, entry_data):
1791
1806
  super().__init__(lightning_file_coordinator)
@@ -1794,11 +1809,7 @@ class MeteocatLightningSensor(CoordinatorEntity[MeteocatLightningFileCoordinator
1794
1809
  self._town_id = entry_data["town_id"]
1795
1810
  self._station_id = entry_data["station_id"]
1796
1811
  self._region_id = entry_data["region_id"]
1797
-
1798
- # Unique ID for the entity
1799
1812
  self._attr_unique_id = f"sensor.{DOMAIN}_{self._town_id}_{self.entity_description.key}"
1800
-
1801
- # Assign entity_category if defined in the description
1802
1813
  self._attr_entity_category = getattr(description, "entity_category", None)
1803
1814
 
1804
1815
  @property
@@ -1820,8 +1831,6 @@ class MeteocatLightningSensor(CoordinatorEntity[MeteocatLightningFileCoordinator
1820
1831
  data = self.coordinator.data.get("town", {})
1821
1832
  else:
1822
1833
  return attributes
1823
-
1824
- # Agregar atributos específicos
1825
1834
  attributes.update({
1826
1835
  "cloud_cloud": data.get("cc", 0),
1827
1836
  "cloud_ground_neg": data.get("cg-", 0),
@@ -1837,4 +1846,408 @@ class MeteocatLightningSensor(CoordinatorEntity[MeteocatLightningFileCoordinator
1837
1846
  name=f"Meteocat {self._station_id} {self._town_name}",
1838
1847
  manufacturer="Meteocat",
1839
1848
  model="Meteocat API",
1840
- )
1849
+ )
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
+
1928
+ class MeteocatSunSensor(CoordinatorEntity[MeteocatSunFileCoordinator], SensorEntity):
1929
+ """Representation of Meteocat Sun sensors (sunrise/sunset)."""
1930
+ _attr_has_entity_name = True
1931
+
1932
+ def __init__(self, sun_file_coordinator, description, entry_data):
1933
+ """Initialize the Sun sensor."""
1934
+ super().__init__(sun_file_coordinator)
1935
+ self.entity_description = description
1936
+ self._town_name = entry_data["town_name"]
1937
+ self._town_id = entry_data["town_id"]
1938
+ self._station_id = entry_data["station_id"]
1939
+ self._attr_unique_id = f"sensor.{DOMAIN}_{self._town_id}_{self.entity_description.key}"
1940
+ self._attr_entity_category = getattr(description, "entity_category", None)
1941
+
1942
+ _LOGGER.debug(
1943
+ "Inicializando sensor solar: %s, Unique ID: %s",
1944
+ self.entity_description.name,
1945
+ self._attr_unique_id,
1946
+ )
1947
+
1948
+ @property
1949
+ def native_value(self):
1950
+ """Return the sunrise or sunset time as a datetime."""
1951
+ if self.entity_description.key in {SUNRISE, SUNSET}:
1952
+ time_str = self.coordinator.data.get(self.entity_description.key)
1953
+ if time_str:
1954
+ try:
1955
+ return datetime.fromisoformat(time_str)
1956
+ except ValueError:
1957
+ _LOGGER.error("Formato de fecha inválido para %s: %s", self.entity_description.key, time_str)
1958
+ return None
1959
+ return None
1960
+
1961
+ @property
1962
+ def extra_state_attributes(self):
1963
+ """Return additional attributes for the sensor."""
1964
+ attributes = super().extra_state_attributes or {}
1965
+ if self.entity_description.key in {SUNRISE, SUNSET}:
1966
+ time_str = self.coordinator.data.get(self.entity_description.key)
1967
+ if time_str:
1968
+ try:
1969
+ dt = datetime.fromisoformat(time_str)
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
+
1977
+ except ValueError:
1978
+ attributes["friendly_time"] = None
1979
+ attributes["friendly_date"] = None
1980
+ attributes["friendly_day"] = None
1981
+ else:
1982
+ attributes["friendly_time"] = None
1983
+ attributes["friendly_date"] = None
1984
+ attributes["friendly_day"] = None
1985
+ return attributes
1986
+
1987
+ @property
1988
+ def device_info(self) -> DeviceInfo:
1989
+ """Return the device info."""
1990
+ return DeviceInfo(
1991
+ identifiers={(DOMAIN, self._town_id)},
1992
+ name=f"Meteocat {self._station_id} {self._town_name}",
1993
+ manufacturer="Meteocat",
1994
+ model="Meteocat API",
1995
+ )
1996
+
1997
+ class MeteocatSunStatusSensor(CoordinatorEntity[MeteocatSunCoordinator], SensorEntity):
1998
+ """Representation of Meteocat Sun file status sensor."""
1999
+ _attr_has_entity_name = True
2000
+
2001
+ def __init__(self, sun_coordinator, description, entry_data):
2002
+ """Initialize the Sun status sensor."""
2003
+ super().__init__(sun_coordinator)
2004
+ self.entity_description = description
2005
+ self._town_name = entry_data["town_name"]
2006
+ self._town_id = entry_data["town_id"]
2007
+ self._station_id = entry_data["station_id"]
2008
+ self._attr_unique_id = f"sensor.{DOMAIN}_{self._town_id}_sun_status"
2009
+ self._attr_entity_category = getattr(description, "entity_category", None)
2010
+
2011
+ _LOGGER.debug(
2012
+ "Inicializando sensor: %s, Unique ID: %s",
2013
+ self.entity_description.name,
2014
+ self._attr_unique_id,
2015
+ )
2016
+
2017
+ def _get_data_update(self):
2018
+ """Obtain the update date from the coordinator and convert to UTC."""
2019
+ data_update = self.coordinator.data.get("actualitzat", {}).get("dataUpdate")
2020
+ if data_update:
2021
+ try:
2022
+ local_time = datetime.fromisoformat(data_update)
2023
+ return local_time.astimezone(ZoneInfo("UTC"))
2024
+ except ValueError:
2025
+ _LOGGER.error("Formato de fecha de actualización inválido: %s", data_update)
2026
+ return None
2027
+
2028
+ @property
2029
+ def native_value(self):
2030
+ """Return the status of the sun file based on the update date."""
2031
+ data_update = self._get_data_update()
2032
+ if not data_update:
2033
+ return "unknown"
2034
+ now = datetime.now(timezone.utc).astimezone(TIMEZONE)
2035
+ if (now - data_update) > timedelta(days=1):
2036
+ return "obsolete"
2037
+ return "updated"
2038
+
2039
+ @property
2040
+ def extra_state_attributes(self):
2041
+ """Return additional attributes for the sensor."""
2042
+ attributes = super().extra_state_attributes or {}
2043
+ data_update = self._get_data_update()
2044
+ if data_update:
2045
+ attributes["update_date"] = data_update.isoformat()
2046
+ return attributes
2047
+
2048
+ @property
2049
+ def device_info(self) -> DeviceInfo:
2050
+ """Return the device info."""
2051
+ return DeviceInfo(
2052
+ identifiers={(DOMAIN, self._town_id)},
2053
+ name=f"Meteocat {self._station_id} {self._town_name}",
2054
+ manufacturer="Meteocat",
2055
+ model="Meteocat API",
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
+ )