conson-xp 1.18.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 (176) hide show
  1. conson_xp-1.18.0.dist-info/METADATA +412 -0
  2. conson_xp-1.18.0.dist-info/RECORD +176 -0
  3. conson_xp-1.18.0.dist-info/WHEEL +4 -0
  4. conson_xp-1.18.0.dist-info/entry_points.txt +5 -0
  5. conson_xp-1.18.0.dist-info/licenses/LICENSE +29 -0
  6. xp/__init__.py +9 -0
  7. xp/cli/__init__.py +5 -0
  8. xp/cli/__main__.py +6 -0
  9. xp/cli/commands/__init__.py +153 -0
  10. xp/cli/commands/conbus/__init__.py +25 -0
  11. xp/cli/commands/conbus/conbus.py +128 -0
  12. xp/cli/commands/conbus/conbus_actiontable_commands.py +233 -0
  13. xp/cli/commands/conbus/conbus_autoreport_commands.py +108 -0
  14. xp/cli/commands/conbus/conbus_blink_commands.py +163 -0
  15. xp/cli/commands/conbus/conbus_config_commands.py +29 -0
  16. xp/cli/commands/conbus/conbus_custom_commands.py +57 -0
  17. xp/cli/commands/conbus/conbus_datapoint_commands.py +113 -0
  18. xp/cli/commands/conbus/conbus_discover_commands.py +61 -0
  19. xp/cli/commands/conbus/conbus_event_commands.py +81 -0
  20. xp/cli/commands/conbus/conbus_lightlevel_commands.py +207 -0
  21. xp/cli/commands/conbus/conbus_linknumber_commands.py +102 -0
  22. xp/cli/commands/conbus/conbus_modulenumber_commands.py +104 -0
  23. xp/cli/commands/conbus/conbus_msactiontable_commands.py +94 -0
  24. xp/cli/commands/conbus/conbus_output_commands.py +163 -0
  25. xp/cli/commands/conbus/conbus_raw_commands.py +62 -0
  26. xp/cli/commands/conbus/conbus_receive_commands.py +59 -0
  27. xp/cli/commands/conbus/conbus_scan_commands.py +58 -0
  28. xp/cli/commands/file_commands.py +186 -0
  29. xp/cli/commands/homekit/__init__.py +3 -0
  30. xp/cli/commands/homekit/homekit.py +118 -0
  31. xp/cli/commands/homekit/homekit_start_commands.py +43 -0
  32. xp/cli/commands/module_commands.py +187 -0
  33. xp/cli/commands/reverse_proxy_commands.py +178 -0
  34. xp/cli/commands/server/__init__.py +3 -0
  35. xp/cli/commands/server/server_commands.py +135 -0
  36. xp/cli/commands/telegram/__init__.py +5 -0
  37. xp/cli/commands/telegram/telegram.py +41 -0
  38. xp/cli/commands/telegram/telegram_blink_commands.py +79 -0
  39. xp/cli/commands/telegram/telegram_checksum_commands.py +112 -0
  40. xp/cli/commands/telegram/telegram_discover_commands.py +41 -0
  41. xp/cli/commands/telegram/telegram_linknumber_commands.py +86 -0
  42. xp/cli/commands/telegram/telegram_parse_commands.py +75 -0
  43. xp/cli/commands/telegram/telegram_version_commands.py +52 -0
  44. xp/cli/main.py +87 -0
  45. xp/cli/utils/__init__.py +1 -0
  46. xp/cli/utils/click_tree.py +57 -0
  47. xp/cli/utils/datapoint_type_choice.py +57 -0
  48. xp/cli/utils/decorators.py +351 -0
  49. xp/cli/utils/error_handlers.py +201 -0
  50. xp/cli/utils/formatters.py +312 -0
  51. xp/cli/utils/module_type_choice.py +56 -0
  52. xp/cli/utils/serial_number_type.py +52 -0
  53. xp/cli/utils/system_function_choice.py +57 -0
  54. xp/cli/utils/xp_module_type.py +53 -0
  55. xp/connection/__init__.py +13 -0
  56. xp/connection/exceptions.py +22 -0
  57. xp/models/__init__.py +36 -0
  58. xp/models/actiontable/__init__.py +1 -0
  59. xp/models/actiontable/actiontable.py +43 -0
  60. xp/models/actiontable/msactiontable_xp20.py +53 -0
  61. xp/models/actiontable/msactiontable_xp24.py +58 -0
  62. xp/models/actiontable/msactiontable_xp33.py +65 -0
  63. xp/models/conbus/__init__.py +1 -0
  64. xp/models/conbus/conbus.py +87 -0
  65. xp/models/conbus/conbus_autoreport.py +67 -0
  66. xp/models/conbus/conbus_blink.py +80 -0
  67. xp/models/conbus/conbus_client_config.py +55 -0
  68. xp/models/conbus/conbus_connection_status.py +40 -0
  69. xp/models/conbus/conbus_custom.py +58 -0
  70. xp/models/conbus/conbus_datapoint.py +89 -0
  71. xp/models/conbus/conbus_discover.py +64 -0
  72. xp/models/conbus/conbus_event_raw.py +47 -0
  73. xp/models/conbus/conbus_lightlevel.py +52 -0
  74. xp/models/conbus/conbus_linknumber.py +54 -0
  75. xp/models/conbus/conbus_output.py +57 -0
  76. xp/models/conbus/conbus_raw.py +45 -0
  77. xp/models/conbus/conbus_receive.py +42 -0
  78. xp/models/conbus/conbus_writeconfig.py +60 -0
  79. xp/models/homekit/__init__.py +1 -0
  80. xp/models/homekit/homekit_accessory.py +35 -0
  81. xp/models/homekit/homekit_config.py +106 -0
  82. xp/models/homekit/homekit_conson_config.py +86 -0
  83. xp/models/log_entry.py +130 -0
  84. xp/models/protocol/__init__.py +1 -0
  85. xp/models/protocol/conbus_protocol.py +312 -0
  86. xp/models/response.py +42 -0
  87. xp/models/telegram/__init__.py +1 -0
  88. xp/models/telegram/action_type.py +31 -0
  89. xp/models/telegram/datapoint_type.py +82 -0
  90. xp/models/telegram/event_telegram.py +140 -0
  91. xp/models/telegram/event_type.py +15 -0
  92. xp/models/telegram/input_action_type.py +69 -0
  93. xp/models/telegram/input_type.py +17 -0
  94. xp/models/telegram/module_type.py +188 -0
  95. xp/models/telegram/module_type_code.py +205 -0
  96. xp/models/telegram/output_telegram.py +103 -0
  97. xp/models/telegram/reply_telegram.py +297 -0
  98. xp/models/telegram/system_function.py +116 -0
  99. xp/models/telegram/system_telegram.py +94 -0
  100. xp/models/telegram/telegram.py +28 -0
  101. xp/models/telegram/telegram_type.py +19 -0
  102. xp/models/telegram/timeparam_type.py +51 -0
  103. xp/models/write_config_type.py +33 -0
  104. xp/services/__init__.py +26 -0
  105. xp/services/actiontable/__init__.py +1 -0
  106. xp/services/actiontable/actiontable_serializer.py +273 -0
  107. xp/services/actiontable/msactiontable_serializer.py +7 -0
  108. xp/services/actiontable/msactiontable_xp20_serializer.py +169 -0
  109. xp/services/actiontable/msactiontable_xp24_serializer.py +120 -0
  110. xp/services/actiontable/msactiontable_xp33_serializer.py +239 -0
  111. xp/services/conbus/__init__.py +1 -0
  112. xp/services/conbus/actiontable/__init__.py +1 -0
  113. xp/services/conbus/actiontable/actiontable_download_service.py +158 -0
  114. xp/services/conbus/actiontable/actiontable_list_service.py +91 -0
  115. xp/services/conbus/actiontable/actiontable_show_service.py +89 -0
  116. xp/services/conbus/actiontable/actiontable_upload_service.py +211 -0
  117. xp/services/conbus/actiontable/msactiontable_service.py +232 -0
  118. xp/services/conbus/conbus_blink_all_service.py +181 -0
  119. xp/services/conbus/conbus_blink_service.py +158 -0
  120. xp/services/conbus/conbus_custom_service.py +156 -0
  121. xp/services/conbus/conbus_datapoint_queryall_service.py +182 -0
  122. xp/services/conbus/conbus_datapoint_service.py +170 -0
  123. xp/services/conbus/conbus_discover_service.py +312 -0
  124. xp/services/conbus/conbus_event_raw_service.py +181 -0
  125. xp/services/conbus/conbus_output_service.py +194 -0
  126. xp/services/conbus/conbus_raw_service.py +122 -0
  127. xp/services/conbus/conbus_receive_service.py +115 -0
  128. xp/services/conbus/conbus_scan_service.py +150 -0
  129. xp/services/conbus/write_config_service.py +194 -0
  130. xp/services/homekit/__init__.py +1 -0
  131. xp/services/homekit/homekit_cache_service.py +307 -0
  132. xp/services/homekit/homekit_conbus_service.py +93 -0
  133. xp/services/homekit/homekit_config_validator.py +310 -0
  134. xp/services/homekit/homekit_conson_validator.py +121 -0
  135. xp/services/homekit/homekit_dimminglight.py +182 -0
  136. xp/services/homekit/homekit_dimminglight_service.py +148 -0
  137. xp/services/homekit/homekit_hap_service.py +342 -0
  138. xp/services/homekit/homekit_lightbulb.py +120 -0
  139. xp/services/homekit/homekit_lightbulb_service.py +86 -0
  140. xp/services/homekit/homekit_module_service.py +56 -0
  141. xp/services/homekit/homekit_outlet.py +168 -0
  142. xp/services/homekit/homekit_outlet_service.py +121 -0
  143. xp/services/homekit/homekit_service.py +359 -0
  144. xp/services/log_file_service.py +309 -0
  145. xp/services/module_type_service.py +257 -0
  146. xp/services/protocol/__init__.py +21 -0
  147. xp/services/protocol/conbus_event_protocol.py +360 -0
  148. xp/services/protocol/conbus_protocol.py +318 -0
  149. xp/services/protocol/protocol_factory.py +78 -0
  150. xp/services/protocol/telegram_protocol.py +264 -0
  151. xp/services/reverse_proxy_service.py +435 -0
  152. xp/services/server/__init__.py +1 -0
  153. xp/services/server/base_server_service.py +366 -0
  154. xp/services/server/cp20_server_service.py +65 -0
  155. xp/services/server/device_service_factory.py +94 -0
  156. xp/services/server/server_service.py +428 -0
  157. xp/services/server/xp130_server_service.py +67 -0
  158. xp/services/server/xp20_server_service.py +92 -0
  159. xp/services/server/xp230_server_service.py +58 -0
  160. xp/services/server/xp24_server_service.py +245 -0
  161. xp/services/server/xp33_server_service.py +535 -0
  162. xp/services/telegram/__init__.py +1 -0
  163. xp/services/telegram/telegram_blink_service.py +138 -0
  164. xp/services/telegram/telegram_checksum_service.py +149 -0
  165. xp/services/telegram/telegram_datapoint_service.py +82 -0
  166. xp/services/telegram/telegram_discover_service.py +277 -0
  167. xp/services/telegram/telegram_link_number_service.py +216 -0
  168. xp/services/telegram/telegram_output_service.py +322 -0
  169. xp/services/telegram/telegram_service.py +380 -0
  170. xp/services/telegram/telegram_version_service.py +288 -0
  171. xp/utils/__init__.py +12 -0
  172. xp/utils/checksum.py +61 -0
  173. xp/utils/dependencies.py +531 -0
  174. xp/utils/event_helper.py +31 -0
  175. xp/utils/serialization.py +205 -0
  176. xp/utils/time_utils.py +134 -0
