conson-xp 1.23.0__py3-none-any.whl → 1.25.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.23.0
3
+ Version: 1.25.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.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
1
+ conson_xp-1.25.0.dist-info/METADATA,sha256=SHMAjJSulwuqw_08c2vljlv4EeJ-Yk3O8l6rs5a_8Kk,10298
2
+ conson_xp-1.25.0.dist-info/WHEEL,sha256=tsUv_t7BDeJeRHaSrczbGeuK-TtDpGsWi_JfpzD255I,90
3
+ conson_xp-1.25.0.dist-info/entry_points.txt,sha256=1OcdIcDM1hz3ljCXgybaPUh1IOFEwkaTgLIW9u9zGeg,50
4
+ conson_xp-1.25.0.dist-info/licenses/LICENSE,sha256=rxj6woMM-r3YCyGq_UHFtbh7kHTAJgrccH6O-33IDE4,1419
5
+ xp/__init__.py,sha256=8oS2mib7fQyhvVyBRnvOzVSyN-c8IUOz8SSUi5u_gIM,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
@@ -14,7 +14,7 @@ xp/cli/commands/conbus/conbus_blink_commands.py,sha256=UK-Ey4K0FvaPQ96U0gyMid236
14
14
  xp/cli/commands/conbus/conbus_config_commands.py,sha256=BugIbgNX6_s4MySGvI6tWZkwguciajHUX2Xz8XBux7k,716
15
15
  xp/cli/commands/conbus/conbus_custom_commands.py,sha256=lICT93ijMdhVRm8KjNMLo7kQ2BLlnOZvMPbR3SxSmZ4,1692
16
16
  xp/cli/commands/conbus/conbus_datapoint_commands.py,sha256=r36OuTjREtbGKL-bskAGa0-WLw7x06td6woZn3GYJNA,3630
17
- xp/cli/commands/conbus/conbus_discover_commands.py,sha256=-y3TDgOnw1_cjvxvgyfQ1GQE2_WmYq-l8Md7DsdTXmo,1719
17
+ xp/cli/commands/conbus/conbus_discover_commands.py,sha256=MnTCzvERO5xerfs0fuuIBoo1O9h_0IfoJ6snLGVl0lA,1899
18
18
  xp/cli/commands/conbus/conbus_event_commands.py,sha256=7URf-2u8Kzcy0chLYShbZfCbKawf--i-8U88AjhxleQ,3177
19
19
  xp/cli/commands/conbus/conbus_lightlevel_commands.py,sha256=FpCwogdxa7yFUjlrxM7e8Q2Ut32tKAHabngQQChvtJI,6763
20
20
  xp/cli/commands/conbus/conbus_linknumber_commands.py,sha256=KitaGDM5HpwVUz8rLpO8VZUypUTcAg3Bzl0DVm6gnSk,3391
@@ -22,7 +22,7 @@ xp/cli/commands/conbus/conbus_modulenumber_commands.py,sha256=L7-6y3rDllOjQ9g6Bk
22
22
  xp/cli/commands/conbus/conbus_msactiontable_commands.py,sha256=fb9MQ4O04H0Dinpt7vSF5GtfntTZHelQ5TuUmSBbCTg,2899
23
23
  xp/cli/commands/conbus/conbus_output_commands.py,sha256=zdRVbHzVhMbZpG2x5WXtujc3wKTsoQUV4IgkVIbJbCc,5019
24
24
  xp/cli/commands/conbus/conbus_raw_commands.py,sha256=8BKUarwvHgz-sxML7n99YVsb8B1HJNExjQpRsuY_tQw,1829
25
- xp/cli/commands/conbus/conbus_receive_commands.py,sha256=2lZP0a3dte3Q_Vp28xYkqLAoxnvArS9SsQdeedOHcQw,1788
25
+ xp/cli/commands/conbus/conbus_receive_commands.py,sha256=_PsC-3xidmJBuOWUS60iDzhSHYYn5ZFmORXap-ljVGM,1902
26
26
  xp/cli/commands/conbus/conbus_scan_commands.py,sha256=JfXucOwOadvLEKT_fW9fwvqWKHaEODOojLjnO8JV_00,1730
27
27
  xp/cli/commands/file_commands.py,sha256=GV102X7FRZDUNKLlzvSsIGcoXAaofOzmjCp3HUpE9lw,5532
28
28
  xp/cli/commands/homekit/__init__.py,sha256=qqwY8ulxTx1S74Mzpb6EKjBLT6fWTNdf9PQ3HKuERKY,50
