/* nutdrv_qx_masterguard.c - Subdriver for Masterguard A/E Series * * Copyright (C) * 2020-2021 Edgar Fuß , Mathematisches Institut der Universität Bonn * * 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, or a 2-clause BSD License. * * 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_masterguard.h" #include #define MASTERGUARD_VERSION "Masterguard 0.02" /* series (for un-SKIP) */ static char masterguard_my_series = '?'; /* slave address for commands that require it */ static char masterguard_my_slaveaddr[3] = "??"; /* null-terminated for strtol() in claim() */ /* next slaveaddr to use after the SS command (which needs the old one) has been run */ static long masterguard_next_slaveaddr; /* output current/power computation */ static long masterguard_my_power = 0; /* battery voltage computation */ static long masterguard_my_numcells = 0; /* ranges */ static info_rw_t masterguard_r_slaveaddr[] = { { "0", NULL }, { "99", NULL }, { "" , NULL } }; static info_rw_t masterguard_r_batpacks[] = { { "0", NULL }, /* actually 1 for most models, see masterguard_model() */ { "9", NULL }, /* varies across models, see masterguard_model() */ { "" , NULL } }; static info_rw_t masterguard_r_offdelay[] = { { "0", NULL }, { "5940", NULL }, /* 99*60 */ { "" , NULL } }; static info_rw_t masterguard_r_ondelay[] = { { "0", NULL }, { "599940", NULL }, /* 9999*60 */ { "" , NULL } }; /* enums */ static info_rw_t *masterguard_e_outvolts = NULL; /* set in masterguard_output_voltages() */ /* preprocess functions */ /* set masterguard_my_slaveaddr (for masterguard_add_slaveaddr) */ static int masterguard_slaveaddr(item_t *item, char *value, const size_t valuelen) { if (strlen(item->value) != 2) { upsdebugx(2, "slaveaddr length not 2"); return -1; } memcpy(masterguard_my_slaveaddr, item->value, 2); if (valuelen >= 3) memcpy(value, item->value, 3); return 0; } /* set masterguard_my_series (for activating supported commands in masterguard_claim() */ static int masterguard_series(item_t *item, char *value, const size_t valuelen) { NUT_UNUSED_VARIABLE(valuelen); switch (item->value[0]) { case 'A': break; case 'E': break; default: upsdebugx(2, "unknown series %s", item->value); return -1; } masterguard_my_series = item->value[0]; memcpy(value, item->value, 2); return 0; } /* Convert strangely formatted model name in WH output * (spaces, -19 only after battery packs) to something readable * Also set min/max battery packs according to model */ static int masterguard_model(item_t *item, char *value, const size_t valuelen) { char *model; int rack; char min_bp, max_bp; rack = (strstr(item->value, "-19") != NULL); if (strncmp(item->value, "A 700", 6) == 0) { model = "A700"; min_bp = 0; max_bp = 0; } else if (strncmp(item->value, "A 1000", 6) == 0) { model = "A1000"; min_bp = 0; max_bp = 2; } else if (strncmp(item->value, "A 2000", 6) == 0) { model = "A2000"; min_bp = rack ? 1 : 0; max_bp = rack ? 5 : 2; } else if (strncmp(item->value, "A 3000", 6) == 0) { model = "A3000"; min_bp = rack ? 1 : 0; max_bp = rack ? 5 : 2; } else if (strncmp(item->value, "E 60", 4) == 0) { model = "E60"; min_bp = 1; max_bp = 1; /* ??? */ } else if (strncmp(item->value, "E100", 4) == 0) { model = "E100"; min_bp = 1; max_bp = 1; /* ??? */ } else if (strncmp(item->value, "E200", 4) == 0) { model = "E200"; min_bp = 1; max_bp = 1; /* ??? */ } else { upsdebugx(2, "unknown T %s", item->value); return -1; } masterguard_r_batpacks[0].value[0] = '0' + min_bp; masterguard_r_batpacks[1].value[0] = '0' + max_bp; snprintf(value, valuelen, "%s%s", model, rack ? "-19" : ""); return 0; } /* set masterguard_my_power (for power/current calculations) according to model */ static int masterguard_power(item_t *item, char *value, const size_t valuelen) { int p; if (strncmp(item->value, "A 700", 6) == 0) { p = 700; } else if (strncmp(item->value, "A 1000", 6) == 0) { p = 1000; } else if (strncmp(item->value, "A 2000", 6) == 0) { p = 2000; } else if (strncmp(item->value, "A 3000", 6) == 0) { p = 3000; } else if (strncmp(item->value, "E 60", 4) == 0) { p = 6000; } else if (strncmp(item->value, "E100", 4) == 0) { p = 10000; } else if (strncmp(item->value, "E200", 4) == 0) { p = 20000; } else { upsdebugx(2, "unknown T %s", item->value); return -1; } masterguard_my_power = p; snprintf(value, valuelen, "%d", p); return 0; } /* convert mmm.ss to seconds */ static int masterguard_mmm_ss(item_t *item, char *value, const size_t valuelen) { int m, s; if (sscanf(item->value, "%d.%d", &m, &s) != 2) { upsdebugx(2, "unparsable mmm.ss %s", item->value); return -1; } snprintf(value, valuelen, "%d", 60*m + s); return 0; } /* convert hhh to seconds */ static int masterguard_hhh(item_t *item, char *value, const size_t valuelen) { int h; if (sscanf(item->value, "%d", &h) != 1) { upsdebugx(2, "unparsable hhh %s", item->value); return -1; } snprintf(value, valuelen, "%d", 60*60*h); return 0; } /* convert TTTT:hh:mm:dd to seconds */ static int masterguard_tttt_hh_mm_ss(item_t *item, char *value, const size_t valuelen) { int t, h, m, s; if (sscanf(item->value, "%d:%d:%d:%d", &t, &h, &m, &s) != 4) { upsdebugx(2, "unparsable TTTT:hh:mm:ss %s", item->value); return -1; } snprintf(value, valuelen, "%d", 86400*t + 3600*h + 60*m + s); return 0; } /* set masterguard_my_numcells (for nominal battery voltage computation) */ static int masterguard_numcells(item_t *item, char *value, const size_t valuelen) { int v; if (sscanf(item->value, "%d", &v) != 1) { upsdebugx(2, "unparsable vvv %s", item->value); return -1; } masterguard_my_numcells = v; snprintf(value, valuelen, "%d", v); return 0; } /* compute nominal battery voltage */ static int masterguard_battvolt(item_t *item, char *value, const size_t valuelen) { float s; if (sscanf(item->value, "%f", &s) != 1) { upsdebugx(2, "unparsable ss.ss %s", item->value); return -1; } snprintf(value, valuelen, "%.2f", masterguard_my_numcells * s); return 0; } /* compute output power from load percentage */ static int masterguard_ups_power(item_t *item, char *value, const size_t valuelen) { int q; if (sscanf(item->value, "%d", &q) != 1) { upsdebugx(2, "unparsable qqq %s", item->value); return -1; } snprintf(value, valuelen, "%.0f", q / 100.0 * masterguard_my_power + 0.5); return 0; } /* helper routine, not to be called from table */ static int masterguard_output_current_fraction(item_t *item, char *value, const size_t valuelen, double fraction) { NUT_UNUSED_VARIABLE(item); snprintf(value, valuelen, "%.2f", fraction * masterguard_my_power / strtod(dstate_getinfo("output.voltage") , NULL) + 0.005); return 0; } /* compute output current from load percentage and output voltage */ static int masterguard_output_current(item_t *item, char *value, const size_t valuelen) { int q; if (sscanf(item->value, "%d", &q) != 1) { upsdebugx(2, "unparsable qqq %s", item->value); return -1; } return masterguard_output_current_fraction(item, value, valuelen, q/100.0); } /* compute nominal output current from output voltage */ static int masterguard_output_current_nominal(item_t *item, char *value, const size_t valuelen) { return masterguard_output_current_fraction(item, value, valuelen, 1.0); } /* digest status bits */ static int masterguard_status(item_t *item, char *value, const size_t valuelen) { int neg; char *s; switch (item->value[0]) { case '0': neg = 1; break; case '1': neg = 0; break; default: upsdebugx(2, "unknown flag value %c", item->value[0]); return -1; } switch (item->from) { case 53: /* B7 */ s = "OL"; neg = !neg; break; case 54: /* B6 */ s = "LB"; break; case 55: /* B5 */ s = "BYPASS"; break; case 56: /* B4 */ s = neg ? "" : "UPS Failed"; neg = 0; break; case 57: /* B3 */ s = neg ? "online" : "offline"; neg = 0; break; case 58: /* B2 */ s = "CAL"; break; case 59: /* B1 */ s = "FSD"; break; /* 60: blank */ /* 61: B0 reserved */ /* 62: T7 reserved */ case 63: /* T6 */ s = neg ? "" : "problems in parallel operation mode"; neg = 0; break; /* 64: T5 part of a parallel set */ case 65: /* T4 */ s = "RB"; break; case 66: /* T3 */ s = neg ? "" : "no battery connected"; neg = 0; break; case 67: /* T210 */ neg = 0; if (strncmp(item->value, "000", 3) == 0) { s = "no test in progress"; } else if (strncmp(item->value, "001", 3) == 0) { s = "in progress"; } else if (strncmp(item->value, "010", 3) == 0) { s = "OK"; } else if (strncmp(item->value, "011", 3) == 0) { s = "failed"; } else if (strncmp(item->value, "100", 3) == 0) { s = "not possible"; } else if (strncmp(item->value, "101", 3) == 0) { s = "aborted"; } else if (strncmp(item->value, "110", 3) == 0) { s = "autonomy time calibration in progress"; } else if (strncmp(item->value, "111", 3) == 0) { s = "unknown"; } else { upsdebugx(2, "unknown test result %s", item->value); return -1; } break; default: upsdebugx(2, "unknown flag position %d", item->from); return -1; } snprintf(value, valuelen, "%s%s", neg ? "!" : "", s); return 0; } /* convert beeper status bit to string required by NUT */ static int masterguard_beeper_status(item_t *item, char *value, const size_t valuelen) { switch (item->value[0]) { case '0': if (valuelen >= 9) strcpy(value, "disabled"); else *value = '\0'; break; case '1': if (valuelen >= 8) strcpy(value, "enabled"); else *value = '\0'; break; default: upsdebugx(2, "unknown beeper status %c", item->value[0]); return -1; } return 0; } /* parse list of available (nominal) output voltages into masterguard_w_outvolts enum */ static int masterguard_output_voltages(item_t *item, char *value, const size_t valuelen) { char sep[] = " "; char *w; size_t n = 0; strncpy(value, item->value, valuelen); /* save before strtok mangles it */ for (w = strtok(item->value, sep); w; w = strtok(NULL, sep)) { n++; upsdebugx(4, "output voltage #%zu: %s", n, w); if ((masterguard_e_outvolts = realloc(masterguard_e_outvolts, n * sizeof(info_rw_t))) == NULL) { upsdebugx(1, "output voltages: allocating #%zu failed", n); return -1; } strncpy(masterguard_e_outvolts[n - 1].value, w, SMALLBUF - 1); masterguard_e_outvolts[n - 1].preprocess = NULL; } /* need to do this seperately in case the loop is run zero times */ if ((masterguard_e_outvolts = realloc(masterguard_e_outvolts, (n + 1) * sizeof(info_rw_t))) == NULL) { upsdebugx(1, "output voltages: allocating terminator after #%zu failed", n); return -1; } masterguard_e_outvolts[n].value[0] = '\0'; masterguard_e_outvolts[n].preprocess = NULL; return 0; } /* parse fault record string into readable form */ static int masterguard_fault(item_t *item, char *value, const size_t valuelen) { char c; float f; int t, h, m, s; long l; if (sscanf(item->value, "%c %f %d:%d:%d:%d", &c, &f, &t, &h, &m, &s) != 6) { upsdebugx(1, "unparsable fault record %s", item->value); return -1; } l = 86400*t + 3600*h + 60*m + s; snprintf(value, valuelen, "%ld: ", l); switch (c) { case '0': snprintfcat(value, valuelen, "none"); break; case '1': snprintfcat(value, valuelen, "bus fault (%.0fV)", f); break; case '2': snprintfcat(value, valuelen, "inverter fault (%.0fV)", f); break; case '3': snprintfcat(value, valuelen, "overheat fault (%.0fC)", f); break; case '4': snprintfcat(value, valuelen, "battery overvoltage fault (%.2fV)", f); break; case '5': snprintfcat(value, valuelen, "battery mode overload fault (%.0f%%)", f); break; case '6': snprintfcat(value, valuelen, "bypass mode overload fault (%.0f%%)", f); break; case '7': snprintfcat(value, valuelen, "inverter mode outpt short-circuit fault (%.0fV)", f); break; case '8': snprintfcat(value, valuelen, "fan lock fault"); break; case '9': snprintfcat(value, valuelen, "battery fault (%.0fV)", f); break; case 'A': snprintfcat(value, valuelen, "charger fault"); break; case 'B': snprintfcat(value, valuelen, "EPO activated"); break; case 'C': snprintfcat(value, valuelen, "parallel error"); break; case 'D': snprintfcat(value, valuelen, "MCU communication error"); break; case 'E': case 'F': upsdebugx(1, "reserved fault id %c", c); return -1; default: upsdebugx(1, "unknown fault id %c", c); return -1; } return 0; } /* pre-command preprocessing functions */ /* add slave address (from masterguard_my_slaveaddr) to commands that require it */ static int masterguard_add_slaveaddr(item_t *item, char *command, const size_t commandlen) { NUT_UNUSED_VARIABLE(item); NUT_UNUSED_VARIABLE(commandlen); size_t l; l = strlen(command); if (strncmp(command + l - 4, ",XX\r", 4) != 0) { upsdebugx(1, "add slaveaddr: no ,XX\\r at end of command %s", command); return -1; } upsdebugx(4, "add slaveaddr %s to command %s", masterguard_my_slaveaddr, command); memcpy(command + l - 3, masterguard_my_slaveaddr, 2); return 0; } /* instant command preprocessing functions */ /* helper, not to be called directly from table */ /*!! use parameter from the value field instead of ups.delay.{shutdown,return}?? */ static int masterguard_shutdown(item_t *item, char *value, const size_t valuelen, const int stayoff) { NUT_UNUSED_VARIABLE(item); long offdelay; char *p; const char *val, *name; char offstr[3]; offdelay = strtol((val = dstate_getinfo(name = "ups.delay.shutdown")), &p, 10); if (*p != '\0') goto ill; if (offdelay < 0) { goto ill; } else if (offdelay < 60) { offstr[0] = '.'; offstr[1] = '0' + (char)offdelay / 6; } else if (offdelay <= 99*60) { int m = (int)(offdelay / 60); offstr[0] = '0' + (char)(m / 10); offstr[1] = '0' + (char)(m % 10); } else goto ill; offstr[2] = '\0'; if (stayoff) { snprintf(value, valuelen, "S%s\r", offstr); } else { long ondelay; ondelay = strtol((val = dstate_getinfo(name = "ups.delay.start")), &p, 10); if (*p != '\0') goto ill; if (ondelay < 0 || ondelay > 9999*60) goto ill; snprintf(value, valuelen, "S%sR%04ld\r", offstr, ondelay); } return 0; ill: upsdebugx(2, "shutdown: illegal %s %s", name, val); return -1; } static int masterguard_shutdown_return(item_t *item, char *value, const size_t valuelen) { return masterguard_shutdown(item, value, valuelen, 0); } static int masterguard_shutdown_stayoff(item_t *item, char *value, const size_t valuelen) { return masterguard_shutdown(item, value, valuelen, 1); } static int masterguard_test_battery(item_t *item, char *value, const size_t valuelen) { NUT_UNUSED_VARIABLE(item); long duration; char *p; if (value[0] == '\0') { upsdebugx(2, "battery test: no duration"); return -1; } duration = strtol(value, &p, 10); if (*p != '\0') goto ill; if (duration == 10) { strncpy(value, "T\r", valuelen); return 0; } if (duration < 60 || duration > 99*60) goto ill; snprintf(value, valuelen, "T%02ld\r", duration / 60); return 0; ill: upsdebugx(2, "battery test: illegal duration %s", value); return -1; } /* variable setting preprocessing functions */ /* set variable, input format specifier (d/f/s, thms) in item->dfl */ static int masterguard_setvar(item_t *item, char *value, const size_t valuelen) { char *p; char t = 's'; long i = 0; double f = 0.0; char s[80]; if (value[0] == '\0') { upsdebugx(2, "setvar: no value"); return -1; } if (!item->dfl || item->dfl[0] == '\0') { upsdebugx(2, "setvar: no dfl"); return -1; } if (item->dfl[1] == '\0') { t = item->dfl[0]; switch (t) { case 'd': i = strtol(value, &p, 10); if (*p != '\0') goto ill; break; case 'f': f = strtod(value, &p); if (*p != '\0') { goto ill; } else if (errno) { upsdebug_with_errno(2, "setvar: f value %s", value); return -1; } break; case 's': /* copy to s to avoid snprintf()ing value to itself */ if (strlen(value) >= sizeof s) goto ill; strcpy(s, value); break; default: upsdebugx(2, "setvar: unknown dfl %c", item->dfl[0]); return -1; } } else if (strncmp(item->dfl, "thms", 4) == 0) { int tt, h, m, sec; if (sscanf(item->value, "%d:%d:%d:%d", &tt, &h, &m, &sec) == 4) { if (tt < 0 || tt > 9999 || h < 0 || h > 23 || m < 0 || m > 59 || sec < 0 || sec > 59) goto ill; } else { long l; char *pl; l = strtol(value, &pl, 10); if (*pl != '\0') goto ill; sec = l % 60; l /= 60; m = l % 60; l /= 60; h = l % 24; l /= 24; if (l > 9999) goto ill; tt = (int)l; } snprintf(s, sizeof s, "%04d:%02d:%02d:%02d", tt, h, m, sec); } else { upsdebugx(2, "setvar: unknown dfl %s", item->dfl); return -1; } #ifdef HAVE_PRAGMAS_FOR_GCC_DIAGNOSTIC_IGNORED_FORMAT_NONLITERAL #pragma GCC diagnostic push #endif #ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_FORMAT_NONLITERAL #pragma GCC diagnostic ignored "-Wformat-nonliteral" #endif #ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_FORMAT_SECURITY #pragma GCC diagnostic ignored "-Wformat-security" #endif switch (t) { case 'd': snprintf(value, valuelen, item->command, i); break; case 'f': snprintf(value, valuelen, item->command, f); break; case 's': snprintf(value, valuelen, item->command, s); break; } #ifdef HAVE_PRAGMAS_FOR_GCC_DIAGNOSTIC_IGNORED_FORMAT_NONLITERAL #pragma GCC diagnostic pop #endif return 0; ill: upsdebugx(2, "setvar: illegal %s value %s", item->dfl, value); return -1; } /* record new slave address in masterguard_next_slaveaddr; moved to masterguard_my_slaveaddr in masterguard_new_slaveaddr() after the slaveaddr-changing command finished */ static int masterguard_set_slaveaddr(item_t *item, char *value, const size_t valuelen) { char *p; masterguard_next_slaveaddr = strtol(value, &p, 10); if (*p != '\0') { upsdebugx(2, "set_slaveaddr: illegal value %s", value); return -1; } upsdebugx(3, "next slaveaddr %ld", masterguard_next_slaveaddr); return masterguard_setvar(item, value, valuelen); } /* variable setting answer preprocessing functions */ /* set my_slaveaddr to next_slaveaddr /after/ issuing the SS command (which, itself, needs the /old/ slaveaddr) */ static int masterguard_new_slaveaddr(item_t *item, const int len) { NUT_UNUSED_VARIABLE(item); upsdebugx(3, "saved slaveaddr %ld", masterguard_next_slaveaddr); if (masterguard_next_slaveaddr < 0 || masterguard_next_slaveaddr > 99) { upsdebugx(2, "%s: illegal value %ld", __func__, masterguard_next_slaveaddr); return -1; } masterguard_my_slaveaddr[0] = '0' + (char)(masterguard_next_slaveaddr / 10); masterguard_my_slaveaddr[1] = '0' + (char)(masterguard_next_slaveaddr % 10); upsdebugx(3, "new slaveaddr %s", masterguard_my_slaveaddr); return len; } /* qx2nut lookup table */ static item_t masterguard_qx2nut[] = { /* static values */ /* type flags rw command answer len leading value from to dfl qxflags precmd preans preproc */ { "device.mfr", 0, NULL, "", "", 0, '\0', "", 0, 0, "Masterguard", QX_FLAG_STATIC | QX_FLAG_ABSENT,NULL, NULL, NULL }, { "load.high", 0, NULL, "", "", 0, '\0', "", 0, 0, "140", QX_FLAG_STATIC | QX_FLAG_ABSENT,NULL, NULL, NULL }, /* battery.charge.low */ /* battery.charge.warning */ { "battery.type", 0, NULL, "", "", 0, '\0', "", 0, 0, "PbAc", QX_FLAG_STATIC | QX_FLAG_ABSENT,NULL, NULL, NULL }, /* variables */ /* * > [WH\r] * < [(XX VV.VV PP.PP TTTTTTTTTTTTTTTTTTTTTTTTTTTTTT B MMM FF.FF VVV SS.SS HHH.hh GGG.gg RRR mm nn MMM NNN FF.FF FF.FF\r] * 01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012 * 0 1 2 3 4 5 6 7 8 9 0 1 * (00 10.06 03.09 A 700 + 0 Bat Pack-19 0 230 50.00 012 02.30 006.00 012.00 018 10 40 160 276 47.00 53.00 */ /* type flags rw command answer len leading value from to dfl qxflags precmd preans preproc */ { "ups.id", ST_FLAG_RW, masterguard_r_slaveaddr,"WH\r", "", 113, '(', "", 1, 2, "%.0f", QX_FLAG_SEMI_STATIC | QX_FLAG_RANGE, NULL, NULL, masterguard_slaveaddr }, { "ups.firmware", 0, NULL, "WH\r", "", 113, '(', "", 4, 8, "%s", QX_FLAG_STATIC, NULL, NULL, NULL }, { "ups.firmware.aux", 0, NULL, "WH\r", "", 113, '(', "", 10, 14, "%s", QX_FLAG_STATIC, NULL, NULL, NULL }, /* several values are deduced from the T field */ { "experimental.series", 0, NULL, "WH\r", "", 113, '(', "", 16, 16, "%s", QX_FLAG_STATIC | QX_FLAG_NONUT, NULL, NULL, masterguard_series }, { "device.model", 0, NULL, "WH\r", "", 113, '(', "", 16, 45, "%s", QX_FLAG_STATIC, NULL, NULL, masterguard_model }, { "ups.power.nominal", 0, NULL, "WH\r", "", 113, '(', "", 16, 45, "%s", QX_FLAG_STATIC, NULL, NULL, masterguard_power }, /* not used, use GS instead because the value is settable { "battery.packs", 0, NULL, "WH\r", "", 113, '(', "", 47, 47, "%.0f", QX_FLAG_STATIC, NULL, NULL, NULL }, */ { "input.voltage.nominal", 0, NULL, "WH\r", "", 113, '(', "", 49, 51, "%.0f", QX_FLAG_STATIC, NULL, NULL, NULL }, { "input.frequency.nominal", 0, NULL, "WH\r", "", 113, '(', "", 53, 57, "%.2f", QX_FLAG_STATIC, NULL, NULL, NULL }, { "experimental.number_of_battery_cells", 0, NULL, "WH\r", "", 113, '(', "", 59, 61, "%.0f", QX_FLAG_STATIC | QX_FLAG_NONUT, NULL, NULL, masterguard_numcells }, { "experimental.nominal_cell_voltage", 0, NULL, "WH\r", "", 113, '(', "", 63, 67, "%.2f", QX_FLAG_STATIC | QX_FLAG_NONUT, NULL, NULL, NULL }, { "battery.voltage.nominal", 0, NULL, "WH\r", "", 113, '(', "", 63, 67, "%.2f", QX_FLAG_STATIC, NULL, NULL, masterguard_battvolt}, { "experimental.runtime_half", 0, NULL, "WH\r", "", 113, '(', "", 69, 74, "%.0f", QX_FLAG_STATIC | QX_FLAG_NONUT, NULL, NULL, masterguard_mmm_ss }, { "experimental.runtime_full", 0, NULL, "WH\r", "", 113, '(', "", 76, 81, "%.0f", QX_FLAG_STATIC | QX_FLAG_NONUT, NULL, NULL, masterguard_mmm_ss }, { "experimental.recharge_time", 0, NULL, "WH\r", "", 113, '(', "", 83, 85, "%.0f", QX_FLAG_STATIC | QX_FLAG_NONUT, NULL, NULL, masterguard_hhh }, /*!! what's the difference between low/high and low.critical/high.critical?? */ { "ambient.0.temperature.low", 0, NULL, "WH\r", "", 113, '(', "", 87, 88, "%.0f", QX_FLAG_STATIC, NULL, NULL, NULL }, { "ambient.0.temperature.high", 0, NULL, "WH\r", "", 113, '(', "", 90, 91, "%.0f", QX_FLAG_STATIC, NULL, NULL, NULL }, { "input.voltage.low.critical", 0, NULL, "WH\r", "", 113, '(', "", 93, 95, "%.0f", QX_FLAG_STATIC, NULL, NULL, NULL }, { "input.voltage.high.critical",0, NULL, "WH\r", "", 113, '(', "", 97, 99, "%.0f", QX_FLAG_STATIC, NULL, NULL, NULL }, { "input.frequency.low", 0, NULL, "WH\r", "", 113, '(', "", 101, 105, "%.2f", QX_FLAG_STATIC, NULL, NULL, NULL }, { "input.frequency.high", 0, NULL, "WH\r", "", 113, '(', "", 107, 111, "%.2f", QX_FLAG_STATIC, NULL, NULL, NULL }, /* * > [Q3\r] * 76543210 76543210 * < [(XX MMM.M NNN.N PPP.P QQQ RR.R SS.SS TT.T ttt.tt CCC BBBBBBBB TTTTTTTT\r] * 01234567890123456789012345678901234567890123456789012345678901234567890 * 0 1 2 3 4 5 6 7 * (00 225.9 225.9 229.3 043 50.0 02.27 23.4 017.03 100 00000000 00000000 * (01 226.9 226.9 226.9 039 50.0 02.30 21.8 000.00 000 01100000 00011000 */ /* type flags rw command answer len leading value from to dfl qxflags precmd preans preproc */ { "input.voltage", 0, NULL, "Q3\r", "", 71, '(', "", 4, 8, "%.1f", 0, NULL, NULL, NULL }, { "experimental.input_fault_voltage", 0, NULL, "Q3\r", "", 71, '(', "", 10, 14, "%.1f", QX_FLAG_NONUT, NULL, NULL, NULL }, { "output.voltage", 0, NULL, "Q3\r", "", 71, '(', "", 16, 20, "%.1f", 0, NULL, NULL, NULL }, { "ups.load", 0, NULL, "Q3\r", "", 71, '(', "", 22, 24, "%.0f", 0, NULL, NULL, NULL }, { "ups.power", 0, NULL, "Q3\r", "", 71, '(', "", 22, 24, "%.0f", 0, NULL, NULL, masterguard_ups_power }, { "output.current", 0, NULL, "Q3\r", "", 71, '(', "", 22, 24, "%f", 0, NULL, NULL, masterguard_output_current }, { "input.frequency", 0, NULL, "Q3\r", "", 71, '(', "", 26, 29, "%.1f", 0, NULL, NULL, NULL }, { "battery.voltage", 0, NULL, "Q3\r", "", 71, '(', "", 31, 35, "%.1f", 0, NULL, NULL, masterguard_battvolt }, { "ups.temperature", 0, NULL, "Q3\r", "", 71, '(', "", 37, 40, "%.1f", 0, NULL, NULL, NULL }, /*!! report both ups.temperature and ambient.0.temperature?? */ { "ambient.0.temperature", 0, NULL, "Q3\r", "", 71, '(', "", 37, 40, "%.1f", 0, NULL, NULL, NULL }, { "battery.runtime", 0, NULL, "Q3\r", "", 71, '(', "", 42, 47, "%.0f", 0, NULL, NULL, masterguard_mmm_ss }, { "battery.charge", 0, NULL, "Q3\r", "", 71, '(', "", 49, 51, "%.0f", 0, NULL, NULL, NULL }, /* Status bits, first half (B) */ { "ups.status", 0, NULL, "Q3\r", "", 71, '(', "", 53, 53, NULL, QX_FLAG_QUICK_POLL, NULL, NULL, masterguard_status }, /* B7: Utility Fail */ { "ups.status", 0, NULL, "Q3\r", "", 71, '(', "", 54, 54, NULL, QX_FLAG_QUICK_POLL, NULL, NULL, masterguard_status }, /* B6: Battery Low */ { "ups.status", 0, NULL, "Q3\r", "", 71, '(', "", 55, 55, NULL, QX_FLAG_QUICK_POLL, NULL, NULL, masterguard_status }, /* B5: Bypass/Boost Active */ { "ups.alarm", 0, NULL, "Q3\r", "", 71, '(', "", 56, 56, NULL, QX_FLAG_QUICK_POLL, NULL, NULL, masterguard_status }, /* B4: UPS Failed */ { "ups.type", 0, NULL, "Q3\r", "", 71, '(', "", 57, 57, NULL, QX_FLAG_STATIC, NULL, NULL, masterguard_status }, /* B3: UPS Type */ { "ups.status", 0, NULL, "Q3\r", "", 71, '(', "", 58, 58, NULL, QX_FLAG_QUICK_POLL, NULL, NULL, masterguard_status }, /* B2: Test in Progress */ { "ups.status", 0, NULL, "Q3\r", "", 71, '(', "", 59, 59, NULL, QX_FLAG_QUICK_POLL, NULL, NULL, masterguard_status }, /* B1: Shutdown Active */ /* unused */ /* B0: unused */ /* Status bits, second half (T) */ /* unused */ /* T7: unused */ { "ups.alarm", 0, NULL, "Q3\r", "", 69, '(', "", 63, 63, NULL, QX_FLAG_QUICK_POLL, NULL, NULL, masterguard_status }, /* T6: problems in parallel operation mode */ /* part of a parallel set */ /* T5: is part of a parallel set */ { "ups.status", 0, NULL, "Q3\r", "", 71, '(', "", 65, 65, NULL, QX_FLAG_QUICK_POLL, NULL, NULL, masterguard_status }, /* T4: Battery: end of service life */ { "ups.alarm", 0, NULL, "Q3\r", "", 71, '(', "", 66, 66, NULL, QX_FLAG_QUICK_POLL, NULL, NULL, masterguard_status }, /* T3: battery connected */ { "ups.test.result", 0, NULL, "Q3\r", "", 71, '(', "", 67, 69, NULL, QX_FLAG_QUICK_POLL, NULL, NULL, masterguard_status }, /* T210: Test Status */ /* * > [GS,XX\r] * < [(XX,ii p a\r] * 01234567890 * 0 1 * (00,00 0 1 */ /* type flags rw command answer len leading value from to dfl qxflags precmd preans preproc */ /* ups.id obtained via WH */ { "ups.id", 0, NULL, "SS%02d--,XX\r","", 0, '\0', "", 0, 0, "d", QX_FLAG_SETVAR | QX_FLAG_RANGE, masterguard_add_slaveaddr, masterguard_new_slaveaddr, masterguard_set_slaveaddr }, { "battery.packs", ST_FLAG_RW, masterguard_r_batpacks, "GS,XX\r", "", 0, '(', "", 7, 7, "%.0f", QX_FLAG_SEMI_STATIC | QX_FLAG_RANGE, masterguard_add_slaveaddr, NULL, NULL }, { "battery.packs", 0, NULL, "SS--%1d-,XX\r","", 0, '\0', "", 0, 0, "d", QX_FLAG_SETVAR | QX_FLAG_RANGE, masterguard_add_slaveaddr, NULL, masterguard_setvar }, /*!! which QX_FLAGs to use?? (changed by instcmd) */ { "ups.beeper.status", 0, NULL, "GS,XX\r", "", 11, '(', "", 9, 9, NULL, QX_FLAG_SEMI_STATIC, masterguard_add_slaveaddr, NULL, masterguard_beeper_status }, /* set with beeper.{en,dis}able */ /* * > [GBS,XX\r] * < [(XX,CCC hhhh HHHH AAAA BBBB DDDD EEE SS.SS\r] * 0123456789012345678901234567890123456789012 * 0 1 2 3 4 * (00,100 0017 0000 0708 0712 0994 115 02.28 * (01,000 0000 0360 0708 0712 0994 076 02.30 */ /* type flags rw command answer len leading value from to dfl qxflags precmd preans preproc */ { "battery.charge", 0, NULL, "GBS,XX\r", "", 43, '(', "", 4, 6, "%.0f", 0, masterguard_add_slaveaddr, NULL, NULL }, /* * hhhh: hold time (minutes) * HHHH: recharge time to 90% (minutes) * AAAA: Ageing factor (promilles) * BBBB: Ageing factor time dependant (promilles) * DDDD: Ageing factor cyclic use (promilles) * EEE: Calibration factor (percent) * SS.SS: Actual battery cell voltage */ /* * > [GSN,XX\r] * < [(XX,SSN=SSSSSSSSSSSnnnnn\r] * 0123456789012345678901234 * 0 1 2 * (00,SSN=6A1212 2782 */ /* type flags rw command answer len leading value from to dfl qxflags precmd preans preproc */ { "device.part", 0, NULL, "GSN,XX\r", "", 25, '(', "", 8, 18, "%s", QX_FLAG_STATIC | QX_FLAG_TRIM, masterguard_add_slaveaddr, NULL, NULL }, { "device.serial", 0, NULL, "GSN,XX\r", "", 25, '(', "", 20, 23, "%s", QX_FLAG_STATIC, masterguard_add_slaveaddr, NULL, NULL }, /* * > [DRC,XX\r] * < [(XX,TTTT:hh:mm:ss\r] * 012345678901234567 * 0 1 * (00,1869:19:06:37 */ /* type flags rw command answer len leading value from to dfl qxflags precmd preans preproc */ /* this is not really the uptime, but the running time since last maintenance */ { "device.uptime", ST_FLAG_RW, NULL, "DRC,XX\r", "", 17, '(', "", 4, 16, "%.0f", QX_FLAG_SEMI_STATIC, masterguard_add_slaveaddr, NULL, masterguard_tttt_hh_mm_ss }, { "device.uptime", 0, NULL, "SRC%s,XX\r", "", 0, '\0', "", 0, 0, "thms", QX_FLAG_SETVAR, masterguard_add_slaveaddr, NULL, masterguard_setvar }, /* * > [MSO\r] * < [(220 230 240\r] * 0123456789012 * 0 1 * (220 230 240 */ /* type flags rw command answer len leading value from to dfl qxflags precmd preans preproc */ { "experimental.output_voltages", 0, NULL, "MSO\r", "", 5, '(', "", 1, 0, "%s", QX_FLAG_STATIC | QX_FLAG_NONUT, NULL, NULL, masterguard_output_voltages }, /* * > [PNV\r] * < [(PNV=nnn\r] * 012345678 * 0 * (PNV=230 */ /* type flags rw command answer len leading value from to dfl qxflags precmd preans preproc */ { "output.voltage.nominal", ST_FLAG_RW, NULL /* see claim */, "PNV\r", "", 8, '(', "", 5, 7, "%.0f", QX_FLAG_SEMI_STATIC | QX_FLAG_ENUM, NULL, NULL, NULL }, { "output.voltage.nominal", 0, NULL, "PNV=%03d\r", "", 0, '\0', "", 0, 0, "d", QX_FLAG_SETVAR, NULL, NULL, masterguard_setvar }, { "output.current.nominal", 0, NULL, "PNV\r", "", 8, '(', "", 5, 7, "%.0f", QX_FLAG_SEMI_STATIC, NULL, NULL, masterguard_output_current_nominal }, /* * > [FLT,XX\r] * < [(XX,A aaaa TTTT:hh:mm:ss B bbbb TTTT:hh:mm:ss C cccc TTTT:hh:mm:ss D dddd TTTT:hh:mm:ss E eeee TTTT:hh:mm:ss\r * 0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678 * 0 1 2 3 4 5 6 7 8 9 0 * (00,7 0043 0000:16:48:06 0 0000 0000:00:00:00 0 0000 0000:00:00:00 0 0000 0000:00:00:00 0 0000 0000:00:00:00 * (01,9 0010 1780:14:57:19 7 0046 0000:21:14:41 0 0000 0000:00:00:00 0 0000 0000:00:00:00 0 0000 0000:00:00:00 */ /* type flags rw command answer len leading value from to dfl qxflags precmd preans preproc */ { "experimental.fault_1", 0, NULL, "FLT,XX\r", "", 108, '(', "", 4, 23, "%s", QX_FLAG_NONUT, masterguard_add_slaveaddr, NULL, masterguard_fault }, { "experimental.fault_2", 0, NULL, "FLT,XX\r", "", 108, '(', "", 25, 44, "%s", QX_FLAG_NONUT, masterguard_add_slaveaddr, NULL, masterguard_fault }, { "experimental.fault_3", 0, NULL, "FLT,XX\r", "", 108, '(', "", 46, 65, "%s", QX_FLAG_NONUT, masterguard_add_slaveaddr, NULL, masterguard_fault }, { "experimental.fault_4", 0, NULL, "FLT,XX\r", "", 108, '(', "", 67, 86, "%s", QX_FLAG_NONUT, masterguard_add_slaveaddr, NULL, masterguard_fault }, { "experimental.fault_5", 0, NULL, "FLT,XX\r", "", 108, '(', "", 88, 107, "%s", QX_FLAG_NONUT, masterguard_add_slaveaddr, NULL, masterguard_fault }, /* instant commands */ /* type flags rw command answer len leading value from to dfl qxflags precmd preans preproc */ /*!! what's the difference between load.off.delay and shutdown.stayoff?? */ #if 0 { "load.off", 0, NULL, "S.0\r", "", 0, '\0', "", 0, 0, NULL, QX_FLAG_CMD, NULL, NULL, NULL }, { "load.on", 0, NULL, "C\r", "", 0, '\0', "", 0, 0, NULL, QX_FLAG_CMD, NULL, NULL, NULL }, /* load.off.delay */ /* load.on.delay */ #endif { "shutdown.return", 0, NULL, NULL, "", 0, '\0', "", 0, 0, NULL, QX_FLAG_CMD, NULL, NULL, masterguard_shutdown_return }, { "shutdown.stayoff", 0, NULL, NULL, "", 0, '\0', "", 0, 0, NULL, QX_FLAG_CMD, NULL, NULL, masterguard_shutdown_stayoff }, { "shutdown.stop", 0, NULL, "C\r", "", 0, '\0', "", 0, 0, NULL, QX_FLAG_CMD, NULL, NULL, NULL }, /* shutdown.reboot */ /* shutdown.reboot.graceful */ /* test.panel.start */ /* test.panel.stop */ /* test.failure.start */ /* test.failure.stop */ { "test.battery.start", 0, NULL, NULL, "", 0, '\0', "", 0, 0, NULL, QX_FLAG_CMD, NULL, NULL, masterguard_test_battery }, { "test.battery.start.quick", 0, NULL, "T\r", "", 0, '\0', "", 0, 0, NULL, QX_FLAG_CMD, NULL, NULL, NULL }, { "test.battery.start.deep", 0, NULL, "TUD\r", "", 0, '\0', "", 0, 0, NULL, QX_FLAG_CMD, NULL, NULL, NULL }, { "test.battery.stop", 0, NULL, "CT\r", "", 0, '\0', "", 0, 0, NULL, QX_FLAG_CMD, NULL, NULL, NULL }, /* test.system.start */ /* calibrate.start */ /* calibrate.stop */ { "bypass.start", 0, NULL, "FOFF\r", "", 0, '\0', "", 0, 0, NULL, QX_FLAG_CMD, NULL, NULL, NULL }, { "bypass.stop", 0, NULL, "FON\r", "", 0, '\0', "", 0, 0, NULL, QX_FLAG_CMD, NULL, NULL, NULL }, /* reset.input.minmax */ /* reset.watchdog */ { "beeper.enable", 0, NULL, "SS---1,XX\r", "", 0, '\0', "", 0, 0, NULL, QX_FLAG_CMD, masterguard_add_slaveaddr, NULL, NULL }, { "beeper.disable", 0, NULL, "SS---0,XX\r", "", 0, '\0', "", 0, 0, NULL, QX_FLAG_CMD, masterguard_add_slaveaddr, NULL, NULL }, /* beeper.mute */ /* beeper.toggle */ /* outlet.* */ /* server variables */ /* type flags rw command answer len leading value from to dfl qxflags precmd preans preproc */ { "ups.delay.shutdown", ST_FLAG_RW, masterguard_r_offdelay, NULL, "", 0, '\0', "", 0, 0, DEFAULT_OFFDELAY, QX_FLAG_ABSENT | QX_FLAG_SETVAR | QX_FLAG_RANGE, NULL, NULL, NULL }, { "ups.delay.start", ST_FLAG_RW, masterguard_r_ondelay, NULL, "", 0, '\0', "", 0, 0, DEFAULT_ONDELAY, QX_FLAG_ABSENT | QX_FLAG_SETVAR | QX_FLAG_RANGE, NULL, NULL, NULL }, /* end marker */ { NULL, 0, NULL, NULL, "", 0, 0, "", 0, 0, NULL, 0, NULL, NULL, NULL } }; /*!! todo untested: Sxx (.n/nn) C after S.0 unused: >G01,00 (00,00000 additional E series commands: PSR BUS* V INVDC use ups.{delay,timer}.{start,reboot,shutdown}? report ups.contacts? how to report battery.charger.status? set battery.packs.bad? how to report battery aeging/run time? how to report nominal hold time at half/full load? */ /* commands supported by A series */ static char *masterguard_commands_a[] = { "Q", "Q1", "Q3", "T", "TL", "S", "C", "CT", "WH", "M", "N", "O", "DECO", "DRC", "SRC", "FLT", "FCLR", "G", "SS", "GS", "MSO", "PNV", "FOFF", "FON", "TUD", "GBS", "SSN", "GSN", NULL }; /* commands supported by E series */ static char *masterguard_commands_e[] = { "Q", "Q1", "Q3", "PSR", "T", "TL", "S", "C", "CT", "WH", "DRC", "SRC", "FLT", "FCLR", "SS", "GS", "MSO", "PNV", "FOFF", "FON", "TUD", "GBS", "SSN", "GSN", "BUS", "V", "INVDC", "BUSP", "BUSN", NULL }; /* claim function. fetch some mandatory values, * disable unsupported commands, * set enum for supported output voltages */ static int masterguard_claim(void) { item_t *item; /* mandatory values */ char *mandatory[] = { "series", /* SKIP */ "device.model", /* minimal number of battery packs */ "ups.power.nominal", /* load computation */ "ups.id", /* slave address */ "output_voltages", /* output voltages enum */ #if 0 "battery.packs", /* battery voltage computation */ #endif NULL }; char **sp; long config_slaveaddr; char *sa; char **commands; if ((sa = getval("slave_address")) != NULL) { char *p; if (*sa == '\0') { upsdebugx(2, "claim: empty slave_address"); return 0; } config_slaveaddr = strtol(sa, &p, 10); if (*p != '\0' || config_slaveaddr < 0 || config_slaveaddr > 99) { upsdebugx(2, "claim: illegal slave_address %s", sa); return 0; } } else { config_slaveaddr = -1; } for (sp = mandatory; *sp != NULL; sp++) { char value[SMALLBUF] = ""; if ((item = find_nut_info(*sp, 0, QX_FLAG_SETVAR)) == NULL) { upsdebugx(2, "claim: cannot find %s", *sp); return 0; } /* since qx_process_answer() is not exported, there's no way * to avoid sending the same command to the UPS again */ if (qx_process(item, NULL) < 0) { upsdebugx(2, "claim: cannot process %s", *sp); return 0; } /* only call the preprocess function; don't call ups_infoval_set() * because that does a dstate_setinfo() before dstate_setflags() * is called (via qx_set_var() in qx_ups_walk() with QX_WALKMODE_INIT); * that leads to r/w vars ending up r/o. */ if (item->preprocess == NULL ) { upsdebugx(2, "claim: no preprocess function for %s", *sp); return 0; } if (item->preprocess(item, value, sizeof value)) { upsdebugx(2, "claim: failed to preprocess %s", *sp); return 0; } } if (config_slaveaddr >= 0 && config_slaveaddr != strtol(masterguard_my_slaveaddr, NULL, 10)) { upsdebugx(2, "claim: slave address mismatch: want %02ld, have %s", config_slaveaddr, masterguard_my_slaveaddr); return 0; } switch (masterguard_my_series) { case 'A': commands = masterguard_commands_a; break; case 'E': commands = masterguard_commands_e; break; default: return 0; } /* set SKIP flag for unimplemented commands */ for (item = masterguard_qx2nut; item->info_type != NULL; item++) { int match = 0; if (item->command == NULL || item->command[0] == '\0') continue; for (sp = commands; sp != NULL; sp++) { const char *p = *sp, *q = item->command; while (1) { if (*p == '\0' && (*q < 'A' || *q > 'Z')) { match = 1; break; } else if (*p == '\0' || *q < 'A' || *q > 'Z' || *p != *q) { match = 0; break; } p++; q++; } if (match) break; } if (nut_debug_level >= 3) { char cmd[10]; char *p = cmd; const char *q = item->command; while (*q >= 'A' && *q <= 'Z') { *p++ = *q++; if (p - cmd >= (ptrdiff_t)sizeof cmd - 1) break; } *p++ = '\0'; upsdebugx(3, "command %s %simplemented", cmd, match ? "" : "NOT "); } if (!match) item->qxflags |= QX_FLAG_SKIP; } /* set enum for output.voltage.nominal */ if ((item = find_nut_info("output.voltage.nominal", QX_FLAG_ENUM, QX_FLAG_SETVAR)) == NULL) { upsdebugx(2, "claim: cannot find output.voltage.nominal"); return 0; } item->info_rw = masterguard_e_outvolts; return 1; } static void masterguard_makevartable(void) { addvar(VAR_VALUE, "series", "Series (A/E)"); addvar(VAR_VALUE, "slave_address", "Slave address (UPS id) to match"); addvar(VAR_VALUE, "input_fault_voltage", "Input fault voltage (whatever that means)"); addvar(VAR_VALUE, "number_of_battery_cells", "Number of battery cells in series"); addvar(VAR_VALUE, "nominal_cell_voltage", "Nominal battery cell voltage"); addvar(VAR_VALUE, "runtime_half", "Nominal battery run time at 50% load (seconds)"); addvar(VAR_VALUE, "runtime_full", "Nominal battery run time at 100% load (seconds)"); addvar(VAR_VALUE, "recharge_time", "Nominal battery recharge time to 95% capacity (seconds)"); addvar(VAR_VALUE, "output_voltages", "Possible output voltages (volts)"); addvar(VAR_VALUE, "fault_1", "Fault record 1 (newest)"); addvar(VAR_VALUE, "fault_2", "Fault record 2"); addvar(VAR_VALUE, "fault_3", "Fault record 3"); addvar(VAR_VALUE, "fault_4", "Fault record 4"); addvar(VAR_VALUE, "fault_5", "Fault record 5 (oldest)"); } #ifdef TESTING static testing_t masterguard_testing[] = { { NULL } }; #endif /* TESTING */ subdriver_t masterguard_subdriver = { MASTERGUARD_VERSION, masterguard_claim, masterguard_qx2nut, NULL, /* initups */ NULL, /* intinfo */ masterguard_makevartable, NULL, /* accepted */ NULL, /* rejected */ #ifdef TESTING masterguard_testing, #endif /* TESTING */ };