conson-xp 1.19.0__py3-none-any.whl → 1.21.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.19.0.dist-info → conson_xp-1.21.0.dist-info}/METADATA +6 -1
- {conson_xp-1.19.0.dist-info → conson_xp-1.21.0.dist-info}/RECORD +27 -13
- {conson_xp-1.19.0.dist-info → conson_xp-1.21.0.dist-info}/WHEEL +1 -1
- xp/__init__.py +1 -1
- xp/cli/commands/__init__.py +4 -0
- xp/cli/commands/conbus/conbus_receive_commands.py +2 -1
- xp/cli/commands/term/__init__.py +5 -0
- xp/cli/commands/term/term.py +12 -0
- xp/cli/commands/term/term_commands.py +31 -0
- xp/cli/main.py +7 -35
- xp/models/conbus/conbus_client_config.py +1 -0
- xp/models/conbus/conbus_logger_config.py +107 -0
- xp/models/term/__init__.py +11 -0
- xp/models/term/protocol_keys_config.py +45 -0
- xp/services/conbus/conbus_receive_service.py +58 -30
- xp/services/protocol/conbus_event_protocol.py +36 -3
- xp/term/__init__.py +1 -0
- xp/term/app.py +158 -0
- xp/term/protocol.tcss +135 -0
- xp/term/protocol.yml +139 -0
- xp/term/widgets/__init__.py +1 -0
- xp/term/widgets/protocol_log.py +393 -0
- xp/utils/dependencies.py +25 -6
- xp/utils/logging.py +102 -0
- xp/utils/state_machine.py +81 -0
- {conson_xp-1.19.0.dist-info → conson_xp-1.21.0.dist-info}/entry_points.txt +0 -0
- {conson_xp-1.19.0.dist-info → conson_xp-1.21.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
This module implements the Twisted protocol for Conbus communication.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
+
import asyncio
|
|
6
7
|
import logging
|
|
7
8
|
from queue import SimpleQueue
|
|
8
9
|
from random import randint
|
|
@@ -208,6 +209,14 @@ class ConbusEventProtocol(protocol.Protocol, protocol.ClientFactory):
|
|
|
208
209
|
f"F{system_function.value}"
|
|
209
210
|
f"D{data_value}"
|
|
210
211
|
)
|
|
212
|
+
self.send_raw_telegram(payload)
|
|
213
|
+
|
|
214
|
+
def send_raw_telegram(self, payload: str) -> None:
|
|
215
|
+
"""Send telegram with specified parameters.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
payload: Telegram to send.
|
|
219
|
+
"""
|
|
211
220
|
self.telegram_queue.put_nowait(payload.encode())
|
|
212
221
|
self.call_later(0.0, self.start_queue_manager)
|
|
213
222
|
|
|
@@ -302,14 +311,21 @@ class ConbusEventProtocol(protocol.Protocol, protocol.ClientFactory):
|
|
|
302
311
|
self.logger.info("Stopping reactor")
|
|
303
312
|
self._reactor.stop()
|
|
304
313
|
|
|
305
|
-
def
|
|
306
|
-
"""
|
|
307
|
-
# Connect to TCP server
|
|
314
|
+
def connect(self) -> None:
|
|
315
|
+
"""Connect to TCP server."""
|
|
308
316
|
self.logger.info(
|
|
309
317
|
f"Connecting to TCP server {self.cli_config.ip}:{self.cli_config.port}"
|
|
310
318
|
)
|
|
311
319
|
self._reactor.connectTCP(self.cli_config.ip, self.cli_config.port, self)
|
|
312
320
|
|
|
321
|
+
def disconnect(self) -> None:
|
|
322
|
+
"""Disconnect from TCP server."""
|
|
323
|
+
self.logger.info("Disconnecting TCP server")
|
|
324
|
+
self._reactor.disconnectAll()
|
|
325
|
+
|
|
326
|
+
def start_reactor(self) -> None:
|
|
327
|
+
"""Start the reactor if it's running."""
|
|
328
|
+
self.connect()
|
|
313
329
|
# Run the reactor (which now uses asyncio underneath)
|
|
314
330
|
self.logger.info("Starting reactor event loop.")
|
|
315
331
|
self._reactor.run()
|
|
@@ -340,6 +356,23 @@ class ConbusEventProtocol(protocol.Protocol, protocol.ClientFactory):
|
|
|
340
356
|
later = randint(10, 80) / 100
|
|
341
357
|
self.call_later(later, self.process_telegram_queue)
|
|
342
358
|
|
|
359
|
+
def set_event_loop(self, event_loop: asyncio.AbstractEventLoop) -> None:
|
|
360
|
+
"""Change the event loop.
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
event_loop: the event loop instance.
|
|
364
|
+
"""
|
|
365
|
+
reactor = self._reactor
|
|
366
|
+
if hasattr(reactor, "_asyncioEventloop"):
|
|
367
|
+
reactor._asyncioEventloop = event_loop
|
|
368
|
+
|
|
369
|
+
# Set reactor to running state
|
|
370
|
+
if not reactor.running:
|
|
371
|
+
reactor.running = True
|
|
372
|
+
if hasattr(reactor, "startRunning"):
|
|
373
|
+
reactor.startRunning()
|
|
374
|
+
self.logger.info("Set reactor to running state")
|
|
375
|
+
|
|
343
376
|
def __enter__(self) -> "ConbusEventProtocol":
|
|
344
377
|
"""Enter context manager.
|
|
345
378
|
|
xp/term/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""TUI (Terminal User Interface) module for XP."""
|
xp/term/app.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Protocol Monitor TUI Application."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
|
|
6
|
+
from textual.app import App, ComposeResult
|
|
7
|
+
from textual.containers import Horizontal, Vertical
|
|
8
|
+
from textual.widgets import DataTable, Footer, Static
|
|
9
|
+
|
|
10
|
+
from xp.models.term import ProtocolKeysConfig
|
|
11
|
+
from xp.term.widgets.protocol_log import ProtocolLogWidget
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ProtocolMonitorApp(App[None]):
|
|
15
|
+
"""Textual app for real-time protocol monitoring.
|
|
16
|
+
|
|
17
|
+
Displays live RX/TX telegram stream from Conbus server in an interactive
|
|
18
|
+
terminal interface with keyboard shortcuts for control.
|
|
19
|
+
|
|
20
|
+
Attributes:
|
|
21
|
+
container: ServiceContainer for dependency injection.
|
|
22
|
+
CSS_PATH: Path to CSS stylesheet file.
|
|
23
|
+
BINDINGS: Keyboard bindings for app actions.
|
|
24
|
+
TITLE: Application title displayed in header.
|
|
25
|
+
ENABLE_COMMAND_PALETTE: Disable Textual's command palette feature.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
CSS_PATH = Path(__file__).parent / "protocol.tcss"
|
|
29
|
+
TITLE = "Protocol Monitor"
|
|
30
|
+
ENABLE_COMMAND_PALETTE = False
|
|
31
|
+
|
|
32
|
+
BINDINGS = [
|
|
33
|
+
("Q", "quit", "Quit"),
|
|
34
|
+
("C", "toggle_connection", "Connect"),
|
|
35
|
+
("R", "reset", "Reset"),
|
|
36
|
+
("0-9,a-q", "protocol_keys", "Keys"),
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
def __init__(self, container: Any) -> None:
|
|
40
|
+
"""Initialize the Protocol Monitor app.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
container: ServiceContainer for resolving services.
|
|
44
|
+
"""
|
|
45
|
+
super().__init__()
|
|
46
|
+
self.container = container
|
|
47
|
+
self.protocol_widget: Optional[ProtocolLogWidget] = None
|
|
48
|
+
self.status_widget: Optional[Static] = None
|
|
49
|
+
self.status_text_widget: Optional[Static] = None
|
|
50
|
+
self.help_table: Optional[DataTable] = None
|
|
51
|
+
self.protocol_keys = self._load_protocol_keys()
|
|
52
|
+
|
|
53
|
+
def _load_protocol_keys(self) -> ProtocolKeysConfig:
|
|
54
|
+
"""Load protocol keys from YAML config file.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
ProtocolKeysConfig instance.
|
|
58
|
+
"""
|
|
59
|
+
config_path = Path(__file__).parent / "protocol.yml"
|
|
60
|
+
return ProtocolKeysConfig.from_yaml(config_path)
|
|
61
|
+
|
|
62
|
+
def compose(self) -> ComposeResult:
|
|
63
|
+
"""Compose the app layout with widgets.
|
|
64
|
+
|
|
65
|
+
Yields:
|
|
66
|
+
ProtocolLogWidget and Footer widgets.
|
|
67
|
+
"""
|
|
68
|
+
with Horizontal(id="main-container"):
|
|
69
|
+
self.protocol_widget = ProtocolLogWidget(container=self.container)
|
|
70
|
+
yield self.protocol_widget
|
|
71
|
+
|
|
72
|
+
# Help menu (hidden by default)
|
|
73
|
+
help_container = Vertical(id="help-menu")
|
|
74
|
+
help_container.border_title = "Help menu"
|
|
75
|
+
help_container.can_focus = False
|
|
76
|
+
with help_container:
|
|
77
|
+
self.help_table = DataTable(id="help-table", show_header=False)
|
|
78
|
+
self.help_table.can_focus = False
|
|
79
|
+
yield self.help_table
|
|
80
|
+
|
|
81
|
+
with Horizontal(id="footer-container"):
|
|
82
|
+
yield Footer()
|
|
83
|
+
self.status_widget = Static("○", id="status-line")
|
|
84
|
+
yield self.status_widget
|
|
85
|
+
|
|
86
|
+
def action_toggle_connection(self) -> None:
|
|
87
|
+
"""Toggle connection on 'c' key press.
|
|
88
|
+
|
|
89
|
+
Connects if disconnected/failed, disconnects if connected/connecting.
|
|
90
|
+
"""
|
|
91
|
+
if self.protocol_widget:
|
|
92
|
+
from xp.term.widgets.protocol_log import ConnectionState
|
|
93
|
+
|
|
94
|
+
state = self.protocol_widget.connection_state
|
|
95
|
+
if state in (ConnectionState.CONNECTED, ConnectionState.CONNECTING):
|
|
96
|
+
self.protocol_widget.disconnect()
|
|
97
|
+
else:
|
|
98
|
+
self.protocol_widget.connect()
|
|
99
|
+
|
|
100
|
+
def action_reset(self) -> None:
|
|
101
|
+
"""Reset and clear protocol widget on 'r' key press."""
|
|
102
|
+
if self.protocol_widget:
|
|
103
|
+
self.protocol_widget.clear_log()
|
|
104
|
+
|
|
105
|
+
def on_key(self, event: Any) -> None:
|
|
106
|
+
"""Handle key press events for protocol keys.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
event: Key press event from Textual.
|
|
110
|
+
"""
|
|
111
|
+
if event.key in self.protocol_keys.protocol and self.protocol_widget:
|
|
112
|
+
key_config = self.protocol_keys.protocol[event.key]
|
|
113
|
+
for telegram in key_config.telegrams:
|
|
114
|
+
self.protocol_widget.send_telegram(key_config.name, telegram)
|
|
115
|
+
|
|
116
|
+
def on_mount(self) -> None:
|
|
117
|
+
"""Set up status line updates when app mounts."""
|
|
118
|
+
if self.protocol_widget:
|
|
119
|
+
self.protocol_widget.watch(
|
|
120
|
+
self.protocol_widget,
|
|
121
|
+
"connection_state",
|
|
122
|
+
self._update_status,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# Initialize help table
|
|
126
|
+
if self.help_table:
|
|
127
|
+
self.help_table.add_columns("Key", "Command")
|
|
128
|
+
for key, config in self.protocol_keys.protocol.items():
|
|
129
|
+
self.help_table.add_row(key, config.name)
|
|
130
|
+
|
|
131
|
+
def _update_status(self, state: Any) -> None:
|
|
132
|
+
"""Update status line with connection state.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
state: Current connection state.
|
|
136
|
+
"""
|
|
137
|
+
if self.status_widget:
|
|
138
|
+
# Map states to colored dots
|
|
139
|
+
status_map = {
|
|
140
|
+
"CONNECTED": "[green]●[/green]",
|
|
141
|
+
"CONNECTING": "[yellow]●[/yellow]",
|
|
142
|
+
"DISCONNECTING": "[yellow]●[/yellow]",
|
|
143
|
+
"FAILED": "[red]●[/red]",
|
|
144
|
+
"DISCONNECTED": "○",
|
|
145
|
+
}
|
|
146
|
+
dot = status_map.get(state.value, "○")
|
|
147
|
+
self.status_widget.update(dot)
|
|
148
|
+
|
|
149
|
+
def on_protocol_log_widget_status_message_changed(
|
|
150
|
+
self, message: ProtocolLogWidget.StatusMessageChanged
|
|
151
|
+
) -> None:
|
|
152
|
+
"""Handle status message changes from protocol widget.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
message: Message containing the status text.
|
|
156
|
+
"""
|
|
157
|
+
if self.status_text_widget:
|
|
158
|
+
self.status_text_widget.update(message.message)
|
xp/term/protocol.tcss
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/* Protocol Monitor TUI Styling */
|
|
2
|
+
|
|
3
|
+
/* App-level styling */
|
|
4
|
+
Screen {
|
|
5
|
+
background: $background;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/* Protocol Log Widget */
|
|
9
|
+
ProtocolLogWidget {
|
|
10
|
+
border: solid $success;
|
|
11
|
+
width: 1fr;
|
|
12
|
+
height: 1fr;
|
|
13
|
+
background: $background;
|
|
14
|
+
padding: 1;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
ProtocolLogWidget > RichLog {
|
|
18
|
+
background: $background !important;
|
|
19
|
+
scrollbar-background: $background;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
ProtocolLogWidget > RichLog:focus {
|
|
23
|
+
background: $background !important;
|
|
24
|
+
background-tint: transparent;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
ProtocolLogWidget > .connection-status {
|
|
28
|
+
color: $text;
|
|
29
|
+
text-align: center;
|
|
30
|
+
padding: 1;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
ProtocolLogWidget > .connection-status.connecting {
|
|
34
|
+
color: $warning;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
ProtocolLogWidget > .connection-status.connected {
|
|
38
|
+
color: $success;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
ProtocolLogWidget > .connection-status.failed {
|
|
42
|
+
color: $error;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* Message display styling */
|
|
46
|
+
.message-tx {
|
|
47
|
+
color: $success;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.message-rx {
|
|
51
|
+
color: $success;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.message-frame {
|
|
55
|
+
color: $text-muted;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* Main container */
|
|
59
|
+
#main-container {
|
|
60
|
+
height: 1fr;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* Help menu styling */
|
|
64
|
+
#help-menu {
|
|
65
|
+
width: 35;
|
|
66
|
+
height: 1fr;
|
|
67
|
+
background: $background;
|
|
68
|
+
border: solid $success;
|
|
69
|
+
padding: 1;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
#help-menu:focus {
|
|
73
|
+
border: solid $success;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
#help-title {
|
|
77
|
+
color: $success;
|
|
78
|
+
text-align: center;
|
|
79
|
+
text-style: bold;
|
|
80
|
+
margin-bottom: 1;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
#help-table {
|
|
84
|
+
background: $background;
|
|
85
|
+
height: auto;
|
|
86
|
+
color: $success;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
DataTable {
|
|
90
|
+
background: $background;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
DataTable > .datatable--header {
|
|
94
|
+
background: $background;
|
|
95
|
+
color: $success;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
DataTable > .datatable--cursor {
|
|
99
|
+
background: $background;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
DataTable:focus > .datatable--cursor {
|
|
103
|
+
background: $background;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* Footer styling */
|
|
107
|
+
#footer-container {
|
|
108
|
+
dock: bottom;
|
|
109
|
+
height: 1;
|
|
110
|
+
background: $background;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
Footer {
|
|
114
|
+
width: auto;
|
|
115
|
+
background: $background;
|
|
116
|
+
color: $text;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
#status-text {
|
|
120
|
+
dock: right;
|
|
121
|
+
width: auto;
|
|
122
|
+
padding: 0 1;
|
|
123
|
+
background: $background;
|
|
124
|
+
color: $text;
|
|
125
|
+
text-align: right;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
#status-line {
|
|
129
|
+
dock: right;
|
|
130
|
+
width: auto;
|
|
131
|
+
padding: 0 1;
|
|
132
|
+
background: $background;
|
|
133
|
+
color: $text;
|
|
134
|
+
text-align: right;
|
|
135
|
+
}
|
xp/term/protocol.yml
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
protocol:
|
|
2
|
+
"1":
|
|
3
|
+
name: "Discover"
|
|
4
|
+
telegrams:
|
|
5
|
+
- S0000000000F01D00
|
|
6
|
+
"2":
|
|
7
|
+
name: "Error Code"
|
|
8
|
+
telegrams:
|
|
9
|
+
- S0020044966F02D10
|
|
10
|
+
"3":
|
|
11
|
+
name: "Module Type"
|
|
12
|
+
telegrams:
|
|
13
|
+
- S0020044966F02D00
|
|
14
|
+
"4":
|
|
15
|
+
name: "Auto Report"
|
|
16
|
+
telegrams:
|
|
17
|
+
- S0020044966F02D21
|
|
18
|
+
"5":
|
|
19
|
+
name: "Link Number"
|
|
20
|
+
telegrams:
|
|
21
|
+
- S0020044966F02D04
|
|
22
|
+
"6":
|
|
23
|
+
name: "Blink On"
|
|
24
|
+
telegrams:
|
|
25
|
+
- S0020044966F01D01
|
|
26
|
+
"7":
|
|
27
|
+
name: "Blink Off"
|
|
28
|
+
telegrams:
|
|
29
|
+
- S0020044966F01D00
|
|
30
|
+
"8":
|
|
31
|
+
name: "Output 1 On"
|
|
32
|
+
telegrams:
|
|
33
|
+
- S0020044966F02101
|
|
34
|
+
"9":
|
|
35
|
+
name: "Output 1 Off"
|
|
36
|
+
telegrams:
|
|
37
|
+
- S0020044966F02100
|
|
38
|
+
"0":
|
|
39
|
+
name: "Output State"
|
|
40
|
+
telegrams:
|
|
41
|
+
- S0020044966F02D09
|
|
42
|
+
"a":
|
|
43
|
+
name: "Module State"
|
|
44
|
+
telegrams:
|
|
45
|
+
- S0020044966F02D09
|
|
46
|
+
"b":
|
|
47
|
+
name: "All Off"
|
|
48
|
+
telegrams:
|
|
49
|
+
- E02L00I00M
|
|
50
|
+
- E02L00I00B
|
|
51
|
+
"c":
|
|
52
|
+
name: "All On"
|
|
53
|
+
telegrams:
|
|
54
|
+
- E02L00I08M
|
|
55
|
+
- E02L00I08B
|
|
56
|
+
|
|
57
|
+
"d":
|
|
58
|
+
name: "All 1 On"
|
|
59
|
+
telegrams:
|
|
60
|
+
- E02L01I08M
|
|
61
|
+
- E02L01I08B
|
|
62
|
+
|
|
63
|
+
"e":
|
|
64
|
+
name: "All 1 Off"
|
|
65
|
+
telegrams:
|
|
66
|
+
- E02L01I00M
|
|
67
|
+
- E02L01I00B
|
|
68
|
+
|
|
69
|
+
"f":
|
|
70
|
+
name: "All 2 On"
|
|
71
|
+
telegrams:
|
|
72
|
+
- E02L02I08M
|
|
73
|
+
- E02L02I08B
|
|
74
|
+
|
|
75
|
+
"g":
|
|
76
|
+
name: "All 2 Off"
|
|
77
|
+
telegrams:
|
|
78
|
+
- E02L02I00M
|
|
79
|
+
- E02L02I00B
|
|
80
|
+
|
|
81
|
+
"h":
|
|
82
|
+
name: "All 3 On"
|
|
83
|
+
telegrams:
|
|
84
|
+
- E02L03I08M
|
|
85
|
+
- E02L03I08B
|
|
86
|
+
|
|
87
|
+
"i":
|
|
88
|
+
name: "All 3 Off"
|
|
89
|
+
telegrams:
|
|
90
|
+
- E02L03I00M
|
|
91
|
+
- E02L03I00B
|
|
92
|
+
|
|
93
|
+
"j":
|
|
94
|
+
name: "All 4 On"
|
|
95
|
+
telegrams:
|
|
96
|
+
- E02L04I08M
|
|
97
|
+
- E02L04I08B
|
|
98
|
+
|
|
99
|
+
"k":
|
|
100
|
+
name: "All 4 Off"
|
|
101
|
+
telegrams:
|
|
102
|
+
- E02L04I00M
|
|
103
|
+
- E02L04I00B
|
|
104
|
+
|
|
105
|
+
"l":
|
|
106
|
+
name: "All 5 On"
|
|
107
|
+
telegrams:
|
|
108
|
+
- E02L05I08M
|
|
109
|
+
- E02L05I08B
|
|
110
|
+
|
|
111
|
+
"m":
|
|
112
|
+
name: "All 5 Off"
|
|
113
|
+
telegrams:
|
|
114
|
+
- E02L05I00M
|
|
115
|
+
- E02L05I00B
|
|
116
|
+
|
|
117
|
+
"n":
|
|
118
|
+
name: "All 6 On"
|
|
119
|
+
telegrams:
|
|
120
|
+
- E02L06I08M
|
|
121
|
+
- E02L06I08B
|
|
122
|
+
|
|
123
|
+
"o":
|
|
124
|
+
name: "All 6 Off"
|
|
125
|
+
telegrams:
|
|
126
|
+
- E02L06I00M
|
|
127
|
+
- E02L06I00B
|
|
128
|
+
|
|
129
|
+
"p":
|
|
130
|
+
name: "All 7 On"
|
|
131
|
+
telegrams:
|
|
132
|
+
- E02L07I08M
|
|
133
|
+
- E02L07I08B
|
|
134
|
+
|
|
135
|
+
"q":
|
|
136
|
+
name: "All 7 Off"
|
|
137
|
+
telegrams:
|
|
138
|
+
- E02L07I00M
|
|
139
|
+
- E02L07I00B
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""TUI widgets package."""
|