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,149 @@
|
|
|
1
|
+
"""Checksum service for telegram protocol validation and generation.
|
|
2
|
+
|
|
3
|
+
This service provides business logic for checksum operations,
|
|
4
|
+
following the layered architecture pattern.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Union
|
|
8
|
+
|
|
9
|
+
from xp.models.response import Response
|
|
10
|
+
from xp.utils.checksum import calculate_checksum, calculate_checksum32
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TelegramChecksumService:
|
|
14
|
+
"""Service class for checksum operations."""
|
|
15
|
+
|
|
16
|
+
def __init__(self) -> None:
|
|
17
|
+
"""Initialize the checksum service."""
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
@staticmethod
|
|
21
|
+
def calculate_simple_checksum(data: str) -> Response:
|
|
22
|
+
"""Calculate simple XOR checksum for string data.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
data: String data to calculate checksum for.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Response object with checksum result.
|
|
29
|
+
"""
|
|
30
|
+
try:
|
|
31
|
+
checksum = calculate_checksum(data)
|
|
32
|
+
|
|
33
|
+
return Response(
|
|
34
|
+
success=True,
|
|
35
|
+
data={"input": data, "checksum": checksum, "algorithm": "simple_xor"},
|
|
36
|
+
error=None,
|
|
37
|
+
)
|
|
38
|
+
except Exception as e:
|
|
39
|
+
return Response(
|
|
40
|
+
success=False, data=None, error=f"Checksum calculation failed: {e}"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def calculate_crc32_checksum(data: Union[str, bytes]) -> Response:
|
|
45
|
+
"""Calculate CRC32 checksum for data.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
data: String or bytes data to calculate checksum for.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Response object with checksum result.
|
|
52
|
+
"""
|
|
53
|
+
try:
|
|
54
|
+
# Convert string to bytes if needed
|
|
55
|
+
if isinstance(data, str):
|
|
56
|
+
byte_data = data.encode("utf-8")
|
|
57
|
+
else: # isinstance(data, bytes)
|
|
58
|
+
byte_data = data
|
|
59
|
+
|
|
60
|
+
checksum = calculate_checksum32(byte_data)
|
|
61
|
+
|
|
62
|
+
return Response(
|
|
63
|
+
success=True,
|
|
64
|
+
data={
|
|
65
|
+
"input": data,
|
|
66
|
+
"input_type": "string" if isinstance(data, str) else "bytes",
|
|
67
|
+
"input_length": len(byte_data),
|
|
68
|
+
"checksum": checksum,
|
|
69
|
+
"algorithm": "crc32",
|
|
70
|
+
},
|
|
71
|
+
error=None,
|
|
72
|
+
)
|
|
73
|
+
except Exception as e:
|
|
74
|
+
return Response(
|
|
75
|
+
success=False,
|
|
76
|
+
data=None,
|
|
77
|
+
error=f"CRC32 checksum calculation failed: {e}",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
@staticmethod
|
|
81
|
+
def validate_checksum(data: str, expected_checksum: str) -> Response:
|
|
82
|
+
"""Validate data against expected simple checksum.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
data: Original data.
|
|
86
|
+
expected_checksum: Expected checksum value.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
Response object with validation result.
|
|
90
|
+
"""
|
|
91
|
+
try:
|
|
92
|
+
calculated_checksum = calculate_checksum(data)
|
|
93
|
+
is_valid = calculated_checksum == expected_checksum
|
|
94
|
+
|
|
95
|
+
return Response(
|
|
96
|
+
success=True,
|
|
97
|
+
data={
|
|
98
|
+
"input": data,
|
|
99
|
+
"calculated_checksum": calculated_checksum,
|
|
100
|
+
"expected_checksum": expected_checksum,
|
|
101
|
+
"is_valid": is_valid,
|
|
102
|
+
},
|
|
103
|
+
error=None,
|
|
104
|
+
)
|
|
105
|
+
except Exception as e:
|
|
106
|
+
return Response(
|
|
107
|
+
success=False, data=None, error=f"Checksum validation failed: {e}"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
@staticmethod
|
|
111
|
+
def validate_crc32_checksum(
|
|
112
|
+
data: Union[str, bytes], expected_checksum: str
|
|
113
|
+
) -> Response:
|
|
114
|
+
"""Validate data against expected CRC32 checksum.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
data: Original data (string or bytes).
|
|
118
|
+
expected_checksum: Expected CRC32 checksum value.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Response object with validation result.
|
|
122
|
+
"""
|
|
123
|
+
try:
|
|
124
|
+
# Convert string to bytes if needed
|
|
125
|
+
if isinstance(data, str):
|
|
126
|
+
byte_data = data.encode("utf-8")
|
|
127
|
+
else: # isinstance(data, bytes)
|
|
128
|
+
byte_data = data
|
|
129
|
+
|
|
130
|
+
calculated_checksum = calculate_checksum32(byte_data)
|
|
131
|
+
is_valid = calculated_checksum == expected_checksum
|
|
132
|
+
|
|
133
|
+
return Response(
|
|
134
|
+
success=True,
|
|
135
|
+
data={
|
|
136
|
+
"input_type": "string" if isinstance(data, str) else "bytes",
|
|
137
|
+
"input_length": len(byte_data),
|
|
138
|
+
"calculated_checksum": calculated_checksum,
|
|
139
|
+
"expected_checksum": expected_checksum,
|
|
140
|
+
"is_valid": is_valid,
|
|
141
|
+
},
|
|
142
|
+
error=None,
|
|
143
|
+
)
|
|
144
|
+
except Exception as e:
|
|
145
|
+
return Response(
|
|
146
|
+
success=False,
|
|
147
|
+
data=None,
|
|
148
|
+
error=f"CRC32 checksum validation failed: {e}",
|
|
149
|
+
)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Service for processing Telegram protocol datapoint values."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class TelegramDatapointService:
|
|
5
|
+
"""Service for processing Telegram protocol datapoint values.
|
|
6
|
+
|
|
7
|
+
Provides methods to parse and extract values from different types of
|
|
8
|
+
Telegram datapoints including autoreport status, light level outputs,
|
|
9
|
+
and link number values.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def get_autoreport_status(self, data_value: str) -> bool:
|
|
13
|
+
"""Get the autoreport status value.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
data_value: The raw autoreport status data value (PP or AA).
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
The autoreport status: Enable (True) or disable (False).
|
|
20
|
+
"""
|
|
21
|
+
status_value = True if data_value == "PP" else False
|
|
22
|
+
return status_value
|
|
23
|
+
|
|
24
|
+
def get_autoreport_status_data_value(self, status_value: bool) -> str:
|
|
25
|
+
"""Get the autoreport status data_value.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
status_value: Enable (True) or disable (False).
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
data_value: The raw autoreport status data value (PP or AA).
|
|
32
|
+
"""
|
|
33
|
+
data_value = "PP" if status_value else "AA"
|
|
34
|
+
return data_value
|
|
35
|
+
|
|
36
|
+
def get_lightlevel(self, data_value: str, output_number: int) -> int:
|
|
37
|
+
"""Extract the light level for a specific output number.
|
|
38
|
+
|
|
39
|
+
Parses comma-separated output data in the format "output:level[%]"
|
|
40
|
+
and returns the level for the requested output number.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
data_value: Comma-separated string of output:level pairs
|
|
44
|
+
(e.g., "1:50[%],2:75[%]").
|
|
45
|
+
output_number: The output number to get the level for.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
The light level as an integer (0 if output not found).
|
|
49
|
+
"""
|
|
50
|
+
level = 0
|
|
51
|
+
for output_data in data_value.split(","):
|
|
52
|
+
if ":" in output_data:
|
|
53
|
+
output_str, level_str = output_data.split(":")
|
|
54
|
+
if int(output_str) == output_number:
|
|
55
|
+
level_str = level_str.replace("[%]", "")
|
|
56
|
+
level = int(level_str)
|
|
57
|
+
break
|
|
58
|
+
return level
|
|
59
|
+
|
|
60
|
+
def get_linknumber(self, data_value: str) -> int:
|
|
61
|
+
"""Parse and return the link number value.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
data_value: The raw link number data value as a string.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
The link number as an integer.
|
|
68
|
+
"""
|
|
69
|
+
link_number_value = int(data_value)
|
|
70
|
+
return link_number_value
|
|
71
|
+
|
|
72
|
+
def get_modulenumber(self, data_value: str) -> int:
|
|
73
|
+
"""Parse and return the module number value.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
data_value: The raw module number data value as a string.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
The module number as an integer.
|
|
80
|
+
"""
|
|
81
|
+
module_number_value = int(data_value)
|
|
82
|
+
return module_number_value
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"""Service for device discover telegram operations.
|
|
2
|
+
|
|
3
|
+
This service handles generation and parsing of device discover system telegrams
|
|
4
|
+
used for enumerating all connected devices on the console bus.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import List, Set
|
|
8
|
+
|
|
9
|
+
from xp.models.telegram.reply_telegram import ReplyTelegram
|
|
10
|
+
from xp.models.telegram.system_function import SystemFunction
|
|
11
|
+
from xp.models.telegram.system_telegram import SystemTelegram
|
|
12
|
+
from xp.utils.checksum import calculate_checksum
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DiscoverError(Exception):
|
|
16
|
+
"""Raised when discover operations fail."""
|
|
17
|
+
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DeviceInfo:
|
|
22
|
+
"""Information about a discovered device."""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self, serial_number: str, checksum_valid: bool = True, raw_telegram: str = ""
|
|
26
|
+
):
|
|
27
|
+
"""Initialize device info.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
serial_number: 10-digit module serial number.
|
|
31
|
+
checksum_valid: Whether the telegram checksum is valid.
|
|
32
|
+
raw_telegram: Raw telegram string.
|
|
33
|
+
"""
|
|
34
|
+
self.serial_number = serial_number
|
|
35
|
+
self.checksum_valid = checksum_valid
|
|
36
|
+
self.raw_telegram = raw_telegram
|
|
37
|
+
|
|
38
|
+
def __str__(self) -> str:
|
|
39
|
+
"""Return string representation of device.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
String with serial number and checksum status.
|
|
43
|
+
"""
|
|
44
|
+
status = "✓" if self.checksum_valid else "✗"
|
|
45
|
+
return f"Device {self.serial_number} ({status})"
|
|
46
|
+
|
|
47
|
+
def __repr__(self) -> str:
|
|
48
|
+
"""Return repr representation of device.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
DeviceInfo constructor representation.
|
|
52
|
+
"""
|
|
53
|
+
return f"DeviceInfo(serial='{self.serial_number}', checksum_valid={self.checksum_valid})"
|
|
54
|
+
|
|
55
|
+
def to_dict(self) -> dict:
|
|
56
|
+
"""Convert to dictionary for JSON serialization.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Dictionary with device information.
|
|
60
|
+
"""
|
|
61
|
+
return {
|
|
62
|
+
"serial_number": self.serial_number,
|
|
63
|
+
"checksum_valid": self.checksum_valid,
|
|
64
|
+
"raw_telegram": self.raw_telegram,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class TelegramDiscoverService:
|
|
69
|
+
"""
|
|
70
|
+
Service for generating and handling device discover telegrams.
|
|
71
|
+
|
|
72
|
+
Handles discover broadcasting and response parsing:
|
|
73
|
+
- Discover request: <S0000000000F01D00{checksum}>
|
|
74
|
+
- Discover responses: <R{serial}F01D{checksum}>
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
def __init__(self) -> None:
|
|
78
|
+
"""Initialize the discover service."""
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
@staticmethod
|
|
82
|
+
def generate_discover_telegram() -> str:
|
|
83
|
+
"""
|
|
84
|
+
Generate a broadcast discover telegram to enumerate all devices.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
Formatted discover telegram string: "<S0000000000F01D00FA>"
|
|
88
|
+
"""
|
|
89
|
+
# Build the data part of the telegram
|
|
90
|
+
# S0000000000F01D00 - Broadcast (all zeros) discover command
|
|
91
|
+
data_part = "S0000000000F01D00"
|
|
92
|
+
|
|
93
|
+
# Calculate checksum
|
|
94
|
+
checksum = calculate_checksum(data_part)
|
|
95
|
+
|
|
96
|
+
# Build complete telegram
|
|
97
|
+
telegram = f"<{data_part}{checksum}>"
|
|
98
|
+
|
|
99
|
+
return telegram
|
|
100
|
+
|
|
101
|
+
def create_discover_telegram_object(self) -> SystemTelegram:
|
|
102
|
+
"""Create a SystemTelegram object for discover broadcast.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
SystemTelegram object representing the discover command.
|
|
106
|
+
"""
|
|
107
|
+
raw_telegram = self.generate_discover_telegram()
|
|
108
|
+
|
|
109
|
+
# Extract checksum from the generated telegram
|
|
110
|
+
checksum = raw_telegram[-3:-1] # Get checksum before closing >
|
|
111
|
+
|
|
112
|
+
telegram = SystemTelegram(
|
|
113
|
+
serial_number="0000000000", # Broadcast address
|
|
114
|
+
system_function=SystemFunction.DISCOVERY,
|
|
115
|
+
datapoint_type=None,
|
|
116
|
+
checksum=checksum,
|
|
117
|
+
raw_telegram=raw_telegram,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
return telegram
|
|
121
|
+
|
|
122
|
+
@staticmethod
|
|
123
|
+
def is_discover_response(reply_telegram: ReplyTelegram) -> bool:
|
|
124
|
+
"""Check if a reply telegram is a discover response.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
reply_telegram: Reply telegram to check.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
True if this is a discover response, False otherwise.
|
|
131
|
+
"""
|
|
132
|
+
return reply_telegram.system_function == SystemFunction.DISCOVERY
|
|
133
|
+
|
|
134
|
+
@staticmethod
|
|
135
|
+
def _generate_discover_response(serial_number: str) -> str:
|
|
136
|
+
"""Generate discover response telegram for a device.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
serial_number: 10-digit module serial number.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Formatted discover response telegram.
|
|
143
|
+
"""
|
|
144
|
+
# Format: <R{serial}F01D{checksum}>
|
|
145
|
+
data_part = f"R{serial_number}F01D"
|
|
146
|
+
checksum = calculate_checksum(data_part)
|
|
147
|
+
telegram = f"<{data_part}{checksum}>"
|
|
148
|
+
return telegram
|
|
149
|
+
|
|
150
|
+
@staticmethod
|
|
151
|
+
def get_unique_devices(devices: List[DeviceInfo]) -> List[DeviceInfo]:
|
|
152
|
+
"""Filter out duplicate devices based on serial number.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
devices: List of discovered devices.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
List of unique devices (first occurrence of each serial number).
|
|
159
|
+
"""
|
|
160
|
+
seen_serials: Set[str] = set()
|
|
161
|
+
unique_devices = []
|
|
162
|
+
|
|
163
|
+
for device in devices:
|
|
164
|
+
if device.serial_number not in seen_serials:
|
|
165
|
+
seen_serials.add(device.serial_number)
|
|
166
|
+
unique_devices.append(device)
|
|
167
|
+
|
|
168
|
+
return unique_devices
|
|
169
|
+
|
|
170
|
+
@staticmethod
|
|
171
|
+
def validate_discover_response_format(raw_telegram: str) -> bool:
|
|
172
|
+
"""Validate if a raw telegram matches discover response format.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
raw_telegram: Raw telegram string to validate.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
True if format matches discover response pattern.
|
|
179
|
+
"""
|
|
180
|
+
# Discover response format: <R{10-digit-serial}F01D{2-char-checksum}>
|
|
181
|
+
import re
|
|
182
|
+
|
|
183
|
+
match = re.compile(r"^<R(\d{10})F01D([A-Z0-9]{2})>$").match(
|
|
184
|
+
raw_telegram.strip()
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
return match is not None
|
|
188
|
+
|
|
189
|
+
def generate_discover_summary(self, devices: List[DeviceInfo]) -> dict:
|
|
190
|
+
"""Generate a summary of a discover results.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
devices: List of discovered devices.
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
Dictionary with discover statistics.
|
|
197
|
+
"""
|
|
198
|
+
unique_devices = self.get_unique_devices(devices)
|
|
199
|
+
valid_devices = [d for d in unique_devices if d.checksum_valid]
|
|
200
|
+
invalid_devices = [d for d in unique_devices if not d.checksum_valid]
|
|
201
|
+
|
|
202
|
+
# Group by serial number prefixes for pattern analysis
|
|
203
|
+
serial_prefixes = {}
|
|
204
|
+
for device in unique_devices:
|
|
205
|
+
prefix = device.serial_number[:4] # First 4 digits
|
|
206
|
+
if prefix not in serial_prefixes:
|
|
207
|
+
serial_prefixes[prefix] = 0
|
|
208
|
+
serial_prefixes[prefix] += 1
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
"total_responses": len(devices),
|
|
212
|
+
"unique_devices": len(unique_devices),
|
|
213
|
+
"valid_checksums": len(valid_devices),
|
|
214
|
+
"invalid_checksums": len(invalid_devices),
|
|
215
|
+
"success_rate": (
|
|
216
|
+
(len(valid_devices) / len(unique_devices) * 100)
|
|
217
|
+
if unique_devices
|
|
218
|
+
else 0
|
|
219
|
+
),
|
|
220
|
+
"duplicate_responses": len(devices) - len(unique_devices),
|
|
221
|
+
"serial_prefixes": serial_prefixes,
|
|
222
|
+
"device_list": [device.serial_number for device in valid_devices],
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
def format_discover_results(self, devices: List[DeviceInfo]) -> str:
|
|
226
|
+
"""Format discover results for human-readable output.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
devices: List of discovered devices.
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
Formatted string summary.
|
|
233
|
+
"""
|
|
234
|
+
if not devices:
|
|
235
|
+
return "No devices discovered"
|
|
236
|
+
|
|
237
|
+
summary = self.generate_discover_summary(devices)
|
|
238
|
+
unique_devices = self.get_unique_devices(devices)
|
|
239
|
+
|
|
240
|
+
lines = [
|
|
241
|
+
"=== Device Discover Results ===",
|
|
242
|
+
f"Total Responses: {summary['total_responses']}",
|
|
243
|
+
f"Unique Devices: {summary['unique_devices']}",
|
|
244
|
+
f"Valid Checksums: {summary['valid_checksums']}/{summary['unique_devices']} ({summary['success_rate']:.1f}%)",
|
|
245
|
+
]
|
|
246
|
+
|
|
247
|
+
if summary["duplicate_responses"] > 0:
|
|
248
|
+
lines.append(f"Duplicate Responses: {summary['duplicate_responses']}")
|
|
249
|
+
|
|
250
|
+
lines.extend("\nDiscovered Devices:")
|
|
251
|
+
lines.append("-" * 40)
|
|
252
|
+
|
|
253
|
+
for device in unique_devices:
|
|
254
|
+
status_icon = "✓" if device.checksum_valid else "✗"
|
|
255
|
+
lines.append(f"{status_icon} {device.serial_number}")
|
|
256
|
+
|
|
257
|
+
if summary["serial_prefixes"]:
|
|
258
|
+
lines.append("\nSerial Number Distribution:")
|
|
259
|
+
for prefix, count in sorted(summary["serial_prefixes"].items()):
|
|
260
|
+
lines.append(f" {prefix}xxxx: {count} device(s)")
|
|
261
|
+
|
|
262
|
+
return "\n".join(lines)
|
|
263
|
+
|
|
264
|
+
@staticmethod
|
|
265
|
+
def is_discover_request(telegram: SystemTelegram) -> bool:
|
|
266
|
+
"""Check if telegram is a discover request.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
telegram: System telegram to check.
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
True if this is a discover request, False otherwise.
|
|
273
|
+
"""
|
|
274
|
+
return (
|
|
275
|
+
telegram.system_function == SystemFunction.DISCOVERY
|
|
276
|
+
and telegram.serial_number == "0000000000"
|
|
277
|
+
) # Broadcast address
|