/*
Damon Hart-Davis licenses this file to you
under the Apache Licence, Version 2.0 (the "Licence");
you may not use this file except in compliance
with the Licence. You may obtain a copy of the Licence at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing,
software distributed under the Licence is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the Licence for the
specific language governing permissions and limitations
under the Licence.

Author(s) / Copyright (s): Damon Hart-Davis 2014--2023
*/

static const char *Id = "$Id: powermng.cpp 67070 2026-04-08 09:03:21Z dhd $";

/*
This is to be run periodically (every few minutes, typically 10m)
to monitor battery levels (and other factors)
and adjust flags and other system parameters
to modulate consumption to match.

This assumes a controllable 'dump' load of ~1A/13W.
The load actually is taken off-grid when being 'dumped' to!

Still a bit hard-wired here: replaces a shell script that did the same.

Equivalent to /local/k8055/check-status.sh on SheevaPlug as of 2014-06
but designed to be much lighter on CPU, and has some locking for safety.
This also gives decent 'verbose' logging of actions and reasons.

As of 2018-06 unit tests are being created for key calculations.

As of 2020-06 being prepared for RPi3B with Expander Pi HAT,
cf RPi1/2 with stack of 3 GPIO boards (RTC, ADC, custom).
*/

/*
See also:
http://hertaville.com/2013/04/01/interfacing-an-i2c-gpio-expander-mcp23017-to-the-raspberry-pi-using-c/
https://code.google.com/p/mas-test-svn/source/browse/trunk/tests/i2c/i2c-base-test/i2c-test.c?r=8
http://unicorn.drogon.net/q2w.c
*/

#include "powermng.h"
#include "mbaccess.h"
#include "i2caccess.h"
#include "ExpanderPi.h"

#if defined(linux) || defined(__linux)
#include <linux/i2c-dev.h>
#include <linux/i2c.h>
#endif

#include <sys/ioctl.h>
#include <sys/param.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/types.h>

#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>


//#ifndef __cplusplus
//typedef enum { false, true } bool;
//#endif

/*
If ISEXPPI is defined then presence of Expander Pi is known at compile time.

If 1 then GPIO/ADC I/O should be done only via Expander Pi.
If 0 then GPIO/ADC I/O should be done only via pre-Expander Pi methods..
If undefined, then multiple methods may need to be tried.
*/

#if defined(ISEXPPI)
#if ISEXPPI
// Presence of Expander Pi HAT indicates RPi3B or later and stretch or later.
#define RPI3B 1
#endif
#endif

// BASIC OPERATIONAL SWITCHES/FEATURES.
// If true then B2 voltage is read and used.
static constexpr bool ENABLE_B2 = false;
// If true then B2 is included in system power status,
// eg very low B2 can prevent B1 dumping.
static constexpr bool B2InSoC = false;
// If true then enable INA219 high-side current/power monitor.
static constexpr bool ENABLE_INA219 = false;
// If true then enable the ambient light sensor.
static constexpr bool ENABLE_AL = false;
// Use Meterbus to gather parameters directly from (SS-MPPT-15L) over MODBUS.
static constexpr bool ENABLE_METERBUS = true;

// Directory for flags to check and set.
static const char FLAG_DIR[] = "/run";
#define FLAG_DIR_LEN 4

// Log directory.
// If absent, log is not done.
static const char *LOG_DIR = "/var/log/powermng";


// Maximum allow base flag name length (ignoring trailing \0).
#define MAX_FLAG_NAME 64

// Input flag to indicate sunny(ish) weather forecast for next day.
static const char *FLAGI_FORECASTGENGOOD = "FORECAST_PV_GEN_GOOD";

// Input flag to indicate no dumping allowed (volatile; disappears at reboot).
static const char *FLAGI_NODUMP = "NODUMP";
// Input flag to indicate GB grid peak (mainly winter weekday evenings).
// When true then make best efforts to move 'dump' load off grid.
static const char *FLAGI_GBGRIDPEAK = "GBGRIDPEAK";
//// Output flag to indicate daylight (bright enough not to require desk lamp).
//static const char *FLAGO_DAYLIGHT = "DAYLIGHT";
// Output flag to indicate one or both batteries full, no hysteresis.
// Typically in float phase.
static const char *FLAGO_BATTFULL = "EXTERNAL_BATTERY_FULL";
// Output flag to indicate one or both batteries not full, no hysteresis.
static const char *FLAGIO_BATTNOTFULL = "EXTERNAL_BATTERY_NOTFULL";
// Output flag to indicate one or both batteries very high, no hysteresis.
// Typically in/beyond absorption charging phase.
static const char *FLAGO_BATTVHIGH = "EXTERNAL_BATTERY_VHIGH";
// Output flag to indicate one or both batteries not very high, no hysteresis.
static const char *FLAGIO_BATTNOTVHIGH = "EXTERNAL_BATTERY_NOTVHIGH";
// Output flag to indicate one or both batteries 'high', no hysteresis.
// If true then likely charging and fairly full.
static const char *FLAGO_BATTHIGH = "EXTERNAL_BATTERY_HIGH";
// Convenience reverse of FLAGO_BATTVHIGH, no hysteresis.
// Exists for benefit of subsystems that use presence of flag as lack of juice.
static const char *FLAGIO_BATTNOTHIGH = "EXTERNAL_BATTERY_NOTHIGH";
// Output flag to indicate one or both batteries low, no hysteresis.
static const char *FLAGO_BATTLOW = "EXTERNAL_BATTERY_LOW";
// Output flag to indicate one or both batteries not low, no hysteresis.
static const char *FLAGO_BATTNOTLOW = "EXTERNAL_BATTERY_NOTLOW";
// Output flag to indicate one or both batteries very low, no hysteresis.
static const char *FLAGO_BATTVLOW = "EXTERNAL_BATTERY_VLOW";
// Output flag to indicate one or both batteries not very low, no hysteresis.
static const char *FLAGIO_BATTNOTVLOW = "EXTERNAL_BATTERY_NOTVLOW";
// Input/output (persistent/hysteresis) flag to indicate known net discharging.
static const char *FLAGIO_DISCHARGING = "DISCHARGING";
// Input/output (persistent/hysteresis) flag to indicate known net discharging.
static const char *FLAGIO_CHARGING = "CHARGING";
// Input/output (persistent/hysteresis) flag to indicate mild energy dump
// from B1 (moving load off-grid).
static const char *FLAGIO_DUMPING = "DUMPING";
// Input/output (persistent/hysteresis) flag to note when B1 dumping stopped.
static const char *FLAGIO_DUMPINGEND = "DUMPINGEND";
// Pseudo-flag containing last data set collected, written unless 'no-log' mode.
// Mainly to be used for the timestamp;
// this may be truncated and re-written while being read.
static const char *FLAGO_LASTDATA = "LASTDATA";

// Externally-provided flags about (GB) grid status.
// If no flags are present then grid is in a 'normal' state.
// If (7d) red flag is present then grid is at its most carbon intensive.
// NOTE sense is opposite of 'green' flags in greenness terms.
//static const char *gridRedFlag = "/rw/docs-public/www.hd.org/Damon/Env/_gridCarbonIntensityGB.7d.red.flag";
// If (7d) flag is present then grid is not in lowest-carbon-intensity region.
//static const char *gridNotGreenFlag = "/rw/docs-public/www.hd.org/Damon/Env/_gridCarbonIntensityGB.7d.flag";
// If (7d) flag is present then grid is not in lowest-carbon-intensity region
// or bulk grid storage is being drawn from (eg pumped hydro).
//static const char *gridNotVeryGreenFlag = "/rw/docs-public/www.hd.org/Damon/Env/_gridCarbonIntensityGB.7d.supergreen.flag";

// External flag for microgeneration / dark / exports / imports.
// If flag is present then it seems to be dark, with no PV generation.
static const char *darkFlag = "/var/log/SunnyBeam/DARK.flag";
// External flag indicating microgen too low to cover base loads.
static const char *lowGenFlag = "/var/log/SunnyBeam/LOW.flag";

// External flag when present indicates exporting/spilling to grid.
// MUST NOT be present if comms to device measuring export is lost.
// Update cadence SHOULD be similar to or faster than this code runs.
//static const char *exportFlag = "/var/log/Enphase/EXPORT.flag";
// DHD20260407: Eddi-derived significant-export flag.
static const char *exportFlag = "/tmp/EXPORTING.flag";
// DHD20260407: Eddi-derived significant-import flag.
static const char *importFlag = "/tmp/IMPORTING.flag";

// Threshold (on 11-bit sample) bright enough not to require desk lamp.
static constexpr int LDR_daylight_thr = 650;

#if 1 // Direct to supply near RPi (not via signal cable between floors)...
// Volts per ulp for Batt1 and Batt2 (~9.382mV/ulp)
// with series resistance of 47k (off-board) + 10k (on-board) and 6.8k drop.
#define offBoardSeriesResistancek 47
#else // +ve sense connections made at service panel.
// Volts per ulp for Batt1 and Batt2 (~9.529mV/ulp)
// with resistance of 1k+47k (off-board) + 10k (on-board) and 6.8k drop.
#define offBoardSeriesResistancek 48
#endif
static constexpr float mVperUlpBatt = (offBoardSeriesResistancek+10.0f+6.8f)/6.8f;


// Controllable dumpload approx expected mA and mW.
static constexpr uint16_t DUMPLOAD_APPROX_mW = 15500;
// Use low-end voltage (12.0V) to get a conservative current estimate.
static constexpr uint16_t DUMPLOAD_APPROX_mA = DUMPLOAD_APPROX_mW / 12;

// Normal minimum number of minutes of LA VH (absorption) before starting dump.
// This is a continuous figure, so quite a tough metric.
// Nominally absorption could take up to 24h; not with PV!
// See: http://www.exide.com/Media/files/Downloads/TransAmer/Battery%20Care%20and%20Maintenance/Battery%20Charging%20&%20Storage%20Guidelines%20%205_9_13.pdf
// Many commercial solar controllers have a 2h limit, though not continuous.
static constexpr int BATT1_MINVH_BEFORE_DUMP_M = LESS_DoD ? 39 : 29;
// Minimum number of minutes of absorption before dump if poor forecast tomorrow.
static constexpr int BATT1_PFMINVH_BEFORE_DUMP_M = BATT1_MINVH_BEFORE_DUMP_M+10;
// Minimum number of minutes at 'high' before dump if not enough absorption.
// Quite long as only a stop-gap if there isn't enough continuous good sun,
// and should allow decent carry-over of load beyond sun-down.
static constexpr int BATT1_MINH_BEFORE_DUMP_M = 119;


