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.
@@ -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
+ )
@@ -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(