예제 #1
0
    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
예제 #2
0
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))
예제 #3
0
    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
예제 #4
0
    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)
예제 #5
0
    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
예제 #7
0
    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']
예제 #8
0
    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)
예제 #9
0
    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)
예제 #10
0
    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]
예제 #11
0
    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)
예제 #12
0
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
예제 #13
0
 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")
예제 #14
0
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')
예제 #16
0
    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
예제 #17
0
    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
예제 #18
0
    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']
예제 #19
0
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
예제 #20
0
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
예제 #21
0
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
예제 #22
0
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
예제 #23
0
    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)
예제 #24
0
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
예제 #25
0
    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']
예제 #26
0
    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']
예제 #27
0
    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')
예제 #29
0
    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)
예제 #30
0
    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)
예제 #31
0
    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
예제 #32
0
    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
예제 #33
0
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
예제 #34
0
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
예제 #35
0
    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