111 lines
4.7 KiB
Python
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)
|