diff --git a/NEWS.adoc b/NEWS.adoc index 101ae2dcd9..b7fe76bc5f 100644 --- a/NEWS.adoc +++ b/NEWS.adoc @@ -73,6 +73,26 @@ https://github.com/networkupstools/nut/milestone/9 and misfired on some platforms, and the way to print had a theoretical potential for buffer overflow. [#2915] + - `nutdrv_qx` driver updates: + * Introduced a `voltronic-axpert` subdriver for Voltronic Axpert inverters + which speak the P30 protocol, currently in a highly experimental state: + with initial support for query commands, but most values are "hidden" + from default NUT builds by being defined in `experimental.*` namespace, + and should also be enabled by `configure --with-unmapped-data-points`. + Development was based on work done in the Voltronic Sunny subdriver in + https://github.com/nickma82/nut/tree/nutdrv_qx_voltronic-sunny_rebased%2Bcommand + [#1407] ++ +Still TODO: + - Implement commands to write to the inverter. + - Remove commented code originating from `sunny` driver. + - Further testing. + - Understand how to map values we can read/set with those devices + to the NUT standard vocabulary at `docs/nut-names.txt` (or extend + it by discussion and agreement with community -- notably to track + a `pv.*` namespace for photovoltaic, separately from `ups.*` etc.) + - Update docs (manpages, acknowledgements...) + - `usbhid-ups` driver updates: * The `cps-hid` subdriver's existing mechanism for fixing broken report descriptors was extended to cover a newly reported case of nominal UPS diff --git a/docs/man/nutdrv_qx.txt b/docs/man/nutdrv_qx.txt index 2ec5f2e610..2b16bfe03e 100644 --- a/docs/man/nutdrv_qx.txt +++ b/docs/man/nutdrv_qx.txt @@ -88,7 +88,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', 'gtec', 'hunnox', 'innovart31', 'masterguard', 'mecer', 'megatec', 'megatec/old', 'mustek', 'q1', 'q2', 'q6', 'voltronic', 'voltronic-qs', 'voltronic-qs-hex' and 'zinto'. +Supported values: 'bestups', 'gtec', 'hunnox', 'innovart31', 'masterguard', 'mecer', 'megatec', 'megatec/old', 'mustek', 'q1', 'q2', 'q6', 'voltronic', 'voltronic-axpert', '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. @@ -244,6 +244,22 @@ The acceptable range is +60..599940+ seconds. The acceptable range is +12..540+ seconds. +VOLTRONIC-AXPERT PROTOCOL +~~~~~~~~~~~~~~~~~~~~~~~~~ + +NOTE: This protocol is currently largely experimental, many features may +be only exposed in specially configured builds of the NUT driver program. + +*reset_to_default*:: +Reset capability options and their limits to safe default values + +*bypass_alarm*:: +Alarm (BEEP!) at Bypass Mode [enabled/disabled] + +*battery_alarm*:: +Alarm (BEEP!) at Battery Mode [enabled/disabled] + + VOLTRONIC PROTOCOL ~~~~~~~~~~~~~~~~~~ @@ -771,6 +787,19 @@ MASTERGUARD UNITS The driver is supposed to support both "new" A series (A700/1000/2000/3000 and their -19 cousins) and E series (E60/100/200) but was tested only on A due to lack of E hardware. +VOLTRONIC-AXPERT UNITS +~~~~~~~~~~~~~~~~~~~~~~~~~ + +This protocol supports Voltronic Power Axpert inverters, based on P30 protocol +(used e.g. in photovoltaic equipment, often paired with an UPS charged by the +solar panels). + +The code is currently largely experimental, with data point and command names +not aligned with concepts that are part of NUT standard vocabulary (this many +of them are prefixed with `experimental.` namespace). In fact, many features +may be only exposed in specially configured builds of the NUT driver program. + + VOLTRONIC-QS UNITS ~~~~~~~~~~~~~~~~~~ diff --git a/docs/nut.dict b/docs/nut.dict index 7022f56b0b..575ae0a0fc 100644 --- a/docs/nut.dict +++ b/docs/nut.dict @@ -1,4 +1,4 @@ -personal_ws-1.1 en 3473 utf-8 +personal_ws-1.1 en 3476 utf-8 AAC AAS ABI @@ -80,6 +80,7 @@ Autobook Autoconfigure Avocent Axel +Axpert Axxium BATGNn BATNn @@ -1616,6 +1617,7 @@ avahi avr awd awk +axpert b'some b'string bAlternateSetting @@ -2814,6 +2816,7 @@ pts ptv pty pulizzi +pv pw pwd pwmib diff --git a/drivers/Makefile.am b/drivers/Makefile.am index 562cf82095..8f3e6a072d 100644 --- a/drivers/Makefile.am +++ b/drivers/Makefile.am @@ -389,12 +389,15 @@ nutdrv_qx_CFLAGS += -DQX_USB nutdrv_qx_SOURCES += $(LIBUSB_IMPL) usb-common.c nutdrv_qx_LDADD += $(LIBUSB_LIBS) endif WITH_USB -NUTDRV_QX_SUBDRIVERS = nutdrv_qx_bestups.c nutdrv_qx_blazer-common.c \ +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_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_mustek.c nutdrv_qx_q1.c nutdrv_qx_q2.c nutdrv_qx_q6.c \ + nutdrv_qx_voltronic.c common_voltronic-crc.c \ + nutdrv_qx_voltronic-axpert.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_gtec.c nutdrv_qx_SOURCES += $(NUTDRV_QX_SUBDRIVERS) @@ -415,10 +418,13 @@ dist_noinst_HEADERS = \ safenet.h serial.h sms_ser.h snmp-ups.h solis.h tripplite.h tripplite-hid.h \ upshandler.h usb-common.h usbhid-ups.h powercom-hid.h compaq-mib.h idowell-hid.h \ apcsmart.h apcsmart_tabs.h apcsmart-old.h apcupsd-ups.h cyberpower-mib.h riello.h openups-hid.h \ - delta_ups-mib.h nutdrv_qx.h nutdrv_qx_bestups.h nutdrv_qx_blazer-common.h \ + delta_ups-mib.h \ + nutdrv_qx_bestups.h nutdrv_qx_blazer-common.h \ nutdrv_qx_gtec.h 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_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 \ + nutdrv_qx.h common_voltronic-crc.h nutdrv_qx_voltronic.h \ + nutdrv_qx_voltronic-axpert.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 \ apc-pdu-mib.h apc-epdu-mib.h ecoflow-hid.h ever-hid.h eaton-pdu-genesis2-mib.h eaton-pdu-marlin-mib.h eaton-pdu-marlin-helpers.h \ diff --git a/drivers/common_voltronic-crc.c b/drivers/common_voltronic-crc.c new file mode 100644 index 0000000000..2e05603883 --- /dev/null +++ b/drivers/common_voltronic-crc.c @@ -0,0 +1,304 @@ +/* common_voltronic-crc.c - Common CRC routines for Voltronic Power devices + * + * Copyright (C) + * 2014 Daniele Pezzini + * + * 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 "common.h" +#include "common_voltronic-crc.h" + +/* CRC table - filled at runtime by common_voltronic_crc_init() */ +static unsigned short crc_table[256]; + +/* Flag (0/1) just to be sure all is properly initialized */ +static int initialized = 0; + +/* Fill CRC table: this function MUST be called before anything else that needs to perform CRC operations */ +static void common_voltronic_crc_init(void) +{ + unsigned short dividend; + + /* Already been here */ + if (initialized) + return; + + /* Compute remainder of each possible dividend */ + for (dividend = 0; dividend < 256; dividend++) { + + unsigned short bit; + /* Start with dividend followed by zeros */ + unsigned long remainder = dividend << 8; + + /* Modulo 2 division, a bit at a time */ + for (bit = 0; bit < 8; bit++) + /* Try to divide the current data bit */ + if (remainder & 0x8000) + remainder = (remainder << 1) ^ 0x1021; + else + remainder <<= 1; + + /* Store the result into the table */ + crc_table[dividend] = remainder & 0xffff; + + } + + /* Ready, now */ + initialized = 1; +} + +/* See header file for details */ +unsigned short common_voltronic_crc_compute(const char *input, const size_t len) +{ + unsigned short crc, crc_MSB, crc_LSB; + unsigned long remainder = 0; + size_t byte; + + /* Make sure all is ready */ + if (!initialized) + common_voltronic_crc_init(); + + /* Divide *input* by the polynomial, a byte at a time */ + for (byte = 0; byte < len; byte++) + remainder = (remainder << 8) ^ crc_table[(input[byte] ^ (remainder >> 8)) & 0xff]; + + /* The final remainder is the CRC */ + crc = remainder & 0xffff; + + /* Escape characters with a special meaning */ + + crc_MSB = (crc >> 8) & 0xff; + if ( + crc_MSB == 10 || /* LF */ + crc_MSB == 13 || /* CR */ + crc_MSB == 40 /* ( */ + ) + crc_MSB++; + + crc_LSB = crc & 0xff; + if ( + crc_LSB == 10 || /* LF */ + crc_LSB == 13 || /* CR */ + crc_LSB == 40 /* ( */ + ) + crc_LSB++; + + crc = ((crc_MSB & 0xff) << 8) + crc_LSB; + + return crc; +} + +/* See header file for details */ +unsigned short common_voltronic_crc_calc(const char *input, const size_t inputlen) +{ + size_t len; + char *cr = memchr(input, '\r', inputlen); + + /* No CR, fall back to string length (and hope *input* doesn't contain inner '\0's) */ + if (cr == NULL) + len = strlen(input); + else + len = cr - input; + + /* At least 1 byte expected */ + if (!len) + return -1; + + /* Compute (and return) CRC */ + return common_voltronic_crc_compute(input, len); +} + +/* See header file for details */ +int common_voltronic_crc_calc_and_add(const char *input, const size_t inputlen, char *output, const size_t outputlen) +{ + unsigned short crc, crc_MSB, crc_LSB; + size_t len; + char *cr = memchr(input, '\r', inputlen); + + /* No CR, fall back to string length (and hope *input* doesn't contain inner '\0's) */ + if (cr == NULL) + len = strlen(input); + else + len = cr - input; + + /* At least 1 byte expected */ + if (!len) + return -1; + + /* To accomodate CRC, *output* must have space for at least 2 bytes more than the actually used size of *input*. + * Also, pretend that *input* is a valid null-terminated string and so reserve the final byte in *output* for the terminating '\0'. */ + if ( + (cr == NULL && outputlen < len + 3) || /* 2-bytes CRC + 1 byte for terminating '\0' */ + (cr != NULL && outputlen < len + 4) /* 2-bytes CRC + 1 byte for trailing CR + 1 byte for terminating '\0' */ + ) + return -1; + + /* Compute CRC */ + crc = common_voltronic_crc_compute(input, len); + crc_MSB = (crc >> 8) & 0xff; + crc_LSB = crc & 0xff; + + /* Clear *output* */ + memset(output, 0, outputlen); + + /* Copy *input* to *output* */ + memcpy(output, input, len); + + /* Write CRC to *output* */ + output[len++] = crc_MSB; + output[len++] = crc_LSB; + + /* Reinstate the trailing CR in *output*, if appropriate */ + if (cr != NULL) + output[len++] = '\r'; + + return (int)len; +} + +/* See header file for details */ +int common_voltronic_crc_calc_and_add_m(char *input, const size_t inputlen) +{ + int len; + char *buf = xcalloc(inputlen, sizeof(char)); + + if (!buf) + return -1; + + /* Compute CRC and copy *input*, with CRC added to it, to buf */ + len = common_voltronic_crc_calc_and_add(input, inputlen, buf, inputlen); + + /* Failed */ + if (len == -1) { + free(buf); + return -1; + } + + /* Successfully computed CRC and copied *input*, with CRC added to it, to buf */ + + /* Clear *input* */ + memset(input, 0, inputlen); + + /* Copy back buf to *input* */ + memcpy(input, buf, len); + + free(buf); + return len; +} + +/* See header file for details */ +int common_voltronic_crc_check(const char *input, const size_t inputlen) +{ + unsigned short crc, crc_MSB, crc_LSB; + char *cr = memchr(input, '\r', inputlen); + size_t len; + + /* No CR, fall back to string length (and hope *input* doesn't contain inner '\0's) */ + if (cr == NULL) + len = strlen(input); + else + len = cr - input; + + /* Minimum length: 1 byte + 2 bytes CRC -> 3 */ + if (len < 3) + return -1; + + /* Compute CRC */ + crc = common_voltronic_crc_compute(input, len - 2); + crc_MSB = (crc >> 8) & 0xff; + crc_LSB = crc & 0xff; + + /* Fail */ + if ( + crc_MSB != (unsigned char)input[len - 2] || + crc_LSB != (unsigned char)input[len - 1] + ) + return -1; + + /* Success */ + return 0; +} + +/* See header file for details */ +int common_voltronic_crc_check_and_remove(const char *input, const size_t inputlen, char *output, const size_t outputlen) +{ + char *cr; + size_t len; + + /* Failed to check *input* CRC */ + if (common_voltronic_crc_check(input, inputlen)) + return -1; + + /* *input* successfully validated -> remove CRC bytes */ + + cr = memchr(input, '\r', inputlen); + /* No CR, fall back to string length */ + if (cr == NULL) + len = strlen(input); + else + len = cr - input; + + /* *output* must have space for at least 2 bytes less than the actually used size of *input*. + * Also, pretend that *input* is a valid null-terminated string and so reserve the final byte in *output* for the terminating '\0'. */ + len -= 2; /* Consider 2-bytes CRC length */ + if ( + (cr == NULL && outputlen < len + 1) || /* 1 byte for terminating '\0' */ + (cr != NULL && outputlen < len + 2) /* 1 byte for terminating '\0' + 1 byte for trailing CR; 2-byte CRC */ + ) + return -1; + + /* Clear *output* */ + memset(output, 0, outputlen); + + /* Copy *input* to *output* */ + memcpy(output, input, len); + + /* Reinstate the trailing CR in *output*, if appropriate */ + if (cr != NULL) + output[len++] = '\r'; + + return (int)len; +} + +/* See header file for details */ +int common_voltronic_crc_check_and_remove_m(char *input, const size_t inputlen) +{ + int len; + char *buf = xcalloc(inputlen, sizeof(char)); + + if (!buf) + return -1; + + /* Check CRC and copy *input*, purged of the CRC, to buf */ + len = common_voltronic_crc_check_and_remove(input, inputlen, buf, inputlen); + + /* Failed */ + if (len == -1) { + free(buf); + return -1; + } + + /* Successfully checked CRC and copied *input*, purged of the CRC, to buf */ + + /* Clear *input* */ + memset(input, 0, inputlen); + + /* Copy back buf to *input* */ + memcpy(input, buf, len); + + free(buf); + return len; +} diff --git a/drivers/common_voltronic-crc.h b/drivers/common_voltronic-crc.h new file mode 100644 index 0000000000..3731127fe7 --- /dev/null +++ b/drivers/common_voltronic-crc.h @@ -0,0 +1,106 @@ +/* common_voltronic-crc.h - Common CRC routines for Voltronic Power devices + * + * Copyright (C) + * 2014 Daniele Pezzini + * + * 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 COMMON_VOLTRONIC_CRC_H +#define COMMON_VOLTRONIC_CRC_H + +/* NOTE + * ---- + * The provided functions implement a simple 2-bytes non-reflected CRC (polynomial: 0x1021) as used in some Voltronic Power devices (LF, CR and '(' are 'escaped'). + * + * Apart from the basic functions (that require you to know the exact length, *len*, of what you feed them) to compute CRC and check whether a given byte array, *input*, is CRC-valid or not, some helper functions are provided, that automagically calculate the length of the part of a given *input* (of max size *inputlen*) to be used. + * When using one of these functions (that, having been developed to work with Voltronic Power devices, expect a CR-terminated string), take into consideration that: + * - *input* is considered first as a CR-terminated byte array that may contain inner '\0's and its length is calculated till the position of the expected CR; + * - if a CR cannot be found, then, and only then, *input* is considered as a null-terminated byte string and its length calculated accordingly. + * Therefore, the presence of one (or more) CR automatically: + * - discards whatever stands after (the first CR); + * - includes whatever stands before (the first CR). + * So, as a CR takes precedence, if you know *input* is not (or may be not) CR-terminated, make sure the remaining bytes after the end of the string till *inputlen*-length are all 0'ed (or at least don't contain any CR), if you don't want to risk them being included. + * Also, if you know *input* may contain inner CRs, you better use one of the functions that don't try to guess the part of *input* to be used and cook up instead your own routines using only the provided basic functions to compute and check CRC. + * + * Don't forget, too, that CRC may contain a '\0' and hence null-terminated strings will be fooled. */ + +#include + +/* Compute CRC of a given *input* till the length of *len* bytes. + * Return: + * - the CRC computed from *len* bytes of *input*. */ +unsigned short common_voltronic_crc_compute(const char *input, const size_t len); + +/* Compute CRC (till the first CR, if any, or till the end of the string) of *input* (of max size *inputlen*). + * Please note that *input* must be: + * - at least 1 byte long (not counting the optional trailing CR and the terminating '\0' byte); + * - either a valid null-terminated byte string or CR-terminated. + * Return: + * - -1, on failure (i.e: *input* not fulfilling the aforementioned conditions); + * - the CRC computed from *input*, on success. */ +unsigned short common_voltronic_crc_calc(const char *input, const size_t inputlen); + +/* Compute CRC (till the first CR, if any, or till the end of the string) of *input* (of max size *inputlen*), then write to *output* (of max size *outputlen*): + * - *input* (minus the trailing CR, if any); + * - the computed 2-bytes CRC; + * - a trailing CR (if present in *input*). + * Please note that: + * - *input* must be at least 1 byte long (not counting the optional trailing CR and the terminating '\0' byte) and either be a valid null-terminated byte string or CR-terminated; + * - *output* must have space, to accomodate the computed 2-bytes CRC, for at least 2 bytes more (not counting the trailing, reserved, byte for the terminating '\0') than the actually used size of *input*. + * Return: + * - -1, on failure (i.e: either *input* or *output* not fulfilling the aforementioned conditions); + * - the number of bytes written to *output*, on success. */ +int common_voltronic_crc_calc_and_add(const char *input, const size_t inputlen, char *output, const size_t outputlen); + +/* Compute CRC (till the first CR, if any, or till the end of the string) of *input* (of max size *inputlen*), then add to it the computed 2-bytes CRC (before the trailing CR, if present). + * Please note that *input* must: + * - be at least 1 byte long (not counting the optional trailing CR and the terminating '\0' byte); + * - either be a valid null-terminated byte string or CR-terminated; + * - have space, to accomodate the computed 2-bytes CRC, for at least 2 bytes more (not counting the trailing, reserved, byte for the terminating '\0') than the actually used size. + * Return: + * - -1, on failure (i.e: *input* not fulfilling the aforementioned conditions); + * - the number of bytes that make up the modified *input*, on success. */ +int common_voltronic_crc_calc_and_add_m(char *input, const size_t inputlen); + +/* Check *input* (of max size *inputlen*) CRC. + * Please note that *input* must be: + * - at least 3 bytes long (not counting the optional trailing CR and the terminating '\0' byte); + * - either a valid null-terminated byte string or CR-terminated. + * Return: + * - -1, on failure (i.e: *input* not fulfilling the aforementioned conditions or not CRC-validated); + * - 0, on success (i.e.: *input* successfully validated). */ +int common_voltronic_crc_check(const char *input, const size_t inputlen); + +/* Check *input* (of max size *inputlen*) CRC and copy *input*, purged of the CRC, to *output* (of max size *outputlen*). + * Please note that: + * - *input* must be at least 3 bytes long (not counting the optional trailing CR and the terminating '\0' byte) and either be a valid null-terminated byte string or CR-terminated; + * - *output* must have space for at least 2 bytes less (not counting the trailing, reserved, byte for the terminating '\0') than the actually used size of *input*. + * Return: + * - -1, on failure (i.e: either *input* or *output* not fulfilling the aforementioned conditions or *input* not CRC-validated); + * - the number of bytes written to *output*, on success. */ +int common_voltronic_crc_check_and_remove(const char *input, const size_t inputlen, char *output, const size_t outputlen); + +/* Check *input* (of max size *inputlen*) CRC and remove it from *input*. + * Please note that *input* must be: + * - at least 3 bytes long (not counting the optional trailing CR and the terminating '\0' byte); + * - either a valid null-terminated byte string or CR-terminated. + * Return: + * - -1, on failure; + * - the number of bytes that make up the modified *input*, on success. */ +int common_voltronic_crc_check_and_remove_m(char *input, const size_t inputlen); + +#endif /* COMMON_VOLTRONIC_CRC_H */ diff --git a/drivers/nutdrv_qx.c b/drivers/nutdrv_qx.c index 6246a42182..5ec5892db2 100644 --- a/drivers/nutdrv_qx.c +++ b/drivers/nutdrv_qx.c @@ -82,6 +82,7 @@ #include "nutdrv_qx_voltronic.h" #include "nutdrv_qx_voltronic-qs.h" #include "nutdrv_qx_voltronic-qs-hex.h" +#include "nutdrv_qx_voltronic-axpert.h" #include "nutdrv_qx_zinto.h" #include "nutdrv_qx_masterguard.h" #include "nutdrv_qx_ablerex.h" @@ -90,6 +91,7 @@ /* Reference list of available non-USB subdrivers */ static subdriver_t *subdriver_list[] = { &voltronic_subdriver, + &voltronic_axpert_subdriver, &voltronic_qs_subdriver, &voltronic_qs_hex_subdriver, &mustek_subdriver, @@ -163,7 +165,7 @@ static struct { /* == Support functions == */ static int subdriver_matcher(void); -static ssize_t qx_command(const char *cmd, char *buf, size_t buflen); +static ssize_t qx_command(const char *cmd, size_t cmdlen, char *buf, size_t buflen); static int qx_process_answer(item_t *item, const size_t len); /* returns just 0 or -1 */ static bool_t qx_ups_walk(walkmode_t mode); static void ups_status_set(void); @@ -577,12 +579,13 @@ static USBDeviceMatcher_t *reopen_matcher = NULL; static USBDeviceMatcher_t *regex_matcher = NULL; static int langid_fix = -1; -static int (*subdriver_command)(const char *cmd, char *buf, size_t buflen) = NULL; +static int (*subdriver_command)(const char *cmd, size_t cmdlen, char *buf, size_t buflen) = NULL; /* Cypress communication subdriver */ -static int cypress_command(const char *cmd, char *buf, size_t buflen) +static int cypress_command(const char *cmd, size_t cmdlen, char *buf, size_t buflen) { char tmp[SMALLBUF]; + size_t tmplen; int ret = 0; size_t i; @@ -595,9 +598,10 @@ static int cypress_command(const char *cmd, char *buf, size_t buflen) /* Send command */ memset(tmp, 0, sizeof(tmp)); - snprintf(tmp, sizeof(tmp), "%s", cmd); + tmplen = cmdlen > sizeof(tmp) ? sizeof(tmp) : cmdlen; + memcpy(tmp, cmd, tmplen); - for (i = 0; i < strlen(tmp); i += (size_t)ret) { + for (i = 0; i < tmplen; i += (size_t)ret) { /* Write data in 8-byte chunks */ /* ret = usb->set_report(udev, 0, (unsigned char *)&tmp[i], 8); */ @@ -653,11 +657,11 @@ static int cypress_command(const char *cmd, char *buf, size_t buflen) } /* SGS communication subdriver */ -static int sgs_command(const char *cmd, char *buf, size_t buflen) +static int sgs_command(const char *cmd, size_t cmdlen, char *buf, size_t buflen) { char tmp[SMALLBUF]; int ret = 0; - size_t cmdlen, i; + size_t i; if (buflen > INT_MAX) { upsdebugx(3, "%s: requested to read too much (%" PRIuSIZE "), " @@ -667,8 +671,6 @@ static int sgs_command(const char *cmd, char *buf, size_t buflen) } /* Send command */ - cmdlen = strlen(cmd); - for (i = 0; i < cmdlen; i += (size_t)ret) { memset(tmp, 0, sizeof(tmp)); @@ -758,9 +760,10 @@ static int sgs_command(const char *cmd, char *buf, size_t buflen) } /* Phoenix communication subdriver */ -static int phoenix_command(const char *cmd, char *buf, size_t buflen) +static int phoenix_command(const char *cmd, size_t cmdlen, char *buf, size_t buflen) { char tmp[SMALLBUF]; + size_t tmplen; int ret; size_t i; @@ -808,9 +811,10 @@ static int phoenix_command(const char *cmd, char *buf, size_t buflen) /* Send command */ memset(tmp, 0, sizeof(tmp)); - snprintf(tmp, sizeof(tmp), "%s", cmd); + tmplen = cmdlen > sizeof(tmp) ? sizeof(tmp) : cmdlen; + memcpy(tmp, cmd, tmplen); - for (i = 0; i < strlen(tmp); i += (size_t)ret) { + for (i = 0; i < tmplen; i += (size_t)ret) { /* Write data in 8-byte chunks */ /* ret = usb->set_report(udev, 0, (unsigned char *)&tmp[i], 8); */ @@ -863,9 +867,10 @@ static int phoenix_command(const char *cmd, char *buf, size_t buflen) } /* Ippon communication subdriver */ -static int ippon_command(const char *cmd, char *buf, size_t buflen) +static int ippon_command(const char *cmd, size_t cmdlen, char *buf, size_t buflen) { char tmp[64]; + size_t tmplen; int ret; size_t i, len; @@ -877,9 +882,11 @@ static int ippon_command(const char *cmd, char *buf, size_t buflen) } /* Send command */ - snprintf(tmp, sizeof(tmp), "%s", cmd); + memset(tmp, 0, sizeof(tmp)); + tmplen = cmdlen > sizeof(tmp) ? sizeof(tmp) : cmdlen; + memcpy(tmp, cmd, tmplen); - for (i = 0; i < strlen(tmp); i += (size_t)ret) { + for (i = 0; i < tmplen; i += (size_t)ret) { /* Write data in 8-byte chunks */ ret = usb_control_msg(udev, @@ -1011,7 +1018,7 @@ static int hunnox_protocol(int asking_for) } /* Krauler communication subdriver */ -static int krauler_command(const char *cmd, char *buf, size_t buflen) +static int krauler_command(const char *cmd, size_t cmdlen, char *buf, size_t buflen) { /* Still not implemented: * 0x6 T (don't know how to pass the parameter) @@ -1047,7 +1054,7 @@ static int krauler_command(const char *cmd, char *buf, size_t buflen) int retry; - if (strcmp(cmd, command[i].str)) { + if (strncmp(cmd, command[i].str, cmdlen)) { continue; } @@ -1144,7 +1151,7 @@ static int krauler_command(const char *cmd, char *buf, size_t buflen) } /* Fabula communication subdriver */ -static int fabula_command(const char *cmd, char *buf, size_t buflen) +static int fabula_command(const char *cmd, size_t cmdlen, char *buf, size_t buflen) { const struct { const char *str; /* Megatec command */ @@ -1170,7 +1177,7 @@ static int fabula_command(const char *cmd, char *buf, size_t buflen) for (i = 0; commands[i].str; i++) { - if (strcmp(cmd, commands[i].str)) + if (strncmp(cmd, commands[i].str, cmdlen)) continue; index = commands[i].index; @@ -1263,7 +1270,7 @@ static int fabula_command(const char *cmd, char *buf, size_t buflen) /* Hunnox communication subdriver, based on Fabula code above so repeats * much of it currently. Possible future optimization is to refactor shared * code into new routines to be called from both (or more) methods.*/ -static int hunnox_command(const char *cmd, char *buf, size_t buflen) +static int hunnox_command(const char *cmd, size_t cmdlen, char *buf, size_t buflen) { /* The hunnox_patch was an argument in initial implementation of PR #638 * which added "hunnox" support; keeping it fixed here helps to visibly @@ -1295,7 +1302,7 @@ static int hunnox_command(const char *cmd, char *buf, size_t buflen) for (i = 0; commands[i].str; i++) { - if (strcmp(cmd, commands[i].str)) + if (strncmp(cmd, commands[i].str, cmdlen)) continue; index = commands[i].index; @@ -1431,7 +1438,7 @@ static int hunnox_command(const char *cmd, char *buf, size_t buflen) } /* Fuji communication subdriver */ -static int fuji_command(const char *cmd, char *buf, size_t buflen) +static int fuji_command(const char *cmd, size_t cmdlen, char *buf, size_t buflen) { unsigned char tmp[8]; char command[SMALLBUF] = "", @@ -1503,7 +1510,7 @@ static int fuji_command(const char *cmd, char *buf, size_t buflen) if (strlen(command) > 3) { /* Be 'megatec-y': echo the unsupported command back */ upsdebugx(3, "%s: unsupported command %s", __func__, command); - return snprintf(buf, buflen, "%s", cmd); + return snprintf(buf, buflen, "%.*s", (int)cmdlen, cmd); } /* Expected length of the answer to the ongoing query @@ -1582,13 +1589,12 @@ static int fuji_command(const char *cmd, char *buf, size_t buflen) } /* Phoenixtec (Masterguard) communication subdriver */ -static int phoenixtec_command(const char *cmd, char *buf, size_t buflen) +static int phoenixtec_command(const char *cmd, size_t cmdlen, char *buf, size_t buflen) { int ret; char *p, *e = NULL; char *l[] = { "T", "TL", "S", "C", "CT", "M", "N", "O", "SRC", "FCLR", "SS", "TUD", "SSN", NULL }; /* commands that don't return an answer */ char **lp; - size_t cmdlen = strlen(cmd); if (cmdlen > INT_MAX) { upsdebugx(3, "%s: requested command is too long (%" PRIuSIZE ")", @@ -1659,7 +1665,7 @@ static int phoenixtec_command(const char *cmd, char *buf, size_t buflen) } /* SNR communication subdriver */ -static int snr_command(const char *cmd, char *buf, size_t buflen) +static int snr_command(const char *cmd, size_t cmdlen, char *buf, size_t buflen) { /*ATTENTION: This subdriver uses short buffer with length 102 byte*/ const struct { @@ -1701,7 +1707,7 @@ static int snr_command(const char *cmd, char *buf, size_t buflen) int retry; - if (strcmp(cmd, command[i].str)) { + if (strncmp(cmd, command[i].str, cmdlen)) { continue; } @@ -1776,12 +1782,9 @@ static int snr_command(const char *cmd, char *buf, size_t buflen) return snprintf(buf, buflen, "%s", cmd); } -static int ablerex_command(const char *cmd, char *buf, size_t buflen) +static int ablerex_command(const char *cmd, size_t cmdlen, char *buf, size_t buflen) { - int iii; - int len; - int idx; - int retry; + size_t iii, len, idx, retry; char tmp[64]; char tmpryy[64]; @@ -1801,7 +1804,7 @@ static int ablerex_command(const char *cmd, char *buf, size_t buflen) tmp[1] = 0; tmp[2] = 1 + (char)strcspn(cmd, "\r"); - for (iii = 0 ; iii < tmp[2] ; iii++) + for (iii = 0 ; iii < (unsigned char)tmp[2] && iii < cmdlen && (iii + 3) < sizeof(tmp) ; iii++) { tmp[3+iii] = cmd[iii]; } @@ -1831,7 +1834,7 @@ static int ablerex_command(const char *cmd, char *buf, size_t buflen) break; } } - upsdebugx(3, "R3 read%d: %.*s", len, len, tmpryy); + upsdebugx(3, "R3 read%" PRIuSIZE ": %.*s", len, (int)len, tmpryy); if (len > 0) { len ++; @@ -1873,11 +1876,12 @@ static void *ablerex_subdriver_fun(USBDevice_t *device) } /* Gtec communication subdriver (based on Cypress) */ -static int gtec_command(const char *cmd, char *buf, size_t buflen) +static int gtec_command(const char *cmd, size_t cmdlen, char *buf, size_t buflen) { char tmp[SMALLBUF]; int ret = 0; - size_t i; + size_t i, tmpstrlen; + size_t tmplen = cmdlen > sizeof(tmp) ? sizeof(tmp) : cmdlen; if (buflen > INT_MAX) { upsdebugx(3, "%s: requested to read too much (%" PRIuSIZE "), " @@ -1888,9 +1892,11 @@ static int gtec_command(const char *cmd, char *buf, size_t buflen) /* Send command */ memset(tmp, 0, sizeof(tmp)); - snprintf(tmp, sizeof(tmp), "%s", cmd); + memcpy(tmp, cmd, tmplen); - for (i = 0; i < strlen(tmp); i += (size_t)ret) { + tmp[sizeof(tmp) - 1] = '\0'; + tmpstrlen = strlen(tmp); + for (i = 0; i < tmpstrlen; i += (size_t)ret) { /* Write data in 8-byte chunks */ /* ret = usb->set_report(udev, 0, (unsigned char *)&tmp[i], 8); */ @@ -2050,14 +2056,16 @@ static void load_armac_endpoint_cache(void) */ #define ARMAC_READ_SIZE_FOR_CONTROL 6 #define ARMAC_READ_SIZE_FOR_INTERRUPT 64 -static int armac_command(const char *cmd, char *buf, size_t buflen) +static int armac_command(const char *cmd, size_t cmdlen, char *buf, size_t buflen) { - char tmpbuf[ARMAC_READ_SIZE_FOR_INTERRUPT]; - int ret = 0; - size_t i, bufpos; - const size_t cmdlen = strlen(cmd); - bool_t use_interrupt = FALSE; - int read_size = ARMAC_READ_SIZE_FOR_CONTROL; + char tmpbuf[ARMAC_READ_SIZE_FOR_INTERRUPT]; + int ret = 0; + size_t i, bufpos; + const size_t cmdstrlen = strnlen(cmd, cmdlen); /* Length of cmd string (excluding terminating '\0'), or cmdlen if the string is too long */ + const size_t cmddatalen = cmdstrlen >= cmdlen ? cmdlen : cmdstrlen + 1; /* Amount of useful/valid data bytes in cmd string (max=cmdlen, or length of cmd+'\0' if the string is short enough) */ + const size_t tmplen = cmddatalen > sizeof(tmpbuf) ? sizeof(tmpbuf) : cmddatalen; /* How much of cmd[] we can copy into tmp[] so it fits (and remains useful), including the terminating '\0' */ + bool_t use_interrupt = FALSE; + int read_size = ARMAC_READ_SIZE_FOR_CONTROL; /* UPS ignores (doesn't echo back) unsupported commands which makes * the initialization long. List commands tested to be unsupported: @@ -2071,6 +2079,16 @@ static int armac_command(const char *cmd, char *buf, size_t buflen) NULL }; + if (cmdstrlen >= cmdlen || cmd[cmdstrlen - 1] != '\0') { + upsdebugx(2, "%s: strlen(cmd) > cmdlen (provided by caller), would effectively truncate!", __func__); + /* TOTHINK: // cmd[cmdlen] = '\0'; */ + } + + if (cmdstrlen + 1 > sizeof(tmpbuf)) { + upsdebugx(2, "%s: strlen(cmd) or cmdlen (provided by caller) are longer than tmp buffer, would effectively truncate!", __func__); + /* TOTHINK: // cmd[sizeof(tmpbuf)] = '\0'; */ + } + if (!armac_endpoint_cache.initialized) { load_armac_endpoint_cache(); } @@ -2095,14 +2113,22 @@ static int armac_command(const char *cmd, char *buf, size_t buflen) && armac_endpoint_cache.in_wMaxPacketSize == 64; #endif /* WITH_LIBUSB_1_0 */ - if (use_interrupt && cmdlen + 1 < armac_endpoint_cache.in_wMaxPacketSize) { + if (use_interrupt && cmddatalen < armac_endpoint_cache.in_wMaxPacketSize) { + /* We deal with strings here, always leave last byte as '\0' + * and the first byte tmpbuf[0] is used for the string length. + * So tmpdatalen is how much of cmd we can copy into tmp so it + * fits and makes sense (just the whole string if short enough). + */ + size_t tmpdatalen = tmplen < sizeof(tmpbuf) - 1 ? tmplen : sizeof(tmpbuf) - 2; + memset(tmpbuf, 0, sizeof(tmpbuf)); - tmpbuf[0] = 0xa0 + cmdlen; - memcpy(tmpbuf + 1, cmd, cmdlen); + tmpbuf[0] = 0xa0 + tmpdatalen; + memcpy(tmpbuf + 1, cmd, tmpdatalen); + /* Include terminating '\0' in the transfer */ ret = usb_interrupt_write(udev, armac_endpoint_cache.out_endpoint_address, - (usb_ctrl_charbuf)tmpbuf, cmdlen + 1, 5000); + (usb_ctrl_charbuf)tmpbuf, tmpdatalen + 1, 5000); read_size = ARMAC_READ_SIZE_FOR_INTERRUPT; } else { @@ -2119,8 +2145,9 @@ static int armac_command(const char *cmd, char *buf, size_t buflen) /* Send command to the UPS in 3-byte chunks. Most fit 1 chunk, except for eg. * parameterized tests. */ - for (i = 0; i < cmdlen;) { - const size_t bytes_to_send = (cmdlen <= (i + 3)) ? (cmdlen - i) : 3; + for (i = 0; i < cmddatalen;) { + const size_t bytes_to_send = (cmddatalen <= (i + 3)) ? (cmddatalen - i) : 3; + memset(tmpbuf, 0, sizeof(tmpbuf)); tmpbuf[0] = 0xa0 + bytes_to_send; memcpy(tmpbuf + 1, cmd + i, bytes_to_send); @@ -2990,7 +3017,7 @@ void upsdrv_shutdown(void) # ifndef TESTING static const struct { const char *name; - int (*command)(const char *cmd, char *buf, size_t buflen); + int (*command)(const char *cmd, size_t cmdlen, char *buf, size_t buflen); } usbsubdriver[] = { { "cypress", &cypress_command }, { "phoenixtec", &phoenixtec_command }, @@ -3598,7 +3625,7 @@ void upsdrv_cleanup(void) /* Generic command processing function: send a command and read a reply. * Returns < 0 on error, 0 on timeout and the number of bytes read on success. */ -static ssize_t qx_command(const char *cmd, char *buf, size_t buflen) +static ssize_t qx_command(const char *cmd, size_t cmdlen, char *buf, size_t buflen) { #ifndef TESTING ssize_t ret = -1; @@ -3632,7 +3659,7 @@ static ssize_t qx_command(const char *cmd, char *buf, size_t buflen) dstate_setinfo("driver.state", "reconnect.updateinfo"); } - ret = (*subdriver_command)(cmd, buf, buflen); + ret = (*subdriver_command)(cmd, cmdlen, buf, buflen); if (ret >= 0) { return ret; @@ -3716,7 +3743,7 @@ static ssize_t qx_command(const char *cmd, char *buf, size_t buflen) ser_flush_io(upsfd); - ret = ser_send(upsfd, "%s", cmd); + ret = ser_send_buf(upsfd, cmd, cmdlen); if (ret <= 0) { upsdebugx(3, "send: %s (%" PRIiSIZE ")", @@ -4383,8 +4410,8 @@ static int qx_process_answer(item_t *item, const size_t len) /* Short reply */ if (item->answer_len && len < item->answer_len) { - upsdebugx(2, "%s: short reply (%s)", - __func__, item->info_type); + upsdebugx(2, "%s: short reply (%s) %zu<%zu", + __func__, item->info_type, len, item->answer_len); return -1; } @@ -4426,6 +4453,7 @@ int qx_process(item_t *item, const char *command) (strlen(command) >= SMALLBUF ? strlen(command) + 1 : SMALLBUF) : (item->command && strlen(item->command) >= SMALLBUF ? strlen(item->command) + 1 : SMALLBUF); size_t cmdsz = (sizeof(char) * cmdlen); /* in bytes, to be pedantic */ + int cmd_len; if ( !(cmd = xmalloc(cmdsz)) ) { upslogx(LOG_ERR, "qx_process() failed to allocate buffer"); @@ -4434,12 +4462,12 @@ int qx_process(item_t *item, const char *command) /* Prepare the command to be used */ memset(cmd, 0, cmdsz); - snprintf(cmd, cmdsz, "%s", command ? command : item->command); + snprintf(cmd, cmdsz, "%s%n", command ? command : item->command, &cmd_len); /* Preprocess the command */ if ( item->preprocess_command != NULL && - item->preprocess_command(item, cmd, cmdsz) == -1 + (cmd_len = item->preprocess_command(item, cmd, cmdsz)) == -1 ) { upsdebugx(4, "%s: failed to preprocess command [%s]", __func__, item->info_type); @@ -4448,7 +4476,7 @@ int qx_process(item_t *item, const char *command) } /* Send the command */ - len = qx_command(cmd, buf, sizeof(buf)); + len = qx_command(cmd, cmd_len, buf, sizeof(buf)); memset(item->answer, 0, sizeof(item->answer)); diff --git a/drivers/nutdrv_qx.h b/drivers/nutdrv_qx.h index 65ebe178e9..ba15e61c70 100644 --- a/drivers/nutdrv_qx.h +++ b/drivers/nutdrv_qx.h @@ -98,7 +98,7 @@ typedef struct item_t { int (*preprocess_command)(struct item_t *item, char *command, const size_t commandlen); /* Last chance to preprocess the command to be sent to the UPS (e.g. to add CRC, ...). * This function is given the currently processed item (item), the command to be sent to the UPS (command) and its size_t (commandlen). - * Return -1 in case of errors, else 0. + * Return -1 in case of errors, else 0 if a NUL terminated string, else the length of the command in bytes. * command must be filled with the actual command to be sent to the UPS. */ int (*preprocess_answer)(struct item_t *item, const int len); diff --git a/drivers/nutdrv_qx_voltronic-axpert.c b/drivers/nutdrv_qx_voltronic-axpert.c new file mode 100644 index 0000000000..02a33f592f --- /dev/null +++ b/drivers/nutdrv_qx_voltronic-axpert.c @@ -0,0 +1,4379 @@ +/* nutdrv_qx_voltronic-axpert.c - Subdriver for Voltronic Power Axpert + * + * Copyright (C) + * 2014 Daniele Pezzini + * 2022 Graham Leggett + * 2025 Jim Klimov + * + * 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_voltronic-axpert.h" +#include "common_voltronic-crc.h" + +#include "nut_float.h" + +#define VOLTRONIC_AXPERT_VERSION "Voltronic-Axpert 0.01" + +/* For dev-testing: known ways to get data points, need mapping into NUT variables */ +#if WITH_UNMAPPED_DATA_POINTS +# define TESTING_AXPERT_UNMAPPED_MAPPINGS_DEFAULT 1 +#else +# define TESTING_AXPERT_UNMAPPED_MAPPINGS_DEFAULT 0 +#endif + +/* NOTE for dev-testing: you can enable single or combo code paths + * with macros below, to enable visibility of certain data point + * or command/setting groups. It suffices to e.g. add `+ 1` to the + * line you want to activate in the custom build of NUT for testing. + */ +#define TESTING_AXPERT_CAPS_QUERY TESTING_AXPERT_UNMAPPED_MAPPINGS_DEFAULT +#define TESTING_AXPERT_CAPS_CHANGE TESTING_AXPERT_UNMAPPED_MAPPINGS_DEFAULT +#define TESTING_AXPERT_OPERATIONAL_OPTIONS_QUERY_CHANGE TESTING_AXPERT_UNMAPPED_MAPPINGS_DEFAULT +#define TESTING_AXPERT_PFC_CURVE_QUERY_CHANGE TESTING_AXPERT_UNMAPPED_MAPPINGS_DEFAULT +#define TESTING_AXPERT_GENERATOR_AS_AC_SOURCE_QUERY_CHANGE TESTING_AXPERT_UNMAPPED_MAPPINGS_DEFAULT +#define TESTING_AXPERT_PV_SUPPLY_PRIORITY_QUERY_CHANGE TESTING_AXPERT_UNMAPPED_MAPPINGS_DEFAULT +#define TESTING_AXPERT_NOMINAL_VOLTAGE_CHANGE TESTING_AXPERT_UNMAPPED_MAPPINGS_DEFAULT +#define TESTING_AXPERT_OUTPUT_REALPOWER_NOMINAL_QUERY TESTING_AXPERT_UNMAPPED_MAPPINGS_DEFAULT +#define TESTING_AXPERT_RATINGS_QUERY_3 TESTING_AXPERT_UNMAPPED_MAPPINGS_DEFAULT +#define TESTING_AXPERT_ACCEPTABLE_LIMITS_QUERY TESTING_AXPERT_UNMAPPED_MAPPINGS_DEFAULT +#define TESTING_AXPERT_GRID_OUTPUT_VOLTAGE_LIMITS_QUERY TESTING_AXPERT_UNMAPPED_MAPPINGS_DEFAULT +#define TESTING_AXPERT_WAITING_TIME_BEFORE_GRID_CONNECTION_QUERY_CHANGE TESTING_AXPERT_UNMAPPED_MAPPINGS_DEFAULT +#define TESTING_AXPERT_BATTERY_CHARGING_DATA_QUERY TESTING_AXPERT_UNMAPPED_MAPPINGS_DEFAULT +#define TESTING_AXPERT_BATTERY_CHARGING_LIMITS_CHANGE TESTING_AXPERT_UNMAPPED_MAPPINGS_DEFAULT +#define TESTING_AXPERT_BATTERY_CHARGING_LIMITS_QUERY_CHANGE TESTING_AXPERT_UNMAPPED_MAPPINGS_DEFAULT +#define TESTING_AXPERT_MAX_OUTPUT_POWER_QUERY_CHANGE TESTING_AXPERT_UNMAPPED_MAPPINGS_DEFAULT +#define TESTING_AXPERT_PF_QUERY_CHANGE TESTING_AXPERT_UNMAPPED_MAPPINGS_DEFAULT +#define TESTING_AXPERT_DATETIME_QUERY_CHANGE TESTING_AXPERT_UNMAPPED_MAPPINGS_DEFAULT +#define TESTING_AXPERT_BATTERY_INFO_QUERY TESTING_AXPERT_UNMAPPED_MAPPINGS_DEFAULT +#define TESTING_AXPERT_FAULT_TYPE_QUERY TESTING_AXPERT_UNMAPPED_MAPPINGS_DEFAULT +#define TESTING_AXPERT_SELFTEST_RESULT_QUERY TESTING_AXPERT_UNMAPPED_MAPPINGS_DEFAULT +#define TESTING_AXPERT_ENERGY_STATS_QUERY TESTING_AXPERT_UNMAPPED_MAPPINGS_DEFAULT +#define TESTING_AXPERT_INSTCMD TESTING_AXPERT_UNMAPPED_MAPPINGS_DEFAULT + +/* Warning: unlocks a method not used in any code path: */ +#define TESTING_AXPERT_FUNC_PROCESS_SIGN 0 // TESTING_AXPERT_UNMAPPED_MAPPINGS_DEFAULT + +/* Support functions */ +static int voltronic_sunny_claim(void); +static void voltronic_axpert_initinfo(void); +static void voltronic_sunny_makevartable(void); + +static int voltronic_axpert_clear_flags(const char *varname, const unsigned long flag, const unsigned long noflag, const unsigned long clearflag); +#if TESTING_AXPERT_NOMINAL_VOLTAGE_CHANGE || TESTING_AXPERT_GRID_OUTPUT_VOLTAGE_LIMITS_QUERY || TESTING_AXPERT_WAITING_TIME_BEFORE_GRID_CONNECTION_QUERY_CHANGE || TESTING_AXPERT_BATTERY_CHARGING_LIMITS_CHANGE || TESTING_AXPERT_BATTERY_CHARGING_LIMITS_QUERY_CHANGE || TESTING_AXPERT_MAX_OUTPUT_POWER_QUERY_CHANGE || TESTING_AXPERT_PF_QUERY_CHANGE +static int voltronic_axpert_add_flags(const char *varname, const unsigned long flag, const unsigned long noflag, const unsigned long addflag); +#endif /* TESTING_AXPERT_NOMINAL_VOLTAGE_CHANGE || TESTING_AXPERT_GRID_OUTPUT_VOLTAGE_LIMITS_QUERY || TESTING_AXPERT_WAITING_TIME_BEFORE_GRID_CONNECTION_QUERY_CHANGE || TESTING_AXPERT_BATTERY_CHARGING_LIMITS_CHANGE || TESTING_AXPERT_BATTERY_CHARGING_LIMITS_QUERY_CHANGE || TESTING_AXPERT_MAX_OUTPUT_POWER_QUERY_CHANGE || TESTING_AXPERT_PF_QUERY_CHANGE */ + +#if TESTING_AXPERT_ENERGY_STATS_QUERY +static int voltronic_sunny_checksum(const char *string); +#endif /* TESTING_AXPERT_ENERGY_STATS_QUERY */ + +#if TESTING_AXPERT_OUTPUT_REALPOWER_NOMINAL_QUERY || TESTING_AXPERT_BATTERY_CHARGING_DATA_QUERY || TESTING_AXPERT_BATTERY_CHARGING_LIMITS_QUERY_CHANGE || TESTING_AXPERT_MAX_OUTPUT_POWER_QUERY_CHANGE || TESTING_AXPERT_BATTERY_CHARGING_DATA_QUERY || TESTING_AXPERT_BATTERY_CHARGING_LIMITS_QUERY_CHANGE || TESTING_AXPERT_MAX_OUTPUT_POWER_QUERY_CHANGE +static void voltronic_sunny_update_related_vars_limits(item_t *item, const char *value); +#endif /* TESTING_AXPERT_OUTPUT_REALPOWER_NOMINAL_QUERY || TESTING_AXPERT_FUNC_PROCESS_SIGN || TESTING_AXPERT_GRID_OUTPUT_VOLTAGE_LIMITS_QUERY || TESTING_AXPERT_WAITING_TIME_BEFORE_GRID_CONNECTION_QUERY_CHANGE || TESTING_AXPERT_BATTERY_CHARGING_DATA_QUERY || TESTING_AXPERT_BATTERY_CHARGING_LIMITS_QUERY_CHANGE || TESTING_AXPERT_MAX_OUTPUT_POWER_QUERY_CHANGE || TESTING_AXPERT_PF_QUERY_CHANGE || TESTING_AXPERT_FAULT_TYPE_QUERY || TESTING_AXPERT_BATTERY_CHARGING_DATA_QUERY || TESTING_AXPERT_BATTERY_CHARGING_LIMITS_QUERY_CHANGE || TESTING_AXPERT_MAX_OUTPUT_POWER_QUERY_CHANGE */ + +#if TESTING_AXPERT_NOMINAL_VOLTAGE_CHANGE || TESTING_AXPERT_GRID_OUTPUT_VOLTAGE_LIMITS_QUERY || TESTING_AXPERT_WAITING_TIME_BEFORE_GRID_CONNECTION_QUERY_CHANGE || TESTING_AXPERT_BATTERY_CHARGING_LIMITS_CHANGE || TESTING_AXPERT_BATTERY_CHARGING_LIMITS_QUERY_CHANGE || TESTING_AXPERT_MAX_OUTPUT_POWER_QUERY_CHANGE || TESTING_AXPERT_PF_QUERY_CHANGE +static int voltronic_sunny_OEEPB(void); +#endif /* TESTING_AXPERT_NOMINAL_VOLTAGE_CHANGE || TESTING_AXPERT_GRID_OUTPUT_VOLTAGE_LIMITS_QUERY || TESTING_AXPERT_WAITING_TIME_BEFORE_GRID_CONNECTION_QUERY_CHANGE || TESTING_AXPERT_BATTERY_CHARGING_LIMITS_CHANGE || TESTING_AXPERT_BATTERY_CHARGING_LIMITS_QUERY_CHANGE || TESTING_AXPERT_MAX_OUTPUT_POWER_QUERY_CHANGE || TESTING_AXPERT_PF_QUERY_CHANGE */ + +/* Range/enum functions */ +#if TESTING_AXPERT_PV_SUPPLY_PRIORITY_QUERY_CHANGE +static int voltronic_sunny_pv_priority_enum(char *value, const size_t len); +#endif /* TESTING_AXPERT_PV_SUPPLY_PRIORITY_QUERY_CHANGE */ + +#if TESTING_AXPERT_GRID_OUTPUT_VOLTAGE_LIMITS_QUERY +static int voltronic_sunny_grid_inout_freq_max(char *value, const size_t len); +static int voltronic_sunny_grid_inout_freq_min(char *value, const size_t len); +#endif /* TESTING_AXPERT_GRID_OUTPUT_VOLTAGE_LIMITS_QUERY */ + +#if TESTING_AXPERT_BATTERY_CHARGING_DATA_QUERY || TESTING_AXPERT_BATTERY_CHARGING_LIMITS_CHANGE +static int voltronic_sunny_bc_v_bulk(char *value, const size_t len); +#endif /* TESTING_AXPERT_BATTERY_CHARGING_DATA_QUERY || TESTING_AXPERT_BATTERY_CHARGING_LIMITS_CHANGE */ + +#if TESTING_AXPERT_BATTERY_CHARGING_LIMITS_QUERY_CHANGE +static int voltronic_sunny_pv_input_volt_max(char *value, const size_t len); +#endif /* TESTING_AXPERT_BATTERY_CHARGING_LIMITS_QUERY_CHANGE */ + +/* Answer preprocess functions */ +static int voltronic_axpert_checkcrc(item_t *item, const int len); + +/* Command preprocess functions */ +static int voltronic_axpert_crc(item_t *item, char *command, const size_t commandlen); +#if TESTING_AXPERT_FAULT_TYPE_QUERY +static int voltronic_sunny_fault_query(item_t *item, char *command, const size_t commandlen); +#endif /* TESTING_AXPERT_FAULT_TYPE_QUERY */ + +#if TESTING_AXPERT_ENERGY_STATS_QUERY +static int voltronic_sunny_energy_hour(item_t *item, char *command, const size_t commandlen); +static int voltronic_sunny_energy_day(item_t *item, char *command, const size_t commandlen); +static int voltronic_sunny_energy_month(item_t *item, char *command, const size_t commandlen); +static int voltronic_sunny_energy_year(item_t *item, char *command, const size_t commandlen); +#endif /* TESTING_AXPERT_ENERGY_STATS_QUERY */ + +/* Preprocess functions */ +static int voltronic_axpert_hex_preprocess(item_t *item, char *value, const size_t valuelen); +#if TESTING_AXPERT_OUTPUT_REALPOWER_NOMINAL_QUERY || TESTING_AXPERT_FUNC_PROCESS_SIGN || TESTING_AXPERT_GRID_OUTPUT_VOLTAGE_LIMITS_QUERY || TESTING_AXPERT_WAITING_TIME_BEFORE_GRID_CONNECTION_QUERY_CHANGE || TESTING_AXPERT_BATTERY_CHARGING_DATA_QUERY || TESTING_AXPERT_BATTERY_CHARGING_LIMITS_QUERY_CHANGE || TESTING_AXPERT_MAX_OUTPUT_POWER_QUERY_CHANGE || TESTING_AXPERT_PF_QUERY_CHANGE || TESTING_AXPERT_FAULT_TYPE_QUERY +static int voltronic_sunny_basic_preprocess(item_t *item, char *value, const size_t valuelen); +#endif /* TESTING_AXPERT_OUTPUT_REALPOWER_NOMINAL_QUERY || TESTING_AXPERT_FUNC_PROCESS_SIGN || TESTING_AXPERT_GRID_OUTPUT_VOLTAGE_LIMITS_QUERY || TESTING_AXPERT_WAITING_TIME_BEFORE_GRID_CONNECTION_QUERY_CHANGE || TESTING_AXPERT_BATTERY_CHARGING_DATA_QUERY || TESTING_AXPERT_BATTERY_CHARGING_LIMITS_QUERY_CHANGE || TESTING_AXPERT_MAX_OUTPUT_POWER_QUERY_CHANGE || TESTING_AXPERT_PF_QUERY_CHANGE || TESTING_AXPERT_FAULT_TYPE_QUERY */ +static int voltronic_axpert_protocol(item_t *item, char *value, const size_t valuelen); +static int voltronic_axpert_fw(item_t *item, char *value, const size_t valuelen); +static int voltronic_axpert_serial_numb(item_t *item, char *value, const size_t valuelen); +static int voltronic_axpert_capability(item_t *item, char *value, const size_t valuelen); + +#if TESTING_AXPERT_CAPS_CHANGE +static int voltronic_axpert_capability_set(item_t *item, char *value, const size_t valuelen); +static int voltronic_axpert_capability_reset(item_t *item, char *value, const size_t valuelen); +static int voltronic_axpert_capability_set_nonut(item_t *item, char *value, const size_t valuelen); +#endif /* TESTING_AXPERT_CAPS_CHANGE */ + +#if TESTING_AXPERT_OPERATIONAL_OPTIONS_QUERY_CHANGE || TESTING_AXPERT_PFC_CURVE_QUERY_CHANGE || TESTING_AXPERT_GENERATOR_AS_AC_SOURCE_QUERY_CHANGE || TESTING_AXPERT_MAX_OUTPUT_POWER_QUERY_CHANGE +static int voltronic_sunny_01(item_t *item, char *value, const size_t valuelen); +static int voltronic_sunny_01_set(item_t *item, char *value, const size_t valuelen); +#endif /* TESTING_AXPERT_OPERATIONAL_OPTIONS_QUERY_CHANGE || TESTING_AXPERT_PFC_CURVE_QUERY_CHANGE || TESTING_AXPERT_GENERATOR_AS_AC_SOURCE_QUERY_CHANGE || TESTING_AXPERT_MAX_OUTPUT_POWER_QUERY_CHANGE */ + +#if TESTING_AXPERT_PV_SUPPLY_PRIORITY_QUERY_CHANGE +static int voltronic_sunny_pv_priority(item_t *item, char *value, const size_t valuelen); +static int voltronic_sunny_pv_priority_set(item_t *item, char *value, const size_t valuelen); +#endif /* TESTING_AXPERT_PV_SUPPLY_PRIORITY_QUERY_CHANGE */ + +#if TESTING_AXPERT_GRID_OUTPUT_VOLTAGE_LIMITS_QUERY || TESTING_AXPERT_WAITING_TIME_BEFORE_GRID_CONNECTION_QUERY_CHANGE || TESTING_AXPERT_BATTERY_CHARGING_DATA_QUERY || TESTING_AXPERT_BATTERY_CHARGING_LIMITS_QUERY_CHANGE || TESTING_AXPERT_MAX_OUTPUT_POWER_QUERY_CHANGE || TESTING_AXPERT_PF_QUERY_CHANGE +static int voltronic_sunny_unskip_setvar(item_t *item, char *value, const size_t valuelen); +#endif /* TESTING_AXPERT_GRID_OUTPUT_VOLTAGE_LIMITS_QUERY || TESTING_AXPERT_WAITING_TIME_BEFORE_GRID_CONNECTION_QUERY_CHANGE || TESTING_AXPERT_BATTERY_CHARGING_DATA_QUERY || TESTING_AXPERT_BATTERY_CHARGING_LIMITS_QUERY_CHANGE || TESTING_AXPERT_MAX_OUTPUT_POWER_QUERY_CHANGE || TESTING_AXPERT_PF_QUERY_CHANGE */ + +static int voltronic_axpert_qpiri_battery_type(item_t *item, char *value, const size_t valuelen); +static int voltronic_axpert_qpiri_model_type(item_t *item, char *value, const size_t valuelen); +static int voltronic_axpert_transformer(item_t *item, char *value, const size_t valuelen); + +#if TESTING_AXPERT_NOMINAL_VOLTAGE_CHANGE +static int voltronic_sunny_volt_nom_set(item_t *item, char *value, const size_t valuelen); +#endif /* TESTING_AXPERT_NOMINAL_VOLTAGE_CHANGE */ + +#if TESTING_AXPERT_NOMINAL_VOLTAGE_CHANGE || TESTING_AXPERT_GRID_OUTPUT_VOLTAGE_LIMITS_QUERY || TESTING_AXPERT_WAITING_TIME_BEFORE_GRID_CONNECTION_QUERY_CHANGE || TESTING_AXPERT_BATTERY_CHARGING_LIMITS_CHANGE || TESTING_AXPERT_BATTERY_CHARGING_LIMITS_QUERY_CHANGE || TESTING_AXPERT_MAX_OUTPUT_POWER_QUERY_CHANGE || TESTING_AXPERT_PF_QUERY_CHANGE +static int voltronic_sunny_process_setvar(item_t *item, char *value, const size_t valuelen); +#endif /* TESTING_AXPERT_NOMINAL_VOLTAGE_CHANGE || TESTING_AXPERT_GRID_OUTPUT_VOLTAGE_LIMITS_QUERY || TESTING_AXPERT_WAITING_TIME_BEFORE_GRID_CONNECTION_QUERY_CHANGE || TESTING_AXPERT_BATTERY_CHARGING_LIMITS_CHANGE || TESTING_AXPERT_BATTERY_CHARGING_LIMITS_QUERY_CHANGE || TESTING_AXPERT_MAX_OUTPUT_POWER_QUERY_CHANGE || TESTING_AXPERT_PF_QUERY_CHANGE */ + +#if TESTING_AXPERT_OUTPUT_REALPOWER_NOMINAL_QUERY +static int voltronic_sunny_basic_preprocess_and_update_related_vars_limits(item_t *item, char *value, const size_t valuelen); +#endif /* TESTING_AXPERT_OUTPUT_REALPOWER_NOMINAL_QUERY */ + +#if TESTING_AXPERT_RATINGS_QUERY_3 +static int voltronic_sunny_yymmdd(item_t *item, char *value, const size_t valuelen); +static int voltronic_sunny_hh_mm(item_t *item, char *value, const size_t valuelen); +#endif /* TESTING_AXPERT_RATINGS_QUERY_3 */ + +#if TESTING_AXPERT_MAX_OUTPUT_POWER_QUERY_CHANGE +static int voltronic_sunny_lst(item_t *item, char *value, const size_t valuelen); +#endif /* TESTING_AXPERT_MAX_OUTPUT_POWER_QUERY_CHANGE */ + +#if TESTING_AXPERT_ACCEPTABLE_LIMITS_QUERY +static int voltronic_sunny_set_limits(item_t *item, char *value, const size_t valuelen); +#endif /* TESTING_AXPERT_ACCEPTABLE_LIMITS_QUERY */ + +#if TESTING_AXPERT_BATTERY_CHARGING_DATA_QUERY || TESTING_AXPERT_BATTERY_CHARGING_LIMITS_QUERY_CHANGE || TESTING_AXPERT_MAX_OUTPUT_POWER_QUERY_CHANGE +static int voltronic_sunny_unskip_setvar_and_update_related_vars_limits(item_t *item, char *value, const size_t valuelen); +#endif /* TESTING_AXPERT_BATTERY_CHARGING_DATA_QUERY || TESTING_AXPERT_BATTERY_CHARGING_LIMITS_QUERY_CHANGE || TESTING_AXPERT_MAX_OUTPUT_POWER_QUERY_CHANGE */ + +#if TESTING_AXPERT_BATTERY_CHARGING_LIMITS_QUERY_CHANGE +static int voltronic_sunny_charger_limits_set(item_t *item, char *value, const size_t valuelen); +static int voltronic_sunny_discharging_limits_set(item_t *item, char *value, const size_t valuelen); +#endif /* TESTING_AXPERT_BATTERY_CHARGING_LIMITS_QUERY_CHANGE */ + +#if TESTING_AXPERT_MAX_OUTPUT_POWER_QUERY_CHANGE +static int voltronic_sunny_hhmm(item_t *item, char *value, const size_t valuelen); +static int voltronic_sunny_hhmm_set(item_t *item, char *value, const size_t valuelen); +static int voltronic_sunny_hhmm_x2_set(item_t *item, char *value, const size_t valuelen); +#endif /* TESTING_AXPERT_MAX_OUTPUT_POWER_QUERY_CHANGE */ + +#if TESTING_AXPERT_PF_QUERY_CHANGE +static int voltronic_sunny_pf(item_t *item, char *value, const size_t valuelen); +static int voltronic_sunny_pfc_set(item_t *item, char *value, const size_t valuelen); +#endif /* TESTING_AXPERT_PF_QUERY_CHANGE */ + +#if TESTING_AXPERT_DATETIME_QUERY_CHANGE || TESTING_AXPERT_ENERGY_STATS_QUERY || TESTING_AXPERT_RATINGS_QUERY_3 || TESTING_AXPERT_FAULT_TYPE_QUERY +static int voltronic_sunny_date(item_t *item, char *value, const size_t valuelen); +#endif /* TESTING_AXPERT_DATETIME_QUERY_CHANGE || TESTING_AXPERT_ENERGY_STATS_QUERY || TESTING_AXPERT_RATINGS_QUERY_3 || TESTING_AXPERT_FAULT_TYPE_QUERY */ + +#if TESTING_AXPERT_DATETIME_QUERY_CHANGE || TESTING_AXPERT_FAULT_TYPE_QUERY +static int voltronic_sunny_time(item_t *item, char *value, const size_t valuelen); +#endif /* TESTING_AXPERT_DATETIME_QUERY_CHANGE || TESTING_AXPERT_FAULT_TYPE_QUERY */ + +#if TESTING_AXPERT_DATETIME_QUERY_CHANGE +static int voltronic_sunny_date_set(item_t *item, char *value, const size_t valuelen); +static int voltronic_sunny_time_set(item_t *item, char *value, const size_t valuelen); +#endif /* TESTING_AXPERT_DATETIME_QUERY_CHANGE */ + +#if TESTING_AXPERT_FUNC_PROCESS_SIGN +/* WARNING: Currently unused in any code path! */ +static int voltronic_sunny_process_sign(item_t *item, char *value, const size_t valuelen); +#endif /* TESTING_AXPERT_FUNC_PROCESS_SIGN */ + +static int voltronic_axpert_status(item_t *item, char *value, const size_t valuelen); +static int voltronic_sunny_warning(item_t *item, char *value, const size_t valuelen); +static int voltronic_axpert_mode(item_t *item, char *value, const size_t valuelen); + +#if TESTING_AXPERT_BATTERY_INFO_QUERY +static int voltronic_sunny_batt_runtime(item_t *item, char *value, const size_t valuelen); +#endif /* TESTING_AXPERT_BATTERY_INFO_QUERY */ + +#if TESTING_AXPERT_FAULT_TYPE_QUERY +static int voltronic_sunny_fault(item_t *item, char *value, const size_t valuelen); +static int voltronic_sunny_date_skip_me(item_t *item, char *value, const size_t valuelen); +static int voltronic_sunny_time_skip_me(item_t *item, char *value, const size_t valuelen); +static int voltronic_sunny_skip_me(item_t *item, char *value, const size_t valuelen); +static int voltronic_sunny_fault_status(item_t *item, char *value, const size_t valuelen); +static int voltronic_sunny_fault_id(item_t *item, char *value, const size_t valuelen); +#endif /* TESTING_AXPERT_FAULT_TYPE_QUERY */ + +#if TESTING_AXPERT_SELFTEST_RESULT_QUERY +static int voltronic_sunny_self_test_result(item_t *item, char *value, const size_t valuelen); +#endif /* TESTING_AXPERT_SELFTEST_RESULT_QUERY */ + + +/* == Global vars == */ + +#if TESTING_AXPERT_CAPS_CHANGE +/* Capability vars ("enabled"/"disabled") */ +static char *bypass_alarm, + *battery_alarm; +#endif /* TESTING_AXPERT_CAPS_CHANGE */ + +/* Global flags */ +static int crc = 1, /* Whether device puts CRC in its replies or not */ + line_loss = 0, /* Whether device has lost connection to the grid or not */ + pv_loss = 0; /* Whether devoce has lost connection to PV or not */ + +static double fw = 0; /* Firmware version */ +static int protocol = 0, /* Protocol used by device */ +#if TESTING_AXPERT_FAULT_TYPE_QUERY + fault_id, /* Fault ID */ +#endif /* TESTING_AXPERT_FAULT_TYPE_QUERY */ +#if TESTING_AXPERT_PV_SUPPLY_PRIORITY_QUERY_CHANGE + model_id = 0, /* Model ID */ +#endif /* TESTING_AXPERT_PV_SUPPLY_PRIORITY_QUERY_CHANGE */ + model_type = 0; /* Model type */ + + +/* == Ranges/enums/lengths == */ + +/* Enumlist for battery type */ +static info_rw_t voltronic_axpert_e_battery_type[] = { + { "AGM", 0 }, /* Type: 0 */ + { "Flooded", 0 }, /* Type: 1 */ + { "User", 0 }, /* Type: 2 */ + { "", 0 } + +}; +/* Enumlist for device model type */ +static info_rw_t voltronic_axpert_e_model_type[] = { + { "Grid-tie", 0 }, /* Type: 0 */ + { "Off-grid", 0 }, /* Type: 1 */ + { "Hybrid", 0 }, /* Type: 10 */ + { "Off Grid with 2 Trackers", 0 }, /* Type: 11 */ + { "Off Grid with 3 Trackers", 0 }, /* Type: 20 */ + { "", 0 } +}; + +/* Enumlist for device capabilities that have a NUT var */ +static info_rw_t voltronic_axpert_e_cap[] = { + { "no", 0 }, + { "yes", 0 }, + { "", 0 } +}; + +#if TESTING_AXPERT_CAPS_CHANGE || TESTING_AXPERT_OPERATIONAL_OPTIONS_QUERY_CHANGE || TESTING_AXPERT_PFC_CURVE_QUERY_CHANGE || TESTING_AXPERT_GENERATOR_AS_AC_SOURCE_QUERY_CHANGE +/* Enumlist for NONUT capabilities */ +static info_rw_t voltronic_axpert_e_cap_nonut[] = { + { "disabled", 0 }, + { "enabled", 0 }, + { "", 0 } +}; +#endif /* TESTING_AXPERT_CAPS_CHANGE */ + +#if TESTING_AXPERT_PV_SUPPLY_PRIORITY_QUERY_CHANGE +/* Enumlist for PV energy supply priority */ +static info_rw_t voltronic_sunny_e_pv_priority[] = { + { "Battery-Load", voltronic_sunny_pv_priority_enum }, /* Priority: 01; Type: Off-grid (01) */ + { "Load-Battery", voltronic_sunny_pv_priority_enum }, /* Priority: 02; Type: Off-grid (01); Model: 150 */ + { "Load-Battery (grid relay disconnected)", voltronic_sunny_pv_priority_enum }, /* Priority: 02; Type: Off-grid (01); Model: 151 */ + { "Battery-Load-Grid", voltronic_sunny_pv_priority_enum }, /* Priority: 01; Type: Grid-tie with backup (10) */ + { "Load-Battery-Grid", voltronic_sunny_pv_priority_enum }, /* Priority: 02; Type: Grid-tie with backup (10) */ + { "Load-Grid-Battery", voltronic_sunny_pv_priority_enum }, /* Priority: 03; Type: Grid-tie with backup (10) */ + { "", 0 } +}; + +/* Preprocess enum value for PV energy supply priority */ +static int voltronic_sunny_pv_priority_enum(char *value, const size_t len) +{ + NUT_UNUSED_VARIABLE(len); /* FIXME? strncasecmp(value, expected, len) but make sure we check the whole fixed argument or it is not equal */ + + switch (model_type) + { + case 1: /* Off-grid */ + if ( + !strcasecmp(value, "Battery-Load") || /* Priority: 01 */ + !strcasecmp(value, "Load-Battery") || /* Priority: 02; Model: 150 */ + !strcasecmp(value, "Load-Battery (grid relay disconnected)") /* Priority: 02; Model: 151 */ + ) + return 0; + break; + case 10: /* Grid-tie with backup */ + if ( + !strcasecmp(value, "Battery-Load-Grid") || /* Priority: 01 */ + !strcasecmp(value, "Load-Battery-Grid") || /* Priority: 02 */ + !strcasecmp(value, "Load-Grid-Battery") /* Priority: 03 */ + ) + return 0; + break; + case 0: /* Grid-tie */ + case 11: /* Self-use */ + default: + break; + } + + return -1; +} +#endif /* TESTING_AXPERT_PV_SUPPLY_PRIORITY_QUERY_CHANGE */ + +/* Enumlist for nominal voltage */ +static info_rw_t voltronic_axpert_e_volt_nom[] = { + { "101", 0 }, /* Low voltage models */ + { "110", 0 }, /* Low voltage models */ + { "120", 0 }, /* Low voltage models */ + { "127", 0 }, /* Low voltage models */ + { "202", 0 }, /* High voltage models */ + { "208", 0 }, /* High voltage models */ + { "220", 0 }, /* High voltage models */ + { "230", 0 }, /* High voltage models */ + { "240", 0 }, /* High voltage models */ + { "", 0 } +}; + +#if TESTING_AXPERT_NOMINAL_VOLTAGE_CHANGE +/* Enumlist for nominal frequency */ +static info_rw_t voltronic_sunny_e_freq_nom[] = { + { "50", 0 }, + { "60", 0 }, + { "", 0 } +}; + +/* Range for number of MPP trackers in use */ +static info_rw_t voltronic_sunny_r_mpp_number[] = { + { "01", 0 }, + { "99", 0 }, + { "", 0 } +}; +#endif /* TESTING_AXPERT_NOMINAL_VOLTAGE_CHANGE */ + +#if TESTING_AXPERT_GRID_OUTPUT_VOLTAGE_LIMITS_QUERY +/* Range for maximum grid output voltage: overwritten (if appropriate) at runtime by voltronic_sunny_set_limits() (QVFTR #1, #2) */ +static info_rw_t voltronic_sunny_r_grid_output_volt_max[] = { + { "240", 0 }, + { "276", 0 }, + { "", 0 } +}; + +/* Range for maximum grid input voltage: overwritten (if appropriate) at runtime by voltronic_sunny_set_limits() (QVFTR #1, #2) */ +static info_rw_t voltronic_sunny_r_grid_input_volt_max[] = { + { "240", 0 }, + { "280", 0 }, + { "", 0 } +}; + +/* Range for minimum grid output voltage: overwritten (if appropriate) at runtime by voltronic_sunny_set_limits() (QVFTR #3, #4) */ +static info_rw_t voltronic_sunny_r_grid_output_volt_min[] = { + { "176", 0 }, + { "220", 0 }, + { "", 0 } +}; + +/* Range for minimum grid input voltage: overwritten (if appropriate) at runtime by voltronic_sunny_set_limits() (QVFTR #3, #4) */ +static info_rw_t voltronic_sunny_r_grid_input_volt_min[] = { + { "175", 0 }, + { "220", 0 }, + { "", 0 } +}; + +/* Range for maximum grid input/output frequency: overwritten (if appropriate) at runtime by voltronic_sunny_set_limits() (QVFTR #5, #6) */ +static info_rw_t voltronic_sunny_r_grid_inout_freq_max[] = { /* FIXME: ranges only support ints */ + { "50.2", voltronic_sunny_grid_inout_freq_max }, /* Nominal frequency (QPIRI #2) == 50.0 */ + { "54.8", voltronic_sunny_grid_inout_freq_max }, /* Nominal frequency (QPIRI #2) == 50.0 */ + { "60.2", voltronic_sunny_grid_inout_freq_max }, /* Nominal frequency (QPIRI #2) == 60.0 */ + { "64.8", voltronic_sunny_grid_inout_freq_max }, /* Nominal frequency (QPIRI #2) == 60.0 */ + { "", 0 } +}; + +/* Preprocess range value for (not overwritten) maximum grid input/output frequency */ +static int voltronic_sunny_grid_inout_freq_max(char *value, const size_t len) +{ + char *ptr; + const int val = strtol(value, &ptr, 10) * 10 + (*ptr == '.' ? strtol(++ptr, NULL, 10) : 0); + double gfn; + const char *gridfreqnom = dstate_getinfo("grid.frequency.nominal"); + + NUT_UNUSED_VARIABLE(len); /* FIXME? */ + + if (!gridfreqnom) { + upsdebugx(2, "%s: unable to get grid.frequency.nominal", __func__); + return -1; + } + + gfn = strtod(gridfreqnom, NULL); + + switch (val) + { + case 502: + case 548: + /* Nominal frequency (QPIRI #2) == 50.0 */ + if (gfn == 50.0) + return 0; + break; + case 602: + case 648: + /* Nominal frequency (QPIRI #2) == 60.0 */ + if (gfn == 60.0) + return 0; + break; + default: + upsdebugx(2, "%s: unknown value (%s)", __func__, value); + break; + } + + return -1; +} + +/* Range for minimum grid input/output frequency: overwritten (if appropriate) at runtime by voltronic_sunny_set_limits() (QVFTR #7, #8) */ +static info_rw_t voltronic_sunny_r_grid_inout_freq_min[] = { /* FIXME: ranges only support ints */ + { "45.0", voltronic_sunny_grid_inout_freq_min }, /* Nominal frequency (QPIRI #2) == 50.0 */ + { "49.8", voltronic_sunny_grid_inout_freq_min }, /* Nominal frequency (QPIRI #2) == 50.0 */ + { "55.0", voltronic_sunny_grid_inout_freq_min }, /* Nominal frequency (QPIRI #2) == 60.0 */ + { "59.8", voltronic_sunny_grid_inout_freq_min }, /* Nominal frequency (QPIRI #2) == 60.0 */ + { "", 0 } +}; + +/* Preprocess range value for (not overwritten) minimum grid input/output frequency */ +static int voltronic_sunny_grid_inout_freq_min(char *value, const size_t len) +{ + char *ptr; + const int val = strtol(value, &ptr, 10) * 10 + (*ptr == '.' ? strtol(++ptr, NULL, 10) : 0); + double gfn; + const char *gridfreqnom = dstate_getinfo("grid.frequency.nominal"); + + NUT_UNUSED_VARIABLE(len); /* FIXME? */ + + if (!gridfreqnom) { + upsdebugx(2, "%s: unable to get grid.frequency.nominal", __func__); + return -1; + } + + gfn = strtod(gridfreqnom, NULL); + + switch (val) + { + case 450: + case 498: + /* Nominal frequency (QPIRI #2) == 50.0 */ + if (gfn == 50.0) + return 0; + break; + case 550: + case 598: + /* Nominal frequency (QPIRI #2) == 60.0 */ + if (gfn == 60.0) + return 0; + break; + default: + upsdebugx(2, "%s: unknown value (%s)", __func__, value); + break; + } + + return -1; +} +#endif /* TESTING_AXPERT_GRID_OUTPUT_VOLTAGE_LIMITS_QUERY */ + +#if TESTING_AXPERT_WAITING_TIME_BEFORE_GRID_CONNECTION_QUERY_CHANGE +/* Range for waiting time before grid connection: overwritten (if appropriate) at runtime by voltronic_sunny_set_limits() (QVFTR #9, #10) */ +static info_rw_t voltronic_sunny_r_grid_waiting_time[] = { + { "5", 0 }, + { "999", 0 }, + { "", 0 } +}; +#endif /* TESTING_AXPERT_WAITING_TIME_BEFORE_GRID_CONNECTION_QUERY_CHANGE */ + +#if TESTING_AXPERT_BATTERY_CHARGING_DATA_QUERY || TESTING_AXPERT_BATTERY_CHARGING_LIMITS_CHANGE +/* Range for maximum battery-charging current: filled at runtime by voltronic_sunny_set_limits() (QVFTR #11, #12), overwritten (if appropriate) by voltronic_sunny_update_related_vars_limits() */ +static info_rw_t voltronic_sunny_r_bc_v_floating[] = { + { "", 0 }, + { "", 0 }, + { "", 0 } +}; + +/* Range for maximum battery-charging current: overwritten (if appropriate) at runtime by voltronic_sunny_set_limits() (QVFTR #13, #14) and voltronic_sunny_update_related_vars_limits() */ +static info_rw_t voltronic_sunny_r_bc_c_max[] = { /* FIXME: ranges only support ints */ + { "0.5", 0 }, + { "25.0", 0 }, + { "", 0 } +}; + +/* Range for bulk battery-charging voltage: overwritten (if appropriate) at runtime by voltronic_sunny_set_limits() (QVFTR #25, #26) and voltronic_sunny_update_related_vars_limits() */ +static info_rw_t voltronic_sunny_r_bc_v_bulk[] = { + { "47", 0 }, + { "50", voltronic_sunny_bc_v_bulk }, /* IFF FW version (QSVFW2) >= 0.3 */ + { "57", voltronic_sunny_bc_v_bulk }, /* IFF FW version (QSVFW2) < 0.3 */ + { "", 0 } +}; + +/* Preprocess range value for (not overwritten) bulk battery-charging voltage */ +static int voltronic_sunny_bc_v_bulk(char *value, const size_t len) +{ + const int val = strtol(value, NULL, 10); + + NUT_UNUSED_VARIABLE(len); /* FIXME? */ + + switch (val) + { + case 50: /* FW version (QSVFW2) >= 0.3 */ + if (fw >= 0.3) + return 0; + break; + case 57: /* FW version (QSVFW2) < 0.3 */ + if (fw < 0.3) + return 0; + break; + default: + upsdebugx(2, "%s: unknown value (%s)", __func__, value); + break; + } + + return -1; +} +#endif /* TESTING_AXPERT_BATTERY_CHARGING_DATA_QUERY || TESTING_AXPERT_BATTERY_CHARGING_LIMITS_CHANGE */ + +#if TESTING_AXPERT_BATTERY_CHARGING_LIMITS_QUERY_CHANGE +/* Range for minimum floating battery-charging current: filled at runtime by voltronic_sunny_update_related_vars_limits() */ +static info_rw_t voltronic_sunny_r_bc_c_floating_low[] = { + { "0", 0 }, + { "", 0 }, + { "", 0 } +}; + +/* Range for restart battery-charging voltage: filled at runtime by voltronic_sunny_update_related_vars_limits() */ +static info_rw_t voltronic_sunny_r_bc_v_restart[] = { + { "0", 0 }, + { "", 0 }, + { "", 0 } +}; + +/* Range for floating battery-charging current time thershold */ +static info_rw_t voltronic_sunny_r_bc_time_threshold[] = { + { "0", 0 }, + { "900", 0 }, + { "", 0 } +}; + +/* Range for cut-off battery-discharging voltage when grid is not available: overwritten (if appropriate) at runtime by voltronic_sunny_update_related_vars_limits() */ +static info_rw_t voltronic_sunny_r_bd_v_cutoff_gridoff[] = { + { "40", 0 }, + { "48", 0 }, + { "", 0 } +}; + +/* Range for cut-off battery-discharging voltage when grid is available: overwritten (if appropriate) at runtime by voltronic_sunny_update_related_vars_limits() */ +static info_rw_t voltronic_sunny_r_bd_v_cutoff_gridon[] = { + { "40", 0 }, + { "48", 0 }, + { "", 0 } +}; + +/* Range for restart battery-discharging voltage when grid is unavailable: filled at runtime by voltronic_sunny_update_related_vars_limits() */ +static info_rw_t voltronic_sunny_r_bd_v_restart_gridoff[] = { + { "", 0 }, + { "", 0 }, + { "", 0 } +}; + +/* Range for restart battery-discharging voltage when grid is available: filled at runtime by voltronic_sunny_update_related_vars_limits() */ +static info_rw_t voltronic_sunny_r_bd_v_restart_gridon[] = { + { "", 0 }, + { "", 0 }, + { "", 0 } +}; + +/* Range for maximum PV input voltage: overwritten (if appropriate) at runtime by voltronic_sunny_set_limits() (QVFTR #15, #16) */ +static info_rw_t voltronic_sunny_r_pv_input_volt_max[] = { + { "450", 0 }, + { "510", voltronic_sunny_pv_input_volt_max }, /* P16 */ + { "550", voltronic_sunny_pv_input_volt_max }, /* P15 */ + { "", 0 } +}; + +/* Preprocess range value for (not overwritten) maximum PV input voltage */ +static int voltronic_sunny_pv_input_volt_max(char *value, const size_t len) +{ + const int val = strtol(value, NULL, 10); + + NUT_UNUSED_VARIABLE(len); /* FIXME? */ + + switch (val) + { + case 510: /* P16 */ + if (protocol == 16) + return 0; + break; + case 550: /* P15 */ + if (protocol == 15) + return 0; + break; + default: + upsdebugx(2, "%s: unknown value (%s)", __func__, value); + break; + } + + return -1; +} + +/* Range for minimum PV input voltage: overwritten (if appropriate) at runtime by voltronic_sunny_set_limits() (QVFTR #17, #18) */ +static info_rw_t voltronic_sunny_r_pv_input_volt_min[] = { + { "90", 0 }, + { "200", 0 }, + { "", 0 } +}; + +/* Range for maximum MPP voltage: overwritten (if appropriate) at runtime by voltronic_sunny_set_limits() (QVFTR #19, #20) */ +static info_rw_t voltronic_sunny_r_mpp_input_volt_max[] = { + { "400", 0 }, + { "450", 0 }, + { "", 0 } +}; + +/* Range for minimum MPP voltage: overwritten (if appropriate) at runtime by voltronic_sunny_set_limits() (QVFTR #21, #22) */ +static info_rw_t voltronic_sunny_r_mpp_input_volt_min[] = { + { "110", 0 }, + { "200", 0 }, + { "", 0 } +}; +#endif /* TESTING_AXPERT_BATTERY_CHARGING_LIMITS_QUERY_CHANGE */ + +#if TESTING_AXPERT_MAX_OUTPUT_POWER_QUERY_CHANGE +/* Range for maximum output power: filled(/overwritten, if appropriate) at runtime by voltronic_sunny_set_limits() (QVFTR #23, #24) and voltronic_sunny_update_related_vars_limits() */ +static info_rw_t voltronic_sunny_r_output_realpower_max[] = { + { "0", 0 }, + { "", 0 }, + { "", 0 } +}; + +/* Range for maximum power feeding grid: max value filled at runtime by voltronic_sunny_update_related_vars_limits() */ +static info_rw_t voltronic_sunny_r_grid_realpower_max[] = { + { "0", 0 }, + { "", 0 }, + { "", 0 } +}; + +/* Range for LCD sleep time (time after which LCD screen-saver starts) */ +static info_rw_t voltronic_sunny_r_lcd_sleep_time[] = { + { "0", 0 }, /* 00 */ + { "2970", 0 }, /* 99 */ + { "", 0 } +}; + +/* Time (hh:mm) length */ +static info_rw_t voltronic_sunny_l_hhmm[] = { + { "5", 0 }, + { "", 0 } +}; + +/* Range for maximum grid input average voltage */ +static info_rw_t voltronic_sunny_r_grid_in_avg_volt_max[] = { + { "235", 0 }, + { "265", 0 }, + { "", 0 } +}; + +/* Range for grid power deviation */ +static info_rw_t voltronic_sunny_r_grid_power_deviation[] = { + { "0", 0 }, + { "999", 0 }, + { "", 0 } +}; +#endif /* TESTING_AXPERT_MAX_OUTPUT_POWER_QUERY_CHANGE */ + +#if TESTING_AXPERT_PF_QUERY_CHANGE +/* Range for power factor */ +static info_rw_t voltronic_sunny_r_output_powerfactor[] = { /* FIXME: 1. nutdrv_qx setvar+RANGE doesn't support negative values; 2. values should be divided by 100 */ + { "-99", 0 }, + { "-90", 0 }, + { "90", 0 }, + { "100", 0 }, + { "", 0 } +}; + +/* Range for power percent setting */ +static info_rw_t voltronic_sunny_r_powerpercent_setting[] = { + { "10", 0 }, + { "100", 0 }, + { "", 0 } +}; + +/* Range for power factor_percent */ +static info_rw_t voltronic_sunny_r_powerfactor_percent[] = { + { "50", 0 }, + { "100", 0 }, + { "", 0 } +}; + +/* Range for power factor curve */ +static info_rw_t voltronic_sunny_r_powerfactor_curve[] = { /* FIXME: nutdrv_qx setvar+RANGE doesn't support negative values */ + { "-99", 0 }, + { "-90", 0 }, + { "", 0 } +}; +#endif /* TESTING_AXPERT_PF_QUERY_CHANGE */ + +#if TESTING_AXPERT_DATETIME_QUERY_CHANGE +/* Date (YYYY/MM/DD) length */ +static info_rw_t voltronic_sunny_l_date[] = { + { "10", 0 }, + { "", 0 } +}; + +/* Time (hh:mm:ss) length */ +static info_rw_t voltronic_sunny_l_time[] = { + { "8", 0 }, + { "", 0 } +}; +#endif /* TESTING_AXPERT_DATETIME_QUERY_CHANGE */ + + +/* == qx2nut lookup table == */ +static item_t voltronic_axpert_qx2nut[] = { + + /*#######################################################################################################################################################################################################################################################################################################################################################################################################* + *# info_type |info_flags |info_rw |command |answer |answer |leading|value |from |to |dfl |qxflags |preprocess_command |preprocess_answer |preprocess #* + *# | | | |_len | | | | | | | | | | #* + *#######################################################################################################################################################################################################################################################################################################################################################################################################*/ + + /* Query device for protocol + * > [QPI\r] + * < [(PI30\r] + * 012345 + * 0 + */ + + { "device.firmware.aux", 0, NULL, "QPI\r", "", 6, '(', "", 1, 4, "%s", QX_FLAG_STATIC, voltronic_axpert_crc, voltronic_axpert_checkcrc, voltronic_axpert_protocol }, + + /* Query device for firmware version + * > [QVFW\r] + * < [(VERFW:00074.50\r] + * 0123456789012345 + * 0 1 + */ + + { "device.firmware", 0, NULL, "QVFW\r", "", 16, '(', "", 7, 14, "0X%s", QX_FLAG_STATIC, voltronic_axpert_crc, voltronic_axpert_checkcrc, voltronic_axpert_fw }, + + /* Query device for main CPU processor version + * > [QVFW\r] + * < [(VERFW:00074.50\r] + * 0123456789012345 + * 0 1 + */ + + { "device.firmware.main", 0, NULL, "QVFW\r", "", 16, '(', "", 7, 14, "0X%s", QX_FLAG_STATIC, voltronic_axpert_crc, voltronic_axpert_checkcrc, voltronic_axpert_hex_preprocess }, + + /* Query device for SCC1 CPU Firmware version + * > [QVFW2\r] + * < [(VERFW2:00000.31\r] + * 01234567890123456 + * 0 1 + */ + + { "device.firmware.scc1", 0, NULL, "QVFW2\r", "", 17, '(', "", 8, 15, "0X%s", QX_FLAG_STATIC, voltronic_axpert_crc, voltronic_axpert_checkcrc, voltronic_axpert_hex_preprocess }, + + /* Query device for SCC2 CPU Firmware version + * > [QVFW3\r] + * < [(VERFW3:00000.31\r] + * 01234567890123456 + * 0 1 + */ + + { "device.firmware.scc2", 0, NULL, "QVFW3\r", "", 17, '(', "", 8, 15, "0X%s", QX_FLAG_STATIC, voltronic_axpert_crc, voltronic_axpert_checkcrc, voltronic_axpert_hex_preprocess }, + + /* Query device for SCC3 CPU Firmware version + * > [QVFW4\r] + * < [(VERFW4:00000.31\r] + * 01234567890123456 + * 0 1 + */ + + { "device.firmware.scc3", 0, NULL, "QVFW4\r", "", 17, '(', "", 8, 15, "0X%s", QX_FLAG_STATIC, voltronic_axpert_crc, voltronic_axpert_checkcrc, voltronic_axpert_hex_preprocess }, + + + /* Query device for serial number + * > [QID\r] + * < [(12345679012345\r] <- As far as I know it hasn't a fixed length -> min length = ( + \r = 2 + * 0123456789012345 + * 0 1 + */ + + { "device.serial", 0, NULL, "QID\r", "", 2, '(', "", 1, 0, "%s", QX_FLAG_STATIC, voltronic_axpert_crc, voltronic_axpert_checkcrc, voltronic_axpert_serial_numb }, + + /*#######################################################################################################################################################################################################################################################################################################################################################################################################################################*/ + + /* Query device for capability; only those capabilities whom the device is capable of are reported as Enabled or Disabled + * > [QFLAG\r] + * < [(EakxyDbjuvz\r] + * 01234567890123 + * 0 * min length = ( + E + D + \r = 4 + * + * A Enable/disable silence buzzer or open buzzer + * B Enable/Disable overload bypass function + * J Enable/Disable power saving + * K Enable/Disable LCD display escape to default page after 1min timeout + * U Enable/Disable overload restart + * V Enable/Disable over temperature restart + * X Enable/Disable backlight on + * Y Enable/Disable alarm on when primary source interrupt + * Z Enable/Disable fault code record + * L Enable/Disable data log pop-up + */ + + { "battery.energysave", ST_FLAG_RW, voltronic_axpert_e_cap, "QFLAG\r", "", 4, '(', "", 1, 0, "%s", QX_FLAG_ENUM | QX_FLAG_SEMI_STATIC, voltronic_axpert_crc, voltronic_axpert_checkcrc, voltronic_axpert_capability }, + { "ups.beeper.status", 0, NULL, "QFLAG\r", "", 4, '(', "", 1, 0, "%s", QX_FLAG_SEMI_STATIC, voltronic_axpert_crc, voltronic_axpert_checkcrc, voltronic_axpert_capability }, +#if TESTING_AXPERT_CAPS_QUERY + /* Not available in NUT */ + /* FIXME: ups_status BYPASS and RB(?) */ + { "experimental.bypass_alarm", 0, NULL, "QFLAG\r", "", 4, '(', "", 1, 0, "%s", QX_FLAG_SEMI_STATIC | QX_FLAG_NONUT, voltronic_axpert_crc, voltronic_axpert_checkcrc, voltronic_axpert_capability }, + { "experimental.battery_alarm", 0, NULL, "QFLAG\r", "", 4, '(', "", 1, 0, "%s", QX_FLAG_SEMI_STATIC | QX_FLAG_NONUT, voltronic_axpert_crc, voltronic_axpert_checkcrc, voltronic_axpert_capability }, +#endif /* TESTING_AXPERT_CAPS_QUERY */ + +#if TESTING_AXPERT_CAPS_CHANGE + /* Enable or Disable or Reset to safe default values capability options + * > [PEX\r] > [PDX\r] > [PF\r] + * < [(ACK\r] < [(ACK\r] < [(ACK\r] + * 01234 01234 01234 + * 0 0 0 + */ + + { "battery.energysave", 0, voltronic_axpert_e_cap, "P%sJ\r", "", 5, '(', "", 1, 3, NULL, QX_FLAG_SETVAR | QX_FLAG_ENUM | QX_FLAG_SKIP, voltronic_axpert_crc, voltronic_axpert_checkcrc, voltronic_axpert_capability_set }, + /* Not available in NUT */ + { "experimental.reset_to_default", 0, NULL, "PF\r", "", 5, '(', "", 1, 3, NULL, QX_FLAG_SETVAR | QX_FLAG_NONUT, voltronic_axpert_crc, voltronic_axpert_checkcrc, voltronic_axpert_capability_reset }, + /* FIXME: ups_status BYPASS and RB(?) */ + { "experimental.bypass_alarm", 0, voltronic_axpert_e_cap_nonut, "P%sP\r", "", 5, '(', "", 1, 3, NULL, QX_FLAG_SETVAR | QX_FLAG_ENUM | QX_FLAG_NONUT | QX_FLAG_SKIP, voltronic_axpert_crc, voltronic_axpert_checkcrc, voltronic_axpert_capability_set_nonut }, + { "experimental.battery_alarm", 0, voltronic_axpert_e_cap_nonut, "P%sB\r", "", 5, '(', "", 1, 3, NULL, QX_FLAG_SETVAR | QX_FLAG_ENUM | QX_FLAG_NONUT | QX_FLAG_SKIP, voltronic_axpert_crc, voltronic_axpert_checkcrc, voltronic_axpert_capability_set_nonut }, +#endif /* TESTING_AXPERT_CAPS_CHANGE */ + +#if TESTING_AXPERT_OPERATIONAL_OPTIONS_QUERY_CHANGE + /* Query device for operational options flag (P16 only) + * > [QENF\r] + * < [(A1B1C1D1E1F0G1\r] <- required options (length: 16) + * < [(A1B0C1D0E1F0G0H0I_J_\r] <- known available options + * 0123456789012345678901 + * 0 1 2 + */ + + { "experimental.charge_battery", ST_FLAG_RW, voltronic_axpert_e_cap_nonut, "QENF\r", "", 16, '(', "", 2, 2, "%s", QX_FLAG_ENUM | QX_FLAG_NONUT | QX_FLAG_SEMI_STATIC, NULL, voltronic_axpert_checkcrc, voltronic_sunny_01 }, /* A */ + { "experimental.charge_battery_from_ac", ST_FLAG_RW, voltronic_axpert_e_cap_nonut, "QENF\r", "", 16, '(', "", 4, 4, "%s", QX_FLAG_ENUM | QX_FLAG_NONUT | QX_FLAG_SEMI_STATIC, NULL, voltronic_axpert_checkcrc, voltronic_sunny_01 }, /* B */ + { "experimental.feed_grid", ST_FLAG_RW, voltronic_axpert_e_cap_nonut, "QENF\r", "", 16, '(', "", 6, 6, "%s", QX_FLAG_ENUM | QX_FLAG_NONUT | QX_FLAG_SEMI_STATIC, NULL, voltronic_axpert_checkcrc, voltronic_sunny_01 }, /* C */ + { "experimental.discharge_battery_when_pv_on", ST_FLAG_RW, voltronic_axpert_e_cap_nonut, "QENF\r", "", 16, '(', "", 8, 8, "%s", QX_FLAG_ENUM | QX_FLAG_NONUT | QX_FLAG_SEMI_STATIC, NULL, voltronic_axpert_checkcrc, voltronic_sunny_01 }, /* D */ + { "experimental.discharge_battery_when_pv_off", ST_FLAG_RW, voltronic_axpert_e_cap_nonut, "QENF\r", "", 16, '(', "", 10, 10, "%s", QX_FLAG_ENUM | QX_FLAG_NONUT | QX_FLAG_SEMI_STATIC, NULL, voltronic_axpert_checkcrc, voltronic_sunny_01 }, /* E */ + { "experimental.feed_grid_from_battery_when_pv_on", ST_FLAG_RW, voltronic_axpert_e_cap_nonut, "QENF\r", "", 16, '(', "", 12, 12, "%s", QX_FLAG_ENUM | QX_FLAG_NONUT | QX_FLAG_SEMI_STATIC, NULL, voltronic_axpert_checkcrc, voltronic_sunny_01 }, /* F */ + { "experimental.feed_grid_from_battery_when_pv_off", ST_FLAG_RW, voltronic_axpert_e_cap_nonut, "QENF\r", "", 16, '(', "", 14, 14, "%s", QX_FLAG_ENUM | QX_FLAG_NONUT | QX_FLAG_SEMI_STATIC, NULL, voltronic_axpert_checkcrc, voltronic_sunny_01 }, /* G */ +/* { "unknown.?", ST_FLAG_RW, voltronic_axpert_e_cap_nonut, "QENF\r", "", 18, '(', "", 16, 16, "%s", QX_FLAG_ENUM | QX_FLAG_NONUT | QX_FLAG_SEMI_STATIC, NULL, voltronic_axpert_checkcrc, voltronic_sunny_01 }, *//* H */ +/* { "unknown.?", ST_FLAG_RW, voltronic_axpert_e_cap_nonut, "QENF\r", "", 20, '(', "", 18, 18, "%s", QX_FLAG_ENUM | QX_FLAG_NONUT | QX_FLAG_SEMI_STATIC, NULL, voltronic_axpert_checkcrc, voltronic_sunny_01 }, *//* I */ +/* { "unknown.?", ST_FLAG_RW, voltronic_axpert_e_cap_nonut, "QENF\r", "", 22, '(', "", 20, 20, "%s", QX_FLAG_ENUM | QX_FLAG_NONUT | QX_FLAG_SEMI_STATIC, NULL, voltronic_axpert_checkcrc, voltronic_sunny_01 }, *//* J */ + + /* Enable (: 1) or disable (: 0) operational option