Quellcode durchsuchen

Apple Battery Saver: Abschaltung vor Tiefentladung

Schaltet den Mac kontrolliert ab, bevor der Akku in die Tiefentladung
gerät. Ausgelöst wird auf Ladestand ODER minimaler Zellspannung - je
nachdem, was zuerst eintritt.

Die Zellspannung ist dabei der eigentliche Schutz: Der Ladestand in
Prozent ist nur eine Schätzung aus dem Kapazitätsmodell des Akkus.
Driftet dessen Kalibrierung, wird die Prozentanzeige unbrauchbar -
genau dieser Fall hat den Vorgängerakku bei 58 Zyklen zerstört, als
die Zellen bis auf 2,71 V entladen wurden und die Schutzschaltung
dauerhaft verriegelte.

Enthalten:
- battery-saver.sh mit --status und --test
- LaunchDaemon (60 s Intervall, RunAtLoad)
- Konfigurationsdatei mit Schwellwerten
- install.sh / uninstall.sh

Sicherheitsmechanismen: greift nur im Akkubetrieb, löst erst nach drei
Messungen in Folge aus, prüft Messwerte und Konfiguration auf
Plausibilität und hält bei unklarer Datenlage still.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Marius vor 1 Woche
Ursprung
Commit
d52cfa4155
7 geänderte Dateien mit 601 neuen und 1 gelöschten Zeilen
  1. 3 0
      .gitignore
  2. 113 1
      README.md
  3. 40 0
      battery-saver.conf
  4. 293 0
      battery-saver.sh
  5. 35 0
      de.smallmountains.batterysaver.plist
  6. 73 0
      install.sh
  7. 44 0
      uninstall.sh

+ 3 - 0
.gitignore

@@ -0,0 +1,3 @@
+.DS_Store
+*.log
+*.state

+ 113 - 1
README.md

@@ -1,2 +1,114 @@
-# Apple-Battery-Saver
+# Apple Battery Saver
 
