From 7a8430dc160bd5821a3a2f9e09dd739ce9164fc5 Mon Sep 17 00:00:00 2001 From: Axel Gembe Date: Tue, 14 Nov 2023 01:47:38 +0700 Subject: [PATCH 1/7] apc_modbus: Updates, command, writable variables and outlet group support This adds support for commands, writable variables and outlet groups. - For commands there is a new table called `apc_modbus_command_map` which defines the supported commands as a tuple of command name, register offset and value to write. This also adds a new instcmd status called `STAT_INSTCMD_CONVERSION_FAILED` for when conversion of values fails. On startup all the commands are registered using `dstate_addcmd`. This also adds support for the `upsdrv_shutdown` function. - For writable variables we added a new flag called `APC_VF_RW` to the existing variables that indicates a writable variable. We added code to convert from a string to UINT/INT/STRING variables with output in APCs register format. There is a new `_apc_modbus_setvar` function that handles setting variables and rereading them from the device. This also adds a new setvar status called `STAT_SET_CONVERSION_FAILED` for when conversion of values fails. Variables are now correctly set as `ST_FLAG_STRING` and `ST_FLAG_RW` and we call `dstate_setaux` to give a maximum length for strings. - For outlet groups, we now have names, configurable delays and commands per outlet group. Devices have an outlet group called MOG (Main outlet group) that switches all the outputs of the UPS and 1-3 SOGs (Switched outlet groups) that can be controlled independently. Note that MOG is `outlet.group.0` and the SOGs start at `outlet.group.1`. The outlet groups should have markings with the same index at the back of the unit. - This also reduces the length of some of the defines to make the variable maps more readable. - It also adds a comment to when the reopen matcher is created that clarifies why we create it. Signed-off-by: Axel Gembe --- drivers/apc_modbus.c | 544 +++++++++++++++++++++++++++++++++++++------ drivers/apc_modbus.h | 78 +++++++ drivers/upshandler.h | 18 +- 3 files changed, 562 insertions(+), 78 deletions(-) create mode 100644 drivers/apc_modbus.h diff --git a/drivers/apc_modbus.c b/drivers/apc_modbus.c index 7e437e2fce..27ffe244e0 100644 --- a/drivers/apc_modbus.c +++ b/drivers/apc_modbus.c @@ -24,6 +24,7 @@ #endif /* defined NUT_MODBUS_HAS_USB */ #include "timehead.h" #include "nut_stdint.h" +#include "apc_modbus.h" #include #include @@ -31,7 +32,7 @@ #include #define DRIVER_NAME "NUT APC Modbus driver" -#define DRIVER_VERSION "0.01" +#define DRIVER_VERSION "0.10" #if defined NUT_MODBUS_HAS_USB @@ -89,23 +90,30 @@ upsdrv_info_t upsdrv_info = { }; typedef enum { - APC_MODBUS_VALUE_TYPE_INT = 0, - APC_MODBUS_VALUE_TYPE_UINT, - APC_MODBUS_VALUE_TYPE_STRING + APC_VT_INT = 0, + APC_VT_UINT, + APC_VT_STRING } apc_modbus_value_types; -static const apc_modbus_value_types apc_modbus_value_types_max = APC_MODBUS_VALUE_TYPE_STRING; +typedef enum { + APC_VF_NONE = 0, + APC_VF_RW = (1 << 0) +} apc_modbus_value_flags; + +static const apc_modbus_value_types apc_modbus_value_types_max = APC_VT_STRING; + +typedef union { + int64_t int_value; + uint64_t uint_value; + char *string_value; +} apc_modbus_value_data_t; typedef struct { apc_modbus_value_types type; const char *format; int scale; void *variable_ptr; - union { - int64_t int_value; - uint64_t uint_value; - char *string_value; - } data; + apc_modbus_value_data_t data; } apc_modbus_value_t; typedef struct { @@ -178,6 +186,37 @@ static int _apc_modbus_to_string(const uint16_t *value, const size_t value_len, return 1; } +static int _apc_modbus_from_string(const char *value, uint16_t *output, size_t output_len) +{ + size_t value_len, vi, oi; + uint16_t tmp; + + if (value == NULL || output == NULL) { + /* Invalid parameters */ + return 0; + } + + value_len = strlen(value); + + if (value_len > (output_len * sizeof(uint16_t))) { + /* Output buffer too small */ + return 0; + } + + for (vi = 0, oi = 0; vi < value_len && oi < output_len; vi += 2, oi++) { + tmp = value[vi] << 8; + if (vi + 1 < value_len) + tmp |= value[vi + 1]; + output[oi] = tmp; + } + + for (; oi < output_len; oi++) { + output[oi] = 0; + } + + return 1; +} + /* Converts a Modbus integer to a uint64_t. * See MPAO-98KJ7F_EN section 1.3.1 Numbers. */ static int _apc_modbus_to_uint64(const uint16_t *value, const size_t value_len, uint64_t *output) @@ -197,6 +236,51 @@ static int _apc_modbus_to_uint64(const uint16_t *value, const size_t value_len, return 1; } +static int _apc_modbus_from_uint64(uint64_t value, uint16_t *output, size_t output_len) +{ + ssize_t oi; + size_t bits; + + if (output == NULL) { + /* Invalid parameters */ + return 0; + } + + bits = output_len * sizeof(uint16_t) * 8; + if (bits < 64) { + if (value > ((1ULL << bits) - 1)) { + /* Overflow */ + return 0; + } + } + + for (oi = output_len - 1; oi >= 0; oi--) { + output[oi] = (uint16_t)(value & 0xFFFF); + value >>= 16; + } + + return 1; +} + +static int _apc_modbus_from_uint64_string(const char *value, uint16_t *output, size_t output_len) +{ + uint64_t value_uint; + char *endptr; + + if (value == NULL || output == NULL) { + /* Invalid parameters */ + return 0; + } + + errno = 0; + value_uint = strtoull(value, &endptr, 0); + if (endptr == value || *endptr != '\0' || errno > 0) { + return 0; + } + + return _apc_modbus_from_uint64(value_uint, output, output_len); +} + static int _apc_modbus_to_int64(const uint16_t *value, const size_t value_len, int64_t *output) { size_t shiftval; @@ -212,6 +296,50 @@ static int _apc_modbus_to_int64(const uint16_t *value, const size_t value_len, i return 1; } +static int _apc_modbus_from_int64(int64_t value, uint16_t *output, size_t output_len) +{ + ssize_t oi; + size_t bits; + + if (output == NULL) { + /* Invalid parameters */ + return 0; + } + + bits = output_len * sizeof(uint16_t) * 8; + if (value > ((1LL << (bits - 1)) - 1) || + value < -(1LL << (bits - 1))) { + /* Overflow */ + return 0; + } + + for (oi = output_len - 1; oi >= 0; oi--) { + output[oi] = (uint16_t)(value & 0xFFFF); + value >>= 16; + } + + return 1; +} + +static int _apc_modbus_from_int64_string(const char *value, uint16_t *output, size_t output_len) +{ + int64_t value_int; + char *endptr; + + if (value == NULL || output == NULL) { + /* Invalid parameters */ + return 0; + } + + errno = 0; + value_int = strtoll(value, &endptr, 0); + if (endptr == value || *endptr != '\0' || errno > 0) { + return 0; + } + + return _apc_modbus_from_int64(value_int, output, output_len); +} + static int _apc_modbus_to_double(const apc_modbus_value_t *value, double *output) { int factor; @@ -225,13 +353,13 @@ static int _apc_modbus_to_double(const apc_modbus_value_t *value, double *output assert(value->type <= apc_modbus_value_types_max); switch (value->type) { - case APC_MODBUS_VALUE_TYPE_INT: + case APC_VT_INT: *output = (double)value->data.int_value / factor; break; - case APC_MODBUS_VALUE_TYPE_UINT: + case APC_VT_UINT: *output = (double)value->data.uint_value / factor; break; - case APC_MODBUS_VALUE_TYPE_STRING: + case APC_VT_STRING: return 0; } @@ -332,7 +460,7 @@ static int _apc_modbus_voltage_to_nut(const apc_modbus_value_t *value, char *out return 0; } - if (value->type != APC_MODBUS_VALUE_TYPE_UINT) { + if (value->type != APC_VT_UINT) { return 0; } @@ -360,7 +488,7 @@ static int _apc_modbus_efficiency_to_nut(const apc_modbus_value_t *value, char * return 0; } - if (value->type != APC_MODBUS_VALUE_TYPE_INT) { + if (value->type != APC_VT_INT) { return 0; } @@ -413,7 +541,7 @@ static int _apc_modbus_status_change_cause_to_nut(const apc_modbus_value_t *valu return 0; } - if (value->type != APC_MODBUS_VALUE_TYPE_UINT) { + if (value->type != APC_VT_UINT) { return 0; } @@ -526,35 +654,62 @@ static int _apc_modbus_status_change_cause_to_nut(const apc_modbus_value_t *valu static apc_modbus_converter_t _apc_modbus_status_change_cause_conversion = { _apc_modbus_status_change_cause_to_nut, NULL }; +static const time_t apc_date_start_offset = 946684800; /* 2000-01-01 00:00 */ + static int _apc_modbus_date_to_nut(const apc_modbus_value_t *value, char *output, size_t output_len) { struct tm *tm_info; time_t time_stamp; - const time_t start_offset = 946684800; /* 2000-01-01 00:00 */ if (value == NULL || output == NULL || output_len == 0) { /* Invalid parameters */ return 0; } - if (value->type != APC_MODBUS_VALUE_TYPE_UINT) { + if (value->type != APC_VT_UINT) { return 0; } - time_stamp = ((int64_t)value->data.uint_value * 86400) + start_offset; + time_stamp = ((int64_t)value->data.uint_value * 86400) + apc_date_start_offset; tm_info = gmtime(&time_stamp); strftime(output, output_len, "%Y-%m-%d", tm_info); return 1; } -static apc_modbus_converter_t _apc_modbus_date_conversion = { _apc_modbus_date_to_nut, NULL }; +static int _apc_modbus_date_from_nut(const char *value, uint16_t *output, size_t output_len) +{ + struct tm tm_struct; + time_t epoch_time; + uint64_t uint_value; + + if (value == NULL || output == NULL || output_len == 0) { + /* Invalid parameters */ + return 0; + } + + memset(&tm_struct, 0, sizeof(tm_struct)); + if (strptime(value, "%Y-%m-%d", &tm_struct) == NULL) { + return 0; + } + + if ((epoch_time = mktime(&tm_struct)) == -1) { + return 0; + } + + uint_value = (epoch_time - apc_date_start_offset) / 86400; + + return _apc_modbus_from_uint64(uint_value, output, output_len); +} + +static apc_modbus_converter_t _apc_modbus_date_conversion = { _apc_modbus_date_to_nut, _apc_modbus_date_from_nut }; typedef struct { const char *nut_variable_name; size_t modbus_addr; size_t modbus_len; /* Number of uint16_t registers */ apc_modbus_value_types value_type; + apc_modbus_value_flags value_flags; apc_modbus_converter_t *value_converter; const char *value_format; int value_scale; @@ -563,50 +718,73 @@ typedef struct { /* Values that only need to be updated once on startup */ static apc_modbus_register_t apc_modbus_register_map_inventory[] = { - { "ups.firmware", 516, 8, APC_MODBUS_VALUE_TYPE_STRING, NULL, "%s", 0, NULL }, - { "ups.model", 532, 16, APC_MODBUS_VALUE_TYPE_STRING, NULL, "%s", 0, NULL }, /* also device.model, filled automatically */ - { "ups.serial", 564, 8, APC_MODBUS_VALUE_TYPE_STRING, NULL, "%s", 0, NULL }, /* also device.serial, filled automatically */ - { "ups.power.nominal", 588, 1, APC_MODBUS_VALUE_TYPE_UINT, &_apc_modbus_double_conversion, "%.0f", 0, &power_nominal }, - { "ups.realpower.nominal", 589, 1, APC_MODBUS_VALUE_TYPE_UINT, &_apc_modbus_double_conversion, "%.0f", 0, &realpower_nominal }, - { "ups.mfr.date", 591, 1, APC_MODBUS_VALUE_TYPE_UINT, &_apc_modbus_date_conversion, NULL, 0, NULL }, - { "battery.date", 595, 1, APC_MODBUS_VALUE_TYPE_UINT, &_apc_modbus_date_conversion, NULL, 0, NULL }, - { "ups.id", 596, 8, APC_MODBUS_VALUE_TYPE_STRING, NULL, "%s", 0, NULL }, - { NULL, 0, 0, 0, NULL, NULL, 0.0f, NULL } + { "ups.firmware", 516, 8, APC_VT_STRING, 0, NULL, "%s", 0, NULL }, + { "ups.model", 532, 16, APC_VT_STRING, 0, NULL, "%s", 0, NULL }, /* also device.model, filled automatically */ + { "ups.serial", 564, 8, APC_VT_STRING, 0, NULL, "%s", 0, NULL }, /* also device.serial, filled automatically */ + { "ups.power.nominal", 588, 1, APC_VT_UINT, 0, &_apc_modbus_double_conversion, "%.0f", 0, &power_nominal }, + { "ups.realpower.nominal", 589, 1, APC_VT_UINT, 0, &_apc_modbus_double_conversion, "%.0f", 0, &realpower_nominal }, + { "ups.mfr.date", 591, 1, APC_VT_UINT, 0, &_apc_modbus_date_conversion, NULL, 0, NULL }, + { "battery.date", 595, 1, APC_VT_UINT, APC_VF_RW, &_apc_modbus_date_conversion, NULL, 0, NULL }, + { "ups.id", 596, 8, APC_VT_STRING, APC_VF_RW, NULL, "%s", 0, NULL }, + { "outlet.group.0.name", 604, 8, APC_VT_STRING, APC_VF_RW, NULL, "%s", 0, NULL }, + { "outlet.group.1.name", 612, 8, APC_VT_STRING, APC_VF_RW, NULL, "%s", 0, NULL }, + { "outlet.group.2.name", 620, 8, APC_VT_STRING, APC_VF_RW, NULL, "%s", 0, NULL }, + { "outlet.group.3.name", 628, 8, APC_VT_STRING, APC_VF_RW, NULL, "%s", 0, NULL }, + { NULL, 0, 0, 0, 0, NULL, NULL, 0.0f, NULL } }; static apc_modbus_register_t apc_modbus_register_map_status[] = { - { "input.transfer.reason", 2, 1, APC_MODBUS_VALUE_TYPE_UINT, &_apc_modbus_status_change_cause_conversion, NULL, 0, NULL }, - { NULL, 0, 0, 0, NULL, NULL, 0.0f, NULL } + { "input.transfer.reason", 2, 1, APC_VT_UINT, 0, &_apc_modbus_status_change_cause_conversion, NULL, 0, NULL }, + { NULL, 0, 0, 0, 0, NULL, NULL, 0.0f, NULL } }; static apc_modbus_register_t apc_modbus_register_map_dynamic[] = { - { "battery.runtime", 128, 2, APC_MODBUS_VALUE_TYPE_UINT, NULL, "%u", 0, NULL }, - { "battery.charge", 130, 1, APC_MODBUS_VALUE_TYPE_UINT, &_apc_modbus_double_conversion, "%.2f", 9, NULL }, - { "battery.voltage", 131, 1, APC_MODBUS_VALUE_TYPE_INT, &_apc_modbus_double_conversion, "%.2f", 5, NULL }, - { "battery.date.maintenance", 133, 1, APC_MODBUS_VALUE_TYPE_UINT, &_apc_modbus_date_conversion, NULL, 0, NULL }, - { "battery.temperature", 135, 1, APC_MODBUS_VALUE_TYPE_INT, &_apc_modbus_double_conversion, "%.2f", 7, NULL }, - { "ups.load", 136, 1, APC_MODBUS_VALUE_TYPE_UINT, &_apc_modbus_double_conversion, "%.2f", 8, NULL }, - { "ups.realpower", 136, 1, APC_MODBUS_VALUE_TYPE_UINT, &_apc_modbus_power_conversion, "%.2f", 8, &realpower_nominal }, - { "ups.power", 138, 1, APC_MODBUS_VALUE_TYPE_UINT, &_apc_modbus_power_conversion, "%.2f", 8, &power_nominal }, - { "output.current", 140, 1, APC_MODBUS_VALUE_TYPE_UINT, &_apc_modbus_double_conversion, "%.2f", 5, NULL }, - { "output.voltage", 142, 1, APC_MODBUS_VALUE_TYPE_UINT, &_apc_modbus_double_conversion, "%.2f", 6, NULL }, - { "output.frequency", 144, 1, APC_MODBUS_VALUE_TYPE_UINT, &_apc_modbus_double_conversion, "%.2f", 7, NULL }, - { "experimental.output.energy", 145, 2, APC_MODBUS_VALUE_TYPE_UINT, NULL, "%u", 0, NULL }, - { "input.voltage", 151, 1, APC_MODBUS_VALUE_TYPE_UINT, &_apc_modbus_voltage_conversion, "%.2f", 6, NULL }, - { "ups.efficiency", 154, 1, APC_MODBUS_VALUE_TYPE_INT, &_apc_modbus_efficiency_conversion, "%.1f", 7, NULL }, - { "ups.timer.shutdown", 155, 1, APC_MODBUS_VALUE_TYPE_INT, NULL, "%d", 0, NULL }, - { "ups.timer.start", 156, 1, APC_MODBUS_VALUE_TYPE_INT, NULL, "%d", 0, NULL }, - { "ups.timer.reboot", 157, 2, APC_MODBUS_VALUE_TYPE_INT, NULL, "%d", 0, NULL }, - { NULL, 0, 0, 0, NULL, NULL, 0.0f, NULL } + { "battery.runtime", 128, 2, APC_VT_UINT, 0, NULL, "%u", 0, NULL }, + { "battery.charge", 130, 1, APC_VT_UINT, 0, &_apc_modbus_double_conversion, "%.2f", 9, NULL }, + { "battery.voltage", 131, 1, APC_VT_INT, 0, &_apc_modbus_double_conversion, "%.2f", 5, NULL }, + { "battery.date.maintenance", 133, 1, APC_VT_UINT, 0, &_apc_modbus_date_conversion, NULL, 0, NULL }, + { "battery.temperature", 135, 1, APC_VT_INT, 0, &_apc_modbus_double_conversion, "%.2f", 7, NULL }, + { "ups.load", 136, 1, APC_VT_UINT, 0, &_apc_modbus_double_conversion, "%.2f", 8, NULL }, + { "ups.realpower", 136, 1, APC_VT_UINT, 0, &_apc_modbus_power_conversion, "%.2f", 8, &realpower_nominal }, + { "ups.power", 138, 1, APC_VT_UINT, 0, &_apc_modbus_power_conversion, "%.2f", 8, &power_nominal }, + { "output.current", 140, 1, APC_VT_UINT, 0, &_apc_modbus_double_conversion, "%.2f", 5, NULL }, + { "output.voltage", 142, 1, APC_VT_UINT, 0, &_apc_modbus_double_conversion, "%.2f", 6, NULL }, + { "output.frequency", 144, 1, APC_VT_UINT, 0, &_apc_modbus_double_conversion, "%.2f", 7, NULL }, + { "experimental.output.energy", 145, 2, APC_VT_UINT, 0, NULL, "%u", 0, NULL }, + { "input.voltage", 151, 1, APC_VT_UINT, 0, &_apc_modbus_voltage_conversion, "%.2f", 6, NULL }, + { "ups.efficiency", 154, 1, APC_VT_INT, 0, &_apc_modbus_efficiency_conversion, "%.1f", 7, NULL }, + { "ups.timer.shutdown", 155, 1, APC_VT_INT, 0, NULL, "%d", 0, NULL }, + { "ups.timer.start", 156, 1, APC_VT_INT, 0, NULL, "%d", 0, NULL }, + { "ups.timer.reboot", 157, 2, APC_VT_INT, 0, NULL, "%d", 0, NULL }, + { NULL, 0, 0, 0, 0, NULL, NULL, 0.0f, NULL } }; static apc_modbus_register_t apc_modbus_register_map_static[] = { - { "input.transfer.high", 1026, 1, APC_MODBUS_VALUE_TYPE_UINT, NULL, "%u", 0, NULL }, - { "input.transfer.low", 1027, 1, APC_MODBUS_VALUE_TYPE_UINT, NULL, "%u", 0, NULL }, - { "ups.delay.shutdown", 1029, 1, APC_MODBUS_VALUE_TYPE_INT, NULL, "%d", 0, NULL }, - { "ups.delay.start", 1030, 1, APC_MODBUS_VALUE_TYPE_INT, NULL, "%d", 0, NULL }, - { "ups.delay.reboot", 1031, 2, APC_MODBUS_VALUE_TYPE_INT, NULL, "%d", 0, NULL }, - { NULL, 0, 0, 0, NULL, NULL, 0.0f, NULL } + { "input.transfer.high", 1026, 1, APC_VT_UINT, APC_VF_RW, NULL, "%u", 0, NULL }, + { "input.transfer.low", 1027, 1, APC_VT_UINT, APC_VF_RW, NULL, "%u", 0, NULL }, + { "ups.delay.shutdown", 1029, 1, APC_VT_INT, APC_VF_RW, NULL, "%d", 0, NULL }, + { "ups.delay.start", 1030, 1, APC_VT_INT, APC_VF_RW, NULL, "%d", 0, NULL }, + { "ups.delay.reboot", 1031, 2, APC_VT_INT, APC_VF_RW, NULL, "%d", 0, NULL }, + { "outlet.group.0.delay.shutdown", 1029, 1, APC_VT_INT, APC_VF_RW, NULL, "%d", 0, NULL }, + { "outlet.group.0.delay.start", 1030, 1, APC_VT_INT, APC_VF_RW, NULL, "%d", 0, NULL }, + { "outlet.group.0.delay.reboot", 1031, 2, APC_VT_INT, APC_VF_RW, NULL, "%d", 0, NULL }, + { "outlet.group.1.delay.shutdown", 1034, 1, APC_VT_INT, APC_VF_RW, NULL, "%d", 0, NULL }, + { "outlet.group.1.delay.start", 1035, 1, APC_VT_INT, APC_VF_RW, NULL, "%d", 0, NULL }, + { "outlet.group.1.delay.reboot", 1036, 2, APC_VT_INT, APC_VF_RW, NULL, "%d", 0, NULL }, + { "outlet.group.2.delay.shutdown", 1039, 1, APC_VT_INT, APC_VF_RW, NULL, "%d", 0, NULL }, + { "outlet.group.2.delay.start", 1040, 1, APC_VT_INT, APC_VF_RW, NULL, "%d", 0, NULL }, + { "outlet.group.2.delay.reboot", 1041, 2, APC_VT_INT, APC_VF_RW, NULL, "%d", 0, NULL }, + { "outlet.group.3.delay.shutdown", 1044, 1, APC_VT_INT, APC_VF_RW, NULL, "%d", 0, NULL }, + { "outlet.group.3.delay.start", 1045, 1, APC_VT_INT, APC_VF_RW, NULL, "%d", 0, NULL }, + { "outlet.group.3.delay.reboot", 1046, 2, APC_VT_INT, APC_VF_RW, NULL, "%d", 0, NULL }, + { NULL, 0, 0, 0, 0, NULL, NULL, 0.0f, NULL } +}; + +static apc_modbus_register_t* apc_modbus_register_maps[] = { + apc_modbus_register_map_inventory, + apc_modbus_register_map_status, + apc_modbus_register_map_dynamic, + apc_modbus_register_map_static }; static void _apc_modbus_close(int free_modbus) @@ -757,6 +935,7 @@ static int _apc_modbus_update_value(apc_modbus_register_t *regs_info, const uint { apc_modbus_value_t value; char strbuf[33], nutvbuf[128]; + int dstate_flags; if (regs_info == NULL || regs == NULL || regs_len == 0) { /* Invalid parameters */ @@ -770,19 +949,19 @@ static int _apc_modbus_update_value(apc_modbus_register_t *regs_info, const uint assert(regs_info->value_type <= apc_modbus_value_types_max); switch (regs_info->value_type) { - case APC_MODBUS_VALUE_TYPE_STRING: + case APC_VT_STRING: _apc_modbus_to_string(regs, regs_info->modbus_len, strbuf, sizeof(strbuf)); value.data.string_value = strbuf; break; - case APC_MODBUS_VALUE_TYPE_INT: + case APC_VT_INT: _apc_modbus_to_int64(regs, regs_info->modbus_len, &value.data.int_value); break; - case APC_MODBUS_VALUE_TYPE_UINT: + case APC_VT_UINT: _apc_modbus_to_uint64(regs, regs_info->modbus_len, &value.data.uint_value); break; } - if (regs_info->value_converter != NULL) { + if (regs_info->value_converter != NULL && regs_info->value_converter->apc_to_nut != NULL) { /* If we have a converter, use it and set the value as a string */ if (!regs_info->value_converter->apc_to_nut(&value, nutvbuf, sizeof(nutvbuf))) { upslogx(LOG_ERR, "%s: Failed to convert register %" PRIuSIZE ":%" PRIuSIZE, __func__, @@ -799,13 +978,13 @@ static int _apc_modbus_update_value(apc_modbus_register_t *regs_info, const uint #endif assert(regs_info->value_type <= apc_modbus_value_types_max); switch (regs_info->value_type) { - case APC_MODBUS_VALUE_TYPE_STRING: + case APC_VT_STRING: dstate_setinfo(regs_info->nut_variable_name, regs_info->value_format, value.data.string_value); break; - case APC_MODBUS_VALUE_TYPE_INT: + case APC_VT_INT: dstate_setinfo(regs_info->nut_variable_name, regs_info->value_format, value.data.int_value); break; - case APC_MODBUS_VALUE_TYPE_UINT: + case APC_VT_UINT: dstate_setinfo(regs_info->nut_variable_name, regs_info->value_format, value.data.uint_value); break; } @@ -814,6 +993,18 @@ static int _apc_modbus_update_value(apc_modbus_register_t *regs_info, const uint #endif } + dstate_flags = 0; + if (regs_info->value_type == APC_VT_STRING) { + dstate_flags |= ST_FLAG_STRING; + } + if ((regs_info->value_flags & APC_VF_RW)) { + dstate_flags |= ST_FLAG_RW; + } + dstate_setflags(regs_info->nut_variable_name, dstate_flags); + if (regs_info->value_type == APC_VT_STRING) { + dstate_setaux(regs_info->nut_variable_name, regs_info->modbus_len * sizeof(uint16_t)); + } + return 1; } @@ -837,11 +1028,38 @@ static int _apc_modbus_process_registers(apc_modbus_register_t* values, const ui static int _apc_modbus_read_inventory(void) { - uint16_t regbuf[88]; + uint16_t regbuf[120]; + int start_addr; + uint16_t sog_relay_config; + int outlet_group_count; /* Inventory Information */ - if (_apc_modbus_read_registers(modbus_ctx, 516, 88, regbuf)) { - _apc_modbus_process_registers(apc_modbus_register_map_inventory, regbuf, 88, 516); + start_addr = apc_modbus_register_map_inventory[0].modbus_addr; + if (_apc_modbus_read_registers(modbus_ctx, start_addr, SIZEOF_ARRAY(regbuf), regbuf)) { + sog_relay_config = regbuf[APC_MODBUS_SOGRELAYCONFIGSETTING_BF_REG - start_addr]; + + outlet_group_count = 0; + if ((sog_relay_config & APC_MODBUS_SOGRELAYCONFIGSETTING_BF_MOG_PRESENT)) { + outlet_group_count++; + } + if ((sog_relay_config & APC_MODBUS_SOGRELAYCONFIGSETTING_BF_SOG_0_PRESENT)) { + outlet_group_count++; + } + if ((sog_relay_config & APC_MODBUS_SOGRELAYCONFIGSETTING_BF_SOG_1_PRESENT)) { + outlet_group_count++; + } + if ((sog_relay_config & APC_MODBUS_SOGRELAYCONFIGSETTING_BF_SOG_2_PRESENT)) { + outlet_group_count++; + } + /* Documentation says there is a bit for SOG3, but everything else does not have it */ + if ((sog_relay_config & APC_MODBUS_SOGRELAYCONFIGSETTING_BF_SOG_3_PRESENT)) { + upslogx(LOG_WARNING, "%s: SOG3 present, but we don't know how to use it", __func__); + outlet_group_count++; + } + + dstate_setinfo("outlet.group.count", "%d", outlet_group_count); + + _apc_modbus_process_registers(apc_modbus_register_map_inventory, regbuf, SIZEOF_ARRAY(regbuf), start_addr); } else { return 0; } @@ -849,8 +1067,185 @@ static int _apc_modbus_read_inventory(void) return 1; } +static int _apc_modbus_setvar(const char *nut_varname, const char *str_value) +{ + size_t mi, i; + int addr, nb, r; + apc_modbus_register_t *apc_map = NULL, *apc_value = NULL; + uint16_t reg_value[16]; + + for (mi = 0; mi < SIZEOF_ARRAY(apc_modbus_register_maps); mi++) { + apc_map = apc_modbus_register_maps[mi]; + + for (i = 0; apc_map[i].nut_variable_name; i++) { + if (!strcasecmp(nut_varname, apc_map[i].nut_variable_name)) { + apc_value = &apc_map[i]; + break; + } + } + } + + if (!apc_map || !apc_value) { + upslogx(LOG_WARNING, "%s: [%s] is unknown", __func__, nut_varname); + return STAT_SET_UNKNOWN; + } + + if (!(apc_value->value_flags & APC_VF_RW)) { + upslogx(LOG_WARNING, "%s: [%s] is not writable", __func__, nut_varname); + return STAT_SET_INVALID; + } + + assert(apc_value->modbus_len < SIZEOF_ARRAY(reg_value)); + + if (apc_value->value_converter && apc_value->value_converter->nut_to_apc) { + if (!apc_value->value_converter->nut_to_apc(str_value, reg_value, apc_value->modbus_len)) { + upslogx(LOG_WARNING, "%s: [%s] failed to convert value", __func__, nut_varname); + return STAT_SET_CONVERSION_FAILED; + } + } else { + assert(apc_value->value_type <= apc_modbus_value_types_max); + switch (apc_value->value_type) { + case APC_VT_STRING: + r = _apc_modbus_from_string(str_value, reg_value, apc_value->modbus_len); + break; + case APC_VT_INT: + r = _apc_modbus_from_int64_string(str_value, reg_value, apc_value->modbus_len); + break; + case APC_VT_UINT: + r = _apc_modbus_from_uint64_string(str_value, reg_value, apc_value->modbus_len); + break; + } + + if (!r) { + upslogx(LOG_WARNING, "%s: [%s] failed to convert value", __func__, nut_varname); + return STAT_SET_CONVERSION_FAILED; + } + } + + addr = apc_value->modbus_addr; + nb = apc_value->modbus_len; + if (modbus_write_registers(modbus_ctx, addr, nb, reg_value) < 0) { + upslogx(LOG_ERR, "%s: Write of %d:%d failed: %s (%s)", __func__, addr, addr + nb, modbus_strerror(errno), device_path); + _apc_modbus_handle_error(modbus_ctx); + return STAT_SET_FAILED; + } + + /* There seem to be some communication problems if we don't wait after writing. + * Maybe there is some register we need to poll for write completion? + */ + usleep(100000); + + upslogx(LOG_INFO, "SET %s='%s'", nut_varname, str_value); + + if (_apc_modbus_read_registers(modbus_ctx, addr, nb, reg_value)) { + _apc_modbus_process_registers(apc_map, reg_value, nb, addr); + } + + return STAT_SET_HANDLED; +} + +typedef struct { + const char *nut_command_name; + size_t modbus_addr; + size_t modbus_len; /* Number of uint16_t registers */ + uint64_t value; +} apc_modbus_command_t; + +static apc_modbus_command_t apc_modbus_command_map[] = { + { "test.battery.start", APC_MODBUS_REPLACEBATTERYTESTCOMMAND_BF_REG, 1, APC_MODBUS_REPLACEBATTERYTESTCOMMAND_BF_START }, + { "test.battery.stop", APC_MODBUS_REPLACEBATTERYTESTCOMMAND_BF_REG, 1, APC_MODBUS_REPLACEBATTERYTESTCOMMAND_BF_ABORT }, + { "test.panel.start", APC_MODBUS_USERINTERFACECOMMAND_BF_REG, 1, APC_MODBUS_USERINTERFACECOMMAND_BF_SHORT_TEST }, + { "calibrate.start", APC_MODBUS_RUNTIMECALIBRATIONCOMMAND_BF_REG, 1, APC_MODBUS_RUNTIMECALIBRATIONCOMMAND_BF_START }, + { "calibrate.stop", APC_MODBUS_RUNTIMECALIBRATIONCOMMAND_BF_REG, 1, APC_MODBUS_RUNTIMECALIBRATIONCOMMAND_BF_ABORT }, + { "bypass.start", APC_MODBUS_UPSCOMMAND_BF_REG, 2, APC_MODBUS_UPSCOMMAND_BF_OUTPUT_INTO_BYPASS }, + { "bypass.stop", APC_MODBUS_UPSCOMMAND_BF_REG, 2, APC_MODBUS_UPSCOMMAND_BF_OUTPUT_OUT_OF_BYPASS }, + { "load.off", APC_MODBUS_OUTLETCOMMAND_BF_REG, 2, APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_OFF }, + { "load.on", APC_MODBUS_OUTLETCOMMAND_BF_REG, 2, APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_ON }, + { "load.off.delay", APC_MODBUS_OUTLETCOMMAND_BF_REG, 2, APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_OFF | APC_MODBUS_OUTLETCOMMAND_BF_MOD_USE_OFF_DELAY }, + { "load.on.delay", APC_MODBUS_OUTLETCOMMAND_BF_REG, 2, APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_ON | APC_MODBUS_OUTLETCOMMAND_BF_MOD_USE_ON_DELAY }, + { "shutdown.return", APC_MODBUS_OUTLETCOMMAND_BF_REG, 2, APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_SHUTDOWN | APC_MODBUS_OUTLETCOMMAND_BF_MOD_USE_OFF_DELAY }, + { "shutdown.stayoff", APC_MODBUS_OUTLETCOMMAND_BF_REG, 2, APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_OFF | APC_MODBUS_OUTLETCOMMAND_BF_MOD_USE_OFF_DELAY }, + { "shutdown.reboot", APC_MODBUS_OUTLETCOMMAND_BF_REG, 2, APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_REBOOT }, + { "shutdown.reboot.graceful", APC_MODBUS_OUTLETCOMMAND_BF_REG, 2, APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_REBOOT | APC_MODBUS_OUTLETCOMMAND_BF_MOD_USE_OFF_DELAY }, + { "beeper.mute", APC_MODBUS_USERINTERFACECOMMAND_BF_REG, 1, APC_MODBUS_USERINTERFACECOMMAND_BF_MUTE_ALL_ACTIVE_AUDIBLE_ALARMS }, + { "outlet.0.shutdown.return", APC_MODBUS_OUTLETCOMMAND_BF_REG, 2, + APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_SHUTDOWN | APC_MODBUS_OUTLETCOMMAND_BF_TARGET_MAIN_OUTLET_GROUP | APC_MODBUS_OUTLETCOMMAND_BF_MOD_USE_OFF_DELAY }, + { "outlet.0.load.off", APC_MODBUS_OUTLETCOMMAND_BF_REG, 2, + APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_OFF | APC_MODBUS_OUTLETCOMMAND_BF_TARGET_MAIN_OUTLET_GROUP }, + { "outlet.0.load.on", APC_MODBUS_OUTLETCOMMAND_BF_REG, 2, + APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_ON | APC_MODBUS_OUTLETCOMMAND_BF_TARGET_MAIN_OUTLET_GROUP }, + { "outlet.0.load.cycle", APC_MODBUS_OUTLETCOMMAND_BF_REG, 2, + APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_REBOOT | APC_MODBUS_OUTLETCOMMAND_BF_TARGET_MAIN_OUTLET_GROUP }, + { "outlet.1.shutdown.return", APC_MODBUS_OUTLETCOMMAND_BF_REG, 2, + APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_SHUTDOWN | APC_MODBUS_OUTLETCOMMAND_BF_TARGET_SWITCHED_OUTLET_GROUP_0 | APC_MODBUS_OUTLETCOMMAND_BF_MOD_USE_OFF_DELAY }, + { "outlet.1.load.off", APC_MODBUS_OUTLETCOMMAND_BF_REG, 2, + APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_OFF | APC_MODBUS_OUTLETCOMMAND_BF_TARGET_SWITCHED_OUTLET_GROUP_0 }, + { "outlet.1.load.on", APC_MODBUS_OUTLETCOMMAND_BF_REG, 2, + APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_ON | APC_MODBUS_OUTLETCOMMAND_BF_TARGET_SWITCHED_OUTLET_GROUP_0 }, + { "outlet.1.load.cycle", APC_MODBUS_OUTLETCOMMAND_BF_REG, 2, + APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_REBOOT | APC_MODBUS_OUTLETCOMMAND_BF_TARGET_SWITCHED_OUTLET_GROUP_0 }, + { "outlet.2.shutdown.return", APC_MODBUS_OUTLETCOMMAND_BF_REG, 2, + APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_SHUTDOWN | APC_MODBUS_OUTLETCOMMAND_BF_TARGET_SWITCHED_OUTLET_GROUP_1 | APC_MODBUS_OUTLETCOMMAND_BF_MOD_USE_OFF_DELAY }, + { "outlet.2.load.off", APC_MODBUS_OUTLETCOMMAND_BF_REG, 2, + APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_OFF | APC_MODBUS_OUTLETCOMMAND_BF_TARGET_SWITCHED_OUTLET_GROUP_1 }, + { "outlet.2.load.on", APC_MODBUS_OUTLETCOMMAND_BF_REG, 2, + APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_ON | APC_MODBUS_OUTLETCOMMAND_BF_TARGET_SWITCHED_OUTLET_GROUP_1 }, + { "outlet.2.load.cycle", APC_MODBUS_OUTLETCOMMAND_BF_REG, 2, + APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_REBOOT | APC_MODBUS_OUTLETCOMMAND_BF_TARGET_SWITCHED_OUTLET_GROUP_1 }, + { "outlet.3.shutdown.return", APC_MODBUS_OUTLETCOMMAND_BF_REG, 2, + APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_SHUTDOWN | APC_MODBUS_OUTLETCOMMAND_BF_TARGET_SWITCHED_OUTLET_GROUP_2 | APC_MODBUS_OUTLETCOMMAND_BF_MOD_USE_OFF_DELAY }, + { "outlet.3.load.off", APC_MODBUS_OUTLETCOMMAND_BF_REG, 2, + APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_OFF | APC_MODBUS_OUTLETCOMMAND_BF_TARGET_SWITCHED_OUTLET_GROUP_2 }, + { "outlet.3.load.on", APC_MODBUS_OUTLETCOMMAND_BF_REG, 2, + APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_ON | APC_MODBUS_OUTLETCOMMAND_BF_TARGET_SWITCHED_OUTLET_GROUP_2 }, + { "outlet.3.load.cycle", APC_MODBUS_OUTLETCOMMAND_BF_REG, 2, + APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_REBOOT | APC_MODBUS_OUTLETCOMMAND_BF_TARGET_SWITCHED_OUTLET_GROUP_2 }, + { NULL, 0, 0, 0 } +}; + +static int _apc_modbus_instcmd(const char *nut_cmdname, const char *extra) +{ + size_t i; + int addr, nb; + apc_modbus_command_t *apc_command = NULL; + uint16_t value[4]; /* Max 64-bit */ + + NUT_UNUSED_VARIABLE(extra); + + for (i = 0; apc_modbus_command_map[i].nut_command_name; i++) { + if (!strcasecmp(nut_cmdname, apc_modbus_command_map[i].nut_command_name)) { + apc_command = &apc_modbus_command_map[i]; + break; + } + } + + if (!apc_command) { + upslogx(LOG_WARNING, "%s: [%s] is unknown", __func__, nut_cmdname); + return STAT_INSTCMD_UNKNOWN; + } + + assert(apc_command->modbus_len <= SIZEOF_ARRAY(value)); + + if (!_apc_modbus_from_uint64(apc_command->value, value, apc_command->modbus_len)) { + upslogx(LOG_WARNING, "%s: [%s] failed to convert value", __func__, nut_cmdname); + return STAT_INSTCMD_CONVERSION_FAILED; + } + + addr = apc_command->modbus_addr; + nb = apc_command->modbus_len; + if (modbus_write_registers(modbus_ctx, addr, nb, value) < 0) { + upslogx(LOG_ERR, "%s: Write of %d:%d failed: %s (%s)", __func__, addr, addr + nb, modbus_strerror(errno), device_path); + _apc_modbus_handle_error(modbus_ctx); + return STAT_INSTCMD_FAILED; + } + + return STAT_INSTCMD_HANDLED; +} + void upsdrv_initinfo(void) { + size_t i; + if (!_apc_modbus_read_inventory()) { fatalx(EXIT_FAILURE, "Can't read inventory information from the UPS"); } @@ -858,6 +1253,14 @@ void upsdrv_initinfo(void) dstate_setinfo("ups.mfr", "American Power Conversion"); /* also device.mfr, filled automatically */ dstate_setinfo("device.type", "ups"); + + for (i = 0; apc_modbus_command_map[i].nut_command_name; i++) { + dstate_addcmd(apc_modbus_command_map[i].nut_command_name); + } + + upsh.setvar = _apc_modbus_setvar; + upsh.instcmd = _apc_modbus_instcmd; + } void upsdrv_updateinfo(void) @@ -950,8 +1353,8 @@ void upsdrv_updateinfo(void) } /* Static Data */ - if (_apc_modbus_read_registers(modbus_ctx, 1026, 7, regbuf)) { - _apc_modbus_process_registers(apc_modbus_register_map_static, regbuf, 7, 1026); + if (_apc_modbus_read_registers(modbus_ctx, 1026, 22, regbuf)) { + _apc_modbus_process_registers(apc_modbus_register_map_static, regbuf, 22, 1026); } else { dstate_datastale(); return; @@ -964,9 +1367,7 @@ void upsdrv_updateinfo(void) void upsdrv_shutdown(void) { - /* TODO: replace with a proper shutdown function */ - upslogx(LOG_ERR, "shutdown not supported"); - set_exit_flag(-1); + modbus_write_register(modbus_ctx, APC_MODBUS_OUTLETCOMMAND_BF_REG, APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_SHUTDOWN); } void upsdrv_help(void) @@ -1341,6 +1742,9 @@ void upsdrv_initups(void) } #if defined NUT_MODBUS_HAS_USB + /* This creates an exact matcher after the first connection so that on + * reconnect we are more likely to match the exact device we connected to + * the first time. */ _apc_modbus_create_reopen_matcher(); #endif /* defined NUT_MODBUS_HAS_USB */ diff --git a/drivers/apc_modbus.h b/drivers/apc_modbus.h new file mode 100644 index 0000000000..d537b7ca00 --- /dev/null +++ b/drivers/apc_modbus.h @@ -0,0 +1,78 @@ +/* apc_modbus.h - Driver for APC Modbus UPS + * Copyright © 2023 Axel Gembe + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#ifndef APC_MODBUS_H +#define APC_MODBUS_H + +#define APC_MODBUS_SOGRELAYCONFIGSETTING_BF_REG 590 +#define APC_MODBUS_SOGRELAYCONFIGSETTING_BF_MOG_PRESENT (1 << 0) +#define APC_MODBUS_SOGRELAYCONFIGSETTING_BF_SOG_0_PRESENT (1 << 1) +#define APC_MODBUS_SOGRELAYCONFIGSETTING_BF_SOG_1_PRESENT (1 << 2) +#define APC_MODBUS_SOGRELAYCONFIGSETTING_BF_SOG_2_PRESENT (1 << 3) +#define APC_MODBUS_SOGRELAYCONFIGSETTING_BF_SOG_3_PRESENT (1 << 4) + +#define APC_MODBUS_UPSCOMMAND_BF_REG 1536 +/* 0 - 2 are reserved */ +#define APC_MODBUS_UPSCOMMAND_BF_RESTORE_FACTORY_SETTINGS (1 << 3) +#define APC_MODBUS_UPSCOMMAND_BF_OUTPUT_INTO_BYPASS (1 << 4) +#define APC_MODBUS_UPSCOMMAND_BF_OUTPUT_OUT_OF_BYPASS (1 << 5) +/* 6 - 8 are reserved */ +#define APC_MODBUS_UPSCOMMAND_BF_CLEAR_FAULTS (1 << 9) +/* 10 - 12 are reserved */ +#define APC_MODBUS_UPSCOMMAND_BF_RESET_STRINGS (1 << 13) +#define APC_MODBUS_UPSCOMMAND_BF_RESET_LOGS (1 << 14) + +#define APC_MODBUS_OUTLETCOMMAND_BF_REG 1538 +#define APC_MODBUS_OUTLETCOMMAND_BF_CMD_CANCEL (1 << 0) +#define APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_ON (1 << 1) +#define APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_OFF (1 << 2) +#define APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_SHUTDOWN (1 << 3) +#define APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_REBOOT (1 << 4) +#define APC_MODBUS_OUTLETCOMMAND_BF_MOD_COLD_BOOT_ALLOWED (1 << 5) +#define APC_MODBUS_OUTLETCOMMAND_BF_MOD_USE_ON_DELAY (1 << 6) +#define APC_MODBUS_OUTLETCOMMAND_BF_MOD_USE_OFF_DELAY (1 << 7) +#define APC_MODBUS_OUTLETCOMMAND_BF_TARGET_MAIN_OUTLET_GROUP (1 << 8) +#define APC_MODBUS_OUTLETCOMMAND_BF_TARGET_SWITCHED_OUTLET_GROUP_0 (1 << 9) +#define APC_MODBUS_OUTLETCOMMAND_BF_TARGET_SWITCHED_OUTLET_GROUP_1 (1 << 10) +#define APC_MODBUS_OUTLETCOMMAND_BF_TARGET_SWITCHED_OUTLET_GROUP_2 (1 << 11) +#define APC_MODBUS_OUTLETCOMMAND_BF_SOURCE_USB_PORT (1 << 12) +#define APC_MODBUS_OUTLETCOMMAND_BF_SOURCE_LOCAL_USER (1 << 13) +#define APC_MODBUS_OUTLETCOMMAND_BF_SOURCE_RJ45_PORT (1 << 14) +#define APC_MODBUS_OUTLETCOMMAND_BF_SOURCE_SMART_SLOT_1 (1 << 15) +#define APC_MODBUS_OUTLETCOMMAND_BF_SOURCE_SMART_SLOT_2 (1 << 16) +#define APC_MODBUS_OUTLETCOMMAND_BF_SOURCE_INTERNAL_NETWORK_1 (1 << 17) +#define APC_MODBUS_OUTLETCOMMAND_BF_SOURCE_INTERNAL_NETWORK_2 (1 << 18) + +#define APC_MODBUS_REPLACEBATTERYTESTCOMMAND_BF_REG 1541 +#define APC_MODBUS_REPLACEBATTERYTESTCOMMAND_BF_START (1 << 0) +#define APC_MODBUS_REPLACEBATTERYTESTCOMMAND_BF_ABORT (1 << 1) + +#define APC_MODBUS_RUNTIMECALIBRATIONCOMMAND_BF_REG 1542 +#define APC_MODBUS_RUNTIMECALIBRATIONCOMMAND_BF_START (1 << 0) +#define APC_MODBUS_RUNTIMECALIBRATIONCOMMAND_BF_ABORT (1 << 1) + +#define APC_MODBUS_USERINTERFACECOMMAND_BF_REG 1543 +#define APC_MODBUS_USERINTERFACECOMMAND_BF_SHORT_TEST (1 << 0) +#define APC_MODBUS_USERINTERFACECOMMAND_BF_CONTINUOUS_TEST (1 << 1) +#define APC_MODBUS_USERINTERFACECOMMAND_BF_MUTE_ALL_ACTIVE_AUDIBLE_ALARMS (1 << 2) +#define APC_MODBUS_USERINTERFACECOMMAND_BF_CANCEL_MUTE (1 << 3) +/* 4 is reserved */ +#define APC_MODBUS_USERINTERFACECOMMAND_BF_ACKNOWLEDGE_BATTERY_ALARMS (1 << 5) +#define APC_MODBUS_USERINTERFACECOMMAND_BF_ACKNOWLEDGE_SITE_WIRING_ALARM (1 << 6) + +#endif /* APC_MODBUS_H */ diff --git a/drivers/upshandler.h b/drivers/upshandler.h index fea10bc20c..78e6ffef0a 100644 --- a/drivers/upshandler.h +++ b/drivers/upshandler.h @@ -22,18 +22,20 @@ /* return values for instcmd */ enum { - STAT_INSTCMD_HANDLED = 0, /* completed successfully */ - STAT_INSTCMD_UNKNOWN, /* unspecified error */ - STAT_INSTCMD_INVALID, /* invalid command */ - STAT_INSTCMD_FAILED /* command failed */ + STAT_INSTCMD_HANDLED = 0, /* completed successfully */ + STAT_INSTCMD_UNKNOWN, /* unspecified error */ + STAT_INSTCMD_INVALID, /* invalid command */ + STAT_INSTCMD_FAILED, /* command failed */ + STAT_INSTCMD_CONVERSION_FAILED /* could not convert value */ }; /* return values for setvar */ enum { - STAT_SET_HANDLED = 0, /* completed successfully */ - STAT_SET_UNKNOWN, /* unspecified error */ - STAT_SET_INVALID, /* not writeable */ - STAT_SET_FAILED /* writing failed */ + STAT_SET_HANDLED = 0, /* completed successfully */ + STAT_SET_UNKNOWN, /* unspecified error */ + STAT_SET_INVALID, /* not writeable */ + STAT_SET_FAILED, /* writing failed */ + STAT_SET_CONVERSION_FAILED /* could not convert value from string */ }; /* structure for funcs that get called by msg parse routine */ From 16ce37009a904e9a5d05b7f10e2e79bd079155b7 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 16 Nov 2023 09:04:50 +0100 Subject: [PATCH 2/7] drivers/Makefile.am: dist the apc_modbus.h file Signed-off-by: Jim Klimov --- drivers/Makefile.am | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/drivers/Makefile.am b/drivers/Makefile.am index f6c369dd6d..03efb37bf6 100644 --- a/drivers/Makefile.am +++ b/drivers/Makefile.am @@ -343,7 +343,8 @@ nutdrv_qx_SOURCES += $(NUTDRV_QX_SUBDRIVERS) # tracking (which is automatic), but to ensure these files are # distributed by "make dist". -dist_noinst_HEADERS = apc-mib.h apc-iem-mib.h apc-hid.h arduino-hid.h baytech-mib.h bcmxcp.h bcmxcp_ser.h \ +dist_noinst_HEADERS = \ + apc_modbus.h apc-mib.h apc-iem-mib.h apc-hid.h arduino-hid.h baytech-mib.h bcmxcp.h bcmxcp_ser.h \ bcmxcp_io.h belkin.h belkin-hid.h bestpower-mib.h blazer.h cps-hid.h dstate.h \ dummy-ups.h explore-hid.h gamatronic.h genericups.h \ generic_gpio_common.h generic_gpio_libgpiod.h \ From 8ee9cbca34c4202df76b7746ca55e75a6cae10d1 Mon Sep 17 00:00:00 2001 From: Axel Gembe Date: Thu, 23 Nov 2023 00:56:09 +0700 Subject: [PATCH 3/7] apc_modbus: Add ups.test.result This outputs the test result, the source of the start of the test and a result modifier. Signed-off-by: Axel Gembe --- drivers/apc_modbus.c | 89 ++++++++++++++++++++++++++++++++++++++++++++ drivers/apc_modbus.h | 13 +++++++ 2 files changed, 102 insertions(+) diff --git a/drivers/apc_modbus.c b/drivers/apc_modbus.c index 27ffe244e0..1974f12ab9 100644 --- a/drivers/apc_modbus.c +++ b/drivers/apc_modbus.c @@ -654,6 +654,94 @@ static int _apc_modbus_status_change_cause_to_nut(const apc_modbus_value_t *valu static apc_modbus_converter_t _apc_modbus_status_change_cause_conversion = { _apc_modbus_status_change_cause_to_nut, NULL }; +static int _apc_modbus_string_join(const char *values[], size_t values_len, const char *separator, char *output, size_t output_len) +{ + size_t i; + size_t output_idx; + int res; + + if (values == NULL || values_len == 0 || separator == NULL || output == NULL || output_len == 0) { + /* Invalid parameters */ + return 0; + } + + output_idx = 0; + + for (i = 0; i < values_len && output_idx < output_len; i++) { + if (values[i] == NULL) + continue; + + if (i == 0) { + res = snprintf(output + output_idx, output_len - output_idx, "%s", values[i]); + } else { + res = snprintf(output + output_idx, output_len - output_idx, "%s%s", separator, values[i]); + } + + if (res < 0 || (size_t)res >= output_len) { + return 0; + } + + output_idx += res; + } + + return 1; +} + +static int _apc_modbus_battery_test_status_to_nut(const apc_modbus_value_t *value, char *output, size_t output_len) +{ + const char *result, *source, *modifier; + const char *values[3]; + + if (value == NULL || output == NULL || output_len == 0) { + /* Invalid parameters */ + return 0; + } + + if (value->type != APC_VT_UINT) { + return 0; + } + + result = NULL; + if ((value->data.uint_value & APC_MODBUS_REPLACEBATTERYTESTSTATUS_BF_PENDING)) { + result = "Pending"; + } else if ((value->data.uint_value & APC_MODBUS_REPLACEBATTERYTESTSTATUS_BF_INPROGRESS)) { + result = "InProgress"; + } else if ((value->data.uint_value & APC_MODBUS_REPLACEBATTERYTESTSTATUS_BF_PASSED)) { + result = "Passed"; + } else if ((value->data.uint_value & APC_MODBUS_REPLACEBATTERYTESTSTATUS_BF_FAILED)) { + result = "Failed"; + } else if ((value->data.uint_value & APC_MODBUS_REPLACEBATTERYTESTSTATUS_BF_REFUSED)) { + result = "Refused"; + } else if ((value->data.uint_value & APC_MODBUS_REPLACEBATTERYTESTSTATUS_BF_ABORTED)) { + result = "Aborted"; + } + + source = NULL; + if ((value->data.uint_value & APC_MODBUS_REPLACEBATTERYTESTSTATUS_BF_SOURCE_PROTOCOL)) { + source = "Source: Protocol"; + } else if ((value->data.uint_value & APC_MODBUS_REPLACEBATTERYTESTSTATUS_BF_SOURCE_LOCALUI)) { + source = "Source: LocalUI"; + } else if ((value->data.uint_value & APC_MODBUS_REPLACEBATTERYTESTSTATUS_BF_SOURCE_INTERNAL)) { + source = "Source: Internal"; + } + + modifier = NULL; + if ((value->data.uint_value & APC_MODBUS_REPLACEBATTERYTESTSTATUS_BF_MOD_INVALIDSTATE)) { + modifier = "Modifier: InvalidState"; + } else if ((value->data.uint_value & APC_MODBUS_REPLACEBATTERYTESTSTATUS_BF_MOD_INTERNALFAULT)) { + modifier = "Modifier: InternalFault"; + } else if ((value->data.uint_value & APC_MODBUS_REPLACEBATTERYTESTSTATUS_BF_MOD_STATEOFCHARGENOTACCEPTABLE)) { + modifier = "Modifier: StateOfChargeNotAcceptable"; + } + + values[0] = result; + values[1] = source; + values[2] = modifier; + return _apc_modbus_string_join(values, SIZEOF_ARRAY(values), ", ", output, output_len); +} + +static apc_modbus_converter_t _apc_modbus_battery_test_status_conversion = { _apc_modbus_battery_test_status_to_nut, NULL }; + static const time_t apc_date_start_offset = 946684800; /* 2000-01-01 00:00 */ static int _apc_modbus_date_to_nut(const apc_modbus_value_t *value, char *output, size_t output_len) @@ -735,6 +823,7 @@ static apc_modbus_register_t apc_modbus_register_map_inventory[] = { static apc_modbus_register_t apc_modbus_register_map_status[] = { { "input.transfer.reason", 2, 1, APC_VT_UINT, 0, &_apc_modbus_status_change_cause_conversion, NULL, 0, NULL }, + { "ups.test.result", 23, 1, APC_VT_UINT, 0, &_apc_modbus_battery_test_status_conversion, NULL, 0, NULL }, { NULL, 0, 0, 0, 0, NULL, NULL, 0.0f, NULL } }; diff --git a/drivers/apc_modbus.h b/drivers/apc_modbus.h index d537b7ca00..d175eb4974 100644 --- a/drivers/apc_modbus.h +++ b/drivers/apc_modbus.h @@ -19,6 +19,19 @@ #ifndef APC_MODBUS_H #define APC_MODBUS_H +#define APC_MODBUS_REPLACEBATTERYTESTSTATUS_BF_PENDING (1 << 0) +#define APC_MODBUS_REPLACEBATTERYTESTSTATUS_BF_INPROGRESS (1 << 1) +#define APC_MODBUS_REPLACEBATTERYTESTSTATUS_BF_PASSED (1 << 2) +#define APC_MODBUS_REPLACEBATTERYTESTSTATUS_BF_FAILED (1 << 3) +#define APC_MODBUS_REPLACEBATTERYTESTSTATUS_BF_REFUSED (1 << 4) +#define APC_MODBUS_REPLACEBATTERYTESTSTATUS_BF_ABORTED (1 << 5) +#define APC_MODBUS_REPLACEBATTERYTESTSTATUS_BF_SOURCE_PROTOCOL (1 << 6) +#define APC_MODBUS_REPLACEBATTERYTESTSTATUS_BF_SOURCE_LOCALUI (1 << 7) +#define APC_MODBUS_REPLACEBATTERYTESTSTATUS_BF_SOURCE_INTERNAL (1 << 8) +#define APC_MODBUS_REPLACEBATTERYTESTSTATUS_BF_MOD_INVALIDSTATE (1 << 9) +#define APC_MODBUS_REPLACEBATTERYTESTSTATUS_BF_MOD_INTERNALFAULT (1 << 10) +#define APC_MODBUS_REPLACEBATTERYTESTSTATUS_BF_MOD_STATEOFCHARGENOTACCEPTABLE (1 << 11) + #define APC_MODBUS_SOGRELAYCONFIGSETTING_BF_REG 590 #define APC_MODBUS_SOGRELAYCONFIGSETTING_BF_MOG_PRESENT (1 << 0) #define APC_MODBUS_SOGRELAYCONFIGSETTING_BF_SOG_0_PRESENT (1 << 1) From 0df06e91bfa3c825e9d6cb2c886158a42c7b9c69 Mon Sep 17 00:00:00 2001 From: Axel Gembe Date: Thu, 23 Nov 2023 02:04:09 +0700 Subject: [PATCH 4/7] apc_modbus: Fix re-read of values at the end of setvar The for loop searching for the correct register map kept on looping after finding the value which caused the `apc_map` mariable to potentially point to the wrong register map. This can cause the re-read of the set value at the end of the function to fail. Signed-off-by: Axel Gembe --- drivers/apc_modbus.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/apc_modbus.c b/drivers/apc_modbus.c index 1974f12ab9..1831a145f6 100644 --- a/drivers/apc_modbus.c +++ b/drivers/apc_modbus.c @@ -1163,7 +1163,7 @@ static int _apc_modbus_setvar(const char *nut_varname, const char *str_value) apc_modbus_register_t *apc_map = NULL, *apc_value = NULL; uint16_t reg_value[16]; - for (mi = 0; mi < SIZEOF_ARRAY(apc_modbus_register_maps); mi++) { + for (mi = 0; mi < SIZEOF_ARRAY(apc_modbus_register_maps) && apc_value == NULL; mi++) { apc_map = apc_modbus_register_maps[mi]; for (i = 0; apc_map[i].nut_variable_name; i++) { From b66881ac58926bbb80c59475eac7c93be32ad7ac Mon Sep 17 00:00:00 2001 From: Axel Gembe Date: Fri, 24 Nov 2023 14:03:14 +0700 Subject: [PATCH 5/7] build: Add a fallback for timegm on Windows / MINGW MINGW headers do not provide a `timegm` implementation but there is a `_mkgmtime` which does the same: https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/mkgmtime-mkgmtime32-mkgmtime64 Signed-off-by: Axel Gembe --- configure.ac | 10 +++++++++- include/timehead.h | 8 ++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index 7013806812..b038ffd803 100644 --- a/configure.ac +++ b/configure.ac @@ -752,7 +752,7 @@ AC_CHECK_FUNCS(strtok_r fileno sigemptyset sigaction, dnl For these we have a fallback implementation via the other, dnl if at least one is available, so initial check is quiet. dnl This typically pops up in POSIX vs. Windows builds: -AC_CHECK_FUNCS(localtime_r localtime_s gmtime_r gmtime_s, +AC_CHECK_FUNCS(localtime_r localtime_s gmtime_r gmtime_s timegm _mkgmtime, [], []) AC_MSG_CHECKING([for at least one gmtime implementation]) @@ -771,6 +771,14 @@ AS_IF([test x"${ac_cv_func_localtime_s}-${ac_cv_func_localtime_r}" = "xno-no"], AC_MSG_RESULT([yes]) ]) +AC_MSG_CHECKING([for at least one timegm implementation]) +AS_IF([test x"${ac_cv_func_timegm}-${ac_cv_func__mkgmtime}" = "xno-no"], [ + AC_MSG_RESULT([no]) + AC_MSG_WARN([Required C library routine timegm nor _mkgmtime was not found]) + ],[ + AC_MSG_RESULT([yes]) + ]) + AC_LANG_PUSH([C]) AC_CHECK_HEADER([string.h], diff --git a/include/timehead.h b/include/timehead.h index 7a5d989a7d..ed36d1b168 100644 --- a/include/timehead.h +++ b/include/timehead.h @@ -73,6 +73,14 @@ static inline struct tm *gmtime_r( const time_t *timer, struct tm *buf ) { # endif #endif +#ifndef HAVE_TIMEGM +# ifdef HAVE__MKGMTIME +# define timegm(tm) _mkgmtime(tm) +# else +# error "No fallback implementation for timegm" +# endif +#endif + #ifdef __cplusplus /* *INDENT-OFF* */ } From 15948981e5e6c1c32962bd763bcd95116b165f8d Mon Sep 17 00:00:00 2001 From: Axel Gembe Date: Thu, 23 Nov 2023 02:05:45 +0700 Subject: [PATCH 6/7] apc_modbus: Always store times in UTC This changes `_apc_modbus_date_from_nut` to use `timegm` instead of `mktime` which does not add the current time zone information. This fixes dates that are off-by-one compared to what was set. Signed-off-by: Axel Gembe --- drivers/apc_modbus.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/drivers/apc_modbus.c b/drivers/apc_modbus.c index 1831a145f6..9939794ff5 100644 --- a/drivers/apc_modbus.c +++ b/drivers/apc_modbus.c @@ -746,7 +746,7 @@ static const time_t apc_date_start_offset = 946684800; /* 2000-01-01 00:00 */ static int _apc_modbus_date_to_nut(const apc_modbus_value_t *value, char *output, size_t output_len) { - struct tm *tm_info; + struct tm tm_info; time_t time_stamp; if (value == NULL || output == NULL || output_len == 0) { @@ -759,8 +759,8 @@ static int _apc_modbus_date_to_nut(const apc_modbus_value_t *value, char *output } time_stamp = ((int64_t)value->data.uint_value * 86400) + apc_date_start_offset; - tm_info = gmtime(&time_stamp); - strftime(output, output_len, "%Y-%m-%d", tm_info); + gmtime_r(&time_stamp, &tm_info); + strftime(output, output_len, "%Y-%m-%d", &tm_info); return 1; } @@ -781,7 +781,7 @@ static int _apc_modbus_date_from_nut(const char *value, uint16_t *output, size_t return 0; } - if ((epoch_time = mktime(&tm_struct)) == -1) { + if ((epoch_time = timegm(&tm_struct)) == -1) { return 0; } From e52658cce4d4c004faab9ce712bb472d7a9d4b48 Mon Sep 17 00:00:00 2001 From: Axel Gembe Date: Thu, 23 Nov 2023 02:12:35 +0700 Subject: [PATCH 7/7] apc_modbus: Add target outlet group to load.* and shutdown.* commands Maybe this is needed, needs to be confirmed. Signed-off-by: Axel Gembe --- drivers/apc_modbus.c | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/drivers/apc_modbus.c b/drivers/apc_modbus.c index 9939794ff5..8aafce1479 100644 --- a/drivers/apc_modbus.c +++ b/drivers/apc_modbus.c @@ -1248,15 +1248,23 @@ static apc_modbus_command_t apc_modbus_command_map[] = { { "calibrate.stop", APC_MODBUS_RUNTIMECALIBRATIONCOMMAND_BF_REG, 1, APC_MODBUS_RUNTIMECALIBRATIONCOMMAND_BF_ABORT }, { "bypass.start", APC_MODBUS_UPSCOMMAND_BF_REG, 2, APC_MODBUS_UPSCOMMAND_BF_OUTPUT_INTO_BYPASS }, { "bypass.stop", APC_MODBUS_UPSCOMMAND_BF_REG, 2, APC_MODBUS_UPSCOMMAND_BF_OUTPUT_OUT_OF_BYPASS }, - { "load.off", APC_MODBUS_OUTLETCOMMAND_BF_REG, 2, APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_OFF }, - { "load.on", APC_MODBUS_OUTLETCOMMAND_BF_REG, 2, APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_ON }, - { "load.off.delay", APC_MODBUS_OUTLETCOMMAND_BF_REG, 2, APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_OFF | APC_MODBUS_OUTLETCOMMAND_BF_MOD_USE_OFF_DELAY }, - { "load.on.delay", APC_MODBUS_OUTLETCOMMAND_BF_REG, 2, APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_ON | APC_MODBUS_OUTLETCOMMAND_BF_MOD_USE_ON_DELAY }, - { "shutdown.return", APC_MODBUS_OUTLETCOMMAND_BF_REG, 2, APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_SHUTDOWN | APC_MODBUS_OUTLETCOMMAND_BF_MOD_USE_OFF_DELAY }, - { "shutdown.stayoff", APC_MODBUS_OUTLETCOMMAND_BF_REG, 2, APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_OFF | APC_MODBUS_OUTLETCOMMAND_BF_MOD_USE_OFF_DELAY }, - { "shutdown.reboot", APC_MODBUS_OUTLETCOMMAND_BF_REG, 2, APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_REBOOT }, - { "shutdown.reboot.graceful", APC_MODBUS_OUTLETCOMMAND_BF_REG, 2, APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_REBOOT | APC_MODBUS_OUTLETCOMMAND_BF_MOD_USE_OFF_DELAY }, { "beeper.mute", APC_MODBUS_USERINTERFACECOMMAND_BF_REG, 1, APC_MODBUS_USERINTERFACECOMMAND_BF_MUTE_ALL_ACTIVE_AUDIBLE_ALARMS }, + { "load.off", APC_MODBUS_OUTLETCOMMAND_BF_REG, 2, + APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_OFF | APC_MODBUS_OUTLETCOMMAND_BF_TARGET_MAIN_OUTLET_GROUP }, + { "load.on", APC_MODBUS_OUTLETCOMMAND_BF_REG, 2, + APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_ON | APC_MODBUS_OUTLETCOMMAND_BF_TARGET_MAIN_OUTLET_GROUP }, + { "load.off.delay", APC_MODBUS_OUTLETCOMMAND_BF_REG, 2, + APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_OFF | APC_MODBUS_OUTLETCOMMAND_BF_TARGET_MAIN_OUTLET_GROUP | APC_MODBUS_OUTLETCOMMAND_BF_MOD_USE_OFF_DELAY }, + { "load.on.delay", APC_MODBUS_OUTLETCOMMAND_BF_REG, 2, + APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_ON | APC_MODBUS_OUTLETCOMMAND_BF_TARGET_MAIN_OUTLET_GROUP | APC_MODBUS_OUTLETCOMMAND_BF_MOD_USE_ON_DELAY }, + { "shutdown.return", APC_MODBUS_OUTLETCOMMAND_BF_REG, 2, + APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_SHUTDOWN | APC_MODBUS_OUTLETCOMMAND_BF_TARGET_MAIN_OUTLET_GROUP | APC_MODBUS_OUTLETCOMMAND_BF_MOD_USE_OFF_DELAY }, + { "shutdown.stayoff", APC_MODBUS_OUTLETCOMMAND_BF_REG, 2, + APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_OFF | APC_MODBUS_OUTLETCOMMAND_BF_TARGET_MAIN_OUTLET_GROUP | APC_MODBUS_OUTLETCOMMAND_BF_MOD_USE_OFF_DELAY }, + { "shutdown.reboot", APC_MODBUS_OUTLETCOMMAND_BF_REG, 2, + APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_REBOOT | APC_MODBUS_OUTLETCOMMAND_BF_TARGET_MAIN_OUTLET_GROUP }, + { "shutdown.reboot.graceful", APC_MODBUS_OUTLETCOMMAND_BF_REG, 2, + APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_REBOOT | APC_MODBUS_OUTLETCOMMAND_BF_MOD_USE_OFF_DELAY }, { "outlet.0.shutdown.return", APC_MODBUS_OUTLETCOMMAND_BF_REG, 2, APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_SHUTDOWN | APC_MODBUS_OUTLETCOMMAND_BF_TARGET_MAIN_OUTLET_GROUP | APC_MODBUS_OUTLETCOMMAND_BF_MOD_USE_OFF_DELAY }, { "outlet.0.load.off", APC_MODBUS_OUTLETCOMMAND_BF_REG, 2, @@ -1456,7 +1464,7 @@ void upsdrv_updateinfo(void) void upsdrv_shutdown(void) { - modbus_write_register(modbus_ctx, APC_MODBUS_OUTLETCOMMAND_BF_REG, APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_SHUTDOWN); + modbus_write_register(modbus_ctx, APC_MODBUS_OUTLETCOMMAND_BF_REG, APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_SHUTDOWN | APC_MODBUS_OUTLETCOMMAND_BF_TARGET_MAIN_OUTLET_GROUP); } void upsdrv_help(void)