#if !defined(RPI3B)
// CPU speed management from RPI3B h/w onwards sophisticated enough as-is.
// Path to file to set CPU-utilisation threshold (in %)
// Above which the ondemand governor raises the CPU frequency.
static const char *UP_THRESHOLD_PATH = "/sys/devices/system/cpu/cpufreq/ondemand/up_threshold";
// Default up-threshold value (%): RPi defaults to 60 or 70.
static constexpr int DEFAULT_UP_THRESHOLD = 60;
// Power-conserving up-threshold value (%).
static constexpr int CONSERVING_UP_THRESHOLD = 95;
//
// Path to file to set sampling rate and thus response latency from idle.
static const char *SAMPLING_RATE_PATH = "/sys/devices/system/cpu/cpufreq/ondemand/sampling_rate";
// Default RPi sampling rate (100ms).
static constexpr int DEFAULT_SAMPLING_RATE = 100000;
// Minimum sampling interval to minimise latency from idle (25ms).
// Minimum allowed 10000 (10ms), may waste energy on excessive wakeups/work.
static constexpr int RESPONSIVE_SAMPLING_RATE = 25000;
//
// Path to file to set sampling down factor.
// A larger value means a longer run-on at max CPU speed before drop.
static const char *SAMPLING_DOWN_FACTOR_PATH = "/sys/devices/system/cpu/cpufreq/ondemand/sampling_down_factor";
// Default RPi sampling down factor: 50 (@100ms sample rate) = ~5s run-on.
static constexpr int DEFAULT_SAMPLING_DOWN_FACTOR = 50;
// Reduced sampling down factor to drop to low speed sooner.
static constexpr int CONSERVING_SAMPLING_DOWN_FACTOR = 2; // 5;
#endif // !defined(RPI3B)


// System mode flags.
static bool verbose = false;
static bool nolog = false; // Do not write to the log.
static bool nosetflags = false; // Do not set run flags, etc.
static bool looping = false;
static bool sleepfirst = false;



#if !defined(ISEXPPI) || !ISEXPPI
// Only needed for RPi1/2 without ExpanderPi.
// Echo/cat the supplied text into the extant file/device at the given path.
// Does not create the file if it does not exist.
// Returns true in case of success.
static bool doEcho(const char * const path, const char * const text)
    {
    assert(NULL != path);
    assert(NULL != text);
    const int tl = strlen(text);
    const int fd = open(path, O_WRONLY); // Don't create.
    if((fd < 0) ||
       (write(fd, text, tl) != tl))
        {
        fprintf(stderr, "Unable to echo to %s.\n", path);
        return(false);
        }
    close(fd);
    return(true);
    }
#endif


#if !defined(RPI3B)
// CPU speed management from RPI3B h/w onwards sophisticated enough as-is.

// Adjust ondemand governor's up_threshold, if possible [0,100].
// Inhibited if 'nosetflags' is true.
static void adj_up_threshold(const uint8_t up_threshold)
    {
    if(nosetflags) { return; }
    if(verbose) { fprintf(stderr, "Setting up_threshold to %u%%.\n", (unsigned)up_threshold); }
    char buf[5]; // Big enough for "100\n\0".
    snprintf(buf, sizeof(buf), "%u\n", (unsigned)up_threshold);
    doEcho(UP_THRESHOLD_PATH, buf);
    }

// Adjust ondemand governor's sampling_rate, if possible; [10000,[.
// Inhibited if 'nosetflags' is true.
static void adj_sampling_rate(const uint32_t sampling_rate)
    {
    if(nosetflags) { return; }
    if(verbose) { fprintf(stderr, "Setting sampling_rate to %u.\n", (unsigned)sampling_rate); }
    char buf[8]; // Big enough for "100000\n\0".
    snprintf(buf, sizeof(buf), "%u\n", (unsigned)sampling_rate);
    doEcho(SAMPLING_RATE_PATH, buf);
    }

// Adjust ondemand governor's sampling_down_factor, if possible; [1--255].
// Inhibited if 'nosetflags' is true.
static void adj_sampling_down_factor(const uint8_t sampling_down_factor)
    {
    if(nosetflags) { return; }
    if(verbose) { fprintf(stderr, "Setting sampling_down_factor to %u.\n", (unsigned)sampling_down_factor); }
    char buf[5]; // Big enough for "255\n\0".
    snprintf(buf, sizeof(buf), "%u\n", (unsigned)sampling_down_factor);
    doEcho(SAMPLING_DOWN_FACTOR_PATH, buf);
    }

#endif // !defined(RPI3B)


// Read the power consumption from 12V (to 5V) in mW.
// Fills in busVoltage with the supply voltage at the shunt top-end.
// Returns -1 if unsuccessful.
static int getPowerDrain(const int fd, int *const busVoltageP)
    {
    if(!ENABLE_INA219) { return(-1); }

    unsigned char buf3[3];

#if 0
    // Reset.
    // Write 1 to RST bit in config register.
    buf3[0] = 0;
    buf3[1] = 0x80;
    buf3[2] = 0;
    if(!write_i2c_bytes(fd, INA219_addr, 3, buf3))
        {
        fprintf(stderr, "Failed reset of INA219 (%d).\n", errno);
        return(-1);
        }
    usleep(1000); // Allow think time..
#endif

    constexpr uint16_t calibration = 8192;
    // Write calibration value.
    buf3[0] = 5;
    buf3[1] = (calibration >> 8);
    buf3[2] = (calibration & 0xff);
    if(!write_i2c_bytes(fd, INA219_addr, 3, buf3))
        {
        fprintf(stderr, "Failed config of INA219.\n");
        return(-1);
        }

    // Configure.
    // D15: RST = 0 (no reset)
    // D14: 0
    // D13: BRNG = 1 (Bus Voltage Range, 32V FSR)
    // D12--D11: PGA = 00 (PGA gain and range, +/- 40mV = 400mA w/ 100mR.
    // D10--D7: BADC = 0011
    // D6--D3: SADC = 0011 (12-bit samples, 532uS)
    // D2--D0: MODE = 011 (Shunt and bus, triggered).
    constexpr uint16_t config = 0b0010000110011011;
    buf3[0] = 0;
    buf3[1] = (config >> 8);
    buf3[2] = (config & 0xff);
    if(!write_i2c_bytes(fd, INA219_addr, 3, buf3))
        {
        fprintf(stderr, "Failed config of INA219.\n");
        return(-1);
        }
    // Allow recovery (40us) and conersion (532us) time.
    usleep(1000);

    // Get bus voltage.
    // Spin (a little) until ready if need be.
    if(!read_i2c_register(fd, INA219_addr, 2, 2, buf3))
        {
        fprintf(stderr, "Failed bus voltage read from INA219.\n");
        return(-1);
        }
    const int bv = (((buf3[0] << 7) & 0x7f00) + ((buf3[1] >> 1) & (0x7e)));
    if(verbose) { fprintf(stderr, "INA219 bus voltage read %2x %2x = %d.\n", (int) buf3[0], (int) buf3[1], bv); }
    if(NULL != busVoltageP) { *busVoltageP = bv; }
    // TODO: check the CNVR (ready) bit 1; loop if 0.
    // TODO: check the OVR (overflow) bit 0; abort if 1.

#if 1
    if(verbose)
        {
        // Get shunt voltage.
        if(!read_i2c_register(fd, INA219_addr, 1, 2, buf3))
            {
            fprintf(stderr, "Failed shunt voltage read from INA219.\n");
            return(-1);
            }
        if(verbose) { fprintf(stderr, "INA219 shunt voltage read %2x %2x = %fV.\n", (int) buf3[0], (int) buf3[1], 10e-6f * ((((int8_t)buf3[0]) << 8) + (buf3[1] & 0xff))); }

        // Get current.
        if(!read_i2c_register(fd, INA219_addr, 4, 2, buf3))
            {
            fprintf(stderr, "Failed current read from INA219.\n");
            return(-1);
            }
        if(verbose) { fprintf(stderr, "INA219 current read %2x %2x = %fA.\n", (int) buf3[0], (int) buf3[1], 50e-6f * ((((int8_t)buf3[0]) << 8) + (buf3[1] & 0xff))); }
    }
#endif

    // Get power.
    if(!read_i2c_register(fd, INA219_addr, 3, 2, buf3))
        {
        fprintf(stderr, "Failed power read from INA219.\n");
        return(-1);
        }
    const int power = ((((int8_t)buf3[0]) << 8) + (buf3[1] & 0xff));
    if(verbose) { fprintf(stderr, "INA219 power read %2x %2x = %dmW.\n", (int) buf3[0], buf3[1], power); }

#if 1
    // Shut down (put into low-power mode).
    // Write 0 to config register.
    buf3[0] = 0;
    buf3[1] = 0;
    buf3[2] = 0;
    if(!write_i2c_bytes(fd, INA219_addr, 3, buf3))
        {
        fprintf(stderr, "Failed shutdown of INA219.\n");
        return(-1);
        }
#endif

    return(power);
    }


// Reads 11-bit value [0,2047], from specified ADC channel.
// Returns -1 in case of failure.
static int readADC(const int fd, const uint8_t addr, const uint8_t channel)
    {
    // Write single-byte command to do one-shot 12-bit conversion from ADC.
    const uint8_t wc = 0x80 | ((channel & 3) << 5);
    if(verbose) { fprintf(stderr, "ADC command %x %x\n", addr, wc); }
    if(!write_i2c_1_byte_command(fd, addr, wc)) { return(-1); }
    // Read 3-byte response (MMMMDDDD DDDDDDDD CCCCCCCC).
    // Spin a little until conversion is done (RDY bit low).
    unsigned char rdbuf[3];
    int i;
    for(i = 4; --i >= 0; )
        {
        // Sleep 5ms for conversion to complete (240SPS@12bits).
        usleep(5000);
        if(!read_i2c_bytes(fd, addr, 3, rdbuf))
            {
            fprintf(stderr, "Failed to read ADC result.\n");
            return(-1);
            }
        if(rdbuf[2] & 0x80) { continue; } // Not RDY.
        const int result = ((rdbuf[0] & 0xff) << 8) + (rdbuf[1] & 0xff);
        if(verbose)
            { fprintf(stderr, "ADC (%x %d) response %d (%x %x %x)\n", addr, channel, result, rdbuf[0], rdbuf[1], rdbuf[2]); }
        return(result);
        }
    fprintf(stderr, "ADC read did not complete in reasonable time.\n");
    return(-1);
    }

