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 +5 -3
- pypck/connection.py +19 -30
- pypck/{module.py → device.py} +184 -343
- pypck/inputs.py +11 -9
- pypck/lcn_defs.py +21 -0
- pypck/pck_commands.py +4 -2
- pypck/status_requester.py +160 -0
- {pypck-0.9.1.dist-info → pypck-0.9.3.dist-info}/METADATA +2 -2
- pypck-0.9.3.dist-info/RECORD +14 -0
- pypck/py.typed +0 -0
- pypck-0.9.1.dist-info/RECORD +0 -14
- {pypck-0.9.1.dist-info → pypck-0.9.3.dist-info}/WHEEL +0 -0
- {pypck-0.9.1.dist-info → pypck-0.9.3.dist-info}/licenses/LICENSE +0 -0
- {pypck-0.9.1.dist-info → pypck-0.9.3.dist-info}/top_level.txt +0 -0
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
|
-
"
|
|
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.
|
|
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
|
|
363
|
+
# replace all device_connections with current local_seg_id with new
|
|
364
364
|
# local_seg_id
|
|
365
|
-
for addr in list(self.
|
|
365
|
+
for addr in list(self.device_connections):
|
|
366
366
|
if addr.seg_id == old_local_seg_id:
|
|
367
|
-
address_conn = self.
|
|
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.
|
|
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
|
|
382
|
-
"""Create and/or return the given
|
|
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
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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():
|
pypck/{module.py → device.py}
RENAMED
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
"""
|
|
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
|
|
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
|
-
|
|
33
|
-
|
|
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,
|
|
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__(
|
|
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) ->
|
|
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,
|
|
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 [
|
|
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,
|
|
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.
|
|
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.
|
|
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
|
pypck-0.9.1.dist-info/RECORD
DELETED
|
@@ -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
|
|
File without changes
|
|
File without changes
|