/*
 * powerp-bin.c - Model specific routines for CyberPower binary
 *                protocol UPSes
 *
 * Copyright (C)
 *	2007        Doug Reynolds <mav@wastegate.net>
 *	2007-2009   Arjen de Korte <adkorte-guest@alioth.debian.org>
 *
 * 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
 */

/*
 * Throughout this driver, READ and WRITE comments are shown. These are
 * the typical commands to and replies from the UPS that was used for
 * decoding the protocol (with a serial logger).
 */

#include "main.h"
#include "serial.h"

#include "powerp-bin.h"
#include "nut_stdint.h"
#include "nut_float.h"

#include <math.h>

#define POWERPANEL_BIN_VERSION	"Powerpanel-Binary 0.65"

typedef struct {
	unsigned char	start;
	unsigned char	i_volt;
	unsigned char	o_volt;
	unsigned char	o_load;
	unsigned char	fill_4;
	unsigned char	b_chrg;
	unsigned char	u_temp;
	unsigned char	i_freq;
	unsigned char	fill_8;
	unsigned char	flags[4];
	unsigned char	stop;
} status_t;

typedef struct {
	const char	*val;
	const char	command;
} valtab_t;

static enum {
	PR = 0,
	OP = 1
} type = PR;

static unsigned char	powpan_answer[SMALLBUF];

/* PR series */
static const valtab_t	tran_high_pr[] = {
	{ "138", -9 }, { "139", -8 }, { "140", -7 }, { "141", -6 }, { "142", -5 },
	{ "143", -4 }, { "144", -3 }, { "145", -2 }, { "146", -1 }, { "147",  0 },
	{ NULL, 0 }
};

/* OP series */
static const valtab_t	tran_high_op[] = {
	{ "140", -5 }, { "141", -4 }, { "142", -3 }, { "143", -2 }, { "144", -1 },
	{ "145",  0 }, { "146", +1 }, { "147", +2 }, { "148", +3 }, { "149", +4 },
	{ "150", +5 }, { NULL, 0 }
};

/* PR series */
static const valtab_t	tran_low_pr[] = {
	{ "88",  0 }, { "89", +1 }, { "90", +2 }, { "91", +3 }, { "92", +4 },
	{ "93", +5 }, { "94", +6 }, { "95", +7 }, { "96", +8 }, { "97", +9 },
	{ NULL, 0 }
};

/* OP series */
static const valtab_t	tran_low_op[] = {
	{ "85", -5 }, { "86", -4 }, { "87", -3 }, { "88", -2 }, { "89", -1 },
	{ "90",  0 }, { "91", +1 }, { "92", +2 }, { "93", +3 }, { "94", +4 },
	{ "95", +5 }, { NULL, 0 }
};

/* PR series */
static const valtab_t	batt_low_pr[] = {
	{ "25", -6 }, { "30", -5 }, { "35", -3 }, { "40", -1 }, { "45",  0 },
	{ "50", +2 }, { "55", +4 }, { "60", +6 }, { NULL, 0 }
};

/* OP series */
static const valtab_t	batt_low_op[] = {
	{ "15", -8 }, { "18", -7 }, { "19", -6 }, { "20", -5 }, { "22", -4 },
	{ "24", -3 }, { "25", -2 }, { "26", -1 }, { "28",  0 }, { "30", +1 },
	{ "32", +2 }, { "34", +3 }, { "35", +4 }, { "36", +5 }, { "38", +6 },
	{ "40", +7 }, { NULL, 0 }
};

/* PR series */
static const valtab_t	out_volt_pr[] = {
	{ "110", -10 }, { "120",  0 }, { "130", +10 }, { NULL, 0 }
};

/* OP series */
static const valtab_t	out_volt_op[] = {
	{ "110", -2 }, { "115", -1 }, { "120",  0 }, { "124", +1 }, { "128", +2 },
	{ "130", +3 }, { NULL, 0 }
};

static const valtab_t 	yes_no_info[] = {
	{ "yes", 2 }, { "no", 0 },
	{ NULL, 0 }
};

