conson-xp 1.17.0__py3-none-any.whl → 1.19.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.17.0
3
+ Version: 1.19.0
4
4
  Summary: XP Protocol Communication Tools
5
5
  Author-Email: ldvchosal <ldvchosal@github.com>
6
6
  License: MIT License
@@ -306,6 +306,7 @@ xp conbus datapoint query
306
306
  xp conbus discover
307
307
 
308
308
  xp conbus event
309
+ xp conbus event list
309
310
  xp conbus event raw
310
311
 
311
312
 
@@ -1,8 +1,8 @@
1
- conson_xp-1.17.0.dist-info/METADATA,sha256=Ib3pQUP44vhlduSotZu5juw2A86rHXgnO1e3fHrhuLg,9506
2
- conson_xp-1.17.0.dist-info/WHEEL,sha256=9P2ygRxDrTJz3gsagc0Z96ukrxjr-LFBGOgv3AuKlCA,90
3
- conson_xp-1.17.0.dist-info/entry_points.txt,sha256=1OcdIcDM1hz3ljCXgybaPUh1IOFEwkaTgLIW9u9zGeg,50
4
- conson_xp-1.17.0.dist-info/licenses/LICENSE,sha256=rxj6woMM-r3YCyGq_UHFtbh7kHTAJgrccH6O-33IDE4,1419
5
- xp/__init__.py,sha256=3Amcz5pSjBDLeWf7aPWXM1DtmHeDt9T4cbNlNM6t964,181
1
+ conson_xp-1.19.0.dist-info/METADATA,sha256=-VtLm8xePli914-pZt50momoSfpi6BARWkhyla_IiYA,9527
2
+ conson_xp-1.19.0.dist-info/WHEEL,sha256=9P2ygRxDrTJz3gsagc0Z96ukrxjr-LFBGOgv3AuKlCA,90
3
+ conson_xp-1.19.0.dist-info/entry_points.txt,sha256=1OcdIcDM1hz3ljCXgybaPUh1IOFEwkaTgLIW9u9zGeg,50
4
+ conson_xp-1.19.0.dist-info/licenses/LICENSE,sha256=rxj6woMM-r3YCyGq_UHFtbh7kHTAJgrccH6O-33IDE4,1419
5
+ xp/__init__.py,sha256=xqDwwDvx5lt_aNSuHKANp3WXQCP4x1a9OzyP8CniQnE,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=wvo9Z5viwpjvO2432E7YP5HWjLLiW1IFpyXLc5puuGY,4766
@@ -15,7 +15,7 @@ xp/cli/commands/conbus/conbus_config_commands.py,sha256=BugIbgNX6_s4MySGvI6tWZkw
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
17
  xp/cli/commands/conbus/conbus_discover_commands.py,sha256=-y3TDgOnw1_cjvxvgyfQ1GQE2_WmYq-l8Md7DsdTXmo,1719
18
- xp/cli/commands/conbus/conbus_event_commands.py,sha256=8IjQfX9vXlTRprb1oGkMRHRDPmxb02ZnmVbv3ltCqGk,3369
18
+ xp/cli/commands/conbus/conbus_event_commands.py,sha256=7URf-2u8Kzcy0chLYShbZfCbKawf--i-8U88AjhxleQ,3177
19
19
  xp/cli/commands/conbus/conbus_lightlevel_commands.py,sha256=FpCwogdxa7yFUjlrxM7e8Q2Ut32tKAHabngQQChvtJI,6763
20
20
  xp/cli/commands/conbus/conbus_linknumber_commands.py,sha256=KitaGDM5HpwVUz8rLpO8VZUypUTcAg3Bzl0DVm6gnSk,3391
21
21
  xp/cli/commands/conbus/conbus_modulenumber_commands.py,sha256=L7-6y3rDllOjQ9g6Bk_RiTKIhAOHVPLdxWif9exkngs,3463
@@ -47,12 +47,13 @@ xp/cli/utils/datapoint_type_choice.py,sha256=HcydhlqxZ7YyorEeTjFGkypF2JnYNPvOzkl
47
47
  xp/cli/utils/decorators.py,sha256=iAWm75VK_opqjX2h7ATZXlMzPINhF1FeeACzukm7ldQ,10149
48
48
  xp/cli/utils/error_handlers.py,sha256=zL4f6996Is0lWl8uITNqiLyPNaeW6uUbkew7BRCFi8Y,6581
49
49
  xp/cli/utils/formatters.py,sha256=fl7UmX6yLypqc0_QWevgPO2L6XA2Kd_0d_GSLUV5U30,9776
50
+ xp/cli/utils/module_type_choice.py,sha256=TPIEDsO0fNDu2HOQQ16WCJ-a7o2f58j3IKd8B0XsBDQ,1658
50
51
  xp/cli/utils/serial_number_type.py,sha256=GUs3jtVI6EVulvt6fCDN6H6vxhiJwdMmdIvLjDlGGZ4,1466
51
52
  xp/cli/utils/system_function_choice.py,sha256=0J02EMgAQcsrE-9rEkv6YHelBoBkZ73T8VLBSm6YO5k,1623
52
53
  xp/cli/utils/xp_module_type.py,sha256=qSFJBRceqPi_cUFPxAWtLUNq37-KwUEjo9ekYOj7kLQ,1471
53
54
  xp/connection/__init__.py,sha256=ClJsVWALYZgAGYZK_Jznd3YKLrHDu17kBfwugjuPfu0,209
54
55
  xp/connection/exceptions.py,sha256=7CcRUzkyay5zA6Z9-5dIDRzua806v5N7pCcJazP_1dE,365
55
- xp/models/__init__.py,sha256=UaUiuvWevneh9gPzKNaVsuy6rxM7YlZg4mi8VlEJpfg,1210
56
+ xp/models/__init__.py,sha256=lROqr559DGd8WpJJUtfPT95VERCwMZHpBDEc96QSxQ0,1312
56
57
  xp/models/actiontable/__init__.py,sha256=6kVq1rTOlpc24sZxGGVWkY48tqR42YWHLQHqakWqlPc,43
57
58
  xp/models/actiontable/actiontable.py,sha256=bIeluZhMsvukkSwy2neaewavU8YR6Pso3PIvJ8ENlGg,1251
58
59
  xp/models/actiontable/msactiontable_xp20.py,sha256=C_lYYIQagEFap0S5S40_S7AhLO2UZG2EmXjjeem7uw8,1967
@@ -67,6 +68,7 @@ xp/models/conbus/conbus_connection_status.py,sha256=iGbmtBaAMwV6UD7XG3H3tnB0fl2M
67
68
  xp/models/conbus/conbus_custom.py,sha256=8H2sPR6_LIlksuOvL7-8bPkzAJLR0rpYiiwfYYFVjEo,1965
68
69
  xp/models/conbus/conbus_datapoint.py,sha256=4ncR-vB2lRzRBAA30rYn8eguyTxsZoOKrrXtjGmPpWg,3396
69
70
  xp/models/conbus/conbus_discover.py,sha256=nxxUEKfEsH1kd0BF8ovMs7zLujRhrq1oL9ZJtysPr5o,2238
71
+ xp/models/conbus/conbus_event_list.py,sha256=M8aHRHVB5VDIjqMzjO86xlERt7AMdfjIjt1b70RF52Y,958
70
72
  xp/models/conbus/conbus_event_raw.py,sha256=i5gc7z-0yeunWOZ4rw3AiBt4MANezmhBQKjOOQk3oDc,1567
71
73
  xp/models/conbus/conbus_lightlevel.py,sha256=GQGhzrCBEJROosNHInXIzBy6MD2AskEIMoFEGgZ60-0,1695
72
74
  xp/models/conbus/conbus_linknumber.py,sha256=uFzKzfB06oIzZEKCb5X2JEI80JjMPFuYglsT1W1k8j4,1815
@@ -119,7 +121,8 @@ xp/services/conbus/conbus_custom_service.py,sha256=4aneYdPObiZOGxPFYg5Wr70cl_xFx
119
121
  xp/services/conbus/conbus_datapoint_queryall_service.py,sha256=p9R02cVimhdJILHQ6BoeZj8Hog4oRpqBnMo3t4R8ecY,6816
120
122
  xp/services/conbus/conbus_datapoint_service.py,sha256=SYhHj9RmTmaJ750tyZ1IW2kl7tgDQ1xm_EM1zUjk1aQ,6421
121
123
  xp/services/conbus/conbus_discover_service.py,sha256=sSCSDNWWGtx5QOShwJfcbG54WCYH-BxWvgE10ghibN4,12326
122
- xp/services/conbus/conbus_event_raw_service.py,sha256=zNY7GxT4R6ROsT1dDhoOoJkGtGbv2_AIBgOlLxZJl1A,7068
124
+ xp/services/conbus/conbus_event_list_service.py,sha256=0xyXXNU44epN5bFkU6oiZMyhxfUguul3evqClvPJDcA,3618
125
+ xp/services/conbus/conbus_event_raw_service.py,sha256=FZFu-LNLInrTKTpiGLyootozvyIF5Si5FMrxNk2ALD0,7000
123
126
  xp/services/conbus/conbus_output_service.py,sha256=mHFOAPx2zo0TStZ3pokp6v94AQjIamcwZDeg5YH_-eo,7240
124
127
  xp/services/conbus/conbus_raw_service.py,sha256=4yZLLTIAOxpgByUTWZXw1ihGa6Xtl98ckj9T7VfprDI,4335
125
128
  xp/services/conbus/conbus_receive_service.py,sha256=frXrS0OyKKvYYQTWdma21Kd0BKw5aSuHn3ZXTTqOaj0,3953
@@ -142,7 +145,7 @@ xp/services/homekit/homekit_service.py,sha256=0lW-hg40ETco3gDBEYkR_sX-UIYsLSKCD4
142
145
  xp/services/log_file_service.py,sha256=fvPcZQj8XOKUA-4ep5R8n0PelvwvRlTLlVxvIWM5KR4,10506
143
146
  xp/services/module_type_service.py,sha256=xWhr1EAZMykL5uNWHWdpa5T8yNruGKH43XRTOS8GwZg,7477
144
147
  xp/services/protocol/__init__.py,sha256=qRufBmqRKGzpuzZ5bxBbmwf510TT00Ke8s5HcWGnqRY,818
145
- xp/services/protocol/conbus_event_protocol.py,sha256=btWLGM-onWXVIvL5atD7HgQKNcx6F8dNqTZf2CSquiE,12272
148
+ xp/services/protocol/conbus_event_protocol.py,sha256=6ihDsWj5k08Hb3OpYd3xBZCS-yPa16FfWtFSxJknIwo,12852
146
149
  xp/services/protocol/conbus_protocol.py,sha256=JO7yLkD_ohPT0ETjnAIx4CGpZyobf4LdbuozM_43btE,10276
147
150
  xp/services/protocol/protocol_factory.py,sha256=PmjN9AtW9sxNo3voqUiNgQA-pTvX1RW4XXFlHKfFr5Q,2470
148
151
  xp/services/protocol/telegram_protocol.py,sha256=Ki5DrXsKxiaqLcdP9WWUuhUI7cPu2DfwyZkh-Gv9Lb8,9496
@@ -168,8 +171,8 @@ xp/services/telegram/telegram_service.py,sha256=XrP1CPi0ckxoKBaNwLA6lo-TogWxXgmX
168
171
  xp/services/telegram/telegram_version_service.py,sha256=M5HdOTsLdcwo122FP-jW6R740ktLrtKf2TiMDVz23h8,10528
169
172
  xp/utils/__init__.py,sha256=_avMF_UOkfR3tNeDIPqQ5odmbq5raKkaq1rZ9Cn1CJs,332
170
173
  xp/utils/checksum.py,sha256=HDpiQxmdIedbCbZ4o_Box0i_Zig417BtCV_46ZyhiTk,1711
171
- xp/utils/dependencies.py,sha256=QsZfPDMdlrQK01YQ4PRQ8Q59ZF9w22h1evWX3J4xCjE,20930
174
+ xp/utils/dependencies.py,sha256=PYe-RvmfGBRXWnLKX62nXGMDFN7PQW3deoGCkIVEG4s,21274
172
175
  xp/utils/event_helper.py,sha256=W-A_xmoXlpWZBbJH6qdaN50o3-XrwFsDgvAGMJDiAgo,1001
173
176
  xp/utils/serialization.py,sha256=RWHHk86feaB4ZP7rjE4qOWK0900yg2joUBDkP76gfOY,4618
174
177
  xp/utils/time_utils.py,sha256=dEyViDlAG9GWU-J3D_YVa-sGma6yiyyMTgN4h2x3PY4,3781
175
- conson_xp-1.17.0.dist-info/RECORD,,
178
+ conson_xp-1.19.0.dist-info/RECORD,,
xp/__init__.py CHANGED
@@ -3,7 +3,7 @@
3
3
  conson-xp package.
4
4
  """
5
5
 
6
- __version__ = "1.17.0"
6
+ __version__ = "1.19.0"
7
7
  __manufacturer__ = "salchichon"
8
8
  __model__ = "xp.cli"
9
9
  __serial__ = "2025.09.23.000"
@@ -6,8 +6,9 @@ import click
6
6
 
7
7
  from xp.cli.commands.conbus.conbus import conbus
8
8
  from xp.cli.utils.decorators import connection_command
9
+ from xp.cli.utils.module_type_choice import MODULE_TYPE
9
10
  from xp.models import ConbusEventRawResponse
10
- from xp.models.telegram.module_type_code import ModuleTypeCode
11
+ from xp.services.conbus.conbus_event_list_service import ConbusEventListService
11
12
  from xp.services.conbus.conbus_event_raw_service import ConbusEventRawService
12
13
 
13
14
 
@@ -17,16 +18,40 @@ def conbus_event() -> None:
17
18
  pass
18
19
 
19
20
 
21
+ @conbus_event.command("list")
22
+ @click.pass_context
23
+ def list_events(ctx: click.Context) -> None:
24
+ r"""List configured event telegrams from module action tables.
25
+
26
+ Reads conson.yml configuration, parses action tables, and groups
27
+ modules by their event keys to show which modules are assigned to
28
+ each event (button configuration).
29
+
30
+ Output is sorted by module count (most frequently used events first).
31
+
32
+ Args:
33
+ ctx: Click context object.
34
+
35
+ Examples:
36
+ \b
37
+ xp conbus event list
38
+ """
39
+ service: ConbusEventListService = (
40
+ ctx.obj.get("container").get_container().resolve(ConbusEventListService)
41
+ )
42
+ click.echo(json.dumps(service.list_events().to_dict(), indent=2))
43
+
44
+
20
45
  @conbus_event.command("raw")
21
- @click.argument("module_type", type=str)
22
- @click.argument("link_number", type=int)
23
- @click.argument("input_number", type=int)
24
- @click.argument("time_ms", type=int, default=1000)
46
+ @click.argument("module_type", type=MODULE_TYPE)
47
+ @click.argument("link_number", type=click.IntRange(0, 99))
48
+ @click.argument("input_number", type=click.IntRange(0, 9))
49
+ @click.argument("time_ms", type=click.IntRange(min=1), default=1000)
25
50
  @click.pass_context
26
51
  @connection_command()
27
52
  def send_event_raw(
28
53
  ctx: click.Context,
29
- module_type: str,
54
+ module_type: int,
30
55
  link_number: int,
31
56
  input_number: int,
32
57
  time_ms: int,
@@ -45,40 +70,6 @@ def send_event_raw(
45
70
  xp conbus event raw CP20 00 00
46
71
  xp conbus event raw XP33 00 00 500
47
72
  """
48
- # Validate parameters
49
- if link_number < 0 or link_number > 99:
50
- click.echo(
51
- json.dumps({"error": "Link number must be between 0 and 99"}, indent=2)
52
- )
53
- return
54
-
55
- if input_number < 0 or input_number > 9:
56
- click.echo(
57
- json.dumps({"error": "Input number must be between 0 and 9"}, indent=2)
58
- )
59
- return
60
-
61
- if time_ms <= 0:
62
- click.echo(json.dumps({"error": "Time must be greater than 0"}, indent=2))
63
- return
64
-
65
- # Resolve module type to numeric code
66
- module_type_code: int = 0
67
- try:
68
- # Try to get the enum value by name
69
- module_type_enum = ModuleTypeCode[module_type.upper()]
70
- module_type_code = module_type_enum.value
71
- except KeyError:
72
- # Module type not found
73
- click.echo(
74
- json.dumps(
75
- {
76
- "error": f"Unknown module type: {module_type}. Use module types like CP20, XP33, XP24, etc."
77
- },
78
- indent=2,
79
- )
80
- )
81
- return
82
73
 
83
74
  def on_finish(response: ConbusEventRawResponse) -> None:
84
75
  """Handle successful completion of event raw operation.
@@ -100,7 +91,7 @@ def send_event_raw(
100
91
  ctx.obj.get("container").get_container().resolve(ConbusEventRawService)
101
92
  )
102
93
  service.run(
103
- module_type_code=module_type_code,
94
+ module_type_code=module_type,
104
95
  link_number=link_number,
105
96
  input_number=input_number,
106
97
  time_ms=time_ms,
@@ -0,0 +1,56 @@
1
+ """Click parameter type for ModuleTypeCode enum validation."""
2
+
3
+ from typing import Any, Optional
4
+
5
+ import click
6
+
7
+ from xp.models.telegram.module_type_code import ModuleTypeCode
8
+
9
+
10
+ class ModuleTypeChoice(click.ParamType):
11
+ """Click parameter type for validating ModuleTypeCode enum values.
12
+
13
+ Attributes:
14
+ name: The parameter type name.
15
+ choices: List of valid choice strings.
16
+ """
17
+
18
+ name = "module_type"
19
+
20
+ def __init__(self) -> None:
21
+ """Initialize the ModuleTypeChoice parameter type."""
22
+ self.choices = [key for key in ModuleTypeCode.__members__.keys()]
23
+
24
+ def convert(
25
+ self, value: Any, param: Optional[click.Parameter], ctx: Optional[click.Context]
26
+ ) -> int:
27
+ """Convert and validate input to ModuleTypeCode value.
28
+
29
+ Args:
30
+ value: The input value to convert.
31
+ param: The Click parameter.
32
+ ctx: The Click context.
33
+
34
+ Returns:
35
+ Module type code integer value if valid.
36
+ """
37
+ if value is None:
38
+ self.fail("Module type is required", param, ctx)
39
+
40
+ # Convert to upper for comparison
41
+ normalized_value = value.upper()
42
+
43
+ if normalized_value in self.choices:
44
+ # Return the actual enum value (integer)
45
+ return ModuleTypeCode[normalized_value].value
46
+
47
+ # If not found, show error with available choices
48
+ choices_list = "\n".join(f" - {choice}" for choice in sorted(self.choices))
49
+ self.fail(
50
+ f"{value!r} is not a valid module type. " f"Choose from:\n{choices_list}",
51
+ param,
52
+ ctx,
53
+ )
54
+
55
+
56
+ MODULE_TYPE = ModuleTypeChoice()
xp/models/__init__.py CHANGED
@@ -5,6 +5,7 @@ from xp.models.conbus.conbus_client_config import ConbusClientConfig
5
5
  from xp.models.conbus.conbus_connection_status import ConbusConnectionStatus
6
6
  from xp.models.conbus.conbus_datapoint import ConbusDatapointResponse
7
7
  from xp.models.conbus.conbus_discover import ConbusDiscoverResponse
8
+ from xp.models.conbus.conbus_event_list import ConbusEventListResponse
8
9
  from xp.models.conbus.conbus_event_raw import ConbusEventRawResponse
9
10
  from xp.models.log_entry import LogEntry
10
11
  from xp.models.telegram.event_telegram import EventTelegram
@@ -31,6 +32,7 @@ __all__ = [
31
32
  "ConbusResponse",
32
33
  "ConbusDatapointResponse",
33
34
  "ConbusDiscoverResponse",
35
+ "ConbusEventListResponse",
34
36
  "ConbusEventRawResponse",
35
37
  "ConbusConnectionStatus",
36
38
  ]
@@ -0,0 +1,34 @@
1
+ """Conbus event list response model."""
2
+
3
+ from dataclasses import dataclass
4
+ from datetime import datetime
5
+ from typing import Any, Dict, Optional
6
+
7
+
8
+ @dataclass
9
+ class ConbusEventListResponse:
10
+ """Represents a response from Conbus event list operation.
11
+
12
+ Attributes:
13
+ events: Dict mapping event keys to list of module names.
14
+ timestamp: Timestamp of the response.
15
+ """
16
+
17
+ events: Dict[str, list[str]]
18
+ timestamp: Optional[datetime] = None
19
+
20
+ def __post_init__(self) -> None:
21
+ """Initialize timestamp if not provided."""
22
+ if self.timestamp is None:
23
+ self.timestamp = datetime.now()
24
+
25
+ def to_dict(self) -> Dict[str, Any]:
26
+ """Convert to dictionary for JSON serialization.
27
+
28
+ Returns:
29
+ Dictionary representation of the response.
30
+ """
31
+ return {
32
+ "events": self.events,
33
+ "timestamp": self.timestamp.isoformat() if self.timestamp else None,
34
+ }
@@ -0,0 +1,91 @@
1
+ """Conbus Event List Service for listing configured event telegrams.
2
+
3
+ This service parses action tables from conson.yml and groups events
4
+ by button configuration to show which modules are assigned to each event.
5
+ """
6
+
7
+ import logging
8
+ from collections import defaultdict
9
+ from typing import Dict, List
10
+
11
+ from xp.models import ConbusEventListResponse
12
+ from xp.models.homekit.homekit_conson_config import ConsonModuleListConfig
13
+ from xp.services.actiontable.actiontable_serializer import ActionTableSerializer
14
+
15
+
16
+ class ConbusEventListService:
17
+ """Service for listing configured event telegrams from action tables.
18
+
19
+ Parses action tables from conson.yml configuration and groups modules
20
+ by their event keys to identify common button configurations.
21
+
22
+ Attributes:
23
+ conson_config: Configuration containing module action tables.
24
+ logger: Logger instance for the service.
25
+ """
26
+
27
+ def __init__(self, conson_config: ConsonModuleListConfig) -> None:
28
+ """Initialize the Conbus event list service.
29
+
30
+ Args:
31
+ conson_config: ConsonModuleListConfig instance with module action tables.
32
+ """
33
+ self.conson_config = conson_config
34
+ self.logger = logging.getLogger(__name__)
35
+
36
+ def list_events(self) -> ConbusEventListResponse:
37
+ """List all configured events from module action tables.
38
+
39
+ Parses action tables, extracts event information (module_type, link, input),
40
+ groups modules by event key, and sorts by usage count.
41
+
42
+ Returns:
43
+ ConbusEventListResponse with events dict mapping event keys to module names.
44
+ """
45
+ # Dict to track which modules are assigned to each event
46
+ # event_key -> set of module names (using set for automatic deduplication)
47
+ event_modules: Dict[str, set[str]] = defaultdict(set)
48
+
49
+ # Process each module's action table
50
+ for module in self.conson_config.root:
51
+ # Skip modules without action table
52
+ if not module.action_table:
53
+ continue
54
+
55
+ # Process each action in the module's action table
56
+ for action in module.action_table:
57
+ try:
58
+ # Use existing ActionTableSerializer to parse action
59
+ entry = ActionTableSerializer.parse_action_string(action)
60
+
61
+ # Extract event data from parsed entry
62
+ module_type_name = entry.module_type.name
63
+ link = entry.link_number
64
+ input_num = entry.module_input
65
+
66
+ # Create event key (space-separated format)
67
+ event_key = f"{module_type_name} {link:02d} {input_num:02d}"
68
+
69
+ # Add this module to the event (set automatically deduplicates)
70
+ event_modules[event_key].add(
71
+ f"{module.serial_number}:{entry.module_output}"
72
+ )
73
+
74
+ except ValueError as e:
75
+ # Invalid action format - log warning and skip
76
+ self.logger.warning(
77
+ f"Invalid action '{action}' in module '{module.serial_number}': {e}"
78
+ )
79
+ continue
80
+
81
+ # Convert sets to sorted lists and sort events by module count (descending)
82
+ events_dict: Dict[str, List[str]] = {
83
+ event_key: sorted(list(modules))
84
+ for event_key, modules in sorted(
85
+ event_modules.items(),
86
+ key=lambda item: len(item[1]),
87
+ reverse=True,
88
+ )
89
+ }
90
+
91
+ return ConbusEventListResponse(events=events_dict)
@@ -63,13 +63,11 @@ class ConbusEventRawService:
63
63
  payload = f"E{self.module_type_code:02d}L{self.link_number:02d}I{self.input_number:02d}M"
64
64
  self.logger.debug(f"Sending MAKE event: {payload}")
65
65
  self.conbus_protocol.telegram_queue.put_nowait(payload.encode())
66
- self.conbus_protocol._reactor.callLater(
67
- 0.0, self.conbus_protocol.start_queue_manager
68
- )
66
+ self.conbus_protocol.call_later(0.0, self.conbus_protocol.start_queue_manager)
69
67
 
70
68
  # Schedule BREAK event after delay
71
69
  delay_seconds = self.time_ms / 1000.0
72
- self.break_event_call = self.conbus_protocol._reactor.callLater(
70
+ self.break_event_call = self.conbus_protocol.call_later(
73
71
  delay_seconds, self._send_break_event
74
72
  )
75
73
 
@@ -78,9 +76,7 @@ class ConbusEventRawService:
78
76
  payload = f"E{self.module_type_code:02d}L{self.link_number:02d}I{self.input_number:02d}B"
79
77
  self.logger.debug(f"Sending BREAK event: {payload}")
80
78
  self.conbus_protocol.telegram_queue.put_nowait(payload.encode())
81
- self.conbus_protocol._reactor.callLater(
82
- 0.0, self.conbus_protocol.start_queue_manager
83
- )
79
+ self.conbus_protocol.call_later(0.0, self.conbus_protocol.start_queue_manager)
84
80
 
85
81
  def telegram_sent(self, telegram_sent: str) -> None:
86
82
  """Handle telegram sent event.
@@ -7,7 +7,7 @@ import logging
7
7
  from queue import SimpleQueue
8
8
  from random import randint
9
9
  from threading import Lock
10
- from typing import Any, Optional
10
+ from typing import Any, Callable, Optional
11
11
 
12
12
  from psygnal import Signal
13
13
  from twisted.internet import protocol
@@ -209,7 +209,27 @@ class ConbusEventProtocol(protocol.Protocol, protocol.ClientFactory):
209
209
  f"D{data_value}"
210
210
  )
211
211
  self.telegram_queue.put_nowait(payload.encode())
212
- self._reactor.callLater(0.0, self.start_queue_manager)
212
+ self.call_later(0.0, self.start_queue_manager)
213
+
214
+ def call_later(
215
+ self,
216
+ delay: float,
217
+ callable_action: Callable[..., Any],
218
+ *args: object,
219
+ **kw: object,
220
+ ) -> DelayedCall:
221
+ """Schedule a callable to be called later.
222
+
223
+ Args:
224
+ delay: Delay in seconds before calling.
225
+ callable_action: The callable to execute.
226
+ args: Positional arguments to pass to callable.
227
+ kw: Keyword arguments to pass to callable.
228
+
229
+ Returns:
230
+ DelayedCall object that can be cancelled.
231
+ """
232
+ return self._reactor.callLater(delay, callable_action, *args, **kw)
213
233
 
214
234
  def buildProtocol(self, addr: IAddress) -> protocol.Protocol:
215
235
  """Build protocol instance for connection.
@@ -264,9 +284,7 @@ class ConbusEventProtocol(protocol.Protocol, protocol.ClientFactory):
264
284
  def _reset_timeout(self) -> None:
265
285
  """Reset the inactivity timeout."""
266
286
  self._cancel_timeout()
267
- self.timeout_call = self._reactor.callLater(
268
- self.timeout_seconds, self._on_timeout
269
- )
287
+ self.timeout_call = self.call_later(self.timeout_seconds, self._on_timeout)
270
288
 
271
289
  def _cancel_timeout(self) -> None:
272
290
  """Cancel the inactivity timeout."""
@@ -320,7 +338,7 @@ class ConbusEventProtocol(protocol.Protocol, protocol.ClientFactory):
320
338
  telegram = self.telegram_queue.get_nowait()
321
339
  self.sendFrame(telegram)
322
340
  later = randint(10, 80) / 100
323
- self._reactor.callLater(later, self.process_telegram_queue)
341
+ self.call_later(later, self.process_telegram_queue)
324
342
 
325
343
  def __enter__(self) -> "ConbusEventProtocol":
326
344
  """Enter context manager.
xp/utils/dependencies.py CHANGED
@@ -43,6 +43,7 @@ from xp.services.conbus.conbus_datapoint_service import (
43
43
  ConbusDatapointService,
44
44
  )
45
45
  from xp.services.conbus.conbus_discover_service import ConbusDiscoverService
46
+ from xp.services.conbus.conbus_event_list_service import ConbusEventListService
46
47
  from xp.services.conbus.conbus_event_raw_service import ConbusEventRawService
47
48
  from xp.services.conbus.conbus_output_service import ConbusOutputService
48
49
  from xp.services.conbus.conbus_raw_service import ConbusRawService
@@ -189,6 +190,14 @@ class ServiceContainer:
189
190
  scope=punq.Scope.singleton,
190
191
  )
191
192
 
193
+ self.container.register(
194
+ ConbusEventListService,
195
+ factory=lambda: ConbusEventListService(
196
+ conson_config=self.container.resolve(ConsonModuleListConfig)
197
+ ),
198
+ scope=punq.Scope.singleton,
199
+ )
200
+
192
201
  self.container.register(
193
202
  ConbusBlinkService,
194
203
  factory=lambda: ConbusBlinkService(