Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix pairing url generator #303

Merged
merged 1 commit into from
Jan 30, 2021
Merged
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
31 changes: 31 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: CI

on:
- push
- pull_request

jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.5, 3.6, 3.7, 3.8]

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: Test with tox
run: tox
- name: Lint with tox
run: TOXENV=lint tox
- name: pylint with tox
run: TOXENV=pylint tox
- name: Docs with tox
run: TOXENV=docs tox
124 changes: 75 additions & 49 deletions pyhap/accessory.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
"""Module for the Accessory classes."""
import itertools
import logging
import struct

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

if SUPPORT_QR_CODE:
Expand Down Expand Up @@ -51,13 +56,14 @@ def __init__(self, driver, display_name, aid=None):
def __repr__(self):
"""Return the representation of the accessory."""
services = [s.display_name for s in self.services]
return "<accessory display_name='{}' services={}>" \
.format(self.display_name, services)
return "<accessory display_name='{}' services={}>".format(
self.display_name, services
)

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

def _set_services(self):
Expand Down Expand Up @@ -85,29 +91,31 @@ def add_info_service(self):
Called in `__init__` to be sure that it is the first service added.
May be overridden.
"""
serv_info = self.driver.loader.get_service('AccessoryInformation')
serv_info.configure_char('Name', value=self.display_name)
serv_info.configure_char('SerialNumber', value='default')
serv_info = self.driver.loader.get_service("AccessoryInformation")
serv_info.configure_char("Name", value=self.display_name)
serv_info.configure_char("SerialNumber", value="default")
self.add_service(serv_info)

def set_info_service(self, firmware_revision=None, manufacturer=None,
model=None, serial_number=None):
def set_info_service(
self, firmware_revision=None, manufacturer=None, model=None, serial_number=None
):
"""Quick assign basic accessory information."""
serv_info = self.get_service('AccessoryInformation')
serv_info = self.get_service("AccessoryInformation")
if firmware_revision:
serv_info.configure_char(
'FirmwareRevision', value=firmware_revision)
serv_info.configure_char("FirmwareRevision", value=firmware_revision)
if manufacturer:
serv_info.configure_char('Manufacturer', value=manufacturer)
serv_info.configure_char("Manufacturer", value=manufacturer)
if model:
serv_info.configure_char('Model', value=model)
serv_info.configure_char("Model", value=model)
if serial_number:
if len(serial_number) >= 1:
serv_info.configure_char('SerialNumber', value=serial_number)
serv_info.configure_char("SerialNumber", value=serial_number)
else:
logger.warning(
"Couldn't add SerialNumber for %s. The SerialNumber must "
"be at least one character long.", self.display_name)
"be at least one character long.",
self.display_name,
)

def add_preload_service(self, service, chars=None):
"""Create a service with the given name and add it to this acc."""
Expand All @@ -123,8 +131,7 @@ def add_preload_service(self, service, chars=None):
def set_primary_service(self, primary_service):
"""Set the primary service of the acc."""
for service in self.services:
service.is_primary_service = service.type_id == \
primary_service.type_id
service.is_primary_service = service.type_id == primary_service.type_id

def config_changed(self):
"""Notify the accessory about configuration changes.
Expand All @@ -142,7 +149,8 @@ def config_changed(self):
Deprecated. Use `driver.config_changed()` instead.
"""
logger.warning(
'This method is now deprecated. Use \'driver.config_changed\' instead.')
"This method is now deprecated. Use 'driver.config_changed' instead."
)
self.driver.config_changed()

def add_service(self, *servs):
Expand Down Expand Up @@ -184,24 +192,27 @@ def xhm_uri(self):

:rtype: str
"""
buffer = bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00')
payload = 0
payload |= 0 & 0x7 # version

value_low = int(self.driver.state.pincode.replace(b'-', b''), 10)
value_low |= 1 << 28
struct.pack_into('>L', buffer, 4, value_low)
payload <<= 4
payload |= 0 & 0xF # reserved bits

if self.category == CATEGORY_OTHER:
buffer[4] = buffer[4] | 1 << 7
payload <<= 8
payload |= self.category & 0xFF # category

value_high = self.category >> 1
struct.pack_into('>L', buffer, 0, value_high)
payload <<= 4
payload |= 2 & 0xF # flags

encoded_payload = base36.dumps(
struct.unpack_from('>L', buffer, 4)[0] +
(struct.unpack_from('>L', buffer, 0)[0] * (1 << 32))).upper()
encoded_payload = encoded_payload.rjust(9, '0')
payload <<= 27
payload |= (
int(self.driver.state.pincode.replace(b"-", b""), 10) & 0x7FFFFFFF
) # pincode

return 'X-HM://' + encoded_payload + self.driver.state.setup_id
encoded_payload = base36.dumps(payload).upper()
encoded_payload = encoded_payload.rjust(9, "0")

return "X-HM://" + encoded_payload + self.driver.state.setup_id

