woninet 2.0.1__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.
Files changed (53) hide show
  1. woninet/__init__.py +2 -0
  2. woninet/__main__.py +4 -0
  3. woninet/core/__init__.py +2 -0
  4. woninet/core/alerts.py +251 -0
  5. woninet/core/collectors.py +303 -0
  6. woninet/core/engine.py +353 -0
  7. woninet/core/graph.py +63 -0
  8. woninet/core/manual_ip_enumerator.py +33 -0
  9. woninet/core/models.py +124 -0
  10. woninet/core/storage.py +163 -0
  11. woninet/core/subnet_enumerator.py +32 -0
  12. woninet/database/__init__.py +2 -0
  13. woninet/database/base.py +9 -0
  14. woninet/database/engine.py +44 -0
  15. woninet/database/repositories/__init__.py +2 -0
  16. woninet/database/repositories/alert_event_repository.py +69 -0
  17. woninet/database/repositories/alert_state_repository.py +81 -0
  18. woninet/database/repositories/device_repository.py +105 -0
  19. woninet/database/repositories/metric_repository.py +75 -0
  20. woninet/database/tables.py +89 -0
  21. woninet/logging.yaml +49 -0
  22. woninet/main.py +225 -0
  23. woninet/server/__init__.py +2 -0
  24. woninet/server/app.py +101 -0
  25. woninet/server/dependencies.py +25 -0
  26. woninet/server/routes/__init__.py +2 -0
  27. woninet/server/routes/api/__init__.py +0 -0
  28. woninet/server/routes/api/devices.py +43 -0
  29. woninet/server/routes/api/stats.py +34 -0
  30. woninet/server/routes/page/__init__.py +0 -0
  31. woninet/server/routes/page/devices.py +28 -0
  32. woninet/server/static/css/404.css +164 -0
  33. woninet/server/static/css/style.css +368 -0
  34. woninet/server/static/js/app.js +242 -0
  35. woninet/server/static/js/device_info.js +95 -0
  36. woninet/server/static/swagger-ui/favicon-32x32.png +0 -0
  37. woninet/server/static/swagger-ui/swagger-ui-bundle.js +2 -0
  38. woninet/server/static/swagger-ui/swagger-ui.css +3 -0
  39. woninet/server/templates/404.html +76 -0
  40. woninet/server/templates/dashboard.html +118 -0
  41. woninet/server/templates/device_info.html +93 -0
  42. woninet/utilities/__init__.py +2 -0
  43. woninet/utilities/arguments.py +94 -0
  44. woninet/utilities/cli_parser.py +90 -0
  45. woninet/utilities/detect_ip_range.py +21 -0
  46. woninet/utilities/ip_validator.py +39 -0
  47. woninet/utilities/logger.py +97 -0
  48. woninet-2.0.1.dist-info/METADATA +243 -0
  49. woninet-2.0.1.dist-info/RECORD +53 -0
  50. woninet-2.0.1.dist-info/WHEEL +5 -0
  51. woninet-2.0.1.dist-info/entry_points.txt +2 -0
  52. woninet-2.0.1.dist-info/licenses/LICENSE +21 -0
  53. woninet-2.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,93 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Device Info</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css">
