def log_transferred_value(text: str, aid: int, characteristic: AbstractCharacteristic, value, filtered_value): """ Logs the transfer of a value between controller and acccessory or vice versa. For characteristics of type TLV8, a decoder is used if available else a deep decode is done. :param text: a `str` to express which direction of transfer takes place :param aid: the accessory id :param characteristic: the characteristic for which the transfer takes place :param value: the value that was transferred """ iid = int(characteristic.iid) debug_value = value filtered_debug_value = filtered_value characteristic_name = CharacteristicsTypes.get_short(characteristic.type) if characteristic.format == CharacteristicFormats.tlv8: bytes_value = base64.b64decode(value) filtered_bytes_value = base64.b64decode(filtered_value) decoder = decoder_loader.load(characteristic.type) if decoder: try: debug_value = tlv8.format_string(decoder(bytes_value)) filtered_debug_value = tlv8.format_string( decoder(filtered_bytes_value)) except Exception as e: logging.error('problem decoding', e) else: debug_value = tlv8.format_string(tlv8.deep_decode(bytes_value)) filtered_debug_value = tlv8.format_string( tlv8.deep_decode(filtered_bytes_value)) logging.info( '%s %s.%s (type %s / %s): \n\toriginal value: %s\n\tfiltered value: %s' % (text, aid, iid, characteristic.type, characteristic_name, debug_value, filtered_debug_value))
def write(request, expected): # TODO document me body = tlv8.encode(request) logger.debug('entering write function %s', tlv8.format_string(tlv8.decode(body))) request_tlv = tlv8.encode([ tlv8.Entry(AdditionalParameterTypes.ParamReturnResponse, bytearray(b'\x01')), tlv8.Entry(AdditionalParameterTypes.Value, body) ]) transaction_id = random.randrange(0, 255) # construct a hap characteristic write request following chapter 7.3.4.4 page 94 spec R2 data = bytearray([0x00, HapBleOpCodes.CHAR_WRITE, transaction_id]) data.extend(characteristic_id.to_bytes(length=2, byteorder='little')) data.extend(len(request_tlv).to_bytes(length=2, byteorder='little')) data.extend(request_tlv) logger.debug('sent %s', bytes(data).hex()) # write the request to the characteristic characteristic.write_value(value=data) # reading hap characteristic write response following chapter 7.3.4.5 page 95 spec R2 data = [] while len(data) == 0: time.sleep(1) logger.debug('reading characteristic') data = characteristic.read_value() resp_data = [b for b in data] expected_length = int.from_bytes(bytes(resp_data[3:5]), byteorder='little') logger.debug( 'control field: {c:x}, tid: {t:x}, status: {s:x}, length: {length}' .format(c=resp_data[0], t=resp_data[1], s=resp_data[2], length=expected_length)) while len(resp_data[3:]) < expected_length: time.sleep(1) logger.debug('reading characteristic') data = characteristic.read_value() resp_data.extend([b for b in data]) logger.debug('data %s of %s', len(resp_data[3:]), expected_length) logger.debug('received %s', bytes(resp_data).hex()) logger.debug('decode %s', bytes(resp_data[5:]).hex()) resp_tlv = tlv8.decode( bytes([int(a) for a in resp_data[5:]]), expected={AdditionalParameterTypes.Value: tlv8.DataType.BYTES}) result = tlv8.decode( resp_tlv.first_by_id(AdditionalParameterTypes.Value).data, expected) logger.debug('leaving write function %s', tlv8.format_string(result)) return result
def write_http(request, expected): body = tlv8.encode(request) logging.debug('write message: %s', tlv8.format_string(tlv8.deep_decode(body))) connection.putrequest('POST', '/pair-setup', skip_accept_encoding=True) connection.putheader('Content-Type', 'application/pairing+tlv8') connection.putheader('Content-Length', len(body)) connection.endheaders(body) resp = connection.getresponse() response_tlv = tlv8.decode(resp.read(), expected) logging.debug('response: %s', tlv8.format_string(response_tlv)) return response_tlv
def test_format_string(self): data = [ tlv8.Entry(1, 3.141), tlv8.Entry(2, [ tlv8.Entry(3, 'hello'), tlv8.Entry(4, 'world'), ]), tlv8.Entry(1, 2) ] print(tlv8.format_string(data))
def put_characteristics(self, characteristics, do_conversion=False): """ Update the values of writable characteristics. The characteristics have to be identified by accessory id (aid), instance id (iid). If do_conversion is False (the default), the value must be of proper format for the characteristic since no conversion is done. If do_conversion is True, the value is converted. :param characteristics: a list of 3-tupels of accessory id, instance id and the value :param do_conversion: select if conversion is done (False is default) :return: a dict from (aid, iid) onto {status, description} :raises FormatError: if the input value could not be converted to the target type and conversion was requested """ if not self.session: self.session = BleSession(self.pairing_data, self.adapter) results = {} for aid, cid, value in characteristics: # reply with an error if the characteristic does not exist if not self._find_characteristic_in_pairing_data(aid, cid): results[(aid, cid)] = { 'status': HapBleStatusCodes.INVALID_REQUEST, 'description': HapBleStatusCodes[HapBleStatusCodes.INVALID_REQUEST] } continue value = tlv8.encode([ tlv8.Entry(AdditionalParameterTypes.Value, self._convert_from_python(aid, cid, value)) ]) body = len(value).to_bytes(length=2, byteorder='little') + value try: fc, fc_info = self.session.find_characteristic_by_iid(cid) response = self.session.request(fc, cid, HapBleOpCodes.CHAR_WRITE, body) logger.debug('response %s', tlv8.format_string(response)) # TODO does the response contain useful information here? except RequestRejected as e: results[(aid, cid)] = { 'status': e.status, 'description': e.message, } except Exception as e: self.session.close() self.session = None raise e return results
def test_entrylist_format_string(self): el = tlv8.EntryList([ tlv8.Entry(1, 1), tlv8.Entry(2, tlv8.EntryList([ tlv8.Entry(4, 4), tlv8.Entry(5, 5) ])), tlv8.Entry(3, 3), ]) result = tlv8.format_string(el) expected = """[ <1, 1>, <2, [ <4, 4>, <5, 5>, ]>, <3, 3>, ]""" self.assertEqual(result, expected)
def request(self, feature_char, feature_char_id, op, body=None): transaction_id = random.randrange(0, 255) data = bytearray([0x00, op, transaction_id]) data.extend(feature_char_id.to_bytes(length=2, byteorder='little')) if body: logger.debug('body: %s', body) data.extend(body) logger.debug('data: %s', data) cnt_bytes = self.c2a_counter.to_bytes(8, byteorder='little') cipher_and_mac = chacha20_aead_encrypt(bytes(), self.c2a_key, cnt_bytes, bytes([0, 0, 0, 0]), data) cipher_and_mac[0].extend(cipher_and_mac[1]) data = cipher_and_mac[0] logger.debug('cipher and mac %s', cipher_and_mac[0].hex()) result = feature_char.write_value(value=data) logger.debug('write resulted in: %s', result) self.c2a_counter += 1 data = [] while not data or len(data) == 0: time.sleep(1) logger.debug('reading characteristic') data = feature_char.read_value() if not data and not self.device.is_connected(): raise AccessoryDisconnectedError('Characteristic read failed') resp_data = bytearray([b for b in data]) logger.debug('read: %s', bytearray(resp_data).hex()) data = chacha20_aead_decrypt( bytes(), self.a2c_key, self.a2c_counter.to_bytes(8, byteorder='little'), bytes([0, 0, 0, 0]), resp_data) logger.debug('decrypted: %s', bytearray(data).hex()) if not data: return {} # parse header and check stuff logger.debug('parse sig read response %s', bytes([int(a) for a in data]).hex()) # handle the header data cf = data[0] logger.debug('control field %d', cf) tid = data[1] logger.debug('transaction id %d (expected was %d)', tid, transaction_id) status = data[2] logger.debug('status code %d (%s)', status, HapBleStatusCodes[status]) assert cf == 0x02 assert tid == transaction_id if status != HapBleStatusCodes.SUCCESS: raise RequestRejected(status, HapBleStatusCodes[status]) self.a2c_counter += 1 # get body length length = int.from_bytes(data[3:5], byteorder='little') logger.debug('expected body length %d (got %d)', length, len(data[5:])) # parse tlvs and analyse information tlv = tlv8.decode(data[5:]) logger.debug('received TLV: %s', tlv8.format_string(tlv)) return tlv
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
print(json.dumps(data, indent=4, cls=tlv8.JsonEncoder)) if args.output == 'compact': for accessory in data: aid = accessory['aid'] for service in accessory['services']: s_type = service['type'] s_iid = service['iid'] print('{aid}.{iid}: >{stype}<'.format(aid=aid, iid=s_iid, stype=ServicesTypes.get_short(s_type))) for characteristic in service['characteristics']: c_iid = characteristic['iid'] value = characteristic.get('value', '') c_type = characteristic['type'] c_format = characteristic['format'] # we need to get the entry list from the decoder into a string and reformat it for better # readability. if args.decode and c_format in [CharacteristicFormats.tlv8] and isinstance(value, tlv8.EntryList): value = tlv8.format_string(value) value = ' '.join(value.splitlines(keepends=True)) value = '\n ' + value perms = ','.join(characteristic['perms']) desc = characteristic.get('description', '') c_type = CharacteristicsTypes.get_short(c_type) print(' {aid}.{iid}: ({description}) >{ctype}< [{perms}]'.format(aid=aid, iid=c_iid, ctype=c_type, perms=perms, description=desc)) print(' Value: {value}'.format(value=value))
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 = tlv8.encode([ tlv8.Entry(TlvTypes.State, States.M1), tlv8.Entry(TlvTypes.Method, Methods.RemovePairing), tlv8.Entry(TlvTypes.Identifier, pairingIdToDelete.encode()) ]) if connection_type == 'IP': if not IP_TRANSPORT_SUPPORTED: raise TransportNotSupportedError('IP') session = IpSession(pairing_data) response = session.post('/pairings', request_tlv, content_type='application/pairing+tlv8') session.close() data = response.read() data = tlv8.decode( data, { TlvTypes.State: tlv8.DataType.INTEGER, TlvTypes.Error: tlv8.DataType.INTEGER }) elif connection_type == 'BLE': if not BLE_TRANSPORT_SUPPORTED: raise TransportNotSupportedError('BLE') inner = tlv8.encode([ tlv8.Entry(AdditionalParameterTypes.ParamReturnResponse, bytearray(b'\x01')), tlv8.Entry(AdditionalParameterTypes.Value, 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 = tlv8.decode( response.first_by_id(AdditionalParameterTypes.Value).data, { TlvTypes.State: tlv8.DataType.INTEGER, TlvTypes.Error: tlv8.DataType.INTEGER }) 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', tlv8.format_string(data)) state = data.first_by_id(TlvTypes.State).data if len(data) == 1 and state == States.M2: if not pairingId: del self.pairings[alias] else: error = data.first_by_id(TlvTypes.Error) if error and error.data == Errors.Authentication: raise AuthenticationError( 'Remove pairing failed: missing authentication') else: raise UnknownError('Remove pairing failed: unknown error')