text: """
  Device entity for Zigbee Home Automation.
  
  For more details about this component, please refer to the documentation at
  https://home-assistant.io/components/zha/
  """
  
  import logging
  import time
  
  from homeassistant.core import callback
  from homeassistant.util import slugify
  from .entity import ZhaEntity
  from .const import POWER_CONFIGURATION_CHANNEL, SIGNAL_STATE_ATTR
  
  _LOGGER = logging.getLogger(__name__)
  
  BATTERY_SIZES = {
      0: 'No battery',
      1: 'Built in',
      2: 'Other',
      3: 'AA',
      4: 'AAA',
      5: 'C',
      6: 'D',
      7: 'CR2',
      8: 'CR123A',
      9: 'CR2450',
      10: 'CR2032',
      11: 'CR1632',
      255: 'Unknown'
  }
  
  STATE_ONLINE = 'online'
  STATE_OFFLINE = 'offline'
  
  
  class ZhaDeviceEntity(ZhaEntity):
      """A base class for ZHA devices."""
  
      def __init__(self, zha_device, channels, keepalive_interval=7200,
                   **kwargs):
          """Init ZHA endpoint entity."""
          ieee = zha_device.ieee
          ieeetail = ''.join(['%02x' % (o, ) for o in ieee[-4:]])
          unique_id = None
          if zha_device.manufacturer is not None and \
                  zha_device.model is not None:
              unique_id = "{}_{}_{}".format(
                  slugify(zha_device.manufacturer),
                  slugify(zha_device.model),
                  ieeetail,
              )
          else:
              unique_id = str(ieeetail)
  
          kwargs['component'] = 'zha'
          super().__init__(unique_id, zha_device, channels, skip_entity_id=True,
                           **kwargs)
  
          self._keepalive_interval = keepalive_interval
          self._device_state_attributes.update({
              'nwk': '0x{0:04x}'.format(zha_device.nwk),
              'ieee': str(zha_device.ieee),
              'lqi': zha_device.lqi,
              'rssi': zha_device.rssi,
          })
          self._should_poll = True
          self._battery_channel = self.cluster_channels.get(
              POWER_CONFIGURATION_CHANNEL)
  
      @property
      def state(self) -> str:
          """Return the state of the entity."""
          return self._state
  
      @property
      def available(self):
          """Return True if device is available."""
          return self._zha_device.available
  
      @property
      def device_state_attributes(self):
          """Return device specific state attributes."""
          update_time = None
          device = self._zha_device
          if device.last_seen is not None and not self.available:
              time_struct = time.localtime(device.last_seen)
              update_time = time.strftime("%Y-%m-%dT%H:%M:%S", time_struct)
              self._device_state_attributes['last_seen'] = update_time
          if ('last_seen' in self._device_state_attributes and
                  self.available):
              del self._device_state_attributes['last_seen']
          self._device_state_attributes['lqi'] = device.lqi
          self._device_state_attributes['rssi'] = device.rssi
          return self._device_state_attributes
  
      async def async_added_to_hass(self):
          """Run when about to be added to hass."""
          await super().async_added_to_hass()
          if self._battery_channel:
              await self.async_accept_signal(
                  self._battery_channel, SIGNAL_STATE_ATTR,
                  self.async_update_state_attribute)
              # only do this on add to HA because it is static
              await self._async_init_battery_values()
  
      async def async_update(self):
          """Handle polling."""
          if self._zha_device.last_seen is None:
              self._zha_device.update_available(False)
          else:
              difference = time.time() - self._zha_device.last_seen
              if difference > self._keepalive_interval:
                  self._zha_device.update_available(False)
              else:
                  self._zha_device.update_available(True)
                  if self._battery_channel:
                      await self.async_get_latest_battery_reading()
  
      @callback
      def async_set_available(self, available):
          """Set entity availability."""
          if available:
              self._state = STATE_ONLINE
          else:
              self._state = STATE_OFFLINE
          super().async_set_available(available)
  
      async def _async_init_battery_values(self):
          """Get initial battery level and battery info from channel cache."""
          battery_size = await self._battery_channel.get_attribute_value(
              'battery_size')
          if battery_size is not None:
              self._device_state_attributes['battery_size'] = BATTERY_SIZES.get(
                  battery_size, 'Unknown')
  
          battery_quantity = await self._battery_channel.get_attribute_value(
              'battery_quantity')
          if battery_quantity is not None:
              self._device_state_attributes['battery_quantity'] = \
                  battery_quantity
          await self.async_get_latest_battery_reading()
  
      async def async_get_latest_battery_reading(self):
          """Get the latest battery reading from channels cache."""
          battery = await self._battery_channel.get_attribute_value(
              'battery_percentage_remaining')
          if battery is not None:
              self._device_state_attributes['battery_level'] = battery
  