pypck 0.8.12__py3-none-any.whl → 0.9.2__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/module.py CHANGED
@@ -3,26 +3,176 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
- from collections.abc import Awaitable, Callable, Sequence
6
+ import logging
7
+ from collections.abc import Callable, Sequence
8
+ from dataclasses import dataclass, field
7
9
  from typing import TYPE_CHECKING, Any, cast
8
10
 
9
11
  from pypck import inputs, lcn_defs
10
12
  from pypck.helpers import TaskRegistry
11
13
  from pypck.lcn_addr import LcnAddr
12
14
  from pypck.pck_commands import PckGenerator
13
- from pypck.request_handlers import (
14
- CommentRequestHandler,
15
- GroupMembershipDynamicRequestHandler,
16
- GroupMembershipStaticRequestHandler,
17
- NameRequestHandler,
18
- OemTextRequestHandler,
19
- SerialRequestHandler,
20
- StatusRequestsHandler,
21
- )
22
15
 
23
16
  if TYPE_CHECKING:
24
17
  from pypck.connection import PchkConnectionManager
25
18
 
19
+ _LOGGER = logging.getLogger(__name__)
20
+
21
+
22
+ @dataclass
23
+ class Serials:
24
+ """Data class for module serials."""
25
+
26
+ hardware_serial: int
27
+ manu: int
28
+ software_serial: int
29
+ hardware_type: lcn_defs.HardwareType
30
+
31
+
32
+ @dataclass(unsafe_hash=True)
33
+ class StatusRequest:
34
+ """Data class for status requests."""
35
+
36
+ type: type[inputs.Input] # Type of the input expected as response
37
+ parameters: frozenset[tuple[str, Any]] # {(parameter_name, parameter_value)}
38
+ timestamp: float = field(
39
+ compare=False
40
+ ) # timestamp the response was received; -1=no timestamp
41
+ response: asyncio.Future[inputs.Input] = field(
42
+ compare=False
43
+ ) # Future to hold the response input object
44
+
45
+
46
+ class StatusRequester:
47
+ """Handling of status requests."""
48
+
49
+ def __init__(
50
+ self,
51
+ device_connection: ModuleConnection,
52
+ ) -> None:
53
+ """Initialize the context."""
54
+ self.device_connection = device_connection
55
+ self.last_requests: set[StatusRequest] = set()
56
+ self.unregister_inputs = self.device_connection.register_for_inputs(
57
+ self.input_callback
58
+ )
59
+ self.max_response_age = self.device_connection.conn.settings["MAX_RESPONSE_AGE"]
60
+ # asyncio.get_running_loop().create_task(self.prune_loop())
61
+
62
+ async def prune_loop(self) -> None:
63
+ """Periodically prune old status requests."""
64
+ while True:
65
+ await asyncio.sleep(self.max_response_age)
66
+ self.prune_status_requests()
67
+
68
+ def prune_status_requests(self) -> None:
69
+ """Prune old status requests."""
70
+ entries_to_remove = {
71
+ request
72
+ for request in self.last_requests
73
+ if asyncio.get_running_loop().time() - request.timestamp
74
+ > self.max_response_age
75
+ }
76
+ for entry in entries_to_remove:
77
+ entry.response.cancel()
78
+ self.last_requests.difference_update(entries_to_remove)
79
+
80
+ def get_status_requests(
81
+ self,
82
+ request_type: type[inputs.Input],
83
+ parameters: frozenset[tuple[str, Any]] | None = None,
84
+ max_age: int = 0,
85
+ ) -> list[StatusRequest]:
86
+ """Get the status requests for the given type and parameters."""
87
+ if parameters is None:
88
+ parameters = frozenset()
89
+ loop = asyncio.get_running_loop()
90
+ results = [
91
+ request
92
+ for request in self.last_requests
93
+ if request.type == request_type
94
+ and parameters.issubset(request.parameters)
95
+ and (
96
+ (request.timestamp == -1)
97
+ or (max_age == -1)
98
+ or (loop.time() - request.timestamp < max_age)
99
+ )
100
+ ]
101
+ results.sort(key=lambda request: request.timestamp, reverse=True)
102
+ return results
103
+
104
+ def input_callback(self, inp: inputs.Input) -> None:
105
+ """Handle incoming inputs and set the result for the corresponding requests."""
106
+ requests = [
107
+ request
108
+ for request in self.get_status_requests(type(inp))
109
+ if all(
110
+ getattr(inp, parameter_name) == parameter_value
111
+ for parameter_name, parameter_value in request.parameters
112
+ )
113
+ ]
114
+ for request in requests:
115
+ if request.response.done() or request.response.cancelled():
116
+ continue
117
+ request.timestamp = asyncio.get_running_loop().time()
118
+ request.response.set_result(inp)
119
+
120
+ async def request(
121
+ self,
122
+ response_type: type[inputs.Input],
123
+ request_pck: str,
124
+ request_acknowledge: bool = False,
125
+ max_age: int = 0, # -1: no age limit / infinite age
126
+ **request_kwargs: Any,
127
+ ) -> inputs.Input | None:
128
+ """Execute a status request and wait for the response."""
129
+ parameters = frozenset(request_kwargs.items())
130
+
131
+ # check if we already have a received response for the current request
132
+ if requests := self.get_status_requests(response_type, parameters, max_age):
133
+ try:
134
+ async with asyncio.timeout(
135
+ self.device_connection.conn.settings["DEFAULT_TIMEOUT"]
136
+ ):
137
+ return await requests[0].response
138
+ except asyncio.TimeoutError:
139
+ return None
140
+ except asyncio.CancelledError:
141
+ return None
142
+
143
+ # no stored request or forced request: set up a new request
144
+ request = StatusRequest(
145
+ response_type,
146
+ frozenset(request_kwargs.items()),
147
+ -1,
148
+ asyncio.get_running_loop().create_future(),
149
+ )
150
+
151
+ self.last_requests.discard(request)
152
+ self.last_requests.add(request)
153
+ result = None
154
+ # send the request up to NUM_TRIES and wait for response future completion
155
+ for _ in range(self.device_connection.conn.settings["NUM_TRIES"]):
156
+ await self.device_connection.send_command(request_acknowledge, request_pck)
157
+
158
+ try:
159
+ async with asyncio.timeout(
160
+ self.device_connection.conn.settings["DEFAULT_TIMEOUT"]
161
+ ):
162
+ # Need to shield the future. Otherwise it would get cancelled.
163
+ result = await asyncio.shield(request.response)
164
+ break
165
+ except asyncio.TimeoutError:
166
+ continue
167
+ except asyncio.CancelledError:
168
+ break
169
+
170
+ # if we got no results, remove the request from the set
171
+ if result is None:
172
+ request.response.cancel()
173
+ self.last_requests.discard(request)
174
+ return result
175
+
26
176
 
