Skip to content

Commit 34f1104

Browse files
authored
Merge branch 'dev' into typing
2 parents 785eeba + f95cd15 commit 34f1104

File tree

2 files changed

+92
-115
lines changed

2 files changed

+92
-115
lines changed

pyhap/accessory_driver.py

Lines changed: 90 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"""
1818
import asyncio
1919
import base64
20+
from collections import defaultdict
2021
from concurrent.futures import ThreadPoolExecutor
2122
import hashlib
2223
import logging
@@ -26,14 +27,14 @@
2627
import tempfile
2728
import threading
2829
import time
29-
from typing import Optional
30+
from typing import Any, Dict, List, Optional, Tuple
3031

3132
from zeroconf import ServiceInfo
3233
from zeroconf.asyncio import AsyncZeroconf
3334

3435
from pyhap import util
3536
from pyhap.accessory import Accessory, get_topic
36-
from pyhap.characteristic import CharacteristicError
37+
from pyhap.characteristic import Characteristic, CharacteristicError
3738
from pyhap.const import (
3839
HAP_PERMISSION_NOTIFY,
3940
HAP_PROTOCOL_SHORT_VERSION,
@@ -53,6 +54,7 @@
5354
from pyhap.hsrp import Server as SrpServer
5455
from pyhap.loader import Loader
5556
from pyhap.params import get_srp_context
57+
from pyhap.service import Service
5658
from pyhap.state import State
5759

5860
from .const import HAP_SERVER_STATUS
@@ -67,12 +69,13 @@
6769
VALID_MDNS_REGEX = re.compile(r"[^A-Za-z0-9\-]+")
6870
LEADING_TRAILING_SPACE_DASH = re.compile(r"^[ -]+|[ -]+$")
6971
DASH_REGEX = re.compile(r"[-]+")
72+
KEYS_TO_EXCLUDE = {HAP_REPR_IID, HAP_REPR_AID}
7073

7174

7275
def _wrap_char_setter(char, value, client_addr):
7376
"""Process an characteristic setter callback trapping and logging all exceptions."""
7477
try:
75-
result = char.client_update_value(value, client_addr)
78+
response = char.client_update_value(value, client_addr)
7679
except Exception: # pylint: disable=broad-except
7780
logger.exception(
7881
"%s: Error while setting characteristic %s to %s",
@@ -81,7 +84,7 @@ def _wrap_char_setter(char, value, client_addr):
8184
value,
8285
)
8386
return HAP_SERVER_STATUS.SERVICE_COMMUNICATION_FAILURE, None
84-
return HAP_SERVER_STATUS.SUCCESS, result
87+
return HAP_SERVER_STATUS.SUCCESS, response
8588

8689

8790
def _wrap_acc_setter(acc, updates_by_service, client_addr):
@@ -859,122 +862,98 @@ def set_characteristics(self, chars_query, client_addr):
859862
:type chars_query: dict
860863
"""
861864
# TODO: Add support for chars that do no support notifications.
862-
updates = {}
863-
setter_results = {}
864-
setter_responses = {}
865-
had_error = False
866-
had_write_response = False
867-
expired = False
868865

869-
if HAP_REPR_PID in chars_query:
870-
pid = chars_query[HAP_REPR_PID]
871-
expire_time = self.prepared_writes.get(client_addr, {}).pop(pid, None)
872-
if expire_time is None or time.time() > expire_time:
873-
expired = True
866+
queries: List[Dict[str, Any]] = chars_query[HAP_REPR_CHARS]
874867

875-
for cq in chars_query[HAP_REPR_CHARS]:
876-
aid, iid = cq[HAP_REPR_AID], cq[HAP_REPR_IID]
877-
setter_results.setdefault(aid, {})
868+
self._notify(queries, client_addr)
878869

879-
if HAP_REPR_WRITE_RESPONSE in cq:
880-
setter_responses.setdefault(aid, {})
881-
had_write_response = True
870+
updates_by_accessories_services: Dict[
871+
Accessory, Dict[Service, Dict[Characteristic, Any]]
872+
] = defaultdict(lambda: defaultdict(dict))
873+
results: Dict[int, Dict[int, Dict[str, Any]]] = defaultdict(
874+
lambda: defaultdict(dict)
875+
)
876+
char_to_iid: Dict[Characteristic, int] = {}
882877

