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,239 @@
1
+ """Serializer for XP33 Action Table telegram encoding/decoding."""
2
+
3
+ from xp.models.actiontable.msactiontable_xp33 import (
4
+ Xp33MsActionTable,
5
+ Xp33Output,
6
+ Xp33Scene,
7
+ )
8
+ from xp.models.telegram.timeparam_type import TimeParam
9
+ from xp.utils.serialization import bits_to_byte, byte_to_bits, de_nibbles, nibbles
10
+
11
+
12
+ class Xp33MsActionTableSerializer:
13
+ """Handles serialization/deserialization of XP33 action tables to/from telegrams."""
14
+
15
+ @staticmethod
16
+ def _percentage_to_byte(percentage: int) -> int:
17
+ """Convert percentage (0-100) to byte value for telegram encoding."""
18
+ return min(max(percentage, 0), 100)
19
+
20
+ @staticmethod
21
+ def _byte_to_percentage(byte_val: int) -> int:
22
+ """Convert byte value from telegram to percentage (0-100)."""
23
+ return min(max(byte_val, 0), 100)
24
+
25
+ @staticmethod
26
+ def _time_param_to_byte(time_param: TimeParam) -> int:
27
+ """Convert TimeParam enum to byte value for telegram encoding."""
28
+ return time_param.value
29
+
30
+ @staticmethod
31
+ def _byte_to_time_param(byte_val: int) -> TimeParam:
32
+ """Convert byte value from telegram to TimeParam enum."""
33
+ try:
34
+ return TimeParam(byte_val)
35
+ except ValueError:
36
+ return TimeParam.NONE
37
+
38
+ @staticmethod
39
+ def to_data(action_table: Xp33MsActionTable) -> str:
40
+ """Serialize action table to telegram format.
41
+
42
+ Args:
43
+ action_table: XP33 MS action table to serialize.
44
+
45
+ Returns:
46
+ Serialized action table data string.
47
+ """
48
+ # Create 32-byte array
49
+ raw_bytes = bytearray(32)
50
+
51
+ # Encode output min/max levels (bytes 0-5)
52
+ outputs = [action_table.output1, action_table.output2, action_table.output3]
53
+ for i, output in enumerate(outputs):
54
+ raw_bytes[2 * i] = Xp33MsActionTableSerializer._percentage_to_byte(
55
+ output.min_level
56
+ )
57
+ raw_bytes[2 * i + 1] = Xp33MsActionTableSerializer._percentage_to_byte(
58
+ output.max_level
59
+ )
60
+
61
+ # Encode scenes (bytes 6-21)
62
+ scenes = [
63
+ action_table.scene1,
64
+ action_table.scene2,
65
+ action_table.scene3,
66
+ action_table.scene4,
67
+ ]
68
+ for scene_idx, scene in enumerate(scenes):
69
+ offset = 6 + (4 * scene_idx)
70
+ raw_bytes[offset] = Xp33MsActionTableSerializer._time_param_to_byte(
71
+ scene.time
72
+ )
73
+ raw_bytes[offset + 1] = Xp33MsActionTableSerializer._percentage_to_byte(
74
+ scene.output1_level
75
+ )
76
+ raw_bytes[offset + 2] = Xp33MsActionTableSerializer._percentage_to_byte(
77
+ scene.output2_level
78
+ )
79
+ raw_bytes[offset + 3] = Xp33MsActionTableSerializer._percentage_to_byte(
80
+ scene.output3_level
81
+ )
82
+
83
+ # Encode bit flags (bytes 22-24)
84
+ scene_outputs_bits = [False] * 8
85
+ start_at_full_bits = [False] * 8
86
+ leading_edge_bits = [False] * 8
87
+
88
+ for i, output in enumerate(outputs):
89
+ if i < 3: # Only 3 outputs
90
+ scene_outputs_bits[i] = output.scene_outputs
91
+ start_at_full_bits[i] = output.start_at_full
92
+ leading_edge_bits[i] = output.leading_edge
93
+
94
+ raw_bytes[22] = bits_to_byte(scene_outputs_bits)
95
+ raw_bytes[23] = bits_to_byte(start_at_full_bits)
96
+ raw_bytes[24] = bits_to_byte(leading_edge_bits)
97
+
98
+ # Bytes 25-31 are padding (already 0)
99
+ # Convert to hex string using nibble encoding
100
+ encoded_data = nibbles(raw_bytes)
101
+
102
+ # Convert raw bytes to hex string with A-P encoding
103
+ return "AAAA" + encoded_data
104
+
105
+ @staticmethod
106
+ def from_data(msactiontable_rawdata: str) -> Xp33MsActionTable:
107
+ """Deserialize action table from raw data parts.
108
+
109
+ Args:
110
+ msactiontable_rawdata: Raw action table data string.
111
+
112
+ Returns:
113
+ Deserialized XP33 MS action table.
114
+
115
+ Raises:
116
+ ValueError: If data length is less than 68 characters.
117
+ """
118
+ raw_length = len(msactiontable_rawdata)
119
+ if raw_length < 68: # Minimum: 4 char prefix + 64 chars data
120
+ raise ValueError(
121
+ f"Msactiontable is too short ({raw_length}), minimum 68 characters required"
122
+ )
123
+
124
+ # Remove action table count prefix (first 4 characters: AAAA, AAAB, etc.)
125
+ data = msactiontable_rawdata[4:]
126
+
127
+ # Take first 64 chars (32 bytes) as per pseudocode
128
+ hex_data = data[:64]
129
+
130
+ # Convert hex string to bytes using deNibble (A-P encoding)
131
+ raw_bytes = de_nibbles(hex_data)
132
+
133
+ # Decode outputs
134
+ output1 = Xp33MsActionTableSerializer._decode_output(raw_bytes, 0)
135
+ output2 = Xp33MsActionTableSerializer._decode_output(raw_bytes, 1)
136
+ output3 = Xp33MsActionTableSerializer._decode_output(raw_bytes, 2)
137
+
138
+ # Decode scenes
139
+ scene1 = Xp33MsActionTableSerializer._decode_scene(raw_bytes, 0)
140
+ scene2 = Xp33MsActionTableSerializer._decode_scene(raw_bytes, 1)
141
+ scene3 = Xp33MsActionTableSerializer._decode_scene(raw_bytes, 2)
142
+ scene4 = Xp33MsActionTableSerializer._decode_scene(raw_bytes, 3)
143
+
144
+ return Xp33MsActionTable(
145
+ output1=output1,
146
+ output2=output2,
147
+ output3=output3,
148
+ scene1=scene1,
149
+ scene2=scene2,
150
+ scene3=scene3,
151
+ scene4=scene4,
152
+ )
153
+
154
+ @staticmethod
155
+ def _decode_output(raw_bytes: bytearray, output_index: int) -> Xp33Output:
156
+ """Extract output configuration from raw bytes.
157
+
158
+ Args:
159
+ raw_bytes: Raw byte array containing output data.
160
+ output_index: Index of the output to decode.
161
+
162
+ Returns:
163
+ Decoded XP33 output configuration.
164
+ """
165
+ # Read min/max levels from appropriate offsets
166
+ min_level = Xp33MsActionTableSerializer._byte_to_percentage(
167
+ raw_bytes[2 * output_index]
168
+ )
169
+ max_level = Xp33MsActionTableSerializer._byte_to_percentage(
170
+ raw_bytes[2 * output_index + 1]
171
+ )
172
+
173
+ # Extract bit flags from bytes 22-24
174
+ scene_outputs_bits = byte_to_bits(raw_bytes[22])
175
+ start_at_full_bits = byte_to_bits(raw_bytes[23])
176
+
177
+ # Handle dimFunction with exception handling as per specification
178
+ if len(raw_bytes) > 24:
179
+ leading_edge_bits = byte_to_bits(raw_bytes[24])
180
+ else:
181
+ leading_edge_bits = [False] * 8
182
+
183
+ # Map bit flags to output properties
184
+ scene_outputs = (
185
+ scene_outputs_bits[output_index]
186
+ if output_index < len(scene_outputs_bits)
187
+ else False
188
+ )
189
+ start_at_full = (
190
+ start_at_full_bits[output_index]
191
+ if output_index < len(start_at_full_bits)
192
+ else False
193
+ )
194
+ leading_edge = (
195
+ leading_edge_bits[output_index]
196
+ if output_index < len(leading_edge_bits)
197
+ else False
198
+ )
199
+
200
+ return Xp33Output(
201
+ min_level=min_level,
202
+ max_level=max_level,
203
+ scene_outputs=scene_outputs,
204
+ start_at_full=start_at_full,
205
+ leading_edge=leading_edge,
206
+ )
207
+
208
+ @staticmethod
209
+ def _decode_scene(raw_bytes: bytearray, scene_index: int) -> Xp33Scene:
210
+ """Extract scene configuration from raw bytes.
211
+
212
+ Args:
213
+ raw_bytes: Raw byte array containing scene data.
214
+ scene_index: Index of the scene to decode.
215
+
216
+ Returns:
217
+ Decoded XP33 scene configuration.
218
+ """
219
+ # Calculate scene offset: 6 + (4 * scene_index)
220
+ offset = 6 + (4 * scene_index)
221
+
222
+ # Parse time parameter and output levels
223
+ time_param = Xp33MsActionTableSerializer._byte_to_time_param(raw_bytes[offset])
224
+ output1_level = Xp33MsActionTableSerializer._byte_to_percentage(
225
+ raw_bytes[offset + 1]
226
+ )
227
+ output2_level = Xp33MsActionTableSerializer._byte_to_percentage(
228
+ raw_bytes[offset + 2]
229
+ )
230
+ output3_level = Xp33MsActionTableSerializer._byte_to_percentage(
231
+ raw_bytes[offset + 3]
232
+ )
233
+
234
+ return Xp33Scene(
235
+ output1_level=output1_level,
236
+ output2_level=output2_level,
237
+ output3_level=output3_level,
238
+ time=time_param,
239
+ )
@@ -0,0 +1 @@
1
+ """Conbus service layer."""
@@ -0,0 +1 @@
1
+ """Action table services for Conbus."""
@@ -0,0 +1,158 @@
1
+ """Service for downloading ActionTable via Conbus protocol."""
2
+
3
+ import logging
4
+ from dataclasses import asdict
5
+ from typing import Any, Callable, Dict, Optional
6
+
7
+ from twisted.internet.posixbase import PosixReactorBase
8
+
9
+ from xp.models import ConbusClientConfig
10
+ from xp.models.actiontable.actiontable import ActionTable
11
+ from xp.models.protocol.conbus_protocol import TelegramReceivedEvent
12
+ from xp.models.telegram.system_function import SystemFunction
13
+ from xp.models.telegram.telegram_type import TelegramType
14
+ from xp.services.actiontable.actiontable_serializer import ActionTableSerializer
15
+ from xp.services.protocol import ConbusProtocol
16
+ from xp.services.telegram.telegram_service import TelegramService
17
+
18
+
19
+ class ActionTableService(ConbusProtocol):
20
+ """TCP client service for downloading action tables from Conbus modules.
21
+
22
+ Manages TCP socket connections, handles telegram generation and transmission,
23
+ and processes server responses for action table downloads.
24
+ """
25
+
26
+ def __init__(
27
+ self,
28
+ cli_config: ConbusClientConfig,
29
+ reactor: PosixReactorBase,
30
+ actiontable_serializer: ActionTableSerializer,
31
+ telegram_service: TelegramService,
32
+ ) -> None:
33
+ """Initialize the action table download service.
34
+
35
+ Args:
36
+ cli_config: Conbus client configuration.
37
+ reactor: Twisted reactor instance.
38
+ actiontable_serializer: Action table serializer.
39
+ telegram_service: Telegram service for parsing.
40
+ """
41
+ super().__init__(cli_config, reactor)
42
+ self.serializer = actiontable_serializer
43
+ self.telegram_service = telegram_service
44
+ self.serial_number: str = ""
45
+ self.progress_callback: Optional[Callable[[str], None]] = None
46
+ self.error_callback: Optional[Callable[[str], None]] = None
47
+ self.finish_callback: Optional[
48
+ Callable[[ActionTable, Dict[str, Any], list[str]], None]
49
+ ] = None
50
+
51
+ self.actiontable_data: list[str] = []
52
+ # Set up logging
53
+ self.logger = logging.getLogger(__name__)
54
+
55
+ def connection_established(self) -> None:
56
+ """Handle connection established event."""
57
+ self.logger.debug(
58
+ "Connection established, sending download actiontable telegram"
59
+ )
60
+ self.send_telegram(
61
+ telegram_type=TelegramType.SYSTEM,
62
+ serial_number=self.serial_number,
63
+ system_function=SystemFunction.DOWNLOAD_ACTIONTABLE,
64
+ data_value="00",
65
+ )
66
+
67
+ def telegram_sent(self, telegram_sent: str) -> None:
68
+ """Handle telegram sent event.
69
+
70
+ Args:
71
+ telegram_sent: The telegram that was sent.
72
+ """
73
+ self.logger.debug(f"Telegram sent: {telegram_sent}")
74
+
75
+ def telegram_received(self, telegram_received: TelegramReceivedEvent) -> None:
76
+ """Handle telegram received event.
77
+
78
+ Args:
79
+ telegram_received: The telegram received event.
80
+ """
81
+ self.logger.debug(f"Telegram received: {telegram_received}")
82
+ if (
83
+ not telegram_received.checksum_valid
84
+ or telegram_received.telegram_type != TelegramType.REPLY.value
85
+ or telegram_received.serial_number != self.serial_number
86
+ ):
87
+ self.logger.debug("Not a reply response")
88
+ return
89
+
90
+ reply_telegram = self.telegram_service.parse_reply_telegram(
91
+ telegram_received.frame
92
+ )
93
+ if reply_telegram.system_function not in (
94
+ SystemFunction.ACTIONTABLE,
95
+ SystemFunction.EOF,
96
+ ):
97
+ self.logger.debug("Not a actiontable response")
98
+ return
99
+
100
+ if reply_telegram.system_function == SystemFunction.ACTIONTABLE:
101
+ self.logger.debug("Saving actiontable response")
102
+ data_part = reply_telegram.data_value[2:]
103
+ self.actiontable_data.append(data_part)
104
+ if self.progress_callback:
105
+ self.progress_callback(".")
106
+
107
+ self.send_telegram(
108
+ telegram_type=TelegramType.SYSTEM,
109
+ serial_number=self.serial_number,
110
+ system_function=SystemFunction.ACK,
111
+ data_value="00",
112
+ )
113
+ return
114
+
115
+ if reply_telegram.system_function == SystemFunction.EOF:
116
+ all_data = "".join(self.actiontable_data)
117
+ # Deserialize from received data
118
+ actiontable = self.serializer.from_encoded_string(all_data)
119
+ actiontable_dict = asdict(actiontable)
120
+ actiontable_short = self.serializer.format_decoded_output(actiontable)
121
+ if self.finish_callback:
122
+ self.finish_callback(actiontable, actiontable_dict, actiontable_short)
123
+
124
+ def failed(self, message: str) -> None:
125
+ """Handle failed connection event.
126
+
127
+ Args:
128
+ message: Failure message.
129
+ """
130
+ self.logger.debug(f"Failed: {message}")
131
+ if self.error_callback:
132
+ self.error_callback(message)
133
+
134
+ def start(
135
+ self,
136
+ serial_number: str,
137
+ progress_callback: Callable[[str], None],
138
+ error_callback: Callable[[str], None],
139
+ finish_callback: Callable[[ActionTable, Dict[str, Any], list[str]], None],
140
+ timeout_seconds: Optional[float] = None,
141
+ ) -> None:
142
+ """Run reactor in dedicated thread with its own event loop.
143
+
144
+ Args:
145
+ serial_number: Module serial number.
146
+ progress_callback: Callback for progress updates.
147
+ error_callback: Callback for errors.
148
+ finish_callback: Callback when download completes.
149
+ timeout_seconds: Optional timeout in seconds.
150
+ """
151
+ self.logger.info("Starting actiontable")
152
+ self.serial_number = serial_number
153
+ if timeout_seconds:
154
+ self.timeout_seconds = timeout_seconds
155
+ self.progress_callback = progress_callback
156
+ self.error_callback = error_callback
157
+ self.finish_callback = finish_callback
158
+ self.start_reactor()
@@ -0,0 +1,91 @@
1
+ """Service for listing modules with action table configurations from conson.yml."""
2
+
3
+ import logging
4
+ from pathlib import Path
5
+ from typing import Any, Callable, Optional
6
+
7
+
8
+ class ActionTableListService:
9
+ """Service for listing modules with action table configurations.
10
+
11
+ Reads conson.yml and returns a list of all modules that have action table
12
+ configurations defined.
13
+ """
14
+
15
+ def __init__(self) -> None:
16
+ """Initialize the action table list service."""
17
+ self.logger = logging.getLogger(__name__)
18
+ self.finish_callback: Optional[Callable[[dict[str, Any]], None]] = None
19
+ self.error_callback: Optional[Callable[[str], None]] = None
20
+
21
+ def __enter__(self) -> "ActionTableListService":
22
+ """Context manager entry.
23
+
24
+ Returns:
25
+ Self for context manager use.
26
+ """
27
+ return self
28
+
29
+ def __exit__(self, _exc_type: Any, _exc_val: Any, _exc_tb: Any) -> None:
30
+ """Context manager exit."""
31
+ pass
32
+
33
+ def start(
34
+ self,
35
+ finish_callback: Callable[[dict[str, Any]], None],
36
+ error_callback: Callable[[str], None],
37
+ config_path: Optional[Path] = None,
38
+ ) -> None:
39
+ """List all modules with action table configurations.
40
+
41
+ Args:
42
+ finish_callback: Callback to invoke with the module list.
43
+ error_callback: Callback to invoke on error.
44
+ config_path: Optional path to conson.yml. Defaults to current directory.
45
+ """
46
+ self.finish_callback = finish_callback
47
+ self.error_callback = error_callback
48
+
49
+ # Default to current directory if not specified
50
+ if config_path is None:
51
+ config_path = Path.cwd() / "conson.yml"
52
+
53
+ # Check if config file exists
54
+ if not config_path.exists():
55
+ self._handle_error("Error: conson.yml not found in current directory")
56
+ return
57
+
58
+ # Load configuration
59
+ try:
60
+ from xp.models.homekit.homekit_conson_config import ConsonModuleListConfig
61
+
62
+ config = ConsonModuleListConfig.from_yaml(str(config_path))
63
+ except Exception as e:
64
+ self.logger.error(f"Failed to load conson.yml: {e}")
65
+ self._handle_error(f"Error: Failed to load conson.yml: {e}")
66
+ return
67
+
68
+ # Filter modules that have action_table configured
69
+ modules_with_actiontable = [
70
+ {
71
+ "serial_number": module.serial_number,
72
+ "module_type": module.module_type,
73
+ }
74
+ for module in config.root
75
+ ]
76
+
77
+ # Prepare result
78
+ result = {"modules": modules_with_actiontable}
79
+
80
+ # Invoke callback
81
+ if self.finish_callback is not None:
82
+ self.finish_callback(result)
83
+
84
+ def _handle_error(self, message: str) -> None:
85
+ """Handle error and invoke error callback.
86
+
87
+ Args:
88
+ message: Error message.
89
+ """
90
+ if self.error_callback is not None:
91
+ self.error_callback(message)
@@ -0,0 +1,89 @@
1
+ """Service for showing action table configuration for a specific module."""
2
+
3
+ import logging
4
+ from pathlib import Path
5
+ from typing import Any, Callable, Optional
6
+
7
+ from xp.models.homekit.homekit_conson_config import ConsonModuleConfig
8
+
9
+
10
+ class ActionTableShowService:
11
+ """Service for showing action table configuration for a specific module.
12
+
13
+ Reads conson.yml and returns the action table configuration for the specified
14
+ module serial number.
15
+ """
16
+
17
+ def __init__(self) -> None:
18
+ """Initialize the action table show service."""
19
+ self.logger = logging.getLogger(__name__)
20
+ self.finish_callback: Optional[Callable[[ConsonModuleConfig], None]] = None
21
+ self.error_callback: Optional[Callable[[str], None]] = None
22
+
23
+ def __enter__(self) -> "ActionTableShowService":
24
+ """Context manager entry.
25
+
26
+ Returns:
27
+ Self for context manager use.
28
+ """
29
+ return self
30
+
31
+ def __exit__(self, _exc_type: Any, _exc_val: Any, _exc_tb: Any) -> None:
32
+ """Context manager exit."""
33
+ pass
34
+
35
+ def start(
36
+ self,
37
+ serial_number: str,
38
+ finish_callback: Callable[[ConsonModuleConfig], None],
39
+ error_callback: Callable[[str], None],
40
+ config_path: Optional[Path] = None,
41
+ ) -> None:
42
+ """Show action table configuration for a specific module.
43
+
44
+ Args:
45
+ serial_number: Module serial number.
46
+ finish_callback: Callback to invoke with the module configuration.
47
+ error_callback: Callback to invoke on error.
48
+ config_path: Optional path to conson.yml. Defaults to current directory.
49
+ """
50
+ self.finish_callback = finish_callback
51
+ self.error_callback = error_callback
52
+
53
+ # Default to current directory if not specified
54
+ if config_path is None:
55
+ config_path = Path.cwd() / "conson.yml"
56
+
57
+ # Check if config file exists
58
+ if not config_path.exists():
59
+ self._handle_error("Error: conson.yml not found in current directory")
60
+ return
61
+
62
+ # Load configuration
63
+ try:
64
+ from xp.models.homekit.homekit_conson_config import ConsonModuleListConfig
65
+
66
+ config = ConsonModuleListConfig.from_yaml(str(config_path))
67
+ except Exception as e:
68
+ self.logger.error(f"Failed to load conson.yml: {e}")
69
+ self._handle_error(f"Error: Failed to load conson.yml: {e}")
70
+ return
71
+
72
+ # Find module
73
+ module = config.find_module(serial_number)
74
+ if not module:
75
+ self._handle_error(f"Error: Module {serial_number} not found in conson.yml")
76
+ return
77
+
78
+ # Invoke callback
79
+ if self.finish_callback is not None:
80
+ self.finish_callback(module)
81
+
82
+ def _handle_error(self, message: str) -> None:
83
+ """Handle error and invoke error callback.
84
+
85
+ Args:
86
+ message: Error message.
87
+ """
88
+ if self.error_callback is not None:
89
+ self.error_callback(message)