"""Support for MotionMount numeric control.""" from datetime import timedelta import logging import socket from typing import override import motionmount from homeassistant.components.select import SelectEntity from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from . import MotionMountConfigEntry from .const import DOMAIN, WALL_PRESET_NAME from .entity import MotionMountEntity _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(seconds=60) PARALLEL_UPDATES = 0 async def async_setup_entry( hass: HomeAssistant, entry: MotionMountConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up Vogel's MotionMount from a config entry.""" mm = entry.runtime_data async_add_entities([MotionMountPresets(mm, entry)], True) class MotionMountPresets(MotionMountEntity, SelectEntity): """The presets of a MotionMount.""" _attr_should_poll = True _attr_translation_key = "motionmount_preset" _name_to_index: dict[str, int] def __init__( self, mm: motionmount.MotionMount, config_entry: MotionMountConfigEntry, ) -> None: """Initialize Preset selector.""" super().__init__(mm, config_entry) self._attr_unique_id = f"{self._base_unique_id}-preset" self._presets: list[motionmount.Preset] = [] self._attr_current_option = None def _update_options(self, presets: list[motionmount.Preset]) -> None: """Convert presets to select options.""" # Ordered list of options (wall first, then presets) options = [WALL_PRESET_NAME] + [preset.name for preset in presets] # Build mapping name → index (wall = 0) self._name_to_index = {WALL_PRESET_NAME: 0} self._name_to_index.update({preset.name: preset.index for preset in presets}) self._attr_options = options async def _ensure_connected(self) -> bool: """Make sure there is a connection with the MotionMount. Returns false if the connection failed to be ensured. """ if self.mm.is_connected: return True try: await self.mm.connect() except ConnectionError, TimeoutError, socket.gaierror: # We're not interested in exceptions here. In case of a failed connection # the try/except from the caller will report it. # The purpose of `_ensure_connected()` is only to make sure we try to # reconnect, where failures should not be logged each time return False # Check we're properly authenticated or be able to become so if not self.mm.is_authenticated: if self.pin is None: await self.mm.disconnect() self.config_entry.async_start_reauth(self.hass) return False await self.mm.authenticate(self.pin) if not self.mm.is_authenticated: self.pin = None await self.mm.disconnect() self.config_entry.async_start_reauth(self.hass) return False _LOGGER.debug("Successfully reconnected to MotionMount") return True async def async_update(self) -> None: """Get latest state from MotionMount.""" if not await self._ensure_connected(): return try: self._presets = await self.mm.get_presets() except (TimeoutError, socket.gaierror) as ex: _LOGGER.warning("Failed to communicate with MotionMount: %s", ex) else: self._update_options(self._presets) @property @override def current_option(self) -> str | None: """Get the current option.""" # When the mount is moving we return the currently selected option if self.mm.is_moving: return self._attr_current_option # When the mount isn't moving we select the option # that matches the current position self._attr_current_option = None if self.mm.extension == 0 and self.mm.turn == 0: self._attr_current_option = self._attr_options[0] # Select Wall preset else: for preset in self._presets: if ( preset.extension == self.mm.extension and preset.turn == self.mm.turn ): self._attr_current_option = f"{preset.index}: {preset.name}" break return self._attr_current_option @override async def async_select_option(self, option: str) -> None: """Set the new option.""" index = self._name_to_index.get(option) if index is None: raise HomeAssistantError(f"Unknown preset selected: {option}") try: await self.mm.go_to_preset(index) except (TimeoutError, socket.gaierror) as ex: raise HomeAssistantError( translation_domain=DOMAIN, translation_key="failed_communication", ) from ex else: self._attr_current_option = option