ohmu
·
2026-10-15
1#include "battery.h"
2#include "common/io.h"
3#include "common/strutil.h"
4#include "common/debug.h"
5
6#include <dirent.h>
7#include <unistd.h>
8#include <fcntl.h>
9#include <string.h>
10
11// https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-class-power
12
13static bool parseBattery(int dfd, const char* id, FFBatteryOptions* options, FFlist* results, bool* acConnected) {
14 FF_STRBUF_AUTO_DESTROY tmpBuffer = ffStrbufCreate();
15
16 {
17 char present = '\0';
18 if (ffReadFileDataRelative(dfd, "present", 1, &present) && present == '0') {
19 FF_DEBUG("Battery \"%s\": Not present", id);
20 return false;
21 }
22 }
23
24 // type must exist
25 if (!ffReadFileBufferRelative(dfd, "type", &tmpBuffer)) {
26 FF_DEBUG("Battery \"%s\": No type file", id);
27 return false;
28 }
29 ffStrbufTrimRightSpace(&tmpBuffer);
30 if (ffStrbufEqualS(&tmpBuffer, "Mains")) {
31 if (*acConnected) {
32 FF_DEBUG("Battery \"%s\": Type is Mains, but AC is already connected", id);
33 return false;
34 }
35
36 char online = '\0';
37 if (ffReadFileDataRelative(dfd, "online", 1, &online) == 1 && online == '1') {
38 *acConnected = true;
39 }
40 FF_DEBUG("Battery \"%s\": Type is Mains, AC Connected: %s", id, *acConnected ? "Yes" : "No");
41 return false;
42 } else if (!ffStrbufEqualS(&tmpBuffer, "Battery")) {
43 FF_DEBUG("Battery \"%s\": Type is not Battery or Mains, but \"%s\"", id, tmpBuffer.chars);
44 return false;
45 }
46
47 // scope may not exist or must not be "Device"
48 if (ffReadFileBufferRelative(dfd, "scope", &tmpBuffer)) {
49 ffStrbufTrimRightSpace(&tmpBuffer);
50
51 FF_DEBUG("Battery \"%s\": Scope is \"%s\"", id, tmpBuffer.chars);
52 if (ffStrbufEqualS(&tmpBuffer, "Device")) {
53 return false;
54 }
55 }
56
57 // `capacity` must exist
58 // This is expensive in my laptop
59 if (!ffReadFileBufferRelative(dfd, "capacity", &tmpBuffer)) {
60 FF_DEBUG("Battery \"%s\": No capacity file", id);
61 return false;
62 }
63
64 FFBatteryResult* result = FF_LIST_ADD(FFBatteryResult, *results);
65 ffStrbufInit(&result->manufacturer);
66 ffStrbufInit(&result->modelName);
67 ffStrbufInit(&result->technology);
68 ffStrbufInit(&result->serial);
69 ffStrbufInit(&result->manufactureDate);
70 result->status = FF_BATTERY_STATUS_NONE;
71 result->capacity = ffStrbufToDouble(&tmpBuffer, 0);
72 result->cycleCount = 0;
73 result->temperature = FF_BATTERY_TEMP_UNSET;
74 result->timeRemaining = -1;
75
76 // At this point, we have a battery. Try to get as much values as possible.
77
78 if (ffReadFileBufferRelative(dfd, "manufacturer", &result->manufacturer)) {
79 ffStrbufTrimRightSpace(&result->manufacturer);
80 } else if (ffStrEquals(id, "macsmc-battery")) { // asahi
81 ffStrbufSetStatic(&result->manufacturer, "Apple Inc.");
82 }
83
84 if (ffReadFileBufferRelative(dfd, "model_name", &result->modelName)) {
85 ffStrbufTrimRightSpace(&result->modelName);
86 }
87
88 if (ffReadFileBufferRelative(dfd, "technology", &result->technology)) {
89 ffStrbufTrimRightSpace(&result->technology);
90 }
91
92 if (ffReadFileBufferRelative(dfd, "status", &tmpBuffer)) {
93 ffStrbufTrimRightSpace(&tmpBuffer);
94 }
95
96 // Unknown, Charging, Discharging, Not charging, Full
97
98 if (ffStrbufEqualS(&tmpBuffer, "Discharging")) {
99 result->status |= FF_BATTERY_STATUS_DISCHARGING;
100 FF_STRBUF_AUTO_DESTROY now = ffStrbufCreate();
101 if (ffReadFileBufferRelative(dfd, "time_to_empty_now", &now)) {
102 result->timeRemaining = (int32_t) ffStrbufToSInt(&now, 0);
103 } else {
104 if (ffReadFileBufferRelative(dfd, "charge_now", &now)) {
105 int64_t chargeNow = ffStrbufToSInt(&now, 0);
106 if (chargeNow > 0) {
107 if (ffReadFileBufferRelative(dfd, "current_now", &now)) {
108 int64_t currentNow = ffStrbufToSInt(&now, INT64_MIN);
109 if (currentNow < 0) {
110 currentNow = -currentNow;
111 }
112 if (currentNow > 0) {
113 result->timeRemaining = (int32_t) ((chargeNow * 3600) / currentNow);
114 }
115 }
116 }
117 }
118 }
119 } else if (ffStrbufEqualS(&tmpBuffer, "Charging")) {
120 result->status |= FF_BATTERY_STATUS_CHARGING;
121 } else if (ffStrbufEqualS(&tmpBuffer, "Unknown")) {
122 result->status |= FF_BATTERY_STATUS_UNKNOWN;
123 }
124
125 if (ffReadFileBufferRelative(dfd, "capacity_level", &tmpBuffer)) {
126 ffStrbufTrimRightSpace(&tmpBuffer);
127 if (ffStrbufEqualS(&tmpBuffer, "Critical")) {
128 result->status |= FF_BATTERY_STATUS_CRITICAL;
129 }
130 }
131
132 if (ffReadFileBufferRelative(dfd, "serial_number", &result->serial)) {
133 ffStrbufTrimRightSpace(&result->serial);
134 }
135
136 if (ffReadFileBufferRelative(dfd, "cycle_count", &tmpBuffer)) {
137 int64_t cycleCount = ffStrbufToSInt(&tmpBuffer, 0);
138 result->cycleCount = cycleCount < 0 || cycleCount > UINT32_MAX ? 0 : (uint32_t) cycleCount;
139 }
140
141 if (ffReadFileBufferRelative(dfd, "manufacture_year", &tmpBuffer)) {
142 int year = (int) ffStrbufToSInt(&tmpBuffer, 0);
143 if (year > 0) {
144 if (ffReadFileBufferRelative(dfd, "manufacture_month", &tmpBuffer)) {
145 int month = (int) ffStrbufToSInt(&tmpBuffer, 0);
146 if (month > 0) {
147 if (ffReadFileBufferRelative(dfd, "manufacture_day", &tmpBuffer)) {
148 int day = (int) ffStrbufToSInt(&tmpBuffer, 0);
149 if (day > 0) {
150 ffStrbufSetF(&result->manufactureDate, "%.4d-%.2d-%.2d", year, month, day);
151 }
152 }
153 }
154 }
155 }
156 }
157
158 if (options->temp) {
159 if (ffReadFileBufferRelative(dfd, "temp", &tmpBuffer)) {
160 result->temperature = ffStrbufToDouble(&tmpBuffer, FF_BATTERY_TEMP_UNSET);
161 if (result->temperature != FF_BATTERY_TEMP_UNSET) {
162 result->temperature /= 10;
163 }
164 }
165 }
166
167 FF_DEBUG("Battery \"%s\": Capacity: %.2f%%, Status: \"%x\", Time Remaining: %d seconds, Temperature: %.1f°C, Cycle Count: %u",
168 id,
169 result->capacity,
170 result->status,
171 result->timeRemaining,
172 result->temperature,
173 result->cycleCount);
174 return true;
175}
176
177const char* ffDetectBattery(FFBatteryOptions* options, FFlist* results) {
178 FF_AUTO_CLOSE_DIR DIR* dirp = opendir("/sys/class/power_supply/");
179 if (dirp == nullptr) {
180 FF_DEBUG("opendir(\"/sys/class/power_supply/\") failed: %s", strerror(errno));
181 return "opendir(\"/sys/class/power_supply/\") == nullptr";
182 }
183
184 bool acConnected = false;
185
186 struct dirent* entry;
187 while ((entry = readdir(dirp)) != nullptr) {
188 if (entry->d_name[0] == '.') {
189 continue;
190 }
191
192 FF_AUTO_CLOSE_FD int dfd = openat(dirfd(dirp), entry->d_name, O_RDONLY | O_CLOEXEC | O_PATH | O_DIRECTORY);
193 if (dfd >= 0) {
194 parseBattery(dfd, entry->d_name, options, results, &acConnected);
195 }
196 }
197
198 if (acConnected) {
199 FF_LIST_FOR_EACH (FFBatteryResult, batt, *results) {
200 batt->status |= FF_BATTERY_STATUS_AC_CONNECTED;
201 }
202 }
203
204 return nullptr;
205}