conson-xp 0.11.19__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: conson-xp
3
- Version: 0.11.19
3
+ Version: 0.11.21
4
4
  Summary: XP Protocol Communication Tools
5
5
  Author-Email: ldvchosal <ldvchosal@github.com>
6
6
  License: MIT License
@@ -1,8 +1,8 @@
1
- conson_xp-0.11.19.dist-info/METADATA,sha256=p1zZBcnLWzxqgjDpG5PV79RjfUlqHFro4Lg9UR7DCwM,9298
2
- conson_xp-0.11.19.dist-info/WHEEL,sha256=9P2ygRxDrTJz3gsagc0Z96ukrxjr-LFBGOgv3AuKlCA,90
3
- conson_xp-0.11.19.dist-info/entry_points.txt,sha256=1OcdIcDM1hz3ljCXgybaPUh1IOFEwkaTgLIW9u9zGeg,50
4
- conson_xp-0.11.19.dist-info/licenses/LICENSE,sha256=rxj6woMM-r3YCyGq_UHFtbh7kHTAJgrccH6O-33IDE4,1419
5
- xp/__init__.py,sha256=GI7bh7zmr-OWMd0qpVNaJWLLfl-rOYs03w6gLooWVqQ,177
1
+ conson_xp-0.11.21.dist-info/METADATA,sha256=HwBfLHuKhBtOi-5wmg081HGTAng6BXa2E8W-hkfzkXU,9298
2
+ conson_xp-0.11.21.dist-info/WHEEL,sha256=9P2ygRxDrTJz3gsagc0Z96ukrxjr-LFBGOgv3AuKlCA,90
3
+ conson_xp-0.11.21.dist-info/entry_points.txt,sha256=1OcdIcDM1hz3ljCXgybaPUh1IOFEwkaTgLIW9u9zGeg,50
4
+ conson_xp-0.11.21.dist-info/licenses/LICENSE,sha256=rxj6woMM-r3YCyGq_UHFtbh7kHTAJgrccH6O-33IDE4,1419
5
+ xp/__init__.py,sha256=Yo_o4SM3_458AZhuCwUhwwY3C5xsGcGv2-pF3Z-8Fhs,177
6
6
  xp/api/__init__.py,sha256=fCWjkuTzDlyJ8hIpU_QEBNcah3BB2bqVSfZwmP4ee5k,51
7
7
  xp/api/main.py,sha256=8OMXUYKnpsqjjnLChokC_M0DXZy-whrxY2dU4QTvr8A,3389
8
8
  xp/api/models/__init__.py,sha256=Xl8j2qiHc-Vb8b1ZmNZaMj7RtMvk0G_2SOvMgQumYzs,54
@@ -17,7 +17,7 @@ xp/api/routers/conbus_output.py,sha256=U-i7kjZv7H6aE0O_8AcZIKZDJuYwU8XostEWoNccn
17
17
  xp/api/routers/errors.py,sha256=5vzpeTt0cPQZDHL5YgrYJUxMh4uI7IgUNFAvL0NpLKM,1267
18
18
  xp/cli/__init__.py,sha256=KWYwhOU46dNEerlc2i4FHsVR4Fxmt90Pw5L5i2v1Jqc,80
19
19
  xp/cli/__main__.py,sha256=l2iKwMdat5rTGd3JWs-uGksnYYDDffp_Npz05QdKEeU,117
20
- xp/cli/commands/__init__.py,sha256=_Ia9P0gK7tCeiWbuU7p8RPgvBhQx1kFC6i3b1g1UuOo,4482
20
+ xp/cli/commands/__init__.py,sha256=dJ_VaOojg14vdx4OwxbDH-egJd16h3uT7f5DSQX4ZB0,4459
21
21
  xp/cli/commands/api.py,sha256=d9uD5NnDV807suEK6eLzJSl62TGTtFSwIA_foPc6wtg,340
