/* tripplite.c - model specific routines for Tripp Lite SmartUPS models
   (tested with:
   "SMART 700" [on back -- "SmartPro UPS" on front], "SMART700SER")

   tripplite.c was derived from Russell Kroll's bestups.c by Rik Faith.

   Copyright (C) 1999  Russell Kroll <rkroll@exploits.org>
   Copyright (C) 2001  Rickard E. (Rik) Faith <faith@alephnull.com>
   Copyright (C) 2004,2008  Nicholas J. Kain <nicholas@kain.us>

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 2 of the License, or
   (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/

/* REFERENCE 1

   A few magic numbers were derived from the GPL'd file
   opensrc_server/upscmd.cpp, formerly (but not longer) available from
   Tripp Lite at http://www.tripplite.com/linux/.
*/

/* REFERENCE 2

   Other hints on commands were found on these web pages:
   http://www.kursknet.ru/~boa/ups/stinfo_command.html
   http://www.kursknet.ru/~boa/ups/rtinfo_command.html

   These pages confirm the information in the Tripp Lite source code
   referenced above and add more details.

   The first page tells how to derive the VA rating from w_value and
   l_value.  It's a confusing explanation because shifts are used to
   mask out bits.  Here is an example starting with the formula on the
   web page and proceeding to a formula that uses C syntax.

         I have a SMART 700 (700VA, 450W)
         w_value = 0x84 [available from upsc as REG1]
         l_value=- 0x60 [available from upsc as REG2]

         Unit Type: bit 6 of w_value is 0 so I have a Smart (vs. a Unison)

         VA Rating: ((([V:W Result]<<2)*8)+([V:L Result]>>3))*5
                    = (((w_value<<2)*8)+([l_value]>>3)) * 5
                    = ((w_value & 0x3f)*32 + (l_value >> 3)) * 5
                    = (4 * 32 + 12) * 5
                    = 700
*/

/* Known UPS Commands:
 *
 * :N%02X -- delay the UPS for provided time (hex seconds)
 * :H%06X -- reboot the UPS.  UPS will restart after provided time (hex s)
 * :A     -- begins a self-test
 * :C     -- fetches result of a self-test
 * :K1    -- turns on power receptacles
 * :K0    -- turns off power receptacles
 * :K3    -- turns on bank 1 receptacle(s)
 * :K2    -- turns off bank 1 receptacle(s)
 * :K5    -- turns on bank 2 receptacle(s)
 * :K4    -- turns off bank 2 receptacle(s)
 * :G     -- unconfirmed: shuts down UPS until power returns
 * :Q1    -- enable "Remote Reboot"
 * :Q0    -- disable "Remote Reboot"
 * :W     -- returns 'W' data
 * :L     -- returns 'L' data
 * :V     -- returns 'V' data (firmware revision)
 * :X     -- returns 'X' data (firmware checksum)
 * :D     -- returns general status data
 * :B     -- returns battery voltage (hexadecimal decivolts)
 * :I     -- returns minimum input voltage (hexadecimal hertz)
 * :M     -- returns maximum input voltage (hexadecimal hertz)
 * :P     -- returns power rating
 * :Z     -- unknown
 * :U     -- unknown
 * :O     -- unknown
 * :E     -- unknown
 * :Y     -- returns mains frequency  (':D' is preferred)
 * :T     -- returns ups temperature  (':D' is preferred)
 * :R     -- returns input voltage    (':D' is preferred)
 * :F     -- returns load percentage  (':D' is preferred)
 * :S     -- enables remote reboot/remote power on
 */

/*  Returned value from ':D' looks like:
 *
 *  0123456789abcdef01234
 *  ABCCDEFFGGGGHHIIIIJJJ
 *  A    0=LB   1=OK
 *  B    0=OVER 1=OK
 *  CC   INFO_UTILITY
 *  D    0=normal 1=TRIM 2=BOOST 3="EXTRA BOOST"
 *  E    0=OFF 1=OB 2=OL 3=OB (1 and 3 are the same?)
 *  FF   f(INFO_UPSTEMP)
 *  GG   ? INFO_BATTPCT (00 when OB, values don't match table we use)
 *  HH   ? (always 00)
 *  II   INFO_LOADPCT
 *  JJJJ ? (e.g., 5B82 5B82 5982 037B 0082)
 *  KKK  INFO_ACFREQ * 10
 */

