#!/usr/bin/env python3
# vim: set ft=python:

import xml.etree.ElementTree as ET
import os
from enum import Enum
import time
import signal
import sys
from datetime import date as D

BOINC_XML = "/Library/Application Support/BOINC Data/global_prefs_override.xml"
BOINC_PASS = os.popen("cat '/Library/Application Support/BOINC Data/gui_rpc_auth.cfg'").read()
BOINC_CMD = "env boinccmd --host localhost --passwd '" + BOINC_PASS + "'"

def set_high():
    set_params(cpu=45, time_range=TimeRange(7, 30, 22, 30))


def set_low():
    set_params(cpu=10, time_range=TimeRange(3, 59, 4, 00))


def send_notification(message):
    os.system("terminal-notifier -title 'BOINC Auto discharger' -message '%s'" % (message))

class State(Enum):
    CHARGING = 1
    MUST_DISCHARGE = 2
    DISCHARGING = 3
    MUST_CHARGE = 4

    def next(self, percent, charging):
        state = None
        if (self is State.DISCHARGING and percent <= 20) or \
                (self is State.CHARGING and not charging):
            state = State.MUST_CHARGE
        elif (self is State.CHARGING and percent >= 90) or (self is State.DISCHARGING and charging):
            state = State.MUST_DISCHARGE
        elif (self is State.MUST_CHARGE and charging):
            state = State.CHARGING
        elif (self is State.MUST_DISCHARGE and not charging):
            state = State.DISCHARGING
        else:
            return self

        state.action()
        return state

    def action(self):
        if self is State.MUST_CHARGE:
            set_low()
            send_notification("Connect the charger! BOINC slowed down")
        elif self is State.MUST_DISCHARGE:
            send_notification("Disconnect the charger to speed up BOINC")
        elif self is State.DISCHARGING:
            set_high()
        else:
            set_low()

    def __str__(self):
        if self is State.DISCHARGING:
            return "Discharging, BOINC at full speed"
        if self is State.CHARGING:
            return "Charging, BOINC at slow speed"
        if self is State.MUST_CHARGE:
            return "Waiting for charger, BOINC at slow speed"
        return "Waiting for charger disconnect, BOINC at slow speed"


class TimeRange:
    def __init__(self, start_h, start_m, end_h, end_m):
        self.start_h = start_h
        self.start_m = start_m
        self.end_h = end_h
        self.end_m = end_m

    def valid(self):
        if self.start_h is None or self.end_h is None or self.start_m is None or self.end_m is None:
            return False
        if not (0 <= self.start_h < 24 and 0 < self.end_h <= 24 and 0 <= self.start_m < 60 \
                and 0 <= self.end_m < 60):
            return False
        return self.start_h * 60 + self.start_m < self.end_h * 60 + self.end_m

    def get_start(self):
        return self.start_h + self.start_m / 60

    def get_end(self):
        return self.end_h + self.end_m / 60


def set_param(tree, name, value):
    dom = tree.find(name)
    dom.text = "%2.6f" % (value)


def set_params(cpu=None, time_range=None, disable=True):
    tree = ET.parse(BOINC_XML)

    if cpu is not None and 0 < cpu <= 100:
        set_param(tree, 'cpu_usage_limit', cpu)

    if time_range is not None and time_range.valid():
        set_param(tree, 'start_hour', time_range.get_start())
        set_param(tree, 'end_hour', time_range.get_end())

    tree.write(BOINC_XML)
    os.system(BOINC_CMD + ' --read_global_prefs_override')

def charge_status():
    percent = int(os.popen("pmset -g batt | awk '/Internal/ {print $3}'").read()[:-3])
    charging = not os.system("pmset -g batt | grep discharging 2>&1 >/dev/null") == 0
    return (percent, charging)


def signal_handler(sig, frame):
    print('')
    sys.exit(0)


def main():
    signal.signal(signal.SIGINT, signal_handler)

    status = None
    percent, charging = charge_status()

    if charging:
        status = State.CHARGING
    else:
        status = State.DISCHARGING

    os.system('clear')
    status = status.next(percent, charging)
    status.action()

    while True:
        percent, charging = charge_status()
        count = int(os.popen("system_profiler SPPowerDataType | grep " +
                             "'Cycle Count' | awk '{print $3}'").read())
        cycles_remaining = 1000 - count
        care_days = (D(2021, 6, 30) - D.today()).days

        stats = None
        if len(sys.argv) > 2 and sys.argv[2] == "stats":
            stats = os.popen('istats').read()

        os.system('clear')
        print("Battery: %d%% - %s\nCharge count: %d" % (percent, status, count))
        print("Remaining: %d days, %d cycles\nRate needed: %.5f cycles a day" %
              (care_days, cycles_remaining, cycles_remaining / care_days))

        if stats is not None:
            print('\n' + stats[:stats.rfind('\n\n')])

        status = status.next(percent, charging)
        time.sleep(10 if len(sys.argv) <= 1 or sys.argv[1] is None else
                   int(sys.argv[1]))

if __name__ == "__main__":
    main()

