#!/bin/sh
# Computes every few minutes heat-battery target top-up limit % from grid.
# A higher top-up limit also requests more eager and faster top-up.
# Note: at each of 2%/25%/50%/75% target fill, top-up becomes more aggressive.
# At 0% target fill no top-up will happen, even if 'level' becomes negative.


##########
# May be used / adapted / etc without any promise of fitness for purpose
# under the terms of the Apache License Version 2.0, January 2004
#     http://www.apache.org/licenses/LICENSE-2.0
##########


# Usage:
#     $0 [-dryrun] [-policy MAINDHW AGGRESSIVECARBON] [-dirs CONTROLPARAMSDIR GRIDINTENSITYDIR ELOGDIR DLOGDIR LOGDIR HTMLDIR] [-datetime YYYY-MM-ddTHH:mmZ]
#
# With -dryrun no file outputs are written,
# and the log record is echoed to stdout.
# All other INFO/WARNING/ERROR should be written to stderr.

# Atomically updates simple text file with % max grid top-up (or 0 for none).
# Writes a log of computed values and results.
# Atomically updates simple HTML file to stdout with the computation/stats.

# RUN script/test/heat-battery-target-tests.sh AFTER ANY SIGNIFICANT CHANGES.

# THIS LOGIC IS DRIVEN BY CARBON, FOR COMBI + HEAT-BATTERY + PV.
# Logic for eg heat-battery only or ToU-pricing would be simple to swap in.

# See NOTES, TODO and changelog at the end of the file.

dryrun="false"
if [ "-dryrun" = "$1" ]; then
    dryrun="true"
    echo INFO: dryrun 1>&2
    shift
fi

# If true, grid boost is disabled.  Usually false.
# Stats will continue to be generated.
#BOOSTDISABLED="false"
BOOSTDISABLED="true"

# If true, this is the main DHW source, not a backup.
# DHD20220923: combi is acting up, so have turned it off and this "true".
# DHD20221004: combi fixed again...
# DHD20231213: combi is acting up, so combi is off and this is "true"...
# DHD20240103: combi is fixed.
POLICYMAINDHW="false"
# If true, be more aggressive about carbon saving, even if it costs money.
POLICYAGGRESSIVECARBON="true"
# Override from command-line if requested.
#     [-policy MAINDHW AGGRESSIVECARBON
if [ "-policy" = "$1" ]; then
    if [ "true" = "$2" ]; then POLICYMAINDHW="true"; else POLICYMAINDHW="false"; fi
    if [ "true" = "$3" ]; then POLICYAGGRESSIVECARBON="true"; else POLICYAGGRESSIVECARBON="false"; fi
    echo INFO: policies: POLICYMAINDHW=$POLICYMAINDHW POLICYAGGRESSIVECARBON=$POLICYAGGRESSIVECARBON 1>&2
    shift
    shift
    shift
fi

# Soft parameters for energy management.
SOFTP=data/consolidated/softparams.txt
. ${SOFTP}

# Storage CoP threshold: default to 300.
STORECOPPC="${SOFTPMINSTORAGECOPPC-300}"

# Directories for key I/O, that can be overridden on command-line eg for test.
# Control parameters.
CONTROLPARAMSDIR=data/heatBattery/16WWDHW
# Live computed grid intensity directory.
GRIDINTENSITYDIR=.
# Enphase AC Battery log directory.
ELOGDIR=/var/log/Enphase/
# Eddi diverter live log directory.
DLOGDIR=data/eddi/log/live
# Output live log directory.
LOGDIR=data/heatBattery/log/live
# Output HTML/txt file directory.
HTMLDIR=.
# Override from command-line if requested.
#    [-dirs CONTROLPARAMSDIR GRIDINTENSITYDIR ELOGDIR LOGDIR HTMLDIR]
if [ "-dirs" = "$1" ]; then
    CONTROLPARAMSDIR="$2"
    GRIDINTENSITYDIR="$3"
    ELOGDIR="$4"
    DLOGDIR="$5"
    LOGDIR="$6"
    HTMLDIR="$7"
    shift
    shift
    shift
    shift
    shift
    shift
    shift
fi
if [ ! -d "$CONTROLPARAMSDIR" ]; then
    echo "ERROR: no CONTROLPARAMSDIR $CONTROLPARAMSDIR" 1>&2
    exit 1
fi
if [ ! -d "$GRIDINTENSITYDIR" ]; then
    echo "WARNING: no (input) GRIDINTENSITYDIR $GRIDINTENSITYDIR" 1>&2
    exit 1
fi
if [ ! -d "$ELOGDIR" ]; then
    echo "WARNING: no (input) ELOGDIR $ELOGDIR" 1>&2
fi
if [ ! -d "$LOGDIR" ]; then
    echo "ERROR: no LOGDIR $LOGDIR" 1>&2
    exit 1
fi
if [ ! -d "$HTMLDIR" ]; then
    echo "ERROR: no HTMLDIR $HTMLDIR" 1>&2
    exit 1
fi

# Output .html/.txt files.
OUTFILEBASE=_heat-battery-target
# Output HTML file.
HTMLOUTFILE=${HTMLDIR}/${OUTFILEBASE}.html
# Output text file (max % to top-up to from grid).
TXTOUTFILE=${HTMLDIR}/${OUTFILEBASE}.txt

# Compute UTC YYYYMMDD date to match data logging.
UTCDATE="$(date -u +%Y%m%d)"
ISOUTCDATETIME="$(date -u +%Y-%m-%dT%H:%MZ)"
# Numeric month (UTC): no leading 0 when < 10.
MONTH="$(echo "$ISOUTCDATETIME" | awk -F- '{print int(0+$2)}')"
# Today's log file.
LOGFILE="${LOGDIR}/${UTCDATE}.log"
# Yesterday in same format as UTCDATE (if easy to compute), else blank.
#YESTERDAY="$(date -u --date yesterday +%Y%m%d)"
# Yesterday's log file, if it exists, else blank.
#YLOGFILE=""
#POSSIBLEYLF=${LOGDIR}/${YESTERDAY}.log
#if [ -e ${POSSIBLEYLF} ]; then YLOGFILE=${POSSIBLEYLF}; fi
# Local timezone.
LZONE="Europe/London"
# Local time (with DST etc) hour of day for 16WW.
LHOUR="$(TZ="$LZONE"; export TZ; date +%H)"
# Next hour local time HH, and deal with wrapping.
LHOURNEXT="$(echo "$LHOUR" | awk '{if($1>22){print "00"}else{nh=$1+1;nhs=""nh;if(length(nhs)<2){nhs="0"nhs}print nhs}}')"
# Local time minutes (yes, just in case on non-hour timezone!)...
LMINS="$(TZ="$LZONE"; export TZ; date "+%M")"

# Our set of single-letter output flags, possibly empty (shown as "-").
# Order is not important.
OUTPUTFLAGS=""
# For meanings of flags see end of file.

# WINTER==true for months with lowest solar generation and highest heat demand.
WINTER="false"
if [ "$MONTH" -gt 10 ] || [ "$MONTH" -lt 3 ]; then
    WINTER="true"
    OUTPUTFLAGS="${OUTPUTFLAGS}W"
fi

# Directory for system energy-level flags in.
FLAGDIR=/run
# Forecast tomorrow sunny flag.
FORECASTGOOD=${FLAGDIR}/FORECAST_PV_GEN_GOOD.flag
if [ -e "$FORECASTGOOD" ]; then
    OUTPUTFLAGS="${OUTPUTFLAGS}F"