#include "main.h"
#include "serial.h"
#include "tripplite.h"
#include "nut_stdint.h"
#include "nut_float.h"
#include <ctype.h>

#define DRIVER_NAME	"Tripp-Lite SmartUPS driver"
#define DRIVER_VERSION	"0.99"

/* driver description structure */
upsdrv_info_t upsdrv_info = {
	DRIVER_NAME,
	DRIVER_VERSION,
	"Russell Kroll <rkroll@exploits.org>\n" \
	"Rickard E. (Rik) Faith <faith@alephnull.com>\n" \
	"Nicholas J. Kain <nicholas@kain.us>",
	DRV_STABLE,
	{ NULL }
};

/* Time in seconds to delay before shutting down. */
static unsigned int offdelay = DEFAULT_OFFDELAY;
static unsigned int startdelay = DEFAULT_STARTDELAY;
static unsigned int bootdelay = DEFAULT_BOOTDELAY;

static long hex2d(char *start, unsigned int len)
{
	char buf[32];

	snprintf(buf, sizeof buf, "%.*s", len, start);
	return strtol(buf, NULL, 16);
}

/* The UPS that I'm using (SMART700SER) has the bizarre characteristic
 * of innately echoing back commands.  Therefore, we cannot use
 * ser_get_line and must manually discard our echoed command.
 *
 * All UPS commands are challenge-response, so this function makes things
 * very clean.
 *
 * return: # of chars in buf, excluding terminating \0 */
static ssize_t send_cmd(const char *str, char *buf, size_t len)
{
	char c;
	ssize_t ret = 0;
	ssize_t i = 0;

	ser_flush_io(upsfd);
	ser_send(upsfd, "%s", str);

	if (!len || !buf || len > SSIZE_MAX)
		return -1;

	for (;;) {
		ret = ser_get_char(upsfd, &c, SER_WAIT_SEC, SER_WAIT_USEC);
		if (ret < 1)
			return -1;
		if (c == ENDCHAR)
			break;
	}
	do {
		ret = ser_get_char(upsfd, &c, SER_WAIT_SEC, SER_WAIT_USEC);
		if (ret < 1)
			return -1;

		if (c == IGNCHAR || c == ENDCHAR)
			continue;
		buf[i++] = c;
	} while (c != ENDCHAR && i < (int)len);
	buf[i] = '\0';
	return i;
}

static void get_letter_cmd(const char *str, char *buf, size_t len)
{
	int tries;
	ssize_t ret;

	for (tries = 0; tries < MAXTRIES; ++tries) {
		ret = send_cmd(str, buf, len);
		if ((ret > 0) && isalnum((unsigned char)buf[0]))
			return;
	}
	fatalx(EXIT_FAILURE, "\nFailed to find UPS - giving up...");
}

static ssize_t do_reboot_now(void)
{
	char buf[256], cmd[16];

	snprintf(cmd, sizeof cmd, ":H%06X\r", startdelay);
	return send_cmd(cmd, buf, sizeof buf);
}

static void do_reboot(void)
{
	char buf[256], cmd[16];

	snprintf(cmd, sizeof cmd, ":N%02X\r", bootdelay);
	send_cmd(cmd, buf, sizeof buf);
	do_reboot_now();
}

static ssize_t soft_shutdown(void)
{
	char buf[256], cmd[16];

	snprintf(cmd, sizeof cmd, ":N%02X\r", offdelay);
	send_cmd(cmd, buf, sizeof buf);
	return send_cmd(":G\r", buf, sizeof buf);
}

