Add Battery sensor via sysfs power_supply

This commit is contained in:
2026-04-01 02:52:36 +09:00
parent 51106dd300
commit a5259088d6
2 changed files with 111 additions and 0 deletions

View File

@@ -62,6 +62,33 @@ const CATEGORIES = {
summaryKey: 'percent', summaryKey: 'percent',
sortOrder: 3, sortOrder: 3,
}, },
Battery: {
icon: ['battery-symbolic', 'battery-full-charged-symbolic', 'plug-symbolic'],
// status codes: 1=Charging, 2=Discharging, 3=Not charging, 4=Full
_statusNames: { 1: 'Charging', 2: 'Discharging', 3: 'Not charging', 4: 'Full' },
format: function (v, dec, _unit, key) {
if (key.endsWith('/percent'))
return (dec ? '%.1f' : '%.0f').format(v) + '%';
if (key.endsWith('/status'))
return this._statusNames[v] ?? 'Unknown';
if (key.endsWith('/power'))
return (dec ? '%.2f' : '%.1f').format(v) + ' W';
if (key.endsWith('/energy_now') || key.endsWith('/energy_full'))
return '%.1f Wh'.format(v);
if (key.endsWith('/cycles'))
return '%.0f'.format(v);
if (key.endsWith('/online'))
return v ? 'Yes' : 'No';
return (dec ? '%.2f' : '%.1f').format(v);
},
summary: (r) => {
for (let k of Object.keys(r))
if (k.endsWith('/percent')) return r[k];
return null;
},
summaryKey: 'percent',
sortOrder: 4,
},
}; };
const DEFAULT_CATEGORY = { const DEFAULT_CATEGORY = {

View File

@@ -12,6 +12,7 @@ Interfaces:
org.sensord.Thermal — hwmon temperatures (°C) org.sensord.Thermal — hwmon temperatures (°C)
org.sensord.Cpu — per-core and total usage (%) org.sensord.Cpu — per-core and total usage (%)
org.sensord.Memory — memory utilization (bytes/%) org.sensord.Memory — memory utilization (bytes/%)
org.sensord.Battery — battery state (%/W/status)
Each interface exposes: Each interface exposes:
GetReadings() → a{sd} GetReadings() → a{sd}
@@ -60,6 +61,7 @@ INTROSPECTION = f"""
{make_iface_xml("Thermal")} {make_iface_xml("Thermal")}
{make_iface_xml("Cpu")} {make_iface_xml("Cpu")}
{make_iface_xml("Memory")} {make_iface_xml("Memory")}
{make_iface_xml("Battery")}
</node> </node>
""" """
@@ -317,6 +319,87 @@ class MemorySensor:
os.close(self.fd) os.close(self.fd)
class BatterySensor:
"""power_supply sysfs → battery state."""
PS_BASE = "/sys/class/power_supply"
# status string → numeric code for a{sd}
STATUS_MAP = {
"Charging": 1.0, "Discharging": 2.0,
"Not charging": 3.0, "Full": 4.0,
}
class Supply:
__slots__ = ("name", "path", "is_battery")
def __init__(self, name, path, is_battery):
self.name = name
self.path = path
self.is_battery = is_battery
def __init__(self):
self.supplies = []
if not os.path.isdir(self.PS_BASE):
return
for entry in sorted(os.listdir(self.PS_BASE)):
path = os.path.join(self.PS_BASE, entry)
ptype = self._read(path, "type")
if ptype == "Battery":
self.supplies.append(self.Supply(entry, path, True))
print(f" battery: {entry}", file=sys.stderr)
elif ptype == "Mains":
self.supplies.append(self.Supply(entry, path, False))
print(f" battery: {entry} (ac)", file=sys.stderr)
@staticmethod
def _read(path, name):
try:
with open(os.path.join(path, name)) as f:
return f.read().strip()
except OSError:
return None
@property
def available(self):
return any(s.is_battery for s in self.supplies)
def sample(self):
r = {}
for s in self.supplies:
if s.is_battery:
cap = self._read(s.path, "capacity")
if cap is not None:
r[f"{s.name}/percent"] = float(cap)
status = self._read(s.path, "status")
if status is not None:
r[f"{s.name}/status"] = self.STATUS_MAP.get(status, 0.0)
power = self._read(s.path, "power_now")
if power is not None:
r[f"{s.name}/power"] = round(int(power) / 1e6, 2)
e_now = self._read(s.path, "energy_now")
e_full = self._read(s.path, "energy_full")
if e_now is not None and e_full is not None:
r[f"{s.name}/energy_now"] = round(int(e_now) / 1e6, 2)
r[f"{s.name}/energy_full"] = round(int(e_full) / 1e6, 2)
cycles = self._read(s.path, "cycle_count")
if cycles is not None:
r[f"{s.name}/cycles"] = float(cycles)
else:
online = self._read(s.path, "online")
if online is not None:
r[f"{s.name}/online"] = float(online)
return r
def close(self):
pass
# ── daemon ──────────────────────────────────────────────────── # ── daemon ────────────────────────────────────────────────────
@@ -325,6 +408,7 @@ SENSORS = {
"Thermal": (ThermalSensor, 2), "Thermal": (ThermalSensor, 2),
"Cpu": (CpuSensor, 1), "Cpu": (CpuSensor, 1),
"Memory": (MemorySensor, 2), "Memory": (MemorySensor, 2),
"Battery": (BatterySensor, 5),
} }