fi

# Get live computed grid intensity.
# Should be relatively up to date (or sister file should be) to be believed.
GRIDINTENSITYFILE=${GRIDINTENSITYDIR}/_gridCarbonIntensityGB.txt
GRIDMEANINTENSITYFILE="${GRIDINTENSITYDIR}/_gridCarbonIntensityGB.mean.txt"
GRIDINTENSITY=""
if [ -s "$GRIDINTENSITYFILE" ]; then
    # Check that intensity has been recomputed recently (<1h), ie is not stale.
    if [ -s _gridCarbonIntensityGB.html ]; then if [ "" != "$(find _gridCarbonIntensityGB.html -mmin -60)" ]; then
        GRIDINTENSITY="$(cat "$GRIDINTENSITYFILE")"
    fi; fi
fi
# Expected grid demand level H / - / L.
# DEFAULT SCHEME:
# Lookup from local time hour of day through grid intensity top-up thresholds.
# Effectively a join.
# However, if a mean intensity value is available,
# force H to be 0, read L from the file, and have - be the mean intensity,
# capped by the L threshold.
if [ ! -s "$CONTROLPARAMSDIR/storage-charge-pref-by-hour-local-time.csv" ]; then
    echo "ERROR: missing by-hour control file" 1>&2
    exit 1
fi
#if [ ! -s "$CONTROLPARAMSDIR/intensity-threshold-by-HML.csv" ]; then
#    echo "ERROR: missing by-HML control file" 1>&2
#    exit 1
#fi
EXPGRIDDEMAND="$(awk -F, '$1=='"$LHOUR"'{l=$2;if(""==l){l="-"};print l}'\
    <"$CONTROLPARAMSDIR/storage-charge-pref-by-hour-local-time.csv")"
# The threshold below which grid intensity has to be for discretionary top-up.
MAXTUGRIDINTENS=0
# The 'L' (off-peak) intensity ceiling, or "".
# Will be "" if not following mean or in 'H' period.
LMAXTUGRIDINTENS=""
# Grid carbon intensity upper threshold to top-up heat battery from grid.
GRIDMEANINTENSITY=""
if [ ! -s "${GRIDMEANINTENSITYFILE}" ]; then
    # Use 0 if no mean intensity available.
    MAXTUGRIDINTENS=0
else
    GRIDMEANINTENSITY="$(cat "${GRIDMEANINTENSITYFILE}")"
    if [ "" = "${GRIDMEANINTENSITY}" ] || [ "${GRIDMEANINTENSITY}" -lt 0 ]; then GRIDMEANINTENSITY=0; fi
    # H => 0, else fraction of 7d mean.
    if [ "H" = "$EXPGRIDDEMAND" ]; then
        MAXTUGRIDINTENS=0
    else
        MAXTUGRIDINTENS="$(expr "${GRIDMEANINTENSITY}" \* 100 / "${STORECOPPC}")"
        LMAXTUGRIDINTENS="${MAXTUGRIDINTENS}"
    fi
fi
# Be extra cautious going into high-demand ('H') times.
if [ "-" != "$EXPGRIDDEMAND" ]; then
    OUTPUTFLAGS="${OUTPUTFLAGS}${EXPGRIDDEMAND}"
fi
# Next hour demand/threshold and if threshold is about to fall.
MAXTUGRIDINTENSABOUTTOFALL="false"
NEXPGRIDDEMAND=""
NMAXTUGRIDINTENS=""
# Assumes that this script is called several (>=4) times per hour.
if [ "$LMINS" -ge 40 ]; then 
    # Extra processing in last part of each hour.
    NEXPGRIDDEMAND=`awk -F, '$1=='$LHOURNEXT'{l=$2;if(""==l){l="-"};print l}'\
        <$CONTROLPARAMSDIR/storage-charge-pref-by-hour-local-time.csv`
    if [ "" = "$NEXPGRIDDEMAND" ]; then NEXPGRIDDEMAND="-"; fi
    NMAXTUGRIDINTENS=`awk -F, '$1=="'$NEXPGRIDDEMAND'"{l=$2;if(""==l){l=0};print l}'\
        <$CONTROLPARAMSDIR/intensity-threshold-by-HML.csv`
    if [ 0 -ge "${NMAXTUGRIDINTENS:-1}" ]; then
        # If next hour contains no-top-up high-demand period,
        # then stop any top-up well before.
        MAXTUGRIDINTENS=0;
    elif [ "${MAXTUGRIDINTENS:-0}" -gt "${NMAXTUGRIDINTENS:-1}" ]; then
        MAXTUGRIDINTENSABOUTTOFALL="true"
    fi
fi
GRIDINTENSITYLOWENOUGH="false"
GRIDSUPERGREEN="false"
GRIDRED="false"
if [ "" != "$GRIDINTENSITY" ]; then
    # DHD20221006: switching to 7d version of red/supergreen flags.
    if [ -e _gridCarbonIntensityGB.7d.red.flag ]; then
        GRIDRED="true"
        OUTPUTFLAGS="${OUTPUTFLAGS}R"
        # DHD20221005: push threshold down significantly (~10%) when red.
        MAXTUGRIDINTENS="`expr \( $MAXTUGRIDINTENS \* 9 \) / 10`"
    elif [ ! -e _gridCarbonIntensityGB.7d.supergreen.flag ]; then
        GRIDSUPERGREEN="true"
        OUTPUTFLAGS="${OUTPUTFLAGS}G"
    fi
    # Computed intensity must be *below* threshold.
    # Thus threshold of 0 means no top-up.
    if [ "$GRIDINTENSITY" -lt "$MAXTUGRIDINTENS" ]; then
        GRIDINTENSITYLOWENOUGH="true";
    fi
elif [ "true" != "$POLICYMAINDHW" ]; then
    # If heat battery is NOT the primary DHW source
    # then assume grid intensity may be high/red if unknown.
    GRIDRED="true"
    OUTPUTFLAGS="${OUTPUTFLAGS}r"
fi


# Check if filling storage now would have a high effective CoP.
# Only if grid neither "H" (predictd high demand) nor "R" (high intensity).
GRIDINTBIGFALL="false"
PREDICTEDINTRISE="false"
if [ "H" != "${EXPGRIDDEMAND}" ] && [ "true" != "${GRIDRED}" ]; then
    # Check for fall in intensity so storing now a good CoP.
    # See if enough below historic 7d mean.
    if [ "" != "$GRIDINTENSITY" ] && [ "" != "$GRIDMEANINTENSITY" ]; then
        LAGCOPSTORE="$(awk -v GRIDINTENSITY="$GRIDINTENSITY" -vGRIDMEANINTENSITY="$GRIDMEANINTENSITY" 'BEGIN{ printf("%d", int(100 * GRIDMEANINTENSITY / GRIDINTENSITY)); }')"
        if [ "$LAGCOPSTORE" -gt "${STORECOPPC}" ]; then
            GRIDINTBIGFALL="true"
            OUTPUTFLAGS="${OUTPUTFLAGS}Q"
        else
            OUTPUTFLAGS="${OUTPUTFLAGS}q"
        fi
    fi
    # Check for coming sharp rise in intensity so storing now a good CoP.
    # (Possibly shade in tank level from 2.0 to 3.0, ie mean >>2.)
    # Use NatGrid carbon intensity prediction.
    # Do this as little as possible to be kind to the NESO and the network!
    # So only use the API when GRIDINTBIGFALL is false.
    PREDICTEDCOPSTORE="$(sh script/predicted_CoP_from_storing_now.sh)"
    if [ "" != "$PREDICTEDCOPSTORE" ]; then
        if [ "$PREDICTEDCOPSTORE" -gt "${STORECOPPC}" ]; then
            PREDICTEDINTRISE="true"
            OUTPUTFLAGS="${OUTPUTFLAGS}P"
        else
            OUTPUTFLAGS="${OUTPUTFLAGS}p"
        fi
    fi
