def identify(self): """ This call can be used to trigger the identification of a paired accessory. A successful call should cause the accessory to perform some specific action by which it can be distinguished from the others (blink a LED for example). It uses the identify characteristic as described on page 152 of the spec. :return True, if the identification was run, False otherwise """ if not self.session: self.session = IpSession(self.pairing_data) if 'accessories' not in self.pairing_data: self.list_accessories_and_characteristics() # we are looking for a characteristic of the identify type identify_type = CharacteristicsTypes.get_uuid( CharacteristicsTypes.IDENTIFY) # search all accessories, all services and all characteristics for accessory in self.pairing_data['accessories']: aid = accessory['aid'] for service in accessory['services']: for characteristic in service['characteristics']: iid = characteristic['iid'] c_type = CharacteristicsTypes.get_uuid( characteristic['type']) if identify_type == c_type: # found the identify characteristic, so let's put a value there self.put_characteristics([(aid, iid, True)]) return True return False
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 list_accessories_and_characteristics(self): """ This retrieves a current set of accessories and characteristics behind this pairing. :return: the accessory data as described in the spec on page 73 and following :raises AccessoryNotFoundError: if the device can not be found via zeroconf """ if not self.session: self.session = IpSession(self.pairing_data) try: response = self.session.get('/accessories') except (AccessoryDisconnectedError, EncryptionError): self.session.close() self.session = None raise tmp = response.read().decode() accessories = json.loads(tmp)['accessories'] for accessory in accessories: for service in accessory['services']: service['type'] = service['type'].upper() try: service['type'] = ServicesTypes.get_uuid(service['type']) except KeyError: pass for characteristic in service['characteristics']: characteristic['type'] = characteristic['type'].upper() try: characteristic['type'] = CharacteristicsTypes.get_uuid(characteristic['type']) except KeyError: pass self.pairing_data['accessories'] = accessories return accessories
def setup(self): """Configure an entity baed on its HomeKit characterstics metadata.""" # pylint: disable=import-error from homekit.model.characteristics import CharacteristicsTypes pairing_data = self._accessory.pairing.pairing_data get_uuid = CharacteristicsTypes.get_uuid characteristic_types = [ get_uuid(c) for c in self.get_characteristic_types() ] self._chars_to_poll = [] self._chars = {} self._char_names = {} for accessory in pairing_data.get('accessories', []): if accessory['aid'] != self._aid: continue for service in accessory['services']: if service['iid'] != self._iid: continue for char in service['characteristics']: uuid = CharacteristicsTypes.get_uuid(char['type']) if uuid not in characteristic_types: continue self._setup_characteristic(char)
def setup(self): """Configure an entity baed on its HomeKit characterstics metadata.""" # pylint: disable=import-error from homekit.model.characteristics import CharacteristicsTypes accessories = self._accessory.accessories get_uuid = CharacteristicsTypes.get_uuid characteristic_types = [ get_uuid(c) for c in self.get_characteristic_types() ] self.pollable_characteristics = [] self._chars = {} self._char_names = {} for accessory in accessories: if accessory["aid"] != self._aid: continue self._accessory_info = get_accessory_information(accessory) for service in accessory["services"]: if service["iid"] != self._iid: continue for char in service["characteristics"]: try: uuid = CharacteristicsTypes.get_uuid(char["type"]) except KeyError: # If a KeyError is raised its a non-standard # characteristic. We must ignore it in this case. continue if uuid not in characteristic_types: continue self._setup_characteristic(char)
def __init__(self, iid: int, characteristic_type: str, characteristic_format: str): if type(self) is AbstractCharacteristic: raise Exception( 'AbstractCharacteristic is an abstract class and cannot be instantiated directly' ) self.type = CharacteristicsTypes.get_uuid( characteristic_type) # page 65, see ServicesTypes self.iid = iid # page 65, unique instance id self.perms = [ CharacteristicPermissions.paired_read ] # page 65, array of values from CharacteristicPermissions self.format = characteristic_format # page 66, one of CharacteristicsTypes self.value = None # page 65, required but depends on format self.ev = None # boolean, not required, page 65 self.description = None # string, not required, page 65 self.unit = None # string, not required,page 66, valid values are in CharacteristicUnits self.minValue = None # number, not required, page 66, used if format is int* or float self.maxValue = None # number, not required, page 66, used if format is int* or float self.minStep = None # number, not required, page 66, used if format is int* or float self.maxLen = 64 # number, not required, page 66, used if format is string self.maxDataLen = 2097152 # number, not required, page 66, used if format is data self.valid_values = None # array, not required, see page 67, all numeric entries are allowed values self.valid_values_range = None # 2 element array, not required, see page 67 self._set_value_callback = None self._get_value_callback = None
def update_characteristics(self, characteristics): """Synchronise light state with Home Assistant.""" # pylint: disable=import-error from homekit.model.characteristics import CharacteristicsTypes for characteristic in characteristics: ctype = characteristic['type'] ctype = CharacteristicsTypes.get_short(ctype) if ctype == "on": self._chars['on'] = characteristic['iid'] self._on = characteristic['value'] elif ctype == 'brightness': self._chars['brightness'] = characteristic['iid'] self._features |= SUPPORT_BRIGHTNESS self._brightness = characteristic['value'] elif ctype == 'color-temperature': self._chars['color_temperature'] = characteristic['iid'] self._features |= SUPPORT_COLOR_TEMP self._color_temperature = characteristic['value'] elif ctype == "hue": self._chars['hue'] = characteristic['iid'] self._features |= SUPPORT_COLOR self._hue = characteristic['value'] elif ctype == "saturation": self._chars['saturation'] = characteristic['iid'] self._features |= SUPPORT_COLOR self._saturation = characteristic['value']
def setup(self): """Configure an entity baed on its HomeKit characterstics metadata.""" # pylint: disable=import-error from homekit.model.characteristics import CharacteristicsTypes pairing_data = self._accessory.pairing.pairing_data get_uuid = CharacteristicsTypes.get_uuid characteristic_types = [ get_uuid(c) for c in self.get_characteristic_types() ] self._chars_to_poll = [] self._chars = {} self._char_names = {} for accessory in pairing_data.get('accessories', []): if accessory['aid'] != self._aid: continue self._accessory_info = get_accessory_information(accessory) for service in accessory['services']: if service['iid'] != self._iid: continue for char in service['characteristics']: try: uuid = CharacteristicsTypes.get_uuid(char['type']) except KeyError: # If a KeyError is raised its a non-standard # characteristic. We must ignore it in this case. continue if uuid not in characteristic_types: continue self._setup_characteristic(char)
def add_pairing(self, additional_controller_pairing_identifier, ios_device_ltpk, permissions): if not self.session: self.session = BleSession(self.pairing_data, self.adapter) if permissions == 'User': permissions = TlvTypes.Permission_RegularUser elif permissions == 'Admin': permissions = TlvTypes.Permission_AdminUser else: print('UNKNOWN') request_tlv = tlv8.encode([ tlv8.Entry(TlvTypes.State, States.M1), tlv8.Entry(TlvTypes.Method, Methods.AddPairing), tlv8.Entry(TlvTypes.Identifier, additional_controller_pairing_identifier.encode()), tlv8.Entry(TlvTypes.PublicKey, bytes.fromhex(ios_device_ltpk)), tlv8.Entry(TlvTypes.Permissions, permissions) ]) request_tlv = tlv8.encode([ tlv8.Entry(AdditionalParameterTypes.ParamReturnResponse, bytearray(b'\x01')), tlv8.Entry(AdditionalParameterTypes.Value, request_tlv) ]) body = len(request_tlv).to_bytes(length=2, byteorder='little') + request_tlv cid = -1 for a in self.pairing_data['accessories']: for s in a['services']: for c in s['characteristics']: if CharacteristicsTypes.get_short_uuid(c['type'].upper()) == CharacteristicsTypes.PAIRING_PAIRINGS: cid = c['iid'] fc, _ = self.session.find_characteristic_by_iid(cid) response = self.session.request(fc, cid, HapBleOpCodes.CHAR_WRITE, body) # TODO handle response properly print('unhandled response:', response)
def load(self, char_type: str): """ This function loads a decoder for the specified characteristics: - get the name of the characteristic via the given uuid (via `CharacteristicsTypes.get_short()`) - load a module from `homekit.model.characteristics` plus the name of the characteristic - the module must contain a function `decoder` :param char_type: the uuid of the characteristic :return: a function that decodes the value of the characteristic into a `tlv8.EntryList` """ characteristic_name = CharacteristicsTypes.get_short(char_type) mod_name = characteristic_name.replace('-', '_') if char_type not in self.decoders: try: logging.info('loading module %s for type %s', mod_name, char_type) module = importlib.import_module( 'homekit.model.characteristics.' + mod_name) decoder = getattr(module, 'decoder') self.decoders[char_type] = decoder return decoder except Exception as e: logging.error('Error loading decoder: %s for type %s', e, char_type) return None else: logging.info('got decoder for %s from cache', char_type) return self.decoders[char_type]
def setup(self): """Configure an entity baed on its HomeKit characterstics metadata.""" # pylint: disable=import-error from homekit.model.characteristics import CharacteristicsTypes pairing_data = self._accessory.pairing.pairing_data get_uuid = CharacteristicsTypes.get_uuid characteristic_types = [ get_uuid(c) for c in self.get_characteristic_types() ] self._chars_to_poll = [] self._chars = {} self._char_names = {} for accessory in pairing_data.get('accessories', []): if accessory['aid'] != self._aid: continue for service in accessory['services']: if service['iid'] != self._iid: continue for char in service['characteristics']: try: uuid = CharacteristicsTypes.get_uuid(char['type']) except KeyError: # If a KeyError is raised its a non-standard # characteristic. We must ignore it in this case. continue if uuid not in characteristic_types: continue self._setup_characteristic(char)
def find_characteristic_by_uuid(device, service_uuid, char_uuid): """ # TODO document me :param device: :param service_uuid: :param char_uuid: :return: """ service_found = None logger.debug('services: %s', device.services) for possible_service in device.services: if ServicesTypes.get_short( service_uuid.upper()) == ServicesTypes.get_short( possible_service.uuid.upper()): service_found = possible_service break logger.debug('searched service: %s', service_found) if not service_found: logging.error('searched service not found.') return None, None result_char = None result_char_id = None for characteristic in service_found.characteristics: logger.debug( 'char: %s %s', characteristic.uuid, CharacteristicsTypes.get_short(characteristic.uuid.upper())) if CharacteristicsTypes.get_short( char_uuid.upper()) == CharacteristicsTypes.get_short( characteristic.uuid.upper()): result_char = characteristic for descriptor in characteristic.descriptors: value = descriptor.read_value() if descriptor.uuid == CharacteristicInstanceID: cid = int.from_bytes(value, byteorder='little') result_char_id = cid if not result_char: logging.error('searched char not found.') return None, None logger.debug('searched char: %s %s', result_char, result_char_id) return result_char, result_char_id
def get_hk_char_value(self, characteristic_type): """Return the value for a given characteristic type enum.""" state = self._accessory.current_state.get(self._aid) if not state: return None char = self._chars.get(CharacteristicsTypes.get_short(characteristic_type)) if not char: return None return state.get(char, {}).get("value")
def get_accessory_information(accessory): """Obtain the accessory information service of a HomeKit device.""" result = {} for service in accessory["services"]: stype = service["type"].upper() if ServicesTypes.get_short(stype) != "accessory-information": continue for characteristic in service["characteristics"]: ctype = CharacteristicsTypes.get_short(characteristic["type"]) if "value" in characteristic: result[ctype] = characteristic["value"] return result
def test_get_short(self): self.assertEqual(CharacteristicsTypes.get_short(CharacteristicsTypes.ON), 'on') self.assertEqual(CharacteristicsTypes.get_short(CharacteristicsTypes.get_uuid(CharacteristicsTypes.ON)), 'on') self.assertEqual(CharacteristicsTypes.get_short(CharacteristicsTypes.DOOR_STATE_TARGET), 'door-state.target') self.assertEqual(CharacteristicsTypes.get_short(CharacteristicsTypes.AIR_PURIFIER_STATE_CURRENT), 'air-purifier.state.current') self.assertEqual(CharacteristicsTypes.get_short('1a'), 'lock-management.auto-secure-timeout')
def __init__(self, hass, entity_id, pairing, accessory): """Create a helper for a given accessory/entity.""" self.hass = hass self.entity_id = entity_id self.pairing = pairing self.accessory = accessory self.characteristics = {} for service in self.accessory.services: service_name = ServicesTypes.get_short(service.type) for char in service.characteristics: char_name = CharacteristicsTypes.get_short(char.type) self.characteristics[(service_name, char_name)] = char
def update_characteristics(self, characteristics): """Synchronise the switch state with Home Assistant.""" # pylint: disable=import-error from homekit.model.characteristics import CharacteristicsTypes for characteristic in characteristics: ctype = characteristic['type'] ctype = CharacteristicsTypes.get_short(ctype) if ctype == "on": self._chars['on'] = characteristic['iid'] self._on = characteristic['value'] elif ctype == "outlet-in-use": self._chars['outlet-in-use'] = characteristic['iid'] self._outlet_in_use = characteristic['value']
def get_serial(accessory): """Obtain the serial number of a HomeKit device.""" # pylint: disable=import-error from homekit.model.services import ServicesTypes from homekit.model.characteristics import CharacteristicsTypes for service in accessory['services']: if ServicesTypes.get_short(service['type']) != \ 'accessory-information': continue for characteristic in service['characteristics']: ctype = CharacteristicsTypes.get_short(characteristic['type']) if ctype != 'serial-number': continue return characteristic['value'] return None
def get_accessory_information(accessory): """Obtain the accessory information service of a HomeKit device.""" # pylint: disable=import-error from homekit.model.services import ServicesTypes from homekit.model.characteristics import CharacteristicsTypes result = {} for service in accessory["services"]: stype = service["type"].upper() if ServicesTypes.get_short(stype) != "accessory-information": continue for characteristic in service["characteristics"]: ctype = CharacteristicsTypes.get_short(characteristic["type"]) if "value" in characteristic: result[ctype] = characteristic["value"] return result
def get_accessory_information(accessory): """Obtain the accessory information service of a HomeKit device.""" # pylint: disable=import-error from homekit.model.services import ServicesTypes from homekit.model.characteristics import CharacteristicsTypes result = {} for service in accessory['services']: stype = service['type'].upper() if ServicesTypes.get_short(stype) != 'accessory-information': continue for characteristic in service['characteristics']: ctype = CharacteristicsTypes.get_short(characteristic['type']) if 'value' in characteristic: result[ctype] = characteristic['value'] return result
def _setup_characteristic(self, char): """Configure an entity based on a HomeKit characteristics metadata.""" # Build up a list of (aid, iid) tuples to poll on update() self.pollable_characteristics.append((self._aid, char["iid"])) # Build a map of ctype -> iid short_name = CharacteristicsTypes.get_short(char["type"]) self._chars[short_name] = char["iid"] self._char_names[char["iid"]] = short_name # Callback to allow entity to configure itself based on this # characteristics metadata (valid values, value ranges, features, etc) setup_fn_name = escape_characteristic_name(short_name) setup_fn = getattr(self, f"_setup_{setup_fn_name}", None) if not setup_fn: return setup_fn(char)
def get_serial(accessory): """Obtain the serial number of a HomeKit device.""" # pylint: disable=import-error from homekit.model.services import ServicesTypes from homekit.model.characteristics import CharacteristicsTypes for service in accessory['services']: if ServicesTypes.get_short(service['type']) != \ 'accessory-information': continue for characteristic in service['characteristics']: ctype = CharacteristicsTypes.get_short( characteristic['type']) if ctype != 'serial-number': continue return characteristic['value'] return None
def update_characteristics(self, characteristics): """Synchronise the Alarm Control Panel state with Home Assistant.""" # pylint: disable=import-error from homekit.model.characteristics import CharacteristicsTypes for characteristic in characteristics: ctype = characteristic['type'] ctype = CharacteristicsTypes.get_short(ctype) if ctype == "security-system-state.current": self._chars['security-system-state.current'] = \ characteristic['iid'] self._state = CURRENT_STATE_MAP[characteristic['value']] elif ctype == "security-system-state.target": self._chars['security-system-state.target'] = \ characteristic['iid'] elif ctype == "battery-level": self._chars['battery-level'] = characteristic['iid'] self._battery_level = characteristic['value']
def update_characteristics(self, characteristics): """Synchronise the Lock state with Home Assistant.""" # pylint: disable=import-error from homekit.model.characteristics import CharacteristicsTypes for characteristic in characteristics: ctype = characteristic['type'] ctype = CharacteristicsTypes.get_short(ctype) if ctype == "lock-mechanism.current-state": self._chars['lock-mechanism.current-state'] = \ characteristic['iid'] self._state = CURRENT_STATE_MAP[characteristic['value']] elif ctype == "lock-mechanism.target-state": self._chars['lock-mechanism.target-state'] = \ characteristic['iid'] elif ctype == "battery-level": self._chars['battery-level'] = characteristic['iid'] self._battery_level = characteristic['value']
def list_pairings(self): if not self.session: self.session = BleSession(self.pairing_data, self.adapter) request_tlv = tlv8.encode([ tlv8.Entry(TlvTypes.State, States.M1), tlv8.Entry(TlvTypes.Method, Methods.ListPairings) ]) request_tlv = tlv8.encode([ tlv8.Entry(AdditionalParameterTypes.ParamReturnResponse, bytearray(b'\x01')), tlv8.Entry(AdditionalParameterTypes.Value, request_tlv) ]) body = len(request_tlv).to_bytes(length=2, byteorder='little') + request_tlv cid = -1 for a in self.pairing_data['accessories']: for s in a['services']: for c in s['characteristics']: if CharacteristicsTypes.get_short_uuid(c['type'].upper( )) == CharacteristicsTypes.PAIRING_PAIRINGS: cid = c['iid'] fc, _ = self.session.find_characteristic_by_iid(cid) response = self.session.request(fc, cid, HapBleOpCodes.CHAR_WRITE, body) response = tlv8.decode( response.first_by_id(AdditionalParameterTypes.Value).data) tmp = [] r = {} for d in response[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
def test_get_short(self): self.assertEqual( CharacteristicsTypes.get_short(CharacteristicsTypes.ON), 'on') self.assertEqual( CharacteristicsTypes.get_short( CharacteristicsTypes.get_uuid(CharacteristicsTypes.ON)), 'on') self.assertEqual( CharacteristicsTypes.get_short( CharacteristicsTypes.DOOR_STATE_TARGET), 'door-state.target') self.assertEqual( CharacteristicsTypes.get_short( CharacteristicsTypes.AIR_PURIFIER_STATE_CURRENT), 'air-purifier.state.current') self.assertEqual(CharacteristicsTypes.get_short('1a'), 'Unknown Characteristic 1a')
def _setup_characteristic(self, char): """Configure an entity based on a HomeKit characteristics metadata.""" # pylint: disable=import-error from homekit.model.characteristics import CharacteristicsTypes # Build up a list of (aid, iid) tuples to poll on update() self.pollable_characteristics.append((self._aid, char["iid"])) # Build a map of ctype -> iid short_name = CharacteristicsTypes.get_short(char["type"]) self._chars[short_name] = char["iid"] self._char_names[char["iid"]] = short_name # Callback to allow entity to configure itself based on this # characteristics metadata (valid values, value ranges, features, etc) setup_fn_name = escape_characteristic_name(short_name) setup_fn = getattr(self, "_setup_{}".format(setup_fn_name), None) if not setup_fn: return # pylint: disable=not-callable setup_fn(char)
def _setup_characteristic(self, char): """Configure an entity based on a HomeKit characteristics metadata.""" # pylint: disable=import-error from homekit.model.characteristics import CharacteristicsTypes # Build up a list of (aid, iid) tuples to poll on update() self._chars_to_poll.append((self._aid, char['iid'])) # Build a map of ctype -> iid short_name = CharacteristicsTypes.get_short(char['type']) self._chars[short_name] = char['iid'] self._char_names[char['iid']] = short_name # Callback to allow entity to configure itself based on this # characteristics metadata (valid values, value ranges, features, etc) setup_fn_name = escape_characteristic_name(short_name) setup_fn = getattr(self, '_setup_{}'.format(setup_fn_name), None) if not setup_fn: return # pylint: disable=not-callable setup_fn(char)
def list_pairings(self): if not self.session: self.session = BleSession(self.pairing_data, self.adapter) request_tlv = TLV.encode_list([(TLV.kTLVType_State, TLV.M1), (TLV.kTLVType_Method, TLV.ListPairings) ]) request_tlv = TLV.encode_list([(TLV.kTLVHAPParamParamReturnResponse, bytearray(b'\x01')), (TLV.kTLVHAPParamValue, request_tlv)]) body = len(request_tlv).to_bytes(length=2, byteorder='little') + request_tlv cid = -1 for a in self.pairing_data['accessories']: for s in a['services']: for c in s['characteristics']: if CharacteristicsTypes.get_short_uuid(c['type'].upper( )) == CharacteristicsTypes.PAIRING_PAIRINGS: cid = c['iid'] fc, _ = self.session.find_characteristic_by_iid(cid) response = self.session.request(fc, cid, HapBleOpCodes.CHAR_WRITE, body) response = TLV.decode_bytes(response[1]) tmp = [] r = {} for d in response[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 identify(self): """ This call can be used to trigger the identification of a paired accessory. A successful call should cause the accessory to perform some specific action by which it can be distinguished from the others (blink a LED for example). It uses the identify characteristic as described on page 152 of the spec. :return True, if the identification was run, False otherwise """ if not self.session: self.session = BleSession(self.pairing_data, self.adapter) cid = -1 aid = -1 for a in self.pairing_data['accessories']: for s in a['services']: for c in s['characteristics']: if CharacteristicsTypes.get_short_uuid(c['type'].upper()) == CharacteristicsTypes.IDENTIFY: aid = a['aid'] cid = c['iid'] self.put_characteristics([(aid, cid, True)]) # TODO check for errors return True
def create_proxy(accessories_and_characteristics): """ Create a proxy in front of a set of accessories, services and characteristics. This allows to follow the communication of a controller and an accessory (e.g. an iPhone and some HomeKit IP camera). :param accessories_and_characteristics: the accessory data as described in the spec on page 73 and following. This contains all the data that will be used to create the proxy. :return: a list of `Accessory` instances whose services and characteristics are replaced by proxied versions. That means characteristic's callback functions for getting and setting values relay those calls to the proxied characteristics. """ accessories = [] logging.info('%<------ creating proxy ------') for accessory in accessories_and_characteristics: proxy_accessory = Accessory( '', '', '', '', '', ) aid = accessory['aid'] proxy_accessory.aid = aid logging.info('accessory with aid=%s', aid) proxy_accessory.services = [] accessories.append(proxy_accessory) for service in accessory['services']: service_iid = service['iid'] service_type = service['type'] short_type = ServicesTypes.get_short(service_type) logging.info(' %i.%i: >%s< (%s)', aid, service_iid, short_type, service_type) proxy_service = ProxyService(service_iid, service_type) proxy_accessory.add_service(proxy_service) for characteristic in service['characteristics']: characteristic_iid = characteristic['iid'] characteristic_type = characteristic['type'] short_type = CharacteristicsTypes.get_short( characteristic_type) characteristic_format = characteristic['format'] characteristic_value = characteristic.get('value') characteristic_perms = characteristic['perms'] logging.info(' %i.%i: %s >%s< (%s) [%s] %s', aid, characteristic_iid, characteristic_value, short_type, characteristic_type, ','.join(characteristic_perms), characteristic_format) proxy_characteristic = ProxyCharacteristic( characteristic_iid, characteristic_type, characteristic_format) proxy_service.append_characteristic(proxy_characteristic) if characteristic_value: proxy_characteristic.value = characteristic_value proxy_characteristic.perms = characteristic_perms proxy_characteristic.set_set_value_callback( generate_set_value_callback(accessory['aid'], proxy_characteristic)) proxy_characteristic.set_get_value_callback( generate_get_value_callback(accessory['aid'], proxy_characteristic)) logging.info('%<------ finished creating proxy ------') return accessories
def read_characteristics(device): # TODO document me # FIXME: This only works on non secure sessions logger.debug('resolved %d services', len(device.services)) a_data = {'aid': 1, 'services': []} resolved_data = { 'data': [a_data], } for service in device.services: logger.debug('found service with UUID %s (%s)', service.uuid, ServicesTypes.get_short(service.uuid.upper())) s_data = {'characteristics': [], 'iid': None} for characteristic in service.characteristics: logger.debug( '\tfound characteristic with UUID %s (%s)', characteristic.uuid, CharacteristicsTypes.get_short(characteristic.uuid.upper())) if characteristic.uuid.upper( ) == CharacteristicsTypes.SERVICE_INSTANCE_ID: sid = int.from_bytes(characteristic.read_value(), byteorder='little') logger.debug('\t\tread service id %d', sid) s_data['iid'] = sid else: c_data = { 'iid': None, 'type': characteristic.uuid.upper(), 'perms': [] } iid = None for descriptor in characteristic.descriptors: value = descriptor.read_value() if descriptor.uuid == CharacteristicInstanceID: iid = int.from_bytes(value, byteorder='little') logger.debug('\t\tread characteristic id %d', iid) c_data['iid'] = iid else: # print('\t\t', 'D', descriptor.uuid, value) pass if iid: v = iid.to_bytes(length=2, byteorder='little') tid = random.randrange(0, 255) characteristic.write_value( [0x00, HapBleOpCodes.CHAR_SIG_READ, tid, v[0], v[1]]) d = parse_sig_read_response(characteristic.read_value(), tid) for k in d: if k == 'service_type': s_data['type'] = d[k].upper() elif k == 'sid': s_data['iid'] = d[k] else: c_data[k] = d[k] if c_data['iid']: s_data['characteristics'].append(c_data) if s_data['iid']: a_data['services'].append(s_data) logging.debug('data: %s', resolved_data) return resolved_data
def __init__(self, pairing_data, adapter): self.adapter = adapter self.pairing_data = pairing_data self.c2a_counter = 0 self.a2c_counter = 0 self.c2a_key = None self.a2c_key = None self.device = None mac_address = self.pairing_data['AccessoryMAC'] manager = DeviceManager(self.adapter) self.device = manager.make_device(mac_address) logger.debug('connecting to device') self.device.connect() logger.debug('connected to device') uuid_map = {} for s in self.device.services: for c in s.characteristics: uuid_map[(s.uuid.upper(), c.uuid.upper())] = c self.uuid_map = {} self.iid_map = {} self.short_map = {} for a in pairing_data['accessories']: for s in a['services']: s_short = None if s['type'].endswith(ServicesTypes.baseUUID): s_short = s['type'].split('-', 1)[0].lstrip('0') for c in s['characteristics']: char = uuid_map.get((s['type'], c['type']), None) if not char: continue self.iid_map[c['iid']] = (char, c) self.uuid_map[(s['type'], c['type'])] = (char, c) if s_short and c['type'].endswith( CharacteristicsTypes.baseUUID): c_short = c['type'].split('-', 1)[0].lstrip('0') self.short_map[(s_short, c_short)] = (char, c) service_type_short = ServicesTypes.get_short(s['type']) characteristic_type_short = CharacteristicsTypes.get_short( c['type']) self.short_map[service_type_short, characteristic_type_short] = (char, c) pair_verify_char, pair_verify_char_info = self.short_map.get( (ServicesTypes.PAIRING_SERVICE, CharacteristicsTypes.PAIR_VERIFY), (None, None)) if not pair_verify_char: print('verify characteristic not found') # TODO Have exception here sys.exit(-1) write_fun = create_ble_pair_setup_write(pair_verify_char, pair_verify_char_info['iid']) state_machine = get_session_keys(self.pairing_data) request, expected = state_machine.send(None) while True: try: response = write_fun(request, expected) request, expected = state_machine.send(response) except StopIteration as result: self.c2a_key, self.a2c_key = result.value break logger.debug('pair_verified, keys: \n\t\tc2a: %s\n\t\ta2c: %s', self.c2a_key.hex(), self.a2c_key.hex()) self.c2a_counter = 0 self.a2c_counter = 0