pypck 0.9.1__py3-none-any.whl → 0.9.3__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/__init__.py CHANGED
@@ -2,20 +2,22 @@
2
2
 
3
3
  from pypck import (
4
4
  connection,
5
+ device,
5
6
  helpers,
6
7
  inputs,
7
8
  lcn_addr,
8
9
  lcn_defs,
9
- module,
10
10
  pck_commands,
11
+ status_requester,
11
12
  )
12
13
 
13
14
  __all__ = [
14
15
  "connection",
15
- "inputs",
16
+ "device",
16
17
  "helpers",
18
+ "inputs",
17
19
  "lcn_addr",
18
20
  "lcn_defs",
19
- "module",
20
21
  "pck_commands",
22
+ "status_requester",
21
23
  ]
pypck/connection.py CHANGED
@@ -9,10 +9,10 @@ from types import TracebackType
9
9
  from typing import Any
10
10
 
11
11
  from pypck import inputs, lcn_defs
12
+ from pypck.device import DeviceConnection
12
13
  from pypck.helpers import TaskRegistry
13
14
  from pypck.lcn_addr import LcnAddr
14
15
  from pypck.lcn_defs import LcnEvent
15
- from pypck.module import GroupConnection, ModuleConnection
16
16
  from pypck.pck_commands import PckGenerator
17
17
 
18
18
  _LOGGER = logging.getLogger(__name__)
@@ -131,7 +131,7 @@ class PchkConnectionManager:
131
131
  # stored in this dictionary. Communication to groups is handled by
132
132
  # GroupConnection object that are created on the fly and not stored
133
133
  # permanently.
134
- self.address_conns: dict[LcnAddr, ModuleConnection] = {}
134
+ self.device_connections: dict[LcnAddr, DeviceConnection] = {}
135
135
  self.segment_coupler_ids: list[int] = []
136
136
 
137
137
  self.input_callbacks: set[Callable[[inputs.Input], None]] = set()
@@ -360,15 +360,15 @@ class PchkConnectionManager:
360
360
  old_local_seg_id = self.local_seg_id
361
361
 
362
362
  self.local_seg_id = local_seg_id
363
- # replace all address_conns with current local_seg_id with new
363
+ # replace all device_connections with current local_seg_id with new
364
364
  # local_seg_id
365
- for addr in list(self.address_conns):
365
+ for addr in list(self.device_connections):
366
366
  if addr.seg_id == old_local_seg_id:
367
- address_conn = self.address_conns.pop(addr)
367
+ address_conn = self.device_connections.pop(addr)
368
368
  address_conn.addr = LcnAddr(
369
369
  self.local_seg_id, addr.addr_id, addr.is_group
370
370
  )
371
- self.address_conns[address_conn.addr] = address_conn
371
+ self.device_connections[address_conn.addr] = address_conn
372
372
 
373
373
  def physical_to_logical(self, addr: LcnAddr) -> LcnAddr:
374
374
  """Convert the physical segment id of an address to the logical one."""
@@ -378,39 +378,28 @@ class PchkConnectionManager:
378
378
  addr.is_group,
379
379
  )
380
380
 
381
- def get_module_conn(self, addr: LcnAddr) -> ModuleConnection:
382
- """Create and/or return the given LCN module."""
383
- assert not addr.is_group
381
+ def get_device_connection(self, addr: LcnAddr) -> DeviceConnection:
382
+ """Create and/or return a connection to the given module or group."""
384
383
  if addr.seg_id == 0 and self.local_seg_id != -1:
385
384
  addr = LcnAddr(self.local_seg_id, addr.addr_id, addr.is_group)
386
- address_conn = self.address_conns.get(addr, None)
387
- if address_conn is None:
388
- address_conn = ModuleConnection(
389
- self, addr, wants_ack=self.settings["ACKNOWLEDGE"]
390
- )
391
- self.address_conns[addr] = address_conn
392
385
 
393
- return address_conn
394
-
395
- def get_group_conn(self, addr: LcnAddr) -> GroupConnection:
396
- """Create and return the GroupConnection for the given group."""
397
- assert addr.is_group
398
- if addr.seg_id == 0 and self.local_seg_id != -1:
399
- addr = LcnAddr(self.local_seg_id, addr.addr_id, addr.is_group)
400
- return GroupConnection(self, addr)
386
+ device_connection = self.device_connections.get(addr, None)
387
+ if device_connection is None:
388
+ device_connection = DeviceConnection(
389
+ self,
390
+ addr,
391
+ wants_ack=False if addr.is_group else self.settings["ACKNOWLEDGE"],
392
+ )
393
+ self.device_connections[addr] = device_connection
401
394
 
402
- def get_address_conn(self, addr: LcnAddr) -> ModuleConnection | GroupConnection:
403
- """Create and/or return a connection to the given module or group."""
404
- if addr.is_group:
405
- return self.get_group_conn(addr)
406
- return self.get_module_conn(addr)
395
+ return device_connection
407
396
 
408
397
  # Other
409
398
 
410
399
  async def dump_modules(self) -> dict[str, dict[str, dict[str, Any]]]:
411
400
  """Dump all modules and information about them in a JSON serializable dict."""
412
401
  dump: dict[str, dict[str, dict[str, Any]]] = {}
