/* nutdrv_qx_bestups.c - Subdriver for Best Power/Sola Australia UPSes
 *
 * Copyright (C)
 *   2014 Daniele Pezzini <hyouko@gmail.com>
 * Based on:
 *  bestups.c - Copyright (C)
 *    1999 Russell Kroll <rkroll@exploits.org>
 *         Jason White <jdwhite@jdwhite.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
 *
 */

#include "main.h"
#include "nut_float.h"
#include "nut_stdint.h"
#include "nutdrv_qx.h"
#include "nutdrv_qx_blazer-common.h"
#include "nutdrv_qx_bestups.h"

#define BESTUPS_VERSION "BestUPS 0.08"

/* Support functions */
static int	bestups_claim(void);
static void	bestups_initups(void);
static void	bestups_makevartable(void);

/* Answer preprocess functions */
static int	bestups_preprocess_id_answer(item_t *item, const int len);

/* Preprocess functions */
static int	bestups_process_setvar(item_t *item, char *value, const size_t valuelen);
static int	bestups_process_bbb_status_bit(item_t *item, char *value, const size_t valuelen);
static int	bestups_manufacturer(item_t *item, char *value, const size_t valuelen);
static int	bestups_model(item_t *item, char *value, const size_t valuelen);
static int	bestups_batt_runtime(item_t *item, char *value, const size_t valuelen);
static int	bestups_batt_packs(item_t *item, char *value, const size_t valuelen);
static int	bestups_get_pins_shutdown_mode(item_t *item, char *value, const size_t valuelen);
static int	bestups_voltage_settings(item_t *item, char *value, const size_t valuelen);

/* ups.conf settings */
static int	pins_shutdown_mode;

/* General settings */
static int	inverted_bbb_bit = 0;


/* == Ranges/enums == */

/* Range for ups.delay.start */
static info_rw_t	bestups_r_ondelay[] = {
	{ "60", 0 },
	{ "599940", 0 },
	{ "", 0 }
};

/* Range for ups.delay.shutdown */
static info_rw_t	bestups_r_offdelay[] = {
	{ "12", 0 },
	{ "5940", 0 },
	{ "", 0 }
};

/* Range for number of battery packs */
static info_rw_t	bestups_r_batt_packs[] = {
	{ "0", 0 },
	{ "5", 0 },
	{ "", 0 }
};

/* Range for pin shutdown mode */
static info_rw_t	bestups_r_pins_shutdown_mode[] = {
	{ "0", 0 },
	{ "6", 0 },
	{ "", 0 }
};


