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.
- {conson_xp-1.52.0.dist-info → conson_xp-2.0.1.dist-info}/METADATA +1 -11
- {conson_xp-1.52.0.dist-info → conson_xp-2.0.1.dist-info}/RECORD +20 -39
- xp/__init__.py +1 -1
- xp/cli/commands/__init__.py +0 -4
- xp/cli/commands/term/term_commands.py +1 -1
- xp/cli/main.py +23 -7
- xp/models/conbus/conbus_client_config.py +2 -0
- xp/models/protocol/conbus_protocol.py +30 -25
- xp/models/term/accessory_state.py +1 -1
- xp/services/protocol/__init__.py +2 -3
- xp/services/protocol/conbus_event_protocol.py +6 -6
- xp/services/term/homekit_accessory_driver.py +5 -2
- xp/services/term/homekit_service.py +118 -11
- xp/term/homekit.py +140 -8
- xp/term/homekit.tcss +4 -4
- xp/term/widgets/room_list.py +61 -3
- xp/utils/dependencies.py +24 -154
- xp/cli/commands/homekit/__init__.py +0 -3
- xp/cli/commands/homekit/homekit.py +0 -120
- xp/cli/commands/homekit/homekit_start_commands.py +0 -44
- xp/services/homekit/__init__.py +0 -1
- xp/services/homekit/homekit_cache_service.py +0 -313
- xp/services/homekit/homekit_conbus_service.py +0 -99
- xp/services/homekit/homekit_config_validator.py +0 -327
- xp/services/homekit/homekit_conson_validator.py +0 -130
- xp/services/homekit/homekit_dimminglight.py +0 -189
- xp/services/homekit/homekit_dimminglight_service.py +0 -155
- xp/services/homekit/homekit_hap_service.py +0 -351
- xp/services/homekit/homekit_lightbulb.py +0 -125
- xp/services/homekit/homekit_lightbulb_service.py +0 -91
- xp/services/homekit/homekit_module_service.py +0 -60
- xp/services/homekit/homekit_outlet.py +0 -175
- xp/services/homekit/homekit_outlet_service.py +0 -127
- xp/services/homekit/homekit_service.py +0 -371
- xp/services/protocol/protocol_factory.py +0 -84
- xp/services/protocol/telegram_protocol.py +0 -270
- {conson_xp-1.52.0.dist-info → conson_xp-2.0.1.dist-info}/WHEEL +0 -0
- {conson_xp-1.52.0.dist-info → conson_xp-2.0.1.dist-info}/entry_points.txt +0 -0
- {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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|