22
22
  xp/cli/commands/api_start_commands.py,sha256=q9yOkiIY-NmRD04pq3N6rRe-BjnrcMRZmai8Jg5TKhA,3116
23
23
  xp/cli/commands/conbus/__init__.py,sha256=gE3K5OEoXkkZX8UOc2v3nreQQzwkOQi7n0VZ-Z2juXA,495
@@ -29,7 +29,7 @@ xp/cli/commands/conbus/conbus_config_commands.py,sha256=MBLt7Z95-v2Drvr80r4HQxx8
29
29
  xp/cli/commands/conbus/conbus_custom_commands.py,sha256=NH8WDm31-7tb-KYH8nZprEXgo8a4asLWmNd0EgJVSHQ,1336
30
30
  xp/cli/commands/conbus/conbus_datapoint_commands.py,sha256=Ozc36DFKVfzDZyWU9xVdhebiYy8yodb_DgVKEpzK9Jg,2842
31
31
  xp/cli/commands/conbus/conbus_discover_commands.py,sha256=5HwuhlC3qyYJqLBuqQVyu5kr493BJOZ281V2pXtfoCY,961
32
- xp/cli/commands/conbus/conbus_lightlevel_commands.py,sha256=-0rkE6Y5XForr3WRYSuBT02JO2rXuXLYHkEwbMVNF14,3744
32
+ xp/cli/commands/conbus/conbus_lightlevel_commands.py,sha256=hMzQewTgqGNB6s_Pag2HNfImhpgXnitwUVgMM6XXJv8,4161
33
33
  xp/cli/commands/conbus/conbus_linknumber_commands.py,sha256=Q8hh222Gq7xDD-dRvwS4tiLQj3l913n-Vk1J73QxGGc,1954
34
34
  xp/cli/commands/conbus/conbus_msactiontable_commands.py,sha256=n1g-ZKjjjbCQqTPGCrcCB595QoRClv0DGvGEx8y7BAY,1728
35
35
  xp/cli/commands/conbus/conbus_output_commands.py,sha256=B_N31H4uBjahBxr6VNsj_bnsJsqkQc4CTIofDBKY4v8,2885
@@ -118,7 +118,7 @@ xp/services/conbus/actiontable/msactiontable_service.py,sha256=89pxbUVlKd6mplpIe
118
118
  xp/services/conbus/actiontable/msactiontable_xp20_serializer.py,sha256=EYspooOdi0Z8oaXGxpazwnUoTmh-d7U9auhu11iBgmU,6527
119
119
  xp/services/conbus/actiontable/msactiontable_xp24_serializer.py,sha256=J_ve6uyMmsb_QT2Py1ciZngf4C0pfHpkbWFc8OL2QEk,3752
120
120
  xp/services/conbus/actiontable/msactiontable_xp33_serializer.py,sha256=AeGBbum5eIqlgVm9t4igfF6vyl-ACVpuzSUQm4TYfUQ,7962
121
- xp/services/conbus/conbus_autoreport_get_service.py,sha256=d97x_2AMq9BZsRS8ZvwN-LgdyNPzdDiyqjSqvWDqYjQ,4949
121
+ xp/services/conbus/conbus_autoreport_get_service.py,sha256=mNWFOt0-Ahx5N_Ayyq4oa-rdf2Xcu4rwz7erIjfdNBg,5206
122
122
  xp/services/conbus/conbus_autoreport_set_service.py,sha256=0fa2xGVE1GSoBvEcOH8cnjAdhvEoamvHE27t40f7lUk,4937
123
123
  xp/services/conbus/conbus_blink_all_service.py,sha256=t_oaztEFRirjS3nL50AEBrKKpNXYssWwSBwWjWhhKqU,5959
124
124
  xp/services/conbus/conbus_blink_service.py,sha256=mHaD-84RCmEGqD2cD4Ve85A36D08tMBPwMgDEyAEMCc,5031
