#!/usr/bin/env python # SPDX-License-Identifier: CC0-1.0 # Copyright © 2021 mpan; ; CC0 1.0 (THIS SCRIPT!) # Context: # Reference: https://github.com/wogscpar/upower-python # -*- encoding: utf-8 -*- # @File : .battery-warn # @Time : 2025/12/29 00:32:53 # @Author : SilverAg.L # bash pipeline was still too complicated. # Dependencies (pacman): python-dbus, libnotify, pipewire-audio import dbus from os import getenv from subprocess import run as start_process # region config AUD_OUT_EMB = ( # my laptop embedded speaker "alsa_output.pci-0000_00_1f" ".3-platform-skl_hda_dsp_generic.HiFi__Speaker__sink" ) # better searched by editor like VSCode. AUD_FILE = "~/.local/share/.low_power.wav".replace("~", getenv("HOME"), 1) # endregion config class UPowerManager(): def __init__(self): self.UPOWER_NAME = "org.freedesktop.UPower" self.UPOWER_PATH = "/org/freedesktop/UPower" self.DBUS_PROPERTIES = "org.freedesktop.DBus.Properties" self.bus = dbus.SystemBus() def __upower(self, subpath=None, *, interface=None): if not subpath: subpath = "" upower_proxy = self.bus.get_object( self.UPOWER_NAME, self.UPOWER_PATH + subpath ) if interface is None: interface = self.UPOWER_NAME return dbus.Interface(upower_proxy, interface) def __device(self, devpath): devproxy = self.bus.get_object(self.UPOWER_NAME, devpath) return dbus.Interface(devproxy, self.DBUS_PROPERTIES) def detect_devices(self): return self.__upower().EnumerateDevices() def get_display_device(self): return self.__upower().GetDisplayDevice() def get_critical_action(self): return self.__upower().GetCriticalAction() def get_device_info(self, dev, property): return self.__device(dev).Get( self.UPOWER_NAME + ".Device", property) def get_full_device_info(self, dev): return { 'HasHistory': self.get_device_info(dev, "HasHistory"), 'HasStatistics': self.get_device_info(dev, "HasStatistics"), 'IsPresent': self.get_device_info(dev, "IsPresent"), 'IsRechargeable': self.get_device_info(dev, "IsRechargeable"), 'Online': self.get_device_info(dev, "Online"), 'PowerSupply': self.get_device_info(dev, "PowerSupply"), 'Capacity': self.get_device_info(dev, "Capacity"), 'Energy': self.get_device_info(dev, "Energy"), 'EnergyEmpty': self.get_device_info(dev, "EnergyEmpty"), 'EnergyFull': self.get_device_info(dev, "EnergyFull"), 'EnergyFullDesign': self.get_device_info(dev, "EnergyFullDesign"), 'EnergyRate': self.get_device_info(dev, "EnergyRate"), 'Luminosity': self.get_device_info(dev, "Luminosity"), 'Percentage': self.get_device_info(dev, "Percentage"), 'Temperature': self.get_device_info(dev, "Temperature"), 'Voltage': self.get_device_info(dev, "Voltage"), 'TimeToEmpty': self.get_device_info(dev, "TimeToEmpty"), 'TimeToFull': self.get_device_info(dev, "TimeToFull"), 'IconName': self.get_device_info(dev, "IconName"), 'Model': self.get_device_info(dev, "Model"), 'NativePath': self.get_device_info(dev, "NativePath"), 'Serial': self.get_device_info(dev, "Serial"), 'Vendor': self.get_device_info(dev, "Vendor"), 'State': self.get_device_info(dev, "State"), 'Technology': self.get_device_info(dev, "Technology"), 'Type': self.get_device_info(dev, "Type"), 'WarningLevel': self.get_device_info(dev, "WarningLevel"), 'UpdateTime': self.get_device_info(dev, "UpdateTime") } def is_lid_present(self): return bool(self.__upower(interface=self.DBUS_PROPERTIES).Get( self.UPOWER_NAME, 'LidIsPresent')) def is_lid_closed(self): return bool(self.__upower(interface=self.DBUS_PROPERTIES).Get( self.UPOWER_NAME, 'LidIsClosed')) def on_battery(self): return bool(self.__upower(interface=self.DBUS_PROPERTIES).Get( self.UPOWER_NAME, 'OnBattery')) def has_wakeup_capabilities(self): return bool(self.__upower( "/Wakeups", interface=self.DBUS_PROPERTIES ).Get(self.UPOWER_NAME + '.Wakeups', 'HasCapability')) def get_wakeups_data(self): return self.__upower( "/Wakeups", interface=self.UPOWER_NAME + '.Wakeups' ).GetData() def get_wakeups_total(self): return self.__upower( "/Wakeups", interface=self.UPOWER_NAME + '.Wakeups' ).GetTotal() def is_loading(self, battery): return int(self.get_device_info(battery, "State")) == 1 def get_state(self, battery): return { 0: "Unknown", 1: "Loading", 2: "Discharging", 3: "Empty", 4: "Fully charged", 5: "Pending charge", 6: "Pending discharge" }.get(int(self.get_device_info(battery, "State")), "Unknown") def is_supplying_battery(self, battery): return ( int(self.get_device_info(battery, "Type")) == 2 and bool(self.get_device_info(battery, "PowerSupply")) ) def push_notification(title, message, timeout=10000): BUS_NAME = INTERFACE = "org.freedesktop.Notifications" OBJECT_PATH = "/org/freedesktop/Notifications" notify = dbus.Interface( dbus.SessionBus().get_object(BUS_NAME, OBJECT_PATH), INTERFACE ) notify.Notify( "battery-warn-script", # app_name 0, # replaces_id "", # app_icon title, # summary message, # body [], # actions {}, # hints timeout # expire_timeout ) if __name__ == "__main__": upowr = UPowerManager() if not upowr.on_battery(): exit(0) low_power_detected = False for device in upowr.detect_devices(): if not upowr.is_supplying_battery(device): continue # seems waybar visual is 1% lower than actual. if upowr.get_device_info(device, "Percentage") < 21: bat_id = upowr.get_device_info(device, "NativePath") push_notification( "Power Hint", f"{bat_id} is running out. Recharge soon!" ) low_power_detected = True if low_power_detected: start_process([ "pw-play", f"--target={AUD_OUT_EMB}", AUD_FILE ])