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,312 @@
1
+ """Output formatting utilities for CLI commands."""
2
+
3
+ import json
4
+ from typing import Any, Dict, Optional
5
+
6
+
7
+ class OutputFormatter:
8
+ """Handles standardized output formatting for CLI commands."""
9
+
10
+ def __init__(self, json_output: bool = False):
11
+ """Initialize the output formatter.
12
+
13
+ Args:
14
+ json_output: Whether to format output as JSON (default: False).
15
+ """
16
+ self.json_output = json_output
17
+
18
+ def success_response(self, data: Dict[str, Any]) -> str:
19
+ """Format a successful response.
20
+
21
+ Args:
22
+ data: Response data to format.
23
+
24
+ Returns:
25
+ Formatted success response as string.
26
+ """
27
+ if self.json_output:
28
+ return json.dumps(data, indent=2)
29
+ return self._format_text_response(data)
30
+
31
+ def error_response(
32
+ self, error: str, extra_data: Optional[Dict[str, Any]] = None
33
+ ) -> str:
34
+ """Format an error response.
35
+
36
+ Args:
37
+ error: Error message.
38
+ extra_data: Additional error data to include.
39
+
40
+ Returns:
41
+ Formatted error response as string.
42
+ """
43
+ error_data = {"success": False, "error": error}
44
+ if extra_data:
45
+ error_data.update(extra_data)
46
+
47
+ if self.json_output:
48
+ return json.dumps(error_data, indent=2)
49
+ return f"Error: {error}"
50
+
51
+ def validation_response(self, is_valid: bool, data: Dict[str, Any]) -> str:
52
+ """Format a validation response.
53
+
54
+ Args:
55
+ is_valid: Whether validation passed.
56
+ data: Validation data to include.
57
+
58
+ Returns:
59
+ Formatted validation response as string.
60
+ """
61
+ if self.json_output:
62
+ response_data = {"valid": is_valid} | data
63
+ return json.dumps(response_data, indent=2)
64
+
65
+ status = "✓ Valid" if is_valid else "✗ Invalid"
66
+ return f"Status: {status}"
67
+
68
+ def checksum_status(self, is_valid: bool) -> str:
69
+ """Format checksum validation status.
70
+
71
+ Args:
72
+ is_valid: Whether checksum is valid.
73
+
74
+ Returns:
75
+ Formatted checksum status as string.
76
+ """
77
+ if self.json_output:
78
+ return json.dumps({"checksum_valid": is_valid}, indent=2)
79
+
80
+ return "✓ Valid" if is_valid else "✗ Invalid"
81
+
82
+ @staticmethod
83
+ def _format_text_response(data: Dict[str, Any]) -> str:
84
+ """Format data for human-readable text output.
85
+
86
+ Args:
87
+ data: Data dictionary to format.
88
+
89
+ Returns:
90
+ Formatted text output as string.
91
+ """
92
+ lines = []
93
+
94
+ # Handle common data patterns
95
+ if "telegram" in data:
96
+ lines.append(f"Telegram: {data['telegram']}")
97
+
98
+ if "serial_number" in data:
99
+ lines.append(f"Serial: {data['serial_number']}")
100
+
101
+ if "operation" in data:
102
+ lines.append(f"Operation: {data['operation']}")
103
+
104
+ if "count" in data:
105
+ lines.append(f"Count: {data['count']}")
106
+
107
+ # Add any remaining fields
108
+ for key, value in data.items():
109
+ if key not in ("telegram", "serial_number", "operation", "count"):
110
+ if isinstance(value, (str, int, float)):
111
+ lines.append(f"{key.replace('_', ' ').title()}: {value}")
112
+
113
+ return "\n".join(lines)
114
+
115
+
116
+ class TelegramFormatter(OutputFormatter):
117
+ """Specialized formatter for telegram-related output."""
118
+
119
+ def format_telegram_summary(
120
+ self, telegram_data: Dict[str, Any], service_formatter_method: Any = None
121
+ ) -> str:
122
+ """Format telegram summary using service method when available.
123
+
124
+ Args:
125
+ telegram_data: Telegram data to format.
126
+ service_formatter_method: Optional service formatter method.
127
+
128
+ Returns:
129
+ Formatted telegram summary as string.
130
+ """
131
+ if self.json_output:
132
+ return json.dumps(telegram_data, indent=2)
133
+
134
+ if service_formatter_method:
135
+ return str(service_formatter_method)
136
+
137
+ # Fallback formatting
138
+ lines = []
139
+ if "telegram_type" in telegram_data:
140
+ lines.append(f"Type: {telegram_data['telegram_type'].title()}")
141
+ if "raw_telegram" in telegram_data:
142
+ lines.append(f"Raw: {telegram_data['raw_telegram']}")
143
+ if "timestamp" in telegram_data:
144
+ lines.append(f"Timestamp: {telegram_data['timestamp']}")
145
+
146
+ return "\n".join(lines)
147
+
148
+ def format_validation_result(
149
+ self, parsed_telegram: Any, checksum_valid: Optional[bool], service_summary: str
150
+ ) -> str:
151
+ """Format telegram validation results.
152
+
153
+ Args:
154
+ parsed_telegram: Parsed telegram object.
155
+ checksum_valid: Whether checksum is valid.
156
+ service_summary: Summary from service.
157
+
158
+ Returns:
159
+ Formatted validation result as string.
160
+ """
161
+ if self.json_output:
162
+ output = parsed_telegram.to_dict()
163
+ output["checksum_valid"] = checksum_valid
164
+ return json.dumps(output, indent=2)
165
+
166
+ lines = [service_summary]
167
+ if checksum_valid is not None:
168
+ status = "✓ Valid" if checksum_valid else "✗ Invalid"
169
+ lines.append(f"Checksum validation: {status}")
170
+
171
+ return "\n".join(lines)
172
+
173
+
174
+ class ListFormatter(OutputFormatter):
175
+ """Specialized formatter for list-based output."""
176
+
177
+ def format_list_response(
178
+ self, items: list, title: str, item_formatter: Any = None
179
+ ) -> str:
180
+ """Format a list of items with optional custom formatter.
181
+
182
+ Args:
183
+ items: List of items to format.
184
+ title: Title for the list.
185
+ item_formatter: Optional custom formatter function.
186
+
187
+ Returns:
188
+ Formatted list as string.
189
+ """
190
+ if self.json_output:
191
+ return json.dumps(
192
+ {
193
+ "items": [
194
+ item.to_dict() if hasattr(item, "to_dict") else item
195
+ for item in items
196
+ ],
197
+ "count": len(items),
198
+ },
199
+ indent=2,
200
+ )
201
+
202
+ lines = [f"{title}: {len(items)} items", "-" * 50]
203
+
204
+ for i, item in enumerate(items, 1):
205
+ if item_formatter:
206
+ lines.append(f"{i}. {item_formatter(item)}")
207
+ elif hasattr(item, "__str__"):
208
+ lines.append(f"{i}. {item}")
209
+ else:
210
+ lines.append(f"{i}. {item}")
211
+
212
+ return "\n".join(lines)
213
+
214
+ def format_search_results(self, matches: list, query: str) -> str:
215
+ """Format search results.
216
+
217
+ Args:
218
+ matches: List of matching items.
219
+ query: Search query string.
220
+
221
+ Returns:
222
+ Formatted search results as string.
223
+ """
224
+ if self.json_output:
225
+ return json.dumps(
226
+ {
227
+ "query": query,
228
+ "matches": [
229
+ item.to_dict() if hasattr(item, "to_dict") else item
230
+ for item in matches
231
+ ],
232
+ "count": len(matches),
233
+ },
234
+ indent=2,
235
+ )
236
+
237
+ if not matches:
238
+ return f"No items found matching '{query}'"
239
+
240
+ lines = [f"Found {len(matches)} items matching '{query}':", "-" * 60]
241
+ for item in matches:
242
+ if (
243
+ hasattr(item, "code")
244
+ and hasattr(item, "name")
245
+ and hasattr(item, "description")
246
+ ):
247
+ lines.append(f"{item.code:2} - {item.name}: {item.description}")
248
+ else:
249
+ lines.append(str(item))
250
+
251
+ return "\n".join(lines)
252
+
253
+
254
+ class StatisticsFormatter(OutputFormatter):
255
+ """Specialized formatter for statistics and analysis output."""
256
+
257
+ def format_file_statistics(
258
+ self, file_path: str, stats: Dict[str, Any], entry_count: int
259
+ ) -> str:
260
+ """Format file analysis statistics.
261
+
262
+ Args:
263
+ file_path: Path to the analyzed file.
264
+ stats: Statistics dictionary.
265
+ entry_count: Total number of entries.
266
+
267
+ Returns:
268
+ Formatted statistics as string.
269
+ """
270
+ if self.json_output:
271
+ return json.dumps(
272
+ {
273
+ "file_path": file_path,
274
+ "statistics": stats,
275
+ "entry_count": entry_count,
276
+ },
277
+ indent=2,
278
+ )
279
+
280
+ lines = [
281
+ "=== Console Bus Log Summary ===",
282
+ f"File: {file_path}",
283
+ f"Entries: {entry_count}",
284
+ ]
285
+
286
+ # Time range
287
+ if stats.get("time_range", {}).get("start"):
288
+ time_range = stats["time_range"]
289
+ lines.extend(
290
+ [
291
+ f"Time Range: {time_range['start']} - {time_range['end']}",
292
+ f"Duration: {time_range['duration_seconds']:.3f} seconds",
293
+ ]
294
+ )
295
+
296
+ # Telegram distribution
297
+ lines.append("\nTelegram Distribution:")
298
+ type_counts = stats.get("telegram_type_counts", {})
299
+ total = stats.get("total_entries", 0)
300
+
301
+ for t_type, count in type_counts.items():
302
+ percentage = (count / total * 100) if total > 0 else 0
303
+ lines.append(f" {t_type.capitalize()}: {count} ({percentage:.1f}%)")
304
+
305
+ # Direction distribution
306
+ lines.append("\nDirection Distribution:")
307
+ dir_counts = stats.get("direction_counts", {})
308
+ for direction, count in dir_counts.items():
309
+ percentage = (count / total * 100) if total > 0 else 0
310
+ lines.append(f" {direction.upper()}: {count} ({percentage:.1f}%)")
311
+
312
+ return "\n".join(lines)
@@ -0,0 +1,56 @@
1
+ """Click parameter type for ModuleTypeCode enum validation."""
2
+
3
+ from typing import Any, Optional
4
+
5
+ import click
6
+
7
+ from xp.models.telegram.module_type_code import ModuleTypeCode
8
+
9
+
10
+ class ModuleTypeChoice(click.ParamType):
11
+ """Click parameter type for validating ModuleTypeCode enum values.
12
+
13
+ Attributes:
14
+ name: The parameter type name.
15
+ choices: List of valid choice strings.
16
+ """
17
+
18
+ name = "module_type"
19
+
20
+ def __init__(self) -> None:
21
+ """Initialize the ModuleTypeChoice parameter type."""
22
+ self.choices = [key for key in ModuleTypeCode.__members__.keys()]
23
+
24
+ def convert(
25
+ self, value: Any, param: Optional[click.Parameter], ctx: Optional[click.Context]
26
+ ) -> int:
27
+ """Convert and validate input to ModuleTypeCode value.
28
+
29
+ Args:
30
+ value: The input value to convert.
31
+ param: The Click parameter.
32
+ ctx: The Click context.
33
+
34
+ Returns:
35
+ Module type code integer value if valid.
36
+ """
37
+ if value is None:
38
+ self.fail("Module type is required", param, ctx)
39
+
40
+ # Convert to upper for comparison
41
+ normalized_value = value.upper()
42
+
43
+ if normalized_value in self.choices:
44
+ # Return the actual enum value (integer)
45
+ return ModuleTypeCode[normalized_value].value
46
+
47
+ # If not found, show error with available choices
48
+ choices_list = "\n".join(f" - {choice}" for choice in sorted(self.choices))
49
+ self.fail(
50
+ f"{value!r} is not a valid module type. " f"Choose from:\n{choices_list}",
51
+ param,
52
+ ctx,
53
+ )
54
+
55
+
56
+ MODULE_TYPE = ModuleTypeChoice()
@@ -0,0 +1,52 @@
1
+ """Click parameter type for serial number validation."""
2
+
3
+ from typing import Any, Optional
4
+
5
+ import click
6
+
7
+
8
+ class SerialNumberParamType(click.ParamType):
9
+ """Click parameter type for validating and formatting serial numbers.
10
+
11
+ Attributes:
12
+ name: The parameter type name.
13
+ """
14
+
15
+ name = "serial_number"
16
+
17
+ def convert(
18
+ self, value: Any, param: Optional[click.Parameter], ctx: Optional[click.Context]
19
+ ) -> Optional[str]:
20
+ """Convert and validate serial number input.
21
+
22
+ Args:
23
+ value: The input value to convert.
24
+ param: The Click parameter.
25
+ ctx: The Click context.
26
+
27
+ Returns:
28
+ 10-character zero-padded serial number string, or None if input is None.
29
+ """
30
+ if value is None:
31
+ return None
32
+
33
+ # Convert to string if not already
34
+ str_value = str(value)
35
+
36
+ # Check if contains only numeric characters (empty string should be treated as "0")
37
+ if not str_value.isdigit() and str_value != "":
38
+ self.fail(f"{value!r} contains non-numeric characters", param, ctx)
39
+
40
+ # Handle empty string as zero
41
+ if str_value == "":
42
+ str_value = "0"
43
+
44
+ # Check length constraints
45
+ if len(str_value) > 10:
46
+ self.fail(f"{value!r} is longer than 10 characters", param, ctx)
47
+
48
+ # Pad left with zeros if length < 10
49
+ return str_value.zfill(10)
50
+
51
+
52
+ SERIAL = SerialNumberParamType()
@@ -0,0 +1,57 @@
1
+ """Click parameter type for SystemFunction enum validation."""
2
+
3
+ from typing import Any, Optional
4
+
5
+ import click
6
+
7
+ from xp.models.telegram.system_function import SystemFunction
8
+
9
+
10
+ # noinspection DuplicatedCode
11
+ class SystemFunctionChoice(click.ParamType):
12
+ """Click parameter type for validating SystemFunction enum values.
13
+
14
+ Attributes:
15
+ name: The parameter type name.
16
+ choices: List of valid choice strings.
17
+ """
18
+
19
+ name = "system_function"
20
+
21
+ def __init__(self) -> None:
22
+ """Initialize the SystemFunctionChoice parameter type."""
23
+ self.choices = [key.lower() for key in SystemFunction.__members__.keys()]
24
+
25
+ def convert(
26
+ self, value: Any, param: Optional[click.Parameter], ctx: Optional[click.Context]
27
+ ) -> Any:
28
+ """Convert and validate input to SystemFunction enum.
29
+
30
+ Args:
31
+ value: The input value to convert.
32
+ param: The Click parameter.
33
+ ctx: The Click context.
34
+
35
+ Returns:
36
+ SystemFunction enum member if valid, None if input is None.
37
+ """
38
+ if value is None:
39
+ return value
40
+
41
+ # Convert to lower for comparison
42
+ normalized_value = value.lower()
43
+
44
+ if normalized_value in self.choices:
45
+ # Return the actual enum member
46
+ return SystemFunction[normalized_value.upper()]
47
+
48
+ # If not found, show error with available choices
49
+ self.fail(
50
+ f"{value!r} is not a valid choice. "
51
+ f'Choose from: {", ".join(self.choices)}',
52
+ param,
53
+ ctx,
54
+ )
55
+
56
+
57
+ SYSTEM_FUNCTION = SystemFunctionChoice()
@@ -0,0 +1,53 @@
1
+ """Click parameter type for XP module type validation."""
2
+
3
+ from typing import Any, Optional
4
+
5
+ import click
6
+
7
+
8
+ class XpModuleTypeChoice(click.ParamType):
9
+ """Click parameter type for validating XP module types.
10
+
11
+ Attributes:
12
+ name: The parameter type name.
13
+ choices: List of valid module type strings.
14
+ """
15
+
16
+ name = "xpmoduletype"
17
+
18
+ def __init__(self) -> None:
19
+ """Initialize the XpModuleTypeChoice parameter type."""
20
+ self.choices = ["xp20", "xp24", "xp31", "xp33"]
21
+
22
+ def convert(
23
+ self, value: Any, param: Optional[click.Parameter], ctx: Optional[click.Context]
24
+ ) -> Any:
25
+ """Convert and validate XP module type input.
26
+
27
+ Args:
28
+ value: The input value to convert.
29
+ param: The Click parameter.
30
+ ctx: The Click context.
31
+
32
+ Returns:
33
+ Lowercase module type string if valid, None if input is None.
34
+ """
35
+ if value is None:
36
+ return value
37
+
38
+ # Convert to lower for comparison
39
+ normalized_value = value.lower()
40
+
41
+ if normalized_value in self.choices:
42
+ return normalized_value
43
+
44
+ # If not found, show error with available choices
45
+ choices_list = "\n".join(f" - {choice}" for choice in sorted(self.choices))
46
+ self.fail(
47
+ f"{value!r} is not a valid choice. " f"Choose from:\n{choices_list}",
48
+ param,
49
+ ctx,
50
+ )
51
+
52
+
53
+ XP_MODULE_TYPE = XpModuleTypeChoice()
@@ -0,0 +1,13 @@
1
+ """Connection layer for XP CLI tool."""
2
+
3
+ from xp.connection.exceptions import (
4
+ ProtocolError,
5
+ ValidationError,
6
+ XPError,
7
+ )
8
+
9
+ __all__ = [
10
+ "XPError",
11
+ "ProtocolError",
12
+ "ValidationError",
13
+ ]
@@ -0,0 +1,22 @@
1
+ """Connection-related exceptions for XP CLI tool.
2
+
3
+ Following the architecture requirement for structured error handling.
4
+ """
5
+
6
+
7
+ class XPError(Exception):
8
+ """Base exception for XP CLI tool."""
9
+
10
+ pass
11
+
12
+
13
+ class ProtocolError(XPError):
14
+ """Console bus protocol errors."""
15
+
16
+ pass
17
+
18
+
19
+ class ValidationError(XPError):
20
+ """Input validation errors."""
21
+
22
+ pass
xp/models/__init__.py ADDED
@@ -0,0 +1,36 @@
1
+ """Data models for XP CLI tool."""
2
+
3
+ from xp.models.conbus.conbus import ConbusRequest, ConbusResponse
4
+ from xp.models.conbus.conbus_client_config import ConbusClientConfig
5
+ from xp.models.conbus.conbus_connection_status import ConbusConnectionStatus
6
+ from xp.models.conbus.conbus_datapoint import ConbusDatapointResponse
7
+ from xp.models.conbus.conbus_discover import ConbusDiscoverResponse
8
+ from xp.models.conbus.conbus_event_raw import ConbusEventRawResponse
9
+ from xp.models.log_entry import LogEntry
10
+ from xp.models.telegram.event_telegram import EventTelegram
11
+ from xp.models.telegram.event_type import EventType
12
+ from xp.models.telegram.input_type import InputType
13
+ from xp.models.telegram.module_type import (
14
+ ModuleType,
15
+ get_all_module_types,
16
+ is_valid_module_code,
17
+ )
18
+ from xp.models.telegram.module_type_code import ModuleTypeCode
19
+
20
+ __all__ = [
21
+ "EventTelegram",
22
+ "EventType",
23
+ "InputType",
24
+ "ModuleType",
25
+ "ModuleTypeCode",
26
+ "get_all_module_types",
27
+ "is_valid_module_code",
28
+ "LogEntry",
29
+ "ConbusClientConfig",
30
+ "ConbusRequest",
31
+ "ConbusResponse",
32
+ "ConbusDatapointResponse",
33
+ "ConbusDiscoverResponse",
34
+ "ConbusEventRawResponse",
35
+ "ConbusConnectionStatus",
36
+ ]
@@ -0,0 +1 @@
1
+ """Action table models for XP protocol."""
@@ -0,0 +1,43 @@
1
+ """XP20 Action Table models for input actions and settings."""
2
+
3
+ from dataclasses import dataclass, field
4
+
5
+ from xp.models import ModuleTypeCode
6
+ from xp.models.telegram.input_action_type import InputActionType
7
+ from xp.models.telegram.timeparam_type import TimeParam
8
+
9
+
10
+ # CP20 0 0 > 1 OFF;
11
+ # CP20 0 0 > 1 ~ON;
12
+ @dataclass
13
+ class ActionTableEntry:
14
+ """Entry in an action table mapping input events to output actions.
15
+
16
+ Attributes:
17
+ module_type: Type code of the module.
18
+ link_number: Link number for the action.
19
+ module_input: Input number on the module.
20
+ module_output: Output number on the module.
21
+ command: Action type to perform.
22
+ parameter: Time parameter for the action.
23
+ inverted: Whether the action is inverted.
24
+ """
25
+
26
+ module_type: ModuleTypeCode = ModuleTypeCode.CP20
27
+ link_number: int = 0
28
+ module_input: int = 0
29
+ module_output: int = 1
30
+ command: InputActionType = InputActionType.OFF
31
+ parameter: TimeParam = TimeParam.NONE
32
+ inverted: bool = False
33
+
34
+
35
+ @dataclass
36
+ class ActionTable:
37
+ """Action Table for managing action on events.
38
+
39
+ Attributes:
40
+ entries: List of action table entries.
41
+ """
42
+
43
+ entries: list[ActionTableEntry] = field(default_factory=list)