diff --git a/org.sensord/gnome-sensor-tray/src/extension.js b/org.sensord/gnome-sensor-tray/src/extension.js index 75361d0..546fd74 100644 --- a/org.sensord/gnome-sensor-tray/src/extension.js +++ b/org.sensord/gnome-sensor-tray/src/extension.js @@ -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 = { diff --git a/org.sensord/sensord.py b/org.sensord/sensord.py index 8112f91..80d52ee 100644 --- a/org.sensord/sensord.py +++ b/org.sensord/sensord.py @@ -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")} """ @@ -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), }