conson-xp 1.22.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: conson-xp
3
- Version: 1.22.0
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
@@ -1,8 +1,8 @@
1
- conson_xp-1.22.0.dist-info/METADATA,sha256=SHz732F4Z0CbbWIukCpttstkgiNLFBgqYAZQg94OkEk,10298
2
- conson_xp-1.22.0.dist-info/WHEEL,sha256=tsUv_t7BDeJeRHaSrczbGeuK-TtDpGsWi_JfpzD255I,90
3
- conson_xp-1.22.0.dist-info/entry_points.txt,sha256=1OcdIcDM1hz3ljCXgybaPUh1IOFEwkaTgLIW9u9zGeg,50
4
- conson_xp-1.22.0.dist-info/licenses/LICENSE,sha256=rxj6woMM-r3YCyGq_UHFtbh7kHTAJgrccH6O-33IDE4,1419
5
- xp/__init__.py,sha256=cGDFz-p2G9nzESEviCX7b8voSRj-fYSsRjnhBPsDwSE,181
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
@@ -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=t_ovcLbwXays-y8u-EqFpDSfo2Xc_BNl3jAj9PqxRwg,13885
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,13 +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/protocol.py,sha256=ERntzYvNMyI-VDCYW7EnhpPu-V6WPmyEk0xcQfm7LFM,4399
180
- xp/term/protocol.tcss,sha256=biaIv6X-bmD1tbXENFUNsXOF8ABkuoRAUgeSDqwudtQ,2126
181
+ xp/term/protocol.py,sha256=acYYsv7_4z5ePrnslSC1exKzqbKOE5ZGds4J33Q2XNs,4784
182
+ xp/term/protocol.tcss,sha256=r_KfxrbpycGHLVXqZc6INBBcUJME0hLrAZkF1oqnab4,2126
181
183
  xp/term/protocol.yml,sha256=kiTe_QSMPmLvLA0ZyIhNaDPwBdi6khh5C1NSR7I9TN0,2124
182
184
  xp/term/widgets/__init__.py,sha256=ftWmN_fmjxy2E8Qfm-YSRmzKfgL0KTBCTpgvYWCPbUY,274
183
185
  xp/term/widgets/help_menu.py,sha256=bdT5AYRdtKt_tvZTVbG7-DPMb1mj78kggtjjsa-95BA,1780
184
- xp/term/widgets/protocol_log.py,sha256=yuSfc61azgQmWQxLvBcmVik3cJAW1Hocj2Sk8qr3hSg,14343
185
- xp/term/widgets/status_footer.py,sha256=VTN5owCprMbYmNiEbNWS_4CE8yxysDi8IBCECuU9EQY,1663
186
+ xp/term/widgets/protocol_log.py,sha256=OeVg9VCvIl9HXm6j_HPAGOIAYxO0MxlKcS9QmBEOKx4,13018
187
+ xp/term/widgets/status_footer.py,sha256=eRZHkrG5aZCMulibX56KfFyGHS8IgL-7psvr9f9S6FI,1992
186
188
  xp/utils/__init__.py,sha256=_avMF_UOkfR3tNeDIPqQ5odmbq5raKkaq1rZ9Cn1CJs,332
187
189
  xp/utils/checksum.py,sha256=HDpiQxmdIedbCbZ4o_Box0i_Zig417BtCV_46ZyhiTk,1711
188
190
  xp/utils/dependencies.py,sha256=ECS6p0eXzocM5INLwJeckHXn_Dim18uOjXTJ29qQvkQ,22001
@@ -191,4 +193,4 @@ xp/utils/logging.py,sha256=rZDXwlBrYK8A6MPq5StsMNpgsRowzJXM6fvROPwJdGM,3750
191
193
  xp/utils/serialization.py,sha256=RWHHk86feaB4ZP7rjE4qOWK0900yg2joUBDkP76gfOY,4618
