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/__init__.py +0 -2
- pypck/connection.py +12 -31
- pypck/lcn_defs.py +22 -0
- pypck/module.py +416 -230
- pypck/py.typed +0 -0
- pypck-0.9.1.dist-info/METADATA +65 -0
- pypck-0.9.1.dist-info/RECORD +14 -0
- pypck/request_handlers.py +0 -694
- pypck/timeout_retry.py +0 -110
- pypck-0.8.10.dist-info/METADATA +0 -145
- pypck-0.8.10.dist-info/RECORD +0 -15
- {pypck-0.8.10.dist-info → pypck-0.9.1.dist-info}/WHEEL +0 -0
- {pypck-0.8.10.dist-info → pypck-0.9.1.dist-info}/licenses/LICENSE +0 -0
- {pypck-0.8.10.dist-info → pypck-0.9.1.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
|
|
|
@@ -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,
|
|
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
|
|
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
|
|
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
|
-
#
|
|
802
|
-
|
|
803
|
-
timeout: int = self.conn.settings["DEFAULT_TIMEOUT"]
|
|
890
|
+
# StatusRequester
|
|
891
|
+
self.status_requester = StatusRequester(self)
|
|
804
892
|
|
|
805
|
-
|
|
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
|
-
|
|
814
|
-
|
|
815
|
-
self.
|
|
816
|
-
self.
|
|
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.
|
|
969
|
-
"comment": self.
|
|
970
|
-
"oem_text": self.
|
|
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(
|
|
973
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1035
|
+
return cast(inputs.ModStatusOutput, result)
|
|
982
1036
|
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
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
|
-
|
|
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
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
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
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
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
|
|
1144
|
+
async def request_serials(self, max_age: int = 0) -> Serials:
|
|
1026
1145
|
"""Request module serials."""
|
|
1027
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
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
|
|
1038
|
-
"""Request
|
|
1039
|
-
|
|
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
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
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
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
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
|
-
|
|
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)
|