# Module `lib.tools.battery`

Manage the battery

Expand source code

    # Distributed under Pycameresp License
    # Copyright (c) 2023 Remi BERTHOLET
    # pylint:disable=consider-using-f-string
    """ Manage the battery """
    import uasyncio
    import machine
    import tools.jsonconfig
    import tools.logger
    import tools.tasking
    import tools.support

    if tools.support.battery():
            try:
                    BROWNOUT_RESET = machine.BROWNOUT_RESET
            except:
                    BROWNOUT_RESET = 6

            MAX_BROWNOUT_RESET = 32

            class BatteryConfig(tools.jsonconfig.JsonConfig):
                    """ Battery configuration """
                    def __init__(self):
                            """ Constructor """
                            tools.jsonconfig.JsonConfig.__init__(self)

                            # Battery monitoring
                            self.activated = False # Monitoring status
                            self.level_gpio    = 12  # Monitoring GPIO
                            self.full_battery  = 188 # 4.2V mesured with resistor 100k + 47k
                            self.empty_battery = 158 # 3.6V mesured with resistor 100k + 47k

                            # Force deep sleep if to many successive brown out reset detected
                            self.brownout_detection = True
                            self.brownout_count = 0

            class Battery:
                    """ Manage the battery information """
                    config = None
                    level = [-2]

                    @staticmethod
                    def init():
                            """ Init battery class """
                            # If config not yet read
                            if Battery.config is None:
                                    Battery.config = BatteryConfig()
                                    # If config failed to read
                                    if Battery.config.load() is False:
                                            # Write default config
                                            Battery.config.save()

                    @staticmethod
                    def get_level():
                            """ Return the battery level between 0% to 100% (0%=3.6V 100%=4.2V).
                                    For the ESP32CAM with Gpio12, the value can be read only before the open of camera and SD card.
                                    The voltage always smaller than 1.5V otherwise the card does not boot (JTAG detection I think).
                                    This GPIO 12 of the ESP32CAM not have a pull up resistor, it is the only one which allows the ADC measurement.
                                    I had to patch the micropython firmware to be able to read the GPIO 12."""
                            Battery.init()
                            # If battery level not yet read at start
                            if Battery.level[0] == -2:
                                    level = -1
                                    try:
                                            adc = machine.ADC(machine.Pin(Battery.config.level_gpio))
                                            adc.atten(machine.ADC.ATTN_11DB)
                                            adc.width(machine.ADC.WIDTH_9BIT)
                                            count = 3
                                            val = 0
                                            for i in range(count):
                                                    val += adc.read()
                                            # If battery level pin not connected
                                            if val < (Battery.config.empty_battery * count) // 2:
                                                    level = -1
                                            else:
                                                    # Compute battery level
                                                    level = Battery.calc_percent(val/count, Battery.config)
                                                    if level < 0.:
                                                            level = 0
                                                    elif level > 100.:
                                                            level = 100
                                                    else:
                                                            level = int(level)
                                            tools.logger.syslog("Battery level %d %% (%d)"%(level, int(val/count)))
                                    except Exception as err:
                                            tools.logger.syslog(err,"Cannot read battery status")
                                    Battery.level[0] = level
                            return Battery.level[0]

                    @staticmethod
                    def is_activated():
                            """ Indicates if the battery management activated """
                            Battery.init()
                            return Battery.config.activated

                    @staticmethod
                    def calc_percent(x, config):
                            """ Calc the percentage of battery according to the configuration """
                            x1 = config.full_battery
                            y1 = 100
                            x2 = config.empty_battery
                            y2 = 0

                            a = (y1 - y2)/(x1 - x2)
                            b = y1 - (a * x1)
                            y = a*x + b
                            return y

                    @staticmethod
                    def protect():
                            """ Protect the battery """
                            Battery.init()
                            Battery.keep_reset_cause()
                            if Battery.manage_level() or Battery.is_too_many_brownout():
                                    # Too many brownout reset
                                    # Slow deepsleep during 1 hour
                                    if Battery.config.brownout_count < MAX_BROWNOUT_RESET + 60:
                                            tools.logger.syslog("Sleep 1 minute")
                                            machine.deepsleep(600*1000)
                                    # Slow deepsleep during one day
                                    elif Battery.config.brownout_count < MAX_BROWNOUT_RESET + 60 + 24:
                                            tools.logger.syslog("Sleep 1 hour")
                                            machine.deepsleep(3600*1000)
                                    # Slow deepsleep during three days
                                    elif Battery.config.brownout_count < MAX_BROWNOUT_RESET + 60 + 24 + 8:
                                            tools.logger.syslog("Sleep 3 hours")
                                            machine.deepsleep(3*3600*1000)
                                    # Slow deepsleep during one week
                                    elif Battery.config.brownout_count < MAX_BROWNOUT_RESET + 60 + 24 + 8 + 7:
                                            tools.logger.syslog("Sleep 24 hours")
                                            machine.deepsleep(24*3600*1000)
                                    # Deepsleep infinite
                                    else:
                                            tools.logger.syslog("Sleep infinite")
                                            machine.deepsleep()

                    @staticmethod
                    def manage_level():
                            """ Checks if the battery level is sufficient.
                                    If the battery is too low, we enter indefinite deep sleep to protect the battery """
                            deepsleep = False
                            Battery.init()
                            if Battery.config.activated:
                                    # Can only be done once at boot before start the camera and sd card
                                    battery_level = Battery.get_level()

                                    # If the battery is too low
                                    if battery_level > 5 or battery_level < 0:
                                            battery_protect = False
                                    else:
                                            battery_protect = True

                                    # Case the battery has not enough current and must be protected
                                    if battery_protect:
                                            deepsleep = True
                                            tools.logger.syslog("Battery too low %d %%"%battery_level)
                            return deepsleep

                    @staticmethod
                    def keep_reset_cause():
                            """ Keep reset cause """
                            causes = {
                                    machine.PWRON_RESET     : "Power on",
                                    machine.HARD_RESET      : "Hard",
                                    machine.WDT_RESET       : "Watch dog",
                                    machine.DEEPSLEEP_RESET : "Deep sleep",
                                    machine.SOFT_RESET      : "Soft",
                                    BROWNOUT_RESET          : "Brownout",
                            }.setdefault(machine.reset_cause(), "%d"%machine.reset_cause())
                            tools.logger.syslog("%s Start %s"%('-'*10,'-'*10), display=False)
                            tools.logger.syslog("%s reset"%causes)

                    @staticmethod
                    def is_too_many_brownout():
                            """ Checks the number of brownout reset """
                            deepsleep = False

                            Battery.config.refresh()

                            if Battery.config.brownout_detection:
                                    # If the reset can probably due to insufficient battery
                                    if machine.reset_cause() == BROWNOUT_RESET:
                                            Battery.config.brownout_count += 1
                                    else:
                                            Battery.config.brownout_count = 0

                                    Battery.config.save()

                                    # if the number of consecutive brownout resets is too high
                                    if Battery.config.brownout_count > MAX_BROWNOUT_RESET:
                                            # Battery too low, save the battery status
                                            tools.logger.syslog("Too many successive brownout reset %d"%Battery.config.brownout_count)
                                            deepsleep = True
                            return deepsleep

                    @staticmethod
                    def reset_brownout():
                            """ Reset brownout counter if wifi connected """
                            if Battery.config is not None:
                                    if Battery.config.brownout_count > 0:
                                            Battery.config.brownout_count = 0
                                            Battery.config.save()

                    @staticmethod
                    async def task():
                            """ Internal periodic task """
                            if Battery.config is not None:
                                    Battery.config.refresh()
                            else:
                                    Battery.protect()
                            await uasyncio.sleep(11)
                            return True

                    @staticmethod
                    def start(**kwargs):
                            """ Start battery monitoring task """
                            if tools.support.battery():
                                    Battery.protect()
                                    tools.tasking.Tasks.create_monitor(Battery.task)
                            else:
                                    tools.logger.syslog("Battery management not supported on this hardware")

## Classes

`class Battery`

  Manage the battery information

  Expand source code

      class Battery:
              """ Manage the battery information """
              config = None
              level = [-2]

              @staticmethod
              def init():
                      """ Init battery class """
                      # If config not yet read
                      if Battery.config is None:
                              Battery.config = BatteryConfig()
                              # If config failed to read
                              if Battery.config.load() is False:
                                      # Write default config
                                      Battery.config.save()

              @staticmethod
              def get_level():
                      """ Return the battery level between 0% to 100% (0%=3.6V 100%=4.2V).
                              For the ESP32CAM with Gpio12, the value can be read only before the open of camera and SD card.
                              The voltage always smaller than 1.5V otherwise the card does not boot (JTAG detection I think).
                              This GPIO 12 of the ESP32CAM not have a pull up resistor, it is the only one which allows the ADC measurement.
                              I had to patch the micropython firmware to be able to read the GPIO 12."""
                      Battery.init()
                      # If battery level not yet read at start
                      if Battery.level[0] == -2:
                              level = -1
                              try:
                                      adc = machine.ADC(machine.Pin(Battery.config.level_gpio))
                                      adc.atten(machine.ADC.ATTN_11DB)
                                      adc.width(machine.ADC.WIDTH_9BIT)
                                      count = 3
                                      val = 0
                                      for i in range(count):
                                              val += adc.read()
                                      # If battery level pin not connected
                                      if val < (Battery.config.empty_battery * count) // 2:
                                              level = -1
                                      else:
                                              # Compute battery level
                                              level = Battery.calc_percent(val/count, Battery.config)
                                              if level < 0.:
                                                      level = 0
                                              elif level > 100.:
                                                      level = 100
                                              else:
                                                      level = int(level)
                                      tools.logger.syslog("Battery level %d %% (%d)"%(level, int(val/count)))
                              except Exception as err:
                                      tools.logger.syslog(err,"Cannot read battery status")
                              Battery.level[0] = level
                      return Battery.level[0]

              @staticmethod
              def is_activated():
                      """ Indicates if the battery management activated """
                      Battery.init()
                      return Battery.config.activated

              @staticmethod
              def calc_percent(x, config):
                      """ Calc the percentage of battery according to the configuration """
                      x1 = config.full_battery
                      y1 = 100
                      x2 = config.empty_battery
                      y2 = 0

                      a = (y1 - y2)/(x1 - x2)
                      b = y1 - (a * x1)
                      y = a*x + b
                      return y

              @staticmethod
              def protect():
                      """ Protect the battery """
                      Battery.init()
                      Battery.keep_reset_cause()
                      if Battery.manage_level() or Battery.is_too_many_brownout():
                              # Too many brownout reset
                              # Slow deepsleep during 1 hour
                              if Battery.config.brownout_count < MAX_BROWNOUT_RESET + 60:
                                      tools.logger.syslog("Sleep 1 minute")
                                      machine.deepsleep(600*1000)
                              # Slow deepsleep during one day
                              elif Battery.config.brownout_count < MAX_BROWNOUT_RESET + 60 + 24:
                                      tools.logger.syslog("Sleep 1 hour")
                                      machine.deepsleep(3600*1000)
                              # Slow deepsleep during three days
                              elif Battery.config.brownout_count < MAX_BROWNOUT_RESET + 60 + 24 + 8:
                                      tools.logger.syslog("Sleep 3 hours")
                                      machine.deepsleep(3*3600*1000)
                              # Slow deepsleep during one week
                              elif Battery.config.brownout_count < MAX_BROWNOUT_RESET + 60 + 24 + 8 + 7:
                                      tools.logger.syslog("Sleep 24 hours")
                                      machine.deepsleep(24*3600*1000)
                              # Deepsleep infinite
                              else:
                                      tools.logger.syslog("Sleep infinite")
                                      machine.deepsleep()

              @staticmethod
              def manage_level():
                      """ Checks if the battery level is sufficient.
                              If the battery is too low, we enter indefinite deep sleep to protect the battery """
                      deepsleep = False
                      Battery.init()
                      if Battery.config.activated:
                              # Can only be done once at boot before start the camera and sd card
                              battery_level = Battery.get_level()

                              # If the battery is too low
                              if battery_level > 5 or battery_level < 0:
                                      battery_protect = False
                              else:
                                      battery_protect = True

                              # Case the battery has not enough current and must be protected
                              if battery_protect:
                                      deepsleep = True
                                      tools.logger.syslog("Battery too low %d %%"%battery_level)
                      return deepsleep

              @staticmethod
              def keep_reset_cause():
                      """ Keep reset cause """
                      causes = {
                              machine.PWRON_RESET     : "Power on",
                              machine.HARD_RESET      : "Hard",
                              machine.WDT_RESET       : "Watch dog",
                              machine.DEEPSLEEP_RESET : "Deep sleep",
                              machine.SOFT_RESET      : "Soft",
                              BROWNOUT_RESET          : "Brownout",
                      }.setdefault(machine.reset_cause(), "%d"%machine.reset_cause())
                      tools.logger.syslog("%s Start %s"%('-'*10,'-'*10), display=False)
                      tools.logger.syslog("%s reset"%causes)

              @staticmethod
              def is_too_many_brownout():
                      """ Checks the number of brownout reset """
                      deepsleep = False

                      Battery.config.refresh()

                      if Battery.config.brownout_detection:
                              # If the reset can probably due to insufficient battery
                              if machine.reset_cause() == BROWNOUT_RESET:
                                      Battery.config.brownout_count += 1
                              else:
                                      Battery.config.brownout_count = 0

                              Battery.config.save()

                              # if the number of consecutive brownout resets is too high
                              if Battery.config.brownout_count > MAX_BROWNOUT_RESET:
                                      # Battery too low, save the battery status
                                      tools.logger.syslog("Too many successive brownout reset %d"%Battery.config.brownout_count)
                                      deepsleep = True
                      return deepsleep

              @staticmethod
              def reset_brownout():
                      """ Reset brownout counter if wifi connected """
                      if Battery.config is not None:
                              if Battery.config.brownout_count > 0:
                                      Battery.config.brownout_count = 0
                                      Battery.config.save()

              @staticmethod
              async def task():
                      """ Internal periodic task """
                      if Battery.config is not None:
                              Battery.config.refresh()
                      else:
                              Battery.protect()
                      await uasyncio.sleep(11)
                      return True

              @staticmethod
              def start(**kwargs):
                      """ Start battery monitoring task """
                      if tools.support.battery():
                              Battery.protect()
                              tools.tasking.Tasks.create_monitor(Battery.task)
                      else:
                              tools.logger.syslog("Battery management not supported on this hardware")

  ### Class variables

  `var config`
  `var level`

  ### Static methods

  `def calc_percent(x, config)`

    Calc the percentage of battery according to the configuration

    Expand source code

        @staticmethod
        def calc_percent(x, config):
                """ Calc the percentage of battery according to the configuration """
                x1 = config.full_battery
                y1 = 100
                x2 = config.empty_battery
                y2 = 0

                a = (y1 - y2)/(x1 - x2)
                b = y1 - (a * x1)
                y = a*x + b
                return y

  `def get_level()`

    Return the battery level between 0% to 100% (0%=3.6V 100%=4.2V). For the ESP32CAM with Gpio12, the value can be read only before the open of camera and SD card. The voltage always smaller than 1.5V otherwise the card does not boot (JTAG detection I think). This GPIO 12 of the ESP32CAM not have a pull up resistor, it is the only one which allows the ADC measurement. I had to patch the micropython firmware to be able to read the GPIO 12.

    Expand source code

        @staticmethod
        def get_level():
                """ Return the battery level between 0% to 100% (0%=3.6V 100%=4.2V).
                        For the ESP32CAM with Gpio12, the value can be read only before the open of camera and SD card.
                        The voltage always smaller than 1.5V otherwise the card does not boot (JTAG detection I think).
                        This GPIO 12 of the ESP32CAM not have a pull up resistor, it is the only one which allows the ADC measurement.
                        I had to patch the micropython firmware to be able to read the GPIO 12."""
                Battery.init()
                # If battery level not yet read at start
                if Battery.level[0] == -2:
                        level = -1
                        try:
                                adc = machine.ADC(machine.Pin(Battery.config.level_gpio))
                                adc.atten(machine.ADC.ATTN_11DB)
                                adc.width(machine.ADC.WIDTH_9BIT)
                                count = 3
                                val = 0
                                for i in range(count):
                                        val += adc.read()
                                # If battery level pin not connected
                                if val < (Battery.config.empty_battery * count) // 2:
                                        level = -1
                                else:
                                        # Compute battery level
                                        level = Battery.calc_percent(val/count, Battery.config)
                                        if level < 0.:
                                                level = 0
                                        elif level > 100.:
                                                level = 100
                                        else:
                                                level = int(level)
                                tools.logger.syslog("Battery level %d %% (%d)"%(level, int(val/count)))
                        except Exception as err:
                                tools.logger.syslog(err,"Cannot read battery status")
                        Battery.level[0] = level
                return Battery.level[0]

  `def init()`

    Init battery class

    Expand source code

        @staticmethod
        def init():
                """ Init battery class """
                # If config not yet read
                if Battery.config is None:
                        Battery.config = BatteryConfig()
                        # If config failed to read
                        if Battery.config.load() is False:
                                # Write default config
                                Battery.config.save()

  `def is_activated()`

    Indicates if the battery management activated

    Expand source code

        @staticmethod
        def is_activated():
                """ Indicates if the battery management activated """
                Battery.init()
                return Battery.config.activated

  `def is_too_many_brownout()`

    Checks the number of brownout reset

    Expand source code

        @staticmethod
        def is_too_many_brownout():
                """ Checks the number of brownout reset """
                deepsleep = False

                Battery.config.refresh()

                if Battery.config.brownout_detection:
                        # If the reset can probably due to insufficient battery
                        if machine.reset_cause() == BROWNOUT_RESET:
                                Battery.config.brownout_count += 1
                        else:
                                Battery.config.brownout_count = 0

                        Battery.config.save()

                        # if the number of consecutive brownout resets is too high
                        if Battery.config.brownout_count > MAX_BROWNOUT_RESET:
                                # Battery too low, save the battery status
                                tools.logger.syslog("Too many successive brownout reset %d"%Battery.config.brownout_count)
                                deepsleep = True
                return deepsleep

  `def keep_reset_cause()`

    Keep reset cause

    Expand source code

        @staticmethod
        def keep_reset_cause():
                """ Keep reset cause """
                causes = {
                        machine.PWRON_RESET     : "Power on",
                        machine.HARD_RESET      : "Hard",
                        machine.WDT_RESET       : "Watch dog",
                        machine.DEEPSLEEP_RESET : "Deep sleep",
                        machine.SOFT_RESET      : "Soft",
                        BROWNOUT_RESET          : "Brownout",
                }.setdefault(machine.reset_cause(), "%d"%machine.reset_cause())
                tools.logger.syslog("%s Start %s"%('-'*10,'-'*10), display=False)
                tools.logger.syslog("%s reset"%causes)

  `def manage_level()`

    Checks if the battery level is sufficient. If the battery is too low, we enter indefinite deep sleep to protect the battery

    Expand source code

        @staticmethod
        def manage_level():
                """ Checks if the battery level is sufficient.
                        If the battery is too low, we enter indefinite deep sleep to protect the battery """
                deepsleep = False
                Battery.init()
                if Battery.config.activated:
                        # Can only be done once at boot before start the camera and sd card
                        battery_level = Battery.get_level()

                        # If the battery is too low
                        if battery_level > 5 or battery_level < 0:
                                battery_protect = False
                        else:
                                battery_protect = True

                        # Case the battery has not enough current and must be protected
                        if battery_protect:
                                deepsleep = True
                                tools.logger.syslog("Battery too low %d %%"%battery_level)
                return deepsleep

  `def protect()`

    Protect the battery

    Expand source code

        @staticmethod
        def protect():
                """ Protect the battery """
                Battery.init()
                Battery.keep_reset_cause()
                if Battery.manage_level() or Battery.is_too_many_brownout():
                        # Too many brownout reset
                        # Slow deepsleep during 1 hour
                        if Battery.config.brownout_count < MAX_BROWNOUT_RESET + 60:
                                tools.logger.syslog("Sleep 1 minute")
                                machine.deepsleep(600*1000)
                        # Slow deepsleep during one day
                        elif Battery.config.brownout_count < MAX_BROWNOUT_RESET + 60 + 24:
                                tools.logger.syslog("Sleep 1 hour")
                                machine.deepsleep(3600*1000)
                        # Slow deepsleep during three days
                        elif Battery.config.brownout_count < MAX_BROWNOUT_RESET + 60 + 24 + 8:
                                tools.logger.syslog("Sleep 3 hours")
                                machine.deepsleep(3*3600*1000)
                        # Slow deepsleep during one week
                        elif Battery.config.brownout_count < MAX_BROWNOUT_RESET + 60 + 24 + 8 + 7:
                                tools.logger.syslog("Sleep 24 hours")
                                machine.deepsleep(24*3600*1000)
                        # Deepsleep infinite
                        else:
                                tools.logger.syslog("Sleep infinite")
                                machine.deepsleep()

  `def reset_brownout()`

    Reset brownout counter if wifi connected

    Expand source code

        @staticmethod
        def reset_brownout():
                """ Reset brownout counter if wifi connected """
                if Battery.config is not None:
                        if Battery.config.brownout_count > 0:
                                Battery.config.brownout_count = 0
                                Battery.config.save()

  `def start(**kwargs)`

    Start battery monitoring task

    Expand source code

        @staticmethod
        def start(**kwargs):
                """ Start battery monitoring task """
                if tools.support.battery():
                        Battery.protect()
                        tools.tasking.Tasks.create_monitor(Battery.task)
                else:
                        tools.logger.syslog("Battery management not supported on this hardware")

  `async def task()`

    Internal periodic task

    Expand source code

        @staticmethod
        async def task():
                """ Internal periodic task """
                if Battery.config is not None:
                        Battery.config.refresh()
                else:
                        Battery.protect()
                await uasyncio.sleep(11)
                return True

`class BatteryConfig`

  Battery configuration

  Constructor

  Expand source code

      class BatteryConfig(tools.jsonconfig.JsonConfig):
              """ Battery configuration """
              def __init__(self):
                      """ Constructor """
                      tools.jsonconfig.JsonConfig.__init__(self)

                      # Battery monitoring
                      self.activated = False # Monitoring status
                      self.level_gpio    = 12  # Monitoring GPIO
                      self.full_battery  = 188 # 4.2V mesured with resistor 100k + 47k
                      self.empty_battery = 158 # 3.6V mesured with resistor 100k + 47k

                      # Force deep sleep if to many successive brown out reset detected
                      self.brownout_detection = True
                      self.brownout_count = 0

  ### Ancestors

  - tools.jsonconfig.JsonConfig