conson-xp 0.11.21__py3-none-any.whl → 1.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {conson_xp-0.11.21.dist-info → conson_xp-1.0.1.dist-info}/METADATA +1 -1
- {conson_xp-0.11.21.dist-info → conson_xp-1.0.1.dist-info}/RECORD +20 -21
- xp/__init__.py +1 -1
- xp/cli/commands/conbus/conbus_config_commands.py +3 -12
- xp/cli/commands/conbus/conbus_linknumber_commands.py +24 -11
- xp/cli/commands/conbus/conbus_output_commands.py +44 -18
- xp/models/conbus/conbus.py +12 -11
- xp/models/conbus/conbus_linknumber.py +1 -1
- xp/services/conbus/conbus_autoreport_get_service.py +29 -80
- xp/services/conbus/conbus_datapoint_service.py +6 -1
- xp/services/conbus/conbus_lightlevel_get_service.py +36 -84
- xp/services/conbus/conbus_linknumber_get_service.py +86 -0
- xp/services/conbus/conbus_linknumber_set_service.py +155 -0
- xp/services/conbus/conbus_output_service.py +129 -92
- xp/services/conbus/conbus_scan_service.py +94 -98
- xp/services/protocol/conbus_protocol.py +1 -0
- xp/utils/dependencies.py +21 -44
- xp/services/conbus/conbus_connection_pool.py +0 -148
- xp/services/conbus/conbus_linknumber_service.py +0 -197
- xp/services/conbus/conbus_service.py +0 -306
- {conson_xp-0.11.21.dist-info → conson_xp-1.0.1.dist-info}/WHEEL +0 -0
- {conson_xp-0.11.21.dist-info → conson_xp-1.0.1.dist-info}/entry_points.txt +0 -0
- {conson_xp-0.11.21.dist-info → conson_xp-1.0.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -9,18 +9,14 @@ from typing import Callable, Optional
|
|
|
9
9
|
|
|
10
10
|
from twisted.internet.posixbase import PosixReactorBase
|
|
11
11
|
|
|
12
|
-
from xp.models import ConbusClientConfig
|
|
12
|
+
from xp.models import ConbusClientConfig, ConbusDatapointResponse
|
|
13
13
|
from xp.models.conbus.conbus_lightlevel import ConbusLightlevelResponse
|
|
14
|
-
from xp.models.protocol.conbus_protocol import TelegramReceivedEvent
|
|
15
14
|
from xp.models.telegram.datapoint_type import DataPointType
|
|
16
|
-
from xp.
|
|
17
|
-
from xp.models.telegram.system_function import SystemFunction
|
|
18
|
-
from xp.models.telegram.telegram_type import TelegramType
|
|
19
|
-
from xp.services.protocol import ConbusProtocol
|
|
15
|
+
from xp.services.conbus.conbus_datapoint_service import ConbusDatapointService
|
|
20
16
|
from xp.services.telegram.telegram_service import TelegramService
|
|
21
17
|
|
|
22
18
|
|
|
23
|
-
class ConbusLightlevelGetService(
|
|
19
|
+
class ConbusLightlevelGetService(ConbusDatapointService):
|
|
24
20
|
"""
|
|
25
21
|
Service for receiving telegrams from Conbus servers.
|
|
26
22
|
|
|
@@ -35,93 +31,46 @@ class ConbusLightlevelGetService(ConbusProtocol):
|
|
|
35
31
|
reactor: PosixReactorBase,
|
|
36
32
|
) -> None:
|
|
37
33
|
"""Initialize the Conbus client send service"""
|
|
38
|
-
super().__init__(cli_config, reactor)
|
|
39
|
-
self.telegram_service = telegram_service
|
|
40
|
-
self.serial_number: str = ""
|
|
34
|
+
super().__init__(telegram_service, cli_config, reactor)
|
|
41
35
|
self.output_number: int = 0
|
|
42
|
-
self.
|
|
36
|
+
self.service_callback: Optional[Callable[[ConbusLightlevelResponse], None]] = (
|
|
43
37
|
None
|
|
44
38
|
)
|
|
45
|
-
self.service_response: ConbusLightlevelResponse = ConbusLightlevelResponse(
|
|
46
|
-
success=False,
|
|
47
|
-
serial_number=self.serial_number,
|
|
48
|
-
output_number=self.output_number,
|
|
49
|
-
level=0,
|
|
50
|
-
timestamp=datetime.now(),
|
|
51
|
-
)
|
|
52
39
|
|
|
53
40
|
# Set up logging
|
|
54
41
|
self.logger = logging.getLogger(__name__)
|
|
55
42
|
|
|
56
|
-
def
|
|
57
|
-
self
|
|
58
|
-
|
|
59
|
-
telegram_type=TelegramType.SYSTEM,
|
|
60
|
-
serial_number=self.serial_number,
|
|
61
|
-
system_function=SystemFunction.READ_DATAPOINT,
|
|
62
|
-
data_value=str(DataPointType.MODULE_LIGHT_LEVEL.value),
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
def telegram_sent(self, telegram_sent: str) -> None:
|
|
66
|
-
self.service_response.sent_telegram = telegram_sent
|
|
67
|
-
|
|
68
|
-
def telegram_received(self, telegram_received: TelegramReceivedEvent) -> None:
|
|
69
|
-
|
|
70
|
-
self.logger.debug(f"Telegram received: {telegram_received}")
|
|
71
|
-
if not self.service_response.received_telegrams:
|
|
72
|
-
self.service_response.received_telegrams = []
|
|
73
|
-
self.service_response.received_telegrams.append(telegram_received.frame)
|
|
43
|
+
def finish_service_callback(
|
|
44
|
+
self, datapoint_response: ConbusDatapointResponse
|
|
45
|
+
) -> None:
|
|
74
46
|
|
|
75
|
-
|
|
76
|
-
not telegram_received.checksum_valid
|
|
77
|
-
or telegram_received.telegram_type != TelegramType.REPLY
|
|
78
|
-
or telegram_received.serial_number != self.serial_number
|
|
79
|
-
):
|
|
80
|
-
self.logger.debug("Not a reply")
|
|
81
|
-
return
|
|
47
|
+
self.logger.debug("Parsing datapoint response")
|
|
82
48
|
|
|
83
|
-
|
|
84
|
-
|
|
49
|
+
level = 0
|
|
50
|
+
if datapoint_response.success and datapoint_response.datapoint_telegram:
|
|
51
|
+
for output_data in datapoint_response.datapoint_telegram.data_value.split(
|
|
52
|
+
","
|
|
53
|
+
):
|
|
54
|
+
if ":" in output_data:
|
|
55
|
+
output_str, level_str = output_data.split(":")
|
|
56
|
+
if int(output_str) == self.output_number:
|
|
57
|
+
level_str = level_str.replace("[%]", "")
|
|
58
|
+
level = int(level_str)
|
|
59
|
+
break
|
|
60
|
+
|
|
61
|
+
service_response = ConbusLightlevelResponse(
|
|
62
|
+
success=datapoint_response.success,
|
|
63
|
+
serial_number=self.serial_number,
|
|
64
|
+
output_number=self.output_number,
|
|
65
|
+
level=level,
|
|
66
|
+
error=datapoint_response.error,
|
|
67
|
+
sent_telegram=datapoint_response.sent_telegram,
|
|
68
|
+
received_telegrams=datapoint_response.received_telegrams,
|
|
69
|
+
timestamp=datetime.now(),
|
|
85
70
|
)
|
|
86
71
|
|
|
87
|
-
if
|
|
88
|
-
|
|
89
|
-
or reply_telegram.system_function != SystemFunction.READ_DATAPOINT
|
|
90
|
-
or reply_telegram.datapoint_type != DataPointType.MODULE_LIGHT_LEVEL
|
|
91
|
-
):
|
|
92
|
-
self.logger.debug("Not a lightlevel telegram")
|
|
93
|
-
return
|
|
94
|
-
|
|
95
|
-
self.logger.debug("Received lightlevel status telegram")
|
|
96
|
-
lightlevel = self.extract_lightlevel(reply_telegram)
|
|
97
|
-
self.succeed(lightlevel)
|
|
98
|
-
|
|
99
|
-
def extract_lightlevel(self, reply_telegram: ReplyTelegram) -> int:
|
|
100
|
-
level = 0
|
|
101
|
-
for output_data in reply_telegram.data_value.split(","):
|
|
102
|
-
if ":" in output_data:
|
|
103
|
-
output_str, level_str = output_data.split(":")
|
|
104
|
-
if int(output_str) == self.output_number:
|
|
105
|
-
level_str = level_str.replace("[%]", "")
|
|
106
|
-
level = int(level_str)
|
|
107
|
-
break
|
|
108
|
-
return level
|
|
109
|
-
|
|
110
|
-
def succeed(self, lightlevel: int) -> None:
|
|
111
|
-
self.service_response.success = True
|
|
112
|
-
self.service_response.timestamp = datetime.now()
|
|
113
|
-
self.service_response.serial_number = self.serial_number
|
|
114
|
-
self.service_response.level = lightlevel
|
|
115
|
-
if self.finish_callback:
|
|
116
|
-
self.finish_callback(self.service_response)
|
|
117
|
-
|
|
118
|
-
def failed(self, message: str) -> None:
|
|
119
|
-
self.logger.debug(f"Failed with message: {message}")
|
|
120
|
-
self.service_response.success = False
|
|
121
|
-
self.service_response.timestamp = datetime.now()
|
|
122
|
-
self.service_response.error = message
|
|
123
|
-
if self.finish_callback:
|
|
124
|
-
self.finish_callback(self.service_response)
|
|
72
|
+
if self.service_callback:
|
|
73
|
+
self.service_callback(service_response)
|
|
125
74
|
|
|
126
75
|
def get_light_level(
|
|
127
76
|
self,
|
|
@@ -143,7 +92,10 @@ class ConbusLightlevelGetService(ConbusProtocol):
|
|
|
143
92
|
self.logger.info("Starting get_lightlevel_status")
|
|
144
93
|
if timeout_seconds:
|
|
145
94
|
self.timeout_seconds = timeout_seconds
|
|
146
|
-
self.finish_callback = finish_callback
|
|
147
95
|
self.serial_number = serial_number
|
|
148
96
|
self.output_number = output_number
|
|
97
|
+
self.datapoint_type = DataPointType.MODULE_LIGHT_LEVEL
|
|
98
|
+
|
|
99
|
+
self.finish_callback = self.finish_service_callback
|
|
100
|
+
self.service_callback = finish_callback
|
|
149
101
|
self.start_reactor()
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Conbus Link Number Service for setting module link numbers.
|
|
2
|
+
|
|
3
|
+
This service handles setting link numbers for modules through Conbus telegrams.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
from typing import Callable, Optional
|
|
8
|
+
|
|
9
|
+
from twisted.internet.posixbase import PosixReactorBase
|
|
10
|
+
|
|
11
|
+
from xp.models import ConbusClientConfig, ConbusDatapointResponse
|
|
12
|
+
from xp.models.conbus.conbus_linknumber import ConbusLinknumberResponse
|
|
13
|
+
from xp.models.telegram.datapoint_type import DataPointType
|
|
14
|
+
from xp.services.conbus.conbus_datapoint_service import ConbusDatapointService
|
|
15
|
+
from xp.services.telegram.telegram_service import TelegramService
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ConbusLinknumberGetService(ConbusDatapointService):
|
|
19
|
+
"""
|
|
20
|
+
Service for receiving telegrams from Conbus servers.
|
|
21
|
+
|
|
22
|
+
Uses composition with ConbusService to provide receive-only functionality
|
|
23
|
+
for collecting waiting event telegrams from the server.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
telegram_service: TelegramService,
|
|
29
|
+
cli_config: ConbusClientConfig,
|
|
30
|
+
reactor: PosixReactorBase,
|
|
31
|
+
) -> None:
|
|
32
|
+
"""Initialize the Conbus client send service"""
|
|
33
|
+
super().__init__(telegram_service, cli_config, reactor)
|
|
34
|
+
self.service_callback: Optional[Callable[[ConbusLinknumberResponse], None]] = (
|
|
35
|
+
None
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# Set up logging
|
|
39
|
+
self.logger = logging.getLogger(__name__)
|
|
40
|
+
|
|
41
|
+
def finish_service_callback(
|
|
42
|
+
self, datapoint_response: ConbusDatapointResponse
|
|
43
|
+
) -> None:
|
|
44
|
+
|
|
45
|
+
self.logger.debug("Parsing datapoint response")
|
|
46
|
+
link_number_value = 0
|
|
47
|
+
if datapoint_response.success and datapoint_response.datapoint_telegram:
|
|
48
|
+
link_number_value = int(datapoint_response.datapoint_telegram.data_value)
|
|
49
|
+
|
|
50
|
+
linknumber_response = ConbusLinknumberResponse(
|
|
51
|
+
success=datapoint_response.success,
|
|
52
|
+
result="SUCCESS" if datapoint_response.success else "FAILURE",
|
|
53
|
+
link_number=link_number_value,
|
|
54
|
+
serial_number=self.serial_number,
|
|
55
|
+
error=datapoint_response.error,
|
|
56
|
+
sent_telegram=datapoint_response.sent_telegram,
|
|
57
|
+
received_telegrams=datapoint_response.received_telegrams,
|
|
58
|
+
timestamp=datapoint_response.timestamp,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if self.service_callback:
|
|
62
|
+
self.service_callback(linknumber_response)
|
|
63
|
+
|
|
64
|
+
def get_linknumber(
|
|
65
|
+
self,
|
|
66
|
+
serial_number: str,
|
|
67
|
+
finish_callback: Callable[[ConbusLinknumberResponse], None],
|
|
68
|
+
timeout_seconds: Optional[float] = None,
|
|
69
|
+
) -> None:
|
|
70
|
+
"""
|
|
71
|
+
Get the current auto report status for a specific module.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
:param serial_number: 10-digit module serial number
|
|
75
|
+
:param finish_callback: callback function to call when the linknumber status is
|
|
76
|
+
:param timeout_seconds: timeout in seconds
|
|
77
|
+
|
|
78
|
+
"""
|
|
79
|
+
self.logger.info("Starting get_linknumber")
|
|
80
|
+
if timeout_seconds:
|
|
81
|
+
self.timeout_seconds = timeout_seconds
|
|
82
|
+
self.serial_number = serial_number
|
|
83
|
+
self.datapoint_type = DataPointType.LINK_NUMBER
|
|
84
|
+
self.finish_callback = self.finish_service_callback
|
|
85
|
+
self.service_callback = finish_callback
|
|
86
|
+
self.start_reactor()
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Conbus Link Number Service for setting module link numbers.
|
|
2
|
+
|
|
3
|
+
This service handles setting link numbers for modules through Conbus telegrams.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from typing import Callable, Optional
|
|
9
|
+
|
|
10
|
+
from twisted.internet.posixbase import PosixReactorBase
|
|
11
|
+
|
|
12
|
+
from xp.models import ConbusClientConfig
|
|
13
|
+
from xp.models.conbus.conbus_linknumber import ConbusLinknumberResponse
|
|
14
|
+
from xp.models.protocol.conbus_protocol import TelegramReceivedEvent
|
|
15
|
+
from xp.models.telegram.datapoint_type import DataPointType
|
|
16
|
+
from xp.models.telegram.system_function import SystemFunction
|
|
17
|
+
from xp.models.telegram.telegram_type import TelegramType
|
|
18
|
+
from xp.services.protocol import ConbusProtocol
|
|
19
|
+
from xp.services.telegram.telegram_service import TelegramService
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ConbusLinknumberSetService(ConbusProtocol):
|
|
23
|
+
"""
|
|
24
|
+
Service for setting module link numbers via Conbus telegrams.
|
|
25
|
+
|
|
26
|
+
Handles link number assignment by sending F04D04 telegrams and processing
|
|
27
|
+
ACK/NAK responses from modules.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
telegram_service: TelegramService,
|
|
33
|
+
cli_config: ConbusClientConfig,
|
|
34
|
+
reactor: PosixReactorBase,
|
|
35
|
+
) -> None:
|
|
36
|
+
"""Initialize the Conbus link number set service"""
|
|
37
|
+
super().__init__(cli_config, reactor)
|
|
38
|
+
self.telegram_service = telegram_service
|
|
39
|
+
self.serial_number: str = ""
|
|
40
|
+
self.link_number: int = 0
|
|
41
|
+
self.finish_callback: Optional[Callable[[ConbusLinknumberResponse], None]] = (
|
|
42
|
+
None
|
|
43
|
+
)
|
|
44
|
+
self.service_response: ConbusLinknumberResponse = ConbusLinknumberResponse(
|
|
45
|
+
success=False, serial_number=self.serial_number, result=""
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Set up logging
|
|
49
|
+
self.logger = logging.getLogger(__name__)
|
|
50
|
+
|
|
51
|
+
def connection_established(self) -> None:
|
|
52
|
+
self.logger.debug(
|
|
53
|
+
f"Connection established, setting link number {self.link_number}..."
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Validate parameters before sending
|
|
57
|
+
if not self.serial_number or len(self.serial_number) != 10:
|
|
58
|
+
self.failed(f"Serial number must be 10 digits, got: {self.serial_number}")
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
if not (0 <= self.link_number <= 99):
|
|
62
|
+
self.failed(f"Link number must be between 0-99, got: {self.link_number}")
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
# Send F04D04{link_number} telegram
|
|
66
|
+
# F04 = WRITE_CONFIG, D04 = LINK_NUMBER datapoint type
|
|
67
|
+
self.send_telegram(
|
|
68
|
+
telegram_type=TelegramType.SYSTEM,
|
|
69
|
+
serial_number=self.serial_number,
|
|
70
|
+
system_function=SystemFunction.WRITE_CONFIG,
|
|
71
|
+
data_value=f"{DataPointType.LINK_NUMBER.value}{self.link_number:02d}",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def telegram_sent(self, telegram_sent: str) -> None:
|
|
75
|
+
self.service_response.sent_telegram = telegram_sent
|
|
76
|
+
|
|
77
|
+
def telegram_received(self, telegram_received: TelegramReceivedEvent) -> None:
|
|
78
|
+
self.logger.debug(f"Telegram received: {telegram_received}")
|
|
79
|
+
|
|
80
|
+
if not self.service_response.received_telegrams:
|
|
81
|
+
self.service_response.received_telegrams = []
|
|
82
|
+
self.service_response.received_telegrams.append(telegram_received.frame)
|
|
83
|
+
|
|
84
|
+
if (
|
|
85
|
+
not telegram_received.checksum_valid
|
|
86
|
+
or telegram_received.telegram_type != TelegramType.REPLY
|
|
87
|
+
or telegram_received.serial_number != self.serial_number
|
|
88
|
+
):
|
|
89
|
+
self.logger.debug("Not a reply for our serial number")
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
# Parse the reply telegram
|
|
93
|
+
reply_telegram = self.telegram_service.parse_reply_telegram(
|
|
94
|
+
telegram_received.frame
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
if not reply_telegram:
|
|
98
|
+
self.logger.debug("Failed to parse reply telegram")
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
# Check for ACK or NAK response
|
|
102
|
+
if reply_telegram.system_function == SystemFunction.ACK:
|
|
103
|
+
self.logger.debug("Received ACK response")
|
|
104
|
+
self.succeed(SystemFunction.ACK)
|
|
105
|
+
elif reply_telegram.system_function == SystemFunction.NAK:
|
|
106
|
+
self.logger.debug("Received NAK response")
|
|
107
|
+
self.failed("Module responded with NAK")
|
|
108
|
+
else:
|
|
109
|
+
self.logger.debug(
|
|
110
|
+
f"Unexpected system function: {reply_telegram.system_function}"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
def succeed(self, system_function: SystemFunction) -> None:
|
|
114
|
+
self.logger.debug("Successfully set link number")
|
|
115
|
+
self.service_response.success = True
|
|
116
|
+
self.service_response.timestamp = datetime.now()
|
|
117
|
+
self.service_response.serial_number = self.serial_number
|
|
118
|
+
self.service_response.result = "ACK"
|
|
119
|
+
self.service_response.link_number = self.link_number
|
|
120
|
+
if self.finish_callback:
|
|
121
|
+
self.finish_callback(self.service_response)
|
|
122
|
+
|
|
123
|
+
def failed(self, message: str) -> None:
|
|
124
|
+
self.logger.debug(f"Failed with message: {message}")
|
|
125
|
+
self.service_response.success = False
|
|
126
|
+
self.service_response.timestamp = datetime.now()
|
|
127
|
+
self.service_response.serial_number = self.serial_number
|
|
128
|
+
self.service_response.result = "NAK"
|
|
129
|
+
self.service_response.error = message
|
|
130
|
+
if self.finish_callback:
|
|
131
|
+
self.finish_callback(self.service_response)
|
|
132
|
+
|
|
133
|
+
def set_linknumber(
|
|
134
|
+
self,
|
|
135
|
+
serial_number: str,
|
|
136
|
+
link_number: int,
|
|
137
|
+
finish_callback: Callable[[ConbusLinknumberResponse], None],
|
|
138
|
+
timeout_seconds: Optional[float] = None,
|
|
139
|
+
) -> None:
|
|
140
|
+
"""
|
|
141
|
+
Set the link number for a specific module.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
serial_number: 10-digit module serial number
|
|
145
|
+
link_number: Link number to set (0-99)
|
|
146
|
+
finish_callback: Callback function to call when operation completes
|
|
147
|
+
timeout_seconds: Optional timeout in seconds
|
|
148
|
+
"""
|
|
149
|
+
self.logger.info("Starting set_linknumber")
|
|
150
|
+
if timeout_seconds:
|
|
151
|
+
self.timeout_seconds = timeout_seconds
|
|
152
|
+
self.serial_number = serial_number
|
|
153
|
+
self.link_number = link_number
|
|
154
|
+
self.finish_callback = finish_callback
|
|
155
|
+
self.start_reactor()
|
|
@@ -1,137 +1,174 @@
|
|
|
1
|
-
"""Conbus
|
|
1
|
+
"""Conbus Output Service for sending action telegrams to Conbus modules.
|
|
2
2
|
|
|
3
|
-
This service
|
|
4
|
-
|
|
3
|
+
This service handles sending action telegrams (ON/OFF) to module outputs
|
|
4
|
+
and processing ACK/NAK responses.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import logging
|
|
8
8
|
from datetime import datetime
|
|
9
|
-
from typing import
|
|
9
|
+
from typing import Callable, Optional
|
|
10
10
|
|
|
11
|
-
from
|
|
11
|
+
from twisted.internet.posixbase import PosixReactorBase
|
|
12
|
+
|
|
13
|
+
from xp.models import ConbusClientConfig
|
|
12
14
|
from xp.models.conbus.conbus_output import ConbusOutputResponse
|
|
15
|
+
from xp.models.protocol.conbus_protocol import TelegramReceivedEvent
|
|
13
16
|
from xp.models.telegram.action_type import ActionType
|
|
14
|
-
from xp.models.telegram.
|
|
17
|
+
from xp.models.telegram.output_telegram import OutputTelegram
|
|
15
18
|
from xp.models.telegram.system_function import SystemFunction
|
|
16
|
-
from xp.
|
|
17
|
-
from xp.services.
|
|
18
|
-
from xp.services.telegram.telegram_output_service import
|
|
19
|
-
|
|
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
|
+
)
|
|
20
25
|
|
|
21
26
|
|
|
22
27
|
class ConbusOutputError(Exception):
|
|
23
|
-
"""Raised when Conbus
|
|
28
|
+
"""Raised when Conbus output operations fail"""
|
|
24
29
|
|
|
25
30
|
pass
|
|
26
31
|
|
|
27
32
|
|
|
28
|
-
class ConbusOutputService:
|
|
33
|
+
class ConbusOutputService(ConbusProtocol):
|
|
29
34
|
"""
|
|
30
|
-
|
|
35
|
+
Service for sending action telegrams to Conbus module outputs.
|
|
31
36
|
|
|
32
|
-
Manages
|
|
33
|
-
|
|
37
|
+
Manages action telegram transmission (ON/OFF) and processes
|
|
38
|
+
ACK/NAK responses from modules.
|
|
34
39
|
"""
|
|
35
40
|
|
|
36
41
|
def __init__(
|
|
37
42
|
self,
|
|
38
|
-
telegram_service: TelegramService,
|
|
39
43
|
telegram_output_service: TelegramOutputService,
|
|
40
|
-
|
|
41
|
-
|
|
44
|
+
cli_config: ConbusClientConfig,
|
|
45
|
+
reactor: PosixReactorBase,
|
|
42
46
|
):
|
|
43
|
-
"""Initialize the Conbus
|
|
47
|
+
"""Initialize the Conbus output service
|
|
44
48
|
|
|
45
49
|
Args:
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
conbus_service: ConbusService for dependency injection
|
|
50
|
+
telegram_output_service: TelegramOutputService for telegram generation/parsing
|
|
51
|
+
cli_config: Conbus client configuration
|
|
52
|
+
reactor: Twisted reactor for async operations
|
|
50
53
|
"""
|
|
51
|
-
|
|
52
|
-
# Service dependencies
|
|
53
|
-
self.telegram_service = telegram_service
|
|
54
|
+
super().__init__(cli_config, reactor)
|
|
54
55
|
self.telegram_output_service = telegram_output_service
|
|
55
|
-
self.
|
|
56
|
-
self.
|
|
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
|
+
)
|
|
57
67
|
|
|
58
68
|
# Set up logging
|
|
59
69
|
self.logger = logging.getLogger(__name__)
|
|
60
70
|
|
|
61
|
-
def
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def __exit__(
|
|
65
|
-
self,
|
|
66
|
-
_exc_type: Optional[type],
|
|
67
|
-
_exc_val: Optional[Exception],
|
|
68
|
-
_exc_tb: Optional[Any],
|
|
69
|
-
) -> None:
|
|
70
|
-
# Cleanup logic if needed
|
|
71
|
-
pass
|
|
72
|
-
|
|
73
|
-
def get_output_state(self, serial_number: str) -> ConbusDatapointResponse:
|
|
74
|
-
# TODO: Migrate to new ConbusDatapointService callback-based API
|
|
75
|
-
# Send status query using custom telegram method
|
|
76
|
-
response = self.datapoint_service.query_datapoint( # type: ignore[call-arg,func-returns-value]
|
|
77
|
-
serial_number=serial_number,
|
|
78
|
-
datapoint_type=DataPointType.MODULE_OUTPUT_STATE, # "12"
|
|
71
|
+
def connection_established(self) -> None:
|
|
72
|
+
self.logger.debug(
|
|
73
|
+
f"Connection established, sending action {self.action_type} to output {self.output_number}..."
|
|
79
74
|
)
|
|
80
75
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
76
|
+
# Validate parameters before sending
|
|
77
|
+
try:
|
|
78
|
+
self.telegram_output_service.validate_output_number(self.output_number)
|
|
79
|
+
self.telegram_output_service.validate_serial_number(self.serial_number)
|
|
80
|
+
except XPOutputError as e:
|
|
81
|
+
self.failed(str(e))
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
# Send F27D{output:02d}{action} telegram
|
|
85
|
+
# F27 = ACTION, D = data with output number and action type
|
|
86
|
+
self.send_telegram(
|
|
87
|
+
telegram_type=TelegramType.SYSTEM,
|
|
88
|
+
serial_number=self.serial_number,
|
|
89
|
+
system_function=SystemFunction.ACTION,
|
|
90
|
+
data_value=f"{self.output_number:02d}{self.action_type.value}",
|
|
88
91
|
)
|
|
89
92
|
|
|
90
|
-
|
|
93
|
+
def telegram_sent(self, telegram_sent: str) -> None:
|
|
94
|
+
self.service_response.sent_telegram = telegram_sent
|
|
91
95
|
|
|
92
|
-
def
|
|
93
|
-
self
|
|
94
|
-
) -> ConbusOutputResponse:
|
|
96
|
+
def telegram_received(self, telegram_received: TelegramReceivedEvent) -> None:
|
|
97
|
+
self.logger.debug(f"Telegram received: {telegram_received}")
|
|
95
98
|
|
|
96
|
-
|
|
97
|
-
|
|
99
|
+
if not self.service_response.received_telegrams:
|
|
100
|
+
self.service_response.received_telegrams = []
|
|
101
|
+
self.service_response.received_telegrams.append(telegram_received.frame)
|
|
98
102
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
103
|
+
if (
|
|
104
|
+
not telegram_received.checksum_valid
|
|
105
|
+
or telegram_received.telegram_type != TelegramType.REPLY
|
|
106
|
+
or telegram_received.serial_number != self.serial_number
|
|
107
|
+
):
|
|
108
|
+
self.logger.debug("Not a reply for our serial number")
|
|
109
|
+
return
|
|
102
110
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
SystemFunction.ACTION, # "27"
|
|
107
|
-
input_action, # "00AA", "01AA", etc.
|
|
111
|
+
# Parse the reply telegram to get ACK/NAK
|
|
112
|
+
output_telegram = self.telegram_output_service.parse_reply_telegram(
|
|
113
|
+
telegram_received.frame
|
|
108
114
|
)
|
|
109
115
|
|
|
110
|
-
if (
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
or len(response.received_telegrams) <= 0
|
|
116
|
+
if output_telegram and output_telegram.system_function in (
|
|
117
|
+
SystemFunction.ACK,
|
|
118
|
+
SystemFunction.NAK,
|
|
114
119
|
):
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
action_type=action_type,
|
|
121
|
-
error=response.error,
|
|
122
|
-
timestamp=response.timestamp or datetime.now(),
|
|
123
|
-
received_telegrams=response.received_telegrams,
|
|
120
|
+
self.logger.debug(f"Received {output_telegram.system_function} response")
|
|
121
|
+
self.succeed(output_telegram)
|
|
122
|
+
else:
|
|
123
|
+
self.logger.debug(
|
|
124
|
+
f"Unexpected system function: {output_telegram.system_function}"
|
|
124
125
|
)
|
|
125
126
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
127
|
+
def succeed(self, output_telegram: OutputTelegram) -> None:
|
|
128
|
+
self.logger.debug("Successfully sent action to output")
|
|
129
|
+
self.service_response.success = True
|
|
130
|
+
self.service_response.timestamp = datetime.now()
|
|
131
|
+
self.service_response.serial_number = self.serial_number
|
|
132
|
+
self.service_response.output_number = self.output_number
|
|
133
|
+
self.service_response.action_type = self.action_type
|
|
134
|
+
self.service_response.output_telegram = output_telegram
|
|
135
|
+
if self.finish_callback:
|
|
136
|
+
self.finish_callback(self.service_response)
|
|
137
|
+
|
|
138
|
+
def failed(self, message: str) -> None:
|
|
139
|
+
self.logger.debug(f"Failed with message: {message}")
|
|
140
|
+
self.service_response.success = False
|
|
141
|
+
self.service_response.timestamp = datetime.now()
|
|
142
|
+
self.service_response.serial_number = self.serial_number
|
|
143
|
+
self.service_response.output_number = self.output_number
|
|
144
|
+
self.service_response.action_type = self.action_type
|
|
145
|
+
self.service_response.error = message
|
|
146
|
+
if self.finish_callback:
|
|
147
|
+
self.finish_callback(self.service_response)
|
|
148
|
+
|
|
149
|
+
def send_action(
|
|
150
|
+
self,
|
|
151
|
+
serial_number: str,
|
|
152
|
+
output_number: int,
|
|
153
|
+
action_type: ActionType,
|
|
154
|
+
finish_callback: Callable[[ConbusOutputResponse], None],
|
|
155
|
+
timeout_seconds: Optional[float] = None,
|
|
156
|
+
) -> None:
|
|
157
|
+
"""
|
|
158
|
+
Send an action telegram to a module output.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
serial_number: 10-digit module serial number
|
|
162
|
+
output_number: Output number (0-99)
|
|
163
|
+
action_type: Action to perform (ON_RELEASE, OFF_PRESS, etc.)
|
|
164
|
+
finish_callback: Callback function to call when operation completes
|
|
165
|
+
timeout_seconds: Optional timeout in seconds
|
|
166
|
+
"""
|
|
167
|
+
self.logger.info("Starting send_action")
|
|
168
|
+
if timeout_seconds:
|
|
169
|
+
self.timeout_seconds = timeout_seconds
|
|
170
|
+
self.serial_number = serial_number
|
|
171
|
+
self.output_number = output_number
|
|
172
|
+
self.action_type = action_type
|
|
173
|
+
self.finish_callback = finish_callback
|
|
174
|
+
self.start_reactor()
|