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,428 @@
1
+ """Conbus Server Service for emulating device discover responses.
2
+
3
+ This service implements a TCP server that listens on port 10001 and responds to
4
+ Discover Request telegrams with configurable device information.
5
+ """
6
+
7
+ import logging
8
+ import socket
9
+ import threading
10
+ from pathlib import Path
11
+ from typing import Dict, List, Optional
12
+
13
+ from xp.models.homekit.homekit_conson_config import (
14
+ ConsonModuleConfig,
15
+ ConsonModuleListConfig,
16
+ )
17
+ from xp.services.server.base_server_service import BaseServerService
18
+ from xp.services.server.device_service_factory import DeviceServiceFactory
19
+ from xp.services.telegram.telegram_discover_service import TelegramDiscoverService
20
+ from xp.services.telegram.telegram_service import TelegramService
21
+
22
+
23
+ class ServerError(Exception):
24
+ """Raised when Conbus server operations fail."""
25
+
26
+ pass
27
+
28
+
29
+ class ServerService:
30
+ """
31
+ Main TCP server implementation for Conbus device emulation.
32
+
33
+ Manages TCP socket lifecycle, handles client connections,
34
+ parses Discover Request telegrams, and coordinates device responses.
35
+ """
36
+
37
+ def __init__(
38
+ self,
39
+ telegram_service: TelegramService,
40
+ discover_service: TelegramDiscoverService,
41
+ device_factory: DeviceServiceFactory,
42
+ config_path: str = "server.yml",
43
+ port: int = 10001,
44
+ ):
45
+ """Initialize the Conbus server service.
46
+
47
+ Args:
48
+ telegram_service: Service for parsing system telegrams.
49
+ discover_service: Service for handling discover requests.
50
+ device_factory: Factory for creating device service instances (injected via DI).
51
+ config_path: Path to the server configuration file.
52
+ port: TCP port to listen on.
53
+ """
54
+ self.telegram_service = telegram_service
55
+ self.discover_service = discover_service
56
+ self.device_factory = device_factory
57
+ self.config_path = config_path
58
+ self.port = port
59
+ self.server_socket: Optional[socket.socket] = None
60
+ self.is_running = False
61
+ self.devices: List[ConsonModuleConfig] = []
62
+ self.device_services: Dict[str, BaseServerService] = (
63
+ {}
64
+ ) # serial -> device service instance
65
+
66
+ # Collect device buffer to broadcast to client
67
+ self.collector_thread: Optional[threading.Thread] = (
68
+ None # Background thread for storm
69
+ )
70
+ self.collector_stop_event = threading.Event() # Event to stop thread
71
+ self.collector_buffer: list[str] = [] # All collected buffers
72
+
73
+ # Set up logging
74
+ self.logger = logging.getLogger(__name__)
75
+
76
+ # Load device configuration
77
+ self._load_device_config()
78
+
79
+ def _load_device_config(self) -> None:
80
+ """Load device configurations from server.yml."""
81
+ try:
82
+ if Path(self.config_path).exists():
83
+ config = ConsonModuleListConfig.from_yaml(self.config_path)
84
+ self.devices = [module for module in config.root if module.enabled]
85
+ self._create_device_services()
86
+ self.logger.info(f"Loaded {len(self.devices)} devices from config")
87
+ else:
88
+ self.logger.warning(
89
+ f"Config file {self.config_path} not found, using empty device list"
90
+ )
91
+ self.devices = []
92
+ self.device_services = {}
93
+ except Exception as e:
94
+ self.logger.error(f"Error loading config file: {e}")
95
+ self.devices = []
96
+ self.device_services = {}
97
+
98
+ def _create_device_services(self) -> None:
99
+ """Create device service instances based on device configuration."""
100
+ self.device_services = {}
101
+
102
+ for module in self.devices:
103
+ module_type = module.module_type
104
+ serial_number = module.serial_number
105
+
106
+ try:
107
+ # Use factory to create device instance
108
+ self.device_services[serial_number] = self.device_factory.create_device(
109
+ module_type, serial_number
110
+ )
111
+
112
+ except ValueError as e:
113
+ # Factory raises ValueError for unknown device types
114
+ self.logger.warning(str(e))
115
+
116
+ except Exception as e:
117
+ self.logger.error(
118
+ f"Error creating device service for {serial_number}: {e}"
119
+ )
120
+
121
+ def start_server(self) -> None:
122
+ """Start the TCP server on port 10001.
123
+
124
+ Raises:
125
+ ServerError: If server is already running or fails to start.
126
+ """
127
+ if self.is_running:
128
+ raise ServerError("Server is already running")
129
+
130
+ try:
131
+ # Create TCP socket
132
+ self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
133
+ self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
134
+
135
+ # Bind to port 10001 on all interfaces
136
+ self.server_socket.bind(("0.0.0.0", self.port))
137
+ self.server_socket.listen(1) # Accept single connection as per spec
138
+
139
+ self._start_device_collector_thread()
140
+
141
+ self.is_running = True
142
+ self.logger.info(f"Conbus emulator server started on port {self.port}")
143
+ self.logger.info(
144
+ f"Configured devices: {list([device.serial_number for device in self.devices])}"
145
+ )
146
+
147
+ # Start accepting connections
148
+ self._accept_connections()
149
+
150
+ except Exception as e:
151
+ self.logger.error(f"Failed to start server: {e}")
152
+ raise ServerError(f"Failed to start server: {e}")
153
+
154
+ def stop_server(self) -> None:
155
+ """Stop the TCP server."""
156
+ if not self.is_running:
157
+ return
158
+
159
+ self.is_running = False
160
+
161
+ if self.server_socket:
162
+ try:
163
+ self.server_socket.close()
164
+ self.logger.info("Conbus emulator server stopped")
165
+ except Exception as e:
166
+ self.logger.error(f"Error closing server socket: {e}")
167
+
168
+ def _accept_connections(self) -> None:
169
+ """Accept and handle client connections."""
170
+ while self.is_running:
171
+ try:
172
+ # Accept connection
173
+ if self.server_socket is None:
174
+ break
175
+ client_socket, client_address = self.server_socket.accept()
176
+ self.logger.info(f"Client connected from {client_address}")
177
+
178
+ # Handle client in separate thread
179
+ client_thread = threading.Thread(
180
+ target=self._handle_client, args=(client_socket, client_address)
181
+ )
182
+ client_thread.daemon = True
183
+ client_thread.start()
184
+
185
+ except Exception as e:
186
+ if self.is_running:
187
+ self.logger.error(f"Error accepting connection: {e}")
188
+ break
189
+
190
+ def _handle_client(
191
+ self, client_socket: socket.socket, client_address: tuple[str, int]
192
+ ) -> None:
193
+ """Handle individual client connection."""
194
+ try:
195
+
196
+ idle_timeout = 300
197
+ rcv_timeout = 10
198
+
199
+ # Set timeout for idle connections (30 seconds as per spec)
200
+ client_socket.settimeout(rcv_timeout)
201
+ timeout = idle_timeout / rcv_timeout
202
+
203
+ while True:
204
+
205
+ # send waiting buffer
206
+ for i in range(len(self.collector_buffer)):
207
+ buffer = self.collector_buffer.pop()
208
+ client_socket.send(buffer.encode("latin-1"))
209
+ self.logger.debug(f"Sent buffer to {client_address}")
210
+
211
+ # Receive data from client
212
+ data = None
213
+ try:
214
+ data = client_socket.recv(1024)
215
+ except socket.timeout:
216
+ pass
217
+ finally:
218
+ timeout -= 1
219
+
220
+ if not data:
221
+ if timeout <= 0:
222
+ break
223
+ continue
224
+
225
+ # reset timeout on receiving data
226
+ timeout = idle_timeout / rcv_timeout
227
+
228
+ message = data.decode("latin-1").strip()
229
+ self.logger.debug(f"Received from {client_address}: {message}")
230
+
231
+ # Process request (discover or data request)
232
+ responses = self._process_request(message)
233
+
234
+ # Send responses
235
+ for response in responses:
236
+ client_socket.send(response.encode("latin-1"))
237
+ self.logger.debug(f"Sent to {client_address}: {response[:-1]}")
238
+
239
+ except socket.timeout:
240
+ self.logger.debug(f"Client {client_address} timed out")
241
+ except Exception as e:
242
+ self.logger.error(f"Error handling client {client_address}: {e}")
243
+ finally:
244
+ try:
245
+ client_socket.close()
246
+ self.logger.info(f"Client {client_address} disconnected")
247
+ except Exception as e:
248
+ self.logger.error(f"Error closing client socket: {e}")
249
+
250
+ def _process_request(self, message: str) -> List[str]:
251
+ """Process incoming request and generate responses.
252
+
253
+ Args:
254
+ message: Message potentially containing multiple telegrams in format <TELEGRAM><TELEGRAM2>...
255
+
256
+ Returns:
257
+ List of responses for all processed telegrams.
258
+ """
259
+ responses: list[str] = []
260
+
261
+ try:
262
+ # Split message into individual telegrams (enclosed in angle brackets)
263
+ telegrams = self._split_telegrams(message)
264
+
265
+ if not telegrams:
266
+ self.logger.warning(f"No valid telegrams found in message: {message}")
267
+ return responses
268
+
269
+ # Process each telegram
270
+ for telegram in telegrams:
271
+ telegram_responses = self._process_single_telegram(telegram)
272
+ responses.extend(telegram_responses)
273
+
274
+ except Exception as e:
275
+ self.logger.error(f"Error processing request: {e}")
276
+
277
+ return responses
278
+
279
+ def _split_telegrams(self, message: str) -> List[str]:
280
+ """Split message into individual telegrams.
281
+
282
+ Args:
283
+ message: Raw message containing one or more telegrams in format <TELEGRAM><TELEGRAM2>...
284
+
285
+ Returns:
286
+ List of individual telegram strings including angle brackets.
287
+ """
288
+ telegrams = []
289
+ start = 0
290
+
291
+ while True:
292
+ # Find the start of a telegram
293
+ start_idx = message.find("<", start)
294
+ if start_idx == -1:
295
+ break
296
+
297
+ # Find the end of the telegram
298
+ end_idx = message.find(">", start_idx)
299
+ if end_idx == -1:
300
+ self.logger.warning(
301
+ f"Incomplete telegram found starting at position {start_idx}"
302
+ )
303
+ break
304
+
305
+ # Extract telegram including angle brackets
306
+ telegram = message[start_idx : end_idx + 1]
307
+ telegrams.append(telegram)
308
+
309
+ # Move to the next position
310
+ start = end_idx + 1
311
+
312
+ return telegrams
313
+
314
+ def _process_single_telegram(self, telegram: str) -> List[str]:
315
+ """Process a single telegram and generate responses.
316
+
317
+ Args:
318
+ telegram: A single telegram string.
319
+
320
+ Returns:
321
+ List of response strings for this telegram.
322
+ """
323
+ responses: list[str] = []
324
+
325
+ try:
326
+ # Parse the telegram
327
+ parsed_telegram = self.telegram_service.parse_system_telegram(telegram)
328
+
329
+ if not parsed_telegram:
330
+ self.logger.warning(f"Failed to parse telegram: {telegram}")
331
+ return responses
332
+
333
+ # Handle discover requests
334
+ if self.discover_service.is_discover_request(parsed_telegram):
335
+ for device_service in self.device_services.values():
336
+ discover_response = device_service.generate_discover_response()
337
+ responses.append(f"{discover_response}\n")
338
+ else:
339
+ # Handle data requests for specific devices
340
+ serial_number = parsed_telegram.serial_number
341
+
342
+ # If broadcast (0000000000), respond from all devices
343
+ if serial_number == "0000000000":
344
+ for device_service in self.device_services.values():
345
+ broadcast_response: Optional[str] = (
346
+ device_service.process_system_telegram(parsed_telegram)
347
+ )
348
+ if broadcast_response:
349
+ responses.append(f"{broadcast_response}\n")
350
+ # If specific device - lookup by string serial number
351
+ else:
352
+ if serial_number in self.device_services:
353
+ device_service = self.device_services[serial_number]
354
+ device_response: Optional[str] = (
355
+ device_service.process_system_telegram(parsed_telegram)
356
+ )
357
+ if device_response:
358
+ responses.append(f"{device_response}\n")
359
+ else:
360
+ self.logger.debug(
361
+ f"No device found for serial: {serial_number}"
362
+ )
363
+
364
+ except Exception as e:
365
+ self.logger.error(f"Error processing telegram: {e}")
366
+
367
+ return responses
368
+
369
+ def get_server_status(self) -> dict:
370
+ """Get current server status.
371
+
372
+ Returns:
373
+ Dictionary containing server status information.
374
+ """
375
+ return {
376
+ "running": self.is_running,
377
+ "port": self.port,
378
+ "devices_configured": len(self.devices),
379
+ "device_list": list([device.serial_number for device in self.devices]),
380
+ }
381
+
382
+ def reload_config(self) -> None:
383
+ """Reload device configuration from file."""
384
+ self._load_device_config()
385
+ self.logger.info(
386
+ f"Configuration reloaded: {len(self.devices)} devices, {len(self.device_services)} services"
387
+ )
388
+
389
+ def _start_device_collector_thread(self) -> None:
390
+ """Start device buffer collector thread."""
391
+ if self.collector_thread and self.collector_thread.is_alive():
392
+ self.logger.debug("Collector thread already running")
393
+ return
394
+
395
+ # Start background thread to send storm telegrams
396
+ self.collector_thread = threading.Thread(
397
+ target=self._device_collector_thread, daemon=True, name="DeviceCollector"
398
+ )
399
+ self.collector_thread.start()
400
+ self.logger.info("Collector thread started")
401
+
402
+ def _stop_device_collector_thread(self) -> None:
403
+ """Stop device buffer collector thread."""
404
+ if not self.collector_thread or not self.collector_thread.is_alive():
405
+ self.logger.debug("Collector thread not running")
406
+ return
407
+
408
+ self.logger.info(f"Stopping collector thread: {self.collector_thread.name}")
409
+
410
+ # Wait for thread to finish (with timeout)
411
+ if self.collector_thread and self.collector_thread.is_alive():
412
+ self.collector_thread.join(timeout=1.0)
413
+
414
+ self.logger.info("Collector stopped.")
415
+
416
+ def _device_collector_thread(self) -> None:
417
+ """Device buffer collector thread."""
418
+ self.logger.info("Collector thread starting")
419
+
420
+ while True:
421
+ collected = 0
422
+ for device_service in self.device_services.values():
423
+ telegram_buffer = device_service.collect_telegram_buffer()
424
+ self.collector_buffer.extend(telegram_buffer)
425
+ collected += len(telegram_buffer)
426
+
427
+ # Wait a bit before checking again
428
+ self.collector_stop_event.wait(timeout=1)
@@ -0,0 +1,67 @@
1
+ """XP130 Server Service for device emulation.
2
+
3
+ This service provides XP130-specific device emulation functionality,
4
+ including response generation and device configuration handling.
5
+ XP130 is an Ethernet/TCPIP interface module.
6
+ """
7
+
8
+ from typing import Dict, Optional
9
+
10
+ from xp.models import ModuleTypeCode
11
+ from xp.services.actiontable.msactiontable_serializer import MsActionTableSerializer
12
+ from xp.services.server.base_server_service import BaseServerService
13
+
14
+
15
+ class XP130ServerError(Exception):
16
+ """Raised when XP130 server operations fail."""
17
+
18
+ pass
19
+
20
+
21
+ class XP130ServerService(BaseServerService):
22
+ """
23
+ XP130 device emulation service.
24
+
25
+ Generates XP130-specific responses, handles XP130 device configuration,
26
+ and implements XP130 telegram format for Ethernet/TCPIP interface module.
27
+ """
28
+
29
+ def __init__(
30
+ self,
31
+ serial_number: str,
32
+ _variant: str = "",
33
+ _msactiontable_serializer: Optional[MsActionTableSerializer] = None,
34
+ ):
35
+ """Initialize XP130 server service.
36
+
37
+ Args:
38
+ serial_number: The device serial number.
39
+ _variant: Reserved parameter for consistency (unused).
40
+ _msactiontable_serializer: Generic MsActionTable serializer (unused).
41
+ """
42
+ super().__init__(serial_number)
43
+ self.device_type = "XP130"
44
+ self.module_type_code = ModuleTypeCode.XP130 # XP130 module type from registry
45
+ self.firmware_version = "XP130_V1.02.15"
46
+
47
+ # XP130-specific network configuration
48
+ self.ip_address = "192.168.1.100"
49
+ self.subnet_mask = "255.255.255.0"
50
+ self.gateway = "192.168.1.1"
51
+
52
+ def get_device_info(self) -> Dict:
53
+ """Get XP130 device information.
54
+
55
+ Returns:
56
+ Dictionary containing device information.
57
+ """
58
+ return {
59
+ "serial_number": self.serial_number,
60
+ "device_type": self.device_type,
61
+ "firmware_version": self.firmware_version,
62
+ "status": self.device_status,
63
+ "link_number": self.link_number,
64
+ "ip_address": self.ip_address,
65
+ "subnet_mask": self.subnet_mask,
66
+ "gateway": self.gateway,
67
+ }
@@ -0,0 +1,92 @@
1
+ """XP20 Server Service for device emulation.
2
+
3
+ This service provides XP20-specific device emulation functionality,
4
+ including response generation and device configuration handling.
5
+ """
6
+
7
+ from typing import Dict, Optional
8
+
9
+ from xp.models import ModuleTypeCode
10
+ from xp.models.actiontable.msactiontable_xp20 import Xp20MsActionTable
11
+ from xp.services.actiontable.msactiontable_xp20_serializer import (
12
+ Xp20MsActionTableSerializer,
13
+ )
14
+ from xp.services.server.base_server_service import BaseServerService
15
+
16
+
17
+ class XP20ServerError(Exception):
18
+ """Raised when XP20 server operations fail."""
19
+
20
+ pass
21
+
22
+
23
+ class XP20ServerService(BaseServerService):
24
+ """
25
+ XP20 device emulation service.
26
+
27
+ Generates XP20-specific responses, handles XP20 device configuration,
28
+ and implements XP20 telegram format.
29
+ """
30
+
31
+ def __init__(
32
+ self,
33
+ serial_number: str,
34
+ _variant: str = "",
35
+ msactiontable_serializer: Optional[Xp20MsActionTableSerializer] = None,
36
+ ):
37
+ """Initialize XP20 server service.
38
+
39
+ Args:
40
+ serial_number: The device serial number.
41
+ _variant: Reserved parameter for consistency (unused).
42
+ msactiontable_serializer: MsActionTable serializer (injected via DI).
43
+ """
44
+ super().__init__(serial_number)
45
+ self.device_type = "XP20"
46
+ self.module_type_code = ModuleTypeCode.XP20 # XP20 module type from registry
47
+ self.firmware_version = "XP20_V0.01.05"
48
+
49
+ # MsActionTable support
50
+ self.msactiontable_serializer = (
51
+ msactiontable_serializer or Xp20MsActionTableSerializer()
52
+ )
53
+ self.msactiontable = self._get_default_msactiontable()
54
+
55
+ def _get_msactiontable_serializer(self) -> Optional[Xp20MsActionTableSerializer]:
56
+ """Get the MsActionTable serializer for XP20.
57
+
58
+ Returns:
59
+ The XP20 MsActionTable serializer instance.
60
+ """
61
+ return self.msactiontable_serializer
62
+
63
+ def _get_msactiontable(self) -> Optional[Xp20MsActionTable]:
64
+ """Get the MsActionTable for XP20.
65
+
66
+ Returns:
67
+ The XP20 MsActionTable instance.
68
+ """
69
+ return self.msactiontable
70
+
71
+ def _get_default_msactiontable(self) -> Xp20MsActionTable:
72
+ """Generate default MsActionTable configuration.
73
+
74
+ Returns:
75
+ Default XP20 MsActionTable with all inputs unconfigured.
76
+ """
77
+ # All inputs unconfigured (all flags False, AND functions empty)
78
+ return Xp20MsActionTable()
79
+
80
+ def get_device_info(self) -> Dict:
81
+ """Get XP20 device information.
82
+
83
+ Returns:
84
+ Dictionary containing device information.
85
+ """
86
+ return {
87
+ "serial_number": self.serial_number,
88
+ "device_type": self.device_type,
89
+ "firmware_version": self.firmware_version,
90
+ "status": self.device_status,
91
+ "link_number": self.link_number,
92
+ }
@@ -0,0 +1,58 @@
1
+ """XP230 Server Service for device emulation.
2
+
3
+ This service provides XP230-specific device emulation functionality,
4
+ including response generation and device configuration handling.
5
+ """
6
+
7
+ from typing import Dict, Optional
8
+
9
+ from xp.models import ModuleTypeCode
10
+ from xp.services.actiontable.msactiontable_serializer import MsActionTableSerializer
11
+ from xp.services.server.base_server_service import BaseServerService
12
+
13
+
14
+ class XP230ServerError(Exception):
15
+ """Raised when XP230 server operations fail."""
16
+
17
+ pass
18
+
19
+
20
+ class XP230ServerService(BaseServerService):
21
+ """
22
+ XP230 device emulation service.
23
+
24
+ Generates XP230-specific responses, handles XP230 device configuration,
25
+ and implements XP230 telegram format.
26
+ """
27
+
28
+ def __init__(
29
+ self,
30
+ serial_number: str,
31
+ _variant: str = "",
32
+ _msactiontable_serializer: Optional[MsActionTableSerializer] = None,
33
+ ):
34
+ """Initialize XP230 server service.
35
+
36
+ Args:
37
+ serial_number: The device serial number.
38
+ _variant: Reserved parameter for consistency (unused).
39
+ _msactiontable_serializer: Generic MsActionTable serializer (unused).
40
+ """
41
+ super().__init__(serial_number)
42
+ self.device_type = "XP230"
43
+ self.module_type_code = ModuleTypeCode.XP230 # XP230 module type from registry
44
+ self.firmware_version = "XP230_V1.00.04"
45
+
46
+ def get_device_info(self) -> Dict:
47
+ """Get XP230 device information.
48
+
49
+ Returns:
50
+ Dictionary containing device information.
51
+ """
52
+ return {
53
+ "serial_number": self.serial_number,
54
+ "device_type": self.device_type,
55
+ "firmware_version": self.firmware_version,
56
+ "status": self.device_status,
57
+ "link_number": self.link_number,
58
+ }