conson-xp 1.34.0__py3-none-any.whl → 1.36.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.
Files changed (31) hide show
  1. {conson_xp-1.34.0.dist-info → conson_xp-1.36.0.dist-info}/METADATA +1 -1
  2. {conson_xp-1.34.0.dist-info → conson_xp-1.36.0.dist-info}/RECORD +31 -31
  3. xp/__init__.py +1 -1
  4. xp/cli/commands/conbus/conbus_actiontable_commands.py +34 -35
  5. xp/cli/commands/conbus/conbus_autoreport_commands.py +11 -10
  6. xp/cli/commands/conbus/conbus_custom_commands.py +6 -4
  7. xp/cli/commands/conbus/conbus_datapoint_commands.py +8 -6
  8. xp/cli/commands/conbus/conbus_lightlevel_commands.py +19 -16
  9. xp/cli/commands/conbus/conbus_linknumber_commands.py +7 -6
  10. xp/cli/commands/conbus/conbus_modulenumber_commands.py +7 -6
  11. xp/cli/commands/conbus/conbus_msactiontable_commands.py +7 -9
  12. xp/cli/commands/conbus/conbus_output_commands.py +9 -7
  13. xp/cli/commands/conbus/conbus_raw_commands.py +7 -2
  14. xp/cli/commands/conbus/conbus_scan_commands.py +4 -2
  15. xp/services/conbus/actiontable/actiontable_download_service.py +79 -37
  16. xp/services/conbus/actiontable/actiontable_list_service.py +17 -17
  17. xp/services/conbus/actiontable/actiontable_upload_service.py +78 -36
  18. xp/services/conbus/actiontable/msactiontable_service.py +88 -48
  19. xp/services/conbus/conbus_custom_service.py +81 -26
  20. xp/services/conbus/conbus_datapoint_queryall_service.py +90 -43
  21. xp/services/conbus/conbus_datapoint_service.py +76 -28
  22. xp/services/conbus/conbus_output_service.py +82 -22
  23. xp/services/conbus/conbus_raw_service.py +78 -37
  24. xp/services/conbus/conbus_scan_service.py +86 -42
  25. xp/services/conbus/write_config_service.py +76 -26
  26. xp/services/term/state_monitor_service.py +35 -15
  27. xp/term/widgets/modules_list.py +4 -2
  28. xp/utils/dependencies.py +10 -20
  29. {conson_xp-1.34.0.dist-info → conson_xp-1.36.0.dist-info}/WHEEL +0 -0
  30. {conson_xp-1.34.0.dist-info → conson_xp-1.36.0.dist-info}/entry_points.txt +0 -0
  31. {conson_xp-1.34.0.dist-info → conson_xp-1.36.0.dist-info}/licenses/LICENSE +0 -0
@@ -5,9 +5,7 @@ import json
5
5
  import click
6
6
 
7
7
  from xp.cli.commands.conbus.conbus import conbus_output
8
- from xp.cli.utils.decorators import (
9
- connection_command,
10
- )
8
+ from xp.cli.utils.decorators import connection_command
11
9
  from xp.cli.utils.serial_number_type import SERIAL
12
10
  from xp.models import ConbusDatapointResponse
13
11
  from xp.models.conbus.conbus_output import ConbusOutputResponse
@@ -45,14 +43,16 @@ def xp_output_on(ctx: click.Context, serial_number: str, output_number: int) ->
45
43
  response: Output response object.
46
44
  """
47
45
  click.echo(json.dumps(response.to_dict(), indent=2))
46
+ service.stop_reactor()
48
47
 
49
48
  with service:
49
+ service.on_finish.connect(on_finish)
50
50
  service.send_action(
51
51
  serial_number=serial_number,
52
52
  output_number=output_number,
53
53
  action_type=ActionType.ON_RELEASE,
54
- finish_callback=on_finish,
55
54
  )
55
+ service.start_reactor()
56
56
 
57
57
 
58
58
  @conbus_output.command("off")
@@ -83,14 +83,16 @@ def xp_output_off(ctx: click.Context, serial_number: str, output_number: int) ->
83
83
  response: Output response object.
84
84
  """
