/* pijuice.c Driver for the PiJuice HAT (www.pijuice.com), addressed via i2c.

	Copyright (C) 2019 Andrew Anderson <aander07@gmail.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
 */

#include "main.h"

#include <sys/ioctl.h>
#include "nut_stdint.h"

#define DRIVER_NAME                         "PiJuice UPS driver"
#define DRIVER_VERSION                      "0.17"

/*
 * Linux I2C userland is a bit of a mess until distros refresh to
 * the i2c-tools 4.x release that profides i2c/smbus.h for userspace
 * instead of (re)using linux/i2c-dev.h, which conflicts with a
 * kernel header of the same name.
 *
 * See:
 * https://i2c.wiki.kernel.org/index.php/Plans_for_I2C_Tools_4
 */
#if HAVE_LINUX_SMBUS_H
#	include <i2c/smbus.h>
#endif
#if HAVE_LINUX_I2C_DEV_H
#	include <linux/i2c-dev.h> /* for I2C_SLAVE */
# if !HAVE_LINUX_SMBUS_H
#  ifndef I2C_FUNC_I2C
#	include <linux/i2c.h>
#  endif
# endif
#endif

/* Forward decls */
static int instcmd(const char *cmdname, const char *extra);

/*
 * i2c-tools pre-4.0 has a userspace header with a name that conflicts
 * with a kernel header, so it may be ignored/removed by distributions
 * when packaging i2c-tools.
 *
 * This will cause the driver to be un-buildable on certain
 * configurations, so include the necessary bits here to handle this
 * situation.
 */
#if WITH_LINUX_I2C
# if !HAVE_DECL_I2C_SMBUS_ACCESS
static inline __s32 i2c_smbus_access(int file, char read_write, __u8 command,
                                     int size, union i2c_smbus_data *data)
{
	struct i2c_smbus_ioctl_data args;
	__s32 err;

	args.read_write = read_write;
	args.command = command;
	args.size = size;
	args.data = data;

	err = ioctl(file, I2C_SMBUS, &args);
	if (err == -1)
		err = -errno;
	return err;
}
# endif

# if !HAVE_DECL_I2C_SMBUS_READ_BYTE_DATA
static inline __s32 i2c_smbus_read_byte_data(int file, __u8 command)
{
	union i2c_smbus_data data;
	int err;

	if ((err = i2c_smbus_access(
		file, I2C_SMBUS_READ, command,
		I2C_SMBUS_BYTE_DATA, &data)) < 0
	) {
		return err;
	} else {
		return 0x0FF & data.byte;
	}
}
# endif

# if !HAVE_DECL_I2C_SMBUS_WRITE_BYTE_DATA
static inline __s32 i2c_smbus_write_byte_data(int file, __u8 command, __u8 value)
{
	union i2c_smbus_data data;
	int err;

	data.byte = value;
	if ((err = i2c_smbus_access(
		file, I2C_SMBUS_WRITE, command,
		I2C_SMBUS_BYTE_DATA, &data)) < 0
	) {
		return err;
	} else {
		return 0x0FF & data.byte;
	}
}
# endif

# if !HAVE_DECL_I2C_SMBUS_READ_WORD_DATA
static inline __s32 i2c_smbus_read_word_data(int file, __u8 command)
{
	union i2c_smbus_data data;
	int err;

	if ((err = i2c_smbus_access(
		file, I2C_SMBUS_READ, command,
		I2C_SMBUS_WORD_DATA, &data)) < 0
	) {
		return err;
	} else {
		return 0x0FFFF & data.word;
	}
}
# endif

# if !HAVE_DECL_I2C_SMBUS_WRITE_WORD_DATA
static inline __s32 i2c_smbus_write_word_data(int file, __u8 command, __u16 value)
{
	union i2c_smbus_data data;
	int err;

	data.word = value;
	if ((err = i2c_smbus_access(
		file, I2C_SMBUS_WRITE, command,
		I2C_SMBUS_WORD_DATA, &data)) < 0
	) {
		return err;
	} else {
		return 0x0FFFF & data.word;
	}
}
# endif

