conson-xp 1.21.0__py3-none-any.whl → 1.23.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.21.0.dist-info → conson_xp-1.23.0.dist-info}/METADATA +32 -17
- {conson_xp-1.21.0.dist-info → conson_xp-1.23.0.dist-info}/RECORD +17 -13
- xp/__init__.py +1 -1
- xp/cli/commands/term/term_commands.py +1 -1
- xp/models/term/connection_state.py +58 -0
- xp/models/term/status_message.py +16 -0
- xp/services/protocol/conbus_event_protocol.py +19 -6
- xp/term/{app.py → protocol.py} +19 -40
- xp/term/protocol.tcss +1 -1
- xp/term/protocol.yml +14 -14
- xp/term/widgets/__init__.py +6 -0
- xp/term/widgets/help_menu.py +55 -0
- xp/term/widgets/protocol_log.py +37 -84
- xp/term/widgets/status_footer.py +63 -0
- {conson_xp-1.21.0.dist-info → conson_xp-1.23.0.dist-info}/WHEEL +0 -0
- {conson_xp-1.21.0.dist-info → conson_xp-1.23.0.dist-info}/entry_points.txt +0 -0
- {conson_xp-1.21.0.dist-info → conson_xp-1.23.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: conson-xp
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.23.0
|
|
4
4
|
Summary: XP Protocol Communication Tools
|
|
5
5
|
Author-Email: ldvchosal <ldvchosal@github.com>
|
|
6
6
|
License: MIT License
|
|
@@ -60,7 +60,7 @@ Description-Content-Type: text/markdown
|
|
|
60
60
|
[](https://opensource.org/licenses/MIT)
|
|
61
61
|
[](https://mypy-lang.org/)
|
|
62
62
|
|
|
63
|
-
> **A powerful Python CLI
|
|
63
|
+
> **A powerful Python CLI toolkit for CONSON XP Protocol operations**
|
|
64
64
|
|
|
65
65
|
Control and communicate with XP devices through console bus (Conbus), parse telegrams in real-time, and integrate with smart home systems like Apple HomeKit.
|
|
66
66
|
|
|
@@ -81,7 +81,7 @@ Bridge XP devices to Apple HomeKit for seamless smart home control
|
|
|
81
81
|
Automatically discover XP servers and scan connected modules on your network
|
|
82
82
|
|
|
83
83
|
⚡ **Modern Architecture**
|
|
84
|
-
|
|
84
|
+
Comprehensive type safety and robust error handling
|
|
85
85
|
|
|
86
86
|
---
|
|
87
87
|
|
|
@@ -96,9 +96,6 @@ xp telegram parse "<E14L00I02MAK>"
|
|
|
96
96
|
|
|
97
97
|
# Discover XP servers on your network
|
|
98
98
|
xp conbus discover
|
|
99
|
-
|
|
100
|
-
# Start the REST API server
|
|
101
|
-
xp api start
|
|
102
99
|
```
|
|
103
100
|
|
|
104
101
|
## 📦 Installation
|
|
@@ -167,7 +164,32 @@ xp module search "push button"
|
|
|
167
164
|
xp module list --group-by-category
|
|
168
165
|
```
|
|
169
166
|
|
|
167
|
+
### 🖥️ Terminal UI (TUI)
|
|
168
|
+
|
|
169
|
+
**Real-time Protocol Monitor**
|
|
170
|
+
|
|
171
|
+
Launch an interactive terminal interface for live protocol monitoring and control:
|
|
170
172
|
|
|
173
|
+
```bash
|
|
174
|
+
# Start the protocol monitor TUI
|
|
175
|
+
xp term protocol
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**Features:**
|
|
179
|
+
- 📊 **Live Telegram Stream**: Real-time RX/TX telegram monitoring from Conbus server
|
|
180
|
+
- ⌨️ **Keyboard Shortcuts**: Quick access controls for common operations
|
|
181
|
+
- `Q` - Quit application
|
|
182
|
+
- `C` - Toggle connection (connect/disconnect)
|
|
183
|
+
- `R` - Reset and clear log
|
|
184
|
+
- `0-9, a-q` - Send predefined protocol telegrams
|
|
185
|
+
- 🎨 **Visual Status Indicators**: Color-coded connection states
|
|
186
|
+
- 🟢 Green - Connected
|
|
187
|
+
- 🟡 Yellow - Connecting/Disconnecting
|
|
188
|
+
- 🔴 Red - Failed
|
|
189
|
+
- ⚪ White - Disconnected
|
|
190
|
+
- 📝 **Interactive Display**: Scrollable telegram log with detailed parsing information
|
|
191
|
+
|
|
192
|
+
The TUI provides a convenient way to monitor and interact with XP devices without juggling multiple terminal commands.
|
|
171
193
|
|
|
172
194
|
### 🔧 Advanced Features
|
|
173
195
|
|
|
@@ -201,13 +223,7 @@ xp checksum calculate "E14L00I02M" --algorithm crc32
|
|
|
201
223
|
```
|
|
202
224
|
</details>
|
|
203
225
|
|
|
204
|
-
### 🌐
|
|
205
|
-
|
|
206
|
-
**REST API Server**
|
|
207
|
-
```bash
|
|
208
|
-
# Start API server with interactive docs at /docs
|
|
209
|
-
xp api start
|
|
210
|
-
```
|
|
226
|
+
### 🌐 Integration
|
|
211
227
|
|
|
212
228
|
**HomeKit Smart Home Bridge**
|
|
213
229
|
```bash
|
|
@@ -232,7 +248,7 @@ xp reverse-proxy start
|
|
|
232
248
|
|
|
233
249
|
**Layered Design**
|
|
234
250
|
```
|
|
235
|
-
CLI Layer →
|
|
251
|
+
CLI Layer → Services → Models → Connection Layer
|
|
236
252
|
```
|
|
237
253
|
|
|
238
254
|
**Key Components**: Telegram processing • Real-time Conbus communication • HomeKit bridge • Multiple XP server support • Configuration management
|
|
@@ -256,9 +272,8 @@ pdm run check
|
|
|
256
272
|
<details>
|
|
257
273
|
<summary><b>Project Structure</b></summary>
|
|
258
274
|
|
|
259
|
-
```
|
|
275
|
+
```
|
|
260
276
|
src/xp/
|
|
261
|
-
├── api/ # FastAPI REST endpoints
|
|
262
277
|
├── cli/ # Command-line interface
|
|
263
278
|
├── models/ # Core data models
|
|
264
279
|
├── services/ # Business logic
|
|
@@ -407,7 +422,7 @@ xp term protocol
|
|
|
407
422
|
```
|
|
408
423
|
</details>
|
|
409
424
|
|
|
410
|
-
**Requirements**: Python 3.10+ •
|
|
425
|
+
**Requirements**: Python 3.10+ • Pydantic • Click • HAP-python
|
|
411
426
|
|
|
412
427
|
## License
|
|
413
428
|
|
|
@@ -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.23.0.dist-info/METADATA,sha256=z9DdbWCsagoeEX32rY76lsao7924etwx1cYvdWevwSg,10298
|
|
2
|
+
conson_xp-1.23.0.dist-info/WHEEL,sha256=tsUv_t7BDeJeRHaSrczbGeuK-TtDpGsWi_JfpzD255I,90
|
|
3
|
+
conson_xp-1.23.0.dist-info/entry_points.txt,sha256=1OcdIcDM1hz3ljCXgybaPUh1IOFEwkaTgLIW9u9zGeg,50
|
|
4
|
+
conson_xp-1.23.0.dist-info/licenses/LICENSE,sha256=rxj6woMM-r3YCyGq_UHFtbh7kHTAJgrccH6O-33IDE4,1419
|
|
5
|
+
xp/__init__.py,sha256=a3NpXD1LnawKeJVM7klz-0Z1EzpYFllOS4ifHJUbU_4,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
|
|
@@ -42,7 +42,7 @@ xp/cli/commands/telegram/telegram_parse_commands.py,sha256=_OYOso1hS4f_ox96qlkYL
|
|
|
42
42
|
xp/cli/commands/telegram/telegram_version_commands.py,sha256=WQyx1-B9yJ8V9WrFyBpOvULJ-jq12GoZZDDoRbM7eyw,1553
|
|
43
43
|
xp/cli/commands/term/__init__.py,sha256=1NNST_8YJfj5LCujQISwQflK6LyEn7mDmZpMpvI9d-o,116
|
|
44
44
|
xp/cli/commands/term/term.py,sha256=gjvsv2OE-F_KNWQrWi04fXQ5cGo0l8P-Ortbb5KTA-A,309
|
|
45
|
-
xp/cli/commands/term/term_commands.py,sha256=
|
|
45
|
+
xp/cli/commands/term/term_commands.py,sha256=ccBdvvyxjh2-cptmI9ohVIa02OOfG0dzO1JFb4KTowQ,744
|
|
46
46
|
xp/cli/main.py,sha256=ap5jU0DrSnrCKDKqGXcz9N-sngZodyyN-5ReWE8Fh1s,1817
|
|
47
47
|
xp/cli/utils/__init__.py,sha256=gTGIj60Uai0iE7sr9_TtEpl04fD7krtTzbbigXUsUVU,46
|
|
48
48
|
xp/cli/utils/click_tree.py,sha256=ilmM2IMa_c-TqUMsv2alrZXuS0BNhvVlrBlSfyN8lzM,1670
|
|
@@ -105,7 +105,9 @@ xp/models/telegram/telegram.py,sha256=IJUxHX6ftLcET9C1pjvLhUO5Db5JO6W7rUItzdEW30
|
|
|
105
105
|
xp/models/telegram/telegram_type.py,sha256=GhqKP63oNMyh2tIvCPcsC5RFp4s4JjhmEqCLCC-8XMk,423
|
|
106
106
|
xp/models/telegram/timeparam_type.py,sha256=Ar8xvSfPmOAgR2g2Je0FgvP01SL7bPvZn5_HrVDpmJM,1137
|
|
107
107
|
xp/models/term/__init__.py,sha256=c1AMtVitYk80o9K_zWjYNzZYpFDASqM8S1Djm1PD4Qo,192
|
|
108
|
+
xp/models/term/connection_state.py,sha256=floDRMeMcfgMrYIVsyoVHBXHtxd3hqm-xOdr3oXtaHY,1793
|
|
108
109
|
xp/models/term/protocol_keys_config.py,sha256=CTujcfI2_NOeltjvHy_cnsHzxLSVsGFXieMZlD-zj0Q,1204
|
|
110
|
+
xp/models/term/status_message.py,sha256=DOmzL0dbig5mP1UEoXdgzGT4UG2RyAXa_yRVo5c4x8w,394
|
|
109
111
|
xp/models/write_config_type.py,sha256=T2RaO52RpzoJ4782uMHE-fX7Ymx3CaIQAEwByydXq1M,881
|
|
110
112
|
xp/services/__init__.py,sha256=W9YZyrkh7vm--ZHhAXNQiOYQs5yhhmUHXP5I0Lf1XBg,782
|
|
111
113
|
xp/services/actiontable/__init__.py,sha256=z6js4EuJ6xKHaseTEhuEvKo1tr9K1XyQiruReJtBiPY,26
|
|
@@ -151,7 +153,7 @@ xp/services/homekit/homekit_service.py,sha256=0lW-hg40ETco3gDBEYkR_sX-UIYsLSKCD4
|
|
|
151
153
|
xp/services/log_file_service.py,sha256=fvPcZQj8XOKUA-4ep5R8n0PelvwvRlTLlVxvIWM5KR4,10506
|
|
152
154
|
xp/services/module_type_service.py,sha256=xWhr1EAZMykL5uNWHWdpa5T8yNruGKH43XRTOS8GwZg,7477
|
|
153
155
|
xp/services/protocol/__init__.py,sha256=qRufBmqRKGzpuzZ5bxBbmwf510TT00Ke8s5HcWGnqRY,818
|
|
154
|
-
xp/services/protocol/conbus_event_protocol.py,sha256=
|
|
156
|
+
xp/services/protocol/conbus_event_protocol.py,sha256=h9ZdnN9CWSBXQqE-M9qmPPbMT3r25cxzXuJsvURp1WQ,14390
|
|
155
157
|
xp/services/protocol/conbus_protocol.py,sha256=JO7yLkD_ohPT0ETjnAIx4CGpZyobf4LdbuozM_43btE,10276
|
|
156
158
|
xp/services/protocol/protocol_factory.py,sha256=PmjN9AtW9sxNo3voqUiNgQA-pTvX1RW4XXFlHKfFr5Q,2470
|
|
157
159
|
xp/services/protocol/telegram_protocol.py,sha256=Ki5DrXsKxiaqLcdP9WWUuhUI7cPu2DfwyZkh-Gv9Lb8,9496
|
|
@@ -176,11 +178,13 @@ xp/services/telegram/telegram_output_service.py,sha256=UaUv_14fR8o5K2PxQBXrCzx-H
|
|
|
176
178
|
xp/services/telegram/telegram_service.py,sha256=XrP1CPi0ckxoKBaNwLA6lo-TogWxXgmXDOsU4Xl8BlY,13237
|
|
177
179
|
xp/services/telegram/telegram_version_service.py,sha256=M5HdOTsLdcwo122FP-jW6R740ktLrtKf2TiMDVz23h8,10528
|
|
178
180
|
xp/term/__init__.py,sha256=Xg2DhBeI3xQJLfc7_BPWI1por-rUXemyer5OtOt9Cus,51
|
|
179
|
-
xp/term/
|
|
180
|
-
xp/term/protocol.tcss,sha256=
|
|
181
|
-
xp/term/protocol.yml,sha256=
|
|
182
|
-
xp/term/widgets/__init__.py,sha256=
|
|
183
|
-
xp/term/widgets/
|
|
181
|
+
xp/term/protocol.py,sha256=acYYsv7_4z5ePrnslSC1exKzqbKOE5ZGds4J33Q2XNs,4784
|
|
182
|
+
xp/term/protocol.tcss,sha256=r_KfxrbpycGHLVXqZc6INBBcUJME0hLrAZkF1oqnab4,2126
|
|
183
|
+
xp/term/protocol.yml,sha256=kiTe_QSMPmLvLA0ZyIhNaDPwBdi6khh5C1NSR7I9TN0,2124
|
|
184
|
+
xp/term/widgets/__init__.py,sha256=ftWmN_fmjxy2E8Qfm-YSRmzKfgL0KTBCTpgvYWCPbUY,274
|
|
185
|
+
xp/term/widgets/help_menu.py,sha256=bdT5AYRdtKt_tvZTVbG7-DPMb1mj78kggtjjsa-95BA,1780
|
|
186
|
+
xp/term/widgets/protocol_log.py,sha256=OeVg9VCvIl9HXm6j_HPAGOIAYxO0MxlKcS9QmBEOKx4,13018
|
|
187
|
+
xp/term/widgets/status_footer.py,sha256=eRZHkrG5aZCMulibX56KfFyGHS8IgL-7psvr9f9S6FI,1992
|
|
184
188
|
xp/utils/__init__.py,sha256=_avMF_UOkfR3tNeDIPqQ5odmbq5raKkaq1rZ9Cn1CJs,332
|
|
185
189
|
xp/utils/checksum.py,sha256=HDpiQxmdIedbCbZ4o_Box0i_Zig417BtCV_46ZyhiTk,1711
|
|
186
190
|
xp/utils/dependencies.py,sha256=ECS6p0eXzocM5INLwJeckHXn_Dim18uOjXTJ29qQvkQ,22001
|
|
@@ -189,4 +193,4 @@ xp/utils/logging.py,sha256=rZDXwlBrYK8A6MPq5StsMNpgsRowzJXM6fvROPwJdGM,3750
|
|
|
189
193
|
xp/utils/serialization.py,sha256=RWHHk86feaB4ZP7rjE4qOWK0900yg2joUBDkP76gfOY,4618
|
|
190
194
|
xp/utils/state_machine.py,sha256=Oe2sLwCh9z_vr1tF6X0ZRGTeuckRQAGzmef7xc9CNdc,2413
|
|
191
195
|
xp/utils/time_utils.py,sha256=dEyViDlAG9GWU-J3D_YVa-sGma6yiyyMTgN4h2x3PY4,3781
|
|
192
|
-
conson_xp-1.
|
|
196
|
+
conson_xp-1.23.0.dist-info/RECORD,,
|
xp/__init__.py
CHANGED
|
@@ -21,7 +21,7 @@ def protocol_monitor(ctx: Context) -> None:
|
|
|
21
21
|
\b
|
|
22
22
|
xp term protocol
|
|
23
23
|
"""
|
|
24
|
-
from xp.term.
|
|
24
|
+
from xp.term.protocol import ProtocolMonitorApp
|
|
25
25
|
|
|
26
26
|
# Resolve ServiceContainer from context
|
|
27
27
|
container = ctx.obj.get("container").get_container()
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Connection state management module."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
from xp.utils.state_machine import StateMachine
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ConnectionState(str, Enum):
|
|
9
|
+
"""Connection state enumeration.
|
|
10
|
+
|
|
11
|
+
Attributes:
|
|
12
|
+
DISCONNECTING: Disconnecting to server.
|
|
13
|
+
DISCONNECTED: Not connected to server.
|
|
14
|
+
CONNECTING: Connection in progress.
|
|
15
|
+
CONNECTED: Successfully connected.
|
|
16
|
+
FAILED: Connection failed.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
DISCONNECTING = "DISCONNECTING"
|
|
20
|
+
DISCONNECTED = "DISCONNECTED"
|
|
21
|
+
CONNECTING = "CONNECTING"
|
|
22
|
+
CONNECTED = "CONNECTED"
|
|
23
|
+
FAILED = "FAILED"
|
|
24
|
+
|
|
25
|
+
@staticmethod
|
|
26
|
+
def create_state_machine() -> StateMachine:
|
|
27
|
+
"""Create and configure state machine for connection management.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Configured StateMachine with connection state transitions.
|
|
31
|
+
"""
|
|
32
|
+
sm = StateMachine(ConnectionState.DISCONNECTED)
|
|
33
|
+
|
|
34
|
+
# Define valid transitions
|
|
35
|
+
sm.define_transition(
|
|
36
|
+
"connect", {ConnectionState.DISCONNECTED, ConnectionState.FAILED}
|
|
37
|
+
)
|
|
38
|
+
sm.define_transition(
|
|
39
|
+
"disconnect", {ConnectionState.CONNECTED, ConnectionState.CONNECTING}
|
|
40
|
+
)
|
|
41
|
+
sm.define_transition(
|
|
42
|
+
"connecting", {ConnectionState.DISCONNECTED, ConnectionState.FAILED}
|
|
43
|
+
)
|
|
44
|
+
sm.define_transition("connected", {ConnectionState.CONNECTING})
|
|
45
|
+
sm.define_transition(
|
|
46
|
+
"disconnecting", {ConnectionState.CONNECTED, ConnectionState.CONNECTING}
|
|
47
|
+
)
|
|
48
|
+
sm.define_transition("disconnected", {ConnectionState.DISCONNECTING})
|
|
49
|
+
sm.define_transition(
|
|
50
|
+
"failed",
|
|
51
|
+
{
|
|
52
|
+
ConnectionState.CONNECTING,
|
|
53
|
+
ConnectionState.CONNECTED,
|
|
54
|
+
ConnectionState.DISCONNECTING,
|
|
55
|
+
},
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
return sm
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Status message models for terminal UI."""
|
|
2
|
+
|
|
3
|
+
from textual.message import Message
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class StatusMessageChanged(Message):
|
|
7
|
+
"""Message posted when status message changes."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, message: str) -> None:
|
|
10
|
+
"""Initialize the message.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
message: The status message to display.
|
|
14
|
+
"""
|
|
15
|
+
super().__init__()
|
|
16
|
+
self.message = message
|
|
@@ -17,7 +17,7 @@ from twisted.internet.interfaces import IAddress, IConnector
|
|
|
17
17
|
from twisted.internet.posixbase import PosixReactorBase
|
|
18
18
|
from twisted.python.failure import Failure
|
|
19
19
|
|
|
20
|
-
from xp.models import ConbusClientConfig
|
|
20
|
+
from xp.models import ConbusClientConfig, ModuleTypeCode
|
|
21
21
|
from xp.models.protocol.conbus_protocol import (
|
|
22
22
|
TelegramReceivedEvent,
|
|
23
23
|
)
|
|
@@ -168,9 +168,6 @@ class ConbusEventProtocol(protocol.Protocol, protocol.ClientFactory):
|
|
|
168
168
|
|
|
169
169
|
Args:
|
|
170
170
|
data: Raw telegram payload (without checksum/framing).
|
|
171
|
-
|
|
172
|
-
Raises:
|
|
173
|
-
IOError: If transport is not open.
|
|
174
171
|
"""
|
|
175
172
|
self.on_send_frame.emit(data)
|
|
176
173
|
|
|
@@ -180,8 +177,9 @@ class ConbusEventProtocol(protocol.Protocol, protocol.ClientFactory):
|
|
|
180
177
|
frame = b"<" + frame_data.encode() + b">"
|
|
181
178
|
|
|
182
179
|
if not self.transport:
|
|
183
|
-
self.logger.info("Invalid transport")
|
|
184
|
-
|
|
180
|
+
self.logger.info("Invalid transport, connection closed.")
|
|
181
|
+
self.on_connection_failed.emit(Failure("Invalid transport."))
|
|
182
|
+
return
|
|
185
183
|
|
|
186
184
|
self.logger.debug(f"Sending frame: {frame.decode()}")
|
|
187
185
|
self.transport.write(frame) # type: ignore
|
|
@@ -211,6 +209,21 @@ class ConbusEventProtocol(protocol.Protocol, protocol.ClientFactory):
|
|
|
211
209
|
)
|
|
212
210
|
self.send_raw_telegram(payload)
|
|
213
211
|
|
|
212
|
+
def send_event_telegram(
|
|
213
|
+
self, module_type_code: ModuleTypeCode, link_number: int, input_number: int
|
|
214
|
+
) -> None:
|
|
215
|
+
"""Send telegram with specified parameters.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
module_type_code: Type code of module.
|
|
219
|
+
link_number: Link number.
|
|
220
|
+
input_number: Input number.
|
|
221
|
+
"""
|
|
222
|
+
payload = (
|
|
223
|
+
f"E" f"{module_type_code}" f"L{link_number:02d}" f"I{input_number:02d}"
|
|
224
|
+
)
|
|
225
|
+
self.send_raw_telegram(payload)
|
|
226
|
+
|
|
214
227
|
def send_raw_telegram(self, payload: str) -> None:
|
|
215
228
|
"""Send telegram with specified parameters.
|
|
216
229
|
|
xp/term/{app.py → protocol.py}
RENAMED
|
@@ -4,11 +4,13 @@ from pathlib import Path
|
|
|
4
4
|
from typing import Any, Optional
|
|
5
5
|
|
|
6
6
|
from textual.app import App, ComposeResult
|
|
7
|
-
from textual.containers import Horizontal
|
|
8
|
-
from textual.widgets import DataTable, Footer, Static
|
|
7
|
+
from textual.containers import Horizontal
|
|
9
8
|
|
|
10
9
|
from xp.models.term import ProtocolKeysConfig
|
|
10
|
+
from xp.models.term.status_message import StatusMessageChanged
|
|
11
|
+
from xp.term.widgets.help_menu import HelpMenuWidget
|
|
11
12
|
from xp.term.widgets.protocol_log import ProtocolLogWidget
|
|
13
|
+
from xp.term.widgets.status_footer import StatusFooterWidget
|
|
12
14
|
|
|
13
15
|
|
|
14
16
|
class ProtocolMonitorApp(App[None]):
|
|
@@ -45,9 +47,8 @@ class ProtocolMonitorApp(App[None]):
|
|
|
45
47
|
super().__init__()
|
|
46
48
|
self.container = container
|
|
47
49
|
self.protocol_widget: Optional[ProtocolLogWidget] = None
|
|
48
|
-
self.
|
|
49
|
-
self.
|
|
50
|
-
self.help_table: Optional[DataTable] = None
|
|
50
|
+
self.help_menu: Optional[HelpMenuWidget] = None
|
|
51
|
+
self.footer_widget: Optional[StatusFooterWidget] = None
|
|
51
52
|
self.protocol_keys = self._load_protocol_keys()
|
|
52
53
|
|
|
53
54
|
def _load_protocol_keys(self) -> ProtocolKeysConfig:
|
|
@@ -70,18 +71,13 @@ class ProtocolMonitorApp(App[None]):
|
|
|
70
71
|
yield self.protocol_widget
|
|
71
72
|
|
|
72
73
|
# Help menu (hidden by default)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
with Horizontal(id="footer-container"):
|
|
82
|
-
yield Footer()
|
|
83
|
-
self.status_widget = Static("○", id="status-line")
|
|
84
|
-
yield self.status_widget
|
|
74
|
+
self.help_menu = HelpMenuWidget(
|
|
75
|
+
protocol_keys=self.protocol_keys, id="help-menu"
|
|
76
|
+
)
|
|
77
|
+
yield self.help_menu
|
|
78
|
+
|
|
79
|
+
self.footer_widget = StatusFooterWidget(id="footer-container")
|
|
80
|
+
yield self.footer_widget
|
|
85
81
|
|
|
86
82
|
def action_toggle_connection(self) -> None:
|
|
87
83
|
"""Toggle connection on 'c' key press.
|
|
@@ -122,37 +118,20 @@ class ProtocolMonitorApp(App[None]):
|
|
|
122
118
|
self._update_status,
|
|
123
119
|
)
|
|
124
120
|
|
|
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
121
|
def _update_status(self, state: Any) -> None:
|
|
132
122
|
"""Update status line with connection state.
|
|
133
123
|
|
|
134
124
|
Args:
|
|
135
125
|
state: Current connection state.
|
|
136
126
|
"""
|
|
137
|
-
if self.
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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:
|
|
127
|
+
if self.footer_widget:
|
|
128
|
+
self.footer_widget.update_status(state)
|
|
129
|
+
|
|
130
|
+
def on_status_message_changed(self, message: StatusMessageChanged) -> None:
|
|
152
131
|
"""Handle status message changes from protocol widget.
|
|
153
132
|
|
|
154
133
|
Args:
|
|
155
134
|
message: Message containing the status text.
|
|
156
135
|
"""
|
|
157
|
-
if self.
|
|
158
|
-
self.
|
|
136
|
+
if self.footer_widget:
|
|
137
|
+
self.footer_widget.update_message(message.message)
|
xp/term/protocol.tcss
CHANGED
xp/term/protocol.yml
CHANGED
|
@@ -55,85 +55,85 @@ protocol:
|
|
|
55
55
|
- E02L00I08B
|
|
56
56
|
|
|
57
57
|
"d":
|
|
58
|
-
name: "
|
|
58
|
+
name: "Link 1 On"
|
|
59
59
|
telegrams:
|
|
60
60
|
- E02L01I08M
|
|
61
61
|
- E02L01I08B
|
|
62
62
|
|
|
63
63
|
"e":
|
|
64
|
-
name: "
|
|
64
|
+
name: "Link 1 Off"
|
|
65
65
|
telegrams:
|
|
66
66
|
- E02L01I00M
|
|
67
67
|
- E02L01I00B
|
|
68
68
|
|
|
69
69
|
"f":
|
|
70
|
-
name: "
|
|
70
|
+
name: "Link 2 On"
|
|
71
71
|
telegrams:
|
|
72
72
|
- E02L02I08M
|
|
73
73
|
- E02L02I08B
|
|
74
74
|
|
|
75
75
|
"g":
|
|
76
|
-
name: "
|
|
76
|
+
name: "Link 2 Off"
|
|
77
77
|
telegrams:
|
|
78
78
|
- E02L02I00M
|
|
79
79
|
- E02L02I00B
|
|
80
80
|
|
|
81
81
|
"h":
|
|
82
|
-
name: "
|
|
82
|
+
name: "Link 3 On"
|
|
83
83
|
telegrams:
|
|
84
84
|
- E02L03I08M
|
|
85
85
|
- E02L03I08B
|
|
86
86
|
|
|
87
87
|
"i":
|
|
88
|
-
name: "
|
|
88
|
+
name: "Link 3 Off"
|
|
89
89
|
telegrams:
|
|
90
90
|
- E02L03I00M
|
|
91
91
|
- E02L03I00B
|
|
92
92
|
|
|
93
93
|
"j":
|
|
94
|
-
name: "
|
|
94
|
+
name: "Link 4 On"
|
|
95
95
|
telegrams:
|
|
96
96
|
- E02L04I08M
|
|
97
97
|
- E02L04I08B
|
|
98
98
|
|
|
99
99
|
"k":
|
|
100
|
-
name: "
|
|
100
|
+
name: "Link 4 Off"
|
|
101
101
|
telegrams:
|
|
102
102
|
- E02L04I00M
|
|
103
103
|
- E02L04I00B
|
|
104
104
|
|
|
105
105
|
"l":
|
|
106
|
-
name: "
|
|
106
|
+
name: "Link 5 On"
|
|
107
107
|
telegrams:
|
|
108
108
|
- E02L05I08M
|
|
109
109
|
- E02L05I08B
|
|
110
110
|
|
|
111
111
|
"m":
|
|
112
|
-
name: "
|
|
112
|
+
name: "Link 5 Off"
|
|
113
113
|
telegrams:
|
|
114
114
|
- E02L05I00M
|
|
115
115
|
- E02L05I00B
|
|
116
116
|
|
|
117
117
|
"n":
|
|
118
|
-
name: "
|
|
118
|
+
name: "Link 6 On"
|
|
119
119
|
telegrams:
|
|
120
120
|
- E02L06I08M
|
|
121
121
|
- E02L06I08B
|
|
122
122
|
|
|
123
123
|
"o":
|
|
124
|
-
name: "
|
|
124
|
+
name: "Link 6 Off"
|
|
125
125
|
telegrams:
|
|
126
126
|
- E02L06I00M
|
|
127
127
|
- E02L06I00B
|
|
128
128
|
|
|
129
129
|
"p":
|
|
130
|
-
name: "
|
|
130
|
+
name: "Link 7 On"
|
|
131
131
|
telegrams:
|
|
132
132
|
- E02L07I08M
|
|
133
133
|
- E02L07I08B
|
|
134
134
|
|
|
135
135
|
"q":
|
|
136
|
-
name: "
|
|
136
|
+
name: "Link 7 Off"
|
|
137
137
|
telegrams:
|
|
138
138
|
- E02L07I00M
|
|
139
139
|
- E02L07I00B
|
xp/term/widgets/__init__.py
CHANGED
|
@@ -1 +1,7 @@
|
|
|
1
1
|
"""TUI widgets package."""
|
|
2
|
+
|
|
3
|
+
from xp.term.widgets.help_menu import HelpMenuWidget
|
|
4
|
+
from xp.term.widgets.protocol_log import ProtocolLogWidget
|
|
5
|
+
from xp.term.widgets.status_footer import StatusFooterWidget
|
|
6
|
+
|
|
7
|
+
__all__ = ["HelpMenuWidget", "ProtocolLogWidget", "StatusFooterWidget"]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Help Menu Widget for displaying keyboard shortcuts and protocol keys."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from textual.app import ComposeResult
|
|
6
|
+
from textual.containers import Vertical
|
|
7
|
+
from textual.widgets import DataTable
|
|
8
|
+
|
|
9
|
+
from xp.models.term import ProtocolKeysConfig
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class HelpMenuWidget(Vertical):
|
|
13
|
+
"""Help menu widget displaying keyboard shortcuts and protocol keys.
|
|
14
|
+
|
|
15
|
+
Displays a table of available keyboard shortcuts mapped to their
|
|
16
|
+
corresponding protocol commands.
|
|
17
|
+
|
|
18
|
+
Attributes:
|
|
19
|
+
protocol_keys: Configuration of protocol keys and their telegrams.
|
|
20
|
+
help_table: DataTable widget for displaying key mappings.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
protocol_keys: ProtocolKeysConfig,
|
|
26
|
+
*args: Any,
|
|
27
|
+
**kwargs: Any,
|
|
28
|
+
) -> None:
|
|
29
|
+
"""Initialize the Help Menu widget.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
protocol_keys: Configuration containing protocol key mappings.
|
|
33
|
+
args: Additional positional arguments for Vertical.
|
|
34
|
+
kwargs: Additional keyword arguments for Vertical.
|
|
35
|
+
"""
|
|
36
|
+
super().__init__(*args, **kwargs)
|
|
37
|
+
self.protocol_keys = protocol_keys
|
|
38
|
+
self.help_table: DataTable = DataTable(id="help-table", show_header=False)
|
|
39
|
+
self.help_table.can_focus = False
|
|
40
|
+
self.border_title = "Help menu"
|
|
41
|
+
self.can_focus = False
|
|
42
|
+
|
|
43
|
+
def compose(self) -> ComposeResult:
|
|
44
|
+
"""Compose the help menu layout.
|
|
45
|
+
|
|
46
|
+
Yields:
|
|
47
|
+
DataTable widget with key mappings.
|
|
48
|
+
"""
|
|
49
|
+
yield self.help_table
|
|
50
|
+
|
|
51
|
+
def on_mount(self) -> None:
|
|
52
|
+
"""Populate help table when widget mounts."""
|
|
53
|
+
self.help_table.add_columns("Key", "Command")
|
|
54
|
+
for key, config in self.protocol_keys.protocol.items():
|
|
55
|
+
self.help_table.add_row(key, config.name)
|
xp/term/widgets/protocol_log.py
CHANGED
|
@@ -2,71 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import logging
|
|
5
|
-
from enum import Enum
|
|
6
5
|
from typing import Any, Optional
|
|
7
6
|
|
|
8
|
-
from textual.message import Message
|
|
9
7
|
from textual.reactive import reactive
|
|
10
8
|
from textual.widget import Widget
|
|
11
9
|
from textual.widgets import RichLog
|
|
10
|
+
from twisted.python.failure import Failure
|
|
12
11
|
|
|
13
12
|
from xp.models.protocol.conbus_protocol import TelegramReceivedEvent
|
|
13
|
+
from xp.models.term.connection_state import ConnectionState
|
|
14
|
+
from xp.models.term.status_message import StatusMessageChanged
|
|
14
15
|
from xp.services.conbus.conbus_receive_service import ConbusReceiveService
|
|
15
16
|
from xp.services.protocol import ConbusEventProtocol
|
|
16
|
-
from xp.utils.state_machine import StateMachine
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class ConnectionState(str, Enum):
|
|
20
|
-
"""Connection state enumeration.
|
|
21
|
-
|
|
22
|
-
Attributes:
|
|
23
|
-
DISCONNECTING: Disconnecting to server.
|
|
24
|
-
DISCONNECTED: Not connected to server.
|
|
25
|
-
CONNECTING: Connection in progress.
|
|
26
|
-
CONNECTED: Successfully connected.
|
|
27
|
-
FAILED: Connection failed.
|
|
28
|
-
"""
|
|
29
|
-
|
|
30
|
-
DISCONNECTING = "DISCONNECTING"
|
|
31
|
-
DISCONNECTED = "DISCONNECTED"
|
|
32
|
-
CONNECTING = "CONNECTING"
|
|
33
|
-
CONNECTED = "CONNECTED"
|
|
34
|
-
FAILED = "FAILED"
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def create_connection_state_machine() -> StateMachine:
|
|
38
|
-
"""Create and configure state machine for connection management.
|
|
39
|
-
|
|
40
|
-
Returns:
|
|
41
|
-
Configured StateMachine with connection state transitions.
|
|
42
|
-
"""
|
|
43
|
-
sm = StateMachine(ConnectionState.DISCONNECTED)
|
|
44
|
-
|
|
45
|
-
# Define valid transitions
|
|
46
|
-
sm.define_transition(
|
|
47
|
-
"connect", {ConnectionState.DISCONNECTED, ConnectionState.FAILED}
|
|
48
|
-
)
|
|
49
|
-
sm.define_transition(
|
|
50
|
-
"disconnect", {ConnectionState.CONNECTED, ConnectionState.CONNECTING}
|
|
51
|
-
)
|
|
52
|
-
sm.define_transition(
|
|
53
|
-
"connecting", {ConnectionState.DISCONNECTED, ConnectionState.FAILED}
|
|
54
|
-
)
|
|
55
|
-
sm.define_transition("connected", {ConnectionState.CONNECTING})
|
|
56
|
-
sm.define_transition(
|
|
57
|
-
"disconnecting", {ConnectionState.CONNECTED, ConnectionState.CONNECTING}
|
|
58
|
-
)
|
|
59
|
-
sm.define_transition("disconnected", {ConnectionState.DISCONNECTING})
|
|
60
|
-
sm.define_transition(
|
|
61
|
-
"failed",
|
|
62
|
-
{
|
|
63
|
-
ConnectionState.CONNECTING,
|
|
64
|
-
ConnectionState.CONNECTED,
|
|
65
|
-
ConnectionState.DISCONNECTING,
|
|
66
|
-
},
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
return sm
|
|
70
17
|
|
|
71
18
|
|
|
72
19
|
class ProtocolLogWidget(Widget):
|
|
@@ -84,18 +31,6 @@ class ProtocolLogWidget(Widget):
|
|
|
84
31
|
log_widget: RichLog widget for displaying messages.
|
|
85
32
|
"""
|
|
86
33
|
|
|
87
|
-
class StatusMessageChanged(Message):
|
|
88
|
-
"""Message posted when status message changes."""
|
|
89
|
-
|
|
90
|
-
def __init__(self, message: str) -> None:
|
|
91
|
-
"""Initialize the message.
|
|
92
|
-
|
|
93
|
-
Args:
|
|
94
|
-
message: The status message to display.
|
|
95
|
-
"""
|
|
96
|
-
super().__init__()
|
|
97
|
-
self.message = message
|
|
98
|
-
|
|
99
34
|
connection_state = reactive(ConnectionState.DISCONNECTED)
|
|
100
35
|
|
|
101
36
|
def __init__(self, container: Any) -> None:
|
|
@@ -111,7 +46,7 @@ class ProtocolLogWidget(Widget):
|
|
|
111
46
|
self.service: Optional[ConbusReceiveService] = None
|
|
112
47
|
self.logger = logging.getLogger(__name__)
|
|
113
48
|
self.log_widget: Optional[RichLog] = None
|
|
114
|
-
self._state_machine =
|
|
49
|
+
self._state_machine = ConnectionState.create_state_machine()
|
|
115
50
|
|
|
116
51
|
def compose(self) -> Any:
|
|
117
52
|
"""Compose the widget layout.
|
|
@@ -134,6 +69,7 @@ class ProtocolLogWidget(Widget):
|
|
|
134
69
|
|
|
135
70
|
# Connect psygnal signals
|
|
136
71
|
self.protocol.on_connection_made.connect(self._on_connection_made)
|
|
72
|
+
self.protocol.on_connection_failed.connect(self._on_connection_failed)
|
|
137
73
|
self.protocol.on_telegram_received.connect(self._on_telegram_received)
|
|
138
74
|
self.protocol.on_telegram_sent.connect(self._on_telegram_sent)
|
|
139
75
|
self.protocol.on_timeout.connect(self._on_timeout)
|
|
@@ -169,10 +105,8 @@ class ProtocolLogWidget(Widget):
|
|
|
169
105
|
# Transition to CONNECTING
|
|
170
106
|
if self._state_machine.transition("connecting", ConnectionState.CONNECTING):
|
|
171
107
|
self.connection_state = ConnectionState.CONNECTING
|
|
172
|
-
self.
|
|
173
|
-
self.
|
|
174
|
-
f"Connecting to {self.protocol.cli_config.ip}:{self.protocol.cli_config.port}..."
|
|
175
|
-
)
|
|
108
|
+
self.post_status(
|
|
109
|
+
f"Connecting to {self.protocol.cli_config.ip}:{self.protocol.cli_config.port}..."
|
|
176
110
|
)
|
|
177
111
|
|
|
178
112
|
# Store protocol reference
|
|
@@ -222,15 +156,16 @@ class ProtocolLogWidget(Widget):
|
|
|
222
156
|
|
|
223
157
|
except Exception as e:
|
|
224
158
|
self.logger.error(f"Connection failed: {e}")
|
|
159
|
+
self.post_status(f"Connection failed: {e}")
|
|
225
160
|
# Transition to FAILED
|
|
226
161
|
if self._state_machine.transition("failed", ConnectionState.FAILED):
|
|
227
162
|
self.connection_state = ConnectionState.FAILED
|
|
228
|
-
self.post_message(self.StatusMessageChanged(f"Connection error: {e}"))
|
|
229
163
|
|
|
230
164
|
def _start_connection(self) -> None:
|
|
231
165
|
"""Start connection (sync wrapper for async method)."""
|
|
232
166
|
# Use run_worker to run async method from sync context
|
|
233
167
|
self.logger.debug("Start connection")
|
|
168
|
+
self.post_status("Start connection")
|
|
234
169
|
self.run_worker(self._start_connection_async(), exclusive=True)
|
|
235
170
|
|
|
236
171
|
def _on_connection_made(self) -> None:
|
|
@@ -239,16 +174,26 @@ class ProtocolLogWidget(Widget):
|
|
|
239
174
|
Sets state to CONNECTED and displays success message.
|
|
240
175
|
"""
|
|
241
176
|
self.logger.debug("Connection made")
|
|
177
|
+
self.post_status("Connection made")
|
|
242
178
|
# Transition to CONNECTED
|
|
243
179
|
if self._state_machine.transition("connected", ConnectionState.CONNECTED):
|
|
244
180
|
self.connection_state = ConnectionState.CONNECTED
|
|
245
181
|
if self.protocol:
|
|
246
|
-
self.
|
|
247
|
-
self.
|
|
248
|
-
f"Connected to {self.protocol.cli_config.ip}:{self.protocol.cli_config.port}"
|
|
249
|
-
)
|
|
182
|
+
self.post_status(
|
|
183
|
+
f"Connected to {self.protocol.cli_config.ip}:{self.protocol.cli_config.port}"
|
|
250
184
|
)
|
|
251
185
|
|
|
186
|
+
def _on_connection_failed(self, failure: Failure) -> None:
|
|
187
|
+
"""Handle connection failed signal.
|
|
188
|
+
|
|
189
|
+
Sets state to DISCONNECTED and displays success message.
|
|
190
|
+
"""
|
|
191
|
+
self.logger.debug("Connection failed")
|
|
192
|
+
self.post_status(failure.getErrorMessage())
|
|
193
|
+
# Transition to CONNECTED
|
|
194
|
+
if self._state_machine.transition("disconnected", ConnectionState.DISCONNECTED):
|
|
195
|
+
self.connection_state = ConnectionState.DISCONNECTED
|
|
196
|
+
|
|
252
197
|
def _on_telegram_received(self, event: TelegramReceivedEvent) -> None:
|
|
253
198
|
"""Handle telegram received signal.
|
|
254
199
|
|
|
@@ -289,7 +234,15 @@ class ProtocolLogWidget(Widget):
|
|
|
289
234
|
if self._state_machine.transition("failed", ConnectionState.FAILED):
|
|
290
235
|
self.connection_state = ConnectionState.FAILED
|
|
291
236
|
self.logger.error(f"Connection failed: {error}")
|
|
292
|
-
self.
|
|
237
|
+
self.post_status(f"Failed: {error}")
|
|
238
|
+
|
|
239
|
+
def post_status(self, message: str) -> None:
|
|
240
|
+
"""Post status message.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
message: message to be sent to status bar.
|
|
244
|
+
"""
|
|
245
|
+
self.post_message(StatusMessageChanged(message))
|
|
293
246
|
|
|
294
247
|
def connect(self) -> None:
|
|
295
248
|
"""Connect to Conbus server.
|
|
@@ -326,7 +279,7 @@ class ProtocolLogWidget(Widget):
|
|
|
326
279
|
"disconnecting", ConnectionState.DISCONNECTING
|
|
327
280
|
):
|
|
328
281
|
self.connection_state = ConnectionState.DISCONNECTING
|
|
329
|
-
self.
|
|
282
|
+
self.post_status("Disconnecting...")
|
|
330
283
|
|
|
331
284
|
if self.protocol:
|
|
332
285
|
self.protocol.disconnect()
|
|
@@ -334,7 +287,7 @@ class ProtocolLogWidget(Widget):
|
|
|
334
287
|
# Transition to DISCONNECTED
|
|
335
288
|
if self._state_machine.transition("disconnected", ConnectionState.DISCONNECTED):
|
|
336
289
|
self.connection_state = ConnectionState.DISCONNECTED
|
|
337
|
-
self.
|
|
290
|
+
self.post_status("Disconnected")
|
|
338
291
|
|
|
339
292
|
def send_telegram(self, name: str, telegram: str) -> None:
|
|
340
293
|
"""Send a raw telegram string.
|
|
@@ -349,19 +302,19 @@ class ProtocolLogWidget(Widget):
|
|
|
349
302
|
|
|
350
303
|
try:
|
|
351
304
|
# Remove angle brackets if present
|
|
352
|
-
self.
|
|
305
|
+
self.post_status(f"{name} sent.")
|
|
353
306
|
# Send raw telegram
|
|
354
307
|
self.protocol.send_raw_telegram(telegram)
|
|
355
308
|
|
|
356
309
|
except Exception as e:
|
|
357
310
|
self.logger.error(f"Failed to send telegram: {e}")
|
|
358
|
-
self.
|
|
311
|
+
self.post_status(f"Failed: {e}")
|
|
359
312
|
|
|
360
313
|
def clear_log(self) -> None:
|
|
361
314
|
"""Clear the protocol log widget."""
|
|
362
315
|
if self.log_widget:
|
|
363
316
|
self.log_widget.clear()
|
|
364
|
-
self.
|
|
317
|
+
self.post_status("Log cleared")
|
|
365
318
|
|
|
366
319
|
def on_unmount(self) -> None:
|
|
367
320
|
"""Clean up when widget unmounts.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Status Footer Widget for displaying app footer with connection status."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from textual.app import ComposeResult
|
|
6
|
+
from textual.containers import Horizontal
|
|
7
|
+
from textual.widgets import Footer, Static
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class StatusFooterWidget(Horizontal):
|
|
11
|
+
"""Footer widget with connection status indicator.
|
|
12
|
+
|
|
13
|
+
Combines the Textual Footer with a status indicator dot that shows
|
|
14
|
+
the current connection state.
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
status_widget: Static widget displaying colored status dot.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
21
|
+
"""Initialize the Status Footer widget.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
args: Additional positional arguments for Horizontal.
|
|
25
|
+
kwargs: Additional keyword arguments for Horizontal.
|
|
26
|
+
"""
|
|
27
|
+
super().__init__(*args, **kwargs)
|
|
28
|
+
self.status_text_widget: Static = Static("", id="status-text")
|
|
29
|
+
self.status_widget: Static = Static("○", id="status-line")
|
|
30
|
+
|
|
31
|
+
def compose(self) -> ComposeResult:
|
|
32
|
+
"""Compose the footer layout.
|
|
33
|
+
|
|
34
|
+
Yields:
|
|
35
|
+
Footer and status indicator widgets.
|
|
36
|
+
"""
|
|
37
|
+
yield Footer()
|
|
38
|
+
yield self.status_text_widget
|
|
39
|
+
yield self.status_widget
|
|
40
|
+
|
|
41
|
+
def update_status(self, state: Any) -> None:
|
|
42
|
+
"""Update status indicator with connection state.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
state: Current connection state (ConnectionState enum).
|
|
46
|
+
"""
|
|
47
|
+
# Map states to colored dots
|
|
48
|
+
dot = {
|
|
49
|
+
"CONNECTED": "[green]●[/green]",
|
|
50
|
+
"CONNECTING": "[yellow]●[/yellow]",
|
|
51
|
+
"DISCONNECTING": "[yellow]●[/yellow]",
|
|
52
|
+
"FAILED": "[red]●[/red]",
|
|
53
|
+
"DISCONNECTED": "○",
|
|
54
|
+
}.get(state.value, "○")
|
|
55
|
+
self.status_widget.update(dot)
|
|
56
|
+
|
|
57
|
+
def update_message(self, message: str) -> None:
|
|
58
|
+
"""Update status text with message.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
message: Status message to display.
|
|
62
|
+
"""
|
|
63
|
+
self.status_text_widget.update(message)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|