conson-xp 0.11.16__py3-none-any.whl → 0.11.21__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.16.dist-info → conson_xp-0.11.21.dist-info}/METADATA +1 -1
- {conson_xp-0.11.16.dist-info → conson_xp-0.11.21.dist-info}/RECORD +19 -17
- xp/__init__.py +1 -1
- xp/cli/commands/__init__.py +0 -1
- xp/cli/commands/conbus/conbus_blink_commands.py +38 -20
- xp/cli/commands/conbus/conbus_lightlevel_commands.py +35 -16
- xp/cli/commands/conbus/conbus_raw_commands.py +21 -26
- xp/models/conbus/conbus_blink.py +24 -4
- xp/services/conbus/conbus_autoreport_get_service.py +17 -10
- xp/services/conbus/conbus_blink_all_service.py +162 -0
- xp/services/conbus/conbus_blink_service.py +95 -157
- xp/services/conbus/conbus_discover_service.py +2 -2
- xp/services/conbus/conbus_lightlevel_get_service.py +149 -0
- xp/services/conbus/conbus_lightlevel_set_service.py +205 -0
- xp/services/conbus/conbus_raw_service.py +77 -54
- xp/utils/dependencies.py +20 -12
- xp/services/conbus/conbus_lightlevel_service.py +0 -205
- {conson_xp-0.11.16.dist-info → conson_xp-0.11.21.dist-info}/WHEEL +0 -0
- {conson_xp-0.11.16.dist-info → conson_xp-0.11.21.dist-info}/entry_points.txt +0 -0
- {conson_xp-0.11.16.dist-info → conson_xp-0.11.21.dist-info}/licenses/LICENSE +0 -0
|
@@ -5,62 +5,116 @@ various types of telegrams including discover, version, and sensor data requests
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import logging
|
|
8
|
-
from
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from typing import Callable, Optional
|
|
9
10
|
|
|
10
|
-
from
|
|
11
|
+
from twisted.internet.posixbase import PosixReactorBase
|
|
12
|
+
|
|
13
|
+
from xp.models import ConbusClientConfig
|
|
11
14
|
from xp.models.conbus.conbus_blink import ConbusBlinkResponse
|
|
12
|
-
from xp.models.
|
|
15
|
+
from xp.models.protocol.conbus_protocol import TelegramReceivedEvent
|
|
13
16
|
from xp.models.telegram.system_function import SystemFunction
|
|
14
|
-
from xp.
|
|
15
|
-
from xp.services.
|
|
16
|
-
from xp.services.telegram.telegram_blink_service import TelegramBlinkService
|
|
17
|
+
from xp.models.telegram.telegram_type import TelegramType
|
|
18
|
+
from xp.services.protocol import ConbusProtocol
|
|
17
19
|
from xp.services.telegram.telegram_service import TelegramService
|
|
18
20
|
|
|
19
21
|
|
|
20
|
-
class ConbusBlinkService:
|
|
22
|
+
class ConbusBlinkService(ConbusProtocol):
|
|
21
23
|
"""
|
|
22
|
-
|
|
24
|
+
Service for receiving telegrams from Conbus servers.
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
Uses composition with ConbusService to provide receive-only functionality
|
|
27
|
+
for collecting waiting event telegrams from the server.
|
|
26
28
|
"""
|
|
27
29
|
|
|
28
30
|
def __init__(
|
|
29
31
|
self,
|
|
30
|
-
conbus_service: ConbusService,
|
|
31
|
-
telegram_discover_service: TelegramDiscoverService,
|
|
32
|
-
telegram_blink_service: TelegramBlinkService,
|
|
33
32
|
telegram_service: TelegramService,
|
|
34
|
-
|
|
33
|
+
cli_config: ConbusClientConfig,
|
|
34
|
+
reactor: PosixReactorBase,
|
|
35
|
+
) -> None:
|
|
35
36
|
"""Initialize the Conbus client send service"""
|
|
36
|
-
|
|
37
|
-
# Service dependencies
|
|
38
|
-
self.conbus_service = conbus_service
|
|
39
|
-
self.telegram_discover_service = telegram_discover_service
|
|
40
|
-
self.telegram_blink_service = telegram_blink_service
|
|
41
|
-
self.blink_service = (
|
|
42
|
-
self.telegram_blink_service
|
|
43
|
-
) # Alias for backward compatibility
|
|
37
|
+
super().__init__(cli_config, reactor)
|
|
44
38
|
self.telegram_service = telegram_service
|
|
39
|
+
self.serial_number: str = ""
|
|
40
|
+
self.on_or_off = "none"
|
|
41
|
+
self.finish_callback: Optional[Callable[[ConbusBlinkResponse], None]] = None
|
|
42
|
+
self.service_response: ConbusBlinkResponse = ConbusBlinkResponse(
|
|
43
|
+
success=False,
|
|
44
|
+
serial_number=self.serial_number,
|
|
45
|
+
system_function=SystemFunction.NONE,
|
|
46
|
+
operation=self.on_or_off,
|
|
47
|
+
)
|
|
45
48
|
|
|
46
49
|
# Set up logging
|
|
47
50
|
self.logger = logging.getLogger(__name__)
|
|
48
51
|
|
|
49
|
-
def
|
|
50
|
-
|
|
52
|
+
def connection_established(self) -> None:
|
|
53
|
+
self.logger.debug("Connection established, retrieving autoreport status...")
|
|
54
|
+
# Blink is 05, Unblink is 06
|
|
55
|
+
system_function = SystemFunction.UNBLINK
|
|
56
|
+
if self.on_or_off.lower() == "on":
|
|
57
|
+
system_function = SystemFunction.BLINK
|
|
51
58
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
self.send_telegram(
|
|
60
|
+
telegram_type=TelegramType.SYSTEM,
|
|
61
|
+
serial_number=self.serial_number,
|
|
62
|
+
system_function=system_function,
|
|
63
|
+
data_value="00",
|
|
64
|
+
)
|
|
65
|
+
self.service_response.system_function = system_function
|
|
66
|
+
self.service_response.operation = self.on_or_off
|
|
67
|
+
|
|
68
|
+
def telegram_sent(self, telegram_sent: str) -> None:
|
|
69
|
+
system_telegram = self.telegram_service.parse_system_telegram(telegram_sent)
|
|
70
|
+
self.service_response.sent_telegram = system_telegram
|
|
71
|
+
|
|
72
|
+
def telegram_received(self, telegram_received: TelegramReceivedEvent) -> None:
|
|
73
|
+
|
|
74
|
+
self.logger.debug(f"Telegram received: {telegram_received}")
|
|
75
|
+
if not self.service_response.received_telegrams:
|
|
76
|
+
self.service_response.received_telegrams = []
|
|
77
|
+
self.service_response.received_telegrams.append(telegram_received.frame)
|
|
78
|
+
|
|
79
|
+
if (
|
|
80
|
+
not telegram_received.checksum_valid
|
|
81
|
+
or telegram_received.telegram_type != TelegramType.REPLY
|
|
82
|
+
or telegram_received.serial_number != self.serial_number
|
|
83
|
+
):
|
|
84
|
+
self.logger.debug("Not a reply")
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
reply_telegram = self.telegram_service.parse_reply_telegram(
|
|
88
|
+
telegram_received.frame
|
|
89
|
+
)
|
|
90
|
+
if reply_telegram is not None and reply_telegram.system_function in (
|
|
91
|
+
SystemFunction.ACK,
|
|
92
|
+
SystemFunction.NAK,
|
|
93
|
+
):
|
|
94
|
+
self.logger.debug("Received blink response")
|
|
95
|
+
self.service_response.success = True
|
|
96
|
+
self.service_response.timestamp = datetime.now()
|
|
97
|
+
self.service_response.serial_number = self.serial_number
|
|
98
|
+
self.service_response.reply_telegram = reply_telegram
|
|
99
|
+
|
|
100
|
+
if self.finish_callback:
|
|
101
|
+
self.finish_callback(self.service_response)
|
|
102
|
+
|
|
103
|
+
def failed(self, message: str) -> None:
|
|
104
|
+
self.logger.debug(f"Failed with message: {message}")
|
|
105
|
+
self.service_response.success = False
|
|
106
|
+
self.service_response.timestamp = datetime.now()
|
|
107
|
+
self.service_response.error = message
|
|
108
|
+
if self.finish_callback:
|
|
109
|
+
self.finish_callback(self.service_response)
|
|
60
110
|
|
|
61
111
|
def send_blink_telegram(
|
|
62
|
-
self,
|
|
63
|
-
|
|
112
|
+
self,
|
|
113
|
+
serial_number: str,
|
|
114
|
+
on_or_off: str,
|
|
115
|
+
finish_callback: Callable[[ConbusBlinkResponse], None],
|
|
116
|
+
timeout_seconds: Optional[float] = None,
|
|
117
|
+
) -> None:
|
|
64
118
|
"""
|
|
65
119
|
Send blink command to start blinking module LED.
|
|
66
120
|
|
|
@@ -70,127 +124,11 @@ class ConbusBlinkService:
|
|
|
70
124
|
xp conbus blink 0012345008 on
|
|
71
125
|
xp conbus blink 0012345008 off
|
|
72
126
|
"""
|
|
73
|
-
# Blink is 05, Unblink is 06
|
|
74
|
-
system_function = SystemFunction.UNBLINK
|
|
75
|
-
if on_or_off.lower() == "on":
|
|
76
|
-
system_function = SystemFunction.BLINK
|
|
77
|
-
|
|
78
|
-
# Send blink telegram using custom method (F05D00)
|
|
79
|
-
with self.conbus_service:
|
|
80
|
-
|
|
81
|
-
response = self.conbus_service.send_telegram(
|
|
82
|
-
serial_number,
|
|
83
|
-
system_function, # Blink or Unblink function code
|
|
84
|
-
"00", # Status data point
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
reply_telegram = None
|
|
88
|
-
if (
|
|
89
|
-
response.success
|
|
90
|
-
and response.received_telegrams is not None
|
|
91
|
-
and len(response.received_telegrams) > 0
|
|
92
|
-
):
|
|
93
|
-
ack_or_nak = response.received_telegrams[0]
|
|
94
|
-
parsed_telegram = self.telegram_service.parse_telegram(ack_or_nak)
|
|
95
|
-
if isinstance(parsed_telegram, ReplyTelegram):
|
|
96
|
-
reply_telegram = parsed_telegram
|
|
97
|
-
|
|
98
|
-
return ConbusBlinkResponse(
|
|
99
|
-
serial_number=serial_number,
|
|
100
|
-
operation=on_or_off,
|
|
101
|
-
system_function=system_function,
|
|
102
|
-
response=response,
|
|
103
|
-
reply_telegram=reply_telegram,
|
|
104
|
-
success=response.success,
|
|
105
|
-
timestamp=response.timestamp,
|
|
106
|
-
)
|
|
107
|
-
|
|
108
|
-
def blink_all(self, on_or_off: str) -> ConbusBlinkResponse:
|
|
109
|
-
"""
|
|
110
|
-
Send blink command to all discovered devices.
|
|
111
127
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
# to avoid connection conflicts
|
|
120
|
-
with self.conbus_service:
|
|
121
|
-
# First discover all devices using the same connection
|
|
122
|
-
telegram = self.telegram_discover_service.generate_discover_telegram()
|
|
123
|
-
discover_responses = self.conbus_service.send_raw_telegram(telegram)
|
|
124
|
-
|
|
125
|
-
if not discover_responses.success:
|
|
126
|
-
return ConbusBlinkResponse(
|
|
127
|
-
success=False,
|
|
128
|
-
serial_number="all",
|
|
129
|
-
operation=on_or_off,
|
|
130
|
-
system_function=(
|
|
131
|
-
SystemFunction.BLINK
|
|
132
|
-
if on_or_off == "on"
|
|
133
|
-
else SystemFunction.UNBLINK
|
|
134
|
-
),
|
|
135
|
-
error="Failed to discover devices",
|
|
136
|
-
)
|
|
137
|
-
|
|
138
|
-
# Parse received telegrams to extract device information
|
|
139
|
-
discovered_devices = self.parse_discovered_devices(discover_responses)
|
|
140
|
-
|
|
141
|
-
# If no devices discovered, return success with appropriate message
|
|
142
|
-
if not discovered_devices:
|
|
143
|
-
return ConbusBlinkResponse(
|
|
144
|
-
success=True,
|
|
145
|
-
serial_number="all",
|
|
146
|
-
operation=on_or_off,
|
|
147
|
-
system_function=(
|
|
148
|
-
SystemFunction.BLINK
|
|
149
|
-
if on_or_off == "on"
|
|
150
|
-
else SystemFunction.UNBLINK
|
|
151
|
-
),
|
|
152
|
-
error="No devices discovered",
|
|
153
|
-
)
|
|
154
|
-
|
|
155
|
-
# Send blink command to each discovered device
|
|
156
|
-
all_blink_telegram = []
|
|
157
|
-
for serial_number in discovered_devices:
|
|
158
|
-
blink_telegram = self.telegram_blink_service.generate_blink_telegram(
|
|
159
|
-
serial_number, on_or_off
|
|
160
|
-
)
|
|
161
|
-
all_blink_telegram.append(blink_telegram)
|
|
162
|
-
|
|
163
|
-
# Send all blink telegrams using the same connection
|
|
164
|
-
response = self.conbus_service.send_raw_telegrams(all_blink_telegram)
|
|
165
|
-
|
|
166
|
-
return ConbusBlinkResponse(
|
|
167
|
-
success=response.success,
|
|
168
|
-
serial_number="all",
|
|
169
|
-
operation=on_or_off,
|
|
170
|
-
system_function=(
|
|
171
|
-
SystemFunction.BLINK
|
|
172
|
-
if on_or_off == "on"
|
|
173
|
-
else SystemFunction.UNBLINK
|
|
174
|
-
),
|
|
175
|
-
received_telegrams=response.received_telegrams,
|
|
176
|
-
)
|
|
177
|
-
|
|
178
|
-
def parse_discovered_devices(self, responses: ConbusResponse) -> list[str]:
|
|
179
|
-
discovered_devices: list[str] = []
|
|
180
|
-
if responses.received_telegrams is None:
|
|
181
|
-
return discovered_devices
|
|
182
|
-
for telegrams_str in responses.received_telegrams:
|
|
183
|
-
for telegram_str in telegrams_str.split("\n"):
|
|
184
|
-
try:
|
|
185
|
-
# Parse telegram using TelegramService
|
|
186
|
-
telegram_result = self.telegram_service.parse_telegram(telegram_str)
|
|
187
|
-
# Only process telegrams that have a serial_number attribute
|
|
188
|
-
if hasattr(telegram_result, "serial_number"):
|
|
189
|
-
discovered_devices.append(telegram_result.serial_number)
|
|
190
|
-
|
|
191
|
-
except Exception as e:
|
|
192
|
-
self.logger.warning(
|
|
193
|
-
f"Failed to parse telegram '{telegram_str}': {e}"
|
|
194
|
-
)
|
|
195
|
-
continue
|
|
196
|
-
return discovered_devices
|
|
128
|
+
self.logger.info("Starting get_autoreport_status")
|
|
129
|
+
if timeout_seconds:
|
|
130
|
+
self.timeout_seconds = timeout_seconds
|
|
131
|
+
self.finish_callback = finish_callback
|
|
132
|
+
self.serial_number = serial_number
|
|
133
|
+
self.on_or_off = on_or_off
|
|
134
|
+
self.start_reactor()
|
|
@@ -59,8 +59,8 @@ class ConbusDiscoverService(ConbusProtocol):
|
|
|
59
59
|
self.discovered_device_result.received_telegrams.append(telegram_received.frame)
|
|
60
60
|
|
|
61
61
|
if (
|
|
62
|
-
telegram_received.
|
|
63
|
-
and telegram_received.
|
|
62
|
+
telegram_received.checksum_valid
|
|
63
|
+
and telegram_received.telegram_type == TelegramType.REPLY.value
|
|
64
64
|
and telegram_received.payload[11:16] == "F01D"
|
|
65
65
|
and len(telegram_received.payload) == 15
|
|
66
66
|
):
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Conbus Auto Report Service for getting and setting module auto report status.
|
|
2
|
+
|
|
3
|
+
This service handles auto report status operations 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_lightlevel import ConbusLightlevelResponse
|
|
14
|
+
from xp.models.protocol.conbus_protocol import TelegramReceivedEvent
|
|
15
|
+
from xp.models.telegram.datapoint_type import DataPointType
|
|
16
|
+
from xp.models.telegram.reply_telegram import ReplyTelegram
|
|
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
|
|
20
|
+
from xp.services.telegram.telegram_service import TelegramService
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ConbusLightlevelGetService(ConbusProtocol):
|
|
24
|
+
"""
|
|
25
|
+
Service for receiving telegrams from Conbus servers.
|
|
26
|
+
|
|
27
|
+
Uses composition with ConbusService to provide receive-only functionality
|
|
28
|
+
for collecting waiting event telegrams from the server.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
telegram_service: TelegramService,
|
|
34
|
+
cli_config: ConbusClientConfig,
|
|
35
|
+
reactor: PosixReactorBase,
|
|
36
|
+
) -> None:
|
|
37
|
+
"""Initialize the Conbus client send service"""
|
|
38
|
+
super().__init__(cli_config, reactor)
|
|
39
|
+
self.telegram_service = telegram_service
|
|
40
|
+
self.serial_number: str = ""
|
|
41
|
+
self.output_number: int = 0
|
|
42
|
+
self.finish_callback: Optional[Callable[[ConbusLightlevelResponse], None]] = (
|
|
43
|
+
None
|
|
44
|
+
)
|
|
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
|
+
|
|
53
|
+
# Set up logging
|
|
54
|
+
self.logger = logging.getLogger(__name__)
|
|
55
|
+
|
|
56
|
+
def connection_established(self) -> None:
|
|
57
|
+
self.logger.debug("Connection established, retrieving lightlevel status...")
|
|
58
|
+
self.send_telegram(
|
|
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)
|
|
74
|
+
|
|
75
|
+
if (
|
|
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
|
|
82
|
+
|
|
83
|
+
reply_telegram = self.telegram_service.parse_reply_telegram(
|
|
84
|
+
telegram_received.frame
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
if (
|
|
88
|
+
not reply_telegram
|
|
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)
|
|
125
|
+
|
|
126
|
+
def get_light_level(
|
|
127
|
+
self,
|
|
128
|
+
serial_number: str,
|
|
129
|
+
output_number: int,
|
|
130
|
+
finish_callback: Callable[[ConbusLightlevelResponse], None],
|
|
131
|
+
timeout_seconds: Optional[float] = None,
|
|
132
|
+
) -> None:
|
|
133
|
+
"""
|
|
134
|
+
Get the current auto report status for a specific module.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
:param serial_number: 10-digit module serial number
|
|
138
|
+
:param output_number: output module number
|
|
139
|
+
:param finish_callback: callback function to call when the lightlevel status is
|
|
140
|
+
:param timeout_seconds: timeout in seconds
|
|
141
|
+
|
|
142
|
+
"""
|
|
143
|
+
self.logger.info("Starting get_lightlevel_status")
|
|
144
|
+
if timeout_seconds:
|
|
145
|
+
self.timeout_seconds = timeout_seconds
|
|
146
|
+
self.finish_callback = finish_callback
|
|
147
|
+
self.serial_number = serial_number
|
|
148
|
+
self.output_number = output_number
|
|
149
|
+
self.start_reactor()
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""Conbus Lightlevel Service for controlling light levels on Conbus modules.
|
|
2
|
+
|
|
3
|
+
This service implements lightlevel control operations for XP modules,
|
|
4
|
+
including setting specific light levels, turning lights on/off, and
|
|
5
|
+
querying current light levels.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from typing import Callable, Optional
|
|
11
|
+
|
|
12
|
+
from twisted.internet.posixbase import PosixReactorBase
|
|
13
|
+
|
|
14
|
+
from xp.models import ConbusClientConfig
|
|
15
|
+
from xp.models.conbus.conbus_lightlevel import ConbusLightlevelResponse
|
|
16
|
+
from xp.models.protocol.conbus_protocol import TelegramReceivedEvent
|
|
17
|
+
from xp.models.telegram.datapoint_type import DataPointType
|
|
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
|
+
|
|
22
|
+
|
|
23
|
+
class ConbusLightlevelError(Exception):
|
|
24
|
+
"""Raised when Conbus lightlevel operations fail"""
|
|
25
|
+
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ConbusLightlevelSetService(ConbusProtocol):
|
|
30
|
+
"""
|
|
31
|
+
Service for controlling light levels on Conbus modules.
|
|
32
|
+
|
|
33
|
+
Manages lightlevel operations including setting specific levels,
|
|
34
|
+
turning lights on/off, and querying current states.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
cli_config: ConbusClientConfig,
|
|
40
|
+
reactor: PosixReactorBase,
|
|
41
|
+
):
|
|
42
|
+
"""Initialize the Conbus lightlevel service"""
|
|
43
|
+
super().__init__(cli_config, reactor)
|
|
44
|
+
self.serial_number: str = ""
|
|
45
|
+
self.output_number: int = 0
|
|
46
|
+
self.level: int = 0
|
|
47
|
+
self.finish_callback: Optional[Callable[[ConbusLightlevelResponse], None]] = (
|
|
48
|
+
None
|
|
49
|
+
)
|
|
50
|
+
self.service_response: ConbusLightlevelResponse = ConbusLightlevelResponse(
|
|
51
|
+
success=False,
|
|
52
|
+
serial_number=self.serial_number,
|
|
53
|
+
output_number=self.output_number,
|
|
54
|
+
level=None,
|
|
55
|
+
timestamp=datetime.now(),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Set up logging
|
|
59
|
+
self.logger = logging.getLogger(__name__)
|
|
60
|
+
|
|
61
|
+
def connection_established(self) -> None:
|
|
62
|
+
self.logger.debug(
|
|
63
|
+
f"Connection established, setting light level for output {self.output_number} to {self.level}%..."
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Format data as output_number:level (e.g., ""15" + "02:050")
|
|
67
|
+
data_value = f"{DataPointType.MODULE_LIGHT_LEVEL.value}{self.output_number:02d}:{self.level:03d}"
|
|
68
|
+
|
|
69
|
+
# Send telegram using WRITE_CONFIG function with MODULE_LIGHT_LEVEL datapoint
|
|
70
|
+
self.send_telegram(
|
|
71
|
+
telegram_type=TelegramType.SYSTEM,
|
|
72
|
+
serial_number=self.serial_number,
|
|
73
|
+
system_function=SystemFunction.WRITE_CONFIG, # "04"
|
|
74
|
+
data_value=data_value,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def telegram_sent(self, telegram_sent: str) -> None:
|
|
78
|
+
self.service_response.sent_telegram = telegram_sent
|
|
79
|
+
|
|
80
|
+
def telegram_received(self, telegram_received: TelegramReceivedEvent) -> None:
|
|
81
|
+
self.logger.debug(f"Telegram received: {telegram_received}")
|
|
82
|
+
if not self.service_response.received_telegrams:
|
|
83
|
+
self.service_response.received_telegrams = []
|
|
84
|
+
self.service_response.received_telegrams.append(telegram_received.frame)
|
|
85
|
+
|
|
86
|
+
if (
|
|
87
|
+
not telegram_received.checksum_valid
|
|
88
|
+
or telegram_received.telegram_type != TelegramType.REPLY
|
|
89
|
+
or telegram_received.serial_number != self.serial_number
|
|
90
|
+
):
|
|
91
|
+
self.logger.debug("Not a reply for our serial number")
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
# Any valid reply means success (ACK or NAK)
|
|
95
|
+
if telegram_received.telegram_type == TelegramType.REPLY:
|
|
96
|
+
self.logger.debug("Received lightlevel response")
|
|
97
|
+
self.succeed()
|
|
98
|
+
|
|
99
|
+
def succeed(self) -> None:
|
|
100
|
+
self.logger.debug("Succeed")
|
|
101
|
+
self.service_response.success = True
|
|
102
|
+
self.service_response.serial_number = self.serial_number
|
|
103
|
+
self.service_response.output_number = self.output_number
|
|
104
|
+
self.service_response.level = self.level
|
|
105
|
+
self.service_response.timestamp = datetime.now()
|
|
106
|
+
|
|
107
|
+
if self.finish_callback:
|
|
108
|
+
self.finish_callback(self.service_response)
|
|
109
|
+
|
|
110
|
+
def failed(self, message: str) -> None:
|
|
111
|
+
self.logger.debug(f"Failed with message: {message}")
|
|
112
|
+
self.service_response.success = False
|
|
113
|
+
self.service_response.serial_number = self.serial_number
|
|
114
|
+
self.service_response.output_number = self.output_number
|
|
115
|
+
self.service_response.level = self.level
|
|
116
|
+
self.service_response.timestamp = datetime.now()
|
|
117
|
+
self.service_response.error = message
|
|
118
|
+
|
|
119
|
+
if self.finish_callback:
|
|
120
|
+
self.finish_callback(self.service_response)
|
|
121
|
+
|
|
122
|
+
def set_lightlevel(
|
|
123
|
+
self,
|
|
124
|
+
serial_number: str,
|
|
125
|
+
output_number: int,
|
|
126
|
+
level: int,
|
|
127
|
+
finish_callback: Callable[[ConbusLightlevelResponse], None],
|
|
128
|
+
timeout_seconds: Optional[float] = None,
|
|
129
|
+
) -> None:
|
|
130
|
+
"""Set light level for a specific output on a module.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
serial_number: Module serial number
|
|
134
|
+
output_number: Output number (0-based, 0-8)
|
|
135
|
+
level: Light level percentage (0-100)
|
|
136
|
+
finish_callback: Callback function to call when operation completes
|
|
137
|
+
timeout_seconds: Optional timeout in seconds
|
|
138
|
+
|
|
139
|
+
Examples:
|
|
140
|
+
\b
|
|
141
|
+
xp conbus lightlevel set 0012345008 2 50
|
|
142
|
+
xp conbus lightlevel set 0012345008 0 100
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
self.logger.info(
|
|
146
|
+
f"Setting light level for {serial_number} output {output_number} to {level}%"
|
|
147
|
+
)
|
|
148
|
+
if timeout_seconds:
|
|
149
|
+
self.timeout_seconds = timeout_seconds
|
|
150
|
+
self.finish_callback = finish_callback
|
|
151
|
+
self.serial_number = serial_number
|
|
152
|
+
self.output_number = output_number
|
|
153
|
+
self.level = level
|
|
154
|
+
|
|
155
|
+
# Validate output_number range (0-8)
|
|
156
|
+
if not 0 <= self.output_number <= 8:
|
|
157
|
+
self.failed(
|
|
158
|
+
f"Output number must be between 0 and 8, got {self.output_number}"
|
|
159
|
+
)
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
# Validate level range
|
|
163
|
+
if not 0 <= self.level <= 100:
|
|
164
|
+
self.failed(f"Light level must be between 0 and 100, got {self.level}")
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
self.start_reactor()
|
|
168
|
+
|
|
169
|
+
def turn_off(
|
|
170
|
+
self,
|
|
171
|
+
serial_number: str,
|
|
172
|
+
output_number: int,
|
|
173
|
+
finish_callback: Callable[[ConbusLightlevelResponse], None],
|
|
174
|
+
timeout_seconds: Optional[float] = None,
|
|
175
|
+
) -> None:
|
|
176
|
+
"""Turn off light (set level to 0) for a specific output.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
serial_number: Module serial number
|
|
180
|
+
output_number: Output number (0-8)
|
|
181
|
+
finish_callback: Callback function to call when operation completes
|
|
182
|
+
timeout_seconds: Optional timeout in seconds
|
|
183
|
+
"""
|
|
184
|
+
self.set_lightlevel(
|
|
185
|
+
serial_number, output_number, 0, finish_callback, timeout_seconds
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
def turn_on(
|
|
189
|
+
self,
|
|
190
|
+
serial_number: str,
|
|
191
|
+
output_number: int,
|
|
192
|
+
finish_callback: Callable[[ConbusLightlevelResponse], None],
|
|
193
|
+
timeout_seconds: Optional[float] = None,
|
|
194
|
+
) -> None:
|
|
195
|
+
"""Turn on light (set level to 80%) for a specific output.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
serial_number: Module serial number
|
|
199
|
+
output_number: Output number (0-8)
|
|
200
|
+
finish_callback: Callback function to call when operation completes
|
|
201
|
+
timeout_seconds: Optional timeout in seconds
|
|
202
|
+
"""
|
|
203
|
+
self.set_lightlevel(
|
|
204
|
+
serial_number, output_number, 80, finish_callback, timeout_seconds
|
|
205
|
+
)
|