Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions src/adjustor/core/charge_full_once.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import logging
import os
from typing import Callable

logger = logging.getLogger(__name__)


def detect_ac_online() -> bool | None:
base = "/sys/class/power_supply"
try:
for name in os.listdir(base):
type_path = os.path.join(base, name, "type")
online_path = os.path.join(base, name, "online")
try:
with open(type_path) as f:
if f.read().strip() != "Mains":
continue
with open(online_path) as f:
return f.read().strip() == "1"
except OSError:
continue
except Exception as e:
logger.error(f"charge_once: AC detection failed: {e}")
return None


class ChargeFullOncePolicy:
def __init__(self) -> None:
self.override_active: bool = False
self.ac_online: bool | None = None
self._last_applied: bool | None = None

def open(self) -> None:
self.override_active = False
self.ac_online = None
self._last_applied = None

def sync(self, enabled: bool) -> None:
self._last_applied = enabled

def _apply(self, enabled: bool, set_fn: Callable[[bool], None]) -> None:
if self._last_applied != enabled:
set_fn(enabled)
self._last_applied = enabled

def update(
self,
normal_limit_enabled: bool,
action_pressed: bool,
set_fn: Callable[[bool], None],
) -> None:
self.ac_online = detect_ac_online()

if not normal_limit_enabled:
if self.override_active:
logger.info(
"charge_once: Canceling override, charge limit disabled by user."
)
self.override_active = False
self._apply(False, set_fn)
return

if self.ac_online is not True:
if self.override_active:
logger.info(
"charge_once: AC disconnected or unknown, restoring charge limit."
)
self._apply(True, set_fn)
self.override_active = False
else:
self._apply(True, set_fn)
return

if action_pressed:
if self.override_active:
logger.info(
"charge_once: User canceled full charge, restoring charge limit."
)
self._apply(True, set_fn)
self.override_active = False
else:
logger.info(
"charge_once: Starting full-charge override, disabling charge limit."
)
self._apply(False, set_fn)
self.override_active = True
return

if not self.override_active:
self._apply(True, set_fn)
102 changes: 83 additions & 19 deletions src/adjustor/drivers/battery/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from hhd.plugins import Context, Event, HHDPlugin, load_relative_yaml
from hhd.plugins.conf import Config

from adjustor.core.charge_once import ChargeFullOncePolicy
from adjustor.core.alib import AlibParams, DeviceParams, alib
from adjustor.core.fan import fan_worker, get_fan_info
from adjustor.core.platform import get_platform_choices, set_platform_profile
Expand All @@ -16,6 +17,15 @@

APPLY_DELAY = 0.7

CHARGE_LIMITS = {
"p65": 65,
"p70": 70,
"p80": 80,
"p85": 85,
"p90": 90,
"p95": 95,
}


def set_charge_limit(bat: str, lim: int):
try:
Expand Down Expand Up @@ -99,8 +109,20 @@ def __init__(self, always_enable: bool = False) -> None:
self.charge_bypass_prev = None
self.charge_limit_prev = None

self.charge_policy = ChargeFullOncePolicy()
self.charge_once_action = None
self.emit = None

self.always_enable = always_enable

def _set_charge_once_action(self, action: str | None) -> None:
if self.charge_once_action == action:
return

self.charge_once_action = action
if self.emit is not None:
self.emit({"type": "settings"})

def settings(self):
if not self.enabled:
self.initialized = False
Expand All @@ -111,6 +133,16 @@ def settings(self):

if not self.charge_limit_fn:
del out["tdp"]["battery"]["children"]["charge_limit"]
del out["tdp"]["battery"]["children"]["charge_once"]
del out["tdp"]["battery"]["children"]["charge_once_restore"]
elif self.charge_once_action == "start":
del out["tdp"]["battery"]["children"]["charge_once_restore"]
elif self.charge_once_action == "restore":
del out["tdp"]["battery"]["children"]["charge_once"]
else:
del out["tdp"]["battery"]["children"]["charge_once"]
del out["tdp"]["battery"]["children"]["charge_once_restore"]

if not self.charge_bypass_fn:
del out["tdp"]["battery"]["children"]["charge_bypass"]
elif not self.bypass_awake:
Expand All @@ -125,6 +157,7 @@ def open(
):
self.emit = emit
self.startup = True
self.charge_policy.open()

for bat in os.listdir("/sys/class/power_supply"):
if not bat.startswith("BAT"):
Expand Down Expand Up @@ -189,34 +222,65 @@ def update(self, conf: Config):
# Charge limit
if self.charge_limit_fn:
lim = conf["tdp.battery.charge_limit"].to(str)
limit = CHARGE_LIMITS.get(lim)
charge_limit_enabled = limit is not None
start_pressed = conf.get_action("tdp.battery.charge_once")
restore_pressed = conf.get_action("tdp.battery.charge_once_restore")
action_pressed = start_pressed or restore_pressed

if lim != self.charge_limit_prev:
self.queue_charge_limit = curr + APPLY_DELAY
self.charge_limit_prev = lim

def apply_charge_limit(enabled: bool):
set_charge_limit(
self.charge_limit_fn,
limit if enabled and limit is not None else 100,
)

if self.startup and not charge_limit_enabled:
self.charge_policy.sync(False)

if self.startup or (
self.queue_charge_limit and self.queue_charge_limit < curr
):
self.queue_charge_limit = None
self.charge_limit_prev = lim

match lim:
case "p65":
set_charge_limit(self.charge_limit_fn, 65)
case "p70":
set_charge_limit(self.charge_limit_fn, 70)
case "p80":
set_charge_limit(self.charge_limit_fn, 80)
case "p85":
set_charge_limit(self.charge_limit_fn, 85)
case "p90":
set_charge_limit(self.charge_limit_fn, 90)
case "p95":
set_charge_limit(self.charge_limit_fn, 95)
case "disabled":
# Avoid writing charge limit on startup if
# disabled, so that if user does not use us
# we do not overwrite their setting.
if not self.startup:
set_charge_limit(self.charge_limit_fn, 100)
if charge_limit_enabled:
if not self.charge_policy.override_active:
apply_charge_limit(True)
self.charge_policy.sync(not self.charge_policy.override_active)
elif lim == "disabled":
# Avoid writing charge limit on startup if disabled, so
# that if user does not use us we do not overwrite their
# setting.
if not self.startup:
apply_charge_limit(False)
self.charge_policy.sync(False)
else:
logger.error(f"Invalid charge limit: {lim}")

charge_limit_pending = self.queue_charge_limit is not None
if (
not charge_limit_pending
or action_pressed
or self.charge_policy.override_active
):
self.charge_policy.update(
charge_limit_enabled,
action_pressed,
apply_charge_limit,
)

if self.charge_policy.override_active:
action = "restore"
elif charge_limit_enabled and self.charge_policy.ac_online is True:
action = "start"
else:
action = None
self._set_charge_once_action(action)
else:
self._set_charge_once_action(None)

self.startup = False
14 changes: 14 additions & 0 deletions src/adjustor/drivers/battery/battery.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,20 @@ children:
disabled: Unset
default: disabled

charge_once:
type: action
title: Charge to 100% Once
tags: [ non-essential ]
hint: >-
Temporarily disables the charge limit until the charger is unplugged.

charge_once_restore:
type: action
title: Restore Charge Limit
tags: [ non-essential ]
hint: >-
Re-enables the charge limit now, before unplugging.

charge_bypass:
type: multiple
title: Charge Bypass
Expand Down