85
85
  click.echo(json.dumps(response.to_dict(), indent=2))
86
+ service.stop_reactor()
86
87
 
87
88
  with service:
89
+ service.on_finish.connect(on_finish)
88
90
  service.send_action(
89
91
  serial_number=serial_number,
90
92
  output_number=output_number,
91
93
  action_type=ActionType.OFF_PRESS,
92
- finish_callback=on_finish,
93
94
  )
95
+ service.start_reactor()
94
96
 
95
97
 
96
98
  @conbus_output.command("status")
@@ -121,10 +123,10 @@ def xp_output_status(ctx: click.Context, serial_number: str) -> None:
121
123
  click.echo(json.dumps(response.to_dict(), indent=2))
122
124
 
123
125
  with service:
126
+ service.on_finish.connect(on_finish)
124
127
  service.query_datapoint(
125
128
  serial_number=serial_number,
126
129
  datapoint_type=DataPointType.MODULE_OUTPUT_STATE,
127
- finish_callback=on_finish,
128
130
  )
129
131
 
130
132
 
@@ -156,8 +158,8 @@ def xp_module_state(ctx: click.Context, serial_number: str) -> None:
156
158
  click.echo(json.dumps(response.to_dict(), indent=2))
157
159
 
158
160
  with service:
161
+ service.on_finish.connect(on_finish)
159
162
  service.query_datapoint(
160
163
  serial_number=serial_number,
161
164
  datapoint_type=DataPointType.MODULE_STATE,
162
- finish_callback=on_finish,
163
165
  )
@@ -52,11 +52,16 @@ def send_raw_telegrams(ctx: Context, raw_telegrams: str) -> None:
52
52
  service_response: Raw response object.
53
53
  """
54
54
  click.echo(json.dumps(service_response.to_dict(), indent=2))
55
+ service.stop_reactor()
55
56
 
56
57
  with service:
58
+ # Connect service signals
59
+ service.on_progress.connect(on_progress)
60
+ service.on_finish.connect(on_finish)
61
+ # Setup
57
62
  service.send_raw_telegram(
58
63
  raw_input=raw_telegrams,
59
- progress_callback=on_progress,
60
- finish_callback=on_finish,
61
64
  timeout_seconds=5.0,
62
65
  )
66
+ # Start (blocks until completion)
67
+ service.start_reactor()
@@ -48,11 +48,13 @@ def scan_module(ctx: Context, serial_number: str, function_code: str) -> None:
48
48
  service_response: Scan response object.
49
49
  """
50
50
  click.echo(json.dumps(service_response.to_dict(), indent=2))
51
+ service.stop_reactor()
51
52
 
52
53
  with service:
54
+ service.on_progress.connect(on_progress)
55
+ service.on_finish.connect(on_finish)
53
56
  service.scan_module(
54
57
  serial_number=serial_number,
55
58
  function_code=function_code,
56
- progress_callback=on_progress,
57
- finish_callback=on_finish,
58
59
  )
60
+ service.start_reactor()
@@ -2,62 +2,68 @@
2
2
 
3
3
  import logging
4
4
  from dataclasses import asdict
5
- from typing import Any, Callable, Dict, Optional
5
+ from typing import Any, Optional
6
6
 
7
- from twisted.internet.posixbase import PosixReactorBase
7
+ from psygnal import Signal
8
8
 
9
- from xp.models import ConbusClientConfig
10
- from xp.models.actiontable.actiontable import ActionTable
11
9
  from xp.models.protocol.conbus_protocol import TelegramReceivedEvent
12
10
  from xp.models.telegram.system_function import SystemFunction
13
11
  from xp.models.telegram.telegram_type import TelegramType
14
12
  from xp.services.actiontable.actiontable_serializer import ActionTableSerializer
15
- from xp.services.protocol import ConbusProtocol
13
+ from xp.services.protocol.conbus_event_protocol import ConbusEventProtocol
16
14
  from xp.services.telegram.telegram_service import TelegramService
17
15
 
18
16
 
