conson-xp 1.14.0__py3-none-any.whl → 1.16.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: conson-xp
3
- Version: 1.14.0
3
+ Version: 1.16.0
4
4
  Summary: XP Protocol Communication Tools
5
5
  Author-Email: ldvchosal <ldvchosal@github.com>
6
6
  License: MIT License
@@ -47,6 +47,7 @@ Requires-Dist: HAP-python[QRCode]>=5.0.0
47
47
  Requires-Dist: punq>=0.7.0
48
48
  Requires-Dist: twisted>=25.5.0
49
49
  Requires-Dist: bubus>=1.5.6
50
+ Requires-Dist: psygnal>=0.15.0
50
51
  Description-Content-Type: text/markdown
51
52
 
52
53
  # 🔌 XP Protocol Communication Tool
@@ -1,8 +1,8 @@
1
- conson_xp-1.14.0.dist-info/METADATA,sha256=uGAklMxvS6lLMkbZJhbKRbacnW6j4BPusG0fN7QngI4,9437
2
- conson_xp-1.14.0.dist-info/WHEEL,sha256=9P2ygRxDrTJz3gsagc0Z96ukrxjr-LFBGOgv3AuKlCA,90
3
- conson_xp-1.14.0.dist-info/entry_points.txt,sha256=1OcdIcDM1hz3ljCXgybaPUh1IOFEwkaTgLIW9u9zGeg,50
4
- conson_xp-1.14.0.dist-info/licenses/LICENSE,sha256=rxj6woMM-r3YCyGq_UHFtbh7kHTAJgrccH6O-33IDE4,1419
5
- xp/__init__.py,sha256=GE61XDgO4hO22ArZVSpL0z7P_TUHoWJwF3OWDR1fkTw,181
1
+ conson_xp-1.16.0.dist-info/METADATA,sha256=vy5t4nVHJcDwnzs6v6zmBlGsEOz-fu5AS9Xr7XfcIGo,9468
2
+ conson_xp-1.16.0.dist-info/WHEEL,sha256=9P2ygRxDrTJz3gsagc0Z96ukrxjr-LFBGOgv3AuKlCA,90
3
+ conson_xp-1.16.0.dist-info/entry_points.txt,sha256=1OcdIcDM1hz3ljCXgybaPUh1IOFEwkaTgLIW9u9zGeg,50
4
+ conson_xp-1.16.0.dist-info/licenses/LICENSE,sha256=rxj6woMM-r3YCyGq_UHFtbh7kHTAJgrccH6O-33IDE4,1419
5
+ xp/__init__.py,sha256=VssXZod82LgI2ba-rx7RfBTkJ4tXhhfY1_DlC0xf4sU,181
6
6
  xp/cli/__init__.py,sha256=QjnKB1KaI2aIyKlzrnvCwfbBuUj8HNgwNMvNJVQofbI,81
7
7
  xp/cli/__main__.py,sha256=l2iKwMdat5rTGd3JWs-uGksnYYDDffp_Npz05QdKEeU,117
8
8
  xp/cli/commands/__init__.py,sha256=EGDWTEH_pCKIMWelnjhK4_PxBsvIH24rO9fz1nF1pKA,4638
@@ -14,7 +14,7 @@ xp/cli/commands/conbus/conbus_blink_commands.py,sha256=UK-Ey4K0FvaPQ96U0gyMid236
14
14
  xp/cli/commands/conbus/conbus_config_commands.py,sha256=BugIbgNX6_s4MySGvI6tWZkwguciajHUX2Xz8XBux7k,716
15
15
  xp/cli/commands/conbus/conbus_custom_commands.py,sha256=lICT93ijMdhVRm8KjNMLo7kQ2BLlnOZvMPbR3SxSmZ4,1692
16
16
  xp/cli/commands/conbus/conbus_datapoint_commands.py,sha256=r36OuTjREtbGKL-bskAGa0-WLw7x06td6woZn3GYJNA,3630
17
- xp/cli/commands/conbus/conbus_discover_commands.py,sha256=Jt-OjARc-QXCr453bhKG0zXRqvOMVKZr328zNxSDulY,1373
17
+ xp/cli/commands/conbus/conbus_discover_commands.py,sha256=-y3TDgOnw1_cjvxvgyfQ1GQE2_WmYq-l8Md7DsdTXmo,1719
18
18
  xp/cli/commands/conbus/conbus_lightlevel_commands.py,sha256=FpCwogdxa7yFUjlrxM7e8Q2Ut32tKAHabngQQChvtJI,6763
19
19
  xp/cli/commands/conbus/conbus_linknumber_commands.py,sha256=KitaGDM5HpwVUz8rLpO8VZUypUTcAg3Bzl0DVm6gnSk,3391
20
20
  xp/cli/commands/conbus/conbus_modulenumber_commands.py,sha256=L7-6y3rDllOjQ9g6Bk_RiTKIhAOHVPLdxWif9exkngs,3463
@@ -39,7 +39,7 @@ xp/cli/commands/telegram/telegram_discover_commands.py,sha256=0UArJinw1eWFbee5EG
39
39
  xp/cli/commands/telegram/telegram_linknumber_commands.py,sha256=7j0-E5Moqqga4NrKDch82C6glaFDFMQn5_3hMwie7BQ,2511
40
40
  xp/cli/commands/telegram/telegram_parse_commands.py,sha256=_OYOso1hS4f_ox96qlkYL2SuFnmimpAvqqdYlLzX9yo,2232
41
41
  xp/cli/commands/telegram/telegram_version_commands.py,sha256=WQyx1-B9yJ8V9WrFyBpOvULJ-jq12GoZZDDoRbM7eyw,1553
42
- xp/cli/main.py,sha256=52nMDP2Pq-W1PP4_BU5ybyTuO6QrStWOeLr44Wl_gB8,2812
42
+ xp/cli/main.py,sha256=3TY4wZoKMK8kQBgOn0WshTsag4J4ofoGoGPgg12wueM,2810
43
43
  xp/cli/utils/__init__.py,sha256=gTGIj60Uai0iE7sr9_TtEpl04fD7krtTzbbigXUsUVU,46
44
44
  xp/cli/utils/click_tree.py,sha256=ilmM2IMa_c-TqUMsv2alrZXuS0BNhvVlrBlSfyN8lzM,1670
45
45
  xp/cli/utils/datapoint_type_choice.py,sha256=HcydhlqxZ7YyorEeTjFGkypF2JnYNPvOzkl1rhZ93Fc,1666
