Exemplo n.º 1
0
    def remove_pairing(self, alias):
        """
        Remove a pairing between the controller and the accessory. The pairing data is delete on both ends, on the
        accessory and the controller.

        Important: no automatic saving of the pairing data is performed. If you don't do this, the accessory seems still
            to be paired on the next start of the application.

        :param alias: the controller's alias for the accessory
        :raises AuthenticationError: if the controller isn't authenticated to the accessory.
        :raises AccessoryNotFoundError: if the device can not be found via zeroconf
        :raises UnknownError: on unknown errors
        """
        # package visibility like in java would be nice here
        pairing_data = self.pairings[alias]._get_pairing_data()
        request_tlv = TLV.encode_list([
            (TLV.kTLVType_State, TLV.M1),
            (TLV.kTLVType_Method, TLV.RemovePairing),
            (TLV.kTLVType_Identifier, pairing_data['iOSPairingId'].encode())
        ]).decode()  # decode is required because post needs a string representation
        session = Session(pairing_data)
        response = session.post('/pairings', request_tlv)
        session.close()
        data = response.read()
        data = TLV.decode_bytes(data)
        # handle the result, spec says, if it has only one entry with state == M2 we unpaired, else its an error.
        if len(data) == 1 and data[0][0] == TLV.kTLVType_State and data[0][1] == TLV.M2:
            del self.pairings[alias]
        else:
            if data[TLV.kTLVType_Error] == TLV.kTLVError_Authentication:
                raise AuthenticationError('Remove pairing failed: missing authentication')
            else:
                raise UnknownError('Remove pairing failed: unknown error')
Exemplo n.º 2
0
    def identify_ble(accessory_mac, adapter='hci0'):
        """
        This call can be used to trigger the identification of an accessory, that was not yet paired. A successful call
        should cause the accessory to perform some specific action by which it can be distinguished from others (blink a
        LED for example).

        It uses the /identify url as described on page 88 of the spec.

        :param accessory_mac: the accessory's mac address (e.g. retrieved via discover)
        :raises AccessoryNotFoundError: if the accessory could not be looked up via Bonjour
        :param adapter: the bluetooth adapter to be used (defaults to hci0)
        :raises AlreadyPairedError: if the accessory is already paired
        """
        if not BLE_TRANSPORT_SUPPORTED:
            raise TransportNotSupportedError('BLE')
        from .ble_impl.device import DeviceManager
        manager = DeviceManager(adapter)
        device = manager.make_device(accessory_mac)
        device.connect()

        disco_info = device.get_homekit_discovery_data()
        if disco_info.get('flags', 'unknown') == 'paired':
            raise AlreadyPairedError(
                'identify of {mac_address} failed not allowed as device already paired'.format(
                    mac_address=accessory_mac),
            )

        identify, identify_iid = find_characteristic_by_uuid(
            device,
            ServicesTypes.ACCESSORY_INFORMATION_SERVICE,
            CharacteristicsTypes.IDENTIFY,
        )

        if not identify:
            raise AccessoryNotFoundError(
                'Device with address {mac_address} exists but did not find IDENTIFY characteristic'.format(
                    mac_address=accessory_mac)
            )

        value = TLV.encode_list([
            (1, b'\x01')
        ])
        body = len(value).to_bytes(length=2, byteorder='little') + value

        tid = random.randrange(0, 255)

        request = bytearray([0x00, HapBleOpCodes.CHAR_WRITE, tid])
        request.extend(identify_iid.to_bytes(length=2, byteorder='little'))
        request.extend(body)

        identify.write_value(request)
        response = bytearray(identify.read_value())

        if not response or not response[2] == 0x00:
            raise UnknownError('Unpaired identify failed')

        return True
Exemplo n.º 3
0
    def list_pairings(self):
        """
        This method returns all pairings of a HomeKit accessory. This always includes the local controller and can only
        be done by an admin controller.

        The keys in the resulting dicts are:
         * pairingId: the pairing id of the controller
         * publicKey: the ED25519 long-term public key of the controller
         * permissions: bit value for the permissions
         * controllerType: either admin or regular

        :return: a list of dicts
        :raises: UnknownError: if it receives unexpected data
        :raises: UnpairedError: if the polled accessory is not paired
        """
        if not self.session:
            self.session = IpSession(self.pairing_data)
        request_tlv = TLV.encode_list([(TLV.kTLVType_State, TLV.M1),
                                       (TLV.kTLVType_Method, TLV.ListPairings)
                                       ])
        try:
            response = self.session.sec_http.post('/pairings',
                                                  request_tlv.decode())
            data = response.read()
        except (AccessoryDisconnectedError, EncryptionError):
            self.session.close()
            self.session = None
            raise
        data = TLV.decode_bytes(data)

        if not (data[0][0] == TLV.kTLVType_State and data[0][1] == TLV.M2):
            raise UnknownError('unexpected data received: ' + str(data))
        elif data[1][0] == TLV.kTLVType_Error and data[1][
                1] == TLV.kTLVError_Authentication:
            raise UnpairedError('Must be paired')
        else:
            tmp = []
            r = {}
            for d in data[1:]:
                if d[0] == TLV.kTLVType_Identifier:
                    r = {}
                    tmp.append(r)
                    r['pairingId'] = d[1].decode()
                if d[0] == TLV.kTLVType_PublicKey:
                    r['publicKey'] = d[1].hex()
                if d[0] == TLV.kTLVType_Permissions:
                    controller_type = 'regular'
                    if d[1] == b'\x01':
                        controller_type = 'admin'
                    r['permissions'] = int.from_bytes(d[1], byteorder='little')
                    r['controllerType'] = controller_type
            return tmp
Exemplo n.º 4
0
    def remove_pairing(self, alias, pairingId=None):
        """
        Remove a pairing between the controller and the accessory. The pairing data is delete on both ends, on the
        accessory and the controller.

        Important: no automatic saving of the pairing data is performed. If you don't do this, the accessory seems still
            to be paired on the next start of the application.

        :param alias: the controller's alias for the accessory
        :param pairingId: the pairing id to be removed
        :raises AuthenticationError: if the controller isn't authenticated to the accessory.
        :raises AccessoryNotFoundError: if the device can not be found via zeroconf
        :raises UnknownError: on unknown errors
        """
        # package visibility like in java would be nice here
        pairing_data = self.pairings[alias]._get_pairing_data()
        connection_type = pairing_data['Connection']
        if not pairingId:
            pairingIdToDelete = pairing_data['iOSPairingId']
        else:
            pairingIdToDelete = pairingId

        # Prepare the common (for IP and BLE) request data
        request_tlv = TLV.encode_list([
            (TLV.kTLVType_State, TLV.M1),
            (TLV.kTLVType_Method, TLV.RemovePairing),
            (TLV.kTLVType_Identifier, pairingIdToDelete.encode())
        ])

        if connection_type == 'IP':
            if not IP_TRANSPORT_SUPPORTED:
                raise TransportNotSupportedError('IP')
            session = IpSession(pairing_data)
            # decode is required because post needs a string representation
            response = session.post('/pairings', request_tlv)
            session.close()
            data = response.read()
            data = TLV.decode_bytes(data)
        elif connection_type == 'BLE':
            if not BLE_TRANSPORT_SUPPORTED:
                raise TransportNotSupportedError('BLE')
            inner = TLV.encode_list([(TLV.kTLVHAPParamParamReturnResponse,
                                      bytearray(b'\x01')),
                                     (TLV.kTLVHAPParamValue, request_tlv)])

            body = len(inner).to_bytes(length=2, byteorder='little') + inner

            from .ble_impl.device import DeviceManager
            manager = DeviceManager(self.ble_adapter)
            device = manager.make_device(pairing_data['AccessoryMAC'])
            device.connect()

            logging.debug('resolved %d services', len(device.services))
            pair_remove_char, pair_remove_char_id = find_characteristic_by_uuid(
                device, ServicesTypes.PAIRING_SERVICE,
                CharacteristicsTypes.PAIRING_PAIRINGS)
            logging.debug('setup char: %s %s', pair_remove_char,
                          pair_remove_char.service.device)

            session = BleSession(pairing_data, self.ble_adapter)
            response = session.request(pair_remove_char, pair_remove_char_id,
                                       HapBleOpCodes.CHAR_WRITE, body)
            data = TLV.decode_bytes(response[1])
        else:
            raise Exception('not implemented (neither IP nor BLE)')

        # act upon the response (the same is returned for IP and BLE accessories)
        # handle the result, spec says, if it has only one entry with state == M2 we unpaired, else its an error.
        logging.debug('response data: %s', data)
        if len(data) == 1 and data[0][0] == TLV.kTLVType_State and data[0][
                1] == TLV.M2:
            if not pairingId:
                del self.pairings[alias]
        else:
            if data[1][0] == TLV.kTLVType_Error and data[1][
                    1] == TLV.kTLVError_Authentication:
                raise AuthenticationError(
                    'Remove pairing failed: missing authentication')
            else:
                raise UnknownError('Remove pairing failed: unknown error')
Exemplo n.º 5
0
    def list_pairings(self):
        """
        This method returns all pairings of a HomeKit accessory. This always includes the local controller and can only
        be done by an admin controller.

        The keys in the resulting dicts are:
         * pairingId: the pairing id of the controller
         * publicKey: the ED25519 long-term public key of the controller
         * permissions: bit value for the permissions
         * controllerType: either admin or regular

        :return: a list of dicts
        :raises: UnknownError: if it receives unexpected data
        :raises: UnpairedError: if the polled accessory is not paired
        """
        if not self.session:
            self.session = IpSession(self.pairing_data)
        request_tlv = tlv8.encode([
            tlv8.Entry(TlvTypes.State, States.M1),
            tlv8.Entry(TlvTypes.Method, Methods.ListPairings)
        ])
        try:
            response = self.session.sec_http.post('/pairings', request_tlv)
            data = response.read()
        except (AccessoryDisconnectedError, EncryptionError):
            self.session.close()
            self.session = None
            raise
        data = tlv8.decode(
            data, {
                TlvTypes.State: tlv8.DataType.INTEGER,
                TlvTypes.Error: tlv8.DataType.INTEGER,
                TlvTypes.Identifier: tlv8.DataType.BYTES,
                TlvTypes.PublicKey: tlv8.DataType.BYTES,
                TlvTypes.Permissions: tlv8.DataType.BYTES
            })

        error = data.first_by_id(TlvTypes.Error)
        if not (data.first_by_id(TlvTypes.State).data == States.M2):
            raise UnknownError('unexpected data received: ' +
                               tlv8.format_string(data))
        elif error and error.data == Errors.Authentication:
            raise UnpairedError('Must be paired')
        else:
            tmp = []
            r = {}
            for d in data[1:]:
                if d.type_id == TlvTypes.Identifier:
                    r = {}
                    tmp.append(r)
                    r['pairingId'] = d.data.decode()
                if d.type_id == TlvTypes.PublicKey:
                    r['publicKey'] = d.data.hex()
                if d.type_id == TlvTypes.Permissions:
                    controller_type = 'regular'
                    if d.data == b'\x01':
                        controller_type = 'admin'
                    r['permissions'] = int.from_bytes(d.data,
                                                      byteorder='little')
                    r['controllerType'] = controller_type
            tmp.sort(key=lambda x: x['pairingId'])
            return tmp