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,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
|
+
)
|