1#!/usr/local/bin/python3.8
2
3from gi.repository import Gio, GObject, CScreensaver, GLib
4from enum import IntEnum
5
6from dbusdepot.baseClient import BaseClient
7
8class DeviceType(IntEnum):
9    Unknown = 0
10    LinePower = 1
11    Battery = 2
12    Ups = 3
13    Monitor = 4
14    Mouse = 5
15    Keyboard = 6
16    Pda = 7
17    Phone = 8
18
19class DeviceState(IntEnum):
20    Unknown = 0
21    Charging = 1
22    Discharging = 2
23    Empty = 3
24    FullyCharged = 4
25    PendingCharge = 5
26    PendingDischarge = 6
27
28class UPowerClient(BaseClient):
29    """
30    This client communicates with the upower provider, tracking the power
31    state of the system (laptops - are we on battery or plugged in?)
32    """
33    __gsignals__ = {
34        'power-state-changed': (GObject.SignalFlags.RUN_LAST, None, ()),
35        'percentage-changed': (GObject.SignalFlags.RUN_LAST, None, (GObject.Object,))
36    }
37
38    UPOWER_SERVICE = "org.freedesktop.UPower"
39    UPOWER_PATH    = "/org/freedesktop/UPower"
40
41    def __init__(self):
42        super(UPowerClient, self).__init__(Gio.BusType.SYSTEM,
43                                           CScreensaver.UPowerProxy,
44                                           self.UPOWER_SERVICE,
45                                           self.UPOWER_PATH)
46
47        self.have_battery = False
48        self.plugged_in = False
49
50        self.update_state_id = 0
51        self.devices_dirty = True
52
53        self.relevant_devices = []
54
55    def on_client_setup_complete(self):
56        self.proxy.connect("device-removed", self.on_device_added_or_removed)
57        self.proxy.connect("device-added", self.on_device_added_or_removed)
58        self.proxy.connect("notify::on-battery", self.on_battery_changed)
59
60        self.queue_update_state()
61
62    def on_device_added_or_removed(self, proxy, path):
63        self.devices_dirty = True
64        self.queue_update_state()
65
66    def on_battery_changed(self, proxy, pspec, data=None):
67        self.queue_update_state()
68
69    def queue_update_state(self):
70        if self.update_state_id > 0:
71            GObject.source_remove(self.update_state_id)
72            self.update_state_id = 0
73
74        GObject.idle_add(self.idle_update_cb)
75
76    def idle_update_cb(self, data=None):
77        if self.devices_dirty:
78            self.rescan_devices()
79
80        self.devices_dirty = False
81
82        if self.update_state():
83            self.emit_changed()
84
85    def rescan_devices(self):
86        if len(self.relevant_devices) > 0:
87            for path, dev in self.relevant_devices:
88                dev.disconnect(dev.prop_changed_id)
89                del dev
90                del path
91
92        self.relevant_devices = []
93
94        try:
95            # The return type for this call has to be overridden in gdbus-codegen
96            # (See the Makefile.am) - or else we get utf-8 errors (python3 issue?)
97            for path in self.proxy.call_enumerate_devices_sync():
98                try:
99                    dev = CScreensaver.UPowerDeviceProxy.new_for_bus_sync(Gio.BusType.SYSTEM,
100                                                                          Gio.DBusProxyFlags.NONE,
101                                                                          self.UPOWER_SERVICE,
102                                                                          path,
103                                                                          None)
104
105                    if dev.get_property("type") in (DeviceType.Battery, DeviceType.LinePower):
106                        self.relevant_devices.append((path, dev))
107                        dev.prop_changed_id = dev.connect("notify", self.on_device_properties_changed)
108                except GLib.Error:
109                    print("UPowerClient had trouble connecting with device:", path, " - skipping it")
110        except GLib.Error:
111            print("UPowerClient had trouble enumerating through devices.  The battery indicator will be disabled")
112
113        self.queue_update_state()
114
115    def update_state(self):
116        changes = False
117
118        old_plugged_in = self.plugged_in
119        old_have_battery = self.have_battery
120
121        # UPower doesn't necessarily have a LinePower device if there are no batteries.
122        # Default to plugged in, then.
123        new_plugged_in = True
124        new_have_battery = False
125
126        for path, dev in self.relevant_devices:
127            if dev.get_property("type") == DeviceType.LinePower:
128                new_plugged_in = dev.get_property("online")
129            if dev.get_property("type") == DeviceType.Battery:
130                new_have_battery = True
131
132        if (new_plugged_in != old_plugged_in) or (new_have_battery != old_have_battery):
133            changes = True
134            self.have_battery = new_have_battery
135            self.plugged_in = new_plugged_in
136
137        return changes
138
139    def on_device_properties_changed(self, proxy, pspec, data=None):
140        if pspec.name in ("online", "icon-name", "state"):
141            self.queue_update_state()
142
143        if pspec.name == "percentage":
144            self.emit_percentage_changed(proxy)
145
146    def emit_changed(self):
147        self.emit("power-state-changed")
148
149    def emit_percentage_changed(self, battery):
150        self.emit("percentage-changed", battery)
151
152    def get_batteries(self):
153        if len(self.relevant_devices) == 0:
154            return []
155
156        ret = []
157
158        for path, dev in self.relevant_devices:
159            if dev.get_property("type") == DeviceType.Battery:
160                ret.append((path, dev))
161
162        return ret
163
164    def full_and_on_ac_or_no_batteries(self):
165        """
166        This figures out whether the power widget should be shown or not -
167        currently we only show the widget if we have batteries and are not
168        plugged in.
169        """
170        batteries = self.get_batteries()
171
172        if batteries == []:
173            return True
174
175        all_batteries_full = True
176
177        for path, dev in batteries:
178            if dev.get_property("state") not in (DeviceState.FullyCharged, DeviceState.Unknown):
179                all_batteries_full = False
180                break
181
182        return self.plugged_in and all_batteries_full
183
184    def on_failure(self, *args):
185        print("Failed to establish a connection with UPower - the battery indicator will be disabled.")
186