27
177
  class AbstractConnection:
28
178
  """Organizes communication with a specific module.
@@ -34,17 +184,14 @@ class AbstractConnection:
34
184
  self,
35
185
  conn: PchkConnectionManager,
36
186
  addr: LcnAddr,
37
- software_serial: int | None = None,
38
187
  wants_ack: bool = False,
39
188
  ) -> None:
40
189
  """Construct AbstractConnection instance."""
41
190
  self.conn = conn
42
191
  self.addr = addr
43
192
  self.wants_ack = wants_ack
44
-
45
- if software_serial is None:
46
- software_serial = -1
47
- self._software_serial: int = software_serial
193
+ self.serials = Serials(-1, -1, -1, lcn_defs.HardwareType.UNKNOWN)
194
+ self._serials_known = asyncio.Event()
48
195
 
49
196
  @property
50
197
  def task_registry(self) -> TaskRegistry:
@@ -66,47 +213,6 @@ class AbstractConnection:
66
213
  """Return whether this connection refers to a module or group."""
67
214
  return self.addr.is_group
68
215
 
69
- @property
70
- def serials(self) -> dict[str, int | lcn_defs.HardwareType]:
71
- """Return serial numbers of a module."""
72
- return {
73
- "hardware_serial": -1,
74
- "manu": -1,
75
- "software_serial": self._software_serial,
76
- "hardware_type": lcn_defs.HardwareType.UNKNOWN,
77
- }
78
-
79
- @property
80
- def hardware_serial(self) -> int:
81
- """Get the hardware serial number."""
82
- return cast(int, self.serials["hardware_serial"])
83
-
84
- @property
85
- def software_serial(self) -> int:
86
- """Get the software serial number."""
87
- return cast(int, self.serials["software_serial"])
88
-
89
- @property
90
- def manu(self) -> int:
91
- """Get the manufacturing number."""
92
- return cast(int, self.serials["manu"])
93
-
94
- @property
95
- def hardware_type(self) -> lcn_defs.HardwareType:
96
- """Get the hardware type."""
97
- return cast(lcn_defs.HardwareType, self.serials["hardware_type"])
98
-
99
- @property
100
- def serial_known(self) -> Awaitable[bool]:
101
- """Check if serials have already been received from module."""
102
- event = asyncio.Event()
103
- event.set()
104
- return event.wait()
105
-
106
- async def request_serials(self) -> dict[str, int | lcn_defs.HardwareType]:
107
- """Request module serials."""
108
- return self.serials
109
-
110
216
  async def send_command(self, wants_ack: bool, pck: str | bytes) -> bool:
111
217
  """Send a command to the module represented by this class.