static ssize_t hard_shutdown(void)
{
	char buf[256], cmd[16];

	snprintf(cmd, sizeof cmd, ":N%02X\r", offdelay);
	send_cmd(cmd, buf, sizeof buf);
	return send_cmd(":K0\r", buf, sizeof buf);
}

static int instcmd(const char *cmdname, const char *extra)
{
	char buf[256];

	/* May be used in logging below, but not as a command argument */
	NUT_UNUSED_VARIABLE(extra);
	upsdebug_INSTCMD_STARTING(cmdname, extra);

	if (!strcasecmp(cmdname, "test.battery.start")) {
		upslog_INSTCMD_POWERSTATE_MAYBE(cmdname, extra);
		send_cmd(":A\r", buf, sizeof buf);
		return STAT_INSTCMD_HANDLED;
	}
	if (!strcasecmp(cmdname, "load.off")) {
		upslog_INSTCMD_POWERSTATE_CHANGE(cmdname, extra);
		send_cmd(":K0\r", buf, sizeof buf);
		return STAT_INSTCMD_HANDLED;
	}
	if (!strcasecmp(cmdname, "load.on")) {
		upslog_INSTCMD_POWERSTATE_MAYBE(cmdname, extra);
		send_cmd(":K1\r", buf, sizeof buf);
		return STAT_INSTCMD_HANDLED;
	}
	if (!strcasecmp(cmdname, "outlet.1.load.off")) {
		upslog_INSTCMD_POWERSTATE_CHANGE(cmdname, extra);
		send_cmd(":K2\r", buf, sizeof buf);
		return STAT_INSTCMD_HANDLED;
	}
	if (!strcasecmp(cmdname, "outlet.1.load.on")) {
		upslog_INSTCMD_POWERSTATE_MAYBE(cmdname, extra);
		send_cmd(":K3\r", buf, sizeof buf);
		return STAT_INSTCMD_HANDLED;
	}
	if (!strcasecmp(cmdname, "outlet.2.load.off")) {
		upslog_INSTCMD_POWERSTATE_CHANGE(cmdname, extra);
		send_cmd(":K4\r", buf, sizeof buf);
		return STAT_INSTCMD_HANDLED;
	}
	if (!strcasecmp(cmdname, "outlet.2.load.on")) {
		upslog_INSTCMD_POWERSTATE_MAYBE(cmdname, extra);
		send_cmd(":K5\r", buf, sizeof buf);
		return STAT_INSTCMD_HANDLED;
	}
	if (!strcasecmp(cmdname, "shutdown.reboot")) {
		upslog_INSTCMD_POWERSTATE_CHANGE(cmdname, extra);
		do_reboot_now();
		return STAT_INSTCMD_HANDLED;
	}
	if (!strcasecmp(cmdname, "shutdown.reboot.graceful")) {
		upslog_INSTCMD_POWERSTATE_CHANGE(cmdname, extra);
		do_reboot();
		return STAT_INSTCMD_HANDLED;
	}
	if (!strcasecmp(cmdname, "shutdown.return")) {
		upslog_INSTCMD_POWERSTATE_CHANGE(cmdname, extra);
		soft_shutdown();
		return STAT_INSTCMD_HANDLED;
	}
	if (!strcasecmp(cmdname, "shutdown.stayoff")) {
		upslog_INSTCMD_POWERSTATE_CHANGE(cmdname, extra);
		hard_shutdown();
		return STAT_INSTCMD_HANDLED;
	}

	upslog_INSTCMD_UNKNOWN(cmdname, extra);
	return STAT_INSTCMD_UNKNOWN;
}

