Skip to content

Commit 0302111

Browse files
committed
Some testing around automatic reconnects
1 parent 5cd977d commit 0302111

3 files changed

Lines changed: 40 additions & 39 deletions

File tree

homekit/aio/controller/ip/connection.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,17 @@ def connection_lost(self, exception):
5858
self.connection._connection_lost(exception)
5959

6060
def send_bytes(self, payload):
61+
if self.transport.is_closing():
62+
# FIXME: It would be nice to try and wait for the reconnect in future.
63+
# In that case we need to make sure we do it at a layer above send_bytes otherwise
64+
# we might encrypt payloads with the last sessions keys then wait for a new connection
65+
# to send them - and on that connection the keys would be different.
66+
# Also need to make sure that the new connection has chance to pair-verify before
67+
# queued writes can happy.
68+
raise AccessoryDisconnectedError('Transport is closed')
69+
6170
self.transport.write(payload)
71+
6272
# We return a future so that our caller can block on a reply
6373
# We can send many requests and dispatch the results in order
6474
# Should mean we don't need locking around request/reply cycles
@@ -378,6 +388,15 @@ def _connection_lost(self, exception):
378388
"""
379389
logger.info("Connection %r lost.", self)
380390

391+
if not self.when_connected.done():
392+
self.when_connected.set_exception(
393+
AccessoryDisconnectedError(
394+
'Current connection attempt failed and will be retried',
395+
)
396+
)
397+
398+
self.when_connected = asyncio.Future()
399+
381400
if self.auto_reconnect and not self.closing:
382401
asyncio.ensure_future(self._reconnect())
383402

@@ -393,9 +412,6 @@ async def _connect_once(self):
393412
)
394413

395414
async def _reconnect(self):
396-
if self.when_connected.done():
397-
self.when_connected = asyncio.Future()
398-
399415
# FIXME: How to integrate discovery here?
400416
# There is aiozeroconf but that doesn't work on Windows until python 3.9
401417
# In HASS, zeroconf is a service provided by HASS itself and want to be able to

homekit/aio/controller/ip/pairing.py

Lines changed: 9 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,7 @@
2222

2323
from homekit.controller.tools import AbstractPairing, check_convert_value
2424
from homekit.protocol.statuscodes import HapStatusCodes
25-
from homekit.exceptions import UnknownError, UnpairedError, \
26-
AccessoryDisconnectedError, EncryptionError
25+
from homekit.exceptions import UnknownError, UnpairedError
2726
from homekit.protocol.tlv import TLV
2827
from homekit.model.characteristics import CharacteristicsTypes
2928
from homekit.model.services import ServicesTypes
@@ -79,12 +78,7 @@ async def list_accessories_and_characteristics(self):
7978
"""
8079
await self._ensure_connected()
8180

82-
try:
83-
response = await self.connection.get_json('/accessories')
84-
except (AccessoryDisconnectedError, EncryptionError):
85-
self.session.close()
86-
self.session = None
87-
raise
81+
response = await self.connection.get_json('/accessories')
8882

8983
accessories = response['accessories']
9084

@@ -123,15 +117,10 @@ async def list_pairings(self):
123117
"""
124118
await self._ensure_connected()
125119

126-
try:
127-
data = await self.connection.post_tlv('/pairings', [
128-
(TLV.kTLVType_State, TLV.M1),
129-
(TLV.kTLVType_Method, TLV.ListPairings)
130-
])
131-
except (AccessoryDisconnectedError, EncryptionError):
132-
self.session.close()
133-
self.session = None
134-
raise
120+
data = await self.connection.post_tlv('/pairings', [
121+
(TLV.kTLVType_State, TLV.M1),
122+
(TLV.kTLVType_Method, TLV.ListPairings)
123+
])
135124

136125
if not (data[0][0] == TLV.kTLVType_State and data[0][1] == TLV.M2):
137126
raise UnknownError('unexpected data received: ' + str(data))
@@ -188,12 +177,7 @@ async def get_characteristics(self, characteristics, include_meta=False, include
188177
if include_events:
189178
url += '&ev=1'
190179

191-
try:
192-
response = await self.connection.get_json(url)
193-
except (AccessoryDisconnectedError, EncryptionError):
194-
self.session.close()
195-
self.session = None
196-
raise
180+
response = await self.connection.get_json(url)
197181

198182
tmp = {}
199183
for c in response['characteristics']:
@@ -347,21 +331,10 @@ async def get_events(self, characteristics, callback_fun, max_events=-1, max_sec
347331
event_count = 0
348332
s = time.time()
349333
while (max_events == -1 or event_count < max_events) and (max_seconds == -1 or s + max_seconds >= time.time()):
350-
try:
351-
r = self.session.sec_http.handle_event_response()
352-
body = r.read().decode()
353-
except (AccessoryDisconnectedError, EncryptionError):
354-
self.session.close()
355-
self.session = None
356-
raise
334+
r = self.session.sec_http.handle_event_response()
335+
body = r.read().decode()
357336

358337
if len(body) > 0:
359-
try:
360-
r = json.loads(body)
361-
except JSONDecodeError:
362-
self.session.close()
363-
self.session = None
364-
raise AccessoryDisconnectedError("Session closed after receiving malformed response from device")
365338
tmp = []
366339
for c in r['characteristics']:
367340
tmp.append((c['aid'], c['iid'], c['value']))

tests/aio/test_ip_pairing.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import pytest
77

88
from homekit import AccessoryServer
9+
from homekit.exceptions import AccessoryDisconnectedError
910
from homekit.model import Accessory
1011
from homekit.model.services import LightBulbService
1112
from homekit.model import mixin as model_mixin
@@ -111,6 +112,17 @@ async def test_get_characteristics_after_failure(pairing):
111112

112113
pairing.connection.transport.close()
113114

115+
# The connection is closed but the reconnection mechanism hasn't kicked in yet.
116+
# Attempts to use the connection should fail.
117+
with pytest.raises(AccessoryDisconnectedError):
118+
characteristics = await pairing.get_characteristics([
119+
(1, 10),
120+
])
121+
122+
# We can't await a close - this lets the coroutine fall into the 'reactor'
123+
# and process queued work which will include the real transport.close work.
124+
await asyncio.sleep(0)
125+
114126
characteristics = await pairing.get_characteristics([
115127
(1, 10),
116128
])

0 commit comments

Comments
 (0)