112
218
 
@@ -138,9 +244,7 @@ class AbstractConnection:
138
244
  self.wants_ack, PckGenerator.dim_output(output_id, percent, ramp)
139
245
  )
140
246
 
141
- async def dim_all_outputs(
142
- self, percent: float, ramp: int, software_serial: int | None = None
143
- ) -> bool:
247
+ async def dim_all_outputs(self, percent: float, ramp: int) -> bool:
144
248
  """Send a dim command for all output-ports.
145
249
 
146
250
  :param float percent: Brightness in percent 0..100
@@ -151,13 +255,10 @@ class AbstractConnection:
151
255
  :returns: True if command was sent successfully, False otherwise
152
256
  :rtype: bool
153
257
  """
154
- if software_serial is None:
155
- await self.serial_known
156
- software_serial = self.software_serial
157
-
258
+ await self._serials_known.wait()
158
259
  return await self.send_command(
159
260
  self.wants_ack,
160
- PckGenerator.dim_all_outputs(percent, ramp, software_serial),
261
+ PckGenerator.dim_all_outputs(percent, ramp, self.serials.software_serial),
161
262
  )
162
263
 
163
264
  async def rel_output(self, output_id: int, percent: float) -> bool:
@@ -404,7 +505,7 @@ class AbstractConnection:
404
505
  var: lcn_defs.Var,
405
506
  value: float | lcn_defs.VarValue,
406
507
  unit: lcn_defs.VarUnit = lcn_defs.VarUnit.NATIVE,
407
- software_serial: int | None = None,
508
+ software_serial: int = -1,
408
509
  ) -> bool:
409
510
  """Send a command to set the absolute value to a variable.
410
511
 
@@ -418,9 +519,9 @@ class AbstractConnection:
418
519
  if not isinstance(value, lcn_defs.VarValue):
419
520
  value = lcn_defs.VarValue.from_var_unit(value, unit, True)
420
521
 
421
- if software_serial is None:
422
- await self.serial_known
423
- software_serial = self.software_serial
522
+ if software_serial == -1:
523
+ await self._serials_known.wait()
524
+ software_serial = self.serials.software_serial
424
525
 
425
526
  if lcn_defs.Var.to_var_id(var) != -1:
426
527
  # Absolute commands for variables 1-12 are not supported
@@ -433,23 +534,25 @@ class AbstractConnection:
433
534
  # We fake the missing command by using reset and relative
434
535
  # commands.
435
536
  success = await self.send_command(
436
- self.wants_ack, PckGenerator.var_reset(var, software_serial)
537
+ self.wants_ack,
538
+ PckGenerator.var_reset(var, software_serial),
437
539
  )
438
540
  if not success:
439
541
  return False
440
542
  return await self.send_command(
441
543
  self.wants_ack,
442
544
  PckGenerator.var_rel(
443
- var, lcn_defs.RelVarRef.CURRENT, value.to_native(), software_serial
545
+ var,
546
+ lcn_defs.RelVarRef.CURRENT,
547
+ value.to_native(),
548
+ software_serial,
444
549
  ),
445
550
  )
446
551
  return await self.send_command(
447
552
  self.wants_ack, PckGenerator.var_abs(var, value.to_native())
448
553
  )
449
554
 
450
- async def var_reset(
451
- self, var: lcn_defs.Var, software_serial: int | None = None
452
- ) -> bool:
555
+ async def var_reset(self, var: lcn_defs.Var, software_serial: int = -1) -> bool:
453
556
  """Send a command to reset the variable value.
454
557
 
455
558
  :param Var var: Variable
@@ -457,9 +560,9 @@ class AbstractConnection:
457
560
  :returns: True if command was sent successfully, False otherwise
458
561
  :rtype: bool
459
562
  """
460
- if software_serial is None:
461
- await self.serial_known
462
- software_serial = self.software_serial
563
+ if software_serial == -1:
564
+ await self._serials_known.wait()
565
+ software_serial = self.serials.software_serial
463
566
 
464
567
  return await self.send_command(
465
568
  self.wants_ack, PckGenerator.var_reset(var, software_serial)
@@ -471,7 +574,7 @@ class AbstractConnection:
471
574
  value: float | lcn_defs.VarValue,
472
575
  unit: lcn_defs.VarUnit = lcn_defs.VarUnit.NATIVE,
473
576
  value_ref: lcn_defs.RelVarRef = lcn_defs.RelVarRef.CURRENT,
474
- software_serial: int | None = None,
577
+ software_serial: int = -1,
475
578
  ) -> bool:
476
579
  """Send a command to change the value of a variable.