// Construct the full file path name for a flag in buf.
// The buffer should usually be at least FLAG_BUF_SIZE chars.
static constexpr int FLAG_BUF_SIZE =
    FLAG_DIR_LEN + 1 /* / */ + MAX_FLAG_NAME + 6 /* .flag\0 */;
static void makeFullFlagPath(char * const buf, const int bufsize,
                             const char * const flagName)
    {
    if(strlen(flagName) > MAX_FLAG_NAME)
        {
        fprintf(stderr, "Bad flag name (too long); not setting flag.\n");
        buf[0] = '\0';
        return;
        }
    strcpy(buf, FLAG_DIR);
    strcat(buf, "/");
    strncat(buf, flagName, MAX_FLAG_NAME);
    strcat(buf, ".flag");
    }

// Read (input) flag; returns true iff exists/set.
static bool isFlagSet(const char * const flagName)
    {
    char buf[FLAG_BUF_SIZE];
    makeFullFlagPath(buf, sizeof(buf), flagName);
    if('\0' == buf[0]) { return(false); }
    const bool exists = (0 == access(buf, R_OK));
    if(verbose) { fprintf(stderr, "Input flag %s is %s.\n", flagName, exists ? "set" : "unset"); }
    return(exists);
    }

// Returns number of seconds system up, or -1 in case of error.
static long systemUptime()
    {
    struct sysinfo sinfo;
    if(0 == sysinfo(&sinfo)) { return(sinfo.uptime); }
    return(-1); // Error.
    }

// Get last-modification time of flag, or -1 if flag not set (does not exist).
static time_t flagTime(const char * const flagName)
    {
    char buf[FLAG_BUF_SIZE];
    makeFullFlagPath(buf, sizeof(buf), flagName);
    if('\0' == buf[0]) { return(-1); }
    struct stat s;
    if(stat(buf, &s) < 0) { return(-1); }
    return(s.st_mtime);
    }

// Returns true if specified external flag file is present.
static bool isExternalFlagSet(const char * const pathName)
    {
    const bool exists = (0 == access(pathName, R_OK));
    if(verbose) { fprintf(stderr, "External flag file %s is %s.\n", pathName, exists ? "present" : "absent"); }
    return(exists);
    }

// Create/remove flag in flags directory; flag file present if value true.
// This won't create/touch/remove unless necessary to reduce filesystem load.
// This also means that a flag should retain its modification time until cleared.
// (Flags area may often be in an in-memory volatile filesystem.)
// Inhibited if 'nosetflags' is true.
static void doFlag(const char * const flagName, const bool flagValue)
    {
    if(nosetflags) { return; }
    if(verbose) { fprintf(stderr, "Flag %s is %s.\n", flagName, flagValue ? "set" : "unset"); }
    char buf[FLAG_BUF_SIZE];
    makeFullFlagPath(buf, sizeof(buf), flagName);
    if('\0' == buf[0]) { return; }
    const bool exists = (0 == access(buf, R_OK));
    // If state is already correct, do nothing.
    if(exists == flagValue) { return; }
    // Flag should be absent: remove.
    if(!flagValue)
        {
        if(verbose) { fprintf(stderr, "Removing/clearing flag %s...\n", buf); }
        if(0 != unlink(buf))
            { fprintf(stderr, "ERROR removing flag %s.\n", buf); }
        }
    else
        {
        if(verbose) { fprintf(stderr, "Creating/setting flag %s...\n", buf); }
        const int fd = creat(buf, 0444);
        if(fd < 0)
            { fprintf(stderr, "ERROR creating flag %s.\n", buf); }
        else { close(fd); }
        }
    }
    
// Set external dump control through GPIO to given state.
// Inhibited if nosetflags is true.
static void setDumpingGPIO(const bool isDumping)
    {
    if(nosetflags) { return; }

#if defined(ISEXPPI) && ISEXPPI
    // For RPi3B using ExpanderPi GPIO expander.
    turnDumpOn_ExP(isDumping);
#else
    // For RPi1 and 2 on old HAT stack.
    // Controlling DUMP directly from GPI23.
    static const char *device = "/sys/class/gpio/gpio23/value";
    const bool exists = (0 == access(device, R_OK));
    if(!exists)
        {
        if(verbose) { fprintf(stderr, "GPIO for dumping does not exist yet: %s\n", device); }
        if(!doEcho("/sys/class/gpio/export", "23") ||
           !doEcho("/sys/class/gpio/gpio23/direction", "out") ||
           (0 != access(device, R_OK)))
            {
            fprintf(stderr, "GPIO for dumping CANNOT BE CREATED: %s\n", device);
            return;
            }
        }
    if(!doEcho(device, isDumping ? "1" : "0"))
        { fprintf(stderr, "GPIO for dumping CANNOT BE SET: %s\n", device); }
#endif
    }