19
- class ActionTableService(ConbusProtocol):
17
+ class ActionTableService:
20
18
  """TCP client service for downloading action tables from Conbus modules.
21
19
 
22
20
  Manages TCP socket connections, handles telegram generation and transmission,
23
21
  and processes server responses for action table downloads.
22
+
23
+ Attributes:
24
+ on_progress: Signal emitted with telegram frame when progress is made.
25
+ on_error: Signal emitted with error message string when an error occurs.
26
+ on_finish: Signal emitted with (ActionTable, Dict[str, Any], list[str]) when complete.
24
27
  """
25
28
 
29
+ on_progress: Signal = Signal(str)
30
+ on_error: Signal = Signal(str)
31
+ on_finish: Signal = Signal(object) # (ActionTable, Dict[str, Any], list[str])
32
+
26
33
  def __init__(
27
34
  self,
28
- cli_config: ConbusClientConfig,
29
- reactor: PosixReactorBase,
35
+ conbus_protocol: ConbusEventProtocol,
30
36
  actiontable_serializer: ActionTableSerializer,
31
37
  telegram_service: TelegramService,
32
38
  ) -> None:
33
39
  """Initialize the action table download service.
34
40
 
35
41
  Args:
36
- cli_config: Conbus client configuration.
37
- reactor: Twisted reactor instance.
42
+ conbus_protocol: ConbusEventProtocol instance.
38
43
  actiontable_serializer: Action table serializer.
39
44
  telegram_service: Telegram service for parsing.
40
45
  """
41
- super().__init__(cli_config, reactor)
46
+ self.conbus_protocol = conbus_protocol
42
47
  self.serializer = actiontable_serializer
43
48
  self.telegram_service = telegram_service
44
49
  self.serial_number: str = ""
45
- self.progress_callback: Optional[Callable[[str], None]] = None
46
- self.error_callback: Optional[Callable[[str], None]] = None
47
- self.finish_callback: Optional[
48
- Callable[[ActionTable, Dict[str, Any], list[str]], None]
49
- ] = None
50
-
51
50
  self.actiontable_data: list[str] = []
52
51
  # Set up logging
53
52
  self.logger = logging.getLogger(__name__)
54
53
 
55
- def connection_established(self) -> None:
54
+ # Connect protocol signals
55
+ self.conbus_protocol.on_connection_made.connect(self.connection_made)
56
+ self.conbus_protocol.on_telegram_sent.connect(self.telegram_sent)
57
+ self.conbus_protocol.on_telegram_received.connect(self.telegram_received)
58
+ self.conbus_protocol.on_timeout.connect(self.timeout)
59
+ self.conbus_protocol.on_failed.connect(self.failed)
60
+
61
+ def connection_made(self) -> None:
56
62
  """Handle connection established event."""
57
63
  self.logger.debug(
58
64
  "Connection established, sending download actiontable telegram"
59
65
  )
