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