diff --git a/bin/.battery-warn b/bin/.battery-warn
new file mode 100755
index 0000000..6a22f57
--- /dev/null
+++ b/bin/.battery-warn
@@ -0,0 +1,197 @@
+#!/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
+ ])
diff --git a/readme.md b/readme.md
index 56daa0f..28b7c9b 100644
--- a/readme.md
+++ b/readme.md
@@ -43,3 +43,6 @@
- `hoyocloud-chromium-userscript.js`顾名思义,用于**在 Chrome 里**(firefox 不需要)游玩米哈游云游戏的油猴脚本。
参见 [Bilibili 专栏](https://www.bilibili.com/opus/842314310196658193)。
+
+- `bin/.battery-warn`虽说也是自用,但一是配置项并不算敏感,稍微改改`config`段也可以泛用;二是参考文献写得有点啰嗦,我懒得再缝第二遍。
+ 个人建议用于**定时任务(cron 或 systemd timer)**。