ndVpi 1.100.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.
- ndvpi/__init__.py +25 -0
- ndvpi/charm_enhance.py +279 -0
- ndvpi/cli.py +15 -0
- ndvpi/collquan_ext.py +151 -0
- ndvpi/diagnostics.py +126 -0
- ndvpi/history.py +92 -0
- ndvpi/menu.py +270 -0
- ndvpi/monitor.py +122 -0
- ndvpi/network_config.py +325 -0
- ndvpi/network_info.py +151 -0
- ndvpi/optimizer.py +113 -0
- ndvpi/version.py +1 -0
- ndvpi/wifi.py +178 -0
- ndvpi-1.100.0.dist-info/METADATA +176 -0
- ndvpi-1.100.0.dist-info/RECORD +18 -0
- ndvpi-1.100.0.dist-info/WHEEL +5 -0
- ndvpi-1.100.0.dist-info/entry_points.txt +2 -0
- ndvpi-1.100.0.dist-info/top_level.txt +1 -0
ndvpi/__init__.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""ndVpi — фоновая оптимизация сети и сканер Wi-Fi."""
|
|
2
|
+
from .version import __version__
|
|
3
|
+
from .wifi import WiFiScanner
|
|
4
|
+
from .network_info import NetworkInfo
|
|
5
|
+
from .optimizer import NetworkOptimizer
|
|
6
|
+
from .network_config import NetworkConfig
|
|
7
|
+
from .monitor import NetworkMonitor
|
|
8
|
+
from .diagnostics import NetworkDiagnostics
|
|
9
|
+
from .history import SessionHistory
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"__version__",
|
|
13
|
+
"WiFiScanner",
|
|
14
|
+
"NetworkInfo",
|
|
15
|
+
"NetworkOptimizer",
|
|
16
|
+
"NetworkConfig",
|
|
17
|
+
"NetworkMonitor",
|
|
18
|
+
"NetworkDiagnostics",
|
|
19
|
+
"SessionHistory",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
from . import collquan_ext # noqa: F401
|
|
24
|
+
except Exception:
|
|
25
|
+
pass
|
ndvpi/charm_enhance.py
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""Мягкая интеграция с CharmPkk: красиво, если есть; обычно, если нет."""
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
_charm = None
|
|
5
|
+
_c = None
|
|
6
|
+
_available = False
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
from charmpkk import console as _c
|
|
10
|
+
_charm = __import__("charmpkk")
|
|
11
|
+
_available = True
|
|
12
|
+
except Exception:
|
|
13
|
+
_available = False
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def has_charm():
|
|
17
|
+
return _available
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def console():
|
|
21
|
+
return _c
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def theme(name="tokyo_night"):
|
|
25
|
+
if not _available:
|
|
26
|
+
return None
|
|
27
|
+
return _c.get_theme(name)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ---------- Цветной вывод ----------
|
|
31
|
+
|
|
32
|
+
def _fg(color):
|
|
33
|
+
if not _available or not color:
|
|
34
|
+
return None
|
|
35
|
+
if isinstance(color, str) and color.startswith("#"):
|
|
36
|
+
return _c.fg_hex(color)
|
|
37
|
+
return color
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def print_msg(text, color=None, style=None):
|
|
41
|
+
if _available:
|
|
42
|
+
print(_c.colorize(text, fg=_fg(color), style=style))
|
|
43
|
+
else:
|
|
44
|
+
print(text)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def banner(title, subtitle=None, color="#7aa2ff"):
|
|
48
|
+
if _available:
|
|
49
|
+
return _c.banner(title, subtitle, fg=_fg(color))
|
|
50
|
+
if subtitle:
|
|
51
|
+
return f"\n=== {title} ===\n{subtitle}\n"
|
|
52
|
+
return f"\n=== {title} ===\n"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def box(content, title=None, color="#7aa2ff"):
|
|
56
|
+
if _available:
|
|
57
|
+
return _c.box(content, title=title, border_fg=_fg(color))
|
|
58
|
+
lines = content if isinstance(content, list) else [content]
|
|
59
|
+
header = f"--- {title} ---" if title else "-" * 20
|
|
60
|
+
return "\n".join([header] + [str(l) for l in lines] + ["-" * 20])
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def table(headers, rows, header_color="#7aa2ff"):
|
|
64
|
+
if _available:
|
|
65
|
+
t = _c.Table(headers=headers, rows=rows, header_fg=_fg(header_color))
|
|
66
|
+
return t.render()
|
|
67
|
+
all_rows = [list(headers)] + [list(r) for r in rows]
|
|
68
|
+
n_cols = len(headers)
|
|
69
|
+
widths = [max(len(str(r[i])) if i < len(r) else 0 for r in all_rows)
|
|
70
|
+
for i in range(n_cols)]
|
|
71
|
+
lines = []
|
|
72
|
+
for i, row in enumerate(all_rows):
|
|
73
|
+
cells = []
|
|
74
|
+
for j in range(n_cols):
|
|
75
|
+
v = str(row[j]) if j < len(row) else ""
|
|
76
|
+
cells.append(v.ljust(widths[j]))
|
|
77
|
+
lines.append(" | ".join(cells))
|
|
78
|
+
if i == 0:
|
|
79
|
+
lines.append("-+-".join("-" * w for w in widths))
|
|
80
|
+
return "\n".join(lines)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def key_value(data, key_color="#8b93a7"):
|
|
84
|
+
if _available:
|
|
85
|
+
return _c.key_value(data, key_fg=_fg(key_color))
|
|
86
|
+
max_k = max((len(str(k)) for k in data), default=0)
|
|
87
|
+
return "\n".join(f" {str(k).ljust(max_k)}: {v}" for k, v in data.items())
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# ---------- Сообщения: печать (возвращают None) ----------
|
|
91
|
+
|
|
92
|
+
def info(msg, color="#3b82f6"):
|
|
93
|
+
if _available:
|
|
94
|
+
print(_c.info(msg))
|
|
95
|
+
else:
|
|
96
|
+
print(f"[i] {msg}")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def success(msg, color="#22c55e"):
|
|
100
|
+
if _available:
|
|
101
|
+
print(_c.success(msg))
|
|
102
|
+
else:
|
|
103
|
+
print(f"[+] {msg}")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def warning(msg, color="#f59e0b"):
|
|
107
|
+
if _available:
|
|
108
|
+
print(_c.warning(msg))
|
|
109
|
+
else:
|
|
110
|
+
print(f"[!] {msg}")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def error(msg, color="#ef4444"):
|
|
114
|
+
if _available:
|
|
115
|
+
print(_c.error(msg))
|
|
116
|
+
else:
|
|
117
|
+
print(f"[x] {msg}")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# ---------- Сообщения: рендер (возвращают строку) ----------
|
|
121
|
+
# Используйте эти версии, когда нужно встроить сообщение в составной текст
|
|
122
|
+
# (например, в "\n".join(...)), а не печатать его сразу.
|
|
123
|
+
|
|
124
|
+
def render_info(msg, color="#3b82f6"):
|
|
125
|
+
if _available:
|
|
126
|
+
return _c.info(msg)
|
|
127
|
+
return f"[i] {msg}"
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def render_success(msg, color="#22c55e"):
|
|
131
|
+
if _available:
|
|
132
|
+
return _c.success(msg)
|
|
133
|
+
return f"[+] {msg}"
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def render_warning(msg, color="#f59e0b"):
|
|
137
|
+
if _available:
|
|
138
|
+
return _c.warning(msg)
|
|
139
|
+
return f"[!] {msg}"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def render_error(msg, color="#ef4444"):
|
|
143
|
+
if _available:
|
|
144
|
+
return _c.error(msg)
|
|
145
|
+
return f"[x] {msg}"
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
# ---------- Интерактив ----------
|
|
149
|
+
|
|
150
|
+
def ask(prompt, default=None):
|
|
151
|
+
if _available:
|
|
152
|
+
return _c.ask(prompt, default=default)
|
|
153
|
+
hint = f" [{default}]" if default is not None else ""
|
|
154
|
+
try:
|
|
155
|
+
v = input(f"? {prompt}{hint}: ").strip()
|
|
156
|
+
return v or default
|
|
157
|
+
except (EOFError, KeyboardInterrupt):
|
|
158
|
+
return default
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def confirm(prompt, default=False):
|
|
162
|
+
if _available:
|
|
163
|
+
return _c.confirm(prompt, default=default)
|
|
164
|
+
d = "Y/n" if default else "y/N"
|
|
165
|
+
try:
|
|
166
|
+
v = input(f"? {prompt} ({d}): ").strip().lower()
|
|
167
|
+
except (EOFError, KeyboardInterrupt):
|
|
168
|
+
return default
|
|
169
|
+
return v.startswith("y") if v else default
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def select(title, options, color="#7aa2ff"):
|
|
173
|
+
if not options:
|
|
174
|
+
return None
|
|
175
|
+
if _available:
|
|
176
|
+
return _c.select(title, options, fg_cursor=_fg(color), fg_title=_fg(color))
|
|
177
|
+
print(title)
|
|
178
|
+
for i, opt in enumerate(options, 1):
|
|
179
|
+
print(f" {i}. {opt}")
|
|
180
|
+
try:
|
|
181
|
+
idx = int(input("> ").strip()) - 1
|
|
182
|
+
if 0 <= idx < len(options):
|
|
183
|
+
return options[idx]
|
|
184
|
+
except (ValueError, EOFError, KeyboardInterrupt):
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# ---------- Прогресс и спиннеры ----------
|
|
189
|
+
|
|
190
|
+
class _NullProgress:
|
|
191
|
+
def __enter__(self):
|
|
192
|
+
return self
|
|
193
|
+
|
|
194
|
+
def __exit__(self, *exc):
|
|
195
|
+
print()
|
|
196
|
+
|
|
197
|
+
def update(self, current):
|
|
198
|
+
sys.stdout.write(f"\r {current}%")
|
|
199
|
+
sys.stdout.flush()
|
|
200
|
+
|
|
201
|
+
def finish(self, msg=None):
|
|
202
|
+
if msg:
|
|
203
|
+
print(msg)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def progress(total=100, label="", color="#7aa2ff"):
|
|
207
|
+
if _available:
|
|
208
|
+
return _c.FancyProgressBar(total=total, label=label, fg=_fg(color))
|
|
209
|
+
return _NullProgress()
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class _NullSpinner:
|
|
213
|
+
def __enter__(self):
|
|
214
|
+
print()
|
|
215
|
+
return self
|
|
216
|
+
|
|
217
|
+
def __exit__(self, *exc):
|
|
218
|
+
print()
|
|
219
|
+
|
|
220
|
+
def stop(self, final=None):
|
|
221
|
+
if final:
|
|
222
|
+
print(final)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def spinner(text="Loading", color="#7aa2ff"):
|
|
226
|
+
if _available:
|
|
227
|
+
return _c.Spinner(text, fg=_fg(color))
|
|
228
|
+
print(text + "...")
|
|
229
|
+
return _NullSpinner()
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
# ---------- Live Dashboard ----------
|
|
233
|
+
|
|
234
|
+
class LiveDashboard:
|
|
235
|
+
"""Живой дашборд. С CharmPkk использует Live, без него — ANSI-перерисовку."""
|
|
236
|
+
|
|
237
|
+
def __init__(self):
|
|
238
|
+
self._live = None
|
|
239
|
+
self._running = False
|
|
240
|
+
self._last_lines = 0
|
|
241
|
+
|
|
242
|
+
def __enter__(self):
|
|
243
|
+
if _available:
|
|
244
|
+
self._live = _c.Live()
|
|
245
|
+
self._live.__enter__()
|
|
246
|
+
self._running = True
|
|
247
|
+
return self
|
|
248
|
+
|
|
249
|
+
def update(self, content):
|
|
250
|
+
if not self._running:
|
|
251
|
+
return
|
|
252
|
+
|
|
253
|
+
if _available and self._live:
|
|
254
|
+
self._live.update(content)
|
|
255
|
+
else:
|
|
256
|
+
lines = content.split("\n")
|
|
257
|
+
if self._last_lines > 0:
|
|
258
|
+
sys.stdout.write(f"\033[{self._last_lines}A")
|
|
259
|
+
|
|
260
|
+
max_lines = max(self._last_lines, len(lines))
|
|
261
|
+
for i in range(max_lines):
|
|
262
|
+
sys.stdout.write("\033[2K")
|
|
263
|
+
if i < len(lines):
|
|
264
|
+
sys.stdout.write(lines[i])
|
|
265
|
+
sys.stdout.write("\n")
|
|
266
|
+
|
|
267
|
+
if max_lines > len(lines):
|
|
268
|
+
sys.stdout.write(f"\033[{max_lines - len(lines)}A")
|
|
269
|
+
|
|
270
|
+
self._last_lines = len(lines)
|
|
271
|
+
sys.stdout.flush()
|
|
272
|
+
|
|
273
|
+
def __exit__(self, *exc):
|
|
274
|
+
self._running = False
|
|
275
|
+
if _available and self._live:
|
|
276
|
+
self._live.__exit__(*exc)
|
|
277
|
+
else:
|
|
278
|
+
sys.stdout.write("\n")
|
|
279
|
+
return False
|
ndvpi/cli.py
ADDED
ndvpi/collquan_ext.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Интеграция ndVpi с Collquan (CharmPkk)."""
|
|
2
|
+
try:
|
|
3
|
+
from charmpkk.terminals import CollquanShell
|
|
4
|
+
except Exception:
|
|
5
|
+
CollquanShell = None
|
|
6
|
+
|
|
7
|
+
from .wifi import WiFiScanner
|
|
8
|
+
from .network_info import NetworkInfo
|
|
9
|
+
from .optimizer import NetworkOptimizer
|
|
10
|
+
from .network_config import NetworkConfig, _is_admin
|
|
11
|
+
from .monitor import NetworkMonitor
|
|
12
|
+
from .diagnostics import NetworkDiagnostics
|
|
13
|
+
from .history import SessionHistory
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
_optimizer = NetworkOptimizer()
|
|
17
|
+
_config = NetworkConfig()
|
|
18
|
+
_history = SessionHistory() # Глобальная история для Collquan
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _cmd_scan(args):
|
|
22
|
+
data = WiFiScanner().current_network()
|
|
23
|
+
if not data:
|
|
24
|
+
print("Нет данных о Wi-Fi.")
|
|
25
|
+
return
|
|
26
|
+
for k, v in data.items():
|
|
27
|
+
print(f"{k}: {v}")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _cmd_info(args):
|
|
31
|
+
info = NetworkInfo()
|
|
32
|
+
print(f"IP: {info.local_ip()}")
|
|
33
|
+
print(f"Шлюз: {info.gateway()}")
|
|
34
|
+
print(f"DNS: {', '.join(info.dns_servers()) or '—'}")
|
|
35
|
+
print(f"Ping: {info.ping() or '—'} ms")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _cmd_opt(args):
|
|
39
|
+
if args and args[0] == "start":
|
|
40
|
+
_optimizer.start()
|
|
41
|
+
print("Оптимизатор запущен.")
|
|
42
|
+
elif args and args[0] == "stop":
|
|
43
|
+
_optimizer.stop()
|
|
44
|
+
print("Оптимизатор остановлен.")
|
|
45
|
+
else:
|
|
46
|
+
print("Использование: ndopt start|stop")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _cmd_status(args):
|
|
50
|
+
s = _optimizer.status()
|
|
51
|
+
print(f"Запущен: {s['running']}")
|
|
52
|
+
print(f"DNS-кэш: {s['dns_cache_size']} записей (TTL {s['dns_ttl']}s)")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _cmd_dns(args):
|
|
56
|
+
if not args or args[0] == "show":
|
|
57
|
+
servers = _config.get_dns()
|
|
58
|
+
print("DNS:", ", ".join(servers) if servers else "автоматически")
|
|
59
|
+
return
|
|
60
|
+
if args[0] == "set":
|
|
61
|
+
if len(args) < 2:
|
|
62
|
+
print("Использование: nddns set <ip1> [ip2]...")
|
|
63
|
+
return
|
|
64
|
+
ok, msg = _config.set_dns(args[1:])
|
|
65
|
+
print(("[+] " if ok else "[x] ") + msg)
|
|
66
|
+
return
|
|
67
|
+
if args[0] == "reset":
|
|
68
|
+
ok, msg = _config.reset_dns()
|
|
69
|
+
print(("[+] " if ok else "[x] ") + msg)
|
|
70
|
+
return
|
|
71
|
+
print("Использование: nddns [show|set <ip>...|reset]")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _cmd_pass(args):
|
|
75
|
+
if len(args) < 2:
|
|
76
|
+
print("Использование: ndpass <ssid> <new_password>")
|
|
77
|
+
return
|
|
78
|
+
ssid, pwd = args[0], args[1]
|
|
79
|
+
print(f"[!] Внимание: смена пароля для '{ssid}' может разорвать соединение.")
|
|
80
|
+
if input("Продолжить? [y/N] ").strip().lower() != "y":
|
|
81
|
+
print("Отменено.")
|
|
82
|
+
return
|
|
83
|
+
ok, msg = _config.change_wifi_password(ssid, pwd)
|
|
84
|
+
print(("[+] " if ok else "[x] ") + msg)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _cmd_adapters(args):
|
|
88
|
+
if not args or args[0] == "list":
|
|
89
|
+
for a in _config.list_adapters():
|
|
90
|
+
print(f" {a.get('name', '?'):<30} {a}")
|
|
91
|
+
return
|
|
92
|
+
if args[0] in ("enable", "disable") and len(args) >= 2:
|
|
93
|
+
ok, msg = _config.set_adapter_state(
|
|
94
|
+
args[1], enabled=(args[0] == "enable"))
|
|
95
|
+
print(("[+] " if ok else "[x] ") + msg)
|
|
96
|
+
return
|
|
97
|
+
print("Использование: ndadapters [list|enable|disable <name>]")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _cmd_mtu(args):
|
|
101
|
+
if not args:
|
|
102
|
+
print("Использование: ndmtu <interface> [new_mtu]")
|
|
103
|
+
return
|
|
104
|
+
iface = args[0]
|
|
105
|
+
if len(args) == 1:
|
|
106
|
+
mtu = _config.get_mtu(iface)
|
|
107
|
+
print(f"MTU {iface} = {mtu if mtu is not None else '—'}")
|
|
108
|
+
return
|
|
109
|
+
ok, msg = _config.set_mtu(iface, args[1])
|
|
110
|
+
print(("[+] " if ok else "[x] ") + msg)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _cmd_admin(args):
|
|
114
|
+
print("Администратор:", "да" if _is_admin() else "нет")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _cmd_live(args):
|
|
118
|
+
print("Запуск мониторинга в реальном времени (Ctrl+C для выхода)...")
|
|
119
|
+
monitor = NetworkMonitor(history=_history)
|
|
120
|
+
monitor.run()
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _cmd_diag(args):
|
|
124
|
+
print("Запуск диагностики сети...\n")
|
|
125
|
+
diag = NetworkDiagnostics()
|
|
126
|
+
print(diag.get_report())
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _cmd_export(args):
|
|
130
|
+
if not _history.records:
|
|
131
|
+
print("История пуста.")
|
|
132
|
+
return
|
|
133
|
+
reports_dir = Path.home() / ".ndvpi" / "reports"
|
|
134
|
+
p_json = _history.export_json(reports_dir / "collquan_session.json")
|
|
135
|
+
p_csv = _history.export_csv(reports_dir / "collquan_session.csv")
|
|
136
|
+
print(f"Экспортировано:\n {p_json}\n {p_csv}")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
if CollquanShell is not None:
|
|
140
|
+
CollquanShell.register("ndscan", "Сканировать Wi-Fi")(_cmd_scan)
|
|
141
|
+
CollquanShell.register("ndinfo", "Информация о сети")(_cmd_info)
|
|
142
|
+
CollquanShell.register("ndopt", "Оптимизатор: start/stop")(_cmd_opt)
|
|
143
|
+
CollquanShell.register("ndstatus", "Статус оптимизатора")(_cmd_status)
|
|
144
|
+
CollquanShell.register("nddns", "DNS: show/set/reset")(_cmd_dns)
|
|
145
|
+
CollquanShell.register("ndpass", "Сменить пароль Wi-Fi: <ssid> <pass>")(_cmd_pass)
|
|
146
|
+
CollquanShell.register("ndadapters", "Адаптеры: list/enable/disable")(_cmd_adapters)
|
|
147
|
+
CollquanShell.register("ndmtu", "MTU: <iface> [new_mtu]")(_cmd_mtu)
|
|
148
|
+
CollquanShell.register("ndadmin", "Проверить права администратора")(_cmd_admin)
|
|
149
|
+
CollquanShell.register("ndlive", "Live-монитор сети")(_cmd_live)
|
|
150
|
+
CollquanShell.register("nddiag", "Диагностика сети")(_cmd_diag)
|
|
151
|
+
CollquanShell.register("ndexport", "Экспорт истории в JSON/CSV")(_cmd_export)
|
ndvpi/diagnostics.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Диагностика сети: выявление проблем и рекомендации."""
|
|
2
|
+
import socket
|
|
3
|
+
from .network_info import NetworkInfo
|
|
4
|
+
from .wifi import WiFiScanner
|
|
5
|
+
from .network_config import NetworkConfig
|
|
6
|
+
from . import charm_enhance as ui
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class CheckResult:
|
|
10
|
+
def __init__(self, name, ok, details, recommendation=""):
|
|
11
|
+
self.name = name
|
|
12
|
+
self.ok = ok
|
|
13
|
+
self.details = details
|
|
14
|
+
self.recommendation = recommendation
|
|
15
|
+
|
|
16
|
+
def __str__(self):
|
|
17
|
+
return f"{'[OK]' if self.ok else '[FAIL]'} {self.name}: {self.details}"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class NetworkDiagnostics:
|
|
21
|
+
def __init__(self):
|
|
22
|
+
self.info = NetworkInfo()
|
|
23
|
+
self.wifi = WiFiScanner()
|
|
24
|
+
self.config = NetworkConfig()
|
|
25
|
+
|
|
26
|
+
def run_checks(self):
|
|
27
|
+
results = []
|
|
28
|
+
|
|
29
|
+
# 1. Адаптеры
|
|
30
|
+
adapters = self.config.list_adapters()
|
|
31
|
+
enabled = [a for a in adapters
|
|
32
|
+
if a.get("state") in ("Connected", "Подключено", "up", "Подключен")]
|
|
33
|
+
if not adapters:
|
|
34
|
+
results.append(CheckResult("Адаптеры", False,
|
|
35
|
+
"Не найдены сетевые адаптеры",
|
|
36
|
+
"Проверьте драйверы сетевой карты"))
|
|
37
|
+
elif not enabled:
|
|
38
|
+
results.append(CheckResult("Адаптеры", False,
|
|
39
|
+
"Все адаптеры отключены",
|
|
40
|
+
"Включите сетевой адаптер в настройках ОС"))
|
|
41
|
+
else:
|
|
42
|
+
results.append(CheckResult("Адаптеры", True,
|
|
43
|
+
f"Активен: {enabled[0].get('name', 'Неизвестно')}"))
|
|
44
|
+
|
|
45
|
+
# 2. Wi-Fi
|
|
46
|
+
wifi_data = self.wifi.current_network()
|
|
47
|
+
if wifi_data:
|
|
48
|
+
results.append(CheckResult("Wi-Fi", True,
|
|
49
|
+
f"Подключено к {wifi_data.get('ssid', '?')}"))
|
|
50
|
+
else:
|
|
51
|
+
results.append(CheckResult("Wi-Fi", True, "Wi-Fi не активен (возможно Ethernet)"))
|
|
52
|
+
|
|
53
|
+
# 3. Шлюз
|
|
54
|
+
gw = self.info.gateway()
|
|
55
|
+
if not gw:
|
|
56
|
+
results.append(CheckResult("Шлюз", False,
|
|
57
|
+
"Шлюз не найден",
|
|
58
|
+
"Проверьте настройки DHCP или задайте IP вручную"))
|
|
59
|
+
else:
|
|
60
|
+
gw_ping = self.info.ping(gw, timeout=1)
|
|
61
|
+
if gw_ping is not None:
|
|
62
|
+
results.append(CheckResult("Шлюз", True,
|
|
63
|
+
f"{gw} доступен ({gw_ping:.0f} ms)"))
|
|
64
|
+
else:
|
|
65
|
+
results.append(CheckResult("Шлюз", False,
|
|
66
|
+
f"{gw} не отвечает",
|
|
67
|
+
"Проблема с роутером или кабелем. Перезагрузите роутер."))
|
|
68
|
+
|
|
69
|
+
# 4. DNS
|
|
70
|
+
try:
|
|
71
|
+
socket.gethostbyname("google.com")
|
|
72
|
+
results.append(CheckResult("DNS", True, "Разрешение имён работает"))
|
|
73
|
+
except socket.gaierror:
|
|
74
|
+
results.append(CheckResult("DNS", False,
|
|
75
|
+
"Не удаётся разрешить домены",
|
|
76
|
+
"Смените DNS на 1.1.1.1 (Cloudflare) или 8.8.8.8 (Google)"))
|
|
77
|
+
|
|
78
|
+
# 5. Пинг/Интернет
|
|
79
|
+
ping_stats = self.info.ping_series("8.8.8.8", count=3, timeout=1)
|
|
80
|
+
if not ping_stats:
|
|
81
|
+
results.append(CheckResult("Интернет", False,
|
|
82
|
+
"Нет доступа в интернет",
|
|
83
|
+
"Проверьте подключение к провайдеру."))
|
|
84
|
+
else:
|
|
85
|
+
if ping_stats["loss"] > 0:
|
|
86
|
+
results.append(CheckResult("Потери пакетов", False,
|
|
87
|
+
f"Потери: {ping_stats['loss']:.1f}%",
|
|
88
|
+
"Возможны помехи Wi-Fi или перегрузка канала."))
|
|
89
|
+
elif ping_stats["avg"] > 150:
|
|
90
|
+
results.append(CheckResult("Пинг", False,
|
|
91
|
+
f"Высокий пинг: {ping_stats['avg']:.0f} ms",
|
|
92
|
+
"Попробуйте запустить оптимизатор (TCP_NODELAY) или сменить DNS."))
|
|
93
|
+
else:
|
|
94
|
+
results.append(CheckResult("Интернет", True,
|
|
95
|
+
f"Стабильно ({ping_stats['avg']:.0f} ms)"))
|
|
96
|
+
|
|
97
|
+
return results
|
|
98
|
+
|
|
99
|
+
def get_report(self):
|
|
100
|
+
"""Возвращает готовый к печати отчёт (строкой)."""
|
|
101
|
+
results = self.run_checks()
|
|
102
|
+
fails = [r for r in results if not r.ok]
|
|
103
|
+
|
|
104
|
+
if ui.has_charm():
|
|
105
|
+
rows = []
|
|
106
|
+
for r in results:
|
|
107
|
+
status = "✔ OK" if r.ok else "✖ ОШИБКА"
|
|
108
|
+
rows.append([status, r.name, r.details or "", r.recommendation or ""])
|
|
109
|
+
|
|
110
|
+
out = [ui.table(["Статус", "Проверка", "Детали", "Рекомендация"], rows)]
|
|
111
|
+
out.append("")
|
|
112
|
+
if fails:
|
|
113
|
+
out.append(ui.render_warning(
|
|
114
|
+
"Обнаружены проблемы. Ознакомьтесь с рекомендациями выше."))
|
|
115
|
+
else:
|
|
116
|
+
out.append(ui.render_success("Сеть работает стабильно."))
|
|
117
|
+
return "\n".join(out)
|
|
118
|
+
|
|
119
|
+
# Plain-режим
|
|
120
|
+
out = ["=== Диагностика сети ==="]
|
|
121
|
+
for r in results:
|
|
122
|
+
status = "[+]" if r.ok else "[x]"
|
|
123
|
+
out.append(f"{status} {r.name}: {r.details}")
|
|
124
|
+
if not r.ok and r.recommendation:
|
|
125
|
+
out.append(f" -> Рекомендация: {r.recommendation}")
|
|
126
|
+
return "\n".join(out)
|
ndvpi/history.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""История метрик сети за сессию и экспорт в JSON/CSV."""
|
|
2
|
+
import json
|
|
3
|
+
import csv
|
|
4
|
+
import time
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
class SessionHistory:
|
|
8
|
+
"""Сборщик истории пингов и скоростей."""
|
|
9
|
+
|
|
10
|
+
def __init__(self):
|
|
11
|
+
self.records = [] # Список dict: {"ts": float, "type": str, "data": dict}
|
|
12
|
+
|
|
13
|
+
def add_ping(self, host, stats):
|
|
14
|
+
if stats:
|
|
15
|
+
self.records.append({
|
|
16
|
+
"ts": time.time(),
|
|
17
|
+
"type": "ping",
|
|
18
|
+
"host": host,
|
|
19
|
+
"data": stats
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
def add_speed(self, download_mbps, upload_mbps):
|
|
23
|
+
self.records.append({
|
|
24
|
+
"ts": time.time(),
|
|
25
|
+
"type": "speed",
|
|
26
|
+
"data": {"download": download_mbps, "upload": upload_mbps}
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
def clear(self):
|
|
30
|
+
self.records.clear()
|
|
31
|
+
|
|
32
|
+
def summary(self):
|
|
33
|
+
"""Возвращает усреднённые значения за сессию."""
|
|
34
|
+
pings = [r for r in self.records if r["type"] == "ping"]
|
|
35
|
+
speeds = [r for r in self.records if r["type"] == "speed"]
|
|
36
|
+
|
|
37
|
+
result = {"samples": len(self.records)}
|
|
38
|
+
if pings:
|
|
39
|
+
avg_ping = sum(p["data"]["avg"] for p in pings) / len(pings)
|
|
40
|
+
avg_loss = sum(p["data"]["loss"] for p in pings) / len(pings)
|
|
41
|
+
avg_jitter = sum(p["data"]["jitter"] for p in pings) / len(pings)
|
|
42
|
+
result.update({
|
|
43
|
+
"avg_ping_ms": round(avg_ping, 2),
|
|
44
|
+
"avg_loss_pct": round(avg_loss, 2),
|
|
45
|
+
"avg_jitter_ms": round(avg_jitter, 2)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
if speeds:
|
|
49
|
+
avg_down = sum(s["data"]["download"] for s in speeds) / len(speeds)
|
|
50
|
+
avg_up = sum(s["data"]["upload"] for s in speeds) / len(speeds)
|
|
51
|
+
result.update({
|
|
52
|
+
"avg_download_mbps": round(avg_down, 2),
|
|
53
|
+
"avg_upload_mbps": round(avg_up, 2)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
return result
|
|
57
|
+
|
|
58
|
+
def export_json(self, filepath):
|
|
59
|
+
path = Path(filepath)
|
|
60
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
61
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
62
|
+
json.dump(self.records, f, indent=2, ensure_ascii=False)
|
|
63
|
+
return str(path)
|
|
64
|
+
|
|
65
|
+
def export_csv(self, filepath):
|
|
66
|
+
path = Path(filepath)
|
|
67
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
68
|
+
|
|
69
|
+
with open(path, "w", encoding="utf-8", newline="") as f:
|
|
70
|
+
writer = csv.writer(f)
|
|
71
|
+
writer.writerow(["Timestamp", "Type", "Min/Avg/Download", "Max/Upload", "Loss/Jitter"])
|
|
72
|
+
|
|
73
|
+
for r in self.records:
|
|
74
|
+
if r["type"] == "ping":
|
|
75
|
+
d = r["data"]
|
|
76
|
+
writer.writerow([
|
|
77
|
+
time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(r["ts"])),
|
|
78
|
+
"Ping",
|
|
79
|
+
f"{d['avg']:.1f} ms",
|
|
80
|
+
f"{d['max']:.1f} ms",
|
|
81
|
+
f"{d['loss']:.1f}% / {d['jitter']:.1f} ms"
|
|
82
|
+
])
|
|
83
|
+
elif r["type"] == "speed":
|
|
84
|
+
d = r["data"]
|
|
85
|
+
writer.writerow([
|
|
86
|
+
time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(r["ts"])),
|
|
87
|
+
"Speed",
|
|
88
|
+
f"{d['download']:.1f} Mbps",
|
|
89
|
+
f"{d['upload']:.1f} Mbps",
|
|
90
|
+
"-"
|
|
91
|
+
])
|
|
92
|
+
return str(path)
|