# DBus Next Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Replace the GLib/PyGObject DBus event loop with `dbus-next`'s asyncio backend.

**Architecture:** Keep `power_thread` unchanged as the register-writing worker. Move DBus access into small helper functions in `throttled.py` so callbacks can be unit-tested without a live system bus. The main thread runs an asyncio DBus loop and performs shutdown cleanup around the existing worker threads.

**Tech Stack:** Python stdlib `asyncio`, `threading.Event`, `dbus-next==0.2.3`, `configparser==7.2.0`.

---

### Task 1: Add Callback Tests

**Files:**
- Create: `tests/test_dbus_helpers.py`
- Modify: none

- [ ] **Step 1: Write the failing tests**

```python
import importlib.util
import unittest
from pathlib import Path
from unittest import mock


ROOT = Path(__file__).resolve().parents[1]


def load_throttled():
    spec = importlib.util.spec_from_file_location('throttled_under_test', ROOT / 'throttled.py')
    module = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(module)
    return module


class Variant:
    def __init__(self, value):
        self.value = value


class DBusHelperTests(unittest.TestCase):
    def test_ac_signal_callback_updates_power_source(self):
        throttled = load_throttled()
        throttled.power['source'] = 'BATTERY'
        throttled.power['method'] = 'polling'

        throttled.handle_ac_properties_changed('org.freedesktop.UPower', {'OnBattery': Variant(False)}, [])

        self.assertEqual(throttled.power, {'source': 'AC', 'method': 'dbus'})

    def test_resume_callback_reapplies_settings_only_after_wake(self):
        throttled = load_throttled()
        calls = []

        with mock.patch.object(throttled, 'undervolt', lambda config: calls.append(('undervolt', config))):
            with mock.patch.object(throttled, 'set_icc_max', lambda config: calls.append(('iccmax', config))):
                throttled.handle_sleep_prepare(True, 'config')
                self.assertEqual(calls, [])

                throttled.handle_sleep_prepare(False, 'config')
                self.assertEqual(calls, [('undervolt', 'config'), ('iccmax', 'config')])

    def test_dbus_resume_signal_enabled_when_undervolt_or_iccmax_configured(self):
        throttled = load_throttled()
        config = throttled.configparser.ConfigParser()
        config.add_section('UNDERVOLT')
        config.set('UNDERVOLT', 'CORE', '-50')

        self.assertIs(throttled.should_listen_for_resume(config), True)

    def test_dbus_resume_signal_disabled_without_undervolt_or_iccmax(self):
        throttled = load_throttled()
        config = throttled.configparser.ConfigParser()

        self.assertIs(throttled.should_listen_for_resume(config), False)


if __name__ == '__main__':
    unittest.main()
```

- [ ] **Step 2: Run tests to verify they fail**

Run: `python3 -m unittest tests/test_dbus_helpers.py`

Expected: FAIL because `handle_ac_properties_changed`, `handle_sleep_prepare`, and `should_listen_for_resume` are not defined.

### Task 2: Implement dbus-next Runtime

**Files:**
- Modify: `throttled.py`
- Modify: `requirements.txt`
- Modify: `package/PKGBUILD`
- Modify: `README.md`

- [ ] **Step 1: Update imports and dependencies**

Replace GLib/dbus-python imports with:

```python
import asyncio

from dbus_next.aio import MessageBus
from dbus_next.constants import BusType
from dbus_next.errors import DBusError
```

Set `requirements.txt` to:

```text
configparser==7.2.0
dbus-next==0.2.3
```

- [ ] **Step 2: Add testable helper functions**

Add helper functions near the existing power-source helpers:

```python
def unwrap_dbus_value(value):
    return value.value if hasattr(value, 'value') else value


def handle_sleep_prepare(sleeping, config):
    if not sleeping:
        undervolt(config)
        set_icc_max(config)


def handle_ac_properties_changed(if_name, changed, invalidated):
    if "OnBattery" in changed:
        power['method'] = 'dbus'
        power['source'] = 'BATTERY' if bool(unwrap_dbus_value(changed['OnBattery'])) else 'AC'


def should_listen_for_resume(config):
    return any(
        config.getfloat(key, plane, fallback=0) != 0 for plane in VOLTAGE_PLANES for key in UNDERVOLT_KEYS + ICCMAX_KEYS
    )
```

- [ ] **Step 3: Add asyncio DBus setup**

Implement helpers that connect to UPower and login1 through `dbus-next` proxies and register callbacks using `on_properties_changed()` and `on_prepare_for_sleep()`.

- [ ] **Step 4: Replace `GLib.MainLoop()` startup**

Move the daemon event wait into `asyncio.run(run_dbus_loop(config))`; keep the existing worker thread creation and shutdown handling in `main()`.

- [ ] **Step 5: Update README and package metadata**

Replace references to `dbus`/`gobject` native bindings with `dbus-next`; remove distro packages that existed only to compile PyGObject/dbus-python.

### Task 3: Verify

**Files:**
- Test: `tests/test_dbus_helpers.py`

- [ ] **Step 1: Run callback tests**

Run: `python3 -m unittest tests/test_dbus_helpers.py`

Expected: all tests pass.

- [ ] **Step 2: Run syntax check**

Run: `python3 -c "compile(open('throttled.py', encoding='utf-8').read(), 'throttled.py', 'exec')"`

Expected: exit code 0.

- [ ] **Step 3: Inspect dependency references**

Run: `rg -n "PyGObject|pygobject|gobject|dbus-python|dbus.mainloop|gi.repository|python-gobject|python-dbus([^[:alnum:]_-]|$)|libgirepository|dbus-glib|cairo-gobject|python3-cairo|python3-gobject" README.md requirements.txt package/PKGBUILD install.sh throttled.py`

Expected: no output.
