Skip to content
Closed
Show file tree
Hide file tree
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
161 changes: 132 additions & 29 deletions homekit/controller/ble_impl/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
import sys
import uuid
import struct
import threading
import queue
from distutils.util import strtobool

from homekit.controller.tools import AbstractPairing
Expand Down Expand Up @@ -64,7 +66,13 @@ def __init__(self, pairing_data, adapter='hci0'):
"""
self.adapter = adapter
self.pairing_data = pairing_data
self.session = None
self._session = None

@property
def session(self):
if not self._session:
self._session = BleSession(self)
return self._session

def close(self):
pass
Expand All @@ -81,8 +89,8 @@ def list_accessories_and_characteristics(self):
return resolved_data['data']

def list_pairings(self):
if not self.session:
self.session = BleSession(self.pairing_data, self.adapter)
if not self.session.connected:
self.session.connect()
request_tlv = TLV.encode_list([
(TLV.kTLVType_State, TLV.M1),
(TLV.kTLVType_Method, TLV.ListPairings)
Expand Down Expand Up @@ -119,10 +127,6 @@ def list_pairings(self):
r['controllerType'] = controller_type
return tmp

def get_events(self, characteristics, callback_fun, max_events=-1, max_seconds=-1):
# TODO implementation still missing
pass

def identify(self):
"""
This call can be used to trigger the identification of a paired accessory. A successful call should
Expand All @@ -133,8 +137,8 @@ def identify(self):

:return True, if the identification was run, False otherwise
"""
if not self.session:
self.session = BleSession(self.pairing_data, self.adapter)
if not self.session.connected:
self.session.connect()
cid = -1
aid = -1
for a in self.pairing_data['accessories']:
Expand Down Expand Up @@ -165,8 +169,8 @@ def get_characteristics(self, characteristics, include_meta=False, include_perms
(1, 37): {'description': 'Resource does not exist.', 'status': -70409}
}
"""
if not self.session:
self.session = BleSession(self.pairing_data, self.adapter)
if not self.session.connected:
self.session.connect()

