conson-xp 1.5.0__py3-none-any.whl → 1.7.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.
Files changed (27) hide show
  1. {conson_xp-1.5.0.dist-info → conson_xp-1.7.0.dist-info}/METADATA +1 -1
  2. {conson_xp-1.5.0.dist-info → conson_xp-1.7.0.dist-info}/RECORD +27 -24
  3. xp/__init__.py +1 -1
  4. xp/cli/commands/conbus/conbus_msactiontable_commands.py +11 -2
  5. xp/services/actiontable/__init__.py +1 -0
  6. xp/services/actiontable/msactiontable_serializer.py +7 -0
  7. xp/services/{conbus/actiontable → actiontable}/msactiontable_xp20_serializer.py +13 -25
  8. xp/services/{conbus/actiontable → actiontable}/msactiontable_xp24_serializer.py +25 -39
  9. xp/services/{conbus/actiontable → actiontable}/msactiontable_xp33_serializer.py +4 -21
  10. xp/services/conbus/actiontable/actiontable_service.py +1 -1
  11. xp/services/conbus/actiontable/msactiontable_service.py +44 -11
  12. xp/services/protocol/conbus_protocol.py +0 -2
  13. xp/services/server/base_server_service.py +83 -5
  14. xp/services/server/cp20_server_service.py +9 -1
  15. xp/services/server/device_service_factory.py +94 -0
  16. xp/services/server/server_service.py +16 -61
  17. xp/services/server/xp130_server_service.py +10 -2
  18. xp/services/server/xp20_server_service.py +44 -2
  19. xp/services/server/xp230_server_service.py +10 -2
  20. xp/services/server/xp24_server_service.py +54 -1
  21. xp/services/server/xp33_server_service.py +42 -1
  22. xp/services/telegram/telegram_service.py +4 -1
  23. xp/utils/dependencies.py +27 -6
  24. {conson_xp-1.5.0.dist-info → conson_xp-1.7.0.dist-info}/WHEEL +0 -0
  25. {conson_xp-1.5.0.dist-info → conson_xp-1.7.0.dist-info}/entry_points.txt +0 -0
  26. {conson_xp-1.5.0.dist-info → conson_xp-1.7.0.dist-info}/licenses/LICENSE +0 -0
  27. /xp/services/{conbus/actiontable → actiontable}/actiontable_serializer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: conson-xp
3
- Version: 1.5.0
3
+ Version: 1.7.0
4
4
  Summary: XP Protocol Communication Tools
5
5
  Author-Email: ldvchosal <ldvchosal@github.com>
6
6
  License: MIT License
@@ -1,8 +1,8 @@
1
- conson_xp-1.5.0.dist-info/METADATA,sha256=to2xeqnuavfCJCA0S4VC3trJaxL02i7c-4RQuU-1qjE,9274
2
- conson_xp-1.5.0.dist-info/WHEEL,sha256=9P2ygRxDrTJz3gsagc0Z96ukrxjr-LFBGOgv3AuKlCA,90
3
- conson_xp-1.5.0.dist-info/entry_points.txt,sha256=1OcdIcDM1hz3ljCXgybaPUh1IOFEwkaTgLIW9u9zGeg,50
4
- conson_xp-1.5.0.dist-info/licenses/LICENSE,sha256=rxj6woMM-r3YCyGq_UHFtbh7kHTAJgrccH6O-33IDE4,1419
5
- xp/__init__.py,sha256=UqV_T6LhENt9Tn3-NIiQjKGRaHZSmX5DPf-StL2UAO0,180
1
+ conson_xp-1.7.0.dist-info/METADATA,sha256=zJcFXnX0j-cYc8sCjdUkPuDsGp0-ji8KCGwE79oxZNQ,9274
2
+ conson_xp-1.7.0.dist-info/WHEEL,sha256=9P2ygRxDrTJz3gsagc0Z96ukrxjr-LFBGOgv3AuKlCA,90
3
+ conson_xp-1.7.0.dist-info/entry_points.txt,sha256=1OcdIcDM1hz3ljCXgybaPUh1IOFEwkaTgLIW9u9zGeg,50
4
+ conson_xp-1.7.0.dist-info/licenses/LICENSE,sha256=rxj6woMM-r3YCyGq_UHFtbh7kHTAJgrccH6O-33IDE4,1419
5
+ xp/__init__.py,sha256=IyP3xj9rnVfGLnpDwGvX8oBbLbm-WT5Qx42Tf8OD9I4,180
6
6
  xp/cli/__init__.py,sha256=QjnKB1KaI2aIyKlzrnvCwfbBuUj8HNgwNMvNJVQofbI,81
7
7
  xp/cli/__main__.py,sha256=l2iKwMdat5rTGd3JWs-uGksnYYDDffp_Npz05QdKEeU,117
8
8
  xp/cli/commands/__init__.py,sha256=02CbZoKmNX-fn5etX4Hdgg2lUt1MsLFPYx2VkXZyFJ8,4394
@@ -17,7 +17,7 @@ xp/cli/commands/conbus/conbus_datapoint_commands.py,sha256=r36OuTjREtbGKL-bskAGa
17
17
  xp/cli/commands/conbus/conbus_discover_commands.py,sha256=Jt-OjARc-QXCr453bhKG0zXRqvOMVKZr328zNxSDulY,1373
18
18
  xp/cli/commands/conbus/conbus_lightlevel_commands.py,sha256=FpCwogdxa7yFUjlrxM7e8Q2Ut32tKAHabngQQChvtJI,6763
19
19
  xp/cli/commands/conbus/conbus_linknumber_commands.py,sha256=pn491Iy_LcwZ5XLtvNTWu8qG_Vdaaxr7le1tTuVK7Qw,3392
20
- xp/cli/commands/conbus/conbus_msactiontable_commands.py,sha256=kKWX9S0Ip9eX9EyWPwmyKMtt6q-jRXcOaicMAKt84Ls,2652
20
+ xp/cli/commands/conbus/conbus_msactiontable_commands.py,sha256=fb9MQ4O04H0Dinpt7vSF5GtfntTZHelQ5TuUmSBbCTg,2899
21
21
  xp/cli/commands/conbus/conbus_output_commands.py,sha256=zdRVbHzVhMbZpG2x5WXtujc3wKTsoQUV4IgkVIbJbCc,5019
22
22
  xp/cli/commands/conbus/conbus_raw_commands.py,sha256=8BKUarwvHgz-sxML7n99YVsb8B1HJNExjQpRsuY_tQw,1829
23
23
  xp/cli/commands/conbus/conbus_receive_commands.py,sha256=WdH7fYcbjweIGxD2uxrTRD8lJzViMSVdsaHTvSDJNCQ,1757
@@ -97,14 +97,16 @@ xp/models/telegram/telegram_type.py,sha256=GhqKP63oNMyh2tIvCPcsC5RFp4s4JjhmEqCLC
97
97
  xp/models/telegram/timeparam_type.py,sha256=Ar8xvSfPmOAgR2g2Je0FgvP01SL7bPvZn5_HrVDpmJM,1137
98
98
  xp/models/write_config_type.py,sha256=T2RaO52RpzoJ4782uMHE-fX7Ymx3CaIQAEwByydXq1M,881
99
99
  xp/services/__init__.py,sha256=W9YZyrkh7vm--ZHhAXNQiOYQs5yhhmUHXP5I0Lf1XBg,782
100
+ xp/services/actiontable/__init__.py,sha256=z6js4EuJ6xKHaseTEhuEvKo1tr9K1XyQiruReJtBiPY,26
101
+ xp/services/actiontable/actiontable_serializer.py,sha256=x45-8d5Ba9l3hX2TFC5nqKv-g_244g-VTWhXvVXL8Jg,5159
102
+ xp/services/actiontable/msactiontable_serializer.py,sha256=RRL6TZ1gpSQw81kAiw2BV3jTqm4fCJC0pWIcO26Cmos,174
103
+ xp/services/actiontable/msactiontable_xp20_serializer.py,sha256=3Lz6t3uRYhoeMRhjDAO1XuWPJzH-ML13t05UQLFUW-s,6057
104
+ xp/services/actiontable/msactiontable_xp24_serializer.py,sha256=zdKzcrKqD41POqj_1c4B4why_Jp9mNXncajsnXXBtPw,4215
105
+ xp/services/actiontable/msactiontable_xp33_serializer.py,sha256=xoZBA38pBSUPA9nn7HgaH1ZM5sR2heQbJ6JVlPVbzUY,8400
100
106
  xp/services/conbus/__init__.py,sha256=Hi35sMKu9o6LpYoi2tQDaQoMb8M5sOt_-LUTxxaCU_0,28
101
107
  xp/services/conbus/actiontable/__init__.py,sha256=oD6vRk_Ye-eZ9s_hldAgtRJFu4mfAnODqpkJUGHHszk,40
