2455 lines
63 KiB
C
2455 lines
63 KiB
C
/* nutdrv_qx.c - Driver for USB and serial UPS units with Q* protocols
|
|
*
|
|
* Copyright (C)
|
|
* 2013 Daniele Pezzini <hyouko@gmail.com>
|
|
* Based on:
|
|
* usbhid-ups.c - Copyright (C)
|
|
* 2003-2012 Arnaud Quette <arnaud.quette@gmail.com>
|
|
* 2005 John Stamp <kinsayder@hotmail.com>
|
|
* 2005-2006 Peter Selinger <selinger@users.sourceforge.net>
|
|
* 2007-2009 Arjen de Korte <adkorte-guest@alioth.debian.org>
|
|
* blazer.c - Copyright (C)
|
|
* 2008-2009 Arjen de Korte <adkorte-guest@alioth.debian.org>
|
|
* 2012 Arnaud Quette <ArnaudQuette@Eaton.com>
|
|
* blazer_ser.c - Copyright (C)
|
|
* 2008 Arjen de Korte <adkorte-guest@alioth.debian.org>
|
|
* blazer_usb.c - Copyright (C)
|
|
* 2003-2009 Arjen de Korte <adkorte-guest@alioth.debian.org>
|
|
* 2011-2012 Arnaud Quette <arnaud.quette@free.fr>
|
|
*
|
|
* 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
|
|
*
|
|
*/
|
|
|
|
#define DRIVER_VERSION "0.06"
|
|
|
|
#include "main.h"
|
|
|
|
#include <math.h>
|
|
|
|
/* note: QX_USB/QX_SERIAL set through Makefile */
|
|
#ifdef QX_USB
|
|
#include "libusb.h"
|
|
#include "usb-common.h"
|
|
|
|
#ifdef QX_SERIAL
|
|
#define DRIVER_NAME "Generic Q* USB/Serial driver"
|
|
#else
|
|
#define DRIVER_NAME "Generic Q* USB driver"
|
|
#endif /* QX_SERIAL */
|
|
#else
|
|
#define DRIVER_NAME "Generic Q* Serial driver"
|
|
#endif /* QX_USB */
|
|
|
|
#ifdef QX_SERIAL
|
|
#include "serial.h"
|
|
#define SER_WAIT_SEC 1 /* 3 seconds for Best UPS */
|
|
#endif /* QX_SERIAL */
|
|
|
|
#include "nutdrv_qx.h"
|
|
|
|
/* == Subdrivers == */
|
|
/* Include all known subdrivers */
|
|
#include "nutdrv_qx_mecer.h"
|
|
#include "nutdrv_qx_megatec.h"
|
|
#include "nutdrv_qx_megatec-old.h"
|
|
#include "nutdrv_qx_mustek.h"
|
|
#include "nutdrv_qx_q1.h"
|
|
#include "nutdrv_qx_voltronic.h"
|
|
#include "nutdrv_qx_voltronic-qs.h"
|
|
#include "nutdrv_qx_zinto.h"
|
|
|
|
/* Master list of avaiable subdrivers */
|
|
static subdriver_t *subdriver_list[] = {
|
|
&voltronic_subdriver,
|
|
&voltronic_qs_subdriver,
|
|
&mustek_subdriver,
|
|
&megatec_old_subdriver,
|
|
&mecer_subdriver,
|
|
&megatec_subdriver,
|
|
&zinto_subdriver,
|
|
/* Fallback Q1 subdriver */
|
|
&q1_subdriver,
|
|
NULL
|
|
};
|
|
|
|
|
|
/* == Driver description structure == */
|
|
upsdrv_info_t upsdrv_info = {
|
|
DRIVER_NAME,
|
|
DRIVER_VERSION,
|
|
"Daniele Pezzini <hyouko@gmail.com>" \
|
|
"Arnaud Quette <arnaud.quette@gmail.com>" \
|
|
"John Stamp <kinsayder@hotmail.com>" \
|
|
"Peter Selinger <selinger@users.sourceforge.net>" \
|
|
"Arjen de Korte <adkorte-guest@alioth.debian.org>",
|
|
DRV_BETA,
|
|
#ifdef QX_USB
|
|
{ &comm_upsdrv_info, NULL }
|
|
#else
|
|
{ NULL }
|
|
#endif /* QX_USB */
|
|
};
|
|
|
|
|
|
/* == Data walk modes == */
|
|
typedef enum {
|
|
QX_WALKMODE_INIT = 0,
|
|
QX_WALKMODE_QUICK_UPDATE,
|
|
QX_WALKMODE_FULL_UPDATE
|
|
} walkmode_t;
|
|
|
|
|
|
/* == Global vars == */
|
|
/* Pointer to the active subdriver object (changed in subdriver_matcher() function) */
|
|
static subdriver_t *subdriver = NULL;
|
|
|
|
static int pollfreq = DEFAULT_POLLFREQ;
|
|
static int ups_status = 0;
|
|
static bool_t data_has_changed = FALSE; /* for SEMI_STATIC data polling */
|
|
|
|
static time_t lastpoll; /* Timestamp the last polling */
|
|
|
|
#if defined(QX_USB) && defined(QX_SERIAL)
|
|
static int is_usb = 0; /* Whether the device is connected through USB (1) or serial (0) */
|
|
#endif /* QX_USB && QX_SERIAL */
|
|
|
|
static struct {
|
|
char command[SMALLBUF]; /* Command sent to the UPS to get answer/to execute an instant command */
|
|
char answer[SMALLBUF]; /* Answer from the UPS, filled at runtime */
|
|
} previous_item = { "", "" }; /* Hold the values of the item processed just before the actual one */
|
|
|
|
|
|
/* == Support functions == */
|
|
static int subdriver_matcher(void);
|
|
static int qx_command(const char *cmd, char *buf, size_t buflen);
|
|
static int qx_process_answer(item_t *item, const int len);
|
|
static bool_t qx_ups_walk(walkmode_t mode);
|
|
static void ups_status_set(void);
|
|
static void ups_alarm_set(void);
|
|
static void qx_set_var(item_t *item);
|
|
|
|
|
|
/* == Struct & data for status processing == */
|
|
typedef struct {
|
|
const char *status_str; /* UPS status string */
|
|
const int status_mask; /* UPS status mask */
|
|
} status_lkp_t;
|
|
|
|
static status_lkp_t status_info[] = {
|
|
/* Map status strings to bit masks */
|
|
{ "OL", STATUS(OL) },
|
|
{ "LB", STATUS(LB) },
|
|
{ "RB", STATUS(RB) },
|
|
{ "CHRG", STATUS(CHRG) },
|
|
{ "DISCHRG", STATUS(DISCHRG) },
|
|
{ "BYPASS", STATUS(BYPASS) },
|
|
{ "CAL", STATUS(CAL) },
|
|
{ "OFF", STATUS(OFF) },
|
|
{ "OVER", STATUS(OVER) },
|
|
{ "TRIM", STATUS(TRIM) },
|
|
{ "BOOST", STATUS(BOOST) },
|
|
{ "FSD", STATUS(FSD) },
|
|
{ NULL, 0 },
|
|
};
|
|
|
|
|
|
/* == battery.{charge,runtime} guesstimation == */
|
|
/* Support functions */
|
|
static int qx_battery(void);
|
|
static int qx_load(void);
|
|
static void qx_initbattery(void);
|
|
|
|
/* Battery data */
|
|
static struct {
|
|
double packs; /* Battery voltage multiplier */
|
|
struct {
|
|
double act; /* Actual runtime on battery */
|
|
double nom; /* Nominal runtime on battery (full load) */
|
|
double est; /* Estimated runtime remaining (full load) */
|
|
double exp; /* Load exponent */
|
|
} runt;
|
|
struct {
|
|
double act; /* Actual battery voltage */
|
|
double high; /* Battery float voltage */
|
|
double nom; /* Nominal battery voltage */
|
|
double low; /* Battery low voltage */
|
|
} volt;
|
|
struct {
|
|
double act; /* Actual battery charge */
|
|
long time; /* Recharge time from empty to full */
|
|
} chrg;
|
|
} batt = { 1, { -1, -1, 0, 0 }, { -1, -1, -1, -1 }, { -1, 43200 } };
|
|
|
|
/* Load data */
|
|
static struct {
|
|
double act; /* Actual load (reported by the UPS) */
|
|
double low; /* Idle load */
|
|
double eff; /* Effective load */
|
|
} load = { 0, 0.1, 1 };
|
|
|
|
static time_t battery_lastpoll = 0;
|
|
|
|
/* Fill batt.volt.act and guesstimate the battery charge if it isn't already available. */
|
|
static int qx_battery(void)
|
|
{
|
|
const char *val = dstate_getinfo("battery.voltage");
|
|
|
|
if (!val) {
|
|
upsdebugx(2, "%s: unable to get battery.voltage", __func__);
|
|
return -1;
|
|
}
|
|
|
|
batt.volt.act = batt.packs * strtod(val, NULL);
|
|
|
|
if (batt.chrg.act == -1 && batt.volt.low > 0 && batt.volt.high > batt.volt.low) {
|
|
|
|
batt.chrg.act = 100 * (batt.volt.act - batt.volt.low) / (batt.volt.high - batt.volt.low);
|
|
|
|
if (batt.chrg.act < 0) {
|
|
batt.chrg.act = 0;
|
|
}
|
|
|
|
if (batt.chrg.act > 100) {
|
|
batt.chrg.act = 100;
|
|
}
|
|
|
|
dstate_setinfo("battery.charge", "%.0f", batt.chrg.act);
|
|
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* Load for battery.{charge,runtime} from runtimecal */
|
|
static int qx_load(void)
|
|
{
|
|
const char *val = dstate_getinfo("ups.load");
|
|
|
|
if (!val) {
|
|
upsdebugx(2, "%s: unable to get ups.load", __func__);
|
|
return -1;
|
|
}
|
|
|
|
load.act = strtod(val, NULL);
|
|
|
|
load.eff = pow(load.act / 100, batt.runt.exp);
|
|
|
|
if (load.eff < load.low) {
|
|
load.eff = load.low;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* Guesstimation: init */
|
|
static void qx_initbattery(void)
|
|
{
|
|
if (!dstate_getinfo("battery.charge") || !dstate_getinfo("battery.runtime")) {
|
|
|
|
const char *val;
|
|
|
|
val = dstate_getinfo("battery.voltage.high");
|
|
if (val) {
|
|
batt.volt.high = strtod(val, NULL);
|
|
}
|
|
|
|
val = dstate_getinfo("battery.voltage.low");
|
|
if (val) {
|
|
batt.volt.low = strtod(val, NULL);
|
|
}
|
|
|
|
val = dstate_getinfo("battery.voltage.nominal");
|
|
if (val) {
|
|
batt.volt.nom = strtod(val, NULL);
|
|
}
|
|
|
|
/* If no values are available for both battery.voltage.{low,high} either from the UPS or provided by the user in ups.conf, try to guesstimate them, but announce it! */
|
|
if (batt.volt.nom != -1 && (batt.volt.low == -1 || batt.volt.high == -1)) {
|
|
|
|
upslogx(LOG_INFO, "No values for battery high/low voltages");
|
|
|
|
/* Basic formula, which should cover most cases */
|
|
batt.volt.low = 104 * batt.volt.nom / 120;
|
|
batt.volt.high = 130 * batt.volt.nom / 120;
|
|
|
|
/* Publish these data too */
|
|
dstate_setinfo("battery.voltage.low", "%.2f", batt.volt.low);
|
|
dstate_setinfo("battery.voltage.high", "%.2f", batt.volt.high);
|
|
|
|
upslogx(LOG_INFO, "Using 'guesstimation' (low: %f, high: %f)!", batt.volt.low, batt.volt.high);
|
|
|
|
}
|
|
|
|
val = dstate_getinfo("battery.packs");
|
|
if (val && (strspn(val, "0123456789 .") == strlen(val))) {
|
|
batt.packs = strtod(val, NULL);
|
|
} else {
|
|
|
|
/* qx_battery -> batt.volt.act */
|
|
if (!qx_battery() && batt.volt.nom != -1) {
|
|
|
|
const double packs[] = { 120, 100, 80, 60, 48, 36, 30, 24, 18, 12, 8, 6, 4, 3, 2, 1, 0.5, -1 };
|
|
int i;
|
|
|
|
/* The battery voltage will quickly return to at least the nominal value after discharging them.
|
|
* For overlapping battery.voltage.low/high ranges therefor choose the one with the highest multiplier. */
|
|
for (i = 0; packs[i] > 0; i++) {
|
|
|
|
if (packs[i] * batt.volt.act > 1.2 * batt.volt.nom) {
|
|
continue;
|
|
}
|
|
|
|
if (packs[i] * batt.volt.act < 0.8 * batt.volt.nom) {
|
|
upslogx(LOG_INFO, "Can't autodetect number of battery packs [%.0f/%.2f]", batt.volt.nom, batt.volt.act);
|
|
break;
|
|
}
|
|
|
|
batt.packs = packs[i];
|
|
break;
|
|
|
|
}
|
|
|
|
} else {
|
|
upslogx(LOG_INFO, "Can't autodetect number of battery packs [%.0f/%.2f]", batt.volt.nom, batt.volt.act);
|
|
}
|
|
|
|
}
|
|
|
|
/* Update batt.{chrg,volt}.act */
|
|
qx_battery();
|
|
|
|
val = getval("runtimecal");
|
|
if (val) {
|
|
|
|
double rh, lh, rl, ll;
|
|
|
|
time(&battery_lastpoll);
|
|
|
|
if (sscanf(val, "%lf,%lf,%lf,%lf", &rh, &lh, &rl, &ll) < 4) {
|
|
fatalx(EXIT_FAILURE, "Insufficient parameters for runtimecal");
|
|
}
|
|
|
|
if ((rl < rh) || (rh <= 0)) {
|
|
fatalx(EXIT_FAILURE, "Parameter out of range (runtime)");
|
|
}
|
|
|
|
if ((lh > 100) || (ll > lh) || (ll <= 0)) {
|
|
fatalx(EXIT_FAILURE, "Parameter out of range (load)");
|
|
}
|
|
|
|
batt.runt.exp = log(rl / rh) / log(lh / ll);
|
|
upsdebugx(2, "%s: battery runtime exponent: %.3f", __func__, batt.runt.exp);
|
|
|
|
batt.runt.nom = rh * pow(lh / 100, batt.runt.exp);
|
|
upsdebugx(2, "%s: battery runtime nominal: %.1f", __func__, batt.runt.nom);
|
|
|
|
} else {
|
|
|
|
upslogx(LOG_INFO, "Battery runtime will not be calculated (runtimecal not set)");
|
|
return;
|
|
|
|
}
|
|
|
|
val = dstate_getinfo("battery.charge");
|
|
if (!val && batt.volt.nom != -1) {
|
|
batt.volt.low = batt.volt.nom;
|
|
batt.volt.high = 1.15 * batt.volt.nom;
|
|
|
|
if (qx_battery())
|
|
fatalx(EXIT_FAILURE, "Initial battery charge undetermined");
|
|
|
|
val = dstate_getinfo("battery.charge");
|
|
}
|
|
|
|
if (val) {
|
|
batt.runt.est = batt.runt.nom * strtod(val, NULL) / 100;
|
|
upsdebugx(2, "%s: battery runtime estimate: %.1f", __func__, batt.runt.est);
|
|
} else {
|
|
fatalx(EXIT_FAILURE, "Initial battery charge undetermined");
|
|
}
|
|
|
|
val = getval("chargetime");
|
|
if (val) {
|
|
batt.chrg.time = strtol(val, NULL, 10);
|
|
|
|
if (batt.chrg.time <= 0) {
|
|
fatalx(EXIT_FAILURE, "Charge time out of range [1..s]");
|
|
}
|
|
|
|
upsdebugx(2, "%s: battery charge time: %ld", __func__, batt.chrg.time);
|
|
} else {
|
|
upslogx(LOG_INFO, "No charge time specified, using built in default [%ld seconds]", batt.chrg.time);
|
|
}
|
|
|
|
val = getval("idleload");
|
|
if (val) {
|
|
load.low = strtod(val, NULL) / 100;
|
|
|
|
if ((load.low <= 0) || (load.low > 1)) {
|
|
fatalx(EXIT_FAILURE, "Idle load out of range [0..100]");
|
|
}
|
|
|
|
upsdebugx(2, "%s: minimum load used (idle): %.3f", __func__, load.low);
|
|
} else {
|
|
upslogx(LOG_INFO, "No idle load specified, using built in default [%.1f %%]", 100 * load.low);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/* == USB communication subdrivers == */
|
|
#if defined(QX_USB) && !defined(TESTING)
|
|
static usb_communication_subdriver_t *usb = &usb_subdriver;
|
|
static usb_dev_handle *udev = NULL;
|
|
static USBDevice_t usbdevice;
|
|
static USBDeviceMatcher_t *reopen_matcher = NULL;
|
|
static USBDeviceMatcher_t *regex_matcher = NULL;
|
|
static int langid_fix = -1;
|
|
|
|
static int (*subdriver_command)(const char *cmd, char *buf, size_t buflen) = NULL;
|
|
|
|
/* Cypress communication subdriver */
|
|
static int cypress_command(const char *cmd, char *buf, size_t buflen)
|
|
{
|
|
char tmp[SMALLBUF];
|
|
int ret;
|
|
size_t i;
|
|
|
|
/* Send command */
|
|
memset(tmp, 0, sizeof(tmp));
|
|
snprintf(tmp, sizeof(tmp), "%s", cmd);
|
|
|
|
for (i = 0; i < strlen(tmp); i += ret) {
|
|
|
|
/* Write data in 8-byte chunks */
|
|
/* ret = usb->set_report(udev, 0, (unsigned char *)&tmp[i], 8); */
|
|
ret = usb_control_msg(udev, USB_ENDPOINT_OUT + USB_TYPE_CLASS + USB_RECIP_INTERFACE, 0x09, 0x200, 0, &tmp[i], 8, 5000);
|
|
|
|
if (ret <= 0) {
|
|
upsdebugx(3, "send: %s", ret ? usb_strerror() : "timeout");
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
upsdebugx(3, "send: %.*s", (int)strcspn(tmp, "\r"), tmp);
|
|
|
|
/* Read reply */
|
|
memset(buf, 0, buflen);
|
|
|
|
for (i = 0; (i <= buflen-8) && (strchr(buf, '\r') == NULL); i += ret) {
|
|
|
|
/* Read data in 8-byte chunks */
|
|
/* ret = usb->get_interrupt(udev, (unsigned char *)&buf[i], 8, 1000); */
|
|
ret = usb_interrupt_read(udev, 0x81, &buf[i], 8, 1000);
|
|
|
|
/* Any errors here mean that we are unable to read a reply (which will happen after successfully writing a command to the UPS) */
|
|
if (ret <= 0) {
|
|
upsdebugx(3, "read: %s", ret ? usb_strerror() : "timeout");
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
upsdebugx(3, "read: %.*s", (int)strcspn(buf, "\r"), buf);
|
|
return i;
|
|
}
|
|
|
|
/* Phoenix communication subdriver */
|
|
static int phoenix_command(const char *cmd, char *buf, size_t buflen)
|
|
{
|
|
char tmp[SMALLBUF];
|
|
int ret;
|
|
size_t i;
|
|
|
|
for (i = 0; i < 8; i++) {
|
|
|
|
/* Read data in 8-byte chunks */
|
|
/* ret = usb->get_interrupt(udev, (unsigned char *)tmp, 8, 1000); */
|
|
ret = usb_interrupt_read(udev, 0x81, tmp, 8, 1000);
|
|
|
|
/* This USB to serial implementation is crappy.
|
|
* In order to read correct replies we need to flush the output buffers of the converter until we get no more data (ie, it times out). */
|
|
switch (ret)
|
|
{
|
|
case -EPIPE: /* Broken pipe */
|
|
usb_clear_halt(udev, 0x81);
|
|
case -ETIMEDOUT: /* Connection timed out */
|
|
break;
|
|
}
|
|
|
|
if (ret < 0) {
|
|
upsdebugx(3, "flush: %s", usb_strerror());
|
|
break;
|
|
}
|
|
|
|
upsdebug_hex(4, "dump", tmp, ret);
|
|
}
|
|
|
|
/* Send command */
|
|
memset(tmp, 0, sizeof(tmp));
|
|
snprintf(tmp, sizeof(tmp), "%s", cmd);
|
|
|
|
for (i = 0; i < strlen(tmp); i += ret) {
|
|
|
|
/* Write data in 8-byte chunks */
|
|
/* ret = usb->set_report(udev, 0, (unsigned char *)&tmp[i], 8); */
|
|
ret = usb_control_msg(udev, USB_ENDPOINT_OUT + USB_TYPE_CLASS + USB_RECIP_INTERFACE, 0x09, 0x200, 0, &tmp[i], 8, 1000);
|
|
|
|
if (ret <= 0) {
|
|
upsdebugx(3, "send: %s", ret ? usb_strerror() : "timeout");
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
upsdebugx(3, "send: %.*s", (int)strcspn(tmp, "\r"), tmp);
|
|
|
|
/* Read reply */
|
|
memset(buf, 0, buflen);
|
|
|
|
for (i = 0; (i <= buflen-8) && (strchr(buf, '\r') == NULL); i += ret) {
|
|
|
|
/* Read data in 8-byte chunks */
|
|
/* ret = usb->get_interrupt(udev, (unsigned char *)&buf[i], 8, 1000); */
|
|
ret = usb_interrupt_read(udev, 0x81, &buf[i], 8, 1000);
|
|
|
|
/* Any errors here mean that we are unable to read a reply (which will happen after successfully writing a command to the UPS) */
|
|
if (ret <= 0) {
|
|
upsdebugx(3, "read: %s", ret ? usb_strerror() : "timeout");
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
upsdebugx(3, "read: %.*s", (int)strcspn(buf, "\r"), buf);
|
|
return i;
|
|
}
|
|
|
|
/* Ippon communication subdriver */
|
|
static int ippon_command(const char *cmd, char *buf, size_t buflen)
|
|
{
|
|
char tmp[64];
|
|
int ret, len;
|
|
size_t i;
|
|
|
|
/* Send command */
|
|
snprintf(tmp, sizeof(tmp), "%s", cmd);
|
|
|
|
for (i = 0; i < strlen(tmp); i += ret) {
|
|
|
|
/* Write data in 8-byte chunks */
|
|
ret = usb_control_msg(udev, USB_ENDPOINT_OUT + USB_TYPE_CLASS + USB_RECIP_INTERFACE, 0x09, 0x2, 0, &tmp[i], 8, 1000);
|
|
|
|
if (ret <= 0) {
|
|
upsdebugx(3, "send: %s", (ret != -ETIMEDOUT) ? usb_strerror() : "Connection timed out");
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
upsdebugx(3, "send: %.*s", (int)strcspn(tmp, "\r"), tmp);
|
|
|
|
/* Read all 64 bytes of the reply in one large chunk */
|
|
ret = usb_interrupt_read(udev, 0x81, tmp, sizeof(tmp), 1000);
|
|
|
|
/* Any errors here mean that we are unable to read a reply (which will happen after successfully writing a command to the UPS) */
|
|
if (ret <= 0) {
|
|
upsdebugx(3, "read: %s", (ret != -ETIMEDOUT) ? usb_strerror() : "Connection timed out");
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* As Ippon will always return 64 bytes in response, we have to
|
|
* calculate and return length of actual response data here.
|
|
* Empty response will look like 0x00 0x0D, otherwise it will be
|
|
* data string terminated by 0x0D.
|
|
*/
|
|
len = (int)strcspn(tmp, "\r");
|
|
upsdebugx(3, "read: %.*s", len, tmp);
|
|
if (len > 0) {
|
|
len ++;
|
|
}
|
|
snprintf(buf, buflen, "%.*s", len, tmp);
|
|
return len;
|
|
}
|
|
|
|
/* Krauler communication subdriver */
|
|
static int krauler_command(const char *cmd, char *buf, size_t buflen)
|
|
{
|
|
/* Still not implemented:
|
|
* 0x6 T<n> (don't know how to pass the parameter)
|
|
* 0x68 and 0x69 both cause shutdown after an undefined interval */
|
|
const struct {
|
|
const char *str; /* Megatec command */
|
|
const int index; /* Krauler string index for this command */
|
|
const char prefix; /* Character to replace the first byte in reply */
|
|
} command[] = {
|
|
{ "Q1\r", 0x03, '(' },
|
|
{ "F\r", 0x0d, '#' },
|
|
{ "I\r", 0x0c, '#' },
|
|
{ "T\r", 0x04, '\r' },
|
|
{ "TL\r", 0x05, '\r' },
|
|
{ "Q\r", 0x07, '\r' },
|
|
{ "C\r", 0x0b, '\r' },
|
|
{ "CT\r", 0x0b, '\r' },
|
|
{ NULL }
|
|
};
|
|
|
|
int i;
|
|
|
|
upsdebugx(3, "send: %.*s", (int)strcspn(cmd, "\r"), cmd);
|
|
|
|
for (i = 0; command[i].str; i++) {
|
|
|
|
int retry;
|
|
|
|
if (strcmp(cmd, command[i].str)) {
|
|
continue;
|
|
}
|
|
|
|
for (retry = 0; retry < 10; retry++) {
|
|
|
|
int ret;
|
|
|
|
if (langid_fix != -1) {
|
|
/* Apply langid_fix value */
|
|
ret = usb_get_string(udev, command[i].index, langid_fix, buf, buflen);
|
|
}
|
|
else {
|
|
ret = usb_get_string_simple(udev, command[i].index, buf, buflen);
|
|
}
|
|
|
|
if (ret <= 0) {
|
|
upsdebugx(3, "read: %s", ret ? usb_strerror() : "timeout");
|
|
return ret;
|
|
}
|
|
|
|
/* This may serve in the future */
|
|
upsdebugx(1, "received %d (%d)", ret, buf[0]);
|
|
|
|
if (langid_fix != -1) {
|
|
/* Limit this check, at least for now */
|
|
/* Invalid receive size - message corrupted */
|
|
if (ret != buf[0]) {
|
|
upsdebugx(1, "size mismatch: %d / %d", ret, buf[0]);
|
|
continue;
|
|
}
|
|
|
|
/* Simple unicode -> ASCII inplace conversion
|
|
* FIXME: this code is at least shared with mge-shut/libshut
|
|
* Create a common function? */
|
|
unsigned int di, si, size = buf[0];
|
|
for (di = 0, si = 2; si < size; si += 2) {
|
|
|
|
if (di >= (buflen - 1))
|
|
break;
|
|
|
|
if (buf[si + 1]) /* high byte */
|
|
buf[di++] = '?';
|
|
else
|
|
buf[di++] = buf[si];
|
|
|
|
}
|
|
|
|
buf[di] = 0;
|
|
ret = di;
|
|
}
|
|
|
|
/* "UPS No Ack" has a special meaning */
|
|
if (!strcasecmp(buf, "UPS No Ack")) {
|
|
upsdebugx(3, "read: %.*s", (int)strcspn(buf, "\r"), buf);
|
|
continue;
|
|
}
|
|
|
|
/* Replace the first byte of what we received with the correct one */
|
|
buf[0] = command[i].prefix;
|
|
|
|
upsdebugx(3, "read: %.*s", (int)strcspn(buf, "\r"), buf);
|
|
return ret;
|
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
/* Echo the unknown command back */
|
|
upsdebugx(3, "read: %.*s", (int)strcspn(cmd, "\r"), cmd);
|
|
return snprintf(buf, buflen, "%s", cmd);
|
|
}
|
|
|
|
static void *cypress_subdriver(USBDevice_t *device)
|
|
{
|
|
subdriver_command = &cypress_command;
|
|
return NULL;
|
|
}
|
|
|
|
static void *ippon_subdriver(USBDevice_t *device)
|
|
{
|
|
subdriver_command = &ippon_command;
|
|
return NULL;
|
|
}
|
|
|
|
static void *krauler_subdriver(USBDevice_t *device)
|
|
{
|
|
subdriver_command = &krauler_command;
|
|
return NULL;
|
|
}
|
|
|
|
static void *phoenix_subdriver(USBDevice_t *device)
|
|
{
|
|
subdriver_command = &phoenix_command;
|
|
return NULL;
|
|
}
|
|
|
|
/* USB VendorID/ProductID match - note: rightmost comment is used for naming rules by tools/nut-usbinfo.pl */
|
|
static usb_device_id_t qx_usb_id[] = {
|
|
{ USB_DEVICE(0x05b8, 0x0000), &cypress_subdriver }, /* Agiler UPS */
|
|
{ USB_DEVICE(0x0001, 0x0000), &krauler_subdriver }, /* Krauler UP-M500VA */
|
|
{ USB_DEVICE(0xffff, 0x0000), &krauler_subdriver }, /* Ablerex 625L USB */
|
|
{ USB_DEVICE(0x0665, 0x5161), &cypress_subdriver }, /* Belkin F6C1200-UNV/Voltronic Power UPSes */
|
|
{ USB_DEVICE(0x06da, 0x0002), &cypress_subdriver }, /* Online Yunto YQ450 */
|
|
{ USB_DEVICE(0x06da, 0x0003), &ippon_subdriver }, /* Mustek Powermust */
|
|
{ USB_DEVICE(0x06da, 0x0004), &cypress_subdriver }, /* Phoenixtec Innova 3/1 T */
|
|
{ USB_DEVICE(0x06da, 0x0005), &cypress_subdriver }, /* Phoenixtec Innova RT */
|
|
{ USB_DEVICE(0x06da, 0x0201), &cypress_subdriver }, /* Phoenixtec Innova T */
|
|
{ USB_DEVICE(0x06da, 0x0601), &phoenix_subdriver }, /* Online Zinto A */
|
|
{ USB_DEVICE(0x0f03, 0x0001), &cypress_subdriver }, /* Unitek Alpha 1200Sx */
|
|
{ USB_DEVICE(0x14f0, 0x00c9), &phoenix_subdriver }, /* GE EP series */
|
|
/* End of list */
|
|
{ -1, -1, NULL }
|
|
};
|
|
|
|
static int device_match_func(USBDevice_t *hd, void *privdata)
|
|
{
|
|
if (subdriver_command) {
|
|
return 1;
|
|
}
|
|
|
|
switch (is_usb_device_supported(qx_usb_id, hd))
|
|
{
|
|
case SUPPORTED:
|
|
return 1;
|
|
|
|
case POSSIBLY_SUPPORTED:
|
|
case NOT_SUPPORTED:
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
static USBDeviceMatcher_t device_matcher = {
|
|
&device_match_func,
|
|
NULL,
|
|
NULL
|
|
};
|
|
#endif /* QX_USB && !TESTING */
|
|
|
|
|
|
/* == Driver functions implementations == */
|
|
|
|
/* Process instant command and take action. */
|
|
int instcmd(const char *cmdname, const char *extradata)
|
|
{
|
|
item_t *item;
|
|
char value[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 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 instcmd("beeper.enable", NULL);
|
|
}
|
|
|
|
upslogx(LOG_INFO, "%s(%s, %s)", __func__, cmdname, extradata ? extradata : "[NULL]");
|
|
|
|
/* Retrieve item by command name */
|
|
item = find_nut_info(cmdname, QX_FLAG_CMD, QX_FLAG_SKIP);
|
|
|
|
/* Check for fallback if not found */
|
|
if (item == NULL) {
|
|
|
|
if (!strcasecmp(cmdname, "load.on")) {
|
|
return instcmd("load.on.delay", "0");
|
|
}
|
|
|
|
if (!strcasecmp(cmdname, "load.off")) {
|
|
return instcmd("load.off.delay", "0");
|
|
}
|
|
|
|
if (!strcasecmp(cmdname, "shutdown.return")) {
|
|
|
|
int ret;
|
|
|
|
/* Ensure "ups.start.auto" is set to "yes", if supported */
|
|
if (dstate_getinfo("ups.start.auto")) {
|
|
if (setvar("ups.start.auto", "yes") != STAT_SET_HANDLED) {
|
|
upslogx(LOG_ERR, "%s: FAILED", __func__);
|
|
return STAT_INSTCMD_FAILED;
|
|
}
|
|
}
|
|
|
|
ret = instcmd("load.on.delay", dstate_getinfo("ups.delay.start"));
|
|
if (ret != STAT_INSTCMD_HANDLED) {
|
|
return ret;
|
|
}
|
|
|
|
return instcmd("load.off.delay", dstate_getinfo("ups.delay.shutdown"));
|
|
|
|
}
|
|
|
|
if (!strcasecmp(cmdname, "shutdown.stayoff")) {
|
|
|
|
int ret;
|
|
|
|
/* Ensure "ups.start.auto" is set to "no", if supported */
|
|
if (dstate_getinfo("ups.start.auto")) {
|
|
if (setvar("ups.start.auto", "no") != STAT_SET_HANDLED) {
|
|
upslogx(LOG_ERR, "%s: FAILED", __func__);
|
|
return STAT_INSTCMD_FAILED;
|
|
}
|
|
}
|
|
|
|
ret = instcmd("load.on.delay", "-1");
|
|
if (ret != STAT_INSTCMD_HANDLED) {
|
|
return ret;
|
|
}
|
|
|
|
return instcmd("load.off.delay", dstate_getinfo("ups.delay.shutdown"));
|
|
|
|
}
|
|
|
|
upsdebugx(2, "%s: command %s unavailable", __func__, cmdname);
|
|
return STAT_INSTCMD_INVALID;
|
|
}
|
|
|
|
/* If extradata is empty, use the default value from the blazer to NUT table */
|
|
extradata = extradata ? extradata : item->dfl;
|
|
snprintf(value, sizeof(value), "%s", extradata ? extradata : "");
|
|
|
|
/* Preprocess command */
|
|
if (item->preprocess != NULL && item->preprocess(item, value, sizeof(value))) {
|
|
/* Something went wrong */
|
|
upslogx(LOG_ERR, "%s: FAILED", __func__);
|
|
return STAT_INSTCMD_FAILED;
|
|
}
|
|
|
|
/* No preprocess function -> nothing to do with extradata */
|
|
if (item->preprocess == NULL)
|
|
snprintf(value, sizeof(value), "%s", "");
|
|
|
|
/* Send the command, get the reply */
|
|
if (qx_process(item, strlen(value) > 0 ? value : NULL)) {
|
|
/* Something went wrong */
|
|
upslogx(LOG_ERR, "%s: FAILED", __func__);
|
|
return STAT_INSTCMD_FAILED;
|
|
}
|
|
|
|
/* We got a reply from the UPS: either subdriver->accepted (-> command handled) or the command itself echoed back (-> command failed) */
|
|
if (strlen(item->value) > 0) {
|
|
|
|
if (subdriver->accepted != NULL && !strcasecmp(item->value, subdriver->accepted)) {
|
|
upslogx(LOG_INFO, "%s: SUCCEED", __func__);
|
|
/* Set the status so that SEMI_STATIC vars are polled */
|
|
data_has_changed = TRUE;
|
|
return STAT_INSTCMD_HANDLED;
|
|
}
|
|
|
|
upslogx(LOG_ERR, "%s: FAILED", __func__);
|
|
return STAT_INSTCMD_FAILED;
|
|
|
|
}
|
|
|
|
/* No reply from the UPS -> command handled */
|
|
upslogx(LOG_INFO, "%s: SUCCEED", __func__);
|
|
/* Set the status so that SEMI_STATIC vars are polled */
|
|
data_has_changed = TRUE;
|
|
return STAT_INSTCMD_HANDLED;
|
|
}
|
|
|
|
/* Set r/w variable to a value. */
|
|
int setvar(const char *varname, const char *val)
|
|
{
|
|
item_t *item;
|
|
char value[SMALLBUF];
|
|
st_tree_t *root = (st_tree_t *)dstate_getroot();
|
|
int ok = 0;
|
|
|
|
/* Retrieve variable */
|
|
item = find_nut_info(varname, QX_FLAG_SETVAR, QX_FLAG_SKIP);
|
|
|
|
if (item == NULL) {
|
|
upsdebugx(2, "%s: element %s unavailable", __func__, varname);
|
|
return STAT_SET_UNKNOWN;
|
|
}
|
|
|
|
/* No NUT variable is available for this item, so we're handling a one-time setvar from ups.conf */
|
|
if (item->qxflags & QX_FLAG_NONUT) {
|
|
|
|
const char *userval;
|
|
|
|
/* Nothing to do */
|
|
if (!testvar(item->info_type)) {
|
|
upsdebugx(2, "%s: nothing to do.. [%s]", __func__, item->info_type);
|
|
return STAT_SET_HANDLED;
|
|
}
|
|
|
|
userval = getval(item->info_type);
|
|
|
|
upslogx(LOG_INFO, "%s(%s, %s)", __func__, varname, userval ? userval : "[NULL]");
|
|
|
|
snprintf(value, sizeof(value), "%s", userval ? userval : "");
|
|
|
|
/* This item is available in NUT */
|
|
} else {
|
|
|
|
upslogx(LOG_INFO, "%s(%s, %s)", __func__, varname, strlen(val) ? val : "[NULL]");
|
|
|
|
if (!strlen(val)) {
|
|
upslogx(LOG_ERR, "%s: value not given for %s", __func__, item->info_type);
|
|
return STAT_SET_UNKNOWN; /* TODO: HANDLED but FAILED, not UNKNOWN! */
|
|
}
|
|
|
|
snprintf(value, sizeof(value), "%s", val);
|
|
|
|
/* Nothing to do */
|
|
if (!strcasecmp(dstate_getinfo(item->info_type), value)) {
|
|
upslogx(LOG_INFO, "%s: nothing to do.. [%s]", __func__, item->info_type);
|
|
return STAT_SET_HANDLED;
|
|
}
|
|
|
|
}
|
|
|
|
/* Check if given value is in the range of accepted values (range) */
|
|
if (item->qxflags & QX_FLAG_RANGE) {
|
|
|
|
int valuetoset, min, max;
|
|
|
|
if (strspn(value, "0123456789 .") != strlen(value)) {
|
|
upslogx(LOG_ERR, "%s: non numerical value [%s: %s]", __func__, item->info_type, value);
|
|
return STAT_SET_UNKNOWN; /* TODO: HANDLED but FAILED, not UNKNOWN! */
|
|
}
|
|
|
|
valuetoset = strtol(value, NULL, 10);
|
|
|
|
/* No NUT var is available for this item, so take its range from qx2nut table */
|
|
if (item->qxflags & QX_FLAG_NONUT) {
|
|
|
|
info_rw_t *rvalue;
|
|
|
|
if (!strlen(value)) {
|
|
upslogx(LOG_ERR, "%s: value not given for %s", __func__, item->info_type);
|
|
return STAT_SET_UNKNOWN; /* TODO: HANDLED but FAILED, not UNKNOWN! */
|
|
}
|
|
|
|
min = max = -1;
|
|
|
|
/* Loop on all existing values */
|
|
for (rvalue = item->info_rw; rvalue != NULL && strlen(rvalue->value) > 0; rvalue++) {
|
|
|
|
if (rvalue->preprocess && rvalue->preprocess(rvalue->value, sizeof(rvalue->value)))
|
|
continue;
|
|
|
|
if (min < 0) {
|
|
min = strtol(rvalue->value, NULL, 10);
|
|
continue;
|
|
}
|
|
|
|
max = strtol(rvalue->value, NULL, 10);
|
|
|
|
/* valuetoset is in the range */
|
|
if (min <= valuetoset && valuetoset <= max) {
|
|
ok = 1;
|
|
break;
|
|
}
|
|
|
|
min = -1;
|
|
max = -1;
|
|
|
|
}
|
|
|
|
/* We have a NUT var for this item, so check given value against the already set range */
|
|
} else {
|
|
|
|
const range_t *range = state_getrangelist(root, item->info_type);
|
|
|
|
/* Unable to find tree node for var */
|
|
if (!range) {
|
|
upsdebugx(2, "%s: unable to find tree node for %s", __func__, item->info_type);
|
|
return STAT_SET_UNKNOWN;
|
|
}
|
|
|
|
while (range) {
|
|
|
|
min = range->min;
|
|
max = range->max;
|
|
|
|
/* valuetoset is in the range */
|
|
if (min <= valuetoset && valuetoset <= max) {
|
|
ok = 1;
|
|
break;
|
|
}
|
|
range = range->next;
|
|
}
|
|
|
|
}
|
|
|
|
if (!ok) {
|
|
upslogx(LOG_ERR, "%s: value out of range [%s: %s]", __func__, item->info_type, value);
|
|
return STAT_SET_UNKNOWN; /* TODO: HANDLED but FAILED, not UNKNOWN! */
|
|
}
|
|
|
|
/* Check if given value is in the range of accepted values (enum) */
|
|
} else if (item->qxflags & QX_FLAG_ENUM) {
|
|
|
|
/* No NUT var is available for this item, so take its range from qx2nut table */
|
|
if (item->qxflags & QX_FLAG_NONUT) {
|
|
|
|
info_rw_t *envalue;
|
|
|
|
if (!strlen(value)) {
|
|
upslogx(LOG_ERR, "%s: value not given for %s", __func__, item->info_type);
|
|
return STAT_SET_UNKNOWN; /* TODO: HANDLED but FAILED, not UNKNOWN! */
|
|
}
|
|
|
|
/* Loop on all existing values */
|
|
for (envalue = item->info_rw; envalue != NULL && strlen(envalue->value) > 0; envalue++) {
|
|
|
|
if (envalue->preprocess && envalue->preprocess(envalue->value, sizeof(envalue->value)))
|
|
continue;
|
|
|
|
if (strcasecmp(envalue->value, value))
|
|
continue;
|
|
|
|
/* value found */
|
|
ok = 1;
|
|
break;
|
|
|
|
}
|
|
|
|
/* We have a NUT var for this item, so check given value against the already set range */
|
|
} else {
|
|
|
|
const enum_t *enumlist = state_getenumlist(root, item->info_type);
|
|
|
|
/* Unable to find tree node for var */
|
|
if (!enumlist) {
|
|
upsdebugx(2, "%s: unable to find tree node for %s", __func__, item->info_type);
|
|
return STAT_SET_UNKNOWN;
|
|
}
|
|
|
|
while (enumlist) {
|
|
|
|
/* If this is not the right value, go on to the next */
|
|
if (strcasecmp(enumlist->val, value)) {
|
|
enumlist = enumlist->next;
|
|
continue;
|
|
}
|
|
|
|
/* value found in enumlist */
|
|
ok = 1;
|
|
break;
|
|
}
|
|
|
|
}
|
|
|
|
if (!ok) {
|
|
upslogx(LOG_ERR, "%s: value out of range [%s: %s]", __func__, item->info_type, value);
|
|
return STAT_SET_UNKNOWN; /* TODO: HANDLED but FAILED, not UNKNOWN! */
|
|
}
|
|
|
|
/* Check if given value is not too long (string) */
|
|
} else if (item->info_flags & ST_FLAG_STRING) {
|
|
|
|
const int aux = state_getaux(root, item->info_type);
|
|
|
|
/* Unable to find tree node for var */
|
|
if (aux < 0) {
|
|
upsdebugx(2, "%s: unable to find tree node for %s", __func__, item->info_type);
|
|
return STAT_SET_UNKNOWN;
|
|
}
|
|
|
|
if (aux < (int)strlen(value)) {
|
|
upslogx(LOG_ERR, "%s: value is too long [%s: %s]", __func__, item->info_type, value);
|
|
return STAT_SET_UNKNOWN; /* TODO: HANDLED but FAILED, not UNKNOWN! */
|
|
}
|
|
|
|
}
|
|
|
|
/* Preprocess value: from NUT-compliant to UPS-compliant */
|
|
if (item->preprocess != NULL && item->preprocess(item, value, sizeof(value))) {
|
|
/* Something went wrong */
|
|
upslogx(LOG_ERR, "%s: FAILED", __func__);
|
|
return STAT_SET_UNKNOWN; /* TODO: HANDLED but FAILED, not UNKNOWN! */
|
|
}
|
|
|
|
/* Handle server side variable */
|
|
if (item->qxflags & QX_FLAG_ABSENT) {
|
|
upsdebugx(2, "%s: setting server side variable %s", __func__, item->info_type);
|
|
dstate_setinfo(item->info_type, "%s", value);
|
|
upslogx(LOG_INFO, "%s: SUCCEED", __func__);
|
|
return STAT_SET_HANDLED;
|
|
}
|
|
|
|
/* No preprocess function -> nothing to do with val */
|
|
if (item->preprocess == NULL)
|
|
snprintf(value, sizeof(value), "%s", "");
|
|
|
|
/* Actual variable setting */
|
|
if (qx_process(item, strlen(value) > 0 ? value : NULL)) {
|
|
/* Something went wrong */
|
|
upslogx(LOG_ERR, "%s: FAILED", __func__);
|
|
return STAT_SET_UNKNOWN; /* TODO: HANDLED but FAILED, not UNKNOWN! */
|
|
}
|
|
|
|
/* We got a reply from the UPS: either subdriver->accepted (-> command handled) or the command itself echoed back (-> command failed) */
|
|
if (strlen(item->value) > 0) {
|
|
|
|
if (subdriver->accepted != NULL && !strcasecmp(item->value, subdriver->accepted)) {
|
|
upslogx(LOG_INFO, "%s: SUCCEED", __func__);
|
|
/* Set the status so that SEMI_STATIC vars are polled */
|
|
data_has_changed = TRUE;
|
|
return STAT_SET_HANDLED;
|
|
}
|
|
|
|
upslogx(LOG_ERR, "%s: FAILED", __func__);
|
|
return STAT_SET_UNKNOWN; /* TODO: HANDLED but FAILED, not UNKNOWN! */
|
|
|
|
}
|
|
|
|
/* No reply from the UPS -> command handled */
|
|
upslogx(LOG_INFO, "%s: SUCCEED", __func__);
|
|
/* Set the status so that SEMI_STATIC vars are polled */
|
|
data_has_changed = TRUE;
|
|
return STAT_SET_HANDLED;
|
|
}
|
|
|
|
/* Try to shutdown the UPS */
|
|
void upsdrv_shutdown(void)
|
|
{
|
|
int retry;
|
|
item_t *item;
|
|
const char *val;
|
|
|
|
upsdebugx(1, "%s...", __func__);
|
|
|
|
/* Get user-defined delays */
|
|
|
|
/* Start delay */
|
|
item = find_nut_info("ups.delay.start", 0, QX_FLAG_SKIP);
|
|
|
|
/* Don't know what happened */
|
|
if (!item)
|
|
fatalx(EXIT_FAILURE, "Unable to set start delay");
|
|
|
|
/* Set the default value */
|
|
dstate_setinfo(item->info_type, "%s", item->dfl);
|
|
|
|
/* Set var flags/range/enum */
|
|
qx_set_var(item);
|
|
|
|
/* Retrieve user defined delay settings */
|
|
val = getval(QX_VAR_ONDELAY);
|
|
|
|
if (val && setvar(item->info_type, val) != STAT_SET_HANDLED) {
|
|
fatalx(EXIT_FAILURE, "Start delay '%s' out of range", val);
|
|
}
|
|
|
|
/* Shutdown delay */
|
|
item = find_nut_info("ups.delay.shutdown", 0, QX_FLAG_SKIP);
|
|
|
|
/* Don't know what happened */
|
|
if (!item)
|
|
fatalx(EXIT_FAILURE, "Unable to set shutdown delay");
|
|
|
|
/* Set the default value */
|
|
dstate_setinfo(item->info_type, "%s", item->dfl);
|
|
|
|
/* Set var flags/range/enum */
|
|
qx_set_var(item);
|
|
|
|
/* Retrieve user defined delay settings */
|
|
val = getval(QX_VAR_OFFDELAY);
|
|
|
|
if (val && setvar(item->info_type, val) != STAT_SET_HANDLED) {
|
|
fatalx(EXIT_FAILURE, "Shutdown delay '%s' out of range", val);
|
|
}
|
|
|
|
/* Stop pending shutdowns */
|
|
if (find_nut_info("shutdown.stop", QX_FLAG_CMD, QX_FLAG_SKIP)) {
|
|
|
|
for (retry = 1; retry <= MAXTRIES; retry++) {
|
|
|
|
if (instcmd("shutdown.stop", NULL) != STAT_INSTCMD_HANDLED) {
|
|
continue;
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
if (retry > MAXTRIES) {
|
|
upslogx(LOG_NOTICE, "No shutdown pending");
|
|
}
|
|
|
|
}
|
|
|
|
/* Shutdown */
|
|
for (retry = 1; retry <= MAXTRIES; retry++) {
|
|
|
|
if (testvar("stayoff")) {
|
|
|
|
if (instcmd("shutdown.stayoff", NULL) != STAT_INSTCMD_HANDLED) {
|
|
continue;
|
|
}
|
|
|
|
} else {
|
|
|
|
if (instcmd("shutdown.return", NULL) != STAT_INSTCMD_HANDLED) {
|
|
continue;
|
|
}
|
|
|
|
}
|
|
|
|
fatalx(EXIT_SUCCESS, "Shutting down in %s seconds", dstate_getinfo("ups.delay.shutdown"));
|
|
}
|
|
|
|
fatalx(EXIT_FAILURE, "Shutdown failed!");
|
|
}
|
|
|
|
void upsdrv_help(void)
|
|
{
|
|
printf("Read The Fine Manual ('man 8 nutdrv_qx')\n");
|
|
}
|
|
|
|
/* Adding flags/vars */
|
|
void upsdrv_makevartable(void)
|
|
{
|
|
char temp[SMALLBUF];
|
|
int i;
|
|
|
|
upsdebugx(1, "%s...", __func__);
|
|
|
|
snprintf(temp, sizeof(temp), "Set shutdown delay, in seconds (default=%s)", DEFAULT_OFFDELAY);
|
|
addvar(VAR_VALUE, QX_VAR_OFFDELAY, temp);
|
|
|
|
snprintf(temp, sizeof(temp), "Set startup delay, in seconds (default=%s)", DEFAULT_ONDELAY);
|
|
addvar(VAR_VALUE, QX_VAR_ONDELAY, temp);
|
|
|
|
addvar(VAR_FLAG, "stayoff", "If invoked the UPS won't return after a shutdown when FSD arises");
|
|
|
|
snprintf(temp, sizeof(temp), "Set polling frequency, in seconds, to reduce data flow (default=%d)", DEFAULT_POLLFREQ);
|
|
addvar(VAR_VALUE, QX_VAR_POLLFREQ, temp);
|
|
|
|
addvar(VAR_VALUE, "protocol", "Preselect communication protocol (skip autodetection)");
|
|
|
|
/* battery.{charge,runtime} guesstimation */
|
|
addvar(VAR_VALUE, "runtimecal", "Parameters used for runtime calculation");
|
|
addvar(VAR_VALUE, "chargetime", "Nominal charge time for UPS battery");
|
|
addvar(VAR_VALUE, "idleload", "Minimum load to be used for runtime calculation");
|
|
|
|
#ifdef QX_USB
|
|
addvar(VAR_VALUE, "subdriver", "Serial-over-USB subdriver selection");
|
|
addvar(VAR_VALUE, "vendorid", "Regular expression to match UPS Manufacturer numerical ID (4 digits hexadecimal)");
|
|
addvar(VAR_VALUE, "productid", "Regular expression to match UPS Product numerical ID (4 digits hexadecimal)");
|
|
|
|
addvar(VAR_VALUE, "vendor", "Regular expression to match UPS Manufacturer string");
|
|
addvar(VAR_VALUE, "product", "Regular expression to match UPS Product string");
|
|
addvar(VAR_VALUE, "serial", "Regular expression to match UPS Serial number");
|
|
|
|
addvar(VAR_VALUE, "bus", "Regular expression to match USB bus name");
|
|
|
|
addvar(VAR_VALUE, "langid_fix", "Apply the language ID workaround to the krauler subdriver (0x409 or 0x4095)");
|
|
#endif /* QX_USB */
|
|
|
|
#ifdef QX_SERIAL
|
|
addvar(VAR_VALUE, "cablepower", "Set cable power for serial interface");
|
|
#endif /* QX_SERIAL */
|
|
|
|
/* Subdrivers flags/vars */
|
|
for (i = 0; subdriver_list[i] != NULL; i++) {
|
|
|
|
if (subdriver_list[i]->makevartable != NULL)
|
|
subdriver_list[i]->makevartable();
|
|
|
|
}
|
|
}
|
|
|
|
/* Update UPS status/infos */
|
|
void upsdrv_updateinfo(void)
|
|
{
|
|
time_t now;
|
|
static int retry = 0;
|
|
|
|
upsdebugx(1, "%s...", __func__);
|
|
|
|
time(&now);
|
|
|
|
/* Clear status buffer before beginning */
|
|
status_init();
|
|
|
|
/* Do a full update (polling) every pollfreq or upon data change (i.e. setvar/instcmd) */
|
|
if ((now > (lastpoll + pollfreq)) || (data_has_changed == TRUE)) {
|
|
|
|
upsdebugx(1, "Full update...");
|
|
|
|
/* Clear ups_status */
|
|
ups_status = 0;
|
|
|
|
alarm_init();
|
|
|
|
if (qx_ups_walk(QX_WALKMODE_FULL_UPDATE) == FALSE) {
|
|
|
|
if (retry < MAXTRIES || retry == MAXTRIES) {
|
|
upsdebugx(1, "Communications with the UPS lost: status read failed!");
|
|
retry++;
|
|
} else {
|
|
dstate_datastale();
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
lastpoll = now;
|
|
data_has_changed = FALSE;
|
|
|
|
ups_alarm_set();
|
|
alarm_commit();
|
|
|
|
} else {
|
|
|
|
upsdebugx(1, "Quick update...");
|
|
|
|
/* Quick poll data only to see if the UPS is still connected */
|
|
if (qx_ups_walk(QX_WALKMODE_QUICK_UPDATE) == FALSE) {
|
|
|
|
if (retry < MAXTRIES || retry == MAXTRIES) {
|
|
upsdebugx(1, "Communications with the UPS lost: status read failed!");
|
|
retry++;
|
|
} else {
|
|
dstate_datastale();
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
}
|
|
|
|
ups_status_set();
|
|
status_commit();
|
|
|
|
if (retry > MAXTRIES) {
|
|
upslogx(LOG_NOTICE, "Communications with the UPS re-established");
|
|
}
|
|
|
|
retry = 0;
|
|
|
|
dstate_dataok();
|
|
}
|
|
|
|
/* Initialise data from UPS */
|
|
void upsdrv_initinfo(void)
|
|
{
|
|
char *val;
|
|
|
|
upsdebugx(1, "%s...", __func__);
|
|
|
|
dstate_setinfo("driver.version.data", "%s", subdriver->name);
|
|
|
|
/* Initialise data */
|
|
if (qx_ups_walk(QX_WALKMODE_INIT) == FALSE) {
|
|
fatalx(EXIT_FAILURE, "Can't initialise data from the UPS");
|
|
}
|
|
|
|
/* Init battery guesstimation */
|
|
qx_initbattery();
|
|
|
|
if (dstate_getinfo("ups.delay.start")) {
|
|
|
|
/* Retrieve user defined delay settings */
|
|
val = getval(QX_VAR_ONDELAY);
|
|
|
|
if (val && setvar("ups.delay.start", val) != STAT_SET_HANDLED) {
|
|
fatalx(EXIT_FAILURE, "Start delay '%s' out of range", val);
|
|
}
|
|
|
|
}
|
|
|
|
if (dstate_getinfo("ups.delay.shutdown")) {
|
|
|
|
/* Retrieve user defined delay settings */
|
|
val = getval(QX_VAR_OFFDELAY);
|
|
|
|
if (val && setvar("ups.delay.shutdown", val) != STAT_SET_HANDLED) {
|
|
fatalx(EXIT_FAILURE, "Shutdown delay '%s' out of range", val);
|
|
}
|
|
|
|
}
|
|
|
|
if (!find_nut_info("load.off", QX_FLAG_CMD, QX_FLAG_SKIP) && find_nut_info("load.off.delay", QX_FLAG_CMD, QX_FLAG_SKIP)) {
|
|
/* Adds default with a delay value of '0' (= immediate) */
|
|
dstate_addcmd("load.off");
|
|
}
|
|
|
|
if (!find_nut_info("load.on", QX_FLAG_CMD, QX_FLAG_SKIP) && find_nut_info("load.on.delay", QX_FLAG_CMD, QX_FLAG_SKIP)) {
|
|
/* Adds default with a delay value of '0' (= immediate) */
|
|
dstate_addcmd("load.on");
|
|
}
|
|
|
|
/* Init polling frequency */
|
|
val = getval(QX_VAR_POLLFREQ);
|
|
if (val)
|
|
pollfreq = strtol(val, NULL, 10);
|
|
|
|
dstate_setinfo("driver.parameter.pollfreq", "%d", pollfreq);
|
|
|
|
time(&lastpoll);
|
|
|
|
/* Install handlers */
|
|
upsh.setvar = setvar;
|
|
upsh.instcmd = instcmd;
|
|
|
|
/* Subdriver initinfo */
|
|
if (subdriver->initinfo != NULL)
|
|
subdriver->initinfo();
|
|
}
|
|
|
|
/* Open the port and the like and choose the subdriver */
|
|
void upsdrv_initups(void)
|
|
{
|
|
upsdebugx(1, "%s...", __func__);
|
|
|
|
#if defined(QX_SERIAL) && defined(QX_USB)
|
|
|
|
/* Whether the device is connected through USB or serial */
|
|
if (
|
|
!strcasecmp(dstate_getinfo("driver.parameter.port"), "auto") ||
|
|
getval("subdriver") ||
|
|
getval("vendorid") ||
|
|
getval("productid") ||
|
|
getval("vendor") ||
|
|
getval("product") ||
|
|
getval("serial") ||
|
|
getval("bus") ||
|
|
getval("langid_fix")
|
|
) {
|
|
/* USB */
|
|
is_usb = 1;
|
|
} else {
|
|
/* Serial */
|
|
is_usb = 0;
|
|
}
|
|
|
|
#endif /* QX_SERIAL && QX_USB */
|
|
|
|
/* Serial */
|
|
#ifdef QX_SERIAL
|
|
|
|
#ifdef QX_USB
|
|
if (!is_usb) {
|
|
#endif /* QX_USB */
|
|
|
|
#ifndef TESTING
|
|
|
|
const struct {
|
|
const char *val;
|
|
const int dtr;
|
|
const int rts;
|
|
} cablepower[] = {
|
|
{ "normal", 1, 0 }, /* Default */
|
|
{ "reverse", 0, 1 },
|
|
{ "both", 1, 1 },
|
|
{ "none", 0, 0 },
|
|
{ NULL }
|
|
};
|
|
|
|
int i;
|
|
const char *val;
|
|
struct termios tio;
|
|
|
|
/* Open and lock the serial port and set the speed to 2400 baud. */
|
|
upsfd = ser_open(device_path);
|
|
ser_set_speed(upsfd, device_path, B2400);
|
|
|
|
if (tcgetattr(upsfd, &tio)) {
|
|
fatal_with_errno(EXIT_FAILURE, "tcgetattr");
|
|
}
|
|
|
|
/* Use canonical mode input processing (to read reply line) */
|
|
tio.c_lflag |= ICANON; /* Canonical input (erase and kill processing) */
|
|
|
|
tio.c_cc[VEOF] = _POSIX_VDISABLE;
|
|
tio.c_cc[VEOL] = '\r';
|
|
tio.c_cc[VERASE] = _POSIX_VDISABLE;
|
|
tio.c_cc[VINTR] = _POSIX_VDISABLE;
|
|
tio.c_cc[VKILL] = _POSIX_VDISABLE;
|
|
tio.c_cc[VQUIT] = _POSIX_VDISABLE;
|
|
tio.c_cc[VSUSP] = _POSIX_VDISABLE;
|
|
tio.c_cc[VSTART] = _POSIX_VDISABLE;
|
|
tio.c_cc[VSTOP] = _POSIX_VDISABLE;
|
|
|
|
if (tcsetattr(upsfd, TCSANOW, &tio)) {
|
|
fatal_with_errno(EXIT_FAILURE, "tcsetattr");
|
|
}
|
|
|
|
val = getval("cablepower");
|
|
for (i = 0; val && cablepower[i].val; i++) {
|
|
|
|
if (!strcasecmp(val, cablepower[i].val)) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!cablepower[i].val) {
|
|
fatalx(EXIT_FAILURE, "Value '%s' not valid for 'cablepower'", val);
|
|
}
|
|
|
|
ser_set_dtr(upsfd, cablepower[i].dtr);
|
|
ser_set_rts(upsfd, cablepower[i].rts);
|
|
|
|
/* Allow some time to settle for the cablepower */
|
|
usleep(100000);
|
|
|
|
#endif /* TESTING */
|
|
|
|
#ifdef QX_USB
|
|
} else { /* is_usb */
|
|
#endif /* QX_USB */
|
|
|
|
#endif /* QX_SERIAL */
|
|
|
|
/* USB */
|
|
#ifdef QX_USB
|
|
|
|
#ifndef TESTING
|
|
|
|
const struct {
|
|
const char *name;
|
|
int (*command)(const char *cmd, char *buf, size_t buflen);
|
|
} usbsubdriver[] = {
|
|
{ "cypress", &cypress_command },
|
|
{ "phoenix", &phoenix_command },
|
|
{ "ippon", &ippon_command },
|
|
{ "krauler", &krauler_command },
|
|
{ NULL }
|
|
};
|
|
|
|
int ret, langid;
|
|
char tbuf[255]; /* Some devices choke on size > 255 */
|
|
char *regex_array[6];
|
|
|
|
char *subdrv = getval("subdriver");
|
|
|
|
regex_array[0] = getval("vendorid");
|
|
regex_array[1] = getval("productid");
|
|
regex_array[2] = getval("vendor");
|
|
regex_array[3] = getval("product");
|
|
regex_array[4] = getval("serial");
|
|
regex_array[5] = getval("bus");
|
|
|
|
/* Check for language ID workaround (#1) */
|
|
if (getval("langid_fix")) {
|
|
/* Skip "0x" prefix and set back to hexadecimal */
|
|
if (sscanf(getval("langid_fix") + 2, "%x", &langid_fix) != 1) {
|
|
upslogx(LOG_NOTICE, "Error enabling language ID workaround");
|
|
} else {
|
|
upsdebugx(2, "Language ID workaround enabled (using '0x%x')", langid_fix);
|
|
}
|
|
}
|
|
|
|
/* Pick up the subdriver name if set explicitly */
|
|
if (subdrv) {
|
|
|
|
int i;
|
|
|
|
if (!regex_array[0] || !regex_array[1]) {
|
|
fatalx(EXIT_FAILURE, "When specifying a subdriver, 'vendorid' and 'productid' are mandatory.");
|
|
}
|
|
|
|
for (i = 0; usbsubdriver[i].name; i++) {
|
|
|
|
if (strcasecmp(subdrv, usbsubdriver[i].name)) {
|
|
continue;
|
|
}
|
|
|
|
subdriver_command = usbsubdriver[i].command;
|
|
break;
|
|
}
|
|
|
|
if (!subdriver_command) {
|
|
fatalx(EXIT_FAILURE, "Subdriver '%s' not found!", subdrv);
|
|
}
|
|
|
|
}
|
|
|
|
ret = USBNewRegexMatcher(®ex_matcher, regex_array, REG_ICASE | REG_EXTENDED);
|
|
switch (ret)
|
|
{
|
|
case -1:
|
|
fatal_with_errno(EXIT_FAILURE, "USBNewRegexMatcher");
|
|
case 0:
|
|
break; /* All is well */
|
|
default:
|
|
fatalx(EXIT_FAILURE, "Invalid regular expression: %s", regex_array[ret]);
|
|
}
|
|
|
|
/* Link the matchers */
|
|
regex_matcher->next = &device_matcher;
|
|
|
|
ret = usb->open(&udev, &usbdevice, regex_matcher, NULL);
|
|
if (ret < 0) {
|
|
fatalx(EXIT_FAILURE,
|
|
"No supported devices found. Please check your device availability with 'lsusb'\n"
|
|
"and make sure you have an up-to-date version of NUT. If this does not help,\n"
|
|
"try running the driver with at least 'subdriver', 'vendorid' and 'productid'\n"
|
|
"options specified. Please refer to the man page for details about these options\n"
|
|
"(man 8 nutdrv_qx).\n");
|
|
}
|
|
|
|
if (!subdriver_command) {
|
|
fatalx(EXIT_FAILURE, "No subdriver selected");
|
|
}
|
|
|
|
/* Create a new matcher for later reopening */
|
|
ret = USBNewExactMatcher(&reopen_matcher, &usbdevice);
|
|
if (ret) {
|
|
fatal_with_errno(EXIT_FAILURE, "USBNewExactMatcher");
|
|
}
|
|
|
|
/* Link the matchers */
|
|
reopen_matcher->next = regex_matcher;
|
|
|
|
dstate_setinfo("ups.vendorid", "%04x", usbdevice.VendorID);
|
|
dstate_setinfo("ups.productid", "%04x", usbdevice.ProductID);
|
|
|
|
/* Check for language ID workaround (#2) */
|
|
if (langid_fix != -1) {
|
|
/* Future improvement:
|
|
* Asking for the zero'th index is special - it returns a string descriptor that contains all the language IDs supported by the device.
|
|
* Typically there aren't many - often only one.
|
|
* The language IDs are 16 bit numbers, and they start at the third byte in the descriptor.
|
|
* See USB 2.0 specification, section 9.6.7, for more information on this.
|
|
* This should allow automatic application of the workaround */
|
|
ret = usb_get_string(udev, 0, 0, tbuf, sizeof(tbuf));
|
|
if (ret >= 4) {
|
|
langid = tbuf[2] | (tbuf[3] << 8);
|
|
upsdebugx(1, "First supported language ID: 0x%x (please report to the NUT maintainer!)", langid);
|
|
}
|
|
}
|
|
|
|
#endif /* TESTING */
|
|
|
|
#ifdef QX_SERIAL
|
|
} /* is_usb */
|
|
#endif /* QX_SERIAL */
|
|
|
|
#endif /* QX_USB */
|
|
|
|
/* Choose subdriver */
|
|
if (!subdriver_matcher())
|
|
fatalx(EXIT_FAILURE, "Device not supported!");
|
|
|
|
/* Subdriver initups */
|
|
if (subdriver->initups != NULL)
|
|
subdriver->initups();
|
|
}
|
|
|
|
/* Close the ports and the like */
|
|
void upsdrv_cleanup(void)
|
|
{
|
|
upsdebugx(1, "%s...", __func__);
|
|
|
|
#ifndef TESTING
|
|
|
|
#ifdef QX_SERIAL
|
|
|
|
#ifdef QX_USB
|
|
if (!is_usb) {
|
|
#endif /* QX_USB */
|
|
|
|
ser_set_dtr(upsfd, 0);
|
|
ser_close(upsfd, device_path);
|
|
|
|
#ifdef QX_USB
|
|
} else { /* is_usb */
|
|
#endif /* QX_USB */
|
|
|
|
#endif /* QX_SERIAL */
|
|
|
|
#ifdef QX_USB
|
|
|
|
usb->close(udev);
|
|
USBFreeExactMatcher(reopen_matcher);
|
|
USBFreeRegexMatcher(regex_matcher);
|
|
free(usbdevice.Vendor);
|
|
free(usbdevice.Product);
|
|
free(usbdevice.Serial);
|
|
free(usbdevice.Bus);
|
|
|
|
#ifdef QX_SERIAL
|
|
} /* is_usb */
|
|
#endif /* QX_SERIAL */
|
|
|
|
#endif /* QX_USB */
|
|
|
|
#endif /* TESTING */
|
|
|
|
}
|
|
|
|
|
|
/* == Support functions == */
|
|
|
|
/* Generic command processing function: send a command and read a reply.
|
|
* Returns < 0 on error, 0 on timeout and the number of bytes read on success. */
|
|
static int qx_command(const char *cmd, char *buf, size_t buflen)
|
|
{
|
|
#ifndef TESTING
|
|
|
|
int ret = -1;
|
|
|
|
#ifdef QX_USB
|
|
|
|
#ifdef QX_SERIAL
|
|
/* Communication: USB */
|
|
if (is_usb) {
|
|
#endif /* QX_SERIAL */
|
|
|
|
if (udev == NULL) {
|
|
ret = usb->open(&udev, &usbdevice, reopen_matcher, NULL);
|
|
|
|
if (ret < 1) {
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
ret = (*subdriver_command)(cmd, buf, buflen);
|
|
|
|
if (ret >= 0) {
|
|
return ret;
|
|
}
|
|
|
|
switch (ret)
|
|
{
|
|
case -EBUSY: /* Device or resource busy */
|
|
fatal_with_errno(EXIT_FAILURE, "Got disconnected by another driver");
|
|
|
|
case -EPERM: /* Operation not permitted */
|
|
fatal_with_errno(EXIT_FAILURE, "Permissions problem");
|
|
|
|
case -EPIPE: /* Broken pipe */
|
|
if (usb_clear_halt(udev, 0x81) == 0) {
|
|
upsdebugx(1, "Stall condition cleared");
|
|
break;
|
|
}
|
|
#ifdef ETIME
|
|
case -ETIME: /* Timer expired */
|
|
#endif /* ETIME */
|
|
if (usb_reset(udev) == 0) {
|
|
upsdebugx(1, "Device reset handled");
|
|
}
|
|
case -ENODEV: /* No such device */
|
|
case -EACCES: /* Permission denied */
|
|
case -EIO: /* I/O error */
|
|
case -ENXIO: /* No such device or address */
|
|
case -ENOENT: /* No such file or directory */
|
|
/* Uh oh, got to reconnect! */
|
|
usb->close(udev);
|
|
udev = NULL;
|
|
break;
|
|
|
|
case -ETIMEDOUT: /* Connection timed out */
|
|
case -EOVERFLOW: /* Value too large for defined data type */
|
|
#ifdef EPROTO
|
|
case -EPROTO: /* Protocol error */
|
|
#endif
|
|
default:
|
|
break;
|
|
}
|
|
|
|
#ifdef QX_SERIAL
|
|
/* Communication: serial */
|
|
} else { /* !is_usb */
|
|
#endif /* QX_SERIAL */
|
|
|
|
#endif /* QX_USB */
|
|
|
|
#ifdef QX_SERIAL
|
|
|
|
ser_flush_io(upsfd);
|
|
|
|
ret = ser_send(upsfd, "%s", cmd);
|
|
|
|
if (ret <= 0) {
|
|
upsdebugx(3, "send: %s", ret ? strerror(errno) : "timeout");
|
|
return ret;
|
|
}
|
|
|
|
upsdebugx(3, "send: '%.*s'", (int)strcspn(cmd, "\r"), cmd);
|
|
|
|
ret = ser_get_buf(upsfd, buf, buflen, SER_WAIT_SEC, 0);
|
|
|
|
if (ret <= 0) {
|
|
upsdebugx(3, "read: %s", ret ? strerror(errno) : "timeout");
|
|
return ret;
|
|
}
|
|
|
|
upsdebugx(3, "read: '%.*s'", (int)strcspn(buf, "\r"), buf);
|
|
|
|
#ifdef QX_USB
|
|
} /* !is_usb */
|
|
#endif /* QX_USB */
|
|
|
|
#endif /* QX_SERIAL */
|
|
|
|
return ret;
|
|
|
|
#else /* TESTING */
|
|
|
|
testing_t *testing = subdriver->testing;
|
|
int i;
|
|
|
|
memset(buf, 0, buflen);
|
|
|
|
upsdebugx(3, "send: '%.*s'", (int)strcspn(cmd, "\r"), cmd);
|
|
|
|
for (i = 0; cmd && testing[i].cmd; i++) {
|
|
|
|
if (strcasecmp(cmd, testing[i].cmd)) {
|
|
continue;
|
|
}
|
|
|
|
upsdebugx(3, "read: '%.*s'", (int)strcspn(testing[i].answer, "\r"), testing[i].answer);
|
|
return snprintf(buf, buflen, "%s", testing[i].answer);
|
|
|
|
}
|
|
|
|
/* If the driver expects some kind of reply in case of error.. */
|
|
if (subdriver->rejected != NULL) {
|
|
|
|
/* ..fulfill its expectations.. */
|
|
upsdebugx(3, "read: '%.*s'", (int)strcspn(subdriver->rejected, "\r"), subdriver->rejected);
|
|
return snprintf(buf, buflen, "%s", subdriver->rejected);
|
|
|
|
/* ..otherwise.. */
|
|
} else {
|
|
|
|
/* ..echo back the command */
|
|
upsdebugx(3, "read: '%.*s'", (int)strcspn(cmd, "\r"), cmd);
|
|
return snprintf(buf, buflen, "%s", cmd);
|
|
|
|
}
|
|
|
|
#endif /* TESTING */
|
|
}
|
|
|
|
/* Update ups_status to remember this status item.
|
|
* Interpretation is done in ups_status_set(). */
|
|
void update_status(const char *value)
|
|
{
|
|
status_lkp_t *status_item;
|
|
int clear = 0;
|
|
|
|
upsdebugx(5, "%s: %s", __func__, value);
|
|
|
|
if (*value == '!') {
|
|
value++;
|
|
clear = 1;
|
|
}
|
|
|
|
for (status_item = status_info; status_item->status_str != NULL ; status_item++) {
|
|
|
|
if (strcasecmp(status_item->status_str, value))
|
|
continue;
|
|
|
|
if (clear) {
|
|
ups_status &= ~status_item->status_mask;
|
|
} else {
|
|
ups_status |= status_item->status_mask;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
upsdebugx(5, "%s: Warning! %s not in list of known values", __func__, value);
|
|
}
|
|
|
|
/* Choose subdriver */
|
|
static int subdriver_matcher(void)
|
|
{
|
|
const char *protocol = getval("protocol");
|
|
int i;
|
|
|
|
/* Select the subdriver for this device */
|
|
for (i = 0; subdriver_list[i] != NULL; i++) {
|
|
|
|
int j;
|
|
|
|
/* If protocol is set in ups.conf, use it */
|
|
if (protocol) {
|
|
|
|
char subdrv_name[SMALLBUF];
|
|
|
|
/* Get rid of subdriver version */
|
|
snprintf(subdrv_name, sizeof(subdrv_name), "%.*s", (int)strcspn(subdriver_list[i]->name, " "), subdriver_list[i]->name);
|
|
|
|
if (strcasecmp(subdrv_name, protocol)) {
|
|
upsdebugx(2, "Skipping protocol %s", subdriver_list[i]->name);
|
|
continue;
|
|
}
|
|
|
|
}
|
|
|
|
/* Give every subdriver some tries */
|
|
for (j = 0; j < MAXTRIES; j++) {
|
|
|
|
subdriver = subdriver_list[i];
|
|
|
|
if (subdriver->claim()) {
|
|
break;
|
|
}
|
|
|
|
subdriver = NULL;
|
|
|
|
}
|
|
|
|
if (subdriver != NULL)
|
|
break;
|
|
|
|
}
|
|
|
|
if (!subdriver) {
|
|
upslogx(LOG_ERR, "Device not supported!");
|
|
return 0;
|
|
}
|
|
|
|
upslogx(LOG_INFO, "Using protocol: %s", subdriver->name);
|
|
|
|
return 1;
|
|
}
|
|
|
|
/* Set vars boundaries */
|
|
static void qx_set_var(item_t *item)
|
|
{
|
|
if (!(item->qxflags & QX_FLAG_NONUT))
|
|
dstate_setflags(item->info_type, item->info_flags);
|
|
|
|
/* Set max length for strings, if needed */
|
|
if (item->info_flags & ST_FLAG_STRING && !(item->qxflags & QX_FLAG_NONUT))
|
|
dstate_setaux(item->info_type, strtol(item->info_rw[0].value, NULL, 10));
|
|
|
|
/* Set enum list */
|
|
if (item->qxflags & QX_FLAG_ENUM) {
|
|
|
|
info_rw_t *envalue;
|
|
char buf[LARGEBUF] = "";
|
|
|
|
/* Loop on all existing values */
|
|
for (envalue = item->info_rw; envalue != NULL && strlen(envalue->value) > 0; envalue++) {
|
|
|
|
if (envalue->preprocess && envalue->preprocess(envalue->value, sizeof(envalue->value)))
|
|
continue;
|
|
|
|
/* This item is not available yet in NUT, so publish these data in the logs */
|
|
if (item->qxflags & QX_FLAG_NONUT) {
|
|
|
|
snprintfcat(buf, sizeof(buf), " %s", envalue->value);
|
|
|
|
/* This item is available in NUT, add its enum to the variable */
|
|
} else {
|
|
|
|
dstate_addenum(item->info_type, "%s", envalue->value);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (item->qxflags & QX_FLAG_NONUT)
|
|
upslogx(LOG_INFO, "%s, settable values:%s", item->info_type, strlen(buf) > 0 ? buf : " none");
|
|
|
|
}
|
|
|
|
/* Set range */
|
|
if (item->qxflags & QX_FLAG_RANGE) {
|
|
|
|
info_rw_t *rvalue, *from = NULL, *to = NULL;
|
|
int ok = 0;
|
|
|
|
/* Loop on all existing values */
|
|
for (rvalue = item->info_rw; rvalue != NULL && strlen(rvalue->value) > 0; rvalue++) {
|
|
|
|
if (rvalue->preprocess && rvalue->preprocess(rvalue->value, sizeof(rvalue->value)))
|
|
continue;
|
|
|
|
if (!from) {
|
|
from = rvalue;
|
|
continue;
|
|
}
|
|
|
|
to = rvalue;
|
|
|
|
/* This item is not available yet in NUT, so publish these data in the logs */
|
|
if (item->qxflags & QX_FLAG_NONUT) {
|
|
|
|
upslogx(LOG_INFO, "%s, settable range: %s..%s", item->info_type, from->value, to->value);
|
|
ok++;
|
|
|
|
/* This item is available in NUT, add its range to the variable */
|
|
} else {
|
|
|
|
dstate_addrange(item->info_type, strtol(from->value, NULL, 10), strtol(to->value, NULL, 10));
|
|
|
|
}
|
|
|
|
from = NULL;
|
|
to = NULL;
|
|
|
|
}
|
|
|
|
/* This item is not available yet in NUT and we weren't able to get its range; let people know it */
|
|
if ((item->qxflags & QX_FLAG_NONUT) && !ok)
|
|
upslogx(LOG_INFO, "%s, settable range: none", item->info_type);
|
|
|
|
}
|
|
}
|
|
|
|
/* Walk UPS variables and set elements of the qx2nut array. */
|
|
static bool_t qx_ups_walk(walkmode_t mode)
|
|
{
|
|
item_t *item;
|
|
int retcode;
|
|
|
|
/* Clear batt.{chrg,runt}.act for guesstimation */
|
|
if (mode == QX_WALKMODE_FULL_UPDATE) {
|
|
batt.runt.act = -1;
|
|
batt.chrg.act = -1;
|
|
}
|
|
|
|
/* 3 modes: QX_WALKMODE_INIT, QX_WALKMODE_QUICK_UPDATE and QX_WALKMODE_FULL_UPDATE */
|
|
|
|
/* Device data walk */
|
|
for (item = subdriver->qx2nut; item->info_type != NULL; item++) {
|
|
|
|
/* Skip this item */
|
|
if (item->qxflags & QX_FLAG_SKIP)
|
|
continue;
|
|
|
|
upsdebugx(10, "%s: processing: %s", __func__, item->info_type);
|
|
|
|
/* Filter data according to mode */
|
|
switch (mode)
|
|
{
|
|
/* Device capabilities enumeration */
|
|
case QX_WALKMODE_INIT:
|
|
|
|
/* Special case for handling server side variables */
|
|
if (item->qxflags & QX_FLAG_ABSENT) {
|
|
|
|
/* Already set */
|
|
if (dstate_getinfo(item->info_type))
|
|
continue;
|
|
|
|
dstate_setinfo(item->info_type, "%s", item->dfl);
|
|
|
|
/* Set var flags/range/enum */
|
|
qx_set_var(item);
|
|
|
|
continue;
|
|
}
|
|
|
|
/* Allow duplicates for these NUT variables */
|
|
if (!strncmp(item->info_type, "ups.alarm", 9) || !strncmp(item->info_type, "ups.status", 10))
|
|
break;
|
|
|
|
/* This one doesn't exist yet */
|
|
if (dstate_getinfo(item->info_type) == NULL)
|
|
break;
|
|
|
|
continue;
|
|
|
|
case QX_WALKMODE_QUICK_UPDATE:
|
|
|
|
/* Quick update only deals with status and alarms! */
|
|
if (!(item->qxflags & QX_FLAG_QUICK_POLL))
|
|
continue;
|
|
|
|
break;
|
|
|
|
case QX_WALKMODE_FULL_UPDATE:
|
|
|
|
/* These don't need polling after initinfo() */
|
|
if (item->qxflags & (QX_FLAG_ABSENT | QX_FLAG_CMD | QX_FLAG_SETVAR | QX_FLAG_STATIC))
|
|
continue;
|
|
|
|
/* These need to be polled after user changes (setvar / instcmd) */
|
|
if ((item->qxflags & QX_FLAG_SEMI_STATIC) && (data_has_changed == FALSE))
|
|
continue;
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
fatalx(EXIT_FAILURE, "%s: unknown update mode!", __func__);
|
|
|
|
}
|
|
|
|
/* Instant commands */
|
|
if (item->qxflags & QX_FLAG_CMD) {
|
|
dstate_addcmd(item->info_type);
|
|
continue;
|
|
}
|
|
|
|
/* Setvars */
|
|
if (item->qxflags & QX_FLAG_SETVAR) {
|
|
|
|
if (item->qxflags & QX_FLAG_NONUT) {
|
|
setvar(item->info_type, NULL);
|
|
item->qxflags |= QX_FLAG_SKIP;
|
|
}
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
/* Check whether the previous item uses the same command and then use its answer, if available.. */
|
|
if (strlen(previous_item.command) > 0 && strlen(previous_item.answer) > 0 && !strcasecmp(previous_item.command, item->command)) {
|
|
|
|
snprintf(item->answer, sizeof(item->answer), "%s", previous_item.answer);
|
|
|
|
/* Process the answer */
|
|
retcode = qx_process_answer(item, strlen(item->answer));
|
|
|
|
/* ..otherwise: execute command to get answer from the UPS */
|
|
} else
|
|
retcode = qx_process(item, NULL);
|
|
|
|
/* Record item as previous_item */
|
|
snprintf(previous_item.command, sizeof(previous_item.command), "%s", item->command);
|
|
snprintf(previous_item.answer, sizeof(previous_item.answer), "%s", item->answer);
|
|
|
|
if (retcode) {
|
|
|
|
if (item->qxflags & QX_FLAG_QUICK_POLL)
|
|
return FALSE;
|
|
|
|
if (mode == QX_WALKMODE_INIT)
|
|
/* Skip this item from now on */
|
|
item->qxflags |= QX_FLAG_SKIP;
|
|
|
|
/* Clear data from the item */
|
|
snprintf(item->answer, sizeof(item->answer), "%s", "");
|
|
snprintf(item->value, sizeof(item->value), "%s", "");
|
|
|
|
/* Don't know what happened, try again later... */
|
|
continue;
|
|
|
|
}
|
|
|
|
/* Process the value we got back (set status bits and set the value of other parameters) */
|
|
retcode = ups_infoval_set(item);
|
|
|
|
/* Clear data from the item */
|
|
snprintf(item->answer, sizeof(item->answer), "%s", "");
|
|
snprintf(item->value, sizeof(item->value), "%s", "");
|
|
|
|
/* Uh-oh! Some error! */
|
|
if (retcode == -1)
|
|
continue;
|
|
|
|
/* Set var flags/range/enum (not for ups.{alarm.status}, hence the retcode check) */
|
|
if (retcode && mode == QX_WALKMODE_INIT) {
|
|
qx_set_var(item);
|
|
}
|
|
|
|
}
|
|
|
|
/* Clear data from previous_item */
|
|
snprintf(previous_item.command, sizeof(previous_item.command), "%s", "");
|
|
snprintf(previous_item.answer, sizeof(previous_item.answer), "%s", "");
|
|
|
|
/* Update battery guesstimation */
|
|
if (mode == QX_WALKMODE_FULL_UPDATE && (batt.runt.act == -1 || batt.chrg.act == -1)) {
|
|
|
|
if (getval("runtimecal")) {
|
|
|
|
time_t battery_now;
|
|
|
|
time(&battery_now);
|
|
|
|
/* OL */
|
|
if (ups_status & STATUS(OL)) {
|
|
|
|
batt.runt.est += batt.runt.nom * difftime(battery_now, battery_lastpoll) / batt.chrg.time;
|
|
if (batt.runt.est > batt.runt.nom) {
|
|
batt.runt.est = batt.runt.nom;
|
|
}
|
|
|
|
/* OB */
|
|
} else {
|
|
|
|
batt.runt.est -= load.eff * difftime(battery_now, battery_lastpoll);
|
|
if (batt.runt.est < 0) {
|
|
batt.runt.est = 0;
|
|
}
|
|
|
|
}
|
|
|
|
if (batt.chrg.act == -1)
|
|
dstate_setinfo("battery.charge", "%.0f", 100 * batt.runt.est / batt.runt.nom);
|
|
|
|
if (batt.runt.act == -1 && !qx_load())
|
|
dstate_setinfo("battery.runtime", "%.0f", batt.runt.est / load.eff);
|
|
|
|
battery_lastpoll = battery_now;
|
|
|
|
} else {
|
|
|
|
qx_battery();
|
|
|
|
}
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
/* Convert the local status information to NUT format and set NUT alarms. */
|
|
static void ups_alarm_set(void)
|
|
{
|
|
if (ups_status & STATUS(RB)) {
|
|
alarm_set("Replace battery!");
|
|
}
|
|
if (ups_status & STATUS(FSD)) {
|
|
alarm_set("Shutdown imminent!");
|
|
}
|
|
}
|
|
|
|
/* Convert the local status information to NUT format and set NUT status. */
|
|
static void ups_status_set(void)
|
|
{
|
|
if (ups_status & STATUS(OL)) {
|
|
status_set("OL"); /* On line */
|
|
} else {
|
|
status_set("OB"); /* On battery */
|
|
}
|
|
if (ups_status & STATUS(DISCHRG)) {
|
|
status_set("DISCHRG"); /* Discharging */
|
|
}
|
|
if (ups_status & STATUS(CHRG)) {
|
|
status_set("CHRG"); /* Charging */
|
|
}
|
|
if (ups_status & STATUS(LB)) {
|
|
status_set("LB"); /* Low battery */
|
|
}
|
|
if (ups_status & STATUS(OVER)) {
|
|
status_set("OVER"); /* Overload */
|
|
}
|
|
if (ups_status & STATUS(RB)) {
|
|
status_set("RB"); /* Replace battery */
|
|
}
|
|
if (ups_status & STATUS(TRIM)) {
|
|
status_set("TRIM"); /* SmartTrim */
|
|
}
|
|
if (ups_status & STATUS(BOOST)) {
|
|
status_set("BOOST"); /* SmartBoost */
|
|
}
|
|
if (ups_status & STATUS(BYPASS)) {
|
|
status_set("BYPASS"); /* On bypass */
|
|
}
|
|
if (ups_status & STATUS(OFF)) {
|
|
status_set("OFF"); /* UPS is off */
|
|
}
|
|
if (ups_status & STATUS(CAL)) {
|
|
status_set("CAL"); /* Calibration */
|
|
}
|
|
if (ups_status & STATUS(FSD)) {
|
|
status_set("FSD"); /* Forced shutdown */
|
|
}
|
|
}
|
|
|
|
/* Find element definition in qx2nut array by NUT varname optionally filtered by its qxflags:
|
|
* - 'flag': flags that have to be set in the item, i.e. if one of the flags is absent in the item it won't be returned
|
|
* - 'noflag': flags that have to be absent in the item, i.e. if at least one of the flags is set in the item it won't be returned */
|
|
item_t *find_nut_info(const char *varname, const unsigned long flag, const unsigned long noflag)
|
|
{
|
|
item_t *item;
|
|
|
|
for (item = subdriver->qx2nut; item->info_type != NULL; item++) {
|
|
|
|
if (strcasecmp(item->info_type, varname))
|
|
continue;
|
|
|
|
if (flag && ((item->qxflags & flag) != flag))
|
|
continue;
|
|
|
|
if (noflag && (item->qxflags & noflag))
|
|
continue;
|
|
|
|
return item;
|
|
}
|
|
|
|
upsdebugx(2, "%s: info type %s not found", __func__, varname);
|
|
return NULL;
|
|
}
|
|
|
|
/* Process the answer we got back from the UPS
|
|
* Return -1 on errors, 0 on success */
|
|
static int qx_process_answer(item_t *item, const int len)
|
|
{
|
|
/* Query rejected by the UPS */
|
|
if (subdriver->rejected && !strcasecmp(item->answer, subdriver->rejected)) {
|
|
upsdebugx(2, "%s: query rejected by the UPS (%s)", __func__, item->info_type);
|
|
return -1;
|
|
}
|
|
|
|
/* Short reply */
|
|
if (item->answer_len && len < item->answer_len) {
|
|
upsdebugx(2, "%s: short reply (%s)", __func__, item->info_type);
|
|
return -1;
|
|
}
|
|
|
|
/* Wrong leading character */
|
|
if (item->leading && item->answer[0] != item->leading) {
|
|
upsdebugx(2, "%s: %s - invalid start character [%02x], expected [%02x]", __func__, item->info_type, item->answer[0], item->leading);
|
|
return -1;
|
|
}
|
|
|
|
/* Get value */
|
|
if (strlen(item->answer)) {
|
|
snprintf(item->value, sizeof(item->value), "%.*s", item->to ? 1 + item->to - item->from : (int)strcspn(item->answer, "\r") - item->from, item->answer + item->from);
|
|
} else {
|
|
snprintf(item->value, sizeof(item->value), "%s", "");
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* Send the command to the UPS and process the reply.
|
|
* Return -1 on errors, 0 on success */
|
|
int qx_process(item_t *item, const char *command)
|
|
{
|
|
char buf[SMALLBUF] = "";
|
|
|
|
/* Send the command */
|
|
int len = qx_command(command ? command : item->command, buf, sizeof(buf));
|
|
|
|
snprintf(item->answer, sizeof(item->answer), "%s", buf);
|
|
|
|
/* Process the answer to get the value */
|
|
return qx_process_answer(item, len);
|
|
}
|
|
|
|
/* Process the value we got back (set status bits and set the value of other parameters). */
|
|
/* Return -1 on failure, 0 for a status update and 1 in all other cases */
|
|
int ups_infoval_set(item_t *item)
|
|
{
|
|
char value[SMALLBUF] = "";
|
|
|
|
/* Item need to be preprocessed? */
|
|
if (item->preprocess != NULL){
|
|
|
|
/* Process the value returned by the UPS to NUT standards */
|
|
if (item->preprocess(item, value, sizeof(value))) {
|
|
upsdebugx(4, "%s: failed to preprocess value [%s: %s]", __func__, item->info_type, item->value);
|
|
return -1;
|
|
}
|
|
|
|
/* Deal with status items */
|
|
if (!strncmp(item->info_type, "ups.status", 10)) {
|
|
if (strlen(value) > 0)
|
|
update_status(value);
|
|
return 0;
|
|
}
|
|
|
|
/* Deal with alarm items */
|
|
if (!strncmp(item->info_type, "ups.alarm", 9)) {
|
|
if (strlen(value) > 0)
|
|
alarm_set(value);
|
|
return 0;
|
|
}
|
|
|
|
} else {
|
|
|
|
snprintf(value, sizeof(value), "%s", item->value);
|
|
|
|
/* Cover most of the cases: either left/right filled with hashes, spaces or a mix of both */
|
|
if (item->qxflags & QX_FLAG_TRIM) {
|
|
|
|
char buf[SMALLBUF];
|
|
|
|
snprintf(buf, sizeof(buf), "%s", ltrim(value, ' '));
|
|
snprintf(value, sizeof(value), "%s", buf);
|
|
|
|
snprintf(buf, sizeof(buf), "%s", rtrim(value, ' '));
|
|
snprintf(value, sizeof(value), "%s", buf);
|
|
|
|
snprintf(buf, sizeof(buf), "%s", ltrim(value, '#'));
|
|
snprintf(value, sizeof(value), "%s", buf);
|
|
|
|
snprintf(buf, sizeof(buf), "%s", rtrim(value, '#'));
|
|
snprintf(value, sizeof(value), "%s", buf);
|
|
|
|
snprintf(buf, sizeof(buf), "%s", ltrim(value, ' '));
|
|
snprintf(value, sizeof(value), "%s", buf);
|
|
|
|
snprintf(buf, sizeof(buf), "%s", rtrim(value, ' '));
|
|
snprintf(value, sizeof(value), "%s", buf);
|
|
|
|
snprintf(buf, sizeof(buf), "%s", ltrim(value, '#'));
|
|
snprintf(value, sizeof(value), "%s", buf);
|
|
|
|
snprintf(buf, sizeof(buf), "%s", rtrim(value, '#'));
|
|
snprintf(value, sizeof(value), "%s", buf);
|
|
|
|
}
|
|
|
|
if (strcasecmp(item->dfl, "%s")) {
|
|
|
|
if (strspn(value, "0123456789 .") != strlen(value)) {
|
|
upsdebugx(2, "%s: non numerical value [%s: %s]", __func__, item->info_type, value);
|
|
return -1;
|
|
}
|
|
|
|
snprintf(value, sizeof(value), item->dfl, strtod(value, NULL));
|
|
}
|
|
|
|
}
|
|
|
|
if (item->qxflags & QX_FLAG_NONUT) {
|
|
upslogx(LOG_INFO, "%s: %s", item->info_type, value);
|
|
return 1;
|
|
}
|
|
|
|
if (!strlen(value)) {
|
|
upsdebugx(1, "%s: non significant value [%s]", __func__, item->info_type);
|
|
return -1;
|
|
}
|
|
|
|
dstate_setinfo(item->info_type, "%s", value);
|
|
|
|
/* Fill batt.{chrg,runt}.act for guesstimation */
|
|
if (!strcasecmp(item->info_type, "battery.charge"))
|
|
batt.chrg.act = strtol(value, NULL, 10);
|
|
else if (!strcasecmp(item->info_type, "battery.runtime"))
|
|
batt.runt.act = strtol(value, NULL, 10);
|
|
|
|
return 1;
|
|
}
|
|
|
|
/* Return actual status */
|
|
int qx_status(void)
|
|
{
|
|
return ups_status;
|
|
}
|