conson-xp 1.19.0__py3-none-any.whl → 1.20.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.19.0.dist-info → conson_xp-1.20.0.dist-info}/METADATA +6 -1
- {conson_xp-1.19.0.dist-info → conson_xp-1.20.0.dist-info}/RECORD +23 -13
- {conson_xp-1.19.0.dist-info → conson_xp-1.20.0.dist-info}/WHEEL +1 -1
- xp/__init__.py +1 -1
- xp/cli/commands/__init__.py +4 -0
- xp/cli/commands/conbus/conbus_receive_commands.py +2 -1
- xp/cli/commands/term/__init__.py +5 -0
- xp/cli/commands/term/term.py +12 -0
- xp/cli/commands/term/term_commands.py +31 -0
- xp/cli/main.py +7 -35
- xp/models/conbus/conbus_client_config.py +1 -0
- xp/models/conbus/conbus_logger_config.py +107 -0
- xp/services/conbus/conbus_receive_service.py +58 -30
- xp/services/protocol/conbus_event_protocol.py +28 -3
- xp/tui/__init__.py +1 -0
- xp/tui/app.py +72 -0
- xp/tui/protocol.tcss +50 -0
- xp/tui/widgets/__init__.py +1 -0
- xp/tui/widgets/protocol_log.py +312 -0
- xp/utils/dependencies.py +25 -6
- xp/utils/logging.py +91 -0
- {conson_xp-1.19.0.dist-info → conson_xp-1.20.0.dist-info}/entry_points.txt +0 -0
- {conson_xp-1.19.0.dist-info → conson_xp-1.20.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: conson-xp
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.20.0
|
|
4
4
|
Summary: XP Protocol Communication Tools
|
|
5
5
|
Author-Email: ldvchosal <ldvchosal@github.com>
|
|
6
6
|
License: MIT License
|
|
@@ -48,6 +48,7 @@ Requires-Dist: punq>=0.7.0
|
|
|
48
48
|
Requires-Dist: twisted>=25.5.0
|
|
49
49
|
Requires-Dist: bubus>=1.5.6
|
|
50
50
|
Requires-Dist: psygnal>=0.15.0
|
|
51
|
+
Requires-Dist: textual>=1.0.0
|
|
51
52
|
Description-Content-Type: text/markdown
|
|
52
53
|
|
|
53
54
|
# 🔌 XP Protocol Communication Tool
|
|
@@ -398,6 +399,10 @@ xp telegram parse
|
|
|
398
399
|
xp telegram validate
|
|
399
400
|
xp telegram version
|
|
400
401
|
|
|
402
|
+
|
|
403
|
+
xp term
|
|
404
|
+
xp term protocol
|
|
405
|
+
|
|
401
406
|
<!-- END CLI HELP -->
|
|
402
407
|
```
|
|
403
408
|
</details>
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
conson_xp-1.
|
|
2
|
-
conson_xp-1.
|
|
3
|
-
conson_xp-1.
|
|
4
|
-
conson_xp-1.
|
|
5
|
-
xp/__init__.py,sha256=
|
|
1
|
+
conson_xp-1.20.0.dist-info/METADATA,sha256=vAvQzvNII8d7oxEEbo1rSLyswX3lROWxh7cERQ-y3_Y,9584
|
|
2
|
+
conson_xp-1.20.0.dist-info/WHEEL,sha256=tsUv_t7BDeJeRHaSrczbGeuK-TtDpGsWi_JfpzD255I,90
|
|
3
|
+
conson_xp-1.20.0.dist-info/entry_points.txt,sha256=1OcdIcDM1hz3ljCXgybaPUh1IOFEwkaTgLIW9u9zGeg,50
|
|
4
|
+
conson_xp-1.20.0.dist-info/licenses/LICENSE,sha256=rxj6woMM-r3YCyGq_UHFtbh7kHTAJgrccH6O-33IDE4,1419
|
|
5
|
+
xp/__init__.py,sha256=T304nC2FLiJmOCyBS3xq0x0Bb4Nv_XAMMRN4xZtqUec,181
|
|
6
6
|
xp/cli/__init__.py,sha256=QjnKB1KaI2aIyKlzrnvCwfbBuUj8HNgwNMvNJVQofbI,81
|
|
7
7
|
xp/cli/__main__.py,sha256=l2iKwMdat5rTGd3JWs-uGksnYYDDffp_Npz05QdKEeU,117
|
|
8
|
-
xp/cli/commands/__init__.py,sha256=
|
|
8
|
+
xp/cli/commands/__init__.py,sha256=noh8fdZAWq-ihJEboP8WugbIgq4LJ3jUWMRA7720xWE,4909
|
|
9
9
|
xp/cli/commands/conbus/__init__.py,sha256=gE3K5OEoXkkZX8UOc2v3nreQQzwkOQi7n0VZ-Z2juXA,495
|
|
10
10
|
xp/cli/commands/conbus/conbus.py,sha256=eqdY8ArapvD08Z4p7Xk7eh4z0dESHuMSw7PKtwTJRYU,3021
|
|
11
11
|
xp/cli/commands/conbus/conbus_actiontable_commands.py,sha256=cdjLV9cnm7teEOlu5Jf1MS_aL7lNy8KiDIyjCQa5Nzw,7138
|
|
@@ -22,7 +22,7 @@ xp/cli/commands/conbus/conbus_modulenumber_commands.py,sha256=L7-6y3rDllOjQ9g6Bk
|
|
|
22
22
|
xp/cli/commands/conbus/conbus_msactiontable_commands.py,sha256=fb9MQ4O04H0Dinpt7vSF5GtfntTZHelQ5TuUmSBbCTg,2899
|
|
23
23
|
xp/cli/commands/conbus/conbus_output_commands.py,sha256=zdRVbHzVhMbZpG2x5WXtujc3wKTsoQUV4IgkVIbJbCc,5019
|
|
24
24
|
xp/cli/commands/conbus/conbus_raw_commands.py,sha256=8BKUarwvHgz-sxML7n99YVsb8B1HJNExjQpRsuY_tQw,1829
|
|
25
|
-
xp/cli/commands/conbus/conbus_receive_commands.py,sha256=
|
|
25
|
+
xp/cli/commands/conbus/conbus_receive_commands.py,sha256=2lZP0a3dte3Q_Vp28xYkqLAoxnvArS9SsQdeedOHcQw,1788
|
|
26
26
|
xp/cli/commands/conbus/conbus_scan_commands.py,sha256=JfXucOwOadvLEKT_fW9fwvqWKHaEODOojLjnO8JV_00,1730
|
|
27
27
|
xp/cli/commands/file_commands.py,sha256=GV102X7FRZDUNKLlzvSsIGcoXAaofOzmjCp3HUpE9lw,5532
|
|
28
28
|
xp/cli/commands/homekit/__init__.py,sha256=qqwY8ulxTx1S74Mzpb6EKjBLT6fWTNdf9PQ3HKuERKY,50
|
|
@@ -40,7 +40,10 @@ xp/cli/commands/telegram/telegram_discover_commands.py,sha256=0UArJinw1eWFbee5EG
|
|
|
40
40
|
xp/cli/commands/telegram/telegram_linknumber_commands.py,sha256=7j0-E5Moqqga4NrKDch82C6glaFDFMQn5_3hMwie7BQ,2511
|
|
41
41
|
xp/cli/commands/telegram/telegram_parse_commands.py,sha256=_OYOso1hS4f_ox96qlkYL2SuFnmimpAvqqdYlLzX9yo,2232
|
|
42
42
|
xp/cli/commands/telegram/telegram_version_commands.py,sha256=WQyx1-B9yJ8V9WrFyBpOvULJ-jq12GoZZDDoRbM7eyw,1553
|
|
43
|
-
xp/cli/
|
|
43
|
+
xp/cli/commands/term/__init__.py,sha256=1NNST_8YJfj5LCujQISwQflK6LyEn7mDmZpMpvI9d-o,116
|
|
44
|
+
xp/cli/commands/term/term.py,sha256=gjvsv2OE-F_KNWQrWi04fXQ5cGo0l8P-Ortbb5KTA-A,309
|
|
45
|
+
xp/cli/commands/term/term_commands.py,sha256=3qwiGlEEgMIz5AG7fq5U_9SZSWSDfrKwYggHbiv5kRk,738
|
|
46
|
+
xp/cli/main.py,sha256=ap5jU0DrSnrCKDKqGXcz9N-sngZodyyN-5ReWE8Fh1s,1817
|
|
44
47
|
xp/cli/utils/__init__.py,sha256=gTGIj60Uai0iE7sr9_TtEpl04fD7krtTzbbigXUsUVU,46
|
|
45
48
|
xp/cli/utils/click_tree.py,sha256=ilmM2IMa_c-TqUMsv2alrZXuS0BNhvVlrBlSfyN8lzM,1670
|
|
46
49
|
xp/cli/utils/datapoint_type_choice.py,sha256=HcydhlqxZ7YyorEeTjFGkypF2JnYNPvOzkl1rhZ93Fc,1666
|
|
@@ -63,7 +66,7 @@ xp/models/conbus/__init__.py,sha256=VIusMWQdBtlwDgj7oSj06wQkklihTp4oWFShvP_JUgA,
|
|
|
63
66
|
xp/models/conbus/conbus.py,sha256=mZQzKPfrdttT-qUnYUSyrEYyc_eHs8z301E5ejeiyvk,2689
|
|
64
67
|
xp/models/conbus/conbus_autoreport.py,sha256=lKotDfxRBb7h2Z1d4qI3KhhLJhFDwKqLbSdG5Makm8Y,2289
|
|
65
68
|
xp/models/conbus/conbus_blink.py,sha256=XEAPtA-O76ulX6Zh1oYzsWF6L4css6xJBuUTwNcDQKc,2911
|
|
66
|
-
xp/models/conbus/conbus_client_config.py,sha256=
|
|
69
|
+
xp/models/conbus/conbus_client_config.py,sha256=MliIffoP0Ku1FxpNznMJPBrpdz9OVKZcs9IJ4g1Z3dE,1485
|
|
67
70
|
xp/models/conbus/conbus_connection_status.py,sha256=iGbmtBaAMwV6UD7XG3H3tnB0fl2MR8rJhpjrLH2KjsE,1097
|
|
68
71
|
xp/models/conbus/conbus_custom.py,sha256=8H2sPR6_LIlksuOvL7-8bPkzAJLR0rpYiiwfYYFVjEo,1965
|
|
69
72
|
xp/models/conbus/conbus_datapoint.py,sha256=4ncR-vB2lRzRBAA30rYn8eguyTxsZoOKrrXtjGmPpWg,3396
|
|
@@ -72,6 +75,7 @@ xp/models/conbus/conbus_event_list.py,sha256=M8aHRHVB5VDIjqMzjO86xlERt7AMdfjIjt1
|
|
|
72
75
|
xp/models/conbus/conbus_event_raw.py,sha256=i5gc7z-0yeunWOZ4rw3AiBt4MANezmhBQKjOOQk3oDc,1567
|
|
73
76
|
xp/models/conbus/conbus_lightlevel.py,sha256=GQGhzrCBEJROosNHInXIzBy6MD2AskEIMoFEGgZ60-0,1695
|
|
74
77
|
xp/models/conbus/conbus_linknumber.py,sha256=uFzKzfB06oIzZEKCb5X2JEI80JjMPFuYglsT1W1k8j4,1815
|
|
78
|
+
xp/models/conbus/conbus_logger_config.py,sha256=cFWjWn8tc_hPPI2kQAib_Akddar8O-3zkoj6wLBsdUo,3328
|
|
75
79
|
xp/models/conbus/conbus_output.py,sha256=q7QKsD_CWT7YOk-V3otKWD1VM7qThrSLIUOunntMrMc,1953
|
|
76
80
|
xp/models/conbus/conbus_raw.py,sha256=xqvYao6IE1SXum7JBgZpSuWXm9x_QZquS9N_3_r0Hjs,1460
|
|
77
81
|
xp/models/conbus/conbus_receive.py,sha256=-1u1qK-texfKCNZV-GYf_9KyLtJdIrx7HuZsKzu26Ow,1322
|
|
@@ -125,7 +129,7 @@ xp/services/conbus/conbus_event_list_service.py,sha256=0xyXXNU44epN5bFkU6oiZMyhx
|
|
|
125
129
|
xp/services/conbus/conbus_event_raw_service.py,sha256=FZFu-LNLInrTKTpiGLyootozvyIF5Si5FMrxNk2ALD0,7000
|
|
126
130
|
xp/services/conbus/conbus_output_service.py,sha256=mHFOAPx2zo0TStZ3pokp6v94AQjIamcwZDeg5YH_-eo,7240
|
|
127
131
|
xp/services/conbus/conbus_raw_service.py,sha256=4yZLLTIAOxpgByUTWZXw1ihGa6Xtl98ckj9T7VfprDI,4335
|
|
128
|
-
xp/services/conbus/conbus_receive_service.py,sha256=
|
|
132
|
+
xp/services/conbus/conbus_receive_service.py,sha256=38lAZ0tc2AjBfcqI7qje-ES_QHiHZ3Ayybrp1ZC8ceM,5412
|
|
129
133
|
xp/services/conbus/conbus_scan_service.py,sha256=tHJ5qaxcNXxAZb2D2F1v6IrzydfxjJOYllM6Txt1eBE,5176
|
|
130
134
|
xp/services/conbus/write_config_service.py,sha256=6feNdixI_Nli4MRLe15nea-7gTEXMUwZIvTqv_1OqHI,7157
|
|
131
135
|
xp/services/homekit/__init__.py,sha256=xAMKmln_AmEFdOOJGKWYi96seRlKDQpKx3-hm7XbdIo,36
|
|
@@ -145,7 +149,7 @@ xp/services/homekit/homekit_service.py,sha256=0lW-hg40ETco3gDBEYkR_sX-UIYsLSKCD4
|
|
|
145
149
|
xp/services/log_file_service.py,sha256=fvPcZQj8XOKUA-4ep5R8n0PelvwvRlTLlVxvIWM5KR4,10506
|
|
146
150
|
xp/services/module_type_service.py,sha256=xWhr1EAZMykL5uNWHWdpa5T8yNruGKH43XRTOS8GwZg,7477
|
|
147
151
|
xp/services/protocol/__init__.py,sha256=qRufBmqRKGzpuzZ5bxBbmwf510TT00Ke8s5HcWGnqRY,818
|
|
148
|
-
xp/services/protocol/conbus_event_protocol.py,sha256=
|
|
152
|
+
xp/services/protocol/conbus_event_protocol.py,sha256=48KCTkJLDHV1ijVXHf0TraY663Nk3_dEV3lkZpvduDo,13671
|
|
149
153
|
xp/services/protocol/conbus_protocol.py,sha256=JO7yLkD_ohPT0ETjnAIx4CGpZyobf4LdbuozM_43btE,10276
|
|
150
154
|
xp/services/protocol/protocol_factory.py,sha256=PmjN9AtW9sxNo3voqUiNgQA-pTvX1RW4XXFlHKfFr5Q,2470
|
|
151
155
|
xp/services/protocol/telegram_protocol.py,sha256=Ki5DrXsKxiaqLcdP9WWUuhUI7cPu2DfwyZkh-Gv9Lb8,9496
|
|
@@ -169,10 +173,16 @@ xp/services/telegram/telegram_link_number_service.py,sha256=1_c-_QCRPTHYn3BmMElr
|
|
|
169
173
|
xp/services/telegram/telegram_output_service.py,sha256=UaUv_14fR8o5K2PxQBXrCzx-Hohnk-gzbev_oLw_Clc,10799
|
|
170
174
|
xp/services/telegram/telegram_service.py,sha256=XrP1CPi0ckxoKBaNwLA6lo-TogWxXgmXDOsU4Xl8BlY,13237
|
|
171
175
|
xp/services/telegram/telegram_version_service.py,sha256=M5HdOTsLdcwo122FP-jW6R740ktLrtKf2TiMDVz23h8,10528
|
|
176
|
+
xp/tui/__init__.py,sha256=Xg2DhBeI3xQJLfc7_BPWI1por-rUXemyer5OtOt9Cus,51
|
|
177
|
+
xp/tui/app.py,sha256=NBCdFCgftckey5TgxDFn-SeOLSFggapshIwKDcLM5QY,2227
|
|
178
|
+
xp/tui/protocol.tcss,sha256=njMgFgz4oD4Qjw3dyoX1SfCuvlfGg6QrrkZ2COFZ0yM,768
|
|
179
|
+
xp/tui/widgets/__init__.py,sha256=Ewiza9u6k5K50zZRIMD7jjOHY1IvGhoX1ViwlqhdGms,27
|
|
180
|
+
xp/tui/widgets/protocol_log.py,sha256=0twJHiOTNcOOw3eQtYkjb1x37adecypPPPT1fuWb2q4,11541
|
|
172
181
|
xp/utils/__init__.py,sha256=_avMF_UOkfR3tNeDIPqQ5odmbq5raKkaq1rZ9Cn1CJs,332
|
|
173
182
|
xp/utils/checksum.py,sha256=HDpiQxmdIedbCbZ4o_Box0i_Zig417BtCV_46ZyhiTk,1711
|
|
174
|
-
xp/utils/dependencies.py,sha256=
|
|
183
|
+
xp/utils/dependencies.py,sha256=ECS6p0eXzocM5INLwJeckHXn_Dim18uOjXTJ29qQvkQ,22001
|
|
175
184
|
xp/utils/event_helper.py,sha256=W-A_xmoXlpWZBbJH6qdaN50o3-XrwFsDgvAGMJDiAgo,1001
|
|
185
|
+
xp/utils/logging.py,sha256=5ol2JrnFtjs9QtBYW4KeKYzcFzCxbCn8BsIq3aIF4H4,3395
|
|
176
186
|
xp/utils/serialization.py,sha256=RWHHk86feaB4ZP7rjE4qOWK0900yg2joUBDkP76gfOY,4618
|
|
177
187
|
xp/utils/time_utils.py,sha256=dEyViDlAG9GWU-J3D_YVa-sGma6yiyyMTgN4h2x3PY4,3781
|
|
178
|
-
conson_xp-1.
|
|
188
|
+
conson_xp-1.20.0.dist-info/RECORD,,
|
xp/__init__.py
CHANGED
xp/cli/commands/__init__.py
CHANGED
|
@@ -86,6 +86,8 @@ from xp.cli.commands.telegram.telegram_parse_commands import (
|
|
|
86
86
|
validate_telegram,
|
|
87
87
|
)
|
|
88
88
|
from xp.cli.commands.telegram.telegram_version_commands import generate_version_request
|
|
89
|
+
from xp.cli.commands.term.term import term
|
|
90
|
+
from xp.cli.commands.term.term_commands import protocol_monitor
|
|
89
91
|
|
|
90
92
|
__all__ = [
|
|
91
93
|
# Main command groups (conbus excluded to avoid module shadowing)
|
|
@@ -109,7 +111,9 @@ __all__ = [
|
|
|
109
111
|
"checksum",
|
|
110
112
|
"homekit",
|
|
111
113
|
"homekit_start",
|
|
114
|
+
"term",
|
|
112
115
|
# Individual command functions
|
|
116
|
+
"protocol_monitor",
|
|
113
117
|
"conbus_download_msactiontable",
|
|
114
118
|
"conbus_download_actiontable",
|
|
115
119
|
"send_blink_on_telegram",
|
|
@@ -56,4 +56,5 @@ def receive_telegrams(ctx: Context, timeout: float) -> None:
|
|
|
56
56
|
ctx.obj.get("container").get_container().resolve(ConbusReceiveService)
|
|
57
57
|
)
|
|
58
58
|
with service:
|
|
59
|
-
service.
|
|
59
|
+
service.init(progress, on_finish, timeout)
|
|
60
|
+
service.start_reactor()
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Term CLI group definition for TUI commands."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from click_help_colors import HelpColorsGroup
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@click.group(
|
|
8
|
+
cls=HelpColorsGroup, help_headers_color="yellow", help_options_color="green"
|
|
9
|
+
)
|
|
10
|
+
def term() -> None:
|
|
11
|
+
"""Terminal UI commands for interactive monitoring and control."""
|
|
12
|
+
pass
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Term protocol CLI command for TUI monitoring."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from click import Context
|
|
5
|
+
|
|
6
|
+
from xp.cli.commands.term.term import term
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@term.command("protocol")
|
|
10
|
+
@click.pass_context
|
|
11
|
+
def protocol_monitor(ctx: Context) -> None:
|
|
12
|
+
r"""Start TUI for real-time protocol monitoring.
|
|
13
|
+
|
|
14
|
+
Displays live RX/TX telegram stream from Conbus server
|
|
15
|
+
in an interactive terminal interface.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
ctx: Click context object.
|
|
19
|
+
|
|
20
|
+
Examples:
|
|
21
|
+
\b
|
|
22
|
+
xp term protocol
|
|
23
|
+
"""
|
|
24
|
+
from xp.tui.app import ProtocolMonitorApp
|
|
25
|
+
|
|
26
|
+
# Resolve ServiceContainer from context
|
|
27
|
+
container = ctx.obj.get("container").get_container()
|
|
28
|
+
|
|
29
|
+
# Initialize and run Textual app
|
|
30
|
+
app = ProtocolMonitorApp(container=container)
|
|
31
|
+
app.run()
|
xp/cli/main.py
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
"""XP CLI tool entry point with modular command structure."""
|
|
2
2
|
|
|
3
|
-
import logging
|
|
4
|
-
|
|
5
3
|
import click
|
|
6
4
|
from click_help_colors import HelpColorsGroup
|
|
7
5
|
|
|
@@ -16,8 +14,10 @@ from xp.cli.commands.server.server_commands import server
|
|
|
16
14
|
|
|
17
15
|
# Import command groups from modular structure
|
|
18
16
|
from xp.cli.commands.telegram.telegram_parse_commands import telegram
|
|
17
|
+
from xp.cli.commands.term.term import term
|
|
19
18
|
from xp.cli.utils.click_tree import add_tree_command
|
|
20
19
|
from xp.utils.dependencies import ServiceContainer
|
|
20
|
+
from xp.utils.logging import LoggerService
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
@click.group(
|
|
@@ -31,44 +31,15 @@ def cli(ctx: click.Context) -> None:
|
|
|
31
31
|
Args:
|
|
32
32
|
ctx: Click context object for passing state between commands.
|
|
33
33
|
"""
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
# Force format on root logger and all handlers
|
|
39
|
-
formatter = logging.Formatter(log_format, datefmt=date_format)
|
|
40
|
-
root_logger = logging.getLogger()
|
|
41
|
-
root_logger.setLevel(logging.DEBUG)
|
|
42
|
-
|
|
43
|
-
# Update all existing handlers or create new one
|
|
44
|
-
if root_logger.handlers:
|
|
45
|
-
for handler in root_logger.handlers:
|
|
46
|
-
handler.setFormatter(formatter)
|
|
47
|
-
else:
|
|
48
|
-
handler = logging.StreamHandler()
|
|
49
|
-
handler.setFormatter(formatter)
|
|
50
|
-
root_logger.addHandler(handler)
|
|
51
|
-
|
|
52
|
-
# Suppress pyhap.hap_protocol logs
|
|
53
|
-
|
|
54
|
-
# bubus
|
|
55
|
-
logging.getLogger("bubus").setLevel(logging.WARNING)
|
|
56
|
-
|
|
57
|
-
# xp
|
|
58
|
-
logging.getLogger("xp").setLevel(logging.DEBUG)
|
|
59
|
-
logging.getLogger("xp.services.homekit").setLevel(logging.DEBUG)
|
|
60
|
-
|
|
61
|
-
# pyhap
|
|
62
|
-
logging.getLogger("pyhap").setLevel(logging.WARNING)
|
|
63
|
-
logging.getLogger("pyhap.hap_handler").setLevel(logging.WARNING)
|
|
64
|
-
logging.getLogger("pyhap.hap_protocol").setLevel(logging.WARNING)
|
|
65
|
-
# logging.getLogger('pyhap.accessory_driver').setLevel(logging.WARNING)
|
|
34
|
+
container = ServiceContainer()
|
|
35
|
+
logger_config = container.get_container().resolve(LoggerService)
|
|
36
|
+
logger_config.setup()
|
|
66
37
|
|
|
67
38
|
# Initialize the service container and store it in the context
|
|
68
39
|
ctx.ensure_object(dict)
|
|
69
40
|
# Only create a new container if one wasn't provided (e.g., for testing)
|
|
70
41
|
if "container" not in ctx.obj:
|
|
71
|
-
ctx.obj["container"] =
|
|
42
|
+
ctx.obj["container"] = container
|
|
72
43
|
|
|
73
44
|
|
|
74
45
|
# Register all command groups
|
|
@@ -79,6 +50,7 @@ cli.add_command(module)
|
|
|
79
50
|
cli.add_command(file)
|
|
80
51
|
cli.add_command(server)
|
|
81
52
|
cli.add_command(reverse_proxy)
|
|
53
|
+
cli.add_command(term)
|
|
82
54
|
|
|
83
55
|
# Add the tree command
|
|
84
56
|
add_tree_command(cli)
|
|
@@ -49,6 +49,7 @@ class ConbusClientConfig(BaseModel):
|
|
|
49
49
|
except FileNotFoundError:
|
|
50
50
|
logger.error(f"File {file_path} does not exist, loading default")
|
|
51
51
|
return cls()
|
|
52
|
+
|
|
52
53
|
except yaml.YAMLError:
|
|
53
54
|
logger.error(f"File {file_path} is not valid")
|
|
54
55
|
# Return default config if YAML parsing fails
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Logger configuration models for XP application."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Dict, Union
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
from pydantic import BaseModel, Field, field_validator
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class LoggingConfig(BaseModel):
|
|
12
|
+
"""Logging configuration.
|
|
13
|
+
|
|
14
|
+
Attributes:
|
|
15
|
+
path: log folder.
|
|
16
|
+
default_level: DEBUG, WARNING, INFO, ERROR, CRITICAL.
|
|
17
|
+
levels: Per-module log level overrides.
|
|
18
|
+
max_bytes: Maximum size in bytes before rotating (default: 1MB).
|
|
19
|
+
backup_count: Number of backup files to keep (default: 365).
|
|
20
|
+
log_format: Log message format string.
|
|
21
|
+
date_format: Date format string for timestamps.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
path: str = "log"
|
|
25
|
+
default_level: str = "DEBUG"
|
|
26
|
+
levels: Dict[str, int] = {
|
|
27
|
+
"xp": logging.DEBUG,
|
|
28
|
+
"xp.services.homekit": logging.WARNING,
|
|
29
|
+
"xp.services.server": logging.WARNING,
|
|
30
|
+
}
|
|
31
|
+
max_bytes: int = 1024 * 1024 # 1MB
|
|
32
|
+
backup_count: int = 365
|
|
33
|
+
log_format: str = (
|
|
34
|
+
"%(asctime)s - [%(threadName)s-%(thread)d] - %(levelname)s - %(name)s - %(message)s"
|
|
35
|
+
)
|
|
36
|
+
date_format: str = "%H:%M:%S"
|
|
37
|
+
|
|
38
|
+
@field_validator("levels", mode="before")
|
|
39
|
+
@classmethod
|
|
40
|
+
def convert_level_names(cls, v: Dict[str, Union[str, int]]) -> Dict[str, int]:
|
|
41
|
+
"""Convert string level names to numeric values.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
v: Dictionary with string or int log levels.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Dictionary with numeric log levels.
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
ValueError: If an invalid log level name is provided.
|
|
51
|
+
"""
|
|
52
|
+
level_map = {
|
|
53
|
+
"DEBUG": logging.DEBUG,
|
|
54
|
+
"INFO": logging.INFO,
|
|
55
|
+
"WARNING": logging.WARNING,
|
|
56
|
+
"ERROR": logging.ERROR,
|
|
57
|
+
"CRITICAL": logging.CRITICAL,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
result = {}
|
|
61
|
+
for module, level in v.items():
|
|
62
|
+
if isinstance(level, str):
|
|
63
|
+
level_upper = level.upper()
|
|
64
|
+
if level_upper not in level_map:
|
|
65
|
+
raise ValueError(
|
|
66
|
+
f"Invalid log level '{level}' for module '{module}'. "
|
|
67
|
+
f"Must be one of: {', '.join(level_map.keys())}"
|
|
68
|
+
)
|
|
69
|
+
result[module] = level_map[level_upper]
|
|
70
|
+
else:
|
|
71
|
+
result[module] = level
|
|
72
|
+
return result
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class ConbusLoggerConfig(BaseModel):
|
|
76
|
+
"""Logging configuration.
|
|
77
|
+
|
|
78
|
+
Attributes:
|
|
79
|
+
log: LoggingConfig instance for logging settings.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
log: LoggingConfig = Field(default_factory=LoggingConfig)
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def from_yaml(cls, file_path: str) -> "ConbusLoggerConfig":
|
|
86
|
+
"""Load configuration from YAML file.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
file_path: Path to the YAML configuration file.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
ConbusClientConfig instance loaded from file or default config.
|
|
93
|
+
"""
|
|
94
|
+
logger = logging.getLogger(__name__)
|
|
95
|
+
try:
|
|
96
|
+
with Path(file_path).open("r") as file:
|
|
97
|
+
data = yaml.safe_load(file)
|
|
98
|
+
return cls(**data)
|
|
99
|
+
|
|
100
|
+
except FileNotFoundError:
|
|
101
|
+
logger.error(f"File {file_path} does not exist, loading default")
|
|
102
|
+
return cls()
|
|
103
|
+
|
|
104
|
+
except yaml.YAMLError:
|
|
105
|
+
logger.error(f"File {file_path} is not valid")
|
|
106
|
+
# Return default config if YAML parsing fails
|
|
107
|
+
return cls()
|
|
@@ -1,51 +1,55 @@
|
|
|
1
1
|
"""Conbus Receive Service for receiving telegrams from Conbus servers.
|
|
2
2
|
|
|
3
|
-
This service uses
|
|
3
|
+
This service uses ConbusEventProtocol to provide receive-only functionality,
|
|
4
4
|
allowing clients to receive waiting event telegrams using empty telegram sends.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
import asyncio
|
|
7
8
|
import logging
|
|
8
|
-
from typing import Callable, Optional
|
|
9
|
+
from typing import Any, Callable, Optional
|
|
9
10
|
|
|
10
|
-
from twisted.internet.posixbase import PosixReactorBase
|
|
11
|
-
|
|
12
|
-
from xp.models import ConbusClientConfig
|
|
13
11
|
from xp.models.conbus.conbus_receive import ConbusReceiveResponse
|
|
14
12
|
from xp.models.protocol.conbus_protocol import TelegramReceivedEvent
|
|
15
|
-
from xp.services.protocol import
|
|
13
|
+
from xp.services.protocol.conbus_event_protocol import ConbusEventProtocol
|
|
16
14
|
|
|
17
15
|
|
|
18
|
-
class ConbusReceiveService
|
|
16
|
+
class ConbusReceiveService:
|
|
19
17
|
"""
|
|
20
18
|
Service for receiving telegrams from Conbus servers.
|
|
21
19
|
|
|
22
|
-
Uses
|
|
20
|
+
Uses ConbusEventProtocol to provide receive-only functionality
|
|
23
21
|
for collecting waiting event telegrams from the server.
|
|
22
|
+
|
|
23
|
+
Attributes:
|
|
24
|
+
conbus_protocol: Protocol instance for Conbus communication.
|
|
24
25
|
"""
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
reactor: PosixReactorBase,
|
|
30
|
-
) -> None:
|
|
27
|
+
conbus_protocol: ConbusEventProtocol
|
|
28
|
+
|
|
29
|
+
def __init__(self, conbus_protocol: ConbusEventProtocol) -> None:
|
|
31
30
|
"""Initialize the Conbus receive service.
|
|
32
31
|
|
|
33
32
|
Args:
|
|
34
|
-
|
|
35
|
-
reactor: Twisted reactor instance.
|
|
33
|
+
conbus_protocol: ConbusEventProtocol instance.
|
|
36
34
|
"""
|
|
37
|
-
super().__init__(cli_config, reactor)
|
|
38
35
|
self.progress_callback: Optional[Callable[[str], None]] = None
|
|
39
36
|
self.finish_callback: Optional[Callable[[ConbusReceiveResponse], None]] = None
|
|
40
37
|
self.receive_response: ConbusReceiveResponse = ConbusReceiveResponse(
|
|
41
38
|
success=True
|
|
42
39
|
)
|
|
43
40
|
|
|
41
|
+
self.conbus_protocol: ConbusEventProtocol = conbus_protocol
|
|
42
|
+
self.conbus_protocol.on_connection_made.connect(self.connection_made)
|
|
43
|
+
self.conbus_protocol.on_telegram_sent.connect(self.telegram_sent)
|
|
44
|
+
self.conbus_protocol.on_telegram_received.connect(self.telegram_received)
|
|
45
|
+
self.conbus_protocol.on_timeout.connect(self.timeout)
|
|
46
|
+
self.conbus_protocol.on_failed.connect(self.failed)
|
|
47
|
+
|
|
44
48
|
# Set up logging
|
|
45
49
|
self.logger = logging.getLogger(__name__)
|
|
46
50
|
|
|
47
|
-
def
|
|
48
|
-
"""Handle connection
|
|
51
|
+
def connection_made(self) -> None:
|
|
52
|
+
"""Handle connection made event."""
|
|
49
53
|
self.logger.debug("Connection established, waiting for telegrams.")
|
|
50
54
|
|
|
51
55
|
def telegram_sent(self, telegram_sent: str) -> None:
|
|
@@ -70,17 +74,13 @@ class ConbusReceiveService(ConbusProtocol):
|
|
|
70
74
|
self.receive_response.received_telegrams = []
|
|
71
75
|
self.receive_response.received_telegrams.append(telegram_received.frame)
|
|
72
76
|
|
|
73
|
-
def timeout(self) ->
|
|
74
|
-
"""Handle timeout event to stop receiving.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
False to stop the reactor.
|
|
78
|
-
"""
|
|
79
|
-
self.logger.info("Receive stopped after: %ss", self.timeout_seconds)
|
|
77
|
+
def timeout(self) -> None:
|
|
78
|
+
"""Handle timeout event to stop receiving."""
|
|
79
|
+
timeout = self.conbus_protocol.timeout_seconds
|
|
80
|
+
self.logger.info("Receive stopped after: %ss", timeout)
|
|
80
81
|
self.receive_response.success = True
|
|
81
82
|
if self.finish_callback:
|
|
82
83
|
self.finish_callback(self.receive_response)
|
|
83
|
-
return False
|
|
84
84
|
|
|
85
85
|
def failed(self, message: str) -> None:
|
|
86
86
|
"""Handle failed connection event.
|
|
@@ -94,22 +94,50 @@ class ConbusReceiveService(ConbusProtocol):
|
|
|
94
94
|
if self.finish_callback:
|
|
95
95
|
self.finish_callback(self.receive_response)
|
|
96
96
|
|
|
97
|
-
def
|
|
97
|
+
def init(
|
|
98
98
|
self,
|
|
99
99
|
progress_callback: Callable[[str], None],
|
|
100
100
|
finish_callback: Callable[[ConbusReceiveResponse], None],
|
|
101
101
|
timeout_seconds: Optional[float] = None,
|
|
102
|
+
event_loop: Optional[asyncio.AbstractEventLoop] = None,
|
|
102
103
|
) -> None:
|
|
103
|
-
"""
|
|
104
|
+
"""Setup callbacks and timeout for receiving telegrams.
|
|
104
105
|
|
|
105
106
|
Args:
|
|
106
107
|
progress_callback: Callback for each received telegram.
|
|
107
108
|
finish_callback: Callback when receiving completes.
|
|
108
109
|
timeout_seconds: Optional timeout in seconds.
|
|
110
|
+
event_loop: Optional event loop to use for async operations.
|
|
109
111
|
"""
|
|
110
112
|
self.logger.info("Starting receive")
|
|
111
113
|
if timeout_seconds:
|
|
112
|
-
self.timeout_seconds = timeout_seconds
|
|
114
|
+
self.conbus_protocol.timeout_seconds = timeout_seconds
|
|
113
115
|
self.progress_callback = progress_callback
|
|
114
116
|
self.finish_callback = finish_callback
|
|
115
|
-
|
|
117
|
+
|
|
118
|
+
if event_loop:
|
|
119
|
+
self.conbus_protocol.set_event_loop(event_loop)
|
|
120
|
+
|
|
121
|
+
def start_reactor(self) -> None:
|
|
122
|
+
"""Start the reactor."""
|
|
123
|
+
self.conbus_protocol.start_reactor()
|
|
124
|
+
|
|
125
|
+
def __enter__(self) -> "ConbusReceiveService":
|
|
126
|
+
"""Enter context manager.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Self for context manager protocol.
|
|
130
|
+
"""
|
|
131
|
+
# Reset state for singleton reuse
|
|
132
|
+
self.receive_response = ConbusReceiveResponse(success=True)
|
|
133
|
+
return self
|
|
134
|
+
|
|
135
|
+
def __exit__(
|
|
136
|
+
self, _exc_type: Optional[type], _exc_val: Optional[Exception], _exc_tb: Any
|
|
137
|
+
) -> None:
|
|
138
|
+
"""Exit context manager and disconnect signals."""
|
|
139
|
+
self.conbus_protocol.on_connection_made.disconnect(self.connection_made)
|
|
140
|
+
self.conbus_protocol.on_telegram_sent.disconnect(self.telegram_sent)
|
|
141
|
+
self.conbus_protocol.on_telegram_received.disconnect(self.telegram_received)
|
|
142
|
+
self.conbus_protocol.on_timeout.disconnect(self.timeout)
|
|
143
|
+
self.conbus_protocol.on_failed.disconnect(self.failed)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
This module implements the Twisted protocol for Conbus communication.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
+
import asyncio
|
|
6
7
|
import logging
|
|
7
8
|
from queue import SimpleQueue
|
|
8
9
|
from random import randint
|
|
@@ -302,14 +303,21 @@ class ConbusEventProtocol(protocol.Protocol, protocol.ClientFactory):
|
|
|
302
303
|
self.logger.info("Stopping reactor")
|
|
303
304
|
self._reactor.stop()
|
|
304
305
|
|
|
305
|
-
def
|
|
306
|
-
"""
|
|
307
|
-
# Connect to TCP server
|
|
306
|
+
def connect(self) -> None:
|
|
307
|
+
"""Connect to TCP server."""
|
|
308
308
|
self.logger.info(
|
|
309
309
|
f"Connecting to TCP server {self.cli_config.ip}:{self.cli_config.port}"
|
|
310
310
|
)
|
|
311
311
|
self._reactor.connectTCP(self.cli_config.ip, self.cli_config.port, self)
|
|
312
312
|
|
|
313
|
+
def disconnect(self) -> None:
|
|
314
|
+
"""Disconnect from TCP server."""
|
|
315
|
+
self.logger.info("Disconnecting TCP server")
|
|
316
|
+
self._reactor.disconnectAll()
|
|
317
|
+
|
|
318
|
+
def start_reactor(self) -> None:
|
|
319
|
+
"""Start the reactor if it's running."""
|
|
320
|
+
self.connect()
|
|
313
321
|
# Run the reactor (which now uses asyncio underneath)
|
|
314
322
|
self.logger.info("Starting reactor event loop.")
|
|
315
323
|
self._reactor.run()
|
|
@@ -340,6 +348,23 @@ class ConbusEventProtocol(protocol.Protocol, protocol.ClientFactory):
|
|
|
340
348
|
later = randint(10, 80) / 100
|
|
341
349
|
self.call_later(later, self.process_telegram_queue)
|
|
342
350
|
|
|
351
|
+
def set_event_loop(self, event_loop: asyncio.AbstractEventLoop) -> None:
|
|
352
|
+
"""Change the event loop.
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
event_loop: the event loop instance.
|
|
356
|
+
"""
|
|
357
|
+
reactor = self._reactor
|
|
358
|
+
if hasattr(reactor, "_asyncioEventloop"):
|
|
359
|
+
reactor._asyncioEventloop = event_loop
|
|
360
|
+
|
|
361
|
+
# Set reactor to running state
|
|
362
|
+
if not reactor.running:
|
|
363
|
+
reactor.running = True
|
|
364
|
+
if hasattr(reactor, "startRunning"):
|
|
365
|
+
reactor.startRunning()
|
|
366
|
+
self.logger.info("Set reactor to running state")
|
|
367
|
+
|
|
343
368
|
def __enter__(self) -> "ConbusEventProtocol":
|
|
344
369
|
"""Enter context manager.
|
|
345
370
|
|
xp/tui/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""TUI (Terminal User Interface) module for XP."""
|
xp/tui/app.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Protocol Monitor TUI Application."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
|
|
6
|
+
from textual.app import App, ComposeResult
|
|
7
|
+
from textual.widgets import Footer, Header
|
|
8
|
+
|
|
9
|
+
from xp.tui.widgets.protocol_log import ProtocolLogWidget
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ProtocolMonitorApp(App[None]):
|
|
13
|
+
"""Textual app for real-time protocol monitoring.
|
|
14
|
+
|
|
15
|
+
Displays live RX/TX telegram stream from Conbus server in an interactive
|
|
16
|
+
terminal interface with keyboard shortcuts for control.
|
|
17
|
+
|
|
18
|
+
Attributes:
|
|
19
|
+
container: ServiceContainer for dependency injection.
|
|
20
|
+
CSS_PATH: Path to CSS stylesheet file.
|
|
21
|
+
BINDINGS: Keyboard bindings for app actions.
|
|
22
|
+
TITLE: Application title displayed in header.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
CSS_PATH = Path(__file__).parent / "protocol.tcss"
|
|
26
|
+
TITLE = "Protocol Monitor"
|
|
27
|
+
|
|
28
|
+
BINDINGS = [
|
|
29
|
+
("q", "quit", "Quit"),
|
|
30
|
+
("c", "connect", "Connect"),
|
|
31
|
+
("d", "disconnect", "Disconnect"),
|
|
32
|
+
("1", "discover", "Discover"),
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
def __init__(self, container: Any) -> None:
|
|
36
|
+
"""Initialize the Protocol Monitor app.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
container: ServiceContainer for resolving services.
|
|
40
|
+
"""
|
|
41
|
+
super().__init__()
|
|
42
|
+
self.container = container
|
|
43
|
+
self.protocol_widget: Optional[ProtocolLogWidget] = None
|
|
44
|
+
|
|
45
|
+
def compose(self) -> ComposeResult:
|
|
46
|
+
"""Compose the app layout with widgets.
|
|
47
|
+
|
|
48
|
+
Yields:
|
|
49
|
+
Header, ProtocolLogWidget, and Footer widgets.
|
|
50
|
+
"""
|
|
51
|
+
yield Header()
|
|
52
|
+
self.protocol_widget = ProtocolLogWidget(container=self.container)
|
|
53
|
+
yield self.protocol_widget
|
|
54
|
+
yield Footer()
|
|
55
|
+
|
|
56
|
+
def action_discover(self) -> None:
|
|
57
|
+
"""Send discover telegram on 'D' key press.
|
|
58
|
+
|
|
59
|
+
Sends predefined discover telegram <S0000000000F01D00FA> to the bus.
|
|
60
|
+
"""
|
|
61
|
+
if self.protocol_widget:
|
|
62
|
+
self.protocol_widget.send_discover()
|
|
63
|
+
|
|
64
|
+
def action_connect(self) -> None:
|
|
65
|
+
"""Connect protocol on 'c' key press."""
|
|
66
|
+
if self.protocol_widget:
|
|
67
|
+
self.protocol_widget.connect()
|
|
68
|
+
|
|
69
|
+
def action_disconnect(self) -> None:
|
|
70
|
+
"""Disconnect protocol on 'd' key press."""
|
|
71
|
+
if self.protocol_widget:
|
|
72
|
+
self.protocol_widget.disconnect()
|
xp/tui/protocol.tcss
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/* Protocol Monitor TUI Styling */
|
|
2
|
+
|
|
3
|
+
/* App-level styling */
|
|
4
|
+
Screen {
|
|
5
|
+
background: $background;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/* Protocol Log Widget */
|
|
9
|
+
ProtocolLogWidget {
|
|
10
|
+
border: solid $primary;
|
|
11
|
+
height: 1fr;
|
|
12
|
+
background: $surface;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
ProtocolLogWidget > .connection-status {
|
|
16
|
+
color: $text;
|
|
17
|
+
text-align: center;
|
|
18
|
+
padding: 1;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
ProtocolLogWidget > .connection-status.connecting {
|
|
22
|
+
color: $warning;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
ProtocolLogWidget > .connection-status.connected {
|
|
26
|
+
color: $success;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
ProtocolLogWidget > .connection-status.failed {
|
|
30
|
+
color: $error;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* Message display styling */
|
|
34
|
+
.message-tx {
|
|
35
|
+
color: $success;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.message-rx {
|
|
39
|
+
color: $success;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.message-frame {
|
|
43
|
+
color: $text-muted;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* Footer styling */
|
|
47
|
+
Footer {
|
|
48
|
+
background: $panel;
|
|
49
|
+
color: $text;
|
|
50
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""TUI widgets package."""
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"""Protocol Log Widget for displaying telegram stream."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import Any, Optional
|
|
7
|
+
|
|
8
|
+
from textual.reactive import reactive
|
|
9
|
+
from textual.widget import Widget
|
|
10
|
+
from textual.widgets import RichLog
|
|
11
|
+
|
|
12
|
+
from xp.models.protocol.conbus_protocol import TelegramReceivedEvent
|
|
13
|
+
from xp.services.conbus.conbus_receive_service import ConbusReceiveService
|
|
14
|
+
from xp.services.protocol import ConbusEventProtocol
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ConnectionState(str, Enum):
|
|
18
|
+
"""Connection state enumeration.
|
|
19
|
+
|
|
20
|
+
Attributes:
|
|
21
|
+
DISCONNECTED: Not connected to server.
|
|
22
|
+
CONNECTING: Connection in progress.
|
|
23
|
+
CONNECTED: Successfully connected.
|
|
24
|
+
FAILED: Connection failed.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
DISCONNECTED = "DISCONNECTED"
|
|
28
|
+
CONNECTING = "CONNECTING"
|
|
29
|
+
CONNECTED = "CONNECTED"
|
|
30
|
+
FAILED = "FAILED"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ProtocolLogWidget(Widget):
|
|
34
|
+
"""Widget for displaying protocol telegram stream.
|
|
35
|
+
|
|
36
|
+
Connects to Conbus server via ConbusReceiveService and displays
|
|
37
|
+
live RX/TX telegram stream with color-coded direction markers.
|
|
38
|
+
|
|
39
|
+
Attributes:
|
|
40
|
+
container: ServiceContainer for dependency injection.
|
|
41
|
+
connection_state: Current connection state (reactive).
|
|
42
|
+
protocol: Reference to ConbusEventProtocol (prevents duplicate connections).
|
|
43
|
+
service: ConbusReceiveService instance.
|
|
44
|
+
logger: Logger instance for this widget.
|
|
45
|
+
log_widget: RichLog widget for displaying messages.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
connection_state = reactive(ConnectionState.DISCONNECTED)
|
|
49
|
+
|
|
50
|
+
def __init__(self, container: Any) -> None:
|
|
51
|
+
"""Initialize the Protocol Log widget.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
container: ServiceContainer for resolving services.
|
|
55
|
+
"""
|
|
56
|
+
super().__init__()
|
|
57
|
+
self.container = container
|
|
58
|
+
self.protocol: Optional[ConbusEventProtocol] = None
|
|
59
|
+
self.service: Optional[ConbusReceiveService] = None
|
|
60
|
+
self.logger = logging.getLogger(__name__)
|
|
61
|
+
self.log_widget: Optional[RichLog] = None
|
|
62
|
+
|
|
63
|
+
def compose(self) -> Any:
|
|
64
|
+
"""Compose the widget layout.
|
|
65
|
+
|
|
66
|
+
Yields:
|
|
67
|
+
RichLog widget for message display.
|
|
68
|
+
"""
|
|
69
|
+
self.log_widget = RichLog(highlight=True, markup=True)
|
|
70
|
+
yield self.log_widget
|
|
71
|
+
|
|
72
|
+
async def on_mount(self) -> None:
|
|
73
|
+
"""Initialize connection when widget mounts.
|
|
74
|
+
|
|
75
|
+
Delays connection by 0.5s to let UI render first.
|
|
76
|
+
Resolves ConbusReceiveService and connects signals.
|
|
77
|
+
"""
|
|
78
|
+
# Resolve service from container (singleton)
|
|
79
|
+
self.service = self.container.resolve(ConbusReceiveService)
|
|
80
|
+
self.protocol = self.service.conbus_protocol
|
|
81
|
+
|
|
82
|
+
# Connect psygnal signals
|
|
83
|
+
self.protocol.on_connection_made.connect(self._on_connection_made)
|
|
84
|
+
self.protocol.on_telegram_received.connect(self._on_telegram_received)
|
|
85
|
+
self.protocol.on_telegram_sent.connect(self._on_telegram_sent)
|
|
86
|
+
self.protocol.on_timeout.connect(self._on_timeout)
|
|
87
|
+
self.protocol.on_failed.connect(self._on_failed)
|
|
88
|
+
|
|
89
|
+
# Delay connection to let UI render
|
|
90
|
+
await asyncio.sleep(0.5)
|
|
91
|
+
await self._start_connection_async()
|
|
92
|
+
|
|
93
|
+
async def _start_connection_async(self) -> None:
|
|
94
|
+
"""Start TCP connection to Conbus server (async).
|
|
95
|
+
|
|
96
|
+
Guards against duplicate connections and sets up protocol signals.
|
|
97
|
+
Integrates Twisted reactor with Textual's asyncio loop cleanly.
|
|
98
|
+
"""
|
|
99
|
+
# Guard against duplicate connections (race condition)
|
|
100
|
+
if self.service is None:
|
|
101
|
+
self.logger.error("Service not initialized")
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
if self.protocol is None:
|
|
105
|
+
self.logger.error("Protocol not initialized")
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
# Set state to connecting
|
|
110
|
+
self.connection_state = ConnectionState.CONNECTING
|
|
111
|
+
if self.log_widget:
|
|
112
|
+
self.log_widget.write("[yellow]Connecting to Conbus server...[/yellow]")
|
|
113
|
+
|
|
114
|
+
# Store protocol reference
|
|
115
|
+
self.logger.info(f"Protocol object: {self.protocol}")
|
|
116
|
+
self.logger.info(f"Reactor object: {self.protocol._reactor}")
|
|
117
|
+
self.logger.info(f"Reactor running: {self.protocol._reactor.running}")
|
|
118
|
+
|
|
119
|
+
# Setup service callbacks
|
|
120
|
+
def progress_callback(telegram: str) -> None:
|
|
121
|
+
"""Handle progress updates for telegram reception.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
telegram: Received telegram string.
|
|
125
|
+
"""
|
|
126
|
+
pass
|
|
127
|
+
|
|
128
|
+
def finish_callback(response: Any) -> None:
|
|
129
|
+
"""Handle completion of telegram reception.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
response: Response object from telegram reception.
|
|
133
|
+
"""
|
|
134
|
+
pass
|
|
135
|
+
|
|
136
|
+
# Get the currently running asyncio event loop (Textual's loop)
|
|
137
|
+
event_loop = asyncio.get_running_loop()
|
|
138
|
+
self.logger.info(f"Current running loop: {event_loop}")
|
|
139
|
+
self.logger.info(f"Loop is running: {event_loop.is_running()}")
|
|
140
|
+
|
|
141
|
+
self.service.init(
|
|
142
|
+
progress_callback=progress_callback,
|
|
143
|
+
finish_callback=finish_callback,
|
|
144
|
+
timeout_seconds=None, # Continuous monitoring
|
|
145
|
+
event_loop=event_loop,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
reactor = self.service.conbus_protocol._reactor
|
|
149
|
+
# Schedule the connection on the running asyncio loop
|
|
150
|
+
# This ensures connectTCP is called in the context of the running loop
|
|
151
|
+
|
|
152
|
+
def do_connect() -> None:
|
|
153
|
+
"""Execute TCP connection in event loop context."""
|
|
154
|
+
self.logger.info("Executing connectTCP in event loop callback")
|
|
155
|
+
if self.protocol is not None:
|
|
156
|
+
reactor.connectTCP(
|
|
157
|
+
self.protocol.cli_config.ip,
|
|
158
|
+
self.protocol.cli_config.port,
|
|
159
|
+
self.protocol,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
event_loop.call_soon(do_connect)
|
|
163
|
+
self.logger.info("Scheduled connectTCP on running loop")
|
|
164
|
+
|
|
165
|
+
if self.log_widget:
|
|
166
|
+
self.log_widget.write(
|
|
167
|
+
f"[dim]→ {self.protocol.cli_config.ip}:{self.protocol.cli_config.port}[/dim]"
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# Wait for connection to establish
|
|
171
|
+
await asyncio.sleep(1.0)
|
|
172
|
+
self.logger.info(f"After 1s - transport: {self.protocol.transport}")
|
|
173
|
+
|
|
174
|
+
except Exception as e:
|
|
175
|
+
self.logger.error(f"Connection failed: {e}")
|
|
176
|
+
self.connection_state = ConnectionState.FAILED
|
|
177
|
+
if self.log_widget:
|
|
178
|
+
self.log_widget.write(f"[red]Connection error: {e}[/red]")
|
|
179
|
+
# Exit app after brief delay
|
|
180
|
+
self.set_timer(2.0, self.app.exit)
|
|
181
|
+
|
|
182
|
+
def _start_connection(self) -> None:
|
|
183
|
+
"""Start connection (sync wrapper for async method)."""
|
|
184
|
+
# Use run_worker to run async method from sync context
|
|
185
|
+
self.run_worker(self._start_connection_async(), exclusive=True)
|
|
186
|
+
|
|
187
|
+
def _on_connection_made(self) -> None:
|
|
188
|
+
"""Handle connection established signal.
|
|
189
|
+
|
|
190
|
+
Sets state to CONNECTED and displays success message.
|
|
191
|
+
"""
|
|
192
|
+
self.connection_state = ConnectionState.CONNECTED
|
|
193
|
+
if self.log_widget:
|
|
194
|
+
self.log_widget.write("[green]Connected to Conbus server[/green]")
|
|
195
|
+
self.log_widget.write("[dim]---[/dim]")
|
|
196
|
+
|
|
197
|
+
def _on_telegram_received(self, event: TelegramReceivedEvent) -> None:
|
|
198
|
+
"""Handle telegram received signal.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
event: Telegram received event with frame data.
|
|
202
|
+
"""
|
|
203
|
+
if self.log_widget:
|
|
204
|
+
# Display [RX] in green, frame in gray
|
|
205
|
+
self.log_widget.write(f"[green]\\[RX][/green] [dim]{event.frame}[/dim]")
|
|
206
|
+
|
|
207
|
+
def _on_telegram_sent(self, telegram: str) -> None:
|
|
208
|
+
"""Handle telegram sent signal.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
telegram: Sent telegram string.
|
|
212
|
+
"""
|
|
213
|
+
if self.log_widget:
|
|
214
|
+
# Display [TX] in green, frame in gray
|
|
215
|
+
self.log_widget.write(f"[green]\\[TX][/green] [dim]{telegram}[/dim]")
|
|
216
|
+
|
|
217
|
+
def _on_timeout(self) -> None:
|
|
218
|
+
"""Handle timeout signal.
|
|
219
|
+
|
|
220
|
+
Logs timeout but continues monitoring (no action needed).
|
|
221
|
+
"""
|
|
222
|
+
self.logger.debug("Timeout occurred (continuous monitoring)")
|
|
223
|
+
|
|
224
|
+
def _on_failed(self, error: str) -> None:
|
|
225
|
+
"""Handle connection failed signal.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
error: Error message describing the failure.
|
|
229
|
+
"""
|
|
230
|
+
self.connection_state = ConnectionState.FAILED
|
|
231
|
+
self.logger.error(f"Connection failed: {error}")
|
|
232
|
+
|
|
233
|
+
if self.log_widget:
|
|
234
|
+
self.log_widget.write(f"[red]Connection failed: {error}[/red]")
|
|
235
|
+
|
|
236
|
+
# Exit app after brief delay to show error
|
|
237
|
+
self.set_timer(2.0, self.app.exit)
|
|
238
|
+
|
|
239
|
+
def connect(self) -> None:
|
|
240
|
+
"""Connect to Conbus server."""
|
|
241
|
+
self._start_connection()
|
|
242
|
+
|
|
243
|
+
def disconnect(self) -> None:
|
|
244
|
+
"""Disconnect from Conbus server."""
|
|
245
|
+
if self.protocol:
|
|
246
|
+
self.protocol.disconnect()
|
|
247
|
+
|
|
248
|
+
def send_discover(self) -> None:
|
|
249
|
+
"""Send discover telegram.
|
|
250
|
+
|
|
251
|
+
Sends predefined discover telegram <S0000000000F01D00FA> to the bus.
|
|
252
|
+
Called when user presses 'd' key.
|
|
253
|
+
"""
|
|
254
|
+
if self.protocol is None:
|
|
255
|
+
self.logger.warning("Cannot send discover: not connected")
|
|
256
|
+
if self.log_widget:
|
|
257
|
+
self.log_widget.write(
|
|
258
|
+
"[yellow]Not connected, cannot send discover[/yellow]"
|
|
259
|
+
)
|
|
260
|
+
return
|
|
261
|
+
|
|
262
|
+
try:
|
|
263
|
+
# Send discover telegram
|
|
264
|
+
# Note: The telegram includes framing <>, but protocol may add it
|
|
265
|
+
# Check if protocol expects with or without brackets
|
|
266
|
+
from xp.models.telegram.system_function import SystemFunction
|
|
267
|
+
from xp.models.telegram.telegram_type import TelegramType
|
|
268
|
+
|
|
269
|
+
# Send discover: S 0000000000 F01 D00
|
|
270
|
+
self.protocol.send_telegram(
|
|
271
|
+
telegram_type=TelegramType.SYSTEM,
|
|
272
|
+
serial_number="0000000000",
|
|
273
|
+
system_function=SystemFunction.DISCOVERY,
|
|
274
|
+
data_value="00",
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
if self.log_widget:
|
|
278
|
+
self.log_widget.write("[yellow]Discover telegram sent[/yellow]")
|
|
279
|
+
|
|
280
|
+
except Exception as e:
|
|
281
|
+
self.logger.error(f"Failed to send discover: {e}")
|
|
282
|
+
if self.log_widget:
|
|
283
|
+
self.log_widget.write(f"[red]Failed to send discover: {e}[/red]")
|
|
284
|
+
|
|
285
|
+
def on_unmount(self) -> None:
|
|
286
|
+
"""Clean up when widget unmounts.
|
|
287
|
+
|
|
288
|
+
Disconnects signals and closes transport connection.
|
|
289
|
+
"""
|
|
290
|
+
if self.protocol is not None:
|
|
291
|
+
try:
|
|
292
|
+
# Disconnect all signals
|
|
293
|
+
self.protocol.on_connection_made.disconnect(self._on_connection_made)
|
|
294
|
+
self.protocol.on_telegram_received.disconnect(
|
|
295
|
+
self._on_telegram_received
|
|
296
|
+
)
|
|
297
|
+
self.protocol.on_telegram_sent.disconnect(self._on_telegram_sent)
|
|
298
|
+
self.protocol.on_timeout.disconnect(self._on_timeout)
|
|
299
|
+
self.protocol.on_failed.disconnect(self._on_failed)
|
|
300
|
+
|
|
301
|
+
# Close transport if connected
|
|
302
|
+
if self.protocol.transport:
|
|
303
|
+
self.protocol.disconnect()
|
|
304
|
+
|
|
305
|
+
# Reset protocol reference
|
|
306
|
+
self.protocol = None
|
|
307
|
+
|
|
308
|
+
# Set state to disconnected
|
|
309
|
+
self.connection_state = ConnectionState.DISCONNECTED
|
|
310
|
+
|
|
311
|
+
except Exception as e:
|
|
312
|
+
self.logger.error(f"Error during cleanup: {e}")
|
xp/utils/dependencies.py
CHANGED
|
@@ -7,6 +7,7 @@ from twisted.internet.interfaces import IConnector
|
|
|
7
7
|
from twisted.internet.posixbase import PosixReactorBase
|
|
8
8
|
|
|
9
9
|
from xp.models import ConbusClientConfig
|
|
10
|
+
from xp.models.conbus.conbus_logger_config import ConbusLoggerConfig
|
|
10
11
|
from xp.models.homekit.homekit_config import HomekitConfig
|
|
11
12
|
from xp.models.homekit.homekit_conson_config import ConsonModuleListConfig
|
|
12
13
|
from xp.services.actiontable.actiontable_serializer import ActionTableSerializer
|
|
@@ -72,6 +73,7 @@ from xp.services.telegram.telegram_discover_service import TelegramDiscoverServi
|
|
|
72
73
|
from xp.services.telegram.telegram_link_number_service import LinkNumberService
|
|
73
74
|
from xp.services.telegram.telegram_output_service import TelegramOutputService
|
|
74
75
|
from xp.services.telegram.telegram_service import TelegramService
|
|
76
|
+
from xp.utils.logging import LoggerService
|
|
75
77
|
|
|
76
78
|
asyncioreactor.install()
|
|
77
79
|
from twisted.internet import reactor # noqa: E402
|
|
@@ -87,7 +89,8 @@ class ServiceContainer:
|
|
|
87
89
|
|
|
88
90
|
def __init__(
|
|
89
91
|
self,
|
|
90
|
-
|
|
92
|
+
client_config_path: str = "cli.yml",
|
|
93
|
+
logger_config_path: str = "logger.yml",
|
|
91
94
|
homekit_config_path: str = "homekit.yml",
|
|
92
95
|
conson_config_path: str = "conson.yml",
|
|
93
96
|
server_port: int = 10001,
|
|
@@ -97,14 +100,16 @@ class ServiceContainer:
|
|
|
97
100
|
Initialize the service container.
|
|
98
101
|
|
|
99
102
|
Args:
|
|
100
|
-
|
|
103
|
+
client_config_path: Path to the Conbus CLI configuration file
|
|
104
|
+
logger_config_path: Path to the Conbus Loggerr configuration file
|
|
101
105
|
homekit_config_path: Path to the HomeKit configuration file
|
|
102
106
|
conson_config_path: Path to the Conson configuration file
|
|
103
107
|
server_port: Port for the server service
|
|
104
108
|
reverse_proxy_port: Port for the reverse proxy service
|
|
105
109
|
"""
|
|
106
110
|
self.container = punq.Container()
|
|
107
|
-
self.
|
|
111
|
+
self._client_config_path = client_config_path
|
|
112
|
+
self._logger_config_path = logger_config_path
|
|
108
113
|
self._homekit_config_path = homekit_config_path
|
|
109
114
|
self._conson_config_path = conson_config_path
|
|
110
115
|
self._server_port = server_port
|
|
@@ -117,7 +122,13 @@ class ServiceContainer:
|
|
|
117
122
|
# ConbusClientConfig
|
|
118
123
|
self.container.register(
|
|
119
124
|
ConbusClientConfig,
|
|
120
|
-
factory=lambda: ConbusClientConfig.from_yaml(self.
|
|
125
|
+
factory=lambda: ConbusClientConfig.from_yaml(self._client_config_path),
|
|
126
|
+
scope=punq.Scope.singleton,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
self.container.register(
|
|
130
|
+
ConbusLoggerConfig,
|
|
131
|
+
factory=lambda: ConbusLoggerConfig.from_yaml(self._logger_config_path),
|
|
121
132
|
scope=punq.Scope.singleton,
|
|
122
133
|
)
|
|
123
134
|
|
|
@@ -332,8 +343,7 @@ class ServiceContainer:
|
|
|
332
343
|
self.container.register(
|
|
333
344
|
ConbusReceiveService,
|
|
334
345
|
factory=lambda: ConbusReceiveService(
|
|
335
|
-
|
|
336
|
-
reactor=self.container.resolve(PosixReactorBase),
|
|
346
|
+
conbus_protocol=self.container.resolve(ConbusEventProtocol)
|
|
337
347
|
),
|
|
338
348
|
scope=punq.Scope.singleton,
|
|
339
349
|
)
|
|
@@ -387,6 +397,15 @@ class ServiceContainer:
|
|
|
387
397
|
scope=punq.Scope.singleton,
|
|
388
398
|
)
|
|
389
399
|
|
|
400
|
+
# Logging
|
|
401
|
+
self.container.register(
|
|
402
|
+
LoggerService,
|
|
403
|
+
factory=lambda: LoggerService(
|
|
404
|
+
logger_config=self.container.resolve(ConbusLoggerConfig),
|
|
405
|
+
),
|
|
406
|
+
scope=punq.Scope.singleton,
|
|
407
|
+
)
|
|
408
|
+
|
|
390
409
|
# Module type services layer
|
|
391
410
|
self.container.register(ModuleTypeService, scope=punq.Scope.singleton)
|
|
392
411
|
|
xp/utils/logging.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Logging service for XP application."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from logging.handlers import RotatingFileHandler
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from xp.models.conbus.conbus_logger_config import ConbusLoggerConfig
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class LoggerService:
|
|
11
|
+
"""Service for managing logging configuration and setup."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, logger_config: ConbusLoggerConfig):
|
|
14
|
+
"""Initialize LoggerService with configuration.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
logger_config: Logger configuration object.
|
|
18
|
+
"""
|
|
19
|
+
self.logging_config = logger_config.log
|
|
20
|
+
self.logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
def setup(self) -> None:
|
|
23
|
+
"""Setup console and file logging with configured levels."""
|
|
24
|
+
# Setup file logging for term app
|
|
25
|
+
self.setup_console_logging(
|
|
26
|
+
self.logging_config.log_format, self.logging_config.date_format
|
|
27
|
+
)
|
|
28
|
+
self.setup_file_logging(
|
|
29
|
+
self.logging_config.log_format, self.logging_config.date_format
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
for module in self.logging_config.levels.keys():
|
|
33
|
+
logging.getLogger(module).setLevel(self.logging_config.levels[module])
|
|
34
|
+
|
|
35
|
+
def setup_console_logging(self, log_format: str, date_format: str) -> None:
|
|
36
|
+
"""Setup console logging with specified format.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
log_format: Log message format string.
|
|
40
|
+
date_format: Date format string for log timestamps.
|
|
41
|
+
"""
|
|
42
|
+
# Force format on root logger and all handlers
|
|
43
|
+
formatter = logging.Formatter(log_format, datefmt=date_format)
|
|
44
|
+
root_logger = logging.getLogger()
|
|
45
|
+
|
|
46
|
+
# Set log level from CLI argument
|
|
47
|
+
numeric_level = getattr(logging, self.logging_config.default_level.upper())
|
|
48
|
+
root_logger.setLevel(numeric_level)
|
|
49
|
+
|
|
50
|
+
# Update all existing handlers or create new one
|
|
51
|
+
if root_logger.handlers:
|
|
52
|
+
for handler in root_logger.handlers:
|
|
53
|
+
handler.setFormatter(formatter)
|
|
54
|
+
else:
|
|
55
|
+
handler = logging.StreamHandler()
|
|
56
|
+
handler.setFormatter(formatter)
|
|
57
|
+
root_logger.addHandler(handler)
|
|
58
|
+
|
|
59
|
+
def setup_file_logging(self, log_format: str, date_format: str) -> None:
|
|
60
|
+
"""Setup file logging with rotation for term application.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
log_format: Log message format string.
|
|
64
|
+
date_format: Date format string for log timestamps.
|
|
65
|
+
"""
|
|
66
|
+
log_path = Path(self.logging_config.path)
|
|
67
|
+
log_level = self.logging_config.default_level
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
# Create log directory if it doesn't exist
|
|
71
|
+
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
72
|
+
|
|
73
|
+
# Create rotating file handler
|
|
74
|
+
file_handler = RotatingFileHandler(
|
|
75
|
+
log_path,
|
|
76
|
+
maxBytes=self.logging_config.max_bytes,
|
|
77
|
+
backupCount=self.logging_config.backup_count,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Configure formatter to match console format
|
|
81
|
+
formatter = logging.Formatter(log_format, datefmt=date_format)
|
|
82
|
+
file_handler.setFormatter(formatter)
|
|
83
|
+
file_handler.setLevel(log_level)
|
|
84
|
+
|
|
85
|
+
# Attach to root logger
|
|
86
|
+
root_logger = logging.getLogger()
|
|
87
|
+
root_logger.addHandler(file_handler)
|
|
88
|
+
|
|
89
|
+
except (OSError, PermissionError) as e:
|
|
90
|
+
self.logger.warning(f"Failed to setup file logging at {log_path}: {e}")
|
|
91
|
+
self.logger.warning("Continuing without file logging")
|
|
File without changes
|
|
File without changes
|