static int setvar(const char *varname, const char *val)
{
	upsdebug_SET_STARTING(varname, val);

	if (!strcasecmp(varname, "ups.delay.shutdown")) {
		int ipv = atoi(val);
		if (ipv >= 0)
			offdelay = (unsigned int)ipv;
		dstate_setinfo("ups.delay.shutdown", "%u", offdelay);
		return STAT_SET_HANDLED;
	}
	if (!strcasecmp(varname, "ups.delay.start")) {
		int ipv = atoi(val);
		if (ipv >= 0)
			startdelay = (unsigned int)ipv;
		dstate_setinfo("ups.delay.start", "%u", startdelay);
		return STAT_SET_HANDLED;
	}
	if (!strcasecmp(varname, "ups.delay.reboot")) {
		int ipv = atoi(val);
		if (ipv >= 0)
			bootdelay = (unsigned int)ipv;
		dstate_setinfo("ups.delay.reboot", "%u", bootdelay);
		return STAT_SET_HANDLED;
	}

	upslog_SET_UNKNOWN(varname, val);
	return STAT_SET_UNKNOWN;
}

void upsdrv_initinfo(void)
{
	const char *model;
	char w_value[16], l_value[16], v_value[16], x_value[16];
	int  gen, plugs;
	long w, l, va;

	get_letter_cmd(":W\r", w_value, sizeof w_value);
	get_letter_cmd(":L\r", l_value, sizeof l_value);
	get_letter_cmd(":V\r", v_value, sizeof v_value);
	get_letter_cmd(":X\r", x_value, sizeof x_value);

	dstate_setinfo("ups.mfr", "%s", "Tripp Lite");

	w = hex2d(w_value, 2);
	l = hex2d(l_value, 2);

	model = "Smart";
	if (w & 0x40)
		model = "Unison";

	va = ((w & 0x3f) * 32 + (l >> 3)) * 5;  /* New formula */
	if (!(w & 0x80))
		va = l / 2;   /* Old formula */

	gen = 1 + (!(x_value[0] & 0x07) * !(x_value[1] & 0x07));
	plugs = x_value[0] - !!(x_value[1] >> 3) * 8;

	dstate_setinfo("ups.model", "%s %ld", model, va);
	dstate_setinfo("ups.firmware", "%c%c (Gen %d)",
			'A'+v_value[0]-'0', 'A'+v_value[1]-'0', gen);

	dstate_setinfo("ups.delay.shutdown", "%u", offdelay);
	dstate_setflags("ups.delay.shutdown", ST_FLAG_RW | ST_FLAG_STRING);
	dstate_setaux("ups.delay.shutdown", 3);
	dstate_setinfo("ups.delay.start", "%u", startdelay);
	dstate_setflags("ups.delay.start", ST_FLAG_RW | ST_FLAG_STRING);
	dstate_setaux("ups.delay.start", 8);
	dstate_setinfo("ups.delay.reboot", "%u", bootdelay);
	dstate_setflags("ups.delay.reboot", ST_FLAG_RW | ST_FLAG_STRING);
	dstate_setaux("ups.delay.reboot", 3);

	dstate_addcmd("test.battery.start"); /* Turns off automatically */
	dstate_addcmd("load.off");
	dstate_addcmd("load.on");
	dstate_addcmd("shutdown.reboot");
	dstate_addcmd("shutdown.reboot.graceful");
	dstate_addcmd("shutdown.return");
	dstate_addcmd("shutdown.stayoff");

	if (plugs > 1) {
		dstate_addcmd("outlet.1.load.off");
		dstate_addcmd("outlet.1.load.on");
		if (plugs > 2) {
			dstate_addcmd("outlet.2.load.off");
			dstate_addcmd("outlet.2.load.on");
		}
	}

	upsh.instcmd = instcmd;
	upsh.setvar = setvar;

	printf("Detected %s %s on %s\n",
	dstate_getinfo("ups.mfr"), dstate_getinfo("ups.model"), device_path);
}

void upsdrv_shutdown(void)
{
	/* Only implement "shutdown.default"; do not invoke
	 * general handling of other `sdcommands` here */

	int	ret = do_loop_shutdown_commands("shutdown.return", NULL);
	if (handling_upsdrv_shutdown > 0)
		set_exit_flag(ret == STAT_INSTCMD_HANDLED ? EF_EXIT_SUCCESS : EF_EXIT_FAILURE);
}

