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.
- conson_xp-1.18.0.dist-info/METADATA +412 -0
- conson_xp-1.18.0.dist-info/RECORD +176 -0
- conson_xp-1.18.0.dist-info/WHEEL +4 -0
- conson_xp-1.18.0.dist-info/entry_points.txt +5 -0
- conson_xp-1.18.0.dist-info/licenses/LICENSE +29 -0
- xp/__init__.py +9 -0
- xp/cli/__init__.py +5 -0
- xp/cli/__main__.py +6 -0
- xp/cli/commands/__init__.py +153 -0
- xp/cli/commands/conbus/__init__.py +25 -0
- xp/cli/commands/conbus/conbus.py +128 -0
- xp/cli/commands/conbus/conbus_actiontable_commands.py +233 -0
- xp/cli/commands/conbus/conbus_autoreport_commands.py +108 -0
- xp/cli/commands/conbus/conbus_blink_commands.py +163 -0
- xp/cli/commands/conbus/conbus_config_commands.py +29 -0
- xp/cli/commands/conbus/conbus_custom_commands.py +57 -0
- xp/cli/commands/conbus/conbus_datapoint_commands.py +113 -0
- xp/cli/commands/conbus/conbus_discover_commands.py +61 -0
- xp/cli/commands/conbus/conbus_event_commands.py +81 -0
- xp/cli/commands/conbus/conbus_lightlevel_commands.py +207 -0
- xp/cli/commands/conbus/conbus_linknumber_commands.py +102 -0
- xp/cli/commands/conbus/conbus_modulenumber_commands.py +104 -0
- xp/cli/commands/conbus/conbus_msactiontable_commands.py +94 -0
- xp/cli/commands/conbus/conbus_output_commands.py +163 -0
- xp/cli/commands/conbus/conbus_raw_commands.py +62 -0
- xp/cli/commands/conbus/conbus_receive_commands.py +59 -0
- xp/cli/commands/conbus/conbus_scan_commands.py +58 -0
- xp/cli/commands/file_commands.py +186 -0
- xp/cli/commands/homekit/__init__.py +3 -0
- xp/cli/commands/homekit/homekit.py +118 -0
- xp/cli/commands/homekit/homekit_start_commands.py +43 -0
- xp/cli/commands/module_commands.py +187 -0
- xp/cli/commands/reverse_proxy_commands.py +178 -0
- xp/cli/commands/server/__init__.py +3 -0
- xp/cli/commands/server/server_commands.py +135 -0
- xp/cli/commands/telegram/__init__.py +5 -0
- xp/cli/commands/telegram/telegram.py +41 -0
- xp/cli/commands/telegram/telegram_blink_commands.py +79 -0
- xp/cli/commands/telegram/telegram_checksum_commands.py +112 -0
- xp/cli/commands/telegram/telegram_discover_commands.py +41 -0
- xp/cli/commands/telegram/telegram_linknumber_commands.py +86 -0
- xp/cli/commands/telegram/telegram_parse_commands.py +75 -0
- xp/cli/commands/telegram/telegram_version_commands.py +52 -0
- xp/cli/main.py +87 -0
- xp/cli/utils/__init__.py +1 -0
- xp/cli/utils/click_tree.py +57 -0
- xp/cli/utils/datapoint_type_choice.py +57 -0
- xp/cli/utils/decorators.py +351 -0
- xp/cli/utils/error_handlers.py +201 -0
- xp/cli/utils/formatters.py +312 -0
- xp/cli/utils/module_type_choice.py +56 -0
- xp/cli/utils/serial_number_type.py +52 -0
- xp/cli/utils/system_function_choice.py +57 -0
- xp/cli/utils/xp_module_type.py +53 -0
- xp/connection/__init__.py +13 -0
- xp/connection/exceptions.py +22 -0
- xp/models/__init__.py +36 -0
- xp/models/actiontable/__init__.py +1 -0
- xp/models/actiontable/actiontable.py +43 -0
- xp/models/actiontable/msactiontable_xp20.py +53 -0
- xp/models/actiontable/msactiontable_xp24.py +58 -0
- xp/models/actiontable/msactiontable_xp33.py +65 -0
- xp/models/conbus/__init__.py +1 -0
- xp/models/conbus/conbus.py +87 -0
- xp/models/conbus/conbus_autoreport.py +67 -0
- xp/models/conbus/conbus_blink.py +80 -0
- xp/models/conbus/conbus_client_config.py +55 -0
- xp/models/conbus/conbus_connection_status.py +40 -0
- xp/models/conbus/conbus_custom.py +58 -0
- xp/models/conbus/conbus_datapoint.py +89 -0
- xp/models/conbus/conbus_discover.py +64 -0
- xp/models/conbus/conbus_event_raw.py +47 -0
- xp/models/conbus/conbus_lightlevel.py +52 -0
- xp/models/conbus/conbus_linknumber.py +54 -0
- xp/models/conbus/conbus_output.py +57 -0
- xp/models/conbus/conbus_raw.py +45 -0
- xp/models/conbus/conbus_receive.py +42 -0
- xp/models/conbus/conbus_writeconfig.py +60 -0
- xp/models/homekit/__init__.py +1 -0
- xp/models/homekit/homekit_accessory.py +35 -0
- xp/models/homekit/homekit_config.py +106 -0
- xp/models/homekit/homekit_conson_config.py +86 -0
- xp/models/log_entry.py +130 -0
- xp/models/protocol/__init__.py +1 -0
- xp/models/protocol/conbus_protocol.py +312 -0
- xp/models/response.py +42 -0
- xp/models/telegram/__init__.py +1 -0
- xp/models/telegram/action_type.py +31 -0
- xp/models/telegram/datapoint_type.py +82 -0
- xp/models/telegram/event_telegram.py +140 -0
- xp/models/telegram/event_type.py +15 -0
- xp/models/telegram/input_action_type.py +69 -0
- xp/models/telegram/input_type.py +17 -0
- xp/models/telegram/module_type.py +188 -0
- xp/models/telegram/module_type_code.py +205 -0
- xp/models/telegram/output_telegram.py +103 -0
- xp/models/telegram/reply_telegram.py +297 -0
- xp/models/telegram/system_function.py +116 -0
- xp/models/telegram/system_telegram.py +94 -0
- xp/models/telegram/telegram.py +28 -0
- xp/models/telegram/telegram_type.py +19 -0
- xp/models/telegram/timeparam_type.py +51 -0
- xp/models/write_config_type.py +33 -0
- xp/services/__init__.py +26 -0
- xp/services/actiontable/__init__.py +1 -0
- xp/services/actiontable/actiontable_serializer.py +273 -0
- xp/services/actiontable/msactiontable_serializer.py +7 -0
- xp/services/actiontable/msactiontable_xp20_serializer.py +169 -0
- xp/services/actiontable/msactiontable_xp24_serializer.py +120 -0
- xp/services/actiontable/msactiontable_xp33_serializer.py +239 -0
- xp/services/conbus/__init__.py +1 -0
- xp/services/conbus/actiontable/__init__.py +1 -0
- xp/services/conbus/actiontable/actiontable_download_service.py +158 -0
- xp/services/conbus/actiontable/actiontable_list_service.py +91 -0
- xp/services/conbus/actiontable/actiontable_show_service.py +89 -0
- xp/services/conbus/actiontable/actiontable_upload_service.py +211 -0
- xp/services/conbus/actiontable/msactiontable_service.py +232 -0
- xp/services/conbus/conbus_blink_all_service.py +181 -0
- xp/services/conbus/conbus_blink_service.py +158 -0
- xp/services/conbus/conbus_custom_service.py +156 -0
- xp/services/conbus/conbus_datapoint_queryall_service.py +182 -0
- xp/services/conbus/conbus_datapoint_service.py +170 -0
- xp/services/conbus/conbus_discover_service.py +312 -0
- xp/services/conbus/conbus_event_raw_service.py +181 -0
- xp/services/conbus/conbus_output_service.py +194 -0
- xp/services/conbus/conbus_raw_service.py +122 -0
- xp/services/conbus/conbus_receive_service.py +115 -0
- xp/services/conbus/conbus_scan_service.py +150 -0
- xp/services/conbus/write_config_service.py +194 -0
- xp/services/homekit/__init__.py +1 -0
- xp/services/homekit/homekit_cache_service.py +307 -0
- xp/services/homekit/homekit_conbus_service.py +93 -0
- xp/services/homekit/homekit_config_validator.py +310 -0
- xp/services/homekit/homekit_conson_validator.py +121 -0
- xp/services/homekit/homekit_dimminglight.py +182 -0
- xp/services/homekit/homekit_dimminglight_service.py +148 -0
- xp/services/homekit/homekit_hap_service.py +342 -0
- xp/services/homekit/homekit_lightbulb.py +120 -0
- xp/services/homekit/homekit_lightbulb_service.py +86 -0
- xp/services/homekit/homekit_module_service.py +56 -0
- xp/services/homekit/homekit_outlet.py +168 -0
- xp/services/homekit/homekit_outlet_service.py +121 -0
- xp/services/homekit/homekit_service.py +359 -0
- xp/services/log_file_service.py +309 -0
- xp/services/module_type_service.py +257 -0
- xp/services/protocol/__init__.py +21 -0
- xp/services/protocol/conbus_event_protocol.py +360 -0
- xp/services/protocol/conbus_protocol.py +318 -0
- xp/services/protocol/protocol_factory.py +78 -0
- xp/services/protocol/telegram_protocol.py +264 -0
- xp/services/reverse_proxy_service.py +435 -0
- xp/services/server/__init__.py +1 -0
- xp/services/server/base_server_service.py +366 -0
- xp/services/server/cp20_server_service.py +65 -0
- xp/services/server/device_service_factory.py +94 -0
- xp/services/server/server_service.py +428 -0
- xp/services/server/xp130_server_service.py +67 -0
- xp/services/server/xp20_server_service.py +92 -0
- xp/services/server/xp230_server_service.py +58 -0
- xp/services/server/xp24_server_service.py +245 -0
- xp/services/server/xp33_server_service.py +535 -0
- xp/services/telegram/__init__.py +1 -0
- xp/services/telegram/telegram_blink_service.py +138 -0
- xp/services/telegram/telegram_checksum_service.py +149 -0
- xp/services/telegram/telegram_datapoint_service.py +82 -0
- xp/services/telegram/telegram_discover_service.py +277 -0
- xp/services/telegram/telegram_link_number_service.py +216 -0
- xp/services/telegram/telegram_output_service.py +322 -0
- xp/services/telegram/telegram_service.py +380 -0
- xp/services/telegram/telegram_version_service.py +288 -0
- xp/utils/__init__.py +12 -0
- xp/utils/checksum.py +61 -0
- xp/utils/dependencies.py +531 -0
- xp/utils/event_helper.py +31 -0
- xp/utils/serialization.py +205 -0
- 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)
|