/* microsol-common.c - common framework for Microsol Solis-based UPS hardware Copyright (C) 2004 Silvino B. Magalhães 2019 Roberto Panerai Velloso 2021 Ygor A. S. Regados 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 2021/03/19 - Version 0.70 - Initial release, based on solis driver */ #include "main.h" /* Includes "config.h", must be first */ #include #include #include #include "serial.h" #include "nut_float.h" #include "nut_stdint.h" #include "microsol-common.h" #include "timehead.h" #define false 0 #define true 1 #define RESP_END 0xFE #define ENDCHAR 13 /* replies end with CR */ /* solis commands */ #define CMD_UPSCONT 0xCC #define CMD_SHUT 0xDD #define CMD_SHUTRET 0xDE #define CMD_EVENT 0xCE #define CMD_DUMP 0xCD #define M_UNKN "Unknown solis model" #define NO_SOLIS "Solis not detected! aborting ..." #define UPS_DATE "UPS Date %4d/%02d/%02d" #define SYS_DATE "System Date %4d/%02d/%02d day of week %s" #define ERR_PACK "Wrong package" #define NO_EVENT "No events" #define UPS_TIME "UPS internal Time %0d:%02d:%02d" #define PRG_DAYS "Programming Shutdown Sun Mon Tue Wed Thu Fri Sat" #define PRG_ONON "External shutdown programming active" #define PRG_ONOU "Internal shutdown programming active" #define TIME_OFF "UPS Time power off %02d:%02d" #define TIME_ON "UPS Time power on %02d:%02d" #define PRG_ONOF "Shutdown programming not activated" #define TODAY_DD "Shutdown today at %02d:%02d" #define SHUT_NOW "Shutdown now!" #define FMT_DAYS " %d %d %d %d %d %d %d" /* Date, time and programming group */ static int const BASE_YEAR = 1998; /* Note: code below uses relative "unsigned char" years */ static int device_day, device_month, device_year; static int device_hour, device_minute, device_second; static int power_off_hour, power_off_minute; static int power_on_hour, power_on_minute; static uint8_t device_days_on = 0, device_days_off = 0, days_to_shutdown = 0; static int isprogram = 0, progshut = 0, prgups = 0; static int hourshut, minshut; static int host_year, host_month, host_day; static int host_week; static int host_hour, host_minute, host_second; /* buffers */ unsigned char received_packet[PACKET_SIZE]; /* Identification */ const char *model_name; unsigned int ups_model; bool_t input_220v, output_220v; /* logical */ bool_t detected = 0; bool_t line_unpowered, overheat; bool_t overload, critical_battery, inverter_working; static bool_t recharging; static bool_t packet_parsed = false; double input_voltage, input_current, input_frequency; double output_voltage, output_current, output_frequency; double input_low_limit, input_high_limit; int battery_extension; double battery_voltage, battery_charge; double temperature; double apparent_power, real_power, ups_load; int load_power_factor, nominal_power; /** * Convert standard days string to firmware format * This is needed because UPS sends binary date rotated * from current week day (first bit = current day) */ static char *convert_days(char *cop) { static char alt[8]; int ish, fim; /* FIXME? Are range-checks needed for values more than 6? wire noise etc? */ if (host_week == 6) ish = 0; else ish = 1 + host_week; fim = 7 - ish; /* rotate left only 7 bits */ if (fim > 0) { memcpy(alt, &cop[ish], (size_t)fim); } else { fatalx(EXIT_FAILURE, "%s: value out of range: %d (%d)", __func__, fim, ish); } if (ish > 0) memcpy(&alt[fim], cop, (size_t)ish); alt[7] = 0; /* string terminator */ return alt; } /** Convert bitstring (e.g. 1100101) to binary */ static uint8_t bitstring_to_binary(char *binStr) { uint8_t result = 0; unsigned int i; for (i = 0; i < 7; ++i) { char ch = binStr[i]; if (ch == '1' || ch == '0') result += ((ch - '0') << (6 - i)); else return 0; } return result; } /** * Revert firmware format to standard string binary days * This is needed because UPS sends binary date rotated * from current week day (first bit = current day) */ static uint8_t revert_days(unsigned char firmware_week) { char ordered_week[8]; int i; for (i = 0; i < (6 - host_week); ++i) ordered_week[i] = (firmware_week >> (5 - host_week - i)) & 0x01; for (i = 0; i < host_week + 1; ++i) ordered_week[i + (6 - host_week)] = (firmware_week >> (6 - i)) & 0x01; for (i = 0; i < 7; i++) ordered_week[i] += '0'; ordered_week[7] = 0; /* string terminator */ return bitstring_to_binary(ordered_week); } /** Parse time string from parameters and store their values */ static bool_t set_schedule_time(char *hour, bool_t off_time) { int string_hour, string_minute; if ((strlen(hour) != 5) || (sscanf(hour, "%d:%d", &string_hour, &string_minute) != 2)) return 0; if (off_time) { power_off_hour = string_hour; power_off_minute = string_minute; } else { power_on_hour = string_hour; power_on_minute = string_minute; } return 1; } /** Send immediate shutdown command to UPS */ static void send_shutdown(void) { unsigned int i; for (i = 0; i < 10; i++) ser_send_char(upsfd, CMD_SHUT); upslogx(LOG_NOTICE, "UPS shutdown command sent"); } /** Store clock updates and shutdown schedules to UPS */ static void save_ups_config(void) { unsigned int i; int checksum = 0; unsigned char configuration_packet[12]; /* Prepare configuration packet */ /* FIXME? Check for overflows with int => char truncations? */ configuration_packet[0] = (unsigned char)0xCF; configuration_packet[1] = (unsigned char)host_hour; configuration_packet[2] = (unsigned char)host_minute; configuration_packet[3] = (unsigned char)host_second; configuration_packet[4] = (unsigned char)power_on_hour; configuration_packet[5] = (unsigned char)power_on_minute; configuration_packet[6] = (unsigned char)power_off_hour; configuration_packet[7] = (unsigned char)power_off_minute; configuration_packet[8] = (unsigned char)(host_week << 5); configuration_packet[8] = (unsigned char)configuration_packet[8] | (unsigned char)host_day; configuration_packet[9] = (unsigned char)(host_month << 4); configuration_packet[9] = (unsigned char)configuration_packet[9] | (unsigned char)(host_year - BASE_YEAR); configuration_packet[10] = (unsigned char)device_days_off; /* MSB zero */ configuration_packet[10] = configuration_packet[10] & (~(0x80)); /* Calculate packet content checksum */ for (i = 0; i < 11; i++) { checksum += configuration_packet[i]; } /* FIXME? Does truncation to char have same effect as %256 ? */ configuration_packet[11] = (unsigned char)(checksum % 256); /* Send final packet and checksum to serial port */ for (i = 0; i < 12; i++) { ser_send_char(upsfd, configuration_packet[i]); } } /** Log shut-down schedule data stored in UPS */ static void print_info(void) { /* sunday, monday, tuesday, wednesday, thursday, friday, saturday */ char week_days[7] = { 0, 0, 0, 0, 0, 0, 0 }; unsigned int i; upslogx(LOG_NOTICE, UPS_DATE, device_year, device_month, device_day); upslogx(LOG_NOTICE, UPS_TIME, device_hour, device_minute, device_second); if (prgups > 0) { /* this is the string to binary standard */ for (i = 0; i < 7; i++) { week_days[i] = (days_to_shutdown >> (6 - i)) & 0x01; } if (prgups == 3) upslogx(LOG_NOTICE, PRG_ONOU); else upslogx(LOG_NOTICE, PRG_ONON); upslogx(LOG_NOTICE, TIME_ON, power_on_hour, power_on_minute); upslogx(LOG_NOTICE, TIME_OFF, power_off_hour, power_off_minute); upslogx(LOG_NOTICE, PRG_DAYS); upslogx(LOG_NOTICE, FMT_DAYS, week_days[0], week_days[1], week_days[2], week_days[3], week_days[4], week_days[5], week_days[6]); } else { upslogx(LOG_NOTICE, PRG_ONOF); } } /** Parses received packet with UPS readings and configuration. */ static void scan_received_pack(void) { /* UPS internal time */ device_year = (received_packet[19] & 0x0F) + BASE_YEAR; device_month = (received_packet[19] & 0xF0) >> 4; device_day = (received_packet[18] & 0x1F); device_hour = received_packet[11]; device_minute = received_packet[10]; device_second = received_packet[9]; /* UPS power cycle schedule if in programmed shutdown mode */ if (prgups == 3) { device_days_on = received_packet[17]; days_to_shutdown = revert_days(device_days_on); /* Automatic UPS power-off time */ power_off_hour = received_packet[15]; power_off_minute = received_packet[16]; /* Automatic UPS power-on time */ power_on_hour = received_packet[13]; power_on_minute = received_packet[14]; } /* These UPS have 110V- or 220V-output models */ if ((0x01 & received_packet[20]) == 0x01) { output_220v = 1; } /* UPS state flags */ critical_battery = (0x04 & received_packet[20]) == 0x04; inverter_working = (0x08 & received_packet[20]) == 0x08; overheat = (0x10 & received_packet[20]) == 0x10; line_unpowered = (0x20 & received_packet[20]) == 0x20; overload = (0x80 & received_packet[20]) == 0x80; recharging = (0x02 & received_packet[20]) == 0x02; if (line_unpowered) { recharging = false; } /* Check if input voltage is 110V or 220V */ if ((0x40 & received_packet[20]) == 0x40) { input_220v = 1; } else { input_220v = 0; } /* Internal battery temperature */ temperature = 0x7F & received_packet[4]; if (0x80 & received_packet[4]) { temperature -= 128; } /* Parse model-specific data (current and voltages). * Doing it here as these values are used for the next calculations. */ scan_received_pack_model_specific(); ups_load = (apparent_power / nominal_power) * 100.0; if (battery_charge > 100.0) { battery_charge = 100.0; } else if (battery_charge < 0.0) { battery_charge = 0.0; } output_frequency = 60; if (!inverter_working) { output_voltage = 0; output_frequency = 0; } if (!line_unpowered && inverter_working) output_frequency = input_frequency; if (apparent_power < 0) load_power_factor = 0; else { if (d_equal(apparent_power, 0)) load_power_factor = 100; else load_power_factor = ((real_power / apparent_power) * 100); if (load_power_factor > 100) { load_power_factor = 100; } } /* input 110V or 220v */ if (input_220v == 0) { input_low_limit = 75; input_high_limit = 150; } else { input_low_limit = 150; input_high_limit = 300; } } /** * Start processing of received packets * * Packet format: 25-bytes binary structure * Byte 1: Packet type/UPS model * Byte 2: Output voltage data * Byte 3: Input voltage data * Byte 4: Battery voltage data * Byte 5: UPS temperature data * Byte 6: Output current data * Byte 7: Electrical relay setup * Byte 8-9: Real power data * Byte 10: UPS clock - seconds * Byte 11: UPS clock - minutes * Byte 12: UPS clock - hours * Byte 13: Zero * Byte 14: UPS scheduler - power-on hour * Byte 15: UPS scheduler - power-on minute * Byte 16: UPS scheduler - power-off hour * Byte 17: UPS scheduler - power-off minute * Byte 18: UPS scheduler - weekdays * Byte 19: UPS clock - day of month * Byte 20: UPS clock - year (since 1998) (left 4 bits) and month (right 4 bits) * Byte 21: UPS flags (power status, battery status, overload, overheat, nominal input voltage, nominal output voltage) * Byte 22-23: Input frequency data * Byte 24: Packet checksum * Byte 25: Packet delimiter, always 0xFE */ static void comm_receive(const unsigned char *bufptr, size_t size) { size_t i; if (size == PACKET_SIZE) { int checksum = 0; upsdebug_hex(3, "comm_receive: bufptr", bufptr, size); /* Calculate packet checksum */ for (i = 0; i < PACKET_SIZE - 2; i++) { checksum += bufptr[i]; } checksum = checksum % 256; upsdebugx(4, "%s: calculated checksum = 0x%02x, bufptr[23] = 0x%02x", __func__, checksum, bufptr[23]); /* Only proceed if checksum matches and packet delimiter is found */ if (checksum == bufptr[23] && bufptr[24] == 254) { upsdebugx(4, "%s: valid packet received", __func__); memcpy(received_packet, bufptr, PACKET_SIZE); if ((received_packet[0] & 0xF0) == 0xA0 || (received_packet[0] & 0xF0) == 0xB0) { /* If UPS still not detected, compare with available lists */ if (!detected) { ups_model = received_packet[0]; detected = true; } if (!ups_model_defined()) { upslogx(LOG_DEBUG, M_UNKN); } scan_received_pack(); } } } } /** Refresh host time variables */ static void refresh_host_time(void) { const time_t epoch = time(NULL); struct tm now; localtime_r(&epoch, &now); host_year = now.tm_year + 1900; host_month = now.tm_mon + 1; host_day = now.tm_mday; host_week = now.tm_wday; host_hour = now.tm_hour; host_minute = now.tm_min; host_second = now.tm_sec; } /** Query shut-down schedule configuration */ static void setup_poweroff_schedule(void) { bool_t i1 = 0, i2 = 0; char *daysoff; refresh_host_time(); if (testvar("prgshut")) { prgups = atoi(getval("prgshut")); } if (prgups > 0 && prgups < 3) { if (testvar("daysweek")) { device_days_on = bitstring_to_binary(convert_days(getval("daysweek"))); } if (testvar("daysoff")) { daysoff = getval("daysoff"); days_to_shutdown = bitstring_to_binary(daysoff); device_days_off = bitstring_to_binary(convert_days(daysoff)); } if (testvar("houron")) { i1 = set_schedule_time(getval("houron"), 0); } if (testvar("houroff")) { i2 = set_schedule_time(getval("houroff"), 1); } if (i1 && i2 && (device_days_on > 0)) { isprogram = 1; /* If configured to shut-down UPS, push schedule to internal configuration */ if (prgups == 2) { save_ups_config(); } } else { if (i2 == 1 && device_days_off > 0) { isprogram = 1; device_days_on = device_days_off; } } } } /** Check shut-down schedule and sets system to shut down if needed */ static void check_shutdown_schedule(void) { bool_t is_shutdown_day = 0; if (isprogram || prgups == 3) { refresh_host_time(); is_shutdown_day = (days_to_shutdown >> (6 - host_week)) & 0x01; if (is_shutdown_day) { upslogx(LOG_NOTICE, TODAY_DD, hourshut, minshut); if (host_hour == hourshut && host_minute >= minshut) { upslogx(LOG_NOTICE, SHUT_NOW); progshut = 1; } } } } /** Resynchronizes packet boundaries */ static void resynchronize_packet(void) { unsigned char sync_received_byte = 0; unsigned short i; /* Flush serial port buffers */ ser_flush_io(upsfd); upsdebugx(3, "%s: Synchronizing packet boundaries...", __func__); /* * - Read until end-of-response character (0xFE): * read up to 3 packets in size before giving up * synchronizing with the device. */ for (i = 0; i < PACKET_SIZE * 3 && sync_received_byte != RESP_END; i++) { ser_get_char(upsfd, &sync_received_byte, 3, 0); } /* If no packet boundary was found, terminate communication */ if (sync_received_byte != RESP_END) { fatalx(EXIT_FAILURE, NO_SOLIS); } } /** Synchronize packet receiving and setup basic variables */ static void get_base_info(void) { unsigned char packet[PACKET_SIZE]; ssize_t tam; if (testvar("battext")) { battery_extension = atoi(getval("battext")); } setup_poweroff_schedule(); /* dummy read attempt to sync - throw it out */ upsdebugx(3, "%s: sending CMD_UPSCONT and ENDCHAR", __func__); ser_send(upsfd, "%c%c", CMD_UPSCONT, ENDCHAR); resynchronize_packet (); upsdebugx(4, "%s: requesting %d bytes from ser_get_buf_len()", __func__, PACKET_SIZE); tam = ser_get_buf_len(upsfd, packet, PACKET_SIZE, 3, 0); upsdebugx(2, "%s: received %zd bytes from ser_get_buf_len()", __func__, tam); if (tam > 0 && nut_debug_level >= 4) { upsdebug_hex(4, "received from ser_get_buf_len()", packet, (size_t)tam); } comm_receive(packet, (size_t)tam); if (!detected) { fatalx(EXIT_FAILURE, NO_SOLIS); } set_ups_model(); /* Setup power-off times */ if (prgups != 0) { if (prgups == 1) { /* If only this host is meant to be powered off, use proper time. */ hourshut = power_off_hour; minshut = power_off_minute; } else { /* If the UPS is to be powered off too, give * a 5-minute grace time to shutdown hosts */ if (power_off_minute < 5) { if (power_off_hour > 1) hourshut = power_off_hour - 1; else hourshut = 23; minshut = 60 - (5 - power_off_minute); } else { hourshut = power_off_hour; minshut = power_off_minute - 5; } } } /* manufacturer */ dstate_setinfo("ups.mfr", "%s", "APC"); dstate_setinfo("ups.model", "%s", model_name); dstate_setinfo("input.transfer.low", "%03.1f", input_low_limit); dstate_setinfo("input.transfer.high", "%03.1f", input_high_limit); dstate_addcmd("shutdown.return"); /* CMD_SHUTRET */ dstate_addcmd("shutdown.stayoff"); /* CMD_SHUT */ upslogx(LOG_NOTICE, "Detected %s on %s", dstate_getinfo("ups.model"), device_path); print_info(); } /** Retrieves new packet from serial connection and parses it */ static void get_updated_info(void) { unsigned char temp[256]; ssize_t tam; check_shutdown_schedule(); /* get update package */ temp[0] = 0; /* flush temp buffer */ upsdebugx(3, "%s: requesting %d bytes from ser_get_buf_len()", __func__, PACKET_SIZE); tam = ser_get_buf_len(upsfd, temp, PACKET_SIZE, 3, 0); upsdebugx(2, "%s: received %zd bytes from ser_get_buf_len()", __func__, tam); if (tam > 0 && nut_debug_level >= 4) upsdebug_hex(4, "received from ser_get_buf_len()", temp, (size_t)tam); packet_parsed = false; if (temp[24] == RESP_END) { /* Packet boundary found, process packet */ comm_receive(temp, (size_t)tam); packet_parsed = true; } else { /* Malformed packet received, possible boundary desynchronization. */ upsdebugx(3, "%s: Malformed packet received, trying to resynchronize...", __func__); resynchronize_packet (); } } static int instcmd(const char *cmdname, const char *extra) { /* Power-cycle UPS */ if (!strcasecmp(cmdname, "shutdown.return")) { ser_send_char(upsfd, CMD_SHUTRET); /* 0xDE */ return STAT_INSTCMD_HANDLED; } /* Power-off UPS */ if (!strcasecmp(cmdname, "shutdown.stayoff")) { ser_send_char(upsfd, CMD_SHUT); /* 0xDD */ return STAT_INSTCMD_HANDLED; } upslogx(LOG_NOTICE, "instcmd: unknown command [%s] [%s]", cmdname, extra); return STAT_INSTCMD_UNKNOWN; } void upsdrv_initinfo(void) { get_base_info(); upsh.instcmd = instcmd; } void upsdrv_updateinfo(void) { get_updated_info(); if (packet_parsed) { dstate_setinfo("battery.charge", "%03.1f", battery_charge); dstate_setinfo("battery.voltage", "%02.1f", battery_voltage); dstate_setinfo("input.frequency", "%2.1f", input_frequency); dstate_setinfo("input.voltage", "%03.1f", input_voltage); dstate_setinfo("output.current", "%03.1f", output_current); dstate_setinfo("output.power", "%03.1f", apparent_power); dstate_setinfo("output.powerfactor", "%0.2f", load_power_factor / 100.0); dstate_setinfo("output.realpower", "%03.1f", real_power); dstate_setinfo("output.voltage", "%03.1f", output_voltage); dstate_setinfo("ups.temperature", "%2.2f", temperature); dstate_setinfo("ups.load", "%03.1f", ups_load); status_init(); if (!line_unpowered) { status_set("OL"); /* On line */ } else { status_set("OB"); /* On battery */ } if (overload) { status_set("OVER"); /* Overload */ } if (overheat) { status_set("OVERHEAT"); /* Overheat */ } if (recharging) { status_set("CHRG"); /* Charging battery */ } if (critical_battery) { status_set("LB"); /* Critically low battery */ } if (progshut) { /* Software-based shutdown now */ if (prgups == 2) send_shutdown(); /* Send command to shutdown UPS in 4-5 minutes */ /* Workaround for triggering servers' power-off before UPS power-off */ status_set("LB"); } status_commit(); dstate_dataok(); } else { /* * If no packet was processed, report data as stale. * Most likely to be fixed on next received packet. */ dstate_datastale (); } } /*! @brief Power down the attached load immediately. * Basic idea: find out line status and send appropriate command. * - on battery: send normal shutdown, UPS will return by itself on utility * - on line: send shutdown+return, UPS will cycle and return soon. */ void upsdrv_shutdown(void) { if (!line_unpowered) { /* on line */ upslogx(LOG_NOTICE, "On line, sending power cycle command..."); ser_send_char(upsfd, CMD_SHUTRET); } else { upslogx(LOG_NOTICE, "On battery, sending power off command..."); ser_send_char(upsfd, CMD_SHUT); } } void upsdrv_help(void) { printf("\nAPC/Microsol options\n\n"); printf(" Battery extension (AH)\n"); printf(" battext = 80\n\n"); printf(" Scheduled UPS power on/off\n"); printf(" prgshut = 0 (default, no scheduled shutdown)\n"); printf(" prgshut = 1 (software-based shutdown schedule without UPS power-off)\n"); printf(" prgshut = 2 (software-based shutdown schedule with UPS power-off)\n"); printf(" prgshut = 3 (internal UPS shutdown schedule)\n\n"); printf(" Schedule configuration:\n"); printf(" daysweek = 1010101 (power on days)\n"); printf(" daysoff = 1010101 (power off days)\n"); printf(" where each digit is a day from sun...sat with 0 = off and 1 = on\n\n"); printf(" houron = hh:mm hh = hour 0-23 mm = minute 0-59 separated with :\n"); printf(" houroff = hh:mm hh = hour 0-23 mm = minute 0-59 separated with :\n"); printf(" where houron is power-on hour and houroff is shutdown and power-off hour\n\n"); printf(" Use daysweek and houron to programming and save UPS power on/off\n"); printf(" These are valid only if prgshut = 2 or 3\n"); } void upsdrv_makevartable(void) { addvar(VAR_VALUE, "battext", "Battery extension (0-80AH)"); addvar(VAR_VALUE, "prgshut", "Scheduled power-off mode (0-3)"); addvar(VAR_VALUE, "daysweek", "Days of week for UPS shutdown"); addvar(VAR_VALUE, "daysoff", "Days of week for driver-induced shutdown"); addvar(VAR_VALUE, "houron", "Power on hour (hh:mm)"); addvar(VAR_VALUE, "houroff", "Power off hour (hh:mm)"); } void upsdrv_initups(void) { upsfd = ser_open(device_path); ser_set_speed(upsfd, device_path, B9600); ser_set_dtr(upsfd, 1); ser_set_rts(upsfd, 0); } void upsdrv_cleanup(void) { ser_close(upsfd, device_path); }