102
- xp/services/conbus/actiontable/actiontable_serializer.py,sha256=x45-8d5Ba9l3hX2TFC5nqKv-g_244g-VTWhXvVXL8Jg,5159
103
- xp/services/conbus/actiontable/actiontable_service.py,sha256=QJwROShPU7uoexB9GxT6if8u8Cfa8yJO3WJqAHNjqMY,5633
104
- xp/services/conbus/actiontable/msactiontable_service.py,sha256=u64nejKvHzMdmlK9VoM7P3uMGIfjyfo2xp9dXXlgvjc,7451
105
- xp/services/conbus/actiontable/msactiontable_xp20_serializer.py,sha256=EYspooOdi0Z8oaXGxpazwnUoTmh-d7U9auhu11iBgmU,6527
106
- xp/services/conbus/actiontable/msactiontable_xp24_serializer.py,sha256=30qsk9UKje1n32PPc4YoGV1lw_ZvgxNqqd8ZDgzMJpg,4504
107
- xp/services/conbus/actiontable/msactiontable_xp33_serializer.py,sha256=nuWfka4U9W4lpTcS8uD6azXFcryPb0CUO5O7Z28G1k8,8901
108
+ xp/services/conbus/actiontable/actiontable_service.py,sha256=uy-BFCsjDoe1ZuZy9cTwRSIfMSxznLEN-iMtTsPW3EI,5626
109
+ xp/services/conbus/actiontable/msactiontable_service.py,sha256=yeMJRHVJe2s_-MquOdZDDym3g7135J3CzhxfEa6Qmkc,8357
108
110
  xp/services/conbus/conbus_blink_all_service.py,sha256=OaEg4b8AEiEruHSkZ5jDtaoI81vwwxLq4KWXO7zBdD0,6582
109
111
  xp/services/conbus/conbus_blink_service.py,sha256=x9uM-sLnIEV8wSNsvJgo08E042g-Hh2ZF3rXkz-k_9s,5824
110
112
  xp/services/conbus/conbus_custom_service.py,sha256=4aneYdPObiZOGxPFYg5Wr70cl_xFxlQIdJBPQSa0enI,5826
@@ -133,19 +135,20 @@ xp/services/homekit/homekit_service.py,sha256=0lW-hg40ETco3gDBEYkR_sX-UIYsLSKCD4
133
135
  xp/services/log_file_service.py,sha256=fvPcZQj8XOKUA-4ep5R8n0PelvwvRlTLlVxvIWM5KR4,10506
134
136
  xp/services/module_type_service.py,sha256=xWhr1EAZMykL5uNWHWdpa5T8yNruGKH43XRTOS8GwZg,7477
135
137
  xp/services/protocol/__init__.py,sha256=WuYn2iEcvsOIXnn5HCrU9kD3PjuMX1sIh0ljKISDoJw,720
136
- xp/services/protocol/conbus_protocol.py,sha256=G39YPMpwhvvhFPYrzNxx6y2Is6DSP2UyCLm4T7RLPVc,10404
138
+ xp/services/protocol/conbus_protocol.py,sha256=JO7yLkD_ohPT0ETjnAIx4CGpZyobf4LdbuozM_43btE,10276
137
139
  xp/services/protocol/protocol_factory.py,sha256=PmjN9AtW9sxNo3voqUiNgQA-pTvX1RW4XXFlHKfFr5Q,2470
138
140
  xp/services/protocol/telegram_protocol.py,sha256=Ki5DrXsKxiaqLcdP9WWUuhUI7cPu2DfwyZkh-Gv9Lb8,9496
139
141
  xp/services/reverse_proxy_service.py,sha256=BUOlcLlTU-R5iuC_96rasug21xo19wK9_4fMQXxc0QM,15061
140
142
  xp/services/server/__init__.py,sha256=QEcCj-jK0goAukJCe15TKYFQfSAzWsduPT_wW0HxZU8,48
141
- xp/services/server/base_server_service.py,sha256=AkeLWMOTasIIiBBGM_uTCXJ31yG1ciF98b9xKyq8VSs,9997
142
- xp/services/server/cp20_server_service.py,sha256=PkdkORQ-aIHtQb-wuAgkRxKcdpNWpvys_p1sXJg0yoI,1679
143
- xp/services/server/server_service.py,sha256=pRE_hdAlQfQBj10Y15IiBOz2wQL9LghoigZvcywbnKI,17899
144
- xp/services/server/xp130_server_service.py,sha256=mD3vE-JDR9s_o7zjVCu4cibM8hUbwJ1oxgb_JwtQ2WU,1819
145
- xp/services/server/xp20_server_service.py,sha256=s9RrqhCZ8xtgEzc8GXTlG81b4LtZLCFy79DhzBLTPjA,1428
146
- xp/services/server/xp230_server_service.py,sha256=c3kzkA-fEOglrjLISQLbyk_rUdKzwN20hc0qtF9MEAQ,1443
147
- xp/services/server/xp24_server_service.py,sha256=_QMHe0UgxVlyB0DZmP1KPdjheT1qE8V8-EW55FM58DY,6606
148
- xp/services/server/xp33_server_service.py,sha256=BhZaphtb1hgedTmB1bTuLlWTIZp3AOuec2gffDvKmRM,18329
143
+ xp/services/server/base_server_service.py,sha256=B-ntxp3swbwuri-9_2EuvBDi-4Uo9AH-AA4iAFGWIS4,12682
144
+ xp/services/server/cp20_server_service.py,sha256=SXdI6Jt400T9sLdw86ovEqKRGeV3nYVaHEA9Gcj6W2A,2041
145
+ xp/services/server/device_service_factory.py,sha256=Y4TvSFALeq0zYzHfCwcbikSpmIyYbLcvm9756n5Jm7Q,3744
146
+ xp/services/server/server_service.py,sha256=JPRFMto2l956dW7vfSclQugu2vdF0fssxxUOYjHNtA4,15833
147
+ xp/services/server/xp130_server_service.py,sha256=YnvetDp72-QzkyDGB4qfZZIwFs03HuibUOz2zb9XR0c,2191
148
+ xp/services/server/xp20_server_service.py,sha256=1wJ7A-bRkN9O5Spu3q3LNDW31mNtNF2eNMQ5E6O2ltA,2928
149
+ xp/services/server/xp230_server_service.py,sha256=k9ftCY5tjLFP31mKVCspq283RVaPkGx-Yq61Urk8JLs,1815
150
+ xp/services/server/xp24_server_service.py,sha256=S4kDZHf6SsFTwIzk1PwkWntFHtmOuVcz6UclkRdTGsc,8670
151
+ xp/services/server/xp33_server_service.py,sha256=X5BJr7RYueHAPNrfW-HnqV7ZN-OAouKxH1qMdDADqhk,19745
149
152
  xp/services/telegram/__init__.py,sha256=kv0JgMg13Fp18WgGQpalNRAWwiWbrz18X4kZAP9xpSQ,48
150
153
  xp/services/telegram/telegram_blink_service.py,sha256=Xctc9mCSZiiW1YTh8cA-4jlc8fTioS5OxT6ymhSqiYI,4487
151
154
  xp/services/telegram/telegram_checksum_service.py,sha256=rp_C5PlraOOIyqZDp9XjBBNZLUeBLdQNNHVpN6D-1v8,4729
@@ -153,12 +156,12 @@ xp/services/telegram/telegram_datapoint_service.py,sha256=A3ERuFSWc22uJUoymH-2dW
153
156
  xp/services/telegram/telegram_discover_service.py,sha256=oTpiq-yzP_UmC0xVOMMFeHO-rIlK1pF3aG-Kq4SeiBI,9025
154
157
  xp/services/telegram/telegram_link_number_service.py,sha256=1_c-_QCRPTHYn3BmMElrBJqGG6vnoIst8CB-N42hazk,6862
155
158
  xp/services/telegram/telegram_output_service.py,sha256=UaUv_14fR8o5K2PxQBXrCzx-Hohnk-gzbev_oLw_Clc,10799
156
- xp/services/telegram/telegram_service.py,sha256=CQKmwV0Jmlr1WwrshaANyp_e77DjBzXzuFL1U5DRgFI,13092
159
+ xp/services/telegram/telegram_service.py,sha256=XrP1CPi0ckxoKBaNwLA6lo-TogWxXgmXDOsU4Xl8BlY,13237
157
160
  xp/services/telegram/telegram_version_service.py,sha256=M5HdOTsLdcwo122FP-jW6R740ktLrtKf2TiMDVz23h8,10528
158
161
  xp/utils/__init__.py,sha256=_avMF_UOkfR3tNeDIPqQ5odmbq5raKkaq1rZ9Cn1CJs,332
159
162
  xp/utils/checksum.py,sha256=HDpiQxmdIedbCbZ4o_Box0i_Zig417BtCV_46ZyhiTk,1711
