Example #1
0
    def connect(self):
        """
        Establishes a connection to a BLE device. This function does not return
        until the services exposed by the device are fully resolved.

        Eventually the connection will timeout and an `AccessoryNotFound` error
        will be raised.
        """
        super().connect()

        try:
            if not self.services:
                logger.debug('waiting for services to be resolved')
                for i in range(20):
                    if self.is_services_resolved():
                        break
                    time.sleep(1)
                else:
                    raise AccessoryNotFoundError(
                        'Unable to resolve device services + characteristics')

                # This is called automatically when the mainloop is running, but we
                # want to avoid running it and blocking for an indeterminate amount of time.
                logger.debug('enumerating resolved services')
                self.services_resolved()
        except dbus.exceptions.DBusException:
            raise AccessoryNotFoundError(
                'Unable to resolve device services + characteristics')
Example #2
0
    def identify(accessory_id):
        """
        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_id: the accessory's pairing id (e.g. retrieved via discover)
        :raises AccessoryNotFoundError: if the accessory could not be looked up via Bonjour
        :raises AlreadyPairedError: if the accessory is already paired
        """
        if not IP_TRANSPORT_SUPPORTED:
            raise TransportNotSupportedError('IP')
        connection_data = find_device_ip_and_port(accessory_id)
        if connection_data is None:
            raise AccessoryNotFoundError(
                'Cannot find accessory with id "{i}".'.format(i=accessory_id))

        conn = HomeKitHTTPConnection(connection_data['ip'],
                                     port=connection_data['port'])
        conn.request('POST', '/identify')
        resp = conn.getresponse()

        # spec says status code 400 on any error (page 88). It also says status should be -70401 (which is "Request
        # denied due to insufficient privileges." table 5-12 page 80) but this sounds odd.
        if resp.code == 400:
            data = json.loads(resp.read().decode())
            code = data['status']
            conn.close()
            raise AlreadyPairedError(
                'identify failed because: {reason} ({code}).'.format(
                    reason=HapStatusCodes[code], code=code))
        conn.close()
Example #3
0
    def __init__(self, pairing_data):
        """

        :param pairing_data:
        :raises AccessoryNotFoundError: if the device can not be found via zeroconf
        """
        connected = False
        if 'AccessoryIP' in pairing_data and 'AccessoryPort' in pairing_data:
            # if it is known, try it
            accessory_ip = pairing_data['AccessoryIP']
            accessory_port = pairing_data['AccessoryPort']
            conn = HomeKitHTTPConnection(accessory_ip, port=accessory_port)
            try:
                conn.connect()
                c2a_key, a2c_key = get_session_keys(conn, pairing_data)
                connected = True
            except Exception as e:
                connected = False
        if not connected:
            # no connection yet, so ip / port might have changed and we need to fall back to slow zeroconf lookup
            device_id = pairing_data['AccessoryPairingID']
            connection_data = find_device_ip_and_port(device_id)
            if connection_data is None:
                raise AccessoryNotFoundError(
                    'Device {id} not found'.format(id=pairing_data['AccessoryPairingID']))
            conn = HomeKitHTTPConnection(connection_data['ip'], port=connection_data['port'])
            pairing_data['AccessoryIP'] = connection_data['ip']
            pairing_data['AccessoryPort'] = connection_data['port']
            c2a_key, a2c_key = get_session_keys(conn, pairing_data)

        self.sock = conn.sock
        self.c2a_key = c2a_key
        self.a2c_key = a2c_key
        self.pairing_data = pairing_data
        self.sec_http = SecureHttp(self)
Example #4
0
    def __init__(self, pairing_data):
        """

        :param pairing_data:
        :raises AccessoryNotFoundError: if the device can not be found via zeroconf
        """
        logging.debug('init session')
        connected = False
        self.pairing_data = pairing_data

        if 'AccessoryIP' in pairing_data and 'AccessoryPort' in pairing_data:
            # if it is known, try it
            accessory_ip = pairing_data['AccessoryIP']
            accessory_port = pairing_data['AccessoryPort']
            connected = self._connect(accessory_ip, accessory_port)

        if not connected:
            # no connection yet, so ip / port might have changed and we need to fall back to slow zeroconf lookup
            device_id = pairing_data['AccessoryPairingID']
            connection_data = find_device_ip_and_port(device_id)

            # update pairing data with the IP/port we elaborated above, perhaps next time they are valid
            pairing_data['AccessoryIP'] = connection_data['ip']
            pairing_data['AccessoryPort'] = connection_data['port']

            if connection_data is None:
                raise AccessoryNotFoundError(
                    'Device {id} not found'.format(id=pairing_data['AccessoryPairingID']))

            if not self._connect(connection_data['ip'], connection_data['port']):
                return

        logging.debug('session established')

        self.sec_http = SecureHttp(self)
Example #5
0
    def start_pairing(self, alias, accessory_id):
        """
        This starts a pairing attempt with the IP accessory identified by its id.
        It returns a callable (finish_pairing) which you must call with the pairing pin.

        Accessories can be found via the discover method. The id field is the accessory's id for the second parameter.

        The required pin is either printed on the accessory or displayed. Must be a string of the form 'XXX-YY-ZZZ'. If
        this format is not used, a MalformedPinError is raised.

        Important: no automatic saving of the pairing data is performed. If you don't do this, the information is lost
            and you have to reset the accessory!

        :param alias: the alias for the accessory in the controllers data
        :param accessory_id: the accessory's id
        :param pin: function to return the accessory's pin
        :raises AccessoryNotFoundError: if no accessory with the given id can be found
        :raises AlreadyPairedError: if the alias was already used
        :raises UnavailableError: if the device is already paired
        :raises MaxTriesError: if the device received more than 100 unsuccessful attempts
        :raises BusyError: if a parallel pairing is ongoing
        :raises AuthenticationError: if the verification of the device's SRP proof fails
        :raises MaxPeersError: if the device cannot accept an additional pairing
        :raises UnavailableError: on wrong pin
        """
        if not IP_TRANSPORT_SUPPORTED:
            raise TransportNotSupportedError('IP')
        if alias in self.pairings:
            raise AlreadyPairedError(
                'Alias "{a}" is already paired.'.format(a=alias))

        connection_data = find_device_ip_and_port(accessory_id)
        if connection_data is None:
            raise AccessoryNotFoundError(
                'Cannot find accessory with id "{i}".'.format(i=accessory_id))
        conn = HomeKitHTTPConnection(connection_data['ip'],
                                     port=connection_data['port'])

        try:
            write_fun = create_ip_pair_setup_write(conn)
            salt, pub_key = perform_pair_setup_part1(write_fun)
        except Exception:
            conn.close()
            raise

        def finish_pairing(pin):
            Controller.check_pin_format(pin)
            try:
                pairing = perform_pair_setup_part2(pin, str(uuid.uuid4()),
                                                   write_fun, salt, pub_key)
            finally:
                conn.close()
            pairing['AccessoryIP'] = connection_data['ip']
            pairing['AccessoryPort'] = connection_data['port']
            pairing['Connection'] = 'IP'
            self.pairings[alias] = IpPairing(pairing)

        return finish_pairing
Example #6
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
Example #7
0
 def request_pin(self, alias, accessory_id):
     connection_data = find_device_ip_and_port(accessory_id)
     if connection_data is None:
         raise AccessoryNotFoundError('Cannot find accessory with id "{i}".'.format(i=accessory_id))
     conn = HomeKitHTTPConnection(connection_data['ip'], port=connection_data['port'])
     try:
         pairing = request_pin_setup(conn, str(uuid.uuid4()))
     finally:
         conn.close()
     pairing['AccessoryIP'] = connection_data['ip']
     pairing['AccessoryPort'] = connection_data['port']
     self.pairings[alias] = Pairing(pairing)
Example #8
0
    def get_characteristics(self, characteristics):
        """Fake implementation of get_characteristics."""
        if not self.available:
            raise AccessoryNotFoundError('Accessory not found')

        results = {}
        for aid, cid in characteristics:
            for accessory in self.accessories:
                if aid != accessory.aid:
                    continue
                for service in accessory.services:
                    for char in service.characteristics:
                        if char.iid != cid:
                            continue
                        results[(aid, cid)] = {'value': char.get_value()}
        return results
Example #9
0
    def perform_pairing(self, alias, accessory_id, pin):
        """
        This performs a pairing attempt with the accessory identified by its id.

        Accessories can be found via the discover method. The id field is the accessory's for the second parameter.

        The required pin is either printed on the accessory or displayed. Must be a string of the form 'XXX-YY-ZZZ'.

        Important: no automatic saving of the pairing data is performed. If you don't do this, the information is lost
            and you have to reset the accessory!

        :param alias: the alias for the accessory in the controllers data
        :param accessory_id: the accessory's id
        :param pin: the accessory's pin
        :raises AccessoryNotFoundError: if no accessory with the given id can be found
        :raises AlreadyPairedError: if the alias was already used
        :raises UnavailableError: if the device is already paired
        :raises MaxTriesError: if the device received more than 100 unsuccessful attempts
        :raises BusyError: if a parallel pairing is ongoing
        :raises AuthenticationError: if the verification of the device's SRP proof fails
        :raises MaxPeersError: if the device cannot accept an additional pairing
        :raises UnavailableError: on wrong pin
        """
        if alias in self.pairings:
            raise AlreadyPairedError(
                'Alias "{a}" is already paired.'.format(a=alias))
        connection_data = find_device_ip_and_port(accessory_id)
        if connection_data is None:
            raise AccessoryNotFoundError(
                'Cannot find accessory with id "{i}".'.format(i=accessory_id))
        conn = HomeKitHTTPConnection(connection_data['ip'],
                                     port=connection_data['port'])
        try:
            pairing = perform_pair_setup(conn, pin, str(uuid.uuid4()))
        finally:
            conn.close()
        pairing['AccessoryIP'] = connection_data['ip']
        pairing['AccessoryPort'] = connection_data['port']
        self.pairings[alias] = Pairing(pairing)