413
- for address_conn in self.address_conns.values():
402
+ for address_conn in self.device_connections.values():
414
403
  seg = f"{address_conn.addr.seg_id:d}"
415
404
  addr = f"{address_conn.addr.addr_id}"
416
405
  if seg not in dump:
@@ -484,7 +473,7 @@ class PchkConnectionManager:
484
473
  if isinstance(inp, inputs.ModInput):
485
474
  logical_source_addr = self.physical_to_logical(inp.physical_source_addr)
486
475
  if not logical_source_addr.is_group:
487
- module_conn = self.get_module_conn(logical_source_addr)
476
+ module_conn = self.get_device_connection(logical_source_addr)
488
477
  if isinstance(inp, inputs.ModSn):
489
478
  # used to extend scan_modules() timeout
490
479
  if self.module_serial_number_received.locked():
@@ -1,17 +1,18 @@
1
- """Module and group classes."""
1
+ """LCN devices: Modules and groups."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
6
  import logging
7
7
  from collections.abc import Callable, Sequence
8
- from dataclasses import dataclass, field
8
+ from dataclasses import dataclass
9
9
  from typing import TYPE_CHECKING, Any, cast
10
10
 
11
11
  from pypck import inputs, lcn_defs
12
12
  from pypck.helpers import TaskRegistry
13
13
  from pypck.lcn_addr import LcnAddr
14
14
  from pypck.pck_commands import PckGenerator
15
+ from pypck.status_requester import StatusRequester
15
16
 
16
17
  if TYPE_CHECKING:
17
18
  from pypck.connection import PchkConnectionManager
@@ -29,153 +30,8 @@ class Serials:
29
30
  hardware_type: lcn_defs.HardwareType
30
31
 
31
32
 
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
-
176
-
177
- class AbstractConnection:
178
- """Organizes communication with a specific module.
33
+ class DeviceConnection:
34
+ """Organizes communication with a specific module/group.
179
35
 
180
36
  Sends status requests to the connection and handles status responses.
181
37
  """
@@ -193,6 +49,22 @@ class AbstractConnection:
193
49
  self.serials = Serials(-1, -1, -1, lcn_defs.HardwareType.UNKNOWN)
194
50
  self._serials_known = asyncio.Event()
195
51
 
52
+ self.input_callbacks: set[Callable[[inputs.Input], None]] = set()
53
+
54
+ # List of queued acknowledge codes from the LCN modules.
55
+ self.acknowledges: asyncio.Queue[lcn_defs.AcknowledgeErrorCode] = (
56
+ asyncio.Queue()
57
+ )
58
+
59
+ # StatusRequester
60
+ self.status_requester = StatusRequester(self)
61
+
62
+ if self.addr.is_group:
63
+ self.wants_ack = False # groups do not send acks
64
+ self._serials_known.set()
65
+ else:
66
+ self.task_registry.create_task(self._request_device_properties())
67
+
196
68
  @property
197
69
  def task_registry(self) -> TaskRegistry:
198
70
  """Get the task registry."""
@@ -216,6 +88,17 @@ class AbstractConnection:
216
88
  async def send_command(self, wants_ack: bool, pck: str | bytes) -> bool:
217
89
  """Send a command to the module represented by this class.
218
90
 
91
+ :param bool wants_ack: Also send a request for acknowledge.
92
+ :param str pck: PCK command (without header).
93
+ """
94
+ if not self.addr.is_group and wants_ack:
95
+ return await self.send_command_with_ack(pck)
96
+
97
+ return await self._send_command(wants_ack, pck)
98
+
99
+ async def _send_command(self, wants_ack: bool, pck: str | bytes) -> bool:
100
+ """Send a command to the module represented by this class.
101
+
219
102
  :param bool wants_ack: Also send a request for acknowledge.
220
103
  :param str pck: PCK command (without header).
221
104
  """
@@ -226,6 +109,50 @@ class AbstractConnection:
226
109
  return await self.conn.send_command(header + pck)
227
110
  return await self.conn.send_command(header.encode() + pck)
228
111
 
112
+ async def serials_known(self) -> None:
113
+ """Wait until the serials of this device are known."""
114
+ await self._serials_known.wait()
115
+
116
+ # ##
117
+ # ## Retry logic if an acknowledge is requested
118
+ # ##
119
+
120
+ async def send_command_with_ack(self, pck: str | bytes) -> bool:
121
+ """Send a PCK command and ensure receiving of an acknowledgement.
122
+
123
+ Resends the PCK command if no acknowledgement has been received
124
+ within timeout.
125
+
126
+ :param str pck: PCK command (without header).
127
+ :returns: True if acknowledge was received, False otherwise
128
+ :rtype: bool
129
+ """
130
+ count = 0
131
+ while count < self.conn.settings["NUM_TRIES"]:
132
+ await self._send_command(True, pck)
133
+ try:
134
+ code = await asyncio.wait_for(
135
+ self.acknowledges.get(),
136
+ timeout=self.conn.settings["DEFAULT_TIMEOUT"],
137
+ )
138
+ except asyncio.TimeoutError:
139
+ count += 1
140
+ continue
141
+ if code == lcn_defs.AcknowledgeErrorCode.OK:
142
+ return True
143
+ break
144
+ return False
145
+
146
+ async def on_ack(
147
+ self, code: lcn_defs.AcknowledgeErrorCode = lcn_defs.AcknowledgeErrorCode.OK
148
+ ) -> None:
149
+ """Is called whenever an acknowledge is received from the LCN module.
150
+
151
+ :param int code: The LCN internal code. -1 means
152
+ "positive" acknowledge
153
+ """
154
+ await self.acknowledges.put(code)
155
+
229
156
  # ##
230
157
  # ## Methods for sending PCK commands
231
158
  # ##
@@ -516,6 +443,22 @@ class AbstractConnection:
516
443
  :returns: True if command was sent successfully, False otherwise
517
444
  :rtype: bool
518
445
  """
446
+ if self.addr.is_group:
447
+ result = True
448
+ # for new modules (>=0x170206)
449
+ result &= await self.var_abs(var, value, unit, 0x170206)
450
+
451
+ # for old modules (<0x170206)
452
+ if var in [
453
+ lcn_defs.Var.TVAR,
454
+ lcn_defs.Var.R1VAR,
455
+ lcn_defs.Var.R2VAR,
456
+ lcn_defs.Var.R1VARSETPOINT,
457
+ lcn_defs.Var.R2VARSETPOINT,
458
+ ]:
459
+ result &= await self.var_abs(var, value, unit, 0x000000)
460
+ return result
461
+
519
462
  if not isinstance(value, lcn_defs.VarValue):
520
463
  value = lcn_defs.VarValue.from_var_unit(value, unit, True)
521
464
 
@@ -560,6 +503,19 @@ class AbstractConnection:
560
503
  :returns: True if command was sent successfully, False otherwise
561
504
  :rtype: bool
562
505
  """
506
+ if self.addr.is_group:
507
+ result = True
508
+ result &= await self.var_reset(var, 0x170206)
509
+ if var in [
510
+ lcn_defs.Var.TVAR,
511
+ lcn_defs.Var.R1VAR,
512
+ lcn_defs.Var.R2VAR,
513
+ lcn_defs.Var.R1VARSETPOINT,
514
+ lcn_defs.Var.R2VARSETPOINT,
515
+ ]:
516
+ result &= await self.var_reset(var, 0)
517
+ return result
518
+
563
519
  if software_serial == -1:
564
520
  await self._serials_known.wait()
565
521
  software_serial = self.serials.software_serial
@@ -586,6 +542,24 @@ class AbstractConnection:
586
542
  :returns: True if command was sent successfully, False otherwise
587
543
  :rtype: bool
588
544
  """
545
+ if self.addr.is_group:
546
+ result = True
547
+ result &= await self.var_rel(var, value, software_serial=0x170206)
548
+ if var in [
549
+ lcn_defs.Var.TVAR,
550
+ lcn_defs.Var.R1VAR,
551
+ lcn_defs.Var.R2VAR,
552
+ lcn_defs.Var.R1VARSETPOINT,
553
+ lcn_defs.Var.R2VARSETPOINT,
554
+ lcn_defs.Var.THRS1,
555
+ lcn_defs.Var.THRS2,
556
+ lcn_defs.Var.THRS3,
557
+ lcn_defs.Var.THRS4,
558
+ lcn_defs.Var.THRS5,
559
+ ]:
560
+ result &= await self.var_rel(var, value, software_serial=0)
561
+ return result
562
+
589
563
  if not isinstance(value, lcn_defs.VarValue):
590
564
  value = lcn_defs.VarValue.from_var_unit(value, unit, False)
591
565
 
@@ -699,7 +673,10 @@ class AbstractConnection:
699
673
  )
700
674
 
701
675
  async def lock_keys_tab_a_temporary(
702
- self, delay_time: int, delay_unit: lcn_defs.TimeUnit, states: list[bool]
676
+ self,
677
+ delay_time: int,
678
+ delay_unit: lcn_defs.TimeUnit,
679
+ states: list[lcn_defs.KeyLockStateModifier],
703
680
  ) -> bool:
704
681
  """Send a command to lock keys in table A temporary.
705
682
 