@@ -127,7 +127,8 @@ xp/services/conbus/conbus_custom_service.py,sha256=jM6B085GTT8epCE9EqVTT8mZLPMXM
127
127
  xp/services/conbus/conbus_datapoint_queryall_service.py,sha256=FBpFmwXVKoTiqMwQW-z2eYbamKL_ZGlTI86720ZcGt8,6095
128
128
  xp/services/conbus/conbus_datapoint_service.py,sha256=CspxjhEi8awX_dv6EpJpYjTtCsfLgjJ4ROl6jD1TOiY,5289
129
129
  xp/services/conbus/conbus_discover_service.py,sha256=kA_ewxXgbqXng0cXBjSgQSGYesyesbN2wK0tMp1fgfc,4281
130
- xp/services/conbus/conbus_lightlevel_service.py,sha256=Lfa1ruDDA4_M1103Ha-wSlGSjZmsGDpXuF22RfGBYDQ,7243
130
+ xp/services/conbus/conbus_lightlevel_get_service.py,sha256=vrr5HhR5l58oOezTz2rorO-RHoyGbVuHrx2rGfKvH20,5739
131
+ xp/services/conbus/conbus_lightlevel_set_service.py,sha256=6Mhq9xjWx-ixGVfcWBcJSnuYT256m8Lps1g8Tg-1nkM,7483
131
132
  xp/services/conbus/conbus_linknumber_service.py,sha256=r3Lyb7IV4i5kRY6zTnRK9jmC1vo8dpyHwh4WwdwrlL4,7703
132
133
  xp/services/conbus/conbus_output_service.py,sha256=_sHGF6AWadhT_CNSnRPaXwQLaNEwQHSVuyKtyZ9jgFc,5115
133
134
  xp/services/conbus/conbus_raw_service.py,sha256=riANTAiXTdSzOWbp_g-IQjdRhAf8CzZnNJtlT-aDvRs,3746
@@ -174,8 +175,8 @@ xp/services/telegram/telegram_service.py,sha256=Mp20QB50WDp43QBl8TzsR9BKV_hHP53R
174
175
  xp/services/telegram/telegram_version_service.py,sha256=21v256THg0lidTyUtp2i8r6TAl8J0CTaZo4bZw59nFk,10479
175
176
  xp/utils/__init__.py,sha256=clisBctja_VwtkAM9wnbOGDRPfbKHlCUqq4Sa4Z6zng,331
176
177
  xp/utils/checksum.py,sha256=HDpiQxmdIedbCbZ4o_Box0i_Zig417BtCV_46ZyhiTk,1711
177
- xp/utils/dependencies.py,sha256=ZG374ofjrWF7P4MqO3vpeuweD5M9d_RL0682ouJrQ3M,20566
178
+ xp/utils/dependencies.py,sha256=VztcJM27MA6mykoVuX84zXMRsmKYuSGsVOKu5Gaw0b4,20490
178
179
  xp/utils/event_helper.py,sha256=W-A_xmoXlpWZBbJH6qdaN50o3-XrwFsDgvAGMJDiAgo,1001
179
180
  xp/utils/serialization.py,sha256=hZcrpzaBOe3a2wS_RFCIRs9PvnYsrbK7Ulq6s1rU0q4,4456
180
181
  xp/utils/time_utils.py,sha256=Yi3JxHrHsd9ZZxTBRIqlTOz1FYTCeE1IZbefDaXIop8,3800
181
- conson_xp-0.11.19.dist-info/RECORD,,
182
+ conson_xp-0.11.21.dist-info/RECORD,,
xp/__init__.py CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  """conson-xp"""
4
4
 
5
- __version__ = "0.11.19"
5
+ __version__ = "0.11.21"
6
6
  __manufacturer__ = "salchichon"
7
7
  __model__ = "xp.cli"
8
8
  __serial__ = "2025.09.23.000"
