Exemple #1
0
    async def _send_command(self, identifier: str,
                            content: Dict[str, object]) -> Dict[str, object]:
        """Send a command to the device and return response."""
        await self._connect()
        if self._protocol is None:
            raise RuntimeError("not connected to companion")

        try:
            resp = await self._protocol.exchange_opack(
                FrameType.E_OPACK,
                {
                    "_i": identifier,
                    "_x": 12356,  # Dummy XID, not sure what to use
                    "_t": "2",  # Request
                    "_c": content,
                },
            )
        except Exception as ex:
            raise exceptions.ProtocolError(
                f"Command {identifier} failed") from ex
        else:
            # Check if an error was present and throw exception if that's the case
            if "_em" in resp:
                raise exceptions.ProtocolError(
                    f"Command {identifier} failed: {resp['_em']}")

        return resp
Exemple #2
0
    async def finish_pairing(self, username: str,
                             pin_code: int) -> HapCredentials:
        """Finish pairing process.

        A username (generated by new_credentials) and the PIN code shown on
        screen must be provided.
        """
        # Step 1
        client_id = (binascii.hexlify(
            self.srp.credentials.client_id).decode("ascii").upper())
        self.srp.step1(client_id, pin_code)
        resp = await self._send_plist(method="pin", user=client_id)
        resp = plistlib.loads(resp.body if isinstance(resp.body, bytes) else
                              resp.body.encode("utf-8"))
        if not isinstance(resp, dict):
            raise exceptions.ProtocolError(f"exoected dict, got {type(resp)}")

        # Step 2
        pub_key, key_proof = self.srp.step2(resp["pk"], resp["salt"])
        await self._send_plist(pk=binascii.unhexlify(pub_key),
                               proof=binascii.unhexlify(key_proof))

        # Step 3
        epk, tag = self.srp.step3()
        await self._send_plist(epk=epk, authTag=tag)
        return self.srp.credentials
Exemple #3
0
    async def _send_command(
        self,
        identifier: str,
        content: Dict[str, object],
        message_type: MessageType = MessageType.Request,
    ) -> Mapping[str, Any]:
        """Send a command to the device and return response."""
        await self.connect()
        if self._protocol is None:
            raise RuntimeError("not connected to companion")

        try:
            resp = await self._protocol.exchange_opack(
                FrameType.E_OPACK,
                {
                    "_i": identifier,
                    "_t": message_type.value,
                    "_c": content,
                },
            )
        except exceptions.ProtocolError:
            raise
        except Exception as ex:
            raise exceptions.ProtocolError(
                f"Command {identifier} failed") from ex
        else:
            return resp
Exemple #4
0
    async def app_list(self) -> List[App]:
        """Fetch a list of apps that can be launched."""
        app_list = await self.api.app_list()
        if "_c" not in app_list:
            raise exceptions.ProtocolError("missing content in response")

        content = cast(dict, app_list["_c"])
        return [App(name, bundle_id) for bundle_id, name in content.items()]
Exemple #5
0
    async def set_volume(self, level: float) -> None:
        """Change current volume level."""
        if self._output_device_uid is None:
            raise exceptions.ProtocolError("no output device")

        await self.protocol.send(
            messages.set_volume(self._output_device_uid, level / 100.0))

        if self._volume != level:
            await asyncio.wait_for(self._volume_event.wait(), timeout=5.0)
Exemple #6
0
def get_audio_properties(
        properties: Mapping[str, str]) -> Tuple[int, int, int]:
    """Parse Zeroconf properties and return sample rate, channels and sample size."""
    try:
        sample_rate = int(properties.get("sr", DEFAULT_SAMPLE_RATE))
        channels = int(properties.get("ch", DEAFULT_CHANNELS))
        sample_size = int(int(properties.get("ss", DEFAULT_SAMPLE_SIZE)) / 8)
    except Exception as ex:
        raise exceptions.ProtocolError("invalid audio property") from ex
    else:
        return sample_rate, channels, sample_size
Exemple #7
0
    async def _exchange_generic_opack(
        self,
        frame_type: FrameType,
        data: Dict[str, Any],
        identifier: FrameIdType,
        timeout: float,
    ) -> Dict[str, object]:
        _LOGGER.debug("Exchange OPACK: %s", data)

        self.send_opack(frame_type, data)
        self._queues[identifier] = SharedData()
        unpacked_object = await self._queues[identifier].wait(timeout)

        if not isinstance(unpacked_object, dict):
            raise exceptions.ProtocolError(
                f"Received unexpected type: {type(unpacked_object)}")

        if "_em" in unpacked_object:
            raise exceptions.ProtocolError(
                f"Command failed: {unpacked_object['_em']}")

        return unpacked_object
