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 {}
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
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 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 {}
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)