conson-xp 1.18.0__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 (176) hide show
  1. conson_xp-1.18.0.dist-info/METADATA +412 -0
  2. conson_xp-1.18.0.dist-info/RECORD +176 -0
  3. conson_xp-1.18.0.dist-info/WHEEL +4 -0
  4. conson_xp-1.18.0.dist-info/entry_points.txt +5 -0
  5. conson_xp-1.18.0.dist-info/licenses/LICENSE +29 -0
  6. xp/__init__.py +9 -0
  7. xp/cli/__init__.py +5 -0
  8. xp/cli/__main__.py +6 -0
  9. xp/cli/commands/__init__.py +153 -0
  10. xp/cli/commands/conbus/__init__.py +25 -0
  11. xp/cli/commands/conbus/conbus.py +128 -0
  12. xp/cli/commands/conbus/conbus_actiontable_commands.py +233 -0
  13. xp/cli/commands/conbus/conbus_autoreport_commands.py +108 -0
  14. xp/cli/commands/conbus/conbus_blink_commands.py +163 -0
  15. xp/cli/commands/conbus/conbus_config_commands.py +29 -0
  16. xp/cli/commands/conbus/conbus_custom_commands.py +57 -0
  17. xp/cli/commands/conbus/conbus_datapoint_commands.py +113 -0
  18. xp/cli/commands/conbus/conbus_discover_commands.py +61 -0
  19. xp/cli/commands/conbus/conbus_event_commands.py +81 -0
  20. xp/cli/commands/conbus/conbus_lightlevel_commands.py +207 -0
  21. xp/cli/commands/conbus/conbus_linknumber_commands.py +102 -0
  22. xp/cli/commands/conbus/conbus_modulenumber_commands.py +104 -0
  23. xp/cli/commands/conbus/conbus_msactiontable_commands.py +94 -0
  24. xp/cli/commands/conbus/conbus_output_commands.py +163 -0
  25. xp/cli/commands/conbus/conbus_raw_commands.py +62 -0
  26. xp/cli/commands/conbus/conbus_receive_commands.py +59 -0
  27. xp/cli/commands/conbus/conbus_scan_commands.py +58 -0
  28. xp/cli/commands/file_commands.py +186 -0
  29. xp/cli/commands/homekit/__init__.py +3 -0
  30. xp/cli/commands/homekit/homekit.py +118 -0
  31. xp/cli/commands/homekit/homekit_start_commands.py +43 -0
  32. xp/cli/commands/module_commands.py +187 -0
  33. xp/cli/commands/reverse_proxy_commands.py +178 -0
  34. xp/cli/commands/server/__init__.py +3 -0
  35. xp/cli/commands/server/server_commands.py +135 -0
  36. xp/cli/commands/telegram/__init__.py +5 -0
  37. xp/cli/commands/telegram/telegram.py +41 -0
  38. xp/cli/commands/telegram/telegram_blink_commands.py +79 -0
  39. xp/cli/commands/telegram/telegram_checksum_commands.py +112 -0
  40. xp/cli/commands/telegram/telegram_discover_commands.py +41 -0
  41. xp/cli/commands/telegram/telegram_linknumber_commands.py +86 -0
  42. xp/cli/commands/telegram/telegram_parse_commands.py +75 -0
  43. xp/cli/commands/telegram/telegram_version_commands.py +52 -0
  44. xp/cli/main.py +87 -0
  45. xp/cli/utils/__init__.py +1 -0
  46. xp/cli/utils/click_tree.py +57 -0
  47. xp/cli/utils/datapoint_type_choice.py +57 -0
  48. xp/cli/utils/decorators.py +351 -0
  49. xp/cli/utils/error_handlers.py +201 -0
  50. xp/cli/utils/formatters.py +312 -0
  51. xp/cli/utils/module_type_choice.py +56 -0
  52. xp/cli/utils/serial_number_type.py +52 -0
  53. xp/cli/utils/system_function_choice.py +57 -0
  54. xp/cli/utils/xp_module_type.py +53 -0
  55. xp/connection/__init__.py +13 -0
  56. xp/connection/exceptions.py +22 -0
  57. xp/models/__init__.py +36 -0
  58. xp/models/actiontable/__init__.py +1 -0
  59. xp/models/actiontable/actiontable.py +43 -0
  60. xp/models/actiontable/msactiontable_xp20.py +53 -0
  61. xp/models/actiontable/msactiontable_xp24.py +58 -0
  62. xp/models/actiontable/msactiontable_xp33.py +65 -0
  63. xp/models/conbus/__init__.py +1 -0
  64. xp/models/conbus/conbus.py +87 -0
  65. xp/models/conbus/conbus_autoreport.py +67 -0
  66. xp/models/conbus/conbus_blink.py +80 -0
  67. xp/models/conbus/conbus_client_config.py +55 -0
  68. xp/models/conbus/conbus_connection_status.py +40 -0
  69. xp/models/conbus/conbus_custom.py +58 -0
  70. xp/models/conbus/conbus_datapoint.py +89 -0
  71. xp/models/conbus/conbus_discover.py +64 -0
  72. xp/models/conbus/conbus_event_raw.py +47 -0
  73. xp/models/conbus/conbus_lightlevel.py +52 -0
  74. xp/models/conbus/conbus_linknumber.py +54 -0
  75. xp/models/conbus/conbus_output.py +57 -0
  76. xp/models/conbus/conbus_raw.py +45 -0
  77. xp/models/conbus/conbus_receive.py +42 -0
  78. xp/models/conbus/conbus_writeconfig.py +60 -0
  79. xp/models/homekit/__init__.py +1 -0
  80. xp/models/homekit/homekit_accessory.py +35 -0
  81. xp/models/homekit/homekit_config.py +106 -0
  82. xp/models/homekit/homekit_conson_config.py +86 -0
  83. xp/models/log_entry.py +130 -0
  84. xp/models/protocol/__init__.py +1 -0
  85. xp/models/protocol/conbus_protocol.py +312 -0
  86. xp/models/response.py +42 -0
  87. xp/models/telegram/__init__.py +1 -0
  88. xp/models/telegram/action_type.py +31 -0
  89. xp/models/telegram/datapoint_type.py +82 -0
  90. xp/models/telegram/event_telegram.py +140 -0
  91. xp/models/telegram/event_type.py +15 -0
  92. xp/models/telegram/input_action_type.py +69 -0
  93. xp/models/telegram/input_type.py +17 -0
  94. xp/models/telegram/module_type.py +188 -0
  95. xp/models/telegram/module_type_code.py +205 -0
  96. xp/models/telegram/output_telegram.py +103 -0
  97. xp/models/telegram/reply_telegram.py +297 -0
  98. xp/models/telegram/system_function.py +116 -0
  99. xp/models/telegram/system_telegram.py +94 -0
  100. xp/models/telegram/telegram.py +28 -0
  101. xp/models/telegram/telegram_type.py +19 -0
  102. xp/models/telegram/timeparam_type.py +51 -0
  103. xp/models/write_config_type.py +33 -0
  104. xp/services/__init__.py +26 -0
  105. xp/services/actiontable/__init__.py +1 -0
  106. xp/services/actiontable/actiontable_serializer.py +273 -0
  107. xp/services/actiontable/msactiontable_serializer.py +7 -0
  108. xp/services/actiontable/msactiontable_xp20_serializer.py +169 -0
  109. xp/services/actiontable/msactiontable_xp24_serializer.py +120 -0
  110. xp/services/actiontable/msactiontable_xp33_serializer.py +239 -0
  111. xp/services/conbus/__init__.py +1 -0
  112. xp/services/conbus/actiontable/__init__.py +1 -0
  113. xp/services/conbus/actiontable/actiontable_download_service.py +158 -0
  114. xp/services/conbus/actiontable/actiontable_list_service.py +91 -0
  115. xp/services/conbus/actiontable/actiontable_show_service.py +89 -0
  116. xp/services/conbus/actiontable/actiontable_upload_service.py +211 -0
  117. xp/services/conbus/actiontable/msactiontable_service.py +232 -0
  118. xp/services/conbus/conbus_blink_all_service.py +181 -0
  119. xp/services/conbus/conbus_blink_service.py +158 -0
  120. xp/services/conbus/conbus_custom_service.py +156 -0
  121. xp/services/conbus/conbus_datapoint_queryall_service.py +182 -0
  122. xp/services/conbus/conbus_datapoint_service.py +170 -0
  123. xp/services/conbus/conbus_discover_service.py +312 -0
  124. xp/services/conbus/conbus_event_raw_service.py +181 -0
  125. xp/services/conbus/conbus_output_service.py +194 -0
  126. xp/services/conbus/conbus_raw_service.py +122 -0
  127. xp/services/conbus/conbus_receive_service.py +115 -0
  128. xp/services/conbus/conbus_scan_service.py +150 -0
  129. xp/services/conbus/write_config_service.py +194 -0
  130. xp/services/homekit/__init__.py +1 -0
  131. xp/services/homekit/homekit_cache_service.py +307 -0
  132. xp/services/homekit/homekit_conbus_service.py +93 -0
  133. xp/services/homekit/homekit_config_validator.py +310 -0
  134. xp/services/homekit/homekit_conson_validator.py +121 -0
  135. xp/services/homekit/homekit_dimminglight.py +182 -0
  136. xp/services/homekit/homekit_dimminglight_service.py +148 -0
  137. xp/services/homekit/homekit_hap_service.py +342 -0
  138. xp/services/homekit/homekit_lightbulb.py +120 -0
  139. xp/services/homekit/homekit_lightbulb_service.py +86 -0
  140. xp/services/homekit/homekit_module_service.py +56 -0
  141. xp/services/homekit/homekit_outlet.py +168 -0
  142. xp/services/homekit/homekit_outlet_service.py +121 -0
  143. xp/services/homekit/homekit_service.py +359 -0
  144. xp/services/log_file_service.py +309 -0
  145. xp/services/module_type_service.py +257 -0
  146. xp/services/protocol/__init__.py +21 -0
  147. xp/services/protocol/conbus_event_protocol.py +360 -0
  148. xp/services/protocol/conbus_protocol.py +318 -0
  149. xp/services/protocol/protocol_factory.py +78 -0
  150. xp/services/protocol/telegram_protocol.py +264 -0
  151. xp/services/reverse_proxy_service.py +435 -0
  152. xp/services/server/__init__.py +1 -0
  153. xp/services/server/base_server_service.py +366 -0
  154. xp/services/server/cp20_server_service.py +65 -0
  155. xp/services/server/device_service_factory.py +94 -0
  156. xp/services/server/server_service.py +428 -0
  157. xp/services/server/xp130_server_service.py +67 -0
  158. xp/services/server/xp20_server_service.py +92 -0
  159. xp/services/server/xp230_server_service.py +58 -0
  160. xp/services/server/xp24_server_service.py +245 -0
  161. xp/services/server/xp33_server_service.py +535 -0
  162. xp/services/telegram/__init__.py +1 -0
  163. xp/services/telegram/telegram_blink_service.py +138 -0
  164. xp/services/telegram/telegram_checksum_service.py +149 -0
  165. xp/services/telegram/telegram_datapoint_service.py +82 -0
  166. xp/services/telegram/telegram_discover_service.py +277 -0
  167. xp/services/telegram/telegram_link_number_service.py +216 -0
  168. xp/services/telegram/telegram_output_service.py +322 -0
  169. xp/services/telegram/telegram_service.py +380 -0
  170. xp/services/telegram/telegram_version_service.py +288 -0
  171. xp/utils/__init__.py +12 -0
  172. xp/utils/checksum.py +61 -0
  173. xp/utils/dependencies.py +531 -0
  174. xp/utils/event_helper.py +31 -0
  175. xp/utils/serialization.py +205 -0
  176. xp/utils/time_utils.py +134 -0