# if !HAVE_DECL_I2C_SMBUS_READ_BLOCK_DATA
static inline __u8* i2c_smbus_read_i2c_block_data(int file, __u8 command, __u8 length, __u8 *values)
{
	union i2c_smbus_data data;
	int err;

	if ( length > I2C_SMBUS_BLOCK_MAX)
	{
		length = I2C_SMBUS_BLOCK_MAX;
	}

	data.block[0] = length;
	memcpy(data.block + 1, values, length);

	if ((err = i2c_smbus_access(
		file, I2C_SMBUS_READ, command,
		I2C_SMBUS_I2C_BLOCK_DATA, &data)) < 0
	) {
		return NULL;
	} else {
		memcpy(values, &data.block[1], data.block[0]);
	}

	return values;
}
# endif
#endif /* if WITH_LINUX_I2C */

#define STATUS_CMD                          0x40
#define CHARGE_LEVEL_CMD                    0x41
#define CHARGE_LEVEL_HI_RES_CMD             0x42
#define FAULT_EVENT_CMD                     0x44
#define BUTTON_EVENT_CMD                    0x45
#define BATTERY_TEMPERATURE_CMD             0x47
#define BATTERY_VOLTAGE_CMD                 0x49
#define BATTERY_CURRENT_CMD                 0x4b
#define IO_VOLTAGE_CMD                      0x4d
#define IO_CURRENT_CMD                      0x4f

#define CHARGING_CONFIG_CMD                 0x51
#define BATTERY_PROFILE_ID_CMD              0x52
#define BATTERY_PROFILE_CMD                 0x53
#define BATTERY_EXT_PROFILE_CMD             0x54
#define BATTERY_TEMP_SENSE_CONFIG_CMD       0x5D

#define POWER_INPUTS_CONFIG_CMD             0x5E
#define RUN_PIN_CONFIG_CMD                  0x5F
#define POWER_REGULATOR_CONFIG_CMD          0x60
#define WATCHDOG_ACTIVATION_CMD             0x61
#define POWER_OFF_CMD                       0x62
#define WAKEUP_ON_CHARGE_CMD                0x63
#define SYSTEM_POWER_SWITCH_CTRL_CMD        0x64

#define LED_STATE_CMD                       0x66
#define LED_BLINK_CMD                       0x68
#define LED_CONFIGURATION_CMD               0x6A
#define BUTTON_CONFIGURATION_CMD            0x6E

#define IO1_CONFIGURATION_CMD               0x72
#define IO1_PIN_ACCESS_CMD                  0x75

#define IO2_CONFIGURATION_CMD               0x77
#define IO2_PIN_ACCESS_CMD                  0x7A

#define I2C_ADDRESS_CMD                     0x7C

#define ID_EEPROM_WRITE_PROTECT_CTRL_CMD    0x7E
#define ID_EEPROM_ADDRESS_CMD               0x7F

#define RTC_TIME_CMD                        0xB0
#define RTC_ALARM_CMD                       0xB9
#define RTC_CTRL_STATUS_CMD                 0xC2

#define RESET_TO_DEFAULT_CMD                0xF0
#define FIRMWARE_VERSION_CMD                0xFD

#define BATT_NORMAL                         0
#define BATT_CHARGING_FROM_IN               1
#define BATT_CHARGING_FROM_5V               2
#define BATT_NOT_PRESENT                    3

#define POWER_NOT_PRESENT                   0
#define POWER_BAD                           1
#define POWER_WEAK                          2
#define POWER_PRESENT                       3

#define LOW_BATTERY_THRESHOLD               25.0
#define HIGH_BATTERY_THRESHOLD              75.0
#define NOMINAL_BATTERY_VOLTAGE             4.18

static uint8_t i2c_address    = 0x14;
static uint8_t shutdown_delay = 30;

/*
 * Flags used to indicate a change in power status
 */
static uint8_t usb_power = 0;
static uint8_t gpio_power = 0;
static uint8_t battery_power = 0;

/*
 * Smooth out i2c read errors by holding the most recent
 * battery charge level reading
 */
static float battery_charge_level = 0;

/* driver description structure */
upsdrv_info_t upsdrv_info = {
	DRIVER_NAME,
	DRIVER_VERSION,
	"Andrew Anderson <aander07@gmail.com>",
	DRV_EXPERIMENTAL,
	{ NULL }
};

