conson-xp 1.39.0__py3-none-any.whl → 1.41.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.39.0
3
+ Version: 1.41.0
4
4
  Summary: XP Protocol Communication Tools
5
5
  Author-Email: ldvchosal <ldvchosal@github.com>
6
6
  License: MIT License
@@ -376,6 +376,7 @@ xp conbus msactiontable
376
376
  xp conbus msactiontable download
377
377
  xp conbus msactiontable list
378
378
  xp conbus msactiontable show
379
+ xp conbus msactiontable upload
379
380
 
380
381
 
381
382
  xp conbus output
@@ -1,8 +1,8 @@
1
- conson_xp-1.39.0.dist-info/METADATA,sha256=ZnnTqw5hdtgrYNryR8of7kj7-8cD0Hy5GB4h_syNtok,11330
2
- conson_xp-1.39.0.dist-info/WHEEL,sha256=tsUv_t7BDeJeRHaSrczbGeuK-TtDpGsWi_JfpzD255I,90
3
- conson_xp-1.39.0.dist-info/entry_points.txt,sha256=1OcdIcDM1hz3ljCXgybaPUh1IOFEwkaTgLIW9u9zGeg,50
4
- conson_xp-1.39.0.dist-info/licenses/LICENSE,sha256=rxj6woMM-r3YCyGq_UHFtbh7kHTAJgrccH6O-33IDE4,1419
5
- xp/__init__.py,sha256=XLd0Ey5N2BokGf5bKHhDEB2BpUIExFL_W41vN72w-Ko,181
1
+ conson_xp-1.41.0.dist-info/METADATA,sha256=pML1FepvoeR1ktngrS69ieWa5rMO0j43m8BC22G_UW0,11361
2
+ conson_xp-1.41.0.dist-info/WHEEL,sha256=tsUv_t7BDeJeRHaSrczbGeuK-TtDpGsWi_JfpzD255I,90
3
+ conson_xp-1.41.0.dist-info/entry_points.txt,sha256=1OcdIcDM1hz3ljCXgybaPUh1IOFEwkaTgLIW9u9zGeg,50
4
+ conson_xp-1.41.0.dist-info/licenses/LICENSE,sha256=rxj6woMM-r3YCyGq_UHFtbh7kHTAJgrccH6O-33IDE4,1419
5
+ xp/__init__.py,sha256=fIc-tmd56J8tWGwB8NrHfsUhz6rBuPHOZmnmWfimedo,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=G7A1KFRSV0CEeDTqr_khu-K9_sc01CTI2KSfkFcaBRM,4949
@@ -20,7 +20,7 @@ xp/cli/commands/conbus/conbus_export_commands.py,sha256=KSeXZbD6tO0_BMgqmg20iVaE
20
20
  xp/cli/commands/conbus/conbus_lightlevel_commands.py,sha256=AQhVneN5_rH6wd7D4KW80XIMh9MGjJv85gN57S206j4,7036
21
21
  xp/cli/commands/conbus/conbus_linknumber_commands.py,sha256=hVr1g6nDTa4MezW8joHPjPuZcMp2ttd9PfZaT9sQED4,3528
22
22
  xp/cli/commands/conbus/conbus_modulenumber_commands.py,sha256=FjFWnLU_aUoAXQ2tKKLC-ichNiIb90_9OWpSdIUyHvc,3600
23
- xp/cli/commands/conbus/conbus_msactiontable_commands.py,sha256=Xs6TxwyzqMuUtvH1aQkQQToTR57Zzbs4IyeH28NKrtc,5616
23
+ xp/cli/commands/conbus/conbus_msactiontable_commands.py,sha256=2M4abf8i51HwLF3wmUAgoCN-veXKMA8Fd0nWThyNPdg,9706
24
24
  xp/cli/commands/conbus/conbus_output_commands.py,sha256=rJx8pfsl_ZeCNXhEelsY7mfYnaj_DHdz4TC-e8d5QGs,5286
25
25
  xp/cli/commands/conbus/conbus_raw_commands.py,sha256=892-S6wxp5xNPz6K86Le8KtQXNO4a0PQv20Hzx3vhiA,1996
26
26
  xp/cli/commands/conbus/conbus_receive_commands.py,sha256=_PsC-3xidmJBuOWUS60iDzhSHYYn5ZFmORXap-ljVGM,1902
@@ -58,10 +58,9 @@ xp/cli/utils/xp_module_type.py,sha256=qSFJBRceqPi_cUFPxAWtLUNq37-KwUEjo9ekYOj7kL
58
58
  xp/models/__init__.py,sha256=lROqr559DGd8WpJJUtfPT95VERCwMZHpBDEc96QSxQ0,1312
59
59
  xp/models/actiontable/__init__.py,sha256=6kVq1rTOlpc24sZxGGVWkY48tqR42YWHLQHqakWqlPc,43
60
60
  xp/models/actiontable/actiontable.py,sha256=bIeluZhMsvukkSwy2neaewavU8YR6Pso3PIvJ8ENlGg,1251
61
- xp/models/actiontable/msactiontable.py,sha256=42RdQB3rUzAP_UeH5PS6iADOnkev47rDR77_yttSkBg,192
62
- xp/models/actiontable/msactiontable_xp20.py,sha256=iMNKrDpLcaynaG1pflfyoQjey-KUsoSjqy4J4XF-YGk,2031
63
- xp/models/actiontable/msactiontable_xp24.py,sha256=yYaWS7VRr2EyLXozfoJWLmgQmMU9dGo81V4VU4q6cUo,9464
64
- xp/models/actiontable/msactiontable_xp33.py,sha256=K1noQe5TNoTFLWE58r0-ZOB8lYM3oXFqoNL7z7Uob5A,2945
61
+ xp/models/actiontable/msactiontable_xp20.py,sha256=zc9akPpuaW-pBk1vD9xn0JDWm_c2fiJYDuuk-DbvbGQ,5006
62
+ xp/models/actiontable/msactiontable_xp24.py,sha256=tpgYvlQwxhjo70Ucsg_rB9ox7-jlG2b-GBj-UXwP2Ic,9377
63
+ xp/models/actiontable/msactiontable_xp33.py,sha256=p_0HrvUmnqEEUlle7n0vpspGXFPrO5pXZeVF7n9K19g,11781
65
64
  xp/models/conbus/__init__.py,sha256=VIusMWQdBtlwDgj7oSj06wQkklihTp4oWFShvP_JUgA,35
66
65
  xp/models/conbus/conbus.py,sha256=mZQzKPfrdttT-qUnYUSyrEYyc_eHs8z301E5ejeiyvk,2689
67
66
  xp/models/conbus/conbus_autoreport.py,sha256=lKotDfxRBb7h2Z1d4qI3KhhLJhFDwKqLbSdG5Makm8Y,2289
@@ -82,7 +81,7 @@ xp/models/conbus/conbus_raw.py,sha256=xqvYao6IE1SXum7JBgZpSuWXm9x_QZquS9N_3_r0Hj
82
81
  xp/models/conbus/conbus_receive.py,sha256=-1u1qK-texfKCNZV-GYf_9KyLtJdIrx7HuZsKzu26Ow,1322
83
82
  xp/models/conbus/conbus_writeconfig.py,sha256=z8fdJeFLyGJW7UMHcHxGrHIMS6gG1D3aXeYUkBuwnEg,2136
84
83
  xp/models/config/__init__.py,sha256=gEZnX9eE3DjFtLtF32riEjJQLypqQRbyPauBI4Cowbs,36
85
- xp/models/config/conson_module_config.py,sha256=KL9AYVUiet50SaVajIjicW6FNQ38Qm1vk1jZbZnMb00,2708
84
+ xp/models/config/conson_module_config.py,sha256=2uM1M4oKgQ3pkuEAEHHFMwZVGpWImJEe91TWgfS1ikU,2981
86
85
  xp/models/homekit/__init__.py,sha256=5HDSOClCu0ArK3IICn3_LDMMLBAzLjBxUUSF73bxSSk,34
