conson-xp 1.28.0__py3-none-any.whl → 1.29.0__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.
- {conson_xp-1.28.0.dist-info → conson_xp-1.29.0.dist-info}/METADATA +1 -1
- {conson_xp-1.28.0.dist-info → conson_xp-1.29.0.dist-info}/RECORD +11 -10
- xp/__init__.py +1 -1
- xp/models/term/module_state.py +2 -0
- xp/services/server/client_buffer_manager.py +69 -0
- xp/services/server/server_service.py +21 -11
- xp/services/term/state_monitor_service.py +105 -5
- xp/term/widgets/modules_list.py +16 -9
- {conson_xp-1.28.0.dist-info → conson_xp-1.29.0.dist-info}/WHEEL +0 -0
- {conson_xp-1.28.0.dist-info → conson_xp-1.29.0.dist-info}/entry_points.txt +0 -0
- {conson_xp-1.28.0.dist-info → conson_xp-1.29.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
conson_xp-1.
|
|
2
|
-
conson_xp-1.
|
|
3
|
-
conson_xp-1.
|
|
4
|
-
conson_xp-1.
|
|
5
|
-
xp/__init__.py,sha256=
|
|
1
|
+
conson_xp-1.29.0.dist-info/METADATA,sha256=k_I_mYoEaWaCsYx4kk3JOZ5FaBuAFkggSTKk28JMyGk,10312
|
|
2
|
+
conson_xp-1.29.0.dist-info/WHEEL,sha256=tsUv_t7BDeJeRHaSrczbGeuK-TtDpGsWi_JfpzD255I,90
|
|
3
|
+
conson_xp-1.29.0.dist-info/entry_points.txt,sha256=1OcdIcDM1hz3ljCXgybaPUh1IOFEwkaTgLIW9u9zGeg,50
|
|
4
|
+
conson_xp-1.29.0.dist-info/licenses/LICENSE,sha256=rxj6woMM-r3YCyGq_UHFtbh7kHTAJgrccH6O-33IDE4,1419
|
|
5
|
+
xp/__init__.py,sha256=CTv0SXJngPEN2pQxHNESqU87b-94c8Ke6ShS2yRsqDA,181
|
|
6
6
|
xp/cli/__init__.py,sha256=QjnKB1KaI2aIyKlzrnvCwfbBuUj8HNgwNMvNJVQofbI,81
|
|
7
7
|
xp/cli/__main__.py,sha256=l2iKwMdat5rTGd3JWs-uGksnYYDDffp_Npz05QdKEeU,117
|
|
8
8
|
xp/cli/commands/__init__.py,sha256=noh8fdZAWq-ihJEboP8WugbIgq4LJ3jUWMRA7720xWE,4909
|
|
@@ -106,7 +106,7 @@ xp/models/telegram/telegram_type.py,sha256=GhqKP63oNMyh2tIvCPcsC5RFp4s4JjhmEqCLC
|
|
|
106
106
|
xp/models/telegram/timeparam_type.py,sha256=Ar8xvSfPmOAgR2g2Je0FgvP01SL7bPvZn5_HrVDpmJM,1137
|
|
107
107
|
xp/models/term/__init__.py,sha256=aFvzGZHr_dI6USb8MJuYLSLMvxi_ZWMVtokHDt8428s,263
|
|
108
108
|
xp/models/term/connection_state.py,sha256=floDRMeMcfgMrYIVsyoVHBXHtxd3hqm-xOdr3oXtaHY,1793
|
|
109
|
-
xp/models/term/module_state.py,sha256=
|
|
109
|
+
xp/models/term/module_state.py,sha256=tg5V3HNicXhXE10WuDSCN8OleVrorXrOosXOEgEAVE0,934
|
|
110
110
|
xp/models/term/protocol_keys_config.py,sha256=CTujcfI2_NOeltjvHy_cnsHzxLSVsGFXieMZlD-zj0Q,1204
|
|
111
111
|
xp/models/term/status_message.py,sha256=DOmzL0dbig5mP1UEoXdgzGT4UG2RyAXa_yRVo5c4x8w,394
|
|
112
112
|
xp/models/term/telegram_display.py,sha256=RJDrJh4tqRmT0i1-tfYy17paEmVb3HY3DMuFPsEhZyc,533
|
|
@@ -162,9 +162,10 @@ xp/services/protocol/telegram_protocol.py,sha256=Ki5DrXsKxiaqLcdP9WWUuhUI7cPu2Df
|
|
|
162
162
|
xp/services/reverse_proxy_service.py,sha256=BUOlcLlTU-R5iuC_96rasug21xo19wK9_4fMQXxc0QM,15061
|
|
163
163
|
xp/services/server/__init__.py,sha256=QEcCj-jK0goAukJCe15TKYFQfSAzWsduPT_wW0HxZU8,48
|
|
164
164
|
xp/services/server/base_server_service.py,sha256=B-ntxp3swbwuri-9_2EuvBDi-4Uo9AH-AA4iAFGWIS4,12682
|
|
165
|
+
xp/services/server/client_buffer_manager.py,sha256=1d_MqfzuUqBwaQUiC1n5K76WwSxrdngYAmNH7he6u3o,2235
|
|
165
166
|
xp/services/server/cp20_server_service.py,sha256=SXdI6Jt400T9sLdw86ovEqKRGeV3nYVaHEA9Gcj6W2A,2041
|
|
166
167
|
xp/services/server/device_service_factory.py,sha256=Y4TvSFALeq0zYzHfCwcbikSpmIyYbLcvm9756n5Jm7Q,3744
|
|
167
|
-
xp/services/server/server_service.py,sha256=
|
|
168
|
+
xp/services/server/server_service.py,sha256=2t3guPVX3YUyNJo7B5b1U80eRMyEgE7irT2X8MMQMag,16302
|
|
168
169
|
xp/services/server/xp130_server_service.py,sha256=YnvetDp72-QzkyDGB4qfZZIwFs03HuibUOz2zb9XR0c,2191
|
|
169
170
|
xp/services/server/xp20_server_service.py,sha256=1wJ7A-bRkN9O5Spu3q3LNDW31mNtNF2eNMQ5E6O2ltA,2928
|
|
170
171
|
xp/services/server/xp230_server_service.py,sha256=k9ftCY5tjLFP31mKVCspq283RVaPkGx-Yq61Urk8JLs,1815
|
|
@@ -181,7 +182,7 @@ xp/services/telegram/telegram_service.py,sha256=XrP1CPi0ckxoKBaNwLA6lo-TogWxXgmX
|
|
|
181
182
|
xp/services/telegram/telegram_version_service.py,sha256=M5HdOTsLdcwo122FP-jW6R740ktLrtKf2TiMDVz23h8,10528
|
|
182
183
|
xp/services/term/__init__.py,sha256=BIeOK042bMR-0l6MA80wdW5VuHlpWOXtRER9IG5ilQA,245
|
|
183
184
|
xp/services/term/protocol_monitor_service.py,sha256=PhEzLNzWf1XieQw94ua-hJu9ccwrAzhdxSZGe4kHghs,9945
|
|
184
|
-
xp/services/term/state_monitor_service.py,sha256=
|
|
185
|
+
xp/services/term/state_monitor_service.py,sha256=PgwCH8nce1RODV33aJefiX3on-pSGEgP_4FDAoU5Trc,16218
|
|
185
186
|
xp/term/__init__.py,sha256=Xg2DhBeI3xQJLfc7_BPWI1por-rUXemyer5OtOt9Cus,51
|
|
186
187
|
xp/term/protocol.py,sha256=oLJAExvIaOSpy75A5TaYB_7R9skTTtNtPx8hiJLdy_U,3425
|
|
187
188
|
xp/term/protocol.tcss,sha256=r_KfxrbpycGHLVXqZc6INBBcUJME0hLrAZkF1oqnab4,2126
|
|
@@ -189,7 +190,7 @@ xp/term/state.py,sha256=sR7I6t4gJSkO2YS3TwonAnGPR_f43coCk4xKdWETus0,3233
|
|
|
189
190
|
xp/term/state.tcss,sha256=Njp7fc16cCunLq7hi5RvXjPi4jSCGi5aPDnusb9dq1Y,1401
|
|
190
191
|
xp/term/widgets/__init__.py,sha256=ftWmN_fmjxy2E8Qfm-YSRmzKfgL0KTBCTpgvYWCPbUY,274
|
|
191
192
|
xp/term/widgets/help_menu.py,sha256=w2NjwiC_s16St0rigZ9ef9S0V9Y4v0J5eCVCHAdRKF4,1789
|
|
192
|
-
xp/term/widgets/modules_list.py,sha256=
|
|
193
|
+
xp/term/widgets/modules_list.py,sha256=DD0PUnY4gv05hkCVxThWULHg1ZNNY8xr1XFaZrv9kS4,7666
|
|
193
194
|
xp/term/widgets/protocol_log.py,sha256=CJUpckWj7GC1kVqixDadteyGnI4aHyzd4kkH-pSbzno,2600
|
|
194
195
|
xp/term/widgets/status_footer.py,sha256=bxrcqKzJ9V0aPSn_WwraVpJz7NxBUh3yIjA3fwv5nVA,3256
|
|
195
196
|
xp/utils/__init__.py,sha256=_avMF_UOkfR3tNeDIPqQ5odmbq5raKkaq1rZ9Cn1CJs,332
|
|
@@ -200,4 +201,4 @@ xp/utils/logging.py,sha256=rZDXwlBrYK8A6MPq5StsMNpgsRowzJXM6fvROPwJdGM,3750
|
|
|
200
201
|
xp/utils/serialization.py,sha256=RWHHk86feaB4ZP7rjE4qOWK0900yg2joUBDkP76gfOY,4618
|
|
201
202
|
xp/utils/state_machine.py,sha256=Oe2sLwCh9z_vr1tF6X0ZRGTeuckRQAGzmef7xc9CNdc,2413
|
|
202
203
|
xp/utils/time_utils.py,sha256=dEyViDlAG9GWU-J3D_YVa-sGma6yiyyMTgN4h2x3PY4,3781
|
|
203
|
-
conson_xp-1.
|
|
204
|
+
conson_xp-1.29.0.dist-info/RECORD,,
|
xp/__init__.py
CHANGED
xp/models/term/module_state.py
CHANGED
|
@@ -13,6 +13,7 @@ class ModuleState:
|
|
|
13
13
|
name: Module name/identifier (e.g., A01, A02).
|
|
14
14
|
serial_number: Module serial number.
|
|
15
15
|
module_type: Module type designation (e.g., XP130, XP230, XP24).
|
|
16
|
+
link_number: Link number for the module.
|
|
16
17
|
outputs: Output states as space-separated binary values. Empty string for modules without outputs.
|
|
17
18
|
auto_report: Auto-report enabled status (Y/N).
|
|
18
19
|
error_status: Module status ("OK" or error code like "E10").
|
|
@@ -22,6 +23,7 @@ class ModuleState:
|
|
|
22
23
|
name: str
|
|
23
24
|
serial_number: str
|
|
24
25
|
module_type: str
|
|
26
|
+
link_number: int
|
|
25
27
|
outputs: str
|
|
26
28
|
auto_report: bool
|
|
27
29
|
error_status: str
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Client buffer manager for broadcasting telegrams to connected clients.
|
|
2
|
+
|
|
3
|
+
This module provides thread-safe management of per-client telegram queues,
|
|
4
|
+
enabling broadcast of telegrams from device services to all connected clients.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import queue
|
|
8
|
+
import socket
|
|
9
|
+
import threading
|
|
10
|
+
from typing import Dict, Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ClientBufferManager:
|
|
14
|
+
"""
|
|
15
|
+
Thread-safe manager for client telegram queues.
|
|
16
|
+
|
|
17
|
+
Manages individual queues for each connected client, providing thread-safe
|
|
18
|
+
operations for client registration, unregistration, and telegram broadcasting.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self) -> None:
|
|
22
|
+
"""Initialize the client buffer manager."""
|
|
23
|
+
self._buffers: Dict[socket.socket, queue.Queue[str]] = {}
|
|
24
|
+
self._lock = threading.Lock()
|
|
25
|
+
|
|
26
|
+
def register_client(self, client_socket: socket.socket) -> queue.Queue[str]:
|
|
27
|
+
"""Register a new client and create its telegram queue.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
client_socket: The socket of the connecting client.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
The newly created queue for this client.
|
|
34
|
+
"""
|
|
35
|
+
with self._lock:
|
|
36
|
+
client_queue: queue.Queue[str] = queue.Queue()
|
|
37
|
+
self._buffers[client_socket] = client_queue
|
|
38
|
+
return client_queue
|
|
39
|
+
|
|
40
|
+
def unregister_client(self, client_socket: socket.socket) -> None:
|
|
41
|
+
"""Unregister a client and remove its telegram queue.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
client_socket: The socket of the disconnecting client.
|
|
45
|
+
"""
|
|
46
|
+
with self._lock:
|
|
47
|
+
self._buffers.pop(client_socket, None)
|
|
48
|
+
|
|
49
|
+
def broadcast(self, telegram: str) -> None:
|
|
50
|
+
"""Broadcast a telegram to all connected clients.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
telegram: The telegram string to broadcast.
|
|
54
|
+
"""
|
|
55
|
+
with self._lock:
|
|
56
|
+
for client_queue in self._buffers.values():
|
|
57
|
+
client_queue.put(telegram)
|
|
58
|
+
|
|
59
|
+
def get_queue(self, client_socket: socket.socket) -> Optional[queue.Queue[str]]:
|
|
60
|
+
"""Retrieve the queue for a specific client.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
client_socket: The socket of the client.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
The client's queue if registered, None otherwise.
|
|
67
|
+
"""
|
|
68
|
+
with self._lock:
|
|
69
|
+
return self._buffers.get(client_socket)
|
|
@@ -5,6 +5,7 @@ Discover Request telegrams with configurable device information.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import logging
|
|
8
|
+
import queue
|
|
8
9
|
import socket
|
|
9
10
|
import threading
|
|
10
11
|
from pathlib import Path
|
|
@@ -15,6 +16,7 @@ from xp.models.homekit.homekit_conson_config import (
|
|
|
15
16
|
ConsonModuleListConfig,
|
|
16
17
|
)
|
|
17
18
|
from xp.services.server.base_server_service import BaseServerService
|
|
19
|
+
from xp.services.server.client_buffer_manager import ClientBufferManager
|
|
18
20
|
from xp.services.server.device_service_factory import DeviceServiceFactory
|
|
19
21
|
from xp.services.telegram.telegram_discover_service import TelegramDiscoverService
|
|
20
22
|
from xp.services.telegram.telegram_service import TelegramService
|
|
@@ -68,7 +70,7 @@ class ServerService:
|
|
|
68
70
|
None # Background thread for storm
|
|
69
71
|
)
|
|
70
72
|
self.collector_stop_event = threading.Event() # Event to stop thread
|
|
71
|
-
self.
|
|
73
|
+
self.client_buffers = ClientBufferManager() # Per-client queue manager
|
|
72
74
|
|
|
73
75
|
# Set up logging
|
|
74
76
|
self.logger = logging.getLogger(__name__)
|
|
@@ -191,6 +193,9 @@ class ServerService:
|
|
|
191
193
|
self, client_socket: socket.socket, client_address: tuple[str, int]
|
|
192
194
|
) -> None:
|
|
193
195
|
"""Handle individual client connection."""
|
|
196
|
+
# Register client and get its dedicated queue
|
|
197
|
+
client_queue = self.client_buffers.register_client(client_socket)
|
|
198
|
+
|
|
194
199
|
try:
|
|
195
200
|
|
|
196
201
|
idle_timeout = 300
|
|
@@ -202,11 +207,14 @@ class ServerService:
|
|
|
202
207
|
|
|
203
208
|
while True:
|
|
204
209
|
|
|
205
|
-
# send waiting buffer
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
+
# send waiting buffer from client's dedicated queue
|
|
211
|
+
while not client_queue.empty():
|
|
212
|
+
try:
|
|
213
|
+
buffer = client_queue.get_nowait()
|
|
214
|
+
client_socket.send(buffer.encode("latin-1"))
|
|
215
|
+
self.logger.debug(f"Sent buffer to {client_address}")
|
|
216
|
+
except queue.Empty:
|
|
217
|
+
break
|
|
210
218
|
|
|
211
219
|
# Receive data from client
|
|
212
220
|
data = None
|
|
@@ -230,11 +238,8 @@ class ServerService:
|
|
|
230
238
|
|
|
231
239
|
# Process request (discover or data request)
|
|
232
240
|
responses = self._process_request(message)
|
|
233
|
-
|
|
234
|
-
# Send responses
|
|
235
241
|
for response in responses:
|
|
236
|
-
|
|
237
|
-
self.logger.debug(f"Sent to {client_address}: {response[:-1]}")
|
|
242
|
+
self.client_buffers.broadcast(response)
|
|
238
243
|
|
|
239
244
|
except socket.timeout:
|
|
240
245
|
self.logger.debug(f"Client {client_address} timed out")
|
|
@@ -242,6 +247,8 @@ class ServerService:
|
|
|
242
247
|
self.logger.error(f"Error handling client {client_address}: {e}")
|
|
243
248
|
finally:
|
|
244
249
|
try:
|
|
250
|
+
# Unregister client before closing socket
|
|
251
|
+
self.client_buffers.unregister_client(client_socket)
|
|
245
252
|
client_socket.close()
|
|
246
253
|
self.logger.info(f"Client {client_address} disconnected")
|
|
247
254
|
except Exception as e:
|
|
@@ -330,6 +337,8 @@ class ServerService:
|
|
|
330
337
|
self.logger.warning(f"Failed to parse telegram: {telegram}")
|
|
331
338
|
return responses
|
|
332
339
|
|
|
340
|
+
self.client_buffers.broadcast(parsed_telegram.raw_telegram)
|
|
341
|
+
|
|
333
342
|
# Handle discover requests
|
|
334
343
|
if self.discover_service.is_discover_request(parsed_telegram):
|
|
335
344
|
for device_service in self.device_services.values():
|
|
@@ -421,7 +430,8 @@ class ServerService:
|
|
|
421
430
|
collected = 0
|
|
422
431
|
for device_service in self.device_services.values():
|
|
423
432
|
telegram_buffer = device_service.collect_telegram_buffer()
|
|
424
|
-
|
|
433
|
+
for telegram in telegram_buffer:
|
|
434
|
+
self.client_buffers.broadcast(telegram)
|
|
425
435
|
collected += len(telegram_buffer)
|
|
426
436
|
|
|
427
437
|
# Wait a bit before checking again
|
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
4
|
from datetime import datetime
|
|
5
|
-
from typing import Dict, List
|
|
5
|
+
from typing import Dict, List, Optional
|
|
6
6
|
|
|
7
7
|
from psygnal import Signal
|
|
8
8
|
|
|
9
9
|
from xp.models.homekit.homekit_conson_config import ConsonModuleListConfig
|
|
10
10
|
from xp.models.protocol.conbus_protocol import TelegramReceivedEvent
|
|
11
11
|
from xp.models.telegram.datapoint_type import DataPointType
|
|
12
|
+
from xp.models.telegram.module_type_code import ModuleTypeCode
|
|
12
13
|
from xp.models.telegram.system_function import SystemFunction
|
|
13
14
|
from xp.models.telegram.telegram_type import TelegramType
|
|
14
15
|
from xp.models.term.connection_state import ConnectionState
|
|
@@ -78,6 +79,7 @@ class StateMonitorService:
|
|
|
78
79
|
name=module_config.name,
|
|
79
80
|
serial_number=module_config.serial_number,
|
|
80
81
|
module_type=module_config.module_type,
|
|
82
|
+
link_number=module_config.link_number,
|
|
81
83
|
outputs="", # Empty initially
|
|
82
84
|
auto_report=auto_report,
|
|
83
85
|
error_status="OK",
|
|
@@ -239,15 +241,24 @@ class StateMonitorService:
|
|
|
239
241
|
def _on_telegram_received(self, event: TelegramReceivedEvent) -> None:
|
|
240
242
|
"""Handle telegram received event.
|
|
241
243
|
|
|
242
|
-
|
|
244
|
+
Routes telegrams to appropriate handlers based on type.
|
|
245
|
+
Processes reply telegrams for datapoint queries and event telegrams for state changes.
|
|
243
246
|
|
|
244
247
|
Args:
|
|
245
248
|
event: Telegram received event.
|
|
246
249
|
"""
|
|
247
|
-
#
|
|
248
|
-
if event.telegram_type
|
|
249
|
-
|
|
250
|
+
# Route based on telegram type
|
|
251
|
+
if event.telegram_type == TelegramType.REPLY:
|
|
252
|
+
self._handle_reply_telegram(event)
|
|
253
|
+
elif event.telegram_type == TelegramType.EVENT:
|
|
254
|
+
self._handle_event_telegram(event)
|
|
255
|
+
|
|
256
|
+
def _handle_reply_telegram(self, event: TelegramReceivedEvent) -> None:
|
|
257
|
+
"""Handle reply telegram for datapoint queries.
|
|
250
258
|
|
|
259
|
+
Args:
|
|
260
|
+
event: Telegram received event.
|
|
261
|
+
"""
|
|
251
262
|
serial_number = event.serial_number
|
|
252
263
|
if not serial_number or serial_number not in self._module_states:
|
|
253
264
|
return
|
|
@@ -289,6 +300,95 @@ class StateMonitorService:
|
|
|
289
300
|
self.on_connection_state_changed.emit(self._connection_state)
|
|
290
301
|
self.on_status_message.emit(f"Protocol error: {failure}")
|
|
291
302
|
|
|
303
|
+
def _find_module_by_link(self, link_number: int) -> Optional[ModuleState]:
|
|
304
|
+
"""Find module state by link number.
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
link_number: Link number to search for.
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
ModuleState if found, None otherwise.
|
|
311
|
+
"""
|
|
312
|
+
for module_state in self._module_states.values():
|
|
313
|
+
if module_state.link_number == link_number:
|
|
314
|
+
return module_state
|
|
315
|
+
return None
|
|
316
|
+
|
|
317
|
+
def _update_output_bit(
|
|
318
|
+
self, module_state: ModuleState, output_number: int, output_state: bool
|
|
319
|
+
) -> None:
|
|
320
|
+
"""Update a single output bit in module state.
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
module_state: Module state to update.
|
|
324
|
+
output_number: Output number (0-3 for XP24).
|
|
325
|
+
output_state: True for ON, False for OFF.
|
|
326
|
+
"""
|
|
327
|
+
# Parse existing outputs string "0 1 0 0" → [0, 1, 0, 0]
|
|
328
|
+
outputs = module_state.outputs.split() if module_state.outputs else []
|
|
329
|
+
|
|
330
|
+
# Ensure we have enough outputs
|
|
331
|
+
while len(outputs) <= output_number:
|
|
332
|
+
outputs.append("0")
|
|
333
|
+
|
|
334
|
+
# Update the specific output
|
|
335
|
+
outputs[output_number] = "1" if output_state else "0"
|
|
336
|
+
|
|
337
|
+
# Convert back to string format
|
|
338
|
+
module_state.outputs = " ".join(outputs)
|
|
339
|
+
|
|
340
|
+
def _handle_event_telegram(self, event: TelegramReceivedEvent) -> None:
|
|
341
|
+
"""Handle event telegram for output state changes.
|
|
342
|
+
|
|
343
|
+
Processes XP24 output event telegrams to update module state in real-time.
|
|
344
|
+
Output events use input_number field with values 80-83 to represent outputs 0-3.
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
event: Telegram received event containing event telegram.
|
|
348
|
+
"""
|
|
349
|
+
# Parse the event telegram
|
|
350
|
+
event_telegram = self._telegram_service.parse_event_telegram(event.frame)
|
|
351
|
+
if not event_telegram:
|
|
352
|
+
self.logger.debug("Failed to parse event telegram")
|
|
353
|
+
return
|
|
354
|
+
|
|
355
|
+
# Only process XP24 output events
|
|
356
|
+
if event_telegram.module_type != ModuleTypeCode.XP24.value:
|
|
357
|
+
self.logger.debug(
|
|
358
|
+
f"Ignoring event from module type {event_telegram.module_type}"
|
|
359
|
+
)
|
|
360
|
+
return
|
|
361
|
+
|
|
362
|
+
# Validate output number range (80-83 for XP24 outputs 0-3)
|
|
363
|
+
if not (80 <= event_telegram.input_number <= 83):
|
|
364
|
+
self.logger.debug(
|
|
365
|
+
f"Ignoring input event I{event_telegram.input_number:02d}"
|
|
366
|
+
)
|
|
367
|
+
return
|
|
368
|
+
|
|
369
|
+
# Find module by link number
|
|
370
|
+
module_state = self._find_module_by_link(event_telegram.link_number)
|
|
371
|
+
if not module_state:
|
|
372
|
+
self.logger.debug(
|
|
373
|
+
f"Module not found for link number {event_telegram.link_number}"
|
|
374
|
+
)
|
|
375
|
+
return
|
|
376
|
+
|
|
377
|
+
# Convert input_number to output_number (80→0, 81→1, 82→2, 83→3)
|
|
378
|
+
output_number = event_telegram.input_number - 80
|
|
379
|
+
output_state = event_telegram.is_button_press # M=True, B=False
|
|
380
|
+
|
|
381
|
+
# Update output state
|
|
382
|
+
self._update_output_bit(module_state, output_number, output_state)
|
|
383
|
+
module_state.last_update = datetime.now()
|
|
384
|
+
|
|
385
|
+
# Emit signal for UI update
|
|
386
|
+
self.on_module_state_changed.emit(module_state)
|
|
387
|
+
self.logger.debug(
|
|
388
|
+
f"Updated {module_state.name} output {output_number} to "
|
|
389
|
+
f"{'ON' if output_state else 'OFF'}"
|
|
390
|
+
)
|
|
391
|
+
|
|
292
392
|
def cleanup(self) -> None:
|
|
293
393
|
"""Clean up service resources."""
|
|
294
394
|
self._disconnect_signals()
|
xp/term/widgets/modules_list.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from datetime import datetime
|
|
4
4
|
from typing import Any, List, Optional
|
|
5
5
|
|
|
6
|
+
from rich.text import Text
|
|
6
7
|
from textual.app import ComposeResult
|
|
7
8
|
from textual.widgets import DataTable, Static
|
|
8
9
|
|
|
@@ -14,7 +15,7 @@ class ModulesListWidget(Static):
|
|
|
14
15
|
"""Widget displaying module states in a data table.
|
|
15
16
|
|
|
16
17
|
Shows module information with real-time updates from StateMonitorService.
|
|
17
|
-
Table displays: name, serial_number, module_type, outputs, report, status, last_update.
|
|
18
|
+
Table displays: name, serial_number, module_type, link_number, outputs, report, status, last_update.
|
|
18
19
|
|
|
19
20
|
Attributes:
|
|
20
21
|
service: StateMonitorService for module state updates.
|
|
@@ -54,11 +55,9 @@ class ModulesListWidget(Static):
|
|
|
54
55
|
self.border_title = "Modules"
|
|
55
56
|
|
|
56
57
|
if self.table:
|
|
57
|
-
# Set table to full width
|
|
58
|
-
self.table.styles.width = "100%"
|
|
59
|
-
|
|
60
58
|
# Setup table columns
|
|
61
59
|
self.table.add_column("name", key="name")
|
|
60
|
+
self.table.add_column("link", key="link_number")
|
|
62
61
|
self.table.add_column("serial number", key="serial_number")
|
|
63
62
|
self.table.add_column("module type", key="module_type")
|
|
64
63
|
self.table.add_column("outputs", key="outputs")
|
|
@@ -115,13 +114,17 @@ class ModulesListWidget(Static):
|
|
|
115
114
|
row_key, "outputs", self._format_outputs(module_state.outputs)
|
|
116
115
|
)
|
|
117
116
|
self.table.update_cell(
|
|
118
|
-
row_key,
|
|
117
|
+
row_key,
|
|
118
|
+
"report",
|
|
119
|
+
Text(self._format_report(module_state.auto_report), justify="center"),
|
|
119
120
|
)
|
|
120
121
|
self.table.update_cell(row_key, "status", module_state.error_status)
|
|
121
122
|
self.table.update_cell(
|
|
122
123
|
row_key,
|
|
123
124
|
"last_update",
|
|
124
|
-
|
|
125
|
+
Text(
|
|
126
|
+
self._format_last_update(module_state.last_update), justify="center"
|
|
127
|
+
),
|
|
125
128
|
)
|
|
126
129
|
else:
|
|
127
130
|
# Add new row
|
|
@@ -138,12 +141,13 @@ class ModulesListWidget(Static):
|
|
|
138
141
|
|
|
139
142
|
row_key = self.table.add_row(
|
|
140
143
|
module_state.name,
|
|
144
|
+
Text(str(module_state.link_number), justify="right"),
|
|
141
145
|
module_state.serial_number,
|
|
142
146
|
module_state.module_type,
|
|
143
147
|
self._format_outputs(module_state.outputs),
|
|
144
|
-
self._format_report(module_state.auto_report),
|
|
148
|
+
Text(self._format_report(module_state.auto_report), justify="center"),
|
|
145
149
|
module_state.error_status,
|
|
146
|
-
self._format_last_update(module_state.last_update),
|
|
150
|
+
Text(self._format_last_update(module_state.last_update), justify="center"),
|
|
147
151
|
)
|
|
148
152
|
self._row_keys[module_state.serial_number] = row_key
|
|
149
153
|
|
|
@@ -213,5 +217,8 @@ class ModulesListWidget(Static):
|
|
|
213
217
|
self.table.update_cell(
|
|
214
218
|
row_key,
|
|
215
219
|
"last_update",
|
|
216
|
-
|
|
220
|
+
Text(
|
|
221
|
+
self._format_last_update(module_state.last_update),
|
|
222
|
+
justify="center",
|
|
223
|
+
),
|
|
217
224
|
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|