Skip to content

Commit 2091bec

Browse files
committed
Add low battery notifications
Fixes #584
1 parent b00f17d commit 2091bec

File tree

10 files changed

+165
-24
lines changed

10 files changed

+165
-24
lines changed

daemon/src/battery/mod.rs

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
1-
use std::path::Path;
1+
use std::{path::Path, time::Duration};
22

3+
use log::{error, info};
34
use rayhunter::Device;
45
use serde::Serialize;
6+
use tokio::select;
7+
use tokio_util::{sync::CancellationToken, task::TaskTracker};
58

6-
use crate::error::RayhunterError;
9+
use crate::{
10+
error::RayhunterError,
11+
notifications::{Notification, NotificationType},
12+
};
713

814
pub mod orbic;
915
pub mod tmobile;
1016
pub mod wingtech;
1117

18+
const LOW_BATTERY_LEVEL: u8 = 10;
19+
1220
#[derive(Clone, Copy, PartialEq, Debug, Serialize)]
1321
pub struct BatteryState {
1422
level: u8,
@@ -45,3 +53,59 @@ pub async fn get_battery_status(device: &Device) -> Result<BatteryState, Rayhunt
4553
_ => return Err(RayhunterError::FunctionNotSupportedForDeviceError),
4654
})
4755
}
56+
57+
pub fn run_battery_notification_worker(
58+
task_tracker: &TaskTracker,
59+
device: Device,
60+
notification_channel: tokio::sync::mpsc::Sender<Notification>,
61+
shutdown_token: CancellationToken,
62+
) {
63+
task_tracker.spawn(async move {
64+
// Don't send a notification initially if the device starts at a low battery level.
65+
let mut triggered = match get_battery_status(&device).await {
66+
Err(RayhunterError::FunctionNotSupportedForDeviceError) => {
67+
info!("Battery level function not supported for device");
68+
false
69+
}
70+
Err(e) => {
71+
error!("Failed to get battery status: {e}");
72+
true
73+
}
74+
Ok(status) => status.level < LOW_BATTERY_LEVEL,
75+
};
76+
77+
loop {
78+
select! {
79+
_ = shutdown_token.cancelled() => break,
80+
_ = tokio::time::sleep(Duration::from_secs(15)) => {}
81+
}
82+
83+
let status = match get_battery_status(&device).await {
84+
Err(e) => {
85+
error!("Failed to get battery status: {e}");
86+
continue;
87+
}
88+
Ok(status) => status,
89+
};
90+
91+
// To avoid flapping, if the notification has already been triggered
92+
// wait until the device has been plugged in and the battery level
93+
// is high enough to re-enable notifications.
94+
if triggered && status.is_plugged_in && status.level > LOW_BATTERY_LEVEL {
95+
triggered = false;
96+
continue;
97+
}
98+
if !triggered && !status.is_plugged_in && status.level <= LOW_BATTERY_LEVEL {
99+
notification_channel
100+
.send(Notification::new(
101+
NotificationType::LowBattery,
102+
"Rayhunter's battery is low".to_string(),
103+
None,
104+
))
105+
.await
106+
.expect("Failed to send to notification channel");
107+
triggered = true;
108+
}
109+
}
110+
});
111+
}

daemon/src/config.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use rayhunter::Device;
55
use rayhunter::analysis::analyzer::AnalyzerConfig;
66

77
use crate::error::RayhunterError;
8+
use crate::notifications::NotificationType;
89

910
#[derive(Debug, Clone, Deserialize, Serialize)]
1011
#[serde(default)]
@@ -17,6 +18,7 @@ pub struct Config {
1718
pub colorblind_mode: bool,
1819
pub key_input_mode: u8,
1920
pub ntfy_url: Option<String>,
21+
pub enabled_notifications: Vec<NotificationType>,
2022
pub analyzers: AnalyzerConfig,
2123
}
2224

@@ -32,6 +34,7 @@ impl Default for Config {
3234
key_input_mode: 0,
3335
analyzers: AnalyzerConfig::default(),
3436
ntfy_url: None,
37+
enabled_notifications: vec![NotificationType::Warning, NotificationType::LowBattery],
3538
}
3639
}
3740
}

daemon/src/diag.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ use rayhunter::qmdl::QmdlWriter;
2424

2525
use crate::analysis::{AnalysisCtrlMessage, AnalysisWriter};
2626
use crate::display;
27-
use crate::notifications::Notification;
27+
use crate::notifications::{Notification, NotificationType};
2828
use crate::qmdl_store::{RecordingStore, RecordingStoreError};
2929
use crate::server::ServerState;
3030

