#!/usr/bin/env python
# SPDX-License-Identifier: CC0-1.0
# Copyright © 2021 mpan; <https://mpan.pl/>; CC0 1.0 (THIS SCRIPT!)
# Arch 论坛贴: <https://bbs.archlinux.org/viewtopic.php?id=269453>
# UPower 实现: https://github.com/wogscpar/upower-python

# -*- encoding: utf-8 -*-
# @File   : .battery-warn
# @Time   : 2025/12/29 00:32:53
# @Author : SilverAg.L

# 搁 bash 折腾管道还是太原始了。
# 包依赖 (pacman): python-dbus, libnotify, pipewire-audio

import dbus

from os import getenv
from subprocess import run as start_process

# region config
# 哪怕设备名也不稳定。pipewire 滚了几轮，桌面扬声器居然不见了。
# AUD_OUT_EMB = (  # my laptop embedded speaker
#     "alsa_output.pci-0000_00_1f"
#     ".3-platform-skl_hda_dsp_generic.HiFi__Speaker__sink"
# )
# 环境变量展开这一块。同时也是为了查找起来方便。毕竟已经有 gtklock 这个例外了。
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
        ])