@@ -38,7 +38,6 @@ from xp.cli.commands.conbus.conbus_datapoint_commands import (
38
38
  )
39
39
  from xp.cli.commands.conbus.conbus_discover_commands import send_discover_telegram
40
40
  from xp.cli.commands.conbus.conbus_lightlevel_commands import (
41
- xp_lightlevel_get,
42
41
  xp_lightlevel_off,
43
42
  xp_lightlevel_on,
44
43
  xp_lightlevel_set,
@@ -10,9 +10,10 @@ from xp.cli.utils.decorators import (
10
10
  handle_service_errors,
11
11
  )
12
12
  from xp.cli.utils.serial_number_type import SERIAL
13
- from xp.services.conbus.conbus_lightlevel_service import (
13
+ from xp.models.conbus.conbus_lightlevel import ConbusLightlevelResponse
14
+ from xp.services.conbus.conbus_lightlevel_set_service import (
14
15
  ConbusLightlevelError,
15
- ConbusLightlevelService,
16
+ ConbusLightlevelSetService,
16
17
  )
17
18
 
18
19
 
@@ -34,12 +35,17 @@ def xp_lightlevel_set(
34
35
  xp conbus lightlevel set 0123450001 2 50 # Set output 2 to 50%
35
36
  xp conbus lightlevel set 0011223344 0 100 # Set output 0 to 100%
36
37
  """
37
- service = ctx.obj.get("container").get_container().resolve(ConbusLightlevelService)
38
38
 
39
- with service:
40
- response = service.set_lightlevel(serial_number, output_number, level)
39
+ def finish(response: "ConbusLightlevelResponse") -> None:
41
40
  click.echo(json.dumps(response.to_dict(), indent=2))
42
41
 
42
+ service = (
43
+ ctx.obj.get("container").get_container().resolve(ConbusLightlevelSetService)
44
+ )
45
+
46
+ with service:
47
+ service.set_lightlevel(serial_number, output_number, level, finish, 0.5)
48
+
43
49
 
44
50
  @conbus_lightlevel.command("off")
45
51
  @click.argument("serial_number", type=SERIAL)
@@ -58,12 +64,17 @@ def xp_lightlevel_off(
58
64
  xp conbus lightlevel off 0123450001 2 # Turn off output 2
59
65
  xp conbus lightlevel off 0011223344 0 # Turn off output 0
60
66
  """
61
- service = ctx.obj.get("container").get_container().resolve(ConbusLightlevelService)
62
67
 
63
- with service:
64
- response = service.turn_off(serial_number, output_number)
68
+ def finish(response: "ConbusLightlevelResponse") -> None:
65
69
  click.echo(json.dumps(response.to_dict(), indent=2))
66
70
 
71
+ service = (
72
+ ctx.obj.get("container").get_container().resolve(ConbusLightlevelSetService)
73
+ )
74
+
75
+ with service:
76
+ service.turn_off(serial_number, output_number, finish, 0.5)
77
+
67
78
 
68
79
  @conbus_lightlevel.command("on")
69
80
  @click.argument("serial_number", type=SERIAL)
@@ -82,12 +93,17 @@ def xp_lightlevel_on(
82
93
  xp conbus lightlevel on 0123450001 2 # Turn on output 2 (80%)
83
94
  xp conbus lightlevel on 0011223344 0 # Turn on output 0 (80%)
84
95
  """
85
- service = ctx.obj.get("container").get_container().resolve(ConbusLightlevelService)
86
96
 
87
- with service:
88
- response = service.turn_on(serial_number, output_number)
97
+ def finish(response: "ConbusLightlevelResponse") -> None:
89
98
  click.echo(json.dumps(response.to_dict(), indent=2))
90
99
 
100
+ service = (
101
+ ctx.obj.get("container").get_container().resolve(ConbusLightlevelSetService)
102
+ )
103
+
104
+ with service:
105
+ service.turn_on(serial_number, output_number, finish, 0.5)
106
+
91
107
 
92
108
  @conbus_lightlevel.command("get")
93
109
  @click.argument("serial_number", type=SERIAL)
@@ -99,15 +115,18 @@ def xp_lightlevel_get(
99
115
  ctx: click.Context, serial_number: str, output_number: int
100
116
  ) -> None:
101
117
  """Get current light level for output_number on XP module serial_number
102
-
103
118
  Examples:
104
-
105
119
  \b
106
120
  xp conbus lightlevel get 0123450001 2 # Get light level for output 2
107
121
  xp conbus lightlevel get 0011223344 0 # Get light level for output 0
108
122
  """
109
- service = ctx.obj.get("container").get_container().resolve(ConbusLightlevelService)
110
123
 
111
- with service:
112
- response = service.get_lightlevel(serial_number, output_number)
124
+ def finish(response: "ConbusLightlevelResponse") -> None:
113
125
  click.echo(json.dumps(response.to_dict(), indent=2))
126
+
127
+ service = (
128
+ ctx.obj.get("container").get_container().resolve(ConbusLightlevelSetService)
129
+ )
130
+
131
+ with service:
132
+ service.get_lightlevel(serial_number, output_number, finish, 0.5)
@@ -79,22 +79,29 @@ class ConbusAutoreportGetService(ConbusProtocol):
79
79
  telegram_received.frame
80
80
  )
81
81
  if (
82
- reply_telegram is not None
83
- and reply_telegram.system_function == SystemFunction.READ_DATAPOINT
84
- and reply_telegram.datapoint_type == DataPointType.AUTO_REPORT_STATUS
82
+ not reply_telegram
83
+ or reply_telegram.system_function != SystemFunction.READ_DATAPOINT
84
+ or reply_telegram.datapoint_type != DataPointType.AUTO_REPORT_STATUS
85
85
  ):
86
- self.logger.debug("Received autoreport status telegram")
87
- self.service_response.success = True
88
- self.service_response.timestamp = datetime.now()
89
- self.service_response.serial_number = self.serial_number
90
- self.service_response.auto_report_status = reply_telegram.data_value
86
+ self.logger.debug("Not an autoreport reply")
87
+ return
88
+
89
+ autoreport_status = reply_telegram.data_value
90
+ self.succeed(autoreport_status)
91
91
 
92
- if self.finish_callback:
93
- self.finish_callback(self.service_response)
92
+ def succeed(self, autoreport_status: str) -> None:
93
+ self.logger.debug("Received autoreport status: {autoreport_status}")
94
+ self.service_response.success = True
95
+ self.service_response.serial_number = self.serial_number
96
+ self.service_response.timestamp = datetime.now()
97
+ self.service_response.auto_report_status = autoreport_status
98
+ if self.finish_callback:
99
+ self.finish_callback(self.service_response)
94
100
 
95
101
  def failed(self, message: str) -> None:
96
102
  self.logger.debug(f"Failed with message: {message}")
97
103
  self.service_response.success = False
104
+ self.service_response.serial_number = self.serial_number
98
105
  self.service_response.timestamp = datetime.now()
99
106
  self.service_response.error = message
100
107
  if self.finish_callback:
@@ -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
+ )
xp/utils/dependencies.py CHANGED
@@ -37,7 +37,7 @@ from xp.services.conbus.conbus_datapoint_service import (
37
37
  ConbusDatapointService,
38
38
  )
39
39
  from xp.services.conbus.conbus_discover_service import ConbusDiscoverService
40
- from xp.services.conbus.conbus_lightlevel_service import ConbusLightlevelService
40
+ from xp.services.conbus.conbus_lightlevel_set_service import ConbusLightlevelSetService
41
41
  from xp.services.conbus.conbus_linknumber_service import ConbusLinknumberService
42
42
  from xp.services.conbus.conbus_output_service import ConbusOutputService
43
43
  from xp.services.conbus.conbus_raw_service import ConbusRawService
@@ -224,11 +224,10 @@ class ServiceContainer:
224
224
  )
225
225
 
226
226
  self.container.register(
227
- ConbusLightlevelService,
228
- factory=lambda: ConbusLightlevelService(
229
- telegram_service=self.container.resolve(TelegramService),
230
- conbus_service=self.container.resolve(ConbusService),
231
- datapoint_service=self.container.resolve(ConbusDatapointService),
227
+ ConbusLightlevelSetService,
228
+ factory=lambda: ConbusLightlevelSetService(
229
+ cli_config=self.container.resolve(ConbusClientConfig),
230
+ reactor=self.container.resolve(PosixReactorBase),
232
231
  ),
233
232
  scope=punq.Scope.singleton,
234
233
  )
@@ -1,205 +0,0 @@
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 Any, Optional
11
-
12
- from xp.models.conbus.conbus_lightlevel import ConbusLightlevelResponse
13
- from xp.models.telegram.datapoint_type import DataPointType
14
- from xp.models.telegram.system_function import SystemFunction
15
- from xp.services.conbus.conbus_datapoint_service import ConbusDatapointService
16
- from xp.services.conbus.conbus_service import ConbusService
17
- from xp.services.telegram.telegram_service import TelegramService
18
-
19
-
20
- class ConbusLightlevelError(Exception):
21
- """Raised when Conbus lightlevel operations fail"""
22
-
23
- pass
24
-
25
-
26
- class ConbusLightlevelService:
27
- """
28
- Service for controlling light levels on Conbus modules.
29
-
30
- Manages lightlevel operations including setting specific levels,
31
- turning lights on/off, and querying current states.
32
- """
33
-
34
- def __init__(
35
- self,
36
- telegram_service: TelegramService,
37
- conbus_service: ConbusService,
38
- datapoint_service: ConbusDatapointService,
39
- ):
40
- """Initialize the Conbus lightlevel service"""
41
-
42
- # Service dependencies
43
- self.telegram_service = telegram_service
44
- self.conbus_service = conbus_service
45
- self.datapoint_service = datapoint_service
46
-
47
- # Set up logging
48
- self.logger = logging.getLogger(__name__)
49
-
50
- def __enter__(self) -> "ConbusLightlevelService":
51
- return self
52
-
53
- def __exit__(
54
- self,
55
- _exc_type: Optional[type],
56
- _exc_val: Optional[Exception],
57
- _exc_tb: Optional[Any],
58
- ) -> None:
59
- # Cleanup logic if needed
60
- pass
61
-
62
- def set_lightlevel(
63
- self, serial_number: str, output_number: int, level: int
64
- ) -> ConbusLightlevelResponse:
65
- """Set light level for a specific output on a module.
66
-
67
- Args:
68
- serial_number: Module serial number
69
- output_number: Output number (0-based)
70
- level: Light level percentage (0-100)
71
-
72
- Returns:
73
- ConbusLightlevelResponse with operation result
74
- """
75
-
76
- # Validate output_number range (0-8)
77
- if not 0 <= output_number <= 8:
78
- return ConbusLightlevelResponse(
79
- success=False,
80
- serial_number=serial_number,
81
- output_number=output_number,
82
- level=level,
83
- timestamp=datetime.now(),
84
- error=f"Output number must be between 0 and 8, got {output_number}",
85
- )
86
-
87
- # Validate level range
88
- if not 0 <= level <= 100:
89
- return ConbusLightlevelResponse(
90
- success=False,
91
- serial_number=serial_number,
92
- output_number=output_number,
93
- level=level,
94
- timestamp=datetime.now(),
95
- error=f"Light level must be between 0 and 100, got {level}",
96
- )
97
-
98
- # Format data as output_number:level (e.g., "02:050")
99
- data = f"{output_number:02d}:{level:03d}"
100
-
101
- # Send telegram using WRITE_CONFIG function with MODULE_LIGHT_LEVEL datapoint
102
- response = self.conbus_service.send_telegram(
103
- serial_number,
104
- SystemFunction.WRITE_CONFIG, # "04"
105
- f"{DataPointType.MODULE_LIGHT_LEVEL.value}{data}", # "15" + "02:050"
106
- )
107
-
108
- return ConbusLightlevelResponse(
109
- success=response.success,
110
- serial_number=serial_number,
111
- output_number=output_number,
112
- level=level,
113
- timestamp=response.timestamp or datetime.now(),
114
- sent_telegram=response.sent_telegram,
115
- received_telegrams=response.received_telegrams,
116
- error=response.error,
117
- )
118
-
119
- def turn_off(
120
- self, serial_number: str, output_number: int
121
- ) -> ConbusLightlevelResponse:
122
- """Turn off light (set level to 0) for a specific output.
123
-
124
- Args:
125
- serial_number: Module serial number
126
- output_number: Output number (0-8)
127
-
128
- Returns:
129
- ConbusLightlevelResponse with operation result
130
- """
131
- return self.set_lightlevel(serial_number, output_number, 0)
132
-
133
- def turn_on(
134
- self, serial_number: str, output_number: int
135
- ) -> ConbusLightlevelResponse:
136
- """Turn on light (set level to 80%) for a specific output.
137
-
138
- Args:
139
- serial_number: Module serial number
140
- output_number: Output number (0-8)
141
-
142
- Returns:
143
- ConbusLightlevelResponse with operation result
144
- """
145
- return self.set_lightlevel(serial_number, output_number, 80)
146
-
147
- def get_lightlevel(
148
- self, serial_number: str, output_number: int
149
- ) -> ConbusLightlevelResponse:
150
- """Query current light level for a specific output.
151
-
152
- Args:
153
- serial_number: Module serial number
154
- output_number: Output number (0-8)
155
-
156
- Returns:
157
- ConbusLightlevelResponse with current light level
158
- """
159
-
160
- # TODO: Migrate to new ConbusDatapointService callback-based API
161
- # Query MODULE_LIGHT_LEVEL datapoint
162
- datapoint_response = self.datapoint_service.query_datapoint( # type: ignore[call-arg,func-returns-value]
163
- serial_number=serial_number,
164
- datapoint_type=DataPointType.MODULE_LIGHT_LEVEL,
165
- )
166
-
167
- if not datapoint_response.success:
168
- return ConbusLightlevelResponse(
169
- success=False,
170
- serial_number=serial_number,
171
- output_number=output_number,
172
- level=None,
173
- timestamp=datetime.now(),
174
- error=datapoint_response.error or "Failed to query light level",
175
- )
176
-
177
- # Parse the response to extract level for specific output
178
- level = None
179
- if (
180
- datapoint_response.datapoint_telegram
181
- and datapoint_response.datapoint_telegram.data_value
182
- ):
183
- try:
184
- # Parse response format like "00:050,01:025,02:100"
185
- data_value = str(datapoint_response.datapoint_telegram.data_value)
186
- for output_data in data_value.split(","):
187
- if ":" in output_data:
188
- output_str, level_str = output_data.split(":")
189
- if int(output_str) == output_number:
190
- level_str = level_str.replace("[%]", "")
191
- level = int(level_str)
192
- break
193
- except (ValueError, AttributeError) as e:
194
- self.logger.debug(f"Failed to parse light level data: {e}")
195
-
196
- return ConbusLightlevelResponse(
197
- success=datapoint_response.success,
198
- serial_number=serial_number,
199
- output_number=output_number,
200
- level=level,
201
- timestamp=datetime.now(),
202
- sent_telegram=datapoint_response.sent_telegram,
203
- received_telegrams=datapoint_response.received_telegrams,
204
- error=datapoint_response.error if level is None else None,
205
- )