/* The macros below all write into a "data" variable defined by the routine
 * scope which calls them, with respective type of uint8_t for "byte" and
 * uint16_t for "word" macros. Native i2c functions operate with __s32 type
 * (currently, signed 32-bit ints?) with negative values for error returns.
 * Note: some manpages refer to "s32" while headers on my and CI systems use
 * a "__s32" type. Maybe this is something to determine in configure script?
 * Code below was fixed to convert the valid values and avoid compiler
 * warnings about comparing whether unsigned ints happened to be negative.
 */
#define I2C_READ_BYTE(fd, cmd, label) \
	{ \
		__s32 sData; \
		if ((sData = i2c_smbus_read_byte_data(upsfd, cmd)) < 0 ) { \
			upsdebugx(2, "Failure reading the i2c bus [%s]", label); \
			return; \
		} ; \
		data = (uint8_t) sData; \
	}

#define I2C_WRITE_BYTE(fd, cmd, value, label) \
	{ \
		if ( i2c_smbus_write_byte_data(upsfd, cmd, value) < 0 ) { \
			upsdebugx(2, "Failure writing to the i2c bus [%s]", label); \
			return; \
		} ; \
	}

#define I2C_READ_WORD(fd, cmd, label) \
	{ \
		__s32 sData; \
		if ((sData = i2c_smbus_read_word_data(upsfd, cmd)) < 0 ) { \
			upsdebugx(2, "Failure reading the i2c bus [%s]", label); \
			return; \
		} ; \
		data = (uint16_t) sData; \
	}

#define I2C_READ_BLOCK(fd, cmd, size, block, label) \
	if ((i2c_smbus_read_i2c_block_data(upsfd, cmd, size, block)) < 0 ) { \
		upsdebugx(2, "Failure reading the i2c bus [%s]", label); \
		return; \
	}

static inline int open_i2c_bus(char *path, uint8_t addr)
{
	int file;

	if ((file = open(path, O_RDWR)) < 0)
	{
		fatal_with_errno(EXIT_FAILURE, "Failed to open the i2c bus on %s", path);
	}

	if (ioctl(file, I2C_SLAVE, addr) < 0)
	{
		fatal_with_errno(EXIT_FAILURE, "Failed to acquire the i2c bus and/or talk to the UPS");
	}

	return file;
}

static void get_charge_level_hi_res(void)
{
	uint8_t cmd = CHARGE_LEVEL_HI_RES_CMD;
	uint16_t data;

	upsdebugx( 3, __func__ );

	I2C_READ_WORD( upsfd, cmd, __func__ )

	/*
	 * Use an external variable to allow for missed i2c bus
	 * reads; the charge level data may be slightly stale,
	 * but no other options seem reasonable:
	 *
	 * 1) store 0
	 * 	Leads to a false report of a depleted battery, possibly
	 * 	triggering an immediate shutdown if on battery power only
	 * 2) store -1
	 * 	Adds a lot of logic to "skip" over negative charge levels,
	 * 	which effectively accomplishes the same thing
	 * 3) retry the read immediately
	 * 	Could tie up the i2c bus and make matters exponentially worse
	 */
	battery_charge_level = data / 10.0;

	upsdebugx( 1, "Battery Charge Level: %02.1f%%", battery_charge_level );
	dstate_setinfo( "battery.charge", "%02.1f", battery_charge_level );
}

static void get_status(void)
{
	uint8_t	cmd = STATUS_CMD, data, batteryStatus, powerInput, powerInput5vIo;

	upsdebugx( 3, __func__ );

	I2C_READ_BYTE( upsfd, cmd, __func__ )

	batteryStatus = data >> 2 & 0x03;
	switch( batteryStatus )
	{
		case BATT_NORMAL:
			upsdebugx( 1, "Battery Status: Normal" );
			dstate_setinfo( "battery.packs", "%d", 1 );
			dstate_setinfo( "battery.packs.bad", "%d", 0 );
			break;
		case BATT_CHARGING_FROM_IN:
			upsdebugx( 1, "Battery Status: Charging from IN" );
			dstate_setinfo( "battery.packs", "%d", 1 );
			dstate_setinfo( "battery.packs.bad", "%d", 0 );
			break;
		case BATT_CHARGING_FROM_5V:
			upsdebugx( 1, "Battery Status: Charging from 5V" );
			dstate_setinfo( "battery.packs", "%d", 1 );
			dstate_setinfo( "battery.packs.bad", "%d", 0 );
			break;
		case BATT_NOT_PRESENT:
			upsdebugx( 1, "Battery Status: Not Present" );
			dstate_setinfo( "battery.packs", "%d", 0 );
			dstate_setinfo( "battery.packs.bad", "%d", 1 );
			break;
		default:
			upsdebugx( 1, "battery.status: UNKNOWN" );
	}

	powerInput = data >> 4 & 0x03;
	switch( powerInput )
	{
		case POWER_NOT_PRESENT:
			upsdebugx( 1, "Power Input: Not Present" );
			break;
		case POWER_BAD:
			upsdebugx( 1, "Power Input: Bad" );
			break;
		case POWER_WEAK:
			upsdebugx( 1, "Power Input: Weak" );
			break;
		case POWER_PRESENT:
			upsdebugx( 1, "Power Input: Present" );
			break;
		default:
			upsdebugx( 1, "Power Input: UNKNOWN" );
	}

	powerInput5vIo = data >> 6 & 0x03;
	switch( powerInput5vIo )
	{
		case POWER_NOT_PRESENT :
			upsdebugx(1, "Power Input 5v: Not Present");
			break;
		case POWER_BAD:
			upsdebugx(1, "Power Input 5v: Bad");
			break;
		case POWER_WEAK:
			upsdebugx(1, "Power Input 5v: Weak");
			break;
		case POWER_PRESENT:
			upsdebugx(1, "Power Input 5v: Present");
			break;
		default:
			upsdebugx(1, "Power Input 5v: UNKNOWN");
	}

	if ( batteryStatus == BATT_NORMAL
	 ||  batteryStatus == BATT_CHARGING_FROM_IN
	 ||  batteryStatus == BATT_CHARGING_FROM_5V )
	{
		get_charge_level_hi_res();

		if ( battery_charge_level <= LOW_BATTERY_THRESHOLD )
		{
			upsdebugx( 1, "Battery Charge Status: LOW" );
			status_set("LB");
		}
		else if ( battery_charge_level > HIGH_BATTERY_THRESHOLD )
		{
			upsdebugx( 1, "Battery Charge Status: HIGH" );
			status_set("HB");
		}
	}
	else if ( batteryStatus == BATT_NOT_PRESENT )
	{
		status_set("RB");
	}

	if ( batteryStatus  <= BATT_NOT_PRESENT
	 &&  powerInput     <= POWER_PRESENT
	 &&  powerInput5vIo <= POWER_PRESENT )
	{
		if ( powerInput       == POWER_NOT_PRESENT
		 &&  ( powerInput5vIo != POWER_NOT_PRESENT ))
		{
			if ( usb_power != 1 || gpio_power != 0 )
			{
				upslogx( LOG_NOTICE, "On USB power" );
			}
			usb_power     = 1;
			gpio_power    = 0;
			battery_power = 0;
			upsdebugx( 1, "On USB power [%d:%d:%d]", usb_power, gpio_power, battery_power );

			status_set("OL");
			if ( batteryStatus == BATT_CHARGING_FROM_5V )
			{
				status_set("CHRG");
				upsdebugx( 1, "Battery Charger Status: charging" );
				dstate_setinfo( "battery.charger.status", "%s", "charging" );
			}
			else if ( batteryStatus == BATT_NORMAL )
			{
				upsdebugx( 1, "Battery Charger Status: resting" );
				dstate_setinfo( "battery.charger.status", "%s", "resting" );
			}
		}
		else if ( powerInput5vIo == POWER_NOT_PRESENT
		      &&  powerInput     != POWER_NOT_PRESENT
		      &&  powerInput     <= POWER_PRESENT )
		{
			if ( gpio_power != 1 || usb_power != 0 )
			{
				upslogx( LOG_NOTICE, "On 5V_GPIO power" );
			}
			usb_power     = 0;
			gpio_power    = 1;
			battery_power = 0;
			upsdebugx( 1, "On 5V_GPIO power [%d:%d:%d]", usb_power, gpio_power, battery_power );

			status_set("OL");
			if ( batteryStatus == BATT_CHARGING_FROM_IN )
			{
				status_set("CHRG");
				upsdebugx( 1, "Battery Charger Status: charging" );
				dstate_setinfo( "battery.charger.status", "%s", "charging" );
			}
			else if ( batteryStatus == BATT_NORMAL )
			{
				upsdebugx( 1, "Battery Charger Status: resting" );
				dstate_setinfo( "battery.charger.status", "%s", "resting" );
			}
		}
		else if ( ( powerInput     != POWER_NOT_PRESENT && powerInput     <= POWER_PRESENT )
		       && ( powerInput5vIo != POWER_NOT_PRESENT && powerInput5vIo <= POWER_PRESENT ))
		{
			if ( usb_power != 1 || gpio_power != 1 )
			{
				upslogx( LOG_NOTICE, "On USB and 5V_GPIO power" );
			}
			usb_power     = 1;
			gpio_power    = 1;
			battery_power = 0;
			upsdebugx( 1, "On USB and 5V_GPIO power [%d:%d:%d]", usb_power, gpio_power, battery_power );

			status_set("OL");
			if ( batteryStatus == BATT_CHARGING_FROM_IN )
			{
				status_set("CHRG");
				upsdebugx( 1, "Battery Charger Status: charging" );
				dstate_setinfo("battery.charger.status", "%s", "charging");
			}
			else if ( batteryStatus == BATT_NORMAL )
			{
				upsdebugx( 1, "Battery Charger Status: resting" );
				dstate_setinfo( "battery.charger.status", "%s", "resting" );
			}
		}
		else if ( powerInput == POWER_NOT_PRESENT && powerInput5vIo == POWER_NOT_PRESENT )
		{
			if ( usb_power != 0 || gpio_power != 0 )
			{
				upslogx( LOG_NOTICE, "On Battery power" );
			}
			usb_power     = 0;
			gpio_power    = 0;
			battery_power = 1;
			upsdebugx( 1, "On Battery power [%d:%d:%d]", usb_power, gpio_power, battery_power );

			status_set("OB");
			status_set("DISCHRG");
		}
	}
}

