conson-xp 1.4.0__py3-none-any.whl → 1.6.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.4.0.dist-info → conson_xp-1.6.0.dist-info}/METADATA +1 -1
- {conson_xp-1.4.0.dist-info → conson_xp-1.6.0.dist-info}/RECORD +24 -21
- xp/__init__.py +1 -1
- xp/services/actiontable/__init__.py +1 -0
- xp/services/actiontable/msactiontable_serializer.py +7 -0
- xp/services/conbus/actiontable/actiontable_service.py +1 -1
- xp/services/conbus/actiontable/msactiontable_service.py +3 -3
- xp/services/server/base_server_service.py +119 -1
- xp/services/server/cp20_server_service.py +9 -1
- xp/services/server/device_service_factory.py +94 -0
- xp/services/server/server_service.py +102 -59
- xp/services/server/xp130_server_service.py +10 -2
- xp/services/server/xp20_server_service.py +44 -2
- xp/services/server/xp230_server_service.py +10 -2
- xp/services/server/xp24_server_service.py +54 -1
- xp/services/server/xp33_server_service.py +231 -2
- xp/utils/dependencies.py +21 -6
- {conson_xp-1.4.0.dist-info → conson_xp-1.6.0.dist-info}/WHEEL +0 -0
- {conson_xp-1.4.0.dist-info → conson_xp-1.6.0.dist-info}/entry_points.txt +0 -0
- {conson_xp-1.4.0.dist-info → conson_xp-1.6.0.dist-info}/licenses/LICENSE +0 -0
- /xp/services/{conbus/actiontable → actiontable}/actiontable_serializer.py +0 -0
- /xp/services/{conbus/actiontable → actiontable}/msactiontable_xp20_serializer.py +0 -0
- /xp/services/{conbus/actiontable → actiontable}/msactiontable_xp24_serializer.py +0 -0
- /xp/services/{conbus/actiontable → actiontable}/msactiontable_xp33_serializer.py +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
conson_xp-1.
|
|
2
|
-
conson_xp-1.
|
|
3
|
-
conson_xp-1.
|
|
4
|
-
conson_xp-1.
|
|
5
|
-
xp/__init__.py,sha256=
|
|
1
|
+
conson_xp-1.6.0.dist-info/METADATA,sha256=NeCIj-sEltfweIjEbTTiJMkUhm6_JDk3o4qqgj2AQy4,9274
|
|
2
|
+
conson_xp-1.6.0.dist-info/WHEEL,sha256=9P2ygRxDrTJz3gsagc0Z96ukrxjr-LFBGOgv3AuKlCA,90
|
|
3
|
+
conson_xp-1.6.0.dist-info/entry_points.txt,sha256=1OcdIcDM1hz3ljCXgybaPUh1IOFEwkaTgLIW9u9zGeg,50
|
|
4
|
+
conson_xp-1.6.0.dist-info/licenses/LICENSE,sha256=rxj6woMM-r3YCyGq_UHFtbh7kHTAJgrccH6O-33IDE4,1419
|
|
5
|
+
xp/__init__.py,sha256=ch-TVxJdSXvFaMGwGhp04iVA5GRAiAhsU9Ehnb0Y3qM,180
|
|
6
6
|
xp/cli/__init__.py,sha256=QjnKB1KaI2aIyKlzrnvCwfbBuUj8HNgwNMvNJVQofbI,81
|
|
7
7
|
xp/cli/__main__.py,sha256=l2iKwMdat5rTGd3JWs-uGksnYYDDffp_Npz05QdKEeU,117
|
|
8
8
|
xp/cli/commands/__init__.py,sha256=02CbZoKmNX-fn5etX4Hdgg2lUt1MsLFPYx2VkXZyFJ8,4394
|
|
@@ -97,14 +97,16 @@ xp/models/telegram/telegram_type.py,sha256=GhqKP63oNMyh2tIvCPcsC5RFp4s4JjhmEqCLC
|
|
|
97
97
|
xp/models/telegram/timeparam_type.py,sha256=Ar8xvSfPmOAgR2g2Je0FgvP01SL7bPvZn5_HrVDpmJM,1137
|
|
98
98
|
xp/models/write_config_type.py,sha256=T2RaO52RpzoJ4782uMHE-fX7Ymx3CaIQAEwByydXq1M,881
|
|
99
99
|
xp/services/__init__.py,sha256=W9YZyrkh7vm--ZHhAXNQiOYQs5yhhmUHXP5I0Lf1XBg,782
|
|
100
|
+
xp/services/actiontable/__init__.py,sha256=z6js4EuJ6xKHaseTEhuEvKo1tr9K1XyQiruReJtBiPY,26
|
|
101
|
+
xp/services/actiontable/actiontable_serializer.py,sha256=x45-8d5Ba9l3hX2TFC5nqKv-g_244g-VTWhXvVXL8Jg,5159
|
|
102
|
+
xp/services/actiontable/msactiontable_serializer.py,sha256=RRL6TZ1gpSQw81kAiw2BV3jTqm4fCJC0pWIcO26Cmos,174
|
|
103
|
+
xp/services/actiontable/msactiontable_xp20_serializer.py,sha256=EYspooOdi0Z8oaXGxpazwnUoTmh-d7U9auhu11iBgmU,6527
|
|
104
|
+
xp/services/actiontable/msactiontable_xp24_serializer.py,sha256=30qsk9UKje1n32PPc4YoGV1lw_ZvgxNqqd8ZDgzMJpg,4504
|
|
105
|
+
xp/services/actiontable/msactiontable_xp33_serializer.py,sha256=nuWfka4U9W4lpTcS8uD6azXFcryPb0CUO5O7Z28G1k8,8901
|
|
100
106
|
xp/services/conbus/__init__.py,sha256=Hi35sMKu9o6LpYoi2tQDaQoMb8M5sOt_-LUTxxaCU_0,28
|
|
101
107
|
xp/services/conbus/actiontable/__init__.py,sha256=oD6vRk_Ye-eZ9s_hldAgtRJFu4mfAnODqpkJUGHHszk,40
|
|
102
|
-
xp/services/conbus/actiontable/
|
|
103
|
-
xp/services/conbus/actiontable/
|
|
104
|
-
xp/services/conbus/actiontable/msactiontable_service.py,sha256=u64nejKvHzMdmlK9VoM7P3uMGIfjyfo2xp9dXXlgvjc,7451
|
|
105
|
-
xp/services/conbus/actiontable/msactiontable_xp20_serializer.py,sha256=EYspooOdi0Z8oaXGxpazwnUoTmh-d7U9auhu11iBgmU,6527
|
|
106
|
-
xp/services/conbus/actiontable/msactiontable_xp24_serializer.py,sha256=30qsk9UKje1n32PPc4YoGV1lw_ZvgxNqqd8ZDgzMJpg,4504
|
|
107
|
-
xp/services/conbus/actiontable/msactiontable_xp33_serializer.py,sha256=nuWfka4U9W4lpTcS8uD6azXFcryPb0CUO5O7Z28G1k8,8901
|
|
108
|
+
xp/services/conbus/actiontable/actiontable_service.py,sha256=uy-BFCsjDoe1ZuZy9cTwRSIfMSxznLEN-iMtTsPW3EI,5626
|
|
109
|
+
xp/services/conbus/actiontable/msactiontable_service.py,sha256=wVYFBqj7gngZ-6Kk5AsNiTYeNe3xZoPMr4-lmcbAUAE,7430
|
|
108
110
|
xp/services/conbus/conbus_blink_all_service.py,sha256=OaEg4b8AEiEruHSkZ5jDtaoI81vwwxLq4KWXO7zBdD0,6582
|
|
109
111
|
xp/services/conbus/conbus_blink_service.py,sha256=x9uM-sLnIEV8wSNsvJgo08E042g-Hh2ZF3rXkz-k_9s,5824
|
|
110
112
|
xp/services/conbus/conbus_custom_service.py,sha256=4aneYdPObiZOGxPFYg5Wr70cl_xFxlQIdJBPQSa0enI,5826
|
|
@@ -138,14 +140,15 @@ xp/services/protocol/protocol_factory.py,sha256=PmjN9AtW9sxNo3voqUiNgQA-pTvX1RW4
|
|
|
138
140
|
xp/services/protocol/telegram_protocol.py,sha256=Ki5DrXsKxiaqLcdP9WWUuhUI7cPu2DfwyZkh-Gv9Lb8,9496
|
|
139
141
|
xp/services/reverse_proxy_service.py,sha256=BUOlcLlTU-R5iuC_96rasug21xo19wK9_4fMQXxc0QM,15061
|
|
140
142
|
xp/services/server/__init__.py,sha256=QEcCj-jK0goAukJCe15TKYFQfSAzWsduPT_wW0HxZU8,48
|
|
141
|
-
xp/services/server/base_server_service.py,sha256=
|
|
142
|
-
xp/services/server/cp20_server_service.py,sha256=
|
|
143
|
-
xp/services/server/
|
|
144
|
-
xp/services/server/
|
|
145
|
-
xp/services/server/
|
|
146
|
-
xp/services/server/
|
|
147
|
-
xp/services/server/
|
|
148
|
-
xp/services/server/
|
|
143
|
+
xp/services/server/base_server_service.py,sha256=Yt1gfepRt7GLo_hsBqIzNMUye_JtEM8FupP9uTYJBUA,13294
|
|
144
|
+
xp/services/server/cp20_server_service.py,sha256=SXdI6Jt400T9sLdw86ovEqKRGeV3nYVaHEA9Gcj6W2A,2041
|
|
145
|
+
xp/services/server/device_service_factory.py,sha256=Y4TvSFALeq0zYzHfCwcbikSpmIyYbLcvm9756n5Jm7Q,3744
|
|
146
|
+
xp/services/server/server_service.py,sha256=N-_WVNG2J2tH-12ZLAo5SLGIHuEseZObJ6X7PhQt8Yo,16216
|
|
147
|
+
xp/services/server/xp130_server_service.py,sha256=YnvetDp72-QzkyDGB4qfZZIwFs03HuibUOz2zb9XR0c,2191
|
|
148
|
+
xp/services/server/xp20_server_service.py,sha256=1wJ7A-bRkN9O5Spu3q3LNDW31mNtNF2eNMQ5E6O2ltA,2928
|
|
149
|
+
xp/services/server/xp230_server_service.py,sha256=k9ftCY5tjLFP31mKVCspq283RVaPkGx-Yq61Urk8JLs,1815
|
|
150
|
+
xp/services/server/xp24_server_service.py,sha256=S4kDZHf6SsFTwIzk1PwkWntFHtmOuVcz6UclkRdTGsc,8670
|
|
151
|
+
xp/services/server/xp33_server_service.py,sha256=X5BJr7RYueHAPNrfW-HnqV7ZN-OAouKxH1qMdDADqhk,19745
|
|
149
152
|
xp/services/telegram/__init__.py,sha256=kv0JgMg13Fp18WgGQpalNRAWwiWbrz18X4kZAP9xpSQ,48
|
|
150
153
|
xp/services/telegram/telegram_blink_service.py,sha256=Xctc9mCSZiiW1YTh8cA-4jlc8fTioS5OxT6ymhSqiYI,4487
|
|
151
154
|
xp/services/telegram/telegram_checksum_service.py,sha256=rp_C5PlraOOIyqZDp9XjBBNZLUeBLdQNNHVpN6D-1v8,4729
|
|
@@ -157,8 +160,8 @@ xp/services/telegram/telegram_service.py,sha256=CQKmwV0Jmlr1WwrshaANyp_e77DjBzXz
|
|
|
157
160
|
xp/services/telegram/telegram_version_service.py,sha256=M5HdOTsLdcwo122FP-jW6R740ktLrtKf2TiMDVz23h8,10528
|
|
158
161
|
xp/utils/__init__.py,sha256=_avMF_UOkfR3tNeDIPqQ5odmbq5raKkaq1rZ9Cn1CJs,332
|
|
159
162
|
xp/utils/checksum.py,sha256=HDpiQxmdIedbCbZ4o_Box0i_Zig417BtCV_46ZyhiTk,1711
|
|
160
|
-
xp/utils/dependencies.py,sha256=
|
|
163
|
+
xp/utils/dependencies.py,sha256=xNnwVWxMThq8V9fGRgqMUJABMBaZ88NF7rW3iYBQlDE,18505
|
|
161
164
|
xp/utils/event_helper.py,sha256=W-A_xmoXlpWZBbJH6qdaN50o3-XrwFsDgvAGMJDiAgo,1001
|
|
162
165
|
xp/utils/serialization.py,sha256=RWHHk86feaB4ZP7rjE4qOWK0900yg2joUBDkP76gfOY,4618
|
|
163
166
|
xp/utils/time_utils.py,sha256=dEyViDlAG9GWU-J3D_YVa-sGma6yiyyMTgN4h2x3PY4,3781
|
|
164
|
-
conson_xp-1.
|
|
167
|
+
conson_xp-1.6.0.dist-info/RECORD,,
|
xp/__init__.py
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Action table utils."""
|
|
@@ -10,7 +10,7 @@ from xp.models.actiontable.actiontable import ActionTable
|
|
|
10
10
|
from xp.models.protocol.conbus_protocol import TelegramReceivedEvent
|
|
11
11
|
from xp.models.telegram.system_function import SystemFunction
|
|
12
12
|
from xp.models.telegram.telegram_type import TelegramType
|
|
13
|
-
from xp.services.
|
|
13
|
+
from xp.services.actiontable.actiontable_serializer import ActionTableSerializer
|
|
14
14
|
from xp.services.protocol import ConbusProtocol
|
|
15
15
|
from xp.services.telegram.telegram_service import TelegramService
|
|
16
16
|
|
|
@@ -12,13 +12,13 @@ from xp.models.actiontable.msactiontable_xp33 import Xp33MsActionTable
|
|
|
12
12
|
from xp.models.protocol.conbus_protocol import TelegramReceivedEvent
|
|
13
13
|
from xp.models.telegram.system_function import SystemFunction
|
|
14
14
|
from xp.models.telegram.telegram_type import TelegramType
|
|
15
|
-
from xp.services.
|
|
15
|
+
from xp.services.actiontable.msactiontable_xp20_serializer import (
|
|
16
16
|
Xp20MsActionTableSerializer,
|
|
17
17
|
)
|
|
18
|
-
from xp.services.
|
|
18
|
+
from xp.services.actiontable.msactiontable_xp24_serializer import (
|
|
19
19
|
Xp24MsActionTableSerializer,
|
|
20
20
|
)
|
|
21
|
-
from xp.services.
|
|
21
|
+
from xp.services.actiontable.msactiontable_xp33_serializer import (
|
|
22
22
|
Xp33MsActionTableSerializer,
|
|
23
23
|
)
|
|
24
24
|
from xp.services.protocol import ConbusProtocol
|
|
@@ -5,8 +5,9 @@ containing common functionality like module type response generation.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import logging
|
|
8
|
+
import threading
|
|
8
9
|
from abc import ABC
|
|
9
|
-
from typing import Optional
|
|
10
|
+
from typing import Any, Optional
|
|
10
11
|
|
|
11
12
|
from xp.models import ModuleTypeCode
|
|
12
13
|
from xp.models.telegram.datapoint_type import DataPointType
|
|
@@ -42,6 +43,12 @@ class BaseServerService(ABC):
|
|
|
42
43
|
self.temperature: str = "+23,5§C"
|
|
43
44
|
self.voltage: str = "+12,5§V"
|
|
44
45
|
|
|
46
|
+
self.telegram_buffer: list[str] = []
|
|
47
|
+
self.telegram_buffer_lock = threading.Lock() # Lock for socket set
|
|
48
|
+
|
|
49
|
+
# MsActionTable download state (None, "ack_sent", "data_sent")
|
|
50
|
+
self.msactiontable_download_state: Optional[str] = None
|
|
51
|
+
|
|
45
52
|
def generate_datapoint_type_response(
|
|
46
53
|
self, datapoint_type: DataPointType
|
|
47
54
|
) -> Optional[str]:
|
|
@@ -147,6 +154,83 @@ class BaseServerService(ABC):
|
|
|
147
154
|
|
|
148
155
|
return None
|
|
149
156
|
|
|
157
|
+
def _get_msactiontable_serializer(self) -> Optional[Any]:
|
|
158
|
+
"""Get the MsActionTable serializer for this device.
|
|
159
|
+
|
|
160
|
+
Subclasses should override this to return their specific serializer.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
The serializer instance, or None if not supported.
|
|
164
|
+
"""
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
def _get_msactiontable(self) -> Optional[Any]:
|
|
168
|
+
"""Get the MsActionTable for this device.
|
|
169
|
+
|
|
170
|
+
Subclasses should override this to return their msactiontable instance.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
The msactiontable instance, or None if not supported.
|
|
174
|
+
"""
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
def _handle_download_msactiontable_request(
|
|
178
|
+
self, request: SystemTelegram
|
|
179
|
+
) -> Optional[str]:
|
|
180
|
+
"""Handle F13D - DOWNLOAD_MSACTIONTABLE request.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
request: The system telegram request to process.
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
ACK telegram if request is valid, NAK otherwise.
|
|
187
|
+
"""
|
|
188
|
+
if (
|
|
189
|
+
request.system_function == SystemFunction.DOWNLOAD_MSACTIONTABLE
|
|
190
|
+
and self.msactiontable_download_state is None
|
|
191
|
+
):
|
|
192
|
+
self.msactiontable_download_state = "ack_sent"
|
|
193
|
+
# Send ACK and queue data telegram
|
|
194
|
+
return self._build_response_telegram(f"R{self.serial_number}F18D") # ACK
|
|
195
|
+
|
|
196
|
+
return self._build_response_telegram(f"R{self.serial_number}F19D") # NAK
|
|
197
|
+
|
|
198
|
+
def _handle_download_msactiontable_ack_request(
|
|
199
|
+
self, _request: SystemTelegram
|
|
200
|
+
) -> Optional[str]:
|
|
201
|
+
"""Handle MsActionTable download ACK protocol.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
_request: The system telegram request (unused, kept for signature consistency).
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
Data telegram, EOF telegram, or NAK if state is invalid.
|
|
208
|
+
"""
|
|
209
|
+
serializer = self._get_msactiontable_serializer()
|
|
210
|
+
msactiontable = self._get_msactiontable()
|
|
211
|
+
|
|
212
|
+
# Only handle if serializer and msactiontable are available
|
|
213
|
+
if not serializer or msactiontable is None:
|
|
214
|
+
return None
|
|
215
|
+
|
|
216
|
+
# Handle F18D - CONTINUE (after ACK or data)
|
|
217
|
+
if self.msactiontable_download_state == "ack_sent":
|
|
218
|
+
# Send MsActionTable data
|
|
219
|
+
encoded_data = serializer.to_data(msactiontable)
|
|
220
|
+
data_telegram = self._build_response_telegram(
|
|
221
|
+
f"R{self.serial_number}F17D{encoded_data}"
|
|
222
|
+
)
|
|
223
|
+
self.msactiontable_download_state = "data_sent"
|
|
224
|
+
return data_telegram
|
|
225
|
+
|
|
226
|
+
elif self.msactiontable_download_state == "data_sent":
|
|
227
|
+
# Send EOF
|
|
228
|
+
eof_telegram = self._build_response_telegram(f"R{self.serial_number}F16D")
|
|
229
|
+
self.msactiontable_download_state = None
|
|
230
|
+
return eof_telegram
|
|
231
|
+
|
|
232
|
+
return self._build_response_telegram(f"R{self.serial_number}F19D") # NAK
|
|
233
|
+
|
|
150
234
|
def process_system_telegram(self, request: SystemTelegram) -> Optional[str]:
|
|
151
235
|
"""Template method for processing system telegrams.
|
|
152
236
|
|
|
@@ -173,6 +257,15 @@ class BaseServerService(ABC):
|
|
|
173
257
|
elif request.system_function == SystemFunction.ACTION:
|
|
174
258
|
return self._handle_action_request(request)
|
|
175
259
|
|
|
260
|
+
elif request.system_function == SystemFunction.DOWNLOAD_MSACTIONTABLE:
|
|
261
|
+
return self._handle_download_msactiontable_request(request)
|
|
262
|
+
|
|
263
|
+
elif (
|
|
264
|
+
request.system_function == SystemFunction.ACK
|
|
265
|
+
and self.msactiontable_download_state
|
|
266
|
+
):
|
|
267
|
+
return self._handle_download_msactiontable_ack_request(request)
|
|
268
|
+
|
|
176
269
|
self.logger.warning(f"Unhandled {self.device_type} request: {request}")
|
|
177
270
|
return None
|
|
178
271
|
|
|
@@ -257,3 +350,28 @@ class BaseServerService(ABC):
|
|
|
257
350
|
The response telegram string, or None if request cannot be handled.
|
|
258
351
|
"""
|
|
259
352
|
return None
|
|
353
|
+
|
|
354
|
+
def add_telegram_buffer(self, telegram: str) -> None:
|
|
355
|
+
"""Add telegram to the buffer.
|
|
356
|
+
|
|
357
|
+
Args:
|
|
358
|
+
telegram: The telegram string to add to the buffer.
|
|
359
|
+
"""
|
|
360
|
+
self.logger.debug(f"Add telegram to the buffer: {telegram}")
|
|
361
|
+
with self.telegram_buffer_lock:
|
|
362
|
+
self.telegram_buffer.append(telegram)
|
|
363
|
+
|
|
364
|
+
def collect_telegram_buffer(self) -> list[str]:
|
|
365
|
+
"""Collecting telegrams from the buffer.
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
List of telegram strings from the buffer. The buffer is cleared after collection.
|
|
369
|
+
"""
|
|
370
|
+
self.logger.debug(
|
|
371
|
+
f"Collecting {self.serial_number} telegrams from buffer: {len(self.telegram_buffer)}"
|
|
372
|
+
)
|
|
373
|
+
with self.telegram_buffer_lock:
|
|
374
|
+
result = self.telegram_buffer.copy()
|
|
375
|
+
self.logger.debug(f"Resetting {self.serial_number} buffer")
|
|
376
|
+
self.telegram_buffer.clear()
|
|
377
|
+
return result
|
|
@@ -8,6 +8,7 @@ from typing import Dict, Optional
|
|
|
8
8
|
|
|
9
9
|
from xp.models import ModuleTypeCode
|
|
10
10
|
from xp.models.telegram.system_telegram import SystemTelegram
|
|
11
|
+
from xp.services.actiontable.msactiontable_serializer import MsActionTableSerializer
|
|
11
12
|
from xp.services.server.base_server_service import BaseServerService
|
|
12
13
|
|
|
13
14
|
|
|
@@ -25,11 +26,18 @@ class CP20ServerService(BaseServerService):
|
|
|
25
26
|
and implements CP20 telegram format.
|
|
26
27
|
"""
|
|
27
28
|
|
|
28
|
-
def __init__(
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
serial_number: str,
|
|
32
|
+
_variant: str = "",
|
|
33
|
+
_msactiontable_serializer: Optional[MsActionTableSerializer] = None,
|
|
34
|
+
):
|
|
29
35
|
"""Initialize CP20 server service.
|
|
30
36
|
|
|
31
37
|
Args:
|
|
32
38
|
serial_number: The device serial number.
|
|
39
|
+
_variant: Reserved parameter for consistency (unused).
|
|
40
|
+
_msactiontable_serializer: Generic MsActionTable serializer (unused).
|
|
33
41
|
"""
|
|
34
42
|
super().__init__(serial_number)
|
|
35
43
|
self.device_type = "CP20"
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Device Service Factory for creating device instances.
|
|
2
|
+
|
|
3
|
+
This module provides a factory for creating device service instances
|
|
4
|
+
with proper dependency injection of serializers.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from xp.services.actiontable.msactiontable_serializer import MsActionTableSerializer
|
|
8
|
+
from xp.services.actiontable.msactiontable_xp20_serializer import (
|
|
9
|
+
Xp20MsActionTableSerializer,
|
|
10
|
+
)
|
|
11
|
+
from xp.services.actiontable.msactiontable_xp24_serializer import (
|
|
12
|
+
Xp24MsActionTableSerializer,
|
|
13
|
+
)
|
|
14
|
+
from xp.services.actiontable.msactiontable_xp33_serializer import (
|
|
15
|
+
Xp33MsActionTableSerializer,
|
|
16
|
+
)
|
|
17
|
+
from xp.services.server.base_server_service import BaseServerService
|
|
18
|
+
from xp.services.server.cp20_server_service import CP20ServerService
|
|
19
|
+
from xp.services.server.xp20_server_service import XP20ServerService
|
|
20
|
+
from xp.services.server.xp24_server_service import XP24ServerService
|
|
21
|
+
from xp.services.server.xp33_server_service import XP33ServerService
|
|
22
|
+
from xp.services.server.xp130_server_service import XP130ServerService
|
|
23
|
+
from xp.services.server.xp230_server_service import XP230ServerService
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class DeviceServiceFactory:
|
|
27
|
+
"""Factory for creating device service instances.
|
|
28
|
+
|
|
29
|
+
Encapsulates device creation logic and handles serializer injection
|
|
30
|
+
for different device types.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
xp20ms_serializer: Xp20MsActionTableSerializer,
|
|
36
|
+
xp24ms_serializer: Xp24MsActionTableSerializer,
|
|
37
|
+
xp33ms_serializer: Xp33MsActionTableSerializer,
|
|
38
|
+
ms_serializer: MsActionTableSerializer,
|
|
39
|
+
):
|
|
40
|
+
"""Initialize device service factory.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
xp20ms_serializer: XP20 MsActionTable serializer (injected via DI).
|
|
44
|
+
xp24ms_serializer: XP24 MsActionTable serializer (injected via DI).
|
|
45
|
+
xp33ms_serializer: XP33 MsActionTable serializer (injected via DI).
|
|
46
|
+
ms_serializer: Generic MsActionTable serializer (injected via DI).
|
|
47
|
+
"""
|
|
48
|
+
self.xp20ms_serializer = xp20ms_serializer
|
|
49
|
+
self.xp24ms_serializer = xp24ms_serializer
|
|
50
|
+
self.xp33ms_serializer = xp33ms_serializer
|
|
51
|
+
self.ms_serializer = ms_serializer
|
|
52
|
+
|
|
53
|
+
def create_device(self, module_type: str, serial_number: str) -> BaseServerService:
|
|
54
|
+
"""Create device instance for given module type.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
module_type: Module type code (e.g., "XP20", "XP33LR").
|
|
58
|
+
serial_number: Device serial number.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Device service instance configured with appropriate serializer.
|
|
62
|
+
|
|
63
|
+
Raises:
|
|
64
|
+
ValueError: If module_type is unknown or unsupported.
|
|
65
|
+
"""
|
|
66
|
+
# Map module types to their constructors and parameters
|
|
67
|
+
if module_type == "CP20":
|
|
68
|
+
return CP20ServerService(serial_number, "CP20", self.ms_serializer)
|
|
69
|
+
|
|
70
|
+
elif module_type == "XP24":
|
|
71
|
+
return XP24ServerService(serial_number, "XP24", self.xp24ms_serializer)
|
|
72
|
+
|
|
73
|
+
elif module_type == "XP33":
|
|
74
|
+
return XP33ServerService(serial_number, "XP33", self.xp33ms_serializer)
|
|
75
|
+
|
|
76
|
+
elif module_type == "XP33LR":
|
|
77
|
+
return XP33ServerService(serial_number, "XP33LR", self.xp33ms_serializer)
|
|
78
|
+
|
|
79
|
+
elif module_type == "XP33LED":
|
|
80
|
+
return XP33ServerService(serial_number, "XP33LED", self.xp33ms_serializer)
|
|
81
|
+
|
|
82
|
+
elif module_type == "XP20":
|
|
83
|
+
return XP20ServerService(serial_number, "XP20", self.xp20ms_serializer)
|
|
84
|
+
|
|
85
|
+
elif module_type == "XP130":
|
|
86
|
+
return XP130ServerService(serial_number, "XP130", self.ms_serializer)
|
|
87
|
+
|
|
88
|
+
elif module_type == "XP230":
|
|
89
|
+
return XP230ServerService(serial_number, "XP230", self.ms_serializer)
|
|
90
|
+
|
|
91
|
+
else:
|
|
92
|
+
raise ValueError(
|
|
93
|
+
f"Unknown device type '{module_type}' for serial {serial_number}"
|
|
94
|
+
)
|
|
@@ -8,19 +8,14 @@ import logging
|
|
|
8
8
|
import socket
|
|
9
9
|
import threading
|
|
10
10
|
from pathlib import Path
|
|
11
|
-
from typing import Dict, List, Optional
|
|
11
|
+
from typing import Dict, List, Optional
|
|
12
12
|
|
|
13
13
|
from xp.models.homekit.homekit_conson_config import (
|
|
14
14
|
ConsonModuleConfig,
|
|
15
15
|
ConsonModuleListConfig,
|
|
16
16
|
)
|
|
17
17
|
from xp.services.server.base_server_service import BaseServerService
|
|
18
|
-
from xp.services.server.
|
|
19
|
-
from xp.services.server.xp20_server_service import XP20ServerService
|
|
20
|
-
from xp.services.server.xp24_server_service import XP24ServerService
|
|
21
|
-
from xp.services.server.xp33_server_service import XP33ServerService
|
|
22
|
-
from xp.services.server.xp130_server_service import XP130ServerService
|
|
23
|
-
from xp.services.server.xp230_server_service import XP230ServerService
|
|
18
|
+
from xp.services.server.device_service_factory import DeviceServiceFactory
|
|
24
19
|
from xp.services.telegram.telegram_discover_service import TelegramDiscoverService
|
|
25
20
|
from xp.services.telegram.telegram_service import TelegramService
|
|
26
21
|
|
|
@@ -43,6 +38,7 @@ class ServerService:
|
|
|
43
38
|
self,
|
|
44
39
|
telegram_service: TelegramService,
|
|
45
40
|
discover_service: TelegramDiscoverService,
|
|
41
|
+
device_factory: DeviceServiceFactory,
|
|
46
42
|
config_path: str = "server.yml",
|
|
47
43
|
port: int = 10001,
|
|
48
44
|
):
|
|
@@ -51,25 +47,28 @@ class ServerService:
|
|
|
51
47
|
Args:
|
|
52
48
|
telegram_service: Service for parsing system telegrams.
|
|
53
49
|
discover_service: Service for handling discover requests.
|
|
50
|
+
device_factory: Factory for creating device service instances (injected via DI).
|
|
54
51
|
config_path: Path to the server configuration file.
|
|
55
52
|
port: TCP port to listen on.
|
|
56
53
|
"""
|
|
57
54
|
self.telegram_service = telegram_service
|
|
58
55
|
self.discover_service = discover_service
|
|
56
|
+
self.device_factory = device_factory
|
|
59
57
|
self.config_path = config_path
|
|
60
58
|
self.port = port
|
|
61
59
|
self.server_socket: Optional[socket.socket] = None
|
|
62
60
|
self.is_running = False
|
|
63
61
|
self.devices: List[ConsonModuleConfig] = []
|
|
64
|
-
self.device_services: Dict[
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
73
72
|
|
|
74
73
|
# Set up logging
|
|
75
74
|
self.logger = logging.getLogger(__name__)
|
|
@@ -105,44 +104,14 @@ class ServerService:
|
|
|
105
104
|
serial_number = module.serial_number
|
|
106
105
|
|
|
107
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
|
+
)
|
|
108
111
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
serial_number
|
|
113
|
-
)
|
|
114
|
-
if module_type == "XP24":
|
|
115
|
-
self.device_services[serial_number] = XP24ServerService(
|
|
116
|
-
serial_number
|
|
117
|
-
)
|
|
118
|
-
elif module_type == "XP33":
|
|
119
|
-
self.device_services[serial_number] = XP33ServerService(
|
|
120
|
-
serial_number, "XP33"
|
|
121
|
-
)
|
|
122
|
-
elif module_type == "XP33LR":
|
|
123
|
-
self.device_services[serial_number] = XP33ServerService(
|
|
124
|
-
serial_number, "XP33LR"
|
|
125
|
-
)
|
|
126
|
-
elif module_type == "XP33LED":
|
|
127
|
-
self.device_services[serial_number] = XP33ServerService(
|
|
128
|
-
serial_number, "XP33LED"
|
|
129
|
-
)
|
|
130
|
-
elif module_type == "XP20":
|
|
131
|
-
self.device_services[serial_number] = XP20ServerService(
|
|
132
|
-
serial_number
|
|
133
|
-
)
|
|
134
|
-
elif module_type == "XP130":
|
|
135
|
-
self.device_services[serial_number] = XP130ServerService(
|
|
136
|
-
serial_number
|
|
137
|
-
)
|
|
138
|
-
elif module_type == "XP230":
|
|
139
|
-
self.device_services[serial_number] = XP230ServerService(
|
|
140
|
-
serial_number
|
|
141
|
-
)
|
|
142
|
-
else:
|
|
143
|
-
self.logger.warning(
|
|
144
|
-
f"Unknown device type '{module_type}' for serial {serial_number}"
|
|
145
|
-
)
|
|
112
|
+
except ValueError as e:
|
|
113
|
+
# Factory raises ValueError for unknown device types
|
|
114
|
+
self.logger.warning(str(e))
|
|
146
115
|
|
|
147
116
|
except Exception as e:
|
|
148
117
|
self.logger.error(
|
|
@@ -167,6 +136,8 @@ class ServerService:
|
|
|
167
136
|
self.server_socket.bind(("0.0.0.0", self.port))
|
|
168
137
|
self.server_socket.listen(1) # Accept single connection as per spec
|
|
169
138
|
|
|
139
|
+
self._start_device_collector_thread()
|
|
140
|
+
|
|
170
141
|
self.is_running = True
|
|
171
142
|
self.logger.info(f"Conbus emulator server started on port {self.port}")
|
|
172
143
|
self.logger.info(
|
|
@@ -221,17 +192,44 @@ class ServerService:
|
|
|
221
192
|
) -> None:
|
|
222
193
|
"""Handle individual client connection."""
|
|
223
194
|
try:
|
|
195
|
+
|
|
196
|
+
idle_timeout = 300
|
|
197
|
+
rcv_timeout = 10
|
|
198
|
+
|
|
224
199
|
# Set timeout for idle connections (30 seconds as per spec)
|
|
225
|
-
client_socket.settimeout(
|
|
200
|
+
client_socket.settimeout(rcv_timeout)
|
|
201
|
+
timeout = idle_timeout / rcv_timeout
|
|
226
202
|
|
|
227
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
|
+
|
|
228
211
|
# Receive data from client
|
|
229
|
-
data
|
|
212
|
+
self.logger.debug(f"Receiving data {client_address}")
|
|
213
|
+
data = None
|
|
214
|
+
try:
|
|
215
|
+
data = client_socket.recv(1024)
|
|
216
|
+
except socket.timeout:
|
|
217
|
+
self.logger.debug(
|
|
218
|
+
f"Timeout receiving data {client_address} ({timeout})"
|
|
219
|
+
)
|
|
220
|
+
finally:
|
|
221
|
+
timeout -= 1
|
|
222
|
+
|
|
230
223
|
if not data:
|
|
231
|
-
|
|
224
|
+
if timeout <= 0:
|
|
225
|
+
break
|
|
226
|
+
continue
|
|
227
|
+
|
|
228
|
+
# reset timeout on receiving data
|
|
229
|
+
timeout = idle_timeout / rcv_timeout
|
|
232
230
|
|
|
233
231
|
message = data.decode("latin-1").strip()
|
|
234
|
-
self.logger.
|
|
232
|
+
self.logger.debug(f"Received from {client_address}: {message}")
|
|
235
233
|
|
|
236
234
|
# Process request (discover or data request)
|
|
237
235
|
responses = self._process_request(message)
|
|
@@ -239,10 +237,10 @@ class ServerService:
|
|
|
239
237
|
# Send responses
|
|
240
238
|
for response in responses:
|
|
241
239
|
client_socket.send(response.encode("latin-1"))
|
|
242
|
-
self.logger.
|
|
240
|
+
self.logger.debug(f"Sent to {client_address}: {response[:-1]}")
|
|
243
241
|
|
|
244
242
|
except socket.timeout:
|
|
245
|
-
self.logger.
|
|
243
|
+
self.logger.debug(f"Client {client_address} timed out")
|
|
246
244
|
except Exception as e:
|
|
247
245
|
self.logger.error(f"Error handling client {client_address}: {e}")
|
|
248
246
|
finally:
|
|
@@ -390,3 +388,48 @@ class ServerService:
|
|
|
390
388
|
self.logger.info(
|
|
391
389
|
f"Configuration reloaded: {len(self.devices)} devices, {len(self.device_services)} services"
|
|
392
390
|
)
|
|
391
|
+
|
|
392
|
+
def _start_device_collector_thread(self) -> None:
|
|
393
|
+
"""Start device buffer collector thread."""
|
|
394
|
+
if self.collector_thread and self.collector_thread.is_alive():
|
|
395
|
+
self.logger.debug("Collector thread already running")
|
|
396
|
+
return
|
|
397
|
+
|
|
398
|
+
# Start background thread to send storm telegrams
|
|
399
|
+
self.collector_thread = threading.Thread(
|
|
400
|
+
target=self._device_collector_thread, daemon=True, name="DeviceCollector"
|
|
401
|
+
)
|
|
402
|
+
self.collector_thread.start()
|
|
403
|
+
self.logger.info("Collector thread started")
|
|
404
|
+
|
|
405
|
+
def _stop_device_collector_thread(self) -> None:
|
|
406
|
+
"""Stop device buffer collector thread."""
|
|
407
|
+
if not self.collector_thread or not self.collector_thread.is_alive():
|
|
408
|
+
self.logger.debug("Collector thread not running")
|
|
409
|
+
return
|
|
410
|
+
|
|
411
|
+
self.logger.info(f"Stopping collector thread: {self.collector_thread.name}")
|
|
412
|
+
|
|
413
|
+
# Wait for thread to finish (with timeout)
|
|
414
|
+
if self.collector_thread and self.collector_thread.is_alive():
|
|
415
|
+
self.collector_thread.join(timeout=1.0)
|
|
416
|
+
|
|
417
|
+
self.logger.info("Collector stopped.")
|
|
418
|
+
|
|
419
|
+
def _device_collector_thread(self) -> None:
|
|
420
|
+
"""Device buffer collector thread."""
|
|
421
|
+
self.logger.info("Collector thread starting")
|
|
422
|
+
|
|
423
|
+
while True:
|
|
424
|
+
self.logger.debug(
|
|
425
|
+
f"Collector thread collecting ({len(self.collector_buffer)})"
|
|
426
|
+
)
|
|
427
|
+
collected = 0
|
|
428
|
+
for device_service in self.device_services.values():
|
|
429
|
+
telegram_buffer = device_service.collect_telegram_buffer()
|
|
430
|
+
self.collector_buffer.extend(telegram_buffer)
|
|
431
|
+
collected += len(telegram_buffer)
|
|
432
|
+
|
|
433
|
+
# Wait a bit before checking again
|
|
434
|
+
self.logger.debug(f"Collector thread collected ({collected})")
|
|
435
|
+
self.collector_stop_event.wait(timeout=1)
|
|
@@ -5,9 +5,10 @@ including response generation and device configuration handling.
|
|
|
5
5
|
XP130 is an Ethernet/TCPIP interface module.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from typing import Dict
|
|
8
|
+
from typing import Dict, Optional
|
|
9
9
|
|
|
10
10
|
from xp.models import ModuleTypeCode
|
|
11
|
+
from xp.services.actiontable.msactiontable_serializer import MsActionTableSerializer
|
|
11
12
|
from xp.services.server.base_server_service import BaseServerService
|
|
12
13
|
|
|
13
14
|
|
|
@@ -25,11 +26,18 @@ class XP130ServerService(BaseServerService):
|
|
|
25
26
|
and implements XP130 telegram format for Ethernet/TCPIP interface module.
|
|
26
27
|
"""
|
|
27
28
|
|
|
28
|
-
def __init__(
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
serial_number: str,
|
|
32
|
+
_variant: str = "",
|
|
33
|
+
_msactiontable_serializer: Optional[MsActionTableSerializer] = None,
|
|
34
|
+
):
|
|
29
35
|
"""Initialize XP130 server service.
|
|
30
36
|
|
|
31
37
|
Args:
|
|
32
38
|
serial_number: The device serial number.
|
|
39
|
+
_variant: Reserved parameter for consistency (unused).
|
|
40
|
+
_msactiontable_serializer: Generic MsActionTable serializer (unused).
|
|
33
41
|
"""
|
|
34
42
|
super().__init__(serial_number)
|
|
35
43
|
self.device_type = "XP130"
|
|
@@ -4,9 +4,13 @@ This service provides XP20-specific device emulation functionality,
|
|
|
4
4
|
including response generation and device configuration handling.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from typing import Dict
|
|
7
|
+
from typing import Dict, Optional
|
|
8
8
|
|
|
9
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
|
+
)
|
|
10
14
|
from xp.services.server.base_server_service import BaseServerService
|
|
11
15
|
|
|
12
16
|
|
|
@@ -24,17 +28,55 @@ class XP20ServerService(BaseServerService):
|
|
|
24
28
|
and implements XP20 telegram format.
|
|
25
29
|
"""
|
|
26
30
|
|
|
27
|
-
def __init__(
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
serial_number: str,
|
|
34
|
+
_variant: str = "",
|
|
35
|
+
msactiontable_serializer: Optional[Xp20MsActionTableSerializer] = None,
|
|
36
|
+
):
|
|
28
37
|
"""Initialize XP20 server service.
|
|
29
38
|
|
|
30
39
|
Args:
|
|
31
40
|
serial_number: The device serial number.
|
|
41
|
+
_variant: Reserved parameter for consistency (unused).
|
|
42
|
+
msactiontable_serializer: MsActionTable serializer (injected via DI).
|
|
32
43
|
"""
|
|
33
44
|
super().__init__(serial_number)
|
|
34
45
|
self.device_type = "XP20"
|
|
35
46
|
self.module_type_code = ModuleTypeCode.XP20 # XP20 module type from registry
|
|
36
47
|
self.firmware_version = "XP20_V0.01.05"
|
|
37
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
|
+
|
|
38
80
|
def get_device_info(self) -> Dict:
|
|
39
81
|
"""Get XP20 device information.
|
|
40
82
|
|
|
@@ -4,9 +4,10 @@ This service provides XP230-specific device emulation functionality,
|
|
|
4
4
|
including response generation and device configuration handling.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from typing import Dict
|
|
7
|
+
from typing import Dict, Optional
|
|
8
8
|
|
|
9
9
|
from xp.models import ModuleTypeCode
|
|
10
|
+
from xp.services.actiontable.msactiontable_serializer import MsActionTableSerializer
|
|
10
11
|
from xp.services.server.base_server_service import BaseServerService
|
|
11
12
|
|
|
12
13
|
|
|
@@ -24,11 +25,18 @@ class XP230ServerService(BaseServerService):
|
|
|
24
25
|
and implements XP230 telegram format.
|
|
25
26
|
"""
|
|
26
27
|
|
|
27
|
-
def __init__(
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
serial_number: str,
|
|
31
|
+
_variant: str = "",
|
|
32
|
+
_msactiontable_serializer: Optional[MsActionTableSerializer] = None,
|
|
33
|
+
):
|
|
28
34
|
"""Initialize XP230 server service.
|
|
29
35
|
|
|
30
36
|
Args:
|
|
31
37
|
serial_number: The device serial number.
|
|
38
|
+
_variant: Reserved parameter for consistency (unused).
|
|
39
|
+
_msactiontable_serializer: Generic MsActionTable serializer (unused).
|
|
32
40
|
"""
|
|
33
41
|
super().__init__(serial_number)
|
|
34
42
|
self.device_type = "XP230"
|
|
@@ -7,9 +7,15 @@ including response generation and device configuration handling.
|
|
|
7
7
|
from typing import Dict, Optional
|
|
8
8
|
|
|
9
9
|
from xp.models import ModuleTypeCode
|
|
10
|
+
from xp.models.actiontable.msactiontable_xp24 import InputAction, Xp24MsActionTable
|
|
10
11
|
from xp.models.telegram.datapoint_type import DataPointType
|
|
12
|
+
from xp.models.telegram.input_action_type import InputActionType
|
|
11
13
|
from xp.models.telegram.system_function import SystemFunction
|
|
12
14
|
from xp.models.telegram.system_telegram import SystemTelegram
|
|
15
|
+
from xp.models.telegram.timeparam_type import TimeParam
|
|
16
|
+
from xp.services.actiontable.msactiontable_xp24_serializer import (
|
|
17
|
+
Xp24MsActionTableSerializer,
|
|
18
|
+
)
|
|
13
19
|
from xp.services.server.base_server_service import BaseServerService
|
|
14
20
|
|
|
15
21
|
|
|
@@ -37,11 +43,18 @@ class XP24ServerService(BaseServerService):
|
|
|
37
43
|
and implements XP24 telegram format.
|
|
38
44
|
"""
|
|
39
45
|
|
|
40
|
-
def __init__(
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
serial_number: str,
|
|
49
|
+
_variant: str = "",
|
|
50
|
+
msactiontable_serializer: Optional[Xp24MsActionTableSerializer] = None,
|
|
51
|
+
):
|
|
41
52
|
"""Initialize XP24 server service.
|
|
42
53
|
|
|
43
54
|
Args:
|
|
44
55
|
serial_number: The device serial number.
|
|
56
|
+
_variant: Reserved parameter for consistency (unused).
|
|
57
|
+
msactiontable_serializer: MsActionTable serializer (injected via DI).
|
|
45
58
|
"""
|
|
46
59
|
super().__init__(serial_number)
|
|
47
60
|
self.device_type = "XP24"
|
|
@@ -53,6 +66,12 @@ class XP24ServerService(BaseServerService):
|
|
|
53
66
|
self.output_2: XP24Output = XP24Output()
|
|
54
67
|
self.output_3: XP24Output = XP24Output()
|
|
55
68
|
|
|
69
|
+
# MsActionTable support
|
|
70
|
+
self.msactiontable_serializer = (
|
|
71
|
+
msactiontable_serializer or Xp24MsActionTableSerializer()
|
|
72
|
+
)
|
|
73
|
+
self.msactiontable = self._get_default_msactiontable()
|
|
74
|
+
|
|
56
75
|
def _handle_device_specific_action_request(
|
|
57
76
|
self, request: SystemTelegram
|
|
58
77
|
) -> Optional[str]:
|
|
@@ -175,6 +194,40 @@ class XP24ServerService(BaseServerService):
|
|
|
175
194
|
f"{1 if self.output_3.state else 0}"
|
|
176
195
|
)
|
|
177
196
|
|
|
197
|
+
def _get_msactiontable_serializer(self) -> Optional[Xp24MsActionTableSerializer]:
|
|
198
|
+
"""Get the MsActionTable serializer for XP24.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
The XP24 MsActionTable serializer instance.
|
|
202
|
+
"""
|
|
203
|
+
return self.msactiontable_serializer
|
|
204
|
+
|
|
205
|
+
def _get_msactiontable(self) -> Optional[Xp24MsActionTable]:
|
|
206
|
+
"""Get the MsActionTable for XP24.
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
The XP24 MsActionTable instance.
|
|
210
|
+
"""
|
|
211
|
+
return self.msactiontable
|
|
212
|
+
|
|
213
|
+
def _get_default_msactiontable(self) -> Xp24MsActionTable:
|
|
214
|
+
"""Generate default MsActionTable configuration.
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
Default XP24 MsActionTable with all inputs set to VOID.
|
|
218
|
+
"""
|
|
219
|
+
return Xp24MsActionTable(
|
|
220
|
+
input1_action=InputAction(type=InputActionType.VOID, param=TimeParam.NONE),
|
|
221
|
+
input2_action=InputAction(type=InputActionType.VOID, param=TimeParam.NONE),
|
|
222
|
+
input3_action=InputAction(type=InputActionType.VOID, param=TimeParam.NONE),
|
|
223
|
+
input4_action=InputAction(type=InputActionType.VOID, param=TimeParam.NONE),
|
|
224
|
+
mutex12=False,
|
|
225
|
+
mutex34=False,
|
|
226
|
+
curtain12=False,
|
|
227
|
+
curtain34=False,
|
|
228
|
+
mutual_deadtime=12, # MS300
|
|
229
|
+
)
|
|
230
|
+
|
|
178
231
|
def get_device_info(self) -> Dict:
|
|
179
232
|
"""Get XP24 device information.
|
|
180
233
|
|
|
@@ -5,12 +5,18 @@ including response generation and device configuration handling for
|
|
|
5
5
|
3-channel light dimmer modules.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
+
import socket
|
|
9
|
+
import threading
|
|
8
10
|
from typing import Dict, Optional
|
|
9
11
|
|
|
10
12
|
from xp.models import ModuleTypeCode
|
|
13
|
+
from xp.models.actiontable.msactiontable_xp33 import Xp33MsActionTable
|
|
11
14
|
from xp.models.telegram.datapoint_type import DataPointType
|
|
12
15
|
from xp.models.telegram.system_function import SystemFunction
|
|
13
16
|
from xp.models.telegram.system_telegram import SystemTelegram
|
|
17
|
+
from xp.services.actiontable.msactiontable_xp33_serializer import (
|
|
18
|
+
Xp33MsActionTableSerializer,
|
|
19
|
+
)
|
|
14
20
|
from xp.services.server.base_server_service import BaseServerService
|
|
15
21
|
|
|
16
22
|
|
|
@@ -28,12 +34,18 @@ class XP33ServerService(BaseServerService):
|
|
|
28
34
|
and implements XP33 telegram format for 3-channel dimmer modules.
|
|
29
35
|
"""
|
|
30
36
|
|
|
31
|
-
def __init__(
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
serial_number: str,
|
|
40
|
+
variant: str = "XP33LR",
|
|
41
|
+
msactiontable_serializer: Optional[Xp33MsActionTableSerializer] = None,
|
|
42
|
+
):
|
|
32
43
|
"""Initialize XP33 server service.
|
|
33
44
|
|
|
34
45
|
Args:
|
|
35
46
|
serial_number: The device serial number.
|
|
36
47
|
variant: Device variant (XP33, XP33LR, or XP33LED).
|
|
48
|
+
msactiontable_serializer: MsActionTable serializer (injected via DI).
|
|
37
49
|
"""
|
|
38
50
|
super().__init__(serial_number)
|
|
39
51
|
self.variant = variant # XP33 or XP33LR or XP33LED
|
|
@@ -72,6 +84,23 @@ class XP33ServerService(BaseServerService):
|
|
|
72
84
|
4: [0, 0, 0], # Scene 4: Off
|
|
73
85
|
}
|
|
74
86
|
|
|
87
|
+
# Storm mode state (XP33 Storm Simulator)
|
|
88
|
+
self.storm_mode = False # Track if device is in storm mode
|
|
89
|
+
self.last_response: Optional[str] = None # Cache last response for storm replay
|
|
90
|
+
self.storm_thread: Optional[threading.Thread] = (
|
|
91
|
+
None # Background thread for storm
|
|
92
|
+
)
|
|
93
|
+
self.storm_stop_event = threading.Event() # Event to stop storm thread
|
|
94
|
+
self.client_sockets: set[socket.socket] = set() # All active client sockets
|
|
95
|
+
self.client_sockets_lock = threading.Lock() # Lock for socket set
|
|
96
|
+
self.storm_packets_sent = 0 # Counter for packets sent during storm
|
|
97
|
+
|
|
98
|
+
# MsActionTable support
|
|
99
|
+
self.msactiontable_serializer = (
|
|
100
|
+
msactiontable_serializer or Xp33MsActionTableSerializer()
|
|
101
|
+
)
|
|
102
|
+
self.msactiontable = self._get_default_msactiontable()
|
|
103
|
+
|
|
75
104
|
def _handle_device_specific_action_request(
|
|
76
105
|
self, request: SystemTelegram
|
|
77
106
|
) -> Optional[str]:
|
|
@@ -160,11 +189,32 @@ class XP33ServerService(BaseServerService):
|
|
|
160
189
|
def _handle_device_specific_data_request(
|
|
161
190
|
self, request: SystemTelegram
|
|
162
191
|
) -> Optional[str]:
|
|
163
|
-
"""Handle XP33-specific data requests."""
|
|
192
|
+
"""Handle XP33-specific data requests with storm mode support."""
|
|
164
193
|
if not request.datapoint_type:
|
|
194
|
+
# Check for D99 storm trigger (not in DataPointType enum)
|
|
195
|
+
if request.data and request.data.startswith("99"):
|
|
196
|
+
return self._trigger_storm_mode()
|
|
165
197
|
return None
|
|
166
198
|
|
|
167
199
|
datapoint_type = request.datapoint_type
|
|
200
|
+
|
|
201
|
+
# Storm mode handling
|
|
202
|
+
if datapoint_type == DataPointType.MODULE_ERROR_CODE:
|
|
203
|
+
if self.storm_mode:
|
|
204
|
+
# MODULE_ERROR_CODE query stops storm
|
|
205
|
+
return self._exit_storm_mode()
|
|
206
|
+
else:
|
|
207
|
+
# Normal operation - return error code 00
|
|
208
|
+
return self._build_error_code_response("00")
|
|
209
|
+
|
|
210
|
+
# If in storm mode and not MODULE_ERROR_CODE query, ignore (background thread is sending)
|
|
211
|
+
if self.storm_mode:
|
|
212
|
+
self.logger.debug(
|
|
213
|
+
f"Ignoring query during storm mode for device {self.serial_number}"
|
|
214
|
+
)
|
|
215
|
+
return None # Background thread is sending storm telegrams
|
|
216
|
+
|
|
217
|
+
# Normal data request handling
|
|
168
218
|
handler = {
|
|
169
219
|
DataPointType.MODULE_OUTPUT_STATE: self._handle_read_module_output_state,
|
|
170
220
|
DataPointType.MODULE_STATE: self._handle_read_module_state,
|
|
@@ -183,6 +233,9 @@ class XP33ServerService(BaseServerService):
|
|
|
183
233
|
)
|
|
184
234
|
telegram = self._build_response_telegram(data_part)
|
|
185
235
|
|
|
236
|
+
# Cache response for potential storm replay
|
|
237
|
+
self.last_response = telegram
|
|
238
|
+
|
|
186
239
|
self.logger.debug(
|
|
187
240
|
f"Generated {self.device_type} module type response: {telegram}"
|
|
188
241
|
)
|
|
@@ -230,6 +283,157 @@ class XP33ServerService(BaseServerService):
|
|
|
230
283
|
]
|
|
231
284
|
return ",".join(levels)
|
|
232
285
|
|
|
286
|
+
def _trigger_storm_mode(self) -> Optional[str]:
|
|
287
|
+
"""Trigger storm mode via D99 query.
|
|
288
|
+
|
|
289
|
+
Starts a background thread that sends 2 packets per second.
|
|
290
|
+
If storm is already active, this is a no-op.
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
None (no response - storm mode activated).
|
|
294
|
+
"""
|
|
295
|
+
# If storm already active, just log and continue
|
|
296
|
+
if self.storm_mode and self.storm_thread and self.storm_thread.is_alive():
|
|
297
|
+
self.logger.debug(
|
|
298
|
+
f"Storm already active for device {self.serial_number}, "
|
|
299
|
+
f"sent {self.storm_packets_sent}/200 packets"
|
|
300
|
+
)
|
|
301
|
+
return None
|
|
302
|
+
|
|
303
|
+
if not self.last_response:
|
|
304
|
+
self.logger.warning(
|
|
305
|
+
f"Cannot trigger storm for device {self.serial_number}: "
|
|
306
|
+
f"no cached response"
|
|
307
|
+
)
|
|
308
|
+
return None
|
|
309
|
+
|
|
310
|
+
self.storm_mode = True
|
|
311
|
+
self.storm_packets_sent = 0
|
|
312
|
+
self.storm_stop_event.clear()
|
|
313
|
+
|
|
314
|
+
# Start background thread to send storm telegrams
|
|
315
|
+
self.storm_thread = threading.Thread(
|
|
316
|
+
target=self._storm_sender_thread,
|
|
317
|
+
daemon=True,
|
|
318
|
+
name=f"Storm-{self.serial_number}",
|
|
319
|
+
)
|
|
320
|
+
self.storm_thread.start()
|
|
321
|
+
|
|
322
|
+
self.logger.info(
|
|
323
|
+
f"Storm triggered via D99 query for device {self.serial_number}"
|
|
324
|
+
)
|
|
325
|
+
return None # No response when entering storm mode
|
|
326
|
+
|
|
327
|
+
def _exit_storm_mode(self) -> str:
|
|
328
|
+
"""Exit storm mode and return error code FE.
|
|
329
|
+
|
|
330
|
+
Stops the background storm thread and returns error code.
|
|
331
|
+
|
|
332
|
+
Returns:
|
|
333
|
+
MODULE_ERROR_CODE response with error code FE (buffer overflow).
|
|
334
|
+
"""
|
|
335
|
+
self.logger.info(
|
|
336
|
+
f"MODULE_ERROR_CODE query received, stopping storm for device {self.serial_number}"
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
# Signal the storm thread to stop
|
|
340
|
+
self.storm_stop_event.set()
|
|
341
|
+
self.storm_mode = False
|
|
342
|
+
|
|
343
|
+
# Wait for thread to finish (with timeout)
|
|
344
|
+
if self.storm_thread and self.storm_thread.is_alive():
|
|
345
|
+
self.storm_thread.join(timeout=1.0)
|
|
346
|
+
|
|
347
|
+
self.logger.info(
|
|
348
|
+
f"Storm stopped after {self.storm_packets_sent} packets for device {self.serial_number}"
|
|
349
|
+
)
|
|
350
|
+
self.logger.info(
|
|
351
|
+
f"Storm stopped, returning to normal operation for device {self.serial_number}"
|
|
352
|
+
)
|
|
353
|
+
return self._build_error_code_response("FE")
|
|
354
|
+
|
|
355
|
+
def _storm_sender_thread(self) -> None:
|
|
356
|
+
"""Background thread that sends storm telegrams continuously.
|
|
357
|
+
|
|
358
|
+
Sends 2 packets per second (500ms delay) until:
|
|
359
|
+
- 200 packets have been sent, or
|
|
360
|
+
- Storm mode is stopped via stop event
|
|
361
|
+
|
|
362
|
+
The storm persists across socket disconnections. If the client disconnects
|
|
363
|
+
and reconnects, the storm will continue on the new connection.
|
|
364
|
+
"""
|
|
365
|
+
if not self.last_response:
|
|
366
|
+
self.logger.error(
|
|
367
|
+
f"Storm thread started but missing cached response for {self.serial_number}"
|
|
368
|
+
)
|
|
369
|
+
self.storm_mode = False
|
|
370
|
+
return
|
|
371
|
+
|
|
372
|
+
self.logger.info(
|
|
373
|
+
f"Storm thread started, sending 200 duplicate telegrams at 2 packets/sec for device {self.serial_number}"
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
# Type narrowing for mypy
|
|
377
|
+
cached_response: str = self.last_response
|
|
378
|
+
max_packets = 200
|
|
379
|
+
packets_per_second = 2
|
|
380
|
+
delay_between_packets = 1.0 / packets_per_second # 0.5 seconds
|
|
381
|
+
|
|
382
|
+
try:
|
|
383
|
+
while (
|
|
384
|
+
self.storm_packets_sent < max_packets
|
|
385
|
+
and not self.storm_stop_event.is_set()
|
|
386
|
+
):
|
|
387
|
+
# Wait for a valid socket (client may have disconnected and reconnected)
|
|
388
|
+
self.add_telegram_buffer(cached_response)
|
|
389
|
+
self.storm_packets_sent += 1
|
|
390
|
+
self.logger.debug(
|
|
391
|
+
f"Storm packet {self.storm_packets_sent}/{max_packets} sent for {self.serial_number}"
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
# Wait before sending next packet (0.5 seconds for 2 packets/sec)
|
|
395
|
+
if self.storm_packets_sent < max_packets:
|
|
396
|
+
self.storm_stop_event.wait(timeout=delay_between_packets)
|
|
397
|
+
|
|
398
|
+
# Log completion status
|
|
399
|
+
if self.storm_packets_sent >= max_packets:
|
|
400
|
+
self.logger.info(
|
|
401
|
+
f"Storm completed: sent all {self.storm_packets_sent} packets for {self.serial_number}"
|
|
402
|
+
)
|
|
403
|
+
elif self.storm_stop_event.is_set():
|
|
404
|
+
self.logger.info(
|
|
405
|
+
f"Storm stopped by error code query: sent {self.storm_packets_sent} packets for {self.serial_number}"
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
# Clean up storm mode
|
|
409
|
+
self.storm_mode = False
|
|
410
|
+
|
|
411
|
+
except Exception as e:
|
|
412
|
+
self.logger.error(
|
|
413
|
+
f"Unexpected error in storm thread for {self.serial_number}: {e}"
|
|
414
|
+
)
|
|
415
|
+
self.storm_mode = False
|
|
416
|
+
|
|
417
|
+
def _build_error_code_response(self, error_code: str) -> str:
|
|
418
|
+
"""Build MODULE_ERROR_CODE response telegram.
|
|
419
|
+
|
|
420
|
+
Args:
|
|
421
|
+
error_code: Error code (00 = normal, FE = buffer overflow).
|
|
422
|
+
|
|
423
|
+
Returns:
|
|
424
|
+
The complete MODULE_ERROR_CODE response telegram.
|
|
425
|
+
"""
|
|
426
|
+
data_part = (
|
|
427
|
+
f"R{self.serial_number}"
|
|
428
|
+
f"F02D{DataPointType.MODULE_ERROR_CODE.value}"
|
|
429
|
+
f"{error_code}"
|
|
430
|
+
)
|
|
431
|
+
telegram = self._build_response_telegram(data_part)
|
|
432
|
+
self.logger.debug(
|
|
433
|
+
f"Generated {self.device_type} error code response: {telegram}"
|
|
434
|
+
)
|
|
435
|
+
return telegram
|
|
436
|
+
|
|
233
437
|
def set_channel_dimming(self, channel: int, level: int) -> bool:
|
|
234
438
|
"""Set individual channel dimming level.
|
|
235
439
|
|
|
@@ -261,6 +465,31 @@ class XP33ServerService(BaseServerService):
|
|
|
261
465
|
return True
|
|
262
466
|
return False
|
|
263
467
|
|
|
468
|
+
def _get_msactiontable_serializer(self) -> Optional[Xp33MsActionTableSerializer]:
|
|
469
|
+
"""Get the MsActionTable serializer for XP33.
|
|
470
|
+
|
|
471
|
+
Returns:
|
|
472
|
+
The XP33 MsActionTable serializer instance.
|
|
473
|
+
"""
|
|
474
|
+
return self.msactiontable_serializer
|
|
475
|
+
|
|
476
|
+
def _get_msactiontable(self) -> Optional[Xp33MsActionTable]:
|
|
477
|
+
"""Get the MsActionTable for XP33.
|
|
478
|
+
|
|
479
|
+
Returns:
|
|
480
|
+
The XP33 MsActionTable instance.
|
|
481
|
+
"""
|
|
482
|
+
return self.msactiontable
|
|
483
|
+
|
|
484
|
+
def _get_default_msactiontable(self) -> Xp33MsActionTable:
|
|
485
|
+
"""Generate default MsActionTable configuration.
|
|
486
|
+
|
|
487
|
+
Returns:
|
|
488
|
+
Default XP33 MsActionTable with all outputs at 0-100% range, no scenes configured.
|
|
489
|
+
"""
|
|
490
|
+
# All outputs at 0-100% range, no scenes configured
|
|
491
|
+
return Xp33MsActionTable()
|
|
492
|
+
|
|
264
493
|
def get_device_info(self) -> Dict:
|
|
265
494
|
"""Get XP33 device information.
|
|
266
495
|
|
xp/utils/dependencies.py
CHANGED
|
@@ -9,18 +9,19 @@ from twisted.internet.posixbase import PosixReactorBase
|
|
|
9
9
|
from xp.models import ConbusClientConfig
|
|
10
10
|
from xp.models.homekit.homekit_config import HomekitConfig
|
|
11
11
|
from xp.models.homekit.homekit_conson_config import ConsonModuleListConfig
|
|
12
|
-
from xp.services.
|
|
13
|
-
from xp.services.
|
|
14
|
-
from xp.services.
|
|
15
|
-
from xp.services.conbus.actiontable.msactiontable_xp20_serializer import (
|
|
12
|
+
from xp.services.actiontable.actiontable_serializer import ActionTableSerializer
|
|
13
|
+
from xp.services.actiontable.msactiontable_serializer import MsActionTableSerializer
|
|
14
|
+
from xp.services.actiontable.msactiontable_xp20_serializer import (
|
|
16
15
|
Xp20MsActionTableSerializer,
|
|
17
16
|
)
|
|
18
|
-
from xp.services.
|
|
17
|
+
from xp.services.actiontable.msactiontable_xp24_serializer import (
|
|
19
18
|
Xp24MsActionTableSerializer,
|
|
20
19
|
)
|
|
21
|
-
from xp.services.
|
|
20
|
+
from xp.services.actiontable.msactiontable_xp33_serializer import (
|
|
22
21
|
Xp33MsActionTableSerializer,
|
|
23
22
|
)
|
|
23
|
+
from xp.services.conbus.actiontable.actiontable_service import ActionTableService
|
|
24
|
+
from xp.services.conbus.actiontable.msactiontable_service import MsActionTableService
|
|
24
25
|
from xp.services.conbus.conbus_blink_all_service import ConbusBlinkAllService
|
|
25
26
|
from xp.services.conbus.conbus_blink_service import ConbusBlinkService
|
|
26
27
|
from xp.services.conbus.conbus_custom_service import ConbusCustomService
|
|
@@ -49,6 +50,7 @@ from xp.services.module_type_service import ModuleTypeService
|
|
|
49
50
|
from xp.services.protocol.protocol_factory import TelegramFactory
|
|
50
51
|
from xp.services.protocol.telegram_protocol import TelegramProtocol
|
|
51
52
|
from xp.services.reverse_proxy_service import ReverseProxyService
|
|
53
|
+
from xp.services.server.device_service_factory import DeviceServiceFactory
|
|
52
54
|
from xp.services.server.server_service import ServerService
|
|
53
55
|
from xp.services.telegram.telegram_blink_service import TelegramBlinkService
|
|
54
56
|
from xp.services.telegram.telegram_discover_service import TelegramDiscoverService
|
|
@@ -324,12 +326,25 @@ class ServiceContainer:
|
|
|
324
326
|
# Module type services layer
|
|
325
327
|
self.container.register(ModuleTypeService, scope=punq.Scope.singleton)
|
|
326
328
|
|
|
329
|
+
# Device service factory
|
|
330
|
+
self.container.register(
|
|
331
|
+
DeviceServiceFactory,
|
|
332
|
+
factory=lambda: DeviceServiceFactory(
|
|
333
|
+
xp20ms_serializer=self.container.resolve(Xp20MsActionTableSerializer),
|
|
334
|
+
xp24ms_serializer=self.container.resolve(Xp24MsActionTableSerializer),
|
|
335
|
+
xp33ms_serializer=self.container.resolve(Xp33MsActionTableSerializer),
|
|
336
|
+
ms_serializer=self.container.resolve(MsActionTableSerializer),
|
|
337
|
+
),
|
|
338
|
+
scope=punq.Scope.singleton,
|
|
339
|
+
)
|
|
340
|
+
|
|
327
341
|
# Server services layer
|
|
328
342
|
self.container.register(
|
|
329
343
|
ServerService,
|
|
330
344
|
factory=lambda: ServerService(
|
|
331
345
|
telegram_service=self.container.resolve(TelegramService),
|
|
332
346
|
discover_service=self.container.resolve(TelegramDiscoverService),
|
|
347
|
+
device_factory=self.container.resolve(DeviceServiceFactory),
|
|
333
348
|
config_path="server.yml",
|
|
334
349
|
port=self._server_port,
|
|
335
350
|
),
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|