diff --git a/NEWS.adoc b/NEWS.adoc index f5b44de78e..bad715de33 100644 --- a/NEWS.adoc +++ b/NEWS.adoc @@ -178,7 +178,9 @@ https://github.com/networkupstools/nut/milestone/11 * added Visench C1K (using serial port converter with USB ID `1a86:7523`) as known supported by `nutdrv_qx` (Megatec protocol) since at least NUT v2.7.4 release. [#2395] - * introduced `innovart31` protocol support for Innova RT 3/1 UPSes. [#2712] + * introduced `innovart31` protocol support for Innova RT 3/1 UPSes. [#2712, #2798] + * introduced `q2` and `q6` protocol support; currently also based/tested + on Innova devices, but other models than RT 3/1. [#2798] * extended Voltronic protocol to support longer numbers as remaining `battery.runtime` value. [#2765] diff --git a/docs/man/nutdrv_qx.txt b/docs/man/nutdrv_qx.txt index f70f175e50..810f6a0f45 100644 --- a/docs/man/nutdrv_qx.txt +++ b/docs/man/nutdrv_qx.txt @@ -79,7 +79,7 @@ If you set stayoff in linkman:ups.conf[5] when FSD arises the UPS will call a *s *protocol =* 'string':: Skip autodetection of the protocol to use and only use the one specified. -Supported values: 'bestups', 'hunnox', 'innovart31', 'masterguard', 'mecer', 'megatec', 'megatec/old', 'mustek', 'q1', 'voltronic', 'voltronic-qs', 'voltronic-qs-hex' and 'zinto'. +Supported values: 'bestups', 'hunnox', 'innovart31', 'masterguard', 'mecer', 'megatec', 'megatec/old', 'mustek', 'q1', 'q2', 'q6', 'voltronic', 'voltronic-qs', 'voltronic-qs-hex' and 'zinto'. + Run the driver program with the `--help` option to see the exact list of `protocol` values it would currently recognize. @@ -159,8 +159,8 @@ If not specified, the driver defaults to 10%. Only used if *runtimecal* is also specified. -BESTUPS, INNOVART31, MECER, MEGATEC, MEGATEC/OLD, MUSTEK, Q1, VOLTRONIC-QS, VOLTRONIC-QS-HEX, ZINTO PROTOCOLS -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +BESTUPS, INNOVART31, MECER, MEGATEC, MEGATEC/OLD, MUSTEK, Q1, Q2, Q6, VOLTRONIC-QS, VOLTRONIC-QS-HEX, ZINTO PROTOCOLS +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ *ignoresab*:: Some UPSes incorrectly report the `Shutdown Active' bit as always on, consequently making the driver believe the UPS is nearing a shutdown (and, as a result, ups.status always contains +FSD+... and you know what this means). @@ -168,8 +168,8 @@ Setting this flag will make the driver ignore the `Shutdown Active' bit. [[old-blazer-protocols-options]] -MECER, MEGATAEC, MEGATEC/OLD, MUSTEK, ZINTO PROTOCOLS -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +MECER, MEGATEC, MEGATEC/OLD, MUSTEK, ZINTO PROTOCOLS +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ *ondelay*:: The acceptable range is +0..599940+ seconds. @@ -208,8 +208,8 @@ Safeguard against talking to the wrong one of several identical UPSes on the sam Note that when changing *ups.id* (through linkman:upsrw[8]) the driver will continue to talk to the UPS with the new 'slave address', but won't claim it again on restart until the *slave_addr* parameter is adjusted. -INNOVART31, Q1 PROTOCOLS -~~~~~~~~~~~~~~~~~~~~~~~~ +INNOVART31, Q1, Q2, Q6 PROTOCOLS +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ *ondelay*:: The acceptable range is +0..599940+ seconds. @@ -218,6 +218,13 @@ The acceptable range is +0..599940+ seconds. The acceptable range is +12..600+ seconds. +Q2, Q6 PROTOCOLS +~~~~~~~~~~~~~~~~ + +*nooutstats*:: +Some UPSes don't support `WA` command which returns output load stats. Using this flag will make the driver ignore these requests. + + VOLTRONIC-QS, VOLTRONIC-QS-HEX PROTOCOLS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -451,8 +458,8 @@ Stop a running battery test. (Not available on some hardware) -BESTUPS, INNOVART31, MECER, MEGATEC, MEGATEC/OLD, MUSTEK, Q1, ZINTO PROTOCOLS -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +BESTUPS, INNOVART31, MECER, MEGATEC, MEGATEC/OLD, MUSTEK, Q1, Q2, Q6, ZINTO PROTOCOLS +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ *test.battery.start* 'value':: Perform a battery test for the duration of 'value' seconds (truncated to 60 seconds) [+60..5940+]. diff --git a/docs/nut.dict b/docs/nut.dict index b3eb29502b..dcfae08263 100644 --- a/docs/nut.dict +++ b/docs/nut.dict @@ -646,7 +646,6 @@ MBATTCHG MCU MDigest MEC -MEGATAEC MERCHANTABILITY MF MH @@ -2482,6 +2481,7 @@ nolock nombattvolt noncommercially noout +nooutstats norating noro noscanlangid diff --git a/docs/nutdrv_qx-subdrivers.txt b/docs/nutdrv_qx-subdrivers.txt index dfabbc8f55..a3a5bfb730 100644 --- a/docs/nutdrv_qx-subdrivers.txt +++ b/docs/nutdrv_qx-subdrivers.txt @@ -1178,6 +1178,8 @@ For more details, have a look at the currently available subdrivers: - +nutdrv_qx_megatec-old.+{+c+,+h+} - +nutdrv_qx_mustek.+{+c+,+h+} - +nutdrv_qx_q1.+{+c+,+h+} +- +nutdrv_qx_q2.+{+c+,+h+} +- +nutdrv_qx_q6.+{+c+,+h+} - +nutdrv_qx_voltronic.+{+c+,+h+} - +nutdrv_qx_voltronic-qs.+{+c+,+h+} - +nutdrv_qx_voltronic-qs-hex.+{+c+,+h+} diff --git a/drivers/Makefile.am b/drivers/Makefile.am index 454f154e2f..ab351d6134 100644 --- a/drivers/Makefile.am +++ b/drivers/Makefile.am @@ -382,7 +382,7 @@ NUTDRV_QX_SUBDRIVERS = nutdrv_qx_bestups.c nutdrv_qx_blazer-common.c \ nutdrv_qx_innovart31.c \ nutdrv_qx_masterguard.c \ nutdrv_qx_mecer.c nutdrv_qx_megatec.c nutdrv_qx_megatec-old.c \ - nutdrv_qx_mustek.c nutdrv_qx_q1.c nutdrv_qx_voltronic.c \ + nutdrv_qx_mustek.c nutdrv_qx_q1.c nutdrv_qx_q2.c nutdrv_qx_q6.c nutdrv_qx_voltronic.c \ nutdrv_qx_voltronic-qs.c nutdrv_qx_voltronic-qs-hex.c nutdrv_qx_zinto.c \ nutdrv_qx_hunnox.c nutdrv_qx_ablerex.c nutdrv_qx_SOURCES += $(NUTDRV_QX_SUBDRIVERS) @@ -408,7 +408,7 @@ dist_noinst_HEADERS = \ nutdrv_qx_innovart31.h \ nutdrv_qx_masterguard.h \ nutdrv_qx_mecer.h nutdrv_qx_ablerex.h \ - nutdrv_qx_megatec.h nutdrv_qx_megatec-old.h nutdrv_qx_mustek.h nutdrv_qx_q1.h nutdrv_qx_hunnox.h \ + nutdrv_qx_megatec.h nutdrv_qx_megatec-old.h nutdrv_qx_mustek.h nutdrv_qx_q1.h nutdrv_qx_q2.h nutdrv_qx_q6.h nutdrv_qx_hunnox.h \ nutdrv_qx_voltronic.h nutdrv_qx_voltronic-qs.h nutdrv_qx_voltronic-qs-hex.h nutdrv_qx_zinto.h \ upsdrvquery.h \ xppc-mib.h huawei-mib.h eaton-ats16-nmc-mib.h eaton-ats16-nm2-mib.h apc-ats-mib.h raritan-px2-mib.h eaton-ats30-mib.h \ diff --git a/drivers/nutdrv_qx.c b/drivers/nutdrv_qx.c index f92edac817..c16171454e 100644 --- a/drivers/nutdrv_qx.c +++ b/drivers/nutdrv_qx.c @@ -58,7 +58,7 @@ # define DRIVER_NAME "Generic Q* Serial driver" #endif /* QX_USB */ -#define DRIVER_VERSION "0.39" +#define DRIVER_VERSION "0.40" #ifdef QX_SERIAL # include "serial.h" @@ -77,6 +77,8 @@ #include "nutdrv_qx_megatec-old.h" #include "nutdrv_qx_mustek.h" #include "nutdrv_qx_q1.h" +#include "nutdrv_qx_q2.h" +#include "nutdrv_qx_q6.h" #include "nutdrv_qx_voltronic.h" #include "nutdrv_qx_voltronic-qs.h" #include "nutdrv_qx_voltronic-qs-hex.h" @@ -99,6 +101,8 @@ static subdriver_t *subdriver_list[] = { &hunnox_subdriver, &ablerex_subdriver, &innovart31_subdriver, + &q2_subdriver, + &q6_subdriver, /* Fallback Q1 subdriver */ &q1_subdriver, NULL @@ -258,6 +262,19 @@ int qx_multiply_battvolt(item_t *item, char *value, const size_t valuelen) { return 0; } +/* Convert kilo-values to their full representation */ +int qx_multiply_x1000(item_t *item, char *value, const size_t valuelen) { + float s = 0; + + if (sscanf(item->value, "%f", &s) != 1) { + upsdebugx(2, "unparsable ss.ss %s", item->value); + return -1; + } + + snprintf(value, valuelen, "%.2f", s * 1000.0); + return 0; +} + /* Fill batt.volt.act and guesstimate the battery charge * if it isn't already available. */ static int qx_battery(void) diff --git a/drivers/nutdrv_qx.h b/drivers/nutdrv_qx.h index faffe717cc..74af864cdf 100644 --- a/drivers/nutdrv_qx.h +++ b/drivers/nutdrv_qx.h @@ -189,6 +189,9 @@ void update_status(const char *nutvalue); /* Let subdrivers reference this: for devices that report "battery.voltage" of a single cell/pack, optionally multiply that into representing the whole assembly */ int qx_multiply_battvolt(item_t *item, char *value, const size_t valuelen); + /* Convert kilo-values to their full representation */ +int qx_multiply_x1000(item_t *item, char *value, const size_t valuelen); + /* Data for processing status values */ #define STATUS(x) ((unsigned int)1U< diff --git a/drivers/nutdrv_qx_blazer-common.h b/drivers/nutdrv_qx_blazer-common.h index f8344b9404..b79edbd990 100644 --- a/drivers/nutdrv_qx_blazer-common.h +++ b/drivers/nutdrv_qx_blazer-common.h @@ -1,4 +1,4 @@ -/* nutdrv_qx_blazer-common.h - Common functions/settings for nutdrv_qx_{innovart31,mecer,megatec,megatec-old,mustek,q1,voltronic-qs,zinto}.{c,h} +/* nutdrv_qx_blazer-common.h - Common functions/settings for nutdrv_qx_{innovart31,mecer,megatec,megatec-old,mustek,q1,q2,q6,voltronic-qs,zinto}.{c,h} * * Copyright (C) * 2013 Daniele Pezzini diff --git a/drivers/nutdrv_qx_innovart31.c b/drivers/nutdrv_qx_innovart31.c index 4bef9b7fa6..3f75340587 100644 --- a/drivers/nutdrv_qx_innovart31.c +++ b/drivers/nutdrv_qx_innovart31.c @@ -25,20 +25,13 @@ #include "nutdrv_qx_innovart31.h" -#define INNOVART31_VERSION "INNOVART31 0.01" +#define INNOVART31_VERSION "INNOVART31 0.02" -/* Internal function: used to convert kilo-values to their full representation */ -static int innovart31_x1000(item_t *item, char *value, const size_t valuelen) { - float s = 0; +/* Support functions */ +static int innovart31_claim(void); +static void innovart31_initups(void); +static void innovart31_initinfo(void); - if (sscanf(item->value, "%f", &s) != 1) { - upsdebugx(2, "unparsable ss.ss %s", item->value); - return -1; - } - - snprintf(value, valuelen, "%.2f", s * 1000.0); - return 0; -} /* qx2nut lookup table */ static item_t innovart31_qx2nut[] = { @@ -50,19 +43,15 @@ static item_t innovart31_qx2nut[] = { */ /* Common parameters */ - { "input.voltage", 0, NULL, "Q6\r", "", 110, '(', "", 1, 5, "%.1f", 0, NULL, NULL, NULL }, { "input.L1-N.voltage", 0, NULL, "Q6\r", "", 110, '(', "", 1, 5, "%.1f", 0, NULL, NULL, NULL }, { "input.L2-N.voltage", 0, NULL, "Q6\r", "", 110, '(', "", 7, 11, "%.1f", 0, NULL, NULL, NULL }, { "input.L3-N.voltage", 0, NULL, "Q6\r", "", 110, '(', "", 13, 17, "%.1f", 0, NULL, NULL, NULL }, { "input.frequency", 0, NULL, "Q6\r", "", 110, '(', "", 19, 22, "%.1f", 0, NULL, NULL, NULL }, { "output.voltage", 0, NULL, "Q6\r", "", 110, '(', "", 24, 28, "%.1f", 0, NULL, NULL, NULL }, -/* { "output.L1-N.voltage", 0, NULL, "Q6\r", "", 110, '(', "", 24, 28, "%.1f", 0, NULL, NULL, NULL }, - { "output.L2-N.voltage", 0, NULL, "Q6\r", "", 110, '(', "", 30, 34, "%.1f", 0, NULL, NULL, NULL }, - { "output.L3-N.voltage", 0, NULL, "Q6\r", "", 110, '(', "", 36, 40, "%.1f", 0, NULL, NULL, NULL },*/ { "output.frequency", 0, NULL, "Q6\r", "", 110, '(', "", 42, 45, "%.1f", 0, NULL, NULL, NULL }, { "ups.load", 0, NULL, "Q6\r", "", 110, '(', "", 47, 49, "%.0f", 0, NULL, NULL, NULL }, { "ups.temperature", 0, NULL, "Q6\r", "", 110, '(', "", 71, 74, "%.1f", 0, NULL, NULL, NULL }, - { "battery.voltage", 0, NULL, "Q6\r", "", 110, '(', "", 59, 63, "%.2f", 0, NULL, NULL, qx_multiply_battvolt }, + { "battery.voltage", 0, NULL, "Q6\r", "", 110, '(', "", 59, 63, "%.1f", 0, NULL, NULL, qx_multiply_battvolt }, { "battery.runtime", 0, NULL, "Q6\r", "", 110, '(', "", 76, 80, "%.0f", 0, NULL, NULL, NULL }, { "battery.charge", 0, NULL, "Q6\r", "", 110, '(', "", 82, 84, "%.0f", 0, NULL, NULL, NULL }, @@ -75,8 +64,8 @@ static item_t innovart31_qx2nut[] = { /* Output consumption parameters */ { "output.current", 0, NULL, "WA\r", "", 80, '(', "", 49, 53, "%.1f", 0, NULL, NULL, NULL }, - { "ups.realpower", 0, NULL, "WA\r", "", 80, '(', "", 37, 41, "%.1f", 0, NULL, NULL, innovart31_x1000 }, - { "ups.power", 0, NULL, "WA\r", "", 80, '(', "", 43, 47, "%.1f", 0, NULL, NULL, innovart31_x1000 }, + { "ups.realpower", 0, NULL, "WA\r", "", 80, '(', "", 37, 41, "%.1f", 0, NULL, NULL, qx_multiply_x1000 }, + { "ups.power", 0, NULL, "WA\r", "", 80, '(', "", 43, 47, "%.1f", 0, NULL, NULL, qx_multiply_x1000 }, /* * > [BPS\r] @@ -87,9 +76,6 @@ static item_t innovart31_qx2nut[] = { /* Bypass parameters */ { "input.bypass.voltage", 0, NULL, "BPS\r", "", 24, '(', "", 1, 5, "%.1f", 0, NULL, NULL, NULL }, -/* { "input.bypass.L1-N.voltage", 0, NULL, "BPS\r", "", 24, '(', "", 1, 5, "%.1f", 0, NULL, NULL, NULL }, - { "input.bypass.L2-N.voltage", 0, NULL, "BPS\r", "", 24, '(', "", 7, 11, "%.1f", 0, NULL, NULL, NULL }, - { "input.bypass.L3-N.voltage", 0, NULL, "BPS\r", "", 24, '(', "", 13, 17, "%.1f", 0, NULL, NULL, NULL },*/ { "input.bypass.frequency", 0, NULL, "BPS\r", "", 24, '(', "", 19, 22, "%.1f", 0, NULL, NULL, NULL }, /* @@ -165,8 +151,8 @@ static item_t innovart31_qx2nut[] = { /* Testing table */ #ifdef TESTING static testing_t innovart31_testing[] = { - { "Q1\r", "(215.0 195.0 230.0 014 49.0 22.7 30.0 00000000\r", -1 }, { "Q6\r", "(227.0 225.6 230.0 50.0 229.9 000.0 000.0 49.9 007 000 000 327.8 000.0 23.0 06932 100 32 00000000 00000000 11\r", -1 }, + { "Q1\r", "(215.0 195.0 230.0 014 49.0 22.7 30.0 00000000\r", -1 }, { "WA\r", "(001.4 000.0 000.0 001.4 000.0 000.0 001.4 001.4 006.5 000.0 000.0 007 00000000\r", -1 }, { "BPS\r", "(230.4 000.0 000.0 49.9\r", -1 }, { "F\r", "#230.0 087 288.0 50.0\r", -1 }, @@ -184,10 +170,53 @@ static testing_t innovart31_testing[] = { }; #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 innovart31_claim(void) +{ + /* We need Q6, Q1, WA, BPS, F, FW? and SASV07? to use this subdriver */ + struct { + char *var; + char *cmd; + } mandatory[] = { + { "input.L1-N.voltage", "Q6" }, + { "ups.type", "Q1" }, + { "output.current", "WA" }, + { "input.bypass.voltage", "BPS" }, + { "input.voltage.nominal", "F" }, + { "ups.firmware", "FW?" }, + { "ups.serial", "SASV07?" }, + { NULL, NULL} + }; + int vari; + char *sp; + item_t *item; + + for (vari = 0; mandatory[vari].var; vari++) { + sp = mandatory[vari].var; + item = find_nut_info(sp, 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 */ + if (ups_infoval_set(item) != 1) + return 0; + } + + return 1; +} + /* Subdriver-specific initups */ static void innovart31_initups(void) { - blazer_initups(innovart31_qx2nut); + blazer_initups_light(innovart31_qx2nut); } /* Subdriver-specific initinfo */ @@ -201,11 +230,11 @@ static void innovart31_initinfo(void) /* Subdriver interface */ subdriver_t innovart31_subdriver = { INNOVART31_VERSION, - blazer_claim, + innovart31_claim, innovart31_qx2nut, innovart31_initups, innovart31_initinfo, - blazer_makevartable, + blazer_makevartable_light, "ACK", "NAK\r", #ifdef TESTING diff --git a/drivers/nutdrv_qx_q2.c b/drivers/nutdrv_qx_q2.c new file mode 100644 index 0000000000..4f2cd8abd3 --- /dev/null +++ b/drivers/nutdrv_qx_q2.c @@ -0,0 +1,301 @@ +/* nutdrv_qx_q2.c - Subdriver for Q2 Megatec protocol variant + * + * Copyright (C) + * 2024 Viktor Drobot + * + * 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_q2.h" + +#define Q2_VERSION "Q2 0.01" + +/* Support functions */ +static int q2_process_topology_bits(item_t *item, char *value, const size_t valuelen); +static int q2_claim(void); +static void q2_initups(void); +static void q2_initinfo(void); +static void q2_makevartable(void); + +/* qx2nut lookup table */ +static item_t q2_qx2nut[] = { + + /* + * > [Q2\r] + * < [(MMM.M MMM.M MMM.M NNN.N PPP.P PPP.P PPP.P QQQ QQQ QQQ RR.R SSS.S TT.T b7b6b5b4b3b2b1b0 ttt.tt CCC BB ff ff ff ff wwwwwwww YO\r] + * 012345678901234567890123456789012345678901234567890123456789012345678901 2 3 4 5 6 7 8 901234567890123456789012345678901234567 + * 0 1 2 3 4 5 6 7 8 9 10 11 + */ + + /* Common parameters */ + { "input.L1-N.voltage", 0, NULL, "Q2\r", "", 118, '(', "", 1, 5, "%.1f", 0, NULL, NULL, NULL }, + { "input.L2-N.voltage", 0, NULL, "Q2\r", "", 118, '(', "", 7, 11, "%.1f", 0, NULL, NULL, NULL }, + { "input.L3-N.voltage", 0, NULL, "Q2\r", "", 118, '(', "", 13, 17, "%.1f", 0, NULL, NULL, NULL }, + { "input.voltage.fault", 0, NULL, "Q2\r", "", 118, '(', "", 19, 23, "%.1f", 0, NULL, NULL, NULL }, + { "input.frequency", 0, NULL, "Q2\r", "", 118, '(', "", 55, 58, "%.1f", 0, NULL, NULL, NULL }, + { "output.L1-N.voltage", 0, NULL, "Q2\r", "", 118, '(', "", 25, 29, "%.1f", 0, NULL, NULL, NULL }, + { "output.L2-N.voltage", 0, NULL, "Q2\r", "", 118, '(', "", 31, 35, "%.1f", 0, NULL, NULL, NULL }, + { "output.L3-N.voltage", 0, NULL, "Q2\r", "", 118, '(', "", 37, 41, "%.1f", 0, NULL, NULL, NULL }, + { "output.L1.load", 0, NULL, "Q2\r", "", 118, '(', "", 43, 45, "%.0f", 0, NULL, NULL, NULL }, + { "output.L2.load", 0, NULL, "Q2\r", "", 118, '(', "", 47, 49, "%.0f", 0, NULL, NULL, NULL }, + { "output.L3.load", 0, NULL, "Q2\r", "", 118, '(', "", 51, 53, "%.0f", 0, NULL, NULL, NULL }, + { "ups.temperature", 0, NULL, "Q2\r", "", 118, '(', "", 66, 69, "%.1f", 0, NULL, NULL, NULL }, + { "battery.voltage", 0, NULL, "Q2\r", "", 118, '(', "", 60, 64, "%.1f", 0, NULL, NULL, qx_multiply_battvolt }, + { "battery.runtime", 0, NULL, "Q2\r", "", 118, '(', "", 80, 85, "%.0f", 0, NULL, NULL, NULL }, + { "battery.charge", 0, NULL, "Q2\r", "", 118, '(', "", 87, 89, "%.0f", 0, NULL, NULL, NULL }, + { "experimental.input.topology", 0, NULL, "Q2\r", "", 118, '(', "", 115, 115, "%s", QX_FLAG_STATIC, NULL, NULL, q2_process_topology_bits }, /* Input transformer topology */ + { "experimental.output.topology", 0, NULL, "Q2\r", "", 118, '(', "", 116, 116, "%s", QX_FLAG_STATIC, NULL, NULL, q2_process_topology_bits }, /* Output transformer topology */ + /* TODO handle BB (91-92) properly: mb use "experimental." vars for that */ + + /* + * > [WA\r] + * < [(WWW.W WWW.W WWW.W VVV.V VVV.V VVV.V TTT.T SSS.S AAA.A AAA.A AAA.A QQQ b7b6b5b4b3b2b1b0\r] + * 012345678901234567890123456789012345678901234567890123456789012345678901 2 3 4 5 6 7 8 9 + * 0 1 2 3 4 5 6 7 + */ + + /* Output consumption parameters */ + { "output.L1.realpower", 0, NULL, "WA\r", "", 80, '(', "", 1, 5, "%.1f", 0, NULL, NULL, qx_multiply_x1000 }, + { "output.L2.realpower", 0, NULL, "WA\r", "", 80, '(', "", 7, 11, "%.1f", 0, NULL, NULL, qx_multiply_x1000 }, + { "output.L3.realpower", 0, NULL, "WA\r", "", 80, '(', "", 13, 17, "%.1f", 0, NULL, NULL, qx_multiply_x1000 }, + { "output.L1.power", 0, NULL, "WA\r", "", 80, '(', "", 19, 23, "%.1f", 0, NULL, NULL, qx_multiply_x1000 }, + { "output.L2.power", 0, NULL, "WA\r", "", 80, '(', "", 25, 29, "%.1f", 0, NULL, NULL, qx_multiply_x1000 }, + { "output.L3.power", 0, NULL, "WA\r", "", 80, '(', "", 31, 35, "%.1f", 0, NULL, NULL, qx_multiply_x1000 }, + { "output.L1.current", 0, NULL, "WA\r", "", 80, '(', "", 49, 53, "%.1f", 0, NULL, NULL, NULL }, + { "output.L2.current", 0, NULL, "WA\r", "", 80, '(', "", 55, 59, "%.1f", 0, NULL, NULL, NULL }, + { "output.L3.current", 0, NULL, "WA\r", "", 80, '(', "", 61, 65, "%.1f", 0, NULL, NULL, NULL }, + { "ups.realpower", 0, NULL, "WA\r", "", 80, '(', "", 37, 41, "%.1f", 0, NULL, NULL, qx_multiply_x1000 }, + { "ups.power", 0, NULL, "WA\r", "", 80, '(', "", 43, 47, "%.1f", 0, NULL, NULL, qx_multiply_x1000 }, + { "ups.load", 0, NULL, "WA\r", "", 80, '(', "", 67, 69, "%.0f", 0, NULL, NULL, NULL }, + + /* + * > [Q1\r] + * < [(MMM.M NNN.N PPP.P QQQ RR.R S.SS TT.T b7b6b5b4b3b2b1b0\r] + * 012345678901234567890123456789012345678 9 0 1 2 3 4 5 6 + * 0 1 2 3 4 + */ + + /* Status bits */ + { "ups.status", 0, NULL, "Q1\r", "", 47, '(', "", 38, 38, NULL, QX_FLAG_QUICK_POLL, NULL, NULL, blazer_process_status_bits }, /* Utility Fail (Immediate) */ + { "ups.status", 0, NULL, "Q1\r", "", 47, '(', "", 39, 39, NULL, QX_FLAG_QUICK_POLL, NULL, NULL, blazer_process_status_bits }, /* Battery Low */ + { "ups.status", 0, NULL, "Q1\r", "", 47, '(', "", 40, 40, NULL, QX_FLAG_QUICK_POLL, NULL, NULL, blazer_process_status_bits }, /* Bypass/Boost or Buck Active */ + { "ups.alarm", 0, NULL, "Q1\r", "", 47, '(', "", 41, 41, NULL, 0, NULL, NULL, blazer_process_status_bits }, /* UPS Failed */ + { "ups.type", 0, NULL, "Q1\r", "", 47, '(', "", 42, 42, "%s", QX_FLAG_STATIC, NULL, NULL, blazer_process_status_bits }, /* UPS Type */ + { "ups.status", 0, NULL, "Q1\r", "", 47, '(', "", 43, 43, NULL, QX_FLAG_QUICK_POLL, NULL, NULL, blazer_process_status_bits }, /* Test in Progress */ + { "ups.status", 0, NULL, "Q1\r", "", 47, '(', "", 44, 44, NULL, QX_FLAG_QUICK_POLL, NULL, NULL, blazer_process_status_bits }, /* Shutdown Active */ + { "ups.beeper.status", 0, NULL, "Q1\r", "", 47, '(', "", 45, 45, "%s", 0, NULL, NULL, blazer_process_status_bits }, /* Beeper status */ + + /* Instant commands */ + { "beeper.toggle", 0, NULL, "Q\r", "", 0, 0, "", 0, 0, NULL, QX_FLAG_CMD, NULL, NULL, NULL }, + { "load.off", 0, NULL, "S00R0000\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 }, + { "shutdown.return", 0, NULL, "S%s\r", "", 0, 0, "", 0, 0, NULL, QX_FLAG_CMD, NULL, NULL, blazer_process_command }, + { "shutdown.stayoff", 0, NULL, "S%sR0000\r", "", 0, 0, "", 0, 0, NULL, QX_FLAG_CMD, NULL, NULL, blazer_process_command }, + { "shutdown.stop", 0, NULL, "C\r", "", 0, 0, "", 0, 0, NULL, QX_FLAG_CMD, NULL, NULL, NULL }, + { "test.battery.start", 0, NULL, "T%02d\r", "", 0, 0, "", 0, 0, NULL, QX_FLAG_CMD, NULL, NULL, blazer_process_command }, + { "test.battery.start.deep", 0, NULL, "TL\r", "", 0, 0, "", 0, 0, NULL, QX_FLAG_CMD, NULL, NULL, NULL }, + { "test.battery.start.quick", 0, NULL, "T\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 }, + + /* Server-side settable vars */ + { "ups.delay.start", ST_FLAG_RW, blazer_r_ondelay, NULL, "", 0, 0, "", 0, 0, DEFAULT_ONDELAY, QX_FLAG_ABSENT | QX_FLAG_SETVAR | QX_FLAG_RANGE, NULL, NULL, blazer_process_setvar }, + { "ups.delay.shutdown", ST_FLAG_RW, blazer_r_offdelay, NULL, "", 0, 0, "", 0, 0, DEFAULT_OFFDELAY, QX_FLAG_ABSENT | QX_FLAG_SETVAR | QX_FLAG_RANGE, NULL, NULL, blazer_process_setvar }, + + /* End of structure. */ + { NULL, 0, NULL, NULL, "", 0, 0, "", 0, 0, NULL, 0, NULL, NULL, NULL } +}; + +/* Testing table */ +#ifdef TESTING +static testing_t q2_testing[] = { + { "Q2\r", "(226.6 224.3 228.0 226.6 229.9 000.0 000.0 000 000 000 49.9 327.6 18.0 00000000 999.24 100 02 00 00 00 00 00000000 11\r", -1 }, + { "WA\r", "(001.4 000.0 000.0 001.4 000.0 000.0 001.4 001.4 006.5 000.0 000.0 007 00000000\r", -1 }, + { "Q1\r", "(215.0 195.0 230.0 014 49.0 22.7 30.0 00000000\r", -1 }, + { "Q\r", "", -1 }, + { "S03\r", "", -1 }, + { "C\r", "", -1 }, + { "S02R0005\r", "", -1 }, + { "S.5R0000\r", "", -1 }, + { "T04\r", "", -1 }, + { "TL\r", "", -1 }, + { "T\r", "", -1 }, + { "CT\r", "", -1 }, + { NULL } +}; +#endif /* TESTING */ + +/* == Support functions == */ + +/* This function allows to process input/output transformers topology bits */ +static int q2_process_topology_bits(item_t *item, char *value, const size_t valuelen) +{ + char *val = ""; + + if (strspn(item->value, "01") != 1) { + upsdebugx(3, "%s: unexpected value %s@%d->%s", __func__, item->value, item->from, item->value); + return -1; + } + + switch (item->from) + { + case 115: /* Input topology - experimental.input.topology */ + + if (item->value[0] == '1') + val = "star"; + else if (item->value[0] == '0') + val = "delta"; + else + val = "unknown"; + break; + + case 116: /* Output topology - experimental.output.topology */ + + if (item->value[0] == '1') + val = "star"; + else if (item->value[0] == '0') + val = "delta"; + else + val = "unknown"; + break; + + default: + /* Don't know what happened */ + return -1; + } + + snprintf(value, valuelen, "%s", val); + + return 0; +} + +/* This function allows the subdriver to "claim" a device: + * return 1 if the device is supported by this subdriver, else 0 + */ +static int q2_claim(void) +{ + /* We need Q2, Q1, and WA to use this subdriver (WA only if user hasn't requested 'nooutstats' flag) */ + /* TODO think whether we should check for BPS (and bypass-related variables) too */ + struct { + char *var; + char *cmd; + } mandatory[] = { + { "input.L1-N.voltage", "Q2" }, + { "ups.type", "Q1" }, + { "output.L1.current", "WA"}, + { NULL, NULL } + }; + int vari; + char *sp, *cmd; + item_t *item; + + for (vari = 0; mandatory[vari].var; vari++) { + sp = mandatory[vari].var; + cmd = mandatory[vari].cmd; + + /* Ignore checks for 'WA' command support if explicitly asked */ + if (testvar("nooutstats") && (strcasecmp(cmd, "WA") == 0)) + continue; + + item = find_nut_info(sp, 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 */ + if (ups_infoval_set(item) != 1) + return 0; + } + + return 1; +} + +/* Subdriver-specific initups */ +static void q2_initups(void) +{ + int nr, nos, isb; + item_t *item; + + nr = testvar("norating"); + nos = testvar("nooutstats"); + isb = testvar("ignoresab"); + + if (!nr && !nos && !isb) + return; + + for (item = q2_qx2nut; item->info_type != NULL; item++) { + + if (!item->command) + continue; + + /* norating */ + if (nr && (strcasecmp(item->command, "F\r") == 0)) { + upsdebugx(2, "%s: skipping %s", __func__, item->info_type); + item->qxflags |= QX_FLAG_SKIP; + } + + /* nooutstats */ + if (nos && (strcasecmp(item->command, "WA\r") == 0)) { + upsdebugx(2, "%s: skipping %s", __func__, item->info_type); + item->qxflags |= QX_FLAG_SKIP; + } + + /* ignoresab */ + if (isb && (strcasecmp(item->info_type, "ups.status") == 0) && item->from == 44 && item->to == 44) { + upsdebugx(2, "%s: skipping %s ('Shutdown Active' bit)", __func__, item->info_type); + item->qxflags |= QX_FLAG_SKIP; + } + } +} + +/* Subdriver-specific initinfo */ +static void q2_initinfo(void) +{ + dstate_setinfo("input.phases", "%d", 3); + dstate_setinfo("output.phases", "%d", 3); +} + +/* Subdriver-specific flags/vars */ +static void q2_makevartable(void) +{ + addvar(VAR_FLAG, "norating", "Skip reading rating information from UPS"); + addvar(VAR_FLAG, "nooutstats", "Skip reading output load stats information from UPS"); + + blazer_makevartable_light(); +} + +/* Subdriver interface */ +subdriver_t q2_subdriver = { + Q2_VERSION, + q2_claim, + q2_qx2nut, + q2_initups, + q2_initinfo, + q2_makevartable, + "ACK", + NULL, +#ifdef TESTING + q2_testing, +#endif /* TESTING */ +}; diff --git a/drivers/nutdrv_qx_q2.h b/drivers/nutdrv_qx_q2.h new file mode 100644 index 0000000000..6ce138ca34 --- /dev/null +++ b/drivers/nutdrv_qx_q2.h @@ -0,0 +1,29 @@ +/* nutdrv_qx_q2.h - Subdriver for Q2 Megatec protocol variant + * + * Copyright (C) + * 2024 Viktor Drobot + * + * 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 + * + */ + +#ifndef NUTDRV_QX_Q2_H +#define NUTDRV_QX_Q2_H + +#include "nutdrv_qx.h" + +extern subdriver_t q2_subdriver; + +#endif /* NUTDRV_QX_Q2_H */ diff --git a/drivers/nutdrv_qx_q6.c b/drivers/nutdrv_qx_q6.c new file mode 100644 index 0000000000..243d44d7b5 --- /dev/null +++ b/drivers/nutdrv_qx_q6.c @@ -0,0 +1,302 @@ +/* nutdrv_qx_q6.c - Subdriver for Q6 Megatec protocol variant + * + * Copyright (C) + * 2024 Viktor Drobot + * + * 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_q6.h" + +#define Q6_VERSION "Q6 0.01" + +/* Support functions */ +static int q6_process_topology_bits(item_t *item, char *value, const size_t valuelen); +static int q6_claim(void); +static void q6_initups(void); +static void q6_initinfo(void); +static void q6_makevartable(void); + +/* qx2nut lookup table */ +static item_t q6_qx2nut[] = { + + /* + * > [Q6\r] + * < [(MMM.M MMM.M MMM.M NN.N PPP.P PPP.P PPP.P RR.R QQQ QQQ QQQ SSS.S VVV.V TT.T ttttt CCC KB ffffffff wwwwwwww YO\r] + * 01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789 + * 0 1 2 3 4 5 6 7 8 9 10 + */ + + /* Common parameters */ + { "input.L1-N.voltage", 0, NULL, "Q6\r", "", 110, '(', "", 1, 5, "%.1f", 0, NULL, NULL, NULL }, + { "input.L2-N.voltage", 0, NULL, "Q6\r", "", 110, '(', "", 7, 11, "%.1f", 0, NULL, NULL, NULL }, + { "input.L3-N.voltage", 0, NULL, "Q6\r", "", 110, '(', "", 13, 17, "%.1f", 0, NULL, NULL, NULL }, + { "input.frequency", 0, NULL, "Q6\r", "", 110, '(', "", 19, 22, "%.1f", 0, NULL, NULL, NULL }, + { "output.L1-N.voltage", 0, NULL, "Q6\r", "", 110, '(', "", 24, 28, "%.1f", 0, NULL, NULL, NULL }, + { "output.L2-N.voltage", 0, NULL, "Q6\r", "", 110, '(', "", 30, 34, "%.1f", 0, NULL, NULL, NULL }, + { "output.L3-N.voltage", 0, NULL, "Q6\r", "", 110, '(', "", 36, 40, "%.1f", 0, NULL, NULL, NULL }, + { "output.frequency", 0, NULL, "Q6\r", "", 110, '(', "", 42, 45, "%.1f", 0, NULL, NULL, NULL }, + { "output.L1.load", 0, NULL, "Q6\r", "", 110, '(', "", 47, 49, "%.0f", 0, NULL, NULL, NULL }, + { "output.L2.load", 0, NULL, "Q6\r", "", 110, '(', "", 51, 53, "%.0f", 0, NULL, NULL, NULL }, + { "output.L3.load", 0, NULL, "Q6\r", "", 110, '(', "", 55, 57, "%.0f", 0, NULL, NULL, NULL }, + { "ups.temperature", 0, NULL, "Q6\r", "", 110, '(', "", 71, 74, "%.1f", 0, NULL, NULL, NULL }, + { "battery.voltage", 0, NULL, "Q6\r", "", 110, '(', "", 59, 63, "%.1f", 0, NULL, NULL, qx_multiply_battvolt }, + { "battery.runtime", 0, NULL, "Q6\r", "", 110, '(', "", 76, 80, "%.0f", 0, NULL, NULL, NULL }, + { "battery.charge", 0, NULL, "Q6\r", "", 110, '(', "", 82, 84, "%.0f", 0, NULL, NULL, NULL }, + { "experimental.battery.voltage.negative", 0, NULL, "Q6\r", "", 110, '(', "", 65, 69, "%.1f", 0, NULL, NULL, qx_multiply_battvolt }, /* Negative battery voltage */ + { "experimental.input.topology", 0, NULL, "Q6\r", "", 110, '(', "", 107, 107, "%s", QX_FLAG_STATIC, NULL, NULL, q6_process_topology_bits }, /* Input transformer topology */ + { "experimental.output.topology", 0, NULL, "Q6\r", "", 110, '(', "", 108, 108, "%s", QX_FLAG_STATIC, NULL, NULL, q6_process_topology_bits }, /* Output transformer topology */ + /* TODO handle KB (86-87) properly: mb use "experimental." vars for that */ + + /* + * > [WA\r] + * < [(WWW.W WWW.W WWW.W VVV.V VVV.V VVV.V TTT.T SSS.S AAA.A AAA.A AAA.A QQQ b7b6b5b4b3b2b1b0\r] + * 012345678901234567890123456789012345678901234567890123456789012345678901 2 3 4 5 6 7 8 9 + * 0 1 2 3 4 5 6 7 + */ + + /* Output consumption parameters */ + { "output.L1.realpower", 0, NULL, "WA\r", "", 80, '(', "", 1, 5, "%.1f", 0, NULL, NULL, qx_multiply_x1000 }, + { "output.L2.realpower", 0, NULL, "WA\r", "", 80, '(', "", 7, 11, "%.1f", 0, NULL, NULL, qx_multiply_x1000 }, + { "output.L3.realpower", 0, NULL, "WA\r", "", 80, '(', "", 13, 17, "%.1f", 0, NULL, NULL, qx_multiply_x1000 }, + { "output.L1.power", 0, NULL, "WA\r", "", 80, '(', "", 19, 23, "%.1f", 0, NULL, NULL, qx_multiply_x1000 }, + { "output.L2.power", 0, NULL, "WA\r", "", 80, '(', "", 25, 29, "%.1f", 0, NULL, NULL, qx_multiply_x1000 }, + { "output.L3.power", 0, NULL, "WA\r", "", 80, '(', "", 31, 35, "%.1f", 0, NULL, NULL, qx_multiply_x1000 }, + { "output.L1.current", 0, NULL, "WA\r", "", 80, '(', "", 49, 53, "%.1f", 0, NULL, NULL, NULL }, + { "output.L2.current", 0, NULL, "WA\r", "", 80, '(', "", 55, 59, "%.1f", 0, NULL, NULL, NULL }, + { "output.L3.current", 0, NULL, "WA\r", "", 80, '(', "", 61, 65, "%.1f", 0, NULL, NULL, NULL }, + { "ups.realpower", 0, NULL, "WA\r", "", 80, '(', "", 37, 41, "%.1f", 0, NULL, NULL, qx_multiply_x1000 }, + { "ups.power", 0, NULL, "WA\r", "", 80, '(', "", 43, 47, "%.1f", 0, NULL, NULL, qx_multiply_x1000 }, + { "ups.load", 0, NULL, "WA\r", "", 80, '(', "", 67, 69, "%.0f", 0, NULL, NULL, NULL }, + + /* + * > [Q1\r] + * < [(MMM.M NNN.N PPP.P QQQ RR.R S.SS TT.T b7b6b5b4b3b2b1b0\r] + * 012345678901234567890123456789012345678 9 0 1 2 3 4 5 6 + * 0 1 2 3 4 + */ + + /* Status bits */ + { "ups.status", 0, NULL, "Q1\r", "", 47, '(', "", 38, 38, NULL, QX_FLAG_QUICK_POLL, NULL, NULL, blazer_process_status_bits }, /* Utility Fail (Immediate) */ + { "ups.status", 0, NULL, "Q1\r", "", 47, '(', "", 39, 39, NULL, QX_FLAG_QUICK_POLL, NULL, NULL, blazer_process_status_bits }, /* Battery Low */ + { "ups.status", 0, NULL, "Q1\r", "", 47, '(', "", 40, 40, NULL, QX_FLAG_QUICK_POLL, NULL, NULL, blazer_process_status_bits }, /* Bypass/Boost or Buck Active */ + { "ups.alarm", 0, NULL, "Q1\r", "", 47, '(', "", 41, 41, NULL, 0, NULL, NULL, blazer_process_status_bits }, /* UPS Failed */ + { "ups.type", 0, NULL, "Q1\r", "", 47, '(', "", 42, 42, "%s", QX_FLAG_STATIC, NULL, NULL, blazer_process_status_bits }, /* UPS Type */ + { "ups.status", 0, NULL, "Q1\r", "", 47, '(', "", 43, 43, NULL, QX_FLAG_QUICK_POLL, NULL, NULL, blazer_process_status_bits }, /* Test in Progress */ + { "ups.status", 0, NULL, "Q1\r", "", 47, '(', "", 44, 44, NULL, QX_FLAG_QUICK_POLL, NULL, NULL, blazer_process_status_bits }, /* Shutdown Active */ + { "ups.beeper.status", 0, NULL, "Q1\r", "", 47, '(', "", 45, 45, "%s", 0, NULL, NULL, blazer_process_status_bits }, /* Beeper status */ + + /* Instant commands */ + { "beeper.toggle", 0, NULL, "Q\r", "", 0, 0, "", 0, 0, NULL, QX_FLAG_CMD, NULL, NULL, NULL }, + { "load.off", 0, NULL, "S00R0000\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 }, + { "shutdown.return", 0, NULL, "S%s\r", "", 0, 0, "", 0, 0, NULL, QX_FLAG_CMD, NULL, NULL, blazer_process_command }, + { "shutdown.stayoff", 0, NULL, "S%sR0000\r", "", 0, 0, "", 0, 0, NULL, QX_FLAG_CMD, NULL, NULL, blazer_process_command }, + { "shutdown.stop", 0, NULL, "C\r", "", 0, 0, "", 0, 0, NULL, QX_FLAG_CMD, NULL, NULL, NULL }, + { "test.battery.start", 0, NULL, "T%02d\r", "", 0, 0, "", 0, 0, NULL, QX_FLAG_CMD, NULL, NULL, blazer_process_command }, + { "test.battery.start.deep", 0, NULL, "TL\r", "", 0, 0, "", 0, 0, NULL, QX_FLAG_CMD, NULL, NULL, NULL }, + { "test.battery.start.quick", 0, NULL, "T\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 }, + + /* Server-side settable vars */ + { "ups.delay.start", ST_FLAG_RW, blazer_r_ondelay, NULL, "", 0, 0, "", 0, 0, DEFAULT_ONDELAY, QX_FLAG_ABSENT | QX_FLAG_SETVAR | QX_FLAG_RANGE, NULL, NULL, blazer_process_setvar }, + { "ups.delay.shutdown", ST_FLAG_RW, blazer_r_offdelay, NULL, "", 0, 0, "", 0, 0, DEFAULT_OFFDELAY, QX_FLAG_ABSENT | QX_FLAG_SETVAR | QX_FLAG_RANGE, NULL, NULL, blazer_process_setvar }, + + /* End of structure. */ + { NULL, 0, NULL, NULL, "", 0, 0, "", 0, 0, NULL, 0, NULL, NULL, NULL } +}; + +/* Testing table */ +#ifdef TESTING +static testing_t q6_testing[] = { + { "Q6\r", "(227.0 225.6 230.0 50.0 229.9 000.0 000.0 49.9 007 000 000 327.8 000.0 23.0 06932 100 32 00000000 00000000 11\r", -1 }, + { "WA\r", "(001.4 000.0 000.0 001.4 000.0 000.0 001.4 001.4 006.5 000.0 000.0 007 00000000\r", -1 }, + { "Q1\r", "(215.0 195.0 230.0 014 49.0 22.7 30.0 00000000\r", -1 }, + { "Q\r", "", -1 }, + { "S03\r", "", -1 }, + { "C\r", "", -1 }, + { "S02R0005\r", "", -1 }, + { "S.5R0000\r", "", -1 }, + { "T04\r", "", -1 }, + { "TL\r", "", -1 }, + { "T\r", "", -1 }, + { "CT\r", "", -1 }, + { NULL } +}; +#endif /* TESTING */ + +/* == Support functions == */ + +/* This function allows to process input/output transformers topology bits */ +static int q6_process_topology_bits(item_t *item, char *value, const size_t valuelen) +{ + char *val = ""; + + if (strspn(item->value, "01") != 1) { + upsdebugx(3, "%s: unexpected value %s@%d->%s", __func__, item->value, item->from, item->value); + return -1; + } + + switch (item->from) + { + case 107: /* Input topology - experimental.input.topology */ + + if (item->value[0] == '1') + val = "star"; + else if (item->value[0] == '0') + val = "delta"; + else + val = "unknown"; + break; + + case 108: /* Output topology - experimental.output.topology */ + + if (item->value[0] == '1') + val = "star"; + else if (item->value[0] == '0') + val = "delta"; + else + val = "unknown"; + break; + + default: + /* Don't know what happened */ + return -1; + } + + snprintf(value, valuelen, "%s", val); + + return 0; +} + +/* This function allows the subdriver to "claim" a device: + * return 1 if the device is supported by this subdriver, else 0 + */ +static int q6_claim(void) +{ + /* We need Q6, Q1, and WA to use this subdriver (WA only if user hasn't requested 'nooutstats' flag) */ + /* TODO think whether we should check for BPS (and bypass-related variables) too */ + struct { + char *var; + char *cmd; + } mandatory[] = { + { "input.L1-N.voltage", "Q6" }, + { "ups.type", "Q1" }, + { "output.L1.current", "WA"}, + { NULL, NULL } + }; + int vari; + char *sp, *cmd; + item_t *item; + + for (vari = 0; mandatory[vari].var; vari++) { + sp = mandatory[vari].var; + cmd = mandatory[vari].cmd; + + /* Ignore checks for 'WA' command support if explicitly asked */ + if (testvar("nooutstats") && (strcasecmp(cmd, "WA") == 0)) + continue; + + item = find_nut_info(sp, 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 */ + if (ups_infoval_set(item) != 1) + return 0; + } + + return 1; +} + +/* Subdriver-specific initups */ +static void q6_initups(void) +{ + int nr, nos, isb; + item_t *item; + + nr = testvar("norating"); + nos = testvar("nooutstats"); + isb = testvar("ignoresab"); + + if (!nr && !nos && !isb) + return; + + for (item = q6_qx2nut; item->info_type != NULL; item++) { + + if (!item->command) + continue; + + /* norating */ + if (nr && (strcasecmp(item->command, "F\r") == 0)) { + upsdebugx(2, "%s: skipping %s", __func__, item->info_type); + item->qxflags |= QX_FLAG_SKIP; + } + + /* nooutstats */ + if (nos && (strcasecmp(item->command, "WA\r") == 0)) { + upsdebugx(2, "%s: skipping %s", __func__, item->info_type); + item->qxflags |= QX_FLAG_SKIP; + } + + /* ignoresab */ + if (isb && (strcasecmp(item->info_type, "ups.status") == 0) && item->from == 44 && item->to == 44) { + upsdebugx(2, "%s: skipping %s ('Shutdown Active' bit)", __func__, item->info_type); + item->qxflags |= QX_FLAG_SKIP; + } + } +} + +/* Subdriver-specific initinfo */ +static void q6_initinfo(void) +{ + dstate_setinfo("input.phases", "%d", 3); + dstate_setinfo("output.phases", "%d", 3); +} + +/* Subdriver-specific flags/vars */ +static void q6_makevartable(void) +{ + addvar(VAR_FLAG, "norating", "Skip reading rating information from UPS"); + addvar(VAR_FLAG, "nooutstats", "Skip reading output load stats information from UPS"); + + blazer_makevartable_light(); +} + +/* Subdriver interface */ +subdriver_t q6_subdriver = { + Q6_VERSION, + q6_claim, + q6_qx2nut, + q6_initups, + q6_initinfo, + q6_makevartable, + "ACK", + NULL, +#ifdef TESTING + q6_testing, +#endif /* TESTING */ +}; diff --git a/drivers/nutdrv_qx_q6.h b/drivers/nutdrv_qx_q6.h new file mode 100644 index 0000000000..c9ede053da --- /dev/null +++ b/drivers/nutdrv_qx_q6.h @@ -0,0 +1,29 @@ +/* nutdrv_qx_q6.h - Subdriver for Q6 Megatec protocol variant + * + * Copyright (C) + * 2024 Viktor Drobot + * + * 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 + * + */ + +#ifndef NUTDRV_QX_Q6_H +#define NUTDRV_QX_Q6_H + +#include "nutdrv_qx.h" + +extern subdriver_t q6_subdriver; + +#endif /* NUTDRV_QX_Q6_H */