static void get_battery_temperature(void)
{
	uint8_t cmd = BATTERY_TEMPERATURE_CMD;
	int16_t data;

	upsdebugx( 3, __func__ );

	I2C_READ_WORD( upsfd, cmd, __func__ )

	upsdebugx( 1, "Battery Temperature: %d°C", data );
	dstate_setinfo( "battery.temperature", "%d", data );
}

static void get_battery_voltage(void)
{
	uint8_t cmd = BATTERY_VOLTAGE_CMD;
	int16_t data;

	upsdebugx( 3, __func__ );

	I2C_READ_WORD( upsfd, cmd, __func__ )

	upsdebugx( 1, "Battery Voltage: %0.3fV", data / 1000.0 );
	dstate_setinfo( "battery.voltage", "%0.3f", data / 1000.0 );
}

static void get_battery_current(void)
{
	uint8_t cmd = BATTERY_CURRENT_CMD;
	int16_t data;

	upsdebugx( 3, __func__ );

	/*
	 * The reported current can actually be negative, so we cannot
	 * check for I2C failure by looking for negative values
	 */
	data = i2c_smbus_read_word_data(upsfd, cmd);

	if ( data & ( 1 << 15 ) )
	{
		data = data - ( 1 << 16 );
	}

	upsdebugx( 1, "Battery Current: %0.3fA", data / 1000.0 );
	dstate_setinfo( "battery.current", "%0.3f", data / 1000.0 );
}

static void get_io_voltage(void)
{
	uint8_t cmd = IO_VOLTAGE_CMD;
	int16_t data;

	upsdebugx( 3, __func__ );

	I2C_READ_WORD( upsfd, cmd, __func__ )

	upsdebugx( 1, "Input Voltage: %.3fV", data / 1000.0 );
	dstate_setinfo( "input.voltage", "%.3f", data / 1000.0 );
}

static void get_io_current(void)
{
	uint8_t cmd = IO_CURRENT_CMD;
	int16_t data;

	upsdebugx( 3, __func__ );

	/*
	 * The reported current can actually be negative, so we cannot
	 * check for I2C failure by looking for negative values
	 */
	data = i2c_smbus_read_word_data(upsfd, cmd);

	if ( data & ( 1 << 15 ) )
	{
		data = data - ( 1 << 16 );
	}

	upsdebugx( 1, "Input Current: %.3fA", data / 1000.0 );
	dstate_setinfo( "input.current", "%.3f", data / 1000.0 );
}

