pypck 0.9.8__tar.gz → 0.9.9__tar.gz

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.
Files changed (31) hide show
  1. {pypck-0.9.8/pypck.egg-info → pypck-0.9.9}/PKG-INFO +1 -1
  2. pypck-0.9.9/VERSION +1 -0
  3. {pypck-0.9.8 → pypck-0.9.9}/pypck/device.py +32 -49
  4. {pypck-0.9.8 → pypck-0.9.9}/pypck/inputs.py +6 -3
  5. {pypck-0.9.8 → pypck-0.9.9}/pypck/pck_commands.py +4 -4
  6. pypck-0.9.9/pypck/status_requester.py +155 -0
  7. {pypck-0.9.8 → pypck-0.9.9/pypck.egg-info}/PKG-INFO +1 -1
  8. {pypck-0.9.8 → pypck-0.9.9}/tests/test_commands.py +4 -4
  9. {pypck-0.9.8 → pypck-0.9.9}/tests/test_messages.py +1 -1
  10. {pypck-0.9.8 → pypck-0.9.9}/tests/test_status_requester.py +28 -27
  11. pypck-0.9.8/VERSION +0 -1
  12. pypck-0.9.8/pypck/status_requester.py +0 -107
  13. {pypck-0.9.8 → pypck-0.9.9}/LICENSE +0 -0
  14. {pypck-0.9.8 → pypck-0.9.9}/README.md +0 -0
  15. {pypck-0.9.8 → pypck-0.9.9}/pypck/__init__.py +0 -0
  16. {pypck-0.9.8 → pypck-0.9.9}/pypck/connection.py +0 -0
  17. {pypck-0.9.8 → pypck-0.9.9}/pypck/helpers.py +0 -0
  18. {pypck-0.9.8 → pypck-0.9.9}/pypck/lcn_addr.py +0 -0
  19. {pypck-0.9.8 → pypck-0.9.9}/pypck/lcn_defs.py +0 -0
  20. {pypck-0.9.8 → pypck-0.9.9}/pypck/py.typed +0 -0
  21. {pypck-0.9.8 → pypck-0.9.9}/pypck.egg-info/SOURCES.txt +0 -0
  22. {pypck-0.9.8 → pypck-0.9.9}/pypck.egg-info/dependency_links.txt +0 -0
  23. {pypck-0.9.8 → pypck-0.9.9}/pypck.egg-info/not-zip-safe +0 -0
  24. {pypck-0.9.8 → pypck-0.9.9}/pypck.egg-info/top_level.txt +0 -0
  25. {pypck-0.9.8 → pypck-0.9.9}/pyproject.toml +0 -0
  26. {pypck-0.9.8 → pypck-0.9.9}/setup.cfg +0 -0
  27. {pypck-0.9.8 → pypck-0.9.9}/tests/test_connection.py +0 -0
  28. {pypck-0.9.8 → pypck-0.9.9}/tests/test_dyn_text.py +0 -0
  29. {pypck-0.9.8 → pypck-0.9.9}/tests/test_input.py +0 -0
  30. {pypck-0.9.8 → pypck-0.9.9}/tests/test_module.py +0 -0
  31. {pypck-0.9.8 → pypck-0.9.9}/tests/test_vars.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pypck
3
- Version: 0.9.8
3
+ Version: 0.9.9
4
4
  Summary: LCN-PCK library
5
5
  Home-page: https://github.com/alengwenus/pypck
6
6
  Author-email: Andre Lengwenus <alengwenus@gmail.com>
pypck-0.9.9/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.9.9
@@ -827,15 +827,13 @@ class DeviceConnection:
827
827
  _LOGGER.info("Status requests are not supported for groups.")
828
828
  return None
829
829
 