fi
PREDICTEDINTRISEFLAG="/tmp/PREDICTEDINTRISE.flag"
# Set a flag in the filesystem if a steep rise in intensitt is preducted.
# This encourages filling up storage if a renewables drought is coming.
if [ "false" = "$dryrun" ]; then
if [ "true" = "$PREDICTEDINTRISE" ]; then
    touch "$PREDICTEDINTRISEFLAG"
else
    rm -f "$PREDICTEDINTRISEFLAG"
fi
fi


# Diverter storage log directory.
# Log files name of form: data/eddi/log/live/20220223.log
DLOGFILE=${DLOGDIR}/${UTCDATE}.log
# Log records are of the form:
#{"eddi":[{"sno":0,"dat":"23-02-2022","tim":"18:57:53","ectp2":38,"ectt1":"Internal Load","ectt2":"Grid","ectt3":"None","bsm":0,"bst":0,"cmt":254,"dst":1,"div":0,"frq":50.14,"fwv":"3200S3.048","grd":41,"pha":1,"pri":1,"sta":1,"tz":0,"vol":2457,"hpri":1,"hno":1,"ht1":"Tank 1","ht2":"Tank 2","r1a":0,"r2a":0,"rbc":0,"rbt":119,"tp1":127,"tp2":127}]}
# Primarily used to add some useful data to the log, closing the loop.
DLOGnotStale="false"
Drecord=""
Dstatus=""
Dhpri=""
DSTOPPED="false"
# DDivW: W diverted now.
DDivW=0
if [ -s "$DLOGFILE" ]; then if [ "" != "$(find $DLOGFILE -mmin -20)" ]; then
    DLOGnotStale="true"
    Drecord="$(tail -1 "$DLOGFILE")"
    DDivW="$(echo "$Drecord" | sed -n -e 's/^.*"div":\([0-9]*\)[^0-9].*$/\1/p')"
    Dstatus="$(echo "$Drecord" | sed -n -e 's/^.*"sta":\([1-6]\)[^0-9].*$/\1/p')"
    if [ "6" = "$Dstatus" ]; then
        DSTOPPED="true";
        OUTPUTFLAGS="${OUTPUTFLAGS}S"
    fi
    Dhpri="$(echo "$Drecord" | sed -n -e 's/^.*"hpri":\([12]\)[^0-9].*$/\1/p')"
fi; fi
# Today's Eddi frequency response log file (if any).
DLOGFRFILE=${DLOGDIR}/${UTCDATE}.freqResp.log


# Compute percentage of recent days where heat battery has NOT filled.
# As this approaches 0% then reduce the amount of grid top-up.
# The aim is to try to capture all available diversion.
# Read Eddi recent live logs (if any) in DLOGDIR to compute this.
# Uses one ~weekly cycle to be able to respond to seasons and weather.
#
# Maximum history (live log) age (in days) to use.
# Should be at least 7 days given typical household weekly activity cycle.
TUMODULATEMAXHIST=8
#TUMODLOGS="$(find ${DLOGDIR} -name '2???????.log' -mtime -$TUMODULATEMAXHIST | sort)"
TUMODLOGS="$(find ${DLOGDIR} -name '2???????.log' -mtime -$TUMODULATEMAXHIST \! -name ${UTCDATE}.log | sort)"
#echo INFO: TUMODLOGS "${TUMODLOGS}"
TUMODLOGCOUNT="$(echo $TUMODLOGS | wc -w)"
#echo INFO: TUMODLOGCOUNT "${TUMODLOGCOUNT}"
TUMODWEEKPLUS="false"
if [ "$TUMODLOGCOUNT" -ge 7 ]; then
    # At least a week's feedback data.
    TUMODWEEKPLUS="true"
fi
#
# Compute percentage of days that heat battery DID NOT fill [0,100].
# If this is low, then decrease the grid top-up.
# In the absence of suitable log data allow full top-up.
TUMODNOTFULLPC=100
TUMODNOTFULLCOUNT=0
TUMODFULLCOUNT=0
if [ "$TUMODLOGCOUNT" -gt 0 ]; then
    if [ "" != "$(egrep -l '"sta":[56]' $TUMODLOGS)" ]; then
        # Careful count of full days...
        for f in $TUMODLOGS;
            do
            if [ "" != "$(sh script/checkForDHWHBFull.sh<$f)" ]; then
                TUMODFULLCOUNT="$(expr 1 + "$TUMODFULLCOUNT")"
            fi
            done
    fi
    TUMODNOTFULLCOUNT="`expr $TUMODLOGCOUNT - $TUMODFULLCOUNT`"
    TUMODNOTFULLPC="`expr \( $TUMODNOTFULLCOUNT \* 100 \) / $TUMODLOGCOUNT`"
fi


# Maximum number of days between DHW tank pasteurisations.
# None are multiples of 7 to help smear demand across days of the week.
# Must be >= TUMODULATEMAXHIST.
# DHD20251215: extended from 27 to 30 to trim demand, still at least monthly..
PASTEURDAYSMAX=30
# Minimum number of days between DHW tank pasteurisations.  Must be >> 2.
# DHD20250309: PASTEURDAYSMIN was 9, now 15.
# DHD20251215: PASTEURDAYSMIN was 15, now 18, to trim demand.
PASTEURDAYSMIN=18
# If true then pasteurisation has run recently.
PASTEURRECENT="false"
# If true then pasteurisation has run very recently.
PASTEURVERYRECENT="false"
# If true then pasteurisation has run too long ago.
PASTEURANCIENT="false"
# If true then pasteurisation cycle is pending good grid conditions..
PASTEURPENDING="false"
# If true then run pasteurisation.
PASTEURRUN="false"
# Recent log files to inspect for pasterisation evidence..
PASTEURLOGS=""

# Logs must include today, to terminate pasteurisation.
PASTEURLOGSMIN="$(find ${DLOGDIR} -name '2???????.log' -mtime "-$PASTEURDAYSMIN" | sort)"
PASTEURLOGS="$PASTEURLOGSMIN"
# Recent pasteurisation?
if [ "" != "$(cat /dev/null ${PASTEURLOGSMIN} | sh script/checkForDHWHot.sh)" ]; then
    PASTEURRECENT="true"
    # Very recent pasteurisation?
    if [ "" != "$(cat /dev/null $(find ${DLOGDIR} -name '2???????.log' -mtime "-$(expr "$PASTEURDAYSMIN" / 2 + 1)" | sort) | sh script/checkForDHWHot.sh)" ]; then
        PASTEURVERYRECENT="true"
    fi
fi

# If there has been recent pasteurisation, divert to the heat battery only.
DOUTPUTPRI=1
# If no very recent pasteurisation, DHW cylinder is the diversion priority.
if "$PASTEURVERYRECENT"; then DOUTPUTPRI=2; fi
OUTPUTFLAGS="${OUTPUTFLAGS}${DOUTPUTPRI}"
# If the Eddi priority is not as required, attempt to correct it.
if [ "$DOUTPUTPRI" != "$Dhpri" ] && [ "false" = "$dryrun" ] ; then
    sh script/myenergi/eddiSetHeaterPri-netrc.sh "${DOUTPUTPRI}">/dev/null 2>&1 
fi

# Minimise work on pasteurisation outside 'L' hours.
if [ "L" = "${EXPGRIDDEMAND}" ]; then
    if "$PASTEURRECENT"; then
        : Recent pasteurisation, do nothing now.
    else
        # Prioritise PV diversion to DHW tank to pasteurise during the day!
        # Logs must include today, to terminate pasteurisation.
        PASTEURLOGSMAX="$(find ${DLOGDIR} -name '2???????.log' -mtime "-$PASTEURDAYSMAX" | sort)"
        PASTEURLOGS="$PASTEURLOGSMAX"
        # If no sign of DHW being pasteurised within max days, do it now.
        if [ "" = "$(cat /dev/null ${PASTEURLOGSMAX} | sh script/checkForDHWHot.sh)" ]; then
            PASTEURANCIENT="true"
            PASTEURRUN="true"
            OUTPUTFLAGS="${OUTPUTFLAGS}s"
# TODO: improve deferral of boost to pasteurise.
        # Else if super-green, and winter or not forecast sunny, pasteurise.
        elif [ "true" = "${GRIDSUPERGREEN}" ] && [ "true" = "$WINTER" -o ! -e "$FORECASTGOOD" ]; then
            PASTEURRUN="true"
            OUTPUTFLAGS="${OUTPUTFLAGS}s"
        # Indicate waiting to pasteurise soonish...
        else
            PASTEURPENDING="true"
            OUTPUTFLAGS="${OUTPUTFLAGS}w"
        fi
    fi
# Provide a status indication even outside L hours.
elif [ "false" = "$PASTEURRECENT" ]; then
    PASTEURPENDING="true"
    OUTPUTFLAGS="${OUTPUTFLAGS}w"
fi


# Record if heat battery is nearly full or full.
# Set flag accessible to per-minute diversion control.
HEATBATTERYNF="false"
HBNFFLAGFILE="/tmp/heat_battery_near_full.flag"
if [ "true" = "$(sh .work/script/heat_battery_near_full.sh)" ]; then
    HEATBATTERYNF="true"
    touch "$HBNFFLAGFILE"
    OUTPUTFLAGS="${OUTPUTFLAGS}T"
else
    rm -f "$HBNFFLAGFILE"
fi

# Record if heat battery is not cold (not empty).
# Set flag accessible to per-minute diversion control.
HEATBATTERYNC="false"
HBNCFLAGFILE="/tmp/heat_battery_not_cold.flag"
if [ "true" = "$(sh .work/script/heat_battery_not_cold.sh)" ]; then
    HEATBATTERYNC="true"
    touch "$HBNCFLAGFILE"
    OUTPUTFLAGS="${OUTPUTFLAGS}C"
else
    rm -f "$HBNCFLAGFILE"
fi


# TOP-UP LIMIT FROM GRID
# 50% is about 1 day's mean demand, and should be reasonably measurable.
# 25% is enough for washing hands and dishes, or a quick shower.
# Minimum amount to top up heat battery to when top-up is allowed at all.
# This may be above zero when the heat battery is the sole source of DHW.
# Should be fairly high since recovery time is several hours, eg for a shower.
# Should be much less than 100.
# A round number of % (a multiple of 5 or 10) may be helpful.
# DEFAULT TO ZERO (for when the heat battery is not the primary DHW source).
TOPUPMINPCMAX=0
# DHD20231017: enough for hand washing; forces to slowest fill (@<25).
#TOPUPMINPCMAX=24
if [ "true" = "$POLICYMAINDHW" ]; then
    # DHD20231216: enough for a bath, and so that 2/3rds at H/R times is <50%.
    TOPUPMINPCMAX=74
fi
# Maximum amount to top up heat battery to when top-up is allowed at all.
# Should be higher than TOPUPMINPCMAX.
# Should be less than or equal to 100.
# A round number of % (a multiple of 5 or 10) may be helpful.
TOPUPMAXPCMAX=50
if [ "true" = "$POLICYAGGRESSIVECARBON" ] || [ "true" = "$POLICYMAINDHW" ]; then
    #TOPUPMAXPCMAX=95
    TOPUPMAXPCMAX=100
fi


# Compute % top-up limit.
# This is the maximum percent full that the heat battery should be
# topped up from the grid at the moment.
# Zero, or a % less than the current battery content, implies no top-up.
# DEFAULT TO ZERO
TOPUPMAXPC=$TOPUPMINPCMAX


# PTOPUPMAXPC is putative top-up level whether actually used/allowed or not!
# Start at normal target ~half fill.
PTOPUPMAXPC=$TOPUPMAXPCMAX


# Downwards adjustments to max fill level.
# Top-up less if good sunshine forecast for tomorrow.
# Also if not Nov/Dec/Jan since significant diversion very unlikely.
# "Sunny" is often not enough for any significant diversion Nov/Dec/Jan.
if [ "true" != "$WINTER" -a -e $FORECASTGOOD ]; then
    # Leave less space for PV if the forecast is not 'sunny', top-up higher.
    PTOPUPMAXPC="`expr \( $TOPUPMINPCMAX + 6 \* $PTOPUPMAXPC \) / 7`"
fi
# If not 'L' grid time of day and not supergreen then cut max top-up %.
# (So at expected L demand *or* when supergreen at any time, don't cut it!)
if [ "L" != "$EXPGRIDDEMAND" -a "true" != "$GRIDSUPERGREEN" ]; then
    PTOPUPMAXPC="`expr \( $TOPUPMINPCMAX + 3 \* $PTOPUPMAXPC \) / 4`"
fi
# Reduce top-up if it seems that there is likely to be PV diversion available.
LIKELYPVFORDIVERSION="false"
# If we have at least a week's feedback data adjust top-up level...
if [ "$PTOPUPMAXPC" -gt "$TOPUPMINPCMAX" ]; then
    # Modulate downwards given how often heat battery has filled recently.
    # A full heat battery potentially means potential unused/missed diversion.
    # If full every day then no need for big grid top-up.
    # If not full any day then low-carbon grid-top should be allowed.
    if [ "$TUMODNOTFULLPC" -eq 0 -a "$PTOPUPMAXPC" -gt "$TOPUPMINPCMAX" ]; then
        # Even if full every day allow small opportunistic top-up, eg to 1%.
        PTOPUPMAXPC="`expr $TOPUPMINPCMAX + 1`"
        LIKELYPVFORDIVERSION="true"
    else
        PTOPUPMAXPC="`expr \( \( \( $PTOPUPMAXPC - $TOPUPMINPCMAX \) \* $TUMODNOTFULLPC \) / 100 \) + $TOPUPMINPCMAX`"
        if [ "$TUMODNOTFULLPC" -le 50 ]; then
            LIKELYPVFORDIVERSION="true"
        fi
    fi
fi