9
+ <link rel="stylesheet" href="/static/css/style.css">
10
+ </head>
11
+ <body>
12
+ <div class="dashboard">
13
+ <header class="topbar">
14
+ <div>
15
+ <p class="eyebrow">Device Details</p>
16
+ <h1 id="device-ip"></h1>
17
+ <p class="subtitle">Live monitoring and alert history</p>
18
+ </div>
19
+ </header>
20
+
21
+ <section class="overview-grid">
22
+ <div class="kpi-card">
23
+ <div class="kpi-icon primary">
24
+ <i class="fa-solid fa-network-wired"></i>
25
+ <span>MAC Address</span>
26
+ </div>
27
+ <h2 style="font-size: 1rem;" id="device-mac"></h2>
28
+ </div>
29
+ <div class="kpi-card">
30
+ <div class="kpi-icon primary">
31
+ <i class="fa-solid fa-wave-square"></i>
32
+ <span>Latency</span>
33
+ </div>
34
+ <h2 id="device-latency"></h2>
35
+ </div>
36
+ <div class="kpi-card">
37
+ <div class="kpi-icon warning">
38
+ <i class="fa-solid fa-triangle-exclamation"></i>
39
+ <span>Packet Loss</span>
40
+ </div>
41
+ <h2 id="device-packet-loss"></h2>
42
+ </div>
43
+ </section>
44
+
45
+ <main class="main-layout">
46
+ <section class="panel">
47
+ <div class="panel-header">
48
+ <h2>Device Details</h2>
49
+ </div>
50
+ <div class="device-grid">
51
+ <article class="device-card" id="device-card">
52
+ <div class="device-header">
53
+ <div class="device-status" id="device-status"></div>
54
+ </div>
55
+
56
+ <div class="metrics">
57
+ <div class="metric">
58
+ <div class="metric-label">Last Seen</div>
59
+ <div class="metric-value" id="device-last-seen"></div>
60
+ </div>
61
+ <div class="metric">
62
+ <div class="metric-label">Latency State</div>
63
+ <div class="metric-value" id="alert-state-latency"></div>
64
+ </div>
65
+ <div class="metric">
66
+ <div class="metric-label">Packet Loss State</div>
67
+ <div class="metric-value" id="alert-state-packet-loss">}</div>
68
+ </div>
69
+ </div>
70
+ </article>
71
+ </div>
72
+ </section>
73
+
74
+ <aside class="events-panel">
75
+ <h3>Alert Events</h3>
76
+ <div id="events"></div>
77
+ </aside>
78
+ </main>
79
+
80
+ <br />
81
+ <section class="panel">
82
+ <h2>Recent Device Events Graph</h2>
83
+ <img width="100%" src="placeholder" alt="Device recent events line graph" id="device-recent-events-line-graph"/>
84
+ </section>
85
+
86
+ <div style="margin-top: 20px;">
87
+ <a href="/" class="back-link">Back to Dashboard</a>
88
+ </div>
89
+ </div>
90
+
91
+ <script src="/static/js/device_info.js"></script>
92
+ </body>
93
+ </html>
@@ -0,0 +1,2 @@
1
+ # woninet/core/__init__.py
2
+ # (This file intentionally left blank)
@@ -0,0 +1,94 @@
1
+ import argparse
2
+ from woninet.utilities.cli_parser import list_devices, show_device_info, report_stats
3
+
4
+
5
+ def args() -> argparse.Namespace:
6
+ """
7
+ The argument handler.
8
+
9
+ check `woninet -h` for more info.
10
+ """
11
+ description = """
12
+ A local network monitoring system. It enumerates all candidate IP addresses in your /24 subnet
13
+ and performs ARP + ICMP host detection to determine existence, reachability, and latency. Metrics
14
+ are collected continuously and processed through an alert engine."""
15
+
16
+ epilog = ""
17
+
18
+ parser = argparse.ArgumentParser(
19
+ description=description,
20
+ formatter_class=argparse.RawDescriptionHelpFormatter,
21
+ prog="woninet",
22
+ epilog=epilog,
23
+ )
24
+
25
+ subparsers = parser.add_subparsers(dest="command")
26
+
27
+ device_parser = subparsers.add_parser(
28
+ "device", help="Fetch Device informations from the database."
29
+ )
30
+ device_subparsers = device_parser.add_subparsers(dest="action", required=True)
31
+
32
+ device_list = device_subparsers.add_parser(
33
+ "list", description="List all devices.", help="List all devices."
34
+ )
35
+ device_list.set_defaults(func=list_devices)
36
+
37
+ device_show = device_subparsers.add_parser(
38
+ "show", description="Show device info for a given IP.", help="Show device info."
39
+ )
40
+ device_show.add_argument("ip", help="Device IP address")
41
+ device_show.set_defaults(func=show_device_info)
42
+
43
+ stats_parser = subparsers.add_parser(
44
+ "stats", help="Output a JSON report for the woninet statistics."
45
+ )
46
+
47
+ stats_parser.add_argument(
48
+ "--output",
49
+ default="stats.json",
50
+ help="Output path for the JSON report for the woninet statistics (default: %(default)s).",
51
+ )
52
+ stats_parser.set_defaults(func=report_stats)
53
+
54
+ parser.add_argument(
55
+ "--cli",
56
+ action="store_true",
57
+ help="Run woninet in CLI mode.",
58
+ )
59
+
60
+ parser.add_argument(
61
+ "-p",
62
+ "--port",
63
+ type=int,
64
+ default=8080,
65
+ help="Port number to serve the web dashboard (defaut: %(default)s).",
66
+ )
67
+
68
+ parser.add_argument(
69
+ "--logs",
70
+ type=str,
71
+ help="Output path for storing logs.",
72
+ )
73
+
74
+ parser.add_argument(
75
+ "--color",
76
+ type=str,
77
+ choices=["true", "false"],
78
+ default="true",
79
+ help="Whether to enable colored console log output (default: %(default)s)",
80
+ )
81
+
82
+ parser.add_argument(
83
+ "-v",
84
+ "--verbose",
85
+ action="count",
86
+ default=0,
87
+ help="Increase verbosity (repeat for more detail)",
88
+ )
89
+
90
+ parser.add_argument(
91
+ "-V", "--version", action="store_true", help="Prints the version and quit."
92
+ )
93
+
94
+ return parser.parse_args()
@@ -0,0 +1,90 @@
1
+ import json
2
+ from typing import Any
3
+ from woninet.main import NetworkMonitorCore
4
+
5
+
6
+ def list_devices(args):
7
+ """
8
+ Prints list of recorded devices information from the database to stdout.
9
+ Following entries are shown:
10
+ 1. Device IP address.
11
+ 2. Device MAC address.
12
+ 3. Device last seen timestamp.
13
+ """
14
+ monitor: NetworkMonitorCore = args.monitor
15
+ devices = monitor.get_device_history()
16
+ if len(devices) == 0:
17
+ print("No recorded device was found.")
18
+ else:
19
+ for device in devices:
20
+ row = f"Device {device.ip}: MAC={device.mac}, Last seen={device.last_seen}"
21
+ print(row)
22
+
23
+
24
+ def show_device_info(args):
25
+ """
26
+ Prints information for a given IP address. Following entries are shown:
27
+ 1. Device IP address.
28
+ 2. Device MAC address.
29
+ 3. Device last captured latency.
30
+ 4. Device last seen timestamp.
31
+ 5. Last alert state of device.
32
+ 6. Last 10 alert events of device.
33
+ """
34
+ monitor: NetworkMonitorCore = args.monitor
35
+ device, devices_state, recent_device_alert_events = monitor.get_device_info(args.ip)
36
+ if device is None:
37
+ print(f"Device {args.ip} was not found.")
38
+ else:
39
+ latency = device.latency
40
+ if latency == 0:
41
+ latency = "OFFLINE"
42
+ print(
43
+ f"Found device {device.ip}: MAC={device.mac}, Last latency: {latency}, Last seen={device.last_seen}"
44
+ )
45
+ for metric, state in devices_state.items():
46
+ print(f"Current alert state for {metric}: state={state}")
47
+ print("Recent alert events:")
48
+ for event in recent_device_alert_events:
49
+ content = f"ID: {event.id} - Metric: {event.metric},\t Value={event.value},"
50
+ content += (
51
+ f"\t Event type={event.event_type},\t Timestamp={event.timestamp}"
52
+ )
53
+ print(content)
54
+
55
+
56
+ def report_stats(args) -> None:
57
+ """
58
+ Output statistics report into given file.
59
+ following statistics are provided:
60
+ 1. devices count.
61
+ 2. devices list.
62
+ 3. recent alert events.
63
+ """
64
+ try:
65
+ monitor: NetworkMonitorCore = args.monitor
66
+ devices_count = monitor.count_resources()[0]
67
+ devices = monitor.get_device_history()
68
+ output: str = args.output
69
+ result: dict[str, Any] = {}
70
+
71
+ result["devices_count"] = devices_count
72
+ result["devices"] = [
73
+ {
74
+ "ip": dev.ip,
75
+ "mac": dev.mac,
76
+ "last_seen": dev.last_seen.isoformat(),
77
+ "alert_state": monitor.storage.get_device_alert_state(ip=dev.ip),
78
+ }
79
+ for dev in devices
80
+ ]
81
+
82
+ result["recent_alert_events"] = monitor.classify_recent_alert_events()
83
+
84
+ with open(output, "w") as stats:
85
+ json.dump(result, stats, indent=2)
86
+
87
+ print(f"Statistics report saved to {output}")
88
+
89
+ except Exception as e:
90
+ print(e)
@@ -0,0 +1,21 @@
1
+ def detect_ip_range(ip: str) -> bool:
2
+ """
3
+ Detect if an IP address is provided as a range.
4
+ (e.g., `192.168.10.10-20`)
5
+
6
+ Args:
7
+ ip (str): IP address to detect.
8
+
9
+ Returns:
10
+ bool: Boolean indicating if IP address is provided as a range.
11
+
12
+ Raises:
13
+ ValueError: If the range notation is used in any of the first three octets.\n
14
+ (e.g., `192.168.1-10.10` - `192.168-188.1.10` - `192.180.168.1.10`)
15
+ """
16
+ base, last_octet = ip.rsplit(".", maxsplit=1)
17
+ if "-" in base:
18
+ raise ValueError("Ip addess range is only allowed in the last octet.")
19
+ if "-" in last_octet:
20
+ return True
21
+ return False
@@ -0,0 +1,39 @@
1
+ from ipaddress import ip_address
2
+ from woninet.core.models import Device
3
+
4
+
5
+ def is_ip_list_valid(ip_list: list[str]) -> bool:
6
+ """
7
+ Validate a list of IP addresses.
8
+
9
+ Args:
10
+ ip_list (list[str]): IP addresses to validate.
11
+
12
+ Returns:
13
+ bool: Boolean indicating if IP addresses are valid.
14
+ """
15
+ for ip in ip_list:
16
+ try:
17
+ ip_address(ip)
18
+ except ValueError:
19
+ return False
20
+ return True
21
+
22
+
23
+ def is_device_ip_valid(devices: dict[str, Device]) -> bool:
24
+ """
25
+ Validate the dictionary of candidate devices.
26
+
27
+ Args:
28
+ devices (dict[str,Device]): Dictrionary of IP addresses and Device model
29
+ instances to validate.
30
+
31
+ Returns:
32
+ bool: Boolean indicating if IP addresses are valid.
33
+ """
34
+ for ip, dev in devices.items():
35
+ try:
36
+ ip_address(ip)
37
+ except ValueError:
38
+ return False
39
+ return True
@@ -0,0 +1,97 @@
1
+ import logging
2
+ from sys import stdout
3
+
4
+ # Trace level definition
5
+ TRACE_LEVEL = 5
6
+ logging.addLevelName(TRACE_LEVEL, "TRACE")
7
+
8
+
9
+ def trace(self, msg, *args, **kwargs) -> None:
10
+ if self.isEnabledFor(TRACE_LEVEL):
11
+ self._log(TRACE_LEVEL, msg, args, **kwargs)
12
+
13
+
14
+ logging.Logger.trace = trace
15
+
16
+
17
+ class ColorFormatter(logging.Formatter):
18
+ """
19
+ Applies ANSI color codes to log level names.
20
+ Colors are applied only when the output stream is an
21
+ interactive terminal.
22
+ """
23
+
24
+ COLORS = {
25
+ TRACE_LEVEL: "\033[34m",
26
+ logging.DEBUG: "\033[36m",
27
+ logging.INFO: "\033[32m",
28
+ logging.WARNING: "\033[33m",
29
+ logging.ERROR: "\033[31m",
30
+ logging.CRITICAL: "\033[1;31m",
31
+ }
32
+ RESET = "\033[0m"
33
+
34
+ def formatMessage(self, record) -> str:
35
+ # Only colorize output if writing to an interactive terminal
36
+ if stdout.isatty():
37
+ color = self.COLORS.get(record.levelno, "")
38
+
39
+ # Apply ANSI color escape codes to the level name
40
+ level_name = record.levelname
41
+ colored = f"{color}{level_name}{self.RESET}"
42
+
43
+ # Clone the LogRecord to avoid mutating the original record object
44
+ new_record = logging.makeLogRecord(record.__dict__.copy())
45
+ new_record.levelname = colored
46
+
47
+ return super().formatMessage(new_record)
48
+
49
+ # Fallback - No colors for redirected outputs
50
+ return super().formatMessage(record)
51
+
52
+
53
+ def get_core_logger(
54
+ log_output: str | None = None, use_color: bool = True
55
+ ) -> logging.Logger:
56
+ """
57
+ Create and modify the core logger.
58
+
59
+ Args:
60
+ log_output (str|None): Output path for storing logs; `None` if not provided.
61
+ use_color (bool): Whether to enable colored console log output.
62
+
63
+ Returns:
64
+ Logger: The core logger.
65
+ """
66
+ console_format = "%(asctime)s [%(levelname)s] %(message)s"
67
+ file_format = "%(asctime)s [%(levelname)s] %(message)s"
68
+ date_format = "%Y-%m-%d %H:%M:%S"
69
+
70
+ core_logger = logging.getLogger("core")
71
+
72
+ # Prevent duplicated logging
73
+ if not core_logger.handlers:
74
+ console_handler = logging.StreamHandler()
75
+ if use_color:
76
+ console_handler.setFormatter(
77
+ ColorFormatter(fmt=console_format, datefmt=date_format)
78
+ )
79
+ else:
80
+ console_handler.setFormatter(
81
+ logging.Formatter(file_format, datefmt=date_format)
82
+ )
83
+ console_handler.setLevel(logging.NOTSET)
84
+
85
+ if log_output:
86
+ file_handler = logging.FileHandler(log_output)
87
+ file_handler.setFormatter(
88
+ logging.Formatter(file_format, datefmt=date_format)
89
+ )
90
+ file_handler.setLevel(logging.NOTSET)
91
+ core_logger.addHandler(file_handler)
92
+
93
+ core_logger.addHandler(console_handler)
94
+
95
+ core_logger.setLevel(logging.INFO)
96
+
97
+ return core_logger
@@ -0,0 +1,243 @@
1
+ Metadata-Version: 2.4
2
+ Name: woninet
3
+ Version: 2.0.1
4
+ Summary: A Python-based ARP+ICMP network monitoring tool
5
+ Author-email: Alireza Teymuri <teymuri.alireza98@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/teymuri-alireza/woninet
8
+ Project-URL: Documentation, https://github.com/teymuri-alireza/woninet/tree/main/docs
9
+ Project-URL: Repository, https://github.com/teymuri-alireza/woninet
10
+ Project-URL: Issues, https://github.com/teymuri-alireza/woninet/issues
11
+ Project-URL: Changelog, https://github.com/teymuri-alireza/woninet/blob/main/CHANGELOG.md
12
+ Keywords: network,network-monitoring,icmp,arp
13
+ Requires-Python: >=3.12
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Requires-Dist: icmplib<3.1,>=3.0.4
17
+ Requires-Dist: fastapi<0.137,>=0.136.0
18
+ Requires-Dist: Jinja2<3.2,>=3.1.6
19
+ Requires-Dist: uvicorn<0.45,>=0.44.0
20
+ Requires-Dist: PyYAML<6.1,>=6.0.3
21
+ Requires-Dist: SQLAlchemy<2.1,>=2.0.46
22
+ Requires-Dist: matplotlib<3.12,>=3.11.0
23
+ Provides-Extra: test
24
+ Requires-Dist: pytest<9.2,>=9.1.1; extra == "test"
25
+ Requires-Dist: httpx<0.29,>=0.28.1; extra == "test"
26
+ Dynamic: license-file
27
+
28
+ # woninet
29
+
30
+ A local network monitoring system.
31
+
32
+ **woninet** continuously monitors devices on a local network using ARP
33
+ and ICMP probes. It enumerates candidate IP addresses in a /24 subnet,
34
+ determines device availability, measures latency and packet loss, stores
35
+ metrics in SQLite, and evaluates configurable alert rules in real time.
36
+
37
+ **Note: This script requires sudo privileges.**
38
+
39
+ ## Features
40
+
41
+ - **ARP + ICMP Host Detection** (Distinguishes real devices from nonexistent
42
+ IPs and filters out Wi‑Fi ARP‑delay noise.)
43
+
44
+ - **Alert Engine** (Evaluates real‑time alert rules (e.g., latency thresholds))
45
+
46
+ - **Threaded Collectors**
47
+
48
+ - **SQLite database** (for devices, recorded metrics, alert states, and alert events.)
49
+
50
+ - **Positional Argument for easier access to the database from the CLI**
51
+
52
+ - **Detailed Logging**
53
+
54
+ ## Future Plans
55
+
56
+ - Add ARP table caching to further reduce Wi‑Fi latency noise.
57
+ - Accept MAC addresses as command‑line arguments.
58
+ - Fetch MAC address from database for offline devices.
59
+ - Use ARP noise limit as device state for styling in front-end.
60
+ - Add CLI argument to accept target IP addresses from a file.
61
+
62
+ ## Known Issues
63
+
64
+ - Monitoring Scope Limited to /24 Subnet
65
+
66
+ ## How To Run
67
+
68
+ **Note:** *woninet* is under active development.
69
+
70
+ ## Installation
71
+
72
+ Install from PyPI:
73
+
74
+ ```shell
75
+ pip install woninet
76
+ ```
77
+
78
+ Then run:
79
+
80
+ ```shell
81
+ woninet --help
82
+ ```
83
+
84
+ ## Installation from Source
85
+
86
+ ### 1. Clone the repository
87
+
88
+ ```shell
89
+ git clone https://github.com/teymuri-alireza/woninet
90
+ cd woninet
91
+ ```
92
+
93
+ ### 2. Create and activate a virtual environment:
94
+
95
+ **Note:** Create the virtual environment outside the project directory to avoid
96
+ `Multiple top-level packages discovered in a flat-layout` errors.
97
+
98
+ **Unix/Linux note:** The `--copies` flag forces Python to copy the interpreter
99
+ instead of using a symbolic link. This is required because you will apply Linux
100
+ capabilities directly to the Python binary in the virtual environment.
101
+
102
+ #### Linux/macOS
103
+
104
+ ```shell
105
+ python3 -m venv --copies ../venv
106
+ source ../venv/bin/activate
107
+ ```
108
+
109
+ #### Windows (PowerShell)
110
+
111
+ ```shell
112
+ py -m venv ..\venv
113
+ ..\venv\Scripts\Activate.ps1
114
+ ```
115
+
116
+ ### 3. Install woninet locally:
117
+
118
+ ```shell
119
+ pip3 install .
120
+ ```
121
+
122
+ ### 4. Run woninet
123
+
124
+ **Unix/Linux only:** To allow raw ICMP without running as root, grant the required
125
+ capability to the Python interpreter inside the virtual environment:
126
+
127
+ ```shell
128
+ sudo setcap cap_net_raw=eip ../venv/bin/python3
129
+ ```
130
+
131
+ Finally, run woninet normally:
132
+
133
+ ```shell
134
+ woninet --version
135
+ ```
136
+
137
+ **Alternative (not recommended):** You can run woninet with sudo instead of using
138
+ setcap, but this is discouraged for security reasons:
139
+
140
+ ```shell
141
+ sudo ../venv/bin/python3 -m woninet
142
+ ```
143
+
144
+ ## Configuration
145
+
146
+ You can define alert rules in a JSON file. Rename the example file at
147
+ `woninet/config.example.json` to `config.json`. The application will load the alert
148
+ rules from that file at startup.
149
+
150
+ ### Example:
151
+
152
+ ```json
153
+ {
154
+ "monitoring": {
155
+ "arp_noise_limit": 300.0,
156
+ "max_workers": 4
157
+ },
158
+ "database": "woninet.db",
159
+ "target_ip_list": [
160
+ "192.168.1.1-100"
161
+ // or
162
+ "192.168.1.1", "192.168.1.10", "192.168.1.20"
163
+ ],
164
+ "alert_rules":[
165
+ {
166
+ "metric": "latency",
167
+ "threshold": 100,
168
+ "consecutive_checks": 3
169
+ },
170
+ {
171
+ "metric": "packet_loss",
172
+ "threshold": 0.0,
173
+ "consecutive_checks": 1
174
+ }
175
+ ]
176
+ }
177
+ ```
178
+
179
+ ### Explanations:
180
+
181
+ #### Monitoring Settings
182
+
183
+ - **arp_noise_limit**: Latency threshold in milliseconds used to filter ARP
184
+ resolution noise. Set to 0 to disable filtering. Helps eliminate Wi‑Fi latency spikes.
185
+ - **max_workers**: Maximum number of thread workers used to send ICMP pings.
186
+ Higher values may increase system load and latency.
187
+
188
+ #### Database
189
+
190
+ - **database**: The path to the SQLite database file where metrics and alerts are stored.
191
+
192
+ #### Target IP List
193
+
194
+ - A single or range of target IP addresses to scan.
195
+
196
+ #### Alert Rules
197
+
198
+ - **metric**: The metric name to evaluate (`latency` or `packet_loss`)
199
+ - **threshold**: The value that triggers the alert. For latency, this is in milliseconds.
200
+ For packet loss, this is a percentage (0.0-100.0).
201
+ - **consecutive_checks**: The number of consecutive evaluations required before the alert
202
+ state changes. Higher values reduce false positives.
203
+
204
+ #### Notes
205
+
206
+ - **Packet Loss**: Values are in the range [0.0, 100.0]. A value of 100.0 indicates 100%
207
+ packet loss. A value of 0.0 will trigger on any packet loss.
208
+ - **Latency**: All latency thresholds are evaluated in milliseconds.
209
+
210
+ ## Usage
211
+
212
+ This [guide](./docs/usage.md) explains common ways to run *woninet* and how to resolve common issues.
213
+
214
+ ## Testing
215
+
216
+ If you want to run the test suite locally, install the optional test dependencies:
217
+
218
+ ```shell
219
+ pip install -e ".[test]"
220
+ ```
221
+
222
+ Then run:
223
+
224
+ ```shell
225
+ pytest
226
+ ```
227
+
228
+ The test suite currently focuses on API behavior and core logic, and it uses pytest for automated verification.
229
+
230
+ ## Requirements
231
+
232
+ - Python >= 3.12
233
+ - Root privileges
234
+
235
+ ## Documentation
236
+
237
+ if you have any question or need clarification, check the [documentation](./docs/documentation.md) guide.
238
+
239
+ ## Contribution
240
+
241
+ Thanks for considering contributing to *woninet!*
242
+ To help us maintain code quality, stability, and a predictable release process, please review
243
+ and follow the guidelines in our [Contributing Guide.](./CONTRIBUTING.md)