/* == qx2nut lookup table == */
static item_t	bestups_qx2nut[] = {

	/* Query UPS for status
	 * > [Q1\r]
	 * < [(226.0 195.0 226.0 014 49.0 27.5 30.0 00001000\r]
	 *    01234567890123456789012345678901234567890123456
	 *    0         1         2         3         4
	 */

	{ "input.voltage",		0,	NULL,	"Q1\r",	"",	47,	'(',	"",	1,	5,	"%.1f",	0,	NULL,	NULL,	NULL },
	{ "input.voltage.fault",	0,	NULL,	"Q1\r",	"",	47,	'(',	"",	7,	11,	"%.1f",	0,	NULL,	NULL,	NULL },
	{ "output.voltage",		0,	NULL,	"Q1\r",	"",	47,	'(',	"",	13,	17,	"%.1f",	0,	NULL,	NULL,	NULL },
	{ "ups.load",			0,	NULL,	"Q1\r",	"",	47,	'(',	"",	19,	21,	"%.0f",	0,	NULL,	NULL,	NULL },
	{ "input.frequency",		0,	NULL,	"Q1\r",	"",	47,	'(',	"",	23,	26,	"%.1f",	0,	NULL,	NULL,	NULL },
	{ "battery.voltage",		0,	NULL,	"Q1\r",	"",	47,	'(',	"",	28,	31,	"%.2f",	0,	NULL,	NULL,	qx_multiply_battvolt },
	{ "ups.temperature",		0,	NULL,	"Q1\r",	"",	47,	'(',	"",	33,	36,	"%.1f",	0,	NULL,	NULL,	NULL },
	/* Status bits */
	{ "ups.status",			0,	NULL,	"Q1\r",	"",	47,	'(',	"",	38,	38,	NULL,	QX_FLAG_QUICK_POLL,	NULL,	NULL,	blazer_process_status_bits },		/* Utility Fail (Immediate) */
	{ "ups.status",			0,	NULL,	"Q1\r",	"",	47,	'(',	"",	39,	39,	NULL,	QX_FLAG_QUICK_POLL,	NULL,	NULL,	blazer_process_status_bits },		/* Battery Low */
	{ "ups.alarm",			0,	NULL,	"Q1\r",	"",	47,	'(',	"",	41,	41,	NULL,	0,			NULL,	NULL,	blazer_process_status_bits },		/* UPS Failed */
	{ "ups.type",			0,	NULL,	"Q1\r",	"",	47,	'(',	"",	42,	42,	"%s",	QX_FLAG_STATIC,		NULL,	NULL,	blazer_process_status_bits },		/* UPS Type */
	{ "ups.status",			0,	NULL,	"Q1\r",	"",	47,	'(',	"",	43,	43,	NULL,	QX_FLAG_QUICK_POLL,	NULL,	NULL,	blazer_process_status_bits },		/* Test in Progress */
	{ "ups.status",			0,	NULL,	"Q1\r",	"",	47,	'(',	"",	44,	44,	NULL,	QX_FLAG_QUICK_POLL,	NULL,	NULL,	blazer_process_status_bits },		/* Shutdown Active */
/*	{ "ups.beeper.status",		0,	NULL,	"Q1\r",	"",	47,	'(',	"",	45,	45,	"%s",	0,			NULL,	NULL,	blazer_process_status_bits },		*//* Beeper status: not supported; always 0 */
	{ "ups.status",			0,	NULL,	"Q1\r",	"",	47,	'(',	"",	40,	40,	NULL,	QX_FLAG_QUICK_POLL,	NULL,	NULL,	bestups_process_bbb_status_bit },	/* Bypass/Boost or Buck Active - keep this one at the end as it needs the processed data from the previous items */

	/* Query UPS for ratings and model infos
	 * > [ID\r]
	 * < [FOR,750,120,120,20.0,27.6\r]	case #1: length = 26
	 * < [FOR,1500,120,120,20.0,27.6\r]	case #2: length = 27
	 * < [FOR,3000,120,120,20.0,100.6\r]	case #3: length = 28
	 * < [FOR, 750,120,120,20.0, 27.6\r]	after being preprocessed: length = 28
	 *    0123456789012345678901234567
	 *    0         1         2
	 */

	{ "device.mfr",			0,	NULL,	"ID\r",	"",	28,	0,	"",	0,	2,	"%s",	QX_FLAG_STATIC,		NULL,	bestups_preprocess_id_answer,	bestups_manufacturer },
	{ "device.model",		0,	NULL,	"ID\r",	"",	28,	0,	"",	0,	2,	"%s",	QX_FLAG_STATIC,		NULL,	bestups_preprocess_id_answer,	bestups_model },
	{ "ups.power.nominal",		0,	NULL,	"ID\r",	"",	28,	0,	"",	4,	7,	"%.0f",	QX_FLAG_STATIC,		NULL,	bestups_preprocess_id_answer,	NULL },
	{ "input.voltage.nominal",	0,	NULL,	"ID\r",	"",	28,	0,	"",	9,	11,	"%.0f",	QX_FLAG_STATIC,		NULL,	bestups_preprocess_id_answer,	NULL },
	{ "output.voltage.nominal",	0,	NULL,	"ID\r",	"",	28,	0,	"",	13,	15,	"%.0f",	QX_FLAG_STATIC,		NULL,	bestups_preprocess_id_answer,	NULL },
	{ "battery.voltage.low",	0,	NULL,	"ID\r",	"",	28,	0,	"",	17,	20,	"%.1f",	QX_FLAG_SEMI_STATIC,	NULL,	bestups_preprocess_id_answer,	NULL },
	{ "battery.voltage.high",	0,	NULL,	"ID\r",	"",	28,	0,	"",	22,	26,	"%.1f",	QX_FLAG_SEMI_STATIC,	NULL,	bestups_preprocess_id_answer,	NULL },

	/* Query UPS for battery runtime (not available on the Patriot Pro/Sola 320 model series)
	 * > [RT\r]
	 * < [025\r]
	 *    0123
	 *    0
	 */

	{ "battery.runtime",	0,	NULL,	"RT\r",	"",	4,	0,	"",	0,	2,	"%.0f",	 QX_FLAG_SKIP,	NULL,	NULL,	bestups_batt_runtime },

	/* Query UPS for number of battery packs (available only on the Axxium/Sola 620 model series)
	 * > [BP?\r]
	 * < [02\r]
	 *    012
	 *    0
	 */

	{ "battery.packs",	ST_FLAG_RW,	bestups_r_batt_packs,	"BP?\r",	"",	3,	0,	"",	0,	1,	"%d",	QX_FLAG_SEMI_STATIC | QX_FLAG_RANGE | QX_FLAG_SKIP,	NULL,	NULL,	bestups_batt_packs },

	/* Set number of battery packs to n (integer, 0-5) (available only on the Axxium/Sola 620 model series)
	 * > [BPn\r]
	 * < []
	 */

	{ "battery.packs",	0,		bestups_r_batt_packs,	"BP%.0f\r",	"",	0,	0,	"",	0,	0,	NULL,	QX_FLAG_SETVAR | QX_FLAG_RANGE | QX_FLAG_SKIP,		NULL,	NULL,	bestups_process_setvar },

	/* Query UPS for shutdown mode functionality of Pin 1 and Pin 7 on the UPS DB9 communication port (Per Best Power's EPS-0059)
	 * > [SS?\r]
	 * < [0\r]
	 *    01
	 *    0
	 */

	{ "pins_shutdown_mode",	ST_FLAG_RW,	bestups_r_pins_shutdown_mode,	"SS?\r",	"",	2,	0,	"",	0,	0,	"%.0f",	QX_FLAG_SEMI_STATIC | QX_FLAG_RANGE | QX_FLAG_NONUT,			NULL,	NULL,	bestups_get_pins_shutdown_mode },

	/* Set shutdown mode functionality of Pin 1 and Pin 7 on the UPS DB9 communication port (Per Best Power's EPS-0059) to n (integer, 0-6)
	 * > [SSn\r]
	 * < []
	 */

	{ "pins_shutdown_mode",	0,		bestups_r_pins_shutdown_mode,	"SS%.0f\r",	"",	0,	0,	"",	0,	0,	NULL,	QX_FLAG_SETVAR | QX_FLAG_RANGE | QX_FLAG_NONUT | QX_FLAG_SKIP,		NULL,	NULL,	bestups_process_setvar },

	/* Query UPS for voltage settings
	 * > [M\r]
	 * < [0\r]
	 *    01
	 *    0
	 */

	{ "input.transfer.low",		0,	NULL,	"M\r",	"",	2,	0,	"",	0,	0,	"%d",	0,	NULL,	NULL,	bestups_voltage_settings },
	{ "input.transfer.boost.low",	0,	NULL,	"M\r",	"",	2,	0,	"",	0,	0,	"%d",	0,	NULL,	NULL,	bestups_voltage_settings },
	{ "input.transfer.boost.high",	0,	NULL,	"M\r",	"",	2,	0,	"",	0,	0,	"%d",	0,	NULL,	NULL,	bestups_voltage_settings },
	{ "input.voltage.nominal",	0,	NULL,	"M\r",	"",	2,	0,	"",	0,	0,	"%d",	0,	NULL,	NULL,	bestups_voltage_settings },
	{ "output.voltage.nominal",	0,	NULL,	"M\r",	"",	2,	0,	"",	0,	0,	"%d",	0,	NULL,	NULL,	bestups_voltage_settings },
	{ "input.transfer.trim.low",	0,	NULL,	"M\r",	"",	2,	0,	"",	0,	0,	"%d",	0,	NULL,	NULL,	bestups_voltage_settings },
	{ "input.transfer.trim.high",	0,	NULL,	"M\r",	"",	2,	0,	"",	0,	0,	"%d",	0,	NULL,	NULL,	bestups_voltage_settings },
	{ "input.transfer.high",	0,	NULL,	"M\r",	"",	2,	0,	"",	0,	0,	"%d",	0,	NULL,	NULL,	bestups_voltage_settings },

	/* Instant commands */
	{ "shutdown.return",		0,	NULL,	"S%s\r",	"",	0,	0,	"",	0,	0,	NULL,	QX_FLAG_CMD,	NULL,	NULL,	blazer_process_command },
	{ "shutdown.stayoff",		0,	NULL,	"S%s\r",	"",	0,	0,	"",	0,	0,	NULL,	QX_FLAG_CMD,	NULL,	NULL,	blazer_process_command },
	{ "shutdown.stop",		0,	NULL,	"C\r",		"",	0,	0,	"",	0,	0,	NULL,	QX_FLAG_CMD,	NULL,	NULL,	NULL },
	{ "load.on",			0,	NULL,	"C\r",		"",	0,	0,	"",	0,	0,	NULL,	QX_FLAG_CMD,	NULL,	NULL,	NULL },
	{ "load.off",			0,	NULL,	"S00R0000\r",	"",	0,	0,	"",	0,	0,	NULL,	QX_FLAG_CMD,	NULL,	NULL,	NULL },
	{ "test.battery.start",		0,	NULL,	"T%02d\r",	"",	0,	0,	"",	0,	0,	NULL,	QX_FLAG_CMD,	NULL,	NULL,	blazer_process_command },
	{ "test.battery.start.deep",	0,	NULL,	"TL\r",		"",	0,	0,	"",	0,	0,	NULL,	QX_FLAG_CMD,	NULL,	NULL,	NULL },
	{ "test.battery.start.quick",	0,	NULL,	"T\r",		"",	0,	0,	"",	0,	0,	NULL,	QX_FLAG_CMD,	NULL,	NULL,	NULL },
	{ "test.battery.stop",		0,	NULL,	"CT\r",		"",	0,	0,	"",	0,	0,	NULL,	QX_FLAG_CMD,	NULL,	NULL,	NULL },

	/* Server-side settable vars */
	{ "ups.delay.start",		ST_FLAG_RW,	bestups_r_ondelay,	NULL,	"",	0,	0,	"",	0,	0,	DEFAULT_ONDELAY,	QX_FLAG_ABSENT | QX_FLAG_SETVAR | QX_FLAG_RANGE,	NULL,	NULL,	blazer_process_setvar },
	{ "ups.delay.shutdown",		ST_FLAG_RW,	bestups_r_offdelay,	NULL,	"",	0,	0,	"",	0,	0,	DEFAULT_OFFDELAY,	QX_FLAG_ABSENT | QX_FLAG_SETVAR | QX_FLAG_RANGE,	NULL,	NULL,	blazer_process_setvar },

	/* End of structure. */
	{ NULL,		0,	NULL,	NULL,	"",	0,	0,	"",	0,	0,	NULL,	0,	NULL,	NULL,	NULL }
};


