Skip to content

Commit

Permalink
Merge pull request #333 from ikalchev/v3.4.0
Browse files Browse the repository at this point in the history
V3.4.0
  • Loading branch information
ikalchev authored Mar 6, 2021
2 parents 1ef5d98 + 85671a1 commit 0974827
Show file tree
Hide file tree
Showing 28 changed files with 1,000 additions and 424 deletions.
24 changes: 22 additions & 2 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -35,7 +35,7 @@ jobs:

strategy:
matrix:
python-version: [3.7]
python-version: [3.9]

steps:
- uses: actions/checkout@v1
Expand All @@ -60,3 +60,23 @@ jobs:
fail_ci_if_error: true
path_to_write_report: ./coverage/codecov_report.txt
verbose: true

bandit:
runs-on: ubuntu-latest

strategy:
matrix:
python-version: [3.9]

steps:
- uses: actions/checkout@v1
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install tox tox-gh-actions
- name: Bandit
run: TOXENV=bandit tox
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,25 @@ Sections
### Developers
-->

## [3.4.0] - 2021-03-06

### Added
- Python 3.10 support. [#328](https://github.com/ikalchev/HAP-python/pull/328)

### Fixed
- Improve connection stability with large responses. [#320](https://github.com/ikalchev/HAP-python/pull/320)
- Fix `Accessroy.run` not being awaited from a bridge. [#323](https://github.com/ikalchev/HAP-python/pull/323)
- Clean up event subscriptions on client disconnect. [#324](https://github.com/ikalchev/HAP-python/pull/324)

### Removed
- Remove legacy python code. [#321](https://github.com/ikalchev/HAP-python/pull/321)
- Remove deprecated `get_char_loader` and `get_serv_loader`. [#322](https://github.com/ikalchev/HAP-python/pull/322)

### Developers
- Increase code coverage. [#325](https://github.com/ikalchev/HAP-python/pull/325), [#326](https://github.com/ikalchev/HAP-python/pull/326), [#330](https://github.com/ikalchev/HAP-python/pull/330), [#331](https://github.com/ikalchev/HAP-python/pull/331), [#332](https://github.com/ikalchev/HAP-python/pull/332)
- Add bandit to CI. [#329](https://github.com/ikalchev/HAP-python/pull/329)


## [3.3.2] - 2021-03-01

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
49 changes: 6 additions & 43 deletions pyhap/accessory.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@
import itertools
import logging

from pyhap import util, SUPPORT_QR_CODE
from pyhap import SUPPORT_QR_CODE, util
from pyhap.const import (
STANDALONE_AID,
CATEGORY_BRIDGE,
CATEGORY_OTHER,
HAP_REPR_AID,
HAP_REPR_IID,
HAP_REPR_SERVICES,
HAP_REPR_VALUE,
CATEGORY_OTHER,
CATEGORY_BRIDGE,
STANDALONE_AID,
)
from pyhap.iid_manager import IIDManager

Expand All @@ -25,9 +25,6 @@ class Accessory:
"""A representation of a HAP accessory.
Inherit from this class to build your own accessories.
At the end of the init of this class, the _set_services method is called.
Use this to set your HAP services.
"""

category = CATEGORY_OTHER
Expand All @@ -51,7 +48,6 @@ def __init__(self, driver, display_name, aid=None):
self.iid_manager = IIDManager()

self.add_info_service()
self._set_services()

def __repr__(self):
"""Return the representation of the accessory."""
Expand All @@ -60,19 +56,6 @@ def __repr__(self):
self.display_name, services
)

def __getstate__(self):
state = self.__dict__.copy()
state["driver"] = None
state["run_sentinel"] = None
return state

def _set_services(self):
"""Set the services for this accessory.
.. deprecated:: 2.0
Initialize the service inside the accessory `init` method instead.
"""

@property
def available(self):
"""Accessory is available.
Expand Down Expand Up @@ -107,7 +90,7 @@ def set_info_service(
serv_info.configure_char("Manufacturer", value=manufacturer)
if model:
serv_info.configure_char("Model", value=model)
if serial_number:
if serial_number is not None:
if len(serial_number) >= 1:
serv_info.configure_char("SerialNumber", value=serial_number)
else:
Expand All @@ -133,26 +116,6 @@ def set_primary_service(self, primary_service):
for service in self.services:
service.is_primary_service = service.type_id == primary_service.type_id

def config_changed(self):
"""Notify the accessory about configuration changes.
These include new services or updated characteristic values, e.g.
the Name of a service changed.
This method also notifies the driver about the change, so that it can
publish the changes to the world.
.. note:: If you are changing the configuration of a bridged accessory
(i.e. an Accessory that is contained in a Bridge),
you should call the `config_changed` method on the Bridge.
Deprecated. Use `driver.config_changed()` instead.
"""
logger.warning(
"This method is now deprecated. Use 'driver.config_changed' instead."
)
self.driver.config_changed()

def add_service(self, *servs):
"""Add the given services to this Accessory.
Expand Down Expand Up @@ -405,7 +368,7 @@ def get_characteristic(self, aid, iid):
async def run(self):
"""Schedule tasks for each of the accessories' run method."""
for acc in self.accessories.values():
self.driver.async_add_job(acc.run)
await self.driver.async_add_job(acc.run)

async def stop(self):
"""Calls stop() on all contained accessories."""
Expand Down
77 changes: 47 additions & 30 deletions pyhap/accessory_driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,31 +18,30 @@
import asyncio
import base64
from concurrent.futures import ThreadPoolExecutor
import functools
import hashlib
import tempfile
import json
import logging
import os
import re
import socket
import sys
import tempfile
import threading
import re

from zeroconf import ServiceInfo, Zeroconf

from pyhap import util
from pyhap.accessory import get_topic
from pyhap.characteristic import CharacteristicError
from pyhap.const import (
MAX_CONFIG_VERSION,
HAP_PERMISSION_NOTIFY,
HAP_REPR_ACCS,
HAP_REPR_AID,
HAP_REPR_CHARS,
HAP_REPR_IID,
HAP_REPR_STATUS,
HAP_REPR_VALUE,
MAX_CONFIG_VERSION,
STANDALONE_AID,
)
from pyhap.encoder import AccessoryEncoder
Expand All @@ -51,6 +50,7 @@
from pyhap.loader import Loader
from pyhap.params import get_srp_context
from pyhap.state import State

from .util import callback

logger = logging.getLogger(__name__)
Expand All @@ -64,20 +64,6 @@
VALID_MDNS_REGEX = re.compile(r"[^A-Za-z0-9\-]+")


def is_callback(func):
"""Check if function is callback."""
return "_pyhap_callback" in getattr(func, "__dict__", {})


def iscoro(func):
"""Check if the function is a coroutine or if the function is a ``functools.partial``,
check the wrapped function for the same.
"""
if isinstance(func, functools.partial):
func = func.func
return asyncio.iscoroutinefunction(func)


class AccessoryMDNSServiceInfo(ServiceInfo):
"""A mDNS service info representation of an accessory."""

Expand Down Expand Up @@ -231,7 +217,7 @@ def __init__(
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
Expand Down Expand Up @@ -266,7 +252,7 @@ def start(self):
"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")
Expand All @@ -277,6 +263,19 @@ def start(self):
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.
Expand All @@ -288,11 +287,9 @@ def start_service(self):
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,
Expand All @@ -302,12 +299,14 @@ def start_service(self):

# 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:
Expand All @@ -322,7 +321,7 @@ def start_service(self):

def stop(self):
"""Method to stop pyhap."""
self.loop.call_soon_threadsafe(self.loop.create_task, self.async_stop())
self.add_job(self.async_stop)

async def async_stop(self):
"""Stops the AccessoryDriver and shutdown all remaining tasks."""
Expand Down Expand Up @@ -376,9 +375,9 @@ def async_add_job(self, target, *args):

if asyncio.iscoroutine(target):
task = self.loop.create_task(target)
elif is_callback(target):
elif util.is_callback(target):
self.loop.call_soon(target, *args)
elif iscoro(target):
elif util.iscoro(target):
task = self.loop.create_task(target(*args))
else:
task = self.loop.run_in_executor(None, target, *args)
Expand Down Expand Up @@ -431,6 +430,24 @@ def async_subscribe_client_topic(self, client, topic, subscribe=True):
if not subscribed_clients:
del self.topics[topic]

def connection_lost(self, client):
"""Called when a connection is lost to a client.
This method must be run in the event loop.
:param client: A client (address, port) tuple that should be unsubscribed.
:type client: tuple <str, int>
"""
client_topics = []
for topic, subscribed_clients in self.topics.items():
if client in subscribed_clients:
# Make a copy to avoid changing
# self.topics during iteration
client_topics.append(topic)

for topic in client_topics:
self.async_subscribe_client_topic(client, topic, subscribe=False)

def publish(self, data, sender_client_addr=None):
"""Publishes an event to the client.
Expand Down
4 changes: 2 additions & 2 deletions pyhap/const.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""This module contains constants used by other modules."""
MAJOR_VERSION = 3
MINOR_VERSION = 3
PATCH_VERSION = 2
MINOR_VERSION = 4
PATCH_VERSION = 0
__short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION)
__version__ = '{}.{}'.format(__short_version__, PATCH_VERSION)
REQUIRED_PYTHON_VER = (3, 5)
Expand Down
Loading

0 comments on commit 0974827

Please sign in to comment.