nut/drivers/powerp-txt.c
2010-03-26 00:20:59 +01:00

544 lines
13 KiB
C

/*
* powerp-txt.c - Model specific routines for CyberPower text
* protocol UPSes
*
* Copyright (C)
* 2007 Doug Reynolds <mav@wastegate.net>
* 2007-2008 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-txt.h"
typedef struct {
float i_volt;
float o_volt;
int o_load;
int b_chrg;
int u_temp;
float i_freq;
unsigned char flags[2];
} status_t;
static int ondelay = 1; /* minutes */
static int offdelay = 60; /* seconds */
static char powpan_answer[SMALLBUF];
static const struct {
char *var;
char *get;
char *set;
} vartab[] = {
{ "input.transfer.high", "P6\r", "C2:%03d\r" },
{ "input.transfer.low", "P7\r", "C3:%03d\r" },
{ "battery.charge.low", "P8\r", "C4:%02d\r" },
{ NULL }
};
static const struct {
char *cmd;
char *command;
} cmdtab[] = {
{ "test.battery.start.quick", "T\r" },
{ "test.battery.stop", "CT\r" },
{ "beeper.enable", "C7:1\r" },
{ "beeper.disable", "C7:0\r" },
{ "beeper.on", NULL },
{ "beeper.off", NULL },
{ "shutdown.stop", "C\r" },
{ NULL }
};
static int powpan_command(const char *command)
{
int ret;
ser_flush_io(upsfd);
ret = ser_send_pace(upsfd, UPSDELAY, "%s", command);
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", command, strlen(command));
usleep(100000);
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(powpan_answer));
return -1;
}
if (ret == 0) {
upsdebugx(3, "read: timeout");
upsdebug_hex(4, " \\_", powpan_answer, strlen(powpan_answer));
return -1;
}
upsdebug_hex(3, "read", powpan_answer, ret);
return ret;
}
static int powpan_instcmd(const char *cmdname, const char *extra)
{
int i;
char command[SMALLBUF];
if (!strcasecmp(cmdname, "beeper.off")) {
/* compatibility mode for old command */
upslogx(LOG_WARNING,
"The 'beeper.off' command has been renamed to 'beeper.disable'");
return powpan_instcmd("beeper.disable", NULL);
}
if (!strcasecmp(cmdname, "beeper.on")) {
/* compatibility mode for old command */
upslogx(LOG_WARNING,
"The 'beeper.on' command has been renamed to 'beeper.enable'");
return powpan_instcmd("beeper.enable", NULL);
}
for (i = 0; cmdtab[i].cmd != NULL; i++) {
if (strcasecmp(cmdname, cmdtab[i].cmd)) {
continue;
}
if ((powpan_command(cmdtab[i].command) == 2) && (!strcasecmp(powpan_answer, "#0"))) {
return STAT_INSTCMD_HANDLED;
}
upslogx(LOG_ERR, "%s: command [%s] failed", __func__, cmdname);
return STAT_INSTCMD_FAILED;
}
if (!strcasecmp(cmdname, "shutdown.return")) {
if (offdelay < 60) {
snprintf(command, sizeof(command), "Z.%d\r", offdelay / 6);
} else {
snprintf(command, sizeof(command), "Z%02d\r", offdelay / 60);
}
} else if (!strcasecmp(cmdname, "shutdown.stayoff")) {
if (offdelay < 60) {
snprintf(command, sizeof(command), "S.%d\r", offdelay / 6);
} else {
snprintf(command, sizeof(command), "S%02d\r", offdelay / 60);
}
} else if (!strcasecmp(cmdname, "shutdown.reboot")) {
if (offdelay < 60) {
snprintf(command, sizeof(command), "S.%dR%04d\r", offdelay / 6, ondelay);
} else {
snprintf(command, sizeof(command), "S%02dR%04d\r", offdelay / 60, ondelay);
}
} else {
upslogx(LOG_NOTICE, "%s: command [%s] unknown", __func__, cmdname);
return STAT_INSTCMD_UNKNOWN;
}
if ((powpan_command(command) == 2) && (!strcasecmp(powpan_answer, "#0"))) {
return STAT_INSTCMD_HANDLED;
}
upslogx(LOG_ERR, "%s: command [%s] failed", __func__, cmdname);
return STAT_INSTCMD_FAILED;
}
static int powpan_setvar(const char *varname, const char *val)
{
char command[SMALLBUF];
int i;
for (i = 0; vartab[i].var != NULL; i++) {
if (strcasecmp(varname, vartab[i].var)) {
continue;
}
if (!strcasecmp(val, dstate_getinfo(varname))) {
upslogx(LOG_INFO, "%s: [%s] no change for variable [%s]", __func__, val, varname);
return STAT_SET_HANDLED;
}
snprintf(command, sizeof(command), vartab[i].set, atoi(val));
if ((powpan_command(command) == 2) && (!strcasecmp(powpan_answer, "#0"))) {
dstate_setinfo(varname, "%s", val);
return STAT_SET_HANDLED;
}
upslogx(LOG_ERR, "%s: setting variable [%s] to [%s] failed", __func__, varname, val);
return STAT_SET_UNKNOWN;
}
upslogx(LOG_ERR, "%s: variable [%s] not found", __func__, varname);
return STAT_SET_UNKNOWN;
}
static void powpan_initinfo()
{
int i;
char *s;
dstate_setinfo("ups.delay.start", "%d", 60 * ondelay);
dstate_setinfo("ups.delay.shutdown", "%d", offdelay);
/*
* NOTE: The reply is already in the buffer, since the P4\r command
* was used for autodetection of the UPS. No need to do it again.
*/
if ((s = strtok(&powpan_answer[1], ",")) != NULL) {
dstate_setinfo("ups.model", "%s", rtrim(s, ' '));
}
if ((s = strtok(NULL, ",")) != NULL) {
dstate_setinfo("ups.firmware", "%s", s);
}
if ((s = strtok(NULL, ",")) != NULL) {
dstate_setinfo("ups.serial", "%s", s);
}
if ((s = strtok(NULL, ",")) != NULL) {
dstate_setinfo("ups.mfr", "%s", rtrim(s, ' '));
}
/*
* WRITE P3\r
* READ #12.0,002,008.0,00\r
*/
if (powpan_command("P3\r") > 0) {
if ((s = strtok(&powpan_answer[1], ",")) != NULL) {
dstate_setinfo("battery.voltage.nominal", "%g", strtod(s, NULL));
}
if ((s = strtok(NULL, ",")) != NULL) {
dstate_setinfo("battery.packs", "%li", strtol(s, NULL, 10));
}
if ((s = strtok(NULL, ",")) != NULL) {
dstate_setinfo("battery.capacity", "%g", strtod(s, NULL));
}
}
/*
* WRITE P2\r
* READ #1200,0720,120,47,63\r
*/
if (powpan_command("P2\r") > 0) {
if ((s = strtok(&powpan_answer[1], ",")) != NULL) {
dstate_setinfo("ups.power.nominal", "%li", strtol(s, NULL, 10));
}
if ((s = strtok(NULL, ",")) != NULL) {
dstate_setinfo("ups.realpower.nominal", "%li", strtol(s, NULL, 10));
}
if ((s = strtok(NULL, ",")) != NULL) {
dstate_setinfo("input.voltage.nominal", "%li", strtol(s, NULL, 10));
}
if ((s = strtok(NULL, ",")) != NULL) {
dstate_setinfo("input.frequency.low", "%li", strtol(s, NULL, 10));
}
if ((s = strtok(NULL, ",")) != NULL) {
dstate_setinfo("input.frequency.high", "%li", strtol(s, NULL, 10));
}
}
/*
* WRITE P1\r
* READ #120,138,088,20\r
*/
if (powpan_command("P1\r") > 0) {
if ((s = strtok(&powpan_answer[1], ",")) != NULL) {
dstate_setinfo("input.voltage.nominal", "%li", strtol(s, NULL, 10));
}
if ((s = strtok(NULL, ",")) != NULL) {
dstate_setinfo("input.transfer.high", "%li", strtol(s, NULL, 10));
}
if ((s = strtok(NULL, ",")) != NULL) {
dstate_setinfo("input.transfer.low", "%li", strtol(s, NULL, 10));
}
if ((s = strtok(NULL, ",")) != NULL) {
dstate_setinfo("battery.charge.low", "%li", strtol(s, NULL, 10));
}
}
for (i = 0; cmdtab[i].cmd != NULL; i++) {
dstate_addcmd(cmdtab[i].cmd);
}
for (i = 0; vartab[i].var != NULL; i++) {
if (!dstate_getinfo(vartab[i].var)) {
continue;
}
if (powpan_command(vartab[i].get) < 1) {
continue;
}
if ((s = strtok(&powpan_answer[1], ",")) != NULL) {
dstate_setflags(vartab[i].var, ST_FLAG_RW);
dstate_addenum(vartab[i].var, "%li", strtol(s, NULL, 10));
}
while ((s = strtok(NULL, ",")) != NULL) {
dstate_addenum(vartab[i].var, "%li", strtol(s, NULL, 10));
}
}
/*
* WRITE P5\r
* READ #<unknown>\r
*/
if (powpan_command("P5\r") > 0) {
/*
* Looking at the format of the commands "P<n>\r" it seems likely
* that this command exists also. Let's see if someone cares to
* tell us if it does (should be visible when running with -DDDDD).
*/
}
/*
* WRITE P9\r
* READ #<unknown>\r
*/
if (powpan_command("P9\r") > 0) {
/*
* Looking at the format of the commands "P<n>\r" it seems likely
* that this command exists also. Let's see if someone cares to
* tell us if it does (should be visible when running with -DDDDD).
*/
}
/*
* Cancel pending shutdown.
* WRITE C\r
* READ #0\r
*/
powpan_command("C\r");
dstate_addcmd("shutdown.return");
dstate_addcmd("shutdown.stayoff");
dstate_addcmd("shutdown.reboot");
}
static int powpan_status(status_t *status)
{
int ret;
ser_flush_io(upsfd);
/*
* WRITE D\r
* READ #I119.0O119.0L000B100T027F060.0S..\r
* 01234567890123456789012345678901234
* 0 1 2 3
*/
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, powpan_answer, 35, SER_WAIT_SEC, SER_WAIT_USEC);
if (ret < 0) {
upsdebug_with_errno(3, "read");
upsdebug_hex(4, " \\_", powpan_answer, 35);
return -1;
}
if (ret == 0) {
upsdebugx(3, "read: timeout");
upsdebug_hex(4, " \\_", powpan_answer, 35);
return -1;
}
upsdebug_hex(3, "read", powpan_answer, ret);
ret = sscanf(powpan_answer, "#I%fO%fL%dB%dT%dF%fS%2c\r",
&status->i_volt, &status->o_volt, &status->o_load,
&status->b_chrg, &status->u_temp, &status->i_freq,
status->flags);
if (ret < 7) {
upsdebugx(4, "Parsing status string failed");
return -1;
}
return 0;
}
static int powpan_updateinfo()
{
status_t status;
if (powpan_status(&status)) {
return -1;
}
dstate_setinfo("input.voltage", "%.1f", status.i_volt);
dstate_setinfo("output.voltage", "%.1f", status.o_volt);
dstate_setinfo("ups.load", "%d", status.o_load);
dstate_setinfo("input.frequency", "%.1f", status.i_freq);
dstate_setinfo("ups.temperature", "%d", status.u_temp);
dstate_setinfo("battery.charge", "%d", status.b_chrg);
status_init();
if (status.flags[0] & 0x40) {
status_set("OB");
} else {
status_set("OL");
}
if (status.flags[0] & 0x20) {
status_set("LB");
}
/* !OB && !TEST */
if (!(status.flags[0] & 0x48)) {
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) {
/* ignore */
} 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] & 0x08) {
status_set("TEST");
}
if (status.flags[0] == 0) {
status_set("OFF");
}
status_commit();
return (status.flags[0] & 0x40) ? 1 : 0;
}
static int powpan_initups()
{
int ret, i;
upsdebugx(1, "Trying text protocol...");
ser_set_speed(upsfd, device_path, B2400);
/* This fails for many devices, so don't bother to complain */
powpan_command("\r\r");
for (i = 0; i < MAXTRIES; i++) {
const char *val;
/*
* WRITE P4\r
* READ #BC1200 ,1.600,000000000000,CYBER POWER
* 01234567890123456789012345678901234567890123456
* 0 1 2 3 4
*/
ret = powpan_command("P4\r");
if (ret < 1) {
continue;
}
if (ret < 46) {
upsdebugx(2, "Expected 46 bytes, but only got %d", ret);
continue;
}
if (powpan_answer[0] != '#') {
upsdebugx(2, "Expected start character '#', but got '%c'", powpan_answer[0]);
continue;
}
val = getval("ondelay");
if (val) {
ondelay = strtol(val, NULL, 10);
}
if ((ondelay < 0) || (ondelay > 9999)) {
fatalx(EXIT_FAILURE, "Start delay '%d' out of range [0..9999]", ondelay);
}
val = getval("offdelay");
if (val) {
offdelay = strtol(val, NULL, 10);
}
if ((offdelay < 6) || (offdelay > 600)) {
fatalx(EXIT_FAILURE, "Shutdown delay '%d' out of range [6..600]", offdelay);
}
/* Truncate to nearest setable value */
if (offdelay < 60) {
offdelay -= (offdelay % 6);
} else {
offdelay -= (offdelay % 60);
}
return ret;
}
return -1;
}
subdriver_t powpan_text = {
"text",
powpan_instcmd,
powpan_setvar,
powpan_initups,
powpan_initinfo,
powpan_updateinfo
};