/* Older models report the model in a numeric format 'rOnn' */
static const struct {
	const char	*val;
	const char	*model;
} modeltab[] = {
	{ "rO10", "OP1000AVR" },
	{ "rO27", "OP320AVR" },
	{ "rO29", "OP500AVR" },
	{ "rO31", "OP800AVR" },
	{ "rO33", "OP850AVR" },
	{ "rO37", "OP900AVR" },
	{ "rO39", "OP650AVR" },
	{ "rO41", "OP700AVR" },
	{ "rO43", "OP1250AVR" },
	{ "rO45", "OP1500AVR" },
	{ NULL, NULL }
};

static const struct {
	const char	*var;
	const char	*get;
	const char	*set;
	const valtab_t	*map[2];
} vartab[] = {
	{ "input.transfer.high", "R\x02\r", "Q\x02%c\r", { tran_high_pr, tran_high_op } },
	{ "input.transfer.low", "R\x04\r", "Q\x04%c\r", { tran_low_pr, tran_low_op } },
	{ "battery.charge.low", "R\x08\r", "Q\x08%c\r", { batt_low_pr, batt_low_op } },
	{ "ups.start.battery", "R\x0F\r", "Q\x0F%c\r", { yes_no_info, yes_no_info } },
	{ "output.voltage.nominal", "R\x18\r", "Q\x18%c\r", { out_volt_pr, out_volt_op } },
	{ NULL, NULL, NULL, { NULL, 0 } }
};

static const struct {
	const char	*cmd;
	const char	*command;
	const size_t	len;
} cmdtab[] = {
	{ "test.battery.start.quick", "T\230\r", 3 },		/* 20 seconds test */
	{ "test.battery.stop", "CT\r", 3 },
	{ "beeper.toggle", "B\r", 2 },
	{ "shutdown.reboot", "S\0\0R\0\1W\r", 8},
	/* the shutdown.stayoff command behaves
	 * as shutdown.return when on battery */
	{ "shutdown.stayoff", "S\0\0W\r", 5 },
	{ "shutdown.stop", "C\r", 2 },
	{ NULL, NULL, 0 }
};

/* map UPS data to (approximated) input/output voltage */
static int op_volt(unsigned char in)
{
	if (in < 26) {
		return 0;
	}

	return (((float)in * 200 / 230) + 6);
}

/* map UPS data to (approximated) load */
static int op_load(unsigned char in)
{
	if (in > 108) {
		return 200;
	}

	return (in * 200) / 108;
}

/* map UPS data to (approximated) charge percentage */
static int op_chrg(unsigned char in)
{
	if (in > 197) {
		return 100;
	}

	if (in < 151) {
		return 0;
	}

	return ((in - 151) * 100) / 46;
}

/* map UPS data to (approximated) temperature */
static float op_temp(unsigned char in)
{
	return (pow((float)in / 32, 2) + 10);
}

/* map UPS data to (approximated) frequency */
static float op_freq(unsigned char in)
{
	return (12600.0 / (in + 32));
}

static ssize_t powpan_command(const char *buf, size_t bufsize)
{
	ssize_t	ret;

	ser_flush_io(upsfd);

	ret = ser_send_buf_pace(upsfd, UPSDELAY, buf, bufsize);

	if (ret < 0) {
		upsdebug_with_errno(3, "send");
		return -1;
	}

	if (ret == 0) {
		upsdebug_with_errno(3, "send: timeout");
		return -1;
	}

	upsdebug_hex(3, "send", buf, bufsize);

	usleep(100000);

	ret = ser_get_buf_len(upsfd, powpan_answer, bufsize-1, SER_WAIT_SEC, SER_WAIT_USEC);

	if (ret < 0) {
		upsdebug_with_errno(3, "read");
		upsdebug_hex(4, "  \\_", buf, bufsize-1);
		return -1;
	}

	if (ret == 0) {
		upsdebugx(3, "read: timeout");
		upsdebug_hex(4, "  \\_", buf, bufsize-1);
		return -1;
	}

	upsdebug_hex(3, "read", powpan_answer, (size_t)ret);
	return ret;
}

