Improve battery.el UPower support

For discussion, see the following threads:
https://lists.gnu.org/archive/html/emacs-devel/2020-01/msg00843.html
https://lists.gnu.org/archive/html/emacs-devel/2020-02/msg00042.html
https://lists.gnu.org/archive/html/emacs-devel/2020-02/msg00282.html

* etc/NEWS: Announce that battery-upower is enabled by default.

* lisp/battery.el (battery-upower-device): Accept both battery and
line power device names, or a list thereof (bug#39491).
(battery-upower-line-power-device): Remove user option; superseded
by battery-upower-device.
(battery-upower-subscribe): New user option.
(battery-status-function): Check whether a UPower service is
provided without activating it.
(display-battery-mode): Subscribe to UPower signals when using
battery-upower.
(battery-upower): Merge data from multiple power sources.  Calculate
terse battery status %b based on average battery load percentage
rather than coarse and often missing BatteryLevel (bug#39491).  Add
support for average temperature %d.

(battery-upower-dbus-service)
(battery-upower-dbus-interface)
(battery-upower-dbus-path)
(battery-upower-dbus-device-interface)
(battery-upower-dbus-device-path)
(battery-upower-device-all-properties): Rename to...
(battery-upower-service)
(battery-upower-interface)
(battery-upower-path)
(battery-upower-device-interface)
(battery-upower-device-path)
(battery--upower-device-properties): ...these, respectively.

(battery-upower-device-list): Rename to...
(battery--upower-devices) ...this.  Return a flat list of device
names determined by battery-upower-device.
(battery-upower-types, battery-upower-states)
(battery-upower-device-property, battery-upower-device-autodetect):
Remove.
(battery--upower-signals): New variable.
(battery--upower-signal-handler, battery--upower-props-changed)
(battery--upower-unsubscribe, battery--upower-subsribe)
(battery--upower-state): New functions.

* test/lisp/battery-tests.el (battery-upower-state)
(battery-upower-state-unknown): New tests.
This commit is contained in:
Basil L. Contovounesios 2020-06-11 13:49:31 +01:00
parent 23a148c950
commit 453d30d92c
3 changed files with 250 additions and 105 deletions

View file

@ -463,6 +463,16 @@ have now been removed.
** Battery
---
*** UPower is now the default battery status backend when available.
UPower support via the function 'battery-upower' was added in Emacs
26.1, but was disabled by default. It is now the default value of
'battery-status-function' when the system provides a UPower D-Bus
service. The user options 'battery-upower-device' and
'battery-upower-subscribe' control which power sources to query and
whether to respond to status change notifications in addition to
polling, respectively.
---
*** A richer syntax can be used to format battery status information.
The user options 'battery-mode-line-format' and

View file

@ -44,21 +44,40 @@
:group 'hardware)
(defcustom battery-upower-device nil
"UPower device of the `:battery' type.
Use `battery-upower-device-list' to list all available UPower devices.
If set to nil, then autodetect `:battery' device."
:version "28.1"
:type '(choice string (const :tag "Autodetect" nil)))
"Preferred UPower device name(s).
When `battery-status-function' is set to `battery-upower', this
user option specifies which power sources to query for status
information and merge into a single report.
(defcustom battery-upower-line-power-device nil
"UPower device of the `:line-power' type.
Use `battery-upower-device-list' to list all available UPower devices.
If set to nil, then autodetect `:battery' device."
When nil (the default), `battery-upower' queries all present
battery and line power devices as determined by the UPower
EnumerateDevices method. A string or a nonempty list of strings
names particular devices to query instead. UPower battery and
line power device names typically follow the patterns
\"battery_BATN\" and \"line_power_ACN\", respectively, with N
starting at 0 when present. Device names should not include the
leading D-Bus path \"/org/freedesktop/UPower/devices/\"."
:version "28.1"
:type '(choice string (const :tag "Autodetect" nil)))
:type '(choice (const :tag "Autodetect all devices" nil)
(string :tag "Device")
(repeat :tag "Devices" string)))
(defconst battery-upower-dbus-service "org.freedesktop.UPower"
"Well-known UPower service name for the D-Bus system.")
(defcustom battery-upower-subscribe t
"Whether to subscribe to UPower device change signals.
When nil, battery status information is polled every
`battery-update-interval' seconds. When non-nil (the default),
the battery status is also updated whenever a power source is
added or removed, or when the system starts or stops running on
battery power.
This only takes effect when `battery-status-function' is set to
`battery-upower' before enabling `display-battery-mode'."
:version "28.1"
:type 'boolean)
(defconst battery-upower-service "org.freedesktop.UPower"
"Well-known name of the UPower D-Bus service.
See URL `https://upower.freedesktop.org/docs/ref-dbus.html'.")
(defun battery--files (dir)
"Return a list of absolute file names in DIR or nil on error.
@ -74,7 +93,7 @@ Value does not include \".\" or \"..\"."
(nreverse dirs)))
(defcustom battery-status-function
(cond ((dbus-ping :system battery-upower-dbus-service)
(cond ((member battery-upower-service (dbus-list-activatable-names))
#'battery-upower)
((and (eq system-type 'gnu/linux)
(battery--find-linux-sysfs-batteries))
@ -219,11 +238,15 @@ seconds."
(setq battery-mode-line-string "")
(or global-mode-string (setq global-mode-string '("")))
(and battery-update-timer (cancel-timer battery-update-timer))
(battery--upower-unsubscribe)
(if (and battery-status-function battery-mode-line-format)
(if (not display-battery-mode)
(setq global-mode-string
(delq 'battery-mode-line-string global-mode-string))
(add-to-list 'global-mode-string 'battery-mode-line-string t)
(and (eq battery-status-function #'battery-upower)
battery-upower-subscribe
(battery--upower-subsribe))
(setq battery-update-timer (run-at-time nil battery-update-interval
#'battery-update-handler))
(battery-update))
@ -577,123 +600,172 @@ The following %-sequences are provided:
(_ "N/A"))))))
;;; `upowerd' interface.
(defconst battery-upower-dbus-interface "org.freedesktop.UPower"
"The interface to UPower.
See URL `https://upower.freedesktop.org/docs/'.")
;;; UPower interface.
(defconst battery-upower-dbus-path "/org/freedesktop/UPower"
"D-Bus path to talk to UPower service.")
(defconst battery-upower-interface "org.freedesktop.UPower"
"Name of the UPower D-Bus interface.
See URL `https://upower.freedesktop.org/docs/UPower.html'.")
(defconst battery-upower-dbus-device-interface
(concat battery-upower-dbus-interface ".Device")
"The Device interface of the UPower.
(defconst battery-upower-path "/org/freedesktop/UPower"
"D-Bus object providing `battery-upower-interface'.")
(defconst battery-upower-device-interface "org.freedesktop.UPower.Device"
"Name of the UPower Device D-Bus interface.
See URL `https://upower.freedesktop.org/docs/Device.html'.")
(defconst battery-upower-dbus-device-path
(concat battery-upower-dbus-path "/devices")
"D-Bus path to talk to devices part of the UPower service.")
(defconst battery-upower-device-path "/org/freedesktop/UPower/devices"
"D-Bus object providing `battery-upower-device-interface'.")
(defconst battery-upower-types
'((0 . :unknown) (1 . :line-power) (2 . :battery)
(3 . :ups) (4 . :monitor) (5 . :mouse)
(6 . :keyboard) (7 . :pda) (8 . :phone))
"Type of the device.")
(defvar battery--upower-signals nil
"Handles for UPower signal subscriptions.")
(defconst battery-upower-states
'((0 . "unknown") (1 . "charging") (2 . "discharging")
(3 . "empty") (4 . "fully-charged") (5 . "pending-charge")
(6 . "pending-discharge"))
"Alist of battery power states.
Only valid for `:battery' devices.")
(defun battery--upower-signal-handler (&rest _)
"Update battery status on receiving a UPower D-Bus signal."
(timer-event-handler battery-update-timer))
(defun battery-upower-device-property (device property)
"Get value of the single PROPERTY for the UPower DEVICE."
(dbus-get-property
:system battery-upower-dbus-service
(expand-file-name device battery-upower-dbus-device-path)
battery-upower-dbus-device-interface
property))
(defun battery--upower-props-changed (_interface changed _invalidated)
"Update status when system starts/stops running on battery.
Intended as a UPower PropertiesChanged signal handler."
(when (assoc "OnBattery" changed)
(battery--upower-signal-handler)))
(defun battery-upower-device-all-properties (device)
(defun battery--upower-unsubscribe ()
"Unsubscribe from UPower device change signals."
(mapc #'dbus-unregister-object battery--upower-signals)
(setq battery--upower-signals ()))
(defun battery--upower-subsribe ()
"Subscribe to UPower device change signals."
(push (dbus-register-signal :system battery-upower-service
battery-upower-path
dbus-interface-properties
"PropertiesChanged"
#'battery--upower-props-changed)
battery--upower-signals)
(dolist (method '("DeviceAdded" "DeviceRemoved"))
(push (dbus-register-signal :system battery-upower-service
battery-upower-path
battery-upower-interface
method #'battery--upower-signal-handler)
battery--upower-signals)))
(defun battery--upower-device-properties (device)
"Return value for all available properties for the UPower DEVICE."
(dbus-get-all-properties
:system battery-upower-dbus-service
(expand-file-name device battery-upower-dbus-device-path)
battery-upower-dbus-device-interface))
:system battery-upower-service
(expand-file-name device battery-upower-device-path)
battery-upower-device-interface))
(defun battery-upower-device-list ()
"Return list of all available UPower devices.
Each element is the cons cell in form: (DEVICE . DEVICE-TYPE)."
(mapcar (lambda (device-path)
(let* ((device (file-relative-name
device-path battery-upower-dbus-device-path))
(type-num (battery-upower-device-property device "Type")))
(cons device (or (cdr (assq type-num battery-upower-types))
:unknown))))
(dbus-call-method :system battery-upower-dbus-service
battery-upower-dbus-path
battery-upower-dbus-interface
"EnumerateDevices")))
(defun battery--upower-devices ()
"List all UPower devices according to `battery-upower-device'."
(cond ((stringp battery-upower-device)
(list battery-upower-device))
(battery-upower-device)
((dbus-call-method :system battery-upower-service
battery-upower-path
battery-upower-interface
"EnumerateDevices"))))
(defun battery-upower-device-autodetect (device-type)
"Return first matching UPower device of DEVICE-TYPE."
(car (rassq device-type (battery-upower-device-list))))
(defun battery--upower-state (props state)
"Merge the UPower battery state in PROPS with STATE.
This is an extension of the UPower DisplayDevice algorithm for
merging multiple battery states into one. PROPS is an alist of
battery properties from `battery-upower-device-interface', and
STATE is a symbol representing the state to merge with."
;; Map UPower enum into our printable symbols.
(let* ((new (pcase (cdr (assoc "State" props))
(1 'charging)
(2 'discharging)
(3 'empty)
(4 'fully-charged)
(5 'pending-charge)
(6 'pending-discharge)))
;; Unknown state represented by nil.
(either (delq nil (list new state))))
;; Earlier states override later ones.
(car (cond ((memq 'charging either))
((memq 'discharging either))
((memq 'pending-charge either))
((memq 'pending-discharge either))
;; Only options left are full or empty,
;; but if they conflict return nil.
((null (cdr either)) either)
((apply #'eq either) either)))))
(defun battery-upower ()
"Get battery status from dbus Upower interface.
This function works only in systems with `upowerd' daemon
running.
"Get battery status from UPower D-Bus interface.
This function works only in systems that provide a UPower D-Bus
service.
The following %-sequences are provided:
%c Current capacity (mWh)
%p Battery load percentage
%r Current rate
%r Current rate of charge or discharge
%L AC line status (verbose)
%B Battery status (verbose)
%b Battery status: empty means high, `-' means low,
`!' means critical, and `+' means charging
%L AC line status (verbose)
%d Temperature (in degrees Celsius)
%p Battery load percentage
%s Remaining time (to charge or discharge) in seconds
%m Remaining time (to charge or discharge) in minutes
%h Remaining time (to charge or discharge) in hours
%t Remaining time (to charge or discharge) in the form `h:min'"
(let* ((bat-device (or battery-upower-device
(battery-upower-device-autodetect :battery)))
(bat-props (when bat-device
(battery-upower-device-all-properties bat-device)))
(percents (cdr (assoc "Percentage" bat-props)))
(time-to-empty (cdr (assoc "TimeToEmpty" bat-props)))
(time-to-full (cdr (assoc "TimeToFull" bat-props)))
(state (cdr (assoc "State" bat-props)))
(level (cdr (assoc "BatteryLevel" bat-props)))
(energy (cdr (assoc "Energy" bat-props)))
(energy-rate (cdr (assoc "EnergyRate" bat-props)))
(lp-device (or battery-upower-line-power-device
(battery-upower-device-autodetect :line-power)))
(online-p (when lp-device
(battery-upower-device-property lp-device "Online")))
(seconds (if online-p time-to-full time-to-empty))
(minutes (when seconds (/ seconds 60)))
(hours (when minutes (/ minutes 60)))
(remaining-time (when hours
(format "%d:%02d" hours (mod minutes 60)))))
(list (cons ?c (if energy (number-to-string (round (* 1000 energy))) "N/A"))
(cons ?p (if percents (number-to-string (round percents)) "N/A"))
(cons ?r (if energy-rate
(concat (number-to-string energy-rate) " W")
(let ((count 0) props type line-status state load temperature
secs mins hrs total-energy total-rate total-tte total-ttf)
;; Merge information from all available or specified UPower
;; devices like other `battery-status-function's.
(dolist (device (battery--upower-devices))
(setq props (battery--upower-device-properties device))
(setq type (cdr (assoc "Type" props)))
(cond
((and (eq type 1) (not (eq line-status 'online)))
;; It's a line power device: `online' if currently providing
;; power, any other non-nil value if simply present.
(setq line-status (if (cdr (assoc "Online" props)) 'online t)))
((and (eq type 2) (cdr (assoc "IsPresent" props)))
;; It's a battery.
(setq count (1+ count))
(setq state (battery--upower-state props state))
(let ((energy (cdr (assoc "Energy" props)))
(rate (cdr (assoc "EnergyRate" props)))
(percent (cdr (assoc "Percentage" props)))
(temp (cdr (assoc "Temperature" props)))
(tte (cdr (assoc "TimeToEmpty" props)))
(ttf (cdr (assoc "TimeToFull" props))))
(when energy (setq total-energy (+ (or total-energy 0) energy)))
(when rate (setq total-rate (+ (or total-rate 0) rate)))
(when percent (setq load (+ (or load 0) percent)))
(when temp (setq temperature (+ (or temperature 0) temp)))
(when tte (setq total-tte (+ (or total-tte 0) tte)))
(when ttf (setq total-ttf (+ (or total-ttf 0) ttf)))))))
(when (> count 1)
;; Averages over multiple batteries.
(when load (setq load (/ load count)))
(when temperature (setq temperature (/ temperature count))))
(when (setq secs (if (eq line-status 'online) total-ttf total-tte))
(setq mins (/ secs 60))
(setq hrs (/ secs 3600)))
(list (cons ?c (if total-energy
(format "%.0f" (* total-energy 1000))
"N/A"))
(cons ?B (if state
(cdr (assq state battery-upower-states))
"unknown"))
(cons ?b (cond ((= level 3) "-")
((= level 4) "!")
(online-p "+")
(t "")))
(cons ?L (if online-p "on-line" (if lp-device "off-line" "unknown")))
(cons ?s (if seconds (number-to-string seconds) "N/A"))
(cons ?m (if minutes (number-to-string minutes) "N/A"))
(cons ?h (if hours (number-to-string hours) "N/A"))
(cons ?t (or remaining-time "N/A")))))
(cons ?r (if total-rate (format "%.1f W" total-rate) "N/A"))
(cons ?L (cond ((eq line-status 'online) "on-line")
(line-status "off-line")
("N/A")))
(cons ?B (format "%s" (or state 'unknown)))
(cons ?b (cond ((eq state 'charging) "+")
((and load (< load battery-load-critical)) "!")
((and load (< load battery-load-low)) "-")
("")))
;; Zero usually means unknown.
(cons ?d (if (and temperature (/= temperature 0))
(format "%.0f" temperature)
"N/A"))
(cons ?p (if load (format "%.0f" load) "N/A"))
(cons ?s (if secs (number-to-string secs) "N/A"))
(cons ?m (if mins (number-to-string mins) "N/A"))
(cons ?h (if hrs (number-to-string hrs) "N/A"))
(cons ?t (if hrs (format "%d:%02d" hrs (% mins 60)) "N/A")))))
;;; `apm' interface for BSD.

View file

@ -81,6 +81,69 @@
(should (equal (match-string 2 str) "mWh")))
(should-not (string-match (rx battery--acpi-capacity eos) "45 mW")))
(ert-deftest battery-upower-state ()
"Test `battery--upower-state'."
;; Charging.
(dolist (total '(nil charging discharging empty fully-charged
pending-charge pending-discharge))
(should (eq (battery--upower-state '(("State" . 1)) total) 'charging)))
(dolist (state '(nil 0 1 2 3 4 5 6))
(should (eq (battery--upower-state `(("State" . ,state)) 'charging)
'charging)))
;; Discharging.
(dolist (total '(nil discharging empty fully-charged
pending-charge pending-discharge))
(should (eq (battery--upower-state '(("State" . 2)) total) 'discharging)))
(dolist (state '(nil 0 2 3 4 5 6))
(should (eq (battery--upower-state `(("State" . ,state)) 'discharging)
'discharging)))
;; Pending charge.
(dolist (total '(nil empty fully-charged pending-charge pending-discharge))
(should (eq (battery--upower-state '(("State" . 5)) total)
'pending-charge)))
(dolist (state '(nil 0 3 4 5 6))
(should (eq (battery--upower-state `(("State" . ,state)) 'pending-charge)
'pending-charge)))
;; Pending discharge.
(dolist (total '(nil empty fully-charged pending-discharge))
(should (eq (battery--upower-state '(("State" . 6)) total)
'pending-discharge)))
(dolist (state '(nil 0 3 4 6))
(should (eq (battery--upower-state `(("State" . ,state)) 'pending-discharge)
'pending-discharge)))
;; Empty.
(dolist (total '(nil empty))
(should (eq (battery--upower-state '(("State" . 3)) total) 'empty)))
(dolist (state '(nil 0 3))
(should (eq (battery--upower-state `(("State" . ,state)) 'empty) 'empty)))
;; Fully charged.
(dolist (total '(nil fully-charged))
(should (eq (battery--upower-state '(("State" . 4)) total) 'fully-charged)))
(dolist (state '(nil 0 4))
(should (eq (battery--upower-state `(("State" . ,state)) 'fully-charged)
'fully-charged))))
(ert-deftest battery-upower-state-unknown ()
"Test `battery--upower-state' with unknown states."
;; Unknown running total retains new state.
(should-not (battery--upower-state () nil))
(should-not (battery--upower-state '(("State" . state)) nil))
(should-not (battery--upower-state '(("State" . 0)) nil))
(should (eq (battery--upower-state '(("State" . 1)) nil) 'charging))
(should (eq (battery--upower-state '(("State" . 2)) nil) 'discharging))
(should (eq (battery--upower-state '(("State" . 3)) nil) 'empty))
(should (eq (battery--upower-state '(("State" . 4)) nil) 'fully-charged))
(should (eq (battery--upower-state '(("State" . 5)) nil) 'pending-charge))
(should (eq (battery--upower-state '(("State" . 6)) nil) 'pending-discharge))
;; Unknown new state retains running total.
(dolist (props '(() (("State" . state)) (("State" . 0))))
(dolist (total '(nil charging discharging empty fully-charged
pending-charge pending-discharge))
(should (eq (battery--upower-state props total) total))))
;; Conflicting empty and fully-charged.
(should-not (battery--upower-state '(("State" . 3)) 'fully-charged))
(should-not (battery--upower-state '(("State" . 4)) 'empty)))
(ert-deftest battery-format ()
"Test `battery-format'."
(should (equal (battery-format "" ()) ""))