# Upwards adjustments to max fill level.
# Top up higher the further that live intensity is below the threshold.
# Also should help somewhat randomise 'off' time based on local DHW use.
if [ "true" = "$GRIDINTENSITYLOWENOUGH" ]; then
    # Scale up top up by as much as an extra 25% of current value.
    GIUPLIFTMAXPC=25
    GIUPLIFTMAX="`expr \( $PTOPUPMAXPC \* $GIUPLIFTMAXPC \) / 100`"
    GIUHEADROOM="`expr 100 - $PTOPUPMAXPC`"
    if [ "$GIUHEADROOM" -lt "$GIUPLIFTMAX" ]; then
        if [ "$GIUHEADROOM" -gt 0 ]; then
            GIUPLIFTMAX=$GIUHEADROOM
        else
            GIUPLIFTMAX=0
        fi
    fi
    if [ "true" = "$MAXTUGRIDINTENSABOUTTOFALL" ]; then
        # Reduce this when the threshold is about to fall.
        # This may help further randomise 'off' time.
        GIUPLIFTMAX="`expr $GIUPLIFTMAX / 2`"
    fi
    if [ "$GIUPLIFTMAX" -gt 1 ]; then
        GIUPLIFT="`expr \( $GIUPLIFTMAX \* \( $MAXTUGRIDINTENS - $GRIDINTENSITY \) \) / $MAXTUGRIDINTENS`"
        if [ "$GIUPLIFT" -gt 0 ]; then
            PTOPUPMAXPC="`expr $PTOPUPMAXPC + $GIUPLIFT`"
        fi
    fi
fi
# Force up to minimum value for this day of the week, in "L" period, if any.
DailyMinPCApplied=""
if [ "$PTOPUPMAXPC" -lt 100 ]; then
    if [ "L" = "$EXPGRIDDEMAND" ]; then
        #DOW="`date -u +%u`"
        DOW="`TZ=$LZONE; export TZ; date +%u`"
        if [ ! -s $CONTROLPARAMSDIR/min-fill-by-dow.csv ]; then
            echo "ERROR: missing by-day-of-week control file" 1>&2
            exit 1
        fi
        DOWMINLTARGET=`awk -F, '$1=='$DOW'{l=$2;print l}'\
            <$CONTROLPARAMSDIR/min-fill-by-dow.csv`
        if [ "" != "$DOWMINLTARGET" ]; then
            # There is a current d-o-w floor % target.
            DailyMinPCApplied="$DOWMINLTARGET"
            if [ "$DOWMINLTARGET" -gt "$PTOPUPMAXPC" ]; then
                PTOPUPMAXPC="$DOWMINLTARGET"
                OUTPUTFLAGS="${OUTPUTFLAGS}D"
            fi
        fi
    fi
fi

# Ensure that PTOPUPMAXPC stays in bounds.
if [ "$PTOPUPMAXPC" -lt "$TOPUPMINPCMAX" ]; then PTOPUPMAXPC=$TOPUPMINPCMAX; fi
if [ "$PTOPUPMAXPC" -gt 100 ]; then PTOPUPMAXPC=100; fi


# Allow top-up when:
#   * Grid intensity is acceptably low given time of day
#     OR this is the main DHW source and predicted demand is low
if [ \( "true" = "$GRIDINTENSITYLOWENOUGH" -o \
         \( "true" = "$POLICYMAINDHW" -a "" != "$DailyMinPCApplied" \) \
         \) ]; then 
    # Use putative top-up level.
    TOPUPMAXPC=$PTOPUPMAXPC
fi

# Allow minimal top-up when:
#    * Top-up level allowed is otherwise zero.
#    * Grid intensity is below the 'L' threshold, so carbon would be saved.
#    * Not in an 'H' hour.
if [ "$TOPUPMAXPC" -le 1 ]; then
    if [ "H" != "$EXPGRIDDEMAND" -a \
         "${GRIDINTENSITY:-999}" -lt "${LMAXTUGRIDINTENS:-190}" ]; then
        TOPUPMAXPC="1";
    fi
fi

# DEMAND REDUCTION AT PEAK TIMES
# If grid intensity red/unknown, or peak demand time, then cut max top-up %.
# Also if higher than maximum allowed 'L' intensity threshold.
# May cut limit to below the normal minimum, but not to zero.
# Attempts to reactively minimise top-ups during the grid's worst times.
# Less restrictive to top-up (eg when info missing) when POLICYMAINDHW=true.
# WHEN CLOSE to H predicted period, set back target a little.
if [ "$TOPUPMAXPC" -gt 1 ]; then
    if [ "true" = "$GRIDRED" -o \
        "H" = "$EXPGRIDDEMAND" -o \
        \( "${GRIDINTENSITY:-999}" -ge "${LMAXTUGRIDINTENS:-190}" \) ]; then
        TOPUPMAXPC="`expr \( $TOPUPMAXPC \* 2 \) / 3`"
    elif [ "H" = "$NEXPGRIDDEMAND" ]; then
        TOPUPMAXPC="`expr \( $TOPUPMAXPC \* 5 \) / 6`"
    fi
    # Avoid accidentally zeroing TOPUPMAXPC.
    if [ "$TOPUPMAXPC" -lt 1 ]; then TOPUPMAXPC=1; fi
fi

# Dither towards new TOPUPMAXPC value to defuse ramp-up if new value is higher.
# Only in the first part of the hour so will be full value by end of hour,
# OR old value was zero, so this may be ramping up after a back-off.
if [ "$TOPUPMAXPC" -gt 1 ]; then
    # Read old TOPUPMAXPC if present to help dither/randomise transition.
    OLDTOPUPMAXPC=""
    if [ -s $TXTOUTFILE ]; then
        # Canonicalise/sanitise a bit...
        OLDTOPUPMAXPC="`awk < $TXTOUTFILE '($1>=0)&&($1<=100) {print int($1)}'`"
    fi
    if [ "" != "$OLDTOPUPMAXPC" ]; then
        # Either in first 20 minutes of the hour and this is a ramp up
        # or old value was 0 so this may be recovery from voltage sag.
        if [ \( "$LMINS" -lt 20 -a "$TOPUPMAXPC" -gt "$OLDTOPUPMAXPC" \) -o \
             \( 0 -eq "$OLDTOPUPMAXPC" \) ]; then
            # Result higher than old target, up to current target.
            # Slow the ramp-up: linear gives ~2 ticks to reach full value.
            TOPUPMAXPC="`awk -v MIN=$OLDTOPUPMAXPC -v MAX=$TOPUPMAXPC 'BEGIN{srand(); print int(MIN+1+(rand()*rand())*(MAX-MIN-1));}'`"
        fi
    fi
fi

# OPPORTUNISTIC MINIMAL DHW 'KEEP-HOT' TOP-UP.
# This may also allow dynamic response to draw-down.
#
# If grid not red and not at or about to be peak time
# and there is not likely to be lots of PV available for diversion
# and grid intensity is enough below gas burn per the off-peak threshold ('L')
# and the top-up target percentage is otherwise zero
# then keep at least a minimal amount of directly-usuable DHW available
# to try to further reduce or eliminate gas use for small DHW demands
# by raising the target to 1.
if [ "true" != "$GRIDRED" -a \
     "H" != "$EXPGRIDDEMAND" -a "H" != "$NEXPGRIDDEMAND" -a \
     "" != "$LMAXTUGRIDINTENS" ]; then
    if [ "false" = "$LIKELYPVFORDIVERSION" ]; then
        if [ "$GRIDINTENSITY" -lt "$LMAXTUGRIDINTENS" ]; then
            if [ "$TOPUPMAXPC" -le 0 ]; then
                TOPUPMAXPC=1
                OUTPUTFLAGS="${OUTPUTFLAGS}k"
            fi
        fi
    fi
