number.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. """Number platform for Solar of Things integration."""
  2. from __future__ import annotations
  3. import logging
  4. from homeassistant.components.number import NumberDeviceClass, NumberEntity, NumberMode
  5. from homeassistant.config_entries import ConfigEntry
  6. from homeassistant.const import PERCENTAGE
  7. from homeassistant.core import HomeAssistant
  8. from homeassistant.helpers.entity_platform import AddEntitiesCallback
  9. from homeassistant.helpers.update_coordinator import CoordinatorEntity
  10. from .api import resolve_setting_key
  11. from .const import DOMAIN
  12. _LOGGER = logging.getLogger(__name__)
  13. async def async_setup_entry(
  14. hass: HomeAssistant,
  15. entry: ConfigEntry,
  16. async_add_entities: AddEntitiesCallback,
  17. ) -> None:
  18. """Set up number entities (controls) for each device."""
  19. data = hass.data[DOMAIN][entry.entry_id]
  20. api = data["api"]
  21. station_id: str = data["station_id"]
  22. device_coordinators = data["device_coordinators"]
  23. entities: list[NumberEntity] = []
  24. for device_id, coordinator in device_coordinators.items():
  25. device_name = (coordinator.device_meta or {}).get("name") or device_id
  26. entities.extend(
  27. [
  28. SolarOfThingsBatteryChargeLimitNumber(api, coordinator, station_id, device_id, device_name),
  29. SolarOfThingsBatteryDischargeLimitNumber(api, coordinator, station_id, device_id, device_name),
  30. SolarOfThingsGridChargeLimitNumber(api, coordinator, station_id, device_id, device_name),
  31. ]
  32. )
  33. async_add_entities(entities)
  34. class _BaseNumber(CoordinatorEntity, NumberEntity):
  35. # Canonical setting key this entity controls (set by subclasses via
  36. # _setting_key). See resolve_setting_key() in api.py: lets a firmware
  37. # that exposes this control under a different key name still work, and a
  38. # firmware that doesn't expose it under any known name report
  39. # unavailable instead of offering a control guaranteed to fail.
  40. _setting_key: str = ""
  41. def __init__(self, api, coordinator, station_id: str, device_id: str, device_name: str) -> None:
  42. super().__init__(coordinator)
  43. self._api = api
  44. self._station_id = station_id
  45. self._device_id = device_id
  46. self._device_name = device_name
  47. @property
  48. def device_info(self):
  49. return {
  50. "identifiers": {(DOMAIN, self._device_id)},
  51. "name": self._device_name,
  52. "manufacturer": "Siseli",
  53. "model": (self.coordinator.data.get("device_meta") or {}).get("model") if self.coordinator.data else None,
  54. "via_device": (DOMAIN, self._station_id),
  55. }
  56. @property
  57. def available(self) -> bool:
  58. if not super().available:
  59. return False
  60. settings = (self.coordinator.data or {}).get("settings")
  61. return resolve_setting_key(settings, self._setting_key) is not None
  62. @property
  63. def native_value(self):
  64. """Return the setting's numeric value.
  65. get_device_settings() returns {key: {"key", "value", "valueDisplay",
  66. ...}} per key, not a bare number — this was previously returning that
  67. whole object, which HA's number platform cannot render as a value.
  68. """
  69. settings = ((self.coordinator.data or {}).get("settings") or {})
  70. key = resolve_setting_key(settings, self._setting_key)
  71. entry = settings.get(key) if key else None
  72. if entry is None:
  73. return None
  74. raw = entry.get("value") if isinstance(entry, dict) else entry
  75. try:
  76. return float(raw)
  77. except (TypeError, ValueError):
  78. return None
  79. class SolarOfThingsBatteryChargeLimitNumber(_BaseNumber):
  80. _setting_key = "batteryChargeLimit"
  81. def __init__(self, api, coordinator, station_id: str, device_id: str, device_name: str) -> None:
  82. super().__init__(api, coordinator, station_id, device_id, device_name)
  83. self._attr_name = f"{device_name} Battery Charge Limit"
  84. self._attr_unique_id = f"{DOMAIN}_{station_id}_{device_id}_battery_charge_limit"
  85. self._attr_native_min_value = 0
  86. self._attr_native_max_value = 100
  87. self._attr_native_step = 1
  88. self._attr_native_unit_of_measurement = PERCENTAGE
  89. self._attr_mode = NumberMode.SLIDER
  90. self._attr_icon = "mdi:battery-arrow-up"
  91. async def async_set_native_value(self, value: float) -> None:
  92. settings = (self.coordinator.data or {}).get("settings")
  93. await self.hass.async_add_executor_job(
  94. self._api.set_battery_charge_limit, self._device_id, int(value), settings
  95. )
  96. await self.coordinator.async_request_refresh()
  97. class SolarOfThingsBatteryDischargeLimitNumber(_BaseNumber):
  98. _setting_key = "batteryDischargeLimit"
  99. def __init__(self, api, coordinator, station_id: str, device_id: str, device_name: str) -> None:
  100. super().__init__(api, coordinator, station_id, device_id, device_name)
  101. self._attr_name = f"{device_name} Battery Discharge Limit"
  102. self._attr_unique_id = f"{DOMAIN}_{station_id}_{device_id}_battery_discharge_limit"
  103. self._attr_native_min_value = 0
  104. self._attr_native_max_value = 100
  105. self._attr_native_step = 1
  106. self._attr_native_unit_of_measurement = PERCENTAGE
  107. self._attr_mode = NumberMode.SLIDER
  108. self._attr_icon = "mdi:battery-arrow-down"
  109. async def async_set_native_value(self, value: float) -> None:
  110. settings = (self.coordinator.data or {}).get("settings")
  111. await self.hass.async_add_executor_job(
  112. self._api.set_battery_discharge_limit, self._device_id, int(value), settings
  113. )
  114. await self.coordinator.async_request_refresh()
  115. class SolarOfThingsGridChargeLimitNumber(_BaseNumber):
  116. _setting_key = "gridChargeLimit"
  117. def __init__(self, api, coordinator, station_id: str, device_id: str, device_name: str) -> None:
  118. super().__init__(api, coordinator, station_id, device_id, device_name)
  119. self._attr_name = f"{device_name} Grid Charge Limit"
  120. self._attr_unique_id = f"{DOMAIN}_{station_id}_{device_id}_grid_charge_limit"
  121. self._attr_native_min_value = 0
  122. self._attr_native_max_value = 5000
  123. self._attr_native_step = 100
  124. self._attr_native_unit_of_measurement = "W"
  125. self._attr_mode = NumberMode.SLIDER
  126. self._attr_device_class = NumberDeviceClass.POWER
  127. self._attr_icon = "mdi:transmission-tower-import"
  128. async def async_set_native_value(self, value: float) -> None:
  129. settings = (self.coordinator.data or {}).get("settings")
  130. await self.hass.async_add_executor_job(
  131. self._api.set_grid_charge_limit, self._device_id, int(value), settings
  132. )
  133. await self.coordinator.async_request_refresh()