Skip to content

Commit

Permalink
Merge pull request #431 from ikalchev/v4.6.0
Browse files Browse the repository at this point in the history
V4.6.0
  • Loading branch information
ikalchev authored Dec 10, 2022
2 parents fbd1f4b + 666c3c1 commit 8b62c19
Show file tree
Hide file tree
Showing 19 changed files with 270 additions and 33 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,4 @@ pip-selfcheck.json

# HAP-python-generated files
accessory.pickle
accessory.state
/*.state
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ Sections
### Developers
-->

## [4.6.0] - 2022-12-10

- Patch for [WinError 5] Access Denied. [#421](https://github.com/ikalchev/HAP-python/pull/421)
- Add support for a custom iid manager. [#423](https://github.com/ikalchev/HAP-python/pull/423)
- Fix pairing with iOS 16. [#424](https://github.com/ikalchev/HAP-python/pull/424)
- Fix error logging when `get_characteristics` fails. [#425](https://github.com/ikalchev/HAP-python/pull/425)
- Add necessary support for Adaptive Lightning. [#428](https://github.com/ikalchev/HAP-python/pull/428)

## [4.5.0] - 2022-06-28

- Speed up "get accessories". [#418](https://github.com/ikalchev/HAP-python/pull/418)
Expand Down
93 changes: 93 additions & 0 deletions adaptive_lightbulb.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
"""This virtual lightbulb implements the bare minimum needed for HomeKit
controller to recognize it as having AdaptiveLightning
"""
import logging
import signal
import random
import tlv8
import base64

from pyhap.accessory import Accessory
from pyhap.accessory_driver import AccessoryDriver
from pyhap.const import (CATEGORY_LIGHTBULB,
HAP_REPR_IID)
from pyhap.loader import get_loader

# Define tlv8 Keys and Values
SUPPORTED_TRANSITION_CONFIGURATION = 1
CHARACTERISTIC_IID = 1
TRANSITION_TYPE = 2

BRIGHTNESS = 1
COLOR_TEMPERATURE = 2

logging.basicConfig(level=logging.DEBUG, format="[%(module)s] %(message)s")

def bytes_to_base64_string(value: bytes) -> str:
return base64.b64encode(value).decode('ASCII')

class LightBulb(Accessory):
"""Fake lightbulb, logs what the client sets."""

category = CATEGORY_LIGHTBULB

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

serv_light = self.add_preload_service('Lightbulb', [
# The names here refer to the Characteristic name defined
# in characteristic.json
"Brightness",
"ColorTemperature",
"ActiveTransitionCount",
"TransitionControl",
"SupportedTransitionConfiguration"])

self.char_on = serv_light.configure_char(
'On', setter_callback=self.set_on)
self.char_br = serv_light.configure_char(
'Brightness', setter_callback=self.set_brightness)
self.char_ct = serv_light.configure_char(
'ColorTemperature', setter_callback=self.set_ct, value=140)

# Via this structure we advertise to the controller that we are
# capable of autonomous transitions between states on brightness
# and color temperature.
supported_transitions = [tlv8.Entry(SUPPORTED_TRANSITION_CONFIGURATION, [
tlv8.Entry(CHARACTERISTIC_IID, self.char_br.to_HAP()[HAP_REPR_IID]),
tlv8.Entry(TRANSITION_TYPE, BRIGHTNESS),
tlv8.Entry(CHARACTERISTIC_IID, self.char_ct.to_HAP()[HAP_REPR_IID]),
tlv8.Entry(TRANSITION_TYPE, COLOR_TEMPERATURE)
])]

bytes_data = tlv8.encode(supported_transitions)
b64str = bytes_to_base64_string(bytes_data)

self.char_atc = serv_light.configure_char(
'ActiveTransitionCount', setter_callback=self.set_atc)
self.char_tc = serv_light.configure_char(
'TransitionControl', setter_callback=self.set_tc)
self.char_stc = serv_light.configure_char(
'SupportedTransitionConfiguration',
value=b64str)

def set_on(self, value):
logging.info("Write On State: %s", value)

def set_ct(self, value):
logging.info("Bulb color temp: %s", value)

def set_atc(self, value):
logging.info("Write to ActiveTransactionCount: %s", value)

def set_tc(self, value):
logging.info("Write to TransitionControl: %s", value)

def set_brightness(self, value):
logging.info("Bulb brightness: %s", value)

driver = AccessoryDriver(port=51826, persist_file='adaptive_lightbulb.state')
driver.add_accessory(accessory=LightBulb(driver, 'Lightbulb'))
signal.signal(signal.SIGTERM, driver.signal_handler)
driver.start()

18 changes: 11 additions & 7 deletions pyhap/accessory.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class Accessory:

category = CATEGORY_OTHER

def __init__(self, driver, display_name, aid=None):
def __init__(self, driver, display_name, aid=None, iid_manager=None):
"""Initialise with the given properties.
:param display_name: Name to be displayed in the Home app.
Expand All @@ -51,7 +51,7 @@ def __init__(self, driver, display_name, aid=None):
self.display_name = display_name
self.driver = driver
self.services = []
self.iid_manager = IIDManager()
self.iid_manager = iid_manager or IIDManager()
self.setter_callback = None

self.add_info_service()
Expand Down Expand Up @@ -116,9 +116,11 @@ def set_info_service(
self.display_name,
)

def add_preload_service(self, service, chars=None):
def add_preload_service(self, service, chars=None, unique_id=None):
"""Create a service with the given name and add it to this acc."""
service = self.driver.loader.get_service(service)
if unique_id is not None:
service.unique_id = unique_id
if chars:
chars = chars if isinstance(chars, list) else [chars]
for char_name in chars:
Expand All @@ -144,12 +146,12 @@ def add_service(self, *servs):
:type: Service
"""
for s in servs:
s.broker = self
self.services.append(s)
self.iid_manager.assign(s)
s.broker = self
for c in s.characteristics:
self.iid_manager.assign(c)
c.broker = self
self.iid_manager.assign(c)

def get_service(self, name):
"""Return a Service with the given name.
Expand Down Expand Up @@ -323,8 +325,10 @@ class Bridge(Accessory):

category = CATEGORY_BRIDGE

def __init__(self, driver, display_name):
super().__init__(driver, display_name, aid=STANDALONE_AID)
def __init__(self, driver, display_name, iid_manager=None):
super().__init__(
driver, display_name, aid=STANDALONE_AID, iid_manager=iid_manager
)
self.accessories = {} # aid: acc

def add_accessory(self, acc):
Expand Down
22 changes: 19 additions & 3 deletions pyhap/accessory_driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -626,13 +626,15 @@ def async_persist(self):
Must be run in the event loop.
"""
loop = asyncio.get_event_loop()
logger.debug("Scheduling write of accessory state to disk")
asyncio.ensure_future(loop.run_in_executor(None, self.persist))

def persist(self):
"""Saves the state of the accessory.
Must run in executor.
"""
logger.debug("Writing of accessory state to disk")
tmp_filename = None
try:
temp_dir = os.path.dirname(self.persist_file)
Expand All @@ -641,7 +643,13 @@ def persist(self):
) as file_handle:
tmp_filename = file_handle.name
self.encoder.persist(file_handle, self.state)
if os.name == 'nt': # Or `[WinError 5] Access Denied` will be raised on Windows
os.chmod(tmp_filename, 0o644)
os.chmod(self.persist_file, 0o644)
os.replace(tmp_filename, self.persist_file)
except Exception: # pylint: disable=broad-except
logger.exception("Failed to persist accessory state")
raise
finally:
if tmp_filename and os.path.exists(tmp_filename):
os.remove(tmp_filename)
Expand Down Expand Up @@ -672,7 +680,9 @@ def pair(self, client_uuid, client_public, client_permissions):
:return: Whether the pairing is successful.
:rtype: bool
"""
logger.info("Paired with %s.", client_uuid)
logger.info(
"Paired with %s with permissions %s.", client_uuid, client_permissions
)
self.state.add_paired_client(client_uuid, client_public, client_permissions)
self.async_persist()
return True
Expand Down Expand Up @@ -801,10 +811,16 @@ def get_characteristics(self, char_ids):
rep[HAP_REPR_VALUE] = char.get_value()
rep[HAP_REPR_STATUS] = HAP_SERVER_STATUS.SUCCESS
except CharacteristicError:
logger.error("Error getting value for characteristic %s.", id)
logger.error(
"%s: Error getting value for characteristic %s.",
self.accessory.display_name,
(aid, iid),
)
except Exception: # pylint: disable=broad-except
logger.exception(
"Unexpected error getting value for characteristic %s.", id
"%s: Unexpected error getting value for characteristic %s.",
self.accessory.display_name,
(aid, iid),
)

chars.append(rep)
Expand Down
2 changes: 1 addition & 1 deletion pyhap/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,7 @@ def _setup_stream_management(self, options):

def _create_stream_management(self, stream_idx, options):
"""Create a stream management service."""
management = self.add_preload_service("CameraRTPStreamManagement")
management = self.add_preload_service("CameraRTPStreamManagement", unique_id=stream_idx)
management.configure_char(
"StreamingStatus",
getter_callback=lambda: self._get_streaming_status(stream_idx),
Expand Down
16 changes: 12 additions & 4 deletions pyhap/characteristic.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,8 @@
a temperature measuring or a device status.
"""
import logging

from uuid import UUID


from pyhap.const import (
HAP_PERMISSION_READ,
HAP_REPR_DESC,
Expand Down Expand Up @@ -133,10 +131,16 @@ class Characteristic:
"_uuid_str",
"_loader_display_name",
"allow_invalid_client_values",
"unique_id",
)

def __init__(
self, display_name, type_id, properties, allow_invalid_client_values=False
self,
display_name,
type_id,
properties,
allow_invalid_client_values=False,
unique_id=None,
):
"""Initialise with the given properties.
Expand Down Expand Up @@ -169,12 +173,16 @@ def __init__(
self.getter_callback = None
self.setter_callback = None
self.service = None
self.unique_id = unique_id
self._uuid_str = uuid_to_hap_type(type_id)
self._loader_display_name = None

def __repr__(self):
"""Return the representation of the characteristic."""
return f"<characteristic display_name={self.display_name} value={self.value} properties={self.properties}>"
return (
f"<characteristic display_name={self.display_name} unique_id={self.unique_id} "
f"value={self.value} properties={self.properties}>"
)

def _get_default_value(self):
"""Return default value for format."""
Expand Down
2 changes: 1 addition & 1 deletion pyhap/const.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""This module contains constants used by other modules."""
MAJOR_VERSION = 4
MINOR_VERSION = 5
MINOR_VERSION = 6
PATCH_VERSION = 0
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__ = f"{__short_version__}.{PATCH_VERSION}"
Expand Down
34 changes: 28 additions & 6 deletions pyhap/hap_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from chacha20poly1305_reuseable import ChaCha20Poly1305Reusable as ChaCha20Poly1305

from pyhap import tlv

from pyhap.const import (
CATEGORY_BRIDGE,
HAP_PERMISSIONS,
Expand Down Expand Up @@ -416,7 +417,11 @@ def _pairing_five(self, client_username, client_ltpk, encryption_key):
cipher = ChaCha20Poly1305(encryption_key)
aead_message = bytes(cipher.encrypt(self.PAIRING_5_NONCE, bytes(message), b""))

client_uuid = uuid.UUID(str(client_username, "utf-8"))
client_username_str = str(client_username, "utf-8")
client_uuid = uuid.UUID(client_username_str)
logger.debug(
"Finishing pairing with admin %s uuid=%s", client_username_str, client_uuid
)
should_confirm = self.accessory_handler.pair(
client_uuid, client_ltpk, HAP_PERMISSIONS.ADMIN
)
Expand Down Expand Up @@ -668,11 +673,18 @@ def handle_pairings(self):

def _handle_add_pairing(self, tlv_objects):
"""Update client information."""
logger.debug("%s: Adding client pairing.", self.client_address)
client_username = tlv_objects[HAP_TLV_TAGS.USERNAME]
client_username_str = str(client_username, "utf-8")
client_public = tlv_objects[HAP_TLV_TAGS.PUBLIC_KEY]
permissions = tlv_objects[HAP_TLV_TAGS.PERMISSIONS]
client_uuid = uuid.UUID(str(client_username, "utf-8"))
client_uuid = uuid.UUID(client_username_str)
logger.debug(
"%s: Adding client pairing for %s uuid=%s with permissions %s.",
self.client_address,
client_username_str,
client_uuid,
permissions,
)
should_confirm = self.accessory_handler.pair(
client_uuid, client_public, permissions
)
Expand All @@ -685,10 +697,17 @@ def _handle_add_pairing(self, tlv_objects):

def _handle_remove_pairing(self, tlv_objects):
"""Remove pairing with the client."""
logger.debug("%s: Removing client pairing.", self.client_address)
client_username = tlv_objects[HAP_TLV_TAGS.USERNAME]
client_uuid = uuid.UUID(str(client_username, "utf-8"))
client_username_str = str(client_username, "utf-8")
client_uuid = uuid.UUID(client_username_str)
was_paired = self.state.paired
logger.debug(
"%s: Removing client pairing (%s) uuid=%s (was previously paired=%s).",
self.client_address,
client_username_str,
client_uuid,
was_paired,
)
# If the client does not exist, we must
# respond with success per the spec
if client_uuid in self.state.paired_clients:
Expand All @@ -713,7 +732,10 @@ def _handle_list_pairings(self):
response.extend(
[
HAP_TLV_TAGS.USERNAME,
str(client_uuid).encode("utf-8"),
# iOS 16+ requires the username to be uppercase
# or it will unpair the accessory because it thinks
# the username is invalid
str(client_uuid).encode("utf-8").upper(),
HAP_TLV_TAGS.PUBLIC_KEY,
client_public,
HAP_TLV_TAGS.PERMISSIONS,
Expand Down
12 changes: 10 additions & 2 deletions pyhap/iid_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,17 @@ def assign(self, obj):
)
return

iid = self.get_iid_for_obj(obj)
self.iids[obj] = iid
self.objs[iid] = obj

def get_iid_for_obj(self, obj):
"""Get the IID for the given object.
Override this method to provide custom IID assignment.
"""
self.counter += 1
self.iids[obj] = self.counter
self.objs[self.counter] = obj
return self.counter

def get_obj(self, iid):
"""Get the object that is assigned the given IID."""
Expand Down
Loading

0 comments on commit 8b62c19

Please sign in to comment.