conson-xp 1.18.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. conson_xp-1.18.0.dist-info/METADATA +412 -0
  2. conson_xp-1.18.0.dist-info/RECORD +176 -0
  3. conson_xp-1.18.0.dist-info/WHEEL +4 -0
  4. conson_xp-1.18.0.dist-info/entry_points.txt +5 -0
  5. conson_xp-1.18.0.dist-info/licenses/LICENSE +29 -0
  6. xp/__init__.py +9 -0
  7. xp/cli/__init__.py +5 -0
  8. xp/cli/__main__.py +6 -0
  9. xp/cli/commands/__init__.py +153 -0
  10. xp/cli/commands/conbus/__init__.py +25 -0
  11. xp/cli/commands/conbus/conbus.py +128 -0
  12. xp/cli/commands/conbus/conbus_actiontable_commands.py +233 -0
  13. xp/cli/commands/conbus/conbus_autoreport_commands.py +108 -0
  14. xp/cli/commands/conbus/conbus_blink_commands.py +163 -0
  15. xp/cli/commands/conbus/conbus_config_commands.py +29 -0
  16. xp/cli/commands/conbus/conbus_custom_commands.py +57 -0
  17. xp/cli/commands/conbus/conbus_datapoint_commands.py +113 -0
  18. xp/cli/commands/conbus/conbus_discover_commands.py +61 -0
  19. xp/cli/commands/conbus/conbus_event_commands.py +81 -0
  20. xp/cli/commands/conbus/conbus_lightlevel_commands.py +207 -0
  21. xp/cli/commands/conbus/conbus_linknumber_commands.py +102 -0
  22. xp/cli/commands/conbus/conbus_modulenumber_commands.py +104 -0
  23. xp/cli/commands/conbus/conbus_msactiontable_commands.py +94 -0
  24. xp/cli/commands/conbus/conbus_output_commands.py +163 -0
  25. xp/cli/commands/conbus/conbus_raw_commands.py +62 -0
  26. xp/cli/commands/conbus/conbus_receive_commands.py +59 -0
  27. xp/cli/commands/conbus/conbus_scan_commands.py +58 -0
  28. xp/cli/commands/file_commands.py +186 -0
  29. xp/cli/commands/homekit/__init__.py +3 -0
  30. xp/cli/commands/homekit/homekit.py +118 -0
  31. xp/cli/commands/homekit/homekit_start_commands.py +43 -0
  32. xp/cli/commands/module_commands.py +187 -0
  33. xp/cli/commands/reverse_proxy_commands.py +178 -0
  34. xp/cli/commands/server/__init__.py +3 -0
  35. xp/cli/commands/server/server_commands.py +135 -0
  36. xp/cli/commands/telegram/__init__.py +5 -0
  37. xp/cli/commands/telegram/telegram.py +41 -0
  38. xp/cli/commands/telegram/telegram_blink_commands.py +79 -0
  39. xp/cli/commands/telegram/telegram_checksum_commands.py +112 -0
  40. xp/cli/commands/telegram/telegram_discover_commands.py +41 -0
  41. xp/cli/commands/telegram/telegram_linknumber_commands.py +86 -0
  42. xp/cli/commands/telegram/telegram_parse_commands.py +75 -0
  43. xp/cli/commands/telegram/telegram_version_commands.py +52 -0
  44. xp/cli/main.py +87 -0
  45. xp/cli/utils/__init__.py +1 -0
  46. xp/cli/utils/click_tree.py +57 -0
  47. xp/cli/utils/datapoint_type_choice.py +57 -0
  48. xp/cli/utils/decorators.py +351 -0
  49. xp/cli/utils/error_handlers.py +201 -0
  50. xp/cli/utils/formatters.py +312 -0
  51. xp/cli/utils/module_type_choice.py +56 -0
  52. xp/cli/utils/serial_number_type.py +52 -0
  53. xp/cli/utils/system_function_choice.py +57 -0
  54. xp/cli/utils/xp_module_type.py +53 -0
  55. xp/connection/__init__.py +13 -0
  56. xp/connection/exceptions.py +22 -0
  57. xp/models/__init__.py +36 -0
  58. xp/models/actiontable/__init__.py +1 -0
  59. xp/models/actiontable/actiontable.py +43 -0
  60. xp/models/actiontable/msactiontable_xp20.py +53 -0
  61. xp/models/actiontable/msactiontable_xp24.py +58 -0
  62. xp/models/actiontable/msactiontable_xp33.py +65 -0
  63. xp/models/conbus/__init__.py +1 -0
  64. xp/models/conbus/conbus.py +87 -0
  65. xp/models/conbus/conbus_autoreport.py +67 -0
  66. xp/models/conbus/conbus_blink.py +80 -0
  67. xp/models/conbus/conbus_client_config.py +55 -0
  68. xp/models/conbus/conbus_connection_status.py +40 -0
  69. xp/models/conbus/conbus_custom.py +58 -0
  70. xp/models/conbus/conbus_datapoint.py +89 -0
  71. xp/models/conbus/conbus_discover.py +64 -0
  72. xp/models/conbus/conbus_event_raw.py +47 -0
  73. xp/models/conbus/conbus_lightlevel.py +52 -0
  74. xp/models/conbus/conbus_linknumber.py +54 -0
  75. xp/models/conbus/conbus_output.py +57 -0
  76. xp/models/conbus/conbus_raw.py +45 -0
  77. xp/models/conbus/conbus_receive.py +42 -0
  78. xp/models/conbus/conbus_writeconfig.py +60 -0
  79. xp/models/homekit/__init__.py +1 -0
  80. xp/models/homekit/homekit_accessory.py +35 -0
  81. xp/models/homekit/homekit_config.py +106 -0
  82. xp/models/homekit/homekit_conson_config.py +86 -0
  83. xp/models/log_entry.py +130 -0
  84. xp/models/protocol/__init__.py +1 -0
  85. xp/models/protocol/conbus_protocol.py +312 -0
  86. xp/models/response.py +42 -0
  87. xp/models/telegram/__init__.py +1 -0
  88. xp/models/telegram/action_type.py +31 -0
  89. xp/models/telegram/datapoint_type.py +82 -0
  90. xp/models/telegram/event_telegram.py +140 -0
  91. xp/models/telegram/event_type.py +15 -0
  92. xp/models/telegram/input_action_type.py +69 -0
  93. xp/models/telegram/input_type.py +17 -0
  94. xp/models/telegram/module_type.py +188 -0
  95. xp/models/telegram/module_type_code.py +205 -0
  96. xp/models/telegram/output_telegram.py +103 -0
  97. xp/models/telegram/reply_telegram.py +297 -0
  98. xp/models/telegram/system_function.py +116 -0
  99. xp/models/telegram/system_telegram.py +94 -0
  100. xp/models/telegram/telegram.py +28 -0
  101. xp/models/telegram/telegram_type.py +19 -0
  102. xp/models/telegram/timeparam_type.py +51 -0
  103. xp/models/write_config_type.py +33 -0
  104. xp/services/__init__.py +26 -0
  105. xp/services/actiontable/__init__.py +1 -0
  106. xp/services/actiontable/actiontable_serializer.py +273 -0
  107. xp/services/actiontable/msactiontable_serializer.py +7 -0
  108. xp/services/actiontable/msactiontable_xp20_serializer.py +169 -0
  109. xp/services/actiontable/msactiontable_xp24_serializer.py +120 -0
  110. xp/services/actiontable/msactiontable_xp33_serializer.py +239 -0
  111. xp/services/conbus/__init__.py +1 -0
  112. xp/services/conbus/actiontable/__init__.py +1 -0
  113. xp/services/conbus/actiontable/actiontable_download_service.py +158 -0
  114. xp/services/conbus/actiontable/actiontable_list_service.py +91 -0
  115. xp/services/conbus/actiontable/actiontable_show_service.py +89 -0
  116. xp/services/conbus/actiontable/actiontable_upload_service.py +211 -0
  117. xp/services/conbus/actiontable/msactiontable_service.py +232 -0
  118. xp/services/conbus/conbus_blink_all_service.py +181 -0
  119. xp/services/conbus/conbus_blink_service.py +158 -0
  120. xp/services/conbus/conbus_custom_service.py +156 -0
  121. xp/services/conbus/conbus_datapoint_queryall_service.py +182 -0
  122. xp/services/conbus/conbus_datapoint_service.py +170 -0
  123. xp/services/conbus/conbus_discover_service.py +312 -0
  124. xp/services/conbus/conbus_event_raw_service.py +181 -0
  125. xp/services/conbus/conbus_output_service.py +194 -0
  126. xp/services/conbus/conbus_raw_service.py +122 -0
  127. xp/services/conbus/conbus_receive_service.py +115 -0
  128. xp/services/conbus/conbus_scan_service.py +150 -0
  129. xp/services/conbus/write_config_service.py +194 -0
  130. xp/services/homekit/__init__.py +1 -0
  131. xp/services/homekit/homekit_cache_service.py +307 -0
  132. xp/services/homekit/homekit_conbus_service.py +93 -0
  133. xp/services/homekit/homekit_config_validator.py +310 -0
  134. xp/services/homekit/homekit_conson_validator.py +121 -0
  135. xp/services/homekit/homekit_dimminglight.py +182 -0
  136. xp/services/homekit/homekit_dimminglight_service.py +148 -0
  137. xp/services/homekit/homekit_hap_service.py +342 -0
  138. xp/services/homekit/homekit_lightbulb.py +120 -0
  139. xp/services/homekit/homekit_lightbulb_service.py +86 -0
  140. xp/services/homekit/homekit_module_service.py +56 -0
  141. xp/services/homekit/homekit_outlet.py +168 -0
  142. xp/services/homekit/homekit_outlet_service.py +121 -0
  143. xp/services/homekit/homekit_service.py +359 -0
  144. xp/services/log_file_service.py +309 -0
  145. xp/services/module_type_service.py +257 -0
  146. xp/services/protocol/__init__.py +21 -0
  147. xp/services/protocol/conbus_event_protocol.py +360 -0
  148. xp/services/protocol/conbus_protocol.py +318 -0
  149. xp/services/protocol/protocol_factory.py +78 -0
  150. xp/services/protocol/telegram_protocol.py +264 -0
  151. xp/services/reverse_proxy_service.py +435 -0
  152. xp/services/server/__init__.py +1 -0
  153. xp/services/server/base_server_service.py +366 -0
  154. xp/services/server/cp20_server_service.py +65 -0
  155. xp/services/server/device_service_factory.py +94 -0
  156. xp/services/server/server_service.py +428 -0
  157. xp/services/server/xp130_server_service.py +67 -0
  158. xp/services/server/xp20_server_service.py +92 -0
  159. xp/services/server/xp230_server_service.py +58 -0
  160. xp/services/server/xp24_server_service.py +245 -0
  161. xp/services/server/xp33_server_service.py +535 -0
  162. xp/services/telegram/__init__.py +1 -0
  163. xp/services/telegram/telegram_blink_service.py +138 -0
  164. xp/services/telegram/telegram_checksum_service.py +149 -0
  165. xp/services/telegram/telegram_datapoint_service.py +82 -0
  166. xp/services/telegram/telegram_discover_service.py +277 -0
  167. xp/services/telegram/telegram_link_number_service.py +216 -0
  168. xp/services/telegram/telegram_output_service.py +322 -0
  169. xp/services/telegram/telegram_service.py +380 -0
  170. xp/services/telegram/telegram_version_service.py +288 -0
  171. xp/utils/__init__.py +12 -0
  172. xp/utils/checksum.py +61 -0
  173. xp/utils/dependencies.py +531 -0
  174. xp/utils/event_helper.py +31 -0
  175. xp/utils/serialization.py +205 -0
  176. xp/utils/time_utils.py +134 -0
@@ -0,0 +1,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()