206 lines
5.9 KiB
Lua
206 lines
5.9 KiB
Lua
-- Show instantaneous battery power above an ETA based on a time-weighted,
|
|
-- five-minute rolling average. Energy values are normalized to µWh so that
|
|
-- both energy_* and charge_* battery drivers can produce an estimate.
|
|
local command = [[
|
|
bat=/sys/class/power_supply/BAT0
|
|
|
|
number() {
|
|
if [ -r "$1" ]; then
|
|
value=$(cat "$1" 2>/dev/null)
|
|
case "$value" in
|
|
''|*[!0-9]*) ;;
|
|
*) printf '%s' "$value"; return 0 ;;
|
|
esac
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
status=$(cat "$bat/status" 2>/dev/null) || exit 1
|
|
power=$(number "$bat/power_now")
|
|
voltage=$(number "$bat/voltage_now")
|
|
|
|
if [ -z "$power" ]; then
|
|
current=$(number "$bat/current_now")
|
|
if [ -n "$current" ] && [ -n "$voltage" ]; then
|
|
power=$(awk -v current="$current" -v voltage="$voltage" \
|
|
'BEGIN { printf "%.0f", current * voltage / 1000000 }')
|
|
fi
|
|
fi
|
|
|
|
energy_now=$(number "$bat/energy_now")
|
|
energy_full=$(number "$bat/energy_full")
|
|
|
|
if [ -z "$energy_now" ] || [ -z "$energy_full" ]; then
|
|
charge_now=$(number "$bat/charge_now")
|
|
charge_full=$(number "$bat/charge_full")
|
|
if [ -n "$charge_now" ] && [ -n "$charge_full" ] && [ -n "$voltage" ]; then
|
|
energy_now=$(awk -v charge="$charge_now" -v voltage="$voltage" \
|
|
'BEGIN { printf "%.0f", charge * voltage / 1000000 }')
|
|
energy_full=$(awk -v charge="$charge_full" -v voltage="$voltage" \
|
|
'BEGIN { printf "%.0f", charge * voltage / 1000000 }')
|
|
fi
|
|
fi
|
|
|
|
printf '%s\t%s\t%s\t%s\n' "$status" "$power" "$energy_now" "$energy_full"
|
|
]]
|
|
|
|
local sample_window_seconds = 5 * 60
|
|
local power_samples = {}
|
|
local sampled_state = nil
|
|
local request_in_flight = false
|
|
|
|
local function renderLines(top, bottom)
|
|
barWidget.render(ui.column({ gap = 0, align = "center" }, {
|
|
ui.label({ text = top, fontSize = 10 }),
|
|
ui.label({ text = bottom, fontSize = 9 }),
|
|
}))
|
|
end
|
|
|
|
local function resetSamples(state)
|
|
if sampled_state ~= state then
|
|
power_samples = {}
|
|
sampled_state = state
|
|
end
|
|
end
|
|
|
|
local function addPowerSample(state, power, now)
|
|
resetSamples(state)
|
|
|
|
local latest = power_samples[#power_samples]
|
|
if latest ~= nil and latest.time == now then
|
|
latest.power = power
|
|
else
|
|
table.insert(power_samples, { time = now, power = power })
|
|
end
|
|
|
|
local cutoff = now - sample_window_seconds
|
|
while #power_samples > 1 and power_samples[2].time <= cutoff do
|
|
table.remove(power_samples, 1)
|
|
end
|
|
end
|
|
|
|
local function rollingAverage(now)
|
|
if #power_samples == 0 then
|
|
return nil, 0
|
|
end
|
|
|
|
if #power_samples == 1 then
|
|
return power_samples[1].power, 0
|
|
end
|
|
|
|
local cutoff = now - sample_window_seconds
|
|
local integral = 0
|
|
local duration = 0
|
|
|
|
for index = 2, #power_samples do
|
|
local earlier = power_samples[index - 1]
|
|
local later = power_samples[index]
|
|
local interval = later.time - earlier.time
|
|
local start_time = math.max(earlier.time, cutoff)
|
|
local end_time = math.min(later.time, now)
|
|
|
|
if interval > 0 and end_time > start_time then
|
|
local start_fraction = (start_time - earlier.time) / interval
|
|
local end_fraction = (end_time - earlier.time) / interval
|
|
local start_power = earlier.power + (later.power - earlier.power) * start_fraction
|
|
local end_power = earlier.power + (later.power - earlier.power) * end_fraction
|
|
local elapsed = end_time - start_time
|
|
|
|
integral = integral + (start_power + end_power) * elapsed / 2
|
|
duration = duration + elapsed
|
|
end
|
|
end
|
|
|
|
if duration == 0 then
|
|
return power_samples[#power_samples].power, 0
|
|
end
|
|
|
|
return integral / duration, duration
|
|
end
|
|
|
|
local function formatDuration(hours)
|
|
local minutes = math.max(1, math.floor(hours * 60 + 0.5))
|
|
local whole_hours = math.floor(minutes / 60)
|
|
local remaining_minutes = minutes % 60
|
|
|
|
if whole_hours > 0 then
|
|
return string.format("%dh %02dm", whole_hours, remaining_minutes)
|
|
end
|
|
|
|
return string.format("%dm", remaining_minutes)
|
|
end
|
|
|
|
local function formatSampleDuration(seconds)
|
|
local minutes = math.floor(seconds / 60)
|
|
return string.format("%dm %02ds", minutes, seconds % 60)
|
|
end
|
|
|
|
function update()
|
|
noctalia.setUpdateInterval(5000)
|
|
|
|
if request_in_flight then
|
|
return
|
|
end
|
|
|
|
request_in_flight = true
|
|
noctalia.runAsync(command, function(result)
|
|
request_in_flight = false
|
|
|
|
if result.exitCode ~= 0 then
|
|
renderLines("—", "Unavailable")
|
|
barWidget.setTooltip("Battery rate and remaining-time estimate unavailable")
|
|
return
|
|
end
|
|
|
|
local state, power, energy_now, energy_full = string.match(
|
|
result.stdout,
|
|
"^([^\t]+)\t([^\t]*)\t([^\t]*)\t([^\r\n]*)"
|
|
)
|
|
power = tonumber(power)
|
|
energy_now = tonumber(energy_now)
|
|
energy_full = tonumber(energy_full)
|
|
|
|
if state ~= "Discharging" and state ~= "Charging" then
|
|
resetSamples(state)
|
|
renderLines("—", state)
|
|
barWidget.setTooltip("Battery: " .. state)
|
|
return
|
|
end
|
|
|
|
if power == nil or power <= 0 then
|
|
renderLines("—", "Calculating…")
|
|
barWidget.setTooltip("Battery: " .. state .. " · waiting for a power reading")
|
|
return
|
|
end
|
|
|
|
local now = os.time()
|
|
addPowerSample(state, power, now)
|
|
local average_power, sample_duration = rollingAverage(now)
|
|
local label = "Calculating…"
|
|
|
|
if average_power ~= nil and average_power > 0 and energy_now ~= nil and energy_full ~= nil then
|
|
local energy_remaining = energy_now
|
|
if state == "Charging" then
|
|
energy_remaining = energy_full - energy_now
|
|
end
|
|
|
|
if energy_remaining >= 0 then
|
|
local suffix = state == "Charging" and " to full" or " left"
|
|
label = formatDuration(energy_remaining / average_power) .. suffix
|
|
end
|
|
end
|
|
|
|
local direction = state == "Charging" and "↑" or "↓"
|
|
renderLines(string.format("%s %.1f W", direction, power / 1000000), label)
|
|
barWidget.setTooltip(
|
|
string.format(
|
|
"Battery: %s · %.1f W now · %.1f W rolling average (%s sampled of 5m)",
|
|
state,
|
|
power / 1000000,
|
|
average_power / 1000000,
|
|
formatSampleDuration(sample_duration)
|
|
)
|
|
)
|
|
end)
|
|
end
|