results = {}
for aid, cid in characteristics:
Expand All @@ -181,7 +185,6 @@ def get_characteristics(self, characteristics, include_meta=False, include_perms
response = self.session.request(fc, cid, HapBleOpCodes.CHAR_READ)
except Exception as e:
self.session.close()
self.session = None
raise e

value = self._convert_to_python(aid, cid, response[1]) if 1 in response else None
Expand Down Expand Up @@ -343,8 +346,8 @@ def put_characteristics(self, characteristics, do_conversion=False):
:raises FormatError: if the input value could not be converted to the target type and conversion was
requested
"""
if not self.session:
self.session = BleSession(self.pairing_data, self.adapter)
if not self.session.connected:
self.session.connect()

results = {}

Expand Down Expand Up @@ -372,14 +375,13 @@ def put_characteristics(self, characteristics, do_conversion=False):
}
except Exception as e:
self.session.close()
self.session = None
raise e

return results

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 not self.session.connected:
self.session.connect()
if permissions == 'User':
permissions = TLV.kTLVType_Permission_RegularUser
elif permissions == 'Admin':
Expand Down Expand Up @@ -412,22 +414,112 @@ def add_pairing(self, additional_controller_pairing_identifier, ios_device_ltpk,
# TODO handle response properly
print('unhandled response:', response)

def get_message_bus(self):
if not self.session.connected:
self.session.connect()
return BleMessageBus(self)


class BleMessageBus(object):

# FIXME: Think about some kind of stop() / close() to get rid of the thread

def __init__(self, pairing):
self.pairing = pairing
self.session = pairing.session

self.queue = queue.Queue()

self.pairing.session.device.manager.start_discovery()
self.monitor = threading.Thread(target=self.pairing.session.device.manager.run)
self.monitor.start()

self.session.device.subscribers.append(self._handle_event)

def _handle_event(self, event, uuid=None):
print(event, uuid)
if event == 'char_updated':
if not uuid or uuid not in self.session.uuid_map:
print('unknown uuid')
return
_, c = self.session.uuid_map[uuid]
self.get_characteristics([
(1, c['iid']),
])
elif event == 'gsn':
self.get_characteristics(self.session.subscriptions)

def __iter__(self):
return self

def __next__(self):
return self.queue.get()

def get_characteristics(self, characteristics, include_meta=False, include_perms=False, include_type=False,
include_events=False):
self.queue.put_nowait(
self.pairing.get_characteristics(
characteristics,
include_meta=include_meta,
include_perms=include_perms,
include_type=include_type,
include_events=include_events,
)
)

def put_characteristics(self, characteristics, do_conversion=False):
self.queue.put_nowait(
self.pairing.put_characteristics(characteristics, do_conversion)
)

def subscribe(self, characteristics):
session = self.pairing.session
for (aid, iid) in characteristics:
print('subscribe', iid)
session.subscriptions.add((aid, iid))
if session.connected:
print('enable_not')
fc, fc_id = session.find_characteristic_by_iid(iid)
fc.enable_notifications()

def unsubscribe(self, characteristics):
for (aid, iid) in characteristics:
self.subscriptions.discard((aid, iid))
if self.pairing.session.connected:
fc, fc_id = self.pairing.session.find_characteristic_by_iid(iid)
fc.disable_notifications()


class BleSession(object):

def __init__(self, pairing_data, adapter):
self.adapter = adapter
self.pairing_data = pairing_data
def __init__(self, pairing):
self.pairing = pairing
self.session_lock = threading.Lock()
self.subscriptions = set()
self.connected = False

mac_address = self.pairing.pairing_data['AccessoryMAC']
manager = DeviceManager(self.pairing.adapter)
self.device = manager.make_device(mac_address)
self.device.disconnect()

self.device.subscribers.append(self._handle_event)

def _handle_event(self, event, uuid=None):
if event == 'disconnected':
self.close()

def connect(self):
with self.session_lock:
if not self.connected:
self._connect()

def _connect(self):
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')
Expand All @@ -440,7 +532,7 @@ def __init__(self, pairing_data, adapter):
self.uuid_map = {}
self.iid_map = {}
self.short_map = {}
for a in pairing_data['accessories']:
for a in self.pairing.pairing_data['accessories']:
for s in a['services']:
s_short = None
if s['type'].endswith(ServicesTypes.baseUUID):
Expand All @@ -451,7 +543,7 @@ def __init__(self, pairing_data, adapter):
if not char:
continue
self.iid_map[c['iid']] = (char, c)
self.uuid_map[(s['type'], c['type'])] = (char, c)
self.uuid_map[char.uuid] = (char, c)

if s_short and c['type'].endswith(CharacteristicsTypes.baseUUID):
c_short = c['type'].split('-', 1)[0].lstrip('0')
Expand All @@ -472,18 +564,29 @@ def __init__(self, pairing_data, adapter):
sys.exit(-1)

write_fun = create_ble_pair_setup_write(pair_verify_char, pair_verify_char_info['iid'])
self.c2a_key, self.a2c_key = get_session_keys(None, self.pairing_data, write_fun)
self.c2a_key, self.a2c_key = get_session_keys(None, self.pairing.pairing_data, write_fun)
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

self.connected = True

for (aid, iid) in self.subscriptions:
print('ENABLE')
fc, fc_id = self.find_characteristic_by_iid(iid)
fc.enable_notifications()

print('REALLY')

def __del__(self):
self.close()

def close(self):
logger.debug('closing session')
self.device.disconnect()
if self.conneced:
self.device.disconnect()
self.connected = False

def find_characteristic_by_iid(self, cid):
return self.iid_map.get(cid, (None, None))
Expand Down
24 changes: 24 additions & 0 deletions homekit/controller/ble_impl/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ def __init__(self, *args, **kwargs):
self.name = self._properties.Get('org.bluez.Device1', 'Alias')
self.homekit_discovery_data = self.get_homekit_discovery_data()

self.subscribers = []
self.gsn = self.homekit_discovery_data.get('gsn', 0)

def get_homekit_discovery_data(self):
"""
Retrieve and decode the latest ManufacturerData from BlueZ for a
Expand Down Expand Up @@ -114,6 +117,27 @@ def characteristic_write_value_succeeded(self, characteristic):
def characteristic_write_value_failed(self, characteristic, error):
logger.debug('write failed: %s %s', characteristic, error)

def advertised(self):
data = self.get_homekit_discovery_data()
if 'gsn' not in data:
return
if data['gsn'] != self.gsn:
for callback in self.subscribers:
callback('gsn')
self.gsn = data['gsn']

def disconnect_succeeded(self):
for callback in self.subscribers:
callback('disconnect')

def characteristic_value_updated(self, characteristic, value):
if value != b'':
# We are only interested in in blank values
return

for callback in self.subscribers:
callback('char_updated', characteristic.uuid)


class DeviceManager(gatt.DeviceManager):

Expand Down
Loading