pypck 0.9.6__py3-none-any.whl → 0.9.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pypck/device.py +39 -23
- pypck/status_requester.py +51 -104
- {pypck-0.9.6.dist-info → pypck-0.9.8.dist-info}/METADATA +1 -1
- {pypck-0.9.6.dist-info → pypck-0.9.8.dist-info}/RECORD +7 -7
- {pypck-0.9.6.dist-info → pypck-0.9.8.dist-info}/WHEEL +0 -0
- {pypck-0.9.6.dist-info → pypck-0.9.8.dist-info}/licenses/LICENSE +0 -0
- {pypck-0.9.6.dist-info → pypck-0.9.8.dist-info}/top_level.txt +0 -0
pypck/device.py
CHANGED
|
@@ -58,6 +58,7 @@ class DeviceConnection:
|
|
|
58
58
|
|
|
59
59
|
# StatusRequester
|
|
60
60
|
self.status_requester = StatusRequester(self)
|
|
61
|
+
self.request_lock = asyncio.Lock()
|
|
61
62
|
|
|
62
63
|
if self.addr.is_group:
|
|
63
64
|
self.wants_ack = False # groups do not send acks
|
|
@@ -794,11 +795,15 @@ class DeviceConnection:
|
|
|
794
795
|
"groups": {
|
|
795
796
|
"static": sorted(
|
|
796
797
|
addr.addr_id
|
|
797
|
-
for addr in
|
|
798
|
+
for addr in (
|
|
799
|
+
await self.request_group_memberships(dynamic=False) or set()
|
|
800
|
+
)
|
|
798
801
|
),
|
|
799
802
|
"dynamic": sorted(
|
|
800
803
|
addr.addr_id
|
|
801
|
-
for addr in
|
|
804
|
+
for addr in (
|
|
805
|
+
await self.request_group_memberships(dynamic=True) or set()
|
|
806
|
+
)
|
|
802
807
|
),
|
|
803
808
|
},
|
|
804
809
|
}
|
|
@@ -829,7 +834,7 @@ class DeviceConnection:
|
|
|
829
834
|
output_id=output_port.value,
|
|
830
835
|
)
|
|
831
836
|
|
|
832
|
-
return cast(inputs.ModStatusOutput, result)
|
|
837
|
+
return cast(inputs.ModStatusOutput | None, result)
|
|
833
838
|
|
|
834
839
|
async def request_status_relays(
|
|
835
840
|
self, max_age: int = 0
|
|
@@ -845,7 +850,7 @@ class DeviceConnection:
|
|
|
845
850
|
max_age=max_age,
|
|
846
851
|
)
|
|
847
852
|
|
|
848
|
-
return cast(inputs.ModStatusRelays, result)
|
|
853
|
+
return cast(inputs.ModStatusRelays | None, result)
|
|
849
854
|
|
|
850
855
|
async def request_status_motor_position(
|
|
851
856
|
self,
|
|
@@ -879,7 +884,7 @@ class DeviceConnection:
|
|
|
879
884
|
motor=motor.value,
|
|
880
885
|
)
|
|
881
886
|
|
|
882
|
-
return cast(inputs.ModStatusMotorPositionBS4, result)
|
|
887
|
+
return cast(inputs.ModStatusMotorPositionBS4 | None, result)
|
|
883
888
|
|
|
884
889
|
async def request_status_binary_sensors(
|
|
885
890
|
self, max_age: int = 0
|
|
@@ -895,7 +900,7 @@ class DeviceConnection:
|
|
|
895
900
|
max_age=max_age,
|
|
896
901
|
)
|
|
897
902
|
|
|
898
|
-
return cast(inputs.ModStatusBinSensors, result)
|
|
903
|
+
return cast(inputs.ModStatusBinSensors | None, result)
|
|
899
904
|
|
|
900
905
|
async def request_status_variable(
|
|
901
906
|
self,
|
|
@@ -907,13 +912,17 @@ class DeviceConnection:
|
|
|
907
912
|
_LOGGER.info("Status requests are not supported for groups.")
|
|
908
913
|
return None
|
|
909
914
|
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
915
|
+
response_variable = variable
|
|
916
|
+
|
|
917
|
+
# for old modules the variable response is typeless
|
|
918
|
+
# - do not use concurrent requests
|
|
919
|
+
# - do not use buffered response
|
|
920
|
+
if has_typeless_response := not lcn_defs.Var.has_type_in_response(
|
|
921
|
+
variable, self.serials.software_serial
|
|
922
|
+
):
|
|
923
|
+
await self.request_lock.acquire()
|
|
913
924
|
max_age = 0
|
|
914
|
-
|
|
915
|
-
else:
|
|
916
|
-
variable_response = variable
|
|
925
|
+
response_variable = lcn_defs.Var.UNKNOWN
|
|
917
926
|
|
|
918
927
|
result = await self.status_requester.request(
|
|
919
928
|
response_type=inputs.ModStatusVar,
|
|
@@ -921,14 +930,20 @@ class DeviceConnection:
|
|
|
921
930
|
variable, self.serials.software_serial
|
|
922
931
|
),
|
|
923
932
|
max_age=max_age,
|
|
924
|
-
var=
|
|
933
|
+
var=response_variable,
|
|
925
934
|
)
|
|
926
935
|
|
|
927
|
-
result = cast(inputs.ModStatusVar, result)
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
936
|
+
result = cast(inputs.ModStatusVar | None, result)
|
|
937
|
+
|
|
938
|
+
# for old modules (typeless response) we need to set the original variable
|
|
939
|
+
# - call input_callbacks with the original variable type
|
|
940
|
+
if result is not None and has_typeless_response:
|
|
941
|
+
result.var = variable
|
|
942
|
+
for input_callback in self.input_callbacks:
|
|
943
|
+
input_callback(result)
|
|
944
|
+
|
|
945
|
+
if self.request_lock.locked():
|
|
946
|
+
self.request_lock.release()
|
|
932
947
|
return result
|
|
933
948
|
|
|
934
949
|
async def request_status_led_and_logic_ops(
|
|
@@ -945,7 +960,7 @@ class DeviceConnection:
|
|
|
945
960
|
max_age=max_age,
|
|
946
961
|
)
|
|
947
962
|
|
|
948
|
-
return cast(inputs.ModStatusLedsAndLogicOps, result)
|
|
963
|
+
return cast(inputs.ModStatusLedsAndLogicOps | None, result)
|
|
949
964
|
|
|
950
965
|
async def request_status_locked_keys(
|
|
951
966
|
self, max_age: int = 0
|
|
@@ -961,7 +976,7 @@ class DeviceConnection:
|
|
|
961
976
|
max_age=max_age,
|
|
962
977
|
)
|
|
963
978
|
|
|
964
|
-
return cast(inputs.ModStatusKeyLocks, result)
|
|
979
|
+
return cast(inputs.ModStatusKeyLocks | None, result)
|
|
965
980
|
|
|
966
981
|
# Request module properties
|
|
967
982
|
|
|
@@ -1063,7 +1078,7 @@ class DeviceConnection:
|
|
|
1063
1078
|
|
|
1064
1079
|
async def request_group_memberships(
|
|
1065
1080
|
self, dynamic: bool = False, max_age: int = 0
|
|
1066
|
-
) -> set[LcnAddr]:
|
|
1081
|
+
) -> set[LcnAddr] | None:
|
|
1067
1082
|
"""Request module static/dynamic group memberships."""
|
|
1068
1083
|
if self.addr.is_group:
|
|
1069
1084
|
_LOGGER.info("Status requests are not supported for groups.")
|
|
@@ -1079,5 +1094,6 @@ class DeviceConnection:
|
|
|
1079
1094
|
max_age=max_age,
|
|
1080
1095
|
dynamic=dynamic,
|
|
1081
1096
|
)
|
|
1082
|
-
|
|
1083
|
-
|
|
1097
|
+
if result is not None:
|
|
1098
|
+
return set(cast(inputs.ModStatusGroups, result).groups)
|
|
1099
|
+
return None
|
pypck/status_requester.py
CHANGED
|
@@ -32,6 +32,8 @@ class StatusRequest:
|
|
|
32
32
|
class StatusRequester:
|
|
33
33
|
"""Handling of status requests."""
|
|
34
34
|
|
|
35
|
+
current_request: StatusRequest
|
|
36
|
+
|
|
35
37
|
def __init__(
|
|
36
38
|
self,
|
|
37
39
|
device_connection: DeviceConnection,
|
|
@@ -39,69 +41,23 @@ class StatusRequester:
|
|
|
39
41
|
"""Initialize the context."""
|
|
40
42
|
self.device_connection = device_connection
|
|
41
43
|
self.last_requests: set[StatusRequest] = set()
|
|
42
|
-
self.unregister_inputs = self.device_connection.register_for_inputs(
|
|
43
|
-
self.input_callback
|
|
44
|
-
)
|
|
45
44
|
self.max_response_age = self.device_connection.conn.settings["MAX_RESPONSE_AGE"]
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
async def prune_loop(self) -> None:
|
|
49
|
-
"""Periodically prune old status requests."""
|
|
50
|
-
while True:
|
|
51
|
-
await asyncio.sleep(self.max_response_age)
|
|
52
|
-
self.prune_status_requests()
|
|
53
|
-
|
|
54
|
-
def prune_status_requests(self) -> None:
|
|
55
|
-
"""Prune old status requests."""
|
|
56
|
-
entries_to_remove = {
|
|
57
|
-
request
|
|
58
|
-
for request in self.last_requests
|
|
59
|
-
if asyncio.get_running_loop().time() - request.timestamp
|
|
60
|
-
> self.max_response_age
|
|
61
|
-
}
|
|
62
|
-
for entry in entries_to_remove:
|
|
63
|
-
entry.response.cancel()
|
|
64
|
-
self.last_requests.difference_update(entries_to_remove)
|
|
65
|
-
|
|
66
|
-
def get_status_requests(
|
|
67
|
-
self,
|
|
68
|
-
request_type: type[inputs.Input],
|
|
69
|
-
parameters: frozenset[tuple[str, Any]] | None = None,
|
|
70
|
-
max_age: int = 0,
|
|
71
|
-
) -> list[StatusRequest]:
|
|
72
|
-
"""Get the status requests for the given type and parameters."""
|
|
73
|
-
if parameters is None:
|
|
74
|
-
parameters = frozenset()
|
|
75
|
-
loop = asyncio.get_running_loop()
|
|
76
|
-
results = [
|
|
77
|
-
request
|
|
78
|
-
for request in self.last_requests
|
|
79
|
-
if request.type == request_type
|
|
80
|
-
and parameters.issubset(request.parameters)
|
|
81
|
-
and (
|
|
82
|
-
(request.timestamp == -1)
|
|
83
|
-
or (max_age == -1)
|
|
84
|
-
or (loop.time() - request.timestamp < max_age)
|
|
85
|
-
)
|
|
86
|
-
]
|
|
87
|
-
results.sort(key=lambda request: request.timestamp, reverse=True)
|
|
88
|
-
return results
|
|
45
|
+
self.request_lock = asyncio.Lock()
|
|
89
46
|
|
|
90
47
|
def input_callback(self, inp: inputs.Input) -> None:
|
|
91
48
|
"""Handle incoming inputs and set the result for the corresponding requests."""
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
request.response.set_result(inp)
|
|
49
|
+
if (
|
|
50
|
+
self.current_request.response.done()
|
|
51
|
+
or self.current_request.response.cancelled()
|
|
52
|
+
):
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
if isinstance(inp, self.current_request.type) and all(
|
|
56
|
+
getattr(inp, parameter_name) == parameter_value
|
|
57
|
+
for parameter_name, parameter_value in self.current_request.parameters
|
|
58
|
+
):
|
|
59
|
+
self.current_request.timestamp = asyncio.get_running_loop().time()
|
|
60
|
+
self.current_request.response.set_result(inp)
|
|
105
61
|
|
|
106
62
|
async def request(
|
|
107
63
|
self,
|
|
@@ -112,49 +68,40 @@ class StatusRequester:
|
|
|
112
68
|
**request_kwargs: Any,
|
|
113
69
|
) -> inputs.Input | None:
|
|
114
70
|
"""Execute a status request and wait for the response."""
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
try:
|
|
145
|
-
async with asyncio.timeout(
|
|
146
|
-
self.device_connection.conn.settings["DEFAULT_TIMEOUT"]
|
|
147
|
-
):
|
|
148
|
-
# Need to shield the future. Otherwise it would get cancelled.
|
|
149
|
-
result = await asyncio.shield(request.response)
|
|
71
|
+
async with self.request_lock:
|
|
72
|
+
self.current_request = StatusRequest(
|
|
73
|
+
response_type,
|
|
74
|
+
frozenset(request_kwargs.items()),
|
|
75
|
+
-1,
|
|
76
|
+
asyncio.get_running_loop().create_future(),
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
unregister_inputs = self.device_connection.register_for_inputs(
|
|
80
|
+
self.input_callback
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
result = None
|
|
84
|
+
# send the request up to NUM_TRIES and wait for response future completion
|
|
85
|
+
for _ in range(self.device_connection.conn.settings["NUM_TRIES"]):
|
|
86
|
+
await self.device_connection.send_command(
|
|
87
|
+
request_acknowledge, request_pck
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
async with asyncio.timeout(
|
|
92
|
+
self.device_connection.conn.settings["DEFAULT_TIMEOUT"]
|
|
93
|
+
):
|
|
94
|
+
# Need to shield the future. Otherwise it would get cancelled.
|
|
95
|
+
result = await asyncio.shield(self.current_request.response)
|
|
96
|
+
break
|
|
97
|
+
except asyncio.TimeoutError:
|
|
98
|
+
continue
|
|
99
|
+
except asyncio.CancelledError:
|
|
150
100
|
break
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
request.response.cancel()
|
|
159
|
-
self.last_requests.discard(request)
|
|
160
|
-
return result
|
|
101
|
+
|
|
102
|
+
# if we got no results, remove the request from the set
|
|
103
|
+
if result is None:
|
|
104
|
+
self.current_request.response.cancel()
|
|
105
|
+
|
|
106
|
+
unregister_inputs()
|
|
107
|
+
return result
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
pypck/__init__.py,sha256=jVx-aBsV_LmBf6jiivMrMcBUofC_AOseywDafgOzAS4,323
|
|
2
2
|
pypck/connection.py,sha256=n3itRe8oQtw64vyWGYhl6j4QJC6wgeeHitBSn-Cl2_4,23330
|
|
3
|
-
pypck/device.py,sha256=
|
|
3
|
+
pypck/device.py,sha256=m-JUdea-yzsGMqGZEKvTQkxUpjlaUwR3KKMCFi35RYs,40103
|
|
4
4
|
pypck/helpers.py,sha256=_5doqIsSRpqdQNPIUsjFh813xKGuMuEFY6sNGobJGIk,1280
|
|
5
5
|
pypck/inputs.py,sha256=F7E8rprIhYzZnHARozt_hguYNgJaiNP3htrZ2E3Qa5I,45951
|
|
6
6
|
pypck/lcn_addr.py,sha256=N2Od8KuANOglqKjf596hJVH1SRcG7MhESKA5YYlDnbw,1946
|
|
7
7
|
pypck/lcn_defs.py,sha256=wSceYBwM46NqPwvff1hi8RluqUECmNY1gNcm1kDKTaI,43356
|
|
8
8
|
pypck/pck_commands.py,sha256=eJxmh2e8EbKGpek97L2961Kr_nVfT8rKgJCN3YgjIQM,50458
|
|
9
9
|
pypck/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
-
pypck/status_requester.py,sha256=
|
|
11
|
-
pypck-0.9.
|
|
12
|
-
pypck-0.9.
|
|
13
|
-
pypck-0.9.
|
|
14
|
-
pypck-0.9.
|
|
15
|
-
pypck-0.9.
|
|
10
|
+
pypck/status_requester.py,sha256=70RETS9tetq7eMRainQ1xws7ziK5rWnhkc3BtUUcqEw,3693
|
|
11
|
+
pypck-0.9.8.dist-info/licenses/LICENSE,sha256=iYB6zyMJvShfAzQE7nhYFgLzzZuBmhasLw5fYP9KRz4,1023
|
|
12
|
+
pypck-0.9.8.dist-info/METADATA,sha256=S3pUkmBRAvz0Qbw9-brGEvV96g9JgRJFmXqJaSqNwM0,2682
|
|
13
|
+
pypck-0.9.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
14
|
+
pypck-0.9.8.dist-info/top_level.txt,sha256=59ried49iFueDa5mQ_5BGVZcESjjzi4MZZKLcganvQA,6
|
|
15
|
+
pypck-0.9.8.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|