/* == Testing table == */
#ifdef TESTING
static testing_t	bestups_testing[] = {
	{ "Q1\r",	"(215.0 195.0 230.0 014 49.0 22.7 30.0 00100000\r",	-1 },
	{ "ID\r",	"FOR,750,120,120,20.0,27.6\r",	-1 },
	{ "RT\r",	"015\r",	-1 },
	{ "BP?\r",	"02\r",	-1 },
	{ "BP1\r",	"",	-1 },
	{ "SS?\r",	"0\r",	-1 },
	{ "SS2\r",	"",	-1 },
	{ "M\r",	"0\r",	-1 },
	{ "S03\r",	"",	-1 },
	{ "C\r",	"",	-1 },
	{ "S02R0005\r",	"",	-1 },
	{ "S.5R0001\r",	"",	-1 },
	{ "T04\r",	"",	-1 },
	{ "TL\r",	"",	-1 },
	{ "T\r",	"",	-1 },
	{ "CT\r",	"",	-1 },
	{ NULL }
};
#endif	/* TESTING */


/* == Support functions == */

/* This function allows the subdriver to "claim" a device: return 1 if the device is supported by this subdriver, else 0. */
static int	bestups_claim(void)
{
	/* We need at least Q1 and ID to run this subdriver */

	item_t	*item = find_nut_info("input.voltage", 0, 0);

	/* Don't know what happened */
	if (!item)
		return 0;

	/* No reply/Unable to get value */
	if (qx_process(item, NULL))
		return 0;

	/* Unable to process value */
	if (ups_infoval_set(item) != 1)
		return 0;

	/* UPS Model */
	item = find_nut_info("device.model", 0, 0);

	/* Don't know what happened */
	if (!item) {
		dstate_delinfo("input.voltage");
		return 0;
	}

	/* No reply/Unable to get value */
	if (qx_process(item, NULL)) {
		dstate_delinfo("input.voltage");
		return 0;
	}

	/* Unable to process value */
	if (ups_infoval_set(item) != 1) {
		dstate_delinfo("input.voltage");
		return 0;
	}

	return 1;
}

/* Subdriver-specific initups */
static void	bestups_initups(void)
{
	blazer_initups_light(bestups_qx2nut);
}

/* Subdriver-specific flags/vars */
static void	bestups_makevartable(void)
{
	addvar(VAR_VALUE, "pins_shutdown_mode", "Set shutdown mode functionality of Pin 1 and Pin 7 on the UPS DB9 communication port (Per Best Power's EPS-0059) to n (integer, 0-6)");

	blazer_makevartable_light();
}


/* == Answer preprocess functions == */

/* Preprocess the answer we got back from the UPS when queried with 'ID\r': make data begin always at the same indexes */
static int	bestups_preprocess_id_answer(item_t *item, const int len)
{
	int	i;
	char	refined[SMALLBUF] = "",
		rawval[SMALLBUF] = "",
		*token,
		*saveptr = NULL;

	if (len <= 0)
		return len;

	if (len < 25 || len > 27) {
		upsdebugx(4, "%s: wrong length [%s: %d]", __func__, item->info_type, len);
		return -1;
	}

	/* e.g.:
	 *  1. item->answer = "FOR,750,120,120,20.0,27.6\r";	len = 26
	 *  2. item->answer = "FOR,1500,120,120,20.0,27.6\r";	len = 27
	 *  3. item->answer = "FOR,3000,120,120,20.0,100.6\r";	len = 28 */
	upsdebugx(4, "read: '%.*s'", (int)strcspn(item->answer, "\r"), item->answer);

	snprintf(rawval, sizeof(rawval), "%s", item->answer);

	for (i = 1, token = strtok_r(rawval, ",", &saveptr); token != NULL; i++, token = strtok_r(NULL, ",", &saveptr)) {

		switch (i)
		{
		case 1:
			snprintf(refined, sizeof(refined), "%s", token);
			continue;
		case 2:	/* Output power */
			snprintfcat(refined, sizeof(refined), ",%4s", token);
			continue;
		case 6:	/* Battery voltage at full charge (+ trailing CR) */
			snprintfcat(refined, sizeof(refined), ",%6s", token);
			continue;
		default:
			snprintfcat(refined, sizeof(refined), ",%s", token);
		}

	}

	if (i != 7 || strlen(refined) != 28) {
		upsdebugx(2, "noncompliant reply: '%.*s'", (int)strcspn(refined, "\r"), refined);
		return -1;
	}

	upsdebugx(4, "read: '%.*s'", (int)strcspn(refined, "\r"), refined);

	/* e.g.: item->answer = "FOR, 750,120,120,20.0, 27.6\r"; len = 28 */
	return snprintf(item->answer, sizeof(item->answer), "%s", refined);
}


