mirror of
https://github.com/morgan9e/linux-sys-telemetry
synced 2026-04-14 00:04:07 +09:00
Add Battery sensor via sysfs power_supply
This commit is contained in:
@@ -62,6 +62,33 @@ const CATEGORIES = {
|
||||
summaryKey: 'percent',
|
||||
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 = {
|
||||
|
||||
@@ -12,6 +12,7 @@ Interfaces:
|
||||
org.sensord.Thermal — hwmon temperatures (°C)
|
||||
org.sensord.Cpu — per-core and total usage (%)
|
||||
org.sensord.Memory — memory utilization (bytes/%)
|
||||
org.sensord.Battery — battery state (%/W/status)
|
||||
|
||||
Each interface exposes:
|
||||
GetReadings() → a{sd}
|
||||
@@ -60,6 +61,7 @@ INTROSPECTION = f"""
|
||||
{make_iface_xml("Thermal")}
|
||||
{make_iface_xml("Cpu")}
|
||||
{make_iface_xml("Memory")}
|
||||
{make_iface_xml("Battery")}
|
||||
</node>
|
||||
"""
|
||||
|
||||
@@ -317,6 +319,87 @@ class MemorySensor:
|
||||
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 ────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -325,6 +408,7 @@ SENSORS = {
|
||||
"Thermal": (ThermalSensor, 2),
|
||||
"Cpu": (CpuSensor, 1),
|
||||
"Memory": (MemorySensor, 2),
|
||||
"Battery": (BatterySensor, 5),
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user