conson-xp 1.18.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 (176) hide show
  1. conson_xp-1.18.0.dist-info/METADATA +412 -0
  2. conson_xp-1.18.0.dist-info/RECORD +176 -0
  3. conson_xp-1.18.0.dist-info/WHEEL +4 -0
  4. conson_xp-1.18.0.dist-info/entry_points.txt +5 -0
  5. conson_xp-1.18.0.dist-info/licenses/LICENSE +29 -0
  6. xp/__init__.py +9 -0
  7. xp/cli/__init__.py +5 -0
  8. xp/cli/__main__.py +6 -0
  9. xp/cli/commands/__init__.py +153 -0
  10. xp/cli/commands/conbus/__init__.py +25 -0
  11. xp/cli/commands/conbus/conbus.py +128 -0
  12. xp/cli/commands/conbus/conbus_actiontable_commands.py +233 -0
  13. xp/cli/commands/conbus/conbus_autoreport_commands.py +108 -0
  14. xp/cli/commands/conbus/conbus_blink_commands.py +163 -0
  15. xp/cli/commands/conbus/conbus_config_commands.py +29 -0
  16. xp/cli/commands/conbus/conbus_custom_commands.py +57 -0
  17. xp/cli/commands/conbus/conbus_datapoint_commands.py +113 -0
  18. xp/cli/commands/conbus/conbus_discover_commands.py +61 -0
  19. xp/cli/commands/conbus/conbus_event_commands.py +81 -0
  20. xp/cli/commands/conbus/conbus_lightlevel_commands.py +207 -0
  21. xp/cli/commands/conbus/conbus_linknumber_commands.py +102 -0
  22. xp/cli/commands/conbus/conbus_modulenumber_commands.py +104 -0
  23. xp/cli/commands/conbus/conbus_msactiontable_commands.py +94 -0
  24. xp/cli/commands/conbus/conbus_output_commands.py +163 -0
  25. xp/cli/commands/conbus/conbus_raw_commands.py +62 -0
  26. xp/cli/commands/conbus/conbus_receive_commands.py +59 -0
  27. xp/cli/commands/conbus/conbus_scan_commands.py +58 -0
  28. xp/cli/commands/file_commands.py +186 -0
  29. xp/cli/commands/homekit/__init__.py +3 -0
  30. xp/cli/commands/homekit/homekit.py +118 -0
  31. xp/cli/commands/homekit/homekit_start_commands.py +43 -0
  32. xp/cli/commands/module_commands.py +187 -0
  33. xp/cli/commands/reverse_proxy_commands.py +178 -0
  34. xp/cli/commands/server/__init__.py +3 -0
  35. xp/cli/commands/server/server_commands.py +135 -0
  36. xp/cli/commands/telegram/__init__.py +5 -0
  37. xp/cli/commands/telegram/telegram.py +41 -0
  38. xp/cli/commands/telegram/telegram_blink_commands.py +79 -0
  39. xp/cli/commands/telegram/telegram_checksum_commands.py +112 -0
  40. xp/cli/commands/telegram/telegram_discover_commands.py +41 -0
  41. xp/cli/commands/telegram/telegram_linknumber_commands.py +86 -0
  42. xp/cli/commands/telegram/telegram_parse_commands.py +75 -0
  43. xp/cli/commands/telegram/telegram_version_commands.py +52 -0
  44. xp/cli/main.py +87 -0
  45. xp/cli/utils/__init__.py +1 -0
  46. xp/cli/utils/click_tree.py +57 -0
  47. xp/cli/utils/datapoint_type_choice.py +57 -0
  48. xp/cli/utils/decorators.py +351 -0
  49. xp/cli/utils/error_handlers.py +201 -0
  50. xp/cli/utils/formatters.py +312 -0
  51. xp/cli/utils/module_type_choice.py +56 -0
  52. xp/cli/utils/serial_number_type.py +52 -0
  53. xp/cli/utils/system_function_choice.py +57 -0
  54. xp/cli/utils/xp_module_type.py +53 -0
  55. xp/connection/__init__.py +13 -0
  56. xp/connection/exceptions.py +22 -0
  57. xp/models/__init__.py +36 -0
  58. xp/models/actiontable/__init__.py +1 -0
  59. xp/models/actiontable/actiontable.py +43 -0
  60. xp/models/actiontable/msactiontable_xp20.py +53 -0
  61. xp/models/actiontable/msactiontable_xp24.py +58 -0
  62. xp/models/actiontable/msactiontable_xp33.py +65 -0
  63. xp/models/conbus/__init__.py +1 -0
  64. xp/models/conbus/conbus.py +87 -0
  65. xp/models/conbus/conbus_autoreport.py +67 -0
  66. xp/models/conbus/conbus_blink.py +80 -0
  67. xp/models/conbus/conbus_client_config.py +55 -0
  68. xp/models/conbus/conbus_connection_status.py +40 -0
  69. xp/models/conbus/conbus_custom.py +58 -0
  70. xp/models/conbus/conbus_datapoint.py +89 -0
  71. xp/models/conbus/conbus_discover.py +64 -0
  72. xp/models/conbus/conbus_event_raw.py +47 -0
  73. xp/models/conbus/conbus_lightlevel.py +52 -0
  74. xp/models/conbus/conbus_linknumber.py +54 -0
  75. xp/models/conbus/conbus_output.py +57 -0
  76. xp/models/conbus/conbus_raw.py +45 -0
  77. xp/models/conbus/conbus_receive.py +42 -0
  78. xp/models/conbus/conbus_writeconfig.py +60 -0
  79. xp/models/homekit/__init__.py +1 -0
  80. xp/models/homekit/homekit_accessory.py +35 -0
  81. xp/models/homekit/homekit_config.py +106 -0
  82. xp/models/homekit/homekit_conson_config.py +86 -0
  83. xp/models/log_entry.py +130 -0
  84. xp/models/protocol/__init__.py +1 -0
  85. xp/models/protocol/conbus_protocol.py +312 -0
  86. xp/models/response.py +42 -0
  87. xp/models/telegram/__init__.py +1 -0
  88. xp/models/telegram/action_type.py +31 -0
  89. xp/models/telegram/datapoint_type.py +82 -0
  90. xp/models/telegram/event_telegram.py +140 -0
  91. xp/models/telegram/event_type.py +15 -0
  92. xp/models/telegram/input_action_type.py +69 -0
  93. xp/models/telegram/input_type.py +17 -0
  94. xp/models/telegram/module_type.py +188 -0
  95. xp/models/telegram/module_type_code.py +205 -0
  96. xp/models/telegram/output_telegram.py +103 -0
  97. xp/models/telegram/reply_telegram.py +297 -0
  98. xp/models/telegram/system_function.py +116 -0
  99. xp/models/telegram/system_telegram.py +94 -0
  100. xp/models/telegram/telegram.py +28 -0
  101. xp/models/telegram/telegram_type.py +19 -0
  102. xp/models/telegram/timeparam_type.py +51 -0
  103. xp/models/write_config_type.py +33 -0
  104. xp/services/__init__.py +26 -0
  105. xp/services/actiontable/__init__.py +1 -0
  106. xp/services/actiontable/actiontable_serializer.py +273 -0
  107. xp/services/actiontable/msactiontable_serializer.py +7 -0
  108. xp/services/actiontable/msactiontable_xp20_serializer.py +169 -0
  109. xp/services/actiontable/msactiontable_xp24_serializer.py +120 -0
  110. xp/services/actiontable/msactiontable_xp33_serializer.py +239 -0
  111. xp/services/conbus/__init__.py +1 -0
  112. xp/services/conbus/actiontable/__init__.py +1 -0
  113. xp/services/conbus/actiontable/actiontable_download_service.py +158 -0
  114. xp/services/conbus/actiontable/actiontable_list_service.py +91 -0
  115. xp/services/conbus/actiontable/actiontable_show_service.py +89 -0
  116. xp/services/conbus/actiontable/actiontable_upload_service.py +211 -0
  117. xp/services/conbus/actiontable/msactiontable_service.py +232 -0
  118. xp/services/conbus/conbus_blink_all_service.py +181 -0
  119. xp/services/conbus/conbus_blink_service.py +158 -0
  120. xp/services/conbus/conbus_custom_service.py +156 -0
  121. xp/services/conbus/conbus_datapoint_queryall_service.py +182 -0
  122. xp/services/conbus/conbus_datapoint_service.py +170 -0
  123. xp/services/conbus/conbus_discover_service.py +312 -0
  124. xp/services/conbus/conbus_event_raw_service.py +181 -0
  125. xp/services/conbus/conbus_output_service.py +194 -0
  126. xp/services/conbus/conbus_raw_service.py +122 -0
  127. xp/services/conbus/conbus_receive_service.py +115 -0
  128. xp/services/conbus/conbus_scan_service.py +150 -0
  129. xp/services/conbus/write_config_service.py +194 -0
  130. xp/services/homekit/__init__.py +1 -0
  131. xp/services/homekit/homekit_cache_service.py +307 -0
  132. xp/services/homekit/homekit_conbus_service.py +93 -0
  133. xp/services/homekit/homekit_config_validator.py +310 -0
  134. xp/services/homekit/homekit_conson_validator.py +121 -0
  135. xp/services/homekit/homekit_dimminglight.py +182 -0
  136. xp/services/homekit/homekit_dimminglight_service.py +148 -0
  137. xp/services/homekit/homekit_hap_service.py +342 -0
  138. xp/services/homekit/homekit_lightbulb.py +120 -0
  139. xp/services/homekit/homekit_lightbulb_service.py +86 -0
  140. xp/services/homekit/homekit_module_service.py +56 -0
  141. xp/services/homekit/homekit_outlet.py +168 -0
  142. xp/services/homekit/homekit_outlet_service.py +121 -0
  143. xp/services/homekit/homekit_service.py +359 -0
  144. xp/services/log_file_service.py +309 -0
  145. xp/services/module_type_service.py +257 -0
  146. xp/services/protocol/__init__.py +21 -0
  147. xp/services/protocol/conbus_event_protocol.py +360 -0
  148. xp/services/protocol/conbus_protocol.py +318 -0
  149. xp/services/protocol/protocol_factory.py +78 -0
  150. xp/services/protocol/telegram_protocol.py +264 -0
  151. xp/services/reverse_proxy_service.py +435 -0
  152. xp/services/server/__init__.py +1 -0
  153. xp/services/server/base_server_service.py +366 -0
  154. xp/services/server/cp20_server_service.py +65 -0
  155. xp/services/server/device_service_factory.py +94 -0
  156. xp/services/server/server_service.py +428 -0
  157. xp/services/server/xp130_server_service.py +67 -0
  158. xp/services/server/xp20_server_service.py +92 -0
  159. xp/services/server/xp230_server_service.py +58 -0
  160. xp/services/server/xp24_server_service.py +245 -0
  161. xp/services/server/xp33_server_service.py +535 -0
  162. xp/services/telegram/__init__.py +1 -0
  163. xp/services/telegram/telegram_blink_service.py +138 -0
  164. xp/services/telegram/telegram_checksum_service.py +149 -0
  165. xp/services/telegram/telegram_datapoint_service.py +82 -0
  166. xp/services/telegram/telegram_discover_service.py +277 -0
  167. xp/services/telegram/telegram_link_number_service.py +216 -0
  168. xp/services/telegram/telegram_output_service.py +322 -0
  169. xp/services/telegram/telegram_service.py +380 -0
  170. xp/services/telegram/telegram_version_service.py +288 -0
  171. xp/utils/__init__.py +12 -0
  172. xp/utils/checksum.py +61 -0
  173. xp/utils/dependencies.py +531 -0
  174. xp/utils/event_helper.py +31 -0
  175. xp/utils/serialization.py +205 -0
  176. xp/utils/time_utils.py +134 -0
@@ -0,0 +1,216 @@
1
+ """Service for link number telegram operations.
2
+
3
+ This service handles generation and parsing of link number system telegrams
4
+ used for setting and reading module link numbers.
5
+ """
6
+
7
+ from contextlib import suppress
8
+ from typing import Optional
9
+
10
+ from xp.models.telegram.datapoint_type import DataPointType
11
+ from xp.models.telegram.reply_telegram import ReplyTelegram
12
+ from xp.models.telegram.system_function import SystemFunction
13
+ from xp.models.telegram.system_telegram import SystemTelegram
14
+ from xp.utils.checksum import calculate_checksum
15
+
16
+
17
+ class LinkNumberError(Exception):
18
+ """Raised when link number operations fail."""
19
+
20
+ pass
21
+
22
+
23
+ class LinkNumberService:
24
+ """
25
+ Service for generating and handling link number system telegrams.
26
+
27
+ Handles telegrams for setting module link numbers using the F04D04 format:
28
+ <S{serial_number}F04D04{link_number}{checksum}>
29
+ """
30
+
31
+ def __init__(self) -> None:
32
+ """Initialize the link number service."""
33
+ pass
34
+
35
+ @staticmethod
36
+ def generate_set_link_number_telegram(serial_number: str, link_number: int) -> str:
37
+ """Generate a telegram to set a module's link number.
38
+
39
+ Args:
40
+ serial_number: The 10-digit module serial number.
41
+ link_number: The link number to set (0-99).
42
+
43
+ Returns:
44
+ Formatted telegram string (e.g., "<S0012345005F04D0425FO>").
45
+
46
+ Raises:
47
+ LinkNumberError: If parameters are invalid.
48
+ """
49
+ # Validate serial number
50
+ if not serial_number or len(serial_number) != 10:
51
+ raise LinkNumberError(
52
+ f"Serial number must be 10 digits, got: {serial_number}"
53
+ )
54
+
55
+ if not serial_number.isdigit():
56
+ raise LinkNumberError(
57
+ f"Serial number must contain only digits: {serial_number}"
58
+ )
59
+
60
+ # Validate link number range
61
+ if not (0 <= link_number <= 99):
62
+ raise LinkNumberError(
63
+ f"Link number must be between 0-99, got: {link_number}"
64
+ )
65
+
66
+ # Format link number with leading zero if needed
67
+ link_number_str = f"{link_number:02d}"
68
+
69
+ # Build the data part of the telegram
70
+ data_part = f"S{serial_number}F04D04{link_number_str}"
71
+
72
+ # Calculate checksum
73
+ checksum = calculate_checksum(data_part)
74
+
75
+ # Build complete telegram
76
+ telegram = f"<{data_part}{checksum}>"
77
+
78
+ return telegram
79
+
80
+ @staticmethod
81
+ def generate_read_link_number_telegram(serial_number: str) -> str:
82
+ """Generate a telegram to read a module's current link number.
83
+
84
+ Args:
85
+ serial_number: The 10-digit module serial number.
86
+
87
+ Returns:
88
+ Formatted telegram string for reading link number.
89
+
90
+ Raises:
91
+ LinkNumberError: If serial number is invalid.
92
+ """
93
+ # Validate serial number
94
+ if not serial_number or len(serial_number) != 10:
95
+ raise LinkNumberError(
96
+ f"Serial number must be 10 digits, got: {serial_number}"
97
+ )
98
+
99
+ if not serial_number.isdigit():
100
+ raise LinkNumberError(
101
+ f"Serial number must contain only digits: {serial_number}"
102
+ )
103
+
104
+ # Build the data part for reading (F03D04 - READ_CONFIG, LINK_NUMBER)
105
+ data_part = f"S{serial_number}F03D04"
106
+
107
+ # Calculate checksum
108
+ checksum = calculate_checksum(data_part)
109
+
110
+ # Build complete telegram
111
+ telegram = f"<{data_part}{checksum}>"
112
+
113
+ return telegram
114
+
115
+ def create_set_link_number_telegram_object(
116
+ self, serial_number: str, link_number: int
117
+ ) -> SystemTelegram:
118
+ """Create a SystemTelegram object for setting link number.
119
+
120
+ Args:
121
+ serial_number: The 10-digit module serial number.
122
+ link_number: The link number to set (0-99).
123
+
124
+ Returns:
125
+ SystemTelegram object representing the set link number command.
126
+ """
127
+ raw_telegram = self.generate_set_link_number_telegram(
128
+ serial_number, link_number
129
+ )
130
+
131
+ # Extract checksum from the generated telegram
132
+ checksum = raw_telegram[-3:-1] # Get checksum before closing >
133
+
134
+ telegram = SystemTelegram(
135
+ serial_number=serial_number,
136
+ system_function=SystemFunction.WRITE_CONFIG,
137
+ datapoint_type=DataPointType.LINK_NUMBER,
138
+ checksum=checksum,
139
+ raw_telegram=raw_telegram,
140
+ )
141
+
142
+ return telegram
143
+
144
+ def create_read_link_number_telegram_object(
145
+ self, serial_number: str
146
+ ) -> SystemTelegram:
147
+ """Create a SystemTelegram object for reading link number.
148
+
149
+ Args:
150
+ serial_number: The 10-digit module serial number.
151
+
152
+ Returns:
153
+ SystemTelegram object representing the read link number command.
154
+ """
155
+ raw_telegram = self.generate_read_link_number_telegram(serial_number)
156
+
157
+ # Extract checksum from the generated telegram
158
+ checksum = raw_telegram[-3:-1] # Get checksum before closing >
159
+
160
+ telegram = SystemTelegram(
161
+ serial_number=serial_number,
162
+ system_function=SystemFunction.READ_CONFIG,
163
+ datapoint_type=DataPointType.LINK_NUMBER,
164
+ checksum=checksum,
165
+ raw_telegram=raw_telegram,
166
+ )
167
+
168
+ return telegram
169
+
170
+ @staticmethod
171
+ def parse_link_number_from_reply(reply_telegram: ReplyTelegram) -> Optional[int]:
172
+ """Parse the link number value from a reply telegram.
173
+
174
+ Args:
175
+ reply_telegram: Reply telegram containing link number data.
176
+
177
+ Returns:
178
+ Link number if successfully parsed, None otherwise.
179
+ """
180
+ if (
181
+ reply_telegram.datapoint_type != DataPointType.LINK_NUMBER
182
+ or not reply_telegram.data_value
183
+ ):
184
+ return None
185
+
186
+ with suppress(ValueError, TypeError):
187
+ # The data value should contain the link number
188
+ link_number = int(reply_telegram.data_value)
189
+ if 0 <= link_number <= 99:
190
+ return link_number
191
+
192
+ return None
193
+
194
+ @staticmethod
195
+ def is_ack_response(reply_telegram: ReplyTelegram) -> bool:
196
+ """Check if a reply telegram is an ACK response.
197
+
198
+ Args:
199
+ reply_telegram: Reply telegram to check.
200
+
201
+ Returns:
202
+ True if this is an ACK response (F18D), False otherwise.
203
+ """
204
+ return reply_telegram.system_function == SystemFunction.ACK
205
+
206
+ @staticmethod
207
+ def is_nak_response(reply_telegram: ReplyTelegram) -> bool:
208
+ """Check if a reply telegram is a NAK response.
209
+
210
+ Args:
211
+ reply_telegram: Reply telegram to check.
212
+
213
+ Returns:
214
+ True if this is a NAK response (F19D), False otherwise.
215
+ """
216
+ return reply_telegram.system_function == SystemFunction.NAK
@@ -0,0 +1,322 @@
1
+ """XP output service for handling XP output device operations."""
2
+
3
+ import re
4
+ from typing import Dict
5
+
6
+ from xp.models.telegram.action_type import ActionType
7
+ from xp.models.telegram.datapoint_type import DataPointType
8
+ from xp.models.telegram.output_telegram import OutputTelegram
9
+ from xp.models.telegram.system_function import SystemFunction
10
+ from xp.services.telegram.telegram_service import TelegramService
11
+ from xp.utils.checksum import calculate_checksum
12
+
13
+
14
+ class XPOutputError(Exception):
15
+ """Raised when XP24 action operations fail."""
16
+
17
+ pass
18
+
19
+
20
+ class TelegramOutputService:
21
+ """Service for XP action operations.
22
+
23
+ Handles parsing and validation of XP24 action telegrams,
24
+ status queries, and action command generation.
25
+
26
+ Attributes:
27
+ MAX_OUTPUTS: Maximum number of outputs supported.
28
+ XP_OUTPUT_PATTERN: Regex pattern for XP24 action telegrams.
29
+ XP_ACK_NAK_PATTERN: Regex pattern for ACK/NAK response telegrams.
30
+ telegram_service: TelegramService instance for parsing.
31
+ """
32
+
33
+ MAX_OUTPUTS = 99
34
+
35
+ # Regex pattern for XP24 action telegrams
36
+ XP_OUTPUT_PATTERN = re.compile(r"^<S(\d{10})F27D(\d{2})(A[AB])([A-Z0-9]{2})>$")
37
+ XP_ACK_NAK_PATTERN = re.compile(r"^<R(\d{10})F(1[89])D([A-Z0-9]{2})>$")
38
+
39
+ def __init__(self, telegram_service: TelegramService) -> None:
40
+ """Initialize the XP output service.
41
+
42
+ Args:
43
+ telegram_service: TelegramService instance for parsing operations.
44
+ """
45
+ self.telegram_service = telegram_service
46
+
47
+ def validate_output_number(self, output_number: int) -> None:
48
+ """Validate XP24 output number according to architecture constraints.
49
+
50
+ Args:
51
+ output_number: Output number to validate (0-3).
52
+
53
+ Raises:
54
+ XPOutputError: If output number is invalid.
55
+ """
56
+ if not isinstance(output_number, int):
57
+ raise XPOutputError(
58
+ f"Output number must be integer, got {type(output_number)}"
59
+ )
60
+
61
+ if not (0 <= output_number <= self.MAX_OUTPUTS):
62
+ raise XPOutputError(
63
+ f"Invalid output number: {output_number}. "
64
+ f"XP24 supports outputs 0-{self.MAX_OUTPUTS}"
65
+ )
66
+
67
+ @staticmethod
68
+ def validate_serial_number(serial_number: str) -> None:
69
+ """Validate serial number format.
70
+
71
+ Args:
72
+ serial_number: Serial number to validate.
73
+
74
+ Raises:
75
+ XPOutputError: If serial number is invalid.
76
+ """
77
+ if not isinstance(serial_number, str):
78
+ raise XPOutputError(
79
+ f"Serial number must be string, got {type(serial_number)}"
80
+ )
81
+
82
+ if len(serial_number) != 10 or not serial_number.isdigit():
83
+ raise XPOutputError(
84
+ f"Invalid serial number: {serial_number}. "
85
+ "Serial number must be exactly 10 digits"
86
+ )
87
+
88
+ def generate_system_action_telegram(
89
+ self, serial_number: str, output_number: int, action: ActionType
90
+ ) -> str:
91
+ """Generate XP24 action telegram string.
92
+
93
+ Args:
94
+ serial_number: Target module serial number.
95
+ output_number: Output number (0-3).
96
+ action: Action type (PRESS/RELEASE).
97
+
98
+ Returns:
99
+ Complete telegram string with checksum.
100
+
101
+ Raises:
102
+ XPOutputError: If parameters are invalid.
103
+ """
104
+ # Validate outputs according to architecture constraints
105
+ self.validate_serial_number(serial_number)
106
+ self.validate_output_number(output_number)
107
+
108
+ if not isinstance(action, ActionType):
109
+ raise XPOutputError(f"Invalid action type: {action}")
110
+
111
+ function_code = SystemFunction.ACTION.value
112
+ # Build data part without checksum
113
+ data_part = (
114
+ f"S{serial_number}F{function_code}D{output_number:02d}{action.value}"
115
+ )
116
+
117
+ # Calculate checksum
118
+ checksum = calculate_checksum(data_part)
119
+
120
+ # Return complete telegram
121
+ return f"<{data_part}{checksum}>"
122
+
123
+ def generate_system_status_telegram(self, serial_number: str) -> str:
124
+ """Generate XP output status query telegram.
125
+
126
+ Args:
127
+ serial_number: Target module serial number.
128
+
129
+ Returns:
130
+ Complete status query telegram string.
131
+ """
132
+ # Validate outputs
133
+ self.validate_serial_number(serial_number)
134
+ function_code = SystemFunction.READ_DATAPOINT.value
135
+ datapoint_code = DataPointType.MODULE_OUTPUT_STATE.value
136
+
137
+ # Build data part without checksum
138
+ data_part = f"S{serial_number}F{function_code}D{datapoint_code}"
139
+
140
+ # Calculate checksum
141
+ checksum = calculate_checksum(data_part)
142
+
143
+ # Return complete telegram
144
+ return f"<{data_part}{checksum}>"
145
+
146
+ def parse_reply_telegram(self, raw_telegram: str) -> OutputTelegram:
147
+ """Parse a raw XP output response telegram string.
148
+
149
+ Args:
150
+ raw_telegram: The raw telegram string (e.g., "<R0012345003F18DFF>").
151
+
152
+ Returns:
153
+ XPOutputTelegram object with parsed data.
154
+
155
+ Raises:
156
+ XPOutputError: If telegram format is invalid.
157
+ """
158
+ if not raw_telegram:
159
+ raise XPOutputError("Empty telegram string")
160
+
161
+ # Validate and parse using regex
162
+ match = self.XP_ACK_NAK_PATTERN.match(raw_telegram.strip())
163
+ if not match:
164
+ raise XPOutputError(
165
+ f"Invalid XP24 response telegram format: {raw_telegram}"
166
+ )
167
+
168
+ try:
169
+ serial_number = match.group(1)
170
+ ack_nak = match.group(2)
171
+ checksum = match.group(3)
172
+
173
+ # Parse action type
174
+ system_function = SystemFunction.from_code(ack_nak)
175
+ if system_function is None:
176
+ raise XPOutputError(f"Unknown system_function: {ack_nak}")
177
+
178
+ # Create telegram object
179
+ telegram = OutputTelegram(
180
+ serial_number=serial_number,
181
+ system_function=system_function,
182
+ checksum=checksum,
183
+ raw_telegram=raw_telegram,
184
+ )
185
+
186
+ # Validate checksum
187
+ telegram.checksum_validated = self.telegram_service.validate_checksum(
188
+ telegram
189
+ )
190
+
191
+ return telegram
192
+
193
+ except ValueError as e:
194
+ raise XPOutputError(f"Invalid values in XP24 action telegram: {e}")
195
+
196
+ def parse_system_telegram(self, raw_telegram: str) -> OutputTelegram:
197
+ """Parse a raw XP output telegram string.
198
+
199
+ Args:
200
+ raw_telegram: The raw telegram string (e.g., "<S0012345008F27D00AAFN>").
201
+
202
+ Returns:
203
+ XPOutputTelegram object with parsed data.
204
+
205
+ Raises:
206
+ XPOutputError: If telegram format is invalid.
207
+ """
208
+ if not raw_telegram:
209
+ raise XPOutputError("Empty telegram string")
210
+
211
+ # Validate and parse using regex
212
+ match = self.XP_OUTPUT_PATTERN.match(raw_telegram.strip())
213
+ if not match:
214
+ raise XPOutputError(f"Invalid XP24 action telegram format: {raw_telegram}")
215
+
216
+ try:
217
+ serial_number = match.group(1)
218
+ output_number = int(match.group(2))
219
+ action_code = match.group(3)
220
+ checksum = match.group(4)
221
+
222
+ # Validate output number
223
+ self.validate_output_number(output_number)
224
+
225
+ # Parse action type
226
+ action_type = ActionType.from_code(action_code)
227
+ if action_type is None:
228
+ raise XPOutputError(f"Unknown action code: {action_code}")
229
+
230
+ # Create telegram object
231
+ telegram = OutputTelegram(
232
+ serial_number=serial_number,
233
+ output_number=output_number,
234
+ action_type=action_type,
235
+ checksum=checksum,
236
+ raw_telegram=raw_telegram,
237
+ )
238
+
239
+ # Validate checksum
240
+ telegram.checksum_validated = self.telegram_service.validate_checksum(
241
+ telegram
242
+ )
243
+
244
+ return telegram
245
+
246
+ except ValueError as e:
247
+ raise XPOutputError(f"Invalid values in XP24 action telegram: {e}")
248
+
249
+ def parse_status_response(self, raw_telegram: str) -> list[bool]:
250
+ """Parse XP24 status response telegram to extract output states.
251
+
252
+ Args:
253
+ raw_telegram: Raw reply telegram (e.g., "<R0012345008F02D12xxxx1110FJ>").
254
+
255
+ Returns:
256
+ Dictionary mapping output numbers (0-3) to their states (True=ON, False=OFF).
257
+
258
+ Raises:
259
+ XPOutputError: If output telegram is invalid.
260
+ """
261
+ if not raw_telegram:
262
+ raise XPOutputError("Empty status response telegram")
263
+
264
+ # Look for status pattern in reply telegram
265
+ reply_telegram = self.telegram_service.parse_reply_telegram(raw_telegram)
266
+ if not reply_telegram or not reply_telegram.data_value:
267
+ raise XPOutputError("Not a reply telegram")
268
+
269
+ if (
270
+ not reply_telegram.datapoint_type
271
+ or not reply_telegram.datapoint_type == DataPointType.MODULE_OUTPUT_STATE
272
+ ):
273
+ raise XPOutputError("Not a DataPoint telegram")
274
+
275
+ status_bits = reply_telegram.data_value.replace("xxxx", "")[::-1][0:4]
276
+ if len(status_bits) != 4:
277
+ raise XPOutputError("Not a module_output_state telegram")
278
+
279
+ status = [False, False, False, False]
280
+ for i in range(4):
281
+ status[i] = status_bits[i] == "1"
282
+
283
+ return status
284
+
285
+ @staticmethod
286
+ def format_status_summary(status: Dict[int, bool]) -> str:
287
+ """Format status dictionary into human-readable summary.
288
+
289
+ Args:
290
+ status: Dictionary mapping output numbers to states.
291
+
292
+ Returns:
293
+ Formatted status summary string.
294
+ """
295
+ lines = ["XP24 Output Status:"]
296
+ for output_num in sorted(status.keys()):
297
+ state = "ON" if status[output_num] else "OFF"
298
+ lines.append(f" Output {output_num}: {state}")
299
+
300
+ return "\n".join(lines)
301
+
302
+ @staticmethod
303
+ def format_action_summary(telegram: OutputTelegram) -> str:
304
+ """Format XP24 action telegram for human-readable output.
305
+
306
+ Args:
307
+ telegram: The parsed action telegram.
308
+
309
+ Returns:
310
+ Formatted string summary.
311
+ """
312
+ checksum_status = ""
313
+ if telegram.checksum_validated is not None:
314
+ status_indicator = "✓" if telegram.checksum_validated else "✗"
315
+ checksum_status = f" ({status_indicator})"
316
+
317
+ return (
318
+ f"XP Output: {telegram}\n"
319
+ f"Raw: {telegram.raw_telegram}\n"
320
+ f"Timestamp: {telegram.timestamp}\n"
321
+ f"Checksum: {telegram.checksum}{checksum_status}"
322
+ )