import smbus
from datetime import date, timedelta
from struct import pack, unpack

class Error(Exception):
  """Base class for SBS exceptions"""
  pass
class InvalidValueError(Error):
  """Raised when the value of an SBS property is not valid for the current
  battery state.
  """
  pass

class Device(smbus.Device):
  #
  # Lifecycle
  #
  """Helper class for interaction with SBS devices"""
  def __init__(self, bus=None, addr=0x0B):
    """Initializes a new instance of the SBS class.
    
    Keyword Arguments:
    bus  -- SMBus driver to use.  Creates a new instance by default.
    addr -- Slave address for the SBS device (default 0x0B)
    """
    if bus is None:
      bus = smbus.Bus()
    smbus.Device.__init__(self, bus, addr)
    self._reinit()
  def _reinit(self):
    """(Protected) Reinitializes cached register values in case of register or
    firmware changes.
    """
    specinfo = self.SpecificationInfo()
    self._vscale = specinfo['vscale']
    self._ipscale = specinfo['ipscale']
    self.BatteryMode()
  #
  # Protected Helper Methods
  #
  def _voltage(self, word):
    """(Protected) Converts and scales a voltage retrieved from a register
    access.
    """
    return word / 1000.0 * self._vscale
  def _current(self, word):
    """(Protected) Converts and scales a current retrieved from a register
    access.
    """
    return word / 1000.0 * self._ipscale
  def _capacity(self, word):
    """(Protected) Converts and scales a capacity retrieved from a register
    access.
    """
    if self._capm:
      # CAPM set: data reports in 10 mWh
      return (word / 1E2 * self._ipscale, 'Wh')
    else:
      # CAPM clear: data reports in 1 mAh
      return (word / 1E3 * self._ipscale, 'Ah')
  def _temperature(self, word):
    """(Protected) Converts and scales a temperature retrieved from a register
    access.
    """
    return word / 10.0
  #
  # Core SBS Commands
  #
  def BatteryMode(self, **kwargs):
    """Gets or sets the battery mode (register 0x03).  The mode word is
    expanded into named values in a dict.

    Keyword Arguments (Settable):
    CAPM -- Capacity Mode (True for W*h, False for A*h)
    CHGM -- Charger Mode (True to disable charger broadcasts)
    AM   -- Alarm Bit (True to disable alarm broadcasts)
    PB   -- Primary Battery (True if the battery is in a secondary role)
    CC   -- Charge Controller Enabled

    Keyword Arguments (Read-only):
    CF   -- True when a battery conditioning cycle is active
    PBS  -- Primary Battery Support (True if supported)
    ICC  -- Internal Charge Controller (True if supported)
    """
    mode = self.read_word(0x03)
    # Set/Clear bits as requested
    if len(kwargs) > 0:
      if 'CAPM' in kwargs:
        if kwargs['CAPM']:
          mode = mode |  0x8000
        else:
          mode = mode & ~0x8000
      if 'CHGM' in kwargs:
        if kwargs['CHGM']:
          mode = mode |  0x4000
        else:
          mode = mode & ~0x4000
      if 'AM' in kwargs:
        if kwargs['AM']:
          mode = mode |  0x2000
        else:
          mode = mode & ~0x2000
      if 'PB' in kwargs:
        if kwargs['PB']:
          mode = mode |  0x0200
        else:
          mode = mode & ~0x0200
      if 'CC' in kwargs:
        if kwargs['CC']:
          mode = mode |  0x0100
        else:
          mode = mode & ~0x0100
      self.write_word(0x03, mode)
    # Use this opportunity to keep CAPM up to date
    self._capm = bool(mode & 0x8000)
    return {
        'CAPM': bool(mode & 0x8000),
        'CHGM': bool(mode & 0x4000),
        'AM':   bool(mode & 0x2000),
        'PB':   bool(mode & 0x0200),
        'CC':   bool(mode & 0x0100),
        'CF':   bool(mode & 0x0080),
        'PBS':  bool(mode & 0x0002),
        'ICC':  bool(mode & 0x0001)
    }
  def Temperature(self):
    """Measures the battery temperature (Kelvin)."""
    return self._temperature(self.read_word(0x08))
  def Voltage(self):
    """Measures the aggregate battery output voltage (Volts)."""
    return self._voltage(self.read_word(0x09))
  def Current(self):
    """Measures the battery output current (Amps).  Negative values indicate
    discharge while positive values indicate charge."""
    return self._current(self.read_word_signed(0x0A))
  def AverageCurrent(self):
    """Measures the battery output current (Amps) as averaged over the
    previous minute.  Negative values indicate discharge while positive values
    indicate charge."""
    return self._current(self.read_word_signed(0x0B))
  def MaxError(self):
    """Returns the expected margin of error in the State of Charge estimates
    (Percent)."""
    return self.read_byte(0x0C)
  def RelativeStateOfCharge(self):
    """Estimates the remaining battery capacity as a percentage of
    FullChargeCapacity()."""
    return self.read_byte(0x0D)
  def AbsoluteStateOfCharge(self):
    """Estimates the remaining battery capacity as a percentage of
    DesignCapacity()."""
    return self.read_byte(0x0E)
  def RemainingCapacity(self):
    """Estimates the remaining battery capacity (A*h or W*h)."""
    return self._capacity(self.read_word(0x0F))
  def FullChargeCapacity(self):
    """Estimates the capacity of the battery when fully charged (A*h or W*h)."""
    return self._capacity(self.read_word(0x10))
  def RunTimeToEmpty(self):
    """Estimates the remaining time until the battery is depleted
    (datetime.timedelta).
    
    Raises InvalidValueError if the battery is not currently discharging"""
    minutes = self.read_word(0x11)
    if minutes == 0xFFFF:
      raise InvalidValueError, 'Battery is not currently discharging'
    else:
      return timedelta(minutes=minutes)
  def AverageTimeToEmpty(self):
    """Estimates the remaining time until the battery is depleted based on the
    one-minute average current consumption (datetime.timedelta).
    
    Raises InvalidValueError if the battery is not currently discharging"""
    minutes = self.read_word(0x12)
    if minutes == 0xFFFF:
      raise InvalidValueError, 'Battery is not currently discharging'
    else:
      return timedelta(minutes=minutes)
  def AverageTimeToFull(self):
    """Estimates the remaining time until the battery is fully charged based
    on the one-minute average current consumption (datetime.timedelta).
    
    Raises InvalidValueError if the battery is not currently charging"""
    minutes = self.read_word(0x13)
    if minutes == 0xFFFF:
      raise InvalidValueError, 'Battery is not currently charging'
    else:
      return timedelta(minutes=minutes)
  def ChargingCurrent(self):
    """Returns the design charging current for this battery (Amps)."""
    curr = self.read_word(0x14)
    if curr == 0xFFFF:
      raise Exception, 'Not implemented'
    else:
      return curr / 1000.0
  def ChargingVoltage(self):
    """Returns the design charging voltage for this battery (Volts)."""
    volt = self.read_word(0x15)
    if volt == 0xFFFF:
      raise Exception, 'Not implemented'
    else:
      return volt / 1000.0
  def BatteryStatus(self):
    """Gets the battery status word (register 0x16).  The status word is
    expanded into named values in a dict.

    Keyword Values:
    OCA  -- Overcharged Alarm (True if triggered)
    TCA  -- Terminate Charge Alarm (True if triggered)
    OTA  -- Overtemperature Alarm (True if triggered)
    TDA  -- Terminate Discharge Alarm (True if triggered)
    RCA  -- Remaining Capacity Alarm (True if triggered)
    RTA  -- Remaining Time Alarm (True if triggered)
    INIT -- True if the battery has been initialized
    DSG  -- Charge FET Test (False if the battery is charging)
    FC   -- True if the battery is fully charged
    FD   -- True if the battery is fully discharged