File indexing completed on 2024-06-23 05:30:43

0001 #!/usr/bin/env python3
0002 
0003 # SPDX-FileCopyrightText: 2023 Fushan Wen <qydwhotmail@gmail.com>
0004 # SPDX-License-Identifier: GPL-2.0-or-later
0005 
0006 # pylint: disable=too-many-arguments
0007 
0008 # For FreeBSD CI which only has python 3.9
0009 from __future__ import annotations
0010 
0011 import os
0012 import sys
0013 import threading
0014 import time
0015 from io import TextIOWrapper
0016 from typing import Any, Final
0017 
0018 sys.path.append(os.path.dirname(os.path.abspath(__file__)))
0019 
0020 from gi.repository import Gio, GLib
0021 from GLibMainLoopThread import GLibMainLoopThread
0022 
0023 
0024 def skip_doc(file_handler: TextIOWrapper) -> str:
0025     """
0026     Gio.DBusNodeInfo.new_for_xml doesn't like doc
0027     """
0028     introspection_xml: str = ""
0029     skip_line: bool = False
0030     for l in file_handler.readlines():
0031         if not skip_line:
0032             if "<doc:doc>" in l:
0033                 skip_line = "</doc:doc>" not in l  # In case they are in the same line
0034                 continue
0035             else:
0036                 introspection_xml += l + "\n"
0037         else:
0038             if "</doc:doc>" in l:
0039                 skip_line = False
0040     return introspection_xml
0041 
0042 
0043 class OrgFreedesktopUPower:
0044     """
0045     D-Bus interfaces for org.freedesktop.UPower and org.freedesktop.UPower.Device
0046     """
0047 
0048     BUS_NAME: Final = "org.freedesktop.UPower"
0049     OBJECT_PATH: Final = "/org/freedesktop/UPower"
0050     DEVICE_IFACE_NAME: Final = "org.freedesktop.UPower.Device"
0051     BATTERY0_OBJECT_PATH: Final = "/org/freedesktop/UPower/devices/battery_BAT0"
0052     BATTERY1_OBJECT_PATH: Final = "/org/freedesktop/UPower/devices/battery_BAT1"
0053     WIRELESS_MOUSE_OBJECT_PATH: Final = "/org/freedesktop/UPower/devices/mouse_hidpp_battery_0"
0054     WIRELESS_KEYBOARD_OBJECT_PATH: Final = "/org/freedesktop/UPower/devices/keyboard_hidpp_battery_1"
0055     AC_OBJECT_PATH: Final = "/org/freedesktop/UPower/devices/line_power_AC"
0056     DISPLAY_DEVICE_OBJECT_PATH: Final = "/org/freedesktop/UPower/devices/DisplayDevice"
0057 
0058     __connection: Gio.DBusConnection
0059 
0060     def __init__(self, device_properties: dict[str, dict[str, GLib.Variant]] | None = None, enable_display_device: bool = True) -> None:
0061         self.__upower_reg_id: int = 0
0062         self.__device_reg_id_map: dict[str, int] = {}  # object_path: reg_id
0063         self.__display_device_reg_id: int = 0
0064 
0065         self.upower_properties: dict[str, GLib.Variant] = {
0066             "DaemonVersion": GLib.Variant("s", "1.90.2"),
0067             "OnBattery": GLib.Variant("b", False),
0068         }
0069 
0070         if device_properties:
0071             self.device_properties = device_properties.copy()
0072         else:
0073             self.device_properties: dict[str, dict[str, GLib.Variant]] = {
0074                 self.BATTERY0_OBJECT_PATH: {
0075                     "NativePath": GLib.Variant("s", "BAT0"),
0076                     "Vendor": GLib.Variant("s", "Konqi"),
0077                     "Model": GLib.Variant("s", "Primary"),
0078                     "Serial": GLib.Variant("s", "00150 2020/04/05"),
0079                     "UpdateTime": GLib.Variant('t', int(time.time())),
0080                     "Type": GLib.Variant("u", 2),  # Battery
0081                     "PowerSupply": GLib.Variant("b", True),
0082                     "HasHistory": GLib.Variant("b", False),
0083                     "HasStatistics": GLib.Variant("b", False),
0084                     "Online": GLib.Variant("b", False),  # only valid for AC
0085                     "Energy": GLib.Variant("d", 20.0),
0086                     "EnergyEmpty": GLib.Variant("d", 0.0),
0087                     "EnergyFull": GLib.Variant("d", 40.0),  # Wh
0088                     "EnergyFullDesign": GLib.Variant("d", 100.0),
0089                     "EnergyRate": GLib.Variant("d", -20.0),  # Charging
0090                     "Voltage": GLib.Variant("d", 12.184),
0091                     "ChargeCycles": GLib.Variant('i', 88),
0092                     "Luminosity": GLib.Variant("d", 0.0),
0093                     "TimeToEmpty": GLib.Variant("x", 0),
0094                     "TimeToFull": GLib.Variant("x", 0),
0095                     "Percentage": GLib.Variant("d", 50),
0096                     "Temperature": GLib.Variant("d", 20.0),
0097                     "IsPresent": GLib.Variant("b", True),
0098                     "State": GLib.Variant("u", 1),  # Charging
0099                     "IsRechargeable": GLib.Variant("b", True),
0100                     "Capacity": GLib.Variant("d", 40.0),
0101                     "Technology": GLib.Variant("u", 1),  # Lithium ion
0102                     "BatteryLevel": GLib.Variant("u", 1),  # None (the battery does not use a coarse level of battery reporting)
0103                     "WarningLevel": GLib.Variant("u", 1),  # None
0104                     "IconName": GLib.Variant("s", ""),
0105                 },
0106                 self.BATTERY1_OBJECT_PATH: {
0107                     "NativePath": GLib.Variant("s", "BAT1"),
0108                     "Vendor": GLib.Variant("s", "Katie"),
0109                     "Model": GLib.Variant("s", "Secondary"),
0110                     "Serial": GLib.Variant("s", "00150 2020/04/05"),
0111                     "UpdateTime": GLib.Variant('t', int(time.time())),
0112                     "Type": GLib.Variant("u", 2),  # Battery
0113                     "PowerSupply": GLib.Variant("b", True),
0114                     "HasHistory": GLib.Variant("b", False),
0115                     "HasStatistics": GLib.Variant("b", False),
0116                     "Online": GLib.Variant("b", False),  # only valid for AC
0117                     "Energy": GLib.Variant("d", 40.0),
0118                     "EnergyEmpty": GLib.Variant("d", 0.0),
0119                     "EnergyFull": GLib.Variant("d", 40.0),
0120                     "EnergyFullDesign": GLib.Variant("d", 100.0),
0121                     "EnergyRate": GLib.Variant("d", 0.0),  # Not Charging
0122                     "Voltage": GLib.Variant("d", 12.184),
0123                     "ChargeCycles": GLib.Variant('i', 88),
0124                     "Luminosity": GLib.Variant("d", 0.0),
0125                     "TimeToEmpty": GLib.Variant("x", 0),
0126                     "TimeToFull": GLib.Variant("x", 0),
0127                     "Percentage": GLib.Variant("d", 100),
0128                     "Temperature": GLib.Variant("d", 30.0),
0129                     "IsPresent": GLib.Variant("b", True),
0130                     "State": GLib.Variant("u", 4),  # Fully Charged
0131                     "IsRechargeable": GLib.Variant("b", True),
0132                     "Capacity": GLib.Variant("d", 60.0),
0133                     "Technology": GLib.Variant("u", 1),  # Lithium ion
0134                     "BatteryLevel": GLib.Variant("u", 1),  # None (the battery does not use a coarse level of battery reporting)
0135                     "WarningLevel": GLib.Variant("u", 1),  # None
0136                     "IconName": GLib.Variant("s", ""),
0137                 },
0138                 self.AC_OBJECT_PATH: {
0139                     "NativePath": GLib.Variant("s", "AC"),
0140                     "Vendor": GLib.Variant("s", ""),
0141                     "Model": GLib.Variant("s", ""),
0142                     "Serial": GLib.Variant("s", ""),
0143                     "UpdateTime": GLib.Variant('t', int(time.time())),
0144                     "Type": GLib.Variant("u", 1),  # Line Power
0145                     "PowerSupply": GLib.Variant("b", True),
0146                     "HasHistory": GLib.Variant("b", False),
0147                     "HasStatistics": GLib.Variant("b", False),
0148                     "Online": GLib.Variant("b", True),
0149                     "Energy": GLib.Variant("d", 0.0),  # only valid for batteries
0150                     "EnergyEmpty": GLib.Variant("d", 0.0),  # only valid for batteries
0151                     "EnergyFull": GLib.Variant("d", 0.0),  # only valid for batteries
0152                     "EnergyFullDesign": GLib.Variant("d", 0.0),  # only valid for batteries
0153                     "EnergyRate": GLib.Variant("d", 0.0),  # only valid for batteries
0154                     "Voltage": GLib.Variant("d", 0.0),
0155                     "ChargeCycles": GLib.Variant('i', -1),  # only valid for batteries
0156                     "Luminosity": GLib.Variant("d", 0.0),
0157                     "TimeToEmpty": GLib.Variant("x", 0),  # only valid for batteries
0158                     "TimeToFull": GLib.Variant("x", 0),  # only valid for batteries
0159                     "Percentage": GLib.Variant("d", 0.0),  # only valid for batteries
0160                     "Temperature": GLib.Variant("d", 0.0),  # only valid for batteries
0161                     "IsPresent": GLib.Variant("b", False),  # only valid for batteries
0162                     "State": GLib.Variant("u", 0),  # Unknown
0163                     "IsRechargeable": GLib.Variant("b", False),
0164                     "Capacity": GLib.Variant("d", 0.0),  # only valid for batteries
0165                     "Technology": GLib.Variant("u", 0),  # Unknown, only valid for batteries
0166                     "WarningLevel": GLib.Variant("u", 1),  # None
0167                     "BatteryLevel": GLib.Variant("u", 1),  # None
0168                     "IconName": GLib.Variant("s", ""),
0169                 },
0170                 self.WIRELESS_MOUSE_OBJECT_PATH: {
0171                     "NativePath": GLib.Variant("s", "hidpp_battery_0"),
0172                     "Vendor": GLib.Variant("s", "KDE"),
0173                     "Model": GLib.Variant("s", "Gaming Mouse"),
0174                     "Serial": GLib.Variant("s", "1234-5678-90"),
0175                     "UpdateTime": GLib.Variant('t', int(time.time())),
0176                     "Type": GLib.Variant("u", 5),  # Mouse
0177                     "PowerSupply": GLib.Variant("b", False),
0178                     "HasHistory": GLib.Variant("b", False),
0179                     "HasStatistics": GLib.Variant("b", False),
0180                     "Online": GLib.Variant("b", False),  # only valid for AC
0181                     "Energy": GLib.Variant("d", 0.0),  # only valid for batteries
0182                     "EnergyEmpty": GLib.Variant("d", 0.0),  # only valid for batteries
0183                     "EnergyFull": GLib.Variant("d", 0.0),  # only valid for batteries
0184                     "EnergyFullDesign": GLib.Variant("d", 0.0),  # only valid for batteries
0185                     "EnergyRate": GLib.Variant("d", 0.0),  # only valid for batteries
0186                     "Voltage": GLib.Variant("d", 0.0),
0187                     "ChargeCycles": GLib.Variant('i', -1),  # only valid for batteries
0188                     "Luminosity": GLib.Variant("d", 0.0),
0189                     "TimeToEmpty": GLib.Variant("x", 0),  # only valid for batteries
0190                     "TimeToFull": GLib.Variant("x", 0),  # only valid for batteries
0191                     "Percentage": GLib.Variant("d", 100.0),  # only valid for batteries
0192                     "Temperature": GLib.Variant("d", 0.0),  # only valid for batteries
0193                     "IsPresent": GLib.Variant("b", False),  # only valid for batteries
0194                     "State": GLib.Variant("u", 0),  # Unknown, only valid for batteries
0195                     "IsRechargeable": GLib.Variant("b", False),  # only valid for batteries
0196                     "Capacity": GLib.Variant("d", 0.0),  # only valid for batteries
0197                     "Technology": GLib.Variant("u", 0),  # Unknown, only valid for batteries
0198                     "WarningLevel": GLib.Variant("u", 1),  # None
0199                     "BatteryLevel": GLib.Variant("u", 8),  # Full
0200                     "IconName": GLib.Variant("s", ""),
0201                 },
0202             }
0203         self.device_object_paths: GLib.Variant = GLib.Variant("(ao)", [list(self.device_properties.keys())])
0204 
0205         self.display_device_properties: dict[str, dict[str, GLib.Variant]] = {}
0206         self.__enable_display_device: bool = enable_display_device
0207         self.update_display_device_properties(self.BATTERY0_OBJECT_PATH)
0208 
0209         self.is_online: bool = False
0210         self.registered_event = threading.Event()
0211 
0212         self.__owner_id: int = Gio.bus_own_name(Gio.BusType.SYSTEM, self.BUS_NAME, Gio.BusNameOwnerFlags.NONE, self.on_bus_acquired, None, None)
0213         assert self.__owner_id > 0
0214 
0215     def quit(self) -> None:
0216         if self.__enable_display_device:
0217             self.__connection.unregister_object(self.__display_device_reg_id)
0218             self.__display_device_reg_id = 0
0219         [self.__connection.unregister_object(self.__device_reg_id_map[object_path]) for object_path in self.__device_reg_id_map]
0220         self.__device_reg_id_map = {}
0221         self.__connection.unregister_object(self.__upower_reg_id)
0222         self.__upower_reg_id = 0
0223         Gio.bus_unown_name(self.__owner_id)
0224         self.__connection.flush_sync(None)  # Otherwise flaky
0225 
0226     def set_upower_property(self, property_name: str, value: GLib.Variant) -> None:
0227         self.upower_properties[property_name] = value
0228         changed_properties = GLib.Variant("a{sv}", {
0229             property_name: self.upower_properties[property_name],
0230         })
0231         Gio.DBusConnection.emit_signal(self.__connection, None, self.OBJECT_PATH, "org.freedesktop.DBus.Properties", "PropertiesChanged", GLib.Variant.new_tuple(GLib.Variant("s", self.BUS_NAME), changed_properties, GLib.Variant("as", ())))
0232 
0233     def add_device(self, object_path: str, properties: dict[str, GLib.Variant]) -> None:
0234         """
0235         Helper function to add a device to upower with the given object path and properties
0236         """
0237         assert object_path not in self.device_properties
0238         assert object_path not in self.__device_reg_id_map
0239         self.device_properties[object_path] = properties
0240         with open("../applets/batterymonitor/dbus/org.freedesktop.UPower.Device.xml", encoding="utf-8") as file_handler:
0241             introspection_data = Gio.DBusNodeInfo.new_for_xml(skip_doc(file_handler))
0242             reg_id: int = self.__connection.register_object(object_path, introspection_data.interfaces[0], self.device_handle_method_call, self.device_handle_get_property, self.device_handle_set_property)
0243             assert reg_id > 0
0244             self.__device_reg_id_map[object_path] = reg_id
0245 
0246         Gio.DBusConnection.emit_signal(self.__connection, None, self.OBJECT_PATH, "org.freedesktop.UPower", "DeviceAdded", GLib.Variant("(o)", [object_path]))
0247 
0248     def remove_device(self, object_path: str) -> None:
0249         """
0250         Helper function to remove a device from upower with the given object path
0251         """
0252         assert object_path in self.device_properties
0253         assert object_path in self.__device_reg_id_map
0254         Gio.DBusConnection.emit_signal(self.__connection, None, self.OBJECT_PATH, "org.freedesktop.UPower", "DeviceRemoved", GLib.Variant("(o)", [object_path]))
0255         self.__connection.unregister_object(self.__device_reg_id_map[object_path])
0256         del self.__device_reg_id_map[object_path]
0257         del self.device_properties[object_path]
0258 
0259     def set_device_property(self, object_path: str, property_name: str, value: GLib.Variant, update_time: int | None = None) -> None:
0260         assert object_path != self.DISPLAY_DEVICE_OBJECT_PATH
0261         if update_time:
0262             self.device_properties[object_path]["UpdateTime"] = GLib.Variant('t', update_time)
0263         else:
0264             self.device_properties[object_path]["UpdateTime"] = GLib.Variant('t', int(time.time()))
0265 
0266         self.device_properties[object_path][property_name] = value
0267         changed_properties = GLib.Variant("a{sv}", {
0268             property_name: self.device_properties[object_path][property_name],
0269             "UpdateTime": self.device_properties[object_path]["UpdateTime"],
0270         })
0271         Gio.DBusConnection.emit_signal(self.__connection, None, object_path, "org.freedesktop.DBus.Properties", "PropertiesChanged", GLib.Variant.new_tuple(GLib.Variant("s", self.DEVICE_IFACE_NAME), changed_properties, GLib.Variant("as", ())))
0272 
0273         if self.__enable_display_device:
0274             self.update_display_device_properties(self.BATTERY0_OBJECT_PATH)
0275             changed_properties: dict[str, GLib.Variant] = {
0276                 property_name: self.display_device_properties[property_name],
0277                 "UpdateTime": self.display_device_properties["UpdateTime"],
0278             }
0279             if property_name == "IsPresent":
0280                 changed_properties["Energy"] = self.display_device_properties["Energy"]
0281                 changed_properties["EnergyEmpty"] = self.display_device_properties["EnergyEmpty"]
0282                 changed_properties["EnergyFull"] = self.display_device_properties["EnergyFull"]
0283                 changed_properties["EnergyFullDesign"] = self.display_device_properties["EnergyFullDesign"]
0284                 changed_properties["EnergyRate"] = self.display_device_properties["EnergyRate"]
0285                 changed_properties["TimeToEmpty"] = self.display_device_properties["TimeToEmpty"]
0286                 changed_properties["TimeToFull"] = self.display_device_properties["TimeToFull"]
0287                 changed_properties["Percentage"] = self.display_device_properties["Percentage"]
0288                 changed_properties["Capacity"] = self.display_device_properties["Capacity"]
0289 
0290             Gio.DBusConnection.emit_signal(self.__connection, None, self.DISPLAY_DEVICE_OBJECT_PATH, "org.freedesktop.DBus.Properties", "PropertiesChanged", GLib.Variant.new_tuple(GLib.Variant("s", self.DEVICE_IFACE_NAME), GLib.Variant("a{sv}", changed_properties), GLib.Variant("as", ())))
0291 
0292     def update_display_device_properties(self, default_object_path: str) -> None:
0293         if not self.__enable_display_device:
0294             return
0295 
0296         def condition(e: str) -> bool:
0297             return self.device_properties[e]["PowerSupply"].get_boolean() and self.device_properties[e]["IsPresent"].get_boolean()
0298 
0299         self.display_device_properties = {
0300             "NativePath": GLib.Variant("s", ""),
0301             "Vendor": GLib.Variant("s", ""),
0302             "Model": GLib.Variant("s", ""),
0303             "Serial": GLib.Variant("s", ""),
0304             "UpdateTime": GLib.Variant('t', self.device_properties[default_object_path]["UpdateTime"].get_uint64() if default_object_path in self.device_properties else int(time.time())),
0305             "Type": GLib.Variant("u", 2),  # Battery
0306             "PowerSupply": GLib.Variant("b", True),
0307             "HasHistory": GLib.Variant("b", self.device_properties[default_object_path]["HasHistory"].get_boolean() if default_object_path in self.device_properties else False),
0308             "HasStatistics": GLib.Variant("b", self.device_properties[default_object_path]["HasStatistics"].get_boolean() if default_object_path in self.device_properties else False),
0309             "Online": GLib.Variant("b", self.device_properties[default_object_path]["Online"].get_boolean() if default_object_path in self.device_properties else False),
0310             "Energy": GLib.Variant("d", sum([self.device_properties[e]["Energy"].get_double() for e in self.device_properties if condition(e)])),
0311             "EnergyEmpty": GLib.Variant("d", sum([self.device_properties[e]["EnergyEmpty"].get_double() for e in self.device_properties if condition(e)])),
0312             "EnergyFull": GLib.Variant("d", sum([self.device_properties[e]["EnergyFull"].get_double() for e in self.device_properties if condition(e)])),  # Wh
0313             "EnergyFullDesign": GLib.Variant("d", sum([self.device_properties[e]["EnergyFullDesign"].get_double() for e in self.device_properties if condition(e)])),
0314             "EnergyRate": GLib.Variant("d", sum([self.device_properties[e]["EnergyRate"].get_double() for e in self.device_properties if condition(e)])),
0315             "Voltage": GLib.Variant("d", self.device_properties[default_object_path]["Voltage"].get_double() if default_object_path in self.device_properties else 0.0),
0316             "ChargeCycles": GLib.Variant('i', self.device_properties[default_object_path]["ChargeCycles"].get_int32() if default_object_path in self.device_properties else 0),
0317             "Luminosity": GLib.Variant("d", self.device_properties[default_object_path]["Luminosity"].get_double() if default_object_path in self.device_properties else 0.0),
0318             "TimeToEmpty": GLib.Variant("x", sum([self.device_properties[e]["TimeToEmpty"].get_int64() for e in self.device_properties if condition(e)])),
0319             "TimeToFull": GLib.Variant("x", sum([self.device_properties[e]["TimeToFull"].get_int64() for e in self.device_properties if condition(e)])),
0320             "Percentage": GLib.Variant("d", 100 * sum([self.device_properties[e]["Energy"].get_double() for e in self.device_properties if condition(e)]) / sum([self.device_properties[e]["EnergyFull"].get_double() for e in self.device_properties if condition(e)])),
0321             "Temperature": GLib.Variant("d", self.device_properties[default_object_path]["Temperature"].get_double() if default_object_path in self.device_properties else 20.0),
0322             "IsPresent": GLib.Variant("b", self.device_properties[default_object_path]["IsPresent"].get_boolean() if default_object_path in self.device_properties else True),
0323             "State": GLib.Variant("u", self.device_properties[default_object_path]["State"].get_uint32() if default_object_path in self.device_properties else 1),  # Charging
0324             "IsRechargeable": GLib.Variant("b", self.device_properties[default_object_path]["IsRechargeable"].get_boolean() if default_object_path in self.device_properties else True),
0325             "Capacity": GLib.Variant("d", sum([self.device_properties[e]["Capacity"].get_double() for e in self.device_properties if condition(e)])),
0326             "Technology": GLib.Variant("u", self.device_properties[default_object_path]["Technology"].get_uint32() if default_object_path in self.device_properties else 1),  # Lithium ion
0327             "BatteryLevel": GLib.Variant("u", self.device_properties[default_object_path]["BatteryLevel"].get_uint32() if default_object_path in self.device_properties else 1),  # None (the battery does not use a coarse level of battery reporting)
0328             "WarningLevel": GLib.Variant("u", self.device_properties[default_object_path]["WarningLevel"].get_uint32() if default_object_path in self.device_properties else 1),  # None
0329             "IconName": GLib.Variant("s", self.device_properties[default_object_path]["IconName"].get_string() if default_object_path in self.device_properties else ""),
0330         }
0331 
0332     def set_energy_props(self, object_path: str, energy_rate: float | None = None, percentage: float | None = None, update_time: int | None = None) -> None:
0333         """
0334         Helper function to update energy rate and remaining time.
0335         @param percentage 0-100
0336         """
0337         changed_properties: dict[str, GLib.Variant] = {}
0338         if energy_rate is not None:
0339             self.device_properties[object_path]["EnergyRate"] = GLib.Variant("d", energy_rate)
0340             changed_properties["EnergyRate"] = self.device_properties[object_path]["EnergyRate"]
0341         else:
0342             energy_rate = self.device_properties[object_path]["EnergyRate"].get_double()
0343 
0344         if update_time is not None:
0345             self.device_properties[object_path]["UpdateTime"] = GLib.Variant('t', update_time)
0346             self.device_properties[self.BATTERY0_OBJECT_PATH]["UpdateTime"] = self.device_properties[object_path]["UpdateTime"]
0347         else:
0348             self.device_properties[object_path]["UpdateTime"] = GLib.Variant('t', self.device_properties[self.BATTERY0_OBJECT_PATH]["UpdateTime"].get_uint64() + 100)
0349             self.device_properties[self.BATTERY0_OBJECT_PATH]["UpdateTime"] = self.device_properties[object_path]["UpdateTime"]
0350         changed_properties["UpdateTime"] = self.device_properties[object_path]["UpdateTime"]
0351 
0352         if percentage is not None:
0353             self.device_properties[object_path]["Percentage"] = GLib.Variant("d", percentage)
0354             self.device_properties[object_path]["Energy"] = GLib.Variant("d", percentage / 100 * self.device_properties[object_path]["EnergyFull"].get_double())
0355             changed_properties["Percentage"] = self.device_properties[object_path]["Percentage"]
0356             changed_properties["Energy"] = self.device_properties[object_path]["Energy"]
0357             if percentage == 100:
0358                 self.device_properties[object_path]["State"] = GLib.Variant("u", 4)  # Charging
0359                 changed_properties["State"] = self.device_properties[object_path]["State"]
0360         else:
0361             percentage = self.device_properties[object_path]["Percentage"].get_double()
0362 
0363         if energy_rate < 0:
0364             self.device_properties[object_path]["TimeToEmpty"] = GLib.Variant("x", 0)
0365             self.device_properties[object_path]["TimeToFull"] = GLib.Variant("x", int((100 - percentage) / 100 * self.device_properties[object_path]["EnergyFull"].get_double() / -energy_rate * 3600))  # seconds
0366             self.device_properties[object_path]["State"] = GLib.Variant("u", 1)  # Charging
0367             changed_properties["State"] = self.device_properties[object_path]["State"]
0368         elif energy_rate > 0:
0369             self.device_properties[object_path]["TimeToEmpty"] = GLib.Variant("x", int(self.device_properties[object_path]["Energy"].get_double() / energy_rate * 3600))  # seconds
0370             self.device_properties[object_path]["TimeToFull"] = GLib.Variant("x", 0)
0371             self.device_properties[object_path]["State"] = GLib.Variant("u", 2)  # Discharging
0372             changed_properties["State"] = self.device_properties[object_path]["State"]
0373         else:
0374             self.device_properties[object_path]["TimeToEmpty"] = GLib.Variant("x", 0)
0375             self.device_properties[object_path]["TimeToFull"] = GLib.Variant("x", 0)
0376 
0377         changed_properties["TimeToEmpty"] = self.device_properties[object_path]["TimeToEmpty"]
0378         changed_properties["TimeToFull"] = self.device_properties[object_path]["TimeToFull"]
0379 
0380         Gio.DBusConnection.emit_signal(self.__connection, None, object_path, "org.freedesktop.DBus.Properties", "PropertiesChanged", GLib.Variant.new_tuple(GLib.Variant("s", self.DEVICE_IFACE_NAME), GLib.Variant("a{sv}", changed_properties), GLib.Variant("as", ())))
0381 
0382         if self.__enable_display_device:
0383             self.update_display_device_properties(self.BATTERY0_OBJECT_PATH)
0384             changed_properties = GLib.Variant("a{sv}", {
0385                 "EnergyRate": self.display_device_properties["EnergyRate"],
0386                 "Percentage": self.display_device_properties["Percentage"],
0387                 "Energy": self.display_device_properties["Energy"],
0388                 "State": self.display_device_properties["State"],
0389                 "TimeToFull": self.display_device_properties["TimeToFull"],
0390                 "TimeToEmpty": self.display_device_properties["TimeToEmpty"],
0391                 "UpdateTime": self.display_device_properties["UpdateTime"],
0392             })
0393             Gio.DBusConnection.emit_signal(self.__connection, None, self.DISPLAY_DEVICE_OBJECT_PATH, "org.freedesktop.DBus.Properties", "PropertiesChanged", GLib.Variant.new_tuple(GLib.Variant("s", self.DEVICE_IFACE_NAME), changed_properties, GLib.Variant("as", ())))
0394 
0395     def set_ac_plugged(self) -> None:
0396         """
0397         Helper function to simulate plugging in the AC line
0398         """
0399         self.device_properties[self.AC_OBJECT_PATH]["Online"] = GLib.Variant("b", True)
0400         self.device_properties[self.AC_OBJECT_PATH]["IsPresent"] = GLib.Variant("b", True)
0401         ac_changed_properties = GLib.Variant("a{sv}", {
0402             "Online": self.device_properties[self.AC_OBJECT_PATH]["Online"],
0403             "IsPresent": self.device_properties[self.AC_OBJECT_PATH]["IsPresent"],
0404         })
0405         Gio.DBusConnection.emit_signal(self.__connection, None, self.AC_OBJECT_PATH, "org.freedesktop.DBus.Properties", "PropertiesChanged", GLib.Variant.new_tuple(GLib.Variant("s", self.DEVICE_IFACE_NAME), ac_changed_properties, GLib.Variant("as", ())))
0406 
0407         self.set_energy_props(self.BATTERY0_OBJECT_PATH, -20.0, 90.0)
0408         self.set_upower_property("OnBattery", GLib.Variant("b", False))
0409 
0410     def set_ac_unplugged(self) -> None:
0411         """
0412         Helper function to simulate unplugging the AC line
0413         """
0414         self.device_properties[self.AC_OBJECT_PATH]["Online"] = GLib.Variant("b", False)
0415         self.device_properties[self.AC_OBJECT_PATH]["IsPresent"] = GLib.Variant("b", False)
0416         ac_changed_properties = GLib.Variant("a{sv}", {
0417             "Online": self.device_properties[self.AC_OBJECT_PATH]["Online"],
0418             "IsPresent": self.device_properties[self.AC_OBJECT_PATH]["IsPresent"],
0419         })
0420 
0421         self.set_energy_props(self.BATTERY0_OBJECT_PATH, 20.0, 80.0)
0422         self.set_upower_property("OnBattery", GLib.Variant("b", True))
0423 
0424     def on_bus_acquired(self, connection: Gio.DBusConnection, name: str, *args) -> None:
0425         """
0426         Interface is ready, now register objects.
0427         """
0428         self.__connection = connection
0429 
0430         with open("../applets/batterymonitor/dbus/org.freedesktop.UPower.xml", encoding="utf-8") as file_handler:
0431             introspection_data = Gio.DBusNodeInfo.new_for_xml(skip_doc(file_handler))
0432             self.__upower_reg_id = connection.register_object(self.OBJECT_PATH, introspection_data.interfaces[0], self.upower_handle_method_call, self.upower_handle_get_property, self.upower_handle_set_property)
0433             assert self.__upower_reg_id > 0
0434 
0435         assert len(self.__device_reg_id_map) == 0
0436         with open("../applets/batterymonitor/dbus/org.freedesktop.UPower.Device.xml", encoding="utf-8") as file_handler:
0437             introspection_data = Gio.DBusNodeInfo.new_for_xml(skip_doc(file_handler))
0438             for object_path in self.device_properties:
0439                 reg_id: int = connection.register_object(object_path, introspection_data.interfaces[0], self.device_handle_method_call, self.device_handle_get_property, self.device_handle_set_property)
0440                 assert reg_id > 0
0441                 self.__device_reg_id_map[object_path] = reg_id
0442             if self.__enable_display_device:
0443                 self.__display_device_reg_id = connection.register_object(self.DISPLAY_DEVICE_OBJECT_PATH, introspection_data.interfaces[0], self.device_handle_method_call, self.device_handle_get_property, self.device_handle_set_property)
0444                 assert self.__display_device_reg_id > 0
0445 
0446         self.is_online = True
0447         self.registered_event.set()
0448 
0449     def upower_handle_method_call(self, connection: Gio.DBusConnection, sender: str, object_path: str, interface_name: str, method_name: str, parameters: GLib.Variant, invocation: Gio.DBusMethodInvocation) -> None:
0450         """
0451         Handles method calls for org.freedesktop.UPower
0452         """
0453         assert interface_name == self.BUS_NAME, f"Unknown interface {interface_name}"
0454         print(f"upower call {method_name}", file=sys.stderr, flush=True)
0455 
0456         if method_name == "EnumerateDevices":
0457             invocation.return_value(self.device_object_paths)
0458         elif method_name == "GetDisplayDevice":
0459             invocation.return_value(GLib.Variant("(o)", ["/org/freedesktop/UPower/devices/DisplayDevice"] if self.__enable_display_device else ["/"]))
0460         elif method_name == "GetCriticalAction":
0461             invocation.return_value(GLib.Variant("(s)", ["PowerOff"]))
0462         else:
0463             assert False, f"Unknown method {method_name}"
0464 
0465     def upower_handle_get_property(self, connection: Gio.DBusConnection, sender: str, object_path: str, interface_name: str, value: Any):
0466         """
0467         Handles properties for org.freedesktop.UPower
0468         """
0469         if value not in self.upower_properties:
0470             print(f"{value} does not exist", file=sys.stderr, flush=True)
0471             return None
0472 
0473         print(f"upower get_property {value}")
0474         return self.upower_properties[value]
0475 
0476     def upower_handle_set_property(self, connection: Gio.DBusConnection, sender: str, object_path: str, interface_name: str, key: str, value: Any) -> bool:
0477         """
0478         Handles properties for org.freedesktop.UPower
0479         """
0480         assert False, "Only read-only properties"
0481 
0482     def device_handle_method_call(self, connection: Gio.DBusConnection, sender: str, object_path: str, interface_name: str, method_name: str, parameters: GLib.Variant, invocation: Gio.DBusMethodInvocation) -> None:
0483         """
0484         Handles method calls for org.freedesktop.UPower.Device
0485         """
0486         assert interface_name == self.DEVICE_IFACE_NAME, f"Unknown interface {interface_name}"
0487         assert method_name in ("Refresh", "GetHistory", "GetStatistics"), f"Unknown method {method_name}"
0488         print(f"device {object_path} call {method_name}", file=sys.stderr, flush=True)
0489 
0490     def device_handle_get_property(self, connection: Gio.DBusConnection, sender: str, object_path: str, interface_name: str, value: Any):
0491         """
0492         Handles properties for org.freedesktop.UPower.Device
0493         """
0494         assert interface_name == self.DEVICE_IFACE_NAME, f"Wrong interface name {interface_name} from {sender}"
0495         assert object_path in self.device_properties or (object_path == self.DISPLAY_DEVICE_OBJECT_PATH and self.__enable_display_device), f"Unknown object path {object_path}"
0496         print(f"device {object_path} get_property {value}")
0497 
0498         return self.device_properties[object_path][value] if object_path != self.DISPLAY_DEVICE_OBJECT_PATH else self.display_device_properties[value]
0499 
0500     def device_handle_set_property(self, connection: Gio.DBusConnection, sender: str, object_path: str, interface_name: str, key: str, value: Any) -> bool:
0501         """
0502         Handles properties for org.freedesktop.UPower.Device
0503         """
0504         assert False, "Only read-only properties"