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.
- conson_xp-1.18.0.dist-info/METADATA +412 -0
- conson_xp-1.18.0.dist-info/RECORD +176 -0
- conson_xp-1.18.0.dist-info/WHEEL +4 -0
- conson_xp-1.18.0.dist-info/entry_points.txt +5 -0
- conson_xp-1.18.0.dist-info/licenses/LICENSE +29 -0
- xp/__init__.py +9 -0
- xp/cli/__init__.py +5 -0
- xp/cli/__main__.py +6 -0
- xp/cli/commands/__init__.py +153 -0
- xp/cli/commands/conbus/__init__.py +25 -0
- xp/cli/commands/conbus/conbus.py +128 -0
- xp/cli/commands/conbus/conbus_actiontable_commands.py +233 -0
- xp/cli/commands/conbus/conbus_autoreport_commands.py +108 -0
- xp/cli/commands/conbus/conbus_blink_commands.py +163 -0
- xp/cli/commands/conbus/conbus_config_commands.py +29 -0
- xp/cli/commands/conbus/conbus_custom_commands.py +57 -0
- xp/cli/commands/conbus/conbus_datapoint_commands.py +113 -0
- xp/cli/commands/conbus/conbus_discover_commands.py +61 -0
- xp/cli/commands/conbus/conbus_event_commands.py +81 -0
- xp/cli/commands/conbus/conbus_lightlevel_commands.py +207 -0
- xp/cli/commands/conbus/conbus_linknumber_commands.py +102 -0
- xp/cli/commands/conbus/conbus_modulenumber_commands.py +104 -0
- xp/cli/commands/conbus/conbus_msactiontable_commands.py +94 -0
- xp/cli/commands/conbus/conbus_output_commands.py +163 -0
- xp/cli/commands/conbus/conbus_raw_commands.py +62 -0
- xp/cli/commands/conbus/conbus_receive_commands.py +59 -0
- xp/cli/commands/conbus/conbus_scan_commands.py +58 -0
- xp/cli/commands/file_commands.py +186 -0
- xp/cli/commands/homekit/__init__.py +3 -0
- xp/cli/commands/homekit/homekit.py +118 -0
- xp/cli/commands/homekit/homekit_start_commands.py +43 -0
- xp/cli/commands/module_commands.py +187 -0
- xp/cli/commands/reverse_proxy_commands.py +178 -0
- xp/cli/commands/server/__init__.py +3 -0
- xp/cli/commands/server/server_commands.py +135 -0
- xp/cli/commands/telegram/__init__.py +5 -0
- xp/cli/commands/telegram/telegram.py +41 -0
- xp/cli/commands/telegram/telegram_blink_commands.py +79 -0
- xp/cli/commands/telegram/telegram_checksum_commands.py +112 -0
- xp/cli/commands/telegram/telegram_discover_commands.py +41 -0
- xp/cli/commands/telegram/telegram_linknumber_commands.py +86 -0
- xp/cli/commands/telegram/telegram_parse_commands.py +75 -0
- xp/cli/commands/telegram/telegram_version_commands.py +52 -0
- xp/cli/main.py +87 -0
- xp/cli/utils/__init__.py +1 -0
- xp/cli/utils/click_tree.py +57 -0
- xp/cli/utils/datapoint_type_choice.py +57 -0
- xp/cli/utils/decorators.py +351 -0
- xp/cli/utils/error_handlers.py +201 -0
- xp/cli/utils/formatters.py +312 -0
- xp/cli/utils/module_type_choice.py +56 -0
- xp/cli/utils/serial_number_type.py +52 -0
- xp/cli/utils/system_function_choice.py +57 -0
- xp/cli/utils/xp_module_type.py +53 -0
- xp/connection/__init__.py +13 -0
- xp/connection/exceptions.py +22 -0
- xp/models/__init__.py +36 -0
- xp/models/actiontable/__init__.py +1 -0
- xp/models/actiontable/actiontable.py +43 -0
- xp/models/actiontable/msactiontable_xp20.py +53 -0
- xp/models/actiontable/msactiontable_xp24.py +58 -0
- xp/models/actiontable/msactiontable_xp33.py +65 -0
- xp/models/conbus/__init__.py +1 -0
- xp/models/conbus/conbus.py +87 -0
- xp/models/conbus/conbus_autoreport.py +67 -0
- xp/models/conbus/conbus_blink.py +80 -0
- xp/models/conbus/conbus_client_config.py +55 -0
- xp/models/conbus/conbus_connection_status.py +40 -0
- xp/models/conbus/conbus_custom.py +58 -0
- xp/models/conbus/conbus_datapoint.py +89 -0
- xp/models/conbus/conbus_discover.py +64 -0
- xp/models/conbus/conbus_event_raw.py +47 -0
- xp/models/conbus/conbus_lightlevel.py +52 -0
- xp/models/conbus/conbus_linknumber.py +54 -0
- xp/models/conbus/conbus_output.py +57 -0
- xp/models/conbus/conbus_raw.py +45 -0
- xp/models/conbus/conbus_receive.py +42 -0
- xp/models/conbus/conbus_writeconfig.py +60 -0
- xp/models/homekit/__init__.py +1 -0
- xp/models/homekit/homekit_accessory.py +35 -0
- xp/models/homekit/homekit_config.py +106 -0
- xp/models/homekit/homekit_conson_config.py +86 -0
- xp/models/log_entry.py +130 -0
- xp/models/protocol/__init__.py +1 -0
- xp/models/protocol/conbus_protocol.py +312 -0
- xp/models/response.py +42 -0
- xp/models/telegram/__init__.py +1 -0
- xp/models/telegram/action_type.py +31 -0
- xp/models/telegram/datapoint_type.py +82 -0
- xp/models/telegram/event_telegram.py +140 -0
- xp/models/telegram/event_type.py +15 -0
- xp/models/telegram/input_action_type.py +69 -0
- xp/models/telegram/input_type.py +17 -0
- xp/models/telegram/module_type.py +188 -0
- xp/models/telegram/module_type_code.py +205 -0
- xp/models/telegram/output_telegram.py +103 -0
- xp/models/telegram/reply_telegram.py +297 -0
- xp/models/telegram/system_function.py +116 -0
- xp/models/telegram/system_telegram.py +94 -0
- xp/models/telegram/telegram.py +28 -0
- xp/models/telegram/telegram_type.py +19 -0
- xp/models/telegram/timeparam_type.py +51 -0
- xp/models/write_config_type.py +33 -0
- xp/services/__init__.py +26 -0
- xp/services/actiontable/__init__.py +1 -0
- xp/services/actiontable/actiontable_serializer.py +273 -0
- xp/services/actiontable/msactiontable_serializer.py +7 -0
- xp/services/actiontable/msactiontable_xp20_serializer.py +169 -0
- xp/services/actiontable/msactiontable_xp24_serializer.py +120 -0
- xp/services/actiontable/msactiontable_xp33_serializer.py +239 -0
- xp/services/conbus/__init__.py +1 -0
- xp/services/conbus/actiontable/__init__.py +1 -0
- xp/services/conbus/actiontable/actiontable_download_service.py +158 -0
- xp/services/conbus/actiontable/actiontable_list_service.py +91 -0
- xp/services/conbus/actiontable/actiontable_show_service.py +89 -0
- xp/services/conbus/actiontable/actiontable_upload_service.py +211 -0
- xp/services/conbus/actiontable/msactiontable_service.py +232 -0
- xp/services/conbus/conbus_blink_all_service.py +181 -0
- xp/services/conbus/conbus_blink_service.py +158 -0
- xp/services/conbus/conbus_custom_service.py +156 -0
- xp/services/conbus/conbus_datapoint_queryall_service.py +182 -0
- xp/services/conbus/conbus_datapoint_service.py +170 -0
- xp/services/conbus/conbus_discover_service.py +312 -0
- xp/services/conbus/conbus_event_raw_service.py +181 -0
- xp/services/conbus/conbus_output_service.py +194 -0
- xp/services/conbus/conbus_raw_service.py +122 -0
- xp/services/conbus/conbus_receive_service.py +115 -0
- xp/services/conbus/conbus_scan_service.py +150 -0
- xp/services/conbus/write_config_service.py +194 -0
- xp/services/homekit/__init__.py +1 -0
- xp/services/homekit/homekit_cache_service.py +307 -0
- xp/services/homekit/homekit_conbus_service.py +93 -0
- xp/services/homekit/homekit_config_validator.py +310 -0
- xp/services/homekit/homekit_conson_validator.py +121 -0
- xp/services/homekit/homekit_dimminglight.py +182 -0
- xp/services/homekit/homekit_dimminglight_service.py +148 -0
- xp/services/homekit/homekit_hap_service.py +342 -0
- xp/services/homekit/homekit_lightbulb.py +120 -0
- xp/services/homekit/homekit_lightbulb_service.py +86 -0
- xp/services/homekit/homekit_module_service.py +56 -0
- xp/services/homekit/homekit_outlet.py +168 -0
- xp/services/homekit/homekit_outlet_service.py +121 -0
- xp/services/homekit/homekit_service.py +359 -0
- xp/services/log_file_service.py +309 -0
- xp/services/module_type_service.py +257 -0
- xp/services/protocol/__init__.py +21 -0
- xp/services/protocol/conbus_event_protocol.py +360 -0
- xp/services/protocol/conbus_protocol.py +318 -0
- xp/services/protocol/protocol_factory.py +78 -0
- xp/services/protocol/telegram_protocol.py +264 -0
- xp/services/reverse_proxy_service.py +435 -0
- xp/services/server/__init__.py +1 -0
- xp/services/server/base_server_service.py +366 -0
- xp/services/server/cp20_server_service.py +65 -0
- xp/services/server/device_service_factory.py +94 -0
- xp/services/server/server_service.py +428 -0
- xp/services/server/xp130_server_service.py +67 -0
- xp/services/server/xp20_server_service.py +92 -0
- xp/services/server/xp230_server_service.py +58 -0
- xp/services/server/xp24_server_service.py +245 -0
- xp/services/server/xp33_server_service.py +535 -0
- xp/services/telegram/__init__.py +1 -0
- xp/services/telegram/telegram_blink_service.py +138 -0
- xp/services/telegram/telegram_checksum_service.py +149 -0
- xp/services/telegram/telegram_datapoint_service.py +82 -0
- xp/services/telegram/telegram_discover_service.py +277 -0
- xp/services/telegram/telegram_link_number_service.py +216 -0
- xp/services/telegram/telegram_output_service.py +322 -0
- xp/services/telegram/telegram_service.py +380 -0
- xp/services/telegram/telegram_version_service.py +288 -0
- xp/utils/__init__.py +12 -0
- xp/utils/checksum.py +61 -0
- xp/utils/dependencies.py +531 -0
- xp/utils/event_helper.py +31 -0
- xp/utils/serialization.py +205 -0
- 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
|
+
]
|