@@ -770,199 +747,6 @@ class AbstractConnection:
770
747
  """
771
748
  return await self.send_command(self.wants_ack, pck)
772
749
 
773
-
774
- class GroupConnection(AbstractConnection):
775
- """Organizes communication with a specific group.
776
-
777
- It is assumed that all modules within this group are newer than FW170206
778
- """
779
-
780
- def __init__(
781
- self,
782
- conn: PchkConnectionManager,
783
- addr: LcnAddr,
784
- ):
785
- """Construct GroupConnection instance."""
786
- assert addr.is_group
787
- super().__init__(conn, addr, wants_ack=False)
788
- self._serials_known.set()
789
-
790
- async def var_abs(
791
- self,
792
- var: lcn_defs.Var,
793
- value: float | lcn_defs.VarValue,
794
- unit: lcn_defs.VarUnit = lcn_defs.VarUnit.NATIVE,
795
- software_serial: int = -1,
796
- ) -> bool:
797
- """Send a command to set the absolute value to a variable.
798
-
799
- :param Var var: Variable
800
- :param float value: Absolute value to set
801
- :param VarUnit unit: Unit of variable
802
- """
803
- result = True
804
- # for new modules (>=0x170206)
805
- result &= await super().var_abs(var, value, unit, 0x170206)
806
-
807
- # for old modules (<0x170206)
808
- if var in [
809
- lcn_defs.Var.TVAR,
810
- lcn_defs.Var.R1VAR,
811
- lcn_defs.Var.R2VAR,
812
- lcn_defs.Var.R1VARSETPOINT,
813
- lcn_defs.Var.R2VARSETPOINT,
814
- ]:
815
- result &= await super().var_abs(var, value, unit, 0x000000)
816
- return result
817
-
818
- async def var_reset(
819
- self, var: lcn_defs.Var, software_serial: int | None = None
820
- ) -> bool:
821
- """Send a command to reset the variable value.
822
-
823
- :param Var var: Variable
824
- """
825
- result = True
826
- result &= await super().var_reset(var, 0x170206)
827
- if var in [
828
- lcn_defs.Var.TVAR,
829
- lcn_defs.Var.R1VAR,
830
- lcn_defs.Var.R2VAR,
831
- lcn_defs.Var.R1VARSETPOINT,
832
- lcn_defs.Var.R2VARSETPOINT,
833
- ]:
834
- result &= await super().var_reset(var, 0)
835
- return result
836
-
837
- async def var_rel(
838
- self,
839
- var: lcn_defs.Var,
840
- value: float | lcn_defs.VarValue,
841
- unit: lcn_defs.VarUnit = lcn_defs.VarUnit.NATIVE,
842
- value_ref: lcn_defs.RelVarRef = lcn_defs.RelVarRef.CURRENT,
843
- software_serial: int = -1,
844
- ) -> bool:
845
- """Send a command to change the value of a variable.
846
-
847
- :param Var var: Variable
848
- :param float value: Relative value to add (may also be
849
- negative)
850
- :param VarUnit unit: Unit of variable
851
- """
852
- result = True
853
- result &= await super().var_rel(var, value, software_serial=0x170206)
854
- if var in [
855
- lcn_defs.Var.TVAR,
856
- lcn_defs.Var.R1VAR,
857
- lcn_defs.Var.R2VAR,
858
- lcn_defs.Var.R1VARSETPOINT,
859
- lcn_defs.Var.R2VARSETPOINT,
860
- lcn_defs.Var.THRS1,
861
- lcn_defs.Var.THRS2,
862
- lcn_defs.Var.THRS3,
863
- lcn_defs.Var.THRS4,
864
- lcn_defs.Var.THRS5,
865
- ]:
866
- result &= await super().var_rel(var, value, software_serial=0)
867
- return result
868
-
869
-
870
- class ModuleConnection(AbstractConnection):
871
- """Organizes communication with a specific module or group."""
872
-
873
- def __init__(
874
- self,
875
- conn: PchkConnectionManager,
876
- addr: LcnAddr,
877
- has_s0_enabled: bool = False,
878
- wants_ack: bool = True,
879
- ):
880
- """Construct ModuleConnection instance."""
881
- assert not addr.is_group
882
- super().__init__(conn, addr, wants_ack=wants_ack)
883
- self.has_s0_enabled = has_s0_enabled
884
-
885
- self.input_callbacks: set[Callable[[inputs.Input], None]] = set()
886
-
887
- # List of queued acknowledge codes from the LCN modules.
888
- self.acknowledges: asyncio.Queue[int] = asyncio.Queue()
889
-
890
- # StatusRequester
891
- self.status_requester = StatusRequester(self)
892
-
893
- self.task_registry.create_task(self.request_module_properties())
894
-
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()
899
-
900
- async def send_command(self, wants_ack: bool, pck: str | bytes) -> bool:
901
- """Send a command to the module represented by this class.
902
-
903
- :param bool wants_ack: Also send a request for acknowledge.
904
- :param str pck: PCK command (without header).
905
- """
906
- if wants_ack:
907
- return await self.send_command_with_ack(pck)
908
-
909
- return await super().send_command(False, pck)
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
-
915
- # ##
916
- # ## Retry logic if an acknowledge is requested
917
- # ##
918
-
919
- async def send_command_with_ack(self, pck: str | bytes) -> bool:
920
- """Send a PCK command and ensure receiving of an acknowledgement.
921
-
922
- Resends the PCK command if no acknowledgement has been received
923
- within timeout.
924
-
925
- :param str pck: PCK command (without header).
926
- :returns: True if acknowledge was received, False otherwise
927
- :rtype: bool
928
- """
929
- count = 0
930
- while count < self.conn.settings["NUM_TRIES"]:
931
- await super().send_command(True, pck)
932
- try:
933
- code = await asyncio.wait_for(
934
- self.acknowledges.get(),
935
- timeout=self.conn.settings["DEFAULT_TIMEOUT"],
936
- )
937
- except asyncio.TimeoutError:
938
- count += 1
939
- continue
940
- if code == -1:
941
- return True
942
- break
943
- return False
944
-
945
- async def on_ack(self, code: int = -1) -> None:
946
- """Is called whenever an acknowledge is received from the LCN module.
947
-
948
- :param int code: The LCN internal code. -1 means
949
- "positive" acknowledge
950
- """
951
- await self.acknowledges.put(code)
952
-
953
- def set_s0_enabled(self, s0_enabled: bool) -> None:
954
- """Set the activation status for S0 variables.
955
-
956
- :param bool s0_enabled: If True, a BU4L has to be connected
957
- to the hardware module and S0 mode
958
- has to be activated in LCN-PRO.
959
- """
960
- self.has_s0_enabled = s0_enabled
961
-
962
- def get_s0_enabled(self) -> bool:
963
- """Get the activation status for S0 variables."""
964
- return self.has_s0_enabled
965
-
966
750
  # ##
967
751
  # ## Methods for handling input objects
968
752
  # ##
@@ -1019,12 +803,25 @@ class ModuleConnection(AbstractConnection):
1019
803
  },
1020
804
  }
1021
805
 
806
+ # ##
807
+ # ## Methods for requesting module properties and status
808
+ # ##
809
+
810
+ async def _request_device_properties(self) -> None:
811
+ """Request module properties (serials)."""
812
+ self.serials = await self.request_serials()
813
+ self._serials_known.set()
814
+
1022
815
  # Request status methods
1023
816
 
1024
817
  async def request_status_output(
1025
818
  self, output_port: lcn_defs.OutputPort, max_age: int = 0
1026
819
  ) -> inputs.ModStatusOutput | None:
1027
820
  """Request the status of an output port from a module."""
821
+ if self.addr.is_group:
822
+ _LOGGER.info("Status requests are not supported for groups.")
823
+ return None
824
+
1028
825
  result = await self.status_requester.request(
1029
826
  response_type=inputs.ModStatusOutput,
1030
827
  request_pck=PckGenerator.request_output_status(output_id=output_port.value),
@@ -1038,6 +835,10 @@ class ModuleConnection(AbstractConnection):
1038
835
  self, max_age: int = 0
1039
836
  ) -> inputs.ModStatusRelays | None:
1040
837
  """Request the status of relays from a module."""
838
+ if self.addr.is_group:
839
+ _LOGGER.info("Status requests are not supported for groups.")
840
+ return None
841
+
1041
842
  result = await self.status_requester.request(
1042
843
  response_type=inputs.ModStatusRelays,
1043
844
  request_pck=PckGenerator.request_relays_status(),
@@ -1053,6 +854,10 @@ class ModuleConnection(AbstractConnection):
1053
854
  max_age: int = 0,
1054
855
  ) -> inputs.ModStatusMotorPositionBS4 | None:
1055
856
  """Request the status of motor positions from a module."""
857
+ if self.addr.is_group:
858
+ _LOGGER.info("Status requests are not supported for groups.")
859
+ return None
860
+
1056
861
  if motor not in (
1057
862
  lcn_defs.MotorPort.MOTOR1,
1058
863
  lcn_defs.MotorPort.MOTOR2,
@@ -1080,6 +885,10 @@ class ModuleConnection(AbstractConnection):
1080
885
  self, max_age: int = 0
1081
886
  ) -> inputs.ModStatusBinSensors | None:
1082
887
  """Request the status of binary sensors from a module."""
888
+ if self.addr.is_group:
889
+ _LOGGER.info("Status requests are not supported for groups.")
890
+ return None
891
+
1083
892
  result = await self.status_requester.request(
1084
893
  response_type=inputs.ModStatusBinSensors,
1085
894
  request_pck=PckGenerator.request_bin_sensors_status(),
@@ -1094,6 +903,10 @@ class ModuleConnection(AbstractConnection):
1094
903
  max_age: int = 0,
1095
904
  ) -> inputs.ModStatusVar | None:
1096
905
  """Request the status of a variable from a module."""
906
+ if self.addr.is_group:
907
+ _LOGGER.info("Status requests are not supported for groups.")
908
+ return None
909
+
1097
910
  # do not use buffered response for old modules
1098
911
  # (variable response is typeless)
1099
912
  if self.serials.software_serial < 0x170206:
@@ -1119,6 +932,10 @@ class ModuleConnection(AbstractConnection):
1119
932
  self, max_age: int = 0
1120
933
  ) -> inputs.ModStatusLedsAndLogicOps | None:
1121
934
  """Request the status of LEDs and logic operations from a module."""
935
+ if self.addr.is_group:
936
+ _LOGGER.info("Status requests are not supported for groups.")
937
+ return None
938
+
1122
939
  result = await self.status_requester.request(
1123
940
  response_type=inputs.ModStatusLedsAndLogicOps,
1124
941
  request_pck=PckGenerator.request_leds_and_logic_ops(),
@@ -1131,6 +948,10 @@ class ModuleConnection(AbstractConnection):
1131
948
  self, max_age: int = 0
1132
949
  ) -> inputs.ModStatusKeyLocks | None:
1133
950
  """Request the status of locked keys from a module."""
951
+ if self.addr.is_group:
952
+ _LOGGER.info("Status requests are not supported for groups.")
953
+ return None
954
+
1134
955
  result = await self.status_requester.request(
1135
956
  response_type=inputs.ModStatusKeyLocks,
1136
957
  request_pck=PckGenerator.request_key_lock_status(),
@@ -1143,6 +964,10 @@ class ModuleConnection(AbstractConnection):
1143
964
 
1144
965
  async def request_serials(self, max_age: int = 0) -> Serials:
1145
966
  """Request module serials."""
967
+ if self.addr.is_group:
968
+ _LOGGER.info("Status requests are not supported for groups.")
969
+ return Serials(-1, -1, -1, lcn_defs.HardwareType.UNKNOWN)
970
+
1146
971
  result = cast(
1147
972
  inputs.ModSn | None,
1148
973
  await self.status_requester.request(
@@ -1163,6 +988,10 @@ class ModuleConnection(AbstractConnection):
1163
988
 
1164
989
  async def request_name(self, max_age: int = 0) -> str | None:
1165
990
  """Request module name."""
991
+ if self.addr.is_group:
992
+ _LOGGER.info("Status requests are not supported for groups.")
993
+ return None
994
+
1166
995
  coros = [
1167
996
  self.status_requester.request(
1168
997
  response_type=inputs.ModNameComment,
@@ -1183,6 +1012,10 @@ class ModuleConnection(AbstractConnection):
1183
1012
 
1184
1013
  async def request_comment(self, max_age: int = 0) -> str | None:
1185
1014
  """Request module name."""
1015
+ if self.addr.is_group:
1016
+ _LOGGER.info("Status requests are not supported for groups.")
1017
+ return None
1018
+
1186
1019
  coros = [
1187
1020
  self.status_requester.request(
1188
1021
  response_type=inputs.ModNameComment,
@@ -1203,6 +1036,10 @@ class ModuleConnection(AbstractConnection):
1203
1036
 
1204
1037
  async def request_oem_text(self, max_age: int = 0) -> str | None:
1205
1038
  """Request module name."""
1039
+ if self.addr.is_group:
1040
+ _LOGGER.info("Status requests are not supported for groups.")
1041
+ return None
1042
+
1206
1043
  coros = [
1207
1044
  self.status_requester.request(
1208
1045
  response_type=inputs.ModNameComment,
@@ -1225,6 +1062,10 @@ class ModuleConnection(AbstractConnection):
1225
1062
  self, dynamic: bool = False, max_age: int = 0
1226
1063
  ) -> set[LcnAddr]:
1227
1064
  """Request module static/dynamic group memberships."""
1065
+ if self.addr.is_group:
1066
+ _LOGGER.info("Status requests are not supported for groups.")
1067
+ return set()
1068
+
1228
1069
  result = await self.status_requester.request(
1229
1070
  response_type=inputs.ModStatusGroups,
1230
1071
  request_pck=(
pypck/inputs.py CHANGED
@@ -280,17 +280,15 @@ class Ping(Input):
280
280
  class ModAck(ModInput):
281
281
  """Acknowledge message received from module."""
282
282
 
283
- def __init__(self, physical_source_addr: LcnAddr, code: int):
283
+ def __init__(
284
+ self, physical_source_addr: LcnAddr, code: lcn_defs.AcknowledgeErrorCode
285
+ ):
284
286
  """Construct ModInput object."""
285
287
  super().__init__(physical_source_addr)
286
288
  self.code = code
287
289
 
288
- def get_code(self) -> int:
289
- """Return the acknowledge code.
290
-
291
- :return: Acknowledge code.
292
- :rtype: int
293
- """
290
+ def get_code(self) -> lcn_defs.AcknowledgeErrorCode:
291
+ """Return the acknowledge code."""
294
292
  return self.code
295
293
 
296
294
  @staticmethod
@@ -310,14 +308,18 @@ class ModAck(ModInput):
310
308
  addr = LcnAddr(
311
309
  int(matcher_pos.group("seg_id")), int(matcher_pos.group("mod_id"))
312
310
  )
313
- return [ModAck(addr, -1)]
311
+ return [ModAck(addr, lcn_defs.AcknowledgeErrorCode.OK)]
314
312
 
315
313
  matcher_neg = PckParser.PATTERN_ACK_NEG.match(data)
316
314
  if matcher_neg:
317
315
  addr = LcnAddr(
318
316
  int(matcher_neg.group("seg_id")), int(matcher_neg.group("mod_id"))
319
317
  )
320
- return [ModAck(addr, int(matcher_neg.group("code")))]
318
+ return [
319
+ ModAck(
320
+ addr, lcn_defs.AcknowledgeErrorCode(int(matcher_neg.group("code")))
321
+ )
322
+ ]
321
323
 
322
324
  return None
323
325
 
pypck/lcn_defs.py CHANGED
@@ -1444,6 +1444,27 @@ class AccessControlPeriphery(Enum):
1444
1444
  CODELOCK = "codelock"
1445
1445
 
1446
1446
 
1447
+ class AcknowledgeErrorCode(Enum):
1448
+ """Acknowledge error codes."""
1449
+
1450
+ UNKNOWN = -1
1451
+ OK = 0
1452
+ UNKNOWN_COMMAND = 5
1453
+ WRONG_PARAMETER_COUNT = 6
1454
+ INVALID_PARAMETER_VALUE = 7
1455
+ CURRENTLY_NOT_ALLOWED = 8
1456
+ NOT_ALLOWED_BY_PROGRAMMING = 9
1457
+ INAPPROPRIATE_MODULE = 10
1458
+ MISSING_PERIPHERY = 11
1459
+ PROGRAMMING_MODE_REQUIRED = 12
1460
+ FUSE_DEFECT = 14
1461
+
1462
+ @classmethod
1463
+ def _missing_(cls, value: Any) -> AcknowledgeErrorCode:
1464
+ """Handle missing values."""
1465
+ return cls.UNKNOWN
1466
+
1467
+
1447
1468
  class LcnEvent(Enum):
1448
1469
  """LCN events."""
1449
1470
 
pypck/pck_commands.py CHANGED
@@ -1048,7 +1048,9 @@ class PckGenerator:
1048
1048
 
1049
1049
  @staticmethod
1050
1050
  def lock_keys_tab_a_temporary(
1051
- time: int, time_unit: lcn_defs.TimeUnit, keys: list[bool]
1051
+ time: int,
1052
+ time_unit: lcn_defs.TimeUnit,
1053
+ keys: list[lcn_defs.KeyLockStateModifier],
1052
1054
  ) -> str:
1053
1055
  """Generate a command to lock keys for table A temporary.