fi

# Final overriding boost disable if set.
if [ "true" = "$BOOSTDISABLED" ]; then
    OUTPUTFLAGS="${OUTPUTFLAGS}N"
    TOPUPMAXPC=0
fi


# START OUTPUT GENERATION (txt, HTML, log)...
TMPFILE=${OUTFILEBASE}.$$.tmp
# Trap unexpected exits with tidyup.
trap "/bin/rm -f ${TMPFILE}; exit 1" 1 2 15


# ASCII plain-text file generation
##################################
# Contains single decimal number in range [0-100] for simplicity in parsing.
# Eg, entire file content might be the two characters "50".
if [ "false" = "$dryrun" ]; then
    echo "$TOPUPMAXPC" > "$TMPFILE"
    # Atomically move into place.
    # TODO: consider reducing filesystem traffic if value unchanged...
    /bin/chmod -f 644 "$TMPFILE"
    /bin/mv -f "$TMPFILE" "$TXTOUTFILE"
    /bin/chmod a+r,a-wx "$TXTOUTFILE"
fi

# LOG GENERATION
################
# Appends record to log file for today.
LOGRECORD="$ISOUTCDATETIME MT ${TOPUPMAXPC:--} GI ${GRIDINTENSITY:--} IT ${MAXTUGRIDINTENS:--} F ${OUTPUTFLAGS:--} ES ${Dstatus:--}"
# Format / example record:
#     2022-02-23T19:08Z MT 0 GI - IT 50 F rbvx ES -
# ie:
#   * UTC-timestamp
#   * "MT%" max-top-up-percentage
#   * "GT" grid-intensity-gCO2perkWh-or-dash
#   * "IT" intensity-threshold
#   * "F" status-flags-or-dash
#   * "ES" eddi-status-or-dash
if [ "false" = "$dryrun" ]; then
    if [ -f "$LOGFILE" ]; then /bin/chmod u+w "$LOGFILE"; fi
    echo "$LOGRECORD" >> "$LOGFILE"
    /bin/chmod a+r,o-wx "$LOGFILE"
else
    echo INFO: log record 1>&2
    echo "$LOGRECORD"
fi


# HTML GENERATION
#################
# Generates HTML5 (not necessarily XHMTL).

# Could put back auto-refresh on this...
OTHERMETA=

# Generate HTML header in simplified form...
# Title of this page.
TITLE="Heat Battery Grid Top-up Maximum"
TITLEFLAG=""
if [ "true" = "$UNUSED" ]; then
    TITLEFLAG="!! "
fi
# Series title text.
SERIESTITLE="Earth Notes"
# Canonical URL.
CANONURL=https://www.earth.org.uk/${OUTFILEBASE}.html 
# Home link to the index page.
rm -f $TMPFILE
cat >> $TMPFILE << EOHEADER
<!DOCTYPE html>
<html lang=en>
<head>
<meta charset=utf-8>
<meta name=viewport content="width=device-width,initial-scale=1,minimum-scale=1">$OTHERMETA
<title>$TITLEFLAG$TITLE - $SERIESTITLE</title>
<meta property=og:title content="$TITLEFLAG$TITLE - $SERIESTITLE">
<meta property=og:description name=description content="Heat battery target and stats for 16WW.">
<meta name=twitter:card content=summary_large_image><meta name=twitter:site content=@EarthOrgUK><meta property=og:image content=https://www.earth.org.uk/img/2022/20220205-heatbatterytarget.png>
<link href=$CANONURL rel=canonical>
<style>@media print{.noprint{display:none!important}}</style>
</head>
<body>
<div itemscope itemtype=http://schema.org/WebPage>
<h1 itemprop="headline name" style="font-family:sans-serif;margin:0;padding:0"><span style="color:green">$SERIESTITLE</span>: $TITLE</h1>
<p>(See the <a href="//www.earth.org.uk/data/heatBattery/16WWDHW/">control parameters</a>, <a href="//www.earth.org.uk/data/heatBattery/log/">logs</a>, <a href="//www.earth.org.uk/heat-battery-topup-dataset.html">dataset</a> and <a href="note-on-solar-DHW-for-16WW-UniQ-and-PV-diversion.html">backstory</a>.)</p>
<p itemprop=description>This page shows the maximum percent-full to top-up to from the grid, if any, for the DHW heat battery.</p>
EOHEADER

#if [ 0 = "$TOPUPMAXPC" ]; then
#    echo "<p><strong>NO HEAT BATTERY TOP-UP FROM GRID CURRENTLY</strong>.</p>" >> "$TMPFILE"
#fi

# Collect energy input for logging last as may be fragile.
# May end up capturing a tiny bit of a current boost's energy.
ET="$(sh script/myenergi/eddiDaySummary.sh -kWhToday)"
ETDETAIL="..."
if [ "0" != "$ET" ]; then
    ETDETAIL="$(sh script/myenergi/eddiDaySummary.sh | awk -F, '{printf("DHW h1d=%.1f h1b=%.1f, heat battery h2d=%.1f h2b=%.1f",$3,$4,$7,$8)}')"
fi
echo "<p>Diversion plus boost today: ${ET}kWh (diversion now ${DDivW}W, ${ETDETAIL})</p>" >> "$TMPFILE"

echo "<p>Heat battery control log entry: <code>$LOGRECORD</code></p>" >> "$TMPFILE"
echo "<p>Putative current raw top-up level (when allowed): ${PTOPUPMAXPC}% (normal min ${TOPUPMINPCMAX}%, max ${TOPUPMAXPCMAX}%).<p>" >> "$TMPFILE"
case "$Dhpri" in
  1) echo "<li>Eddi output priority ${Dhpri}: <strong>DHW tank, for pasteurisation</strong>.</li>";;
  2) echo "<li>Eddi output priority ${Dhpri}: Thermino heat battery.</li>";;
esac >> "$TMPFILE"
case "$Dstatus" in
  6) echo "<li>Eddi state ${Dstatus}: stopped (low grid frequency or diversion prevention).</li>";;
  5) echo "<li>Eddi state ${Dstatus}: maximum temperature reached (heat battery full).</li>";;
  4) echo "<li>Eddi state ${Dstatus}: boost (grid to DHW).</li>";;
  3) echo "<li>Eddi state ${Dstatus}: divert (PV to DHW).</li>";;
esac >> "$TMPFILE"
echo "<p>Flags/status:</p><ul>" >> "$TMPFILE"
echo "<li>[MT] <meter min=0 max=100 value=$TOPUPMAXPC title=$TOPUPMAXPC%>$TOPUPMAXPC%</meter> heat battery Maximum Top-up level: $TOPUPMAXPC%</li>" >> "$TMPFILE"
if [ "" = "$GRIDINTENSITY" ]; then
    echo "<li>[GI] Grid Intensity currently unknown.</li>" >> "$TMPFILE"
else
    echo "<li>[GI] Grid Intensity gCO2/kWh: $GRIDINTENSITY (7d mean ${GRIDMEANINTENSITY:-unknown})</li>" >> "$TMPFILE"
