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,103 @@
|
|
|
1
|
+
"""XP output telegram model for console bus communication.
|
|
2
|
+
|
|
3
|
+
XP output telegrams are used for controlling relay inputs on XP modules.
|
|
4
|
+
Each XP24 module has 4 inputs (0-3) that can be pressed or released.
|
|
5
|
+
Each XP33 module has 3 inputs (0-2) that can be pressed or released.
|
|
6
|
+
Each XP31 module has 1 inputs (0-0) that can be pressed or released.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
from xp.models.telegram.action_type import ActionType
|
|
14
|
+
from xp.models.telegram.system_function import SystemFunction
|
|
15
|
+
from xp.models.telegram.telegram import Telegram
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class OutputTelegram(Telegram):
|
|
20
|
+
"""Represent a parsed XP output telegram from the console bus.
|
|
21
|
+
|
|
22
|
+
Format: <S{serial_number}F27D{input:02d}{action}{checksum}>
|
|
23
|
+
Examples: <S0012345008F27D00AAFN>
|
|
24
|
+
|
|
25
|
+
Attributes:
|
|
26
|
+
serial_number: Serial number of the device.
|
|
27
|
+
output_number: Output number (0-3 for XP24, 0-2 for XP33, 0 for XP31).
|
|
28
|
+
action_type: Type of action to perform.
|
|
29
|
+
system_function: System function code.
|
|
30
|
+
action_description: Human-readable action description.
|
|
31
|
+
input_description: Human-readable input description.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
serial_number: str = ""
|
|
35
|
+
output_number: Optional[int] = (
|
|
36
|
+
None # 0-3 for XP24 modules, 0-2 for XP33, 0 for XP31
|
|
37
|
+
)
|
|
38
|
+
action_type: Optional[ActionType] = None
|
|
39
|
+
system_function: SystemFunction = SystemFunction.ACTION
|
|
40
|
+
|
|
41
|
+
def __post_init__(self) -> None:
|
|
42
|
+
"""Initialize timestamp if not provided."""
|
|
43
|
+
if self.timestamp is None:
|
|
44
|
+
self.timestamp = datetime.now()
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def action_description(self) -> str:
|
|
48
|
+
"""Get human-readable action description.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Human-readable description of the action.
|
|
52
|
+
"""
|
|
53
|
+
descriptions = {
|
|
54
|
+
ActionType.OFF_PRESS: "Press (Make)",
|
|
55
|
+
ActionType.ON_RELEASE: "Release (Break)",
|
|
56
|
+
}
|
|
57
|
+
return (
|
|
58
|
+
descriptions.get(self.action_type, "Unknown Action")
|
|
59
|
+
if self.action_type
|
|
60
|
+
else "Unknown Action"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def input_description(self) -> str:
|
|
65
|
+
"""Get human-readable input description.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Description of the input/output number.
|
|
69
|
+
"""
|
|
70
|
+
return f"Input {self.output_number}"
|
|
71
|
+
|
|
72
|
+
def to_dict(self) -> dict:
|
|
73
|
+
"""Convert to dictionary for JSON serialization.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Dictionary representation of the output telegram.
|
|
77
|
+
"""
|
|
78
|
+
return {
|
|
79
|
+
"serial_number": self.serial_number,
|
|
80
|
+
"system_function": self.system_function,
|
|
81
|
+
"output_number": self.output_number,
|
|
82
|
+
"input_description": self.input_description,
|
|
83
|
+
"action_type": {
|
|
84
|
+
"code": self.action_type.value if self.action_type else None,
|
|
85
|
+
"description": self.action_description,
|
|
86
|
+
},
|
|
87
|
+
"checksum": self.checksum,
|
|
88
|
+
"checksum_validated": self.checksum_validated,
|
|
89
|
+
"raw_telegram": self.raw_telegram,
|
|
90
|
+
"timestamp": self.timestamp.isoformat() if self.timestamp else None,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
def __str__(self) -> str:
|
|
94
|
+
"""Return human-readable string representation.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Formatted string representation.
|
|
98
|
+
"""
|
|
99
|
+
return (
|
|
100
|
+
f"XP Output: {self.action_description} "
|
|
101
|
+
f"on {self.input_description} "
|
|
102
|
+
f"for device {self.serial_number}"
|
|
103
|
+
)
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
"""Reply telegram model for console bus communication.
|
|
2
|
+
|
|
3
|
+
Reply telegrams are responses to system telegrams, containing the requested data
|
|
4
|
+
like temperature readings, status information, etc.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from typing import Any, Optional
|
|
10
|
+
|
|
11
|
+
from xp.models.telegram.datapoint_type import DataPointType
|
|
12
|
+
from xp.models.telegram.system_function import SystemFunction
|
|
13
|
+
from xp.models.telegram.telegram import Telegram
|
|
14
|
+
from xp.models.telegram.telegram_type import TelegramType
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class ReplyTelegram(Telegram):
|
|
19
|
+
"""Represents a parsed reply telegram from the console bus.
|
|
20
|
+
|
|
21
|
+
Format: <R{serial_number}F{function_code}D{data}{checksum}>
|
|
22
|
+
Format: <R{serial_number}F{function_code}D{datapoint_type}{data_value}{checksum}>
|
|
23
|
+
|
|
24
|
+
Examples:
|
|
25
|
+
- raw_telegram: <R0020012521F02D18+26,0§CIL>
|
|
26
|
+
- telegram_type : ReplyTelegram (R)
|
|
27
|
+
- serial_number: 0020012521
|
|
28
|
+
- function_code: 02
|
|
29
|
+
- data: 18+26,0§C
|
|
30
|
+
- datapoint_type: 18
|
|
31
|
+
- data_value: +26,0§C
|
|
32
|
+
- checksum: IL
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
serial_number: Serial number of the device.
|
|
36
|
+
system_function: System function code.
|
|
37
|
+
data: Raw data payload.
|
|
38
|
+
datapoint_type: Type of datapoint.
|
|
39
|
+
data_value: Parsed data value.
|
|
40
|
+
parse_datapoint_value: Parsed value based on datapoint type.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
serial_number: str = ""
|
|
44
|
+
system_function: SystemFunction = SystemFunction.NONE
|
|
45
|
+
data: str = ""
|
|
46
|
+
datapoint_type: Optional[DataPointType] = None
|
|
47
|
+
data_value: str = ""
|
|
48
|
+
|
|
49
|
+
def __post_init__(self) -> None:
|
|
50
|
+
"""Initialize timestamp and telegram type."""
|
|
51
|
+
if self.timestamp is None:
|
|
52
|
+
self.timestamp = datetime.now()
|
|
53
|
+
self.telegram_type = TelegramType.REPLY
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def parse_datapoint_value(self) -> dict[str, Any]:
|
|
57
|
+
"""Parse the data value based on data point type.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Dictionary containing parsed value and metadata.
|
|
61
|
+
"""
|
|
62
|
+
if self.datapoint_type == DataPointType.TEMPERATURE:
|
|
63
|
+
return self._parse_temperature_value()
|
|
64
|
+
elif self.datapoint_type == DataPointType.SW_TOP_VERSION:
|
|
65
|
+
return self._parse_humidity_value()
|
|
66
|
+
elif self.datapoint_type == DataPointType.VOLTAGE:
|
|
67
|
+
return self._parse_voltage_value()
|
|
68
|
+
elif self.datapoint_type == DataPointType.MODULE_ENERGY_LEVEL:
|
|
69
|
+
return self._parse_current_value()
|
|
70
|
+
elif self.datapoint_type == DataPointType.MODULE_TYPE:
|
|
71
|
+
return self._parse_module_type_value()
|
|
72
|
+
elif self.datapoint_type == DataPointType.SW_VERSION:
|
|
73
|
+
return self._parse_sw_version_value()
|
|
74
|
+
return {"raw_value": self.data_value, "parsed": False}
|
|
75
|
+
|
|
76
|
+
def _parse_temperature_value(self) -> dict:
|
|
77
|
+
"""Parse temperature value like '+26,0§C'.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Dictionary containing parsed temperature value and metadata.
|
|
81
|
+
"""
|
|
82
|
+
try:
|
|
83
|
+
# Remove unit indicator (§C)
|
|
84
|
+
value_part = self.data_value.replace("§C", "")
|
|
85
|
+
# Replace comma with dot for decimal
|
|
86
|
+
value_str = value_part.replace(",", ".")
|
|
87
|
+
temperature = float(value_str)
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
"value": temperature,
|
|
91
|
+
"unit": "°C",
|
|
92
|
+
"formatted": f"{temperature:.1f}°C",
|
|
93
|
+
"raw_value": self.data_value,
|
|
94
|
+
"parsed": True,
|
|
95
|
+
}
|
|
96
|
+
except (ValueError, AttributeError):
|
|
97
|
+
return {
|
|
98
|
+
"raw_value": self.data_value,
|
|
99
|
+
"parsed": False,
|
|
100
|
+
"error": "Failed to parse temperature",
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
def _parse_humidity_value(self) -> dict:
|
|
104
|
+
"""Parse humidity value like '+65,5§H'.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Dictionary containing parsed humidity value and metadata.
|
|
108
|
+
"""
|
|
109
|
+
try:
|
|
110
|
+
# Remove unit indicator (§H)
|
|
111
|
+
value_part = self.data_value.replace("§RH", "")
|
|
112
|
+
# Replace comma with dot for decimal
|
|
113
|
+
value_str = value_part.replace(",", ".")
|
|
114
|
+
humidity = float(value_str)
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
"value": humidity,
|
|
118
|
+
"unit": "%RH",
|
|
119
|
+
"formatted": f"{humidity:.1f}%RH",
|
|
120
|
+
"raw_value": self.data_value,
|
|
121
|
+
"parsed": True,
|
|
122
|
+
}
|
|
123
|
+
except (ValueError, AttributeError):
|
|
124
|
+
return {
|
|
125
|
+
"raw_value": self.data_value,
|
|
126
|
+
"parsed": False,
|
|
127
|
+
"error": "Failed to parse humidity",
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
def _parse_voltage_value(self) -> dict:
|
|
131
|
+
"""Parse voltage value like '+12,5§V'.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Dictionary containing parsed voltage value and metadata.
|
|
135
|
+
"""
|
|
136
|
+
try:
|
|
137
|
+
# Remove unit indicator (§V)
|
|
138
|
+
value_part = self.data_value.replace("§V", "")
|
|
139
|
+
# Replace comma with dot for decimal
|
|
140
|
+
value_str = value_part.replace(",", ".")
|
|
141
|
+
voltage = float(value_str)
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
"value": voltage,
|
|
145
|
+
"unit": "V",
|
|
146
|
+
"formatted": f"{voltage:.1f}V",
|
|
147
|
+
"raw_value": self.data_value,
|
|
148
|
+
"parsed": True,
|
|
149
|
+
}
|
|
150
|
+
except (ValueError, AttributeError):
|
|
151
|
+
return {
|
|
152
|
+
"raw_value": self.data_value,
|
|
153
|
+
"parsed": False,
|
|
154
|
+
"error": "Failed to parse voltage",
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
def _parse_current_value(self) -> dict:
|
|
158
|
+
"""Parse current value like '+0,25§A'.
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
Dictionary containing parsed current value and metadata.
|
|
162
|
+
"""
|
|
163
|
+
try:
|
|
164
|
+
# Remove unit indicator (§A)
|
|
165
|
+
value_part = self.data_value.replace("§A", "")
|
|
166
|
+
# Replace comma with dot for decimal
|
|
167
|
+
value_str = value_part.replace(",", ".")
|
|
168
|
+
current = float(value_str)
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
"value": current,
|
|
172
|
+
"unit": "A",
|
|
173
|
+
"formatted": f"{current:.2f}A",
|
|
174
|
+
"raw_value": self.data_value,
|
|
175
|
+
"parsed": True,
|
|
176
|
+
}
|
|
177
|
+
except (ValueError, AttributeError):
|
|
178
|
+
return {
|
|
179
|
+
"raw_value": self.data_value,
|
|
180
|
+
"parsed": False,
|
|
181
|
+
"error": "Failed to parse current",
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
def _parse_module_type_value(self) -> dict:
|
|
185
|
+
"""Parse status value.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Dictionary containing parsed module type value.
|
|
189
|
+
"""
|
|
190
|
+
# Status values are typically alphanumeric codes
|
|
191
|
+
return {
|
|
192
|
+
"module_type": self.data_value,
|
|
193
|
+
"raw_value": self.data_value,
|
|
194
|
+
"parsed": True,
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
def _parse_sw_version_value(self) -> dict:
|
|
198
|
+
"""Parse version value like 'XP230_V1.00.04'.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
Dictionary containing parsed version information.
|
|
202
|
+
"""
|
|
203
|
+
try:
|
|
204
|
+
# Version format: {PRODUCT}_{VERSION}
|
|
205
|
+
# Examples: XP230_V1.00.04, XP20_V0.01.05, XP33LR_V0.04.02, XP24_V0.34.03
|
|
206
|
+
if "_V" in self.data_value:
|
|
207
|
+
parts = self.data_value.split("_V", 1)
|
|
208
|
+
if len(parts) == 2:
|
|
209
|
+
product = parts[0]
|
|
210
|
+
version = parts[1]
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
"product": product,
|
|
214
|
+
"version": version,
|
|
215
|
+
"full_version": self.data_value,
|
|
216
|
+
"formatted": f"{product} v{version}",
|
|
217
|
+
"raw_value": self.data_value,
|
|
218
|
+
"parsed": True,
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
# If format doesn't match expected pattern, treat as raw
|
|
222
|
+
return {
|
|
223
|
+
"full_version": self.data_value,
|
|
224
|
+
"formatted": self.data_value,
|
|
225
|
+
"raw_value": self.data_value,
|
|
226
|
+
"parsed": False,
|
|
227
|
+
"error": "Version format not recognized",
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
except (ValueError, AttributeError):
|
|
231
|
+
return {
|
|
232
|
+
"raw_value": self.data_value,
|
|
233
|
+
"parsed": False,
|
|
234
|
+
"error": "Failed to parse version",
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
def to_dict(self) -> dict[str, Any]:
|
|
238
|
+
"""Convert to dictionary for JSON serialization.
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
Dictionary representation of the reply telegram.
|
|
242
|
+
"""
|
|
243
|
+
parsed_data = self.parse_datapoint_value
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
"serial_number": self.serial_number,
|
|
247
|
+
"system_function": (
|
|
248
|
+
{
|
|
249
|
+
"code": (
|
|
250
|
+
self.system_function.value if self.system_function else None
|
|
251
|
+
),
|
|
252
|
+
"description": (
|
|
253
|
+
self.system_function.name if self.system_function else None
|
|
254
|
+
),
|
|
255
|
+
}
|
|
256
|
+
if self.system_function
|
|
257
|
+
else None
|
|
258
|
+
),
|
|
259
|
+
"datapoint_type": (
|
|
260
|
+
{
|
|
261
|
+
"code": self.datapoint_type.value if self.datapoint_type else None,
|
|
262
|
+
"description": (
|
|
263
|
+
self.datapoint_type.name if self.datapoint_type else None
|
|
264
|
+
),
|
|
265
|
+
}
|
|
266
|
+
if self.datapoint_type
|
|
267
|
+
else None
|
|
268
|
+
),
|
|
269
|
+
"data_value": {"raw": self.data_value, "parsed": parsed_data},
|
|
270
|
+
"checksum": self.checksum,
|
|
271
|
+
"checksum_validated": self.checksum_validated,
|
|
272
|
+
"raw_telegram": self.raw_telegram,
|
|
273
|
+
"timestamp": self.timestamp.isoformat() if self.timestamp else None,
|
|
274
|
+
"telegram_type": self.telegram_type.value,
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
def __str__(self) -> str:
|
|
278
|
+
"""Human-readable string representation.
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
Formatted string representation.
|
|
282
|
+
"""
|
|
283
|
+
parsed = self.parse_datapoint_value
|
|
284
|
+
if parsed.get("parsed", False) and "formatted" in parsed:
|
|
285
|
+
value_display = parsed["formatted"]
|
|
286
|
+
else:
|
|
287
|
+
value_display = self.data_value
|
|
288
|
+
|
|
289
|
+
system_func_name = (
|
|
290
|
+
self.system_function.name if self.system_function else "Unknown"
|
|
291
|
+
)
|
|
292
|
+
datapoint_name = self.datapoint_type.name if self.datapoint_type else "Unknown"
|
|
293
|
+
return (
|
|
294
|
+
f"Reply Telegram: {system_func_name}\n "
|
|
295
|
+
f"for {datapoint_name} = {value_display} "
|
|
296
|
+
f"from device {self.serial_number}"
|
|
297
|
+
)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""System function enumeration for system telegrams."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SystemFunction(str, Enum):
|
|
8
|
+
"""System function codes for system telegrams.
|
|
9
|
+
|
|
10
|
+
Attributes:
|
|
11
|
+
NONE: Undefined function.
|
|
12
|
+
DISCOVERY: Discover function.
|
|
13
|
+
READ_DATAPOINT: Read datapoint.
|
|
14
|
+
READ_CONFIG: Read configuration.
|
|
15
|
+
WRITE_CONFIG: Write configuration.
|
|
16
|
+
BLINK: Blink LED function.
|
|
17
|
+
UNBLINK: Unblink LED function.
|
|
18
|
+
UPLOAD_FIRMWARE_START: Start upload firmware.
|
|
19
|
+
UPLOAD_FIRMWARE_STOP: Stop upload firmware.
|
|
20
|
+
UPLOAD_FIRMWARE: Upload firmware.
|
|
21
|
+
UPLOAD_ACTIONTABLE: Upload ActionTable to module.
|
|
22
|
+
DOWNLOAD_ACTIONTABLE: Download ActionTable.
|
|
23
|
+
UPLOAD_MSACTIONTABLE: Upload module specific action table to module.
|
|
24
|
+
DOWNLOAD_MSACTIONTABLE: Download module specific action table.
|
|
25
|
+
TELEGRAM_WRITE_START: Start writing telegram.
|
|
26
|
+
TELEGRAM_READ_START: Start reading telegram.
|
|
27
|
+
EOF: End of msactiontable response.
|
|
28
|
+
TELEGRAM: Module specific telegram response.
|
|
29
|
+
MSACTIONTABLE: Module specific action table response.
|
|
30
|
+
ACTIONTABLE: Module specific action table response.
|
|
31
|
+
ACK: Acknowledge response.
|
|
32
|
+
NAK: Not acknowledge response.
|
|
33
|
+
UPLOAD_TOP_FIRMWARE_START: Start upload firmware (TOP).
|
|
34
|
+
UPLOAD_TOP_FIRMWARE_STOP: Stop upload firmware (TOP).
|
|
35
|
+
UPLOAD_TOP_FIRMWARE: Upload firmware (TOP).
|
|
36
|
+
ROTATE_ENABLE: Enable rotate.
|
|
37
|
+
ROTATE_DISABLE: Disable rotate.
|
|
38
|
+
UNKNOWN_26: Used after discover, unknown purpose.
|
|
39
|
+
ACTION: Action function.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
NONE = "00" # F00D Undefined
|
|
43
|
+
DISCOVERY = "01" # F01D Discover function
|
|
44
|
+
READ_DATAPOINT = "02" # F02D Read datapoint
|
|
45
|
+
READ_CONFIG = "03" # F03D Read configuration
|
|
46
|
+
WRITE_CONFIG = "04" # F04D Write configuration
|
|
47
|
+
BLINK = "05" # F05D Blink LED function
|
|
48
|
+
UNBLINK = "06" # F06D Unblink LED function
|
|
49
|
+
|
|
50
|
+
UPLOAD_FIRMWARE_START = "07" # F07D Start Upload firmware
|
|
51
|
+
UPLOAD_FIRMWARE_STOP = "08" # F08D Stop Upload firmware
|
|
52
|
+
UPLOAD_FIRMWARE = "09" # F09D Upload firmware
|
|
53
|
+
|
|
54
|
+
UPLOAD_ACTIONTABLE = "10" # F10D Upload ActionTable
|
|
55
|
+
DOWNLOAD_ACTIONTABLE = "11" # F11D Download ActionTable
|
|
56
|
+
UPLOAD_MSACTIONTABLE = "12" # F12D Upload MsActionTable to module
|
|
57
|
+
DOWNLOAD_MSACTIONTABLE = "13" # F13D Download MsActionTable
|
|
58
|
+
|
|
59
|
+
TELEGRAM_WRITE_START = "14" # F14D Start writing telegram
|
|
60
|
+
TELEGRAM_READ_START = "15" # F15D Start reading telegram
|
|
61
|
+
EOF = "16" # F16D End of msactiontable response
|
|
62
|
+
TELEGRAM = "17" # F17D module specific Telegram response
|
|
63
|
+
MSACTIONTABLE = "17" # F17D module specific ms action table (Telegram) response
|
|
64
|
+
ACTIONTABLE = "17" # F17D module specific action table (Telegram) response
|
|
65
|
+
ACK = "18" # F18D Acknowledge / continue response
|
|
66
|
+
NAK = "19" # F19D Not acknowledge response
|
|
67
|
+
|
|
68
|
+
UPLOAD_TOP_FIRMWARE_START = "20" # F20D Start Upload firmware (TOP)
|
|
69
|
+
UPLOAD_TOP_FIRMWARE_STOP = "21" # F21D Stop Upload firmware (TOP)
|
|
70
|
+
UPLOAD_TOP_FIRMWARE = "22" # F22D Upload firmware (TOP)
|
|
71
|
+
|
|
72
|
+
ROTATE_ENABLE = "23" # F23D Enable rotate
|
|
73
|
+
ROTATE_DISABLE = "24" # F24D Disable rotate
|
|
74
|
+
|
|
75
|
+
UNKNOWN_26 = "26" # F26D Used after discover, but don't know what it is
|
|
76
|
+
ACTION = "27" # F27D Action function
|
|
77
|
+
|
|
78
|
+
def get_description(self) -> str:
|
|
79
|
+
"""Get the description of the SystemFunction.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Human-readable description of the function.
|
|
83
|
+
"""
|
|
84
|
+
return (
|
|
85
|
+
{
|
|
86
|
+
self.DISCOVERY: "Discover function",
|
|
87
|
+
self.READ_DATAPOINT: "Read datapoint",
|
|
88
|
+
self.READ_CONFIG: "Read configuration",
|
|
89
|
+
self.WRITE_CONFIG: "Write configuration",
|
|
90
|
+
self.BLINK: "Blink LED function",
|
|
91
|
+
self.DOWNLOAD_MSACTIONTABLE: "Download the msactiontable",
|
|
92
|
+
self.DOWNLOAD_ACTIONTABLE: "Download ActionTable",
|
|
93
|
+
self.EOF: "End of msactiontable response",
|
|
94
|
+
self.ACTIONTABLE: "Actiontable response",
|
|
95
|
+
self.MSACTIONTABLE: "Msactiontable response",
|
|
96
|
+
self.UNBLINK: "Unblink LED function",
|
|
97
|
+
self.ACK: "Acknowledge response",
|
|
98
|
+
self.NAK: "Not acknowledge response",
|
|
99
|
+
self.ACTION: "Action function",
|
|
100
|
+
}
|
|
101
|
+
).get(self, "Unknown function")
|
|
102
|
+
|
|
103
|
+
@classmethod
|
|
104
|
+
def from_code(cls, code: str) -> Optional["SystemFunction"]:
|
|
105
|
+
"""Get SystemFunction from code string.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
code: Function code string.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
SystemFunction instance if found, None otherwise.
|
|
112
|
+
"""
|
|
113
|
+
for func in cls:
|
|
114
|
+
if func.value.lower() == code.lower():
|
|
115
|
+
return func
|
|
116
|
+
return None
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""System telegram model for console bus communication.
|
|
2
|
+
|
|
3
|
+
System telegrams are used for system-related information like updating firmware
|
|
4
|
+
and reading temperature from modules.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from typing import Any, Optional
|
|
10
|
+
|
|
11
|
+
from xp.models.telegram.datapoint_type import DataPointType
|
|
12
|
+
from xp.models.telegram.system_function import SystemFunction
|
|
13
|
+
from xp.models.telegram.telegram import Telegram
|
|
14
|
+
from xp.models.telegram.telegram_type import TelegramType
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class SystemTelegram(Telegram):
|
|
19
|
+
"""Represents a parsed system telegram from the console bus.
|
|
20
|
+
|
|
21
|
+
Format: <S{serial_number}F{function_code}D{datapoint_type}{checksum}>
|
|
22
|
+
Examples: <S0020012521F02D18FN>
|
|
23
|
+
|
|
24
|
+
Attributes:
|
|
25
|
+
serial_number: Serial number of the device (0020012521)
|
|
26
|
+
system_function: System function code (02).
|
|
27
|
+
data: Data payload (18)
|
|
28
|
+
datapoint_type: Type of datapoint (18).
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
serial_number: str = ""
|
|
32
|
+
system_function: Optional[SystemFunction] = None
|
|
33
|
+
data: str = ""
|
|
34
|
+
datapoint_type: Optional[DataPointType] = None
|
|
35
|
+
|
|
36
|
+
def __post_init__(self) -> None:
|
|
37
|
+
"""Initialize timestamp and telegram type."""
|
|
38
|
+
if self.timestamp is None:
|
|
39
|
+
self.timestamp = datetime.now()
|
|
40
|
+
self.telegram_type = TelegramType.SYSTEM
|
|
41
|
+
|
|
42
|
+
def to_dict(self) -> dict[str, Any]:
|
|
43
|
+
"""Convert to dictionary for JSON serialization.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Dictionary representation of the system telegram.
|
|
47
|
+
"""
|
|
48
|
+
return {
|
|
49
|
+
"serial_number": self.serial_number,
|
|
50
|
+
"system_function": (
|
|
51
|
+
{
|
|
52
|
+
"code": (
|
|
53
|
+
self.system_function.value if self.system_function else None
|
|
54
|
+
),
|
|
55
|
+
"description": (
|
|
56
|
+
self.system_function.name if self.system_function else None
|
|
57
|
+
),
|
|
58
|
+
}
|
|
59
|
+
if self.system_function
|
|
60
|
+
else None
|
|
61
|
+
),
|
|
62
|
+
"datapoint_type": (
|
|
63
|
+
{
|
|
64
|
+
"code": self.datapoint_type.value if self.datapoint_type else None,
|
|
65
|
+
"description": (
|
|
66
|
+
self.datapoint_type.name if self.datapoint_type else None
|
|
67
|
+
),
|
|
68
|
+
}
|
|
69
|
+
if self.datapoint_type
|
|
70
|
+
else None
|
|
71
|
+
),
|
|
72
|
+
"checksum": self.checksum,
|
|
73
|
+
"checksum_validated": self.checksum_validated,
|
|
74
|
+
"raw_telegram": self.raw_telegram,
|
|
75
|
+
"timestamp": self.timestamp.isoformat() if self.timestamp else None,
|
|
76
|
+
"telegram_type": self.telegram_type.value,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
def __str__(self) -> str:
|
|
80
|
+
"""Human-readable string representation.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Formatted string representation.
|
|
84
|
+
"""
|
|
85
|
+
system_func_name = (
|
|
86
|
+
self.system_function.name if self.system_function else "Unknown"
|
|
87
|
+
)
|
|
88
|
+
data = self.data or "None"
|
|
89
|
+
data = self.datapoint_type.name if self.datapoint_type else data
|
|
90
|
+
return (
|
|
91
|
+
f"System Telegram: {system_func_name} "
|
|
92
|
+
f"with data {data} "
|
|
93
|
+
f"from device {self.serial_number}"
|
|
94
|
+
)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Base telegram model for console bus communication."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from xp.models.telegram.telegram_type import TelegramType
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class Telegram:
|
|
12
|
+
"""Represents an abstract telegram from the console bus.
|
|
13
|
+
|
|
14
|
+
Can be an EventTelegram, SystemTelegram or ReplyTelegram.
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
checksum: Telegram checksum value.
|
|
18
|
+
raw_telegram: Raw telegram string.
|
|
19
|
+
checksum_validated: Whether checksum validation passed.
|
|
20
|
+
timestamp: Timestamp when telegram was received.
|
|
21
|
+
telegram_type: Type of telegram (EVENT, SYSTEM, or REPLY).
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
checksum: str
|
|
25
|
+
raw_telegram: str
|
|
26
|
+
checksum_validated: Optional[bool] = None
|
|
27
|
+
timestamp: Optional[datetime] = None
|
|
28
|
+
telegram_type: TelegramType = TelegramType.EVENT
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Telegram type enumeration for console bus communication."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TelegramType(str, Enum):
|
|
7
|
+
"""Enumeration of telegram types in the console bus system.
|
|
8
|
+
|
|
9
|
+
Attributes:
|
|
10
|
+
EVENT: Event telegram (E).
|
|
11
|
+
REPLY: Reply telegram (R).
|
|
12
|
+
SYSTEM: System telegram (S).
|
|
13
|
+
CPEVENT: CP event telegram (O).
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
EVENT = "E"
|
|
17
|
+
REPLY = "R"
|
|
18
|
+
SYSTEM = "S"
|
|
19
|
+
CPEVENT = "O"
|