conson-xp 1.2.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.
Files changed (62) hide show
  1. {conson_xp-1.2.0.dist-info → conson_xp-1.4.0.dist-info}/METADATA +1 -5
  2. {conson_xp-1.2.0.dist-info → conson_xp-1.4.0.dist-info}/RECORD +43 -60
  3. xp/__init__.py +1 -1
  4. xp/cli/commands/__init__.py +0 -2
  5. xp/cli/commands/conbus/conbus_actiontable_commands.py +5 -3
  6. xp/cli/commands/conbus/conbus_autoreport_commands.py +39 -21
  7. xp/cli/commands/conbus/conbus_blink_commands.py +8 -8
  8. xp/cli/commands/conbus/conbus_config_commands.py +3 -1
  9. xp/cli/commands/conbus/conbus_custom_commands.py +3 -1
  10. xp/cli/commands/conbus/conbus_datapoint_commands.py +4 -2
  11. xp/cli/commands/conbus/conbus_discover_commands.py +5 -3
  12. xp/cli/commands/conbus/conbus_lightlevel_commands.py +68 -32
  13. xp/cli/commands/conbus/conbus_linknumber_commands.py +32 -17
  14. xp/cli/commands/conbus/conbus_msactiontable_commands.py +11 -4
  15. xp/cli/commands/conbus/conbus_output_commands.py +6 -2
  16. xp/cli/commands/conbus/conbus_receive_commands.py +5 -3
  17. xp/cli/commands/file_commands.py +9 -3
  18. xp/cli/commands/homekit/homekit_start_commands.py +3 -1
  19. xp/cli/commands/module_commands.py +12 -4
  20. xp/cli/commands/reverse_proxy_commands.py +3 -1
  21. xp/cli/main.py +0 -2
  22. xp/models/conbus/conbus_datapoint.py +3 -0
  23. xp/models/conbus/conbus_discover.py +19 -3
  24. xp/models/conbus/conbus_writeconfig.py +60 -0
  25. xp/models/telegram/system_telegram.py +4 -4
  26. xp/services/conbus/conbus_datapoint_service.py +9 -6
  27. xp/services/conbus/conbus_discover_service.py +120 -2
  28. xp/services/conbus/conbus_scan_service.py +1 -1
  29. xp/services/conbus/{conbus_linknumber_set_service.py → write_config_service.py} +78 -66
  30. xp/services/protocol/telegram_protocol.py +4 -4
  31. xp/services/server/base_server_service.py +9 -4
  32. xp/services/server/cp20_server_service.py +2 -1
  33. xp/services/server/server_service.py +75 -4
  34. xp/services/server/xp130_server_service.py +2 -1
  35. xp/services/server/xp20_server_service.py +2 -1
  36. xp/services/server/xp230_server_service.py +2 -1
  37. xp/services/server/xp24_server_service.py +123 -50
  38. xp/services/server/xp33_server_service.py +150 -20
  39. xp/services/telegram/telegram_datapoint_service.py +70 -0
  40. xp/utils/dependencies.py +4 -46
  41. xp/api/__init__.py +0 -1
  42. xp/api/main.py +0 -125
  43. xp/api/models/__init__.py +0 -1
  44. xp/api/models/api.py +0 -31
  45. xp/api/models/discover.py +0 -31
  46. xp/api/routers/__init__.py +0 -17
  47. xp/api/routers/conbus.py +0 -5
  48. xp/api/routers/conbus_blink.py +0 -117
  49. xp/api/routers/conbus_custom.py +0 -71
  50. xp/api/routers/conbus_datapoint.py +0 -74
  51. xp/api/routers/conbus_output.py +0 -167
  52. xp/api/routers/errors.py +0 -38
  53. xp/cli/commands/api.py +0 -12
  54. xp/cli/commands/api_start_commands.py +0 -132
  55. xp/services/conbus/conbus_autoreport_get_service.py +0 -94
  56. xp/services/conbus/conbus_autoreport_set_service.py +0 -141
  57. xp/services/conbus/conbus_lightlevel_get_service.py +0 -109
  58. xp/services/conbus/conbus_lightlevel_set_service.py +0 -225
  59. xp/services/conbus/conbus_linknumber_get_service.py +0 -94
  60. {conson_xp-1.2.0.dist-info → conson_xp-1.4.0.dist-info}/WHEEL +0 -0
  61. {conson_xp-1.2.0.dist-info → conson_xp-1.4.0.dist-info}/entry_points.txt +0 -0
  62. {conson_xp-1.2.0.dist-info → conson_xp-1.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -44,7 +44,9 @@ class ConbusDatapointService(ConbusProtocol):
44
44
  self.telegram_service = telegram_service
45
45
  self.serial_number: str = ""
46
46
  self.datapoint_type: Optional[DataPointType] = None
47
- self.finish_callback: Optional[Callable[[ConbusDatapointResponse], None]] = None
47
+ self.datapoint_finished_callback: Optional[
48
+ Callable[[ConbusDatapointResponse], None]
49
+ ] = None
48
50
  self.service_response: ConbusDatapointResponse = ConbusDatapointResponse(
49
51
  success=False,
50
52
  serial_number=self.serial_number,
@@ -125,8 +127,9 @@ class ConbusDatapointService(ConbusProtocol):
125
127
  self.service_response.system_function = SystemFunction.READ_DATAPOINT
126
128
  self.service_response.datapoint_type = self.datapoint_type
127
129
  self.service_response.datapoint_telegram = datapoint_telegram
128
- if self.finish_callback:
129
- self.finish_callback(self.service_response)
130
+ self.service_response.data_value = datapoint_telegram.data_value
131
+ if self.datapoint_finished_callback:
132
+ self.datapoint_finished_callback(self.service_response)
130
133
 
131
134
  def failed(self, message: str) -> None:
132
135
  """Handle failed connection event.
@@ -139,8 +142,8 @@ class ConbusDatapointService(ConbusProtocol):
139
142
  self.service_response.timestamp = datetime.now()
140
143
  self.service_response.serial_number = self.serial_number
141
144
  self.service_response.error = message
142
- if self.finish_callback:
143
- self.finish_callback(self.service_response)
145
+ if self.datapoint_finished_callback:
146
+ self.datapoint_finished_callback(self.service_response)
144
147
 
145
148
  def query_datapoint(
146
149
  self,
@@ -160,7 +163,7 @@ class ConbusDatapointService(ConbusProtocol):
160
163
  self.logger.info("Starting query_datapoint")
161
164
  if timeout_seconds:
162
165
  self.timeout_seconds = timeout_seconds
163
- self.finish_callback = finish_callback
166
+ self.datapoint_finished_callback = finish_callback
164
167
  self.serial_number = serial_number
165
168
  self.datapoint_type = datapoint_type
166
169
  self.start_reactor()
@@ -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
 
@@ -10,7 +10,7 @@ from typing import Callable, Optional
10
10
  from twisted.internet.posixbase import PosixReactorBase
11
11
 
12
12
  from xp.models import ConbusClientConfig
13
- from xp.models.conbus.conbus_linknumber import ConbusLinknumberResponse
13
+ from xp.models.conbus.conbus_writeconfig import ConbusWriteConfigResponse
14
14
  from xp.models.protocol.conbus_protocol import TelegramReceivedEvent
15
15
  from xp.models.telegram.datapoint_type import DataPointType
16
16
  from xp.models.telegram.system_function import SystemFunction
@@ -19,11 +19,11 @@ from xp.services.protocol import ConbusProtocol
19
19
  from xp.services.telegram.telegram_service import TelegramService
20
20
 
21
21
 
22
- class ConbusLinknumberSetService(ConbusProtocol):
22
+ class WriteConfigService(ConbusProtocol):
23
23
  """
24
- Service for setting module link numbers via Conbus telegrams.
24
+ Service for writing module settings via Conbus telegrams.
25
25
 
26
- Handles link number assignment by sending F04D04 telegrams and processing
26
+ Handles setting assignment by sending F04DXX telegrams and processing
27
27
  ACK/NAK responses from modules.
28
28
  """
29
29
 
@@ -42,13 +42,17 @@ class ConbusLinknumberSetService(ConbusProtocol):
42
42
  """
43
43
  super().__init__(cli_config, reactor)
44
44
  self.telegram_service = telegram_service
45
+ self.datapoint_type: Optional[DataPointType] = None
45
46
  self.serial_number: str = ""
46
- self.link_number: int = 0
47
- self.finish_callback: Optional[Callable[[ConbusLinknumberResponse], None]] = (
48
- None
49
- )
50
- self.service_response: ConbusLinknumberResponse = ConbusLinknumberResponse(
51
- success=False, serial_number=self.serial_number, result=""
47
+ self.data_value: str = ""
48
+ self.write_config_finished_callback: Optional[
49
+ Callable[[ConbusWriteConfigResponse], None]
50
+ ] = None
51
+ self.write_config_response: ConbusWriteConfigResponse = (
52
+ ConbusWriteConfigResponse(
53
+ success=False,
54
+ serial_number=self.serial_number,
55
+ )
52
56
  )
53
57
 
54
58
  # Set up logging
@@ -56,26 +60,30 @@ class ConbusLinknumberSetService(ConbusProtocol):
56
60
 
57
61
  def connection_established(self) -> None:
58
62
  """Handle connection established event."""
59
- self.logger.debug(
60
- f"Connection established, setting link number {self.link_number}."
61
- )
63
+ self.logger.debug(f"Connection established, writing config {self.data_value}.")
62
64
 
63
65
  # Validate parameters before sending
64
66
  if not self.serial_number or len(self.serial_number) != 10:
65
67
  self.failed(f"Serial number must be 10 digits, got: {self.serial_number}")
66
68
  return
67
69
 
68
- if not (0 <= self.link_number <= 99):
69
- self.failed(f"Link number must be between 0-99, got: {self.link_number}")
70
+ if len(self.data_value) < 2:
71
+ self.failed(f"data_value must be at least 2 bytes, got: {self.data_value}")
72
+ return
73
+
74
+ if not self.datapoint_type:
75
+ self.failed(f"datapoint_type must be defined, got: {self.datapoint_type}")
70
76
  return
71
77
 
72
- # Send F04D04{link_number} telegram
73
- # F04 = WRITE_CONFIG, D04 = LINK_NUMBER datapoint type
78
+ # Send WRITE_CONFIG telegram
79
+ # Function F04 = WRITE_CONFIG,
80
+ # Datapoint = D datapoint_type
81
+ # Data = XX
74
82
  self.send_telegram(
75
83
  telegram_type=TelegramType.SYSTEM,
76
84
  serial_number=self.serial_number,
77
85
  system_function=SystemFunction.WRITE_CONFIG,
78
- data_value=f"{DataPointType.LINK_NUMBER.value}{self.link_number:02d}",
86
+ data_value=f"{self.datapoint_type.value}{self.data_value}",
79
87
  )
80
88
 
81
89
  def telegram_sent(self, telegram_sent: str) -> None:
@@ -84,7 +92,7 @@ class ConbusLinknumberSetService(ConbusProtocol):
84
92
  Args:
85
93
  telegram_sent: The telegram that was sent.
86
94
  """
87
- self.service_response.sent_telegram = telegram_sent
95
+ self.write_config_response.sent_telegram = telegram_sent
88
96
 
89
97
  def telegram_received(self, telegram_received: TelegramReceivedEvent) -> None:
90
98
  """Handle telegram received event.
@@ -94,9 +102,9 @@ class ConbusLinknumberSetService(ConbusProtocol):
94
102
  """
95
103
  self.logger.debug(f"Telegram received: {telegram_received}")
96
104
 
97
- if not self.service_response.received_telegrams:
98
- self.service_response.received_telegrams = []
99
- self.service_response.received_telegrams.append(telegram_received.frame)
105
+ if not self.write_config_response.received_telegrams:
106
+ self.write_config_response.received_telegrams = []
107
+ self.write_config_response.received_telegrams.append(telegram_received.frame)
100
108
 
101
109
  if (
102
110
  not telegram_received.checksum_valid
@@ -111,71 +119,75 @@ class ConbusLinknumberSetService(ConbusProtocol):
111
119
  telegram_received.frame
112
120
  )
113
121
 
114
- if not reply_telegram:
115
- self.logger.debug("Failed to parse reply telegram")
122
+ if not reply_telegram or reply_telegram.system_function not in (
123
+ SystemFunction.ACK,
124
+ SystemFunction.NAK,
125
+ ):
126
+ self.logger.debug("Not a write config reply")
116
127
  return
117
128
 
118
- # Check for ACK or NAK response
119
- if reply_telegram.system_function == SystemFunction.ACK:
120
- self.logger.debug("Received ACK response")
121
- self.succeed(SystemFunction.ACK)
122
- elif reply_telegram.system_function == SystemFunction.NAK:
123
- self.logger.debug("Received NAK response")
124
- self.failed("Module responded with NAK")
125
- else:
126
- self.logger.debug(
127
- f"Unexpected system function: {reply_telegram.system_function}"
128
- )
129
+ succeed = (
130
+ True if reply_telegram.system_function == SystemFunction.ACK else False
131
+ )
132
+ self.finished(
133
+ succeed_or_failed=succeed, system_function=reply_telegram.system_function
134
+ )
129
135
 
130
- def succeed(self, system_function: SystemFunction) -> None:
131
- """Handle successful link number set operation.
136
+ def failed(self, message: str) -> None:
137
+ """Handle telegram failed event.
132
138
 
133
139
  Args:
134
- system_function: The system function from the reply telegram.
140
+ message: The error message.
135
141
  """
136
- self.logger.debug("Successfully set link number")
137
- self.service_response.success = True
138
- self.service_response.timestamp = datetime.now()
139
- self.service_response.serial_number = self.serial_number
140
- self.service_response.result = "ACK"
141
- self.service_response.link_number = self.link_number
142
- if self.finish_callback:
143
- self.finish_callback(self.service_response)
142
+ self.logger.debug("Failed to send telegram")
143
+ self.finished(succeed_or_failed=False, message=message)
144
144
 
145
- def failed(self, message: str) -> None:
146
- """Handle failed connection event.
145
+ def finished(
146
+ self,
147
+ succeed_or_failed: bool,
148
+ message: Optional[str] = None,
149
+ system_function: Optional[SystemFunction] = None,
150
+ ) -> None:
151
+ """Handle successful link number set operation.
147
152
 
148
153
  Args:
149
- message: Failure message.
154
+ succeed_or_failed: succeed true, failed false.
155
+ message: error message if any.
156
+ system_function: The system function from the reply telegram.
150
157
  """
151
- self.logger.debug(f"Failed with message: {message}")
152
- self.service_response.success = False
153
- self.service_response.timestamp = datetime.now()
154
- self.service_response.serial_number = self.serial_number
155
- self.service_response.result = "NAK"
156
- self.service_response.error = message
157
- if self.finish_callback:
158
- self.finish_callback(self.service_response)
159
-
160
- def set_linknumber(
158
+ self.logger.debug("finished writing config")
159
+ self.write_config_response.success = succeed_or_failed
160
+ self.write_config_response.error = message
161
+ self.write_config_response.timestamp = datetime.now()
162
+ self.write_config_response.serial_number = self.serial_number
163
+ self.write_config_response.system_function = system_function
164
+ self.write_config_response.datapoint_type = self.datapoint_type
165
+ self.write_config_response.data_value = self.data_value
166
+ if self.write_config_finished_callback:
167
+ self.write_config_finished_callback(self.write_config_response)
168
+
169
+ def write_config(
161
170
  self,
162
171
  serial_number: str,
163
- link_number: int,
164
- finish_callback: Callable[[ConbusLinknumberResponse], None],
172
+ datapoint_type: DataPointType,
173
+ data_value: str,
174
+ finish_callback: Callable[[ConbusWriteConfigResponse], None],
165
175
  timeout_seconds: Optional[float] = None,
166
176
  ) -> None:
167
- """Set the link number for a specific module.
177
+ """Write config to a specific module.
168
178
 
169
179
  Args:
170
180
  serial_number: 10-digit module serial number.
171
- link_number: Link number to set (0-99).
181
+ datapoint_type: the datapoint type to write to.
182
+ data_value: the data to write.
172
183
  finish_callback: Callback function to call when operation completes.
173
184
  timeout_seconds: Optional timeout in seconds.
174
185
  """
175
- self.logger.info("Starting set_linknumber")
186
+ self.logger.info("Starting write_config")
176
187
  if timeout_seconds:
177
188
  self.timeout_seconds = timeout_seconds
178
189
  self.serial_number = serial_number
179
- self.link_number = link_number
180
- self.finish_callback = finish_callback
190
+ self.datapoint_type = datapoint_type
191
+ self.data_value = data_value
192
+ self.write_config_finished_callback = finish_callback
181
193
  self.start_reactor()
@@ -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: