810 lines
22 KiB
C
810 lines
22 KiB
C
|
/* microsol-common.c - common framework for Microsol Solis-based UPS hardware
|
||
|
|
||
|
Copyright (C) 2004 Silvino B. Magalhães <sbm2yk@gmail.com>
|
||
|
2019 Roberto Panerai Velloso <rvelloso@gmail.com>
|
||
|
2021 Ygor A. S. Regados <ygorre@tutanota.com>
|
||
|
|
||
|
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 <time.h>
|
||
|
#include <ctype.h>
|
||
|
#include <stdio.h>
|
||
|
#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);
|
||
|
}
|