conson-xp 1.45.0__py3-none-any.whl → 1.47.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 (179) hide show
  1. {conson_xp-1.45.0.dist-info → conson_xp-1.47.0.dist-info}/METADATA +1 -1
  2. conson_xp-1.47.0.dist-info/RECORD +210 -0
  3. xp/__init__.py +3 -2
  4. xp/cli/commands/conbus/conbus.py +1 -1
  5. xp/cli/commands/conbus/conbus_actiontable_commands.py +33 -15
  6. xp/cli/commands/conbus/conbus_autoreport_commands.py +8 -4
  7. xp/cli/commands/conbus/conbus_blink_commands.py +20 -10
  8. xp/cli/commands/conbus/conbus_config_commands.py +2 -1
  9. xp/cli/commands/conbus/conbus_custom_commands.py +4 -2
  10. xp/cli/commands/conbus/conbus_datapoint_commands.py +10 -5
  11. xp/cli/commands/conbus/conbus_discover_commands.py +8 -4
  12. xp/cli/commands/conbus/conbus_event_commands.py +8 -4
  13. xp/cli/commands/conbus/conbus_export_commands.py +8 -4
  14. xp/cli/commands/conbus/conbus_lightlevel_commands.py +16 -8
  15. xp/cli/commands/conbus/conbus_linknumber_commands.py +8 -4
  16. xp/cli/commands/conbus/conbus_modulenumber_commands.py +8 -4
  17. xp/cli/commands/conbus/conbus_msactiontable_commands.py +78 -40
  18. xp/cli/commands/conbus/conbus_output_commands.py +16 -8
  19. xp/cli/commands/conbus/conbus_raw_commands.py +6 -3
  20. xp/cli/commands/conbus/conbus_receive_commands.py +6 -3
  21. xp/cli/commands/conbus/conbus_scan_commands.py +6 -3
  22. xp/cli/commands/file_commands.py +6 -3
  23. xp/cli/commands/homekit/homekit.py +4 -2
  24. xp/cli/commands/homekit/homekit_start_commands.py +2 -1
  25. xp/cli/commands/module_commands.py +8 -4
  26. xp/cli/commands/reverse_proxy_commands.py +8 -4
  27. xp/cli/commands/server/server_commands.py +6 -3
  28. xp/cli/commands/telegram/telegram_blink_commands.py +4 -2
  29. xp/cli/commands/telegram/telegram_checksum_commands.py +4 -2
  30. xp/cli/commands/telegram/telegram_discover_commands.py +2 -1
  31. xp/cli/commands/telegram/telegram_linknumber_commands.py +4 -2
  32. xp/cli/commands/telegram/telegram_parse_commands.py +4 -2
  33. xp/cli/commands/telegram/telegram_version_commands.py +2 -1
  34. xp/cli/commands/term/term_commands.py +4 -2
  35. xp/cli/main.py +2 -1
  36. xp/cli/utils/click_tree.py +6 -3
  37. xp/cli/utils/datapoint_type_choice.py +4 -2
  38. xp/cli/utils/decorators.py +42 -21
  39. xp/cli/utils/error_handlers.py +16 -8
  40. xp/cli/utils/formatters.py +22 -11
  41. xp/cli/utils/module_type_choice.py +4 -2
  42. xp/cli/utils/serial_number_type.py +4 -2
  43. xp/cli/utils/system_function_choice.py +4 -2
  44. xp/cli/utils/xp_module_type.py +4 -2
  45. xp/models/actiontable/actiontable.py +8 -8
  46. xp/models/actiontable/actiontable_type.py +20 -0
  47. xp/models/actiontable/msactiontable_xp20.py +8 -4
  48. xp/models/actiontable/msactiontable_xp24.py +12 -6
  49. xp/models/actiontable/msactiontable_xp33.py +20 -10
  50. xp/models/conbus/conbus.py +8 -4
  51. xp/models/conbus/conbus_autoreport.py +4 -2
  52. xp/models/conbus/conbus_blink.py +4 -2
  53. xp/models/conbus/conbus_client_config.py +6 -3
  54. xp/models/conbus/conbus_connection_status.py +4 -2
  55. xp/models/conbus/conbus_custom.py +4 -2
  56. xp/models/conbus/conbus_datapoint.py +4 -2
  57. xp/models/conbus/conbus_discover.py +6 -3
  58. xp/models/conbus/conbus_event_list.py +4 -2
  59. xp/models/conbus/conbus_event_raw.py +4 -2
  60. xp/models/conbus/conbus_export.py +2 -1
  61. xp/models/conbus/conbus_lightlevel.py +4 -2
  62. xp/models/conbus/conbus_linknumber.py +4 -2
  63. xp/models/conbus/conbus_logger_config.py +8 -4
  64. xp/models/conbus/conbus_output.py +4 -2
  65. xp/models/conbus/conbus_raw.py +4 -2
  66. xp/models/conbus/conbus_receive.py +4 -2
  67. xp/models/conbus/conbus_writeconfig.py +4 -2
  68. xp/models/config/conson_module_config.py +8 -4
  69. xp/models/homekit/homekit_accessory.py +4 -2
  70. xp/models/homekit/homekit_config.py +12 -6
  71. xp/models/log_entry.py +16 -8
  72. xp/models/protocol/conbus_protocol.py +36 -18
  73. xp/models/response.py +12 -8
  74. xp/models/telegram/action_type.py +4 -2
  75. xp/models/telegram/datapoint_type.py +4 -2
  76. xp/models/telegram/event_telegram.py +14 -7
  77. xp/models/telegram/event_type.py +2 -1
  78. xp/models/telegram/input_action_type.py +2 -1
  79. xp/models/telegram/input_type.py +2 -1
  80. xp/models/telegram/module_type.py +24 -12
  81. xp/models/telegram/module_type_code.py +2 -1
  82. xp/models/telegram/output_telegram.py +16 -10
  83. xp/models/telegram/reply_telegram.py +24 -13
  84. xp/models/telegram/system_function.py +6 -3
  85. xp/models/telegram/system_telegram.py +10 -6
  86. xp/models/telegram/telegram.py +2 -1
  87. xp/models/telegram/telegram_type.py +2 -1
  88. xp/models/telegram/timeparam_type.py +2 -1
  89. xp/models/term/connection_state.py +4 -2
  90. xp/models/term/module_state.py +2 -1
  91. xp/models/term/protocol_keys_config.py +6 -3
  92. xp/models/term/status_message.py +2 -1
  93. xp/models/term/telegram_display.py +2 -1
  94. xp/models/write_config_type.py +4 -2
  95. xp/services/actiontable/actiontable_serializer.py +34 -41
  96. xp/services/actiontable/download_state_machine.py +281 -0
  97. xp/services/actiontable/msactiontable_xp20_serializer.py +77 -49
  98. xp/services/actiontable/msactiontable_xp24_serializer.py +78 -53
  99. xp/services/actiontable/msactiontable_xp33_serializer.py +39 -9
  100. xp/services/actiontable/serializer_protocol.py +76 -0
  101. xp/services/conbus/actiontable/actiontable_download_service.py +134 -280
  102. xp/services/conbus/actiontable/actiontable_list_service.py +17 -4
  103. xp/services/conbus/actiontable/actiontable_show_service.py +10 -6
  104. xp/services/conbus/actiontable/actiontable_upload_service.py +17 -9
  105. xp/services/conbus/conbus_blink_all_service.py +16 -8
  106. xp/services/conbus/conbus_blink_service.py +14 -7
  107. xp/services/conbus/conbus_custom_service.py +16 -8
  108. xp/services/conbus/conbus_datapoint_queryall_service.py +18 -9
  109. xp/services/conbus/conbus_datapoint_service.py +18 -9
  110. xp/services/conbus/conbus_discover_service.py +24 -13
  111. xp/services/conbus/conbus_event_list_service.py +11 -7
  112. xp/services/conbus/conbus_event_raw_service.py +18 -10
  113. xp/services/conbus/conbus_export_service.py +28 -14
  114. xp/services/conbus/conbus_output_service.py +18 -10
  115. xp/services/conbus/conbus_raw_service.py +16 -8
  116. xp/services/conbus/conbus_receive_service.py +18 -10
  117. xp/services/conbus/conbus_scan_service.py +18 -10
  118. xp/services/conbus/msactiontable/msactiontable_upload_service.py +17 -9
  119. xp/services/conbus/write_config_service.py +18 -9
  120. xp/services/homekit/homekit_cache_service.py +12 -6
  121. xp/services/homekit/homekit_conbus_service.py +12 -6
  122. xp/services/homekit/homekit_config_validator.py +34 -17
  123. xp/services/homekit/homekit_conson_validator.py +18 -9
  124. xp/services/homekit/homekit_dimminglight.py +14 -7
  125. xp/services/homekit/homekit_dimminglight_service.py +14 -7
  126. xp/services/homekit/homekit_hap_service.py +18 -9
  127. xp/services/homekit/homekit_lightbulb.py +10 -5
  128. xp/services/homekit/homekit_lightbulb_service.py +10 -5
  129. xp/services/homekit/homekit_module_service.py +8 -4
  130. xp/services/homekit/homekit_outlet.py +14 -7
  131. xp/services/homekit/homekit_outlet_service.py +12 -6
  132. xp/services/homekit/homekit_service.py +24 -12
  133. xp/services/log_file_service.py +16 -8
  134. xp/services/module_type_service.py +10 -5
  135. xp/services/protocol/conbus_event_protocol.py +140 -21
  136. xp/services/protocol/conbus_protocol.py +36 -19
  137. xp/services/protocol/protocol_factory.py +12 -6
  138. xp/services/protocol/telegram_protocol.py +12 -6
  139. xp/services/reverse_proxy_service.py +26 -14
  140. xp/services/server/base_server_service.py +42 -23
  141. xp/services/server/client_buffer_manager.py +12 -7
  142. xp/services/server/cp20_server_service.py +10 -7
  143. xp/services/server/device_service_factory.py +12 -8
  144. xp/services/server/server_service.py +18 -11
  145. xp/services/server/xp130_server_service.py +11 -8
  146. xp/services/server/xp20_server_service.py +16 -10
  147. xp/services/server/xp230_server_service.py +10 -7
  148. xp/services/server/xp24_server_service.py +22 -13
  149. xp/services/server/xp33_server_service.py +44 -25
  150. xp/services/telegram/telegram_blink_service.py +14 -8
  151. xp/services/telegram/telegram_checksum_service.py +12 -7
  152. xp/services/telegram/telegram_datapoint_service.py +14 -9
  153. xp/services/telegram/telegram_discover_service.py +28 -15
  154. xp/services/telegram/telegram_link_number_service.py +18 -10
  155. xp/services/telegram/telegram_output_service.py +24 -12
  156. xp/services/telegram/telegram_service.py +22 -11
  157. xp/services/telegram/telegram_version_service.py +14 -8
  158. xp/services/term/protocol_monitor_service.py +30 -16
  159. xp/services/term/state_monitor_service.py +39 -21
  160. xp/term/protocol.py +12 -6
  161. xp/term/state.py +12 -7
  162. xp/term/widgets/help_menu.py +6 -3
  163. xp/term/widgets/modules_list.py +20 -10
  164. xp/term/widgets/protocol_log.py +12 -6
  165. xp/term/widgets/status_footer.py +10 -5
  166. xp/utils/checksum.py +6 -3
  167. xp/utils/dependencies.py +26 -31
  168. xp/utils/event_helper.py +6 -4
  169. xp/utils/logging.py +6 -3
  170. xp/utils/serialization.py +30 -16
  171. xp/utils/state_machine.py +16 -9
  172. xp/utils/time_utils.py +6 -3
  173. conson_xp-1.45.0.dist-info/RECORD +0 -210
  174. xp/services/conbus/msactiontable/msactiontable_download_service.py +0 -275
  175. xp/services/conbus/msactiontable/msactiontable_list_service.py +0 -100
  176. xp/services/conbus/msactiontable/msactiontable_show_service.py +0 -89
  177. {conson_xp-1.45.0.dist-info → conson_xp-1.47.0.dist-info}/WHEEL +0 -0
  178. {conson_xp-1.45.0.dist-info → conson_xp-1.47.0.dist-info}/entry_points.txt +0 -0
  179. {conson_xp-1.45.0.dist-info → conson_xp-1.47.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,104 +1,58 @@
1
1
  """Service for downloading ActionTable via Conbus protocol."""
2
2
 
3
3
  import logging
4
- from dataclasses import asdict
5
- from enum import Enum
6
4
  from typing import Any, Optional
7
5
 
8
- from psygnal import SignalInstance
9
- from statemachine import State, StateMachine
6
+ from psygnal import Signal
10
7
 
11
- from xp.models.actiontable.actiontable import ActionTable
8
+ from xp.models.actiontable.actiontable_type import ActionTableType
12
9
  from xp.models.protocol.conbus_protocol import TelegramReceivedEvent
13
10
  from xp.models.telegram.datapoint_type import DataPointType
14
11
  from xp.models.telegram.reply_telegram import ReplyTelegram
15
- from xp.models.telegram.system_function import SystemFunction
16
- from xp.models.telegram.telegram_type import TelegramType
17
12
  from xp.services.actiontable.actiontable_serializer import ActionTableSerializer
18
- from xp.services.protocol.conbus_event_protocol import ConbusEventProtocol
19
- from xp.services.telegram.telegram_service import TelegramService
20
-
21
- # Constants
22
- NO_ERROR_CODE = "00"
23
- CHUNK_HEADER_LENGTH = 2 # data_value format: 2-char counter + actiontable chunk
24
- MAX_ERROR_RETRIES = 3 # Max retries for error_status_received before giving up
25
-
26
-
27
- class Phase(Enum):
28
- """Download workflow phases.
29
-
30
- The download workflow consists of three sequential phases:
31
- - INIT: Drain pending telegrams, query error status → proceed to DOWNLOAD
32
- - DOWNLOAD: Request actiontable, receive chunks with ACK, until EOF
33
- - CLEANUP: Drain pending telegrams, query error status → proceed to COMPLETED
34
-
35
- Attributes:
36
- INIT: Initial phase - drain pending telegrams and query error status.
37
- DOWNLOAD: Download phase - request actiontable and receive chunks.
38
- CLEANUP: Cleanup phase - drain remaining telegrams and verify status.
13
+ from xp.services.actiontable.download_state_machine import (
14
+ MAX_ERROR_RETRIES,
15
+ DownloadStateMachine,
16
+ )
17
+ from xp.services.actiontable.msactiontable_xp20_serializer import (
18
+ Xp20MsActionTableSerializer,
19
+ )
20
+ from xp.services.actiontable.msactiontable_xp24_serializer import (
21
+ Xp24MsActionTableSerializer,
22
+ )
23
+ from xp.services.actiontable.msactiontable_xp33_serializer import (
24
+ Xp33MsActionTableSerializer,
25
+ )
26
+ from xp.services.actiontable.serializer_protocol import ActionTableSerializerProtocol
27
+ from xp.services.protocol.conbus_event_protocol import (
28
+ NO_ERROR_CODE,
29
+ ConbusEventProtocol,
30
+ )
31
+
32
+
33
+ class ActionTableDownloadService(DownloadStateMachine):
39
34
  """
35
+ Service for downloading action tables from Conbus modules via TCP.
40
36
 
41
- INIT = "init"
42
- DOWNLOAD = "download"
43
- CLEANUP = "cleanup"
44
-
45
-
46
- class ActionTableDownloadService(StateMachine):
47
- """Service for downloading action tables from Conbus modules via TCP.
48
-
49
- Inherits from StateMachine - the service IS the state machine. Uses guard
50
- conditions to share states between INIT and CLEANUP phases.
51
- see: Download-ActionTable-Workflow.dot
37
+ Inherits from ActionTableDownloadStateMachine and overrides on_enter_*
38
+ methods to add protocol-specific behavior.
52
39
 
53
- States (9 total):
54
- idle -> receiving -> resetting -> waiting_ok -> requesting
55
- -> waiting_data <-> receiving_chunk -> processing_eof -> completed
56
-
57
- Phases - INIT and CLEANUP share the same states (receiving, resetting, waiting_ok):
40
+ The workflow consists of three phases:
58
41
 
59
42
  INIT phase (drain → reset → wait_ok):
60
- idle -> receiving -> resetting -> waiting_ok --(guard: is_init_phase)--> requesting
43
+ Connection established, drain pending telegrams, query error status.
61
44
 
62
45
  DOWNLOAD phase (request → receive chunks → EOF):
63
- requesting -> waiting_data <-> receiving_chunk -> processing_eof
46
+ Request actiontable, receive and ACK chunks until EOF.
64
47
 
65
48
  CLEANUP phase (drain → reset → wait_ok):
66
- processing_eof -> receiving -> resetting -> waiting_ok --(guard: is_cleanup_phase)--> completed
67
-
68
- The drain/reset/wait_ok cycle:
69
- 1. Drain pending telegrams (receiving state discards telegram without reading them).
70
- There may be a lot of telegram. We are listening and receiving until no more
71
- telegram arrive and timeout occurs.
72
- 2. Timeout triggers error status query (resetting)
73
- 3. Wait for response (waiting_ok)
74
- 4. On no error: guard determines target (requesting or completed)
75
- On error: retry from drain step
49
+ After EOF, drain remaining telegrams and verify final status.
76
50
 
77
51
  Attributes:
78
52
  on_progress: Signal emitted with "." for each chunk received.
79
53
  on_error: Signal emitted with error message string.
80
54
  on_actiontable_received: Signal emitted with (ActionTable, dict, list).
81
55
  on_finish: Signal emitted when download and cleanup completed.
82
- idle: Initial state - waiting for connection.
83
- receiving: Drain telegrams state (INIT or CLEANUP phase).
84
- resetting: Query error status state.
85
- waiting_ok: Await error status response state.
86
- requesting: DOWNLOAD phase - send download request state.
87
- waiting_data: DOWNLOAD phase - await chunks state.
88
- receiving_chunk: DOWNLOAD phase - process chunk state.
89
- processing_eof: DOWNLOAD phase - deserialize result state.
90
- completed: Final state - download finished.
91
- do_connect: Transition from idle to receiving.
92
- filter_telegram: Self-transition in receiving to drain telegrams.
93
- do_timeout: Transition on timeout events.
94
- send_error_status: Transition from resetting to waiting_ok.
95
- error_status_received: Transition when error status is received.
96
- no_error_status_received: Transition when no error status received.
97
- send_download: Transition from requesting to waiting_data.
98
- receive_chunk: Transition from waiting_data to receiving_chunk.
99
- send_ack: Transition from receiving_chunk to waiting_data.
100
- receive_eof: Transition from waiting_data to processing_eof.
101
- do_finish: Transition from processing_eof to receiving (cleanup).
102
56
 
103
57
  Example:
104
58
  >>> with download_service as service:
@@ -107,141 +61,72 @@ class ActionTableDownloadService(StateMachine):
107
61
  ... service.start_reactor()
108
62
  """
109
63
 
110
- # States - unified for INIT and CLEANUP phases using guards
111
- idle = State(initial=True)
112
- receiving = State() # Drain telegrams (INIT or CLEANUP phase)
113
- resetting = State() # Query error status
114
- waiting_ok = State() # Await error status response
115
-
116
- requesting = State() # DOWNLOAD phase: send download request
117
- waiting_data = State() # DOWNLOAD phase: await chunks
118
- receiving_chunk = State() # DOWNLOAD phase: process chunk
119
- processing_eof = State() # DOWNLOAD phase: deserialize result
120
-
121
- completed = State(final=True)
122
-
123
- # Phase transitions - shared states with guards for phase-dependent routing
124
- do_connect = idle.to(receiving)
125
- filter_telegram = receiving.to(receiving) # Self-transition: drain to /dev/null
126
- do_timeout = receiving.to(resetting) | waiting_ok.to(receiving)
127
- send_error_status = resetting.to(waiting_ok)
128
- error_status_received = waiting_ok.to(
129
- receiving, cond="can_retry"
130
- ) # Retry if under limit
131
-
132
- # Conditional transitions based on phase
133
- no_error_status_received = waiting_ok.to(
134
- requesting, cond="is_init_phase"
135
- ) | waiting_ok.to(completed, cond="is_cleanup_phase")
136
-
137
- # DOWNLOAD phase transitions
138
- send_download = requesting.to(waiting_data)
139
- receive_chunk = waiting_data.to(receiving_chunk)
140
- send_ack = receiving_chunk.to(waiting_data)
141
- receive_eof = waiting_data.to(processing_eof)
142
-
143
- # Return to drain/reset cycle for CLEANUP phase
144
- do_finish = processing_eof.to(receiving)
64
+ # Service signals
65
+ on_progress: Signal = Signal(str)
66
+ on_error: Signal = Signal(str)
67
+ on_finish: Signal = Signal()
68
+ on_actiontable_received: Signal = Signal(Any, dict[str, Any], list[str])
145
69
 
146
70
  def __init__(
147
71
  self,
148
72
  conbus_protocol: ConbusEventProtocol,
149
73
  actiontable_serializer: ActionTableSerializer,
150
- telegram_service: TelegramService,
74
+ msactiontable_serializer_xp20: Xp20MsActionTableSerializer,
75
+ msactiontable_serializer_xp24: Xp24MsActionTableSerializer,
76
+ msactiontable_serializer_xp33: Xp33MsActionTableSerializer,
151
77
  ) -> None:
152
- """Initialize the action table download service.
78
+ """
79
+ Initialize the action table download service.
153
80
 
154
81
  Args:
155
82
  conbus_protocol: ConbusEventProtocol instance.
156
83
  actiontable_serializer: Action table serializer.
157
- telegram_service: Telegram service for parsing.
84
+ msactiontable_serializer_xp20: XP20 master station action table serializer.
85
+ msactiontable_serializer_xp24: XP24 master station action table serializer.
86
+ msactiontable_serializer_xp33: XP33 master station action table serializer.
158
87
  """
159
88
  self.conbus_protocol = conbus_protocol
160
- self.serializer = actiontable_serializer
161
- self.telegram_service = telegram_service
89
+ self.actiontable_serializer = actiontable_serializer
90
+ self.msactiontable_serializer_xp20 = msactiontable_serializer_xp20
91
+ self.msactiontable_serializer_xp24 = msactiontable_serializer_xp24
92
+ self.msactiontable_serializer_xp33 = msactiontable_serializer_xp33
93
+ self.serializer: ActionTableSerializerProtocol = actiontable_serializer
94
+
162
95
  self.serial_number: str = ""
163
96
  self.actiontable_data: list[str] = []
164
- self.logger = logging.getLogger(__name__)
165
- self._phase: Phase = Phase.INIT
166
- self._error_retry_count: int = 0
167
97
  self._signals_connected: bool = False
168
98
 
169
- # Signals (instance attributes to avoid conflict with statemachine)
170
- self.on_progress: SignalInstance = SignalInstance((str,))
171
- self.on_error: SignalInstance = SignalInstance((str,))
172
- self.on_finish: SignalInstance = SignalInstance()
173
- self.on_actiontable_received: SignalInstance = SignalInstance(
174
- (ActionTable, dict[str, Any], list[str])
175
- )
99
+ # Initialize state machine (must be last - triggers introspection)
100
+ super().__init__()
176
101
 
177
- # Initialize state machine first (before connecting signals)
178
- super().__init__(allow_event_without_transition=True)
102
+ # Override logger for service-specific logging
103
+ self.logger = logging.getLogger(__name__)
179
104
 
180
105
  # Connect protocol signals
181
106
  self._connect_signals()
182
107
 
183
- # Guard conditions for phase-dependent transitions
184
-
185
- def is_init_phase(self) -> bool:
186
- """Guard: check if currently in INIT phase.
187
-
188
- Returns:
189
- True if in INIT phase, False otherwise.
190
- """
191
- return self._phase == Phase.INIT
192
-
193
- def is_cleanup_phase(self) -> bool:
194
- """Guard: check if currently in CLEANUP phase.
195
-
196
- Returns:
197
- True if in CLEANUP phase, False otherwise.
198
- """
199
- return self._phase == Phase.CLEANUP
200
-
201
- def can_retry(self) -> bool:
202
- """Guard: check if retry is allowed (under max limit).
203
-
204
- Returns:
205
- True if retry count is below MAX_ERROR_RETRIES, False otherwise.
206
- """
207
- return self._error_retry_count < MAX_ERROR_RETRIES
208
-
209
- # State machine lifecycle hooks
210
- # Note: receiving state is used to drain pending telegrams from the connection
211
- # pipe. Any telegram received in this state is intentionally discarded (sent
212
- # to /dev/null) to ensure a clean state before processing.
108
+ # Override state entry hooks with protocol behavior
213
109
 
214
110
  def on_enter_receiving(self) -> None:
215
- """Enter receiving state - drain pending telegrams."""
216
- self.logger.debug(f"Entering RECEIVING state (phase={self._phase.value})")
111
+ """Enter receiving state - wait for telegrams to drain."""
112
+ self.logger.debug(f"Entering RECEIVING state (phase={self.phase.value})")
217
113
  self.conbus_protocol.wait()
218
114
 
219
115
  def on_enter_resetting(self) -> None:
220
- """Enter resetting state - query error status."""
221
- self.logger.debug(f"Entering RESETTING state (phase={self._phase.value})")
222
- self.conbus_protocol.send_telegram(
223
- telegram_type=TelegramType.SYSTEM,
224
- serial_number=self.serial_number,
225
- system_function=SystemFunction.READ_DATAPOINT,
226
- data_value=DataPointType.MODULE_ERROR_CODE.value,
227
- )
116
+ """Enter resetting state - send error status query."""
117
+ self.logger.debug(f"Entering RESETTING state (phase={self.phase.value})")
118
+ self.conbus_protocol.send_error_status_query(serial_number=self.serial_number)
228
119
  self.send_error_status()
229
120
 
230
121
  def on_enter_waiting_ok(self) -> None:
231
- """Enter waiting_ok state - awaiting error status response."""
232
- self.logger.debug(f"Entering WAITING_OK state (phase={self._phase.value})")
122
+ """Enter waiting_ok state - wait for error status response."""
123
+ self.logger.debug(f"Entering WAITING_OK state (phase={self.phase.value})")
233
124
  self.conbus_protocol.wait()
234
125
 
235
126
  def on_enter_requesting(self) -> None:
236
127
  """Enter requesting state - send download request."""
237
- self._phase = Phase.DOWNLOAD
238
- self.logger.debug("Entering REQUESTING state - sending download request")
239
- self.conbus_protocol.send_telegram(
240
- telegram_type=TelegramType.SYSTEM,
241
- serial_number=self.serial_number,
242
- system_function=SystemFunction.DOWNLOAD_ACTIONTABLE,
243
- data_value=NO_ERROR_CODE,
244
- )
128
+ self.enter_download_phase() # Sets phase to DOWNLOAD
129
+ self.conbus_protocol.send_download_request(serial_number=self.serial_number)
245
130
  self.send_download()
246
131
 
247
132
  def on_enter_waiting_data(self) -> None:
@@ -252,33 +137,29 @@ class ActionTableDownloadService(StateMachine):
252
137
  def on_enter_receiving_chunk(self) -> None:
253
138
  """Enter receiving_chunk state - send ACK."""
254
139
  self.logger.debug("Entering RECEIVING_CHUNK state - sending ACK")
255
- self.conbus_protocol.send_telegram(
256
- telegram_type=TelegramType.SYSTEM,
257
- serial_number=self.serial_number,
258
- system_function=SystemFunction.ACK,
259
- data_value=NO_ERROR_CODE,
260
- )
140
+ self.conbus_protocol.send_ack(serial_number=self.serial_number)
261
141
  self.send_ack()
262
142
 
263
143
  def on_enter_processing_eof(self) -> None:
264
- """Enter processing_eof state - deserialize and emit result, then cleanup."""
144
+ """Enter processing_eof state - deserialize and emit result."""
265
145
  self.logger.debug("Entering PROCESSING_EOF state - deserializing")
266
146
  all_data = "".join(self.actiontable_data)
267
147
  actiontable = self.serializer.from_encoded_string(all_data)
268
- actiontable_dict = asdict(actiontable)
269
- actiontable_short = self.serializer.format_decoded_output(actiontable)
270
- self.on_actiontable_received.emit(
271
- actiontable, actiontable_dict, actiontable_short
272
- )
273
- # Switch to CLEANUP phase before returning to receiving state
274
- self._phase = Phase.CLEANUP
275
- self.do_finish()
148
+ actiontable_short = self.serializer.to_short_string(actiontable)
149
+ self.on_actiontable_received.emit(actiontable, actiontable_short)
150
+ # Switch to CLEANUP phase
151
+ self.start_cleanup_phase()
276
152
 
277
153
  def on_enter_completed(self) -> None:
278
- """Enter completed state - download finished."""
154
+ """Enter completed state - emit finish signal."""
279
155
  self.logger.debug("Entering COMPLETED state - download finished")
280
156
  self.on_finish.emit()
281
157
 
158
+ def on_max_retries_exceeded(self) -> None:
159
+ """Handle max retries exceeded - emit error signal."""
160
+ self.logger.error(f"Max error retries ({MAX_ERROR_RETRIES}) exceeded")
161
+ self.on_error.emit(f"Module error persists after {MAX_ERROR_RETRIES} retries")
162
+
282
163
  # Protocol event handlers
283
164
 
284
165
  def _on_connection_made(self) -> None:
@@ -287,26 +168,18 @@ class ActionTableDownloadService(StateMachine):
287
168
  if self.idle.is_active:
288
169
  self.do_connect()
289
170
 
290
- def _on_telegram_sent(self, telegram_sent: str) -> None:
291
- """Handle telegram sent event.
292
-
293
- Args:
294
- telegram_sent: The telegram that was sent.
295
- """
296
- self.logger.debug(f"Telegram sent: {telegram_sent}")
297
-
298
171
  def _on_read_datapoint_received(self, reply_telegram: ReplyTelegram) -> None:
299
- """Handle READ_DATAPOINT response for error status check.
172
+ """
173
+ Handle READ_DATAPOINT response for error status check.
300
174
 
301
175
  Args:
302
176
  reply_telegram: The parsed reply telegram.
303
177
  """
304
178
  self.logger.debug(f"Received READ_DATAPOINT in {self.current_state}")
179
+ if reply_telegram.serial_number != self.serial_number:
180
+ return
305
181
 
306
182
  if reply_telegram.datapoint_type != DataPointType.MODULE_ERROR_CODE:
307
- self.logger.debug(
308
- f"Filtered: not a MODULE_ERROR_CODE (got {reply_telegram.datapoint_type})"
309
- )
310
183
  return
311
184
 
312
185
  if not self.waiting_ok.is_active:
@@ -314,49 +187,46 @@ class ActionTableDownloadService(StateMachine):
314
187
 
315
188
  is_no_error = reply_telegram.data_value == NO_ERROR_CODE
316
189
  if is_no_error:
317
- self._error_retry_count = 0 # Reset on success
318
- self.no_error_status_received() # Guards determine target state
190
+ self.handle_no_error_received()
319
191
  else:
320
- self._error_retry_count += 1
321
- self.logger.debug(
322
- f"Error status received, retry {self._error_retry_count}/{MAX_ERROR_RETRIES}"
323
- )
324
- # Guard can_retry blocks transition if max retries exceeded
325
- self.error_status_received()
326
- # Check if guard blocked the transition (still in waiting_ok)
327
- if self.waiting_ok.is_active:
328
- self.logger.error(
329
- f"Max error retries ({MAX_ERROR_RETRIES}) exceeded, giving up"
330
- )
331
- self.on_error.emit(
332
- f"Module error persists after {MAX_ERROR_RETRIES} retries"
333
- )
334
-
335
- def _on_actiontable_chunk_received(self, reply_telegram: ReplyTelegram) -> None:
336
- """Handle actiontable chunk telegram received.
192
+ self.handle_error_received()
193
+
194
+ def _on_actiontable_chunk_received(
195
+ self, reply_telegram: ReplyTelegram, actiontable_chunk: str
196
+ ) -> None:
197
+ """
198
+ Handle actiontable chunk telegram received.
337
199
 
338
200
  Args:
339
201
  reply_telegram: The parsed reply telegram containing chunk data.
202
+ actiontable_chunk: The chunk data.
340
203
  """
341
204
  self.logger.debug(f"Received actiontable chunk in {self.current_state}")
205
+ if reply_telegram.serial_number != self.serial_number:
206
+ return
207
+
342
208
  if self.waiting_data.is_active:
343
- data_part = reply_telegram.data_value[CHUNK_HEADER_LENGTH:]
344
- self.actiontable_data.append(data_part)
209
+ self.actiontable_data.append(actiontable_chunk)
345
210
  self.on_progress.emit(".")
346
211
  self.receive_chunk()
347
212
 
348
- def _on_eof_received(self, _reply_telegram: ReplyTelegram) -> None:
349
- """Handle EOF telegram received.
213
+ def _on_eof_received(self, reply_telegram: ReplyTelegram) -> None:
214
+ """
215
+ Handle EOF telegram received.
350
216
 
351
217
  Args:
352
- _reply_telegram: The parsed reply telegram (unused).
218
+ reply_telegram: The parsed reply telegram (unused).
353
219
  """
354
220
  self.logger.debug(f"Received EOF in {self.current_state}")
221
+ if reply_telegram.serial_number != self.serial_number:
222
+ return
223
+
355
224
  if self.waiting_data.is_active:
356
225
  self.receive_eof()
357
226
 
358
227
  def _on_telegram_received(self, telegram_received: TelegramReceivedEvent) -> None:
359
- """Handle telegram received event.
228
+ """
229
+ Handle telegram received event.
360
230
 
361
231
  Args:
362
232
  telegram_received: The telegram received event.
@@ -369,42 +239,9 @@ class ActionTableDownloadService(StateMachine):
369
239
  self.filter_telegram()
370
240
  return
371
241
 
372
- # Filter invalid telegrams
373
- if not telegram_received.checksum_valid:
374
- self.logger.debug("Filtered: invalid checksum")
375
- return
376
-
377
- if telegram_received.telegram_type != TelegramType.REPLY.value:
378
- self.logger.debug(
379
- f"Filtered: not a reply (got {telegram_received.telegram_type})"
380
- )
381
- return
382
-
383
- if telegram_received.serial_number != self.serial_number:
384
- self.logger.debug(
385
- f"Filtered: wrong serial {telegram_received.serial_number} != {self.serial_number}"
386
- )
387
- return
388
-
389
- reply_telegram = self.telegram_service.parse_reply_telegram(
390
- telegram_received.frame
391
- )
392
-
393
- if reply_telegram.system_function == SystemFunction.READ_DATAPOINT:
394
- self._on_read_datapoint_received(reply_telegram)
395
- return
396
-
397
- if reply_telegram.system_function == SystemFunction.ACTIONTABLE:
398
- self._on_actiontable_chunk_received(reply_telegram)
399
- return
400
-
401
- if reply_telegram.system_function == SystemFunction.EOF:
402
- self._on_eof_received(reply_telegram)
403
- return
404
-
405
242
  def _on_timeout(self) -> None:
406
243
  """Handle timeout event."""
407
- self.logger.debug(f"Timeout occurred (phase={self._phase.value})")
244
+ self.logger.debug(f"Timeout occurred (phase={self.phase.value})")
408
245
  if self.receiving.is_active:
409
246
  self.do_timeout() # receiving -> resetting
410
247
  elif self.waiting_ok.is_active:
@@ -417,7 +254,8 @@ class ActionTableDownloadService(StateMachine):
417
254
  self.on_error.emit("Timeout")
418
255
 
419
256
  def _on_failed(self, message: str) -> None:
420
- """Handle failed connection event.
257
+ """
258
+ Handle failed connection event.
421
259
 
422
260
  Args:
423
261
  message: Failure message.
@@ -430,15 +268,18 @@ class ActionTableDownloadService(StateMachine):
430
268
  def configure(
431
269
  self,
432
270
  serial_number: str,
271
+ actiontable_type: ActionTableType,
433
272
  timeout_seconds: Optional[float] = 2.0,
434
273
  ) -> None:
435
- """Configure download parameters before starting.
274
+ """
275
+ Configure download parameters before starting.
436
276
 
437
277
  Sets the target module serial number and timeout. Call this before
438
278
  start_reactor() to configure the download target.
439
279
 
440
280
  Args:
441
281
  serial_number: Module serial number to download from.
282
+ actiontable_type: Type of action table to download.
442
283
  timeout_seconds: Timeout in seconds for each operation (default 2.0).
443
284
 
444
285
  Raises:
@@ -448,11 +289,20 @@ class ActionTableDownloadService(StateMachine):
448
289
  raise RuntimeError("Cannot configure while download in progress")
449
290
  self.logger.info("Configuring actiontable download")
450
291
  self.serial_number = serial_number
292
+ if actiontable_type == ActionTableType.ACTIONTABLE:
293
+ self.serializer = self.actiontable_serializer
294
+ elif actiontable_type == ActionTableType.MSACTIONTABLE_XP20:
295
+ self.serializer = self.msactiontable_serializer_xp20
296
+ elif actiontable_type == ActionTableType.MSACTIONTABLE_XP24:
297
+ self.serializer = self.msactiontable_serializer_xp24
298
+ elif actiontable_type == ActionTableType.MSACTIONTABLE_XP33:
299
+ self.serializer = self.msactiontable_serializer_xp33
451
300
  if timeout_seconds:
452
301
  self.conbus_protocol.timeout_seconds = timeout_seconds
453
302
 
454
303
  def set_timeout(self, timeout_seconds: float) -> None:
455
- """Set operation timeout.
304
+ """
305
+ Set operation timeout.
456
306
 
457
307
  Args:
458
308
  timeout_seconds: Timeout in seconds.
@@ -472,8 +322,14 @@ class ActionTableDownloadService(StateMachine):
472
322
  if self._signals_connected:
473
323
  return
474
324
  self.conbus_protocol.on_connection_made.connect(self._on_connection_made)
475
- self.conbus_protocol.on_telegram_sent.connect(self._on_telegram_sent)
476
325
  self.conbus_protocol.on_telegram_received.connect(self._on_telegram_received)
326
+ self.conbus_protocol.on_read_datapoint_received.connect(
327
+ self._on_read_datapoint_received
328
+ )
329
+ self.conbus_protocol.on_actiontable_chunk_received.connect(
330
+ self._on_actiontable_chunk_received
331
+ )
332
+ self.conbus_protocol.on_eof_received.connect(self._on_eof_received)
477
333
  self.conbus_protocol.on_timeout.connect(self._on_timeout)
478
334
  self.conbus_protocol.on_failed.connect(self._on_failed)
479
335
  self._signals_connected = True
@@ -483,8 +339,14 @@ class ActionTableDownloadService(StateMachine):
483
339
  if not self._signals_connected:
484
340
  return
485
341
  self.conbus_protocol.on_connection_made.disconnect(self._on_connection_made)
486
- self.conbus_protocol.on_telegram_sent.disconnect(self._on_telegram_sent)
487
342
  self.conbus_protocol.on_telegram_received.disconnect(self._on_telegram_received)
343
+ self.conbus_protocol.on_read_datapoint_received.disconnect(
344
+ self._on_read_datapoint_received
345
+ )
346
+ self.conbus_protocol.on_actiontable_chunk_received.disconnect(
347
+ self._on_actiontable_chunk_received
348
+ )
349
+ self.conbus_protocol.on_eof_received.disconnect(self._on_eof_received)
488
350
  self.conbus_protocol.on_timeout.disconnect(self._on_timeout)
489
351
  self.conbus_protocol.on_failed.disconnect(self._on_failed)
490
352
  self._signals_connected = False
@@ -497,20 +359,12 @@ class ActionTableDownloadService(StateMachine):
497
359
  """
498
360
  # Reset state for singleton reuse
499
361
  self.actiontable_data = []
500
- self._phase = Phase.INIT
501
- self._error_retry_count = 0
502
- # Reset state machine to idle
503
- self._reset_state()
362
+ # Reset state machine
363
+ self.reset()
504
364
  # Reconnect signals (in case previously disconnected)
505
365
  self._connect_signals()
506
366
  return self
507
367
 
508
- def _reset_state(self) -> None:
509
- """Reset state machine to initial state."""
510
- # python-statemachine uses model.state to track current state
511
- # Set it directly to the initial state id
512
- self.model.state = self.idle.id
513
-
514
368
  def __exit__(
515
369
  self, _exc_type: Optional[type], _exc_val: Optional[Exception], _exc_tb: Any
516
370
  ) -> None:
@@ -8,7 +8,8 @@ from psygnal import Signal
8
8
 
9
9
 
10
10
  class ActionTableListService:
11
- """Service for listing modules with action table configurations.
11
+ """
12
+ Service for listing modules with action table configurations.
12
13
 
13
14
  Reads conson.yml and returns a list of all modules that have action table
14
15
  configurations defined.
@@ -26,7 +27,8 @@ class ActionTableListService:
26
27
  self.logger = logging.getLogger(__name__)
27
28
 
28
29
  def __enter__(self) -> "ActionTableListService":
29
- """Context manager entry.
30
+ """
31
+ Context manager entry.
30
32
 
31
33
  Returns:
32
34
  Self for context manager use.
@@ -43,7 +45,8 @@ class ActionTableListService:
43
45
  self,
44
46
  config_path: Optional[Path] = None,
45
47
  ) -> None:
46
- """List all modules with action table configurations.
48
+ """
49
+ List all modules with action table configurations.
47
50
 
48
51
  Args:
49
52
  config_path: Optional path to conson.yml. Defaults to current directory.
@@ -73,6 +76,15 @@ class ActionTableListService:
73
76
  "serial_number": module.serial_number,
74
77
  "module_type": module.module_type,
75
78
  "action_table": len(module.action_table) if module.action_table else 0,
79
+ "msaction_table": (
80
+ 1
81
+ if (
82
+ module.xp20_msaction_table
83
+ or module.xp24_msaction_table
84
+ or module.xp33_msaction_table
85
+ )
86
+ else 0
87
+ ),
76
88
  }
77
89
  for module in config.root
78
90
  ]
@@ -84,7 +96,8 @@ class ActionTableListService:
84
96
  self.on_finish.emit(result)
85
97
 
86
98
  def _handle_error(self, message: str) -> None:
87
- """Handle error and emit error signal.
99
+ """
100
+ Handle error and emit error signal.
88
101
 
89
102
  Args:
90
103
  message: Error message.