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,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
|
xp/services/__init__.py
ADDED
|
@@ -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,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)
|