fi
echo "<li>[IT] Grid gCO2/kWh Intensity Threshold below which discretionary top-up is allowed: $MAXTUGRIDINTENS.</li>" >> "$TMPFILE"
echo "<li>[F ${EXPGRIDDEMAND}] expected grid demand from High (peak) / - (normal) / <!-- Medium (off-peak) / --> Low (off-peak).</li>" >> "$TMPFILE"
if [ "" != "$DailyMinPCApplied" ]; then
    echo "<li>[F D] Daily minimum top-up value (${DailyMinPCApplied}%) applicable.</li>" >> "$TMPFILE"
fi
if [ "true" = "$DSTOPPED" ]; then
    echo "<li>[F S] grid response; Stopped boost/divert.</li>" >> "$TMPFILE"
fi
if [ -e $FORECASTGOOD ]; then
    echo "<li>[F F] good Forecast PV generation tomorrow.</li>" >> "$TMPFILE"
fi
if [ "true" = "$GRIDSUPERGREEN" ]; then
    echo "<li>[F G] grid is super-Green (low) intensity and no draw-down from storage.</li>" >> "$TMPFILE"
fi
if [ "true" = "$GRIDRED" ]; then
    echo "<li>[F R] grid is Red (high) intensity, or unknown [r].</li>" >> "$TMPFILE"
fi
if [ "false" != "$WINTER" ]; then
    echo "<li>[F W] solar generation Winter minimum.</li>" >> "$TMPFILE"
fi
if [ "false" != "$PASTEURPENDING" ]; then
    echo "<li>[F w] waiting for good grid conditions to run DHW cylinder sterilisation/pasteurisation.</li>" >> "$TMPFILE"
fi
if [ "false" != "$PASTEURRUN" ]; then
    echo "<li>[F s] sterilisation/pasteurisation in progress.</li>" >> "$TMPFILE"
fi
if [ "true" = "$HEATBATTERYNF" ]; then
    echo "<li>[F T] heat battery nearly full or full.</li>" >> "$TMPFILE"
fi
if [ "false" != "$POLICYMAINDHW" ]; then
    echo "<li>Heat battery set as primary DHW source.</li>" >> "$TMPFILE"
fi
# Show which days heat battery was full/not.
TANKSFULL=""
for f in ${TUMODLOGS-/dev/null};
    do
    SYM="n"; if [ "" != "$(sh script/checkForDHWHBFull.sh<$f)" ]; then SYM="F"; fi
    TANKSFULL="$TANKSFULL$SYM"
    done
echo "<li>Recent ($TUMODNOTFULLCOUNT/$TUMODLOGCOUNT) full days excluding today when heat battery did not fill [$TANKSFULL]: ${TUMODNOTFULLPC}%</li>" >> "$TMPFILE"
# Show recent pasteurisation activity (DHW tank Hot).
DHWTANKHOT=""
for f in ${PASTEURLOGS-/dev/null};
    do
    SYM="-"; if [ "" != "$(sh script/checkForDHWHot.sh < "$f")" ]; then SYM="H"; fi
    DHWTANKHOT="$DHWTANKHOT$SYM"
    done
echo "<li>Recent DHW cylinder pasteurisation days including today, tank maximally Hot [$DHWTANKHOT].</li>" >> "$TMPFILE"
if [ -s "$DLOGFRFILE" ]; then
    FRPC="?"
    DLOGS6="$(egrep -c '"sta":[6]' "$DLOGFILE")"
    DLOGS346="$(egrep -c '"sta":[346]' "$DLOGFILE")"
    if [ "" != "$DLOGS346" ] && [ "0" != "$DLOGS346" ]; then
        FRPC="$(expr \( 100 \* "$DLOGS6" \) / "$DLOGS346")"
    fi
    echo "<li>Grid frequency response minutes today (~${FRPC}% of active): $(wc -l < "$DLOGFRFILE")</li>" >> "$TMPFILE"
fi
echo "</ul>" >> "$TMPFILE"

if [ -s out/hourly/heatBatTarget.png ]; then
    echo "<figure>" >> "$TMPFILE"
    echo '<img src=out/hourly/heatBatTarget.png width=800 height=200 style="max-width:100%;height:auto" alt="last 24/48h" title="last 24/48h">' >> "$TMPFILE"
    echo "<figcaption>Previous day and today: heat battery max % fill/target from grid, grid intensity, intensity threshold, eddi state (black=stopped=60, red=full=50, yellow=boost=40, green=divert=30).</figcaption>" >> "$TMPFILE"
    echo "</figure>" >> "$TMPFILE"
fi

echo "<p>External control tables:</p><ul>" >> "$TMPFILE"
echo "<li>Expected <a href=$CONTROLPARAMSDIR/storage-charge-pref-by-hour-local-time.csv>grid demand by local hour of day</a> [CSV].</li>" >> "$TMPFILE"
echo "<li>Grid <a href=$CONTROLPARAMSDIR/intensity-threshold-by-HML.csv>intensity limit by demand level</a> [CSV].</li>" >> "$TMPFILE"
echo "<li><a href=$CONTROLPARAMSDIR/min-fill-by-dow.csv>Minimum (low-carbon) top-up by day of week</a> [CSV].</li>" >> "$TMPFILE"
echo "</ul>" >> "$TMPFILE"

# Wrap up the HTML page.
cat >> "$TMPFILE" << FOOTER
<footer><p><small>
This page was automatically generated <time itemprop=dateModified>$ISOUTCDATETIME</time>.<br />
First published <time itemprop=datePublished>2022-01-29</time>.<br />
Copyright &copy; <a href="http://d.hd.org/"><span itemprop=author>Damon Hart-Davis</span></a> <span itemprop=copyrightYear>2022</span> to <time>$(cat .work/copyrightYearLatest.txt)</time>. [<a href="/">home</a>]<br />
</small></p></footer>
</div></body></html>
FOOTER

if [ "false" = "$dryrun" ]; then
    # Atomically move into place.
    /bin/chmod -f 644 $TMPFILE
    /bin/mv -f "$TMPFILE" "$HTMLOUTFILE"
    /bin/chmod a+r,a-wx "$HTMLOUTFILE"
else
    #more "$TMPFILE"
    rm -f "$TMPFILE"
fi


# Pasteurisation, as late as possible and slightly randomised start, if needed.
# Boost heater 1 (DHW cyclinder) directly.
# Boost for slightly longer than the interval between script runs.
if [ "true" = "$PASTEURRUN" ]; then
    if [ "false" = "$dryrun" ]; then
        sleep "$(awk 'BEGIN{srand(); print int(33*rand());}')"
        sh script/myenergi/eddiBoost-netrc.sh 11 1
    fi
fi


exit 0


# NOTE: interaction with Enphase AC Battery (ACB).
#   * A general aim is to avoid taking energy from the ACB
#     that has already taken some storage losses,
#     and load it into the heat battery where it will leak faster,
#     and maybe empty the ACB forcing imports eg until after dawn.
#     (However a very-nearly-empty ACB need not prevent heat top-up.)
#   * A non-empty ACB overnight is taken as an indication of good solar PV
#     generation the previous day, and a predictor of likely decent PV
#     the next day allowing direct heat battery fill by diversion.
#     Ie if we have a fullish ACB we probably shouldn't top-up the heat battery.

