switch.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  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 .const import DOMAIN
  10. _LOGGER = logging.getLogger(__name__)
  11. async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback) -> None:
  12. data = hass.data[DOMAIN][entry.entry_id]
  13. api = data["api"]
  14. station_id: str = data["station_id"]
  15. device_coordinators = data["device_coordinators"]
  16. entities: list[SwitchEntity] = []
  17. for device_id, coordinator in device_coordinators.items():
  18. device_name = (coordinator.device_meta or {}).get("name") or device_id
  19. entities.extend(
  20. [
  21. SolarOfThingsGridChargingSwitch(api, coordinator, station_id, device_id, device_name),
  22. SolarOfThingsGridFeedInSwitch(api, coordinator, station_id, device_id, device_name),
  23. SolarOfThingsBackupModeSwitch(api, coordinator, station_id, device_id, device_name),
  24. ]
  25. )
  26. async_add_entities(entities)
  27. def _setting_value(coordinator_data: dict | None, key: str) -> int | None:
  28. """Extract the integer value for a device setting key from coordinator data."""
  29. settings = ((coordinator_data or {}).get("settings") or {})
  30. entry = settings.get(key)
  31. if entry is None:
  32. return None
  33. raw = entry.get("value") if isinstance(entry, dict) else entry
  34. try:
  35. return int(raw)
  36. except (TypeError, ValueError):
  37. return None
  38. class _BaseSwitch(CoordinatorEntity, SwitchEntity):
  39. def __init__(self, api, coordinator, station_id: str, device_id: str, device_name: str) -> None:
  40. super().__init__(coordinator)
  41. self._api = api
  42. self._station_id = station_id
  43. self._device_id = device_id
  44. self._device_name = device_name
  45. @property
  46. def device_info(self):
  47. return {
  48. "identifiers": {(DOMAIN, self._device_id)},
  49. "name": self._device_name,
  50. "manufacturer": "Siseli",
  51. "model": (self.coordinator.data.get("device_meta") or {}).get("model") if self.coordinator.data else None,
  52. "via_device": (DOMAIN, self._station_id),
  53. }
  54. class SolarOfThingsGridChargingSwitch(_BaseSwitch):
  55. """Switch for AC Input Range setting (acInputRangeSetting).
  56. 0 = Appliance mode – wide input voltage range, grid charging allowed.
  57. 1 = UPS mode – narrow voltage range, stricter bypass behaviour.
  58. The switch reports ON when the inverter is in Appliance (grid-charging) mode.
  59. """
  60. def __init__(self, api, coordinator, station_id: str, device_id: str, device_name: str) -> None:
  61. super().__init__(api, coordinator, station_id, device_id, device_name)
  62. self._attr_name = f"{device_name} Grid Charging (AC Input Range)"
  63. self._attr_unique_id = f"{DOMAIN}_{station_id}_{device_id}_grid_charging"
  64. self._attr_device_class = SwitchDeviceClass.SWITCH
  65. self._attr_icon = "mdi:transmission-tower"
  66. @property
  67. def is_on(self) -> bool | None:
  68. val = _setting_value(self.coordinator.data, "acInputRangeSetting")
  69. if val is None:
  70. return None
  71. return val == 0 # 0=Appliance (charging OK), 1=UPS (bypass)
  72. async def async_turn_on(self, **kwargs):
  73. await self.hass.async_add_executor_job(self._api.set_grid_charging, self._device_id, True)
  74. await self.coordinator.async_request_refresh()
  75. async def async_turn_off(self, **kwargs):
  76. await self.hass.async_add_executor_job(self._api.set_grid_charging, self._device_id, False)
  77. await self.coordinator.async_request_refresh()
  78. class SolarOfThingsGridFeedInSwitch(_BaseSwitch):
  79. """Switch for GRID grid switch (batteryPowerLimitingSetting).
  80. 0 = OFF (grid switch disabled / feed-in off).
  81. 1 = ON (grid switch enabled / feed-in on).
  82. """
  83. def __init__(self, api, coordinator, station_id: str, device_id: str, device_name: str) -> None:
  84. super().__init__(api, coordinator, station_id, device_id, device_name)
  85. self._attr_name = f"{device_name} Grid Feed-In"
  86. self._attr_unique_id = f"{DOMAIN}_{station_id}_{device_id}_grid_feed_in"
  87. self._attr_device_class = SwitchDeviceClass.SWITCH
  88. self._attr_icon = "mdi:transmission-tower-export"
  89. @property
  90. def is_on(self) -> bool | None:
  91. val = _setting_value(self.coordinator.data, "batteryPowerLimitingSetting")
  92. if val is None:
  93. return None
  94. return val == 1 # 1=ON
  95. async def async_turn_on(self, **kwargs):
  96. await self.hass.async_add_executor_job(self._api.set_grid_feed_in, self._device_id, True)
  97. await self.coordinator.async_request_refresh()
  98. async def async_turn_off(self, **kwargs):
  99. await self.hass.async_add_executor_job(self._api.set_grid_feed_in, self._device_id, False)
  100. await self.coordinator.async_request_refresh()
  101. class SolarOfThingsBackupModeSwitch(_BaseSwitch):
  102. """Switch that maps to Output Source Priority SBU (backup/off-grid biased).
  103. ON → outputSourcePrioritySetting = 2 (SBU: Solar+Battery first, grid last).
  104. OFF → outputSourcePrioritySetting = 1 (SUB: Solar first, grid as supplement).
  105. Note: turning this switch ON will also change the Operating Mode select entity
  106. to 'Solar+Battery First (SBU)', which is the expected behaviour.
  107. """
  108. def __init__(self, api, coordinator, station_id: str, device_id: str, device_name: str) -> None:
  109. super().__init__(api, coordinator, station_id, device_id, device_name)
  110. self._attr_name = f"{device_name} Backup Mode (SBU Priority)"
  111. self._attr_unique_id = f"{DOMAIN}_{station_id}_{device_id}_backup_mode"
  112. self._attr_device_class = SwitchDeviceClass.SWITCH
  113. self._attr_icon = "mdi:battery-lock"
  114. @property
  115. def is_on(self) -> bool | None:
  116. val = _setting_value(self.coordinator.data, "outputSourcePrioritySetting")
  117. if val is None:
  118. return None
  119. return val == 2 # SBU
  120. async def async_turn_on(self, **kwargs):
  121. await self.hass.async_add_executor_job(self._api.set_backup_mode, self._device_id, True)
  122. await self.coordinator.async_request_refresh()
  123. async def async_turn_off(self, **kwargs):
  124. await self.hass.async_add_executor_job(self._api.set_backup_mode, self._device_id, False)
  125. await self.coordinator.async_request_refresh()