diff --git a/configure.ac b/configure.ac index ca5212050f..ffc3c302a6 100644 --- a/configure.ac +++ b/configure.ac @@ -523,14 +523,30 @@ NUT_ARG_WITH([linux_i2c], [build and install i2c drivers], [auto]) if test "${nut_with_linux_i2c}" != no; then case ${target_os} in linux* ) + AC_CHECK_HEADER( + [linux/i2c-dev.h], + [AC_DEFINE([HAVE_LINUX_I2C_DEV_H], [1], + [Define to 1 if you have .])] + ) + AC_CHECK_HEADER( + [i2c/smbus.h], + [AC_DEFINE([HAVE_LINUX_SMBUS_H], [1], + [Define to 1 if you have .])] + ) AC_CHECK_DECLS( - [i2c_smbus_read_word_data, i2c_smbus_write_word_data, i2c_smbus_read_block_data], + [i2c_smbus_access, i2c_smbus_read_byte_data, i2c_smbus_write_byte_data, i2c_smbus_read_word_data, i2c_smbus_write_word_data, i2c_smbus_read_block_data], [nut_with_linux_i2c="yes"], [nut_with_linux_i2c="no"], [#include + #ifdef HAVE_LINUX_I2C_DEV_H #include + #endif + #ifdef HAVE_LINUX_SMBUS_H + #include + #endif ] ) + AC_SEARCH_LIBS([i2c_smbus_read_byte], [i2c]) ;; * ) nut_with_linux_i2c="no" diff --git a/docs/man/Makefile.am b/docs/man/Makefile.am index fef33f6db0..86434fb33d 100644 --- a/docs/man/Makefile.am +++ b/docs/man/Makefile.am @@ -620,16 +620,16 @@ endif HTML_MODBUS_MANS = phoenixcontact_modbus.html -SRC_LINUX_I2C_PAGES = asem.txt +SRC_LINUX_I2C_PAGES = asem.txt pijuice.txt if WITH_MANS -MAN_LINUX_I2C_PAGES = asem.8 +MAN_LINUX_I2C_PAGES = asem.8 pijuice.8 endif if WITH_LINUX_I2C man8_MANS += $(MAN_LINUX_I2C_PAGES) endif -HTML_LINUX_I2C_MANS = asem.html +HTML_LINUX_I2C_MANS = asem.html pijuice.html # SOME_DRIVERS endif diff --git a/docs/man/index.txt b/docs/man/index.txt index d32175db31..b15c6a4518 100644 --- a/docs/man/index.txt +++ b/docs/man/index.txt @@ -91,6 +91,7 @@ Drivers - linkman:oneac[8] - linkman:optiups[8] - linkman:phoenixcontact_modbus[8] +- linkman:pijuice[8] - linkman:powercom[8] - linkman:powerman-pdu[8] - linkman:powerpanel[8] diff --git a/docs/man/pijuice.txt b/docs/man/pijuice.txt new file mode 100644 index 0000000000..1643208f34 --- /dev/null +++ b/docs/man/pijuice.txt @@ -0,0 +1,81 @@ +PIJUICE(8) +========== + +NAME +---- +pijuice - driver for UPS in PiJuice HAT + +NOTE +---- +This man page only documents the hardware-specific features of the +*pijuice* driver. For information about the core driver, see +linkman:nutupsdrv[8]. + +NOTE: This manual page was hastily adapted from related `asem` driver +manpage based on information from the original pull request, and so +may not fully apply to PiJuice HAT, patches from experts are welcome. + +SUPPORTED HARDWARE +------------------ +The *pijuice* driver supports the portable PiJuice HAT UPS for Raspberry Pi +embedded PCs. + +EXTRA ARGUMENTS +--------------- + +The required parameter for this driver is the I2C bus name: + +*port*='dev-node':: +On the PiJuice HAT, this should be `/dev/i2c-1`. + +INSTALLATION +------------ +NOTE: This section was copied from `asem` driver manpage and may not fully +apply to PiJuice HAT, patches are welcome. + +This driver is specific to the Linux I2C API, and requires the lm_sensors +libi2c-dev or its equivalent to compile. + +Beware that the SystemIO memory used by the I2C controller is reserved by ACPI. +If only a native I2C driver (e.g. i2c_i801, as of 3.5.X Linux kernels) is +available, then you'll need to relax the ACPI resources check. For example, you +can boot with the `acpi_enforce_resources=lax` option. + +////////////////////////////////////////// +Optional: use DIAGNOSTICS to describe troubleshooting techniques that are +longer than what can be conveniently described in the driver error messages. + +DIAGNOSTICS +----------- + +////////////////////////////////////////// + +KNOWN ISSUES AND BUGS +--------------------- +NOTE: This section was copied from `asem` driver manpage and may not fully +apply to PiJuice HAT, patches are welcome. + +The driver shutdown function is not implemented, so other arrangements must be +made to turn off the UPS. + +AUTHORS +------- +Andrew Anderson + +SEE ALSO +-------- + +The core driver: +~~~~~~~~~~~~~~~~ +linkman:nutupsdrv[8] + +Internet resources: +~~~~~~~~~~~~~~~~~~~ +Initial pull requests adding this driver: + +* https://github.com/networkupstools/nut/pull/730 +* https://github.com/PiSupply/PiJuice/issues/124 + +Product home page: https://uk.pi-supply.com/products/pijuice-standard + +The NUT (Network UPS Tools) home page: http://www.networkupstools.org/ diff --git a/docs/nut.dict b/docs/nut.dict index e7795b49cf..921ab16cbc 100644 --- a/docs/nut.dict +++ b/docs/nut.dict @@ -751,6 +751,8 @@ PhaseWin PhoenixContact PhoenixTec Phoenixtec +Pi +PiJuice Plesser PnP Pohle @@ -1970,6 +1972,7 @@ phoenixcontact picocom pid pidpath +pijuice pinout pinouts pkg diff --git a/drivers/Makefile.am b/drivers/Makefile.am index 1251ff2850..0f21c71dd7 100644 --- a/drivers/Makefile.am +++ b/drivers/Makefile.am @@ -45,7 +45,7 @@ SERIAL_USB_DRIVERLIST = \ NEONXML_DRIVERLIST = netxml-ups MACOSX_DRIVERLIST = macosx-ups MODBUS_DRIVERLIST = phoenixcontact_modbus -LINUX_I2C_DRIVERLIST = asem +LINUX_I2C_DRIVERLIST = asem pijuice # distribute all drivers, even ones that are not built by default EXTRA_PROGRAMS = $(SERIAL_DRIVERLIST) $(SNMP_DRIVERLIST) $(USB_DRIVERLIST) $(SERIAL_USB_DRIVERLIST) $(NEONXML_DRIVERLIST) $(MACOSX_DRIVERLIST) @@ -128,6 +128,7 @@ mge_utalk_SOURCES = mge-utalk.c microdowell_SOURCES = microdowell.c oneac_SOURCES = oneac.c optiups_SOURCES = optiups.c +pijuice_SOURCES = pijuice.c powercom_SOURCES = powercom.c powercom_LDADD = $(LDADD) -lm powerpanel_SOURCES = powerpanel.c powerp-bin.c powerp-txt.c @@ -241,9 +242,11 @@ macosx_ups_SOURCES = macosx-ups.c phoenixcontact_modbus_SOURCES = phoenixcontact_modbus.c phoenixcontact_modbus_LDADD = $(LDADD_DRIVERS) $(LIBMODBUS_LIBS) -# Asem +# Linux I2C drivers asem_LDADD = $(LDADD_DRIVERS) asem_SOURCES = asem.c +pijuice_LDADD = $(LDADD_DRIVERS) +pijuice_SOURCES = pijuice.c # nutdrv_qx USB/Serial nutdrv_qx_SOURCES = nutdrv_qx.c diff --git a/drivers/asem.c b/drivers/asem.c index d5d6ea511a..fd0d39225c 100644 --- a/drivers/asem.c +++ b/drivers/asem.c @@ -30,13 +30,14 @@ so you need to boot with acpi_enforce_resources=lax option. */ -/* Depends on i2c-dev.h, Linux only */ +#include "main.h" + #include #include #include +/* Depends on i2c-dev.h, Linux only */ #include - -#include "main.h" +#include #ifndef __STR__ # define __STR__(x) #x diff --git a/drivers/pijuice.c b/drivers/pijuice.c new file mode 100644 index 0000000000..2211bff82f --- /dev/null +++ b/drivers/pijuice.c @@ -0,0 +1,836 @@ +/* pijuice.c Driver for the PiJuice HAT (www.pijuice.com), addressed via i2c. + + Copyright (C) 2019 Andrew Anderson + + 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 +#include + +/* + * 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 +#endif +#if HAVE_LINUX_I2C_DEV_H +# include /* for I2C_SLAVE */ +# if !HAVE_LINUX_SMBUS_H +# ifndef I2C_FUNC_I2C +# include +# endif +# endif +#endif + +/* + * 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 + +#define DRIVER_NAME "PiJuice UPS driver" +#define DRIVER_VERSION "0.9" + +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 ", + DRV_EXPERIMENTAL, + { NULL } +}; + +#define I2C_READ_BYTE(fd, cmd, label) \ + if ((data = i2c_smbus_read_byte_data(upsfd, cmd)) < 0 ) { \ + upsdebugx(2, "Failure reading the i2c bus [%s]", label); \ + return; \ + } + +#define I2C_WRITE_BYTE(fd, cmd, value, label) \ + if ((data = 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) \ + if ((data = i2c_smbus_read_word_data(upsfd, cmd)) < 0 ) { \ + upsdebugx(2, "Failure reading the i2c bus [%s]", label); \ + return; \ + } + +#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() +{ + uint8_t cmd = CHARGE_LEVEL_HI_RES_CMD; + uint16_t data; + + upsdebugx( 3, __FUNCTION__ ); + + I2C_READ_WORD( upsfd, cmd, __FUNCTION__ ) + + /* + * 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() +{ + uint8_t cmd = STATUS_CMD; + uint8_t data; + char status_buf[ST_MAX_VALUE_LEN]; + + upsdebugx( 3, __FUNCTION__ ); + + memset( status_buf, 0, ST_MAX_VALUE_LEN ); + + I2C_READ_BYTE( upsfd, cmd, __FUNCTION__ ) + + uint8_t 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" ); + } + + uint8_t 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" ); + } + + uint8_t 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" ); + snprintfcat( status_buf, ST_MAX_VALUE_LEN, "LB " ); + } + else if ( battery_charge_level > HIGH_BATTERY_THRESHOLD ) + { + upsdebugx( 1, "Battery Charge Status: HIGH" ); + snprintfcat( status_buf, ST_MAX_VALUE_LEN, "HB " ); + } + } + else if ( batteryStatus == BATT_NOT_PRESENT ) + { + snprintfcat( status_buf, ST_MAX_VALUE_LEN, "RB " ); + } + + if ( batteryStatus <= BATT_NOT_PRESENT && + powerInput <= POWER_PRESENT && + powerInput5vIo <= POWER_PRESENT ) + { + if ( powerInput == POWER_NOT_PRESENT && + ( powerInput5vIo != POWER_NOT_PRESENT && + powerInput5vIo <= POWER_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 ); + + snprintfcat( status_buf, sizeof(status_buf), "OL" ); + if ( batteryStatus == BATT_CHARGING_FROM_5V ) + { + snprintfcat( status_buf, sizeof( status_buf ), " 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" ); + } + status_set( status_buf ); + } + 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 ); + + snprintfcat( status_buf, sizeof(status_buf), "OL" ); + if ( batteryStatus == BATT_CHARGING_FROM_IN ) + { + snprintfcat( status_buf, sizeof(status_buf), " CHRG" ); + status_set( status_buf ); + upsdebugx( 1, "Battery Charger Status: charging" ); + dstate_setinfo( "battery.charger.status", "%s", "charging" ); + } + else if ( batteryStatus == BATT_NORMAL ) + { + status_set( status_buf ); + 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 ); + + snprintfcat( status_buf, sizeof( status_buf ), "OL" ); + if ( batteryStatus == BATT_CHARGING_FROM_IN ) + { + snprintfcat( status_buf, sizeof(status_buf), " CHRG"); + status_set( status_buf ); + upsdebugx( 1, "Battery Charger Status: charging" ); + dstate_setinfo("battery.charger.status", "%s", "charging"); + } + else if ( batteryStatus == BATT_NORMAL ) + { + status_set( status_buf ); + 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 ); + + snprintfcat( status_buf, sizeof(status_buf), "OB DISCHRG" ); + status_set( status_buf ); + } + } +} + +static void get_battery_temperature() +{ + uint8_t cmd = BATTERY_TEMPERATURE_CMD; + int16_t data; + + upsdebugx( 3, __FUNCTION__ ); + + I2C_READ_WORD( upsfd, cmd, __FUNCTION__ ) + + upsdebugx( 1, "Battery Temperature: %d°C", data ); + dstate_setinfo( "battery.temperature", "%d", data ); +} + +static void get_battery_voltage() +{ + uint8_t cmd = BATTERY_VOLTAGE_CMD; + int16_t data; + + upsdebugx( 3, __FUNCTION__ ); + + I2C_READ_WORD( upsfd, cmd, __FUNCTION__ ) + + upsdebugx( 1, "Battery Voltage: %0.3fV", data / 1000.0 ); + dstate_setinfo( "battery.voltage", "%0.3f", data / 1000.0 ); +} + +static void get_battery_current() +{ + uint8_t cmd = BATTERY_CURRENT_CMD; + int16_t data; + + upsdebugx( 3, __FUNCTION__ ); + + /* + * 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() +{ + uint8_t cmd = IO_VOLTAGE_CMD; + int16_t data; + + upsdebugx( 3, __FUNCTION__ ); + + I2C_READ_WORD( upsfd, cmd, __FUNCTION__ ) + + upsdebugx( 1, "Input Voltage: %.3fV", data / 1000.0 ); + dstate_setinfo( "input.voltage", "%.3f", data / 1000.0 ); +} + +static void get_io_current() +{ + uint8_t cmd = IO_CURRENT_CMD; + int16_t data; + + upsdebugx( 3, __FUNCTION__ ); + + /* + * 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() +{ + uint8_t cmd = FIRMWARE_VERSION_CMD; + uint16_t data; + uint8_t major, minor; + + upsdebugx( 3, __FUNCTION__ ); + + I2C_READ_WORD( upsfd, cmd, __FUNCTION__ ) + + 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() +{ + uint8_t cmd = BATTERY_PROFILE_CMD; + __u8 block[I2C_SMBUS_BLOCK_MAX]; + + upsdebugx( 3, __FUNCTION__ ); + + I2C_READ_BLOCK( upsfd, cmd, 14, block, __FUNCTION__ ) + + 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() +{ + uint8_t cmd = BATTERY_EXT_PROFILE_CMD; + __u8 block[I2C_SMBUS_BLOCK_MAX]; + + upsdebugx( 3, __FUNCTION__ ); + + I2C_READ_BLOCK( upsfd, cmd, 17, block, __FUNCTION__ ) + + 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() +{ + uint8_t cmd = POWER_OFF_CMD; + uint8_t data; + + upsdebugx( 3, __FUNCTION__ ); + + I2C_READ_BYTE( upsfd, cmd, __FUNCTION__ ) + + if ( data == 255 ) + { + upsdebugx( 1, "Power Off: DISABLED" ); + } + else if ( data <= 250 ) + { + upsdebugx( 1, "Power Off: %d Seconds", data ); + } +} + +static void set_power_off() +{ + uint8_t cmd = POWER_OFF_CMD; + uint8_t data; + + upsdebugx( 3, __FUNCTION__ ); + + /* + * Acceptable values for shutdown_delay are 1-250, + * use 0/255 to clear a scheduled power off command + */ + + if ( shutdown_delay > 255 ) + { + 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, __FUNCTION__ ) +} + +static void get_time() +{ + 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, __FUNCTION__ ); + + I2C_READ_BLOCK( upsfd, cmd, 9, block, __FUNCTION__ ) + + 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() +{ + uint8_t cmd = I2C_ADDRESS_CMD; + uint8_t data; + + upsdebugx( 3, __FUNCTION__ ); + + I2C_READ_BYTE( upsfd, cmd, __FUNCTION__ ) + + 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) +{ + + 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(); +} + +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(); +} + +void upsdrv_shutdown(void) +{ + set_power_off(); +} + +void upsdrv_help(void) +{ + printf("\nThe default I2C address is 20 [0x14]\n"); + printf("\n"); +} + +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 ); + + /* probe ups type */ + get_firmware_version(); + + /* get variables and flags from the command line */ + + if (getval("i2c_address")) + i2c_address = atoi(getval("i2c_address")); +} + +void upsdrv_cleanup(void) +{ + close(upsfd); +}