160
- xp/utils/dependencies.py,sha256=4G7r0m1HY9UV4E0zLS8L-axcNiX2mM-N6OOAU8dVHVM,17740
163
+ xp/utils/dependencies.py,sha256=1XDwIg3OsmLvOazMQ3qaktcsitYW8E400RxihNWgyt0,18894
161
164
  xp/utils/event_helper.py,sha256=W-A_xmoXlpWZBbJH6qdaN50o3-XrwFsDgvAGMJDiAgo,1001
162
165
  xp/utils/serialization.py,sha256=RWHHk86feaB4ZP7rjE4qOWK0900yg2joUBDkP76gfOY,4618
163
166
  xp/utils/time_utils.py,sha256=dEyViDlAG9GWU-J3D_YVa-sGma6yiyyMTgN4h2x3PY4,3781
164
- conson_xp-1.5.0.dist-info/RECORD,,
167
+ conson_xp-1.7.0.dist-info/RECORD,,
xp/__init__.py CHANGED
@@ -3,7 +3,7 @@
3
3
  conson-xp package.
4
4
  """
5
5
 
6
- __version__ = "1.5.0"
6
+ __version__ = "1.7.0"
7
7
  __manufacturer__ = "salchichon"
8
8
  __model__ = "xp.cli"
9
9
  __serial__ = "2025.09.23.000"
@@ -49,13 +49,22 @@ def conbus_download_msactiontable(
49
49
  click.echo(progress, nl=False)
50
50
 
51
51
  def on_finish(
52
- action_table: Union[Xp20MsActionTable | Xp24MsActionTable | Xp33MsActionTable],
52
+ action_table: Union[
53
+ Xp20MsActionTable, Xp24MsActionTable, Xp33MsActionTable, None
54
+ ],
53
55
  ) -> None:
54
56
  """Handle successful completion of MS action table download.
55
57
 
56
58
  Args:
57
- action_table: Downloaded MS action table object.
59
+ action_table: Downloaded MS action table object or None if failed.
60
+
61
+ Raises:
62
+ Abort: If action table download failed.
58
63
  """
64
+ if action_table is None:
65
+ click.echo("Error: Failed to download MS action table")
66
+ raise click.Abort()
67
+
59
68
  output = {
60
69
  "serial_number": serial_number,
61
70
  "xpmoduletype": xpmoduletype,
@@ -0,0 +1 @@
1
+ """Action table utils."""
@@ -0,0 +1,7 @@
1
+ """Generic MsActionTable serializer base class for type hints."""
2
+
3
+
4
+ class MsActionTableSerializer:
5
+ """Serializer for ActionTable telegram encoding/decoding."""
6
+
7
+ pass
@@ -46,8 +46,9 @@ class Xp20MsActionTableSerializer:
46
46
  input_channel, input_index, raw_bytes
47
47
  )
48
48
 
49
+ encoded_data = nibbles(raw_bytes)
49
50
  # Convert raw bytes to hex string with A-P encoding
50
- return nibbles(raw_bytes)
51
+ return "AAAA" + encoded_data
51
52
 
52
53
  @staticmethod
53
54
  def from_data(msactiontable_rawdata: str) -> Xp20MsActionTable:
@@ -62,13 +63,20 @@ class Xp20MsActionTableSerializer:
62
63
  Raises:
63
64
  ValueError: If input length is not 64 characters
64
65
  """
65
- if len(msactiontable_rawdata) != 64:
66
+ raw_length = len(msactiontable_rawdata)
67
+ if raw_length < 68: # Minimum: 4 char prefix + 64 chars data
66
68
  raise ValueError(
67
- f"XP20 action table data must be 64 characters long, got {len(msactiontable_rawdata)}"
69
+ f"XP20 action table data must be 68 characters long, got {len(msactiontable_rawdata)}"
68
70
  )
69
71
 
70
- # Convert hex string to bytes using de_nibble (A-P encoding)
71
- raw_bytes = de_nibbles(msactiontable_rawdata)
72
+ # Remove action table count prefix (first 4 characters: AAAA, AAAB, etc.)
73
+ data = msactiontable_rawdata[4:]
74
+
75
+ # Take first 64 chars (32 bytes) as per pseudocode
76
+ hex_data = data[:64]
77
+
78
+ # Convert hex string to bytes using deNibble (A-P encoding)
79
+ raw_bytes = de_nibbles(hex_data)
72
80
 
73
81
  # Decode input channels
74
82
  input_channels = []
@@ -159,23 +167,3 @@ class Xp20MsActionTableSerializer:
159
167
  and_functions_byte |= 1 << bit_index
160
168
 
161
169
  raw_bytes[AND_FUNCTIONS_INDEX + input_index] = and_functions_byte
162
-
163
- @staticmethod
164
- def from_telegrams(ms_telegrams: str) -> Xp20MsActionTable:
165
- """Legacy method for backward compatibility. Use from_data() instead.
166
-
167
- Args:
168
- ms_telegrams: Full telegram string
169
-
170
- Returns:
171
- Decoded XP20 action table
172
- """
173
- # Extract data portion from telegram (skip header, take action table data)
174
- # Based on XP24 pattern: telegram[16:84] gives us the 68-char data portion
175
- # For XP20, we need 64 chars, so we take the first 64 chars after removing count
176
- data_parts = ms_telegrams[16:84]
177
-
178
- # Remove action table count (first 4 chars: AAAA, AAAB, etc.)
179
- hex_data = data_parts[4:68] # Take 64 chars after count
180
-
181
- return Xp20MsActionTableSerializer.from_data(hex_data)
@@ -3,7 +3,7 @@
3
3
  from xp.models.actiontable.msactiontable_xp24 import InputAction, Xp24MsActionTable
4
4
  from xp.models.telegram.input_action_type import InputActionType
5
5
  from xp.models.telegram.timeparam_type import TimeParam
6
- from xp.utils.serialization import de_nibbles
6
+ from xp.utils.serialization import de_nibbles, nibbles
7
7
 
8
8
 
9
9
  class Xp24MsActionTableSerializer:
@@ -17,11 +17,12 @@ class Xp24MsActionTableSerializer:
17
17
  action_table: XP24 MS action table to serialize.
18
18
 
19
19
  Returns:
20
- Serialized action table data string.
20
+ Serialized action table data string (68 characters).
21
21
  """
22
- data_parts: list[str] = []
22
+ # Build byte array for the action table (32 bytes total)
23
+ raw_bytes = bytearray()
23
24
 
24
- # Encode all 4 input actions
25
+ # Encode all 4 input actions (2 bytes each = 8 bytes total)
25
26
  input_actions = [
26
27
  action_table.input1_action,
27
28
  action_table.input2_action,
@@ -30,26 +31,24 @@ class Xp24MsActionTableSerializer:
30
31
  ]
31
32
 
32
33
  for action in input_actions:
33
- # Use enum value directly as function ID
34
- function_id = action.type.value
35
- # Convert parameter to int (None becomes 0)
36
- param_id = action.param.value
37
- data_parts.append(f"{function_id:02X}{param_id:02X}")
38
-
39
- # Add settings as hex values
40
- data_parts.extend(
41
- [
42
- "AB" if action_table.mutex12 else "AA",
43
- "AB" if action_table.mutex34 else "AA",
44
- f"{action_table.mutual_deadtime:02X}",
45
- "AB" if action_table.curtain12 else "AA",
46
- "AB" if action_table.curtain34 else "AA",
47
- "A" * 38, # padding
48
- ]
49
- )
34
+ raw_bytes.append(action.type.value)
35
+ raw_bytes.append(action.param.value)
36
+
37
+ # Add settings (5 bytes)
38
+ raw_bytes.append(0x01 if action_table.mutex12 else 0x00)
39
+ raw_bytes.append(0x01 if action_table.mutex34 else 0x00)
40
+ raw_bytes.append(action_table.mutual_deadtime)
41
+ raw_bytes.append(0x01 if action_table.curtain12 else 0x00)
42
+ raw_bytes.append(0x01 if action_table.curtain34 else 0x00)
43
+
44
+ # Add padding to reach 32 bytes (19 more bytes needed)
45
+ raw_bytes.extend([0x00] * 19)
46
+
47
+ # Encode to A-P nibbles (32 bytes -> 64 chars)
48
+ encoded_data = nibbles(bytes(raw_bytes))
50
49
 
51
- data = "AAAA".join(data_parts)
52
- return data
50
+ # Prepend action table count "AAAA" (4 chars) -> total 68 chars
51
+ return "AAAA" + encoded_data
53
52
 
54
53
  @staticmethod
55
54
  def from_data(msactiontable_rawdata: str) -> Xp24MsActionTable:
@@ -66,7 +65,9 @@ class Xp24MsActionTableSerializer:
66
65
  """
