pypck 0.9.8__py3-none-any.whl → 0.9.9__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
@@ -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
pypck/inputs.py CHANGED
@@ -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.
pypck/pck_commands.py CHANGED
@@ -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"
pypck/status_requester.py CHANGED
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  import asyncio
6
6
  import logging
7
7
  from dataclasses import dataclass, field
8
- from typing import TYPE_CHECKING, Any
8
+ from typing import TYPE_CHECKING, Any, Generic, TypeVar, cast
9
9
 
10
10
  from pypck import inputs
11
11
 
@@ -14,25 +14,27 @@ if TYPE_CHECKING:
14
14
 
15
15
  _LOGGER = logging.getLogger(__name__)
16
16
 
17
+ ResponseT = TypeVar("ResponseT", bound=inputs.Input)
18
+
17
19
 
18
20
  @dataclass(unsafe_hash=True)
19
- class StatusRequest:
21
+ class StatusRequest(Generic[ResponseT]):
20
22
  """Data class for status requests."""
21
23
 
22
- type: type[inputs.Input] # Type of the input expected as response
24
+ type: type[ResponseT] # Type of the input expected as response
23
25
  parameters: frozenset[tuple[str, Any]] # {(parameter_name, parameter_value)}
24
26
  timestamp: float = field(
25
27
  compare=False
26
28
  ) # timestamp the response was received; -1=no timestamp
27
- response: asyncio.Future[inputs.Input] = field(
29
+ response: asyncio.Future[ResponseT] = field(
28
30
  compare=False
29
- ) # Future to hold the response input object
31
+ ) # the response input object
30
32
 
31
33
 
32
34
  class StatusRequester:
33
35
  """Handling of status requests."""
34
36
 
35
- current_request: StatusRequest
37
+ current_request: StatusRequest[inputs.Input] | None
36
38
 
37
39
  def __init__(
38
40
  self,
@@ -40,35 +42,86 @@ class StatusRequester:
40
42
  ) -> None:
41
43
  """Initialize the context."""
42
44
  self.device_connection = device_connection
43
- self.last_requests: set[StatusRequest] = set()
45
+ self.current_request = None
46
+ self.request_cache: set[StatusRequest[inputs.Input]] = set()
44
47
  self.max_response_age = self.device_connection.conn.settings["MAX_RESPONSE_AGE"]
45
48
  self.request_lock = asyncio.Lock()
46
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
+
47
77
  def input_callback(self, inp: inputs.Input) -> None:
48
78
  """Handle incoming inputs and set the result for the corresponding requests."""
79
+ # Update current request (if it exists)
49
80
  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
81
+ self.current_request is not None
82
+ and not self.current_request.response.done()
58
83
  ):
59
- self.current_request.timestamp = asyncio.get_running_loop().time()
60
- self.current_request.response.set_result(inp)
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)
61
100
 
62
101
  async def request(
63
102
  self,
64
- response_type: type[inputs.Input],
103
+ response_type: type[ResponseT],
65
104
  request_pck: str,
66
105
  request_acknowledge: bool = False,
67
106
  max_age: int = 0, # -1: no age limit / infinite age
68
107
  **request_kwargs: Any,
69
- ) -> inputs.Input | None:
108
+ ) -> ResponseT | None:
70
109
  """Execute a status request and wait for the response."""
71
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
72
125
  self.current_request = StatusRequest(
73
126
  response_type,
74
127
  frozenset(request_kwargs.items()),
@@ -76,10 +129,6 @@ class StatusRequester:
76
129
  asyncio.get_running_loop().create_future(),
77
130
  )
78
131
 
79
- unregister_inputs = self.device_connection.register_for_inputs(
80
- self.input_callback
81
- )
82
-
83
132
  result = None
84
133
  # send the request up to NUM_TRIES and wait for response future completion
85
134
  for _ in range(self.device_connection.conn.settings["NUM_TRIES"]):
@@ -99,9 +148,8 @@ class StatusRequester:
99
148
  except asyncio.CancelledError:
100
149
  break
101
150
 
102
- # if we got no results, remove the request from the set
103
- if result is None:
104
- self.current_request.response.cancel()
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)
105
154
 
106
- unregister_inputs()
107
- return result
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>
@@ -0,0 +1,15 @@
1
+ pypck/__init__.py,sha256=jVx-aBsV_LmBf6jiivMrMcBUofC_AOseywDafgOzAS4,323
2
+ pypck/connection.py,sha256=n3itRe8oQtw64vyWGYhl6j4QJC6wgeeHitBSn-Cl2_4,23330
3
+ pypck/device.py,sha256=1R9C4Wra8llzJph4eher-KPF2iF7uGOspq3eWcqTLyM,39591
4
+ pypck/helpers.py,sha256=_5doqIsSRpqdQNPIUsjFh813xKGuMuEFY6sNGobJGIk,1280
5
+ pypck/inputs.py,sha256=dw9ebhP9RBM9d-lPSFqRrFH1TOETNsT4RNschNjAnA8,46056
6
+ pypck/lcn_addr.py,sha256=N2Od8KuANOglqKjf596hJVH1SRcG7MhESKA5YYlDnbw,1946
7
+ pypck/lcn_defs.py,sha256=wSceYBwM46NqPwvff1hi8RluqUECmNY1gNcm1kDKTaI,43356
8
+ pypck/pck_commands.py,sha256=SL9zpsswRGCHjAWm4uKWFTqtSBx2jURFHzq86QrtwuU,50442
9
+ pypck/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ pypck/status_requester.py,sha256=Ea9c9moA1Kxoh3U9tPVqHKAmnRwq-1vraP_cwqxGZt8,5774
11
+ pypck-0.9.9.dist-info/licenses/LICENSE,sha256=iYB6zyMJvShfAzQE7nhYFgLzzZuBmhasLw5fYP9KRz4,1023
12
+ pypck-0.9.9.dist-info/METADATA,sha256=HER_BuajbZll66fngbD2rQVTR6bcice7kOMW8bOLggk,2682
13
+ pypck-0.9.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
+ pypck-0.9.9.dist-info/top_level.txt,sha256=59ried49iFueDa5mQ_5BGVZcESjjzi4MZZKLcganvQA,6
15
+ pypck-0.9.9.dist-info/RECORD,,
@@ -1,15 +0,0 @@
1
- pypck/__init__.py,sha256=jVx-aBsV_LmBf6jiivMrMcBUofC_AOseywDafgOzAS4,323
2
- pypck/connection.py,sha256=n3itRe8oQtw64vyWGYhl6j4QJC6wgeeHitBSn-Cl2_4,23330
3
- pypck/device.py,sha256=m-JUdea-yzsGMqGZEKvTQkxUpjlaUwR3KKMCFi35RYs,40103
4
- pypck/helpers.py,sha256=_5doqIsSRpqdQNPIUsjFh813xKGuMuEFY6sNGobJGIk,1280
5
- pypck/inputs.py,sha256=F7E8rprIhYzZnHARozt_hguYNgJaiNP3htrZ2E3Qa5I,45951
6
- pypck/lcn_addr.py,sha256=N2Od8KuANOglqKjf596hJVH1SRcG7MhESKA5YYlDnbw,1946
7
- pypck/lcn_defs.py,sha256=wSceYBwM46NqPwvff1hi8RluqUECmNY1gNcm1kDKTaI,43356
8
- pypck/pck_commands.py,sha256=eJxmh2e8EbKGpek97L2961Kr_nVfT8rKgJCN3YgjIQM,50458
9
- pypck/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
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