@@ -74,11 +74,11 @@ xp/models/conbus/conbus_receive.py,sha256=-1u1qK-texfKCNZV-GYf_9KyLtJdIrx7HuZsKz
74
74
  xp/models/conbus/conbus_writeconfig.py,sha256=z8fdJeFLyGJW7UMHcHxGrHIMS6gG1D3aXeYUkBuwnEg,2136
75
75
  xp/models/homekit/__init__.py,sha256=5HDSOClCu0ArK3IICn3_LDMMLBAzLjBxUUSF73bxSSk,34
76
76
  xp/models/homekit/homekit_accessory.py,sha256=NsHFhskuxIdJpF9-MvXHtjkLYvNHmSGFOy0GmQv3PY4,1038
77
- xp/models/homekit/homekit_config.py,sha256=Y1WwrbISRtJOkKVBnXQULb3vAOzcOdt95hBAI8cM_MU,2771
77
+ xp/models/homekit/homekit_config.py,sha256=Y_k92PsKHFBnn3r1_RSZHJP5uLH27Gw8G7Bj5N8jvUE,2904
78
78
  xp/models/homekit/homekit_conson_config.py,sha256=NML644Ij7abstMbud-TUPcxraGY4vQwKrkJOwftv2pM,2603
79
79
  xp/models/log_entry.py,sha256=kPcYuAirCXugfL3YkOK9cQVlkNWxG_8a4AVuhsykHL0,4355
80
80
  xp/models/protocol/__init__.py,sha256=TJ_CJKchA-xgQiv5vCo_ndBBZjrcaTmjT74bR0T-5Cw,38
81
- xp/models/protocol/conbus_protocol.py,sha256=tSnI5pxBTP_f1DUzEM3XbyjK7vsqwm0gpzHIH1gTg8E,8854
81
+ xp/models/protocol/conbus_protocol.py,sha256=3uWYE_t_-mp_2wPEgbDHbZoeQSEv48IdRcQpQyemEY0,9141
82
82
  xp/models/response.py,sha256=h6_B1k_v6IrWhgNWBohEGQGRCp5TcVhgQ3RJS8gTkhY,1230
83
83
  xp/models/telegram/__init__.py,sha256=-_exhjlRLbBNuPxHC4tLA2SAgf8M0yHJMeyEoQIk9PI,53
84
84
  xp/models/telegram/action_type.py,sha256=vkw_chTgmsadksGXvZ9D_qYGpjOwCw-OgbEi8Bml17g,783
@@ -115,8 +115,8 @@ xp/services/conbus/conbus_blink_all_service.py,sha256=OaEg4b8AEiEruHSkZ5jDtaoI81
115
115
  xp/services/conbus/conbus_blink_service.py,sha256=x9uM-sLnIEV8wSNsvJgo08E042g-Hh2ZF3rXkz-k_9s,5824
116
116
  xp/services/conbus/conbus_custom_service.py,sha256=4aneYdPObiZOGxPFYg5Wr70cl_xFxlQIdJBPQSa0enI,5826
117
117
  xp/services/conbus/conbus_datapoint_queryall_service.py,sha256=p9R02cVimhdJILHQ6BoeZj8Hog4oRpqBnMo3t4R8ecY,6816
118
- xp/services/conbus/conbus_datapoint_service.py,sha256=NsqRQfNsZ4_Pbe7kcMQpUqfhVPH7H148JDWH49ExQ1E,6392
119
- xp/services/conbus/conbus_discover_service.py,sha256=lH6I8YcN7Beo_f-M8XkNZ_5UuNB-x2R9U5xJNTK-QXE,10110
118
+ xp/services/conbus/conbus_datapoint_service.py,sha256=SYhHj9RmTmaJ750tyZ1IW2kl7tgDQ1xm_EM1zUjk1aQ,6421
119
+ xp/services/conbus/conbus_discover_service.py,sha256=sSCSDNWWGtx5QOShwJfcbG54WCYH-BxWvgE10ghibN4,12326
120
120
  xp/services/conbus/conbus_output_service.py,sha256=mHFOAPx2zo0TStZ3pokp6v94AQjIamcwZDeg5YH_-eo,7240
121
121
  xp/services/conbus/conbus_raw_service.py,sha256=4yZLLTIAOxpgByUTWZXw1ihGa6Xtl98ckj9T7VfprDI,4335
122
122
  xp/services/conbus/conbus_receive_service.py,sha256=frXrS0OyKKvYYQTWdma21Kd0BKw5aSuHn3ZXTTqOaj0,3953
@@ -124,21 +124,22 @@ xp/services/conbus/conbus_scan_service.py,sha256=tHJ5qaxcNXxAZb2D2F1v6IrzydfxjJO
124
124
  xp/services/conbus/write_config_service.py,sha256=6feNdixI_Nli4MRLe15nea-7gTEXMUwZIvTqv_1OqHI,7157
125
125
  xp/services/homekit/__init__.py,sha256=xAMKmln_AmEFdOOJGKWYi96seRlKDQpKx3-hm7XbdIo,36
126
126
  xp/services/homekit/homekit_cache_service.py,sha256=NdijyH5_iyhsTHBb-OyT8Y2xnNDj8F5MP8neoVQ26hY,11010
127
- xp/services/homekit/homekit_conbus_service.py,sha256=EYhYqGISPxUB6QrMDLWRPOA4S8f9AgJf0D_BKqXlGLA,3482
127
+ xp/services/homekit/homekit_conbus_service.py,sha256=porvo4nZ4PE0R6bdaKwClDW2kevkW_8DOzw4NOO6DZQ,3348
128
128
  xp/services/homekit/homekit_config_validator.py,sha256=1jCUrfMUqthEoNc7wAk4io1PpbptqgsdnCOCzz62Io8,10732
129
129
  xp/services/homekit/homekit_conson_validator.py,sha256=V_Otxu0q3LK-3GP3-Zw0nZyV_rb9RjHp4tWcvzYJeGs,3787
130
130
  xp/services/homekit/homekit_dimminglight.py,sha256=O5SwnhkrJQHm7BopI9nL9QcfRSokcxn-H4csZMg0B5c,5770
131
131
  xp/services/homekit/homekit_dimminglight_service.py,sha256=lxFE2k8a8V9YQwpWPAAmbUvYD3zDlVa337xzDQl6tjM,5231
132
132
  xp/services/homekit/homekit_hap_service.py,sha256=YrFe10XPBf6EC2SRnWmcCbjdVkrHjxSNPwYDPVvQiDQ,12527
133
133
  xp/services/homekit/homekit_lightbulb.py,sha256=7HGMIPwEcmvSs89ENcDhdb8g0R9WMq7115gYuwcskAs,3661