+Schaltet einen Mac kontrolliert ab, bevor der Akku in die Tiefentladung gerät.
+
+Ausgelöst wird auf **Ladestand oder minimaler Zellspannung** — je nachdem, was zuerst eintritt. Läuft still als LaunchDaemon im Hintergrund, ohne Menüleisten-Icon und ohne Abhängigkeiten.
+
+## Warum nicht einfach die Prozentanzeige?
+
+Weil genau die das Problem verursacht.
+
+Der Ladestand in Prozent ist kein Messwert, sondern eine Schätzung aus dem Kapazitätsmodell des Akkus. Dieses Modell muss der Akku regelmäßig nachkalibrieren. Tut er das nicht, driftet die Anzeige — und macOS trifft seine Abschaltentscheidung auf Basis falscher Zahlen.
+
+Der Anlass für dieses Projekt war genau dieser Fall: Ein Akku meldete ein seit 53 Zyklen nicht mehr nachkalibriertes Kapazitätsmodell. macOS wurden zu hohe Restkapazitäten gemeldet, das Gerät lief über den sicheren Abschaltpunkt hinaus weiter, und die Zellen wurden bis auf **2,71 V** entladen. Üblich ist eine Abschaltung bei 3,0–3,2 V. Die Schutzschaltung des Akkus verriegelte daraufhin dauerhaft — Totalausfall bei 58 Ladezyklen.
+
+Die Zellspannung ist dagegen ein direkter physikalischer Messwert, unabhängig von jeder Kalibrierung. Sie ist deshalb hier der eigentliche Schutz; die Prozentgrenze ist nur der bequeme Auslöser für den Normalfall.
+
+## Schwellwerte
+
+| Stufe | Bedingung | Aktion |
+|---|---|---|
+| Warnung | ≤ 20 % **oder** ≤ 3550 mV | Mitteilung |
+| Abschaltung | ≤ 10 % **oder** ≤ 3400 mV | Sauberes Herunterfahren |
+
+Ausgewertet wird immer die **niedrigste Einzelzelle**, nicht der Durchschnitt.
+
+Zur Orientierung:
+
+```
+4300 mV   voll
+3550 mV   ca. 25-30 %
+3400 mV   ca. 10-15 %   <- Abschaltung
+3000 mV   macOS schaltet üblicherweise selbst ab
+2700 mV   Schutzschaltung verriegelt dauerhaft (Totalausfall)
+```
+
+## Installation
+
+```bash
+git clone https://git.smallmountains.de/Marius/Apple-Battery-Saver.git
+cd Apple-Battery-Saver
+sudo ./install.sh
+```
+
+Installiert wird nach:
+
+| Datei | Ziel |
+|---|---|
+| Programm | `/usr/local/bin/battery-saver.sh` |
+| Konfiguration | `/usr/local/etc/battery-saver.conf` |
+| LaunchDaemon | `/Library/LaunchDaemons/de.smallmountains.batterysaver.plist` |
+| Protokoll | `/var/log/battery-saver.log` |
+
+## Verwendung
+
+```bash
+battery-saver.sh --status      # Messwerte und Bewertung anzeigen
+sudo battery-saver.sh --test   # Volle Logik simulieren, ohne abzuschalten
+tail -f /var/log/battery-saver.log
+```
+
+Beispielausgabe von `--status`:
+
+```
+Netzteil angeschlossen : Yes
+Ladestand              : 98 %
+Packspannung           : 12897 mV
+Zellspannungen         : 4301,4283,4311
+Niedrigste Zelle       : 4283 mV
+
+Schwellwerte
+  Warnung    : <= 20 %  oder  <= 3550 mV
+  Abschaltung: <= 10 %  oder  <= 3400 mV
+  Auslösung nach 3 Messungen in Folge
+
+Bewertung: am Netz - Überwachung pausiert
+```
+
+## Konfiguration
+
+Schwellwerte in `/usr/local/etc/battery-saver.conf` anpassen, danach neu laden:
+
+```bash
+sudo launchctl kickstart -k system/de.smallmountains.batterysaver
+```
+
+`DRY_RUN=1` unterdrückt das Herunterfahren dauerhaft — es wird dann nur protokolliert und gewarnt. Sinnvoll, um das Verhalten erst einmal im Echtbetrieb zu beobachten.
+
+## Sicherheitsmechanismen
+
+Ein Programm, das den Rechner abschalten darf, muss bei unklarer Datenlage stillhalten. Eingebaut sind deshalb:
+
+- **Nur im Akkubetrieb.** Hängt das Gerät am Netz, passiert nichts; der Zähler wird zurückgesetzt.
+- **Mehrfachmessung.** Erst nach drei aufeinanderfolgenden Messungen unter der Grenze wird abgeschaltet (bei 60 s Intervall also rund drei Minuten). Ein einzelner Ausreißer löst nichts aus.
+- **Plausibilitätsprüfung.** Unplausible Werte führen zu „keine Aktion" statt zu einer Abschaltung. Fehlt die Zellspannung, bleibt nur die Prozentgrenze aktiv.
+- **Vorwarnung.** Ab 20 % gibt es eine Mitteilung, damit noch selbst reagiert werden kann.
+- **Konfigurationsprüfung.** Unzulässige Schwellwerte (Zahlendreher, Buchstaben, vertauschte Warn-/Abschaltgrenzen) legen den Dienst still, statt ihn auslösen zu lassen. Ein `HALT_MV=44000` statt `4400` würde sonst im Akkubetrieb sofort abschalten.
+- **Sauberes Herunterfahren** über System Events, damit Programme ihre Dokumente sichern können. Nur wenn keine GUI-Session existiert, wird hart abgeschaltet.
+
+## Grenzen
+
+**Der Dienst schützt nur im wachen Zustand.** Liegt das Gerät zugeklappt und entlädt sich im Standby, läuft kein Prozess, der eingreifen könnte. Für längere Lagerung hilft nur echtes Hibernate oder Ausschalten.
+
+**Automatisches Herunterfahren kann ungespeicherte Arbeit kosten.** Die Warnstufe bei 20 % ist die Gelegenheit, selbst zu reagieren.
+
+## Deinstallation
+
+```bash
+sudo ./uninstall.sh           # Konfiguration und Protokoll bleiben
+sudo ./uninstall.sh --purge   # entfernt alles
+```
+
+## Lizenz
+
+MIT

