diff --git a/NEWS.adoc b/NEWS.adoc index 91b22aa97f..c29d4d638f 100644 --- a/NEWS.adoc +++ b/NEWS.adoc @@ -153,6 +153,12 @@ https://github.com/networkupstools/nut/milestone/11 models with serial port, made by NHS Sistemas Eletronicos LTDA and popular in Brazil. Currently this driver only builds on Linux. [#2692] + - `usbhid-ups` and `netxml-ups` updated to handle "No battery installed!" + alarm also to set the `RB` (Replace Battery) value in `ups.status`. + This may cause dual triggering of notifications (as an `ALARM` generally + and as an important `REPLBATT` status in particular) in `upsmon`, but + better safe than sorry. [#415] + - 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] @@ -246,7 +252,7 @@ https://github.com/networkupstools/nut/milestone/11 that there is no common standard for what constitutes an alarm and such alarm states were also previously observed for less severe reasons. This depends on the manufacturer/device-specific implementation in the driver. - [issue #2657, PR #2658] + [issues #415, #2657, PR #2658] * Updated documentation, end-user clients (CGI, NUT-Monitor UI); * Updated `upsmon` client with ability to report entering and exiting the ALARM status if reported by the driver; @@ -261,6 +267,14 @@ https://github.com/networkupstools/nut/milestone/11 from templates). [issue #321, PR #2383] * added an `OBLBDURATION` (seconds) setting to optionally delay raising the alarm for immediate shutdown in critical situation. [#321] + * optimized `parse_status()` by not checking further strings if we had + a match; report unexpected tokens in debug log. [#415] + * revised internal `do_notify()` method to support formatting strings + with two `%s` placeholders, to use if certain use-cases pass any extra + information (e.g. not just "we have alarms" but their values too). [#415] + * introduced handling for "unknown" `ups.status` tokens, reporting them + as "OTHER" notification type (whenever the set of such tokens appears + or changes) or "NOTOTHER" when they disappear. [#415] - More systemd integration: * Introduced a `nut-sleep.service` unit which stops `nut.target` when a diff --git a/UPGRADING.adoc b/UPGRADING.adoc index 2a2bfe79ff..3912b1149a 100644 --- a/UPGRADING.adoc +++ b/UPGRADING.adoc @@ -59,7 +59,13 @@ Changes from 2.8.2 to 2.8.3 some alarms can contribute to unwanted/early shutdowns. For this reason a `0|1` setting `ALARMCRITICAL` was introduced into `upsmon.conf` (default is `1`), for such users to be able to prevent their `upsmon` from treating - the `ALARM` status as overly severe when it is not in fact. [#2658] + the `ALARM` status as overly severe when it is not in fact. [#2658, #415] + +- `usbhid-ups` and `netxml-ups` updated to handle "No battery installed!" + alarm also to set the `RB` (Replace Battery) value in `ups.status`. + This may cause dual triggering of notifications (as an `ALARM` generally + and as an important `REPLBATT` status in particular) in `upsmon`, but + better safe than sorry. [#415] - `usbhid-ups` subdriver `PowerCOM HID` subdriver sent UPS `shutdown` and `stayoff` commands in wrong byte order, at least for devices currently diff --git a/clients/upsmon.c b/clients/upsmon.c index 647418c4ba..8c6a7876d2 100644 --- a/clients/upsmon.c +++ b/clients/upsmon.c @@ -3,6 +3,7 @@ Copyright (C) 1998 Russell Kroll 2012 Arnaud Quette + 2017 Eaton (author: Arnaud Quette ) 2020-2024 Jim Klimov This program is free software; you can redistribute it and/or modify @@ -175,6 +176,9 @@ static int nut_debug_level_global = -1; */ static int nut_debug_level_args = 0; +/* pre-declare internal methods */ +static int get_var(utype_t *ups, const char *var, char *buf, size_t bufsize); + static void setflag(int *val, int flag) { *val |= flag; @@ -295,7 +299,7 @@ static void notify(const char *notice, int flags, const char *ntype, #endif upsdebugx(6, "%s: sending notification for [%s]: type %s with flags 0x%04x: %s", - __func__, upsname, ntype, flags, notice); + __func__, upsname ? upsname : "upsmon itself", ntype, flags, notice); if (flag_isset(flags, NOTIFY_IGNORE)) { upsdebugx(6, "%s: NOTIFY_IGNORE", __func__); @@ -376,7 +380,7 @@ static void notify(const char *notice, int flags, const char *ntype, #endif } -static void do_notify(const utype_t *ups, int ntype) +static void do_notify(const utype_t *ups, int ntype, const char *extra) { int i; char msg[SMALLBUF], *upsname = NULL; @@ -387,6 +391,9 @@ static void do_notify(const utype_t *ups, int ntype) for (i = 0; notifylist[i].name != NULL; i++) { if (notifylist[i].type == ntype) { + const char *msgfmt = (notifylist[i].msg ? notifylist[i].msg : notifylist[i].stockmsg); + char *s = strstr(msgfmt, "%s"); + upsdebugx(2, "%s: ntype 0x%04x (%s)", __func__, ntype, notifylist[i].name); #ifdef HAVE_PRAGMAS_FOR_GCC_DIAGNOSTIC_IGNORED_FORMAT_NONLITERAL @@ -398,9 +405,33 @@ static void do_notify(const utype_t *ups, int ntype) #ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_FORMAT_SECURITY #pragma GCC diagnostic ignored "-Wformat-security" #endif - snprintf(msg, sizeof(msg), - notifylist[i].msg ? notifylist[i].msg : notifylist[i].stockmsg, - ups ? ups->sys : ""); + if (s) { + /* FIXME: Check user inputs to rule out + * any *OTHER* formatting characters. + * NOTE: Not addressing now, PR pending + * and queued for NUT v2.8.4 */ + + /* Got at least one "%s" */ + if (strstr(s + 1, "%s")) { + /* Got a second "%s" */ + /* FIXME: if (extra != NULL) and we + * only have one "%s", plaster another + * to "msgfmt" and follow this path? + */ + snprintf(msg, sizeof(msg), + msgfmt, + upsname ? upsname : "", + NUT_STRARG(extra)); + } else { + snprintf(msg, sizeof(msg), + msgfmt, + upsname ? upsname : ""); + } + } else { + /* No "%s" in this msgfmt! e.g. NOTIFY_NOPARENT, + * SUSPEND_STARTING, SUSPEND_FINISHED ... */ + snprintf(msg, sizeof(msg), "%s", msgfmt); + } #ifdef HAVE_PRAGMAS_FOR_GCC_DIAGNOSTIC_IGNORED_FORMAT_NONLITERAL #pragma GCC diagnostic pop #endif @@ -597,7 +628,7 @@ static void ups_is_alive(utype_t *ups) /* only notify for 0->1 transitions (to ignore the first connect) */ if (ups->commstate == 0) - do_notify(ups, NOTIFY_COMMOK); + do_notify(ups, NOTIFY_COMMOK, NULL); ups->commstate = 1; } @@ -612,7 +643,7 @@ static void ups_is_gone(utype_t *ups) ups->commstate = 0; /* COMMBAD is the initial loss of communications */ - do_notify(ups, NOTIFY_COMMBAD); + do_notify(ups, NOTIFY_COMMBAD, NULL); return; } @@ -625,7 +656,7 @@ static void ups_is_gone(utype_t *ups) /* now only complain if we haven't lately */ if ((now - ups->lastncwarn) > nocommwarntime) { /* NOCOMM indicates a persistent condition */ - do_notify(ups, NOTIFY_NOCOMM); + do_notify(ups, NOTIFY_NOCOMM, NULL); ups->lastncwarn = now; } } @@ -672,7 +703,7 @@ static void ups_is_off(utype_t *ups) upsdebugx(3, "%s: %s (first time)", __func__, ups->sys); /* must have changed from !OFF to OFF, so notify */ - do_notify(ups, NOTIFY_OFF); + do_notify(ups, NOTIFY_OFF, NULL); setflag(&ups->status, ST_OFF); } @@ -682,7 +713,7 @@ static void ups_is_notoff(utype_t *ups) ups->offsince = 0; ups->offstate = 0; if (flag_isset(ups->status, ST_OFF)) { /* actual change */ - do_notify(ups, NOTIFY_NOTOFF); + do_notify(ups, NOTIFY_NOTOFF, NULL); clearflag(&ups->status, ST_OFF); try_restore_pollfreq(ups); } @@ -703,7 +734,7 @@ static void ups_is_bypass(utype_t *ups) /* must have changed from !BYPASS to BYPASS, so notify */ - do_notify(ups, NOTIFY_BYPASS); + do_notify(ups, NOTIFY_BYPASS, NULL); setflag(&ups->status, ST_BYPASS); } @@ -712,7 +743,7 @@ static void ups_is_notbypass(utype_t *ups) /* Called when BYPASS is NOT among known states */ ups->bypassstate = 0; if (flag_isset(ups->status, ST_BYPASS)) { /* actual change */ - do_notify(ups, NOTIFY_NOTBYPASS); + do_notify(ups, NOTIFY_NOTBYPASS, NULL); clearflag(&ups->status, ST_BYPASS); try_restore_pollfreq(ups); } @@ -739,7 +770,11 @@ static void ups_is_eco(utype_t *ups) /* must have changed from !ECO to ECO, so notify */ - do_notify(ups, NOTIFY_ECO); + /* FIXME: Dig in driver/vendor specific info points to discern + * ECO, HE, ESS, ABM and other fancy words, with optional extra + * string argument? + */ + do_notify(ups, NOTIFY_ECO, NULL); setflag(&ups->status, ST_ECO); } @@ -748,7 +783,7 @@ static void ups_is_noteco(utype_t *ups) /* Called when ECO is NOT among known states */ ups->ecostate = 0; if (flag_isset(ups->status, ST_ECO)) { /* actual change */ - do_notify(ups, NOTIFY_NOTECO); + do_notify(ups, NOTIFY_NOTECO, NULL); clearflag(&ups->status, ST_ECO); try_restore_pollfreq(ups); } @@ -756,8 +791,26 @@ static void ups_is_noteco(utype_t *ups) static void ups_is_alarm(utype_t *ups) { - if (flag_isset(ups->status, ST_ALARM)) { /* no change */ - upsdebugx(4, "%s: %s (no change)", __func__, ups->sys); + /* Track the earlier reported string, re-notify if needed */ + static char alarms_prev[SMALLBUF]; + char alarms[SMALLBUF]; + + if (flag_isset(ups->status, ST_ALARM)) { /* potentially no change */ + /* TODO: add device.alarm.count? */ + if (get_var(ups, "alarm", alarms, sizeof(alarms)) == 0) { + if (!strcmp(alarms_prev, alarms)) { + upsdebugx(4, "%s: %s (no change)", __func__, ups->sys); + } else { + upsdebugx(4, "%s: %s: updated ups.alarm value is %s", + __func__, ups->sys, alarms); + strncpy(alarms_prev, alarms, sizeof(alarms_prev)); + /* FIXME: Check for two "%s" placeholders? */ + do_notify(ups, NOTIFY_ALARM, alarms); + } + } else { + upsdebugx(4, "%s: %s: still on alarm, failed to get current ups.alarm value", + __func__, ups->sys); + } return; } @@ -769,7 +822,20 @@ static void ups_is_alarm(utype_t *ups) /* must have changed from !ALARM to ALARM, so notify */ - do_notify(ups, NOTIFY_ALARM); + /* Pass `ups.status` string as the extra argument, + * so if the formatting string allows - it is detailed + * in the notification. */ + if (get_var(ups, "alarm", alarms, sizeof(alarms)) == 0) { + upsdebugx(4, "%s: %s: current ups.alarm value is %s", + __func__, ups->sys, alarms); + do_notify(ups, NOTIFY_ALARM, alarms); + } else { + upsdebugx(4, "%s: %s: failed to get current ups.alarm value", + __func__, ups->sys); + do_notify(ups, NOTIFY_ALARM, NULL); + alarms[0] = '\0'; + } + strncpy(alarms_prev, alarms, sizeof(alarms_prev)); setflag(&ups->status, ST_ALARM); } @@ -778,7 +844,7 @@ static void ups_is_notalarm(utype_t *ups) /* Called when ALARM is NOT among known states */ ups->alarmstate = 0; if (flag_isset(ups->status, ST_ALARM)) { /* actual change */ - do_notify(ups, NOTIFY_NOTALARM); + do_notify(ups, NOTIFY_NOTALARM, NULL); clearflag(&ups->status, ST_ALARM); try_restore_pollfreq(ups); } @@ -799,7 +865,7 @@ static void ups_on_batt(utype_t *ups) /* must have changed from OL to OB, so notify */ - do_notify(ups, NOTIFY_ONBATT); + do_notify(ups, NOTIFY_ONBATT, NULL); setflag(&ups->status, ST_ONBATT); clearflag(&ups->status, ST_ONLINE); } @@ -817,7 +883,7 @@ static void ups_on_line(utype_t *ups) /* ignore the first OL at startup, otherwise send the notifier */ if (ups->linestate != -1) - do_notify(ups, NOTIFY_ONLINE); + do_notify(ups, NOTIFY_ONLINE, NULL); ups->linestate = 1; @@ -855,7 +921,7 @@ static void doshutdown(void) upslogx(LOG_CRIT, "Executing automatic power-fail shutdown"); wall("Executing automatic power-fail shutdown\n"); - do_notify(NULL, NOTIFY_SHUTDOWN); + do_notify(NULL, NOTIFY_SHUTDOWN, NULL); sleep(finaldelay); @@ -1029,6 +1095,14 @@ static int get_var(utype_t *ups, const char *var, char *buf, size_t bufsize) numq = 3; } + if (!strcmp(var, "alarm")) { + /* Opaque string */ + query[0] = "VAR"; + query[1] = ups->upsname; + query[2] = "ups.alarm"; + numq = 3; + } + if (numq == 0) { upslogx(LOG_ERR, "get_var: programming error: var=%s", var); return -1; @@ -1416,7 +1490,7 @@ static void ups_low_batt(utype_t *ups) /* must have changed from !LB to LB, so notify */ - do_notify(ups, NOTIFY_LOWBATT); + do_notify(ups, NOTIFY_LOWBATT, NULL); setflag(&ups->status, ST_LOWBATT); } @@ -1427,7 +1501,7 @@ static void upsreplbatt(utype_t *ups) time(&now); if ((now - ups->lastrbwarn) > rbwarntime) { - do_notify(ups, NOTIFY_REPLBATT); + do_notify(ups, NOTIFY_REPLBATT, NULL); ups->lastrbwarn = now; } } @@ -1443,7 +1517,7 @@ static void ups_is_cal(utype_t *ups) /* must have changed from !CAL to CAL, so notify */ - do_notify(ups, NOTIFY_CAL); + do_notify(ups, NOTIFY_CAL, NULL); setflag(&ups->status, ST_CAL); } @@ -1451,7 +1525,7 @@ static void ups_is_notcal(utype_t *ups) { /* Called when CAL is NOT among known states */ if (flag_isset(ups->status, ST_CAL)) { /* actual change */ - do_notify(ups, NOTIFY_NOTCAL); + do_notify(ups, NOTIFY_NOTCAL, NULL); clearflag(&ups->status, ST_CAL); try_restore_pollfreq(ups); } @@ -1468,7 +1542,7 @@ static void ups_fsd(utype_t *ups) /* must have changed from !FSD to FSD, so notify */ - do_notify(ups, NOTIFY_FSD); + do_notify(ups, NOTIFY_FSD, NULL); setflag(&ups->status, ST_FSD); } @@ -1519,6 +1593,17 @@ static void redefine_ups(utype_t *ups, unsigned int pv, const char *un, ups->retain = 1; if (ups->pv != pv) { + /* TOCHECK: We start a config loading loop with totalpv=0 + * initially or in reload_conf(), and ignore duplicates + * in addups() unless reloading. If we reload a config + * with two definitions of same ups->sys name, will their + * power values be not ignored and add up? Should we + * subtract the older value from totalpv here? How do + * we differentiate (cookie?) first seeing an ups->sys + * which we knew about earlier during a reload_conf() + * from seeing it again? This is a somewhat rare case + * of self-inflicted pain, if it does even happen :) + */ upslogx(LOG_INFO, "UPS [%s]: redefined power value to %d", ups->sys, pv); ups->pv = pv; @@ -1595,7 +1680,8 @@ static void redefine_ups(utype_t *ups, unsigned int pv, const char *un, /* secondary|slave -> primary|master */ if ( ( (!strcasecmp(managerialOption, "primary")) || (!strcasecmp(managerialOption, "master")) ) - && (!flag_isset(ups->status, ST_PRIMARY)) ) { + && (!flag_isset(ups->status, ST_PRIMARY)) + ) { upslogx(LOG_INFO, "UPS [%s]: redefined as a primary", ups->sys); setflag(&ups->status, ST_PRIMARY); @@ -1607,7 +1693,8 @@ static void redefine_ups(utype_t *ups, unsigned int pv, const char *un, /* primary|master -> secondary|slave */ if ( ( (!strcasecmp(managerialOption, "secondary")) || (!strcasecmp(managerialOption, "slave")) ) - && (flag_isset(ups->status, ST_PRIMARY)) ) { + && (flag_isset(ups->status, ST_PRIMARY)) + ) { upslogx(LOG_INFO, "UPS [%s]: redefined as a secondary", ups->sys); clearflag(&ups->status, ST_PRIMARY); return; @@ -1681,7 +1768,8 @@ static void addups(int reloading, const char *sys, const char *pvs, tmp = tmp->next; } - tmp = xmalloc(sizeof(utype_t)); + tmp = xcalloc(1, sizeof(utype_t)); + /* TOTHINK: init (UPSCONN_t)tmp->conn struct fields too? */ tmp->sys = xstrdup(sys); tmp->pv = pv; @@ -1692,11 +1780,16 @@ static void addups(int reloading, const char *sys, const char *pvs, tmp->pw = xstrdup(pw); tmp->status = 0; + tmp->status_tokens = NULL; tmp->retain = 1; - /* ignore initial COMMOK and ONLINE by default */ + /* ignore initial COMMOK, ONLINE, etc. by default */ tmp->commstate = -1; tmp->linestate = -1; + tmp->offstate = -1; + tmp->bypassstate = -1; + tmp->ecostate = -1; + tmp->alarmstate = -1; /* forget poll-failure logging throttling */ tmp->pollfail_log_throttle_count = -1; @@ -1707,6 +1800,9 @@ static void addups(int reloading, const char *sys, const char *pvs, tmp->lastrbwarn = 0; tmp->lastncwarn = 0; + tmp->offsince = 0; + tmp->oblbsince = 0; + if ( (!strcasecmp(managerialOption, "primary")) || (!strcasecmp(managerialOption, "master")) ) { setflag(&tmp->status, ST_PRIMARY); @@ -2248,6 +2344,14 @@ static void ups_free(utype_t *ups) free(ups->hostname); free(ups->un); free(ups->pw); + + /* We usually free and nullify these as we have no use for them, + * but if something remains (had active alert etc.) - do it here: */ + if (ups->status_tokens) { + state_infofree(ups->status_tokens); + ups->status_tokens = NULL; + } + free(ups); } @@ -2272,6 +2376,7 @@ static void upsmon_cleanup(void) free(shutdowncmd); free(notifycmd); free(powerdownflag); + free(configfile); for (i = 0; notifylist[i].name != NULL; i++) { free(notifylist[i].msg); @@ -2429,7 +2534,9 @@ static int try_connect(utype_t *ups) /* deal with the contents of STATUS or ups.status for this ups */ static void parse_status(utype_t *ups, char *status) { - char *statword, *ptr; + char *statword, *ptr, other_stat_words[SMALLBUF]; + int handled_stat_words = 0, changed_other_stat_words = 0; + st_tree_timespec_t st_start; clear_alarm(); @@ -2462,41 +2569,179 @@ static void parse_status(utype_t *ups, char *status) ups_is_notalarm(ups); statword = status; + /* Track what status tokens we no longer see */ + state_get_timestamp(&st_start); + other_stat_words[0] = '\0'; /* split up the status words and parse each one separately */ while (statword != NULL) { + int handled = 0; + ptr = strchr(statword, ' '); if (ptr) *ptr++ = '\0'; upsdebugx(3, "parsing: [%s]", statword); + handled_stat_words++; - if (!strcasecmp(statword, "OL")) + /* Keep in sync with "Status data" chapter of docs/new-drivers.txt */ + if (!strcasecmp(statword, "OL")) { ups_on_line(ups); - if (!strcasecmp(statword, "OB")) + handled++; + } + else if (!strcasecmp(statword, "OB")) { ups_on_batt(ups); - if (!strcasecmp(statword, "LB")) + handled++; + } + else if (!strcasecmp(statword, "LB")) { ups_low_batt(ups); - if (!strcasecmp(statword, "RB")) + handled++; + } + else if (!strcasecmp(statword, "RB")) { upsreplbatt(ups); - if (!strcasecmp(statword, "CAL")) + handled++; + } + else if (!strcasecmp(statword, "CAL")) { ups_is_cal(ups); - if (!strcasecmp(statword, "OFF")) + handled++; + } + else if (!strcasecmp(statword, "OFF")) { ups_is_off(ups); - if (!strcasecmp(statword, "BYPASS")) + handled++; + } + else if (!strcasecmp(statword, "BYPASS")) { ups_is_bypass(ups); - if (!strcasecmp(statword, "ECO")) + handled++; + } + else if (!strcasecmp(statword, "ECO")) { ups_is_eco(ups); - if (!strcasecmp(statword, "ALARM")) + handled++; + } + else if (!strcasecmp(statword, "ALARM")) { ups_is_alarm(ups); + handled++; + } /* do it last to override any possible OL */ - if (!strcasecmp(statword, "FSD")) + else if (!strcasecmp(statword, "FSD")) { ups_fsd(ups); + handled++; + } + /* Known standard status tokens, some being obsoleted, no upsmon reaction assigned */ + else if ( + !strcasecmp(statword, "HB") + || !strcasecmp(statword, "CHRG") + || !strcasecmp(statword, "DISCHRG") + || !strcasecmp(statword, "OVER") + || !strcasecmp(statword, "TRIM") + || !strcasecmp(statword, "BOOST") + ) { + /* FIXME: Do we want these logged similar to OTHERs? */ + upsdebugx(4, "Known and ignored status token: [%s]", statword); + handled++; + } + + if (!handled) { + /* NOTE: Could just state_getinfo() but then + * to refresh the timestamp we'd need to walk + * it again. So replicating a bit of that code. */ + st_tree_t *sttmp = (ups->status_tokens ? state_tree_find(ups->status_tokens, statword) : NULL); + + /* Driver reported something non-standard? */ + upsdebugx(4, "Unexpected status token: [%s]", statword); + + snprintfcat(other_stat_words, + sizeof(other_stat_words), "%s%s", + *other_stat_words ? " " : "", + statword); + + /* Part of our job is to update the timestamp, + * so we can report which tokens disappeared + * and eject them from the list. + * + * The recorded value currently does not matter. + * + * FIXME: Use the value as e.g. "0"/"1" and + * maybe the state->aux as counter, to track + * and report that the non-standard token + * was seen more than once? + */ + if (sttmp) { + /* Start from the discovered node to shortcut to + * (static) st_tree_node_refresh_timestamp(sttmp) + * and complete the info-setting quickly. + */ + state_setinfo(&sttmp, statword, "1"); + } else { + /* This token was not seen last time => new state + * Handle with a notification type "OTHER" + * to report the new un-handled status. */ + changed_other_stat_words++; + upsdebugx(5, "Unexpected status token: [%s]: appeared", statword); + state_setinfo(&(ups->status_tokens), statword, "1"); + } + } update_crittimer(ups); statword = ptr; } + + if (ups->status_tokens) { + st_tree_t *node = ups->status_tokens, *sttmp = node; + + /* Scroll to the leftmost entry for alphanumeric sorted processing */ + while (sttmp) { + node = sttmp; + sttmp = sttmp->left; + } + + /* Go from left to right, on a freeing spree if need be */ + while (node) { + sttmp = node->right; + if (st_tree_node_compare_timestamp(node, &st_start) < 0) { + upsdebugx(5, "Unexpected status token: [%s]: disappeared", + NUT_STRARG(node->var)); + changed_other_stat_words++; + + /* whatever is on the left, hang it off current right */ + if (node->right) { + node->right->left = node->left; /* May be NULL*/ + } + /* whatever is on the right, hang it off current left */ + if (node->left) { + node->left->right = node->right; /* May be NULL*/ + } + + if (node == ups->status_tokens) { + ups->status_tokens = node->left ? node->left : node->right; + } + + /* forget the neighbors before dropping the remaining + * "tree" of one node */ + node->right = NULL; + node->left = NULL; + + state_infofree(node); + } + node = sttmp; + } + } + + if (changed_other_stat_words) { + if (*other_stat_words) { + do_notify(ups, NOTIFY_OTHER, other_stat_words); + } else { + do_notify(ups, NOTIFY_NOTOTHER, NULL); + + /* No words remain, drop the tree if still there */ + if (ups->status_tokens) { + state_infofree(ups->status_tokens); + ups->status_tokens = NULL; + } + } + } + + upsdebugx(3, "Handled %d status tokens", handled_stat_words); } /* see what the status of the UPS is and handle any changes */ @@ -2994,7 +3239,7 @@ static void check_parent(void) return; lastwarn = now; - do_notify(NULL, NOTIFY_NOPARENT); + do_notify(NULL, NOTIFY_NOPARENT, NULL); /* also do this in case the notifier isn't being effective */ upslogx(LOG_ALERT, "Parent died - shutdown impossible"); @@ -3377,11 +3622,13 @@ int main(int argc, char *argv[]) while (exit_flag == 0) { utype_t *ups; - time_t start, end, now; + time_t ttNow; + struct timeval start, end, now; #ifndef WIN32 - time_t prev; + struct timeval prev; #endif double dt = 0; + int sleep_overhead_tolerance = 5; if (isInhibitSupported()) init_Inhibitor(prog); @@ -3425,7 +3672,7 @@ int main(int argc, char *argv[]) } if (sleep_inhibitor_status == 1) { /* Preparing for sleep */ - do_notify(NULL, NOTIFY_SUSPEND_STARTING); + do_notify(NULL, NOTIFY_SUSPEND_STARTING, NULL); upslogx(LOG_INFO, "%s: Processing OS going to sleep", prog); Uninhibit(&sleep_inhibitor_fd); @@ -3463,17 +3710,18 @@ int main(int argc, char *argv[]) switch (sleep_inhibitor_status) { case 0: /* Waking up */ - do_notify(NULL, NOTIFY_SUSPEND_FINISHED); + do_notify(NULL, NOTIFY_SUSPEND_FINISHED, NULL); upslogx(LOG_INFO, "%s: Processing OS wake-up after sleep", prog); upsnotify(NOTIFY_STATE_WATCHDOG, NULL); upsnotify(NOTIFY_STATE_RELOADING, NULL); init_Inhibitor(prog); - time(&now); + gettimeofday(&now, NULL); + time(&ttNow); for (ups = firstups; ups != NULL; ups = ups->next) { ups->status = 0; - ups->lastpoll = now; + ups->lastpoll = ttNow; } set_reload_flag(1); @@ -3511,38 +3759,63 @@ int main(int argc, char *argv[]) /* reap children that have exited */ waitpid(-1, NULL, WNOHANG); - time(&start); + gettimeofday(&start, NULL); + upsdebugx(4, "Beginning %u-sec delay between main loop cycles", sleepval); if (isPreparingForSleepSupported()) { sleep_inhibitor_status = -2; + /* talking to sdbus and/or many short sleeps + * on congested systems can take time */ + sleep_overhead_tolerance = 15; now = start; - while (sleep_inhibitor_status < 0 && dt < sleepval) { + while (sleep_inhibitor_status < 0 && dt < sleepval && !exit_flag) { prev = now; + upsdebugx(7, "delay between main loop cycles: before sleep 1..."); + /* WARNING: This call can take several seconds itself + * on some systems, seen e.g. with Ubuntu in WSL after + * the PC spent some life-time sleeping */ sleep(1); + upsdebugx(7, "delay between main loop cycles: after sleep 1..."); sleep_inhibitor_status = isPreparingForSleep(); - time(&now); - dt = difftime(now, start); - upsdebugx(7, "start=%" PRIiMAX " now=%" PRIiMAX " dt=%g sleepval=%u sleep_inhibitor_status=%d", - (intmax_t)start, (intmax_t)now, dt, sleepval, sleep_inhibitor_status); - if (dt > (sleepval + 5) || difftime(now, prev) > 5) { - upsdebugx(2, "It seems we have slept without warning or the system clock was changed"); + upsdebugx(7, "delay between main loop cycles: after isPreparingForSleep()..."); + gettimeofday(&now, NULL); + dt = difftimeval(now, start); + upsdebugx(7, "start=%" PRIiMAX ".%06" PRIiMAX + " prev=%" PRIiMAX ".%06" PRIiMAX + " now=%" PRIiMAX ".%06" PRIiMAX + " dt=%.06f sleepval=%u sleep_inhibitor_status=%d", + (intmax_t)start.tv_sec, (intmax_t)start.tv_usec, + (intmax_t)prev.tv_sec, (intmax_t)prev.tv_usec, + (intmax_t)now.tv_sec, (intmax_t)now.tv_usec, + dt, sleepval, sleep_inhibitor_status); + if (dt > (sleepval + sleep_overhead_tolerance) || difftimeval(now, prev) > sleep_overhead_tolerance) { + upsdebugx(2, "It seems we have slept without warning or the system clock was changed (while in delay between main loop cycles)"); if (sleep_inhibitor_status < 0) sleep_inhibitor_status = 0; /* behave as woken up */ } else if (dt < 0) { - upsdebugx(2, "It seems the system clock was changed into the past"); + upsdebugx(2, "It seems the system clock was changed into the past (while in delay between main loop cycles)"); sleep_inhibitor_status = 0; /* behave as woken up */ } } if (sleep_inhibitor_status >= 0) { + gettimeofday(&end, NULL); + upsdebugx(4, "%u-sec delay between main loop cycles finished, took %.06f", + sleepval, difftimeval(end, start)); upsdebugx(2, "Aborting polling delay between main loop cycles because OS is preparing for sleep or just woke up"); goto end_loop_cycle; } + + /* NOTE: if exit_flag was raised in sleep-loop above, + * so we aborted it, we end soon after ifdef/endif, + * and so not handling here specially */ } else { /* sleep tight */ sleep(sleepval); } - time(&end); + gettimeofday(&end, NULL); + upsdebugx(4, "%u-sec delay between main loop cycles finished, took %.06f", + sleepval, difftimeval(end, start)); #else maxhandle = 0; memset(&handles, 0, sizeof(handles)); @@ -3560,9 +3833,12 @@ int main(int argc, char *argv[]) handles[maxhandle] = pipe_connection_overlapped.hEvent; maxhandle++; - time(&start); + gettimeofday(&start, NULL); + upsdebugx(4, "Beginning %u-sec delay between main loop cycles", sleepval); ret = WaitForMultipleObjects(maxhandle, handles, FALSE, sleepval*1000); - time(&end); + gettimeofday(&end, NULL); + upsdebugx(4, "%u-sec delay between main loop cycles finished, took %.06f", + sleepval, difftimeval(end, start)); if (ret == WAIT_FAILED) { upslogx(LOG_ERR, "Wait failed"); @@ -3611,8 +3887,8 @@ int main(int argc, char *argv[]) /* General-purpose handling of time jumps for OSes/run-times * without NUT direct support for suspend/inhibit */ - dt = difftime(end, start); - if (dt > (sleepval + 5)) { + dt = difftimeval(end, start); + if (dt > (sleepval + sleep_overhead_tolerance)) { upsdebugx(2, "It seems we have slept without warning or the system clock was changed"); if (sleep_inhibitor_status < 0) sleep_inhibitor_status = 0; /* behave as woken up */ diff --git a/clients/upsmon.h b/clients/upsmon.h index ec68bb1a07..ca0dba5c99 100644 --- a/clients/upsmon.h +++ b/clients/upsmon.h @@ -3,6 +3,7 @@ Copyright (C) 2000 Russell Kroll 2012 Arnaud Quette + 2017 Eaton (author: Arnaud Quette ) 2020-2024 Jim Klimov This program is free software; you can redistribute it and/or modify @@ -23,6 +24,8 @@ #ifndef NUT_UPSMON_H_SEEN #define NUT_UPSMON_H_SEEN 1 +#include "state.h" + /* flags for ups->status */ #define ST_ONLINE (1 << 0) /* UPS is on line (OL) */ @@ -38,6 +41,7 @@ #define ST_BYPASS (1 << 9) /* UPS is on bypass so not protecting */ #define ST_ECO (1 << 10) /* UPS is in ECO (High Efficiency) mode or similar tweak, e.g. Energy Saver System mode */ #define ST_ALARM (1 << 11) /* UPS has at least one active alarm */ +#define ST_OTHER (1 << 12) /* UPS has at least one unclassified status token */ /* required contents of flag file */ #define SDMAGIC "upsmon-shutdown-file" @@ -62,6 +66,7 @@ typedef struct { char *un; /* username (optional for now) */ char *pw; /* password from conf */ int status; /* status (see flags above) */ + st_tree_t *status_tokens; /* parsed ups.status, mapping each token to whatever value if it is currently set (evicted when not) */ int retain; /* tracks deletions at reload */ /* handle suppression of COMMOK and ONLINE at startup */ @@ -116,6 +121,10 @@ typedef struct { #define NOTIFY_ALARM 18 /* UPS has at least one active alarm */ #define NOTIFY_NOTALARM 19 /* UPS has no active alarms */ +/* Special handling below */ +#define NOTIFY_OTHER 28 /* UPS has at least one unclassified status token */ +#define NOTIFY_NOTOTHER 29 /* UPS has no unclassified status tokens anymore */ + #define NOTIFY_SUSPEND_STARTING 30 /* OS is entering sleep/suspend/hibernate slumber mode, and we know it */ #define NOTIFY_SUSPEND_FINISHED 31 /* OS just finished sleep/suspend/hibernate slumber mode, and we know it */ @@ -165,9 +174,21 @@ static struct { { NOTIFY_NOTBYPASS,"NOTBYPASS",NULL, "UPS %s: no longer on bypass", NOTIFY_DEFAULT }, { NOTIFY_ECO, "ECO", NULL, "UPS %s: in ECO mode (as defined by vendor)", NOTIFY_DEFAULT }, { NOTIFY_NOTECO, "NOTECO", NULL, "UPS %s: no longer in ECO mode", NOTIFY_DEFAULT }, - { NOTIFY_ALARM, "ALARM", NULL, "UPS %s is in an alarm state (has active alarms)", NOTIFY_DEFAULT }, + + /* NOTE: We remember the ups.alarm value and report it here, + * maybe optionally - e.g. check if the "ALARM" formatting + * string has actually one or two "%s" placeholders inside. + * Do issue a new notification if ups.alarm value changes. + */ + { NOTIFY_ALARM, "ALARM", NULL, "UPS %s: one or more active alarms: [%s]", NOTIFY_DEFAULT }, { NOTIFY_NOTALARM, "NOTALARM", NULL, "UPS %s is no longer in an alarm state (no active alarms)", NOTIFY_DEFAULT }, + /* Special handling, two string placeholders! + * Reported when status_tokens tree changes (and is not empty in the end) */ + { NOTIFY_OTHER, "OTHER", NULL, "UPS %s: has at least one unclassified status token: [%s]", NOTIFY_DEFAULT }, + /* Reported when status_tokens tree becomes empty */ + { NOTIFY_NOTOTHER, "NOTOTHER", NULL, "UPS %s has no unclassified status tokens anymore", NOTIFY_DEFAULT }, + { NOTIFY_SUSPEND_STARTING, "SUSPEND_STARTING", NULL, "OS is entering sleep/suspend/hibernate mode", NOTIFY_DEFAULT }, { NOTIFY_SUSPEND_FINISHED, "SUSPEND_FINISHED", NULL, "OS just finished sleep/suspend/hibernate mode, de-activating obsolete UPS readings to avoid an unfortunate shutdown", NOTIFY_DEFAULT }, diff --git a/common/common.c b/common/common.c index 3e0aa4336f..b4f1b5456f 100644 --- a/common/common.c +++ b/common/common.c @@ -308,6 +308,8 @@ int isPreparingForSleep(void) if (isSupported_PreparingForSleep == 0) { /* Already determined that we can not use it, e.g. due to perms */ errno = isSupported_PreparingForSleep_errno; + upsdebug_with_errno(8, "%s: isSupported_PreparingForSleep=%d", + __func__, isSupported_PreparingForSleep); return -errno; } @@ -357,16 +359,19 @@ int isPreparingForSleep(void) if (val == prev) { /* Unchanged */ + upsdebugx(8, "%s: state unchanged", __func__); return -1; } /* First run and not immediately going to sleep, assume unchanged (no-op for upsmon et al) */ if (prev < 0 && !val) { prev = val; + upsdebugx(8, "%s: state unchanged (assumed): first run and not immediately going to sleep", __func__); return -1; } /* 0 or 1 */ + upsdebugx(8, "%s: state changed): %" PRIi32 " -> %" PRIi32, __func__, prev, val); prev = val; return val; } @@ -2208,6 +2213,21 @@ const char *str_upsnotify_state(upsnotify_state_t state) { } } +static void upsnotify_suggest_NUT_QUIET_INIT_UPSNOTIFY_once(void) { + static int reported = 0; + + if (reported) + return; + + reported = 1; + + if (getenv("NUT_QUIET_INIT_UPSNOTIFY")) + return; + + upsdebugx(1, "On systems without service units, " + "consider `export NUT_QUIET_INIT_UPSNOTIFY=true`"); +} + /* Send (daemon) state-change notifications to an * external service management framework such as systemd */ @@ -2307,9 +2327,9 @@ int upsnotify(upsnotify_state_t state, const char *fmt, ...) "skipped for libcommonclient build, " "will not spam more about it", __func__, str_upsnotify_state(state)); - upsdebugx(1, "On systems without service units, " - "consider `export NUT_QUIET_INIT_UPSNOTIFY=true`"); + upsnotify_suggest_NUT_QUIET_INIT_UPSNOTIFY_once(); } + upsnotify_reported_disabled_systemd = 1; # else /* not WITHOUT_LIBSYSTEMD */ if (!getenv("NOTIFY_SOCKET")) { @@ -2319,8 +2339,7 @@ int upsnotify(upsnotify_state_t state, const char *fmt, ...) "was requested, but not running as a service " "unit now, will not spam more about it", __func__, str_upsnotify_state(state)); - upsdebugx(1, "On systems without service units, " - "consider `export NUT_QUIET_INIT_UPSNOTIFY=true`"); + upsnotify_suggest_NUT_QUIET_INIT_UPSNOTIFY_once(); } upsnotify_reported_disabled_systemd = 1; } else { @@ -2616,9 +2635,8 @@ int upsnotify(upsnotify_state_t state, const char *fmt, ...) "no notification tech defined, " "will not spam more about it", __func__, str_upsnotify_state(state)); - upsdebugx(1, "On systems without service units, " - "consider `export NUT_QUIET_INIT_UPSNOTIFY=true`"); upsnotify_reported_disabled_notech = 1; + upsnotify_suggest_NUT_QUIET_INIT_UPSNOTIFY_once(); } else { upsdebugx(6, "%s: failed to notify about state %s", @@ -2632,9 +2650,8 @@ int upsnotify(upsnotify_state_t state, const char *fmt, ...) upsdebugx(upsnotify_report_verbosity, "%s: logged the systemd watchdog situation once, " "will not spam more about it", __func__); - upsdebugx(1, "On systems without service units, " - "consider `export NUT_QUIET_INIT_UPSNOTIFY=true`"); upsnotify_reported_watchdog_systemd = 1; + upsnotify_suggest_NUT_QUIET_INIT_UPSNOTIFY_once(); } # endif #endif diff --git a/common/nutconf.cpp b/common/nutconf.cpp index cd8a76d5e9..3f1eef9aef 100644 --- a/common/nutconf.cpp +++ b/common/nutconf.cpp @@ -1324,6 +1324,10 @@ UpsmonConfiguration::NotifyType UpsmonConfiguration::NotifyTypeFromString(const return NOTIFY_ALARM; else if(str=="NOTALARM") return NOTIFY_NOTALARM; + else if(str=="OTHER") + return NOTIFY_OTHER; + else if(str=="NOTOTHER") + return NOTIFY_NOTOTHER; else if(str=="SUSPEND_STARTING") return NOTIFY_SUSPEND_STARTING; else if(str=="SUSPEND_FINISHED") diff --git a/common/nutwriter.cpp b/common/nutwriter.cpp index cafac79e48..bc26783d78 100644 --- a/common/nutwriter.cpp +++ b/common/nutwriter.cpp @@ -408,6 +408,8 @@ const NotifyFlagsStrings::TypeStrings NotifyFlagsStrings::type_str = { "NOTECO", // NOTIFY_NOTECO "ALARM", // NOTIFY_ALARM "NOTALARM", // NOTIFY_NOTALARM + "OTHER", // NOTIFY_OTHER + "NOTOTHER", // NOTIFY_NOTOTHER "SUSPEND_STARTING", // NOTIFY_SUSPEND_STARTING "SUSPEND_FINISHED", // NOTIFY_SUSPEND_FINISHED }; diff --git a/conf/upsmon.conf.sample.in b/conf/upsmon.conf.sample.in index e5d7235859..60bb61f1b5 100644 --- a/conf/upsmon.conf.sample.in +++ b/conf/upsmon.conf.sample.in @@ -334,9 +334,18 @@ POWERDOWNFLAG "@POWERDOWNFLAG@" # NOTIFYMSG NOTBYPASS "UPS %s: no longer on bypass" # NOTIFYMSG ECO "UPS %s: in ECO mode (as defined by vendor)" # NOTIFYMSG NOTECO "UPS %s: no longer in ECO mode (as defined by vendor)" -# NOTIFYMSG ALARM "UPS %s is in an alarm state (has active alarms)" +# NOTIFYMSG ALARM "UPS %s: one or more active alarms: [%s]" +# or NOTIFYMSG ALARM "UPS %s: one or more active alarms (check ups.alarm)" # NOTIFYMSG NOTALARM "UPS %s is no longer in an alarm state (no active alarms)" # +# Special handling is provided for surprise tokens seen in ups.status, which +# are not in the standard NUT dictionary (but some drivers are known to use); +# note that unlike other formatting strings, the "OTHER" one has two string +# placeholders "%s" (it is safe to use one, leaving just the UPS name, or none): +# +# NOTIFYMSG OTHER "UPS %s: has at least one unclassified status token: [%s]" +# NOTIFYMSG NOTOTHER "UPS %s has no unclassified status tokens anymore" +# # A few messages not directly related to UPS events are also available: # # NOTIFYMSG SUSPEND_STARTING "OS is entering sleep/suspend/hibernate mode" @@ -356,6 +365,7 @@ POWERDOWNFLAG "@POWERDOWNFLAG@" # REPLBATT : The UPS battery is bad and needs to be replaced # NOCOMM : A UPS is unavailable (can't be contacted for monitoring) # NOPARENT : The process that shuts down the system has died (shutdown impossible) +# ALARM : UPS has one or more alarms (look at "ups.alarm" for details) # -------------------------------------------------------------------------- # NOTIFYFLAG - change behavior of upsmon when NOTIFY events occur @@ -388,6 +398,9 @@ POWERDOWNFLAG "@POWERDOWNFLAG@" # NOTIFYFLAG ALARM SYSLOG+WALL # NOTIFYFLAG NOTALARM SYSLOG+WALL # +# NOTIFYFLAG OTHER SYSLOG+WALL +# NOTIFYFLAG NOTOTHER SYSLOG+WALL +# # NOTIFYFLAG SUSPEND_STARTING SYSLOG+WALL # NOTIFYFLAG SUSPEND_FINISHED SYSLOG+WALL # diff --git a/docs/man/nutconf.txt b/docs/man/nutconf.txt index ebacfb86bd..74835f3a50 100644 --- a/docs/man/nutconf.txt +++ b/docs/man/nutconf.txt @@ -115,6 +115,8 @@ Notification types are: - 'NOTBYPASS' (no longer on bypass) - 'ALARM' (UPS is in an alarm state (has active alarms)) - 'NOTALARM' (UPS is no longer in an alarm state (no active alarms)) +- 'OTHER' (UPS has at least one unclassified status token) +- 'NOTOTHER' (UPS has no unclassified status tokens anymore) - 'SUSPEND_STARTING' (OS is entering sleep/suspend/hibernate mode) - 'SUSPEND_FINISHED' (OS just finished sleep/suspend/hibernate mode) diff --git a/docs/man/upsmon.conf.txt b/docs/man/upsmon.conf.txt index 144c115ee9..15620e1df5 100644 --- a/docs/man/upsmon.conf.txt +++ b/docs/man/upsmon.conf.txt @@ -280,10 +280,18 @@ for more details see linkman:upsmon[8]. NOTECO;; UPS no longer in ECO mode (see above) -ALARM;; UPS is in an alarm state (has active alarms) +ALARM;; UPS has one or more active alarms (check ups.alarm); +for this notification, the `message` can contain a second `%s` placeholder +to substitute the current value of `ups.alarm`. NOTALARM;; UPS is no longer in an alarm state (no active alarms) +OTHER;; UPS has at least one unclassified `ups.status` token; +for this notification, the `message` can contain a second `%s` placeholder +to substitute the current collection of such tokens. + +NOTOTHER;; UPS has no unclassified status tokens anymore + SUSPEND_STARTING;; OS is entering sleep/suspend/hibernate mode SUSPEND_FINISHED;; OS just finished sleep/suspend/hibernate mode, diff --git a/docs/man/upsmon.txt b/docs/man/upsmon.txt index 7529bf2e72..55110802bc 100644 --- a/docs/man/upsmon.txt +++ b/docs/man/upsmon.txt @@ -193,11 +193,17 @@ on battery life). Older devices only implemented one or the other. UPS no longer in ECO mode (see above). *ALARM*:: -UPS is in an alarm state (has active alarms). +UPS has one or more active alarms (look at `ups.alarm` for details). *NOTALARM*:: UPS is no longer in an alarm state (no active alarms). +*OTHER*:: +UPS has at least one unclassified status token. + +*NOTOTHER*:: +UPS has no unclassified status tokens anymore. + *SUSPEND_STARTING*:: OS is entering sleep/suspend/hibernate mode. diff --git a/docs/new-drivers.txt b/docs/new-drivers.txt index 6c85afcc06..f7ad204627 100644 --- a/docs/new-drivers.txt +++ b/docs/new-drivers.txt @@ -244,9 +244,20 @@ Possible values for status_set: BOOST -- UPS is boosting incoming voltage FSD -- Forced Shutdown (restricted use, see the note below) -Anything else will not be recognized by the usual clients. Coordinate -with the nut-upsdev list before creating something new, since there will be -duplication and ugliness otherwise. +Anything else will not be recognized by the usual clients expecting a +particular NUT standard release. New tokens may appear over time, but +driver developers should coordinate with the nut-upsdev list before creating +something new, since there will be duplication and ugliness otherwise. +It is possible that eventually, due to hardware and software design evolution, +some concepts would be superseded by others. Fundamental meanings of the +flags listed above should not change (but these flags may become no longer +issued by the current NUT drivers; then may still be used e.g. in forks or +older packaged builds). + +Clients however MUST accept any space-separated tokens in `ups.status` +without error or crash, and MUST treat those defined above with the +ascribed meanings, but MAY ignore unidentified tokens (quietly by default, +or acknowledge the skip with a debug log message). [NOTE] ============================================================================== diff --git a/docs/nut.dict b/docs/nut.dict index b50bb33833..23a634c93e 100644 --- a/docs/nut.dict +++ b/docs/nut.dict @@ -1,4 +1,4 @@ -personal_ws-1.1 en 3259 utf-8 +personal_ws-1.1 en 3260 utf-8 AAC AAS ABI @@ -755,6 +755,7 @@ NOTIFYFLAG NOTIFYFLAGS NOTIFYMSG NOTOFF +NOTOTHER NQA NTP NUT's diff --git a/drivers/netxml-ups.c b/drivers/netxml-ups.c index cfdc31632c..9c8e01d2f5 100644 --- a/drivers/netxml-ups.c +++ b/drivers/netxml-ups.c @@ -42,7 +42,7 @@ #include "nut_stdint.h" #define DRIVER_NAME "network XML UPS" -#define DRIVER_VERSION "0.46" +#define DRIVER_VERSION "0.47" /** *_OBJECT query multi-part body boundary */ #define FORM_POST_BOUNDARY "NUT-NETXML-UPS-OBJECTS" @@ -1020,7 +1020,7 @@ static void netxml_status_set(void) if (STATUS_BIT(OVERLOAD)) { status_set("OVER"); /* overload */ } - if (STATUS_BIT(REPLACEBATT)) { + if (STATUS_BIT(REPLACEBATT) || STATUS_BIT(NOBATTERY)) { status_set("RB"); /* replace batt */ } if (STATUS_BIT(TRIM)) { diff --git a/drivers/usbhid-ups.c b/drivers/usbhid-ups.c index 895e3891ef..6d579ac24a 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.58" +#define DRIVER_VERSION "0.59" #define HU_VAR_WAITBEFORERECONNECT "waitbeforereconnect" @@ -2409,7 +2409,7 @@ static void ups_status_set(void) if (ups_status & STATUS(OVERLOAD)) { status_set("OVER"); /* overload */ } - if (ups_status & STATUS(REPLACEBATT)) { + if ((ups_status & STATUS(REPLACEBATT)) || (ups_status & STATUS(NOBATTERY))) { if (lbrb_log_delay_sec < 1 || (!isCalibrating && !lbrb_log_delay_without_calibrating) || !last_lb_start /* Calibration ended (not LB anymore) */ diff --git a/include/nutconf.hpp b/include/nutconf.hpp index a97298ce82..a3bdf7fdaf 100644 --- a/include/nutconf.hpp +++ b/include/nutconf.hpp @@ -1475,6 +1475,9 @@ class UpsmonConfiguration : public Serialisable NOTIFY_ALARM, NOTIFY_NOTALARM, + NOTIFY_OTHER = 28, + NOTIFY_NOTOTHER, + NOTIFY_SUSPEND_STARTING = 30, NOTIFY_SUSPEND_FINISHED, diff --git a/scripts/augeas/nutupsmonconf.aug.in b/scripts/augeas/nutupsmonconf.aug.in index e972a406d7..7e7f219433 100644 --- a/scripts/augeas/nutupsmonconf.aug.in +++ b/scripts/augeas/nutupsmonconf.aug.in @@ -127,6 +127,8 @@ let upsmon_notify_type = "ONLINE" | "NOTECO" | "ALARM" | "NOTALARM" + | "OTHER" + | "NOTOTHER" | "SUSPEND_STARTING" | "SUSPEND_FINISHED" diff --git a/scripts/misc/notifyme-debug b/scripts/misc/notifyme-debug index fb1620aa5c..22e2f79e76 100755 --- a/scripts/misc/notifyme-debug +++ b/scripts/misc/notifyme-debug @@ -1,12 +1,17 @@ #!/bin/sh -# This is a sample NUT notification script aimed to aid debugging of upsmon -# behavior. It creates or appends to a log file named per user ID in the -# temporary location with an entry per line like this: +# This is a sample NUT notification script aimed to aid debugging of +# `upsmon` behavior, as its NOTIFYCMD (as specified in upsmon.conf). +# This script creates or appends to a log file named per user ID in +# the specified or default temporary location with an entry per line +# like this: # Sat Apr 6 17:23:06 UTC 2024 [uid=77(nut) gid=77(nut) groups=77(nut)] COMMBAD [eco650]: Communications with UPS eco650 lost (1 tokens) # Sat Apr 6 17:28:01 UTC 2024 [uid=77(nut) gid=77(nut) groups=77(nut)] NOCOMM [eco650]: UPS eco650 is unavailable (1 tokens) # # Copyright (C) 2023-2024 by Jim Klimov # Licensed under the terms of the Network UPS Tools source license (GPLv2+) -printf '%s\t[%s]\t%s\t[%s]:\t%s\t(%s tokens)\n' "`date -u`" "`id`" "${NOTIFYTYPE-}" "${UPSNAME-}" "$*" "$#" >> "/tmp/notifyme-`id -u`.log" +[ -n "${TEMPDIR-}" ] || TEMPDIR="${TMPDIR-}" +[ -n "${TEMPDIR-}" ] || { [ -d "/dev/shm" && TEMPDIR="/dev/shm" || TEMPDIR="/tmp" ; } + +printf '%s\t[%s]\t%s\t[%s]:\t%s\t(%s tokens)\n' "`date -u`" "`id`" "${NOTIFYTYPE-}" "${UPSNAME-}" "$*" "$#" >> "${TEMPDIR}/notifyme-`id -u`.log" diff --git a/tests/NIT/Makefile.am b/tests/NIT/Makefile.am index 069825355f..825df6bd92 100644 --- a/tests/NIT/Makefile.am +++ b/tests/NIT/Makefile.am @@ -31,16 +31,20 @@ check-NIT-devel: $(abs_srcdir)/nit.sh +@cd "$(top_builddir)/drivers" && $(MAKE) $(AM_MAKEFLAGS) -s dummy-ups$(EXEEXT) upsdrvctl$(EXEEXT) +@$(MAKE) $(AM_MAKEFLAGS) check-NIT +# Allow to override with make/env vars; provide sensible defaults (see nit.sh): +#NIT_CASE = testcase_sandbox_start_drivers_after_upsd +NIT_CASE = testgroup_sandbox_upsmon_master +NUT_PORT = 12345 check-NIT-sandbox: $(abs_srcdir)/nit.sh [ -n "$${DEBUG_SLEEP-}" ] && [ "$${DEBUG_SLEEP-}" -gt 0 ] || DEBUG_SLEEP=600 ; export DEBUG_SLEEP ; \ LANG=C LC_ALL=C TZ=UTC \ - NUT_PORT=12345 NIT_CASE=testcase_sandbox_start_drivers_after_upsd NUT_FOREGROUND_WITH_PID=true \ + NUT_PORT=$(NUT_PORT) NIT_CASE="$(NIT_CASE)" NUT_FOREGROUND_WITH_PID=true \ "$(abs_srcdir)/nit.sh" check-NIT-sandbox-devel: $(abs_srcdir)/nit.sh +[ -n "$${DEBUG_SLEEP-}" ] && [ "$${DEBUG_SLEEP-}" -gt 0 ] || DEBUG_SLEEP=600 ; export DEBUG_SLEEP ; \ LANG=C LC_ALL=C TZ=UTC \ - NUT_PORT=12345 NIT_CASE=testcase_sandbox_start_drivers_after_upsd NUT_FOREGROUND_WITH_PID=true \ + NUT_PORT=$(NUT_PORT) NIT_CASE="$(NIT_CASE)" NUT_FOREGROUND_WITH_PID=true \ $(MAKE) $(AM_MAKEFLAGS) check-NIT-devel SPELLCHECK_SRC = README.adoc diff --git a/tests/NIT/nit.sh b/tests/NIT/nit.sh index 0d04dd54e3..2da6495092 100755 --- a/tests/NIT/nit.sh +++ b/tests/NIT/nit.sh @@ -23,6 +23,8 @@ # TESTDIR=/tmp/nut-NIT to propose a location for "etc" and "run" # mock locations (under some circumstances, the # script can anyway choose to `mktemp` another) +# NUT_DEBUG_UPSMON_NOTIFY_SYSLOG=false To *disable* upsmon +# notifications spilling to the script's console. # # Common sandbox run for testing goes from NUT root build directory like: # DEBUG_SLEEP=600 NUT_PORT=12345 NIT_CASE=testcase_sandbox_start_drivers_after_upsd NUT_FOREGROUND_WITH_PID=true make check-NIT & @@ -571,12 +573,33 @@ updatecfg_upsmon_supplies() { generatecfg_upsmon_trivial() { # Populate the configs for the run rm -f "$NUT_STATEPATH/upsmon.sd.log" - ( echo 'MINSUPPLIES 0' > "$NUT_CONFPATH/upsmon.conf" || exit - echo 'SHUTDOWNCMD "echo \"`date`: TESTING_DUMMY_SHUTDOWN_NOW\" | tee \"'"$NUT_STATEPATH"'/upsmon.sd.log\" "' >> "$NUT_CONFPATH/upsmon.conf" || exit + ( echo 'MINSUPPLIES 0' > "$NUT_CONFPATH/upsmon.conf" || exit + echo 'SHUTDOWNCMD "echo \"`date`: TESTING_DUMMY_SHUTDOWN_NOW\" | tee \"'"$NUT_STATEPATH"'/upsmon.sd.log\" "' >> "$NUT_CONFPATH/upsmon.conf" || exit - if [ -n "${NUT_DEBUG_MIN-}" ] ; then - echo "DEBUG_MIN ${NUT_DEBUG_MIN}" >> "$NUT_CONFPATH/upsmon.conf" || exit - fi + NOTIFYTGT="" + if [ -x "${TOP_SRCDIR-}/scripts/misc/notifyme-debug" ] ; then + echo "NOTIFYCMD \"TEMPDIR='${NUT_STATEPATH}' ${TOP_SRCDIR-}/scripts/misc/notifyme-debug\"" >> "$NUT_CONFPATH/upsmon.conf" || exit + + # NOTE: "SYSLOG" typically ends up in console log of the NIT run and + # "EXEC" goes to a log file like tests/NIT/tmp/run/notifyme-399.log + NOTIFYTGT="EXEC" + fi + if [ x"$NUT_DEBUG_UPSMON_NOTIFY_SYSLOG" != xfalse ] ; then + [ -n "${NOTIFYTGT}" ] && NOTIFYTGT="${NOTIFYTGT}+SYSLOG" || NOTIFYTGT="SYSLOG" + fi + + if [ -n "${NOTIFYTGT}" ]; then + if [ -s "${TOP_SRCDIR-}/conf/upsmon.conf.sample.in" ] ; then + grep -E '# NOTIFYFLAG .*SYSLOG\+WALL$' \ + < "${TOP_SRCDIR-}/conf/upsmon.conf.sample.in" \ + | sed 's,^# \(NOTIFYFLAG[^A-Z_]*[A-Z_]*\)[^A-Z_]*SYSLOG.*$,\1\t'"${NOTIFYTGT}"',' \ + >> "$NUT_CONFPATH/upsmon.conf" || exit + fi + fi + + if [ -n "${NUT_DEBUG_MIN-}" ] ; then + echo "DEBUG_MIN ${NUT_DEBUG_MIN}" >> "$NUT_CONFPATH/upsmon.conf" || exit + fi ) || die "Failed to populate temporary FS structure for the NIT: upsmon.conf" if [ "`id -u`" = 0 ]; then @@ -1172,15 +1195,23 @@ testcase_sandbox_upsc_query_timer() { OUT2="`upsc dummy@localhost:$NUT_PORT ups.status`" || die "[testcase_sandbox_upsc_query_timer] upsd does not respond on port ${NUT_PORT} ($?): $OUT2" OUT3="" OUT4="" + OUT5="" + + # The SUT may be busy, or dummy-ups can have a loop delay + # (pollfreq) after reading the file before wrapping around if [ x"$OUT1" = x"$OUT2" ]; then sleep 3 OUT3="`upsc dummy@localhost:$NUT_PORT ups.status`" || die "[testcase_sandbox_upsc_query_timer] upsd does not respond on port ${NUT_PORT} ($?): $OUT3" if [ x"$OUT2" = x"$OUT3" ]; then sleep 3 OUT4="`upsc dummy@localhost:$NUT_PORT ups.status`" || die "[testcase_sandbox_upsc_query_timer] upsd does not respond on port ${NUT_PORT} ($?): $OUT4" + if [ x"$OUT3" = x"$OUT4" ]; then + sleep 8 + OUT5="`upsc dummy@localhost:$NUT_PORT ups.status`" || die "[testcase_sandbox_upsc_query_timer] upsd does not respond on port ${NUT_PORT} ($?): $OUT4" + fi fi fi - if echo "$OUT1$OUT2$OUT3$OUT4" | grep "OB" && echo "$OUT1$OUT2$OUT3$OUT4" | grep "OL" ; then + if echo "$OUT1$OUT2$OUT3$OUT4$OUT5" | grep "OB" && echo "$OUT1$OUT2$OUT3$OUT4$OUT5" | grep "OL" ; then log_info "[testcase_sandbox_upsc_query_timer] PASSED: ups.status flips over time" PASSED="`expr $PASSED + 1`" else @@ -1667,6 +1698,7 @@ if [ -n "${DEBUG_SLEEP-}" ] ; then log_info "You may want to press Ctrl+Z now and command 'bg' to the shell, if you did not start '$0 &' backgrounded already" log_info "To kill the script early, return it to foreground with 'fg' and press Ctrl+C, or 'kill -2 \$PID_NIT_SCRIPT' (kill -2 $$)" + log_info "To trace built programs, check scripts/valgrind/README.adoc and LD_LIBRARY_PATH for this build" log_info "Remember that in shell/scripting you can probe for '${NUT_CONFPATH}/NIT.env-sandbox-ready' which only appears at this moment" touch "${NUT_CONFPATH}/NIT.env-sandbox-ready" diff --git a/tools/nutconf/nutconf-cli.cpp b/tools/nutconf/nutconf-cli.cpp index a12b071eb5..f72aa7a7df 100644 --- a/tools/nutconf/nutconf-cli.cpp +++ b/tools/nutconf/nutconf-cli.cpp @@ -145,7 +145,7 @@ const char * Usage::s_text[] = { "Notification types:", " ONLINE, ONBATT, LOWBATT, FSD, COMMOK, COMMBAD, SHUTDOWN, REPLBATT, NOCOMM, NOPARENT,", " CAL, NOTCAL, OFF, NOTOFF, BYPASS, NOTBYPASS, ECO, NOTECO, ALARM, NOTALARM,", - " SUSPEND_STARTING, SUSPEND_FINISHED", + " OTHER, NOTOTHER, SUSPEND_STARTING, SUSPEND_FINISHED", "Notification flags:", " SYSLOG, WALL, EXEC, IGNORE", "User specification:",