aboutsummaryrefslogtreecommitdiff
path: root/isv.c
diff options
context:
space:
mode:
Diffstat (limited to 'isv.c')
-rw-r--r--isv.c861
1 files changed, 861 insertions, 0 deletions
diff --git a/isv.c b/isv.c
new file mode 100644
index 0000000..f8603a7
--- /dev/null
+++ b/isv.c
@@ -0,0 +1,861 @@
+/**
+ * Copyright (C) 2020 Evgeny Zinoviev
+ * This file is part of isv <https://github.com/gch1p/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 <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <string.h>
+#include <getopt.h>
+#include <stdarg.h>
+#include <assert.h>
+
+#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 <COMMAND>,\n"
+ " --raw <COMMAND>: execute arbitrary command and print inverter's\n"
+ " response. Command example: ^P005PI\n"
+ " -t <TIMEOUT>,\n"
+ " --timeout <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 <FORMAT>,\n"
+ " --format <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 <YYYY>\n"
+ " --get-month-generated <YYYY> <MM>\n"
+ " --get-day-generated <YYYY> <MM> <DD>\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 <ID>\n"
+ " ID: parallel machine ID\n"
+ "\n"
+ " --get-parallel-general-status <ID>\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 <FLAG> 0|1\n"
+ " --set-defaults\n"
+ " --set-battery-max-charging-current <ID> <AMPS>\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 <ID> <AMPS>\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 <CV> <FV>\n"
+ " CV: constant voltage (48.0~58.4)\n"
+ " FV: float voltage (48.0~58.4)\n"
+ "\n"
+ " --set-ac-output-rated-voltage <V>\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 <CV> <DV>\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 <ID> <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 <ID> <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 <V>\n"
+ " V: cut-off voltage (40.0~48.0)\n"
+ "\n"
+ " --set-solar-configuration <ID>\n"
+ " ID: serial number\n"
+ "\n"
+ " --clear-generated-data\n"
+ " Clears all data of generated energy.\n"
+ "\n"
+ " --set-date-time <YYYY> <MM> <DD> <hh> <mm> <ss>\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 <START> <END>\n"
+ " START: starting time, hh:mm format\n"
+ " END: ending time, hh:mm format\n"
+ "\n"
+ " --set-ac-supply-load-time-bucket <START> <END>\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\n",
+ 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\n", 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\n", 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\n", 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\n");
+
+ 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;
+} \ No newline at end of file