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')
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
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
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')
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