@@ -128,12 +128,12 @@ xp/services/conbus/conbus_blink_service.py,sha256=x9uM-sLnIEV8wSNsvJgo08E042g-Hh
128
128
  xp/services/conbus/conbus_custom_service.py,sha256=4aneYdPObiZOGxPFYg5Wr70cl_xFxlQIdJBPQSa0enI,5826
129
129
  xp/services/conbus/conbus_datapoint_queryall_service.py,sha256=p9R02cVimhdJILHQ6BoeZj8Hog4oRpqBnMo3t4R8ecY,6816
130
130
  xp/services/conbus/conbus_datapoint_service.py,sha256=SYhHj9RmTmaJ750tyZ1IW2kl7tgDQ1xm_EM1zUjk1aQ,6421
131
- xp/services/conbus/conbus_discover_service.py,sha256=sSCSDNWWGtx5QOShwJfcbG54WCYH-BxWvgE10ghibN4,12326
131
+ xp/services/conbus/conbus_discover_service.py,sha256=ZwjYBlgP6FgpHBJk7pcKr4JHfH7WUHDxe4he4F_HblQ,12740
132
132
  xp/services/conbus/conbus_event_list_service.py,sha256=0xyXXNU44epN5bFkU6oiZMyhxfUguul3evqClvPJDcA,3618
133
133
  xp/services/conbus/conbus_event_raw_service.py,sha256=FZFu-LNLInrTKTpiGLyootozvyIF5Si5FMrxNk2ALD0,7000
134
134
  xp/services/conbus/conbus_output_service.py,sha256=mHFOAPx2zo0TStZ3pokp6v94AQjIamcwZDeg5YH_-eo,7240
135
135
  xp/services/conbus/conbus_raw_service.py,sha256=4yZLLTIAOxpgByUTWZXw1ihGa6Xtl98ckj9T7VfprDI,4335
136
- xp/services/conbus/conbus_receive_service.py,sha256=38lAZ0tc2AjBfcqI7qje-ES_QHiHZ3Ayybrp1ZC8ceM,5412
136
+ xp/services/conbus/conbus_receive_service.py,sha256=7wOaEDrdoXwZE9MeUM89eB3hobYpvtbYk_YLv3MVAtc,5352
137
137
  xp/services/conbus/conbus_scan_service.py,sha256=tHJ5qaxcNXxAZb2D2F1v6IrzydfxjJOYllM6Txt1eBE,5176
138
138
  xp/services/conbus/write_config_service.py,sha256=6feNdixI_Nli4MRLe15nea-7gTEXMUwZIvTqv_1OqHI,7157
139
139
  xp/services/homekit/__init__.py,sha256=xAMKmln_AmEFdOOJGKWYi96seRlKDQpKx3-hm7XbdIo,36
@@ -153,7 +153,7 @@ xp/services/homekit/homekit_service.py,sha256=0lW-hg40ETco3gDBEYkR_sX-UIYsLSKCD4
153
153
  xp/services/log_file_service.py,sha256=fvPcZQj8XOKUA-4ep5R8n0PelvwvRlTLlVxvIWM5KR4,10506
154
154
  xp/services/module_type_service.py,sha256=xWhr1EAZMykL5uNWHWdpa5T8yNruGKH43XRTOS8GwZg,7477
155
155
  xp/services/protocol/__init__.py,sha256=qRufBmqRKGzpuzZ5bxBbmwf510TT00Ke8s5HcWGnqRY,818
156
- xp/services/protocol/conbus_event_protocol.py,sha256=h9ZdnN9CWSBXQqE-M9qmPPbMT3r25cxzXuJsvURp1WQ,14390
156
+ xp/services/protocol/conbus_event_protocol.py,sha256=7u_Gv7vM53Ikzo56D5Vua4y8_0vsl6nUjfRJmrzgUhk,14936
157
157
  xp/services/protocol/conbus_protocol.py,sha256=JO7yLkD_ohPT0ETjnAIx4CGpZyobf4LdbuozM_43btE,10276
158
158
  xp/services/protocol/protocol_factory.py,sha256=PmjN9AtW9sxNo3voqUiNgQA-pTvX1RW4XXFlHKfFr5Q,2470
159
159
  xp/services/protocol/telegram_protocol.py,sha256=Ki5DrXsKxiaqLcdP9WWUuhUI7cPu2DfwyZkh-Gv9Lb8,9496
@@ -180,10 +180,10 @@ xp/services/telegram/telegram_version_service.py,sha256=M5HdOTsLdcwo122FP-jW6R74
180
180
  xp/term/__init__.py,sha256=Xg2DhBeI3xQJLfc7_BPWI1por-rUXemyer5OtOt9Cus,51