#!/usr/bin/env python3
# vim: set ft=python:

import xml.etree.ElementTree as ET
import os
from enum import Enum
import time
import signal
import sys
from datetime import date as D

BOINC_XML = "/Library/Application Support/BOINC Data/global_prefs_override.xml"
BOINC_PASS = os.popen("cat '/Library/Application Support/BOINC Data/gui_rpc_auth.cfg'").read()
BOINC_CMD = "env boinccmd --host localhost --passwd '" + BOINC_PASS + "'"

def set_high():
    set_params(cpu=45, time_range=TimeRange(7, 30, 22, 30))


def set_low():
    set_params(cpu=10, time_range=TimeRange(3, 59, 4, 00))


def send_notification(message):
    os.system("terminal-notifier -title 'BOINC Auto discharger' -message '%s'" % (message))

class State(Enum):
    CHARGING = 1
    MUST_DISCHARGE = 2
    DISCHARGING = 3
    MUST_CHARGE = 4

    def next(self, percent, charging):
        state = None
        if (self is State.DISCHARGING and percent <= 20) or \
                (self is State.CHARGING and not charging):
            state = State.MUST_CHARGE
        elif (self is State.CHARGING and percent >= 90) or (self is State.DISCHARGING and charging):
            state = State.MUST_DISCHARGE
        elif (self is State.MUST_CHARGE and charging):
            state = State.CHARGING
        elif (self is State.MUST_DISCHARGE and not charging):
            state = State.DISCHARGING
        else:
            return self

        state.action()
        return state

    def action(self):
        if self is State.MUST_CHARGE:
            set_low()
            send_notification("Connect the charger! BOINC slowed down")
        elif self is State.MUST_DISCHARGE:
            send_notification("Disconnect the charger to speed up BOINC")
        elif self is State.DISCHARGING:
            set_high()
        else:
            set_low()

    def __str__(self):
        if self is State.DISCHARGING:
            return "Discharging, BOINC at full speed"
        if self is State.CHARGING:
            return "Charging, BOINC at slow speed"
        if self is State.MUST_CHARGE:
            return "Waiting for charger, BOINC at slow speed"
        return "Waiting for charger disconnect, BOINC at slow speed"


class TimeRange:
    def __init__(self, start_h, start_m, end_h, end_m):
        self.start_h = start_h
        self.start_m = start_m
        self.end_h = end_h
        self.end_m = end_m

    def valid(self):
        if self.start_h is None or self.end_h is None or self.start_m is None or self.end_m is None:
            return False
        if not (0 <= self.start_h < 24 and 0 < self.end_h <= 24 and 0 <= self.start_m < 60 \
                and 0 <= self.end_m < 60):
            return False
        return self.start_h * 60 + self.start_m < self.end_h * 60 + self.end_m

    def get_start(self):
        return self.start_h + self.start_m / 60

    def get_end(self):
        return self.end_h + self.end_m / 60


def set_param(tree, name, value):
    dom = tree.find(name)
    dom.text = "%2.6f" % (value)


def set_params(cpu=None, time_range=None, disable=True):
    tree = ET.parse(BOINC_XML)

    if cpu is not None and 0 < cpu <= 100:
        set_param(tree, 'cpu_usage_limit', cpu)

    if time_range is not None and time_range.valid():
        set_param(tree, 'start_hour', time_range.get_start())
        set_param(tree, 'end_hour', time_range.get_end())

    tree.write(BOINC_XML)
    os.system(BOINC_CMD + ' --read_global_prefs_override')

def charge_status():
    percent = int(os.popen("pmset -g batt | awk '/Internal/ {print $3}'").read()[:-3])
    charging = not os.system("pmset -g batt | grep discharging 2>&1 >/dev/null") == 0
    return (percent, charging)


def signal_handler(sig, frame):
    print('')
    sys.exit(0)


def main():
    signal.signal(signal.SIGINT, signal_handler)

    status = None
    percent, charging = charge_status()

    if charging:
        status = State.CHARGING
    else:
        status = State.DISCHARGING

    os.system('clear')
    status = status.next(percent, charging)
    status.action()

    while True:
        percent, charging = charge_status()
        count = int(os.popen("system_profiler SPPowerDataType | grep " +
                             "'Cycle Count' | awk '{print $3}'").read())
        cycles_remaining = 1000 - count
        care_days = (D(2021, 6, 30) - D.today()).days

        stats = None
        if len(sys.argv) > 2 and sys.argv[2] == "stats":
            stats = os.popen('istats').read()

        os.system('clear')
        print("Battery: %d%% - %s\nCharge count: %d" % (percent, status, count))
        print("Remaining: %d days, %d cycles\nRate needed: %.5f cycles a day" %
              (care_days, cycles_remaining, cycles_remaining / care_days))

        if stats is not None:
            print('\n' + stats[:stats.rfind('\n\n')])

        status = status.next(percent, charging)
        time.sleep(10 if len(sys.argv) <= 1 or sys.argv[1] is None else
                   int(sys.argv[1]))

if __name__ == "__main__":
    main()

#!/usr/bin/env python3 # vim: set ft=python: