Update Battery widget plugin (#311)

* fix: invalid check in getWarningThreshold

* fix: invalid status

* feat: use dbus events instead of polling

* fix: invalid status again

* fix: dump version and set valid plugin API

* fix: add commandExists check

* fix: getColor function (warning color when not charging)

* feat: use upower instead of cat /sys

* feat: notify on error

* fix: cleaner code

* fix: use noctalia.tr for notification (not logs, intended)

* feat: migrate from dbus-monitor to gdbus (everything in one line)

* feat: use gdbus to init and update, parse only when needed

* feat: add label_content and optimisations

* fix: remove multiple commands check not needed anymore

* fix: update README and dump version

* fix: default data, warning_threshold check, floor percent
This commit is contained in:
Yocraft-2000
2026-08-12 09:37:30 -04:00
committed by GitHub
parent 97389b0ff8
commit e9121c114c
4 changed files with 197 additions and 57 deletions
+166 -43
View File
@@ -1,13 +1,70 @@
local glyph
local color
local percent
local status
local data = { percent = 0, status = "unknown" }
local label_content = noctalia.getConfig("label_content")
local warning_threshold = 20
local path = "/sys/class/power_supply/"
local batName
local stateMap = {
[0] = "unknown",
[1] = "charging",
[2] = "discharging",
[3] = "empty",
[4] = "fully-charged",
[5] = "pending-charge",
[6] = "pending-discharge",
}
local function shellEscape(raw)
return "'" .. raw:gsub("'", "'\\''") .. "'"
end
local function notifyError()
noctalia.notifyError("Battery widget", noctalia.tr("notify.error"))
end
if not noctalia.commandExists("gdbus") then
noctalia.log("battery-widget: gdbus not found.")
notifyError()
return
end
local function parse(output)
data = data or {}
local percent = output:match("'Percentage':%s*<([%d%.]+)>")
if percent then
data.percent = tonumber(percent) or 0
end
local state = output:match("'State':%s*<uint32%s+(%d+)>")
if state then
data.status = stateMap[tonumber(state)] or "unknown"
end
if label_content == "time" then
local timeToEmpty = output:match("'TimeToEmpty':%s*<int64%s+(%-?%d+)>")
if timeToEmpty then
data.timeToEmpty = tonumber(timeToEmpty)
end
local timeToFull = output:match("'TimeToFull':%s*<int64%s+(%-?%d+)>")
if timeToFull then
data.timeToFull = tonumber(timeToFull)
end
elseif label_content == "rate" then
local rate = output:match("'EnergyRate':%s*<([%-%d%.]+)>")
if rate then
data.rate = tonumber(rate)
end
end
return data
end
local function getBatName()
local config = noctalia.getConfig("battery")
@@ -30,17 +87,17 @@ local function getBatName()
end
local function getGlyph()
if status == "Charging" then
if data.status == "charging" then
glyph = "battery-charging"
elseif status == "Full" or status == "Not Charging" then
elseif data.status == "fully-charged" or data.status == "pending-charge" then
glyph = "battery-plugged"
elseif status == "Unknown" then
elseif data.status == "unknown" then
glyph = "battery-exclamation"
else
glyph = percent >= 85 and "battery-4"
or percent >= 55 and "battery-3"
or percent >= 30 and "battery-2"
or percent >= 10 and "battery-1"
glyph = data.percent >= 85 and "battery-4"
or data.percent >= 55 and "battery-3"
or data.percent >= 30 and "battery-2"
or data.percent >= 10 and "battery-1"
or "battery-0"
end
end
@@ -51,36 +108,85 @@ local function getWarningThreshold()
function(result)
if result.exitCode ~= 0 then
noctalia.log("battery-widget: failed to get warning_threshold: " .. result.stderr)
notifyError()
return
end
local res = result.stdout
if not warning_threshold then
noctalia.log("battery-widget: invalid warning_threshold: " .. res)
if not res then
noctalia.log("battery-widget: invalid warning_threshold, keeping default")
notifyError()
return
end
warning_threshold = tonumber(noctalia.string.trim(res))
local parsed = tonumber(noctalia.string.trim(res))
if parsed then
warning_threshold = parsed
else
noctalia.log("battery-widget: invalid warning_threshold, keeping default")
notifyError()
end
end
)
end
local function getColor()
if status ~= "Charging" and percent <= warning_threshold then
color = noctalia.getConfig("warning_color")
if data.status == "charging" or data.status == "fully-charged" or data.status == "pending-charge" then
color = noctalia.getConfig("charging_color")
return
end
if status == "Charging" or status == "Full" or status == "Not Charging" then
color = noctalia.getConfig("charging_color")
if data.percent <= warning_threshold then
color = noctalia.getConfig("warning_color")
return
end
color = noctalia.getConfig("color")
end
local function formatTime(seconds)
if not seconds or seconds <= 0 then
return nil
end
local hours = math.floor(seconds / 3600)
local minutes = math.floor((seconds % 3600) / 60)
if hours > 0 and minutes > 0 then
return string.format("%dh %dm", hours, minutes)
elseif hours > 0 then
return string.format("%dh", hours)
else
return string.format("%dm", minutes)
end
end
local function formatRate(rate)
if not rate or rate <= 0 then
return nil
end
return string.format("%.1f W", rate)
end
local function formatPercent()
return math.floor((data.percent or 0) + 0.5) .. "%"
end
local function getLabelText()
if label_content == "time" then
local connected = data.status == "charging" or data.status == "fully-charged" or data.status == "pending-charge"
local seconds = connected and data.timeToFull or data.timeToEmpty
return formatTime(seconds) or formatPercent()
elseif label_content == "rate" then
return formatRate(data.rate) or formatPercent()
end
return formatPercent()
end
local function render()
if (noctalia.getConfig("hide_plugged") and (status == "Charging" or status == "Not Charging"))
or (noctalia.getConfig("hide_full") and status == "Full") then
if (noctalia.getConfig("hide_plugged") and (data.status == "charging" or data.status == "pending-charge"))
or (noctalia.getConfig("hide_full") and data.status == "fully-charged") then
desktopWidget.render(ui.row())
return
end
@@ -90,7 +196,7 @@ local function render()
table.insert(content, ui.glyph({ name = glyph, color = color }))
end
if noctalia.getConfig("show_label") then
table.insert(content, ui.label({ text = percent .. "%", color = color }))
table.insert(content, ui.label({ text = getLabelText(), color = color }))
end
local vertical = noctalia.getConfig("layout") == "vertical"
@@ -105,38 +211,56 @@ local function render()
)
end
local function shellEscape(raw)
return "'" .. raw:gsub("'", "'\\''") .. "'"
local function refreshBattery(output)
parse(output)
getGlyph()
getColor()
render()
end
local function refreshBattery()
noctalia.runAsync(
"cat "
.. shellEscape(path .. batName .. "/capacity")
.. " "
.. shellEscape(path .. batName .. "/status"),
local function initData()
local cmd = string.format(
"gdbus call --system --dest org.freedesktop.UPower --object-path %s --method org.freedesktop.DBus.Properties.GetAll org.freedesktop.UPower.Device",
shellEscape("/org/freedesktop/UPower/devices/battery_" .. batName)
)
noctalia.runAsync(cmd,
function(result)
if result.exitCode ~= 0 then
noctalia.log("battery-widget: failed to read battery: " .. result.stderr)
return
end
local lines = {}
for line in result.stdout:gmatch("[^\r\n]+") do
table.insert(lines, line)
end
if not lines[1] or not lines[2] then
if not result.stdout then
noctalia.log("battery-widget: invalid battery output")
return
end
percent = tonumber(noctalia.string.trim(lines[1]))
status = noctalia.string.trim(lines[2])
refreshBattery(result.stdout)
end
)
end
getGlyph()
getColor()
render()
local function watchBattery()
local patterns = { "Percentage", "State" }
if label_content == "time" then
table.insert(patterns, "TimeToEmpty")
table.insert(patterns, "TimeToFull")
elseif label_content == "rate" then
table.insert(patterns, "EnergyRate")
end
local cmd = string.format(
"gdbus monitor --system --dest org.freedesktop.UPower --object-path %s",
shellEscape("/org/freedesktop/UPower/devices/battery_" .. batName)
)
noctalia.runStream(cmd,
function(line)
for _, pattern in ipairs(patterns) do
if line:find(pattern) then
refreshBattery(line)
break
end
end
end
)
end
@@ -144,12 +268,11 @@ end
batName = getBatName()
if not batName then
noctalia.log("battery-widget: battery not found")
notifyError()
return
end
getWarningThreshold()
function update()
noctalia.setUpdateInterval(noctalia.getConfig("refresh_interval") * 1000)
refreshBattery()
end
initData()
watchBattery()