# NOTE: meaning of flags (in F field)
#   1 Eddi heater 1 (DHW cylinder) has priority for diversion.
#   2 Eddi heater 2 (Thermino heat battery) has priority for diversion.
#   B AC-coupled Batteries not (nearly) empty
#   b AC-coupled Batteries state unknown
#   C Thermal store (heat battery) not cold
#   D daily minimum % fill applied to calculations
#   F good PV generation Forecast tomorrow
#   G grid is super-Green
#   H expected High/peak grid demand
#   I Importing from grid significantly (reserved)
#   i Importing from grid unknown (reserved)
#   k Keep warm and allow dynamic response
#   L expected Low/off-peak grid demand
#   M expected Medium/off-peak grid demand
#   N no boost permitted (ie boost is disabled)
#   P Predicted intensity to rise significantly (>2x) over next days
#   p Predicted intensity not forecast to rise significantly.
#   Q intensity fallen significantly (>2x) compared to last 7d.
#   q intensity not fallen significantly (>2x) compared to last 7d.
#   R grid Red intensity flag
#   r grid Red intensity assumed as state unknown
#   S eddi Stopped due to low grid frequency or otherwise
#   s forcing sterilisation cycle (even if general boost not allowed).
#   T Thermal store (heat battery) nearly full or full.
#   V grid Voltage sag
#   v grid Voltage sag unknown
#   W Winter PV gen minimum (Nov/Dec/Jan)
#   w waiting for good grid conditions to run pasteurisation cycle.
#   X eXporting to grid significantly
#   x eXporting to grid unknown


#
# TODO: let non-empty ACB inhibit Thermino top-up if intensity due to rise much.
# TODO: modulate 'minimum' a little based on grid intensity vs target and RAG.
# TODO: if battery actually at ~100% then don't inhibit topup.
#
# TODO: allow stand-alone mode (ie combi broken or not present):
#   * There is no backup source of DHW: heat battery must never get empty.
#   * Trivial but bad solution would be to keep full from mains 24x7.
#   * Better may be to set a minimum % (lower in H/red grid times).
#   * Better still may be to push minimum up to cover predicted demand.
#   * This should still let PV diversion do as much as possible in summer.
# TODO: aggressive carbon-saving mode, eg top-up even from battery <<'M' GI.
#
# TODO: allow use of yesterday's Enphase log until 00:30-ish.
# TODO: increase % if 'L' hour and no sun forecast and intensity << threshold.

# NOTES for lower-level controller:
# Randomly reduce power at on transitions and close to target, ~1--4 mins in 5.
#   * Should slightly randomly delay on transitions.
#   * Should taper heating power as target is reached.


# CHANGELOG (partial)
# 2026-04-12: computing 'P' in all non-R/H times and exporting a flag.
# 2026-02-23: adding Thermino 'not-cold' handling (flag 'C').
# 2025-05-26: switch Eddi div pri to 1 (cylinder) at 8d since last at max temp.
# 2025-05-11: added '1' and '2' flags for Eddi heater priority.
# 2025-05-11: Eddi heater 2 (Thermino) priority when pasterisation not due.
# 2025-03-09: extended PASTEURDAYSMIN from 9 to 15 for non-diversion.
# 2024-12-08: added 'w' to indicate waiting for super-green to sterilise.
# 2024-12-05: starting support for DHW tank sterilisation cycle 's'.
# 2024-11-27: computing IT as fraction of rolling 7d mean.
# 2024-11-27: loading soft parameters from data/consolidated/softparams.txt now.
# 2024-11-24: minimising calls to NESO API and thus P/p/Q/q flag computation.
# 2024-11-12: BOOSTDISABLED ('N' flag) added; set ready for heat pump works.
# 2024-09-04: 'Q'(/'q') flag for when significant intensity fall seen.
# 2024-09-03: 'P'(/'p') flag for when significant intensity rise predicted.
# 2023-11-07: when GI below 'L' threshold always allow minimal (to 1%) top-up.
# 2023-10-26: reduce MT when intensity > 'L' level (190 if not set).
# 2023-07-28: improvement on Thermino side to top-up reluctantly when MT<50%.
# 2023-07-02: bugfix: day-of-week fill minimum is now for local timezone.
# 2023-01-29: dropped ERMSVMIN from 244 to 242 observing transients this am.
# 2023-01-29: raised TOPUPMAXPCMAX from 95 to 100; allows more LC grid capture.
# 2023-01-21: when not peak/H and not R and below 'L', 'k'eep warm %-fill > 0.
# 2022-11-10: use 7d mean intensity (capped by 'L') threshold for 'normal'.
# 2022-11-06: switch from 24h to 7d versions of grid red/supergreen flags.
# 2022-11-05: raising GI threshold a little when green, lowering when red.
# 2022-10-15: ERMSVMIN now <244V to allow a little sag from Thermino itself.
# 2022-10-03: more gradual ramp up of MT target %-fill value.
# 2022-09-27: less 'R'/'H' reduction in % - less likely to cause annoyance.
# 2022-09-26: when near to 'H' predicted demand, drop target a little.
# 2022-09-24: ignore intensity for d-o-w target low period when POLICYMAINDHW.
# 2022-09-23: when POLICYMAINDHW="true" maintain top-up when some info missing.
# 2022-09-23: min/max thresholds adjusted (upwards).
# 2022-09-23: emergency flip to POLICYMAINDHW="true" as combi playing up!
# 2022-06-25: potentially allow small top-up (eg to 1%) even if full every day.
# 2022-05-23: grid low voltage thresh ERMSVMIN now <245V, and effects broadened.
# 2022-04-21: ignore current-day log in counting days where battery filled.
# 2022-04-20: 'D' flag assigned to show daily nominal minimum being applied.
# 2022-04-18: allow minimum "L"-period top-up by day of week, eg for bath day!
# 2022-04-07: allow higher top-up in practice by re-arranging calculations.
# 2022-04-04: removing B flag when battery inhibition removed by TUMOD.
# 2022-04-02: indicating in HTML page what top-up would be, when allowed.
# 2022-03-15: with enough TUMODLOGCOUNT then ACB status need not inhibit topup.
# 2022-03-14: multi-day feedback to leave enough space for most diversion.
# 2022-03-09: shortened stale log file limit for Enphase and Eddi to 20 minutes.
# 2022-03-07: top-up higher if Nov/Dec/Jan.
# 2022-03-07: trimming top-up % boost when int threshold about to fall.
# 2022-02-23: added ES eddi-status field to end of log line.
# 2022-02-20: allowing higher top-up the further GI is below IT.
# 2022-02-20: removed downward IT dither complexity.
# 2022-02-15: reduced amount by which MT is reduced eg for AC Battery non-empty.
# 2022-02-14: making script testable (and creating unit-test script alongside).
# 2022-02-06: check that both intensity and battery stats are not stale.
# 2022-02-04: allow system to possibly top-up even if no grid intensity data.
# 2022-02-04: allow system to top-up in 'L' hours even if Enphase data absent.
# 2022-02-02: changed poor-forecast uplift in top-up max % from +5% abs to *1.2.
# 2022-02-02: added 'H'/'-'/'M'/'L' grid demand flags now shown explicitly.
# 2022-02-02: added 'R' grid red intensity flag.
# 2022-02-02: added 'G' super-green grid intensity flag.
# ...
# 2022-01-29: created


# VERSION
# IDINTENSd: heat-battery-target.sh 48495 2022-11-05 13:42:01Z dhd $ #