181
181
  xp/term/protocol.py,sha256=acYYsv7_4z5ePrnslSC1exKzqbKOE5ZGds4J33Q2XNs,4784
182
182
  xp/term/protocol.tcss,sha256=r_KfxrbpycGHLVXqZc6INBBcUJME0hLrAZkF1oqnab4,2126
183
- xp/term/protocol.yml,sha256=kiTe_QSMPmLvLA0ZyIhNaDPwBdi6khh5C1NSR7I9TN0,2124
183
+ xp/term/protocol.yml,sha256=BI1dyWfYsINsJnbSR-z4fzFOsYcY27dS6it8eo7AVnU,2124
184
184
  xp/term/widgets/__init__.py,sha256=ftWmN_fmjxy2E8Qfm-YSRmzKfgL0KTBCTpgvYWCPbUY,274
185
185
  xp/term/widgets/help_menu.py,sha256=bdT5AYRdtKt_tvZTVbG7-DPMb1mj78kggtjjsa-95BA,1780
186
- xp/term/widgets/protocol_log.py,sha256=OeVg9VCvIl9HXm6j_HPAGOIAYxO0MxlKcS9QmBEOKx4,13018
186
+ xp/term/widgets/protocol_log.py,sha256=X-2CBcfZ4LpWrUVLxxyYqd54lP10mg0t7oclKh3sLkI,11243
187
187
  xp/term/widgets/status_footer.py,sha256=eRZHkrG5aZCMulibX56KfFyGHS8IgL-7psvr9f9S6FI,1992
188
188
  xp/utils/__init__.py,sha256=_avMF_UOkfR3tNeDIPqQ5odmbq5raKkaq1rZ9Cn1CJs,332
189
189
  xp/utils/checksum.py,sha256=HDpiQxmdIedbCbZ4o_Box0i_Zig417BtCV_46ZyhiTk,1711
@@ -193,4 +193,4 @@ xp/utils/logging.py,sha256=rZDXwlBrYK8A6MPq5StsMNpgsRowzJXM6fvROPwJdGM,3750
193
193
  xp/utils/serialization.py,sha256=RWHHk86feaB4ZP7rjE4qOWK0900yg2joUBDkP76gfOY,4618
194
194
  xp/utils/state_machine.py,sha256=Oe2sLwCh9z_vr1tF6X0ZRGTeuckRQAGzmef7xc9CNdc,2413
195
195
  xp/utils/time_utils.py,sha256=dEyViDlAG9GWU-J3D_YVa-sGma6yiyyMTgN4h2x3PY4,3781
196
- conson_xp-1.23.0.dist-info/RECORD,,
196
+ conson_xp-1.25.0.dist-info/RECORD,,
xp/__init__.py CHANGED
@@ -3,7 +3,7 @@
3
3
  conson-xp package.
4
4
  """
5
5
 
6
- __version__ = "1.23.0"
6
+ __version__ = "1.25.0"
7
7
  __manufacturer__ = "salchichon"
8
8
  __model__ = "xp.cli"
9
9
  __serial__ = "2025.09.23.000"
@@ -36,6 +36,7 @@ def send_discover_telegram(ctx: click.Context) -> None:
36
36
  discovered_devices: Discover response with all found devices.
37
37
  """
38
38
  click.echo(json.dumps(discovered_devices.to_dict(), indent=2))
39
+ service.stop_reactor()
39
40
 
40
41
  def on_device_discovered(discovered_device: DiscoveredDevice) -> None:
41
42
  """Handle discovery of sa single module.
@@ -57,5 +58,9 @@ def send_discover_telegram(ctx: click.Context) -> None:
57
58
  service: ConbusDiscoverService = (
58
59
  ctx.obj.get("container").get_container().resolve(ConbusDiscoverService)
59
60
  )
60
- service.run(progress, on_device_discovered, on_finish, 5)
61
- service.start_reactor()
61
+ with service:
62
+ service.on_progress.connect(progress)
63
+ service.on_device_discovered.connect(on_device_discovered)
64
+ service.on_finish.connect(on_finish)
65
+ service.set_timeout(5)
66
+ service.start_reactor()
@@ -43,8 +43,9 @@ def receive_telegrams(ctx: Context, timeout: float) -> None:
43
43
  response_received: Receive response object with telegrams.
44
44
  """
45
45
  click.echo(json.dumps(response_received.to_dict(), indent=2))
46
+ service.stop_reactor()
46
47
 
47
- def progress(telegram_received: str) -> None:
48
+ def on_progress(telegram_received: str) -> None:
48
49
  """Handle progress updates during telegram receive operation.
49
50
 
50
51
  Args:
@@ -56,5 +57,7 @@ def receive_telegrams(ctx: Context, timeout: float) -> None:
56
57
  ctx.obj.get("container").get_container().resolve(ConbusReceiveService)
57
58
  )
58
59
  with service:
59
- service.init(progress, on_finish, timeout)
60
+ service.on_progress.connect(on_progress)
61
+ service.on_finish.connect(on_finish)
62
+ service.set_timeout(timeout)
60
63
  service.start_reactor()
@@ -4,8 +4,11 @@ This service implements a TCP client that connects to Conbus servers and sends
4
4
  discover telegrams to find modules on the network.
5
5
  """
6
6
 
7
+ import asyncio
7
8
  import logging
8
- from typing import Callable, Optional
9
+ from typing import Any, Optional
10
+
11
+ from psygnal import Signal
9
12
 
10
13
  from xp.models import ConbusDiscoverResponse
11
14
  from xp.models.conbus.conbus_discover import DiscoveredDevice
@@ -26,9 +29,15 @@ class ConbusDiscoverService:
26
29
 
27
30
  Attributes:
28
31
  conbus_protocol: Protocol instance for Conbus communication.
32
+ on_progress: Signal emitted when discovery progress is made (with serial number).
33
+ on_finish: Signal emitted when discovery finishes (with result).
34
+ on_device_discovered: Signal emitted when a device is discovered (with device info).
29
35
  """
30
36
 
31
37
  conbus_protocol: ConbusEventProtocol
38
+ on_progress: Signal = Signal(str)
39
+ on_finish: Signal = Signal(DiscoveredDevice)
40
+ on_device_discovered: Signal = Signal(ConbusDiscoverResponse)
32
41
 
33
42
  def __init__(self, conbus_protocol: ConbusEventProtocol) -> None:
34
43
  """Initialize the Conbus discover service.
@@ -36,12 +45,6 @@ class ConbusDiscoverService:
36
45
  Args:
37
46
  conbus_protocol: ConbusProtocol.
38
47
  """
39
- self.progress_callback: Optional[Callable[[str], None]] = None
40
- self.device_discover_callback: Optional[Callable[[DiscoveredDevice], None]] = (
41
- None
42
- )
43
- self.finish_callback: Optional[Callable[[ConbusDiscoverResponse], None]] = None
44
-
45
48
  self.conbus_protocol: ConbusEventProtocol = conbus_protocol
46
49
  self.conbus_protocol.on_connection_made.connect(self.connection_made)
47
50
  self.conbus_protocol.on_telegram_sent.connect(self.telegram_sent)
@@ -135,9 +138,7 @@ class ConbusDiscoverService:
135
138
  "module_type_name": None,
136
139
  }
137
140
  self.discovered_device_result.discovered_devices.append(device)
138
-
139
- if self.device_discover_callback:
140
- self.device_discover_callback(device)
141
+ self.on_device_discovered.emit(device)
141
142
 
142
143
  # Send READ_DATAPOINT telegram to query module type
143
144
  self.logger.debug(f"Sending module type query for {serial_number}")
@@ -147,8 +148,7 @@ class ConbusDiscoverService:
147
148
  system_function=SystemFunction.READ_DATAPOINT,
148
149
  data_value=DataPointType.MODULE_TYPE.value,
149
150
  )
150
- if self.progress_callback:
151
- self.progress_callback(serial_number)
151
+ self.on_progress.emit(serial_number)
152
152
 
153
153
  def handle_module_type_code_response(
154
154
  self, serial_number: str, module_type_code: str
@@ -194,8 +194,7 @@ class ConbusDiscoverService:
194
194
  device["module_type_code"] = code
195
195
  device["module_type_name"] = module_type_name
196
196
 
197
- if self.device_discover_callback:
198
- self.device_discover_callback(device)
197
+ self.on_device_discovered.emit(device)
199
198
 
200
199
  self.logger.debug(
201
200
  f"Updated device {serial_number} with module_type {module_type_name}"
@@ -231,9 +230,7 @@ class ConbusDiscoverService:
231
230
  self.logger.debug(
232
231
  f"Updated device {serial_number} with module_type {module_type}"
233
232
  )
234
- if self.device_discover_callback:
235
- self.device_discover_callback(device)
236
-
233
+ self.on_device_discovered.emit(device)
237
234
  break
238
235
 
239
236
  self.conbus_protocol.send_telegram(
@@ -249,10 +246,7 @@ class ConbusDiscoverService:
249
246
  self.logger.info("Discovery stopped after: %ss", timeout)
250
247
  self.discovered_device_result.success = False
251
248
  self.discovered_device_result.error = "Discovered device timeout"
252
- if self.finish_callback:
253
- self.finish_callback(self.discovered_device_result)
254
-
255
- self.stop_reactor()
249
+ self.on_finish.emit(self.discovered_device_result)
256
250
 
257
251
  def failed(self, message: str) -> None:
258
252
  """Handle failed connection event.