def get_characteristic(self, aid, iid):
"""Get the characteristic for the given IID.
Expand Down Expand Up @@ -244,17 +255,27 @@ def setup_message(self):
pincode = self.driver.state.pincode.decode()
if SUPPORT_QR_CODE:
xhm_uri = self.xhm_uri()
print('Setup payload: {}'.format(xhm_uri), flush=True)
print('Scan this code with your HomeKit app on your iOS device:',
flush=True)
print("Setup payload: {}".format(xhm_uri), flush=True)
print(
"Scan this code with your HomeKit app on your iOS device:", flush=True
)
print(QRCode(xhm_uri).terminal(quiet_zone=2), flush=True)
print('Or enter this code in your HomeKit app on your iOS device: '
'{}'.format(pincode), flush=True)
print(
"Or enter this code in your HomeKit app on your iOS device: "
"{}".format(pincode),
flush=True,
)
else:
print('To use the QR Code feature, use \'pip install '
'HAP-python[QRCode]\'', flush=True)
print('Enter this code in your HomeKit app on your iOS device: {}'
.format(pincode), flush=True)
print(
"To use the QR Code feature, use 'pip install " "HAP-python[QRCode]'",
flush=True,
)
print(
"Enter this code in your HomeKit app on your iOS device: {}".format(
pincode
),
flush=True,
)

@staticmethod
def run_at_interval(seconds):
Expand All @@ -272,14 +293,16 @@ def run(self):
Determines the interval on which the decorated method will be called.
:type seconds: float
"""

def _repeat(func):
async def _wrapper(self, *args):
while True:
await self.driver.async_add_job(func, self, *args)
if await util.event_wait(
self.driver.aio_stop_event, seconds):
if await util.event_wait(self.driver.aio_stop_event, seconds):
break

return _wrapper

return _repeat

async def run(self):
Expand Down Expand Up @@ -351,8 +374,11 @@ def add_accessory(self, acc):

if acc.aid is None:
# For some reason AID=7 gets unsupported. See issue #61
acc.aid = next(aid for aid in itertools.count(2)
if aid != 7 and aid not in self.accessories)
acc.aid = next(
aid
for aid in itertools.count(2)
if aid != 7 and aid not in self.accessories
)
elif acc.aid == self.aid or acc.aid in self.accessories:
raise ValueError("Duplicate AID found when attempting to add accessory")

Expand Down Expand Up @@ -389,4 +415,4 @@ async def stop(self):


def get_topic(aid, iid):
return str(aid) + '.' + str(iid)
return str(aid) + "." + str(iid)
1 change: 0 additions & 1 deletion pyhap/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,6 @@


FFMPEG_CMD = (
# pylint: disable=bad-continuation
'ffmpeg -re -f avfoundation -framerate {fps} -i 0:0 -threads 0 '
'-vcodec libx264 -an -pix_fmt yuv420p -r {fps} -f rawvideo -tune zerolatency '
'-vf scale={width}:{height} -b:v {v_max_bitrate}k -bufsize {v_max_bitrate}k '
Expand Down
113 changes: 113 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
[tool.black]
target-version = ["py37", "py38"]
exclude = 'generated'

[tool.isort]
# https://github.com/PyCQA/isort/wiki/isort-Settings
profile = "black"
# will group `import x` and `from x import` of the same module.
force_sort_within_sections = true
known_first_party = [
"pyhap",
]
combine_as_imports = true

[tool.pylint.MASTER]
ignore = [
"tests",
]
# Use a conservative default here; 2 should speed up most setups and not hurt
# any too bad. Override on command line as appropriate.
# Disabled for now: https://github.com/PyCQA/pylint/issues/3584
#jobs = 2
load-plugins = [
"pylint_strict_informational",
]
persistent = false
extension-pkg-whitelist = [
"ciso8601",
"cv2",
]

[tool.pylint.BASIC]
good-names = [
"_",
"ev",
"ex",
"fp",
"i",
"id",
"j",
"k",
"Run",
"T",
]

[tool.pylint."MESSAGES CONTROL"]
# Reasons disabled:
# format - handled by black
# locally-disabled - it spams too much
# duplicate-code - unavoidable
# cyclic-import - doesn't test if both import on load
# abstract-class-little-used - prevents from setting right foundation
# unused-argument - generic callbacks and setup methods create a lot of warnings
# too-many-* - are not enforced for the sake of readability
# too-few-* - same as too-many-*
# abstract-method - with intro of async there are always methods missing
# inconsistent-return-statements - doesn't handle raise
# too-many-ancestors - it's too strict.
# wrong-import-order - isort guards this
disable = [
"format",
"abstract-class-little-used",
"abstract-method",
"cyclic-import",
"duplicate-code",
"inconsistent-return-statements",
"locally-disabled",
"not-context-manager",
"too-few-public-methods",
"too-many-ancestors",
"too-many-arguments",
"too-many-branches",
"too-many-instance-attributes",
"too-many-lines",
"too-many-locals",
"too-many-public-methods",
"too-many-return-statements",
"too-many-statements",
"too-many-boolean-expressions",
"unused-argument",
"wrong-import-order",
]
enable = [
#"useless-suppression", # temporarily every now and then to clean them up
"use-symbolic-message-instead",
]

[tool.pylint.REPORTS]
score = false

[tool.pylint.TYPECHECK]
ignored-classes = [
"_CountingAttr", # for attrs
]

[tool.pylint.FORMAT]
expected-line-ending-format = "LF"

[tool.pylint.EXCEPTIONS]
overgeneral-exceptions = [
"BaseException",
"Exception",
"HomeAssistantError",
]

[tool.pytest.ini_options]
testpaths = [
"tests",
]
norecursedirs = [
".git",
"testing_config",
]
2 changes: 2 additions & 0 deletions requirements_test.txt
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
base36
flake8
flake8-docstrings
pydocstyle
pylint
pytest
pytest-cov
pytest-timeout>=1.2.1
pyqrcode
tox
Loading