/* == Preprocess functions == */

/* *SETVAR(/NONUT)* Preprocess setvars */
static int	bestups_process_setvar(item_t *item, char *value, const size_t valuelen)
{
	double	val;

	if (!strlen(value)) {
		upsdebugx(2, "%s: value not given for %s", __func__, item->info_type);
		return -1;
	}

	val = strtod(value, NULL);

	if (!strcasecmp(item->info_type, "pins_shutdown_mode")) {

		if (d_equal(val, pins_shutdown_mode)) {
			upslogx(LOG_INFO, "%s is already set to %.0f", item->info_type, val);
			return -1;
		}

	}

	snprintf_dynamic(value, valuelen, item->command, "%f", val);

	return 0;
}

/* Bypass/Boost or Buck status */
static int	bestups_process_bbb_status_bit(item_t *item, char *value, const size_t valuelen)
{
	/* Bypass/Boost/Buck bit is not reliable when a battery test, shutdown or on battery condition occurs: always ignore it in these cases */
	if (!((unsigned int)(qx_status()) & STATUS(OL)) || ((unsigned int)(qx_status()) & (STATUS(CALIB) | STATUS(FSD)))) {

		if (item->value[0] == '1')
			item->value[0] = '0';

		return blazer_process_status_bits(item, value, valuelen);

	}

	/* UPSes with inverted bypass/boost/buck bit */
	if (inverted_bbb_bit) {

		if (item->value[0] == '1')
			item->value[0] = '0';

		else if (item->value[0] == '0')
			item->value[0] = '1';

	}

	return blazer_process_status_bits(item, value, valuelen);
}

/* Identify UPS manufacturer */
static int	bestups_manufacturer(item_t *item, char *value, const size_t valuelen)
{
	/* Best Power devices */
	if (
		!strcmp(item->value, "AX1") ||
		!strcmp(item->value, "FOR") ||
		!strcmp(item->value, "FTC") ||
		!strcmp(item->value, "PR2") ||
		!strcmp(item->value, "PRO")
	) {
		snprintf_dynamic(value, valuelen, item->dfl, "%s", "Best Power");
		return 0;
	}

	/* Sola Australia devices */
	if (
		!strcmp(item->value, "325") ||
		!strcmp(item->value, "520") ||
		!strcmp(item->value, "620")
	) {
		snprintf_dynamic(value, valuelen, item->dfl, "%s", "Sola Australia");
		return 0;
	}

	/* Unknown devices */
	snprintf_dynamic(value, valuelen, item->dfl, "%s", "Unknown");

	return 0;
}

/* Identify UPS model and unskip qx2nut table's items accordingly */
static int	bestups_model(item_t *item, char *value, const size_t valuelen)
{
	item_t	*unskip;

	/* Best Power devices */

	if (!strcmp(item->value, "AX1")) {

		snprintf_dynamic(value, valuelen, item->dfl, "%s", "Axxium Rackmount");

	} else if (!strcmp(item->value, "FOR")) {

		snprintf_dynamic(value, valuelen, item->dfl, "%s", "Fortress");

	} else if (!strcmp(item->value, "FTC")) {

		snprintf_dynamic(value, valuelen, item->dfl, "%s", "Fortress Telecom");

	} else if (!strcmp(item->value, "PR2")) {

		snprintf_dynamic(value, valuelen, item->dfl, "%s", "Patriot Pro II");
		inverted_bbb_bit = 1;

	} else if (!strcmp(item->value, "PRO")) {

		snprintf_dynamic(value, valuelen, item->dfl, "%s", "Patriot Pro");
		inverted_bbb_bit = 1;

	/* Sola Australia devices */
	} else if (
		!strcmp(item->value, "320") ||
		!strcmp(item->value, "325") ||
		!strcmp(item->value, "520") ||
		!strcmp(item->value, "525") ||
		!strcmp(item->value, "620")
	) {

		snprintf(value, valuelen, "Sola %s", item->value);

	/* Unknown devices */
	} else {

		snprintf(value, valuelen, "Unknown (%s)", item->value);
		upslogx(LOG_INFO, "Unknown model detected - please report this ID: '%s'", item->value);

	}

	/* Unskip qx2nut table's items according to the UPS model */

	/* battery.runtime var is not available on the Patriot Pro/Sola 320 model series: leave it skipped in these cases, otherwise unskip it */
	if (strcmp(item->value, "PRO") && strcmp(item->value, "320")) {

		unskip = find_nut_info("battery.runtime", 0, 0);

		/* Don't know what happened */
		if (!unskip)
			return -1;

		unskip->qxflags &= ~QX_FLAG_SKIP;

	}

	/* battery.packs var is available only on the Axxium/Sola 620 model series: unskip it in these cases */
	if (!strcmp(item->value, "AX1") || !strcmp(item->value, "620")) {

		unskip = find_nut_info("battery.packs", 0, QX_FLAG_SETVAR);

		/* Don't know what happened */
		if (!unskip)
			return -1;

		unskip->qxflags &= ~QX_FLAG_SKIP;

	}

	return 0;
}

/* Battery runtime */
static int	bestups_batt_runtime(item_t *item, char *value, const size_t valuelen)
{
	double	runtime;

	if (strspn(item->value, "0123456789 .") != strlen(item->value)) {
		upsdebugx(2, "%s: non numerical value [%s: %s]", __func__, item->info_type, item->value);
		return -1;
	}

	/* Battery runtime is reported by the UPS in minutes, NUT expects seconds */
	runtime = strtod(item->value, NULL) * 60;

	snprintf_dynamic(value, valuelen, item->dfl, "%f", runtime);

	return 0;
}