static int powpan_instcmd(const char *cmdname, const char *extra)
{
	int	i;

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

	for (i = 0; cmdtab[i].cmd != NULL; i++) {
		ssize_t	ret;

		if (strcasecmp(cmdname, cmdtab[i].cmd)) {
			continue;
		}

		upslog_INSTCMD_POWERSTATE_CHECKED(cmdname, extra);
		ret = powpan_command(cmdtab[i].command, cmdtab[i].len);
		assert(cmdtab[i].len < SSIZE_MAX);
		if (ret >= 0
		 && (ret == (ssize_t)(cmdtab[i].len - 1))
		 && (!memcmp(powpan_answer, cmdtab[i].command, cmdtab[i].len - 1))
		) {
			return STAT_INSTCMD_HANDLED;
		}

		upslog_INSTCMD_FAILED(cmdname, extra);
		return STAT_INSTCMD_FAILED;
	}

	upslog_INSTCMD_UNKNOWN(cmdname, extra);
	return STAT_INSTCMD_UNKNOWN;
}

static int powpan_setvar(const char *varname, const char *val)
{
	char	command[SMALLBUF];
	int 	i, j;

	upsdebug_SET_STARTING(varname, val);

	for (i = 0;  vartab[i].var != NULL; i++) {

		if (strcasecmp(varname, vartab[i].var)) {
			continue;
		}

		if (!strcasecmp(val, dstate_getinfo(varname))) {
			upslogx(LOG_INFO, "powpan_setvar: [%s] no change for variable [%s]", val, varname);
			return STAT_SET_HANDLED;
		}

		for (j = 0; vartab[i].map[type][j].val != NULL; j++) {

			if (strcasecmp(val, vartab[i].map[type][j].val)) {
				continue;
			}

			snprintf_dynamic(command, sizeof(command), vartab[i].set,
				"%c", vartab[i].map[type][j].command);

			if ((powpan_command(command, 4) == 3) && (!memcmp(powpan_answer, command, 3))) {
				dstate_setinfo(varname, "%s", val);
				return STAT_SET_HANDLED;
			}

			upslog_SET_FAILED(varname, val);
			return STAT_SET_UNKNOWN;	/* FIXME: ..._FAILED ? */
		}

		upslog_SET_INVALID(varname, val);
		return STAT_SET_UNKNOWN;	/* FIXME: ..._INVALID? */
	}

	upslog_SET_UNKNOWN(varname, val);
	return STAT_SET_UNKNOWN;
}

static void powpan_initinfo(void)
{
	int	i, j;
	char	*s;

	dstate_setinfo("ups.delay.start", "%d", 45);
	dstate_setinfo("ups.delay.shutdown", "%d", 0);	/* almost immediate */

	/*
	 * NOTE: The reply is already in the buffer, since the F\r command
	 * was used for autodetection of the UPS. No need to do it again.
	 */
	if ((s = strtok((char *)&powpan_answer[1], ".")) != NULL) {
		for (i = 0; modeltab[i].val != NULL; i++) {
			if (!strncmp(s, modeltab[i].val, strlen(modeltab[i].val))) {
				break;
			}
		}
		if (modeltab[i].model) {
			/* model found in table */
			dstate_setinfo("ups.model", "%s", modeltab[i].model);
		} else {
			/* report model value as is */
			dstate_setinfo("ups.model", "%s", str_rtrim(s, ' '));
		}
	}
	if ((s = strtok(NULL, ".")) != NULL) {
		dstate_setinfo("input.voltage.nominal", "%d", (unsigned char)s[0]);
	}
	if ((s = strtok(NULL, ".")) != NULL) {
		dstate_setinfo("input.frequency.nominal", "%d", (unsigned char)s[0]);
	}
	if ((s = strtok(NULL, ".")) != NULL) {
		dstate_setinfo("ups.firmware", "%c.%c%c%c", s[0], s[1], s[2], s[3]);
	}

	for (i = 0; cmdtab[i].cmd != NULL; i++) {
		dstate_addcmd(cmdtab[i].cmd);
	}

	for (i = 0; vartab[i].var != NULL; i++) {

		if (powpan_command(vartab[i].get, 3) < 2) {
			continue;
		}

		for (j = 0; vartab[i].map[type][j].val != NULL; j++) {

			if (vartab[i].map[type][j].command != powpan_answer[1]) {
				continue;
			}

			dstate_setinfo(vartab[i].var, "%s", vartab[i].map[type][j].val);
			break;
		}

		if (dstate_getinfo(vartab[i].var) == NULL) {
			upslogx(LOG_WARNING, "warning: [%d] unknown value for [%s]!",
				powpan_answer[1], vartab[i].var);
			continue;
		}

		dstate_setflags(vartab[i].var, ST_FLAG_RW);

		for (j = 0; vartab[i].map[type][j].val != NULL; j++) {
			dstate_addenum(vartab[i].var, "%s", vartab[i].map[type][j].val);
		}
	}

	/*
	 * FIXME: The following commands need to be reverse engineered. It
	 * looks like they are used in detecting the UPS model. To rule out
	 * any incompatibilities, only use them when running in debug mode.
	 */
	if (nut_debug_level > 2) {
		powpan_command("R\x29\r", 3);
		powpan_command("R\x2B\r", 3);
		powpan_command("R\x3D\r", 3);
	}

	dstate_addcmd("shutdown.stayoff");
	dstate_addcmd("shutdown.reboot");

	/* install handlers */
	upsh.instcmd = powpan_instcmd;
	upsh.setvar = powpan_setvar;
}

