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,309 @@
|
|
|
1
|
+
"""Log file parsing service for console bus communication logs."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
from xp.models.log_entry import LogEntry
|
|
9
|
+
from xp.services.telegram.telegram_service import TelegramParsingError, TelegramService
|
|
10
|
+
from xp.utils.time_utils import (
|
|
11
|
+
TimeParsingError,
|
|
12
|
+
calculate_duration_ms,
|
|
13
|
+
parse_log_timestamp,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class LogFileParsingError(Exception):
|
|
18
|
+
"""Raised when log file parsing fails."""
|
|
19
|
+
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class LogFileService:
|
|
24
|
+
"""
|
|
25
|
+
Service for parsing console bus log files.
|
|
26
|
+
|
|
27
|
+
Handles parsing of log files containing timestamped telegram transmissions
|
|
28
|
+
and receptions with automatic telegram parsing and validation.
|
|
29
|
+
|
|
30
|
+
Attributes:
|
|
31
|
+
telegram_service: Telegram service for parsing telegrams.
|
|
32
|
+
LOG_LINE_PATTERN: Regex pattern for log line format.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
# Regex pattern for log line format: HH:MM:SS,mmm [TX/RX] <telegram>
|
|
36
|
+
LOG_LINE_PATTERN = re.compile(
|
|
37
|
+
r"^(\d{2}:\d{2}:\d{2},\d{3})\s+\[([TR]X)\]\s+(<[^>]+>)\s*$"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def __init__(self, telegram_service: TelegramService):
|
|
41
|
+
"""Initialize the log file service.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
telegram_service: Telegram service for parsing telegrams.
|
|
45
|
+
"""
|
|
46
|
+
self.telegram_service = telegram_service
|
|
47
|
+
|
|
48
|
+
def parse_log_file(
|
|
49
|
+
self, file_path: str, base_date: Optional[datetime] = None
|
|
50
|
+
) -> List[LogEntry]:
|
|
51
|
+
"""Parse a console bus log file into LogEntry objects.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
file_path: Path to the log file.
|
|
55
|
+
base_date: Base date for timestamps (defaults to today).
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
List of parsed LogEntry objects.
|
|
59
|
+
|
|
60
|
+
Raises:
|
|
61
|
+
LogFileParsingError: If file cannot be read or parsed.
|
|
62
|
+
"""
|
|
63
|
+
try:
|
|
64
|
+
path = Path(file_path)
|
|
65
|
+
if not path.exists():
|
|
66
|
+
raise LogFileParsingError(f"Log file not found: {file_path}")
|
|
67
|
+
|
|
68
|
+
if not path.is_file():
|
|
69
|
+
raise LogFileParsingError(f"Path is not a file: {file_path}")
|
|
70
|
+
|
|
71
|
+
with Path(path).open("r", encoding="utf-8", errors="replace") as f:
|
|
72
|
+
lines = f.readlines()
|
|
73
|
+
|
|
74
|
+
return self.parse_log_lines(lines, base_date)
|
|
75
|
+
|
|
76
|
+
except IOError as e:
|
|
77
|
+
raise LogFileParsingError(f"Error reading log file {file_path}: {e}")
|
|
78
|
+
|
|
79
|
+
def parse_log_lines(
|
|
80
|
+
self, lines: List[str], base_date: Optional[datetime] = None
|
|
81
|
+
) -> List[LogEntry]:
|
|
82
|
+
"""Parse log lines into LogEntry objects.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
lines: List of log lines to parse.
|
|
86
|
+
base_date: Base date for timestamps.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
List of parsed LogEntry objects.
|
|
90
|
+
"""
|
|
91
|
+
entries = []
|
|
92
|
+
|
|
93
|
+
for line_number, line in enumerate(lines, 1):
|
|
94
|
+
line = line.strip()
|
|
95
|
+
if not line: # Skip empty lines
|
|
96
|
+
continue
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
entry = self._parse_log_line(line, line_number, base_date)
|
|
100
|
+
if entry:
|
|
101
|
+
entries.append(entry)
|
|
102
|
+
except Exception as e:
|
|
103
|
+
# Create entry with parse error for malformed lines
|
|
104
|
+
entry = LogEntry(
|
|
105
|
+
timestamp=base_date or datetime.now(),
|
|
106
|
+
direction="UNKNOWN",
|
|
107
|
+
raw_telegram=line,
|
|
108
|
+
parse_error=f"Line parsing failed: {e}",
|
|
109
|
+
line_number=line_number,
|
|
110
|
+
)
|
|
111
|
+
entries.append(entry)
|
|
112
|
+
|
|
113
|
+
return entries
|
|
114
|
+
|
|
115
|
+
def _parse_log_line(
|
|
116
|
+
self, line: str, line_number: int, base_date: Optional[datetime] = None
|
|
117
|
+
) -> Optional[LogEntry]:
|
|
118
|
+
"""Parse a single log line into a LogEntry.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
line: Log line to parse.
|
|
122
|
+
line_number: Line number in the file.
|
|
123
|
+
base_date: Base date for timestamp.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
LogEntry object or None if line format is invalid.
|
|
127
|
+
"""
|
|
128
|
+
match = self.LOG_LINE_PATTERN.match(line)
|
|
129
|
+
if not match:
|
|
130
|
+
raise LogFileParsingError(f"Invalid log line format: {line}")
|
|
131
|
+
|
|
132
|
+
timestamp_str = match.group(1)
|
|
133
|
+
direction = match.group(2)
|
|
134
|
+
telegram_str = match.group(3)
|
|
135
|
+
|
|
136
|
+
# Parse timestamp
|
|
137
|
+
try:
|
|
138
|
+
timestamp = parse_log_timestamp(timestamp_str, base_date)
|
|
139
|
+
except TimeParsingError as e:
|
|
140
|
+
raise LogFileParsingError(f"Invalid timestamp in line {line_number}: {e}")
|
|
141
|
+
|
|
142
|
+
# Create initial log entry
|
|
143
|
+
entry = LogEntry(
|
|
144
|
+
timestamp=timestamp,
|
|
145
|
+
direction=direction,
|
|
146
|
+
raw_telegram=telegram_str,
|
|
147
|
+
line_number=line_number,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# Try to parse the telegram
|
|
151
|
+
try:
|
|
152
|
+
parsed_telegram = self.telegram_service.parse_telegram(telegram_str)
|
|
153
|
+
entry.parsed_telegram = parsed_telegram
|
|
154
|
+
except TelegramParsingError as e:
|
|
155
|
+
entry.parse_error = str(e)
|
|
156
|
+
|
|
157
|
+
return entry
|
|
158
|
+
|
|
159
|
+
def validate_log_format(self, file_path: str) -> bool:
|
|
160
|
+
"""Validate that a file follows the expected log format.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
file_path: Path to the log file.
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
True if format is valid, False otherwise.
|
|
167
|
+
"""
|
|
168
|
+
try:
|
|
169
|
+
entries = self.parse_log_file(file_path)
|
|
170
|
+
# Check if at least some entries parsed successfully
|
|
171
|
+
valid_entries = [e for e in entries if e.is_valid_parse]
|
|
172
|
+
return len(valid_entries) > 0
|
|
173
|
+
except LogFileParsingError:
|
|
174
|
+
return False
|
|
175
|
+
|
|
176
|
+
def extract_telegrams(self, file_path: str) -> List[str]:
|
|
177
|
+
"""Extract all telegram strings from a log file.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
file_path: Path to the log file.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
List of telegram strings.
|
|
184
|
+
"""
|
|
185
|
+
entries = self.parse_log_file(file_path)
|
|
186
|
+
return [entry.raw_telegram for entry in entries]
|
|
187
|
+
|
|
188
|
+
@staticmethod
|
|
189
|
+
def get_file_statistics(entries: List[LogEntry]) -> Dict[str, Any]:
|
|
190
|
+
"""Generate statistics for a list of log entries.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
entries: List of LogEntry objects.
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
Dictionary containing statistics.
|
|
197
|
+
"""
|
|
198
|
+
if not entries:
|
|
199
|
+
return {"total_entries": 0}
|
|
200
|
+
|
|
201
|
+
# Basic counts
|
|
202
|
+
total_entries = len(entries)
|
|
203
|
+
valid_parses = len([e for e in entries if e.is_valid_parse])
|
|
204
|
+
parse_errors = total_entries - valid_parses
|
|
205
|
+
|
|
206
|
+
# Direction counts
|
|
207
|
+
tx_count = len([e for e in entries if e.direction == "TX"])
|
|
208
|
+
rx_count = len([e for e in entries if e.direction == "RX"])
|
|
209
|
+
|
|
210
|
+
# Type counts
|
|
211
|
+
event_count = len([e for e in entries if e.telegram_type == "E"])
|
|
212
|
+
system_count = len([e for e in entries if e.telegram_type == "S"])
|
|
213
|
+
reply_count = len([e for e in entries if e.telegram_type == "R"])
|
|
214
|
+
unknown_count = len([e for e in entries if e.telegram_type == "unknown"])
|
|
215
|
+
|
|
216
|
+
# Checksum validation
|
|
217
|
+
validated_entries = [e for e in entries if e.checksum_validated is not None]
|
|
218
|
+
valid_checksums = len([e for e in validated_entries if e.checksum_validated])
|
|
219
|
+
invalid_checksums = len(validated_entries) - valid_checksums
|
|
220
|
+
|
|
221
|
+
# Time range
|
|
222
|
+
timestamps = [e.timestamp for e in entries]
|
|
223
|
+
start_time = min(timestamps) if timestamps else None
|
|
224
|
+
end_time = max(timestamps) if timestamps else None
|
|
225
|
+
duration_ms = (
|
|
226
|
+
calculate_duration_ms(start_time, end_time)
|
|
227
|
+
if start_time and end_time
|
|
228
|
+
else 0
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
# Device analysis
|
|
232
|
+
devices = set()
|
|
233
|
+
for entry in entries:
|
|
234
|
+
if entry.parsed_telegram:
|
|
235
|
+
if hasattr(entry.parsed_telegram, "serial_number"):
|
|
236
|
+
devices.add(entry.parsed_telegram.serial_number)
|
|
237
|
+
elif hasattr(entry.parsed_telegram, "module_type"):
|
|
238
|
+
devices.add(f"Module_{entry.parsed_telegram.module_type}")
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
"total_entries": total_entries,
|
|
242
|
+
"valid_parses": valid_parses,
|
|
243
|
+
"parse_errors": parse_errors,
|
|
244
|
+
"parse_success_rate": (
|
|
245
|
+
(valid_parses / total_entries * 100) if total_entries > 0 else 0
|
|
246
|
+
),
|
|
247
|
+
"direction_counts": {"tx": tx_count, "rx": rx_count},
|
|
248
|
+
"telegram_type_counts": {
|
|
249
|
+
"event": event_count,
|
|
250
|
+
"system": system_count,
|
|
251
|
+
"reply": reply_count,
|
|
252
|
+
"unknown": unknown_count,
|
|
253
|
+
},
|
|
254
|
+
"checksum_validation": {
|
|
255
|
+
"validated_count": len(validated_entries),
|
|
256
|
+
"valid_checksums": valid_checksums,
|
|
257
|
+
"invalid_checksums": invalid_checksums,
|
|
258
|
+
"validation_success_rate": (
|
|
259
|
+
(valid_checksums / len(validated_entries) * 100)
|
|
260
|
+
if validated_entries
|
|
261
|
+
else 0
|
|
262
|
+
),
|
|
263
|
+
},
|
|
264
|
+
"time_range": {
|
|
265
|
+
"start": (
|
|
266
|
+
start_time.strftime("%H:%M:%S.%f")[:-3] if start_time else None
|
|
267
|
+
),
|
|
268
|
+
"end": end_time.strftime("%H:%M:%S.%f")[:-3] if end_time else None,
|
|
269
|
+
"duration_ms": duration_ms,
|
|
270
|
+
"duration_seconds": duration_ms / 1000 if duration_ms > 0 else 0,
|
|
271
|
+
},
|
|
272
|
+
"devices": sorted(list(devices)),
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
@staticmethod
|
|
276
|
+
def filter_entries(
|
|
277
|
+
entries: List[LogEntry],
|
|
278
|
+
telegram_type: Optional[str] = None,
|
|
279
|
+
direction: Optional[str] = None,
|
|
280
|
+
start_time: Optional[datetime] = None,
|
|
281
|
+
end_time: Optional[datetime] = None,
|
|
282
|
+
) -> List[LogEntry]:
|
|
283
|
+
"""Filter log entries based on criteria.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
entries: List of LogEntry objects to filter.
|
|
287
|
+
telegram_type: Filter by telegram type (event, system, reply).
|
|
288
|
+
direction: Filter by direction (TX, RX).
|
|
289
|
+
start_time: Filter entries after this time.
|
|
290
|
+
end_time: Filter entries before this time.
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
Filtered list of LogEntry objects.
|
|
294
|
+
"""
|
|
295
|
+
filtered = entries.copy()
|
|
296
|
+
|
|
297
|
+
if telegram_type:
|
|
298
|
+
filtered = [e for e in filtered if e.telegram_type == telegram_type.lower()]
|
|
299
|
+
|
|
300
|
+
if direction:
|
|
301
|
+
filtered = [e for e in filtered if e.direction == direction.upper()]
|
|
302
|
+
|
|
303
|
+
if start_time:
|
|
304
|
+
filtered = [e for e in filtered if e.timestamp >= start_time]
|
|
305
|
+
|
|
306
|
+
if end_time:
|
|
307
|
+
filtered = [e for e in filtered if e.timestamp <= end_time]
|
|
308
|
+
|
|
309
|
+
return filtered
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""Module Type Service for XP module management.
|
|
2
|
+
|
|
3
|
+
This module provides lookup, validation, and search functionality for XP system module types.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Dict, List, Optional, Union
|
|
7
|
+
|
|
8
|
+
from xp.models.telegram.module_type import (
|
|
9
|
+
ModuleType,
|
|
10
|
+
get_all_module_types,
|
|
11
|
+
get_module_types_by_category,
|
|
12
|
+
is_valid_module_code,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ModuleTypeNotFoundError(Exception):
|
|
17
|
+
"""Raised when a module type cannot be found."""
|
|
18
|
+
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ModuleTypeService:
|
|
23
|
+
"""
|
|
24
|
+
Service for managing module type operations.
|
|
25
|
+
|
|
26
|
+
Provides lookup, validation, and search functionality for XP system module types.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self) -> None:
|
|
30
|
+
"""Initialize the module type service."""
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
@staticmethod
|
|
34
|
+
def get_module_type(identifier: Union[int, str]) -> ModuleType:
|
|
35
|
+
"""
|
|
36
|
+
Get module type by code or name.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
identifier: Module code (int) or name (str)
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
ModuleType instance
|
|
43
|
+
|
|
44
|
+
Raises:
|
|
45
|
+
ModuleTypeNotFoundError: If module type is not found
|
|
46
|
+
"""
|
|
47
|
+
if isinstance(identifier, int):
|
|
48
|
+
module_type = ModuleType.from_code(identifier)
|
|
49
|
+
if not module_type:
|
|
50
|
+
raise ModuleTypeNotFoundError(
|
|
51
|
+
f"Module type with code {identifier} not found"
|
|
52
|
+
)
|
|
53
|
+
elif isinstance(identifier, str):
|
|
54
|
+
module_type = ModuleType.from_name(identifier)
|
|
55
|
+
if not module_type:
|
|
56
|
+
raise ModuleTypeNotFoundError(
|
|
57
|
+
f"Module type with name '{identifier}' not found"
|
|
58
|
+
)
|
|
59
|
+
else:
|
|
60
|
+
raise ModuleTypeNotFoundError(
|
|
61
|
+
f"Invalid identifier type: {type(identifier)}"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
return module_type
|
|
65
|
+
|
|
66
|
+
@staticmethod
|
|
67
|
+
def list_all_modules() -> List[ModuleType]:
|
|
68
|
+
"""
|
|
69
|
+
Get all available module types.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
List of all ModuleType instances
|
|
73
|
+
"""
|
|
74
|
+
return get_all_module_types()
|
|
75
|
+
|
|
76
|
+
@staticmethod
|
|
77
|
+
def list_modules_by_category() -> Dict[str, List[ModuleType]]:
|
|
78
|
+
"""
|
|
79
|
+
Get module types grouped by category.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Dictionary with category names as keys and lists of ModuleType as values
|
|
83
|
+
"""
|
|
84
|
+
return get_module_types_by_category()
|
|
85
|
+
|
|
86
|
+
@staticmethod
|
|
87
|
+
def search_modules(
|
|
88
|
+
query: str, search_fields: Optional[List[str]] = None
|
|
89
|
+
) -> List[ModuleType]:
|
|
90
|
+
"""
|
|
91
|
+
Search for module types matching a query string.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
query: Search query string
|
|
95
|
+
search_fields: Fields to search in ('name', 'description'). Defaults to both.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
List of matching ModuleType instances
|
|
99
|
+
"""
|
|
100
|
+
if search_fields is None:
|
|
101
|
+
search_fields = ["name", "description"]
|
|
102
|
+
|
|
103
|
+
query_lower = query.lower()
|
|
104
|
+
matching_modules = []
|
|
105
|
+
|
|
106
|
+
for module_type in get_all_module_types():
|
|
107
|
+
match_found = False
|
|
108
|
+
|
|
109
|
+
if "name" in search_fields and query_lower in module_type.name.lower():
|
|
110
|
+
match_found = True
|
|
111
|
+
elif (
|
|
112
|
+
"description" in search_fields
|
|
113
|
+
and query_lower in module_type.description.lower()
|
|
114
|
+
):
|
|
115
|
+
match_found = True
|
|
116
|
+
|
|
117
|
+
if match_found:
|
|
118
|
+
matching_modules.append(module_type)
|
|
119
|
+
|
|
120
|
+
return matching_modules
|
|
121
|
+
|
|
122
|
+
@staticmethod
|
|
123
|
+
def get_modules_by_category(category: str) -> List[ModuleType]:
|
|
124
|
+
"""
|
|
125
|
+
Get all module types in a specific category.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
category: Category name
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
List of ModuleType instances in the category
|
|
132
|
+
"""
|
|
133
|
+
return get_module_types_by_category().get(category, [])
|
|
134
|
+
|
|
135
|
+
@staticmethod
|
|
136
|
+
def get_push_button_panels() -> List[ModuleType]:
|
|
137
|
+
"""
|
|
138
|
+
Get all push button panel module types.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
List of push button panel ModuleType instances
|
|
142
|
+
"""
|
|
143
|
+
return [
|
|
144
|
+
module for module in get_all_module_types() if module.is_push_button_panel
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
@staticmethod
|
|
148
|
+
def get_ir_capable_modules() -> List[ModuleType]:
|
|
149
|
+
"""
|
|
150
|
+
Get all IR-capable module types.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
List of IR-capable ModuleType instances
|
|
154
|
+
"""
|
|
155
|
+
return [module for module in get_all_module_types() if module.is_ir_capable]
|
|
156
|
+
|
|
157
|
+
@staticmethod
|
|
158
|
+
def validate_module_code(code: int) -> bool:
|
|
159
|
+
"""
|
|
160
|
+
Validate if a module code is valid.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
code: Module type code to validate
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
True if valid, False otherwise
|
|
167
|
+
"""
|
|
168
|
+
return is_valid_module_code(code)
|
|
169
|
+
|
|
170
|
+
def get_module_info_summary(self, identifier: Union[int, str]) -> str:
|
|
171
|
+
"""
|
|
172
|
+
Get a human-readable summary of a module type.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
identifier: Module code (int) or name (str)
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
Formatted string with module information
|
|
179
|
+
"""
|
|
180
|
+
try:
|
|
181
|
+
module_type = self.get_module_type(identifier)
|
|
182
|
+
return self._format_module_summary(module_type)
|
|
183
|
+
except ModuleTypeNotFoundError as e:
|
|
184
|
+
return f"Error: {e}"
|
|
185
|
+
|
|
186
|
+
def get_all_modules_summary(self, group_by_category: bool = False) -> str:
|
|
187
|
+
"""
|
|
188
|
+
Get a formatted summary of all module types.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
group_by_category: Whether to group modules by category
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
Formatted string with all module information
|
|
195
|
+
"""
|
|
196
|
+
if group_by_category:
|
|
197
|
+
return self._format_modules_by_category()
|
|
198
|
+
return self._format_all_modules()
|
|
199
|
+
|
|
200
|
+
@staticmethod
|
|
201
|
+
def _format_module_summary(module_type: ModuleType) -> str:
|
|
202
|
+
"""Format a single module type for display.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
module_type: The module type to format.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
Formatted string with module information.
|
|
209
|
+
"""
|
|
210
|
+
summary = f"Module: {module_type.name} (Code {module_type.code})\n"
|
|
211
|
+
summary += f"Description: {module_type.description}\n"
|
|
212
|
+
summary += f"Category: {module_type.category}\n"
|
|
213
|
+
|
|
214
|
+
features = []
|
|
215
|
+
if module_type.is_push_button_panel:
|
|
216
|
+
features.append("Push Button Panel")
|
|
217
|
+
if module_type.is_ir_capable:
|
|
218
|
+
features.append("IR Capable")
|
|
219
|
+
if module_type.is_reserved:
|
|
220
|
+
features.append("Reserved")
|
|
221
|
+
|
|
222
|
+
if features:
|
|
223
|
+
summary += f"Features: {', '.join(features)}\n"
|
|
224
|
+
|
|
225
|
+
return summary.strip()
|
|
226
|
+
|
|
227
|
+
@staticmethod
|
|
228
|
+
def _format_all_modules() -> str:
|
|
229
|
+
"""Format all modules in a simple list.
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
Formatted string with all modules.
|
|
233
|
+
"""
|
|
234
|
+
modules = get_all_module_types()
|
|
235
|
+
lines = ["Code | Name | Description", "-" * 60]
|
|
236
|
+
|
|
237
|
+
for module in modules:
|
|
238
|
+
lines.append(f"{module.code:4} | {module.name:10} | {module.description}")
|
|
239
|
+
|
|
240
|
+
return "\n".join(lines)
|
|
241
|
+
|
|
242
|
+
@staticmethod
|
|
243
|
+
def _format_modules_by_category() -> str:
|
|
244
|
+
"""Format modules grouped by category.
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
Formatted string with modules grouped by category.
|
|
248
|
+
"""
|
|
249
|
+
categories = get_module_types_by_category()
|
|
250
|
+
lines = []
|
|
251
|
+
|
|
252
|
+
for category, modules in categories.items():
|
|
253
|
+
lines.append(f"\n=== {category} ===")
|
|
254
|
+
for module in modules:
|
|
255
|
+
lines.append(f" {module.code:2} - {module.name}: {module.description}")
|
|
256
|
+
|
|
257
|
+
return "\n".join(lines).strip()
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Protocol layer services for XP."""
|
|
2
|
+
|
|
3
|
+
from xp.models.protocol.conbus_protocol import (
|
|
4
|
+
ConnectionMadeEvent,
|
|
5
|
+
EventTelegramReceivedEvent,
|
|
6
|
+
InvalidTelegramReceivedEvent,
|
|
7
|
+
ModuleDiscoveredEvent,
|
|
8
|
+
TelegramReceivedEvent,
|
|
9
|
+
)
|
|
10
|
+
from xp.services.protocol.conbus_event_protocol import ConbusEventProtocol
|
|
11
|
+
from xp.services.protocol.conbus_protocol import ConbusProtocol
|
|
12
|
+
from xp.services.protocol.telegram_protocol import TelegramProtocol
|
|
13
|
+
|
|
14
|
+
__all__ = ["TelegramProtocol", "ConbusProtocol", "ConbusEventProtocol"]
|
|
15
|
+
|
|
16
|
+
# Rebuild models after TelegramProtocol and ConbusProtocol are imported to resolve forward references
|
|
17
|
+
ConnectionMadeEvent.model_rebuild()
|
|
18
|
+
InvalidTelegramReceivedEvent.model_rebuild()
|
|
19
|
+
ModuleDiscoveredEvent.model_rebuild()
|
|
20
|
+
TelegramReceivedEvent.model_rebuild()
|
|
21
|
+
EventTelegramReceivedEvent.model_rebuild()
|