1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-06 05:25:41 -05:00

power: use native upower APIs

This commit is contained in:
bbedward
2025-07-16 12:35:46 -04:00
parent 21bc4ece52
commit 2a84a6423b
5 changed files with 291 additions and 191 deletions

View File

@@ -8,206 +8,205 @@ pragma ComponentBehavior: Bound
Singleton {
id: root
// Debug mode for testing (disabled for now)
property bool debugMode: false
// Battery properties - using shell command method (native UPower API commented out due to issues)
property bool batteryAvailable: systemBatteryPercentage > 0
property int batteryLevel: systemBatteryPercentage
property bool batteryAvailable: UPower.displayDevice?.isLaptopBattery ?? false
property int batteryLevel: batteryAvailable ? Math.round(UPower.displayDevice.percentage * 100) : 0
property string batteryStatus: {
return systemBatteryState === "charging" ? "Charging" :
systemBatteryState === "discharging" ? "Discharging" :
systemBatteryState === "fully-charged" ? "Full" :
systemBatteryState === "empty" ? "Empty" : "Unknown"
if (!batteryAvailable) return "No Battery"
if (UPower.displayDevice.state === UPowerDeviceState.Charging) return "Charging"
if (UPower.displayDevice.state === UPowerDeviceState.Discharging) return "Discharging"
if (UPower.displayDevice.state === UPowerDeviceState.FullyCharged) return "Full"
if (UPower.displayDevice.state === UPowerDeviceState.Empty) return "Empty"
if (UPower.displayDevice.state === UPowerDeviceState.PendingCharge) return "Pending Charge"
if (UPower.displayDevice.state === UPowerDeviceState.PendingDischarge) return "Pending Discharge"
return "Unknown"
}
property int timeRemaining: 0 // Not implemented for shell fallback
property bool isCharging: systemBatteryState === "charging"
property bool isLowBattery: systemBatteryPercentage <= 20
property int timeRemaining: {
if (!batteryAvailable) return 0
return UPower.onBattery ? (UPower.displayDevice.timeToEmpty || 0) : (UPower.displayDevice.timeToFull || 0)
}
property bool isCharging: batteryAvailable && (UPower.displayDevice.state === UPowerDeviceState.Charging || (!UPower.onBattery && batteryLevel < 100))
property bool isLowBattery: batteryAvailable && batteryLevel <= 20
/* Native UPower API (commented out - not working correctly, returns 1% instead of actual values)
property bool batteryAvailable: (UPower.displayDevice && UPower.displayDevice.ready && UPower.displayDevice.percentage > 0) || systemBatteryPercentage > 0
property int batteryLevel: {
if (UPower.displayDevice && UPower.displayDevice.ready && UPower.displayDevice.percentage > 0) {
return Math.round(UPower.displayDevice.percentage)
}
return systemBatteryPercentage
}
property string batteryStatus: {
if (UPower.displayDevice && UPower.displayDevice.ready) {
switch(UPower.displayDevice.state) {
case UPowerDeviceState.Charging: return "Charging"
case UPowerDeviceState.Discharging: return "Discharging"
case UPowerDeviceState.FullyCharged: return "Full"
case UPowerDeviceState.Empty: return "Empty"
case UPowerDeviceState.PendingCharge: return "Pending Charge"
case UPowerDeviceState.PendingDischarge: return "Pending Discharge"
case UPowerDeviceState.Unknown:
default: return "Unknown"
}
}
return systemBatteryState === "charging" ? "Charging" :
systemBatteryState === "discharging" ? "Discharging" :
systemBatteryState === "fully-charged" ? "Full" :
systemBatteryState === "empty" ? "Empty" : "Unknown"
}
property int timeRemaining: (UPower.displayDevice && UPower.displayDevice.ready) ? (UPower.displayDevice.timeToEmpty || UPower.displayDevice.timeToFull || 0) : 0
property bool isCharging: {
if (UPower.displayDevice && UPower.displayDevice.ready) {
return UPower.displayDevice.state === UPowerDeviceState.Charging
}
return systemBatteryState === "charging"
}
property bool isLowBattery: {
if (UPower.displayDevice && UPower.displayDevice.ready) {
return UPower.displayDevice.percentage <= 20
}
return systemBatteryPercentage <= 20
}
*/
property int batteryHealth: 100 // Default fallback
property string batteryTechnology: "Li-ion" // Default fallback
property int cycleCount: 0 // Not implemented for shell fallback
property int batteryCapacity: 45000 // Default fallback
property var powerProfiles: availableProfiles
property string activePowerProfile: "balanced" // Default fallback
property int batteryHealth: batteryAvailable && UPower.displayDevice.healthSupported ? Math.round(UPower.displayDevice.healthPercentage * 100) : 100
property string batteryTechnology: batteryAvailable ? "Li-ion" : "N/A"
property int batteryCapacity: batteryAvailable ? Math.round(UPower.displayDevice.energyCapacity * 1000) : 0
// System battery info from shell command (primary source)
property int systemBatteryPercentage: 100 // Default value, will be updated by shell command
property string systemBatteryState: "charging" // Default value, will be updated by shell command
// Shell command fallback for battery info
Process {
id: batteryProcess
running: false
command: ["upower", "-i", "/org/freedesktop/UPower/devices/battery_BAT1"]
property var powerProfiles: {
if (!powerProfilesAvailable || typeof PowerProfiles === "undefined") {
return ["power-saver", "balanced", "performance"]
}
stdout: StdioCollector {
onStreamFinished: {
if (text.trim()) {
let output = text.trim()
let percentageMatch = output.match(/percentage:\s*(\d+)%/)
let stateMatch = output.match(/state:\s*(\w+)/)
if (percentageMatch) {
root.systemBatteryPercentage = parseInt(percentageMatch[1])
console.log("Battery percentage updated to:", root.systemBatteryPercentage)
}
if (stateMatch) {
root.systemBatteryState = stateMatch[1]
console.log("Battery state updated to:", root.systemBatteryState)
}
let profiles = [
PowerProfile.PowerSaver,
PowerProfile.Balanced,
PowerProfile.Performance
].filter(profile => {
if (profile === PowerProfile.Performance && !PowerProfiles.hasPerformanceProfile) {
return false
}
return true
})
return profiles.map(profile => {
switch(profile) {
case PowerProfile.PowerSaver: return "power-saver"
case PowerProfile.Performance: return "performance"
case PowerProfile.Balanced:
default: return "balanced"
}
})
}
property string activePowerProfile: {
if (powerProfilesAvailable && typeof PowerProfiles !== "undefined") {
try {
switch(PowerProfiles.profile) {
case PowerProfile.PowerSaver: return "power-saver"
case PowerProfile.Performance: return "performance"
default: return "balanced"
}
} catch (error) {
return "balanced"
}
}
return "balanced"
}
property bool powerProfilesAvailable: false
property string powerProfilesError: powerProfilesAvailable ? "" : "Power profiles daemon not available. Install and enable power-profiles-daemon."
property bool suggestPowerSaver: batteryAvailable && isLowBattery && UPower.onBattery && activePowerProfile !== "power-saver"
Process {
id: checkPowerProfilesDaemon
command: ["bash", "-c", "systemctl is-active power-profiles-daemon || pgrep -x power-profiles-daemon > /dev/null"]
running: false
onExited: (exitCode) => {
if (exitCode !== 0) {
console.warn("Battery process failed with exit code:", exitCode)
}
powerProfilesAvailable = (exitCode === 0)
}
}
Connections {
target: UPower
function onOnBatteryChanged() {
batteryAvailableChanged()
isChargingChanged()
}
}
// Timer to periodically check battery status
Timer {
interval: 5000 // Check every 5 seconds
running: true
repeat: true
onTriggered: {
batteryProcess.running = true
Connections {
target: typeof PowerProfiles !== "undefined" ? PowerProfiles : null
function onProfileChanged() {
activePowerProfileChanged()
}
}
Connections {
target: UPower.displayDevice
function onPercentageChanged() {
batteryLevelChanged()
isLowBatteryChanged()
}
function onStateChanged() {
batteryStatusChanged()
isChargingChanged()
}
function onTimeToEmptyChanged() {
timeRemainingChanged()
}
function onTimeToFullChanged() {
timeRemainingChanged()
}
function onReadyChanged() {
batteryAvailableChanged()
}
function onIsLaptopBatteryChanged() {
batteryAvailableChanged()
}
function onEnergyChanged() {
batteryCapacityChanged()
}
function onEnergyCapacityChanged() {
batteryCapacityChanged()
}
function onHealthPercentageChanged() {
batteryHealthChanged()
}
}
Component.onCompleted: {
// Initial battery check
batteryProcess.running = true
// Get current power profile
getCurrentProfile()
console.log("BatteryService initialized with shell command approach")
checkPowerProfilesDaemon.running = true
}
property var availableProfiles: {
// Try to use power-profiles-daemon via shell command
return ["power-saver", "balanced", "performance"]
}
signal showErrorMessage(string message)
function setBatteryProfile(profileName) {
console.log("Setting power profile to:", profileName)
powerProfileProcess.command = ["powerprofilesctl", "set", profileName]
powerProfileProcess.running = true
}
// Process to set power profile
Process {
id: powerProfileProcess
running: false
onExited: (exitCode) => {
if (exitCode === 0) {
console.log("Power profile set successfully")
// Update current profile
getCurrentProfile()
} else {
console.warn("Failed to set power profile, exit code:", exitCode)
}
}
}
// Process to get current power profile
Process {
id: getCurrentProfileProcess
running: false
command: ["powerprofilesctl", "get"]
stdout: StdioCollector {
onStreamFinished: {
if (text.trim()) {
root.activePowerProfile = text.trim()
console.log("Current power profile:", root.activePowerProfile)
}
}
if (!powerProfilesAvailable) {
console.warn("Power profiles daemon not available")
showErrorMessage("power-profiles-daemon not available")
return
}
onExited: (exitCode) => {
if (exitCode !== 0) {
console.warn("Failed to get current power profile, exit code:", exitCode)
try {
switch(profileName) {
case "power-saver":
PowerProfiles.profile = PowerProfile.PowerSaver
break
case "balanced":
PowerProfiles.profile = PowerProfile.Balanced
break
case "performance":
PowerProfiles.profile = PowerProfile.Performance
break
default:
console.warn("Unknown profile:", profileName)
return
}
console.log("Power profile set successfully to:", PowerProfiles.profile)
} catch (error) {
console.error("Failed to set power profile:", error)
showErrorMessage("power-profiles-daemon not available")
}
}
function getCurrentProfile() {
getCurrentProfileProcess.running = true
}
function getBatteryIcon() {
if (!root.batteryAvailable) return "power"
if (!batteryAvailable) {
switch(activePowerProfile) {
case "power-saver": return "energy_savings_leaf"
case "performance": return "rocket_launch"
default: return "balance"
}
}
let level = root.batteryLevel
let charging = root.isCharging
const level = batteryLevel
const charging = isCharging
if (charging) {
if (level >= 90) return "battery_charging_full"
if (level >= 60) return "battery_charging_90"
if (level >= 30) return "battery_charging_60"
if (level >= 80) return "battery_charging_90"
if (level >= 60) return "battery_charging_80"
if (level >= 50) return "battery_charging_60"
if (level >= 30) return "battery_charging_50"
if (level >= 20) return "battery_charging_30"
return "battery_charging_20"
} else {
if (level >= 90) return "battery_full"
if (level >= 60) return "battery_6_bar"
if (level >= 50) return "battery_5_bar"
if (level >= 40) return "battery_4_bar"
if (level >= 30) return "battery_3_bar"
if (level >= 20) return "battery_2_bar"
if (level >= 95) return "battery_full"
if (level >= 85) return "battery_6_bar"
if (level >= 70) return "battery_5_bar"
if (level >= 55) return "battery_4_bar"
if (level >= 40) return "battery_3_bar"
if (level >= 25) return "battery_2_bar"
if (level >= 10) return "battery_1_bar"
return "battery_alert"
}
}
function formatTimeRemaining() {
if (root.timeRemaining <= 0) return "Unknown"
if (!batteryAvailable || timeRemaining <= 0) return "Unknown"
let hours = Math.floor(root.timeRemaining / 3600)
let minutes = Math.floor((root.timeRemaining % 3600) / 60)
const hours = Math.floor(timeRemaining / 3600)
const minutes = Math.floor((timeRemaining % 3600) / 60)
if (hours > 0) {
return hours + "h " + minutes + "m"

View File

@@ -29,6 +29,7 @@ Singleton {
property int retryDelay: 30000 // 30 seconds
property int lastFetchTime: 0
property int minFetchInterval: 30000 // 30 seconds minimum between fetches
property int persistentRetryCount: 0 // Track persistent retry attempts for backoff
// Weather icon mapping (based on wttr.in weather codes)
property var weatherIcons: ({
@@ -121,6 +122,11 @@ Singleton {
function handleWeatherSuccess() {
root.retryAttempts = 0
root.persistentRetryCount = 0 // Reset persistent retry count on success
// Stop any persistent retry timer if running
if (persistentRetryTimer.running) {
persistentRetryTimer.stop()
}
// Don't restart the timer - let it continue its normal interval
if (updateTimer.interval !== root.updateInterval) {
updateTimer.interval = root.updateInterval
@@ -133,12 +139,17 @@ Singleton {
console.log(`Weather fetch failed, retrying in ${root.retryDelay/1000}s (attempt ${root.retryAttempts}/${root.maxRetryAttempts})`)
retryTimer.start()
} else {
console.warn("Weather fetch failed after maximum retry attempts")
console.warn("Weather fetch failed after maximum retry attempts, will keep trying...")
root.weather.available = false
root.weather.loading = false
// Reset retry count but keep trying with exponential backoff
root.retryAttempts = 0
// Set longer interval for next automatic retry
updateTimer.interval = root.updateInterval * 2
// Use exponential backoff: 1min, 2min, 4min, then cap at 5min
const backoffDelay = Math.min(60000 * Math.pow(2, persistentRetryCount), 300000)
persistentRetryCount++
console.log(`Scheduling persistent retry in ${backoffDelay/1000}s`)
persistentRetryTimer.interval = backoffDelay
persistentRetryTimer.start()
}
}
@@ -223,6 +234,17 @@ Singleton {
}
}
Timer {
id: persistentRetryTimer
interval: 60000 // Will be dynamically set
running: false
repeat: false
onTriggered: {
console.log("Persistent retry attempt...")
root.fetchWeather()
}
}
Component.onCompleted: {
// Watch for preference changes to refetch weather
Prefs.weatherLocationOverrideChanged.connect(() => {

View File

@@ -9,7 +9,7 @@ import "../Services"
PanelWindow {
id: batteryControlPopup
visible: root.batteryPopupVisible && BatteryService.batteryAvailable
visible: root.batteryPopupVisible
implicitWidth: 400
implicitHeight: 300
@@ -84,7 +84,7 @@ PanelWindow {
width: parent.width
Text {
text: "Battery Information"
text: BatteryService.batteryAvailable ? "Battery Information" : "Power Management"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
@@ -119,7 +119,6 @@ PanelWindow {
}
}
// Battery status card
Rectangle {
width: parent.width
height: 120
@@ -127,13 +126,13 @@ PanelWindow {
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.5)
border.color: BatteryService.isCharging ? Theme.primary : (BatteryService.isLowBattery ? Theme.error : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12))
border.width: BatteryService.isCharging || BatteryService.isLowBattery ? 2 : 1
visible: BatteryService.batteryAvailable
Row {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingL
// Large battery icon
Text {
text: BatteryService.getBatteryIcon()
font.family: Theme.iconFont
@@ -188,10 +187,53 @@ PanelWindow {
}
}
// No battery info card
Rectangle {
width: parent.width
height: 80
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.5)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: 1
visible: !BatteryService.batteryAvailable
Row {
anchors.centerIn: parent
spacing: Theme.spacingL
Text {
text: BatteryService.getBatteryIcon()
font.family: Theme.iconFont
font.pixelSize: 36
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
Text {
text: "No Battery Detected"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
Text {
text: "Power profile management is available"
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
}
}
}
}
// Battery details
Column {
width: parent.width
spacing: Theme.spacingM
visible: BatteryService.batteryAvailable
Text {
text: "Battery Details"
@@ -224,23 +266,6 @@ PanelWindow {
}
}
// Cycle count
Column {
spacing: 2
Text {
text: "Cycle Count"
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
font.weight: Font.Medium
}
Text {
text: BatteryService.cycleCount > 0 ? BatteryService.cycleCount.toString() : "Unknown"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
}
}
// Health
Column {
@@ -280,11 +305,11 @@ PanelWindow {
}
}
// Power profiles (if available)
// Power profiles
Column {
width: parent.width
spacing: Theme.spacingM
visible: BatteryService.powerProfiles.length > 0
visible: true
Text {
text: "Power Profile"
@@ -380,4 +405,44 @@ PanelWindow {
}
}
}
// Error toast
Rectangle {
id: errorToast
width: Math.min(300, parent.width - Theme.spacingL * 2)
height: 50
radius: Theme.cornerRadius
color: Theme.error
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: Theme.spacingL
visible: false
z: 1000
Text {
anchors.centerIn: parent
text: "power-profiles-daemon not available"
color: "white"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
}
Timer {
id: hideTimer
interval: 3000
onTriggered: errorToast.visible = false
}
function show() {
visible = true
hideTimer.restart()
}
}
Connections {
target: BatteryService
function onShowErrorMessage(message) {
errorToast.show()
}
}
}

View File

@@ -9,19 +9,18 @@ Rectangle {
signal toggleBatteryPopup()
width: 70 // Increased width to accommodate percentage text
width: BatteryService.batteryAvailable ? 70 : 40
height: 30
radius: Theme.cornerRadius
color: batteryArea.containsMouse || batteryPopupVisible ?
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) :
Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08)
visible: BatteryService.batteryAvailable
visible: true
Row {
anchors.centerIn: parent
spacing: 4
// Battery icon - Material Design icons already show level visually
Text {
text: BatteryService.getBatteryIcon()
font.family: Theme.iconFont
@@ -34,7 +33,6 @@ Rectangle {
}
anchors.verticalCenter: parent.verticalCenter
// Subtle pulse animation for charging
SequentialAnimation on opacity {
running: BatteryService.isCharging
loops: Animation.Infinite
@@ -43,7 +41,6 @@ Rectangle {
}
}
// Battery percentage text
Text {
text: BatteryService.batteryLevel + "%"
font.pixelSize: Theme.fontSizeSmall
@@ -55,6 +52,7 @@ Rectangle {
return Theme.surfaceText
}
anchors.verticalCenter: parent.verticalCenter
visible: BatteryService.batteryAvailable
}
}
@@ -78,7 +76,7 @@ Rectangle {
color: Theme.surfaceContainer
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 1
visible: batteryArea.containsMouse && !batteryPopupVisible && BatteryService.batteryAvailable
visible: batteryArea.containsMouse && !batteryPopupVisible
anchors.bottom: parent.top
anchors.bottomMargin: Theme.spacingS
@@ -100,7 +98,15 @@ Rectangle {
Text {
id: tooltipText
text: {
if (!BatteryService.batteryAvailable) return "No battery"
if (!BatteryService.batteryAvailable) {
let profile = BatteryService.activePowerProfile
switch(profile) {
case "power-saver": return "Power Profile: Power Saver"
case "balanced": return "Power Profile: Balanced"
case "performance": return "Power Profile: Performance"
default: return "Power Profile: " + profile
}
}
let status = BatteryService.batteryStatus
let level = BatteryService.batteryLevel + "%"

View File

@@ -34,15 +34,23 @@ Rectangle {
visible: !weather || !weather.available || weather.temp === 0
Text {
text: "cloud_off"
text: weather && weather.loading ? "cloud_sync" : "cloud_off"
font.family: theme.iconFont
font.pixelSize: theme.iconSize + 8
color: Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.5)
anchors.horizontalCenter: parent.horizontalCenter
RotationAnimation on rotation {
from: 0
to: 360
duration: 2000
running: weather && weather.loading
loops: Animation.Infinite
}
}
Text {
text: "No Weather Data"
text: weather && weather.loading ? "Loading Weather..." : "No Weather Data"
font.pixelSize: theme.fontSizeMedium
color: Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.7)
anchors.horizontalCenter: parent.horizontalCenter