87
86
  xp/models/homekit/homekit_accessory.py,sha256=NsHFhskuxIdJpF9-MvXHtjkLYvNHmSGFOy0GmQv3PY4,1038
88
87
  xp/models/homekit/homekit_config.py,sha256=Y_k92PsKHFBnn3r1_RSZHJP5uLH27Gw8G7Bj5N8jvUE,2904
@@ -117,9 +116,9 @@ xp/services/__init__.py,sha256=W9YZyrkh7vm--ZHhAXNQiOYQs5yhhmUHXP5I0Lf1XBg,782
117
116
  xp/services/actiontable/__init__.py,sha256=z6js4EuJ6xKHaseTEhuEvKo1tr9K1XyQiruReJtBiPY,26
118
117
  xp/services/actiontable/actiontable_serializer.py,sha256=U7bhd8lYMUJAsFydCt_Y5uOJoUODhSjRlUQPD6jsqMo,8517
119
118
  xp/services/actiontable/msactiontable_serializer.py,sha256=RRL6TZ1gpSQw81kAiw2BV3jTqm4fCJC0pWIcO26Cmos,174
120
- xp/services/actiontable/msactiontable_xp20_serializer.py,sha256=UP274qQfjFbAMojpeeqz72vJvNgAo_J5nyr4OFExsqE,6404
121
- xp/services/actiontable/msactiontable_xp24_serializer.py,sha256=kgEKT-GSlye4ExjeXIAB1O-qi0cAm6POxoC2bKlwBi0,4601
122
- xp/services/actiontable/msactiontable_xp33_serializer.py,sha256=XwBG-flfrSYxm0uy93gutZRj0LKGvOcJGnJpIFQQOuI,8747
119
+ xp/services/actiontable/msactiontable_xp20_serializer.py,sha256=5e9L-xlFUSu2RJnhr0BQIbhIPZVRiOhp8kKvbWsU5BQ,6438
120
+ xp/services/actiontable/msactiontable_xp24_serializer.py,sha256=ku84HCCjYDM9XpRKIWx1HnW1_U0LyFDH57f9Gig-pgQ,4607
121
+ xp/services/actiontable/msactiontable_xp33_serializer.py,sha256=MFjEwSsIa8l3RJt-ig828E6kyiFYYXMHFKe4Q0A3NvA,8781
123
122
  xp/services/conbus/__init__.py,sha256=Hi35sMKu9o6LpYoi2tQDaQoMb8M5sOt_-LUTxxaCU_0,28
124
123
  xp/services/conbus/actiontable/__init__.py,sha256=oD6vRk_Ye-eZ9s_hldAgtRJFu4mfAnODqpkJUGHHszk,40
125
124
  xp/services/conbus/actiontable/actiontable_download_service.py,sha256=C6cjNRRsl7_jjn94I6ycCDvoqIpivNv0cMVkR-CQBXk,7608
@@ -140,9 +139,10 @@ xp/services/conbus/conbus_raw_service.py,sha256=FmUaF9o2nFZVP8LpabKIwkg0P8coLCke
140
139
  xp/services/conbus/conbus_receive_service.py,sha256=7wOaEDrdoXwZE9MeUM89eB3hobYpvtbYk_YLv3MVAtc,5352
141
140
  xp/services/conbus/conbus_scan_service.py,sha256=QN7_x8BtNbHnqG7akcooAAcjz9Ex2y3VNDdhShKHUX8,6824
142
141
  xp/services/conbus/msactiontable/__init__.py,sha256=rDYzumPSfcTjDADHxjE7bXQoeWtZTDGaYzFTYdVl_9g,42
143
- xp/services/conbus/msactiontable/msactiontable_download_service.py,sha256=V3qa_DIPC2G2ihCNxPo0f17AY5v7mHQsZd9y3dOyekg,10077
144
- xp/services/conbus/msactiontable/msactiontable_list_service.py,sha256=9mAybvJLoZOzJSEV0QRh5iYY_j0ArbyZvFZa8y9rpWE,2886
142
+ xp/services/conbus/msactiontable/msactiontable_download_service.py,sha256=YAQeUAO04VkRTEvWwXBD_b6tdVjDYk55K4pZd7lxfE8,10049
143
+ xp/services/conbus/msactiontable/msactiontable_list_service.py,sha256=bTqUI2xs3Ie0MeZ_PYm-Bgx9A-Eewlpc8Tv6jhi1_kA,3127
145
144
  xp/services/conbus/msactiontable/msactiontable_show_service.py,sha256=pyoB5xN1bDh0E8kity4k0UnYpc7-YWhs8oIMvAeC9Xk,3023
145
+ xp/services/conbus/msactiontable/msactiontable_upload_service.py,sha256=D_sWGFlABVCBxzqt-6y8eB--ZpywS5MjC6ER-JESq74,12336
146
146
  xp/services/conbus/write_config_service.py,sha256=PQsN7rtTKHpwtAG8moLksUfRVqqE_0sxdE37meR1ZQ0,8935
147
147
  xp/services/homekit/__init__.py,sha256=xAMKmln_AmEFdOOJGKWYi96seRlKDQpKx3-hm7XbdIo,36
148
148
  xp/services/homekit/homekit_cache_service.py,sha256=NdijyH5_iyhsTHBb-OyT8Y2xnNDj8F5MP8neoVQ26hY,11010
@@ -201,10 +201,10 @@ xp/term/widgets/protocol_log.py,sha256=CJUpckWj7GC1kVqixDadteyGnI4aHyzd4kkH-pSbz
201
201
  xp/term/widgets/status_footer.py,sha256=bxrcqKzJ9V0aPSn_WwraVpJz7NxBUh3yIjA3fwv5nVA,3256
202
202
  xp/utils/__init__.py,sha256=_avMF_UOkfR3tNeDIPqQ5odmbq5raKkaq1rZ9Cn1CJs,332
203
203
  xp/utils/checksum.py,sha256=HDpiQxmdIedbCbZ4o_Box0i_Zig417BtCV_46ZyhiTk,1711
204
- xp/utils/dependencies.py,sha256=Z1vRSWr_dhmyNhD4dstJg-ZOlKVPPq_viGGNJ32GRNs,24584
204
+ xp/utils/dependencies.py,sha256=zrvWx28N0f28JwRDRyqaf5Q9eV_yLwh9xDw9mYBUXEQ,25379
205
205
  xp/utils/event_helper.py,sha256=W-A_xmoXlpWZBbJH6qdaN50o3-XrwFsDgvAGMJDiAgo,1001
206
206
  xp/utils/logging.py,sha256=rZDXwlBrYK8A6MPq5StsMNpgsRowzJXM6fvROPwJdGM,3750
207
207
  xp/utils/serialization.py,sha256=RWHHk86feaB4ZP7rjE4qOWK0900yg2joUBDkP76gfOY,4618
208
208
  xp/utils/state_machine.py,sha256=Oe2sLwCh9z_vr1tF6X0ZRGTeuckRQAGzmef7xc9CNdc,2413
209
209
  xp/utils/time_utils.py,sha256=dEyViDlAG9GWU-J3D_YVa-sGma6yiyyMTgN4h2x3PY4,3781
210
- conson_xp-1.39.0.dist-info/RECORD,,
210
+ conson_xp-1.41.0.dist-info/RECORD,,
xp/__init__.py CHANGED
@@ -3,7 +3,7 @@
3
3
  conson-xp package.
4
4
  """
5
5
 
6
- __version__ = "1.39.0"
6
+ __version__ = "1.41.0"
7
7
  __manufacturer__ = "salchichon"
8
8
  __model__ = "xp.cli"
9
9
  __serial__ = "2025.09.23.000"
@@ -25,6 +25,9 @@ from xp.services.conbus.msactiontable.msactiontable_list_service import (
25
25
  from xp.services.conbus.msactiontable.msactiontable_show_service import (
26
26
  MsActionTableShowService,
27
27
  )
28
+ from xp.services.conbus.msactiontable.msactiontable_upload_service import (
29
+ MsActionTableUploadService,
30
+ )
28
31
 
29
32
 
30
33
  @conbus_msactiontable.command("download", short_help="Download MSActionTable")
@@ -55,29 +58,30 @@ def conbus_download_msactiontable(
55
58
  click.echo(progress, nl=False)
56
59
 
57
60
  def on_finish(
58
- msaction_table: Union[
59
- Xp20MsActionTable, Xp24MsActionTable, Xp33MsActionTable, None
60
- ],
61
- msaction_table_short: str,
61
+ msaction_table: Union[Xp20MsActionTable, Xp24MsActionTable, Xp33MsActionTable],
62
+ msaction_table_short: list[str],
62
63
  ) -> None:
63
- """Handle successful completion of MS action table download.
64
+ """Handle successful completion of XP24 MS action table download.
64
65
 
65
66
  Args:
66
- msaction_table: Downloaded MS action table object or None if failed.
67
- msaction_table_short: Short version of MS action table object or None if failed.
68
-
69
- Raises:
70
- Abort: If action table download failed.
67
+ msaction_table: Downloaded XP MS action table object.
68
+ msaction_table_short: Short version of XP24 MS action table.
71
69
  """
72
70
  service.stop_reactor()
73
- if msaction_table is None:
74
- click.echo("Error: Failed to download MS action table")
75
- raise click.Abort()
71
+
72
+ # Format short representation based on module type
73
+ short_field_name = f"{xpmoduletype}_msaction_table"
74
+ # XP24 returns single-element list, XP20/XP33 return multi-line lists
75
+ short_value: Union[str, list[str]]
76
+ if len(msaction_table_short) == 1:
77
+ short_value = msaction_table_short[0]
78
+ else:
79
+ short_value = msaction_table_short
76
80
 
77
81
  output = {
78
82
  "serial_number": serial_number,
79
83
  "xpmoduletype": xpmoduletype,
80
- "msaction_table_short": msaction_table_short,
84
+ short_field_name: short_value,
81
85
  "msaction_table": msaction_table.model_dump(),
82
86
  }
83
87
  click.echo(json.dumps(output, indent=2, default=str))
@@ -93,6 +97,8 @@ def conbus_download_msactiontable(
93
97
  with service:
94
98
  service.on_progress.connect(on_progress)
95
99
  service.on_error.connect(on_error)
100
+
101
+ # Connect to the appropriate signal based on module type
96
102
  service.on_finish.connect(on_finish)
97
103
  service.start(
98
104
  serial_number=serial_number,
@@ -155,9 +161,37 @@ def conbus_show_msactiontable(ctx: Context, serial_number: str) -> None:
155
161
  Args:
156
162
  module: Dictionary containing module configuration.
157
163
  """
164
+ click.echo(f"\nModule: {module.name} ({module.serial_number})")
165
+
166
+ # Display short format if action table exists
167
+ if module.xp33_msaction_table:
168
+ click.echo("Short:")
169
+ for line in module.xp33_msaction_table:
170
+ click.echo(f" - {line}")
171
+ elif module.xp24_msaction_table:
172
+ click.echo("Short:")
173
+ for line in module.xp24_msaction_table:
174
+ click.echo(f" - {line}")
175
+ elif module.xp20_msaction_table:
176
+ click.echo("Short:")
177
+ for line in module.xp20_msaction_table:
178
+ click.echo(f" - {line}")
179
+
180
+ # Display full YAML format
181
+ click.echo("Full:")
158
182
  module_data = module.model_dump()
159
183
  module_data.pop("action_table", None)
160
- click.echo(json.dumps(module_data, indent=2, default=str))
184
+
185
+ # Show the action table in YAML format
186
+ if module.xp33_msaction_table:
187
+ yaml_dict = {"xp33_msaction_table": module_data}
188
+ click.echo(_format_yaml(yaml_dict, indent=2))
189
+ elif module.xp24_msaction_table:
190
+ yaml_dict = {"xp24_msaction_table": module_data}
191
+ click.echo(_format_yaml(yaml_dict, indent=2))
192
+ elif module.xp20_msaction_table:
193
+ yaml_dict = {"xp20_msaction_table": module_data}
194
+ click.echo(_format_yaml(yaml_dict, indent=2))
161
195
 
162
196
  def error_callback(error: str) -> None:
163
197
  """Handle errors during action table show.
@@ -173,3 +207,88 @@ def conbus_show_msactiontable(ctx: Context, serial_number: str) -> None:
173
207
  finish_callback=on_finish,
174
208
  error_callback=error_callback,
175
209
  )
210
+
211
+
212
+ @conbus_msactiontable.command("upload", short_help="Upload MSActionTable")
213
+ @click.argument("serial_number", type=SERIAL)
214
+ @click.argument("xpmoduletype", type=XP_MODULE_TYPE)
215
+ @click.pass_context
216
+ @connection_command()
217
+ def conbus_upload_msactiontable(
218
+ ctx: Context, serial_number: str, xpmoduletype: str
219
+ ) -> None:
220
+ """Upload MS action table from conson.yml to XP module.
221
+
222
+ Args:
223
+ ctx: Click context object.
224
+ serial_number: 10-digit module serial number.
225
+ xpmoduletype: XP module type.
226
+ """
227
+ service: MsActionTableUploadService = (
228
+ ctx.obj.get("container").get_container().resolve(MsActionTableUploadService)
229
+ )
230
+
231
+ def on_progress(progress: str) -> None:
232
+ """Handle progress updates during MS action table upload.
233
+
234
+ Args:
235
+ progress: Progress message string.
236
+ """
237
+ click.echo(progress, nl=False)
238
+
239
+ def on_finish(success: bool) -> None:
240
+ """Handle successful completion of MS action table upload.
241
+
242
+ Args:
243
+ success: Whether upload was successful.
244
+ """
245
+ service.stop_reactor()
246
+ if success:
247
+ click.echo("\nMsactiontable uploaded successfully")
248
+
249
+ def on_error(error: str) -> None:
250
+ """Handle errors during MS action table upload.
251
+
252
+ Args:
253
+ error: Error message string.
254
+ """
255
+ service.stop_reactor()
256
+ click.echo(f"\nError: {error}")
257
+
258
+ click.echo(f"Uploading msactiontable to {serial_number}...")
259
+
260
+ with service:
261
+ service.on_progress.connect(on_progress)
262
+ service.on_error.connect(on_error)
263
+ service.on_finish.connect(on_finish)
264
+ service.start(
265
+ serial_number=serial_number,
266
+ xpmoduletype=xpmoduletype,
267
+ )
268
+ service.start_reactor()
269
+
270
+
271
+ def _format_yaml(data: dict, indent: int = 0) -> str:
272
+ """Format a dictionary as YAML-like output.
273
+
274
+ Args:
275
+ data: Dictionary to format.
276
+ indent: Current indentation level.
277
+
278
+ Returns:
279
+ YAML-like formatted string.
280
+ """
281
+ lines: list[str] = []
282
+ for key, value in data.items():
283
+ if isinstance(value, dict):
284
+ lines.extend((f"{' ' * indent}{key}:", _format_yaml(value, indent + 2)))
285
+ elif isinstance(value, list):
286
+ lines.append(f"{' ' * indent}{key}:")
287
+ for item in value:
288
+ if isinstance(item, dict):
289
+ lines.append(_format_yaml(item, indent + 2))
290
+ else:
291
+ lines.append(f"{' ' * (indent + 2)}- {item}")
292
+ else:
293
+ lines.append(f"{' ' * indent}{key}: {value}")
294
+ return "\n".join(lines)
@@ -2,8 +2,6 @@
2
2
 
3
3
  from pydantic import BaseModel, Field
4
4
 
5
- from xp.models.actiontable.msactiontable import MsActionTable
6
-
7
5
 
8
6
  class InputChannel(BaseModel):
9
7
  """Configuration for a single input channel in XP20 action table.
@@ -25,7 +23,7 @@ class InputChannel(BaseModel):
25
23
  ta_function: bool = False
26
24
 
27
25
 
28
- class Xp20MsActionTable(MsActionTable):
26
+ class Xp20MsActionTable(BaseModel):
29
27
  """XP20 Action Table for managing 8 input channels.
30
28
 
31
29
  Contains configuration for 8 input channels (input1 through input8),
@@ -51,3 +49,95 @@ class Xp20MsActionTable(MsActionTable):
51
49
  input6: InputChannel = Field(default_factory=InputChannel)
52
50
  input7: InputChannel = Field(default_factory=InputChannel)
53
51
  input8: InputChannel = Field(default_factory=InputChannel)
52
+
53
+ def to_short_format(self) -> list[str]:
54
+ """Convert action table to short format string.
55
+
56
+ Returns:
57
+ Short format string with each channel on a separate line.
58
+ Example:
59
+ CH1 I:0 S:0 G:0 AND:00000000 SA:0 TA:0
60
+ CH2 I:0 S:0 G:0 AND:00000000 SA:0 TA:0
61
+ ...
62
+ """
63
+ lines = []
64
+ for i in range(1, 9):
65
+ channel = getattr(self, f"input{i}")
66
+ # Convert and_functions list to binary string
67
+ and_bits = "".join("1" if bit else "0" for bit in channel.and_functions)
68
+ line = (
69
+ f"CH{i} "
70
+ f"I:{1 if channel.invert else 0} "
71
+ f"S:{1 if channel.short_long else 0} "
72
+ f"G:{1 if channel.group_on_off else 0} "
73
+ f"AND:{and_bits} "
74
+ f"SA:{1 if channel.sa_function else 0} "
75
+ f"TA:{1 if channel.ta_function else 0}"
76
+ )
77
+ lines.append(line)
78
+ return lines
79
+
80
+ @classmethod
81
+ def from_short_format(cls, short_str: list[str]) -> "Xp20MsActionTable":
82
+ """Parse short format string into action table.
83
+
84
+ Args:
85
+ short_str: Short format string with 8 channel lines.
86
+
87
+ Returns:
88
+ Xp20MsActionTable instance.
89
+
90
+ Raises:
91
+ ValueError: If format is invalid.
92
+ """
93
+ import re
94
+
95
+ if len(short_str) != 8:
96
+ raise ValueError(f"Expected 8 channel lines, got {len(short_str)}")
97
+
98
+ pattern = re.compile(
99
+ r"^CH([1-8]) I:([01]) S:([01]) G:([01]) AND:([01]{8}) SA:([01]) TA:([01])$"
100
+ )
101
+
102
+ channels = {}
103
+ for line in short_str:
104
+ line = line.strip()
105
+ match = pattern.match(line)
106
+ if not match:
107
+ raise ValueError(f"Invalid channel format: {line}")
108
+
109
+ ch_num = int(match.group(1))
110
+ invert = match.group(2) == "1"
111
+ short_long = match.group(3) == "1"
112
+ group_on_off = match.group(4) == "1"
113
+ and_bits = match.group(5)
114
+ sa_function = match.group(6) == "1"
115
+ ta_function = match.group(7) == "1"
116
+
117
+ # Convert binary string to list of bools
118
+ and_functions = [bit == "1" for bit in and_bits]
119
+
120
+ channels[ch_num] = InputChannel(
121
+ invert=invert,
122
+ short_long=short_long,
123
+ group_on_off=group_on_off,
124
+ and_functions=and_functions,
125
+ sa_function=sa_function,
126
+ ta_function=ta_function,
127
+ )
128
+
129
+ # Verify all channels are present
130
+ for i in range(1, 9):
131
+ if i not in channels:
132
+ raise ValueError(f"Missing channel {i}")
133
+
134
+ return cls(
135
+ input1=channels[1],
136
+ input2=channels[2],
137
+ input3=channels[3],
138
+ input4=channels[4],
139
+ input5=channels[5],
140
+ input6=channels[6],
141
+ input7=channels[7],
142
+ input8=channels[8],
143
+ )
@@ -4,7 +4,6 @@ from typing import Any, ClassVar, Union
4
4
 
5
5
  from pydantic import BaseModel, ConfigDict, Field, field_validator
6
6
 
7
- from xp.models.actiontable.msactiontable import MsActionTable
8
7
  from xp.models.telegram.input_action_type import InputActionType
9
8
  from xp.models.telegram.timeparam_type import TimeParam
10
9
 
@@ -82,7 +81,7 @@ class InputAction(BaseModel):
82
81
  raise ValueError(f"Invalid type for TimeParam: {type(v)}")
83
82
 
84
83
 
85
- class Xp24MsActionTable(MsActionTable):
84
+ class Xp24MsActionTable(BaseModel):
86
85
  """XP24 Action Table for managing input actions and settings.
87
86
 
88
87
  Each input has an action type (TOGGLE, ON, LEVELSET, etc.)
@@ -159,7 +158,7 @@ class Xp24MsActionTable(MsActionTable):
159
158
  curtain34: bool = False # Curtain setting for inputs 3-4
160
159
  mutual_deadtime: int = MS300 # Master timing (MS300=12 or MS500=20)
161
160
 
162
- def to_short_format(self) -> str:
161
+ def to_short_format(self) -> list[str]:
163
162
  """Convert action table to short format string.
164
163
 
165
164
  Returns:
@@ -179,7 +178,7 @@ class Xp24MsActionTable(MsActionTable):
179
178
  param_value = action.param.value
180
179
  action_parts.append(f"{short_code}:{param_value}")
181
180
 
182
- result = f"XP24 {' '.join(action_parts)}"
181
+ result = " ".join(action_parts)
183
182
 
184
183
  # Add settings
185
184
  settings = (
@@ -191,10 +190,10 @@ class Xp24MsActionTable(MsActionTable):
191
190
  )
192
191
  result = f"{result} | {settings}"
193
192
 
194
- return result
193
+ return [result]
195
194
 
196
195
  @classmethod
197
- def from_short_format(cls, short_str: str) -> "Xp24MsActionTable":
196
+ def from_short_format(cls, short_str: list[str]) -> "Xp24MsActionTable":
198
197
  """Parse short format string into action table.
199
198
 
200
199
  Args:
@@ -207,20 +206,20 @@ class Xp24MsActionTable(MsActionTable):
207
206
  ValueError: If format is invalid.
208
207
  """
209
208
  # Split by pipe to separate actions from settings
210
- parts = short_str.split("|")
209
+ parts = short_str[0].split("|")
211
210
  action_part = parts[0].strip()
212
211
  settings_part = parts[1].strip()
213
212
 
214
213
  # Parse action part
215
214
  tokens = action_part.split()
216
- if len(tokens) != 5 or tokens[0] != "XP24":
215
+ if len(tokens) != 4:
217
216
  raise ValueError(
218
- f"Invalid short format: expected 'XP24 <a1> <a2> <a3> <a4>', got '{action_part}'"
217
+ f"Invalid short format: expected '<a1> <a2> <a3> <a4>', got '{action_part}'"
219
218
  )
220
219
 
221
220
  # Parse input actions
222
221
  input_actions = []
223
- for i, token in enumerate(tokens[1:5], 1):
222
+ for i, token in enumerate(tokens[0:4], 1):
224
223
  if ":" not in token:
225
224
  raise ValueError(f"Invalid action format at position {i}: '{token}'")
226
225
 
@@ -4,7 +4,6 @@ from typing import Union
4
4
 
5
5
  from pydantic import BaseModel, Field, field_validator
6
6
 
7
- from xp.models.actiontable.msactiontable import MsActionTable
8
7
  from xp.models.telegram.timeparam_type import TimeParam
9
8
 
10
9
 
@@ -70,7 +69,7 @@ class Xp33Scene(BaseModel):
70
69
  raise ValueError(f"Invalid type for TimeParam: {type(v)}")
71
70
 
72
71
 
73
- class Xp33MsActionTable(MsActionTable):
72
+ class Xp33MsActionTable(BaseModel):
74
73
  """XP33 Action Table for managing outputs and scenes.
75
74
 
76
75
  Attributes:
@@ -91,3 +90,271 @@ class Xp33MsActionTable(MsActionTable):
91
90
  scene2: Xp33Scene = Field(default_factory=Xp33Scene)
92
91
  scene3: Xp33Scene = Field(default_factory=Xp33Scene)
93
92
  scene4: Xp33Scene = Field(default_factory=Xp33Scene)
93
+
94
+ def to_short_format(self) -> list[str]:
95
+ """Convert action table to short format string.
96
+
97
+ Returns:
98
+ Short format string (multi-line format with OUT and SCENE lines).
99
+ """
100
+ lines = []
101
+
102
+ # Format outputs
103
+ outputs = [
104
+ (1, self.output1),
105
+ (2, self.output2),
106
+ (3, self.output3),
107
+ ]
108
+ for num, output in outputs:
109
+ lines.append(f"OUT{num} {self._format_output(output)}")
110
+
111
+ # Format scenes
112
+ scenes = [
113
+ (1, self.scene1),
114
+ (2, self.scene2),
115
+ (3, self.scene3),
116
+ (4, self.scene4),
117
+ ]
118
+ for num, scene in scenes:
119
+ lines.append(f"SCENE{num} {self._format_scene(scene)}")
120
+
121
+ return lines
122
+
123
+ @classmethod
124
+ def from_short_format(cls, short_str: list[str]) -> "Xp33MsActionTable":
125
+ """Parse short format string into action table.
126
+
127
+ Args:
128
+ short_str: Short format string (list of lines).
129
+
130
+ Returns:
131
+ Xp33MsActionTable instance.
132
+
133
+ Raises:
134
+ ValueError: If format is invalid.
135
+ """
136
+ # Parse outputs and scenes from lines
137
+ outputs = {}
138
+ scenes = {}
139
+
140
+ for line in short_str:
141
+ line = line.strip()
142
+ if not line:
143
+ continue
144
+
145
+ if line.startswith("OUT"):
146
+ # Parse output line: OUT1 MIN:0 MAX:100 SO:0 SF:0 LE:0
147
+ parts = line.split(None, 1)
148
+ if len(parts) != 2:
149
+ raise ValueError(f"Invalid output line format: '{line}'")
150
+
151
+ out_key = parts[0] # OUT1, OUT2, OUT3
152
+ if not out_key.startswith("OUT"):
153
+ raise ValueError(f"Expected OUT prefix, got: '{out_key}'")
154
+
155
+ try:
156
+ out_num = int(out_key[3:])
157
+ if out_num not in (1, 2, 3):
158
+ raise ValueError(
159
+ f"Invalid output number: {out_num}, expected 1-3"
160
+ )
161
+ except ValueError:
162
+ raise ValueError(f"Invalid output number in: '{out_key}'")
163
+
164
+ outputs[out_num] = cls._parse_output(parts[1])
165
+
166
+ elif line.startswith("SCENE"):
167
+ # Parse scene line: SCENE1 OUT1:0 OUT2:0 OUT3:0 T:NONE
168
+ parts = line.split(None, 1)
169
+ if len(parts) != 2:
170
+ raise ValueError(f"Invalid scene line format: '{line}'")
171
+
172
+ scene_key = parts[0] # SCENE1, SCENE2, etc.
173
+ if not scene_key.startswith("SCENE"):
174
+ raise ValueError(f"Expected SCENE prefix, got: '{scene_key}'")
175
+
176
+ try:
177
+ scene_num = int(scene_key[5:])
178
+ if scene_num not in (1, 2, 3, 4):
179
+ raise ValueError(
180
+ f"Invalid scene number: {scene_num}, expected 1-4"
181
+ )
182
+ except ValueError:
183
+ raise ValueError(f"Invalid scene number in: '{scene_key}'")
184
+
185
+ scenes[scene_num] = cls._parse_scene(parts[1])
186
+
187
+ # Validate we have all required outputs and scenes
188
+ for i in (1, 2, 3):
189
+ if i not in outputs:
190
+ raise ValueError(f"Missing output{i} configuration")
191
+
192
+ for i in (1, 2, 3, 4):
193
+ if i not in scenes:
194
+ raise ValueError(f"Missing scene{i} configuration")
195
+
196
+ return cls(
197
+ output1=outputs[1],
198
+ output2=outputs[2],
199
+ output3=outputs[3],
200
+ scene1=scenes[1],
201
+ scene2=scenes[2],
202
+ scene3=scenes[3],
203
+ scene4=scenes[4],
204
+ )
205
+
206
+ @staticmethod
207
+ def _format_output(output: Xp33Output) -> str:
208
+ """Format output configuration to short string.
209
+
210
+ Args:
211
+ output: Xp33Output instance.
212
+
213
+ Returns:
214
+ Short string like "MIN:10 MAX:90 SO:1 SF:0 LE:1".
215
+ """
216
+ return (
217
+ f"MIN:{output.min_level} "
218
+ f"MAX:{output.max_level} "
219
+ f"SO:{1 if output.scene_outputs else 0} "
220
+ f"SF:{1 if output.start_at_full else 0} "
221
+ f"LE:{1 if output.leading_edge else 0}"
222
+ )
223
+
224
+ @staticmethod
225
+ def _parse_output(output_str: str) -> Xp33Output:
226
+ """Parse output configuration from short string.
227
+
228
+ Args:
229
+ output_str: Short string like "MIN:10 MAX:90 SO:1 SF:0 LE:1".
230
+
231
+ Returns:
232
+ Xp33Output instance.
233
+
234
+ Raises:
235
+ ValueError: If format is invalid.
236
+ """
237
+ # Parse key:value pairs
238
+ parts = output_str.split()
239
+ params = {}
240
+
241
+ for part in parts:
242
+ if ":" not in part:
243
+ raise ValueError(f"Invalid output parameter format: '{part}'")
244
+
245
+ key, value = part.split(":", 1)
246
+ params[key] = value
247
+
248
+ # Validate required keys
249
+ required_keys = ["MIN", "MAX", "SO", "SF", "LE"]
250
+ for key in required_keys:
251
+ if key not in params:
252
+ raise ValueError(f"Missing required parameter: {key}")
253
+
254
+ # Parse and validate values
255
+ try:
256
+ min_level = int(params["MIN"])
257
+ max_level = int(params["MAX"])
258
+ scene_outputs = params["SO"] == "1"
259
+ start_at_full = params["SF"] == "1"
260
+ leading_edge = params["LE"] == "1"
261
+
262
+ # Validate ranges
263
+ if not (0 <= min_level <= 100):
264
+ raise ValueError(f"MIN level out of range (0-100): {min_level}")
265
+ if not (0 <= max_level <= 100):
266
+ raise ValueError(f"MAX level out of range (0-100): {max_level}")
267
+
268
+ return Xp33Output(
269
+ min_level=min_level,
270
+ max_level=max_level,
271
+ scene_outputs=scene_outputs,
272
+ start_at_full=start_at_full,
273
+ leading_edge=leading_edge,
274
+ )
275
+ except ValueError as e:
276
+ raise ValueError(f"Invalid output parameter value: {e}")
277
+
278
+ @staticmethod
279
+ def _format_scene(scene: Xp33Scene) -> str:
280
+ """Format scene configuration to short string.
281
+
282
+ Args:
283
+ scene: Xp33Scene instance.
284
+
285
+ Returns:
286
+ Short string like "OUT1:50 OUT2:60 OUT3:70 T:T5SEC".
287
+ """
288
+ time_str = scene.time.name
289
+ return (
290
+ f"OUT1:{scene.output1_level} "
291
+ f"OUT2:{scene.output2_level} "
292
+ f"OUT3:{scene.output3_level} "
293
+ f"T:{time_str}"
294
+ )
295
+
296
+ @staticmethod
297
+ def _parse_scene(scene_str: str) -> Xp33Scene:
298
+ """Parse scene configuration from short string.
299
+
300
+ Args:
301
+ scene_str: Short string like "OUT1:50 OUT2:60 OUT3:70 T:T5SEC".
302
+
303
+ Returns:
304
+ Xp33Scene instance.
305
+
306
+ Raises:
307
+ ValueError: If format is invalid.
308
+ """
309
+ # Parse key:value pairs
310
+ parts = scene_str.split()
311
+ params = {}
312
+
313
+ for part in parts:
314
+ if ":" not in part:
315
+ raise ValueError(f"Invalid scene parameter format: '{part}'")
316
+
317
+ key, value = part.split(":", 1)
318
+ params[key] = value
319
+
320
+ # Validate required keys
321
+ required_keys = ["OUT1", "OUT2", "OUT3", "T"]
322
+ for key in required_keys:
323
+ if key not in params:
324
+ raise ValueError(f"Missing required parameter: {key}")
325
+
326
+ # Parse and validate values
327
+ try:
328
+ output1_level = int(params["OUT1"])
329
+ output2_level = int(params["OUT2"])
330
+ output3_level = int(params["OUT3"])
331
+
332
+ # Validate ranges
333
+ if not (0 <= output1_level <= 100):
334
+ raise ValueError(f"OUT1 level out of range (0-100): {output1_level}")
335
+ if not (0 <= output2_level <= 100):
336
+ raise ValueError(f"OUT2 level out of range (0-100): {output2_level}")
337
+ if not (0 <= output3_level <= 100):
338
+ raise ValueError(f"OUT3 level out of range (0-100): {output3_level}")
339
+
340
+ # Parse time parameter - support both name and numeric value
341
+ time_str = params["T"]
342
+ try:
343
+ # Try parsing as enum name first
344
+ time_param = TimeParam[time_str]
345
+ except KeyError:
346
+ # Try parsing as numeric value
347
+ try:
348
+ time_value = int(time_str)
349
+ time_param = TimeParam(time_value)
350
+ except (ValueError, KeyError):
351
+ raise ValueError(f"Invalid TimeParam: '{time_str}'")
352
+
353
+ return Xp33Scene(
354
+ output1_level=output1_level,
355
+ output2_level=output2_level,
356
+ output3_level=output3_level,
357
+ time=time_param,
358
+ )
359
+ except ValueError as e:
360
+ raise ValueError(f"Invalid scene parameter value: {e}")
@@ -23,7 +23,9 @@ class ConsonModuleConfig(BaseModel):
23
23
  sw_version: Optional software version.
24
24
  hw_version: Optional hardware version.
25
25
  action_table: Optional action table configuration.
26
- msaction_table: Optional ms action table configuration.
26
+ xp20_msaction_table: Optional xp20 ms action table configuration.
27
+ xp24_msaction_table: Optional xp24 ms action table configuration.
28
+ xp33_msaction_table: Optional xp33 ms action table configuration.
27
29
  auto_report_status: Optional auto report status.
28
30
  """
29
31
 
@@ -40,7 +42,9 @@ class ConsonModuleConfig(BaseModel):
40
42
  hw_version: Optional[str] = None
41
43
  auto_report_status: Optional[str] = None
42
44
  action_table: Optional[List[str]] = None
43
- msaction_table: Optional[str] = None
45
+ xp20_msaction_table: Optional[List[str]] = None
46
+ xp24_msaction_table: Optional[List[str]] = None
47
+ xp33_msaction_table: Optional[List[str]] = None
44
48
 
45
49
 
46
50
  class ConsonModuleListConfig(BaseModel):
@@ -16,7 +16,7 @@ class Xp20MsActionTableSerializer:
16
16
  """Handles serialization/deserialization of XP20 action tables to/from telegrams."""
17
17
 
18
18
  @staticmethod
19
- def format_decoded_output(action_table: Xp20MsActionTable) -> str:
19
+ def format_decoded_output(action_table: Xp20MsActionTable) -> list[str]:
20
20
  """Serialize XP20 action table to humane compact readable format.
21
21
 
22
22
  Args:
@@ -25,7 +25,7 @@ class Xp20MsActionTableSerializer:
25
25
  Returns:
26
26
  Human-readable string describing XP20 action table
27
27
  """
28
- return ""
28
+ return action_table.to_short_format()
29
29
 
30
30
  @staticmethod
31
31
  def to_data(action_table: Xp20MsActionTable) -> str:
@@ -10,7 +10,7 @@ class Xp24MsActionTableSerializer:
10
10
  """Handles serialization/deserialization of XP24 action tables to/from telegrams."""
11
11
 
12
12
  @staticmethod
13
- def format_decoded_output(action_table: Xp24MsActionTable) -> str:
13
+ def format_decoded_output(action_table: Xp24MsActionTable) -> list[str]:
14
14
  """Serialize XP24 action table to humane compact readable format.
15
15
 
16
16
  Args:
@@ -13,7 +13,7 @@ class Xp33MsActionTableSerializer:
13
13
  """Handles serialization/deserialization of XP33 action tables to/from telegrams."""
14
14
 
15
15
  @staticmethod
16
- def format_decoded_output(action_table: Xp33MsActionTable) -> str:
16
+ def format_decoded_output(action_table: Xp33MsActionTable) -> list[str]:
17
17
  """Serialize XP33 action table to humane compact readable format.
18
18
 
19
19
  Args:
@@ -22,7 +22,7 @@ class Xp33MsActionTableSerializer:
22
22
  Returns:
23
23
  Human-readable string describing XP33 action table
24
24
  """
25
- return ""
25
+ return action_table.to_short_format()
26
26
 
27
27
  @staticmethod
28
28
  def _percentage_to_byte(percentage: int) -> int:
@@ -5,7 +5,6 @@ from typing import Any, Optional, Union
5
5
 
6
6
  from psygnal import Signal
7
7
 
8
- from xp.models.actiontable.msactiontable import MsActionTable
9
8
  from xp.models.actiontable.msactiontable_xp20 import Xp20MsActionTable
10
9
  from xp.models.actiontable.msactiontable_xp24 import Xp24MsActionTable
11
10
  from xp.models.actiontable.msactiontable_xp33 import Xp33MsActionTable
@@ -42,12 +41,12 @@ class MsActionTableDownloadService:
42
41
  conbus_protocol: Protocol instance for Conbus communication.
43
42
  on_progress: Signal emitted for progress updates (str).
44
43
  on_error: Signal emitted for errors (str).
45
- on_finish: Signal emitted when download completes (MsActionTable or None).
44
+ on_finish: Signal emitted when XP download completes (Xp20MsActionTable, str).
46
45
  """
47
46
 
48
47
  on_progress: Signal = Signal(str)
49
48
  on_error: Signal = Signal(str)
50
- on_finish: Signal = Signal(MsActionTable, str) # Union type for Xp20/24/33 or None
49
+ on_finish: Signal = Signal(object, list[str])
51
50
 
52
51
  def __init__(
53
52
  self,
@@ -188,7 +187,7 @@ class MsActionTableDownloadService:
188
187
  def succeed(
189
188
  self,
190
189
  msactiontable: Union[Xp20MsActionTable, Xp24MsActionTable, Xp33MsActionTable],
191
- msactiontable_short: str,
190
+ msactiontable_short: list[str],
192
191
  ) -> None:
193
192
  """Handle succeed connection event.
194
193
 
@@ -196,6 +195,7 @@ class MsActionTableDownloadService:
196
195
  msactiontable: result.
197
196
  msactiontable_short: result in short form.
198
197
  """
198
+ # Emit to the appropriate signal based on module type
199
199
  self.on_finish.emit(msactiontable, msactiontable_short)
200
200
 
201
201
  def start(
@@ -72,7 +72,15 @@ class MsActionTableListService:
72
72
  {
73
73
  "serial_number": module.serial_number,
74
74
  "module_type": module.module_type,
75
- "msaction_table": 1 if module.msaction_table else 0,
75
+ "msaction_table": (
76
+ 1
77
+ if (
78
+ module.xp20_msaction_table
79
+ or module.xp24_msaction_table
80
+ or module.xp33_msaction_table
81
+ )
82
+ else 0
83
+ ),
76
84
  }
77
85
  for module in config.root
78
86
  ]
@@ -0,0 +1,324 @@
1
+ """Service for uploading MS action tables via Conbus protocol."""
2
+
3
+ import logging
4
+ from typing import Any, Optional, Union
5
+
6
+ from psygnal import Signal
7
+
8
+ from xp.models.config.conson_module_config import ConsonModuleListConfig
9
+ from xp.models.protocol.conbus_protocol import TelegramReceivedEvent
10
+ from xp.models.telegram.system_function import SystemFunction
11
+ from xp.models.telegram.telegram_type import TelegramType
12
+ from xp.services.actiontable.msactiontable_xp20_serializer import (
13
+ Xp20MsActionTableSerializer,
14
+ )
15
+ from xp.services.actiontable.msactiontable_xp24_serializer import (
16
+ Xp24MsActionTableSerializer,
17
+ )
18
+ from xp.services.actiontable.msactiontable_xp33_serializer import (
19
+ Xp33MsActionTableSerializer,
20
+ )
21
+ from xp.services.protocol.conbus_event_protocol import ConbusEventProtocol
22
+ from xp.services.telegram.telegram_service import TelegramService
23
+
24
+
25
+ class MsActionTableUploadError(Exception):
26
+ """Raised when MS action table upload operations fail."""
27
+
28
+ pass
29
+
30
+
31
+ class MsActionTableUploadService:
32
+ """TCP client service for uploading MS action tables to Conbus modules.
33
+
34
+ Manages TCP socket connections, handles telegram generation and transmission,
35
+ and processes server responses for MS action table uploads.
36
+
37
+ Attributes:
38
+ on_progress: Signal emitted with telegram frame when progress is made.
39
+ on_error: Signal emitted with error message string when an error occurs.
40
+ on_finish: Signal emitted with bool (True on success) when upload completes.
41
+ """
42
+
43
+ on_progress: Signal = Signal(str)
44
+ on_error: Signal = Signal(str)
45
+ on_finish: Signal = Signal(bool) # True on success
46
+
47
+ def __init__(
48
+ self,
49
+ conbus_protocol: ConbusEventProtocol,
50
+ xp20ms_serializer: Xp20MsActionTableSerializer,
51
+ xp24ms_serializer: Xp24MsActionTableSerializer,
52
+ xp33ms_serializer: Xp33MsActionTableSerializer,
53
+ telegram_service: TelegramService,
54
+ conson_config: ConsonModuleListConfig,
55
+ ) -> None:
56
+ """Initialize the MS action table upload service.
57
+
58
+ Args:
59
+ conbus_protocol: ConbusEventProtocol for communication.
60
+ xp20ms_serializer: XP20 MS action table serializer.
61
+ xp24ms_serializer: XP24 MS action table serializer.
62
+ xp33ms_serializer: XP33 MS action table serializer.
63
+ telegram_service: Telegram service for parsing.
64
+ conson_config: Conson module list configuration.
65
+ """
66
+ self.conbus_protocol = conbus_protocol
67
+ self.xp20ms_serializer = xp20ms_serializer
68
+ self.xp24ms_serializer = xp24ms_serializer
69
+ self.xp33ms_serializer = xp33ms_serializer
70
+ self.serializer: Union[
71
+ Xp20MsActionTableSerializer,
72
+ Xp24MsActionTableSerializer,
73
+ Xp33MsActionTableSerializer,
74
+ ] = xp20ms_serializer
75
+ self.telegram_service = telegram_service
76
+ self.conson_config = conson_config
77
+ self.serial_number: str = ""
78
+ self.xpmoduletype: str = ""
79
+
80
+ # Upload state
81
+ self.upload_data: str = ""
82
+ self.upload_initiated: bool = False
83
+
84
+ # Set up logging
85
+ self.logger = logging.getLogger(__name__)
86
+
87
+ # Connect protocol signals
88
+ self.conbus_protocol.on_connection_made.connect(self.connection_made)
89
+ self.conbus_protocol.on_telegram_sent.connect(self.telegram_sent)
90
+ self.conbus_protocol.on_telegram_received.connect(self.telegram_received)
91
+ self.conbus_protocol.on_timeout.connect(self.timeout)
92
+ self.conbus_protocol.on_failed.connect(self.failed)
93
+
94
+ def connection_made(self) -> None:
95
+ """Handle connection established event."""
96
+ self.logger.debug(
97
+ "Connection established, sending upload msactiontable telegram"
98
+ )
99
+ self.conbus_protocol.send_telegram(
100
+ telegram_type=TelegramType.SYSTEM,
101
+ serial_number=self.serial_number,
102
+ system_function=SystemFunction.UPLOAD_MSACTIONTABLE,
103
+ data_value="00",
104
+ )
105
+
106
+ def telegram_sent(self, telegram_sent: str) -> None:
107
+ """Handle telegram sent event.
108
+
109
+ Args:
110
+ telegram_sent: The telegram that was sent.
111
+ """
112
+ self.logger.debug(f"Telegram sent: {telegram_sent}")
113
+
114
+ def telegram_received(self, telegram_received: TelegramReceivedEvent) -> None:
115
+ """Handle telegram received event.
116
+
117
+ Args:
118
+ telegram_received: The telegram received event.
119
+ """
120
+ self.logger.debug(f"Telegram received: {telegram_received}")
121
+ if (
122
+ not telegram_received.checksum_valid
123
+ or telegram_received.telegram_type != TelegramType.REPLY.value
124
+ or telegram_received.serial_number != self.serial_number
125
+ ):
126
+ self.logger.debug("Not a reply response")
127
+ return
128
+
129
+ reply_telegram = self.telegram_service.parse_reply_telegram(
130
+ telegram_received.frame
131
+ )
132
+
133
+ self._handle_upload_response(reply_telegram)
134
+
135
+ def _handle_upload_response(self, reply_telegram: Any) -> None:
136
+ """Handle telegram responses during upload.
137
+
138
+ Args:
139
+ reply_telegram: Parsed reply telegram.
140
+ """
141
+ if reply_telegram.system_function == SystemFunction.ACK:
142
+ self.logger.debug("Received ACK for upload")
143
+
144
+ if not self.upload_initiated:
145
+ # First ACK - send data chunk
146
+ self.logger.debug("Sending msactiontable data")
147
+ self.conbus_protocol.send_telegram(
148
+ telegram_type=TelegramType.SYSTEM,
149
+ serial_number=self.serial_number,
150
+ system_function=SystemFunction.MSACTIONTABLE,
151
+ data_value=self.upload_data,
152
+ )
153
+ self.upload_initiated = True
154
+ self.on_progress.emit(".")
155
+ else:
156
+ # Second ACK - send EOF
157
+ self.logger.debug("Data sent, sending EOF")
158
+ self.conbus_protocol.send_telegram(
159
+ telegram_type=TelegramType.SYSTEM,
160
+ serial_number=self.serial_number,
161
+ system_function=SystemFunction.EOF,
162
+ data_value="00",
163
+ )
164
+ self.on_finish.emit(True)
165
+ elif reply_telegram.system_function == SystemFunction.NAK:
166
+ self.logger.debug("Received NAK during upload")
167
+ self.failed("Upload failed: NAK received")
168
+ else:
169
+ self.logger.debug(f"Unexpected response during upload: {reply_telegram}")
170
+
171
+ def timeout(self) -> None:
172
+ """Handle timeout event."""
173
+ self.logger.debug("Upload timeout")
174
+ self.failed("Upload timeout")
175
+
176
+ def failed(self, message: str) -> None:
177
+ """Handle failed connection event.
178
+
179
+ Args:
180
+ message: Failure message.
181
+ """
182
+ self.logger.debug(f"Failed: {message}")
183
+ self.on_error.emit(message)
184
+
185
+ def start(
186
+ self,
187
+ serial_number: str,
188
+ xpmoduletype: str,
189
+ timeout_seconds: Optional[float] = None,
190
+ ) -> None:
191
+ """Upload MS action table to module.
192
+
193
+ Uploads the MS action table configuration to the specified module.
194
+
195
+ Args:
196
+ serial_number: Module serial number.
197
+ xpmoduletype: XP module type (xp20, xp24, xp33).
198
+ timeout_seconds: Optional timeout in seconds.
199
+
200
+ Raises:
201
+ MsActionTableUploadError: If configuration or validation errors occur.
202
+ """
203
+ self.logger.info("Starting msactiontable upload")
204
+ self.serial_number = serial_number
205
+ self.xpmoduletype = xpmoduletype
206
+
207
+ # Select serializer based on module type
208
+ if xpmoduletype == "xp20":
209
+ self.serializer = self.xp20ms_serializer
210
+ config_field = "xp20_msaction_table"
211
+ elif xpmoduletype == "xp24":
212
+ self.serializer = self.xp24ms_serializer
213
+ config_field = "xp24_msaction_table"
214
+ elif xpmoduletype == "xp33":
215
+ self.serializer = self.xp33ms_serializer
216
+ config_field = "xp33_msaction_table"
217
+ else:
218
+ raise MsActionTableUploadError(f"Unsupported module type: {xpmoduletype}")
219
+
220
+ if timeout_seconds:
221
+ self.conbus_protocol.timeout_seconds = timeout_seconds
222
+
223
+ # Find module
224
+ module = self.conson_config.find_module(serial_number)
225
+ if not module:
226
+ self.failed(f"Module {serial_number} not found in conson.yml")
227
+ return
228
+
229
+ # Validate module type matches
230
+ if module.module_type.lower() != xpmoduletype.lower():
231
+ self.failed(
232
+ f"Module type mismatch: module has type {module.module_type}, "
233
+ f"but {xpmoduletype} was specified"
234
+ )
235
+ return
236
+
237
+ # Get msactiontable config for the module type
238
+ msactiontable_config = getattr(module, config_field, None)
239
+
240
+ if not msactiontable_config:
241
+ self.failed(
242
+ f"Module {serial_number} does not have {config_field} configured in conson.yml"
243
+ )
244
+ return
245
+
246
+ if not isinstance(msactiontable_config, list) or len(msactiontable_config) == 0:
247
+ self.failed(
248
+ f"Module {serial_number} has empty {config_field} list in conson.yml"
249
+ )
250
+ return
251
+
252
+ # Parse MS action table from short format (first element)
253
+ try:
254
+ short_format = msactiontable_config
255
+ msactiontable: Union[
256
+ "Xp20MsActionTable", "Xp24MsActionTable", "Xp33MsActionTable"
257
+ ]
258
+ if xpmoduletype == "xp20":
259
+ from xp.models.actiontable.msactiontable_xp20 import Xp20MsActionTable
260
+
261
+ msactiontable = Xp20MsActionTable.from_short_format(short_format)
262
+ elif xpmoduletype == "xp24":
263
+ from xp.models.actiontable.msactiontable_xp24 import Xp24MsActionTable
264
+
265
+ msactiontable = Xp24MsActionTable.from_short_format(short_format)
266
+ elif xpmoduletype == "xp33":
267
+ from xp.models.actiontable.msactiontable_xp33 import Xp33MsActionTable
268
+
269
+ msactiontable = Xp33MsActionTable.from_short_format(short_format)
270
+ except (ValueError, AttributeError) as e:
271
+ self.logger.error(f"Invalid msactiontable format: {e}")
272
+ self.failed(f"Invalid msactiontable format: {e}")
273
+ return
274
+
275
+ # Serialize to telegram data (68 characters: AAAA + 64 data chars)
276
+ self.upload_data = self.serializer.to_data(msactiontable) # type: ignore[arg-type]
277
+
278
+ self.logger.debug(
279
+ f"Upload data encoded: {len(self.upload_data)} chars (single chunk)"
280
+ )
281
+
282
+ def set_timeout(self, timeout_seconds: float) -> None:
283
+ """Set operation timeout.
284
+
285
+ Args:
286
+ timeout_seconds: Timeout in seconds.
287
+ """
288
+ self.conbus_protocol.timeout_seconds = timeout_seconds
289
+
290
+ def start_reactor(self) -> None:
291
+ """Start the reactor."""
292
+ self.conbus_protocol.start_reactor()
293
+
294
+ def stop_reactor(self) -> None:
295
+ """Stop the reactor."""
296
+ self.conbus_protocol.stop_reactor()
297
+
298
+ def __enter__(self) -> "MsActionTableUploadService":
299
+ """Enter context manager - reset state for singleton reuse.
300
+
301
+ Returns:
302
+ Self for context manager protocol.
303
+ """
304
+ # Reset state
305
+ self.upload_data = ""
306
+ self.upload_initiated = False
307
+ self.serial_number = ""
308
+ self.xpmoduletype = ""
309
+ return self
310
+
311
+ def __exit__(self, _exc_type: Any, _exc_val: Any, _exc_tb: Any) -> None:
312
+ """Exit context manager - cleanup signals and reactor."""
313
+ # Disconnect protocol signals
314
+ self.conbus_protocol.on_connection_made.disconnect(self.connection_made)
315
+ self.conbus_protocol.on_telegram_sent.disconnect(self.telegram_sent)
316
+ self.conbus_protocol.on_telegram_received.disconnect(self.telegram_received)
317
+ self.conbus_protocol.on_timeout.disconnect(self.timeout)
318
+ self.conbus_protocol.on_failed.disconnect(self.failed)
319
+ # Disconnect service signals
320
+ self.on_progress.disconnect()
321
+ self.on_error.disconnect()
322
+ self.on_finish.disconnect()
323
+ # Stop reactor
324
+ self.stop_reactor()
xp/utils/dependencies.py CHANGED
@@ -60,6 +60,9 @@ from xp.services.conbus.msactiontable.msactiontable_list_service import (
60
60
  from xp.services.conbus.msactiontable.msactiontable_show_service import (
61
61
  MsActionTableShowService,
62
62
  )
63
+ from xp.services.conbus.msactiontable.msactiontable_upload_service import (
64
+ MsActionTableUploadService,
65
+ )
63
66
  from xp.services.conbus.write_config_service import WriteConfigService
64
67
  from xp.services.homekit.homekit_cache_service import HomeKitCacheService
65
68
  from xp.services.homekit.homekit_conbus_service import HomeKitConbusService
@@ -381,6 +384,19 @@ class ServiceContainer:
381
384
  scope=punq.Scope.singleton,
382
385
  )
383
386
 
387
+ self.container.register(
388
+ MsActionTableUploadService,
389
+ factory=lambda: MsActionTableUploadService(
390
+ conbus_protocol=self.container.resolve(ConbusEventProtocol),
391
+ xp20ms_serializer=self.container.resolve(Xp20MsActionTableSerializer),
392
+ xp24ms_serializer=self.container.resolve(Xp24MsActionTableSerializer),
393
+ xp33ms_serializer=self.container.resolve(Xp33MsActionTableSerializer),
394
+ telegram_service=self.container.resolve(TelegramService),
395
+ conson_config=self.container.resolve(ConsonModuleListConfig),
396
+ ),
397
+ scope=punq.Scope.singleton,
398
+ )
399
+
384
400
  self.container.register(
385
401
  ConbusCustomService,
386
402
  factory=lambda: ConbusCustomService(
@@ -1,9 +0,0 @@
1
- """Base class for MS Action Table models."""
2
-
3
- from pydantic import BaseModel
4
-
5
-
6
- class MsActionTable(BaseModel):
7
- """Base class for all MS Action Table types (XP20, XP24, XP33)."""
8
-
9
- pass