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.
- {conson_xp-1.45.0.dist-info → conson_xp-1.47.0.dist-info}/METADATA +1 -1
- conson_xp-1.47.0.dist-info/RECORD +210 -0
- xp/__init__.py +3 -2
- xp/cli/commands/conbus/conbus.py +1 -1
- xp/cli/commands/conbus/conbus_actiontable_commands.py +33 -15
- xp/cli/commands/conbus/conbus_autoreport_commands.py +8 -4
- xp/cli/commands/conbus/conbus_blink_commands.py +20 -10
- xp/cli/commands/conbus/conbus_config_commands.py +2 -1
- xp/cli/commands/conbus/conbus_custom_commands.py +4 -2
- xp/cli/commands/conbus/conbus_datapoint_commands.py +10 -5
- xp/cli/commands/conbus/conbus_discover_commands.py +8 -4
- xp/cli/commands/conbus/conbus_event_commands.py +8 -4
- xp/cli/commands/conbus/conbus_export_commands.py +8 -4
- xp/cli/commands/conbus/conbus_lightlevel_commands.py +16 -8
- xp/cli/commands/conbus/conbus_linknumber_commands.py +8 -4
- xp/cli/commands/conbus/conbus_modulenumber_commands.py +8 -4
- xp/cli/commands/conbus/conbus_msactiontable_commands.py +78 -40
- xp/cli/commands/conbus/conbus_output_commands.py +16 -8
- xp/cli/commands/conbus/conbus_raw_commands.py +6 -3
- xp/cli/commands/conbus/conbus_receive_commands.py +6 -3
- xp/cli/commands/conbus/conbus_scan_commands.py +6 -3
- xp/cli/commands/file_commands.py +6 -3
- xp/cli/commands/homekit/homekit.py +4 -2
- xp/cli/commands/homekit/homekit_start_commands.py +2 -1
- xp/cli/commands/module_commands.py +8 -4
- xp/cli/commands/reverse_proxy_commands.py +8 -4
- xp/cli/commands/server/server_commands.py +6 -3
- xp/cli/commands/telegram/telegram_blink_commands.py +4 -2
- xp/cli/commands/telegram/telegram_checksum_commands.py +4 -2
- xp/cli/commands/telegram/telegram_discover_commands.py +2 -1
- xp/cli/commands/telegram/telegram_linknumber_commands.py +4 -2
- xp/cli/commands/telegram/telegram_parse_commands.py +4 -2
- xp/cli/commands/telegram/telegram_version_commands.py +2 -1
- xp/cli/commands/term/term_commands.py +4 -2
- xp/cli/main.py +2 -1
- xp/cli/utils/click_tree.py +6 -3
- xp/cli/utils/datapoint_type_choice.py +4 -2
- xp/cli/utils/decorators.py +42 -21
- xp/cli/utils/error_handlers.py +16 -8
- xp/cli/utils/formatters.py +22 -11
- xp/cli/utils/module_type_choice.py +4 -2
- xp/cli/utils/serial_number_type.py +4 -2
- xp/cli/utils/system_function_choice.py +4 -2
- xp/cli/utils/xp_module_type.py +4 -2
- xp/models/actiontable/actiontable.py +8 -8
- xp/models/actiontable/actiontable_type.py +20 -0
- xp/models/actiontable/msactiontable_xp20.py +8 -4
- xp/models/actiontable/msactiontable_xp24.py +12 -6
- xp/models/actiontable/msactiontable_xp33.py +20 -10
- xp/models/conbus/conbus.py +8 -4
- xp/models/conbus/conbus_autoreport.py +4 -2
- xp/models/conbus/conbus_blink.py +4 -2
- xp/models/conbus/conbus_client_config.py +6 -3
- xp/models/conbus/conbus_connection_status.py +4 -2
- xp/models/conbus/conbus_custom.py +4 -2
- xp/models/conbus/conbus_datapoint.py +4 -2
- xp/models/conbus/conbus_discover.py +6 -3
- xp/models/conbus/conbus_event_list.py +4 -2
- xp/models/conbus/conbus_event_raw.py +4 -2
- xp/models/conbus/conbus_export.py +2 -1
- xp/models/conbus/conbus_lightlevel.py +4 -2
- xp/models/conbus/conbus_linknumber.py +4 -2
- xp/models/conbus/conbus_logger_config.py +8 -4
- xp/models/conbus/conbus_output.py +4 -2
- xp/models/conbus/conbus_raw.py +4 -2
- xp/models/conbus/conbus_receive.py +4 -2
- xp/models/conbus/conbus_writeconfig.py +4 -2
- xp/models/config/conson_module_config.py +8 -4
- xp/models/homekit/homekit_accessory.py +4 -2
- xp/models/homekit/homekit_config.py +12 -6
- xp/models/log_entry.py +16 -8
- xp/models/protocol/conbus_protocol.py +36 -18
- xp/models/response.py +12 -8
- xp/models/telegram/action_type.py +4 -2
- xp/models/telegram/datapoint_type.py +4 -2
- xp/models/telegram/event_telegram.py +14 -7
- xp/models/telegram/event_type.py +2 -1
- xp/models/telegram/input_action_type.py +2 -1
- xp/models/telegram/input_type.py +2 -1
- xp/models/telegram/module_type.py +24 -12
- xp/models/telegram/module_type_code.py +2 -1
- xp/models/telegram/output_telegram.py +16 -10
- xp/models/telegram/reply_telegram.py +24 -13
- xp/models/telegram/system_function.py +6 -3
- xp/models/telegram/system_telegram.py +10 -6
- xp/models/telegram/telegram.py +2 -1
- xp/models/telegram/telegram_type.py +2 -1
- xp/models/telegram/timeparam_type.py +2 -1
- xp/models/term/connection_state.py +4 -2
- xp/models/term/module_state.py +2 -1
- xp/models/term/protocol_keys_config.py +6 -3
- xp/models/term/status_message.py +2 -1
- xp/models/term/telegram_display.py +2 -1
- xp/models/write_config_type.py +4 -2
- xp/services/actiontable/actiontable_serializer.py +34 -41
- xp/services/actiontable/download_state_machine.py +281 -0
- xp/services/actiontable/msactiontable_xp20_serializer.py +77 -49
- xp/services/actiontable/msactiontable_xp24_serializer.py +78 -53
- xp/services/actiontable/msactiontable_xp33_serializer.py +39 -9
- xp/services/actiontable/serializer_protocol.py +76 -0
- xp/services/conbus/actiontable/actiontable_download_service.py +134 -280
- xp/services/conbus/actiontable/actiontable_list_service.py +17 -4
- xp/services/conbus/actiontable/actiontable_show_service.py +10 -6
- xp/services/conbus/actiontable/actiontable_upload_service.py +17 -9
- xp/services/conbus/conbus_blink_all_service.py +16 -8
- xp/services/conbus/conbus_blink_service.py +14 -7
- xp/services/conbus/conbus_custom_service.py +16 -8
- xp/services/conbus/conbus_datapoint_queryall_service.py +18 -9
- xp/services/conbus/conbus_datapoint_service.py +18 -9
- xp/services/conbus/conbus_discover_service.py +24 -13
- xp/services/conbus/conbus_event_list_service.py +11 -7
- xp/services/conbus/conbus_event_raw_service.py +18 -10
- xp/services/conbus/conbus_export_service.py +28 -14
- xp/services/conbus/conbus_output_service.py +18 -10
- xp/services/conbus/conbus_raw_service.py +16 -8
- xp/services/conbus/conbus_receive_service.py +18 -10
- xp/services/conbus/conbus_scan_service.py +18 -10
- xp/services/conbus/msactiontable/msactiontable_upload_service.py +17 -9
- xp/services/conbus/write_config_service.py +18 -9
- xp/services/homekit/homekit_cache_service.py +12 -6
- xp/services/homekit/homekit_conbus_service.py +12 -6
- xp/services/homekit/homekit_config_validator.py +34 -17
- xp/services/homekit/homekit_conson_validator.py +18 -9
- xp/services/homekit/homekit_dimminglight.py +14 -7
- xp/services/homekit/homekit_dimminglight_service.py +14 -7
- xp/services/homekit/homekit_hap_service.py +18 -9
- xp/services/homekit/homekit_lightbulb.py +10 -5
- xp/services/homekit/homekit_lightbulb_service.py +10 -5
- xp/services/homekit/homekit_module_service.py +8 -4
- xp/services/homekit/homekit_outlet.py +14 -7
- xp/services/homekit/homekit_outlet_service.py +12 -6
- xp/services/homekit/homekit_service.py +24 -12
- xp/services/log_file_service.py +16 -8
- xp/services/module_type_service.py +10 -5
- xp/services/protocol/conbus_event_protocol.py +140 -21
- xp/services/protocol/conbus_protocol.py +36 -19
- xp/services/protocol/protocol_factory.py +12 -6
- xp/services/protocol/telegram_protocol.py +12 -6
- xp/services/reverse_proxy_service.py +26 -14
- xp/services/server/base_server_service.py +42 -23
- xp/services/server/client_buffer_manager.py +12 -7
- xp/services/server/cp20_server_service.py +10 -7
- xp/services/server/device_service_factory.py +12 -8
- xp/services/server/server_service.py +18 -11
- xp/services/server/xp130_server_service.py +11 -8
- xp/services/server/xp20_server_service.py +16 -10
- xp/services/server/xp230_server_service.py +10 -7
- xp/services/server/xp24_server_service.py +22 -13
- xp/services/server/xp33_server_service.py +44 -25
- xp/services/telegram/telegram_blink_service.py +14 -8
- xp/services/telegram/telegram_checksum_service.py +12 -7
- xp/services/telegram/telegram_datapoint_service.py +14 -9
- xp/services/telegram/telegram_discover_service.py +28 -15
- xp/services/telegram/telegram_link_number_service.py +18 -10
- xp/services/telegram/telegram_output_service.py +24 -12
- xp/services/telegram/telegram_service.py +22 -11
- xp/services/telegram/telegram_version_service.py +14 -8
- xp/services/term/protocol_monitor_service.py +30 -16
- xp/services/term/state_monitor_service.py +39 -21
- xp/term/protocol.py +12 -6
- xp/term/state.py +12 -7
- xp/term/widgets/help_menu.py +6 -3
- xp/term/widgets/modules_list.py +20 -10
- xp/term/widgets/protocol_log.py +12 -6
- xp/term/widgets/status_footer.py +10 -5
- xp/utils/checksum.py +6 -3
- xp/utils/dependencies.py +26 -31
- xp/utils/event_helper.py +6 -4
- xp/utils/logging.py +6 -3
- xp/utils/serialization.py +30 -16
- xp/utils/state_machine.py +16 -9
- xp/utils/time_utils.py +6 -3
- conson_xp-1.45.0.dist-info/RECORD +0 -210
- xp/services/conbus/msactiontable/msactiontable_download_service.py +0 -275
- xp/services/conbus/msactiontable/msactiontable_list_service.py +0 -100
- xp/services/conbus/msactiontable/msactiontable_show_service.py +0 -89
- {conson_xp-1.45.0.dist-info → conson_xp-1.47.0.dist-info}/WHEEL +0 -0
- {conson_xp-1.45.0.dist-info → conson_xp-1.47.0.dist-info}/entry_points.txt +0 -0
- {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
|
-
"""
|
|
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
|
|
33
|
-
"""
|
|
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
|
-
|
|
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
|
|
96
|
-
"""
|
|
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
|
|
115
|
+
action_table: ActionTable to encode
|
|
100
116
|
|
|
101
117
|
Returns:
|
|
102
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
198
|
-
"""
|
|
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
|
|
261
|
-
"""
|
|
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.
|
|
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
|
|
20
|
-
"""
|
|
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
|
-
|
|
36
|
+
encoded_data: 64-character hex string with A-P encoding
|
|
24
37
|
|
|
25
38
|
Returns:
|
|
26
|
-
|
|
39
|
+
Decoded XP20 action table
|
|
40
|
+
|
|
41
|
+
Raises:
|
|
42
|
+
ValueError: If input length is not 64 characters
|
|
27
43
|
"""
|
|
28
|
-
|
|
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
|
|
32
|
-
"""
|
|
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
|
|
67
|
-
"""
|
|
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
|
-
|
|
119
|
+
action_table: XP20 action table to serialize
|
|
71
120
|
|
|
72
121
|
Returns:
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
Raises:
|
|
76
|
-
ValueError: If input length is not 64 characters
|
|
122
|
+
Human-readable string describing XP20 action table
|
|
77
123
|
"""
|
|
78
|
-
|
|
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
|
-
|
|
91
|
-
|
|
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
|
-
|
|
94
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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:
|
|
115
|
-
"""
|
|
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
|
-
"""
|
|
176
|
+
"""
|
|
177
|
+
Encode input channel configuration into raw bytes.
|
|
150
178
|
|
|
151
179
|
Args:
|
|
152
180
|
input_channel: Input channel configuration to encode
|