830
- result = await self.status_requester.request(
830
+ return await self.status_requester.request(
831
831
  response_type=inputs.ModStatusOutput,
832
832
  request_pck=PckGenerator.request_output_status(output_id=output_port.value),
833
833
  max_age=max_age,
834
834
  output_id=output_port.value,
835
835
  )
836
836
 
837
- return cast(inputs.ModStatusOutput | None, result)
838
-
839
837
  async def request_status_relays(
840
838
  self, max_age: int = 0
841
839
  ) -> inputs.ModStatusRelays | None:
@@ -844,14 +842,12 @@ class DeviceConnection:
844
842
  _LOGGER.info("Status requests are not supported for groups.")
845
843
  return None
846
844
 
847
- result = await self.status_requester.request(
845
+ return await self.status_requester.request(
848
846
  response_type=inputs.ModStatusRelays,
849
847
  request_pck=PckGenerator.request_relays_status(),
850
848
  max_age=max_age,
851
849
  )
852
850
 
853
- return cast(inputs.ModStatusRelays | None, result)
854
-
855
851
  async def request_status_motor_position(
856
852
  self,
857
853
  motor: lcn_defs.MotorPort,
@@ -877,15 +873,13 @@ class DeviceConnection:
877
873
  _LOGGER.debug("Only BS4 mode is supported for motor position requests.")
878
874
  return None
879
875
 
880
- result = await self.status_requester.request(
876
+ return await self.status_requester.request(
881
877
  response_type=inputs.ModStatusMotorPositionBS4,
882
878
  request_pck=PckGenerator.request_motor_position_status(motor.value // 2),
883
879
  max_age=max_age,
884
880
  motor=motor.value,
885
881
  )
886
882
 
887
- return cast(inputs.ModStatusMotorPositionBS4 | None, result)
888
-
889
883
  async def request_status_binary_sensors(
890
884
  self, max_age: int = 0
891
885
  ) -> inputs.ModStatusBinSensors | None:
@@ -894,14 +888,12 @@ class DeviceConnection:
894
888
  _LOGGER.info("Status requests are not supported for groups.")
895
889
  return None
896
890
 
897
- result = await self.status_requester.request(
891
+ return await self.status_requester.request(
898
892
  response_type=inputs.ModStatusBinSensors,
899
893
  request_pck=PckGenerator.request_bin_sensors_status(),
900
894
  max_age=max_age,
901
895
  )
902
896
 
903
- return cast(inputs.ModStatusBinSensors | None, result)
904
-
905
897
  async def request_status_variable(
906
898
  self,
907
899
  variable: lcn_defs.Var,
@@ -933,8 +925,6 @@ class DeviceConnection:
933
925
  var=response_variable,
934
926
  )
935
927
 
936
- result = cast(inputs.ModStatusVar | None, result)
937
-
938
928
  # for old modules (typeless response) we need to set the original variable
939
929
  # - call input_callbacks with the original variable type
940
930
  if result is not None and has_typeless_response:
@@ -954,14 +944,12 @@ class DeviceConnection:
954
944
  _LOGGER.info("Status requests are not supported for groups.")
955
945
  return None
956
946
 
957
- result = await self.status_requester.request(
947
+ return await self.status_requester.request(
958
948
  response_type=inputs.ModStatusLedsAndLogicOps,
959
949
  request_pck=PckGenerator.request_leds_and_logic_ops(),
960
950
  max_age=max_age,
961
951
  )
962
952
 
963
- return cast(inputs.ModStatusLedsAndLogicOps | None, result)
964
-
965
953
  async def request_status_locked_keys(
966
954
  self, max_age: int = 0
967
955
  ) -> inputs.ModStatusKeyLocks | None:
@@ -970,14 +958,12 @@ class DeviceConnection:
970
958
  _LOGGER.info("Status requests are not supported for groups.")
971
959
  return None
972
960
 
973
- result = await self.status_requester.request(
961
+ return await self.status_requester.request(
974
962
  response_type=inputs.ModStatusKeyLocks,
975
963
  request_pck=PckGenerator.request_key_lock_status(),
976
964
  max_age=max_age,
977
965
  )
978
966
 
979
- return cast(inputs.ModStatusKeyLocks | None, result)
980
-
981
967
  # Request module properties
982
968
 
983
969
  async def request_serials(self, max_age: int = 0) -> Serials:
@@ -986,23 +972,20 @@ class DeviceConnection:
986
972
  _LOGGER.info("Status requests are not supported for groups.")
987
973
  return Serials(-1, -1, -1, lcn_defs.HardwareType.UNKNOWN)
988
974
 
989
- result = cast(
990
- inputs.ModSn | None,
991
- await self.status_requester.request(
992
- response_type=inputs.ModSn,
993
- request_pck=PckGenerator.request_serial(),
994
- max_age=max_age,
995
- ),
975
+ result = await self.status_requester.request(
976
+ response_type=inputs.ModSn,
977
+ request_pck=PckGenerator.request_serial(),
978
+ max_age=max_age,
996
979
  )
997
980
 
998
- if result is None:
999
- return Serials(-1, -1, -1, lcn_defs.HardwareType.UNKNOWN)
1000
- return Serials(
1001
- result.hardware_serial,
1002
- result.manu,
1003
- result.software_serial,
1004
- result.hardware_type,
1005
- )
981
+ if isinstance(result, inputs.ModSn):
982
+ return Serials(
983
+ result.hardware_serial,
984
+ result.manu,
985
+ result.software_serial,
986
+ result.hardware_type,
987
+ )
988
+ return Serials(-1, -1, -1, lcn_defs.HardwareType.UNKNOWN)
1006
989
 
1007
990
  async def request_name(self, max_age: int = 0) -> str | None:
1008
991
  """Request module name."""
@@ -1010,7 +993,7 @@ class DeviceConnection:
1010
993
  _LOGGER.info("Status requests are not supported for groups.")
1011
994
  return None
1012
995
 
1013
- coros = [
996
+ coros = (
1014
997
  self.status_requester.request(
1015
998
  response_type=inputs.ModNameComment,
1016
999
  request_pck=PckGenerator.request_name(block_id),
@@ -1018,8 +1001,8 @@ class DeviceConnection:
1018
1001
  command="N",
1019
1002
  block_id=block_id,
1020
1003
  )
1021
- for block_id in [0, 1]
1022
- ]
1004
+ for block_id in (0, 1)
1005
+ )
1023
1006
 
1024
1007
  coro_results = [await coro for coro in coros]
1025
1008
  if not all(coro_results):
@@ -1034,7 +1017,7 @@ class DeviceConnection:
1034
1017
  _LOGGER.info("Status requests are not supported for groups.")
1035
1018
  return None
1036
1019
 
1037
- coros = [
1020
+ coros = (
1038
1021
  self.status_requester.request(
1039
1022
  response_type=inputs.ModNameComment,
1040
1023
  request_pck=PckGenerator.request_comment(block_id),
@@ -1042,15 +1025,15 @@ class DeviceConnection:
1042
1025
  command="K",
1043
1026
  block_id=block_id,
1044
1027
  )
1045
- for block_id in [0, 1, 2]
1046
- ]
1028
+ for block_id in (0, 1, 2)
1029
+ )
1047
1030
 
1048
1031
  coro_results = [await coro for coro in coros]
1049
1032
  if not all(coro_results):
1050
1033
  return None
1051
1034
  results = cast(list[inputs.ModNameComment], coro_results)
1052
- name = "".join([result.text for result in results if result])
1053
- return name
1035
+ comment = "".join([result.text for result in results if result])
1036
+ return comment
1054
1037
 
1055
1038
  async def request_oem_text(self, max_age: int = 0) -> str | None:
1056
1039
  """Request module name."""
@@ -1058,7 +1041,7 @@ class DeviceConnection:
1058
1041
  _LOGGER.info("Status requests are not supported for groups.")
1059
1042
  return None
1060
1043
 
1061
- coros = [
1044
+ coros = (
1062
1045
  self.status_requester.request(
1063
1046
  response_type=inputs.ModNameComment,
1064
1047
  request_pck=PckGenerator.request_oem_text(block_id),
@@ -1066,15 +1049,15 @@ class DeviceConnection:
1066
1049
  command="O",
1067
1050
  block_id=block_id,
1068
1051
  )
1069
- for block_id in [0, 1, 2, 3]
1070
- ]
1052
+ for block_id in (0, 1, 2, 3)
1053
+ )
1071
1054
 
1072
1055
  coro_results = [await coro for coro in coros]
1073
1056
  if not all(coro_results):
1074
1057
  return None
1075
1058
  results = cast(list[inputs.ModNameComment], coro_results)
1076
- name = "".join([result.text for result in results if result])
1077
- return name
1059
+ oem_text = "".join([result.text for result in results if result])
1060
+ return oem_text
1078
1061
 
1079
1062
  async def request_group_memberships(
1080
1063
  self, dynamic: bool = False, max_age: int = 0
@@ -1095,5 +1078,5 @@ class DeviceConnection:
1095
1078
  dynamic=dynamic,
1096
1079
  )
1097
1080
  if result is not None:
1098
- return set(cast(inputs.ModStatusGroups, result).groups)
1081
+ return set(result.groups)
1099
1082
  return None
@@ -26,6 +26,7 @@ class Input:
26
26
 
27
27
  def __init__(self) -> None:
28
28
  """Construct Input object."""
29
+ self.pck = ""
29
30
 
30
31
  @staticmethod
31
32
  def try_parse(data: str) -> list[Input] | None:
@@ -1368,9 +1369,11 @@ class InputParser:
1368
1369
  :rtype: List with instances of :class:`~pypck.input.Input`
1369
1370
  """
1370
1371
  for parser in InputParser.parsers:
1371
- ret: list[Input] | None = parser.try_parse(data)
1372
- if ret is not None:
1373
- return ret
1372
+ returns: list[Input] | None = parser.try_parse(data)
1373
+ if returns is not None:
1374
+ for ret in returns:
1375
+ ret.pck = data
1376
+ return returns
1374
1377
 
1375
1378
  # We must never get to this point since the Unknown parser matches
1376
1379
  # everything.
@@ -880,20 +880,20 @@ class PckGenerator:
880
880
  if software_serial >= 0x170206:
881
881
  var_id = lcn_defs.Var.to_var_id(var)
882
882
  if var_id != -1:
883
- return f"MWT{var_id + 1:03d}"
883
+ return f"MWT{var_id + 1}"
884
884
 
885
885
  set_point_id = lcn_defs.Var.to_set_point_id(var)
886
886
  if set_point_id != -1:
887
- return f"MWS{set_point_id + 1:03d}"
887
+ return f"MWS{set_point_id + 1}"
888
888
 
889
889
  thrs_register_id = lcn_defs.Var.to_thrs_register_id(var)
890
890
  if thrs_register_id != -1:
891
891
  # Whole register
892
- return f"SE{thrs_register_id + 1:03d}"
892
+ return f"SE{thrs_register_id + 1}"
893
893
 
894
894
  s0_id = lcn_defs.Var.to_s0_id(var)
895
895
  if s0_id != -1:
896
- return f"MWC{s0_id + 1:03d}"
896
+ return f"MWC{s0_id + 1}"
897
897
  else:
898
898
  if var == lcn_defs.Var.VAR1ORTVAR:
899
899
  pck = "MWV"
@@ -0,0 +1,155 @@
1
+ """Status requester."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import logging
7
+ from dataclasses import dataclass, field
8
+ from typing import TYPE_CHECKING, Any, Generic, TypeVar, cast
9
+
10
+ from pypck import inputs
11
+
12
+ if TYPE_CHECKING:
13
+ from pypck.device import DeviceConnection
14
+
15
+ _LOGGER = logging.getLogger(__name__)
16
+
17
+ ResponseT = TypeVar("ResponseT", bound=inputs.Input)
18
+
19
+
20
+ @dataclass(unsafe_hash=True)
21
+ class StatusRequest(Generic[ResponseT]):
22
+ """Data class for status requests."""
23
+
24
+ type: type[ResponseT] # Type of the input expected as response
25
+ parameters: frozenset[tuple[str, Any]] # {(parameter_name, parameter_value)}
26
+ timestamp: float = field(
27
+ compare=False
28
+ ) # timestamp the response was received; -1=no timestamp
29
+ response: asyncio.Future[ResponseT] = field(
30
+ compare=False
31
+ ) # the response input object
32
+
33
+
34
+ class StatusRequester:
35
+ """Handling of status requests."""
36
+
37
+ current_request: StatusRequest[inputs.Input] | None
38
+
39
+ def __init__(
40
+ self,
41
+ device_connection: DeviceConnection,
42
+ ) -> None:
43
+ """Initialize the context."""
44
+ self.device_connection = device_connection
45
+ self.current_request = None
46
+ self.request_cache: set[StatusRequest[inputs.Input]] = set()
47
+ self.max_response_age = self.device_connection.conn.settings["MAX_RESPONSE_AGE"]
48
+ self.request_lock = asyncio.Lock()
49
+
50
+ self.unregister_inputs = self.device_connection.register_for_inputs(
51
+ self.input_callback
52
+ )
53
+
54
+ def get_status_requests(
55
+ self,
56
+ request_type: type[ResponseT],
57
+ parameters: frozenset[tuple[str, Any]] | None = None,
58
+ max_age: int = 0,
59
+ ) -> list[StatusRequest[ResponseT]]:
60
+ """Get the status requests for the given type and parameters."""
61
+ if parameters is None:
62
+ parameters = frozenset()
63
+ results = [
64
+ request
65
+ for request in self.request_cache
66
+ if request.type == request_type
67
+ and parameters.issubset(request.parameters)
68
+ and (
69
+ (request.timestamp == -1)
70
+ or (max_age == -1)
71
+ or (asyncio.get_running_loop().time() - request.timestamp < max_age)
72
+ )
73
+ ]
74
+ results.sort(key=lambda request: request.timestamp, reverse=True)
75
+ return cast(list[StatusRequest[ResponseT]], results)
76
+
77
+ def input_callback(self, inp: inputs.Input) -> None:
78
+ """Handle incoming inputs and set the result for the corresponding requests."""
79
+ # Update current request (if it exists)
80
+ if (
81
+ self.current_request is not None
82
+ and not self.current_request.response.done()
83
+ ):
84
+ if isinstance(inp, self.current_request.type) and all(
85
+ getattr(inp, parameter_name) == parameter_value
86
+ for parameter_name, parameter_value in self.current_request.parameters
87
+ ):
88
+ self.current_request.timestamp = asyncio.get_running_loop().time()
89
+ self.current_request.response.set_result(inp)
90
+
91
+ # Update cached requests
92
+ for request in self.get_status_requests(type(inp)):
93
+ if all(
94
+ getattr(inp, parameter_name) == parameter_value
95
+ for parameter_name, parameter_value in request.parameters
96
+ ):
97
+ request.timestamp = asyncio.get_running_loop().time()
98
+ request.response = asyncio.get_running_loop().create_future()
99
+ request.response.set_result(inp)
100
+
101
+ async def request(
102
+ self,
103
+ response_type: type[ResponseT],
104
+ request_pck: str,
105
+ request_acknowledge: bool = False,
106
+ max_age: int = 0, # -1: no age limit / infinite age
107
+ **request_kwargs: Any,
108
+ ) -> ResponseT | None:
109
+ """Execute a status request and wait for the response."""
110
+ async with self.request_lock:
111
+ # check for matching request in cache
112
+ if requests := self.get_status_requests(
113
+ response_type,
114
+ frozenset(request_kwargs.items()),
115
+ max_age,
116
+ ):
117
+ _LOGGER.debug(
118
+ "from %s: %s (cached)",
119
+ self.device_connection.conn.connection_id,
120
+ requests[0].response.result().pck,
121
+ )
122
+ return requests[0].response.result()
123
+
124
+ # no matching request in cache
125
+ self.current_request = StatusRequest(
126
+ response_type,
127
+ frozenset(request_kwargs.items()),
128
+ -1,
129
+ asyncio.get_running_loop().create_future(),
130
+ )
131
+
132
+ result = None
133
+ # send the request up to NUM_TRIES and wait for response future completion
134
+ for _ in range(self.device_connection.conn.settings["NUM_TRIES"]):
135
+ await self.device_connection.send_command(
136
+ request_acknowledge, request_pck
137
+ )
138
+
139
+ try:
140
+ async with asyncio.timeout(
141
+ self.device_connection.conn.settings["DEFAULT_TIMEOUT"]
142
+ ):
143
+ # Need to shield the future. Otherwise it would get cancelled.
144
+ result = await asyncio.shield(self.current_request.response)
145
+ break
146
+ except asyncio.TimeoutError:
147
+ continue
148
+ except asyncio.CancelledError:
149
+ break
150
+
151
+ if result is not None: # add request to cache
152
+ self.request_cache.discard(self.current_request)
153
+ self.request_cache.add(self.current_request)
154
+
155
+ return cast(ResponseT | None, result)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pypck
3
- Version: 0.9.8
3
+ Version: 0.9.9
4
4
  Summary: LCN-PCK library
5
5
  Home-page: https://github.com/alengwenus/pypck
6
6
  Author-email: Andre Lengwenus <alengwenus@gmail.com>
@@ -89,7 +89,7 @@ COMMANDS: dict[str | bytes, Any] = {
89
89
  "STX": (PckGenerator.request_key_lock_status,),
90
90
  # Variable status (new commands)
91
91
  **{
92
- f"MWT{Var.to_var_id(var) + 1:03d}": (
92
+ f"MWT{Var.to_var_id(var) + 1}": (
93
93
  PckGenerator.request_var_status,
94
94
  var,
95
95
  NEW_VAR_SW_AGE,
@@ -97,7 +97,7 @@ COMMANDS: dict[str | bytes, Any] = {
97
97
  for var in Var.variables()
98
98
  },
99
99
  **{
100
- f"MWS{Var.to_set_point_id(var) + 1:03d}": (
100
+ f"MWS{Var.to_set_point_id(var) + 1}": (
101
101
  PckGenerator.request_var_status,
102
102
  var,
103
103
  NEW_VAR_SW_AGE,
@@ -105,7 +105,7 @@ COMMANDS: dict[str | bytes, Any] = {
105
105
  for var in Var.set_points()
106
106
  },
107
107
  **{
108
- f"MWC{Var.to_s0_id(var) + 1:03d}": (
108
+ f"MWC{Var.to_s0_id(var) + 1}": (
109
109
  PckGenerator.request_var_status,
110
110
  var,
111
111
  NEW_VAR_SW_AGE,
@@ -113,7 +113,7 @@ COMMANDS: dict[str | bytes, Any] = {
113
113
  for var in Var.s0s()
114
114
  },
115
115
  **{
116
- f"SE{Var.to_thrs_register_id(var) + 1:03d}": (
116
+ f"SE{Var.to_thrs_register_id(var) + 1}": (
117
117
  PckGenerator.request_var_status,
118
118
  var,
119
119
  NEW_VAR_SW_AGE,
@@ -361,4 +361,4 @@ def test_message_parsing_mod_inputs(
361
361
  for idx, inp in enumerate(inputs):
362
362
  exp = (expected[idx][0])(LcnAddr(0, 10, False), *expected[idx][1:])
363
363
  assert type(inp) is type(exp) # pylint: disable=unidiomatic-typecheck
364
- assert vars(inp) == vars(exp)
364
+ assert vars(inp) == vars(exp) | {"pck": message}
@@ -1,6 +1,7 @@
1
1
  """Test the status requester of a module connection."""
2
2
 
3
3
  import asyncio
4
+ from unittest.mock import call
4
5
 
5
6
  from pypck.pck_commands import PckGenerator
6
7
  from pypck.status_requester import StatusRequest
@@ -38,32 +39,32 @@ async def test_request_status(module10: MockDeviceConnection) -> None:
38
39
  assert result.states == RELAY_STATES
39
40
 
40
41
 
41
- # async def test_request_status_stored(module10: MockDeviceConnection) -> None:
42
- # """Test requesting the status of a module with stored status request."""
43
- # status_request = StatusRequest(
44
- # type=inputs.ModStatusRelays,
45
- # parameters=frozenset(),
46
- # timestamp=asyncio.get_running_loop().time(),
47
- # response=asyncio.get_running_loop().create_future(),
48
- # )
49
- # status_request.response.set_result(
50
- # inputs.ModStatusRelays(module10.addr, RELAY_STATES)
51
- # )
52
- # module10.status_requester.last_requests.add(status_request)
53
-
54
- # result = await module10.status_requester.request(
55
- # response_type=inputs.ModStatusRelays,
56
- # request_pck=PckGenerator.request_relays_status(),
57
- # max_age=10,
58
- # )
59
-
60
- # assert isinstance(result, inputs.ModStatusRelays)
61
- # assert result.physical_source_addr == module10.addr
62
- # assert result.states == RELAY_STATES
63
- # assert (
64
- # call(False, PckGenerator.request_relays_status())
65
- # not in module10.send_command.await_args_list
66
- # )
42
+ async def test_request_status_stored(module10: MockDeviceConnection) -> None:
43
+ """Test requesting the status of a module with stored status request."""
44
+ status_request = StatusRequest(
45
+ type=inputs.ModStatusRelays,
46
+ parameters=frozenset(),
47
+ timestamp=asyncio.get_running_loop().time(),
48
+ response=asyncio.get_running_loop().create_future(),
49
+ )
50
+ status_request.response.set_result(
51
+ inputs.ModStatusRelays(module10.addr, RELAY_STATES)
52
+ )
53
+ module10.status_requester.request_cache.add(status_request)
54
+
55
+ result = await module10.status_requester.request(
56
+ response_type=inputs.ModStatusRelays,
57
+ request_pck=PckGenerator.request_relays_status(),
58
+ max_age=10,
59
+ )
60
+
61
+ assert isinstance(result, inputs.ModStatusRelays)
62
+ assert result.physical_source_addr == module10.addr
63
+ assert result.states == RELAY_STATES
64
+ assert (
65
+ call(False, PckGenerator.request_relays_status())
66
+ not in module10.send_command.await_args_list
67
+ )
67
68
 
68
69
 
69
70
  async def test_request_status_expired(module10: MockDeviceConnection) -> None:
@@ -76,7 +77,7 @@ async def test_request_status_expired(module10: MockDeviceConnection) -> None:
76
77
  response=asyncio.get_running_loop().create_future(),
77
78
  )
78
79
  status_request.response.set_result(inputs.ModStatusRelays(module10.addr, states))
79
- module10.status_requester.last_requests.add(status_request)
80
+ module10.status_requester.request_cache.add(status_request)
80
81
 
81
82
  request_task = asyncio.create_task(
82
83
  module10.status_requester.request(
pypck-0.9.8/VERSION DELETED
@@ -1 +0,0 @@
1
- 0.9.8
@@ -1,107 +0,0 @@
1
- """Status requester."""
2
-
3
- from __future__ import annotations
4
-
5
- import asyncio
6
- import logging
7
- from dataclasses import dataclass, field
8
- from typing import TYPE_CHECKING, Any
9
-
10
- from pypck import inputs
11
-
12
- if TYPE_CHECKING:
13
- from pypck.device import DeviceConnection
14
-
15
- _LOGGER = logging.getLogger(__name__)
16
-
17
-
18
- @dataclass(unsafe_hash=True)
19
- class StatusRequest:
20
- """Data class for status requests."""
21
-
22
- type: type[inputs.Input] # Type of the input expected as response
23
- parameters: frozenset[tuple[str, Any]] # {(parameter_name, parameter_value)}
24
- timestamp: float = field(
25
- compare=False
26
- ) # timestamp the response was received; -1=no timestamp
27
- response: asyncio.Future[inputs.Input] = field(
28
- compare=False
29
- ) # Future to hold the response input object
30
-
31
-
32
- class StatusRequester:
33
- """Handling of status requests."""
34
-
35
- current_request: StatusRequest
36
-
37
- def __init__(
38
- self,
39
- device_connection: DeviceConnection,
40
- ) -> None:
41
- """Initialize the context."""
42
- self.device_connection = device_connection
43
- self.last_requests: set[StatusRequest] = set()
44
- self.max_response_age = self.device_connection.conn.settings["MAX_RESPONSE_AGE"]
45
- self.request_lock = asyncio.Lock()
46
-
47
- def input_callback(self, inp: inputs.Input) -> None:
48
- """Handle incoming inputs and set the result for the corresponding requests."""
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)
61
-
62
- async def request(
63
- self,
64
- response_type: type[inputs.Input],
65
- request_pck: str,
66
- request_acknowledge: bool = False,
67
- max_age: int = 0, # -1: no age limit / infinite age
68
- **request_kwargs: Any,
69
- ) -> inputs.Input | None:
70
- """Execute a status request and wait for the 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:
100
- break
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
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes