conson-xp 1.40.0__py3-none-any.whl → 1.43.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.40.0
3
+ Version: 1.43.0
4
4
  Summary: XP Protocol Communication Tools
5
5
  Author-Email: ldvchosal <ldvchosal@github.com>
6
6
  License: MIT License
@@ -49,6 +49,7 @@ Requires-Dist: twisted>=25.5.0
49
49
  Requires-Dist: bubus>=1.5.6
50
50
  Requires-Dist: psygnal>=0.15.0
51
51
  Requires-Dist: textual>=1.0.0
52
+ Requires-Dist: python-statemachine>=2.5.0
52
53
  Description-Content-Type: text/markdown
53
54
 
54
55
  # 🔌 XP Protocol Communication Tool
@@ -376,6 +377,7 @@ xp conbus msactiontable
376
377
  xp conbus msactiontable download
377
378
  xp conbus msactiontable list
378
379
  xp conbus msactiontable show
380
+ xp conbus msactiontable upload
379
381
 
380
382
 
381
383
  xp conbus output
@@ -1,14 +1,14 @@
1
- conson_xp-1.40.0.dist-info/METADATA,sha256=FxnffMc3lu49w-4RKgg8x5f8wH3rl7rdjqCvr-BqhLI,11330
2
- conson_xp-1.40.0.dist-info/WHEEL,sha256=tsUv_t7BDeJeRHaSrczbGeuK-TtDpGsWi_JfpzD255I,90
3
- conson_xp-1.40.0.dist-info/entry_points.txt,sha256=1OcdIcDM1hz3ljCXgybaPUh1IOFEwkaTgLIW9u9zGeg,50
4
- conson_xp-1.40.0.dist-info/licenses/LICENSE,sha256=rxj6woMM-r3YCyGq_UHFtbh7kHTAJgrccH6O-33IDE4,1419
5
- xp/__init__.py,sha256=J-7i4_8TyVn5z8NeO8OcuNsvrQlrsttTuKcPk6MvDMo,181
1
+ conson_xp-1.43.0.dist-info/METADATA,sha256=UEKUuurqgD2CZtaFX4SC-svZfzBSrOhJ7T1Dqg-HTK4,11403
2
+ conson_xp-1.43.0.dist-info/WHEEL,sha256=tsUv_t7BDeJeRHaSrczbGeuK-TtDpGsWi_JfpzD255I,90
3
+ conson_xp-1.43.0.dist-info/entry_points.txt,sha256=1OcdIcDM1hz3ljCXgybaPUh1IOFEwkaTgLIW9u9zGeg,50
4
+ conson_xp-1.43.0.dist-info/licenses/LICENSE,sha256=rxj6woMM-r3YCyGq_UHFtbh7kHTAJgrccH6O-33IDE4,1419
5
+ xp/__init__.py,sha256=af_iRTXOsyO7zIzqM-7iNsCptIKD9yYi2OXdX5aD458,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
9
9
  xp/cli/commands/conbus/__init__.py,sha256=HYaX2__XxwD3Xaw4CzflvL8CwoUa4yR6wOyH8wwyofM,535
10
10
  xp/cli/commands/conbus/conbus.py,sha256=1j9-Nwnf6nqD4ztBZnZEbJOTlkYo4D-Ks2J7BGNF3MU,3575
11
- xp/cli/commands/conbus/conbus_actiontable_commands.py,sha256=fIEe45iMWZ3Dbr0STzO8qbtWHR-R8qpGa_sLNeBm_og,7471
11
+ xp/cli/commands/conbus/conbus_actiontable_commands.py,sha256=yLVSADwei7tX6JFa25TCpTxyMHyNJfWlDFqpR8AGRmo,7657
12
12
  xp/cli/commands/conbus/conbus_autoreport_commands.py,sha256=TgXFVpoyFNiEatL6r78IghzyF0R2S3cgTrGZaiJPjwA,3934
13
13
  xp/cli/commands/conbus/conbus_blink_commands.py,sha256=HRn4Lr_BO7_WynsaUnO_hKezOi3MVhkPYEOnh0rMMlg,5324
14
14
  xp/cli/commands/conbus/conbus_config_commands.py,sha256=BugIbgNX6_s4MySGvI6tWZkwguciajHUX2Xz8XBux7k,716
@@ -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=9JNZRemAgUUaNkKPzDUgIY5jnv-hCK8-bM5XjLYUIjc,7808
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
@@ -59,7 +59,7 @@ 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
61
  xp/models/actiontable/msactiontable_xp20.py,sha256=zc9akPpuaW-pBk1vD9xn0JDWm_c2fiJYDuuk-DbvbGQ,5006
62
- xp/models/actiontable/msactiontable_xp24.py,sha256=ePuw5sAwmnUWZoti_uadvG1E-d7XGmucPm3WW2dQP0c,9415
62
+ xp/models/actiontable/msactiontable_xp24.py,sha256=tpgYvlQwxhjo70Ucsg_rB9ox7-jlG2b-GBj-UXwP2Ic,9377
63
63
  xp/models/actiontable/msactiontable_xp33.py,sha256=p_0HrvUmnqEEUlle7n0vpspGXFPrO5pXZeVF7n9K19g,11781
64
64
  xp/models/conbus/__init__.py,sha256=VIusMWQdBtlwDgj7oSj06wQkklihTp4oWFShvP_JUgA,35
65
65
  xp/models/conbus/conbus.py,sha256=mZQzKPfrdttT-qUnYUSyrEYyc_eHs8z301E5ejeiyvk,2689
@@ -121,7 +121,7 @@ xp/services/actiontable/msactiontable_xp24_serializer.py,sha256=ku84HCCjYDM9XpRK
121
121
  xp/services/actiontable/msactiontable_xp33_serializer.py,sha256=MFjEwSsIa8l3RJt-ig828E6kyiFYYXMHFKe4Q0A3NvA,8781
122
122
  xp/services/conbus/__init__.py,sha256=Hi35sMKu9o6LpYoi2tQDaQoMb8M5sOt_-LUTxxaCU_0,28
123
123
  xp/services/conbus/actiontable/__init__.py,sha256=oD6vRk_Ye-eZ9s_hldAgtRJFu4mfAnODqpkJUGHHszk,40
124
- xp/services/conbus/actiontable/actiontable_download_service.py,sha256=C6cjNRRsl7_jjn94I6ycCDvoqIpivNv0cMVkR-CQBXk,7608
124
+ xp/services/conbus/actiontable/actiontable_download_service.py,sha256=OD58pJyk-ij26a_ISc4OoK4X7rVj7rKwanV2wYVhsQY,18670
125
125
  xp/services/conbus/actiontable/actiontable_list_service.py,sha256=BYNpxdhdVvn5Z3XXvUYyiGGZ3fOoKR1F0boYDfAQ80c,2894
126
126
  xp/services/conbus/actiontable/actiontable_show_service.py,sha256=Z11bVYstnIhahFKTL8WqgTmK8rMK6kvismdy7679ojY,3013
127
127
  xp/services/conbus/actiontable/actiontable_upload_service.py,sha256=HTWJqJTIDFsp5v0SBx1Wt5gUeso6w03N6bKbtsbHtJY,9705
@@ -142,6 +142,7 @@ xp/services/conbus/msactiontable/__init__.py,sha256=rDYzumPSfcTjDADHxjE7bXQoeWtZ
142
142
  xp/services/conbus/msactiontable/msactiontable_download_service.py,sha256=YAQeUAO04VkRTEvWwXBD_b6tdVjDYk55K4pZd7lxfE8,10049
143
143
  xp/services/conbus/msactiontable/msactiontable_list_service.py,sha256=bTqUI2xs3Ie0MeZ_PYm-Bgx9A-Eewlpc8Tv6jhi1_kA,3127
144
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
145
146
  xp/services/conbus/write_config_service.py,sha256=PQsN7rtTKHpwtAG8moLksUfRVqqE_0sxdE37meR1ZQ0,8935
146
147
  xp/services/homekit/__init__.py,sha256=xAMKmln_AmEFdOOJGKWYi96seRlKDQpKx3-hm7XbdIo,36
147
148
  xp/services/homekit/homekit_cache_service.py,sha256=NdijyH5_iyhsTHBb-OyT8Y2xnNDj8F5MP8neoVQ26hY,11010
@@ -160,7 +161,7 @@ xp/services/homekit/homekit_service.py,sha256=0lW-hg40ETco3gDBEYkR_sX-UIYsLSKCD4
160
161
  xp/services/log_file_service.py,sha256=fvPcZQj8XOKUA-4ep5R8n0PelvwvRlTLlVxvIWM5KR4,10506
161
162
  xp/services/module_type_service.py,sha256=xWhr1EAZMykL5uNWHWdpa5T8yNruGKH43XRTOS8GwZg,7477
162
163
  xp/services/protocol/__init__.py,sha256=qRufBmqRKGzpuzZ5bxBbmwf510TT00Ke8s5HcWGnqRY,818
163
- xp/services/protocol/conbus_event_protocol.py,sha256=PpSr5sWcibN_xb2iMWAtWc0dTkUkFZelgYuPiejutkE,15155
164
+ xp/services/protocol/conbus_event_protocol.py,sha256=XjdVwy7P87Utu_UqK7upfugrA5fYG2iWBwC4go9KFLY,15498
164
165
  xp/services/protocol/conbus_protocol.py,sha256=JO7yLkD_ohPT0ETjnAIx4CGpZyobf4LdbuozM_43btE,10276
165
166
  xp/services/protocol/protocol_factory.py,sha256=PmjN9AtW9sxNo3voqUiNgQA-pTvX1RW4XXFlHKfFr5Q,2470
166
167
  xp/services/protocol/telegram_protocol.py,sha256=Ki5DrXsKxiaqLcdP9WWUuhUI7cPu2DfwyZkh-Gv9Lb8,9496
@@ -170,7 +171,7 @@ xp/services/server/base_server_service.py,sha256=B-ntxp3swbwuri-9_2EuvBDi-4Uo9AH
170
171
  xp/services/server/client_buffer_manager.py,sha256=1d_MqfzuUqBwaQUiC1n5K76WwSxrdngYAmNH7he6u3o,2235
171
172
  xp/services/server/cp20_server_service.py,sha256=SXdI6Jt400T9sLdw86ovEqKRGeV3nYVaHEA9Gcj6W2A,2041
172
173
  xp/services/server/device_service_factory.py,sha256=Y4TvSFALeq0zYzHfCwcbikSpmIyYbLcvm9756n5Jm7Q,3744
173
- xp/services/server/server_service.py,sha256=YAkn3rxNYnxs3E1Dpwtt3ZzymFXZHKEz8Nitim-Xewg,16300
174
+ xp/services/server/server_service.py,sha256=2jMrL-Azn7KaqpgSI1ztBh0UD3JWom5ns33IkqMCDac,16302
174
175
  xp/services/server/xp130_server_service.py,sha256=YnvetDp72-QzkyDGB4qfZZIwFs03HuibUOz2zb9XR0c,2191
175
176
  xp/services/server/xp20_server_service.py,sha256=1wJ7A-bRkN9O5Spu3q3LNDW31mNtNF2eNMQ5E6O2ltA,2928
176
177
  xp/services/server/xp230_server_service.py,sha256=k9ftCY5tjLFP31mKVCspq283RVaPkGx-Yq61Urk8JLs,1815
@@ -200,10 +201,10 @@ xp/term/widgets/protocol_log.py,sha256=CJUpckWj7GC1kVqixDadteyGnI4aHyzd4kkH-pSbz
200
201
  xp/term/widgets/status_footer.py,sha256=bxrcqKzJ9V0aPSn_WwraVpJz7NxBUh3yIjA3fwv5nVA,3256
201
202
  xp/utils/__init__.py,sha256=_avMF_UOkfR3tNeDIPqQ5odmbq5raKkaq1rZ9Cn1CJs,332
202
203
  xp/utils/checksum.py,sha256=HDpiQxmdIedbCbZ4o_Box0i_Zig417BtCV_46ZyhiTk,1711
203
- xp/utils/dependencies.py,sha256=Z1vRSWr_dhmyNhD4dstJg-ZOlKVPPq_viGGNJ32GRNs,24584
204
+ xp/utils/dependencies.py,sha256=zrvWx28N0f28JwRDRyqaf5Q9eV_yLwh9xDw9mYBUXEQ,25379
204
205
  xp/utils/event_helper.py,sha256=W-A_xmoXlpWZBbJH6qdaN50o3-XrwFsDgvAGMJDiAgo,1001
205
206
  xp/utils/logging.py,sha256=rZDXwlBrYK8A6MPq5StsMNpgsRowzJXM6fvROPwJdGM,3750
206
207
  xp/utils/serialization.py,sha256=RWHHk86feaB4ZP7rjE4qOWK0900yg2joUBDkP76gfOY,4618
207
208
  xp/utils/state_machine.py,sha256=Oe2sLwCh9z_vr1tF6X0ZRGTeuckRQAGzmef7xc9CNdc,2413
208
209
  xp/utils/time_utils.py,sha256=dEyViDlAG9GWU-J3D_YVa-sGma6yiyyMTgN4h2x3PY4,3781
209
- conson_xp-1.40.0.dist-info/RECORD,,
210
+ conson_xp-1.43.0.dist-info/RECORD,,
xp/__init__.py CHANGED
@@ -3,7 +3,7 @@
3
3
  conson-xp package.
4
4
  """
5
5
 
6
- __version__ = "1.40.0"
6
+ __version__ = "1.43.0"
7
7
  __manufacturer__ = "salchichon"
8
8
  __model__ = "xp.cli"
9
9
  __serial__ = "2025.09.23.000"
@@ -61,7 +61,7 @@ def conbus_download_actiontable(ctx: Context, serial_number: str) -> None:
61
61
  """
62
62
  click.echo(progress, nl=False)
63
63
 
64
- def on_finish(
64
+ def on_actiontable_received(
65
65
  _actiontable: ActionTable,
66
66
  actiontable_dict: Dict[str, Any],
67
67
  actiontable_short: list[str],
@@ -79,6 +79,9 @@ def conbus_download_actiontable(ctx: Context, serial_number: str) -> None:
79
79
  "actiontable": actiontable_dict,
80
80
  }
81
81
  click.echo(json.dumps(output, indent=2, default=str))
82
+
83
+ def on_finish() -> None:
84
+ """Handle successful completion of action table download."""
82
85
  service.stop_reactor()
83
86
 
84
87
  def on_error(error: str) -> None:
@@ -93,6 +96,7 @@ def conbus_download_actiontable(ctx: Context, serial_number: str) -> None:
93
96
  with service:
94
97
  service.on_progress.connect(on_progress)
95
98
  service.on_finish.connect(on_finish)
99
+ service.on_actiontable_received.connect(on_actiontable_received)
96
100
  service.on_error.connect(on_error)
97
101
  service.start(serial_number=serial_number)
98
102
  service.start_reactor()
@@ -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")
@@ -206,6 +209,65 @@ def conbus_show_msactiontable(ctx: Context, serial_number: str) -> None:
206
209
  )
207
210
 
208
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
+
209
271
  def _format_yaml(data: dict, indent: int = 0) -> str:
210
272
  """Format a dictionary as YAML-like output.
211
273
 
@@ -178,7 +178,7 @@ class Xp24MsActionTable(BaseModel):
178
178
  param_value = action.param.value
179
179
  action_parts.append(f"{short_code}:{param_value}")
180
180
 
181
- result = f"XP24 {' '.join(action_parts)}"
181
+ result = " ".join(action_parts)
182
182
 
183
183
  # Add settings
184
184
  settings = (
@@ -212,14 +212,14 @@ class Xp24MsActionTable(BaseModel):
212
212
 
213
213
  # Parse action part
214
214
  tokens = action_part.split()
215
- if len(tokens) != 5 or tokens[0] != "XP24":
215
+ if len(tokens) != 4:
216
216
  raise ValueError(
217
- 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}'"
218
218
  )
219
219
 
220
220
  # Parse input actions
221
221
  input_actions = []
222
- for i, token in enumerate(tokens[1:5], 1):
222
+ for i, token in enumerate(tokens[0:4], 1):
223
223
  if ":" not in token:
224
224
  raise ValueError(f"Invalid action format at position {i}: '{token}'")
225
225
 
@@ -4,10 +4,13 @@ import logging
4
4
  from dataclasses import asdict
5
5
  from typing import Any, Dict, Optional
6
6
 
7
- from psygnal import Signal
7
+ from psygnal import SignalInstance
8
+ from statemachine import State, StateMachine
8
9
 
9
10
  from xp.models.actiontable.actiontable import ActionTable
10
11
  from xp.models.protocol.conbus_protocol import TelegramReceivedEvent
12
+ from xp.models.telegram.datapoint_type import DataPointType
13
+ from xp.models.telegram.reply_telegram import ReplyTelegram
11
14
  from xp.models.telegram.system_function import SystemFunction
12
15
  from xp.models.telegram.telegram_type import TelegramType
13
16
  from xp.services.actiontable.actiontable_serializer import ActionTableSerializer
@@ -15,9 +18,11 @@ from xp.services.protocol.conbus_event_protocol import ConbusEventProtocol
15
18
  from xp.services.telegram.telegram_service import TelegramService
16
19
 
17
20
 
18
- class ActionTableDownloadService:
21
+ class ActionTableDownloadService(StateMachine):
19
22
  """TCP client service for downloading action tables from Conbus modules.
20
23
 
24
+ Inherits from StateMachine - the service IS the state machine.
25
+
21
26
  Manages TCP socket connections, handles telegram generation and transmission,
22
27
  and processes server responses for action table downloads.
23
28
 
@@ -25,13 +30,74 @@ class ActionTableDownloadService:
25
30
  on_progress: Signal emitted with telegram frame when progress is made.
26
31
  on_error: Signal emitted with error message string when an error occurs.
27
32
  on_finish: Signal emitted with (ActionTable, Dict[str, Any], list[str]) when complete.
33
+ idle: Initial state, waiting for connection.
34
+ receiving: Listening for telegrams, filtering relevant responses.
35
+ resetting: Timeout occurred, preparing error status query.
36
+ waiting_ok: Sent error status query, awaiting ACK/NAK.
37
+ requesting: Ready to send download request.
38
+ waiting_data: Awaiting actiontable chunk or EOF.
39
+ receiving_chunk: Processing received actiontable data.
40
+ processing_eof: Received EOF, deserializing actiontable.
41
+ completed: Download finished successfully.
42
+ do_connect: Transition from idle to receiving.
43
+ do_timeout: Transition from receiving to resetting.
44
+ send_error_status: Transition from resetting to waiting_ok.
45
+ error_status_received: Transition from waiting_ok to receiving (retry).
46
+ no_error_status_received: Transition from waiting_ok to requesting or completed.
47
+ send_download: Transition from requesting to waiting_data.
48
+ receive_chunk: Transition from waiting_data to receiving_chunk.
49
+ send_ack: Transition from receiving_chunk to waiting_data.
50
+ receive_eof: Transition from waiting_data to processing_eof.
51
+ do_finish: Transition from processing_eof to receiving.
52
+ receiving2: Second receiving state after EOF processing.
53
+ resetting2: Second resetting state for finalization phase.
54
+ waiting_ok2: Second waiting_ok state for finalization phase.
55
+ filter_telegram: Self-transition for filtering telegrams in receiving state.
56
+ filter_telegram2: Self-transition for filtering telegrams in receiving2 state.
57
+ do_timeout2: Timeout transition for finalization phase.
58
+ send_error_status2: Error status query transition for finalization phase.
59
+ error_status_received2: Error received transition for finalization phase.
60
+ no_error_status_received2: No error received transition to completed state.
28
61
  """
29
62
 
30
- on_progress: Signal = Signal(str)
31
- on_error: Signal = Signal(str)
32
- on_finish: Signal = Signal(
33
- ActionTable, Dict[str, Any], list[str]
34
- ) # (ActionTable, Dict[str, Any], list[str])
63
+ # States (9 states as per spec)
64
+ idle = State(initial=True)
65
+ receiving = State()
66
+ resetting = State()
67
+ waiting_ok = State()
68
+
69
+ requesting = State()
70
+ waiting_data = State()
71
+ receiving_chunk = State()
72
+ processing_eof = State()
73
+
74
+ receiving2 = State()
75
+ resetting2 = State()
76
+ waiting_ok2 = State()
77
+
78
+ completed = State(final=True)
79
+
80
+ # Phase 1: Connection & Initialization
81
+ do_connect = idle.to(receiving)
82
+ filter_telegram = receiving.to(receiving) # Self-transition for filtering
83
+ do_timeout = receiving.to(resetting) | waiting_ok.to(receiving)
84
+ send_error_status = resetting.to(waiting_ok)
85
+ error_status_received = waiting_ok.to(receiving)
86
+ no_error_status_received = waiting_ok.to(requesting)
87
+
88
+ # Phase 2: Download
89
+ send_download = requesting.to(waiting_data)
90
+ receive_chunk = waiting_data.to(receiving_chunk)
91
+ send_ack = receiving_chunk.to(waiting_data)
92
+ receive_eof = waiting_data.to(processing_eof)
93
+
94
+ # Phase 3: Finalization
95
+ do_finish = processing_eof.to(receiving2)
96
+ filter_telegram2 = receiving2.to(receiving2) # Self-transition for filtering
97
+ do_timeout2 = receiving2.to(resetting2) | waiting_ok2.to(receiving2)
98
+ send_error_status2 = resetting2.to(waiting_ok2)
99
+ error_status_received2 = waiting_ok2.to(receiving2)
100
+ no_error_status_received2 = waiting_ok2.to(completed)
35
101
 
36
102
  def __init__(
37
103
  self,
@@ -51,29 +117,127 @@ class ActionTableDownloadService:
51
117
  self.telegram_service = telegram_service
52
118
  self.serial_number: str = ""
53
119
  self.actiontable_data: list[str] = []
54
- # Set up logging
55
120
  self.logger = logging.getLogger(__name__)
56
121
 
122
+ # Signals (instance attributes to avoid conflict with statemachine)
123
+ self.on_progress: SignalInstance = SignalInstance((str,))
124
+ self.on_error: SignalInstance = SignalInstance((str,))
125
+ self.on_finish: SignalInstance = SignalInstance()
126
+ self.on_actiontable_received: SignalInstance = SignalInstance(
127
+ (ActionTable, Dict[str, Any], list[str])
128
+ )
129
+
57
130
  # Connect protocol signals
58
- self.conbus_protocol.on_connection_made.connect(self.connection_made)
59
- self.conbus_protocol.on_telegram_sent.connect(self.telegram_sent)
60
- self.conbus_protocol.on_telegram_received.connect(self.telegram_received)
61
- self.conbus_protocol.on_timeout.connect(self.timeout)
62
- self.conbus_protocol.on_failed.connect(self.failed)
131
+ self.conbus_protocol.on_connection_made.connect(self._on_connection_made)
132
+ self.conbus_protocol.on_telegram_sent.connect(self._on_telegram_sent)
133
+ self.conbus_protocol.on_telegram_received.connect(self._on_telegram_received)
134
+ self.conbus_protocol.on_timeout.connect(self._on_timeout)
135
+ self.conbus_protocol.on_failed.connect(self._on_failed)
63
136
 
64
- def connection_made(self) -> None:
65
- """Handle connection established event."""
66
- self.logger.debug(
67
- "Connection established, sending download actiontable telegram"
137
+ # Initialize state machine
138
+ super().__init__(allow_event_without_transition=True)
139
+
140
+ # State machine lifecycle hooks
141
+
142
+ def on_enter_receiving(self) -> None:
143
+ """Enter receiving state - listening for telegrams."""
144
+ self.logger.debug("Entering RECEIVING state - waiting for telegrams")
145
+ self.conbus_protocol.wait()
146
+
147
+ def on_enter_receiving2(self) -> None:
148
+ """Enter receiving state - listening for telegrams."""
149
+ self.logger.debug("Entering RECEIVING2 state - waiting for telegrams")
150
+ self.conbus_protocol.wait()
151
+
152
+ def on_enter_resetting(self) -> None:
153
+ """Enter resetting state - query error status."""
154
+ self.logger.debug("Entering RESETTING state - querying error status")
155
+
156
+ # query_datapoint_module_error_code
157
+ self.conbus_protocol.send_telegram(
158
+ telegram_type=TelegramType.SYSTEM,
159
+ serial_number=self.serial_number,
160
+ system_function=SystemFunction.READ_DATAPOINT,
161
+ data_value=DataPointType.MODULE_ERROR_CODE.value,
68
162
  )
163
+ self.send_error_status()
164
+
165
+ def on_enter_resetting2(self) -> None:
166
+ """Enter resetting state - query error status."""
167
+ self.logger.debug("Entering RESETTING2 state - querying error status")
168
+
169
+ # query_datapoint_module_error_code
170
+ self.conbus_protocol.send_telegram(
171
+ telegram_type=TelegramType.SYSTEM,
172
+ serial_number=self.serial_number,
173
+ system_function=SystemFunction.READ_DATAPOINT,
174
+ data_value=DataPointType.MODULE_ERROR_CODE.value,
175
+ )
176
+ self.send_error_status2()
177
+
178
+ def on_enter_waiting_ok(self) -> None:
179
+ """Enter waiting_ok state - awaiting ERROR/NO_ERROR."""
180
+ self.logger.debug("Entering WAITING_OK state - awaiting ERROR/NO_ERROR")
181
+ self.conbus_protocol.wait()
182
+
183
+ def on_enter_waiting_ok2(self) -> None:
184
+ """Enter waiting_ok state - awaiting ERROR/NO_ERROR."""
185
+ self.logger.debug("Entering WAITING_OK state - awaiting ERROR/NO_ERROR")
186
+ self.conbus_protocol.wait()
187
+
188
+ def on_enter_requesting(self) -> None:
189
+ """Enter requesting state - send download request."""
190
+ self.logger.debug("Entering REQUESTING state - sending download request")
69
191
  self.conbus_protocol.send_telegram(
70
192
  telegram_type=TelegramType.SYSTEM,
71
193
  serial_number=self.serial_number,
72
194
  system_function=SystemFunction.DOWNLOAD_ACTIONTABLE,
73
195
  data_value="00",
74
196
  )
197
+ self.send_download()
198
+
199
+ def on_enter_waiting_data(self) -> None:
200
+ """Enter waiting_data state - wait for actiontable chunks."""
201
+ self.logger.debug("Entering WAITING_DATA state - awaiting chunks")
202
+ self.conbus_protocol.wait()
75
203
 
76
- def telegram_sent(self, telegram_sent: str) -> None:
204
+ def on_enter_receiving_chunk(self) -> None:
205
+ """Enter receiving_chunk state - send ACK."""
206
+ self.logger.debug("Entering RECEIVING_CHUNK state - sending ACK")
207
+ self.conbus_protocol.send_telegram(
208
+ telegram_type=TelegramType.SYSTEM,
209
+ serial_number=self.serial_number,
210
+ system_function=SystemFunction.ACK,
211
+ data_value="00",
212
+ )
213
+ self.send_ack()
214
+
215
+ def on_enter_processing_eof(self) -> None:
216
+ """Enter processing_eof state - deserialize and emit result."""
217
+ self.logger.debug("Entering PROCESSING_EOF state - deserializing")
218
+ all_data = "".join(self.actiontable_data)
219
+ actiontable = self.serializer.from_encoded_string(all_data)
220
+ actiontable_dict = asdict(actiontable)
221
+ actiontable_short = self.serializer.format_decoded_output(actiontable)
222
+ self.on_actiontable_received.emit(
223
+ actiontable, actiontable_dict, actiontable_short
224
+ )
225
+ self.do_finish()
226
+
227
+ def on_enter_completed(self) -> None:
228
+ """Enter completed state - download finished."""
229
+ self.logger.debug("Entering COMPLETED state - download finished")
230
+ self.on_finish.emit()
231
+
232
+ # Protocol event handlers
233
+
234
+ def _on_connection_made(self) -> None:
235
+ """Handle connection established event."""
236
+ self.logger.debug("Connection made")
237
+ if self.idle.is_active:
238
+ self.do_connect()
239
+
240
+ def _on_telegram_sent(self, telegram_sent: str) -> None:
77
241
  """Handle telegram sent event.
78
242
 
79
243
  Args:
@@ -81,59 +245,148 @@ class ActionTableDownloadService:
81
245
  """
82
246
  self.logger.debug(f"Telegram sent: {telegram_sent}")
83
247
 
84
- def telegram_received(self, telegram_received: TelegramReceivedEvent) -> None:
248
+ def _on_read_datapoint_received(self, reply_telegram: ReplyTelegram) -> None:
249
+ """Handle READ_DATAPOINT response for error status check.
250
+
251
+ Args:
252
+ reply_telegram: The parsed reply telegram.
253
+ """
254
+ self.logger.debug(f"Received READ_DATAPOINT in {self.current_state}")
255
+
256
+ if reply_telegram.datapoint_type != DataPointType.MODULE_ERROR_CODE:
257
+ self.logger.debug(
258
+ f"Filtered: not a MODULE_ERROR_CODE (got {reply_telegram.datapoint_type})"
259
+ )
260
+ return
261
+
262
+ if reply_telegram.data_value == "00":
263
+ if self.waiting_ok.is_active:
264
+ self.no_error_status_received()
265
+
266
+ if reply_telegram.data_value != "00":
267
+ if self.waiting_ok.is_active:
268
+ self.error_status_received()
269
+
270
+ if reply_telegram.data_value == "00":
271
+ if self.waiting_ok2.is_active:
272
+ self.no_error_status_received2()
273
+
274
+ if reply_telegram.data_value != "00":
275
+ if self.waiting_ok2.is_active:
276
+ self.error_status_received2()
277
+
278
+ def _on_ack_received(self, _reply_telegram: ReplyTelegram) -> None:
279
+ """Handle ACK telegram received.
280
+
281
+ Args:
282
+ _reply_telegram: The parsed reply telegram (unused).
283
+ """
284
+ self.logger.debug(f"Received ACK in {self.current_state}")
285
+ if self.waiting_ok.is_active:
286
+ self.ack_received()
287
+
288
+ if self.waiting_ok2.is_active:
289
+ self.ack_received2()
290
+
291
+ def _on_nack_received(self, _reply_telegram: ReplyTelegram) -> None:
292
+ """Handle NAK telegram received.
293
+
294
+ Args:
295
+ _reply_telegram: The parsed reply telegram (unused).
296
+ """
297
+ self.logger.debug(f"Received NAK in {self.current_state}")
298
+ if self.waiting_ok.is_active:
299
+ self.nak_received()
300
+
301
+ def _on_actiontable_chunk_received(self, reply_telegram: ReplyTelegram) -> None:
302
+ """Handle actiontable chunk telegram received.
303
+
304
+ Args:
305
+ reply_telegram: The parsed reply telegram containing chunk data.
306
+ """
307
+ self.logger.debug(f"Received actiontable chunk in {self.current_state}")
308
+ if self.waiting_data.is_active:
309
+ data_part = reply_telegram.data_value[2:]
310
+ self.actiontable_data.append(data_part)
311
+ self.on_progress.emit(".")
312
+ self.receive_chunk()
313
+
314
+ def _on_eof_received(self, _reply_telegram: ReplyTelegram) -> None:
315
+ """Handle EOF telegram received.
316
+
317
+ Args:
318
+ _reply_telegram: The parsed reply telegram (unused).
319
+ """
320
+ self.logger.debug(f"Received EOF in {self.current_state}")
321
+ if self.waiting_data.is_active:
322
+ self.receive_eof()
323
+
324
+ def _on_telegram_received(self, telegram_received: TelegramReceivedEvent) -> None:
85
325
  """Handle telegram received event.
86
326
 
87
327
  Args:
88
328
  telegram_received: The telegram received event.
89
329
  """
90
- self.logger.debug(f"Telegram received: {telegram_received}")
91
- if (
92
- not telegram_received.checksum_valid
93
- or telegram_received.telegram_type != TelegramType.REPLY.value
94
- or telegram_received.serial_number != self.serial_number
95
- ):
96
- self.logger.debug("Not a reply response")
330
+ self.logger.debug(f"Received{telegram_received} in {self.current_state}")
331
+ if self.receiving.is_active:
332
+ self.filter_telegram()
333
+ return
334
+
335
+ # Filter invalid telegrams
336
+ if not telegram_received.checksum_valid:
337
+ self.logger.debug("Filtered: invalid checksum")
338
+ return
339
+ if telegram_received.telegram_type != TelegramType.REPLY.value:
340
+ self.logger.debug(
341
+ f"Filtered: not a reply (got {telegram_received.telegram_type})"
342
+ )
343
+ return
344
+ if telegram_received.serial_number != self.serial_number:
345
+ self.logger.debug(
346
+ f"Filtered: wrong serial {telegram_received.serial_number} != {self.serial_number}"
347
+ )
97
348
  return
98
349
 
99
350
  reply_telegram = self.telegram_service.parse_reply_telegram(
100
351
  telegram_received.frame
101
352
  )
102
- if reply_telegram.system_function not in (
103
- SystemFunction.ACTIONTABLE,
104
- SystemFunction.EOF,
105
- ):
106
- self.logger.debug("Not a actiontable response")
353
+
354
+ if reply_telegram.system_function == SystemFunction.READ_DATAPOINT:
355
+ self._on_read_datapoint_received(reply_telegram)
107
356
  return
108
357
 
109
- if reply_telegram.system_function == SystemFunction.ACTIONTABLE:
110
- self.logger.debug("Saving actiontable response")
111
- data_part = reply_telegram.data_value[2:]
112
- self.actiontable_data.append(data_part)
113
- self.on_progress.emit(".")
358
+ if reply_telegram.system_function == SystemFunction.ACK:
359
+ self._on_ack_received(reply_telegram)
360
+ return
114
361
 
115
- self.conbus_protocol.send_telegram(
116
- telegram_type=TelegramType.SYSTEM,
117
- serial_number=self.serial_number,
118
- system_function=SystemFunction.ACK,
119
- data_value="00",
120
- )
362
+ if reply_telegram.system_function == SystemFunction.NAK:
363
+ self._on_nack_received(reply_telegram)
364
+ return
365
+
366
+ if reply_telegram.system_function == SystemFunction.ACTIONTABLE:
367
+ self._on_actiontable_chunk_received(reply_telegram)
121
368
  return
122
369
 
123
370
  if reply_telegram.system_function == SystemFunction.EOF:
124
- all_data = "".join(self.actiontable_data)
125
- # Deserialize from received data
126
- actiontable = self.serializer.from_encoded_string(all_data)
127
- actiontable_dict = asdict(actiontable)
128
- actiontable_short = self.serializer.format_decoded_output(actiontable)
129
- self.on_finish.emit(actiontable, actiontable_dict, actiontable_short)
130
-
131
- def timeout(self) -> None:
371
+ self._on_eof_received(reply_telegram)
372
+ return
373
+
374
+ def _on_timeout(self) -> None:
132
375
  """Handle timeout event."""
133
376
  self.logger.debug("Timeout occurred")
134
- self.failed("Timeout")
135
-
136
- def failed(self, message: str) -> None:
377
+ if self.receiving.is_active:
378
+ self.do_timeout() # receiving -> resetting
379
+ elif self.waiting_ok.is_active:
380
+ self.do_timeout() # waiting_ok -> receiving
381
+ elif self.receiving2.is_active:
382
+ self.do_timeout2() # receiving2 -> resetting2
383
+ elif self.waiting_ok2.is_active:
384
+ self.do_timeout2() # waiting_ok2 -> receiving2
385
+ else:
386
+ self.logger.debug("Timeout in non-recoverable state")
387
+ self.on_error.emit("Timeout")
388
+
389
+ def _on_failed(self, message: str) -> None:
137
390
  """Handle failed connection event.
138
391
 
139
392
  Args:
@@ -142,10 +395,12 @@ class ActionTableDownloadService:
142
395
  self.logger.debug(f"Failed: {message}")
143
396
  self.on_error.emit(message)
144
397
 
398
+ # Public API
399
+
145
400
  def start(
146
401
  self,
147
402
  serial_number: str,
148
- timeout_seconds: Optional[float] = None,
403
+ timeout_seconds: Optional[float] = 2.0,
149
404
  ) -> None:
150
405
  """Run reactor in dedicated thread with its own event loop.
151
406
 
@@ -153,11 +408,10 @@ class ActionTableDownloadService:
153
408
  serial_number: Module serial number.
154
409
  timeout_seconds: Optional timeout in seconds.
155
410
  """
156
- self.logger.info("Starting actiontable")
411
+ self.logger.info("Starting actiontable download")
157
412
  self.serial_number = serial_number
158
413
  if timeout_seconds:
159
414
  self.conbus_protocol.timeout_seconds = timeout_seconds
160
- # Caller invokes start_reactor()
161
415
 
162
416
  def set_timeout(self, timeout_seconds: float) -> None:
163
417
  """Set operation timeout.
@@ -183,18 +437,28 @@ class ActionTableDownloadService:
183
437
  """
184
438
  # Reset state for singleton reuse
185
439
  self.actiontable_data = []
440
+ self._download_complete = False
441
+ # Reset state machine to idle
442
+ self._reset_state()
186
443
  return self
187
444
 
445
+ def _reset_state(self) -> None:
446
+ """Reset state machine to initial state."""
447
+ # python-statemachine uses model.state to track current state
448
+ # Set it directly to the initial state id
449
+ self.model.state = self.idle.id
450
+ self._download_complete = False
451
+
188
452
  def __exit__(
189
453
  self, _exc_type: Optional[type], _exc_val: Optional[Exception], _exc_tb: Any
190
454
  ) -> None:
191
455
  """Exit context manager and disconnect signals."""
192
456
  # Disconnect protocol signals
193
- self.conbus_protocol.on_connection_made.disconnect(self.connection_made)
194
- self.conbus_protocol.on_telegram_sent.disconnect(self.telegram_sent)
195
- self.conbus_protocol.on_telegram_received.disconnect(self.telegram_received)
196
- self.conbus_protocol.on_timeout.disconnect(self.timeout)
197
- self.conbus_protocol.on_failed.disconnect(self.failed)
457
+ self.conbus_protocol.on_connection_made.disconnect(self._on_connection_made)
458
+ self.conbus_protocol.on_telegram_sent.disconnect(self._on_telegram_sent)
459
+ self.conbus_protocol.on_telegram_received.disconnect(self._on_telegram_received)
460
+ self.conbus_protocol.on_timeout.disconnect(self._on_timeout)
461
+ self.conbus_protocol.on_failed.disconnect(self._on_failed)
198
462
  # Disconnect service signals
199
463
  self.on_progress.disconnect()
200
464
  self.on_error.disconnect()
@@ -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()
@@ -103,6 +103,16 @@ class ConbusEventProtocol(protocol.Protocol, protocol.ClientFactory):
103
103
  # Start inactivity timeout
104
104
  self._reset_timeout()
105
105
 
106
+ def wait(self, wait_timeout: Optional[float] = None) -> None:
107
+ """Wait for incoming telegrams with optional timeout override.
108
+
109
+ Args:
110
+ wait_timeout: Optional timeout in seconds to override default.
111
+ """
112
+ if wait_timeout:
113
+ self.timeout_seconds = wait_timeout
114
+ self._reset_timeout()
115
+
106
116
  def dataReceived(self, data: bytes) -> None:
107
117
  """Handle received data from TCP connection.
108
118
 
@@ -337,7 +337,7 @@ class ServerService:
337
337
  self.logger.warning(f"Failed to parse telegram: {telegram}")
338
338
  return responses
339
339
 
340
- self.client_buffers.broadcast(parsed_telegram.raw_telegram)
340
+ # self.client_buffers.broadcast(parsed_telegram.raw_telegram)
341
341
 
342
342
  # Handle discover requests
343
343
  if self.discover_service.is_discover_request(parsed_telegram):
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(