+ 40 - 0
battery-saver.conf

@@ -0,0 +1,40 @@
+# Apple Battery Saver - Konfiguration
+#
+# Installiert nach: /usr/local/etc/battery-saver.conf
+# Nach Änderungen neu laden:
+#   sudo launchctl kickstart -k system/de.smallmountains.batterysaver
+
+# --- Ladestand (Prozent) ---------------------------------------------------
+# Bequemer Auslöser für den Normalfall. Achtung: Die Prozentanzeige ist eine
+# Schätzung des Akku-Kapazitätsmodells und kann bei Kalibrierungsdrift stark
+# danebenliegen. Sie ist deshalb NICHT der eigentliche Schutz.
+
+WARN_PCT=20
+HALT_PCT=10
+
+# --- Zellspannung (Millivolt) ----------------------------------------------
+# Der eigentliche Schutz. Direkt gemessen, unabhängig von jeder Kalibrierung.
+# Ausgewertet wird immer die NIEDRIGSTE Einzelzelle.
+#
+# Orientierung:
+#   4300 mV  voll
+#   3550 mV  ca. 25-30 %
+#   3400 mV  ca. 10-15 %   <- Abschaltung
+#   3000 mV  macOS schaltet üblicherweise selbst ab
+#   2700 mV  Schutzschaltung des Akkus verriegelt dauerhaft (Totalausfall)
+
+WARN_MV=3550
+HALT_MV=3400
+
+# --- Verhalten -------------------------------------------------------------
+# So viele Messungen in Folge müssen unter der Abschaltgrenze liegen, bevor
+# tatsächlich heruntergefahren wird. Bei 60 s Intervall bedeutet 3 also rund
+# drei Minuten. Schützt vor Fehlauslösung durch einen einzelnen Ausreißer.
+CONSECUTIVE=3
+
+# Mindestabstand zwischen zwei Warnmeldungen in Sekunden.
+WARN_COOLDOWN=600
+
+# Auf 1 setzen, um das Herunterfahren dauerhaft zu unterdrücken. Es wird dann
+# nur protokolliert und gewarnt. Nützlich zum Beobachten im Echtbetrieb.
+DRY_RUN=0

+ 293 - 0
battery-saver.sh