// Do work, returning exit code (zero if all is well).
// At a high level, most of the interesting stuff happens here.
int doWork()
    {
    if(verbose)
        {
#if defined(ISEXPPI)
        fprintf(stderr, "ISEXPPI=%d: Expander Pi is %sin use.\n", int(ISEXPPI), ((ISEXPPI)?"":"not "));
#else
        fprintf(stderr, "ISEXPPI undefined: use of Expander Pi not known.\n");
#endif
        }

    const int fd = i2c_setup();
    if(fd < 0) { return(1); } // Exit with error if set-up not possible.

    // Record when the measurements are being taken.
    struct timeval measurementTime;
    gettimeofday(&measurementTime, NULL);

    // TAKE POWER MEASUREMENT ASAP (possibly after a sleep for accuracy).
    // Fetch battery voltage from controller if available.
    uint16_t Vb_f_raw;
    int Vb_f_mV = -1;
    if(ENABLE_METERBUS && getMODBUSReg(SS_REG_Vb_f, Vb_f_raw))
        {
        Vb_f_mV = fMODBUSTmV(Vb_f_raw);
        // Postpone Vb_f_mV print until after all power measurements taken.
        }
    // b1AtCtrl true if voltage successfully measured at controller/bank.
    const bool b1AtCtlr = (-1 != Vb_f_mV);
    // Fetch load current to compute load power if we fetched voltage OK.
    uint16_t Adc_il_f;
    int Adc_il_f_mA = -1;
    int32_t loadPowermW = -1;
    if(b1AtCtlr && getMODBUSReg(SS_REG_Adc_il_f, Adc_il_f))
        {
        Adc_il_f_mA = fMODBUSTmA(Adc_il_f);
        // Postpone Adc_il_f_mA print until after all power measurements taken.
        loadPowermW = (uint32_t)(
            (int32_t)fMODBUSTmV(Vb_f_raw) * (int32_t)fMODBUSTmA(Adc_il_f)
                + 999l) / 1000l;
        if(verbose) { fprintf(stderr, "LOAD POWER (mW): %lu\n", (unsigned long)loadPowermW); }
        }
    // Measure system power consumption (from 12V).
    int bv = -1;
    const int32_t powermW = ENABLE_INA219 ? getPowerDrain(fd, &bv) : loadPowermW;
    // Postpone print until after all power measurements taken.
    if(verbose && (-1 != Vb_f_mV)) { fprintf(stdout, "SS-MPPT-15L Vb_f slow battery voltage (mV): %d\n", Vb_f_mV); }
    if(verbose && (-1 != Adc_il_f_mA)) { fprintf(stdout, "SS-MPPT-15L Adc_il_f load current (mA): %u\n", Adc_il_f_mA); }

    // Note if dumping upon entry.
    const bool wasDumping = isFlagSet(FLAGIO_DUMPING);
    if(verbose && wasDumping) { fprintf(stderr, "System dumping for %ldm.\n", (long)((measurementTime.tv_sec - flagTime(FLAGIO_DUMPING)) / 60)); }

    // Set/remove the 'DAYLIGHT' and related flags as appropriate.
    //const bool isDay = (ldr > LDR_daylight_thr);
    //if(verbose) { fprintf(stderr, "LDR %d vs daylight threshold %d.\n", ldr, LDR_daylight_thr); }
    // Measure actual charging current, or assume it based on grid-tie.
    // Note that this is gross, not net of load current, it seems.
    uint16_t Adc_ic_f;
    int chargemA = -1;
    if(getMODBUSReg(SS_REG_Adc_ic_f, Adc_ic_f))
        {
        chargemA = fMODBUSTmA(Adc_ic_f);
        if(verbose) { fprintf(stderr, "SS-MPPT-15L Adc_ic_f battery charge current (mA): %u\n", fMODBUSTmA(Adc_ic_f)); }
        }
    // Infer darkness from lack of charging (charge current exactly 0).
    // use external flag if cannot read charge current.
    const bool isDark = (chargemA >= 0) ? (0 == chargemA) :
        isExternalFlagSet(darkFlag);
    const bool significantGridTieGeneration =
        !isDark && !isExternalFlagSet(lowGenFlag);
    // Read the LDR and set/remove the 'DAYLIGHT' flag as appropriate.
    // (Use PV current as proxy for light level if no LDR.)
    const int ldr = ENABLE_AL ? readADC(fd, ADC2_addr, LDR_zch_ADC2) : chargemA;

    // Net charge (+ve) or load current (-ve) in mA.
    // If one/both values is unavailable then use 0 for it/them.
    const int_fast16_t netChargemA = MAX(0, chargemA) - MAX(0, Adc_il_f_mA);
    if(verbose) { fprintf(stderr, "NET battery charge current (mA): %d\n", netChargemA); }

    // Note if net discharge (or charge) via this controller.
    // If not known then default to assuming no (net) discharge or discharge.
    const bool isNetDischarging = (netChargemA < 0);
    doFlag(FLAGIO_DISCHARGING, isNetDischarging);
    const bool isNetCharging = (netChargemA > 0);
    doFlag(FLAGIO_CHARGING, isNetCharging);

    uint16_t T_batt;
    const bool read_T_batt = getMODBUSReg(SS_REG_T_batt, T_batt);
    // Signed battery temperature in Celsius; impossible value if unavailable.
    const int_fast16_t s_T_batt = read_T_batt ? ((int_fast16_t) T_batt) : -999;
    // Signed battery temperature threshold compensation voltage.
    // 0 if battery temp not known.
    // Goes down as temperature goes up; +ve below base temperature (~20C).
    const int_fast16_t _T_batt_temp_comp_mV = (!read_T_batt) ? 0 :
        computeLATempComp_mV(s_T_batt);
    if(verbose && read_T_batt) { fprintf(stdout, "SS-MPPT-15L T_batt (C): %d, temperature compensation %dmV.\n", s_T_batt, _T_batt_temp_comp_mV); }

    // Compute LA battery impedance in milliohms.
    const uint16_t LAmo = sagLADumpCorrmo(read_T_batt?T_batt:20);
    if(verbose) { fprintf(stderr, "Estimated LA battery impedance: %dmohm.\n", LAmo); }

    // Adjustment to battery voltage to back out effect of charge/load.
    // Ie add to measured voltage to get 'ideal'/'internal' voltage.
    const int_fast16_t sagB1mV = int_fast16_t((netChargemA * int32_t(LAmo)) / 1000);
    if(verbose) { fprintf(stderr, "Estimated LA battery sag/(-rise) from internal impedance, and nominal internal voltage, effective w/ temp comp: %dmV, %dmV, %dmV.\n", sagB1mV, int(Vb_f_mV) - sagB1mV, int(Vb_f_mV) - sagB1mV - _T_batt_temp_comp_mV); }

    // Compute compensation for battery sag at battery terminals.
    // Use actual net battery current outflow if known and +ve,
    // else estimate based on expected controlled dump's consumption.
    // This is a non-negative value; does not include any rise from charging.
    // Compute net load at battery terminals (mA) when discharging else 0.
    const uint16_t batTerminalLoadmA = (b1AtCtlr && (-1 != Adc_il_f_mA) && (-1 != chargemA)) ?
        (MAX(Adc_il_f_mA - chargemA, 0)) :
        (wasDumping ? DUMPLOAD_APPROX_mA : 0);
    // Compute sag at battery terminals (mV) when discharging else 0.
    // Have to be slightly careful about overflow and underflow...
    const uint16_t batTerminalSagmV =
        uint16_t((LAmo * uint32_t(batTerminalLoadmA) +500) / 1000);
    if(verbose) { fprintf(stderr, "Estimated actual battery sag from load (mV): %dmV.\n", batTerminalSagmV); }
    // Compute compensation for expected battery and wiring sag for dump load.
    const uint16_t sagLAForDumpLoadmV =
        uint16_t((LAmo*uint32_t(DUMPLOAD_APPROX_mA) +500)/1000);
    const uint16_t sagWiringForDumpLoadmV =
        uint16_t((sagWiringmVperADumpCorr*uint32_t(DUMPLOAD_APPROX_mA) +500)/1000);
    // Dynamic threshold compensation for load current.
    // This should be subtracted from nominal threshold to allow for sag.
    // If only RPi Vbus measurement is available, allow for wiring sag also.
    // This compensation is capped for sanity!
    // Lower nominal thresholds against which readings are measured, by this.
    static constexpr uint16_t maxB1LoadSagCorrmV = 300;
    const int B1LoadSagCorr = -MIN(maxB1LoadSagCorrmV, batTerminalSagmV +
        ((wasDumping && !b1AtCtlr) ? sagWiringForDumpLoadmV : 0));
    if(verbose) { fprintf(stderr, "LA/B1 dynamic threshold adjustment for load current %dmV.\n", B1LoadSagCorr); }
    if(verbose && !wasDumping) { fprintf(stderr, "B1 and wiring dump load sag allowances %dmV, %dmV.\n", sagLAForDumpLoadmV, sagWiringForDumpLoadmV); }

    // Read both 12V-nominal battery inputs (fsd ~19.22V) raw.
    // DHD20140708: BATT1 1328 raw = 12.48V direct to local rail.
    // DHD20140708: BATT2 1359 raw = 12.77V direct to local rail.
    // If voltage can't be read then -ve result should force VLOW mode.
#if defined(ISEXPPI) && ISEXPPI
    const int batt1mVLocal = int(readB1mV_ExP());
#else
    const int batt1 = readADC(fd, ADC1_addr, BATT1_zch_ADC1);
    const int batt1mVLocal = (int) (batt1 * mVperUlpBatt);
#endif
    //const int batt2 = ENABLE_B2 ? readADC(fd, ADC2_addr, BATT2_zch_ADC2) : 0;
    //const int batt2mV = ENABLE_B2 ? (int) (batt2 * mVperUlpBatt) : -1;
    if(verbose) { fprintf(stderr, "Battery voltage 1 local (LA) %dmV.\n", batt1mVLocal); }
    //if(verbose) { fprintf(stderr, "Battery voltage 2 local (Li) %dmV.\n", batt2mV); }

    // Estimate supply impedance to Vbus ie RPi intake (if enabled).
    if(true && verbose)
        {
        const int supplyDropmV = Vb_f_mV - batt1mVLocal;
        const int impliedSupplyImpedancemo = (1000*supplyDropmV) / Adc_il_f_mA;
        fprintf(stderr, "Supply drop B1 to BV %dmV; implied supply impedance %dmohm.\n", supplyDropmV, impliedSupplyImpedancemo);

        // Get load voltage.
        uint16_t Adc_vl_f;
        if(getMODBUSReg(SS_REG_Adc_vl_f, Adc_vl_f))
            {
            const int Adc_vl_f_mV = fMODBUSTmV(Adc_vl_f);
            fprintf(stdout, "Adc_vl_f load voltage (mV): %u\n", Adc_vl_f_mV);
            // Compute drop/impedance to controller load terminals...
            const int controllerDropmV = Vb_f_mV - (int)Adc_vl_f_mV;
            const int impliedControllerImpedancemo = (1000*controllerDropmV) / Adc_il_f_mA;
            fprintf(stderr, "Supply drop B1 to controller load terminals %dmV; implied controller impedance %dmohm.\n", controllerDropmV, impliedControllerImpedancemo);
            // Compute drop/impedance from controller load terminals...
            const int supplyDropWiringmV = (int)Adc_vl_f_mV - batt1mVLocal;
            const int impliedWiringImpedancemo = (1000*supplyDropWiringmV) / Adc_il_f_mA;
            fprintf(stderr, "Supply drop controller to BV %dmV; implied wiring impedance %dmohm.\n", supplyDropWiringmV, impliedWiringImpedancemo);
            }
        }

    // If INA219 not in use, take bus voltage from batt1 ADC.
    const int busVoltage = ENABLE_INA219 ? bv : batt1mVLocal;

    // If Meterbus not in use or not available, use ADC value.
    const int batt1mV = (!ENABLE_METERBUS || (-1 == Vb_f_mV)) ?
        batt1mVLocal : Vb_f_mV;

//    // Flag 'moreThanDaySinceVLOW' True if 'NOTVLOW' flag is set
//    // AND long time (>>24h) since battery was last VLOW
//    //     OR flag set soon enough after boot to be likely spurious.
//    // If true, dumping may be reduced so as to help achieve a full charge
//    // (including desulphation) regularly and thus maintain battery condition.
//    static constexpr long MAX_TIME_SINCE_VLOW_S = 28*60*60L; // 28h
//    long timeSinceVLOW = -1;
//    const bool moreThanDaySinceVLOW =
//        isFlagSet(FLAGIO_BATTNOTVLOW) &&
//        (((timeSinceVLOW = (measurementTime.tv_sec-flagTime(FLAGIO_BATTNOTVLOW)))>MAX_TIME_SINCE_VLOW_S) ||
//         (systemUptime() - timeSinceVLOW < 3600L));
//    if(verbose && !moreThanDaySinceVLOW) { fprintf(stderr, "Not long since VLOW (or system not been up long enough to be sure).\n"); }

    // Flag 'muchMoreThanDaySinceVHIGH' true if 'NOTVHIGH' flag is set
    // AND long time (>>24h) since battery was last VHIGH
    //     OR flag set soon enough after boot to be likely spurious.
    // If true, dumping is less aggressive so as to help achieve a full charge
    // (including desulphation) regularly and thus maintain battery condition.
    static constexpr long MAX_TIME_SINCE_VHIGH_S = 80*60*60L; // 80h, ~3d+8h
    long timeSinceVHIGH = -1;
    const bool muchMoreThanDaySinceVHIGH =
        (isFlagSet(FLAGIO_BATTNOTVHIGH) &&
        (((timeSinceVHIGH = (measurementTime.tv_sec-flagTime(FLAGIO_BATTNOTVHIGH)))>MAX_TIME_SINCE_VHIGH_S) ||
         (systemUptime() - timeSinceVHIGH < 3600L)));
    if(verbose && muchMoreThanDaySinceVHIGH) { fprintf(stderr, "Long time since VHIGH >%0.1fd (or system not been up long enough to be sure).\n", MAX_TIME_SINCE_VHIGH_S/(24.0f*60*60)); }

    // Flag 'manyDaysSinceFULL' true if 'NOTFULL' flag is set
    // AND long time (>>1w) since battery was last FULL
    //     (thinking of typical lead-acid equalisation schedules)
    //     OR flag set soon enough after boot to be likely spurious.
    // If true, dumping is less aggressive so as to help achieve a full charge
    // (including desulphation) regularly,
    // or more aggressively conserve what charge there is for darker days,
    // and thus maintain battery condition.
    // This might be considered a 'not summer' detector.
    // Note FULL gap 2018/07/03 to > 2018/07/15 in summer heatwave.
    // Note FULL gap 2021/09/24 to 2021/10/31.
    static constexpr long MAX_TIME_SINCE_FULL_S = 45*24*60*60L; // 45d, 6W
    long timeSinceFULL = -1;
    const bool manyDaysSinceFULL =
        (isFlagSet(FLAGIO_BATTNOTFULL) &&
        (((timeSinceFULL = (measurementTime.tv_sec-flagTime(FLAGIO_BATTNOTFULL)))>MAX_TIME_SINCE_FULL_S) ||
         (systemUptime() - timeSinceFULL < 4*3600L)));
    if(verbose && manyDaysSinceFULL) { fprintf(stderr, "Long time since FULL >%.1fd (or system not been up long enough to be sure).\n", MAX_TIME_SINCE_FULL_S/(24.0f*60*60)); }

    // Compute basic compensation for temperature and sag/rise from dis/charge.
    const int B1BasicComp =
        + (int)B1LoadSagCorr // Resistive sag compensation (-ve for discharge).
        + (int)_T_batt_temp_comp_mV; // Temp compensation (-ve when cold).
    if(verbose) { fprintf(stderr, "Battery voltage 1 basic compensation %dmV.\n", B1BasicComp); }

    // Adjust output flags based on battery levels.

    // VERY LOW threshold calc.
    // NOT CURRENTLY: Some extra hindcast/forecast- based protection at this low end.
    // Compensate for sag from load,
    // but never adjust this protective very low threshold downwards overall.
    // Generally aim to keep no-load voltage high enough to avoid sulphation.
    constexpr int B1VLb = BATT1_THR_VL_mV;
    const int B1VL = MAX(BATT1_THR_VL_mV, B1VLb
        //+ (int)extraMarginmV // +ve with poor forecast or poor recent charge.
        + B1BasicComp);
    //const int B2VL = (extraMarginLT || wasDumping) ? BATT2_THR_VL_mV : BATT2_PFTHR_VL_mV;
    const bool isVLOW = (batt1mV <= B1VL);
                        // || (ENABLE_B2 && B2InSoC && (batt2mV <= B2VL));
    if(verbose) { fprintf(stderr, "Battery voltage 1 'very low' threshold %dmV (raw %dmV).\n", B1VL, BATT1_THR_VL_mV); }
    //if(ENABLE_B2 && verbose) { fprintf(stderr, "Battery voltage 2 'very low' threshold %dmV.\n", B2VL); }
    doFlag(FLAGIO_BATTNOTVLOW, !isVLOW);
    doFlag(FLAGO_BATTVLOW, isVLOW);

    // LOW threshold calc.
    // Some extra hindcast/forecast- based protection at this low end.
    // Compensate for sag from load.
    constexpr int B1Lb = BATT1_THR_L_mV;
    const int B1Lu = B1Lb
//        + (int)extraMarginmV // +ve with poor forecast or poor recent charge.
        + B1BasicComp;
    const int B1L = MAX(B1VL, B1Lu);
    const bool isLOW = isVLOW || (batt1mV <= B1L);
    if(verbose) { fprintf(stderr, "Battery voltage 1 'low' threshold %dmV (raw %dmV).\n", B1L, BATT1_THR_L_mV); }
    doFlag(FLAGO_BATTNOTLOW, !isLOW);
    doFlag(FLAGO_BATTLOW, isLOW);
    // Compute margin below which dumping may oscillate or be unsafe.
    // If not currently dumping then pre-compensate for expected sag.
    // This sag depends on how close to the battery its voltage is measured.
    // Capped at resting 'full' voltage when discharging.
    const int B1LmRaw = MAX(B1L, B1Lu+(wasDumping?0:
        (MAX(BATT1_DLdelta_mV, sagLAForDumpLoadmV) +
            (b1AtCtlr?0:sagWiringForDumpLoadmV))));
    const int B1Lm = isNetDischarging ? MIN(BATT1_RESTING_FULL_mV, B1LmRaw) :
        B1LmRaw;
    if(verbose) { fprintf(stderr, "Battery voltage 1 dump margin threshold %dmV (B1 delta %dmV, sag for dump load %dmV).\n", B1Lm, BATT1_DLdelta_mV, sagLAForDumpLoadmV); }

    // (Not) HIGH threshold calc.
    // Compensate for sag from load but not rise from charging,
    // because this voltage should only be achievable when charging.
    const int B1NH = BATT1_THR_H_mV
//        + (int)extraMarginmV // +ve with poor forecast or poor recent charge.
        + (int)B1LoadSagCorr; // -ve compensation for resistive sag.
    //const int B2NH = (extraMarginLT ? BATT2_THR_H_mV : BATT2_PFTHR_H_mV) -
    //    (wasDumping ? BATT2_DUMPCORR_mV : 0);
    const bool isNOTHIGH = isLOW || (batt1mV <= B1NH);
                            // || (ENABLE_B2 && B2InSoC && (batt2mV <= B2NH));
    if(verbose) { fprintf(stderr, "Battery voltage 1 'not high' threshold %dmV (raw %dmV).\n", B1NH, BATT1_THR_H_mV); }
    //if(ENABLE_B2 && verbose) { fprintf(stderr, "Battery voltage 2 'not high' threshold %dmV.\n", B2NH); }
    doFlag(FLAGIO_BATTNOTHIGH, isNOTHIGH);
    doFlag(FLAGO_BATTHIGH, !isNOTHIGH);

    // Fetch charge state if at least 'HIGH', else leave at zero.
    uint16_t charge_state = 0;
    if(!isNOTHIGH) { getMODBUSReg(SS_REG_charge_state, charge_state); }
    if(verbose && !isNOTHIGH) { fprintf(stderr, "Charge state %d.\n", (int)charge_state); }

    // Also compute if at least in absorption phase (ie beyond bulk),
    // which means that the system probably has spare energy to dump,
    // and in any case is not necessarily charging at max efficiency.
    const bool isUnknownChargeState = (0 == charge_state);
    const bool isAbsOrBetter = (charge_state >= cs_ABSORPTION);

    // VERY HIGH threshold calc.
    // No sag nor rise compensation, nor extra safety margins,
    // though logically pushed up to be be no lower than HIGH.
    // Does require hitting at least absorption stage if status available.
    const bool isVHIGH = (!isNOTHIGH) &&
        (batt1mV > BATT1_THR_VH_mV) &&
        (isAbsOrBetter || isUnknownChargeState);
    if(verbose) { fprintf(stderr, "Battery voltage 1 'very high' raw threshold %dmV.\n", BATT1_THR_VH_mV); }
    doFlag(FLAGIO_BATTNOTVHIGH, !isVHIGH);
    doFlag(FLAGO_BATTVHIGH, isVHIGH);

    // FULL threshold calc.
    // FULL means VHIGH and controller is available and reporting FLOAT status.
    const bool isFULL = isVHIGH && (cs_FLOAT == charge_state);
    const bool isNOTFULL = !isFULL;
    doFlag(FLAGIO_BATTNOTFULL, isNOTFULL);
    doFlag(FLAGO_BATTFULL, isFULL);

    // Do basic computation of usable SoC, [0,100] percent.
    // This SoC estimate will be wrong if there are external loads
    // or charging sources not seen by the controller.
    const uint8_t B1UsableSoC = computeLAUsableSoC(
        batt1mV,
        sagB1mV,
        _T_batt_temp_comp_mV);

    // If true, suppress most dumping to retain energy.
    // Mainly triggered in winter when maximally conserving charge.
    // Triggering this from only manyDaysSinceFULL is too slow.
    //const bool suppressMostDumping = LESS_DoD && manyDaysSinceFULL;
    const bool suppressMostDumping = LESS_DoD && muchMoreThanDaySinceVHIGH;

    // Protective +ve compensation on lower thresholds (raising them)
    // based on backward- and forward- looking measures.
    // Add further safety margin if not VHIGH or FULL for a while
    // or insolation forecast is poor.
    const uint8_t extraMarginPC =
        ((!isFlagSet(FLAGI_FORECASTGENGOOD)) ? PF_reserve_PC : 0) + 
        (muchMoreThanDaySinceVHIGH ? (PF_reserve_PC) : 0) +
        (manyDaysSinceFULL ? (4 * PF_reserve_PC) : 0);
    const bool extraMarginLT = (0 != extraMarginPC);
//    const uint16_t extraMarginmV = extraMarginLT ? BATT1_PFTHR_delta_mV : 0;
    if(verbose && extraMarginLT) { fprintf(stderr, "Battery extra USoC reserve %d%% for poor forecast and/or VHIGH / FULL not recent.\n", int(extraMarginPC)); }
    const uint8_t minUsableSoCToDumpThrPC =
        baseUsableSoCToDumpThrPC + extraMarginPC;
    // Higher threshold for some less-important dump activity.
    const uint8_t higherUsableSoCToDumpThrPC = (100 + minUsableSoCToDumpThrPC) / 2;
    const bool minUsableSoCToDump = (B1UsableSoC >= (suppressMostDumping ?
        higherUsableSoCToDumpThrPC : minUsableSoCToDumpThrPC));
    if(verbose) { fprintf(stderr, "Battery 1 effective/usable SoC %d%%, dump thresholds min %d%% higher %d%% (uncompensated USoC %d%%); usable to dump = %d.\n", int(B1UsableSoC), int(minUsableSoCToDumpThrPC), int(higherUsableSoCToDumpThrPC), int(computeLAUsableSoC(batt1mV)), int(minUsableSoCToDump)); }

    // Compute short status flag name.
    const char * const bsfl =
        (isVLOW ? "VL" :
        (isLOW ? "L" :
        (isNOTHIGH ? "OK" :
        (!isVHIGH ? "H" :
        (isNOTFULL ? "VH" :
        "F")))));

#if !defined(RPI3B)
    // DON'T ATTEMPT TO FIDDLE WITH CPU CLOCK ON RPI3B AND LATER.
    // Adjust CPU governor based on battery state.
    // May also be driven by recent charge behaviour and/or forecast.
    // TODO: may also be driven by time-of-day Web server demand for example.
    // Use a lower sampling down factor for faster clock drop clock when low.
    //adj_sampling_down_factor((isLOW || (muchMoreThanDaySinceVHIGH && isNetDischarging)) ? CONSERVING_SAMPLING_DOWN_FACTOR : DEFAULT_SAMPLING_DOWN_FACTOR);
    adj_sampling_down_factor(isLOW ? CONSERVING_SAMPLING_DOWN_FACTOR : DEFAULT_SAMPLING_DOWN_FACTOR);
    // Use a higher threshold to save energy when batteries low.
    adj_up_threshold(isLOW ? CONSERVING_UP_THRESHOLD : DEFAULT_UP_THRESHOLD);
    // Use a higher sampling rate for lower latency unless batteries are low.
    adj_sampling_rate(isVLOW ? DEFAULT_SAMPLING_RATE : RESPONSIVE_SAMPLING_RATE);
#endif

    // Minimum dump (on) time in normal circumstances (s), to reduce cycling.
    // This assumes that dump power controlled by this is relatively low.
    // Minimum dump off time may be longer, to reduce duty cycle.
    // 30 minutes (with 60 minutes off) or thereabouts works well.
    static constexpr long MIN_DUMP_S = 19 * 60; // ~20 minutes.

    // Prepare to recompute if dumping is desirable.
    // Defaults to continuing as before.
    bool isDumping = wasDumping;
    // Single-char indicator of reason for (new) dumping state.
    // Defaults to '-' for no change.
    char dumpReason = '-';

    time_t start; // Inline start times for various points below.
    int minS; // Inline minimum (sec) times for various points below.
    //uint16_t v16u; // 16-but unsigned value for various points below.

    // Avoid/stop dumping immediately if battery SoC appears to be LOW.
    // This should be the shortest code path to save energy when battery low.
    if(isLOW)
        {
        dumpReason = 'L'; // *L*OW battery.
        isDumping = false;
        }
    // Stop/avoid dumping if not permitted.
    else if(isFlagSet(FLAGI_NODUMP))
        {
        if(verbose) { fprintf(stderr, "Dumping not permitted.\n"); }
        dumpReason = 'n'; // Dumping *n*ot permitted.
        isDumping = false;
        }
    // If only recently stopped dumping then don't restart.
    // This helps cap the amount of cycling of the dump relay and contact wear.
    // Spend longer off when the system SoC hasn't been good...
    else if(!wasDumping &&
        ((measurementTime.tv_sec-flagTime(FLAGIO_DUMPINGEND)) <
            (muchMoreThanDaySinceVHIGH ? 2*MIN_DUMP_S : MIN_DUMP_S)))
        {
        if(verbose) { fprintf(stderr, "Too soon since dumping stopped to restart.\n"); }
        dumpReason = 't'; // In minimum *t*ime since dumping stopped.
        isDumping = false;
        }
    // If only recently started dumping then keep going.
    // This helps cap the amount of cycling of the dump relay and contact wear.
    else if(wasDumping &&
        ((measurementTime.tv_sec-flagTime(FLAGIO_DUMPING)) < MIN_DUMP_S))
        {
        if(verbose) { fprintf(stderr, "Too soon since dumping started to stop.\n"); }
        dumpReason = 'T'; // In minimum *T*ime since dumping started.
        isDumping = true;
        }
    // Avoid dumping if battery level can't easily support extra/dump load.
    else if(batt1mV <= B1Lm)
        {
        if(verbose) { fprintf(stderr, "Insufficient margin to dump, threshold %dmV.\n", B1Lm); }
        dumpReason = 'm'; // Insufficient *m*argin to dump.
        isDumping = false;
        }
    // If battery is FULL then dump.
    else if(isFULL)
        {
        if(verbose) { fprintf(stderr, "Battery is FULL.\n"); }
        dumpReason = 'F'; // Battery is *F*ULL.
        isDumping = true;
        }
    // If VERY HIGH for long enough then dump.
    else if(isVHIGH &&
        ((start = flagTime(FLAGO_BATTVHIGH)) > 0) &&
        ((measurementTime.tv_sec - start) >
            (minS = (60 * (extraMarginLT ? BATT1_PFMINVH_BEFORE_DUMP_M :
                                           BATT1_MINVH_BEFORE_DUMP_M)))))
        {
        dumpReason = 'V'; // *V*ERY HIGH battery.
        isDumping = true;
        }
#if 1 
    // Unless conserving charge in winter...
    // Determine dumping start by at least reaching absorption stage.
    else if(!suppressMostDumping &&
        (!isNOTHIGH) && isAbsOrBetter &&
        ((start = flagTime(FLAGO_BATTHIGH)) > 0) &&
        ((measurementTime.tv_sec - start) > (minS = (60 * BATT1_MINH_BEFORE_DUMP_M))))
        {
        dumpReason = 'A'; // In *A*bsoption or better.
        isDumping = true;
        }
#else
    // Unless conserving charge in winter...
    // If HIGH for long enough then dump regardless of controller.
    else if(!suppressMostDumping &&
        (!isNOTHIGH) &&
        ((start = flagTime(FLAGO_BATTHIGH)) > 0) &&
        ((measurementTime.tv_sec - start) > (minS = (60 * BATT1_MINH_BEFORE_DUMP_M))))
        {
        dumpReason = 'H'; // *H*IGH battery.
        isDumping = true;
        }
#endif

    // Avoid dumping if SoC insufficient.
    else if(!minUsableSoCToDump)
        {
        if(verbose) { fprintf(stderr, "Low usable *S*oC ~%d%% (min %d%%|%d%%).\n", int(B1UsableSoC), int(minUsableSoCToDumpThrPC), int(higherUsableSoCToDumpThrPC)); }
        dumpReason = 'S'; // *S*oC too low.
        isDumping = false;
        }
    // IF VHIGH within the last day or so (to help avoid sulphation)
    //     AND at least HIGH,
    //     AND achieving significant net/excess charge current
    // (enough to sustain the expected dump load directly)
    // THEN enable dumping.
    // TODO: may also want to limit this to when net charge rcvd so far today.
    // If already dumping then lower the threshold, eg for hysteresis.
    // This is intended to help bring on 'dump' loads in the morning
    // while the battery is charging and there seems to be spare capacity.
    else if((!LESS_DoD) && (!muchMoreThanDaySinceVHIGH) && (!isNOTHIGH) &&
        (chargemA >= (wasDumping ? 1*DUMPLOAD_APPROX_mA :
                                   2*DUMPLOAD_APPROX_mA)) &&
        (-1 != Adc_il_f_mA) && (chargemA >= Adc_il_f_mA))
        {
        if(verbose) { fprintf(stderr, "Battery bank charging %dmA.\n", chargemA); }
        dumpReason = 'c'; // Battery is *c*harging.
        isDumping = true;
        }
    // Take dump/optional load off grid if (highest priority first):
    //    * in typical winter grid peak demand window
    //    * [disabled] in peak grid carbon intensity window (red)
    // providing that the effective SoC is OK.
    // Applies even when conserving charge in the depths of winter.
    else if(/*isExternalFlagSet(gridRedFlag)||*/ isFlagSet(FLAGI_GBGRIDPEAK))
        {
        if(verbose) { fprintf(stderr, "Dumping driven by high-priority external factors.\n"); }
        dumpReason = 'E'; // Dump for *E*xternal for high-priority factor.
        isDumping = true;
        }
    // Ride through brief dips in generation.
    // If OK and was dumping and was recently HIGH then keep dumping.
    // Unless conserving charge in winter.
    else if(!suppressMostDumping &&
        ((wasDumping && (isNOTHIGH && !isLOW) &&
            ((start = flagTime(FLAGIO_BATTNOTHIGH)) > 0) &&
            ((measurementTime.tv_sec - start) < 2*MIN_DUMP_S))))
        {
        if(verbose) { fprintf(stderr, "Ride through brief dip NOTHIGH.\n"); }
        dumpReason = 'r'; // Allowed to *r*ide through a brief dip into OK.
        isDumping = true;
        }
    // Avoid dumping if house is exporting to grid
    // and implicitly from previous cases:
    //   * storage is not full
    //   * the grid is not at peak intensity/demand.
    //
    // This allows storage to continue charging if the grid is not desperate.
    //
    // Avoid dumping if exporting when any of:
    //   * the battery is not at least HIGH (and most dumping is suppressed)
    //   * the battery has not been at least VHIGH for a while (>> ~1d)
    // Only block low-priority dumping with this.
    // 
    // To save time, and spurious activations, there is an extra 'sanity'
    // guard for this, that there must be significant grid-tie generation.
    //
    // After 'r' ride-through to prevent drop-outs such as:
//2021/04/05T16:10:06Z AL 1404 B1 14094 B2 -1 P 14644 BV 13758 ST F D F A1P 19674 B1T 12 UC 100 A1V 33085
//2021/04/05T16:20:06Z AL 1595 B1 14060 B2 -1 P 15930 BV 13776 ST F D F A1P 22316 B1T 12 UC 100 A1V 30271
//2021/04/05T16:30:06Z AL 805 B1 13413 B2 -1 P 14688 BV 13109 ST OK - s A1P 8939 B1T 12 UC 100 A1V 28574
//2021/04/05T16:40:06Z AL 1088 B1 14094 B2 -1 P 3214 BV 13995 ST F - t A1P 876 B1T 12 UC 100 A1V 33069
//2021/04/05T16:50:06Z AL 431 B1 14090 B2 -1 P 2424 BV 13934 ST F - t A1P 6734 B1T 12 UC 100 A1V 31934
//2021/04/05T17:00:06Z AL 764 B1 14090 B2 -1 P 3269 BV 13951 ST F - t A1P 10675 B1T 12 UC 100 A1V 29517
//2021/04/05T17:10:06Z AL 677 B1 13965 B2 -1 P 3073 BV 13793 ST F D F A1P 9407 B1T 12 UC 100 A1V 27594
//2021/04/05T17:20:06Z AL 595 B1 13199 B2 -1 P 12988 BV 12942 ST OK D T A1P 7837 B
    else if(LESS_DoD &&
            (muchMoreThanDaySinceVHIGH ||
                (isNOTHIGH && suppressMostDumping)) &&
            significantGridTieGeneration &&
            isExternalFlagSet(exportFlag))
        {
        // Note 1: MUST allow dumping <= HIGH to allow dump on float when full,
        //     at least in normal/stable circumstances.
        // Note 2: exportFlag must be real-time and similar/faster cadence
        //     than this runs; must NOT be present if comms to device
        //     measuring export/import is lost.
        // Note 3: small timing errors should be buffered by the AC battery,
        //     ie if this stops dumping and then export stops, the AC battery
        //     should be capable of covering the load until this next runs.
        if(verbose) { fprintf(stderr, "Do not dump when *S*oC ~%d%% (not full) and house is exporting to grid.\n", int(B1UsableSoC)); }
        dumpReason = 's'; // Exporting with *S*oC too low.
        isDumping = false;
        }
    // Unless conserving charge in winter...
    // If HIGH and was dumping, carry on dumping.
    // This helps ride through brief dips in sunshine.
    else if(!suppressMostDumping &&
            (wasDumping && !isNOTHIGH))
        {
        if(verbose) { fprintf(stderr, "Keep dumping while HIGH.\n"); }
        dumpReason = 'h'; // Keep dumping while *h*igh.
        isDumping = true;
        }
    // When LESS_DoD then only consider lower-priority external factors
    // if not many days since last FULL (eg in depths of winter)
    // and if battery has received a good recent charge to absorption/better,
    // and usable SoC is reasonably high.  Only expend ~10% of battery
    // capacity on non-urgent dump loads.
    //
    // The primary motivation for this is to try to ensure that the battery
    // has a chance to reach FULL periodically, even mid-winter.
    //
    // On the other hand, if the battery is getting very nearly full
    // allow some to bleed off to help the grid.
    else if(LESS_DoD &&
             (manyDaysSinceFULL ||
                (muchMoreThanDaySinceVHIGH &&
                    (B1UsableSoC < higherUsableSoCToDumpThrPC))))
        {
        if(verbose) { fprintf(stderr, "Low-priority dumping NOT permitted, usable SoC ~%d%% (min %d%%).\n", int(B1UsableSoC), int(minUsableSoCToDumpThrPC)); }
        dumpReason = 'N'; // *N*ot high enough priority dump given SoC.
        isDumping = false;
        }
    // Take dump/optional load off grid if:
    //    * the house is significantly importing from grid
    //
    // This case can only be reached if there is adequate SoC and margin
    // to allow dumping.
    else
        {
        if(verbose) { fprintf(stderr, "Dumping driven by external factors.\n"); }
        dumpReason = 'e'; // Dump for *e*xternal factors.
        isDumping = isExternalFlagSet(importFlag);
        //    * the battery is very nearly full
        //    * low microgeneration (ie dark) so as to reduce grid draw
        //      esp at night, when microgen not able to cover house load
        //    * if not in minimum demand/intensity (green or very green)
        //      and the battery is fairly/near full
        //isDumping = (
        //    (B1UsableSoC > 95) ||
        //    (!significantGridTieGeneration) ||
        //    ((B1UsableSoC >= higherUsableSoCToDumpThrPC) &&
        //        isExternalFlagSet(gridNotGreenFlag)) ||
        //    ((B1UsableSoC >= 90) &&
        //        isExternalFlagSet(gridNotVeryGreenFlag))
        //    );
        }
    // Set flag (and converse, for tracking).
    doFlag(FLAGIO_DUMPING, isDumping);
    doFlag(FLAGIO_DUMPINGEND, !isDumping);
    // Set GPIO for dump control.
    setDumpingGPIO(isDumping);

    // Prepare the log output (if logging or verbose).
    struct tm tm;
    constexpr int MAX_LOG_LINE = 256;
    char logLineBuf[MAX_LOG_LINE];
    logLineBuf[0] = '\0'; // Ensure terminated.
    if(!nolog || verbose || looping)
        {
        // Extra values just for logging.
        uint16_t Power_out;
        int32_t ArrayPower1mW = -1;
        // Optimisation: saves fetching/computing array power at night.
        if(0 == chargemA) { ArrayPower1mW = 0; }
        // Get array power.
        else if(getMODBUSReg(SS_REG_Power_out, Power_out))
            {
            ArrayPower1mW = fMODBUSTmW32(Power_out);
            if(verbose) { fprintf(stderr, "SS-MPPT-15L Power_out A1P (mW) %ld.\n", (long)ArrayPower1mW); }
            }
        // Get array voltage.
        uint16_t Adc_va_f;
        int32_t ArrayVoltage1mV = -1;
        if(getMODBUSReg(SS_REG_Adc_va_f, Adc_va_f))
            {
            ArrayVoltage1mV = fMODBUSTmV(Adc_va_f);
            if(verbose) { fprintf(stdout, "SS-MPPT-15L Adc_va_f array voltage (mV): %d\n", int(ArrayVoltage1mV)); }
            }

        char tmbuf[64];
        tmbuf[0] = '\0'; // Ensure terminated.
        if(gmtime_r(&measurementTime.tv_sec, &tm) != NULL)
            { strftime(tmbuf, sizeof(tmbuf), "%Y/%m/%dT%H:%M:%SZ", &tm); }
        // Length-limited log-line generation, space-separated fields.
        // Each field value (or fixed-length group of fields)
        // is preceded with a field (group) name.
        // AL ambient light uncalibrated; higher is brighter.
        snprintf(logLineBuf, MAX_LOG_LINE,
            "%s AL %d B1 %d B2 -1 P %ld BV %d ST %s %c %c A1P %ld B1T %d UC %d A1V %d\n",
            tmbuf,
            ldr,
            batt1mV, /* batt2mV, */
            (long)powermW, busVoltage,
            bsfl, (isDumping ? 'D' : '-'), dumpReason,
            long(ArrayPower1mW),
            s_T_batt,
            int(B1UsableSoC),
            int(ArrayVoltage1mV));
        if(verbose) { fprintf(stderr, "%s: %s", FLAGO_LASTDATA, logLineBuf); }
        // If looping but not verbose print full status line as-if to log.
        // Log to stdout, not stderr.
        else if(looping) { fprintf(stdout, "%s", logLineBuf); }
        }
    // Write 'flag' and logs unless inhibited.
    if(!nolog)
        {
        char buf[FLAG_BUF_SIZE];
        strcpy(buf, FLAG_DIR);
        strcat(buf, "/");
        strcat(buf, FLAGO_LASTDATA);
        strcat(buf, ".flag");
        assert(strlen(buf) < sizeof(buf));
        const int ldfd = open(buf, O_CREAT | O_WRONLY | O_TRUNC, 0644);
        const int lLBLen = (int) strlen(logLineBuf);
        if((ldfd < 0) ||
           (write(ldfd, logLineBuf, lLBLen) != lLBLen) ||
           (0 != close(ldfd)))
            { fprintf(stderr, "Cannot write %s data flag.\n", buf); }
        if(0 == access(LOG_DIR, W_OK))
            {
            char tmbuf[9];
            tmbuf[0] = '\0'; // Ensure terminated.
            if(gmtime_r(&measurementTime.tv_sec, &tm) != NULL)
                { strftime(tmbuf, sizeof(tmbuf), "%Y%m%d", &tm); }
            char lfnbuf[256];
            snprintf(lfnbuf, sizeof(lfnbuf), "%s/%s.log", LOG_DIR, tmbuf);
            const int lffd = open(lfnbuf, O_CREAT | O_WRONLY | O_APPEND, 0644);
            if((lffd < 0) ||
               (write(lffd, logLineBuf, lLBLen) != lLBLen) ||
               (0 != close(lffd)))
                { fprintf(stderr, "Cannot write to log file %s.\n", lfnbuf); }
            }
        else if(verbose)
            { fprintf(stderr, "Cannot write/open log dir %s.\n", LOG_DIR); }
        }

    close(fd);
    return(0);
    }

