pyControl4.websocket API documentation
Description: Handles Websocket connections to a Control4 Director, allowing for real-time updates using callbacks.

# Module `pyControl4.websocket`

Handles Websocket connections to a Control4 Director, allowing for real-time updates using callbacks.

Expand source code

    """Handles Websocket connections to a Control4 Director, allowing for real-time updates using callbacks."""
    import aiohttp
    import async_timeout
    import socketio
    import logging

    from .error_handling import checkResponseForError

    _LOGGER = logging.getLogger(__name__)

    class _C4DirectorNamespace(socketio.AsyncClientNamespace):
        def __init__(self, *args, **kwargs):
            self.url = kwargs.pop("url")
            self.token = kwargs.pop("token")
            self.callback = kwargs.pop("callback")
            self.session = kwargs.pop("session")
            self.connect_callback = kwargs.pop("connect_callback")
            self.disconnect_callback = kwargs.pop("disconnect_callback")
            super().__init__(*args, **kwargs)
            self.uri = "/api/v1/items/datatoui"
            self.subscriptionId = None
            self.connected = False

        async def on_connect(self):
            _LOGGER.debug("Control4 Director socket.io connection established!")
            if self.connect_callback is not None:
                await self.connect_callback()

        async def on_disconnect(self):
            self.connected = False
            self.subscriptionId = None
            _LOGGER.debug("Control4 Director socket.io disconnected.")
            if self.disconnect_callback is not None:
                await self.disconnect_callback()

        async def trigger_event(self, event, *args):
            if event == "subscribe":
                await self.on_subscribe(*args)
            elif event == "connect":
                await self.on_connect()
            elif event == "disconnect":
                await self.on_disconnect()
            elif event == "clientId":
                await self.on_clientId(*args)
            elif event == self.subscriptionId:
                msg = args[0]
                if "status" in msg:
                    _LOGGER.debug(f'Status message received from Director: {msg["status"]}')
                    await self.emit("2")
                else:
                    await self.callback(args[0])

        async def on_clientId(self, clientId):
            await self.emit("2probe")
            if not self.connected and not self.subscriptionId:
                _LOGGER.debug("Fetching subscriptionID from Control4")
                if self.session is None:
                    async with aiohttp.ClientSession(
                        connector=aiohttp.TCPConnector(verify_ssl=False)
                    ) as session:
                        with async_timeout.timeout(10):
                            async with session.get(
                                self.url + self.uri,
                                params={"JWT": self.token, "SubscriptionClient": clientId},
                            ) as resp:
                                await checkResponseForError(await resp.text())
                                data = await resp.json()
                                self.connected = True
                                self.subscriptionId = data["subscriptionId"]
                                await self.emit("startSubscription", self.subscriptionId)
                else:
                    with async_timeout.timeout(10):
                        async with self.session.get(
                            self.url + self.uri,
                            params={"JWT": self.token, "SubscriptionClient": clientId},
                        ) as resp:
                            await checkResponseForError(await resp.text())
                            data = await resp.json()
                            self.connected = True
                            self.subscriptionId = data["subscriptionId"]
                            await self.emit("startSubscription", self.subscriptionId)

        async def on_subscribe(self, message):
            await self.message(message)

    class C4Websocket:
        def __init__(
            self,
            ip,
            session_no_verify_ssl: aiohttp.ClientSession = None,
            connect_callback=None,
            disconnect_callback=None,
        ):
            """Creates a Control4 Websocket object.

            Parameters:
                `ip` - The IP address of the Control4 Director/Controller.

                `session` - (Optional) Allows the use of an
                            `aiohttp.ClientSession` object
                            for all network requests. This
                            session will not be closed by the library.
                            If not provided, the library will open and
                            close its own `ClientSession`s as needed.

                `connect_callback` - (Optional) A callback to be called when the Websocket connection is opened or reconnected after a network error.

                `disconnect_callback` - (Optional) A callback to be called when the Websocket connection is lost due to a network error.
            """
            self.base_url = "https://{}".format(ip)
            self.wss_url = "wss://{}".format(ip)
            self.session = session_no_verify_ssl
            self.connect_callback = connect_callback
            self.disconnect_callback = disconnect_callback

            # Keep track of the callbacks registered for each item id
            self._item_callbacks = dict()
            # Initialize self._sio to None
            self._sio = None

        @property
        def item_callbacks(self):
            """Returns a dictionary of registered item ids (key) and their callbacks (value).

            item_callbacks cannot be modified directly. Use add_item_callback() and remove_item_callback() instead."""
            return self._item_callbacks

        def add_item_callback(self, item_id, callback):
            """Register a callback to receive updates about an item.
            If a callback is already registered for the item, it will be overwritten with the provided callback.

            Parameters:
                `item_id` - The Control4 item ID.

                `callback` - The callback to be called when an update is received for the provided item id.
            """

            _LOGGER.debug("Subscribing to updates for item id: %s", item_id)

            self._item_callbacks[item_id] = callback

        def remove_item_callback(self, item_id):
            """Unregister callback for an item.

            Parameters:
                `item_id` - The Control4 item ID.
            """
            self._item_callbacks.pop(item_id)

        async def sio_connect(self, director_bearer_token):
            """Start WebSockets connection and listen, using the provided director_bearer_token to authenticate with the Control4 Director.
            If a connection already exists, it will be disconnected and a new connection will be created.

            This function should be called using a new token every 86400 seconds (the expiry time of the director tokens), otherwise the Control4 Director will stop sending WebSocket messages.

            Parameters:
                `director_bearer_token` - The bearer token used to authenticate with the Director. See `pyControl4.account.C4Account.getDirectorBearerToken` for how to get this.
            """
            # Disconnect previous sio object
            await self.sio_disconnect()

            self._sio = socketio.AsyncClient(ssl_verify=False)
            self._sio.register_namespace(
                _C4DirectorNamespace(
                    token=director_bearer_token,
                    url=self.base_url,
                    callback=self._callback,
                    session=self.session,
                    connect_callback=self.connect_callback,
                    disconnect_callback=self.disconnect_callback,
                )
            )
            await self._sio.connect(
                self.wss_url,
                transports=["websocket"],
                headers={"JWT": director_bearer_token},
            )

        async def sio_disconnect(self):
            """Disconnects the WebSockets connection, if it has been created."""
            if isinstance(self._sio, socketio.AsyncClient):
                await self._sio.disconnect()

        async def _callback(self, message):
            if "status" in message:
                _LOGGER.debug(f'Subscription {message["status"]}')
                return True
            if isinstance(message, list):
                for m in message:
                    await self._process_message(m)
            else:
                await self._process_message(message)

        async def _process_message(self, message):
            """Process an incoming event message."""
            _LOGGER.debug(message)
            try:
                c = self._item_callbacks[message["iddevice"]]
            except KeyError:
                _LOGGER.debug("No Callback for device id {}".format(message["iddevice"]))
                return True

            if isinstance(message, list):
                for m in message:
                    await c(message["iddevice"], m)
            else:
                await c(message["iddevice"], message)

        async def _execute_callback(self, callback, *args, **kwargs):
            """Callback with some data capturing any excpetions."""
            try:
                self.sio.emit("ping")
                await callback(*args, **kwargs)
            except Exception as exc:
                _LOGGER.warning("Captured exception during callback: {}".format(str(exc)))

