コード例 #1
0
    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 = Session(self.pairing_data)
        if 'accessories' not in self.pairing_data:
            self.list_accessories_and_characteristics()
        data = []
        characteristics_set = set()
        for characteristic in characteristics:
            aid = characteristic[0]
            iid = characteristic[1]
            value = characteristic[2]
            if do_conversion:
                # evaluate proper format
                c_format = None
                for d in self.pairing_data['accessories']:
                    if 'aid' in d and d['aid'] == aid:
                        for s in d['services']:
                            for c in s['characteristics']:
                                if 'iid' in c and c['iid'] == iid:
                                    c_format = c['format']

                value = check_convert_value(value, c_format)
            characteristics_set.add('{a}.{i}'.format(a=aid, i=iid))
            data.append({'aid': aid, 'iid': iid, 'value': value})
        data = json.dumps({'characteristics': data})

        try:
            response = self.session.put('/characteristics', data)
        except AccessoryDisconnectedError:
            self.session.close()
            self.session = None
            raise

        if response.code != 204:
            data = response.read().decode()
            try:
                data = json.loads(data)['characteristics']
            except JSONDecodeError:
                self.session.close()
                self.session = None
                raise AccessoryDisconnectedError("Session closed after receiving malformed response from device")

            data = {(d['aid'], d['iid']): {'status': d['status'], 'description': HapStatusCodes[d['status']]} for d in
                    data}
            return data
        return {}
コード例 #2
0
    def get_characteristics(self, characteristics, include_meta=False, include_perms=False, include_type=False,
                            include_events=False):
        """
        This method is used to get the current readouts of any characteristic of the accessory.

        :param characteristics: a list of 2-tupels of accessory id and instance id
        :param include_meta: if True, include meta information about the characteristics. This contains the format and
                             the various constraints like maxLen and so on.
        :param include_perms: if True, include the permissions for the requested characteristics.
        :param include_type: if True, include the type of the characteristics in the result. See CharacteristicsTypes
                             for translations.
        :param include_events: if True on a characteristics that supports events, the result will contain information if
                               the controller currently is receiving events for that characteristic. Key is 'ev'.
        :return: a dict mapping 2-tupels of aid and iid to dicts with value or status and description, e.g.
                 {(1, 8): {'value': 23.42}
                  (1, 37): {'description': 'Resource does not exist.', 'status': -70409}
                 }
        """
        if not self.session:
            self.session = IpSession(self.pairing_data)
        url = '/characteristics?id=' + ','.join([str(x[0]) + '.' + str(x[1]) for x in characteristics])
        if include_meta:
            url += '&meta=1'
        if include_perms:
            url += '&perms=1'
        if include_type:
            url += '&type=1'
        if include_events:
            url += '&ev=1'

        try:
            response = self.session.get(url)
        except (AccessoryDisconnectedError, EncryptionError):
            self.session.close()
            self.session = None
            raise

        try:
            data = json.loads(response.read().decode())['characteristics']
        except JSONDecodeError:
            self.session.close()
            self.session = None
            raise AccessoryDisconnectedError("Session closed after receiving malformed response from device")

        tmp = {}
        for c in data:
            key = (c['aid'], c['iid'])
            del c['aid']
            del c['iid']

            if 'status' in c and c['status'] == 0:
                del c['status']
            if 'status' in c and c['status'] != 0:
                c['description'] = HapStatusCodes[c['status']]
            tmp[key] = c
        return tmp
コード例 #3
0
    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
