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.38 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
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
64
def __init__(self, orientation=Gtk.Orientation.HORIZONTAL, config=None):
65
super().__init__(orientation=orientation, config=config)
66
if config is None:
67
config = {}
68
69
self.bus = SystemBus()
70
self.session_bus = SessionBus()
71
self.notification_service = None
72
self.upower = self.bus.get("org.freedesktop.UPower")
73
74
self.button = Gtk.MenuButton(has_frame=False)
75
self.icon = Gtk.Image(pixel_size=config.get("icon_size", 24))
76
self.button.set_child(self.icon)
77
self.append(self.button)
78
79
self.low_threshold = config.get("low_threshold", 15/100)
80
self.low_notification = None
81
self.critical_threshold = config.get("critical_threshold", 5/100)
82
self.critical_notification = None
83
84
self.popover = Gtk.Popover()
85
self.popover_content = Gtk.ListBox(selection_mode=Gtk.SelectionMode.NONE)
86
self.popover_content.set_size_request(320, -1)
87
self.popover.set_child(self.popover_content)
88
89
self.button.set_popover(self.popover)
90
91
self.update_batteries()
92
GLib.timeout_add_seconds(config.get("refresh_interval", 10), self.update_batteries)
93
94
def update_batteries(self):
95
self.popover_content.remove_all()
96
97
total_energy = max_energy = 0
98
any_charging = False
99
100
for device_path in self.upower.EnumerateDevices():
101
device = self.bus.get("org.freedesktop.UPower", device_path)
102
103
# Filter only batteries
104
if device.Type == 2:
105
# This is in Wh but we don't care
106
total_energy += device.Energy
107
max_energy += device.EnergyFull
108
109
if device.State == 1:
110
any_charging = True
111
112
box = Gtk.Box.new(Gtk.Orientation.VERTICAL, 8)
113
charge_fraction = device.Energy / device.EnergyFull
114
meter = Gtk.ProgressBar(show_text=True, fraction=charge_fraction)
115
box.append(meter)
116
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)))
117
charge_label.set_halign(Gtk.Align.CENTER)
118
charge_label.set_xalign(0.5)
119
box.append(charge_label)
120
name_label = Gtk.Label.new(device.Vendor + " " + device.Model)
121
name_label.set_halign(Gtk.Align.CENTER)
122
name_label.set_xalign(0.5)
123
box.prepend(name_label)
124
box.set_margin_start(8)
125
box.set_margin_end(8)
126
box.set_margin_top(8)
127
box.set_margin_bottom(8)
128
row = Gtk.ListBoxRow(child=box, activatable=False)
129
self.popover_content.append(row)
130
131
if not max_energy:
132
return False
133
134
self.button.set_tooltip_text(f"{locale.format_string("%d", total_energy / max_energy * 1000)}‰")
135
136
self.icon.set_from_icon_name(get_battery_icon(total_energy / max_energy, any_charging))
137
138
GLib.idle_add(self.do_notifications, total_energy, max_energy, any_charging)
139
140
return True
141
142
def do_notifications(self, total_energy, max_energy, any_charging):
143
if self.session_bus.get("org.freedesktop.Notifications"):
144
self.notification_service = self.session_bus.get("org.freedesktop.Notifications")
145
else:
146
return
147
148
# This uses threading to make sure it doesn't hang while waiting to deliver the
149
# notification
150
if not any_charging and total_energy / max_energy < self.low_threshold:
151
if not self.low_notification:
152
def send_notification():
153
self.low_notification = self.notification_service.Notify(
154
_("Battery"),
155
self.low_notification or self.critical_notification or 0,
156
"battery-low",
157
_("The energy in the battery is low."),
158
"",
159
[],
160
{},
161
-1
162
)
163
threading.Thread(target=send_notification, daemon=True).start()
164
165
elif self.low_notification:
166
threading.Thread(
167
target=lambda: self.notification_service.CloseNotification(self.low_notification),
168
daemon=True
169
).start()
170
171
self.low_notification = None
172
173
if not any_charging and total_energy / max_energy < self.critical_threshold:
174
if not self.critical_notification:
175
def send_notification():
176
self.critical_notification = self.notification_service.Notify(
177
_("Battery"),
178
self.low_notification or self.critical_notification or 0,
179
"battery-empty",
180
_("The energy in the battery is very low; the device will shut down soon."),
181
"",
182
[],
183
{},
184
-1
185
)
186
threading.Thread(target=send_notification, daemon=True).start()
187
elif self.critical_notification:
188
threading.Thread(
189
target=lambda: self.notification_service.CloseNotification(self.critical_notification),
190
daemon=True
191
).start()
192
193
self.critical_notification = None
194
195
def get_config(self):
196
return {
197
"icon_size": self.icon.get_pixel_size(),
198
"low_threshold": self.low_threshold,
199
"critical_threshold": self.critical_threshold,
200
}
201
202
def shutdown(self, app):
203
return
204