477
580
 
@@ -486,9 +589,9 @@ class AbstractConnection:
486
589
  if not isinstance(value, lcn_defs.VarValue):
487
590
  value = lcn_defs.VarValue.from_var_unit(value, unit, False)
488
591
 
489
- if software_serial is None:
490
- await self.serial_known
491
- software_serial = self.software_serial
592
+ if software_serial == -1:
593
+ await self._serials_known.wait()
594
+ software_serial = self.serials.software_serial
492
595
 
493
596
  return await self.send_command(
494
597
  self.wants_ack,
@@ -510,7 +613,7 @@ class AbstractConnection:
510
613
  return await self.send_command(
511
614
  self.wants_ack,
512
615
  PckGenerator.lock_regulator(
513
- reg_id, state, self.software_serial, target_value
616
+ reg_id, state, self.serials.software_serial, target_value
514
617
  ),
515
618
  )
516
619
 
@@ -681,18 +784,18 @@ class GroupConnection(AbstractConnection):
681
784
  self,
682
785
  conn: PchkConnectionManager,
683
786
  addr: LcnAddr,
684
- software_serial: int = 0x170206,
685
787
  ):
686
788
  """Construct GroupConnection instance."""
687
789
  assert addr.is_group
688
- super().__init__(conn, addr, software_serial=software_serial, wants_ack=False)
790
+ super().__init__(conn, addr, wants_ack=False)
791
+ self._serials_known.set()
689
792
 
690
793
  async def var_abs(
691
794
  self,
692
795
  var: lcn_defs.Var,
693
796
  value: float | lcn_defs.VarValue,
694
797
  unit: lcn_defs.VarUnit = lcn_defs.VarUnit.NATIVE,
695
- software_serial: int | None = None,
798
+ software_serial: int = -1,
696
799
  ) -> bool:
697
800
  """Send a command to set the absolute value to a variable.
698
801
 
@@ -740,7 +843,7 @@ class GroupConnection(AbstractConnection):
740
843
  value: float | lcn_defs.VarValue,
741
844
  unit: lcn_defs.VarUnit = lcn_defs.VarUnit.NATIVE,
742
845
  value_ref: lcn_defs.RelVarRef = lcn_defs.RelVarRef.CURRENT,
743
- software_serial: int | None = None,
846
+ software_serial: int = -1,
744
847
  ) -> bool:
745
848
  """Send a command to change the value of a variable.
746
849
 
@@ -766,15 +869,6 @@ class GroupConnection(AbstractConnection):
766
869
  result &= await super().var_rel(var, value, software_serial=0)
767
870
  return result
768
871
 
769
- async def activate_status_request_handler(self, item: Any, option: Any) -> None:
770
- """Activate a specific TimeoutRetryHandler for status requests."""
771
- await self.conn.segment_scan_completed_event.wait()
772
-
773
- async def activate_status_request_handlers(self) -> None:
774
- """Activate all TimeoutRetryHandlers for status requests."""
775
- # self.request_serial.activate()
776
- await self.conn.segment_scan_completed_event.wait()
777
-
778
872
 
779
873
  class ModuleConnection(AbstractConnection):
780
874
  """Organizes communication with a specific module or group."""
@@ -783,17 +877,12 @@ class ModuleConnection(AbstractConnection):
783
877
  self,
784
878
  conn: PchkConnectionManager,
785
879
  addr: LcnAddr,
786
- activate_status_requests: bool = False,
787
880
  has_s0_enabled: bool = False,
788
- software_serial: int | None = None,
789
881
  wants_ack: bool = True,
790
882
  ):
791
883
  """Construct ModuleConnection instance."""
792
884
  assert not addr.is_group
793
- super().__init__(
794
- conn, addr, software_serial=software_serial, wants_ack=wants_ack
795
- )
796
- self.activate_status_requests = activate_status_requests
885
+ super().__init__(conn, addr, wants_ack=wants_ack)
797
886
  self.has_s0_enabled = has_s0_enabled
798
887
 
799
888
  self.input_callbacks: set[Callable[[inputs.Input], None]] = set()
@@ -801,34 +890,15 @@ class ModuleConnection(AbstractConnection):
801
890
  # List of queued acknowledge codes from the LCN modules.
802
891
  self.acknowledges: asyncio.Queue[int] = asyncio.Queue()
803
892
 
804
- # RequestHandlers
805
- num_tries: int = self.conn.settings["NUM_TRIES"]
806
- timeout: int = self.conn.settings["DEFAULT_TIMEOUT"]
893
+ # StatusRequester
894
+ self.status_requester = StatusRequester(self)
807
895
 
808
- # Serial Number request
809
- self.serials_request_handler = SerialRequestHandler(
810
- self,
811
- num_tries,
812
- timeout,
813
- software_serial=software_serial,
814
- )
896
+ self.task_registry.create_task(self.request_module_properties())
815
897
 
816
- # Name, Comment, OemText requests
817
- self.name_request_handler = NameRequestHandler(self, num_tries, timeout)
818
- self.comment_request_handler = CommentRequestHandler(self, num_tries, timeout)
819
- self.oem_text_request_handler = OemTextRequestHandler(self, num_tries, timeout)
820
-
821
- # Group membership request
822
- self.static_groups_request_handler = GroupMembershipStaticRequestHandler(
823
- self, num_tries, timeout
824
- )
825
- self.dynamic_groups_request_handler = GroupMembershipDynamicRequestHandler(
826
- self, num_tries, timeout
827
- )
828
-
829
- self.status_requests_handler = StatusRequestsHandler(self)
830
- if self.activate_status_requests:
831
- self.task_registry.create_task(self.activate_status_request_handlers())
898
+ async def request_module_properties(self) -> None:
899
+ """Request module properties (serials)."""
900
+ self.serials = await self.request_serials()
901
+ self._serials_known.set()
832
902
 
833
903
  async def send_command(self, wants_ack: bool, pck: str | bytes) -> bool:
834
904
  """Send a command to the module represented by this class.
@@ -841,6 +911,10 @@ class ModuleConnection(AbstractConnection):
841
911
 
842
912
  return await super().send_command(False, pck)
843
913
 
914
+ async def serials_known(self) -> None:
915
+ """Wait until the serials of this module are known."""
916
+ await self._serials_known.wait()
917
+
844
918
  # ##
845
919
  # ## Retry logic if an acknowledge is requested
846
920
  # ##
@@ -879,37 +953,6 @@ class ModuleConnection(AbstractConnection):
879
953
  """
880
954
  await self.acknowledges.put(code)
881
955
 
882
- async def activate_status_request_handler(
883
- self, item: Any, option: Any = None
884
- ) -> None:
885
- """Activate a specific TimeoutRetryHandler for status requests."""
886
- self.task_registry.create_task(
887
- self.status_requests_handler.activate(item, option)
888
- )
889
-
890
- async def activate_status_request_handlers(self) -> None:
891
- """Activate all TimeoutRetryHandlers for status requests."""
892
- self.task_registry.create_task(
893
- self.status_requests_handler.activate_all(activate_s0=self.has_s0_enabled)
894
- )
895
-
896
- async def cancel_status_request_handler(self, item: Any) -> None:
897
- """Cancel a specific TimeoutRetryHandler for status requests."""
898
- await self.status_requests_handler.cancel(item)
899
-
900
- async def cancel_status_request_handlers(self) -> None:
901
- """Canecl all TimeoutRetryHandlers for status requests."""
902
- await self.status_requests_handler.cancel_all()
903
-
904
- async def cancel_requests(self) -> None:
905
- """Cancel all TimeoutRetryHandlers."""
906
- await self.cancel_status_request_handlers()
907
- await self.serials_request_handler.cancel()
908
- await self.name_request_handler.cancel()
909
- await self.oem_text_request_handler.cancel()
910
- await self.static_groups_request_handler.cancel()
911
- await self.dynamic_groups_request_handler.cancel()
912
-
913
956
  def set_s0_enabled(self, s0_enabled: bool) -> None:
914
957
  """Set the activation status for S0 variables.
915
958
 
@@ -947,14 +990,10 @@ class ModuleConnection(AbstractConnection):
947
990
  await self.on_ack(inp.code)
948
991
  return None
949
992
 
950
- # handle typeless variable responses
951
- if isinstance(inp, inputs.ModStatusVar):
952
- inp = self.status_requests_handler.preprocess_modstatusvar(inp)
953
-
954
993
  for input_callback in self.input_callbacks:
955
994
  input_callback(inp)
956
995
 
957
- def dump_details(self) -> dict[str, Any]:
996
+ async def dump_details(self) -> dict[str, Any]:
958
997
  """Dump detailed information about this module."""
959
998
  is_local_segment = self.addr.seg_id in (0, self.conn.local_seg_id)
960
999
  return {
@@ -962,95 +1001,242 @@ class ModuleConnection(AbstractConnection):
962
1001
  "address": self.addr.addr_id,
963
1002
  "is_local_segment": is_local_segment,
964
1003
  "serials": {
965
- "hardware_serial": f"{self.hardware_serial:10X}",
966
- "manu": f"{self.manu:02X}",
967
- "software_serial": f"{self.software_serial:06X}",
968
- "hardware_type": f"{self.hardware_type.value:d}",
969
- "hardware_name": self.hardware_type.description,
1004
+ "hardware_serial": f"{self.serials.hardware_serial:10X}",
1005
+ "manu": f"{self.serials.manu:02X}",
1006
+ "software_serial": f"{self.serials.software_serial:06X}",
1007
+ "hardware_type": f"{self.serials.hardware_type.value:d}",
1008
+ "hardware_name": self.serials.hardware_type.description,
970
1009
  },
971
- "name": self.name,
972
- "comment": self.comment,
973
- "oem_text": self.oem_text,
1010
+ "name": await self.request_name(),
1011
+ "comment": await self.request_comment(),
1012
+ "oem_text": await self.request_oem_text(),
974
1013
  "groups": {
975
- "static": sorted(addr.addr_id for addr in self.static_groups),
976
- "dynamic": sorted(addr.addr_id for addr in self.dynamic_groups),
1014
+ "static": sorted(
1015
+ addr.addr_id
1016
+ for addr in await self.request_group_memberships(dynamic=False)
1017
+ ),
1018
+ "dynamic": sorted(
1019
+ addr.addr_id
1020
+ for addr in await self.request_group_memberships(dynamic=True)
1021
+ ),
977
1022
  },
978
1023
  }
979
1024
 
980
- # ##
981
- # ## Requests
982
- # ##
1025
+ # Request status methods
1026
+
1027
+ async def request_status_output(
1028
+ self, output_port: lcn_defs.OutputPort, max_age: int = 0
1029
+ ) -> inputs.ModStatusOutput | None:
1030
+ """Request the status of an output port from a module."""
1031
+ result = await self.status_requester.request(
1032
+ response_type=inputs.ModStatusOutput,
1033
+ request_pck=PckGenerator.request_output_status(output_id=output_port.value),
1034
+ max_age=max_age,
1035
+ output_id=output_port.value,
1036
+ )
983
1037
 
984
- # ## properties
1038
+ return cast(inputs.ModStatusOutput, result)
985
1039
 
986
- @property
987
- def serials(self) -> dict[str, int | lcn_defs.HardwareType]:
988
- """Return serials number information."""
989
- return self.serials_request_handler.serials
1040
+ async def request_status_relays(
1041
+ self, max_age: int = 0
1042
+ ) -> inputs.ModStatusRelays | None:
1043
+ """Request the status of relays from a module."""
1044
+ result = await self.status_requester.request(
1045
+ response_type=inputs.ModStatusRelays,
1046
+ request_pck=PckGenerator.request_relays_status(),
1047
+ max_age=max_age,
1048
+ )
990
1049
 
991
- @property
992
- def name(self) -> str:
993
- """Return stored name."""
994
- return self.name_request_handler.name
1050
+ return cast(inputs.ModStatusRelays, result)
995
1051
 
996
- @property
997
- def comment(self) -> str:
998
- """Return stored comments."""
999
- return self.comment_request_handler.comment
1052
+ async def request_status_motor_position(
1053
+ self,
1054
+ motor: lcn_defs.MotorPort,
1055
+ positioning_mode: lcn_defs.MotorPositioningMode,
1056
+ max_age: int = 0,
1057
+ ) -> inputs.ModStatusMotorPositionBS4 | None:
1058
+ """Request the status of motor positions from a module."""
1059
+ if motor not in (
1060
+ lcn_defs.MotorPort.MOTOR1,
1061
+ lcn_defs.MotorPort.MOTOR2,
1062
+ lcn_defs.MotorPort.MOTOR3,
1063
+ lcn_defs.MotorPort.MOTOR4,
1064
+ ):
1065
+ _LOGGER.debug(
1066
+ "Only MOTOR1 to MOTOR4 are supported for motor position requests."
1067
+ )
1068
+ return None
1069
+ if positioning_mode != lcn_defs.MotorPositioningMode.BS4:
1070
+ _LOGGER.debug("Only BS4 mode is supported for motor position requests.")
1071
+ return None
1000
1072
 
1001
- @property
1002
- def oem_text(self) -> list[str]:
1003
- """Return stored OEM text."""
1004
- return self.oem_text_request_handler.oem_text
1073
+ result = await self.status_requester.request(
1074
+ response_type=inputs.ModStatusMotorPositionBS4,
1075
+ request_pck=PckGenerator.request_motor_position_status(motor.value // 2),
1076
+ max_age=max_age,
1077
+ motor=motor.value,
1078
+ )
1005
1079
 
1006
- @property
1007
- def static_groups(self) -> set[LcnAddr]:
1008
- """Return static group membership."""
1009
- return self.static_groups_request_handler.groups
1080
+ return cast(inputs.ModStatusMotorPositionBS4, result)
1010
1081
 
1011
- @property
1012
- def dynamic_groups(self) -> set[LcnAddr]:
1013
- """Return dynamic group membership."""
1014
- return self.dynamic_groups_request_handler.groups
1082
+ async def request_status_binary_sensors(
1083
+ self, max_age: int = 0
1084
+ ) -> inputs.ModStatusBinSensors | None:
1085
+ """Request the status of binary sensors from a module."""
1086
+ result = await self.status_requester.request(
1087
+ response_type=inputs.ModStatusBinSensors,
1088
+ request_pck=PckGenerator.request_bin_sensors_status(),
1089
+ max_age=max_age,
1090
+ )
1015
1091
 
1016
- @property
1017
- def groups(self) -> set[LcnAddr]:
1018
- """Return static and dynamic group membership."""
1019
- return self.static_groups | self.dynamic_groups
1092
+ return cast(inputs.ModStatusBinSensors, result)
1020
1093
 
1021
- # ## future properties
1094
+ async def request_status_variable(
1095
+ self,
1096
+ variable: lcn_defs.Var,
1097
+ max_age: int = 0,
1098
+ ) -> inputs.ModStatusVar | None:
1099
+ """Request the status of a variable from a module."""
1100
+ # do not use buffered response for old modules
1101
+ # (variable response is typeless)
1102
+ if self.serials.software_serial < 0x170206:
1103
+ max_age = 0
1104
+
1105
+ result = await self.status_requester.request(
1106
+ response_type=inputs.ModStatusVar,
1107
+ request_pck=PckGenerator.request_var_status(
1108
+ variable, self.serials.software_serial
1109
+ ),
1110
+ max_age=max_age,
1111
+ var=variable,
1112
+ )
1022
1113
 
1023
- @property
1024
- def serial_known(self) -> Awaitable[bool]:
1025
- """Check if serials have already been received from module."""
1026
- return self.serials_request_handler.serial_known.wait()
1114
+ result = cast(inputs.ModStatusVar, result)
1115
+ if result:
1116
+ if result.orig_var == lcn_defs.Var.UNKNOWN:
1117
+ # Response without type (%Msssaaa.wwwww)
1118
+ result.var = variable
1119
+ return result
1120
+
1121
+ async def request_status_led_and_logic_ops(
1122
+ self, max_age: int = 0
1123
+ ) -> inputs.ModStatusLedsAndLogicOps | None:
1124
+ """Request the status of LEDs and logic operations from a module."""
1125
+ result = await self.status_requester.request(
1126
+ response_type=inputs.ModStatusLedsAndLogicOps,
1127
+ request_pck=PckGenerator.request_leds_and_logic_ops(),
1128
+ max_age=max_age,
1129
+ )
1130
+
1131
+ return cast(inputs.ModStatusLedsAndLogicOps, result)
1132
+
1133
+ async def request_status_locked_keys(
1134
+ self, max_age: int = 0
1135
+ ) -> inputs.ModStatusKeyLocks | None:
1136
+ """Request the status of locked keys from a module."""
1137
+ result = await self.status_requester.request(
1138
+ response_type=inputs.ModStatusKeyLocks,
1139
+ request_pck=PckGenerator.request_key_lock_status(),
1140
+ max_age=max_age,
1141
+ )
1142
+
1143
+ return cast(inputs.ModStatusKeyLocks, result)
1144
+
1145
+ # Request module properties
1027
1146
 
1028
- async def request_serials(self) -> dict[str, int | lcn_defs.HardwareType]:
1147
+ async def request_serials(self, max_age: int = 0) -> Serials:
1029
1148
  """Request module serials."""
1030
- return await self.serials_request_handler.request()
1149
+ result = cast(
1150
+ inputs.ModSn | None,
1151
+ await self.status_requester.request(
1152
+ response_type=inputs.ModSn,
1153
+ request_pck=PckGenerator.request_serial(),
1154
+ max_age=max_age,
1155
+ ),
1156
+ )
1157
+
1158
+ if result is None:
1159
+ return Serials(-1, -1, -1, lcn_defs.HardwareType.UNKNOWN)
1160
+ return Serials(
1161
+ result.hardware_serial,
1162
+ result.manu,
1163
+ result.software_serial,
1164
+ result.hardware_type,
1165
+ )
1031
1166
 
1032
- async def request_name(self) -> str:
1167
+ async def request_name(self, max_age: int = 0) -> str | None:
1033
1168
  """Request module name."""
1034
- return await self.name_request_handler.request()
1169
+ coros = [
1170
+ self.status_requester.request(
1171
+ response_type=inputs.ModNameComment,
1172
+ request_pck=PckGenerator.request_name(block_id),
1173
+ max_age=max_age,
1174
+ command="N",
1175
+ block_id=block_id,
1176
+ )
1177
+ for block_id in [0, 1]
1178
+ ]
1035
1179
 
1036
- async def request_comment(self) -> str:
1037
- """Request comments from a module."""
1038
- return await self.comment_request_handler.request()
1180
+ coro_results = [await coro for coro in coros]
1181
+ if not all(coro_results):
1182
+ return None
1183
+ results = cast(list[inputs.ModNameComment], coro_results)
1184
+ name = "".join([result.text for result in results if result])
1185
+ return name
1039
1186
 
1040
- async def request_oem_text(self) -> list[str]:
1041
- """Request OEM text from a module."""
1042
- return await self.oem_text_request_handler.request()
1187
+ async def request_comment(self, max_age: int = 0) -> str | None:
1188
+ """Request module name."""
1189
+ coros = [
1190
+ self.status_requester.request(
1191
+ response_type=inputs.ModNameComment,
1192
+ request_pck=PckGenerator.request_comment(block_id),
1193
+ max_age=max_age,
1194
+ command="K",
1195
+ block_id=block_id,
1196
+ )
1197
+ for block_id in [0, 1, 2]
1198
+ ]
1043
1199
 
1044
- async def request_static_groups(self) -> set[LcnAddr]:
1045
- """Request module static group memberships."""
1046
- return set(await self.static_groups_request_handler.request())
1200
+ coro_results = [await coro for coro in coros]
1201
+ if not all(coro_results):
1202
+ return None
1203
+ results = cast(list[inputs.ModNameComment], coro_results)
1204
+ name = "".join([result.text for result in results if result])
1205
+ return name
1206
+
1207
+ async def request_oem_text(self, max_age: int = 0) -> str | None:
1208
+ """Request module name."""
1209
+ coros = [
1210
+ self.status_requester.request(
1211
+ response_type=inputs.ModNameComment,
1212
+ request_pck=PckGenerator.request_oem_text(block_id),
1213
+ max_age=max_age,
1214
+ command="O",
1215
+ block_id=block_id,
1216
+ )
1217
+ for block_id in [0, 1, 2, 3]
1218
+ ]
1047
1219
 
1048
- async def request_dynamic_groups(self) -> set[LcnAddr]:
1049
- """Request module dynamic group memberships."""
1050
- return set(await self.dynamic_groups_request_handler.request())
1220
+ coro_results = [await coro for coro in coros]
1221
+ if not all(coro_results):
1222
+ return None
1223
+ results = cast(list[inputs.ModNameComment], coro_results)
1224
+ name = "".join([result.text for result in results if result])
1225
+ return name
1226
+
1227
+ async def request_group_memberships(
1228
+ self, dynamic: bool = False, max_age: int = 0
1229
+ ) -> set[LcnAddr]:
1230
+ """Request module static/dynamic group memberships."""
1231
+ result = await self.status_requester.request(
1232
+ response_type=inputs.ModStatusGroups,
1233
+ request_pck=(
1234
+ PckGenerator.request_group_membership_dynamic()
1235
+ if dynamic
1236
+ else PckGenerator.request_group_membership_static()
1237
+ ),
1238
+ max_age=max_age,
1239
+ dynamic=dynamic,
1240
+ )
1051
1241
 
1052
- async def request_groups(self) -> set[LcnAddr]:
1053
- """Request module group memberships."""
1054
- static_groups = await self.static_groups_request_handler.request()
1055
- dynamic_groups = await self.dynamic_groups_request_handler.request()
1056
- return static_groups | dynamic_groups
1242
+ return set(cast(inputs.ModStatusGroups, result).groups)