conson-xp 1.8.0__py3-none-any.whl → 1.10.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.8.0
3
+ Version: 1.10.0
4
4
  Summary: XP Protocol Communication Tools
5
5
  Author-Email: ldvchosal <ldvchosal@github.com>
6
6
  License: MIT License
@@ -276,6 +276,9 @@ xp conbus
276
276
 
277
277
  xp conbus actiontable
278
278
  xp conbus actiontable download
279
+ xp conbus actiontable list
280
+ xp conbus actiontable show
281
+ xp conbus actiontable upload
279
282
 
280
283
 
281
284
  xp conbus autoreport
@@ -1,14 +1,14 @@
1
- conson_xp-1.8.0.dist-info/METADATA,sha256=k_y3CTe1PGLQhEJfZcuuucVkm0KXbimm6gJlZeyiJtg,9274
2
- conson_xp-1.8.0.dist-info/WHEEL,sha256=9P2ygRxDrTJz3gsagc0Z96ukrxjr-LFBGOgv3AuKlCA,90
3
- conson_xp-1.8.0.dist-info/entry_points.txt,sha256=1OcdIcDM1hz3ljCXgybaPUh1IOFEwkaTgLIW9u9zGeg,50
4
- conson_xp-1.8.0.dist-info/licenses/LICENSE,sha256=rxj6woMM-r3YCyGq_UHFtbh7kHTAJgrccH6O-33IDE4,1419
5
- xp/__init__.py,sha256=zNtMk7hYttYnPyJca9Y2bNXm1-ItuKG_qhNSWF9BH6k,180
1
+ conson_xp-1.10.0.dist-info/METADATA,sha256=55VZEVXn3r5ApgKT238-rNMKKW0I9wWr9TiNQTPDdgk,9358
2
+ conson_xp-1.10.0.dist-info/WHEEL,sha256=9P2ygRxDrTJz3gsagc0Z96ukrxjr-LFBGOgv3AuKlCA,90
3
+ conson_xp-1.10.0.dist-info/entry_points.txt,sha256=1OcdIcDM1hz3ljCXgybaPUh1IOFEwkaTgLIW9u9zGeg,50
4
+ conson_xp-1.10.0.dist-info/licenses/LICENSE,sha256=rxj6woMM-r3YCyGq_UHFtbh7kHTAJgrccH6O-33IDE4,1419
5
+ xp/__init__.py,sha256=YMJIBwiewWulRvvOT-p3EV59J1ZquaKNXlV91AizL98,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=02CbZoKmNX-fn5etX4Hdgg2lUt1MsLFPYx2VkXZyFJ8,4394
9
9
  xp/cli/commands/conbus/__init__.py,sha256=gE3K5OEoXkkZX8UOc2v3nreQQzwkOQi7n0VZ-Z2juXA,495
10
10
  xp/cli/commands/conbus/conbus.py,sha256=OTebWu-V-_1tOq2nWExPLtDuAeqy7fB7ltUqzHfgcY8,2705
11
- xp/cli/commands/conbus/conbus_actiontable_commands.py,sha256=H73-iHpsYRiOMl1hDILl2TaBdH3_It770bBZVZUcLyc,1978
11
+ xp/cli/commands/conbus/conbus_actiontable_commands.py,sha256=cdjLV9cnm7teEOlu5Jf1MS_aL7lNy8KiDIyjCQa5Nzw,7138
12
12
  xp/cli/commands/conbus/conbus_autoreport_commands.py,sha256=oZgyUUFNsb4yf2WO81l2w1PrasNwdC__QwxNkJ2jCaU,3794
13
13
  xp/cli/commands/conbus/conbus_blink_commands.py,sha256=UK-Ey4K0FvaPQ96U0gyMid236RlBmUhPNRes9y0SlkM,4848
14
14
  xp/cli/commands/conbus/conbus_config_commands.py,sha256=BugIbgNX6_s4MySGvI6tWZkwguciajHUX2Xz8XBux7k,716
@@ -52,9 +52,9 @@ xp/connection/__init__.py,sha256=ClJsVWALYZgAGYZK_Jznd3YKLrHDu17kBfwugjuPfu0,209
52
52
  xp/connection/exceptions.py,sha256=7CcRUzkyay5zA6Z9-5dIDRzua806v5N7pCcJazP_1dE,365
53
53
  xp/models/__init__.py,sha256=wCyJNKBd8J2ziOm0g00eUZH4OeTaLO5vHuoQGd_AJbg,1111
54
54
  xp/models/actiontable/__init__.py,sha256=6kVq1rTOlpc24sZxGGVWkY48tqR42YWHLQHqakWqlPc,43
55
- xp/models/actiontable/actiontable.py,sha256=D24fftW51fYhz03LzTy21KJLoFLE7KaC6bgiiQcjQRY,1255
55
+ xp/models/actiontable/actiontable.py,sha256=bIeluZhMsvukkSwy2neaewavU8YR6Pso3PIvJ8ENlGg,1251
56
56
  xp/models/actiontable/msactiontable_xp20.py,sha256=C_lYYIQagEFap0S5S40_S7AhLO2UZG2EmXjjeem7uw8,1967
57
- xp/models/actiontable/msactiontable_xp24.py,sha256=hugTkpEfiFwzLNU2xCJgpRtTXglpYG_wC_dRdYCAog4,2093
57
+ xp/models/actiontable/msactiontable_xp24.py,sha256=ne1dC6CA-5GoTnsLCr6Faue_PmSwC7vtUnX4NSRrQ_Y,2089
58
58
  xp/models/actiontable/msactiontable_xp33.py,sha256=2IEA0CBPvnatOueBPZiV0DPc7YFzTQIqIMqed8TKXeM,1932
59
59
  xp/models/conbus/__init__.py,sha256=VIusMWQdBtlwDgj7oSj06wQkklihTp4oWFShvP_JUgA,35
60
60
  xp/models/conbus/conbus.py,sha256=mZQzKPfrdttT-qUnYUSyrEYyc_eHs8z301E5ejeiyvk,2689
@@ -74,7 +74,7 @@ xp/models/conbus/conbus_writeconfig.py,sha256=z8fdJeFLyGJW7UMHcHxGrHIMS6gG1D3aXe
74
74
  xp/models/homekit/__init__.py,sha256=5HDSOClCu0ArK3IICn3_LDMMLBAzLjBxUUSF73bxSSk,34
75
75
  xp/models/homekit/homekit_accessory.py,sha256=NsHFhskuxIdJpF9-MvXHtjkLYvNHmSGFOy0GmQv3PY4,1038
76
76
  xp/models/homekit/homekit_config.py,sha256=Y1WwrbISRtJOkKVBnXQULb3vAOzcOdt95hBAI8cM_MU,2771
77
- xp/models/homekit/homekit_conson_config.py,sha256=S8DbFVV6qL99M8sQlvY4nbb5JC-CeSKdxOJ5WyJ8pnI,1972
77
+ xp/models/homekit/homekit_conson_config.py,sha256=NML644Ij7abstMbud-TUPcxraGY4vQwKrkJOwftv2pM,2603
78
78
  xp/models/log_entry.py,sha256=kPcYuAirCXugfL3YkOK9cQVlkNWxG_8a4AVuhsykHL0,4355
79
79
  xp/models/protocol/__init__.py,sha256=TJ_CJKchA-xgQiv5vCo_ndBBZjrcaTmjT74bR0T-5Cw,38