Exemple #8
0
def _get_pairing_data(message: Dict[str, object]):
    pairing_data = message.get(PAIRING_DATA_KEY)
    if not pairing_data:
        raise exceptions.AuthenticationError("no pairing data in message")

    if not isinstance(pairing_data, bytes):
        raise exceptions.ProtocolError(
            f"Pairing data has unexpected type: {type(pairing_data)}")

    tlv = read_tlv(pairing_data)
    if TlvValue.Error in tlv:
        raise exceptions.AuthenticationError(stringify(tlv))

    return tlv
Exemple #9
0
    async def _session_start(self) -> None:
        local_sid = randint(0, 2**32 - 1)
        resp = await self._send_command("_sessionStart", {
            "_srvT": "com.apple.tvremoteservices",
            "_sid": local_sid
        })

        content = resp.get("_c")
        if content is None:
            raise exceptions.ProtocolError("missing content")

        remote_sid = cast(Mapping[str, Any], resp["_c"])["_sid"]
        self.sid = (remote_sid << 32) | local_sid
        _LOGGER.debug("Started session with SID 0x%X", self.sid)
Exemple #10
0
    async def start(self):
        """Connect to device and listen to incoming messages."""
        if self._is_started:
            raise exceptions.ProtocolError("Already started")

        self._is_started = True
        await self.connection.connect()

        if self.service.credentials:
            self.srp.pairing_id = HapCredentials.parse(
                self.service.credentials).client_id

        _LOGGER.debug("Companion credentials: %s", self.service.credentials)

        await self._setup_encryption()
Exemple #11
0
    async def send_and_receive(
        self,
        method: str,
        uri: str,
        protocol: str = "HTTP/1.1",
        user_agent: str = USER_AGENT,
        content_type: Optional[str] = None,
        headers: Optional[Mapping[str, object]] = None,
        body: Optional[Union[str, bytes]] = None,
        allow_error: bool = False,
    ) -> HttpResponse:
        """Send a HTTP message and return response."""
        output = _format_message(method, uri, protocol, user_agent,
                                 content_type, headers, body)

        _LOGGER.debug("Sending %s message: %s", protocol, output)
        if not self.transport:
            raise RuntimeError("not connected to remote")

        self.transport.write(output)

        event = asyncio.Event()
        self._requests.appendleft(event)
        try:
            await asyncio.wait_for(event.wait(), timeout=4)
            response = cast(HttpResponse, self._responses.get())
        except asyncio.TimeoutError as ex:
            raise TimeoutError(
                f"no response to {method} {uri} ({protocol})") from ex
        finally:
            # If request failed and event is still in request queue, remove it
            if self._requests and self._requests[-1] == event:
                self._requests.pop()

        _LOGGER.debug("Got %s response: %s:", response.protocol, response)

        if response.code in [401, 403]:
            raise exceptions.AuthenticationError("not authenticated")

        # Positive response
        if 200 <= response.code < 300 or allow_error:
            return response

        raise exceptions.ProtocolError(
            f"{protocol} method {method} failed with code "
            f"{response.code}: {response.message}")
Exemple #12
0
    async def exchange_opack(
            self,
            frame_type: FrameType,
            data: object,
            timeout: int = DEFAULT_TIMEOUT) -> Dict[str, object]:
        """Send data as OPACK and decode result as OPACK."""
        _LOGGER.debug("Send OPACK: %s", data)
        _, payload = await self.connection.exchange(frame_type,
                                                    opack.pack(data), timeout)
        unpacked_object, _ = opack.unpack(payload)
        _LOGGER.debug("Receive OPACK: %s", unpacked_object)

        if not isinstance(unpacked_object, dict):
            raise exceptions.ProtocolError(
                f"Received unexpected type: {type(unpacked_object)}")

        return unpacked_object
Exemple #13
0
    async def _send_event(self, identifier: str,
                          content: Mapping[str, Any]) -> None:
        """Subscribe to updates to an event."""
        await self.connect()
        if self._protocol is None:
            raise RuntimeError("not connected to companion")

        try:
            self._protocol.send_opack(
                FrameType.E_OPACK,
                {
                    "_i": identifier,
                    "_t": MessageType.Event.value,
                    "_c": content,
                },
            )
        except exceptions.ProtocolError:
            raise
        except Exception as ex:
            raise exceptions.ProtocolError("Send event failed") from ex
Exemple #14
0
    async def stream_file(self, file: Union[str, io.BufferedReader],
                          **kwargs) -> None:
        """Stream local or remote file to device.

        Supports either local file paths or a HTTP(s) address.

        INCUBATING METHOD - MIGHT CHANGE IN THE FUTURE!
        """
        self.playback_manager.acquire()
        audio_file: Optional[AudioSource] = None
        takeover_release = self.core.takeover(Audio, Metadata, PushUpdater,
                                              RemoteControl)
        try:
            client, _, context = await self.playback_manager.setup()
            client.credentials = parse_credentials(
                self.core.service.credentials)
            client.password = self.core.service.password

            client.listener = self.listener
            await client.initialize(self.core.service.properties)

            # Try to load metadata and pass it along if it succeeds
            metadata: AudioMetadata = EMPTY_METADATA
            try:
                # Source must support seeking to read metadata (or point to file)
                if (isinstance(file, io.BufferedReader)
                        and file.seekable()) or (isinstance(file, str)
                                                 and path.exists(file)):
                    metadata = await get_metadata(file)
                else:
                    _LOGGER.debug(
                        "Seeking not supported by source, not loading metadata"
                    )
            except Exception as ex:
                _LOGGER.exception("Failed to extract metadata from %s: %s",
                                  file, ex)

            # After initialize has been called, all the audio properties will be
            # initialized and can be used in the miniaudio wrapper
            audio_file = await open_source(
                file,
                context.sample_rate,
                context.channels,
                context.bytes_per_channel,
            )

            # If the user didn't change volume level prior to streaming, try to extract
            # volume level from device (if supported). Otherwise set the default level
            # in pyatv.
            if not self.audio.has_changed_volume and "initialVolume" in client.info:
                initial_volume = client.info["initialVolume"]
                if not isinstance(initial_volume, float):
                    raise exceptions.ProtocolError(
                        f"initial volume {initial_volume} has "
                        "incorrect type {type(initial_volume)}", )
                context.volume = initial_volume
            else:
                await self.audio.set_volume(self.audio.volume)

            await client.send_audio(audio_file, metadata)
        finally:
            takeover_release()
            if audio_file:
                await audio_file.close()
            await self.playback_manager.teardown()
Exemple #15
0
    async def send_audio(self,
                         wave_file,
                         metadata: AudioMetadata = EMPTY_METADATA):
        """Send an audio stream to the device."""
        if self.control_client is None or self.timing_client is None:
            raise Exception("not initialized")  # TODO: better exception

        transport = None
        try:
            # Set up the streaming session
            await self._setup_session()

            # Create a socket used for writing audio packets (ugly)
            transport, _ = await self.loop.create_datagram_endpoint(
                AudioProtocol,
                remote_addr=(self.rtsp.connection.remote_ip,
                             self.context.server_port),
            )

            # Start sending sync packets
            self.control_client.start(self.rtsp.connection.remote_ip)

            # Send progress if supported by receiver
            if MetadataType.Progress in self._metadata_types:
                start = self.context.start_ts
                now = self.context.rtptime
                end = (self.context.start_ts +
                       wave_file.getduration() * self.context.sample_rate)
                await self.rtsp.set_parameter("progress",
                                              f"{start}/{now}/{end}")

            # Apply text metadata if it is supported
            self._metadata = metadata
            if MetadataType.Text in self._metadata_types:
                _LOGGER.debug("Playing with metadata: %s", self.metadata)
                await self.rtsp.set_metadata(self.context.rtpseq,
                                             self.context.rtptime,
                                             self.metadata)

            # Set a decent volume (range is [-30.0, 0] and -144 is muted)
            await self.rtsp.set_parameter("volume", "-20")

            # Start keep-alive task to ensure connection is not closed by remote device
            self._keep_alive_task = asyncio.ensure_future(
                self._send_keep_alive())

            listener = self.listener
            if listener:
                listener.playing(self.metadata)

            # Start playback
            await self.rtsp.record(self.context.rtpseq, self.context.rtptime)

            await self._stream_data(wave_file, transport)
        except (  # pylint: disable=try-except-raise
                exceptions.ProtocolError,
                exceptions.AuthenticationError,
        ):
            raise  # Re-raise internal exceptions to maintain a proper stack trace
        except Exception as ex:
            raise exceptions.ProtocolError(
                "an error occurred during streaming") from ex
        finally:
            self._packet_backlog.clear(
            )  # Don't keep old packets around (big!)
            if transport:
                transport.close()
            if self._keep_alive_task:
                self._keep_alive_task.cancel()
                self._keep_alive_task = None
            self.control_client.stop()

            listener = self.listener
            if listener:
                listener.stopped()
Exemple #16
0
 def volume(self) -> float:
     """Return current volume level."""
     volume = self.relay("volume")
     if 0.0 <= volume <= 100.0:
         return volume
     raise exceptions.ProtocolError(f"volume {volume} is out of range")
Exemple #17
0
 async def set_volume(self, level: float) -> None:
     """Change current volume level."""
     if 0.0 <= level <= 100.0:
         await self.relay("set_volume")(level)
     else:
         raise exceptions.ProtocolError(f"volume {level} is out of range")