248 lines
8.3 KiB
Python
248 lines
8.3 KiB
Python
import psutil
|
|
from gi.repository import GLib
|
|
from pydbus import SystemBus
|
|
|
|
from fabric.core import Property, Service, Signal
|
|
|
|
DeviceState = {
|
|
0: "UNKNOWN",
|
|
1: "CHARGING",
|
|
2: "DISCHARGING",
|
|
3: "EMPTY",
|
|
4: "FULLY_CHARGED",
|
|
5: "PENDING_CHARGE",
|
|
6: "PENDING_DISCHARGE",
|
|
}
|
|
|
|
|
|
class Battery(Service):
|
|
@staticmethod
|
|
def seconds_to_hours_minutes(seconds):
|
|
hours = seconds // 3600
|
|
minutes = (seconds % 3600) // 60
|
|
return f"{hours}h {minutes}m" if hours else f"{minutes}m"
|
|
|
|
@staticmethod
|
|
def get_battery_icon_level(percentage):
|
|
"""Get battery icon level based on percentage"""
|
|
if percentage >= 90:
|
|
return "100"
|
|
elif percentage >= 80:
|
|
return "090"
|
|
elif percentage >= 70:
|
|
return "080"
|
|
elif percentage >= 60:
|
|
return "070"
|
|
elif percentage >= 50:
|
|
return "060"
|
|
elif percentage >= 40:
|
|
return "050"
|
|
elif percentage >= 30:
|
|
return "040"
|
|
elif percentage >= 20:
|
|
return "030"
|
|
elif percentage >= 10:
|
|
return "020"
|
|
else:
|
|
return "010"
|
|
|
|
@staticmethod
|
|
def get_battery_icon_file(percentage, is_charging, base_path=""):
|
|
"""Get battery icon file path"""
|
|
level = Battery.get_battery_icon_level(percentage)
|
|
suffix = "-charging" if is_charging else ""
|
|
return f"{base_path}battery/battery-{level}{suffix}.svg"
|
|
|
|
@staticmethod
|
|
def get_profile_display_name(profile: str) -> str:
|
|
"""Get user-friendly display name for power profile"""
|
|
profile_names = {
|
|
"power-saver": "Power Saver",
|
|
"powersave": "Power Saver",
|
|
"power_saver": "Power Saver",
|
|
"balanced": "Balanced",
|
|
"balance": "Balanced",
|
|
"performance": "Performance",
|
|
"performance-mode": "Performance"
|
|
}
|
|
return profile_names.get(profile, profile.title())
|
|
|
|
@Signal
|
|
def changed(self) -> None: ...
|
|
|
|
@Signal
|
|
def profile_changed(self, value: str) -> None: ...
|
|
|
|
@Property(int, "readable")
|
|
def percentage(self):
|
|
if self._use_psutil_fallback:
|
|
if self._psutil_battery:
|
|
return int(self._psutil_battery.percent)
|
|
return 0
|
|
return int(self._battery.Percentage)
|
|
|
|
@Property(str, "readable")
|
|
def temperature(self):
|
|
if self._use_psutil_fallback:
|
|
return "N/A" # psutil doesn't provide temperature
|
|
return (
|
|
f"{self._battery.Temperature}°C"
|
|
if hasattr(self._battery, "Temperature")
|
|
else "N/A"
|
|
)
|
|
|
|
@Property(str, "readable")
|
|
def time_to_empty(self):
|
|
if self._use_psutil_fallback:
|
|
if self._psutil_battery and hasattr(self._psutil_battery, "secsleft"):
|
|
return self.seconds_to_hours_minutes(self._psutil_battery.secsleft)
|
|
return "N/A"
|
|
return self.seconds_to_hours_minutes(getattr(self._battery, "TimeToEmpty", 0))
|
|
|
|
@Property(str, "readable")
|
|
def time_to_full(self):
|
|
if self._use_psutil_fallback:
|
|
return "N/A" # psutil doesn't provide time to full
|
|
return self.seconds_to_hours_minutes(getattr(self._battery, "TimeToFull", 0))
|
|
|
|
@Property(str, "readable")
|
|
def icon_name(self):
|
|
if self._use_psutil_fallback:
|
|
return "battery" # Generic icon name for psutil fallback
|
|
return self._battery.IconName
|
|
|
|
@Property(str, "readable")
|
|
def state(self):
|
|
if self._use_psutil_fallback:
|
|
if self._psutil_battery:
|
|
# psutil returns power_plugged boolean, convert to state
|
|
if self._psutil_battery.power_plugged:
|
|
if self._psutil_battery.percent >= 100:
|
|
return "FULLY_CHARGED"
|
|
else:
|
|
return "CHARGING"
|
|
else:
|
|
return "DISCHARGING"
|
|
return "UNKNOWN"
|
|
return DeviceState.get(self._battery.State, "UNKNOWN")
|
|
|
|
@Property(str, "readable")
|
|
def capacity(self):
|
|
if self._use_psutil_fallback:
|
|
return "N/A" # psutil doesn't provide capacity info
|
|
return f"{int(self._battery.Capacity)}%"
|
|
|
|
@Property(bool, "readable", default_value=False)
|
|
def is_present(self):
|
|
if self._use_psutil_fallback:
|
|
return self._psutil_battery is not None
|
|
return self._battery.IsPresent
|
|
|
|
@Property(str, "readable")
|
|
def power_profile(self):
|
|
if self._profile_proxy:
|
|
return self._profile_proxy.ActiveProfile
|
|
return None
|
|
|
|
@Property(list, "readable")
|
|
def available_profiles(self):
|
|
if self._profile_proxy:
|
|
try:
|
|
profiles = []
|
|
for p in self._profile_proxy.Profiles:
|
|
if hasattr(p, 'Profile'):
|
|
profiles.append(p.Profile)
|
|
elif isinstance(p, dict) and 'Profile' in p:
|
|
profiles.append(p['Profile'])
|
|
elif isinstance(p, str):
|
|
profiles.append(p)
|
|
return profiles
|
|
except Exception:
|
|
return []
|
|
return []
|
|
|
|
def change_power_profile(self, profile: str) -> bool:
|
|
if not self._profile_proxy:
|
|
return False
|
|
|
|
# Get available profiles using the same logic as available_profiles property
|
|
available_profiles = []
|
|
try:
|
|
for p in self._profile_proxy.Profiles:
|
|
if hasattr(p, 'Profile'):
|
|
available_profiles.append(p.Profile)
|
|
elif isinstance(p, dict) and 'Profile' in p:
|
|
available_profiles.append(p['Profile'])
|
|
elif isinstance(p, str):
|
|
available_profiles.append(p)
|
|
except Exception:
|
|
return False
|
|
|
|
if profile not in available_profiles:
|
|
return False
|
|
|
|
try:
|
|
self._profile_proxy.ActiveProfile = profile
|
|
self.profile_changed.emit(profile)
|
|
self.changed.emit()
|
|
return True
|
|
except Exception:
|
|
return False
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self._bus = SystemBus()
|
|
self._use_psutil_fallback = False
|
|
self._psutil_battery = None
|
|
|
|
# Battery device
|
|
try:
|
|
self._battery = self._bus.get(
|
|
"org.freedesktop.UPower", "/org/freedesktop/UPower/devices/battery_BAT0"
|
|
)
|
|
self._battery.onPropertiesChanged = self.handle_battery_change
|
|
except Exception:
|
|
# Fallback to psutil if UPower is not available
|
|
self._use_psutil_fallback = True
|
|
try:
|
|
self._psutil_battery = psutil.sensors_battery()
|
|
if self._psutil_battery is None:
|
|
return # No battery found
|
|
# Start periodic updates for psutil fallback - increased interval
|
|
GLib.timeout_add_seconds(10, self._update_psutil_battery)
|
|
except Exception:
|
|
return # psutil battery not available either
|
|
|
|
# PowerProfiles
|
|
try:
|
|
self._profile_proxy = self._bus.get(
|
|
"net.hadess.PowerProfiles", "/net/hadess/PowerProfiles"
|
|
)
|
|
# Use onPropertiesChanged for consistency with battery device
|
|
self._profile_proxy.onPropertiesChanged = (
|
|
lambda _, changed, __: self._handle_profile_props_changed(changed)
|
|
)
|
|
except Exception:
|
|
self._profile_proxy = None
|
|
|
|
self.changed.emit()
|
|
|
|
def _update_psutil_battery(self):
|
|
"""Update psutil battery data periodically"""
|
|
try:
|
|
self._psutil_battery = psutil.sensors_battery()
|
|
self.changed.emit()
|
|
except Exception:
|
|
pass # Continue trying
|
|
return True # Keep the timeout active
|
|
|
|
def _handle_profile_props_changed(self, changed):
|
|
"""Internal handler for property changes that processes only the changed properties"""
|
|
if "ActiveProfile" in changed:
|
|
new_profile = changed["ActiveProfile"]
|
|
self.profile_changed.emit(new_profile)
|
|
self.changed.emit()
|
|
|
|
def handle_battery_change(self, iface, changed, invalidated):
|
|
self.changed.emit()
|