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 • 6.07 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()
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()
86
87
self.update_batteries()
88
GLib.timeout_add_seconds(10, self.update_batteries)
89
90
def update_batteries(self):
91
self.popover_content.remove_all()
92
93
total_energy = max_energy = 0
94
any_charging = False
95
96
for device_path in self.upower.EnumerateDevices():
97
device = self.bus.get("org.freedesktop.UPower", device_path)
98
99
# Filter only batteries
100
if device.Type == 2:
101
# This is in Wh but we don't care
102
total_energy += device.Energy
103
max_energy += device.EnergyFull
104
105
if device.State == 1:
106
any_charging = True
107
108
if not max_energy:
109
return False
110
111
print(total_energy, max_energy)
112
113
self.button.set_tooltip_text(f"{locale.format_string("%d", total_energy / max_energy * 1000)}‰")
114
115
self.icon.set_from_icon_name(get_battery_icon(total_energy / max_energy, any_charging))
116
117
GLib.idle_add(self.do_notifications, total_energy, max_energy)
118
119
return True
120
121
def do_notifications(self, total_energy, max_energy):
122
if self.session_bus.get("org.freedesktop.Notifications"):
123
self.notification_service = self.session_bus.get("org.freedesktop.Notifications")
124
else:
125
return
126
127
# This uses threading to make sure it doesn't hang while waiting to deliver the
128
# notification
129
if total_energy / max_energy < self.low_threshold:
130
if not self.low_notification:
131
def send_notification():
132
self.low_notification = self.notification_service.Notify(
133
_("Battery"),
134
self.low_notification or self.critical_notification or 0,
135
"battery-low",
136
_("The energy in the battery is low."),
137
"",
138
[],
139
{},
140
-1
141
)
142
threading.Thread(target=send_notification, daemon=True).start()
143
144
elif self.low_notification:
145
threading.Thread(
146
target=lambda: self.notification_service.CloseNotification(self.low_notification),
147
daemon=True
148
).start()
149
150
self.low_notification = None
151
152
if total_energy / max_energy < self.critical_threshold:
153
if not self.critical_notification:
154
def send_notification():
155
self.critical_notification = self.notification_service.Notify(
156
_("Battery"),
157
self.low_notification or self.critical_notification or 0,
158
"battery-empty",
159
_("The energy in the battery is very low; the device will shut down soon."),
160
"",
161
[],
162
{},
163
-1
164
)
165
threading.Thread(target=send_notification, daemon=True).start()
166
elif self.critical_notification:
167
threading.Thread(
168
target=lambda: self.notification_service.CloseNotification(self.critical_notification),
169
daemon=True
170
).start()
171
172
self.critical_notification = None
173
174
def get_config(self):
175
return {
176
"icon_size": self.icon.get_pixel_size(),
177
"low_threshold": self.low_threshold,
178
"critical_threshold": self.critical_threshold,
179
}
180
181
def shutdown(self, app):
182
return
183