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,122 +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
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 ]
874
867
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 )
878
869
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 ] = {}
882
877
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
887
883
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
897
886
898
- if HAP_REPR_VALUE not in cq :
887
+ for query in queries :
888
+ if HAP_REPR_VALUE not in query and not expired :
899
889
continue
900
890
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 )
902
895
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
906
898
else :
907
899
acc = self .accessory .accessories .get (aid )
900
+ char = acc .get_characteristic (aid , iid )
908
901
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
914
904
905
+ if value is not None :
915
906
set_result , set_result_value = _wrap_char_setter (
916
907
char , value , client_addr
917
908
)
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 ]
940
924
941
925
# Accessory level setter callbacks
926
+ acc_set_result = None
942
927
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 )
948
929
949
930
# Service level setter callbacks
950
931
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 :
952
938
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
958
939
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
961
955
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
978
957
979
958
def prepare (self , prepare_query , client_addr ):
980
959
"""Called from ``HAPServerHandler`` when iOS wants to prepare a write.
@@ -1017,3 +996,19 @@ def signal_handler(self, _signal, _frame):
1017
996
except Exception as e :
1018
997
logger .error ("Could not stop AccessoryDriver because of error: %s" , e )
1019
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