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,187 @@
1
+ """Module type operations CLI commands."""
2
+
3
+ import json
4
+ from typing import Any, Dict, Union
5
+
6
+ import click
7
+ from click import Context
8
+ from click_help_colors import HelpColorsGroup
9
+
10
+ from xp.cli.utils.decorators import list_command
11
+ from xp.cli.utils.error_handlers import CLIErrorHandler
12
+ from xp.cli.utils.formatters import ListFormatter, OutputFormatter
13
+ from xp.services.module_type_service import ModuleTypeNotFoundError, ModuleTypeService
14
+
15
+
16
+ @click.group(
17
+ cls=HelpColorsGroup, help_headers_color="yellow", help_options_color="green"
18
+ )
19
+ def module() -> None:
20
+ """Perform module type operations."""
21
+ pass
22
+
23
+
24
+ @module.command("info")
25
+ @click.argument("identifier")
26
+ @click.pass_context
27
+ @list_command(ModuleTypeNotFoundError)
28
+ def module_info(ctx: Context, identifier: str) -> None:
29
+ r"""Get information about a module type by code or name.
30
+
31
+ Args:
32
+ ctx: Click context object.
33
+ identifier: Module code or name.
34
+
35
+ Examples:
36
+ \b
37
+ xp module info 14
38
+ xp module info XP2606
39
+ """
40
+ service: ModuleTypeService = (
41
+ ctx.obj.get("container").get_container().resolve(ModuleTypeService)
42
+ )
43
+ OutputFormatter(True)
44
+
45
+ try:
46
+ # Try to parse as integer first, then as string
47
+ module_id: Union[int, str]
48
+ try:
49
+ module_id = int(identifier)
50
+ except ValueError:
51
+ module_id = identifier
52
+
53
+ module_type = service.get_module_type(module_id)
54
+ click.echo(json.dumps(module_type.to_dict(), indent=2))
55
+
56
+ except ModuleTypeNotFoundError as e:
57
+ CLIErrorHandler.handle_not_found_error(e, "module type", identifier)
58
+
59
+
60
+ @module.command("list")
61
+ @click.option("--category", "-c", help="Filter by category")
62
+ @click.option(
63
+ "--group-by-category", "-g", is_flag=True, help="Group modules by category"
64
+ )
65
+ @click.pass_context
66
+ @list_command(Exception)
67
+ def module_list(ctx: Context, category: str, group_by_category: bool) -> None:
68
+ r"""List module types, optionally filtered by category.
69
+
70
+ Args:
71
+ ctx: Click context object.
72
+ category: Filter by category.
73
+ group_by_category: Group modules by category.
74
+
75
+ Examples:
76
+ \b
77
+ xp module list
78
+ xp module list --category "Interface Panels"
79
+ xp module list --group-by-category
80
+ """
81
+ service: ModuleTypeService = (
82
+ ctx.obj.get("container").get_container().resolve(ModuleTypeService)
83
+ )
84
+ ListFormatter(True)
85
+
86
+ try:
87
+ if category:
88
+ modules = service.get_modules_by_category(category)
89
+ if not modules:
90
+ click.echo(json.dumps({"modules": [], "category": category}))
91
+ return
92
+ else:
93
+ modules = service.list_all_modules()
94
+
95
+ if group_by_category:
96
+ categories = service.list_modules_by_category()
97
+ output: Dict[str, Any] = {
98
+ "modules_by_category": {
99
+ cat: [mod.to_dict() for mod in mods]
100
+ for cat, mods in categories.items()
101
+ }
102
+ }
103
+ else:
104
+ output = {
105
+ "modules": [_module.to_dict() for _module in modules],
106
+ "count": len(modules),
107
+ }
108
+ click.echo(json.dumps(output, indent=2))
109
+
110
+ except Exception as e:
111
+ CLIErrorHandler.handle_service_error(e, "module listing")
112
+
113
+
114
+ @module.command("search")
115
+ @click.argument("query")
116
+ @click.option(
117
+ "--field",
118
+ multiple=True,
119
+ type=click.Choice(["name", "description"]),
120
+ help="Fields to search in (default: both)",
121
+ )
122
+ @click.pass_context
123
+ @list_command(Exception)
124
+ def module_search(ctx: Context, query: str, field: tuple) -> None:
125
+ r"""Search for module types by name or description.
126
+
127
+ Args:
128
+ ctx: Click context object.
129
+ query: Search query.
130
+ field: Fields to search in.
131
+
132
+ Examples:
133
+ \b
134
+ xp module search "push button"
135
+ xp module search --field name "XP"
136
+ """
137
+ service: ModuleTypeService = (
138
+ ctx.obj.get("container").get_container().resolve(ModuleTypeService)
139
+ )
140
+ ListFormatter(True)
141
+
142
+ try:
143
+ search_fields = list(field) if field else ["name", "description"]
144
+ matching_modules = service.search_modules(query, search_fields)
145
+
146
+ output = {
147
+ "query": query,
148
+ "search_fields": search_fields,
149
+ "matches": [_module.to_dict() for _module in matching_modules],
150
+ "count": len(matching_modules),
151
+ }
152
+ click.echo(json.dumps(output, indent=2))
153
+
154
+ except Exception as e:
155
+ CLIErrorHandler.handle_service_error(e, "module search", {"query": query})
156
+
157
+
158
+ @module.command("categories")
159
+ @click.pass_context
160
+ @list_command(Exception)
161
+ def module_categories(ctx: Context) -> None:
162
+ r"""List all available module categories.
163
+
164
+ Args:
165
+ ctx: Click context object.
166
+
167
+ Examples:
168
+ \b
169
+ xp module categories
170
+ """
171
+ service: ModuleTypeService = (
172
+ ctx.obj.get("container").get_container().resolve(ModuleTypeService)
173
+ )
174
+ OutputFormatter(True)
175
+
176
+ try:
177
+ categories = service.list_modules_by_category()
178
+
179
+ output = {
180
+ "categories": {
181
+ category: len(modules) for category, modules in categories.items()
182
+ }
183
+ }
184
+ click.echo(json.dumps(output, indent=2))
185
+
186
+ except Exception as e:
187
+ CLIErrorHandler.handle_service_error(e, "category listing")
@@ -0,0 +1,178 @@
1
+ """Conbus reverse proxy operations CLI commands."""
2
+
3
+ import json
4
+ import signal
5
+ import sys
6
+ from types import FrameType
7
+ from typing import Any, Dict, Optional
8
+
9
+ import click
10
+ from click import Context
11
+ from click_help_colors import HelpColorsGroup
12
+
13
+ from xp.cli.utils.decorators import handle_service_errors
14
+ from xp.cli.utils.error_handlers import CLIErrorHandler
15
+ from xp.cli.utils.formatters import OutputFormatter
16
+ from xp.services.reverse_proxy_service import (
17
+ ReverseProxyError,
18
+ ReverseProxyService,
19
+ )
20
+
21
+ # Global proxy instance
22
+ global_proxy_instance: Optional[ReverseProxyService] = None
23
+
24
+
25
+ @click.group(
26
+ name="rp",
27
+ cls=HelpColorsGroup,
28
+ help_headers_color="yellow",
29
+ help_options_color="green",
30
+ )
31
+ def reverse_proxy() -> None:
32
+ """Perform Conbus reverse proxy operations."""
33
+ pass
34
+
35
+
36
+ @reverse_proxy.command("start")
37
+ @click.option(
38
+ "--port", "-p", default=10001, type=int, help="Port to listen on (default: 10001)"
39
+ )
40
+ @click.option("--config", "-c", default="rp.yml", help="Configuration file path")
41
+ @handle_service_errors(ReverseProxyError)
42
+ @click.pass_context
43
+ def start_proxy(ctx: Context, port: int, config: str) -> None:
44
+ r"""Start the Conbus reverse proxy server.
45
+
46
+ The proxy listens on the specified port and forwards all telegrams
47
+ to the target server configured in cli.yml. All traffic is monitored
48
+ and printed with timestamps.
49
+
50
+ Args:
51
+ ctx: Click context object.
52
+ port: Port to listen on.
53
+ config: Configuration file path.
54
+
55
+ Examples:
56
+ \b
57
+ xp rp start
58
+ xp rp start --port 10002 --config my_cli.yml
59
+
60
+ Raises:
61
+ SystemExit: If proxy is already running.
62
+ """
63
+ global global_proxy_instance
64
+
65
+ try:
66
+ # Check if proxy is already running
67
+ if global_proxy_instance and global_proxy_instance.is_running:
68
+ error_response = {
69
+ "success": False,
70
+ "error": "Reverse proxy is already running",
71
+ }
72
+ click.echo(json.dumps(error_response, indent=2))
73
+ raise SystemExit(1)
74
+
75
+ # Load configuration and create proxy instance
76
+ service: ReverseProxyService = (
77
+ ctx.obj.get("container").get_container().resolve(ReverseProxyService)
78
+ )
79
+ global_proxy_instance = service
80
+
81
+ # Handle graceful shutdown on SIGINT
82
+ def signal_handler(signum: int, frame: Optional[FrameType]) -> None:
83
+ """Handle shutdown signals for graceful proxy termination.
84
+
85
+ Args:
86
+ signum: Signal number received.
87
+ frame: Current stack frame (may be None).
88
+ """
89
+ if global_proxy_instance and global_proxy_instance.is_running:
90
+ timestamp = global_proxy_instance.timestamp()
91
+ print(f"\n{timestamp} [SHUTDOWN] Received interrupt signal ({signum})")
92
+ print(f"\n{timestamp} [SHUTDOWN] Frame is ({frame})")
93
+ global_proxy_instance.stop_proxy()
94
+ sys.exit(0)
95
+
96
+ signal.signal(signal.SIGINT, signal_handler)
97
+
98
+ # Start proxy (this will block)
99
+ result = global_proxy_instance.start_proxy()
100
+ click.echo(json.dumps(result.to_dict(), indent=2))
101
+ if result.success:
102
+ global_proxy_instance.run_blocking()
103
+
104
+ except ReverseProxyError as e:
105
+ CLIErrorHandler.handle_service_error(
106
+ e, "reverse proxy startup", {"port": port, "config": config}
107
+ )
108
+ except KeyboardInterrupt:
109
+ shutdown_response = {
110
+ "success": True,
111
+ "message": "Reverse proxy shutdown by user",
112
+ }
113
+ click.echo(json.dumps(shutdown_response, indent=2))
114
+
115
+
116
+ @reverse_proxy.command("stop")
117
+ @handle_service_errors(ReverseProxyError)
118
+ def stop_proxy() -> None:
119
+ r"""Stop the running Conbus reverse proxy server.
120
+
121
+ Examples:
122
+ \b
123
+ xp rp stop
124
+
125
+ Raises:
126
+ SystemExit: If proxy is not running.
127
+ """
128
+ try:
129
+ if global_proxy_instance is None or not global_proxy_instance.is_running:
130
+ error_response = {
131
+ "success": False,
132
+ "error": "Reverse proxy is not running",
133
+ }
134
+ click.echo(json.dumps(error_response, indent=2))
135
+ raise SystemExit(1)
136
+
137
+ # Stop the proxy
138
+ result = global_proxy_instance.stop_proxy()
139
+
140
+ click.echo(json.dumps(result.to_dict(), indent=2))
141
+
142
+ except ReverseProxyError as e:
143
+ CLIErrorHandler.handle_service_error(e, "reverse proxy stop")
144
+
145
+
146
+ @reverse_proxy.command("status")
147
+ @handle_service_errors(Exception)
148
+ def proxy_status() -> None:
149
+ r"""Get status of the Conbus reverse proxy server.
150
+
151
+ Shows current running state, listen port, target server,
152
+ and active connection details.
153
+
154
+ Examples:
155
+ \b
156
+ xp rp status
157
+ """
158
+ OutputFormatter(True)
159
+
160
+ try:
161
+ status_data: Dict[str, Any]
162
+ if global_proxy_instance is None:
163
+ status_data = {
164
+ "running": False,
165
+ "listen_port": None,
166
+ "target_ip": None,
167
+ "target_port": None,
168
+ "active_connections": 0,
169
+ "connections": {},
170
+ }
171
+ else:
172
+ result = global_proxy_instance.get_status()
173
+ status_data = result.data if result.success else {}
174
+
175
+ click.echo(json.dumps(status_data, indent=2))
176
+
177
+ except Exception as e:
178
+ CLIErrorHandler.handle_service_error(e, "reverse proxy status check")
@@ -0,0 +1,3 @@
1
+ """Server CLI commands package."""
2
+
3
+ __all__ = []
@@ -0,0 +1,135 @@
1
+ """Conbus emulator server operations CLI commands."""
2
+
3
+ import json
4
+ from typing import Any, Dict, Optional
5
+
6
+ import click
7
+ from click import Context
8
+ from click_help_colors import HelpColorsGroup
9
+
10
+ from xp.cli.utils.decorators import handle_service_errors
11
+ from xp.cli.utils.error_handlers import ServerErrorHandler
12
+ from xp.cli.utils.formatters import OutputFormatter
13
+ from xp.services.server.server_service import ServerError, ServerService
14
+
15
+ # Global server instance
16
+ _server_instance: Optional[ServerService] = None
17
+
18
+
19
+ @click.group(
20
+ cls=HelpColorsGroup, help_headers_color="yellow", help_options_color="green"
21
+ )
22
+ def server() -> None:
23
+ """Perform Conbus emulator server operations."""
24
+ pass
25
+
26
+
27
+ @server.command("start")
28
+ @click.option(
29
+ "--port", "-p", default=10001, type=int, help="Port to listen on (default: 10001)"
30
+ )
31
+ @click.option("--config", "-c", default="server.yml", help="Configuration file path")
32
+ @click.pass_context
33
+ @handle_service_errors(ServerError)
34
+ def start_server(ctx: Context, port: int, config: str) -> None:
35
+ r"""Start the Conbus emulator server.
36
+
37
+ Args:
38
+ ctx: Click context object.
39
+ port: Port to listen on.
40
+ config: Configuration file path.
41
+
42
+ Examples:
43
+ \b
44
+ xp server start
45
+ xp server start --port 1001 --config my_config.yml
46
+
47
+ Raises:
48
+ SystemExit: If server is already running.
49
+ """
50
+ global _server_instance
51
+
52
+ try:
53
+ # Check if server is already running
54
+ if _server_instance and _server_instance.is_running:
55
+ error_response = {
56
+ "success": False,
57
+ "error": "Server is already running",
58
+ }
59
+ click.echo(json.dumps(error_response, indent=2))
60
+ raise SystemExit(1)
61
+
62
+ # Get dependencies from container
63
+ _server_instance = (
64
+ ctx.obj.get("container").get_container().resolve(ServerService)
65
+ )
66
+
67
+ status = _server_instance.get_server_status()
68
+ click.echo(json.dumps(status, indent=2))
69
+
70
+ # This will block until server is stopped
71
+ _server_instance.start_server()
72
+
73
+ except ServerError as e:
74
+ ServerErrorHandler.handle_server_startup_error(e, port, config)
75
+ except KeyboardInterrupt:
76
+ shutdown_response = {"success": True, "message": "Server shutdown by user"}
77
+ click.echo(json.dumps(shutdown_response, indent=2))
78
+
79
+
80
+ @server.command("stop")
81
+ @handle_service_errors(ServerError)
82
+ def stop_server() -> None:
83
+ r"""Stop the running Conbus emulator server.
84
+
85
+ Examples:
86
+ \b
87
+ xp server stop
88
+ """
89
+ try:
90
+ if _server_instance is None or not _server_instance.is_running:
91
+ ServerErrorHandler.handle_server_not_running_error()
92
+
93
+ # Stop the server
94
+ if _server_instance is not None:
95
+ _server_instance.stop_server()
96
+
97
+ response = {"success": True, "message": "Server stopped successfully"}
98
+ click.echo(json.dumps(response, indent=2))
99
+
100
+ except ServerError as e:
101
+ ServerErrorHandler.handle_server_startup_error(e, 0, "")
102
+
103
+
104
+ @server.command("status")
105
+ @handle_service_errors(Exception)
106
+ def server_status() -> None:
107
+ r"""Get status of the Conbus emulator server.
108
+
109
+ Examples:
110
+ \b
111
+ xp server status
112
+
113
+ Raises:
114
+ SystemExit: If status cannot be retrieved.
115
+ """
116
+ formatter = OutputFormatter(True)
117
+
118
+ try:
119
+ status: Dict[str, Any]
120
+ if _server_instance is None:
121
+ status = {
122
+ "running": False,
123
+ "port": None,
124
+ "devices_configured": 0,
125
+ "device_list": [],
126
+ }
127
+ else:
128
+ status = _server_instance.get_server_status()
129
+
130
+ click.echo(json.dumps(status, indent=2))
131
+
132
+ except Exception as e:
133
+ error_response = formatter.error_response(str(e))
134
+ click.echo(error_response)
135
+ raise SystemExit(1)
@@ -0,0 +1,5 @@
1
+ """Telegram CLI commands package."""
2
+
3
+ from xp.cli.commands.telegram.telegram import blink, checksum, linknumber, telegram
4
+
5
+ __all__ = ["telegram", "linknumber", "blink", "checksum"]
@@ -0,0 +1,41 @@
1
+ """Shared conbus CLI group definition."""
2
+
3
+ import click
4
+ from click_help_colors import HelpColorsGroup
5
+
6
+
7
+ @click.group(
8
+ cls=HelpColorsGroup, help_headers_color="yellow", help_options_color="green"
9
+ )
10
+ def telegram() -> None:
11
+ """Perform event telegram operations."""
12
+ pass
13
+
14
+
15
+ @click.group(
16
+ cls=HelpColorsGroup, help_headers_color="yellow", help_options_color="green"
17
+ )
18
+ def linknumber() -> None:
19
+ """Perform link number operations for module configuration."""
20
+ pass
21
+
22
+
23
+ @click.group(
24
+ cls=HelpColorsGroup, help_headers_color="yellow", help_options_color="green"
25
+ )
26
+ def blink() -> None:
27
+ """Perform blink operations for module LED control."""
28
+ pass
29
+
30
+
31
+ @click.group(
32
+ cls=HelpColorsGroup, help_headers_color="yellow", help_options_color="green"
33
+ )
34
+ def checksum() -> None:
35
+ """Perform checksum calculation and validation operations."""
36
+ pass
37
+
38
+
39
+ telegram.add_command(linknumber)
40
+ telegram.add_command(blink)
41
+ telegram.add_command(checksum)
@@ -0,0 +1,79 @@
1
+ """Blink operations CLI commands."""
2
+
3
+ import json
4
+
5
+ import click
6
+
7
+ from xp.cli.commands.telegram.telegram import blink
8
+ from xp.cli.utils.decorators import handle_service_errors
9
+ from xp.cli.utils.error_handlers import CLIErrorHandler
10
+ from xp.cli.utils.formatters import OutputFormatter
11
+ from xp.cli.utils.serial_number_type import SERIAL
12
+ from xp.services.telegram.telegram_blink_service import BlinkError, TelegramBlinkService
13
+
14
+
15
+ @blink.command("on")
16
+ @click.argument("serial_number", type=SERIAL)
17
+ @handle_service_errors(BlinkError)
18
+ def blink_on(serial_number: str) -> None:
19
+ r"""Generate a telegram to start blinking module LED.
20
+
21
+ Args:
22
+ serial_number: 10-digit module serial number.
23
+
24
+ Examples:
25
+ \b
26
+ xp blink on 0012345008
27
+ xp blink on 0012345008
28
+ """
29
+ service = TelegramBlinkService()
30
+ OutputFormatter(True)
31
+
32
+ try:
33
+ telegram = service.generate_blink_telegram(serial_number, "on")
34
+
35
+ output = {
36
+ "success": True,
37
+ "telegram": telegram,
38
+ "serial_number": serial_number,
39
+ "operation": "blink_on",
40
+ }
41
+ click.echo(json.dumps(output, indent=2))
42
+
43
+ except BlinkError as e:
44
+ CLIErrorHandler.handle_service_error(
45
+ e, "blink telegram generation", {"serial_number": serial_number}
46
+ )
47
+
48
+
49
+ @blink.command("off")
50
+ @click.argument("serial_number", type=SERIAL)
51
+ @handle_service_errors(BlinkError)
52
+ def blink_off(serial_number: str) -> None:
53
+ r"""Generate a telegram to stop blinking module LED.
54
+
55
+ Args:
56
+ serial_number: 10-digit module serial number.
57
+
58
+ Examples:
59
+ \b
60
+ xp blink off 0012345011
61
+ """
62
+ service = TelegramBlinkService()
63
+ OutputFormatter(True)
64
+
65
+ try:
66
+ telegram = service.generate_blink_telegram(serial_number, "off")
67
+
68
+ output = {
69
+ "success": True,
70
+ "telegram": telegram,
71
+ "serial_number": serial_number,
72
+ "operation": "blink_off",
73
+ }
74
+ click.echo(json.dumps(output, indent=2))
75
+
76
+ except BlinkError as e:
77
+ CLIErrorHandler.handle_service_error(
78
+ e, "unblink telegram generation", {"serial_number": serial_number}
79
+ )