134
- xp/services/homekit/homekit_lightbulb_service.py,sha256=pyRyxrNQqTW79rmGagItNpz7AMXTbBQL_l1o5VXTRn8,2652
134
+ xp/services/homekit/homekit_lightbulb_service.py,sha256=G_ummBFiBurhQ2ZVwJ9l_aZ2MQgl5Uq-oi3KjIrdb-Y,2752
135
135
  xp/services/homekit/homekit_module_service.py,sha256=7lanEinxAfTdn28ZHV-O5YyTqq_3v8FIyP2FI0jsEQM,1526
136
- xp/services/homekit/homekit_outlet.py,sha256=_Glfytfmvydz9TEn69N6bw3Ux0Y-EMw2eJPgeborkoA,5268
137
- xp/services/homekit/homekit_outlet_service.py,sha256=mDvSv9_69Z6jGUBHixmBm7giDCVYtLOWtMZqYjBgUdE,3698
136
+ xp/services/homekit/homekit_outlet.py,sha256=TtrOwVF3BkEvDcTOkNJIWT64zhtPLFkDgtyzW6u_4yQ,5209
137
+ xp/services/homekit/homekit_outlet_service.py,sha256=y7DbWbbvihWwF1Gyl0l9Hup1JHin6PTlDEHdoIqTfEQ,3798
138
138
  xp/services/homekit/homekit_service.py,sha256=0lW-hg40ETco3gDBEYkR_sX-UIYsLSKCD4NWruLXUM8,14031
139
139
  xp/services/log_file_service.py,sha256=fvPcZQj8XOKUA-4ep5R8n0PelvwvRlTLlVxvIWM5KR4,10506
140
140
  xp/services/module_type_service.py,sha256=xWhr1EAZMykL5uNWHWdpa5T8yNruGKH43XRTOS8GwZg,7477
141
- xp/services/protocol/__init__.py,sha256=WuYn2iEcvsOIXnn5HCrU9kD3PjuMX1sIh0ljKISDoJw,720
141
+ xp/services/protocol/__init__.py,sha256=qRufBmqRKGzpuzZ5bxBbmwf510TT00Ke8s5HcWGnqRY,818
142
+ xp/services/protocol/conbus_event_protocol.py,sha256=btWLGM-onWXVIvL5atD7HgQKNcx6F8dNqTZf2CSquiE,12272
142
143
  xp/services/protocol/conbus_protocol.py,sha256=JO7yLkD_ohPT0ETjnAIx4CGpZyobf4LdbuozM_43btE,10276
143
144
  xp/services/protocol/protocol_factory.py,sha256=PmjN9AtW9sxNo3voqUiNgQA-pTvX1RW4XXFlHKfFr5Q,2470
144
145
  xp/services/protocol/telegram_protocol.py,sha256=Ki5DrXsKxiaqLcdP9WWUuhUI7cPu2DfwyZkh-Gv9Lb8,9496
@@ -164,8 +165,8 @@ xp/services/telegram/telegram_service.py,sha256=XrP1CPi0ckxoKBaNwLA6lo-TogWxXgmX
164
165
  xp/services/telegram/telegram_version_service.py,sha256=M5HdOTsLdcwo122FP-jW6R740ktLrtKf2TiMDVz23h8,10528
165
166
  xp/utils/__init__.py,sha256=_avMF_UOkfR3tNeDIPqQ5odmbq5raKkaq1rZ9Cn1CJs,332
166
167
  xp/utils/checksum.py,sha256=HDpiQxmdIedbCbZ4o_Box0i_Zig417BtCV_46ZyhiTk,1711
167
- xp/utils/dependencies.py,sha256=ibTkpQ7nak2ioqlPtb7yCqBJ9tl3NHSxDdODtUItbCg,20281
168
+ xp/utils/dependencies.py,sha256=yU5BcD9DnByEL1dL9gdTEaJfxlyBEkomIg_AXGnzDk0,20591
168
169
  xp/utils/event_helper.py,sha256=W-A_xmoXlpWZBbJH6qdaN50o3-XrwFsDgvAGMJDiAgo,1001
169
170
  xp/utils/serialization.py,sha256=RWHHk86feaB4ZP7rjE4qOWK0900yg2joUBDkP76gfOY,4618
170
171
  xp/utils/time_utils.py,sha256=dEyViDlAG9GWU-J3D_YVa-sGma6yiyyMTgN4h2x3PY4,3781
171
- conson_xp-1.14.0.dist-info/RECORD,,
172
+ conson_xp-1.16.0.dist-info/RECORD,,
xp/__init__.py CHANGED
@@ -3,7 +3,7 @@
3
3
  conson-xp package.
4
4
  """
5
5
 
6
- __version__ = "1.14.0"
6
+ __version__ = "1.16.0"
7
7
  __manufacturer__ = "salchichon"
8
8
  __model__ = "xp.cli"
9
9
  __serial__ = "2025.09.23.000"
@@ -9,6 +9,7 @@ from xp.cli.utils.decorators import (
9
9
  connection_command,
10
10
  )
11
11
  from xp.models import ConbusDiscoverResponse
12
+ from xp.models.conbus.conbus_discover import DiscoveredDevice
12
13
  from xp.services.conbus.conbus_discover_service import (
13
14
  ConbusDiscoverService,
14
15
  )
@@ -36,6 +37,14 @@ def send_discover_telegram(ctx: click.Context) -> None:
36
37
  """
37
38
  click.echo(json.dumps(discovered_devices.to_dict(), indent=2))
38
39
 
40
+ def on_device_discovered(discovered_device: DiscoveredDevice) -> None:
41
+ """Handle discovery of sa single module.
42
+
43
+ Args:
44
+ discovered_device: Discover device.
45
+ """
46
+ click.echo(json.dumps(discovered_device, indent=2))
47
+
39
48
  def progress(_serial_number: str) -> None:
40
49
  """Handle progress updates during device discovery.
41
50
 
@@ -48,5 +57,5 @@ def send_discover_telegram(ctx: click.Context) -> None:
48
57
  service: ConbusDiscoverService = (
49
58
  ctx.obj.get("container").get_container().resolve(ConbusDiscoverService)
50
59
  )
51
- with service:
52
- service.start(progress, on_finish, 0.5)
60
+ service.run(progress, on_device_discovered, on_finish, 5)
61
+ service.start_reactor()
xp/cli/main.py CHANGED
@@ -56,7 +56,7 @@ def cli(ctx: click.Context) -> None:
56
56
 
57
57
  # xp
58
58
  logging.getLogger("xp").setLevel(logging.DEBUG)
59
- logging.getLogger("xp.services.homekit").setLevel(logging.WARNING)
59
+ logging.getLogger("xp.services.homekit").setLevel(logging.DEBUG)
60
60
 
61
61
  # pyhap
62
62
  logging.getLogger("pyhap").setLevel(logging.WARNING)
@@ -55,6 +55,8 @@ class HomekitAccessoryConfig(BaseModel):
55
55
  output_number: Output number for the accessory.
56
56
  description: Description of the accessory.
57
57
  service: Service type for the accessory.
58
+ on_action: on code for the accessory.
59
+ off_action: off code for the accessory.
58
60
  hap_accessory: Optional HAP accessory identifier.
59
61
  """
