#!/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 __battery(self, battery):
battery_proxy = self.bus.get_object(self.UPOWER_NAME, battery)
return dbus.Interface(battery_proxy, 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_percentage(self, battery):
return self.__battery(battery).Get(
self.UPOWER_NAME + ".Device", "Percentage")
def get_full_device_information(self, battery):
def get_property(prop_name):
return self.__battery(battery).Get(
self.UPOWER_NAME + ".Device", prop_name)
return {
'HasHistory': get_property("HasHistory"),
'HasStatistics': get_property("HasStatistics"),
'IsPresent': get_property("IsPresent"),
'IsRechargeable': get_property("IsRechargeable"),
'Online': get_property("Online"),
'PowerSupply': get_property("PowerSupply"),
'Capacity': get_property("Capacity"),
'Energy': get_property("Energy"),
'EnergyEmpty': get_property("EnergyEmpty"),
'EnergyFull': get_property("EnergyFull"),
'EnergyFullDesign': get_property("EnergyFullDesign"),
'EnergyRate': get_property("EnergyRate"),
'Luminosity': get_property("Luminosity"),
'Percentage': get_property("Percentage"),
'Temperature': get_property("Temperature"),
'Voltage': get_property("Voltage"),
'TimeToEmpty': get_property("TimeToEmpty"),
'TimeToFull': get_property("TimeToFull"),
'IconName': get_property("IconName"),
'Model': get_property("Model"),
'NativePath': get_property("NativePath"),
'Serial': get_property("Serial"),
'Vendor': get_property("Vendor"),
'State': get_property("State"),
'Technology': get_property("Technology"),
'Type': get_property("Type"),
'WarningLevel': get_property("WarningLevel"),
'UpdateTime': get_property("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):
state = int(self.__battery(battery).Get(
self.UPOWER_NAME + ".Device", "State"))
return state == 1
def get_state(self, battery):
state = int(self.__battery(battery).Get(
self.UPOWER_NAME + ".Device", "State"))
return {
0: "Unknown",
1: "Loading",
2: "Discharging",
3: "Empty",
4: "Fully charged",
5: "Pending charge",
6: "Pending discharge"
}.get(state, "Unknown")
def push_notification(title, message, timeout=10000):
BUS_NAME = "org.freedesktop.Notifications"
OBJECT_PATH = "/org/freedesktop/Notifications"
INTERFACE = BUS_NAME
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)
devPaths = upowr.detect_devices()
low_power_detected = False
for devPath in devPaths:
info = upowr.get_full_device_information(devPath)
if info['Type'] != 2: # 2 means battery
continue
# seems waybar visual is 1% lower than actual.
if info['Percentage'] < 21:
push_notification(
"Power Hint",
f"{info['NativePath']} is running out. Recharge soon!"
)
low_power_detected = True
if low_power_detected:
start_process([
"pw-play",
f"--target={AUD_OUT_EMB}",
AUD_FILE
])