From 67ff5e60e4de603e395f22b79450e3bff9d0ef6b Mon Sep 17 00:00:00 2001 From: Diogo Tridapalli Date: Sun, 20 Sep 2026 19:38:44 -0300 Subject: [PATCH 01/14] drivers/sms_ser.c: hex-dump sent and received frames at debug level 4 [#1987] The protocol is binary and the driver had no way to show what went over the wire. Use upsdebug_hex() for every request sent and every reply read, to help diagnose the models this driver meets in the field. Developed with Claude Code (model Claude Fable 5.1). Signed-off-by: Diogo Tridapalli --- drivers/sms_ser.c | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/drivers/sms_ser.c b/drivers/sms_ser.c index 8a942a4d24..7a372122e2 100644 --- a/drivers/sms_ser.c +++ b/drivers/sms_ser.c @@ -163,12 +163,14 @@ static int get_ups_nominal(void) { length = sms_prepare_get_status(&bufOut[0]); + upsdebug_hex(4, "sms_ser send", bufOut, length); if (ser_send_buf(upsfd, bufOut, length) == 0) { upsdebugx(LOG_ERR, "Communication error while writing to port"); return -1; } memset(bufIn, 0, BUFFER_SIZE); ret = ser_get_buf_len(upsfd, &bufIn[0], BUFFER_SIZE, 3, 1000); + upsdebug_hex(4, "sms_ser read", bufIn, ret > 0 ? (size_t)ret : 0); if (ret < RESULT_SIZE) { upslogx(LOG_ERR, "Short read from UPS"); @@ -195,12 +197,14 @@ static int get_ups_information(void) { length = sms_prepare_get_information(&bufOut[0]); + upsdebug_hex(4, "sms_ser send", bufOut, length); if (ser_send_buf(upsfd, bufOut, length) == 0) { upsdebugx(LOG_ERR, "Communication error while writing to port"); return -1; } memset(bufIn, 0, BUFFER_SIZE); ret = ser_get_buf_len(upsfd, &bufIn[0], BUFFER_SIZE, 3, 1000); + upsdebug_hex(4, "sms_ser read", bufIn, ret > 0 ? (size_t)ret : 0); if (ret < RESULT_SIZE) { upslogx(LOG_ERR, "Short read from UPS"); @@ -227,12 +231,14 @@ static int get_ups_features(void) { length = sms_prepare_get_features(&bufOut[0]); + upsdebug_hex(4, "sms_ser send", bufOut, length); if (ser_send_buf(upsfd, bufOut, length) == 0) { upsdebugx(LOG_ERR, "Communication error while writing to port"); return -1; } memset(bufIn, 0, BUFFER_SIZE); ret = ser_get_buf_len(upsfd, &bufIn[0], BUFFER_SIZE, 3, 1000); + upsdebug_hex(4, "sms_ser read", bufIn, ret > 0 ? (size_t)ret : 0); if (ret < RESULT_SIZE) { upslogx(LOG_ERR, "Short read from UPS"); @@ -262,6 +268,8 @@ static int sms_instcmd(const char *cmdname, const char *extra) { upslog_INSTCMD_POWERSTATE_MAYBE(cmdname, extra); length = sms_prepare_test_battery_nsec(&bufOut[0], delay); + upsdebug_hex(4, "sms_ser send", bufOut, length); + if (ser_send_buf(upsfd, bufOut, length) == 0) { upsdebugx(3, "failed to send test.battery.start"); return STAT_INSTCMD_FAILED; @@ -274,6 +282,8 @@ static int sms_instcmd(const char *cmdname, const char *extra) { upslog_INSTCMD_POWERSTATE_MAYBE(cmdname, extra); length = sms_prepare_test_battery_low(&bufOut[0]); + upsdebug_hex(4, "sms_ser send", bufOut, length); + if (ser_send_buf(upsfd, bufOut, length) == 0) { upsdebugx(3, "failed to send test.battery.start.quick"); return STAT_INSTCMD_FAILED; @@ -287,6 +297,8 @@ static int sms_instcmd(const char *cmdname, const char *extra) { upslog_INSTCMD_POWERSTATE_MAYBE(cmdname, extra); length = sms_prepare_cancel_test(&bufOut[0]); + upsdebug_hex(4, "sms_ser send", bufOut, length); + if (ser_send_buf(upsfd, bufOut, length) == 0) { upsdebugx(3, "failed to send test.battery.stop"); return STAT_INSTCMD_FAILED; @@ -299,6 +311,8 @@ static int sms_instcmd(const char *cmdname, const char *extra) { if (!strcasecmp(cmdname, "beeper.toggle")) { length = sms_prepare_set_beep(&bufOut[0]); + upsdebug_hex(4, "sms_ser send", bufOut, length); + if (ser_send_buf(upsfd, bufOut, length) == 0) { upsdebugx(3, "failed to send beeper.toggle"); return STAT_INSTCMD_FAILED; @@ -312,6 +326,8 @@ static int sms_instcmd(const char *cmdname, const char *extra) { upslog_INSTCMD_POWERSTATE_CHANGE(cmdname, extra); length = sms_prepare_shutdown_restore(&bufOut[0]); + upsdebug_hex(4, "sms_ser send", bufOut, length); + if (ser_send_buf(upsfd, bufOut, length) == 0) { upsdebugx(3, "failed to send shutdown.return"); return STAT_INSTCMD_FAILED; @@ -335,6 +351,8 @@ static int sms_instcmd(const char *cmdname, const char *extra) { upslog_INSTCMD_POWERSTATE_CHANGE(cmdname, extra); length = sms_prepare_shutdown_nsec(&bufOut[0], delay); + upsdebug_hex(4, "sms_ser send", bufOut, length); + if (ser_send_buf(upsfd, bufOut, length) == 0) { upsdebugx(3, "failed to send shutdown.reboot"); return STAT_INSTCMD_FAILED; @@ -348,6 +366,8 @@ static int sms_instcmd(const char *cmdname, const char *extra) { upslog_INSTCMD_POWERSTATE_MAYBE(cmdname, extra); length = sms_prepare_cancel_shutdown(&bufOut[0]); + upsdebug_hex(4, "sms_ser send", bufOut, length); + if (ser_send_buf(upsfd, bufOut, length) == 0) { upsdebugx(3, "failed to send shutdown.stop"); return STAT_INSTCMD_FAILED; From 1ce747c4259b58ee6a2d44485aac93b6ad0bd529 Mon Sep 17 00:00:00 2001 From: Diogo Tridapalli Date: Sun, 20 Sep 2026 19:38:44 -0300 Subject: [PATCH 02/14] drivers/sms_ser.c: keep the decimal digit of measured values [#1987] The UPS reports tenths (e.g. 0x04ff for 127.9 V), but the long was divided by 10 before becoming a double, so NUT always showed 127.00, 59.00 etc. Developed with Claude Code (model Claude Fable 5.1). Signed-off-by: Diogo Tridapalli --- drivers/sms_ser.c | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/drivers/sms_ser.c b/drivers/sms_ser.c index 7a372122e2..143e8afe82 100644 --- a/drivers/sms_ser.c +++ b/drivers/sms_ser.c @@ -103,43 +103,43 @@ void sms_parse_results(uint8_t *rawvalues, SmsData *results) { memset(buf, 0, BUFFER_SIZE); snprintf(buf, sizeof(buf), "0x%02x%02x", (unsigned char)rawvalues[1], (unsigned char)rawvalues[2]); v = strtol(buf, NULL, 16); /* 16 == hex */ - h = v / 10; + h = (double)v / 10; results->lastinputVac = h; memset(buf, 0, BUFFER_SIZE); snprintf(buf, sizeof(buf), "0x%02x%02x", (unsigned char)rawvalues[3], (unsigned char)rawvalues[4]); v = strtol(buf, NULL, 16); /* 16 == hex */ - h = v / 10; + h = (double)v / 10; results->inputVac = h; memset(buf, 0, BUFFER_SIZE); snprintf(buf, sizeof(buf), "0x%02x%02x", (unsigned char)rawvalues[5], (unsigned char)rawvalues[6]); v = strtol(buf, NULL, 16); /* 16 == hex */ - h = v / 10; + h = (double)v / 10; results->outputVac = h; memset(buf, 0, BUFFER_SIZE); snprintf(buf, sizeof(buf), "0x%02x%02x", (unsigned char)rawvalues[7], (unsigned char)rawvalues[8]); v = strtol(buf, NULL, 16); /* 16 == hex */ - h = v / 10; + h = (double)v / 10; results->outputpower = h; memset(buf, 0, BUFFER_SIZE); snprintf(buf, sizeof(buf), "0x%02x%02x", (unsigned char)rawvalues[9], (unsigned char)rawvalues[10]); v = strtol(buf, NULL, 16); /* 16 == hex */ - h = v / 10; + h = (double)v / 10; results->outputHz = h; memset(buf, 0, BUFFER_SIZE); snprintf(buf, sizeof(buf), "0x%02x%02x", (unsigned char)rawvalues[11], (unsigned char)rawvalues[12]); v = strtol(buf, NULL, 16); /* 16 == hex */ - h = v / 10; + h = (double)v / 10; results->batterylevel = h; memset(buf, 0, BUFFER_SIZE); snprintf(buf, sizeof(buf), "0x%02x%02x", (unsigned char)rawvalues[13], (unsigned char)rawvalues[14]); v = strtol(buf, NULL, 16); /* 16 == hex */ - h = v / 10; + h = (double)v / 10; results->temperatureC = h; byte = rawvalues[15]; From f1dd194cef5ba65ec7ab0df648ca972f19a6bfb7 Mon Sep 17 00:00:00 2001 From: Diogo Tridapalli Date: Sun, 20 Sep 2026 19:38:44 -0300 Subject: [PATCH 03/14] drivers/sms_ser.c, sms_ser.h: build ups.status from all flags, ignore fake BYPASS of line-interactive models [#1987] The status was picked by an exclusive if/else chain that tested the bypass flag first. Newer line-interactive models (SMS Premium, Gamer) raise that flag whenever the inverter runs, so an outage was reported as BYPASS and neither OB nor LB ever reached upsmon. The vendor confirmed to a user in issue #1987 that these models have no bypass and the flag is not valid. The first byte of the 'Q' reply tells the topology ('=' line-interactive, '>' and '<' on-line types); the vendor's PowerView software uses it the same way to decide whether to believe the bypass flag. Store it, and: * always report OL or OB, plus LB, RB and BOOST as independent flags; * report BYPASS only for the on-line types and never while on battery; * report CAL and ups.test.result while the battery test flag is set, so upsmon can tell a test from an outage; * also raise LB while on battery when the charge is at or below battery.charge.low, if the user defined it (the vendor software derives low battery from a threshold too); * publish ups.mfr, ups.model and device.mfr (some clients, e.g. Synology DSM, refuse a server without ups.mfr and ups.model). The unreachable OFF branch is gone with the chain. Developed with Claude Code (model Claude Fable 5.1). Signed-off-by: Diogo Tridapalli --- drivers/sms_ser.c | 72 +++++++++++++++++++++++++++++++++-------------- drivers/sms_ser.h | 12 ++++++-- 2 files changed, 60 insertions(+), 24 deletions(-) diff --git a/drivers/sms_ser.c b/drivers/sms_ser.c index 143e8afe82..a0ab9b1634 100644 --- a/drivers/sms_ser.c +++ b/drivers/sms_ser.c @@ -100,6 +100,8 @@ void sms_parse_results(uint8_t *rawvalues, SmsData *results) { long v; double h; + results->upstype = (char)rawvalues[0]; + memset(buf, 0, BUFFER_SIZE); snprintf(buf, sizeof(buf), "0x%02x%02x", (unsigned char)rawvalues[1], (unsigned char)rawvalues[2]); v = strtol(buf, NULL, 16); /* 16 == hex */ @@ -467,6 +469,9 @@ void upsdrv_initinfo(void) { void upsdrv_updateinfo(void) { char *battery_status; + const char *charge_low; + bool lowbattery; + static bool test_seen = false; upsdebugx(LOG_DEBUG, "upsdrv_updateinfo"); @@ -475,6 +480,9 @@ void upsdrv_updateinfo(void) { dstate_datastale(); return; } + dstate_setinfo("device.mfr", "%s", "SMS"); + dstate_setinfo("ups.mfr", "%s", "SMS"); + dstate_setinfo("ups.model", "%s", DeviceData.model); dstate_setinfo("device.model", "%s", DeviceData.model); dstate_setinfo("ups.firmware", "%s", DeviceData.version); dstate_setinfo("battery.voltage.nominal", "%s", DeviceData.voltageRange); @@ -495,8 +503,11 @@ void upsdrv_updateinfo(void) { dstate_setinfo("ups.temperature", "%.2f", DeviceData.temperatureC); upsdebugx(LOG_DEBUG, "battery level: %.2f", DeviceData.batterylevel); + upsdebugx(LOG_DEBUG, "type: %c", DeviceData.upstype); upsdebugx(LOG_DEBUG, "bypass: %d", DeviceData.bypass); upsdebugx(LOG_DEBUG, "onBattery: %d", DeviceData.onbattery); + upsdebugx(LOG_DEBUG, "lowBattery: %d", DeviceData.lowbattery); + upsdebugx(LOG_DEBUG, "test: %d", DeviceData.test); if (DeviceData.onbattery && (uint8_t)DeviceData.batterylevel < 100) { upsdebugx(LOG_DEBUG, "on battery and battery < last battery"); @@ -513,35 +524,54 @@ void upsdrv_updateinfo(void) { } dstate_setinfo("battery.charger.status", "%s", battery_status); + /* The UPS has a low battery flag of its own; like SMS PowerView, also + * accept a charge threshold (e.g. "default.battery.charge.low" in + * ups.conf). The charge figure only means something while on battery: + * on mains it is a recharge ramp that restarts near 20% after any + * battery use. */ + lowbattery = DeviceData.lowbattery; + charge_low = dstate_getinfo("battery.charge.low"); + if (!lowbattery && DeviceData.onbattery && charge_low + && DeviceData.batterylevel <= strtod(charge_low, NULL)) { + upsdebugx(LOG_DEBUG, "on battery and charge <= battery.charge.low (%s)", charge_low); + lowbattery = true; + } + + /* The flags are independent of each other, so are the NUT statuses */ status_init(); - if (DeviceData.bypass) { - upsdebugx(LOG_DEBUG, "setting status to BYPASS"); - status_set("BYPASS"); - } else if (DeviceData.onbattery) { - upsdebugx(LOG_DEBUG, "setting status to OB"); - status_set("OB"); - } else if (DeviceData.lowbattery) { - upsdebugx(LOG_DEBUG, "setting status to LB"); + status_set(DeviceData.onbattery ? "OB" : "OL"); + + if (lowbattery) { status_set("LB"); - } else if (!DeviceData.upsok) { - upsdebugx(LOG_DEBUG, "setting status to RB"); + } + if (DeviceData.test) { + /* A battery test runs on battery with mains present; CAL lets + * upsmon tell it from an outage */ + status_set("CAL"); + } + if (!DeviceData.upsok) { status_set("RB"); - } else if (DeviceData.boost) { - upsdebugx(LOG_DEBUG, "setting status to BOOST"); + } + if (DeviceData.boost) { status_set("BOOST"); - } else if (!DeviceData.onbattery) { - /* sometimes the flag "onacpower" is not set */ - upsdebugx(LOG_DEBUG, "setting status to OL"); - status_set("OL"); - } else { - /* None of these parameters is ON, but we got some response, - * so the device is (administratively) OFF ? */ - upsdebugx(LOG_DEBUG, "setting status to OFF"); - status_set("OFF"); + } + /* Line-interactive models have no bypass and raise this flag whenever + * the inverter runs (on battery, battery test); SMS PowerView ignores + * it for them too. On battery it could never be true anyway. */ + if (DeviceData.bypass && !DeviceData.onbattery + && DeviceData.upstype != SMS_TYPE_LINE_INTERACTIVE) { + status_set("BYPASS"); } status_commit(); + + if (DeviceData.test) { + dstate_setinfo("ups.test.result", "%s", "In progress"); + test_seen = true; + } else if (test_seen) { + dstate_setinfo("ups.test.result", "%s", "Done"); + } dstate_dataok(); poll_interval = 5; diff --git a/drivers/sms_ser.h b/drivers/sms_ser.h index 282ada7f1e..ea6c3acb04 100644 --- a/drivers/sms_ser.h +++ b/drivers/sms_ser.h @@ -30,7 +30,13 @@ #define DEFAULT_BOOTDELAY 64 /* seconds (max 0xFF) */ #define MAXTRIES 3 +/* First byte of the 'Q' reply: the UPS topology, as decoded by SMS PowerView */ +#define SMS_TYPE_LINE_INTERACTIVE '=' /* "UPS Line Interative" */ +#define SMS_TYPE_ONLINE_LINE_INTERACTIVE '>' /* "On Line Interative" */ +#define SMS_TYPE_ONLINE '<' /* "UPS On Line" */ + typedef struct { + char upstype; /* one of SMS_TYPE_*, only the on-line types have a real bypass */ char model[25]; /* device.model */ char version[7]; /* ups.firmware */ char voltageRange[15]; /* garbage from sms (it's a string with some strange items) */ @@ -40,11 +46,11 @@ typedef struct { bool beepon; /* ups.beeper.status */ bool shutdown; /* ups.status = FSD (the shutdown has started by another via) */ - bool test; /* the UPS is testing the battery, need a status ? */ + bool test; /* the UPS is testing the battery: ups.status = CAL + ups.test.result */ bool upsok; /* ups.status or battery.status ? (Maybe RB if is False ?) */ bool boost; /* ups.status = BOOST */ - bool bypass; /* ups.status = BYPASS */ - bool lowbattery; /* ups.status = LB (OL + LB or OB + LB ?) */ + bool bypass; /* ups.status = BYPASS, but line-interactive models set it whenever the inverter runs */ + bool lowbattery; /* ups.status = LB */ bool onbattery; /* ups.status = OB + battery.charger.status = discharging */ float lastinputVac; /* garbage ? always 000 */ From c9ba9063860b379fcdbead42e58513f9bbb9a2b7 Mon Sep 17 00:00:00 2001 From: Diogo Tridapalli Date: Sun, 20 Sep 2026 19:38:44 -0300 Subject: [PATCH 04/14] drivers/sms_ser.c: validate replies, reopen the port after repeated failures, add output.voltage.nominal and ups.type [#1987] Move the send/receive sequence of the three queries into sms_query(), which flushes stale input, checks that the whole request was written (a return of -1 was not caught before), and rejects replies whose checksum or terminator is wrong. The checksum rule is the one used for requests and was checked against replies of three different models. After MAXTRIES failed polls close the port and reopen it by name without bailing out: a USB-serial adapter that was unplugged leaves a dead descriptor and may return under another device node. Publish ups.type from the topology byte and output.voltage.nominal from the number in the voltage range label. Drop a stray write of that label into battery.voltage.nominal, which was overwritten a few lines later. Developed with Claude Code (model Claude Fable 5.1). Signed-off-by: Diogo Tridapalli --- drivers/sms_ser.c | 123 +++++++++++++++++++++++++++++++++------------- 1 file changed, 88 insertions(+), 35 deletions(-) diff --git a/drivers/sms_ser.c b/drivers/sms_ser.c index a0ab9b1634..728d1f43ee 100644 --- a/drivers/sms_ser.c +++ b/drivers/sms_ser.c @@ -42,6 +42,7 @@ static uint16_t bootdelay = DEFAULT_BOOTDELAY; static uint8_t bufOut[BUFFER_SIZE]; static uint8_t bufIn[BUFFER_SIZE]; static SmsData DeviceData; +static int comm_failures = 0; /* driver description structure */ upsdrv_info_t upsdrv_info = { @@ -157,21 +158,26 @@ void sms_parse_results(uint8_t *rawvalues, SmsData *results) { results->onbattery = ((byte & (mask << 7)) != 0) ? true : false; } -static int get_ups_nominal(void) { - uint8_t length; +/* Sends the request prepared in bufOut and reads the reply into bufIn. + * Every reply is RESULT_SIZE bytes: a header byte, 15 bytes of payload, a + * checksum (the sum of all bytes up to and including it is 0 modulo 256) + * and ENDCHAR. Returns the reply length, or -1 after logging the reason. */ +static ssize_t sms_query(uint8_t length) { ssize_t ret; + uint8_t sum = 0; + size_t i; - upsdebugx(LOG_DEBUG, "get_ups_nominal"); - - length = sms_prepare_get_status(&bufOut[0]); + /* Drop whatever is left of an earlier, late or partial reply */ + ser_flush_in(upsfd, "", 0); upsdebug_hex(4, "sms_ser send", bufOut, length); - if (ser_send_buf(upsfd, bufOut, length) == 0) { - upsdebugx(LOG_ERR, "Communication error while writing to port"); + if (ser_send_buf(upsfd, bufOut, length) != (ssize_t)length) { + upslogx(LOG_ERR, "Communication error while writing to port"); + dstate_datastale(); return -1; } - memset(bufIn, 0, BUFFER_SIZE); - ret = ser_get_buf_len(upsfd, &bufIn[0], BUFFER_SIZE, 3, 1000); + memset(bufIn, 0, sizeof(bufIn)); + ret = ser_get_buf_len(upsfd, &bufIn[0], sizeof(bufIn), 3, 1000); upsdebug_hex(4, "sms_ser read", bufIn, ret > 0 ? (size_t)ret : 0); if (ret < RESULT_SIZE) { @@ -180,6 +186,47 @@ static int get_ups_nominal(void) { return -1; } + for (i = 0; i < RESULT_SIZE - 1; i++) { + sum += bufIn[i]; + } + if (sum != 0 || bufIn[RESULT_SIZE - 1] != ENDCHAR) { + upslogx(LOG_ERR, "Bad checksum or terminator in reply from UPS"); + dstate_datastale(); + return -1; + } + + return ret; +} + +/* Closes and reopens the serial port without giving up if that fails: + * the next polls will try again. */ +static void sms_reconnect(void) { + upslogx(LOG_WARNING, "No valid reply from UPS, reopening %s", device_path); + + if (VALID_FD_SER(upsfd)) { + ser_close(upsfd, device_path); + } + upsfd = ser_open_nf(device_path); + if (INVALID_FD_SER(upsfd)) { + upslogx(LOG_ERR, "Could not reopen %s, will retry", device_path); + return; + } + ser_set_speed_nf(upsfd, device_path, B2400); +} + +static int get_ups_nominal(void) { + uint8_t length; + ssize_t ret; + + upsdebugx(LOG_DEBUG, "get_ups_nominal"); + + length = sms_prepare_get_status(&bufOut[0]); + + ret = sms_query(length); + if (ret < 0) { + return -1; + } + upsdebugx(3, "Get nominal Ok: received byte %" PRIiSIZE, ret); if (bufIn[0] == '=' || bufIn[0] == '<' || bufIn[0] == '>') { @@ -199,18 +246,8 @@ static int get_ups_information(void) { length = sms_prepare_get_information(&bufOut[0]); - upsdebug_hex(4, "sms_ser send", bufOut, length); - if (ser_send_buf(upsfd, bufOut, length) == 0) { - upsdebugx(LOG_ERR, "Communication error while writing to port"); - return -1; - } - memset(bufIn, 0, BUFFER_SIZE); - ret = ser_get_buf_len(upsfd, &bufIn[0], BUFFER_SIZE, 3, 1000); - upsdebug_hex(4, "sms_ser read", bufIn, ret > 0 ? (size_t)ret : 0); - - if (ret < RESULT_SIZE) { - upslogx(LOG_ERR, "Short read from UPS"); - dstate_datastale(); + ret = sms_query(length); + if (ret < 0) { return -1; } @@ -233,18 +270,8 @@ static int get_ups_features(void) { length = sms_prepare_get_features(&bufOut[0]); - upsdebug_hex(4, "sms_ser send", bufOut, length); - if (ser_send_buf(upsfd, bufOut, length) == 0) { - upsdebugx(LOG_ERR, "Communication error while writing to port"); - return -1; - } - memset(bufIn, 0, BUFFER_SIZE); - ret = ser_get_buf_len(upsfd, &bufIn[0], BUFFER_SIZE, 3, 1000); - upsdebug_hex(4, "sms_ser read", bufIn, ret > 0 ? (size_t)ret : 0); - - if (ret < RESULT_SIZE) { - upslogx(LOG_ERR, "Short read from UPS"); - dstate_datastale(); + ret = sms_query(length); + if (ret < 0) { return -1; } @@ -400,6 +427,7 @@ static int sms_setvar(const char *varname, const char *val) { void upsdrv_initinfo(void) { char *battery_status; + const char *range_digits; upsdebugx(LOG_DEBUG, "upsdrv_initinfo"); @@ -419,6 +447,26 @@ void upsdrv_initinfo(void) { dstate_setinfo("ups.firmware", "%s", DeviceData.version); dstate_setinfo("input.voltage.nominal", "%s", DeviceData.voltageRange); dstate_setinfo("input.current.nominal", "%s", DeviceData.currentRange); + /* The voltage range is a label like "EBiS115" or "EBiS220" whose + * number matches the output voltage of the units seen so far */ + range_digits = DeviceData.voltageRange + strcspn(DeviceData.voltageRange, "0123456789"); + if (*range_digits) { + dstate_setinfo("output.voltage.nominal", "%d", atoi(range_digits)); + } + + switch (DeviceData.upstype) { + case SMS_TYPE_LINE_INTERACTIVE: + dstate_setinfo("ups.type", "%s", "line-interactive"); + break; + case SMS_TYPE_ONLINE_LINE_INTERACTIVE: + dstate_setinfo("ups.type", "%s", "online line-interactive"); + break; + case SMS_TYPE_ONLINE: + dstate_setinfo("ups.type", "%s", "online"); + break; + default: + break; + } dstate_setinfo("output.frequency.nominal", "%d", DeviceData.frequency); dstate_setinfo("ups.beeper.status", "%s", (DeviceData.beepon == 1) ? "enabled" : "disabled"); @@ -476,16 +524,21 @@ void upsdrv_updateinfo(void) { upsdebugx(LOG_DEBUG, "upsdrv_updateinfo"); if (get_ups_nominal() != 0) { - upslogx(LOG_ERR, "Short read from UPS"); dstate_datastale(); + /* A USB adapter that was unplugged leaves a dead descriptor behind, + * and may come back under another device node: reopen by name */ + if (++comm_failures >= MAXTRIES) { + sms_reconnect(); + comm_failures = 0; + } return; } + comm_failures = 0; dstate_setinfo("device.mfr", "%s", "SMS"); dstate_setinfo("ups.mfr", "%s", "SMS"); dstate_setinfo("ups.model", "%s", DeviceData.model); dstate_setinfo("device.model", "%s", DeviceData.model); dstate_setinfo("ups.firmware", "%s", DeviceData.version); - dstate_setinfo("battery.voltage.nominal", "%s", DeviceData.voltageRange); dstate_setinfo("output.frequency.nominal", "%d", DeviceData.frequency); dstate_setinfo("ups.beeper.status", "%s", (DeviceData.beepon == 1) ? "enabled" : "disabled"); From d59cab40d70faa88223b6a944da7a87f7eb2024e Mon Sep 17 00:00:00 2001 From: Diogo Tridapalli Date: Sun, 20 Sep 2026 19:38:45 -0300 Subject: [PATCH 05/14] drivers/sms_ser.c, sms_ser.h: send delays as 16-bit values, add delay settings and test.battery.start.deep, bump version [#1987] According to the vendor's PowerView software, the 'S', 'T' and 'R' commands carry 16-bit big-endian values with 0x00 in the unused pair. The driver put the low byte of the delay in the first parameter and 0xFF in the others (a delay of 64 went out as 0x40FF), and sent 'R' with four 0xFF bytes. * shutdown.return (and so upsdrv_shutdown) now sends the new offdelay and ondelay settings, also writable as ups.delay.shutdown and ups.delay.start; ups.delay.reboot is now published too; * delays given in ups.conf, by setvar or as a command argument are checked to be numbers that fit 16 bits; the argument of shutdown.reboot was parsed with the delay as the numeric base; * test.battery.start.quick sent 'L' (test until battery low): it now runs the timed test for 10 seconds, and the new test.battery.start.deep sends 'L'; test.battery.start still takes an optional duration; * one helper sends a prepared command and checks the full write. Not verified on hardware: the unit and effect of the delay values on the UPS side; frames were checked against a protocol simulator only. Developed with Claude Code (model Claude Fable 5.1). Signed-off-by: Diogo Tridapalli --- drivers/sms_ser.c | 225 +++++++++++++++++++++++----------------------- drivers/sms_ser.h | 7 +- 2 files changed, 118 insertions(+), 114 deletions(-) diff --git a/drivers/sms_ser.c b/drivers/sms_ser.c index 728d1f43ee..cfad6eba1d 100644 --- a/drivers/sms_ser.c +++ b/drivers/sms_ser.c @@ -31,7 +31,7 @@ #define ENDCHAR '\r' #define DRIVER_NAME "SMS Brazil UPS driver" -#define DRIVER_VERSION "1.05" +#define DRIVER_VERSION "1.06" #define QUERY_SIZE 7 #define BUFFER_SIZE 18 @@ -39,6 +39,8 @@ #define HUMAN_VALUES 7 static uint16_t bootdelay = DEFAULT_BOOTDELAY; +static uint16_t offdelay = DEFAULT_OFFDELAY; +static uint16_t ondelay = DEFAULT_ONDELAY; static uint8_t bufOut[BUFFER_SIZE]; static uint8_t bufIn[BUFFER_SIZE]; static SmsData DeviceData; @@ -286,124 +288,88 @@ static int get_ups_features(void) { return -1; } -static int sms_instcmd(const char *cmdname, const char *extra) { - size_t length; +/* Parses a delay given as text; returns 0 and leaves *delay alone unless it + * is a number that fits the 16 bits the protocol has for it */ +static int sms_parse_delay(const char *text, uint16_t *delay) { + char *end = NULL; + long value; - upsdebug_INSTCMD_STARTING(cmdname, extra); + if (!text || !*text) { + return 0; + } + value = strtol(text, &end, 10); + if (*end != '\0' || value < 0 || value > UINT16_MAX) { + return 0; + } + *delay = (uint16_t)value; + return 1; +} - if (!strcasecmp(cmdname, "test.battery.start")) { - long delay = extra ? strtol(extra, NULL, 10) : 10; +/* Sends the command prepared in bufOut. Commands are not acknowledged. */ +static int sms_send_command(size_t length, const char *cmdname) { + upsdebug_hex(4, "sms_ser send", bufOut, length); + if (ser_send_buf(upsfd, bufOut, length) != (ssize_t)length) { + upslogx(LOG_ERR, "failed to send %s", cmdname); + return STAT_INSTCMD_FAILED; + } + upsdebugx(3, "command %s OK!", cmdname); + return STAT_INSTCMD_HANDLED; +} - upslog_INSTCMD_POWERSTATE_MAYBE(cmdname, extra); - length = sms_prepare_test_battery_nsec(&bufOut[0], delay); +static int sms_instcmd(const char *cmdname, const char *extra) { + uint16_t delay; - upsdebug_hex(4, "sms_ser send", bufOut, length); + upsdebug_INSTCMD_STARTING(cmdname, extra); - if (ser_send_buf(upsfd, bufOut, length) == 0) { - upsdebugx(3, "failed to send test.battery.start"); - return STAT_INSTCMD_FAILED; + if (!strcasecmp(cmdname, "test.battery.start")) { + /* Test for the number of seconds given, or the quick test's */ + delay = DEFAULT_TESTDELAY; + if (extra && !sms_parse_delay(extra, &delay)) { + upslogx(LOG_ERR, "%s: invalid duration [%s]", cmdname, extra); + return STAT_INSTCMD_CONVERSION_FAILED; } - upsdebugx(3, "command test.battery.start OK!"); - return STAT_INSTCMD_HANDLED; + upslog_INSTCMD_POWERSTATE_MAYBE(cmdname, extra); + return sms_send_command(sms_prepare_test_battery_nsec(&bufOut[0], delay), cmdname); } if (!strcasecmp(cmdname, "test.battery.start.quick")) { upslog_INSTCMD_POWERSTATE_MAYBE(cmdname, extra); - length = sms_prepare_test_battery_low(&bufOut[0]); - - upsdebug_hex(4, "sms_ser send", bufOut, length); - - if (ser_send_buf(upsfd, bufOut, length) == 0) { - upsdebugx(3, "failed to send test.battery.start.quick"); - return STAT_INSTCMD_FAILED; - } - upsdebugx(3, "command test.battery.start.quick OK!"); + return sms_send_command(sms_prepare_test_battery_nsec(&bufOut[0], DEFAULT_TESTDELAY), cmdname); + } - return STAT_INSTCMD_HANDLED; + if (!strcasecmp(cmdname, "test.battery.start.deep")) { + upslog_INSTCMD_POWERSTATE_MAYBE(cmdname, extra); + /* Runs until the UPS reports a low battery */ + return sms_send_command(sms_prepare_test_battery_low(&bufOut[0]), cmdname); } if (!strcasecmp(cmdname, "test.battery.stop")) { upslog_INSTCMD_POWERSTATE_MAYBE(cmdname, extra); - length = sms_prepare_cancel_test(&bufOut[0]); - - upsdebug_hex(4, "sms_ser send", bufOut, length); - - if (ser_send_buf(upsfd, bufOut, length) == 0) { - upsdebugx(3, "failed to send test.battery.stop"); - return STAT_INSTCMD_FAILED; - } - upsdebugx(3, "command test.battery.stop OK!"); - - return STAT_INSTCMD_HANDLED; + return sms_send_command(sms_prepare_cancel_test(&bufOut[0]), cmdname); } if (!strcasecmp(cmdname, "beeper.toggle")) { - length = sms_prepare_set_beep(&bufOut[0]); - - upsdebug_hex(4, "sms_ser send", bufOut, length); - - if (ser_send_buf(upsfd, bufOut, length) == 0) { - upsdebugx(3, "failed to send beeper.toggle"); - return STAT_INSTCMD_FAILED; - } - upsdebugx(3, "command beeper.toggle OK!"); - - return STAT_INSTCMD_HANDLED; + return sms_send_command(sms_prepare_set_beep(&bufOut[0]), cmdname); } if (!strcasecmp(cmdname, "shutdown.return")) { upslog_INSTCMD_POWERSTATE_CHANGE(cmdname, extra); - length = sms_prepare_shutdown_restore(&bufOut[0]); - - upsdebug_hex(4, "sms_ser send", bufOut, length); - - if (ser_send_buf(upsfd, bufOut, length) == 0) { - upsdebugx(3, "failed to send shutdown.return"); - return STAT_INSTCMD_FAILED; - } - upsdebugx(3, "command shutdown.return OK!"); - - return STAT_INSTCMD_HANDLED; + return sms_send_command(sms_prepare_shutdown_restore(&bufOut[0], offdelay, ondelay), cmdname); } if (!strcasecmp(cmdname, "shutdown.reboot")) { - uint16_t delay = bootdelay; - if (extra) { - long ldelay = strtol(extra, NULL, bootdelay); - if (ldelay >= 0 && (intmax_t)ldelay < (intmax_t)UINT16_MAX) { - delay = (uint16_t)ldelay; - } else { - upsdebugx(3, "tried to set up extra shutdown.reboot delay ut it was out of range, keeping default"); - } + delay = bootdelay; + if (extra && !sms_parse_delay(extra, &delay)) { + upslogx(LOG_ERR, "%s: invalid delay [%s]", cmdname, extra); + return STAT_INSTCMD_CONVERSION_FAILED; } - upslog_INSTCMD_POWERSTATE_CHANGE(cmdname, extra); - length = sms_prepare_shutdown_nsec(&bufOut[0], delay); - - upsdebug_hex(4, "sms_ser send", bufOut, length); - - if (ser_send_buf(upsfd, bufOut, length) == 0) { - upsdebugx(3, "failed to send shutdown.reboot"); - return STAT_INSTCMD_FAILED; - } - upsdebugx(3, "command shutdown.reboot OK!"); - - return STAT_INSTCMD_HANDLED; + return sms_send_command(sms_prepare_shutdown_nsec(&bufOut[0], delay), cmdname); } if (!strcasecmp(cmdname, "shutdown.stop")) { upslog_INSTCMD_POWERSTATE_MAYBE(cmdname, extra); - length = sms_prepare_cancel_shutdown(&bufOut[0]); - - upsdebug_hex(4, "sms_ser send", bufOut, length); - - if (ser_send_buf(upsfd, bufOut, length) == 0) { - upsdebugx(3, "failed to send shutdown.stop"); - return STAT_INSTCMD_FAILED; - } - upsdebugx(3, "command shutdown.stop OK!"); - - return STAT_INSTCMD_HANDLED; + return sms_send_command(sms_prepare_cancel_shutdown(&bufOut[0]), cmdname); } upslog_INSTCMD_UNKNOWN(cmdname, extra); @@ -411,18 +377,32 @@ static int sms_instcmd(const char *cmdname, const char *extra) { } static int sms_setvar(const char *varname, const char *val) { + uint16_t *target = NULL; + upsdebug_SET_STARTING(varname, val); if (!strcasecmp(varname, "ups.delay.reboot")) { - int ipv = atoi(val); - if (ipv >= 0) - bootdelay = (unsigned int)ipv; - dstate_setinfo("ups.delay.reboot", "%u", bootdelay); - return STAT_SET_HANDLED; + target = &bootdelay; + } else if (!strcasecmp(varname, "ups.delay.shutdown")) { + target = &offdelay; + } else if (!strcasecmp(varname, "ups.delay.start")) { + target = &ondelay; + } else { + upslog_SET_UNKNOWN(varname, val); + return STAT_SET_UNKNOWN; + } + + if (!sms_parse_delay(val, target)) { + return STAT_SET_CONVERSION_FAILED; } + dstate_setinfo(varname, "%u", *target); + return STAT_SET_HANDLED; +} - upslog_SET_UNKNOWN(varname, val); - return STAT_SET_UNKNOWN; +/* Publishes a delay as a writable number */ +static void sms_publish_delay(const char *varname, uint16_t value) { + dstate_setinfo(varname, "%u", value); + dstate_setflags(varname, ST_FLAG_RW | ST_FLAG_NUMBER); } void upsdrv_initinfo(void) { @@ -505,12 +485,17 @@ void upsdrv_initinfo(void) { dstate_addcmd("test.battery.start"); dstate_addcmd("test.battery.start.quick"); + dstate_addcmd("test.battery.start.deep"); dstate_addcmd("test.battery.stop"); dstate_addcmd("beeper.toggle"); dstate_addcmd("shutdown.return"); dstate_addcmd("shutdown.stop"); dstate_addcmd("shutdown.reboot"); + sms_publish_delay("ups.delay.shutdown", offdelay); + sms_publish_delay("ups.delay.start", ondelay); + sms_publish_delay("ups.delay.reboot", bootdelay); + upsh.instcmd = sms_instcmd; upsh.setvar = sms_setvar; } @@ -689,6 +674,14 @@ void upsdrv_makevartable(void) { snprintf(msg, sizeof msg, "Set reboot delay, in seconds (default=%d).", DEFAULT_BOOTDELAY); addvar(VAR_VALUE, "rebootdelay", msg); + + snprintf(msg, sizeof msg, "Set shutdown delay for shutdown.return, in seconds (default=%d).", + DEFAULT_OFFDELAY); + addvar(VAR_VALUE, "offdelay", msg); + + snprintf(msg, sizeof msg, "Set delay before the output returns after shutdown.return (default=%d).", + DEFAULT_ONDELAY); + addvar(VAR_VALUE, "ondelay", msg); } void upsdrv_initups(void) { @@ -699,10 +692,14 @@ void upsdrv_initups(void) { upsfd = ser_open(device_path); ser_set_speed(upsfd, device_path, B2400); - if ((val = getval("rebootdelay"))) { - int ipv = atoi(val); - if (ipv >= 0) - bootdelay = (unsigned int)ipv; + if ((val = getval("rebootdelay")) && !sms_parse_delay(val, &bootdelay)) { + fatalx(EXIT_FAILURE, "Invalid rebootdelay [%s]", val); + } + if ((val = getval("offdelay")) && !sms_parse_delay(val, &offdelay)) { + fatalx(EXIT_FAILURE, "Invalid offdelay [%s]", val); + } + if ((val = getval("ondelay")) && !sms_parse_delay(val, &ondelay)) { + fatalx(EXIT_FAILURE, "Invalid ondelay [%s]", val); } } @@ -773,11 +770,12 @@ uint8_t sms_prepare_test_battery_low(uint8_t *buffer) { } uint8_t sms_prepare_test_battery_nsec(uint8_t *buffer, uint16_t delay) { + /* The delay is a 16-bit big-endian value, the unused pair is zero */ buffer[0] = 'T'; - buffer[1] = (uint8_t)(delay % 256); - buffer[2] = 255; - buffer[3] = 255; - buffer[4] = 255; + buffer[1] = (uint8_t)(delay >> 8); + buffer[2] = (uint8_t)(delay & 0xFF); + buffer[3] = 0; + buffer[4] = 0; buffer[5] = (buffer[0] + buffer[1] + buffer[2] + buffer[3] + buffer[4]) * 255; buffer[6] = ENDCHAR; @@ -785,23 +783,26 @@ uint8_t sms_prepare_test_battery_nsec(uint8_t *buffer, uint16_t delay) { } uint8_t sms_prepare_shutdown_nsec(uint8_t *buffer, uint16_t delay) { + /* The delay is a 16-bit big-endian value, the unused pair is zero */ buffer[0] = 'S'; - buffer[1] = (uint8_t)(delay % 256); - buffer[2] = 255; - buffer[3] = 255; - buffer[4] = 255; + buffer[1] = (uint8_t)(delay >> 8); + buffer[2] = (uint8_t)(delay & 0xFF); + buffer[3] = 0; + buffer[4] = 0; buffer[5] = (buffer[0] + buffer[1] + buffer[2] + buffer[3] + buffer[4]) * 255; buffer[6] = ENDCHAR; return 7; } -uint8_t sms_prepare_shutdown_restore(uint8_t *buffer) { +uint8_t sms_prepare_shutdown_restore(uint8_t *buffer, uint16_t shutdown_delay, uint16_t restore_delay) { + /* Two 16-bit big-endian values: delay before the output is cut, then + * delay before it returns once mains is back */ buffer[0] = 'R'; - buffer[1] = 255; - buffer[2] = 255; - buffer[3] = 255; - buffer[4] = 255; + buffer[1] = (uint8_t)(shutdown_delay >> 8); + buffer[2] = (uint8_t)(shutdown_delay & 0xFF); + buffer[3] = (uint8_t)(restore_delay >> 8); + buffer[4] = (uint8_t)(restore_delay & 0xFF); buffer[5] = (buffer[0] + buffer[1] + buffer[2] + buffer[3] + buffer[4]) * 255; buffer[6] = ENDCHAR; diff --git a/drivers/sms_ser.h b/drivers/sms_ser.h index ea6c3acb04..43f486f520 100644 --- a/drivers/sms_ser.h +++ b/drivers/sms_ser.h @@ -27,7 +27,10 @@ #include "nut_stdint.h" -#define DEFAULT_BOOTDELAY 64 /* seconds (max 0xFF) */ +#define DEFAULT_BOOTDELAY 64 /* seconds */ +#define DEFAULT_OFFDELAY 30 /* seconds before shutdown.return cuts the output */ +#define DEFAULT_ONDELAY 1 /* delay before the output returns; SMS PowerView never sends 0 */ +#define DEFAULT_TESTDELAY 10 /* seconds, test.battery.start.quick */ #define MAXTRIES 3 /* First byte of the 'Q' reply: the UPS topology, as decoded by SMS PowerView */ @@ -75,7 +78,7 @@ uint8_t sms_prepare_set_beep(uint8_t* buffer); uint8_t sms_prepare_test_battery_low(uint8_t* buffer); uint8_t sms_prepare_test_battery_nsec(uint8_t* buffer, uint16_t delay); uint8_t sms_prepare_shutdown_nsec(uint8_t* buffer, uint16_t delay); -uint8_t sms_prepare_shutdown_restore(uint8_t* buffer); +uint8_t sms_prepare_shutdown_restore(uint8_t* buffer, uint16_t shutdown_delay, uint16_t restore_delay); uint8_t sms_prepare_cancel_test(uint8_t* buffer); uint8_t sms_prepare_cancel_shutdown(uint8_t* buffer); From 8e9c57ace967290d5bbff3254b2e749e1d5cd9aa Mon Sep 17 00:00:00 2001 From: Diogo Tridapalli Date: Sun, 20 Sep 2026 19:42:02 -0300 Subject: [PATCH 06/14] docs/man/sms_ser.txt: document settings, commands and status reporting [#1987] Describe the offdelay, ondelay and (so far undocumented) rebootdelay settings, the instant commands, how ups.status is composed, why BYPASS is not reported for line-interactive models, and what the battery level of these devices means while charging. Developed with Claude Code (model Claude Fable 5.1). Signed-off-by: Diogo Tridapalli --- docs/man/sms_ser.txt | 84 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/docs/man/sms_ser.txt b/docs/man/sms_ser.txt index f60c0c85a9..fdf31bcf48 100644 --- a/docs/man/sms_ser.txt +++ b/docs/man/sms_ser.txt @@ -31,6 +31,90 @@ the `monofasico.xml` file delivered with the device. Other SMS protocols (for their other products) are not supported by this driver. +Models with a USB port that were seen so far have a serial-to-USB adapter +inside (Cypress or Prolific), so the driver talks to the serial device node +which the operating system creates for it, e.g. `/dev/ttyUSB0`. + + +EXTRA ARGUMENTS +--------------- + +This driver supports the following optional settings in the +linkman:ups.conf[5] file: + +*offdelay*='value':: +Time to wait before the output is cut by the `shutdown.return` command +and when the driver is asked to power off the load at the end of a system +shutdown (default=30). The protocol calls it seconds. Also available as +the writable `ups.delay.shutdown` variable. + +*ondelay*='value':: +Time to wait before the output returns, once mains power is back, after +the shutdown above (default=1). The vendor software never sends 0 here. +Also available as the writable `ups.delay.start` variable. + +*rebootdelay*='value':: +Time to wait before the output is cut by the `shutdown.reboot` command +(default=64). Also available as the writable `ups.delay.reboot` variable. + +All three values are limited to 65535 by the protocol. + +As with any driver, a `default.battery.charge.low` or +`override.battery.charge.low` setting may be added, see NOTES below. + + +INSTANT COMMANDS +---------------- + +*beeper.toggle*:: +Toggle the UPS beeper. + +*test.battery.start* ['seconds']:: +Test the battery for the given time (default=10). + +*test.battery.start.quick*:: +Test the battery for 10 seconds. + +*test.battery.start.deep*:: +Test the battery until the UPS considers it low. + +*test.battery.stop*:: +Stop a battery test in progress. + +*shutdown.return*:: +Turn off the load after `ups.delay.shutdown` and return when power is back. + +*shutdown.reboot* ['seconds']:: +Turn off the load after the given time (default is `ups.delay.reboot`). + +*shutdown.stop*:: +Stop a shutdown in progress. + + +NOTES +----- + +The flags of the UPS are reported as independent `ups.status` values: +`OL` or `OB`, with `LB`, `RB` and `BOOST` added as needed. While the UPS +runs a battery test, `CAL` is added and `ups.test.result` says so, which +lets linkman:upsmon[8] tell a test from a power outage. + +The first byte of the status reply tells whether the UPS is a +line-interactive or an on-line design; it is published as `ups.type`. +Line-interactive models have no bypass, but at least some of them raise the +bypass flag whenever their inverter runs. Hence `BYPASS` is only reported +for the on-line designs, and never while on battery. + +The battery level the UPS reports follows the battery voltage while on +battery. On mains power, at least some models restart it at a low value +after any battery use (even a few seconds) and let it climb back to 100 +within minutes, so it should not be read as a state of charge then. +If `battery.charge.low` is defined, the driver raises `LB` when the UPS is +on battery and the level is at or below it, in addition to honoring the +low battery flag of the UPS itself. + +The protocol has no battery runtime value. + AUTHOR ------ From dc77e111435ee07b5f27440848d1a2105a4a1816 Mon Sep 17 00:00:00 2001 From: Diogo Tridapalli Date: Sun, 20 Sep 2026 19:42:02 -0300 Subject: [PATCH 07/14] NEWS.adoc, UPGRADING.adoc, data/driver.list.in: note sms_ser changes, add SMS Premium 1500 VA [#1987] The UPGRADING note covers the behaviors that changed for existing users: the meaning of test.battery.start.quick, the delays sent by the shutdown commands, and the composition of ups.status. Developed with Claude Code (model Claude Fable 5.1). Signed-off-by: Diogo Tridapalli --- NEWS.adoc | 27 +++++++++++++++++++++++++++ UPGRADING.adoc | 19 +++++++++++++++++++ data/driver.list.in | 1 + 3 files changed, 47 insertions(+) diff --git a/NEWS.adoc b/NEWS.adoc index 0c8c44aaee..16ce4e1911 100644 --- a/NEWS.adoc +++ b/NEWS.adoc @@ -360,6 +360,33 @@ https://github.com/networkupstools/nut/milestone/13 - `riello_usb`, `riello_ser` driver updates: * riello_usb, riello_ser: report NAK on RE as failure [issue #3546] + - `sms_ser` driver updates [issue #1987]: + * The `ups.status` is now built from all flags of the UPS, as `OL` or + `OB` with `LB`, `RB` and `BOOST` added as needed, instead of a single + value picked by precedence. Newer line-interactive models (SMS Premium, + SMS Gamer) raise the bypass flag whenever their inverter runs, so a + power outage was reported as `BYPASS` and never as `OB` or `LB`. The + topology byte of the status reply (now published as `ups.type`) is + used to only report `BYPASS` for on-line models, as the vendor software + does. + * A running battery test is reported as `CAL` with `ups.test.result`; + `LB` is also raised while on battery if the charge is at or below + `battery.charge.low`, when that is defined. + * Shutdown and test delays are now sent as 16-bit values as the vendor + software does (a delay of 64 used to be sent as `0x40FF`); added the + `offdelay` and `ondelay` settings with `ups.delay.shutdown`, + `ups.delay.start` and `ups.delay.reboot` variables, and the + `test.battery.start.deep` command. See also `UPGRADING.adoc`. + * Replies are now checked for their checksum and terminator, and the + serial port is reopened after repeated failures (e.g. a re-plugged + USB adapter). + * Measurements keep their decimal digit; `ups.mfr`, `ups.model`, + `device.mfr` and `output.voltage.nominal` are published; sent and + received frames can be seen at debug level 4. + * Tested with an SMS Premium 1500 VA (a line-interactive model); + the new shutdown and test frames were only checked against a protocol + simulator so far. + - `usbhid-ups` driver updates: * When reconnecting, report success more visibly (not only in debug), and do not reset driver state to "quiet" when it will loop trying. [PR #3423] diff --git a/UPGRADING.adoc b/UPGRADING.adoc index d51b3cbf68..8a6d9f984e 100644 --- a/UPGRADING.adoc +++ b/UPGRADING.adoc @@ -41,6 +41,25 @@ Changes from 2.8.5 to 2.8.6 - PLANNED: Keep track of any further API clean-up? +- The `sms_ser` driver changed the meaning of some commands and values + [issue #1987]: + * `test.battery.start.quick` used to start a test that runs until the + battery is low; it now runs a 10 second test. Use the new + `test.battery.start.deep` for the former behavior. + * `shutdown.return` (also used to power off the load at the end of a + system shutdown) and `shutdown.reboot` now send their delays in the + format used by the vendor software. Previously `shutdown.return` sent + no usable delays, and the `shutdown.reboot` delay was mangled (64 was + sent as `0x40FF`), so the timing seen with your device may change. See + the `offdelay`, `ondelay` and `rebootdelay` settings in + linkman:sms_ser[8]. A malformed `rebootdelay` in `ups.conf` is now fatal + instead of being silently ignored. + * `ups.status` may now hold several values (e.g. `OB LB`) where only one + was reported before, and line-interactive models no longer report + `BYPASS`; drop any workarounds which translated that value. With a low + battery properly reported, `upsmon` will now shut the system down where + it previously did not. + - The common configuration and protocol parser now accepts literal `#` characters inside double quotes. An unescaped `#` outside double quotes still starts a comment. Existing `\#` escapes remain supported and are diff --git a/data/driver.list.in b/data/driver.list.in index 0df5e50ce9..fe0b6ff03c 100644 --- a/data/driver.list.in +++ b/data/driver.list.in @@ -1416,6 +1416,7 @@ "SmartLabs" "pdu" "1" "2412S Power Line Modem" "for X10/Insteon" "powerman-pdu (experimental)" "SMS (Brazil)" "ups" "2" "Manager III" "" "blazer_ser" +"SMS (Brazil)" "ups" "2" "Premium 1500 VA (PRM1500BiBiW)" "USB (built-in Prolific PL2303 serial adapter)" "sms_ser" # https://github.com/networkupstools/nut/issues/1987 "SNR" "ups" "2" "SNR-UPS-LID-600" "" "nutdrv_qx" "SNR" "ups" "2" "SNR-UPS-LID-600-XPS" "" "nutdrv_qx" From 2727a359090b8ddf77afec3174538a8f6e4506ef Mon Sep 17 00:00:00 2001 From: Diogo Tridapalli Date: Sun, 20 Sep 2026 21:11:28 -0300 Subject: [PATCH 08/14] drivers/sms_ser.c: convert seconds to the time unit of the UPS [#1987] Timed battery tests on an SMS Premium 1500 (line-interactive), with the durations read from the event log of the UPS: values of 10, 30 and 100 ran for 6, 18 and 59 seconds, so the unit is a hundredth of a minute. This matches the vendor software, whose "10 seconds" test sends 16 and which multiplies its values by 0.6 only for the on-line type (so that type counts in seconds). With the conversion, a request for 30 seconds ran for 30. Apply it to the test duration and to the shutdown delays of 'S' and 'R'. Only the test duration was measured; the shutdown delays are assumed to use the same unit. The restore delay of 'R' is passed through unchanged. Developed with Claude Code (model Claude Fable 5.1). Signed-off-by: Diogo Tridapalli --- drivers/sms_ser.c | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/drivers/sms_ser.c b/drivers/sms_ser.c index cfad6eba1d..f03cc55cb6 100644 --- a/drivers/sms_ser.c +++ b/drivers/sms_ser.c @@ -305,6 +305,21 @@ static int sms_parse_delay(const char *text, uint16_t *delay) { return 1; } +/* Converts seconds into the time unit of the UPS. Line-interactive models + * count in hundredths of a minute (0.6 s): an SMS Premium 1500 tested for + * 6, 18 and 59 seconds when sent 10, 30 and 100. The vendor software scales + * its values by 0.6 for the on-line type, which therefore counts in seconds. + * Measured for the battery test only; assumed for the shutdown delays. */ +static uint16_t sms_seconds_to_units(uint16_t seconds) { + unsigned long units; + + if (DeviceData.upstype == SMS_TYPE_ONLINE) { + return seconds; + } + units = ((unsigned long)seconds * 10 + 3) / 6; + return (units > UINT16_MAX) ? UINT16_MAX : (uint16_t)units; +} + /* Sends the command prepared in bufOut. Commands are not acknowledged. */ static int sms_send_command(size_t length, const char *cmdname) { upsdebug_hex(4, "sms_ser send", bufOut, length); @@ -329,12 +344,12 @@ static int sms_instcmd(const char *cmdname, const char *extra) { return STAT_INSTCMD_CONVERSION_FAILED; } upslog_INSTCMD_POWERSTATE_MAYBE(cmdname, extra); - return sms_send_command(sms_prepare_test_battery_nsec(&bufOut[0], delay), cmdname); + return sms_send_command(sms_prepare_test_battery_nsec(&bufOut[0], sms_seconds_to_units(delay)), cmdname); } if (!strcasecmp(cmdname, "test.battery.start.quick")) { upslog_INSTCMD_POWERSTATE_MAYBE(cmdname, extra); - return sms_send_command(sms_prepare_test_battery_nsec(&bufOut[0], DEFAULT_TESTDELAY), cmdname); + return sms_send_command(sms_prepare_test_battery_nsec(&bufOut[0], sms_seconds_to_units(DEFAULT_TESTDELAY)), cmdname); } if (!strcasecmp(cmdname, "test.battery.start.deep")) { @@ -354,7 +369,7 @@ static int sms_instcmd(const char *cmdname, const char *extra) { if (!strcasecmp(cmdname, "shutdown.return")) { upslog_INSTCMD_POWERSTATE_CHANGE(cmdname, extra); - return sms_send_command(sms_prepare_shutdown_restore(&bufOut[0], offdelay, ondelay), cmdname); + return sms_send_command(sms_prepare_shutdown_restore(&bufOut[0], sms_seconds_to_units(offdelay), ondelay), cmdname); } if (!strcasecmp(cmdname, "shutdown.reboot")) { @@ -364,7 +379,7 @@ static int sms_instcmd(const char *cmdname, const char *extra) { return STAT_INSTCMD_CONVERSION_FAILED; } upslog_INSTCMD_POWERSTATE_CHANGE(cmdname, extra); - return sms_send_command(sms_prepare_shutdown_nsec(&bufOut[0], delay), cmdname); + return sms_send_command(sms_prepare_shutdown_nsec(&bufOut[0], sms_seconds_to_units(delay)), cmdname); } if (!strcasecmp(cmdname, "shutdown.stop")) { From 905209a07de5b58c420b097b2bc0e7edca0f66a7 Mon Sep 17 00:00:00 2001 From: Diogo Tridapalli Date: Sun, 20 Sep 2026 21:11:28 -0300 Subject: [PATCH 09/14] drivers/sms_ser.c: query the UPS type before upsdrv_shutdown() sends delays [#1987] The time unit of the delays depends on the UPS type, which is only known after a status poll, and "driver -k" runs upsdrv_shutdown() in a fresh instance that never polled. Developed with Claude Code (model Claude Fable 5.1). Signed-off-by: Diogo Tridapalli --- drivers/sms_ser.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/drivers/sms_ser.c b/drivers/sms_ser.c index f03cc55cb6..e58cd9b6ee 100644 --- a/drivers/sms_ser.c +++ b/drivers/sms_ser.c @@ -650,6 +650,12 @@ void upsdrv_shutdown(void) { /* OB: the load must remain off until the power returns */ upsdebugx(2, "upsdrv Shutdown execute"); + /* Learn the UPS type, which decides the time unit of the delays; this + * may be a fresh driver instance which never polled the UPS */ + if (get_ups_nominal() != 0) { + upslogx(LOG_WARNING, "No status from UPS, assuming a line-interactive model for the delays"); + } + for (retry = 1; retry <= MAXTRIES; retry++) { /* By default, abort a previously requested shutdown * (if any) and schedule a new one from this moment. */ From c2b920c7ae1c76f32feb919a5c0154789c9ad308 Mon Sep 17 00:00:00 2001 From: Diogo Tridapalli Date: Sun, 20 Sep 2026 21:11:29 -0300 Subject: [PATCH 10/14] drivers/sms_ser.c: battery.charger.status is "discharging" whenever on battery [#1987] At the first poll of an outage the battery level can still read 100, and the status was then reported as "floating". Developed with Claude Code (model Claude Fable 5.1). Signed-off-by: Diogo Tridapalli --- drivers/sms_ser.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/drivers/sms_ser.c b/drivers/sms_ser.c index e58cd9b6ee..165de6fce6 100644 --- a/drivers/sms_ser.c +++ b/drivers/sms_ser.c @@ -478,8 +478,8 @@ void upsdrv_initinfo(void) { dstate_setinfo("battery.voltage", "%.2f", (DeviceData.voltageBattery * DeviceData.batterylevel) / 100); dstate_setinfo("ups.temperature", "%.2f", DeviceData.temperatureC); - if (DeviceData.onbattery && (uint8_t)DeviceData.batterylevel < 100) { - upsdebugx(LOG_DEBUG, "on battery and battery < last battery"); + if (DeviceData.onbattery) { + upsdebugx(LOG_DEBUG, "on battery"); battery_status = "discharging"; } else if (!DeviceData.onbattery && (uint8_t)DeviceData.batterylevel < 100) { upsdebugx(LOG_DEBUG, "on power and battery > last battery"); @@ -562,8 +562,8 @@ void upsdrv_updateinfo(void) { upsdebugx(LOG_DEBUG, "lowBattery: %d", DeviceData.lowbattery); upsdebugx(LOG_DEBUG, "test: %d", DeviceData.test); - if (DeviceData.onbattery && (uint8_t)DeviceData.batterylevel < 100) { - upsdebugx(LOG_DEBUG, "on battery and battery < last battery"); + if (DeviceData.onbattery) { + upsdebugx(LOG_DEBUG, "on battery"); battery_status = "discharging"; } else if (!DeviceData.onbattery && (uint8_t)DeviceData.batterylevel < 100) { upsdebugx(LOG_DEBUG, "on power and battery > last battery"); From 489fe518b9309d3b726d2328b26a7322d7a6c070 Mon Sep 17 00:00:00 2001 From: Diogo Tridapalli Date: Sun, 20 Sep 2026 22:48:00 -0300 Subject: [PATCH 11/14] drivers/sms_ser.c: add "legacydelays" and "novalidate" flags as a way back [#1987] The delay format and the reply validation were checked with one model and a protocol simulator, plus reply samples of two other models. Should a device which worked with earlier versions not cope with either change, these flags restore the earlier behavior without downgrading: the old parameter bytes for 'T', 'S' and 'R', and no checksum/terminator check. Both log a request to report the device. Developed with Claude Code (model Claude Fable 5.1). Signed-off-by: Diogo Tridapalli --- drivers/sms_ser.c | 52 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/drivers/sms_ser.c b/drivers/sms_ser.c index 165de6fce6..72ffac1d00 100644 --- a/drivers/sms_ser.c +++ b/drivers/sms_ser.c @@ -45,6 +45,8 @@ static uint8_t bufOut[BUFFER_SIZE]; static uint8_t bufIn[BUFFER_SIZE]; static SmsData DeviceData; static int comm_failures = 0; +static int novalidate = 0; +static int legacydelays = 0; /* driver description structure */ upsdrv_info_t upsdrv_info = { @@ -191,7 +193,7 @@ static ssize_t sms_query(uint8_t length) { for (i = 0; i < RESULT_SIZE - 1; i++) { sum += bufIn[i]; } - if (sum != 0 || bufIn[RESULT_SIZE - 1] != ENDCHAR) { + if (!novalidate && (sum != 0 || bufIn[RESULT_SIZE - 1] != ENDCHAR)) { upslogx(LOG_ERR, "Bad checksum or terminator in reply from UPS"); dstate_datastale(); return -1; @@ -320,6 +322,18 @@ static uint16_t sms_seconds_to_units(uint16_t seconds) { return (units > UINT16_MAX) ? UINT16_MAX : (uint16_t)units; } +/* Rewrites the parameters of a prepared 'T', 'S' or 'R' command the way + * driver versions before 1.06 sent them, for devices that turn out to rely + * on it (the "legacydelays" flag): the low byte of the delay in seconds, if + * the command had one, and 0xFF in all other parameter bytes. */ +static void sms_apply_legacy_delays(uint8_t *buffer, int has_delay, uint16_t seconds) { + buffer[1] = has_delay ? (uint8_t)(seconds % 256) : 255; + buffer[2] = 255; + buffer[3] = 255; + buffer[4] = 255; + buffer[5] = (buffer[0] + buffer[1] + buffer[2] + buffer[3] + buffer[4]) * 255; +} + /* Sends the command prepared in bufOut. Commands are not acknowledged. */ static int sms_send_command(size_t length, const char *cmdname) { upsdebug_hex(4, "sms_ser send", bufOut, length); @@ -333,6 +347,7 @@ static int sms_send_command(size_t length, const char *cmdname) { static int sms_instcmd(const char *cmdname, const char *extra) { uint16_t delay; + size_t length; upsdebug_INSTCMD_STARTING(cmdname, extra); @@ -344,12 +359,20 @@ static int sms_instcmd(const char *cmdname, const char *extra) { return STAT_INSTCMD_CONVERSION_FAILED; } upslog_INSTCMD_POWERSTATE_MAYBE(cmdname, extra); - return sms_send_command(sms_prepare_test_battery_nsec(&bufOut[0], sms_seconds_to_units(delay)), cmdname); + length = sms_prepare_test_battery_nsec(&bufOut[0], sms_seconds_to_units(delay)); + if (legacydelays) { + sms_apply_legacy_delays(&bufOut[0], 1, delay); + } + return sms_send_command(length, cmdname); } if (!strcasecmp(cmdname, "test.battery.start.quick")) { upslog_INSTCMD_POWERSTATE_MAYBE(cmdname, extra); - return sms_send_command(sms_prepare_test_battery_nsec(&bufOut[0], sms_seconds_to_units(DEFAULT_TESTDELAY)), cmdname); + length = sms_prepare_test_battery_nsec(&bufOut[0], sms_seconds_to_units(DEFAULT_TESTDELAY)); + if (legacydelays) { + sms_apply_legacy_delays(&bufOut[0], 1, DEFAULT_TESTDELAY); + } + return sms_send_command(length, cmdname); } if (!strcasecmp(cmdname, "test.battery.start.deep")) { @@ -369,7 +392,11 @@ static int sms_instcmd(const char *cmdname, const char *extra) { if (!strcasecmp(cmdname, "shutdown.return")) { upslog_INSTCMD_POWERSTATE_CHANGE(cmdname, extra); - return sms_send_command(sms_prepare_shutdown_restore(&bufOut[0], sms_seconds_to_units(offdelay), ondelay), cmdname); + length = sms_prepare_shutdown_restore(&bufOut[0], sms_seconds_to_units(offdelay), ondelay); + if (legacydelays) { + sms_apply_legacy_delays(&bufOut[0], 0, 0); + } + return sms_send_command(length, cmdname); } if (!strcasecmp(cmdname, "shutdown.reboot")) { @@ -379,7 +406,11 @@ static int sms_instcmd(const char *cmdname, const char *extra) { return STAT_INSTCMD_CONVERSION_FAILED; } upslog_INSTCMD_POWERSTATE_CHANGE(cmdname, extra); - return sms_send_command(sms_prepare_shutdown_nsec(&bufOut[0], sms_seconds_to_units(delay)), cmdname); + length = sms_prepare_shutdown_nsec(&bufOut[0], sms_seconds_to_units(delay)); + if (legacydelays) { + sms_apply_legacy_delays(&bufOut[0], 1, delay); + } + return sms_send_command(length, cmdname); } if (!strcasecmp(cmdname, "shutdown.stop")) { @@ -703,6 +734,9 @@ void upsdrv_makevartable(void) { snprintf(msg, sizeof msg, "Set delay before the output returns after shutdown.return (default=%d).", DEFAULT_ONDELAY); addvar(VAR_VALUE, "ondelay", msg); + + addvar(VAR_FLAG, "legacydelays", "Send test and shutdown delays the way driver versions before 1.06 did"); + addvar(VAR_FLAG, "novalidate", "Do not check the checksum and terminator of replies from the UPS"); } void upsdrv_initups(void) { @@ -722,6 +756,14 @@ void upsdrv_initups(void) { if ((val = getval("ondelay")) && !sms_parse_delay(val, &ondelay)) { fatalx(EXIT_FAILURE, "Invalid ondelay [%s]", val); } + if (testvar("legacydelays")) { + legacydelays = 1; + upslogx(LOG_NOTICE, "Sending delays in the legacy format; please report to the NUT project why this was needed"); + } + if (testvar("novalidate")) { + novalidate = 1; + upslogx(LOG_NOTICE, "Not validating replies from the UPS; please report to the NUT project why this was needed"); + } } void upsdrv_cleanup(void) { From d3ee49ae7c92aff95b4aacb7879d4bc34b769373 Mon Sep 17 00:00:00 2001 From: Diogo Tridapalli Date: Sun, 20 Sep 2026 22:48:01 -0300 Subject: [PATCH 12/14] docs/man/sms_ser.txt, NEWS.adoc, UPGRADING.adoc, docs/nut.dict: sms_ser time unit and fallback flags [#1987] Say that the delays are given in seconds and converted to the time unit of the UPS, what was measured and what is assumed, and that the unit of the restore delay is not known. Document the "legacydelays" and "novalidate" flags and add them to the dictionary (the header count is as regenerated by "make spellcheck-sortdict"). Developed with Claude Code (model Claude Fable 5.1). Signed-off-by: Diogo Tridapalli --- NEWS.adoc | 19 +++++++++++++------ UPGRADING.adoc | 10 ++++++++-- docs/man/sms_ser.txt | 37 +++++++++++++++++++++++++++++-------- docs/nut.dict | 4 +++- 4 files changed, 53 insertions(+), 17 deletions(-) diff --git a/NEWS.adoc b/NEWS.adoc index 16ce4e1911..7d17cfec7b 100644 --- a/NEWS.adoc +++ b/NEWS.adoc @@ -376,16 +376,23 @@ https://github.com/networkupstools/nut/milestone/13 software does (a delay of 64 used to be sent as `0x40FF`); added the `offdelay` and `ondelay` settings with `ups.delay.shutdown`, `ups.delay.start` and `ups.delay.reboot` variables, and the - `test.battery.start.deep` command. See also `UPGRADING.adoc`. + `test.battery.start.deep` command. Times given in seconds, for battery + tests and shutdown delays, are converted to the time unit of the UPS, + which is 0.6 seconds for line-interactive models (a 10 second test + used to run for 6). See also `UPGRADING.adoc`. * Replies are now checked for their checksum and terminator, and the serial port is reopened after repeated failures (e.g. a re-plugged USB adapter). + * Two flags give a way back if a device which worked before does not + cope with these changes: `legacydelays` and `novalidate`. Reports + about devices which need them are welcome. * Measurements keep their decimal digit; `ups.mfr`, `ups.model`, - `device.mfr` and `output.voltage.nominal` are published; sent and - received frames can be seen at debug level 4. - * Tested with an SMS Premium 1500 VA (a line-interactive model); - the new shutdown and test frames were only checked against a protocol - simulator so far. + `device.mfr` and `output.voltage.nominal` are published; + `battery.charger.status` no longer says `floating` at the start of an + outage; sent and received frames can be seen at debug level 4. + * Tested with an SMS Premium 1500 VA (a line-interactive model), + including the battery test commands and their timing; the shutdown + commands were only checked against a protocol simulator so far. - `usbhid-ups` driver updates: * When reconnecting, report success more visibly (not only in debug), and diff --git a/UPGRADING.adoc b/UPGRADING.adoc index 8a6d9f984e..566eff3513 100644 --- a/UPGRADING.adoc +++ b/UPGRADING.adoc @@ -50,10 +50,16 @@ Changes from 2.8.5 to 2.8.6 system shutdown) and `shutdown.reboot` now send their delays in the format used by the vendor software. Previously `shutdown.return` sent no usable delays, and the `shutdown.reboot` delay was mangled (64 was - sent as `0x40FF`), so the timing seen with your device may change. See + sent as `0x40FF`), and converts seconds to the time unit of the UPS, + so the timing seen with your device may change. See the `offdelay`, `ondelay` and `rebootdelay` settings in linkman:sms_ser[8]. A malformed `rebootdelay` in `ups.conf` is now fatal - instead of being silently ignored. + instead of being silently ignored. If a shutdown or test which worked + before does not work now, the `legacydelays` flag restores the old + format; please report such a device. + * Replies from the UPS are now checked for their checksum. If a device + which worked before is rejected, the `novalidate` flag turns the check + off; please report such a device. * `ups.status` may now hold several values (e.g. `OB LB`) where only one was reported before, and line-interactive models no longer report `BYPASS`; drop any workarounds which translated that value. With a low diff --git a/docs/man/sms_ser.txt b/docs/man/sms_ser.txt index fdf31bcf48..c7ec2583f2 100644 --- a/docs/man/sms_ser.txt +++ b/docs/man/sms_ser.txt @@ -45,19 +45,40 @@ linkman:ups.conf[5] file: *offdelay*='value':: Time to wait before the output is cut by the `shutdown.return` command and when the driver is asked to power off the load at the end of a system -shutdown (default=30). The protocol calls it seconds. Also available as -the writable `ups.delay.shutdown` variable. +shutdown, in seconds (default=30). Also available as the writable +`ups.delay.shutdown` variable. *ondelay*='value':: Time to wait before the output returns, once mains power is back, after -the shutdown above (default=1). The vendor software never sends 0 here. -Also available as the writable `ups.delay.start` variable. +the shutdown above (default=1). This value is sent to the UPS as is, and +its unit is not known (possibly minutes); the vendor software never sends +0 here. Also available as the writable `ups.delay.start` variable. *rebootdelay*='value':: -Time to wait before the output is cut by the `shutdown.reboot` command -(default=64). Also available as the writable `ups.delay.reboot` variable. - -All three values are limited to 65535 by the protocol. +Time to wait before the output is cut by the `shutdown.reboot` command, +in seconds (default=64). Also available as the writable `ups.delay.reboot` +variable. + +The protocol has 16 bits for each of these values. Line-interactive models +count time in hundredths of a minute (0.6 seconds), so the driver converts +seconds accordingly, which limits them to about 39000 seconds; models of +the on-line type are sent the seconds unchanged. The unit was measured with +battery tests on a line-interactive model and is assumed to be the same for +the shutdown delays. + +*legacydelays*:: +Send the parameters of the battery test and shutdown commands the way +driver versions before 1.06 did (the low byte of the seconds for a timed +test or `shutdown.reboot`, and no delays at all for `shutdown.return`), +instead of the format used by the vendor software. Only meant as a way back +for a device whose shutdown or test worked before and does not now; please +report such a device to the NUT project. + +*novalidate*:: +Do not check the checksum and the terminator of the replies from the UPS. +Only meant as a way back for a device which worked before and whose replies +are now rejected ("Bad checksum or terminator" in the log); please report +such a device to the NUT project. As with any driver, a `default.battery.charge.low` or `override.battery.charge.low` setting may be added, see NOTES below. diff --git a/docs/nut.dict b/docs/nut.dict index ce562515f4..8e054323f2 100644 --- a/docs/nut.dict +++ b/docs/nut.dict @@ -1,4 +1,4 @@ -personal_ws-1.1 en 3833 utf-8 +personal_ws-1.1 en 3839 utf-8 AAC AAS ABI @@ -2546,6 +2546,7 @@ ld ldd ldflags le +legacydelays legrand len leveloffset @@ -2882,6 +2883,7 @@ notifyme notifymsg notifytype notransferoids +novalidate novendor nowait nowarn From d64e5f2aa1b1848892dc74464dfac7fc85fa4779 Mon Sep 17 00:00:00 2001 From: Diogo Tridapalli Date: Mon, 21 Sep 2026 21:37:10 -0300 Subject: [PATCH 13/14] drivers/sms_ser.c, sms_ser.h, docs/man/sms_ser.txt, NEWS.adoc: the restore delay counts in minutes [#1987] Measured on an SMS Premium 1500 VA: a restore delay of 1 brought the output back 63 s after the cut, 3 brought it back after 183 s, so the UPS counts this field in minutes while the shutdown delays count in 0.6 s units. The ondelay setting and ups.delay.start are now in seconds like the other delays, rounded up to whole minutes and never below one minute when sent (the vendor software never sends 0). Default 60. With mains absent the UPS switches itself off about a minute after the cut and re-energises a few seconds after mains returns, whatever the restore delay; noted in the man page. Developed with Claude Code (model Claude Fable 5.1). Signed-off-by: Diogo Tridapalli --- NEWS.adoc | 3 ++- docs/man/sms_ser.txt | 15 +++++++++------ drivers/sms_ser.c | 16 +++++++++++++--- drivers/sms_ser.h | 2 +- 4 files changed, 25 insertions(+), 11 deletions(-) diff --git a/NEWS.adoc b/NEWS.adoc index 7d17cfec7b..905b264a26 100644 --- a/NEWS.adoc +++ b/NEWS.adoc @@ -379,7 +379,8 @@ https://github.com/networkupstools/nut/milestone/13 `test.battery.start.deep` command. Times given in seconds, for battery tests and shutdown delays, are converted to the time unit of the UPS, which is 0.6 seconds for line-interactive models (a 10 second test - used to run for 6). See also `UPGRADING.adoc`. + used to run for 6); the restore delay is sent in whole minutes, as the + UPS counts it. See also `UPGRADING.adoc`. * Replies are now checked for their checksum and terminator, and the serial port is reopened after repeated failures (e.g. a re-plugged USB adapter). diff --git a/docs/man/sms_ser.txt b/docs/man/sms_ser.txt index c7ec2583f2..1ba14d8c00 100644 --- a/docs/man/sms_ser.txt +++ b/docs/man/sms_ser.txt @@ -50,9 +50,12 @@ shutdown, in seconds (default=30). Also available as the writable *ondelay*='value':: Time to wait before the output returns, once mains power is back, after -the shutdown above (default=1). This value is sent to the UPS as is, and -its unit is not known (possibly minutes); the vendor software never sends -0 here. Also available as the writable `ups.delay.start` variable. +the shutdown above, in seconds (default=60). The UPS counts this delay in +whole minutes, so the value is rounded up to the next minute, and never +below one minute, as the vendor software never sends 0 here. When mains +power is absent the UPS switches itself off about a minute after the output +is cut and returns a few seconds after mains power is back, regardless of +this setting. Also available as the writable `ups.delay.start` variable. *rebootdelay*='value':: Time to wait before the output is cut by the `shutdown.reboot` command, @@ -62,9 +65,9 @@ variable. The protocol has 16 bits for each of these values. Line-interactive models count time in hundredths of a minute (0.6 seconds), so the driver converts seconds accordingly, which limits them to about 39000 seconds; models of -the on-line type are sent the seconds unchanged. The unit was measured with -battery tests on a line-interactive model and is assumed to be the same for -the shutdown delays. +the on-line type are sent the seconds unchanged. The unit was measured on a +line-interactive model (SMS Premium 1500 VA) for battery tests and for both +shutdown delays, with the restore delay in minutes as described above. *legacydelays*:: Send the parameters of the battery test and shutdown commands the way diff --git a/drivers/sms_ser.c b/drivers/sms_ser.c index 72ffac1d00..b0b5d5e3da 100644 --- a/drivers/sms_ser.c +++ b/drivers/sms_ser.c @@ -311,7 +311,8 @@ static int sms_parse_delay(const char *text, uint16_t *delay) { * count in hundredths of a minute (0.6 s): an SMS Premium 1500 tested for * 6, 18 and 59 seconds when sent 10, 30 and 100. The vendor software scales * its values by 0.6 for the on-line type, which therefore counts in seconds. - * Measured for the battery test only; assumed for the shutdown delays. */ + * Measured on the same UPS for the battery test and the 'S' and 'R' + * shutdown delays. */ static uint16_t sms_seconds_to_units(uint16_t seconds) { unsigned long units; @@ -322,6 +323,15 @@ static uint16_t sms_seconds_to_units(uint16_t seconds) { return (units > UINT16_MAX) ? UINT16_MAX : (uint16_t)units; } +/* The restore delay of 'R' counts in minutes on the same UPS (1 gave 63 s, + * 3 gave 183 s), and the vendor software never sends 0 there. Rounds up, + * with 1 as the minimum. */ +static uint16_t sms_seconds_to_minutes(uint16_t seconds) { + uint16_t minutes = (uint16_t)((seconds + 59) / 60); + + return minutes ? minutes : 1; +} + /* Rewrites the parameters of a prepared 'T', 'S' or 'R' command the way * driver versions before 1.06 sent them, for devices that turn out to rely * on it (the "legacydelays" flag): the low byte of the delay in seconds, if @@ -392,7 +402,7 @@ static int sms_instcmd(const char *cmdname, const char *extra) { if (!strcasecmp(cmdname, "shutdown.return")) { upslog_INSTCMD_POWERSTATE_CHANGE(cmdname, extra); - length = sms_prepare_shutdown_restore(&bufOut[0], sms_seconds_to_units(offdelay), ondelay); + length = sms_prepare_shutdown_restore(&bufOut[0], sms_seconds_to_units(offdelay), sms_seconds_to_minutes(ondelay)); if (legacydelays) { sms_apply_legacy_delays(&bufOut[0], 0, 0); } @@ -731,7 +741,7 @@ void upsdrv_makevartable(void) { DEFAULT_OFFDELAY); addvar(VAR_VALUE, "offdelay", msg); - snprintf(msg, sizeof msg, "Set delay before the output returns after shutdown.return (default=%d).", + snprintf(msg, sizeof msg, "Set delay before the output returns after shutdown.return, in seconds, rounded up to whole minutes (default=%d).", DEFAULT_ONDELAY); addvar(VAR_VALUE, "ondelay", msg); diff --git a/drivers/sms_ser.h b/drivers/sms_ser.h index 43f486f520..b39cbc6cc1 100644 --- a/drivers/sms_ser.h +++ b/drivers/sms_ser.h @@ -29,7 +29,7 @@ #define DEFAULT_BOOTDELAY 64 /* seconds */ #define DEFAULT_OFFDELAY 30 /* seconds before shutdown.return cuts the output */ -#define DEFAULT_ONDELAY 1 /* delay before the output returns; SMS PowerView never sends 0 */ +#define DEFAULT_ONDELAY 60 /* seconds before the output returns; sent in whole minutes */ #define DEFAULT_TESTDELAY 10 /* seconds, test.battery.start.quick */ #define MAXTRIES 3 From e35a402e68aa5b2685e88c724fd547728eb4ea33 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sat, 10 Oct 2026 20:18:57 +0200 Subject: [PATCH 14/14] drivers/sms_ser.c: sms_parse_delay(): fix range check for value [#3701] Signed-off-by: Jim Klimov --- drivers/sms_ser.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/sms_ser.c b/drivers/sms_ser.c index b0b5d5e3da..42a28c8b9c 100644 --- a/drivers/sms_ser.c +++ b/drivers/sms_ser.c @@ -300,7 +300,7 @@ static int sms_parse_delay(const char *text, uint16_t *delay) { return 0; } value = strtol(text, &end, 10); - if (*end != '\0' || value < 0 || value > UINT16_MAX) { + if (*end != '\0' || value < 0 || (uintmax_t)value > (uintmax_t)UINT16_MAX) { return 0; } *delay = (uint16_t)value;