Skip to content

Commit 1a2c7f6

Browse files
authored
bluetooth-battery@zamszowy: initial release of 'bluetooth-battery@zamszowy' applet (#4324)
Add new applet for displaying bluetooth (and other) devices battery levels.
1 parent 49df4d0 commit 1a2c7f6

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+3032
-0
lines changed

bluetooth-battery@zamszowy/LICENSE

+674
Large diffs are not rendered by default.

bluetooth-battery@zamszowy/README.md

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Bluetooth (and other) devices battery monitor
2+
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.\
5+
Device with lowest battery is displayed in panel. When clicked, it displays list with all monitored devices.
6+
7+
## Settings
8+
You can disable monitoring (it also disables blacklist configuration) of keyboards, mice, headphones or all other devices.\
9+
You can also choose to display in applet icon only, text only or icon and text.
10+
11+
## Blacklist
12+
You can blacklist any device detected by the applet.\
13+
All detected devices are automatically added to blacklist (common devices like mice and keyboards are added just as comments, other are disabled right away).
14+
15+
## Bluetooth Headphones
16+
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).\
18+
This could be somehow circumvented by enabling only one experimental UUID, but it's still not guaranteed to be bug free - expect issues!
19+
20+
## Icons
21+
Icons are based on [Papirus icon theme](https://github.com/PapirusDevelopmentTeam/papirus-icon-theme).
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
const Applet = imports.ui.applet;
2+
const PopupMenu = imports.ui.popupMenu;
3+
const St = imports.gi.St;
4+
const Settings = imports.ui.settings;
5+
const Util = imports.misc.util;
6+
const Lang = imports.lang;
7+
8+
const xml ='<node>\
9+
<interface name="org.freedesktop.UPower.Device">\
10+
<property name="Type" type="u" access="read" />\
11+
<property name="State" type="u" access="read" />\
12+
<property name="Percentage" type="d" access="read" />\
13+
<property name="IsPresent" type="b" access="read" />\
14+
<property name="IconName" type="s" access="read" />\
15+
</interface>\
16+
</node>';
17+
const {Gio, UPowerGlib: UPower} = imports.gi;
18+
const PowerManagerProxy = Gio.DBusProxy.makeProxyWrapper(xml);
19+
20+
var dbusCon;
21+
22+
function BtBattery(metadata, orientation, panel_height, instance_id) {
23+
this._init(metadata, orientation, panel_height, instance_id);
24+
}
25+
26+
BtBattery.prototype = {
27+
__proto__: Applet.TextIconApplet.prototype,
28+
29+
_init_dbus: function() {
30+
31+
let proxy = new PowerManagerProxy(Gio.DBus.system,
32+
'org.freedesktop.UPower',
33+
'/org/freedesktop/UPower',
34+
(proxy, error) => {
35+
if (error) {
36+
return;
37+
}
38+
}
39+
);
40+
dbusCon = proxy.get_connection();
41+
this.dbusAdd = dbusCon.signal_subscribe('org.freedesktop.UPower', 'org.freedesktop.UPower', 'DeviceAdded', null, null, 0, () => {
42+
this.setup();
43+
});
44+
this.dbusRemove = dbusCon.signal_subscribe('org.freedesktop.UPower', 'org.freedesktop.UPower', 'DeviceRemoved', null, null, 0, () => {
45+
this.setup();
46+
});
47+
48+
this.dbus_map = new Map();
49+
},
50+
51+
_init: function(metadata, orientation, panel_height, instance_id) {
52+
this.applet_path = metadata.path;
53+
54+
Applet.TextIconApplet.prototype._init.call(this, orientation, panel_height, instance_id);
55+
56+
this.set_applet_icon_name("battery-100");
57+
this.set_applet_tooltip(_("Show devices battery levels"));
58+
59+
this.menuManager = new PopupMenu.PopupMenuManager(this);
60+
this.menu = new Applet.AppletPopupMenu(this, orientation);
61+
this.menuManager.addMenu(this.menu);
62+
63+
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);
70+
71+
this._init_dbus();
72+
73+
this.setup();
74+
},
75+
76+
_on_settings_change: function() {
77+
this.setup();
78+
},
79+
80+
on_applet_clicked: function() {
81+
this.setup();
82+
if (this.added_count > 0) {
83+
this.menu.toggle();
84+
}
85+
},
86+
87+
setup: function() {
88+
this.menu.removeAll();
89+
this._contentSection = new PopupMenu.PopupMenuSection();
90+
this.menu.addMenuItem(this._contentSection);
91+
92+
this.setup_dbus();
93+
},
94+
95+
blacklist_add: function(dev, active) {
96+
if ((!dev.serial || !this.blacklist.includes(dev.serial)) && (!dev.model || !this.blacklist.includes(dev.model))) {
97+
this.blacklist += (this.blacklist ? "\n" : "") + (active ? "" : "# ") + (dev.model ? dev.model : dev.serial);
98+
}
99+
},
100+
101+
blacklist_containes_active: function(dev) {
102+
var containes = false;
103+
const arr = this.blacklist.split("\n");
104+
for (var i = 0; i < arr.length; i++) {
105+
if (arr[i].startsWith("#")) {
106+
continue;
107+
} else if ((dev.serial && arr[i] == dev.serial) || (dev.model && arr[i] == dev.model)) {
108+
containes = true;
109+
break;
110+
}
111+
}
112+
113+
return containes;
114+
},
115+
116+
setup_dbus: function() {
117+
for (let [ident, props] of this.dbus_map) {
118+
this.dbus_map.set(ident, {device: props.device, proxy: props.proxy, updated: false});
119+
}
120+
121+
var upowerClient = UPower.Client.new_full(null);
122+
var devices = upowerClient.get_devices();
123+
for (let i=0; i < devices.length; i++) {
124+
let dev = devices[i]
125+
if (dev.kind != UPower.DeviceKind.MOUSE
126+
&& dev.kind != UPower.DeviceKind.KEYBOARD
127+
&& dev.kind != UPower.DeviceKind.GAMING_INPUT
128+
&& dev.kind != UPower.DeviceKind.PHONE
129+
&& dev.kind != UPower.DeviceKind.HEADPHONES
130+
&& dev.kind != UPower.DeviceKind.MEDIA_PLAYER) {
131+
132+
if (dev.model || dev.serial) {
133+
// blacklist by default non mouse/kb/phone/gaming input/mediaplayer devices
134+
this.blacklist_add(dev, true);
135+
} else {
136+
// skip entirely devices without model and serial
137+
continue;
138+
}
139+
}
140+
141+
if (this.blacklist_containes_active(dev)) {
142+
continue;
143+
} else {
144+
this.blacklist_add(dev, false);
145+
}
146+
147+
const dev_ident = dev.model ? dev.model : dev.serial;
148+
if (this.dbus_map.has(dev_ident)) {
149+
let d = this.dbus_map.get(dev_ident);
150+
this.dbus_map.set(dev_ident, {device: d.device, proxy: d.proxy, updated: true});
151+
} else {
152+
try {
153+
let proxy = new PowerManagerProxy(Gio.DBus.system, 'org.freedesktop.UPower',
154+
dev.get_object_path(),
155+
(proxy, error) => {
156+
if (error) {
157+
return;
158+
}
159+
proxy.connect('g-properties-changed', this.setup.bind(this));
160+
}
161+
);
162+
this.dbus_map.set(dev_ident, {device: dev, proxy: proxy, updated: true});
163+
} catch (err) {
164+
continue;
165+
}
166+
}
167+
}
168+
169+
for (let [ident, props] of this.dbus_map) {
170+
if (!props.updated) {
171+
this.dbus_map.delete(ident);
172+
}
173+
}
174+
175+
if (this.dbus_map.size == 0) {
176+
this.set_applet_enabled(false);
177+
return;
178+
} else {
179+
this.set_applet_enabled(true);
180+
}
181+
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+
187+
for (let [ident, props] of this.dbus_map) {
188+
let dev = props.device;
189+
dev.refresh_sync(null);
190+
191+
if (dev.battery_level != UPower.DeviceLevel.NONE) {
192+
continue;
193+
}
194+
195+
const name = ident;
196+
const perc = dev.percentage;
197+
const type = dev.kind;
198+
199+
if ((type == UPower.DeviceKind.KEYBOARD && !this.enable_keyboards)
200+
|| (type == UPower.DeviceKind.MOUSE && !this.enable_mice)
201+
|| (type == UPower.DeviceKind.HEADPHONES && !this.enable_headphones)
202+
|| (type != UPower.DeviceKind.KEYBOARD && type != UPower.DeviceKind.MOUSE && type != UPower.DeviceKind.HEADPHONES && !this.enable_others))
203+
{
204+
continue;
205+
}
206+
207+
let ii = new PopupMenu.PopupIconMenuItem(name, this.get_device_batt_icon(type, perc), St.IconType.FULLCOLOR);
208+
let perc_label = new St.Label({text: perc + "%"});
209+
ii.connect('activate', Lang.bind(this, function() { Util.spawn_async(['/usr/bin/cjs', this.applet_path + '/dev-info-window.js', ident, dev.to_text()]); }));
210+
ii.addActor(perc_label, {align: St.Align.END});
211+
this.menu.addMenuItem(ii);
212+
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;
220+
}
221+
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());
226+
227+
if (this.applet_icon == "text") {
228+
this.hide_applet_icon();
229+
} else if (this.applet_icon == "icon") {
230+
this.hide_applet_label(true);
231+
} else {
232+
233+
}
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");
238+
}
239+
},
240+
241+
get_device_batt_icon: function(type, batt) {
242+
if (type == UPower.DeviceKind.KEYBOARD) {
243+
return "keyboard-" + this.perc_to_3_digit_str(this.perc_round_to_10(batt));
244+
} else if (type == UPower.DeviceKind.MOUSE) {
245+
return "mouse-" + this.perc_to_3_digit_str(this.perc_round_to_10(batt));
246+
} else if (type == UPower.DeviceKind.HEADPHONES) {
247+
return "headphones-" + this.perc_to_3_digit_str(this.perc_round_to_10(batt));
248+
} else {
249+
return "battery-" + this.perc_to_3_digit_str(this.perc_round_to_10(batt));
250+
}
251+
},
252+
253+
perc_round_to_20: function(perc) {
254+
const perc_rounded = Math.floor(perc / 10) * 10;
255+
256+
if (perc_rounded % 20 == 0) {
257+
return perc_rounded;
258+
}
259+
260+
const up_dist = Math.abs((perc_rounded + (perc_rounded != 100 ? 10 : 0)) - perc)
261+
const down_dist = Math.abs(perc - (perc_rounded - (perc_rounded != 0 ? 10 : 0)))
262+
263+
let plus_step = 0;
264+
if (down_dist < up_dist) {
265+
plus_step = -10;
266+
} else {
267+
plus_step = 10;
268+
}
269+
270+
return Math.max(Math.min(perc_rounded + plus_step, 100), 0);
271+
},
272+
273+
perc_round_to_10: function(perc) {
274+
return Math.round(perc / 10) * 10;
275+
},
276+
277+
perc_to_3_digit_str: function(perc) {
278+
let str = "";
279+
if (perc == 100) {
280+
str = "100";
281+
} else if (perc < 10) {
282+
str = "00" + perc.toString();
283+
} else {
284+
str = "0" + perc.toString();
285+
}
286+
287+
return str;
288+
},
289+
};
290+
291+
292+
function main(metadata, orientation, panel_height, instance_id) {
293+
return new BtBattery(metadata, orientation, panel_height, instance_id);
294+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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:ARGV[0]});
9+
10+
let scroll = new Gtk.ScrolledWindow();
11+
scroll.set_size_request(640, 480);
12+
13+
let textview = new Gtk.TextView();
14+
textview.set_editable(false);
15+
textview.buffer.text = ARGV[1]
16+
17+
scroll.add(textview);
18+
19+
win.add(scroll);
20+
21+
win.connect("delete-event", () => Gtk.main_quit());
22+
win.connect("key-press-event", function(unused, event) {
23+
let [ok, key] = event.get_keycode();
24+
/* ESC */
25+
if (key == 9) {
26+
Gtk.main_quit();
27+
}});
28+
29+
win.show_all();
30+
Gtk.main();
Loading
Loading
Loading
Loading
Loading
Loading

0 commit comments

Comments
 (0)