/*
 * nutdrv_siemens_sitop.c - model specific routines for the Siemens SITOP UPS500 series
 *
 *
 * Copyright (C) 2018 Matthijs H. ten Berge <m.tenberge@awl.nl>
 *
 * 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
 */

/*
 * Notes:
 * These UPSes operate at 24V DC (both input and output), instead of 110V/230V AC.
 * Therefore, the line input is also referred to by Siemens as 'DC'.
 *
 * The device is configured via DIP-switches.
 * For correct functioning in combination with NUT, set the DIP-switches to the following:
 * switch 1-4: choose whatever suits your situation. Any combination will work with NUT.
 * switch 5 ('=>' / 't'): set to OFF ('t')
 * switch 6-10 (delay): set to OFF (no additional delay)
 * switch 11 (INTERR.): set to ON
 * switch 12 (ON/OFF): set to ON
 *
 * These UPSes are available with serial or USB port.
 * Both are supported by this driver. The version with USB port simply contains
 * a serial-over-USB chip, so as far as this driver is concerned, all models are
 * actually serial models.
 *
 * The FTDI USB-to-serial converters in the USB-models are programmed with a non-standard
 * Product ID (mine had 0403:e0e3), but can be used with the normal ftdi_sio driver:
 * # modprobe ftdi_sio
 * # echo 0403 e0e3 > /sys/bus/usb-serial/drivers/ftdi_sio/new_id
 *
 * This can also be automated via a udev rule:
 * ACTION=="add", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="e0e3", \
 *   RUN+="/sbin/modprobe ftdi_sio", \
 *   RUN+="/bin/sh -c 'echo 0403 e0e3 > /sys/bus/usb-serial/drivers/ftdi_sio/new_id'"
 *
 * Use the following udev rule to create a persistent device name, for example /dev/ttyUPS:
 * SUBSYSTEM=="tty" ATTRS{idVendor}=="0403", ATTRS{idProduct}=="e0e3" SYMLINK+="ttyUPS"
 */

#include "main.h"
#include "serial.h"
#include "nut_stdint.h"

#define DRIVER_NAME	"Siemens SITOP UPS500 series driver"
#define DRIVER_VERSION	"0.08"

#define RX_BUFFER_SIZE 100

/* driver description structure */
upsdrv_info_t upsdrv_info = {
	DRIVER_NAME,
	DRIVER_VERSION,
	"Matthijs H. ten Berge <m.tenberge@awl.nl>",
	DRV_EXPERIMENTAL,
	{ NULL }
};

/* The maximum number of consecutive polls in which the UPS does not provide any data: */
static unsigned int max_polls_without_data;
/* The current number: */
static unsigned int nr_polls_without_data;
/* receive buffer */
static char rx_buffer[RX_BUFFER_SIZE];
static size_t rx_count;

static struct {
	int battery_alarm;
	int dc_input_low;
	int on_battery;
	int battery_above_85_percent;
} current_ups_status;

/* remove n bytes from the head of rx_buffer, shift the remaining bytes to the start */
static void rm_buffer_head(unsigned int n) {
	if (rx_count <= n) {
		/* nothing left */
		rx_count = 0;
		return;
	}
	rx_count -= n;
	memmove(rx_buffer, rx_buffer + n, rx_count);
}

/* parse incoming data from the UPS.
 * return true if something new was received.
 */
static int check_for_new_data(void) {
	int new_data_received = 0;
	int done = 0;
	ssize_t num_received;

	while (!done) {
		/* Get new data from the serial port.
		 * No extra delay, just get the chars that were already buffered.
		 */
		num_received = ser_get_buf(upsfd, rx_buffer + rx_count, RX_BUFFER_SIZE - rx_count, 0, 0);
		if (num_received < 0) {
			/* comm error */
			ser_comm_fail("error %" PRIiSIZE " while reading", num_received);
			/* discard any remaining old data from the receive buffer: */
			rx_count = 0;
			/* try to re-open the serial port: */
			if (upsfd) {
				ser_close(upsfd, device_path);
				upsfd = 0;
			}
			upsfd = ser_open_nf(device_path);
			ser_set_speed_nf(upsfd, device_path, B9600);
			done = 1;
		} else if (num_received == 0) {
			/* no (more) new data */
			done = 1;
		} else {
			rx_count += (unsigned int)num_received;

			/* parse received input data: */
			while (rx_count >= 5) { /* all valid input messages are strings of 5 characters */
				if (strncmp(rx_buffer, "BUFRD", 5) == 0) {
					current_ups_status.battery_alarm = 0;
				} else if (strncmp(rx_buffer, "ALARM", 5) == 0) {
					current_ups_status.battery_alarm = 1;
				} else if (strncmp(rx_buffer, "DC_OK", 5) == 0) {
					current_ups_status.dc_input_low = 0;
				} else if (strncmp(rx_buffer, "DC_LO", 5) == 0) {
					current_ups_status.dc_input_low = 1;
				} else if (strncmp(rx_buffer, "*****", 5) == 0) {
					current_ups_status.on_battery = 0;
				} else if (strncmp(rx_buffer, "*BAT*", 5) == 0) {
					current_ups_status.on_battery = 1;
				} else if (strncmp(rx_buffer, "BA>85", 5) == 0) {
					current_ups_status.battery_above_85_percent = 1;
				} else if (strncmp(rx_buffer, "BA<85", 5) == 0) {
					current_ups_status.battery_above_85_percent = 0;
				} else {
					/* nothing sensible found at the start of the rx_buffer. */
					rm_buffer_head(1);
					continue; /* skip the code below */
				}
				rm_buffer_head(5);
				new_data_received = 1;
			}
		}
	}
	return new_data_received;
}