1054
1056
 
@@ -1085,7 +1087,7 @@ class PckGenerator:
1085
1087
  raise ValueError("Wrong time_unit.")
1086
1088
 
1087
1089
  for key in keys:
1088
- ret += "1" if key else "0"
1090
+ ret += "1" if key == lcn_defs.KeyLockStateModifier.ON else "0"
1089
1091
 
1090
1092
  return ret
1091
1093
 
@@ -0,0 +1,160 @@
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
+ def __init__(
36
+ self,
37
+ device_connection: DeviceConnection,
38
+ ) -> None:
39
+ """Initialize the context."""
40
+ self.device_connection = device_connection
41
+ self.last_requests: set[StatusRequest] = set()
42
+ self.unregister_inputs = self.device_connection.register_for_inputs(
43
+ self.input_callback
44
+ )
45
+ self.max_response_age = self.device_connection.conn.settings["MAX_RESPONSE_AGE"]
46
+ # asyncio.get_running_loop().create_task(self.prune_loop())
47
+
48
+ async def prune_loop(self) -> None:
49
+ """Periodically prune old status requests."""
50
+ while True:
51
+ await asyncio.sleep(self.max_response_age)
52
+ self.prune_status_requests()
53
+
54
+ def prune_status_requests(self) -> None:
55
+ """Prune old status requests."""
56
+ entries_to_remove = {
57
+ request
58
+ for request in self.last_requests
59
+ if asyncio.get_running_loop().time() - request.timestamp
60
+ > self.max_response_age
61
+ }
62
+ for entry in entries_to_remove:
63
+ entry.response.cancel()
64
+ self.last_requests.difference_update(entries_to_remove)
65
+
66
+ def get_status_requests(
67
+ self,
68
+ request_type: type[inputs.Input],
69
+ parameters: frozenset[tuple[str, Any]] | None = None,
70
+ max_age: int = 0,
71
+ ) -> list[StatusRequest]:
72
+ """Get the status requests for the given type and parameters."""
73
+ if parameters is None:
74
+ parameters = frozenset()
75
+ loop = asyncio.get_running_loop()
76
+ results = [
77
+ request
78
+ for request in self.last_requests
79
+ if request.type == request_type
80
+ and parameters.issubset(request.parameters)
81
+ and (
82
+ (request.timestamp == -1)
83
+ or (max_age == -1)
84
+ or (loop.time() - request.timestamp < max_age)
85
+ )
86
+ ]
87
+ results.sort(key=lambda request: request.timestamp, reverse=True)
88
+ return results
89
+
90
+ def input_callback(self, inp: inputs.Input) -> None:
91
+ """Handle incoming inputs and set the result for the corresponding requests."""
92
+ requests = [
93
+ request
94
+ for request in self.get_status_requests(type(inp))
95
+ if all(
96
+ getattr(inp, parameter_name) == parameter_value
97
+ for parameter_name, parameter_value in request.parameters
98
+ )
99
+ ]
100
+ for request in requests:
101
+ if request.response.done() or request.response.cancelled():
102
+ continue
103
+ request.timestamp = asyncio.get_running_loop().time()
104
+ request.response.set_result(inp)
105
+
106
+ async def request(
107
+ self,
108
+ response_type: type[inputs.Input],
109
+ request_pck: str,
110
+ request_acknowledge: bool = False,
111
+ max_age: int = 0, # -1: no age limit / infinite age
112
+ **request_kwargs: Any,
113
+ ) -> inputs.Input | None:
114
+ """Execute a status request and wait for the response."""
115
+ parameters = frozenset(request_kwargs.items())
116
+
117
+ # check if we already have a received response for the current request
118
+ if requests := self.get_status_requests(response_type, parameters, max_age):
119
+ try:
120
+ async with asyncio.timeout(
121
+ self.device_connection.conn.settings["DEFAULT_TIMEOUT"]
122
+ ):
123
+ return await requests[0].response
124
+ except asyncio.TimeoutError:
125
+ return None
126
+ except asyncio.CancelledError:
127
+ return None
128
+
129
+ # no stored request or forced request: set up a new request
130
+ request = StatusRequest(
131
+ response_type,
132
+ frozenset(request_kwargs.items()),
133
+ -1,
134
+ asyncio.get_running_loop().create_future(),
135
+ )
136
+
137
+ self.last_requests.discard(request)
138
+ self.last_requests.add(request)
139
+ result = None
140
+ # send the request up to NUM_TRIES and wait for response future completion
141
+ for _ in range(self.device_connection.conn.settings["NUM_TRIES"]):
142
+ await self.device_connection.send_command(request_acknowledge, request_pck)
143
+
144
+ try:
145
+ async with asyncio.timeout(
146
+ self.device_connection.conn.settings["DEFAULT_TIMEOUT"]
147
+ ):
148
+ # Need to shield the future. Otherwise it would get cancelled.
149
+ result = await asyncio.shield(request.response)
150
+ break
151
+ except asyncio.TimeoutError:
152
+ continue
153
+ except asyncio.CancelledError:
154
+ break
155
+
156
+ # if we got no results, remove the request from the set
157
+ if result is None:
158
+ request.response.cancel()
159
+ self.last_requests.discard(request)
160
+ return result
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pypck
3
- Version: 0.9.1
3
+ Version: 0.9.3
4
4
  Summary: LCN-PCK library
