pypck 0.9.7__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 +41 -46
- pypck/status_requester.py +51 -104
- {pypck-0.9.7.dist-info → pypck-0.9.8.dist-info}/METADATA +1 -1
- {pypck-0.9.7.dist-info → pypck-0.9.8.dist-info}/RECORD +7 -7
- {pypck-0.9.7.dist-info → pypck-0.9.8.dist-info}/WHEEL +0 -0
- {pypck-0.9.7.dist-info → pypck-0.9.8.dist-info}/licenses/LICENSE +0 -0
- {pypck-0.9.7.dist-info → pypck-0.9.8.dist-info}/top_level.txt +0 -0
pypck/device.py
CHANGED
|
@@ -50,8 +50,6 @@ class DeviceConnection:
|
|
|
50
50
|
self._serials_known = asyncio.Event()
|
|
51
51
|
|
|
52
52
|
self.input_callbacks: set[Callable[[inputs.Input], None]] = set()
|
|
53
|
-
self.last_requested_var_without_type_in_response = lcn_defs.Var.UNKNOWN
|
|
54
|
-
self.last_var_lock = asyncio.Lock()
|
|
55
53
|
|
|
56
54
|
# List of queued acknowledge codes from the LCN modules.
|
|
57
55
|
self.acknowledges: asyncio.Queue[lcn_defs.AcknowledgeErrorCode] = (
|
|
@@ -60,6 +58,7 @@ class DeviceConnection:
|
|
|
60
58
|
|
|
61
59
|
# StatusRequester
|
|
62
60
|
self.status_requester = StatusRequester(self)
|
|
61
|
+
self.request_lock = asyncio.Lock()
|
|
63
62
|
|
|
64
63
|
if self.addr.is_group:
|
|
65
64
|
self.wants_ack = False # groups do not send acks
|
|
@@ -773,29 +772,9 @@ class DeviceConnection:
|
|
|
773
772
|
await self.on_ack(inp.code)
|
|
774
773
|
return None
|
|
775
774
|
|
|
776
|
-
# handle typeless variable responses
|
|
777
|
-
if isinstance(inp, inputs.ModStatusVar):
|
|
778
|
-
inp = self.preprocess_modstatusvar(inp)
|
|
779
|
-
|
|
780
775
|
for input_callback in self.input_callbacks:
|
|
781
776
|
input_callback(inp)
|
|
782
777
|
|
|
783
|
-
def preprocess_modstatusvar(self, inp: inputs.ModStatusVar) -> inputs.Input:
|
|
784
|
-
"""Fill typeless response with last requested variable type."""
|
|
785
|
-
if inp.orig_var == lcn_defs.Var.UNKNOWN:
|
|
786
|
-
# Response without type (%Msssaaa.wwwww)
|
|
787
|
-
inp.var = self.last_requested_var_without_type_in_response
|
|
788
|
-
|
|
789
|
-
self.last_requested_var_without_type_in_response = lcn_defs.Var.UNKNOWN
|
|
790
|
-
|
|
791
|
-
if self.last_var_lock.locked():
|
|
792
|
-
self.last_var_lock.release()
|
|
793
|
-
else:
|
|
794
|
-
# Response with variable type (%Msssaaa.Avvvwww)
|
|
795
|
-
inp.var = inp.orig_var
|
|
796
|
-
|
|
797
|
-
return inp
|
|
798
|
-
|
|
799
778
|
async def dump_details(self) -> dict[str, Any]:
|
|
800
779
|
"""Dump detailed information about this module."""
|
|
801
780
|
is_local_segment = self.addr.seg_id in (0, self.conn.local_seg_id)
|
|
@@ -816,11 +795,15 @@ class DeviceConnection:
|
|
|
816
795
|
"groups": {
|
|
817
796
|
"static": sorted(
|
|
818
797
|
addr.addr_id
|
|
819
|
-
for addr in
|
|
798
|
+
for addr in (
|
|
799
|
+
await self.request_group_memberships(dynamic=False) or set()
|
|
800
|
+
)
|
|
820
801
|
),
|
|
821
802
|
"dynamic": sorted(
|
|
822
803
|
addr.addr_id
|
|
823
|
-
for addr in
|
|
804
|
+
for addr in (
|
|
805
|
+
await self.request_group_memberships(dynamic=True) or set()
|
|
806
|
+
)
|
|
824
807
|
),
|
|
825
808
|
},
|
|
826
809
|
}
|
|
@@ -851,7 +834,7 @@ class DeviceConnection:
|
|
|
851
834
|
output_id=output_port.value,
|
|
852
835
|
)
|
|
853
836
|
|
|
854
|
-
return cast(inputs.ModStatusOutput, result)
|
|
837
|
+
return cast(inputs.ModStatusOutput | None, result)
|
|
855
838
|
|
|
856
839
|
async def request_status_relays(
|
|
857
840
|
self, max_age: int = 0
|
|
@@ -867,7 +850,7 @@ class DeviceConnection:
|
|
|
867
850
|
max_age=max_age,
|
|
868
851
|
)
|
|
869
852
|
|
|
870
|
-
return cast(inputs.ModStatusRelays, result)
|
|
853
|
+
return cast(inputs.ModStatusRelays | None, result)
|
|
871
854
|
|
|
872
855
|
async def request_status_motor_position(
|
|
873
856
|
self,
|
|
@@ -901,7 +884,7 @@ class DeviceConnection:
|
|
|
901
884
|
motor=motor.value,
|
|
902
885
|
)
|
|
903
886
|
|
|
904
|
-
return cast(inputs.ModStatusMotorPositionBS4, result)
|
|
887
|
+
return cast(inputs.ModStatusMotorPositionBS4 | None, result)
|
|
905
888
|
|
|
906
889
|
async def request_status_binary_sensors(
|
|
907
890
|
self, max_age: int = 0
|
|
@@ -917,7 +900,7 @@ class DeviceConnection:
|
|
|
917
900
|
max_age=max_age,
|
|
918
901
|
)
|
|
919
902
|
|
|
920
|
-
return cast(inputs.ModStatusBinSensors, result)
|
|
903
|
+
return cast(inputs.ModStatusBinSensors | None, result)
|
|
921
904
|
|
|
922
905
|
async def request_status_variable(
|
|
923
906
|
self,
|
|
@@ -929,17 +912,17 @@ class DeviceConnection:
|
|
|
929
912
|
_LOGGER.info("Status requests are not supported for groups.")
|
|
930
913
|
return None
|
|
931
914
|
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
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()
|
|
924
|
+
max_age = 0
|
|
925
|
+
response_variable = lcn_defs.Var.UNKNOWN
|
|
943
926
|
|
|
944
927
|
result = await self.status_requester.request(
|
|
945
928
|
response_type=inputs.ModStatusVar,
|
|
@@ -947,10 +930,21 @@ class DeviceConnection:
|
|
|
947
930
|
variable, self.serials.software_serial
|
|
948
931
|
),
|
|
949
932
|
max_age=max_age,
|
|
950
|
-
var=
|
|
933
|
+
var=response_variable,
|
|
951
934
|
)
|
|
952
935
|
|
|
953
|
-
|
|
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()
|
|
947
|
+
return result
|
|
954
948
|
|
|
955
949
|
async def request_status_led_and_logic_ops(
|
|
956
950
|
self, max_age: int = 0
|
|
@@ -966,7 +960,7 @@ class DeviceConnection:
|
|
|
966
960
|
max_age=max_age,
|
|
967
961
|
)
|
|
968
962
|
|
|
969
|
-
return cast(inputs.ModStatusLedsAndLogicOps, result)
|
|
963
|
+
return cast(inputs.ModStatusLedsAndLogicOps | None, result)
|
|
970
964
|
|
|
971
965
|
async def request_status_locked_keys(
|
|
972
966
|
self, max_age: int = 0
|
|
@@ -982,7 +976,7 @@ class DeviceConnection:
|
|
|
982
976
|
max_age=max_age,
|
|
983
977
|
)
|
|
984
978
|
|
|
985
|
-
return cast(inputs.ModStatusKeyLocks, result)
|
|
979
|
+
return cast(inputs.ModStatusKeyLocks | None, result)
|
|
986
980
|
|
|
987
981
|
# Request module properties
|
|
988
982
|
|
|
@@ -1084,7 +1078,7 @@ class DeviceConnection:
|
|
|
1084
1078
|
|
|
1085
1079
|
async def request_group_memberships(
|
|
1086
1080
|
self, dynamic: bool = False, max_age: int = 0
|
|
1087
|
-
) -> set[LcnAddr]:
|
|
1081
|
+
) -> set[LcnAddr] | None:
|
|
1088
1082
|
"""Request module static/dynamic group memberships."""
|
|
1089
1083
|
if self.addr.is_group:
|
|
1090
1084
|
_LOGGER.info("Status requests are not supported for groups.")
|
|
@@ -1100,5 +1094,6 @@ class DeviceConnection:
|
|
|
1100
1094
|
max_age=max_age,
|
|
1101
1095
|
dynamic=dynamic,
|
|
1102
1096
|
)
|
|
1103
|
-
|
|
1104
|
-
|
|
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
|