conson-xp 0.11.21__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.
@@ -5,124 +5,120 @@ various types of telegrams including discover, version, and sensor data requests
5
5
  """
6
6
 
7
7
  import logging
8
- import threading
9
- from typing import Any, Callable, List, Optional
8
+ from datetime import datetime
9
+ from typing import Callable, Optional
10
+
11
+ from twisted.internet.posixbase import PosixReactorBase
10
12
 
11
13
  from xp.models import (
12
- ConbusRequest,
14
+ ConbusClientConfig,
13
15
  ConbusResponse,
14
16
  )
15
- from xp.services.conbus.conbus_service import ConbusService
16
- from xp.services.telegram.telegram_service import TelegramService
17
-
18
-
19
- class ConbusScanError(Exception):
20
- """Raised when Conbus client send operations fail"""
17
+ from xp.models.protocol.conbus_protocol import TelegramReceivedEvent
18
+ from xp.services.protocol import ConbusProtocol
21
19
 
22
- pass
23
20
 
24
-
25
- class ConbusScanService:
21
+ class ConbusScanService(ConbusProtocol):
26
22
  """
27
- TCP client service for sending telegrams to Conbus servers.
23
+ Service for querying datapoints from Conbus modules.
28
24
 
29
- Manages TCP socket connections, handles telegram generation and transmission,
30
- and processes server responses.
25
+ Uses ConbusProtocol to provide datapoint query functionality
26
+ for reading sensor data and module information.
31
27
  """
32
28
 
33
29
  def __init__(
34
30
  self,
35
- telegram_service: TelegramService,
36
- conbus_service: ConbusService,
37
- ):
38
- """Initialize the Conbus client send service
39
-
40
- Args:
41
- telegram_service: TelegramService for dependency injection
42
- conbus_service: ConbusService for dependency injection
43
- """
44
-
45
- # Service dependencies
46
- self.telegram_service = telegram_service
47
- self.conbus_service = conbus_service
48
-
31
+ cli_config: ConbusClientConfig,
32
+ reactor: PosixReactorBase,
33
+ ) -> None:
34
+ """Initialize the Conbus datapoint service"""
35
+ super().__init__(cli_config, reactor)
36
+ self.serial_number: str = ""
37
+ self.function_code: str = ""
38
+ self.datapoint_value: int = -1
39
+ self.progress_callback: Optional[Callable[[str], None]] = None
40
+ self.finish_callback: Optional[Callable[[ConbusResponse], None]] = None
41
+ self.service_response: ConbusResponse = ConbusResponse(
42
+ success=False,
43
+ serial_number=self.serial_number,
44
+ sent_telegrams=[],
45
+ received_telegrams=[],
46
+ timestamp=datetime.now(),
47
+ )
49
48
  # Set up logging
50
49
  self.logger = logging.getLogger(__name__)
51
50
 
52
- def scan_module(
53
- self,
54
- serial_number: str,
55
- function_code: str,
56
- progress_callback: Optional[Callable[[ConbusResponse, int, int], Any]] = None,
57
- ) -> List[ConbusResponse]:
58
- """Scan all functions and datapoints for a module with live output"""
59
- results = []
60
- total_combinations = 100 # 65536 combinations
61
- count = 0
62
-
63
- for datapoint_hex in range(99):
64
- data = f"{datapoint_hex:02d}"
65
- count += 1
66
-
67
- try:
68
- telegram_body = f"S{serial_number}F{function_code}D{data}"
69
- response = self.conbus_service.send_telegram_body(telegram_body)
70
- results.append(response)
71
-
72
- # Call progress callback with live results
73
- if progress_callback:
74
- progress_callback(response, count, total_combinations)
75
-
76
- # Small delay to prevent overwhelming the server
77
- import time
78
-
79
- time.sleep(0.001) # 1ms delay
80
-
81
- except Exception as e:
82
- # Create error response for failed scan attempt
83
- error_response = ConbusResponse(
84
- success=False,
85
- request=ConbusRequest(
86
- serial_number=serial_number,
87
- function_code=function_code,
88
- data=data,
89
- ),
90
- error=f"Scan failed for F{function_code}D{data}: {e}",
91
- )
92
- results.append(error_response)
93
-
94
- # Call progress callback with error response
95
- if progress_callback:
96
- progress_callback(error_response, count, total_combinations)
97
-
98
- return results
51
+ def connection_established(self) -> None:
52
+ self.logger.debug("Connection established, starting scan")
53
+ self.scan_next_datacode()
54
+
55
+ def scan_next_datacode(self) -> bool:
56
+ self.datapoint_value += 1
57
+ if self.datapoint_value >= 100:
58
+ if self.finish_callback:
59
+ self.finish_callback(self.service_response)
60
+ return False
61
+
62
+ self.logger.debug(f"Scanning next datacode: {self.datapoint_value:02d}")
63
+ data = f"{self.datapoint_value:02d}"
64
+ telegram_body = f"S{self.serial_number}F{self.function_code}D{data}"
65
+ self.sendFrame(telegram_body.encode())
66
+ return True
67
+
68
+ def telegram_sent(self, telegram_sent: str) -> None:
69
+ self.service_response.success = True
70
+ self.service_response.sent_telegrams.append(telegram_sent)
71
+
72
+ def telegram_received(self, telegram_received: TelegramReceivedEvent) -> None:
73
+ self.logger.debug(f"Telegram received: {telegram_received}")
74
+ if not self.service_response.received_telegrams:
75
+ self.service_response.received_telegrams = []
76
+ self.service_response.received_telegrams.append(telegram_received.frame)
77
+
78
+ if self.progress_callback:
79
+ self.progress_callback(telegram_received.frame)
80
+
81
+ def timeout(self) -> bool:
82
+ self.logger.debug(f"Timeout: {self.timeout_seconds}s")
83
+ continue_scan = self.scan_next_datacode()
84
+ return continue_scan
85
+
86
+ def failed(self, message: str) -> None:
87
+ self.logger.debug(f"Failed with message: {message}")
88
+ self.service_response.success = False
89
+ self.service_response.timestamp = datetime.now()
90
+ self.service_response.error = message
91
+ if self.finish_callback:
92
+ self.finish_callback(self.service_response)
99
93
 
100
94
  def scan_module_background(
101
95
  self,
102
96
  serial_number: str,
103
97
  function_code: str,
104
- progress_callback: Optional[Callable[[ConbusResponse, int, int], Any]] = None,
105
- ) -> threading.Thread:
106
- """Scan module in background with immediate output via callback"""
107
- import threading
108
-
109
- def background_scan() -> List[ConbusResponse]:
110
- return self.scan_module(serial_number, function_code, progress_callback)
111
-
112
- # Start background thread
113
- scan_thread = threading.Thread(target=background_scan, daemon=True)
114
- scan_thread.start()
98
+ progress_callback: Callable[[str], None],
99
+ finish_callback: Callable[[ConbusResponse], None],
100
+ timeout_seconds: Optional[float] = None,
101
+ ) -> None:
102
+ """
103
+ Query a specific datapoint from a module.
115
104
 
116
- return scan_thread
105
+ Args:
106
+ serial_number: 10-digit module serial number
107
+ function_code: the function code to scan
108
+ progress_callback: callback to handle progress
109
+ finish_callback: callback function to call when the datapoint is received
110
+ timeout_seconds: timeout in seconds
111
+
112
+ Returns:
113
+ ConbusDatapointResponse with operation result and datapoint value
114
+ """
117
115
 
118
- def __enter__(self) -> "ConbusScanService":
119
- return self
116
+ self.logger.info("Starting query_datapoint")
117
+ if timeout_seconds:
118
+ self.timeout_seconds = timeout_seconds
120
119
 
121
- def __exit__(
122
- self,
123
- _exc_type: Optional[type],
124
- _exc_val: Optional[BaseException],
125
- _exc_tb: Optional[Any],
126
- ) -> None:
127
- # Cleanup logic if needed
128
- pass
120
+ self.serial_number = serial_number
121
+ self.function_code = function_code
122
+ self.progress_callback = progress_callback
123
+ self.finish_callback = finish_callback
124
+ self.start_reactor()
@@ -145,6 +145,7 @@ class ConbusProtocol(protocol.Protocol, protocol.ClientFactory):
145
145
  self._stop_reactor()
146
146
 
147
147
  def timeout(self) -> bool:
148
+ """Timeout callback, return True to continue waiting for next timeout, False to stop"""
148
149
  self.logger.info("Timeout after: %ss", self.timeout_seconds)
149
150
  self.failed(f"Timeout after: {self.timeout_seconds}s")
150
151
  return False
xp/utils/dependencies.py CHANGED
@@ -25,10 +25,6 @@ from xp.services.conbus.conbus_autoreport_get_service import ConbusAutoreportGet
25
25
  from xp.services.conbus.conbus_autoreport_set_service import ConbusAutoreportSetService
26
26
  from xp.services.conbus.conbus_blink_all_service import ConbusBlinkAllService
27
27
  from xp.services.conbus.conbus_blink_service import ConbusBlinkService
28
- from xp.services.conbus.conbus_connection_pool import (
29
- ConbusConnectionPool,
30
- ConbusSocketConnectionManager,
31
- )
32
28
  from xp.services.conbus.conbus_custom_service import ConbusCustomService
33
29
  from xp.services.conbus.conbus_datapoint_queryall_service import (
34
30
  ConbusDatapointQueryAllService,
@@ -38,12 +34,12 @@ from xp.services.conbus.conbus_datapoint_service import (
38
34
  )
39
35
  from xp.services.conbus.conbus_discover_service import ConbusDiscoverService
40
36
  from xp.services.conbus.conbus_lightlevel_set_service import ConbusLightlevelSetService
41
- from xp.services.conbus.conbus_linknumber_service import ConbusLinknumberService
37
+ from xp.services.conbus.conbus_linknumber_get_service import ConbusLinknumberGetService
38
+ from xp.services.conbus.conbus_linknumber_set_service import ConbusLinknumberSetService
42
39
  from xp.services.conbus.conbus_output_service import ConbusOutputService
43
40
  from xp.services.conbus.conbus_raw_service import ConbusRawService
44
41
  from xp.services.conbus.conbus_receive_service import ConbusReceiveService
45
42
  from xp.services.conbus.conbus_scan_service import ConbusScanService
46
- from xp.services.conbus.conbus_service import ConbusService
47
43
  from xp.services.homekit.homekit_cache_service import HomeKitCacheService
48
44
  from xp.services.homekit.homekit_conbus_service import HomeKitConbusService
49
45
  from xp.services.homekit.homekit_dimminglight_service import HomeKitDimmingLightService
@@ -106,30 +102,13 @@ class ServiceContainer:
106
102
  def _register_services(self) -> None:
107
103
  """Register all services in the container based on dependency graph."""
108
104
 
109
- # ConbusClientConfig (needed by ConbusConnectionPool)
105
+ # ConbusClientConfig
110
106
  self.container.register(
111
107
  ConbusClientConfig,
112
108
  factory=lambda: ConbusClientConfig.from_yaml(self._config_path),
113
109
  scope=punq.Scope.singleton,
114
110
  )
115
111
 
116
- # Core infrastructure layer - ConbusConnectionPool (singleton)
117
- self.container.register(
118
- ConbusSocketConnectionManager,
119
- factory=lambda: ConbusSocketConnectionManager(
120
- cli_config=self.container.resolve(ConbusClientConfig)
121
- ),
122
- scope=punq.Scope.singleton,
123
- )
124
-
125
- self.container.register(
126
- ConbusConnectionPool,
127
- factory=lambda: ConbusConnectionPool(
128
- connection_manager=self.container.resolve(ConbusSocketConnectionManager)
129
- ),
130
- scope=punq.Scope.singleton,
131
- )
132
-
133
112
  # Telegram services layer
134
113
  self.container.register(TelegramService, scope=punq.Scope.singleton)
135
114
  self.container.register(
@@ -143,16 +122,6 @@ class ServiceContainer:
143
122
  self.container.register(TelegramBlinkService, scope=punq.Scope.singleton)
144
123
  self.container.register(LinkNumberService, scope=punq.Scope.singleton)
145
124
 
146
- # ConbusService - depends on ConbusConnectionPool
147
- self.container.register(
148
- ConbusService,
149
- factory=lambda: ConbusService(
150
- client_config=self.container.resolve(ConbusClientConfig),
151
- connection_pool=self.container.resolve(ConbusConnectionPool),
152
- ),
153
- scope=punq.Scope.singleton,
154
- )
155
-
156
125
  # Conbus services layer
157
126
  self.container.register(
158
127
  ConbusDatapointService,
@@ -177,8 +146,8 @@ class ServiceContainer:
177
146
  self.container.register(
178
147
  ConbusScanService,
179
148
  factory=lambda: ConbusScanService(
180
- telegram_service=self.container.resolve(TelegramService),
181
- conbus_service=self.container.resolve(ConbusService),
149
+ cli_config=self.container.resolve(ConbusClientConfig),
150
+ reactor=self.container.resolve(PosixReactorBase),
182
151
  ),
183
152
  scope=punq.Scope.singleton,
184
153
  )
@@ -215,10 +184,9 @@ class ServiceContainer:
215
184
  self.container.register(
216
185
  ConbusOutputService,
217
186
  factory=lambda: ConbusOutputService(
218
- telegram_service=self.container.resolve(TelegramService),
219
187
  telegram_output_service=self.container.resolve(TelegramOutputService),
220
- datapoint_service=self.container.resolve(ConbusDatapointService),
221
- conbus_service=self.container.resolve(ConbusService),
188
+ cli_config=self.container.resolve(ConbusClientConfig),
189
+ reactor=self.container.resolve(PosixReactorBase),
222
190
  ),
223
191
  scope=punq.Scope.singleton,
224
192
  )
@@ -300,12 +268,21 @@ class ServiceContainer:
300
268
  )
301
269
 
302
270
  self.container.register(
303
- ConbusLinknumberService,
304
- factory=lambda: ConbusLinknumberService(
305
- conbus_service=self.container.resolve(ConbusService),
306
- datapoint_service=self.container.resolve(ConbusDatapointService),
307
- link_number_service=self.container.resolve(LinkNumberService),
271
+ ConbusLinknumberGetService,
272
+ factory=lambda: ConbusLinknumberGetService(
273
+ telegram_service=self.container.resolve(TelegramService),
274
+ cli_config=self.container.resolve(ConbusClientConfig),
275
+ reactor=self.container.resolve(PosixReactorBase),
276
+ ),
277
+ scope=punq.Scope.singleton,
278
+ )
279
+
280
+ self.container.register(
281
+ ConbusLinknumberSetService,
282
+ factory=lambda: ConbusLinknumberSetService(
308
283
  telegram_service=self.container.resolve(TelegramService),
284
+ cli_config=self.container.resolve(ConbusClientConfig),
285
+ reactor=self.container.resolve(PosixReactorBase),
309
286
  ),
310
287
  scope=punq.Scope.singleton,
311
288
  )
@@ -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