コード例 #4
0
    def get_events(self,
                   characteristics,
                   callback_fun,
                   max_events=-1,
                   max_seconds=-1):
        """
        This function is called to register for events on characteristics and receive them. Each time events are
        received a call back function is invoked. By that the caller gets information about the events.

        The characteristics are identified via their proper accessory id (aid) and instance id (iid).

        The call back function takes a list of 3-tupels of aid, iid and the value, e.g.:
          [(1, 9, 26.1), (1, 10, 30.5)]

        If the input contains characteristics without the event permission or any other error, the function will return
        a dict containing tupels of aid and iid for each requested characteristic with error. Those who would have
        worked are not in the result.

        :param characteristics: a list of 2-tupels of accessory id (aid) and instance id (iid)
        :param callback_fun: a function that is called each time events were recieved
        :param max_events: number of reported events, default value -1 means unlimited
        :param max_seconds: number of seconds to wait for events, default value -1 means unlimited
        :return: a dict mapping 2-tupels of aid and iid to dicts with status and description, e.g.
                 {(1, 37): {'description': 'Notification is not supported for characteristic.', 'status': -70406}}
        """
        if not self.session:
            self.session = IpSession(self.pairing_data)
        data = []
        characteristics_set = set()
        for characteristic in characteristics:
            aid = characteristic[0]
            iid = characteristic[1]
            characteristics_set.add('{a}.{i}'.format(a=aid, i=iid))
            data.append({'aid': aid, 'iid': iid, 'ev': True})
        data = json.dumps({'characteristics': data})

        try:
            response = self.session.put('/characteristics', data)
        except (AccessoryDisconnectedError, EncryptionError):
            self.session.close()
            self.session = None
            raise

        # handle error responses
        if response.code != 204:
            tmp = {}
            try:
                data = json.loads(response.read().decode())
            except JSONDecodeError:
                self.session.close()
                self.session = None
                raise AccessoryDisconnectedError(
                    "Session closed after receiving malformed response from device"
                )

            for characteristic in data['characteristics']:
                status = characteristic['status']
                if status == 0:
                    continue
                aid = characteristic['aid']
                iid = characteristic['iid']
                tmp[(aid, iid)] = {
                    'status': status,
                    'description': HapStatusCodes[status]
                }
            return tmp

        # wait for incoming events
        event_count = 0
        s = time.time()
        while (max_events == -1 or event_count < max_events) and (
                max_seconds == -1 or s + max_seconds >= time.time()):
            try:
                r = self.session.sec_http.handle_event_response()
                body = r.read().decode()
            except (AccessoryDisconnectedError, EncryptionError):
                self.session.close()
                self.session = None
                raise

            if len(body) > 0:
                try:
                    r = json.loads(body)
                except JSONDecodeError:
                    self.session.close()
                    self.session = None
                    raise AccessoryDisconnectedError(
                        "Session closed after receiving malformed response from device"
                    )
                tmp = []
                for c in r['characteristics']:
                    tmp.append((c['aid'], c['iid'], c['value']))
                callback_fun(tmp)
                event_count += 1
        return {}
コード例 #5
0
    def get_events(self,
                   characteristics,
                   callback_fun,
                   max_events=-1,
                   max_seconds=-1,
                   stop_event: threading.Event = None):
        """
        This function is called to register for events on characteristics and receive them. Each time events are
        received a call back function is invoked. By that the caller gets information about the events.

        The characteristics are identified via their proper accessory id (aid) and instance id (iid).

        The call back function takes a list of 3-tupels of aid, iid and the value, e.g.:
          [(1, 9, 26.1), (1, 10, 30.5)]

        If the input contains characteristics without the event permission or any other error, the function will return
        a dict containing tupels of aid and iid for each requested characteristic with error. Those who would have
        worked are not in the result.

        :param characteristics: a list of 2-tuples of accessory id (aid) and instance id (iid)
        :param callback_fun: a function that is called each time events were received
        :param max_events: number of reported events, default value -1 means unlimited
        :param max_seconds: number of seconds to wait for events, default value -1 means unlimited
        :param stop_event: a threading.Event instance that when set commands this function to clean up and return
        :return: a dict mapping 2-tuples of aid and iid to dicts with status and description, e.g.
                 {(1, 37): {'description': 'Notification is not supported for characteristic.', 'status': -70406}}
        """
        # register for events
        # characteristics = [(a, b, True) for a, b in characteristics]
        tmp = self.put_characteristics(characteristics,
                                       field='ev',
                                       default_value=True)
        if tmp != {}:
            return tmp

        # wait for incoming events
        event_count = 0
        s = time.time()
        while ((max_events == -1 or event_count < max_events)
               and (max_seconds == -1 or s + max_seconds >= time.time())
               and (stop_event is None or not stop_event.isSet())):
            try:
                r = self.session.sec_http.handle_event_response()
                body = r.read().decode()
            except (AccessoryDisconnectedError, EncryptionError):
                self.session.close()
                self.session = None
                raise

            if len(body) > 0:
                try:
                    r = json.loads(body)
                except JSONDecodeError:
                    self.session.close()
                    self.session = None
                    raise AccessoryDisconnectedError(
                        "Session closed after receiving malformed response from device"
                    )
                tmp = []
                for c in r['characteristics']:
                    tmp.append((c['aid'], c['iid'], c['value']))
                callback_fun(tmp)
                event_count += 1
        return self.stop_events(characteristics)