static constexpr uint8_t SLEEP_TIME_S = 5;

int main (const int argc, char *const argv [])
    {
    int c;
    while((c = getopt(argc,argv,"hlnsv")) >= 0)
        {
        switch(c)
            {
            case 'h':
            case '?':
                {
                fprintf(stderr, "Version %s\n", Id);
                fprintf(stderr, "%s [options]\n", argv[0]);
                fprintf(stderr, " -h this help\n");
                fprintf(stderr, " -l looping for performance tuning\n");
                fprintf(stderr, " -n no-log / no-set-flags\n");
                fprintf(stderr, " -s sleep before power measurement\n");
                fprintf(stderr, " -v verbose\n");
                break;
                }

            case 'l':
                {
                looping = true;
                if(verbose) { fprintf(stderr, "Looping mode; ^C to exit.\n"); }
                break;
                }

            case 'n':
                {
                nolog = true;
                nosetflags = true;
                if(verbose) { fprintf(stderr, "No-log / no-set-flags mode.\n"); }
                break;
                }

            case 's':
                {
                sleepfirst = true;
                if(verbose) { fprintf(stderr, "Sleep before power measurement.\n"); }
                break;
                }

            case 'v':
                {
                verbose = true;
                fprintf(stderr, "Verbose mode (%s).\n", Id);
                break;
                }

            }
        }

    int exitCode = 1;
    do  {
        // Allow system to settle if required.
        if(sleepfirst) { sleep(SLEEP_TIME_S); }

        // ACQUIRE MUTEX
        lockAcquire(looping);

        // Take the data samples.
        exitCode = doWork();

        // If looping ensure that any output is flushed.
        if(looping) { fflush(stdout); }

        // RELEASE MUTEX
        lockRelease();

        if(looping && !sleepfirst) { sleep(SLEEP_TIME_S); }
        } while(looping);

    return(exitCode);
    }

