mirror of
https://github.com/morgan9e/linux-sys-telemetry
synced 2026-04-14 00:04:07 +09:00
Fix
This commit is contained in:
@@ -63,6 +63,12 @@ class BatteryClient:
|
|||||||
'(a(dddds))')
|
'(a(dddds))')
|
||||||
return list(r[0]) if r else []
|
return list(r[0]) if r else []
|
||||||
|
|
||||||
|
def get_events(self, date_str):
|
||||||
|
"""Returns list of (ts, type_str)"""
|
||||||
|
r = self._call('GetEvents', GLib.Variant('(s)', (date_str,)),
|
||||||
|
'(a(ds))')
|
||||||
|
return list(r[0]) if r else []
|
||||||
|
|
||||||
def get_current(self):
|
def get_current(self):
|
||||||
r = self._call('GetCurrent', None, '(dddds)')
|
r = self._call('GetCurrent', None, '(dddds)')
|
||||||
if r:
|
if r:
|
||||||
@@ -433,9 +439,10 @@ class SessionRow(Adw.ActionRow):
|
|||||||
|
|
||||||
|
|
||||||
class GapRow(Adw.ActionRow):
|
class GapRow(Adw.ActionRow):
|
||||||
"""Inferred gap between sessions (suspend/shutdown)."""
|
"""Gap between sessions — labeled from events if available, otherwise inferred."""
|
||||||
|
|
||||||
def __init__(self, prev_end_ts, prev_end_level, next_start_ts, next_start_level):
|
def __init__(self, prev_end_ts, prev_end_level, next_start_ts, next_start_level,
|
||||||
|
gap_event=None):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
t_start = datetime.fromtimestamp(prev_end_ts).strftime('%H:%M')
|
t_start = datetime.fromtimestamp(prev_end_ts).strftime('%H:%M')
|
||||||
@@ -443,8 +450,20 @@ class GapRow(Adw.ActionRow):
|
|||||||
duration = next_start_ts - prev_end_ts
|
duration = next_start_ts - prev_end_ts
|
||||||
delta = next_start_level - prev_end_level
|
delta = next_start_level - prev_end_level
|
||||||
|
|
||||||
|
if gap_event == 'suspend':
|
||||||
|
if delta > 0.5:
|
||||||
|
kind = 'Suspended (charged)'
|
||||||
|
icon = 'battery-level-50-charging-symbolic'
|
||||||
|
else:
|
||||||
|
kind = 'Suspended'
|
||||||
|
icon = 'media-playback-pause-symbolic'
|
||||||
|
elif gap_event == 'shutdown':
|
||||||
|
kind = 'Shutdown'
|
||||||
|
icon = 'system-shutdown-symbolic'
|
||||||
|
else:
|
||||||
|
# fallback: infer from level delta
|
||||||
if abs(delta) < 0.5:
|
if abs(delta) < 0.5:
|
||||||
kind = 'Shutdown / Hibernate'
|
kind = 'Shutdown / Suspend'
|
||||||
icon = 'system-shutdown-symbolic'
|
icon = 'system-shutdown-symbolic'
|
||||||
elif delta > 0:
|
elif delta > 0:
|
||||||
kind = 'Suspended (charged)'
|
kind = 'Suspended (charged)'
|
||||||
@@ -596,6 +615,7 @@ class BatteryWindow(Adw.ApplicationWindow):
|
|||||||
|
|
||||||
samples = self.client.get_samples(date_str)
|
samples = self.client.get_samples(date_str)
|
||||||
sessions = self.client.get_sessions(date_str)
|
sessions = self.client.get_sessions(date_str)
|
||||||
|
events = self.client.get_events(date_str)
|
||||||
day_start = datetime.combine(
|
day_start = datetime.combine(
|
||||||
self.current_date, datetime.min.time()).timestamp()
|
self.current_date, datetime.min.time()).timestamp()
|
||||||
|
|
||||||
@@ -624,39 +644,53 @@ class BatteryWindow(Adw.ApplicationWindow):
|
|||||||
self.session_group.set_margin_top(16)
|
self.session_group.set_margin_top(16)
|
||||||
self.content.append(self.session_group)
|
self.content.append(self.session_group)
|
||||||
|
|
||||||
# merge adjacent sessions with same status and small gaps
|
MERGE_GAP = 180 # seconds — merge same-status if gap < this (not a real suspend)
|
||||||
MERGE_GAP = 180 # 3 minutes — merge if same status and gap < this
|
GAP_THRESHOLD = 180 # show gap row if gap >= this
|
||||||
GAP_THRESHOLD = 180 # show gap row if gap >= this between different statuses
|
TRIVIAL_DUR = 300 # seconds — trivial if 0% change AND shorter than this
|
||||||
LVL_THRESHOLD = 0
|
|
||||||
|
|
||||||
merged = []
|
# pass 1: drop trivial sessions (0% change + short duration)
|
||||||
|
filtered = []
|
||||||
for s in sessions:
|
for s in sessions:
|
||||||
|
start_ts, end_ts, start_lvl, end_lvl, status = s
|
||||||
|
dur = end_ts - start_ts
|
||||||
|
if abs(end_lvl - start_lvl) < 0.5 and dur < TRIVIAL_DUR:
|
||||||
|
continue
|
||||||
|
filtered.append(s)
|
||||||
|
|
||||||
|
# pass 2: merge adjacent same-status sessions with small gaps
|
||||||
|
merged = []
|
||||||
|
for s in filtered:
|
||||||
start_ts, end_ts, start_lvl, end_lvl, status = s
|
start_ts, end_ts, start_lvl, end_lvl, status = s
|
||||||
if merged:
|
if merged:
|
||||||
p_start, p_end, p_slvl, p_elvl, p_status = merged[-1]
|
p_start, p_end, p_slvl, p_elvl, p_status = merged[-1]
|
||||||
gap = start_ts - p_end
|
gap = start_ts - p_end
|
||||||
if p_status == status and gap < MERGE_GAP:
|
if p_status == status and gap < MERGE_GAP:
|
||||||
# extend previous session
|
|
||||||
merged[-1] = (p_start, end_ts, p_slvl, end_lvl, status)
|
merged[-1] = (p_start, end_ts, p_slvl, end_lvl, status)
|
||||||
continue
|
continue
|
||||||
merged.append(s)
|
merged.append(s)
|
||||||
|
|
||||||
|
def find_gap_event(gap_start, gap_end):
|
||||||
|
"""Find a suspend or shutdown event within a gap period."""
|
||||||
|
for ts, etype in events:
|
||||||
|
if gap_start <= ts <= gap_end and etype in ('suspend', 'shutdown'):
|
||||||
|
return etype
|
||||||
|
return None
|
||||||
|
|
||||||
prev = None
|
prev = None
|
||||||
for s in merged:
|
for s in merged:
|
||||||
start_ts, end_ts, start_lvl, end_lvl, status = s
|
start_ts, end_ts, start_lvl, end_lvl, status = s
|
||||||
if end_ts - start_ts < 30:
|
|
||||||
prev = s
|
|
||||||
continue
|
|
||||||
|
|
||||||
if prev is not None:
|
if prev is not None:
|
||||||
_, prev_end, _, prev_end_lvl, _ = prev
|
_, prev_end, _, prev_end_lvl, _ = prev
|
||||||
gap = start_ts - prev_end
|
gap = start_ts - prev_end
|
||||||
if gap > GAP_THRESHOLD:
|
if gap > GAP_THRESHOLD:
|
||||||
gap_row = GapRow(prev_end, prev_end_lvl, start_ts, start_lvl)
|
gap_event = find_gap_event(prev_end, start_ts)
|
||||||
self.session_group.add(gap_row)
|
self.session_group.add(
|
||||||
|
GapRow(prev_end, prev_end_lvl, start_ts, start_lvl,
|
||||||
|
gap_event))
|
||||||
|
|
||||||
row = SessionRow(start_ts, end_ts, start_lvl, end_lvl, status)
|
self.session_group.add(
|
||||||
self.session_group.add(row)
|
SessionRow(start_ts, end_ts, start_lvl, end_lvl, status))
|
||||||
prev = s
|
prev = s
|
||||||
|
|
||||||
if not merged:
|
if not merged:
|
||||||
|
|||||||
32
org.batteryd/batteryd
Normal file → Executable file
32
org.batteryd/batteryd
Normal file → Executable file
@@ -48,6 +48,10 @@ DBUS_IFACE_XML = '''<node>
|
|||||||
<arg direction="in" name="date" type="s"/>
|
<arg direction="in" name="date" type="s"/>
|
||||||
<arg direction="out" name="sessions" type="a(dddds)"/>
|
<arg direction="out" name="sessions" type="a(dddds)"/>
|
||||||
</method>
|
</method>
|
||||||
|
<method name="GetEvents">
|
||||||
|
<arg direction="in" name="date" type="s"/>
|
||||||
|
<arg direction="out" name="events" type="a(ds)"/>
|
||||||
|
</method>
|
||||||
<method name="GetCurrent">
|
<method name="GetCurrent">
|
||||||
<arg direction="out" name="level" type="d"/>
|
<arg direction="out" name="level" type="d"/>
|
||||||
<arg direction="out" name="energy_wh" type="d"/>
|
<arg direction="out" name="energy_wh" type="d"/>
|
||||||
@@ -131,6 +135,13 @@ def open_db():
|
|||||||
)
|
)
|
||||||
''')
|
''')
|
||||||
db.execute('CREATE INDEX IF NOT EXISTS idx_sessions_ts ON sessions (start_ts)')
|
db.execute('CREATE INDEX IF NOT EXISTS idx_sessions_ts ON sessions (start_ts)')
|
||||||
|
db.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS events (
|
||||||
|
ts REAL NOT NULL,
|
||||||
|
type TEXT NOT NULL
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
db.execute('CREATE INDEX IF NOT EXISTS idx_events_ts ON events (ts)')
|
||||||
|
|
||||||
# migrations
|
# migrations
|
||||||
cols = {r[1] for r in db.execute('PRAGMA table_info(samples)')}
|
cols = {r[1] for r in db.execute('PRAGMA table_info(samples)')}
|
||||||
@@ -203,6 +214,11 @@ class Battery:
|
|||||||
(self.session_start_ts, end_ts,
|
(self.session_start_ts, end_ts,
|
||||||
self.session_start_level, end_level, status))
|
self.session_start_level, end_level, status))
|
||||||
|
|
||||||
|
def _log_event(self, event_type):
|
||||||
|
now = time.time()
|
||||||
|
self.db.execute('INSERT INTO events (ts, type) VALUES (?, ?)',
|
||||||
|
(now, event_type))
|
||||||
|
|
||||||
def on_suspend(self):
|
def on_suspend(self):
|
||||||
reading = read_battery(self.sys_bus, self.bat_path)
|
reading = read_battery(self.sys_bus, self.bat_path)
|
||||||
if reading:
|
if reading:
|
||||||
@@ -210,6 +226,7 @@ class Battery:
|
|||||||
now = time.time()
|
now = time.time()
|
||||||
if self.current and self.last_state is not None:
|
if self.current and self.last_state is not None:
|
||||||
self._close_session(now, self.current['level'])
|
self._close_session(now, self.current['level'])
|
||||||
|
self._log_event('suspend')
|
||||||
self.flush()
|
self.flush()
|
||||||
self.session_start_ts = 0
|
self.session_start_ts = 0
|
||||||
self.last_state = None
|
self.last_state = None
|
||||||
@@ -217,6 +234,7 @@ class Battery:
|
|||||||
self.current['level'] if self.current else 0)
|
self.current['level'] if self.current else 0)
|
||||||
|
|
||||||
def on_resume(self):
|
def on_resume(self):
|
||||||
|
self._log_event('resume')
|
||||||
reading = read_battery(self.sys_bus, self.bat_path)
|
reading = read_battery(self.sys_bus, self.bat_path)
|
||||||
if reading is None:
|
if reading is None:
|
||||||
return
|
return
|
||||||
@@ -249,6 +267,7 @@ class Battery:
|
|||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
if self.current and self.last_state is not None:
|
if self.current and self.last_state is not None:
|
||||||
self._close_session(time.time(), self.current['level'])
|
self._close_session(time.time(), self.current['level'])
|
||||||
|
self._log_event('shutdown')
|
||||||
self.flush()
|
self.flush()
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
|
|
||||||
@@ -270,6 +289,7 @@ class Battery:
|
|||||||
reading['power_w'], reading['voltage_v'],
|
reading['power_w'], reading['voltage_v'],
|
||||||
STATE_NAMES.get(reading['state'], '?'))
|
STATE_NAMES.get(reading['state'], '?'))
|
||||||
|
|
||||||
|
self._log_event('start')
|
||||||
self.flush_timer_id = GLib.timeout_add_seconds(
|
self.flush_timer_id = GLib.timeout_add_seconds(
|
||||||
FLUSH_INTERVAL, self._on_flush_timer)
|
FLUSH_INTERVAL, self._on_flush_timer)
|
||||||
|
|
||||||
@@ -307,6 +327,15 @@ class Battery:
|
|||||||
return [(float(s), float(e), float(sl), float(el), st)
|
return [(float(s), float(e), float(sl), float(el), st)
|
||||||
for s, e, sl, el, st in rows]
|
for s, e, sl, el, st in rows]
|
||||||
|
|
||||||
|
def query_events(self, date_str):
|
||||||
|
day_start, day_end = _local_day_range(date_str)
|
||||||
|
rows = self.db.execute('''
|
||||||
|
SELECT ts, type FROM events
|
||||||
|
WHERE ts >= ? AND ts < ?
|
||||||
|
ORDER BY ts
|
||||||
|
''', (day_start, day_end)).fetchall()
|
||||||
|
return [(float(ts), t) for ts, t in rows]
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.DEBUG if os.environ.get('BATTERYD_DEBUG') else logging.INFO,
|
level=logging.DEBUG if os.environ.get('BATTERYD_DEBUG') else logging.INFO,
|
||||||
@@ -355,6 +384,9 @@ def main():
|
|||||||
elif method == 'GetSessions':
|
elif method == 'GetSessions':
|
||||||
rows = tracker.query_sessions(params.unpack()[0])
|
rows = tracker.query_sessions(params.unpack()[0])
|
||||||
invocation.return_value(GLib.Variant('(a(dddds))', (rows,)))
|
invocation.return_value(GLib.Variant('(a(dddds))', (rows,)))
|
||||||
|
elif method == 'GetEvents':
|
||||||
|
rows = tracker.query_events(params.unpack()[0])
|
||||||
|
invocation.return_value(GLib.Variant('(a(ds))', (rows,)))
|
||||||
elif method == 'GetCurrent':
|
elif method == 'GetCurrent':
|
||||||
if tracker.current:
|
if tracker.current:
|
||||||
r = tracker.current
|
r = tracker.current
|
||||||
|
|||||||
Reference in New Issue
Block a user