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.
- conson_xp-1.18.0.dist-info/METADATA +412 -0
- conson_xp-1.18.0.dist-info/RECORD +176 -0
- conson_xp-1.18.0.dist-info/WHEEL +4 -0
- conson_xp-1.18.0.dist-info/entry_points.txt +5 -0
- conson_xp-1.18.0.dist-info/licenses/LICENSE +29 -0
- xp/__init__.py +9 -0
- xp/cli/__init__.py +5 -0
- xp/cli/__main__.py +6 -0
- xp/cli/commands/__init__.py +153 -0
- xp/cli/commands/conbus/__init__.py +25 -0
- xp/cli/commands/conbus/conbus.py +128 -0
- xp/cli/commands/conbus/conbus_actiontable_commands.py +233 -0
- xp/cli/commands/conbus/conbus_autoreport_commands.py +108 -0
- xp/cli/commands/conbus/conbus_blink_commands.py +163 -0
- xp/cli/commands/conbus/conbus_config_commands.py +29 -0
- xp/cli/commands/conbus/conbus_custom_commands.py +57 -0
- xp/cli/commands/conbus/conbus_datapoint_commands.py +113 -0
- xp/cli/commands/conbus/conbus_discover_commands.py +61 -0
- xp/cli/commands/conbus/conbus_event_commands.py +81 -0
- xp/cli/commands/conbus/conbus_lightlevel_commands.py +207 -0
- xp/cli/commands/conbus/conbus_linknumber_commands.py +102 -0
- xp/cli/commands/conbus/conbus_modulenumber_commands.py +104 -0
- xp/cli/commands/conbus/conbus_msactiontable_commands.py +94 -0
- xp/cli/commands/conbus/conbus_output_commands.py +163 -0
- xp/cli/commands/conbus/conbus_raw_commands.py +62 -0
- xp/cli/commands/conbus/conbus_receive_commands.py +59 -0
- xp/cli/commands/conbus/conbus_scan_commands.py +58 -0
- xp/cli/commands/file_commands.py +186 -0
- xp/cli/commands/homekit/__init__.py +3 -0
- xp/cli/commands/homekit/homekit.py +118 -0
- xp/cli/commands/homekit/homekit_start_commands.py +43 -0
- xp/cli/commands/module_commands.py +187 -0
- xp/cli/commands/reverse_proxy_commands.py +178 -0
- xp/cli/commands/server/__init__.py +3 -0
- xp/cli/commands/server/server_commands.py +135 -0
- xp/cli/commands/telegram/__init__.py +5 -0
- xp/cli/commands/telegram/telegram.py +41 -0
- xp/cli/commands/telegram/telegram_blink_commands.py +79 -0
- xp/cli/commands/telegram/telegram_checksum_commands.py +112 -0
- xp/cli/commands/telegram/telegram_discover_commands.py +41 -0
- xp/cli/commands/telegram/telegram_linknumber_commands.py +86 -0
- xp/cli/commands/telegram/telegram_parse_commands.py +75 -0
- xp/cli/commands/telegram/telegram_version_commands.py +52 -0
- xp/cli/main.py +87 -0
- xp/cli/utils/__init__.py +1 -0
- xp/cli/utils/click_tree.py +57 -0
- xp/cli/utils/datapoint_type_choice.py +57 -0
- xp/cli/utils/decorators.py +351 -0
- xp/cli/utils/error_handlers.py +201 -0
- xp/cli/utils/formatters.py +312 -0
- xp/cli/utils/module_type_choice.py +56 -0
- xp/cli/utils/serial_number_type.py +52 -0
- xp/cli/utils/system_function_choice.py +57 -0
- xp/cli/utils/xp_module_type.py +53 -0
- xp/connection/__init__.py +13 -0
- xp/connection/exceptions.py +22 -0
- xp/models/__init__.py +36 -0
- xp/models/actiontable/__init__.py +1 -0
- xp/models/actiontable/actiontable.py +43 -0
- xp/models/actiontable/msactiontable_xp20.py +53 -0
- xp/models/actiontable/msactiontable_xp24.py +58 -0
- xp/models/actiontable/msactiontable_xp33.py +65 -0
- xp/models/conbus/__init__.py +1 -0
- xp/models/conbus/conbus.py +87 -0
- xp/models/conbus/conbus_autoreport.py +67 -0
- xp/models/conbus/conbus_blink.py +80 -0
- xp/models/conbus/conbus_client_config.py +55 -0
- xp/models/conbus/conbus_connection_status.py +40 -0
- xp/models/conbus/conbus_custom.py +58 -0
- xp/models/conbus/conbus_datapoint.py +89 -0
- xp/models/conbus/conbus_discover.py +64 -0
- xp/models/conbus/conbus_event_raw.py +47 -0
- xp/models/conbus/conbus_lightlevel.py +52 -0
- xp/models/conbus/conbus_linknumber.py +54 -0
- xp/models/conbus/conbus_output.py +57 -0
- xp/models/conbus/conbus_raw.py +45 -0
- xp/models/conbus/conbus_receive.py +42 -0
- xp/models/conbus/conbus_writeconfig.py +60 -0
- xp/models/homekit/__init__.py +1 -0
- xp/models/homekit/homekit_accessory.py +35 -0
- xp/models/homekit/homekit_config.py +106 -0
- xp/models/homekit/homekit_conson_config.py +86 -0
- xp/models/log_entry.py +130 -0
- xp/models/protocol/__init__.py +1 -0
- xp/models/protocol/conbus_protocol.py +312 -0
- xp/models/response.py +42 -0
- xp/models/telegram/__init__.py +1 -0
- xp/models/telegram/action_type.py +31 -0
- xp/models/telegram/datapoint_type.py +82 -0
- xp/models/telegram/event_telegram.py +140 -0
- xp/models/telegram/event_type.py +15 -0
- xp/models/telegram/input_action_type.py +69 -0
- xp/models/telegram/input_type.py +17 -0
- xp/models/telegram/module_type.py +188 -0
- xp/models/telegram/module_type_code.py +205 -0
- xp/models/telegram/output_telegram.py +103 -0
- xp/models/telegram/reply_telegram.py +297 -0
- xp/models/telegram/system_function.py +116 -0
- xp/models/telegram/system_telegram.py +94 -0
- xp/models/telegram/telegram.py +28 -0
- xp/models/telegram/telegram_type.py +19 -0
- xp/models/telegram/timeparam_type.py +51 -0
- xp/models/write_config_type.py +33 -0
- xp/services/__init__.py +26 -0
- xp/services/actiontable/__init__.py +1 -0
- xp/services/actiontable/actiontable_serializer.py +273 -0
- xp/services/actiontable/msactiontable_serializer.py +7 -0
- xp/services/actiontable/msactiontable_xp20_serializer.py +169 -0
- xp/services/actiontable/msactiontable_xp24_serializer.py +120 -0
- xp/services/actiontable/msactiontable_xp33_serializer.py +239 -0
- xp/services/conbus/__init__.py +1 -0
- xp/services/conbus/actiontable/__init__.py +1 -0
- xp/services/conbus/actiontable/actiontable_download_service.py +158 -0
- xp/services/conbus/actiontable/actiontable_list_service.py +91 -0
- xp/services/conbus/actiontable/actiontable_show_service.py +89 -0
- xp/services/conbus/actiontable/actiontable_upload_service.py +211 -0
- xp/services/conbus/actiontable/msactiontable_service.py +232 -0
- xp/services/conbus/conbus_blink_all_service.py +181 -0
- xp/services/conbus/conbus_blink_service.py +158 -0
- xp/services/conbus/conbus_custom_service.py +156 -0
- xp/services/conbus/conbus_datapoint_queryall_service.py +182 -0
- xp/services/conbus/conbus_datapoint_service.py +170 -0
- xp/services/conbus/conbus_discover_service.py +312 -0
- xp/services/conbus/conbus_event_raw_service.py +181 -0
- xp/services/conbus/conbus_output_service.py +194 -0
- xp/services/conbus/conbus_raw_service.py +122 -0
- xp/services/conbus/conbus_receive_service.py +115 -0
- xp/services/conbus/conbus_scan_service.py +150 -0
- xp/services/conbus/write_config_service.py +194 -0
- xp/services/homekit/__init__.py +1 -0
- xp/services/homekit/homekit_cache_service.py +307 -0
- xp/services/homekit/homekit_conbus_service.py +93 -0
- xp/services/homekit/homekit_config_validator.py +310 -0
- xp/services/homekit/homekit_conson_validator.py +121 -0
- xp/services/homekit/homekit_dimminglight.py +182 -0
- xp/services/homekit/homekit_dimminglight_service.py +148 -0
- xp/services/homekit/homekit_hap_service.py +342 -0
- xp/services/homekit/homekit_lightbulb.py +120 -0
- xp/services/homekit/homekit_lightbulb_service.py +86 -0
- xp/services/homekit/homekit_module_service.py +56 -0
- xp/services/homekit/homekit_outlet.py +168 -0
- xp/services/homekit/homekit_outlet_service.py +121 -0
- xp/services/homekit/homekit_service.py +359 -0
- xp/services/log_file_service.py +309 -0
- xp/services/module_type_service.py +257 -0
- xp/services/protocol/__init__.py +21 -0
- xp/services/protocol/conbus_event_protocol.py +360 -0
- xp/services/protocol/conbus_protocol.py +318 -0
- xp/services/protocol/protocol_factory.py +78 -0
- xp/services/protocol/telegram_protocol.py +264 -0
- xp/services/reverse_proxy_service.py +435 -0
- xp/services/server/__init__.py +1 -0
- xp/services/server/base_server_service.py +366 -0
- xp/services/server/cp20_server_service.py +65 -0
- xp/services/server/device_service_factory.py +94 -0
- xp/services/server/server_service.py +428 -0
- xp/services/server/xp130_server_service.py +67 -0
- xp/services/server/xp20_server_service.py +92 -0
- xp/services/server/xp230_server_service.py +58 -0
- xp/services/server/xp24_server_service.py +245 -0
- xp/services/server/xp33_server_service.py +535 -0
- xp/services/telegram/__init__.py +1 -0
- xp/services/telegram/telegram_blink_service.py +138 -0
- xp/services/telegram/telegram_checksum_service.py +149 -0
- xp/services/telegram/telegram_datapoint_service.py +82 -0
- xp/services/telegram/telegram_discover_service.py +277 -0
- xp/services/telegram/telegram_link_number_service.py +216 -0
- xp/services/telegram/telegram_output_service.py +322 -0
- xp/services/telegram/telegram_service.py +380 -0
- xp/services/telegram/telegram_version_service.py +288 -0
- xp/utils/__init__.py +12 -0
- xp/utils/checksum.py +61 -0
- xp/utils/dependencies.py +531 -0
- xp/utils/event_helper.py +31 -0
- xp/utils/serialization.py +205 -0
- 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()
|