@@ -0,0 +1,51 @@
1
+ """Time parameter enumeration for telegram actions."""
2
+
3
+ from enum import IntEnum
4
+
5
+
6
+ class TimeParam(IntEnum):
7
+ """Time parameter values for action timing.
8
+
9
+ Attributes:
10
+ NONE: No time parameter.
11
+ T05SEC: 0.5 second delay.
12
+ T1SEC: 1 second delay.
13
+ T2SEC: 2 second delay.
14
+ T5SEC: 5 second delay.
15
+ T10SEC: 10 second delay.
16
+ T15SEC: 15 second delay.
17
+ T20SEC: 20 second delay.
18
+ T30SEC: 30 second delay.
19
+ T45SEC: 45 second delay.
20
+ T1MIN: 1 minute delay.
21
+ T2MIN: 2 minute delay.
22
+ T5MIN: 5 minute delay.
23
+ T10MIN: 10 minute delay.
24
+ T15MIN: 15 minute delay.
25
+ T20MIN: 20 minute delay.
26
+ T30MIN: 30 minute delay.
27
+ T45MIN: 45 minute delay.
28
+ T60MIN: 60 minute delay.
29
+ T120MIN: 120 minute delay.
30
+ """
31
+
32
+ NONE = 0
33
+ T05SEC = 1
34
+ T1SEC = 2
35
+ T2SEC = 3
36
+ T5SEC = 4
37
+ T10SEC = 5
38
+ T15SEC = 6
39
+ T20SEC = 7
40
+ T30SEC = 8
41
+ T45SEC = 9
42
+ T1MIN = 10
43
+ T2MIN = 11
44
+ T5MIN = 12
45
+ T10MIN = 13
46
+ T15MIN = 14
47
+ T20MIN = 15
48
+ T30MIN = 16
49
+ T45MIN = 17
50
+ T60MIN = 18
51
+ T120MIN = 19
@@ -0,0 +1,33 @@
1
+ """Write config type enumeration."""
2
+
3
+ from enum import Enum
4
+ from typing import Optional
5
+
6
+
7
+ class WriteConfigType(str, Enum):
8
+ """Write Config types for system telegrams.
9
+
10
+ Attributes:
11
+ LINK_NUMBER: Link number configuration (code 04).
12
+ MODULE_NUMBER: Module number configuration (code 05).
13
+ SYSTEM_TYPE: System type configuration (code 06).
14
+ """
15
+
16
+ LINK_NUMBER = "04"
17
+ MODULE_NUMBER = "05"
18
+ SYSTEM_TYPE = "06" # 00 CP, 01 XP, 02 MIXED
19
+
20
+ @classmethod
21
+ def from_code(cls, code: str) -> Optional["WriteConfigType"]:
22
+ """Get WriteConfigType from code string.
23
+
24
+ Args:
25
+ code: Configuration type code string.
26
+
27
+ Returns:
28
+ WriteConfigType instance if found, None otherwise.
29
+ """
30
+ for dp_type in cls:
31
+ if dp_type.value == code:
32
+ return dp_type
33
+ return None
@@ -0,0 +1,26 @@
1
+ """Service layer for XP CLI tool."""
2
+
3
+ from xp.services.log_file_service import LogFileParsingError, LogFileService
4
+ from xp.services.module_type_service import ModuleTypeNotFoundError, ModuleTypeService
5
+ from xp.services.telegram.telegram_discover_service import (
6
+ DiscoverError,
7
+ TelegramDiscoverService,
8
+ )
9
+ from xp.services.telegram.telegram_link_number_service import (
10
+ LinkNumberError,
11
+ LinkNumberService,
12
+ )
13
+ from xp.services.telegram.telegram_service import TelegramParsingError, TelegramService
14
+
15
+ __all__ = [
16
+ "TelegramService",
17
+ "TelegramParsingError",
18
+ "ModuleTypeService",
19
+ "ModuleTypeNotFoundError",
20
+ "LogFileService",
21
+ "LogFileParsingError",
22
+ "LinkNumberService",
23
+ "LinkNumberError",
24
+ "TelegramDiscoverService",
25
+ "DiscoverError",
26
+ ]
@@ -0,0 +1 @@
1
+ """Action table utils."""
@@ -0,0 +1,273 @@
1
+ """Serializer for ActionTable telegram encoding/decoding."""
2
+
3
+ import re
4
+
5
+ from xp.models import ModuleTypeCode
6
+ from xp.models.actiontable.actiontable import ActionTable, ActionTableEntry
7
+ from xp.models.telegram.input_action_type import InputActionType
8
+ from xp.models.telegram.timeparam_type import TimeParam
9
+ from xp.utils.serialization import (
10
+ byte_to_unsigned,
11
+ de_bcd,
12
+ de_nibbles,
13
+ highest_bit_set,
14
+ lower3,
15
+ nibbles,
16
+ remove_highest_bit,
17
+ to_bcd,
18
+ upper5,
19
+ )
20
+
21
+
22
+ class ActionTableSerializer:
23
+ """Handles serialization/deserialization of ActionTable to/from telegrams.
24
+
25
+ Attributes:
26
+ MAX_ENTRIES: Maximum number of entries in an ActionTable (96).
27
+ """
28
+
29
+ MAX_ENTRIES = 96 # ActionTable must always contain exactly 96 entries
30
+
31
+ @staticmethod
32
+ def from_data(data: bytes) -> ActionTable:
33
+ """Deserialize telegram data to ActionTable.
34
+
35
+ Args:
36
+ data: Raw byte data from telegram
37
+
38
+ Returns:
39
+ Decoded ActionTable
40
+ """
41
+ entries = []
42
+
43
+ # Process data in 5-byte chunks
44
+ for i in range(0, len(data), 5):
45
+ if i + 4 >= len(data):
46
+ break
47
+
48
+ # Extract fields from 5-byte chunk
49
+ module_type_raw = de_bcd(data[i])
50
+ link_number = de_bcd(data[i + 1])
51
+ module_input = de_bcd(data[i + 2])
52
+
53
+ # Extract output and command from byte 3
54
+ module_output = lower3(data[i + 3])
55
+ command_raw = upper5(data[i + 3])
56
+
57
+ parameter_raw = byte_to_unsigned(data[i + 4])
58
+ parameter_raw = remove_highest_bit(parameter_raw)
59
+
60
+ inverted = False
61
+ if highest_bit_set(data[i + 4]):
62
+ inverted = True
63
+
64
+ # Map raw values to enum types
65
+ try:
66
+ module_type = ModuleTypeCode(module_type_raw)
67
+ except ValueError:
68
+ module_type = ModuleTypeCode.NOMOD # Default fallback
69
+
70
+ try:
71
+ command = InputActionType(command_raw)
72
+ except ValueError:
73
+ command = InputActionType.OFF # Default fallback
74
+
75
+ try:
76
+ parameter = TimeParam(parameter_raw)
77
+ except ValueError:
78
+ parameter = TimeParam.NONE # Default fallback
79
+
80
+ if module_type != ModuleTypeCode.NOMOD:
81
+ entry = ActionTableEntry(
82
+ module_type=module_type,
83
+ link_number=link_number,
84
+ module_input=module_input,
85
+ module_output=module_output,
86
+ command=command,
87
+ parameter=parameter,
88
+ inverted=inverted,
89
+ )
90
+ entries.append(entry)
91
+
92
+ return ActionTable(entries=entries)
93
+
94
+ @staticmethod
95
+ def to_data(action_table: ActionTable) -> bytes:
96
+ """Serialize ActionTable to telegram byte data.
97
+
98
+ Args:
99
+ action_table: ActionTable to serialize
100
+
101
+ Returns:
102
+ Raw byte data for telegram (always 480 bytes for 96 entries)
103
+ """
104
+ data = bytearray()
105
+
106
+ for entry in action_table.entries:
107
+ # Encode each entry as 5 bytes
108
+ type_byte = to_bcd(entry.module_type.value)
109
+ link_byte = to_bcd(entry.link_number)
110
+ input_byte = to_bcd(entry.module_input)
111
+
112
+ # Combine output (lower 3 bits) and command (upper 5 bits)
113
+ output_command_byte = (entry.module_output & 0x07) | (
114
+ (entry.command.value & 0x1F) << 3
115
+ )
116
+
117
+ parameter_byte = entry.parameter.value
118
+
119
+ data.extend(
120
+ [type_byte, link_byte, input_byte, output_command_byte, parameter_byte]
121
+ )
122
+
123
+ # Pad to 96 entries with default NOMOD entries (00 00 00 00 00)
124
+ current_entries = len(action_table.entries)
125
+ if current_entries < ActionTableSerializer.MAX_ENTRIES:
126
+ # Default entry: NOMOD 0 0 > 0 OFF (all zeros)
127
+ padding_bytes = [0x00, 0x00, 0x00, 0x00, 0x00]
128
+ for _ in range(ActionTableSerializer.MAX_ENTRIES - current_entries):
129
+ data.extend(padding_bytes)
130
+
131
+ return bytes(data)
132
+
133
+ @staticmethod
134
+ def to_encoded_string(action_table: ActionTable) -> str:
135
+ """Convert ActionTable to base64-encoded string format.
136
+
137
+ Args:
138
+ action_table: ActionTable to encode
139
+
140
+ Returns:
141
+ Base64-encoded string representation
142
+ """
143
+ data = ActionTableSerializer.to_data(action_table)
144
+ return nibbles(data)
145
+
146
+ @staticmethod
147
+ def from_encoded_string(encoded_data: str) -> ActionTable:
148
+ """Convert base64-encoded string to ActionTable.
149
+
150
+ Args:
151
+ encoded_data: Base64-encoded string
152
+
153
+ Returns:
154
+ Decoded ActionTable
155
+ """
156
+ data = de_nibbles(encoded_data)
157
+ return ActionTableSerializer.from_data(data)
158
+
159
+ @staticmethod
160
+ def format_decoded_output(action_table: ActionTable) -> list[str]:
161
+ """Format ActionTable as human-readable decoded output.
162
+
163
+ Args:
164
+ action_table: ActionTable to format
165
+
166
+ Returns:
167
+ List of human-readable string representations
168
+ """
169
+ lines = []
170
+ for entry in action_table.entries:
171
+ # Format: CP20 0 0 > 1 OFF [param];
172
+ module_type = entry.module_type.name
173
+ link = entry.link_number
174
+ input_num = entry.module_input
175
+ output = entry.module_output
176
+ command = entry.command.name
177
+
178
+ # Add prefix for inverted commands
179
+ if entry.inverted:
180
+ command = f"~{command}"
181
+
182
+ # Build base line
183
+ line = f"{module_type} {link} {input_num} > {output} {command}"
184
+
185
+ # Add parameter if present and non-zero
186
+ if entry.parameter is not None and entry.parameter.value != 0:
187
+ line += f" {entry.parameter.value}"
188
+
189
+ # Add semicolon terminator
190
+ line += ";"
191
+
192
+ lines.append(line)
193
+
194
+ return lines
195
+
196
+ @staticmethod
197
+ def parse_action_string(action_str: str) -> ActionTableEntry:
198
+ """Parse action table entry from string format.
199
+
200
+ Args:
201
+ action_str: String in format "CP20 0 0 > 1 OFF" or "CP20 0 1 > 1 ~ON"
202
+
203
+ Returns:
204
+ Parsed ActionTableEntry
205
+
206
+ Raises:
207
+ ValueError: If string format is invalid
208
+ """
209
+ # Remove trailing semicolon if present
210
+ action_str = action_str.strip().rstrip(";")
211
+
212
+ # Pattern: <Type> <Link> <Input> > <Output> <Command> [Parameter]
213
+ pattern = r"^(\w+)\s+(\d+)\s+(\d+)\s+>\s+(\d+)\s+(~?)(\w+)(?:\s+(\d+))?$"
214
+ match = re.match(pattern, action_str)
215
+
216
+ if not match:
217
+ raise ValueError(f"Invalid action table format: {action_str}")
218
+
219
+ (
220
+ module_type_str,
221
+ link_str,
222
+ input_str,
223
+ output_str,
224
+ inverted_str,
225
+ command_str,
226
+ parameter_str,
227
+ ) = match.groups()
228
+
229
+ # Parse module type
230
+ try:
231
+ module_type = ModuleTypeCode[module_type_str]
232
+ except KeyError:
233
+ raise ValueError(f"Invalid module type: {module_type_str}")
234
+
235
+ # Parse command
236
+ try:
237
+ command = InputActionType[command_str]
238
+ except KeyError:
239
+ raise ValueError(f"Invalid command: {command_str}")
240
+
241
+ # Parse parameter (default to NONE)
242
+ parameter = TimeParam.NONE
243
+ if parameter_str:
244
+ try:
245
+ parameter = TimeParam(int(parameter_str))
246
+ except ValueError:
247
+ raise ValueError(f"Invalid parameter: {parameter_str}")
248
+
249
+ return ActionTableEntry(
250
+ module_type=module_type,
251
+ link_number=int(link_str),
252
+ module_input=int(input_str),
253
+ module_output=int(output_str),
254
+ command=command,
255
+ parameter=parameter,
256
+ inverted=bool(inverted_str),
257
+ )
258
+
259
+ @staticmethod
260
+ def parse_action_table(action_strings: list[str]) -> ActionTable:
261
+ """Parse action table from list of string entries.
262
+
263
+ Args:
264
+ action_strings: List of action strings from conson.yml
265
+
266
+ Returns:
267
+ Parsed ActionTable
268
+ """
269
+ entries = [
270
+ ActionTableSerializer.parse_action_string(action_str)
271
+ for action_str in action_strings
272
+ ]
273
+ return ActionTable(entries=entries)
@@ -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
@@ -0,0 +1,169 @@
1
+ """Serializer for XP20 Action Table telegram encoding/decoding."""
2
+
3
+ from xp.models.actiontable.msactiontable_xp20 import InputChannel, Xp20MsActionTable
4
+ from xp.utils.serialization import byte_to_bits, de_nibbles, nibbles
5
+
6
+ # Index constants for clarity in implementation
7
+ SHORT_LONG_INDEX: int = 0
8
+ GROUP_ON_OFF_INDEX: int = 1
9
+ INVERT_INDEX: int = 2
10
+ AND_FUNCTIONS_INDEX: int = 3 # starts at 3, uses indices 3-10
11
+ SA_FUNCTION_INDEX: int = 11
12
+ TA_FUNCTION_INDEX: int = 12
13
+
14
+
15
+ class Xp20MsActionTableSerializer:
16
+ """Handles serialization/deserialization of XP20 action tables to/from telegrams."""
17
+
18
+ @staticmethod
19
+ def to_data(action_table: Xp20MsActionTable) -> str:
20
+ """Serialize XP20 action table to telegram hex string format.
21
+
22
+ Args:
23
+ action_table: XP20 action table to serialize
24
+
25
+ Returns:
26
+ 64-character hex string (32 bytes) with A-P nibble encoding
27
+ """
28
+ # Initialize 32-byte raw data array
29
+ raw_bytes = bytearray(32)
30
+
31
+ # Get all input channels
32
+ input_channels = [
33
+ action_table.input1,
34
+ action_table.input2,
35
+ action_table.input3,
36
+ action_table.input4,
37
+ action_table.input5,
38
+ action_table.input6,
39
+ action_table.input7,
40
+ action_table.input8,
41
+ ]
42
+
43
+ # Encode each input channel
44
+ for input_index, input_channel in enumerate(input_channels):
45
+ Xp20MsActionTableSerializer._encode_input_channel(
46
+ input_channel, input_index, raw_bytes
47
+ )
48
+
49
+ encoded_data = nibbles(raw_bytes)
50
+ # Convert raw bytes to hex string with A-P encoding
51
+ return "AAAA" + encoded_data
52
+
53
+ @staticmethod
54
+ def from_data(msactiontable_rawdata: str) -> Xp20MsActionTable:
55
+ """Deserialize telegram data to XP20 action table.
56
+
57
+ Args:
58
+ msactiontable_rawdata: 64-character hex string with A-P encoding
59
+
60
+ Returns:
61
+ Decoded XP20 action table
62
+
63
+ Raises:
64
+ ValueError: If input length is not 64 characters
65
+ """
66
+ raw_length = len(msactiontable_rawdata)
67
+ if raw_length < 68: # Minimum: 4 char prefix + 64 chars data
68
+ raise ValueError(
69
+ f"XP20 action table data must be 68 characters long, got {len(msactiontable_rawdata)}"
70
+ )
71
+
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)
80
+
81
+ # Decode input channels
82
+ input_channels = []
83
+ for input_index in range(8):
84
+ input_channel = Xp20MsActionTableSerializer._decode_input_channel(
85
+ raw_bytes, input_index
86
+ )
87
+ input_channels.append(input_channel)
88
+
89
+ # Create and return XP20 action table
90
+ return Xp20MsActionTable(
91
+ input1=input_channels[0],
92
+ input2=input_channels[1],
93
+ input3=input_channels[2],
94
+ input4=input_channels[3],
95
+ input5=input_channels[4],
96
+ input6=input_channels[5],
97
+ input7=input_channels[6],
98
+ input8=input_channels[7],
99
+ )
100
+
101
+ @staticmethod
102
+ def _decode_input_channel(raw_bytes: bytearray, input_index: int) -> InputChannel:
103
+ """Extract input channel configuration from raw bytes.
104
+
105
+ Args:
106
+ raw_bytes: Raw byte array from telegram
107
+ input_index: Input channel index (0-7)
108
+
109
+ Returns:
110
+ Decoded input channel configuration
111
+ """
112
+ # Extract bit flags from appropriate offsets
113
+ short_long_flags = byte_to_bits(raw_bytes[SHORT_LONG_INDEX])
114
+ group_on_off_flags = byte_to_bits(raw_bytes[GROUP_ON_OFF_INDEX])
115
+ invert_flags = byte_to_bits(raw_bytes[INVERT_INDEX])
116
+ sa_function_flags = byte_to_bits(raw_bytes[SA_FUNCTION_INDEX])
117
+ ta_function_flags = byte_to_bits(raw_bytes[TA_FUNCTION_INDEX])
118
+
119
+ # Extract AND functions for this input (full byte)
120
+ and_functions_byte = raw_bytes[AND_FUNCTIONS_INDEX + input_index]
121
+ and_functions = byte_to_bits(and_functions_byte)
122
+
123
+ # Create and return input channel
124
+ return InputChannel(
125
+ invert=invert_flags[input_index],
126
+ short_long=short_long_flags[input_index],
127
+ group_on_off=group_on_off_flags[input_index],
128
+ and_functions=and_functions,
129
+ sa_function=sa_function_flags[input_index],
130
+ ta_function=ta_function_flags[input_index],
131
+ )
132
+
133
+ @staticmethod
134
+ def _encode_input_channel(
135
+ input_channel: InputChannel, input_index: int, raw_bytes: bytearray
136
+ ) -> None:
137
+ """Encode input channel configuration into raw bytes.
138
+
139
+ Args:
140
+ input_channel: Input channel configuration to encode
141
+ input_index: Input channel index (0-7)
142
+ raw_bytes: Raw byte array to modify
143
+ """
144
+ # Set bit flags at appropriate positions
145
+ if input_channel.short_long:
146
+ raw_bytes[SHORT_LONG_INDEX] |= 1 << input_index
147
+
148
+ if input_channel.group_on_off:
149
+ raw_bytes[GROUP_ON_OFF_INDEX] |= 1 << input_index
150
+
151
+ if input_channel.invert:
152
+ raw_bytes[INVERT_INDEX] |= 1 << input_index
153
+
154
+ if input_channel.sa_function:
155
+ raw_bytes[SA_FUNCTION_INDEX] |= 1 << input_index
156
+
157
+ if input_channel.ta_function:
158
+ raw_bytes[TA_FUNCTION_INDEX] |= 1 << input_index
159
+
160
+ # Encode AND functions (ensure we have exactly 8 bits)
161
+ and_functions = input_channel.and_functions or [False] * 8
162
+ and_functions_byte = 0
163
+ for bit_index, bit_value in enumerate(
164
+ and_functions[:8]
165
+ ): # Take only first 8 bits
166
+ if bit_value:
167
+ and_functions_byte |= 1 << bit_index
168
+
169
+ raw_bytes[AND_FUNCTIONS_INDEX + input_index] = and_functions_byte
@@ -0,0 +1,120 @@
1
+ """Serializer for XP24 Action Table telegram encoding/decoding."""
2
+
3
+ from xp.models.actiontable.msactiontable_xp24 import InputAction, Xp24MsActionTable
4
+ from xp.models.telegram.input_action_type import InputActionType
5
+ from xp.models.telegram.timeparam_type import TimeParam
6
+ from xp.utils.serialization import de_nibbles, nibbles
7
+
8
+
9
+ class Xp24MsActionTableSerializer:
10
+ """Handles serialization/deserialization of XP24 action tables to/from telegrams."""
11
+
12
+ @staticmethod
13
+ def to_data(action_table: Xp24MsActionTable) -> str:
14
+ """Serialize action table to telegram format.
15
+
16
+ Args:
17
+ action_table: XP24 MS action table to serialize.
18
+
19
+ Returns:
20
+ Serialized action table data string (68 characters).
21
+ """
22
+ # Build byte array for the action table (32 bytes total)
23
+ raw_bytes = bytearray()
24
+
25
+ # Encode all 4 input actions (2 bytes each = 8 bytes total)
26
+ input_actions = [
27
+ action_table.input1_action,
28
+ action_table.input2_action,
29
+ action_table.input3_action,
30
+ action_table.input4_action,
31
+ ]
32
+
33
+ for action in input_actions:
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))
49
+
50
+ # Prepend action table count "AAAA" (4 chars) -> total 68 chars
51
+ return "AAAA" + encoded_data
52
+
53
+ @staticmethod
54
+ def from_data(msactiontable_rawdata: str) -> Xp24MsActionTable:
55
+ """Deserialize action table from raw data parts.
56
+
57
+ Args:
58
+ msactiontable_rawdata: Raw action table data string.
59
+
60
+ Returns:
61
+ Deserialized XP24 MS action table.
62
+
63
+ Raises:
64
+ ValueError: If data length is not 68 bytes.
65
+ """
66
+ raw_length = len(msactiontable_rawdata)
67
+ if raw_length != 68:
68
+ raise ValueError(
69
+ f"Msactiontable is not 68 bytes long ({raw_length}): {msactiontable_rawdata}"
70
+ )
71
+
72
+ # Remove action table count AAAA, AAAB .
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)
80
+
81
+ # Decode input actions from positions 0-3 (2 bytes each)
82
+ input_actions = []
83
+ for pos in range(4):
84
+ input_action = Xp24MsActionTableSerializer._decode_input_action(
85
+ raw_bytes, pos
86
+ )
87
+ input_actions.append(input_action)
88
+
89
+ action_table = Xp24MsActionTable(
90
+ input1_action=input_actions[0],
91
+ input2_action=input_actions[1],
92
+ input3_action=input_actions[2],
93
+ input4_action=input_actions[3],
94
+ mutex12=raw_bytes[8] != 0, # With A-P encoding: AA=0 (False), AB=1 (True)
95
+ mutex34=raw_bytes[9] != 0,
96
+ mutual_deadtime=raw_bytes[10],
97
+ curtain12=raw_bytes[11] != 0,
98
+ curtain34=raw_bytes[12] != 0,
99
+ )
100
+ return action_table
101
+
102
+ @staticmethod
103
+ def _decode_input_action(raw_bytes: bytearray, pos: int) -> InputAction:
104
+ """Decode input action from raw bytes.
105
+
106
+ Args:
107
+ raw_bytes: Raw byte array containing action data.
108
+ pos: Position of the action to decode.
109
+
110
+ Returns:
111
+ Decoded input action.
112
+ """
113
+ function_id = raw_bytes[2 * pos]
114
+ param_id = raw_bytes[2 * pos + 1]
115
+
116
+ # Convert function ID to InputActionType
117
+ action_type = InputActionType(function_id)
118
+ param_type = TimeParam(param_id)
119
+
120
+ return InputAction(action_type, param_type)