60
62
 
@@ -64,6 +66,8 @@ class HomekitAccessoryConfig(BaseModel):
64
66
  output_number: int
65
67
  description: str
66
68
  service: str
69
+ on_action: str
70
+ off_action: str
67
71
  hap_accessory: Optional[int] = None
68
72
 
69
73
 
@@ -12,6 +12,7 @@ from xp.models.homekit.homekit_conson_config import ConsonModuleConfig
12
12
  from xp.models.telegram.datapoint_type import DataPointType
13
13
 
14
14
  if TYPE_CHECKING:
15
+ from xp.services.protocol.conbus_event_protocol import ConbusEventProtocol
15
16
  from xp.services.protocol.conbus_protocol import ConbusProtocol
16
17
  from xp.services.protocol.telegram_protocol import TelegramProtocol
17
18
 
@@ -61,11 +62,15 @@ class SendActionEvent(BaseEvent):
61
62
  serial_number: Serial number of the light bulb set.
62
63
  output_number: Output number of the light bulb set.
63
64
  value: Set light bulb On or Off (True/False).
65
+ on_action: On action E00L00I00.
66
+ off_action: On action E00L00I04.
64
67
  """
65
68
 
66
69
  serial_number: str = Field(description="Serial number of the light bulb set")
67
70
  output_number: int = Field(description="Output number of the light bulb set")
68
71
  value: bool = Field(description="Set light bulb On or Off (True/False)")
72
+ on_action: str = Field(description="on action")
73
+ off_action: str = Field(description="off action")
69
74
 
70
75
 
71
76
  class DatapointEvent(BaseEvent):
@@ -245,7 +250,7 @@ class TelegramEvent(BaseEvent):
245
250
  checksum_valid: Checksum valid true or false.
246
251
  """
247
252
 
248
- protocol: Union[TelegramProtocol, ConbusProtocol] = Field(
253
+ protocol: Union[TelegramProtocol, ConbusProtocol, ConbusEventProtocol] = Field(
249
254
  description="TelegramProtocol instance"
250
255
  )
251
256
  frame: str = Field(description="Frame <S0123450001F02D12FK>")
@@ -130,6 +130,7 @@ class ConbusDatapointService(ConbusProtocol):
130
130
  self.service_response.data_value = datapoint_telegram.data_value
131
131
  if self.datapoint_finished_callback:
132
132
  self.datapoint_finished_callback(self.service_response)
133
+ self._stop_reactor()
133
134
 
134
135
  def failed(self, message: str) -> None:
135
136
  """Handle failed connection event.
@@ -7,49 +7,57 @@ discover telegrams to find modules on the network.
7
7
  import logging
8
8
  from typing import Callable, Optional
9
9
 
10
- from twisted.internet.posixbase import PosixReactorBase
11
-
12
- from xp.models import ConbusClientConfig, ConbusDiscoverResponse
10
+ from xp.models import ConbusDiscoverResponse
13
11
  from xp.models.conbus.conbus_discover import DiscoveredDevice
14
12
  from xp.models.protocol.conbus_protocol import TelegramReceivedEvent
15
13
  from xp.models.telegram.datapoint_type import DataPointType
16
14
  from xp.models.telegram.module_type_code import MODULE_TYPE_REGISTRY
17
15
  from xp.models.telegram.system_function import SystemFunction
18
16
  from xp.models.telegram.telegram_type import TelegramType
19
- from xp.services.protocol.conbus_protocol import ConbusProtocol
17
+ from xp.services.protocol.conbus_event_protocol import ConbusEventProtocol
20
18
 
21
19
 
22
- class ConbusDiscoverService(ConbusProtocol):
20
+ class ConbusDiscoverService:
23
21
  """
24
22
  Service for discovering modules on Conbus servers.
25
23
 
26
24
  Uses ConbusProtocol to provide discovery functionality for finding
27
25
  modules connected to the Conbus network.
26
+
27
+ Attributes:
28
+ conbus_protocol: Protocol instance for Conbus communication.
28
29
  """
29
30
 
30
- def __init__(
31
- self,
32
- cli_config: ConbusClientConfig,
33
- reactor: PosixReactorBase,
34
- ) -> None:
31
+ conbus_protocol: ConbusEventProtocol
32
+
33
+ def __init__(self, conbus_protocol: ConbusEventProtocol) -> None:
35
34
  """Initialize the Conbus discover service.
36
35
 
37
36
  Args:
38
- cli_config: Conbus client configuration.
39
- reactor: Twisted reactor instance.
37
+ conbus_protocol: ConbusProtocol.
40
38
  """
41
- super().__init__(cli_config, reactor)
42
39
  self.progress_callback: Optional[Callable[[str], None]] = None
40
+ self.device_discover_callback: Optional[Callable[[DiscoveredDevice], None]] = (
41
+ None
42
+ )
43
43
  self.finish_callback: Optional[Callable[[ConbusDiscoverResponse], None]] = None
44
44
 
45
+ self.conbus_protocol: ConbusEventProtocol = conbus_protocol
46
+ self.conbus_protocol.on_connection_made.connect(self.connection_made)
47
+ self.conbus_protocol.on_telegram_sent.connect(self.telegram_sent)
48
+ self.conbus_protocol.on_telegram_received.connect(self.telegram_received)
49
+ self.conbus_protocol.on_timeout.connect(self.timeout)
50
+ self.conbus_protocol.on_failed.connect(self.failed)
51
+
45
52
  self.discovered_device_result = ConbusDiscoverResponse(success=False)
46
53
  # Set up logging
47
54
  self.logger = logging.getLogger(__name__)
48
55
 
49
- def connection_established(self) -> None:
56
+ def connection_made(self) -> None:
50
57
  """Handle connection established event."""
51
- self.logger.debug("Connection established, sending discover telegram")
52
- self.send_telegram(
58
+ self.logger.debug("Connection established")
59
+ self.logger.debug("Sending discover telegram")
60
+ self.conbus_protocol.send_telegram(
53
61
  telegram_type=TelegramType.SYSTEM,
54
62
  serial_number="0000000000",
55
63
  system_function=SystemFunction.DISCOVERY,
@@ -83,7 +91,7 @@ class ConbusDiscoverService(ConbusProtocol):
83
91
  and telegram_received.payload[11:16] == "F01D"
84
92
  and len(telegram_received.payload) == 15
85
93
  ):
86
- self.discovered_device(telegram_received.serial_number)
94
+ self.handle_discovered_device(telegram_received.serial_number)
87
95
 
88
96
  # Check for module type response (F02D07)
89
97
  elif (
@@ -109,7 +117,7 @@ class ConbusDiscoverService(ConbusProtocol):
109
117
  else:
110
118
  self.logger.debug("Not a discover or module type response")
111
119
 
112
- def discovered_device(self, serial_number: str) -> None:
120
+ def handle_discovered_device(self, serial_number: str) -> None:
113
121
  """Handle discovered device event.
114
122
 
115
123
  Args:
@@ -128,22 +136,17 @@ class ConbusDiscoverService(ConbusProtocol):
128
136
  }