@@ -207,7 +207,7 @@ impl DiagTask {
207207
info!("a heuristic triggered on this run!");
208208
self.notification_channel
209209
.send(Notification::new(
210-
"heuristic-warning".to_string(),
210+
NotificationType::Warning,
211211
format!("Rayhunter has detected a {:?} severity event", max_type),
212212
Some(Duration::from_secs(60 * 5)),
213213
))

daemon/src/main.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ mod stats;
1414
use std::net::SocketAddr;
1515
use std::sync::Arc;
1616

17+
use crate::battery::run_battery_notification_worker;
1718
use crate::config::{parse_args, parse_config};
1819
use crate::diag::run_diag_read_thread;
1920
use crate::error::RayhunterError;
@@ -260,7 +261,20 @@ async fn run_with_config(
260261
qmdl_store_lock.clone(),
261262
analysis_tx.clone(),
262263
);
263-
run_notification_worker(&task_tracker, notification_service);
264+
265+
run_battery_notification_worker(
266+
&task_tracker,
267+
config.device.clone(),
268+
notification_service.new_handler(),
269+
shutdown_token.clone(),
270+
);
271+
272+
run_notification_worker(
273+
&task_tracker,
274+
notification_service,
275+
config.enabled_notifications.clone(),
276+
);
277+
264278
let state = Arc::new(ServerState {
265279
config_path: args.config_path.clone(),
266280
config,

daemon/src/notifications.rs

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,30 @@ use std::{
55
};
66

77
use log::error;
8+
use serde::{Deserialize, Serialize};
89
use tokio::sync::mpsc::{self, error::TryRecvError};
910
use tokio_util::task::TaskTracker;
1011

12+
#[derive(Hash, Eq, PartialEq, Debug, Clone, Serialize, Deserialize)]
13+
pub enum NotificationType {
14+
Warning,
15+
LowBattery,
16+
}
17+
1118
pub struct Notification {
12-
message_type: String,
19+
notification_type: NotificationType,
1320
message: String,
1421
debounce: Option<Duration>,
1522
}
1623

1724
impl Notification {
18-
pub fn new(message_type: String, message: String, debounce: Option<Duration>) -> Self {
25+
pub fn new(
26+
notification_type: NotificationType,
27+
message: String,
28+
debounce: Option<Duration>,
29+
) -> Self {
1930
Notification {
20-
message_type,
31+
notification_type,
2132
message,
2233
debounce,
2334
}
@@ -52,6 +63,7 @@ impl NotificationService {
5263
pub fn run_notification_worker(
5364
task_tracker: &TaskTracker,
5465
mut notification_service: NotificationService,
66+
enabled_notifications: Vec<NotificationType>,
5567
) {
5668
task_tracker.spawn(async move {
5769
if let Some(url) = notification_service.url
@@ -65,8 +77,12 @@ pub fn run_notification_worker(
6577
loop {
6678
match notification_service.rx.try_recv() {
6779
Ok(notification) => {
80+
if !enabled_notifications.contains(&notification.notification_type) {
81+
continue;
82+
}
83+
6884
let status = notification_statuses
69-
.entry(notification.message_type)
85+
.entry(notification.notification_type)
7086
.or_insert_with(|| NotificationStatus {
7187
message: "".to_string(),
7288
needs_sending: true,

daemon/web/src/lib/components/ConfigForm.svelte

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -111,18 +111,6 @@
111111
</select>
112112
</div>
113113

114-
<div>
115-
<label for="ntfy_url" class="block text-sm font-medium text-gray-700 mb-1">
116-
ntfy URL for Sending Notifications
117-
</label>
118-
<input
119-
id="ntfy_url"
120-
type="url"
121-
bind:value={config.ntfy_url}
122-
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-rayhunter-blue"
123-
/>
124-
</div>
125-
126114
<div class="space-y-3">
127115
<div class="flex items-center">
128116
<input
@@ -137,6 +125,51 @@
137125
</div>
138126
</div>
139127

128+
<div class="border-t pt-4 mt-6 space-y-3">
129+
<h3 class="text-lg font-semibold text-gray-800 mb-4">
130+
Notification Settings
131+
</h3>
132+
<div>
133+
<label for="ntfy_url" class="block text-sm font-medium text-gray-700 mb-1">
134+
ntfy URL for Sending Notifications (if unset you will not receive notifications)
135+
</label>
136+
<input
137+
id="ntfy_url"
138+
type="url"
139+
bind:value={config.ntfy_url}
140+
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-rayhunter-blue"
141+
/>
142+
</div>
143+
144+
<div class="space-y-2">
145+
<div class="block text-sm font-medium text-gray-700 mb-1">
146+
Enabled Notification Types
147+
</div>
148+
<div class="flex items-center">
149+
<input
150+
type="checkbox"
151+
id="enable_warning_notifications"
152+
value="Warning"
153+
bind:group={config.enabled_notifications}
154+
/>
155+
<label for="enable_warning_notifications" class="ml-2 block text-sm text-gray-700">
156+
Warnings
157+
</label>
158+
</div>
159+
<div class="flex items-center">
160+
<input
161+
type="checkbox"
162+
id="enable_lowbattery_notifications"
163+
value="LowBattery"
164+
bind:group={config.enabled_notifications}
165+
/>
166+
<label for="enable_lowbattery_notifications" class="ml-2 block text-sm text-gray-700">
167+
Low Battery
168+
</label>
169+
</div>
170+
</div>
171+
</div>
172+
140173
<div class="border-t pt-4 mt-6">
141174
<h3 class="text-lg font-semibold text-gray-800 mb-4">
142175
Analyzer Heuristic Settings

daemon/web/src/lib/utils.svelte.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,17 @@ export interface AnalyzerConfig {
1212
test_analyzer: boolean;
1313
}
1414

15+
export enum enabled_notifications {
16+
Warning = 'Warning',
17+
LowBattery = 'LowBattery',
18+
}
19+
1520
export interface Config {
1621
ui_level: number;
1722
colorblind_mode: boolean;
1823
key_input_mode: number;
1924
ntfy_url: string;
25+
enabled_notifications: enabled_notifications[];
2026
analyzers: AnalyzerConfig;
2127
}
2228

dist/config.toml.in

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ ui_level = 1
2323
key_input_mode = 0
2424

2525
# If set, attempts to send a notification to the url when a new warning is triggered
26-
# ntfy_url =
26+
ntfy_url = ""
27+
# What notification types to enable. Does nothing if the above ntfy_url is not set.
28+
enabled_notifications = ["Warning", "LowBattery"]
2729

2830
# Analyzer Configuration
2931
# Enable/disable specific IMSI catcher detection heuristics
@@ -35,4 +37,4 @@ lte_sib6_and_7_downgrade = true
3537
null_cipher = true
3638
nas_null_cipher = true
3739
incomplete_sib = true
38-
test_analyzer = false
40+
test_analyzer = false

doc/configuration.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,11 @@ Through web UI you can set:
1313
- **Device Input Mode**, which defines behaviour of built-in power button of the device. *Device Input Mode* could be:
1414
- *Disable button control*: built-in power button of the device is not used by Rayhunter;
1515
- *Double-tap power button to start/stop recording*: double clicking on a built-in power button of the device stops and immediately restarts the recording. This could be useful if Rayhunter's heuristichs is triggered and you get the red line, and you want to "reset" the past warnings. Normally you can do that through web UI, but sometimes it is easier to double tap on power button.
16-
- **ntfy URL for Sending Notifications**, which allows setting a [ntfy](https://ntfy.sh/) URL to which notifications of new detections will be sent. The topic should be unique to your device, e.g., `https://ntfy.sh/rayhunter_notifications_ba9di7ie` or `https://myserver.example.com/rayhunter_notifications_ba9di7ie`. The ntfy Android and iOS apps can then be used to receive notifications. More information can be found in the [ntfy docs](https://docs.ntfy.sh/).
1716
- **Colorblind Mode** enables color blind mode (blue line is shown instead of green line, red line remains red). Please note that this does not cover all types of color blindness, but switching green to blue should be about enough to differentiate the color change for most types of color blindness.
17+
- **ntfy URL**, which allows setting a [ntfy](https://ntfy.sh/) URL to which notifications of new detections will be sent. The topic should be unique to your device, e.g., `https://ntfy.sh/rayhunter_notifications_ba9di7ie` or `https://myserver.example.com/rayhunter_notifications_ba9di7ie`. The ntfy Android and iOS apps can then be used to receive notifications. More information can be found in the [ntfy docs](https://docs.ntfy.sh/).
18+
- **Enabled Notification Types** allows enabling or disabling the following types of notifications:
19+
- *Warnings*, which will alert when a heuristic is triggered. Alerts will be sent at most once every five minutes.
20+
- *Low Battery*, which will alert when the device's battery is low. Notifications may not be supported for all devices—you can check if your device is supported by looking at whether the battery level indicator is functioning on the System Information section of the Rayhunter UI.
1821
- With **Analyzer Heuristic Settings** you can switch on or off built-in [Rayhunter heuristics](heuristics.md). Some heuristics are experimental or can trigger a lot of false positive warnings in some networks (our tests have shown that some heuristics have different behaviour in US or European networks). In that case you can decide whether you would like to have the heuristics that trigger a lot of false positives on or off. Please note that we are constantly improving and adding new heuristics, so new release may reduce false positives in existing heuristics as well.
1922

2023
If you prefer editing `config.toml` file, you need to obtain a shell on your [Orbic](./orbic.md#obtaining-a-shell) or [TP-Link](./tplink-m7350.md#obtaining-a-shell) device and edit the file manually. You can view the [default configuration file on a GitHub](https://github.com/EFForg/rayhunter/blob/main/dist/config.toml.in).

doc/rayhunter_config.png

12.9 KB
Loading

0 commit comments

Comments
 (0)