/* Battery packs */
static int	bestups_batt_packs(item_t *item, char *value, const size_t valuelen)
{
	item_t	*unskip;
	long	l;

	if (strspn(item->value, "0123456789 ") != strlen(item->value)) {
		upsdebugx(2, "%s: non numerical value [%s: %s]", __func__, item->info_type, item->value);
		return -1;
	}

	l = strtol(item->value, NULL, 10);
	if (l < 0 || l > INT_MAX) {
		upsdebugx(2, "%s: value out of range [%s: %s]", __func__, item->info_type, item->value);
		return -1;
	}

	snprintf_dynamic(value, valuelen, item->dfl, "%d", (int)l);

	/* Unskip battery.packs setvar */
	unskip = find_nut_info("battery.packs", QX_FLAG_SETVAR, 0);

	/* Don't know what happened */
	if (!unskip)
		return -1;

	unskip->qxflags &= ~QX_FLAG_SKIP;

	return 0;
}

/* *NONUT* Get shutdown mode functionality of Pin 1 and Pin 7 on the UPS DB9 communication port (Per Best Power's EPS-0059) as set in the UPS */
static int	bestups_get_pins_shutdown_mode(item_t *item, char *value, const size_t valuelen)
{
	item_t	*unskip;
	long	l;

	if (strspn(item->value, "0123456789") != strlen(item->value)) {
		upsdebugx(2, "%s: non numerical value [%s: %s]", __func__, item->info_type, item->value);
		return -1;
	}

	l = strtol(item->value, NULL, 10);
	if (l > INT_MAX) {
		upsdebugx(2, "%s: pins_shutdown_mode out of range [%s: %s]",
			__func__, item->info_type, item->value);
		return -1;
	}
	pins_shutdown_mode = (int)l;

	/* NOTE: Mapping table has "%.0f" for R/W of this concept's value,
	 * meaning zero digits after the decimal point (could as well be
	 * an int right away?)
	 * TODO: Someone with the device should check replacement by "%d".
	 */
	snprintf_dynamic(value, valuelen, item->dfl, "%.0f", (double)pins_shutdown_mode);

	/* We were not asked by the user to change the value */
	if ((item->qxflags & QX_FLAG_NONUT) && !getval(item->info_type))
		return 0;

	/* Unskip setvar */
	unskip = find_nut_info(item->info_type, QX_FLAG_SETVAR, 0);

	/* Don't know what happened */
	if (!unskip)
		return -1;

	unskip->qxflags &= ~QX_FLAG_SKIP;

	return 0;
}

