pypck 0.9.2__py3-none-any.whl → 0.9.4__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} +180 -342
- pypck/inputs.py +11 -9
- pypck/lcn_defs.py +21 -0
- pypck/py.typed +0 -0
- pypck/status_requester.py +160 -0
- {pypck-0.9.2.dist-info → pypck-0.9.4.dist-info}/METADATA +2 -2
- pypck-0.9.4.dist-info/RECORD +15 -0
- pypck-0.9.2.dist-info/RECORD +0 -13
- {pypck-0.9.2.dist-info → pypck-0.9.4.dist-info}/WHEEL +0 -0
- {pypck-0.9.2.dist-info → pypck-0.9.4.dist-info}/licenses/LICENSE +0 -0
- {pypck-0.9.2.dist-info → pypck-0.9.4.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
|
|
|
@@ -773,199 +747,6 @@ class AbstractConnection:
|
|
|
773
747
|
"""
|
|
774
748
|
return await self.send_command(self.wants_ack, pck)
|
|
775
749
|
|
|
776
|
-
|
|
777
|
-
class GroupConnection(AbstractConnection):
|
|
778
|
-
"""Organizes communication with a specific group.
|
|
779
|
-
|
|
780
|
-
It is assumed that all modules within this group are newer than FW170206
|
|
781
|
-
"""
|
|
782
|
-
|
|
783
|
-
def __init__(
|
|
784
|
-
self,
|
|
785
|
-
conn: PchkConnectionManager,
|
|
786
|
-
addr: LcnAddr,
|
|
787
|
-
):
|
|
788
|
-
"""Construct GroupConnection instance."""
|
|
789
|
-
assert addr.is_group
|
|
790
|
-
super().__init__(conn, addr, wants_ack=False)
|
|
791
|
-
self._serials_known.set()
|
|
792
|
-
|
|
793
|
-
async def var_abs(
|
|
794
|
-
self,
|
|
795
|
-
var: lcn_defs.Var,
|
|
796
|
-
value: float | lcn_defs.VarValue,
|
|
797
|
-
unit: lcn_defs.VarUnit = lcn_defs.VarUnit.NATIVE,
|
|
798
|
-
software_serial: int = -1,
|
|
799
|
-
) -> bool:
|
|
800
|
-
"""Send a command to set the absolute value to a variable.
|
|
801
|
-
|
|
802
|
-
:param Var var: Variable
|
|
803
|
-
:param float value: Absolute value to set
|
|
804
|
-
:param VarUnit unit: Unit of variable
|
|
805
|
-
"""
|
|
806
|
-
result = True
|
|
807
|
-
# for new modules (>=0x170206)
|
|
808
|
-
result &= await super().var_abs(var, value, unit, 0x170206)
|
|
809
|
-
|
|
810
|
-
# for old modules (<0x170206)
|
|
811
|
-
if var in [
|
|
812
|
-
lcn_defs.Var.TVAR,
|
|
813
|
-
lcn_defs.Var.R1VAR,
|
|
814
|
-
lcn_defs.Var.R2VAR,
|
|
815
|
-
lcn_defs.Var.R1VARSETPOINT,
|
|
816
|
-
lcn_defs.Var.R2VARSETPOINT,
|
|
817
|
-
]:
|
|
818
|
-
result &= await super().var_abs(var, value, unit, 0x000000)
|
|
819
|
-
return result
|
|
820
|
-
|
|
821
|
-
async def var_reset(
|
|
822
|
-
self, var: lcn_defs.Var, software_serial: int | None = None
|
|
823
|
-
) -> bool:
|
|
824
|
-
"""Send a command to reset the variable value.
|
|
825
|
-
|
|
826
|
-
:param Var var: Variable
|
|
827
|
-
"""
|
|
828
|
-
result = True
|
|
829
|
-
result &= await super().var_reset(var, 0x170206)
|
|
830
|
-
if var in [
|
|
831
|
-
lcn_defs.Var.TVAR,
|
|
832
|
-
lcn_defs.Var.R1VAR,
|
|
833
|
-
lcn_defs.Var.R2VAR,
|
|
834
|
-
lcn_defs.Var.R1VARSETPOINT,
|
|
835
|
-
lcn_defs.Var.R2VARSETPOINT,
|
|
836
|
-
]:
|
|
837
|
-
result &= await super().var_reset(var, 0)
|
|
838
|
-
return result
|
|
839
|
-
|
|
840
|
-
async def var_rel(
|
|
841
|
-
self,
|
|
842
|
-
var: lcn_defs.Var,
|
|
843
|
-
value: float | lcn_defs.VarValue,
|
|
844
|
-
unit: lcn_defs.VarUnit = lcn_defs.VarUnit.NATIVE,
|
|
845
|
-
value_ref: lcn_defs.RelVarRef = lcn_defs.RelVarRef.CURRENT,
|
|
846
|
-
software_serial: int = -1,
|
|
847
|
-
) -> bool:
|
|
848
|
-
"""Send a command to change the value of a variable.
|
|
849
|
-
|
|
850
|
-
:param Var var: Variable
|
|
851
|
-
:param float value: Relative value to add (may also be
|
|
852
|
-
negative)
|
|
853
|
-
:param VarUnit unit: Unit of variable
|
|
854
|
-
"""
|
|
855
|
-
result = True
|
|
856
|
-
result &= await super().var_rel(var, value, software_serial=0x170206)
|
|
857
|
-
if var in [
|
|
858
|
-
lcn_defs.Var.TVAR,
|
|
859
|
-
lcn_defs.Var.R1VAR,
|
|
860
|
-
lcn_defs.Var.R2VAR,
|
|
861
|
-
lcn_defs.Var.R1VARSETPOINT,
|
|
862
|
-
lcn_defs.Var.R2VARSETPOINT,
|
|
863
|
-
lcn_defs.Var.THRS1,
|
|
864
|
-
lcn_defs.Var.THRS2,
|
|
865
|
-
lcn_defs.Var.THRS3,
|
|
866
|
-
lcn_defs.Var.THRS4,
|
|
867
|
-
lcn_defs.Var.THRS5,
|
|
868
|
-
]:
|
|
869
|
-
result &= await super().var_rel(var, value, software_serial=0)
|
|
870
|
-
return result
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
class ModuleConnection(AbstractConnection):
|
|
874
|
-
"""Organizes communication with a specific module or group."""
|
|
875
|
-
|
|
876
|
-
def __init__(
|
|
877
|
-
self,
|
|
878
|
-
conn: PchkConnectionManager,
|
|
879
|
-
addr: LcnAddr,
|
|
880
|
-
has_s0_enabled: bool = False,
|
|
881
|
-
wants_ack: bool = True,
|
|
882
|
-
):
|
|
883
|
-
"""Construct ModuleConnection instance."""
|
|
884
|
-
assert not addr.is_group
|
|
885
|
-
super().__init__(conn, addr, wants_ack=wants_ack)
|
|
886
|
-
self.has_s0_enabled = has_s0_enabled
|
|
887
|
-
|
|
888
|
-
self.input_callbacks: set[Callable[[inputs.Input], None]] = set()
|
|
889
|
-
|
|
890
|
-
# List of queued acknowledge codes from the LCN modules.
|
|
891
|
-
self.acknowledges: asyncio.Queue[int] = asyncio.Queue()
|
|
892
|
-
|
|
893
|
-
# StatusRequester
|
|
894
|
-
self.status_requester = StatusRequester(self)
|
|
895
|
-
|
|
896
|
-
self.task_registry.create_task(self.request_module_properties())
|
|
897
|
-
|
|
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()
|
|
902
|
-
|
|
903
|
-
async def send_command(self, wants_ack: bool, pck: str | bytes) -> bool:
|
|
904
|
-
"""Send a command to the module represented by this class.
|
|
905
|
-
|
|
906
|
-
:param bool wants_ack: Also send a request for acknowledge.
|
|
907
|
-
:param str pck: PCK command (without header).
|
|
908
|
-
"""
|
|
909
|
-
if wants_ack:
|
|
910
|
-
return await self.send_command_with_ack(pck)
|
|
911
|
-
|
|
912
|
-
return await super().send_command(False, pck)
|
|
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
|
-
|
|
918
|
-
# ##
|
|
919
|
-
# ## Retry logic if an acknowledge is requested
|
|
920
|
-
# ##
|
|
921
|
-
|
|
922
|
-
async def send_command_with_ack(self, pck: str | bytes) -> bool:
|
|
923
|
-
"""Send a PCK command and ensure receiving of an acknowledgement.
|
|
924
|
-
|
|
925
|
-
Resends the PCK command if no acknowledgement has been received
|
|
926
|
-
within timeout.
|
|
927
|
-
|
|
928
|
-
:param str pck: PCK command (without header).
|
|
929
|
-
:returns: True if acknowledge was received, False otherwise
|
|
930
|
-
:rtype: bool
|
|
931
|
-
"""
|
|
932
|
-
count = 0
|
|
933
|
-
while count < self.conn.settings["NUM_TRIES"]:
|
|
934
|
-
await super().send_command(True, pck)
|
|
935
|
-
try:
|
|
936
|
-
code = await asyncio.wait_for(
|
|
937
|
-
self.acknowledges.get(),
|
|
938
|
-
timeout=self.conn.settings["DEFAULT_TIMEOUT"],
|
|
939
|
-
)
|
|
940
|
-
except asyncio.TimeoutError:
|
|
941
|
-
count += 1
|
|
942
|
-
continue
|
|
943
|
-
if code == -1:
|
|
944
|
-
return True
|
|
945
|
-
break
|
|
946
|
-
return False
|
|
947
|
-
|
|
948
|
-
async def on_ack(self, code: int = -1) -> None:
|
|
949
|
-
"""Is called whenever an acknowledge is received from the LCN module.
|
|
950
|
-
|
|
951
|
-
:param int code: The LCN internal code. -1 means
|
|
952
|
-
"positive" acknowledge
|
|
953
|
-
"""
|
|
954
|
-
await self.acknowledges.put(code)
|
|
955
|
-
|
|
956
|
-
def set_s0_enabled(self, s0_enabled: bool) -> None:
|
|
957
|
-
"""Set the activation status for S0 variables.
|
|
958
|
-
|
|
959
|
-
:param bool s0_enabled: If True, a BU4L has to be connected
|
|
960
|
-
to the hardware module and S0 mode
|
|
961
|
-
has to be activated in LCN-PRO.
|
|
962
|
-
"""
|
|
963
|
-
self.has_s0_enabled = s0_enabled
|
|
964
|
-
|
|
965
|
-
def get_s0_enabled(self) -> bool:
|
|
966
|
-
"""Get the activation status for S0 variables."""
|
|
967
|
-
return self.has_s0_enabled
|
|
968
|
-
|
|
969
750
|
# ##
|
|
970
751
|
# ## Methods for handling input objects
|
|
971
752
|
# ##
|
|
@@ -1022,12 +803,25 @@ class ModuleConnection(AbstractConnection):
|
|
|
1022
803
|
},
|
|
1023
804
|
}
|
|
1024
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
|
+
|
|
1025
815
|
# Request status methods
|
|
1026
816
|
|
|
1027
817
|
async def request_status_output(
|
|
1028
818
|
self, output_port: lcn_defs.OutputPort, max_age: int = 0
|
|
1029
819
|
) -> inputs.ModStatusOutput | None:
|
|
1030
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
|
+
|
|
1031
825
|
result = await self.status_requester.request(
|
|
1032
826
|
response_type=inputs.ModStatusOutput,
|
|
1033
827
|
request_pck=PckGenerator.request_output_status(output_id=output_port.value),
|
|
@@ -1041,6 +835,10 @@ class ModuleConnection(AbstractConnection):
|
|
|
1041
835
|
self, max_age: int = 0
|
|
1042
836
|
) -> inputs.ModStatusRelays | None:
|
|
1043
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
|
+
|
|
1044
842
|
result = await self.status_requester.request(
|
|
1045
843
|
response_type=inputs.ModStatusRelays,
|
|
1046
844
|
request_pck=PckGenerator.request_relays_status(),
|
|
@@ -1056,6 +854,10 @@ class ModuleConnection(AbstractConnection):
|
|
|
1056
854
|
max_age: int = 0,
|
|
1057
855
|
) -> inputs.ModStatusMotorPositionBS4 | None:
|
|
1058
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
|
+
|
|
1059
861
|
if motor not in (
|
|
1060
862
|
lcn_defs.MotorPort.MOTOR1,
|
|
1061
863
|
lcn_defs.MotorPort.MOTOR2,
|
|
@@ -1083,6 +885,10 @@ class ModuleConnection(AbstractConnection):
|
|
|
1083
885
|
self, max_age: int = 0
|
|
1084
886
|
) -> inputs.ModStatusBinSensors | None:
|
|
1085
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
|
+
|
|
1086
892
|
result = await self.status_requester.request(
|
|
1087
893
|
response_type=inputs.ModStatusBinSensors,
|
|
1088
894
|
request_pck=PckGenerator.request_bin_sensors_status(),
|
|
@@ -1097,6 +903,10 @@ class ModuleConnection(AbstractConnection):
|
|
|
1097
903
|
max_age: int = 0,
|
|
1098
904
|
) -> inputs.ModStatusVar | None:
|
|
1099
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
|
+
|
|
1100
910
|
# do not use buffered response for old modules
|
|
1101
911
|
# (variable response is typeless)
|
|
1102
912
|
if self.serials.software_serial < 0x170206:
|
|
@@ -1122,6 +932,10 @@ class ModuleConnection(AbstractConnection):
|
|
|
1122
932
|
self, max_age: int = 0
|
|
1123
933
|
) -> inputs.ModStatusLedsAndLogicOps | None:
|
|
1124
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
|
+
|
|
1125
939
|
result = await self.status_requester.request(
|
|
1126
940
|
response_type=inputs.ModStatusLedsAndLogicOps,
|
|
1127
941
|
request_pck=PckGenerator.request_leds_and_logic_ops(),
|
|
@@ -1134,6 +948,10 @@ class ModuleConnection(AbstractConnection):
|
|
|
1134
948
|
self, max_age: int = 0
|
|
1135
949
|
) -> inputs.ModStatusKeyLocks | None:
|
|
1136
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
|
+
|
|
1137
955
|
result = await self.status_requester.request(
|
|
1138
956
|
response_type=inputs.ModStatusKeyLocks,
|
|
1139
957
|
request_pck=PckGenerator.request_key_lock_status(),
|
|
@@ -1146,6 +964,10 @@ class ModuleConnection(AbstractConnection):
|
|
|
1146
964
|
|
|
1147
965
|
async def request_serials(self, max_age: int = 0) -> Serials:
|
|
1148
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
|
+
|
|
1149
971
|
result = cast(
|
|
1150
972
|
inputs.ModSn | None,
|
|
1151
973
|
await self.status_requester.request(
|
|
@@ -1166,6 +988,10 @@ class ModuleConnection(AbstractConnection):
|
|
|
1166
988
|
|
|
1167
989
|
async def request_name(self, max_age: int = 0) -> str | None:
|
|
1168
990
|
"""Request module name."""
|
|
991
|
+
if self.addr.is_group:
|
|
992
|
+
_LOGGER.info("Status requests are not supported for groups.")
|
|
993
|
+
return None
|
|
994
|
+
|
|
1169
995
|
coros = [
|
|
1170
996
|
self.status_requester.request(
|
|
1171
997
|
response_type=inputs.ModNameComment,
|
|
@@ -1186,6 +1012,10 @@ class ModuleConnection(AbstractConnection):
|
|
|
1186
1012
|
|
|
1187
1013
|
async def request_comment(self, max_age: int = 0) -> str | None:
|
|
1188
1014
|
"""Request module name."""
|
|
1015
|
+
if self.addr.is_group:
|
|
1016
|
+
_LOGGER.info("Status requests are not supported for groups.")
|
|
1017
|
+
return None
|
|
1018
|
+
|
|
1189
1019
|
coros = [
|
|
1190
1020
|
self.status_requester.request(
|
|
1191
1021
|
response_type=inputs.ModNameComment,
|
|
@@ -1206,6 +1036,10 @@ class ModuleConnection(AbstractConnection):
|
|
|
1206
1036
|
|
|
1207
1037
|
async def request_oem_text(self, max_age: int = 0) -> str | None:
|
|
1208
1038
|
"""Request module name."""
|
|
1039
|
+
if self.addr.is_group:
|
|
1040
|
+
_LOGGER.info("Status requests are not supported for groups.")
|
|
1041
|
+
return None
|
|
1042
|
+
|
|
1209
1043
|
coros = [
|
|
1210
1044
|
self.status_requester.request(
|
|
1211
1045
|
response_type=inputs.ModNameComment,
|
|
@@ -1228,6 +1062,10 @@ class ModuleConnection(AbstractConnection):
|
|
|
1228
1062
|
self, dynamic: bool = False, max_age: int = 0
|
|
1229
1063
|
) -> set[LcnAddr]:
|
|
1230
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
|
+
|
|
1231
1069
|
result = await self.status_requester.request(
|
|
1232
1070
|
response_type=inputs.ModStatusGroups,
|
|
1233
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/py.typed
ADDED
|
File without changes
|
|
@@ -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.4
|
|
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,15 @@
|
|
|
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/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
pypck/status_requester.py,sha256=10N5pbIBe_Ao-9ui_D7mCavk21BYZs9c-kxcTtmi-FI,5721
|
|
11
|
+
pypck-0.9.4.dist-info/licenses/LICENSE,sha256=iYB6zyMJvShfAzQE7nhYFgLzzZuBmhasLw5fYP9KRz4,1023
|
|
12
|
+
pypck-0.9.4.dist-info/METADATA,sha256=9fz5eH50liD4D9pv54ye7XcWc1KDJvi1i46gF3cic_A,2682
|
|
13
|
+
pypck-0.9.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
14
|
+
pypck-0.9.4.dist-info/top_level.txt,sha256=59ried49iFueDa5mQ_5BGVZcESjjzi4MZZKLcganvQA,6
|
|
15
|
+
pypck-0.9.4.dist-info/RECORD,,
|
pypck-0.9.2.dist-info/RECORD
DELETED
|
@@ -1,13 +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=5ytzV81BlFpXg6-lhPkbShIlq_UDb737rt5HbgjfSr8,45141
|
|
8
|
-
pypck/pck_commands.py,sha256=eJxmh2e8EbKGpek97L2961Kr_nVfT8rKgJCN3YgjIQM,50458
|
|
9
|
-
pypck-0.9.2.dist-info/licenses/LICENSE,sha256=iYB6zyMJvShfAzQE7nhYFgLzzZuBmhasLw5fYP9KRz4,1023
|
|
10
|
-
pypck-0.9.2.dist-info/METADATA,sha256=r0YVKu3bk_tczrldgEEHu5vrXwJC7aOjAJMY9fA_ueg,2677
|
|
11
|
-
pypck-0.9.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
12
|
-
pypck-0.9.2.dist-info/top_level.txt,sha256=59ried49iFueDa5mQ_5BGVZcESjjzi4MZZKLcganvQA,6
|
|
13
|
-
pypck-0.9.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|