129
137
  self.discovered_device_result.discovered_devices.append(device)
130
138
 
139
+ if self.device_discover_callback:
140
+ self.device_discover_callback(device)
141
+
131
142
  # Send READ_DATAPOINT telegram to query module type
132
143
  self.logger.debug(f"Sending module type query for {serial_number}")
133
- self.send_telegram(
144
+ self.conbus_protocol.send_telegram(
134
145
  telegram_type=TelegramType.SYSTEM,
135
146
  serial_number=serial_number,
136
147
  system_function=SystemFunction.READ_DATAPOINT,
137
148
  data_value=DataPointType.MODULE_TYPE.value,
138
149
  )
139
-
140
- self.send_telegram(
141
- telegram_type=TelegramType.SYSTEM,
142
- serial_number=serial_number,
143
- system_function=SystemFunction.READ_DATAPOINT,
144
- data_value=DataPointType.MODULE_TYPE_CODE.value,
145
- )
146
-
147
150
  if self.progress_callback:
148
151
  self.progress_callback(serial_number)
149
152
 
@@ -190,11 +193,27 @@ class ConbusDiscoverService(ConbusProtocol):
190
193
  if device["serial_number"] == serial_number:
191
194
  device["module_type_code"] = code
192
195
  device["module_type_name"] = module_type_name
196
+
197
+ if self.device_discover_callback:
198
+ self.device_discover_callback(device)
199
+
193
200
  self.logger.debug(
194
201
  f"Updated device {serial_number} with module_type {module_type_name}"
195
202
  )
196
203
  break
197
204
 
205
+ if self.discovered_device_result.discovered_devices:
206
+ for device in self.discovered_device_result.discovered_devices:
207
+ if not (
208
+ device["serial_number"]
209
+ and device["module_type"]
210
+ and device["module_type_code"]
211
+ and device["module_type_name"]
212
+ ):
213
+ return
214
+
215
+ self.succeed()
216
+
198
217
  def handle_module_type_response(self, serial_number: str, module_type: str) -> None:
199
218
  """Handle module type response and update discovered device.
200
219
 
@@ -212,19 +231,28 @@ class ConbusDiscoverService(ConbusProtocol):
212
231
  self.logger.debug(
213
232
  f"Updated device {serial_number} with module_type {module_type}"
214
233
  )
234
+ if self.device_discover_callback:
235
+ self.device_discover_callback(device)
236
+
215
237
  break
216
238
 
217
- def timeout(self) -> bool:
218
- """Handle timeout event to stop discovery.
239
+ self.conbus_protocol.send_telegram(
240
+ telegram_type=TelegramType.SYSTEM,
241
+ serial_number=serial_number,
242
+ system_function=SystemFunction.READ_DATAPOINT,
243
+ data_value=DataPointType.MODULE_TYPE_CODE.value,
244
+ )
219
245
 
220
- Returns:
221
- False to stop the reactor.
222
- """
223
- self.logger.info("Discovery stopped after: %ss", self.timeout_seconds)
224
- self.discovered_device_result.success = True
246
+ def timeout(self) -> None:
247
+ """Handle timeout event to stop discovery."""
248
+ timeout = self.conbus_protocol.timeout_seconds
249
+ self.logger.info("Discovery stopped after: %ss", timeout)
250
+ self.discovered_device_result.success = False
251
+ self.discovered_device_result.error = "Discovered device timeout"
225
252
  if self.finish_callback:
226
253
  self.finish_callback(self.discovered_device_result)
227
- return False
254
+
255
+ self.stop_reactor()
228
256
 
229
257
  def failed(self, message: str) -> None:
230
258
  """Handle failed connection event.
@@ -238,9 +266,32 @@ class ConbusDiscoverService(ConbusProtocol):
238
266
  if self.finish_callback:
239
267
  self.finish_callback(self.discovered_device_result)
240
268
 
241
- def start(
269
+ self.stop_reactor()
270
+
271
+ def succeed(self) -> None:
272
+ """Handle discovered device success event."""
273
+ self.logger.debug("Succeed")
274
+ self.discovered_device_result.success = True
275
+ self.discovered_device_result.error = None
276
+ if self.finish_callback:
277
+ self.finish_callback(self.discovered_device_result)
278
+
279
+ self.stop_reactor()
280
+
281
+ def stop_reactor(self) -> None:
282
+ """Stop reactor."""
283
+ self.logger.info("Stopping reactor")
284
+ self.conbus_protocol.stop_reactor()
285
+
286
+ def start_reactor(self) -> None:
287
+ """Start reactor."""
288
+ self.logger.info("Starting reactor")
289
+ self.conbus_protocol.start_reactor()
290
+
291
+ def run(
242
292
  self,
243
293
  progress_callback: Callable[[str], None],
294
+ device_discover_callback: Callable[[DiscoveredDevice], None],
244
295
  finish_callback: Callable[[ConbusDiscoverResponse], None],
245
296
  timeout_seconds: Optional[float] = None,
246
297
  ) -> None:
@@ -248,12 +299,14 @@ class ConbusDiscoverService(ConbusProtocol):
248
299
 
249
300
  Args:
250
301
  progress_callback: Callback for each discovered device.
302
+ device_discover_callback: Callback for each discovered device.
251
303
  finish_callback: Callback when discovery completes.
252
304
  timeout_seconds: Optional timeout in seconds.
253
305
  """
254
306
  self.logger.info("Starting discovery")
307
+
255
308
  if timeout_seconds:
256
- self.timeout_seconds = timeout_seconds
309
+ self.conbus_protocol.timeout_seconds = timeout_seconds
257
310
  self.progress_callback = progress_callback
311
+ self.device_discover_callback = device_discover_callback
258
312
  self.finish_callback = finish_callback
259
- self.start_reactor()
@@ -12,7 +12,6 @@ from xp.models.protocol.conbus_protocol import (
12
12
  SendActionEvent,
13
13
  SendWriteConfigEvent,
14
14
  )
15
- from xp.models.telegram.action_type import ActionType
16
15
  from xp.models.telegram.datapoint_type import DataPointType
17
16
  from xp.models.telegram.system_function import SystemFunction
18
17
  from xp.services.protocol.telegram_protocol import TelegramProtocol
@@ -87,11 +86,8 @@ class HomeKitConbusService:
87
86
  """
88
87
  self.logger.debug(f"send_action_event {event}")
89
88
 
90
- action_value = (
91
- ActionType.ON_RELEASE.value if event.value else ActionType.OFF_PRESS.value
92
- )
93
- input_action = f"{event.output_number:02d}{action_value}"
94
- telegram = (
95
- f"S{event.serial_number}F{SystemFunction.ACTION.value}D{input_action}"
96
- )
97
- self.telegram_protocol.sendFrame(telegram.encode())
89
+ telegram = event.on_action if event.value else event.off_action
90
+ telegram_make = f"{telegram}M"
91
+ telegram_break = f"{telegram}B"
92
+ self.telegram_protocol.sendFrame(telegram_make.encode())
93
+ self.telegram_protocol.sendFrame(telegram_break.encode())
@@ -77,6 +77,8 @@ class HomeKitLightbulbService:
77
77
  serial_number=event.serial_number,
78
78
  output_number=event.output_number,
79
79
  value=event.value,
80
+ on_action=event.accessory.on_action,
81
+ off_action=event.accessory.off_action,
80
82
  )
81
83
 
82
84
  self.logger.debug(f"Dispatching SendActionEvent for {event.serial_number}")
@@ -134,19 +134,18 @@ class Outlet(Accessory):
134
134
  value: True to turn on, False to turn off.
135
135
  """
136
136
  # Emit set event
137
- self.logger.debug(f"set_on {value}")
138
-
139
- if value != self.is_on:
140
- self.is_on = value
141
- self.event_bus.dispatch(
142
- OutletSetOnEvent(
143
- serial_number=self.accessory.serial_number,
144
- output_number=self.accessory.output_number,
145
- module=self.module,
146
- accessory=self.accessory,
147
- value=value,
148
- )
137
+ self.logger.debug(f"set_on {value} {self.is_on}")
138
+
139
+ self.is_on = value
140
+ self.event_bus.dispatch(
141
+ OutletSetOnEvent(
142
+ serial_number=self.accessory.serial_number,
143
+ output_number=self.accessory.output_number,
144
+ module=self.module,
145
+ accessory=self.accessory,
146
+ value=value,
149
147
  )
148
+ )
150
149
 
151
150
  def get_on(self) -> bool:
152
151
  """Get the on/off state of the outlet.
@@ -85,6 +85,8 @@ class HomeKitOutletService:
85
85
  serial_number=event.serial_number,
86
86
  output_number=event.output_number,
87
87
  value=event.value,
88
+ on_action=event.accessory.on_action,
89
+ off_action=event.accessory.off_action,
88
90
  )