@@ -0,0 +1,293 @@
+#!/bin/bash
+#
+# Apple Battery Saver
+#
+# Schaltet den Mac kontrolliert ab, bevor der Akku in die Tiefentladung
+# gerät. Löst auf Ladestand ODER minimaler Zellspannung aus - je nachdem,
+# was zuerst eintritt.
+#
+# Die Zellspannung ist der eigentliche Schutz: Der Ladestand in Prozent ist
+# nur eine Schätzung aus dem Kapazitätsmodell des Akkus. Driftet dessen
+# Kalibrierung, wird die Prozentanzeige unbrauchbar - genau das zerstört
+# Akkus. Die Zellspannung ist dagegen ein direkter Messwert.
+#
+# Verwendung:
+#   battery-saver.sh            Normaler Lauf (durch launchd)
+#   battery-saver.sh --status   Aktuelle Messwerte + Bewertung anzeigen
+#   battery-saver.sh --test     Volle Logik simulieren, ohne abzuschalten
+#
+# https://git.smallmountains.de/Marius/Apple-Battery-Saver
+
+set -uo pipefail
+
+VERSION="1.0.0"
+
+# ---------------------------------------------------------------- Defaults
+# Werden von der Konfigurationsdatei überschrieben, falls vorhanden.
+
+WARN_PCT=20         # Ladestand in %, ab dem gewarnt wird
+HALT_PCT=10         # Ladestand in %, ab dem abgeschaltet wird
+WARN_MV=3550        # Minimale Zellspannung in mV, ab der gewarnt wird
+HALT_MV=3400        # Minimale Zellspannung in mV, ab der abgeschaltet wird
+CONSECUTIVE=3       # So viele Messungen in Folge müssen auslösen
+WARN_COOLDOWN=600   # Mindestabstand zwischen zwei Warnungen (Sekunden)
+DRY_RUN=0           # 1 = niemals wirklich abschalten
+
+LOG=/var/log/battery-saver.log
+STATE=/var/run/battery-saver.state
+CONFIG="${BATTERY_SAVER_CONFIG:-/usr/local/etc/battery-saver.conf}"
+
+[ -r "$CONFIG" ] && . "$CONFIG"
+
+# --------------------------------------------------- Konfiguration prüfen
+# Ein Dienst, der als root den Rechner abschalten darf, muss sich gegen
+# Tippfehler in der Konfigurationsdatei wehren. Ein versehentliches
+# HALT_MV=44000 würde sonst im Akkubetrieb sofort auslösen.
+
+config_error=""
+check_range() {
+    local name="$1" val="$2" min="$3" max="$4"
+    if ! [[ "$val" =~ ^[0-9]+$ ]] || [ "$val" -lt "$min" ] || [ "$val" -gt "$max" ]; then
+        config_error="${config_error}  ${name}='${val}' ist unzulässig (erlaubt: ${min}-${max})\n"
+    fi
+}
+
+check_range WARN_PCT      "$WARN_PCT"      0 100
+check_range HALT_PCT      "$HALT_PCT"      0 100
+check_range WARN_MV       "$WARN_MV"       0 4400
+check_range HALT_MV       "$HALT_MV"       0 4400
+check_range CONSECUTIVE   "$CONSECUTIVE"   1 100
+check_range WARN_COOLDOWN "$WARN_COOLDOWN" 0 86400
+
+if [ "$HALT_PCT" -gt "$WARN_PCT" ] 2>/dev/null; then
+    config_error="${config_error}  HALT_PCT (${HALT_PCT}) darf nicht über WARN_PCT (${WARN_PCT}) liegen\n"
+fi
+if [ "$HALT_MV" -gt "$WARN_MV" ] 2>/dev/null; then
+    config_error="${config_error}  HALT_MV (${HALT_MV}) darf nicht über WARN_MV (${WARN_MV}) liegen\n"
+fi
+
+MODE="daemon"
+case "${1:-}" in
+    --status)  MODE="status" ;;
+    --test)    MODE="test"; DRY_RUN=1 ;;
+    --version) echo "Apple Battery Saver $VERSION"; exit 0 ;;
+    "")        ;;
+    *)         echo "Unbekannte Option: $1" >&2; exit 2 ;;
+esac
+
+# ---------------------------------------------------------------- Helpers
+
+log() {
+    local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $*"
+    if [ "$MODE" = "daemon" ]; then
+        echo "$msg" >> "$LOG" 2>/dev/null
+    else
+        echo "$msg"
+    fi
+}
+
+# Ermittelt den aktuell angemeldeten Benutzer, um aus dem root-Kontext
+# des LaunchDaemons heraus in dessen GUI-Session hineinzureichen.
+console_user() { stat -f "%Su" /dev/console 2>/dev/null; }
+
+notify() {
+    local title="$1" message="$2"
+    local user uid
+    user="$(console_user)"
+    if [ -z "$user" ] || [ "$user" = "root" ]; then
+        return 0
+    fi
+    uid="$(id -u "$user" 2>/dev/null)" || return 0
+    launchctl asuser "$uid" sudo -u "$user" \
+        osascript -e "display notification \"${message}\" with title \"${title}\" sound name \"Submarine\"" \
+        >/dev/null 2>&1
+}
+
+# Sauberes Herunterfahren: erst über System Events, damit Programme ihre
+# Dokumente sichern können; nur falls keine GUI-Session existiert, hart.
+do_shutdown() {
+    local user uid
+    user="$(console_user)"
+    if [ -n "$user" ] && [ "$user" != "root" ]; then
+        uid="$(id -u "$user" 2>/dev/null)"
+        if [ -n "$uid" ]; then
+            log "Fahre herunter (grafisch, über System Events)"
+            launchctl asuser "$uid" sudo -u "$user" \
+                osascript -e 'tell application "System Events" to shut down' \
+                >/dev/null 2>&1 && return 0
+        fi
+    fi
+    log "Fahre herunter (shutdown -h now)"
+    /sbin/shutdown -h now >/dev/null 2>&1
+}
+
+# State-Datei: key=value, überlebt zwischen den launchd-Läufen
+state_get() {
+    local key="$1"
+    [ -r "$STATE" ] || { echo 0; return; }
+    local v
+    v="$(grep -m1 "^${key}=" "$STATE" 2>/dev/null | cut -d= -f2)"
+    [[ "$v" =~ ^[0-9]+$ ]] && echo "$v" || echo 0
+}
+
+state_set() {
+    local key="$1" val="$2" tmp
+    tmp="$(mktemp "${STATE}.XXXXXX" 2>/dev/null)" || return 0
+    grep -v "^${key}=" "$STATE" 2>/dev/null > "$tmp"
+    echo "${key}=${val}" >> "$tmp"
+    mv -f "$tmp" "$STATE" 2>/dev/null
+}
+
+# Fehlerhafte Konfiguration: warnen und nichts tun. Niemals abschalten.
+if [ -n "$config_error" ]; then
+    log "FEHLER: Konfiguration ungültig (${CONFIG}) - Dienst greift nicht ein"
+    printf "%b" "$config_error" | while IFS= read -r l; do [ -n "$l" ] && log "$l"; done
+    if [ "$MODE" != "daemon" ]; then
+        echo "FEHLER: Konfiguration ungültig (${CONFIG}):" >&2
+        printf "%b" "$config_error" >&2
+        echo "Es wird nicht abgeschaltet, solange dies nicht behoben ist." >&2
+    fi
+    exit 0
+fi
+
+# ---------------------------------------------------------------- Messung
+
+IOREG="$(ioreg -rn AppleSmartBattery 2>/dev/null | sed 's/^ *//')"
+
+if [ -z "$IOREG" ]; then
+    log "FEHLER: Akku nicht auslesbar (ioreg leer) - keine Aktion"
+    exit 0
+fi
+
+INSTALLED="$(echo "$IOREG" | grep -m1 '"BatteryInstalled"' | awk -F'= ' '{print $2}')"
+EXTERNAL="$(echo  "$IOREG" | grep -m1 '"ExternalConnected"' | awk -F'= ' '{print $2}')"
+PCT="$(echo       "$IOREG" | grep -m1 '"CurrentCapacity"'   | awk -F'= ' '{print $2}')"
+PACK_MV="$(echo   "$IOREG" | grep -m1 '^"Voltage"'          | awk -F'= ' '{print $2}')"
+
+CELLS="$(echo "$IOREG" | grep -o '"CellVoltage"=([^)]*)' | head -1 | sed 's/.*=(//;s/)//')"
+MIN_MV=""
+if [ -n "$CELLS" ]; then
+    MIN_MV="$(echo "$CELLS" | tr ',' '\n' | grep -E '^[0-9]+$' | sort -n | head -1)"
+fi
+
+# Jede Unsicherheit führt zu "nichts tun". Ein Programm, das den Rechner
+# abschalten darf, muss bei unklarer Datenlage stillhalten.
+if [ "$INSTALLED" != "Yes" ]; then
+    log "Kein Akku installiert - keine Aktion"
+    exit 0
+fi
+
+if ! [[ "$PCT" =~ ^[0-9]+$ ]] || [ "$PCT" -gt 100 ]; then
+    log "FEHLER: Ladestand unplausibel ('${PCT}') - keine Aktion"
+    exit 0
+fi
+
+if [ -n "$MIN_MV" ] && { ! [[ "$MIN_MV" =~ ^[0-9]+$ ]] || [ "$MIN_MV" -lt 1000 ] || [ "$MIN_MV" -gt 5000 ]; }; then
+    log "WARNUNG: Zellspannung unplausibel ('${MIN_MV}') - nur Prozentgrenze aktiv"
+    MIN_MV=""
+fi
+
+# ---------------------------------------------------------------- Bewertung
+
+HALT_REASON=""
+WARN_REASON=""
+
+if [ "$PCT" -le "$HALT_PCT" ]; then
+    HALT_REASON="Ladestand ${PCT} % <= ${HALT_PCT} %"
+elif [ -n "$MIN_MV" ] && [ "$MIN_MV" -le "$HALT_MV" ]; then
+    HALT_REASON="Zellspannung ${MIN_MV} mV <= ${HALT_MV} mV"
+elif [ "$PCT" -le "$WARN_PCT" ]; then
+    WARN_REASON="Ladestand ${PCT} % <= ${WARN_PCT} %"
+elif [ -n "$MIN_MV" ] && [ "$MIN_MV" -le "$WARN_MV" ]; then
+    WARN_REASON="Zellspannung ${MIN_MV} mV <= ${WARN_MV} mV"
+fi
+
+# ---------------------------------------------------------------- Status
+
+if [ "$MODE" = "status" ]; then
+    echo "Apple Battery Saver $VERSION - Status"
+    echo "----------------------------------------"
+    echo "Netzteil angeschlossen : ${EXTERNAL}"
+    echo "Ladestand              : ${PCT} %"
+    echo "Packspannung           : ${PACK_MV} mV"
+    echo "Zellspannungen         : ${CELLS:-nicht verfügbar}"
+    echo "Niedrigste Zelle       : ${MIN_MV:-nicht verfügbar} mV"
+    echo
+    echo "Schwellwerte"
+    echo "  Warnung    : <= ${WARN_PCT} %  oder  <= ${WARN_MV} mV"
+    echo "  Abschaltung: <= ${HALT_PCT} %  oder  <= ${HALT_MV} mV"
+    echo "  Auslösung nach ${CONSECUTIVE} Messungen in Folge"
+    echo
+    if [ -n "$HALT_REASON" ]; then
+        echo "Bewertung: ABSCHALTGRENZE erreicht (${HALT_REASON})"
+    elif [ -n "$WARN_REASON" ]; then
+        echo "Bewertung: Warngrenze erreicht (${WARN_REASON})"
+    else
+        echo "Bewertung: unkritisch"
+    fi
+    if [ "$EXTERNAL" = "Yes" ]; then
+        echo "Zustand  : am Netz - Überwachung pausiert, es wird nicht abgeschaltet"
+    else
+        echo "Zustand  : Akkubetrieb - Überwachung aktiv"
+    fi
+    echo
+    echo "Zähler (Messungen in Folge): $(state_get halt_count)"
+    echo "Logdatei: ${LOG}"
+    exit 0
+fi
+
+# ---------------------------------------------------------------- Logik
+
+# Am Netz: nichts tun, Zähler zurücksetzen.
+if [ "$EXTERNAL" = "Yes" ]; then
+    if [ "$(state_get halt_count)" -ne 0 ]; then
+        log "Netzteil angeschlossen - Zähler zurückgesetzt"
+        state_set halt_count 0
+    fi
+    [ "$MODE" = "test" ] && echo "Am Netz - keine Aktion."
+    exit 0
+fi
+
+if [ -n "$HALT_REASON" ]; then
+    COUNT="$(state_get halt_count)"
+    COUNT=$((COUNT + 1))
+    state_set halt_count "$COUNT"
+    log "Abschaltgrenze erreicht (${HALT_REASON}) - Messung ${COUNT}/${CONSECUTIVE}"
+
+    if [ "$COUNT" -ge "$CONSECUTIVE" ]; then
+        if [ "$DRY_RUN" = "1" ]; then
+            log "TESTMODUS: würde jetzt herunterfahren (${HALT_REASON})"
+            echo "TESTMODUS: würde jetzt herunterfahren - ${HALT_REASON}"
+        else
+            log "Schalte ab: ${HALT_REASON}"
+            notify "Akku kritisch" "Der Mac wird jetzt heruntergefahren (${HALT_REASON})."
+            sleep 5
+            state_set halt_count 0
+            do_shutdown
+        fi
+    else
+        notify "Akku kritisch" "Abschaltung in Kürze - bitte Arbeit sichern (${PCT} %)."
+    fi
+    exit 0
+fi
+
+# Unter Warngrenze, aber über Abschaltgrenze
+if [ -n "$WARN_REASON" ]; then
+    state_set halt_count 0
+    NOW="$(date +%s)"
+    LAST="$(state_get last_warn)"
+    if [ $((NOW - LAST)) -ge "$WARN_COOLDOWN" ]; then
+        state_set last_warn "$NOW"
+        log "Warnung: ${WARN_REASON}"
+        notify "Akku wird knapp" "Noch ${PCT} % - bitte anschließen. Abschaltung bei ${HALT_PCT} %."
+    fi
+    [ "$MODE" = "test" ] && echo "Warngrenze erreicht - ${WARN_REASON}"
+    exit 0
+fi
+
+# Alles unkritisch
+if [ "$(state_get halt_count)" -ne 0 ]; then
+    state_set halt_count 0
+fi
+[ "$MODE" = "test" ] && echo "Unkritisch: ${PCT} %, niedrigste Zelle ${MIN_MV:-n/a} mV"
+exit 0

