diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 579fa74b..40fbfe84 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.6, 3.7, 3.8, 3.9] + python-version: [3.7, 3.8, 3.9, "3.10"] steps: - uses: actions/checkout@v1 diff --git a/CHANGELOG.md b/CHANGELOG.md index fcc79a69..3dc67fda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,16 +15,25 @@ Sections ### Breaking Changes ### Developers --> + +## [4.5.0] - 2022-06-28 + +- Speed up "get accessories". [#418](https://github.com/ikalchev/HAP-python/pull/418) +- Increase minimum python version to 3.7. [#417](https://github.com/ikalchev/HAP-python/pull/417) +- Speed up encryption by using ChaCha20Poly1305Reusable. [#413](https://github.com/ikalchev/HAP-python/pull/413) +- Speed up serialization using orjson. [#412](https://github.com/ikalchev/HAP-python/pull/412) +- Avoid redundant parsing of the URL. [#402](https://github.com/ikalchev/HAP-python/pull/402) + ## [4.4.0] - 2022-11-01 ### Added -- Allow invalid client values when enabled. [#392](https://github.com/ikalchev/HAP- python/pull/392) +- Allow invalid client values when enabled. [#392](https://github.com/ikalchev/HAP-python/pull/392) ## [4.3.0] - 2021-10-07 ### Fixed - Only send the latest state in case of multiple events for the same characteristic. [#385](https://github.com/ikalchev/HAP-python/pull/385) -- Handle invalid formats from clients. [#387](https://github.com/ikalchev/HAP- python/pull/387) +- Handle invalid formats from clients. [#387](https://github.com/ikalchev/HAP-python/pull/387) ## [4.2.1] - 2021-09-06 diff --git a/docs/source/conf.py b/docs/source/conf.py index 297b15b0..c0b3be16 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -60,7 +60,7 @@ # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +#language = None # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -158,4 +158,4 @@ ] -# -- Extension configuration ------------------------------------------------- \ No newline at end of file +# -- Extension configuration ------------------------------------------------- diff --git a/pyhap/accessory.py b/pyhap/accessory.py index 083655e4..e8124fb7 100644 --- a/pyhap/accessory.py +++ b/pyhap/accessory.py @@ -245,7 +245,7 @@ def setup_message(self): ) else: print( - "To use the QR Code feature, use 'pip install " "HAP-python[QRCode]'", + "To use the QR Code feature, use 'pip install HAP-python[QRCode]'", flush=True, ) print( diff --git a/pyhap/camera.py b/pyhap/camera.py index c3b8e7a6..17002cfa 100644 --- a/pyhap/camera.py +++ b/pyhap/camera.py @@ -842,7 +842,7 @@ async def start_stream(self, session_info, stream_config): return True - async def stop_stream(self, session_info): # pylint: disable=no-self-use + async def stop_stream(self, session_info): """Stop the stream for the given ``session_id``. This method can be implemented if custom stop stream commands are needed. The @@ -886,7 +886,7 @@ async def reconfigure_stream(self, session_info, stream_config): """ await self.start_stream(session_info, stream_config) - def get_snapshot(self, image_size): # pylint: disable=unused-argument, no-self-use + def get_snapshot(self, image_size): # pylint: disable=unused-argument """Return a jpeg of a snapshot from the camera. Overwrite to implement getting snapshots from your camera. diff --git a/pyhap/characteristic.py b/pyhap/characteristic.py index 02f85675..85461b56 100644 --- a/pyhap/characteristic.py +++ b/pyhap/characteristic.py @@ -82,7 +82,7 @@ PROP_UNIT = "unit" PROP_VALID_VALUES = "ValidValues" -PROP_NUMERIC = (PROP_MAX_VALUE, PROP_MIN_VALUE, PROP_MIN_STEP, PROP_UNIT) +PROP_NUMERIC = {PROP_MAX_VALUE, PROP_MIN_VALUE, PROP_MIN_STEP, PROP_UNIT} CHAR_BUTTON_EVENT = UUID("00000126-0000-1000-8000-0026BB765291") CHAR_PROGRAMMABLE_SWITCH_EVENT = UUID("00000073-0000-1000-8000-0026BB765291") @@ -358,7 +358,10 @@ def to_HAP(self): value = self.get_value() if self.properties[PROP_FORMAT] in HAP_FORMAT_NUMERICS: hap_rep.update( - {k: self.properties[k] for k in self.properties.keys() & PROP_NUMERIC} + { + k: self.properties[k] + for k in PROP_NUMERIC.intersection(self.properties) + } ) if PROP_VALID_VALUES in self.properties: diff --git a/pyhap/const.py b/pyhap/const.py index b3509a06..582659dd 100644 --- a/pyhap/const.py +++ b/pyhap/const.py @@ -1,10 +1,10 @@ """This module contains constants used by other modules.""" MAJOR_VERSION = 4 -MINOR_VERSION = 4 +MINOR_VERSION = 5 PATCH_VERSION = 0 __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" -REQUIRED_PYTHON_VER = (3, 6) +REQUIRED_PYTHON_VER = (3, 7) BASE_UUID = "-0000-1000-8000-0026BB765291" diff --git a/pyhap/hap_crypto.py b/pyhap/hap_crypto.py index 60e39e16..1ca64644 100644 --- a/pyhap/hap_crypto.py +++ b/pyhap/hap_crypto.py @@ -2,9 +2,9 @@ import logging import struct +from chacha20poly1305_reuseable import ChaCha20Poly1305Reusable as ChaCha20Poly1305 from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes -from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 from cryptography.hazmat.primitives.kdf.hkdf import HKDF logger = logging.getLogger(__name__) diff --git a/pyhap/hap_handler.py b/pyhap/hap_handler.py index 22cd8e16..6f4d7aed 100644 --- a/pyhap/hap_handler.py +++ b/pyhap/hap_handler.py @@ -4,7 +4,6 @@ """ import asyncio from http import HTTPStatus -import json import logging from urllib.parse import parse_qs, urlparse import uuid @@ -12,7 +11,7 @@ from cryptography.exceptions import InvalidSignature, InvalidTag from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import ed25519, x25519 -from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 +from chacha20poly1305_reuseable import ChaCha20Poly1305Reusable as ChaCha20Poly1305 from pyhap import tlv from pyhap.const import ( @@ -25,7 +24,7 @@ from pyhap.util import long_to_bytes from .hap_crypto import hap_hkdf, pad_tls_nonce -from .util import to_hap_json +from .util import to_hap_json, from_hap_json # iOS will terminate the connection if it does not respond within # 10 seconds, so we only allow 9 seconds to avoid this. @@ -145,6 +144,7 @@ def __init__(self, accessory_handler, client_address): self.command = None self.headers = None self.request_body = None + self.parsed_url = None self.response = None @@ -199,6 +199,7 @@ def dispatch(self, request, body=None): self.command = request.method.decode() self.headers = {k.decode(): v.decode() for k, v in request.headers} self.request_body = body + self.parsed_url = urlparse(self.path) response = HAPResponse() self.response = response @@ -210,7 +211,7 @@ def dispatch(self, request, body=None): self.headers, ) - path = urlparse(self.path).path + path = self.parsed_url.path try: getattr(self, self.HANDLERS[self.command][path])() except UnprivilegedRequestException: @@ -584,7 +585,7 @@ def handle_get_characteristics(self): raise UnprivilegedRequestException # Check that char exists and ... - params = parse_qs(urlparse(self.path).query) + params = parse_qs(self.parsed_url.query) response = self.accessory_handler.get_characteristics( params["id"][0].split(",") ) @@ -612,7 +613,7 @@ def handle_set_characteristics(self): self.send_response(HTTPStatus.UNAUTHORIZED) return - requested_chars = json.loads(self.request_body.decode("utf-8")) + requested_chars = from_hap_json(self.request_body.decode("utf-8")) logger.debug( "%s: Set characteristics content: %s", self.client_address, requested_chars ) @@ -637,7 +638,7 @@ def handle_prepare(self): self.send_response(HTTPStatus.UNAUTHORIZED) return - request = json.loads(self.request_body.decode("utf-8")) + request = from_hap_json(self.request_body.decode("utf-8")) logger.debug("%s: prepare content: %s", self.client_address, request) response = self.accessory_handler.prepare(request, self.client_address) @@ -742,7 +743,7 @@ def _send_tlv_pairing_response(self, data): def handle_resource(self): """Get a snapshot from the camera.""" - data = json.loads(self.request_body.decode("utf-8")) + data = from_hap_json(self.request_body.decode("utf-8")) if self.accessory_handler.accessory.category == CATEGORY_BRIDGE: accessory = self.accessory_handler.accessory.accessories.get(data["aid"]) diff --git a/pyhap/hap_protocol.py b/pyhap/hap_protocol.py index 33b750ab..597dfb5f 100644 --- a/pyhap/hap_protocol.py +++ b/pyhap/hap_protocol.py @@ -18,7 +18,7 @@ logger = logging.getLogger(__name__) -HIGH_WRITE_BUFFER_SIZE = 2 ** 19 +HIGH_WRITE_BUFFER_SIZE = 2**19 # We timeout idle connections after 90 hours as we must # clean up unused sockets periodically. 90 hours was choosen # as its the longest time we expect a user to be away from diff --git a/pyhap/util.py b/pyhap/util.py index 2f8f92f9..b8f9048c 100644 --- a/pyhap/util.py +++ b/pyhap/util.py @@ -1,11 +1,12 @@ import asyncio import base64 import functools -import json import random import socket from uuid import UUID +import orjson + from .const import BASE_UUID ALPHANUM = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" @@ -157,9 +158,16 @@ def hap_type_to_uuid(hap_type): def to_hap_json(dump_obj): """Convert an object to HAP json.""" - return json.dumps(dump_obj, separators=(",", ":")).encode("utf-8") + return orjson.dumps(dump_obj) # pylint: disable=no-member def to_sorted_hap_json(dump_obj): """Convert an object to sorted HAP json.""" - return json.dumps(dump_obj, sort_keys=True, separators=(",", ":")).encode("utf-8") + return orjson.dumps( # pylint: disable=no-member + dump_obj, option=orjson.OPT_SORT_KEYS # pylint: disable=no-member + ) + + +def from_hap_json(json_str): + """Convert json to an object.""" + return orjson.loads(json_str) # pylint: disable=no-member diff --git a/pylintrc b/pylintrc index b8eeab8e..032f9698 100644 --- a/pylintrc +++ b/pylintrc @@ -10,6 +10,5 @@ disable= too-many-public-methods, too-many-return-statements, too-many-statements, - bad-continuation, unused-argument, consider-using-with diff --git a/requirements.txt b/requirements.txt index 5b6a15d1..87ef3320 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,5 @@ h11 +chacha20poly1305-reuseable cryptography +orjson zeroconf diff --git a/requirements_all.txt b/requirements_all.txt index 41563ce4..89560833 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1,5 +1,6 @@ base36 cryptography +orjson pyqrcode h11 zeroconf diff --git a/setup.py b/setup.py index 8b08f00f..1b70ddae 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ README = f.read() -REQUIRES = ["cryptography", "zeroconf>=0.36.2", "h11"] +REQUIRES = ["cryptography", "chacha20poly1305-reuseable", "orjson>=3.7.2", "zeroconf>=0.36.2", "h11"] setup( diff --git a/tests/conftest.py b/tests/conftest.py index 20476b51..29d05c51 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -52,5 +52,5 @@ def __init__(self): def publish(self, data, client_addr=None, immediate=False): pass - def add_job(self, target, *args): # pylint: disable=no-self-use + def add_job(self, target, *args): asyncio.new_event_loop().run_until_complete(target(*args)) diff --git a/tests/test_characteristic.py b/tests/test_characteristic.py index 828e73f5..9faa25d6 100644 --- a/tests/test_characteristic.py +++ b/tests/test_characteristic.py @@ -45,7 +45,7 @@ def test_repr(): char = get_char(PROPERTIES.copy()) del char.properties["Permissions"] assert ( - char.__repr__() == "" ) diff --git a/tests/test_hap_handler.py b/tests/test_hap_handler.py index 0225cfbd..bbc2fa7c 100644 --- a/tests/test_hap_handler.py +++ b/tests/test_hap_handler.py @@ -3,6 +3,7 @@ import json from unittest.mock import patch +from urllib.parse import urlparse from uuid import UUID import pytest @@ -697,6 +698,8 @@ def test_handle_get_characteristics_encrypted(driver): response = hap_handler.HAPResponse() handler.response = response handler.path = "/characteristics?id=1.11" + handler.parsed_url = urlparse(handler.path) + handler.handle_get_characteristics() assert response.status_code == 200 diff --git a/tests/test_hap_protocol.py b/tests/test_hap_protocol.py index 80f875e9..0d2a58c3 100644 --- a/tests/test_hap_protocol.py +++ b/tests/test_hap_protocol.py @@ -28,7 +28,7 @@ def decrypt(self): self._crypt_in_buffer = bytearray() # Encrypted buffer return decrypted - def encrypt(self, data): # pylint: disable=no-self-use + def encrypt(self, data): """Mock as plaintext.""" return data diff --git a/tests/test_service.py b/tests/test_service.py index e17fc0c9..18a57624 100644 --- a/tests/test_service.py +++ b/tests/test_service.py @@ -30,9 +30,7 @@ def test_repr(): """Test service representation.""" service = Service(uuid1(), "TestService") service.characteristics = [get_chars()[0]] - assert ( - service.__repr__() == "" - ) + assert repr(service) == "" def test_add_characteristic():