Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OSX: implement sensors_battery #1177

Merged
merged 1 commit into from
Nov 19, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion psutil/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2210,7 +2210,7 @@ def sensors_fans():
__all__.append("sensors_fans")


# Linux, Windows, FreeBSD
# Linux, Windows, FreeBSD, OSX
if hasattr(_psplatform, "sensors_battery"):

def sensors_battery():
Expand Down
23 changes: 23 additions & 0 deletions psutil/_psosx.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,29 @@ def disk_partitions(all=False):
return retlist


# =====================================================================
# --- sensors
# =====================================================================


def sensors_battery():
"""Return battery information.
"""
try:
percent, minsleft, power_plugged = cext.sensors_battery()
except NotImplementedError:
# no power source - return None according to interface
return None
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice solution, I like it

power_plugged = power_plugged == 1
if power_plugged:
secsleft = _common.POWER_TIME_UNLIMITED
elif minsleft == -1:
secsleft = _common.POWER_TIME_UNKNOWN
else:
secsleft = minsleft * 60
return _common.sbattery(percent, secsleft, power_plugged)


# =====================================================================
# --- network
# =====================================================================
Expand Down
90 changes: 90 additions & 0 deletions psutil/_psutil_osx.c
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@
#include <IOKit/storage/IOBlockStorageDriver.h>
#include <IOKit/storage/IOMedia.h>
#include <IOKit/IOBSD.h>
#include <IOKit/ps/IOPowerSources.h>
#include <IOKit/ps/IOPSKeys.h>

#include "_psutil_common.h"
#include "_psutil_posix.h"
Expand Down Expand Up @@ -1788,6 +1790,92 @@ psutil_cpu_stats(PyObject *self, PyObject *args) {
}


/*
* Return battery information.
*/
static PyObject *
psutil_sensors_battery(PyObject *self, PyObject *args) {
PyObject *py_tuple = NULL;
CFTypeRef power_info = NULL;
CFArrayRef power_sources_list = NULL;
CFDictionaryRef power_sources_information = NULL;
CFNumberRef capacity_ref = NULL;
CFNumberRef time_to_empty_ref = NULL;
CFStringRef ps_state_ref = NULL;
uint32_t capacity; /* units are percent */
int time_to_empty; /* units are minutes */
int is_power_plugged;

power_info = IOPSCopyPowerSourcesInfo();

if (!power_info) {
PyErr_SetString(PyExc_RuntimeError,
"IOPSCopyPowerSourcesInfo() syscall failed");
goto error;
}

power_sources_list = IOPSCopyPowerSourcesList(power_info);
if (!power_sources_list) {
PyErr_SetString(PyExc_RuntimeError,
"IOPSCopyPowerSourcesList() syscall failed");
goto error;
}

/* Should only get one source. But in practice, check for > 0 sources */
if (!CFArrayGetCount(power_sources_list)) {
PyErr_SetString(PyExc_NotImplementedError, "no battery");
goto error;
}

power_sources_information = IOPSGetPowerSourceDescription(
power_info, CFArrayGetValueAtIndex(power_sources_list, 0));

capacity_ref = (CFNumberRef) CFDictionaryGetValue(
power_sources_information, CFSTR(kIOPSCurrentCapacityKey));
if (!CFNumberGetValue(capacity_ref, kCFNumberSInt32Type, &capacity)) {
PyErr_SetString(PyExc_RuntimeError,
"No battery capacity infomration in power sources info");
goto error;
}

ps_state_ref = (CFStringRef) CFDictionaryGetValue(
power_sources_information, CFSTR(kIOPSPowerSourceStateKey));
is_power_plugged = CFStringCompare(
ps_state_ref, CFSTR(kIOPSACPowerValue), 0)
== kCFCompareEqualTo;

time_to_empty_ref = (CFNumberRef) CFDictionaryGetValue(
power_sources_information, CFSTR(kIOPSTimeToEmptyKey));
if (!CFNumberGetValue(time_to_empty_ref,
kCFNumberIntType, &time_to_empty)) {
/* This value is recommended for non-Apple power sources, so it's not
* an error if it doesn't exist. We'll return -1 for "unknown" */
/* A value of -1 indicates "Still Calculating the Time" also for
* apple power source */
time_to_empty = -1;
}

py_tuple = Py_BuildValue("Iii",
capacity, time_to_empty, is_power_plugged);
if (!py_tuple) {
goto error;
}

CFRelease(power_info);
CFRelease(power_sources_list);
/* Caller should NOT release power_sources_information */

return py_tuple;

error:
if (power_info)
CFRelease(power_info);
if (power_sources_list)
CFRelease(power_sources_list);
Py_XDECREF(py_tuple);
return NULL;
}


/*
* define the psutil C module methods and initialize the module.
Expand Down Expand Up @@ -1854,6 +1942,8 @@ PsutilMethods[] = {
"Return currently connected users as a list of tuples"},
{"cpu_stats", psutil_cpu_stats, METH_VARARGS,
"Return CPU statistics"},
{"sensors_battery", psutil_sensors_battery, METH_VARARGS,
"Return battery information."},

// --- others
{"set_testing", psutil_set_testing, METH_NOARGS,
Expand Down
2 changes: 1 addition & 1 deletion psutil/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
'VERBOSITY',
"HAS_CPU_AFFINITY", "HAS_CPU_FREQ", "HAS_ENVIRON", "HAS_PROC_IO_COUNTERS",
"HAS_IONICE", "HAS_MEMORY_MAPS", "HAS_PROC_CPU_NUM", "HAS_RLIMIT",
"HAS_SENSORS_BATTERY", "HAS_BATTERY""HAS_SENSORS_FANS",
"HAS_SENSORS_BATTERY", "HAS_BATTERY", "HAS_SENSORS_FANS",
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch

"HAS_SENSORS_TEMPERATURES", "HAS_MEMORY_FULL_INFO",
# subprocesses
'pyrun', 'reap_children', 'get_test_subprocess', 'create_zombie_proc',
Expand Down
2 changes: 1 addition & 1 deletion psutil/tests/test_contracts.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ def test_sensors_fans(self):

def test_battery(self):
self.assertEqual(hasattr(psutil, "sensors_battery"),
LINUX or WINDOWS or FREEBSD)
LINUX or WINDOWS or FREEBSD or OSX)

def test_proc_environ(self):
self.assertEqual(hasattr(psutil.Process, "environ"),
Expand Down
13 changes: 13 additions & 0 deletions psutil/tests/test_osx.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from psutil import OSX
from psutil.tests import create_zombie_proc
from psutil.tests import get_test_subprocess
from psutil.tests import HAS_BATTERY
from psutil.tests import MEMORY_TOLERANCE
from psutil.tests import reap_children
from psutil.tests import retry_before_failing
Expand Down Expand Up @@ -285,6 +286,18 @@ def test_net_if_stats(self):
self.assertEqual(stats.mtu,
int(re.findall(r'mtu (\d+)', out)[0]))

# --- sensors_battery

@unittest.skipIf(not HAS_BATTERY, "no battery")
def test_sensors_battery(self):
out = sh("pmset -g batt")
percent = re.search("(\d+)%", out).group(1)
drawing_from = re.search("Now drawing from '([^']+)'", out).group(1)
power_plugged = drawing_from == "AC Power"
psutil_result = psutil.sensors_battery()
self.assertEqual(psutil_result.power_plugged, power_plugged)
self.assertEqual(psutil_result.percent, int(percent))
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sweet!



if __name__ == '__main__':
run_test_module_by_name(__file__)