/* Voltage settings */
static int	bestups_voltage_settings(item_t *item, char *value, const size_t valuelen)
{
	long		index;
	int		val;
	const char	*nominal_voltage;
	const struct {
		const int	low;		/* Low voltage		->	input.transfer.low / input.transfer.boost.low */
		const int	boost;		/* Boost voltage	->	input.transfer.boost.high */
		const int	nominal;	/* Nominal voltage	->	input.voltage.nominal / output.voltage.nominal */
		const int	buck;		/* Buck voltage		->	input.transfer.trim.low */
		const int	high;		/* High voltage		->	input.transfer.high / input.transfer.trim.high */
	} voltage_settings[] = {
		/* U models voltage limits, for:
		 * - Fortress (750U, 1050U, 1425U, 1800U and 2250U)
		 * - Fortress Rackmount (750, 1050, 1425, 1800, and 2250 VA)
		 * - Patriot Pro II (400U, 750U, and 1000U) */
	/* M	  low	boost	nominal	buck	high */
	/* 0 */	{ 96,	109,	120,	130,	146 },	/* LEDs lit: 2,3,4 (Default) */
	/* 1 */	{ 96,	109,	120,	138,	156 },	/* LEDs lit: 1,3,4 */
	/* 2 */	{ 90,	104,	120,	130,	146 },	/* LEDs lit: 2,3,5 */
	/* 3 */	{ 90,	104,	120,	138,	156 },	/* LEDs lit: 1,3,5 */
	/* 4 */	{ 90,	104,	110,	120,	130 },	/* LEDs lit: 3,4,5 */
	/* 5 */	{ 90,	104,	110,	130,	146 },	/* LEDs lit: 2,4,5 */
	/* 6 */	{ 90,	96,	110,	120,	130 },	/* LEDs lit: 3,4,6 */
	/* 7 */	{ 90,	96,	110,	130,	146 },	/* LEDs lit: 2,4,6 */
	/* 8 */	{ 96,	109,	128,	146,	156 },	/* LEDs lit: 1,2,4 */
	/* 9 */	{ 90,	104,	128,	146,	156 },	/* LEDs lit: 1,2,5 */

		/* E models voltage limits, for:
		 * - Fortress (750E, 1050E, 1425E, and 2250E)
		 * - Fortress Rackmount (750, 1050, 1425, and 2250 VA)
		 * - Patriot Pro II (400E, 750E, and 1000E) */
	/* M	  low	boost	nominal	buck	high */
	/* 0 */	{ 200,	222,	240,	250,	284 },	/* LEDs lit: 2,3,4 */
	/* 1 */	{ 200,	222,	240,	264,	290 },	/* LEDs lit: 1,3,4 */
	/* 2 */	{ 188,	210,	240,	250,	284 },	/* LEDs lit: 2,3,5 */
	/* 3 */	{ 188,	210,	240,	264,	290 },	/* LEDs lit: 1,3,5 */
	/* 4 */	{ 188,	210,	230,	244,	270 },	/* LEDs lit: 3,4,5 (Default) */
	/* 5 */	{ 188,	210,	230,	250,	284 },	/* LEDs lit: 2,4,5 */
	/* 6 */	{ 180,	200,	230,	244,	270 },	/* LEDs lit: 3,4,6 */
	/* 7 */	{ 180,	200,	230,	250,	284 },	/* LEDs lit: 2,4,6 */
	/* 8 */	{ 165,	188,	208,	222,	244 },	/* LEDs lit: 4,5,6 */
	/* 9 */	{ 165,	188,	208,	244,	270 }	/* LEDs lit: 3,5,6 */
	};

	if (strspn(item->value, "0123456789") != strlen(item->value)) {
		upsdebugx(2, "%s: non numerical value [%s: %s]", __func__, item->info_type, item->value);
		return -1;
	}

	index = strtol(item->value, NULL, 10);

	if (index < 0 || index > 9) {
		upsdebugx(2, "%s: value '%ld' out of range [0..9]", __func__, index);
		return -1;
	}

	nominal_voltage = dstate_getinfo("input.voltage.nominal");

	if (!nominal_voltage)
		nominal_voltage = dstate_getinfo("output.voltage.nominal");

	if (!nominal_voltage) {
		upsdebugx(2, "%s: unable to get nominal voltage", __func__);
		return -1;
	}

	/* E models */
	if (strtol(nominal_voltage, NULL, 10) > 160)
		index += 10;

	if (!strcasecmp(item->info_type, "input.transfer.low") || !strcasecmp(item->info_type, "input.transfer.boost.low")) {

		val = voltage_settings[index].low;

	} else if (!strcasecmp(item->info_type, "input.transfer.boost.high")) {

		val = voltage_settings[index].boost;

	} else if (!strcasecmp(item->info_type, "input.voltage.nominal") || !strcasecmp(item->info_type, "output.voltage.nominal")) {

		val = voltage_settings[index].nominal;

	} else if (!strcasecmp(item->info_type, "input.transfer.trim.low")) {

		val = voltage_settings[index].buck;

	} else if (!strcasecmp(item->info_type, "input.transfer.trim.high") || !strcasecmp(item->info_type, "input.transfer.high")) {

		val = voltage_settings[index].high;

	} else {

		/* Don't know what happened */
		return -1;

	}

	snprintf_dynamic(value, valuelen, item->dfl, "%d", val);

	return 0;
}


/* == Subdriver interface == */
subdriver_t	bestups_subdriver = {
	BESTUPS_VERSION,
	bestups_claim,
	bestups_qx2nut,
	bestups_initups,
	NULL,
	bestups_makevartable,
	NULL,
	NULL,
#ifdef TESTING
	bestups_testing,
#endif	/* TESTING */
};
