""" A bonary sensor entity that holds the state of each managed_device """ import logging from datetime import datetime from typing import Any from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN, STATE_ON from homeassistant.core import callback, HomeAssistant, State, Event from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.components.switch import ( SwitchEntity, ) from homeassistant.helpers.entity_platform import ( AddEntitiesCallback, ) from homeassistant.helpers.event import ( async_track_state_change_event, ) from .const import ( DOMAIN, name_to_unique_id, get_tz, EVENT_TYPE_SOLAR_OPTIMIZER_ENABLE_STATE_CHANGE, ) from .coordinator import SolarOptimizerCoordinator from .managed_device import ManagedDevice _LOGGER = logging.getLogger(__name__) async def async_setup_entry( hass: HomeAssistant, _, async_add_entities: AddEntitiesCallback ) -> None: """Setup the entries of type Binary sensor, one for each ManagedDevice""" _LOGGER.debug("Calling switch.async_setup_entry") coordinator: SolarOptimizerCoordinator = hass.data[DOMAIN]["coordinator"] entities = [] for _, device in enumerate(coordinator.devices): entity = ManagedDeviceSwitch( coordinator, hass, device.name, name_to_unique_id(device.name), device.entity_id, ) if entity is not None: entities.append(entity) entity = ManagedDeviceEnable(hass, device) if entity is not None: entities.append(entity) async_add_entities(entities) class ManagedDeviceSwitch(CoordinatorEntity, SwitchEntity): """The entity holding the algorithm calculation""" _entity_component_unrecorded_attributes = ( SwitchEntity._entity_component_unrecorded_attributes.union( frozenset( { "is_enabled", "is_active", "is_waiting", "is_usable", "can_change_power", "duration_sec", "duration_power_sec", "power_min", "power_max", "next_date_available", "next_date_available_power", "battery_soc_threshold", "battery_soc", } ) ) ) def __init__(self, coordinator, hass, name, idx, entity_id): _LOGGER.debug("Adding ManagedDeviceSwitch for %s", name) super().__init__(coordinator, context=idx) self._hass: HomeAssistant = hass self.idx = idx self._attr_name = "Solar Optimizer " + name self._attr_unique_id = "solar_optimizer_" + idx self._entity_id = entity_id # Try to get the state if it exists device: ManagedDevice = None if (device := coordinator.get_device_by_unique_id(self.idx)) is not None: self._attr_is_on = device.is_active else: self._attr_is_on = None async def async_added_to_hass(self) -> None: """The entity have been added to hass, listen to state change of the underlying entity""" await super().async_added_to_hass() # Arme l'écoute de la première entité listener_cancel = async_track_state_change_event( self.hass, [self._entity_id], self._on_state_change, ) # desarme le timer lors de la destruction de l'entité self.async_on_remove(listener_cancel) # desarme le timer lors de la destruction de l'entité self.async_on_remove( self._hass.bus.async_listen( event_type=EVENT_TYPE_SOLAR_OPTIMIZER_ENABLE_STATE_CHANGE, listener=self._on_enable_state_change, ) ) @callback async def _on_enable_state_change(self, event: Event) -> None: """Triggered when the ManagedDevice enable state have change""" # is it for me ? if ( not event.data or (device_id := event.data.get("device_unique_id")) != self.idx ): return # search for coordinator and device if not self.coordinator or not ( device := self.coordinator.get_device_by_unique_id(device_id) ): return _LOGGER.info( "Changing enabled state for %s to %s", device_id, device.is_enabled ) self.update_custom_attributes(device) self.async_write_ha_state() @callback async def _on_state_change(self, event: Event) -> None: """The entity have change its state""" _LOGGER.info( "Appel de on_state_change à %s avec l'event %s", datetime.now(), event ) if not event.data: return # search for coordinator and device if not self.coordinator or not ( device := self.coordinator.get_device_by_unique_id(self.idx) ): return new_state: State = event.data.get("new_state") # old_state: State = event.data.get("old_state") if new_state is None or new_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN): _LOGGER.debug("Pas d'état disponible. Evenement ignoré") return # On recherche la date de l'event pour la stocker dans notre état new_state = new_state.state == STATE_ON if new_state == self._attr_is_on: return self._attr_is_on = new_state # On sauvegarde le nouvel état self.update_custom_attributes(device) self.async_write_ha_state() def update_custom_attributes(self, device): """Add some custom attributes to the entity""" current_tz = get_tz(self._hass) self._attr_extra_state_attributes: dict(str, str) = { "is_enabled": device.is_enabled, "is_active": device.is_active, "is_waiting": device.is_waiting, "is_usable": device.is_usable, "can_change_power": device.can_change_power, "current_power": device.current_power, "requested_power": device.requested_power, "duration_sec": device.duration_sec, "duration_power_sec": device.duration_power_sec, "power_min": device.power_min, "power_max": device.power_max, "next_date_available": device.next_date_available.astimezone( current_tz ).isoformat(), "next_date_available_power": device.next_date_available_power.astimezone( current_tz ).isoformat(), "battery_soc_threshold": device._battery_soc_threshold, "battery_soc": device._battery_soc, } @callback def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" _LOGGER.debug("Calling _handle_coordinator_update for %s", self._attr_name) if not self.coordinator or not self.coordinator.data: _LOGGER.debug("No coordinator found or no data...") return device: ManagedDevice = self.coordinator.data.get(self.idx) if not device: # it is possible to not have device in coordinator update (if device is not enabled) _LOGGER.debug("No device %s found ...", self.idx) return self._attr_is_on = device.is_active self.update_custom_attributes(device) self.async_write_ha_state() async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" if not self.coordinator or not self.coordinator.data: return _LOGGER.info("Turn_on Solar Optimizer switch %s", self._attr_name) # search for coordinator and device if not self.coordinator or not ( device := self.coordinator.get_device_by_unique_id(self.idx) ): return if not self._attr_is_on: await device.activate() self._attr_is_on = True self.update_custom_attributes(device) self.async_write_ha_state() async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity on.""" if not self.coordinator or not self.coordinator.data: return _LOGGER.info("Turn_on Solar Optimizer switch %s", self._attr_name) # search for coordinator and device if not self.coordinator or not ( device := self.coordinator.get_device_by_unique_id(self.idx) ): return if self._attr_is_on: await device.deactivate() self._attr_is_on = False self.update_custom_attributes(device) self.async_write_ha_state() @property def device_info(self): # Retournez des informations sur le périphérique associé à votre entité return { "identifiers": {(DOMAIN, "solar_optimizer_device")}, "name": "Solar Optimizer", # Autres attributs du périphérique ici } @property def get_attr_extra_state_attributes(self): """Get the extra state attributes for the entity""" return self._attr_extra_state_attributes class ManagedDeviceEnable(SwitchEntity, RestoreEntity): """The that enables the ManagedDevice optimisation with""" _device: ManagedDevice def __init__(self, hass: HomeAssistant, device: ManagedDevice): self._hass: HomeAssistant = hass self._device = device self._attr_name = "Enable Solar Optimizer " + device.name self._attr_unique_id = "solar_optimizer_enable_" + name_to_unique_id( device.name ) self._attr_is_on = True @property def device_info(self): # Retournez des informations sur le périphérique associé à votre entité return { "identifiers": {(DOMAIN, "solar_optimizer_device")}, "name": "Solar Optimizer", # Autres attributs du périphérique ici } @property def icon(self) -> str | None: return "mdi:check" async def async_added_to_hass(self): await super().async_added_to_hass() # Récupérer le dernier état sauvegardé de l'entité last_state = await self.async_get_last_state() # Si l'état précédent existe, vous pouvez l'utiliser if last_state is not None: self._attr_is_on = last_state.state == "on" else: # Si l'état précédent n'existe pas, initialisez l'état comme vous le souhaitez self._attr_is_on = True # this breaks the start of integration self.update_device_enabled() @callback async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" self._attr_is_on = True self.async_write_ha_state() self.update_device_enabled() @callback async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" self._attr_is_on = False self.async_write_ha_state() self.update_device_enabled() def update_device_enabled(self) -> None: """Update the device is enabled flag""" if not self._device: return self._device.set_enable(self._attr_is_on)