/* Example output 2016/06/01: dump margin threshold 12699mV is good.
Verbose mode ($Id: powermng.cpp 67070 2026-04-08 09:03:21Z dhd $).
Input flag DUMPING is unset.
Input flag FORECAST_PV_GEN_GOOD is unset.
External flag file /var/log/SunnyBeam/DARK.flag is absent.
ADC command 6a 80
ADC (6a 0) response 1342 (5 3e 0)
Battery voltage 1 (LA) 12591mV, 2 (Li) -1mV.
Battery voltage 1 'very low' threshold 12449mV.
Flag EXTERNAL_BATTERY_VLOW is unset.
Battery voltage 1 'low' threshold 12649mV.
Flag EXTERNAL_BATTERY_LOW is set.
Battery voltage 1 dump margin threshold 12699mV.
Battery voltage 1 'not high' threshold 13500mV.
Flag EXTERNAL_BATTERY_NOTHIGH is set.
Flag EXTERNAL_BATTERY_HIGH is unset.
Battery voltage 1 'very high' threshold 13600mV.
Flag EXTERNAL_BATTERY_VHIGH is unset.
Setting up_threshold to 95%
Flag DUMPING is unset.
Flag DUMPINGEND is set.
LASTDATA: 2016/06/01T11:37:40Z AL -1 B1 12591 B2 -1 P -1 BV -1 ST L - L
*/