@@ -263,50 +257,64 @@ class ConbusDiscoverService:
263
257
  self.logger.debug(f"Failed: {message}")
264
258
  self.discovered_device_result.success = False
265
259
  self.discovered_device_result.error = message
266
- if self.finish_callback:
267
- self.finish_callback(self.discovered_device_result)
268
-
269
- self.stop_reactor()
260
+ self.on_finish.emit(self.discovered_device_result)
270
261
 
271
262
  def succeed(self) -> None:
272
263
  """Handle discovered device success event."""
273
264
  self.logger.debug("Succeed")
274
265
  self.discovered_device_result.success = True
275
266
  self.discovered_device_result.error = None
276
- if self.finish_callback:
277
- self.finish_callback(self.discovered_device_result)
267
+ self.on_finish.emit(self.discovered_device_result)
278
268
 
279
- self.stop_reactor()
269
+ def set_timeout(self, timeout_seconds: float) -> None:
270
+ """Setup callbacks and timeout for receiving telegrams.
280
271
 
281
- def stop_reactor(self) -> None:
282
- """Stop reactor."""
283
- self.logger.info("Stopping reactor")
284
- self.conbus_protocol.stop_reactor()
285
-
286
- def start_reactor(self) -> None:
287
- """Start reactor."""
288
- self.logger.info("Starting reactor")
289
- self.conbus_protocol.start_reactor()
272
+ Args:
273
+ timeout_seconds: Optional timeout in seconds.
274
+ """
275
+ self.logger.debug("Set timeout")
276
+ self.conbus_protocol.timeout_seconds = timeout_seconds
290
277
 
