conson-xp 1.0.1__py3-none-any.whl → 1.2.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.0.1.dist-info → conson_xp-1.2.0.dist-info}/METADATA +1 -1
- conson_xp-1.2.0.dist-info/RECORD +181 -0
- xp/__init__.py +4 -3
- xp/api/main.py +18 -3
- xp/api/models/api.py +13 -2
- xp/api/models/discover.py +12 -2
- xp/api/routers/conbus_blink.py +18 -6
- xp/api/routers/conbus_custom.py +11 -3
- xp/api/routers/conbus_datapoint.py +10 -3
- xp/api/routers/conbus_output.py +29 -9
- xp/api/routers/errors.py +6 -5
- xp/cli/__init__.py +1 -1
- xp/cli/commands/__init__.py +1 -0
- xp/cli/commands/api.py +1 -5
- xp/cli/commands/api_start_commands.py +14 -8
- xp/cli/commands/conbus/conbus.py +9 -37
- xp/cli/commands/conbus/conbus_actiontable_commands.py +21 -1
- xp/cli/commands/conbus/conbus_autoreport_commands.py +21 -11
- xp/cli/commands/conbus/conbus_blink_commands.py +53 -21
- xp/cli/commands/conbus/conbus_config_commands.py +7 -4
- xp/cli/commands/conbus/conbus_custom_commands.py +13 -4
- xp/cli/commands/conbus/conbus_datapoint_commands.py +28 -8
- xp/cli/commands/conbus/conbus_discover_commands.py +15 -4
- xp/cli/commands/conbus/conbus_lightlevel_commands.py +50 -11
- xp/cli/commands/conbus/conbus_linknumber_commands.py +21 -11
- xp/cli/commands/conbus/conbus_msactiontable_commands.py +25 -1
- xp/cli/commands/conbus/conbus_output_commands.py +46 -12
- xp/cli/commands/conbus/conbus_raw_commands.py +17 -6
- xp/cli/commands/conbus/conbus_receive_commands.py +15 -7
- xp/cli/commands/conbus/conbus_scan_commands.py +35 -102
- xp/cli/commands/file_commands.py +26 -15
- xp/cli/commands/homekit/homekit.py +14 -8
- xp/cli/commands/homekit/homekit_start_commands.py +5 -5
- xp/cli/commands/module_commands.py +26 -19
- xp/cli/commands/reverse_proxy_commands.py +24 -18
- xp/cli/commands/server/server_commands.py +18 -18
- xp/cli/commands/telegram/telegram.py +4 -12
- xp/cli/commands/telegram/telegram_blink_commands.py +10 -8
- xp/cli/commands/telegram/telegram_checksum_commands.py +19 -8
- xp/cli/commands/telegram/telegram_discover_commands.py +2 -4
- xp/cli/commands/telegram/telegram_linknumber_commands.py +11 -8
- xp/cli/commands/telegram/telegram_parse_commands.py +10 -9
- xp/cli/commands/telegram/telegram_version_commands.py +8 -4
- xp/cli/main.py +5 -1
- xp/cli/utils/click_tree.py +23 -3
- xp/cli/utils/datapoint_type_choice.py +20 -0
- xp/cli/utils/decorators.py +165 -14
- xp/cli/utils/error_handlers.py +49 -18
- xp/cli/utils/formatters.py +95 -10
- xp/cli/utils/serial_number_type.py +18 -0
- xp/cli/utils/system_function_choice.py +20 -0
- xp/cli/utils/xp_module_type.py +20 -0
- xp/connection/__init__.py +1 -1
- xp/connection/exceptions.py +5 -5
- xp/models/__init__.py +1 -1
- xp/models/actiontable/__init__.py +1 -0
- xp/models/actiontable/actiontable.py +17 -1
- xp/models/actiontable/msactiontable_xp20.py +10 -0
- xp/models/actiontable/msactiontable_xp24.py +20 -3
- xp/models/actiontable/msactiontable_xp33.py +27 -4
- xp/models/conbus/__init__.py +1 -0
- xp/models/conbus/conbus.py +34 -4
- xp/models/conbus/conbus_autoreport.py +20 -2
- xp/models/conbus/conbus_blink.py +22 -2
- xp/models/conbus/conbus_client_config.py +22 -1
- xp/models/conbus/conbus_connection_status.py +16 -2
- xp/models/conbus/conbus_custom.py +21 -2
- xp/models/conbus/conbus_datapoint.py +22 -2
- xp/models/conbus/conbus_discover.py +18 -2
- xp/models/conbus/conbus_lightlevel.py +20 -2
- xp/models/conbus/conbus_linknumber.py +20 -2
- xp/models/conbus/conbus_output.py +22 -2
- xp/models/conbus/conbus_raw.py +17 -2
- xp/models/conbus/conbus_receive.py +16 -2
- xp/models/homekit/__init__.py +1 -0
- xp/models/homekit/homekit_accessory.py +15 -1
- xp/models/homekit/homekit_config.py +52 -0
- xp/models/homekit/homekit_conson_config.py +32 -0
- xp/models/log_entry.py +49 -9
- xp/models/protocol/__init__.py +1 -0
- xp/models/protocol/conbus_protocol.py +130 -21
- xp/models/telegram/__init__.py +1 -0
- xp/models/telegram/action_type.py +16 -2
- xp/models/telegram/datapoint_type.py +36 -2
- xp/models/telegram/event_telegram.py +46 -10
- xp/models/telegram/event_type.py +8 -1
- xp/models/telegram/input_action_type.py +34 -2
- xp/models/telegram/input_type.py +9 -1
- xp/models/telegram/module_type.py +69 -19
- xp/models/telegram/module_type_code.py +43 -1
- xp/models/telegram/output_telegram.py +30 -6
- xp/models/telegram/reply_telegram.py +56 -11
- xp/models/telegram/system_function.py +35 -3
- xp/models/telegram/system_telegram.py +18 -4
- xp/models/telegram/telegram.py +12 -3
- xp/models/telegram/telegram_type.py +8 -1
- xp/models/telegram/timeparam_type.py +27 -0
- xp/models/write_config_type.py +17 -2
- xp/services/__init__.py +1 -1
- xp/services/conbus/__init__.py +1 -0
- xp/services/conbus/actiontable/__init__.py +1 -0
- xp/services/conbus/actiontable/actiontable_service.py +33 -2
- xp/services/conbus/actiontable/msactiontable_service.py +40 -3
- xp/services/conbus/actiontable/msactiontable_xp24_serializer.py +36 -4
- xp/services/conbus/actiontable/msactiontable_xp33_serializer.py +45 -5
- xp/services/conbus/conbus_autoreport_get_service.py +17 -8
- xp/services/conbus/conbus_autoreport_set_service.py +29 -16
- xp/services/conbus/conbus_blink_all_service.py +40 -21
- xp/services/conbus/conbus_blink_service.py +37 -13
- xp/services/conbus/conbus_custom_service.py +29 -13
- xp/services/conbus/conbus_datapoint_queryall_service.py +40 -16
- xp/services/conbus/conbus_datapoint_service.py +33 -12
- xp/services/conbus/conbus_discover_service.py +43 -7
- xp/services/conbus/conbus_lightlevel_get_service.py +22 -14
- xp/services/conbus/conbus_lightlevel_set_service.py +40 -20
- xp/services/conbus/conbus_linknumber_get_service.py +18 -10
- xp/services/conbus/conbus_linknumber_set_service.py +34 -8
- xp/services/conbus/conbus_output_service.py +33 -13
- xp/services/conbus/conbus_raw_service.py +36 -16
- xp/services/conbus/conbus_receive_service.py +38 -6
- xp/services/conbus/conbus_scan_service.py +45 -19
- xp/services/homekit/__init__.py +1 -0
- xp/services/homekit/homekit_cache_service.py +31 -6
- xp/services/homekit/homekit_conbus_service.py +33 -2
- xp/services/homekit/homekit_config_validator.py +97 -15
- xp/services/homekit/homekit_conson_validator.py +51 -7
- xp/services/homekit/homekit_dimminglight.py +47 -1
- xp/services/homekit/homekit_dimminglight_service.py +35 -1
- xp/services/homekit/homekit_hap_service.py +71 -18
- xp/services/homekit/homekit_lightbulb.py +35 -1
- xp/services/homekit/homekit_lightbulb_service.py +30 -2
- xp/services/homekit/homekit_module_service.py +23 -1
- xp/services/homekit/homekit_outlet.py +47 -1
- xp/services/homekit/homekit_outlet_service.py +44 -2
- xp/services/homekit/homekit_service.py +113 -19
- xp/services/log_file_service.py +37 -41
- xp/services/module_type_service.py +26 -5
- xp/services/protocol/__init__.py +1 -1
- xp/services/protocol/conbus_protocol.py +115 -20
- xp/services/protocol/protocol_factory.py +40 -0
- xp/services/protocol/telegram_protocol.py +38 -7
- xp/services/reverse_proxy_service.py +79 -14
- xp/services/server/__init__.py +1 -0
- xp/services/server/base_server_service.py +102 -14
- xp/services/server/cp20_server_service.py +12 -4
- xp/services/server/server_service.py +26 -11
- xp/services/server/xp130_server_service.py +11 -3
- xp/services/server/xp20_server_service.py +11 -3
- xp/services/server/xp230_server_service.py +11 -3
- xp/services/server/xp24_server_service.py +33 -6
- xp/services/server/xp33_server_service.py +41 -8
- xp/services/telegram/__init__.py +1 -0
- xp/services/telegram/telegram_blink_service.py +19 -31
- xp/services/telegram/telegram_checksum_service.py +10 -10
- xp/services/telegram/telegram_discover_service.py +58 -29
- xp/services/telegram/telegram_link_number_service.py +27 -40
- xp/services/telegram/telegram_output_service.py +46 -49
- xp/services/telegram/telegram_service.py +41 -41
- xp/services/telegram/telegram_version_service.py +4 -2
- xp/utils/__init__.py +1 -1
- xp/utils/dependencies.py +0 -1
- xp/utils/serialization.py +6 -0
- xp/utils/time_utils.py +6 -11
- conson_xp-1.0.1.dist-info/RECORD +0 -181
- {conson_xp-1.0.1.dist-info → conson_xp-1.2.0.dist-info}/WHEEL +0 -0
- {conson_xp-1.0.1.dist-info → conson_xp-1.2.0.dist-info}/entry_points.txt +0 -0
- {conson_xp-1.0.1.dist-info → conson_xp-1.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
"""Conbus Protocol for XP telegram communication.
|
|
2
|
+
|
|
3
|
+
This module implements the Twisted protocol for Conbus communication.
|
|
4
|
+
"""
|
|
5
|
+
|
|
1
6
|
import logging
|
|
2
7
|
from typing import Any, Optional
|
|
3
8
|
|
|
@@ -17,8 +22,15 @@ from xp.utils import calculate_checksum
|
|
|
17
22
|
|
|
18
23
|
|
|
19
24
|
class ConbusProtocol(protocol.Protocol, protocol.ClientFactory):
|
|
20
|
-
"""
|
|
21
|
-
|
|
25
|
+
"""Twisted protocol for XP telegram communication.
|
|
26
|
+
|
|
27
|
+
Attributes:
|
|
28
|
+
buffer: Buffer for incoming telegram data.
|
|
29
|
+
logger: Logger instance for this protocol.
|
|
30
|
+
cli_config: Conbus configuration settings.
|
|
31
|
+
reactor: Twisted reactor instance.
|
|
32
|
+
timeout_seconds: Timeout duration in seconds.
|
|
33
|
+
timeout_call: Delayed call handle for timeout management.
|
|
22
34
|
"""
|
|
23
35
|
|
|
24
36
|
buffer: bytes
|
|
@@ -28,6 +40,12 @@ class ConbusProtocol(protocol.Protocol, protocol.ClientFactory):
|
|
|
28
40
|
cli_config: ConbusClientConfig,
|
|
29
41
|
reactor: PosixReactorBase,
|
|
30
42
|
) -> None:
|
|
43
|
+
"""Initialize ConbusProtocol.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
cli_config: Configuration for Conbus client connection.
|
|
47
|
+
reactor: Twisted reactor for event handling.
|
|
48
|
+
"""
|
|
31
49
|
self.buffer = b""
|
|
32
50
|
self.logger = logging.getLogger(__name__)
|
|
33
51
|
self.cli_config = cli_config.conbus
|
|
@@ -36,12 +54,24 @@ class ConbusProtocol(protocol.Protocol, protocol.ClientFactory):
|
|
|
36
54
|
self.timeout_call: Optional[DelayedCall] = None
|
|
37
55
|
|
|
38
56
|
def connectionMade(self) -> None:
|
|
57
|
+
"""Handle connection established event.
|
|
58
|
+
|
|
59
|
+
Called when TCP connection is successfully established.
|
|
60
|
+
Starts inactivity timeout monitoring.
|
|
61
|
+
"""
|
|
39
62
|
self.logger.debug("connectionMade")
|
|
40
63
|
self.connection_established()
|
|
41
64
|
# Start inactivity timeout
|
|
42
65
|
self._reset_timeout()
|
|
43
66
|
|
|
44
67
|
def dataReceived(self, data: bytes) -> None:
|
|
68
|
+
"""Handle received data from TCP connection.
|
|
69
|
+
|
|
70
|
+
Parses incoming telegram frames and dispatches events.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
data: Raw bytes received from connection.
|
|
74
|
+
"""
|
|
45
75
|
self.logger.debug("dataReceived")
|
|
46
76
|
self.buffer += data
|
|
47
77
|
|
|
@@ -75,7 +105,7 @@ class ConbusProtocol(protocol.Protocol, protocol.ClientFactory):
|
|
|
75
105
|
)
|
|
76
106
|
|
|
77
107
|
self.logger.debug(
|
|
78
|
-
f"frameReceived payload: {payload.decode()}, checksum: {checksum}"
|
|
108
|
+
f"frameReceived payload: {payload.decode('latin-1')}, checksum: {checksum}"
|
|
79
109
|
)
|
|
80
110
|
|
|
81
111
|
# Reset timeout on activity
|
|
@@ -83,9 +113,9 @@ class ConbusProtocol(protocol.Protocol, protocol.ClientFactory):
|
|
|
83
113
|
|
|
84
114
|
telegram_received = TelegramReceivedEvent(
|
|
85
115
|
protocol=self,
|
|
86
|
-
frame=frame.decode(),
|
|
87
|
-
telegram=telegram.decode(),
|
|
88
|
-
payload=payload.decode(),
|
|
116
|
+
frame=frame.decode("latin-1"),
|
|
117
|
+
telegram=telegram.decode("latin-1"),
|
|
118
|
+
payload=payload.decode("latin-1"),
|
|
89
119
|
telegram_type=telegram_type,
|
|
90
120
|
serial_number=serial_number,
|
|
91
121
|
checksum=checksum,
|
|
@@ -94,11 +124,13 @@ class ConbusProtocol(protocol.Protocol, protocol.ClientFactory):
|
|
|
94
124
|
self.telegram_received(telegram_received)
|
|
95
125
|
|
|
96
126
|
def sendFrame(self, data: bytes) -> None:
|
|
97
|
-
"""
|
|
98
|
-
Send telegram frame
|
|
127
|
+
"""Send telegram frame.
|
|
99
128
|
|
|
100
129
|
Args:
|
|
101
|
-
data: Raw telegram payload (without checksum/framing)
|
|
130
|
+
data: Raw telegram payload (without checksum/framing).
|
|
131
|
+
|
|
132
|
+
Raises:
|
|
133
|
+
IOError: If transport is not open.
|
|
102
134
|
"""
|
|
103
135
|
# Calculate full frame (add checksum and brackets)
|
|
104
136
|
checksum = calculate_checksum(data.decode())
|
|
@@ -112,6 +144,7 @@ class ConbusProtocol(protocol.Protocol, protocol.ClientFactory):
|
|
|
112
144
|
self.logger.debug(f"Sending frame: {frame.decode()}")
|
|
113
145
|
self.transport.write(frame) # type: ignore
|
|
114
146
|
self.telegram_sent(frame.decode())
|
|
147
|
+
self._reset_timeout()
|
|
115
148
|
|
|
116
149
|
def send_telegram(
|
|
117
150
|
self,
|
|
@@ -120,6 +153,14 @@ class ConbusProtocol(protocol.Protocol, protocol.ClientFactory):
|
|
|
120
153
|
system_function: SystemFunction,
|
|
121
154
|
data_value: str,
|
|
122
155
|
) -> None:
|
|
156
|
+
"""Send telegram with specified parameters.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
telegram_type: Type of telegram to send.
|
|
160
|
+
serial_number: Device serial number.
|
|
161
|
+
system_function: System function code.
|
|
162
|
+
data_value: Data value to send.
|
|
163
|
+
"""
|
|
123
164
|
payload = (
|
|
124
165
|
f"{telegram_type.value}"
|
|
125
166
|
f"{serial_number}"
|
|
@@ -129,33 +170,62 @@ class ConbusProtocol(protocol.Protocol, protocol.ClientFactory):
|
|
|
129
170
|
self.sendFrame(payload.encode())
|
|
130
171
|
|
|
131
172
|
def buildProtocol(self, addr: IAddress) -> protocol.Protocol:
|
|
173
|
+
"""Build protocol instance for connection.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
addr: Address of the connection.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Protocol instance for this connection.
|
|
180
|
+
"""
|
|
132
181
|
self.logger.debug(f"buildProtocol: {addr}")
|
|
133
182
|
return self
|
|
134
183
|
|
|
135
184
|
def clientConnectionFailed(self, connector: IConnector, reason: Failure) -> None:
|
|
185
|
+
"""Handle client connection failure.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
connector: Connection connector instance.
|
|
189
|
+
reason: Failure reason details.
|
|
190
|
+
"""
|
|
136
191
|
self.logger.debug(f"clientConnectionFailed: {reason}")
|
|
137
192
|
self.connection_failed(reason)
|
|
138
193
|
self._cancel_timeout()
|
|
139
194
|
self._stop_reactor()
|
|
140
195
|
|
|
141
196
|
def clientConnectionLost(self, connector: IConnector, reason: Failure) -> None:
|
|
197
|
+
"""Handle client connection lost event.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
connector: Connection connector instance.
|
|
201
|
+
reason: Reason for connection loss.
|
|
202
|
+
"""
|
|
142
203
|
self.logger.debug(f"clientConnectionLost: {reason}")
|
|
143
204
|
self.connection_lost(reason)
|
|
144
205
|
self._cancel_timeout()
|
|
145
206
|
self._stop_reactor()
|
|
146
207
|
|
|
147
208
|
def timeout(self) -> bool:
|
|
148
|
-
"""
|
|
209
|
+
"""Handle timeout event.
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
True to continue waiting for next timeout, False to stop.
|
|
213
|
+
"""
|
|
149
214
|
self.logger.info("Timeout after: %ss", self.timeout_seconds)
|
|
150
215
|
self.failed(f"Timeout after: {self.timeout_seconds}s")
|
|
151
216
|
return False
|
|
152
217
|
|
|
153
218
|
def connection_failed(self, reason: Failure) -> None:
|
|
219
|
+
"""Handle connection failure.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
reason: Failure reason details.
|
|
223
|
+
"""
|
|
154
224
|
self.logger.debug(f"Client connection failed: {reason}")
|
|
155
225
|
self.failed(reason.getErrorMessage())
|
|
156
226
|
|
|
157
227
|
def _reset_timeout(self) -> None:
|
|
158
|
-
"""Reset the inactivity timeout"""
|
|
228
|
+
"""Reset the inactivity timeout."""
|
|
159
229
|
self._cancel_timeout()
|
|
160
230
|
self.timeout_call = self.reactor.callLater(
|
|
161
231
|
self.timeout_seconds, self._on_timeout
|
|
@@ -163,26 +233,26 @@ class ConbusProtocol(protocol.Protocol, protocol.ClientFactory):
|
|
|
163
233
|
self.logger.debug(f"Timeout set for {self.timeout_seconds} seconds")
|
|
164
234
|
|
|
165
235
|
def _cancel_timeout(self) -> None:
|
|
166
|
-
"""Cancel the inactivity timeout"""
|
|
236
|
+
"""Cancel the inactivity timeout."""
|
|
167
237
|
if self.timeout_call and self.timeout_call.active():
|
|
168
238
|
self.timeout_call.cancel()
|
|
169
239
|
self.logger.debug("Timeout cancelled")
|
|
170
240
|
|
|
171
241
|
def _on_timeout(self) -> None:
|
|
172
|
-
"""
|
|
242
|
+
"""Handle inactivity timeout expiration."""
|
|
173
243
|
self.logger.debug(f"Conbus timeout after {self.timeout_seconds} seconds")
|
|
174
244
|
continue_work = self.timeout()
|
|
175
245
|
if not continue_work:
|
|
176
246
|
self._stop_reactor()
|
|
177
247
|
|
|
178
248
|
def _stop_reactor(self) -> None:
|
|
179
|
-
"""Stop the reactor if it's running"""
|
|
249
|
+
"""Stop the reactor if it's running."""
|
|
180
250
|
if self.reactor.running:
|
|
181
251
|
self.logger.info("Stopping reactor")
|
|
182
252
|
self.reactor.stop()
|
|
183
253
|
|
|
184
254
|
def start_reactor(self) -> None:
|
|
185
|
-
"""Start the reactor if it's running"""
|
|
255
|
+
"""Start the reactor if it's running."""
|
|
186
256
|
# Connect to TCP server
|
|
187
257
|
self.logger.info(
|
|
188
258
|
f"Connecting to TCP server {self.cli_config.ip}:{self.cli_config.port}"
|
|
@@ -190,11 +260,15 @@ class ConbusProtocol(protocol.Protocol, protocol.ClientFactory):
|
|
|
190
260
|
self.reactor.connectTCP(self.cli_config.ip, self.cli_config.port, self)
|
|
191
261
|
|
|
192
262
|
# Run the reactor (which now uses asyncio underneath)
|
|
193
|
-
self.logger.info("Starting reactor event loop
|
|
263
|
+
self.logger.info("Starting reactor event loop.")
|
|
194
264
|
self.reactor.run()
|
|
195
265
|
|
|
196
266
|
def __enter__(self) -> "ConbusProtocol":
|
|
197
|
-
"""
|
|
267
|
+
"""Enter context manager.
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
Self for context management.
|
|
271
|
+
"""
|
|
198
272
|
return self
|
|
199
273
|
|
|
200
274
|
def __exit__(
|
|
@@ -203,23 +277,44 @@ class ConbusProtocol(protocol.Protocol, protocol.ClientFactory):
|
|
|
203
277
|
_exc_val: Optional[BaseException],
|
|
204
278
|
_exc_tb: Optional[Any],
|
|
205
279
|
) -> None:
|
|
206
|
-
"""Context manager exit - ensure connection is closed"""
|
|
207
|
-
self.logger.debug("Exiting the event loop
|
|
280
|
+
"""Context manager exit - ensure connection is closed."""
|
|
281
|
+
self.logger.debug("Exiting the event loop.")
|
|
208
282
|
self._stop_reactor()
|
|
209
283
|
|
|
210
|
-
"""Override methods"""
|
|
284
|
+
"""Override methods."""
|
|
211
285
|
|
|
212
286
|
def telegram_sent(self, telegram_sent: str) -> None:
|
|
287
|
+
"""Override callback when telegram has been sent.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
telegram_sent: The telegram that was sent.
|
|
291
|
+
"""
|
|
213
292
|
pass
|
|
214
293
|
|
|
215
294
|
def telegram_received(self, telegram_received: TelegramReceivedEvent) -> None:
|
|
295
|
+
"""Override callback when telegram is received.
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
telegram_received: Event containing received telegram details.
|
|
299
|
+
"""
|
|
216
300
|
pass
|
|
217
301
|
|
|
218
302
|
def connection_established(self) -> None:
|
|
303
|
+
"""Override callback when connection established."""
|
|
219
304
|
pass
|
|
220
305
|
|
|
221
306
|
def connection_lost(self, reason: Failure) -> None:
|
|
307
|
+
"""Override callback when connection is lost.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
reason: Reason for connection loss.
|
|
311
|
+
"""
|
|
222
312
|
pass
|
|
223
313
|
|
|
224
314
|
def failed(self, message: str) -> None:
|
|
315
|
+
"""Override callback when connection failed.
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
message: Error message describing the failure.
|
|
319
|
+
"""
|
|
225
320
|
pass
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
"""Protocol Factory for Twisted protocol creation.
|
|
2
|
+
|
|
3
|
+
This module provides factory classes for protocol instantiation.
|
|
4
|
+
"""
|
|
5
|
+
|
|
1
6
|
import logging
|
|
2
7
|
|
|
3
8
|
from bubus import EventBus
|
|
@@ -13,26 +18,61 @@ from xp.services.protocol import TelegramProtocol
|
|
|
13
18
|
|
|
14
19
|
|
|
15
20
|
class TelegramFactory(protocol.ClientFactory):
|
|
21
|
+
"""Factory for creating Telegram protocol instances.
|
|
22
|
+
|
|
23
|
+
Attributes:
|
|
24
|
+
event_bus: Event bus for dispatching protocol events.
|
|
25
|
+
telegram_protocol: Protocol instance to use.
|
|
26
|
+
connector: Connection connector instance.
|
|
27
|
+
logger: Logger instance for this factory.
|
|
28
|
+
"""
|
|
29
|
+
|
|
16
30
|
def __init__(
|
|
17
31
|
self,
|
|
18
32
|
event_bus: EventBus,
|
|
19
33
|
telegram_protocol: TelegramProtocol,
|
|
20
34
|
connector: IConnector,
|
|
21
35
|
) -> None:
|
|
36
|
+
"""Initialize TelegramFactory.
|
|
22
37
|
|
|
38
|
+
Args:
|
|
39
|
+
event_bus: Event bus for protocol events.
|
|
40
|
+
telegram_protocol: Protocol instance to use for connections.
|
|
41
|
+
connector: Connection connector for managing connections.
|
|
42
|
+
"""
|
|
23
43
|
self.event_bus = event_bus
|
|
24
44
|
self.telegram_protocol = telegram_protocol
|
|
25
45
|
self.connector = connector
|
|
26
46
|
self.logger = logging.getLogger(__name__)
|
|
27
47
|
|
|
28
48
|
def buildProtocol(self, addr: IAddress) -> TelegramProtocol:
|
|
49
|
+
"""Build protocol instance for connection.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
addr: Address of the connection.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Telegram protocol instance for this connection.
|
|
56
|
+
"""
|
|
29
57
|
self.logger.debug(f"buildProtocol: {addr}")
|
|
30
58
|
return self.telegram_protocol
|
|
31
59
|
|
|
32
60
|
def clientConnectionFailed(self, connector: IConnector, reason: Failure) -> None:
|
|
61
|
+
"""Handle connection failure event.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
connector: Connection connector instance.
|
|
65
|
+
reason: Failure reason details.
|
|
66
|
+
"""
|
|
33
67
|
self.event_bus.dispatch(ConnectionFailedEvent(reason=str(reason)))
|
|
34
68
|
self.connector.stop()
|
|
35
69
|
|
|
36
70
|
def clientConnectionLost(self, connector: IConnector, reason: Failure) -> None:
|
|
71
|
+
"""Handle connection lost event.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
connector: Connection connector instance.
|
|
75
|
+
reason: Reason for connection loss.
|
|
76
|
+
"""
|
|
37
77
|
self.event_bus.dispatch(ConnectionLostEvent(reason=str(reason)))
|
|
38
78
|
self.connector.stop()
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
"""Telegram Protocol for XP telegram communication.
|
|
2
|
+
|
|
3
|
+
This module provides the protocol implementation for telegram-based communication.
|
|
4
|
+
"""
|
|
5
|
+
|
|
1
6
|
import asyncio
|
|
2
7
|
import logging
|
|
3
8
|
import time
|
|
@@ -15,17 +20,30 @@ from xp.utils import calculate_checksum
|
|
|
15
20
|
|
|
16
21
|
|
|
17
22
|
class TelegramProtocol(protocol.Protocol):
|
|
18
|
-
"""
|
|
19
|
-
Twisted protocol for XP telegram communication with built-in debouncing.
|
|
23
|
+
"""Twisted protocol for XP telegram communication with built-in debouncing.
|
|
20
24
|
|
|
21
25
|
Automatically deduplicates identical telegram frames sent within a
|
|
22
26
|
configurable time window (default 50ms).
|
|
27
|
+
|
|
28
|
+
Attributes:
|
|
29
|
+
buffer: Buffer for incoming telegram data.
|
|
30
|
+
event_bus: Event bus for dispatching protocol events.
|
|
31
|
+
debounce_ms: Debounce time window in milliseconds.
|
|
32
|
+
logger: Logger instance for this protocol.
|
|
33
|
+
send_queue: Dictionary tracking frame send timestamps.
|
|
34
|
+
timer_handle: Handle for cleanup timer.
|
|
23
35
|
"""
|
|
24
36
|
|
|
25
37
|
buffer: bytes
|
|
26
38
|
event_bus: EventBus
|
|
27
39
|
|
|
28
40
|
def __init__(self, event_bus: EventBus, debounce_ms: int = 50) -> None:
|
|
41
|
+
"""Initialize TelegramProtocol.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
event_bus: Event bus for dispatching protocol events.
|
|
45
|
+
debounce_ms: Debounce time window in milliseconds.
|
|
46
|
+
"""
|
|
29
47
|
self.buffer = b""
|
|
30
48
|
self.event_bus = event_bus
|
|
31
49
|
self.debounce_ms = debounce_ms
|
|
@@ -36,6 +54,7 @@ class TelegramProtocol(protocol.Protocol):
|
|
|
36
54
|
self.timer_handle: Optional[asyncio.TimerHandle] = None
|
|
37
55
|
|
|
38
56
|
def connectionMade(self) -> None:
|
|
57
|
+
"""Handle connection established event."""
|
|
39
58
|
self.logger.debug("connectionMade")
|
|
40
59
|
try:
|
|
41
60
|
self.logger.debug("Scheduling async connection handler")
|
|
@@ -45,7 +64,11 @@ class TelegramProtocol(protocol.Protocol):
|
|
|
45
64
|
self.logger.error(f"Error scheduling async handler: {e}", exc_info=True)
|
|
46
65
|
|
|
47
66
|
def _on_task_done(self, task: asyncio.Task) -> None:
|
|
48
|
-
"""
|
|
67
|
+
"""Handle async task completion.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
task: Completed async task.
|
|
71
|
+
"""
|
|
49
72
|
try:
|
|
50
73
|
if task.exception():
|
|
51
74
|
self.logger.error(
|
|
@@ -57,7 +80,7 @@ class TelegramProtocol(protocol.Protocol):
|
|
|
57
80
|
self.logger.error(f"Error in task done callback: {e}", exc_info=True)
|
|
58
81
|
|
|
59
82
|
async def _async_connection_made(self) -> None:
|
|
60
|
-
"""Async handler for connection made"""
|
|
83
|
+
"""Async handler for connection made."""
|
|
61
84
|
self.logger.debug("_async_connectionMade starting")
|
|
62
85
|
self.logger.info("Dispatching ConnectionMadeEvent")
|
|
63
86
|
try:
|
|
@@ -69,12 +92,16 @@ class TelegramProtocol(protocol.Protocol):
|
|
|
69
92
|
)
|
|
70
93
|
|
|
71
94
|
def dataReceived(self, data: bytes) -> None:
|
|
72
|
-
"""
|
|
95
|
+
"""Handle received data from Twisted.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
data: Raw bytes received from connection.
|
|
99
|
+
"""
|
|
73
100
|
task = asyncio.create_task(self._async_dataReceived(data))
|
|
74
101
|
task.add_done_callback(self._on_task_done)
|
|
75
102
|
|
|
76
103
|
async def _async_dataReceived(self, data: bytes) -> None:
|
|
77
|
-
"""Async handler for received data"""
|
|
104
|
+
"""Async handler for received data."""
|
|
78
105
|
self.logger.debug("dataReceived")
|
|
79
106
|
self.buffer += data
|
|
80
107
|
|
|
@@ -137,7 +164,11 @@ class TelegramProtocol(protocol.Protocol):
|
|
|
137
164
|
)
|
|
138
165
|
|
|
139
166
|
def sendFrame(self, data: bytes) -> None:
|
|
140
|
-
"""
|
|
167
|
+
"""Send telegram frame.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
data: Raw telegram payload (without checksum/framing).
|
|
171
|
+
"""
|
|
141
172
|
task = asyncio.create_task(self._async_sendFrame(data))
|
|
142
173
|
task.add_done_callback(self._on_task_done)
|
|
143
174
|
|
|
@@ -16,7 +16,7 @@ from xp.models.response import Response
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
class ReverseProxyError(Exception):
|
|
19
|
-
"""Raised when Conbus reverse proxy operations fail"""
|
|
19
|
+
"""Raised when Conbus reverse proxy operations fail."""
|
|
20
20
|
|
|
21
21
|
pass
|
|
22
22
|
|
|
@@ -28,6 +28,17 @@ class ReverseProxyService:
|
|
|
28
28
|
Accepts client connections on port 10001 and forwards all telegrams
|
|
29
29
|
to the target server configured in cli.yml. Monitors and prints all
|
|
30
30
|
bidirectional traffic with timestamps.
|
|
31
|
+
|
|
32
|
+
Attributes:
|
|
33
|
+
logger: Logger instance for the service.
|
|
34
|
+
listen_port: Port to listen on for client connections.
|
|
35
|
+
server_socket: Main server socket for accepting connections.
|
|
36
|
+
is_running: Flag indicating if proxy is running.
|
|
37
|
+
active_connections: Dictionary of active connection information.
|
|
38
|
+
connection_counter: Counter for connection IDs.
|
|
39
|
+
cli_config: Conbus client configuration.
|
|
40
|
+
target_ip: Target server IP address.
|
|
41
|
+
target_port: Target server port number.
|
|
31
42
|
"""
|
|
32
43
|
|
|
33
44
|
def __init__(
|
|
@@ -35,7 +46,12 @@ class ReverseProxyService:
|
|
|
35
46
|
cli_config: ConbusClientConfig,
|
|
36
47
|
listen_port: int,
|
|
37
48
|
):
|
|
38
|
-
"""Initialize the Conbus reverse proxy service
|
|
49
|
+
"""Initialize the Conbus reverse proxy service.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
cli_config: Conbus client configuration.
|
|
53
|
+
listen_port: Port to listen on for client connections.
|
|
54
|
+
"""
|
|
39
55
|
# Set up logging first
|
|
40
56
|
self.logger = logging.getLogger(__name__)
|
|
41
57
|
|
|
@@ -50,16 +66,28 @@ class ReverseProxyService:
|
|
|
50
66
|
|
|
51
67
|
@property
|
|
52
68
|
def target_ip(self) -> str:
|
|
53
|
-
"""Get target server IP
|
|
69
|
+
"""Get target server IP.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Target server IP address.
|
|
73
|
+
"""
|
|
54
74
|
return self.cli_config.conbus.ip
|
|
55
75
|
|
|
56
76
|
@property
|
|
57
77
|
def target_port(self) -> int:
|
|
58
|
-
"""Get target server port
|
|
78
|
+
"""Get target server port.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Target server port number.
|
|
82
|
+
"""
|
|
59
83
|
return self.cli_config.conbus.port
|
|
60
84
|
|
|
61
85
|
def start_proxy(self) -> Response:
|
|
62
|
-
"""Start the reverse proxy server
|
|
86
|
+
"""Start the reverse proxy server.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
Response object with success status and proxy details.
|
|
90
|
+
"""
|
|
63
91
|
if self.is_running:
|
|
64
92
|
return Response(
|
|
65
93
|
success=False, data=None, error="Reverse proxy is already running"
|
|
@@ -111,7 +139,11 @@ class ReverseProxyService:
|
|
|
111
139
|
)
|
|
112
140
|
|
|
113
141
|
def stop_proxy(self) -> Response:
|
|
114
|
-
"""Stop the reverse proxy server
|
|
142
|
+
"""Stop the reverse proxy server.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
Response object with success status.
|
|
146
|
+
"""
|
|
115
147
|
if not self.is_running:
|
|
116
148
|
return Response(
|
|
117
149
|
success=False, data=None, error="Reverse proxy is not running"
|
|
@@ -139,7 +171,11 @@ class ReverseProxyService:
|
|
|
139
171
|
)
|
|
140
172
|
|
|
141
173
|
def get_status(self) -> Response:
|
|
142
|
-
"""Get current proxy status and active connections
|
|
174
|
+
"""Get current proxy status and active connections.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
Response object with proxy status and connection details.
|
|
178
|
+
"""
|
|
143
179
|
return Response(
|
|
144
180
|
success=True,
|
|
145
181
|
data={
|
|
@@ -161,7 +197,7 @@ class ReverseProxyService:
|
|
|
161
197
|
)
|
|
162
198
|
|
|
163
199
|
def _accept_connections(self) -> None:
|
|
164
|
-
"""Accept and handle client connections"""
|
|
200
|
+
"""Accept and handle client connections."""
|
|
165
201
|
while self.is_running:
|
|
166
202
|
try:
|
|
167
203
|
# Accept connection
|
|
@@ -194,7 +230,13 @@ class ReverseProxyService:
|
|
|
194
230
|
def _handle_client(
|
|
195
231
|
self, client_socket: socket.socket, client_address: tuple, conn_id: str
|
|
196
232
|
) -> None:
|
|
197
|
-
"""Handle individual client connection with server relay
|
|
233
|
+
"""Handle individual client connection with server relay.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
client_socket: Client socket connection.
|
|
237
|
+
client_address: Client address tuple (ip, port).
|
|
238
|
+
conn_id: Connection identifier.
|
|
239
|
+
"""
|
|
198
240
|
try:
|
|
199
241
|
# Connect to target server
|
|
200
242
|
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
@@ -272,7 +314,15 @@ class ReverseProxyService:
|
|
|
272
314
|
dest_label: str,
|
|
273
315
|
conn_id: str,
|
|
274
316
|
) -> None:
|
|
275
|
-
"""Relay data between sockets with telegram monitoring
|
|
317
|
+
"""Relay data between sockets with telegram monitoring.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
source_socket: Source socket to receive from.
|
|
321
|
+
dest_socket: Destination socket to send to.
|
|
322
|
+
source_label: Label for source in logs.
|
|
323
|
+
dest_label: Label for destination in logs.
|
|
324
|
+
conn_id: Connection identifier.
|
|
325
|
+
"""
|
|
276
326
|
try:
|
|
277
327
|
while self.is_running:
|
|
278
328
|
# Receive data from source
|
|
@@ -316,7 +366,11 @@ class ReverseProxyService:
|
|
|
316
366
|
self.logger.error(f"Error in data relay: {e} [{conn_id}]")
|
|
317
367
|
|
|
318
368
|
def _close_connection_pair(self, conn_id: str) -> None:
|
|
319
|
-
"""Close both client and server sockets for a connection
|
|
369
|
+
"""Close both client and server sockets for a connection.
|
|
370
|
+
|
|
371
|
+
Args:
|
|
372
|
+
conn_id: Connection identifier.
|
|
373
|
+
"""
|
|
320
374
|
if conn_id not in self.active_connections:
|
|
321
375
|
return
|
|
322
376
|
|
|
@@ -344,7 +398,10 @@ class ReverseProxyService:
|
|
|
344
398
|
f"Client {client_address} disconnected [{conn_id}] - {bytes_relayed} bytes relayed"
|
|
345
399
|
)
|
|
346
400
|
print(
|
|
347
|
-
f"{self.timestamp()} [DISCONNECTION]
|
|
401
|
+
f"{self.timestamp()} [DISCONNECTION] "
|
|
402
|
+
f"Client {client_address} "
|
|
403
|
+
f"disconnected [{conn_id}] - "
|
|
404
|
+
f"{bytes_relayed} bytes relayed"
|
|
348
405
|
)
|
|
349
406
|
|
|
350
407
|
# Remove from active connections
|
|
@@ -352,11 +409,19 @@ class ReverseProxyService:
|
|
|
352
409
|
|
|
353
410
|
@staticmethod
|
|
354
411
|
def timestamp() -> str:
|
|
355
|
-
"""Generate timestamp string for logging
|
|
412
|
+
"""Generate timestamp string for logging.
|
|
413
|
+
|
|
414
|
+
Returns:
|
|
415
|
+
Timestamp string in HH:MM:SS,mmm format.
|
|
416
|
+
"""
|
|
356
417
|
return datetime.now().strftime("%H:%M:%S,%f")[:-3]
|
|
357
418
|
|
|
358
419
|
def run_blocking(self) -> None:
|
|
359
|
-
"""Run the proxy in blocking mode (for CLI usage)
|
|
420
|
+
"""Run the proxy in blocking mode (for CLI usage).
|
|
421
|
+
|
|
422
|
+
Raises:
|
|
423
|
+
ReverseProxyError: If proxy fails to start.
|
|
424
|
+
"""
|
|
360
425
|
result = self.start_proxy()
|
|
361
426
|
if not result.success:
|
|
362
427
|
raise ReverseProxyError(result.error)
|
xp/services/server/__init__.py
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Server services for XP protocol variants."""
|