nut/drivers/usbhid-ups.c
2015-04-30 15:53:36 +02:00

1575 lines
41 KiB
C

/* usbhid-ups.c - Driver for USB and serial (MGE SHUT) HID UPS units
*
* 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>
*
* This program was sponsored by MGE UPS SYSTEMS, and now Eaton
*
* 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
*
* TODO list:
* - set ST_FLAG_RW according to HIDData_t->Attribute (ATTR_DATA_CST-ATTR_NVOL_VOL)
*/
#define DRIVER_NAME "Generic HID driver"
#define DRIVER_VERSION "0.39"
#include "main.h"
#include "libhid.h"
#include "usbhid-ups.h"
#include "hidparser.h"
#include "hidtypes.h"
/* include all known subdrivers */
#include "mge-hid.h"
#ifndef SHUT_MODE
#include "explore-hid.h"
#include "apc-hid.h"
#include "belkin-hid.h"
#include "cps-hid.h"
#include "liebert-hid.h"
#include "powercom-hid.h"
#include "tripplite-hid.h"
#include "idowell-hid.h"
#include "openups-hid.h"
#endif
/* master list of avaiable subdrivers */
static subdriver_t *subdriver_list[] = {
#ifndef SHUT_MODE
&explore_subdriver,
#endif
&mge_subdriver,
#ifndef SHUT_MODE
&apc_subdriver,
&belkin_subdriver,
&cps_subdriver,
&liebert_subdriver,
&powercom_subdriver,
&tripplite_subdriver,
&idowell_subdriver,
&openups_subdriver,
#endif
NULL
};
upsdrv_info_t upsdrv_info = {
DRIVER_NAME,
DRIVER_VERSION,
"Arnaud Quette <arnaud.quette@gmail.com>\n" \
"Peter Selinger <selinger@users.sourceforge.net>\n" \
"Arjen de Korte <adkorte-guest@alioth.debian.org>\n" \
"John Stamp <kinsayder@hotmail.com>",
/*FIXME: link the subdrivers? do the same as for the mibs! */
#ifndef SHUT_MODE
DRV_STABLE,
#else
DRV_EXPERIMENTAL,
#endif
{ &comm_upsdrv_info, NULL }
};
/* Data walk modes */
typedef enum {
HU_WALKMODE_INIT = 0,
HU_WALKMODE_QUICK_UPDATE,
HU_WALKMODE_FULL_UPDATE
} walkmode_t;
/* pointer to the active subdriver object (changed in callback() function) */
static subdriver_t *subdriver = NULL;
/* Global vars */
static HIDDevice_t *hd = NULL;
static HIDDevice_t curDevice = { 0x0000, 0x0000, NULL, NULL, NULL, NULL };
static HIDDeviceMatcher_t *subdriver_matcher = NULL;
#ifndef SHUT_MODE
static HIDDeviceMatcher_t *exact_matcher = NULL;
static HIDDeviceMatcher_t *regex_matcher = NULL;
#endif
static int pollfreq = DEFAULT_POLLFREQ;
static int ups_status = 0;
static bool_t data_has_changed = FALSE; /* for SEMI_STATIC data polling */
#ifndef SUN_LIBUSB
bool_t use_interrupt_pipe = TRUE;
#else
bool_t use_interrupt_pipe = FALSE;
#endif
static time_t lastpoll; /* Timestamp the last polling */
hid_dev_handle_t udev;
/* support functions */
static hid_info_t *find_nut_info(const char *varname);
static hid_info_t *find_hid_info(const HIDData_t *hiddata);
static const char *hu_find_infoval(info_lkp_t *hid2info, const double value);
static long hu_find_valinfo(info_lkp_t *hid2info, const char* value);
static void process_boolean_info(const char *nutvalue);
static void ups_alarm_set(void);
static void ups_status_set(void);
static bool_t hid_ups_walk(walkmode_t mode);
static int reconnect_ups(void);
static int ups_infoval_set(hid_info_t *item, double value);
static int callback(hid_dev_handle_t udev, HIDDevice_t *hd, unsigned char *rdbuf, int rdlen);
#ifdef DEBUG
static double interval(void);
#endif
/* global variables */
HIDDesc_t *pDesc = NULL; /* parsed Report Descriptor */
reportbuf_t *reportbuf = NULL; /* buffer for most recent reports */
/* ---------------------------------------------------------------------- */
/* data for processing boolean values from UPS */
#define STATUS(x) ((unsigned)1<<x)
typedef enum {
ONLINE = 0, /* on line */
DISCHRG, /* discharging */
CHRG, /* charging */
LOWBATT, /* low battery */
OVERLOAD, /* overload */
REPLACEBATT, /* replace battery */
SHUTDOWNIMM, /* shutdown imminent */
TRIM, /* SmartTrim */
BOOST, /* SmartBoost */
BYPASSAUTO, /* on automatic bypass */
BYPASSMAN, /* on manual/service bypass */
OFF, /* ups is off */
CAL, /* calibration */
OVERHEAT, /* overheat; Belkin, TrippLite */
COMMFAULT, /* UPS fault; Belkin, TrippLite */
DEPLETED, /* battery depleted; Belkin */
TIMELIMITEXP, /* time limit expired; APC */
FULLYCHARGED, /* battery full; CyberPower */
AWAITINGPOWER, /* awaiting power; Belkin, TrippLite */
FANFAIL, /* fan failure; MGE */
NOBATTERY, /* battery missing; MGE */
BATTVOLTLO, /* battery voltage too low; MGE */
BATTVOLTHI, /* battery voltage too high; MGE */
CHARGERFAIL, /* battery charger failure; MGE */
VRANGE, /* voltage out of range */
FRANGE /* frequency out of range */
} status_bit_t;
/* --------------------------------------------------------------- */
/* Struct & data for boolean processing */
/* --------------------------------------------------------------- */
/* Note: this structure holds internal status info, directly as
collected from the hardware; not yet converted to official NUT
status or alarms */
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 internal status strings to bit masks */
{ "online", STATUS(ONLINE) },
{ "dischrg", STATUS(DISCHRG) },
{ "chrg", STATUS(CHRG) },
{ "lowbatt", STATUS(LOWBATT) },
{ "overload", STATUS(OVERLOAD) },
{ "replacebatt", STATUS(REPLACEBATT) },
{ "shutdownimm", STATUS(SHUTDOWNIMM) },
{ "trim", STATUS(TRIM) },
{ "boost", STATUS(BOOST) },
{ "bypassauto", STATUS(BYPASSAUTO) },
{ "bypassman", STATUS(BYPASSMAN) },
{ "off", STATUS(OFF) },
{ "cal", STATUS(CAL) },
{ "overheat", STATUS(OVERHEAT) },
{ "commfault", STATUS(COMMFAULT) },
{ "depleted", STATUS(DEPLETED) },
{ "timelimitexp", STATUS(TIMELIMITEXP) },
{ "fullycharged", STATUS(FULLYCHARGED) },
{ "awaitingpower", STATUS(AWAITINGPOWER) },
{ "fanfail", STATUS(FANFAIL) },
{ "nobattery", STATUS(NOBATTERY) },
{ "battvoltlo", STATUS(BATTVOLTLO) },
{ "battvolthi", STATUS(BATTVOLTHI) },
{ "chargerfail", STATUS(CHARGERFAIL) },
{ "vrange", STATUS(VRANGE) },
{ "frange", STATUS(FRANGE) },
{ NULL, 0 },
};
/* ---------------------------------------------------------------------- */
/* value lookup tables and generic lookup functions */
/* Actual value lookup tables => should be fine for all Mfrs (TODO: validate it!) */
/* the purpose of the following status conversions is to collect
information, not to interpret it. The function
process_boolean_info() remembers these values by updating the global
variable ups_status. Interpretation happens in ups_status_set,
where they are converted to standard NUT status strings. Notice
that the below conversions do not yield standard NUT status
strings; this in indicated being in lower-case characters.
The reason to separate the collection of information from its
interpretation is that not each report received from the UPS may
contain all the status flags, so they must be stored
somewhere. Also, there can be more than one status flag triggering
a certain condition (e.g. a certain UPS might have variables
low_battery, shutdown_imminent, timelimit_exceeded, and each of
these would trigger the NUT status LB. But we have to ensure that
these variables don't unset each other, so they are remembered
separately) */
info_lkp_t online_info[] = {
{ 1, "online", NULL },
{ 0, "!online", NULL },
{ 0, NULL, NULL }
};
info_lkp_t discharging_info[] = {
{ 1, "dischrg", NULL },
{ 0, "!dischrg", NULL },
{ 0, NULL, NULL }
};
info_lkp_t charging_info[] = {
{ 1, "chrg", NULL },
{ 0, "!chrg", NULL },
{ 0, NULL, NULL }
};
info_lkp_t lowbatt_info[] = {
{ 1, "lowbatt", NULL },
{ 0, "!lowbatt", NULL },
{ 0, NULL, NULL }
};
info_lkp_t overload_info[] = {
{ 1, "overload", NULL },
{ 0, "!overload", NULL },
{ 0, NULL, NULL }
};
info_lkp_t replacebatt_info[] = {
{ 1, "replacebatt", NULL },
{ 0, "!replacebatt", NULL },
{ 0, NULL, NULL }
};
info_lkp_t trim_info[] = {
{ 1, "trim", NULL },
{ 0, "!trim", NULL },
{ 0, NULL, NULL }
};
info_lkp_t boost_info[] = {
{ 1, "boost", NULL },
{ 0, "!boost", NULL },
{ 0, NULL, NULL }
};
info_lkp_t bypass_auto_info[] = {
{ 1, "bypassauto", NULL },
{ 0, "!bypassauto", NULL },
{ 0, NULL, NULL }
};
info_lkp_t bypass_manual_info[] = {
{ 1, "bypassman", NULL },
{ 0, "!bypassman", NULL },
{ 0, NULL, NULL }
};
/* note: this value is reverted (0=set, 1=not set). We report "being
off" rather than "being on", so that devices that don't implement
this variable are "on" by default */
info_lkp_t off_info[] = {
{ 0, "off", NULL },
{ 1, "!off", NULL },
{ 0, NULL, NULL }
};
info_lkp_t calibration_info[] = {
{ 1, "cal", NULL },
{ 0, "!cal", NULL },
{ 0, NULL, NULL }
};
/* note: this value is reverted (0=set, 1=not set). We report "battery
not installed" rather than "battery installed", so that devices
that don't implement this variable have a battery by default */
info_lkp_t nobattery_info[] = {
{ 1, "!nobattery", NULL },
{ 0, "nobattery", NULL },
{ 0, NULL, NULL }
};
info_lkp_t fanfail_info[] = {
{ 1, "fanfail", NULL },
{ 0, "!fanfail", NULL },
{ 0, NULL, NULL }
};
info_lkp_t shutdownimm_info[] = {
{ 1, "shutdownimm", NULL },
{ 0, "!shutdownimm", NULL },
{ 0, NULL, NULL }
};
info_lkp_t overheat_info[] = {
{ 1, "overheat", NULL },
{ 0, "!overheat", NULL },
{ 0, NULL, NULL }
};
info_lkp_t awaitingpower_info[] = {
{ 1, "awaitingpower", NULL },
{ 0, "!awaitingpower", NULL },
{ 0, NULL, NULL }
};
info_lkp_t commfault_info[] = {
{ 1, "commfault", NULL },
{ 0, "!commfault", NULL },
{ 0, NULL, NULL }
};
info_lkp_t timelimitexpired_info[] = {
{ 1, "timelimitexp", NULL },
{ 0, "!timelimitexp", NULL },
{ 0, NULL, NULL }
};
info_lkp_t battvoltlo_info[] = {
{ 1, "battvoltlo", NULL },
{ 0, "!battvoltlo", NULL },
{ 0, NULL, NULL }
};
info_lkp_t battvolthi_info[] = {
{ 1, "battvolthi", NULL },
{ 0, "!battvolthi", NULL },
{ 0, NULL, NULL }
};
info_lkp_t chargerfail_info[] = {
{ 1, "chargerfail", NULL },
{ 0, "!chargerfail", NULL },
{ 0, NULL, NULL }
};
info_lkp_t fullycharged_info[] = { /* used by CyberPower and TrippLite */
{ 1, "fullycharged", NULL },
{ 0, "!fullycharged", NULL },
{ 0, NULL, NULL }
};
info_lkp_t depleted_info[] = {
{ 1, "depleted", NULL },
{ 0, "!depleted", NULL },
{ 0, NULL, NULL }
};
info_lkp_t vrange_info[] = {
{ 0, "!vrange", NULL },
{ 1, "vrange", NULL },
{ 0, NULL, NULL }
};
info_lkp_t frange_info[] = {
{ 0, "!frange", NULL },
{ 1, "frange", NULL },
{ 0, NULL, NULL }
};
info_lkp_t test_write_info[] = {
{ 0, "No test", NULL },
{ 1, "Quick test", NULL },
{ 2, "Deep test", NULL },
{ 3, "Abort test", NULL },
{ 0, NULL, NULL }
};
info_lkp_t test_read_info[] = {
{ 1, "Done and passed", NULL },
{ 2, "Done and warning", NULL },
{ 3, "Done and error", NULL },
{ 4, "Aborted", NULL },
{ 5, "In progress", NULL },
{ 6, "No test initiated", NULL },
{ 7, "Test scheduled", NULL },
{ 0, NULL, NULL }
};
info_lkp_t beeper_info[] = {
{ 1, "disabled", NULL },
{ 2, "enabled", NULL },
{ 3, "muted", NULL },
{ 0, NULL, NULL }
};
info_lkp_t yes_no_info[] = {
{ 0, "no", NULL },
{ 1, "yes", NULL },
{ 0, NULL, NULL }
};
info_lkp_t on_off_info[] = {
{ 0, "off", NULL },
{ 1, "on", NULL },
{ 0, NULL, NULL }
};
/* returns statically allocated string - must not use it again before
done with result! */
static const char *date_conversion_fun(double value)
{
static char buf[20];
int year, month, day;
if ((long)value == 0) {
return "not set";
}
year = 1980 + ((long)value >> 9); /* negative value represents pre-1980 date */
month = ((long)value >> 5) & 0x0f;
day = (long)value & 0x1f;
snprintf(buf, sizeof(buf), "%04d/%02d/%02d", year, month, day);
return buf;
}
info_lkp_t date_conversion[] = {
{ 0, NULL, date_conversion_fun }
};
/* returns statically allocated string - must not use it again before
done with result! */
static const char *hex_conversion_fun(double value)
{
static char buf[20];
snprintf(buf, sizeof(buf), "%08lx", (long)value);
return buf;
}
info_lkp_t hex_conversion[] = {
{ 0, NULL, hex_conversion_fun }
};
/* returns statically allocated string - must not use it again before
done with result! */
static const char *stringid_conversion_fun(double value)
{
static char buf[20];
return HIDGetIndexString(udev, (int)value, buf, sizeof(buf));
}
info_lkp_t stringid_conversion[] = {
{ 0, NULL, stringid_conversion_fun }
};
/* returns statically allocated string - must not use it again before
done with result! */
static const char *divide_by_10_conversion_fun(double value)
{
static char buf[20];
snprintf(buf, sizeof(buf), "%0.1f", value * 0.1);
return buf;
}
info_lkp_t divide_by_10_conversion[] = {
{ 0, NULL, divide_by_10_conversion_fun }
};
/* returns statically allocated string - must not use it again before
done with result! */
static const char *kelvin_celsius_conversion_fun(double value)
{
static char buf[20];
/* check if the value is in the Kelvin range, to
* detect buggy value (already expressed in °C), as found
* on some HP implementation */
if ((value >= 273) && (value <= 373)) {
/* the value is indeed in °K */
snprintf(buf, sizeof(buf), "%.1f", value - 273.15);
}
else {
/* else, this is actually °C, not °K! */
snprintf(buf, sizeof(buf), "%.1f", value);
}
return buf;
}
info_lkp_t kelvin_celsius_conversion[] = {
{ 0, NULL, kelvin_celsius_conversion_fun }
};
/*!
* subdriver matcher: only useful for USB mode
* as SHUT is only supported by MGE UPS SYSTEMS units
*/
#ifndef SHUT_MODE
static int match_function_subdriver(HIDDevice_t *d, void *privdata) {
int i;
for (i=0; subdriver_list[i] != NULL; i++) {
if (subdriver_list[i]->claim(d)) {
return 1;
}
}
return 0;
}
static HIDDeviceMatcher_t subdriver_matcher_struct = {
match_function_subdriver,
NULL,
NULL
};
#endif
/* ---------------------------------------------
* driver functions implementations
* --------------------------------------------- */
/* process instant command and take action. */
int instcmd(const char *cmdname, const char *extradata)
{
hid_info_t *hidups_item;
const char *val;
double value;
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);
}
upsdebugx(1, "instcmd(%s, %s)", cmdname, extradata ? extradata : "[NULL]");
/* Retrieve and check netvar & item_path */
hidups_item = find_nut_info(cmdname);
/* Check for fallback if not found */
if (hidups_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")) {
setvar("ups.start.auto", "yes");
}
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")) {
setvar("ups.start.auto", "no");
}
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, "instcmd: info element unavailable %s\n", cmdname);
return STAT_INSTCMD_INVALID;
}
/* Check if the item is an instant command */
if (!(hidups_item->hidflags & HU_TYPE_CMD)) {
upsdebugx(2, "instcmd: %s is not an instant command\n", cmdname);
return STAT_INSTCMD_INVALID;
}
/* If extradata is empty, use the default value from the HID-to-NUT table */
val = extradata ? extradata : hidups_item->dfl;
/* Lookup the new value if needed */
if (hidups_item->hid2info != NULL) {
value = hu_find_valinfo(hidups_item->hid2info, val);
} else {
value = atol(val);
}
/* Actual variable setting */
if (HIDSetDataValue(udev, hidups_item->hiddata, value) == 1) {
upsdebugx(5, "instcmd: SUCCEED\n");
/* Set the status so that SEMI_STATIC vars are polled */
data_has_changed = TRUE;
return STAT_INSTCMD_HANDLED;
}
upsdebugx(3, "instcmd: FAILED\n"); /* TODO: HANDLED but FAILED, not UNKNOWN! */
return STAT_INSTCMD_FAILED;
}
/* set r/w variable to a value. */
int setvar(const char *varname, const char *val)
{
hid_info_t *hidups_item;
double value;
upsdebugx(1, "setvar(%s, %s)", varname, val);
/* retrieve and check netvar & item_path */
hidups_item = find_nut_info(varname);
if (hidups_item == NULL) {
upsdebugx(2, "setvar: info element unavailable %s\n", varname);
return STAT_SET_UNKNOWN;
}
/* Checking item writability and HID Path */
if (!(hidups_item->info_flags & ST_FLAG_RW)) {
upsdebugx(2, "setvar: not writable %s\n", varname);
return STAT_SET_UNKNOWN;
}
/* handle server side variable */
if (hidups_item->hidflags & HU_FLAG_ABSENT) {
upsdebugx(2, "setvar: setting server side variable %s\n", varname);
dstate_setinfo(hidups_item->info_type, "%s", val);
return STAT_SET_HANDLED;
}
/* HU_FLAG_ABSENT is the only case of HID Path == NULL */
if (hidups_item->hidpath == NULL) {
upsdebugx(2, "setvar: ID Path is NULL for %s\n", varname);
return STAT_SET_UNKNOWN;
}
/* Lookup the new value if needed */
if (hidups_item->hid2info != NULL) {
value = hu_find_valinfo(hidups_item->hid2info, val);
} else {
value = atol(val);
}
/* Actual variable setting */
if (HIDSetDataValue(udev, hidups_item->hiddata, value) == 1) {
upsdebugx(5, "setvar: SUCCEED\n");
/* Set the status so that SEMI_STATIC vars are polled */
data_has_changed = TRUE;
return STAT_SET_HANDLED;
}
upsdebugx(3, "setvar: FAILED\n"); /* FIXME: HANDLED but FAILED, not UNKNOWN! */
return STAT_SET_UNKNOWN;
}
void upsdrv_shutdown(void)
{
upsdebugx(1, "upsdrv_shutdown...");
/* Try to shutdown with delay */
if (instcmd("shutdown.return", NULL) == STAT_INSTCMD_HANDLED) {
/* Shutdown successful */
return;
}
/* If the above doesn't work, try shutdown.reboot */
if (instcmd("shutdown.reboot", NULL) == STAT_INSTCMD_HANDLED) {
/* Shutdown successful */
return;
}
/* If the above doesn't work, try load.off.delay */
if (instcmd("load.off.delay", NULL) == STAT_INSTCMD_HANDLED) {
/* Shutdown successful */
return;
}
fatalx(EXIT_FAILURE, "Shutdown failed!");
}
void upsdrv_help(void)
{
/* FIXME: to be completed */
}
void upsdrv_makevartable(void)
{
char temp [MAX_STRING_SIZE];
upsdebugx(1, "upsdrv_makevartable...");
snprintf(temp, sizeof(temp), "Set low battery level, in %% (default=%s).", DEFAULT_LOWBATT);
addvar (VAR_VALUE, HU_VAR_LOWBATT, temp);
snprintf(temp, sizeof(temp), "Set shutdown delay, in seconds (default=%s)", DEFAULT_OFFDELAY);
addvar(VAR_VALUE, HU_VAR_OFFDELAY, temp);
snprintf(temp, sizeof(temp), "Set startup delay, in seconds (default=%s)", DEFAULT_ONDELAY);
addvar(VAR_VALUE, HU_VAR_ONDELAY, temp);
snprintf(temp, sizeof(temp), "Set polling frequency, in seconds, to reduce data flow (default=%d)",
DEFAULT_POLLFREQ);
addvar(VAR_VALUE, HU_VAR_POLLFREQ, temp);
addvar(VAR_FLAG, "pollonly", "Don't use interrupt pipe, only use polling");
#ifndef SHUT_MODE
/* allow -x vendor=X, vendorid=X, product=X, productid=X, serial=X */
nut_usb_addvars();
addvar(VAR_FLAG, "explore", "Diagnostic matching of unsupported UPS");
addvar(VAR_FLAG, "maxreport", "Activate tweak for buggy APC Back-UPS firmware");
addvar(VAR_FLAG, "interruptonly", "Don't use polling, only use interrupt pipe");
addvar(VAR_VALUE, "interruptsize", "Number of bytes to read from interrupt pipe");
#else
addvar(VAR_VALUE, "notification", "Set notification type, (ignored, only for backward compatibility)");
#endif
}
#define MAX_EVENT_NUM 32
void upsdrv_updateinfo(void)
{
hid_info_t *item;
HIDData_t *event[MAX_EVENT_NUM];
int i, evtCount;
double value;
time_t now;
upsdebugx(1, "upsdrv_updateinfo...");
time(&now);
/* check for device availability to set datastale! */
if (hd == NULL) {
/* don't flood reconnection attempts */
if (now < (int)(lastpoll + poll_interval)) {
return;
}
upsdebugx(1, "Got to reconnect!\n");
if (!reconnect_ups()) {
lastpoll = now;
dstate_datastale();
return;
}
hd = &curDevice;
if (hid_ups_walk(HU_WALKMODE_INIT) == FALSE) {
hd = NULL;
return;
}
}
#ifdef DEBUG
interval();
#endif
/* Get HID notifications on Interrupt pipe first */
if (use_interrupt_pipe == TRUE) {
evtCount = HIDGetEvents(udev, event, MAX_EVENT_NUM);
switch (evtCount)
{
case -EBUSY: /* Device or resource busy */
upslog_with_errno(LOG_CRIT, "Got disconnected by another driver");
case -EPERM: /* Operation not permitted */
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! */
hd = NULL;
return;
default:
upsdebugx(1, "Got %i HID objects...", (evtCount >= 0) ? evtCount : 0);
break;
}
} else {
evtCount = 0;
upsdebugx(1, "Not using interrupt pipe...");
}
/* Process pending events (HID notifications on Interrupt pipe) */
for (i = 0; i < evtCount; i++) {
if (HIDGetDataValue(udev, event[i], &value, poll_interval) != 1)
continue;
if (nut_debug_level >= 2) {
upsdebugx(2, "Path: %s, Type: %s, ReportID: 0x%02x, Offset: %i, Size: %i, Value: %g",
HIDGetDataItem(event[i], subdriver->utab),
HIDDataType(event[i]), event[i]->ReportID,
event[i]->Offset, event[i]->Size, value);
}
/* Skip Input reports, if we don't use the Feature report */
item = find_hid_info(FindObject_with_Path(pDesc, &(event[i]->Path), interrupt_only ? ITEM_INPUT:ITEM_FEATURE));
if (!item) {
upsdebugx(3, "NUT doesn't use this HID object");
continue;
}
ups_infoval_set(item, value);
}
#ifdef DEBUG
upsdebugx(1, "took %.3f seconds handling interrupt reports...\n", interval());
#endif
/* clear status buffer before begining */
status_init();
/* Do a full update (polling) every pollfreq or upon data change (ie setvar/instcmd) */
if ((now > (lastpoll + pollfreq)) || (data_has_changed == TRUE)) {
upsdebugx(1, "Full update...");
alarm_init();
if (hid_ups_walk(HU_WALKMODE_FULL_UPDATE) == FALSE)
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 (hid_ups_walk(HU_WALKMODE_QUICK_UPDATE) == FALSE)
return;
}
ups_status_set();
status_commit();
dstate_dataok();
#ifdef DEBUG
upsdebugx(1, "took %.3f seconds handling feature reports...\n", interval());
#endif
}
void upsdrv_initinfo(void)
{
char *val;
upsdebugx(1, "upsdrv_initinfo...");
dstate_setinfo("driver.version.data", "%s", subdriver->name);
/* init polling frequency */
val = getval(HU_VAR_POLLFREQ);
if (val) {
pollfreq = atoi(val);
}
dstate_setinfo("driver.parameter.pollfreq", "%d", pollfreq);
/* ignore (broken) interrupt pipe */
if (testvar("pollonly")) {
use_interrupt_pipe = FALSE;
}
time(&lastpoll);
/* install handlers */
upsh.setvar = setvar;
upsh.instcmd = instcmd;
}
void upsdrv_initups(void)
{
int ret;
char *val;
#ifdef SHUT_MODE
/*!
* SHUT is a serial protocol, so it needs
* only the device path
*/
upsdebugx(1, "upsdrv_initups...");
subdriver_matcher = device_path;
#else
char *regex_array[6];
upsdebugx(1, "upsdrv_initups...");
subdriver_matcher = &subdriver_matcher_struct;
/* enforce use of the "vendorid" option if "explore" is given */
if (testvar("explore") && getval("vendorid")==NULL) {
fatalx(EXIT_FAILURE, "must specify \"vendorid\" when using \"explore\"");
}
/* Activate maxreport tweak */
if (testvar("maxreport")) {
max_report_size = 1;
}
/* process the UPS selection options */
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");
ret = USBNewRegexMatcher(&regex_matcher, regex_array, REG_ICASE | REG_EXTENDED);
switch(ret)
{
case 0:
break;
case -1:
fatal_with_errno(EXIT_FAILURE, "HIDNewRegexMatcher()");
default:
fatalx(EXIT_FAILURE, "invalid regular expression: %s", regex_array[ret]);
}
/* link the matchers */
subdriver_matcher->next = regex_matcher;
#endif /* SHUT_MODE */
/* Search for the first supported UPS matching the
regular expression (USB) or device_path (SHUT) */
ret = comm_driver->open(&udev, &curDevice, subdriver_matcher, &callback);
if (ret < 1)
fatalx(EXIT_FAILURE, "No matching HID UPS found");
hd = &curDevice;
upsdebugx(1, "Detected a UPS: %s/%s", hd->Vendor ? hd->Vendor : "unknown",
hd->Product ? hd->Product : "unknown");
/* Activate Powercom tweaks */
if (testvar("interruptonly")) {
interrupt_only = 1;
}
val = getval("interruptsize");
if (val) {
interrupt_size = atoi(val);
}
if (hid_ups_walk(HU_WALKMODE_INIT) == FALSE) {
fatalx(EXIT_FAILURE, "Can't initialize data from HID UPS");
}
if (dstate_getinfo("battery.charge.low")) {
/* Retrieve user defined battery settings */
val = getval(HU_VAR_LOWBATT);
if (val) {
dstate_setinfo("battery.charge.low", "%ld", strtol(val, NULL, 10));
}
}
if (dstate_getinfo("ups.delay.start")) {
/* Retrieve user defined delay settings */
val = getval(HU_VAR_ONDELAY);
if (val) {
dstate_setinfo("ups.delay.start", "%ld", strtol(val, NULL, 10));
}
}
if (dstate_getinfo("ups.delay.shutdown")) {
/* Retrieve user defined delay settings */
val = getval(HU_VAR_OFFDELAY);
if (val) {
dstate_setinfo("ups.delay.shutdown", "%ld", strtol(val, NULL, 10));
}
}
if (find_nut_info("load.off.delay")) {
/* Adds default with a delay value of '0' (= immediate) */
dstate_addcmd("load.off");
}
if (find_nut_info("load.on.delay")) {
/* Adds default with a delay value of '0' (= immediate) */
dstate_addcmd("load.on");
}
if (find_nut_info("load.off.delay") && find_nut_info("load.on.delay")) {
/* Add composite instcmds (require setting multiple HID values) */
dstate_addcmd("shutdown.return");
dstate_addcmd("shutdown.stayoff");
}
}
void upsdrv_cleanup(void)
{
upsdebugx(1, "upsdrv_cleanup...");
comm_driver->close(udev);
Free_ReportDesc(pDesc);
free_report_buffer(reportbuf);
#ifndef SHUT_MODE
USBFreeExactMatcher(exact_matcher);
USBFreeRegexMatcher(regex_matcher);
free(curDevice.Vendor);
free(curDevice.Product);
free(curDevice.Serial);
free(curDevice.Bus);
#endif
}
/**********************************************************************
* Support functions
*********************************************************************/
void possibly_supported(const char *mfr, HIDDevice_t *hd)
{
upsdebugx(0,
"This %s device (%04x:%04x) is not (or perhaps not yet) supported\n"
"by usbhid-ups. Please make sure you have an up-to-date version of NUT. If\n"
"this does not fix the problem, try running the driver with the\n"
"'-x productid=%04x' option. Please report your results to the NUT user's\n"
"mailing list <nut-upsuser@lists.alioth.debian.org>.\n",
mfr, hd->VendorID, hd->ProductID, hd->ProductID);
}
/* Update ups_status to remember this status item. Interpretation is
done in ups_status_set(). */
static void process_boolean_info(const char *nutvalue)
{
status_lkp_t *status_item;
int clear = 0;
upsdebugx(5, "process_boolean_info: %s", nutvalue);
if (*nutvalue == '!') {
nutvalue++;
clear = 1;
}
for (status_item = status_info; status_item->status_str != NULL ; status_item++)
{
if (strcasecmp(status_item->status_str, nutvalue))
continue;
if (clear) {
ups_status &= ~status_item->status_mask;
} else {
ups_status |= status_item->status_mask;
}
return;
}
upsdebugx(5, "Warning: %s not in list of known values", nutvalue);
}
static int callback(hid_dev_handle_t udev, HIDDevice_t *hd, unsigned char *rdbuf, int rdlen)
{
int i;
const char *mfr = NULL, *model = NULL, *serial = NULL;
#ifndef SHUT_MODE
int ret;
#endif
upsdebugx(2, "Report Descriptor size = %d", rdlen);
upsdebug_hex(3, "Report Descriptor", rdbuf, rdlen);
/* Parse Report Descriptor */
Free_ReportDesc(pDesc);
pDesc = Parse_ReportDesc(rdbuf, rdlen);
if (!pDesc) {
upsdebug_with_errno(1, "Failed to parse report descriptor!");
return 0;
}
/* prepare report buffer */
free_report_buffer(reportbuf);
reportbuf = new_report_buffer(pDesc);
if (!reportbuf) {
upsdebug_with_errno(1, "Failed to allocate report buffer!");
Free_ReportDesc(pDesc);
return 0;
}
/* select the subdriver for this device */
for (i=0; subdriver_list[i] != NULL; i++) {
if (subdriver_list[i]->claim(hd)) {
break;
}
}
subdriver = subdriver_list[i];
if (!subdriver) {
upsdebugx(1, "Manufacturer not supported!");
return 0;
}
upslogx(2, "Using subdriver: %s", subdriver->name);
HIDDumpTree(udev, subdriver->utab);
#ifndef SHUT_MODE
/* create a new matcher for later matching */
USBFreeExactMatcher(exact_matcher);
ret = USBNewExactMatcher(&exact_matcher, hd);
if (ret) {
upsdebug_with_errno(1, "USBNewExactMatcher()");
return 0;
}
regex_matcher->next = exact_matcher;
#endif /* SHUT_MODE */
/* apply subdriver specific formatting */
mfr = subdriver->format_mfr(hd);
model = subdriver->format_model(hd);
serial = subdriver->format_serial(hd);
if (mfr != NULL) {
dstate_setinfo("ups.mfr", "%s", mfr);
} else {
dstate_delinfo("ups.mfr");
}
if (model != NULL) {
dstate_setinfo("ups.model", "%s", model);
} else {
dstate_delinfo("ups.model");
}
if (serial != NULL) {
dstate_setinfo("ups.serial", "%s", serial);
} else {
dstate_delinfo("ups.serial");
}
dstate_setinfo("ups.vendorid", "%04x", hd->VendorID);
dstate_setinfo("ups.productid", "%04x", hd->ProductID);
return 1;
}
#ifdef DEBUG
static double interval(void)
{
struct timeval now;
static struct timeval last;
double ret;
gettimeofday(&now, NULL);
ret = now.tv_sec - last.tv_sec + ((double)(now.tv_usec - last.tv_usec)) / 1000000;
last = now;
return ret;
}
#endif
/* walk ups variables and set elements of the info array. */
static bool_t hid_ups_walk(walkmode_t mode)
{
hid_info_t *item;
double value;
int retcode;
/* 3 modes: HU_WALKMODE_INIT, HU_WALKMODE_QUICK_UPDATE and HU_WALKMODE_FULL_UPDATE */
/* Device data walk ----------------------------- */
for (item = subdriver->hid2nut; item->info_type != NULL; item++) {
#ifdef SHUT_MODE
/* Check if we are asked to stop (reactivity++) in SHUT mode.
* In USB mode, looping through this takes well under a second,
* so any effort to improve reactivity here is wasted. */
if (exit_flag != 0)
return TRUE;
#endif
/* filter data according to mode */
switch (mode)
{
/* Device capabilities enumeration */
case HU_WALKMODE_INIT:
/* Apparently, we are reconnecting, so
* NUT-to-HID translation is already good */
if (item->hiddata != NULL)
break;
/* Create the NUT-to-HID mapping */
item->hiddata = HIDGetItemData(item->hidpath, subdriver->utab);
if (item->hiddata == NULL)
continue;
/* Special case for handling server side variables */
if (item->hidflags & HU_FLAG_ABSENT) {
/* already set */
if (dstate_getinfo(item->info_type))
continue;
dstate_setinfo(item->info_type, "%s", item->dfl);
dstate_setflags(item->info_type, item->info_flags);
/* Set max length for strings, if needed */
if (item->info_flags & ST_FLAG_STRING)
dstate_setaux(item->info_type, item->info_len);
continue;
}
/* Allow duplicates for these NUT variables... */
if (!strncmp(item->info_type, "ups.alarm", 9)) {
break;
}
/* ...this one doesn't exist yet... */
if (dstate_getinfo(item->info_type) == NULL) {
break;
}
/* ...but this one does, so don't use it! */
item->hiddata = NULL;
continue;
case HU_WALKMODE_QUICK_UPDATE:
/* Quick update only deals with status and alarms! */
if (!(item->hidflags & HU_FLAG_QUICK_POLL))
continue;
break;
case HU_WALKMODE_FULL_UPDATE:
/* These don't need polling after initinfo() */
if (item->hidflags & (HU_FLAG_ABSENT | HU_TYPE_CMD | HU_FLAG_STATIC))
continue;
/* These need to be polled after user changes (setvar / instcmd) */
if ( (item->hidflags & HU_FLAG_SEMI_STATIC) && (data_has_changed == FALSE) )
continue;
break;
default:
fatalx(EXIT_FAILURE, "hid_ups_walk: unknown update mode!");
}
retcode = HIDGetDataValue(udev, item->hiddata, &value, poll_interval);
switch (retcode)
{
case -EBUSY: /* Device or resource busy */
upslog_with_errno(LOG_CRIT, "Got disconnected by another driver");
case -EPERM: /* Operation not permitted */
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! */
hd = NULL;
return FALSE;
case 1:
break; /* Found! */
case 0:
continue;
case -ETIMEDOUT: /* Connection timed out */
case -EOVERFLOW: /* Value too large for defined data type */
#ifdef EPROTO
case -EPROTO: /* Protocol error */
#endif
case -EPIPE: /* Broken pipe */
default:
/* Don't know what happened, try again later... */
continue;
}
upsdebugx(2, "Path: %s, Type: %s, ReportID: 0x%02x, Offset: %i, Size: %i, Value: %g",
item->hidpath, HIDDataType(item->hiddata), item->hiddata->ReportID,
item->hiddata->Offset, item->hiddata->Size, value);
if (item->hidflags & HU_TYPE_CMD) {
dstate_addcmd(item->info_type);
continue;
}
/* Process the value we got back (set status bits and
* set the value of other parameters) */
if (ups_infoval_set(item, value) != 1)
continue;
if (mode == HU_WALKMODE_INIT) {
info_lkp_t *info_lkp;
dstate_setflags(item->info_type, item->info_flags);
/* Set max length for strings */
if (item->info_flags & ST_FLAG_STRING) {
dstate_setaux(item->info_type, item->info_len);
}
/* Set enumerated values, only if the data has ST_FLAG_RW */
if (!(item->hidflags & HU_FLAG_ENUM) || !(item->info_flags & ST_FLAG_RW)) {
continue;
}
/* Loop on all existing values */
for (info_lkp = item->hid2info; info_lkp != NULL
&& info_lkp->nut_value != NULL; info_lkp++) {
/* Check if this value is supported */
if (hu_find_infoval(item->hid2info, info_lkp->hid_value) != NULL) {
dstate_addenum(item->info_type, "%s", info_lkp->nut_value);
}
}
}
}
return TRUE;
}
static int reconnect_ups(void)
{
int ret;
upsdebugx(4, "==================================================");
upsdebugx(4, "= device has been disconnected, try to reconnect =");
upsdebugx(4, "==================================================");
ret = comm_driver->open(&udev, &curDevice, subdriver_matcher, NULL);
if (ret > 0) {
return 1;
}
return 0;
}
/* Convert the local status information to NUT format and set NUT
alarms. */
static void ups_alarm_set(void)
{
if (ups_status & STATUS(REPLACEBATT)) {
alarm_set("Replace battery!");
}
if (ups_status & STATUS(SHUTDOWNIMM)) {
alarm_set("Shutdown imminent!");
}
if (ups_status & STATUS(FANFAIL)) {
alarm_set("Fan failure!");
}
if (ups_status & STATUS(NOBATTERY)) {
alarm_set("No battery installed!");
}
if (ups_status & STATUS(BATTVOLTLO)) {
alarm_set("Battery voltage too low!");
}
if (ups_status & STATUS(BATTVOLTHI)) {
alarm_set("Battery voltage too high!");
}
if (ups_status & STATUS(CHARGERFAIL)) {
alarm_set("Battery charger fail!");
}
if (ups_status & STATUS(OVERHEAT)) {
alarm_set("Temperature too high!"); /* overheat; Belkin, TrippLite */
}
if (ups_status & STATUS(COMMFAULT)) {
alarm_set("Internal UPS fault!"); /* UPS fault; Belkin, TrippLite */
}
if (ups_status & STATUS(AWAITINGPOWER)) {
alarm_set("Awaiting power!"); /* awaiting power; Belkin, TrippLite */
}
if (ups_status & STATUS(BYPASSAUTO)) {
alarm_set("Automatic bypass mode!");
}
if (ups_status & STATUS(BYPASSMAN)) {
alarm_set("Manual bypass mode!");
}
}
/* Convert the local status information to NUT format and set NUT
status. */
static void ups_status_set(void)
{
if (ups_status & STATUS(VRANGE)) {
dstate_setinfo("input.transfer.reason", "input voltage out of range");
} else if (ups_status & STATUS(FRANGE)) {
dstate_setinfo("input.transfer.reason", "input frequency out of range");
} else {
dstate_delinfo("input.transfer.reason");
}
if (ups_status & STATUS(ONLINE)) {
status_set("OL"); /* on line */
} else {
status_set("OB"); /* on battery */
}
if ((ups_status & STATUS(DISCHRG)) &&
!(ups_status & STATUS(DEPLETED))) {
status_set("DISCHRG"); /* discharging */
}
if ((ups_status & STATUS(CHRG)) &&
!(ups_status & STATUS(FULLYCHARGED))) {
status_set("CHRG"); /* charging */
}
if (ups_status & (STATUS(LOWBATT) | STATUS(TIMELIMITEXP) | STATUS(SHUTDOWNIMM))) {
status_set("LB"); /* low battery */
}
if (ups_status & STATUS(OVERLOAD)) {
status_set("OVER"); /* overload */
}
if (ups_status & STATUS(REPLACEBATT)) {
status_set("RB"); /* replace batt */
}
if (ups_status & STATUS(TRIM)) {
status_set("TRIM"); /* SmartTrim */
}
if (ups_status & STATUS(BOOST)) {
status_set("BOOST"); /* SmartBoost */
}
if (ups_status & (STATUS(BYPASSAUTO) | STATUS(BYPASSMAN))) {
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 */
}
}
/* find info element definition in info array
* by NUT varname.
*/
static hid_info_t *find_nut_info(const char *varname)
{
hid_info_t *hidups_item;
for (hidups_item = subdriver->hid2nut; hidups_item->info_type != NULL ; hidups_item++) {
if (strcasecmp(hidups_item->info_type, varname))
continue;
if (hidups_item->hiddata != NULL)
return hidups_item;
}
upsdebugx(2, "find_nut_info: unknown info type: %s", varname);
return NULL;
}
/* find info element definition in info array
* by HID data pointer.
*/
static hid_info_t *find_hid_info(const HIDData_t *hiddata)
{
hid_info_t *hidups_item;
for (hidups_item = subdriver->hid2nut; hidups_item->info_type != NULL ; hidups_item++) {
/* Skip server side vars */
if (hidups_item->hidflags & HU_FLAG_ABSENT)
continue;
if (hidups_item->hiddata == hiddata)
return hidups_item;
}
return NULL;
}
/* find the HID Item value matching that NUT value */
/* useful for set with value lookup... */
static long hu_find_valinfo(info_lkp_t *hid2info, const char* value)
{
info_lkp_t *info_lkp;
/* if a conversion function is defined, use 'value' as argument for it */
if (hid2info->nuf != NULL) {
double hid_value;
hid_value = hid2info->nuf(value);
upsdebugx(5, "hu_find_valinfo: found %g (value: %s)", hid_value, value);
return hid_value;
}
for (info_lkp = hid2info; info_lkp->nut_value != NULL; info_lkp++) {
if (!(strcmp(info_lkp->nut_value, value))) {
upsdebugx(5, "hu_find_valinfo: found %s (value: %ld)", info_lkp->nut_value, info_lkp->hid_value);
return info_lkp->hid_value;
}
}
upsdebugx(3, "hu_find_valinfo: no matching HID value for this INFO_* value (%s)", value);
return -1;
}
/* find the NUT value matching that HID Item value */
static const char *hu_find_infoval(info_lkp_t *hid2info, const double value)
{
info_lkp_t *info_lkp;
/* if a conversion function is defined, use 'value' as argument for it */
if (hid2info->fun != NULL) {
return hid2info->fun(value);
}
/* use 'value' as an index for a lookup in an array */
for (info_lkp = hid2info; info_lkp->nut_value != NULL; info_lkp++) {
if (info_lkp->hid_value == (long)value) {
upsdebugx(5, "hu_find_infoval: found %s (value: %ld)", info_lkp->nut_value, (long)value);
return info_lkp->nut_value;
}
}
upsdebugx(3, "hu_find_infoval: no matching INFO_* value for this HID value (%g)", value);
return NULL;
}
/* return -1 on failure, 0 for a status update and 1 in all other cases */
static int ups_infoval_set(hid_info_t *item, double value)
{
const char *nutvalue;
/* need lookup'ed translation? */
if (item->hid2info != NULL){
if ((nutvalue = hu_find_infoval(item->hid2info, value)) == NULL) {
upsdebugx(5, "Lookup [%g] failed for [%s]", value, item->info_type);
return -1;
}
/* deal with boolean items */
if (!strncmp(item->info_type, "BOOL", 4)) {
process_boolean_info(nutvalue);
return 0;
}
/* deal with alarm items */
if (!strncmp(item->info_type, "ups.alarm", 9)) {
alarm_set(nutvalue);
return 0;
}
dstate_setinfo(item->info_type, "%s", nutvalue);
} else {
dstate_setinfo(item->info_type, item->dfl, value);
}
return 1;
}