select.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. """Select platform for Solar of Things integration."""
  2. from __future__ import annotations
  3. import logging
  4. from homeassistant.components.select import SelectEntity
  5. from homeassistant.config_entries import ConfigEntry
  6. from homeassistant.core import HomeAssistant
  7. from homeassistant.helpers.entity_platform import AddEntitiesCallback
  8. from homeassistant.helpers.update_coordinator import CoordinatorEntity
  9. from .api import resolve_setting_key
  10. from .const import DOMAIN
  11. _LOGGER = logging.getLogger(__name__)
  12. # Real API key: outputSourcePrioritySetting (integer value)
  13. # 0 = USO Utility first
  14. # 1 = SUB Solar first (grid supplement)
  15. # 2 = SBU Solar + Battery first (grid only last resort)
  16. OUTPUT_MODE_BY_VALUE: dict[int, str] = {
  17. 0: "Utility First (USO)",
  18. 1: "Solar First (SUB)",
  19. 2: "Solar+Battery First (SBU)",
  20. }
  21. OUTPUT_MODES = list(OUTPUT_MODE_BY_VALUE.values())
  22. # Real API key: chargerSourcePrioritySetting (integer value)
  23. # 0 = CSO Solar + Utility charging (utility has priority)
  24. # 1 = SNU Solar First for charging, utility as fallback
  25. # 2 = OSO Solar Only charging
  26. CHARGER_PRIORITY_BY_VALUE: dict[int, str] = {
  27. 0: "Solar + Utility (CSO)",
  28. 1: "Solar First (SNU)",
  29. 2: "Solar Only (OSO)",
  30. }
  31. CHARGER_PRIORITIES = list(CHARGER_PRIORITY_BY_VALUE.values())
  32. async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback) -> None:
  33. data = hass.data[DOMAIN][entry.entry_id]
  34. api = data["api"]
  35. station_id: str = data["station_id"]
  36. device_coordinators = data["device_coordinators"]
  37. entities: list[SelectEntity] = []
  38. for device_id, coordinator in device_coordinators.items():
  39. device_name = (coordinator.device_meta or {}).get("name") or device_id
  40. entities.extend(
  41. [
  42. SolarOfThingsOperatingModeSelect(api, coordinator, station_id, device_id, device_name),
  43. SolarOfThingsBatteryPrioritySelect(api, coordinator, station_id, device_id, device_name),
  44. ]
  45. )
  46. async_add_entities(entities)
  47. class _BaseSelect(CoordinatorEntity, SelectEntity):
  48. # Canonical setting key this entity controls (set by subclasses). Used
  49. # with resolve_setting_key() so a firmware that exposes the same control
  50. # under a different key name (#18) still reads and writes correctly, and
  51. # a firmware that doesn't expose it under any known name reports
  52. # unavailable instead of offering a control guaranteed to fail.
  53. _canonical_key: str = ""
  54. def __init__(self, api, coordinator, station_id: str, device_id: str, device_name: str) -> None:
  55. super().__init__(coordinator)
  56. self._api = api
  57. self._station_id = station_id
  58. self._device_id = device_id
  59. self._device_name = device_name
  60. @property
  61. def device_info(self):
  62. return {
  63. "identifiers": {(DOMAIN, self._device_id)},
  64. "name": self._device_name,
  65. "manufacturer": "Siseli",
  66. "model": (self.coordinator.data.get("device_meta") or {}).get("model") if self.coordinator.data else None,
  67. "via_device": (DOMAIN, self._station_id),
  68. }
  69. @property
  70. def available(self) -> bool:
  71. if not super().available:
  72. return False
  73. settings = (self.coordinator.data or {}).get("settings")
  74. return resolve_setting_key(settings, self._canonical_key) is not None
  75. class SolarOfThingsOperatingModeSelect(_BaseSelect):
  76. """Select entity for Output Source Priority (outputSourcePrioritySetting).
  77. Reflects the real device API key. Values 0/1/2 map to USO/SUB/SBU.
  78. """
  79. _canonical_key = "outputSourcePrioritySetting"
  80. def __init__(self, api, coordinator, station_id: str, device_id: str, device_name: str) -> None:
  81. super().__init__(api, coordinator, station_id, device_id, device_name)
  82. self._attr_name = f"{device_name} Output Source Priority"
  83. self._attr_unique_id = f"{DOMAIN}_{station_id}_{device_id}_operating_mode"
  84. self._attr_options = OUTPUT_MODES
  85. self._attr_icon = "mdi:cog"
  86. @property
  87. def current_option(self) -> str | None:
  88. settings = ((self.coordinator.data or {}).get("settings") or {})
  89. key = resolve_setting_key(settings, self._canonical_key)
  90. entry = settings.get(key) if key else None
  91. if entry is None:
  92. return None
  93. raw = entry.get("value") if isinstance(entry, dict) else entry
  94. try:
  95. return OUTPUT_MODE_BY_VALUE.get(int(raw))
  96. except (TypeError, ValueError):
  97. return None
  98. async def async_select_option(self, option: str) -> None:
  99. settings = (self.coordinator.data or {}).get("settings")
  100. await self.hass.async_add_executor_job(
  101. self._api.set_operating_mode, self._device_id, option, settings
  102. )
  103. await self.coordinator.async_request_refresh()
  104. class SolarOfThingsBatteryPrioritySelect(_BaseSelect):
  105. """Select entity for Charger Source Priority (chargerSourcePrioritySetting).
  106. Reflects the real device API key. Values 0/1/2 map to CSO/SNU/OSO.
  107. """
  108. _canonical_key = "chargerSourcePrioritySetting"
  109. def __init__(self, api, coordinator, station_id: str, device_id: str, device_name: str) -> None:
  110. super().__init__(api, coordinator, station_id, device_id, device_name)
  111. self._attr_name = f"{device_name} Charger Source Priority"
  112. self._attr_unique_id = f"{DOMAIN}_{station_id}_{device_id}_battery_priority"
  113. self._attr_options = CHARGER_PRIORITIES
  114. self._attr_icon = "mdi:battery-sync"
  115. @property
  116. def current_option(self) -> str | None:
  117. settings = ((self.coordinator.data or {}).get("settings") or {})
  118. key = resolve_setting_key(settings, self._canonical_key)
  119. entry = settings.get(key) if key else None
  120. if entry is None:
  121. return None
  122. raw = entry.get("value") if isinstance(entry, dict) else entry
  123. try:
  124. return CHARGER_PRIORITY_BY_VALUE.get(int(raw))
  125. except (TypeError, ValueError):
  126. return None
  127. async def async_select_option(self, option: str) -> None:
  128. settings = (self.coordinator.data or {}).get("settings")
  129. await self.hass.async_add_executor_job(
  130. self._api.set_battery_priority, self._device_id, option, settings
  131. )
  132. await self.coordinator.async_request_refresh()