192
194
  xp/utils/state_machine.py,sha256=Oe2sLwCh9z_vr1tF6X0ZRGTeuckRQAGzmef7xc9CNdc,2413
193
195
  xp/utils/time_utils.py,sha256=dEyViDlAG9GWU-J3D_YVa-sGma6yiyyMTgN4h2x3PY4,3781
194
- conson_xp-1.22.0.dist-info/RECORD,,
196
+ conson_xp-1.23.0.dist-info/RECORD,,
xp/__init__.py CHANGED
@@ -3,7 +3,7 @@
3
3
  conson-xp package.
4
4
  """
5
5
 
6
- __version__ = "1.22.0"
6
+ __version__ = "1.23.0"
7
7
  __manufacturer__ = "salchichon"
8
8
  __model__ = "xp.cli"
9
9
  __serial__ = "2025.09.23.000"
@@ -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
- raise IOError("Transport is not open")
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/protocol.py CHANGED
@@ -7,6 +7,7 @@ from textual.app import App, ComposeResult
7
7
  from textual.containers import Horizontal
8
8
 
9
9
  from xp.models.term import ProtocolKeysConfig
10
+ from xp.models.term.status_message import StatusMessageChanged
10
11
  from xp.term.widgets.help_menu import HelpMenuWidget
11
12
  from xp.term.widgets.protocol_log import ProtocolLogWidget
12
13
  from xp.term.widgets.status_footer import StatusFooterWidget
@@ -125,3 +126,12 @@ class ProtocolMonitorApp(App[None]):
125
126
  """
126
127
  if self.footer_widget:
127
128
  self.footer_widget.update_status(state)
129
+
130
+ def on_status_message_changed(self, message: StatusMessageChanged) -> None:
131
+ """Handle status message changes from protocol widget.
132
+
133
+ Args:
134
+ message: Message containing the status text.
135
+ """
136
+ if self.footer_widget:
137
+ self.footer_widget.update_message(message.message)
xp/term/protocol.tcss CHANGED
@@ -119,7 +119,7 @@ Footer {
119
119
  #status-text {
120
120
  dock: right;
121
121
  width: auto;
122
- padding: 0 1;
122
+ padding: 0 3;
123
123
  background: $background;
124
124
  color: $text;
125
125
  text-align: right;
@@ -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 = create_connection_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.post_message(
173
- self.StatusMessageChanged(
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.post_message(
247
- self.StatusMessageChanged(
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.post_message(self.StatusMessageChanged(f"Failed: {error}"))
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.post_message(self.StatusMessageChanged("Disconnecting..."))
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.post_message(self.StatusMessageChanged("Disconnected"))
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.post_message(self.StatusMessageChanged(f"Sending {name}..."))
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.post_message(self.StatusMessageChanged(f"Failed: {e}"))
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.post_message(self.StatusMessageChanged("Log cleared"))
317
+ self.post_status("Log cleared")
365
318
 
366
319
  def on_unmount(self) -> None:
367
320
  """Clean up when widget unmounts.
@@ -25,6 +25,7 @@ class StatusFooterWidget(Horizontal):
25
25
  kwargs: Additional keyword arguments for Horizontal.
26
26
  """
27
27
  super().__init__(*args, **kwargs)
28
+ self.status_text_widget: Static = Static("", id="status-text")
28
29
  self.status_widget: Static = Static("○", id="status-line")
29
30
 
30
31
  def compose(self) -> ComposeResult:
@@ -34,6 +35,7 @@ class StatusFooterWidget(Horizontal):
34
35
  Footer and status indicator widgets.
35
36
  """
36
37
  yield Footer()
38
+ yield self.status_text_widget
37
39
  yield self.status_widget
38
40
 
39
41
  def update_status(self, state: Any) -> None:
@@ -51,3 +53,11 @@ class StatusFooterWidget(Horizontal):
51
53
  "DISCONNECTED": "○",
52
54
  }.get(state.value, "○")
53
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)