67
66
  raw_length = len(msactiontable_rawdata)
68
67
  if raw_length != 68:
69
- raise ValueError(f"Msactiontable is not 68 bytes long ({raw_length})")
68
+ raise ValueError(
69
+ f"Msactiontable is not 68 bytes long ({raw_length}): {msactiontable_rawdata}"
70
+ )
70
71
 
71
72
  # Remove action table count AAAA, AAAB .
72
73
  data = msactiontable_rawdata[4:]
@@ -117,18 +118,3 @@ class Xp24MsActionTableSerializer:
117
118
  param_type = TimeParam(param_id)
118
119
 
119
120
  return InputAction(action_type, param_type)
120
-
121
- @staticmethod
122
- def from_telegrams(ms_telegrams: str) -> Xp24MsActionTable:
123
- """Legacy method for backward compatibility. Use from_data() instead.
124
-
125
- Args:
126
- ms_telegrams: Telegram data string.
127
-
128
- Returns:
129
- Deserialized XP24 MS action table.
130
- """
131
- # For backward compatibility, assume full telegrams and extract data
132
- data_parts = ms_telegrams[16:84]
133
-
134
- return Xp24MsActionTableSerializer.from_data(data_parts)
@@ -6,7 +6,7 @@ from xp.models.actiontable.msactiontable_xp33 import (
6
6
  Xp33Scene,
7
7
  )
8
8
  from xp.models.telegram.timeparam_type import TimeParam
9
- from xp.utils.serialization import bits_to_byte, byte_to_bits, de_nibbles, nibble
9
+ from xp.utils.serialization import bits_to_byte, byte_to_bits, de_nibbles, nibbles
10
10
 
11
11
 
12
12
  class Xp33MsActionTableSerializer:
@@ -96,13 +96,11 @@ class Xp33MsActionTableSerializer:
96
96
  raw_bytes[24] = bits_to_byte(leading_edge_bits)
97
97
 
98
98
  # Bytes 25-31 are padding (already 0)
99
-
100
99
  # Convert to hex string using nibble encoding
101
- hex_data = "AAAA"
102
- for byte_val in raw_bytes:
103
- hex_data += nibble(byte_val)
100
+ encoded_data = nibbles(raw_bytes)
104
101
 
105
- return hex_data
102
+ # Convert raw bytes to hex string with A-P encoding
103
+ return "AAAA" + encoded_data
106
104
 
107
105
  @staticmethod
108
106
  def from_data(msactiontable_rawdata: str) -> Xp33MsActionTable:
@@ -239,18 +237,3 @@ class Xp33MsActionTableSerializer:
239
237
  output3_level=output3_level,
240
238
  time=time_param,
241
239
  )
242
-
243
- @staticmethod
244
- def from_telegrams(ms_telegrams: str) -> Xp33MsActionTable:
245
- """Legacy method for backward compatibility. Use from_data() instead.
246
-
247
- Args:
248
- ms_telegrams: Telegram data string.
249
-
250
- Returns:
251
- Deserialized XP33 MS action table.
252
- """
253
- # For backward compatibility, assume full telegrams and extract data
254
- data_parts = ms_telegrams[16:152] # Adjusted for XP33 length
255
-
256
- return Xp33MsActionTableSerializer.from_data(data_parts)
@@ -10,7 +10,7 @@ from xp.models.actiontable.actiontable import ActionTable
10
10
  from xp.models.protocol.conbus_protocol import TelegramReceivedEvent
11
11
  from xp.models.telegram.system_function import SystemFunction
12
12
  from xp.models.telegram.telegram_type import TelegramType
13
- from xp.services.conbus.actiontable.actiontable_serializer import ActionTableSerializer
13
+ from xp.services.actiontable.actiontable_serializer import ActionTableSerializer
14
14
  from xp.services.protocol import ConbusProtocol
15
15
  from xp.services.telegram.telegram_service import TelegramService
16
16
 
@@ -12,13 +12,13 @@ from xp.models.actiontable.msactiontable_xp33 import Xp33MsActionTable
12
12
  from xp.models.protocol.conbus_protocol import TelegramReceivedEvent
13
13
  from xp.models.telegram.system_function import SystemFunction
14
14
  from xp.models.telegram.telegram_type import TelegramType