/* Example output 2016/09/11: checking supply impedance to RPi.
Verbose mode ($Id: powermng.cpp 67070 2026-04-08 09:03:21Z dhd $).
SS-MPPT-15L Vb_f slow battery voltage (mV): 12473
SS-MPPT-15L Adc_il_f load current (mA): 1472
Load power (mW): 18361
Input flag DUMPING is unset.
SS-MPPT-15L Adc_ic_f battery charge current (mA): 658
External flag file /var/log/SunnyBeam/LOW.flag is absent.
ADC command 6a 80
ADC (6a 0) response 1307 (5 1b 0)
Battery voltage 1 local (LA) 12262mV, 2 (Li) -1mV.
Supply drop B1 to BV 211mV; implied supply impedance 0.143342ohm.
Adc_vl_f load voltage (mV): 12397
Supply drop controller to BV 135mV; implied wiring impedance 0.091712ohm.
Input flag EXTERNAL_BATTERY_NOTVHIGH is set.
Long time since VHIGH (or system not been up long enough to be sure).
Battery voltage 1 'very low' threshold 12224mV.
Flag EXTERNAL_BATTERY_VLOW is unset.
Battery voltage 1 'low' threshold 12424mV.
Flag EXTERNAL_BATTERY_LOW is unset.
Battery voltage 1 dump margin threshold 12474mV.
Battery voltage 1 'not high' threshold 13475mV.
Flag EXTERNAL_BATTERY_NOTHIGH is set.
Flag EXTERNAL_BATTERY_HIGH is unset.
Battery voltage 1 'very high' threshold 13599mV.
Flag EXTERNAL_BATTERY_VHIGH is unset.
Flag EXTERNAL_BATTERY_NOTVHIGH is set.
Flag EXTERNAL_BATTERY_NOTFULL is set.
Flag EXTERNAL_BATTERY_FULL is unset.
Setting up_threshold to 60%
Input flag NODUMP is unset.
Insufficient margin to dump, threshold 12474.
Flag DUMPING is unset.
Flag DUMPINGEND is set.
SS-MPPT-15L Power_out A1P (mW) 8214.
LASTDATA: 2016/09/11T08:40:31Z AL 658 B1 12473 B2 -1 P 18361 BV 12262 ST OK - m A1P 8214
*/

