woninet 2.0.2__tar.gz → 2.1.0__tar.gz

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 (68) hide show
  1. {woninet-2.0.2 → woninet-2.1.0}/PKG-INFO +37 -7
  2. {woninet-2.0.2 → woninet-2.1.0}/README.md +35 -6
  3. {woninet-2.0.2 → woninet-2.1.0}/pyproject.toml +2 -1
  4. {woninet-2.0.2 → woninet-2.1.0}/tests/test_alert_events.py +74 -0
  5. woninet-2.1.0/tests/test_api.py +37 -0
  6. woninet-2.1.0/tests/test_system_ping.py +121 -0
  7. {woninet-2.0.2 → woninet-2.1.0}/woninet/__init__.py +1 -1
  8. {woninet-2.0.2 → woninet-2.1.0}/woninet/core/alerts.py +103 -18
  9. {woninet-2.0.2 → woninet-2.1.0}/woninet/core/collectors.py +56 -18
  10. {woninet-2.0.2 → woninet-2.1.0}/woninet/core/engine.py +48 -7
  11. {woninet-2.0.2 → woninet-2.1.0}/woninet/core/graph.py +1 -0
  12. {woninet-2.0.2 → woninet-2.1.0}/woninet/core/manual_ip_enumerator.py +3 -3
  13. {woninet-2.0.2 → woninet-2.1.0}/woninet/core/models.py +25 -11
  14. {woninet-2.0.2 → woninet-2.1.0}/woninet/core/subnet_enumerator.py +1 -1
  15. {woninet-2.0.2 → woninet-2.1.0}/woninet/database/engine.py +1 -1
  16. {woninet-2.0.2 → woninet-2.1.0}/woninet/database/repositories/alert_event_repository.py +1 -1
  17. {woninet-2.0.2 → woninet-2.1.0}/woninet/database/repositories/alert_state_repository.py +3 -3
  18. {woninet-2.0.2 → woninet-2.1.0}/woninet/database/repositories/device_repository.py +13 -4
  19. {woninet-2.0.2 → woninet-2.1.0}/woninet/database/repositories/metric_repository.py +1 -1
  20. {woninet-2.0.2 → woninet-2.1.0}/woninet/database/tables.py +5 -4
  21. woninet-2.1.0/woninet/exc.py +2 -0
  22. {woninet-2.0.2 → woninet-2.1.0}/woninet/main.py +1 -1
  23. {woninet-2.0.2 → woninet-2.1.0}/woninet/server/app.py +1 -4
  24. {woninet-2.0.2 → woninet-2.1.0}/woninet/server/dependencies.py +1 -1
  25. {woninet-2.0.2 → woninet-2.1.0}/woninet/server/routes/api/devices.py +10 -2
  26. {woninet-2.0.2 → woninet-2.1.0}/woninet/server/static/css/style.css +17 -0
  27. {woninet-2.0.2 → woninet-2.1.0}/woninet/server/static/js/app.js +11 -1
  28. {woninet-2.0.2 → woninet-2.1.0}/woninet/server/templates/dashboard.html +6 -1
  29. {woninet-2.0.2 → woninet-2.1.0}/woninet/utilities/arguments.py +2 -2
  30. {woninet-2.0.2 → woninet-2.1.0}/woninet/utilities/create_configuration.py +8 -1
  31. woninet-2.1.0/woninet/utilities/detect_gateway.py +66 -0
  32. {woninet-2.0.2 → woninet-2.1.0}/woninet/utilities/detect_ip_range.py +1 -1
  33. {woninet-2.0.2 → woninet-2.1.0}/woninet/utilities/ip_validator.py +1 -1
  34. woninet-2.1.0/woninet/utilities/ping.py +103 -0
  35. {woninet-2.0.2 → woninet-2.1.0}/woninet.egg-info/PKG-INFO +37 -7
  36. {woninet-2.0.2 → woninet-2.1.0}/woninet.egg-info/SOURCES.txt +5 -1
  37. {woninet-2.0.2 → woninet-2.1.0}/woninet.egg-info/requires.txt +1 -0
  38. woninet-2.0.2/tests/test_api.py +0 -10
  39. {woninet-2.0.2 → woninet-2.1.0}/LICENSE +0 -0
  40. {woninet-2.0.2 → woninet-2.1.0}/setup.cfg +0 -0
  41. {woninet-2.0.2 → woninet-2.1.0}/tests/test_ip_validation.py +0 -0
  42. {woninet-2.0.2 → woninet-2.1.0}/tests/test_storage.py +0 -0
  43. {woninet-2.0.2 → woninet-2.1.0}/woninet/__main__.py +0 -0
  44. {woninet-2.0.2 → woninet-2.1.0}/woninet/core/__init__.py +0 -0
  45. {woninet-2.0.2 → woninet-2.1.0}/woninet/core/storage.py +0 -0
  46. {woninet-2.0.2 → woninet-2.1.0}/woninet/database/__init__.py +0 -0
  47. {woninet-2.0.2 → woninet-2.1.0}/woninet/database/base.py +0 -0
  48. {woninet-2.0.2 → woninet-2.1.0}/woninet/database/repositories/__init__.py +0 -0
  49. {woninet-2.0.2 → woninet-2.1.0}/woninet/logging.yaml +0 -0
  50. {woninet-2.0.2 → woninet-2.1.0}/woninet/server/__init__.py +0 -0
  51. {woninet-2.0.2 → woninet-2.1.0}/woninet/server/routes/__init__.py +0 -0
  52. {woninet-2.0.2 → woninet-2.1.0}/woninet/server/routes/api/__init__.py +0 -0
  53. {woninet-2.0.2 → woninet-2.1.0}/woninet/server/routes/api/stats.py +0 -0
  54. {woninet-2.0.2 → woninet-2.1.0}/woninet/server/routes/page/__init__.py +0 -0
  55. {woninet-2.0.2 → woninet-2.1.0}/woninet/server/routes/page/devices.py +0 -0
  56. {woninet-2.0.2 → woninet-2.1.0}/woninet/server/static/css/404.css +0 -0
  57. {woninet-2.0.2 → woninet-2.1.0}/woninet/server/static/js/device_info.js +0 -0
  58. {woninet-2.0.2 → woninet-2.1.0}/woninet/server/static/swagger-ui/favicon-32x32.png +0 -0
  59. {woninet-2.0.2 → woninet-2.1.0}/woninet/server/static/swagger-ui/swagger-ui-bundle.js +0 -0
  60. {woninet-2.0.2 → woninet-2.1.0}/woninet/server/static/swagger-ui/swagger-ui.css +0 -0
  61. {woninet-2.0.2 → woninet-2.1.0}/woninet/server/templates/404.html +0 -0
  62. {woninet-2.0.2 → woninet-2.1.0}/woninet/server/templates/device_info.html +0 -0
  63. {woninet-2.0.2 → woninet-2.1.0}/woninet/utilities/__init__.py +0 -0
  64. {woninet-2.0.2 → woninet-2.1.0}/woninet/utilities/cli_parser.py +0 -0
  65. {woninet-2.0.2 → woninet-2.1.0}/woninet/utilities/logger.py +0 -0
  66. {woninet-2.0.2 → woninet-2.1.0}/woninet.egg-info/dependency_links.txt +0 -0
  67. {woninet-2.0.2 → woninet-2.1.0}/woninet.egg-info/entry_points.txt +0 -0
  68. {woninet-2.0.2 → woninet-2.1.0}/woninet.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: woninet
3
- Version: 2.0.2
3
+ Version: 2.1.0
4
4
  Summary: A Python-based ARP+ICMP network monitoring tool
5
5
  Author-email: Alireza Teymuri <teymuri.alireza98@gmail.com>
6
6
  License-Expression: MIT
@@ -23,6 +23,7 @@ Requires-Dist: matplotlib<3.12,>=3.11.0
23
23
  Provides-Extra: test
24
24
  Requires-Dist: pytest<9.2,>=9.1.1; extra == "test"
25
25
  Requires-Dist: httpx<0.29,>=0.28.1; extra == "test"
26
+ Requires-Dist: ruff<0.16,>=0.15.11; extra == "test"
26
27
  Dynamic: license-file
27
28
 
28
29
  # woninet
@@ -71,6 +72,8 @@ IPs and filters out Wi‑Fi ARP‑delay noise.)
71
72
 
72
73
  ### Unix/Linux
73
74
 
75
+ 1. **Method 1 (recommended (Uses the `icmplib` module)):**
76
+
74
77
  Create and activate a virtual environment:
75
78
 
76
79
  ```shell
@@ -96,7 +99,28 @@ Then run:
96
99
  woninet --help
97
100
  ```
98
101
 
99
- #### macOS/Windows
102
+ 2. **Method 2 (Uses the system's `ping` command as a fallback):**
103
+
104
+ Create and activate a virtual environment:
105
+
106
+ ```shell
107
+ python3 -m venv venv
108
+ source venv/bin/activate
109
+ ```
110
+
111
+ Install from PyPI:
112
+
113
+ ```shell
114
+ pip install woninet
115
+ ```
116
+
117
+ Then run:
118
+
119
+ ```shell
120
+ woninet --help
121
+ ```
122
+
123
+ ### macOS/Windows
100
124
 
101
125
  Install from PyPI:
102
126
 
@@ -173,7 +197,7 @@ sudo ../venv/bin/python3 -m woninet
173
197
  ## Configuration
174
198
 
175
199
  You can define alert rules in a JSON file. Rename the example file at
176
- `woninet/config.example.json` to `config.json`. The application will load the alert
200
+ `config.example.json` to `config.json`. The application will load the alert
177
201
  rules from that file at startup.
178
202
 
179
203
  ### Example:
@@ -200,6 +224,11 @@ rules from that file at startup.
200
224
  "metric": "packet_loss",
201
225
  "threshold": 0.0,
202
226
  "consecutive_checks": 1
227
+ },
228
+ {
229
+ "metric": "jitter",
230
+ "threshold": 40.0,
231
+ "consecutive_checks": 3
203
232
  }
