conson-xp 1.27.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.27.0.dist-info → conson_xp-1.29.0.dist-info}/METADATA +2 -1
- {conson_xp-1.27.0.dist-info → conson_xp-1.29.0.dist-info}/RECORD +22 -16
- xp/__init__.py +1 -1
- xp/cli/commands/term/term_commands.py +21 -0
- xp/models/term/__init__.py +2 -0
- xp/models/term/module_state.py +30 -0
- xp/services/server/client_buffer_manager.py +69 -0
- xp/services/server/server_service.py +21 -11
- xp/services/server/xp24_server_service.py +1 -4
- xp/services/server/xp33_server_service.py +1 -4
- xp/services/telegram/telegram_output_service.py +33 -0
- xp/services/term/__init__.py +2 -1
- xp/services/term/state_monitor_service.py +413 -0
- xp/term/state.py +97 -0
- xp/term/state.tcss +86 -0
- xp/term/widgets/help_menu.py +3 -3
- xp/term/widgets/modules_list.py +224 -0
- xp/term/widgets/status_footer.py +5 -4
- xp/utils/dependencies.py +20 -0
- {conson_xp-1.27.0.dist-info → conson_xp-1.29.0.dist-info}/WHEEL +0 -0
- {conson_xp-1.27.0.dist-info → conson_xp-1.29.0.dist-info}/entry_points.txt +0 -0
- {conson_xp-1.27.0.dist-info → conson_xp-1.29.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""Modules List Widget for displaying module state table."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Any, List, Optional
|
|
5
|
+
|
|
6
|
+
from rich.text import Text
|
|
7
|
+
from textual.app import ComposeResult
|
|
8
|
+
from textual.widgets import DataTable, Static
|
|
9
|
+
|
|
10
|
+
from xp.models.term.module_state import ModuleState
|
|
11
|
+
from xp.services.term.state_monitor_service import StateMonitorService
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ModulesListWidget(Static):
|
|
15
|
+
"""Widget displaying module states in a data table.
|
|
16
|
+
|
|
17
|
+
Shows module information with real-time updates from StateMonitorService.
|
|
18
|
+
Table displays: name, serial_number, module_type, link_number, outputs, report, status, last_update.
|
|
19
|
+
|
|
20
|
+
Attributes:
|
|
21
|
+
service: StateMonitorService for module state updates.
|
|
22
|
+
table: DataTable widget displaying module information.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
service: Optional[StateMonitorService] = None,
|
|
28
|
+
*args: Any,
|
|
29
|
+
**kwargs: Any,
|
|
30
|
+
) -> None:
|
|
31
|
+
"""Initialize the Modules List widget.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
service: Optional StateMonitorService for signal subscriptions.
|
|
35
|
+
args: Additional positional arguments for Static.
|
|
36
|
+
kwargs: Additional keyword arguments for Static.
|
|
37
|
+
"""
|
|
38
|
+
super().__init__(*args, **kwargs)
|
|
39
|
+
self.service = service
|
|
40
|
+
self.table: Optional[DataTable] = None
|
|
41
|
+
self._row_keys: dict[str, Any] = {} # Map serial_number to row key
|
|
42
|
+
|
|
43
|
+
def compose(self) -> ComposeResult:
|
|
44
|
+
"""Compose the widget layout.
|
|
45
|
+
|
|
46
|
+
Yields:
|
|
47
|
+
DataTable widget.
|
|
48
|
+
"""
|
|
49
|
+
self.table = DataTable(id="modules-table", cursor_type="row")
|
|
50
|
+
yield self.table
|
|
51
|
+
|
|
52
|
+
def on_mount(self) -> None:
|
|
53
|
+
"""Initialize table and subscribe to service signals when widget mounts."""
|
|
54
|
+
# Set border title
|
|
55
|
+
self.border_title = "Modules"
|
|
56
|
+
|
|
57
|
+
if self.table:
|
|
58
|
+
# Setup table columns
|
|
59
|
+
self.table.add_column("name", key="name")
|
|
60
|
+
self.table.add_column("link", key="link_number")
|
|
61
|
+
self.table.add_column("serial number", key="serial_number")
|
|
62
|
+
self.table.add_column("module type", key="module_type")
|
|
63
|
+
self.table.add_column("outputs", key="outputs")
|
|
64
|
+
self.table.add_column("report", key="report")
|
|
65
|
+
self.table.add_column("status", key="status")
|
|
66
|
+
self.table.add_column("last update", key="last_update")
|
|
67
|
+
|
|
68
|
+
if self.service:
|
|
69
|
+
self.service.on_module_list_updated.connect(self.update_module_list)
|
|
70
|
+
self.service.on_module_state_changed.connect(self.update_module_state)
|
|
71
|
+
|
|
72
|
+
def on_unmount(self) -> None:
|
|
73
|
+
"""Unsubscribe from service signals when widget unmounts."""
|
|
74
|
+
if self.service:
|
|
75
|
+
self.service.on_module_list_updated.disconnect(self.update_module_list)
|
|
76
|
+
self.service.on_module_state_changed.disconnect(self.update_module_state)
|
|
77
|
+
|
|
78
|
+
def update_module_list(self, module_states: List[ModuleState]) -> None:
|
|
79
|
+
"""Update entire module list from service.
|
|
80
|
+
|
|
81
|
+
Clears existing table and repopulates with all modules.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
module_states: List of all module states.
|
|
85
|
+
"""
|
|
86
|
+
if not self.table:
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
# Clear existing rows
|
|
90
|
+
self.table.clear()
|
|
91
|
+
self._row_keys.clear()
|
|
92
|
+
|
|
93
|
+
# Add all modules
|
|
94
|
+
for module_state in module_states:
|
|
95
|
+
self._add_module_row(module_state)
|
|
96
|
+
|
|
97
|
+
def update_module_state(self, module_state: ModuleState) -> None:
|
|
98
|
+
"""Update individual module state in table.
|
|
99
|
+
|
|
100
|
+
Updates existing row if module exists, otherwise adds new row.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
module_state: Updated module state.
|
|
104
|
+
"""
|
|
105
|
+
if not self.table:
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
serial_number = module_state.serial_number
|
|
109
|
+
|
|
110
|
+
if serial_number in self._row_keys:
|
|
111
|
+
# Update existing row
|
|
112
|
+
row_key = self._row_keys[serial_number]
|
|
113
|
+
self.table.update_cell(
|
|
114
|
+
row_key, "outputs", self._format_outputs(module_state.outputs)
|
|
115
|
+
)
|
|
116
|
+
self.table.update_cell(
|
|
117
|
+
row_key,
|
|
118
|
+
"report",
|
|
119
|
+
Text(self._format_report(module_state.auto_report), justify="center"),
|
|
120
|
+
)
|
|
121
|
+
self.table.update_cell(row_key, "status", module_state.error_status)
|
|
122
|
+
self.table.update_cell(
|
|
123
|
+
row_key,
|
|
124
|
+
"last_update",
|
|
125
|
+
Text(
|
|
126
|
+
self._format_last_update(module_state.last_update), justify="center"
|
|
127
|
+
),
|
|
128
|
+
)
|
|
129
|
+
else:
|
|
130
|
+
# Add new row
|
|
131
|
+
self._add_module_row(module_state)
|
|
132
|
+
|
|
133
|
+
def _add_module_row(self, module_state: ModuleState) -> None:
|
|
134
|
+
"""Add a module row to the table.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
module_state: Module state to add.
|
|
138
|
+
"""
|
|
139
|
+
if not self.table:
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
row_key = self.table.add_row(
|
|
143
|
+
module_state.name,
|
|
144
|
+
Text(str(module_state.link_number), justify="right"),
|
|
145
|
+
module_state.serial_number,
|
|
146
|
+
module_state.module_type,
|
|
147
|
+
self._format_outputs(module_state.outputs),
|
|
148
|
+
Text(self._format_report(module_state.auto_report), justify="center"),
|
|
149
|
+
module_state.error_status,
|
|
150
|
+
Text(self._format_last_update(module_state.last_update), justify="center"),
|
|
151
|
+
)
|
|
152
|
+
self._row_keys[module_state.serial_number] = row_key
|
|
153
|
+
|
|
154
|
+
def _format_outputs(self, outputs: str) -> str:
|
|
155
|
+
"""Format outputs for display.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
outputs: Raw output string.
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
Formatted output string (empty string for modules without outputs).
|
|
162
|
+
"""
|
|
163
|
+
return outputs
|
|
164
|
+
|
|
165
|
+
def _format_report(self, auto_report: bool) -> str:
|
|
166
|
+
"""Format auto-report status for display.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
auto_report: Auto-report boolean value.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
"Y" if True, "N" if False.
|
|
173
|
+
"""
|
|
174
|
+
return "Y" if auto_report else "N"
|
|
175
|
+
|
|
176
|
+
def _format_last_update(self, last_update: Optional[datetime]) -> str:
|
|
177
|
+
"""Format last update timestamp for display.
|
|
178
|
+
|
|
179
|
+
Shows elapsed time in HH:MM:SS format or "--:--:--" if never updated.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
last_update: Last update timestamp or None.
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
Formatted time string.
|
|
186
|
+
"""
|
|
187
|
+
if last_update is None:
|
|
188
|
+
return "--:--:--"
|
|
189
|
+
|
|
190
|
+
# Calculate elapsed time
|
|
191
|
+
elapsed = datetime.now() - last_update
|
|
192
|
+
total_seconds = int(elapsed.total_seconds())
|
|
193
|
+
|
|
194
|
+
hours = total_seconds // 3600
|
|
195
|
+
minutes = (total_seconds % 3600) // 60
|
|
196
|
+
seconds = total_seconds % 60
|
|
197
|
+
|
|
198
|
+
return f"{hours:02d}:{minutes:02d}:{seconds:02d}"
|
|
199
|
+
|
|
200
|
+
def refresh_last_update_times(self) -> None:
|
|
201
|
+
"""Refresh only the last_update column for all modules.
|
|
202
|
+
|
|
203
|
+
Updates the elapsed time display without querying the service.
|
|
204
|
+
"""
|
|
205
|
+
if not self.table or not self.service:
|
|
206
|
+
return
|
|
207
|
+
|
|
208
|
+
# Update last_update column for each module
|
|
209
|
+
for serial_number, row_key in self._row_keys.items():
|
|
210
|
+
# Get the module state from service
|
|
211
|
+
module_states = self.service.module_states
|
|
212
|
+
module_state = next(
|
|
213
|
+
(m for m in module_states if m.serial_number == serial_number), None
|
|
214
|
+
)
|
|
215
|
+
if module_state:
|
|
216
|
+
# Update only the last_update cell
|
|
217
|
+
self.table.update_cell(
|
|
218
|
+
row_key,
|
|
219
|
+
"last_update",
|
|
220
|
+
Text(
|
|
221
|
+
self._format_last_update(module_state.last_update),
|
|
222
|
+
justify="center",
|
|
223
|
+
),
|
|
224
|
+
)
|
xp/term/widgets/status_footer.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Status Footer Widget for displaying app footer with connection status."""
|
|
2
2
|
|
|
3
|
-
from typing import Any, Optional
|
|
3
|
+
from typing import Any, Optional, Union
|
|
4
4
|
|
|
5
5
|
from textual.app import ComposeResult
|
|
6
6
|
from textual.containers import Horizontal
|
|
@@ -8,6 +8,7 @@ from textual.widgets import Footer, Static
|
|
|
8
8
|
|
|
9
9
|
from xp.models.term.connection_state import ConnectionState
|
|
10
10
|
from xp.services.term.protocol_monitor_service import ProtocolMonitorService
|
|
11
|
+
from xp.services.term.state_monitor_service import StateMonitorService
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
class StatusFooterWidget(Horizontal):
|
|
@@ -17,21 +18,21 @@ class StatusFooterWidget(Horizontal):
|
|
|
17
18
|
the current connection state. Subscribes directly to service signals.
|
|
18
19
|
|
|
19
20
|
Attributes:
|
|
20
|
-
service: ProtocolMonitorService for connection state and status updates.
|
|
21
|
+
service: ProtocolMonitorService or StateMonitorService for connection state and status updates.
|
|
21
22
|
status_widget: Static widget displaying colored status dot.
|
|
22
23
|
status_text_widget: Static widget displaying status messages.
|
|
23
24
|
"""
|
|
24
25
|
|
|
25
26
|
def __init__(
|
|
26
27
|
self,
|
|
27
|
-
service: Optional[ProtocolMonitorService] = None,
|
|
28
|
+
service: Optional[Union[ProtocolMonitorService, StateMonitorService]] = None,
|
|
28
29
|
*args: Any,
|
|
29
30
|
**kwargs: Any,
|
|
30
31
|
) -> None:
|
|
31
32
|
"""Initialize the Status Footer widget.
|
|
32
33
|
|
|
33
34
|
Args:
|
|
34
|
-
service: Optional ProtocolMonitorService for signal subscriptions.
|
|
35
|
+
service: Optional ProtocolMonitorService or StateMonitorService for signal subscriptions.
|
|
35
36
|
args: Additional positional arguments for Horizontal.
|
|
36
37
|
kwargs: Additional keyword arguments for Horizontal.
|
|
37
38
|
"""
|
xp/utils/dependencies.py
CHANGED
|
@@ -75,7 +75,9 @@ from xp.services.telegram.telegram_link_number_service import LinkNumberService
|
|
|
75
75
|
from xp.services.telegram.telegram_output_service import TelegramOutputService
|
|
76
76
|
from xp.services.telegram.telegram_service import TelegramService
|
|
77
77
|
from xp.services.term.protocol_monitor_service import ProtocolMonitorService
|
|
78
|
+
from xp.services.term.state_monitor_service import StateMonitorService
|
|
78
79
|
from xp.term.protocol import ProtocolMonitorApp
|
|
80
|
+
from xp.term.state import StateMonitorApp
|
|
79
81
|
from xp.utils.logging import LoggerService
|
|
80
82
|
|
|
81
83
|
asyncioreactor.install()
|
|
@@ -217,6 +219,24 @@ class ServiceContainer:
|
|
|
217
219
|
scope=punq.Scope.singleton,
|
|
218
220
|
)
|
|
219
221
|
|
|
222
|
+
self.container.register(
|
|
223
|
+
StateMonitorService,
|
|
224
|
+
factory=lambda: StateMonitorService(
|
|
225
|
+
conbus_protocol=self.container.resolve(ConbusEventProtocol),
|
|
226
|
+
conson_config=self.container.resolve(ConsonModuleListConfig),
|
|
227
|
+
telegram_service=self.container.resolve(TelegramService),
|
|
228
|
+
),
|
|
229
|
+
scope=punq.Scope.singleton,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
self.container.register(
|
|
233
|
+
StateMonitorApp,
|
|
234
|
+
factory=lambda: StateMonitorApp(
|
|
235
|
+
state_service=self.container.resolve(StateMonitorService)
|
|
236
|
+
),
|
|
237
|
+
scope=punq.Scope.singleton,
|
|
238
|
+
)
|
|
239
|
+
|
|
220
240
|
self.container.register(
|
|
221
241
|
ConbusEventRawService,
|
|
222
242
|
factory=lambda: ConbusEventRawService(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|