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,360 @@
|
|
|
1
|
+
"""Conbus Event Protocol for XP telegram communication.
|
|
2
|
+
|
|
3
|
+
This module implements the Twisted protocol for Conbus communication.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
from queue import SimpleQueue
|
|
8
|
+
from random import randint
|
|
9
|
+
from threading import Lock
|
|
10
|
+
from typing import Any, Callable, Optional
|
|
11
|
+
|
|
12
|
+
from psygnal import Signal
|
|
13
|
+
from twisted.internet import protocol
|
|
14
|
+
from twisted.internet.base import DelayedCall
|
|
15
|
+
from twisted.internet.interfaces import IAddress, IConnector
|
|
16
|
+
from twisted.internet.posixbase import PosixReactorBase
|
|
17
|
+
from twisted.python.failure import Failure
|
|
18
|
+
|
|
19
|
+
from xp.models import ConbusClientConfig
|
|
20
|
+
from xp.models.protocol.conbus_protocol import (
|
|
21
|
+
TelegramReceivedEvent,
|
|
22
|
+
)
|
|
23
|
+
from xp.models.telegram.system_function import SystemFunction
|
|
24
|
+
from xp.models.telegram.telegram_type import TelegramType
|
|
25
|
+
from xp.utils import calculate_checksum
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ConbusEventProtocol(protocol.Protocol, protocol.ClientFactory):
|
|
29
|
+
"""Twisted protocol for XP telegram communication.
|
|
30
|
+
|
|
31
|
+
Attributes:
|
|
32
|
+
buffer: Buffer for incoming telegram data.
|
|
33
|
+
logger: Logger instance for this protocol.
|
|
34
|
+
cli_config: Conbus configuration settings.
|
|
35
|
+
timeout_seconds: Timeout duration in seconds.
|
|
36
|
+
timeout_call: Delayed call handle for timeout management.
|
|
37
|
+
telegram_queue: FIFO queue for outgoing telegrams.
|
|
38
|
+
queue_manager_running: Flag indicating if queue manager is active.
|
|
39
|
+
queue_manager_lock: Lock for thread-safe queue manager access.
|
|
40
|
+
on_connection_made: Signal emitted when connection is established.
|
|
41
|
+
on_connection_lost: Signal emitted when connection is lost.
|
|
42
|
+
on_connection_failed: Signal emitted when connection fails.
|
|
43
|
+
on_client_connection_failed: Signal emitted when client connection fails.
|
|
44
|
+
on_client_connection_lost: Signal emitted when client connection is lost.
|
|
45
|
+
on_send_frame: Signal emitted when a frame is sent.
|
|
46
|
+
on_telegram_sent: Signal emitted when a telegram is sent.
|
|
47
|
+
on_data_received: Signal emitted when data is received.
|
|
48
|
+
on_telegram_received: Signal emitted when a telegram is received.
|
|
49
|
+
on_timeout: Signal emitted when timeout occurs.
|
|
50
|
+
on_failed: Signal emitted when operation fails.
|
|
51
|
+
on_start_reactor: Signal emitted when reactor starts.
|
|
52
|
+
on_stop_reactor: Signal emitted when reactor stops.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
buffer: bytes
|
|
56
|
+
|
|
57
|
+
telegram_queue: SimpleQueue[bytes] = SimpleQueue() # FIFO
|
|
58
|
+
queue_manager_running: bool = False
|
|
59
|
+
queue_manager_lock: Lock = Lock()
|
|
60
|
+
|
|
61
|
+
on_connection_made: Signal = Signal()
|
|
62
|
+
on_connection_lost: Signal = Signal()
|
|
63
|
+
on_connection_failed: Signal = Signal(Failure)
|
|
64
|
+
on_client_connection_failed: Signal = Signal(Failure)
|
|
65
|
+
on_client_connection_lost: Signal = Signal(Failure)
|
|
66
|
+
on_send_frame: Signal = Signal(bytes)
|
|
67
|
+
on_telegram_sent: Signal = Signal(bytes)
|
|
68
|
+
on_data_received: Signal = Signal(bytes)
|
|
69
|
+
on_telegram_received: Signal = Signal(TelegramReceivedEvent)
|
|
70
|
+
on_timeout: Signal = Signal()
|
|
71
|
+
on_failed: Signal = Signal(str)
|
|
72
|
+
on_start_reactor: Signal = Signal()
|
|
73
|
+
on_stop_reactor: Signal = Signal()
|
|
74
|
+
|
|
75
|
+
def __init__(
|
|
76
|
+
self,
|
|
77
|
+
cli_config: ConbusClientConfig,
|
|
78
|
+
reactor: PosixReactorBase,
|
|
79
|
+
) -> None:
|
|
80
|
+
"""Initialize ConbusProtocol.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
cli_config: Configuration for Conbus client connection.
|
|
84
|
+
reactor: Twisted reactor for event handling.
|
|
85
|
+
"""
|
|
86
|
+
self.buffer = b""
|
|
87
|
+
self.logger = logging.getLogger(__name__)
|
|
88
|
+
self.cli_config = cli_config.conbus
|
|
89
|
+
self._reactor = reactor
|
|
90
|
+
self.timeout_seconds = self.cli_config.timeout
|
|
91
|
+
self.timeout_call: Optional[DelayedCall] = None
|
|
92
|
+
|
|
93
|
+
def connectionMade(self) -> None:
|
|
94
|
+
"""Handle connection established event.
|
|
95
|
+
|
|
96
|
+
Called when TCP connection is successfully established.
|
|
97
|
+
Starts inactivity timeout monitoring.
|
|
98
|
+
"""
|
|
99
|
+
self.logger.debug("connectionMade")
|
|
100
|
+
self.on_connection_made.emit()
|
|
101
|
+
|
|
102
|
+
# Start inactivity timeout
|
|
103
|
+
self._reset_timeout()
|
|
104
|
+
|
|
105
|
+
def dataReceived(self, data: bytes) -> None:
|
|
106
|
+
"""Handle received data from TCP connection.
|
|
107
|
+
|
|
108
|
+
Parses incoming telegram frames and dispatches events.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
data: Raw bytes received from connection.
|
|
112
|
+
"""
|
|
113
|
+
self.logger.debug("dataReceived")
|
|
114
|
+
self.on_data_received.emit(data)
|
|
115
|
+
self.buffer += data
|
|
116
|
+
|
|
117
|
+
while True:
|
|
118
|
+
start = self.buffer.find(b"<")
|
|
119
|
+
if start == -1:
|
|
120
|
+
break
|
|
121
|
+
|
|
122
|
+
end = self.buffer.find(b">", start)
|
|
123
|
+
if end == -1:
|
|
124
|
+
break
|
|
125
|
+
|
|
126
|
+
# <S0123450001F02D12FK>
|
|
127
|
+
# <R0123450001F02D12FK>
|
|
128
|
+
# <E12L01I08MAK>
|
|
129
|
+
frame = self.buffer[start : end + 1] # <S0123450001F02D12FK>
|
|
130
|
+
self.buffer = self.buffer[end + 1 :]
|
|
131
|
+
telegram = frame[1:-1] # S0123450001F02D12FK
|
|
132
|
+
telegram_type = telegram[0:1].decode() # S
|
|
133
|
+
payload = telegram[:-2] # S0123450001F02D12
|
|
134
|
+
checksum = telegram[-2:].decode() # FK
|
|
135
|
+
serial_number = (
|
|
136
|
+
telegram[1:11] if telegram_type in ("S", "R") else b""
|
|
137
|
+
) # 0123450001
|
|
138
|
+
calculated_checksum = calculate_checksum(payload.decode(encoding="latin-1"))
|
|
139
|
+
|
|
140
|
+
checksum_valid = checksum == calculated_checksum
|
|
141
|
+
if not checksum_valid:
|
|
142
|
+
self.logger.debug(
|
|
143
|
+
f"Invalid checksum: {checksum}, calculated: {calculated_checksum}"
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
self.logger.debug(
|
|
147
|
+
f"frameReceived payload: {payload.decode('latin-1')}, checksum: {checksum}"
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# Reset timeout on activity
|
|
151
|
+
self._reset_timeout()
|
|
152
|
+
|
|
153
|
+
telegram_received = TelegramReceivedEvent(
|
|
154
|
+
protocol=self,
|
|
155
|
+
frame=frame.decode("latin-1"),
|
|
156
|
+
telegram=telegram.decode("latin-1"),
|
|
157
|
+
payload=payload.decode("latin-1"),
|
|
158
|
+
telegram_type=telegram_type,
|
|
159
|
+
serial_number=serial_number,
|
|
160
|
+
checksum=checksum,
|
|
161
|
+
checksum_valid=checksum_valid,
|
|
162
|
+
)
|
|
163
|
+
self.on_telegram_received.emit(telegram_received)
|
|
164
|
+
|
|
165
|
+
def sendFrame(self, data: bytes) -> None:
|
|
166
|
+
"""Send telegram frame.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
data: Raw telegram payload (without checksum/framing).
|
|
170
|
+
|
|
171
|
+
Raises:
|
|
172
|
+
IOError: If transport is not open.
|
|
173
|
+
"""
|
|
174
|
+
self.on_send_frame.emit(data)
|
|
175
|
+
|
|
176
|
+
# Calculate full frame (add checksum and brackets)
|
|
177
|
+
checksum = calculate_checksum(data.decode())
|
|
178
|
+
frame_data = data.decode() + checksum
|
|
179
|
+
frame = b"<" + frame_data.encode() + b">"
|
|
180
|
+
|
|
181
|
+
if not self.transport:
|
|
182
|
+
self.logger.info("Invalid transport")
|
|
183
|
+
raise IOError("Transport is not open")
|
|
184
|
+
|
|
185
|
+
self.logger.debug(f"Sending frame: {frame.decode()}")
|
|
186
|
+
self.transport.write(frame) # type: ignore
|
|
187
|
+
self.on_telegram_sent.emit(frame.decode())
|
|
188
|
+
self._reset_timeout()
|
|
189
|
+
|
|
190
|
+
def send_telegram(
|
|
191
|
+
self,
|
|
192
|
+
telegram_type: TelegramType,
|
|
193
|
+
serial_number: str,
|
|
194
|
+
system_function: SystemFunction,
|
|
195
|
+
data_value: str,
|
|
196
|
+
) -> None:
|
|
197
|
+
"""Send telegram with specified parameters.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
telegram_type: Type of telegram to send.
|
|
201
|
+
serial_number: Device serial number.
|
|
202
|
+
system_function: System function code.
|
|
203
|
+
data_value: Data value to send.
|
|
204
|
+
"""
|
|
205
|
+
payload = (
|
|
206
|
+
f"{telegram_type.value}"
|
|
207
|
+
f"{serial_number}"
|
|
208
|
+
f"F{system_function.value}"
|
|
209
|
+
f"D{data_value}"
|
|
210
|
+
)
|
|
211
|
+
self.telegram_queue.put_nowait(payload.encode())
|
|
212
|
+
self.call_later(0.0, self.start_queue_manager)
|
|
213
|
+
|
|
214
|
+
def call_later(
|
|
215
|
+
self,
|
|
216
|
+
delay: float,
|
|
217
|
+
callable_action: Callable[..., Any],
|
|
218
|
+
*args: object,
|
|
219
|
+
**kw: object,
|
|
220
|
+
) -> DelayedCall:
|
|
221
|
+
"""Schedule a callable to be called later.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
delay: Delay in seconds before calling.
|
|
225
|
+
callable_action: The callable to execute.
|
|
226
|
+
args: Positional arguments to pass to callable.
|
|
227
|
+
kw: Keyword arguments to pass to callable.
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
DelayedCall object that can be cancelled.
|
|
231
|
+
"""
|
|
232
|
+
return self._reactor.callLater(delay, callable_action, *args, **kw)
|
|
233
|
+
|
|
234
|
+
def buildProtocol(self, addr: IAddress) -> protocol.Protocol:
|
|
235
|
+
"""Build protocol instance for connection.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
addr: Address of the connection.
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
Protocol instance for this connection.
|
|
242
|
+
"""
|
|
243
|
+
self.logger.debug(f"buildProtocol: {addr}")
|
|
244
|
+
return self
|
|
245
|
+
|
|
246
|
+
def clientConnectionFailed(self, connector: IConnector, reason: Failure) -> None:
|
|
247
|
+
"""Handle client connection failure.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
connector: Connection connector instance.
|
|
251
|
+
reason: Failure reason details.
|
|
252
|
+
"""
|
|
253
|
+
self.logger.debug(f"clientConnectionFailed: {reason}")
|
|
254
|
+
self.on_client_connection_failed.emit(reason)
|
|
255
|
+
self.connection_failed(reason)
|
|
256
|
+
self._cancel_timeout()
|
|
257
|
+
|
|
258
|
+
def clientConnectionLost(self, connector: IConnector, reason: Failure) -> None:
|
|
259
|
+
"""Handle client connection lost event.
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
connector: Connection connector instance.
|
|
263
|
+
reason: Reason for connection loss.
|
|
264
|
+
"""
|
|
265
|
+
self.logger.debug(f"clientConnectionLost: {reason}")
|
|
266
|
+
self.on_connection_lost.emit(reason)
|
|
267
|
+
self._cancel_timeout()
|
|
268
|
+
|
|
269
|
+
def timeout(self) -> None:
|
|
270
|
+
"""Handle timeout event."""
|
|
271
|
+
self.logger.info("Timeout after: %ss", self.timeout_seconds)
|
|
272
|
+
self.on_timeout.emit()
|
|
273
|
+
|
|
274
|
+
def connection_failed(self, reason: Failure) -> None:
|
|
275
|
+
"""Handle connection failure.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
reason: Failure reason details.
|
|
279
|
+
"""
|
|
280
|
+
self.logger.debug(f"Client connection failed: {reason}")
|
|
281
|
+
self.on_connection_failed.emit(reason)
|
|
282
|
+
self.on_failed.emit(reason.getErrorMessage())
|
|
283
|
+
|
|
284
|
+
def _reset_timeout(self) -> None:
|
|
285
|
+
"""Reset the inactivity timeout."""
|
|
286
|
+
self._cancel_timeout()
|
|
287
|
+
self.timeout_call = self.call_later(self.timeout_seconds, self._on_timeout)
|
|
288
|
+
|
|
289
|
+
def _cancel_timeout(self) -> None:
|
|
290
|
+
"""Cancel the inactivity timeout."""
|
|
291
|
+
if self.timeout_call and self.timeout_call.active():
|
|
292
|
+
self.timeout_call.cancel()
|
|
293
|
+
|
|
294
|
+
def _on_timeout(self) -> None:
|
|
295
|
+
"""Handle inactivity timeout expiration."""
|
|
296
|
+
self.timeout()
|
|
297
|
+
self.logger.debug(f"Conbus timeout after {self.timeout_seconds} seconds")
|
|
298
|
+
|
|
299
|
+
def stop_reactor(self) -> None:
|
|
300
|
+
"""Stop the reactor if it's running."""
|
|
301
|
+
if self._reactor.running:
|
|
302
|
+
self.logger.info("Stopping reactor")
|
|
303
|
+
self._reactor.stop()
|
|
304
|
+
|
|
305
|
+
def start_reactor(self) -> None:
|
|
306
|
+
"""Start the reactor if it's running."""
|
|
307
|
+
# Connect to TCP server
|
|
308
|
+
self.logger.info(
|
|
309
|
+
f"Connecting to TCP server {self.cli_config.ip}:{self.cli_config.port}"
|
|
310
|
+
)
|
|
311
|
+
self._reactor.connectTCP(self.cli_config.ip, self.cli_config.port, self)
|
|
312
|
+
|
|
313
|
+
# Run the reactor (which now uses asyncio underneath)
|
|
314
|
+
self.logger.info("Starting reactor event loop.")
|
|
315
|
+
self._reactor.run()
|
|
316
|
+
|
|
317
|
+
def start_queue_manager(self) -> None:
|
|
318
|
+
"""Start the queue manager if it's not running."""
|
|
319
|
+
with self.queue_manager_lock:
|
|
320
|
+
if self.queue_manager_running:
|
|
321
|
+
return
|
|
322
|
+
self.logger.debug("Queue manager: starting")
|
|
323
|
+
self.queue_manager_running = True
|
|
324
|
+
self.process_telegram_queue()
|
|
325
|
+
|
|
326
|
+
def process_telegram_queue(self) -> None:
|
|
327
|
+
"""Start the queue manager if it's not running."""
|
|
328
|
+
self.logger.debug(
|
|
329
|
+
f"Queue manager: processing (remaining: {self.telegram_queue.qsize()})"
|
|
330
|
+
)
|
|
331
|
+
if self.telegram_queue.empty():
|
|
332
|
+
with self.queue_manager_lock:
|
|
333
|
+
self.logger.debug("Queue manager: stopping")
|
|
334
|
+
self.queue_manager_running = False
|
|
335
|
+
return
|
|
336
|
+
|
|
337
|
+
self.logger.debug("Queue manager: event loop")
|
|
338
|
+
telegram = self.telegram_queue.get_nowait()
|
|
339
|
+
self.sendFrame(telegram)
|
|
340
|
+
later = randint(10, 80) / 100
|
|
341
|
+
self.call_later(later, self.process_telegram_queue)
|
|
342
|
+
|
|
343
|
+
def __enter__(self) -> "ConbusEventProtocol":
|
|
344
|
+
"""Enter context manager.
|
|
345
|
+
|
|
346
|
+
Returns:
|
|
347
|
+
Self for context management.
|
|
348
|
+
"""
|
|
349
|
+
self.logger.debug("Entering the event loop.")
|
|
350
|
+
return self
|
|
351
|
+
|
|
352
|
+
def __exit__(
|
|
353
|
+
self,
|
|
354
|
+
_exc_type: Optional[type],
|
|
355
|
+
_exc_val: Optional[BaseException],
|
|
356
|
+
_exc_tb: Optional[Any],
|
|
357
|
+
) -> None:
|
|
358
|
+
"""Context manager exit - ensure connection is closed."""
|
|
359
|
+
self.logger.debug("Exiting the event loop.")
|
|
360
|
+
self.stop_reactor()
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
"""Conbus Protocol for XP telegram communication.
|
|
2
|
+
|
|
3
|
+
This module implements the Twisted protocol for Conbus communication.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
from typing import Any, Optional
|
|
8
|
+
|
|
9
|
+
from twisted.internet import protocol
|
|
10
|
+
from twisted.internet.base import DelayedCall
|
|
11
|
+
from twisted.internet.interfaces import IAddress, IConnector
|
|
12
|
+
from twisted.internet.posixbase import PosixReactorBase
|
|
13
|
+
from twisted.python.failure import Failure
|
|
14
|
+
|
|
15
|
+
from xp.models import ConbusClientConfig
|
|
16
|
+
from xp.models.protocol.conbus_protocol import (
|
|
17
|
+
TelegramReceivedEvent,
|
|
18
|
+
)
|
|
19
|
+
from xp.models.telegram.system_function import SystemFunction
|
|
20
|
+
from xp.models.telegram.telegram_type import TelegramType
|
|
21
|
+
from xp.utils import calculate_checksum
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ConbusProtocol(protocol.Protocol, protocol.ClientFactory):
|
|
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.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
buffer: bytes
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
cli_config: ConbusClientConfig,
|
|
41
|
+
reactor: PosixReactorBase,
|
|
42
|
+
) -> None:
|
|
43
|
+
"""Initialize ConbusProtocol.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
cli_config: Configuration for Conbus client connection.
|
|
47
|
+
reactor: Twisted reactor for event handling.
|
|
48
|
+
"""
|
|
49
|
+
self.buffer = b""
|
|
50
|
+
self.logger = logging.getLogger(__name__)
|
|
51
|
+
self.cli_config = cli_config.conbus
|
|
52
|
+
self.reactor = reactor
|
|
53
|
+
self.timeout_seconds = self.cli_config.timeout
|
|
54
|
+
self.timeout_call: Optional[DelayedCall] = None
|
|
55
|
+
|
|
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
|
+
"""
|
|
62
|
+
self.logger.debug("connectionMade")
|
|
63
|
+
self.connection_established()
|
|
64
|
+
# Start inactivity timeout
|
|
65
|
+
self._reset_timeout()
|
|
66
|
+
|
|
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
|
+
"""
|
|
75
|
+
self.logger.debug("dataReceived")
|
|
76
|
+
self.buffer += data
|
|
77
|
+
|
|
78
|
+
while True:
|
|
79
|
+
start = self.buffer.find(b"<")
|
|
80
|
+
if start == -1:
|
|
81
|
+
break
|
|
82
|
+
|
|
83
|
+
end = self.buffer.find(b">", start)
|
|
84
|
+
if end == -1:
|
|
85
|
+
break
|
|
86
|
+
|
|
87
|
+
# <S0123450001F02D12FK>
|
|
88
|
+
# <R0123450001F02D12FK>
|
|
89
|
+
# <E12L01I08MAK>
|
|
90
|
+
frame = self.buffer[start : end + 1] # <S0123450001F02D12FK>
|
|
91
|
+
self.buffer = self.buffer[end + 1 :]
|
|
92
|
+
telegram = frame[1:-1] # S0123450001F02D12FK
|
|
93
|
+
telegram_type = telegram[0:1].decode() # S
|
|
94
|
+
payload = telegram[:-2] # S0123450001F02D12
|
|
95
|
+
checksum = telegram[-2:].decode() # FK
|
|
96
|
+
serial_number = (
|
|
97
|
+
telegram[1:11] if telegram_type in ("S", "R") else b""
|
|
98
|
+
) # 0123450001
|
|
99
|
+
calculated_checksum = calculate_checksum(payload.decode(encoding="latin-1"))
|
|
100
|
+
|
|
101
|
+
checksum_valid = checksum == calculated_checksum
|
|
102
|
+
if not checksum_valid:
|
|
103
|
+
self.logger.debug(
|
|
104
|
+
f"Invalid checksum: {checksum}, calculated: {calculated_checksum}"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
self.logger.debug(
|
|
108
|
+
f"frameReceived payload: {payload.decode('latin-1')}, checksum: {checksum}"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Reset timeout on activity
|
|
112
|
+
self._reset_timeout()
|
|
113
|
+
|
|
114
|
+
telegram_received = TelegramReceivedEvent(
|
|
115
|
+
protocol=self,
|
|
116
|
+
frame=frame.decode("latin-1"),
|
|
117
|
+
telegram=telegram.decode("latin-1"),
|
|
118
|
+
payload=payload.decode("latin-1"),
|
|
119
|
+
telegram_type=telegram_type,
|
|
120
|
+
serial_number=serial_number,
|
|
121
|
+
checksum=checksum,
|
|
122
|
+
checksum_valid=checksum_valid,
|
|
123
|
+
)
|
|
124
|
+
self.telegram_received(telegram_received)
|
|
125
|
+
|
|
126
|
+
def sendFrame(self, data: bytes) -> None:
|
|
127
|
+
"""Send telegram frame.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
data: Raw telegram payload (without checksum/framing).
|
|
131
|
+
|
|
132
|
+
Raises:
|
|
133
|
+
IOError: If transport is not open.
|
|
134
|
+
"""
|
|
135
|
+
# Calculate full frame (add checksum and brackets)
|
|
136
|
+
checksum = calculate_checksum(data.decode())
|
|
137
|
+
frame_data = data.decode() + checksum
|
|
138
|
+
frame = b"<" + frame_data.encode() + b">"
|
|
139
|
+
|
|
140
|
+
if not self.transport:
|
|
141
|
+
self.logger.info("Invalid transport")
|
|
142
|
+
raise IOError("Transport is not open")
|
|
143
|
+
|
|
144
|
+
self.logger.debug(f"Sending frame: {frame.decode()}")
|
|
145
|
+
self.transport.write(frame) # type: ignore
|
|
146
|
+
self.telegram_sent(frame.decode())
|
|
147
|
+
self._reset_timeout()
|
|
148
|
+
|
|
149
|
+
def send_telegram(
|
|
150
|
+
self,
|
|
151
|
+
telegram_type: TelegramType,
|
|
152
|
+
serial_number: str,
|
|
153
|
+
system_function: SystemFunction,
|
|
154
|
+
data_value: str,
|
|
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
|
+
"""
|
|
164
|
+
payload = (
|
|
165
|
+
f"{telegram_type.value}"
|
|
166
|
+
f"{serial_number}"
|
|
167
|
+
f"F{system_function.value}"
|
|
168
|
+
f"D{data_value}"
|
|
169
|
+
)
|
|
170
|
+
self.sendFrame(payload.encode())
|
|
171
|
+
|
|
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
|
+
"""
|
|
181
|
+
self.logger.debug(f"buildProtocol: {addr}")
|
|
182
|
+
return self
|
|
183
|
+
|
|
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
|
+
"""
|
|
191
|
+
self.logger.debug(f"clientConnectionFailed: {reason}")
|
|
192
|
+
self.connection_failed(reason)
|
|
193
|
+
self._cancel_timeout()
|
|
194
|
+
self._stop_reactor()
|
|
195
|
+
|
|
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
|
+
"""
|
|
203
|
+
self.logger.debug(f"clientConnectionLost: {reason}")
|
|
204
|
+
self.connection_lost(reason)
|
|
205
|
+
self._cancel_timeout()
|
|
206
|
+
self._stop_reactor()
|
|
207
|
+
|
|
208
|
+
def timeout(self) -> bool:
|
|
209
|
+
"""Handle timeout event.
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
True to continue waiting for next timeout, False to stop.
|
|
213
|
+
"""
|
|
214
|
+
self.logger.info("Timeout after: %ss", self.timeout_seconds)
|
|
215
|
+
self.failed(f"Timeout after: {self.timeout_seconds}s")
|
|
216
|
+
return False
|
|
217
|
+
|
|
218
|
+
def connection_failed(self, reason: Failure) -> None:
|
|
219
|
+
"""Handle connection failure.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
reason: Failure reason details.
|
|
223
|
+
"""
|
|
224
|
+
self.logger.debug(f"Client connection failed: {reason}")
|
|
225
|
+
self.failed(reason.getErrorMessage())
|
|
226
|
+
|
|
227
|
+
def _reset_timeout(self) -> None:
|
|
228
|
+
"""Reset the inactivity timeout."""
|
|
229
|
+
self._cancel_timeout()
|
|
230
|
+
self.timeout_call = self.reactor.callLater(
|
|
231
|
+
self.timeout_seconds, self._on_timeout
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
def _cancel_timeout(self) -> None:
|
|
235
|
+
"""Cancel the inactivity timeout."""
|
|
236
|
+
if self.timeout_call and self.timeout_call.active():
|
|
237
|
+
self.timeout_call.cancel()
|
|
238
|
+
|
|
239
|
+
def _on_timeout(self) -> None:
|
|
240
|
+
"""Handle inactivity timeout expiration."""
|
|
241
|
+
self.logger.debug(f"Conbus timeout after {self.timeout_seconds} seconds")
|
|
242
|
+
continue_work = self.timeout()
|
|
243
|
+
if not continue_work:
|
|
244
|
+
self._stop_reactor()
|
|
245
|
+
|
|
246
|
+
def _stop_reactor(self) -> None:
|
|
247
|
+
"""Stop the reactor if it's running."""
|
|
248
|
+
if self.reactor.running:
|
|
249
|
+
self.logger.info("Stopping reactor")
|
|
250
|
+
self.reactor.stop()
|
|
251
|
+
|
|
252
|
+
def start_reactor(self) -> None:
|
|
253
|
+
"""Start the reactor if it's running."""
|
|
254
|
+
# Connect to TCP server
|
|
255
|
+
self.logger.info(
|
|
256
|
+
f"Connecting to TCP server {self.cli_config.ip}:{self.cli_config.port}"
|
|
257
|
+
)
|
|
258
|
+
self.reactor.connectTCP(self.cli_config.ip, self.cli_config.port, self)
|
|
259
|
+
|
|
260
|
+
# Run the reactor (which now uses asyncio underneath)
|
|
261
|
+
self.logger.info("Starting reactor event loop.")
|
|
262
|
+
self.reactor.run()
|
|
263
|
+
|
|
264
|
+
def __enter__(self) -> "ConbusProtocol":
|
|
265
|
+
"""Enter context manager.
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
Self for context management.
|
|
269
|
+
"""
|
|
270
|
+
return self
|
|
271
|
+
|
|
272
|
+
def __exit__(
|
|
273
|
+
self,
|
|
274
|
+
_exc_type: Optional[type],
|
|
275
|
+
_exc_val: Optional[BaseException],
|
|
276
|
+
_exc_tb: Optional[Any],
|
|
277
|
+
) -> None:
|
|
278
|
+
"""Context manager exit - ensure connection is closed."""
|
|
279
|
+
self.logger.debug("Exiting the event loop.")
|
|
280
|
+
self._stop_reactor()
|
|
281
|
+
|
|
282
|
+
"""Override methods."""
|
|
283
|
+
|
|
284
|
+
def telegram_sent(self, telegram_sent: str) -> None:
|
|
285
|
+
"""Override callback when telegram has been sent.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
telegram_sent: The telegram that was sent.
|
|
289
|
+
"""
|
|
290
|
+
pass
|
|
291
|
+
|
|
292
|
+
def telegram_received(self, telegram_received: TelegramReceivedEvent) -> None:
|
|
293
|
+
"""Override callback when telegram is received.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
telegram_received: Event containing received telegram details.
|
|
297
|
+
"""
|
|
298
|
+
pass
|
|
299
|
+
|
|
300
|
+
def connection_established(self) -> None:
|
|
301
|
+
"""Override callback when connection established."""
|
|
302
|
+
pass
|
|
303
|
+
|
|
304
|
+
def connection_lost(self, reason: Failure) -> None:
|
|
305
|
+
"""Override callback when connection is lost.
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
reason: Reason for connection loss.
|
|
309
|
+
"""
|
|
310
|
+
pass
|
|
311
|
+
|
|
312
|
+
def failed(self, message: str) -> None:
|
|
313
|
+
"""Override callback when connection failed.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
message: Error message describing the failure.
|
|
317
|
+
"""
|
|
318
|
+
pass
|