883-
if expired:
884-
setter_results[aid][iid] = HAP_SERVER_STATUS.INVALID_VALUE_IN_REQUEST
885-
had_error = True
886-
continue
878+
expired = False
879+
if HAP_REPR_PID in chars_query:
880+
pid = chars_query[HAP_REPR_PID]
881+
expire_time = self.prepared_writes.get(client_addr, {}).pop(pid, None)
882+
expired = expire_time is None or time.time() > expire_time
887883

888-
if HAP_PERMISSION_NOTIFY in cq:
889-
char_topic = get_topic(aid, iid)
890-
action = "Subscribed" if cq[HAP_PERMISSION_NOTIFY] else "Unsubscribed"
891-
logger.debug(
892-
"%s client %s to topic %s", action, client_addr, char_topic
893-
)
894-
self.async_subscribe_client_topic(
895-
client_addr, char_topic, cq[HAP_PERMISSION_NOTIFY]
896-
)
884+
primary_accessory = self.accessory
885+
primary_aid = primary_accessory.aid
897886

898-
if HAP_REPR_VALUE not in cq:
887+
for query in queries:
888+
if HAP_REPR_VALUE not in query and not expired:
899889
continue
900890

901-
updates.setdefault(aid, {})[iid] = cq[HAP_REPR_VALUE]
891+
aid = query[HAP_REPR_AID]
892+
iid = query[HAP_REPR_IID]
893+
value = query.get(HAP_REPR_VALUE)
894+
write_response_requested = query.get(HAP_REPR_WRITE_RESPONSE, False)
902895

903-
for aid, new_iid_values in updates.items():
904-
if self.accessory.aid == aid:
905-
acc = self.accessory
896+
if aid == primary_aid:
897+
acc = primary_accessory
906898
else:
907899
acc = self.accessory.accessories.get(aid)
900+
char = acc.get_characteristic(aid, iid)
908901

909-
updates_by_service = {}
910-
char_to_iid = {}
911-
for iid, value in new_iid_values.items():
912-
# Characteristic level setter callbacks
913-
char = acc.get_characteristic(aid, iid)
902+
set_result = HAP_SERVER_STATUS.INVALID_VALUE_IN_REQUEST
903+
set_result_value = None
914904

905+
if value is not None:
915906
set_result, set_result_value = _wrap_char_setter(
916907
char, value, client_addr
917908
)
918-
if set_result != HAP_SERVER_STATUS.SUCCESS:
919-
had_error = True
920-
921-
setter_results[aid][iid] = set_result
922-
923-
if set_result_value is not None:
924-
if setter_responses.get(aid, None) is None:
925-
logger.warning(
926-
"Returning write response '%s' when it wasn't requested for %s %s",
927-
set_result_value,
928-
aid,
929-
iid,
930-
)
931-
had_write_response = True
932-
setter_responses.setdefault(aid, {})[iid] = set_result_value
933-
934-
if not char.service or (
935-
not acc.setter_callback and not char.service.setter_callback
936-
):
937-
continue
938-
char_to_iid[char] = iid
939-
updates_by_service.setdefault(char.service, {}).update({char: value})
909+
910+
if set_result_value is not None and write_response_requested:
911+
result = {HAP_REPR_STATUS: set_result, HAP_REPR_VALUE: set_result_value}
912+
else:
913+
result = {HAP_REPR_STATUS: set_result}
914+
915+
results[aid][iid] = result
916+
char_to_iid[char] = iid
917+
service = char.service
918+
updates_by_accessories_services[acc][service][char] = value
919+
920+
# Proccess accessory and service level setter callbacks
921+
for acc, updates_by_service in updates_by_accessories_services.items():
922+
aid = acc.aid
923+
aid_results = results[aid]
940924

941925
# Accessory level setter callbacks
926+
acc_set_result = None
942927
if acc.setter_callback:
943-
set_result = _wrap_acc_setter(acc, updates_by_service, client_addr)
944-
if set_result != HAP_SERVER_STATUS.SUCCESS:
945-
had_error = True
946-
for iid in updates[aid]:
947-
setter_results[aid][iid] = set_result
928+
acc_set_result = _wrap_acc_setter(acc, updates_by_service, client_addr)
948929