## Classes

`class C4Websocket (ip, session_no_verify_ssl: aiohttp.client.ClientSession = None, connect_callback=None, disconnect_callback=None)`

  Creates a Control4 Websocket object.

  ## Parameters

  `ip` - The IP address of the Control4 Director/Controller.

  `session` - (Optional) Allows the use of an `aiohttp.ClientSession` object for all network requests. This session will not be closed by the library. If not provided, the library will open and close its own `ClientSession`s as needed.

  `connect_callback` - (Optional) A callback to be called when the Websocket connection is opened or reconnected after a network error.

  `disconnect_callback` - (Optional) A callback to be called when the Websocket connection is lost due to a network error.

  Expand source code

      class C4Websocket:
          def __init__(
              self,
              ip,
              session_no_verify_ssl: aiohttp.ClientSession = None,
              connect_callback=None,
              disconnect_callback=None,
          ):
              """Creates a Control4 Websocket object.

              Parameters:
                  `ip` - The IP address of the Control4 Director/Controller.

                  `session` - (Optional) Allows the use of an
                              `aiohttp.ClientSession` object
                              for all network requests. This
                              session will not be closed by the library.
                              If not provided, the library will open and
                              close its own `ClientSession`s as needed.

                  `connect_callback` - (Optional) A callback to be called when the Websocket connection is opened or reconnected after a network error.

                  `disconnect_callback` - (Optional) A callback to be called when the Websocket connection is lost due to a network error.
              """
              self.base_url = "https://{}".format(ip)
              self.wss_url = "wss://{}".format(ip)
              self.session = session_no_verify_ssl
              self.connect_callback = connect_callback
              self.disconnect_callback = disconnect_callback

              # Keep track of the callbacks registered for each item id
              self._item_callbacks = dict()
              # Initialize self._sio to None
              self._sio = None

          @property
          def item_callbacks(self):
              """Returns a dictionary of registered item ids (key) and their callbacks (value).

              item_callbacks cannot be modified directly. Use add_item_callback() and remove_item_callback() instead."""
              return self._item_callbacks

          def add_item_callback(self, item_id, callback):
              """Register a callback to receive updates about an item.
              If a callback is already registered for the item, it will be overwritten with the provided callback.

              Parameters:
                  `item_id` - The Control4 item ID.

                  `callback` - The callback to be called when an update is received for the provided item id.
              """

              _LOGGER.debug("Subscribing to updates for item id: %s", item_id)

              self._item_callbacks[item_id] = callback

          def remove_item_callback(self, item_id):
              """Unregister callback for an item.

              Parameters:
                  `item_id` - The Control4 item ID.
              """
              self._item_callbacks.pop(item_id)

          async def sio_connect(self, director_bearer_token):
              """Start WebSockets connection and listen, using the provided director_bearer_token to authenticate with the Control4 Director.
              If a connection already exists, it will be disconnected and a new connection will be created.

              This function should be called using a new token every 86400 seconds (the expiry time of the director tokens), otherwise the Control4 Director will stop sending WebSocket messages.

              Parameters:
                  `director_bearer_token` - The bearer token used to authenticate with the Director. See `pyControl4.account.C4Account.getDirectorBearerToken` for how to get this.
              """
              # Disconnect previous sio object
              await self.sio_disconnect()

              self._sio = socketio.AsyncClient(ssl_verify=False)
              self._sio.register_namespace(
                  _C4DirectorNamespace(
                      token=director_bearer_token,
                      url=self.base_url,
                      callback=self._callback,
                      session=self.session,
                      connect_callback=self.connect_callback,
                      disconnect_callback=self.disconnect_callback,
                  )
              )
              await self._sio.connect(
                  self.wss_url,
                  transports=["websocket"],
                  headers={"JWT": director_bearer_token},
              )

          async def sio_disconnect(self):
              """Disconnects the WebSockets connection, if it has been created."""
              if isinstance(self._sio, socketio.AsyncClient):
                  await self._sio.disconnect()

          async def _callback(self, message):
              if "status" in message:
                  _LOGGER.debug(f'Subscription {message["status"]}')
                  return True
              if isinstance(message, list):
                  for m in message:
                      await self._process_message(m)
              else:
                  await self._process_message(message)

          async def _process_message(self, message):
              """Process an incoming event message."""
              _LOGGER.debug(message)
              try:
                  c = self._item_callbacks[message["iddevice"]]
              except KeyError:
                  _LOGGER.debug("No Callback for device id {}".format(message["iddevice"]))
                  return True

              if isinstance(message, list):
                  for m in message:
                      await c(message["iddevice"], m)
              else:
                  await c(message["iddevice"], message)

          async def _execute_callback(self, callback, *args, **kwargs):
              """Callback with some data capturing any excpetions."""
              try:
                  self.sio.emit("ping")
                  await callback(*args, **kwargs)
              except Exception as exc:
                  _LOGGER.warning("Captured exception during callback: {}".format(str(exc)))

  ### Instance variables

  `var item_callbacks`

    Returns a dictionary of registered item ids (key) and their callbacks (value).

    item_callbacks cannot be modified directly. Use add_item_callback() and remove_item_callback() instead.

    Expand source code

        @property
        def item_callbacks(self):
            """Returns a dictionary of registered item ids (key) and their callbacks (value).

            item_callbacks cannot be modified directly. Use add_item_callback() and remove_item_callback() instead."""
            return self._item_callbacks

  ### Methods

  `def add_item_callback(self, item_id, callback)`

    Register a callback to receive updates about an item. If a callback is already registered for the item, it will be overwritten with the provided callback.

    ## Parameters

    `item_id` - The Control4 item ID.

    `callback` - The callback to be called when an update is received for the provided item id.

    Expand source code

        def add_item_callback(self, item_id, callback):
            """Register a callback to receive updates about an item.
            If a callback is already registered for the item, it will be overwritten with the provided callback.

            Parameters:
                `item_id` - The Control4 item ID.

                `callback` - The callback to be called when an update is received for the provided item id.
            """

            _LOGGER.debug("Subscribing to updates for item id: %s", item_id)

            self._item_callbacks[item_id] = callback

  `def remove_item_callback(self, item_id)`

    Unregister callback for an item.

    ## Parameters

    `item_id` - The Control4 item ID.

    Expand source code

        def remove_item_callback(self, item_id):
            """Unregister callback for an item.

            Parameters:
                `item_id` - The Control4 item ID.
            """
            self._item_callbacks.pop(item_id)

  `async def sio_connect(self, director_bearer_token)`

    Start WebSockets connection and listen, using the provided director_bearer_token to authenticate with the Control4 Director. If a connection already exists, it will be disconnected and a new connection will be created.

    This function should be called using a new token every 86400 seconds (the expiry time of the director tokens), otherwise the Control4 Director will stop sending WebSocket messages.

    ## Parameters

    `director_bearer_token` - The bearer token used to authenticate with the Director. See `C4Account.getDirectorBearerToken()` for how to get this.

    Expand source code

        async def sio_connect(self, director_bearer_token):
            """Start WebSockets connection and listen, using the provided director_bearer_token to authenticate with the Control4 Director.
            If a connection already exists, it will be disconnected and a new connection will be created.

            This function should be called using a new token every 86400 seconds (the expiry time of the director tokens), otherwise the Control4 Director will stop sending WebSocket messages.

            Parameters:
                `director_bearer_token` - The bearer token used to authenticate with the Director. See `pyControl4.account.C4Account.getDirectorBearerToken` for how to get this.
            """
            # Disconnect previous sio object
            await self.sio_disconnect()

            self._sio = socketio.AsyncClient(ssl_verify=False)
            self._sio.register_namespace(
                _C4DirectorNamespace(
                    token=director_bearer_token,
                    url=self.base_url,
                    callback=self._callback,
                    session=self.session,
                    connect_callback=self.connect_callback,
                    disconnect_callback=self.disconnect_callback,
                )
            )
            await self._sio.connect(
                self.wss_url,
                transports=["websocket"],
                headers={"JWT": director_bearer_token},
            )

  `async def sio_disconnect(self)`

    Disconnects the WebSockets connection, if it has been created.

    Expand source code

        async def sio_disconnect(self):
            """Disconnects the WebSockets connection, if it has been created."""
            if isinstance(self._sio, socketio.AsyncClient):
                await self._sio.disconnect()