204
233
  ]
205
234
  }
@@ -224,7 +253,7 @@ Higher values may increase system load and latency.
224
253
 
225
254
  #### Alert Rules
226
255
 
227
- - **metric**: The metric name to evaluate (`latency` or `packet_loss`)
256
+ - **metric**: The metric name to evaluate (`latency`, `packet_loss` or `jitter`)
228
257
  - **threshold**: The value that triggers the alert. For latency, this is in milliseconds.
229
258
  For packet loss, this is a percentage (0.0-100.0).
230
259
  - **consecutive_checks**: The number of consecutive evaluations required before the alert
@@ -235,10 +264,11 @@ state changes. Higher values reduce false positives.
235
264
  - **Packet Loss**: Values are in the range [0.0, 100.0]. A value of 100.0 indicates 100%
236
265
  packet loss. A value of 0.0 will trigger on any packet loss.
237
266
  - **Latency**: All latency thresholds are evaluated in milliseconds.
267
+ - **Jitter**: All jitter thresholds are evaluated in milliseconds.
238
268
 
239
269
  ## Usage
240
270
 
241
- This [guide](./docs/usage.md) explains common ways to run *woninet* and how to resolve common issues.
271
+ This [guide](https://github.com/teymuri-alireza/woninet/blob/main/docs/usage.md) explains common ways to run *woninet* and how to resolve common issues.
242
272
 
243
273
  ## Testing
244
274
 
@@ -263,10 +293,10 @@ The test suite currently focuses on API behavior and core logic, and it uses pyt
263
293
 
264
294
  ## Documentation
265
295
 
266
- if you have any question or need clarification, check the [documentation](./docs/documentation.md) guide.
296
+ if you have any question or need clarification, check the [documentation](.https://github.com/teymuri-alireza/woninet/blob/main/docs/documentation.md) guide.
267
297
 
268
298
  ## Contribution
269
299
 
270
300
  Thanks for considering contributing to *woninet!*
271
301
  To help us maintain code quality, stability, and a predictable release process, please review
272
- and follow the guidelines in our [Contributing Guide.](./CONTRIBUTING.md)
302
+ and follow the guidelines in our [Contributing Guide.](https://github.com/teymuri-alireza/woninet/blob/main/CONTRIBUTING.md)
@@ -44,6 +44,8 @@ IPs and filters out Wi‑Fi ARP‑delay noise.)
44
44
 
45
45
  ### Unix/Linux
46
46
 
47
+ 1. **Method 1 (recommended (Uses the `icmplib` module)):**
48
+
47
49
  Create and activate a virtual environment:
48
50
 
49
51
  ```shell
@@ -69,7 +71,28 @@ Then run:
69
71
  woninet --help
70
72
  ```
71
73
 
72
- #### macOS/Windows
74
+ 2. **Method 2 (Uses the system's `ping` command as a fallback):**
75
+
76
+ Create and activate a virtual environment:
77
+
78
+ ```shell
79
+ python3 -m venv venv
80
+ source venv/bin/activate
81
+ ```
82
+
83
+ Install from PyPI:
84
+
85
+ ```shell
86
+ pip install woninet
87
+ ```
88
+
89
+ Then run:
90
+
91
+ ```shell
92
+ woninet --help
93
+ ```
94
+
95
+ ### macOS/Windows
73
96
 
74
97
  Install from PyPI:
75
98
 
@@ -146,7 +169,7 @@ sudo ../venv/bin/python3 -m woninet
146
169
  ## Configuration
147
170
 
148
171
  You can define alert rules in a JSON file. Rename the example file at
149
- `woninet/config.example.json` to `config.json`. The application will load the alert
172
+ `config.example.json` to `config.json`. The application will load the alert
150
173
  rules from that file at startup.
151
174
 
152
175
  ### Example:
@@ -173,6 +196,11 @@ rules from that file at startup.
173
196
  "metric": "packet_loss",
174
197
  "threshold": 0.0,
175
198
  "consecutive_checks": 1
199
+ },
200
+ {
201
+ "metric": "jitter",
202
+ "threshold": 40.0,
203
+ "consecutive_checks": 3
176
204
  }
177
205
  ]
178
206
  }
