416 lines
13 KiB
C
416 lines
13 KiB
C
/* nutdrv_qx_voltronic-qs-hex.c - Subdriver for Voltronic Power UPSes with QS-Hex protocol
|
|
*
|
|
* Copyright (C)
|
|
* 2014 Daniele Pezzini <hyouko@gmail.com>
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
*
|
|
*/
|
|
|
|
#include "main.h"
|
|
#include "nutdrv_qx.h"
|
|
#include "nutdrv_qx_blazer-common.h"
|
|
|
|
#include "nutdrv_qx_voltronic-qs-hex.h"
|
|
|
|
#define VOLTRONIC_QS_HEX_VERSION "Voltronic-QS-Hex 0.03"
|
|
|
|
/* Support functions */
|
|
static int voltronic_qs_hex_claim(void);
|
|
static void voltronic_qs_hex_initups(void);
|
|
|
|
/* Answer preprocess functions */
|
|
static int voltronic_qs_hex_preprocess_qs_answer(item_t *item, const int len);
|
|
static int voltronic_qs_hex_status_char_to_binary(const unsigned char value);
|
|
|
|
/* Preprocess functions */
|
|
static int voltronic_qs_hex_protocol(item_t *item, char *value, size_t valuelen);
|
|
static int voltronic_qs_hex_input_output_voltage(item_t *item, char *value, size_t valuelen);
|
|
static int voltronic_qs_hex_input_output_voltage(item_t *item, char *value, size_t valuelen);
|
|
static int voltronic_qs_hex_load(item_t *item, char *value, size_t valuelen);
|
|
static int voltronic_qs_hex_frequency(item_t *item, char *value, size_t valuelen);
|
|
static int voltronic_qs_hex_battery_voltage(item_t *item, char *value, size_t valuelen);
|
|
|
|
|
|
/* == Ranges == */
|
|
|
|
/* Range for ups.delay.start */
|
|
static info_rw_t voltronic_qs_hex_r_ondelay[] = {
|
|
{ "60", 0 },
|
|
{ "599940", 0 },
|
|
{ "", 0 }
|
|
};
|
|
|
|
/* Range for ups.delay.shutdown */
|
|
static info_rw_t voltronic_qs_hex_r_offdelay[] = {
|
|
{ "12", 0 },
|
|
{ "540", 0 },
|
|
{ "", 0 }
|
|
};
|
|
|
|
|
|
/* == qx2nut lookup table == */
|
|
static item_t voltronic_qs_hex_qx2nut[] = {
|
|
|
|
/* Query UPS for protocol
|
|
* > [M\r]
|
|
* < [P\r]
|
|
* 01
|
|
* 0
|
|
*/
|
|
|
|
{ "ups.firmware.aux", 0, NULL, "M\r", "", 2, 0, "", 0, 0, "PMV-%s", QX_FLAG_STATIC, NULL, voltronic_qs_hex_protocol },
|
|
|
|
/* Query UPS for status
|
|
* > [QS\r]
|
|
* < [#6C01 35 6C01 35 03 519A 1312D0 E6 1E 00001001\r] (after being preprocessed)
|
|
* 01234567890123456789012345678901234567890123456
|
|
* 0 1 2 3 4
|
|
*/
|
|
|
|
{ "input.voltage", 0, NULL, "QS\r", "", 47, '#', "", 1, 7, "%.1f", 0, voltronic_qs_hex_preprocess_qs_answer, voltronic_qs_hex_input_output_voltage },
|
|
{ "output.voltage", 0, NULL, "QS\r", "", 47, '#', "", 9, 15, "%.1f", 0, voltronic_qs_hex_preprocess_qs_answer, voltronic_qs_hex_input_output_voltage },
|
|
{ "ups.load", 0, NULL, "QS\r", "", 47, '#', "", 17, 18, "%d", 0, voltronic_qs_hex_preprocess_qs_answer, voltronic_qs_hex_load },
|
|
{ "output.frequency", 0, NULL, "QS\r", "", 47, '#', "", 20, 30, "%.1f", 0, voltronic_qs_hex_preprocess_qs_answer, voltronic_qs_hex_frequency },
|
|
{ "battery.voltage", 0, NULL, "QS\r", "", 47, '#', "", 32, 36, "%.2f", 0, voltronic_qs_hex_preprocess_qs_answer, voltronic_qs_hex_battery_voltage },
|
|
/* Status bits */
|
|
{ "ups.status", 0, NULL, "QS\r", "", 47, '#', "", 38, 38, NULL, QX_FLAG_QUICK_POLL, voltronic_qs_hex_preprocess_qs_answer, blazer_process_status_bits }, /* Utility Fail (Immediate) */
|
|
{ "ups.status", 0, NULL, "QS\r", "", 47, '#', "", 39, 39, NULL, QX_FLAG_QUICK_POLL, voltronic_qs_hex_preprocess_qs_answer, blazer_process_status_bits }, /* Battery Low */
|
|
{ "ups.status", 0, NULL, "QS\r", "", 47, '#', "", 40, 40, NULL, QX_FLAG_QUICK_POLL, voltronic_qs_hex_preprocess_qs_answer, blazer_process_status_bits }, /* Bypass/Boost or Buck Active */
|
|
{ "ups.alarm", 0, NULL, "QS\r", "", 47, '#', "", 41, 41, NULL, 0, voltronic_qs_hex_preprocess_qs_answer, blazer_process_status_bits }, /* UPS Failed */
|
|
{ "ups.type", 0, NULL, "QS\r", "", 47, '#', "", 42, 42, "%s", QX_FLAG_STATIC, voltronic_qs_hex_preprocess_qs_answer, blazer_process_status_bits }, /* UPS Type */
|
|
{ "ups.status", 0, NULL, "QS\r", "", 47, '#', "", 43, 43, NULL, QX_FLAG_QUICK_POLL, voltronic_qs_hex_preprocess_qs_answer, blazer_process_status_bits }, /* Test in Progress */
|
|
{ "ups.status", 0, NULL, "QS\r", "", 47, '#', "", 44, 44, NULL, QX_FLAG_QUICK_POLL, voltronic_qs_hex_preprocess_qs_answer, blazer_process_status_bits }, /* Shutdown Active */
|
|
{ "ups.beeper.status", 0, NULL, "QS\r", "", 47, '#', "", 45, 45, "%s", 0, voltronic_qs_hex_preprocess_qs_answer, blazer_process_status_bits }, /* Beeper status */
|
|
|
|
/* Instant commands */
|
|
{ "beeper.toggle", 0, NULL, "Q\r", "", 0, 0, "", 0, 0, NULL, QX_FLAG_CMD, NULL, NULL },
|
|
{ "load.off", 0, NULL, "S00R0000\r", "", 0, 0, "", 0, 0, NULL, QX_FLAG_CMD, NULL, NULL },
|
|
{ "load.on", 0, NULL, "C\r", "", 0, 0, "", 0, 0, NULL, QX_FLAG_CMD, NULL, NULL },
|
|
{ "shutdown.return", 0, NULL, "S%s\r", "", 0, 0, "", 0, 0, NULL, QX_FLAG_CMD, NULL, blazer_process_command },
|
|
{ "shutdown.stayoff", 0, NULL, "S%sR0000\r", "", 0, 0, "", 0, 0, NULL, QX_FLAG_CMD, NULL, blazer_process_command },
|
|
{ "shutdown.stop", 0, NULL, "C\r", "", 0, 0, "", 0, 0, NULL, QX_FLAG_CMD, NULL, NULL },
|
|
{ "test.battery.start.quick", 0, NULL, "T\r", "", 0, 0, "", 0, 0, NULL, QX_FLAG_CMD | QX_FLAG_SKIP, NULL, NULL },
|
|
|
|
/* Server-side settable vars */
|
|
{ "ups.delay.start", ST_FLAG_RW, voltronic_qs_hex_r_ondelay, NULL, "", 0, 0, "", 0, 0, DEFAULT_ONDELAY, QX_FLAG_ABSENT | QX_FLAG_SETVAR | QX_FLAG_RANGE, NULL, blazer_process_setvar },
|
|
{ "ups.delay.shutdown", ST_FLAG_RW, voltronic_qs_hex_r_offdelay, NULL, "", 0, 0, "", 0, 0, DEFAULT_OFFDELAY, QX_FLAG_ABSENT | QX_FLAG_SETVAR | QX_FLAG_RANGE, NULL, blazer_process_setvar },
|
|
|
|
/* End of structure. */
|
|
{ NULL, 0, NULL, NULL, "", 0, 0, "", 0, 0, NULL, 0, NULL, NULL }
|
|
};
|
|
|
|
|
|
/* == Testing table == */
|
|
#ifdef TESTING
|
|
static testing_t voltronic_qs_hex_testing[] = {
|
|
{ "QS\r", "#\x6C\x01 \x35 \x6C\x01 \x35 \x03 \x51\x9A \x28\x02\x12\xD0 \xE6 \x1E \x09\r", 27 },
|
|
{ "M\r", "P\r", -1 },
|
|
{ "Q\r", "", -1 },
|
|
{ "S03\r", "", -1 },
|
|
{ "C\r", "", -1 },
|
|
{ "S02R0005\r", "", -1 },
|
|
{ "S.5R0000\r", "N\r", -1 },
|
|
{ "T04\r", "", -1 },
|
|
{ "TL\r", "", -1 },
|
|
{ "T\r", "", -1 },
|
|
{ "CT\r", "", -1 },
|
|
{ NULL }
|
|
};
|
|
#endif /* TESTING */
|
|
|
|
|
|
/* == Support functions == */
|
|
|
|
/* This function allows the subdriver to "claim" a device: return 1 if the device is supported by this subdriver, else 0. */
|
|
static int voltronic_qs_hex_claim(void)
|
|
{
|
|
/* We need at least M and QS to run this subdriver */
|
|
|
|
/* UPS Protocol */
|
|
item_t *item = find_nut_info("ups.firmware.aux", 0, 0);
|
|
|
|
/* Don't know what happened */
|
|
if (!item)
|
|
return 0;
|
|
|
|
/* No reply/Unable to get value */
|
|
if (qx_process(item, NULL))
|
|
return 0;
|
|
|
|
/* Unable to process value/Protocol not supported */
|
|
if (ups_infoval_set(item) != 1)
|
|
return 0;
|
|
|
|
item = find_nut_info("input.voltage", 0, 0);
|
|
|
|
/* Don't know what happened */
|
|
if (!item) {
|
|
dstate_delinfo("ups.firmware.aux");
|
|
return 0;
|
|
}
|
|
|
|
/* No reply/Unable to get value */
|
|
if (qx_process(item, NULL)) {
|
|
dstate_delinfo("ups.firmware.aux");
|
|
return 0;
|
|
}
|
|
|
|
/* Unable to process value */
|
|
if (ups_infoval_set(item) != 1) {
|
|
dstate_delinfo("ups.firmware.aux");
|
|
return 0;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
/* Subdriver-specific initups */
|
|
static void voltronic_qs_hex_initups(void)
|
|
{
|
|
blazer_initups_light(voltronic_qs_hex_qx2nut);
|
|
}
|
|
|
|
|
|
/* == Answer preprocess functions == */
|
|
|
|
/* Preprocess the answer we got back from the UPS when queried with 'QS\r' */
|
|
static int voltronic_qs_hex_preprocess_qs_answer(item_t *item, const int len)
|
|
{
|
|
int i, token;
|
|
char refined[SMALLBUF] = "";
|
|
|
|
if (len <= 0)
|
|
return len;
|
|
|
|
if (item->answer[0] != '#') {
|
|
upsdebugx(4, "%s: wrong leading character [%s: 0x%0x]", __func__, item->info_type, item->answer[0]);
|
|
return -1;
|
|
}
|
|
|
|
snprintf(refined, sizeof(refined), "%s", "#");
|
|
|
|
/* e.g.: item->answer = "#\x6C\x01 \x35 \x6C\x01 \x35 \x03 \x51\x9A \x28\x02\x12\xD0 \xE6 \x1E \x09\r" */
|
|
upsdebug_hex(4, "read", item->answer, len);
|
|
|
|
for (i = 1, token = 1; i < len; i++) {
|
|
|
|
/* New token */
|
|
if (item->answer[i] == 0x20) {
|
|
snprintfcat(refined, sizeof(refined), "%s", " ");
|
|
token++;
|
|
continue;
|
|
}
|
|
|
|
/* 'Unescape' raw data */
|
|
if (item->answer[i] == 0x28 && i < len) {
|
|
|
|
switch (item->answer[i + 1])
|
|
{
|
|
case 0x00: /* Escaped because: CR */
|
|
snprintfcat(refined, sizeof(refined), "%02x", 0x0D);
|
|
break;
|
|
case 0x01: /* Escaped because: XON */
|
|
snprintfcat(refined, sizeof(refined), "%02x", 0x11);
|
|
break;
|
|
case 0x02: /* Escaped because: XOFF */
|
|
snprintfcat(refined, sizeof(refined), "%02x", 0x13);
|
|
break;
|
|
case 0x03: /* Escaped because: LF */
|
|
snprintfcat(refined, sizeof(refined), "%02x", 0x0A);
|
|
break;
|
|
case 0x04: /* Escaped because: space */
|
|
snprintfcat(refined, sizeof(refined), "%02x", 0x20);
|
|
break;
|
|
default:
|
|
if (token != 10)
|
|
snprintfcat(refined, sizeof(refined), "%02x", ((unsigned char *)item->answer)[i]);
|
|
else
|
|
snprintfcat(refined, sizeof(refined), "%08d", voltronic_qs_hex_status_char_to_binary(((unsigned char *)item->answer)[i]));
|
|
continue;
|
|
}
|
|
|
|
i++;
|
|
continue;
|
|
|
|
}
|
|
|
|
/* Trailing CR */
|
|
if (item->answer[i] == 0x0D)
|
|
break;
|
|
|
|
if (token != 10)
|
|
snprintfcat(refined, sizeof(refined), "%02x", ((unsigned char *)item->answer)[i]);
|
|
else
|
|
snprintfcat(refined, sizeof(refined), "%08d", voltronic_qs_hex_status_char_to_binary(((unsigned char *)item->answer)[i]));
|
|
|
|
}
|
|
|
|
if (token != 10 || strlen(refined) != 46) {
|
|
upsdebugx(2, "noncompliant reply: %s", refined);
|
|
return -1;
|
|
}
|
|
|
|
upsdebugx(4, "read: %s", refined);
|
|
|
|
/* e.g.: item->answer = "#6C01 35 6C01 35 03 519A 1312D0 E6 1E 00001001" */
|
|
return snprintf(item->answer, sizeof(item->answer), "%s\r", refined);
|
|
}
|
|
|
|
/* Transform the QS 'status' char into its binary form (as an int) */
|
|
static int voltronic_qs_hex_status_char_to_binary(const unsigned char value)
|
|
{
|
|
unsigned char remainder = value;
|
|
int ret = 0,
|
|
power = 1;
|
|
|
|
while (remainder) {
|
|
|
|
if (remainder & 1)
|
|
ret += power;
|
|
|
|
power *= 10;
|
|
remainder >>= 1;
|
|
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
|
|
/* == Preprocess functions == */
|
|
|
|
/* Protocol used by the UPS */
|
|
static int voltronic_qs_hex_protocol(item_t *item, char *value, size_t valuelen)
|
|
{
|
|
item_t *unskip;
|
|
|
|
if (strcasecmp(item->value, "P") && strcasecmp(item->value, "T") && strcasecmp(item->value, "V")) {
|
|
upsdebugx(2, "%s: invalid protocol [%s]", __func__, item->value);
|
|
return -1;
|
|
}
|
|
|
|
snprintf(value, valuelen, item->dfl, item->value);
|
|
|
|
/* 'P' UPSes don't support 'T\r' command (battery test) -> leave test.battery.start.quick skipped */
|
|
if (!strcasecmp(item->value, "P"))
|
|
return 0;
|
|
|
|
/* Unskip test.battery.start.quick */
|
|
unskip = find_nut_info("test.battery.start.quick", QX_FLAG_CMD, 0);
|
|
|
|
/* Don't know what happened */
|
|
if (!unskip)
|
|
return -1;
|
|
|
|
unskip->qxflags &= ~QX_FLAG_SKIP;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* Input/Output voltage */
|
|
int voltronic_qs_hex_input_output_voltage(item_t *item, char *value, size_t valuelen)
|
|
{
|
|
int val;
|
|
double ret;
|
|
char *str_end, buf[SMALLBUF] = "";
|
|
|
|
if (strspn(item->value, "0123456789ABCDEFabcdef ") != strlen(item->value)) {
|
|
upsdebugx(2, "%s: non numerical value [%s: %s]", __func__, item->info_type, item->value);
|
|
return -1;
|
|
}
|
|
|
|
val = strtol(item->value, &str_end, 16) * strtol(str_end, NULL, 16) / 51;
|
|
snprintf(buf, sizeof(buf), "%06x", val);
|
|
|
|
ret = strtol(buf + 4, NULL, 16) / 256.0;
|
|
buf[4] = '\0';
|
|
ret += strtol(buf, NULL, 16);
|
|
|
|
snprintf(value, valuelen, item->dfl, ret);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* Device load */
|
|
int voltronic_qs_hex_load(item_t *item, char *value, size_t valuelen)
|
|
{
|
|
if (strspn(item->value, "0123456789ABCDEFabcdef") != strlen(item->value)) {
|
|
upsdebugx(2, "%s: non numerical value [%s: %s]", __func__, item->info_type, item->value);
|
|
return -1;
|
|
}
|
|
|
|
snprintf(value, valuelen, item->dfl, strtol(item->value, NULL, 16));
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* Output frequency */
|
|
int voltronic_qs_hex_frequency(item_t *item, char *value, size_t valuelen)
|
|
{
|
|
double val1, val2, ret;
|
|
char *str_end;
|
|
|
|
if (strspn(item->value, "0123456789ABCDEFabcdef ") != strlen(item->value)) {
|
|
upsdebugx(2, "%s: non numerical value [%s: %s]", __func__, item->info_type, item->value);
|
|
return -1;
|
|
}
|
|
|
|
val1 = strtol(item->value, &str_end, 16);
|
|
val2 = strtol(str_end, NULL, 16);
|
|
|
|
ret = val2 / val1;
|
|
ret = ret > 99.9 ? 99.9 : ret;
|
|
|
|
snprintf(value, valuelen, item->dfl, ret);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* Battery voltage */
|
|
int voltronic_qs_hex_battery_voltage(item_t *item, char *value, size_t valuelen)
|
|
{
|
|
int val1, val2;
|
|
char *str_end;
|
|
|
|
if (strspn(item->value, "0123456789ABCDEFabcdef ") != strlen(item->value)) {
|
|
upsdebugx(2, "%s: non numerical value [%s: %s]", __func__, item->info_type, item->value);
|
|
return -1;
|
|
}
|
|
|
|
val1 = strtol(item->value, &str_end, 16);
|
|
val2 = strtol(str_end, NULL, 16);
|
|
|
|
snprintf(value, valuelen, item->dfl, (val1 * val2) / 510.0);
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
/* == Subdriver interface == */
|
|
subdriver_t voltronic_qs_hex_subdriver = {
|
|
VOLTRONIC_QS_HEX_VERSION,
|
|
voltronic_qs_hex_claim,
|
|
voltronic_qs_hex_qx2nut,
|
|
voltronic_qs_hex_initups,
|
|
NULL,
|
|
blazer_makevartable_light,
|
|
NULL,
|
|
"N\r",
|
|
#ifdef TESTING
|
|
voltronic_qs_hex_testing,
|
|
#endif /* TESTING */
|
|
};
|