void upsdrv_updateinfo(void)
{
	static int numfails;
	char buf[256];
	int bp, temp;
	ssize_t len;
	long volt, load, vmax, vmin, stest;
	int bcond, lstate, tstate, mode;
	float bv, freq;

	len = send_cmd(":D\r", buf, sizeof buf);
	if (len != 21) {
		++numfails;
		if (numfails > MAXTRIES) {
			ser_comm_fail("Data command failed: [%" PRIiSIZE "] bytes != 21 bytes.", len);
			dstate_datastale();
		}
		return;
	}

	volt = hex2d(buf + 2, 2);
	temp = (int)(hex2d(buf + 6, 2)*0.3636 - 21.0);
	load = hex2d(buf + 12, 2);
	freq = hex2d(buf + 18, 3) / 10.0;
	bcond = buf[0];
	lstate = buf[1];
	tstate = buf[4];
	mode = buf[5];
	if (volt > INVOLT_MAX || volt < INVOLT_MIN ||
			temp > TEMP_MAX || temp < TEMP_MIN ||
			load > LOAD_MAX || load < LOAD_MIN ||
			freq > FREQ_MAX || freq < FREQ_MIN) {
		++numfails;
		if (numfails > MAXTRIES) {
			ser_comm_fail("Data out of bounds: [%0ld,%3d,%3ld,%02.2f]",
					volt, temp, load, freq);
			dstate_datastale();
		}
		return;
	}

	send_cmd(":B\r", buf, sizeof buf);
	bv = (float)(hex2d(buf, 2)) / 10.0;
	if (bv > 50.0 || bv < 0.0) {
		++numfails;
		if (numfails > MAXTRIES) {
			ser_comm_fail("Battery voltage out of bounds: [%02.1f]", bv);
			dstate_datastale();
		}
		return;
	}

	send_cmd(":M\r", buf, sizeof buf);
	vmax = hex2d(buf, 2);
	if (vmax > INVOLT_MAX || vmax < INVOLT_MIN) {
		++numfails;
		if (numfails > MAXTRIES) {
			ser_comm_fail("InVoltMax out of bounds: [%ld]", vmax);
			dstate_datastale();
		}
		return;
	}

	send_cmd(":I\r", buf, sizeof buf);
	vmin = hex2d(buf, 2);
	if (vmin > INVOLT_MAX || vmin < INVOLT_MIN) {
		++numfails;
		if (numfails > MAXTRIES) {
			ser_comm_fail("InVoltMin out of bounds: [%ld]", vmin);
			dstate_datastale();
		}
		return;
	}

	send_cmd(":C\r", buf, sizeof buf);
	errno = 0;
	stest = strtol(buf, 0, 10);
	if (errno == ERANGE) {
		++numfails;
		if (numfails > MAXTRIES) {
			ser_comm_fail("Self test is out of range: [%ld]", stest);
			dstate_datastale();
		}
		return;
	}
	if (errno == EINVAL) {
		++numfails;
		if (numfails > MAXTRIES) {
			ser_comm_fail("Self test returned non-numeric data.");
			dstate_datastale();
		}
		return;
	}
	if (stest > 3 || stest < 0) {
		++numfails;
		if (numfails > MAXTRIES) {
			ser_comm_fail("Self test out of bounds: [%ld]", stest);
			dstate_datastale();
		}
		return;
	}

	/* We've successfully gathered all the data for an update. */
	numfails = 0;

	dstate_setinfo("input.voltage", "%0ld", volt);
	dstate_setinfo("ups.temperature", "%3d", temp);
	dstate_setinfo("ups.load", "%3ld", load);
	dstate_setinfo("input.frequency", "%02.2f", freq);

	status_init();

	/* Battery Voltage Condition */
	switch (bcond) {
		case '0': /* Low Battery */
			status_set("LB");
			break;
		case '1': /* Normal */
			break;
		default: /* Unknown */
			upslogx(LOG_ERR, "Unknown battery state: %c", bcond);
			break;
	}

	/* Load State */
	switch (lstate) {
		case '0': /* Overload */
			status_set("OVER");
			break;
		case '1': /* Normal */
			break;
		default: /* Unknown */
			upslogx(LOG_ERR, "Unknown load state: %c", lstate);
			break;
	}

	/* Tap State */
	switch (tstate) {
		case '0': /* Normal */
			break;
		case '1': /* Reducing */
			status_set("TRIM");
			break;
		case '2': /* Boost */
		case '3': /* Extra Boost */
			status_set("BOOST");
			break;
		default: /* Unknown */
			upslogx(LOG_ERR, "Unknown tap state: %c", tstate);
			break;
	}

	/* Mode */
	switch (mode) {
		case '0': /* Off */
			status_set("OFF");
			break;
		case '1': /* On Battery */
			status_set("OB");
			break;
		case '2': /* On Line */
			status_set("OL");
			break;
		case '3': /* On Battery */
			status_set("OB");
			break;
		default: /* Unknown */
			upslogx(LOG_ERR, "Unknown mode state: %c", mode);
			break;
	}

	status_commit();

	/* dq ~= sqrt(dV) is a reasonable approximation
	 * Results fit well against the discrete function used in the Tripp Lite
	 * source, but give a continuous result. */
	if (bv >= MAX_VOLT)
		bp = 100;
	else if (bv <= MIN_VOLT)
		bp = 10;
	else
		bp = (int)(100*sqrt((bv - MIN_VOLT) / (MAX_VOLT - MIN_VOLT)));

	dstate_setinfo("battery.voltage", "%.1f", bv);
	dstate_setinfo("battery.charge",  "%3d", bp);
	dstate_setinfo("input.voltage.maximum", "%ld", vmax);
	dstate_setinfo("input.voltage.minimum", "%ld", vmin);

	switch (stest) {
		case 0:
			dstate_setinfo("ups.test.result", "%s", "OK");
			break;
		case 1:
			dstate_setinfo("ups.test.result", "%s", "Battery Bad (Replace)");
			break;
		case 2:
			dstate_setinfo("ups.test.result", "%s", "In Progress");
			break;
		case 3:
			dstate_setinfo("ups.test.result", "%s", "Bad Inverter");
			break;
		default:
			dstate_setinfo("ups.test.result", "Unknown (%s)", buf);
			break;
	}

	dstate_dataok();
	ser_comm_good();
}

