conson-xp 1.52.0__py3-none-any.whl → 2.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 (39) hide show
  1. {conson_xp-1.52.0.dist-info → conson_xp-2.0.1.dist-info}/METADATA +1 -11
  2. {conson_xp-1.52.0.dist-info → conson_xp-2.0.1.dist-info}/RECORD +20 -39
  3. xp/__init__.py +1 -1
  4. xp/cli/commands/__init__.py +0 -4
  5. xp/cli/commands/term/term_commands.py +1 -1
  6. xp/cli/main.py +23 -7
  7. xp/models/conbus/conbus_client_config.py +2 -0
  8. xp/models/protocol/conbus_protocol.py +30 -25
  9. xp/models/term/accessory_state.py +1 -1
  10. xp/services/protocol/__init__.py +2 -3
  11. xp/services/protocol/conbus_event_protocol.py +6 -6
  12. xp/services/term/homekit_accessory_driver.py +5 -2
  13. xp/services/term/homekit_service.py +118 -11
  14. xp/term/homekit.py +140 -8
  15. xp/term/homekit.tcss +4 -4
  16. xp/term/widgets/room_list.py +61 -3
  17. xp/utils/dependencies.py +24 -154
  18. xp/cli/commands/homekit/__init__.py +0 -3
  19. xp/cli/commands/homekit/homekit.py +0 -120
  20. xp/cli/commands/homekit/homekit_start_commands.py +0 -44
  21. xp/services/homekit/__init__.py +0 -1
  22. xp/services/homekit/homekit_cache_service.py +0 -313
  23. xp/services/homekit/homekit_conbus_service.py +0 -99
  24. xp/services/homekit/homekit_config_validator.py +0 -327
  25. xp/services/homekit/homekit_conson_validator.py +0 -130
  26. xp/services/homekit/homekit_dimminglight.py +0 -189
  27. xp/services/homekit/homekit_dimminglight_service.py +0 -155
  28. xp/services/homekit/homekit_hap_service.py +0 -351
  29. xp/services/homekit/homekit_lightbulb.py +0 -125
  30. xp/services/homekit/homekit_lightbulb_service.py +0 -91
  31. xp/services/homekit/homekit_module_service.py +0 -60
  32. xp/services/homekit/homekit_outlet.py +0 -175
  33. xp/services/homekit/homekit_outlet_service.py +0 -127
  34. xp/services/homekit/homekit_service.py +0 -371
  35. xp/services/protocol/protocol_factory.py +0 -84
  36. xp/services/protocol/telegram_protocol.py +0 -270
  37. {conson_xp-1.52.0.dist-info → conson_xp-2.0.1.dist-info}/WHEEL +0 -0
  38. {conson_xp-1.52.0.dist-info → conson_xp-2.0.1.dist-info}/entry_points.txt +0 -0
  39. {conson_xp-1.52.0.dist-info → conson_xp-2.0.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,270 +0,0 @@
1
- """
2
- Telegram Protocol for XP telegram communication.
3
-
4
- This module provides the protocol implementation for telegram-based communication.
5
- """
6
-
7
- import asyncio
8
- import logging
9
- import time
10
- from typing import Dict, List, Optional
11
-
12
- from bubus import EventBus
13
- from twisted.internet import protocol
14
-
15
- from xp.models.protocol.conbus_protocol import (
16
- ConnectionMadeEvent,
17
- InvalidTelegramReceivedEvent,
18
- TelegramReceivedEvent,
19
- )
20
- from xp.utils import calculate_checksum
21
-
22
-
23
- class TelegramProtocol(protocol.Protocol):
24
- """
25
- Twisted protocol for XP telegram communication with built-in debouncing.
26
-
27
- Automatically deduplicates identical telegram frames sent within a
28
- configurable time window (default 50ms).
29
-
30
- Attributes:
31
- buffer: Buffer for incoming telegram data.
32
- event_bus: Event bus for dispatching protocol events.
33
- debounce_ms: Debounce time window in milliseconds.
34
- logger: Logger instance for this protocol.
35
- send_queue: Dictionary tracking frame send timestamps.
36
- timer_handle: Handle for cleanup timer.
37
- """
38
-
39
- buffer: bytes
40
- event_bus: EventBus
41
-
42
- def __init__(self, event_bus: EventBus, debounce_ms: int = 50) -> None:
43
- """
44
- Initialize TelegramProtocol.
45
-
46
- Args:
47
- event_bus: Event bus for dispatching protocol events.
48
- debounce_ms: Debounce time window in milliseconds.
49
- """
50
- self.buffer = b""
51
- self.event_bus = event_bus
52
- self.debounce_ms = debounce_ms
53
- self.logger = logging.getLogger(__name__)
54
-
55
- # Debounce state
56
- self.send_queue: Dict[bytes, List[float]] = {} # frame -> [timestamps]
57
- self.timer_handle: Optional[asyncio.TimerHandle] = None
58
-
59
- def connectionMade(self) -> None:
60
- """Handle connection established event."""
61
- self.logger.debug("connectionMade")
62
- try:
63
- self.logger.debug("Scheduling async connection handler")
64
- task = asyncio.create_task(self._async_connection_made())
65
- task.add_done_callback(self._on_task_done)
66
- except Exception as e:
67
- self.logger.error(f"Error scheduling async handler: {e}", exc_info=True)
68
-
69
- def _on_task_done(self, task: asyncio.Task) -> None:
70
- """
71
- Handle async task completion.
72
-
73
- Args:
74
- task: Completed async task.
75
- """
76
- try:
77
- if task.exception():
78
- self.logger.error(
79
- f"Async task failed: {task.exception()}", exc_info=task.exception()
80
- )
81
- else:
82
- self.logger.debug("Async task completed successfully")
83
- except Exception as e:
84
- self.logger.error(f"Error in task done callback: {e}", exc_info=True)
85
-
86
- async def _async_connection_made(self) -> None:
87
- """Async handler for connection made."""
88
- self.logger.debug("_async_connectionMade starting")
89
- self.logger.info("Dispatching ConnectionMadeEvent")
90
- try:
91
- await self.event_bus.dispatch(ConnectionMadeEvent(protocol=self))
92
- self.logger.debug("ConnectionMadeEvent dispatched successfully")
93
- except Exception as e:
94
- self.logger.error(
95
- f"Error dispatching ConnectionMadeEvent: {e}", exc_info=True
96
- )
97
-
98
- def dataReceived(self, data: bytes) -> None:
99
- """
100
- Handle received data from Twisted.
101
-
102
- Args:
103
- data: Raw bytes received from connection.
104
- """
105
- task = asyncio.create_task(self._async_dataReceived(data))
106
- task.add_done_callback(self._on_task_done)
107
-
108
- async def _async_dataReceived(self, data: bytes) -> None:
109
- """Async handler for received data."""
110
- self.logger.debug("dataReceived")
111
- self.buffer += data
112
-
113
- while True:
114
- start = self.buffer.find(b"<")
115
- if start == -1:
116
- break
117
-
118
- end = self.buffer.find(b">", start)
119
- if end == -1:
120
- break
121
-
122
- # <S0123450001F02D12FK>
123
- # <R0123450001F02D12FK>
124
- # <E12L01I08MAK>
125
- frame = self.buffer[start : end + 1] # <S0123450001F02D12FK>
126
- self.buffer = self.buffer[end + 1 :]
127
- telegram = frame[1:-1] # S0123450001F02D12FK
128
- telegram_type = telegram[0:1].decode() # S
129
- payload = telegram[:-2] # S0123450001F02D12
130
- checksum = telegram[-2:].decode() # FK
131
- serial_number = (
132
- telegram[1:11] if telegram_type in ("S", "R") else b""
133
- ) # 0123450001
134
- calculated_checksum = calculate_checksum(payload.decode(encoding="latin-1"))
135
-
136
- if checksum != calculated_checksum:
137
- self.logger.debug(
138
- f"Invalid frame: {frame.decode()} "
139
- f"checksum: {checksum}, "
140
- f"expected {calculated_checksum}"
141
- )
142
- await self.event_bus.dispatch(
143
- InvalidTelegramReceivedEvent(
144
- protocol=self,
145
- frame=frame.decode(),
146
- error=f"Invalid checksum ({calculated_checksum} != {checksum})",
147
- )
148
- )
149
- return
150
-
151
- self.logger.debug(
152
- f"frameReceived payload: {payload.decode()}, checksum: {checksum}"
153
- )
154
- # Dispatch event to bubus with await
155
- self.logger.debug("frameReceived about to dispatch TelegramReceivedEvent")
156
- await self.event_bus.dispatch(
157
- TelegramReceivedEvent(
158
- protocol=self,
159
- frame=frame.decode("latin-1"),
160
- telegram=telegram.decode("latin-1"),
161
- payload=payload.decode("latin-1"),
162
- telegram_type=telegram_type,
163
- serial_number=serial_number,
164
- checksum=checksum,
165
- )
166
- )
167
- self.logger.debug(
168
- "frameReceived TelegramReceivedEvent dispatched successfully"
169
- )
170
-
171
- def sendFrame(self, data: bytes) -> None:
172
- """
173
- Send telegram frame.
174
-
175
- Args:
176
- data: Raw telegram payload (without checksum/framing).
177
- """
178
- task = asyncio.create_task(self._async_sendFrame(data))
179
- task.add_done_callback(self._on_task_done)
180
-
181
- async def _async_sendFrame(self, data: bytes) -> None:
182
- """
183
- Send telegram frame with automatic deduplication.
184
-
185
- Args:
186
- data: Raw telegram payload (without checksum/framing)
187
- """
188
- # Calculate full frame (add checksum and brackets)
189
- checksum = calculate_checksum(data.decode())
190
- frame_data = data.decode() + checksum
191
- frame = b"<" + frame_data.encode() + b">"
192
-
193
- # Apply debouncing and send
194
- await self._send_frame_debounce(frame)
195
-
196
- async def _send_frame_debounce(self, frame: bytes) -> None:
197
- """
198
- Apply debouncing logic and send frame if not a duplicate.
199
-
200
- Identical frames within debounce_ms window are deduplicated.
201
- Only the first occurrence is actually sent to the wire.
202
-
203
- Args:
204
- frame: Complete telegram frame (with checksum and brackets)
205
- """
206
- current_time = time.time()
207
-
208
- # Check if identical frame was recently sent
209
- if frame in self.send_queue:
210
- recent_sends = [
211
- ts
212
- for ts in self.send_queue[frame]
213
- if current_time - ts < (self.debounce_ms / 1000.0)
214
- ]
215
-
216
- if recent_sends:
217
- # Duplicate detected - skip sending
218
- self.logger.debug(
219
- f"Debounced duplicate frame: {frame.decode()} "
220
- f"(sent {len(recent_sends)} times in last {self.debounce_ms}ms)"
221
- )
222
- return
223
-
224
- # Not a duplicate - send it
225
- await self._send_frame_immediate(frame)
226
-
227
- # Track this send
228
- if frame not in self.send_queue:
229
- self.send_queue[frame] = []
230
- self.send_queue[frame].append(current_time)
231
-
232
- # Schedule cleanup of old timestamps
233
- self._schedule_cleanup()
234
-
235
- async def _send_frame_immediate(self, frame: bytes) -> None:
236
- """Actually send frame to TCP transport."""
237
- if not self.transport:
238
- self.logger.info("Invalid transport")
239
- raise IOError("Transport is not open")
240
-
241
- self.logger.debug(f"Sending frame: {frame.decode()}")
242
- self.transport.write(frame) # type: ignore
243
-
244
- def _schedule_cleanup(self) -> None:
245
- """Schedule cleanup of old timestamp tracking."""
246
- if self.timer_handle:
247
- self.timer_handle.cancel()
248
-
249
- loop = asyncio.get_event_loop()
250
- self.timer_handle = loop.call_later(
251
- (self.debounce_ms / 1000.0) * 2, # Cleanup after 2x debounce window
252
- self._cleanup_old_timestamps,
253
- )
254
-
255
- def _cleanup_old_timestamps(self) -> None:
256
- """Remove old timestamps to prevent memory leak."""
257
- current_time = time.time()
258
- cutoff = current_time - (self.debounce_ms / 1000.0)
259
-
260
- for frame in list(self.send_queue.keys()):
261
- # Keep only recent timestamps
262
- self.send_queue[frame] = [
263
- ts for ts in self.send_queue[frame] if ts >= cutoff
264
- ]
265
-
266
- # Remove frame if no recent sends
267
- if not self.send_queue[frame]:
268
- del self.send_queue[frame]
269
-
270
- self.timer_handle = None