Files

111 lines
4.7 KiB
Python

import functools
import logging
import time
try:
import psutil
except ImportError:
psutil = None
logger = logging.getLogger(__name__)
def _check_arguments(minimum, resume, wait_interval):
if resume is None:
resume = minimum
if not 0 <= minimum <= 100:
raise ValueError(f"minimum ({minimum}) must be between 0 and 100")
if not minimum <= resume <= 100:
raise ValueError(f"resume ({resume}) must be between minimum ({minimum}) and 100")
if wait_interval <= 0:
raise ValueError(f"wait_interval ({wait_interval}) must be positive")
return resume
def wait_for_battery(minimum=20, resume=None, wait_interval=5):
"""
Return right away if the battery is at least `minimum` percent.
If it is below `minimum`, block until it has charged back up to `resume`
percent (defaults to `minimum`). While blocking, it checks the battery every
`wait_interval` seconds (5 by default).
This is useful where there is no function to decorate, for example in a notebook cell.
See `battery_guard` for the decorator version and advice on choosing `minimum`.
"""
resume = _check_arguments(minimum, resume, wait_interval)
if psutil is None:
logger.warning("Missing psutil library, not sleeping because battery percentage is unknown")
return
waited = False
while True:
try:
battery = psutil.sensors_battery()
if battery is None:
logger.debug("psutil.sensors_battery() reports no battery, resuming")
return
threshold = resume if waited else minimum
if battery.percent >= threshold: # noqa, None comparison can't be reached because of the return above
if waited:
logger.info("Battery at %s%%, resuming", battery.percent)
return
if not waited:
logger.info("Battery at %s%%, waiting until it reaches %s%%",
battery.percent, resume)
waited = True
logger.debug("Battery at %s%%, still waiting", battery.percent)
time.sleep(wait_interval)
except Exception: # noqa: unsupported platform / sensor read failure / who knows -> log it!
logger.warning("Could not read battery status, not sleeping", exc_info=True)
return
def battery_guard(func=None, *, minimum=20, resume=None, wait_interval=5):
"""
Call the function right away if the battery is at least `minimum` percent.
If it is below `minimum`, block until it has charged back up to `resume`
percent (defaults to `minimum`), then call the function.
By default it will check every 5 seconds (`wait_interval`) before it continues.
The battery is only checked when the decorated function is called, a call that
is already running is never paused. It therefore works best on a function that
is called repeatedly, like the body of a loop that processes one item at a time,
rather than on one long-running function. To block without decorating anything,
use `wait_for_battery`.
Note that battery charging slows down when it's near a full charge.
It also may produce more heat and consume more power.
Therefore it's probably better to not set minimum too high.
The recommended range for minimum is between 20 and 80 percent.
There are valid reasons to set minimum higher though, you simply might want
to be able to take it with you and expect it to be charged as much as possible.
Or maybe you want a heavy background task to pause once you leave the office,
and the battery dipping below 95% is the indication that you have left for the train.
Setting it to 100% might also give unexpected results, because some battery management
systems will not start charging until the reported percentage drops below 95% for example.
The percentage psutil.sensors_battery() reports also isn't necessarily the same as the
percentage displayed in your system tray.
This method does not consider whether a charger is plugged in. That is on purpose.
The reason it exists is to deal with chargers that are too slow; a situation
where battery percentage decreases despite having a charger plugged in.
Usable as:
@battery_guard
@battery_guard(minimum=20, resume=25, wait_interval=30)
battery_guard(some_func, minimum=50)(arg1, arg2)
"""
resume = _check_arguments(minimum, resume, wait_interval)
def decorator(f):
@functools.wraps(f)
def wrapper(*args, **kwargs):
wait_for_battery(minimum=minimum, resume=resume, wait_interval=wait_interval)
return f(*args, **kwargs)
return wrapper
return decorator if func is None else decorator(func)