static ssize_t powpan_status(status_t *status)
{
	ssize_t	ret;

	ser_flush_io(upsfd);

	/*
	 * WRITE D\r
	 * READ #VVL.CTF.....\r
	 *      01234567890123
	 */
	ret = ser_send_pace(upsfd, UPSDELAY, "D\r");

	if (ret < 0) {
		upsdebug_with_errno(3, "send");
		return -1;
	}

	if (ret == 0) {
		upsdebug_with_errno(3, "send: timeout");
		return -1;
	}

	upsdebug_hex(3, "send", "D\r", 2);

	usleep(200000);

	ret = ser_get_buf_len(upsfd, status, sizeof(*status), SER_WAIT_SEC, SER_WAIT_USEC);

	if (ret < 0) {
		upsdebug_with_errno(3, "read");
		upsdebug_hex(4, "  \\_", status, sizeof(*status));
		return -1;
	}

	if (ret == 0) {
		upsdebugx(3, "read: timeout");
		upsdebug_hex(4, "  \\_", status, sizeof(*status));
		return -1;
	}

	upsdebug_hex(3, "read", status, (size_t)ret);

	if ((status->flags[0] + status->flags[1]) != 255) {
		upsdebugx(4, "  \\_ : checksum flags[0..1] failed");
		return -1;
	}

	if ((status->flags[2] + status->flags[3]) != 255) {
		upsdebugx(4, "  \\_ : checksum flags[2..3] failed");
		return -1;
	}

	return 0;
}

static int powpan_updateinfo(void)
{
	status_t	status;

	if (powpan_status(&status)) {
		return -1;
	}

	switch (type)
	{
	case OP:
		dstate_setinfo("input.voltage", "%d", op_volt(status.i_volt));
		if (status.flags[0] & 0x84) {
			dstate_setinfo("output.voltage", "%s", dstate_getinfo("output.voltage.nominal"));
		} else {
			dstate_setinfo("output.voltage", "%d", op_volt(status.i_volt));
		}
		dstate_setinfo("ups.load", "%d", op_load(status.o_load));
		dstate_setinfo("battery.charge", "%d", op_chrg(status.b_chrg));
		dstate_setinfo("ups.temperature", "%.1f", op_temp(status.u_temp));
		dstate_setinfo("input.frequency", "%.1f", op_freq(status.i_freq));
		break;

	case PR:
#if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP) && (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_COVERED_SWITCH_DEFAULT)
# pragma GCC diagnostic push
# pragma GCC diagnostic ignored "-Wcovered-switch-default"
#endif
	/* All enum cases defined as of the time of coding
	 * have been covered above. Handle later definitions,
	 * memory corruptions and buggy inputs below...
	 */
	default:
#if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP) && (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_COVERED_SWITCH_DEFAULT)
# pragma GCC diagnostic pop
#endif
		dstate_setinfo("input.voltage", "%d", status.i_volt);
		if (status.flags[0] & 0x84) {
			dstate_setinfo("output.voltage", "%s", dstate_getinfo("output.voltage.nominal"));
		} else {
			dstate_setinfo("output.voltage", "%d", status.o_volt);
		}
		dstate_setinfo("ups.load", "%d", status.o_load);
		dstate_setinfo("battery.charge", "%d", status.b_chrg);
		dstate_setinfo("ups.temperature", "%d", status.u_temp);
		dstate_setinfo("input.frequency", "%.1f", (status.i_freq == 0) ? 0.0 : 45.0 + (float)status.i_freq / 10.0);
		break;
	}

	if (status.flags[0] & 0x01) {
		dstate_setinfo("ups.beeper.status", "enabled");
	} else {
		dstate_setinfo("ups.beeper.status", "disabled");
	}

	status_init();

	if (status.flags[0] & 0x80) {
		status_set("OB");
	} else {
		status_set("OL");
	}

	if (status.flags[0] & 0x40) {
		status_set("LB");
	}

	/* !OB && !TEST */
	if (!(status.flags[0] & 0x84) && status.o_volt) {

		if (status.o_volt < 0.5 * status.i_volt) {
			upsdebugx(2, "%s: output voltage too low", __func__);
		} else if (status.o_volt < 0.95 * status.i_volt) {
			status_set("TRIM");
		} else if (status.o_volt < 1.05 * status.i_volt) {
			upsdebugx(2, "%s: appears to be in BYPASS state", __func__);
		} else if (status.o_volt < 1.5 * status.i_volt) {
			status_set("BOOST");
		} else {
			upsdebugx(2, "%s: output voltage too high", __func__);
		}
	}

	if (status.flags[0] & 0x04) {
		status_set("TEST");
	}

	if (status.flags[0] == 0) {
		status_set("OFF");
	}

	status_commit();

	return (status.flags[0] & 0x80) ? 1 : 0;
}

