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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: conson-xp
3
- Version: 1.3.0
3
+ Version: 1.4.0
4
4
  Summary: XP Protocol Communication Tools
5
5
  Author-Email: ldvchosal <ldvchosal@github.com>
6
6
  License: MIT License
@@ -1,8 +1,8 @@
1
- conson_xp-1.3.0.dist-info/METADATA,sha256=vbTIsfypeY2WgsmWOGOeLT0pMaLOtQAh0PGVDTpkUzA,9274
2
- conson_xp-1.3.0.dist-info/WHEEL,sha256=9P2ygRxDrTJz3gsagc0Z96ukrxjr-LFBGOgv3AuKlCA,90
3
- conson_xp-1.3.0.dist-info/entry_points.txt,sha256=1OcdIcDM1hz3ljCXgybaPUh1IOFEwkaTgLIW9u9zGeg,50
4
- conson_xp-1.3.0.dist-info/licenses/LICENSE,sha256=rxj6woMM-r3YCyGq_UHFtbh7kHTAJgrccH6O-33IDE4,1419
5
- xp/__init__.py,sha256=zgW0EVZ8tN8g8HSuCLDaoFIBrf1aZ7IwB6_185Hu2yk,180
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=oYIHLLsXmgSQHrayS-JgcEpEaK_01Q1WTQEQj79h4j4,1642
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=oYW9xfY4ldlid0XQ5D5ymk0-a9Jvr5EHD9O4Cb6JOPk,3192
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=0rdGLZfi-gsjBeuFiUwGz0tUC85hbZUDxJ93ImBmq4U,5239
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=43Bhj7pVDOjIS2MdlD7iDvNK7Gy5KxX1qB8hiTwY5LQ,5186
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=JKYxGbO2h3muIroYPYxS7An_BNfriXKLf3u0gdN_KnQ,9462
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=HQKx4DW6lJ25dGofjaB_3HHPvEZGbL1XeQ20wU5k_z0,8755
142
- xp/services/server/cp20_server_service.py,sha256=AQhfHBqXQsVL5emkXfSLdjlKaAIWtXXKqyxYNKNKWpQ,1624
143
- xp/services/server/server_service.py,sha256=4lhd8feL7eRR5GZIiuag51z5W20KlGRrIGICgvOXwAQ,12459
144
- xp/services/server/xp130_server_service.py,sha256=QbkOykAdYqSSOeI7uq3g-qy1zlXSB9mjWBfRz1esPis,1764
145
- xp/services/server/xp20_server_service.py,sha256=w8LF6vl6jNeKKlrUVdv-2Tk1qq9DMfe40PiPps6kq84,1374
146
- xp/services/server/xp230_server_service.py,sha256=n8eqmyWWVYVuLl-N3gu1zA0-Viifyo5VyMrGXLcvpgc,1388
147
- xp/services/server/xp24_server_service.py,sha256=ygYym0oCzmHaVwDbl-XAZYyFlLLwyycMAO8g8D1q2iU,3806
148
- xp/services/server/xp33_server_service.py,sha256=qswOUv3YS5pT6rDoony49EC-MFKJZKPo7DI3fKG74ts,6307
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.3.0.dist-info/RECORD,,
164
+ conson_xp-1.4.0.dist-info/RECORD,,
xp/__init__.py CHANGED
@@ -3,7 +3,7 @@
3
3
  conson-xp package.
4
4
  """
5
5
 
6
- __version__ = "1.3.0"
6
+ __version__ = "1.4.0"
7
7
  __manufacturer__ = "salchichon"
8
8
  __model__ = "xp.cli"
9
9
  __serial__ = "2025.09.23.000"
@@ -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 device serial numbers.
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[str]] = None
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
- self.discovered_device_result.discovered_devices.append(serial_number)
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: Optional[float] = None,
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: int = 0
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:02X}",
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:02X}",
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 self._handle_device_specific_data_request(request)
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 = 2 # CP20 module type from registry
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(message)
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: {message}")
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 request: {e}")
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 = 13 # XP130 module type from registry
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 = 33 # XP20 module type from registry
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 = 34 # XP230 module type from registry
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 = 7 # XP24 module type from registry
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
- datapoint_values = {
52
- DataPointType.MODULE_OUTPUT_STATE: "xxxx0001",
53
- DataPointType.MODULE_STATE: "OFF",
54
- DataPointType.MODULE_OPERATING_HOURS: "00:000[H],01:000[H],02:000[H],03:000[H]",
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"F02{datapoint_type.value}"
59
- f"{self.module_type_code}"
60
- f"{datapoint_values.get(datapoint_type)}"
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 _handle_device_specific_action_request(
70
- self, request: SystemTelegram
71
- ) -> Optional[str]:
72
- """Handle XP24-specific action requests.
73
-
74
- Args:
75
- request: The system telegram request.
76
-
77
- Returns:
78
- The response telegram string, or None if request cannot be handled.
79
- """
80
- if request.system_function != SystemFunction.ACTION:
81
- return None
82
-
83
- return self.generate_action_response(request)
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 = 11 # XP33 module type
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 = 31 # XP33LR module type
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 = 30 # XP33LR module type
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 = 11 # XP33 module type
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 XP24-specific data requests."""
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
- datapoint_values = {
86
- DataPointType.MODULE_OUTPUT_STATE: "xxxxx001",
87
- DataPointType.MODULE_STATE: "OFF",
88
- DataPointType.MODULE_OPERATING_HOURS: "00:000[H],01:000[H],02:000[H]",
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"F02{datapoint_type.value}"
93
- f"{self.module_type_code}"
94
- f"{datapoint_values.get(datapoint_type)}"
180
+ f"F02D{datapoint_type.value}"
181
+ f"{self.module_type_code.value:02}"
182
+ f"{data_value}"
95
183
  )
96
- checksum = calculate_checksum(data_part)
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
  }