/** * Copyright (C) 2020 Evgeny Zinoviev * This file is part of isv . * * 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 3 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, see . */ #include #include #include #include #include #include #include #include "variant.h" #include "p18.h" #include "util.h" #include "print.h" #include "libvoltronic/voltronic_dev_usb.h" #define COMMAND_BUF_LENGTH 128 #define RESPONSE_BUF_LENGTH 128 #define PRINT(msg_type) \ { \ P18_MSG_T(msg_type) m = P18_UNPACK_FN_NAME(msg_type)(buffer+5); \ PRINT_FN_NAME(msg_type)(&m, g_format); \ } #define GET_ARGS(len) \ get_args(argc, (const char **)argv, a, (len)) bool g_verbose = false; print_format_t g_format = PRINT_FORMAT_TABLE; static void usageintlist(const int *list, size_t size) { for (size_t i = 0; i < size; i++) { printf("%d", list[i]); if (i < size-1) printf(", "); } } static void usagestrlist(const char **list, size_t size) { for (size_t i = 0; i < size; i++) { printf("%s", list[i]); if (i < size-1) printf(", "); } } static void write_num(char *buf, int n) { assert(n <= 9); *buf++ = n + '0'; *buf++ = '\0'; } static void usage(const char *progname) { printf("Usage: %s OPTIONS\n", progname); printf("\n" "Options:\n" " -h, --help: print this help\n" " -r ,\n" " --raw : execute arbitrary command and print inverter's\n" " response. Command example: ^P005PI\n" " -t ,\n" " --timeout : device read timeout, in milliseconds\n" " -v, --verbose: print debug information, like hexdumps of\n" " communication traffic with inverter\n" " -p, --pretend: do not actually execute command on inverter,\n" " but output some debug info\n" " -f ,\n" " --format : output format for --get and --set options, see below\n" "\n" "Options to get data from inverter:\n" " --get-protocol-id\n" " --get-date-time\n" " --get-total-generated\n" " --get-year-generated \n" " --get-month-generated \n" " --get-day-generated
\n" " --get-series-number\n" " --get-cpu-version\n" " --get-rated-information\n" " --get-general-status\n" " --get-working-mode\n" " --get-faults-warnings\n" " --get-flags\n" " --get-defaults\n" " --get-max-charging-current-selectable-values\n" " --get-max-ac-charging-current-selectable-values\n" " --get-parallel-rated-information \n" " ID: parallel machine ID\n" "\n" " --get-parallel-general-status \n" " ID: parallel machine ID\n" "\n" " --get-ac-charge-time-bucket\n" " --get-ac-supply-load-time-bucket\n" "\n" "Options to set inverter's configuration:\n" " --set-loads-supply 0|1\n" " --set-flag 0|1\n" " --set-defaults\n" " --set-battery-max-charging-current \n" " ID: parallel machine ID (use 0 for a single model)\n" " AMPS: use --get-max-charging-current-selectable-values\n" " to see a list of allowed current values\n" "\n" " --set-battery-max-ac-charging-current \n" " ID: parallel machine ID (use 0 for a single model)\n" " AMPS: use --get-max-ac-charging-current-selectable-values\n" " to see a list of allowed current values\n" "\n" " --set-ac-output-freq 50|60\n" " --set-battery-max-charging-voltage \n" " CV: constant voltage (48.0~58.4)\n" " FV: float voltage (48.0~58.4)\n" "\n" " --set-ac-output-rated-voltage \n" " V: one of: " ); usageintlist(p18_ac_output_rated_voltages, ARRAY_SIZE(p18_ac_output_rated_voltages)); printf("\n\n" " --set-output-source-priority SUB|SBU\n" " SUB for %s\n" " SBU for %s\n", p18_output_source_priority_label(P18_OSP_SOLAR_UTILITY_BATTERY), p18_output_source_priority_label(P18_OSP_SOLAR_BATTERY_UTILITY)); printf("\n" " --set-battery-charging-thresholds \n" " Sets battery re-charging and re-discharigng voltages when\n" " utility is available.\n" "\n" " CV: re-charging voltage\n" " for 12V unit, one of: "); usagestrlist(p18_battery_util_recharging_voltages_12v_unit, ARRAY_SIZE(p18_battery_util_recharging_voltages_12v_unit)); printf("\n" " for 24V unit, one of: "); usagestrlist(p18_battery_util_recharging_voltages_24v_unit, ARRAY_SIZE(p18_battery_util_recharging_voltages_24v_unit)); printf("\n" " for 48V unit, one of: "); usagestrlist(p18_battery_util_recharging_voltages_48v_unit, ARRAY_SIZE(p18_battery_util_recharging_voltages_48v_unit)); printf("\n" " DV: re-discharging voltage\n" " for 12V unit, one of: "); usagestrlist(p18_battery_util_redischarging_voltages_12v_unit, ARRAY_SIZE(p18_battery_util_redischarging_voltages_12v_unit)); printf("\n" " for 24V unit, one of: "); usagestrlist(p18_battery_util_redischarging_voltages_24v_unit, ARRAY_SIZE(p18_battery_util_redischarging_voltages_24v_unit)); printf("\n" " for 48V unit, one of: "); usagestrlist(p18_battery_util_redischarging_voltages_48v_unit, ARRAY_SIZE(p18_battery_util_redischarging_voltages_48v_unit)); printf("\n\n" " --set-charging-source-priority \n" " ID: parallel machine ID (use 0 for a single model)\n" " PRIORITY:\n" " SF: %s,\n" " SU: %s,\n" " S: %s\n", p18_charge_source_priority_label(P18_CSP_SOLAR_FIRST), p18_charge_source_priority_label(P18_CSP_SOLAR_AND_UTILITY), p18_charge_source_priority_label(P18_CSP_SOLAR_ONLY)); printf("\n" " --set-solar-power-priority BLU|LBU\n" " BLU: %s\n" " LBU: %s\n", p18_solar_power_priority_label(P18_SPP_BATTERY_LOAD_UTILITY), p18_solar_power_priority_label(P18_SPP_LOAD_BATTERY_UTILITY)); printf("\n" " --set-ac-input-voltage-range APPLIANCE|UPS\n" " --set-battery-type AGM|FLOODED|USER\n" " --set-output-model \n" " ID: parallel machine ID (use 0 for a single model)\n" " MODEL:\n" " SM: %s\n" " P: %s\n" " P1: %s\n" " P2: %s\n" " P3: %s\n", p18_output_model_setting_label(P18_OMS_SINGLE_MODULE), p18_output_model_setting_label(P18_OMS_PARALLEL_OUTPUT), p18_output_model_setting_label(P18_OMS_PHASE1_OF_3PHASE_OUTPUT), p18_output_model_setting_label(P18_OMS_PHASE2_OF_3PHASE_OUTPUT), p18_output_model_setting_label(P18_OMS_PHASE3_OF_3PHASE_OUTPUT)); printf("\n" " --set-battery-cutoff-voltage \n" " V: cut-off voltage (40.0~48.0)\n" "\n" " --set-solar-configuration \n" " ID: serial number\n" "\n" " --clear-generated-data\n" " Clears all data of generated energy.\n" "\n" " --set-date-time
\n" " YYYY: year\n" " MM: month\n" " DD: day\n" " hh: hours\n" " mm: minutes\n" " ss: seconds\n" "\n" " --set-ac-charge-time-bucket \n" " START: starting time, hh:mm format\n" " END: ending time, hh:mm format\n" "\n" " --set-ac-supply-load-time-bucket \n" " START: starting time, hh:mm format\n" " END: ending time, hh:mm format\n" "\n" ); printf("Flags:\n"); size_t len = ARRAY_SIZE(p18_flags_printable_list); for (size_t i = 0; i < len; i++) { p18_flag_printable_list_item_t item = p18_flags_printable_list[i]; printf(" %s: %s\n", item.key, item.title); } printf("\n" "Formats:\n" " table human-readable table\n" " parsable-table conveniently-parsable table\n" " json JSON object, like {\"ac_output_voltage\":230}\n" " json-w-units JSON object with units, like:\n" " {\"ac_output_voltage\":[230,\"V\"]}\n" ); exit(1); } static void exit_with_error(int code, char *fmt, ...) { static const size_t buf_size = 256; char buf[buf_size]; va_list args; va_start(args, fmt); size_t len = vsnprintf(buf, buf_size, fmt, args); va_end(args); buf[MIN(len, buf_size-1)] = '\0'; ERROR("error: %s\n", buf); if (g_format == PRINT_FORMAT_JSON || g_format == PRINT_FORMAT_JSON_W_UNITS) { print_item_t items[] = { {.key= "error", .value= variant_string(buf)} }; print_json(items, 1, false); } exit(code); } static void get_args(int argc, const char **argv, const char **arguments_dst, size_t count) { size_t i = 0; arguments_dst[i++] = optarg; for (; i < count; i++) { if (optind < argc && *argv[optind] != '-') arguments_dst[i] = argv[optind++]; else exit_with_error(1, "option %s requires %zu arguments", argv[optind-i-1], count); } } static void execute_raw(voltronic_dev_t dev, const char *command, int timeout) { char buffer[RESPONSE_BUF_LENGTH]; int result = voltronic_dev_execute(dev, 0, command, strlen(command), buffer, sizeof(buffer), NULL, timeout); if (result <= 0) exit_with_error(2, "failed to execute %s: %s", command, strerror(errno)); printf("%s\n", buffer); } static void query(voltronic_dev_t dev, int command_key, int timeout, const char **args, size_t args_size, bool pretend) { char buffer[RESPONSE_BUF_LENGTH]; char command[COMMAND_BUF_LENGTH]; if (!p18_build_command(command_key, args, args_size, command)) exit_with_error(1, "invalid query command %d", command_key); if (pretend) { size_t command_len = strlen(command); LOG("would write %zu+3 %s:\n", command_len, (command_len > 1 ? "bytes" : "byte")); HEXDUMP(command, command_len); return; } size_t received; int result = voltronic_dev_execute(dev, 0, command, strlen(command), buffer, sizeof(buffer), &received, timeout); if (result <= 0) exit_with_error(2, "failed to execute %s: %s", command, strerror(errno)); if (command_key < P18_SET_CMDS_ENUM_OFFSET) { size_t data_size; if (!p18_validate_query_response(buffer, received, &data_size)) exit_with_error(2, "invalid response"); if (command_key == P18_QUERY_PROTOCOL_ID) PRINT(protocol_id) else if (command_key == P18_QUERY_CURRENT_TIME) PRINT(current_time) else if (command_key == P18_QUERY_TOTAL_GENERATED) PRINT(total_generated) else if (command_key == P18_QUERY_YEAR_GENERATED) PRINT(year_generated) else if (command_key == P18_QUERY_MONTH_GENERATED) PRINT(month_generated) else if (command_key == P18_QUERY_DAY_GENERATED) PRINT(day_generated) else if (command_key == P18_QUERY_SERIES_NUMBER) PRINT(series_number) else if (command_key == P18_QUERY_CPU_VERSION) PRINT(cpu_version) else if (command_key == P18_QUERY_RATED_INFORMATION) PRINT(rated_information) else if (command_key == P18_QUERY_GENERAL_STATUS) PRINT(general_status) else if (command_key == P18_QUERY_WORKING_MODE) PRINT(working_mode) else if (command_key == P18_QUERY_FAULTS_WARNINGS) PRINT(faults_warnings) else if (command_key == P18_QUERY_FLAGS_STATUSES) PRINT(flags_statuses) else if (command_key == P18_QUERY_DEFAULTS) PRINT(defaults) else if (command_key == P18_QUERY_MAX_CHARGING_CURRENT_SELECTABLE_VALUES) PRINT(max_charging_current_selectable_values) else if (command_key == P18_QUERY_MAX_AC_CHARGING_CURRENT_SELECTABLE_VALUES) PRINT(max_ac_charging_current_selectable_values) else if (command_key == P18_QUERY_PARALLEL_RATED_INFORMATION) PRINT(parallel_rated_information) else if (command_key == P18_QUERY_PARALLEL_GENERAL_STATUS) PRINT(parallel_general_status) else if (command_key == P18_QUERY_AC_CHARGE_TIME_BUCKET) PRINT(ac_charge_time_bucket) else if (command_key == P18_QUERY_AC_SUPPLY_LOAD_TIME_BUCKET) PRINT(ac_supply_load_time_bucket) } else { bool success = p18_set_result(buffer, received); print_set_result(success, g_format); if (!success) exit(2); } } static void validate_date_args(const char *ys, const char *ms, const char *ds) { static char *err_year = "invalid year"; static char *err_month = "invalid month"; static char *err_day = "invalid day"; int y, m = 0, d = 0; /* validate year */ if (!isnumeric(ys) || strlen(ys) != 4) exit_with_error(1, err_year); y = (int)strtoul(ys, NULL, 10); if (y < 2000 || y > 2099) { ERROR("%s\n", err_year); exit(1); } /* validate month */ if (ms != NULL) { if (!isnumeric(ms) || strlen(ms) > 2) exit_with_error(1, err_month); m = (int)strtoul(ms, NULL, 10); if (m < 1 || m > 12) exit_with_error(1, err_month); } /* validate day */ if (ds != NULL) { if (!isnumeric(ds) || strlen(ds) > 2) exit_with_error(1, err_day); d = (int) strtoul(ds, NULL, 10); if (d < 1 || d > 31) exit_with_error(1, err_day); } if (y != 0 && m != 0 && d != 0) { if (!isdatevalid(y, m, d)) exit_with_error(1, "invalid date"); } } static void validate_time_args(const char *hs, const char *ms, const char *ss) { static char *err_hour = "invalid hour"; static char *err_minute = "invalid minute"; static char *err_second = "invalid second"; unsigned int h, m, s; if (!isnumeric(hs) || strlen(hs) > 2) exit_with_error(1, err_hour); h = (unsigned int)strtoul(hs, NULL, 10); if (h > 23) exit_with_error(1, err_hour); if (!isnumeric(ms) || strlen(ms) > 2) exit_with_error(1, err_minute); m = (unsigned int)strtoul(ms, NULL, 10); if (m > 59) exit_with_error(1, err_minute); if (!isnumeric(ss) || strlen(ss) > 2) exit_with_error(1, err_second); s = (unsigned int)strtoul(ss, NULL, 10); if (s > 59) exit_with_error(1, err_second); } static bool get_float(const char *s, float *fptr) { char *endptr; float f = strtof(s, &endptr); if (endptr == s) return false; if (fptr != NULL) *fptr = f; return true; } static bool get_uint(const char *s, unsigned int *iptr) { char *endptr; unsigned int i = (unsigned int)strtoul(s, &endptr, 10); if (endptr == s) return false; if (iptr != NULL) *iptr = i; return true; } static bool is_valid_parallel_id(const char *s) { return isnumeric(s) && strlen(s) == 1; } enum action { ACTION_HELP, ACTION_DUMP, ACTION_EXECUTE, ACTION_QUERY, }; enum { OPT_HELP = 'h', OPT_DUMP = 'd', OPT_VERBOSE = 'v', OPT_RAW = 'r', OPT_PREDENT = 'p', OPT_TIMEOUT = 't', OPT_FORMAT = 'f', }; int main(int argc, char *argv[]) { if (argv[1] == NULL) usage(argv[0]); enum action act = ACTION_HELP; int opt; int command_no = 0, timeout = 1000; bool pretend = false; const char *a[6] = {0}; /* p18 command arguments */ static struct option long_options[] = { {"help", no_argument, 0, OPT_HELP}, {"dump", no_argument, 0, OPT_DUMP}, {"verbose", no_argument, 0, OPT_VERBOSE}, {"raw", required_argument, 0, OPT_RAW}, {"pretend", required_argument, 0, OPT_PREDENT}, {"timeout", required_argument, 0, OPT_TIMEOUT}, {"format", required_argument, 0, OPT_FORMAT}, /* get queries */ {"get-protocol-id", no_argument, 0, P18_QUERY_PROTOCOL_ID}, {"get-date-time", no_argument, 0, P18_QUERY_CURRENT_TIME}, {"get-total-generated", no_argument, 0, P18_QUERY_TOTAL_GENERATED}, {"get-year-generated", required_argument, 0, P18_QUERY_YEAR_GENERATED}, {"get-month-generated", required_argument, 0, P18_QUERY_MONTH_GENERATED}, {"get-day-generated", required_argument, 0, P18_QUERY_DAY_GENERATED}, {"get-series-number", no_argument, 0, P18_QUERY_SERIES_NUMBER}, {"get-cpu-version", no_argument, 0, P18_QUERY_CPU_VERSION}, {"get-rated-information", no_argument, 0, P18_QUERY_RATED_INFORMATION}, {"get-general-status", no_argument, 0, P18_QUERY_GENERAL_STATUS}, {"get-working-mode", no_argument, 0, P18_QUERY_WORKING_MODE}, {"get-faults-warnings", no_argument, 0, P18_QUERY_FAULTS_WARNINGS}, {"get-flags", no_argument, 0, P18_QUERY_FLAGS_STATUSES}, {"get-defaults", no_argument, 0, P18_QUERY_DEFAULTS}, {"get-max-charging-current-selectable-values", no_argument, 0, P18_QUERY_MAX_CHARGING_CURRENT_SELECTABLE_VALUES}, {"get-max-ac-charging-current-selectable-values", no_argument, 0, P18_QUERY_MAX_AC_CHARGING_CURRENT_SELECTABLE_VALUES}, {"get-parallel-rated-information", required_argument, 0, P18_QUERY_PARALLEL_RATED_INFORMATION}, {"get-parallel-general-status", required_argument, 0, P18_QUERY_PARALLEL_GENERAL_STATUS}, {"get-ac-charge-time-bucket", no_argument, 0, P18_QUERY_AC_CHARGE_TIME_BUCKET}, {"get-ac-supply-load-time-bucket", no_argument, 0, P18_QUERY_AC_SUPPLY_LOAD_TIME_BUCKET}, /* set queries */ {"set-loads-supply", required_argument, 0, P18_SET_LOADS}, {"set-flag", required_argument, 0, P18_SET_FLAG}, {"set-defaults", no_argument, 0, P18_SET_DEFAULTS}, {"set-battery-max-charging-current", required_argument, 0, P18_SET_BAT_MAX_CHARGE_CURRENT}, {"set-battery-max-ac-charging-current", required_argument, 0, P18_SET_BAT_MAX_AC_CHARGE_CURRENT}, {"set-ac-output-freq", required_argument, 0, P18_SET_AC_OUTPUT_FREQ}, {"set-battery-max-charging-voltage", required_argument, 0, P18_SET_BAT_MAX_CHARGE_VOLTAGE}, {"set-ac-output-rated-voltage", required_argument, 0, P18_SET_AC_OUTPUT_RATED_VOLTAGE}, {"set-output-source-priority", required_argument, 0, P18_SET_OUTPUT_SOURCE_PRIORITY}, {"set-battery-charging-thresholds", required_argument, 0, P18_SET_BAT_CHARGING_THRESHOLDS_WHEN_UTILITY_AVAIL}, {"set-charging-source-priority", required_argument, 0, P18_SET_CHARGING_SOURCE_PRIORITY}, {"set-solar-power-priority", required_argument, 0, P18_SET_SOLAR_POWER_PRIORITY}, {"set-ac-input-voltage-range", required_argument, 0, P18_SET_AC_INPUT_VOLTAGE_RANGE}, {"set-battery-type", required_argument, 0, P18_SET_BAT_TYPE}, {"set-output-model", required_argument, 0, P18_SET_OUTPUT_MODEL}, {"set-battery-cutoff-voltage", required_argument, 0, P18_SET_BAT_CUTOFF_VOLTAGE}, {"set-solar-configuration", required_argument, 0, P18_SET_SOLAR_CONFIG}, {"clear-generated-data", no_argument , 0, P18_SET_CLEAR_GENERATED}, {"set-date-time", required_argument, 0, P18_SET_DATE_TIME}, {"set-ac-charge-time-bucket", required_argument, 0, P18_SET_AC_CHARGE_TIME_BUCKET}, {"set-ac-supply-load-time-bucket", required_argument, 0, P18_SET_AC_SUPPLY_LOAD_TIME_BUCKET}, {0, 0, 0, 0} }; bool getopt_err = false; while ((opt = getopt_long(argc, argv, "hdvr:pt:f:", long_options, NULL)) != EOF) { if (opt == '?') { getopt_err = true; break; } if (opt == OPT_HELP) act = ACTION_HELP; else if (opt == OPT_DUMP) act = ACTION_DUMP; else if (opt == OPT_VERBOSE) g_verbose = true; else if (opt == OPT_PREDENT) pretend = true; else if (opt == OPT_FORMAT) { if (!strcmp(optarg, "json")) g_format = PRINT_FORMAT_JSON; else if (!strcmp(optarg, "json-w-units")) g_format = PRINT_FORMAT_JSON_W_UNITS; else if (!strcmp(optarg, "table")) g_format = PRINT_FORMAT_TABLE; else if (!strcmp(optarg, "parsable-table")) g_format = PRINT_FORMAT_PARSABLE_TABLE; else exit_with_error(1, "invalid format"); } else if (opt == OPT_RAW) { if (strlen(optarg) > COMMAND_BUF_LENGTH - 1) exit_with_error(1, "command is too long"); a[0] = optarg; act = ACTION_EXECUTE; } else if (opt == OPT_TIMEOUT) { timeout = atoi(optarg); if (timeout <= 0 || timeout > 60000) exit_with_error(1, "invalid timeout"); } else if (opt >= P18_QUERY_CMDS_ENUM_OFFSET) { if (act == ACTION_QUERY) exit_with_error(1, "one query at a time, please"); if (opt >= P18_QUERY_CMDS_ENUM_OFFSET) { act = ACTION_QUERY; command_no = opt; } switch (opt) { case P18_QUERY_YEAR_GENERATED: GET_ARGS(1); validate_date_args(a[0], NULL, NULL); break; case P18_QUERY_MONTH_GENERATED: GET_ARGS(2); validate_date_args(a[0], a[1], NULL); break; case P18_QUERY_DAY_GENERATED: GET_ARGS(3); validate_date_args(a[0], a[1], a[2]); break; case P18_QUERY_PARALLEL_RATED_INFORMATION: case P18_QUERY_PARALLEL_GENERAL_STATUS: GET_ARGS(1); if (!isnumeric(a[0]) || strlen(a[0]) > 1) exit_with_error(1, "invalid argument"); break; case P18_SET_LOADS: GET_ARGS(1); if (strcmp(a[0], "0") != 0 && strcmp(a[0], "1") != 0) exit_with_error(1, "invalid argument, only 0 or 1 allowed"); break; case P18_SET_FLAG: { GET_ARGS(2); bool matchfound = false; FOREACH (const p18_flag_printable_list_item_t *item, p18_flags_printable_list) { if (!strcmp(item->key, a[0])) { a[0] = item->p18_key; matchfound = true; break; } } if (!matchfound) exit_with_error(1, "invalid flag"); if (strcmp(a[1], "0") != 0 && strcmp(a[1], "1") != 0) exit_with_error(1, "invalid flag state, only 0 or 1 allowed"); break; } case P18_SET_BAT_MAX_CHARGE_CURRENT: case P18_SET_BAT_MAX_AC_CHARGE_CURRENT: GET_ARGS(2); if (!is_valid_parallel_id(a[0])) exit_with_error(1, "invalid id"); if (!get_uint(a[1], NULL) || strlen(a[1]) > 3) exit_with_error(1, "invalid argument"); break; case P18_SET_AC_OUTPUT_FREQ: GET_ARGS(1); if (strcmp(a[0], "50") != 0 && strcmp(a[0], "60") != 0) exit_with_error(1, "invalid frequency, only 50 or 60 allowed"); break; case P18_SET_BAT_MAX_CHARGE_VOLTAGE: GET_ARGS(2); float cv, fv; bool cvr = get_float(a[0], &cv); bool fvr = get_float(a[1], &fv); if (!cvr || cv < 48.0 || cv > 58.4) exit_with_error(1, "invalid CV"); if (!fvr || fv < 48.0 || fv > 58.4) exit_with_error(1, "invalid FV"); break; case P18_SET_AC_OUTPUT_RATED_VOLTAGE: { GET_ARGS(1); unsigned int v; if (!get_uint(a[0], &v)) exit_with_error(1, "invalid argument"); bool matchfound = false; FOREACH (const int *allowed_v, p18_ac_output_rated_voltages) { if ((unsigned int)*allowed_v == v) { matchfound = true; break; } } if (!matchfound) exit_with_error(1, "invalid voltage"); break; case P18_SET_OUTPUT_SOURCE_PRIORITY: GET_ARGS(1); const char *allowed[] = {"SUB", "SBU"}; int index; if (!instrarray(a[0], allowed, ARRAY_SIZE(allowed), &index)) exit_with_error(1, "invalid argument"); write_num((char *)a[0], index); break; case P18_SET_BAT_CHARGING_THRESHOLDS_WHEN_UTILITY_AVAIL: GET_ARGS(2); if ( !instrarray(a[0], p18_battery_util_recharging_voltages_12v_unit, ARRAY_SIZE(p18_battery_util_recharging_voltages_12v_unit), NULL) && !instrarray(a[0], p18_battery_util_recharging_voltages_24v_unit, ARRAY_SIZE(p18_battery_util_recharging_voltages_24v_unit), NULL) && !instrarray(a[0], p18_battery_util_recharging_voltages_48v_unit, ARRAY_SIZE(p18_battery_util_recharging_voltages_48v_unit), NULL)) exit_with_error(1, "invalid CV"); if ( !instrarray(a[1], p18_battery_util_redischarging_voltages_12v_unit, ARRAY_SIZE(p18_battery_util_recharging_voltages_12v_unit), NULL) && !instrarray(a[1], p18_battery_util_redischarging_voltages_24v_unit, ARRAY_SIZE(p18_battery_util_recharging_voltages_24v_unit), NULL) && !instrarray(a[1], p18_battery_util_redischarging_voltages_48v_unit, ARRAY_SIZE(p18_battery_util_recharging_voltages_48v_unit), NULL)) exit_with_error(1, "invalid DV"); break; } case P18_SET_CHARGING_SOURCE_PRIORITY: { GET_ARGS(2); if (!is_valid_parallel_id(a[0])) exit_with_error(1, "invalid id"); const char *allowed[] = {"SF", "SU", "S"}; int index; if (!instrarray(a[1], allowed, ARRAY_SIZE(allowed), &index)) exit_with_error(1, "invalid priority"); write_num((char *)a[1], index); break; } case P18_SET_SOLAR_POWER_PRIORITY: { GET_ARGS(1); const char *allowed[] = {"BLU", "LBU"}; int index; if (!instrarray(a[0], allowed, ARRAY_SIZE(allowed), &index)) exit_with_error(1, "invalid priority"); write_num((char *)a[0], index); break; } case P18_SET_AC_INPUT_VOLTAGE_RANGE: { GET_ARGS(1); const char *allowed[] = {"APPLIANCE", "UPS"}; int index; if (!instrarray(a[0], allowed, ARRAY_SIZE(allowed), &index)) exit_with_error(1, "invalid argument"); write_num((char *)a[0], index); break; } case P18_SET_BAT_TYPE: { GET_ARGS(1); const char *allowed[] = {"AGM", "FLOODED", "USER"}; int index; if (!instrarray(a[0], allowed, ARRAY_SIZE(allowed), &index)) exit_with_error(1, "invalid type"); write_num((char *)a[0], index); break; } case P18_SET_OUTPUT_MODEL: { GET_ARGS(2); if (!is_valid_parallel_id(a[0])) exit_with_error(1, "invalid id"); const char *allowed[] = {"SM", "P", "P1", "P2", "P3"}; int index; if (!instrarray(a[1], allowed, ARRAY_SIZE(allowed), &index)) exit_with_error(1, "invalid model"); write_num((char *)a[1], index); break; } case P18_SET_BAT_CUTOFF_VOLTAGE: GET_ARGS(1); float v; bool vr = get_float(a[0], &v); if (!vr || v < 40.0 || v > 48.0) exit_with_error(1, "invalid voltage"); break; case P18_SET_SOLAR_CONFIG: GET_ARGS(1); if (!isnumeric(a[0]) || strlen(a[0]) > 20) exit_with_error(1, "invalid argument"); break; case P18_SET_DATE_TIME: GET_ARGS(6); validate_date_args(a[0], a[1], a[2]); validate_time_args(a[3], a[4], a[5]); break; case P18_SET_AC_CHARGE_TIME_BUCKET: case P18_SET_AC_SUPPLY_LOAD_TIME_BUCKET: GET_ARGS(2); unsigned short start_h, start_m, end_h, end_m; int results; results = sscanf(a[0], "%hu:%hu", &start_h, &start_m); if (results != 2 || start_h > 23 || start_m > 59) exit_with_error(1, "invalid start time"); results = sscanf(a[1], "%hu:%hu", &end_h, &end_m); if (results != 2 || end_h > 23 || end_m > 59) exit_with_error(1, "invalid end time"); char *start_col = strchr(a[0], ':'); char *end_col = strchr(a[1], ':'); *start_col = '\0'; *end_col = '\0'; char *start_m_ptr = start_col+1; char *end_h_ptr = (char *)a[1]; char *end_m_ptr = end_col+1; a[1] = start_m_ptr; a[2] = end_h_ptr; a[3] = end_m_ptr; break; } } } if (optind < argc) exit_with_error(1, "extra parameter found"); if (getopt_err) exit(1); if (act == ACTION_HELP) usage(argv[0]); voltronic_dev_t dev = voltronic_usb_create(0x0665, 0x5161); if (!pretend && !dev) exit_with_error(1, "could not open USB device: %s", strerror(errno)); switch (act) { case ACTION_EXECUTE: execute_raw(dev, a[0], timeout); break; case ACTION_QUERY: query(dev, command_no, timeout, a, sizeof(a), pretend); break; default: exit_with_error(1, "unexpected act %d", act); } if (dev) voltronic_dev_close(dev); return 0; }