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
@@ -5,7 +5,9 @@ import re
5
5
  from xp.models import ModuleTypeCode
6
6
  from xp.models.actiontable.actiontable import ActionTable, ActionTableEntry
7
7
  from xp.models.telegram.input_action_type import InputActionType
8
+ from xp.models.telegram.system_function import SystemFunction
8
9
  from xp.models.telegram.timeparam_type import TimeParam
10
+ from xp.services.actiontable.serializer_protocol import ActionTableSerializerProtocol
9
11
  from xp.utils.serialization import (
10
12
  byte_to_unsigned,
11
13
  de_bcd,
@@ -19,8 +21,9 @@ from xp.utils.serialization import (
19
21
  )
20
22
 
21
23
 
22
- class ActionTableSerializer:
23
- """Handles serialization/deserialization of ActionTable to/from telegrams.
24
+ class ActionTableSerializer(ActionTableSerializerProtocol):
25
+ """
26
+ Handles serialization/deserialization of ActionTable to/from telegrams.
24
27
 
25
28
  Attributes:
26
29
  MAX_ENTRIES: Maximum number of entries in an ActionTable (96).
@@ -29,15 +32,27 @@ class ActionTableSerializer:
29
32
  MAX_ENTRIES = 96 # ActionTable must always contain exactly 96 entries
30
33
 
31
34
  @staticmethod
32
- def from_data(data: bytes) -> ActionTable:
33
- """Deserialize telegram data to ActionTable.
35
+ def download_type() -> SystemFunction:
36
+ """
37
+ Get the download system function type.
38
+
39
+ Returns:
40
+ The download system function: DOWNLOAD_ACTIONTABLE
41
+ """
42
+ return SystemFunction.DOWNLOAD_ACTIONTABLE
43
+
44
+ @staticmethod
45
+ def from_encoded_string(encoded_data: str) -> ActionTable:
46
+ """
47
+ Deserialize telegram data to ActionTable.
34
48
 
35
49
  Args:
36
- data: Raw byte data from telegram
50
+ encoded_data: Raw byte data from telegram
37
51
 
38
52
  Returns:
39
53
  Decoded ActionTable
40
54
  """
55
+ data = de_nibbles(encoded_data)
41
56
  entries = []
42
57
 
43
58
  # Process data in 5-byte chunks
@@ -92,14 +107,15 @@ class ActionTableSerializer:
92
107
  return ActionTable(entries=entries)
93
108
 
94
109
  @staticmethod
95
- def to_data(action_table: ActionTable) -> bytes:
96
- """Serialize ActionTable to telegram byte data.
110
+ def to_encoded_string(action_table: ActionTable) -> str:
111
+ """
112
+ Convert ActionTable to base64-encoded string format.
97
113
 
98
114
  Args:
99
- action_table: ActionTable to serialize
115
+ action_table: ActionTable to encode
100
116
 
101
117
  Returns:
102
- Raw byte data for telegram (always 480 bytes for 96 entries)
118
+ Base64-encoded string representation
103
119
  """
104
120
  data = bytearray()
105
121
 
@@ -128,37 +144,12 @@ class ActionTableSerializer:
128
144
  for _ in range(ActionTableSerializer.MAX_ENTRIES - current_entries):
129
145
  data.extend(padding_bytes)
130
146
 
131
- return bytes(data)
132
-
133
- @staticmethod
134
- def to_encoded_string(action_table: ActionTable) -> str:
135
- """Convert ActionTable to base64-encoded string format.
136
-
137
- Args:
138
- action_table: ActionTable to encode
139
-
140
- Returns:
141
- Base64-encoded string representation
142
- """
143
- data = ActionTableSerializer.to_data(action_table)
144
147
  return nibbles(data)
145
148
 
146
149
  @staticmethod
147
- def from_encoded_string(encoded_data: str) -> ActionTable:
148
- """Convert base64-encoded string to ActionTable.
149
-
150
- Args:
151
- encoded_data: Base64-encoded string
152
-
153
- Returns:
154
- Decoded ActionTable
150
+ def to_short_string(action_table: ActionTable) -> list[str]:
155
151
  """
156
- data = de_nibbles(encoded_data)
157
- return ActionTableSerializer.from_data(data)
158
-
159
- @staticmethod
160
- def format_decoded_output(action_table: ActionTable) -> list[str]:
161
- """Format ActionTable as human-readable decoded output.
152
+ Format ActionTable as human-readable decoded output.
162
153
 
163
154
  Args:
164
155
  action_table: ActionTable to format
@@ -194,8 +185,9 @@ class ActionTableSerializer:
194
185
  return lines
195
186
 
196
187
  @staticmethod
197
- def parse_action_string(action_str: str) -> ActionTableEntry:
198
- """Parse action table entry from string format.
188
+ def _parse_action_string(action_str: str) -> ActionTableEntry:
189
+ """
190
+ Parse action table entry from string format.
199
191
 
200
192
  Args:
201
193
  action_str: String in format "CP20 0 0 > 1 OFF" or "CP20 0 1 > 1 ~ON"
@@ -257,8 +249,9 @@ class ActionTableSerializer:
257
249
  )
258
250
 
259
251
  @staticmethod
260
- def parse_action_table(action_strings: list[str]) -> ActionTable:
261
- """Parse action table from list of string entries.
252
+ def from_short_string(action_strings: list[str]) -> ActionTable:
253
+ """
254
+ Parse action table from short string representation.
262
255
 
263
256
  Args:
264
257
  action_strings: List of action strings from conson.yml
@@ -267,7 +260,7 @@ class ActionTableSerializer:
267
260
  Parsed ActionTable
268
261
  """
269
262
  entries = [
270
- ActionTableSerializer.parse_action_string(action_str)
263
+ ActionTableSerializer._parse_action_string(action_str)
271
264
  for action_str in action_strings
272
265
  ]
273
266
  return ActionTable(entries=entries)
@@ -0,0 +1,281 @@
1
+ """State machine for ActionTable download workflow."""
2
+
3
+ import logging
4
+ from abc import ABCMeta, abstractmethod
5
+ from enum import Enum
6
+
7
+ from statemachine import State, StateMachine
8
+ from statemachine.factory import StateMachineMetaclass
9
+
10
+
11
+ class AbstractStateMachineMeta(StateMachineMetaclass, ABCMeta):
12
+ """
13
+ Combined metaclass for abstract state machines.
14
+
15
+ Combines StateMachineMetaclass (for state machine introspection) with ABCMeta (for
16
+ abstract method enforcement).
17
+ """
18
+
19
+ pass
20
+
21
+
22
+ # Constants
23
+ MAX_ERROR_RETRIES = 3 # Max retries for error_status_received before giving up
24
+
25
+
26
+ class Phase(Enum):
27
+ """
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.
39
+ """
40
+
41
+ INIT = "init"
42
+ DOWNLOAD = "download"
43
+ CLEANUP = "cleanup"
44
+
45
+
46
+ class DownloadStateMachine(StateMachine, metaclass=AbstractStateMachineMeta):
47
+ """
48
+ State machine for ActionTable download workflow.
49
+
50
+ Pure state machine with states, transitions, and guards. Subclasses can
51
+ override on_enter_* methods to add protocol-specific behavior.
52
+
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):
58
+
59
+ INIT phase (drain → reset → wait_ok):
60
+ idle -> receiving -> resetting -> waiting_ok --(guard: is_init_phase)--> requesting
61
+
62
+ DOWNLOAD phase (request → receive chunks → EOF):
63
+ requesting -> waiting_data <-> receiving_chunk -> processing_eof
64
+
65
+ 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 telegrams)
70
+ 2. Timeout triggers error status query (resetting)
71
+ 3. Wait for response (waiting_ok)
72
+ 4. On no error: guard determines target (requesting or completed)
73
+ On error: retry from drain step (limited by MAX_ERROR_RETRIES)
74
+
75
+ Attributes:
76
+ phase: Current workflow phase (INIT, DOWNLOAD, CLEANUP).
77
+ error_retry_count: Current error retry count.
78
+ idle: Initial state before connection.
79
+ receiving: Drain pending telegrams state (INIT or CLEANUP phase).
80
+ resetting: Query error status state.
81
+ waiting_ok: Await error status response state.
82
+ requesting: DOWNLOAD phase state - send download request.
83
+ waiting_data: DOWNLOAD phase state - await chunks.
84
+ receiving_chunk: DOWNLOAD phase state - process chunk.
85
+ processing_eof: DOWNLOAD phase state - deserialize result.
86
+ completed: Final state - download finished.
87
+ do_connect: Transition from idle to receiving.
88
+ filter_telegram: Self-transition in receiving state for draining.
89
+ do_timeout: Timeout transitions from receiving/waiting_ok.
90
+ send_error_status: Transition from resetting to waiting_ok.
91
+ error_status_received: Transition from waiting_ok to receiving on error.
92
+ no_error_status_received: Conditional transition based on phase.
93
+ send_download: Transition from requesting to waiting_data.
94
+ receive_chunk: Transition from waiting_data to receiving_chunk.
95
+ send_ack: Transition from receiving_chunk to waiting_data.
96
+ receive_eof: Transition from waiting_data to processing_eof.
97
+ do_finish: Transition from processing_eof to receiving.
98
+ """
99
+
100
+ # States - unified for INIT and CLEANUP phases using guards
101
+ idle = State(initial=True)
102
+ receiving = State() # Drain telegrams (INIT or CLEANUP phase)
103
+ resetting = State() # Query error status
104
+ waiting_ok = State() # Await error status response
105
+
106
+ requesting = State() # DOWNLOAD phase: send download request
107
+ waiting_data = State() # DOWNLOAD phase: await chunks
108
+ receiving_chunk = State() # DOWNLOAD phase: process chunk
109
+ processing_eof = State() # DOWNLOAD phase: deserialize result
110
+
111
+ completed = State(final=True)
112
+
113
+ # Phase transitions - shared states with guards for phase-dependent routing
114
+ do_connect = idle.to(receiving)
115
+ filter_telegram = receiving.to(receiving) # Self-transition: drain to /dev/null
116
+ do_timeout = receiving.to(resetting) | waiting_ok.to(receiving)
117
+ send_error_status = resetting.to(waiting_ok)
118
+ error_status_received = waiting_ok.to(
119
+ receiving, cond="can_retry"
120
+ ) # Retry if under limit
121
+
122
+ # Conditional transitions based on phase
123
+ no_error_status_received = waiting_ok.to(
124
+ requesting, cond="is_init_phase"
125
+ ) | waiting_ok.to(completed, cond="is_cleanup_phase")
126
+
127
+ # DOWNLOAD phase transitions
128
+ send_download = requesting.to(waiting_data)
129
+ receive_chunk = waiting_data.to(receiving_chunk)
130
+ send_ack = receiving_chunk.to(waiting_data)
131
+ receive_eof = waiting_data.to(processing_eof)
132
+
133
+ # Return to drain/reset cycle for CLEANUP phase
134
+ do_finish = processing_eof.to(receiving)
135
+
136
+ def __init__(self) -> None:
137
+ """Initialize the state machine."""
138
+ self.logger = logging.getLogger(__name__)
139
+ self._phase: Phase = Phase.INIT
140
+ self._error_retry_count: int = 0
141
+
142
+ # Initialize state machine
143
+ super().__init__(allow_event_without_transition=True)
144
+
145
+ @property
146
+ def phase(self) -> Phase:
147
+ """Get current phase."""
148
+ return self._phase
149
+
150
+ @phase.setter
151
+ def phase(self, value: Phase) -> None:
152
+ """
153
+ Set current phase.
154
+
155
+ Args:
156
+ value: The phase value to set.
157
+ """
158
+ self._phase = value
159
+
160
+ @property
161
+ def error_retry_count(self) -> int:
162
+ """Get current error retry count."""
163
+ return self._error_retry_count
164
+
165
+ @error_retry_count.setter
166
+ def error_retry_count(self, value: int) -> None:
167
+ """
168
+ Set error retry count.
169
+
170
+ Args:
171
+ value: The error retry count value to set.
172
+ """
173
+ self._error_retry_count = value
174
+
175
+ # Guard conditions for phase-dependent transitions
176
+
177
+ def is_init_phase(self) -> bool:
178
+ """Guard: check if currently in INIT phase.
179
+
180
+ Returns:
181
+ True if in INIT phase, False otherwise.
182
+ """
183
+ return self._phase == Phase.INIT
184
+
185
+ def is_cleanup_phase(self) -> bool:
186
+ """Guard: check if currently in CLEANUP phase.
187
+
188
+ Returns:
189
+ True if in CLEANUP phase, False otherwise.
190
+ """
191
+ return self._phase == Phase.CLEANUP
192
+
193
+ def can_retry(self) -> bool:
194
+ """Guard: check if retry is allowed (under max limit).
195
+
196
+ Returns:
197
+ True if retry count is under MAX_ERROR_RETRIES, False otherwise.
198
+ """
199
+ return self._error_retry_count < MAX_ERROR_RETRIES
200
+
201
+ # State entry hooks - subclasses MUST implement these
202
+
203
+ @abstractmethod
204
+ def on_enter_receiving(self) -> None:
205
+ """Enter receiving state - drain pending telegrams."""
206
+ ...
207
+
208
+ @abstractmethod
209
+ def on_enter_resetting(self) -> None:
210
+ """Enter resetting state - query error status."""
211
+ ...
212
+
213
+ @abstractmethod
214
+ def on_enter_waiting_ok(self) -> None:
215
+ """Enter waiting_ok state - awaiting error status response."""
216
+ ...
217
+
218
+ @abstractmethod
219
+ def on_enter_requesting(self) -> None:
220
+ """Enter requesting state - send download request."""
221
+ ...
222
+
223
+ @abstractmethod
224
+ def on_enter_waiting_data(self) -> None:
225
+ """Enter waiting_data state - wait for actiontable chunks."""
226
+ ...
227
+
228
+ @abstractmethod
229
+ def on_enter_receiving_chunk(self) -> None:
230
+ """Enter receiving_chunk state - send ACK."""
231
+ ...
232
+
233
+ @abstractmethod
234
+ def on_enter_processing_eof(self) -> None:
235
+ """Enter processing_eof state - deserialize and emit result."""
236
+ ...
237
+
238
+ @abstractmethod
239
+ def on_enter_completed(self) -> None:
240
+ """Enter completed state - download finished."""
241
+ ...
242
+
243
+ @abstractmethod
244
+ def on_max_retries_exceeded(self) -> None:
245
+ """Called when max error retries exceeded."""
246
+ ...
247
+
248
+ # Public methods for state machine control
249
+
250
+ def enter_download_phase(self) -> None:
251
+ """Enter requesting state - send download request."""
252
+ self._phase = Phase.DOWNLOAD
253
+
254
+ def handle_no_error_received(self) -> None:
255
+ """Handle successful error status check (no error)."""
256
+ self._error_retry_count = 0 # Reset on success
257
+ self.no_error_status_received()
258
+
259
+ def handle_error_received(self) -> None:
260
+ """Handle error status received - increment retry and attempt transition."""
261
+ self._error_retry_count += 1
262
+ self.logger.debug(
263
+ f"Error status received, retry {self._error_retry_count}/{MAX_ERROR_RETRIES}"
264
+ )
265
+ # Guard can_retry blocks transition if max retries exceeded
266
+ self.error_status_received()
267
+ # Check if guard blocked the transition (still in waiting_ok)
268
+ if self.waiting_ok.is_active:
269
+ self.on_max_retries_exceeded()
270
+
271
+ def start_cleanup_phase(self) -> None:
272
+ """Switch to CLEANUP phase and trigger do_finish transition."""
273
+ self._phase = Phase.CLEANUP
274
+ self.do_finish()
275
+
276
+ def reset(self) -> None:
277
+ """Reset state machine to initial state."""
278
+ self._phase = Phase.INIT
279
+ self._error_retry_count = 0
280
+ # python-statemachine uses model.state to track current state
281
+ self.model.state = self.idle.id
@@ -1,6 +1,8 @@
1
1
  """Serializer for XP20 Action Table telegram encoding/decoding."""
2
2
 
3
3
  from xp.models.actiontable.msactiontable_xp20 import InputChannel, Xp20MsActionTable
4
+ from xp.models.telegram.system_function import SystemFunction
5
+ from xp.services.actiontable.serializer_protocol import ActionTableSerializerProtocol
4
6
  from xp.utils.serialization import byte_to_bits, de_nibbles, nibbles
5
7
 
6
8
  # Index constants for clarity in implementation
@@ -12,24 +14,70 @@ SA_FUNCTION_INDEX: int = 11
12
14
  TA_FUNCTION_INDEX: int = 12
13
15
 
14
16
 
15
- class Xp20MsActionTableSerializer:
17
+ class Xp20MsActionTableSerializer(ActionTableSerializerProtocol):
16
18
  """Handles serialization/deserialization of XP20 action tables to/from telegrams."""
17
19
 
18
20
  @staticmethod
19
- def format_decoded_output(action_table: Xp20MsActionTable) -> list[str]:
20
- """Serialize XP20 action table to humane compact readable format.
21
+ def download_type() -> SystemFunction:
22
+ """
23
+ Get the download system function type.
24
+
25
+ Returns:
26
+ The download system function: DOWNLOAD_MSACTIONTABLE
27
+ """
28
+ return SystemFunction.DOWNLOAD_MSACTIONTABLE
29
+
30
+ @staticmethod
31
+ def from_encoded_string(encoded_data: str) -> Xp20MsActionTable:
32
+ """
33
+ Deserialize telegram data to XP20 action table.
21
34
 
22
35
  Args:
23
- action_table: XP20 action table to serialize
36
+ encoded_data: 64-character hex string with A-P encoding
24
37
 
25
38
  Returns:
26
- Human-readable string describing XP20 action table
39
+ Decoded XP20 action table
40
+
41
+ Raises:
42
+ ValueError: If input length is not 64 characters
27
43
  """
28
- return action_table.to_short_format()
44
+ raw_length = len(encoded_data)
45
+ if raw_length < 68: # Minimum: 4 char prefix + 64 chars data
46
+ raise ValueError(
47
+ f"XP20 action table data must be 68 characters long, got {len(encoded_data)}"
48
+ )
49
+
50
+ # Remove action table count prefix (first 4 characters: AAAA, AAAB, etc.)
51
+ data = encoded_data[4:]
52
+
53
+ # Take first 64 chars (32 bytes) as per pseudocode
54
+ hex_data = data[:64]
55
+ raw_bytes = de_nibbles(hex_data)
56
+
57
+ # Decode input channels
58
+ input_channels = []
59
+ for input_index in range(8):
60
+ input_channel = Xp20MsActionTableSerializer._decode_input_channel(
61
+ raw_bytes, input_index
62
+ )
63
+ input_channels.append(input_channel)
64
+
65
+ # Create and return XP20 action table
66
+ return Xp20MsActionTable(
67
+ input1=input_channels[0],
68
+ input2=input_channels[1],
69
+ input3=input_channels[2],
70
+ input4=input_channels[3],
71
+ input5=input_channels[4],
72
+ input6=input_channels[5],
73
+ input7=input_channels[6],
74
+ input8=input_channels[7],
75
+ )
29
76
 
30
77
  @staticmethod
31
- def to_data(action_table: Xp20MsActionTable) -> str:
32
- """Serialize XP20 action table to telegram hex string format.
78
+ def to_encoded_string(action_table: Xp20MsActionTable) -> str:
79
+ """
80
+ Serialize XP20 action table to telegram hex string format.
33
81
 
34
82
  Args:
35
83
  action_table: XP20 action table to serialize
@@ -63,56 +111,35 @@ class Xp20MsActionTableSerializer:
63
111
  return "AAAA" + encoded_data
64
112
 
65
113
  @staticmethod
66
- def from_data(msactiontable_rawdata: str) -> Xp20MsActionTable:
67
- """Deserialize telegram data to XP20 action table.
114
+ def to_short_string(action_table: Xp20MsActionTable) -> list[str]:
115
+ """
116
+ Serialize XP20 action table to humane compact readable format.
68
117
 
69
118
  Args:
70
- msactiontable_rawdata: 64-character hex string with A-P encoding
119
+ action_table: XP20 action table to serialize
71
120
 
72
121
  Returns:
73
- Decoded XP20 action table
74
-
75
- Raises:
76
- ValueError: If input length is not 64 characters
122
+ Human-readable string describing XP20 action table
77
123
  """
78
- raw_length = len(msactiontable_rawdata)
79
- if raw_length < 68: # Minimum: 4 char prefix + 64 chars data
80
- raise ValueError(
81
- f"XP20 action table data must be 68 characters long, got {len(msactiontable_rawdata)}"
82
- )
83
-
84
- # Remove action table count prefix (first 4 characters: AAAA, AAAB, etc.)
85
- data = msactiontable_rawdata[4:]
86
-
87
- # Take first 64 chars (32 bytes) as per pseudocode
88
- hex_data = data[:64]
124
+ return action_table.to_short_format()
89
125
 
90
- # Convert hex string to bytes using deNibble (A-P encoding)
91
- raw_bytes = de_nibbles(hex_data)
126
+ @staticmethod
127
+ def from_short_string(action_strings: list[str]) -> Xp20MsActionTable:
128
+ """
129
+ Parse XP20 action table from short string format.
92
130
 
93
- # Decode input channels
94
- input_channels = []
95
- for input_index in range(8):
96
- input_channel = Xp20MsActionTableSerializer._decode_input_channel(
97
- raw_bytes, input_index
98
- )
99
- input_channels.append(input_channel)
131
+ Args:
132
+ action_strings: List of short format strings to parse
100
133
 
101
- # Create and return XP20 action table
102
- return Xp20MsActionTable(
103
- input1=input_channels[0],
104
- input2=input_channels[1],
105
- input3=input_channels[2],
106
- input4=input_channels[3],
107
- input5=input_channels[4],
108
- input6=input_channels[5],
109
- input7=input_channels[6],
110
- input8=input_channels[7],
111
- )
134
+ Returns:
135
+ Parsed XP20 action table
136
+ """
137
+ return Xp20MsActionTable.from_short_format(action_strings)
112
138
 
113
139
  @staticmethod
114
- def _decode_input_channel(raw_bytes: bytearray, input_index: int) -> InputChannel:
115
- """Extract input channel configuration from raw bytes.
140
+ def _decode_input_channel(raw_bytes: bytes, input_index: int) -> InputChannel:
141
+ """
142
+ Extract input channel configuration from raw bytes.
116
143
 
117
144
  Args:
118
145
  raw_bytes: Raw byte array from telegram
@@ -146,7 +173,8 @@ class Xp20MsActionTableSerializer:
146
173
  def _encode_input_channel(
147
174
  input_channel: InputChannel, input_index: int, raw_bytes: bytearray
148
175
  ) -> None:
149
- """Encode input channel configuration into raw bytes.
176
+ """
177
+ Encode input channel configuration into raw bytes.
150
178
 
151
179
  Args:
152
180
  input_channel: Input channel configuration to encode