static void get_firmware_version(void)
{
	uint8_t cmd = FIRMWARE_VERSION_CMD;
	uint16_t data;
	uint8_t major, minor;

	upsdebugx( 3, __func__ );

	I2C_READ_WORD( upsfd, cmd, __func__ )

	major = data >> 4;
	minor = ( data << 4 & 0xf0 ) >> 4;

	if (( major != 1 ) || ( minor > 3 ))
	{
		upslogx( LOG_WARNING, "Unknown Firmware release: %d.%d", major, minor );
	}

	upsdebugx( 1, "UPS Firmware Version: %d.%d", major, minor );
	dstate_setinfo( "ups.firmware", "%d.%d", major, minor );
}

static void get_battery_profile(void)
{
	uint8_t cmd = BATTERY_PROFILE_CMD;
	__u8 block[I2C_SMBUS_BLOCK_MAX];

	upsdebugx( 3, __func__ );

	I2C_READ_BLOCK( upsfd, cmd, 14, block, __func__ )

	upsdebugx( 1, "Battery Capacity: %0.3fAh", ( block[1] << 8 | block[0] ) / 1000.0 );
	dstate_setinfo( "battery.capacity", "%0.3f", ( block[1] << 8 | block[0] ) / 1000.0 );
}

static void get_battery_profile_ext(void)
{
	uint8_t cmd = BATTERY_EXT_PROFILE_CMD;
	__u8 block[I2C_SMBUS_BLOCK_MAX];

	upsdebugx( 3, __func__ );

	I2C_READ_BLOCK( upsfd, cmd, 17, block, __func__ )

	switch( block[0] & 0xFF00 )
	{
		case 0:
			upsdebugx( 1, "Battery Chemistry: LiPO" );
			dstate_setinfo( "battery.type", "%s", "LiPO" );
			break;
		case 1:
			upsdebugx( 1, "Battery Chemistry: LiFePO4" );
			dstate_setinfo( "battery.type", "%s", "LiFePO4" );
			break;
		default:
			upsdebugx( 1, "Battery Chemistry: UNKNOWN" );
			dstate_setinfo( "battery.type", "%s", "UNKNOWN" );
	}
}

static void get_power_off(void)
{
	uint8_t cmd = POWER_OFF_CMD;
	uint8_t data;

	upsdebugx( 3, __func__ );

	I2C_READ_BYTE( upsfd, cmd, __func__ )

	if ( data == 255 )
	{
		upsdebugx( 1, "Power Off: DISABLED" );
	}
	else if ( data <= 250 )
	{
		upsdebugx( 1, "Power Off: %d Seconds", data );
	}
}

static void set_power_off(void)
{
	uint8_t cmd = POWER_OFF_CMD;

	upsdebugx( 3, __func__ );

	/*
	 * Acceptable values for shutdown_delay are 1-250,
	 * use 0/255 to clear a scheduled power off command
	 */

	if ( shutdown_delay > 250 )
	{
		upslogx(
			LOG_WARNING,
			"shutdown delay of >250 seconds requested, shortening to 250 seconds"
		);
		shutdown_delay = 250;
	}

	if ( shutdown_delay == 0 )
	{
		upslogx(
			LOG_WARNING,
			"shutdown delay of 0 seconds requested, using 1 second instead"
		);
		shutdown_delay = 1;
	}

	I2C_WRITE_BYTE( upsfd, cmd, shutdown_delay, __func__ )
}

static void get_time(void)
{
	uint8_t cmd = RTC_TIME_CMD;
	__u8 block[I2C_SMBUS_BLOCK_MAX];
	uint8_t second, minute, hour, day, month, subsecond;
	uint16_t year;

	upsdebugx( 3, __func__ );

	I2C_READ_BLOCK( upsfd, cmd, 9, block, __func__ )

	second     = (( (block[0] >> 4 ) & 0x07) * 10 ) + ( block[0] & 0x0F );
	minute     = (( (block[1] >> 4 ) & 0x07) * 10 ) + ( block[1] & 0x0F );
	hour       = (( (block[2] >> 4 ) & 0x03) * 10 ) + ( block[2] & 0x0F );
	day        = (( (block[4] >> 4 ) & 0x03) * 10 ) + ( block[4] & 0x0F );
	month      = (( (block[5] >> 4 ) & 0x01) * 10 ) + ( block[5] & 0x0F );
	year       = (( (block[6] >> 4 ) & 0x0F) * 10 ) + ( block[6] & 0x0F ) + 2000;
	subsecond  = block[7] * 100 / 256;

	upsdebugx( 1, "UPS Time: %02d:%02d:%02d.%02d", hour, minute, second, subsecond );
	dstate_setinfo( "ups.time", "%02d:%02d:%02d.%02d", hour, minute, second, subsecond );

	upsdebugx( 1, "UPS Date: %04d-%02d-%02d", year, month, day );
	dstate_setinfo( "ups.date", "%04d-%02d-%02d", year, month, day );
}

