summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAnton Khirnov <anton@khirnov.net>2023-01-05 16:27:59 +0100
committerAnton Khirnov <anton@khirnov.net>2023-01-05 16:35:04 +0100
commit4808f43077c9b4bf161e37c6fffc1e9dfbbccf98 (patch)
tree8478d6227d5ee2f03de87dcaced284eaac98981a
parentd2fbf3c11fb10c2b76455df2a718a8c8d88bf16d (diff)
rc: add a battery level monitor to statusbar
Consisting of: - battery object, which is a wrapper around a sysfs dir describing a battery - battery manager, which adds and removes individual batteries as they (dis)appear - e.g. wireless input devices, and maps them to widgets displayed side by side - battery widget, which shows the status of one of the batteries; the widget used is derived from batteryarc [1], but heavily modified [1] https://github.com/streetturtle/awesome-wm-widgets/
-rw-r--r--battery.lua114
-rw-r--r--battery_mng.lua139
-rw-r--r--battery_wgt.lua204
-rw-r--r--rc.lua15
4 files changed, 467 insertions, 5 deletions
diff --git a/battery.lua b/battery.lua
new file mode 100644
index 0000000..abaf86d
--- /dev/null
+++ b/battery.lua
@@ -0,0 +1,114 @@
+local gio = require("lgi").Gio
+
+local object = require("gears.object")
+
+local utils = require("utils")
+
+local Battery = {}
+Battery.__index = Battery
+
+setmetatable(Battery, {
+ __call = function (cls, ...) return cls.new(...) end,
+})
+
+Battery.STATUS = {
+ UNKNOWN = 'Unknown',
+ DISCHARGING = 'Discharging',
+ CHARGING = 'Charging',
+ ['NOT CHARGING'] = 'Not charging',
+ FULL = 'Full',
+}
+
+function Battery:_readfile(name)
+ local file = gio.File.new_for_path(self._path .. name)
+ local contents = file:load_contents(nil)
+
+ -- remove trailing newline
+ if contents then
+ contents = string.gsub(contents, "%s$", "")
+ return string.len(contents) > 0 and contents or nil
+ end
+
+ return nil
+end
+
+function Battery:_readnum(name)
+ local val = self:_readfile(name)
+ return tonumber(val)
+end
+
+function Battery:update()
+ local status = string.upper(self:_readfile('status') or '')
+ self.status = self.STATUS[status]
+ if self.status == nil then
+ utils.log('Battery/' .. self.name, 'Error updating status: %s', tostring(status))
+ return false
+ end
+
+ self.capacity = self:_readnum('capacity')
+ self.capacity_level = self:_readfile('capacity_level')
+
+ local energy = self:_readnum('energy_now')
+ local energy_full = self:_readnum('energy_full')
+ local power = self:_readnum('power_now')
+
+ -- try to compute remaining seconds to charge/discharge,
+ -- if we have the information
+ self.remaining_seconds = nil
+
+ if self.status == self.STATUS.DISCHARGING then
+ if energy and power then
+ self.remaining_seconds = 3600 * energy / power
+ end
+ elseif self.status == self.STATUS.CHARGING then
+ if energy and energy_full and power then
+ self.remaining_seconds = 3600 * (energy_full - energy) / power
+ end
+ end
+
+ if power ~= nil then
+ self.power = power / 1e6
+ end
+
+ self:emit_signal('updated')
+
+ return true
+end
+
+function Battery.new(name)
+ local self = object({class = Battery})
+
+ self.name = name
+ self._path = '/sys/class/power_supply/' .. name .. '/'
+
+ -- skip non-battery power supplies, we don't handle them
+ local type = self:_readfile('type')
+ if type ~= 'Battery' then
+ return nil
+ end
+
+ -- build description string
+ local desc = nil
+
+ -- first try manufacturer+model
+ local manufacturer = self:_readfile('manufacturer')
+ local model = self:_readfile('model_name')
+ if manufacturer or model then
+ desc = (manufacturer or '') .. (manufacturer and ' ' or '') .. (model or '')
+ end
+
+ if not desc then
+ local technology = self:_readfile('technology')
+ desc = (technology or '') .. (technology and ' ' or '') .. type
+ end
+
+ self.desc = desc
+
+ if not self:update() then
+ return nil
+ end
+
+ return self
+end
+
+return Battery
diff --git a/battery_mng.lua b/battery_mng.lua
new file mode 100644
index 0000000..c04c52f
--- /dev/null
+++ b/battery_mng.lua
@@ -0,0 +1,139 @@
+local GFile = require("lgi").Gio.File
+
+local gears = require("gears")
+local spawn = require("awful.spawn")
+local wibox = require("wibox")
+
+local utils = require('utils')
+
+local BatteryWidget = require('battery_wgt')
+local Battery = require('battery')
+
+
+local BatteryManager = {}
+BatteryManager.__index = BatteryManager
+
+setmetatable(BatteryManager, {
+ __call = function (cls, ...) return cls.new(...) end,
+})
+
+function BatteryManager:_log(msg, ...)
+ utils.log('BatteryManager', msg, ...)
+end
+
+function BatteryManager:_warn(msg, ...)
+ utils.warn('BatteryManager', msg, ...)
+end
+
+function BatteryManager:_battery_add(name)
+ if self._batteries[name] ~= nil then
+ -- only warn for usable batteries that are added twice for some reason
+ if self._batteries[name] ~= false then
+ self:_warn('Battery %s already exists', name)
+ end
+ return
+ end
+
+ self:_log('Trying power supply: ' .. name)
+
+ local bat = Battery(name)
+ if not bat then
+ self:_log('Skipping ' .. name .. ': could not be opened or is not a battery')
+ -- remember device as unusable
+ self._batteries[name] = false
+ return
+ end
+ self:_log('Opened battery %s: %s', name, bat.desc)
+
+ local wgt = BatteryWidget(bat)
+
+ self._layout:add(wgt)
+
+ self._batteries[name] = { widget = wgt, battery = bat }
+end
+
+function BatteryManager:_battery_remove(name)
+ self:_log('Removing battery ' .. name)
+
+ local tbl = self._batteries[name]
+ if tbl then
+ self._layout:remove_widgets(tbl.widget)
+ end
+ self._batteries[name] = nil
+end
+
+function BatteryManager:_refresh()
+ for name, item in pairs(self._batteries) do
+ if item and not item.battery:update() then
+ self:_log('Could not update battery %s, removing', name)
+ self:_battery_remove(name)
+ end
+ end
+end
+
+function BatteryManager.new()
+ local self = setmetatable({}, BatteryManager)
+
+ -- the layout containing the individual battery widgets
+ self._layout = wibox.layout.flex.horizontal()
+
+ -- entries are either
+ -- - tables { battery, widget } for existing batteries
+ -- - false for devices that we've seen, but are not valid batteries
+ self._batteries = {}
+
+ -- try adding all existing batteries
+ local ps = GFile.new_for_path('/sys/class/power_supply')
+ local ret = ps:enumerate_children('*', 0)
+ while true do
+ local file_info = ret:next_file()
+ if not file_info then
+ break
+ end
+
+ self:_battery_add(file_info:get_name())
+ end
+
+ -- monitor udev events
+ local function monitor_stdout(line)
+ local action, device = string.match(line, '^KERNEL%[%d+.%d*%]%s+(%a+)%s+([^%s]+)%s+%(power_supply%)$')
+ if action then
+ local name = string.match(device, '([^/]+)$')
+ if name == nil then
+ self:_warn('Could not extract device name from path: %s', device)
+ return
+ end
+
+ if action == 'add' or action == 'change' then
+ -- battery device not seen before, try adding
+ if self._batteries[name] == nil then
+ self:_battery_add(name)
+ end
+
+ -- device is a valid battery, refresh state
+ if self._batteries[name] then
+ self._batteries[name].battery:update()
+ end
+ elseif action == 'remove' then
+ self:_battery_remove(name)
+ end
+ end
+ end
+
+ local function monitor_exit(reason, code)
+ self:_warn('udevadm monitor exited: %s/%d', reason, code)
+ end
+
+ spawn.with_line_callback('udevadm monitor --kernel --subsystem-match=power_supply',
+ { stdout = monitor_stdout, exit = monitor_exit, })
+
+ self:_refresh()
+ gears.timer.start_new(5, function ()
+ self:_refresh()
+ return true
+ end)
+
+ return self._layout
+end
+
+return BatteryManager
diff --git a/battery_wgt.lua b/battery_wgt.lua
new file mode 100644
index 0000000..ddf4120
--- /dev/null
+++ b/battery_wgt.lua
@@ -0,0 +1,204 @@
+-- Battery status widget
+--
+-- Based on Battery Arc Widget by Pavel Makhov
+-- https://github.com/streetturtle/awesome-wm-widgets/tree/master/batteryarc-widget
+-- @copyright 2020 Pavel Makhov
+-- The MIT License (MIT)
+--
+-- Copyright (c) 2017
+--
+-- Permission is hereby granted, free of charge, to any person obtaining a copy
+-- of this software and associated documentation files (the "Software"), to deal
+-- in the Software without restriction, including without limitation the rights
+-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+-- copies of the Software, and to permit persons to whom the Software is
+-- furnished to do so, subject to the following conditions:
+--
+-- The above copyright notice and this permission notice shall be included in all
+-- copies or substantial portions of the Software.
+--
+-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+-- SOFTWARE.
+
+local awful = require("awful")
+local beautiful = require("beautiful")
+-- local menubar_it = require("menubar.icon_theme")
+-- local menubar_utils = require("menubar.utils")
+local naughty = require("naughty")
+local wibox = require("wibox")
+
+local BatteryWidget = {}
+BatteryWidget.__index = BatteryWidget
+
+setmetatable(BatteryWidget, {
+ __call = function (cls, ...) return cls.new(...) end,
+})
+
+local COLORS = {
+ MAIN_FG = beautiful.fg_color,
+ MAIN_BG = '#ffffff11',
+
+
+ CHARGING = "#43a047",
+ CHARGE_MEDIUM = "#c0ca33",
+ CHARGE_LOW = "#e53935",
+
+ NOTIFY_LOW_FG = "#EEE9EF",
+ NOTIFY_LOW_BG = "#F06060",
+}
+
+function BatteryWidget:_show_battery_warning(charge_percent)
+ naughty.notify({
+ -- icon = warning_msg_icon,
+ -- icon_size = 100,
+ title = 'Low battery',
+ text = string.format('battery at %d%%', charge_percent),
+ timeout = 25, -- show the warning for a longer time
+ hover_timeout = 0.5,
+ bg = COLORS.NOTIFY_LOW_BG,
+ fg = COLORS.NOTIFY_LOW_FG,
+ width = 300,
+ })
+end
+
+function BatteryWidget:_update()
+ local capacity = self._bat.capacity
+ if not capacity then
+ local cl = self._bat.capacity_level
+ if cl == 'Full' then capacity = 100
+ elseif cl == 'High' then capacity = 75
+ elseif cl == 'Normal' then capacity = 50
+ elseif cl == 'Low' then capacity = 25
+ elseif cl == 'Critical' then capacity = 5
+ end
+ end
+
+ -- hide the widget if the battery is full
+ if self._bat.status == self._bat.STATUS.FULL then
+ self._placeholder:set_children({})
+ return
+ end
+
+ -- show the widget if it was previously hidden
+ if next(self._placeholder.children) == nil then
+ self._placeholder:set_children({self._arc})
+ end
+
+ if self._level_text ~= nil then
+ --- if battery is fully charged (100) there is not enough place for three digits, so we don't show any text
+ self._level_text.text = (capacirty and capacity >= 100) and '' or
+ string.format('%d', capacity // 1)
+ end
+
+ if self._bat.status == self._bat.STATUS.CHARGING then
+ self._arc.bg = COLORS.CHARGING
+ else
+ self._arc.bg = COLORS.MAIN_BG
+ end
+
+ if capacity then
+ self._arc.value = capacity
+
+ if capacity < 15 then
+ self._arc.colors = { COLORS.CHARGE_LOW }
+ if self._bat.status == self._bat.STATUS.DISCHARGING and
+ os.difftime(os.time(), self._last_battery_check) > 300 then
+ self:_show_battery_warning(capacity)
+ self._last_battery_check = os.time()
+ end
+ elseif capacity < 40 then
+ self._arc.colors = { COLORS.CHARGE_MEDIUM }
+ else
+ self._arc.colors = { COLORS.MAIN_FG }
+ end
+ end
+
+ -- build tooltip text
+ -- start with name + description
+ local tooltip_text = self._bat.name
+ if self._bat.desc then
+ tooltip_text = string.format('%s (%s)', tooltip_text, self._bat.desc)
+ end
+
+ -- percentage/capacity level
+ if self._bat.capacity then
+ tooltip_text = string.format('%s: %d%%', tooltip_text, self._bat.capacity)
+ elseif self._bat.capacity_level then
+ tooltip_text = string.format('%s: %s', tooltip_text, self._bat.capacity_level)
+ end
+
+ -- status + power (if available)
+ tooltip_text = string.format('%s; %s', tooltip_text, self._bat.status)
+ if self._bat.power then
+ tooltip_text = string.format('%s at %.3gW', tooltip_text, self._bat.power)
+ end
+
+ if self._bat.remaining_seconds then
+ local sec = self._bat.remaining_seconds
+
+ local h = sec // 3600
+ sec = sec - 3600 * h
+
+ local m = sec // 60
+ sec = sec - 60 * m
+
+ local s = sec // 1
+
+ local endstate = self._bat.status == self._bat.STATUS.DISCHARGING and 'empty' or 'full'
+ tooltip_text = string.format('%s; %02d:%02d:%02d until %s', tooltip_text, h, m, s, endstate)
+ end
+ self._tooltip.text = tooltip_text
+end
+
+function BatteryWidget.new(battery, user_args)
+ local self = setmetatable({}, BatteryWidget)
+
+ local args = user_args or {}
+
+ self._bat = battery
+
+ -- optional textbox showing battery charge percentage
+ local show_current_level = args.show_current_level or false
+ if show_current_level then
+ self._level_text = wibox.widget.textbox()
+ end
+
+ -- arc indicating battery charge level
+ self._arc = wibox.widget {
+ self._level_text,
+ max_value = 100,
+ rounded_edge = true,
+ thickness = args.arc_thickness or 4,
+ start_angle = 4.71238898, -- 2pi*3/4
+ bg = COLORS.MAIN_BG,
+ paddings = 2,
+ widget = wibox.container.arcchart
+ }
+
+ -- placeholder layout used to hide the widget on full battery
+ self._placeholder = wibox.layout.flex.horizontal()
+
+ -- the actually returned widget, showing the battery only on the primary screen
+ local ret_widget = awful.widget.only_on_screen(self._placeholder, "primary")
+
+ -- tooltip showing extended battery status
+ self._tooltip = awful.tooltip({})
+ self._tooltip:add_to_object(ret_widget)
+
+ self._last_battery_check = os.time()
+
+ -- wibox.widget.imagebox
+ -- local it = menubar_it()
+ -- print(it:find_icon_path('battery-full', 32))
+
+ battery:connect_signal('updated', function () self:_update() end)
+
+ return ret_widget
+end
+
+return BatteryWidget
diff --git a/rc.lua b/rc.lua
index 43d5fe3..2bf47ca 100644
--- a/rc.lua
+++ b/rc.lua
@@ -16,8 +16,9 @@ local utils = require("utils")
local workspace = require("workspace")
-- local widgets
-local pager = require("pager")
-local urgent_wgt = require("urgent_wgt")
+local BatteryManager = require("battery_mng")
+local pager = require("pager")
+local urgent_wgt = require("urgent_wgt")
-- {{{ Error handling
-- Check if awesome encountered an error during startup and fell back to
@@ -79,6 +80,8 @@ update_systray_iconsize()
-- global, shown only on the primary screen
local u_wgt = urgent_wgt.UrgentWidget:new()
+local b_mng = BatteryManager()
+
-- Create a wibox for each screen and add it
mywibox = {}
mypromptbox = {}
@@ -103,12 +106,14 @@ awful.screen.connect_for_each_screen(function(s)
awful.button({ }, 3, function () awful.layout.inc(layouts, -1) end),
awful.button({ }, 4, function () awful.layout.inc(layouts, 1) end),
awful.button({ }, 5, function () awful.layout.inc(layouts, -1) end)))
- local layoutbox_container = wibox.container.constraint(layoutbox, 'max', beautiful.xresources.apply_dpi(tray_icon_size))
local layout_utils = wibox.layout.fixed.horizontal()
- layout_utils:add(layoutbox_container)
+ layout_utils:add(layoutbox)
+ layout_utils:add(b_mng)
layout_utils:add(u_wgt.widget)
+ local utils_bar = wibox.container.constraint(layout_utils, 'max', nil, beautiful.xresources.apply_dpi(tray_icon_size))
+
-- Create a tasklist widget
local pgr = pager.Pager:new(wsp, s)
@@ -117,7 +122,7 @@ awful.screen.connect_for_each_screen(function(s)
layout_bottom:add(systray)
- layout_bottom:add(layout_utils)
+ layout_bottom:add(utils_bar)
layout_bottom:add(clock_time)
layout_bottom:add(clock_date)