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 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 await self.request_group_memberships(dynamic=False)
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 await self.request_group_memberships(dynamic=True)
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
- # do not use buffered response for old modules
911
- # (variable response is typeless)
912
- if self.serials.software_serial < 0x170206:
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
- variable_response = lcn_defs.Var.UNKNOWN
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=variable_response,
933
+ var=response_variable,
925
934
  )
926
935
 
927
- result = cast(inputs.ModStatusVar, result)
928
- if result:
929
- if result.orig_var == lcn_defs.Var.UNKNOWN:
930
- # Response without type (%Msssaaa.wwwww)
931
- result.var = variable
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
- return set(cast(inputs.ModStatusGroups, result).groups)
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
- # asyncio.get_running_loop().create_task(self.prune_loop())
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
- requests = [
93
- request
94
- for request in self.get_status_requests(type(inp))
95
- if all(
96
- getattr(inp, parameter_name) == parameter_value
97
- for parameter_name, parameter_value in request.parameters
98
- )
99
- ]
100
- for request in requests:
101
- if request.response.done() or request.response.cancelled():
102
- continue
103
- request.timestamp = asyncio.get_running_loop().time()
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
- parameters = frozenset(request_kwargs.items())
116
-
117
- # check if we already have a received response for the current request
118
- if requests := self.get_status_requests(response_type, parameters, max_age):
119
- try:
120
- async with asyncio.timeout(
121
- self.device_connection.conn.settings["DEFAULT_TIMEOUT"]
122
- ):
123
- return await requests[0].response
124
- except asyncio.TimeoutError:
125
- return None
126
- except asyncio.CancelledError:
127
- return None
128
-
129
- # no stored request or forced request: set up a new request
130
- request = StatusRequest(
131
- response_type,
132
- frozenset(request_kwargs.items()),
133
- -1,
134
- asyncio.get_running_loop().create_future(),
135
- )
136
-
137
- self.last_requests.discard(request)
138
- self.last_requests.add(request)
139
- result = None
140
- # send the request up to NUM_TRIES and wait for response future completion
141
- for _ in range(self.device_connection.conn.settings["NUM_TRIES"]):
142
- await self.device_connection.send_command(request_acknowledge, request_pck)
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
- except asyncio.TimeoutError:
152
- continue
153
- except asyncio.CancelledError:
154
- break
155
-
156
- # if we got no results, remove the request from the set
157
- if result is None:
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pypck
3
- Version: 0.9.6
3
+ Version: 0.9.8
4
4
  Summary: LCN-PCK library
5
5
  Home-page: https://github.com/alengwenus/pypck
6
6
  Author-email: Andre Lengwenus <alengwenus@gmail.com>
@@ -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=LmoqAyvQD98abCOFgMmIC9-gU6HPq3hHB9uY4wzN1ZY,39430
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=10N5pbIBe_Ao-9ui_D7mCavk21BYZs9c-kxcTtmi-FI,5721
11
- pypck-0.9.6.dist-info/licenses/LICENSE,sha256=iYB6zyMJvShfAzQE7nhYFgLzzZuBmhasLw5fYP9KRz4,1023
12
- pypck-0.9.6.dist-info/METADATA,sha256=D_SIiIwNkfit6m85o_Bc_CIerldSLAZp-qnIiYW5NLQ,2682
13
- pypck-0.9.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
- pypck-0.9.6.dist-info/top_level.txt,sha256=59ried49iFueDa5mQ_5BGVZcESjjzi4MZZKLcganvQA,6
15
- pypck-0.9.6.dist-info/RECORD,,
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