void upsdrv_help(void)
{
}

/* optionally tweak prognames[] entries */
void upsdrv_tweak_prognames(void)
{
}

void upsdrv_makevartable(void)
{
	char msg[256];

	snprintf(msg, sizeof msg, "Set shutdown delay, in seconds (default=%d).",
		DEFAULT_OFFDELAY);
	addvar(VAR_VALUE, "offdelay", msg);
	snprintf(msg, sizeof msg, "Set start delay, in seconds (default=%d).",
		DEFAULT_STARTDELAY);
	addvar(VAR_VALUE, "startdelay", msg);
	snprintf(msg, sizeof msg, "Set reboot delay, in seconds (default=%d).",
		DEFAULT_BOOTDELAY);
	addvar(VAR_VALUE, "rebootdelay", msg);
}

void upsdrv_initups(void)
{
	char	*val;

	upsfd = ser_open(device_path);
	ser_set_speed(upsfd, device_path, B2400);

	if ((val = getval("offdelay"))) {
		int ipv = atoi(val);
		if (ipv >= 0)
			offdelay = (unsigned int)ipv;
	}
	if ((val = getval("startdelay"))) {
		int ipv = atoi(val);
		if (ipv >= 0)
			startdelay = (unsigned int)ipv;
	}
	if ((val = getval("rebootdelay"))) {
		int ipv = atoi(val);
		if (ipv >= 0)
			bootdelay = (unsigned int)ipv;
	}
}

void upsdrv_cleanup(void)
{
	ser_close(upsfd, device_path);
}

