conson-xp 1.3.0__py3-none-any.whl → 1.4.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.3.0.dist-info → conson_xp-1.4.0.dist-info}/METADATA +1 -1
- {conson_xp-1.3.0.dist-info → conson_xp-1.4.0.dist-info}/RECORD +19 -19
- xp/__init__.py +1 -1
- xp/models/conbus/conbus_discover.py +19 -3
- xp/models/telegram/system_telegram.py +4 -4
- xp/services/conbus/conbus_discover_service.py +120 -2
- xp/services/conbus/conbus_scan_service.py +1 -1
- xp/services/protocol/telegram_protocol.py +4 -4
- xp/services/server/base_server_service.py +9 -4
- xp/services/server/cp20_server_service.py +2 -1
- xp/services/server/server_service.py +75 -4
- xp/services/server/xp130_server_service.py +2 -1
- xp/services/server/xp20_server_service.py +2 -1
- xp/services/server/xp230_server_service.py +2 -1
- xp/services/server/xp24_server_service.py +123 -50
- xp/services/server/xp33_server_service.py +150 -20
- {conson_xp-1.3.0.dist-info → conson_xp-1.4.0.dist-info}/WHEEL +0 -0
- {conson_xp-1.3.0.dist-info → conson_xp-1.4.0.dist-info}/entry_points.txt +0 -0
- {conson_xp-1.3.0.dist-info → conson_xp-1.4.0.dist-info}/licenses/LICENSE +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.4.0.dist-info/METADATA,sha256=gVe0dQdYUIfvh1zZ9TLnHgL1bMgCbwaBQDlbnMTGIig,9274
|
|
2
|
+
conson_xp-1.4.0.dist-info/WHEEL,sha256=9P2ygRxDrTJz3gsagc0Z96ukrxjr-LFBGOgv3AuKlCA,90
|
|
3
|
+
conson_xp-1.4.0.dist-info/entry_points.txt,sha256=1OcdIcDM1hz3ljCXgybaPUh1IOFEwkaTgLIW9u9zGeg,50
|
|
4
|
+
conson_xp-1.4.0.dist-info/licenses/LICENSE,sha256=rxj6woMM-r3YCyGq_UHFtbh7kHTAJgrccH6O-33IDE4,1419
|
|
5
|
+
xp/__init__.py,sha256=K0b80vyQTL1NHu42mglJvDuAouKQOPKpfKPus7iPCEg,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
|
|
@@ -64,7 +64,7 @@ xp/models/conbus/conbus_client_config.py,sha256=fWPmHM-OVUzSASKq667JzP7e9_Qp9ZUy
|
|
|
64
64
|
xp/models/conbus/conbus_connection_status.py,sha256=iGbmtBaAMwV6UD7XG3H3tnB0fl2MR8rJhpjrLH2KjsE,1097
|
|
65
65
|
xp/models/conbus/conbus_custom.py,sha256=8H2sPR6_LIlksuOvL7-8bPkzAJLR0rpYiiwfYYFVjEo,1965
|
|
66
66
|
xp/models/conbus/conbus_datapoint.py,sha256=4ncR-vB2lRzRBAA30rYn8eguyTxsZoOKrrXtjGmPpWg,3396
|
|
67
|
-
xp/models/conbus/conbus_discover.py,sha256=
|
|
67
|
+
xp/models/conbus/conbus_discover.py,sha256=nxxUEKfEsH1kd0BF8ovMs7zLujRhrq1oL9ZJtysPr5o,2238
|
|
68
68
|
xp/models/conbus/conbus_lightlevel.py,sha256=GQGhzrCBEJROosNHInXIzBy6MD2AskEIMoFEGgZ60-0,1695
|
|
69
69
|
xp/models/conbus/conbus_linknumber.py,sha256=uFzKzfB06oIzZEKCb5X2JEI80JjMPFuYglsT1W1k8j4,1815
|
|
70
70
|
xp/models/conbus/conbus_output.py,sha256=q7QKsD_CWT7YOk-V3otKWD1VM7qThrSLIUOunntMrMc,1953
|
|
@@ -91,7 +91,7 @@ xp/models/telegram/module_type_code.py,sha256=bg8Zi58yKs5DDnEF0bGnZ9vvpqzmIZzd1k
|
|
|
91
91
|
xp/models/telegram/output_telegram.py,sha256=vTSdeAGk7va89pZ8-oh0cna98N3T6if-6UcrstWsN6s,3473
|
|
92
92
|
xp/models/telegram/reply_telegram.py,sha256=oqNwDvaOhFTPuXL0fP9Ca3rbcKepDhRz9kIneKCk6n0,10376
|
|
93
93
|
xp/models/telegram/system_function.py,sha256=Iv9u4sYCPnMcvlpbBrNNxu0NpUOFsi5kPgT2vrelbVw,3266
|
|
94
|
-
xp/models/telegram/system_telegram.py,sha256=
|
|
94
|
+
xp/models/telegram/system_telegram.py,sha256=9FNQ4Mf47mRK7wGrTg2GzziVsrEWCE5ZkZp5kA7K3w0,3218
|
|
95
95
|
xp/models/telegram/telegram.py,sha256=IJUxHX6ftLcET9C1pjvLhUO5Db5JO6W7rUItzdEW30I,842
|
|
96
96
|
xp/models/telegram/telegram_type.py,sha256=GhqKP63oNMyh2tIvCPcsC5RFp4s4JjhmEqCLCC-8XMk,423
|
|
97
97
|
xp/models/telegram/timeparam_type.py,sha256=Ar8xvSfPmOAgR2g2Je0FgvP01SL7bPvZn5_HrVDpmJM,1137
|
|
@@ -110,11 +110,11 @@ xp/services/conbus/conbus_blink_service.py,sha256=x9uM-sLnIEV8wSNsvJgo08E042g-Hh
|
|
|
110
110
|
xp/services/conbus/conbus_custom_service.py,sha256=4aneYdPObiZOGxPFYg5Wr70cl_xFxlQIdJBPQSa0enI,5826
|
|
111
111
|
xp/services/conbus/conbus_datapoint_queryall_service.py,sha256=p9R02cVimhdJILHQ6BoeZj8Hog4oRpqBnMo3t4R8ecY,6816
|
|
112
112
|
xp/services/conbus/conbus_datapoint_service.py,sha256=NsqRQfNsZ4_Pbe7kcMQpUqfhVPH7H148JDWH49ExQ1E,6392
|
|
113
|
-
xp/services/conbus/conbus_discover_service.py,sha256=
|
|
113
|
+
xp/services/conbus/conbus_discover_service.py,sha256=lH6I8YcN7Beo_f-M8XkNZ_5UuNB-x2R9U5xJNTK-QXE,10110
|
|
114
114
|
xp/services/conbus/conbus_output_service.py,sha256=mHFOAPx2zo0TStZ3pokp6v94AQjIamcwZDeg5YH_-eo,7240
|
|
115
115
|
xp/services/conbus/conbus_raw_service.py,sha256=4yZLLTIAOxpgByUTWZXw1ihGa6Xtl98ckj9T7VfprDI,4335
|
|
116
116
|
xp/services/conbus/conbus_receive_service.py,sha256=frXrS0OyKKvYYQTWdma21Kd0BKw5aSuHn3ZXTTqOaj0,3953
|
|
117
|
-
xp/services/conbus/conbus_scan_service.py,sha256=
|
|
117
|
+
xp/services/conbus/conbus_scan_service.py,sha256=tHJ5qaxcNXxAZb2D2F1v6IrzydfxjJOYllM6Txt1eBE,5176
|
|
118
118
|
xp/services/conbus/write_config_service.py,sha256=qe5TwQdVnMCcJ4lCeZLADdUeoDpbEaV3XoZ19a3F_qk,7128
|
|
119
119
|
xp/services/homekit/__init__.py,sha256=xAMKmln_AmEFdOOJGKWYi96seRlKDQpKx3-hm7XbdIo,36
|
|
120
120
|
xp/services/homekit/homekit_cache_service.py,sha256=NdijyH5_iyhsTHBb-OyT8Y2xnNDj8F5MP8neoVQ26hY,11010
|
|
@@ -135,17 +135,17 @@ xp/services/module_type_service.py,sha256=xWhr1EAZMykL5uNWHWdpa5T8yNruGKH43XRTOS
|
|
|
135
135
|
xp/services/protocol/__init__.py,sha256=WuYn2iEcvsOIXnn5HCrU9kD3PjuMX1sIh0ljKISDoJw,720
|
|
136
136
|
xp/services/protocol/conbus_protocol.py,sha256=G39YPMpwhvvhFPYrzNxx6y2Is6DSP2UyCLm4T7RLPVc,10404
|
|
137
137
|
xp/services/protocol/protocol_factory.py,sha256=PmjN9AtW9sxNo3voqUiNgQA-pTvX1RW4XXFlHKfFr5Q,2470
|
|
138
|
-
xp/services/protocol/telegram_protocol.py,sha256=
|
|
138
|
+
xp/services/protocol/telegram_protocol.py,sha256=Ki5DrXsKxiaqLcdP9WWUuhUI7cPu2DfwyZkh-Gv9Lb8,9496
|
|
139
139
|
xp/services/reverse_proxy_service.py,sha256=BUOlcLlTU-R5iuC_96rasug21xo19wK9_4fMQXxc0QM,15061
|
|
140
140
|
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/server_service.py,sha256=
|
|
144
|
-
xp/services/server/xp130_server_service.py,sha256=
|
|
145
|
-
xp/services/server/xp20_server_service.py,sha256=
|
|
146
|
-
xp/services/server/xp230_server_service.py,sha256=
|
|
147
|
-
xp/services/server/xp24_server_service.py,sha256=
|
|
148
|
-
xp/services/server/xp33_server_service.py,sha256=
|
|
141
|
+
xp/services/server/base_server_service.py,sha256=6RmL7bLRqdjEZgIf7lBwwBOdrpQMWhdQnpuF01469M8,8921
|
|
142
|
+
xp/services/server/cp20_server_service.py,sha256=PkdkORQ-aIHtQb-wuAgkRxKcdpNWpvys_p1sXJg0yoI,1679
|
|
143
|
+
xp/services/server/server_service.py,sha256=K25vq6B83n-iwYwEsani5KLULp9DBUBIoAzrYmXmUJ8,14688
|
|
144
|
+
xp/services/server/xp130_server_service.py,sha256=mD3vE-JDR9s_o7zjVCu4cibM8hUbwJ1oxgb_JwtQ2WU,1819
|
|
145
|
+
xp/services/server/xp20_server_service.py,sha256=s9RrqhCZ8xtgEzc8GXTlG81b4LtZLCFy79DhzBLTPjA,1428
|
|
146
|
+
xp/services/server/xp230_server_service.py,sha256=c3kzkA-fEOglrjLISQLbyk_rUdKzwN20hc0qtF9MEAQ,1443
|
|
147
|
+
xp/services/server/xp24_server_service.py,sha256=_QMHe0UgxVlyB0DZmP1KPdjheT1qE8V8-EW55FM58DY,6606
|
|
148
|
+
xp/services/server/xp33_server_service.py,sha256=DneRmJEd7oZGxC-Tj8KqAt54wEQwSUzKuCFMSEf2_bI,11069
|
|
149
149
|
xp/services/telegram/__init__.py,sha256=kv0JgMg13Fp18WgGQpalNRAWwiWbrz18X4kZAP9xpSQ,48
|
|
150
150
|
xp/services/telegram/telegram_blink_service.py,sha256=Xctc9mCSZiiW1YTh8cA-4jlc8fTioS5OxT6ymhSqiYI,4487
|
|
151
151
|
xp/services/telegram/telegram_checksum_service.py,sha256=rp_C5PlraOOIyqZDp9XjBBNZLUeBLdQNNHVpN6D-1v8,4729
|
|
@@ -161,4 +161,4 @@ xp/utils/dependencies.py,sha256=4G7r0m1HY9UV4E0zLS8L-axcNiX2mM-N6OOAU8dVHVM,1774
|
|
|
161
161
|
xp/utils/event_helper.py,sha256=W-A_xmoXlpWZBbJH6qdaN50o3-XrwFsDgvAGMJDiAgo,1001
|
|
162
162
|
xp/utils/serialization.py,sha256=RWHHk86feaB4ZP7rjE4qOWK0900yg2joUBDkP76gfOY,4618
|
|
163
163
|
xp/utils/time_utils.py,sha256=dEyViDlAG9GWU-J3D_YVa-sGma6yiyyMTgN4h2x3PY4,3781
|
|
164
|
-
conson_xp-1.
|
|
164
|
+
conson_xp-1.4.0.dist-info/RECORD,,
|
xp/__init__.py
CHANGED
|
@@ -2,7 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
4
|
from datetime import datetime
|
|
5
|
-
from typing import Any, Dict, Optional
|
|
5
|
+
from typing import Any, Dict, Optional, TypedDict
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DiscoveredDevice(TypedDict):
|
|
9
|
+
"""Discovered device information.
|
|
10
|
+
|
|
11
|
+
Attributes:
|
|
12
|
+
serial_number: Serial number of the device.
|
|
13
|
+
module_type: Module type name (e.g., "XP24", "XP230"), None if not yet retrieved.
|
|
14
|
+
module_type_code: Module type code (e.g., "13", "10"), None if not yet retrieved.
|
|
15
|
+
module_type_name: Module type name converted from module_type_code, None if not yet retrieved.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
serial_number: str
|
|
19
|
+
module_type: Optional[str]
|
|
20
|
+
module_type_code: Optional[int]
|
|
21
|
+
module_type_name: Optional[str]
|
|
6
22
|
|
|
7
23
|
|
|
8
24
|
@dataclass
|
|
@@ -13,7 +29,7 @@ class ConbusDiscoverResponse:
|
|
|
13
29
|
success: Whether the operation was successful.
|
|
14
30
|
sent_telegram: Telegram sent to discover devices.
|
|
15
31
|
received_telegrams: List of telegrams received.
|
|
16
|
-
discovered_devices: List of discovered
|
|
32
|
+
discovered_devices: List of discovered devices with their module types.
|
|
17
33
|
error: Error message if operation failed.
|
|
18
34
|
timestamp: Timestamp of the response.
|
|
19
35
|
"""
|
|
@@ -21,7 +37,7 @@ class ConbusDiscoverResponse:
|
|
|
21
37
|
success: bool
|
|
22
38
|
sent_telegram: Optional[str] = None
|
|
23
39
|
received_telegrams: Optional[list[str]] = None
|
|
24
|
-
discovered_devices: Optional[list[
|
|
40
|
+
discovered_devices: Optional[list[DiscoveredDevice]] = None
|
|
25
41
|
error: Optional[str] = None
|
|
26
42
|
timestamp: Optional[datetime] = None
|
|
27
43
|
|
|
@@ -22,10 +22,10 @@ class SystemTelegram(Telegram):
|
|
|
22
22
|
Examples: <S0020012521F02D18FN>
|
|
23
23
|
|
|
24
24
|
Attributes:
|
|
25
|
-
serial_number: Serial number of the device
|
|
26
|
-
system_function: System function code.
|
|
27
|
-
data: Data payload
|
|
28
|
-
datapoint_type: Type of datapoint.
|
|
25
|
+
serial_number: Serial number of the device (0020012521)
|
|
26
|
+
system_function: System function code (02).
|
|
27
|
+
data: Data payload (18)
|
|
28
|
+
datapoint_type: Type of datapoint (18).
|
|
29
29
|
"""
|
|
30
30
|
|
|
31
31
|
serial_number: str = ""
|
|
@@ -10,7 +10,10 @@ from typing import Callable, Optional
|
|
|
10
10
|
from twisted.internet.posixbase import PosixReactorBase
|
|
11
11
|
|
|
12
12
|
from xp.models import ConbusClientConfig, ConbusDiscoverResponse
|
|
13
|
+
from xp.models.conbus.conbus_discover import DiscoveredDevice
|
|
13
14
|
from xp.models.protocol.conbus_protocol import TelegramReceivedEvent
|
|
15
|
+
from xp.models.telegram.datapoint_type import DataPointType
|
|
16
|
+
from xp.models.telegram.module_type_code import MODULE_TYPE_REGISTRY
|
|
14
17
|
from xp.models.telegram.system_function import SystemFunction
|
|
15
18
|
from xp.models.telegram.telegram_type import TelegramType
|
|
16
19
|
from xp.services.protocol.conbus_protocol import ConbusProtocol
|
|
@@ -73,6 +76,7 @@ class ConbusDiscoverService(ConbusProtocol):
|
|
|
73
76
|
self.discovered_device_result.received_telegrams = []
|
|
74
77
|
self.discovered_device_result.received_telegrams.append(telegram_received.frame)
|
|
75
78
|
|
|
79
|
+
# Check for discovery response
|
|
76
80
|
if (
|
|
77
81
|
telegram_received.checksum_valid
|
|
78
82
|
and telegram_received.telegram_type == TelegramType.REPLY.value
|
|
@@ -80,8 +84,30 @@ class ConbusDiscoverService(ConbusProtocol):
|
|
|
80
84
|
and len(telegram_received.payload) == 15
|
|
81
85
|
):
|
|
82
86
|
self.discovered_device(telegram_received.serial_number)
|
|
87
|
+
|
|
88
|
+
# Check for module type response (F02D07)
|
|
89
|
+
elif (
|
|
90
|
+
telegram_received.checksum_valid
|
|
91
|
+
and telegram_received.telegram_type == TelegramType.REPLY.value
|
|
92
|
+
and telegram_received.payload[11:17] == "F02D07"
|
|
93
|
+
and len(telegram_received.payload) >= 19
|
|
94
|
+
):
|
|
95
|
+
self.handle_module_type_code_response(
|
|
96
|
+
telegram_received.serial_number, telegram_received.payload[17:19]
|
|
97
|
+
)
|
|
98
|
+
# Check for module type response (F02D00)
|
|
99
|
+
elif (
|
|
100
|
+
telegram_received.checksum_valid
|
|
101
|
+
and telegram_received.telegram_type == TelegramType.REPLY.value
|
|
102
|
+
and telegram_received.payload[11:17] == "F02D00"
|
|
103
|
+
and len(telegram_received.payload) >= 19
|
|
104
|
+
):
|
|
105
|
+
self.handle_module_type_response(
|
|
106
|
+
telegram_received.serial_number, telegram_received.payload[17:19]
|
|
107
|
+
)
|
|
108
|
+
|
|
83
109
|
else:
|
|
84
|
-
self.logger.debug("Not a discover response")
|
|
110
|
+
self.logger.debug("Not a discover or module type response")
|
|
85
111
|
|
|
86
112
|
def discovered_device(self, serial_number: str) -> None:
|
|
87
113
|
"""Handle discovered device event.
|
|
@@ -92,10 +118,102 @@ class ConbusDiscoverService(ConbusProtocol):
|
|
|
92
118
|
self.logger.info("discovered_device: %s", serial_number)
|
|
93
119
|
if not self.discovered_device_result.discovered_devices:
|
|
94
120
|
self.discovered_device_result.discovered_devices = []
|
|
95
|
-
|
|
121
|
+
|
|
122
|
+
# Add device with module_type as None initially
|
|
123
|
+
device: DiscoveredDevice = {
|
|
124
|
+
"serial_number": serial_number,
|
|
125
|
+
"module_type": None,
|
|
126
|
+
"module_type_code": None,
|
|
127
|
+
"module_type_name": None,
|
|
128
|
+
}
|
|
129
|
+
self.discovered_device_result.discovered_devices.append(device)
|
|
130
|
+
|
|
131
|
+
# Send READ_DATAPOINT telegram to query module type
|
|
132
|
+
self.logger.debug(f"Sending module type query for {serial_number}")
|
|
133
|
+
self.send_telegram(
|
|
134
|
+
telegram_type=TelegramType.SYSTEM,
|
|
135
|
+
serial_number=serial_number,
|
|
136
|
+
system_function=SystemFunction.READ_DATAPOINT,
|
|
137
|
+
data_value=DataPointType.MODULE_TYPE.value,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
self.send_telegram(
|
|
141
|
+
telegram_type=TelegramType.SYSTEM,
|
|
142
|
+
serial_number=serial_number,
|
|
143
|
+
system_function=SystemFunction.READ_DATAPOINT,
|
|
144
|
+
data_value=DataPointType.MODULE_TYPE_CODE.value,
|
|
145
|
+
)
|
|
146
|
+
|
|
96
147
|
if self.progress_callback:
|
|
97
148
|
self.progress_callback(serial_number)
|
|
98
149
|
|
|
150
|
+
def handle_module_type_code_response(
|
|
151
|
+
self, serial_number: str, module_type_code: str
|
|
152
|
+
) -> None:
|
|
153
|
+
"""Handle module type code response and update discovered device.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
serial_number: Serial number of the device.
|
|
157
|
+
module_type_code: Module type code from telegram (e.g., "07", "24").
|
|
158
|
+
"""
|
|
159
|
+
self.logger.info(
|
|
160
|
+
f"Received module type code {module_type_code} for {serial_number}"
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# Convert module type code to name
|
|
164
|
+
code = 0
|
|
165
|
+
try:
|
|
166
|
+
# The telegram format uses decimal values represented as strings
|
|
167
|
+
code = int(module_type_code)
|
|
168
|
+
module_info = MODULE_TYPE_REGISTRY.get(code)
|
|
169
|
+
|
|
170
|
+
if module_info:
|
|
171
|
+
module_type_name = module_info["name"]
|
|
172
|
+
self.logger.debug(
|
|
173
|
+
f"Module type code {module_type_code} ({code}) = {module_type_name}"
|
|
174
|
+
)
|
|
175
|
+
else:
|
|
176
|
+
module_type_name = f"UNKNOWN_{module_type_code}"
|
|
177
|
+
self.logger.warning(
|
|
178
|
+
f"Unknown module type code {module_type_code} ({code})"
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
except ValueError:
|
|
182
|
+
self.logger.error(
|
|
183
|
+
f"Invalid module type code format: {module_type_code} for {serial_number}"
|
|
184
|
+
)
|
|
185
|
+
module_type_name = f"INVALID_{module_type_code}"
|
|
186
|
+
|
|
187
|
+
# Find and update the device in discovered_devices
|
|
188
|
+
if self.discovered_device_result.discovered_devices:
|
|
189
|
+
for device in self.discovered_device_result.discovered_devices:
|
|
190
|
+
if device["serial_number"] == serial_number:
|
|
191
|
+
device["module_type_code"] = code
|
|
192
|
+
device["module_type_name"] = module_type_name
|
|
193
|
+
self.logger.debug(
|
|
194
|
+
f"Updated device {serial_number} with module_type {module_type_name}"
|
|
195
|
+
)
|
|
196
|
+
break
|
|
197
|
+
|
|
198
|
+
def handle_module_type_response(self, serial_number: str, module_type: str) -> None:
|
|
199
|
+
"""Handle module type response and update discovered device.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
serial_number: Serial number of the device.
|
|
203
|
+
module_type: Module type code from telegram (e.g., "XP33", "XP24").
|
|
204
|
+
"""
|
|
205
|
+
self.logger.info(f"Received module type {module_type} for {serial_number}")
|
|
206
|
+
|
|
207
|
+
# Find and update the device in discovered_devices
|
|
208
|
+
if self.discovered_device_result.discovered_devices:
|
|
209
|
+
for device in self.discovered_device_result.discovered_devices:
|
|
210
|
+
if device["serial_number"] == serial_number:
|
|
211
|
+
device["module_type"] = module_type
|
|
212
|
+
self.logger.debug(
|
|
213
|
+
f"Updated device {serial_number} with module_type {module_type}"
|
|
214
|
+
)
|
|
215
|
+
break
|
|
216
|
+
|
|
99
217
|
def timeout(self) -> bool:
|
|
100
218
|
"""Handle timeout event to stop discovery.
|
|
101
219
|
|
|
@@ -128,7 +128,7 @@ class ConbusScanService(ConbusProtocol):
|
|
|
128
128
|
function_code: str,
|
|
129
129
|
progress_callback: Callable[[str], None],
|
|
130
130
|
finish_callback: Callable[[ConbusResponse], None],
|
|
131
|
-
timeout_seconds:
|
|
131
|
+
timeout_seconds: float = 0.25,
|
|
132
132
|
) -> None:
|
|
133
133
|
"""Scan a module for all datapoints by function code.
|
|
134
134
|
|
|
@@ -124,7 +124,7 @@ class TelegramProtocol(protocol.Protocol):
|
|
|
124
124
|
payload = telegram[:-2] # S0123450001F02D12
|
|
125
125
|
checksum = telegram[-2:].decode() # FK
|
|
126
126
|
serial_number = (
|
|
127
|
-
telegram[1:11] if telegram_type in "S" else b""
|
|
127
|
+
telegram[1:11] if telegram_type in ("S", "R") else b""
|
|
128
128
|
) # 0123450001
|
|
129
129
|
calculated_checksum = calculate_checksum(payload.decode(encoding="latin-1"))
|
|
130
130
|
|
|
@@ -151,9 +151,9 @@ class TelegramProtocol(protocol.Protocol):
|
|
|
151
151
|
await self.event_bus.dispatch(
|
|
152
152
|
TelegramReceivedEvent(
|
|
153
153
|
protocol=self,
|
|
154
|
-
frame=frame.decode(),
|
|
155
|
-
telegram=telegram.decode(),
|
|
156
|
-
payload=payload.decode(),
|
|
154
|
+
frame=frame.decode("latin-1"),
|
|
155
|
+
telegram=telegram.decode("latin-1"),
|
|
156
|
+
payload=payload.decode("latin-1"),
|
|
157
157
|
telegram_type=telegram_type,
|
|
158
158
|
serial_number=serial_number,
|
|
159
159
|
checksum=checksum,
|
|
@@ -8,6 +8,7 @@ import logging
|
|
|
8
8
|
from abc import ABC
|
|
9
9
|
from typing import Optional
|
|
10
10
|
|
|
11
|
+
from xp.models import ModuleTypeCode
|
|
11
12
|
from xp.models.telegram.datapoint_type import DataPointType
|
|
12
13
|
from xp.models.telegram.system_function import SystemFunction
|
|
13
14
|
from xp.models.telegram.system_telegram import SystemTelegram
|
|
@@ -33,7 +34,7 @@ class BaseServerService(ABC):
|
|
|
33
34
|
|
|
34
35
|
# Must be set by subclasses
|
|
35
36
|
self.device_type: str = ""
|
|
36
|
-
self.module_type_code:
|
|
37
|
+
self.module_type_code: ModuleTypeCode = ModuleTypeCode.NOMOD
|
|
37
38
|
self.hardware_version: str = ""
|
|
38
39
|
self.software_version: str = ""
|
|
39
40
|
self.device_status: str = "OK"
|
|
@@ -54,11 +55,11 @@ class BaseServerService(ABC):
|
|
|
54
55
|
"""
|
|
55
56
|
datapoint_values = {
|
|
56
57
|
DataPointType.TEMPERATURE: self.temperature,
|
|
57
|
-
DataPointType.MODULE_TYPE_CODE: f"{self.module_type_code:
|
|
58
|
+
DataPointType.MODULE_TYPE_CODE: f"{self.module_type_code.value:02}",
|
|
58
59
|
DataPointType.SW_VERSION: self.software_version,
|
|
59
60
|
DataPointType.MODULE_STATE: self.device_status,
|
|
60
61
|
DataPointType.MODULE_TYPE: self.device_type,
|
|
61
|
-
DataPointType.LINK_NUMBER: f"{self.link_number:
|
|
62
|
+
DataPointType.LINK_NUMBER: f"{self.link_number:02}",
|
|
62
63
|
DataPointType.VOLTAGE: self.voltage,
|
|
63
64
|
DataPointType.HW_VERSION: self.hardware_version,
|
|
64
65
|
DataPointType.MODULE_ERROR_CODE: "00",
|
|
@@ -187,11 +188,15 @@ class BaseServerService(ABC):
|
|
|
187
188
|
self.logger.debug(
|
|
188
189
|
f"_handle_return_data_request {self.device_type} request: {request}"
|
|
189
190
|
)
|
|
191
|
+
module_specific = self._handle_device_specific_data_request(request)
|
|
192
|
+
if module_specific:
|
|
193
|
+
return module_specific
|
|
194
|
+
|
|
190
195
|
if request.datapoint_type:
|
|
191
196
|
return self.generate_datapoint_type_response(request.datapoint_type)
|
|
192
197
|
|
|
193
198
|
# Allow device-specific handlers
|
|
194
|
-
return
|
|
199
|
+
return None
|
|
195
200
|
|
|
196
201
|
def _handle_device_specific_data_request(
|
|
197
202
|
self, request: SystemTelegram
|
|
@@ -6,6 +6,7 @@ including response generation and device configuration handling.
|
|
|
6
6
|
|
|
7
7
|
from typing import Dict, Optional
|
|
8
8
|
|
|
9
|
+
from xp.models import ModuleTypeCode
|
|
9
10
|
from xp.models.telegram.system_telegram import SystemTelegram
|
|
10
11
|
from xp.services.server.base_server_service import BaseServerService
|
|
11
12
|
|
|
@@ -32,7 +33,7 @@ class CP20ServerService(BaseServerService):
|
|
|
32
33
|
"""
|
|
33
34
|
super().__init__(serial_number)
|
|
34
35
|
self.device_type = "CP20"
|
|
35
|
-
self.module_type_code =
|
|
36
|
+
self.module_type_code = ModuleTypeCode.CP20 # CP20 module type from registry
|
|
36
37
|
self.firmware_version = "CP20_V0.01.05"
|
|
37
38
|
|
|
38
39
|
def _handle_device_specific_data_request(
|
|
@@ -253,15 +253,86 @@ class ServerService:
|
|
|
253
253
|
self.logger.error(f"Error closing client socket: {e}")
|
|
254
254
|
|
|
255
255
|
def _process_request(self, message: str) -> List[str]:
|
|
256
|
-
"""Process incoming request and generate responses.
|
|
256
|
+
"""Process incoming request and generate responses.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
message: Message potentially containing multiple telegrams in format <TELEGRAM><TELEGRAM2>...
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
List of responses for all processed telegrams.
|
|
263
|
+
"""
|
|
264
|
+
responses: list[str] = []
|
|
265
|
+
|
|
266
|
+
try:
|
|
267
|
+
# Split message into individual telegrams (enclosed in angle brackets)
|
|
268
|
+
telegrams = self._split_telegrams(message)
|
|
269
|
+
|
|
270
|
+
if not telegrams:
|
|
271
|
+
self.logger.warning(f"No valid telegrams found in message: {message}")
|
|
272
|
+
return responses
|
|
273
|
+
|
|
274
|
+
# Process each telegram
|
|
275
|
+
for telegram in telegrams:
|
|
276
|
+
telegram_responses = self._process_single_telegram(telegram)
|
|
277
|
+
responses.extend(telegram_responses)
|
|
278
|
+
|
|
279
|
+
except Exception as e:
|
|
280
|
+
self.logger.error(f"Error processing request: {e}")
|
|
281
|
+
|
|
282
|
+
return responses
|
|
283
|
+
|
|
284
|
+
def _split_telegrams(self, message: str) -> List[str]:
|
|
285
|
+
"""Split message into individual telegrams.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
message: Raw message containing one or more telegrams in format <TELEGRAM><TELEGRAM2>...
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
List of individual telegram strings including angle brackets.
|
|
292
|
+
"""
|
|
293
|
+
telegrams = []
|
|
294
|
+
start = 0
|
|
295
|
+
|
|
296
|
+
while True:
|
|
297
|
+
# Find the start of a telegram
|
|
298
|
+
start_idx = message.find("<", start)
|
|
299
|
+
if start_idx == -1:
|
|
300
|
+
break
|
|
301
|
+
|
|
302
|
+
# Find the end of the telegram
|
|
303
|
+
end_idx = message.find(">", start_idx)
|
|
304
|
+
if end_idx == -1:
|
|
305
|
+
self.logger.warning(
|
|
306
|
+
f"Incomplete telegram found starting at position {start_idx}"
|
|
307
|
+
)
|
|
308
|
+
break
|
|
309
|
+
|
|
310
|
+
# Extract telegram including angle brackets
|
|
311
|
+
telegram = message[start_idx : end_idx + 1]
|
|
312
|
+
telegrams.append(telegram)
|
|
313
|
+
|
|
314
|
+
# Move to the next position
|
|
315
|
+
start = end_idx + 1
|
|
316
|
+
|
|
317
|
+
return telegrams
|
|
318
|
+
|
|
319
|
+
def _process_single_telegram(self, telegram: str) -> List[str]:
|
|
320
|
+
"""Process a single telegram and generate responses.
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
telegram: A single telegram string.
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
List of response strings for this telegram.
|
|
327
|
+
"""
|
|
257
328
|
responses: list[str] = []
|
|
258
329
|
|
|
259
330
|
try:
|
|
260
331
|
# Parse the telegram
|
|
261
|
-
parsed_telegram = self.telegram_service.parse_system_telegram(
|
|
332
|
+
parsed_telegram = self.telegram_service.parse_system_telegram(telegram)
|
|
262
333
|
|
|
263
334
|
if not parsed_telegram:
|
|
264
|
-
self.logger.warning(f"Failed to parse telegram: {
|
|
335
|
+
self.logger.warning(f"Failed to parse telegram: {telegram}")
|
|
265
336
|
return responses
|
|
266
337
|
|
|
267
338
|
# Handle discover requests
|
|
@@ -296,7 +367,7 @@ class ServerService:
|
|
|
296
367
|
)
|
|
297
368
|
|
|
298
369
|
except Exception as e:
|
|
299
|
-
self.logger.error(f"Error processing
|
|
370
|
+
self.logger.error(f"Error processing telegram: {e}")
|
|
300
371
|
|
|
301
372
|
return responses
|
|
302
373
|
|
|
@@ -7,6 +7,7 @@ XP130 is an Ethernet/TCPIP interface module.
|
|
|
7
7
|
|
|
8
8
|
from typing import Dict
|
|
9
9
|
|
|
10
|
+
from xp.models import ModuleTypeCode
|
|
10
11
|
from xp.services.server.base_server_service import BaseServerService
|
|
11
12
|
|
|
12
13
|
|
|
@@ -32,7 +33,7 @@ class XP130ServerService(BaseServerService):
|
|
|
32
33
|
"""
|
|
33
34
|
super().__init__(serial_number)
|
|
34
35
|
self.device_type = "XP130"
|
|
35
|
-
self.module_type_code =
|
|
36
|
+
self.module_type_code = ModuleTypeCode.XP130 # XP130 module type from registry
|
|
36
37
|
self.firmware_version = "XP130_V1.02.15"
|
|
37
38
|
|
|
38
39
|
# XP130-specific network configuration
|
|
@@ -6,6 +6,7 @@ including response generation and device configuration handling.
|
|
|
6
6
|
|
|
7
7
|
from typing import Dict
|
|
8
8
|
|
|
9
|
+
from xp.models import ModuleTypeCode
|
|
9
10
|
from xp.services.server.base_server_service import BaseServerService
|
|
10
11
|
|
|
11
12
|
|
|
@@ -31,7 +32,7 @@ class XP20ServerService(BaseServerService):
|
|
|
31
32
|
"""
|
|
32
33
|
super().__init__(serial_number)
|
|
33
34
|
self.device_type = "XP20"
|
|
34
|
-
self.module_type_code =
|
|
35
|
+
self.module_type_code = ModuleTypeCode.XP20 # XP20 module type from registry
|
|
35
36
|
self.firmware_version = "XP20_V0.01.05"
|
|
36
37
|
|
|
37
38
|
def get_device_info(self) -> Dict:
|
|
@@ -6,6 +6,7 @@ including response generation and device configuration handling.
|
|
|
6
6
|
|
|
7
7
|
from typing import Dict
|
|
8
8
|
|
|
9
|
+
from xp.models import ModuleTypeCode
|
|
9
10
|
from xp.services.server.base_server_service import BaseServerService
|
|
10
11
|
|
|
11
12
|
|
|
@@ -31,7 +32,7 @@ class XP230ServerService(BaseServerService):
|
|
|
31
32
|
"""
|
|
32
33
|
super().__init__(serial_number)
|
|
33
34
|
self.device_type = "XP230"
|
|
34
|
-
self.module_type_code =
|
|
35
|
+
self.module_type_code = ModuleTypeCode.XP230 # XP230 module type from registry
|
|
35
36
|
self.firmware_version = "XP230_V1.00.04"
|
|
36
37
|
|
|
37
38
|
def get_device_info(self) -> Dict:
|
|
@@ -6,6 +6,7 @@ including response generation and device configuration handling.
|
|
|
6
6
|
|
|
7
7
|
from typing import Dict, Optional
|
|
8
8
|
|
|
9
|
+
from xp.models import ModuleTypeCode
|
|
9
10
|
from xp.models.telegram.datapoint_type import DataPointType
|
|
10
11
|
from xp.models.telegram.system_function import SystemFunction
|
|
11
12
|
from xp.models.telegram.system_telegram import SystemTelegram
|
|
@@ -18,6 +19,16 @@ class XP24ServerError(Exception):
|
|
|
18
19
|
pass
|
|
19
20
|
|
|
20
21
|
|
|
22
|
+
class XP24Output:
|
|
23
|
+
"""Represents an XP24 output state.
|
|
24
|
+
|
|
25
|
+
Attributes:
|
|
26
|
+
state: Current state of the output (True=on, False=off).
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
state: bool = False
|
|
30
|
+
|
|
31
|
+
|
|
21
32
|
class XP24ServerService(BaseServerService):
|
|
22
33
|
"""
|
|
23
34
|
XP24 device emulation service.
|
|
@@ -34,30 +45,107 @@ class XP24ServerService(BaseServerService):
|
|
|
34
45
|
"""
|
|
35
46
|
super().__init__(serial_number)
|
|
36
47
|
self.device_type = "XP24"
|
|
37
|
-
self.module_type_code =
|
|
48
|
+
self.module_type_code = ModuleTypeCode.XP24
|
|
49
|
+
self.autoreport_status = True
|
|
38
50
|
self.firmware_version = "XP24_V0.34.03"
|
|
51
|
+
self.output_0: XP24Output = XP24Output()
|
|
52
|
+
self.output_1: XP24Output = XP24Output()
|
|
53
|
+
self.output_2: XP24Output = XP24Output()
|
|
54
|
+
self.output_3: XP24Output = XP24Output()
|
|
55
|
+
|
|
56
|
+
def _handle_device_specific_action_request(
|
|
57
|
+
self, request: SystemTelegram
|
|
58
|
+
) -> Optional[str]:
|
|
59
|
+
"""Handle XP24-specific data requests."""
|
|
60
|
+
telegrams = self._handle_action_module_output_state(request.data)
|
|
61
|
+
self.logger.debug(
|
|
62
|
+
f"Generated {self.device_type} module type responses: {telegrams}"
|
|
63
|
+
)
|
|
64
|
+
return telegrams
|
|
65
|
+
|
|
66
|
+
def _handle_action_module_output_state(self, data_value: str) -> str:
|
|
67
|
+
"""Handle XP24-specific module output state."""
|
|
68
|
+
output_number = int(data_value[:2])
|
|
69
|
+
output_state = data_value[2:]
|
|
70
|
+
if output_number not in range(0, 4):
|
|
71
|
+
return self._build_ack_nak_response_telegram(False)
|
|
72
|
+
|
|
73
|
+
if output_state not in ("AA", "AB"):
|
|
74
|
+
return self._build_ack_nak_response_telegram(False)
|
|
75
|
+
|
|
76
|
+
output = (self.output_0, self.output_1, self.output_2, self.output_3)[
|
|
77
|
+
output_number
|
|
78
|
+
]
|
|
79
|
+
previous_state = output.state
|
|
80
|
+
output.state = True if output_state == "AB" else False
|
|
81
|
+
state_changed = previous_state != output.state
|
|
82
|
+
|
|
83
|
+
telegrams = self._build_ack_nak_response_telegram(True)
|
|
84
|
+
if state_changed and self.autoreport_status:
|
|
85
|
+
telegrams += self._build_make_break_response_telegram(
|
|
86
|
+
output.state, output_number
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
return telegrams
|
|
90
|
+
|
|
91
|
+
def _build_ack_nak_response_telegram(self, ack_or_nak: bool) -> str:
|
|
92
|
+
"""Build a complete ACK or NAK response telegram with checksum.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
ack_or_nak: true: ACK telegram response, false: NAK telegram response.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
The complete telegram with checksum enclosed in angle brackets.
|
|
99
|
+
"""
|
|
100
|
+
data_value = (
|
|
101
|
+
SystemFunction.ACK.value if ack_or_nak else SystemFunction.NAK.value
|
|
102
|
+
)
|
|
103
|
+
data_part = f"R{self.serial_number}" f"F{data_value:02}D"
|
|
104
|
+
return self._build_response_telegram(data_part)
|
|
105
|
+
|
|
106
|
+
def _build_make_break_response_telegram(
|
|
107
|
+
self, make_or_break: bool, output_number: int
|
|
108
|
+
) -> str:
|
|
109
|
+
"""Build a complete ACK or NAK response telegram with checksum.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
make_or_break: true: MAKE event response, false: BREAK event response.
|
|
113
|
+
output_number: output concerned
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
The complete event telegram with checksum enclosed in angle brackets.
|
|
117
|
+
"""
|
|
118
|
+
data_value = "M" if make_or_break else "B"
|
|
119
|
+
data_part = (
|
|
120
|
+
f"E{self.module_type_code.value:02}"
|
|
121
|
+
f"L{self.link_number:02}"
|
|
122
|
+
f"I{output_number:02}"
|
|
123
|
+
f"{data_value}"
|
|
124
|
+
)
|
|
125
|
+
return self._build_response_telegram(data_part)
|
|
39
126
|
|
|
40
127
|
def _handle_device_specific_data_request(
|
|
41
128
|
self, request: SystemTelegram
|
|
42
129
|
) -> Optional[str]:
|
|
43
130
|
"""Handle XP24-specific data requests."""
|
|
44
|
-
if
|
|
45
|
-
request.system_function != SystemFunction.READ_DATAPOINT
|
|
46
|
-
or not request.datapoint_type
|
|
47
|
-
):
|
|
131
|
+
if not request.datapoint_type:
|
|
48
132
|
return None
|
|
49
133
|
|
|
50
134
|
datapoint_type = request.datapoint_type
|
|
51
|
-
|
|
52
|
-
DataPointType.MODULE_OUTPUT_STATE:
|
|
53
|
-
DataPointType.MODULE_STATE:
|
|
54
|
-
DataPointType.MODULE_OPERATING_HOURS:
|
|
55
|
-
}
|
|
135
|
+
handler = {
|
|
136
|
+
DataPointType.MODULE_OUTPUT_STATE: self._handle_read_module_output_state,
|
|
137
|
+
DataPointType.MODULE_STATE: self._handle_read_module_state,
|
|
138
|
+
DataPointType.MODULE_OPERATING_HOURS: self._handle_read_module_operating_hours,
|
|
139
|
+
}.get(datapoint_type)
|
|
140
|
+
if not handler:
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
data_value = handler()
|
|
56
144
|
data_part = (
|
|
57
145
|
f"R{self.serial_number}"
|
|
58
|
-
f"
|
|
59
|
-
f"{self.module_type_code}"
|
|
60
|
-
f"{
|
|
146
|
+
f"F02D{datapoint_type.value}"
|
|
147
|
+
f"{self.module_type_code.value:02}"
|
|
148
|
+
f"{data_value}"
|
|
61
149
|
)
|
|
62
150
|
telegram = self._build_response_telegram(data_part)
|
|
63
151
|
|
|
@@ -66,21 +154,26 @@ class XP24ServerService(BaseServerService):
|
|
|
66
154
|
)
|
|
67
155
|
return telegram
|
|
68
156
|
|
|
69
|
-
def
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
157
|
+
def _handle_read_module_operating_hours(self) -> str:
|
|
158
|
+
"""Handle XP24-specific module operating hours."""
|
|
159
|
+
return "00:000[H],01:000[H],02:000[H],03:000[H]"
|
|
160
|
+
|
|
161
|
+
def _handle_read_module_state(self) -> str:
|
|
162
|
+
"""Handle XP24-specific module state."""
|
|
163
|
+
for output in (self.output_0, self.output_1, self.output_2, self.output_3):
|
|
164
|
+
if output.state:
|
|
165
|
+
return "ON"
|
|
166
|
+
return "OFF"
|
|
167
|
+
|
|
168
|
+
def _handle_read_module_output_state(self) -> str:
|
|
169
|
+
"""Handle XP24-specific module output state."""
|
|
170
|
+
return (
|
|
171
|
+
f"xxxx"
|
|
172
|
+
f"{1 if self.output_0.state else 0}"
|
|
173
|
+
f"{1 if self.output_1.state else 0}"
|
|
174
|
+
f"{1 if self.output_2.state else 0}"
|
|
175
|
+
f"{1 if self.output_3.state else 0}"
|
|
176
|
+
)
|
|
84
177
|
|
|
85
178
|
def get_device_info(self) -> Dict:
|
|
86
179
|
"""Get XP24 device information.
|
|
@@ -91,29 +184,9 @@ class XP24ServerService(BaseServerService):
|
|
|
91
184
|
return {
|
|
92
185
|
"serial_number": self.serial_number,
|
|
93
186
|
"device_type": self.device_type,
|
|
187
|
+
"module_type_code": self.module_type_code.value,
|
|
94
188
|
"firmware_version": self.firmware_version,
|
|
95
189
|
"status": self.device_status,
|
|
96
190
|
"link_number": self.link_number,
|
|
191
|
+
"autoreport_status": self.autoreport_status,
|
|
97
192
|
}
|
|
98
|
-
|
|
99
|
-
def generate_action_response(self, request: SystemTelegram) -> Optional[str]:
|
|
100
|
-
"""Generate action response telegram (simulated).
|
|
101
|
-
|
|
102
|
-
Args:
|
|
103
|
-
request: The system telegram request.
|
|
104
|
-
|
|
105
|
-
Returns:
|
|
106
|
-
The ACK or NAK response telegram string.
|
|
107
|
-
"""
|
|
108
|
-
response = "F19D" # NAK
|
|
109
|
-
if (
|
|
110
|
-
request.system_function == SystemFunction.ACTION
|
|
111
|
-
and request.data[:2] in ("00", "01", "02", "03")
|
|
112
|
-
and request.data[2:] in ("AA", "AB")
|
|
113
|
-
):
|
|
114
|
-
response = "F18D" # ACK
|
|
115
|
-
|
|
116
|
-
data_part = f"R{self.serial_number}{response}"
|
|
117
|
-
telegram = self._build_response_telegram(data_part)
|
|
118
|
-
self._log_response("module_action_response", telegram)
|
|
119
|
-
return telegram
|
|
@@ -7,11 +7,11 @@ including response generation and device configuration handling for
|
|
|
7
7
|
|
|
8
8
|
from typing import Dict, Optional
|
|
9
9
|
|
|
10
|
+
from xp.models import ModuleTypeCode
|
|
10
11
|
from xp.models.telegram.datapoint_type import DataPointType
|
|
11
12
|
from xp.models.telegram.system_function import SystemFunction
|
|
12
13
|
from xp.models.telegram.system_telegram import SystemTelegram
|
|
13
14
|
from xp.services.server.base_server_service import BaseServerService
|
|
14
|
-
from xp.utils import calculate_checksum
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
class XP33ServerError(Exception):
|
|
@@ -38,27 +38,28 @@ class XP33ServerService(BaseServerService):
|
|
|
38
38
|
super().__init__(serial_number)
|
|
39
39
|
self.variant = variant # XP33 or XP33LR or XP33LED
|
|
40
40
|
self.device_type = "XP33"
|
|
41
|
-
self.module_type_code =
|
|
41
|
+
self.module_type_code = ModuleTypeCode.XP33 # XP33 module type
|
|
42
42
|
|
|
43
43
|
# XP33 device characteristics (anonymized for interoperability testing)
|
|
44
44
|
if variant == "XP33LED":
|
|
45
45
|
self.firmware_version = "XP33LED_V0.00.00"
|
|
46
46
|
self.ean_code = "1234567890123" # Test EAN - not a real product code
|
|
47
47
|
self.max_power = 300 # 3 x 100VA
|
|
48
|
-
self.module_type_code =
|
|
48
|
+
self.module_type_code = ModuleTypeCode.XP33LED # XP33LR module type
|
|
49
49
|
elif variant == "XP33LR": # XP33LR
|
|
50
50
|
self.firmware_version = "XP33LR_V0.00.00"
|
|
51
51
|
self.ean_code = "1234567890124" # Test EAN - not a real product code
|
|
52
52
|
self.max_power = 640 # Total 640VA
|
|
53
|
-
self.module_type_code =
|
|
53
|
+
self.module_type_code = ModuleTypeCode.XP33LR # XP33LR module type
|
|
54
54
|
else: # XP33
|
|
55
55
|
self.firmware_version = "XP33_V0.04.02"
|
|
56
56
|
self.ean_code = "1234567890125" # Test EAN - not a real product code
|
|
57
57
|
self.max_power = 100 # Total 640VA
|
|
58
|
-
self.module_type_code =
|
|
58
|
+
self.module_type_code = ModuleTypeCode.XP33 # XP33 module type
|
|
59
59
|
|
|
60
60
|
self.device_status = "00" # Normal status
|
|
61
61
|
self.link_number = 4 # 4 links configured
|
|
62
|
+
self.autoreport_status = True
|
|
62
63
|
|
|
63
64
|
# Channel states (3 channels, 0-100% dimming)
|
|
64
65
|
self.channel_states = [0, 0, 0] # All channels at 0%
|
|
@@ -71,36 +72,164 @@ class XP33ServerService(BaseServerService):
|
|
|
71
72
|
4: [0, 0, 0], # Scene 4: Off
|
|
72
73
|
}
|
|
73
74
|
|
|
75
|
+
def _handle_device_specific_action_request(
|
|
76
|
+
self, request: SystemTelegram
|
|
77
|
+
) -> Optional[str]:
|
|
78
|
+
"""Handle XP33-specific action requests."""
|
|
79
|
+
telegrams = self._handle_action_channel_dimming(request.data)
|
|
80
|
+
self.logger.debug(f"Generated {self.device_type} action responses: {telegrams}")
|
|
81
|
+
return telegrams
|
|
82
|
+
|
|
83
|
+
def _handle_action_channel_dimming(self, data_value: str) -> str:
|
|
84
|
+
"""Handle XP33-specific channel dimming action.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
data_value: Action data in format channel_number:dimming_level.
|
|
88
|
+
E.g., "00:050" means channel 0, 50% dimming.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Response telegram(s) - ACK/NAK, optionally with event telegram.
|
|
92
|
+
"""
|
|
93
|
+
if ":" not in data_value or len(data_value) < 6:
|
|
94
|
+
return self._build_ack_nak_response_telegram(False)
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
parts = data_value.split(":")
|
|
98
|
+
channel_number = int(parts[0])
|
|
99
|
+
dimming_level = int(parts[1])
|
|
100
|
+
except (ValueError, IndexError):
|
|
101
|
+
return self._build_ack_nak_response_telegram(False)
|
|
102
|
+
|
|
103
|
+
if channel_number not in range(len(self.channel_states)):
|
|
104
|
+
return self._build_ack_nak_response_telegram(False)
|
|
105
|
+
|
|
106
|
+
if dimming_level not in range(0, 101):
|
|
107
|
+
return self._build_ack_nak_response_telegram(False)
|
|
108
|
+
|
|
109
|
+
previous_level = self.channel_states[channel_number]
|
|
110
|
+
self.channel_states[channel_number] = dimming_level
|
|
111
|
+
state_changed = (previous_level == 0 and dimming_level > 0) or (
|
|
112
|
+
previous_level > 0 and dimming_level == 0
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
telegrams = self._build_ack_nak_response_telegram(True)
|
|
116
|
+
if state_changed and self.autoreport_status:
|
|
117
|
+
# Report dimming change event
|
|
118
|
+
telegrams += self._build_dimming_event_telegram(
|
|
119
|
+
dimming_level, channel_number
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
return telegrams
|
|
123
|
+
|
|
124
|
+
def _build_ack_nak_response_telegram(self, ack_or_nak: bool) -> str:
|
|
125
|
+
"""Build a complete ACK or NAK response telegram with checksum.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
ack_or_nak: true: ACK telegram response, false: NAK telegram response.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
The complete telegram with checksum enclosed in angle brackets.
|
|
132
|
+
"""
|
|
133
|
+
data_value = (
|
|
134
|
+
SystemFunction.ACK.value if ack_or_nak else SystemFunction.NAK.value
|
|
135
|
+
)
|
|
136
|
+
data_part = f"R{self.serial_number}" f"F{data_value:02}D"
|
|
137
|
+
return self._build_response_telegram(data_part)
|
|
138
|
+
|
|
139
|
+
def _build_dimming_event_telegram(
|
|
140
|
+
self, dimming_level: int, channel_number: int
|
|
141
|
+
) -> str:
|
|
142
|
+
"""Build a complete dimming event telegram with checksum.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
dimming_level: Dimming level 0-100%.
|
|
146
|
+
channel_number: Channel concerned (0-2).
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
The complete event telegram with checksum enclosed in angle brackets.
|
|
150
|
+
"""
|
|
151
|
+
data_value = "M" if dimming_level > 0 else "B"
|
|
152
|
+
data_part = (
|
|
153
|
+
f"E{self.module_type_code.value:02}"
|
|
154
|
+
f"L{self.link_number:02}"
|
|
155
|
+
f"I{channel_number:02}"
|
|
156
|
+
f"{data_value}"
|
|
157
|
+
)
|
|
158
|
+
return self._build_response_telegram(data_part)
|
|
159
|
+
|
|
74
160
|
def _handle_device_specific_data_request(
|
|
75
161
|
self, request: SystemTelegram
|
|
76
162
|
) -> Optional[str]:
|
|
77
|
-
"""Handle
|
|
78
|
-
if
|
|
79
|
-
request.system_function != SystemFunction.READ_DATAPOINT
|
|
80
|
-
or not request.datapoint_type
|
|
81
|
-
):
|
|
163
|
+
"""Handle XP33-specific data requests."""
|
|
164
|
+
if not request.datapoint_type:
|
|
82
165
|
return None
|
|
83
166
|
|
|
84
167
|
datapoint_type = request.datapoint_type
|
|
85
|
-
|
|
86
|
-
DataPointType.MODULE_OUTPUT_STATE:
|
|
87
|
-
DataPointType.MODULE_STATE:
|
|
88
|
-
DataPointType.MODULE_OPERATING_HOURS:
|
|
89
|
-
|
|
168
|
+
handler = {
|
|
169
|
+
DataPointType.MODULE_OUTPUT_STATE: self._handle_read_module_output_state,
|
|
170
|
+
DataPointType.MODULE_STATE: self._handle_read_module_state,
|
|
171
|
+
DataPointType.MODULE_OPERATING_HOURS: self._handle_read_module_operating_hours,
|
|
172
|
+
DataPointType.MODULE_LIGHT_LEVEL: self._handle_read_light_level,
|
|
173
|
+
}.get(datapoint_type)
|
|
174
|
+
if not handler:
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
data_value = handler()
|
|
90
178
|
data_part = (
|
|
91
179
|
f"R{self.serial_number}"
|
|
92
|
-
f"
|
|
93
|
-
f"{self.module_type_code}"
|
|
94
|
-
f"{
|
|
180
|
+
f"F02D{datapoint_type.value}"
|
|
181
|
+
f"{self.module_type_code.value:02}"
|
|
182
|
+
f"{data_value}"
|
|
95
183
|
)
|
|
96
|
-
|
|
97
|
-
telegram = f"<{data_part}{checksum}>"
|
|
184
|
+
telegram = self._build_response_telegram(data_part)
|
|
98
185
|
|
|
99
186
|
self.logger.debug(
|
|
100
187
|
f"Generated {self.device_type} module type response: {telegram}"
|
|
101
188
|
)
|
|
102
189
|
return telegram
|
|
103
190
|
|
|
191
|
+
def _handle_read_module_output_state(self) -> str:
|
|
192
|
+
"""Handle XP33-specific module output state.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
String representation of the output state for 3 channels.
|
|
196
|
+
"""
|
|
197
|
+
return (
|
|
198
|
+
f"xxxxx"
|
|
199
|
+
f"{1 if self.channel_states[0] > 0 else 0}"
|
|
200
|
+
f"{1 if self.channel_states[1] > 0 else 0}"
|
|
201
|
+
f"{1 if self.channel_states[2] > 0 else 0}"
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
def _handle_read_module_state(self) -> str:
|
|
205
|
+
"""Handle XP33-specific module state.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
'ON' if any channel is active, 'OFF' otherwise.
|
|
209
|
+
"""
|
|
210
|
+
if any(level > 0 for level in self.channel_states):
|
|
211
|
+
return "ON"
|
|
212
|
+
return "OFF"
|
|
213
|
+
|
|
214
|
+
def _handle_read_module_operating_hours(self) -> str:
|
|
215
|
+
"""Handle XP33-specific module operating hours.
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
Operating hours for all 3 channels.
|
|
219
|
+
"""
|
|
220
|
+
return "00:000[H],01:000[H],02:000[H]"
|
|
221
|
+
|
|
222
|
+
def _handle_read_light_level(self) -> str:
|
|
223
|
+
"""Handle XP33-specific light level reading.
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
Light levels for all channels in format "00:000[%],01:000[%],02:000[%]".
|
|
227
|
+
"""
|
|
228
|
+
levels = [
|
|
229
|
+
f"{i:02d}:{level:03d}[%]" for i, level in enumerate(self.channel_states)
|
|
230
|
+
]
|
|
231
|
+
return ",".join(levels)
|
|
232
|
+
|
|
104
233
|
def set_channel_dimming(self, channel: int, level: int) -> bool:
|
|
105
234
|
"""Set individual channel dimming level.
|
|
106
235
|
|
|
@@ -147,6 +276,7 @@ class XP33ServerService(BaseServerService):
|
|
|
147
276
|
"max_power": self.max_power,
|
|
148
277
|
"status": self.device_status,
|
|
149
278
|
"link_number": self.link_number,
|
|
279
|
+
"autoreport_status": self.autoreport_status,
|
|
150
280
|
"channel_states": self.channel_states.copy(),
|
|
151
281
|
"available_scenes": list(self.scenes.keys()),
|
|
152
282
|
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|