15
- from xp.services.conbus.actiontable.msactiontable_xp20_serializer import (
15
+ from xp.services.actiontable.msactiontable_xp20_serializer import (
16
16
  Xp20MsActionTableSerializer,
17
17
  )
18
- from xp.services.conbus.actiontable.msactiontable_xp24_serializer import (
18
+ from xp.services.actiontable.msactiontable_xp24_serializer import (
19
19
  Xp24MsActionTableSerializer,
20
20
  )
21
- from xp.services.conbus.actiontable.msactiontable_xp33_serializer import (
21
+ from xp.services.actiontable.msactiontable_xp33_serializer import (
22
22
  Xp33MsActionTableSerializer,
23
23
  )
24
24
  from xp.services.protocol import ConbusProtocol
@@ -74,7 +74,8 @@ class MsActionTableService(ConbusProtocol):
74
74
  self.error_callback: Optional[Callable[[str], None]] = None
75
75
  self.finish_callback: Optional[
76
76
  Callable[
77
- [Union[Xp20MsActionTable, Xp24MsActionTable, Xp33MsActionTable]], None
77
+ [Union[Xp20MsActionTable, Xp24MsActionTable, Xp33MsActionTable, None]],
78
+ None,
78
79
  ]
79
80
  ] = None
80
81
  self.msactiontable_data: list[str] = []
@@ -114,17 +115,32 @@ class MsActionTableService(ConbusProtocol):
114
115
  self.logger.debug("Not a reply response")
115
116
  return
116
117
 
117
- reply_telegram = self.telegram_service.parse_reply_telegram(telegram_received)
118
+ reply_telegram = self.telegram_service.parse_reply_telegram(
119
+ telegram_received.frame
120
+ )
118
121
  if reply_telegram.system_function not in (
119
122
  SystemFunction.MSACTIONTABLE,
123
+ SystemFunction.ACK,
124
+ SystemFunction.NAK,
120
125
  SystemFunction.EOF,
121
126
  ):
122
127
  self.logger.debug("Not a msactiontable response")
123
128
  return
124
129
 
125
- if reply_telegram.system_function == SystemFunction.ACTIONTABLE:
126
- self.logger.debug("Saving msactiontable response")
127
- self.msactiontable_data.append(reply_telegram.data_value)
130
+ if reply_telegram.system_function == SystemFunction.ACK:
131
+ self.logger.debug("Received ACK")
132
+ return
133
+
134
+ if reply_telegram.system_function == SystemFunction.NAK:
135
+ self.logger.debug("Received NAK")
136
+ self.failed("Received NAK")
137
+ return
138
+
139
+ if reply_telegram.system_function == SystemFunction.MSACTIONTABLE:
140
+ self.logger.debug("Received MSACTIONTABLE")
141
+ self.msactiontable_data.extend(
142
+ (reply_telegram.data, reply_telegram.data_value)
143
+ )
128
144
  if self.progress_callback:
129
145
  self.progress_callback(".")
130
146
 
@@ -137,11 +153,14 @@ class MsActionTableService(ConbusProtocol):
137
153
  return
138
154
 
139
155
  if reply_telegram.system_function == SystemFunction.EOF:
156
+ self.logger.debug("Received EOF")
140
157
  all_data = "".join(self.msactiontable_data)
141
158
  # Deserialize from received data
142
159
  msactiontable = self.serializer.from_data(all_data)
143
- if self.finish_callback:
144
- self.finish_callback(msactiontable)
160
+ self.succeed(msactiontable)
161
+ return
162
+
163
+ self.logger.debug("Invalid msactiontable response")
145
164
 
146
165
  def failed(self, message: str) -> None:
147
166
  """Handle failed connection event.
@@ -152,6 +171,20 @@ class MsActionTableService(ConbusProtocol):
152
171
  self.logger.debug(f"Failed: {message}")
153
172
  if self.error_callback:
154
173
  self.error_callback(message)
174
+ self._stop_reactor()
175
+
176
+ def succeed(
177
+ self,
178
+ msactiontable: Union[Xp20MsActionTable, Xp24MsActionTable, Xp33MsActionTable],
179
+ ) -> None:
180
+ """Handle succeed connection event.
181
+
182
+ Args:
183
+ msactiontable: result.
184
+ """
185
+ if self.finish_callback:
186
+ self.finish_callback(msactiontable)
187
+ self._stop_reactor()
155
188
 
156
189
  def start(
157
190
  self,
@@ -160,7 +193,7 @@ class MsActionTableService(ConbusProtocol):
160
193
  progress_callback: Callable[[str], None],
161
194
  error_callback: Callable[[str], None],
162
195
  finish_callback: Callable[
163
- [Union[Xp20MsActionTable, Xp24MsActionTable, Xp33MsActionTable]], None
196
+ [Union[Xp20MsActionTable, Xp24MsActionTable, Xp33MsActionTable, None]], None
164
197
  ],
165
198
  timeout_seconds: Optional[float] = None,
166
199
  ) -> None:
@@ -230,13 +230,11 @@ class ConbusProtocol(protocol.Protocol, protocol.ClientFactory):
230
230
  self.timeout_call = self.reactor.callLater(
231
231
  self.timeout_seconds, self._on_timeout
232
232
  )
233
- self.logger.debug(f"Timeout set for {self.timeout_seconds} seconds")
234
233
 
235
234
  def _cancel_timeout(self) -> None:
236
235
  """Cancel the inactivity timeout."""
237
236
  if self.timeout_call and self.timeout_call.active():
238
237
  self.timeout_call.cancel()
239
- self.logger.debug("Timeout cancelled")
240
238
 
241
239
  def _on_timeout(self) -> None:
242
240
  """Handle inactivity timeout expiration."""
@@ -7,7 +7,7 @@ containing common functionality like module type response generation.
7
7
  import logging
8
8
  import threading
9
9
  from abc import ABC
10
- from typing import Optional
10
+ from typing import Any, Optional
11
11
 
12
12
  from xp.models import ModuleTypeCode
13
13
  from xp.models.telegram.datapoint_type import DataPointType
@@ -46,6 +46,9 @@ class BaseServerService(ABC):
46
46
  self.telegram_buffer: list[str] = []
47
47
  self.telegram_buffer_lock = threading.Lock() # Lock for socket set
48
48
 
49
+ # MsActionTable download state (None, "ack_sent", "data_sent")
50
+ self.msactiontable_download_state: Optional[str] = None
51
+
49
52
  def generate_datapoint_type_response(
50
53
  self, datapoint_type: DataPointType
51
54
  ) -> Optional[str]:
@@ -151,6 +154,76 @@ class BaseServerService(ABC):
151
154
 
152
155
  return None
153
156
 
157
+ def _get_msactiontable_serializer(self) -> Optional[Any]:
158
+ """Get the MsActionTable serializer for this device.
159
+
160
+ Subclasses should override this to return their specific serializer.
161
+
162
+ Returns:
163
+ The serializer instance, or None if not supported.
164
+ """
165
+ return None
166
+
167
+ def _get_msactiontable(self) -> Optional[Any]:
168
+ """Get the MsActionTable for this device.
169
+
170
+ Subclasses should override this to return their msactiontable instance.
171
+
172
+ Returns:
173
+ The msactiontable instance, or None if not supported.
174
+ """
175
+ return None
176
+
177
+ def _handle_download_msactiontable_request(
178
+ self, request: SystemTelegram
179
+ ) -> Optional[str]:
180
+ """Handle F13D - DOWNLOAD_MSACTIONTABLE request.
181
+
182
+ Args:
183
+ request: The system telegram request to process.
184
+
185
+ Returns:
186
+ ACK telegram if request is valid, NAK otherwise.
187
+ """
188
+ serializer = self._get_msactiontable_serializer()
189
+ msactiontable = self._get_msactiontable()
190
+
191
+ # Only handle if serializer and msactiontable are available
192
+ if not serializer or msactiontable is None:
193
+ return None
194
+
195
+ # Send ACK and queue data telegram
196
+ ack_data = self._build_response_telegram(f"R{self.serial_number}F18D") # ACK
197
+
198
+ # Send MsActionTable data
199
+ encoded_data = serializer.to_data(msactiontable)
200
+ data_telegram = self._build_response_telegram(
201
+ f"R{self.serial_number}F17D{encoded_data}"
202
+ )
203
+ self.msactiontable_download_state = "data_sent"
204
+
205
+ # Return ACK and TABLE
206
+ return ack_data + data_telegram
207
+
208
+ def _handle_download_msactiontable_ack_request(
209
+ self, _request: SystemTelegram
210
+ ) -> Optional[str]:
211
+ """Handle MsActionTable download ACK protocol.
212
+
213
+ Args:
214
+ _request: The system telegram request (unused, kept for signature consistency).
215
+
216
+ Returns:
217
+ Data telegram, EOF telegram, or NAK if state is invalid.
218
+ """
219
+ if self.msactiontable_download_state == "data_sent":
220
+ # Send EOF
221
+ eof_telegram = self._build_response_telegram(f"R{self.serial_number}F16D")
222
+ self.msactiontable_download_state = None
223
+ return eof_telegram
224
+
225
+ return self._build_response_telegram(f"R{self.serial_number}F19D") # NAK
226
+
154
227
  def process_system_telegram(self, request: SystemTelegram) -> Optional[str]:
155
228
  """Template method for processing system telegrams.
156
229
 
@@ -177,6 +250,15 @@ class BaseServerService(ABC):
177
250
  elif request.system_function == SystemFunction.ACTION:
178
251
  return self._handle_action_request(request)
179
252
 
253
+ elif request.system_function == SystemFunction.DOWNLOAD_MSACTIONTABLE:
254
+ return self._handle_download_msactiontable_request(request)
255
+
256
+ elif (
257
+ request.system_function == SystemFunction.ACK
258
+ and self.msactiontable_download_state
259
+ ):
260
+ return self._handle_download_msactiontable_ack_request(request)
261
+
180
262
  self.logger.warning(f"Unhandled {self.device_type} request: {request}")
181
263
  return None
182
264
 
@@ -278,11 +360,7 @@ class BaseServerService(ABC):
278
360
  Returns:
279
361
  List of telegram strings from the buffer. The buffer is cleared after collection.
280
362
  """
281
- self.logger.debug(
282
- f"Collecting {self.serial_number} telegrams from buffer: {len(self.telegram_buffer)}"
283
- )
284
363
  with self.telegram_buffer_lock:
285
364
  result = self.telegram_buffer.copy()
286
- self.logger.debug(f"Resetting {self.serial_number} buffer")
287
365
  self.telegram_buffer.clear()
288
366
  return result
@@ -8,6 +8,7 @@ from typing import Dict, Optional
8
8
 
9
9
  from xp.models import ModuleTypeCode
10
10
  from xp.models.telegram.system_telegram import SystemTelegram
11
+ from xp.services.actiontable.msactiontable_serializer import MsActionTableSerializer
11
12
  from xp.services.server.base_server_service import BaseServerService
12
13
 
13
14
 
@@ -25,11 +26,18 @@ class CP20ServerService(BaseServerService):
25
26
  and implements CP20 telegram format.
26
27
  """
27
28
 
28
- def __init__(self, serial_number: str):
29
+ def __init__(
30
+ self,
31
+ serial_number: str,
32
+ _variant: str = "",
33
+ _msactiontable_serializer: Optional[MsActionTableSerializer] = None,
34
+ ):
29
35
  """Initialize CP20 server service.
30
36
 
31
37
  Args:
32
38
  serial_number: The device serial number.
39
+ _variant: Reserved parameter for consistency (unused).
40
+ _msactiontable_serializer: Generic MsActionTable serializer (unused).
33
41
  """
34
42
  super().__init__(serial_number)
35
43
  self.device_type = "CP20"
@@ -0,0 +1,94 @@
1
+ """Device Service Factory for creating device instances.
2
+
3
+ This module provides a factory for creating device service instances
4
+ with proper dependency injection of serializers.
5
+ """
6
+
7
+ from xp.services.actiontable.msactiontable_serializer import MsActionTableSerializer
8
+ from xp.services.actiontable.msactiontable_xp20_serializer import (
9
+ Xp20MsActionTableSerializer,
10
+ )
11
+ from xp.services.actiontable.msactiontable_xp24_serializer import (
12
+ Xp24MsActionTableSerializer,
13
+ )
14
+ from xp.services.actiontable.msactiontable_xp33_serializer import (
15
+ Xp33MsActionTableSerializer,
16
+ )
17
+ from xp.services.server.base_server_service import BaseServerService
18
+ from xp.services.server.cp20_server_service import CP20ServerService
19
+ from xp.services.server.xp20_server_service import XP20ServerService
20
+ from xp.services.server.xp24_server_service import XP24ServerService
21
+ from xp.services.server.xp33_server_service import XP33ServerService
22
+ from xp.services.server.xp130_server_service import XP130ServerService
23
+ from xp.services.server.xp230_server_service import XP230ServerService
24
+
25
+
26
+ class DeviceServiceFactory:
27
+ """Factory for creating device service instances.
28
+
29
+ Encapsulates device creation logic and handles serializer injection
30
+ for different device types.
31
+ """
32
+
33
+ def __init__(
34
+ self,
35
+ xp20ms_serializer: Xp20MsActionTableSerializer,
36
+ xp24ms_serializer: Xp24MsActionTableSerializer,
37
+ xp33ms_serializer: Xp33MsActionTableSerializer,
38
+ ms_serializer: MsActionTableSerializer,
39
+ ):
40
+ """Initialize device service factory.
41
+
42
+ Args:
43
+ xp20ms_serializer: XP20 MsActionTable serializer (injected via DI).
44
+ xp24ms_serializer: XP24 MsActionTable serializer (injected via DI).
45
+ xp33ms_serializer: XP33 MsActionTable serializer (injected via DI).
46
+ ms_serializer: Generic MsActionTable serializer (injected via DI).
47
+ """
48
+ self.xp20ms_serializer = xp20ms_serializer
49
+ self.xp24ms_serializer = xp24ms_serializer
50
+ self.xp33ms_serializer = xp33ms_serializer
51
+ self.ms_serializer = ms_serializer
52
+
53
+ def create_device(self, module_type: str, serial_number: str) -> BaseServerService:
54
+ """Create device instance for given module type.
55
+
56
+ Args:
57
+ module_type: Module type code (e.g., "XP20", "XP33LR").
58
+ serial_number: Device serial number.
59
+
60
+ Returns:
61
+ Device service instance configured with appropriate serializer.
62
+
63
+ Raises:
64
+ ValueError: If module_type is unknown or unsupported.
65
+ """
66
+ # Map module types to their constructors and parameters
67
+ if module_type == "CP20":
68
+ return CP20ServerService(serial_number, "CP20", self.ms_serializer)
69
+
70
+ elif module_type == "XP24":
71
+ return XP24ServerService(serial_number, "XP24", self.xp24ms_serializer)
72
+
73
+ elif module_type == "XP33":
74
+ return XP33ServerService(serial_number, "XP33", self.xp33ms_serializer)
75
+
76
+ elif module_type == "XP33LR":
77
+ return XP33ServerService(serial_number, "XP33LR", self.xp33ms_serializer)
78
+
79
+ elif module_type == "XP33LED":
80
+ return XP33ServerService(serial_number, "XP33LED", self.xp33ms_serializer)
81
+
82
+ elif module_type == "XP20":
83
+ return XP20ServerService(serial_number, "XP20", self.xp20ms_serializer)
84
+
85
+ elif module_type == "XP130":
86
+ return XP130ServerService(serial_number, "XP130", self.ms_serializer)
87
+
88
+ elif module_type == "XP230":
89
+ return XP230ServerService(serial_number, "XP230", self.ms_serializer)
90
+
91
+ else:
92
+ raise ValueError(
93
+ f"Unknown device type '{module_type}' for serial {serial_number}"
94
+ )
@@ -8,19 +8,14 @@ import logging
8
8
  import socket
9
9
  import threading
10
10
  from pathlib import Path
11
- from typing import Dict, List, Optional, Union
11
+ from typing import Dict, List, Optional
12
12
 
13
13
  from xp.models.homekit.homekit_conson_config import (
14
14
  ConsonModuleConfig,
15
15
  ConsonModuleListConfig,
16
16
  )
17
17
  from xp.services.server.base_server_service import BaseServerService
18
- from xp.services.server.cp20_server_service import CP20ServerService
19
- from xp.services.server.xp20_server_service import XP20ServerService
20
- from xp.services.server.xp24_server_service import XP24ServerService
21
- from xp.services.server.xp33_server_service import XP33ServerService
22
- from xp.services.server.xp130_server_service import XP130ServerService
23
- from xp.services.server.xp230_server_service import XP230ServerService
18
+ from xp.services.server.device_service_factory import DeviceServiceFactory
24
19
  from xp.services.telegram.telegram_discover_service import TelegramDiscoverService
25
20
  from xp.services.telegram.telegram_service import TelegramService
26
21
 
@@ -43,6 +38,7 @@ class ServerService:
43
38
  self,
44
39
  telegram_service: TelegramService,
45
40
  discover_service: TelegramDiscoverService,
41
+ device_factory: DeviceServiceFactory,
46
42
  config_path: str = "server.yml",
47
43
  port: int = 10001,
48
44
  ):
@@ -51,25 +47,21 @@ class ServerService:
51
47
  Args:
52
48
  telegram_service: Service for parsing system telegrams.
53
49
  discover_service: Service for handling discover requests.
50
+ device_factory: Factory for creating device service instances (injected via DI).
54
51
  config_path: Path to the server configuration file.
55
52
  port: TCP port to listen on.
56
53
  """
57
54
  self.telegram_service = telegram_service
58
55
  self.discover_service = discover_service
56
+ self.device_factory = device_factory
59
57
  self.config_path = config_path
60
58
  self.port = port
61
59
  self.server_socket: Optional[socket.socket] = None
62
60
  self.is_running = False
63
61
  self.devices: List[ConsonModuleConfig] = []
64
- self.device_services: Dict[
65
- str,
66
- Union[
67
- BaseServerService,
68
- XP33ServerService,
69
- XP20ServerService,
70
- XP130ServerService,
71
- ],
72
- ] = {} # serial -> device service instance
62
+ self.device_services: Dict[str, BaseServerService] = (
63
+ {}
64
+ ) # serial -> device service instance
73
65
 
74
66
  # Collect device buffer to broadcast to client
75
67
  self.collector_thread: Optional[threading.Thread] = (
@@ -112,44 +104,14 @@ class ServerService:
112
104
  serial_number = module.serial_number
113
105
 
114
106
  try:
107
+ # Use factory to create device instance
108
+ self.device_services[serial_number] = self.device_factory.create_device(
109
+ module_type, serial_number
110
+ )
115
111
 
116
- # Serial number is already a string from config
117
- if module_type == "CP20":
118
- self.device_services[serial_number] = CP20ServerService(
119
- serial_number
120
- )
121
- if module_type == "XP24":
122
- self.device_services[serial_number] = XP24ServerService(
123
- serial_number
124
- )
125
- elif module_type == "XP33":
126
- self.device_services[serial_number] = XP33ServerService(
127
- serial_number, "XP33"
128
- )
129
- elif module_type == "XP33LR":
130
- self.device_services[serial_number] = XP33ServerService(
131
- serial_number, "XP33LR"
132
- )
133
- elif module_type == "XP33LED":
134
- self.device_services[serial_number] = XP33ServerService(
135
- serial_number, "XP33LED"
136
- )
137
- elif module_type == "XP20":
138
- self.device_services[serial_number] = XP20ServerService(
139
- serial_number
140
- )
141
- elif module_type == "XP130":
142
- self.device_services[serial_number] = XP130ServerService(
143
- serial_number
144
- )
145
- elif module_type == "XP230":
146
- self.device_services[serial_number] = XP230ServerService(
147
- serial_number
148
- )
149
- else:
150
- self.logger.warning(
151
- f"Unknown device type '{module_type}' for serial {serial_number}"
152
- )
112
+ except ValueError as e:
113
+ # Factory raises ValueError for unknown device types
114
+ self.logger.warning(str(e))
153
115
 
154
116
  except Exception as e:
155
117
  self.logger.error(
@@ -247,14 +209,11 @@ class ServerService:
247
209
  self.logger.debug(f"Sent buffer to {client_address}")
248
210
 
249
211
  # Receive data from client
250
- self.logger.debug(f"Receiving data {client_address}")
251
212
  data = None
252
213
  try:
253
214
  data = client_socket.recv(1024)
254
215
  except socket.timeout:
255
- self.logger.debug(
256
- f"Timeout receiving data {client_address} ({timeout})"
257
- )
216
+ pass
258
217
  finally:
259
218
  timeout -= 1
260
219
 
@@ -459,9 +418,6 @@ class ServerService:
459
418
  self.logger.info("Collector thread starting")
460
419
 
461
420
  while True:
462
- self.logger.debug(
463
- f"Collector thread collecting ({len(self.collector_buffer)})"
464
- )
465
421
  collected = 0
466
422
  for device_service in self.device_services.values():
467
423
  telegram_buffer = device_service.collect_telegram_buffer()
@@ -469,5 +425,4 @@ class ServerService:
469
425
  collected += len(telegram_buffer)
470
426
 
471
427
  # Wait a bit before checking again
472
- self.logger.debug(f"Collector thread collected ({collected})")
473
428
  self.collector_stop_event.wait(timeout=1)
@@ -5,9 +5,10 @@ including response generation and device configuration handling.
5
5
  XP130 is an Ethernet/TCPIP interface module.
6
6
  """
7
7
 
8
- from typing import Dict
8
+ from typing import Dict, Optional
9
9
 
10
10
  from xp.models import ModuleTypeCode
11
+ from xp.services.actiontable.msactiontable_serializer import MsActionTableSerializer
11
12
  from xp.services.server.base_server_service import BaseServerService
12
13
 
13
14
 
@@ -25,11 +26,18 @@ class XP130ServerService(BaseServerService):
25
26
  and implements XP130 telegram format for Ethernet/TCPIP interface module.
26
27
  """
27
28
 
28
- def __init__(self, serial_number: str):
29
+ def __init__(
30
+ self,
31
+ serial_number: str,
32
+ _variant: str = "",
33
+ _msactiontable_serializer: Optional[MsActionTableSerializer] = None,
34
+ ):
29
35
  """Initialize XP130 server service.
30
36
 
31
37
  Args:
32
38
  serial_number: The device serial number.
39
+ _variant: Reserved parameter for consistency (unused).
40
+ _msactiontable_serializer: Generic MsActionTable serializer (unused).
33
41
  """
34
42
  super().__init__(serial_number)
35
43
  self.device_type = "XP130"
@@ -4,9 +4,13 @@ This service provides XP20-specific device emulation functionality,
4
4
  including response generation and device configuration handling.
5
5
  """
6
6
 
7
- from typing import Dict
7
+ from typing import Dict, Optional
8
8
 
9
9
  from xp.models import ModuleTypeCode
10
+ from xp.models.actiontable.msactiontable_xp20 import Xp20MsActionTable
11
+ from xp.services.actiontable.msactiontable_xp20_serializer import (
12
+ Xp20MsActionTableSerializer,
13
+ )
10
14
  from xp.services.server.base_server_service import BaseServerService
11
15
 
12
16
 
@@ -24,17 +28,55 @@ class XP20ServerService(BaseServerService):
24
28
  and implements XP20 telegram format.
25
29
  """
26
30
 
27
- def __init__(self, serial_number: str):
31
+ def __init__(
32
+ self,
33
+ serial_number: str,
34
+ _variant: str = "",
35
+ msactiontable_serializer: Optional[Xp20MsActionTableSerializer] = None,
36
+ ):
28
37
  """Initialize XP20 server service.
29
38
 
30
39
  Args:
31
40
  serial_number: The device serial number.
41
+ _variant: Reserved parameter for consistency (unused).
42
+ msactiontable_serializer: MsActionTable serializer (injected via DI).
32
43
  """
33
44
  super().__init__(serial_number)
34
45
  self.device_type = "XP20"
35
46
  self.module_type_code = ModuleTypeCode.XP20 # XP20 module type from registry
36
47
  self.firmware_version = "XP20_V0.01.05"
37
48
 
49
+ # MsActionTable support
50
+ self.msactiontable_serializer = (
51
+ msactiontable_serializer or Xp20MsActionTableSerializer()
52
+ )
53
+ self.msactiontable = self._get_default_msactiontable()
54
+
55
+ def _get_msactiontable_serializer(self) -> Optional[Xp20MsActionTableSerializer]:
56
+ """Get the MsActionTable serializer for XP20.
57
+
58
+ Returns:
59
+ The XP20 MsActionTable serializer instance.
60
+ """
61
+ return self.msactiontable_serializer
62
+
63
+ def _get_msactiontable(self) -> Optional[Xp20MsActionTable]:
64
+ """Get the MsActionTable for XP20.
65
+
66
+ Returns:
67
+ The XP20 MsActionTable instance.
68
+ """
69
+ return self.msactiontable
70
+
71
+ def _get_default_msactiontable(self) -> Xp20MsActionTable:
72
+ """Generate default MsActionTable configuration.
73
+
74
+ Returns:
75
+ Default XP20 MsActionTable with all inputs unconfigured.
76
+ """
77
+ # All inputs unconfigured (all flags False, AND functions empty)
78
+ return Xp20MsActionTable()
79
+
38
80
  def get_device_info(self) -> Dict:
39
81
  """Get XP20 device information.
40
82
 
@@ -4,9 +4,10 @@ This service provides XP230-specific device emulation functionality,
4
4
  including response generation and device configuration handling.
5
5
  """
6
6
 
7
- from typing import Dict
7
+ from typing import Dict, Optional
8
8
 
9
9
  from xp.models import ModuleTypeCode
10
+ from xp.services.actiontable.msactiontable_serializer import MsActionTableSerializer
10
11
  from xp.services.server.base_server_service import BaseServerService
11
12
 
12
13
 
@@ -24,11 +25,18 @@ class XP230ServerService(BaseServerService):
24
25
  and implements XP230 telegram format.
25
26
  """
26
27
 
27
- def __init__(self, serial_number: str):
28
+ def __init__(
29
+ self,
30
+ serial_number: str,
31
+ _variant: str = "",
32
+ _msactiontable_serializer: Optional[MsActionTableSerializer] = None,
33
+ ):
28
34
  """Initialize XP230 server service.
29
35
 
30
36
  Args:
31
37
  serial_number: The device serial number.
38
+ _variant: Reserved parameter for consistency (unused).
39
+ _msactiontable_serializer: Generic MsActionTable serializer (unused).
32
40
  """
33
41
  super().__init__(serial_number)
34
42
  self.device_type = "XP230"
@@ -7,9 +7,15 @@ including response generation and device configuration handling.
7
7
  from typing import Dict, Optional
8
8
 
9
9
  from xp.models import ModuleTypeCode
10
+ from xp.models.actiontable.msactiontable_xp24 import InputAction, Xp24MsActionTable
10
11
  from xp.models.telegram.datapoint_type import DataPointType
12
+ from xp.models.telegram.input_action_type import InputActionType
11
13
  from xp.models.telegram.system_function import SystemFunction
12
14
  from xp.models.telegram.system_telegram import SystemTelegram
15
+ from xp.models.telegram.timeparam_type import TimeParam
16
+ from xp.services.actiontable.msactiontable_xp24_serializer import (
17
+ Xp24MsActionTableSerializer,
18
+ )
13
19
  from xp.services.server.base_server_service import BaseServerService
14
20
 
15
21
 
@@ -37,11 +43,18 @@ class XP24ServerService(BaseServerService):
37
43
  and implements XP24 telegram format.
38
44
  """
39
45
 
40
- def __init__(self, serial_number: str):
46
+ def __init__(
47
+ self,
48
+ serial_number: str,
49
+ _variant: str = "",
50
+ msactiontable_serializer: Optional[Xp24MsActionTableSerializer] = None,
51
+ ):
41
52
  """Initialize XP24 server service.
42
53
 
43
54
  Args:
44
55
  serial_number: The device serial number.
56
+ _variant: Reserved parameter for consistency (unused).
57
+ msactiontable_serializer: MsActionTable serializer (injected via DI).
45
58
  """
46
59
  super().__init__(serial_number)
47
60
  self.device_type = "XP24"
@@ -53,6 +66,12 @@ class XP24ServerService(BaseServerService):
53
66
  self.output_2: XP24Output = XP24Output()
54
67
  self.output_3: XP24Output = XP24Output()
55
68
 
69
+ # MsActionTable support
70
+ self.msactiontable_serializer = (
71
+ msactiontable_serializer or Xp24MsActionTableSerializer()
72
+ )
73
+ self.msactiontable = self._get_default_msactiontable()
74
+
56
75
  def _handle_device_specific_action_request(
57
76
  self, request: SystemTelegram
58
77
  ) -> Optional[str]:
@@ -175,6 +194,40 @@ class XP24ServerService(BaseServerService):
175
194
  f"{1 if self.output_3.state else 0}"
176
195
  )
177
196
 
197
+ def _get_msactiontable_serializer(self) -> Optional[Xp24MsActionTableSerializer]:
198
+ """Get the MsActionTable serializer for XP24.
199
+
200
+ Returns:
201
+ The XP24 MsActionTable serializer instance.
202
+ """
203
+ return self.msactiontable_serializer
204
+
205
+ def _get_msactiontable(self) -> Optional[Xp24MsActionTable]:
206
+ """Get the MsActionTable for XP24.
207
+
208
+ Returns:
209
+ The XP24 MsActionTable instance.
210
+ """
211
+ return self.msactiontable
212
+
213
+ def _get_default_msactiontable(self) -> Xp24MsActionTable:
214
+ """Generate default MsActionTable configuration.
215
+
216
+ Returns:
217
+ Default XP24 MsActionTable with all inputs set to VOID.
218
+ """
219
+ return Xp24MsActionTable(
220
+ input1_action=InputAction(type=InputActionType.VOID, param=TimeParam.NONE),
221
+ input2_action=InputAction(type=InputActionType.VOID, param=TimeParam.NONE),
222
+ input3_action=InputAction(type=InputActionType.VOID, param=TimeParam.NONE),
223
+ input4_action=InputAction(type=InputActionType.VOID, param=TimeParam.NONE),
224
+ mutex12=False,
225
+ mutex34=False,
226
+ curtain12=False,
227
+ curtain34=False,
228
+ mutual_deadtime=12, # MS300
229
+ )
230
+
178
231
  def get_device_info(self) -> Dict:
179
232
  """Get XP24 device information.
180
233
 
@@ -10,9 +10,13 @@ import threading
10
10
  from typing import Dict, Optional
11
11
 
12
12
  from xp.models import ModuleTypeCode
13
+ from xp.models.actiontable.msactiontable_xp33 import Xp33MsActionTable
13
14
  from xp.models.telegram.datapoint_type import DataPointType
14
15
  from xp.models.telegram.system_function import SystemFunction
15
16
  from xp.models.telegram.system_telegram import SystemTelegram
17
+ from xp.services.actiontable.msactiontable_xp33_serializer import (
18
+ Xp33MsActionTableSerializer,
19
+ )
16
20
  from xp.services.server.base_server_service import BaseServerService
17
21
 
18
22
 
@@ -30,12 +34,18 @@ class XP33ServerService(BaseServerService):
30
34
  and implements XP33 telegram format for 3-channel dimmer modules.
31
35
  """
32
36
 
33
- def __init__(self, serial_number: str, variant: str = "XP33LR"):
37
+ def __init__(
38
+ self,
39
+ serial_number: str,
40
+ variant: str = "XP33LR",
41
+ msactiontable_serializer: Optional[Xp33MsActionTableSerializer] = None,
42
+ ):
34
43
  """Initialize XP33 server service.
35
44
 
36
45
  Args:
37
46
  serial_number: The device serial number.
38
47
  variant: Device variant (XP33, XP33LR, or XP33LED).
48
+ msactiontable_serializer: MsActionTable serializer (injected via DI).
39
49
  """
40
50
  super().__init__(serial_number)
41
51
  self.variant = variant # XP33 or XP33LR or XP33LED
@@ -85,6 +95,12 @@ class XP33ServerService(BaseServerService):
85
95
  self.client_sockets_lock = threading.Lock() # Lock for socket set
86
96
  self.storm_packets_sent = 0 # Counter for packets sent during storm
87
97
 
98
+ # MsActionTable support
99
+ self.msactiontable_serializer = (
100
+ msactiontable_serializer or Xp33MsActionTableSerializer()
101
+ )
102
+ self.msactiontable = self._get_default_msactiontable()
103
+
88
104
  def _handle_device_specific_action_request(
89
105
  self, request: SystemTelegram
90
106
  ) -> Optional[str]:
@@ -449,6 +465,31 @@ class XP33ServerService(BaseServerService):
449
465
  return True
450
466
  return False
451
467
 
468
+ def _get_msactiontable_serializer(self) -> Optional[Xp33MsActionTableSerializer]:
469
+ """Get the MsActionTable serializer for XP33.
470
+
471
+ Returns:
472
+ The XP33 MsActionTable serializer instance.
473
+ """
474
+ return self.msactiontable_serializer
475
+
476
+ def _get_msactiontable(self) -> Optional[Xp33MsActionTable]:
477
+ """Get the MsActionTable for XP33.
478
+
479
+ Returns:
480
+ The XP33 MsActionTable instance.
481
+ """
482
+ return self.msactiontable
483
+
484
+ def _get_default_msactiontable(self) -> Xp33MsActionTable:
485
+ """Generate default MsActionTable configuration.
486
+
487
+ Returns:
488
+ Default XP33 MsActionTable with all outputs at 0-100% range, no scenes configured.
489
+ """
490
+ # All outputs at 0-100% range, no scenes configured
491
+ return Xp33MsActionTable()
492
+
452
493
  def get_device_info(self) -> Dict:
453
494
  """Get XP33 device information.
454
495
 
@@ -3,6 +3,7 @@
3
3
  This module provides telegram parsing functionality for event, system, and reply telegrams.
4
4
  """
5
5
 
6
+ import logging
6
7
  import re
7
8
  from typing import Union
8
9
 
@@ -48,7 +49,8 @@ class TelegramService:
48
49
 
49
50
  def __init__(self) -> None:
50
51
  """Initialize the telegram service."""
51
- pass
52
+ # Set up logging
53
+ self.logger = logging.getLogger(__name__)
52
54
 
53
55
  def parse_event_telegram(self, raw_telegram: str) -> EventTelegram:
54
56
  """Parse a raw telegram string into an EventTelegram object.
@@ -242,6 +244,7 @@ class TelegramService:
242
244
  raise TelegramParsingError("Empty telegram string")
243
245
 
244
246
  # Validate and parse using regex
247
+ self.logger.debug(f"Parsing reply telegram {raw_telegram}")
245
248
  match = self.REPLY_TELEGRAM_PATTERN.match(raw_telegram.strip())
246
249
  if not match:
247
250
  raise TelegramParsingError(f"Invalid reply telegram format: {raw_telegram}")
xp/utils/dependencies.py CHANGED
@@ -9,18 +9,19 @@ from twisted.internet.posixbase import PosixReactorBase
9
9
  from xp.models import ConbusClientConfig
10
10
  from xp.models.homekit.homekit_config import HomekitConfig
11
11
  from xp.models.homekit.homekit_conson_config import ConsonModuleListConfig
12
- from xp.services.conbus.actiontable.actiontable_serializer import ActionTableSerializer
13
- from xp.services.conbus.actiontable.actiontable_service import ActionTableService
14
- from xp.services.conbus.actiontable.msactiontable_service import MsActionTableService
15
- from xp.services.conbus.actiontable.msactiontable_xp20_serializer import (
12
+ from xp.services.actiontable.actiontable_serializer import ActionTableSerializer
13
+ from xp.services.actiontable.msactiontable_serializer import MsActionTableSerializer
14
+ from xp.services.actiontable.msactiontable_xp20_serializer import (
16
15
  Xp20MsActionTableSerializer,
17
16
  )
18
- from xp.services.conbus.actiontable.msactiontable_xp24_serializer import (
17
+ from xp.services.actiontable.msactiontable_xp24_serializer import (
19
18
  Xp24MsActionTableSerializer,
20
19
  )
21
- from xp.services.conbus.actiontable.msactiontable_xp33_serializer import (
20
+ from xp.services.actiontable.msactiontable_xp33_serializer import (
22
21
  Xp33MsActionTableSerializer,
23
22
  )
23
+ from xp.services.conbus.actiontable.actiontable_service import ActionTableService
24
+ from xp.services.conbus.actiontable.msactiontable_service import MsActionTableService
24
25
  from xp.services.conbus.conbus_blink_all_service import ConbusBlinkAllService
25
26
  from xp.services.conbus.conbus_blink_service import ConbusBlinkService
26
27
  from xp.services.conbus.conbus_custom_service import ConbusCustomService
@@ -49,6 +50,7 @@ from xp.services.module_type_service import ModuleTypeService
49
50
  from xp.services.protocol.protocol_factory import TelegramFactory
50
51
  from xp.services.protocol.telegram_protocol import TelegramProtocol
51
52
  from xp.services.reverse_proxy_service import ReverseProxyService
53
+ from xp.services.server.device_service_factory import DeviceServiceFactory
52
54
  from xp.services.server.server_service import ServerService
53
55
  from xp.services.telegram.telegram_blink_service import TelegramBlinkService
54
56
  from xp.services.telegram.telegram_discover_service import TelegramDiscoverService
@@ -324,12 +326,31 @@ class ServiceContainer:
324
326
  # Module type services layer
325
327
  self.container.register(ModuleTypeService, scope=punq.Scope.singleton)
326
328
 
329
+ # MsActionTable serializers
330
+ self.container.register(MsActionTableSerializer, scope=punq.Scope.singleton)
331
+ self.container.register(Xp20MsActionTableSerializer, scope=punq.Scope.singleton)
332
+ self.container.register(Xp24MsActionTableSerializer, scope=punq.Scope.singleton)
333
+ self.container.register(Xp33MsActionTableSerializer, scope=punq.Scope.singleton)
334
+
335
+ # Device service factory
336
+ self.container.register(
337
+ DeviceServiceFactory,
338
+ factory=lambda: DeviceServiceFactory(
339
+ xp20ms_serializer=self.container.resolve(Xp20MsActionTableSerializer),
340
+ xp24ms_serializer=self.container.resolve(Xp24MsActionTableSerializer),
341
+ xp33ms_serializer=self.container.resolve(Xp33MsActionTableSerializer),
342
+ ms_serializer=self.container.resolve(MsActionTableSerializer),
343
+ ),
344
+ scope=punq.Scope.singleton,
345
+ )
346
+
327
347
  # Server services layer
328
348
  self.container.register(
329
349
  ServerService,
330
350
  factory=lambda: ServerService(
331
351
  telegram_service=self.container.resolve(TelegramService),
332
352
  discover_service=self.container.resolve(TelegramDiscoverService),
353
+ device_factory=self.container.resolve(DeviceServiceFactory),
333
354
  config_path="server.yml",
334
355
  port=self._server_port,
335
356
  ),