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',
|
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 = {
|
||||||
|
|||||||
@@ -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),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user