80
80
  xp/models/protocol/conbus_protocol.py,sha256=tSnI5pxBTP_f1DUzEM3XbyjK7vsqwm0gpzHIH1gTg8E,8854
@@ -84,13 +84,13 @@ xp/models/telegram/action_type.py,sha256=vkw_chTgmsadksGXvZ9D_qYGpjOwCw-OgbEi8Bm
84
84
  xp/models/telegram/datapoint_type.py,sha256=clmgqCsTNKuHmWN6ol2Hwj_71I10f36Oq-S5D5ZA9a8,2942
85
85
  xp/models/telegram/event_telegram.py,sha256=FCCfyZXQEUPB6Uo1m7B9nvFCJ0Ipv2CApmImAZo_Xa4,4689
86
86
  xp/models/telegram/event_type.py,sha256=VZhaDpey7KYWnmwN-gstj-r4Vd5hiGdzQuRazUdixB8,333
87
- xp/models/telegram/input_action_type.py,sha256=_0D6U6WslUcA1imht7ZQ5t8EAvkqyk7c2IfmNqZAAR0,1868
87
+ xp/models/telegram/input_action_type.py,sha256=EDYtE4uxByUyGsZTkXxwN9rQxCAejWk08_kv-7COurM,1852
88
88
  xp/models/telegram/input_type.py,sha256=X3AcKKMNHswNZs5xgT_AnxeKQpSx_U7ctGnr6AYqNoU,491
89
89
  xp/models/telegram/module_type.py,sha256=TdrcQC3UcdESzyUmS9PaVeJuF5VxH1WUtDWdf4QRA50,5223
90
90
  xp/models/telegram/module_type_code.py,sha256=bg8Zi58yKs5DDnEF0bGnZ9vvpqzmIZzd1k9Wu4ufB-Y,8177
91
91
  xp/models/telegram/output_telegram.py,sha256=vTSdeAGk7va89pZ8-oh0cna98N3T6if-6UcrstWsN6s,3473
92
92
  xp/models/telegram/reply_telegram.py,sha256=oqNwDvaOhFTPuXL0fP9Ca3rbcKepDhRz9kIneKCk6n0,10376
93
- xp/models/telegram/system_function.py,sha256=Iv9u4sYCPnMcvlpbBrNNxu0NpUOFsi5kPgT2vrelbVw,3266
93
+ xp/models/telegram/system_function.py,sha256=ZiY5preq_teO10R6489ZOaYjkJsYFpHZ5_tJpGM6E-Y,4794
94
94
  xp/models/telegram/system_telegram.py,sha256=9FNQ4Mf47mRK7wGrTg2GzziVsrEWCE5ZkZp5kA7K3w0,3218
95
95
  xp/models/telegram/telegram.py,sha256=IJUxHX6ftLcET9C1pjvLhUO5Db5JO6W7rUItzdEW30I,842
96
96
  xp/models/telegram/telegram_type.py,sha256=GhqKP63oNMyh2tIvCPcsC5RFp4s4JjhmEqCLCC-8XMk,423
@@ -98,14 +98,17 @@ xp/models/telegram/timeparam_type.py,sha256=Ar8xvSfPmOAgR2g2Je0FgvP01SL7bPvZn5_H
98
98
  xp/models/write_config_type.py,sha256=T2RaO52RpzoJ4782uMHE-fX7Ymx3CaIQAEwByydXq1M,881
99
99
  xp/services/__init__.py,sha256=W9YZyrkh7vm--ZHhAXNQiOYQs5yhhmUHXP5I0Lf1XBg,782
100
100
  xp/services/actiontable/__init__.py,sha256=z6js4EuJ6xKHaseTEhuEvKo1tr9K1XyQiruReJtBiPY,26
101
- xp/services/actiontable/actiontable_serializer.py,sha256=x45-8d5Ba9l3hX2TFC5nqKv-g_244g-VTWhXvVXL8Jg,5159
101
+ xp/services/actiontable/actiontable_serializer.py,sha256=U7bhd8lYMUJAsFydCt_Y5uOJoUODhSjRlUQPD6jsqMo,8517
102
102
  xp/services/actiontable/msactiontable_serializer.py,sha256=RRL6TZ1gpSQw81kAiw2BV3jTqm4fCJC0pWIcO26Cmos,174
103
103
  xp/services/actiontable/msactiontable_xp20_serializer.py,sha256=3Lz6t3uRYhoeMRhjDAO1XuWPJzH-ML13t05UQLFUW-s,6057
104
104
  xp/services/actiontable/msactiontable_xp24_serializer.py,sha256=zdKzcrKqD41POqj_1c4B4why_Jp9mNXncajsnXXBtPw,4215
105
105
  xp/services/actiontable/msactiontable_xp33_serializer.py,sha256=xoZBA38pBSUPA9nn7HgaH1ZM5sR2heQbJ6JVlPVbzUY,8400
106
106
  xp/services/conbus/__init__.py,sha256=Hi35sMKu9o6LpYoi2tQDaQoMb8M5sOt_-LUTxxaCU_0,28
107
107
  xp/services/conbus/actiontable/__init__.py,sha256=oD6vRk_Ye-eZ9s_hldAgtRJFu4mfAnODqpkJUGHHszk,40
108
- xp/services/conbus/actiontable/actiontable_service.py,sha256=eLSrB-SF-5jqW5mI43i-r4d5p2KsMpnEFHLf4cbuXng,5688
108
+ xp/services/conbus/actiontable/actiontable_download_service.py,sha256=x9k5VlVjvsAJi4McDGqErLaBE_dosV5uMSrNF_r6ic0,6013
109
+ xp/services/conbus/actiontable/actiontable_list_service.py,sha256=6izVZkM2hlWXUMUo1NkNzSMvPo0wFfDxYTADQBXQptU,3000
110
+ xp/services/conbus/actiontable/actiontable_show_service.py,sha256=jqNZ4UvZPHH66OYuryjnU1Km-a83OCwYvK0vc56oL8I,3017
111
+ xp/services/conbus/actiontable/actiontable_upload_service.py,sha256=re4usQJoa46nw7Vau47YfevFfOtZkKa310Y2TlZPyTY,8187
109
112
  xp/services/conbus/actiontable/msactiontable_service.py,sha256=K0TiYL8g4ac8BS1tqS0UAIYJigOlNhxVLIb8ZFybnVE,8393
110
113
  xp/services/conbus/conbus_blink_all_service.py,sha256=OaEg4b8AEiEruHSkZ5jDtaoI81vwwxLq4KWXO7zBdD0,6582
111
114
  xp/services/conbus/conbus_blink_service.py,sha256=x9uM-sLnIEV8wSNsvJgo08E042g-Hh2ZF3rXkz-k_9s,5824
@@ -160,8 +163,8 @@ xp/services/telegram/telegram_service.py,sha256=XrP1CPi0ckxoKBaNwLA6lo-TogWxXgmX
160
163
  xp/services/telegram/telegram_version_service.py,sha256=M5HdOTsLdcwo122FP-jW6R740ktLrtKf2TiMDVz23h8,10528
161
164
  xp/utils/__init__.py,sha256=_avMF_UOkfR3tNeDIPqQ5odmbq5raKkaq1rZ9Cn1CJs,332
162
165
  xp/utils/checksum.py,sha256=HDpiQxmdIedbCbZ4o_Box0i_Zig417BtCV_46ZyhiTk,1711
163
- xp/utils/dependencies.py,sha256=1XDwIg3OsmLvOazMQ3qaktcsitYW8E400RxihNWgyt0,18894
166
+ xp/utils/dependencies.py,sha256=xUmk4XWGArR5__kHdEG1Y4K_dHxsP5mkXhwon_SW6Eo,20110
164
167
  xp/utils/event_helper.py,sha256=W-A_xmoXlpWZBbJH6qdaN50o3-XrwFsDgvAGMJDiAgo,1001
165
168
  xp/utils/serialization.py,sha256=RWHHk86feaB4ZP7rjE4qOWK0900yg2joUBDkP76gfOY,4618
166
169
  xp/utils/time_utils.py,sha256=dEyViDlAG9GWU-J3D_YVa-sGma6yiyyMTgN4h2x3PY4,3781
167
- conson_xp-1.8.0.dist-info/RECORD,,
170
+ conson_xp-1.10.0.dist-info/RECORD,,
xp/__init__.py CHANGED
@@ -3,7 +3,7 @@
3
3
  conson-xp package.
4
4
  """
5
5
 
6
- __version__ = "1.8.0"
6
+ __version__ = "1.10.0"
7
7
  __manufacturer__ = "salchichon"
8
8
  __model__ = "xp.cli"
9
9
  __serial__ = "2025.09.23.000"
@@ -1,7 +1,9 @@
1
1
  """ActionTable CLI commands."""
2
2
 
3
3
  import json
4
- from dataclasses import asdict
4
+ from contextlib import suppress
5
+ from pathlib import Path
6
+ from typing import Any
5
7
 
6
8
  import click
7
9
  from click import Context
@@ -12,9 +14,28 @@ from xp.cli.utils.decorators import (
12
14
  )
13
15
  from xp.cli.utils.serial_number_type import SERIAL
14
16
  from xp.models.actiontable.actiontable import ActionTable
15
- from xp.services.conbus.actiontable.actiontable_service import (
17
+ from xp.models.homekit.homekit_conson_config import (
18
+ ConsonModuleConfig,
19
+ ConsonModuleListConfig,
20
+ )
21
+ from xp.services.conbus.actiontable.actiontable_download_service import (
16
22
  ActionTableService,
17
23
  )
24
+ from xp.services.conbus.actiontable.actiontable_list_service import (
25
+ ActionTableListService,
26
+ )
27
+ from xp.services.conbus.actiontable.actiontable_show_service import (
28
+ ActionTableShowService,
29
+ )
30
+ from xp.services.conbus.actiontable.actiontable_upload_service import (
31
+ ActionTableUploadService,
32
+ )
33
+
34
+
35
+ class ActionTableError(Exception):
36
+ """Raised when ActionTable operations fail."""
37
+
38
+ pass
18
39
 
19
40
 
20
41
  @conbus_actiontable.command("download", short_help="Download ActionTable")
@@ -40,15 +61,22 @@ def conbus_download_actiontable(ctx: Context, serial_number: str) -> None:
40
61
  """
41
62
  click.echo(progress)
42
63
 
43
- def on_finish(actiontable: ActionTable) -> None:
64
+ def on_finish(
65
+ _actiontable: ActionTable,
66
+ actiontable_dict: dict[str, Any],
67
+ actiontable_short: list[str],
68
+ ) -> None:
44
69
  """Handle successful completion of action table download.
45
70
 
46
71
  Args:
47
- actiontable: Downloaded action table object.
72
+ _actiontable: Downloaded action table object.
73
+ actiontable_dict: Dictionary representation of action table.
74
+ actiontable_short: List of textual format strings.
48
75
  """
49
76
  output = {
50
77
  "serial_number": serial_number,
51
- "actiontable": asdict(actiontable),
78
+ "actiontable_short": actiontable_short,
79
+ "actiontable": actiontable_dict,
52
80
  }
53
81
  click.echo(json.dumps(output, indent=2, default=str))
54
82
 
@@ -67,3 +95,139 @@ def conbus_download_actiontable(ctx: Context, serial_number: str) -> None:
67
95
  finish_callback=on_finish,
68
96
  error_callback=error_callback,
69
97
  )
98
+
99
+
100
+ @conbus_actiontable.command("upload", short_help="Upload ActionTable")
101
+ @click.argument("serial_number", type=SERIAL)
102
+ @click.pass_context
103
+ @connection_command()
104
+ def conbus_upload_actiontable(ctx: Context, serial_number: str) -> None:
105
+ """Upload action table from conson.yml to XP module.
106
+
107
+ Args:
108
+ ctx: Click context object.
109
+ serial_number: 10-digit module serial number.
110
+ """
111
+ service: ActionTableUploadService = (
112
+ ctx.obj.get("container").get_container().resolve(ActionTableUploadService)
113
+ )
114
+
115
+ click.echo(f"Uploading action table to {serial_number}...")
116
+
117
+ # Track number of entries for success message
118
+ entries_count = 0
119
+
120
+ def progress_callback(progress: str) -> None:
121
+ """Handle progress updates during action table upload.
122
+
123
+ Args:
124
+ progress: Progress message string.
125
+ """
126
+ click.echo(progress, nl=False)
127
+
128
+ def success_callback() -> None:
129
+ """Handle successful completion of action table upload."""
130
+ click.echo("\nAction table uploaded successfully")
131
+ if entries_count > 0:
132
+ click.echo(f"{entries_count} entries written")
133
+
134
+ def error_callback(error: str) -> None:
135
+ """Handle errors during action table upload.
136
+
137
+ Args:
138
+ error: Error message string.
139
+
140
+ Raises:
141
+ ActionTableError: Always raised with upload failure message.
142
+ """
143
+ raise ActionTableError(f"Upload failed: {error}")
144
+
145
+ with service:
146
+ # Load config to get entry count for success message
147
+ config_path = Path.cwd() / "conson.yml"
148
+ if config_path.exists():
149
+ with suppress(Exception):
150
+ config = ConsonModuleListConfig.from_yaml(str(config_path))
151
+ module = config.find_module(serial_number)
152
+ if module and module.action_table:
153
+ entries_count = len(module.action_table)
154
+
155
+ service.start(
156
+ serial_number=serial_number,
157
+ progress_callback=progress_callback,
158
+ success_callback=success_callback,
159
+ error_callback=error_callback,
160
+ )
161
+
162
+
163
+ @conbus_actiontable.command("list", short_help="List modules with ActionTable")
164
+ @click.pass_context
165
+ def conbus_list_actiontable(ctx: Context) -> None:
166
+ """List all modules with action table configurations from conson.yml.
167
+
168
+ Args:
169
+ ctx: Click context object.
170
+ """
171
+ service: ActionTableListService = (
172
+ ctx.obj.get("container").get_container().resolve(ActionTableListService)
173
+ )
174
+
175
+ def on_finish(module_list: dict) -> None:
176
+ """Handle successful completion of action table list.
177
+
178
+ Args:
179
+ module_list: Dictionary containing modules and total count.
180
+ """
181
+ click.echo(json.dumps(module_list, indent=2, default=str))
182
+
183
+ def error_callback(error: str) -> None:
184
+ """Handle errors during action table list.
185
+
186
+ Args:
187
+ error: Error message string.
188
+ """
189
+ click.echo(error)
190
+
191
+ with service:
192
+ service.start(
193
+ finish_callback=on_finish,
194
+ error_callback=error_callback,
195
+ )
196
+
197
+
198
+ @conbus_actiontable.command("show", short_help="Show ActionTable configuration")
199
+ @click.argument("serial_number", type=SERIAL)
200
+ @click.pass_context
201
+ def conbus_show_actiontable(ctx: Context, serial_number: str) -> None:
202
+ """Show action table configuration for a specific module from conson.yml.
203
+
204
+ Args:
205
+ ctx: Click context object.
206
+ serial_number: 10-digit module serial number.
207
+ """
208
+ service: ActionTableShowService = (
209
+ ctx.obj.get("container").get_container().resolve(ActionTableShowService)
210
+ )
211
+
212
+ def on_finish(module: ConsonModuleConfig) -> None:
213
+ """Handle successful completion of action table show.
214
+
215
+ Args:
216
+ module: Dictionary containing module configuration.
217
+ """
218
+ click.echo(json.dumps(module.model_dump(), indent=2, default=str))
219
+
220
+ def error_callback(error: str) -> None:
221
+ """Handle errors during action table show.
222
+
223
+ Args:
224
+ error: Error message string.
225
+ """
226
+ click.echo(error)
227
+
228
+ with service:
229
+ service.start(
230
+ serial_number=serial_number,
231
+ finish_callback=on_finish,
232
+ error_callback=error_callback,
233
+ )
@@ -27,7 +27,7 @@ class ActionTableEntry:
27
27
  link_number: int = 0
28
28
  module_input: int = 0
29
29
  module_output: int = 1
30
- command: InputActionType = InputActionType.TURNOFF
30
+ command: InputActionType = InputActionType.OFF
31
31
  parameter: TimeParam = TimeParam.NONE
32
32
  inverted: bool = False
33
33
 
@@ -23,7 +23,7 @@ class InputAction:
23
23
  class Xp24MsActionTable:
24
24
  """XP24 Action Table for managing input actions and settings.
25
25
 
26
- Each input has an action type (TOGGLE, TURNON, LEVELSET, etc.)
26
+ Each input has an action type (TOGGLE, ON, LEVELSET, etc.)
27
27
  with an optional parameter string.
28
28
 
29
29
  Attributes:
@@ -22,6 +22,8 @@ class ConsonModuleConfig(BaseModel):
22
22
  conbus_port: Optional Conbus port number.
23
23
  sw_version: Optional software version.
24
24
  hw_version: Optional hardware version.
25
+ action_table: Optional action table configuration.
26
+ auto_report_status: Optional auto report status.
25
27
  """
26
28
 
27
29
  name: str
@@ -35,6 +37,8 @@ class ConsonModuleConfig(BaseModel):
35
37
  conbus_port: Optional[int] = None
36
38
  sw_version: Optional[str] = None
37
39
  hw_version: Optional[str] = None
40
+ action_table: Optional[List[str]] = None
41
+ auto_report_status: Optional[str] = None
38
42
 
39
43
 
40
44
  class ConsonModuleListConfig(BaseModel):
@@ -66,3 +70,17 @@ class ConsonModuleListConfig(BaseModel):
66
70
  with Path(file_path).open("r") as file:
67
71
  data = yaml.safe_load(file)
68
72
  return cls(root=data)
73
+
74
+ def find_module(self, serial_number: str) -> Optional[ConsonModuleConfig]:
75
+ """Find a module by serial number.
76
+
77
+ Args:
78
+ serial_number: Module serial number to search for.
79
+
80
+ Returns:
81
+ ConsonModuleConfig if found, None otherwise.
82
+ """
83
+ for module in self.root:
84
+ if module.serial_number == serial_number:
85
+ return module
86
+ return None
@@ -8,8 +8,8 @@ class InputActionType(Enum):
8
8
 
9
9
  Attributes:
10
10
  VOID: No action.
11
- TURNON: Turn on action.
12
- TURNOFF: Turn off action.
11
+ ON: Turn on action.
12
+ OFF: Turn off action.
13
13
  TOGGLE: Toggle action.
14
14
  BLOCK: Block action.
15
15
  AUXRELAY: Auxiliary relay action.
@@ -39,8 +39,8 @@ class InputActionType(Enum):
39
39
  """
40
40
 
41
41
  VOID = 0
42
- TURNON = 1
43
- TURNOFF = 2
42
+ ON = 1
43
+ OFF = 2
44
44
  TOGGLE = 3
45
45
  BLOCK = 4
46
46
  AUXRELAY = 5
@@ -15,35 +15,65 @@ class SystemFunction(str, Enum):
15
15
  WRITE_CONFIG: Write configuration.
16
16
  BLINK: Blink LED function.
17
17
  UNBLINK: Unblink LED function.
18
- DOWNLOAD_MSACTIONTABLE: Download module specific action table.
18
+ UPLOAD_FIRMWARE_START: Start upload firmware.
19
+ UPLOAD_FIRMWARE_STOP: Stop upload firmware.
20
+ UPLOAD_FIRMWARE: Upload firmware.
21
+ UPLOAD_ACTIONTABLE: Upload ActionTable to module.
19
22
  DOWNLOAD_ACTIONTABLE: Download ActionTable.
23
+ UPLOAD_MSACTIONTABLE: Upload module specific action table to module.
24
+ DOWNLOAD_MSACTIONTABLE: Download module specific action table.
25
+ TELEGRAM_WRITE_START: Start writing telegram.
26
+ TELEGRAM_READ_START: Start reading telegram.
20
27
  EOF: End of msactiontable response.
28
+ TELEGRAM: Module specific telegram response.
21
29
  MSACTIONTABLE: Module specific action table response.
22
30
  ACTIONTABLE: Module specific action table response.
23
31
  ACK: Acknowledge response.
24
32
  NAK: Not acknowledge response.
33
+ UPLOAD_TOP_FIRMWARE_START: Start upload firmware (TOP).
34
+ UPLOAD_TOP_FIRMWARE_STOP: Stop upload firmware (TOP).
35
+ UPLOAD_TOP_FIRMWARE: Upload firmware (TOP).
36
+ ROTATE_ENABLE: Enable rotate.
37
+ ROTATE_DISABLE: Disable rotate.
25
38
  UNKNOWN_26: Used after discover, unknown purpose.
26
39
  ACTION: Action function.
27
40
  """
28
41
 
29
- NONE = "00" # Undefined
30
- DISCOVERY = "01" # Discover function
31
- READ_DATAPOINT = "02" # Read datapoint
32
- READ_CONFIG = "03" # Read configuration
33
- WRITE_CONFIG = "04" # Write configuration
34
- BLINK = "05" # Blink LED function
35
- UNBLINK = "06" # Unblink LED function
36
- DOWNLOAD_MSACTIONTABLE = (
37
- "13" # Download the module specific action table (MsActionTable)
38
- )
39
- DOWNLOAD_ACTIONTABLE = "11D" # Download ActionTable
40
- EOF = "16" # End of msactiontable response
41
- MSACTIONTABLE = "17" # module specific action table (MsActionTable) response
42
- ACTIONTABLE = "17" # module specific action table (MsActionTable) response
43
- ACK = "18" # Acknowledge response
44
- NAK = "19" # Not acknowledge response
45
- UNKNOWN_26 = "26" # Used after discover, but don't know what it is
46
- ACTION = "27" # Action function
42
+ NONE = "00" # F00D Undefined
43
+ DISCOVERY = "01" # F01D Discover function
44
+ READ_DATAPOINT = "02" # F02D Read datapoint
45
+ READ_CONFIG = "03" # F03D Read configuration
46
+ WRITE_CONFIG = "04" # F04D Write configuration
47
+ BLINK = "05" # F05D Blink LED function
48
+ UNBLINK = "06" # F06D Unblink LED function
49
+
50
+ UPLOAD_FIRMWARE_START = "07" # F07D Start Upload firmware
51
+ UPLOAD_FIRMWARE_STOP = "08" # F08D Stop Upload firmware
52
+ UPLOAD_FIRMWARE = "09" # F09D Upload firmware
53
+
54
+ UPLOAD_ACTIONTABLE = "10" # F10D Upload ActionTable
55
+ DOWNLOAD_ACTIONTABLE = "11" # F11D Download ActionTable
56
+ UPLOAD_MSACTIONTABLE = "12" # F12D Upload MsActionTable to module
57
+ DOWNLOAD_MSACTIONTABLE = "13" # F13D Download MsActionTable
58
+
59
+ TELEGRAM_WRITE_START = "14" # F14D Start writing telegram
60
+ TELEGRAM_READ_START = "15" # F15D Start reading telegram
61
+ EOF = "16" # F16D End of msactiontable response
62
+ TELEGRAM = "17" # F17D module specific Telegram response
63
+ MSACTIONTABLE = "17" # F17D module specific ms action table (Telegram) response
64
+ ACTIONTABLE = "17" # F17D module specific action table (Telegram) response
65
+ ACK = "18" # F18D Acknowledge / continue response
66
+ NAK = "19" # F19D Not acknowledge response
67
+
68
+ UPLOAD_TOP_FIRMWARE_START = "20" # F20D Start Upload firmware (TOP)
69
+ UPLOAD_TOP_FIRMWARE_STOP = "21" # F21D Stop Upload firmware (TOP)
70
+ UPLOAD_TOP_FIRMWARE = "22" # F22D Upload firmware (TOP)
71
+
72
+ ROTATE_ENABLE = "23" # F23D Enable rotate
73
+ ROTATE_DISABLE = "24" # F24D Disable rotate
74
+
75
+ UNKNOWN_26 = "26" # F26D Used after discover, but don't know what it is
76
+ ACTION = "27" # F27D Action function
47
77
 
48
78
  def get_description(self) -> str:
49
79
  """Get the description of the SystemFunction.
@@ -1,5 +1,7 @@
1
1
  """Serializer for ActionTable telegram encoding/decoding."""
2
2
 
3
+ import re
4
+
3
5
  from xp.models import ModuleTypeCode
4
6
  from xp.models.actiontable.actiontable import ActionTable, ActionTableEntry
5
7
  from xp.models.telegram.input_action_type import InputActionType
@@ -18,7 +20,13 @@ from xp.utils.serialization import (
18
20
 
19
21
 
20
22
  class ActionTableSerializer:
21
- """Handles serialization/deserialization of ActionTable to/from telegrams."""
23
+ """Handles serialization/deserialization of ActionTable to/from telegrams.
24
+
25
+ Attributes:
26
+ MAX_ENTRIES: Maximum number of entries in an ActionTable (96).
27
+ """
28
+
29
+ MAX_ENTRIES = 96 # ActionTable must always contain exactly 96 entries
22
30
 
23
31
  @staticmethod
24
32
  def from_data(data: bytes) -> ActionTable:
@@ -57,12 +65,12 @@ class ActionTableSerializer:
57
65
  try:
58
66
  module_type = ModuleTypeCode(module_type_raw)
59
67
  except ValueError:
60
- module_type = ModuleTypeCode.CP20 # Default fallback
68
+ module_type = ModuleTypeCode.NOMOD # Default fallback
61
69
 
62
70
  try:
63
71
  command = InputActionType(command_raw)
64
72
  except ValueError:
65
- command = InputActionType.TURNOFF # Default fallback
73
+ command = InputActionType.OFF # Default fallback
66
74
 
67
75
  try:
68
76
  parameter = TimeParam(parameter_raw)
@@ -91,7 +99,7 @@ class ActionTableSerializer:
91
99
  action_table: ActionTable to serialize
92
100
 
93
101
  Returns:
94
- Raw byte data for telegram
102
+ Raw byte data for telegram (always 480 bytes for 96 entries)
95
103
  """
96
104
  data = bytearray()
97
105
 
@@ -112,6 +120,14 @@ class ActionTableSerializer:
112
120
  [type_byte, link_byte, input_byte, output_command_byte, parameter_byte]
113
121
  )
114
122
 
123
+ # Pad to 96 entries with default NOMOD entries (00 00 00 00 00)
124
+ current_entries = len(action_table.entries)
125
+ if current_entries < ActionTableSerializer.MAX_ENTRIES:
126
+ # Default entry: NOMOD 0 0 > 0 OFF (all zeros)
127
+ padding_bytes = [0x00, 0x00, 0x00, 0x00, 0x00]
128
+ for _ in range(ActionTableSerializer.MAX_ENTRIES - current_entries):
129
+ data.extend(padding_bytes)
130
+
115
131
  return bytes(data)
116
132
 
117
133
  @staticmethod
@@ -141,29 +157,117 @@ class ActionTableSerializer:
141
157
  return ActionTableSerializer.from_data(data)
142
158
 
143
159
  @staticmethod
144
- def format_decoded_output(action_table: ActionTable) -> str:
160
+ def format_decoded_output(action_table: ActionTable) -> list[str]:
145
161
  """Format ActionTable as human-readable decoded output.
146
162
 
147
163
  Args:
148
164
  action_table: ActionTable to format
149
165
 
150
166
  Returns:
151
- Human-readable string representation
167
+ List of human-readable string representations
152
168
  """
153
169
  lines = []
154
170
  for entry in action_table.entries:
155
- # Format: CP20 0 0 > 1 OFF;
171
+ # Format: CP20 0 0 > 1 OFF [param];
156
172
  module_type = entry.module_type.name
157
173
  link = entry.link_number
158
174
  input_num = entry.module_input
159
175
  output = entry.module_output
160
176
  command = entry.command.name
161
177
 
162
- # Add prefix for special commands
178
+ # Add prefix for inverted commands
163
179
  if entry.inverted:
164
180
  command = f"~{command}"
165
181
 
166
- line = f"{module_type} {link} {input_num} > {output} {command};"
182
+ # Build base line
183
+ line = f"{module_type} {link} {input_num} > {output} {command}"
184
+
185
+ # Add parameter if present and non-zero
186
+ if entry.parameter is not None and entry.parameter.value != 0:
187
+ line += f" {entry.parameter.value}"
188
+
189
+ # Add semicolon terminator
190
+ line += ";"
191
+
167
192
  lines.append(line)
168
193
 
169
- return "\n".join(lines)
194
+ return lines
195
+
196
+ @staticmethod
197
+ def parse_action_string(action_str: str) -> ActionTableEntry:
198
+ """Parse action table entry from string format.
199
+
200
+ Args:
201
+ action_str: String in format "CP20 0 0 > 1 OFF" or "CP20 0 1 > 1 ~ON"
202
+
203
+ Returns:
204
+ Parsed ActionTableEntry
205
+
206
+ Raises:
207
+ ValueError: If string format is invalid
208
+ """
209
+ # Remove trailing semicolon if present
210
+ action_str = action_str.strip().rstrip(";")
211
+
212
+ # Pattern: <Type> <Link> <Input> > <Output> <Command> [Parameter]
213
+ pattern = r"^(\w+)\s+(\d+)\s+(\d+)\s+>\s+(\d+)\s+(~?)(\w+)(?:\s+(\d+))?$"
214
+ match = re.match(pattern, action_str)
215
+
216
+ if not match:
217
+ raise ValueError(f"Invalid action table format: {action_str}")
218
+
219
+ (
220
+ module_type_str,
221
+ link_str,
222
+ input_str,
223
+ output_str,
224
+ inverted_str,
225
+ command_str,
226
+ parameter_str,
227
+ ) = match.groups()
228
+
229
+ # Parse module type
230
+ try:
231
+ module_type = ModuleTypeCode[module_type_str]
232
+ except KeyError:
233
+ raise ValueError(f"Invalid module type: {module_type_str}")
234
+
235
+ # Parse command
236
+ try:
237
+ command = InputActionType[command_str]
238
+ except KeyError:
239
+ raise ValueError(f"Invalid command: {command_str}")
240
+
241
+ # Parse parameter (default to NONE)
242
+ parameter = TimeParam.NONE
243
+ if parameter_str:
244
+ try:
245
+ parameter = TimeParam(int(parameter_str))
246
+ except ValueError:
247
+ raise ValueError(f"Invalid parameter: {parameter_str}")
248
+
249
+ return ActionTableEntry(
250
+ module_type=module_type,
251
+ link_number=int(link_str),
252
+ module_input=int(input_str),
253
+ module_output=int(output_str),
254
+ command=command,
255
+ parameter=parameter,
256
+ inverted=bool(inverted_str),
257
+ )
258
+
259
+ @staticmethod
260
+ def parse_action_table(action_strings: list[str]) -> ActionTable:
261
+ """Parse action table from list of string entries.
262
+
263
+ Args:
264
+ action_strings: List of action strings from conson.yml
265
+
266
+ Returns:
267
+ Parsed ActionTable
268
+ """
269
+ entries = [
270
+ ActionTableSerializer.parse_action_string(action_str)
271
+ for action_str in action_strings
272
+ ]
273
+ return ActionTable(entries=entries)
@@ -1,7 +1,8 @@
1
1
  """Service for downloading ActionTable via Conbus protocol."""
2
2
 
3
3
  import logging
4
- from typing import Callable, Optional
4
+ from dataclasses import asdict
5
+ from typing import Any, Callable, Dict, Optional
5
6
 
6
7
  from twisted.internet.posixbase import PosixReactorBase
7
8
 
@@ -16,11 +17,10 @@ from xp.services.telegram.telegram_service import TelegramService
16
17
 
17
18
 
18
19
  class ActionTableService(ConbusProtocol):
19
- """
20
- TCP client service for sending telegrams to Conbus servers.
20
+ """TCP client service for downloading action tables from Conbus modules.
21
21
 
22
22
  Manages TCP socket connections, handles telegram generation and transmission,
23
- and processes server responses.
23
+ and processes server responses for action table downloads.
24
24
  """
25
25
 
26
26
  def __init__(
@@ -30,7 +30,7 @@ class ActionTableService(ConbusProtocol):
30
30
  actiontable_serializer: ActionTableSerializer,
31
31
  telegram_service: TelegramService,
32
32
  ) -> None:
33
- """Initialize the Conbus client send service.
33
+ """Initialize the action table download service.
34
34
 
35
35
  Args:
36
36
  cli_config: Conbus client configuration.
@@ -44,7 +44,10 @@ class ActionTableService(ConbusProtocol):
44
44
  self.serial_number: str = ""
45
45
  self.progress_callback: Optional[Callable[[str], None]] = None
46
46
  self.error_callback: Optional[Callable[[str], None]] = None
47
- self.finish_callback: Optional[Callable[[ActionTable], None]] = None
47
+ self.finish_callback: Optional[
48
+ Callable[[ActionTable, Dict[str, Any], list[str]], None]
49
+ ] = None
50
+
48
51
  self.actiontable_data: list[str] = []
49
52
  # Set up logging
50
53
  self.logger = logging.getLogger(__name__)
@@ -113,8 +116,10 @@ class ActionTableService(ConbusProtocol):
113
116
  all_data = "".join(self.actiontable_data)
114
117
  # Deserialize from received data
115
118
  actiontable = self.serializer.from_encoded_string(all_data)
119
+ actiontable_dict = asdict(actiontable)
120
+ actiontable_short = self.serializer.format_decoded_output(actiontable)
116
121
  if self.finish_callback:
117
- self.finish_callback(actiontable)
122
+ self.finish_callback(actiontable, actiontable_dict, actiontable_short)
118
123
 
119
124
  def failed(self, message: str) -> None:
120
125
  """Handle failed connection event.
@@ -131,7 +136,7 @@ class ActionTableService(ConbusProtocol):
131
136
  serial_number: str,
132
137
  progress_callback: Callable[[str], None],
133
138
  error_callback: Callable[[str], None],
134
- finish_callback: Callable[[ActionTable], None],
139
+ finish_callback: Callable[[ActionTable, Dict[str, Any], list[str]], None],
135
140
  timeout_seconds: Optional[float] = None,
136
141
  ) -> None:
137
142
  """Run reactor in dedicated thread with its own event loop.
@@ -0,0 +1,91 @@
1
+ """Service for listing modules with action table configurations from conson.yml."""
2
+
3
+ import logging
4
+ from pathlib import Path
5
+ from typing import Any, Callable, Optional
6
+
7
+
8
+ class ActionTableListService:
9
+ """Service for listing modules with action table configurations.
10
+
11
+ Reads conson.yml and returns a list of all modules that have action table
12
+ configurations defined.
13
+ """
14
+
15
+ def __init__(self) -> None:
16
+ """Initialize the action table list service."""
17
+ self.logger = logging.getLogger(__name__)
18
+ self.finish_callback: Optional[Callable[[dict[str, Any]], None]] = None
19
+ self.error_callback: Optional[Callable[[str], None]] = None
20
+
21
+ def __enter__(self) -> "ActionTableListService":
22
+ """Context manager entry.
23
+
24
+ Returns:
25
+ Self for context manager use.
26
+ """
27
+ return self
28
+
29
+ def __exit__(self, _exc_type: Any, _exc_val: Any, _exc_tb: Any) -> None:
30
+ """Context manager exit."""
31
+ pass
32
+
33
+ def start(
34
+ self,
35
+ finish_callback: Callable[[dict[str, Any]], None],
36
+ error_callback: Callable[[str], None],
37
+ config_path: Optional[Path] = None,
38
+ ) -> None:
39
+ """List all modules with action table configurations.
40
+
41
+ Args:
42
+ finish_callback: Callback to invoke with the module list.
43
+ error_callback: Callback to invoke on error.
44
+ config_path: Optional path to conson.yml. Defaults to current directory.
45
+ """
46
+ self.finish_callback = finish_callback
47
+ self.error_callback = error_callback
48
+
49
+ # Default to current directory if not specified
50
+ if config_path is None:
51
+ config_path = Path.cwd() / "conson.yml"
52
+
53
+ # Check if config file exists
54
+ if not config_path.exists():
55
+ self._handle_error("Error: conson.yml not found in current directory")
56
+ return
57
+
58
+ # Load configuration
59
+ try:
60
+ from xp.models.homekit.homekit_conson_config import ConsonModuleListConfig
61
+
62
+ config = ConsonModuleListConfig.from_yaml(str(config_path))
63
+ except Exception as e:
64
+ self.logger.error(f"Failed to load conson.yml: {e}")
65
+ self._handle_error(f"Error: Failed to load conson.yml: {e}")
66
+ return
67
+
68
+ # Filter modules that have action_table configured
69
+ modules_with_actiontable = [
70
+ {
71
+ "serial_number": module.serial_number,
72
+ "module_type": module.module_type,
73
+ }
74
+ for module in config.root
75
+ ]
76
+
77
+ # Prepare result
78
+ result = {"modules": modules_with_actiontable}
79
+
80
+ # Invoke callback
81
+ if self.finish_callback is not None:
82
+ self.finish_callback(result)
83
+
84
+ def _handle_error(self, message: str) -> None:
85
+ """Handle error and invoke error callback.
86
+
87
+ Args:
88
+ message: Error message.
89
+ """
90
+ if self.error_callback is not None:
91
+ self.error_callback(message)
@@ -0,0 +1,89 @@
1
+ """Service for showing action table configuration for a specific module."""
2
+
3
+ import logging
4
+ from pathlib import Path
5
+ from typing import Any, Callable, Optional
6
+
7
+ from xp.models.homekit.homekit_conson_config import ConsonModuleConfig
8
+
9
+
10
+ class ActionTableShowService:
11
+ """Service for showing action table configuration for a specific module.
12
+
13
+ Reads conson.yml and returns the action table configuration for the specified
14
+ module serial number.
15
+ """
16
+
17
+ def __init__(self) -> None:
18
+ """Initialize the action table show service."""
19
+ self.logger = logging.getLogger(__name__)
20
+ self.finish_callback: Optional[Callable[[ConsonModuleConfig], None]] = None
21
+ self.error_callback: Optional[Callable[[str], None]] = None
22
+
23
+ def __enter__(self) -> "ActionTableShowService":
24
+ """Context manager entry.
25
+
26
+ Returns:
27
+ Self for context manager use.
28
+ """
29
+ return self
30
+
31
+ def __exit__(self, _exc_type: Any, _exc_val: Any, _exc_tb: Any) -> None:
32
+ """Context manager exit."""
33
+ pass
34
+
35
+ def start(
36
+ self,
37
+ serial_number: str,
38
+ finish_callback: Callable[[ConsonModuleConfig], None],
39
+ error_callback: Callable[[str], None],
40
+ config_path: Optional[Path] = None,
41
+ ) -> None:
42
+ """Show action table configuration for a specific module.
43
+
44
+ Args:
45
+ serial_number: Module serial number.
46
+ finish_callback: Callback to invoke with the module configuration.
47
+ error_callback: Callback to invoke on error.
48
+ config_path: Optional path to conson.yml. Defaults to current directory.
49
+ """
50
+ self.finish_callback = finish_callback
51
+ self.error_callback = error_callback
52
+
53
+ # Default to current directory if not specified
54
+ if config_path is None:
55
+ config_path = Path.cwd() / "conson.yml"
56
+
57
+ # Check if config file exists
58
+ if not config_path.exists():
59
+ self._handle_error("Error: conson.yml not found in current directory")
60
+ return
61
+
62
+ # Load configuration
63
+ try:
64
+ from xp.models.homekit.homekit_conson_config import ConsonModuleListConfig
65
+
66
+ config = ConsonModuleListConfig.from_yaml(str(config_path))
67
+ except Exception as e:
68
+ self.logger.error(f"Failed to load conson.yml: {e}")
69
+ self._handle_error(f"Error: Failed to load conson.yml: {e}")
70
+ return
71
+
72
+ # Find module
73
+ module = config.find_module(serial_number)
74
+ if not module:
75
+ self._handle_error(f"Error: Module {serial_number} not found in conson.yml")
76
+ return
77
+
78
+ # Invoke callback
79
+ if self.finish_callback is not None:
80
+ self.finish_callback(module)
81
+
82
+ def _handle_error(self, message: str) -> None:
83
+ """Handle error and invoke error callback.
84
+
85
+ Args:
86
+ message: Error message.
87
+ """
88
+ if self.error_callback is not None:
89
+ self.error_callback(message)
@@ -0,0 +1,211 @@
1
+ """Service for uploading ActionTable via Conbus protocol."""
2
+
3
+ import logging
4
+ from typing import Any, Callable, Optional
5
+
6
+ from twisted.internet.posixbase import PosixReactorBase
7
+
8
+ from xp.models import ConbusClientConfig
9
+ from xp.models.homekit.homekit_conson_config import ConsonModuleListConfig
10
+ from xp.models.protocol.conbus_protocol import TelegramReceivedEvent
11
+ from xp.models.telegram.system_function import SystemFunction
12
+ from xp.models.telegram.telegram_type import TelegramType
13
+ from xp.services.actiontable.actiontable_serializer import ActionTableSerializer
14
+ from xp.services.protocol import ConbusProtocol
15
+ from xp.services.telegram.telegram_service import TelegramService
16
+
17
+
18
+ class ActionTableUploadService(ConbusProtocol):
19
+ """TCP client service for uploading action tables to Conbus modules.
20
+
21
+ Manages TCP socket connections, handles telegram generation and transmission,
22
+ and processes server responses for action table uploads.
23
+ """
24
+
25
+ def __init__(
26
+ self,
27
+ cli_config: ConbusClientConfig,
28
+ reactor: PosixReactorBase,
29
+ actiontable_serializer: ActionTableSerializer,
30
+ telegram_service: TelegramService,
31
+ conson_config: ConsonModuleListConfig,
32
+ ) -> None:
33
+ """Initialize the action table upload service.
34
+
35
+ Args:
36
+ cli_config: Conbus client configuration.
37
+ reactor: Twisted reactor instance.
38
+ actiontable_serializer: Action table serializer.
39
+ telegram_service: Telegram service for parsing.
40
+ conson_config: Conson module list configuration.
41
+ """
42
+ super().__init__(cli_config, reactor)
43
+ self.serializer = actiontable_serializer
44
+ self.telegram_service = telegram_service
45
+ self.conson_config = conson_config
46
+ self.serial_number: str = ""
47
+ self.progress_callback: Optional[Callable[[str], None]] = None
48
+ self.error_callback: Optional[Callable[[str], None]] = None
49
+ self.success_callback: Optional[Callable[[], None]] = None
50
+
51
+ # Upload state
52
+ self.upload_data_chunks: list[str] = []
53
+ self.current_chunk_index: int = 0
54
+
55
+ # Set up logging
56
+ self.logger = logging.getLogger(__name__)
57
+
58
+ def connection_established(self) -> None:
59
+ """Handle connection established event."""
60
+ self.logger.debug("Connection established, sending upload actiontable telegram")
61
+ self.send_telegram(
62
+ telegram_type=TelegramType.SYSTEM,
63
+ serial_number=self.serial_number,
64
+ system_function=SystemFunction.UPLOAD_ACTIONTABLE,
65
+ data_value="00",
66
+ )
67
+
68
+ def telegram_sent(self, telegram_sent: str) -> None:
69
+ """Handle telegram sent event.
70
+
71
+ Args:
72
+ telegram_sent: The telegram that was sent.
73
+ """
74
+ self.logger.debug(f"Telegram sent: {telegram_sent}")
75
+
76
+ def telegram_received(self, telegram_received: TelegramReceivedEvent) -> None:
77
+ """Handle telegram received event.
78
+
79
+ Args:
80
+ telegram_received: The telegram received event.
81
+ """
82
+ self.logger.debug(f"Telegram received: {telegram_received}")
83
+ if (
84
+ not telegram_received.checksum_valid
85
+ or telegram_received.telegram_type != TelegramType.REPLY.value
86
+ or telegram_received.serial_number != self.serial_number
87
+ ):
88
+ self.logger.debug("Not a reply response")
89
+ return
90
+
91
+ reply_telegram = self.telegram_service.parse_reply_telegram(
92
+ telegram_received.frame
93
+ )
94
+
95
+ self._handle_upload_response(reply_telegram)
96
+
97
+ def _handle_upload_response(self, reply_telegram: Any) -> None:
98
+ """Handle telegram responses during upload.
99
+
100
+ Args:
101
+ reply_telegram: Parsed reply telegram.
102
+ """
103
+ if reply_telegram.system_function == SystemFunction.ACK:
104
+ self.logger.debug("Received ACK for upload")
105
+ # Send next chunk or EOF
106
+ if self.current_chunk_index < len(self.upload_data_chunks):
107
+ chunk = self.upload_data_chunks[self.current_chunk_index]
108
+ self.logger.debug(f"Sending chunk {self.current_chunk_index + 1}")
109
+
110
+ # Calculate prefix: AA, AB, AC, AD, AE, AF, AG, AH, AI, AJ, AK, AL, AM, AN, AO
111
+ # First character: 'A' (fixed)
112
+ # Second character: 'A' + chunk_index (sequential counter A-O for 15 chunks)
113
+ prefix_hex = f"A{ord('A') + self.current_chunk_index:c}"
114
+
115
+ self.send_telegram(
116
+ telegram_type=TelegramType.SYSTEM,
117
+ serial_number=self.serial_number,
118
+ system_function=SystemFunction.ACTIONTABLE,
119
+ data_value=f"{prefix_hex}{chunk}",
120
+ )
121
+ self.current_chunk_index += 1
122
+ if self.progress_callback:
123
+ self.progress_callback(".")
124
+ else:
125
+ # All chunks sent, send EOF
126
+ self.logger.debug("All chunks sent, sending EOF")
127
+ self.send_telegram(
128
+ telegram_type=TelegramType.SYSTEM,
129
+ serial_number=self.serial_number,
130
+ system_function=SystemFunction.EOF,
131
+ data_value="00",
132
+ )
133
+ if self.success_callback:
134
+ self.success_callback()
135
+ self._stop_reactor()
136
+ elif reply_telegram.system_function == SystemFunction.NAK:
137
+ self.logger.debug("Received NAK during upload")
138
+ self.failed("Upload failed: NAK received")
139
+ else:
140
+ self.logger.debug(f"Unexpected response during upload: {reply_telegram}")
141
+
142
+ def failed(self, message: str) -> None:
143
+ """Handle failed connection event.
144
+
145
+ Args:
146
+ message: Failure message.
147
+ """
148
+ self.logger.debug(f"Failed: {message}")
149
+ if self.error_callback:
150
+ self.error_callback(message)
151
+ self._stop_reactor()
152
+
153
+ def start(
154
+ self,
155
+ serial_number: str,
156
+ progress_callback: Callable[[str], None],
157
+ error_callback: Callable[[str], None],
158
+ success_callback: Callable[[], None],
159
+ timeout_seconds: Optional[float] = None,
160
+ ) -> None:
161
+ """Upload action table to module.
162
+
163
+ Uploads the action table configuration to the specified module.
164
+
165
+ Args:
166
+ serial_number: Module serial number.
167
+ progress_callback: Callback for progress updates.
168
+ error_callback: Callback for errors.
169
+ success_callback: Callback when upload completes successfully.
170
+ timeout_seconds: Optional timeout in seconds.
171
+ """
172
+ self.logger.info("Starting actiontable upload")
173
+ self.serial_number = serial_number
174
+ if timeout_seconds:
175
+ self.timeout_seconds = timeout_seconds
176
+ self.progress_callback = progress_callback
177
+ self.error_callback = error_callback
178
+ self.success_callback = success_callback
179
+
180
+ # Find module
181
+ module = self.conson_config.find_module(serial_number)
182
+ if not module:
183
+ self.failed(f"Module {serial_number} not found in conson.yml")
184
+ return
185
+
186
+ # Parse action table strings to ActionTable object
187
+ try:
188
+ module_action_table = module.action_table or []
189
+ action_table = self.serializer.parse_action_table(module_action_table)
190
+ except ValueError as e:
191
+ self.logger.error(f"Invalid action table format: {e}")
192
+ self.failed(f"Invalid action table format: {e}")
193
+ return
194
+
195
+ # Encode action table to hex string
196
+ encoded_data = self.serializer.to_encoded_string(action_table)
197
+
198
+ # Chunk the data into 64 byte chunks
199
+ chunk_size = 64
200
+ self.upload_data_chunks = [
201
+ encoded_data[i : i + chunk_size]
202
+ for i in range(0, len(encoded_data), chunk_size)
203
+ ]
204
+ self.current_chunk_index = 0
205
+
206
+ self.logger.debug(
207
+ f"Upload data encoded: {len(encoded_data)} chars, "
208
+ f"{len(self.upload_data_chunks)} chunks"
209
+ )
210
+
211
+ self.start_reactor()
xp/utils/dependencies.py CHANGED
@@ -20,7 +20,18 @@ from xp.services.actiontable.msactiontable_xp24_serializer import (
20
20
  from xp.services.actiontable.msactiontable_xp33_serializer import (
21
21
  Xp33MsActionTableSerializer,
22
22
  )
23
- from xp.services.conbus.actiontable.actiontable_service import ActionTableService
23
+ from xp.services.conbus.actiontable.actiontable_download_service import (
24
+ ActionTableService,
25
+ )
26
+ from xp.services.conbus.actiontable.actiontable_list_service import (
27
+ ActionTableListService,
28
+ )
29
+ from xp.services.conbus.actiontable.actiontable_show_service import (
30
+ ActionTableShowService,
31
+ )
32
+ from xp.services.conbus.actiontable.actiontable_upload_service import (
33
+ ActionTableUploadService,
34
+ )
24
35
  from xp.services.conbus.actiontable.msactiontable_service import MsActionTableService
25
36
  from xp.services.conbus.conbus_blink_all_service import ConbusBlinkAllService
26
37
  from xp.services.conbus.conbus_blink_service import ConbusBlinkService
@@ -215,6 +226,30 @@ class ServiceContainer:
215
226
  scope=punq.Scope.singleton,
216
227
  )
217
228
 
229
+ self.container.register(
230
+ ActionTableUploadService,
231
+ factory=lambda: ActionTableUploadService(
232
+ cli_config=self.container.resolve(ConbusClientConfig),
233
+ reactor=self.container.resolve(PosixReactorBase),
234
+ actiontable_serializer=self.container.resolve(ActionTableSerializer),
235
+ telegram_service=self.container.resolve(TelegramService),
236
+ conson_config=self.container.resolve(ConsonModuleListConfig),
237
+ ),
238
+ scope=punq.Scope.singleton,
239
+ )
240
+
241
+ self.container.register(
242
+ ActionTableListService,
243
+ factory=ActionTableListService,
244
+ scope=punq.Scope.singleton,
245
+ )
246
+
247
+ self.container.register(
248
+ ActionTableShowService,
249
+ factory=ActionTableShowService,
250
+ scope=punq.Scope.singleton,
251
+ )
252
+
218
253
  self.container.register(
219
254
  Xp20MsActionTableSerializer,
220
255
  factory=lambda: Xp20MsActionTableSerializer,