17
17
"""
18
18
import asyncio
19
19
import base64
20
+ from collections import defaultdict
20
21
from concurrent .futures import ThreadPoolExecutor
21
22
import hashlib
22
23
import logging
26
27
import tempfile
27
28
import threading
28
29
import time
29
- from typing import Optional
30
+ from typing import Any , Dict , List , Optional , Tuple
30
31
31
32
from zeroconf import ServiceInfo
32
33
from zeroconf .asyncio import AsyncZeroconf
33
34
34
35
from pyhap import util
35
36
from pyhap .accessory import Accessory , get_topic
36
- from pyhap .characteristic import CharacteristicError
37
+ from pyhap .characteristic import Characteristic , CharacteristicError
37
38
from pyhap .const import (
38
39
HAP_PERMISSION_NOTIFY ,
39
40
HAP_PROTOCOL_SHORT_VERSION ,
53
54
from pyhap .hsrp import Server as SrpServer
54
55
from pyhap .loader import Loader
55
56
from pyhap .params import get_srp_context
57
+ from pyhap .service import Service
56
58
from pyhap .state import State
57
59
58
60
from .const import HAP_SERVER_STATUS
67
69
VALID_MDNS_REGEX = re .compile (r"[^A-Za-z0-9\-]+" )
68
70
LEADING_TRAILING_SPACE_DASH = re .compile (r"^[ -]+|[ -]+$" )
69
71
DASH_REGEX = re .compile (r"[-]+" )
72
+ KEYS_TO_EXCLUDE = {HAP_REPR_IID , HAP_REPR_AID }
70
73
71
74
72
75
def _wrap_char_setter (char , value , client_addr ):
73
76
"""Process an characteristic setter callback trapping and logging all exceptions."""
74
77
try :
75
- result = char .client_update_value (value , client_addr )
78
+ response = char .client_update_value (value , client_addr )
76
79
except Exception : # pylint: disable=broad-except
77
80
logger .exception (
78
81
"%s: Error while setting characteristic %s to %s" ,
@@ -81,7 +84,7 @@ def _wrap_char_setter(char, value, client_addr):
81
84
value ,
82
85
)
83
86
return HAP_SERVER_STATUS .SERVICE_COMMUNICATION_FAILURE , None
84
- return HAP_SERVER_STATUS .SUCCESS , result
87
+ return HAP_SERVER_STATUS .SUCCESS , response
85
88
86
89
87
90
def _wrap_acc_setter (acc , updates_by_service , client_addr ):
@@ -859,118 +862,98 @@ def set_characteristics(self, chars_query, client_addr):
859
862
:type chars_query: dict
860
863
"""
861
864
# 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
868
865
866
+ queries : List [Dict [str , Any ]] = chars_query [HAP_REPR_CHARS ]
867
+
868
+ self ._notify (queries , client_addr )
869
+
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 ] = {}
877
+
878
+ expired = False
869
879
if HAP_REPR_PID in chars_query :
870
880
pid = chars_query [HAP_REPR_PID ]
871
881
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
874
-
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 , {})
882
+ expired = expire_time is None or time .time () > expire_time
878
883
879
- if HAP_REPR_WRITE_RESPONSE in cq :
880
- setter_responses .setdefault (aid , {})
881
- had_write_response = True
884
+ primary_accessory = self .accessory
885
+ primary_aid = primary_accessory .aid
882
886
883
- if expired :
884
- setter_results [aid ][iid ] = HAP_SERVER_STATUS .INVALID_VALUE_IN_REQUEST
885
- had_error = True
887
+ for query in queries :
888
+ if HAP_REPR_VALUE not in query and not expired :
886
889
continue
887
890
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
- )
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 )
897
895
898
- if HAP_REPR_VALUE not in cq :
899
- continue
896
+ if aid == primary_aid :
897
+ acc = primary_accessory
898
+ else :
899
+ acc = self .accessory .accessories .get (aid )
900
+ char = acc .get_characteristic (aid , iid )
901
+
902
+ set_result = HAP_SERVER_STATUS .INVALID_VALUE_IN_REQUEST
903
+ set_result_value = None
900
904
901
- updates .setdefault (aid , {})[iid ] = cq [HAP_REPR_VALUE ]
905
+ if value is not None :
906
+ set_result , set_result_value = _wrap_char_setter (
907
+ char , value , client_addr
908
+ )
902
909
903
- for aid , new_iid_values in updates .items ():
904
- if self .accessory .aid == aid :
905
- acc = self .accessory
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 }
906
912
else :
907
- acc = self . accessory . accessories . get ( aid )
913
+ result = { HAP_REPR_STATUS : set_result }
908
914
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 )
914
-
915
- set_result , set_result_value = _wrap_char_setter (char , value , client_addr )
916
- if set_result != HAP_SERVER_STATUS .SUCCESS :
917
- had_error = True
918
-
919
- setter_results [aid ][iid ] = set_result
920
-
921
- if set_result_value is not None :
922
- if setter_responses .get (aid , None ) is None :
923
- logger .warning (
924
- "Returning write response '%s' when it wasn't requested for %s %s" ,
925
- set_result_value , aid , iid
926
- )
927
- had_write_response = True
928
- setter_responses .setdefault (aid , {})[iid ] = set_result_value
929
-
930
- if not char .service or (
931
- not acc .setter_callback and not char .service .setter_callback
932
- ):
933
- continue
934
- char_to_iid [char ] = iid
935
- updates_by_service .setdefault (char .service , {}).update ({char : value })
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 ]
936
924
937
925
# Accessory level setter callbacks
926
+ acc_set_result = None
938
927
if acc .setter_callback :
939
- set_result = _wrap_acc_setter (acc , updates_by_service , client_addr )
940
- if set_result != HAP_SERVER_STATUS .SUCCESS :
941
- had_error = True
942
- for iid in updates [aid ]:
943
- setter_results [aid ][iid ] = set_result
928
+ acc_set_result = _wrap_acc_setter (acc , updates_by_service , client_addr )
944
929
945
930
# Service level setter callbacks
946
931
for service , chars in updates_by_service .items ():
947
- 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 :
948
938
continue
949
- set_result = _wrap_service_setter (service , chars , client_addr )
950
- if set_result != HAP_SERVER_STATUS .SUCCESS :
951
- had_error = True
952
- for char in chars :
953
- setter_results [aid ][char_to_iid [char ]] = set_result
954
939
955
- if not had_error and not had_write_response :
956
- 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
957
955
958
- return {
959
- HAP_REPR_CHARS : [
960
- {
961
- HAP_REPR_AID : aid ,
962
- HAP_REPR_IID : iid ,
963
- HAP_REPR_STATUS : status ,
964
- ** (
965
- {HAP_REPR_VALUE : setter_responses [aid ][iid ]}
966
- if setter_responses .get (aid , {}).get (iid , None ) is not None
967
- else {}
968
- )
969
- }
970
- for aid , iid_status in setter_results .items ()
971
- for iid , status in iid_status .items ()
972
- ]
973
- }
956
+ return {HAP_REPR_CHARS : characteristics } if nonempty_results_exist else None
974
957
975
958
def prepare (self , prepare_query , client_addr ):
976
959
"""Called from ``HAPServerHandler`` when iOS wants to prepare a write.
@@ -1013,3 +996,19 @@ def signal_handler(self, _signal, _frame):
1013
996
except Exception as e :
1014
997
logger .error ("Could not stop AccessoryDriver because of error: %s" , e )
1015
998
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 )
0 commit comments