pypck 0.8.10__py3-none-any.whl → 0.9.1__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
 
@@ -678,18 +781,18 @@ class GroupConnection(AbstractConnection):
678
781
  self,
679
782
  conn: PchkConnectionManager,
680
783
  addr: LcnAddr,
681
- software_serial: int = 0x170206,
682
784
  ):
683
785
  """Construct GroupConnection instance."""
684
786
  assert addr.is_group
685
- super().__init__(conn, addr, software_serial=software_serial, wants_ack=False)
787
+ super().__init__(conn, addr, wants_ack=False)
788
+ self._serials_known.set()
686
789
 
687
790
  async def var_abs(
688
791
  self,
689
792
  var: lcn_defs.Var,
690
793
  value: float | lcn_defs.VarValue,
691
794
  unit: lcn_defs.VarUnit = lcn_defs.VarUnit.NATIVE,
692
- software_serial: int | None = None,
795
+ software_serial: int = -1,
693
796
  ) -> bool:
694
797
  """Send a command to set the absolute value to a variable.
695
798
 
@@ -737,7 +840,7 @@ class GroupConnection(AbstractConnection):
737
840
  value: float | lcn_defs.VarValue,
738
841
  unit: lcn_defs.VarUnit = lcn_defs.VarUnit.NATIVE,
739
842
  value_ref: lcn_defs.RelVarRef = lcn_defs.RelVarRef.CURRENT,
740
- software_serial: int | None = None,
843
+ software_serial: int = -1,
741
844
  ) -> bool:
742
845
  """Send a command to change the value of a variable.
743
846
 
@@ -763,15 +866,6 @@ class GroupConnection(AbstractConnection):
763
866
  result &= await super().var_rel(var, value, software_serial=0)
764
867
  return result
765
868
 
766
- async def activate_status_request_handler(self, item: Any, option: Any) -> None:
767
- """Activate a specific TimeoutRetryHandler for status requests."""
768
- await self.conn.segment_scan_completed_event.wait()
769
-
770
- async def activate_status_request_handlers(self) -> None:
771
- """Activate all TimeoutRetryHandlers for status requests."""
772
- # self.request_serial.activate()
773
- await self.conn.segment_scan_completed_event.wait()
774
-
775
869
 
776
870
  class ModuleConnection(AbstractConnection):
777
871
  """Organizes communication with a specific module or group."""
@@ -780,17 +874,12 @@ class ModuleConnection(AbstractConnection):
780
874
  self,
781
875
  conn: PchkConnectionManager,
782
876
  addr: LcnAddr,
783
- activate_status_requests: bool = False,
784
877
  has_s0_enabled: bool = False,
785
- software_serial: int | None = None,
786
878
  wants_ack: bool = True,
787
879
  ):
788
880
  """Construct ModuleConnection instance."""
789
881
  assert not addr.is_group
790
- super().__init__(
791
- conn, addr, software_serial=software_serial, wants_ack=wants_ack
792
- )
793
- self.activate_status_requests = activate_status_requests
882
+ super().__init__(conn, addr, wants_ack=wants_ack)
794
883
  self.has_s0_enabled = has_s0_enabled
795
884
 
796
885
  self.input_callbacks: set[Callable[[inputs.Input], None]] = set()
@@ -798,34 +887,15 @@ class ModuleConnection(AbstractConnection):
798
887
  # List of queued acknowledge codes from the LCN modules.
799
888
  self.acknowledges: asyncio.Queue[int] = asyncio.Queue()
800
889
 
801
- # RequestHandlers
802
- num_tries: int = self.conn.settings["NUM_TRIES"]
803
- timeout: int = self.conn.settings["DEFAULT_TIMEOUT"]
890
+ # StatusRequester
891
+ self.status_requester = StatusRequester(self)
804
892
 
805
- # Serial Number request
806
- self.serials_request_handler = SerialRequestHandler(
807
- self,
808
- num_tries,
809
- timeout,
810
- software_serial=software_serial,
811
- )
893
+ self.task_registry.create_task(self.request_module_properties())
812
894
 