60
- self.send_telegram(
66
+ self.conbus_protocol.send_telegram(
61
67
  telegram_type=TelegramType.SYSTEM,
62
68
  serial_number=self.serial_number,
63
69
  system_function=SystemFunction.DOWNLOAD_ACTIONTABLE,
@@ -101,10 +107,9 @@ class ActionTableService(ConbusProtocol):
101
107
  self.logger.debug("Saving actiontable response")
102
108
  data_part = reply_telegram.data_value[2:]
103
109
  self.actiontable_data.append(data_part)
104
- if self.progress_callback:
105
- self.progress_callback(".")
110
+ self.on_progress.emit(".")
106
111
 
107
- self.send_telegram(
112
+ self.conbus_protocol.send_telegram(
108
113
  telegram_type=TelegramType.SYSTEM,
109
114
  serial_number=self.serial_number,
110
115
  system_function=SystemFunction.ACK,
@@ -118,8 +123,12 @@ class ActionTableService(ConbusProtocol):
118
123
  actiontable = self.serializer.from_encoded_string(all_data)
119
124
  actiontable_dict = asdict(actiontable)
120
125
  actiontable_short = self.serializer.format_decoded_output(actiontable)
121
- if self.finish_callback:
122
- self.finish_callback(actiontable, actiontable_dict, actiontable_short)
126
+ self.on_finish.emit((actiontable, actiontable_dict, actiontable_short))
127
+
128
+ def timeout(self) -> None:
129
+ """Handle timeout event."""
130
+ self.logger.debug("Timeout occurred")
131
+ self.failed("Timeout")
123
132
 
124
133
  def failed(self, message: str) -> None:
125
134
  """Handle failed connection event.
@@ -128,31 +137,64 @@ class ActionTableService(ConbusProtocol):
128
137
  message: Failure message.
129
138
  """
130
139
  self.logger.debug(f"Failed: {message}")
131
- if self.error_callback:
132
- self.error_callback(message)
140
+ self.on_error.emit(message)
133
141
 
134
142
  def start(
135
143
  self,
136
144
  serial_number: str,
137
- progress_callback: Callable[[str], None],
138
- error_callback: Callable[[str], None],
139
- finish_callback: Callable[[ActionTable, Dict[str, Any], list[str]], None],
140
145
  timeout_seconds: Optional[float] = None,
141
146
  ) -> None:
142
147
  """Run reactor in dedicated thread with its own event loop.
143
148
 
144
149
  Args:
145
150
  serial_number: Module serial number.
146
- progress_callback: Callback for progress updates.
147
- error_callback: Callback for errors.
148
- finish_callback: Callback when download completes.
149
151
  timeout_seconds: Optional timeout in seconds.
150
152
  """
151
153
  self.logger.info("Starting actiontable")
152
154
  self.serial_number = serial_number
153
155
  if timeout_seconds:
154
- self.timeout_seconds = timeout_seconds
155
- self.progress_callback = progress_callback
156
- self.error_callback = error_callback
157
- self.finish_callback = finish_callback
158
- self.start_reactor()
156
+ self.conbus_protocol.timeout_seconds = timeout_seconds
157
+ # Caller invokes start_reactor()
158
+
159
+ def set_timeout(self, timeout_seconds: float) -> None:
160
+ """Set operation timeout.
161
+
162
+ Args:
163
+ timeout_seconds: Timeout in seconds.
164
+ """
165
+ self.conbus_protocol.timeout_seconds = timeout_seconds
166
+
167
+ def start_reactor(self) -> None:
168
+ """Start the reactor."""
169
+ self.conbus_protocol.start_reactor()
170
+
171
+ def stop_reactor(self) -> None:
172
+ """Stop the reactor."""
173
+ self.conbus_protocol.stop_reactor()
174
+
175
+ def __enter__(self) -> "ActionTableService":
176
+ """Enter context manager - reset state for singleton reuse.
177
+
178
+ Returns:
179
+ Self for context manager protocol.
180
+ """
181
+ # Reset state for singleton reuse
182
+ self.actiontable_data = []
183
+ return self
184
+
185
+ def __exit__(
186
+ self, _exc_type: Optional[type], _exc_val: Optional[Exception], _exc_tb: Any
187
+ ) -> None:
188
+ """Exit context manager and disconnect signals."""
189
+ # Disconnect protocol signals
190
+ self.conbus_protocol.on_connection_made.disconnect(self.connection_made)
191
+ self.conbus_protocol.on_telegram_sent.disconnect(self.telegram_sent)
192
+ self.conbus_protocol.on_telegram_received.disconnect(self.telegram_received)
193
+ self.conbus_protocol.on_timeout.disconnect(self.timeout)
194
+ self.conbus_protocol.on_failed.disconnect(self.failed)
195
+ # Disconnect service signals
196
+ self.on_progress.disconnect()
197
+ self.on_error.disconnect()
198
+ self.on_finish.disconnect()
199
+ # Stop reactor
200
+ self.stop_reactor()
@@ -2,7 +2,9 @@
2
2
 
3
3
  import logging
4
4
  from pathlib import Path
5
- from typing import Any, Callable, Optional
5
+ from typing import Any, Optional
6
+
7
+ from psygnal import Signal
6
8
 
7
9
 
8
10
  class ActionTableListService:
@@ -10,13 +12,18 @@ class ActionTableListService:
10
12
 
11
13
  Reads conson.yml and returns a list of all modules that have action table
12
14
  configurations defined.
15
+
16
+ Attributes:
17
+ on_finish: Signal emitted with dict[str, Any] when listing completes.
18
+ on_error: Signal emitted with error message string when an error occurs.
13
19
  """
14
20
 
21
+ on_finish: Signal = Signal(object) # dict[str, Any]
22
+ on_error: Signal = Signal(str)
23
+
15
24
  def __init__(self) -> None:
16
25
  """Initialize the action table list service."""
17
26
  self.logger = logging.getLogger(__name__)
18
- self.finish_callback: Optional[Callable[[dict[str, Any]], None]] = None
19
- self.error_callback: Optional[Callable[[str], None]] = None
20
27
 
21
28
  def __enter__(self) -> "ActionTableListService":
22
29
  """Context manager entry.
@@ -28,24 +35,19 @@ class ActionTableListService:
28
35
 
29
36
  def __exit__(self, _exc_type: Any, _exc_val: Any, _exc_tb: Any) -> None:
30
37
  """Context manager exit."""
31
- pass
38
+ # Disconnect service signals
39
+ self.on_finish.disconnect()
40
+ self.on_error.disconnect()
32
41
 
33
42
  def start(
34
43
  self,
35
- finish_callback: Callable[[dict[str, Any]], None],
36
- error_callback: Callable[[str], None],
37
44
  config_path: Optional[Path] = None,
38
45
  ) -> None:
39
46
  """List all modules with action table configurations.
40
47
 
41
48
  Args:
42
- finish_callback: Callback to invoke with the module list.
43
- error_callback: Callback to invoke on error.
44
49
  config_path: Optional path to conson.yml. Defaults to current directory.
45
50
  """
46
- self.finish_callback = finish_callback
47
- self.error_callback = error_callback
48
-
49
51
  # Default to current directory if not specified
50
52
  if config_path is None:
51
53
  config_path = Path.cwd() / "conson.yml"
@@ -77,15 +79,13 @@ class ActionTableListService:
77
79
  # Prepare result
78
80
  result = {"modules": modules_with_actiontable}
79
81
 
80
- # Invoke callback
81
- if self.finish_callback is not None:
82
- self.finish_callback(result)
82
+ # Emit finish signal
83
+ self.on_finish.emit(result)
83
84
 
84
85
  def _handle_error(self, message: str) -> None:
85
- """Handle error and invoke error callback.
86
+ """Handle error and emit error signal.
86
87
 
87
88
  Args:
88
89
  message: Error message.
89
90
  """
90
- if self.error_callback is not None:
91
- self.error_callback(message)
91
+ self.on_error.emit(message)
@@ -1,31 +1,38 @@
1
1
  """Service for uploading ActionTable via Conbus protocol."""
2
2
 
3
3
  import logging
4
- from typing import Any, Callable, Optional
4
+ from typing import Any, Optional
5
5
 
6
- from twisted.internet.posixbase import PosixReactorBase
6
+ from psygnal import Signal
7
7
 
8
- from xp.models import ConbusClientConfig
9
8
  from xp.models.homekit.homekit_conson_config import ConsonModuleListConfig
10
9
  from xp.models.protocol.conbus_protocol import TelegramReceivedEvent
11
10
  from xp.models.telegram.system_function import SystemFunction
12
11
  from xp.models.telegram.telegram_type import TelegramType
13
12
  from xp.services.actiontable.actiontable_serializer import ActionTableSerializer
14
- from xp.services.protocol import ConbusProtocol
13
+ from xp.services.protocol.conbus_event_protocol import ConbusEventProtocol
15
14
  from xp.services.telegram.telegram_service import TelegramService
16
15
 
17
16
 
18
- class ActionTableUploadService(ConbusProtocol):
17
+ class ActionTableUploadService:
19
18
  """TCP client service for uploading action tables to Conbus modules.
20
19
 
21
20
  Manages TCP socket connections, handles telegram generation and transmission,
22
21
  and processes server responses for action table uploads.
22
+
23
+ Attributes:
24
+ on_progress: Signal emitted with telegram frame when progress is made.
25
+ on_error: Signal emitted with error message string when an error occurs.
26
+ on_finish: Signal emitted with bool (True on success) when upload completes.
23
27
  """
24
28
 
29
+ on_progress: Signal = Signal(str)
30
+ on_error: Signal = Signal(str)
31
+ on_finish: Signal = Signal(bool) # True on success
32
+
25
33
  def __init__(
26
34
  self,
27
- cli_config: ConbusClientConfig,
28
- reactor: PosixReactorBase,
35
+ conbus_protocol: ConbusEventProtocol,
29
36
  actiontable_serializer: ActionTableSerializer,
30
37
  telegram_service: TelegramService,
31
38
  conson_config: ConsonModuleListConfig,
@@ -33,20 +40,16 @@ class ActionTableUploadService(ConbusProtocol):
33
40
  """Initialize the action table upload service.
34
41
 
35
42
  Args:
36
- cli_config: Conbus client configuration.
37
- reactor: Twisted reactor instance.
43
+ conbus_protocol: ConbusEventProtocol for communication.
38
44
  actiontable_serializer: Action table serializer.
39
45
  telegram_service: Telegram service for parsing.
40
46
  conson_config: Conson module list configuration.
41
47
  """
42
- super().__init__(cli_config, reactor)
48
+ self.conbus_protocol = conbus_protocol
43
49
  self.serializer = actiontable_serializer
44
50
  self.telegram_service = telegram_service
45
51
  self.conson_config = conson_config
46
52
  self.serial_number: str = ""
47
- self.progress_callback: Optional[Callable[[str], None]] = None
48
- self.error_callback: Optional[Callable[[str], None]] = None
49
- self.success_callback: Optional[Callable[[], None]] = None
50
53
 
51
54
  # Upload state
52
55
  self.upload_data_chunks: list[str] = []
@@ -55,10 +58,17 @@ class ActionTableUploadService(ConbusProtocol):
55
58
  # Set up logging
56
59
  self.logger = logging.getLogger(__name__)
57
60
 
58
- def connection_established(self) -> None:
61
+ # Connect protocol signals
62
+ self.conbus_protocol.on_connection_made.connect(self.connection_made)
63
+ self.conbus_protocol.on_telegram_sent.connect(self.telegram_sent)
64
+ self.conbus_protocol.on_telegram_received.connect(self.telegram_received)
65
+ self.conbus_protocol.on_timeout.connect(self.timeout)
66
+ self.conbus_protocol.on_failed.connect(self.failed)
67
+
68
+ def connection_made(self) -> None:
59
69
  """Handle connection established event."""
60
70
  self.logger.debug("Connection established, sending upload actiontable telegram")
61
- self.send_telegram(
71
+ self.conbus_protocol.send_telegram(
62
72
  telegram_type=TelegramType.SYSTEM,
63
73
  serial_number=self.serial_number,
64
74
  system_function=SystemFunction.UPLOAD_ACTIONTABLE,
@@ -112,33 +122,35 @@ class ActionTableUploadService(ConbusProtocol):
112
122
  # Second character: 'A' + chunk_index (sequential counter A-O for 15 chunks)
113
123
  prefix_hex = f"AAA{ord('A') + self.current_chunk_index:c}"
114
124
 
115
- self.send_telegram(
125
+ self.conbus_protocol.send_telegram(
116
126
  telegram_type=TelegramType.SYSTEM,
117
127
  serial_number=self.serial_number,
118
128
  system_function=SystemFunction.ACTIONTABLE,
119
129
  data_value=f"{prefix_hex}{chunk}",
120
130
  )
121
131
  self.current_chunk_index += 1
122
- if self.progress_callback:
123
- self.progress_callback(".")
132
+ self.on_progress.emit(".")
124
133
  else:
125
134
  # All chunks sent, send EOF
126
135
  self.logger.debug("All chunks sent, sending EOF")
127
- self.send_telegram(
136
+ self.conbus_protocol.send_telegram(
128
137
  telegram_type=TelegramType.SYSTEM,
129
138
  serial_number=self.serial_number,
130
139
  system_function=SystemFunction.EOF,
131
140
  data_value="00",
132
141
  )
133
- if self.success_callback:
134
- self.success_callback()
135
- self._stop_reactor()
142
+ self.on_finish.emit(True)
136
143
  elif reply_telegram.system_function == SystemFunction.NAK:
137
144
  self.logger.debug("Received NAK during upload")
138
145
  self.failed("Upload failed: NAK received")
139
146
  else:
140
147
  self.logger.debug(f"Unexpected response during upload: {reply_telegram}")
141
148
 
149
+ def timeout(self) -> None:
150
+ """Handle timeout event."""
151
+ self.logger.debug("Upload timeout")
152
+ self.failed("Upload timeout")
153
+
142
154
  def failed(self, message: str) -> None:
143
155
  """Handle failed connection event.
144
156
 
@@ -146,16 +158,11 @@ class ActionTableUploadService(ConbusProtocol):
146
158
  message: Failure message.
147
159
  """
148
160
  self.logger.debug(f"Failed: {message}")
149
- if self.error_callback:
150
- self.error_callback(message)
151
- self._stop_reactor()
161
+ self.on_error.emit(message)
152
162
 
153
163
  def start(
154
164
  self,
155
165
  serial_number: str,
156
- progress_callback: Callable[[str], None],
157
- error_callback: Callable[[str], None],
158
- success_callback: Callable[[], None],
159
166
  timeout_seconds: Optional[float] = None,
160
167
  ) -> None:
161
168
  """Upload action table to module.
@@ -164,18 +171,12 @@ class ActionTableUploadService(ConbusProtocol):
164
171
 
165
172
  Args:
166
173
  serial_number: Module serial number.
167
- progress_callback: Callback for progress updates.
168
- error_callback: Callback for errors.
169
- success_callback: Callback when upload completes successfully.
170
174
  timeout_seconds: Optional timeout in seconds.
171
175
  """
172
176
  self.logger.info("Starting actiontable upload")
173
177
  self.serial_number = serial_number
174
178
  if timeout_seconds:
175
- self.timeout_seconds = timeout_seconds
176
- self.progress_callback = progress_callback
177
- self.error_callback = error_callback
178
- self.success_callback = success_callback
179
+ self.conbus_protocol.timeout_seconds = timeout_seconds
179
180
 
180
181
  # Find module
181
182
  module = self.conson_config.find_module(serial_number)
@@ -208,4 +209,45 @@ class ActionTableUploadService(ConbusProtocol):
208
209
  f"{len(self.upload_data_chunks)} chunks"
209
210
  )
210
211
 
211
- self.start_reactor()
212
+ def set_timeout(self, timeout_seconds: float) -> None:
213
+ """Set operation timeout.
214
+
215
+ Args:
216
+ timeout_seconds: Timeout in seconds.
217
+ """
218
+ self.conbus_protocol.timeout_seconds = timeout_seconds
219
+
220
+ def start_reactor(self) -> None:
221
+ """Start the reactor."""
222
+ self.conbus_protocol.start_reactor()
223
+
224
+ def stop_reactor(self) -> None:
225
+ """Stop the reactor."""
226
+ self.conbus_protocol.stop_reactor()
227
+
228
+ def __enter__(self) -> "ActionTableUploadService":
229
+ """Enter context manager - reset state for singleton reuse.
230
+
231
+ Returns:
232
+ Self for context manager protocol.
233
+ """
234
+ # Reset state
235
+ self.upload_data_chunks = []
236
+ self.current_chunk_index = 0
237
+ self.serial_number = ""
238
+ return self
239
+
240
+ def __exit__(self, _exc_type: Any, _exc_val: Any, _exc_tb: Any) -> None:
241
+ """Exit context manager - cleanup signals and reactor."""
242
+ # Disconnect protocol signals
243
+ self.conbus_protocol.on_connection_made.disconnect(self.connection_made)
244
+ self.conbus_protocol.on_telegram_sent.disconnect(self.telegram_sent)
245
+ self.conbus_protocol.on_telegram_received.disconnect(self.telegram_received)
246
+ self.conbus_protocol.on_timeout.disconnect(self.timeout)
247
+ self.conbus_protocol.on_failed.disconnect(self.failed)
248
+ # Disconnect service signals
249
+ self.on_progress.disconnect()
250
+ self.on_error.disconnect()
251
+ self.on_finish.disconnect()
252
+ # Stop reactor
253
+ self.stop_reactor()