949930
# Service level setter callbacks
950931
for service, chars in updates_by_service.items():
951-
if not service.setter_callback:
932+
char_set_result = None
933+
if service.setter_callback:
934+
char_set_result = _wrap_service_setter(service, chars, client_addr)
935+
set_result = char_set_result or acc_set_result
936+
937+
if not set_result:
952938
continue
953-
set_result = _wrap_service_setter(service, chars, client_addr)
954-
if set_result != HAP_SERVER_STATUS.SUCCESS:
955-
had_error = True
956-
for char in chars:
957-
setter_results[aid][char_to_iid[char]] = set_result
958939

959-
if not had_error and not had_write_response:
960-
return None
940+
for char in chars:
941+
aid_results[char_to_iid[char]][HAP_REPR_STATUS] = set_result
942+
943+
characteristics = []
944+
nonempty_results_exist = False
945+
for aid, iid_results in results.items():
946+
for iid, result in iid_results.items():
947+
result[HAP_REPR_AID] = aid
948+
result[HAP_REPR_IID] = iid
949+
characteristics.append(result)
950+
if (
951+
result[HAP_REPR_STATUS] != HAP_SERVER_STATUS.SUCCESS
952+
or HAP_REPR_VALUE in result
953+
):
954+
nonempty_results_exist = True
961955

962-
return {
963-
HAP_REPR_CHARS: [
964-
{
965-
HAP_REPR_AID: aid,
966-
HAP_REPR_IID: iid,
967-
HAP_REPR_STATUS: status,
968-
**(
969-
{HAP_REPR_VALUE: setter_responses[aid][iid]}
970-
if setter_responses.get(aid, {}).get(iid, None) is not None
971-
else {}
972-
),
973-
}
974-
for aid, iid_status in setter_results.items()
975-
for iid, status in iid_status.items()
976-
]
977-
}
956+
return {HAP_REPR_CHARS: characteristics} if nonempty_results_exist else None
978957

979958
def prepare(self, prepare_query, client_addr):
980959
"""Called from ``HAPServerHandler`` when iOS wants to prepare a write.
@@ -1017,3 +996,19 @@ def signal_handler(self, _signal, _frame):
1017996
except Exception as e:
1018997
logger.error("Could not stop AccessoryDriver because of error: %s", e)
1019998
raise
999+
1000+
def _notify(
1001+
self, queries: List[Dict[str, Any]], client_addr: Tuple[str, int]
1002+
) -> None:
1003+
"""Notify the driver that the client has subscribed or unsubscribed."""
1004+
for query in queries:
1005+
if HAP_PERMISSION_NOTIFY not in query:
1006+
continue
1007+
aid = query[HAP_REPR_AID]
1008+
iid = query[HAP_REPR_IID]
1009+
ev = query[HAP_PERMISSION_NOTIFY]
1010+
1011+
char_topic = get_topic(aid, iid)
1012+
action = "Subscribed" if ev else "Unsubscribed"
1013+
logger.debug("%s client %s to topic %s", action, client_addr, char_topic)
1014+
self.async_subscribe_client_topic(client_addr, char_topic, ev)

tests/test_accessory_driver.py

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -177,16 +177,7 @@ def setter_with_write_response(value=0):
177177
},
178178
"mock_addr",
179179
)
180-
assert response == {
181-
HAP_REPR_CHARS: [
182-
{
183-
HAP_REPR_AID: acc.aid,
184-
HAP_REPR_IID: char_nfc_access_control_point_iid,
185-
HAP_REPR_STATUS: 0,
186-
HAP_REPR_VALUE: 1
187-
},
188-
]
189-
}
180+
assert response is None
190181

191182
response = driver.set_characteristics(
192183
{
@@ -200,16 +191,7 @@ def setter_with_write_response(value=0):
200191
},
201192
"mock_addr",
202193
)
203-
assert response == {
204-
HAP_REPR_CHARS: [
205-
{
206-
HAP_REPR_AID: acc.aid,
207-
HAP_REPR_IID: char_nfc_access_control_point_iid,
208-
HAP_REPR_STATUS: 0,
209-
HAP_REPR_VALUE: 1
210-
},
211-
]
212-
}
194+
assert response is None
213195

214196

215197
def test_write_response_returned_when_requested(driver: AccessoryDriver):

0 commit comments

Comments
 (0)