813
- # Name, Comment, OemText requests
814
- self.name_request_handler = NameRequestHandler(self, num_tries, timeout)
815
- self.comment_request_handler = CommentRequestHandler(self, num_tries, timeout)
816
- self.oem_text_request_handler = OemTextRequestHandler(self, num_tries, timeout)
817
-
818
- # Group membership request
819
- self.static_groups_request_handler = GroupMembershipStaticRequestHandler(
820
- self, num_tries, timeout
821
- )
822
- self.dynamic_groups_request_handler = GroupMembershipDynamicRequestHandler(
823
- self, num_tries, timeout
824
- )
825
-
826
- self.status_requests_handler = StatusRequestsHandler(self)
827
- if self.activate_status_requests:
828
- self.task_registry.create_task(self.activate_status_request_handlers())
895
+ async def request_module_properties(self) -> None:
896
+ """Request module properties (serials)."""
897
+ self.serials = await self.request_serials()
898
+ self._serials_known.set()
829
899
 
830
900
  async def send_command(self, wants_ack: bool, pck: str | bytes) -> bool:
831
901
  """Send a command to the module represented by this class.
@@ -838,6 +908,10 @@ class ModuleConnection(AbstractConnection):
838
908
 
839
909
  return await super().send_command(False, pck)
840
910
 
911
+ async def serials_known(self) -> None:
912
+ """Wait until the serials of this module are known."""
913
+ await self._serials_known.wait()
914
+
841
915
  # ##
842
916
  # ## Retry logic if an acknowledge is requested
843
917
  # ##
@@ -876,37 +950,6 @@ class ModuleConnection(AbstractConnection):
876
950
  """
877
951
  await self.acknowledges.put(code)
878
952
 
879
- async def activate_status_request_handler(
880
- self, item: Any, option: Any = None
881
- ) -> None:
882
- """Activate a specific TimeoutRetryHandler for status requests."""
883
- self.task_registry.create_task(
884
- self.status_requests_handler.activate(item, option)
885
- )
886
-
887
- async def activate_status_request_handlers(self) -> None:
888
- """Activate all TimeoutRetryHandlers for status requests."""
889
- self.task_registry.create_task(
890
- self.status_requests_handler.activate_all(activate_s0=self.has_s0_enabled)
891
- )
892
-
893
- async def cancel_status_request_handler(self, item: Any) -> None:
894
- """Cancel a specific TimeoutRetryHandler for status requests."""
895
- await self.status_requests_handler.cancel(item)
896
-
897
- async def cancel_status_request_handlers(self) -> None:
898
- """Canecl all TimeoutRetryHandlers for status requests."""
899
- await self.status_requests_handler.cancel_all()
900
-
901
- async def cancel_requests(self) -> None:
902
- """Cancel all TimeoutRetryHandlers."""
903
- await self.cancel_status_request_handlers()
904
- await self.serials_request_handler.cancel()
905
- await self.name_request_handler.cancel()
906
- await self.oem_text_request_handler.cancel()
907
- await self.static_groups_request_handler.cancel()
908
- await self.dynamic_groups_request_handler.cancel()
909
-
910
953
  def set_s0_enabled(self, s0_enabled: bool) -> None:
