Skip to content

Commit 4521bf4

Browse files
authored
bluetooth-battery@zamszowy: notifications, manual device and fixes (#4341)
* bluetooth-battery@zamszowy: don't use removed interface 'refresh_sync' was deprecated and in latest Glib-Upower it's removed entirely from the API. This does not change functionality, since 'refresh_sync' was working only in debug mode anyway. * bluetooth-battery@zamszowy: don't use deprecated AppletSettings.bindProperty() Use AppletSettings.bind() instead. Add also calling of AppletSettings.finalize() on applet removal. * bluetooth-battery@zamszowy: use different markdown newline approach * bluetooth-battery@zamszowy: change name to match applet UUID * bluetooth-battery@zamszowy: use 'combobox' instead of 'radiogroup' Also remove invalid and duplicated default option. * bluetooth-battery@zamszowy: add option to manually select display device Add possibility to display manually selected device in applet, instead of one that has lowest battery. * bluetooth-battery@zamszowy: add notifications
1 parent b11b9df commit 4521bf4

File tree

5 files changed

+364
-44
lines changed

5 files changed

+364
-44
lines changed

bluetooth-battery@zamszowy/README.md

+11-5
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,26 @@
11
# Bluetooth (and other) devices battery monitor
22

3-
This applet monitors (through UPowerGlib) battery levels of mice, keyboards, headphones and other connected devices.\
4-
It displays icon and text with battery level information, and for mice, keyboards and headphones, battery icon contains also mouse/keyboard/headphones symbol.\
3+
This applet monitors (through UPowerGlib) battery levels of mice, keyboards, headphones and other connected devices.
4+
It displays icon and text with battery level information, and for mice, keyboards and headphones, battery icon contains also mouse/keyboard/headphones symbol.
55
Device with lowest battery is displayed in panel. When clicked, it displays list with all monitored devices.
66

77
## Settings
8-
You can disable monitoring (it also disables blacklist configuration) of keyboards, mice, headphones or all other devices.\
8+
You can disable monitoring (it also disables blacklist configuration) of keyboards, mice, headphones or all other devices.
99
You can also choose to display in applet icon only, text only or icon and text.
1010

11+
## Notifications
12+
Notifications are enabled by default and the applet will emit notification when battery level of any of the monitored devices will drop below configured level.
13+
Another notification with "critical" urgency will be emitted when battery level will drop even further below another configured level.
14+
15+
You can also rely on notifications alone and disable applet icon and text entirely, but configure it to show only when battery drops below configured warning/critical level.
16+
1117
## Blacklist
12-
You can blacklist any device detected by the applet.\
18+
You can blacklist any device detected by the applet.
1319
All detected devices are automatically added to blacklist (common devices like mice and keyboards are added just as comments, other are disabled right away).
1420

1521
## Bluetooth Headphones
1622
Currently (with bluetoothd v5.64) reporting battery percentage for bluetooth headphones can be enabled by starting bluetoothd with experimental features,
17-
but bear in mind that enabling it can cause some issues, like mice not connecting automatically (see https://github.com/bluez/bluez/issues/236 for details).\
23+
but bear in mind that enabling it can cause some issues, like mice not connecting automatically (see https://github.com/bluez/bluez/issues/236 for details).
1824
This could be somehow circumvented by enabling only one experimental UUID, but it's still not guaranteed to be bug free - expect issues!
1925

2026
## Icons

bluetooth-battery@zamszowy/files/bluetooth-battery@zamszowy/applet.js

+191-33
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ const xml ='<node>\
1616
</node>';
1717
const {Gio, UPowerGlib: UPower} = imports.gi;
1818
const PowerManagerProxy = Gio.DBusProxy.makeProxyWrapper(xml);
19+
const { messageTray } = imports.ui.main;
20+
const { SystemNotificationSource, Notification, Urgency } = imports.ui.messageTray;
1921

2022
var dbusCon;
2123

@@ -46,6 +48,21 @@ BtBattery.prototype = {
4648
});
4749

4850
this.dbus_map = new Map();
51+
this.monitored_devs = new Array();
52+
},
53+
54+
_init_notifications: function() {
55+
this.MessageSource = new SystemNotificationSource("Bluetooth Battery");
56+
messageTray.add(this.MessageSource);
57+
58+
this.settings.bind("notification-warn-enable", "notification_warn_enable", this._on_settings_change, null);
59+
this.settings.bind("notification-warn-level", "notification_warn_level", null, null);
60+
this.settings.bind("notification-crit-enable", "notification_crit_enable", this._on_settings_change, null);
61+
this.settings.bind("notification-crit-level", "notification_crit_level", null, null);
62+
this.settings.bind("notification-multiple", "notification_multiple", null, null);
63+
this.settings.bind("notification-applet-icon", "notification_applet_icon", this._on_settings_change, null);
64+
65+
this.notified_devices = new Map();
4966
},
5067

5168
_init: function(metadata, orientation, panel_height, instance_id) {
@@ -61,34 +78,114 @@ BtBattery.prototype = {
6178
this.menuManager.addMenu(this.menu);
6279

6380
this.settings = new Settings.AppletSettings(this, metadata.uuid, instance_id);
64-
this.settings.bindProperty(Settings.BindingDirection.IN, "enable-keyboards", "enable_keyboards", this._on_settings_change, null);
65-
this.settings.bindProperty(Settings.BindingDirection.IN, "enable-mice", "enable_mice", this._on_settings_change, null);
66-
this.settings.bindProperty(Settings.BindingDirection.IN, "enable-headphones", "enable_headphones", this._on_settings_change, null);
67-
this.settings.bindProperty(Settings.BindingDirection.IN, "enable-others", "enable_others", this._on_settings_change, null);
68-
this.settings.bindProperty(Settings.BindingDirection.IN, "applet-icon", "applet_icon", this._on_settings_change, null);
69-
this.settings.bindProperty(Settings.BindingDirection.BIDIRECTIONAL, "blacklist", "blacklist", null, null);
81+
this.settings.bind("enable-keyboards", "enable_keyboards", this._on_settings_change, null);
82+
this.settings.bind("enable-mice", "enable_mice", this._on_settings_change, null);
83+
this.settings.bind("enable-headphones", "enable_headphones", this._on_settings_change, null);
84+
this.settings.bind("enable-others", "enable_others", this._on_settings_change, null);
85+
this.settings.bind("applet-icon", "applet_icon", this._on_settings_change, null);
86+
this.settings.bind("blacklist", "blacklist", null, null);
87+
this.settings.bind("override-entry", "override_entry", null, null);
88+
this.settings.bind("override-enable", "override_enabled", this.on_override_enable, null);
7089

7190
this._init_dbus();
91+
this._init_notifications();
7292

7393
this.setup();
7494
},
7595

7696
_on_settings_change: function() {
7797
this.setup();
98+
this.check_override_device();
99+
},
100+
101+
on_override_enable: function() {
102+
this.check_override_device();
103+
this.setup();
104+
},
105+
106+
check_override_device: function() {
107+
if (!this.override_enabled) {
108+
return;
109+
}
110+
111+
if (!this.monitored_devs.includes(this.override_entry)) {
112+
// entry in settings page seems to be refreshing only when external process returns after a while
113+
Util.spawn_async(['/usr/bin/sleep', "2"], Lang.bind(this, function(stdout){
114+
this.override_entry = this.monitored_devs.length > 0 ? this.monitored_devs[0] : "(no device available)";
115+
this.setup(false);
116+
}));
117+
}
118+
},
119+
120+
_on_override_select: function() {
121+
let args = new Array();
122+
args.push('/usr/bin/cjs');
123+
args.push(this.applet_path + '/override-select.js');
124+
125+
if (this.monitored_devs.length > 0) {
126+
for (const dev of this.monitored_devs) {
127+
args.push(dev);
128+
}
129+
// push active item index as last arg
130+
if (this.monitored_devs.includes(this.override_entry)) {
131+
args.push(this.monitored_devs.indexOf(this.override_entry).toString());
132+
} else {
133+
args.push("0");
134+
}
135+
} else {
136+
args.push('(no device available)');
137+
}
138+
139+
Util.spawn_async(args, Lang.bind(this, function(stdout) {
140+
const trimmed_out = stdout.trim();
141+
142+
if (trimmed_out != "") {
143+
this.override_entry = trimmed_out;
144+
this.setup();
145+
}
146+
}));
147+
},
148+
149+
_on_notification_warn_demo: function() {
150+
this.notify("Test notification", "Battery dropped below " + this.notification_warn_level + "%",
151+
this.get_device_batt_icon(UPower.DeviceKind.KEYBOARD, this.notification_warn_level), Urgency.NORMAL);
152+
},
153+
154+
_on_notification_crit_demo: function() {
155+
this.notify("Test notification", "Battery dropped below " + this.notification_crit_level + "%",
156+
this.get_device_batt_icon(UPower.DeviceKind.MOUSE, this.notification_crit_level), Urgency.CRITICAL);
78157
},
79158

80159
on_applet_clicked: function() {
81160
this.setup();
82-
if (this.added_count > 0) {
161+
if (this.monitored_devs.length > 0) {
83162
this.menu.toggle();
84163
}
85164
},
86165

87-
setup: function() {
166+
on_applet_removed: function() {
167+
this.settings.finalize();
168+
},
169+
170+
notify: function(title, message, icon_name, urgency = Urgency.NORMAL) {
171+
let icon = new St.Icon({ icon_name: icon_name,
172+
icon_type: St.IconType.FULLCOLOR,
173+
icon_size: 16 });
174+
175+
const notification = new Notification(this.MessageSource, title, message, {icon: icon });
176+
notification.setTransient(false);
177+
notification.setUrgency(urgency);
178+
this.MessageSource.notify(notification);
179+
},
180+
181+
setup: function(check_override = true) {
88182
this.menu.removeAll();
89183
this._contentSection = new PopupMenu.PopupMenuSection();
90184
this.menu.addMenuItem(this._contentSection);
91185

186+
if (check_override) {
187+
this.check_override_device();
188+
}
92189
this.setup_dbus();
93190
},
94191

@@ -113,7 +210,37 @@ BtBattery.prototype = {
113210
return containes;
114211
},
115212

213+
notify_if_needed: function() {
214+
if (!this.notification_warn_enable && !this.notification_crit_enable) {
215+
return;
216+
}
217+
218+
for (const dev of this.monitored_devs) {
219+
if (!this.dbus_map.has(dev)) {
220+
continue;
221+
}
222+
223+
const device = this.dbus_map.get(dev).device;
224+
let notified_warn = this.notified_devices.has(dev) ? this.notified_devices.get(dev).warn : false;
225+
let notified_crit = this.notified_devices.has(dev) ? this.notified_devices.get(dev).crit : false;
226+
227+
if (this.notification_warn_enable && !notified_warn && device.percentage < this.notification_warn_level) {
228+
this.notify(dev + " (" + device.percentage + "%)", "Battery dropped below " + this.notification_warn_level + "%",
229+
this.get_device_batt_icon(device.kind, device.percentage));
230+
notified_warn = !this.notification_multiple;
231+
}
232+
if (this.notification_crit_enable && !notified_crit && device.percentage < this.notification_crit_level) {
233+
this.notify(dev + " (" + device.percentage + "%)", "Battery dropped below " + this.notification_crit_level + "%",
234+
this.get_device_batt_icon(device.kind, device.percentage));
235+
notified_crit = !this.notification_multiple;
236+
}
237+
this.notified_devices.set(dev, {warn: notified_warn, crit: notified_crit});
238+
}
239+
},
240+
116241
setup_dbus: function() {
242+
this.monitored_devs = new Array();
243+
117244
for (let [ident, props] of this.dbus_map) {
118245
this.dbus_map.set(ident, {device: props.device, proxy: props.proxy, updated: false});
119246
}
@@ -179,14 +306,8 @@ BtBattery.prototype = {
179306
this.set_applet_enabled(true);
180307
}
181308

182-
let bat_min_perc = "100";
183-
let bat_min_type = UPower.DeviceKind.UNKNOWN;
184-
let bat_min_name = "unknown";
185-
this.added_count = 0;
186-
187309
for (let [ident, props] of this.dbus_map) {
188310
let dev = props.device;
189-
dev.refresh_sync(null);
190311

191312
if (dev.battery_level != UPower.DeviceLevel.NONE) {
192313
continue;
@@ -210,32 +331,69 @@ BtBattery.prototype = {
210331
ii.addActor(perc_label, {align: St.Align.END});
211332
this.menu.addMenuItem(ii);
212333

213-
if (perc <= bat_min_perc) {
214-
bat_min_perc = perc;
215-
bat_min_name = name;
216-
bat_min_type = type;
217-
}
218-
219-
this.added_count += 1;
334+
this.monitored_devs.push(name);
220335
}
221336

222-
if (this.added_count > 0) {
223-
this.set_applet_label(bat_min_perc.toString() + "%");
224-
this.set_applet_icon_name(this.get_device_batt_icon(bat_min_type, bat_min_perc));
225-
this.set_applet_tooltip(bat_min_name.toString());
337+
if (this.monitored_devs == 0) {
338+
this.set_applet_label("");
339+
this.set_applet_tooltip("all BT devices has been disabled");
340+
this.set_applet_icon_name("bluetooth-disabled");
341+
} else {
342+
const [min_name, min_kind, min_perc] = this.get_lowest_battery_device();
226343

227-
if (this.applet_icon == "text") {
228-
this.hide_applet_icon();
229-
} else if (this.applet_icon == "icon") {
230-
this.hide_applet_label(true);
344+
if ((this.notification_applet_icon == "warn" && this.notification_warn_enable && min_perc >= this.notification_warn_level)
345+
|| (this.notification_applet_icon == "crit" && this.notification_crit_enable && min_perc >= this.notification_crit_level)) {
346+
this.set_applet_enabled(false);
231347
} else {
348+
this.set_applet_enabled(true);
349+
if (this.override_enabled
350+
&& this.monitored_devs.includes(this.override_entry)
351+
&& this.dbus_map.has(this.override_entry)) {
232352

353+
let d = this.dbus_map.get(this.override_entry).device;
354+
this.show_hide_text_icon(this.override_entry, d.kind, d.percentage);
355+
} else {
356+
this.show_hide_text_icon(min_name, min_kind, min_perc);
357+
}
233358
}
234-
} else {
235-
this.set_applet_label("");
236-
this.set_applet_tooltip("all BT devices has been disabled");
237-
this.set_applet_icon_name("bluetooth-disabled");
238359
}
360+
361+
this.notify_if_needed();
362+
},
363+
364+
show_hide_text_icon: function(name, kind, perc) {
365+
this.set_applet_label(perc.toString() + "%");
366+
this.set_applet_icon_name(this.get_device_batt_icon(kind, perc));
367+
this.set_applet_tooltip(name.toString());
368+
369+
if (this.applet_icon == "text") {
370+
this.hide_applet_icon();
371+
} else if (this.applet_icon == "icon") {
372+
this.hide_applet_label(true);
373+
}
374+
},
375+
376+
get_lowest_battery_device: function() {
377+
if (this.monitored_devs.length == 0) {
378+
return null;
379+
}
380+
381+
let lowest_bat_dev_ident = null;
382+
let lowest_bat_dev = null;
383+
384+
this.monitored_devs.forEach((name) => {
385+
if (!this.dbus_map.has(name)) {
386+
return;
387+
}
388+
389+
const dev = this.dbus_map.get(name).device;
390+
if (lowest_bat_dev == null || dev.percentage < lowest_bat_dev.percentage) {
391+
lowest_bat_dev = dev;
392+
lowest_bat_dev_ident = name;
393+
}
394+
});
395+
396+
return [lowest_bat_dev_ident, lowest_bat_dev.kind, lowest_bat_dev.percentage];
239397
},
240398

241399
get_device_batt_icon: function(type, batt) {
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
22
"uuid": "bluetooth-battery@zamszowy",
3-
"name": "Device battery",
3+
"name": "Bluetooth battery",
44
"description": "Shows battery levels of different devices"
55
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
#!/usr/bin/gjs
2+
3+
imports.gi.versions.Gtk = "3.0";
4+
const Gtk = imports.gi.Gtk;
5+
6+
Gtk.init(null);
7+
8+
let win = new Gtk.Window({title:""});
9+
10+
let box = new Gtk.Box({orientation: Gtk.Orientation.HORIZONTAL});
11+
win.add(box);
12+
13+
let label = new Gtk.Label({"label":" Device: "});
14+
box.add(label);
15+
16+
let cbox = new Gtk.ComboBoxText();
17+
for (let i = 0; i < (ARGV.length > 1 ? ARGV.length - 1 : ARGV.length); i+=1) {
18+
cbox.append("text", ARGV[i]);
19+
}
20+
// last param is active index
21+
cbox.set_active(ARGV.length > 1 ? parseInt(ARGV[ARGV.length - 1], 10) : 0);
22+
box.add(cbox);
23+
24+
let butOK = new Gtk.Button({"label":"OK"});
25+
butOK.connect("clicked", function() {print(cbox.get_active_text()); Gtk.main_quit()});
26+
box.add(butOK);
27+
28+
win.connect("delete-event", () => Gtk.main_quit());
29+
win.connect("key-press-event", function(unused, event) {
30+
let [ok, key] = event.get_keycode();
31+
/* ESC */
32+
if (key == 9) {
33+
Gtk.main_quit();
34+
}});
35+
36+
win.show_all();
37+
Gtk.main();

0 commit comments

Comments
 (0)