sensor.py 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. """Sensor platform for Solar of Things integration."""
  2. from __future__ import annotations
  3. import logging
  4. from homeassistant.components.sensor import SensorEntity, SensorDeviceClass, SensorStateClass
  5. from homeassistant.config_entries import ConfigEntry
  6. from homeassistant.const import (
  7. PERCENTAGE,
  8. UnitOfElectricCurrent,
  9. UnitOfElectricPotential,
  10. UnitOfEnergy,
  11. UnitOfPower,
  12. )
  13. from homeassistant.core import HomeAssistant
  14. from homeassistant.helpers.entity_platform import AddEntitiesCallback
  15. from homeassistant.helpers.update_coordinator import CoordinatorEntity
  16. from .const import DOMAIN, SENSOR_DEFINITIONS
  17. _LOGGER = logging.getLogger(__name__)
  18. # Map sensor key → translation_key (snake_case)
  19. _TRANSLATION_KEYS: dict[str, str] = {
  20. "pvInputPower": "pv_input_power",
  21. "acOutputActivePower": "ac_output_active_power",
  22. "batteryDischargeCurrent": "battery_discharge_current",
  23. "batteryChargingCurrent": "battery_charging_current",
  24. "batteryVoltage": "battery_voltage",
  25. "batteryPower": "battery_power",
  26. "batterySOC": "battery_soc",
  27. "feedInPower": "feed_in_power",
  28. "gridPower": "grid_power",
  29. "loadPower": "load_power",
  30. "monthly_pv_generated": "monthly_pv_generated",
  31. "monthly_grid_import": "monthly_grid_import",
  32. "monthly_total_consumption": "monthly_total_consumption",
  33. "monthly_solar_percentage": "monthly_solar_percentage",
  34. }
  35. async def async_setup_entry(
  36. hass: HomeAssistant,
  37. entry: ConfigEntry,
  38. async_add_entities: AddEntitiesCallback,
  39. ) -> None:
  40. """Set up Solar of Things sensors."""
  41. data = hass.data[DOMAIN][entry.entry_id]
  42. station_id: str = data["station_id"]
  43. device_coordinators = data["device_coordinators"]
  44. station_coordinator = data["station_coordinator"]
  45. entities: list[SensorEntity] = []
  46. # Per-device sensors
  47. for device_id, coordinator in device_coordinators.items():
  48. device_name = (coordinator.device_meta or {}).get("name") or device_id
  49. for key, definition in SENSOR_DEFINITIONS.items():
  50. if key.startswith("monthly_"):
  51. continue
  52. entities.append(
  53. SolarOfThingsDeviceSensor(
  54. coordinator=coordinator,
  55. station_id=station_id,
  56. device_id=device_id,
  57. device_name=device_name,
  58. sensor_key=key,
  59. sensor_definition=definition,
  60. )
  61. )
  62. # Station-level monthly sensors
  63. if station_coordinator:
  64. for key, definition in SENSOR_DEFINITIONS.items():
  65. if not key.startswith("monthly_"):
  66. continue
  67. entities.append(
  68. SolarOfThingsStationMonthlySensor(
  69. coordinator=station_coordinator,
  70. station_id=station_id,
  71. sensor_key=key,
  72. sensor_definition=definition,
  73. )
  74. )
  75. async_add_entities(entities)
  76. class SolarOfThingsDeviceSensor(CoordinatorEntity, SensorEntity):
  77. """Per-device telemetry sensor."""
  78. def __init__(
  79. self,
  80. coordinator,
  81. station_id: str,
  82. device_id: str,
  83. device_name: str,
  84. sensor_key: str,
  85. sensor_definition: dict,
  86. ) -> None:
  87. super().__init__(coordinator)
  88. self._station_id = station_id
  89. self._device_id = device_id
  90. self._device_name = device_name
  91. self._sensor_key = sensor_key
  92. self._sensor_definition = sensor_definition
  93. self._attr_has_entity_name = True
  94. self._attr_translation_key = _TRANSLATION_KEYS.get(sensor_key)
  95. # Fallback name if no translation key
  96. if not self._attr_translation_key:
  97. self._attr_name = f"{device_name} {sensor_definition['name']}"
  98. self._attr_unique_id = f"{DOMAIN}_{station_id}_{device_id}_{sensor_key}"
  99. self._attr_icon = sensor_definition.get("icon")
  100. unit = sensor_definition.get("unit")
  101. if unit == "W":
  102. self._attr_device_class = SensorDeviceClass.POWER
  103. self._attr_native_unit_of_measurement = UnitOfPower.WATT
  104. self._attr_state_class = SensorStateClass.MEASUREMENT
  105. elif unit == "kWh":
  106. self._attr_device_class = SensorDeviceClass.ENERGY
  107. self._attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR
  108. self._attr_state_class = SensorStateClass.TOTAL_INCREASING
  109. elif unit == "A":
  110. self._attr_device_class = SensorDeviceClass.CURRENT
  111. self._attr_native_unit_of_measurement = UnitOfElectricCurrent.AMPERE
  112. self._attr_state_class = SensorStateClass.MEASUREMENT
  113. elif unit == "V":
  114. self._attr_device_class = SensorDeviceClass.VOLTAGE
  115. self._attr_native_unit_of_measurement = UnitOfElectricPotential.VOLT
  116. self._attr_state_class = SensorStateClass.MEASUREMENT
  117. elif unit == "%":
  118. if "battery" in sensor_key.lower():
  119. self._attr_device_class = SensorDeviceClass.BATTERY
  120. self._attr_native_unit_of_measurement = PERCENTAGE
  121. self._attr_state_class = SensorStateClass.MEASUREMENT
  122. @property
  123. def device_info(self):
  124. return {
  125. "identifiers": {(DOMAIN, self._device_id)},
  126. "name": self._device_name,
  127. "manufacturer": "Siseli",
  128. "model": (self.coordinator.data.get("device_meta") or {}).get("model") if self.coordinator.data else None,
  129. "via_device": (DOMAIN, self._station_id),
  130. }
  131. @property
  132. def native_value(self):
  133. ts = (self.coordinator.data or {}).get("time_series") or {}
  134. val = ts.get(self._sensor_key)
  135. if val is None:
  136. return None
  137. try:
  138. return round(float(val), 2)
  139. except Exception:
  140. return None
  141. class SolarOfThingsStationMonthlySensor(CoordinatorEntity, SensorEntity):
  142. """Station-level monthly summary sensor."""
  143. def __init__(
  144. self,
  145. coordinator,
  146. station_id: str,
  147. sensor_key: str,
  148. sensor_definition: dict,
  149. ) -> None:
  150. super().__init__(coordinator)
  151. self._station_id = station_id
  152. self._sensor_key = sensor_key
  153. self._sensor_definition = sensor_definition
  154. self._attr_has_entity_name = True
  155. self._attr_translation_key = _TRANSLATION_KEYS.get(sensor_key)
  156. if not self._attr_translation_key:
  157. self._attr_name = f"Station {station_id} {sensor_definition['name']}"
  158. self._attr_unique_id = f"{DOMAIN}_{station_id}_{sensor_key}"
  159. self._attr_icon = sensor_definition.get("icon")
  160. unit = sensor_definition.get("unit")
  161. if unit == "kWh":
  162. self._attr_device_class = SensorDeviceClass.ENERGY
  163. self._attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR
  164. self._attr_state_class = SensorStateClass.TOTAL
  165. elif unit == "%":
  166. self._attr_native_unit_of_measurement = PERCENTAGE
  167. self._attr_state_class = SensorStateClass.MEASUREMENT
  168. @property
  169. def device_info(self):
  170. return {
  171. "identifiers": {(DOMAIN, self._station_id)},
  172. "name": f"Solar Station {self._station_id}",
  173. "manufacturer": "Siseli",
  174. "model": "Station",
  175. }
  176. @property
  177. def native_value(self):
  178. monthly = (self.coordinator.data or {}).get("monthly") or {}
  179. val = monthly.get(self._sensor_key)
  180. if val is None:
  181. return None
  182. try:
  183. return round(float(val), 2)
  184. except Exception:
  185. return None