89
91
 
90
92
  self.logger.debug(f"Dispatching SendActionEvent for {event.serial_number}")
@@ -7,10 +7,11 @@ from xp.models.protocol.conbus_protocol import (
7
7
  ModuleDiscoveredEvent,
8
8
  TelegramReceivedEvent,
9
9
  )
10
+ from xp.services.protocol.conbus_event_protocol import ConbusEventProtocol
10
11
  from xp.services.protocol.conbus_protocol import ConbusProtocol
11
12
  from xp.services.protocol.telegram_protocol import TelegramProtocol
12
13
 
13
- __all__ = ["TelegramProtocol", "ConbusProtocol"]
14
+ __all__ = ["TelegramProtocol", "ConbusProtocol", "ConbusEventProtocol"]
14
15
 
15
16
  # Rebuild models after TelegramProtocol and ConbusProtocol are imported to resolve forward references
16
17
  ConnectionMadeEvent.model_rebuild()
@@ -0,0 +1,342 @@
1
+ """Conbus Event Protocol for XP telegram communication.
2
+
3
+ This module implements the Twisted protocol for Conbus communication.
4
+ """
5
+
6
+ import logging
7
+ from queue import SimpleQueue
8
+ from random import randint
9
+ from threading import Lock
10
+ from typing import Any, Optional
11
+
12
+ from psygnal import Signal
13
+ from twisted.internet import protocol
14
+ from twisted.internet.base import DelayedCall
15
+ from twisted.internet.interfaces import IAddress, IConnector
16
+ from twisted.internet.posixbase import PosixReactorBase
17
+ from twisted.python.failure import Failure
18
+
19
+ from xp.models import ConbusClientConfig
20
+ from xp.models.protocol.conbus_protocol import (
21
+ TelegramReceivedEvent,
22
+ )
23
+ from xp.models.telegram.system_function import SystemFunction
24
+ from xp.models.telegram.telegram_type import TelegramType
25
+ from xp.utils import calculate_checksum
26
+
27
+
28
+ class ConbusEventProtocol(protocol.Protocol, protocol.ClientFactory):
29
+ """Twisted protocol for XP telegram communication.
30
+
31
+ Attributes:
32
+ buffer: Buffer for incoming telegram data.
33
+ logger: Logger instance for this protocol.
34
+ cli_config: Conbus configuration settings.
35
+ timeout_seconds: Timeout duration in seconds.
36
+ timeout_call: Delayed call handle for timeout management.
37
+ telegram_queue: FIFO queue for outgoing telegrams.
38
+ queue_manager_running: Flag indicating if queue manager is active.
39
+ queue_manager_lock: Lock for thread-safe queue manager access.
40
+ on_connection_made: Signal emitted when connection is established.
41
+ on_connection_lost: Signal emitted when connection is lost.
42
+ on_connection_failed: Signal emitted when connection fails.
43
+ on_client_connection_failed: Signal emitted when client connection fails.
44
+ on_client_connection_lost: Signal emitted when client connection is lost.
45
+ on_send_frame: Signal emitted when a frame is sent.
46
+ on_telegram_sent: Signal emitted when a telegram is sent.
47
+ on_data_received: Signal emitted when data is received.
48
+ on_telegram_received: Signal emitted when a telegram is received.
49
+ on_timeout: Signal emitted when timeout occurs.
50
+ on_failed: Signal emitted when operation fails.
51
+ on_start_reactor: Signal emitted when reactor starts.
52
+ on_stop_reactor: Signal emitted when reactor stops.
53
+ """
54
+
55
+ buffer: bytes
56
+
57
+ telegram_queue: SimpleQueue[bytes] = SimpleQueue() # FIFO
58
+ queue_manager_running: bool = False
59
+ queue_manager_lock: Lock = Lock()
60
+
61
+ on_connection_made: Signal = Signal()
62
+ on_connection_lost: Signal = Signal()
63
+ on_connection_failed: Signal = Signal(Failure)
64
+ on_client_connection_failed: Signal = Signal(Failure)
65
+ on_client_connection_lost: Signal = Signal(Failure)
66
+ on_send_frame: Signal = Signal(bytes)
67
+ on_telegram_sent: Signal = Signal(bytes)
68
+ on_data_received: Signal = Signal(bytes)
69
+ on_telegram_received: Signal = Signal(TelegramReceivedEvent)
70
+ on_timeout: Signal = Signal()
71
+ on_failed: Signal = Signal(str)
72
+ on_start_reactor: Signal = Signal()
73
+ on_stop_reactor: Signal = Signal()
74
+
75
+ def __init__(
76
+ self,
77
+ cli_config: ConbusClientConfig,
78
+ reactor: PosixReactorBase,
79
+ ) -> None:
80
+ """Initialize ConbusProtocol.
81
+
82
+ Args:
83
+ cli_config: Configuration for Conbus client connection.
84
+ reactor: Twisted reactor for event handling.
85
+ """
86
+ self.buffer = b""
87
+ self.logger = logging.getLogger(__name__)
88
+ self.cli_config = cli_config.conbus
89
+ self._reactor = reactor
90
+ self.timeout_seconds = self.cli_config.timeout
91
+ self.timeout_call: Optional[DelayedCall] = None
92
+
93
+ def connectionMade(self) -> None:
94
+ """Handle connection established event.
95
+
96
+ Called when TCP connection is successfully established.
97
+ Starts inactivity timeout monitoring.
98
+ """
99
+ self.logger.debug("connectionMade")
100
+ self.on_connection_made.emit()
101
+
102
+ # Start inactivity timeout
103
+ self._reset_timeout()
104
+
105
+ def dataReceived(self, data: bytes) -> None:
106
+ """Handle received data from TCP connection.
107
+
108
+ Parses incoming telegram frames and dispatches events.
109
+
110
+ Args:
111
+ data: Raw bytes received from connection.
112
+ """
113
+ self.logger.debug("dataReceived")
114
+ self.on_data_received.emit(data)
115
+ self.buffer += data
116
+
117
+ while True:
118
+ start = self.buffer.find(b"<")
119
+ if start == -1:
120
+ break
121
+
122
+ end = self.buffer.find(b">", start)
123
+ if end == -1:
124
+ break
125
+
126
+ # <S0123450001F02D12FK>
127
+ # <R0123450001F02D12FK>
128
+ # <E12L01I08MAK>
129
+ frame = self.buffer[start : end + 1] # <S0123450001F02D12FK>
130
+ self.buffer = self.buffer[end + 1 :]
131
+ telegram = frame[1:-1] # S0123450001F02D12FK
132
+ telegram_type = telegram[0:1].decode() # S
133
+ payload = telegram[:-2] # S0123450001F02D12
134
+ checksum = telegram[-2:].decode() # FK
135
+ serial_number = (
136
+ telegram[1:11] if telegram_type in ("S", "R") else b""
137
+ ) # 0123450001
138
+ calculated_checksum = calculate_checksum(payload.decode(encoding="latin-1"))
139
+
140
+ checksum_valid = checksum == calculated_checksum
141
+ if not checksum_valid:
142
+ self.logger.debug(
143
+ f"Invalid checksum: {checksum}, calculated: {calculated_checksum}"
144
+ )
145
+
146
+ self.logger.debug(
147
+ f"frameReceived payload: {payload.decode('latin-1')}, checksum: {checksum}"
148
+ )
149
+
150
+ # Reset timeout on activity
151
+ self._reset_timeout()
152
+
153
+ telegram_received = TelegramReceivedEvent(
154
+ protocol=self,
155
+ frame=frame.decode("latin-1"),
156
+ telegram=telegram.decode("latin-1"),
157
+ payload=payload.decode("latin-1"),
158
+ telegram_type=telegram_type,
159
+ serial_number=serial_number,
160
+ checksum=checksum,
161
+ checksum_valid=checksum_valid,
162
+ )
163
+ self.on_telegram_received.emit(telegram_received)
164
+
165
+ def sendFrame(self, data: bytes) -> None:
166
+ """Send telegram frame.
167
+
168
+ Args:
169
+ data: Raw telegram payload (without checksum/framing).
170
+
171
+ Raises:
172
+ IOError: If transport is not open.
173
+ """
174
+ self.on_send_frame.emit(data)
175
+
176
+ # Calculate full frame (add checksum and brackets)
177
+ checksum = calculate_checksum(data.decode())
178
+ frame_data = data.decode() + checksum
179
+ frame = b"<" + frame_data.encode() + b">"
180
+
181
+ if not self.transport:
182
+ self.logger.info("Invalid transport")
183
+ raise IOError("Transport is not open")
184
+
185
+ self.logger.debug(f"Sending frame: {frame.decode()}")
186
+ self.transport.write(frame) # type: ignore
187
+ self.on_telegram_sent.emit(frame.decode())
188
+ self._reset_timeout()
189
+
190
+ def send_telegram(
191
+ self,
192
+ telegram_type: TelegramType,
193
+ serial_number: str,
194
+ system_function: SystemFunction,
195
+ data_value: str,
196
+ ) -> None:
197
+ """Send telegram with specified parameters.
198
+
199
+ Args:
200
+ telegram_type: Type of telegram to send.
201
+ serial_number: Device serial number.
202
+ system_function: System function code.
203
+ data_value: Data value to send.
204
+ """
205
+ payload = (
206
+ f"{telegram_type.value}"
207
+ f"{serial_number}"
208
+ f"F{system_function.value}"
209
+ f"D{data_value}"
210
+ )
211
+ self.telegram_queue.put_nowait(payload.encode())
212
+ self._reactor.callLater(0.0, self.start_queue_manager)
213
+
214
+ def buildProtocol(self, addr: IAddress) -> protocol.Protocol:
215
+ """Build protocol instance for connection.
216
+
217
+ Args:
218
+ addr: Address of the connection.
219
+
220
+ Returns:
221
+ Protocol instance for this connection.
222
+ """
223
+ self.logger.debug(f"buildProtocol: {addr}")
224
+ return self
225
+
226
+ def clientConnectionFailed(self, connector: IConnector, reason: Failure) -> None:
227
+ """Handle client connection failure.
228
+
229
+ Args:
230
+ connector: Connection connector instance.
231
+ reason: Failure reason details.
232
+ """
233
+ self.logger.debug(f"clientConnectionFailed: {reason}")
234
+ self.on_client_connection_failed.emit(reason)
235
+ self.connection_failed(reason)
236
+ self._cancel_timeout()
237
+
238
+ def clientConnectionLost(self, connector: IConnector, reason: Failure) -> None:
239
+ """Handle client connection lost event.
240
+
241
+ Args:
242
+ connector: Connection connector instance.
243
+ reason: Reason for connection loss.
244
+ """
245
+ self.logger.debug(f"clientConnectionLost: {reason}")
246
+ self.on_connection_lost.emit(reason)
247
+ self._cancel_timeout()
248
+
249
+ def timeout(self) -> None:
250
+ """Handle timeout event."""
251
+ self.logger.info("Timeout after: %ss", self.timeout_seconds)
252
+ self.on_timeout.emit()
253
+
254
+ def connection_failed(self, reason: Failure) -> None:
255
+ """Handle connection failure.
256
+
257
+ Args:
258
+ reason: Failure reason details.
259
+ """
260
+ self.logger.debug(f"Client connection failed: {reason}")
261
+ self.on_connection_failed.emit(reason)
262
+ self.on_failed.emit(reason.getErrorMessage())
263
+
264
+ def _reset_timeout(self) -> None:
265
+ """Reset the inactivity timeout."""
266
+ self._cancel_timeout()
267
+ self.timeout_call = self._reactor.callLater(
268
+ self.timeout_seconds, self._on_timeout
269
+ )
270
+
271
+ def _cancel_timeout(self) -> None:
272
+ """Cancel the inactivity timeout."""
273
+ if self.timeout_call and self.timeout_call.active():
274
+ self.timeout_call.cancel()
275
+
276
+ def _on_timeout(self) -> None:
277
+ """Handle inactivity timeout expiration."""
278
+ self.timeout()
279
+ self.logger.debug(f"Conbus timeout after {self.timeout_seconds} seconds")
280
+
281
+ def stop_reactor(self) -> None:
282
+ """Stop the reactor if it's running."""
283
+ if self._reactor.running:
284
+ self.logger.info("Stopping reactor")
285
+ self._reactor.stop()
286
+
287
+ def start_reactor(self) -> None:
288
+ """Start the reactor if it's running."""
289
+ # Connect to TCP server
290
+ self.logger.info(
291
+ f"Connecting to TCP server {self.cli_config.ip}:{self.cli_config.port}"
292
+ )
293
+ self._reactor.connectTCP(self.cli_config.ip, self.cli_config.port, self)
294
+
295
+ # Run the reactor (which now uses asyncio underneath)
296
+ self.logger.info("Starting reactor event loop.")
297
+ self._reactor.run()
298
+
299
+ def start_queue_manager(self) -> None:
300
+ """Start the queue manager if it's not running."""
301
+ with self.queue_manager_lock:
302
+ if self.queue_manager_running:
303
+ return
304
+ self.logger.debug("Queue manager: starting")
305
+ self.queue_manager_running = True
306
+ self.process_telegram_queue()
307
+
308
+ def process_telegram_queue(self) -> None:
309
+ """Start the queue manager if it's not running."""
310
+ self.logger.debug(
311
+ f"Queue manager: processing (remaining: {self.telegram_queue.qsize()})"
312
+ )
313
+ if self.telegram_queue.empty():
314
+ with self.queue_manager_lock:
315
+ self.logger.debug("Queue manager: stopping")
316
+ self.queue_manager_running = False
317
+ return
318
+
319
+ self.logger.debug("Queue manager: event loop")
320
+ telegram = self.telegram_queue.get_nowait()
321
+ self.sendFrame(telegram)
322
+ later = randint(10, 80) / 100
323
+ self._reactor.callLater(later, self.process_telegram_queue)
324
+
325
+ def __enter__(self) -> "ConbusEventProtocol":
326
+ """Enter context manager.
327
+
328
+ Returns:
329
+ Self for context management.
330
+ """
331
+ self.logger.debug("Entering the event loop.")
332
+ return self
333
+
334
+ def __exit__(
335
+ self,
336
+ _exc_type: Optional[type],
337
+ _exc_val: Optional[BaseException],
338
+ _exc_tb: Optional[Any],
339
+ ) -> None:
340
+ """Context manager exit - ensure connection is closed."""
341
+ self.logger.debug("Exiting the event loop.")
342
+ self.stop_reactor()
xp/utils/dependencies.py CHANGED
@@ -58,6 +58,7 @@ from xp.services.homekit.homekit_outlet_service import HomeKitOutletService
58
58
  from xp.services.homekit.homekit_service import HomeKitService