5
5
  Home-page: https://github.com/alengwenus/pypck
6
6
  Author-email: Andre Lengwenus <alengwenus@gmail.com>
@@ -55,7 +55,7 @@ async def main():
55
55
  password="lcn",
56
56
  settings={"SK_NUM_TRIES": 0},
57
57
  ) as pck_client:
58
- module = pck_client.get_address_conn(LcnAddr(0, 10, False))
58
+ module = pck_client.get_device_connection(LcnAddr(0, 10, False))
59
59
 
60
60
  await module.dim_output(0, 100, 0)
61
61
  await asyncio.sleep(1)
@@ -0,0 +1,14 @@
1
+ pypck/__init__.py,sha256=jVx-aBsV_LmBf6jiivMrMcBUofC_AOseywDafgOzAS4,323
2
+ pypck/connection.py,sha256=n3itRe8oQtw64vyWGYhl6j4QJC6wgeeHitBSn-Cl2_4,23330
3
+ pypck/device.py,sha256=sR9FtHQ84uUjxiWBEt8qqGWoEMn9HWwuTlBLzEwUVtw,39313
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/status_requester.py,sha256=10N5pbIBe_Ao-9ui_D7mCavk21BYZs9c-kxcTtmi-FI,5721
10
+ pypck-0.9.3.dist-info/licenses/LICENSE,sha256=iYB6zyMJvShfAzQE7nhYFgLzzZuBmhasLw5fYP9KRz4,1023
11
+ pypck-0.9.3.dist-info/METADATA,sha256=CprJtfVw1P-_Nb9jLPoK8lDFOMYUVI02fxH7xeIgJak,2682
12
+ pypck-0.9.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
13
+ pypck-0.9.3.dist-info/top_level.txt,sha256=59ried49iFueDa5mQ_5BGVZcESjjzi4MZZKLcganvQA,6
14
+ pypck-0.9.3.dist-info/RECORD,,
pypck/py.typed DELETED
File without changes
@@ -1,14 +0,0 @@
1
- pypck/__init__.py,sha256=_40HMoShQbAlJT0ZdOjQagqn2NRTrvUGOmoS3cYBuIA,277
2
- pypck/connection.py,sha256=CrcvSnTn3Pbd9rwP_CBDagocJVG2DEE9KKN95ljdOVY,23839
3
- pypck/helpers.py,sha256=_5doqIsSRpqdQNPIUsjFh813xKGuMuEFY6sNGobJGIk,1280
4
- pypck/inputs.py,sha256=cjhvYKPr5QHw7Rjfkkg8LPbP8ohq_-z_Tvk51Rkx0aY,45828
5
- pypck/lcn_addr.py,sha256=N2Od8KuANOglqKjf596hJVH1SRcG7MhESKA5YYlDnbw,1946
6
- pypck/lcn_defs.py,sha256=hB4fxcjz7zkO3qkgBpWmdg8z17ZLAkZmRCyaucp70eM,42850
7
- pypck/module.py,sha256=gNkLn4UK99Qs0x0apOgytCgPIUZoq6mBLjco0AgXad4,45091
8
- pypck/pck_commands.py,sha256=bkb3q49s4PVY6UNR0B6S31oU7aSaEbpPl3rj0eGxTQU,50380
9
- pypck/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
- pypck-0.9.1.dist-info/licenses/LICENSE,sha256=iYB6zyMJvShfAzQE7nhYFgLzzZuBmhasLw5fYP9KRz4,1023
11
- pypck-0.9.1.dist-info/METADATA,sha256=ZSoV0COOX-ilQrm-S0Xg8gPuiGzqEWm9RPdksfl8E2s,2677
12
- pypck-0.9.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
13
- pypck-0.9.1.dist-info/top_level.txt,sha256=59ried49iFueDa5mQ_5BGVZcESjjzi4MZZKLcganvQA,6
14
- pypck-0.9.1.dist-info/RECORD,,
File without changes