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 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 await self.request_group_memberships(dynamic=False)
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 await self.request_group_memberships(dynamic=True)
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
- # do not use buffered response for old modules
933
- # (variable response is typeless)
934
- if self.serials.software_serial < 0x170206:
935
- if not lcn_defs.Var.has_type_in_response(
936
- variable, self.serials.software_serial
937
- ):
938
- try:
939
- await asyncio.wait_for(self.last_var_lock.acquire(), timeout=3.0)
940
- except asyncio.TimeoutError:
941
- pass
942
- self.last_requested_var_without_type_in_response = variable
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=variable,
933
+ var=response_variable,
951
934
  )
952
935
 
953
- return cast(inputs.ModStatusVar, result)
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
- 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.7
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=Ek-Zy4Id63vl43oW1pSafbDqRAfidhgoJI0F4EMWCXY,40375
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.7.dist-info/licenses/LICENSE,sha256=iYB6zyMJvShfAzQE7nhYFgLzzZuBmhasLw5fYP9KRz4,1023
12
- pypck-0.9.7.dist-info/METADATA,sha256=gooJ1CbK2mtAKuIEs8v3OKV_sDMHziMbmKHGPcEvsmE,2682
13
- pypck-0.9.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
- pypck-0.9.7.dist-info/top_level.txt,sha256=59ried49iFueDa5mQ_5BGVZcESjjzi4MZZKLcganvQA,6
15
- pypck-0.9.7.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