59
59
  from xp.services.log_file_service import LogFileService
60
60
  from xp.services.module_type_service import ModuleTypeService
61
+ from xp.services.protocol import ConbusEventProtocol
61
62
  from xp.services.protocol.protocol_factory import TelegramFactory
62
63
  from xp.services.protocol.telegram_protocol import TelegramProtocol
63
64
  from xp.services.reverse_proxy_service import ReverseProxyService
@@ -163,14 +164,22 @@ class ServiceContainer:
163
164
  )
164
165
 
165
166
  self.container.register(
166
- ConbusDiscoverService,
167
- factory=lambda: ConbusDiscoverService(
167
+ ConbusEventProtocol,
168
+ factory=lambda: ConbusEventProtocol(
168
169
  cli_config=self.container.resolve(ConbusClientConfig),
169
170
  reactor=self.container.resolve(PosixReactorBase),
170
171
  ),
171
172
  scope=punq.Scope.singleton,
172
173
  )
173
174
 
175
+ self.container.register(
176
+ ConbusDiscoverService,
177
+ factory=lambda: ConbusDiscoverService(
178
+ conbus_protocol=self.container.resolve(ConbusEventProtocol)
179
+ ),
180
+ scope=punq.Scope.singleton,
181
+ )
182
+
174
183
  self.container.register(
175
184
  ConbusBlinkService,
176
185
  factory=lambda: ConbusBlinkService(