Repository: ikalchev/HAP-python

Issue:
Do not pass `loop` to asyncio.Event
Passing the loop argument to constructors is deprecated in Python 3.8 and will be removed in Python 3.10. You can trigger this as an error now by running the test suite with error on warning:

```
py.test tests/ -Werror -x
```

Making this change would require to remove all passing of / creation of loop in the accessory.

I tried to do this quickly but it means that the accessory needs to be instantiated from inside the event loop, which is not happening inside the tests.

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/ikalchev/HAP-python/pull/328?src=pr&el=h1) Report
> Merging [#328](https://codecov.io/gh/ikalchev/HAP-python/pull/328?src=pr&el=desc) (ff803a3) into [dev](https://codecov.io/gh/ikalchev/HAP-python/commit/e8920d8450039e30bdaf0a4298405574f532018f?el=desc) (e8920d8) will **decrease** coverage by `0.12%`.
> The diff coverage is `81.25%`.


```diff
@@            Coverage Diff             @@
##              dev     #328      +/-   ##
==========================================
- Coverage   86.09%   85.96%   -0.13%     
==========================================
  Files          19       19              
  Lines        1848     1853       +5     
  Branches      219      219              
==========================================
+ Hits         1591     1593       +2     
- Misses        203      208       +5     
+ Partials       54       52       -2     
```

| [Impacted Files](https://codecov.io/gh/ikalchev/HAP-python/pull/328?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pyhap/accessory\_driver.py](https://codecov.io/gh/ikalchev/HAP-python/pull/328/diff?src=pr&el=tree#diff-cHloYXAvYWNjZXNzb3J5X2RyaXZlci5weQ==) | `86.91% <81.25%> (+0.78%)` | :arrow_up: |
| [pyhap/hap\_server.py](https://codecov.io/gh/ikalchev/HAP-python/pull/328/diff?src=pr&el=tree#diff-cHloYXAvaGFwX3NlcnZlci5weQ==) | `79.31% <0.00%> (-17.25%)` | :arrow_down: |

bdraco: The removed coverage is restored in https://github.com/ikalchev/HAP-python/pull/320 (was adjusted to merge cleanly)

Patch:
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 9c730b0..6113907 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -9,7 +9,7 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        python-version: [3.5, 3.6, 3.7, 3.8]
+        python-version: [3.5, 3.6, 3.7, 3.8, 3.9, "3.10.0-alpha.5"]
 
     steps:
     - uses: actions/checkout@v1
@@ -35,7 +35,7 @@ jobs:
 
     strategy:
       matrix:
-        python-version: [3.7]
+        python-version: [3.9]
 
     steps:
     - uses: actions/checkout@v1
diff --git a/README.md b/README.md
index 8fa5262..fcca1fd 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-[![PyPI version](https://badge.fury.io/py/HAP-python.svg)](https://badge.fury.io/py/HAP-python) [![Build Status](https://github.com/ikalchev/HAP-python/workflows/CI/badge.svg)](https://github.com/ikalchev/HAP-python) [![codecov](https://codecov.io/gh/ikalchev/HAP-python/branch/master/graph/badge.svg)](https://codecov.io/gh/ikalchev/HAP-python) [![Documentation Status](https://readthedocs.org/projects/hap-python/badge/?version=latest)](http://hap-python.readthedocs.io/en/latest/?badge=latest) [![Downloads](https://pepy.tech/badge/hap-python)](https://pepy.tech/project/hap-python)
+[![PyPI version](https://badge.fury.io/py/HAP-python.svg)](https://badge.fury.io/py/HAP-python) [![Build Status](https://github.com/ikalchev/HAP-python/workflows/CI/badge.svg)](https://github.com/ikalchev/HAP-python) [![codecov](https://codecov.io/gh/ikalchev/HAP-python/branch/master/graph/badge.svg)](https://codecov.io/gh/ikalchev/HAP-python) [![Python Versions](https://img.shields.io/pypi/pyversions/HAP-python.svg)](https://pypi.python.org/pypi/HAP-python/) [![Documentation Status](https://readthedocs.org/projects/hap-python/badge/?version=latest)](http://hap-python.readthedocs.io/en/latest/?badge=latest) [![Downloads](https://pepy.tech/badge/hap-python)](https://pepy.tech/project/hap-python)
 # HAP-python
 
 HomeKit Accessory Protocol implementation in python 3.
diff --git a/pyhap/accessory_driver.py b/pyhap/accessory_driver.py
index 3809fd6..ea93713 100644
--- a/pyhap/accessory_driver.py
+++ b/pyhap/accessory_driver.py
@@ -231,7 +231,7 @@ class AccessoryDriver:
         self.encoder = encoder or AccessoryEncoder()
         self.topics = {}  # topic: set of (address, port) of subscribed clients
         self.loader = loader or Loader()
-        self.aio_stop_event = asyncio.Event(loop=loop)
+        self.aio_stop_event = None
         self.stop_event = threading.Event()
 
         self.safe_mode = False
@@ -266,7 +266,7 @@ class AccessoryDriver:
                     "Not setting a child watcher. Set one if "
                     "subprocesses will be started outside the main thread."
                 )
-            self.add_job(self.start_service)
+            self.add_job(self.async_start())
             self.loop.run_forever()
         except KeyboardInterrupt:
             logger.debug("Got a KeyboardInterrupt, stopping driver")
@@ -277,6 +277,19 @@ class AccessoryDriver:
             logger.info("Closed the event loop")
 
     def start_service(self):
+        """Start the service."""
+        self._validate_start()
+        self.add_job(self.async_start)
+
+    def _validate_start(self):
+        """Validate we can start."""
+        if self.accessory is None:
+            raise ValueError(
+                "You must assign an accessory to the driver, "
+                "before you can start it."
+            )
+
+    async def async_start(self):
         """Starts the accessory.
 
         - Call the accessory's run method.
@@ -288,11 +301,9 @@ class AccessoryDriver:
         All of the above are started in separate threads. Accessory thread is set as
         daemon.
         """
-        if self.accessory is None:
-            raise ValueError(
-                "You must assign an accessory to the driver, "
-                "before you can start it."
-            )
+        self._validate_start()
+        self.aio_stop_event = asyncio.Event()
+
         logger.info(
             "Starting accessory %s on address %s, port %s.",
             self.accessory.display_name,
@@ -302,12 +313,14 @@ class AccessoryDriver:
 
         # Start listening for requests
         logger.debug("Starting server.")
-        self.add_job(self.http_server.async_start, self.loop)
+        await self.http_server.async_start(self.loop)
 
         # Advertise the accessory as a mDNS service.
         logger.debug("Starting mDNS.")
         self.mdns_service_info = AccessoryMDNSServiceInfo(self.accessory, self.state)
-        self.advertiser.register_service(self.mdns_service_info)
+        await self.loop.run_in_executor(
+            None, self.advertiser.register_service, self.mdns_service_info
+        )
 
         # Print accessory setup message
         if not self.state.paired:
diff --git a/pyhap/hsrp.py b/pyhap/hsrp.py
index 0bf7548..4c4dcb5 100644
--- a/pyhap/hsrp.py
+++ b/pyhap/hsrp.py
@@ -1,126 +1,112 @@
-# An incomplete implementation of SRP (i.e. the server side of SRP).
-# I remember there was a problem with an srp module that I used
-# as a guideline.
-# TODO: make it a complete implementation.
+# Server Side SRP implementation
+
 import os
 from .util import long_to_bytes
 
-#
-# s - bytes
-# x - int
-# k - int
-# K - int
-# S - int
-# u - bytes
-# p - bytes
-
-
-def padN(bytestr, ctx):
-    return bytestr.rjust(ctx["N_len"] // 8, b'\x00')
-
-
-def _bytes_to_long(s):
-    n = ord(s[0])
-    for b in (ord(x) for x in s[1:]):
-        n = (n << 8) | b
-    return n
-
 
 def bytes_to_long(s):
     # Bytes should be interpreted from left to right, hence the byteorder
     return int.from_bytes(s, byteorder="big")
 
 
-def get_x(u, p, s, ctx):
-    hf = ctx["hashfunc"]()
-    hf.update(u + b":" + p)
-    up = hf.digest()
-    hf = ctx["hashfunc"]()
-    hf.update(s + up)
-    return int(hf.hexdigest(), 16)
+# b    Secret ephemeral values (long)
+# A    Public ephemeral values (long)
+# Ab   Public ephemeral values (bytes)
+# B    Public ephemeral values (long)
+# Bb   Public ephemeral values (bytes)
+# g    A generator modulo N (long)
+# gb   A generator modulo N (bytes)
+# I    Username (bytes)
+# k    Multiplier parameter (long)
+# N    Large safe prime (long)
+# Nb   Large safe prime (bytes)
+# p    Cleartext Password (bytes)
+# s    Salt (bytes)
+# u    Random scrambling parameter (bytes)
+# v    Password verifier (long)
+
+
+class Server:
+    def __init__(self, ctx, u, p, s=None, v=None, b=None):
+        self.hashfunc = ctx["hashfunc"]
+        self.N = ctx["N"]
+        self.Nb = long_to_bytes(self.N)
+        self.g = ctx["g"]
+        self.gb = long_to_bytes(self.g)
+        self.N_len = ctx["N_len"]
+        self.s = s or os.urandom(ctx["salt_len"])
+        self.I = u  # noqa: E741
+        self.p = p
+        self.v = v or self._get_verifier()
+        self.k = self._get_k()
+        self.b = b or bytes_to_long(os.urandom(ctx["secret_len"]))
+        self.B = self._derive_B()
+        self.Bb = long_to_bytes(self.B)
+
+        self.Ab = None
+        self.A = None
+        self.S = None
+        self.Sb = None
+        self.K = None
+        self.Kb = None
+        self.M = None
+        self.u = None
+        self.HAMK = None
 
+    def _digest(self, data):
+        return self.hashfunc(data).digest()
 
-def get_verifier(u, p, s, ctx):
-    x = get_x(u, p, s, ctx)
-    return pow(ctx['g'], x, ctx['N'])
+    def _hexdigest_int16(self, data):
+        return int(self.hashfunc(data).hexdigest(), 16)
 
+    def _derive_B(self):
+        return (self.k * self.v + pow(self.g, self.b, self.N)) % self.N
 
-def get_k(ctx):
-    hf = ctx["hashfunc"]()
-    hf.update(long_to_bytes(ctx["N"]) + padN(long_to_bytes(ctx["g"]), ctx))
-    return int(hf.hexdigest(), 16)
+    def _get_private_key(self):
+        return self._hexdigest_int16(self.s + self._digest(self.I + b":" + self.p))
 
+    def _get_verifier(self):
+        return pow(self.g, self._get_private_key(), self.N)
 
-def get_session_key(S, ctx):
-    hf = ctx['hashfunc']()
-    hf.update(long_to_bytes(S))
-    return int(hf.hexdigest(), 16)
+    def _get_k(self):
+        return self._hexdigest_int16(self.Nb + self._padN(self.gb))
 
+    def _get_K(self):
+        return self._hexdigest_int16(self.Sb)
 
-class Server():
+    def _padN(self, bytestr):
+        return bytestr.rjust(self.N_len // 8, b"\x00")
 
-    def __init__(self, ctx, u, p, s=None, v=None):
-        self.ctx = ctx
-        self.u = u
-        self.p = p
-        self.s = s or os.urandom(self.ctx["salt_len"])
-        self.v = v or get_verifier(u, p, self.s, self.ctx)
-        self.k = get_k(ctx)
-        self.b = bytes_to_long(os.urandom(256))  # TODO: specify length
-        self.B = self.derive_B()
-        self.A = None
-        self.S = None
-        self.K = None
-        self.M = None
-        self.HAMK = None
+    def _derive_premaster_secret(self):
+        self.u = self._hexdigest_int16(self._padN(self.Ab) + self._padN(self.Bb))
+        Avu = self.A * pow(self.v, self.u, self.N)
+        return pow(Avu, self.b, self.N)
 
-    def derive_B(self):
-        return (self.k * self.v + pow(self.ctx["g"], self.b, self.ctx["N"])) \
-            % self.ctx["N"]
+    def _get_M(self):
+        hN = self._digest(self.Nb)
+        hG = self._digest(self.gb)
+        hGroup = bytes(hN[i] ^ hG[i] for i in range(0, len(hN)))
+        hU = self._digest(self.I)
+        return self._digest(hGroup + hU + self.s + self.Ab + self.Bb + self.Kb)
 
     def set_A(self, bytes_A):
-        self.A = int.from_bytes(bytes_A, byteorder="big")
-        self.S = self.derive_premaster_secret()
-        self.K = get_session_key(self.S, self.ctx)
-        self.M = self.get_M()
+        self.A = bytes_to_long(bytes_A)
+        self.Ab = bytes_A
+        self.S = self._derive_premaster_secret()
+        self.Sb = long_to_bytes(self.S)
+        self.K = self._get_K()
+        self.Kb = long_to_bytes(self.K)
+        self.M = self._get_M()
+        self.HAMK = self._get_HAMK()
+
+    def _get_HAMK(self):
+        return self._digest(self.Ab + self.M + self.Kb)
 
     def get_challenge(self):
         return (self.s, self.B)
 
-    def derive_premaster_secret(self):
-        hf = self.ctx['hashfunc']()
-        hf.update(padN(long_to_bytes(self.A), self.ctx) +
-                  padN(long_to_bytes(self.B), self.ctx))
-        U = int(hf.hexdigest(), 16)
-        Avu = self.A * pow(self.v, U, self.ctx["N"])
-        return pow(Avu, self.b, self.ctx["N"])
-
-    def get_M(self):
-        hf = self.ctx['hashfunc']()
-        hf.update(long_to_bytes(self.ctx['N']))
-        hN = hf.digest()
-        hf = self.ctx['hashfunc']()
-        hf.update(long_to_bytes(self.ctx['g']))
-        hG = hf.digest()
-        hGroup = bytes(hN[i] ^ hG[i] for i in range(0, len(hN)))
-        hf = self.ctx['hashfunc']()
-        hf.update(self.u)
-        hU = hf.digest()
-        hf = self.ctx['hashfunc']()
-        hf.update(hGroup + hU + self.s + long_to_bytes(self.A) +
-                  long_to_bytes(self.B) + long_to_bytes(self.K))
-        return hf.digest()
-
     def verify(self, M):
-        if self.M != M:
-            return None
-        self.HAMK = self.get_HAMK()
-        return self.HAMK
-
-    def get_HAMK(self):
-        hf = self.ctx['hashfunc']()
-        hf.update(long_to_bytes(self.A) + self.M + long_to_bytes(self.K))
-        return hf.digest()
+        return self.HAMK if self.M == M else None
 
     def get_session_key(self):
         return self.K
diff --git a/pyhap/params.py b/pyhap/params.py
index 9d76ca7..d06a9d7 100644
--- a/pyhap/params.py
+++ b/pyhap/params.py
@@ -1,24 +1,10 @@
 # hsrp parameters
-ng_order = (1024, 2048, 3072, 4096, 8192)
+ng_order = (3072,)
 
 _ng_const = (
-    # 1024-bit
-    ('''\
-EEAF0AB9ADB38DD69C33F80AFA8FC5E86072618775FF3C0B9EA2314C9C256576D674DF7496\
-EA81D3383B4813D692C6E0E0D5D8E250B98BE48E495C1D6089DAD15DC7D7B46154D6B6CE8E\
-F4AD69B15D4982559B297BCF1885C529F566660E57EC68EDBC3C05726CC02FD4CBF4976EAA\
-9AFD5138FE8376435B9FC61D2FC0EB06E3''', "2"),
-    # 2048
-    ('''\
-AC6BDB41324A9A9BF166DE5E1389582FAF72B6651987EE07FC3192943DB56050A37329CBB4\
-A099ED8193E0757767A13DD52312AB4B03310DCD7F48A9DA04FD50E8083969EDB767B0CF60\
-95179A163AB3661A05FBD5FAAAE82918A9962F0B93B855F97993EC975EEAA80D740ADBF4FF\
-747359D041D5C33EA71D281E446B14773BCA97B43A23FB801676BD207A436C6481F1D2B907\
-8717461A5B9D32E688F87748544523B524B0D57D5EA77A2775D2ECFA032CFBDBF52FB37861\
-60279004E57AE6AF874E7303CE53299CCC041C7BC308D82A5698F3A8D0C38271AE35F8E9DB\
-FBB694B5C803D89F7AE435DE236D525F54759B65E372FCD68EF20FA7111F9E4AFF73''', "2"),
     # 3072
-    ('''\
+    (
+        """\
 FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E08\
 8A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B\
 302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9\
@@ -32,79 +18,22 @@ FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D\
 B3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D226\
 1AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200C\
 BBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFC\
-E0FD108E4B82D120A93AD2CAFFFFFFFFFFFFFFFF''', "5"),
-    # 4096
-    ('''\
-FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E08\
-8A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B\
-302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9\
-A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE6\
-49286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8\
-FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D\
-670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C\
-180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF695581718\
-3995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D\
-04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7D\
-B3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D226\
-1AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200C\
-BBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFC\
-E0FD108E4B82D120A92108011A723C12A787E6D788719A10BDBA5B26\
-99C327186AF4E23C1A946834B6150BDA2583E9CA2AD44CE8DBBBC2DB\
-04DE8EF92E8EFC141FBECAA6287C59474E6BC05D99B2964FA090C3A2\
-233BA186515BE7ED1F612970CEE2D7AFB81BDD762170481CD0069127\
-D5B05AA993B4EA988D8FDDC186FFB7DC90A6C08F4DF435C934063199\
-FFFFFFFFFFFFFFFF''', "5"),
-    # 8192
-    ('''\
-FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E08\
-8A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B\
-302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9\
-A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE6\
-49286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8\
-FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D\
-670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C\
-180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF695581718\
-3995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D\
-04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7D\
-B3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D226\
-1AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200C\
-BBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFC\
-E0FD108E4B82D120A92108011A723C12A787E6D788719A10BDBA5B26\
-99C327186AF4E23C1A946834B6150BDA2583E9CA2AD44CE8DBBBC2DB\
-04DE8EF92E8EFC141FBECAA6287C59474E6BC05D99B2964FA090C3A2\
-233BA186515BE7ED1F612970CEE2D7AFB81BDD762170481CD0069127\
-D5B05AA993B4EA988D8FDDC186FFB7DC90A6C08F4DF435C934028492\
-36C3FAB4D27C7026C1D4DCB2602646DEC9751E763DBA37BDF8FF9406\
-AD9E530EE5DB382F413001AEB06A53ED9027D831179727B0865A8918\
-DA3EDBEBCF9B14ED44CE6CBACED4BB1BDB7F1447E6CC254B33205151\
-2BD7AF426FB8F401378CD2BF5983CA01C64B92ECF032EA15D1721D03\
-F482D7CE6E74FEF6D55E702F46980C82B5A84031900B1C9E59E7C97F\
-BEC7E8F323A97A7E36CC88BE0F1D45B7FF585AC54BD407B22B4154AA\
-CC8F6D7EBF48E1D814CC5ED20F8037E0A79715EEF29BE32806A1D58B\
-B7C5DA76F550AA3D8A1FBFF0EB19CCB1A313D55CDA56C9EC2EF29632\
-387FE8D76E3C0468043E8F663F4860EE12BF2D5B0B7474D6E694F91E\
-6DBE115974A3926F12FEE5E438777CB6A932DF8CD8BEC4D073B931BA\
-3BC832B68D9DD300741FA7BF8AFC47ED2576F6936BA424663AAB639C\
-5AE4F5683423B4742BF1C978238F16CBE39D652DE3FDB8BEFC848AD9\
-22222E04A4037C0713EB57A81A23F0C73473FC646CEA306B4BCBC886\
-2F8385DDFA9D4B7FA2C087E879683303ED5BDD3A062B3CF5B3A278A6\
-6D2A13F83F44F82DDF310EE074AB6A364597E899A0255DC164F31CC5\
-0846851DF9AB48195DED7EA1B1D510BD7EE74D73FAF36BC31ECFA268\
-359046F4EB879F924009438B481C6CD7889A002ED5EE382BC9190DA6\
-FC026E479558E4475677E9AA9E3050E2765694DFC81F56E880B96E71\
-60C980DD98EDD3DFFFFFFFFFFFFFFFFF''', '0x13')
+E0FD108E4B82D120A93AD2CAFFFFFFFFFFFFFFFF""",
+        "5",
+    ),
 )
 
 
-def get_srp_context(ng_group_len, hashfunc, salt_len=16):
+def get_srp_context(ng_group_len, hashfunc, salt_len=16, secret_len=32):
 
     group = _ng_const[ng_order.index(ng_group_len)]
 
     ctx = {
-        'hashfunc': hashfunc,
-        'N': int(group[0], 16),
-        'g': int(group[1], 16),
-        'N_len': ng_group_len,
-        'salt_len': salt_len
+        "hashfunc": hashfunc,
+        "N": int(group[0], 16),
+        "g": int(group[1], 16),
+        "N_len": ng_group_len,
+        "salt_len": salt_len,
+        "secret_len": secret_len,
     }
     return ctx
diff --git a/pyhap/util.py b/pyhap/util.py
index dad1e57..630bd3b 100644
--- a/pyhap/util.py
+++ b/pyhap/util.py
@@ -102,7 +102,7 @@ def byte_bool(boolv):
     return b"\x01" if boolv else b"\x00"
 
 
-async def event_wait(event, timeout, loop=None):
+async def event_wait(event, timeout):
     """Wait for the given event to be set or for the timeout to expire.
 
     :param event: The event to wait for.
@@ -115,7 +115,7 @@ async def event_wait(event, timeout, loop=None):
     :rtype: bool
     """
     try:
-        await asyncio.wait_for(event.wait(), timeout, loop=loop)
+        await asyncio.wait_for(event.wait(), timeout)
     except asyncio.TimeoutError:
         pass
     return event.is_set()
diff --git a/pyproject.toml b/pyproject.toml
index 6a97c07..64a572d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,5 +1,5 @@
 [tool.black]
-target-version = ["py37", "py38"]
+target-version = ["py35", "py36", "py37", "py38"]
 exclude = 'generated'
 
 [tool.isort]
diff --git a/setup.py b/setup.py
index 5120612..919855b 100644
--- a/setup.py
+++ b/setup.py
@@ -56,6 +56,9 @@ setup(
         'Programming Language :: Python :: 3.5',
         'Programming Language :: Python :: 3.6',
         'Programming Language :: Python :: 3.7',
+        'Programming Language :: Python :: 3.8',
+        'Programming Language :: Python :: 3.9',
+        'Programming Language :: Python :: 3.10',
         'Topic :: Home Automation',
         'Topic :: Software Development :: Libraries :: Python Modules',
     ],
diff --git a/tox.ini b/tox.ini
index bfd8914..4b613c2 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
 [tox]
-envlist = py35, py36, py37, py38, docs, lint, pylint
+envlist = py35, py36, py37, py38, py39, py310, docs, lint, pylint
 skip_missing_interpreters = True
 
 [gh-actions]
@@ -8,6 +8,8 @@ python =
     3.6: py36
     3.7: py37
     3.8: py38, mypy
+    3.9: py39, mypy
+    3.10: py310, mypy
 
 [testenv]
 deps =


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index 50fe598..dc809c4 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -8,6 +8,8 @@ import pytest
 from pyhap.accessory_driver import AccessoryDriver
 from pyhap.loader import Loader
 
+from . import AsyncMock
+
 
 @pytest.fixture(scope="session")
 def mock_driver():
@@ -21,9 +23,15 @@ def driver():
     except RuntimeError:
         loop = asyncio.new_event_loop()
         asyncio.set_event_loop(loop)
-    with patch("pyhap.accessory_driver.HAPServer"), patch(
+    with patch(
+        "pyhap.accessory_driver.HAPServer.async_stop", new_callable=AsyncMock
+    ), patch(
+        "pyhap.accessory_driver.HAPServer.async_start", new_callable=AsyncMock
+    ), patch(
         "pyhap.accessory_driver.Zeroconf"
-    ), patch("pyhap.accessory_driver.AccessoryDriver.persist"):
+    ), patch(
+        "pyhap.accessory_driver.AccessoryDriver.persist"
+    ):
 
         yield AccessoryDriver(loop=loop)
 
diff --git a/tests/test_accessory_driver.py b/tests/test_accessory_driver.py
index fef06d2..239f3d4 100644
--- a/tests/test_accessory_driver.py
+++ b/tests/test_accessory_driver.py
@@ -1,9 +1,9 @@
 """Tests for pyhap.accessory_driver."""
 import asyncio
+from concurrent.futures import ThreadPoolExecutor
 import tempfile
 from unittest.mock import MagicMock, patch
 from uuid import uuid1
-from concurrent.futures import ThreadPoolExecutor
 
 import pytest
 
@@ -30,6 +30,8 @@ from pyhap.const import (
 from pyhap.service import Service
 from pyhap.state import State
 
+from . import AsyncMock
+
 CHAR_PROPS = {
     PROP_FORMAT: HAP_FORMAT_INT,
     PROP_PERMISSIONS: HAP_PERMISSION_READ,
@@ -402,9 +404,15 @@ def test_start_from_sync(driver):
 
 @pytest.mark.asyncio
 async def test_start_stop_sync_acc():
-    with patch("pyhap.accessory_driver.HAPServer"), patch(
+    with patch(
+        "pyhap.accessory_driver.HAPServer.async_stop", new_callable=AsyncMock
+    ), patch(
+        "pyhap.accessory_driver.HAPServer.async_start", new_callable=AsyncMock
+    ), patch(
         "pyhap.accessory_driver.Zeroconf"
-    ), patch("pyhap.accessory_driver.AccessoryDriver.persist"), patch(
+    ), patch(
+        "pyhap.accessory_driver.AccessoryDriver.persist"
+    ), patch(
         "pyhap.accessory_driver.AccessoryDriver.load"
     ):
         driver = AccessoryDriver(loop=asyncio.get_event_loop())
@@ -430,9 +438,15 @@ async def test_start_stop_sync_acc():
 @pytest.mark.asyncio
 async def test_start_stop_async_acc():
     """Verify run_at_interval closes the driver."""
-    with patch("pyhap.accessory_driver.HAPServer"), patch(
+    with patch(
+        "pyhap.accessory_driver.HAPServer.async_stop", new_callable=AsyncMock
+    ), patch(
+        "pyhap.accessory_driver.HAPServer.async_start", new_callable=AsyncMock
+    ), patch(
         "pyhap.accessory_driver.Zeroconf"
-    ), patch("pyhap.accessory_driver.AccessoryDriver.persist"), patch(
+    ), patch(
+        "pyhap.accessory_driver.AccessoryDriver.persist"
+    ), patch(
         "pyhap.accessory_driver.AccessoryDriver.load"
     ):
         driver = AccessoryDriver(loop=asyncio.get_event_loop())
@@ -464,6 +478,7 @@ def test_start_without_accessory(driver):
 
 def test_send_events(driver):
     """Test we can send events."""
+    driver.aio_stop_event = MagicMock(is_set=MagicMock(return_value=False))
 
     class LoopMock:
         runcount = 0
@@ -542,15 +557,21 @@ def test_mdns_service_info(driver):
 @pytest.mark.asyncio
 async def test_start_service_and_update_config():
     """Test starting service and updating the config."""
-    with patch("pyhap.accessory_driver.HAPServer"), patch(
+    with patch(
+        "pyhap.accessory_driver.HAPServer.async_stop", new_callable=AsyncMock
+    ), patch(
+        "pyhap.accessory_driver.HAPServer.async_start", new_callable=AsyncMock
+    ), patch(
         "pyhap.accessory_driver.Zeroconf"
-    ), patch("pyhap.accessory_driver.AccessoryDriver.persist"), patch(
+    ), patch(
+        "pyhap.accessory_driver.AccessoryDriver.persist"
+    ), patch(
         "pyhap.accessory_driver.AccessoryDriver.load"
     ):
         driver = AccessoryDriver(loop=asyncio.get_event_loop())
         acc = Accessory(driver, "TestAcc")
         driver.add_accessory(acc)
-        driver.start_service()
+        await driver.async_start()
 
         assert driver.state.config_version == 2
         driver.config_changed()
@@ -558,7 +579,8 @@ async def test_start_service_and_update_config():
         driver.state.config_version = 65535
         driver.config_changed()
         assert driver.state.config_version == 1
-
+        for _ in range(3):
+            await asyncio.sleep(0)
         await driver.async_stop()
         await asyncio.sleep(0)
         assert not driver.loop.is_closed()
diff --git a/tests/test_hsrp.py b/tests/test_hsrp.py
new file mode 100644
index 0000000..faec9cc
--- /dev/null
+++ b/tests/test_hsrp.py
@@ -0,0 +1,65 @@
+"""Tests for pyhap.hsrp."""
+
+# pylint: disable=line-too-long, pointless-string-statement
+
+import hashlib
+from pyhap.hsrp import Server
+from pyhap.params import get_srp_context
+from pyhap.util import long_to_bytes
+
+DUMMY_A = b"Ve\xce\xd4\x90LExKD\x9d7\x16\\@\xb6\xb8\x9f\x01\x1a]\x86\xa4\x1c"
+" \x13\xaa\xc0\x17=\x1f\xafPx\xea/\x01Q\xc8hw\x06\x03\xc8O\x89|\x8d4\xa8\x85"
+"\xd2\xfb:\x0e\xb6PT2V\xb2\xa9\xca\x0bL\x97\r\xee\x88\xbc\xef\x8d\xa6|\xeb \xdc"
+"\x80.\x92\xe0\xe5s\xf5\xf2;\x89LN\\^\x8c\xd1\x00\x99U]]/^\xe9\x1b\xe2\xf3\x1a|"
+"\xc6\x85Q\x95T`b\x8e\x04\xc2\x99\xdd\xdfp\x98\x85\x13\xe5\xaf\xdf\xe0Tm\xa3t\xfe"
+"\xc1_V\x04\xab\xb1\x96\xa8\x9cw\xa40\x95\x8d\x9f|\xf7.\x90\xd2{L\xcc*\xcb\xdde"
+"\x81\x14\x14\xc97\xe7\xa0177\x1b\xe0\xb0\x19\x0f\xf1\x1e;\xc4\xc9\x07\x05zN\xb3"
+"!y\xf2\x9e\xa4N\xbeswxx\x13\x82\x18\xccU\xb4\xec\x7f{\x8eo\x86\x0b\xa6\xff\x9b"
+"\xbcY(0\x16\xba$\x9d\xb9\x8d}\xe5f\x0c)\\\x8b\\\xef\xfd\x0coEg\x13\x13\xa2q\xb9"
+"\xe5\x8a\xfd\x97\x97\xcb\xb1\x15\xd5\xc2\xd7\x07\x91A\xdf\xd7"
+
+
+def test_srp_basic():
+
+    ctx = get_srp_context(3072, hashlib.sha512, 16)
+    b = 191304991611724068381190663629083136274
+    s = long_to_bytes(227710976386754876301088769828140156049)
+    verifier = Server(ctx, b"Pair-Setup", b"123-45-543", s=s, b=b)
+    verifier.set_A(DUMMY_A)
+
+    assert (
+        verifier.k
+        == 8891118944006259431156568541843809053371474718154946070525699599564743247786811275097952247025117806925219847643897478119979876683245412022290811230509536
+    )
+    assert (
+        verifier.get_session_key()
+        == 7776966363435436003301596680621751479448170893927097125414524508260409807602643597201957531811064094375727460485526402929080964822225092649470633176208468
+    )
+    assert (
+        verifier.M
+        == b"\xafnZ\xef\x8e\x84\xbe\xaa\xe2M}5'\x0c\xb8\xb9\x07\x13\xa3t\xbbfOL\x059\xa3T\xaf\x021\x05\xf7*\xdb]\xa3]\x92\xbc\xa7\x0ed\xc1C\x88W\x0b\xe7n\xe6|\x1e\xb4\xf9pUc\xa2\x8d\x05\xd7\xabI"
+    )
+    assert (
+        verifier.S
+        == 74327940101639752536537640881643581886247890122995727869092918508085397047960192114187184206420245499227933354038262980545757154896143196917567791395562849790585173129051928488506985432588320936161016609993624725221069849383124728580710793131421162926844621384309691065416908669855286020750380619018007734494245389837285359061649585082978114606737696983003789452193299203880220013003551748645087934186574940836315605161763958706985646740794424371115818479937015467439653789667600114913036877616558029128521276071759153575011083182650027094873442901697309464533625147028860476977419766721379872518101123122550406587162809198793634217353529574423908555799363233330194347012490634061830786590780000201696990820985363093141614397601285773980430681705777477946555312165250133963931282621724675380164859592461132141730419315498467050491890312826221069184134326282895963295397215898192608240385050625017941322853973472354023693355
+    )
+    assert verifier.get_challenge() == (
+        s,
+        2149981971605054722971448928513305504744266471818820776094113337432031877014471028912971746321748621185649001880451734094103311676264091997241948096711710461140721738956497494552388614895831596671069609694220554015991913746528757304239759620571367574036184864989138266792823575841594621160010011666017298902208272126405229578664943728094068949021795802799552486045670159066273942547651088762352104942364707580142387716636468281068738042936130578774565386637668610429058884417819388838110075674266297699354845325023954873162742733169560666501210723876454859556564325607870517213063038111644227553599978606540729093082921723443122696487068510228710655880466038292327450357013882323502992655150615829432843408599038481983277372215619348128412279375677793332715557041679298014663382481619951610899087031959653365603032111634191603851554865349816117884573658915813848292512124719015181912892538210471183790840676306564839828444134,
+    )
+    assert verifier.b == b
+    assert (
+        verifier.v
+        == 1800954445588585461785592179273284825501707649217210015435034845050179016324355419526711292364866248346582448660643272322280999760562622718989053886869428917425675795172391329924178337579968214001782222575897907780437717763112406095878356902641567396545009429496128133564692965499069074320017151157469160990771527712530637370897276672652870613312504255873634362188551282649472569433062597795005057270622772410668342950279555516133010272639201733492626622809480021268951287298118968011031850511105359580984350020671780470982743318615303989055956125558514263378948829479434245711743458681522240763520911255733079164391662778946744155477806679057949726211652108387739564473209550264487697151825509058193841809273482575660658239177704074882302955007248950743262054925817705066654613816236610736311934089570249355454459951577900707115340781119430461780455828980205046091360390327787803271426555681638302650021637121212829077894589
+    )
+    assert (
+        verifier.N
+        == 5809605995369958062791915965639201402176612226902900533702900882779736177890990861472094774477339581147373410185646378328043729800750470098210924487866935059164371588168047540943981644516632755067501626434556398193186628990071248660819361205119793693985433297036118232914410171876807536457391277857011849897410207519105333355801121109356897459426271845471397952675959440793493071628394122780510124618488232602464649876850458861245784240929258426287699705312584509625419513463605155428017165714465363094021609290561084025893662561222573202082865797821865270991145082200656978177192827024538990239969175546190770645685893438011714430426409338676314743571154537142031573004276428701433036381801705308659830751190352946025482059931306571004727362479688415574702596946457770284148435989129632853918392117997472632693078113129886487399347796982772784615865232621289656944284216824611318709764535152507354116344703769998514148343807
+    )
+    assert verifier.g == 5
+
+    assert (
+        verifier.verify(verifier.M)
+        == b"\xe1\x00\xcf\xe2\x98\xaf\x1e\x02tb\x0b\xfclKF\xee\x1b\x80\xf6\x90\xb7\x8a\x9f\x133y#>\x8d/\xc1\x88\x93\x8eh\tN\x9b\xda\xc2-\x1a(\xe3\xca\x0bf\xf3\xc4\xca\xc4\xec\xfa/\xec\xb7\x16\x81\xdd%\xc9i\xf9\x90"
+    )
+    assert verifier.verify(b"wrong") is None