@@ -0,0 +1,78 @@
1
+ """Protocol Factory for Twisted protocol creation.
2
+
3
+ This module provides factory classes for protocol instantiation.
4
+ """
5
+
6
+ import logging
7
+
8
+ from bubus import EventBus
9
+ from twisted.internet import protocol
10
+ from twisted.internet.interfaces import IAddress, IConnector
11
+ from twisted.python.failure import Failure
12
+
13
+ from xp.models.protocol.conbus_protocol import (
14
+ ConnectionFailedEvent,
15
+ ConnectionLostEvent,
16
+ )
17
+ from xp.services.protocol import TelegramProtocol
18
+
19
+
20
+ class TelegramFactory(protocol.ClientFactory):
21
+ """Factory for creating Telegram protocol instances.
22
+
23
+ Attributes:
24
+ event_bus: Event bus for dispatching protocol events.
25
+ telegram_protocol: Protocol instance to use.
26
+ connector: Connection connector instance.
27
+ logger: Logger instance for this factory.
28
+ """
29
+
30
+ def __init__(
31
+ self,
32
+ event_bus: EventBus,
33
+ telegram_protocol: TelegramProtocol,
34
+ connector: IConnector,
35
+ ) -> None:
36
+ """Initialize TelegramFactory.
37
+
38
+ Args:
39
+ event_bus: Event bus for protocol events.
40
+ telegram_protocol: Protocol instance to use for connections.
41
+ connector: Connection connector for managing connections.
42
+ """
43
+ self.event_bus = event_bus
44
+ self.telegram_protocol = telegram_protocol
45
+ self.connector = connector
46
+ self.logger = logging.getLogger(__name__)
47
+
48
+ def buildProtocol(self, addr: IAddress) -> TelegramProtocol:
49
+ """Build protocol instance for connection.
50
+
51
+ Args:
52
+ addr: Address of the connection.
53
+
54
+ Returns:
55
+ Telegram protocol instance for this connection.
56
+ """
57
+ self.logger.debug(f"buildProtocol: {addr}")
58
+ return self.telegram_protocol
59
+
60
+ def clientConnectionFailed(self, connector: IConnector, reason: Failure) -> None:
61
+ """Handle connection failure event.
62
+
63
+ Args:
64
+ connector: Connection connector instance.
65
+ reason: Failure reason details.
66
+ """
67
+ self.event_bus.dispatch(ConnectionFailedEvent(reason=str(reason)))
68
+ self.connector.stop()
69
+
70
+ def clientConnectionLost(self, connector: IConnector, reason: Failure) -> None:
71
+ """Handle connection lost event.
72
+
73
+ Args:
74
+ connector: Connection connector instance.
75
+ reason: Reason for connection loss.
76
+ """
77
+ self.event_bus.dispatch(ConnectionLostEvent(reason=str(reason)))
78
+ self.connector.stop()
@@ -0,0 +1,264 @@
1
+ """Telegram Protocol for XP telegram communication.
2
+
3
+ This module provides the protocol implementation for telegram-based communication.
4
+ """
5
+
6
+ import asyncio
7
+ import logging
8
+ import time
9
+ from typing import Dict, List, Optional
10
+
11
+ from bubus import EventBus
12
+ from twisted.internet import protocol
13
+
14
+ from xp.models.protocol.conbus_protocol import (
15
+ ConnectionMadeEvent,
16
+ InvalidTelegramReceivedEvent,
17
+ TelegramReceivedEvent,
18
+ )
19
+ from xp.utils import calculate_checksum
20
+
21
+
22
+ class TelegramProtocol(protocol.Protocol):
23
+ """Twisted protocol for XP telegram communication with built-in debouncing.
24
+
25
+ Automatically deduplicates identical telegram frames sent within a
26
+ configurable time window (default 50ms).
27
+
28
+ Attributes:
29
+ buffer: Buffer for incoming telegram data.
30
+ event_bus: Event bus for dispatching protocol events.
31
+ debounce_ms: Debounce time window in milliseconds.
32
+ logger: Logger instance for this protocol.
33
+ send_queue: Dictionary tracking frame send timestamps.
34
+ timer_handle: Handle for cleanup timer.
35
+ """
36
+
37
+ buffer: bytes
38
+ event_bus: EventBus
39
+
40
+ def __init__(self, event_bus: EventBus, debounce_ms: int = 50) -> None:
41
+ """Initialize TelegramProtocol.
42
+
43
+ Args:
44
+ event_bus: Event bus for dispatching protocol events.
45
+ debounce_ms: Debounce time window in milliseconds.
46
+ """
47
+ self.buffer = b""
48
+ self.event_bus = event_bus
49
+ self.debounce_ms = debounce_ms
50
+ self.logger = logging.getLogger(__name__)
51
+
52
+ # Debounce state
53
+ self.send_queue: Dict[bytes, List[float]] = {} # frame -> [timestamps]
54
+ self.timer_handle: Optional[asyncio.TimerHandle] = None
55
+
56
+ def connectionMade(self) -> None:
57
+ """Handle connection established event."""
58
+ self.logger.debug("connectionMade")
59
+ try:
60
+ self.logger.debug("Scheduling async connection handler")
61
+ task = asyncio.create_task(self._async_connection_made())
62
+ task.add_done_callback(self._on_task_done)
63
+ except Exception as e:
64
+ self.logger.error(f"Error scheduling async handler: {e}", exc_info=True)
65
+
66
+ def _on_task_done(self, task: asyncio.Task) -> None:
67
+ """Handle async task completion.
68
+
69
+ Args:
70
+ task: Completed async task.
71
+ """
72
+ try:
73
+ if task.exception():
74
+ self.logger.error(
75
+ f"Async task failed: {task.exception()}", exc_info=task.exception()
76
+ )
77
+ else:
78
+ self.logger.debug("Async task completed successfully")
79
+ except Exception as e:
80
+ self.logger.error(f"Error in task done callback: {e}", exc_info=True)
81
+
82
+ async def _async_connection_made(self) -> None:
83
+ """Async handler for connection made."""
84
+ self.logger.debug("_async_connectionMade starting")
85
+ self.logger.info("Dispatching ConnectionMadeEvent")
86
+ try:
87
+ await self.event_bus.dispatch(ConnectionMadeEvent(protocol=self))
88
+ self.logger.debug("ConnectionMadeEvent dispatched successfully")
89
+ except Exception as e:
90
+ self.logger.error(
91
+ f"Error dispatching ConnectionMadeEvent: {e}", exc_info=True
92
+ )
93
+
94
+ def dataReceived(self, data: bytes) -> None:
95
+ """Handle received data from Twisted.
96
+
97
+ Args:
98
+ data: Raw bytes received from connection.
99
+ """
100
+ task = asyncio.create_task(self._async_dataReceived(data))
101
+ task.add_done_callback(self._on_task_done)
102
+
103
+ async def _async_dataReceived(self, data: bytes) -> None:
104
+ """Async handler for received data."""
105
+ self.logger.debug("dataReceived")
106
+ self.buffer += data
107
+
108
+ while True:
109
+ start = self.buffer.find(b"<")
110
+ if start == -1:
111
+ break
112
+
113
+ end = self.buffer.find(b">", start)
114
+ if end == -1:
115
+ break
116
+
117
+ # <S0123450001F02D12FK>
118
+ # <R0123450001F02D12FK>
119
+ # <E12L01I08MAK>
120
+ frame = self.buffer[start : end + 1] # <S0123450001F02D12FK>
121
+ self.buffer = self.buffer[end + 1 :]
122
+ telegram = frame[1:-1] # S0123450001F02D12FK
123
+ telegram_type = telegram[0:1].decode() # S
124
+ payload = telegram[:-2] # S0123450001F02D12
125
+ checksum = telegram[-2:].decode() # FK
126
+ serial_number = (
127
+ telegram[1:11] if telegram_type in ("S", "R") else b""
128
+ ) # 0123450001
129
+ calculated_checksum = calculate_checksum(payload.decode(encoding="latin-1"))
130
+
131
+ if checksum != calculated_checksum:
132
+ self.logger.debug(
133
+ f"Invalid frame: {frame.decode()} "
134
+ f"checksum: {checksum}, "
135
+ f"expected {calculated_checksum}"
136
+ )
137
+ await self.event_bus.dispatch(
138
+ InvalidTelegramReceivedEvent(
139
+ protocol=self,
140
+ frame=frame.decode(),
141
+ error=f"Invalid checksum ({calculated_checksum} != {checksum})",
142
+ )
143
+ )
144
+ return
145
+
146
+ self.logger.debug(
147
+ f"frameReceived payload: {payload.decode()}, checksum: {checksum}"
148
+ )
149
+ # Dispatch event to bubus with await
150
+ self.logger.debug("frameReceived about to dispatch TelegramReceivedEvent")
151
+ await self.event_bus.dispatch(
152
+ TelegramReceivedEvent(
153
+ protocol=self,
154
+ frame=frame.decode("latin-1"),
155
+ telegram=telegram.decode("latin-1"),
156
+ payload=payload.decode("latin-1"),
157
+ telegram_type=telegram_type,
158
+ serial_number=serial_number,
159
+ checksum=checksum,
160
+ )
161
+ )
162
+ self.logger.debug(
163
+ "frameReceived TelegramReceivedEvent dispatched successfully"
164
+ )
165
+
166
+ def sendFrame(self, data: bytes) -> None:
167
+ """Send telegram frame.
168
+
169
+ Args:
170
+ data: Raw telegram payload (without checksum/framing).
171
+ """
172
+ task = asyncio.create_task(self._async_sendFrame(data))
173
+ task.add_done_callback(self._on_task_done)
174
+
175
+ async def _async_sendFrame(self, data: bytes) -> None:
176
+ """
177
+ Send telegram frame with automatic deduplication.
178
+
179
+ Args:
180
+ data: Raw telegram payload (without checksum/framing)
181
+ """
182
+ # Calculate full frame (add checksum and brackets)
183
+ checksum = calculate_checksum(data.decode())
184
+ frame_data = data.decode() + checksum
185
+ frame = b"<" + frame_data.encode() + b">"
186
+
187
+ # Apply debouncing and send
188
+ await self._send_frame_debounce(frame)
189
+
190
+ async def _send_frame_debounce(self, frame: bytes) -> None:
191
+ """
192
+ Apply debouncing logic and send frame if not a duplicate.
193
+
194
+ Identical frames within debounce_ms window are deduplicated.
195
+ Only the first occurrence is actually sent to the wire.
196
+
197
+ Args:
198
+ frame: Complete telegram frame (with checksum and brackets)
199
+ """
200
+ current_time = time.time()
201
+
202
+ # Check if identical frame was recently sent
203
+ if frame in self.send_queue:
204
+ recent_sends = [
205
+ ts
206
+ for ts in self.send_queue[frame]
207
+ if current_time - ts < (self.debounce_ms / 1000.0)
208
+ ]
209
+
210
+ if recent_sends:
211
+ # Duplicate detected - skip sending
212
+ self.logger.debug(
213
+ f"Debounced duplicate frame: {frame.decode()} "
214
+ f"(sent {len(recent_sends)} times in last {self.debounce_ms}ms)"
215
+ )
216
+ return
217
+
218
+ # Not a duplicate - send it
219
+ await self._send_frame_immediate(frame)
220
+
221
+ # Track this send
222
+ if frame not in self.send_queue:
223
+ self.send_queue[frame] = []
224
+ self.send_queue[frame].append(current_time)
225
+
226
+ # Schedule cleanup of old timestamps
227
+ self._schedule_cleanup()
228
+
229
+ async def _send_frame_immediate(self, frame: bytes) -> None:
230
+ """Actually send frame to TCP transport."""
231
+ if not self.transport:
232
+ self.logger.info("Invalid transport")
233
+ raise IOError("Transport is not open")
234
+
235
+ self.logger.debug(f"Sending frame: {frame.decode()}")
236
+ self.transport.write(frame) # type: ignore
237
+
238
+ def _schedule_cleanup(self) -> None:
239
+ """Schedule cleanup of old timestamp tracking."""
240
+ if self.timer_handle:
241
+ self.timer_handle.cancel()
242
+
243
+ loop = asyncio.get_event_loop()
244
+ self.timer_handle = loop.call_later(
245
+ (self.debounce_ms / 1000.0) * 2, # Cleanup after 2x debounce window
246
+ self._cleanup_old_timestamps,
247
+ )
248
+
249
+ def _cleanup_old_timestamps(self) -> None:
250
+ """Remove old timestamps to prevent memory leak."""
251
+ current_time = time.time()
252
+ cutoff = current_time - (self.debounce_ms / 1000.0)
253
+
254
+ for frame in list(self.send_queue.keys()):
255
+ # Keep only recent timestamps
256
+ self.send_queue[frame] = [
257
+ ts for ts in self.send_queue[frame] if ts >= cutoff
258
+ ]
259
+
260
+ # Remove frame if no recent sends
261
+ if not self.send_queue[frame]:
262
+ del self.send_queue[frame]
263
+
264
+ self.timer_handle = None