/* Example output 2016/10/01
Verbose mode ($Id: powermng.cpp 67070 2026-04-08 09:03:21Z dhd $).
SS-MPPT-15L Vb_f slow battery voltage (mV): 12427
SS-MPPT-15L Adc_il_f load current (mA): 1315
Load power (mW): 16342
Input flag DUMPING is set.
System dumping for 2841m.
SS-MPPT-15L Adc_ic_f battery charge current (mA): 633
External flag file /var/log/SunnyBeam/LOW.flag is absent.
SS-MPPT-15L T_batt (C): 16, discharge compensation -96mV.
LA/B1 dynamic threshold adjustment for load current -61mV.
ADC command 6a 80
ADC (6a 0) response 1300 (5 14 0)
Battery voltage 1 local (LA) 12197mV.
Supply drop B1 to BV 230mV; implied supply impedance 174mohm.
Adc_vl_f load voltage (mV): 12323
Supply drop B1 to controller load terminals 104mV; implied controller impedance
79mohm.
Supply drop controller to BV 126mV; implied wiring impedance 95mohm.
Input flag EXTERNAL_BATTERY_NOTVHIGH is set.
Input flag FORECAST_PV_GEN_GOOD is set.
Battery voltage 1 'very low' threshold 12100mV.
Flag EXTERNAL_BATTERY_VLOW is unset.
Battery voltage 1 'low' threshold 12143mV.
Flag EXTERNAL_BATTERY_LOW is unset.
Battery voltage 1 dump margin threshold 12193mV.
Battery voltage 1 'not high' threshold 13389mV.
Flag EXTERNAL_BATTERY_NOTHIGH is set.
Flag EXTERNAL_BATTERY_HIGH is unset.
Battery voltage 1 'very high' threshold 13600mV.
Flag EXTERNAL_BATTERY_NOTVHIGH is set.
Flag EXTERNAL_BATTERY_VHIGH is unset.
Flag EXTERNAL_BATTERY_NOTFULL is set.
Flag EXTERNAL_BATTERY_FULL is unset.
Setting up_threshold to 60%
Input flag NODUMP is unset.
External flag file
/rw/docs-public/www.hd.org/Damon/Env/_gridCarbonIntensityGB.flag is present.
Dumping driven by external factors.
Flag DUMPING is set.
Flag DUMPINGEND is unset.
SS-MPPT-15L Power_out A1P (mW) 7822.
LASTDATA: 2016/10/01T08:41:46Z AL 633 B1 12427 B2 -1 P 16342 BV 12197 ST OK D e A1P 7822 B1T 16
*/

/* Example output 2016/10/14 under heavy evening load (laptop+phone) charging.
Verbose mode ($Id: powermng.cpp 67070 2026-04-08 09:03:21Z dhd $).
SS-MPPT-15L Vb_f slow battery voltage (mV): 12281
SS-MPPT-15L Adc_il_f load current (mA): 5781
Load power (mW): 70997
Input flag DUMPING is set.
System dumping for 475m.
SS-MPPT-15L Adc_ic_f battery charge current (mA): 0
SS-MPPT-15L T_batt (C): 13, discharge compensation -210mV.
LA/B1 dynamic threshold adjustment for load current -300mV.
ADC command 6a 80
ADC (6a 0) response 1246 (4 de 0)
Battery voltage 1 local (LA) 11690mV.
Supply drop B1 to BV 591mV; implied supply impedance 102mohm.
Adc_vl_f load voltage (mV): 12208
Supply drop B1 to controller load terminals 73mV; implied controller impedance 12mohm.
Supply drop controller to BV 518mV; implied wiring impedance 89mohm.
Input flag EXTERNAL_BATTERY_NOTVHIGH is set.
Long time since VHIGH (or system not been up long enough to be sure).
Battery voltage 1 'very low' threshold 12100mV.
Flag EXTERNAL_BATTERY_VLOW is unset.
Battery voltage 1 'low' threshold 12100mV.
Flag EXTERNAL_BATTERY_LOW is unset.
Battery voltage 1 dump margin threshold 12150mV.
Battery voltage 1 'not high' threshold 13175mV.
Flag EXTERNAL_BATTERY_NOTHIGH is set.
Flag EXTERNAL_BATTERY_HIGH is unset.
Battery voltage 1 'very high' threshold 13600mV.
Flag EXTERNAL_BATTERY_NOTVHIGH is set.
Flag EXTERNAL_BATTERY_VHIGH is unset.
Flag EXTERNAL_BATTERY_NOTFULL is set.
Flag EXTERNAL_BATTERY_FULL is unset.
Setting up_threshold to 60%
Input flag NODUMP is unset.
Dumping driven by external factors.
Flag DUMPING is set.
Flag DUMPINGEND is unset.
LASTDATA: 2016/10/14T17:15:54Z AL 0 B1 12281 B2 -1 P 70997 BV 11690 ST OK D e A1P 0 B1T 13
*/

/* Example output 2021-04-02T10:07Z.
Verbose mode ($Id: powermng.cpp 67070 2026-04-08 09:03:21Z dhd $).
Sleep before power measurement.
ISEXPPI=1: Expander Pi is in use.
LOAD POWER (mW): 16807
SS-MPPT-15L Vb_f slow battery voltage (mV): 13776
SS-MPPT-15L Adc_il_f load current (mA): 1220
Input flag DUMPING is set.
System dumping for 176m.
SS-MPPT-15L Adc_ic_f battery charge current (mA): 3513
External flag file /var/log/SunnyBeam/LOW.flag is absent.
NET battery charge current (mA): 2293
SS-MPPT-15L T_batt (C): 12, temperature compensation 120mV.
Estimated LA battery impedance: 101mohm.
Estimated LA battery sag/(-rise) from internal impedance, and nominal internal voltage, effective w/ temp comp: 231mV, 13545mV, 13425mV.
Estimated actual battery sag from load (mV): 0mV.
LA/B1 dynamic threshold adjustment for load current 0mV.
Battery voltage 1 local (LA) 13468mV.
Supply drop B1 to BV 308mV; implied supply impedance 252mohm.
Adc_vl_f load voltage (mV): 13740
Supply drop B1 to controller load terminals 36mV; implied controller impedance 29mohm.
Supply drop controller to BV 272mV; implied wiring impedance 222mohm.
Input flag EXTERNAL_BATTERY_NOTVHIGH is set.
Input flag EXTERNAL_BATTERY_NOTFULL is set.
Battery voltage 1 basic compensation 120mV.
Battery voltage 1 'very low' threshold 12320mV (raw 12200mV).
Battery voltage 1 'low' threshold 12570mV (raw 12450mV).
Battery voltage 1 dump margin threshold 12570mV (B1 delta 150mV, sag for dump load 130mV).
Battery voltage 1 'not high' threshold 13450mV (raw 13450mV).
Charge state 5.
Battery voltage 1 'very high' raw threshold 13600mV.
Input flag FORECAST_PV_GEN_GOOD is unset.
Battery extra USoC reserve for poor forecast / VHIGH and FULL not recent 5%.
Battery 1 effective/usable SoC 100%, dump thresholds min 60% higher 80% (uncompensated USoC 100%); usable to dump = 1.
Input flag NODUMP is unset.
External flag file /rw/docs-public/www.hd.org/Damon/Env/_gridCarbonIntensityGB.red.flag is absent.
Input flag GBGRIDPEAK is unset.
Keep dumping while HIGH.
SS-MPPT-15L Power_out A1P (mW) 48210.
SS-MPPT-15L Adc_va_f array voltage (mV): 29841
LASTDATA: 2021/04/02T10:06:08Z AL 3513 B1 13776 B2 -1 P 16807 BV 13468 ST H D h A1P 48210 B1T 12 UC 100 A1V 29841
*/

/* Example output 2022-11-20T12:36Z.
% ./powermng -nv
Verbose mode ($Id: powermng.cpp 67070 2026-04-08 09:03:21Z dhd $).
ISEXPPI=1: Expander Pi is in use.
LOAD POWER (mW): 52212
SS-MPPT-15L Vb_f slow battery voltage (mV): 14463
SS-MPPT-15L Adc_il_f load current (mA): 3610
Input flag DUMPING is set.
System dumping for 46m.
SS-MPPT-15L Adc_ic_f battery charge current (mA): 4257
External flag file /var/log/SunnyBeam/LOW.flag is absent.
NET battery charge current (mA): 647
SS-MPPT-15L T_batt (C): 10, temperature compensation 150mV.
Estimated LA battery impedance: 105mohm.
Estimated LA battery sag/(-rise) from internal impedance, and nominal internal
voltage, effective w/ temp comp: 67mV, 14396mV, 14246mV.
Estimated actual battery sag from load (mV): 0mV.
LA/B1 dynamic threshold adjustment for load current 0mV.
Battery voltage 1 local (LA) 13758mV.
Supply drop B1 to BV 705mV; implied supply impedance 195mohm.
Adc_vl_f load voltage (mV): 14506
Supply drop B1 to controller load terminals -43mV; implied controller impedance
-11mohm.
Supply drop controller to BV 748mV; implied wiring impedance 207mohm.
Input flag EXTERNAL_BATTERY_NOTVHIGH is unset.
Input flag EXTERNAL_BATTERY_NOTFULL is set.
Long time since FULL >45.0d (or system not been up long enough to be sure).
Battery voltage 1 basic compensation 150mV.
Battery voltage 1 'very low' threshold 12350mV (raw 12200mV).
Battery voltage 1 'low' threshold 12600mV (raw 12450mV).
Battery voltage 1 dump margin threshold 12600mV (B1 delta 150mV, sag for dump
load 136mV).
Battery voltage 1 'not high' threshold 13450mV (raw 13450mV).
Charge state 6.
Battery voltage 1 'very high' raw threshold 13600mV.
Input flag FORECAST_PV_GEN_GOOD is unset.
Battery extra USoC reserve 15% for poor forecast and/or VHIGH / FULL not
recent.
Battery 1 effective/usable SoC 100%, dump thresholds min 65% higher 82%
(uncompensated USoC 100%); usable to dump = 1.
Input flag NODUMP is unset.
SS-MPPT-15L Power_out A1P (mW) 61104.
SS-MPPT-15L Adc_va_f array voltage (mV): 33982
LASTDATA: 2022/11/20T12:36:47Z AL 4257 B1 14463 B2 -1 P 52212 BV 13758 ST VH D
V A1P 61104 B1T 10 UC 100 A1V 33982
*/
