diff --git a/NEWS.adoc b/NEWS.adoc index d5e9ff680b..dc2714ad1a 100644 --- a/NEWS.adoc +++ b/NEWS.adoc @@ -116,6 +116,10 @@ https://github.com/networkupstools/nut/milestone/11 - usbhid-ups updates: * Support of the `onlinedischarge_log_throttle_hovercharge` in the NUT v2.8.2 release was found to be incomplete. [#2423, follow-up to #2215] + * Added support for `lbrb_log_delay_sec=N` setting to delay propagation of + `LB` or `LB+RB` state (buggy with APC BXnnnnMI devices circa 2023-2024). + This may work better with flags like `onlinedischarge_calibration` and + `lbrb_log_delay_without_calibrating` for some devices. [#2347] * General suggestion from `possibly_supported()` message method for devices with VendorID=`0x06da` (Phoenixtec), seen in some models supported by MGE HID or Liebert HID, updated to suggest trying `nutdrv_qx`. [#334] @@ -222,6 +226,9 @@ during a NUT build. and values (or macros) in the NUT codebase. [issue #1176, issue #31] * custom `distcheck-something` targets did not inherit `DISTCHECK_FLAGS` properly. [#2541] + * added `status_get()` in NUT driver state API, to check if a status + token string had been set recently, and to avoid duplicate settings. + [PR #2565] * local socket/pipe protocol introduced a `LOGOUT` command for cleaner disconnection handling. [#2572] * codebase adapted to the liking of `clang-18` and newer revisions of diff --git a/UPGRADING.adoc b/UPGRADING.adoc index dbd5d7bde3..19175f58f8 100644 --- a/UPGRADING.adoc +++ b/UPGRADING.adoc @@ -50,6 +50,13 @@ Changes from 2.8.2 to 2.8.3 devices; just in case a flag toggle `powercom_sdcmd_byte_order_fallback` was added to set the old behavior (if some devices do need it). [PR #2480] +- Added support for `lbrb_log_delay_sec=N` setting to delay propagation of + `LB` or `LB+RB` state (buggy with APC BXnnnnMI devices/firmwares issued + circa 2023-2024 which flood the logs with spurious LOWBATT and REPLACEBATT + events). This may work better for some devices when combined with flags + like `onlinedischarge_calibration` and `lbrb_log_delay_without_calibrating`. + [#2347] + - Enabled installation of built PDF and HTML (including man page renditions) files under the configured `docdir`. It seems previously they were only built (if requested) but not installed via `make`, unlike the common man diff --git a/ci_build.sh b/ci_build.sh index f9e330922a..cf28f9b2d8 100755 --- a/ci_build.sh +++ b/ci_build.sh @@ -470,7 +470,7 @@ if [ -z "${CANBUILD_LIBGD_CGI-}" ]; then # NUT CI farm with Jenkins can build it; Travis could not [[ "$CI_OS_NAME" = "freebsd" ]] && CANBUILD_LIBGD_CGI=yes \ - || [[ "$TRAVIS_OS_NAME" = "freebsd" ]] && CANBUILD_LIBGD_CGI=no + || { [[ "$TRAVIS_OS_NAME" = "freebsd" ]] && CANBUILD_LIBGD_CGI=no ; } # See also below for some compiler-dependent decisions fi @@ -1294,7 +1294,11 @@ default|default-alldrv|default-alldrv:no-distcheck|default-all-errors|default-sp CONFIG_OPTS+=("--with-cgi=auto") fi else - CONFIG_OPTS+=("--with-cgi=auto") + if [ "${CANBUILD_LIBGD_CGI-}" = "no" ] ; then + CONFIG_OPTS+=("--without-cgi") + else + CONFIG_OPTS+=("--with-cgi=auto") + fi fi ;; "default-alldrv:no-distcheck") diff --git a/data/driver.list.in b/data/driver.list.in index b8f33caf43..7ed01d0987 100644 --- a/data/driver.list.in +++ b/data/driver.list.in @@ -79,6 +79,7 @@ "APC" "ups" "3" "Back-UPS BF500" "USB" "usbhid-ups" "APC" "ups" "3" "BACK-UPS XS LCD" "USB" "usbhid-ups" "APC" "ups" "3" "Back-UPS XS 1000M (Back-UPS Pro 1000, Model BX1000M)" "USB" "usbhid-ups" # https://github.com/networkupstools/nut/issues/139 +"APC" "ups" "3" "Back-UPS BX****MI Series (may need tweaks since 2023)" "USB" "usbhid-ups lbrb_log_delay_sec=N lbrb_log_delay_without_calibrating onlinedischarge_calibration" # https://github.com/networkupstools/nut/issues/2347 "APC" "ups" "3" "SMC2200BI-BR" "USB" "usbhid-ups" # https://github.com/networkupstools/nut/issues/557 "APC" "ups" "3" "Smart-UPS (USB)" "USB" "usbhid-ups" "APC" "ups" "3" "Smart-UPS 750 (SMT750I, USB)" "USB" "usbhid-ups" diff --git a/docs/man/usbhid-ups.txt b/docs/man/usbhid-ups.txt index 87f29c520d..12f9703218 100644 --- a/docs/man/usbhid-ups.txt +++ b/docs/man/usbhid-ups.txt @@ -160,6 +160,20 @@ not forcing it to be fully charged all the time. As long as the current value of `battery.charge` remains at or above this threshold percentage (default 100), the `OL+DISCHRG` message logging is not triggered by variations of the charge. +*lbrb_log_delay_sec*='num':: +Set to delay status-setting (and log messages) about device entering `LB` or +`LB+RB` state. ++ +Some APC BXnnnnMI device models or firmware versions (reportedly 2023-2024) +frequently report low battery, replace battery, and all ok within a couple +of seconds, sometimes but not always preceded by OL+DISCHRG (presumably +calibration). This setting lets the driver ignore short-lived states and +only pay attention if they persist longer than this setting (and the device +power state is `OL`). + +*lbrb_log_delay_without_calibrating*:: +Set to apply `lbrb_log_delay_sec` even if device is not calibrating. + *disable_fix_report_desc*:: Set to disable fix-ups for broken USB encoding, etc. which we apply by default on certain models (vendors/products) which were reported as not following the diff --git a/docs/new-drivers.txt b/docs/new-drivers.txt index 8503a38287..b425a32b56 100644 --- a/docs/new-drivers.txt +++ b/docs/new-drivers.txt @@ -214,7 +214,11 @@ UPS status flags like on line (OL) and on battery (OB) live in ups.status. Don't manipulate this by hand. There are functions which will do this for you. - status_init() -- before doing anything else + status_init() -- before doing anything else (clear internal buffers, + etc.) + + status_get(val) -- optionally check if a status word had been set + since the most-recent status_init() status_set(val) -- add a status word (OB, OL, etc) diff --git a/docs/nut.dict b/docs/nut.dict index 2b770e6d43..ae0598b608 100644 --- a/docs/nut.dict +++ b/docs/nut.dict @@ -1,4 +1,4 @@ -personal_ws-1.1 en 3205 utf-8 +personal_ws-1.1 en 3208 utf-8 AAC AAS ABI @@ -100,6 +100,7 @@ BTS BTV BUFRD BUZ +BXnnnnMI BYP BZ BZOFF @@ -981,6 +982,7 @@ README REALPATH REDi REFREPO +REPLACEBATT REPLBATT REQSSL RETPCT @@ -2150,6 +2152,7 @@ labcd lan langid lasaine +lbrb ld ldd le diff --git a/drivers/dstate.c b/drivers/dstate.c index 876aa16e02..f6edaf616a 100644 --- a/drivers/dstate.c +++ b/drivers/dstate.c @@ -1620,6 +1620,36 @@ void status_init(void) memset(status_buf, 0, sizeof(status_buf)); } +/* check if a status element has been set, return 0 if not, 1 if yes + * (considering a whole-word token in temporary status_buf) */ +int status_get(const char *buf) +{ + char *s = NULL; + size_t offset = 0, buflen = 0; + + if (!buf || !*buf || !*status_buf) + return 0; + + s = strstr(status_buf, buf); + buflen = strlen(buf); + + /* not found */ + if (!s) + return 0; + + offset = status_buf - s; + if (offset == 0 || status_buf[offset - 1] == ' ') { + /* We have hit the start of token */ + if (s[buflen] == '\0' || s[buflen] == ' ') { + /* And we have hit the end of token */ + return 1; + } + } + + /* buf was a substring of some other token */ + return 0; +} + /* add a status element */ void status_set(const char *buf) { @@ -1628,6 +1658,11 @@ void status_set(const char *buf) return; } + if (status_get(buf)) { + upsdebugx(2, "%s: status was already set: %s", __func__, buf); + return; + } + /* separate with a space if multiple elements are present */ if (strlen(status_buf) > 0) { snprintfcat(status_buf, sizeof(status_buf), " %s", buf); diff --git a/drivers/dstate.h b/drivers/dstate.h index 13b2957737..bb578f61aa 100644 --- a/drivers/dstate.h +++ b/drivers/dstate.h @@ -101,6 +101,10 @@ int dstate_is_stale(void); /* clean out the temp space for a new pass */ void status_init(void); +/* check if a status element has been set, return 0 if not, 1 if yes + * (considering a whole-word token in temporary status_buf) */ +int status_get(const char *buf); + /* add a status element */ void status_set(const char *buf); diff --git a/drivers/usbhid-ups.c b/drivers/usbhid-ups.c index 467265a68b..b8ff6662a5 100644 --- a/drivers/usbhid-ups.c +++ b/drivers/usbhid-ups.c @@ -29,7 +29,7 @@ */ #define DRIVER_NAME "Generic HID driver" -#define DRIVER_VERSION "0.55" +#define DRIVER_VERSION "0.56" #define HU_VAR_WAITBEFORERECONNECT "waitbeforereconnect" @@ -142,6 +142,15 @@ static size_t interrupt_pipe_EIO_count = 0; /* How many times we had I/O errors static time_t lastpoll; /* Timestamp the last polling */ hid_dev_handle_t udev = HID_DEV_HANDLE_CLOSED; +/** + * Track when calibration started, whether known from UPS status flags + * or interpreted from OL&DISCHRG combo on some devices (see below). + * The last_calibration_start is reset to 0 when the status becomes + * inactive, and last_calibration_finish is incremented every time. + */ +static time_t last_calibration_start = 0; +static time_t last_calibration_finish = 0; + /** * CyberPower UT series sometime need a bit of help deciding their online status. * This quirk is to enable the special handling of OL & DISCHRG at the same time @@ -196,6 +205,34 @@ static int onlinedischarge_log_throttle_charge = -1; */ static int onlinedischarge_log_throttle_hovercharge = 100; +/** + * Per https://github.com/networkupstools/nut/issues/2347 some + * APC BXnnnnMI devices made (flashed?) in 2023-2024 irregularly + * but frequently spew a series of state changes: + * * (maybe OL+DISCHRG), + * * LB, + * * RB, + * * + * within a couple of seconds. If this tunable is positive, we + * would only report the device states on the bus if they persist + * that long (or more), only then assuming they reflect a real + * problematic state and not some internal calibration. + */ +static int lbrb_log_delay_sec = 0; +/** + * By default we only act on (lbrb_log_delay_sec>0) when the device + * is in calibration mode of whatever nature (directly reported or + * assumed from other flag combos). With this flag we do not check + * for calibration and only look at LB + RB timestamps. + */ +static int lbrb_log_delay_without_calibrating = 0; +/** + * When did we last enter the situation? (if more than lbrb_log_delay_sec + * ago, then set the device status and emit the message) + */ +static time_t last_lb_start = 0; +static time_t last_rb_start = 0; + /* support functions */ static hid_info_t *find_nut_info(const char *varname); static hid_info_t *find_hid_info(const HIDData_t *hiddata); @@ -1015,6 +1052,12 @@ void upsdrv_makevartable(void) addvar(VAR_VALUE, "onlinedischarge_log_throttle_hovercharge", "Set to throttle log messages about discharging while online (only if battery.charge is under this value)"); + addvar(VAR_VALUE, "lbrb_log_delay_sec", + "Set to delay status-setting (and log messages) about device in LB or LB+RB state"); + + addvar(VAR_FLAG, "lbrb_log_delay_without_calibrating", + "Set to apply lbrb_log_delay_sec even if device is not calibrating"); + addvar(VAR_FLAG, "disable_fix_report_desc", "Set to disable fix-ups for broken USB encoding, etc. which we apply by default on certain vendors/products"); @@ -1067,9 +1110,9 @@ void upsdrv_updateinfo(void) upsdebugx(1, "Got to reconnect!"); if (use_interrupt_pipe == TRUE && interrupt_pipe_EIO_count > 0) { - upsdebugx(0, "\nReconnecting. If you saw \"nut_libusb_get_interrupt: Input/Output Error\" " + upsdebugx(0, "Reconnecting. If you saw \"nut_libusb_get_interrupt: Input/Output Error\" " "or similar message in the log above, try setting \"pollonly\" flag in \"ups.conf\" " - "options section for this driver!\n"); + "options section for this driver!"); } if (!reconnect_ups()) { @@ -1367,6 +1410,90 @@ void upsdrv_initups(void) } } + val = getval("lbrb_log_delay_sec"); + if (val) { + int ipv = atoi(val); + if ((ipv == 0 && strcmp("0", val)) || (ipv < 0)) { + lbrb_log_delay_sec = 3; + upslogx(LOG_WARNING, + "Warning: invalid value for " + "lbrb_log_delay_sec: %s, " + "defaulting to %d", + val, lbrb_log_delay_sec); + } else { + lbrb_log_delay_sec = ipv; + } + } else { + /* Activate APC BXnnnMI/BXnnnnMI tweaks, for details see + * https://github.com/networkupstools/nut/issues/2347 + */ + size_t productLen = hd->Product ? strlen(hd->Product) : 0; + + /* FIXME: Consider also ups.mfr.date as 2023 or newer? + * Eventually up to some year this gets fixed? + */ + if (hd->Vendor + && productLen > 6 /* BXnnnMI at least */ + && (!strcmp(hd->Vendor, "APC") || !strcmp(hd->Vendor, "American Power Conversion")) + && (strstr(hd->Product, " BX") || strstr(hd->Product, "BX") == hd->Product) + && (hd->Product[productLen - 2] == 'M' && hd->Product[productLen - 1] == 'I') + ) { + int got_lbrb_log_delay_without_calibrating = testvar("lbrb_log_delay_without_calibrating") ? 1 : 0, + got_onlinedischarge_calibration = testvar("onlinedischarge_calibration") ? 1 : 0, + got_onlinedischarge_log_throttle_sec = testvar("onlinedischarge_log_throttle_sec") ? 1 : 0; + + lbrb_log_delay_sec = 3; + + upslogx(LOG_INFO, "Defaulting lbrb_log_delay_sec=%d " + "for %s model %s%s%s%s%s%s%s%s%s%s", + lbrb_log_delay_sec, + hd->Vendor, hd->Product, + + !got_lbrb_log_delay_without_calibrating + || !got_onlinedischarge_calibration + || !got_onlinedischarge_log_throttle_sec + ? "; consider also setting the " : "", + + !got_lbrb_log_delay_without_calibrating + ? "lbrb_log_delay_without_calibrating " : "", + + !got_lbrb_log_delay_without_calibrating + && (!got_onlinedischarge_calibration + || !got_onlinedischarge_log_throttle_sec) + ? "and/or " : "", + + !got_onlinedischarge_calibration + ? "onlinedischarge_calibration " : "", + + (!got_lbrb_log_delay_without_calibrating + || !got_onlinedischarge_calibration ) + && !got_onlinedischarge_log_throttle_sec + ? "and/or " : "", + + !got_onlinedischarge_log_throttle_sec + ? "onlinedischarge_log_throttle_sec " : "", + + !got_lbrb_log_delay_without_calibrating + || !got_onlinedischarge_calibration + || !got_onlinedischarge_log_throttle_sec + ? "flag" : "", + + 2 > ( got_lbrb_log_delay_without_calibrating + + got_onlinedischarge_calibration + + got_onlinedischarge_log_throttle_sec) + ? "(s)" : "", + + !got_lbrb_log_delay_without_calibrating + || !got_onlinedischarge_calibration + || !got_onlinedischarge_log_throttle_sec + ? " in your configuration" : ""); + } + } + + if (testvar("lbrb_log_delay_without_calibrating")) { + lbrb_log_delay_without_calibrating = 1; + } + if (testvar("disable_fix_report_desc")) { disable_fix_report_desc = 1; } @@ -1967,10 +2094,49 @@ unsigned ups_status_get(void) return ups_status; } +/** Helper to both status_set("CAL") and track last_calibration_start timestamp */ +static void status_set_CAL(void) +{ + /* Note: dstate tokens can only be set, not cleared; a + * dstate_init() wipes the whole internal buffer though. */ + int wasSet = status_get("CAL"); + time_t now; + + time(&now); + + /* A few sanity checks */ + if (wasSet) { + if (!last_calibration_start) { + upsdebugx(2, "%s: status was already set but not time-stamped: CAL", __func__); + } else { + upsdebugx(2, "%s: status was already set %f sec ago : CAL", + __func__, difftime(now, last_calibration_start)); + } + } else { + if (last_calibration_finish) { + upsdebugx(2, "%s: starting a new calibration, last one finished %f sec ago", + __func__, difftime(now, last_calibration_finish)); + } else { + upsdebugx(2, "%s: starting a new calibration, first in this driver's lifetime", + __func__); + } + } + + if (!last_calibration_start) { + last_calibration_start = now; + } + + if (!wasSet) { + status_set("CAL"); /* calibration */ + } +} + /* Convert the local status information to NUT format and set NUT status. */ static void ups_status_set(void) { + int isCalibrating = 0; + if (ups_status & STATUS(VRANGE)) { dstate_setinfo("input.transfer.reason", "input voltage out of range"); } else if (ups_status & STATUS(FRANGE)) { @@ -1986,7 +2152,7 @@ static void ups_status_set(void) * raise FSD urgently. So we first let upsmon know it is just a drill. */ if (ups_status & STATUS(CALIB)) { - status_set("CAL"); /* calibration */ + status_set_CAL(); /* calibration */ } if ((!(ups_status & STATUS(DISCHRG))) && ( @@ -2009,7 +2175,7 @@ static void ups_status_set(void) /* if online but discharging */ if (onlinedischarge_calibration) { /* if we treat OL+DISCHRG as calibrating */ - status_set("CAL"); /* calibration */ + status_set_CAL(); /* calibration */ } if (onlinedischarge_onbattery) { @@ -2153,6 +2319,8 @@ static void ups_status_set(void) status_set("OL"); } + isCalibrating = status_get("CAL"); + if ((ups_status & STATUS(DISCHRG)) && !(ups_status & STATUS(DEPLETED))) { status_set("DISCHRG"); /* discharging */ @@ -2162,13 +2330,58 @@ static void ups_status_set(void) status_set("CHRG"); /* charging */ } if (ups_status & (STATUS(LOWBATT) | STATUS(TIMELIMITEXP) | STATUS(SHUTDOWNIMM))) { - status_set("LB"); /* low battery */ + if (lbrb_log_delay_sec < 1 + || (!isCalibrating && !lbrb_log_delay_without_calibrating) + || !(ups_status & STATUS(ONLINE)) /* assume actual power failure, do not delay */ + ) { + /* Quick and easy decision */ + status_set("LB"); /* low battery */ + } else { + time_t now; + time(&now); + + if (!last_lb_start) { + last_lb_start = now; + } else { + if (difftime(now, last_lb_start) > lbrb_log_delay_sec) { + /* Patience expired */ + status_set("LB"); /* low battery */ + } else { + upsdebugx(2, "%s: throttling LB status due to lbrb_log_delay_sec", __func__); + } + } + } + } else { + last_lb_start = 0; } if (ups_status & STATUS(OVERLOAD)) { status_set("OVER"); /* overload */ } if (ups_status & STATUS(REPLACEBATT)) { - status_set("RB"); /* replace batt */ + if (lbrb_log_delay_sec < 1 + || (!isCalibrating && !lbrb_log_delay_without_calibrating) + || !last_lb_start /* Calibration ended (not LB anymore) */ + || !(ups_status & STATUS(ONLINE)) /* assume actual power failure, do not delay */ + ) { + /* Quick and easy decision */ + status_set("RB"); /* replace batt */ + } else { + time_t now; + time(&now); + + if (!last_rb_start) { + last_rb_start = now; + } else { + if (difftime(now, last_rb_start) > lbrb_log_delay_sec) { + /* Patience expired */ + status_set("RB"); /* replace batt */ + } else { + upsdebugx(2, "%s: throttling RB status due to lbrb_log_delay_sec", __func__); + } + } + } + } else { + last_rb_start = 0; } if (ups_status & STATUS(TRIM)) { status_set("TRIM"); /* SmartTrim */ @@ -2182,6 +2395,15 @@ static void ups_status_set(void) if (ups_status & STATUS(OFF)) { status_set("OFF"); /* ups is off */ } + + if (!isCalibrating) { + if (last_calibration_start) { + time(&last_calibration_finish); + upsdebugx(2, "%s: calibration is no longer in place, took %f sec", + __func__, difftime(last_calibration_finish, last_calibration_start)); + } + last_calibration_start = 0; + } } /* find info element definition in info array diff --git a/m4/nut_check_libgd.m4 b/m4/nut_check_libgd.m4 index a8c7de5d17..f4a025dccc 100644 --- a/m4/nut_check_libgd.m4 +++ b/m4/nut_check_libgd.m4 @@ -141,6 +141,26 @@ if test -z "${nut_have_libgd_seen}"; then ]) ]) + if test "${nut_have_libgd}" = "yes"; then + AC_MSG_CHECKING([whether we can build, link and/or run a program with libgd]) + AC_LANG_PUSH([C]) + AX_RUN_OR_LINK_IFELSE([AC_LANG_PROGRAM([ +#include +#include +], +[ +gdImagePtr im = gdImageCreate(64, 128); +int back_color = gdImageColorAllocate(im, 255, 128, 32); +gdImageFilledRectangle(im, 0, 0, 64, 128, back_color); +gdImageColorTransparent(im, back_color); +gdImagePng(im, stdout); +gdImageDestroy(im); +] + )], [], [nut_have_libgd=no]) + AC_LANG_POP([C]) + AC_MSG_RESULT([${nut_have_libgd}]) + fi + if test "${nut_have_libgd}" = "yes"; then AC_DEFINE(HAVE_LIBGD, 1, [Define if you have Boutell's libgd installed]) LIBGD_CFLAGS="${CFLAGS}"