static void get_i2c_address(void)
{
	uint8_t cmd = I2C_ADDRESS_CMD;
	uint8_t data;

	upsdebugx( 3, __func__ );

	I2C_READ_BYTE( upsfd, cmd, __func__ )

	upsdebugx( 1, "I2C Address: 0x%0x", data );

	if ( data == i2c_address )
	{
		upsdebugx( 1, "Found device '0x%0x' on port '%s'",
			(unsigned int) i2c_address, device_path );
	}
	else
	{
		fatalx( EXIT_FAILURE,
			"Could not find PiJuice HAT at I2C address 0x%0x",
			i2c_address );
	}
}

void upsdrv_initinfo(void)
{
	/* probe ups type */
	get_firmware_version();

	/* get variables and flags from the command line */

	if (getval("i2c_address"))
		i2c_address = atoi(getval("i2c_address"));

	dstate_setinfo( "ups.mfr", "%s", "PiJuice" );
	dstate_setinfo( "ups.type", "%s", "HAT" );

	/* note: for a transition period, these data are redundant */

	dstate_setinfo( "device.mfr", "%s", "PiJuice" );
	dstate_setinfo( "device.type", "%s", "HAT" );

	upsdebugx( 1, "Low Battery Threshold: %0.0f%%", LOW_BATTERY_THRESHOLD );
	dstate_setinfo( "battery.charge.low", "%0.0f", LOW_BATTERY_THRESHOLD );

	upsdebugx( 1, "Nominal Battery Voltage: %0.3fV", NOMINAL_BATTERY_VOLTAGE );
	dstate_setinfo( "battery.voltage.nominal", "%0.3f", NOMINAL_BATTERY_VOLTAGE );

	get_i2c_address();
	get_battery_profile();
	get_battery_profile_ext();

	/* commands ----------------------------------------------- */
	/* FIXME: Check with the device what our instcmd
	 * (nee upsdrv_shutdown() contents) actually does!
	 */
	dstate_addcmd("shutdown.stayoff");

	/* install handlers */
	upsh.instcmd = instcmd;
}

void upsdrv_updateinfo(void)
{
	status_init();

	get_status();
	get_battery_temperature();
	get_battery_voltage();
	get_battery_current();
	get_io_voltage();
	get_io_current();
	get_time();
	get_power_off();

	status_commit();
	dstate_dataok();
}

/* handler for commands to be sent to UPS */
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);

	/* FIXME: Which one is this - "load.off",
	 * "shutdown.stayoff" or "shutdown.return"? */

	/* Shutdown UPS */
	if (!strcasecmp(cmdname, "shutdown.stayoff")) {
		upslog_INSTCMD_POWERSTATE_CHANGE(cmdname, extra);

		set_power_off();

		return STAT_INSTCMD_HANDLED;
	}

	upslog_INSTCMD_UNKNOWN(cmdname, extra);
	return STAT_INSTCMD_UNKNOWN;
}

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

	/* FIXME: Check with the device what our instcmd
	 * (nee upsdrv_shutdown() contents) actually does!
	 */
	int	ret = do_loop_shutdown_commands("shutdown.stayoff", NULL);
	if (handling_upsdrv_shutdown > 0)
		set_exit_flag(ret == STAT_INSTCMD_HANDLED ? EF_EXIT_SUCCESS : EF_EXIT_FAILURE);
}

void upsdrv_help(void)
{
	printf("\nThe default I2C address is 20 [0x14]\n");
	printf("\n");
}

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

void upsdrv_makevartable(void)
{
	addvar(VAR_VALUE, "i2c_address", "Override i2c address setting");
}

void upsdrv_initups(void)
{
	upsfd = open_i2c_bus( device_path, i2c_address );
}

void upsdrv_cleanup(void)
{
	close(upsfd);
}