291
- def run(
278
+ def set_event_loop(
292
279
  self,
293
- progress_callback: Callable[[str], None],
294
- device_discover_callback: Callable[[DiscoveredDevice], None],
295
- finish_callback: Callable[[ConbusDiscoverResponse], None],
296
- timeout_seconds: Optional[float] = None,
280
+ event_loop: asyncio.AbstractEventLoop,
297
281
  ) -> None:
298
- """Run reactor in dedicated thread with its own event loop.
282
+ """Setup callbacks and timeout for receiving telegrams.
299
283
 
300
284
  Args:
301
- progress_callback: Callback for each discovered device.
302
- device_discover_callback: Callback for each discovered device.
303
- finish_callback: Callback when discovery completes.
304
- timeout_seconds: Optional timeout in seconds.
285
+ event_loop: Optional event loop to use for async operations.
305
286
  """
306
- self.logger.info("Starting discovery")
287
+ self.logger.debug("Set eventloop")
288
+ self.conbus_protocol.set_event_loop(event_loop)
307
289
 
308
- if timeout_seconds:
309
- self.conbus_protocol.timeout_seconds = timeout_seconds
310
- self.progress_callback = progress_callback
311
- self.device_discover_callback = device_discover_callback
312
- self.finish_callback = finish_callback
290
+ def start_reactor(self) -> None:
291
+ """Start the reactor."""
292
+ self.conbus_protocol.start_reactor()
293
+
294
+ def stop_reactor(self) -> None:
295
+ """Start the reactor."""
296
+ self.conbus_protocol.stop_reactor()
297
+
298
+ def __enter__(self) -> "ConbusDiscoverService":
299
+ """Enter context manager.
300
+
301
+ Returns:
302
+ Self for context manager protocol.
303
+ """
304
+ # Reset state for singleton reuse
305
+ self.receive_response = ConbusDiscoverResponse(success=True)
306
+ return self
307
+
308
+ def __exit__(
309
+ self, _exc_type: Optional[type], _exc_val: Optional[Exception], _exc_tb: Any
310
+ ) -> None:
311
+ """Exit context manager and disconnect signals."""
312
+ self.conbus_protocol.on_connection_made.disconnect(self.connection_made)
313
+ self.conbus_protocol.on_telegram_sent.disconnect(self.telegram_sent)
314
+ self.conbus_protocol.on_telegram_received.disconnect(self.telegram_received)
315
+ self.conbus_protocol.on_timeout.disconnect(self.timeout)
316
+ self.conbus_protocol.on_failed.disconnect(self.failed)
317
+ self.on_device_discovered.disconnect()
318
+ self.on_progress.disconnect()
319
+ self.on_finish.disconnect()
320
+ self.stop_reactor()
@@ -6,7 +6,9 @@ allowing clients to receive waiting event telegrams using empty telegram sends.
6
6
 
7
7
  import asyncio
8
8
  import logging
9
- from typing import Any, Callable, Optional
9
+ from typing import Any, Optional
10
+
11
+ from psygnal import Signal
10
12
 
11
13
  from xp.models.conbus.conbus_receive import ConbusReceiveResponse
12
14
  from xp.models.protocol.conbus_protocol import TelegramReceivedEvent
@@ -22,9 +24,13 @@ class ConbusReceiveService:
22
24
 
23
25
  Attributes:
24
26
  conbus_protocol: Protocol instance for Conbus communication.
27
+ on_progress: Signal emitted when a telegram is received (with telegram frame).
28
+ on_finish: Signal emitted when receiving finishes (with result).
25
29
  """
26
30
 
27
31
  conbus_protocol: ConbusEventProtocol
32
+ on_progress: Signal = Signal(str)
33
+ on_finish: Signal = Signal(ConbusReceiveResponse)
28
34
 
29
35
  def __init__(self, conbus_protocol: ConbusEventProtocol) -> None:
30
36
  """Initialize the Conbus receive service.
@@ -32,8 +38,6 @@ class ConbusReceiveService:
32
38
  Args:
33
39
  conbus_protocol: ConbusEventProtocol instance.
34
40
  """
35
- self.progress_callback: Optional[Callable[[str], None]] = None
36
- self.finish_callback: Optional[Callable[[ConbusReceiveResponse], None]] = None
37
41
  self.receive_response: ConbusReceiveResponse = ConbusReceiveResponse(
38
42
  success=True
39
43
  )
@@ -67,8 +71,7 @@ class ConbusReceiveService:
67
71
  telegram_received: The telegram received event.
68
72
  """
69
73
  self.logger.debug(f"Telegram received: {telegram_received}")
70
- if self.progress_callback:
71
- self.progress_callback(telegram_received.frame)
74
+ self.on_progress.emit(telegram_received.frame)
72
75
 
73
76
  if not self.receive_response.received_telegrams:
74
77
  self.receive_response.received_telegrams = []
@@ -79,8 +82,7 @@ class ConbusReceiveService:
79
82
  timeout = self.conbus_protocol.timeout_seconds
80
83
  self.logger.info("Receive stopped after: %ss", timeout)
81
84
  self.receive_response.success = True
82
- if self.finish_callback:
83
- self.finish_callback(self.receive_response)
85
+ self.on_finish.emit(self.receive_response)
84
86
 
85
87
  def failed(self, message: str) -> None:
86
88
  """Handle failed connection event.
@@ -91,37 +93,37 @@ class ConbusReceiveService:
91
93
  self.logger.debug("Failed %s:", message)
92
94
  self.receive_response.success = False
93
95
  self.receive_response.error = message
94
- if self.finish_callback:
95
- self.finish_callback(self.receive_response)
96
+ self.on_finish.emit(self.receive_response)
97
+
98
+ def set_timeout(self, timeout_seconds: float) -> None:
99
+ """Setup callbacks and timeout for receiving telegrams.
96
100
 
97
- def init(
101
+ Args:
102
+ timeout_seconds: Optional timeout in seconds.
103
+ """
104
+ self.logger.debug("Set timeout")
105
+ self.conbus_protocol.timeout_seconds = timeout_seconds
106
+
107
+ def set_event_loop(
98
108
  self,
99
- progress_callback: Callable[[str], None],
100
- finish_callback: Callable[[ConbusReceiveResponse], None],
101
- timeout_seconds: Optional[float] = None,
102
- event_loop: Optional[asyncio.AbstractEventLoop] = None,
109
+ event_loop: asyncio.AbstractEventLoop,
103
110
  ) -> None:
104
111
  """Setup callbacks and timeout for receiving telegrams.
105
112
 
106
113
  Args:
107
- progress_callback: Callback for each received telegram.
108
- finish_callback: Callback when receiving completes.
109
- timeout_seconds: Optional timeout in seconds.
110
114
  event_loop: Optional event loop to use for async operations.
111
115
  """
112
- self.logger.info("Starting receive")
113
- if timeout_seconds:
114
- self.conbus_protocol.timeout_seconds = timeout_seconds
115
- self.progress_callback = progress_callback
116
- self.finish_callback = finish_callback
117
-
118
- if event_loop:
119
- self.conbus_protocol.set_event_loop(event_loop)
116
+ self.logger.debug("Set eventloop")
117
+ self.conbus_protocol.set_event_loop(event_loop)
120
118
 
121
119
  def start_reactor(self) -> None:
122
120
  """Start the reactor."""
123
121
  self.conbus_protocol.start_reactor()
124
122
 
123
+ def stop_reactor(self) -> None:
124
+ """Start the reactor."""
125
+ self.conbus_protocol.stop_reactor()
126
+
125
127
  def __enter__(self) -> "ConbusReceiveService":
126
128
  """Enter context manager.
127
129
 
@@ -141,3 +143,6 @@ class ConbusReceiveService:
141
143
  self.conbus_protocol.on_telegram_received.disconnect(self.telegram_received)
142
144
  self.conbus_protocol.on_timeout.disconnect(self.timeout)
143
145
  self.conbus_protocol.on_failed.disconnect(self.failed)
146
+ self.on_progress.disconnect()
147
+ self.on_finish.disconnect()
148
+ self.stop_reactor()
@@ -325,10 +325,23 @@ class ConbusEventProtocol(protocol.Protocol, protocol.ClientFactory):
325
325
  self._reactor.stop()
326
326
 
327
327
  def connect(self) -> None:
328
- """Connect to TCP server."""
328
+ """Connect to TCP server.
329
+
330
+ Automatically detects and integrates with running asyncio event loop if present.
331
+ """
329
332
  self.logger.info(
330
333
  f"Connecting to TCP server {self.cli_config.ip}:{self.cli_config.port}"
331
334
  )
335
+
336
+ # Auto-detect and integrate with asyncio event loop if available
337
+ try:
338
+ event_loop = asyncio.get_running_loop()
339
+ self.logger.debug(f"Detected running event loop: {event_loop}")
340
+ self.set_event_loop(event_loop)
341
+ except RuntimeError:
342
+ # No running event loop - that's fine for non-async contexts
343
+ self.logger.debug("No running event loop detected - using reactor only")
344
+
332
345
  self._reactor.connectTCP(self.cli_config.ip, self.cli_config.port, self)
333
346
 
334
347
  def disconnect(self) -> None:
xp/term/protocol.yml CHANGED
@@ -63,8 +63,8 @@ protocol:
63
63
  "e":
64
64
  name: "Link 1 Off"
65
65
  telegrams:
66
- - E02L01I00M
67
- - E02L01I00B
66
+ - E02L01I09M
67
+ - E02L01I09B
68
68
 
69
69
  "f":
70
70
  name: "Link 2 On"
@@ -75,8 +75,8 @@ protocol:
75
75
  "g":
76
76
  name: "Link 2 Off"
77
77
  telegrams:
78
- - E02L02I00M
79
- - E02L02I00B
78
+ - E02L02I09M
79
+ - E02L02I09B
80
80
 
81
81
  "h":
82
82
  name: "Link 3 On"
@@ -87,8 +87,8 @@ protocol:
87
87
  "i":
88
88
  name: "Link 3 Off"
89
89
  telegrams:
90
- - E02L03I00M
91
- - E02L03I00B
90
+ - E02L03I09M
91
+ - E02L03I09B
92
92
 
93
93
  "j":
94
94
  name: "Link 4 On"
@@ -99,8 +99,8 @@ protocol:
99
99
  "k":
100
100
  name: "Link 4 Off"
101
101
  telegrams:
102
- - E02L04I00M
103
- - E02L04I00B
102
+ - E02L04I09M
103
+ - E02L04I09B
104
104
 
105
105
  "l":
106
106
  name: "Link 5 On"
@@ -111,8 +111,8 @@ protocol:
111
111
  "m":
112
112
  name: "Link 5 Off"
113
113
  telegrams:
114
- - E02L05I00M
115
- - E02L05I00B
114
+ - E02L05I09M
115
+ - E02L05I09B
116
116
 
117
117
  "n":
118
118
  name: "Link 6 On"
@@ -123,8 +123,8 @@ protocol:
123
123
  "o":
124
124
  name: "Link 6 Off"
125
125
  telegrams:
126
- - E02L06I00M
127
- - E02L06I00B
126
+ - E02L06I09M
127
+ - E02L06I09B
128
128
 
129
129
  "p":
130
130
  name: "Link 7 On"
@@ -135,5 +135,5 @@ protocol:
135
135
  "q":
136
136
  name: "Link 7 Off"
137
137
  telegrams:
138
- - E02L07I00M
139
- - E02L07I00B
138
+ - E02L07I09M
139
+ - E02L07I09B
@@ -12,7 +12,6 @@ from twisted.python.failure import Failure
12
12
  from xp.models.protocol.conbus_protocol import TelegramReceivedEvent
13
13
  from xp.models.term.connection_state import ConnectionState
14
14
  from xp.models.term.status_message import StatusMessageChanged
15
- from xp.services.conbus.conbus_receive_service import ConbusReceiveService
16
15
  from xp.services.protocol import ConbusEventProtocol
17
16
 
18
17
 
@@ -26,7 +25,6 @@ class ProtocolLogWidget(Widget):
26
25
  container: ServiceContainer for dependency injection.
27
26
  connection_state: Current connection state (reactive).
28
27
  protocol: Reference to ConbusEventProtocol (prevents duplicate connections).
29
- service: ConbusReceiveService instance.
30
28
  logger: Logger instance for this widget.
31
29
  log_widget: RichLog widget for displaying messages.
32
30
  """
@@ -43,7 +41,6 @@ class ProtocolLogWidget(Widget):
43
41
  self.border_title = "Protocol"
44
42
  self.container = container
45
43
  self.protocol: Optional[ConbusEventProtocol] = None
46
- self.service: Optional[ConbusReceiveService] = None
47
44
  self.logger = logging.getLogger(__name__)
48
45
  self.log_widget: Optional[RichLog] = None
49
46
  self._state_machine = ConnectionState.create_state_machine()
@@ -64,8 +61,7 @@ class ProtocolLogWidget(Widget):
64
61
  Resolves ConbusReceiveService and connects signals.
65
62
  """
66
63
  # Resolve service from container (singleton)
67
- self.service = self.container.resolve(ConbusReceiveService)
68
- self.protocol = self.service.conbus_protocol
64
+ self.protocol = self.container.resolve(ConbusEventProtocol)
69
65
 
70
66
  # Connect psygnal signals
71
67
  self.protocol.on_connection_made.connect(self._on_connection_made)
@@ -86,10 +82,6 @@ class ProtocolLogWidget(Widget):
86
82
  Integrates Twisted reactor with Textual's asyncio loop cleanly.
87
83
  """
88
84
  # Guard against duplicate connections (race condition)
89
- if self.service is None:
90
- self.logger.error("Service not initialized")
91
- return
92
-
93
85
  if self.protocol is None:
94
86
  self.logger.error("Protocol not initialized")
95
87
  return
@@ -109,46 +101,8 @@ class ProtocolLogWidget(Widget):
109
101
  f"Connecting to {self.protocol.cli_config.ip}:{self.protocol.cli_config.port}..."
110
102
  )
