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,312 @@
1
+ """Conbus Discover Service for TCP communication with Conbus servers.
2
+
3
+ This service implements a TCP client that connects to Conbus servers and sends
4
+ discover telegrams to find modules on the network.
5
+ """
6
+
7
+ import logging
8
+ from typing import Callable, Optional
9
+
10
+ from xp.models import ConbusDiscoverResponse
11
+ from xp.models.conbus.conbus_discover import DiscoveredDevice
12
+ from xp.models.protocol.conbus_protocol import TelegramReceivedEvent
13
+ from xp.models.telegram.datapoint_type import DataPointType
14
+ from xp.models.telegram.module_type_code import MODULE_TYPE_REGISTRY
15
+ from xp.models.telegram.system_function import SystemFunction
16
+ from xp.models.telegram.telegram_type import TelegramType
17
+ from xp.services.protocol.conbus_event_protocol import ConbusEventProtocol
18
+
19
+
20
+ class ConbusDiscoverService:
21
+ """
22
+ Service for discovering modules on Conbus servers.
23
+
24
+ Uses ConbusProtocol to provide discovery functionality for finding
25
+ modules connected to the Conbus network.
26
+
27
+ Attributes:
28
+ conbus_protocol: Protocol instance for Conbus communication.
29
+ """
30
+
31
+ conbus_protocol: ConbusEventProtocol
32
+
33
+ def __init__(self, conbus_protocol: ConbusEventProtocol) -> None:
34
+ """Initialize the Conbus discover service.
35
+
36
+ Args:
37
+ conbus_protocol: ConbusProtocol.
38
+ """
39
+ self.progress_callback: Optional[Callable[[str], None]] = None
40
+ self.device_discover_callback: Optional[Callable[[DiscoveredDevice], None]] = (
41
+ None
42
+ )
43
+ self.finish_callback: Optional[Callable[[ConbusDiscoverResponse], None]] = None
44
+
45
+ self.conbus_protocol: ConbusEventProtocol = conbus_protocol
46
+ self.conbus_protocol.on_connection_made.connect(self.connection_made)
47
+ self.conbus_protocol.on_telegram_sent.connect(self.telegram_sent)
48
+ self.conbus_protocol.on_telegram_received.connect(self.telegram_received)
49
+ self.conbus_protocol.on_timeout.connect(self.timeout)
50
+ self.conbus_protocol.on_failed.connect(self.failed)
51
+
52
+ self.discovered_device_result = ConbusDiscoverResponse(success=False)
53
+ # Set up logging
54
+ self.logger = logging.getLogger(__name__)
55
+
56
+ def connection_made(self) -> None:
57
+ """Handle connection established event."""
58
+ self.logger.debug("Connection established")
59
+ self.logger.debug("Sending discover telegram")
60
+ self.conbus_protocol.send_telegram(
61
+ telegram_type=TelegramType.SYSTEM,
62
+ serial_number="0000000000",
63
+ system_function=SystemFunction.DISCOVERY,
64
+ data_value="00",
65
+ )
66
+
67
+ def telegram_sent(self, telegram_sent: str) -> None:
68
+ """Handle telegram sent event.
69
+
70
+ Args:
71
+ telegram_sent: The telegram that was sent.
72
+ """
73
+ self.logger.debug(f"Telegram sent: {telegram_sent}")
74
+ self.discovered_device_result.sent_telegram = telegram_sent
75
+
76
+ def telegram_received(self, telegram_received: TelegramReceivedEvent) -> None:
77
+ """Handle telegram received event.
78
+
79
+ Args:
80
+ telegram_received: The telegram received event.
81
+ """
82
+ self.logger.debug(f"Telegram received: {telegram_received}")
83
+ if not self.discovered_device_result.received_telegrams:
84
+ self.discovered_device_result.received_telegrams = []
85
+ self.discovered_device_result.received_telegrams.append(telegram_received.frame)
86
+
87
+ # Check for discovery response
88
+ if (
89
+ telegram_received.checksum_valid
90
+ and telegram_received.telegram_type == TelegramType.REPLY.value
91
+ and telegram_received.payload[11:16] == "F01D"
92
+ and len(telegram_received.payload) == 15
93
+ ):
94
+ self.handle_discovered_device(telegram_received.serial_number)
95
+
96
+ # Check for module type response (F02D07)
97
+ elif (
98
+ telegram_received.checksum_valid
99
+ and telegram_received.telegram_type == TelegramType.REPLY.value
100
+ and telegram_received.payload[11:17] == "F02D07"
101
+ and len(telegram_received.payload) >= 19
102
+ ):
103
+ self.handle_module_type_code_response(
104
+ telegram_received.serial_number, telegram_received.payload[17:19]
105
+ )
106
+ # Check for module type response (F02D00)
107
+ elif (
108
+ telegram_received.checksum_valid
109
+ and telegram_received.telegram_type == TelegramType.REPLY.value
110
+ and telegram_received.payload[11:17] == "F02D00"
111
+ and len(telegram_received.payload) >= 19
112
+ ):
113
+ self.handle_module_type_response(
114
+ telegram_received.serial_number, telegram_received.payload[17:19]
115
+ )
116
+
117
+ else:
118
+ self.logger.debug("Not a discover or module type response")
119
+
120
+ def handle_discovered_device(self, serial_number: str) -> None:
121
+ """Handle discovered device event.
122
+
123
+ Args:
124
+ serial_number: Serial number of the discovered device.
125
+ """
126
+ self.logger.info("discovered_device: %s", serial_number)
127
+ if not self.discovered_device_result.discovered_devices:
128
+ self.discovered_device_result.discovered_devices = []
129
+
130
+ # Add device with module_type as None initially
131
+ device: DiscoveredDevice = {
132
+ "serial_number": serial_number,
133
+ "module_type": None,
134
+ "module_type_code": None,
135
+ "module_type_name": None,
136
+ }
137
+ self.discovered_device_result.discovered_devices.append(device)
138
+
139
+ if self.device_discover_callback:
140
+ self.device_discover_callback(device)
141
+
142
+ # Send READ_DATAPOINT telegram to query module type
143
+ self.logger.debug(f"Sending module type query for {serial_number}")
144
+ self.conbus_protocol.send_telegram(
145
+ telegram_type=TelegramType.SYSTEM,
146
+ serial_number=serial_number,
147
+ system_function=SystemFunction.READ_DATAPOINT,
148
+ data_value=DataPointType.MODULE_TYPE.value,
149
+ )
150
+ if self.progress_callback:
151
+ self.progress_callback(serial_number)
152
+
153
+ def handle_module_type_code_response(
154
+ self, serial_number: str, module_type_code: str
155
+ ) -> None:
156
+ """Handle module type code response and update discovered device.
157
+
158
+ Args:
159
+ serial_number: Serial number of the device.
160
+ module_type_code: Module type code from telegram (e.g., "07", "24").
161
+ """
162
+ self.logger.info(
163
+ f"Received module type code {module_type_code} for {serial_number}"
164
+ )
165
+
166
+ # Convert module type code to name
167
+ code = 0
168
+ try:
169
+ # The telegram format uses decimal values represented as strings
170
+ code = int(module_type_code)
171
+ module_info = MODULE_TYPE_REGISTRY.get(code)
172
+
173
+ if module_info:
174
+ module_type_name = module_info["name"]
175
+ self.logger.debug(
176
+ f"Module type code {module_type_code} ({code}) = {module_type_name}"
177
+ )
178
+ else:
179
+ module_type_name = f"UNKNOWN_{module_type_code}"
180
+ self.logger.warning(
181
+ f"Unknown module type code {module_type_code} ({code})"
182
+ )
183
+
184
+ except ValueError:
185
+ self.logger.error(
186
+ f"Invalid module type code format: {module_type_code} for {serial_number}"
187
+ )
188
+ module_type_name = f"INVALID_{module_type_code}"
189
+
190
+ # Find and update the device in discovered_devices
191
+ if self.discovered_device_result.discovered_devices:
192
+ for device in self.discovered_device_result.discovered_devices:
193
+ if device["serial_number"] == serial_number:
194
+ device["module_type_code"] = code
195
+ device["module_type_name"] = module_type_name
196
+
197
+ if self.device_discover_callback:
198
+ self.device_discover_callback(device)
199
+
200
+ self.logger.debug(
201
+ f"Updated device {serial_number} with module_type {module_type_name}"
202
+ )
203
+ break
204
+
205
+ if self.discovered_device_result.discovered_devices:
206
+ for device in self.discovered_device_result.discovered_devices:
207
+ if not (
208
+ device["serial_number"]
209
+ and device["module_type"]
210
+ and device["module_type_code"]
211
+ and device["module_type_name"]
212
+ ):
213
+ return
214
+
215
+ self.succeed()
216
+
217
+ def handle_module_type_response(self, serial_number: str, module_type: str) -> None:
218
+ """Handle module type response and update discovered device.
219
+
220
+ Args:
221
+ serial_number: Serial number of the device.
222
+ module_type: Module type code from telegram (e.g., "XP33", "XP24").
223
+ """
224
+ self.logger.info(f"Received module type {module_type} for {serial_number}")
225
+
226
+ # Find and update the device in discovered_devices
227
+ if self.discovered_device_result.discovered_devices:
228
+ for device in self.discovered_device_result.discovered_devices:
229
+ if device["serial_number"] == serial_number:
230
+ device["module_type"] = module_type
231
+ self.logger.debug(
232
+ f"Updated device {serial_number} with module_type {module_type}"
233
+ )
234
+ if self.device_discover_callback:
235
+ self.device_discover_callback(device)
236
+
237
+ break
238
+
239
+ self.conbus_protocol.send_telegram(
240
+ telegram_type=TelegramType.SYSTEM,
241
+ serial_number=serial_number,
242
+ system_function=SystemFunction.READ_DATAPOINT,
243
+ data_value=DataPointType.MODULE_TYPE_CODE.value,
244
+ )
245
+
246
+ def timeout(self) -> None:
247
+ """Handle timeout event to stop discovery."""
248
+ timeout = self.conbus_protocol.timeout_seconds
249
+ self.logger.info("Discovery stopped after: %ss", timeout)
250
+ self.discovered_device_result.success = False
251
+ self.discovered_device_result.error = "Discovered device timeout"
252
+ if self.finish_callback:
253
+ self.finish_callback(self.discovered_device_result)
254
+
255
+ self.stop_reactor()
256
+
257
+ def failed(self, message: str) -> None:
258
+ """Handle failed connection event.
259
+
260
+ Args:
261
+ message: Failure message.
262
+ """
263
+ self.logger.debug(f"Failed: {message}")
264
+ self.discovered_device_result.success = False
265
+ self.discovered_device_result.error = message
266
+ if self.finish_callback:
267
+ self.finish_callback(self.discovered_device_result)
268
+
269
+ self.stop_reactor()
270
+
271
+ def succeed(self) -> None:
272
+ """Handle discovered device success event."""
273
+ self.logger.debug("Succeed")
274
+ self.discovered_device_result.success = True
275
+ self.discovered_device_result.error = None
276
+ if self.finish_callback:
277
+ self.finish_callback(self.discovered_device_result)
278
+
279
+ self.stop_reactor()
280
+
281
+ def stop_reactor(self) -> None:
282
+ """Stop reactor."""
283
+ self.logger.info("Stopping reactor")
284
+ self.conbus_protocol.stop_reactor()
285
+
286
+ def start_reactor(self) -> None:
287
+ """Start reactor."""
288
+ self.logger.info("Starting reactor")
289
+ self.conbus_protocol.start_reactor()
290
+
291
+ def run(
292
+ self,
293
+ progress_callback: Callable[[str], None],
294
+ device_discover_callback: Callable[[DiscoveredDevice], None],
295
+ finish_callback: Callable[[ConbusDiscoverResponse], None],
296
+ timeout_seconds: Optional[float] = None,
297
+ ) -> None:
298
+ """Run reactor in dedicated thread with its own event loop.
299
+
300
+ Args:
301
+ progress_callback: Callback for each discovered device.
302
+ device_discover_callback: Callback for each discovered device.
303
+ finish_callback: Callback when discovery completes.
304
+ timeout_seconds: Optional timeout in seconds.
305
+ """
306
+ self.logger.info("Starting discovery")
307
+
308
+ if timeout_seconds:
309
+ self.conbus_protocol.timeout_seconds = timeout_seconds
310
+ self.progress_callback = progress_callback
311
+ self.device_discover_callback = device_discover_callback
312
+ self.finish_callback = finish_callback
@@ -0,0 +1,181 @@
1
+ """Conbus Event Raw Service for sending raw event telegrams.
2
+
3
+ This service implements a TCP client that connects to Conbus servers and sends
4
+ raw event telegrams to simulate button presses on Conbus modules.
5
+ """
6
+
7
+ import logging
8
+ from typing import Callable, Optional
9
+
10
+ from twisted.internet.base import DelayedCall
11
+
12
+ from xp.models import ConbusEventRawResponse
13
+ from xp.models.protocol.conbus_protocol import TelegramReceivedEvent
14
+ from xp.services.protocol.conbus_event_protocol import ConbusEventProtocol
15
+
16
+
17
+ class ConbusEventRawService:
18
+ """Service for sending raw event telegrams to Conbus servers.
19
+
20
+ Uses ConbusEventProtocol to send MAKE/BREAK event sequences to
21
+ simulate button presses on Conbus modules.
22
+
23
+ Attributes:
24
+ conbus_protocol: Protocol instance for Conbus communication.
25
+ """
26
+
27
+ conbus_protocol: ConbusEventProtocol
28
+
29
+ def __init__(self, conbus_protocol: ConbusEventProtocol) -> None:
30
+ """Initialize the Conbus event raw service.
31
+
32
+ Args:
33
+ conbus_protocol: ConbusEventProtocol instance.
34
+ """
35
+ self.progress_callback: Optional[Callable[[str], None]] = None
36
+ self.finish_callback: Optional[Callable[[ConbusEventRawResponse], None]] = None
37
+
38
+ self.conbus_protocol: ConbusEventProtocol = conbus_protocol
39
+ self.conbus_protocol.on_connection_made.connect(self.connection_made)
40
+ self.conbus_protocol.on_telegram_sent.connect(self.telegram_sent)
41
+ self.conbus_protocol.on_telegram_received.connect(self.telegram_received)
42
+ self.conbus_protocol.on_timeout.connect(self.timeout)
43
+ self.conbus_protocol.on_failed.connect(self.failed)
44
+
45
+ self.event_result = ConbusEventRawResponse(success=False)
46
+ self.logger = logging.getLogger(__name__)
47
+
48
+ # Event parameters
49
+ self.module_type_code: int = 0
50
+ self.link_number: int = 0
51
+ self.input_number: int = 0
52
+ self.time_ms: int = 1000
53
+ self.break_event_call: Optional[DelayedCall] = None
54
+
55
+ def connection_made(self) -> None:
56
+ """Handle connection established event."""
57
+ self.logger.debug("Connection established")
58
+ self.logger.debug("Sending MAKE event telegram")
59
+ self._send_make_event()
60
+
61
+ def _send_make_event(self) -> None:
62
+ """Send MAKE event telegram."""
63
+ payload = f"E{self.module_type_code:02d}L{self.link_number:02d}I{self.input_number:02d}M"
64
+ self.logger.debug(f"Sending MAKE event: {payload}")
65
+ self.conbus_protocol.telegram_queue.put_nowait(payload.encode())
66
+ self.conbus_protocol.call_later(0.0, self.conbus_protocol.start_queue_manager)
67
+
68
+ # Schedule BREAK event after delay
69
+ delay_seconds = self.time_ms / 1000.0
70
+ self.break_event_call = self.conbus_protocol.call_later(
71
+ delay_seconds, self._send_break_event
72
+ )
73
+
74
+ def _send_break_event(self) -> None:
75
+ """Send BREAK event telegram."""
76
+ payload = f"E{self.module_type_code:02d}L{self.link_number:02d}I{self.input_number:02d}B"
77
+ self.logger.debug(f"Sending BREAK event: {payload}")
78
+ self.conbus_protocol.telegram_queue.put_nowait(payload.encode())
79
+ self.conbus_protocol.call_later(0.0, self.conbus_protocol.start_queue_manager)
80
+
81
+ def telegram_sent(self, telegram_sent: str) -> None:
82
+ """Handle telegram sent event.
83
+
84
+ Args:
85
+ telegram_sent: The telegram that was sent.
86
+ """
87
+ self.logger.debug(f"Telegram sent: {telegram_sent}")
88
+ if self.event_result.sent_telegrams is None:
89
+ self.event_result.sent_telegrams = []
90
+ self.event_result.sent_telegrams.append(telegram_sent)
91
+
92
+ def telegram_received(self, telegram_received: TelegramReceivedEvent) -> None:
93
+ """Handle telegram received event.
94
+
95
+ Args:
96
+ telegram_received: The telegram received event.
97
+ """
98
+ self.logger.debug(f"Telegram received: {telegram_received.frame}")
99
+ if self.event_result.received_telegrams is None:
100
+ self.event_result.received_telegrams = []
101
+ self.event_result.received_telegrams.append(telegram_received.frame)
102
+
103
+ # Display progress - show ALL received telegrams
104
+ if self.progress_callback:
105
+ self.progress_callback(telegram_received.frame)
106
+
107
+ def timeout(self) -> None:
108
+ """Handle timeout event.
109
+
110
+ Timeout is the normal/expected way to finish this service.
111
+ """
112
+ timeout_seconds = self.conbus_protocol.timeout_seconds
113
+ self.logger.info("Event raw finished after timeout: %ss", timeout_seconds)
114
+ self.event_result.success = True
115
+ self.event_result.error = None
116
+ if self.finish_callback:
117
+ self.finish_callback(self.event_result)
118
+
119
+ self.stop_reactor()
120
+
121
+ def failed(self, message: str) -> None:
122
+ """Handle failed connection event.
123
+
124
+ Args:
125
+ message: Failure message.
126
+ """
127
+ self.logger.debug(f"Failed: {message}")
128
+ self.event_result.success = False
129
+ self.event_result.error = message
130
+ if self.finish_callback:
131
+ self.finish_callback(self.event_result)
132
+
133
+ self.stop_reactor()
134
+
135
+ def stop_reactor(self) -> None:
136
+ """Stop reactor."""
137
+ self.logger.info("Stopping reactor")
138
+ # Cancel break event call if it's still pending
139
+ if self.break_event_call and self.break_event_call.active():
140
+ self.break_event_call.cancel()
141
+ self.conbus_protocol.stop_reactor()
142
+
143
+ def start_reactor(self) -> None:
144
+ """Start reactor."""
145
+ self.logger.info("Starting reactor")
146
+ self.conbus_protocol.start_reactor()
147
+
148
+ def run(
149
+ self,
150
+ module_type_code: int,
151
+ link_number: int,
152
+ input_number: int,
153
+ time_ms: int,
154
+ progress_callback: Optional[Callable[[str], None]],
155
+ finish_callback: Callable[[ConbusEventRawResponse], None],
156
+ timeout_seconds: int = 5,
157
+ ) -> None:
158
+ """Run reactor in dedicated thread with its own event loop.
159
+
160
+ Args:
161
+ module_type_code: Module type code (numeric, e.g., 2 for CP20, 33 for XP33).
162
+ link_number: Link number (0-99).
163
+ input_number: Input number (0-9).
164
+ time_ms: Delay in milliseconds between MAKE and BREAK events.
165
+ progress_callback: Callback for progress updates (received telegrams).
166
+ finish_callback: Callback when operation completes.
167
+ timeout_seconds: Timeout in seconds (default: 5).
168
+ """
169
+ self.logger.info(
170
+ f"Starting event raw: module={module_type_code}, "
171
+ f"link={link_number}, input={input_number}, time={time_ms}ms"
172
+ )
173
+
174
+ self.module_type_code = module_type_code
175
+ self.link_number = link_number
176
+ self.input_number = input_number
177
+ self.time_ms = time_ms
178
+
179
+ self.conbus_protocol.timeout_seconds = timeout_seconds
180
+ self.progress_callback = progress_callback
181
+ self.finish_callback = finish_callback
@@ -0,0 +1,194 @@
1
+ """Conbus Output Service for sending action telegrams to Conbus modules.
2
+
3
+ This service handles sending action telegrams (ON/OFF) to module outputs
4
+ and processing ACK/NAK responses.
5
+ """
6
+
7
+ import logging
8
+ from datetime import datetime
9
+ from typing import Callable, Optional
10
+
11
+ from twisted.internet.posixbase import PosixReactorBase
12
+
13
+ from xp.models import ConbusClientConfig
14
+ from xp.models.conbus.conbus_output import ConbusOutputResponse
15
+ from xp.models.protocol.conbus_protocol import TelegramReceivedEvent
16
+ from xp.models.telegram.action_type import ActionType
17
+ from xp.models.telegram.output_telegram import OutputTelegram
18
+ from xp.models.telegram.system_function import SystemFunction
19
+ from xp.models.telegram.telegram_type import TelegramType
20
+ from xp.services.protocol import ConbusProtocol
21
+ from xp.services.telegram.telegram_output_service import (
22
+ TelegramOutputService,
23
+ XPOutputError,
24
+ )
25
+
26
+
27
+ class ConbusOutputError(Exception):
28
+ """Raised when Conbus output operations fail."""
29
+
30
+ pass
31
+
32
+
33
+ class ConbusOutputService(ConbusProtocol):
34
+ """
35
+ Service for sending action telegrams to Conbus module outputs.
36
+
37
+ Manages action telegram transmission (ON/OFF) and processes
38
+ ACK/NAK responses from modules.
39
+ """
40
+
41
+ def __init__(
42
+ self,
43
+ telegram_output_service: TelegramOutputService,
44
+ cli_config: ConbusClientConfig,
45
+ reactor: PosixReactorBase,
46
+ ):
47
+ """Initialize the Conbus output service.
48
+
49
+ Args:
50
+ telegram_output_service: TelegramOutputService for telegram generation/parsing.
51
+ cli_config: Conbus client configuration.
52
+ reactor: Twisted reactor for async operations.
53
+ """
54
+ super().__init__(cli_config, reactor)
55
+ self.telegram_output_service = telegram_output_service
56
+ self.serial_number: str = ""
57
+ self.output_number: int = 0
58
+ self.action_type: ActionType = ActionType.ON_RELEASE
59
+ self.finish_callback: Optional[Callable[[ConbusOutputResponse], None]] = None
60
+ self.service_response: ConbusOutputResponse = ConbusOutputResponse(
61
+ success=False,
62
+ serial_number=self.serial_number,
63
+ output_number=self.output_number,
64
+ action_type=self.action_type,
65
+ timestamp=datetime.now(),
66
+ )
67
+
68
+ # Set up logging
69
+ self.logger = logging.getLogger(__name__)
70
+
71
+ def connection_established(self) -> None:
72
+ """Handle connection established event."""
73
+ self.logger.debug(
74
+ f"Connection established, sending action {self.action_type} to output {self.output_number}."
75
+ )
76
+
77
+ # Validate parameters before sending
78
+ try:
79
+ self.telegram_output_service.validate_output_number(self.output_number)
80
+ self.telegram_output_service.validate_serial_number(self.serial_number)
81
+ except XPOutputError as e:
82
+ self.failed(str(e))
83
+ return
84
+
85
+ # Send F27D{output:02d}{action} telegram
86
+ # F27 = ACTION, D = data with output number and action type
87
+ self.send_telegram(
88
+ telegram_type=TelegramType.SYSTEM,
89
+ serial_number=self.serial_number,
90
+ system_function=SystemFunction.ACTION,
91
+ data_value=f"{self.output_number:02d}{self.action_type.value}",
92
+ )
93
+
94
+ def telegram_sent(self, telegram_sent: str) -> None:
95
+ """Handle telegram sent event.
96
+
97
+ Args:
98
+ telegram_sent: The telegram that was sent.
99
+ """
100
+ self.service_response.sent_telegram = telegram_sent
101
+
102
+ def telegram_received(self, telegram_received: TelegramReceivedEvent) -> None:
103
+ """Handle telegram received event.
104
+
105
+ Args:
106
+ telegram_received: The telegram received event.
107
+ """
108
+ self.logger.debug(f"Telegram received: {telegram_received}")
109
+
110
+ if not self.service_response.received_telegrams:
111
+ self.service_response.received_telegrams = []
112
+ self.service_response.received_telegrams.append(telegram_received.frame)
113
+
114
+ if (
115
+ not telegram_received.checksum_valid
116
+ or telegram_received.telegram_type != TelegramType.REPLY
117
+ or telegram_received.serial_number != self.serial_number
118
+ ):
119
+ self.logger.debug("Not a reply for our serial number")
120
+ return
121
+
122
+ # Parse the reply telegram to get ACK/NAK
123
+ output_telegram = self.telegram_output_service.parse_reply_telegram(
124
+ telegram_received.frame
125
+ )
126
+
127
+ if output_telegram and output_telegram.system_function in (
128
+ SystemFunction.ACK,
129
+ SystemFunction.NAK,
130
+ ):
131
+ self.logger.debug(f"Received {output_telegram.system_function} response")
132
+ self.succeed(output_telegram)
133
+ else:
134
+ self.logger.debug(
135
+ f"Unexpected system function: {output_telegram.system_function}"
136
+ )
137
+
138
+ def succeed(self, output_telegram: OutputTelegram) -> None:
139
+ """Handle successful output action.
140
+
141
+ Args:
142
+ output_telegram: The output telegram received as response.
143
+ """
144
+ self.logger.debug("Successfully sent action to output")
145
+ self.service_response.success = True
146
+ self.service_response.timestamp = datetime.now()
147
+ self.service_response.serial_number = self.serial_number
148
+ self.service_response.output_number = self.output_number
149
+ self.service_response.action_type = self.action_type
150
+ self.service_response.output_telegram = output_telegram
151
+ if self.finish_callback:
152
+ self.finish_callback(self.service_response)
153
+
154
+ def failed(self, message: str) -> None:
155
+ """Handle failed connection event.
156
+
157
+ Args:
158
+ message: Failure message.
159
+ """
160
+ self.logger.debug(f"Failed with message: {message}")
161
+ self.service_response.success = False
162
+ self.service_response.timestamp = datetime.now()
163
+ self.service_response.serial_number = self.serial_number
164
+ self.service_response.output_number = self.output_number
165
+ self.service_response.action_type = self.action_type
166
+ self.service_response.error = message
167
+ if self.finish_callback:
168
+ self.finish_callback(self.service_response)
169
+
170
+ def send_action(
171
+ self,
172
+ serial_number: str,
173
+ output_number: int,
174
+ action_type: ActionType,
175
+ finish_callback: Callable[[ConbusOutputResponse], None],
176
+ timeout_seconds: Optional[float] = None,
177
+ ) -> None:
178
+ """Send an action telegram to a module output.
179
+
180
+ Args:
181
+ serial_number: 10-digit module serial number.
182
+ output_number: Output number (0-99).
183
+ action_type: Action to perform (ON_RELEASE, OFF_PRESS, etc.).
184
+ finish_callback: Callback function to call when operation completes.
185
+ timeout_seconds: Optional timeout in seconds.
186
+ """
187
+ self.logger.info("Starting send_action")
188
+ if timeout_seconds:
189
+ self.timeout_seconds = timeout_seconds
190
+ self.serial_number = serial_number
191
+ self.output_number = output_number
192
+ self.action_type = action_type
193
+ self.finish_callback = finish_callback
194
+ self.start_reactor()