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.
- {conson_xp-1.2.0.dist-info → conson_xp-1.4.0.dist-info}/METADATA +1 -5
- {conson_xp-1.2.0.dist-info → conson_xp-1.4.0.dist-info}/RECORD +43 -60
- xp/__init__.py +1 -1
- xp/cli/commands/__init__.py +0 -2
- xp/cli/commands/conbus/conbus_actiontable_commands.py +5 -3
- xp/cli/commands/conbus/conbus_autoreport_commands.py +39 -21
- xp/cli/commands/conbus/conbus_blink_commands.py +8 -8
- xp/cli/commands/conbus/conbus_config_commands.py +3 -1
- xp/cli/commands/conbus/conbus_custom_commands.py +3 -1
- xp/cli/commands/conbus/conbus_datapoint_commands.py +4 -2
- xp/cli/commands/conbus/conbus_discover_commands.py +5 -3
- xp/cli/commands/conbus/conbus_lightlevel_commands.py +68 -32
- xp/cli/commands/conbus/conbus_linknumber_commands.py +32 -17
- xp/cli/commands/conbus/conbus_msactiontable_commands.py +11 -4
- xp/cli/commands/conbus/conbus_output_commands.py +6 -2
- xp/cli/commands/conbus/conbus_receive_commands.py +5 -3
- xp/cli/commands/file_commands.py +9 -3
- xp/cli/commands/homekit/homekit_start_commands.py +3 -1
- xp/cli/commands/module_commands.py +12 -4
- xp/cli/commands/reverse_proxy_commands.py +3 -1
- xp/cli/main.py +0 -2
- xp/models/conbus/conbus_datapoint.py +3 -0
- xp/models/conbus/conbus_discover.py +19 -3
- xp/models/conbus/conbus_writeconfig.py +60 -0
- xp/models/telegram/system_telegram.py +4 -4
- xp/services/conbus/conbus_datapoint_service.py +9 -6
- xp/services/conbus/conbus_discover_service.py +120 -2
- xp/services/conbus/conbus_scan_service.py +1 -1
- xp/services/conbus/{conbus_linknumber_set_service.py → write_config_service.py} +78 -66
- 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
- xp/services/telegram/telegram_datapoint_service.py +70 -0
- xp/utils/dependencies.py +4 -46
- xp/api/__init__.py +0 -1
- xp/api/main.py +0 -125
- xp/api/models/__init__.py +0 -1
- xp/api/models/api.py +0 -31
- xp/api/models/discover.py +0 -31
- xp/api/routers/__init__.py +0 -17
- xp/api/routers/conbus.py +0 -5
- xp/api/routers/conbus_blink.py +0 -117
- xp/api/routers/conbus_custom.py +0 -71
- xp/api/routers/conbus_datapoint.py +0 -74
- xp/api/routers/conbus_output.py +0 -167
- xp/api/routers/errors.py +0 -38
- xp/cli/commands/api.py +0 -12
- xp/cli/commands/api_start_commands.py +0 -132
- xp/services/conbus/conbus_autoreport_get_service.py +0 -94
- xp/services/conbus/conbus_autoreport_set_service.py +0 -141
- xp/services/conbus/conbus_lightlevel_get_service.py +0 -109
- xp/services/conbus/conbus_lightlevel_set_service.py +0 -225
- xp/services/conbus/conbus_linknumber_get_service.py +0 -94
- {conson_xp-1.2.0.dist-info → conson_xp-1.4.0.dist-info}/WHEEL +0 -0
- {conson_xp-1.2.0.dist-info → conson_xp-1.4.0.dist-info}/entry_points.txt +0 -0
- {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.
|
|
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
|
-
|
|
129
|
-
|
|
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.
|
|
143
|
-
self.
|
|
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.
|
|
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
|
-
|
|
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
|
|
|
@@ -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.
|
|
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
|
|
22
|
+
class WriteConfigService(ConbusProtocol):
|
|
23
23
|
"""
|
|
24
|
-
Service for
|
|
24
|
+
Service for writing module settings via Conbus telegrams.
|
|
25
25
|
|
|
26
|
-
Handles
|
|
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.
|
|
47
|
-
self.
|
|
48
|
-
None
|
|
49
|
-
|
|
50
|
-
self.
|
|
51
|
-
|
|
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
|
|
69
|
-
self.failed(f"
|
|
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
|
|
73
|
-
# F04 = WRITE_CONFIG,
|
|
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"{
|
|
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.
|
|
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.
|
|
98
|
-
self.
|
|
99
|
-
self.
|
|
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
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
|
131
|
-
"""Handle
|
|
136
|
+
def failed(self, message: str) -> None:
|
|
137
|
+
"""Handle telegram failed event.
|
|
132
138
|
|
|
133
139
|
Args:
|
|
134
|
-
|
|
140
|
+
message: The error message.
|
|
135
141
|
"""
|
|
136
|
-
self.logger.debug("
|
|
137
|
-
self.
|
|
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
|
|
146
|
-
|
|
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
|
-
|
|
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(
|
|
152
|
-
self.
|
|
153
|
-
self.
|
|
154
|
-
self.
|
|
155
|
-
self.
|
|
156
|
-
self.
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
164
|
-
|
|
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
|
-
"""
|
|
177
|
+
"""Write config to a specific module.
|
|
168
178
|
|
|
169
179
|
Args:
|
|
170
180
|
serial_number: 10-digit module serial number.
|
|
171
|
-
|
|
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
|
|
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.
|
|
180
|
-
self.
|
|
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:
|
|
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:
|