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,380 @@
1
+ """Telegram Service for parsing XP telegrams.
2
+
3
+ This module provides telegram parsing functionality for event, system, and reply telegrams.
4
+ """
5
+
6
+ import logging
7
+ import re
8
+ from typing import Union
9
+
10
+ from xp.models import EventType
11
+ from xp.models.telegram.datapoint_type import DataPointType
12
+ from xp.models.telegram.event_telegram import EventTelegram
13
+ from xp.models.telegram.output_telegram import OutputTelegram
14
+ from xp.models.telegram.reply_telegram import ReplyTelegram
15
+ from xp.models.telegram.system_function import SystemFunction
16
+ from xp.models.telegram.system_telegram import SystemTelegram
17
+ from xp.models.telegram.telegram_type import TelegramType
18
+ from xp.utils.checksum import calculate_checksum
19
+
20
+
21
+ class TelegramParsingError(Exception):
22
+ """Raised when telegram parsing fails."""
23
+
24
+ pass
25
+
26
+
27
+ class TelegramService:
28
+ """Service for parsing event telegrams from the console bus.
29
+
30
+ Handles parsing of telegrams in the format:
31
+ <[EO]{module_type}L{link_number}I{output_number}{event_type}{checksum}>
32
+
33
+ Attributes:
34
+ EVENT_TELEGRAM_PATTERN: Regex pattern for event telegrams.
35
+ SYSTEM_TELEGRAM_PATTERN: Regex pattern for system telegrams.
36
+ REPLY_TELEGRAM_PATTERN: Regex pattern for reply telegrams.
37
+ """
38
+
39
+ # <O06L00I07MAG>
40
+ # <O06L00I07BAJ>
41
+ # <E13L12I02BAB>
42
+ EVENT_TELEGRAM_PATTERN = re.compile(
43
+ r"^<([EO])(\d{1,2})L(\d{2})I(\d{2})([MB])([A-Z0-9]{2})>$"
44
+ )
45
+
46
+ SYSTEM_TELEGRAM_PATTERN = re.compile(r"^<S(\d{10})F(\d{2})D(.{2,})([A-Z0-9]{2})>$")
47
+
48
+ REPLY_TELEGRAM_PATTERN = re.compile(r"^<R(\d{10})F(\d{2})(.+?)([A-Z0-9]{2})>$")
49
+
50
+ def __init__(self) -> None:
51
+ """Initialize the telegram service."""
52
+ # Set up logging
53
+ self.logger = logging.getLogger(__name__)
54
+
55
+ def parse_event_telegram(self, raw_telegram: str) -> EventTelegram:
56
+ """Parse a raw telegram string into an EventTelegram object.
57
+
58
+ Args:
59
+ raw_telegram: The raw telegram string (e.g., "<E14L00I02MAK>").
60
+
61
+ Returns:
62
+ EventTelegram object with parsed data.
63
+
64
+ Raises:
65
+ TelegramParsingError: If the telegram format is invalid.
66
+ """
67
+ if not raw_telegram:
68
+ raise TelegramParsingError("Empty telegram string")
69
+
70
+ # Validate and parse using regex
71
+ match = self.EVENT_TELEGRAM_PATTERN.match(raw_telegram.strip())
72
+ if not match:
73
+ raise TelegramParsingError(f"Invalid telegram format: {raw_telegram}")
74
+
75
+ try:
76
+ event_telegram_type = match.group(1)
77
+ module_type = int(match.group(2))
78
+ link_number = int(match.group(3))
79
+ output_number = int(match.group(4))
80
+ event_type_char = match.group(5)
81
+ checksum = match.group(6)
82
+
83
+ # Validate ranges
84
+ if event_telegram_type not in ("E", "O"):
85
+ raise TelegramParsingError(
86
+ f"Event telegram type (E or O): {event_telegram_type}"
87
+ )
88
+
89
+ if not (0 <= link_number <= 99):
90
+ raise TelegramParsingError(
91
+ f"Link number out of range (0-99): {link_number}"
92
+ )
93
+
94
+ if not (0 <= output_number <= 99):
95
+ raise TelegramParsingError(
96
+ f"Input number out of range (0-99): {output_number}"
97
+ )
98
+
99
+ # Parse event type
100
+ try:
101
+ event_type = EventType(event_type_char)
102
+ except ValueError:
103
+ raise TelegramParsingError(f"Invalid event type: {event_type_char}")
104
+
105
+ # Create the telegram object
106
+ telegram = EventTelegram(
107
+ module_type=module_type,
108
+ link_number=link_number,
109
+ input_number=output_number,
110
+ event_type=event_type,
111
+ checksum=checksum,
112
+ raw_telegram=raw_telegram,
113
+ )
114
+
115
+ # Automatically validate checksum
116
+ telegram.checksum_validated = self.validate_checksum(telegram)
117
+
118
+ return telegram
119
+
120
+ except ValueError as e:
121
+ raise TelegramParsingError(f"Invalid numeric values in telegram: {e}")
122
+
123
+ @staticmethod
124
+ def validate_checksum(
125
+ telegram: Union[EventTelegram, ReplyTelegram, SystemTelegram, OutputTelegram],
126
+ ) -> bool:
127
+ """Validate the checksum of a parsed telegram.
128
+
129
+ Args:
130
+ telegram: The parsed telegram.
131
+
132
+ Returns:
133
+ True if checksum is valid, False otherwise.
134
+ """
135
+ if not telegram.checksum or len(telegram.checksum) != 2:
136
+ return False
137
+
138
+ # Extract the data part (everything between < and checksum)
139
+ raw = telegram.raw_telegram
140
+ if not raw.startswith("<") or not raw.endswith(">"):
141
+ return False
142
+
143
+ # Get the data part without brackets and checksum
144
+ data_part = raw[1:-3] # Remove '<' and last 2 chars (checksum) + '>'
145
+
146
+ # Calculate expected checksum
147
+ expected_checksum = calculate_checksum(data_part)
148
+
149
+ return telegram.checksum == expected_checksum
150
+
151
+ @staticmethod
152
+ def format_event_telegram_summary(telegram: EventTelegram) -> str:
153
+ """Format a telegram for human-readable output.
154
+
155
+ Args:
156
+ telegram: The parsed telegram.
157
+
158
+ Returns:
159
+ Formatted string summary.
160
+ """
161
+ checksum_status = ""
162
+ if telegram.checksum_validated is not None:
163
+ status_indicator = "✓" if telegram.checksum_validated else "✗"
164
+ checksum_status = f" ({status_indicator})"
165
+
166
+ return (
167
+ f"Event: {telegram}\n"
168
+ f"Raw: {telegram.raw_telegram}\n"
169
+ f"Timestamp: {telegram.timestamp}\n"
170
+ f"Checksum: {telegram.checksum}{checksum_status}"
171
+ )
172
+
173
+ def parse_system_telegram(self, raw_telegram: str) -> SystemTelegram:
174
+ """Parse a raw system telegram string into a SystemTelegram object.
175
+
176
+ Args:
177
+ raw_telegram: The raw telegram string (e.g., "<S0020012521F02D18FN>").
178
+
179
+ Returns:
180
+ SystemTelegram object with parsed data.
181
+
182
+ Raises:
183
+ TelegramParsingError: If the telegram format is invalid.
184
+ """
185
+ if not raw_telegram:
186
+ raise TelegramParsingError("Empty telegram string")
187
+
188
+ # Validate and parse using regex
189
+ match = self.SYSTEM_TELEGRAM_PATTERN.match(raw_telegram.strip())
190
+ if not match:
191
+ raise TelegramParsingError(
192
+ f"Invalid system telegram format: {raw_telegram}"
193
+ )
194
+
195
+ try:
196
+ serial_number = match.group(1)
197
+ function_code = match.group(2)
198
+ data = match.group(3)
199
+ checksum = match.group(4)
200
+
201
+ # Parse system function
202
+ system_function = SystemFunction.from_code(function_code)
203
+ if system_function is None:
204
+ raise TelegramParsingError(
205
+ f"Unknown system function code: {function_code}"
206
+ )
207
+
208
+ # Parse data point type
209
+ datapoint_type = None
210
+ if system_function == SystemFunction.READ_DATAPOINT:
211
+ datapoint_type = DataPointType.from_code(data)
212
+
213
+ # Create the telegram object
214
+ telegram = SystemTelegram(
215
+ serial_number=serial_number,
216
+ system_function=system_function,
217
+ data=data,
218
+ datapoint_type=datapoint_type,
219
+ checksum=checksum,
220
+ raw_telegram=raw_telegram,
221
+ )
222
+
223
+ # Automatically validate checksum
224
+ telegram.checksum_validated = self.validate_checksum(telegram)
225
+
226
+ return telegram
227
+
228
+ except ValueError as e:
229
+ raise TelegramParsingError(f"Invalid values in system telegram: {e}")
230
+
231
+ def parse_reply_telegram(self, raw_telegram: str) -> ReplyTelegram:
232
+ """Parse a raw reply telegram string into a ReplyTelegram object.
233
+
234
+ Args:
235
+ raw_telegram: The raw telegram string (e.g., "<R0020012521F02D18+26,0§CIL>").
236
+
237
+ Returns:
238
+ ReplyTelegram object with parsed data.
239
+
240
+ Raises:
241
+ TelegramParsingError: If the telegram format is invalid.
242
+ """
243
+ if not raw_telegram:
244
+ raise TelegramParsingError("Empty telegram string")
245
+
246
+ # Validate and parse using regex
247
+ self.logger.debug(f"Parsing reply telegram {raw_telegram}")
248
+ match = self.REPLY_TELEGRAM_PATTERN.match(raw_telegram.strip())
249
+ if not match:
250
+ raise TelegramParsingError(f"Invalid reply telegram format: {raw_telegram}")
251
+
252
+ try:
253
+ serial_number = match.group(1)
254
+ function_code = match.group(2)
255
+ full_data_value = match.group(3)
256
+ checksum = match.group(4)
257
+
258
+ # Parse system function
259
+ system_function = SystemFunction.from_code(function_code)
260
+ if system_function is None:
261
+ raise TelegramParsingError(
262
+ f"Unknown system function code: {function_code}"
263
+ )
264
+
265
+ # Parse data point and data value from full_data_value
266
+ if full_data_value.startswith("D") and len(full_data_value) >= 3:
267
+ # Regular reply format: D{data_point}{data}
268
+ data = full_data_value[1:3]
269
+ data_value = full_data_value[3:] if len(full_data_value) > 3 else ""
270
+ else:
271
+ # ACK/NAK format: just data (like "D" for ACK/NAK)
272
+ data = "00" # Default to STATUS
273
+ data_value = full_data_value
274
+
275
+ # Parse data point type
276
+ data_point_type = DataPointType.from_code(data)
277
+
278
+ # Create the telegram object
279
+ telegram = ReplyTelegram(
280
+ serial_number=serial_number,
281
+ system_function=system_function,
282
+ data=data,
283
+ datapoint_type=data_point_type,
284
+ data_value=data_value,
285
+ checksum=checksum,
286
+ raw_telegram=raw_telegram,
287
+ )
288
+
289
+ # Automatically validate checksum
290
+ telegram.checksum_validated = self.validate_checksum(telegram)
291
+
292
+ return telegram
293
+
294
+ except ValueError as e:
295
+ raise TelegramParsingError(f"Invalid values in reply telegram: {e}")
296
+
297
+ def parse_telegram(
298
+ self, raw_telegram: str
299
+ ) -> Union[EventTelegram, SystemTelegram, ReplyTelegram]:
300
+ """Auto-detect and parse any type of telegram.
301
+
302
+ Args:
303
+ raw_telegram: The raw telegram string.
304
+
305
+ Returns:
306
+ Appropriate telegram object based on type.
307
+
308
+ Raises:
309
+ TelegramParsingError: If the telegram format is invalid or unknown.
310
+ """
311
+ if not raw_telegram:
312
+ raise TelegramParsingError("Empty telegram string")
313
+
314
+ # Then check general telegram types
315
+ telegram_type_code = (
316
+ raw_telegram.strip()[1] if len(raw_telegram.strip()) > 1 else ""
317
+ )
318
+
319
+ if telegram_type_code in (TelegramType.EVENT.value, TelegramType.CPEVENT.value):
320
+ return self.parse_event_telegram(raw_telegram)
321
+ elif telegram_type_code == TelegramType.SYSTEM.value:
322
+ return self.parse_system_telegram(raw_telegram)
323
+ elif telegram_type_code == TelegramType.REPLY.value:
324
+ return self.parse_reply_telegram(raw_telegram)
325
+ else:
326
+ raise TelegramParsingError(
327
+ f"Unknown telegram type code: {telegram_type_code}"
328
+ )
329
+
330
+ @staticmethod
331
+ def format_system_telegram_summary(telegram: SystemTelegram) -> str:
332
+ """Format a system telegram for human-readable output.
333
+
334
+ Args:
335
+ telegram: The parsed system telegram.
336
+
337
+ Returns:
338
+ Formatted string summary.
339
+ """
340
+ checksum_status = ""
341
+ if telegram.checksum_validated is not None:
342
+ status_indicator = "✓" if telegram.checksum_validated else "✗"
343
+ checksum_status = f" ({status_indicator})"
344
+
345
+ return (
346
+ f"System: {telegram}\n"
347
+ f"Raw: {telegram.raw_telegram}\n"
348
+ f"Timestamp: {telegram.timestamp}\n"
349
+ f"Checksum: {telegram.checksum}{checksum_status}"
350
+ )
351
+
352
+ @staticmethod
353
+ def format_reply_telegram_summary(telegram: ReplyTelegram) -> str:
354
+ """Format a reply telegram for human-readable output.
355
+
356
+ Args:
357
+ telegram: The parsed reply telegram.
358
+
359
+ Returns:
360
+ Formatted string summary.
361
+ """
362
+ parsed_data = telegram.parse_datapoint_value
363
+ data_display = (
364
+ parsed_data.get("formatted", telegram.data_value)
365
+ if parsed_data.get("parsed")
366
+ else telegram.data_value
367
+ )
368
+
369
+ checksum_status = ""
370
+ if telegram.checksum_validated is not None:
371
+ status_indicator = "✓" if telegram.checksum_validated else "✗"
372
+ checksum_status = f" ({status_indicator})"
373
+
374
+ return (
375
+ f"Reply: {telegram}\n"
376
+ f"Data: {data_display}\n"
377
+ f"Raw: {telegram.raw_telegram}\n"
378
+ f"Timestamp: {telegram.timestamp}\n"
379
+ f"Checksum: {telegram.checksum}{checksum_status}"
380
+ )
@@ -0,0 +1,288 @@
1
+ """Version service for handling version information parsing and validation.
2
+
3
+ This service provides business logic for version operations,
4
+ following the layered architecture pattern.
5
+ """
6
+
7
+ import re
8
+
9
+ from xp.models.response import Response
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 VersionParsingError(Exception):
18
+ """Raised when version parsing fails."""
19
+
20
+ pass
21
+
22
+
23
+ class VersionService:
24
+ """Service class for version-related operations."""
25
+
26
+ def __init__(self) -> None:
27
+ """Initialize the version service."""
28
+ pass
29
+
30
+ @staticmethod
31
+ def parse_version_string(version_string: str) -> Response:
32
+ """Parse a version string into its components.
33
+
34
+ Args:
35
+ version_string: Version string in format 'XP230_V1.00.04'
36
+
37
+ Returns:
38
+ Response object with parsed version information
39
+ """
40
+ try:
41
+ # Version format: {PRODUCT}_{VERSION}
42
+ # Examples: XP230_V1.00.04, XP20_V0.01.05, XP33LR_V0.04.02, XP24_V0.34.03
43
+ if "_V" in version_string:
44
+ parts = version_string.split("_V", 1)
45
+ if len(parts) == 2:
46
+ product = parts[0]
47
+ version = parts[1]
48
+
49
+ # Validate version format (should be like 1.00.04)
50
+ version_pattern = re.compile(r"^\d+\.\d+\.\d+$")
51
+ if version_pattern.match(version):
52
+ return Response(
53
+ success=True,
54
+ data={
55
+ "product": product,
56
+ "version": version,
57
+ "full_version": version_string,
58
+ "formatted": f"{product} v{version}",
59
+ "raw_value": version_string,
60
+ "valid_format": True,
61
+ },
62
+ error=None,
63
+ )
64
+ else:
65
+ return Response(
66
+ success=True,
67
+ data={
68
+ "product": product,
69
+ "version": version,
70
+ "full_version": version_string,
71
+ "formatted": f"{product} v{version}",
72
+ "raw_value": version_string,
73
+ "valid_format": False,
74
+ "warning": "Version format doesn't match expected pattern x.xx.xx",
75
+ },
76
+ error=None,
77
+ )
78
+
79
+ # If format doesn't match expected pattern
80
+ return Response(
81
+ success=False,
82
+ data={"raw_value": version_string, "valid_format": False},
83
+ error="Version format not recognized. Expected format: PRODUCT_Vx.xx.xx",
84
+ )
85
+
86
+ except Exception as e:
87
+ return Response(
88
+ success=False, data=None, error=f"Version parsing failed: {e}"
89
+ )
90
+
91
+ @staticmethod
92
+ def generate_version_request_telegram(serial_number: str) -> Response:
93
+ """Generate a system telegram to request version information.
94
+
95
+ Args:
96
+ serial_number: 10-digit serial number of the device
97
+
98
+ Returns:
99
+ Response object with generated telegram
100
+ """
101
+ try:
102
+ if len(serial_number) != 10 or not serial_number.isdigit():
103
+ return Response(
104
+ success=False,
105
+ data=None,
106
+ error="Serial number must be exactly 10 digits",
107
+ )
108
+
109
+ # Build telegram: S{serial_number}F{function}D{data_point}
110
+ # Function 02 = Read Data point, Data Point 02 = Version
111
+ data_part = f"S{serial_number}F02D02"
112
+
113
+ # Calculate checksum
114
+ checksum = calculate_checksum(data_part)
115
+
116
+ # Complete telegram
117
+ telegram = f"<{data_part}{checksum}>"
118
+
119
+ return Response(
120
+ success=True,
121
+ data={
122
+ "telegram": telegram,
123
+ "serial_number": serial_number,
124
+ "function_code": "02",
125
+ "datapoint_code": "02",
126
+ "checksum": checksum,
127
+ "operation": "version_request",
128
+ },
129
+ error=None,
130
+ )
131
+
132
+ except Exception as e:
133
+ return Response(
134
+ success=False,
135
+ data=None,
136
+ error=f"Version request telegram generation failed: {e}",
137
+ )
138
+
139
+ @staticmethod
140
+ def validate_version_telegram(telegram: SystemTelegram) -> Response:
141
+ """Validate if a system telegram is a valid version request.
142
+
143
+ Args:
144
+ telegram: Parsed system telegram
145
+
146
+ Returns:
147
+ Response object with validation result
148
+ """
149
+ try:
150
+ is_version_request = (
151
+ telegram.system_function == SystemFunction.READ_DATAPOINT
152
+ and telegram.datapoint_type == DataPointType.SW_VERSION
153
+ )
154
+
155
+ return Response(
156
+ success=True,
157
+ data={
158
+ "is_version_request": is_version_request,
159
+ "serial_number": telegram.serial_number,
160
+ "function": (
161
+ telegram.system_function.value
162
+ if telegram.system_function
163
+ else None
164
+ ),
165
+ "data_point": (
166
+ telegram.datapoint_type.value
167
+ if telegram.datapoint_type
168
+ else None
169
+ ),
170
+ "function_description": (
171
+ telegram.system_function.name
172
+ if telegram.system_function
173
+ else None
174
+ ),
175
+ "data_point_description": (
176
+ telegram.datapoint_type.name
177
+ if telegram.datapoint_type
178
+ else None
179
+ ),
180
+ },
181
+ error=None,
182
+ )
183
+
184
+ except Exception as e:
185
+ return Response(
186
+ success=False,
187
+ data=None,
188
+ error=f"Version telegram validation failed: {e}",
189
+ )
190
+
191
+ @staticmethod
192
+ def parse_version_reply(telegram: ReplyTelegram) -> Response:
193
+ """Parse version information from a reply telegram.
194
+
195
+ Args:
196
+ telegram: Parsed reply telegram containing version data
197
+
198
+ Returns:
199
+ Response object with version information
200
+ """
201
+ try:
202
+ # Check if this is a version reply
203
+ if telegram.datapoint_type != DataPointType.SW_VERSION:
204
+ return Response(
205
+ success=False,
206
+ data=None,
207
+ error=f"Not a version reply telegram. "
208
+ f"Data point: "
209
+ f"{telegram.datapoint_type.name if telegram.datapoint_type else 'Unknown'}",
210
+ )
211
+
212
+ # Parse the version using the telegram's built-in parser
213
+ parsed_data = telegram.parse_datapoint_value
214
+
215
+ if parsed_data.get("parsed", False):
216
+ return Response(
217
+ success=True,
218
+ data={
219
+ "serial_number": telegram.serial_number,
220
+ "version_info": parsed_data,
221
+ "checksum_valid": telegram.checksum_validated,
222
+ "raw_telegram": telegram.raw_telegram,
223
+ },
224
+ error=None,
225
+ )
226
+ else:
227
+ return Response(
228
+ success=False,
229
+ data={
230
+ "serial_number": telegram.serial_number,
231
+ "raw_value": telegram.data_value,
232
+ "checksum_valid": telegram.checksum_validated,
233
+ "raw_telegram": telegram.raw_telegram,
234
+ },
235
+ error=parsed_data.get(
236
+ "error", "Failed to parse version information"
237
+ ),
238
+ )
239
+
240
+ except Exception as e:
241
+ return Response(
242
+ success=False,
243
+ data=None,
244
+ error=f"Version reply parsing failed: {e}",
245
+ )
246
+
247
+ @staticmethod
248
+ def format_version_summary(version_data: dict) -> str:
249
+ """Format version information for human-readable output.
250
+
251
+ Args:
252
+ version_data: Version information dictionary
253
+
254
+ Returns:
255
+ Formatted string summary
256
+ """
257
+ try:
258
+ if "version_info" in version_data:
259
+ version_info = version_data["version_info"]
260
+ serial = version_data.get("serial_number", "Unknown")
261
+
262
+ if version_info.get("parsed", False):
263
+ product = version_info.get("product", "Unknown")
264
+ version = version_info.get("version", "Unknown")
265
+
266
+ summary = "Device Version Information:\n"
267
+ summary += f"Serial Number: {serial}\n"
268
+ summary += f"Product: {product}\n"
269
+ summary += f"Version: {version}\n"
270
+ summary += (
271
+ f"Full Version: {version_info.get('full_version', 'Unknown')}\n"
272
+ )
273
+
274
+ checksum_status = ""
275
+ if "checksum_valid" in version_data:
276
+ status = "✓" if version_data["checksum_valid"] else "✗"
277
+ checksum_status = f" ({status})"
278
+
279
+ summary += f"Checksum: Valid{checksum_status}"
280
+
281
+ return summary
282
+ else:
283
+ return f"Version parsing failed for device {serial}: {version_info.get('error', 'Unknown error')}"
284
+ else:
285
+ return "No version information available"
286
+
287
+ except Exception as e:
288
+ return f"Error formatting version summary: {e}"
xp/utils/__init__.py ADDED
@@ -0,0 +1,12 @@
1
+ """Utility functions for XP CLI tool."""
2
+
3
+ from xp.utils.checksum import calculate_checksum
4
+ from xp.utils.event_helper import get_first_response
5
+ from xp.utils.time_utils import TimeParsingError, parse_log_timestamp
6
+
7
+ __all__ = [
8
+ "calculate_checksum",
9
+ "parse_log_timestamp",
10
+ "TimeParsingError",
11
+ "get_first_response",
12
+ ]