111
103
 
112
- # Store protocol reference
113
- self.logger.info(f"Protocol object: {self.protocol}")
114
- self.logger.info(f"Reactor object: {self.protocol._reactor}")
115
- self.logger.info(f"Reactor running: {self.protocol._reactor.running}")
116
-
117
- # Setup service callbacks
118
- def progress_callback(telegram: str) -> None:
119
- """Handle progress updates for telegram reception.
120
-
121
- Args:
122
- telegram: Received telegram string.
123
- """
124
- pass
125
-
126
- def finish_callback(response: Any) -> None:
127
- """Handle completion of telegram reception.
128
-
129
- Args:
130
- response: Response object from telegram reception.
131
- """
132
- pass
133
-
134
- # Get the currently running asyncio event loop (Textual's loop)
135
- event_loop = asyncio.get_running_loop()
136
- self.logger.info(f"Current running loop: {event_loop}")
137
- self.logger.info(f"Loop is running: {event_loop.is_running()}")
138
-
139
- self.service.init(
140
- progress_callback=progress_callback,
141
- finish_callback=finish_callback,
142
- timeout_seconds=None, # Continuous monitoring
143
- event_loop=event_loop,
144
- )
145
-
146
- reactor = self.service.conbus_protocol._reactor
147
- reactor.connectTCP(
148
- self.protocol.cli_config.ip,
149
- self.protocol.cli_config.port,
150
- self.protocol,
151
- )
104
+ # Connect to server (auto-detects and integrates with asyncio event loop)
105
+ self.protocol.connect()
152
106
 
153
107
  # Wait for connection to establish
154
108
  await asyncio.sleep(1.0)