From 3e7e68a486530a72186fa0f09055ddd3cf7679f1 Mon Sep 17 00:00:00 2001 From: Morgan Date: Wed, 1 Apr 2026 03:08:28 +0900 Subject: [PATCH] =?UTF-8?q?Add=20GetMeta=20introspection,=20drop=20comment?= =?UTF-8?q?s=20and=20=C2=B0F=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gnome-sensor-tray/src/extension.js | 202 +++++++----------- org.sensord/gnome-sensor-tray/src/prefs.js | 7 - .../gnome-sensor-tray/src/sensorClient.js | 69 +++--- org.sensord/sensord.py | 69 +++++- 4 files changed, 162 insertions(+), 185 deletions(-) diff --git a/org.sensord/gnome-sensor-tray/src/extension.js b/org.sensord/gnome-sensor-tray/src/extension.js index 546fd74..f119202 100644 --- a/org.sensord/gnome-sensor-tray/src/extension.js +++ b/org.sensord/gnome-sensor-tray/src/extension.js @@ -13,101 +13,67 @@ import {Extension, gettext as _} from 'resource:///org/gnome/shell/extensions/ex import SensorClient from './sensorClient.js'; import SensorItem from './sensorItem.js'; -// ---------- helpers ---------- - -// Gio.icon_new_for_string only takes one name; use ThemedIcon for fallbacks function safeIcon(names) { if (typeof names === 'string') names = [names]; return new Gio.ThemedIcon({ names }); } -// ---------- category config ---------- +function formatByUnit(unit, val, dec) { + if (!unit) + return (dec ? '%.2f' : '%.1f').format(val); -const CATEGORIES = { - Thermal: { - icon: ['sensors-temperature-symbolic', 'temperature-symbolic', 'dialog-warning-symbolic'], - format: (v, dec, unit) => { - if (unit === 1) v = v * 9 / 5 + 32; - return (dec ? '%.1f' : '%.0f').format(v) + (unit === 1 ? '\u00b0F' : '\u00b0C'); - }, - convert: (v, unit) => unit === 1 ? v * 9 / 5 + 32 : v, - summary: (r) => Math.max(...Object.values(r)), - sortOrder: 0, - }, - Cpu: { - icon: ['utilities-system-monitor-symbolic', 'org.gnome.SystemMonitor-symbolic', 'computer-symbolic'], - format: (v, dec) => (dec ? '%.1f' : '%.0f').format(v) + '%', - summary: (r) => r['total'] ?? null, - sortOrder: 1, - }, - Power: { - icon: ['battery-full-charged-symbolic', 'battery-symbolic', 'plug-symbolic'], - format: (v, dec) => (dec ? '%.2f' : '%.1f').format(v) + ' W', - summary: (r) => r['package-0'] ?? null, - sortOrder: 2, - }, - Memory: { - icon: ['drive-harddisk-symbolic', 'media-memory-symbolic', 'computer-symbolic'], - format: (v, dec, _unit, key) => { - if (key === 'percent' || key === 'swap_percent') - return (dec ? '%.1f' : '%.0f').format(v) + '%'; - if (v >= 1073741824) - return '%.1f GiB'.format(v / 1073741824); - if (v >= 1048576) - return '%.0f MiB'.format(v / 1048576); - return '%.0f KiB'.format(v / 1024); - }, - summary: (r) => r['percent'] ?? null, - 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 = { - icon: ['dialog-information-symbolic'], - format: (v, dec) => (dec ? '%.2f' : '%.1f').format(v), - sortOrder: 99, -}; - -function catCfg(cat) { - return CATEGORIES[cat] || DEFAULT_CATEGORY; + switch (unit) { + case '%': + return (dec ? '%.1f' : '%.0f').format(val) + '%'; + case 'W': + return (dec ? '%.2f' : '%.1f').format(val) + ' W'; + case 'Wh': + return '%.1f Wh'.format(val); + case '\u00b0C': + return (dec ? '%.1f' : '%.0f').format(val) + '\u00b0C'; + case 'bytes': + if (val >= 1073741824) return '%.1f GiB'.format(val / 1073741824); + if (val >= 1048576) return '%.0f MiB'.format(val / 1048576); + return '%.0f KiB'.format(val / 1024); + case 'count': + return '%.0f'.format(val); + case 'bool': + return val ? 'Yes' : 'No'; + default: + if (unit.startsWith('enum:')) { + let names = unit.slice(5).split(','); + let idx = Math.round(val) - 1; + return (idx >= 0 && idx < names.length) ? names[idx] : 'Unknown'; + } + return (dec ? '%.2f' : '%.1f').format(val) + ' ' + unit; + } } -function formatSensor(cat, key, val, dec, unit) { - let cfg = catCfg(cat); - let v = cfg.convert ? cfg.convert(val, unit) : val; - return cfg.format(v, dec, unit, key); +function autoSummary(readings, units) { + if (!readings || !units) + return null; + if ('total' in readings) return { key: 'total', val: readings['total'] }; + if ('percent' in readings) return { key: 'percent', val: readings['percent'] }; + for (let k of Object.keys(readings)) + if (k.endsWith('/percent')) return { key: k, val: readings[k] }; + let k = Object.keys(readings)[0]; + return k ? { key: k, val: readings[k] } : null; } -// ---------- panel button ---------- +const SORT_ORDER = { Thermal: 0, Cpu: 1, Power: 2, Memory: 3, Battery: 4 }; + +function catIcon(cat, meta) { + let m = meta?.get(cat); + if (m?.icon) + return [m.icon, 'dialog-information-symbolic']; + return ['dialog-information-symbolic']; +} + +function formatSensor(cat, key, val, dec, meta) { + let unit = meta?.get(cat)?.units?.[key] ?? null; + return formatByUnit(unit, val, dec); +} class SensorTrayButton extends PanelMenu.Button { @@ -122,31 +88,26 @@ class SensorTrayButton extends PanelMenu.Button { this._path = path; this._client = new SensorClient(); - // menu state - this._subMenus = {}; // category → PopupSubMenuMenuItem - this._menuItems = {}; // fullKey → SensorItem + this._subMenus = {}; + this._menuItems = {}; this._lastKeys = null; - // panel state this._panelBox = new St.BoxLayout(); this.add_child(this._panelBox); - this._hotLabels = {}; // fullKey → St.Label - this._hotIcons = {}; // fullKey → St.Icon + this._hotLabels = {}; + this._hotIcons = {}; this._buildPanel(); - // settings this._sigIds = []; this._connectSetting('hot-sensors', () => { this._buildPanel(); this._updatePanel(); this._syncPinOrnaments(); }); this._connectSetting('show-icon-on-panel', () => { this._buildPanel(); this._updatePanel(); }); this._connectSetting('panel-spacing', () => { this._buildPanel(); this._updatePanel(); }); - this._connectSetting('unit', () => this._refresh()); this._connectSetting('show-decimal-value', () => this._refresh()); this._connectSetting('position-in-panel', () => this._reposition()); this._connectSetting('panel-box-index', () => this._reposition()); this._connectSetting('update-interval', () => this._restartRefreshTimer()); - // throttle UI repaints via a timer this._dirty = false; this._refreshTimerId = 0; this._startRefreshTimer(); @@ -166,8 +127,6 @@ class SensorTrayButton extends PanelMenu.Button { this._sigIds.push(this._settings.connect('changed::' + key, cb)); } - // ---- panel (top bar): pinned sensors ---- - _buildPanel() { this._panelBox.destroy_all_children(); this._hotLabels = {}; @@ -179,17 +138,17 @@ class SensorTrayButton extends PanelMenu.Button { if (hot.length === 0) { this._panelBox.add_child(new St.Icon({ style_class: 'system-status-icon', - gicon: safeIcon(CATEGORIES.Thermal.icon), + gicon: safeIcon(['sensors-temperature-symbolic', 'dialog-information-symbolic']), })); return; } + let meta = this._client.meta; + for (let i = 0; i < hot.length; i++) { let fullKey = hot[i]; let cat = fullKey.split('/')[0]; - let cfg = catCfg(cat); - // spacer between pinned items if (i > 0) { let spacing = this._settings.get_int('panel-spacing'); this._panelBox.add_child(new St.Widget({ width: spacing })); @@ -198,7 +157,7 @@ class SensorTrayButton extends PanelMenu.Button { if (showIcon) { let icon = new St.Icon({ style_class: 'system-status-icon', - gicon: safeIcon(cfg.icon), + gicon: safeIcon(catIcon(cat, meta)), }); this._hotIcons[fullKey] = icon; this._panelBox.add_child(icon); @@ -217,7 +176,7 @@ class SensorTrayButton extends PanelMenu.Button { _updatePanel() { let dec = this._settings.get_boolean('show-decimal-value'); - let unit = this._settings.get_int('unit'); + let meta = this._client.meta; for (let [fullKey, label] of Object.entries(this._hotLabels)) { let parts = fullKey.split('/'); @@ -230,12 +189,10 @@ class SensorTrayButton extends PanelMenu.Button { continue; } - label.text = formatSensor(cat, key, readings[key], dec, unit); + label.text = formatSensor(cat, key, readings[key], dec, meta); } } - // ---- dropdown: collapsed submenus per category ---- - _onSensorChanged(category, _readings) { if (category === null) { this._menuItems = {}; @@ -276,9 +233,9 @@ class SensorTrayButton extends PanelMenu.Button { _sortedEntries() { let entries = []; for (let [cat, readings] of this._client.readings) { - let cfg = catCfg(cat); + let order = SORT_ORDER[cat] ?? 99; for (let key of Object.keys(readings)) - entries.push({ cat, key, fullKey: cat + '/' + key, sortOrder: cfg.sortOrder }); + entries.push({ cat, key, fullKey: cat + '/' + key, sortOrder: order }); } entries.sort((a, b) => { if (a.sortOrder !== b.sortOrder) return a.sortOrder - b.sortOrder; @@ -302,7 +259,6 @@ class SensorTrayButton extends PanelMenu.Button { let hot = this._settings.get_strv('hot-sensors'); - // group by category let grouped = new Map(); for (let e of entries) { if (!grouped.has(e.cat)) @@ -310,17 +266,18 @@ class SensorTrayButton extends PanelMenu.Button { grouped.get(e.cat).push(e); } - for (let [cat, catEntries] of grouped) { - let cfg = catCfg(cat); + let meta = this._client.meta; + + for (let [cat, catEntries] of grouped) { + let iconNames = catIcon(cat, meta); - // create a collapsed submenu for this category let sub = new PopupMenu.PopupSubMenuMenuItem(cat, true); - sub.icon.gicon = safeIcon(cfg.icon); + sub.icon.gicon = safeIcon(iconNames); this._subMenus[cat] = sub; this.menu.addMenuItem(sub); for (let e of catEntries) { - let gicon = safeIcon(cfg.icon); + let gicon = safeIcon(iconNames); let item = new SensorItem(gicon, e.fullKey, e.key, '\u2026'); if (hot.includes(e.fullKey)) @@ -333,7 +290,6 @@ class SensorTrayButton extends PanelMenu.Button { } } - // settings footer this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); let settingsItem = new PopupMenu.PopupMenuItem(_('Settings')); settingsItem.connect('activate', () => { @@ -351,24 +307,22 @@ class SensorTrayButton extends PanelMenu.Button { _updateValues() { let dec = this._settings.get_boolean('show-decimal-value'); - let unit = this._settings.get_int('unit'); + let meta = this._client.meta; for (let [cat, readings] of this._client.readings) { + let units = meta.get(cat)?.units; + for (let [key, val] of Object.entries(readings)) { let item = this._menuItems[cat + '/' + key]; if (item) - item.value = formatSensor(cat, key, val, dec, unit); + item.value = formatSensor(cat, key, val, dec, meta); } - // update submenu header with a summary (e.g. max temp) let sub = this._subMenus[cat]; if (sub && sub.status) { - let cfg = catCfg(cat); - if (cfg.summary) { - let sv = cfg.summary(readings); - if (sv !== null) - sub.status.text = formatSensor(cat, cfg.summaryKey || '', sv, dec, unit); - } + let s = autoSummary(readings, units); + if (s) + sub.status.text = formatByUnit(units?.[s.key], s.val, dec); } } } @@ -397,8 +351,6 @@ class SensorTrayButton extends PanelMenu.Button { this._updatePanel(); } - // ---- panel position ---- - _reposition() { try { if (!this.container?.get_parent()) return; @@ -417,8 +369,6 @@ class SensorTrayButton extends PanelMenu.Button { } } - // ---- cleanup ---- - _onDestroy() { this._client.destroy(); if (this._refreshTimerId) { @@ -435,8 +385,6 @@ class SensorTrayButton extends PanelMenu.Button { } } -// ---------- extension entry point ---------- - export default class SensorTrayExtension extends Extension { enable() { diff --git a/org.sensord/gnome-sensor-tray/src/prefs.js b/org.sensord/gnome-sensor-tray/src/prefs.js index 76e6c27..e75f2fc 100644 --- a/org.sensord/gnome-sensor-tray/src/prefs.js +++ b/org.sensord/gnome-sensor-tray/src/prefs.js @@ -123,13 +123,6 @@ export default class SensorTrayPreferences extends ExtensionPreferences { _createDisplayGroup() { let group = new Adw.PreferencesGroup({ title: _('Display') }); - let unitRow = new Adw.ComboRow({ - title: _('Temperature Unit'), - model: new Gtk.StringList({ strings: ['\u00b0C', '\u00b0F'] }), - }); - this._settings.bind('unit', unitRow, 'selected', Gio.SettingsBindFlags.DEFAULT); - group.add(unitRow); - group.add(this._switch(_('Show Decimal Values'), 'show-decimal-value')); group.add(this._switch(_('Show Icon on Panel'), 'show-icon-on-panel')); diff --git a/org.sensord/gnome-sensor-tray/src/sensorClient.js b/org.sensord/gnome-sensor-tray/src/sensorClient.js index 8397ae1..2aee0c8 100644 --- a/org.sensord/gnome-sensor-tray/src/sensorClient.js +++ b/org.sensord/gnome-sensor-tray/src/sensorClient.js @@ -5,35 +5,18 @@ const BUS_NAME = 'org.sensord'; const OBJECT_PATH = '/org/sensord'; const IFACE_PREFIX = 'org.sensord.'; -/** - * Generic client for all org.sensord.* D-Bus interfaces. - * - * Every interface has the same shape: - * method GetReadings() → a{sd} - * signal Changed(a{sd}) - * - * The client introspects the object once, discovers all sensor interfaces, - * fetches initial readings, then subscribes to Changed signals. - * Callers get a flat Map> that stays current. - */ export default class SensorClient { constructor() { this._conn = null; this._signalIds = []; - // category → { key: value } e.g. "Power" → { "package-0": 42.3 } this._readings = new Map(); + this._meta = new Map(); this._onChanged = null; this._available = false; this._nameWatchId = 0; } - /** - * Connect to the system bus and start receiving sensor data. - * @param {function(string, Object)} onChanged - * Called with (category, readings) whenever a sensor interface emits Changed - * and also once per interface after initial GetReadings. - */ start(onChanged) { this._onChanged = onChanged; @@ -44,7 +27,6 @@ export default class SensorClient { return; } - // Watch for sensord appearing/disappearing on the bus this._nameWatchId = Gio.bus_watch_name_on_connection( this._conn, BUS_NAME, @@ -63,17 +45,15 @@ export default class SensorClient { this._available = false; this._unsubscribeAll(); this._readings.clear(); + this._meta.clear(); if (this._onChanged) - this._onChanged(null, null); // signal "all gone" + this._onChanged(null, null); } - /** - * Introspect /org/sensord, find all org.sensord.* interfaces, - * call GetReadings on each, subscribe to Changed. - */ _discover() { this._unsubscribeAll(); this._readings.clear(); + this._meta.clear(); let introXml; try { @@ -89,7 +69,6 @@ export default class SensorClient { return; } - // Parse interface names from XML — simple regex is fine here let ifaces = []; let re = /interface\s+name="(org\.sensord\.[^"]+)"/g; let m; @@ -97,9 +76,8 @@ export default class SensorClient { ifaces.push(m[1]); for (let iface of ifaces) { - let category = iface.slice(IFACE_PREFIX.length); // "Power", "Thermal", etc. + let category = iface.slice(IFACE_PREFIX.length); - // Subscribe to Changed signal let sid = this._conn.signal_subscribe( BUS_NAME, iface, 'Changed', OBJECT_PATH, null, Gio.DBusSignalFlags.NONE, @@ -112,7 +90,26 @@ export default class SensorClient { ); this._signalIds.push(sid); - // Fetch initial state + this._conn.call( + BUS_NAME, OBJECT_PATH, iface, 'GetMeta', + null, GLib.VariantType.new('(a{sv})'), + Gio.DBusCallFlags.NONE, 3000, null, + (conn, res) => { + try { + let result = conn.call_finish(res); + let [meta] = result.deep_unpack(); + let parsed = {}; + if (meta['icon']) + parsed.icon = meta['icon'].deep_unpack(); + if (meta['units']) + parsed.units = meta['units'].deep_unpack(); + this._meta.set(category, parsed); + } catch (e) { + console.debug(`sensortray: GetMeta(${iface}) unavailable:`, e.message); + } + }, + ); + this._conn.call( BUS_NAME, OBJECT_PATH, iface, 'GetReadings', null, GLib.VariantType.new('(a{sd})'), @@ -140,18 +137,9 @@ export default class SensorClient { this._signalIds = []; } - /** @returns {boolean} true if sensord is on the bus */ - get available() { - return this._available; - } - - /** - * @returns {Map>} - * category → { key: value } snapshot of all current readings - */ - get readings() { - return this._readings; - } + get available() { return this._available; } + get readings() { return this._readings; } + get meta() { return this._meta; } destroy() { this._unsubscribeAll(); @@ -162,5 +150,6 @@ export default class SensorClient { this._conn = null; this._onChanged = null; this._readings.clear(); + this._meta.clear(); } } diff --git a/org.sensord/sensord.py b/org.sensord/sensord.py index 80d52ee..febedf2 100644 --- a/org.sensord/sensord.py +++ b/org.sensord/sensord.py @@ -16,6 +16,7 @@ Interfaces: Each interface exposes: GetReadings() → a{sd} + GetMeta() → a{sv} (icon: s, units: a{ss}) signal Changed(a{sd}) Usage: @@ -51,6 +52,7 @@ POLICY = """\ def make_iface_xml(name): return f""" + """ @@ -73,6 +75,7 @@ class PowerSensor: """RAPL energy counters → watts.""" RAPL_BASE = "/sys/class/powercap/intel-rapl" + ICON = "battery-full-charged-symbolic" class Zone: __slots__ = ("name", "fd", "wrap", "prev_e", "prev_t") @@ -119,7 +122,7 @@ class PowerSensor: if "energy_uj" in files: try: z = self.Zone(root) - z.sample() # prime + z.sample() self.zones.append(z) print(f" power: {z.name}", file=sys.stderr) except OSError as e: @@ -129,6 +132,9 @@ class PowerSensor: def available(self): return bool(self.zones) + def units(self): + return {z.name: "W" for z in self.zones} + def sample(self): r = {} for z in self.zones: @@ -146,6 +152,7 @@ class ThermalSensor: """hwmon temperature sensors → °C.""" HWMON_BASE = "/sys/class/hwmon" + ICON = "sensors-temperature-symbolic" class Chip: __slots__ = ("label", "fd") @@ -182,7 +189,7 @@ class ThermalSensor: try: chip = self.Chip(full_label, path) - chip.read() # test + chip.read() self.chips.append(chip) print(f" thermal: {full_label}", file=sys.stderr) except OSError as e: @@ -200,6 +207,9 @@ class ThermalSensor: def available(self): return bool(self.chips) + def units(self): + return {c.label: "°C" for c in self.chips} + def sample(self): r = {} for c in self.chips: @@ -217,13 +227,15 @@ class ThermalSensor: class CpuSensor: """/proc/stat → per-core and total CPU usage %.""" + ICON = "utilities-system-monitor-symbolic" + def __init__(self): self.fd = None self.prev = {} try: self.fd = os.open("/proc/stat", os.O_RDONLY) - self._read_stat() # prime + self._read_stat() print(f" cpu: {len(self.prev)} entries", file=sys.stderr) except OSError as e: print(f" cpu skip: {e}", file=sys.stderr) @@ -242,12 +254,18 @@ class CpuSensor: parts = line.split() name = parts[0] vals = [int(v) for v in parts[1:]] - # user nice system idle iowait irq softirq steal idle = vals[3] + vals[4] if len(vals) > 4 else vals[3] total = sum(vals) entries[name] = (idle, total) return entries + def units(self): + r = {} + for name in self.prev: + label = "total" if name == "cpu" else name + r[label] = "%" + return r + def sample(self): cur = self._read_stat() r = {} @@ -270,13 +288,14 @@ class CpuSensor: class MemorySensor: """/proc/meminfo → memory stats in bytes and usage %.""" + ICON = "drive-harddisk-symbolic" KEYS = ("MemTotal", "MemAvailable", "MemFree", "SwapTotal", "SwapFree") def __init__(self): self.fd = None try: self.fd = os.open("/proc/meminfo", os.O_RDONLY) - self.sample() # test + self.sample() print(" memory: ok", file=sys.stderr) except OSError as e: print(f" memory skip: {e}", file=sys.stderr) @@ -285,6 +304,13 @@ class MemorySensor: def available(self): return self.fd is not None + def units(self): + return { + "total": "bytes", "available": "bytes", "used": "bytes", + "percent": "%", + "swap_total": "bytes", "swap_used": "bytes", "swap_percent": "%", + } + def sample(self): os.lseek(self.fd, 0, os.SEEK_SET) raw = os.read(self.fd, 4096).decode() @@ -294,7 +320,7 @@ class MemorySensor: parts = line.split() key = parts[0].rstrip(":") if key in self.KEYS: - vals[key] = int(parts[1]) * 1024 # kB → bytes + vals[key] = int(parts[1]) * 1024 r = {} mt = vals.get("MemTotal", 0) @@ -323,8 +349,8 @@ class BatterySensor: """power_supply sysfs → battery state.""" PS_BASE = "/sys/class/power_supply" + ICON = "battery-symbolic" - # status string → numeric code for a{sd} STATUS_MAP = { "Charging": 1.0, "Discharging": 2.0, "Not charging": 3.0, "Full": 4.0, @@ -365,6 +391,20 @@ class BatterySensor: def available(self): return any(s.is_battery for s in self.supplies) + def units(self): + r = {} + for s in self.supplies: + if s.is_battery: + r[f"{s.name}/percent"] = "%" + r[f"{s.name}/status"] = "enum:Charging,Discharging,Not charging,Full" + r[f"{s.name}/power"] = "W" + r[f"{s.name}/energy_now"] = "Wh" + r[f"{s.name}/energy_full"] = "Wh" + r[f"{s.name}/cycles"] = "count" + else: + r[f"{s.name}/online"] = "bool" + return r + def sample(self): r = {} for s in self.supplies: @@ -404,7 +444,7 @@ class BatterySensor: SENSORS = { - "Power": (PowerSensor, 1), # iface name, interval (sec) + "Power": (PowerSensor, 1), "Thermal": (ThermalSensor, 2), "Cpu": (CpuSensor, 1), "Memory": (MemorySensor, 2), @@ -416,9 +456,9 @@ class Daemon: def __init__(self): self.loop = GLib.MainLoop() self.bus = None - self.sensors = {} # name → sensor instance - self.readings = {} # name → latest {key: value} - self.pending = {} # name → (cls, interval) — not yet available + self.sensors = {} + self.readings = {} + self.pending = {} self.node = Gio.DBusNodeInfo.new_for_xml(INTROSPECTION) for name, (cls, interval) in SENSORS.items(): @@ -458,6 +498,13 @@ class Daemon: name = iface.rsplit(".", 1)[-1] if method == "GetReadings": invocation.return_value(GLib.Variant("(a{sd})", (self.readings.get(name, {}),))) + elif method == "GetMeta": + meta = {} + sensor = self.sensors.get(name) + if sensor: + meta["icon"] = GLib.Variant("s", sensor.ICON) + meta["units"] = GLib.Variant("a{ss}", sensor.units()) + invocation.return_value(GLib.Variant("(a{sv})", (meta,))) else: invocation.return_dbus_error("org.freedesktop.DBus.Error.UnknownMethod", method)