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.
@@ -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 typing import Any, Optional
8
+ from datetime import datetime
9
+ from typing import Callable, Optional
9
10
 
10
- from xp.models import ConbusResponse
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.telegram.reply_telegram import ReplyTelegram
15
+ from xp.models.protocol.conbus_protocol import TelegramReceivedEvent
13
16
  from xp.models.telegram.system_function import SystemFunction
14
- from xp.services import TelegramDiscoverService
15
- from xp.services.conbus.conbus_service import ConbusService
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
- TCP client service for sending telegrams to Conbus servers.
24
+ Service for receiving telegrams from Conbus servers.
23
25
 
24
- Manages TCP socket connections, handles telegram generation and transmission,
25
- and processes server responses.
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 __enter__(self) -> "ConbusBlinkService":
50
- return self
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
- def __exit__(
53
- self,
54
- _exc_type: Optional[type],
55
- _exc_val: Optional[Exception],
56
- _exc_tb: Optional[Any],
57
- ) -> None:
58
- # Cleanup logic if needed
59
- pass
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, serial_number: str, on_or_off: str
63
- ) -> ConbusBlinkResponse:
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
- Args:
113
- on_or_off: "on" or "off" to control blink state
114
-
115
- Returns:
116
- ConbusBlinkResponse: Aggregated response for all devices
117
- """
118
- # Use a single ConbusService instance for both discover and blinking
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.telegram_type == TelegramType.REPLY.value
63
- and telegram_received.checksum_valid
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
+ )