+ 35 - 0
de.smallmountains.batterysaver.plist

@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+    <key>Label</key>
+    <string>de.smallmountains.batterysaver</string>
+
+    <key>ProgramArguments</key>
+    <array>
+        <string>/usr/local/bin/battery-saver.sh</string>
+    </array>
+
+    <!-- Alle 60 Sekunden prüfen -->
+    <key>StartInterval</key>
+    <integer>60</integer>
+
+    <!-- Beim Systemstart einmal sofort laufen -->
+    <key>RunAtLoad</key>
+    <true/>
+
+    <key>StandardOutPath</key>
+    <string>/var/log/battery-saver.out.log</string>
+    <key>StandardErrorPath</key>
+    <string>/var/log/battery-saver.err.log</string>
+
+    <key>ProcessType</key>
+    <string>Background</string>
+
+    <!-- Niedrige Priorität: darf den Rechner nicht ausbremsen -->
+    <key>LowPriorityIO</key>
+    <true/>
+    <key>Nice</key>
+    <integer>10</integer>
+</dict>
+</plist>

+ 73 - 0
install.sh

@@ -0,0 +1,73 @@
+#!/bin/bash
+#
+# Apple Battery Saver - Installation
+# Aufruf:  sudo ./install.sh
+#
+set -euo pipefail
+
+LABEL="de.smallmountains.batterysaver"
+SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+
+BIN_DST=/usr/local/bin/battery-saver.sh
+CONF_DST=/usr/local/etc/battery-saver.conf
+PLIST_DST="/Library/LaunchDaemons/${LABEL}.plist"
+
+if [ "$(id -u)" -ne 0 ]; then
+    echo "Dieses Skript muss als root laufen:  sudo ./install.sh" >&2
+    exit 1
+fi
+
+echo "Apple Battery Saver - Installation"
+echo "=================================="
+
+# --- Programm ---------------------------------------------------------------
+install -d -m 755 /usr/local/bin
+install -m 755 -o root -g wheel "${SRC}/battery-saver.sh" "$BIN_DST"
+echo "  Programm      -> ${BIN_DST}"
+
+# --- Konfiguration ----------------------------------------------------------
+install -d -m 755 /usr/local/etc
+if [ -f "$CONF_DST" ]; then
+    echo "  Konfiguration -> ${CONF_DST} (vorhanden, bleibt unverändert)"
+else
+    install -m 644 -o root -g wheel "${SRC}/battery-saver.conf" "$CONF_DST"
+    echo "  Konfiguration -> ${CONF_DST}"
+fi
+
+# --- Logdatei ---------------------------------------------------------------
+touch /var/log/battery-saver.log
+chmod 644 /var/log/battery-saver.log
+chown root:wheel /var/log/battery-saver.log
+
+# --- LaunchDaemon -----------------------------------------------------------
+# Falls bereits geladen, erst sauber entladen.
+if launchctl print "system/${LABEL}" >/dev/null 2>&1; then
+    echo "  Vorhandenen Dienst entladen ..."
+    launchctl bootout "system/${LABEL}" 2>/dev/null || true
+fi
+
+install -m 644 -o root -g wheel "${SRC}/${LABEL}.plist" "$PLIST_DST"
+echo "  LaunchDaemon  -> ${PLIST_DST}"
+
+if ! launchctl bootstrap system "$PLIST_DST" 2>/dev/null; then
+    # Fallback für ältere macOS-Versionen
+    launchctl load -w "$PLIST_DST"
+fi
+
+# --- Prüfung ----------------------------------------------------------------
+echo
+if launchctl print "system/${LABEL}" >/dev/null 2>&1; then
+    echo "Dienst läuft."
+else
+    echo "WARNUNG: Dienst konnte nicht gestartet werden." >&2
+    exit 1
+fi
+
+echo
+echo "Fertig. Nützliche Befehle:"
+echo "  battery-saver.sh --status                  Messwerte und Bewertung"
+echo "  sudo battery-saver.sh --test               Logik simulieren"
+echo "  tail -f /var/log/battery-saver.log         Protokoll mitlesen"
+echo "  sudo ./uninstall.sh                        Deinstallieren"
+echo
+"$BIN_DST" --status || true

