#!/usr/bin/env python
#
# generate some heat!
#
# Wrap your RasPi in a closed box.  Get a usbrelay1 to control
# an incadescent light bulb in the box.  Heat to 45C.  profit.
#
# This code depends on the program 'usbrelay' to manage your usbrelay
# conencted device. Get it here:  git@github.com:darrylb123/usbrelay.git
#
# makeheatusb will use a lot less CPU than ntpheat, and more directly
# heats the XTAL rather than the CPU.
#
# Avoid the desire to increase the wait time.  The realy clocks twice
# per cycle, and those cycles add up.  Minimize wear on your relay.
#
# Try the simple P controller (the defaul) before tyring the PID controller.
# the PID controller may take some fiddling with the constants to get
# it working better than the simple P controller.

import argparse
import subprocess
import sys
import time

try:
    import ntp.util
except ImportError as e:
    sys.stderr.write("makeheatusb: can't find Python NTP modules "
                     "-- check PYTHONPATH.\n%s\n" % e)
    sys.exit(1)


def run_binary(cmd):
    """\
Run a binary
Return its output if good, None if bad
"""

    try:
        # sadly subprocess.check_output() is not in Python 2.6
        # so use Popen()
        # this throws an exception if not found
        proc = subprocess.Popen(cmd,
                                stdout=subprocess.PIPE,
                                stderr=subprocess.STDOUT,
                                universal_newlines=True)
        output = proc.communicate()[0].split("\n")

        if proc.returncode:
            # non-zero return code, fail
            print("Return %s" % proc.returncode)
            return None

    except ImportError as e:
        sys.stderr.write("Unable to run %s binary\n" % cmd[0])
        print(cmd)
        sys.stderr.write("%s\n" % e)
        return None

    return output


class PID:
    """\
Discrete PID control
"""

    def __init__(self, setpoint=0.0,
                 P=2.0, I=0.0, D=1.0,
                 Derivator=0, Integrator=0,
                 Integrator_max=100, Integrator_min=-100):

        self.Kp = P
        self.Ki = I
        self.Kd = D
        self.Derivator = Derivator
        self.Integrator = Integrator
        self.Integrator_max = Integrator_max
        self.Integrator_min = Integrator_min

        self.set_point = setpoint
        self.error = 0.0

    def __repr__(self):
        return ("D_value=%s, I_value=%s" % (self.D_value, self.I_value))

    def update(self, current_value):
        """
        Calculate PID output value for given reference input and feedback
        """

        self.error = self.set_point - current_value

        self.P_value = self.Kp * self.error
        self.D_value = self.Kd * (self.error - self.Derivator)
        self.Derivator = self.error

        self.Integrator = self.Integrator + self.error

        if self.Integrator > self.Integrator_max:
                self.Integrator = self.Integrator_max
        elif self.Integrator < self.Integrator_min:
                self.Integrator = self.Integrator_min

        self.I_value = self.Integrator * self.Ki

        PID = self.P_value + self.I_value + self.D_value

        return PID

# Work with argvars
parser = argparse.ArgumentParser(description="make heat with USB relay")
parser.add_argument('-p', '--pid',
                    action="store_true",
                    dest='pid',
                    help="Use PID controller instead of simple P controller.")
parser.add_argument('-s', '--step',
                    action="store_true",
                    dest='step',
                    help="Step up 1C every hour for 10 hours, then back down.")
parser.add_argument('-t', '--temp',
                    default=[45.0],
                    dest='target_temp',
                    help="Temperature to hold in C.  Default is 45.0C",
                    nargs=1,
                    type=float)
parser.add_argument('-w', '--wait',
                    default=[60],
                    dest='wait',
                    help="Set delay time in seconds, default is 60",
                    nargs=1,
                    type=float)
parser.add_argument('-v', '--verbose',
                    action="store_true",
                    dest='verbose',
                    help="be verbose")
parser.add_argument('-V', '--version',
                    action="version",
                    version="makeheatusb %s" % ntp.util.stdversion())
args = parser.parse_args()

zone0 = '/sys/class/thermal/thermal_zone0/temp'
cnt = 0

temp_gate = args.target_temp[0]
start_temp_gate = temp_gate
period = float(args.wait[0])

# you will need to personalize these to your relay ID:
usbrelay_on = ['usbrelay', '959BI_1=1']
usbrelay_off = ['usbrelay', '959BI_1=0']

# to adjsut the PID variables
# set I and D to zero
#
# increase P until you get a small overshoot, and mostly damped response,
# to a large temp change
#
# then increase I until the persistent error goes away.
#
# if the temp oscillates then increase D
#
pid = PID(setpoint=temp_gate, P=35.0, I=10.0, D=5.0)

start_time = gmtime()
step_time = start_time
step = 0

try:
    while True:
        if args.step:
            now = gmtime()
            if 3600 < (now - step_time):
                # time to step
                step_time = now
                step += 1
                if 0 <= step:
                    # step up
                    temp_gate += 1.0
                else:
                    # step down
                    temp_gate -= 1.0
                if 9 < step:
                    step = 10-

        # grab the needed output
        output = run_binary(["temper-poll", "-c"])
        try:
            # make sure it is a temperature
            temp = float(output[0])
        except:
            # bad data, exit
            # FIXME, should try to recover?
            print("temper read failed: %s" % output)
            sys.exit(1)

        # the +2- is to create and 80/20 band around the setpoint
        p_val = pid.update(temp) + 20
        p_val1 = p_val
        if p_val > 100:
            p_val1 = 100
        elif p_val < 0:
            p_val1 = 0

        if temp > temp_gate:
            perc_t = 0
        elif temp < (temp_gate - 3):
            perc_t = 100
        else:
            perc_t = ((temp_gate - temp) / 3) * 100

        if args.pid:
            # use PID controller
            perc = p_val1
        else:
            # use P controller
            perc = perc_t

        if perc > 0:
            output = run_binary(usbrelay_on)
            time_on = period * (perc / 100)
            time.sleep(time_on)
        else:
            time_on = 0

        time_off = period - time_on
        output = run_binary(usbrelay_off)

        if args.verbose:
            print("Temp %s, perc %.2f, p_val %s/%s"
                  % (temp, perc_t, p_val, p_val1))
            print("on %s, off %s" % (time_on, time_off))
            print(pid)

        if 0 < time_off:
            time.sleep(time_off)

except KeyboardInterrupt:
    print("\nCaught ^C")
    run_binary(usbrelay_off)
    sys.exit(1)
    # can't fall through ???

except IOError:
    # exception catcher
    # turn off the heat!
    run_binary(usbrelay_off)
    print("\nCaught exception, exiting\n")
    sys.exit(1)
