summaryrefslogtreecommitdiffstats
path: root/src/detection/battery
diff options
context:
space:
mode:
authorsumuel <samuel@yakubos.org>2026-08-17 20:44:55 +0000
committersumuel <samuel@yakubos.org>2026-08-17 20:44:55 +0000
commit76424950e373d3b04ac3dd13019151bfba3e8423 (patch)
tree4c3cfdbda039e592b9186be3e28f8d7cfd439e8a /src/detection/battery
Add the files
Diffstat (limited to 'src/detection/battery')
-rw-r--r--src/detection/battery/battery.h32
-rw-r--r--src/detection/battery/battery_android.c160
-rw-r--r--src/detection/battery/battery_apple.c117
-rw-r--r--src/detection/battery/battery_bsd.c90
-rw-r--r--src/detection/battery/battery_haiku.c71
-rw-r--r--src/detection/battery/battery_linux.c203
-rw-r--r--src/detection/battery/battery_nbsd.c109
-rw-r--r--src/detection/battery/battery_nosupport.c7
-rw-r--r--src/detection/battery/battery_obsd.c60
-rw-r--r--src/detection/battery/battery_windows.c384
10 files changed, 1233 insertions, 0 deletions
diff --git a/src/detection/battery/battery.h b/src/detection/battery/battery.h
new file mode 100644
index 0000000..1a3b4e9
--- /dev/null
+++ b/src/detection/battery/battery.h
@@ -0,0 +1,32 @@
+#pragma once
+
+#include "fastfetch.h"
+#include "modules/battery/option.h"
+
+#define FF_BATTERY_TEMP_UNSET (-DBL_MAX)
+
+typedef enum FFBatteryStatus {
+ FF_BATTERY_STATUS_NONE = 0,
+ FF_BATTERY_STATUS_UNKNOWN = 1 << 0,
+ FF_BATTERY_STATUS_AC_CONNECTED = 1 << 1,
+ FF_BATTERY_STATUS_USB_CONNECTED = 1 << 2,
+ FF_BATTERY_STATUS_WIRELESS_CONNECTED = 1 << 3,
+ FF_BATTERY_STATUS_CHARGING = 1 << 4,
+ FF_BATTERY_STATUS_DISCHARGING = 1 << 5,
+ FF_BATTERY_STATUS_CRITICAL = 1 << 6,
+} FFBatteryStatus;
+
+typedef struct FFBatteryResult {
+ FFstrbuf manufacturer;
+ FFstrbuf manufactureDate;
+ FFstrbuf modelName;
+ FFstrbuf technology;
+ FFstrbuf serial;
+ FFBatteryStatus status;
+ double capacity;
+ double temperature;
+ uint32_t cycleCount;
+ int32_t timeRemaining; // in seconds, -1 if unknown
+} FFBatteryResult;
+
+const char* ffDetectBattery(FFBatteryOptions* options, FFlist* results);
diff --git a/src/detection/battery/battery_android.c b/src/detection/battery/battery_android.c
new file mode 100644
index 0000000..d76c4ad
--- /dev/null
+++ b/src/detection/battery/battery_android.c
@@ -0,0 +1,160 @@
+#include "fastfetch.h"
+#include "battery.h"
+#include "common/strutil.h"
+#include "common/processing.h"
+#include "common/properties.h"
+
+#define FF_TERMUX_API_PATH FASTFETCH_TARGET_DIR_ROOT "/libexec/termux-api"
+#define FF_TERMUX_API_PARAM "BatteryStatus"
+
+static inline void wrapYyjsonFree(yyjson_doc** doc) {
+ assert(doc);
+ if (*doc) {
+ yyjson_doc_free(*doc);
+ }
+}
+
+static const char* parseTermuxApi(FFBatteryOptions* options, FFlist* results) {
+ FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate();
+
+ if (ffProcessAppendStdOut(&buffer, (char* const[]) { FF_TERMUX_API_PATH, FF_TERMUX_API_PARAM, NULL })) {
+ return "Starting `" FF_TERMUX_API_PATH " " FF_TERMUX_API_PARAM "` failed";
+ }
+
+ yyjson_doc* FF_A_CLEANUP(wrapYyjsonFree) doc = yyjson_read_opts(buffer.chars, buffer.length, 0, NULL, NULL);
+ if (!doc) {
+ return "Failed to parse battery info";
+ }
+
+ yyjson_val* root = yyjson_doc_get_root(doc);
+ if (!yyjson_is_obj(root)) {
+ return "Battery info result is not a JSON object";
+ }
+
+ FFBatteryResult* battery = FF_LIST_ADD(FFBatteryResult, *results);
+ battery->temperature = FF_BATTERY_TEMP_UNSET;
+ battery->cycleCount = 0;
+ battery->timeRemaining = -1;
+ battery->status = FF_BATTERY_STATUS_NONE;
+ ffStrbufInit(&battery->manufacturer);
+ ffStrbufInit(&battery->modelName);
+ ffStrbufInit(&battery->technology);
+ ffStrbufInit(&battery->serial);
+ ffStrbufInit(&battery->manufactureDate);
+
+ battery->capacity = yyjson_get_num(yyjson_obj_get(root, "percentage"));
+ const char* acStatus = yyjson_get_str(yyjson_obj_get(root, "plugged"));
+ if (acStatus) {
+ if (ffStrEquals(acStatus, "PLUGGED_AC")) {
+ battery->status |= FF_BATTERY_STATUS_AC_CONNECTED;
+ } else if (ffStrEquals(acStatus, "PLUGGED_USB")) {
+ battery->status |= FF_BATTERY_STATUS_USB_CONNECTED;
+ } else if (ffStrEquals(acStatus, "PLUGGED_WIRELESS")) {
+ battery->status |= FF_BATTERY_STATUS_WIRELESS_CONNECTED;
+ }
+ }
+ const char* status = yyjson_get_str(yyjson_obj_get(root, "status"));
+ if (status) {
+ if (ffStrEquals(status, "CHARGING")) {
+ battery->status |= FF_BATTERY_STATUS_CHARGING;
+ } else if (ffStrEquals(status, "DISCHARGING")) {
+ battery->status |= FF_BATTERY_STATUS_DISCHARGING;
+ }
+ }
+
+ if (options->temp) {
+ battery->temperature = yyjson_get_num(yyjson_obj_get(root, "temperature"));
+ }
+
+ return NULL;
+}
+
+static const char* parseDumpsys(FFBatteryOptions* options, FFlist* results) {
+ FF_STRBUF_AUTO_DESTROY buf = ffStrbufCreate();
+ if (ffProcessAppendStdOut(&buf, (char*[]) {
+ "/system/bin/dumpsys",
+ "battery",
+ NULL,
+ }) != NULL ||
+ buf.length == 0) {
+ return "Executing `/system/bin/dumpsys battery` failed"; // Only works in `adb shell`, or when rooted
+ }
+
+ if (!ffStrbufStartsWithS(&buf, "Current Battery Service state:\n")) {
+ return "Invalid `/system/bin/dumpsys battery` result";
+ }
+
+ const char* start = buf.chars + strlen("Current Battery Service state:\n");
+
+ FF_STRBUF_AUTO_DESTROY temp = ffStrbufCreate();
+ if (!ffParsePropLines(start, "present: ", &temp) || !ffStrbufEqualS(&temp, "true")) {
+ return NULL;
+ }
+ ffStrbufClear(&temp);
+
+ FFBatteryResult* battery = FF_LIST_ADD(FFBatteryResult, *results);
+ battery->temperature = FF_BATTERY_TEMP_UNSET;
+ battery->cycleCount = 0;
+ battery->timeRemaining = -1;
+ battery->capacity = 0;
+ battery->status = FF_BATTERY_STATUS_NONE;
+ ffStrbufInit(&battery->manufacturer);
+ ffStrbufInit(&battery->modelName);
+ ffStrbufInit(&battery->technology);
+ ffStrbufInit(&battery->serial);
+ ffStrbufInit(&battery->manufactureDate);
+
+ if (ffParsePropLines(start, "AC powered: ", &temp) && ffStrbufEqualS(&temp, "true")) {
+ battery->status |= FF_BATTERY_STATUS_AC_CONNECTED;
+ }
+ ffStrbufClear(&temp);
+
+ if (ffParsePropLines(start, "USB powered: ", &temp) && ffStrbufEqualS(&temp, "true")) {
+ battery->status |= FF_BATTERY_STATUS_USB_CONNECTED;
+ }
+ ffStrbufClear(&temp);
+
+ if (ffParsePropLines(start, "Wireless powered: ", &temp) && ffStrbufEqualS(&temp, "true")) {
+ battery->status |= FF_BATTERY_STATUS_WIRELESS_CONNECTED;
+ }
+ ffStrbufClear(&temp);
+
+ {
+ double level = 0, scale = 0;
+ if (ffParsePropLines(start, "level: ", &temp)) {
+ level = ffStrbufToDouble(&temp, -DBL_MAX);
+ }
+ ffStrbufClear(&temp);
+
+ if (ffParsePropLines(start, "scale: ", &temp)) {
+ scale = ffStrbufToDouble(&temp, -DBL_MAX);
+ }
+ ffStrbufClear(&temp);
+
+ if (level > 0 && scale > 0) {
+ battery->capacity = level * 100 / scale;
+ }
+ }
+
+ if (options->temp) {
+ if (ffParsePropLines(start, "temperature: ", &temp)) {
+ battery->temperature = ffStrbufToDouble(&temp, FF_BATTERY_TEMP_UNSET);
+ if (battery->temperature != FF_BATTERY_TEMP_UNSET) {
+ battery->temperature /= 10.0; // Android returns temperature in tenths of a degree
+ }
+ }
+ ffStrbufClear(&temp);
+ }
+
+ ffParsePropLines(start, "technology: ", &battery->technology);
+
+ return NULL;
+}
+
+const char* ffDetectBattery(FFBatteryOptions* options, FFlist* results) {
+ const char* error = parseTermuxApi(options, results);
+ if (error && parseDumpsys(options, results) == NULL) {
+ return NULL;
+ }
+ return error;
+}
diff --git a/src/detection/battery/battery_apple.c b/src/detection/battery/battery_apple.c
new file mode 100644
index 0000000..c22aa6d
--- /dev/null
+++ b/src/detection/battery/battery_apple.c
@@ -0,0 +1,117 @@
+#include "fastfetch.h"
+#include "battery.h"
+#include "common/apple/cf_helpers.h"
+#include "common/apple/smc_temps.h"
+
+#include <IOKit/IOKitLib.h>
+#include <IOKit/pwr_mgt/IOPM.h>
+
+const char* ffDetectBattery(FFBatteryOptions* options, FFlist* results) {
+ FF_IOOBJECT_AUTO_RELEASE io_iterator_t iterator = IO_OBJECT_NULL;
+ if (IOServiceGetMatchingServices(MACH_PORT_NULL, IOServiceMatching("AppleSmartBattery"), &iterator) != kIOReturnSuccess) {
+ return "IOServiceGetMatchingServices() failed";
+ }
+
+ io_registry_entry_t registryEntry;
+ while ((registryEntry = IOIteratorNext(iterator)) != IO_OBJECT_NULL) {
+ FF_IOOBJECT_AUTO_RELEASE io_registry_entry_t entryBattery = registryEntry;
+ FF_CFTYPE_AUTO_RELEASE CFMutableDictionaryRef properties = NULL;
+ if (IORegistryEntryCreateCFProperties(entryBattery, &properties, kCFAllocatorDefault, kNilOptions) != kIOReturnSuccess) {
+ continue;
+ }
+
+ int currentCapacity, maxCapacity;
+
+ if (ffCfDictGetInt(properties, CFSTR(kIOPMPSMaxCapacityKey), &maxCapacity) != NULL || maxCapacity <= 0) {
+ continue;
+ }
+
+ if (ffCfDictGetInt(properties, CFSTR(kIOPMPSCurrentCapacityKey), &currentCapacity) != NULL || currentCapacity <= 0) {
+ continue;
+ }
+
+ bool boolValue;
+
+ FFBatteryResult* battery = FF_LIST_ADD(FFBatteryResult, *results);
+ battery->temperature = FF_BATTERY_TEMP_UNSET;
+ ffStrbufInit(&battery->manufacturer);
+ ffStrbufInit(&battery->modelName);
+ ffStrbufInit(&battery->serial);
+ ffStrbufInit(&battery->technology);
+ ffStrbufInit(&battery->manufactureDate);
+ battery->status = FF_BATTERY_STATUS_NONE;
+ battery->capacity = currentCapacity * 100.0 / maxCapacity;
+ battery->cycleCount = 0;
+ battery->timeRemaining = -1;
+
+ ffCfDictGetString(properties, CFSTR(kIOPMDeviceNameKey), &battery->modelName);
+ ffCfDictGetString(properties, CFSTR(kIOPMPSSerialKey), &battery->serial);
+ ffCfDictGetString(properties, CFSTR(kIOPMPSManufacturerKey), &battery->manufacturer);
+
+ if (!ffCfDictGetBool(properties, CFSTR("built-in"), &boolValue) && boolValue) {
+ if (!battery->manufacturer.length) {
+ ffStrbufAppendS(&battery->manufacturer, "Apple Inc.");
+ }
+ ffStrbufAppendS(&battery->technology, "Lithium");
+ if (!battery->modelName.length) {
+ ffStrbufAppendS(&battery->modelName, "Built-in");
+ }
+ }
+
+ int32_t cycleCount = 0;
+ ffCfDictGetInt(properties, CFSTR(kIOPMPSCycleCountKey), &cycleCount);
+ battery->cycleCount = cycleCount < 0 ? 0 : (uint32_t) cycleCount;
+
+ battery->timeRemaining = -1;
+ if (ffCfDictGetBool(properties, CFSTR(kIOPMPSExternalConnectedKey), &boolValue) == NULL) {
+ if (boolValue) {
+ battery->status |= FF_BATTERY_STATUS_AC_CONNECTED;
+ } else {
+ battery->status |= FF_BATTERY_STATUS_DISCHARGING;
+ ffCfDictGetInt(properties, CFSTR("AvgTimeToEmpty"), &battery->timeRemaining); // in minutes
+ if (battery->timeRemaining < 0 || battery->timeRemaining >= 0xFFFF) {
+ battery->timeRemaining = -1;
+ } else {
+ battery->timeRemaining *= 60;
+ }
+ }
+ }
+ if (ffCfDictGetBool(properties, CFSTR(kIOPMPSIsChargingKey), &boolValue) == NULL && boolValue) {
+ battery->status |= FF_BATTERY_STATUS_CHARGING;
+ }
+ if (ffCfDictGetBool(properties, CFSTR(kIOPMPSAtCriticalLevelKey), &boolValue) == NULL && boolValue) {
+ battery->status |= FF_BATTERY_STATUS_CRITICAL;
+ }
+
+ int sbdsManufactureDate = 0;
+ if (ffCfDictGetInt(properties, CFSTR(kIOPMPSManufactureDateKey), &sbdsManufactureDate) == NULL) {
+ int day = sbdsManufactureDate & 0b11111;
+ int month = (sbdsManufactureDate >> 5) & 0b1111;
+ int year = (sbdsManufactureDate >> 9) + 1800;
+ ffStrbufSetF(&battery->manufactureDate, "%.4d-%.2d-%.2d", year, month, day);
+ } else {
+ CFDictionaryRef batteryData;
+ if (ffCfDictGetDict(properties, CFSTR("BatteryData"), &batteryData) == NULL) {
+ char manufactureDate[sizeof(uint64_t)];
+ if (ffCfDictGetInt64(batteryData, CFSTR(kIOPMPSManufactureDateKey), (int64_t*) manufactureDate) == NULL) {
+ // https://github.com/AsahiLinux/linux/blob/b5c05cbffb0488c7618106926d522cc3b43d93d5/drivers/power/supply/macsmc_power.c#L410-L419
+ int year = (manufactureDate[0] - '0') * 10 + (manufactureDate[1] - '0') + 2000 - 8;
+ int month = (manufactureDate[2] - '0') * 10 + (manufactureDate[3] - '0');
+ int day = (manufactureDate[4] - '0') * 10 + (manufactureDate[3] - '0');
+ ffStrbufSetF(&battery->manufactureDate, "%.4d-%.2d-%.2d", year, month, day);
+ }
+ }
+ }
+
+ if (options->temp) {
+ int64_t temp;
+ if (!ffCfDictGetInt64(properties, CFSTR(kIOPMPSBatteryTemperatureKey), &temp)) {
+ battery->temperature = (double) temp / 10 - 273.15;
+ } else {
+ ffDetectSmcTemps(FF_TEMP_BATTERY, &battery->temperature);
+ }
+ }
+ }
+
+ return NULL;
+}
diff --git a/src/detection/battery/battery_bsd.c b/src/detection/battery/battery_bsd.c
new file mode 100644
index 0000000..336a17e
--- /dev/null
+++ b/src/detection/battery/battery_bsd.c
@@ -0,0 +1,90 @@
+#include "fastfetch.h"
+#include "common/sysctl.h"
+#include "common/io.h"
+#include "battery.h"
+
+#include <dev/acpica/acpiio.h>
+#include <sys/ioctl.h>
+#include <sys/fcntl.h>
+#include <unistd.h>
+
+const char* ffDetectBattery(FF_A_UNUSED FFBatteryOptions* options, FFlist* results) {
+ // https://www.freebsd.org/cgi/man.cgi?acpi_battery(4)
+ // https://gitlab.xfce.org/panel-plugins/xfce4-battery-plugin/-/blob/master/panel-plugin/libacpi.c
+
+ int units = ffSysctlGetInt("hw.acpi.battery.units", -100);
+ if (units < 0) {
+ return "sysctlbyname(\"hw.acpi.battery.units\") failed";
+ }
+
+ if (units == 0) {
+ return NULL;
+ }
+
+ FF_AUTO_CLOSE_FD int acpifd = open("/dev/acpi", O_RDONLY | O_CLOEXEC);
+ if (acpifd < 0) {
+ return "open(\"/dev/acpi\", O_RDONLY | O_CLOEXEC) failed";
+ }
+
+ for (int i = 0; i < units; ++i) {
+ union acpi_battery_ioctl_arg battio;
+ battio.unit = i;
+
+ if (ioctl(acpifd, ACPIIO_BATT_GET_BATTINFO, &battio) < 0 || (battio.battinfo.state == ACPI_BATT_STAT_NOT_PRESENT)) {
+ continue;
+ }
+
+ FFBatteryResult* battery = FF_LIST_ADD(FFBatteryResult, *results);
+ battery->temperature = FF_BATTERY_TEMP_UNSET;
+ battery->cycleCount = 0;
+ ffStrbufInit(&battery->manufacturer);
+ ffStrbufInit(&battery->modelName);
+ ffStrbufInit(&battery->technology);
+ ffStrbufInit(&battery->serial);
+ ffStrbufInit(&battery->manufactureDate);
+ battery->status = FF_BATTERY_STATUS_NONE;
+ battery->timeRemaining = -1;
+ if (battio.battinfo.min > 0) {
+ battery->timeRemaining = battio.battinfo.min * 60;
+ }
+ battery->capacity = battio.battinfo.cap;
+ if (battio.battinfo.state == ACPI_BATT_STAT_INVALID) {
+ battery->status |= FF_BATTERY_STATUS_UNKNOWN;
+ } else {
+ if (battio.battinfo.state & ACPI_BATT_STAT_DISCHARG) {
+ battery->status |= FF_BATTERY_STATUS_DISCHARGING;
+ }
+ if (battio.battinfo.state & ACPI_BATT_STAT_CHARGING) {
+ battery->status |= FF_BATTERY_STATUS_CHARGING;
+ }
+ if (battio.battinfo.state & ACPI_BATT_STAT_CRITICAL) {
+ battery->status |= FF_BATTERY_STATUS_CRITICAL;
+ }
+ }
+
+ int acadStatus;
+ if (ioctl(acpifd, ACPIIO_ACAD_GET_STATUS, &acadStatus) >= 0 && acadStatus) {
+ battery->status |= FF_BATTERY_STATUS_AC_CONNECTED;
+ }
+
+#ifdef ACPIIO_BATT_GET_BIX
+ battio.unit = i;
+ if (ioctl(acpifd, ACPIIO_BATT_GET_BIX, &battio) >= 0) {
+ ffStrbufAppendS(&battery->manufacturer, battio.bix.oeminfo);
+ ffStrbufAppendS(&battery->modelName, battio.bix.model);
+ ffStrbufAppendS(&battery->technology, battio.bix.type);
+ ffStrbufAppendS(&battery->serial, battio.bix.serial);
+ battery->cycleCount = battio.bix.cycles;
+ }
+#elif defined(ACPIIO_BATT_GET_BIF)
+ battio.unit = i;
+ if (ioctl(acpifd, ACPIIO_BATT_GET_BIF, &battio) >= 0) {
+ ffStrbufAppendS(&battery->manufacturer, battio.bif.oeminfo);
+ ffStrbufAppendS(&battery->modelName, battio.bif.model);
+ ffStrbufAppendS(&battery->technology, battio.bif.type);
+ ffStrbufAppendS(&battery->serial, battio.bif.serial);
+ }
+#endif
+ }
+ return NULL;
+}
diff --git a/src/detection/battery/battery_haiku.c b/src/detection/battery/battery_haiku.c
new file mode 100644
index 0000000..aa620e6
--- /dev/null
+++ b/src/detection/battery/battery_haiku.c
@@ -0,0 +1,71 @@
+#include "fastfetch.h"
+#include "battery.h"
+#include "common/io.h"
+
+#include <private/device/power_managment.h>
+#include <sys/ioctl.h>
+#include <fcntl.h>
+
+const char* parseBattery(int dfd, const char* battId, FFlist* results) {
+ FF_AUTO_CLOSE_FD int fd = openat(dfd, battId, O_RDWR);
+ if (fd < 0) {
+ return "openat() failed";
+ }
+
+ acpi_battery_info basic = {};
+ if (ioctl(fd, GET_BATTERY_INFO, &basic, sizeof(basic)) != 0) {
+ return "ioctl(GET_BATTERY_INFO) failed";
+ }
+ acpi_extended_battery_info extended = {};
+ if (ioctl(fd, GET_EXTENDED_BATTERY_INFO, &extended, sizeof(extended)) != 0) {
+ return "ioctl(GET_EXTENDED_BATTERY_INFO) failed";
+ }
+
+ if (extended.last_full_charge == (uint32) -1) {
+ return "Skipped";
+ }
+
+ FFBatteryResult* battery = FF_LIST_ADD(FFBatteryResult, *results);
+ ffStrbufInitS(&battery->modelName, extended.model_number);
+ ffStrbufInitS(&battery->manufacturer, extended.oem_info);
+ ffStrbufInit(&battery->manufactureDate);
+ ffStrbufInitS(&battery->technology, extended.type); // extended.technology?
+ ffStrbufInitS(&battery->serial, extended.serial_number);
+ battery->status = FF_BATTERY_STATUS_NONE;
+ battery->temperature = FF_BATTERY_TEMP_UNSET;
+ battery->cycleCount = extended.cycles;
+ battery->timeRemaining = -1;
+ battery->capacity = (double) basic.capacity * 100. / (double) extended.last_full_charge;
+
+ if (basic.state & BATTERY_DISCHARGING) {
+ battery->status |= FF_BATTERY_STATUS_DISCHARGING;
+ }
+ if (basic.state & BATTERY_CHARGING) {
+ battery->status |= FF_BATTERY_STATUS_CHARGING;
+ }
+ if (basic.state & BATTERY_CRITICAL_STATE) {
+ battery->status |= FF_BATTERY_STATUS_CRITICAL;
+ }
+ if (basic.state & BATTERY_NOT_CHARGING || basic.state & BATTERY_CHARGING) {
+ battery->status |= FF_BATTERY_STATUS_AC_CONNECTED;
+ }
+
+ return NULL;
+}
+
+const char* ffDetectBattery(FF_A_UNUSED FFBatteryOptions* options, FFlist* results) {
+ FF_AUTO_CLOSE_DIR DIR* dir = opendir("/dev/power/acpi_battery/");
+ if (!dir) {
+ return "opendir(/dev/power/acpi_battery) failed";
+ }
+
+ struct dirent* entry;
+ while ((entry = readdir(dir))) {
+ if (entry->d_name[0] == '.') {
+ continue;
+ }
+ parseBattery(dirfd(dir), entry->d_name, results);
+ }
+
+ return NULL;
+}
diff --git a/src/detection/battery/battery_linux.c b/src/detection/battery/battery_linux.c
new file mode 100644
index 0000000..6c1f711
--- /dev/null
+++ b/src/detection/battery/battery_linux.c
@@ -0,0 +1,203 @@
+#include "battery.h"
+#include "common/io.h"
+#include "common/strutil.h"
+#include "common/debug.h"
+
+#include <dirent.h>
+#include <unistd.h>
+#include <fcntl.h>
+
+// https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-class-power
+
+static bool parseBattery(int dfd, const char* id, FFBatteryOptions* options, FFlist* results, bool* acConnected) {
+ FF_STRBUF_AUTO_DESTROY tmpBuffer = ffStrbufCreate();
+
+ {
+ char present = '\0';
+ if (ffReadFileDataRelative(dfd, "present", 1, &present) && present == '0') {
+ FF_DEBUG("Battery \"%s\": Not present", id);
+ return false;
+ }
+ }
+
+ // type must exist
+ if (!ffReadFileBufferRelative(dfd, "type", &tmpBuffer)) {
+ FF_DEBUG("Battery \"%s\": No type file", id);
+ return false;
+ }
+ ffStrbufTrimRightSpace(&tmpBuffer);
+ if (ffStrbufEqualS(&tmpBuffer, "Mains")) {
+ if (*acConnected) {
+ FF_DEBUG("Battery \"%s\": Type is Mains, but AC is already connected", id);
+ return false;
+ }
+
+ char online = '\0';
+ if (ffReadFileDataRelative(dfd, "online", 1, &online) == 1 && online == '1') {
+ *acConnected = true;
+ }
+ FF_DEBUG("Battery \"%s\": Type is Mains, AC Connected: %s", id, *acConnected ? "Yes" : "No");
+ return false;
+ } else if (!ffStrbufEqualS(&tmpBuffer, "Battery")) {
+ FF_DEBUG("Battery \"%s\": Type is not Battery or Mains, but \"%s\"", id, tmpBuffer.chars);
+ return false;
+ }
+
+ // scope may not exist or must not be "Device"
+ if (ffReadFileBufferRelative(dfd, "scope", &tmpBuffer)) {
+ ffStrbufTrimRightSpace(&tmpBuffer);
+
+ FF_DEBUG("Battery \"%s\": Scope is \"%s\"", id, tmpBuffer.chars);
+ if (ffStrbufEqualS(&tmpBuffer, "Device")) {
+ return false;
+ }
+ }
+
+ // `capacity` must exist
+ // This is expensive in my laptop
+ if (!ffReadFileBufferRelative(dfd, "capacity", &tmpBuffer)) {
+ FF_DEBUG("Battery \"%s\": No capacity file", id);
+ return false;
+ }
+
+ FFBatteryResult* result = FF_LIST_ADD(FFBatteryResult, *results);
+ ffStrbufInit(&result->manufacturer);
+ ffStrbufInit(&result->modelName);
+ ffStrbufInit(&result->technology);
+ ffStrbufInit(&result->serial);
+ ffStrbufInit(&result->manufactureDate);
+ result->status = FF_BATTERY_STATUS_NONE;
+ result->capacity = ffStrbufToDouble(&tmpBuffer, 0);
+ result->cycleCount = 0;
+ result->temperature = FF_BATTERY_TEMP_UNSET;
+ result->timeRemaining = -1;
+
+ // At this point, we have a battery. Try to get as much values as possible.
+
+ if (ffReadFileBufferRelative(dfd, "manufacturer", &result->manufacturer)) {
+ ffStrbufTrimRightSpace(&result->manufacturer);
+ } else if (ffStrEquals(id, "macsmc-battery")) { // asahi
+ ffStrbufSetStatic(&result->manufacturer, "Apple Inc.");
+ }
+
+ if (ffReadFileBufferRelative(dfd, "model_name", &result->modelName)) {
+ ffStrbufTrimRightSpace(&result->modelName);
+ }
+
+ if (ffReadFileBufferRelative(dfd, "technology", &result->technology)) {
+ ffStrbufTrimRightSpace(&result->technology);
+ }
+
+ if (ffReadFileBufferRelative(dfd, "status", &tmpBuffer)) {
+ ffStrbufTrimRightSpace(&tmpBuffer);
+ }
+
+ // Unknown, Charging, Discharging, Not charging, Full
+
+ if (ffStrbufEqualS(&tmpBuffer, "Discharging")) {
+ result->status |= FF_BATTERY_STATUS_DISCHARGING;
+ FF_STRBUF_AUTO_DESTROY now = ffStrbufCreate();
+ if (ffReadFileBufferRelative(dfd, "time_to_empty_now", &now)) {
+ result->timeRemaining = (int32_t) ffStrbufToSInt(&now, 0);
+ } else {
+ if (ffReadFileBufferRelative(dfd, "charge_now", &now)) {
+ int64_t chargeNow = ffStrbufToSInt(&now, 0);
+ if (chargeNow > 0) {
+ if (ffReadFileBufferRelative(dfd, "current_now", &now)) {
+ int64_t currentNow = ffStrbufToSInt(&now, INT64_MIN);
+ if (currentNow < 0) {
+ currentNow = -currentNow;
+ }
+ if (currentNow > 0) {
+ result->timeRemaining = (int32_t) ((chargeNow * 3600) / currentNow);
+ }
+ }
+ }
+ }
+ }
+ } else if (ffStrbufEqualS(&tmpBuffer, "Charging")) {
+ result->status |= FF_BATTERY_STATUS_CHARGING;
+ } else if (ffStrbufEqualS(&tmpBuffer, "Unknown")) {
+ result->status |= FF_BATTERY_STATUS_UNKNOWN;
+ }
+
+ if (ffReadFileBufferRelative(dfd, "capacity_level", &tmpBuffer)) {
+ ffStrbufTrimRightSpace(&tmpBuffer);
+ if (ffStrbufEqualS(&tmpBuffer, "Critical")) {
+ result->status |= FF_BATTERY_STATUS_CRITICAL;
+ }
+ }
+
+ if (ffReadFileBufferRelative(dfd, "serial_number", &result->serial)) {
+ ffStrbufTrimRightSpace(&result->serial);
+ }
+
+ if (ffReadFileBufferRelative(dfd, "cycle_count", &tmpBuffer)) {
+ int64_t cycleCount = ffStrbufToSInt(&tmpBuffer, 0);
+ result->cycleCount = cycleCount < 0 || cycleCount > UINT32_MAX ? 0 : (uint32_t) cycleCount;
+ }
+
+ if (ffReadFileBufferRelative(dfd, "manufacture_year", &tmpBuffer)) {
+ int year = (int) ffStrbufToSInt(&tmpBuffer, 0);
+ if (year > 0) {
+ if (ffReadFileBufferRelative(dfd, "manufacture_month", &tmpBuffer)) {
+ int month = (int) ffStrbufToSInt(&tmpBuffer, 0);
+ if (month > 0) {
+ if (ffReadFileBufferRelative(dfd, "manufacture_day", &tmpBuffer)) {
+ int day = (int) ffStrbufToSInt(&tmpBuffer, 0);
+ if (day > 0) {
+ ffStrbufSetF(&result->manufactureDate, "%.4d-%.2d-%.2d", year, month, day);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if (options->temp) {
+ if (ffReadFileBufferRelative(dfd, "temp", &tmpBuffer)) {
+ result->temperature = ffStrbufToDouble(&tmpBuffer, FF_BATTERY_TEMP_UNSET);
+ if (result->temperature != FF_BATTERY_TEMP_UNSET) {
+ result->temperature /= 10;
+ }
+ }
+ }
+
+ FF_DEBUG("Battery \"%s\": Capacity: %.2f%%, Status: \"%x\", Time Remaining: %d seconds, Temperature: %.1f°C, Cycle Count: %u",
+ id,
+ result->capacity,
+ result->status,
+ result->timeRemaining,
+ result->temperature,
+ result->cycleCount);
+ return true;
+}
+
+const char* ffDetectBattery(FFBatteryOptions* options, FFlist* results) {
+ FF_AUTO_CLOSE_DIR DIR* dirp = opendir("/sys/class/power_supply/");
+ if (dirp == NULL) {
+ return "opendir(\"/sys/class/power_supply/\") == NULL";
+ }
+
+ bool acConnected = false;
+
+ struct dirent* entry;
+ while ((entry = readdir(dirp)) != NULL) {
+ if (entry->d_name[0] == '.') {
+ continue;
+ }
+
+ FF_AUTO_CLOSE_FD int dfd = openat(dirfd(dirp), entry->d_name, O_RDONLY | O_CLOEXEC | O_PATH | O_DIRECTORY);
+ if (dfd >= 0) {
+ parseBattery(dfd, entry->d_name, options, results, &acConnected);
+ }
+ }
+
+ if (acConnected) {
+ FF_LIST_FOR_EACH (FFBatteryResult, batt, *results) {
+ batt->status |= FF_BATTERY_STATUS_AC_CONNECTED;
+ }
+ }
+
+ return NULL;
+}
diff --git a/src/detection/battery/battery_nbsd.c b/src/detection/battery/battery_nbsd.c
new file mode 100644
index 0000000..2e8b426
--- /dev/null
+++ b/src/detection/battery/battery_nbsd.c
@@ -0,0 +1,109 @@
+#include "battery.h"
+#include "common/io.h"
+#include "common/FFstrbuf.h"
+#include "common/strutil.h"
+
+#include <prop/prop_array.h>
+#include <prop/prop_bool.h>
+#include <prop/prop_dictionary.h>
+#include <prop/prop_object.h>
+#include <sys/envsys.h>
+#include <prop/proplib.h>
+#include <paths.h>
+#include <time.h>
+#include <unistd.h>
+#include <fcntl.h>
+
+const char* ffDetectBattery(FF_A_UNUSED FFBatteryOptions* options, FFlist* results) {
+ FF_AUTO_CLOSE_FD int fd = open(_PATH_SYSMON, O_RDONLY | O_CLOEXEC);
+ if (fd < 0) {
+ return "open(_PATH_SYSMON, O_RDONLY | O_CLOEXEC) failed";
+ }
+
+ prop_dictionary_t root = NULL;
+ if (prop_dictionary_recv_ioctl(fd, ENVSYS_GETDICTIONARY, &root) < 0) {
+ return "prop_dictionary_recv_ioctl(ENVSYS_GETDICTIONARY) failed";
+ }
+
+ bool acConnected = false;
+ {
+ prop_array_t acad = prop_dictionary_get(root, "acpiacad0");
+ if (acad) {
+ prop_dictionary_t dict = prop_array_get(acad, 0);
+ prop_dictionary_get_uint8(dict, "cur-value", (uint8_t*) &acConnected);
+ }
+ }
+
+ prop_object_iterator_t itKey = prop_dictionary_iterator(root);
+ for (prop_dictionary_keysym_t key; (key = prop_object_iterator_next(itKey)) != NULL;) {
+ if (!ffStrStartsWith(prop_dictionary_keysym_value(key), "acpibat")) {
+ continue;
+ }
+
+ prop_array_t bat = prop_dictionary_get_keysym(root, key);
+ uint32_t max = 0, curr = 0, dischargeRate = 0;
+ bool charging = false, critical = false;
+ prop_object_iterator_t iter = prop_array_iterator(bat);
+ for (prop_dictionary_t dict; (dict = prop_object_iterator_next(iter)) != NULL;) {
+ if (prop_object_type(dict) != PROP_TYPE_DICTIONARY) {
+ continue;
+ }
+
+ const char* desc = NULL;
+ if (!prop_dictionary_get_string(dict, "description", &desc)) {
+ continue;
+ }
+
+ if (ffStrEquals(desc, "present")) {
+ int value = 0;
+ if (prop_dictionary_get_int(dict, "cur-value", &value) && value == 0) {
+ continue;
+ }
+ } else if (ffStrEquals(desc, "charging")) {
+ prop_dictionary_get_uint8(dict, "cur-value", (uint8_t*) &charging);
+ } else if (ffStrEquals(desc, "charge")) {
+ prop_dictionary_get_uint32(dict, "max-value", &max);
+ prop_dictionary_get_uint32(dict, "cur-value", &curr);
+ const char* state = NULL;
+ if (prop_dictionary_get_string(dict, "state", &state) && ffStrEquals(state, "critical")) {
+ critical = true;
+ }
+ } else if (ffStrEquals(desc, "discharge rate")) {
+ prop_dictionary_get_uint(dict, "cur-value", &dischargeRate);
+ }
+ }
+
+ if (max > 0) {
+ FFBatteryResult* battery = FF_LIST_ADD(FFBatteryResult, *results);
+ battery->temperature = FF_BATTERY_TEMP_UNSET;
+ battery->cycleCount = 0;
+ ffStrbufInit(&battery->manufacturer);
+ ffStrbufInit(&battery->modelName);
+ ffStrbufInit(&battery->technology);
+ ffStrbufInit(&battery->serial);
+ ffStrbufInit(&battery->manufactureDate);
+ battery->status = FF_BATTERY_STATUS_NONE;
+ battery->timeRemaining = -1;
+
+ battery->capacity = (double) curr / (double) max * 100.;
+ if (charging) {
+ battery->status |= FF_BATTERY_STATUS_CHARGING;
+ } else if (dischargeRate) {
+ battery->status |= FF_BATTERY_STATUS_DISCHARGING;
+ battery->timeRemaining = (int32_t) ((double) curr / dischargeRate * 3600);
+ }
+ if (critical) {
+ battery->status |= FF_BATTERY_STATUS_CRITICAL;
+ }
+ if (acConnected) {
+ battery->status |= FF_BATTERY_STATUS_AC_CONNECTED;
+ }
+ }
+
+ prop_object_iterator_release(iter);
+ }
+ prop_object_iterator_release(itKey);
+ prop_object_release(root);
+
+ return NULL;
+}
diff --git a/src/detection/battery/battery_nosupport.c b/src/detection/battery/battery_nosupport.c
new file mode 100644
index 0000000..efd0f73
--- /dev/null
+++ b/src/detection/battery/battery_nosupport.c
@@ -0,0 +1,7 @@
+#include "fastfetch.h"
+#include "battery.h"
+
+const char* ffDetectBattery(FFBatteryOptions* options, FFlist* results) {
+ FF_UNUSED(options, results)
+ return "Not supported on this platform";
+}
diff --git a/src/detection/battery/battery_obsd.c b/src/detection/battery/battery_obsd.c
new file mode 100644
index 0000000..ee664ce
--- /dev/null
+++ b/src/detection/battery/battery_obsd.c
@@ -0,0 +1,60 @@
+#include "battery.h"
+#include "common/io.h"
+
+#include <machine/apmvar.h>
+#include <sys/ioctl.h>
+#include <unistd.h>
+#include <fcntl.h>
+
+const char* ffDetectBattery(FF_A_UNUSED FFBatteryOptions* options, FFlist* result) {
+ FF_AUTO_CLOSE_FD int devfd = open("/dev/apm", O_RDONLY | O_CLOEXEC);
+
+ if (devfd < 0) {
+ return "open(dev/apm, O_RDONLY | O_CLOEXEC) failed";
+ }
+
+ struct apm_power_info info = {};
+
+ if (ioctl(devfd, APM_IOC_GETPOWER, &info) < 0) {
+ return "ioctl(APM_IOC_GETPOWER) failed";
+ }
+
+ if (info.battery_state == APM_BATTERY_ABSENT) {
+ return NULL;
+ }
+
+ FFBatteryResult* battery = FF_LIST_ADD(FFBatteryResult, *result);
+ battery->temperature = FF_BATTERY_TEMP_UNSET;
+ battery->cycleCount = 0;
+ battery->timeRemaining = -1;
+ battery->capacity = info.battery_life;
+ ffStrbufInit(&battery->manufacturer);
+ ffStrbufInit(&battery->modelName);
+ ffStrbufInit(&battery->technology);
+ ffStrbufInit(&battery->serial);
+ ffStrbufInit(&battery->manufactureDate);
+ battery->status = FF_BATTERY_STATUS_NONE;
+
+ if (info.ac_state == APM_AC_ON || info.ac_state == APM_AC_BACKUP) {
+ battery->status |= FF_BATTERY_STATUS_AC_CONNECTED;
+ } else if (info.ac_state == APM_AC_OFF) {
+ battery->timeRemaining = (int) info.minutes_left * 60;
+ battery->status |= FF_BATTERY_STATUS_DISCHARGING;
+ }
+
+ if (info.battery_state == APM_BATT_CRITICAL || info.battery_state == APM_BATT_CHARGING || info.battery_state == APM_BATT_UNKNOWN) {
+ switch (info.battery_state) {
+ case APM_BATT_UNKNOWN:
+ battery->status |= FF_BATTERY_STATUS_UNKNOWN;
+ break;
+ case APM_BATT_CHARGING:
+ battery->status |= FF_BATTERY_STATUS_CHARGING;
+ break;
+ case APM_BATT_CRITICAL:
+ battery->status |= FF_BATTERY_STATUS_CRITICAL;
+ break;
+ }
+ }
+
+ return NULL;
+}
diff --git a/src/detection/battery/battery_windows.c b/src/detection/battery/battery_windows.c
new file mode 100644
index 0000000..84f2a90
--- /dev/null
+++ b/src/detection/battery/battery_windows.c
@@ -0,0 +1,384 @@
+#define INITGUID
+
+#include "battery.h"
+
+#include "common/debug.h"
+#include "common/mallocHelper.h"
+#include "common/windows/unicode.h"
+#include "common/windows/wmi.h"
+
+#include <winerror.h>
+
+typedef void(WINAPI* PINTERFACE_REFERENCE)(PVOID Context);
+typedef void(WINAPI* PINTERFACE_DEREFERENCE)(PVOID Context);
+typedef struct _DEVICE_OBJECT* PDEVICE_OBJECT;
+typedef struct _IRP* PIRP;
+#ifdef _WINDOWS_
+ #undef _WINDOWS_
+#endif
+
+#include <batclass.h>
+
+#pragma GCC diagnostic ignored "-Wmultichar"
+
+typedef struct FFBatteryWmiEntry {
+ ULONG tag;
+ FFBatteryResult* result;
+} FFBatteryWmiEntry;
+
+static FFBatteryWmiEntry* getBatteryEntry(FFlist* entries, FFlist* results, ULONG tag) {
+ FF_LIST_FOR_EACH (FFBatteryWmiEntry, entry, *entries) {
+ if (entry->tag == tag) {
+ return entry;
+ }
+ }
+
+ FFBatteryWmiEntry* entry = FF_LIST_ADD(FFBatteryWmiEntry, *entries);
+ entry->tag = tag;
+ FFBatteryResult* battery = FF_LIST_ADD(FFBatteryResult, *results);
+ entry->result = battery;
+ ffStrbufInit(&battery->manufacturer);
+ ffStrbufInit(&battery->manufactureDate);
+ ffStrbufInit(&battery->modelName);
+ ffStrbufInit(&battery->technology);
+ ffStrbufInit(&battery->serial);
+ battery->status = FF_BATTERY_STATUS_NONE;
+ battery->capacity = -1;
+ battery->temperature = FF_BATTERY_TEMP_UNSET;
+ battery->cycleCount = 0;
+ battery->timeRemaining = -1;
+ return entry;
+}
+
+static const char* queryWmiAllData(const GUID* guid, const char* guidStr, PWNODE_ALL_DATA* pAllData, ULONG* pBufferSize) {
+ FF_AUTO_CLOSE_WMI_BLOCK HANDLE hBlock = NULL;
+ ULONG status = WmiOpenBlock(guid, WMIGUID_QUERY, &hBlock);
+ if (status != ERROR_SUCCESS) {
+ FF_DEBUG("WMI: WmiOpenBlock() failed for %s: %s", guidStr, ffDebugWin32Error(status));
+ return "WmiOpenBlock() failed";
+ }
+
+ status = WmiQueryAllDataW(hBlock, pBufferSize, NULL);
+ if (status != ERROR_SUCCESS && status != ERROR_INSUFFICIENT_BUFFER) {
+ FF_DEBUG("WMI: first WmiQueryAllDataW() failed: %s", ffDebugWin32Error(status));
+ return "WmiQueryAllDataW(NULL) failed";
+ }
+
+ if (*pBufferSize == 0) {
+ return "WmiQueryAllDataW(NULL) returned no data";
+ }
+
+ if (*pBufferSize < sizeof(WNODE_ALL_DATA)) {
+ FF_DEBUG("WMI: WmiQueryAllDataW() returned insufficient buffer size: %lu", *pBufferSize);
+ return "WmiQueryAllDataW() returned insufficient data for WNODE_ALL_DATA";
+ }
+
+ *pAllData = (PWNODE_ALL_DATA) malloc(*pBufferSize);
+
+ status = WmiQueryAllDataW(hBlock, pBufferSize, *pAllData);
+ if (status != ERROR_SUCCESS) {
+ FF_DEBUG("WMI: second WmiQueryAllDataW failed: %s", ffDebugWin32Error(status));
+ free(*pAllData);
+ *pAllData = NULL;
+ return "WmiQueryAllDataW(*pAllData) failed";
+ }
+
+ return NULL;
+}
+
+static bool getInstanceData(const PWNODE_ALL_DATA allData, ULONG bufferSize, ULONG index, const uint8_t** instanceData, ULONG* instanceLength) {
+ ULONG dataOffset = 0;
+ ULONG dataLength = 0;
+
+ if (allData->WnodeHeader.Flags & WNODE_FLAG_FIXED_INSTANCE_SIZE) {
+ dataLength = allData->FixedInstanceSize;
+ dataOffset = allData->DataBlockOffset + index * dataLength;
+ } else {
+ dataOffset = allData->OffsetInstanceDataAndLength[index].OffsetInstanceData;
+ dataLength = allData->OffsetInstanceDataAndLength[index].LengthInstanceData;
+ }
+
+ if (dataLength == 0 || dataOffset >= bufferSize || dataLength > bufferSize - dataOffset) {
+ return false;
+ }
+
+ *instanceData = (const uint8_t*) allData + dataOffset;
+ *instanceLength = dataLength;
+ return true;
+}
+
+static void detectStaticData(FFlist* entries, FFlist* results) {
+ FF_DEBUG("detectStaticData");
+ FF_AUTO_FREE PWNODE_ALL_DATA allData = NULL;
+ ULONG bufferSize = 0;
+ const char* error = queryWmiAllData(&BATTERY_STATIC_DATA_WMI_GUID, "BATTERY_STATIC_DATA_WMI_GUID", &allData, &bufferSize);
+ if (error) {
+ return;
+ }
+
+ for (ULONG i = 0; i < allData->InstanceCount; ++i) {
+ const uint8_t* instanceData = NULL;
+ ULONG instanceLength = 0;
+ if (!getInstanceData(allData, bufferSize, i, &instanceData, &instanceLength) || instanceLength < offsetof(BATTERY_WMI_STATIC_DATA, Strings)) {
+ continue;
+ }
+
+ const BATTERY_WMI_STATIC_DATA* data = (const BATTERY_WMI_STATIC_DATA*) instanceData;
+ FFBatteryWmiEntry* entry = getBatteryEntry(entries, results, data->Tag);
+
+ FF_DEBUG("chemistry: %.4s", (const char*) &data->Chemistry);
+#if __BIG_ENDIAN__
+ #define htobe32(x) (x) // Should not happen on Windows, but just in case
+#else
+ #define htobe32(x) __builtin_bswap32(x)
+#endif
+ switch (data->Chemistry) {
+ case htobe32('PbAc'):
+ ffStrbufSetStatic(&entry->result->technology, "Lead Acid");
+ break;
+ case htobe32('LION'):
+ case htobe32('Li-I'):
+ ffStrbufSetStatic(&entry->result->technology, "Lithium Ion");
+ break;
+ case htobe32('NiCd'):
+ ffStrbufSetStatic(&entry->result->technology, "Nickel Cadmium");
+ break;
+ case htobe32('NiMH'):
+ ffStrbufSetStatic(&entry->result->technology, "Nickel Metal Hydride");
+ break;
+ case htobe32('NiZn'):
+ ffStrbufSetStatic(&entry->result->technology, "Nickel Zinc");
+ break;
+ case htobe32('RAM\0'):
+ ffStrbufSetStatic(&entry->result->technology, "Rechargeable Alkaline-Manganese");
+ break;
+ default:
+ ffStrbufSetStatic(&entry->result->technology, data->Technology ? "Rechargeable" : "Non Rechargeable");
+ break;
+ }
+#undef htobe32
+
+ const BATTERY_MANUFACTURE_DATE* manufactureDate = (const BATTERY_MANUFACTURE_DATE*) data->ManufactureDate;
+ if (manufactureDate->Year > 0 && manufactureDate->Month >= 1 && manufactureDate->Month <= 12 && manufactureDate->Day >= 1 && manufactureDate->Day <= 31) {
+ uint16_t year = manufactureDate->Year;
+ ffStrbufSetF(&entry->result->manufactureDate, "%.4u-%.2u-%.2u", (unsigned) (year < 1000 ? (year + 1900) : year), (unsigned) manufactureDate->Month, (unsigned) manufactureDate->Day);
+ }
+
+ // Device Name, Manufacture Name, Serial Number, UniqueID
+ const struct {
+ uint16_t size; // in bytes, including the null terminator
+ wchar_t value[];
+ }* cursor = (const void*) data->Strings;
+
+ FFstrbuf* strings[] = {
+ &entry->result->modelName,
+ &entry->result->manufacturer,
+ &entry->result->serial,
+ };
+
+ for (size_t i = 0; i < ARRAY_SIZE(strings); ++i) {
+ if (cursor->size > sizeof(wchar_t)) {
+ ffStrbufSetNWS(strings[i], cursor->size / sizeof(wchar_t) - 1, cursor->value);
+ }
+ cursor = (const void*) ((const uint8_t*) cursor + sizeof(uint16_t) + cursor->size);
+ }
+ }
+}
+
+static void detectStatus(FFlist* entries, FFlist* results) {
+ FF_DEBUG("detectStatus");
+ FF_AUTO_FREE PWNODE_ALL_DATA allData = NULL;
+ ULONG bufferSize = 0;
+ const char* error = queryWmiAllData(&BATTERY_STATUS_WMI_GUID, "BATTERY_STATUS_WMI_GUID", &allData, &bufferSize);
+ if (error) {
+ return;
+ }
+
+ for (ULONG i = 0; i < allData->InstanceCount; ++i) {
+ const uint8_t* instanceData = NULL;
+ ULONG instanceLength = 0;
+ if (!getInstanceData(allData, bufferSize, i, &instanceData, &instanceLength) || instanceLength < sizeof(BATTERY_WMI_STATUS)) {
+ continue;
+ }
+
+ const BATTERY_WMI_STATUS* data = (const BATTERY_WMI_STATUS*) instanceData;
+ FFBatteryWmiEntry* entry = getBatteryEntry(entries, results, data->Tag);
+ if (data->RemainingCapacity != BATTERY_UNKNOWN_CAPACITY) {
+ entry->result->capacity = data->RemainingCapacity;
+ }
+
+ entry->result->status = FF_BATTERY_STATUS_NONE;
+ if (data->PowerOnline) {
+ entry->result->status |= FF_BATTERY_STATUS_AC_CONNECTED;
+ }
+ if (data->Charging) {
+ entry->result->status |= FF_BATTERY_STATUS_CHARGING;
+ }
+ if (data->Discharging) {
+ entry->result->status |= FF_BATTERY_STATUS_DISCHARGING;
+ }
+ if (data->Critical) {
+ entry->result->status |= FF_BATTERY_STATUS_CRITICAL;
+ }
+ }
+}
+
+static void detectRuntime(FFlist* entries, FFlist* results) {
+ FF_DEBUG("detectRuntime");
+ FF_AUTO_FREE PWNODE_ALL_DATA allData = NULL;
+ ULONG bufferSize = 0;
+ const char* error = queryWmiAllData(&BATTERY_RUNTIME_WMI_GUID, "BATTERY_RUNTIME_WMI_GUID", &allData, &bufferSize);
+ if (error) {
+ return;
+ }
+
+ for (ULONG i = 0; i < allData->InstanceCount; ++i) {
+ const uint8_t* instanceData = NULL;
+ ULONG instanceLength = 0;
+ if (!getInstanceData(allData, bufferSize, i, &instanceData, &instanceLength) || instanceLength < sizeof(BATTERY_WMI_RUNTIME)) {
+ continue;
+ }
+
+ const BATTERY_WMI_RUNTIME* data = (const BATTERY_WMI_RUNTIME*) instanceData;
+ FFBatteryWmiEntry* entry = getBatteryEntry(entries, results, data->Tag);
+ if (data->EstimatedRuntime != BATTERY_UNKNOWN_TIME) {
+ entry->result->timeRemaining = (int32_t) data->EstimatedRuntime;
+ }
+ }
+}
+
+static void detectFullChargedCapacity(FFlist* entries, FFlist* results) {
+ FF_DEBUG("detectFullChargedCapacity");
+ FF_AUTO_FREE PWNODE_ALL_DATA allData = NULL;
+ ULONG bufferSize = 0;
+ const char* error = queryWmiAllData(&BATTERY_FULL_CHARGED_CAPACITY_WMI_GUID, "BATTERY_FULL_CHARGED_CAPACITY_WMI_GUID", &allData, &bufferSize);
+ if (error) {
+ return;
+ }
+
+ for (ULONG i = 0; i < allData->InstanceCount; ++i) {
+ const uint8_t* instanceData = NULL;
+ ULONG instanceLength = 0;
+ if (!getInstanceData(allData, bufferSize, i, &instanceData, &instanceLength) || instanceLength < sizeof(BATTERY_WMI_FULL_CHARGED_CAPACITY)) {
+ continue;
+ }
+
+ const BATTERY_WMI_FULL_CHARGED_CAPACITY* data = (const BATTERY_WMI_FULL_CHARGED_CAPACITY*) instanceData;
+ FFBatteryWmiEntry* entry = getBatteryEntry(entries, results, data->Tag);
+
+ if (data->FullChargedCapacity != BATTERY_UNKNOWN_CAPACITY && entry->result->capacity >= 0) {
+ entry->result->capacity *= 100;
+ entry->result->capacity /= data->FullChargedCapacity;
+ }
+ }
+}
+
+static void detectCycleCount(FFlist* entries, FFlist* results) {
+ FF_DEBUG("detectCycleCount");
+ FF_AUTO_FREE PWNODE_ALL_DATA allData = NULL;
+ ULONG bufferSize = 0;
+ const char* error = queryWmiAllData(&BATTERY_CYCLE_COUNT_WMI_GUID, "BATTERY_CYCLE_COUNT_WMI_GUID", &allData, &bufferSize);
+ if (error) {
+ return;
+ }
+
+ for (ULONG i = 0; i < allData->InstanceCount; ++i) {
+ const uint8_t* instanceData = NULL;
+ ULONG instanceLength = 0;
+ if (!getInstanceData(allData, bufferSize, i, &instanceData, &instanceLength) || instanceLength < sizeof(BATTERY_WMI_CYCLE_COUNT)) {
+ continue;
+ }
+
+ const BATTERY_WMI_CYCLE_COUNT* data = (const BATTERY_WMI_CYCLE_COUNT*) instanceData;
+ getBatteryEntry(entries, results, data->Tag)->result->cycleCount = data->CycleCount;
+ }
+}
+
+static void detectTemperature(FFlist* entries, FFlist* results) {
+ FF_DEBUG("detectTemperature");
+ FF_AUTO_FREE PWNODE_ALL_DATA allData = NULL;
+ ULONG bufferSize = 0;
+ const char* error = queryWmiAllData(&BATTERY_TEMPERATURE_WMI_GUID, "BATTERY_TEMPERATURE_WMI_GUID", &allData, &bufferSize);
+ if (error) {
+ return;
+ }
+
+ for (ULONG i = 0; i < allData->InstanceCount; ++i) {
+ const uint8_t* instanceData = NULL;
+ ULONG instanceLength = 0;
+ if (!getInstanceData(allData, bufferSize, i, &instanceData, &instanceLength) || instanceLength < sizeof(BATTERY_WMI_TEMPERATURE)) {
+ continue;
+ }
+
+ const BATTERY_WMI_TEMPERATURE* data = (const BATTERY_WMI_TEMPERATURE*) instanceData;
+ getBatteryEntry(entries, results, data->Tag)->result->temperature = data->Temperature / 10.0 - 273.15;
+ }
+}
+
+static const char* detectWithNtApi(FFBatteryResult* battery) {
+ // Reports summary battery information, not per battery
+ FF_DEBUG("NtApi: start detection");
+ SYSTEM_BATTERY_STATE info;
+ NTSTATUS status = NtPowerInformation(SystemBatteryState, NULL, 0, &info, sizeof(info));
+ if (!NT_SUCCESS(status)) {
+ FF_DEBUG("NtApi: NtPowerInformation(SystemBatteryState) failed: %s", ffDebugNtStatus(status));
+ return "NtPowerInformation(SystemBatteryState) failed";
+ }
+ if (!info.BatteryPresent) {
+ FF_DEBUG("NtApi reports no battery present");
+ return "No battery present";
+ }
+
+ if (info.MaxCapacity != BATTERY_UNKNOWN_CAPACITY && info.RemainingCapacity != BATTERY_UNKNOWN_CAPACITY) {
+ battery->capacity = info.RemainingCapacity * 100.0 / info.MaxCapacity;
+ }
+ battery->status = FF_BATTERY_STATUS_NONE;
+ if (info.AcOnLine) {
+ battery->status |= FF_BATTERY_STATUS_AC_CONNECTED;
+ }
+ if (info.Charging) {
+ battery->status |= FF_BATTERY_STATUS_CHARGING;
+ }
+ if (info.Discharging) {
+ battery->status |= FF_BATTERY_STATUS_DISCHARGING;
+ }
+ if (info.DefaultAlert1 > 0 && info.RemainingCapacity <= info.DefaultAlert1) {
+ battery->status |= FF_BATTERY_STATUS_CRITICAL;
+ }
+ battery->timeRemaining = info.EstimatedTime == BATTERY_UNKNOWN_TIME ? -1 : (int32_t) info.EstimatedTime;
+ return NULL;
+}
+
+const char* ffDetectBattery(FFBatteryOptions* options, FFlist* results) {
+ FF_DEBUG("WMI: start detection");
+
+ FF_LIST_AUTO_DESTROY entries = ffListCreate();
+ detectStaticData(&entries, results);
+ if (results->length == 0) {
+ return NULL;
+ } else if (results->length == 1) {
+ // Fast path for single battery
+ detectWithNtApi(FF_LIST_FIRST(FFBatteryWmiEntry, entries)->result);
+ } else {
+ detectStatus(&entries, results);
+ detectFullChargedCapacity(&entries, results);
+ detectRuntime(&entries, results);
+ }
+ detectCycleCount(&entries, results);
+ if (options->temp) {
+ detectTemperature(&entries, results);
+ }
+
+ FF_LIST_FOR_EACH (FFBatteryWmiEntry, entry, entries) {
+ FF_DEBUG(
+ "WMI: detected battery tag=%lu, name='%s', charge=%.2f%%, status=0x%x, runtime=%d seconds",
+ entry->tag,
+ entry->result->modelName.length ? entry->result->modelName.chars : "<unknown>",
+ entry->result->capacity,
+ entry->result->status,
+ entry->result->timeRemaining);
+ }
+
+ FF_DEBUG("WMI: finished detection, total results=%u", results->length);
+ return NULL;
+}