911
954
  """Set the activation status for S0 variables.
912
955
 
@@ -944,14 +987,10 @@ class ModuleConnection(AbstractConnection):
944
987
  await self.on_ack(inp.code)
945
988
  return None
946
989
 
947
- # handle typeless variable responses
948
- if isinstance(inp, inputs.ModStatusVar):
949
- inp = self.status_requests_handler.preprocess_modstatusvar(inp)
950
-
951
990
  for input_callback in self.input_callbacks:
952
991
  input_callback(inp)
953
992
 
954
- def dump_details(self) -> dict[str, Any]:
993
+ async def dump_details(self) -> dict[str, Any]:
955
994
  """Dump detailed information about this module."""
956
995
  is_local_segment = self.addr.seg_id in (0, self.conn.local_seg_id)
957
996
  return {
@@ -959,95 +998,242 @@ class ModuleConnection(AbstractConnection):
959
998
  "address": self.addr.addr_id,
960
999
  "is_local_segment": is_local_segment,
961
1000
  "serials": {
962
- "hardware_serial": f"{self.hardware_serial:10X}",
963
- "manu": f"{self.manu:02X}",
964
- "software_serial": f"{self.software_serial:06X}",
965
- "hardware_type": f"{self.hardware_type.value:d}",
966
- "hardware_name": self.hardware_type.description,
1001
+ "hardware_serial": f"{self.serials.hardware_serial:10X}",
1002
+ "manu": f"{self.serials.manu:02X}",
1003
+ "software_serial": f"{self.serials.software_serial:06X}",
1004
+ "hardware_type": f"{self.serials.hardware_type.value:d}",
1005
+ "hardware_name": self.serials.hardware_type.description,
967
1006
  },
968
- "name": self.name,
969
- "comment": self.comment,
970
- "oem_text": self.oem_text,
1007
+ "name": await self.request_name(),
1008
+ "comment": await self.request_comment(),
1009
+ "oem_text": await self.request_oem_text(),
971
1010
  "groups": {
972
- "static": sorted(addr.addr_id for addr in self.static_groups),
973
- "dynamic": sorted(addr.addr_id for addr in self.dynamic_groups),
1011
+ "static": sorted(
1012
+ addr.addr_id
1013
+ for addr in await self.request_group_memberships(dynamic=False)
1014
+ ),
1015
+ "dynamic": sorted(
1016
+ addr.addr_id
1017
+ for addr in await self.request_group_memberships(dynamic=True)
1018
+ ),
974
1019
  },
975
1020
  }
976
1021
 
977
- # ##
978
- # ## Requests
979
- # ##
1022
+ # Request status methods
1023
+
1024
+ async def request_status_output(
1025
+ self, output_port: lcn_defs.OutputPort, max_age: int = 0
1026
+ ) -> inputs.ModStatusOutput | None:
1027
+ """Request the status of an output port from a module."""
1028
+ result = await self.status_requester.request(
1029
+ response_type=inputs.ModStatusOutput,
1030
+ request_pck=PckGenerator.request_output_status(output_id=output_port.value),
1031
+ max_age=max_age,
1032
+ output_id=output_port.value,
1033
+ )
980
1034
 
981
- # ## properties
1035
+ return cast(inputs.ModStatusOutput, result)
982
1036
 
983
- @property
984
- def serials(self) -> dict[str, int | lcn_defs.HardwareType]:
985
- """Return serials number information."""
986
- return self.serials_request_handler.serials
1037
+ async def request_status_relays(
1038
+ self, max_age: int = 0
1039
+ ) -> inputs.ModStatusRelays | None:
1040
+ """Request the status of relays from a module."""
1041
+ result = await self.status_requester.request(
1042
+ response_type=inputs.ModStatusRelays,
1043
+ request_pck=PckGenerator.request_relays_status(),
1044
+ max_age=max_age,
1045
+ )
987
1046
 
988
- @property
989
- def name(self) -> str:
990
- """Return stored name."""
991
- return self.name_request_handler.name
1047
+ return cast(inputs.ModStatusRelays, result)
992
1048
 
993
- @property
994
- def comment(self) -> str:
995
- """Return stored comments."""
996
- return self.comment_request_handler.comment
1049
+ async def request_status_motor_position(
1050
+ self,
1051
+ motor: lcn_defs.MotorPort,
1052
+ positioning_mode: lcn_defs.MotorPositioningMode,
1053
+ max_age: int = 0,
1054
+ ) -> inputs.ModStatusMotorPositionBS4 | None:
1055
+ """Request the status of motor positions from a module."""
1056
+ if motor not in (
1057
+ lcn_defs.MotorPort.MOTOR1,
1058
+ lcn_defs.MotorPort.MOTOR2,
1059
+ lcn_defs.MotorPort.MOTOR3,
1060
+ lcn_defs.MotorPort.MOTOR4,
1061
+ ):
1062
+ _LOGGER.debug(
1063
+ "Only MOTOR1 to MOTOR4 are supported for motor position requests."
1064
+ )
1065
+ return None
1066
+ if positioning_mode != lcn_defs.MotorPositioningMode.BS4:
1067
+ _LOGGER.debug("Only BS4 mode is supported for motor position requests.")
1068
+ return None
997
1069
 
998
- @property
999
- def oem_text(self) -> list[str]:
1000
- """Return stored OEM text."""
1001
- return self.oem_text_request_handler.oem_text
1070
+ result = await self.status_requester.request(
1071
+ response_type=inputs.ModStatusMotorPositionBS4,
1072
+ request_pck=PckGenerator.request_motor_position_status(motor.value // 2),
1073
+ max_age=max_age,
1074
+ motor=motor.value,
1075
+ )
1002
1076
 
1003
- @property
1004
- def static_groups(self) -> set[LcnAddr]:
1005
- """Return static group membership."""
1006
- return self.static_groups_request_handler.groups
1077
+ return cast(inputs.ModStatusMotorPositionBS4, result)
1007
1078
 
1008
- @property
1009
- def dynamic_groups(self) -> set[LcnAddr]:
1010
- """Return dynamic group membership."""
1011
- return self.dynamic_groups_request_handler.groups
1079
+ async def request_status_binary_sensors(
1080
+ self, max_age: int = 0
1081
+ ) -> inputs.ModStatusBinSensors | None:
1082
+ """Request the status of binary sensors from a module."""
1083
+ result = await self.status_requester.request(
1084
+ response_type=inputs.ModStatusBinSensors,
1085
+ request_pck=PckGenerator.request_bin_sensors_status(),
1086
+ max_age=max_age,
1087
+ )
1012
1088
 
1013
- @property
1014
- def groups(self) -> set[LcnAddr]:
1015
- """Return static and dynamic group membership."""
1016
- return self.static_groups | self.dynamic_groups
1089
+ return cast(inputs.ModStatusBinSensors, result)
1017
1090
 
1018
- # ## future properties
1091
+ async def request_status_variable(
1092
+ self,
1093
+ variable: lcn_defs.Var,
1094
+ max_age: int = 0,
1095
+ ) -> inputs.ModStatusVar | None:
1096
+ """Request the status of a variable from a module."""
1097
+ # do not use buffered response for old modules
1098
+ # (variable response is typeless)
1099
+ if self.serials.software_serial < 0x170206:
1100
+ max_age = 0
1101
+
1102
+ result = await self.status_requester.request(
1103
+ response_type=inputs.ModStatusVar,
1104
+ request_pck=PckGenerator.request_var_status(
1105
+ variable, self.serials.software_serial
1106
+ ),
1107
+ max_age=max_age,
1108
+ var=variable,
1109
+ )
1019
1110
 
1020
- @property
1021
- def serial_known(self) -> Awaitable[bool]:
1022
- """Check if serials have already been received from module."""
1023
- return self.serials_request_handler.serial_known.wait()
1111
+ result = cast(inputs.ModStatusVar, result)
1112
+ if result:
1113
+ if result.orig_var == lcn_defs.Var.UNKNOWN:
1114
+ # Response without type (%Msssaaa.wwwww)
1115
+ result.var = variable
1116
+ return result
1117
+
1118
+ async def request_status_led_and_logic_ops(
1119
+ self, max_age: int = 0
1120
+ ) -> inputs.ModStatusLedsAndLogicOps | None:
1121
+ """Request the status of LEDs and logic operations from a module."""
1122
+ result = await self.status_requester.request(
1123
+ response_type=inputs.ModStatusLedsAndLogicOps,
1124
+ request_pck=PckGenerator.request_leds_and_logic_ops(),
1125
+ max_age=max_age,
1126
+ )
1127
+
1128
+ return cast(inputs.ModStatusLedsAndLogicOps, result)
1129
+
1130
+ async def request_status_locked_keys(
1131
+ self, max_age: int = 0
1132
+ ) -> inputs.ModStatusKeyLocks | None:
1133
+ """Request the status of locked keys from a module."""
1134
+ result = await self.status_requester.request(
1135
+ response_type=inputs.ModStatusKeyLocks,
1136
+ request_pck=PckGenerator.request_key_lock_status(),
1137
+ max_age=max_age,
1138
+ )
1139
+
1140
+ return cast(inputs.ModStatusKeyLocks, result)
1141
+
1142
+ # Request module properties
1024
1143
 
1025
- async def request_serials(self) -> dict[str, int | lcn_defs.HardwareType]:
1144
+ async def request_serials(self, max_age: int = 0) -> Serials:
1026
1145
  """Request module serials."""
1027
- return await self.serials_request_handler.request()
1146
+ result = cast(
1147
+ inputs.ModSn | None,
1148
+ await self.status_requester.request(
1149
+ response_type=inputs.ModSn,
1150
+ request_pck=PckGenerator.request_serial(),
1151
+ max_age=max_age,
1152
+ ),
1153
+ )
1154
+
1155
+ if result is None:
1156
+ return Serials(-1, -1, -1, lcn_defs.HardwareType.UNKNOWN)
1157
+ return Serials(
1158
+ result.hardware_serial,
1159
+ result.manu,
1160
+ result.software_serial,
1161
+ result.hardware_type,
1162
+ )
1028
1163
 
1029
- async def request_name(self) -> str:
1164
+ async def request_name(self, max_age: int = 0) -> str | None:
1030
1165
  """Request module name."""
1031
- return await self.name_request_handler.request()
1166
+ coros = [
1167
+ self.status_requester.request(
1168
+ response_type=inputs.ModNameComment,
1169
+ request_pck=PckGenerator.request_name(block_id),
1170
+ max_age=max_age,
1171
+ command="N",
1172
+ block_id=block_id,
1173
+ )
1174
+ for block_id in [0, 1]
1175
+ ]
1032
1176
 
1033
- async def request_comment(self) -> str:
1034
- """Request comments from a module."""
1035
- return await self.comment_request_handler.request()
1177
+ coro_results = [await coro for coro in coros]
1178
+ if not all(coro_results):
1179
+ return None
1180
+ results = cast(list[inputs.ModNameComment], coro_results)
1181
+ name = "".join([result.text for result in results if result])
1182
+ return name
1036
1183
 
1037
- async def request_oem_text(self) -> list[str]:
1038
- """Request OEM text from a module."""
1039
- return await self.oem_text_request_handler.request()
1184
+ async def request_comment(self, max_age: int = 0) -> str | None:
1185
+ """Request module name."""
1186
+ coros = [
1187
+ self.status_requester.request(
1188
+ response_type=inputs.ModNameComment,
1189
+ request_pck=PckGenerator.request_comment(block_id),
1190
+ max_age=max_age,
1191
+ command="K",
1192
+ block_id=block_id,
1193
+ )
1194
+ for block_id in [0, 1, 2]
1195
+ ]
1040
1196
 
1041
- async def request_static_groups(self) -> set[LcnAddr]:
1042
- """Request module static group memberships."""
1043
- return set(await self.static_groups_request_handler.request())
1197
+ coro_results = [await coro for coro in coros]
1198
+ if not all(coro_results):
1199
+ return None
1200
+ results = cast(list[inputs.ModNameComment], coro_results)
1201
+ name = "".join([result.text for result in results if result])
1202
+ return name
1203
+
1204
+ async def request_oem_text(self, max_age: int = 0) -> str | None:
1205
+ """Request module name."""
1206
+ coros = [
1207
+ self.status_requester.request(
1208
+ response_type=inputs.ModNameComment,
1209
+ request_pck=PckGenerator.request_oem_text(block_id),
1210
+ max_age=max_age,
1211
+ command="O",
1212
+ block_id=block_id,
1213
+ )
1214
+ for block_id in [0, 1, 2, 3]
1215
+ ]
1044
1216
 
1045
- async def request_dynamic_groups(self) -> set[LcnAddr]:
1046
- """Request module dynamic group memberships."""
1047
- return set(await self.dynamic_groups_request_handler.request())
1217
+ coro_results = [await coro for coro in coros]
1218
+ if not all(coro_results):
1219
+ return None
1220
+ results = cast(list[inputs.ModNameComment], coro_results)
1221
+ name = "".join([result.text for result in results if result])
1222
+ return name
1223
+
1224
+ async def request_group_memberships(
1225
+ self, dynamic: bool = False, max_age: int = 0
1226
+ ) -> set[LcnAddr]:
1227
+ """Request module static/dynamic group memberships."""
1228
+ result = await self.status_requester.request(
1229
+ response_type=inputs.ModStatusGroups,
1230
+ request_pck=(
1231
+ PckGenerator.request_group_membership_dynamic()
1232
+ if dynamic
1233
+ else PckGenerator.request_group_membership_static()
1234
+ ),
1235
+ max_age=max_age,
1236
+ dynamic=dynamic,
1237
+ )
1048
1238
 
1049
- async def request_groups(self) -> set[LcnAddr]:
1050
- """Request module group memberships."""
1051
- static_groups = await self.static_groups_request_handler.request()
1052
- dynamic_groups = await self.dynamic_groups_request_handler.request()
1053
- return static_groups | dynamic_groups
1239
+ return set(cast(inputs.ModStatusGroups, result).groups)