switch.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. """Switch platform for Solar of Things integration."""
  2. from __future__ import annotations
  3. import logging
  4. from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity
  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. async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback) -> None:
  13. data = hass.data[DOMAIN][entry.entry_id]
  14. api = data["api"]
  15. station_id: str = data["station_id"]
  16. device_coordinators = data["device_coordinators"]
  17. entities: list[SwitchEntity] = []
  18. for device_id, coordinator in device_coordinators.items():
  19. device_name = (coordinator.device_meta or {}).get("name") or device_id
  20. entities.extend(
  21. [
  22. SolarOfThingsGridChargingSwitch(api, coordinator, station_id, device_id, device_name),
  23. SolarOfThingsGridFeedInSwitch(api, coordinator, station_id, device_id, device_name),
  24. SolarOfThingsBackupModeSwitch(api, coordinator, station_id, device_id, device_name),
  25. ]
  26. )
  27. async_add_entities(entities)
  28. def _setting_value(coordinator_data: dict | None, canonical_key: str) -> int | None:
  29. """Extract the integer value for a canonical setting from coordinator data.
  30. Resolves through resolve_setting_key() so a firmware that exposes this
  31. control under a different key name (#18) still reads its real state
  32. instead of showing unknown.
  33. """
  34. settings = ((coordinator_data or {}).get("settings") or {})
  35. key = resolve_setting_key(settings, canonical_key)
  36. entry = settings.get(key) if key else None
  37. if entry is None:
  38. return None
  39. raw = entry.get("value") if isinstance(entry, dict) else entry
  40. try:
  41. return int(raw)
  42. except (TypeError, ValueError):
  43. return None
  44. class _BaseSwitch(CoordinatorEntity, SwitchEntity):
  45. # Canonical setting key this entity controls (set by subclasses). See
  46. # resolve_setting_key() in api.py: lets a firmware that exposes this
  47. # control under a different key name still work, and a firmware that
  48. # doesn't expose it under any known name report unavailable instead of
  49. # offering a control guaranteed to fail.
  50. _canonical_key: str = ""
  51. def __init__(self, api, coordinator, station_id: str, device_id: str, device_name: str) -> None:
  52. super().__init__(coordinator)
  53. self._api = api
  54. self._station_id = station_id
  55. self._device_id = device_id
  56. self._device_name = device_name
  57. @property
  58. def device_info(self):
  59. return {
  60. "identifiers": {(DOMAIN, self._device_id)},
  61. "name": self._device_name,
  62. "manufacturer": "Siseli",
  63. "model": (self.coordinator.data.get("device_meta") or {}).get("model") if self.coordinator.data else None,
  64. "via_device": (DOMAIN, self._station_id),
  65. }
  66. @property
  67. def available(self) -> bool:
  68. if not super().available:
  69. return False
  70. settings = (self.coordinator.data or {}).get("settings")
  71. return resolve_setting_key(settings, self._canonical_key) is not None
  72. class SolarOfThingsGridChargingSwitch(_BaseSwitch):
  73. """Switch for AC Input Range setting (acInputRangeSetting).
  74. 0 = Appliance mode – wide input voltage range, grid charging allowed.
  75. 1 = UPS mode – narrow voltage range, stricter bypass behaviour.
  76. The switch reports ON when the inverter is in Appliance (grid-charging) mode.
  77. """
  78. _canonical_key = "acInputRangeSetting"
  79. def __init__(self, api, coordinator, station_id: str, device_id: str, device_name: str) -> None:
  80. super().__init__(api, coordinator, station_id, device_id, device_name)
  81. self._attr_name = f"{device_name} Grid Charging (AC Input Range)"
  82. self._attr_unique_id = f"{DOMAIN}_{station_id}_{device_id}_grid_charging"
  83. self._attr_device_class = SwitchDeviceClass.SWITCH
  84. self._attr_icon = "mdi:transmission-tower"
  85. @property
  86. def is_on(self) -> bool | None:
  87. val = _setting_value(self.coordinator.data, self._canonical_key)
  88. if val is None:
  89. return None
  90. return val == 0 # 0=Appliance (charging OK), 1=UPS (bypass)
  91. async def async_turn_on(self, **kwargs):
  92. settings = (self.coordinator.data or {}).get("settings")
  93. await self.hass.async_add_executor_job(self._api.set_grid_charging, self._device_id, True, settings)
  94. await self.coordinator.async_request_refresh()
  95. async def async_turn_off(self, **kwargs):
  96. settings = (self.coordinator.data or {}).get("settings")
  97. await self.hass.async_add_executor_job(self._api.set_grid_charging, self._device_id, False, settings)
  98. await self.coordinator.async_request_refresh()
  99. class SolarOfThingsGridFeedInSwitch(_BaseSwitch):
  100. """Switch for GRID grid switch (batteryPowerLimitingSetting).
  101. 0 = OFF (grid switch disabled / feed-in off).
  102. 1 = ON (grid switch enabled / feed-in on).
  103. """
  104. _canonical_key = "batteryPowerLimitingSetting"
  105. def __init__(self, api, coordinator, station_id: str, device_id: str, device_name: str) -> None:
  106. super().__init__(api, coordinator, station_id, device_id, device_name)
  107. self._attr_name = f"{device_name} Grid Feed-In"
  108. self._attr_unique_id = f"{DOMAIN}_{station_id}_{device_id}_grid_feed_in"
  109. self._attr_device_class = SwitchDeviceClass.SWITCH
  110. self._attr_icon = "mdi:transmission-tower-export"
  111. @property
  112. def is_on(self) -> bool | None:
  113. val = _setting_value(self.coordinator.data, self._canonical_key)
  114. if val is None:
  115. return None
  116. return val == 1 # 1=ON
  117. async def async_turn_on(self, **kwargs):
  118. settings = (self.coordinator.data or {}).get("settings")
  119. await self.hass.async_add_executor_job(self._api.set_grid_feed_in, self._device_id, True, settings)
  120. await self.coordinator.async_request_refresh()
  121. async def async_turn_off(self, **kwargs):
  122. settings = (self.coordinator.data or {}).get("settings")
  123. await self.hass.async_add_executor_job(self._api.set_grid_feed_in, self._device_id, False, settings)
  124. await self.coordinator.async_request_refresh()
  125. class SolarOfThingsBackupModeSwitch(_BaseSwitch):
  126. """Switch that maps to Output Source Priority SBU (backup/off-grid biased).
  127. ON → outputSourcePrioritySetting = 2 (SBU: Solar+Battery first, grid last).
  128. OFF → outputSourcePrioritySetting = 1 (SUB: Solar first, grid as supplement).
  129. Note: turning this switch ON will also change the Operating Mode select entity
  130. to 'Solar+Battery First (SBU)', which is the expected behaviour.
  131. """
  132. _canonical_key = "outputSourcePrioritySetting"
  133. def __init__(self, api, coordinator, station_id: str, device_id: str, device_name: str) -> None:
  134. super().__init__(api, coordinator, station_id, device_id, device_name)
  135. self._attr_name = f"{device_name} Backup Mode (SBU Priority)"
  136. self._attr_unique_id = f"{DOMAIN}_{station_id}_{device_id}_backup_mode"
  137. self._attr_device_class = SwitchDeviceClass.SWITCH
  138. self._attr_icon = "mdi:battery-lock"
  139. @property
  140. def is_on(self) -> bool | None:
  141. val = _setting_value(self.coordinator.data, self._canonical_key)
  142. if val is None:
  143. return None
  144. return val == 2 # SBU
  145. async def async_turn_on(self, **kwargs):
  146. settings = (self.coordinator.data or {}).get("settings")
  147. await self.hass.async_add_executor_job(self._api.set_backup_mode, self._device_id, True, settings)
  148. await self.coordinator.async_request_refresh()
  149. async def async_turn_off(self, **kwargs):
  150. settings = (self.coordinator.data or {}).get("settings")
  151. await self.hass.async_add_executor_job(self._api.set_backup_mode, self._device_id, False, settings)
  152. await self.coordinator.async_request_refresh()