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,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
|
+
}
|