static int instcmd(const char *cmdname, const char *extra) {
	/* May be used in logging below, but not as a command argument */
	NUT_UNUSED_VARIABLE(extra);
	upsdebug_INSTCMD_STARTING(cmdname, extra);

	/* Note: the UPS does not really like to receive data.
	 * For example, sending an "R" without \n hangs the serial port.
	 * In that situation, the UPS will no longer send any status updates.
	 * For this reason, an additional \n is appended here.
	 * The commands are sent twice, because the first command is sometimes
	 * lost as well.
	 */
	if (!strcasecmp(cmdname, "shutdown.return")) {
		upslog_INSTCMD_POWERSTATE_CHANGE(cmdname, extra);
		upsdebugx(1, "instcmd: sending command R");
		ser_send_pace(upsfd, 200000, "\n\nR\n\n");
		ser_send_pace(upsfd, 200000, "R\n\n");
		return STAT_INSTCMD_HANDLED;
	}
	if (!strcasecmp(cmdname, "shutdown.stayoff")) {
		upslog_INSTCMD_POWERSTATE_CHANGE(cmdname, extra);
		upsdebugx(1, "instcmd: sending command S");
		ser_send_pace(upsfd, 200000, "\n\nS\n\n");
		ser_send_pace(upsfd, 200000, "S\n\n");
		return STAT_INSTCMD_HANDLED;
	}

	upslog_INSTCMD_UNKNOWN(cmdname, extra);
	return STAT_INSTCMD_UNKNOWN;
}


void upsdrv_initinfo(void) {
	int max_attempts = 5;
	int found = 0;
	while (!found && max_attempts > 0) {
		if (check_for_new_data()) {
			found = 1;
		} else {
			sleep(1); /* Sleep a while, then try again */
		}
		max_attempts--;
	}
	if (!found) {
		fatalx(EXIT_FAILURE, "No data received from the UPS");
	}

	dstate_setinfo("device.mfr", "Siemens");
	dstate_setinfo("device.model", "SITOP UPS500 series");

	/* supported commands: */
	dstate_addcmd("shutdown.stayoff");
	dstate_addcmd("shutdown.return");

	upsh.instcmd = instcmd;
}


void upsdrv_updateinfo(void) {
	if (check_for_new_data()) {
		nr_polls_without_data = 0;
	} else {
		nr_polls_without_data++;
		/* With unsigned int type, we can limit half-way like this: */
		if (nr_polls_without_data > INT_MAX)
			nr_polls_without_data = INT_MAX;
	}

	if (nr_polls_without_data > max_polls_without_data) {
		/* data is stale */
		dstate_datastale();
		return;
	}

	/* This is all we know about the charge level... */
	dstate_setinfo("battery.charge.approx",
		(current_ups_status.battery_above_85_percent) ? ">85" : "<85");

	status_init();

	if (current_ups_status.dc_input_low || current_ups_status.on_battery) {
		status_set("OB");
	} else {
		status_set("OL");
	}
	if (current_ups_status.battery_alarm) {
		status_set("LB");
	}

	status_commit();
	dstate_dataok();
	ser_comm_good();
}

void upsdrv_shutdown(void) {
	/* Only implement "shutdown.default"; do not invoke
	 * general handling of other `sdcommands` here */

	/* by default, tell the UPS to shut down,
	 * then return - DO NOT SLEEP HERE */
	int	ret = do_loop_shutdown_commands("shutdown.return", NULL);
	if (handling_upsdrv_shutdown > 0)
		set_exit_flag(ret == STAT_INSTCMD_HANDLED ? EF_EXIT_SUCCESS : EF_EXIT_FAILURE);
}


void upsdrv_help(void) {
}

/* optionally tweak prognames[] entries */
void upsdrv_tweak_prognames(void)
{
}

/* list flags and values that you want to receive via -x */
void upsdrv_makevartable(void) {
	/* allow '-x max_polls_without_data=<some value>' */
	addvar(VAR_VALUE, "max_polls_without_data", "The maximum number of consecutive polls in which the UPS does not provide any data.");
}

void upsdrv_initups(void) {
	char * maxPollsString;
	unsigned int parsed;

	upsfd = ser_open(device_path);
	ser_set_speed(upsfd, device_path, B9600);

	/*
	 * Fast polling is preferred, because
	 * A) the UPS spits out new data every 75 msec,
	 * B) some models in this SITOP series have a _very_ small capacity
	 *    (< 10sec runtime), so every second might count.
	 */
	if (poll_interval > 5) {
		upslogx(LOG_NOTICE,
			"Option poll_interval is recommended to be lower than 5 (found: %" PRIdMAX ")",
			(intmax_t)poll_interval);
	}

	/* option max_polls_without_data: */
	max_polls_without_data = 2;
	maxPollsString = getval("max_polls_without_data");
	if (maxPollsString) {
		if (str_to_uint(maxPollsString, &parsed, 10) == 1) {
			max_polls_without_data = parsed;
		} else {
			upslog_with_errno(LOG_ERR, "Cannot parse option max_polls_without_data");
		}
	}
}

void upsdrv_cleanup(void) {
	ser_close(upsfd, device_path);
}
