From 8683a7db3b7ab441ebddb5e037079b24d9cce85d Mon Sep 17 00:00:00 2001 From: "SilverAg.L" Date: Mon, 29 Dec 2025 02:40:36 +0800 Subject: [PATCH] battery notify script the script, originally written in bash, is rewrited into python, whose UPower implements (from `wogscpar/upower-python`) make me lazy to check out again. --- bin/.battery-warn | 197 ++++++++++++++++++++++++++++++++++++++++++++++ readme.md | 3 + 2 files changed, 200 insertions(+) create mode 100755 bin/.battery-warn 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)**。