By using this site, you agree to have cookies stored on your device, strictly for functional purposes, such as storing your session and preferences.

Dismiss

 __init__.py

View raw Download
text/plain • 7.42 kiB
Python script, Unicode text, UTF-8 text executable
        
            
1
"""
2
D-Bus laptop battery monitor applet for the Panorama panel.
3
Copyright 2025, roundabout-host.com <vlad@roundabout-host.com>
4
5
This program is free software: you can redistribute it and/or modify
6
it under the terms of the GNU General Public Licence as published by
7
the Free Software Foundation, either version 3 of the Licence, or
8
(at your option) any later version.
9
10
This program is distributed in the hope that it will be useful,
11
but WITHOUT ANY WARRANTY; without even the implied warranty of
12
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
GNU General Public Licence for more details.
14
15
You should have received a copy of the GNU General Public Licence
16
along with this program. If not, see <https://www.gnu.org/licenses/>.
17
"""
18
19
from __future__ import annotations
20
21
import threading
22
23
import panorama_panel
24
import locale
25
from pathlib import Path
26
from pydbus import SystemBus, SessionBus
27
28
import gi
29
gi.require_version("Gtk", "4.0")
30
31
from gi.repository import Gtk, Gdk, GLib, Gio
32
33
34
module_directory = Path(__file__).resolve().parent
35
36
locale.bindtextdomain("panorama-battery-monitor", module_directory / "locale")
37
_ = lambda x: locale.dgettext("panorama-battery-monitor", x)
38
39
40
def get_battery_icon(fraction, charging):
41
if fraction < 1/32:
42
base_icon = "battery-empty"
43
elif fraction < 1/16:
44
base_icon = "battery-caution"
45
elif fraction < 1/4:
46
base_icon = "battery-low"
47
elif fraction < 1/2:
48
base_icon = "battery-medium"
49
elif fraction < 7/8:
50
base_icon = "battery-good"
51
else:
52
base_icon = "battery-full"
53
54
if charging:
55
return base_icon + "-charging"
56
57
return base_icon
58
59
60
class BatteryMonitor(panorama_panel.Applet):
61
name = _("Laptop battery")
62
description = _("Check laptop battery charge")
63
icon = Gio.ThemedIcon.new("battery")
64
65
def __init__(self, orientation=Gtk.Orientation.HORIZONTAL, config=None):
66
super().__init__(orientation=orientation, config=config)
67
if config is None:
68
config = {}
69
70
self.bus = SystemBus()
71
self.session_bus = SessionBus()
72
self.notification_service = None
73
self.upower = self.bus.get("org.freedesktop.UPower")
74
75
self.button = Gtk.MenuButton(has_frame=False)
76
self.icon = Gtk.Image(pixel_size=config.get("icon_size", 24))
77
self.button.set_child(self.icon)
78
self.append(self.button)
79
80
self.low_threshold = config.get("low_threshold", 15/100)
81
self.low_notification = None
82
self.critical_threshold = config.get("critical_threshold", 5/100)
83
self.critical_notification = None
84
85
self.popover = Gtk.Popover()
86
self.popover_content = Gtk.ListBox(selection_mode=Gtk.SelectionMode.NONE)
87
self.popover_content.set_size_request(320, -1)
88
self.popover.set_child(self.popover_content)
89
90
self.button.set_popover(self.popover)
91
92
self.update_batteries()
93
GLib.timeout_add_seconds(config.get("refresh_interval", 10), self.update_batteries)
94
95
def update_batteries(self):
96
self.popover_content.remove_all()
97
98
total_energy = max_energy = 0
99
any_charging = False
100
101
for device_path in self.upower.EnumerateDevices():
102
device = self.bus.get("org.freedesktop.UPower", device_path)
103
104
# Filter only batteries
105
if device.Type == 2:
106
# This is in Wh but we don't care
107
total_energy += device.Energy
108
max_energy += device.EnergyFull
109
110
if device.State == 1:
111
any_charging = True
112
113
box = Gtk.Box.new(Gtk.Orientation.VERTICAL, 8)
114
charge_fraction = device.Energy / device.EnergyFull
115
meter = Gtk.ProgressBar(show_text=True, fraction=charge_fraction)
116
box.append(meter)
117
charge_label = Gtk.Label.new(_("{current} Wh of {full} Wh").format(current=locale.format_string("%.1f", device.Energy), full=locale.format_string("%.1f", device.EnergyFull)))
118
charge_label.set_halign(Gtk.Align.CENTER)
119
charge_label.set_xalign(0.5)
120
box.append(charge_label)
121
name_label = Gtk.Label.new(device.Vendor + " " + device.Model)
122
name_label.set_halign(Gtk.Align.CENTER)
123
name_label.set_xalign(0.5)
124
box.prepend(name_label)
125
box.set_margin_start(8)
126
box.set_margin_end(8)
127
box.set_margin_top(8)
128
box.set_margin_bottom(8)
129
row = Gtk.ListBoxRow(child=box, activatable=False)
130
self.popover_content.append(row)
131
132
if not max_energy:
133
return False
134
135
self.button.set_tooltip_text(f"{locale.format_string("%d", total_energy / max_energy * 1000)}‰")
136
137
self.icon.set_from_icon_name(get_battery_icon(total_energy / max_energy, any_charging))
138
139
GLib.idle_add(self.do_notifications, total_energy, max_energy, any_charging)
140
141
return True
142
143
def do_notifications(self, total_energy, max_energy, any_charging):
144
if self.session_bus.get("org.freedesktop.Notifications"):
145
self.notification_service = self.session_bus.get("org.freedesktop.Notifications")
146
else:
147
return
148
149
# This uses threading to make sure it doesn't hang while waiting to deliver the
150
# notification
151
if not any_charging and total_energy / max_energy < self.low_threshold:
152
if not self.low_notification:
153
def send_notification():
154
self.low_notification = self.notification_service.Notify(
155
_("Battery"),
156
self.low_notification or self.critical_notification or 0,
157
"battery-low",
158
_("The energy in the battery is low."),
159
"",
160
[],
161
{},
162
-1
163
)
164
threading.Thread(target=send_notification, daemon=True).start()
165
166
elif self.low_notification:
167
threading.Thread(
168
target=lambda: self.notification_service.CloseNotification(self.low_notification),
169
daemon=True
170
).start()
171
172
self.low_notification = None
173
174
if not any_charging and total_energy / max_energy < self.critical_threshold:
175
if not self.critical_notification:
176
def send_notification():
177
self.critical_notification = self.notification_service.Notify(
178
_("Battery"),
179
self.low_notification or self.critical_notification or 0,
180
"battery-empty",
181
_("The energy in the battery is very low; the device will shut down soon."),
182
"",
183
[],
184
{},
185
-1
186
)
187
threading.Thread(target=send_notification, daemon=True).start()
188
elif self.critical_notification:
189
threading.Thread(
190
target=lambda: self.notification_service.CloseNotification(self.critical_notification),
191
daemon=True
192
).start()
193
194
self.critical_notification = None
195
196
def get_config(self):
197
return {
198
"icon_size": self.icon.get_pixel_size(),
199
"low_threshold": self.low_threshold,
200
"critical_threshold": self.critical_threshold,
201
}
202
203
def shutdown(self, app):
204
return
205