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.
Files changed (27) hide show
  1. {conson_xp-0.11.19.dist-info → conson_xp-1.0.1.dist-info}/METADATA +1 -1
  2. {conson_xp-0.11.19.dist-info → conson_xp-1.0.1.dist-info}/RECORD +23 -23
  3. xp/__init__.py +1 -1
  4. xp/cli/commands/__init__.py +0 -1
  5. xp/cli/commands/conbus/conbus_config_commands.py +3 -12
  6. xp/cli/commands/conbus/conbus_lightlevel_commands.py +35 -16
  7. xp/cli/commands/conbus/conbus_linknumber_commands.py +24 -11
  8. xp/cli/commands/conbus/conbus_output_commands.py +44 -18
  9. xp/models/conbus/conbus.py +12 -11
  10. xp/models/conbus/conbus_linknumber.py +1 -1
  11. xp/services/conbus/conbus_autoreport_get_service.py +29 -73
  12. xp/services/conbus/conbus_datapoint_service.py +6 -1
  13. xp/services/conbus/conbus_lightlevel_get_service.py +101 -0
  14. xp/services/conbus/conbus_lightlevel_set_service.py +205 -0
  15. xp/services/conbus/conbus_linknumber_get_service.py +86 -0
  16. xp/services/conbus/conbus_linknumber_set_service.py +155 -0
  17. xp/services/conbus/conbus_output_service.py +129 -92
  18. xp/services/conbus/conbus_scan_service.py +94 -98
  19. xp/services/protocol/conbus_protocol.py +1 -0
  20. xp/utils/dependencies.py +26 -50
  21. xp/services/conbus/conbus_connection_pool.py +0 -148
  22. xp/services/conbus/conbus_lightlevel_service.py +0 -205
  23. xp/services/conbus/conbus_linknumber_service.py +0 -197
  24. xp/services/conbus/conbus_service.py +0 -306
  25. {conson_xp-0.11.19.dist-info → conson_xp-1.0.1.dist-info}/WHEEL +0 -0
  26. {conson_xp-0.11.19.dist-info → conson_xp-1.0.1.dist-info}/entry_points.txt +0 -0
  27. {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
- )