text: """Support for hunter douglas shades."""
  import asyncio
  from contextlib import suppress
  import logging
  
  from aiopvapi.helpers.constants import ATTR_POSITION1, ATTR_POSITION_DATA
  from aiopvapi.resources.shade import (
      ATTR_POSKIND1,
      MAX_POSITION,
      MIN_POSITION,
      factory as PvShade,
  )
  import async_timeout
  
  from homeassistant.components.cover import (
      ATTR_POSITION,
      CoverDeviceClass,
      CoverEntity,
      CoverEntityFeature,
  )
  from homeassistant.config_entries import ConfigEntry
  from homeassistant.core import HomeAssistant, callback
  from homeassistant.helpers.entity_platform import AddEntitiesCallback
  from homeassistant.helpers.event import async_call_later
  
  from .const import (
      COORDINATOR,
      DEVICE_INFO,
      DEVICE_MODEL,
      DOMAIN,
      LEGACY_DEVICE_MODEL,
      PV_API,
      PV_ROOM_DATA,
      PV_SHADE_DATA,
      ROOM_ID_IN_SHADE,
      ROOM_NAME_UNICODE,
      SHADE_RESPONSE,
      STATE_ATTRIBUTE_ROOM_NAME,
  )
  from .entity import ShadeEntity
  
  _LOGGER = logging.getLogger(__name__)
  
  # Estimated time it takes to complete a transition
  # from one state to another
  TRANSITION_COMPLETE_DURATION = 40
  
  PARALLEL_UPDATES = 1
  
  RESYNC_DELAY = 60
  
  
  async def async_setup_entry(
      hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
  ) -> None:
      """Set up the hunter douglas shades."""
  
      pv_data = hass.data[DOMAIN][entry.entry_id]
      room_data = pv_data[PV_ROOM_DATA]
      shade_data = pv_data[PV_SHADE_DATA]
      pv_request = pv_data[PV_API]
      coordinator = pv_data[COORDINATOR]
      device_info = pv_data[DEVICE_INFO]
  
      entities = []
      for raw_shade in shade_data.values():
          # The shade may be out of sync with the hub
          # so we force a refresh when we add it if
          # possible
          shade = PvShade(raw_shade, pv_request)
          name_before_refresh = shade.name
          with suppress(asyncio.TimeoutError):
              async with async_timeout.timeout(1):
                  await shade.refresh()
  
          if ATTR_POSITION_DATA not in shade.raw_data:
              _LOGGER.info(
                  "The %s shade was skipped because it is missing position data",
                  name_before_refresh,
              )
              continue
          room_id = shade.raw_data.get(ROOM_ID_IN_SHADE)
          room_name = room_data.get(room_id, {}).get(ROOM_NAME_UNICODE, "")
          entities.append(
              PowerViewShade(
                  coordinator, device_info, room_name, shade, name_before_refresh
              )
          )
      async_add_entities(entities)
  
  
  def hd_position_to_hass(hd_position):
      """Convert hunter douglas position to hass position."""
      return round((hd_position / MAX_POSITION) * 100)
  
  
  def hass_position_to_hd(hass_position):
      """Convert hass position to hunter douglas position."""
      return int(hass_position / 100 * MAX_POSITION)
  
  
  class PowerViewShade(ShadeEntity, CoverEntity):
      """Representation of a powerview shade."""
  
      # The hub frequently reports stale states
      _attr_assumed_state = True
  
      def __init__(self, coordinator, device_info, room_name, shade, name):
          """Initialize the shade."""
          super().__init__(coordinator, device_info, room_name, shade, name)
          self._shade = shade
          self._is_opening = False
          self._is_closing = False
          self._last_action_timestamp = 0
          self._scheduled_transition_update = None
          self._current_cover_position = MIN_POSITION
          self._attr_supported_features = (
              CoverEntityFeature.OPEN
              | CoverEntityFeature.CLOSE
              | CoverEntityFeature.SET_POSITION
          )
          if self._device_info[DEVICE_MODEL] != LEGACY_DEVICE_MODEL:
              self._attr_supported_features |= CoverEntityFeature.STOP
          self._forced_resync = None
  
      @property
      def extra_state_attributes(self):
          """Return the state attributes."""
          return {STATE_ATTRIBUTE_ROOM_NAME: self._room_name}
  
      @property
      def is_closed(self):
          """Return if the cover is closed."""
          return self._current_cover_position == MIN_POSITION
  
      @property
      def is_opening(self):
          """Return if the cover is opening."""
          return self._is_opening
  
      @property
      def is_closing(self):
          """Return if the cover is closing."""
          return self._is_closing
  
      @property
      def current_cover_position(self):
          """Return the current position of cover."""
          return hd_position_to_hass(self._current_cover_position)
  
      @property
      def device_class(self):
          """Return device class."""
          return CoverDeviceClass.SHADE
  
      @property
      def name(self):
          """Return the name of the shade."""
          return self._shade_name
  
      async def async_close_cover(self, **kwargs):
          """Close the cover."""
          await self._async_move(0)
  
      async def async_open_cover(self, **kwargs):
          """Open the cover."""
          await self._async_move(100)
  
      async def async_stop_cover(self, **kwargs):
          """Stop the cover."""
          # Cancel any previous updates
          self._async_cancel_scheduled_transition_update()
          self._async_update_from_command(await self._shade.stop())
          await self._async_force_refresh_state()
  
      async def async_set_cover_position(self, **kwargs):
          """Move the shade to a specific position."""
          if ATTR_POSITION not in kwargs:
              return
          await self._async_move(kwargs[ATTR_POSITION])
  
      async def _async_move(self, target_hass_position):
          """Move the shade to a position."""
          current_hass_position = hd_position_to_hass(self._current_cover_position)
          steps_to_move = abs(current_hass_position - target_hass_position)
          self._async_schedule_update_for_transition(steps_to_move)
          self._async_update_from_command(
              await self._shade.move(
                  {
                      ATTR_POSITION1: hass_position_to_hd(target_hass_position),
                      ATTR_POSKIND1: 1,
                  }
              )
          )
          self._is_opening = False
          self._is_closing = False
          if target_hass_position > current_hass_position:
              self._is_opening = True
          elif target_hass_position < current_hass_position:
              self._is_closing = True
          self.async_write_ha_state()
  
      @callback
      def _async_update_from_command(self, raw_data):
          """Update the shade state after a command."""
          if not raw_data or SHADE_RESPONSE not in raw_data:
              return
          self._async_process_new_shade_data(raw_data[SHADE_RESPONSE])
  
      @callback
      def _async_process_new_shade_data(self, data):
          """Process new data from an update."""
          self._shade.raw_data = data
          self._async_update_current_cover_position()
  
      @callback
      def _async_update_current_cover_position(self):
          """Update the current cover position from the data."""
          _LOGGER.debug("Raw data update: %s", self._shade.raw_data)
          position_data = self._shade.raw_data.get(ATTR_POSITION_DATA, {})
          if ATTR_POSITION1 in position_data:
              self._current_cover_position = int(position_data[ATTR_POSITION1])
          self._is_opening = False
          self._is_closing = False
  
      @callback
      def _async_cancel_scheduled_transition_update(self):
          """Cancel any previous updates."""
          if self._scheduled_transition_update:
              self._scheduled_transition_update()
              self._scheduled_transition_update = None
          if self._forced_resync:
              self._forced_resync()
              self._forced_resync = None
  
      @callback
      def _async_schedule_update_for_transition(self, steps):
          self.async_write_ha_state()
  
          # Cancel any previous updates
          self._async_cancel_scheduled_transition_update()
  
          est_time_to_complete_transition = 1 + int(
              TRANSITION_COMPLETE_DURATION * (steps / 100)
          )
  
          _LOGGER.debug(
              "Estimated time to complete transition of %s steps for %s: %s",
              steps,
              self.name,
              est_time_to_complete_transition,
          )
  
          # Schedule an update for when we expect the transition
          # to be completed.
          self._scheduled_transition_update = async_call_later(
              self.hass,
              est_time_to_complete_transition,
              self._async_complete_schedule_update,
          )
  
      async def _async_complete_schedule_update(self, _):
          """Update status of the cover."""
          _LOGGER.debug("Processing scheduled update for %s", self.name)
          self._scheduled_transition_update = None
          await self._async_force_refresh_state()
          self._forced_resync = async_call_later(
              self.hass, RESYNC_DELAY, self._async_force_resync
          )
  
      async def _async_force_resync(self, *_):
          """Force a resync after an update since the hub may have stale state."""
          self._forced_resync = None
          await self._async_force_refresh_state()
  
      async def _async_force_refresh_state(self):
          """Refresh the cover state and force the device cache to be bypassed."""
          await self._shade.refresh()
          self._async_update_current_cover_position()
          self.async_write_ha_state()
  
      async def async_added_to_hass(self):
          """When entity is added to hass."""
          self._async_update_current_cover_position()
          self.async_on_remove(
              self.coordinator.async_add_listener(self._async_update_shade_from_group)
          )
  
      async def async_will_remove_from_hass(self):
          """Cancel any pending refreshes."""
          self._async_cancel_scheduled_transition_update()
  
      @callback
      def _async_update_shade_from_group(self):
          """Update with new data from the coordinator."""
          if self._scheduled_transition_update or self._forced_resync:
              # If a transition in in progress
              # the data will be wrong
              return
          self._async_process_new_shade_data(self.coordinator.data[self._shade.id])
          self.async_write_ha_state()
  