conson-xp 0.11.19__py3-none-any.whl → 1.0.1__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-0.11.19.dist-info → conson_xp-1.0.1.dist-info}/METADATA +1 -1
- {conson_xp-0.11.19.dist-info → conson_xp-1.0.1.dist-info}/RECORD +23 -23
- xp/__init__.py +1 -1
- xp/cli/commands/__init__.py +0 -1
- xp/cli/commands/conbus/conbus_config_commands.py +3 -12
- xp/cli/commands/conbus/conbus_lightlevel_commands.py +35 -16
- xp/cli/commands/conbus/conbus_linknumber_commands.py +24 -11
- xp/cli/commands/conbus/conbus_output_commands.py +44 -18
- xp/models/conbus/conbus.py +12 -11
- xp/models/conbus/conbus_linknumber.py +1 -1
- xp/services/conbus/conbus_autoreport_get_service.py +29 -73
- xp/services/conbus/conbus_datapoint_service.py +6 -1
- xp/services/conbus/conbus_lightlevel_get_service.py +101 -0
- xp/services/conbus/conbus_lightlevel_set_service.py +205 -0
- xp/services/conbus/conbus_linknumber_get_service.py +86 -0
- xp/services/conbus/conbus_linknumber_set_service.py +155 -0
- xp/services/conbus/conbus_output_service.py +129 -92
- xp/services/conbus/conbus_scan_service.py +94 -98
- xp/services/protocol/conbus_protocol.py +1 -0
- xp/utils/dependencies.py +26 -50
- xp/services/conbus/conbus_connection_pool.py +0 -148
- xp/services/conbus/conbus_lightlevel_service.py +0 -205
- xp/services/conbus/conbus_linknumber_service.py +0 -197
- xp/services/conbus/conbus_service.py +0 -306
- {conson_xp-0.11.19.dist-info → conson_xp-1.0.1.dist-info}/WHEEL +0 -0
- {conson_xp-0.11.19.dist-info → conson_xp-1.0.1.dist-info}/entry_points.txt +0 -0
- {conson_xp-0.11.19.dist-info → conson_xp-1.0.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
"""Connection pooling implementation for Conbus TCP connections.
|
|
2
|
-
|
|
3
|
-
This module provides a singleton connection pool for managing TCP socket connections
|
|
4
|
-
to Conbus servers with automatic lifecycle management, health checking, and reconnection.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import logging
|
|
8
|
-
import socket
|
|
9
|
-
import threading
|
|
10
|
-
import time
|
|
11
|
-
from typing import Any, Optional
|
|
12
|
-
|
|
13
|
-
from xp.models import ConbusClientConfig
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class ConbusSocketConnectionManager:
|
|
17
|
-
"""Connection manager for TCP socket connections to Conbus servers"""
|
|
18
|
-
|
|
19
|
-
def __init__(self, cli_config: ConbusClientConfig):
|
|
20
|
-
self.config = cli_config.conbus
|
|
21
|
-
self.logger = logging.getLogger(__name__)
|
|
22
|
-
|
|
23
|
-
def create(self) -> socket.socket:
|
|
24
|
-
"""Create and configure a new TCP socket connection"""
|
|
25
|
-
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
26
|
-
sock.settimeout(self.config.timeout)
|
|
27
|
-
sock.connect((self.config.ip, self.config.port))
|
|
28
|
-
self.logger.info(
|
|
29
|
-
f"Created new connection to {self.config.ip}:{self.config.port} (timeout: {self.config.timeout})"
|
|
30
|
-
)
|
|
31
|
-
return sock
|
|
32
|
-
|
|
33
|
-
def dispose(self, connection: socket.socket) -> None:
|
|
34
|
-
"""Close and cleanup socket connection"""
|
|
35
|
-
try:
|
|
36
|
-
connection.close()
|
|
37
|
-
self.logger.info("Disposed socket connection")
|
|
38
|
-
except Exception as e:
|
|
39
|
-
self.logger.warning(f"Error disposing connection: {e}")
|
|
40
|
-
|
|
41
|
-
@staticmethod
|
|
42
|
-
def check_aliveness(connection: socket.socket) -> bool:
|
|
43
|
-
"""Verify if connection is still alive"""
|
|
44
|
-
try:
|
|
45
|
-
# Use socket error checking rather than sending empty data
|
|
46
|
-
# to avoid potential protocol issues
|
|
47
|
-
error = connection.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR)
|
|
48
|
-
return error == 0
|
|
49
|
-
except (socket.error, OSError):
|
|
50
|
-
return False
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
class ConbusConnectionPool:
|
|
54
|
-
"""Singleton connection pool for Conbus TCP connections"""
|
|
55
|
-
|
|
56
|
-
_lock = threading.Lock()
|
|
57
|
-
|
|
58
|
-
def __init__(self, connection_manager: ConbusSocketConnectionManager) -> None:
|
|
59
|
-
if hasattr(self, "_initialized"):
|
|
60
|
-
return
|
|
61
|
-
|
|
62
|
-
self._connection_manager = connection_manager
|
|
63
|
-
self._connection: Optional[socket.socket] = None
|
|
64
|
-
self._current_connection: Optional[socket.socket] = None
|
|
65
|
-
self._connection_created_at: Optional[float] = None
|
|
66
|
-
self._lock = threading.Lock()
|
|
67
|
-
self.logger = logging.getLogger(__name__)
|
|
68
|
-
|
|
69
|
-
# Configuration
|
|
70
|
-
self.idle_timeout = 21600 # 6 hours
|
|
71
|
-
self.max_lifetime = 21600 # 6 hours
|
|
72
|
-
self._initialized = True
|
|
73
|
-
|
|
74
|
-
def _is_connection_expired(self) -> bool:
|
|
75
|
-
"""Check if the current connection has expired"""
|
|
76
|
-
if self._connection_created_at is None:
|
|
77
|
-
return True
|
|
78
|
-
|
|
79
|
-
age = time.time() - self._connection_created_at
|
|
80
|
-
return age > self.max_lifetime
|
|
81
|
-
|
|
82
|
-
def _is_connection_alive(self) -> bool:
|
|
83
|
-
"""Check if connection is still alive"""
|
|
84
|
-
if self._connection is None or self._connection_manager is None:
|
|
85
|
-
return False
|
|
86
|
-
|
|
87
|
-
return self._connection_manager.check_aliveness(self._connection)
|
|
88
|
-
|
|
89
|
-
def acquire_connection(self) -> socket.socket:
|
|
90
|
-
"""Acquire a connection from the pool"""
|
|
91
|
-
if self._connection_manager is None:
|
|
92
|
-
raise RuntimeError("Connection pool not initialized")
|
|
93
|
-
|
|
94
|
-
with self._lock:
|
|
95
|
-
# Check if we need a new connection
|
|
96
|
-
if (
|
|
97
|
-
self._connection is None
|
|
98
|
-
or self._is_connection_expired()
|
|
99
|
-
or not self._is_connection_alive()
|
|
100
|
-
):
|
|
101
|
-
|
|
102
|
-
# Close existing connection if any
|
|
103
|
-
if self._connection:
|
|
104
|
-
self._connection_manager.dispose(self._connection)
|
|
105
|
-
self._connection = None
|
|
106
|
-
|
|
107
|
-
# Create new connection
|
|
108
|
-
self._connection = self._connection_manager.create()
|
|
109
|
-
self._connection_created_at = time.time()
|
|
110
|
-
self.logger.debug("Created new connection")
|
|
111
|
-
|
|
112
|
-
self.logger.debug("Acquired connection from pool")
|
|
113
|
-
return self._connection
|
|
114
|
-
|
|
115
|
-
# noinspection PyUnusedLocal
|
|
116
|
-
def release_connection(self, connection: socket.socket) -> None:
|
|
117
|
-
"""Release a connection back to the pool (no-op for single connection pool)"""
|
|
118
|
-
self.logger.debug("Released connection back to pool")
|
|
119
|
-
# For single connection pool, we just log but don't actually close the connection
|
|
120
|
-
|
|
121
|
-
def __enter__(self) -> socket.socket:
|
|
122
|
-
"""Context manager entry - acquire connection"""
|
|
123
|
-
self._current_connection = self.acquire_connection()
|
|
124
|
-
return self._current_connection
|
|
125
|
-
|
|
126
|
-
def __exit__(
|
|
127
|
-
self,
|
|
128
|
-
_exc_type: Optional[type],
|
|
129
|
-
_exc_val: Optional[Exception],
|
|
130
|
-
_exc_tb: Optional[Any],
|
|
131
|
-
) -> None:
|
|
132
|
-
"""Context manager exit - release connection"""
|
|
133
|
-
if hasattr(self, "_current_connection") and self._current_connection:
|
|
134
|
-
self.release_connection(self._current_connection)
|
|
135
|
-
self._current_connection = None
|
|
136
|
-
|
|
137
|
-
def close(self) -> None:
|
|
138
|
-
"""Close the connection pool and cleanup resources"""
|
|
139
|
-
with self._lock:
|
|
140
|
-
if self._connection and self._connection_manager is not None:
|
|
141
|
-
try:
|
|
142
|
-
self._connection_manager.dispose(self._connection)
|
|
143
|
-
self.logger.info("Connection pool closed")
|
|
144
|
-
except Exception as e:
|
|
145
|
-
self.logger.error(f"Error closing connection pool: {e}")
|
|
146
|
-
finally:
|
|
147
|
-
self._connection = None
|
|
148
|
-
self._connection_created_at = None
|
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
"""Conbus Lightlevel Service for controlling light levels on Conbus modules.
|
|
2
|
-
|
|
3
|
-
This service implements lightlevel control operations for XP modules,
|
|
4
|
-
including setting specific light levels, turning lights on/off, and
|
|
5
|
-
querying current light levels.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
import logging
|
|
9
|
-
from datetime import datetime
|
|
10
|
-
from typing import Any, Optional
|
|
11
|
-
|
|
12
|
-
from xp.models.conbus.conbus_lightlevel import ConbusLightlevelResponse
|
|
13
|
-
from xp.models.telegram.datapoint_type import DataPointType
|
|
14
|
-
from xp.models.telegram.system_function import SystemFunction
|
|
15
|
-
from xp.services.conbus.conbus_datapoint_service import ConbusDatapointService
|
|
16
|
-
from xp.services.conbus.conbus_service import ConbusService
|
|
17
|
-
from xp.services.telegram.telegram_service import TelegramService
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class ConbusLightlevelError(Exception):
|
|
21
|
-
"""Raised when Conbus lightlevel operations fail"""
|
|
22
|
-
|
|
23
|
-
pass
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
class ConbusLightlevelService:
|
|
27
|
-
"""
|
|
28
|
-
Service for controlling light levels on Conbus modules.
|
|
29
|
-
|
|
30
|
-
Manages lightlevel operations including setting specific levels,
|
|
31
|
-
turning lights on/off, and querying current states.
|
|
32
|
-
"""
|
|
33
|
-
|
|
34
|
-
def __init__(
|
|
35
|
-
self,
|
|
36
|
-
telegram_service: TelegramService,
|
|
37
|
-
conbus_service: ConbusService,
|
|
38
|
-
datapoint_service: ConbusDatapointService,
|
|
39
|
-
):
|
|
40
|
-
"""Initialize the Conbus lightlevel service"""
|
|
41
|
-
|
|
42
|
-
# Service dependencies
|
|
43
|
-
self.telegram_service = telegram_service
|
|
44
|
-
self.conbus_service = conbus_service
|
|
45
|
-
self.datapoint_service = datapoint_service
|
|
46
|
-
|
|
47
|
-
# Set up logging
|
|
48
|
-
self.logger = logging.getLogger(__name__)
|
|
49
|
-
|
|
50
|
-
def __enter__(self) -> "ConbusLightlevelService":
|
|
51
|
-
return self
|
|
52
|
-
|
|
53
|
-
def __exit__(
|
|
54
|
-
self,
|
|
55
|
-
_exc_type: Optional[type],
|
|
56
|
-
_exc_val: Optional[Exception],
|
|
57
|
-
_exc_tb: Optional[Any],
|
|
58
|
-
) -> None:
|
|
59
|
-
# Cleanup logic if needed
|
|
60
|
-
pass
|
|
61
|
-
|
|
62
|
-
def set_lightlevel(
|
|
63
|
-
self, serial_number: str, output_number: int, level: int
|
|
64
|
-
) -> ConbusLightlevelResponse:
|
|
65
|
-
"""Set light level for a specific output on a module.
|
|
66
|
-
|
|
67
|
-
Args:
|
|
68
|
-
serial_number: Module serial number
|
|
69
|
-
output_number: Output number (0-based)
|
|
70
|
-
level: Light level percentage (0-100)
|
|
71
|
-
|
|
72
|
-
Returns:
|
|
73
|
-
ConbusLightlevelResponse with operation result
|
|
74
|
-
"""
|
|
75
|
-
|
|
76
|
-
# Validate output_number range (0-8)
|
|
77
|
-
if not 0 <= output_number <= 8:
|
|
78
|
-
return ConbusLightlevelResponse(
|
|
79
|
-
success=False,
|
|
80
|
-
serial_number=serial_number,
|
|
81
|
-
output_number=output_number,
|
|
82
|
-
level=level,
|
|
83
|
-
timestamp=datetime.now(),
|
|
84
|
-
error=f"Output number must be between 0 and 8, got {output_number}",
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
# Validate level range
|
|
88
|
-
if not 0 <= level <= 100:
|
|
89
|
-
return ConbusLightlevelResponse(
|
|
90
|
-
success=False,
|
|
91
|
-
serial_number=serial_number,
|
|
92
|
-
output_number=output_number,
|
|
93
|
-
level=level,
|
|
94
|
-
timestamp=datetime.now(),
|
|
95
|
-
error=f"Light level must be between 0 and 100, got {level}",
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
# Format data as output_number:level (e.g., "02:050")
|
|
99
|
-
data = f"{output_number:02d}:{level:03d}"
|
|
100
|
-
|
|
101
|
-
# Send telegram using WRITE_CONFIG function with MODULE_LIGHT_LEVEL datapoint
|
|
102
|
-
response = self.conbus_service.send_telegram(
|
|
103
|
-
serial_number,
|
|
104
|
-
SystemFunction.WRITE_CONFIG, # "04"
|
|
105
|
-
f"{DataPointType.MODULE_LIGHT_LEVEL.value}{data}", # "15" + "02:050"
|
|
106
|
-
)
|
|
107
|
-
|
|
108
|
-
return ConbusLightlevelResponse(
|
|
109
|
-
success=response.success,
|
|
110
|
-
serial_number=serial_number,
|
|
111
|
-
output_number=output_number,
|
|
112
|
-
level=level,
|
|
113
|
-
timestamp=response.timestamp or datetime.now(),
|
|
114
|
-
sent_telegram=response.sent_telegram,
|
|
115
|
-
received_telegrams=response.received_telegrams,
|
|
116
|
-
error=response.error,
|
|
117
|
-
)
|
|
118
|
-
|
|
119
|
-
def turn_off(
|
|
120
|
-
self, serial_number: str, output_number: int
|
|
121
|
-
) -> ConbusLightlevelResponse:
|
|
122
|
-
"""Turn off light (set level to 0) for a specific output.
|
|
123
|
-
|
|
124
|
-
Args:
|
|
125
|
-
serial_number: Module serial number
|
|
126
|
-
output_number: Output number (0-8)
|
|
127
|
-
|
|
128
|
-
Returns:
|
|
129
|
-
ConbusLightlevelResponse with operation result
|
|
130
|
-
"""
|
|
131
|
-
return self.set_lightlevel(serial_number, output_number, 0)
|
|
132
|
-
|
|
133
|
-
def turn_on(
|
|
134
|
-
self, serial_number: str, output_number: int
|
|
135
|
-
) -> ConbusLightlevelResponse:
|
|
136
|
-
"""Turn on light (set level to 80%) for a specific output.
|
|
137
|
-
|
|
138
|
-
Args:
|
|
139
|
-
serial_number: Module serial number
|
|
140
|
-
output_number: Output number (0-8)
|
|
141
|
-
|
|
142
|
-
Returns:
|
|
143
|
-
ConbusLightlevelResponse with operation result
|
|
144
|
-
"""
|
|
145
|
-
return self.set_lightlevel(serial_number, output_number, 80)
|
|
146
|
-
|
|
147
|
-
def get_lightlevel(
|
|
148
|
-
self, serial_number: str, output_number: int
|
|
149
|
-
) -> ConbusLightlevelResponse:
|
|
150
|
-
"""Query current light level for a specific output.
|
|
151
|
-
|
|
152
|
-
Args:
|
|
153
|
-
serial_number: Module serial number
|
|
154
|
-
output_number: Output number (0-8)
|
|
155
|
-
|
|
156
|
-
Returns:
|
|
157
|
-
ConbusLightlevelResponse with current light level
|
|
158
|
-
"""
|
|
159
|
-
|
|
160
|
-
# TODO: Migrate to new ConbusDatapointService callback-based API
|
|
161
|
-
# Query MODULE_LIGHT_LEVEL datapoint
|
|
162
|
-
datapoint_response = self.datapoint_service.query_datapoint( # type: ignore[call-arg,func-returns-value]
|
|
163
|
-
serial_number=serial_number,
|
|
164
|
-
datapoint_type=DataPointType.MODULE_LIGHT_LEVEL,
|
|
165
|
-
)
|
|
166
|
-
|
|
167
|
-
if not datapoint_response.success:
|
|
168
|
-
return ConbusLightlevelResponse(
|
|
169
|
-
success=False,
|
|
170
|
-
serial_number=serial_number,
|
|
171
|
-
output_number=output_number,
|
|
172
|
-
level=None,
|
|
173
|
-
timestamp=datetime.now(),
|
|
174
|
-
error=datapoint_response.error or "Failed to query light level",
|
|
175
|
-
)
|
|
176
|
-
|
|
177
|
-
# Parse the response to extract level for specific output
|
|
178
|
-
level = None
|
|
179
|
-
if (
|
|
180
|
-
datapoint_response.datapoint_telegram
|
|
181
|
-
and datapoint_response.datapoint_telegram.data_value
|
|
182
|
-
):
|
|
183
|
-
try:
|
|
184
|
-
# Parse response format like "00:050,01:025,02:100"
|
|
185
|
-
data_value = str(datapoint_response.datapoint_telegram.data_value)
|
|
186
|
-
for output_data in data_value.split(","):
|
|
187
|
-
if ":" in output_data:
|
|
188
|
-
output_str, level_str = output_data.split(":")
|
|
189
|
-
if int(output_str) == output_number:
|
|
190
|
-
level_str = level_str.replace("[%]", "")
|
|
191
|
-
level = int(level_str)
|
|
192
|
-
break
|
|
193
|
-
except (ValueError, AttributeError) as e:
|
|
194
|
-
self.logger.debug(f"Failed to parse light level data: {e}")
|
|
195
|
-
|
|
196
|
-
return ConbusLightlevelResponse(
|
|
197
|
-
success=datapoint_response.success,
|
|
198
|
-
serial_number=serial_number,
|
|
199
|
-
output_number=output_number,
|
|
200
|
-
level=level,
|
|
201
|
-
timestamp=datetime.now(),
|
|
202
|
-
sent_telegram=datapoint_response.sent_telegram,
|
|
203
|
-
received_telegrams=datapoint_response.received_telegrams,
|
|
204
|
-
error=datapoint_response.error if level is None else None,
|
|
205
|
-
)
|
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
"""Conbus Link Number Service for setting module link numbers.
|
|
2
|
-
|
|
3
|
-
This service handles setting link numbers for modules through Conbus telegrams.
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
import logging
|
|
7
|
-
from typing import Any, Optional
|
|
8
|
-
|
|
9
|
-
from xp.models.conbus.conbus_linknumber import ConbusLinknumberResponse
|
|
10
|
-
from xp.models.telegram.datapoint_type import DataPointType
|
|
11
|
-
from xp.models.telegram.reply_telegram import ReplyTelegram
|
|
12
|
-
from xp.services.conbus.conbus_datapoint_service import ConbusDatapointService
|
|
13
|
-
from xp.services.conbus.conbus_service import ConbusService
|
|
14
|
-
from xp.services.telegram.telegram_link_number_service import (
|
|
15
|
-
LinkNumberError,
|
|
16
|
-
LinkNumberService,
|
|
17
|
-
)
|
|
18
|
-
from xp.services.telegram.telegram_service import TelegramService
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class ConbusLinknumberService:
|
|
22
|
-
"""
|
|
23
|
-
Service for setting and getting module link numbers via Conbus telegrams.
|
|
24
|
-
|
|
25
|
-
Handles link number assignment by sending F04D04 telegrams and processing
|
|
26
|
-
ACK/NAK responses from modules. Also handles link number reading using
|
|
27
|
-
datapoint queries.
|
|
28
|
-
"""
|
|
29
|
-
|
|
30
|
-
def __init__(
|
|
31
|
-
self,
|
|
32
|
-
conbus_service: ConbusService,
|
|
33
|
-
datapoint_service: ConbusDatapointService,
|
|
34
|
-
link_number_service: LinkNumberService,
|
|
35
|
-
telegram_service: TelegramService,
|
|
36
|
-
):
|
|
37
|
-
"""Initialize the Conbus link number service"""
|
|
38
|
-
|
|
39
|
-
# Service dependencies
|
|
40
|
-
self.conbus_service = conbus_service
|
|
41
|
-
self.datapoint_service = datapoint_service
|
|
42
|
-
self.link_number_service = link_number_service
|
|
43
|
-
self.telegram_service = telegram_service
|
|
44
|
-
|
|
45
|
-
# Set up logging
|
|
46
|
-
self.logger = logging.getLogger(__name__)
|
|
47
|
-
|
|
48
|
-
def __enter__(self) -> "ConbusLinknumberService":
|
|
49
|
-
return self
|
|
50
|
-
|
|
51
|
-
def __exit__(
|
|
52
|
-
self,
|
|
53
|
-
_exc_type: Optional[type],
|
|
54
|
-
_exc_val: Optional[Exception],
|
|
55
|
-
_exc_tb: Optional[Any],
|
|
56
|
-
) -> None:
|
|
57
|
-
# Cleanup logic if needed
|
|
58
|
-
pass
|
|
59
|
-
|
|
60
|
-
def set_linknumber(
|
|
61
|
-
self, serial_number: str, link_number: int
|
|
62
|
-
) -> ConbusLinknumberResponse:
|
|
63
|
-
"""
|
|
64
|
-
Set the link number for a specific module.
|
|
65
|
-
|
|
66
|
-
Args:
|
|
67
|
-
serial_number: 10-digit module serial number
|
|
68
|
-
link_number: Link number to set (0-99)
|
|
69
|
-
|
|
70
|
-
Returns:
|
|
71
|
-
ConbusLinknumberResponse with operation result
|
|
72
|
-
|
|
73
|
-
Raises:
|
|
74
|
-
LinkNumberError: If parameters are invalid
|
|
75
|
-
"""
|
|
76
|
-
try:
|
|
77
|
-
# Generate the link number setting telegram
|
|
78
|
-
telegram = self.link_number_service.generate_set_link_number_telegram(
|
|
79
|
-
serial_number, link_number
|
|
80
|
-
)
|
|
81
|
-
|
|
82
|
-
# Send telegram using ConbusService
|
|
83
|
-
with self.conbus_service:
|
|
84
|
-
response = self.conbus_service.send_raw_telegram(telegram)
|
|
85
|
-
|
|
86
|
-
# Determine result based on response
|
|
87
|
-
result = "NAK" # Default to NAK
|
|
88
|
-
if response.success and response.received_telegrams:
|
|
89
|
-
# Try to parse the first received telegram
|
|
90
|
-
if len(response.received_telegrams) > 0:
|
|
91
|
-
received_telegram = response.received_telegrams[0]
|
|
92
|
-
try:
|
|
93
|
-
parsed_telegram = self.telegram_service.parse_telegram(
|
|
94
|
-
received_telegram
|
|
95
|
-
)
|
|
96
|
-
if isinstance(parsed_telegram, ReplyTelegram):
|
|
97
|
-
if self.link_number_service.is_ack_response(
|
|
98
|
-
parsed_telegram
|
|
99
|
-
):
|
|
100
|
-
result = "ACK"
|
|
101
|
-
elif self.link_number_service.is_nak_response(
|
|
102
|
-
parsed_telegram
|
|
103
|
-
):
|
|
104
|
-
result = "NAK"
|
|
105
|
-
except Exception as e:
|
|
106
|
-
self.logger.warning(f"Failed to parse reply telegram: {e}")
|
|
107
|
-
|
|
108
|
-
return ConbusLinknumberResponse(
|
|
109
|
-
success=response.success and result == "ACK",
|
|
110
|
-
result=result,
|
|
111
|
-
link_number=link_number,
|
|
112
|
-
serial_number=serial_number,
|
|
113
|
-
sent_telegram=telegram,
|
|
114
|
-
received_telegrams=response.received_telegrams,
|
|
115
|
-
error=response.error,
|
|
116
|
-
timestamp=response.timestamp,
|
|
117
|
-
)
|
|
118
|
-
|
|
119
|
-
except LinkNumberError as e:
|
|
120
|
-
return ConbusLinknumberResponse(
|
|
121
|
-
success=False,
|
|
122
|
-
result="NAK",
|
|
123
|
-
serial_number=serial_number,
|
|
124
|
-
error=str(e),
|
|
125
|
-
)
|
|
126
|
-
except Exception as e:
|
|
127
|
-
return ConbusLinknumberResponse(
|
|
128
|
-
success=False,
|
|
129
|
-
result="NAK",
|
|
130
|
-
serial_number=serial_number,
|
|
131
|
-
error=f"Unexpected error: {e}",
|
|
132
|
-
)
|
|
133
|
-
|
|
134
|
-
def get_linknumber(self, serial_number: str) -> ConbusLinknumberResponse:
|
|
135
|
-
"""
|
|
136
|
-
Get the current link number for a specific module.
|
|
137
|
-
|
|
138
|
-
Args:
|
|
139
|
-
serial_number: 10-digit module serial number
|
|
140
|
-
|
|
141
|
-
Returns:
|
|
142
|
-
ConbusLinknumberResponse with operation result and link number
|
|
143
|
-
|
|
144
|
-
Raises:
|
|
145
|
-
Exception: If datapoint query fails
|
|
146
|
-
"""
|
|
147
|
-
try:
|
|
148
|
-
# TODO: Migrate to new ConbusDatapointService callback-based API
|
|
149
|
-
# Query the LINK_NUMBER datapoint
|
|
150
|
-
datapoint_response = self.datapoint_service.query_datapoint( # type: ignore[call-arg,func-returns-value]
|
|
151
|
-
serial_number=serial_number,
|
|
152
|
-
datapoint_type=DataPointType.LINK_NUMBER,
|
|
153
|
-
)
|
|
154
|
-
|
|
155
|
-
if datapoint_response.success and datapoint_response.datapoint_telegram:
|
|
156
|
-
# Extract link number from datapoint response
|
|
157
|
-
try:
|
|
158
|
-
link_number_value = int(
|
|
159
|
-
datapoint_response.datapoint_telegram.data_value
|
|
160
|
-
)
|
|
161
|
-
return ConbusLinknumberResponse(
|
|
162
|
-
success=True,
|
|
163
|
-
result="SUCCESS",
|
|
164
|
-
serial_number=serial_number,
|
|
165
|
-
link_number=link_number_value,
|
|
166
|
-
sent_telegram=datapoint_response.sent_telegram,
|
|
167
|
-
received_telegrams=datapoint_response.received_telegrams,
|
|
168
|
-
timestamp=datapoint_response.timestamp,
|
|
169
|
-
)
|
|
170
|
-
except (ValueError, TypeError) as e:
|
|
171
|
-
return ConbusLinknumberResponse(
|
|
172
|
-
success=False,
|
|
173
|
-
result="PARSE_ERROR",
|
|
174
|
-
serial_number=serial_number,
|
|
175
|
-
sent_telegram=datapoint_response.sent_telegram,
|
|
176
|
-
received_telegrams=datapoint_response.received_telegrams,
|
|
177
|
-
error=f"Failed to parse link number: {e}",
|
|
178
|
-
timestamp=datapoint_response.timestamp,
|
|
179
|
-
)
|
|
180
|
-
else:
|
|
181
|
-
return ConbusLinknumberResponse(
|
|
182
|
-
success=False,
|
|
183
|
-
result="QUERY_FAILED",
|
|
184
|
-
serial_number=serial_number,
|
|
185
|
-
sent_telegram=datapoint_response.sent_telegram,
|
|
186
|
-
received_telegrams=datapoint_response.received_telegrams,
|
|
187
|
-
error=datapoint_response.error or "Failed to query link number",
|
|
188
|
-
timestamp=datapoint_response.timestamp,
|
|
189
|
-
)
|
|
190
|
-
|
|
191
|
-
except Exception as e:
|
|
192
|
-
return ConbusLinknumberResponse(
|
|
193
|
-
success=False,
|
|
194
|
-
result="ERROR",
|
|
195
|
-
serial_number=serial_number,
|
|
196
|
-
error=f"Unexpected error: {e}",
|
|
197
|
-
)
|