Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Basic threading functionality for 'get_events' #115

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 42 additions & 51 deletions homekit/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import uuid
import json
import threading
from distutils.util import strtobool
from json.decoder import JSONDecodeError
import time
Expand Down Expand Up @@ -281,22 +282,22 @@ def list_accessories_and_characteristics(self):
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:
except KeyError:
pass

for characteristic in service['characteristics']:
characteristic['type'] = characteristic['type'].upper()
try:
characteristic['type'] = CharacteristicsTypes.get_uuid(characteristic['type'])
except KeyError:
pass
characteristic['type'] = CharacteristicsTypes.get_uuid(characteristic['type'])
except KeyError:
pass

self.pairing_data['accessories'] = accessories
return accessories

Expand Down Expand Up @@ -417,7 +418,7 @@ def get_characteristics(self, characteristics, include_meta=False, include_perms
tmp[key] = c
return tmp

def put_characteristics(self, characteristics, do_conversion=False):
def put_characteristics(self, characteristics, do_conversion=False, field='value', default_value=None):
"""
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
Expand All @@ -438,7 +439,13 @@ def put_characteristics(self, characteristics, do_conversion=False):
for characteristic in characteristics:
aid = characteristic[0]
iid = characteristic[1]
value = characteristic[2]
try:
value = characteristic[2]
except IndexError:
if default_value is not None:
value = default_value
else:
raise
if do_conversion:
# evaluate proper format
c_format = None
Expand All @@ -451,7 +458,7 @@ def put_characteristics(self, characteristics, do_conversion=False):

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.append({'aid': aid, 'iid': iid, field: value})
data = json.dumps({'characteristics': data})

try:
Expand All @@ -475,7 +482,8 @@ def put_characteristics(self, characteristics, do_conversion=False):
return data
return {}

def get_events(self, characteristics, callback_fun, max_events=-1, max_seconds=-1):
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.
Expand All @@ -489,54 +497,26 @@ def get_events(self, characteristics, callback_fun, max_events=-1, max_seconds=-
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 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
:return: a dict mapping 2-tupels of aid and iid to dicts with status and description, e.g.
: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}}
"""
if not self.session:
self.session = Session(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]}
# 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()):
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()
Expand All @@ -557,7 +537,18 @@ def get_events(self, characteristics, callback_fun, max_events=-1, max_seconds=-
tmp.append((c['aid'], c['iid'], c['value']))
callback_fun(tmp)
event_count += 1
return {}
return self.stop_events(characteristics)

def stop_events(self, characteristics):
"""
This functions sets the events to False so that the accessory no longer sends events to the controller

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

:param characteristics: a list of 2-tuples of accessory id (aid) and instance id (iid)
:return: a dict from (aid, iid) onto {status, description}
"""
return self.put_characteristics(characteristics, field='ev', default_value=False)

def identify(self):
"""
Expand Down