static ssize_t powpan_initups(void)
{
	ssize_t	ret;
	int	i;

	upsdebugx(1, "Trying %s protocol...", powpan_binary.version);

	ser_set_speed(upsfd, device_path, B1200);

	/* This fails for many devices, so don't bother to complain */
	ser_send_pace(upsfd, UPSDELAY, "\r\r");

	for (i = 0; i < MAXTRIES; i++) {

		ser_flush_io(upsfd);

		/*
		 * WRITE F\r
		 * READ .PR2200    .x.<.1100
		 *      01234567890123456789
		 */
		ret = ser_send_pace(upsfd, UPSDELAY, "F\r");

		if (ret < 0) {
			upsdebug_with_errno(3, "send");
			continue;
		}

		if (ret == 0) {
			upsdebug_with_errno(3, "send: timeout");
			continue;
		}

		upsdebug_hex(3, "send", "F\r", 2);

		usleep(200000);

		ret = ser_get_line(upsfd, powpan_answer, sizeof(powpan_answer),
			ENDCHAR, IGNCHAR, SER_WAIT_SEC, SER_WAIT_USEC);

		if (ret < 0) {
			upsdebug_with_errno(3, "read");
			upsdebug_hex(4, "  \\_", powpan_answer, strlen((char *)powpan_answer));
			continue;
		}

		if (ret == 0) {
			upsdebugx(3, "read: timeout");
			upsdebug_hex(4, "  \\_", powpan_answer, strlen((char *)powpan_answer));
			continue;
		}

		upsdebug_hex(3, "read", powpan_answer, (size_t)ret);

		if (ret < 20) {
			upsdebugx(2, "Expected 20 bytes but only got %" PRIiSIZE, ret);
			continue;
		}

		if (powpan_answer[0] != '.') {
			upsdebugx(2, "Expected start character '.' but got '%c'", (char)powpan_answer[0]);
			continue;
		}

		/* See if we need to use the 'old' protocol for the OP series */
		if (!strncmp((char *)&powpan_answer[1], "OP", 2)) {
			type = OP;
		}

		/* This is for an older model series, that reports the model numerically */
		if (!strncmp((char *)&powpan_answer[1], "rO", 2)) {
			type = OP;
		}

		if (getval("ondelay")) {
			fatalx(EXIT_FAILURE, "Setting 'ondelay' not supported by %s driver", powpan_binary.version);
		}

		if (getval("offdelay")) {
			fatalx(EXIT_FAILURE, "Setting 'offdelay' not supported by %s driver", powpan_binary.version);
		}

		return ret;
	}

	return -1;
}

subdriver_t powpan_binary = {
	"binary",
	POWERPANEL_BIN_VERSION,
	powpan_instcmd,
	powpan_setvar,
	powpan_initups,
	powpan_initinfo,
	powpan_updateinfo
};
