text: """Axis network device abstraction."""
  
  import asyncio
  
  import async_timeout
  import axis
  from axis.event_stream import OPERATION_INITIALIZED
  from axis.streammanager import SIGNAL_PLAYING
  
  from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
  from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN
  from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
  from homeassistant.const import (
      CONF_HOST,
      CONF_NAME,
      CONF_PASSWORD,
      CONF_PORT,
      CONF_TRIGGER_TIME,
      CONF_USERNAME,
  )
  from homeassistant.core import callback
  from homeassistant.exceptions import ConfigEntryNotReady
  from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
  from homeassistant.helpers.dispatcher import async_dispatcher_send
  
  from .const import (
      ATTR_MANUFACTURER,
      CONF_CAMERA,
      CONF_EVENTS,
      CONF_MODEL,
      DEFAULT_EVENTS,
      DEFAULT_TRIGGER_TIME,
      DOMAIN as AXIS_DOMAIN,
      LOGGER,
  )
  from .errors import AuthenticationRequired, CannotConnect
  
  
  class AxisNetworkDevice:
      """Manages a Axis device."""
  
      def __init__(self, hass, config_entry):
          """Initialize the device."""
          self.hass = hass
          self.config_entry = config_entry
          self.available = True
  
          self.api = None
          self.fw_version = None
          self.product_type = None
  
          self.listeners = []
  
      @property
      def host(self):
          """Return the host of this device."""
          return self.config_entry.data[CONF_HOST]
  
      @property
      def model(self):
          """Return the model of this device."""
          return self.config_entry.data[CONF_MODEL]
  
      @property
      def name(self):
          """Return the name of this device."""
          return self.config_entry.data[CONF_NAME]
  
      @property
      def serial(self):
          """Return the serial number of this device."""
          return self.config_entry.unique_id
  
      @property
      def option_camera(self):
          """Config entry option defining if camera should be used."""
          supported_formats = self.api.vapix.params.image_format
          return self.config_entry.options.get(CONF_CAMERA, bool(supported_formats))
  
      @property
      def option_events(self):
          """Config entry option defining if platforms based on events should be created."""
          return self.config_entry.options.get(CONF_EVENTS, DEFAULT_EVENTS)
  
      @property
      def option_trigger_time(self):
          """Config entry option defining minimum number of seconds to keep trigger high."""
          return self.config_entry.options.get(CONF_TRIGGER_TIME, DEFAULT_TRIGGER_TIME)
  
      @property
      def signal_reachable(self):
          """Device specific event to signal a change in connection status."""
          return f"axis_reachable_{self.serial}"
  
      @property
      def signal_new_event(self):
          """Device specific event to signal new device event available."""
          return f"axis_new_event_{self.serial}"
  
      @property
      def signal_new_address(self):
          """Device specific event to signal a change in device address."""
          return f"axis_new_address_{self.serial}"
  
      @callback
      def async_connection_status_callback(self, status):
          """Handle signals of device connection status.
  
          This is called on every RTSP keep-alive message.
          Only signal state change if state change is true.
          """
  
          if self.available != (status == SIGNAL_PLAYING):
              self.available = not self.available
              async_dispatcher_send(self.hass, self.signal_reachable, True)
  
      @callback
      def async_event_callback(self, action, event_id):
          """Call to configure events when initialized on event stream."""
          if action == OPERATION_INITIALIZED:
              async_dispatcher_send(self.hass, self.signal_new_event, event_id)
  
      @staticmethod
      async def async_new_address_callback(hass, entry):
          """Handle signals of device getting new address.
  
          This is a static method because a class method (bound method),
          can not be used with weak references.
          """
          device = hass.data[AXIS_DOMAIN][entry.unique_id]
          device.api.config.host = device.host
          async_dispatcher_send(hass, device.signal_new_address)
  
      async def async_update_device_registry(self):
          """Update device registry."""
          device_registry = await self.hass.helpers.device_registry.async_get_registry()
          device_registry.async_get_or_create(
              config_entry_id=self.config_entry.entry_id,
              connections={(CONNECTION_NETWORK_MAC, self.serial)},
              identifiers={(AXIS_DOMAIN, self.serial)},
              manufacturer=ATTR_MANUFACTURER,
              model=f"{self.model} {self.product_type}",
              name=self.name,
              sw_version=self.fw_version,
          )
  
      async def async_setup(self):
          """Set up the device."""
          try:
              self.api = await get_device(
                  self.hass,
                  host=self.config_entry.data[CONF_HOST],
                  port=self.config_entry.data[CONF_PORT],
                  username=self.config_entry.data[CONF_USERNAME],
                  password=self.config_entry.data[CONF_PASSWORD],
              )
  
          except CannotConnect:
              raise ConfigEntryNotReady
  
          except Exception:  # pylint: disable=broad-except
              LOGGER.error("Unknown error connecting with Axis device on %s", self.host)
              return False
  
          self.fw_version = self.api.vapix.params.firmware_version
          self.product_type = self.api.vapix.params.prodtype
  
          if self.option_camera:
  
              self.hass.async_create_task(
                  self.hass.config_entries.async_forward_entry_setup(
                      self.config_entry, CAMERA_DOMAIN
                  )
              )
  
          if self.option_events:
  
              self.api.stream.connection_status_callback = (
                  self.async_connection_status_callback
              )
              self.api.enable_events(event_callback=self.async_event_callback)
  
              platform_tasks = [
                  self.hass.config_entries.async_forward_entry_setup(
                      self.config_entry, platform
                  )
                  for platform in [BINARY_SENSOR_DOMAIN, SWITCH_DOMAIN]
              ]
              self.hass.async_create_task(self.start(platform_tasks))
  
          self.config_entry.add_update_listener(self.async_new_address_callback)
  
          return True
  
      async def start(self, platform_tasks):
          """Start the event stream when all platforms are loaded."""
          await asyncio.gather(*platform_tasks)
          self.api.start()
  
      @callback
      def shutdown(self, event):
          """Stop the event stream."""
          self.api.stop()
  
      async def async_reset(self):
          """Reset this device to default state."""
          platform_tasks = []
  
          if self.config_entry.options[CONF_CAMERA]:
              platform_tasks.append(
                  self.hass.config_entries.async_forward_entry_unload(
                      self.config_entry, CAMERA_DOMAIN
                  )
              )
  
          if self.config_entry.options[CONF_EVENTS]:
              self.api.stop()
              platform_tasks += [
                  self.hass.config_entries.async_forward_entry_unload(
                      self.config_entry, platform
                  )
                  for platform in [BINARY_SENSOR_DOMAIN, SWITCH_DOMAIN]
              ]
  
          await asyncio.gather(*platform_tasks)
  
          for unsub_dispatcher in self.listeners:
              unsub_dispatcher()
          self.listeners = []
  
          return True
  
  
  async def get_device(hass, host, port, username, password):
      """Create a Axis device."""
  
      device = axis.AxisDevice(
          host=host, port=port, username=username, password=password, web_proto="http",
      )
  
      device.vapix.initialize_params(preload_data=False)
      device.vapix.initialize_ports()
  
      try:
          with async_timeout.timeout(15):
  
              await asyncio.gather(
                  hass.async_add_executor_job(device.vapix.params.update_brand),
                  hass.async_add_executor_job(device.vapix.params.update_properties),
                  hass.async_add_executor_job(device.vapix.ports.update),
              )
  
          return device
  
      except axis.Unauthorized:
          LOGGER.warning("Connected to device at %s but not registered.", host)
          raise AuthenticationRequired
  
      except (asyncio.TimeoutError, axis.RequestError):
          LOGGER.error("Error connecting to the Axis device at %s", host)
          raise CannotConnect
  
      except axis.AxisException:
          LOGGER.exception("Unknown Axis communication error occurred")
          raise AuthenticationRequired
  