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/__init__.py +0 -2
- pypck/connection.py +12 -31
- pypck/lcn_defs.py +22 -0
- pypck/module.py +416 -230
- pypck-0.9.2.dist-info/METADATA +65 -0
- pypck-0.9.2.dist-info/RECORD +13 -0
- pypck/request_handlers.py +0 -694
- pypck/timeout_retry.py +0 -110
- pypck-0.8.12.dist-info/METADATA +0 -145
- pypck-0.8.12.dist-info/RECORD +0 -15
- {pypck-0.8.12.dist-info → pypck-0.9.2.dist-info}/WHEEL +0 -0
- {pypck-0.8.12.dist-info → pypck-0.9.2.dist-info}/licenses/LICENSE +0 -0
- {pypck-0.8.12.dist-info → pypck-0.9.2.dist-info}/top_level.txt +0 -0
pypck/module.py
CHANGED
|
@@ -3,26 +3,176 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
422
|
-
await self.
|
|
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,
|
|
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,
|
|
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
|
|
461
|
-
await self.
|
|
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
|
|
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
|
|
490
|
-
await self.
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
#
|
|
805
|
-
|
|
806
|
-
timeout: int = self.conn.settings["DEFAULT_TIMEOUT"]
|
|
893
|
+
# StatusRequester
|
|
894
|
+
self.status_requester = StatusRequester(self)
|
|
807
895
|
|
|
808
|
-
|
|
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
|
-
|
|
817
|
-
|
|
818
|
-
self.
|
|
819
|
-
self.
|
|
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.
|
|
972
|
-
"comment": self.
|
|
973
|
-
"oem_text": self.
|
|
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(
|
|
976
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1038
|
+
return cast(inputs.ModStatusOutput, result)
|
|
985
1039
|
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
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
|
-
|
|
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
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
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
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
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
|
|
1147
|
+
async def request_serials(self, max_age: int = 0) -> Serials:
|
|
1029
1148
|
"""Request module serials."""
|
|
1030
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
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
|
|
1041
|
-
"""Request
|
|
1042
|
-
|
|
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
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
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
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
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
|
-
|
|
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)
|