+ 44 - 0
uninstall.sh

@@ -0,0 +1,44 @@
+#!/bin/bash
+#
+# Apple Battery Saver - Deinstallation
+# Aufruf:  sudo ./uninstall.sh          (Konfiguration und Protokoll bleiben)
+#          sudo ./uninstall.sh --purge  (entfernt auch Konfiguration und Protokoll)
+#
+set -uo pipefail
+
+LABEL="de.smallmountains.batterysaver"
+PLIST="/Library/LaunchDaemons/${LABEL}.plist"
+PURGE=0
+[ "${1:-}" = "--purge" ] && PURGE=1
+
+if [ "$(id -u)" -ne 0 ]; then
+    echo "Dieses Skript muss als root laufen:  sudo ./uninstall.sh" >&2
+    exit 1
+fi
+
+echo "Apple Battery Saver - Deinstallation"
+echo "===================================="
+
+if launchctl print "system/${LABEL}" >/dev/null 2>&1; then
+    launchctl bootout "system/${LABEL}" 2>/dev/null || launchctl unload -w "$PLIST" 2>/dev/null
+    echo "  Dienst entladen"
+fi
+
+[ -f "$PLIST" ] && rm -f "$PLIST" && echo "  ${PLIST} entfernt"
+[ -f /usr/local/bin/battery-saver.sh ] && rm -f /usr/local/bin/battery-saver.sh && echo "  /usr/local/bin/battery-saver.sh entfernt"
+rm -f /var/run/battery-saver.state
+
+if [ "$PURGE" -eq 1 ]; then
+    rm -f /usr/local/etc/battery-saver.conf && echo "  Konfiguration entfernt"
+    rm -f /var/log/battery-saver.log /var/log/battery-saver.out.log /var/log/battery-saver.err.log
+    echo "  Protokolle entfernt"
+else
+    echo
+    echo "Konfiguration und Protokoll wurden behalten:"
+    echo "  /usr/local/etc/battery-saver.conf"
+    echo "  /var/log/battery-saver.log"
+    echo "Mit --purge werden auch diese entfernt."
+fi
+
+echo
+echo "Fertig."