@@ -197,7 +225,7 @@ Higher values may increase system load and latency.
197
225
 
198
226
  #### Alert Rules
199
227
 
200
- - **metric**: The metric name to evaluate (`latency` or `packet_loss`)
228
+ - **metric**: The metric name to evaluate (`latency`, `packet_loss` or `jitter`)
201
229
  - **threshold**: The value that triggers the alert. For latency, this is in milliseconds.
202
230
  For packet loss, this is a percentage (0.0-100.0).
203
231
  - **consecutive_checks**: The number of consecutive evaluations required before the alert
@@ -208,10 +236,11 @@ state changes. Higher values reduce false positives.
208
236
  - **Packet Loss**: Values are in the range [0.0, 100.0]. A value of 100.0 indicates 100%
209
237
  packet loss. A value of 0.0 will trigger on any packet loss.
210
238
  - **Latency**: All latency thresholds are evaluated in milliseconds.
239
+ - **Jitter**: All jitter thresholds are evaluated in milliseconds.
211
240
 
212
241
  ## Usage
213
242
 
214
- This [guide](./docs/usage.md) explains common ways to run *woninet* and how to resolve common issues.
243
+ This [guide](https://github.com/teymuri-alireza/woninet/blob/main/docs/usage.md) explains common ways to run *woninet* and how to resolve common issues.
215
244
 
216
245
  ## Testing
217
246
 
@@ -236,10 +265,10 @@ The test suite currently focuses on API behavior and core logic, and it uses pyt
236
265
 
237
266
  ## Documentation
238
267
 
239
- if you have any question or need clarification, check the [documentation](./docs/documentation.md) guide.
268
+ if you have any question or need clarification, check the [documentation](.https://github.com/teymuri-alireza/woninet/blob/main/docs/documentation.md) guide.
240
269
 
241
270
  ## Contribution
242
271
 
243
272
  Thanks for considering contributing to *woninet!*
244
273
  To help us maintain code quality, stability, and a predictable release process, please review
245
- and follow the guidelines in our [Contributing Guide.](./CONTRIBUTING.md)
274
+ and follow the guidelines in our [Contributing Guide.](https://github.com/teymuri-alireza/woninet/blob/main/CONTRIBUTING.md)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "woninet"
7
- version = "2.0.2"
7
+ version = "2.1.0"
8
8
  description = "A Python-based ARP+ICMP network monitoring tool"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -52,4 +52,5 @@ woninet = "woninet.main:main"
52
52
  test = [
53
53
  "pytest>=9.1.1,<9.2",
54
54
  "httpx>=0.28.1,<0.29",
55
+ "ruff>=0.15.11,<0.16",
55
56
  ]
@@ -71,6 +71,40 @@ def test_packet_loss_trigger_alert_event(db_session):
71
71
  assert packet_loss.event_type == "trigger"
72
72
 
73
73
 
74
+ def test_jitter_trigger_alert_event(db_session):
75
+ storage = StorageEngine(session_factory=lambda: db_session)
76
+
77
+ metric = MetricRecord(device_ip="192.168.1.10", metric="jitter", value=60.0)
78
+ storage.store_metric(metric)
79
+
80
+ alert_rule = {"metric": "jitter", "threshold": 40.0, "consecutive_checks": 0}
81
+
82
+ alert = AlertEngine(
83
+ storage=storage,
84
+ rules=[
85
+ AlertRule(
86
+ alert_rule["metric"],
87
+ alert_rule["threshold"],
88
+ alert_rule["consecutive_checks"],
89
+ )
90
+ ],
91
+ )
92
+
93
+ alert.evaluate(
94
+ ip="192.168.1.10",
95
+ metrics_list=[
96
+ MetricRecord(device_ip="192.168.1.10", metric="jitter", value=60.0),
97
+ ],
98
+ default_consecutive_checks={
99
+ "jitter": 0,
100
+ },
101
+ )
102
+
103
+ jitter = storage.get_recent_alert_events()[0]
104
+
105
+ assert jitter.event_type == "trigger"
106
+
107
+
74
108
  def test_latency_recover_alert_event(db_session):
75
109
  storage = StorageEngine(session_factory=lambda: db_session)
76
110
 
@@ -149,3 +183,43 @@ def test_packet_loss_recover_alert_event(db_session):
149
183
  packet_loss = storage.get_recent_alert_events()[0]
150
184
 
151
185
  assert packet_loss.event_type == "recover"
186
+
187
+
188
+ def test_jitter_recover_alert_event(db_session):
189
+ storage = StorageEngine(session_factory=lambda: db_session)
190
+
191
+ metric = MetricRecord(device_ip="192.168.1.10", metric="jitter", value=20.0)
192
+ storage.store_metric(metric)
193
+
194
+ state = storage.get_or_create_alert_state(
195
+ ip="192.168.1.10", metric="jitter", consecutive_checks=0
196
+ )
197
+ state.state = "warning"
198
+ storage.update_alert_state(state)
199
+
200
+ alert_rule = {"metric": "jitter", "threshold": 40.0, "consecutive_checks": 0}
201
+
202
+ alert = AlertEngine(
203
+ storage=storage,
204
+ rules=[
205
+ AlertRule(
206
+ alert_rule["metric"],
207
+ alert_rule["threshold"],
208
+ alert_rule["consecutive_checks"],
209
+ )
210
+ ],
211
+ )
212
+
213
+ alert.evaluate(
214
+ ip="192.168.1.10",
215
+ metrics_list=[
216
+ MetricRecord(device_ip="192.168.1.10", metric="jitter", value=20.0),
217
+ ],
218
+ default_consecutive_checks={
219
+ "jitter": 0,
220
+ },
221
+ )
222
+
223
+ jitter = storage.get_recent_alert_events()[0]
224
+
225
+ assert jitter.event_type == "recover"
@@ -0,0 +1,37 @@
1
+ def test_dashboard_endpoint(client):
2
+ response = client.get("/")
3
+ assert response.status_code == 200
4
+
5
+
6
+ def test_devices_api_returns_mocked_devices(client):
7
+ response = client.get("/api/devices")
8
+
9
+ assert response.status_code == 200
10
+ assert response.json()["devices"][0]["ip"] == "192.168.1.10"
11
+
12
+
13
+ def test_device_info_api_returns_mocked_device(client):
14
+ response = client.get("/api/devices/192.168.100.1")
15
+
16
+ assert response.status_code == 200
17
+ assert response.json()["device"]["ip"] == "192.168.100.1"
18
+
19
+
20
+ def test_device_info_page_returns_mocked_device(client):
21
+ response = client.get("/devices/192.168.100.2")
22
+
23
+ assert response.status_code == 200
24
+
25
+
26
+ def test_device_info_page_returns_unknown_device(client):
27
+ response = client.get("/devices/192.168.100.10")
28
+
29
+ assert response.status_code == 404
30
+
31
+
32
+ def test_stats_api_returns_mocked_devices(client):
33
+ response = client.get("/api/stats")
34
+
35
+ assert response.status_code == 200
36
+ assert response.json()["health"]["engine_alive"]
37
+ assert response.json()["identity"]["server_ip"] == "192.168.200.100"
@@ -0,0 +1,121 @@
1
+ import re
2
+
3
+
4
+ def _parse_ping_output(output: str, os_name: str):
5
+ if os_name != "Windows":
6
+ avg_rtt_regex = r"rtt min/avg/max/mdev = [\d\.]+/([\d\.]+)/[\d\.]+/[\d\.]+ ms"
7
+
8
+ packet_loss_regex = r"([\d\.]+)% packet loss"
9
+
10
+ per_ping_time_regex = r"time[=<]([\d\.]+) ?ms"
11
+ else:
12
+ avg_rtt_regex = r"Average = ([\d\.]+)ms"
13
+
14
+ packet_loss_regex = r"\(([^\)]+)% loss\)"
15
+
16
+ per_ping_time_regex = r"time[=<]([\d\.]+)ms"
17
+
18
+ avg_rtt = 0
19
+ packet_loss = 0.0
20
+ per_ping_times = []
21
+ for line in output.splitlines():
22
+ m = re.search(avg_rtt_regex, line)
23
+ if m:
24
+ try:
25
+ avg_rtt = float(m.group(1))
26
+ except ValueError:
27
+ avg_rtt = 0
28
+ continue
29
+
30
+ m = re.search(packet_loss_regex, line)
31
+ if m:
32
+ try:
33
+ packet_loss = float(m.group(1))
34
+ except ValueError:
35
+ packet_loss = 0.0
36
+
37
+ m = re.search(per_ping_time_regex, line)
38
+ if m:
39
+ try:
40
+ per_ping_times.append(float(m.group(1)))
41
+ except ValueError:
42
+ pass
43
+
44
+ jitter = 0.0
45
+ if len(per_ping_times) >= 2:
46
+ diffs = [
47
+ abs(per_ping_times[i] - per_ping_times[i - 1])
48
+ for i in range(1, len(per_ping_times))
49
+ ]
50
+ jitter = round(sum(diffs) / len(diffs), 3)
51
+
52
+ return avg_rtt, packet_loss, jitter
53
+
54
+
55
+ def test_successful_parse_output_on_linux():
56
+ sample_output = """
57
+ PING 192.168.1.1 (192.168.1.1) 56(84) bytes of data.
58
+ 64 bytes from 192.168.1.1: icmp_seq=1 ttl=64 time=2.67 ms
59
+ 64 bytes from 192.168.1.1: icmp_seq=2 ttl=64 time=6.38 ms
60
+
61
+ --- 192.168.1.1 ping statistics ---
62
+ 2 packets transmitted, 2 received, 0% packet loss, time 1002ms
63
+ rtt min/avg/max/mdev = 2.672/4.524/6.376/1.852 ms
64
+ """
65
+
66
+ avg_rtt, packet_loss, jitter = _parse_ping_output(sample_output, "Linux")
67
+
68
+ assert avg_rtt == 4.524
69
+ assert packet_loss == 0.0
70
+ assert jitter == 3.71
71
+
72
+
73
+ def test_failed_parse_output_on_linux():
74
+ sample_output = """
75
+ PING 192.168.1.2 (192.168.1.2) 56(84) bytes of data.
76
+
77
+ --- 192.168.1.2 ping statistics ---
78
+ 2 packets transmitted, 0 received, 100% packet loss, time 1044ms
79
+ """
80
+
81
+ avg_rtt, packet_loss, jitter = _parse_ping_output(sample_output, "Linux")
82
+
83
+ assert avg_rtt == 0.0
84
+ assert packet_loss == 100.0
85
+ assert jitter == 0.0
86
+
87
+
88
+ def test_successful_parse_output_on_windows():
89
+ sample_output = """
90
+ Pinging 192.168.1.1 with 32 bytes of data:
91
+ Reply from 192.168.1.1: bytes=32 time=6ms TTL=64
92
+ Reply from 192.168.1.1: bytes=32 time=2ms TTL=64
93
+
94
+ Ping statistics for 192.168.1.1:
95
+ Packets: Sent = 2, Received = 2, Lost = 0 (0% loss),
96
+ Approximate round trip times in milli-seconds:
97
+ Minimum = 2ms, Maximum = 6ms, Average = 4ms
98
+ """
99
+
100
+ avg_rtt, packet_loss, jitter = _parse_ping_output(sample_output, "Windows")
101
+
102
+ assert avg_rtt == 4.0
103
+ assert packet_loss == 0.0
104
+ assert jitter == 4.0
105
+
106
+
107
+ def test_failed_parse_output_on_windows():
108
+ sample_output = """
109
+ Pinging 192.168.1.2 with 32 bytes of data:
110
+ Request timed out.
111
+ Request timed out.
112
+
113
+ Ping statistics for 192.168.1.2:
114
+ Packets: Sent = 2, Received = 0, Lost = 2 (100% loss),
115
+ """
116
+
117
+ avg_rtt, packet_loss, jitter = _parse_ping_output(sample_output, "Windows")
118
+
119
+ assert avg_rtt == 0.0
120
+ assert packet_loss == 100.0
121
+ assert jitter == 0.0
@@ -1,2 +1,2 @@
1
1
  # woninet/core/__init__.py
2
- __version__ = "2.0.2"
2
+ __version__ = "2.1.0"
@@ -85,24 +85,34 @@ class AlertEngine:
85
85
  """
86
86
  for metric in metrics_list:
87
87
  if metric is not None:
88
- if metric.metric == "latency":
89
- self._evaluate_latency(
90
- ip=ip,
91
- metric="latency",
92
- value=metric.value,
93
- default_consecutive_checks=default_consecutive_checks[
94
- "latency"
95
- ],
96
- )
97
- elif metric.metric == "packet_loss":
98
- self._evaluate_packet_loss(
99
- ip=ip,
100
- metric="packet_loss",
101
- value=metric.value,
102
- default_consecutive_checks=default_consecutive_checks[
103
- "packet_loss"
104
- ],
105
- )
88
+ if metric.value is not None:
89
+ if metric.metric == "latency":
90
+ self._evaluate_latency(
91
+ ip=ip,
92
+ metric="latency",
93
+ value=metric.value,
94
+ default_consecutive_checks=default_consecutive_checks[
95
+ "latency"
96
+ ],
97
+ )
98
+ elif metric.metric == "packet_loss":
99
+ self._evaluate_packet_loss(
100
+ ip=ip,
101
+ metric="packet_loss",
102
+ value=metric.value,
103
+ default_consecutive_checks=default_consecutive_checks[
104
+ "packet_loss"
105
+ ],
106
+ )
107
+ elif metric.metric == "jitter":
108
+ self._evaluate_jitter(
109
+ ip=ip,
110
+ metric="jitter",
111
+ value=metric.value,
112
+ default_consecutive_checks=default_consecutive_checks[
113
+ "jitter"
114
+ ],
115
+ )
106
116
 
107
117
  def _evaluate_latency(
108
118
  self, ip: str, metric: str, value: float, default_consecutive_checks: int
@@ -249,3 +259,78 @@ class AlertEngine:
249
259
  # Reset consecutive checks to its default value
250
260
  state.consecutive_checks = default_consecutive_checks
251
261
  self.storage.update_alert_state(state)
262
+
263
+ def _evaluate_jitter(
264
+ self, ip: str, metric: str, value: float, default_consecutive_checks: int
265
+ ):
266
+ """
267
+ Evaluate a jitter metric against the related alert rule.
268
+
269
+ If the metric crosses its threshold for a consecutive number of checks,
270
+ trigger or resolve an alert accordingly. Persists state transitions
271
+ and logs any alert changes.
272
+
273
+ Args:
274
+ ip (str): IP address to evaluate.
275
+ metric (str): Metric name (e.g. `jitter`).
276
+ value (float): Value of the current metric.
277
+ default_consecutive_checks (int): Number of consecutive evaluations
278
+ required to confirm a state transition.
279
+
280
+ Side Effects:
281
+ - Persists alert state in the database via `StorageEngine`.
282
+ - Emits log events.
283
+ - Creates `AlertEventTable` records on trigger and recover.
284
+ """
285
+ state = self.storage.get_or_create_alert_state(
286
+ ip=ip, metric=metric, consecutive_checks=default_consecutive_checks
287
+ )
288
+ violated = self.is_metric_violated(metric=metric, value=value)
289
+ if violated:
290
+ if state.state == "ok":
291
+ # Confirm remaining consecutive checks before a state transition
292
+ if state.consecutive_checks > 0:
293
+ state.consecutive_checks = state.consecutive_checks - 1
294
+ self.storage.update_alert_state(state)
295
+ return
296
+ # Update "ok" to "warning"
297
+ state.state = "warning"
298
+ state.triggered_at = datetime.now()
299
+ state.consecutive_checks = default_consecutive_checks
300
+ self.storage.update_alert_state(state)
301
+
302
+ event = AlertEventTable(
303
+ device_ip=ip, metric=metric, value=value, event_type="trigger"
304
+ )
305
+ self.storage.store_alert_event(event=event)
306
+ core_logger.warning(
307
+ f"ALERT TRIGGERED | {ip}, metric={metric}, value={value} ms"
308
+ )
309
+ else:
310
+ # Reset consecutive checks to its default value
311
+ state.consecutive_checks = default_consecutive_checks
312
+ self.storage.update_alert_state(state)
313
+ else:
314
+ if state.state == "warning":
315
+ # Confirm remaining consecutive checks before a state transition
316
+ if state.consecutive_checks > 0:
317
+ state.consecutive_checks = state.consecutive_checks - 1
318
+ self.storage.update_alert_state(state)
319
+ return
320
+ # Update "warning" to "ok"
321
+ state.state = "ok"
322
+ state.triggered_at = datetime.now()
323
+ state.consecutive_checks = default_consecutive_checks
324
+ self.storage.update_alert_state(state)
325
+
326
+ event = AlertEventTable(
327
+ device_ip=ip, metric=metric, value=value, event_type="recover"
328
+ )
329
+ self.storage.store_alert_event(event=event)
330
+ core_logger.warning(
331
+ f"ALERT RECOVERED | {ip}, metric={metric}, value={value} ms"
332
+ )
333
+ else:
334
+ # Reset consecutive checks to its default value
335
+ state.consecutive_checks = default_consecutive_checks
336
+ self.storage.update_alert_state(state)